- ))}
+ {/* ── Memory Modal ── */}
+ {showMemoryModal && (
+
+ {/* Scrollable message area */}
+
+
+ {chatId === null && messages.length === 0 && (
+
+
+
⚡
+
Local-first AI workspace
+
Start with a prompt that moves the project forward.
+
+ ForgeLM keeps your chats, code blocks, and runtime controls in one place so you can inspect,
+ iterate, and debug without losing context.
+
+
+ {starterPrompts.map((prompt) => (
+
+ ))}
+
+ {composerNotice && (
+
+
+
{composerNotice.title}
+
{composerNotice.description}
+
+ {onOpenSidebar && (
+
+ )}
+
+ )}
+
+
+ )}
+
+ {messages.map((msg) => (
+
+ ))}
+
+ {/* Live streaming bubble */}
+ {generating && (streamReasoning || streamContent || streamThinkingOpen) && (
+
+
+ {(streamReasoning || streamThinkingOpen) && (
+
setShowLiveReasoning(event.currentTarget.open)}
+ >
+
+
+
+ Thinking{streamThinkingOpen ? "..." : ""}
+
+ {streamReasoning && (
+
+ )}
+
+ )}
+
+ {streamContent && (
+
+ )}
+
+ {!streamContent && streamThinkingOpen && (
+
+
+
+ )}
+
+
+ )}
+
+ {generating && !streamReasoning && !streamContent && !streamThinkingOpen && (
+
+ )}
+
+
+
+
+
+
+ {/* ── Message Queue indicator ── */}
+ {messageQueue.length > 0 && (
+
+
+
+ {messageQueue.length} message{messageQueue.length > 1 ? "s" : ""} queued
+
+
+
+ )}
+
+ {/* ── Fixed Input ── */}
+
+
+
+
{composerNotice && (
@@ -407,107 +608,107 @@ export default function ChatView({ db, chatId, server, apiKey, model, onChatCrea
)}
)}
-
-
- )}
- {messages.map((msg) => (
-
- ))}
+ {/* Model capabilities — floating above the pill */}
+
+
+ In
+
+
+
+ ·
+
+ Out
+
+
+
- {/* Live streaming bubble */}
- {generating && (streamReasoning || streamContent || streamThinkingOpen) && (
-
- {(streamReasoning || streamThinkingOpen) && (
-
-
- {showLiveReasoning && streamReasoning && (
-
+ {/* The typing pill */}
+
- )}
- {streamContent && (
-
- )}
-
- {!streamContent && streamThinkingOpen && (
-
-
-
- )}
-
- )}
-
- {generating && !streamReasoning && !streamContent && !streamThinkingOpen && (
-
-
-
+ {/* Queue system */}
+ {messageQueue.length > 0 && (
+
+ {isQueueOpen && (
+
+
+ Mensagens em espera
+
+
+ {messageQueue.map((msg, index) => (
+ -
+ {msg}
+
+
+ ))}
+
+
+ )}
+
+
+ )}
- )}
-
-
-
-
- {/* Input */}
-
- {composerNotice && (
-
-
-
{composerNotice.title}
-
{composerNotice.description}
-
- {onOpenSidebar && (
-
- )}
-
- )}
-
-
-
- {input.length} chars
diff --git a/tauri-app/src/components/MessageBubble.css b/tauri-app/src/components/MessageBubble.css
index 78a1ffe..675a568 100644
--- a/tauri-app/src/components/MessageBubble.css
+++ b/tauri-app/src/components/MessageBubble.css
@@ -1,7 +1,7 @@
-/* Edit mode */
+/* ── Edit mode ── */
.msg-edit-container {
width: 100%;
- max-width: 780px;
+ max-width: 800px;
}
.msg-edit-textarea {
@@ -12,17 +12,19 @@
color: var(--text);
font-size: 0.9rem;
font-family: inherit;
- padding: 10px 14px;
+ padding: 11px 14px;
resize: none;
min-height: 80px;
outline: none;
line-height: 1.6;
+ box-shadow: 0 0 0 3px rgba(72,166,255,0.1);
}
.msg-edit-actions {
display: flex;
gap: 6px;
margin-top: 8px;
+ flex-wrap: wrap;
}
.btn-primary {
@@ -31,13 +33,18 @@
border: none;
border-radius: var(--radius-sm);
padding: 6px 14px;
+ min-height: 44px;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
font-family: inherit;
- transition: background 0.15s;
+ transition: background 0.15s, box-shadow 0.15s, transform 0.15s;
}
.btn-primary:hover { background: var(--accent2); }
+.btn-primary:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(72, 166, 255, 0.22);
+}
.btn-secondary {
background: var(--bg4);
@@ -45,108 +52,286 @@
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 6px 14px;
+ min-height: 44px;
font-size: 0.8rem;
cursor: pointer;
font-family: inherit;
- transition: background 0.15s;
+ transition: background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s;
}
.btn-secondary:hover { background: var(--bg5); color: var(--text); }
+.btn-secondary:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(72, 166, 255, 0.18);
+}
.btn-ghost {
- background: none;
- color: var(--text3);
- border: none;
+ background: transparent;
+ color: var(--text2);
+ border: 1px solid transparent;
border-radius: var(--radius-sm);
padding: 6px 10px;
+ min-height: 44px;
font-size: 0.8rem;
cursor: pointer;
font-family: inherit;
- transition: color 0.15s;
+ transition: color 0.15s, background 0.15s, border-color 0.15s;
}
-.btn-ghost:hover { color: var(--text2); }
-
-/* Message actions */
-.reasoning-panel {
- width: 100%;
- margin-bottom: 8px;
+.btn-ghost:hover { color: var(--text); background: rgba(237, 245, 255, 0.05); border-color: rgba(93, 114, 136, 0.28); }
+.btn-ghost:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(72, 166, 255, 0.18);
}
-.reasoning-toggle {
- display: inline-flex;
+/* ── Thinking inside bubble ── */
+.msg-thinking {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+ background: transparent;
+}
+
+.msg-thinking > summary {
+ list-style: none;
+}
+
+.msg-thinking > summary::-webkit-details-marker {
+ display: none;
+}
+
+.thinking-summary {
+ display: flex;
align-items: center;
gap: 8px;
- background: rgba(124, 158, 248, 0.08);
- color: var(--text2);
- border: 1px solid var(--border);
- border-radius: 999px;
- padding: 6px 12px;
- font-size: 0.75rem;
+ padding: 10px 16px;
cursor: pointer;
- font-family: inherit;
+ font-size: 0.75rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: rgba(255, 255, 255, 0.5);
+ user-select: none;
+ transition: color 0.2s ease-out;
}
-.reasoning-toggle:hover {
- background: rgba(124, 158, 248, 0.12);
- color: var(--text);
+.thinking-summary:hover {
+ color: rgba(255, 255, 255, 0.8);
}
-.reasoning-content {
- margin-top: 8px;
- padding: 12px 14px;
- background: rgba(124, 158, 248, 0.06);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- color: var(--text2);
- font-size: 0.84rem;
+.thinking-summary:focus-visible {
+ outline: none;
+ box-shadow: inset 0 0 0 2px rgba(72, 166, 255, 0.3);
+}
+
+.thinking-chevron {
+ width: 14px;
+ height: 14px;
+ stroke: currentColor;
+ stroke-width: 1.8;
+ fill: none;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ transition: transform 0.2s ease-out;
+ flex-shrink: 0;
+}
+
+.msg-thinking[open] .thinking-chevron {
+ transform: rotate(90deg);
+}
+
+.thinking-content {
+ padding: 4px 16px 16px 16px;
+ font-size: 0.875rem;
+ color: rgba(255, 255, 255, 0.6);
line-height: 1.6;
+ font-style: normal;
}
+.thinking-content > :first-child {
+ border-left: 2px solid rgba(255, 255, 255, 0.1);
+ margin-left: 16px;
+ padding-left: 12px;
+}
+
+/* ── Message bubble layout ── */
+.msg-bubble {
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ overflow: hidden;
+}
+
+.msg-content {
+ padding: 13px 16px;
+}
+
+/* ── Message metrics ── */
.message-metrics {
display: flex;
- flex-wrap: wrap;
+ align-items: center;
gap: 8px;
- margin-top: 8px;
- color: var(--text3);
- font-size: 0.72rem;
- font-variant-numeric: tabular-nums;
+ padding-left: 12px;
+ font-size: 0.75rem;
+ opacity: 0.7;
+ font-family: monospace;
+ color: #a8b8cc;
}
-.message-metrics span {
- background: var(--bg3);
- border: 1px solid var(--border);
- border-radius: 999px;
- padding: 3px 8px;
+/* ── Message footer ── */
+.msg-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: stretch;
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
+ background: rgba(0, 0, 0, 0.12);
+ min-height: 32px;
}
+/* ── Compact actions for user bubble (floating bar) ── */
+.msg-wrapper.user {
+ position: relative;
+ margin-bottom: 24px;
+}
+
+.compact-actions {
+ position: absolute;
+ bottom: -12px;
+ right: 0;
+ display: flex;
+ gap: 4px;
+ padding: 4px;
+ border-radius: 8px;
+ background: var(--bg2);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ opacity: 0;
+ pointer-events: none;
+ transform: translateY(-4px);
+ transition: all 0.2s ease-out;
+ z-index: 10;
+}
+
+.msg-wrapper.user:hover .compact-actions,
+.msg-wrapper.user:focus-within .compact-actions {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateY(0);
+}
+
+.compact-actions .action-btn {
+ min-height: 28px;
+ min-width: 28px;
+ width: 28px;
+ height: 28px;
+ padding: 0;
+ background: transparent;
+ border: none;
+ border-radius: 4px;
+ color: #c8d7ea;
+}
+.compact-actions .action-btn svg {
+ width: 14px;
+ height: 14px;
+}
+.compact-actions .action-btn:hover {
+ background: rgba(255, 255, 255, 0.08);
+ color: var(--text);
+}
+.compact-actions .action-btn:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(72, 166, 255, 0.4);
+ opacity: 1;
+}
+.compact-actions .action-btn.copied {
+ color: var(--success);
+ background: rgba(48, 209, 88, 0.12);
+}
+.compact-actions .action-btn.danger {
+ color: #f4c2c2;
+}
+.compact-actions .action-btn.danger:hover {
+ background: rgba(240, 64, 64, 0.12);
+ color: #ffd7d7;
+}
+
+/* ── Message actions (assistant) ── */
.msg-actions {
display: flex;
- flex-wrap: wrap;
- gap: 4px;
- margin-top: 5px;
opacity: 0;
- transition: opacity 0.15s;
+ transition: opacity 0.2s ease-out;
+}
+
+.msg-bubble:hover .msg-actions,
+.msg-bubble:focus-within .msg-actions {
+ opacity: 1;
}
-.msg-wrapper:hover .msg-actions { opacity: 1; }
-.msg-wrapper:focus-within .msg-actions { opacity: 1; }
.action-btn {
- background: none;
- border: 1px solid var(--border);
- color: var(--text2);
- border-radius: var(--radius-sm);
- padding: 3px 9px;
- font-size: 0.72rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 32px;
+ min-width: 36px;
+ background: transparent;
+ border: none;
+ border-left: 1px solid rgba(255, 255, 255, 0.08);
+ color: #c8d7ea;
+ border-radius: 0;
+ padding: 0 12px;
cursor: pointer;
- transition: all 0.15s;
+ transition: background 0.2s ease-out, color 0.2s ease-out;
font-family: inherit;
- white-space: nowrap;
}
-.action-btn:hover { background: var(--bg3); color: var(--text); border-color: var(--border2); }
-.action-btn.copied { color: var(--success); border-color: var(--success); }
-.action-btn.danger:hover { background: rgba(240,64,64,0.1); color: var(--danger); border-color: var(--danger); }
+.action-btn:first-child {
+ border-left: none;
+}
+.action-btn svg {
+ width: 16px;
+ height: 16px;
+ stroke: currentColor;
+ stroke-width: 1.5;
+ fill: none;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+.action-btn:hover {
+ background: rgba(255, 255, 255, 0.06);
+ color: var(--text);
+}
+.action-btn:focus-visible {
+ outline: none;
+ box-shadow: inset 0 0 0 2px rgba(72, 166, 255, 0.3);
+ opacity: 1;
+}
+.action-btn.copied {
+ color: var(--success);
+ background: rgba(48, 209, 88, 0.08);
+}
+.action-btn.danger {
+ color: #f4c2c2;
+}
+.action-btn.danger:hover {
+ background: rgba(240, 64, 64, 0.1);
+ color: #ffd7d7;
+}
@media (hover: none), (max-width: 960px) {
+ .msg-actions { opacity: 1; }
+}
+
+@media (max-width: 640px) {
.msg-actions {
- opacity: 1;
+ width: 100%;
+ }
+
+ .action-btn {
+ flex: 1 1 132px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .action-btn,
+ .btn-primary,
+ .btn-secondary,
+ .btn-ghost {
+ animation: none;
+ transition: none;
}
}
diff --git a/tauri-app/src/components/MessageBubble.test.tsx b/tauri-app/src/components/MessageBubble.test.tsx
new file mode 100644
index 0000000..5eb121a
--- /dev/null
+++ b/tauri-app/src/components/MessageBubble.test.tsx
@@ -0,0 +1,72 @@
+/* @vitest-environment jsdom */
+
+import { createRoot, type Root } from "react-dom/client";
+import { act } from "react-dom/test-utils";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import MessageBubble from "./MessageBubble";
+import type { Message } from "../lib/types";
+
+describe("MessageBubble", () => {
+ let container: HTMLDivElement;
+ let root: Root;
+
+ beforeEach(() => {
+ // @ts-expect-error React test environment flag
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = createRoot(container);
+ Object.assign(navigator, {
+ clipboard: {
+ writeText: vi.fn(() => Promise.resolve()),
+ },
+ });
+ });
+
+ afterEach(() => {
+ act(() => root.unmount());
+ container.remove();
+ vi.restoreAllMocks();
+ // @ts-expect-error React test environment flag
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
+ });
+
+ it("renders accessible thinking and action controls for assistant messages", async () => {
+ const message: Message = {
+ id: 12,
+ chat_id: 4,
+ role: "assistant",
+ content: "Final answer",
+ reasoning_content: "Reasoning details",
+ prompt_tokens: 18,
+ completion_tokens: 24,
+ total_tokens: 42,
+ tokens_per_second: 6.4,
+ created_at: Date.now(),
+ };
+
+ await act(async () => {
+ root.render(
+
`${text}
`}
+ />,
+ );
+ });
+
+ const summary = container.querySelector("summary.reasoning-toggle");
+ expect(summary?.getAttribute("aria-label")).toBe("Toggle thinking details");
+
+ const buttons = Array.from(container.querySelectorAll("button"));
+ expect(buttons.some((node) => node.getAttribute("aria-label") === "Copy message")).toBe(true);
+ expect(buttons.some((node) => node.getAttribute("aria-label") === "Edit message")).toBe(true);
+ expect(buttons.some((node) => node.getAttribute("aria-label") === "Delete message")).toBe(true);
+
+ expect(container.textContent).toContain("Sent 18");
+ expect(container.textContent).toContain("Generated 24");
+ expect(container.textContent).toContain("Total 42");
+ expect(container.textContent).toContain("6.4 tok/s");
+ });
+});
diff --git a/tauri-app/src/components/MessageBubble.tsx b/tauri-app/src/components/MessageBubble.tsx
index 7d768c0..20f56a9 100644
--- a/tauri-app/src/components/MessageBubble.tsx
+++ b/tauri-app/src/components/MessageBubble.tsx
@@ -14,10 +14,9 @@ export default function MessageBubble({ message, onDelete, onEditAndResend, onEd
const [editing, setEditing] = useState(false);
const [editValue, setEditValue] = useState(message.content);
const [copied, setCopied] = useState(false);
- const [showReasoning, setShowReasoning] = useState(false);
const textareaRef = useRef(null);
const bubbleRef = useRef(null);
- const reasoningRef = useRef(null);
+ const thinkingRef = useRef(null);
// Wire up copy-code buttons inside rendered markdown
useEffect(() => {
@@ -36,8 +35,8 @@ export default function MessageBubble({ message, onDelete, onEditAndResend, onEd
});
useEffect(() => {
- if (!reasoningRef.current) return;
- const buttons = reasoningRef.current.querySelectorAll(".copy-code-btn");
+ if (!thinkingRef.current) return;
+ const buttons = thinkingRef.current.querySelectorAll(".copy-code-btn");
buttons.forEach((btn) => {
btn.onclick = () => {
const code = btn.getAttribute("data-code") || "";
@@ -90,6 +89,8 @@ export default function MessageBubble({ message, onDelete, onEditAndResend, onEd
(message.total_tokens ?? 0) > 0
);
+ const showFooter = message.role === "assistant";
+
return (
{editing ? (
@@ -118,56 +119,101 @@ export default function MessageBubble({ message, onDelete, onEditAndResend, onEd
) : (
<>
- {hasReasoning && (
-
-
- {showReasoning && (
-
- )}
-
- )}
-
{message.role === "assistant" ? (
- ) : (
- {message.content}
- )}
+ >
+ {hasReasoning && (
+
+
+
+ Thinking
+
+
+
+ )}
- {hasUsage && (
-
-
Sent {message.prompt_tokens ?? 0}
-
Generated {message.completion_tokens ?? 0}
-
Total {message.total_tokens ?? 0}
- {message.tokens_per_second ?
{message.tokens_per_second.toFixed(1)} tok/s : null}
+
+
+ {showFooter && (
+
+ {hasUsage && (
+
+ Sent {message.prompt_tokens ?? 0}
+ Generated {message.completion_tokens ?? 0}
+ Total {message.total_tokens ?? 0}
+ {message.tokens_per_second ? {message.tokens_per_second.toFixed(1)} tok/s : null}
+
+ )}
+ {!hasUsage &&
}
+
+
+
+
+
+
+ )}
+ ) : (
+ <>
+
+
+
+
+
+
+ >
)}
-
-
-
-
-
-
>
)}
diff --git a/tauri-app/src/components/Sidebar.css b/tauri-app/src/components/Sidebar.css
index ecba78a..e0a02ee 100644
--- a/tauri-app/src/components/Sidebar.css
+++ b/tauri-app/src/components/Sidebar.css
@@ -1,9 +1,7 @@
.sidebar {
width: 280px;
min-width: 280px;
- background:
- radial-gradient(circle at top left, rgba(72, 166, 255, 0.12), transparent 26%),
- var(--bg2);
+ background: var(--bg2);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
@@ -13,7 +11,7 @@
.sidebar-header-actions {
display: flex;
align-items: center;
- gap: 6px;
+ gap: 4px;
}
.sidebar-close-btn {
@@ -24,7 +22,7 @@
.sidebar-header {
display: flex;
align-items: center;
- padding: 14px 14px 12px;
+ padding: 16px 16px 12px;
border-bottom: 1px solid var(--border);
gap: 8px;
flex-shrink: 0;
@@ -33,32 +31,15 @@
.sidebar-brand {
display: flex;
align-items: center;
- gap: 9px;
flex: 1;
min-width: 0;
}
-.brand-icon {
- width: 28px;
- height: 28px;
- background: linear-gradient(135deg, #2b88e6, #49c0ff);
- border-radius: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 14px;
- flex-shrink: 0;
- box-shadow: 0 2px 8px rgba(91,106,240,0.35);
-}
-
.brand-name {
- font-size: 15px;
- font-weight: 800;
- letter-spacing: -0.02em;
- background: linear-gradient(135deg, #f5fbff, #89b5d9);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
+ font-size: 14px;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+ color: var(--text);
}
.icon-btn {
@@ -66,16 +47,15 @@
border: none;
color: var(--text3);
cursor: pointer;
- font-size: 15px;
- width: 36px;
- height: 36px;
+ width: 32px;
+ height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
transition: background 0.15s, color 0.15s;
flex-shrink: 0;
- line-height: 1;
+ padding: 0;
}
.icon-btn:hover {
@@ -83,61 +63,16 @@
color: var(--text);
}
-/* ── Settings panel ── */
-.settings-panel {
- padding: 10px 12px 12px;
- border-bottom: 1px solid var(--border);
- display: flex;
- flex-direction: column;
- gap: 5px;
- background: var(--bg3);
- animation: slide-down 0.15s ease;
- flex-shrink: 0;
-}
-
-@keyframes slide-down {
- from { opacity: 0; transform: translateY(-6px); }
- to { opacity: 1; transform: translateY(0); }
-}
-
-.settings-label {
- font-size: 10px;
- font-weight: 700;
- color: var(--text3);
- text-transform: uppercase;
- letter-spacing: 0.06em;
- margin-top: 4px;
-}
-
-.settings-input {
- background: var(--bg4);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- color: var(--text);
- font-size: 12px;
- padding: 7px 10px;
- outline: none;
- transition: border-color 0.15s, background 0.15s;
- font-family: inherit;
- width: 100%;
- box-sizing: border-box;
-}
-
-.settings-input:focus {
- border-color: var(--accent);
- background: var(--bg5);
-}
-
/* ── Model section ── */
.model-section {
- padding: 8px 12px;
+ padding: 10px 16px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.model-section-label {
- font-size: 10px;
- font-weight: 700;
+ font-size: 11px;
+ font-weight: 600;
color: var(--text3);
text-transform: uppercase;
letter-spacing: 0.06em;
@@ -147,14 +82,7 @@
.model-row {
display: flex;
align-items: center;
- gap: 7px;
-}
-
-.model-help {
- margin-top: 8px;
- font-size: 11px;
- color: var(--text3);
- line-height: 1.5;
+ gap: 8px;
}
.status-dot {
@@ -186,56 +114,67 @@
.model-select {
flex: 1;
- background: var(--bg4);
- border: 1px solid var(--border);
+ background: transparent;
+ border: 1px solid transparent;
border-radius: var(--radius-sm);
color: var(--text);
- font-size: 12px;
- padding: 6px 28px 6px 9px;
+ font-size: 13px;
+ padding: 6px 28px 6px 8px;
outline: none;
cursor: pointer;
min-width: 0;
font-family: inherit;
- transition: border-color 0.15s;
+ transition: border-color 0.15s, background 0.15s;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%236b7280'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
}
+.model-select:hover {
+ background-color: var(--bg3);
+ border-color: var(--border);
+}
+
.model-select:focus {
border-color: var(--accent);
+ background-color: var(--bg3);
+}
+
+.model-select:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
}
/* ── Sidebar actions ── */
.sidebar-actions {
- padding: 10px 12px 6px;
+ padding: 10px 16px 6px;
flex-shrink: 0;
}
.new-chat-btn {
width: 100%;
- background: linear-gradient(135deg, #2b88e6, #49c0ff);
- color: #fff;
- border: none;
+ background: var(--bg3);
+ color: var(--text2);
+ border: 1px solid var(--border);
border-radius: var(--radius-sm);
- padding: 9px 12px;
+ padding: 8px 12px;
font-size: 13px;
- font-weight: 700;
+ font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
- justify-content: center;
- gap: 7px;
- transition: opacity 0.15s, transform 0.1s, box-shadow 0.15s;
+ justify-content: flex-start;
+ gap: 8px;
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
font-family: inherit;
- letter-spacing: 0.01em;
- box-shadow: 0 2px 10px rgba(72,166,255,0.25);
+ height: 36px;
}
.new-chat-btn:hover {
- opacity: 0.9;
- box-shadow: 0 4px 16px rgba(72,166,255,0.4);
+ background: var(--bg4);
+ color: var(--text);
+ border-color: var(--border);
}
.new-chat-btn:active {
@@ -244,12 +183,12 @@
/* ── Chat list ── */
.chat-list-section-label {
- font-size: 10px;
- font-weight: 700;
+ font-size: 11px;
+ font-weight: 600;
color: var(--text3);
text-transform: uppercase;
letter-spacing: 0.06em;
- padding: 8px 12px 4px;
+ padding: 12px 16px 4px;
flex-shrink: 0;
}
@@ -279,7 +218,6 @@
color: var(--text2);
transition: background 0.12s, color 0.12s;
position: relative;
- border-left: 2px solid transparent;
}
.chat-item-button {
@@ -287,7 +225,6 @@
min-width: 0;
display: flex;
align-items: center;
- gap: 8px;
padding: 8px 10px;
background: transparent;
border: 0;
@@ -295,6 +232,7 @@
cursor: pointer;
font: inherit;
text-align: left;
+ border-radius: var(--radius-sm);
}
.chat-item:hover {
@@ -308,19 +246,8 @@
}
.chat-item.active {
- background: rgba(91,106,240,0.1);
+ background: var(--bg4);
color: var(--text);
- border-left-color: var(--accent);
-}
-
-.chat-item-icon {
- font-size: 13px;
- flex-shrink: 0;
- opacity: 0.5;
-}
-
-.chat-item.active .chat-item-icon {
- opacity: 0.8;
}
.chat-item-title {
@@ -329,6 +256,7 @@
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
+ line-height: 1.4;
}
.chat-item-del {
@@ -337,7 +265,6 @@
border: none;
color: var(--text3);
cursor: pointer;
- font-size: 12px;
width: 28px;
height: 28px;
display: flex;
@@ -346,8 +273,8 @@
border-radius: 4px;
transition: opacity 0.15s, background 0.15s, color 0.15s;
flex-shrink: 0;
- line-height: 1;
- margin-right: 6px;
+ padding: 0;
+ margin-right: 4px;
}
.chat-item:hover .chat-item-del {
@@ -366,7 +293,7 @@
/* ── Footer ── */
.sidebar-footer {
- padding: 8px 14px;
+ padding: 8px 16px;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
diff --git a/tauri-app/src/components/Sidebar.tsx b/tauri-app/src/components/Sidebar.tsx
index c1479d5..546957a 100644
--- a/tauri-app/src/components/Sidebar.tsx
+++ b/tauri-app/src/components/Sidebar.tsx
@@ -1,7 +1,5 @@
-import { useState } from "react";
import type { Chat, ModelInfo, ServerStatus } from "../lib/types";
-import RuntimeWorkspace from "../features/runtime/components/RuntimeWorkspace";
-import type { RuntimeConfig, RuntimeDraftFieldMeta, RuntimeDraftFieldState, RuntimeSnapshot } from "../features/runtime/types";
+import { Plus, Settings, X, Trash2, RefreshCw } from "lucide-react";
import "./Sidebar.css";
interface Props {
@@ -15,59 +13,33 @@ interface Props {
onModelChange: (v: string) => void;
status: ServerStatus;
onRefreshModels: () => void;
- runtime: {
- snapshot: RuntimeSnapshot;
- savedBaseline: RuntimeConfig;
- draftConfig: RuntimeConfig;
- draftDirty: boolean;
- draftOpen: boolean;
- draftFields: RuntimeDraftFieldState;
- loading: boolean;
- busy: boolean;
- activeLifecycleCommand: RuntimeSnapshot["active_command"];
- lastLifecycleResult: RuntimeSnapshot["last_command_result"];
- lifecycleBusy: boolean;
- applyImpact: "immediate" | "restart_required" | "save_before_apply";
- openDraft: () => void;
- closeDraft: () => void;
- resetDraft: () => void;
- updateDraft: (patch: Partial
) => void;
- updateDraftFieldMeta: (field: keyof RuntimeDraftFieldState & string, patch: Partial) => void;
- saveConfig: (patch: Partial) => Promise;
- startLocal: () => Promise;
- stopLocal: () => Promise;
- };
+ onOpenSettings: () => void;
onCloseMobile: () => void;
}
export default function Sidebar({
chats, currentChatId, onSelectChat, onNewChat, onDeleteChat,
models, selectedModel, onModelChange, status, onRefreshModels,
- runtime,
+ onOpenSettings,
onCloseMobile,
}: Props) {
- const modelCountLabel = models.length === 1 ? "1 model available" : `${models.length} models available`;
-
return (
<>
))}
diff --git a/tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx b/tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
index 91ac067..6542c31 100644
--- a/tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
+++ b/tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
@@ -16,6 +16,7 @@ import {
resolveDraftModelSelection,
toModelFile,
} from "../runtime-settings-form";
+import { CTX_PRESETS, DEFAULT_CTX_SIZE, MIN_CTX_SIZE } from "../runtime-defaults";
import {
createRuntimeDraftFieldState,
type ModelLibraryState,
@@ -31,7 +32,6 @@ import {
} from "../types";
import "./RuntimeSettingsDrawer.css";
-const CTX_PRESETS = [2048, 4096, 8192, 16384, 32768, 65536];
const REMOTE_PRESET_TEMPLATES = [
{ id: "openai", label: "OpenAI", provider: "open_ai", baseUrl: "https://api.openai.com/v1" },
{ id: "openrouter", label: "OpenRouter", provider: "open_router", baseUrl: "https://openrouter.ai/api/v1" },
@@ -799,10 +799,10 @@ export default function RuntimeSettingsDrawer({
updateDraft({ ctx_size: Math.max(512, parseInt(event.target.value, 10) || 4096) })}
+ onChange={(event) => updateDraft({ ctx_size: Math.max(MIN_CTX_SIZE, parseInt(event.target.value, 10) || DEFAULT_CTX_SIZE) })}
type="number"
/>