dsh-sidenote 0.3.1 → 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/lib/client.js +724 -95
- package/lib/types/client/chat/cards.d.ts +4 -0
- package/lib/types/client/locales.d.ts +10 -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/package.json +1 -1
- package/src/client/chat/cards.ts +186 -4
- package/src/client/chat/transcript.ts +1 -0
- package/src/client/locales.ts +22 -2
- package/src/client/sidechat/PendingCard.tsx +145 -0
- package/src/client/sidechat/SideChatPanel.tsx +27 -1
- package/src/client/sidechat/index.tsx +3 -0
- package/src/client/sidechat/notify.ts +91 -0
- package/src/client/sidechat/pending.ts +238 -0
- package/src/client/sidechat/sidechat.module.css +142 -1
|
@@ -140,6 +140,10 @@ export interface NodeCardInput {
|
|
|
140
140
|
meta?: unknown;
|
|
141
141
|
/** 结果正文(已拼 text 块)。 */
|
|
142
142
|
rawText: string;
|
|
143
|
+
/** 失败标记(0.1.2 节点 isError)——失败结果无 meta(presentationMeta 只投
|
|
144
|
+
* 影成功路径),但 write 的空-meta 回退必须先判它,否则失败的 write 也
|
|
145
|
+
* 会被渲染成 diff 卡(官方 diffCardModel 显式 isError → null)。 */
|
|
146
|
+
isError?: boolean | undefined;
|
|
143
147
|
cwdBase?: string | undefined;
|
|
144
148
|
}
|
|
145
149
|
/**
|
|
@@ -38,8 +38,18 @@ export declare const en: {
|
|
|
38
38
|
permFullAccess: string;
|
|
39
39
|
pendingNotice: string;
|
|
40
40
|
writeNotice: string;
|
|
41
|
+
pendingApprovalTitle: string;
|
|
42
|
+
pendingApprovalDesc: string;
|
|
43
|
+
pendingReject: string;
|
|
44
|
+
pendingAllowOnce: string;
|
|
45
|
+
pendingQuestionTitle: string;
|
|
46
|
+
pendingPlanReview: string;
|
|
47
|
+
pendingQuestionSubmit: string;
|
|
48
|
+
pendingQuestionCancel: string;
|
|
49
|
+
pendingCustomPlaceholder: string;
|
|
41
50
|
modelFollowsMain: string;
|
|
42
51
|
stopReply: string;
|
|
52
|
+
sideChatDone: string;
|
|
43
53
|
stopReplyTitle: string;
|
|
44
54
|
send: string;
|
|
45
55
|
attachTitle: string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 完成通知(WI-03,L3 常驻监听层):侧聊子会话 running true→false 翻转时
|
|
3
|
+
* toast 提醒——用户焦点在主对话时侧聊长任务跑完能被看见(Zed
|
|
4
|
+
* notify_when_agent_waiting 先例)。
|
|
5
|
+
*
|
|
6
|
+
* 事件源是 session.subscribe 快照推送(非轮询);宿主 notifier 入账不依赖
|
|
7
|
+
* 订阅者——面板不可见(SideChatPanel 的 uSES 断订阅)期间事件不丢。
|
|
8
|
+
* 本层是常驻 ctx.effect:Tab 开闭由 betterSidebar.subscribeState 重扫驱动。
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from '../host/contracts.ts';
|
|
11
|
+
/**
|
|
12
|
+
* 翻转侦测核心(抽出来纯可测):running true→false 且距挂接超过保护窗
|
|
13
|
+
* 时返回 true。调用方持状态。
|
|
14
|
+
*/
|
|
15
|
+
export declare function shouldNotify(wasRunning: boolean, running: boolean, attachedAt: number, now: number): boolean;
|
|
16
|
+
export declare function registerCompletionNotify(ctx: Context): void;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R8 侧边审批/提问答复(L0/L1:双版本数据源探测链 + 视图模型归一)。
|
|
3
|
+
*
|
|
4
|
+
* 侦察依据(.vibe Delivery_04/Workitem_02 design.md):
|
|
5
|
+
* - pending 不在 projections.faceOf 体系——0.1.1 在 Session 快照顶层
|
|
6
|
+
* `pending: PendingWait[]`(mux 帧铸造,重连重放 rpcId 不变);0.1.2 移入
|
|
7
|
+
* `ctx.get('uiSession').pendingInteractions`(Map<SessionId, Pending>,
|
|
8
|
+
* 每会话至多一项,precedence 仲裁)。
|
|
9
|
+
* - 写路径都是公开对象的公开方法:0.1.1 `wait.respond({ok,value})`(审批
|
|
10
|
+
* value={sessionId, approvalId, outcome};提问整批 {sessionId, answer},
|
|
11
|
+
* 取消走 {ok:false,error:{code:'cancelled',…}});0.1.2 域对象
|
|
12
|
+
* `answer(outcome|answer)` / `cancel()`。
|
|
13
|
+
* - 竞答安全:先答先得,settle 幂等——后答方 throw,UI catch 吞掉并回退
|
|
14
|
+
* 按钮态(宿主同款 `.catch(() => setAnswered(false))`)。
|
|
15
|
+
*
|
|
16
|
+
* off-face 纪律:两面都 feature-check;全缺席返回 undefined(面板回退
|
|
17
|
+
* pendingBar 跳转行为,不崩)。
|
|
18
|
+
*/
|
|
19
|
+
export interface PendingApprovalView {
|
|
20
|
+
kind: 'approval';
|
|
21
|
+
key: string;
|
|
22
|
+
toolName: string;
|
|
23
|
+
reason?: string | undefined;
|
|
24
|
+
answer: (outcome: 'allowed-once' | 'rejected') => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export interface PendingQuestionOption {
|
|
27
|
+
label: string;
|
|
28
|
+
description?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
export interface PendingQuestionView {
|
|
31
|
+
kind: 'question';
|
|
32
|
+
key: string;
|
|
33
|
+
/** plan-review 按通用提问流降级(提交值逐字回传 asker 原 label)。 */
|
|
34
|
+
planReview: boolean;
|
|
35
|
+
questions: readonly {
|
|
36
|
+
id: string;
|
|
37
|
+
question: string;
|
|
38
|
+
header?: string | undefined;
|
|
39
|
+
detail?: string | undefined;
|
|
40
|
+
options: readonly PendingQuestionOption[];
|
|
41
|
+
multiSelect: boolean;
|
|
42
|
+
}[];
|
|
43
|
+
/** 整批提交(不可拆单题):answers = [{id, selected: label[], custom?}]。 */
|
|
44
|
+
answer: (answers: readonly {
|
|
45
|
+
id: string;
|
|
46
|
+
selected: readonly string[];
|
|
47
|
+
custom?: string;
|
|
48
|
+
}[]) => Promise<void>;
|
|
49
|
+
cancel: () => Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
export type PendingItem = PendingApprovalView | PendingQuestionView;
|
|
52
|
+
export interface PendingStore {
|
|
53
|
+
readonly available: boolean;
|
|
54
|
+
subscribe(fn: () => void): () => void;
|
|
55
|
+
/** 缓存快照:底层引用不变则返回同一数组(uSES 纪律)。 */
|
|
56
|
+
getSnapshot(): readonly PendingItem[];
|
|
57
|
+
}
|
|
58
|
+
interface CtxLike {
|
|
59
|
+
get?: (key: string) => unknown;
|
|
60
|
+
}
|
|
61
|
+
interface SessionLike {
|
|
62
|
+
subscribe(fn: () => void): () => void;
|
|
63
|
+
getSnapshot(): unknown;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 双版本探测链(chatSourceOf 同款纪律):0.1.2 uiSession.pendingInteractions
|
|
67
|
+
* (getSnapshot() 是 Map 才认)优先;0.1.1 Session 快照 pending 数组回退;
|
|
68
|
+
* 双缺席 undefined(面板回退 pendingBar 跳转)。
|
|
69
|
+
*/
|
|
70
|
+
export declare function createPendingStore(ctx: CtxLike, session: SessionLike | undefined, childId: string): PendingStore | undefined;
|
|
71
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sidenote",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "DSH web plugin: Codex-style side chat (fork the current session into a persistent side panel) + selection annotations with numbered badges and a composer chip. A thin consumer of dsh-better-sidebar. / Codex 风格侧边聊天 + 划选注释的 DSH 插件。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/client/chat/cards.ts
CHANGED
|
@@ -356,8 +356,111 @@ function readMetaOf(meta: unknown): { path: string; lines: { number: number; tex
|
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
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
|
+
|
|
359
447
|
/** argsRaw(JSON 字符串)里尽力提取展示字段。 */
|
|
360
|
-
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
|
+
} {
|
|
361
464
|
if (argsRaw === undefined || argsRaw === '') return {}
|
|
362
465
|
try {
|
|
363
466
|
const parsed: unknown = JSON.parse(argsRaw)
|
|
@@ -368,6 +471,13 @@ function argsSummary(argsRaw: string | undefined): { command?: string; descripti
|
|
|
368
471
|
...(typeof a.description === 'string' ? { description: a.description } : {}),
|
|
369
472
|
...(typeof a.file_path === 'string' ? { path: a.file_path } : typeof a.path === 'string' ? { path: a.path } : {}),
|
|
370
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 } : {}),
|
|
371
481
|
}
|
|
372
482
|
} catch {
|
|
373
483
|
return {}
|
|
@@ -383,6 +493,10 @@ export interface NodeCardInput {
|
|
|
383
493
|
meta?: unknown
|
|
384
494
|
/** 结果正文(已拼 text 块)。 */
|
|
385
495
|
rawText: string
|
|
496
|
+
/** 失败标记(0.1.2 节点 isError)——失败结果无 meta(presentationMeta 只投
|
|
497
|
+
* 影成功路径),但 write 的空-meta 回退必须先判它,否则失败的 write 也
|
|
498
|
+
* 会被渲染成 diff 卡(官方 diffCardModel 显式 isError → null)。 */
|
|
499
|
+
isError?: boolean | undefined
|
|
386
500
|
cwdBase?: string | undefined
|
|
387
501
|
}
|
|
388
502
|
|
|
@@ -431,6 +545,71 @@ export function cardModelFromNode(input: NodeCardInput): ToolCardModel {
|
|
|
431
545
|
if (items !== undefined) return { kind: 'todo', title: todoTitleOf(items), items }
|
|
432
546
|
}
|
|
433
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
|
+
|
|
434
613
|
// 其余:generic(标题尽力从参数提取可读形态)。
|
|
435
614
|
const title = args.path !== undefined
|
|
436
615
|
? `${name} · ${shortPath(args.path)}`
|
|
@@ -443,19 +622,22 @@ export function cardModelFromNode(input: NodeCardInput): ToolCardModel {
|
|
|
443
622
|
}
|
|
444
623
|
}
|
|
445
624
|
|
|
446
|
-
/** 工具名 → 图标类别的静态映射(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)。 */
|
|
447
629
|
const KIND_BY_NAME: Record<string, ToolCallKind> = {
|
|
448
630
|
read: 'read',
|
|
449
631
|
write: 'edit',
|
|
450
632
|
edit: 'edit',
|
|
451
|
-
|
|
633
|
+
str_replace_editor: 'edit',
|
|
452
634
|
bash: 'execute',
|
|
453
635
|
'bash-persistent': 'execute',
|
|
454
636
|
pwsh: 'execute',
|
|
455
637
|
'pwsh-persistent': 'execute',
|
|
456
638
|
grep: 'search',
|
|
457
639
|
glob: 'search',
|
|
458
|
-
web_search: '
|
|
640
|
+
web_search: 'search',
|
|
459
641
|
web_fetch: 'fetch',
|
|
460
642
|
}
|
|
461
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
|
@@ -40,10 +40,20 @@ export const en = {
|
|
|
40
40
|
modelSwitchTitle: 'Switch the side chat model',
|
|
41
41
|
permChipTitle: 'Permission mode',
|
|
42
42
|
permFullAccess: 'Full access',
|
|
43
|
-
pendingNotice: '⚠
|
|
44
|
-
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…',
|
|
45
54
|
modelFollowsMain: 'follows main session',
|
|
46
55
|
stopReply: 'Stop',
|
|
56
|
+
sideChatDone: '「{title}」finished replying',
|
|
47
57
|
stopReplyTitle: 'Stop the current reply',
|
|
48
58
|
send: 'Send',
|
|
49
59
|
attachTitle: 'Attach images',
|
|
@@ -198,8 +208,18 @@ export const zh: Record<CopyKey, string> = {
|
|
|
198
208
|
permFullAccess: 'Full access',
|
|
199
209
|
pendingNotice: '⚠ 侧边会话有审批/提问待处理——点击跳到主视图处理',
|
|
200
210
|
writeNotice: '侧边会话与主会话同权限,可以修改文件(当前模式见输入框旁 chip)',
|
|
211
|
+
pendingApprovalTitle: '等待审批',
|
|
212
|
+
pendingApprovalDesc: '工具 {name} 请求越权执行',
|
|
213
|
+
pendingReject: '拒绝',
|
|
214
|
+
pendingAllowOnce: '允许一次',
|
|
215
|
+
pendingQuestionTitle: '提问',
|
|
216
|
+
pendingPlanReview: '计划审批',
|
|
217
|
+
pendingQuestionSubmit: '提交回答',
|
|
218
|
+
pendingQuestionCancel: '取消',
|
|
219
|
+
pendingCustomPlaceholder: '自定义回答…',
|
|
201
220
|
modelFollowsMain: '跟随主会话',
|
|
202
221
|
stopReply: '停止',
|
|
222
|
+
sideChatDone: '「{title}」回复完成',
|
|
203
223
|
stopReplyTitle: '停止当前回复',
|
|
204
224
|
send: '发送',
|
|
205
225
|
attachTitle: '添加图片附件',
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R8 审批/提问答复卡(L2 视图):钉在 composer 上方(原 pendingBar 位置)。
|
|
3
|
+
* 自绘 + primitives 图标(材质同源,不求像素级复刻宿主审批卡)。
|
|
4
|
+
*
|
|
5
|
+
* 竞答纪律:先答先得——点击后置 answered 本地态;answer 失败(已被主视图
|
|
6
|
+
* 抢答/settle)catch 吞错并回退按钮态(宿主 `.catch(() => setAnswered(false))`
|
|
7
|
+
* 同款);卡片消失由快照驱动(pending 项消失即卸载),UI 不自造状态机。
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from 'react'
|
|
10
|
+
import { IconWarningOutline16, IconQuestionOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
11
|
+
import type { PendingItem, PendingQuestionView } from './pending.ts'
|
|
12
|
+
import { t } from '../locales.ts'
|
|
13
|
+
import { useLocaleTick } from '../locale-tick.ts'
|
|
14
|
+
import css from './sidechat.module.css'
|
|
15
|
+
|
|
16
|
+
export function PendingCardList({ items }: { items: readonly PendingItem[] }) {
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
{items.map(item => item.kind === 'approval'
|
|
20
|
+
? <ApprovalCard key={item.key} item={item} />
|
|
21
|
+
: <QuestionCard key={item.key} item={item} />)}
|
|
22
|
+
</>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 审批卡:reason + 拒绝 / 允许一次。 */
|
|
27
|
+
function ApprovalCard({ item }: { item: Extract<PendingItem, { kind: 'approval' }> }) {
|
|
28
|
+
useLocaleTick()
|
|
29
|
+
const [answered, setAnswered] = useState(false)
|
|
30
|
+
const answer = (outcome: 'allowed-once' | 'rejected'): void => {
|
|
31
|
+
setAnswered(true)
|
|
32
|
+
item.answer(outcome).catch(() => { setAnswered(false) })
|
|
33
|
+
}
|
|
34
|
+
return (
|
|
35
|
+
<div className={css.pendingCard} data-kind="approval">
|
|
36
|
+
<div className={css.pendingHead}>
|
|
37
|
+
<IconWarningOutline16 size={14} />
|
|
38
|
+
<span className={css.pendingTitle}>{t('pendingApprovalTitle')}</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div className={css.pendingDesc}>{t('pendingApprovalDesc', { name: item.toolName })}</div>
|
|
41
|
+
{item.reason !== undefined && item.reason !== '' && <div className={css.pendingReason}>{item.reason}</div>}
|
|
42
|
+
<div className={css.pendingActions}>
|
|
43
|
+
<button type="button" className={css.pendingBtnGhost} disabled={answered} onClick={() => { answer('rejected') }}>
|
|
44
|
+
{t('pendingReject')}
|
|
45
|
+
</button>
|
|
46
|
+
<button type="button" className={css.pendingBtnPrimary} disabled={answered} onClick={() => { answer('allowed-once') }}>
|
|
47
|
+
{t('pendingAllowOnce')}
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 提问卡:选项(单选即点即答语义并入整批提交)+ 自定义 + 整批提交/取消。 */
|
|
55
|
+
function QuestionCard({ item }: { item: PendingQuestionView }) {
|
|
56
|
+
useLocaleTick()
|
|
57
|
+
// 每题的选择集合(多选为 Set;单选为单元素 Set)+ 自定义文本。
|
|
58
|
+
const [selections, setSelections] = useState<ReadonlyMap<string, ReadonlySet<string>>>(new Map())
|
|
59
|
+
const [customs, setCustoms] = useState<ReadonlyMap<string, string>>(new Map())
|
|
60
|
+
const [answered, setAnswered] = useState(false)
|
|
61
|
+
|
|
62
|
+
const toggle = (qid: string, label: string, multi: boolean): void => {
|
|
63
|
+
const next = new Map(selections)
|
|
64
|
+
const cur = new Set(next.get(qid) ?? [])
|
|
65
|
+
if (multi) {
|
|
66
|
+
if (cur.has(label)) cur.delete(label)
|
|
67
|
+
else cur.add(label)
|
|
68
|
+
} else {
|
|
69
|
+
cur.clear()
|
|
70
|
+
cur.add(label)
|
|
71
|
+
}
|
|
72
|
+
next.set(qid, cur)
|
|
73
|
+
setSelections(next)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 提交门槛:每题至少有选中项或自定义文本(整批语义,不留空题)。
|
|
77
|
+
const ready = item.questions.every(q =>
|
|
78
|
+
(selections.get(q.id)?.size ?? 0) > 0 || (customs.get(q.id)?.trim() ?? '') !== '',
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const submit = (): void => {
|
|
82
|
+
setAnswered(true)
|
|
83
|
+
item.answer(item.questions.map(q => ({
|
|
84
|
+
id: q.id,
|
|
85
|
+
selected: [...(selections.get(q.id) ?? [])],
|
|
86
|
+
...((customs.get(q.id)?.trim() ?? '') !== '' ? { custom: customs.get(q.id)!.trim() } : {}),
|
|
87
|
+
}))).catch(() => { setAnswered(false) })
|
|
88
|
+
}
|
|
89
|
+
const cancel = (): void => {
|
|
90
|
+
setAnswered(true)
|
|
91
|
+
item.cancel().catch(() => { setAnswered(false) })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className={css.pendingCard} data-kind="question">
|
|
96
|
+
<div className={css.pendingHead}>
|
|
97
|
+
<IconQuestionOutline14 size={14} />
|
|
98
|
+
<span className={css.pendingTitle}>
|
|
99
|
+
{item.planReview ? t('pendingPlanReview') : t('pendingQuestionTitle')}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
{item.questions.map(q => (
|
|
103
|
+
<div key={q.id} className={css.pendingQuestion}>
|
|
104
|
+
{q.header !== undefined && q.header !== '' && <div className={css.pendingQHeader}>{q.header}</div>}
|
|
105
|
+
<div className={css.pendingQText}>{q.question}</div>
|
|
106
|
+
{q.detail !== undefined && q.detail !== '' && <div className={css.pendingReason}>{q.detail}</div>}
|
|
107
|
+
{q.options.map(o => {
|
|
108
|
+
const active = selections.get(q.id)?.has(o.label) === true
|
|
109
|
+
return (
|
|
110
|
+
<button
|
|
111
|
+
key={o.label}
|
|
112
|
+
type="button"
|
|
113
|
+
className={active ? css.pendingOptionActive : css.pendingOption}
|
|
114
|
+
disabled={answered}
|
|
115
|
+
onClick={() => { toggle(q.id, o.label, q.multiSelect) }}
|
|
116
|
+
>
|
|
117
|
+
{o.label}
|
|
118
|
+
{o.description !== undefined && o.description !== '' && <span className={css.pendingOptionDesc}>{o.description}</span>}
|
|
119
|
+
</button>
|
|
120
|
+
)
|
|
121
|
+
})}
|
|
122
|
+
<input
|
|
123
|
+
className={css.pendingCustom}
|
|
124
|
+
placeholder={t('pendingCustomPlaceholder')}
|
|
125
|
+
disabled={answered}
|
|
126
|
+
value={customs.get(q.id) ?? ''}
|
|
127
|
+
onChange={(event) => {
|
|
128
|
+
const next = new Map(customs)
|
|
129
|
+
next.set(q.id, event.target.value)
|
|
130
|
+
setCustoms(next)
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
))}
|
|
135
|
+
<div className={css.pendingActions}>
|
|
136
|
+
<button type="button" className={css.pendingBtnGhost} disabled={answered} onClick={cancel}>
|
|
137
|
+
{t('pendingQuestionCancel')}
|
|
138
|
+
</button>
|
|
139
|
+
<button type="button" className={css.pendingBtnPrimary} disabled={answered || !ready} onClick={submit}>
|
|
140
|
+
{t('pendingQuestionSubmit')}
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
@@ -27,11 +27,15 @@ import { FoldCard } from '../chat/FoldCard.tsx'
|
|
|
27
27
|
import { createFoldStore } from '../chat/viewState.ts'
|
|
28
28
|
import type { ReflowStore } from '../reflow.ts'
|
|
29
29
|
import { showToast } from './toast.tsx'
|
|
30
|
+
import { createPendingStore } from './pending.ts'
|
|
31
|
+
import { PendingCardList } from './PendingCard.tsx'
|
|
30
32
|
import { t } from '../locales.ts'
|
|
31
33
|
import { useLocaleTick } from '../locale-tick.ts'
|
|
32
34
|
import css from './sidechat.module.css'
|
|
33
35
|
|
|
34
36
|
const NOOP_UNSUBSCRIBE = (): void => {}
|
|
37
|
+
/** uSES 空快照常量(禁产新引用——`?? []` 字面量每次新数组会无限重渲)。 */
|
|
38
|
+
const NO_PENDING: readonly import('./pending.ts').PendingItem[] = []
|
|
35
39
|
|
|
36
40
|
/** D2 写型工具名(无 diff 卡时的名字兜底)。 */
|
|
37
41
|
const WRITE_TOOLS = new Set(['edit', 'write', 'str-replace-editor', 'str_replace_editor'])
|
|
@@ -133,6 +137,23 @@ export function SideChatPanel(props: TabComponentProps & { reflow: ReflowStore }
|
|
|
133
137
|
),
|
|
134
138
|
() => (parentSession === undefined ? null : parentSession.getSnapshot()),
|
|
135
139
|
)
|
|
140
|
+
// R8:审批/提问数据源(双版本探测链——0.1.2 uiSession.pendingInteractions
|
|
141
|
+
// 优先,0.1.1 Session 快照 pending 回退;面缺席 undefined → 回退纯跳转
|
|
142
|
+
// pendingBar)。hooks 纪律:必须在相位早退之前(#310 教训)。
|
|
143
|
+
const pendingStore = useMemo(
|
|
144
|
+
() => (childId === undefined || session === undefined ? undefined : createPendingStore(ctx, session, childId)),
|
|
145
|
+
[ctx, session, childId],
|
|
146
|
+
)
|
|
147
|
+
const pendingItems = useSyncExternalStore(
|
|
148
|
+
useCallback(
|
|
149
|
+
(notify: () => void) => (visible && pendingStore !== undefined ? pendingStore.subscribe(notify) : NOOP_UNSUBSCRIBE),
|
|
150
|
+
[visible, pendingStore],
|
|
151
|
+
),
|
|
152
|
+
// NO_PENDING 常量:uSES getSnapshot 禁产新引用(`?? []` 每次新数组 →
|
|
153
|
+
// 无限重渲——实证教训)。
|
|
154
|
+
() => pendingStore?.getSnapshot() ?? NO_PENDING,
|
|
155
|
+
)
|
|
156
|
+
|
|
136
157
|
// D2 提示的消除态(hooks 纪律:必须在相位早退之前——#310 教训)。
|
|
137
158
|
const [writeNoticeDismissed, setWriteNoticeDismissed] = useState(
|
|
138
159
|
() => typeof localStorage !== 'undefined' && localStorage.getItem(WRITE_NOTICE_KEY) === '1',
|
|
@@ -321,7 +342,12 @@ export function SideChatPanel(props: TabComponentProps & { reflow: ReflowStore }
|
|
|
321
342
|
>×</button>
|
|
322
343
|
</div>
|
|
323
344
|
)}
|
|
324
|
-
{
|
|
345
|
+
{/* R8:数据源在场 → 面板内审批/提问答复卡;面缺席(老宿主)→ 回退
|
|
346
|
+
纯跳转 pendingBar(原行为不变)。 */}
|
|
347
|
+
{pendingStore !== undefined && pendingItems.length > 0 && (
|
|
348
|
+
<PendingCardList items={pendingItems} />
|
|
349
|
+
)}
|
|
350
|
+
{pendingStore === undefined && pendingCount > 0 && childId !== undefined && (
|
|
325
351
|
<button
|
|
326
352
|
type="button"
|
|
327
353
|
className={css.pendingBar}
|
|
@@ -29,6 +29,7 @@ import { registerHeaderEntry } from './header.tsx'
|
|
|
29
29
|
import { registerSideCommand } from './slash.ts'
|
|
30
30
|
import { registerSideChatReferenceSource } from './referenceSource.ts'
|
|
31
31
|
import { registerToastHost } from './toast.tsx'
|
|
32
|
+
import { registerCompletionNotify } from './notify.ts'
|
|
32
33
|
|
|
33
34
|
export function registerSideChat(ctx: Context, reflow: ReflowStore): void {
|
|
34
35
|
ctx.effect(
|
|
@@ -79,4 +80,6 @@ export function registerSideChat(ctx: Context, reflow: ReflowStore): void {
|
|
|
79
80
|
registerHeaderEntry(ctx)
|
|
80
81
|
// 全局瞬态通知宿主(P4:转正确认等跨 Tab 寿命的反馈)。
|
|
81
82
|
registerToastHost(ctx)
|
|
83
|
+
// 完成通知(WI-03):侧聊 running 翻转 → toast。
|
|
84
|
+
registerCompletionNotify(ctx)
|
|
82
85
|
}
|