From b0c313e787dfc7a3eefbc4787764917f0e4ea9d8 Mon Sep 17 00:00:00 2001 From: fylorn <249551762+fylorn@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:00:37 +0800 Subject: [PATCH 01/32] Remove dead components, exports and text entries; test that every text entry is used Deletes button-group, progress, the mobile sidebar branch (the window is never narrower than 820px), nav.tsx's revealSection, seven unused exports, the Segmented re-export and the unused date-fns dependency. i18n.test.ts now uses the type checker to fail on any messages() entry nothing reads; it found four more dead entries besides the eleven from the audit. Co-Authored-By: Claude Opus 5.5 --- package.json | 1 - pnpm-lock.yaml | 3 - src/App.i18n.tsx | 2 - src/connection/Remote.tsx | 13 ---- src/connection/connection.i18n.tsx | 6 -- src/hooks/use-mobile.ts | 19 ----- src/i18n.test.ts | 111 ++++++++++++++++++++++++++++- src/keys/limits.ts | 5 -- src/labels.i18n.ts | 2 - src/labels.ts | 5 -- src/mcp/McpPage.i18n.tsx | 2 - src/nav.tsx | 76 -------------------- src/palette/sections.ts | 8 +-- src/plugins/PluginsPage.i18n.tsx | 2 - src/plugins/fields.i18n.ts | 2 - src/plugins/labels.i18n.ts | 2 - src/plugins/model.ts | 2 - src/plugins/parts.i18n.ts | 2 - src/routing/model.ts | 3 - src/security/labels.i18n.tsx | 2 - src/settings/SettingsPage.i18n.tsx | 4 -- src/ui/README.md | 7 +- src/ui/button-group.tsx | 83 --------------------- src/ui/icons.tsx | 1 - src/ui/progress.tsx | 28 -------- src/ui/sidebar.tsx | 56 ++------------- src/ui/ui.i18n.ts | 4 -- src/upstreams/parts.tsx | 3 - 28 files changed, 121 insertions(+), 333 deletions(-) delete mode 100644 src/hooks/use-mobile.ts delete mode 100644 src/ui/button-group.tsx delete mode 100644 src/ui/progress.tsx diff --git a/package.json b/package.json index 0dbe30f3..cea6c988 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "date-fns": "^4.4.0", "lucide-react": "^1.45.0", "radix-ui": "^1.6.7", "react": "^19.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbbe6370..5a3223ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,9 +41,6 @@ importers: cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - date-fns: - specifier: ^4.4.0 - version: 4.4.0 lucide-react: specifier: ^1.45.0 version: 1.45.0(react@19.2.8) diff --git a/src/App.i18n.tsx b/src/App.i18n.tsx index f48060ad..668e0374 100644 --- a/src/App.i18n.tsx +++ b/src/App.i18n.tsx @@ -19,7 +19,6 @@ export const appText = messages( surfaces: { dashboard: "概览", requests: "流量", - sessions: "会话", security: "安全", upstreams: "上游", routing: "路由", @@ -68,7 +67,6 @@ export const appText = messages( surfaces: { dashboard: "Overview", requests: "Traffic", - sessions: "Sessions", security: "Security", upstreams: "Upstreams", routing: "Routing", diff --git a/src/connection/Remote.tsx b/src/connection/Remote.tsx index c8021dbc..bef7e92f 100644 --- a/src/connection/Remote.tsx +++ b/src/connection/Remote.tsx @@ -1,4 +1,3 @@ -import { MonitorIcon } from "lucide-react"; import { Banner } from "@/ui/banner"; import { useText } from "@/i18n"; import { coreText } from "@/i18n/core.i18n"; @@ -6,18 +5,6 @@ import type { Retargeted } from "@/types"; import { remoteText } from "./remote.i18n"; import { useRemote } from "./useRemote"; -/** - * 连着远程 core 时,客户端页、MCP 页顶上**固定**的那一条(设计稿 ⑧):这里改的是这台 - * 机器上的文件,不是服务器上的。不能关 —— 它说的是这一页一直成立的事实。 - */ -export function RemoteNote({ children }: { children: string }) { - return ( - }> - {children} - - ); -} - /** * 把接管着的客户端改为指向服务器之后,**逐个说**:改好了哪几个,哪几个没改成、为什么。 * 一个失败不影响其余的,所以也不能只说一句「失败了」。WSL 里的名字带着发行版;整个 diff --git a/src/connection/connection.i18n.tsx b/src/connection/connection.i18n.tsx index 2a4903f7..7640f7c5 100644 --- a/src/connection/connection.i18n.tsx +++ b/src/connection/connection.i18n.tsx @@ -84,7 +84,6 @@ export const connText = messages( ), test: "测试连接", - testHint: "完成握手并读取服务器 core 的版本。", testOk: (version: string, gateway: string | null) => `连接成功 · core ${version}${gateway ? ` · 网关 ${gateway}` : ""}`, saveAndSwitch: "保存并切换", @@ -162,8 +161,6 @@ export const connText = messages( runOnServer: (version: string) => `服务器需要运行 core ${version}。在服务器上执行以下命令安装此版本,服务器上现有的版本较新或较旧均适用:`, reconnect: "重新连接", - localDown: "本机 core 未在运行", - restartLocal: "重新启动", // 安全模式、配置文件读不了 configBroken: "配置文件有误,网关未启动", @@ -241,7 +238,6 @@ export const connText = messages( ), test: "Test connection", - testHint: "Completes the handshake and reads the server's core version.", testOk: (version: string, gateway: string | null) => `Connected · core ${version}${gateway ? ` · gateway ${gateway}` : ""}`, saveAndSwitch: "Save and switch", @@ -320,8 +316,6 @@ export const connText = messages( runOnServer: (version: string) => `The server needs core ${version}. Run this command on the server to install that version, whether the installed one is newer or older:`, reconnect: "Reconnect", - localDown: "The local core is not running", - restartLocal: "Restart", configBroken: "The configuration file has an error; the gateway is not running", safeMode: "Safe mode", diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts deleted file mode 100644 index 2b0fe1df..00000000 --- a/src/hooks/use-mobile.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from "react" - -const MOBILE_BREAKPOINT = 768 - -export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState(undefined) - - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - } - mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - return () => mql.removeEventListener("change", onChange) - }, []) - - return !!isMobile -} diff --git a/src/i18n.test.ts b/src/i18n.test.ts index c4dca15b..81e34f97 100644 --- a/src/i18n.test.ts +++ b/src/i18n.test.ts @@ -1,5 +1,5 @@ import { readFileSync, readdirSync } from "node:fs"; -import { join } from "node:path"; +import { join, relative } from "node:path"; import ts from "typescript"; import { describe, expect, it } from "vitest"; import { getLang, messages, setLang, textOf } from "./i18n"; @@ -90,3 +90,112 @@ describe("界面文案都在词表里", () => { expect(stale).toEqual([]); }); }); + +/** + * **词表里没有用不到的句子。** + * + * 界面改版时删掉一处文字,词表里那一条常常留着:没有东西会报错,词表越积越多,翻译的人 + * 还在改一句早就没人看得见的话(审查时一次查出过十几条)。这里用 TypeScript 的类型检查 + * 找每一条有没有被读到 —— 按名字搜不行,`title`、`all` 这种名字到处都是。 + * + * 一条算用到了: + * + * · `t.title`、`t.title[guard]["edit"]`、解构 `const { title } = t`:类型检查认得出是哪一条; + * · 按变量取(`t.kinds[kind]`):变量是几个固定的字(`"a" | "b"`)时只算那几条,是任意 + * 字符串时整组都算; + * · 一组整个交出去(`Object.entries(t.secrets)`、存进 `Record`):整组都算; + * · 词表交给一个自己写了形状的参数(`t: { useTable: string }`):按那个形状里的名字算。 + * + * 测试文件里用到的不算:只有测试读的句子,界面上照样看不见。 + */ +describe("词表里没有用不到的句子", () => { + it("每一条都有地方用", { timeout: 120_000 }, () => { + const config = ts.parseJsonConfigFileContent(ts.readConfigFile("tsconfig.json", ts.sys.readFile).config, ts.sys, "."); + const roots = files("src").filter((p) => !/\.test\.tsx?$/.test(p)); + const program = ts.createProgram(roots, config.options); + const checker = program.getTypeChecker(); + const sources = program.getSourceFiles().filter((sf) => !sf.isDeclarationFile && !sf.fileName.includes("node_modules")); + + /** 每一条的声明(`messages(…)` 中文那一份里的属性)→ 文件和路径 */ + const entries = new Map(); + /** 值是一组(嵌套对象)的那几条 → 那一组 */ + const groups = new Map(); + const used = new Set(); + const collect = (obj: ts.ObjectLiteralExpression, prefix: string) => { + for (const p of obj.properties) { + if (!p.name) continue; + const name = p.name.getText(); + entries.set(p, prefix + name); + if (ts.isPropertyAssignment(p) && ts.isObjectLiteralExpression(p.initializer)) { + groups.set(p, p.initializer); + collect(p.initializer, `${prefix}${name}.`); + } + } + }; + for (const sf of sources) { + const visit = (n: ts.Node) => { + if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "messages") { + const zh = n.arguments[0]; + if (zh && ts.isObjectLiteralExpression(zh)) collect(zh, `${relative(".", sf.fileName)}: `); + } + ts.forEachChild(n, visit); + }; + visit(sf); + } + expect(entries.size).toBeGreaterThan(1000); + + const entriesOf = (sym: ts.Symbol | undefined) => (sym?.declarations ?? []).filter((d) => entries.has(d)); + const markAll = (obj: ts.ObjectLiteralExpression) => { + for (const p of obj.properties) { + used.add(p); + if (ts.isPropertyAssignment(p) && ts.isObjectLiteralExpression(p.initializer)) markAll(p.initializer); + } + }; + /** 用到了一条。是一组、又没有接着往下取(整组交出去了)的话,整组都算 */ + const use = (d: ts.Node, at?: ts.Node) => { + used.add(d); + const g = groups.get(d); + if (!g) return; + const p = at?.parent; + const chained = p && (ts.isPropertyAccessExpression(p) || ts.isElementAccessExpression(p)) && p.expression === at; + if (!chained) markAll(g); + }; + const parts = (t: ts.Type) => (t.isUnion() ? t.types : [t]); + + for (const sf of sources) { + const visit = (n: ts.Node) => { + if (ts.isPropertyAccessExpression(n)) { + for (const d of entriesOf(checker.getSymbolAtLocation(n.name))) use(d, n); + } else if (ts.isElementAccessExpression(n)) { + const obj = checker.getTypeAtLocation(n.expression); + const keys = parts(checker.getTypeAtLocation(n.argumentExpression)); + const fixed = keys.every((k) => k.isStringLiteral()); + for (const t of parts(obj)) { + const props = fixed + ? keys.map((k) => t.getProperty((k as ts.StringLiteralType).value)) + : t.getProperties(); + for (const s of props) for (const d of entriesOf(s)) use(d, n); + } + } else if (ts.isBindingElement(n) && ts.isObjectBindingPattern(n.parent)) { + const t = checker.getTypeAtLocation(n.parent); + for (const d of entriesOf(t.getProperty((n.propertyName ?? n.name).getText()))) use(d); + } + // 交给一个形状不同的类型(参数、带类型的变量):按那个类型里的名字算 + if (ts.isIdentifier(n) || ts.isPropertyAccessExpression(n) || ts.isCallExpression(n)) { + const ctx = checker.getContextualType(n); + const type = ctx && checker.getTypeAtLocation(n); + if (ctx && type && type.getProperties().some((s) => entriesOf(s).length > 0)) { + const open = ctx.getStringIndexType() !== undefined || (ctx.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; + const names = open ? type.getProperties().map((s) => s.name) : ctx.getProperties().filter((c) => entriesOf(c).length === 0).map((c) => c.name); + for (const name of names) for (const d of entriesOf(type.getProperty(name))) use(d); + } + } + ts.forEachChild(n, visit); + }; + visit(sf); + } + + const unused = [...entries].filter(([d]) => !used.has(d)).map(([, path]) => path); + expect(unused).toEqual([]); + }); +}); diff --git a/src/keys/limits.ts b/src/keys/limits.ts index 2eca35d4..ad11fa7c 100644 --- a/src/keys/limits.ts +++ b/src/keys/limits.ts @@ -182,11 +182,6 @@ export function resetText(atMs: number, nowMs: number): string { return t.resets(t.on(date, hm)); } -/** 一把密钥有没有哪一条已经到了 */ -export function anyReached(views: readonly KeyLimitView[] | undefined): boolean { - return (views ?? []).some((v) => v.reached); -} - /** 这几把密钥里最早要重新算的那一刻(天、周、月的上限才有)。没有就是 null */ export function nextReset(keys: readonly { limits: readonly KeyLimitView[] }[] | undefined): number | null { let soonest: number | null = null; diff --git a/src/labels.i18n.ts b/src/labels.i18n.ts index 4411d03e..5877c3a8 100644 --- a/src/labels.i18n.ts +++ b/src/labels.i18n.ts @@ -148,7 +148,6 @@ export const labelsText = messages( immediately: "下一个请求即使用新配置。", onRestart: "重新启动客户端后生效;通过环境变量读取配置的客户端需重新打开终端。", }, - fieldsOnly: "字段名已查证,尚未在本机实际运行验证。", }, { groupKinds: { @@ -288,6 +287,5 @@ export const labelsText = messages( onRestart: "Takes effect after the client restarts; clients that read their configuration from environment variables also need the terminal reopened.", }, - fieldsOnly: "Field names are verified; not yet confirmed by an actual run on this machine.", }, ); diff --git a/src/labels.ts b/src/labels.ts index 701879f4..7bca04f1 100644 --- a/src/labels.ts +++ b/src/labels.ts @@ -260,11 +260,6 @@ export function takesEffectText(t: TakesEffect): string { return t === "immediately" ? x.immediately : x.onRestart; } -/** 只查证过字段名的客户端要说出来。实测过的不用说,接管后在本机收到过请求的也不用说 */ -export function fieldsOnlyText(): string { - return textOf(labelsText).fieldsOnly; -} - /** * 按请求头认出来的应用叫什么。**这是旁证,不是身份** —— core 按 * User-Agent 之类猜的,能被伪造,只用来显示;身份是请求带的那把密钥。 diff --git a/src/mcp/McpPage.i18n.tsx b/src/mcp/McpPage.i18n.tsx index b682d08c..d3275d63 100644 --- a/src/mcp/McpPage.i18n.tsx +++ b/src/mcp/McpPage.i18n.tsx @@ -56,7 +56,6 @@ export const mcpText = messages( compareDesc: "各客户端中的配置不一致,不一致的字段已标出。", env: "环境变量", noEnv: "(无)", - file: "配置文件", unify: "如需统一:在矩阵中选择要保留的配置,复制到其他客户端。写入前将显示改动。", // 写入前的确认 @@ -152,7 +151,6 @@ export const mcpText = messages( compareDesc: "The configuration differs across clients; the fields that differ are highlighted.", env: "Environment variables", noEnv: "(none)", - file: "Configuration file", unify: "To make them consistent, choose the configuration to keep in the matrix and copy it to the other clients. The change is shown before anything is written.", diff --git a/src/nav.tsx b/src/nav.tsx index 3ee7c461..ac4d5610 100644 --- a/src/nav.tsx +++ b/src/nav.tsx @@ -112,79 +112,3 @@ export function useNavParams(surface: S, onParams: (params: N cb.current(d.params as NonNullable); }, [d, surface]); } - -/** - * 滚到页面里的一节。页面在 `useNavParams` 里收到「打开到某一节」时调(设置页的 - * `section`)。 - * - * 先找 `[data-section=""]`,没有就找字是 `title` 的标题(`h1`–`h3`)。**节是 - * 陆续画出来的**(语言、外观要先读到当前值),所以等它出现,最多 1.5 秒;它上面 - * 的节后画出来会把它往下挤,所以出现之后再跟 0.6 秒。用户自己滚了、点了、按了键, - * 就不再跟。找不到就什么都不做 —— 停在那一页的顶上。 - */ -export function revealSection(id: string, title?: string) { - if (typeof document === "undefined") return; - let stop = false; - const cancel = () => { - stop = true; - for (const ev of STOP_EVENTS) window.removeEventListener(ev, cancel, true); - }; - for (const ev of STOP_EVENTS) window.addEventListener(ev, cancel, { capture: true, passive: true }); - - const find = (): HTMLElement | null => { - const tagged = document.querySelector(`[data-section="${CSS.escape(id)}"]`); - if (tagged) return tagged; - if (!title) return null; - for (const h of document.querySelectorAll("h1, h2, h3")) { - if (h.textContent?.trim() === title) return h.closest("section") ?? h; - } - return null; - }; - const behavior: ScrollBehavior = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches - ? "auto" - : "smooth"; - /** 离滚动层顶边留一点,标题不贴着工具栏 */ - const MARGIN = 16; - /** 上一次要滚到哪儿。平滑滚动走在半路时 `scrollTop` 不是终点,拿它比会来回拉扯 */ - let target: number | null = null; - const align = (el: HTMLElement) => { - const box = scrollParent(el); - if (!box) return; - const top = Math.max( - 0, - Math.min( - box.scrollHeight - box.clientHeight, - el.getBoundingClientRect().top - box.getBoundingClientRect().top + box.scrollTop - MARGIN, - ), - ); - if (target !== null && Math.abs(top - target) <= 2) return; - target = top; - box.scrollTo({ top, behavior }); - }; - - const start = performance.now(); - let found = 0; - const tick = () => { - if (stop) return; - const now = performance.now(); - const el = find(); - if (el) { - if (found === 0) found = now; - align(el); - } - const done = found > 0 ? now - found > 600 : now - start > 1500; - if (done) cancel(); - else setTimeout(() => requestAnimationFrame(tick), found > 0 ? 120 : 16); - }; - requestAnimationFrame(tick); -} - -const STOP_EVENTS = ["wheel", "pointerdown", "keydown", "touchmove"] as const; - -function scrollParent(el: HTMLElement): HTMLElement | null { - for (let p = el.parentElement; p; p = p.parentElement) { - const oy = getComputedStyle(p).overflowY; - if ((oy === "auto" || oy === "scroll") && p.scrollHeight > p.clientHeight) return p; - } - return null; -} diff --git a/src/palette/sections.ts b/src/palette/sections.ts index 3c521010..4e071049 100644 --- a/src/palette/sections.ts +++ b/src/palette/sections.ts @@ -12,8 +12,7 @@ import { connText } from "@/connection/connection.i18n"; * `useNavParams("settings", …)` 里收 `section`)。 * * **标题直接取各节自己的词表**,不在这里另写一份:命令面板里的「语言」和设置页上那 - * 一节的标题永远是同一个词,设置页按标题的字就找得到那一节(见 nav.tsx 的 - * `revealSection`)。 + * 一节的标题永远是同一个词。 * * **设置页改版时一起改这里**:一项是一件用户会去搜的设置(语言、外观、端口……), * `id` 是设置页认得的那一节。几件设置并进同一节时,它们照样各占一项,`id` 都指向 @@ -64,8 +63,3 @@ export const SETTINGS_SECTIONS: readonly SectionDef[] = [ { id: "diagnostics", title: pick(settingsText, (t) => t.diagnostics), local: true }, { id: "uninstall", title: pick(settingsText, (t) => t.uninstallTitle) }, ]; - -/** 某一节现在的标题(当前语言)。不是这里列的节时是 `undefined` */ -export function sectionTitle(id: string, lang: "zh" | "en"): string | undefined { - return SETTINGS_SECTIONS.find((s) => s.id === id)?.title[lang]; -} diff --git a/src/plugins/PluginsPage.i18n.tsx b/src/plugins/PluginsPage.i18n.tsx index 69927950..b4b89df4 100644 --- a/src/plugins/PluginsPage.i18n.tsx +++ b/src/plugins/PluginsPage.i18n.tsx @@ -36,7 +36,6 @@ export const pluginsPageText = messages( review: "审核更改…", remove: "删除…", }, - actionsFor: (name: string) => `「${name}」的操作`, toggleFor: (name: string) => `启用「${name}」`, turnedOn: (name: ReactNode) => <>已启用插件「{name}」, turnedOff: (name: ReactNode) => <>已停用插件「{name}」, @@ -90,7 +89,6 @@ export const pluginsPageText = messages( review: "Review changes…", remove: "Delete…", }, - actionsFor: (name: string) => `Actions for “${name}”`, toggleFor: (name: string) => `Enable “${name}”`, turnedOn: (name: ReactNode) => <>Plugin “{name}” enabled, turnedOff: (name: ReactNode) => <>Plugin “{name}” disabled, diff --git a/src/plugins/fields.i18n.ts b/src/plugins/fields.i18n.ts index fb66a214..03da6d71 100644 --- a/src/plugins/fields.i18n.ts +++ b/src/plugins/fields.i18n.ts @@ -2,7 +2,6 @@ import { messages } from "@/i18n"; export const pluginFieldsText = messages( { - all: "全部", some: "指定", /** 标题下的一句:这一项按什么匹配。客户端不用说 */ matches: { @@ -24,7 +23,6 @@ export const pluginFieldsText = messages( numberBad: (label: string) => `「${label}」需要填写数字。`, }, { - all: "All", some: "Specific", matches: { clients: "", diff --git a/src/plugins/labels.i18n.ts b/src/plugins/labels.i18n.ts index b9da2237..1483f44e 100644 --- a/src/plugins/labels.i18n.ts +++ b/src/plugins/labels.i18n.ts @@ -34,7 +34,6 @@ export const pluginLabelsText = messages( highRisk: "高风险", chipTip: (what: string, note: string) => (note ? `${what}。${note}。` : `${what}。`), added: "新增", - removed: "已移除", /** 改回答文字的插件,整段模式下文字到齐才交给客户端 */ blockMode: "回答文字整段到齐后才显示", @@ -98,7 +97,6 @@ export const pluginLabelsText = messages( highRisk: "High risk", chipTip: (what: string, note: string) => (note ? `${what}. ${note}.` : `${what}.`), added: "New", - removed: "Removed", blockMode: "Reply text appears once each block is complete", kinds: { conversation: "conversations", embeddings: "embeddings", completions: "completions" } as Record, diff --git a/src/plugins/model.ts b/src/plugins/model.ts index 3332a0f2..3589e1c0 100644 --- a/src/plugins/model.ts +++ b/src/plugins/model.ts @@ -98,8 +98,6 @@ export function sentModel(r: { model: string; routing?: HistoryRow["routing"] }) export const SCOPE_PARTS = ["clients", "models", "upstreams"] as const; export type ScopePart = (typeof SCOPE_PARTS)[number]; -export const EMPTY_SCOPE: PluginScope = { clients: [], models: [], upstreams: [] }; - /** * 一条通配规则对不对得上:`*` 是任意一段(可以为空),别的字符原样比。 * diff --git a/src/plugins/parts.i18n.ts b/src/plugins/parts.i18n.ts index 4c3547ec..78b1a2fc 100644 --- a/src/plugins/parts.i18n.ts +++ b/src/plugins/parts.i18n.ts @@ -13,7 +13,6 @@ export const pluginPartsText = messages( permissions: "申请的权限", /** 这一项权限这一版没有了 */ permissionRemoved: "不再申请", - statsTitle: "运行统计", errorAt: (line: number, column: number | null) => (column != null ? `第 ${line} 行第 ${column} 列` : `第 ${line} 行`), }, { @@ -26,7 +25,6 @@ export const pluginPartsText = messages( codeLabel: "Plugin code", permissions: "Requested permissions", permissionRemoved: "No longer requested", - statsTitle: "Runs", errorAt: (line: number, column: number | null) => (column != null ? `line ${line}, column ${column}` : `line ${line}`), }, ); diff --git a/src/routing/model.ts b/src/routing/model.ts index cb4450c5..30a9d522 100644 --- a/src/routing/model.ts +++ b/src/routing/model.ts @@ -507,6 +507,3 @@ export function strategies(): { id: GroupKind; desc: string }[] { /** 客户端格式:规则条件和试算里可选的几种 */ export const DIALECTS: readonly Dialect[] = ["anthropic", "openai-chat", "openai-responses", "gemini"]; - -/** 辅助请求的类别(不含总称) */ -export const PROBE_IDS = ["health_check", "warmup", "titling", "topic_detect", "suggestion"]; diff --git a/src/security/labels.i18n.tsx b/src/security/labels.i18n.tsx index 8e2790ab..228b9309 100644 --- a/src/security/labels.i18n.tsx +++ b/src/security/labels.i18n.tsx @@ -70,7 +70,6 @@ export const securityLabelsText = messages( custom: "自定义", } as Record, custom: "自定义", - builtin: "内置", /** 命中处在工具结果里(日志的 `tool` 是 `tool_result`) */ toolResult: "工具结果", /** 日志按天分组时,一天的标题 */ @@ -190,7 +189,6 @@ export const securityLabelsText = messages( custom: "Custom", }, custom: "Custom", - builtin: "Built-in", toolResult: "tool result", day: { today: "Today", diff --git a/src/settings/SettingsPage.i18n.tsx b/src/settings/SettingsPage.i18n.tsx index 1f5620d1..79ad2209 100644 --- a/src/settings/SettingsPage.i18n.tsx +++ b/src/settings/SettingsPage.i18n.tsx @@ -60,8 +60,6 @@ export const settingsText = messages( { // 页头的摘要 - running: "运行中", - gateway: "网关", notListening: "未在监听", scopeLocal: "仅本机", scopeLan: "局域网", @@ -145,8 +143,6 @@ export const settingsText = messages( }, { - running: "Running", - gateway: "Gateway", notListening: "Not listening", scopeLocal: "This machine only", scopeLan: "Local network", diff --git a/src/ui/README.md b/src/ui/README.md index 6132ec22..0d311fef 100644 --- a/src/ui/README.md +++ b/src/ui/README.md @@ -410,9 +410,10 @@ useNavParams("keys", (p) => { Wait for the data a dialog needs before rendering it (an edit dialog opened by a deep link may mount before its row is loaded). The Settings page handles `section` itself -(`revealSection` finds `data-section=""`, or the heading whose text is the title in -`palette/sections.ts`). When the Settings page is restructured, keep one palette entry per -setting a user would search for and point its `id` at the section that now holds it. +(`jump` in `settings/kit.tsx` scrolls to the `SettingsGroup` whose `id`, or the +`SettingsRow` whose `anchor`, is that section). When the Settings page is restructured, keep +one palette entry per setting a user would search for (`palette/sections.ts`) and point its +`id` at the group or row that now holds it. ### Command palette and shortcuts diff --git a/src/ui/button-group.tsx b/src/ui/button-group.tsx deleted file mode 100644 index c0244583..00000000 --- a/src/ui/button-group.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" -import { Slot } from "radix-ui" - -import { Separator } from "@/ui/separator" - -const buttonGroupVariants = cva( - "group/button-group flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", - { - variants: { - orientation: { - horizontal: - "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg!", - vertical: - "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg!", - }, - }, - defaultVariants: { - orientation: "horizontal", - }, - } -) - -function ButtonGroup({ - className, - orientation, - ...props -}: React.ComponentProps<"div"> & VariantProps) { - return ( -
- ) -} - -function ButtonGroupText({ - className, - asChild = false, - ...props -}: React.ComponentProps<"div"> & { - asChild?: boolean -}) { - const Comp = asChild ? Slot.Root : "div" - - return ( - - ) -} - -function ButtonGroupSeparator({ - className, - orientation = "vertical", - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { - ButtonGroup, - ButtonGroupSeparator, - ButtonGroupText, - buttonGroupVariants, -} diff --git a/src/ui/icons.tsx b/src/ui/icons.tsx index 4c5d8839..4434ed65 100644 --- a/src/ui/icons.tsx +++ b/src/ui/icons.tsx @@ -18,7 +18,6 @@ export { Gauge as IconDashboard } from "lucide-react"; // 概览 —— 仪表盘 export { List as IconFlow } from "lucide-react"; // 流量 —— 一列记录 export { MessagesSquare as IconSession } from "lucide-react"; // 会话 —— 叠起来的气泡 -export { Search as IconFindings } from "lucide-react"; // 发现 —— 放大镜(看证据) export { ShieldCheck as IconGuard } from "lucide-react"; // 防护 —— 盾牌(配策略) export { Split as IconRoute } from "lucide-react"; // 路由 —— 一条进来分叉出去 export { Router as IconGateway } from "lucide-react"; // 网关 —— 一台路由器 diff --git a/src/ui/progress.tsx b/src/ui/progress.tsx deleted file mode 100644 index 37a0e45b..00000000 --- a/src/ui/progress.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from "react" -import { cn } from "@/lib/utils" -import { Progress as ProgressPrimitive } from "radix-ui" - -function Progress({ - className, - value, - ...props -}: React.ComponentProps) { - return ( - - - - ) -} - -export { Progress } diff --git a/src/ui/sidebar.tsx b/src/ui/sidebar.tsx index 14022d3a..c1dd9d5f 100644 --- a/src/ui/sidebar.tsx +++ b/src/ui/sidebar.tsx @@ -27,18 +27,10 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" import { Slot } from "radix-ui" -import { useIsMobile } from "@/hooks/use-mobile" import { isTyping } from "@/palette/keys" import { Button } from "@/ui/button" import { Input } from "@/ui/input" import { Separator } from "@/ui/separator" -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, -} from "@/ui/sheet" import { Skeleton } from "@/ui/skeleton" import { Tooltip, @@ -50,7 +42,6 @@ import { PanelLeftIcon } from "lucide-react" const SIDEBAR_COOKIE_NAME = "sidebar_state" const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 const SIDEBAR_WIDTH = "16rem" -const SIDEBAR_WIDTH_MOBILE = "18rem" const SIDEBAR_WIDTH_ICON = "3rem" const SIDEBAR_KEYBOARD_SHORTCUT = "b" @@ -58,9 +49,6 @@ type SidebarContextProps = { state: "expanded" | "collapsed" open: boolean setOpen: (open: boolean) => void - openMobile: boolean - setOpenMobile: (open: boolean) => void - isMobile: boolean toggleSidebar: () => void } @@ -105,9 +93,6 @@ function SidebarProvider({ open?: boolean onOpenChange?: (open: boolean) => void }) { - const isMobile = useIsMobile() - const [openMobile, setOpenMobile] = React.useState(false) - // This is the internal state of the sidebar. // We use openProp and setOpenProp for control from outside the component. const [_open, _setOpen] = React.useState(defaultOpen) @@ -129,8 +114,8 @@ function SidebarProvider({ // Helper to toggle the sidebar. const toggleSidebar = React.useCallback(() => { - return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) - }, [isMobile, setOpen, setOpenMobile]) + return setOpen((open) => !open) + }, [setOpen]) // Adds a keyboard shortcut to toggle the sidebar. React.useEffect(() => { @@ -154,12 +139,9 @@ function SidebarProvider({ state, open, setOpen, - isMobile, - openMobile, - setOpenMobile, toggleSidebar, }), - [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + [state, open, setOpen, toggleSidebar] ) return ( @@ -191,14 +173,13 @@ function Sidebar({ collapsible = "offcanvas", className, children, - dir, ...props }: React.ComponentProps<"div"> & { side?: "left" | "right" variant?: "sidebar" | "floating" | "inset" collapsible?: "offcanvas" | "icon" | "none" }) { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + const { state } = useSidebar() if (collapsible === "none") { return ( @@ -215,31 +196,6 @@ function Sidebar({ ) } - if (isMobile) { - return ( - - - - Sidebar - Displays the mobile sidebar. - -
{children}
-
-
- ) - } return (
} & VariantProps) { const Comp = asChild ? Slot.Root : "button" - const { isMobile, state } = useSidebar() + const { state } = useSidebar() const button = (