From 3dfc8d11a639e49dbd087f3a2c8d1fdc4040bde1 Mon Sep 17 00:00:00 2001
From: Sivan
- Reasonix sets DECSET 1007 (alternate-scroll) only — wheel events translate to
- ↑/↓ keypresses for the app, but native click/drag selection is left untouched.
- Pass --no-mouse to opt out entirely.",
+ "In auto/app scroll mode, Reasonix captures mouse-wheel reports so chat history can scroll inside terminals whose native scrollback does not move TUI content. Pass --no-mouse to opt out entirely.",
"cp.title": "Copy / paste",
"cp.body":
@@ -56,12 +56,11 @@
"cp.body.drag":
"In SSH / mosh / tmux, the alt-screen buffer prevents the terminal from extending the selection past the visible viewport — there is no scrollback above the alt-screen to drag into. Two fixes:",
"cp.fix1":
- "/copy — open vim/tmux-style copy mode in-app. Snapshots the current chat to a navigable buffer; y yanks to clipboard via OSC 52 (with a temp-file fallback for terminals that don't support it).",
+ "/copy — copy the latest assistant response through OSC 52. Use /copy all for the full chat or /copy N for the last N serialized chat items. Oversized content falls back to a temp file path.",
"cp.fix2":
"--no-alt-screen — render to shell scrollback instead. Drag-select then works terminal-natively (the chat content is real lines in the scrollback above your cursor). Trade-off: redraw can ghost on resize.",
- "cp.h.copymode": "/copy — copy mode keys",
- "cp.body.osc":
- "y with no active selection yanks just the current line. The yank goes through OSC 52 first (works through SSH, mosh, tmux with set -g set-clipboard on); content larger than 75 KB falls back to a temp file whose path is printed on exit.",
+ "cp.h.copymode": "/copy",
+ "cp.body.osc": "/copy uses OSC 52 first; large content falls back to a temp file.",
};
var zh = {
@@ -105,7 +104,7 @@
"ms.title": "鼠标",
"ms.body":
- "Reasonix 只设置 DECSET 1007(alternate-scroll)——滚轮事件转为 ↑/↓ 按键传给应用,原生点击/拖拽选择不受影响。加 --no-mouse 可完全关闭。",
+ "在 auto/app 滚动模式下,Reasonix 会捕获鼠标滚轮事件,让原生 scrollback 无法滚动 TUI 内容的终端也能滚聊天。加 --no-mouse 可完全关闭。",
"cp.title": "复制 / 粘贴",
"cp.body":
@@ -114,12 +113,11 @@
"cp.body.drag":
"SSH / mosh / tmux 下,alt-screen 缓冲区会阻止终端把选区延伸到可视视口以外——alt-screen 上方根本没有 scrollback 可拖入。两种解决方式:",
"cp.fix1":
- "/copy — 在应用内打开 vim/tmux 风格的复制模式,把当前聊天快照到可导航的缓冲区;y 通过 OSC 52 复制到剪贴板(不支持 OSC 52 的终端会退到临时文件)。",
+ "/copy — 通过 OSC 52 复制最近一条 assistant 回复。用 /copy all 复制完整聊天,或 /copy N 复制最近 N 个序列化聊天项;内容过大时退到临时文件路径。",
"cp.fix2":
"--no-alt-screen — 改为渲染到 shell scrollback。拖拽选择恢复终端原生(聊天内容就是光标上方的真实行)。代价:窗口大小改变时可能出现重绘残影。",
- "cp.h.copymode": "/copy — 复制模式快捷键",
- "cp.body.osc":
- "没有活动选区时按 y 只复制当前行。复制先走 OSC 52(通过 SSH、mosh、开了 set -g set-clipboard on 的 tmux 均可用);超过 75 KB 的内容退到临时文件,路径在退出时打印。",
+ "cp.h.copymode": "/copy",
+ "cp.body.osc": "/copy 优先使用 OSC 52;大内容会退到临时文件。",
};
var DICT = { en: en, zh: zh };
diff --git a/docs/cli-reference.html b/docs/cli-reference.html
index 6de88325b..7fbbef186 100644
--- a/docs/cli-reference.html
+++ b/docs/cli-reference.html
@@ -302,7 +302,7 @@ Notable runtime flags (chat / code)
@@ -345,8 +345,8 @@
- --no-mouseDisable DECSET 1007 (alternate-scroll); wheel reverts to native terminal scroll
+ Disable app-managed mouse tracking; wheel reverts to native terminal scroll
Chat ops
Abort the current model turn (typed alternative to Esc)
-
@@ -685,9 +685,9 @@
- /copyOpen vim/tmux-style copy mode —
+ j/k navigate, v select, y yank to clipboard. The right answer for SSH / mosh / tmux where drag-select can't extend past the viewport
+ /copy [all|last|assistant|N]Copy the latest assistant response by default;
all copies the whole chat and N copies the last N itemsMouse
--no-mouse to opt out entirely.
+ In auto/app scroll mode, Reasonix captures mouse-wheel reports so chat history can
+ scroll inside terminals whose native scrollback does not move TUI content. Pass
+ --no-mouse to opt out entirely.
When drag-select doesn't work
/copy — open vim/tmux-style copy mode in-app.
- Snapshots the current chat to a navigable buffer; y yanks to
- clipboard via OSC 52 (with a temp-file fallback for terminals that don't
- support it).
+ /copy — copy the latest assistant response through
+ OSC 52. Use /copy all for the full chat or /copy N
+ for the last N serialized chat items. Oversized content falls back to a temp
+ file path.
--no-alt-screen — render to shell scrollback
@@ -744,52 +744,6 @@ /copy — copy mode keys| Key | -What it does | -
|---|---|
j / ↓ |
- Cursor down one line | -
k / ↑ |
- Cursor up one line | -
PgUp / PgDn |
- Page up / down | -
g / G |
- Jump to top / bottom | -
v |
- Start (or cancel) selection at the cursor | -
y / Enter |
- Yank selection to clipboard, exit | -
q / Esc |
- Quit without yanking | -
- y with no active selection yanks just the current line. The yank
- goes through OSC 52 first (works through SSH, mosh, tmux with
- set -g set-clipboard on); content larger than 75 KB falls back to
- a temp file whose path is printed on exit.
-
- {tokens.map((line, i) => (
-
- {showLineNumbers && (
- {i + startLine}
- )}
+ {keyed(tokens, (line) =>
+ hashString(line.map((token) => `${token.types.join(".")}:${token.content}`).join("|")),
+ ).map(({ item: line, key }, i) => (
+
+ {showLineNumbers && {i + startLine}}
- {line.map((token, k) => (
-
+ {keyed(line, (token) =>
+ hashString(`${token.types.join(".")}:${token.content}`),
+ ).map(({ item: token, key: tokenKey }) => (
+
))}
diff --git a/desktop/src/CommandPalette.tsx b/desktop/src/CommandPalette.tsx
index 63f903394..787538ef0 100644
--- a/desktop/src/CommandPalette.tsx
+++ b/desktop/src/CommandPalette.tsx
@@ -28,7 +28,7 @@ export type Command = {
run: () => void;
};
-export function useCommandPalette(active: boolean = true) {
+export function useCommandPalette(active = true) {
const [open, setOpen] = useState(false);
useEffect(() => {
// Skip in background tabs — each TabRuntime calls this hook, so without the gate Cmd+K toggles every tab's palette at once.
@@ -181,10 +181,14 @@ const GROUP_ORDER: CommandGroup[] = ["nav", "action", "workspace", "settings"];
function groupLabel(g: CommandGroup): string {
switch (g) {
- case "nav": return t("palette.groupNav");
- case "action": return t("palette.groupAction");
- case "workspace": return t("palette.groupWorkspace");
- case "settings": return t("palette.groupSettings");
+ case "nav":
+ return t("palette.groupNav");
+ case "action":
+ return t("palette.groupAction");
+ case "workspace":
+ return t("palette.groupWorkspace");
+ case "settings":
+ return t("palette.groupSettings");
}
}
@@ -235,9 +239,9 @@ export function CommandPalette({
arr.push(c);
byGroup.set(c.group, arr);
}
- return GROUP_ORDER
- .map((g) => ({ group: g, items: byGroup.get(g) ?? [] }))
- .filter((s) => s.items.length > 0);
+ return GROUP_ORDER.map((g) => ({ group: g, items: byGroup.get(g) ?? [] })).filter(
+ (s) => s.items.length > 0,
+ );
}, [filtered]);
if (!open) return null;
@@ -276,16 +280,15 @@ export function CommandPalette({
- {filtered.length === 0 ? (
- {t("palette.empty")}
- ) : null}
+ {filtered.length === 0 ? {t("palette.empty")} : null}
{grouped.map((section) => (
{groupLabel(section.group)}
{section.items.map((c) => {
const i = filtered.indexOf(c);
return (
-
)}
-
+
);
})}
diff --git a/desktop/src/Markdown.tsx b/desktop/src/Markdown.tsx
index 9544f4347..82ab54501 100644
--- a/desktop/src/Markdown.tsx
+++ b/desktop/src/Markdown.tsx
@@ -3,11 +3,11 @@ import { openPath, openUrl } from "@tauri-apps/plugin-opener";
import { Check, Copy, ExternalLink, FileText } from "lucide-react";
import {
Children,
+ type ReactNode,
cloneElement,
createContext,
isValidElement,
memo,
- type ReactNode,
useContext,
useState,
} from "react";
@@ -24,7 +24,7 @@ async function openWithEditor(
abs: string,
line?: number,
): Promise {
- if (editor && editor.trim()) {
+ if (editor?.trim()) {
await invoke("open_in_editor", { command: editor, path: abs, line: line ?? null });
return;
}
@@ -145,28 +145,21 @@ function FilePill({ path, line }: { path: string; line?: string }) {
}
};
return (
- {
e.preventDefault();
void copyOnly(e);
}}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- void openInEditor();
- }
- }}
title={t("markdown.filePillTitle")}
>
{path}
{line && :{line}}
{done && }
-
+
);
}
@@ -230,16 +223,18 @@ function normalizeMathDelimiters(source: string): string {
.replace(/\\\(/g, "$$")
.replace(/\\\)/g, "$$");
// Restore protected sequences
- result = result.replace(/\x00LB\x00/g, "\\\\[");
+ result = result.split(LB).join("\\\\[");
// Replace | with \vert inside math to prevent GFM table column splitting.
// \vert renders identically to | in KaTeX — it's the same vertical-bar
// glyph — but the markdown parser won't mistake it for a table separator.
- result = result.replace(/\$\$([\s\S]*?)\$\$/g, (_: string, m: string) =>
- "$$" + m.replace(/\|/g, "\\vert ") + "$$",
+ result = result.replace(
+ /\$\$([\s\S]*?)\$\$/g,
+ (_: string, m: string) => `\$\$${m.replace(/\|/g, "\\vert ")}\$\$`,
);
- result = result.replace(/\$([^$\n]+)\$/g, (_: string, m: string) =>
- "$" + m.replace(/\|/g, "\\vert ") + "$",
+ result = result.replace(
+ /\$([^$\n]+)\$/g,
+ (_: string, m: string) => `\$${m.replace(/\|/g, "\\vert ")}\$`,
);
return result;
@@ -349,7 +344,8 @@ export function extractFencedLang(children: ReactNode): string {
function flattenChildText(node: ReactNode): string {
if (typeof node === "string" || typeof node === "number") return String(node);
if (Array.isArray(node)) return node.map(flattenChildText).join("");
- if (isValidElement(node)) return flattenChildText((node.props as { children?: ReactNode }).children);
+ if (isValidElement(node))
+ return flattenChildText((node.props as { children?: ReactNode }).children);
return "";
}
diff --git a/desktop/src/i18n/de.ts b/desktop/src/i18n/de.ts
index 384ff0236..e51df3438 100644
--- a/desktop/src/i18n/de.ts
+++ b/desktop/src/i18n/de.ts
@@ -186,7 +186,8 @@ export const de: typeof en = {
webSearchEngineTavily: "tavily — 1000/Monat kostenlos (TAVILY_API_KEY setzen)",
webSearchEnginePerplexity: "perplexity — AI-native (PERPLEXITY_API_KEY setzen)",
webSearchEngineExa: "exa — AI-native 1000/Monat kostenlos (EXA_API_KEY setzen)",
- webSearchEngineBrave: "brave — unabhängiger Index, 2000/Monat kostenlos (BRAVE_SEARCH_API_KEY setzen)",
+ webSearchEngineBrave:
+ "brave — unabhängiger Index, 2000/Monat kostenlos (BRAVE_SEARCH_API_KEY setzen)",
webSearchEngineOllama: "ollama — Ollama Cloud-Websuche (OLLAMA_API_KEY setzen)",
webSearchEngineNote: "gilt für den nächsten web_search-Aufruf",
webSearchEndpoint: "SearXNG-Endpunkt",
@@ -288,7 +289,8 @@ export const de: typeof en = {
"Jede OpenAI-kompatible ID, die dein Endpunkt bereitstellt (vLLM, Ollama, Together, …).",
modelCustomActive: "Läuft aktuell auf benutzerdefinierter ID: {model}",
contextTokensLabel: "Kontextfenstergröße",
- contextTokensHint: "Überschreiben Sie die Prompt-seitige Token-Obergrenze für das aktuelle Modell (z. B. 1000000 für 1M). Leer lassen für den eingebauten Standard.",
+ contextTokensHint:
+ "Überschreiben Sie die Prompt-seitige Token-Obergrenze für das aktuelle Modell (z. B. 1000000 für 1M). Leer lassen für den eingebauten Standard.",
contextTokensPlaceholder: "Automatisch",
effortSection: "Reasoning-Effort",
ctxWindow: "Kontext",
@@ -699,7 +701,8 @@ export const de: typeof en = {
importSessionCount: "{count} Sitzungen · importiert alle",
importNotFound: "Keine lokalen Sitzungen gefunden",
importPrivacyHint: "Bestehende App-Einstellungen bleiben unverändert.",
- importResult: "{imported} Sitzung(en) importiert, {skipped} übersprungen, {failed} fehlgeschlagen.",
+ importResult:
+ "{imported} Sitzung(en) importiert, {skipped} übersprungen, {failed} fehlgeschlagen.",
continue: "Weiter",
refresh: "Aktualisieren",
importSource: "Quelle",
diff --git a/desktop/src/i18n/en.ts b/desktop/src/i18n/en.ts
index 773fba16f..fba816a28 100644
--- a/desktop/src/i18n/en.ts
+++ b/desktop/src/i18n/en.ts
@@ -276,7 +276,8 @@ export const en = {
modelCustomHint: "Any OpenAI-compatible id your endpoint serves (vLLM, Ollama, Together, …).",
modelCustomActive: "Currently running on a custom id: {model}",
contextTokensLabel: "Context window size",
- contextTokensHint: "Override the prompt-side token cap for the current model (e.g. 1000000 for 1M). Leave empty to use the built-in default.",
+ contextTokensHint:
+ "Override the prompt-side token cap for the current model (e.g. 1000000 for 1M). Leave empty to use the built-in default.",
contextTokensPlaceholder: "auto",
effortSection: "Reasoning effort",
ctxWindow: "Context",
diff --git a/desktop/src/i18n/ja.ts b/desktop/src/i18n/ja.ts
index 3a2a79bbf..0a9aaf871 100644
--- a/desktop/src/i18n/ja.ts
+++ b/desktop/src/i18n/ja.ts
@@ -51,6 +51,7 @@ export const ja = {
searchPlaceholder: "最近のパスを検索…",
empty: "最近のワークスペースはありません",
browse: "ローカルを参照…",
+ removeRecent: "最近の一覧から削除",
},
sidebar: {
newChat: "新しいチャット",
@@ -273,7 +274,8 @@ export const ja = {
modelCustomHint: "エンドポイントが提供するOpenAI互換ID (vLLM, Ollama, Together, …)。",
modelCustomActive: "現在カスタムIDで実行中: {model}",
contextTokensLabel: "コンテキストウィンドウサイズ",
- contextTokensHint: "現在のモデルのプロンプト側トークン上限を上書きします(例: 1000000 で 1M)。空欄なら既定値を使用。",
+ contextTokensHint:
+ "現在のモデルのプロンプト側トークン上限を上書きします(例: 1000000 で 1M)。空欄なら既定値を使用。",
contextTokensPlaceholder: "自動",
effortSection: "推論努力",
ctxWindow: "コンテキスト",
diff --git a/desktop/src/i18n/zh-CN.ts b/desktop/src/i18n/zh-CN.ts
index 769b3e8e9..cab089ea4 100644
--- a/desktop/src/i18n/zh-CN.ts
+++ b/desktop/src/i18n/zh-CN.ts
@@ -270,7 +270,8 @@ export const zhCN: typeof en = {
modelCustomHint: "任何 OpenAI 兼容的 ID(vLLM、Ollama、Together …)。",
modelCustomActive: "当前运行自定义 ID:{model}",
contextTokensLabel: "上下文窗口大小",
- contextTokensHint: "为当前模型覆盖提示侧 token 上限(如 1000000 表示 1M)。留空使用内置默认值。",
+ contextTokensHint:
+ "为当前模型覆盖提示侧 token 上限(如 1000000 表示 1M)。留空使用内置默认值。",
contextTokensPlaceholder: "自动",
effortSection: "推理强度",
ctxWindow: "上下文",
diff --git a/desktop/src/icons.tsx b/desktop/src/icons.tsx
index ade0d397a..b40385c07 100644
--- a/desktop/src/icons.tsx
+++ b/desktop/src/icons.tsx
@@ -14,6 +14,8 @@ function Ic({ size = 14, children, ...rest }: IconProps & { children: React.Reac
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
+ aria-hidden="true"
+ focusable="false"
{...rest}
>
{children}
@@ -22,61 +24,291 @@ function Ic({ size = 14, children, ...rest }: IconProps & { children: React.Reac
}
export const I = {
- plus: (p: IconProps) => ( ),
- search: (p: IconProps) => ( ),
- send: (p: IconProps) => ( ),
- chev: (p: IconProps) => ( ),
- chevR: (p: IconProps) => ( ),
- check: (p: IconProps) => ( ),
- x: (p: IconProps) => ( ),
- pencil: (p: IconProps) => ( ),
- terminal: (p: IconProps) => ( ),
+ plus: (p: IconProps) => (
+
+
+
+ ),
+ search: (p: IconProps) => (
+
+
+
+
+ ),
+ send: (p: IconProps) => (
+
+
+
+ ),
+ chev: (p: IconProps) => (
+
+
+
+ ),
+ chevR: (p: IconProps) => (
+
+
+
+ ),
+ check: (p: IconProps) => (
+
+
+
+ ),
+ x: (p: IconProps) => (
+
+
+
+ ),
+ pencil: (p: IconProps) => (
+
+
+
+
+ ),
+ terminal: (p: IconProps) => (
+
+
+
+
+ ),
brain: (p: IconProps) => (
),
- list: (p: IconProps) => ( ),
- diff: (p: IconProps) => ( ),
- globe: (p: IconProps) => ( ),
- link: (p: IconProps) => ( ),
- wrench: (p: IconProps) => ( ),
- bot: (p: IconProps) => ( ),
- archive: (p: IconProps) => ( ),
- bookmark: (p: IconProps) => ( ),
- warning: (p: IconProps) => ( ),
- zap: (p: IconProps) => ( ),
- database: (p: IconProps) => ( ),
- cpu: (p: IconProps) => ( ),
- coin: (p: IconProps) => ( ),
- file: (p: IconProps) => ( ),
- folder: (p: IconProps) => ( ),
- image: (p: IconProps) => ( ),
- paperclip: (p: IconProps) => ( ),
- mic: (p: IconProps) => ( ),
- sun: (p: IconProps) => ( ),
- moon: (p: IconProps) => ( ),
- panel_l: (p: IconProps) => ( ),
- panel_r: (p: IconProps) => ( ),
- cog: (p: IconProps) => ( ),
- stop: (p: IconProps) => ( ),
- play: (p: IconProps) => ( ),
- more: (p: IconProps) => ( ),
- pin: (p: IconProps) => ( ),
- rotate: (p: IconProps) => ( ),
- branch: (p: IconProps) => ( ),
- at: (p: IconProps) => ( ),
- slash: (p: IconProps) => ( ),
- layers: (p: IconProps) => ( ),
- download: (p: IconProps) => ( ),
- upload: (p: IconProps) => ( ),
- history: (p: IconProps) => ( ),
- shield: (p: IconProps) => ( ),
- warn: (p: IconProps) => ( ),
- help: (p: IconProps) => ( ),
- refresh: (p: IconProps) => ( ),
- copy: (p: IconProps) => ( ),
+ list: (p: IconProps) => (
+
+
+
+ ),
+ diff: (p: IconProps) => (
+
+
+
+
+
+ ),
+ globe: (p: IconProps) => (
+
+
+
+
+ ),
+ link: (p: IconProps) => (
+
+
+
+
+ ),
+ wrench: (p: IconProps) => (
+
+
+
+ ),
+ bot: (p: IconProps) => (
+
+
+
+
+ ),
+ archive: (p: IconProps) => (
+
+
+
+
+ ),
+ bookmark: (p: IconProps) => (
+
+
+
+ ),
+ warning: (p: IconProps) => (
+
+
+
+
+ ),
+ zap: (p: IconProps) => (
+
+
+
+ ),
+ database: (p: IconProps) => (
+
+
+
+
+ ),
+ cpu: (p: IconProps) => (
+
+
+
+
+
+ ),
+ coin: (p: IconProps) => (
+
+
+
+
+ ),
+ file: (p: IconProps) => (
+
+
+
+
+ ),
+ folder: (p: IconProps) => (
+
+
+
+ ),
+ image: (p: IconProps) => (
+
+
+
+
+
+ ),
+ paperclip: (p: IconProps) => (
+
+
+
+ ),
+ mic: (p: IconProps) => (
+
+
+
+
+ ),
+ sun: (p: IconProps) => (
+
+
+
+
+ ),
+ moon: (p: IconProps) => (
+
+
+
+ ),
+ panel_l: (p: IconProps) => (
+
+
+
+
+ ),
+ panel_r: (p: IconProps) => (
+
+
+
+
+ ),
+ cog: (p: IconProps) => (
+
+
+
+
+ ),
+ stop: (p: IconProps) => (
+
+
+
+ ),
+ play: (p: IconProps) => (
+
+
+
+ ),
+ more: (p: IconProps) => (
+
+
+
+
+
+ ),
+ pin: (p: IconProps) => (
+
+
+
+ ),
+ rotate: (p: IconProps) => (
+
+
+
+
+ ),
+ branch: (p: IconProps) => (
+
+
+
+
+
+
+ ),
+ at: (p: IconProps) => (
+
+
+
+
+ ),
+ slash: (p: IconProps) => (
+
+
+
+ ),
+ layers: (p: IconProps) => (
+
+
+
+
+ ),
+ download: (p: IconProps) => (
+
+
+
+ ),
+ upload: (p: IconProps) => (
+
+
+
+ ),
+ history: (p: IconProps) => (
+
+
+
+ ),
+ shield: (p: IconProps) => (
+
+
+
+
+ ),
+ warn: (p: IconProps) => (
+
+
+
+
+ ),
+ help: (p: IconProps) => (
+
+
+
+
+ ),
+ refresh: (p: IconProps) => (
+
+
+
+ ),
+ copy: (p: IconProps) => (
+
+
+
+
+ ),
trash: (p: IconProps) => (
diff --git a/desktop/src/main.tsx b/desktop/src/main.tsx
index 6e44cafd1..76aa99fc1 100644
--- a/desktop/src/main.tsx
+++ b/desktop/src/main.tsx
@@ -12,12 +12,7 @@ import "@fontsource/inter/700.css";
import "katex/dist/katex.min.css";
import { createRoot } from "react-dom/client";
import { App } from "./App";
-import {
- defaultStyleForTheme,
- isTheme,
- isThemeStyle,
- themeForStyle,
-} from "./theme";
+import { defaultStyleForTheme, isTheme, isThemeStyle, themeForStyle } from "./theme";
const stored = localStorage.getItem("reasonix.theme");
const storedStyle = localStorage.getItem("reasonix.themeStyle");
diff --git a/desktop/src/notifications.test.ts b/desktop/src/notifications.test.ts
index 1829a40c5..fcf8d7a45 100644
--- a/desktop/src/notifications.test.ts
+++ b/desktop/src/notifications.test.ts
@@ -1,9 +1,9 @@
import { describe, expect, it } from "vitest";
import { setLang } from "./i18n";
import {
+ type ApprovalSnapshot,
COMPLETION_NOTIFY_MIN_MS,
deriveDesktopNotifications,
- type ApprovalSnapshot,
} from "./notifications";
function emptySnapshot(): ApprovalSnapshot {
diff --git a/desktop/src/notifications.ts b/desktop/src/notifications.ts
index d38634053..ee2124d3e 100644
--- a/desktop/src/notifications.ts
+++ b/desktop/src/notifications.ts
@@ -115,10 +115,7 @@ export function shouldShowCompletionToast(args: {
focused: boolean;
}): boolean {
return (
- args.focused &&
- args.wasBusy &&
- !args.isBusy &&
- args.busyDurationMs >= COMPLETION_NOTIFY_MIN_MS
+ args.focused && args.wasBusy && !args.isBusy && args.busyDurationMs >= COMPLETION_NOTIFY_MIN_MS
);
}
diff --git a/desktop/src/styles.css b/desktop/src/styles.css
index a93b368f7..0dc3ad568 100644
--- a/desktop/src/styles.css
+++ b/desktop/src/styles.css
@@ -95,11 +95,11 @@ body[data-drag-over="1"]::after {
--card: oklch(99.5% 0.003 80);
--card-hover: oklch(96.5% 0.009 78);
--border: oklch(88% 0.016 76);
- --border-strong: oklch(78% 0.020 72);
+ --border-strong: oklch(78% 0.02 72);
--fg: oklch(22% 0.014 55);
--fg-2: oklch(36% 0.013 55);
--muted: oklch(53% 0.011 60);
- --muted-2: oklch(67% 0.010 65);
+ --muted-2: oklch(67% 0.01 65);
--accent: oklch(60% 0.19 38);
--accent-soft: oklch(60% 0.19 38 / 0.1);
@@ -113,11 +113,11 @@ body[data-drag-over="1"]::after {
--danger-soft: oklch(54% 0.2 22 / 0.1);
/* warm amber replaces cool violet in light mode */
--violet: oklch(62% 0.16 52);
- --violet-soft: oklch(62% 0.16 52 / 0.10);
+ --violet-soft: oklch(62% 0.16 52 / 0.1);
--shadow-sm: 0 1px 0 oklch(30% 0.05 50 / 0.05);
--shadow-md: 0 8px 24px -10px oklch(30% 0.05 50 / 0.13);
- --shadow-lg: 0 24px 60px -20px oklch(30% 0.05 50 / 0.20);
+ --shadow-lg: 0 24px 60px -20px oklch(30% 0.05 50 / 0.2);
}
[data-theme-style="porcelain"] {
@@ -366,6 +366,15 @@ html[data-platform="macos"] .app {
text-overflow: ellipsis;
white-space: nowrap;
}
+.tab .tab-main {
+ min-width: 0;
+ flex: 1;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ color: inherit;
+ text-align: left;
+}
.tab .close {
width: 16px;
height: 16px;
@@ -401,8 +410,7 @@ html[data-platform="macos"] .app {
display: grid;
place-items: center;
padding: 32px;
- background:
- radial-gradient(circle at top, var(--accent-soft), transparent 36%),
+ background: radial-gradient(circle at top, var(--accent-soft), transparent 36%),
linear-gradient(180deg, transparent, oklch(0% 0 0 / 0.04));
}
@@ -842,10 +850,10 @@ html[data-platform="macos"] .titlebar .tb-left {
}
.side-head {
- padding: 10px 12px 8px;
+ padding: 10px 10px 8px;
display: flex;
align-items: center;
- gap: 8px;
+ gap: 6px;
}
.side-head .new-btn {
flex: 1;
@@ -854,14 +862,17 @@ html[data-platform="macos"] .titlebar .tb-left {
display: inline-flex;
align-items: center;
flex-wrap: nowrap;
- gap: 8px;
- padding: 0 10px;
- font-size: 14px;
+ gap: 6px;
+ padding: 0 9px;
+ font-size: 13px;
+ line-height: 1;
font-weight: 500;
border-radius: 6px;
background: var(--accent);
color: oklch(99% 0 0);
box-shadow: var(--shadow-sm);
+ white-space: nowrap;
+ overflow: hidden;
}
.side-head .new-btn:hover {
background: var(--accent-strong);
@@ -877,7 +888,7 @@ html[data-platform="macos"] .titlebar .tb-left {
}
.side-head .new-btn kbd {
font-family: inherit;
- font-size: 14px;
+ font-size: 11px;
background: oklch(100% 0 0 / 0.18);
padding: 1px 5px;
border-radius: 3px;
@@ -890,7 +901,7 @@ html[data-platform="macos"] .titlebar .tb-left {
}
.side-head .new-btn .shortcut kbd {
min-width: 0;
- font-size: 14px;
+ font-size: 11px;
font-weight: 500;
line-height: inherit;
color: inherit;
@@ -900,16 +911,11 @@ html[data-platform="macos"] .titlebar .tb-left {
padding: 1px 5px;
box-shadow: none;
}
-@container sidebar (min-width: 191px) {
+@container sidebar (min-width: 272px) {
.side-head .new-btn .shortcut {
display: inline-flex;
}
}
-@container sidebar (max-width: 240px) {
- .side-head .new-btn .shortcut {
- display: none;
- }
-}
@container sidebar (max-width: 190px) {
.side-head .new-btn > span:not(.shortcut) {
display: none;
@@ -1399,27 +1405,44 @@ html[data-platform="macos"] .titlebar .tb-left {
color: var(--fg-2);
cursor: pointer;
}
-.session-menu .sm-item > svg { flex-shrink: 0; opacity: 0.7; }
+.session-menu .sm-item > svg {
+ flex-shrink: 0;
+ opacity: 0.7;
+}
.session-menu .sm-item:hover {
background: var(--panel);
color: var(--fg);
}
-.session-menu .sm-item:hover > svg { opacity: 1; }
+.session-menu .sm-item:hover > svg {
+ opacity: 1;
+}
.session-menu .sm-item:disabled {
opacity: 0.35;
cursor: not-allowed;
}
-.session-menu .sm-item.danger { color: var(--danger); }
-.session-menu .sm-item.danger > svg { opacity: 0.8; }
-.session-menu .sm-item.danger:hover { background: var(--danger-soft); }
+.session-menu .sm-item.danger {
+ color: var(--danger);
+}
+.session-menu .sm-item.danger > svg {
+ opacity: 0.8;
+}
+.session-menu .sm-item.danger:hover {
+ background: var(--danger-soft);
+}
.session-menu .sm-sep {
height: 1px;
background: var(--border);
margin: 4px 0;
}
@keyframes sm-confirm-in {
- from { opacity: 0; transform: scale(0.96) translateY(4px); }
- to { opacity: 1; transform: none; }
+ from {
+ opacity: 0;
+ transform: scale(0.96) translateY(4px);
+ }
+ to {
+ opacity: 1;
+ transform: none;
+ }
}
.session-menu .sm-confirm {
padding: 14px 12px 12px;
@@ -1480,18 +1503,24 @@ html[data-platform="macos"] .titlebar .tb-left {
background: var(--card);
color: var(--fg-2);
}
-.session-menu .sm-confirm-cancel:hover { background: var(--panel); color: var(--fg); }
+.session-menu .sm-confirm-cancel:hover {
+ background: var(--panel);
+ color: var(--fg);
+}
.session-menu .sm-confirm-ok {
border: 1px solid transparent;
background: var(--danger);
color: oklch(99% 0 0);
}
-.session-menu .sm-confirm-ok:hover { background: oklch(52% 0.22 25); }
+.session-menu .sm-confirm-ok:hover {
+ background: oklch(52% 0.22 25);
+}
/* ---- session delete confirmation popover (right-click) ---- */
.session-delete-popover {
position: fixed;
z-index: 80;
+ margin: 0;
background: var(--card);
border: 1px solid var(--border-strong);
border-radius: 8px;
@@ -1499,6 +1528,8 @@ html[data-platform="macos"] .titlebar .tb-left {
padding: 12px;
min-width: 220px;
max-width: 280px;
+ max-height: min(72vh, 360px);
+ overflow-y: auto;
font-size: 14px;
color: var(--fg);
animation: rise 0.16s ease-out;
@@ -1519,10 +1550,12 @@ html[data-platform="macos"] .titlebar .tb-left {
}
.session-delete-popover .actions {
display: flex;
+ flex-wrap: wrap;
gap: 6px;
}
.session-delete-popover button {
- flex: 1;
+ flex: 1 1 92px;
+ min-width: 0;
display: inline-flex;
align-items: center;
justify-content: center;
@@ -1533,6 +1566,9 @@ html[data-platform="macos"] .titlebar .tb-left {
font-weight: 500;
font-family: inherit;
cursor: pointer;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
transition: background 0.12s ease, color 0.12s ease;
}
.session-delete-popover button.cancel {
@@ -1556,8 +1592,12 @@ html[data-platform="macos"] .titlebar .tb-left {
.session-import-popover {
position: fixed;
z-index: 80;
+ margin: 0;
width: 320px;
max-width: min(320px, calc(100vw - 16px));
+ max-height: min(82vh, 560px);
+ overflow-y: auto;
+ overscroll-behavior: contain;
background: var(--card);
border: 1px solid var(--border-strong);
border-radius: 10px;
@@ -1780,17 +1820,22 @@ html[data-platform="macos"] .titlebar .tb-left {
}
.session-import-popover .actions {
display: flex;
+ flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.session-import-popover .actions button {
- flex: 1;
+ flex: 1 1 120px;
+ min-width: 0;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
padding: 8px 12px;
border: 1px solid var(--border-strong);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.session-import-popover .actions button.cancel {
background: transparent;
@@ -1807,7 +1852,9 @@ html[data-platform="macos"] .titlebar .tb-left {
}
/* Folder-delete confirm needs more room: workspace name + session count */
-.folder-menu { max-width: 260px; }
+.folder-menu {
+ max-width: 260px;
+}
.folder-menu .sm-confirm-desc {
max-width: 220px;
-webkit-line-clamp: 3;
@@ -1831,12 +1878,17 @@ html[data-platform="macos"] .titlebar .tb-left {
.side-foot .row {
display: flex;
align-items: center;
+ width: 100%;
gap: 8px;
padding: 6px 8px;
+ border: 0;
border-radius: 6px;
+ background: transparent;
font-size: 14px;
+ font-family: inherit;
color: var(--fg-2);
cursor: pointer;
+ text-align: left;
}
.side-foot .row:hover {
background: var(--panel);
@@ -1969,32 +2021,44 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
flex: 1;
min-height: 0;
position: relative;
+ overflow: hidden;
}
-.thread::-webkit-scrollbar {
+.thread-scroller {
+ scrollbar-width: thin;
+ scrollbar-color: var(--border) transparent;
+ overscroll-behavior: contain;
+}
+.thread-scroller:focus {
+ outline: none;
+}
+.thread-scroller::-webkit-scrollbar {
width: 8px;
}
-.thread::-webkit-scrollbar-thumb {
+.thread-scroller::-webkit-scrollbar-thumb {
background-clip: content-box;
border: 2px solid transparent;
background-color: var(--border);
border-radius: 999px;
transition: border-width 150ms, background-color 200ms;
}
-.thread::-webkit-scrollbar-thumb:hover {
+.thread-scroller::-webkit-scrollbar-thumb:hover {
border-width: 0;
background-color: var(--border-strong);
}
-.thread::-webkit-scrollbar-track {
+.thread-scroller::-webkit-scrollbar-track {
background: transparent;
}
.thread-inner {
max-width: var(--thread-max-width, 740px);
- margin: 0 auto 28px;
+ margin: 0 auto 18px;
padding: 0 32px;
}
.thread-inner--standalone {
margin-top: 28px;
}
+.thread-tail {
+ height: 10px;
+}
.thread-inner > div[data-turn] {
content-visibility: auto;
contain-intrinsic-size: auto 100px;
@@ -2022,7 +2086,13 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
max-height: 240px;
overflow-y: auto;
scrollbar-width: none;
- -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 16%, #000 84%, transparent 100%);
+ -webkit-mask-image: linear-gradient(
+ to bottom,
+ transparent 0%,
+ #000 16%,
+ #000 84%,
+ transparent 100%
+ );
mask-image: linear-gradient(to bottom, transparent 0%, #000 16%, #000 84%, transparent 100%);
}
.jump-scroll::-webkit-scrollbar {
@@ -2035,18 +2105,31 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
justify-content: flex-end;
width: 32px;
min-height: 14px;
+ padding: 0;
+ border: 0;
+ background: transparent;
cursor: pointer;
flex-shrink: 0;
}
+.jump-item:focus-visible {
+ outline: 1px solid var(--accent);
+ outline-offset: 2px;
+}
.jump-dot {
height: 3px;
border-radius: 2px;
background: var(--border-strong);
transition: background 200ms, width 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
-.jump-dot[data-d="0"] { background: var(--accent); }
-.jump-dot[data-d="1"] { background: color-mix(in srgb, var(--accent) 60%, transparent); }
-.jump-dot[data-d="2"] { background: color-mix(in srgb, var(--accent) 35%, transparent); }
+.jump-dot[data-d="0"] {
+ background: var(--accent);
+}
+.jump-dot[data-d="1"] {
+ background: color-mix(in srgb, var(--accent) 60%, transparent);
+}
+.jump-dot[data-d="2"] {
+ background: color-mix(in srgb, var(--accent) 35%, transparent);
+}
.jump-preview {
position: absolute;
right: 100%;
@@ -2070,7 +2153,7 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
/* Jump-to-bottom button — shown when user has scrolled up during streaming */
.thread-jump-bottom {
- position: sticky;
+ position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
@@ -2085,7 +2168,7 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
background: var(--bg);
color: var(--fg);
cursor: pointer;
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: opacity 0.15s, transform 0.15s;
opacity: 0.9;
}
@@ -2095,7 +2178,7 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
transform: translateX(-50%) scale(1.05);
}
[data-theme="light"] .thread-jump-bottom {
- box-shadow: 0 2px 12px rgba(0,0,0,0.10);
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
/* ---------- TURN HEADERS ---------- */
@@ -2282,13 +2365,18 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
.markdown li:last-child {
margin-bottom: 0;
}
-.msg-text ul ul, .markdown ul ul,
-.msg-text ol ol, .markdown ol ol,
-.msg-text ul ol, .markdown ul ol,
-.msg-text ol ul, .markdown ol ul {
+.msg-text ul ul,
+.markdown ul ul,
+.msg-text ol ol,
+.markdown ol ol,
+.msg-text ul ol,
+.markdown ul ol,
+.msg-text ol ul,
+.markdown ol ul {
margin: 4px 0;
}
-.msg-text h1, .markdown h1 {
+.msg-text h1,
+.markdown h1 {
font-size: 1.45em;
font-weight: 700;
letter-spacing: -0.02em;
@@ -2296,7 +2384,8 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
margin: 22px 0 10px;
color: var(--fg);
}
-.msg-text h2, .markdown h2 {
+.msg-text h2,
+.markdown h2 {
font-size: 1.2em;
font-weight: 700;
letter-spacing: -0.015em;
@@ -2304,24 +2393,31 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
margin: 20px 0 8px;
color: var(--fg);
}
-.msg-text h3, .markdown h3 {
+.msg-text h3,
+.markdown h3 {
font-size: 1.05em;
font-weight: 600;
line-height: 1.35;
margin: 18px 0 6px;
color: var(--fg);
}
-.msg-text h4, .markdown h4,
-.msg-text h5, .markdown h5,
-.msg-text h6, .markdown h6 {
+.msg-text h4,
+.markdown h4,
+.msg-text h5,
+.markdown h5,
+.msg-text h6,
+.markdown h6 {
font-size: 1em;
font-weight: 600;
margin: 14px 0 4px;
color: var(--fg-2);
}
-.msg-text h1:first-child, .markdown h1:first-child,
-.msg-text h2:first-child, .markdown h2:first-child,
-.msg-text h3:first-child, .markdown h3:first-child {
+.msg-text h1:first-child,
+.markdown h1:first-child,
+.msg-text h2:first-child,
+.markdown h2:first-child,
+.msg-text h3:first-child,
+.markdown h3:first-child {
margin-top: 0;
}
.msg-text blockquote,
@@ -2607,9 +2703,7 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
cursor: pointer;
vertical-align: baseline;
text-decoration: none;
- transition:
- background 0.12s,
- color 0.12s;
+ transition: background 0.12s, color 0.12s;
white-space: nowrap;
}
.file-pill:hover {
@@ -2755,10 +2849,7 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
border-radius: 999px;
box-shadow: var(--shadow-sm);
cursor: pointer;
- transition:
- background 0.13s ease,
- color 0.13s ease,
- transform 0.13s ease;
+ transition: background 0.13s ease, color 0.13s ease, transform 0.13s ease;
}
.proc-group.is-clipped .proc-group-toggle {
position: absolute;
@@ -3246,11 +3337,7 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
width: 80px;
height: 56px;
border-radius: 6px;
- background: repeating-linear-gradient(
- -45deg,
- var(--panel-2) 0 6px,
- var(--card) 6px 12px
- );
+ background: repeating-linear-gradient(-45deg, var(--panel-2) 0 6px, var(--card) 6px 12px);
border: 1px solid var(--border);
display: flex;
align-items: center;
@@ -3571,6 +3658,12 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
.chip .x {
cursor: pointer;
opacity: 0.5;
+ border: 0;
+ padding: 0;
+ background: transparent;
+ color: inherit;
+ display: inline-flex;
+ align-items: center;
}
.chip .x:hover {
opacity: 1;
@@ -3608,7 +3701,9 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
color: var(--muted);
display: inline-flex;
align-items: center;
+ justify-content: center;
gap: 5px;
+ min-width: 30px;
}
.composer-foot .cf-btn:hover {
background: var(--panel);
@@ -3654,6 +3749,35 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
padding: 1px 5px;
border-radius: 3px;
}
+.composer-model-direct {
+ position: relative;
+ min-width: 0;
+ flex: 0 1 auto;
+}
+.composer-tools-more {
+ position: relative;
+ display: none;
+ flex-shrink: 0;
+}
+.composer-tools-menu {
+ left: auto;
+ right: 0;
+ width: min(360px, calc(100vw - 44px));
+ max-height: min(70vh, 520px);
+ display: block;
+ overflow-y: auto;
+}
+.composer-tools-menu .popup-list {
+ overflow: visible;
+}
+.composer-tools-actions {
+ border-bottom: 1px solid var(--border);
+}
+.popup-item:is(button) {
+ border: 0;
+ background: transparent;
+ font: inherit;
+}
.send-btn {
width: 30px;
@@ -3685,7 +3809,9 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
font-size: 14px;
color: var(--muted-2);
}
-.hint-row .grow { flex: 1; }
+.hint-row .grow {
+ flex: 1;
+}
.hint-row .hint-sep {
width: 1px;
height: 12px;
@@ -3751,6 +3877,20 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
.popup .ph .grow {
flex: 1;
}
+.popup-close {
+ border: 0;
+ padding: 2px;
+ border-radius: 5px;
+ background: transparent;
+ color: inherit;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+}
+.popup-close:hover {
+ background: var(--panel);
+ color: var(--fg);
+}
.popup-list {
overflow-y: auto;
overflow-x: auto;
@@ -3760,6 +3900,7 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
min-width: max-content;
}
.popup-item {
+ width: 100%;
display: grid;
grid-template-columns: 24px 1fr auto;
align-items: center;
@@ -3768,6 +3909,8 @@ html:not([data-platform="macos"]) .shortcut kbd[data-key="mod"] {
border-radius: 6px;
cursor: pointer;
font-size: 14px;
+ color: inherit;
+ text-align: left;
}
.popup-item:hover,
.popup-item[data-active="true"] {
@@ -3860,9 +4003,15 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
font-size: 14px;
color: var(--muted);
border-radius: 5px 5px 0 0;
+ border: 1px solid transparent;
+ background: transparent;
font-family: inherit;
cursor: pointer;
}
+.ctx-tab:focus-visible {
+ outline: 1px solid var(--accent);
+ outline-offset: -2px;
+}
.ctx-tab:hover {
color: var(--fg);
}
@@ -4178,18 +4327,36 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
gap: 6px;
padding: 0 10px;
height: 100%;
+ max-width: min(280px, 42vw);
+ border: 0;
+ background: transparent;
+ color: inherit;
+ font: inherit;
cursor: pointer;
+ min-width: 0;
+}
+.statusbar .seg:disabled {
+ cursor: default;
+ opacity: 0.75;
}
.statusbar .seg:hover {
background: var(--panel);
color: var(--fg);
}
+.statusbar .seg:disabled:hover {
+ background: transparent;
+ color: var(--muted);
+}
.statusbar .seg.theme-trigger.active {
background: var(--accent-soft);
color: var(--accent);
}
.statusbar .seg .v {
color: var(--fg);
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.statusbar .seg .v.ok {
color: var(--success);
@@ -4359,7 +4526,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
display: inline-block;
flex-shrink: 0;
}
-.status-dot.warn { background: var(--warning); }
+.status-dot.warn {
+ background: var(--warning);
+}
.meta-label {
font-size: 10px;
@@ -4393,12 +4562,7 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
/* shimmer for streaming text */
.shimmer {
- background: linear-gradient(
- 90deg,
- var(--fg-2) 0%,
- var(--accent) 50%,
- var(--fg-2) 100%
- );
+ background: linear-gradient(90deg, var(--fg-2) 0%, var(--accent) 50%, var(--fg-2) 100%);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
@@ -4453,7 +4617,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
animation-delay: 0.3s;
}
@keyframes bounce {
- 0%, 80%, 100% {
+ 0%,
+ 80%,
+ 100% {
transform: translateY(0);
opacity: 0.4;
}
@@ -4464,12 +4630,7 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
}
.thinking .label .sh {
- background: linear-gradient(
- 90deg,
- var(--muted) 0%,
- var(--accent) 50%,
- var(--muted) 100%
- );
+ background: linear-gradient(90deg, var(--muted) 0%, var(--accent) 50%, var(--muted) 100%);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
@@ -4606,11 +4767,21 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
background-position: -100% 0;
}
}
-.skel-line.w-90 { width: 92%; }
-.skel-line.w-70 { width: 72%; }
-.skel-line.w-60 { width: 64%; }
-.skel-line.w-40 { width: 44%; }
-.skel-line.w-30 { width: 34%; }
+.skel-line.w-90 {
+ width: 92%;
+}
+.skel-line.w-70 {
+ width: 72%;
+}
+.skel-line.w-60 {
+ width: 64%;
+}
+.skel-line.w-40 {
+ width: 44%;
+}
+.skel-line.w-30 {
+ width: 34%;
+}
/* progressive log (tool live output) */
.live-log {
@@ -4639,15 +4810,27 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
opacity: 0;
animation: line-in 0.25s ease-out forwards;
}
-.live-log .line.ok { color: var(--success); }
-.live-log .line.dim { color: var(--muted); }
+.live-log .line.ok {
+ color: var(--success);
+}
+.live-log .line.dim {
+ color: var(--muted);
+}
@keyframes line-in {
- to { opacity: 1; }
+ to {
+ opacity: 1;
+ }
}
@keyframes rise {
- from { opacity: 0; transform: translateY(8px); }
- to { opacity: 1; transform: translateY(0); }
+ from {
+ opacity: 0;
+ transform: translateY(8px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
.composer-busy-status {
@@ -4713,7 +4896,10 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
justify-content: center;
width: 14px;
height: 14px;
+ border: 0;
border-radius: 50%;
+ padding: 0;
+ background: transparent;
cursor: pointer;
color: var(--muted);
}
@@ -4741,8 +4927,12 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
border-radius: 50%;
background: var(--accent);
}
-.queue-strip .pip.q { background: var(--muted-2); }
-.queue-strip .pip.w { background: var(--warning); }
+.queue-strip .pip.q {
+ background: var(--muted-2);
+}
+.queue-strip .pip.w {
+ background: var(--warning);
+}
/* ---- token stream rate bar ---- */
.tps {
@@ -4770,14 +4960,34 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
opacity: 0.65;
animation: bar 0.9s ease-in-out infinite;
}
-.tps .bars span:nth-child(1) { height: 30%; animation-delay: 0s; }
-.tps .bars span:nth-child(2) { height: 65%; animation-delay: 0.1s; }
-.tps .bars span:nth-child(3) { height: 90%; animation-delay: 0.2s; }
-.tps .bars span:nth-child(4) { height: 50%; animation-delay: 0.3s; }
-.tps .bars span:nth-child(5) { height: 75%; animation-delay: 0.4s; }
+.tps .bars span:nth-child(1) {
+ height: 30%;
+ animation-delay: 0s;
+}
+.tps .bars span:nth-child(2) {
+ height: 65%;
+ animation-delay: 0.1s;
+}
+.tps .bars span:nth-child(3) {
+ height: 90%;
+ animation-delay: 0.2s;
+}
+.tps .bars span:nth-child(4) {
+ height: 50%;
+ animation-delay: 0.3s;
+}
+.tps .bars span:nth-child(5) {
+ height: 75%;
+ animation-delay: 0.4s;
+}
@keyframes bar {
- 0%, 100% { transform: scaleY(0.5); }
- 50% { transform: scaleY(1); }
+ 0%,
+ 100% {
+ transform: scaleY(0.5);
+ }
+ 50% {
+ transform: scaleY(1);
+ }
}
/* shell card while running */
@@ -4805,8 +5015,13 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
animation: pulse-soft 1.6s ease-in-out infinite;
}
@keyframes pulse-soft {
- 0%, 100% { opacity: 0; }
- 50% { opacity: 1; }
+ 0%,
+ 100% {
+ opacity: 0;
+ }
+ 50% {
+ opacity: 1;
+ }
}
.user-status {
display: inline-flex;
@@ -4842,8 +5057,12 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
background: oklch(0% 0 0 / 0.18);
}
@keyframes fade-in {
- from { opacity: 0; }
- to { opacity: 1; }
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
}
.settings {
@@ -4876,12 +5095,17 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
.settings-side .row {
display: flex;
align-items: center;
+ width: 100%;
gap: 10px;
padding: 7px 10px;
+ border: 0;
border-radius: 6px;
+ background: transparent;
font-size: 14px;
+ font-family: inherit;
color: var(--fg-2);
cursor: pointer;
+ text-align: left;
}
.settings-side .row:hover {
background: var(--panel);
@@ -4927,7 +5151,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
font-size: 14px;
margin-top: 2px;
}
-.settings-head .grow { flex: 1; }
+.settings-head .grow {
+ flex: 1;
+}
.settings-head .close-btn {
width: 26px;
height: 26px;
@@ -5284,7 +5510,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
color: var(--muted);
margin-top: 2px;
}
-.scard .grow { flex: 1; }
+.scard .grow {
+ flex: 1;
+}
.scard .mcp-spec-body {
min-width: 0;
flex: 1 1 auto;
@@ -5339,6 +5567,10 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
padding: 14px;
position: relative;
cursor: pointer;
+ color: var(--fg);
+ font: inherit;
+ text-align: left;
+ width: 100%;
}
.mcard[data-on="true"] {
border-color: var(--accent);
@@ -5372,8 +5604,12 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
font-family: inherit;
font-size: 14px;
}
-.mcard .spec .k { color: var(--muted); }
-.mcard .spec .v { color: var(--fg); }
+.mcard .spec .k {
+ color: var(--muted);
+}
+.mcard .spec .v {
+ color: var(--fg);
+}
.mcard .price {
margin-top: 8px;
padding-top: 8px;
@@ -5445,13 +5681,16 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
text-transform: uppercase;
}
.mem-edit .scope[data-s="project"] {
- background: var(--accent-soft); color: var(--accent);
+ background: var(--accent-soft);
+ color: var(--accent);
}
.mem-edit .scope[data-s="user"] {
- background: var(--violet-soft); color: var(--violet);
+ background: var(--violet-soft);
+ color: var(--violet);
}
.mem-edit .scope[data-s="global"] {
- background: var(--success-soft); color: var(--success);
+ background: var(--success-soft);
+ color: var(--success);
}
.mem-edit .txt {
font-size: 14px;
@@ -5489,8 +5728,12 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
margin-top: 4px;
letter-spacing: -0.02em;
}
-.bill-card .v.ok { color: var(--success); }
-.bill-card .v.acc { color: var(--accent); }
+.bill-card .v.ok {
+ color: var(--success);
+}
+.bill-card .v.acc {
+ color: var(--accent);
+}
.bill-card .sub {
font-size: 14px;
color: var(--muted);
@@ -5540,8 +5783,12 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
.usage-table td.num {
text-align: right;
}
-.usage-table td .pos { color: var(--accent); }
-.usage-table td .ok { color: var(--success); }
+.usage-table td .pos {
+ color: var(--accent);
+}
+.usage-table td .ok {
+ color: var(--success);
+}
/* plan-approved banner inline in thread */
.plan-banner {
@@ -5628,47 +5875,149 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
TONE PALETTE — systematized
============================================================ */
:root {
- --tone-ok: oklch(72% 0.16 152);
- --tone-ok-soft: oklch(72% 0.16 152 / 0.12);
- --tone-warn: oklch(78% 0.16 80);
+ --tone-ok: oklch(72% 0.16 152);
+ --tone-ok-soft: oklch(72% 0.16 152 / 0.12);
+ --tone-warn: oklch(78% 0.16 80);
--tone-warn-soft: oklch(78% 0.16 80 / 0.14);
- --tone-err: oklch(68% 0.20 25);
- --tone-err-soft: oklch(68% 0.20 25 / 0.14);
- --tone-info: oklch(70% 0.13 230);
+ --tone-err: oklch(68% 0.2 25);
+ --tone-err-soft: oklch(68% 0.2 25 / 0.14);
+ --tone-info: oklch(70% 0.13 230);
--tone-info-soft: oklch(70% 0.13 230 / 0.14);
- --tone-brand: oklch(66% 0.18 38);
- --tone-brand-soft:oklch(66% 0.18 38 / 0.14);
- --tone-accent: var(--accent);
+ --tone-brand: oklch(66% 0.18 38);
+ --tone-brand-soft: oklch(66% 0.18 38 / 0.14);
+ --tone-accent: var(--accent);
--tone-accent-soft: var(--accent-soft);
- --tone-ghost: oklch(60% 0.005 250);
- --tone-ghost-soft:oklch(60% 0.005 250 / 0.10);
+ --tone-ghost: oklch(60% 0.005 250);
+ --tone-ghost-soft: oklch(60% 0.005 250 / 0.1);
/* states */
- --st-running: oklch(72% 0.16 200);
- --st-done: var(--tone-ok);
- --st-failed: var(--tone-err);
- --st-queued: oklch(65% 0.005 250);
- --st-blocked: oklch(70% 0.14 50);
- --st-skipped: oklch(60% 0.04 250);
- --st-aborted: oklch(60% 0.13 25);
+ --st-running: oklch(72% 0.16 200);
+ --st-done: var(--tone-ok);
+ --st-failed: var(--tone-err);
+ --st-queued: oklch(65% 0.005 250);
+ --st-blocked: oklch(70% 0.14 50);
+ --st-skipped: oklch(60% 0.04 250);
+ --st-aborted: oklch(60% 0.13 25);
}
[data-theme="light"] {
- --tone-ok: oklch(50% 0.16 152);
- --tone-warn: oklch(58% 0.16 75);
- --tone-err: oklch(56% 0.20 25);
- --tone-info: oklch(55% 0.15 230);
- --tone-brand: oklch(50% 0.18 258);
- --tone-ghost: oklch(45% 0.005 250);
+ --tone-ok: oklch(50% 0.16 152);
+ --tone-warn: oklch(58% 0.16 75);
+ --tone-err: oklch(56% 0.2 25);
+ --tone-info: oklch(55% 0.15 230);
+ --tone-brand: oklch(50% 0.18 258);
+ --tone-ghost: oklch(45% 0.005 250);
}
/* ============================================================
APPROVAL CARD — universal (plan / edit / shell / path / checkpoint / refinement / revision)
============================================================ */
-.approval {
- border: 1px solid var(--border);
- background: var(--card);
- border-radius: 10px;
- overflow: hidden;
- position: relative;
+.approval-tray {
+ width: 100%;
+ max-width: var(--thread-max-width, 740px);
+ margin: 0 auto;
+ padding: 0 32px 8px;
+ flex-shrink: 0;
+}
+.approval-tray-head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 2px;
+ color: var(--muted);
+ font-size: 14px;
+}
+.approval-tray-title {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+ color: var(--fg-2);
+}
+.approval-tray-title span {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.approval-tray-count {
+ min-width: 20px;
+ height: 20px;
+ padding: 0 6px;
+ border-radius: 999px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--accent-soft);
+ color: var(--accent);
+ font-size: 12px;
+ font-weight: 600;
+}
+.approval-tray-head .grow {
+ flex: 1;
+}
+.approval-tray-toggle,
+.approval-queue-more {
+ border: 1px solid var(--border);
+ background: var(--panel);
+ color: var(--fg-2);
+ border-radius: 6px;
+ font: inherit;
+ font-size: 13px;
+ cursor: pointer;
+}
+.approval-tray-toggle {
+ padding: 3px 8px;
+ flex-shrink: 0;
+}
+.approval-tray-toggle:hover,
+.approval-queue-more:hover {
+ background: var(--panel-2);
+ color: var(--fg);
+}
+.approval-stack {
+ display: grid;
+ gap: 8px;
+ max-height: min(28vh, 340px);
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ padding-right: 4px;
+ scrollbar-width: thin;
+ scrollbar-color: var(--border) transparent;
+}
+.approval-stack::-webkit-scrollbar {
+ width: 8px;
+}
+.approval-stack::-webkit-scrollbar-thumb {
+ background-color: var(--border);
+ border: 2px solid transparent;
+ background-clip: content-box;
+ border-radius: 999px;
+}
+.approval-slot {
+ min-width: 0;
+}
+.approval-queue-more {
+ min-height: 34px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 7px 10px;
+}
+.approval-connecting {
+ padding: 10px 12px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--panel);
+ color: var(--muted);
+ font-family: inherit;
+ font-size: 13px;
+}
+.approval {
+ border: 1px solid var(--border);
+ background: var(--card);
+ border-radius: 10px;
+ overflow: hidden;
+ position: relative;
}
.approval::before {
content: "";
@@ -5677,12 +6026,24 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
width: 3px;
background: var(--approval-accent, var(--accent));
}
-.approval[data-tone="ok"] { --approval-accent: var(--tone-ok); }
-.approval[data-tone="warn"] { --approval-accent: var(--tone-warn); }
-.approval[data-tone="danger"] { --approval-accent: var(--tone-err); }
-.approval[data-tone="info"] { --approval-accent: var(--tone-info); }
-.approval[data-tone="brand"] { --approval-accent: var(--tone-brand); }
-.approval[data-tone="ghost"] { --approval-accent: var(--tone-ghost); }
+.approval[data-tone="ok"] {
+ --approval-accent: var(--tone-ok);
+}
+.approval[data-tone="warn"] {
+ --approval-accent: var(--tone-warn);
+}
+.approval[data-tone="danger"] {
+ --approval-accent: var(--tone-err);
+}
+.approval[data-tone="info"] {
+ --approval-accent: var(--tone-info);
+}
+.approval[data-tone="brand"] {
+ --approval-accent: var(--tone-brand);
+}
+.approval[data-tone="ghost"] {
+ --approval-accent: var(--tone-ghost);
+}
.approval .ap-head {
display: flex;
@@ -5700,7 +6061,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
background: var(--approval-accent);
color: oklch(99% 0 0);
}
-[data-theme="light"] .approval .ap-ico { color: oklch(100% 0 0); }
+[data-theme="light"] .approval .ap-ico {
+ color: oklch(100% 0 0);
+}
.approval .ap-kind {
font-family: inherit;
font-size: 14px;
@@ -5769,16 +6132,42 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
.approval .ap-foot {
display: flex;
align-items: center;
+ justify-content: flex-end;
+ flex-wrap: wrap;
gap: 6px;
padding: 8px 12px;
border-top: 1px solid var(--border);
background: var(--bg-2);
}
-.approval .ap-foot .grow { flex: 1; }
+.approval .ap-foot .btn {
+ min-width: 0;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.approval .ap-foot .grow {
+ flex: 1 1 16px;
+ min-width: 12px;
+}
.approval .ap-foot .meta {
font-family: inherit;
font-size: 14px;
color: var(--muted);
+ min-width: 0;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ text-align: right;
+}
+.approval .ap-body .btn {
+ min-width: 0;
+ max-width: 100%;
+}
+.approval .ap-body .btn > div {
+ min-width: 0;
+ overflow: hidden;
}
/* ============================================================
@@ -5798,20 +6187,43 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
border-bottom: 1px solid var(--border);
}
.task-card .th .ico {
- width: 24px; height: 24px; border-radius: 6px;
- display: inline-flex; align-items: center; justify-content: center;
- background: var(--accent-soft); color: var(--accent);
+ width: 24px;
+ height: 24px;
+ border-radius: 6px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--accent-soft);
+ color: var(--accent);
+}
+.task-card .th .tt {
+ font-size: 14px;
+ font-weight: 600;
+}
+.task-card .th .ss {
+ font-family: inherit;
+ font-size: 14px;
+ color: var(--muted);
+ margin-top: 1px;
+}
+.task-card .th .grow {
+ flex: 1;
}
-.task-card .th .tt { font-size: 14px; font-weight: 600; }
-.task-card .th .ss { font-family: inherit; font-size: 14px; color: var(--muted); margin-top: 1px; }
-.task-card .th .grow { flex: 1; }
.task-card .th .meter {
- width: 64px; height: 4px; border-radius: 999px;
+ width: 64px;
+ height: 4px;
+ border-radius: 999px;
background: var(--panel);
overflow: hidden;
}
-.task-card .th .meter > span { display: block; height: 100%; background: var(--accent); }
-.task-card .tb { padding: 6px 0; }
+.task-card .th .meter > span {
+ display: block;
+ height: 100%;
+ background: var(--accent);
+}
+.task-card .tb {
+ padding: 6px 0;
+}
.task-step {
display: grid;
grid-template-columns: 48px 18px 1fr auto;
@@ -5820,11 +6232,23 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
padding: 8px 14px;
border-left: 2px solid transparent;
}
-.task-step[data-state="running"] { border-left-color: var(--st-running); background: oklch(72% 0.16 200 / 0.04); }
-.task-step[data-state="done"] { border-left-color: var(--st-done); }
-.task-step[data-state="failed"] { border-left-color: var(--st-failed); background: oklch(68% 0.20 25 / 0.05); }
-.task-step[data-state="blocked"] { border-left-color: var(--st-blocked); }
-.task-step[data-state="skipped"] { opacity: 0.55; }
+.task-step[data-state="running"] {
+ border-left-color: var(--st-running);
+ background: oklch(72% 0.16 200 / 0.04);
+}
+.task-step[data-state="done"] {
+ border-left-color: var(--st-done);
+}
+.task-step[data-state="failed"] {
+ border-left-color: var(--st-failed);
+ background: oklch(68% 0.2 25 / 0.05);
+}
+.task-step[data-state="blocked"] {
+ border-left-color: var(--st-blocked);
+}
+.task-step[data-state="skipped"] {
+ opacity: 0.55;
+}
.task-step .nx {
font-family: inherit;
font-size: 14px;
@@ -5832,34 +6256,127 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
padding-top: 1px;
}
.task-step .st {
- width: 14px; height: 14px; border-radius: 50%;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
margin-top: 2px;
position: relative;
}
-.task-step[data-state="queued"] .st { border: 1.5px dashed var(--border-strong); }
-.task-step[data-state="running"] .st { background: var(--st-running); animation: pulse 1.4s ease-in-out infinite; }
-.task-step[data-state="done"] .st { background: var(--st-done); }
-.task-step[data-state="done"] .st::after { content:"\2713"; position:absolute; inset:0; color:#fff; font-size:14px; display:flex; align-items:center; justify-content:center; }
-.task-step[data-state="failed"] .st { background: var(--st-failed); }
-.task-step[data-state="failed"] .st::after { content:"!"; position:absolute; inset:0; color:#fff; font-size:14px; font-weight:700; display:flex; align-items:center; justify-content:center; }
-.task-step[data-state="blocked"] .st { background: var(--st-blocked); }
-.task-step[data-state="blocked"] .st::after { content:"\f7"; position:absolute; inset:0; color:#fff; font-size:14px; display:flex; align-items:center; justify-content:center; }
-.task-step[data-state="skipped"] .st { background: var(--st-skipped); }
-.task-step[data-state="skipped"] .st::after { content:"\2014"; position:absolute; inset:0; color:#fff; font-size:14px; display:flex; align-items:center; justify-content:center; }
-.task-step .l { font-size: 14px; color: var(--fg); }
-.task-step .l .h { color: var(--muted); font-size: 14px; margin-top: 2px; font-family: inherit; }
-.task-step .t { font-family: inherit; font-size: 14px; color: var(--muted); }
+.task-step[data-state="queued"] .st {
+ border: 1.5px dashed var(--border-strong);
+}
+.task-step[data-state="running"] .st {
+ background: var(--st-running);
+ animation: pulse 1.4s ease-in-out infinite;
+}
+.task-step[data-state="done"] .st {
+ background: var(--st-done);
+}
+.task-step[data-state="done"] .st::after {
+ content: "\2713";
+ position: absolute;
+ inset: 0;
+ color: #fff;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.task-step[data-state="failed"] .st {
+ background: var(--st-failed);
+}
+.task-step[data-state="failed"] .st::after {
+ content: "!";
+ position: absolute;
+ inset: 0;
+ color: #fff;
+ font-size: 14px;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.task-step[data-state="blocked"] .st {
+ background: var(--st-blocked);
+}
+.task-step[data-state="blocked"] .st::after {
+ content: "\f7";
+ position: absolute;
+ inset: 0;
+ color: #fff;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.task-step[data-state="skipped"] .st {
+ background: var(--st-skipped);
+}
+.task-step[data-state="skipped"] .st::after {
+ content: "\2014";
+ position: absolute;
+ inset: 0;
+ color: #fff;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.task-step .l {
+ font-size: 14px;
+ color: var(--fg);
+}
+.task-step .l .h {
+ color: var(--muted);
+ font-size: 14px;
+ margin-top: 2px;
+ font-family: inherit;
+}
+.task-step .t {
+ font-family: inherit;
+ font-size: 14px;
+ color: var(--muted);
+}
/* plan state extensions */
-.plan-row[data-state="failed"] .check { background: var(--st-failed); border-color: var(--st-failed); }
-.plan-row[data-state="blocked"] .check { background: var(--st-blocked); border-color: var(--st-blocked); }
-.plan-row[data-state="skipped"] .check { background: var(--st-skipped); border-color: var(--st-skipped); }
-.plan-row[data-state="failed"] .check::after { content:"!"; color:#fff; font-size:14px; font-weight:700; }
-.plan-row[data-state="blocked"] .check::after { content:"\f7"; color:#fff; font-size:14px; }
-.plan-row[data-state="skipped"] .check::after { content:"\2014"; color:#fff; font-size:14px; }
-.plan-row[data-state="failed"] > .body > .l { color: var(--st-failed); }
-.plan-row[data-state="blocked"] > .body > .l { color: var(--st-blocked); }
-.plan-row[data-state="skipped"] > .body > .l { color: var(--st-skipped); text-decoration: line-through; }
+.plan-row[data-state="failed"] .check {
+ background: var(--st-failed);
+ border-color: var(--st-failed);
+}
+.plan-row[data-state="blocked"] .check {
+ background: var(--st-blocked);
+ border-color: var(--st-blocked);
+}
+.plan-row[data-state="skipped"] .check {
+ background: var(--st-skipped);
+ border-color: var(--st-skipped);
+}
+.plan-row[data-state="failed"] .check::after {
+ content: "!";
+ color: #fff;
+ font-size: 14px;
+ font-weight: 700;
+}
+.plan-row[data-state="blocked"] .check::after {
+ content: "\f7";
+ color: #fff;
+ font-size: 14px;
+}
+.plan-row[data-state="skipped"] .check::after {
+ content: "\2014";
+ color: #fff;
+ font-size: 14px;
+}
+.plan-row[data-state="failed"] > .body > .l {
+ color: var(--st-failed);
+}
+.plan-row[data-state="blocked"] > .body > .l {
+ color: var(--st-blocked);
+}
+.plan-row[data-state="skipped"] > .body > .l {
+ color: var(--st-skipped);
+ text-decoration: line-through;
+}
/* ============================================================
WARN CARD
@@ -5873,10 +6390,27 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
grid-template-columns: 24px 1fr;
gap: 10px;
}
-.warn-card .ico { color: var(--tone-warn); margin-top: 1px; }
-.warn-card .tt { font-size: 14px; font-weight: 600; color: var(--fg); }
-.warn-card .ds { font-size: 14px; color: var(--fg-2); margin-top: 4px; line-height: 1.55; }
-.warn-card .ds code { background: var(--panel); padding: 1px 5px; border-radius: 3px; font-size: 14px; }
+.warn-card .ico {
+ color: var(--tone-warn);
+ margin-top: 1px;
+}
+.warn-card .tt {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--fg);
+}
+.warn-card .ds {
+ font-size: 14px;
+ color: var(--fg-2);
+ margin-top: 4px;
+ line-height: 1.55;
+}
+.warn-card .ds code {
+ background: var(--panel);
+ padding: 1px 5px;
+ border-radius: 3px;
+ font-size: 14px;
+}
/* ============================================================
TIP CARD
@@ -5888,16 +6422,28 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
padding: 12px 14px 8px;
}
.tip-card .head {
- display: flex; align-items: center; gap: 8px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
margin-bottom: 8px;
}
.tip-card .head .ico {
- width: 22px; height: 22px; border-radius: 5px;
- display: inline-flex; align-items: center; justify-content: center;
- background: var(--tone-info); color: oklch(99% 0 0);
+ width: 22px;
+ height: 22px;
+ border-radius: 5px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--tone-info);
+ color: oklch(99% 0 0);
+}
+.tip-card .head .topic {
+ font-size: 14px;
+ font-weight: 600;
+}
+.tip-card .head .grow {
+ flex: 1;
}
-.tip-card .head .topic { font-size: 14px; font-weight: 600; }
-.tip-card .head .grow { flex: 1; }
.tip-card .head .pill {
font-family: inherit;
font-size: 14px;
@@ -5906,7 +6452,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
border-radius: 4px;
color: var(--tone-info);
}
-.tip-card .sec { margin-bottom: 8px; }
+.tip-card .sec {
+ margin-bottom: 8px;
+}
.tip-card .sec .stt {
font-family: inherit;
font-size: 14px;
@@ -5946,26 +6494,47 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
overflow: hidden;
}
.doctor-card .dh {
- display: flex; align-items: center; gap: 10px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid var(--border);
}
.doctor-card .dh .ico {
- width: 24px; height: 24px; border-radius: 6px;
- background: var(--tone-info-soft); color: var(--tone-info);
- display: inline-flex; align-items: center; justify-content: center;
+ width: 24px;
+ height: 24px;
+ border-radius: 6px;
+ background: var(--tone-info-soft);
+ color: var(--tone-info);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+.doctor-card .dh .tt {
+ font-size: 14px;
+ font-weight: 600;
+}
+.doctor-card .dh .grow {
+ flex: 1;
}
-.doctor-card .dh .tt { font-size: 14px; font-weight: 600; }
-.doctor-card .dh .grow { flex: 1; }
.doctor-card .dh .summary {
font-family: inherit;
font-size: 14px;
- display: inline-flex; gap: 8px;
+ display: inline-flex;
+ gap: 8px;
+}
+.doctor-card .dh .summary .b {
+ font-weight: 600;
+}
+.doctor-card .dh .summary .ok {
+ color: var(--tone-ok);
+}
+.doctor-card .dh .summary .warn {
+ color: var(--tone-warn);
+}
+.doctor-card .dh .summary .err {
+ color: var(--tone-err);
}
-.doctor-card .dh .summary .b { font-weight: 600; }
-.doctor-card .dh .summary .ok { color: var(--tone-ok); }
-.doctor-card .dh .summary .warn { color: var(--tone-warn); }
-.doctor-card .dh .summary .err { color: var(--tone-err); }
.doctor-row {
display: grid;
grid-template-columns: 22px 1fr auto;
@@ -5974,22 +6543,46 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
border-bottom: 1px dashed var(--border);
align-items: center;
}
-.doctor-row:last-child { border-bottom: none; }
+.doctor-row:last-child {
+ border-bottom: none;
+}
.doctor-row .ic {
- width: 18px; height: 18px; border-radius: 50%;
- display: inline-flex; align-items: center; justify-content: center;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
color: oklch(99% 0 0);
- font-size: 14px; font-weight: 700;
+ font-size: 14px;
+ font-weight: 700;
+}
+.doctor-row[data-s="ok"] .ic {
+ background: var(--tone-ok);
+}
+.doctor-row[data-s="warn"] .ic {
+ background: var(--tone-warn);
+}
+.doctor-row[data-s="fail"] .ic {
+ background: var(--tone-err);
}
-.doctor-row[data-s="ok"] .ic { background: var(--tone-ok); }
-.doctor-row[data-s="warn"] .ic { background: var(--tone-warn); }
-.doctor-row[data-s="fail"] .ic { background: var(--tone-err); }
.doctor-row .ic::after {
content: attr(data-mark);
}
-.doctor-row .body .nm { font-size: 14px; }
-.doctor-row .body .sub { font-family: inherit; font-size:14px; color: var(--muted); margin-top: 2px; }
-.doctor-row .v { font-family: inherit; font-size:14px; color: var(--fg-2); }
+.doctor-row .body .nm {
+ font-size: 14px;
+}
+.doctor-row .body .sub {
+ font-family: inherit;
+ font-size: 14px;
+ color: var(--muted);
+ margin-top: 2px;
+}
+.doctor-row .v {
+ font-family: inherit;
+ font-size: 14px;
+ color: var(--fg-2);
+}
/* ============================================================
USAGE CARD — full
@@ -6003,11 +6596,23 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
.usage-full .uh {
padding: 10px 14px;
border-bottom: 1px solid var(--border);
- display: flex; gap: 8px; align-items: center;
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+.usage-full .uh .grow {
+ flex: 1;
+}
+.usage-full .uh .tt {
+ font-size: 14px;
+ font-weight: 600;
+}
+.usage-full .uh .ss {
+ font-family: inherit;
+ font-size: 14px;
+ color: var(--muted);
+ margin-top: 1px;
}
-.usage-full .uh .grow { flex: 1; }
-.usage-full .uh .tt { font-size: 14px; font-weight: 600; }
-.usage-full .uh .ss { font-family: inherit; font-size: 14px; color: var(--muted); margin-top:1px; }
.usage-full .ub {
display: grid;
grid-template-columns: repeat(4, 1fr);
@@ -6016,7 +6621,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
padding: 12px 14px;
border-right: 1px solid var(--border);
}
-.usage-full .ucol:last-child { border-right: none; }
+.usage-full .ucol:last-child {
+ border-right: none;
+}
.usage-full .ucol .l {
font-family: inherit;
font-size: 14px;
@@ -6025,34 +6632,67 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
color: var(--muted);
}
.usage-full .ucol .v {
- font-size: 17px; font-weight: 600;
+ font-size: 17px;
+ font-weight: 600;
margin-top: 4px;
letter-spacing: -0.02em;
}
-.usage-full .ucol .v.acc { color: var(--accent); }
-.usage-full .ucol .v.ok { color: var(--tone-ok); }
-.usage-full .ucol .v.vio { color: var(--violet); }
-.usage-full .ucol .pct { font-family: inherit; font-size: 14px; color: var(--muted); margin-top: 2px; }
+.usage-full .ucol .v.acc {
+ color: var(--accent);
+}
+.usage-full .ucol .v.ok {
+ color: var(--tone-ok);
+}
+.usage-full .ucol .v.vio {
+ color: var(--violet);
+}
+.usage-full .ucol .pct {
+ font-family: inherit;
+ font-size: 14px;
+ color: var(--muted);
+ margin-top: 2px;
+}
.usage-full .stack {
- display: flex; height: 6px;
+ display: flex;
+ height: 6px;
border-top: 1px solid var(--border);
}
-.usage-full .stack span { display: block; height: 100%; }
-.usage-full .stack .s1 { background: var(--accent); }
-.usage-full .stack .s2 { background: var(--violet); }
-.usage-full .stack .s3 { background: var(--tone-ok); }
-.usage-full .stack .s4 { background: var(--border-strong); }
+.usage-full .stack span {
+ display: block;
+ height: 100%;
+}
+.usage-full .stack .s1 {
+ background: var(--accent);
+}
+.usage-full .stack .s2 {
+ background: var(--violet);
+}
+.usage-full .stack .s3 {
+ background: var(--tone-ok);
+}
+.usage-full .stack .s4 {
+ background: var(--border-strong);
+}
.usage-full .uf {
- display: flex; gap: 6px; flex-wrap: wrap;
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
padding: 8px 14px;
border-top: 1px solid var(--border);
background: var(--bg-2);
- font-family: inherit; font-size: 14px;
+ font-family: inherit;
+ font-size: 14px;
color: var(--muted);
}
-.usage-full .uf .x { display:inline-flex; align-items:center; gap:4px; }
+.usage-full .uf .x {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
.usage-full .uf .x .sw {
- width: 8px; height: 8px; border-radius: 2px;
+ width: 8px;
+ height: 8px;
+ border-radius: 2px;
}
/* ============================================================
@@ -6065,7 +6705,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
overflow: hidden;
}
.mem-groups .gh {
- display: flex; align-items: center; gap: 8px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
padding: 9px 12px;
border-bottom: 1px solid var(--border);
font-family: inherit;
@@ -6075,13 +6717,26 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
color: var(--muted);
background: var(--bg-2);
}
-.mem-groups .gh.first {}
-.mem-groups .gh .sw { width: 6px; height: 6px; border-radius: 50%; }
-.mem-groups .gh[data-g="user"] .sw { background: var(--violet); }
-.mem-groups .gh[data-g="feedback"] .sw { background: var(--tone-warn); }
-.mem-groups .gh[data-g="project"] .sw { background: var(--accent); }
-.mem-groups .gh[data-g="reference"] .sw { background: var(--tone-info); }
-.mem-groups .gh .grow { flex: 1; }
+.mem-groups .gh .sw {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+}
+.mem-groups .gh[data-g="user"] .sw {
+ background: var(--violet);
+}
+.mem-groups .gh[data-g="feedback"] .sw {
+ background: var(--tone-warn);
+}
+.mem-groups .gh[data-g="project"] .sw {
+ background: var(--accent);
+}
+.mem-groups .gh[data-g="reference"] .sw {
+ background: var(--tone-info);
+}
+.mem-groups .gh .grow {
+ flex: 1;
+}
.mem-groups .gh .cnt {
background: var(--card);
border: 1px solid var(--border);
@@ -6099,10 +6754,20 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
color: var(--fg-2);
align-items: start;
}
-.mem-groups .mrow:last-child { border-bottom: none; }
-.mem-groups .mrow .b { color: var(--muted-2); }
-.mem-groups .mrow .t { line-height: 1.55; }
-.mem-groups .mrow .meta { font-family: inherit; font-size: 14px; color: var(--muted); }
+.mem-groups .mrow:last-child {
+ border-bottom: none;
+}
+.mem-groups .mrow .b {
+ color: var(--muted-2);
+}
+.mem-groups .mrow .t {
+ line-height: 1.55;
+}
+.mem-groups .mrow .meta {
+ font-family: inherit;
+ font-size: 14px;
+ color: var(--muted);
+}
/* ============================================================
SUBAGENT NESTED
@@ -6114,18 +6779,34 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
overflow: hidden;
}
.subagent-nested .sh {
- display: flex; align-items: center; gap: 8px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid var(--border);
}
.subagent-nested .sh .ico {
- width: 22px; height: 22px; border-radius: 5px;
- background: var(--violet-soft); color: var(--violet);
- display: inline-flex; align-items: center; justify-content: center;
+ width: 22px;
+ height: 22px;
+ border-radius: 5px;
+ background: var(--violet-soft);
+ color: var(--violet);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+.subagent-nested .sh .nm {
+ font-size: 14px;
+ font-weight: 600;
+}
+.subagent-nested .sh .ss {
+ font-family: inherit;
+ font-size: 14px;
+ color: var(--muted);
+}
+.subagent-nested .sh .grow {
+ flex: 1;
}
-.subagent-nested .sh .nm { font-size: 14px; font-weight: 600; }
-.subagent-nested .sh .ss { font-family: inherit; font-size: 14px; color: var(--muted); }
-.subagent-nested .sh .grow { flex: 1; }
.subagent-nested .nest {
border-left: 2px solid var(--violet);
margin-left: 22px;
@@ -6147,20 +6828,61 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
color: var(--muted);
margin-bottom: 4px;
}
-.subagent-nested .nest .lab .dot {
- display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 5px; vertical-align: middle;
-}
-.subagent-nested .nest .lab.reason .dot { background: var(--violet); }
-.subagent-nested .nest .lab.tool .dot { background: var(--accent); }
-.subagent-nested .nest .lab.stream .dot { background: var(--tone-info); }
-.subagent-nested .nest .lab.diff .dot { background: var(--tone-ok); }
-.subagent-nested .nest .lab.err .dot { background: var(--tone-err); }
-.subagent-nested .nest .txt { color: var(--fg-2); line-height: 1.55; }
-.subagent-nested .nest .txt code { background: var(--panel); padding:1px 5px; border-radius:3px; font-size:14px; }
-.subagent-nested .nest .mono { font-family: "Geist Mono", monospace; font-size: 14px; color: var(--fg-2); white-space: pre; }
-.subagent-nested .nest .diffbox { font-family: "Geist Mono", monospace; font-size: 14px; }
-.subagent-nested .nest .diffbox .add { color: var(--tone-ok); background: oklch(72% 0.16 152 / 0.08); padding: 0 4px; }
-.subagent-nested .nest .diffbox .del { color: var(--tone-err); background: oklch(68% 0.20 25 / 0.08); padding: 0 4px; text-decoration: line-through; opacity: 0.85; }
+.subagent-nested .nest .lab .dot {
+ display: inline-block;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ margin-right: 5px;
+ vertical-align: middle;
+}
+.subagent-nested .nest .lab.reason .dot {
+ background: var(--violet);
+}
+.subagent-nested .nest .lab.tool .dot {
+ background: var(--accent);
+}
+.subagent-nested .nest .lab.stream .dot {
+ background: var(--tone-info);
+}
+.subagent-nested .nest .lab.diff .dot {
+ background: var(--tone-ok);
+}
+.subagent-nested .nest .lab.err .dot {
+ background: var(--tone-err);
+}
+.subagent-nested .nest .txt {
+ color: var(--fg-2);
+ line-height: 1.55;
+}
+.subagent-nested .nest .txt code {
+ background: var(--panel);
+ padding: 1px 5px;
+ border-radius: 3px;
+ font-size: 14px;
+}
+.subagent-nested .nest .mono {
+ font-family: "Geist Mono", monospace;
+ font-size: 14px;
+ color: var(--fg-2);
+ white-space: pre;
+}
+.subagent-nested .nest .diffbox {
+ font-family: "Geist Mono", monospace;
+ font-size: 14px;
+}
+.subagent-nested .nest .diffbox .add {
+ color: var(--tone-ok);
+ background: oklch(72% 0.16 152 / 0.08);
+ padding: 0 4px;
+}
+.subagent-nested .nest .diffbox .del {
+ color: var(--tone-err);
+ background: oklch(68% 0.2 25 / 0.08);
+ padding: 0 4px;
+ text-decoration: line-through;
+ opacity: 0.85;
+}
/* ============================================================
CODE SEARCH
@@ -6174,17 +6896,29 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
.code-search .ch {
padding: 9px 12px;
border-bottom: 1px solid var(--border);
- display: flex; align-items: center; gap: 8px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
}
.code-search .ch .pat {
font-family: inherit;
font-size: 14px;
color: var(--accent);
}
-.code-search .ch .grow { flex: 1; }
-.code-search .ch .stat { font-family: inherit; font-size: 14px; color: var(--muted); }
-.code-search .file-block { border-bottom: 1px solid var(--border); }
-.code-search .file-block:last-child { border-bottom: none; }
+.code-search .ch .grow {
+ flex: 1;
+}
+.code-search .ch .stat {
+ font-family: inherit;
+ font-size: 14px;
+ color: var(--muted);
+}
+.code-search .file-block {
+ border-bottom: 1px solid var(--border);
+}
+.code-search .file-block:last-child {
+ border-bottom: none;
+}
.code-search .fh {
padding: 6px 12px;
background: var(--bg-2);
@@ -6194,7 +6928,10 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
display: flex;
gap: 8px;
}
-.code-search .fh .n { color: var(--muted); margin-left: auto; }
+.code-search .fh .n {
+ color: var(--muted);
+ margin-left: auto;
+}
.code-search .hit {
display: grid;
grid-template-columns: 44px 1fr;
@@ -6204,9 +6941,19 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
padding: 3px 12px;
align-items: center;
}
-.code-search .hit:hover { background: var(--bg-2); }
-.code-search .hit .ln { color: var(--muted-2); text-align: right; }
-.code-search .hit .ct { color: var(--fg-2); white-space: pre; overflow: hidden; text-overflow: ellipsis; }
+.code-search .hit:hover {
+ background: var(--bg-2);
+}
+.code-search .hit .ln {
+ color: var(--muted-2);
+ text-align: right;
+}
+.code-search .hit .ct {
+ color: var(--fg-2);
+ white-space: pre;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
.code-search .hit .ct mark {
background: oklch(78% 0.16 80 / 0.35);
color: var(--fg);
@@ -6224,22 +6971,49 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
padding: 12px 14px 10px;
}
.ctx-card .h {
- display: flex; align-items: baseline; gap: 6px;
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
margin-bottom: 8px;
}
-.ctx-card .h .tt { font-size: 14px; font-weight: 600; }
-.ctx-card .h .grow { flex: 1; }
-.ctx-card .h .v { font-family: inherit; font-size: 14px; color: var(--fg); }
-.ctx-card .h .v .mut { color: var(--muted); }
+.ctx-card .h .tt {
+ font-size: 14px;
+ font-weight: 600;
+}
+.ctx-card .h .grow {
+ flex: 1;
+}
+.ctx-card .h .v {
+ font-family: inherit;
+ font-size: 14px;
+ color: var(--fg);
+}
+.ctx-card .h .v .mut {
+ color: var(--muted);
+}
.ctx-card .bar {
- display: flex; height: 8px; border-radius: 999px; overflow: hidden;
+ display: flex;
+ height: 8px;
+ border-radius: 999px;
+ overflow: hidden;
background: var(--panel);
}
-.ctx-card .bar span { display: block; height: 100%; }
-.ctx-card .bar .system { background: var(--accent); }
-.ctx-card .bar .tools { background: var(--violet); }
-.ctx-card .bar .log { background: var(--tone-ok); }
-.ctx-card .bar .input { background: var(--tone-warn); }
+.ctx-card .bar span {
+ display: block;
+ height: 100%;
+}
+.ctx-card .bar .system {
+ background: var(--accent);
+}
+.ctx-card .bar .tools {
+ background: var(--violet);
+}
+.ctx-card .bar .log {
+ background: var(--tone-ok);
+}
+.ctx-card .bar .input {
+ background: var(--tone-warn);
+}
.ctx-card .legend {
display: grid;
grid-template-columns: repeat(4, 1fr);
@@ -6248,10 +7022,23 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
font-family: inherit;
font-size: 14px;
}
-.ctx-card .legend > div { display: flex; gap: 5px; align-items: center; }
-.ctx-card .legend .sw { width: 8px; height: 8px; border-radius: 2px; }
-.ctx-card .legend .l { color: var(--muted); }
-.ctx-card .legend .v { color: var(--fg); margin-left: auto; }
+.ctx-card .legend > div {
+ display: flex;
+ gap: 5px;
+ align-items: center;
+}
+.ctx-card .legend .sw {
+ width: 8px;
+ height: 8px;
+ border-radius: 2px;
+}
+.ctx-card .legend .l {
+ color: var(--muted);
+}
+.ctx-card .legend .v {
+ color: var(--fg);
+ margin-left: auto;
+}
.ctx-card .ttop {
margin-top: 12px;
padding-top: 9px;
@@ -6274,10 +7061,24 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
font-size: 14px;
align-items: center;
}
-.ctx-card .ttop .row .n { color: var(--fg-2); }
-.ctx-card .ttop .row .bbar { background: var(--panel); border-radius:2px; height: 4px; overflow:hidden; }
-.ctx-card .ttop .row .bbar span { display:block; height:100%; background: var(--violet); }
-.ctx-card .ttop .row .v { color: var(--muted); text-align: right; }
+.ctx-card .ttop .row .n {
+ color: var(--fg-2);
+}
+.ctx-card .ttop .row .bbar {
+ background: var(--panel);
+ border-radius: 2px;
+ height: 4px;
+ overflow: hidden;
+}
+.ctx-card .ttop .row .bbar span {
+ display: block;
+ height: 100%;
+ background: var(--violet);
+}
+.ctx-card .ttop .row .v {
+ color: var(--muted);
+ text-align: right;
+}
/* ============================================================
FALLBACK
@@ -6297,10 +7098,21 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
font-size: 14px;
color: var(--muted);
}
-.fallback-card .hd { color: var(--fg-2); margin-bottom: 4px; }
-.fallback-card .kv { display: grid; grid-template-columns: 70px 1fr; gap: 4px 12px; }
-.fallback-card .kv .k { color: var(--muted-2); }
-.fallback-card .kv .v { color: var(--fg-2); }
+.fallback-card .hd {
+ color: var(--fg-2);
+ margin-bottom: 4px;
+}
+.fallback-card .kv {
+ display: grid;
+ grid-template-columns: 70px 1fr;
+ gap: 4px 12px;
+}
+.fallback-card .kv .k {
+ color: var(--muted-2);
+}
+.fallback-card .kv .v {
+ color: var(--fg-2);
+}
/* ============================================================
LIVE CARD VARIANTS
@@ -6319,37 +7131,86 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
max-width: 100%;
}
.live-card .lc-ico {
- width: 16px; height: 16px;
- display: inline-flex; align-items: center; justify-content: center;
+ width: 16px;
+ height: 16px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--lc-color);
+}
+.live-card .lc-body {
+ color: var(--fg-2);
+ font-size: 14px;
+}
+.live-card .lc-body .b {
color: var(--lc-color);
+ font-weight: 600;
+}
+.live-card .lc-act {
+ color: var(--lc-color);
+ padding: 0 6px;
+}
+.live-card .lc-act:hover {
+ background: var(--lc-soft);
+ border-radius: 4px;
+}
+
+.live-card[data-v="thinking"] {
+ --lc-color: var(--violet);
+ --lc-soft: var(--violet-soft);
+}
+.live-card[data-v="undo"] {
+ --lc-color: var(--tone-info);
+ --lc-soft: var(--tone-info-soft);
+}
+.live-card[data-v="ctxPressure"] {
+ --lc-color: var(--tone-warn);
+ --lc-soft: var(--tone-warn-soft);
+}
+.live-card[data-v="aborted"] {
+ --lc-color: var(--st-aborted);
+ --lc-soft: oklch(60% 0.13 25 / 0.1);
+}
+.live-card[data-v="retry"] {
+ --lc-color: var(--tone-warn);
+ --lc-soft: var(--tone-warn-soft);
+}
+.live-card[data-v="checkpoint"] {
+ --lc-color: var(--tone-ok);
+ --lc-soft: var(--tone-ok-soft);
+}
+.live-card[data-v="stepProgress"] {
+ --lc-color: var(--accent);
+ --lc-soft: var(--accent-soft);
+}
+.live-card[data-v="mcpEvent"] {
+ --lc-color: var(--violet);
+ --lc-soft: var(--violet-soft);
+}
+.live-card[data-v="sessionOp"] {
+ --lc-color: var(--tone-ghost);
+ --lc-soft: var(--tone-ghost-soft);
}
-.live-card .lc-body { color: var(--fg-2); font-size: 14px; }
-.live-card .lc-body .b { color: var(--lc-color); font-weight: 600; }
-.live-card .lc-act { color: var(--lc-color); padding: 0 6px; }
-.live-card .lc-act:hover { background: var(--lc-soft); border-radius: 4px; }
-
-.live-card[data-v="thinking"] { --lc-color: var(--violet); --lc-soft: var(--violet-soft); }
-.live-card[data-v="undo"] { --lc-color: var(--tone-info); --lc-soft: var(--tone-info-soft); }
-.live-card[data-v="ctxPressure"] { --lc-color: var(--tone-warn); --lc-soft: var(--tone-warn-soft); }
-.live-card[data-v="aborted"] { --lc-color: var(--st-aborted); --lc-soft: oklch(60% 0.13 25 / 0.10); }
-.live-card[data-v="retry"] { --lc-color: var(--tone-warn); --lc-soft: var(--tone-warn-soft); }
-.live-card[data-v="checkpoint"] { --lc-color: var(--tone-ok); --lc-soft: var(--tone-ok-soft); }
-.live-card[data-v="stepProgress"]{ --lc-color: var(--accent); --lc-soft: var(--accent-soft); }
-.live-card[data-v="mcpEvent"] { --lc-color: var(--violet); --lc-soft: var(--violet-soft); }
-.live-card[data-v="sessionOp"] { --lc-color: var(--tone-ghost); --lc-soft: var(--tone-ghost-soft); }
.live-row {
- display: flex; flex-wrap: wrap; gap: 6px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
}
.live-card.step .lc-meter {
- width: 60px; height: 4px;
+ width: 60px;
+ height: 4px;
background: var(--panel);
border-radius: 999px;
overflow: hidden;
margin-left: 4px;
}
-.live-card.step .lc-meter > span { display: block; height: 100%; background: var(--accent); }
+.live-card.step .lc-meter > span {
+ display: block;
+ height: 100%;
+ background: var(--accent);
+}
/* tone palette gallery */
.tone-gallery {
@@ -6370,8 +7231,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
letter-spacing: 0.04em;
text-transform: uppercase;
}
-[data-theme="light"] .tone-gallery .sw { color: oklch(100% 0 0); }
-
+[data-theme="light"] .tone-gallery .sw {
+ color: oklch(100% 0 0);
+}
/* horizontal-cramp fixes */
.main-head h1 {
@@ -6401,7 +7263,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
overflow-x: auto;
scrollbar-width: none;
}
-.statusbar::-webkit-scrollbar { display: none; }
+.statusbar::-webkit-scrollbar {
+ display: none;
+}
/* ============================================================
Composer narrow-width adaptation
============================================================ */
@@ -6454,14 +7318,35 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
/* Collapse composer-foot when the composer column itself is cramped —
container query so panel state, not viewport width, drives it. */
@container composer (max-width: 620px) {
- .composer-foot .cf-btn .label { display: none; }
+ .composer-foot .cf-btn .label {
+ display: none;
+ }
+}
+@container composer (max-width: 560px) {
+ .composer-foot .composer-secondary-action {
+ display: none;
+ }
+ .composer-tools-more {
+ display: inline-flex;
+ }
}
@container composer (max-width: 520px) {
- .hint-row > span:first-child { display: none; }
- .composer-foot .model-pill { max-width: 140px; }
+ .hint-row > span:first-child {
+ display: none;
+ }
+ .composer-foot .model-pill {
+ max-width: 140px;
+ }
+}
+@container composer (max-width: 460px) {
+ .composer-model-direct {
+ display: none;
+ }
}
@container composer (max-width: 420px) {
- .composer-foot .model-pill { max-width: 100px; }
+ .composer-foot .model-pill {
+ max-width: 100px;
+ }
}
/* Main head — same compression behavior */
@@ -6469,7 +7354,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
white-space: nowrap;
}
@media (max-width: 1000px) {
- .main-head .h-btn:not(.primary) span:not(:first-child) { display: none; }
+ .main-head .h-btn:not(.primary) span:not(:first-child) {
+ display: none;
+ }
}
/* ============================================================
@@ -6487,12 +7374,17 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
}
.mode-switch[data-mode="yolo"] {
border-color: var(--tone-err);
- box-shadow: 0 0 0 2px oklch(68% 0.20 25 / 0.18);
+ box-shadow: 0 0 0 2px oklch(68% 0.2 25 / 0.18);
animation: yolo-pulse 1.8s ease-in-out infinite;
}
@keyframes yolo-pulse {
- 0%, 100% { box-shadow: 0 0 0 2px oklch(68% 0.20 25 / 0.18); }
- 50% { box-shadow: 0 0 0 3px oklch(68% 0.20 25 / 0.32); }
+ 0%,
+ 100% {
+ box-shadow: 0 0 0 2px oklch(68% 0.2 25 / 0.18);
+ }
+ 50% {
+ box-shadow: 0 0 0 3px oklch(68% 0.2 25 / 0.32);
+ }
}
.ms-seg {
display: inline-flex;
@@ -6504,25 +7396,33 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
color: var(--muted);
transition: background 0.12s ease, color 0.12s ease;
}
-.ms-seg:hover { color: var(--fg); }
+.ms-seg:hover {
+ color: var(--fg);
+}
.ms-seg[data-on="true"][data-k="review"] {
- background: var(--tone-info-soft); color: var(--tone-info);
+ background: var(--tone-info-soft);
+ color: var(--tone-info);
}
.ms-seg[data-on="true"][data-k="plan"] {
- background: var(--tone-warn-soft); color: var(--tone-warn);
+ background: var(--tone-warn-soft);
+ color: var(--tone-warn);
}
.ms-seg[data-on="true"][data-k="auto"] {
- background: var(--accent-soft); color: var(--accent);
+ background: var(--accent-soft);
+ color: var(--accent);
}
.ms-seg[data-on="true"][data-k="yolo"] {
- background: var(--tone-err); color: oklch(99% 0 0);
+ background: var(--tone-err);
+ color: oklch(99% 0 0);
+}
+[data-theme="light"] .ms-seg[data-on="true"][data-k="yolo"] {
+ color: oklch(100% 0 0);
}
-[data-theme="light"] .ms-seg[data-on="true"][data-k="yolo"] { color: oklch(100% 0 0); }
/* YOLO toast variant */
.toast-yolo {
border-color: var(--tone-err);
- box-shadow: 0 0 0 1px oklch(68% 0.20 25 / 0.18), var(--shadow-lg);
+ box-shadow: 0 0 0 1px oklch(68% 0.2 25 / 0.18), var(--shadow-lg);
display: flex;
align-items: center;
gap: 8px;
@@ -6544,8 +7444,12 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
/* Hide mode labels when narrow; keep icons */
@media (max-width: 1100px) {
- .mode-switch .ms-seg > span { display: none; }
- .mode-switch .ms-seg { padding: 4px 7px; }
+ .mode-switch .ms-seg > span {
+ display: none;
+ }
+ .mode-switch .ms-seg {
+ padding: 4px 7px;
+ }
}
/* ============================================================
@@ -6569,8 +7473,14 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
white-space: nowrap;
}
@keyframes toast-rise {
- from { opacity: 0; transform: translateY(8px); }
- to { opacity: 1; transform: none; }
+ from {
+ opacity: 0;
+ transform: translateY(8px);
+ }
+ to {
+ opacity: 1;
+ transform: none;
+ }
}
/* ============================================================
@@ -6588,7 +7498,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
padding-top: 12vh;
animation: fade-in 0.18s ease-out;
}
-[data-theme="light"] .cmdk-mask { background: oklch(0% 0 0 / 0.2); }
+[data-theme="light"] .cmdk-mask {
+ background: oklch(0% 0 0 / 0.2);
+}
.cmdk {
width: min(640px, 92vw);
max-height: 70vh;
@@ -6630,7 +7542,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
overflow-y: auto;
padding: 4px 0;
}
-.cmdk-group { padding: 4px 0; }
+.cmdk-group {
+ padding: 4px 0;
+}
.cmdk-gh {
padding: 6px 14px 2px;
font-family: inherit;
@@ -6644,8 +7558,13 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
grid-template-columns: 22px 1fr auto auto;
gap: 10px;
align-items: center;
+ width: 100%;
padding: 7px 14px;
+ border: 0;
+ background: transparent;
+ font: inherit;
font-size: 14px;
+ text-align: left;
cursor: pointer;
color: var(--fg-2);
}
@@ -6653,7 +7572,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
background: var(--accent-soft);
color: var(--fg);
}
-.cmdk-row[data-active="true"] .ic { color: var(--accent); }
+.cmdk-row[data-active="true"] .ic {
+ color: var(--accent);
+}
.cmdk-row .ic {
color: var(--muted);
display: inline-flex;
@@ -6680,7 +7601,10 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
padding: 1px 5px;
border-radius: 4px;
}
-.cmdk-row .kb-empty { display: inline-block; width: 1px; }
+.cmdk-row .kb-empty {
+ display: inline-block;
+ width: 1px;
+}
.cmdk-empty {
padding: 24px;
text-align: center;
@@ -6752,16 +7676,38 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
}
.wd-row {
display: grid;
- grid-template-columns: 20px 1fr auto;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ padding: 0 8px 0 0;
+}
+.wd-row:hover {
+ background: var(--bg-2);
+}
+.wd-row-main {
+ display: grid;
+ grid-template-columns: 20px minmax(0, 1fr);
gap: 8px;
align-items: center;
- padding: 7px 12px;
+ min-width: 0;
+ width: 100%;
+ padding: 7px 0 7px 12px;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ text-align: left;
cursor: pointer;
}
-.wd-row:hover { background: var(--bg-2); }
-.wd-row .ic { color: var(--muted); display: inline-flex; }
-.wd-row .b { min-width: 0; }
-.wd-row .b .p {
+.wd-row-main .ic {
+ color: var(--muted);
+ display: inline-flex;
+}
+.wd-row-main .b {
+ min-width: 0;
+ display: block;
+}
+.wd-row-main .b .p {
+ display: block;
font-size: 14px;
font-family: inherit;
color: var(--fg);
@@ -6769,7 +7715,8 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
overflow: hidden;
text-overflow: ellipsis;
}
-.wd-row .b .br {
+.wd-row-main .b .br {
+ display: block;
font-family: inherit;
font-size: 14px;
color: var(--muted);
@@ -6778,7 +7725,9 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
text-overflow: ellipsis;
margin-top: 1px;
}
-.wd-row .pin { color: var(--accent); }
+.wd-row .pin {
+ color: var(--accent);
+}
.wd-row .wd-del {
background: transparent;
border: none;
@@ -6796,7 +7745,7 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
opacity: 1;
}
.wd-row .wd-del:hover {
- background: var(--bg-3, rgba(255,255,255,0.08));
+ background: var(--bg-3, rgba(255, 255, 255, 0.08));
color: var(--tone-err, #ff3b30);
}
.wd-foot {
@@ -6830,6 +7779,13 @@ html:not([data-platform="macos"]) .cmdk-row .kb .shortcut kbd[data-key="mod"] {
.main-head .sub .ws-crumb:hover {
color: var(--accent);
}
+.main-head .sub .ws-crumb {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ color: inherit;
+ cursor: pointer;
+}
/* ============================================================
Splash — opening intro (sub-sea drift, "Reasonix" reveal)
@@ -6923,7 +7879,8 @@ html[data-platform="macos"] .splash {
animation-delay: 0.32s;
}
@keyframes splash-pulse {
- 0%, 100% {
+ 0%,
+ 100% {
opacity: 0.25;
transform: scale(0.8);
}
@@ -7052,16 +8009,34 @@ html[data-platform="macos"] .splash {
.jobs-body > .job-row:first-child {
border-top: none;
}
+.jr-line {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: stretch;
+}
+.jr-line:hover {
+ background: var(--bg-2);
+}
.jr-main {
display: grid;
- grid-template-columns: 22px auto 1fr 60px auto;
+ grid-template-columns: 22px auto minmax(0, 1fr) 60px;
gap: 10px;
align-items: center;
+ width: 100%;
padding: 10px 14px;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ text-align: left;
cursor: pointer;
}
.jr-main:hover {
- background: var(--bg-2);
+ background: transparent;
+}
+.jr-main:focus-visible {
+ outline: 1px solid var(--accent);
+ outline-offset: -2px;
}
.jr-state {
display: inline-flex;
@@ -7124,6 +8099,7 @@ html[data-platform="macos"] .splash {
display: inline-flex;
gap: 4px;
align-items: center;
+ padding-right: 14px;
}
.jr-exit {
font-family: inherit;
@@ -7279,8 +8255,14 @@ html[data-platform="macos"] .splash {
}
@keyframes slide-down {
- from { opacity: 0; transform: translateY(-6px); }
- to { opacity: 1; transform: translateY(0); }
+ from {
+ opacity: 0;
+ transform: translateY(-6px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
.msg-approval {
@@ -7288,8 +8270,14 @@ html[data-platform="macos"] .splash {
}
@keyframes card-in {
- from { opacity: 0; transform: translateY(4px) scale(0.98); }
- to { opacity: 1; transform: translateY(0) scale(1); }
+ from {
+ opacity: 0;
+ transform: translateY(4px) scale(0.98);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
}
.toast {
@@ -7297,8 +8285,14 @@ html[data-platform="macos"] .splash {
}
@keyframes toast-fall {
- from { opacity: 1; transform: translateY(0); }
- to { opacity: 0; transform: translateY(12px); }
+ from {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateY(12px);
+ }
}
.statusbar .seg {
diff --git a/desktop/src/ui/about.tsx b/desktop/src/ui/about.tsx
index c475acc53..f1023419a 100644
--- a/desktop/src/ui/about.tsx
+++ b/desktop/src/ui/about.tsx
@@ -49,9 +49,19 @@ export function AboutModal({ onClose }: { onClose: () => void }) {
}, []);
return (
-
- e.stopPropagation()}>
-
+ {
+ if (e.target === e.currentTarget) onClose();
+ }}
+ >
+
+
@@ -88,10 +98,7 @@ export function AboutModal({ onClose }: { onClose: () => void }) {
);
}
-function CheckStatus({
- check,
- onOpenReleases,
-}: { check: CheckState; onOpenReleases: () => void }) {
+function CheckStatus({ check, onOpenReleases }: { check: CheckState; onOpenReleases: () => void }) {
if (check.kind === "idle" || check.kind === "checking") return null;
if (check.kind === "up-to-date") {
return (
diff --git a/desktop/src/ui/cards.tsx b/desktop/src/ui/cards.tsx
index 6dbde8d36..db2533ee1 100644
--- a/desktop/src/ui/cards.tsx
+++ b/desktop/src/ui/cards.tsx
@@ -1,11 +1,55 @@
-import { memo, useState, type ReactNode } from "react";
-import { I } from "../icons";
+import { type ReactNode, memo, useState } from "react";
import { Markdown } from "../Markdown";
import { t, useLang } from "../i18n";
+import { I } from "../icons";
import { Shortcut } from "./shortcut";
type Tone = "default" | "success" | "warning" | "danger" | "accent" | "violet";
+function hashString(value: string): string {
+ let hash = 0;
+ for (let i = 0; i < value.length; i++) {
+ hash = (hash * 31 + value.charCodeAt(i)) | 0;
+ }
+ return `${value.length}-${(hash >>> 0).toString(36)}`;
+}
+
+function keyed(items: readonly T[], keyFor: (item: T) => string): { item: T; key: string }[] {
+ const seen = new Map();
+ return items.map((item) => {
+ const base = keyFor(item);
+ const count = seen.get(base) ?? 0;
+ seen.set(base, count + 1);
+ return { item, key: count === 0 ? base : `${base}-${count}` };
+ });
+}
+
+function renderReasoningInline(text: string): ReactNode[] {
+ const out: ReactNode[] = [];
+ const re = /`([^`]+)`|\*\*([^*]+)\*\*/g;
+ let last = 0;
+ let match: RegExpExecArray | null = re.exec(text);
+ while (match) {
+ if (match.index > last) out.push(text.slice(last, match.index));
+ const code = match[1];
+ const strong = match[2];
+ const raw = match[0];
+ if (code !== undefined) {
+ out.push(
+
+ {code}
+ ,
+ );
+ } else if (strong !== undefined) {
+ out.push({strong});
+ }
+ last = match.index + raw.length;
+ match = re.exec(text);
+ }
+ if (last < text.length) out.push(text.slice(last));
+ return out;
+}
+
export function Card({
tone = "default",
icon,
@@ -70,15 +114,25 @@ export type PlanItem = {
note?: string;
};
-function derivePlanBadge(items: PlanItem[]): { state: "running" | "done" | "failed" | "waiting" | "blocked"; label: string } {
- if (items.some((x) => x.status === "failed")) return { state: "failed", label: t("planBadge.failed") };
- if (items.some((x) => x.status === "blocked")) return { state: "blocked", label: t("planBadge.blocked") };
- if (items.some((x) => x.status === "active")) return { state: "running", label: t("planBadge.running") };
- if (items.length > 0 && items.every((x) => x.status === "done")) return { state: "done", label: t("planBadge.done") };
+function derivePlanBadge(items: PlanItem[]): {
+ state: "running" | "done" | "failed" | "waiting" | "blocked";
+ label: string;
+} {
+ if (items.some((x) => x.status === "failed"))
+ return { state: "failed", label: t("planBadge.failed") };
+ if (items.some((x) => x.status === "blocked"))
+ return { state: "blocked", label: t("planBadge.blocked") };
+ if (items.some((x) => x.status === "active"))
+ return { state: "running", label: t("planBadge.running") };
+ if (items.length > 0 && items.every((x) => x.status === "done"))
+ return { state: "done", label: t("planBadge.done") };
return { state: "waiting", label: t("planBadge.pending") };
}
-function StatusIcon({ state, label }: { state: "running" | "done" | "failed" | "waiting" | "blocked"; label: string }) {
+function StatusIcon({
+ state,
+ label,
+}: { state: "running" | "done" | "failed" | "waiting" | "blocked"; label: string }) {
switch (state) {
case "running":
return ;
@@ -127,7 +181,9 @@ export function PlanCardView({ items, title }: { items: PlanItem[]; title?: stri
) : null}
- {it.status === "active" ? : null}
+
+ {it.status === "active" ? : null}
+
))}
@@ -151,6 +207,7 @@ export function ReasoningCard({
model?: string;
}) {
useLang();
+ const paragraphs = keyed(text.split(/\n\n+/), hashString);
return (
- {text.split(/\n\n+/).map((para, i) => (
- $1')
- .replace(/\*\*([^*]+)\*\*/g, "$1"),
- }}
- />
+ {paragraphs.map(({ item: para, key }) => (
+
{renderReasoningInline(para)}
))}
{model || tokens !== undefined ? (
@@ -231,6 +281,7 @@ export function ShellCard({
}) {
useLang();
const tone: Tone = state === "failed" ? "danger" : state === "done" ? "success" : "warning";
+ const outputLines = output ? keyed(output.split("\n"), hashString) : [];
return (
{output ? (
- {output.split("\n").map((ln, i) => {
+ {outputLines.map(({ item: ln, key }) => {
if (ln.startsWith(" ✓") || ln.startsWith("✓"))
return (
-
+
{ln}
);
if (ln.startsWith(" ✗") || ln.startsWith("✗") || /error/i.test(ln))
return (
-
+
{ln}
);
- return {ln};
+ return {ln};
})}
) : null}
@@ -484,6 +535,7 @@ export function DiffCard({
useLang();
const adds = lines.filter((x) => x.t === "add").length;
const rms = lines.filter((x) => x.t === "rm").length;
+ const keyedLines = keyed(lines, (line) => hashString(JSON.stringify(line)));
return (
- {lines.map((ln, i) => {
+ {keyedLines.map(({ item: ln, key }) => {
if (ln.t === "hunk")
return (
-
+
{ln.s}
);
@@ -515,7 +567,7 @@ export function DiffCard({
const l = ln.t === "ctx" ? ln.l : ln.t === "rm" ? ln.l : undefined;
const r = ln.t === "ctx" ? ln.r : ln.t === "add" ? ln.r : undefined;
return (
-
+
{l ?? ""}
{r ?? ""}
@@ -552,7 +604,11 @@ export function DiffCard({
// ---- Error ----
-export function ErrorCard({ message, hint, code }: { message: string; hint?: ReactNode; code?: string }) {
+export function ErrorCard({
+ message,
+ hint,
+ code,
+}: { message: string; hint?: ReactNode; code?: string }) {
useLang();
return (
`${hashString(r.url)}-${hashString(r.title)}-${hashString(r.snippet)}`,
+ );
return (
"{query}"
- {results.length} {t("cards.hits")}
+
+ {results.length} {t("cards.hits")}
+
>
}
>
- {results.map((r, i) => (
-
+ {keyedResults.map(({ item: r, key }) => (
+
{r.url}
@@ -625,6 +687,10 @@ export function SubagentCard({
}) {
useLang();
const done = children.filter((c) => c.status === "done").length;
+ const keyedChildren = keyed(
+ children,
+ (child) => `${child.avatar}-${child.what}-${child.role}-${child.status}`,
+ );
return (
- {children.map((c, i) => (
-
+ {keyedChildren.map(({ item: c, key }) => (
+
{c.avatar}
{c.what}
@@ -674,17 +740,22 @@ export type MemRow = { scope: string; txt: string };
export function MemoryCard({ rows }: { rows: MemRow[] }) {
useLang();
+ const keyedRows = keyed(rows, (m) => `${m.scope}-${m.txt}`);
return (
}
kind="memory"
name={t("cards.memoryName")}
- meta={+ {rows.length} {t("cards.memoryCountSuffix")}}
+ meta={
+
+ + {rows.length} {t("cards.memoryCountSuffix")}
+
+ }
>
- {rows.map((m, i) => (
-
+ {keyedRows.map(({ item: m, key }) => (
+
{m.scope}
{m.txt}
diff --git a/desktop/src/ui/composer.tsx b/desktop/src/ui/composer.tsx
index bfa8bb503..e1d06280e 100644
--- a/desktop/src/ui/composer.tsx
+++ b/desktop/src/ui/composer.tsx
@@ -1,3 +1,5 @@
+import { invoke } from "@tauri-apps/api/core";
+import { open as openFileDialog } from "@tauri-apps/plugin-dialog";
import {
type ChangeEvent,
type KeyboardEvent,
@@ -9,14 +11,9 @@ import {
useState,
} from "react";
import type React from "react";
-import { invoke } from "@tauri-apps/api/core";
-import { open as openFileDialog } from "@tauri-apps/plugin-dialog";
-import { t, type TKey } from "../i18n";
+import { type TKey, t } from "../i18n";
import { I } from "../icons";
-import {
- DEFAULT_COMPOSER_ROWS,
- applyComposerTextareaAutosize,
-} from "./composer-sizing";
+import { DEFAULT_COMPOSER_ROWS, applyComposerTextareaAutosize } from "./composer-sizing";
import { fmtElapsed } from "./live";
import { Shortcut } from "./shortcut";
@@ -29,7 +26,12 @@ const EFFORTS: readonly ReasoningEffort[] = ["low", "medium", "high", "max"];
const MODE_INFO: ModeEntry[] = [
{ k: "plan", label: "editMode.plan", icon: , hint: "editMode.planHint" },
- { k: "review", label: "editMode.review", icon: , hint: "editMode.reviewHint" },
+ {
+ k: "review",
+ label: "editMode.review",
+ icon: ,
+ hint: "editMode.reviewHint",
+ },
{ k: "auto", label: "editMode.auto", icon: , hint: "editMode.autoHint" },
{ k: "yolo", label: "editMode.yolo", icon: , hint: "editMode.yoloHint" },
];
@@ -74,14 +76,9 @@ export type MentionItem = {
desc?: string;
};
-export type Chip =
- | { kind: "at"; label: string }
- | { kind: "slash"; label: string };
+export type Chip = { kind: "at"; label: string } | { kind: "slash"; label: string };
-type Popup =
- | { kind: "slash"; query: string }
- | { kind: "at"; query: string; nonce: number }
- | null;
+type Popup = { kind: "slash"; query: string } | { kind: "at"; query: string; nonce: number } | null;
function slashIcon(cmd: string) {
const m: Record = {
@@ -193,8 +190,10 @@ export function Composer({
const [popup, setPopup] = useState(null);
const [activeIdx, setActiveIdx] = useState(0);
const [modelMenuOpen, setModelMenuOpen] = useState(false);
+ const [toolsMenuOpen, setToolsMenuOpen] = useState(false);
const nonceRef = useRef(0);
const modelWrapRef = useRef(null);
+ const toolsWrapRef = useRef(null);
// macOS Chinese IME fires compositionend BEFORE the confirm keydown.
const composingRef = useRef(false);
const compositionEndedAtRef = useRef(0);
@@ -203,6 +202,23 @@ export function Composer({
const historyRef = useRef(initialHistory ? [...initialHistory].reverse() : []);
const [browseIdx, setBrowseIdx] = useState(-1);
const savedDraftRef = useRef("");
+ const queuedSendRows = useMemo(() => {
+ const seen = new Map();
+ return (queuedSends ?? []).map((text, index) => {
+ const occurrence = seen.get(text) ?? 0;
+ seen.set(text, occurrence + 1);
+ return { key: `${text}-${occurrence}`, text, index };
+ });
+ }, [queuedSends]);
+ const chipRows = useMemo(() => {
+ const seen = new Map();
+ return chips.map((chip, index) => {
+ const base = `${chip.kind}-${chip.label}`;
+ const occurrence = seen.get(base) ?? 0;
+ seen.set(base, occurrence + 1);
+ return { key: `${base}-${occurrence}`, chip, index };
+ });
+ }, [chips]);
// `initialHistory` arrives asynchronously (settings load after mount).
// Sync historyRef when it first becomes available and the user hasn't
@@ -218,9 +234,7 @@ export function Composer({
workspaceDir && picked.startsWith(workspaceDir)
? picked.slice(workspaceDir.length).replace(/^[\\/]+/, "")
: picked;
- setDraft((current) =>
- current ? `${current.replace(/\s+$/, "")} @${rel} ` : `@${rel} `,
- );
+ setDraft((current) => (current ? `${current.replace(/\s+$/, "")} @${rel} ` : `@${rel} `));
setChips((c) => [...c, { kind: "at", label: rel }]);
onMentionPicked?.(rel);
textareaRef.current?.focus();
@@ -245,10 +259,7 @@ export function Composer({
useEffect(() => {
if (!modelMenuOpen) return;
const onDown = (e: MouseEvent) => {
- if (
- modelWrapRef.current &&
- !modelWrapRef.current.contains(e.target as Node)
- ) {
+ if (modelWrapRef.current && !modelWrapRef.current.contains(e.target as Node)) {
setModelMenuOpen(false);
}
};
@@ -256,6 +267,17 @@ export function Composer({
return () => window.removeEventListener("mousedown", onDown);
}, [modelMenuOpen]);
+ useEffect(() => {
+ if (!toolsMenuOpen) return;
+ const onDown = (e: MouseEvent) => {
+ if (toolsWrapRef.current && !toolsWrapRef.current.contains(e.target as Node)) {
+ setToolsMenuOpen(false);
+ }
+ };
+ window.addEventListener("mousedown", onDown);
+ return () => window.removeEventListener("mousedown", onDown);
+ }, [toolsMenuOpen]);
+
const attachFile = async (filter?: "image") => {
try {
const picked = await openFileDialog({
@@ -325,12 +347,14 @@ export function Composer({
return base;
}, [popup, mentionResults]);
- const items =
- popup?.kind === "slash" ? slashItems : popup?.kind === "at" ? atItems : [];
+ const popupKind = popup?.kind;
+ const items = popupKind === "slash" ? slashItems : popupKind === "at" ? atItems : [];
useEffect(() => {
- setActiveIdx(0);
- }, [popup?.kind]);
+ if (!popupKind || popupKind === "slash" || popupKind === "at") {
+ setActiveIdx(0);
+ }
+ }, [popupKind]);
useEffect(() => {
setActiveIdx((i) => (items.length ? Math.min(i, items.length - 1) : 0));
@@ -361,6 +385,19 @@ export function Composer({
const dismiss = () => setPopup(null);
+ const openSlashPopup = () => {
+ setToolsMenuOpen(false);
+ setModelMenuOpen(false);
+ setPopup({ kind: "slash", query: "" });
+ };
+
+ const openMentionPopup = () => {
+ setToolsMenuOpen(false);
+ setModelMenuOpen(false);
+ const nonce = ++nonceRef.current;
+ setPopup({ kind: "at", query: "", nonce });
+ };
+
const pickItem = (idx: number) => {
const it = items[idx];
if (!it || !popup) return;
@@ -436,9 +473,7 @@ export function Composer({
}
if (e.key === "ArrowUp") {
e.preventDefault();
- setActiveIdx((i) =>
- items.length ? (i - 1 + items.length) % items.length : 0,
- );
+ setActiveIdx((i) => (items.length ? (i - 1 + items.length) % items.length : 0));
return;
}
if (e.key === "Escape") {
@@ -513,18 +548,23 @@ export function Composer({
return (
- {queuedSends && queuedSends.length > 0 ? (
+ {queuedSendRows.length > 0 ? (
- {t("composer.queueCount", { n: queuedSends.length })}
+ {t("composer.queueCount", { n: queuedSendRows.length })}
- {queuedSends.map((text, i) => (
-
- {text}
+ {queuedSendRows.map((row) => (
+
+ {row.text}
{onDequeueSend ? (
- onDequeueSend(i)}>
+ onDequeueSend(row.index)}
+ title={t("composer.close")}
+ >
-
+
) : null}
))}
@@ -537,9 +577,7 @@ export function Composer({
{busyLabel}
-
- {fmtElapsed(busyElapsedMs ?? 0)}
-
+ {fmtElapsed(busyElapsedMs ?? 0)}
@@ -568,24 +606,20 @@ export function Composer({
- {chips.length > 0 ? (
+ {chipRows.length > 0 ? (
- {chips.map((c, i) => (
-
- {c.kind === "slash" ? (
-
- ) : (
-
- )}
- {c.label}
- (
+
+ {row.chip.kind === "slash" ? : }
+ {row.chip.label}
+
- setChips((cs) => cs.filter((_, j) => j !== i))
- }
+ onClick={() => setChips((cs) => cs.filter((_, j) => j !== row.index))}
+ title={t("composer.close")}
>
-
+
))}
@@ -598,7 +632,9 @@ export function Composer({
onChange={handleChange}
onPaste={(e) => void handlePaste(e)}
onKeyDown={handleKeyDown}
- onCompositionStart={() => { composingRef.current = true; }}
+ onCompositionStart={() => {
+ composingRef.current = true;
+ }}
onCompositionEnd={() => {
composingRef.current = false;
compositionEndedAtRef.current = Date.now();
@@ -630,8 +666,8 @@ export function Composer({
setPopup({ kind: "slash", query: "" })}
+ className="cf-btn composer-secondary-action"
+ onClick={openSlashPopup}
>
@@ -640,11 +676,8 @@ export function Composer({
{
- const nonce = ++nonceRef.current;
- setPopup({ kind: "at", query: "", nonce });
- }}
+ className="cf-btn composer-secondary-action"
+ onClick={openMentionPopup}
>
@@ -654,11 +687,14 @@ export function Composer({
-
+
setModelMenuOpen((v) => !v)}
+ onClick={() => {
+ setToolsMenuOpen(false);
+ setModelMenuOpen((v) => !v);
+ }}
title={t("composer.switchModel")}
>
@@ -681,6 +717,62 @@ export function Composer({
/>
) : null}
+
+ {
+ setModelMenuOpen(false);
+ setToolsMenuOpen((v) => !v);
+ }}
+ title={t("app.titlebar.more")}
+ aria-label={t("app.titlebar.more")}
+ >
+
+
+
+
+ {toolsMenuOpen ? (
+
+
+ ...
+ {t("app.titlebar.more")}
+
+
+
+
+
+
+
+ {t("composer.commandsLabel")}
+ {t("composer.slashHeader")}
+
+
+
+
+
+
+
+ {t("composer.mentionLabel")}
+ {t("composer.atHeader")}
+
+
+
+ {
+ onModelChange(m);
+ setToolsMenuOpen(false);
+ }}
+ onPickEffort={(e) => {
+ onEffortChange(e);
+ setToolsMenuOpen(false);
+ }}
+ />
+
+ ) : null}
+
{busy ? (
{
requestAnimationFrame(() => {
- const el = listRef.current?.querySelector(`[data-active="true"]`);
+ const el = listRef.current?.querySelector(`[data-index="${activeIdx}"]`);
el?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
});
}, [activeIdx]);
@@ -759,15 +851,17 @@ function Popup({
e.preventDefault()}>
{kind === "slash" ? "/" : "@"}
-
- {kind === "slash"
- ? t("composer.slashHeader")
- : t("composer.atHeader")}
-
+ {kind === "slash" ? t("composer.slashHeader") : t("composer.atHeader")}
-
+
-
+
{items.length === 0 ? (
@@ -783,9 +877,15 @@ function Popup({
) : null}
{items.map((it, i) => (
- onPick(i)}
onMouseEnter={() => onHover(i, it)}
@@ -810,10 +910,8 @@ function Popup({
>
)}
-
- {kind === "slash" ? ((it as SlashCmd).kb ?? "") : ""}
-
-
+ {kind === "slash" ? ((it as SlashCmd).kb ?? "") : ""}
+
))}
@@ -844,7 +942,6 @@ function ModelEffortMenu({
onPickModel: (model: string) => void;
onPickEffort: (effort: ReasoningEffort) => void;
}) {
- const [draft, setDraft] = useState(modelLabel);
return (
+
+
+ );
+}
+
+function ModelEffortContent({
+ modelLabel,
+ currentEffort,
+ onPickModel,
+ onPickEffort,
+}: {
+ modelLabel: string;
+ currentEffort: ReasoningEffort;
+ onPickModel: (model: string) => void;
+ onPickEffort: (effort: ReasoningEffort) => void;
+}) {
+ const [draft, setDraft] = useState(modelLabel);
+ return (
+ <>
M
{t("composer.switchModel")}
{KNOWN_MODELS.map((m) => (
-
{m}
-
+
))}
{EFFORTS.map((e) => (
- {e}
{t(`effort.${e}Desc` as TKey)}
-
+
))}
-
+ >
);
}
diff --git a/desktop/src/ui/context-panel.tsx b/desktop/src/ui/context-panel.tsx
index 876d77169..f81b95d1c 100644
--- a/desktop/src/ui/context-panel.tsx
+++ b/desktop/src/ui/context-panel.tsx
@@ -44,21 +44,26 @@ export function ContextPanel({
const usedPct = Math.min(100, (used / CONTEXT_MAX_TOKENS) * 100);
const cachedPct = Math.min(100, (cached / CONTEXT_MAX_TOKENS) * 100);
const free = Math.max(0, CONTEXT_MAX_TOKENS - reserved - used - cached);
+ const tabs: { id: Tab; label: string }[] = [
+ { id: "files", label: t("contextPanel.filesTab") },
+ { id: "tools", label: t("contextPanel.toolsTab") },
+ { id: "memory", label: t("contextPanel.memoryTab") },
+ { id: "rules", label: t("contextPanel.rulesTab") },
+ ];
return (