dsh-sidenote 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@
14
14
  * tools/connection/api-remotes 三处会漂移,钉这一个——architecture.md 第六节)。
15
15
  */
16
16
  import type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-api-remotes/client'
17
+ import { t } from '../locales.ts'
17
18
 
18
19
  /**
19
20
  * 叶子类型不从 dsh-tools 直接拉(api-remotes 未再导出;三处影子会漂移)——
@@ -44,7 +45,10 @@ export type ToolCardModel =
44
45
  }
45
46
  | {
46
47
  kind: 'terminal'
48
+ /** 标题(有人话 description 时 = 「Bash · description」,否则 = 命令原文)。 */
47
49
  title: string
50
+ /** 命令原文(TerminalBlock 的 command——标题让位给 description 后命令仍随行)。 */
51
+ command: string
48
52
  description?: string
49
53
  /** 已解析的 cwd(相对路径已按 cwdBase 折成绝对;无 base 时原样)。 */
50
54
  cwd?: string
@@ -52,6 +56,12 @@ export type ToolCardModel =
52
56
  exitCode?: number
53
57
  signal?: string
54
58
  }
59
+ | {
60
+ kind: 'todo'
61
+ /** 标题(任务 · N 已完成 · N 进行中 · N 待处理,非零组才出现)。 */
62
+ title: string
63
+ items: readonly { content: string; status: string }[]
64
+ }
55
65
  | { kind: 'diff'; title: string; diffs: readonly FileDiff[]; locations?: readonly FileLocation[] }
56
66
  | {
57
67
  kind: 'search'
@@ -111,6 +121,62 @@ function resolveCwd(cwd: string | undefined, cwdBase: string | undefined): strin
111
121
  return `${cwdBase.replace(/\/+$/, '')}/${cwd}`
112
122
  }
113
123
 
