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.
- package/README.md +2 -2
- package/lib/client.js +1015 -203
- package/lib/types/client/chat/ToolCard.d.ts +1 -0
- package/lib/types/client/chat/cards.d.ts +25 -0
- package/lib/types/client/locales.d.ts +15 -0
- package/lib/types/client/sidechat/PendingCard.d.ts +4 -0
- package/lib/types/client/sidechat/notify.d.ts +16 -0
- package/lib/types/client/sidechat/pending.d.ts +71 -0
- package/lib/types/client/sidechat/toast.d.ts +5 -0
- package/package.json +1 -1
- package/src/client/chat/FoldCard.tsx +2 -1
- package/src/client/chat/ReasoningRow.tsx +13 -1
- package/src/client/chat/ToolCard.tsx +20 -4
- package/src/client/chat/cards.ts +285 -11
- package/src/client/chat/transcript.ts +1 -0
- package/src/client/locales.ts +32 -2
- package/src/client/sidechat/ComposerBar.tsx +100 -95
- package/src/client/sidechat/ModelMenu.tsx +3 -2
- package/src/client/sidechat/PendingCard.tsx +145 -0
- package/src/client/sidechat/PermissionChip.tsx +4 -2
- package/src/client/sidechat/SideChatPanel.tsx +32 -2
- package/src/client/sidechat/index.tsx +6 -0
- package/src/client/sidechat/notify.ts +91 -0
- package/src/client/sidechat/pending.ts +238 -0
- package/src/client/sidechat/rows.tsx +1 -0
- package/src/client/sidechat/sidechat.module.css +259 -33
- package/src/client/sidechat/toast.tsx +60 -0
package/src/client/chat/cards.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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): {
|
|
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.
|
|
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
|
-
|
|
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: '
|
|
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),
|
package/src/client/locales.ts
CHANGED
|
@@ -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: '
|
|
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} 个路径',
|