From 857646ea666a8d930a8ffec2f289aa7bebf0e79d Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Thu, 3 Sep 2026 12:05:29 +0800 Subject: [PATCH 1/4] =?UTF-8?q?refactor(client):=20=E6=94=B6=E6=95=9B=20/s?= =?UTF-8?q?idebar=20envelope=20=E8=A7=A3=E6=9E=90=E4=B8=8E=E5=8F=96?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E5=90=8D=E5=8F=8C=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api.ts:call 与 fetchUpload 里逐字相同的响应 envelope 解析 (json().catch(null) + ok/value 判断 + SidebarApiError 构造)收敛为 私有 readEnvelope(),两处调用;判断顺序与错误码回退逻辑不变。 - 取扩展名双实现收敛:extOf 的实现迁至零依赖的 paths.ts(lang.ts 与 service.ts 均可达的基础模块),lang.ts re-export 保持既有导入面 (tests/file-types.spec.ts 从 lang.ts 导入 extOf);service.ts 删除 模块私有的同体 extOfPath,matchFileViewer 改用 extOf。方向说明: service.ts 不能反向 import lang.ts——lang.ts 携带全部 @codemirror/* 依赖且只经 editor 懒加载 chunk 进入浏览器,core bundle 静态引用它 会把 CodeMirror 拉进核心包(构建产物已验证:client.js / client-registry.js 中 0 处 codemirror,client-editor.js 不变)。 --- src/client/api.ts | 39 +++++++++++++++++++++------------------ src/client/lang.ts | 12 +++++------- src/client/paths.ts | 13 +++++++++++++ src/client/service.ts | 11 ++--------- 4 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/client/api.ts b/src/client/api.ts index 206fafc..e780f28 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -129,6 +129,25 @@ export type TerminalDepsStatus = note?: string } +/** + * Parse one `/sidebar` JSON response envelope into its value. A non-ok + * status, an unparseable body, or any shape other than `{ok: true, value}` + * surfaces as {@link SidebarApiError} carrying the wire code (falling back + * to the HTTP status). Shared by the JSON api route and the raw upload + * route, whose envelopes are identical. + */ +async function readEnvelope(response: Response): Promise { + const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null + = await response.json().catch(() => null) + if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) { + throw new SidebarApiError( + parsed?.error?.code ?? 'http', + parsed?.error?.message ?? `HTTP ${response.status}`, + ) + } + return parsed.value as T +} + async function call(method: string, payload: Record, signal?: AbortSignal): Promise { let response: Response try { @@ -141,15 +160,7 @@ async function call(method: string, payload: Record, signal? } catch (error) { throw new SidebarApiError('network', error instanceof Error ? error.message : String(error)) } - const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null - = await response.json().catch(() => null) - if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) { - throw new SidebarApiError( - parsed?.error?.code ?? 'http', - parsed?.error?.message ?? `HTTP ${response.status}`, - ) - } - return parsed.value as T + return readEnvelope(response) } /** @@ -180,15 +191,7 @@ async function fetchUpload( if (error instanceof DOMException && error.name === 'AbortError') throw error throw new SidebarApiError('network', error instanceof Error ? error.message : String(error)) } - const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null - = await response.json().catch(() => null) - if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) { - throw new SidebarApiError( - parsed?.error?.code ?? 'http', - parsed?.error?.message ?? `HTTP ${response.status}`, - ) - } - return parsed.value as T + return readEnvelope(response) } /** One request's session scope: the conversation id plus its cwd when known. */ diff --git a/src/client/lang.ts b/src/client/lang.ts index af83e23..424ddc0 100644 --- a/src/client/lang.ts +++ b/src/client/lang.ts @@ -48,14 +48,12 @@ import { pascal } from '@codemirror/legacy-modes/mode/pascal' import { vb } from '@codemirror/legacy-modes/mode/vb' import { vhdl } from '@codemirror/legacy-modes/mode/vhdl' import { stex } from '@codemirror/legacy-modes/mode/stex' +import { extOf } from './paths.ts' -/** The lowercased file extension of a path ('' when none). */ -export function extOf(path: string): string { - const at = path.lastIndexOf('.') - if (at === -1) return '' - const base = path.slice(at + 1).toLowerCase() - return base.includes('/') || base.includes('\\') ? '' : base -} +// Re-exported for the established import surface (tests import extOf from +// this module); the implementation lives in the dependency-free paths.ts so +// core-bundle modules can share it without pulling the CodeMirror packages. +export { extOf } /** Language key for an extension, or null for plain text. Pure (tested). */ export function languageKeyForExt(ext: string): string | null { diff --git a/src/client/paths.ts b/src/client/paths.ts index 7712aa1..e43ea10 100644 --- a/src/client/paths.ts +++ b/src/client/paths.ts @@ -70,3 +70,16 @@ export function baseName(path: string): string { const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) return at === -1 ? path : path.slice(at + 1) } + +/** + * The lowercased file extension of a path ('' when none). The dot must sit + * inside the last segment — a dot in a directory name is not an extension. + * Shared by the editor language mapping (lang.ts) and the viewer registry's + * extension matching (service.ts), which both live in the core bundle. + */ +export function extOf(path: string): string { + const at = path.lastIndexOf('.') + if (at === -1) return '' + const base = path.slice(at + 1).toLowerCase() + return base.includes('/') || base.includes('\\') ? '' : base +} diff --git a/src/client/service.ts b/src/client/service.ts index 45562f9..54856ff 100644 --- a/src/client/service.ts +++ b/src/client/service.ts @@ -27,6 +27,7 @@ import { type SidebarSnapshot, type SidebarState, type SidebarStore, type SidebarTab, } from './state.ts' import { isNarrowWidth } from './breakpoints.ts' +import { extOf } from './paths.ts' import type { SessionScope } from './api.ts' import type { SidebarPrefs } from '../prefs-shared.ts' @@ -427,14 +428,6 @@ export interface BetterSidebarService { openFile(scope: SessionScope, path: string, title?: string): void } -/** Extract the lowercase extension without leading dot from a path. */ -function extOfPath(path: string): string { - const at = path.lastIndexOf('.') - if (at === -1) return '' - const base = path.slice(at + 1).toLowerCase() - return base.includes('/') || base.includes('\\') ? '' : base -} - /** The file name of a path (both separators). */ function baseNameOf(path: string): string { const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) @@ -571,7 +564,7 @@ export function createBetterSidebarService(store: SidebarStore): BetterSidebarSe const isViewerEnabled = (id: string): boolean => store.getPrefs().viewersEnabled[id] !== false const matchFileViewer = (path: string, head?: Uint8Array): FileViewerDescriptor | undefined => { - const ext = extOfPath(path) + const ext = extOf(path) // Single pass in priority order (descending; stable for equal // priorities — insertion order). Each descriptor gets first refusal in // its own turn: `detect` (when head bytes are available) beats its own From 69cc8014a7178e6a3cdc07ce221f0ac3ecfc5b70 Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Thu, 3 Sep 2026 12:05:41 +0800 Subject: [PATCH 2/4] =?UTF-8?q?refactor(client):=20subagent=20lineage=20?= =?UTF-8?q?=E9=81=8D=E5=8E=86=E6=94=B6=E6=95=9B=E4=B8=BA=E5=85=B1=E4=BA=AB?= =?UTF-8?q?=20subagent-lineage=20=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rootAncestor / countSubagentDescendants / treeSessionIds 三份「沿 subagent-origin 链上行 + seen 集合防环 fail-soft」实现收敛到新模块 src/client/subagent-lineage.ts:核心走链是单个私有生成器 subagentOriginChain()(seen 防环、只在节点为 subagent 且有 parent 时 前行),三个公开函数迁入并基于它重写;isSideThreadSummary 一并迁入 (countSubagentDescendants 依赖它,迁入可避免 detect↔lineage 循环 依赖),subagent-detect.ts / subagent-jobs.ts re-export 全部迁出 符号,既有 import 调用点零变化。 语义逐字保留的等价性论证: - rootAncestor:链为空返回 start.id(原循环条件不满足即返回 current?.id);链非空返回 byId[末节点.parentId]?.id ?? sessionId (原循环退出时 current 恰为该行,断链/行缺失回退 sessionId 不变)。 - countSubagentDescendants:生成器在 seen.add 后 yield、消费方判 parentId === sessionId 即 break——与原循环体内判断顺序一致; descendant 的 running 判定、side-thread 过滤位置不变。 - treeSessionIds:根行缺失(byId[rootId] 无行)时任何上行都不可能 「访问到」根(原实现只匹配真实访问过的行),故提前返回空集; 根行自身(非 subagent)经 summary.id === rootId 特判入集;其余 行经链节点 parentId === rootId 判定,等价于原「上行走到 id === rootId 的行」——两种表述指向同一行(链上节点的 next 行)。 directSubagentCount 不迁移:它是对直接子行的单层过滤,没有上行 walk,无共享逻辑(背景侦查将其归入 walk 家族与实际代码不符)。 collectBranchIds 遍历的是 catalog 结构而非 session 链,保持不动。 --- src/client/subagent-detect.ts | 75 ++----------------- src/client/subagent-jobs.ts | 36 ++------- src/client/subagent-lineage.ts | 131 +++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 98 deletions(-) create mode 100644 src/client/subagent-lineage.ts diff --git a/src/client/subagent-detect.ts b/src/client/subagent-detect.ts index f45b3ad..a8668ef 100644 --- a/src/client/subagent-detect.ts +++ b/src/client/subagent-detect.ts @@ -9,23 +9,20 @@ * - {@link countSubagentDescendants}: uninterrupted subagent-origin lineage * totals (mirror of the official `indexSubagentDescendants` over the * plugin's own summary rows). + * + * The lineage walks themselves ({@link isSideThreadSummary}, {@link + * rootAncestor}, {@link countSubagentDescendants}) live in + * ./subagent-lineage.ts — the single shared walk implementation — and are + * re-exported below for their established import sites. */ import type { SidebarSessionList, - SidebarSessionSummary, SidebarSubagentCatalog, } from '../context-types.ts' -import { SIDE_LABEL_PREFIX } from '../sidechat-core.ts' +import { countSubagentDescendants, isSideThreadSummary, rootAncestor } from './subagent-lineage.ts' -/** - * Side Chat threads ride the subagent origin (main-list hiding + the RPC - * ownership fence) but they are NOT subagent topology: they carry the - * durable 'Side: ' label and live as sidebar tabs. Excluding them here - * keeps the auto-open trigger and the Subagent page counts clean. - */ -export function isSideThreadSummary(summary: SidebarSessionSummary): boolean { - return summary.origin === 'subagent' && summary.displayTitle.startsWith(SIDE_LABEL_PREFIX) -} +export { countSubagentDescendants, isSideThreadSummary, rootAncestor } +export type { SubagentDescendantTotals } from './subagent-lineage.ts' /** Count the direct subagent children of one session (durable `origin` rows). */ export function directSubagentCount( @@ -40,28 +37,6 @@ export function directSubagentCount( return count } -/** - * The main agent of the current session's tree: walk the durable parent - * chain upward until the first non-subagent session. The Subagent page shows - * THIS root's full topology regardless of how deep the current selection is - * (a session whose row is still hydrating, or a broken chain, degrades to - * the session itself). - */ -export function rootAncestor( - byId: SidebarSessionList['byId'], - sessionId: string | undefined, -): string | undefined { - if (sessionId === undefined) return undefined - const seen = new Set() - let current: SidebarSessionSummary | undefined = byId[sessionId] - while (current !== undefined && current.origin === 'subagent' - && current.parentId !== undefined && !seen.has(current.id)) { - seen.add(current.id) - current = byId[current.parentId] - } - return current?.id ?? sessionId -} - /** * Collect every catalog branch (an entry with `hasChildren`) reachable from * the root — the set of catalogs the always-expanded topology consumes. @@ -101,37 +76,3 @@ export function detectNewDirectSubagent( return directSubagentCount(prev.byId, sessionId) === 0 && directSubagentCount(next.byId, sessionId) > 0 } - -/** Descendant totals of one session through an uninterrupted subagent-origin chain. */ -export interface SubagentDescendantTotals { - count: number - runningCount: number -} - -/** - * Index every subagent descendant under each ancestor it reaches through an - * uninterrupted subagent-origin chain (same semantics as the official - * `indexSubagentDescendants`; cycles fail soft). - */ -export function countSubagentDescendants( - byId: SidebarSessionList['byId'], - sessionId: string, -): SubagentDescendantTotals { - const totals: SubagentDescendantTotals = { count: 0, runningCount: 0 } - for (const descendant of Object.values(byId)) { - if (descendant.origin !== 'subagent' || isSideThreadSummary(descendant)) continue - const seen = new Set() - let current: SidebarSessionSummary | undefined = descendant - while (current?.origin === 'subagent' && current.parentId !== undefined - && !seen.has(current.id)) { - seen.add(current.id) - if (current.parentId === sessionId) { - totals.count += 1 - if (descendant.running === true) totals.runningCount += 1 - break - } - current = byId[current.parentId] - } - } - return totals -} diff --git a/src/client/subagent-jobs.ts b/src/client/subagent-jobs.ts index 4962392..76af4a6 100644 --- a/src/client/subagent-jobs.ts +++ b/src/client/subagent-jobs.ts @@ -8,11 +8,11 @@ */ import type { SidebarSessionList, - SidebarSessionSummary, SidebarJobStatus, SidebarJobView, } from '../context-types.ts' import type { CopyKey } from './locales.ts' +import { treeSessionIds } from './subagent-lineage.ts' /** One row of the jobs section: the job plus its owning session's title. */ export interface TreeJob { @@ -26,36 +26,10 @@ export function isJobLive(job: SidebarJobView): boolean { return job.status === 'running' || job.status === 'stopping' } -/** - * Every session id of the topology tree rooted at `rootId` (the root plus - * each session whose uninterrupted subagent-origin chain reaches it — same - * lineage semantics as {@link countSubagentDescendants}; cycles fail soft). - * Sessions outside the tree (orphans, other trees) are excluded, so the - * jobs section never shows foreign work. - */ -export function treeSessionIds( - byId: SidebarSessionList['byId'], - rootId: string | undefined, -): Set { - const ids = new Set() - if (rootId === undefined) return ids - for (const summary of Object.values(byId)) { - const seen = new Set() - let current: SidebarSessionSummary | undefined = summary - let reachesRoot = false - while (current !== undefined && !seen.has(current.id)) { - seen.add(current.id) - if (current.id === rootId) { - reachesRoot = true - break - } - if (current.origin !== 'subagent' || current.parentId === undefined) break - current = byId[current.parentId] - } - if (reachesRoot) ids.add(summary.id) - } - return ids -} +// The lineage walk itself lives in ./subagent-lineage.ts (the single shared +// subagent-origin chain implementation); re-exported for the established +// import sites of this module. +export { treeSessionIds } /** * Whether a NEW background job appeared for one session between two diff --git a/src/client/subagent-lineage.ts b/src/client/subagent-lineage.ts new file mode 100644 index 0000000..d4cce66 --- /dev/null +++ b/src/client/subagent-lineage.ts @@ -0,0 +1,131 @@ +/** + * The shared subagent-lineage walks over the sessions list feed (structural + * mirror world — no runtime imports). Every public walk here answers a + * topology question by following the DURABLE subagent-origin chain upward + * (child → parent → …); one private generator ({@link subagentOriginChain}) + * is the single implementation of that walk, seen-set guarded so cycles fail + * soft (the walk just stops; nothing throws). + * + * subagent-detect.ts and subagent-jobs.ts re-export these for their + * established import sites — the auto-activation triggers and the Subagent + * page both consume them through the original module paths. + */ +import type { SidebarSessionList, SidebarSessionSummary } from '../context-types.ts' +import { SIDE_LABEL_PREFIX } from '../sidechat-core.ts' + +/** + * Side Chat threads ride the subagent origin (main-list hiding + the RPC + * ownership fence) but they are NOT subagent topology: they carry the + * durable 'Side: ' label and live as sidebar tabs. Excluding them here + * keeps the auto-open trigger and the Subagent page counts clean. + */ +export function isSideThreadSummary(summary: SidebarSessionSummary): boolean { + return summary.origin === 'subagent' && summary.displayTitle.startsWith(SIDE_LABEL_PREFIX) +} + +/** + * Yield `start`'s uninterrupted subagent-origin chain upward: each summary + * while it is a subagent with a known parent, then its parent row, and so + * on. A revisited id ends the walk (cycles fail soft); the row that BREAKS + * the chain — the first non-subagent ancestor, or a parent id with no row — + * is deliberately NOT yielded: callers observe it through where the walk + * stopped (the row after the last yielded one is `byId[last.parentId]`). + */ +function* subagentOriginChain( + byId: SidebarSessionList['byId'], + start: SidebarSessionSummary, +): Generator { + const seen = new Set() + let current: SidebarSessionSummary | undefined = start + while (current?.origin === 'subagent' && current.parentId !== undefined && !seen.has(current.id)) { + seen.add(current.id) + yield current + current = byId[current.parentId] + } +} + +/** + * The main agent of the current session's tree: walk the durable parent + * chain upward until the first non-subagent session. The Subagent page shows + * THIS root's full topology regardless of how deep the current selection is + * (a session whose row is still hydrating, or a broken chain, degrades to + * the session itself). + */ +export function rootAncestor( + byId: SidebarSessionList['byId'], + sessionId: string | undefined, +): string | undefined { + if (sessionId === undefined) return undefined + const start = byId[sessionId] + // A hydrating (or unknown) row degrades to the session itself. + if (start === undefined) return sessionId + let last: SidebarSessionSummary | undefined + for (const node of subagentOriginChain(byId, start)) last = node + if (last === undefined) return start.id + // The walk stopped one row past the last chain node: that parent row is + // the root (still missing from the feed → the session itself). Every + // yielded node carries a parent id (the generator's walk condition). + return byId[last.parentId!]?.id ?? sessionId +} + +/** Descendant totals of one session through an uninterrupted subagent-origin chain. */ +export interface SubagentDescendantTotals { + count: number + runningCount: number +} + +/** + * Index every subagent descendant under each ancestor it reaches through an + * uninterrupted subagent-origin chain (same semantics as the official + * `indexSubagentDescendants`; cycles fail soft). + */ +export function countSubagentDescendants( + byId: SidebarSessionList['byId'], + sessionId: string, +): SubagentDescendantTotals { + const totals: SubagentDescendantTotals = { count: 0, runningCount: 0 } + for (const descendant of Object.values(byId)) { + if (descendant.origin !== 'subagent' || isSideThreadSummary(descendant)) continue + for (const node of subagentOriginChain(byId, descendant)) { + if (node.parentId === sessionId) { + totals.count += 1 + if (descendant.running === true) totals.runningCount += 1 + break + } + } + } + return totals +} + +/** + * Every session id of the topology tree rooted at `rootId` (the root plus + * each session whose uninterrupted subagent-origin chain reaches it — same + * lineage semantics as {@link countSubagentDescendants}; cycles fail soft). + * Sessions outside the tree (orphans, other trees) are excluded, so the + * jobs section never shows foreign work. + */ +export function treeSessionIds( + byId: SidebarSessionList['byId'], + rootId: string | undefined, +): Set { + const ids = new Set() + // A root with no row in the feed cannot be reached by walking: only real + // rows anchor tree membership (a parent pointer naming a hydrating root + // does not pull the orphaned branch into the tree). + if (rootId === undefined || byId[rootId] === undefined) return ids + for (const summary of Object.values(byId)) { + // The root row itself belongs to its own tree even though it is not a + // subagent (it is usually the main agent). + if (summary.id === rootId) { + ids.add(summary.id) + continue + } + for (const node of subagentOriginChain(byId, summary)) { + if (node.parentId === rootId) { + ids.add(summary.id) + break + } + } + } + return ids +} From 4002f3cc5c2f875177f13eebedf03d303d5cb45a Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Thu, 3 Sep 2026 12:05:48 +0800 Subject: [PATCH 3/4] =?UTF-8?q?refactor(client):=20=E7=BB=9F=E4=B8=80=20Gi?= =?UTF-8?q?tLens=20=E9=94=99=E8=AF=AF=E5=BD=92=E4=B8=80=E5=8C=96=E4=B8=8E?= =?UTF-8?q?=20SubagentView=20=E6=97=A5=E5=BF=97=E5=88=86=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GitLens.tsx:六处 catch 里的 reason instanceof Error ? reason.message : String(reason)(refreshTarget/refresh/loadMoreLog/ commit/checkout/runConfirmed)收敛为模块级 errorMessage(), 两处模板字符串插值场景(historyLoadError/checkoutError 前缀) 同样复用;归一化表达式与输出文本逐字不变。 - SubagentView.tsx:openSubagent/open session 失败的 console.warn 改为 console.error,与同严重度失败路径对齐(Sidebar.tsx tab badge、TerminalView.tsx xterm open、service.ts urlTarget/plugin callback 均为 console.error);前缀与消息文本不变。tests/ 无任何 钉 console.warn 的断言,已核实。 --- src/client/SubagentView.tsx | 4 ++-- src/client/changes/GitLens.tsx | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/client/SubagentView.tsx b/src/client/SubagentView.tsx index 5ea2f74..3f28979 100644 --- a/src/client/SubagentView.tsx +++ b/src/client/SubagentView.tsx @@ -715,7 +715,7 @@ export function SubagentView(props: { try { sessions.openSubagent?.(address) } catch (error) { - console.warn('[dsh-better-sidebar] openSubagent failed:', error) + console.error('[dsh-better-sidebar] openSubagent failed:', error) } }, [sessions, onOpenChild]) @@ -725,7 +725,7 @@ export function SubagentView(props: { try { sessions.open?.(rootId) } catch (error) { - console.warn('[dsh-better-sidebar] open session failed:', error) + console.error('[dsh-better-sidebar] open session failed:', error) } }, [sessions, rootId]) diff --git a/src/client/changes/GitLens.tsx b/src/client/changes/GitLens.tsx index 71b7324..106d06e 100644 --- a/src/client/changes/GitLens.tsx +++ b/src/client/changes/GitLens.tsx @@ -64,6 +64,12 @@ function refNames(refs: string): string[] { )] } +/** One thrown value as display text (every error banner/row here normalizes + * through this so non-Error rejections never render as '[object Object]'). */ +function errorMessage(reason: unknown): string { + return reason instanceof Error ? reason.message : String(reason) +} + /** The pending destructive action (discard / revert / cherry-pick), gated by a confirm modal. */ interface ConfirmState { title: string @@ -157,7 +163,7 @@ export function GitLens(props: GitLensProps) { setLogEnded(logResult.length < LOG_BATCH) } catch (reason) { if (options.generation === refreshGeneration.current) { - setError(reason instanceof Error ? reason.message : String(reason)) + setError(errorMessage(reason)) } } finally { if (options.loading && options.generation === refreshGeneration.current) setLoading(false) @@ -219,7 +225,7 @@ export function GitLens(props: GitLensProps) { await refreshTarget(target, { loading: !silent, generation }) } catch (reason) { if (generation === refreshGeneration.current) { - setError(reason instanceof Error ? reason.message : String(reason)) + setError(errorMessage(reason)) if (!silent) setLoading(false) } } finally { @@ -289,7 +295,7 @@ export function GitLens(props: GitLensProps) { if (next.length < LOG_BATCH) setLogEnded(true) } catch (reason) { if (generation === refreshGeneration.current && target === chosenPathRef.current) { - setCommitError(`${t('historyLoadError')}: ${reason instanceof Error ? reason.message : String(reason)}`) + setCommitError(`${t('historyLoadError')}: ${errorMessage(reason)}`) } } finally { if (generation === refreshGeneration.current && target === chosenPathRef.current) setLogLoadingMore(false) @@ -355,7 +361,7 @@ export function GitLens(props: GitLensProps) { setCommitMsg('') await refresh() } catch (reason) { - setCommitError(reason instanceof Error ? reason.message : String(reason)) + setCommitError(errorMessage(reason)) } finally { setBusy(false) } @@ -369,7 +375,7 @@ export function GitLens(props: GitLensProps) { await api.gitCheckout(gitScope, branch, selectedWorktree) await refresh() } catch (reason) { - setCommitError(`${t('checkoutError')}: ${reason instanceof Error ? reason.message : String(reason)}`) + setCommitError(`${t('checkoutError')}: ${errorMessage(reason)}`) } finally { setBusy(false) } @@ -384,7 +390,7 @@ export function GitLens(props: GitLensProps) { await confirmState.onConfirm() await refresh() } catch (reason) { - setCommitError(reason instanceof Error ? reason.message : String(reason)) + setCommitError(errorMessage(reason)) } finally { setBusy(false) } From bc1984b2f261f112117bb03d882df61906ded57b Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Thu, 3 Sep 2026 12:05:48 +0800 Subject: [PATCH 4/4] =?UTF-8?q?refactor(sidechat):=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E9=9B=B6=E5=BC=95=E7=94=A8=E7=9A=84=20sidechatSeed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/sidechat-core.ts 的 sidechatSeed()(buildSidechatInheritance 的 seed 半边,doc 注明 test convenience)在 src+tests 全仓零引用,也无 barrel 转出口,删除。同文件的 SeedEvent 类型与 copyEvents 仍被 SidechatInheritance 使用,保留;其余导出不动。 --- src/sidechat-core.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/sidechat-core.ts b/src/sidechat-core.ts index a818ef6..51b2a4b 100644 --- a/src/sidechat-core.ts +++ b/src/sidechat-core.ts @@ -245,11 +245,6 @@ export function buildSidechatInheritance(events: readonly SidechatLogEvent[]): S return { seed, snapshot: null } } -/** The seed half of {@link buildSidechatInheritance} (test convenience). */ -export function sidechatSeed(events: readonly SidechatLogEvent[]): SeedEvent[] { - return buildSidechatInheritance(events).seed -} - /** * Structured text snapshot of the parent's OPEN turn (from its `turn/start` * to the log tail): the accumulated assistant/reasoning output verbatim