124
+ /**
125
+ * 展示态路径缩短:>3 段的绝对路径只保末 3 段(≈仓库相对形态,与主区
126
+ * 「Read · dsh-sidenote/scripts/x.sh」同款)。窄栏标题位只够放语义尾部。
127
+ */
128
+ export function shortPath(p: string): string {
129
+ if (!p.startsWith('/')) return p
130
+ const segs = p.split('/').filter(s => s !== '')
131
+ return segs.length > 3 ? segs.slice(-3).join('/') : p
132
+ }
133
+
134
+ /**
135
+ * 标题整形:「Tool /abs/path」→「Tool · 末3段」(主区同名同款);
136
+ * 裸绝对路径标题 → 末 3 段;不含路径的标题原样。
137
+ */
138
+ export function displayTitle(title: string): string {
139
+ const m = /^([A-Za-z][\w-]*) (\/.+)$/.exec(title)
140
+ const name = m?.[1]
141
+ const path = m?.[2]
142
+ if (name !== undefined && path !== undefined) return `${name} · ${shortPath(path)}`
143
+ return title.startsWith('/') ? shortPath(title) : title
144
+ }
145
+
146
+ /** todo_write 入参形状校验(字段不齐即放弃 todo 卡)。两种数据源形状:
147
+ * 0.1.1 wire:callView.rawInput = todos 数组本身(dsh-tool-todo presentCall 实证);
148
+ * 0.1.2 argsRaw:{ todos: [...] }。 */
149
+ function todoItemsOf(raw: unknown): readonly { content: string; status: string }[] | undefined {
150
+ const list = Array.isArray(raw)
151
+ ? raw
152
+ : typeof raw === 'object' && raw !== null ? (raw as { todos?: unknown }).todos : undefined
153
+ if (!Array.isArray(list) || list.length === 0) return undefined
154
+ const items: { content: string; status: string }[] = []
155
+ for (const it of list) {
156
+ const o = it as { content?: unknown; status?: unknown }
157
+ if (typeof o?.content !== 'string' || typeof o?.status !== 'string') return undefined
158
+ items.push({ content: o.content, status: o.status })
159
+ }
160
+ return items
161
+ }
162
+
163
+ /** todo 卡标题:任务 · 非零状态组计数(主区「任务 · N 已完成 · …」同款)。 */
164
+ function todoTitleOf(items: readonly { content: string; status: string }[]): string {
165
+ let done = 0
166
+ let doing = 0
167
+ let pending = 0
168
+ for (const it of items) {
169
+ if (it.status === 'completed') done += 1
170
+ else if (it.status === 'in_progress') doing += 1
171
+ else pending += 1
172
+ }
173
+ const parts: string[] = []
174
+ if (done > 0) parts.push(t('todoDone', { n: done }))
175
+ if (doing > 0) parts.push(t('todoDoing', { n: doing }))
176
+ if (pending > 0) parts.push(t('todoPending', { n: pending }))
177
+ return parts.length === 0 ? t('todoTitle') : `${t('todoTitle')} · ${parts.join(' · ')}`
178
+ }
179
+
114
180
  export interface CardModelInput {
115
181
  /** 工具名兜底(callView 缺失时的标题)。 */
116
182
  toolName: string
@@ -138,9 +204,13 @@ export function cardModelOf(input: CardModelInput): ToolCardModel {
138
204
  case 'terminal': {
139
205
  const rc = result?.card === 'terminal' ? result : undefined
140
206
  const cc = call?.card === 'terminal' ? call : undefined
207
+ // wire 的 title 即命令原文;有人话 description 时标题让位(主区
208
+ // 「Bash · 描述」同款),命令随行进 TerminalBlock。
209
+ const command = rc?.title ?? cc?.title ?? input.toolName
141
210
  return {
142
211
  kind: 'terminal',
143
- title: rc?.title ?? cc?.title ?? input.toolName,
212
+ title: cc?.description !== undefined ? `${displayToolName(input.toolName)} · ${cc.description}` : command,
213
+ command,
144
214
  ...(cc?.description !== undefined ? { description: cc.description } : {}),
145
215
  ...(resolveCwd(cc?.cwd, input.cwdBase) !== undefined ? { cwd: resolveCwd(cc?.cwd, input.cwdBase) } : {}),
146
216
  ...(rc?.output !== undefined ? { output: rc.output } : {}),
@@ -153,7 +223,7 @@ export function cardModelOf(input: CardModelInput): ToolCardModel {
153
223
  const cc = call?.card === 'diff' ? call : undefined
154
224
  return {
155
225
  kind: 'diff',
156
- title: rc?.title ?? cc?.title ?? input.toolName,
226
+ title: displayTitle(rc?.title ?? cc?.title ?? input.toolName),
157
227
  diffs: rc?.diffs ?? cc?.diffs ?? [],
158
228
  ...(cc?.locations !== undefined ? { locations: cc.locations } : {}),
159
229
  }
@@ -177,9 +247,11 @@ export function cardModelOf(input: CardModelInput): ToolCardModel {
177
247
  case 'read': {
178
248
  const rc = result?.card === 'read' ? result : undefined
179
249
  if (rc === undefined) return genericModel(input, call, result)
250
+ // 标题从 path 重建(「Read · 末3段」)——0.1.1 wire 的绝对路径标题与
251
+ // 0.1.2 推导路径统一到同一形态(主区「Read · 仓相对路径」同款)。
180
252
  return {
181
253
  kind: 'read',
182
- title: rc.title ?? call?.title ?? input.toolName,
254
+ title: `Read · ${shortPath(rc.path)}`,
183
255
  path: rc.path,
184
256
  lines: rc.lines,
185
257
  totalLines: rc.totalLines,
@@ -216,9 +288,14 @@ function genericModel(
216
288
  ): ToolCardModel {
217
289
  const gc = call?.card === 'generic' ? call : undefined
218
290
  const gr = result?.card === 'generic' ? result : undefined
291
+ // todo_write → 任务卡(主区「任务 · N 已完成 · …」同款;rawInput 即 {todos})。
292
+ if (input.toolName === 'todo_write') {
293
+ const items = todoItemsOf(gc?.rawInput)
294
+ if (items !== undefined) return { kind: 'todo', title: todoTitleOf(items), items }
295
+ }
219
296
  return {
220
297
  kind: 'generic',
221
- title: gr?.title ?? gc?.title ?? input.toolName,
298
+ title: displayTitle(gr?.title ?? gc?.title ?? input.toolName),
222
299
  icon: gc?.kind ?? 'other',
223
300
  ...(gc?.rawInput !== undefined ? { rawInput: gc.rawInput } : {}),
224
301
  bodyText: blocksText(gr?.content) ?? blocksText(gc?.content) ?? input.rawText,
@@ -226,6 +303,11 @@ function genericModel(
226
303
  }
227
304
  }
228
305
 
306
+ /** 工具机器名 → 展示名(主区「Bash · …」的同款首字母大写)。 */
307
+ function displayToolName(name: string): string {
308
+ return name.charAt(0).toUpperCase() + name.slice(1)
309
+ }
310
+
229
311
  // ── 0.1.2 推导路径(callView/resultView 在 0.1.2 从节点模型移除) ────────────
230
312
  //
231
313
  // 0.1.2 重构:工具卡从「wire 携带渲染意图(viewFor 现算)」改为「客户端从原始
@@ -274,8 +356,111 @@ function readMetaOf(meta: unknown): { path: string; lines: { number: number; tex
274
356
  }
275
357
  }
276
358
 
359
+ /**
360
+ * FsDiffMeta 校验(0.1.2 diff 卡,dsh-tool-fs computeHunkDiffs 产物的忠实子集)。
361
+ * 返回值三态:{diffs 非空} / 'empty'(空数组——write create/同内容覆写是合法值,
362
+ * 客户端回退 args 整文件 diff)/ undefined(malformed——降级)。
363
+ * 逐项校验,一项违规整卡降级(官方 narrowDiffs 同款)。
364
+ */
365
+ function diffMetaOf(meta: unknown): { diffs: readonly FileDiff[] } | 'empty' | undefined {
366
+ if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) return undefined
367
+ const m = meta as { diffs?: unknown }
368
+ if (!Array.isArray(m.diffs)) return undefined
369
+ if (m.diffs.length === 0) return 'empty'
370
+ const diffs: FileDiff[] = []
371
+ for (const raw of m.diffs) {
372
+ const d = raw as { path?: unknown; oldText?: unknown; newText?: unknown }
373
+ if (typeof d?.path !== 'string') return undefined
374
+ if (d.oldText !== null && typeof d.oldText !== 'string') return undefined
375
+ if (typeof d.newText !== 'string') return undefined
376
+ diffs.push({ path: d.path, oldText: d.oldText, newText: d.newText } as FileDiff)
377
+ }
378
+ return { diffs }
379
+ }
380
+
381
+ /** SearchMeta 校验(0.1.2 search 卡;truncated/total 是硬字段;空结果合法)。 */
382
+ function searchMetaOf(meta: unknown):
383
+ | { shape: 'matches'; files: readonly { path: string; matches: readonly { lineNumber: number; line: string }[] }[]; truncated: boolean; total: number }
384
+ | { shape: 'paths'; paths: readonly string[]; truncated: boolean; total: number }
385
+ | undefined {
386
+ if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) return undefined
387
+ const m = meta as Record<string, unknown>
388
+ if (typeof m.truncated !== 'boolean') return undefined
389
+ if (typeof m.total !== 'number' || !Number.isInteger(m.total) || m.total < 0) return undefined
390
+ if (m.shape === 'matches') {
391
+ if (!Array.isArray(m.files)) return undefined
392
+ const files: { path: string; matches: { lineNumber: number; line: string }[] }[] = []
393
+ for (const rawF of m.files) {
394
+ const f = rawF as { path?: unknown; matches?: unknown }
395
+ if (typeof f?.path !== 'string' || !Array.isArray(f.matches)) return undefined
396
+ const matches: { lineNumber: number; line: string }[] = []
397
+ for (const rawM of f.matches) {
398
+ const mm = rawM as { lineNumber?: unknown; line?: unknown }
399
+ if (typeof mm?.lineNumber !== 'number' || !Number.isInteger(mm.lineNumber) || mm.lineNumber < 1) return undefined
400
+ if (typeof mm.line !== 'string') return undefined
401
+ matches.push({ lineNumber: mm.lineNumber, line: mm.line })
402
+ }
403
+ files.push({ path: f.path, matches })
404
+ }
405
+ return { shape: 'matches', files, truncated: m.truncated, total: m.total }
406
+ }
407
+ if (m.shape === 'paths') {
408
+ if (!Array.isArray(m.paths) || !m.paths.every(p => typeof p === 'string')) return undefined
409
+ return { shape: 'paths', paths: m.paths as readonly string[], truncated: m.truncated, total: m.total }
410
+ }
411
+ return undefined
412
+ }
413
+
414
+ /** WebSearchMeta 校验(sources 逐项 url 必填、title/snippet 可选;publishedAt 校验后丢弃)。 */
415
+ function webSearchMetaOf(meta: unknown): { sources: readonly { url: string; title?: string; snippet?: string }[]; truncated: boolean; answer?: string } | undefined {
416
+ if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) return undefined
417
+ const m = meta as Record<string, unknown>
418
+ if (typeof m.truncated !== 'boolean') return undefined
419
+ if (!Array.isArray(m.sources)) return undefined
420
+ const sources: { url: string; title?: string; snippet?: string }[] = []
421
+ for (const raw of m.sources) {
422
+ const s = raw as { url?: unknown; title?: unknown; snippet?: unknown; publishedAt?: unknown }
423
+ if (typeof s?.url !== 'string') return undefined
424
+ if (s.title !== undefined && typeof s.title !== 'string') return undefined
425
+ if (s.snippet !== undefined && typeof s.snippet !== 'string') return undefined
426
+ if (s.publishedAt !== undefined && typeof s.publishedAt !== 'string') return undefined
427
+ sources.push({
428
+ url: s.url,
429
+ ...(s.title !== undefined ? { title: s.title as string } : {}),
430
+ ...(s.snippet !== undefined ? { snippet: s.snippet as string } : {}),
431
+ })
432
+ }
433
+ if (m.answer !== undefined && typeof m.answer !== 'string') return undefined
434
+ return { sources, truncated: m.truncated, ...(m.answer !== undefined ? { answer: m.answer as string } : {}) }
435
+ }
436
+
437
+ /** WebFetchMeta 校验(url + 整数 statusCode + truncated 硬字段)。 */
438
+ function webFetchMetaOf(meta: unknown): { url: string; statusCode: number; truncated: boolean } | undefined {
439
+ if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) return undefined
440
+ const m = meta as Record<string, unknown>
441
+ if (typeof m.url !== 'string') return undefined
442
+ if (typeof m.statusCode !== 'number' || !Number.isInteger(m.statusCode)) return undefined
443
+ if (typeof m.truncated !== 'boolean') return undefined
444
+ return { url: m.url, statusCode: m.statusCode, truncated: m.truncated }
445
+ }
446
+
277
447
  /** argsRaw(JSON 字符串)里尽力提取展示字段。 */
278
- function argsSummary(argsRaw: string | undefined): { command?: string; description?: string; path?: string } {
448
+ function argsSummary(argsRaw: string | undefined): {
449
+ command?: string
450
+ description?: string
451
+ path?: string
452
+ todos?: unknown
453
+ /** diff 回退/标题:write 的 content、edit 的 old_string/new_string。 */
454
+ content?: string
455
+ oldString?: string
456
+ newString?: string
457
+ /** search 标题:pattern/include。 */
458
+ pattern?: string
459
+ include?: string
460
+ /** web 标题:queries(search)/ url(fetch)。 */
461
+ queries?: readonly string[]
462
+ url?: string
463
+ } {
279
464
  if (argsRaw === undefined || argsRaw === '') return {}
280
465
  try {
281
466
  const parsed: unknown = JSON.parse(argsRaw)
@@ -285,6 +470,14 @@ function argsSummary(argsRaw: string | undefined): { command?: string; descripti
285
470
  ...(typeof a.command === 'string' ? { command: a.command } : {}),
286
471
  ...(typeof a.description === 'string' ? { description: a.description } : {}),
287
472
  ...(typeof a.file_path === 'string' ? { path: a.file_path } : typeof a.path === 'string' ? { path: a.path } : {}),
473
+ ...(Array.isArray(a.todos) ? { todos: a.todos } : {}),
474
+ ...(typeof a.content === 'string' ? { content: a.content } : {}),
475
+ ...(typeof a.old_string === 'string' ? { oldString: a.old_string } : {}),
476
+ ...(typeof a.new_string === 'string' ? { newString: a.new_string } : {}),
477
+ ...(typeof a.pattern === 'string' ? { pattern: a.pattern } : {}),
478
+ ...(typeof a.include === 'string' ? { include: a.include } : {}),
479
+ ...(Array.isArray(a.queries) && a.queries.every(q => typeof q === 'string') ? { queries: a.queries as readonly string[] } : {}),
480
+ ...(typeof a.url === 'string' ? { url: a.url } : {}),
288
481
  }
289
482
  } catch {
290
483
  return {}
@@ -300,6 +493,10 @@ export interface NodeCardInput {
300
493
  meta?: unknown
301
494
  /** 结果正文(已拼 text 块)。 */
302
495
  rawText: string
496
+ /** 失败标记(0.1.2 节点 isError)——失败结果无 meta(presentationMeta 只投
497
+ * 影成功路径),但 write 的空-meta 回退必须先判它,否则失败的 write 也
498
+ * 会被渲染成 diff 卡(官方 diffCardModel 显式 isError → null)。 */
499
+ isError?: boolean | undefined
303
500
  cwdBase?: string | undefined
304
501
  }
305
502
 
@@ -312,11 +509,14 @@ export function cardModelFromNode(input: NodeCardInput): ToolCardModel {
312
509
  const name = input.name
313
510
 
314
511
  // bash 家族 → terminal 卡(含 terminal_send 不在此列——那是另一个工具面)。
512
+ // 标题让位给人话 description(主区「Bash · 描述」同款);命令随行进 TerminalBlock。
315
513
  if (name === 'bash' || name === 'bash-persistent' || name === 'pwsh' || name === 'pwsh-persistent') {
316
514
  const { output, exitCode, signal } = parseExitStatus(input.rawText)
515
+ const command = args.command ?? name
317
516
  return {
318
517
  kind: 'terminal',
319
- title: args.command ?? name,
518
+ title: args.description !== undefined ? `${displayToolName(name)} · ${args.description}` : command,
519
+ command,
320
520
  ...(args.description !== undefined ? { description: args.description } : {}),
321
521
  output,
322
522
  ...(exitCode !== undefined ? { exitCode } : {}),
@@ -330,7 +530,7 @@ export function cardModelFromNode(input: NodeCardInput): ToolCardModel {
330
530
  if (meta !== undefined) {
331
531
  return {
332
532
  kind: 'read',
333
- title: `Read ${meta.path}`,
533
+ title: `Read · ${shortPath(meta.path)}`,
334
534
  path: meta.path,
335
535
  lines: meta.lines,
336
536
  totalLines: meta.totalLines,
@@ -339,9 +539,80 @@ export function cardModelFromNode(input: NodeCardInput): ToolCardModel {
339
539
  }
340
540
  }
341
541
 
542
+ // todo_write → 任务卡(标题带非零状态计数)。
543
+ if (name === 'todo_write') {
544
+ const items = todoItemsOf(args.todos)
545
+ if (items !== undefined) return { kind: 'todo', title: todoTitleOf(items), items }
546
+ }
547
+
548
+ // write/edit → diff 卡(0.1.2 meta 逆向:FsDiffMeta {diffs: FileDiff[]})。
549
+ // 官方语义(diffCardModel 实证):失败结果不走 diff 卡;write 的 meta
550
+ // 缺失/malformed/空数组 → 回退 args 整文件 diff(create 语义);edit 的
551
+ // meta 缺失/空 → 降级 generic。str_replace_editor settled 无 meta,官方走
552
+ // generic——不在此列。
553
+ if ((name === 'write' || name === 'edit') && input.isError !== true) {
554
+ const path = args.path
555
+ if (path !== undefined && path.trim() !== '') {
556
+ const dm = diffMetaOf(input.meta)
557
+ if (dm !== undefined && dm !== 'empty') {
558
+ return {
559
+ kind: 'diff',
560
+ title: displayTitle(`${name === 'write' ? 'Write' : 'Edit'} ${path}`),
561
+ diffs: dm.diffs,
562
+ locations: [{ path } as FileLocation],
563
+ }
564
+ }
565
+ // write 回退:整文件 diff(create/同内容覆写/meta 缺席同形)。
566
+ if (name === 'write' && args.content !== undefined) {
567
+ return {
568
+ kind: 'diff',
569
+ title: displayTitle(`Write ${path}`),
570
+ diffs: [{ path, oldText: null, newText: args.content } as FileDiff],
571
+ locations: [{ path } as FileLocation],
572
+ }
573
+ }
574
+ }
575
+ }
576
+
577
+ // grep/glob → search 卡(SearchMeta:shape 与工具名交叉校验;空结果合法)。
578
+ if ((name === 'grep' || name === 'glob') && input.isError !== true) {
579
+ const sm = searchMetaOf(input.meta)
580
+ const wantShape = name === 'grep' ? 'matches' : 'paths'
581
+ if (sm !== undefined && sm.shape === wantShape && args.pattern !== undefined) {
582
+ const where = args.path !== undefined && args.path.trim() !== '' ? ` in ${shortPath(args.path)}` : ''
583
+ const inc = name === 'grep' && args.include !== undefined && args.include.trim() !== '' ? ` (${args.include})` : ''
584
+ const title = `${name === 'grep' ? 'Grep' : 'Glob'} ${args.pattern}${where}${inc}`
585
+ return sm.shape === 'matches'
586
+ ? { kind: 'search', title, shape: 'matches', files: sm.files, truncated: sm.truncated, total: sm.total }
587
+ : { kind: 'search', title, shape: 'paths', paths: sm.paths, truncated: sm.truncated, total: sm.total }
588
+ }
589
+ }
590
+
591
+ // web_search/web_fetch → web 卡(meta 直读;publishedAt 校验后降采样丢弃)。
592
+ if (name === 'web_search' && input.isError !== true) {
593
+ const wm = webSearchMetaOf(input.meta)
594
+ const queries = args.queries?.filter(q => q.trim() !== '') ?? []
595
+ if (wm !== undefined && queries.length > 0) {
596
+ return {
597
+ kind: 'web',
598
+ title: queries.join(', '),
599
+ webKind: 'search',
600
+ sources: wm.sources,
601
+ truncated: wm.truncated,
602
+ ...(wm.answer !== undefined ? { answer: wm.answer } : {}),
603
+ }
604
+ }
605
+ }
606
+ if (name === 'web_fetch' && input.isError !== true) {
607
+ const wm = webFetchMetaOf(input.meta)
608
+ if (wm !== undefined && args.url !== undefined && args.url.trim() !== '') {
609
+ return { kind: 'web', title: args.url, webKind: 'fetch', url: wm.url, statusCode: wm.statusCode, truncated: wm.truncated }
610
+ }
611
+ }
612
+
342
613
  // 其余:generic(标题尽力从参数提取可读形态)。
343
614
  const title = args.path !== undefined
344
- ? `${name} ${args.path}`
615
+ ? `${name} · ${shortPath(args.path)}`
345
616
  : args.command ?? name
346
617
  return {
347
618
  kind: 'generic',
@@ -351,19 +622,22 @@ export function cardModelFromNode(input: NodeCardInput): ToolCardModel {
351
622
  }
352
623
  }
353
624
 
354
- /** 工具名 → 图标类别的静态映射(0.1.2 没有 callView.kind 可用)。 */
625
+ /** 工具名 → 图标类别的静态映射(0.1.2 没有 callView.kind 可用)。
626
+ * wire 名实证:str_replace_editor 是下划线(uitool client.js L381);
627
+ * web_search 的 0.1.1 wire kind 是 'search'(presentSearchCall),
628
+ * 'fetch' 是 web_fetch 的(presentFetchCall)。 */
355
629
  const KIND_BY_NAME: Record<string, ToolCallKind> = {
356
630
  read: 'read',
357
631
  write: 'edit',
358
632
  edit: 'edit',
359
- 'str-replace-editor': 'edit',
633
+ str_replace_editor: 'edit',
360
634
  bash: 'execute',
361
635
  'bash-persistent': 'execute',
362
636
  pwsh: 'execute',
363
637
  'pwsh-persistent': 'execute',
364
638
  grep: 'search',
365
639
  glob: 'search',
366
- web_search: 'fetch',
640
+ web_search: 'search',
367
641
  web_fetch: 'fetch',
368
642
  }
369
643
 
@@ -133,6 +133,7 @@ export function nodeToMessage(node: unknown): ChatMessage | null {
133
133
  argsRaw: typeof call?.argsRaw === 'string' ? call.argsRaw : undefined,
134
134
  meta: n.meta,
135
135
  rawText: text,
136
+ ...(n.isError === true ? { isError: true } : {}),
136
137
  })
137
138
  return {
138
139
  key: seqKey('t', n),
@@ -30,16 +30,30 @@ export const en = {
30
30
  stopped: 'Stopped',
31
31
  toolLabel: 'Tool',
32
32
  failed: 'failed',
33
+ todoTitle: 'Tasks',
34
+ todoDone: '{n} done',
35
+ todoDoing: '{n} in progress',
36
+ todoPending: '{n} pending',
33
37
  running: 'running…',
34
38
  inputPlaceholder: 'Message… (Enter to send, Shift+Enter for a newline)',
35
39
  modelLabel: 'Model: {name}',
36
40
  modelSwitchTitle: 'Switch the side chat model',
37
41
  permChipTitle: 'Permission mode',
38
42
  permFullAccess: 'Full access',
39
- pendingNotice: '⚠ 侧边会话有审批/提问待处理——点击跳到主视图处理',
40
- writeNotice: '侧边会话与主会话同权限,可以修改文件(当前模式见输入框旁 chip)',
43
+ pendingNotice: '⚠ The side session has a pending approval/question — click to handle it in the main view',
44
+ writeNotice: 'The side session shares the main session\'s permissions and can modify files (current mode: chip next to the input)',
45
+ pendingApprovalTitle: 'Waiting for approval',
46
+ pendingApprovalDesc: 'Tool {name} requests elevated execution',
47
+ pendingReject: 'Reject',
48
+ pendingAllowOnce: 'Allow once',
49
+ pendingQuestionTitle: 'Question',
50
+ pendingPlanReview: 'Plan review',
51
+ pendingQuestionSubmit: 'Submit',
52
+ pendingQuestionCancel: 'Cancel',
53
+ pendingCustomPlaceholder: 'Custom answer…',
41
54
  modelFollowsMain: 'follows main session',
42
55
  stopReply: 'Stop',
56
+ sideChatDone: '「{title}」finished replying',
43
57
  stopReplyTitle: 'Stop the current reply',
44
58
  send: 'Send',
45
59
  attachTitle: 'Attach images',
@@ -140,6 +154,7 @@ export const en = {
140
154
  reflowAllDone: 'Reflowed ✓',
141
155
  promote: 'Save as session',
142
156
  promoteTitle: 'Fork this side chat into a standalone session in the main list',
157
+ promoteDone: 'Saved as a regular session — it\'s open in the main view now',
143
158
 
144
159
  // 工具卡叶子块 labels(0.1.2 起必填——host/labels.ts)
145
160
  readWindow: '{shown} of {total} lines',
@@ -181,6 +196,10 @@ export const zh: Record<CopyKey, string> = {
181
196
  stopped: '已停止',
182
197
  toolLabel: '工具',
183
198
  failed: '失败',
199
+ todoTitle: '任务',
200
+ todoDone: '{n} 已完成',
201
+ todoDoing: '{n} 进行中',
202
+ todoPending: '{n} 待处理',
184
203
  running: '执行中…',
185
204
  inputPlaceholder: '输入消息…(Enter 发送,Shift+Enter 换行)',
186
205
  modelLabel: '模型:{name}',
@@ -189,8 +208,18 @@ export const zh: Record<CopyKey, string> = {
189
208
  permFullAccess: 'Full access',
190
209
  pendingNotice: '⚠ 侧边会话有审批/提问待处理——点击跳到主视图处理',
191
210
  writeNotice: '侧边会话与主会话同权限,可以修改文件(当前模式见输入框旁 chip)',
211
+ pendingApprovalTitle: '等待审批',
212
+ pendingApprovalDesc: '工具 {name} 请求越权执行',
213
+ pendingReject: '拒绝',
214
+ pendingAllowOnce: '允许一次',
215
+ pendingQuestionTitle: '提问',
216
+ pendingPlanReview: '计划审批',
217
+ pendingQuestionSubmit: '提交回答',
218
+ pendingQuestionCancel: '取消',
219
+ pendingCustomPlaceholder: '自定义回答…',
192
220
  modelFollowsMain: '跟随主会话',
193
221
  stopReply: '停止',
222
+ sideChatDone: '「{title}」回复完成',
194
223
  stopReplyTitle: '停止当前回复',
195
224
  send: '发送',
196
225
  attachTitle: '添加图片附件',
@@ -279,6 +308,7 @@ export const zh: Record<CopyKey, string> = {
279
308
  reflowAllDone: '已回流 ✓',
280
309
  promote: '保存为正式会话',
281
310
  promoteTitle: '把侧边聊天转为会话列表里的正式会话',
311
+ promoteDone: '已保存为正式会话——已在主视图打开',
282
312
 
283
313
  readWindow: '{shown}/{total} 行',
284
314
  searchPathsSummary: '{shown}/{total} 个路径',