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
|
@@ -42,13 +42,24 @@ export type ToolCardModel = {
|
|
|
42
42
|
locations?: readonly FileLocation[];
|
|
43
43
|
} | {
|
|
44
44
|
kind: 'terminal';
|
|
45
|
+
/** 标题(有人话 description 时 = 「Bash · description」,否则 = 命令原文)。 */
|
|
45
46
|
title: string;
|
|
47
|
+
/** 命令原文(TerminalBlock 的 command——标题让位给 description 后命令仍随行)。 */
|
|
48
|
+
command: string;
|
|
46
49
|
description?: string;
|
|
47
50
|
/** 已解析的 cwd(相对路径已按 cwdBase 折成绝对;无 base 时原样)。 */
|
|
48
51
|
cwd?: string;
|
|
49
52
|
output?: string;
|
|
50
53
|
exitCode?: number;
|
|
51
54
|
signal?: string;
|
|
55
|
+
} | {
|
|
56
|
+
kind: 'todo';
|
|
57
|
+
/** 标题(任务 · N 已完成 · N 进行中 · N 待处理,非零组才出现)。 */
|
|
58
|
+
title: string;
|
|
59
|
+
items: readonly {
|
|
60
|
+
content: string;
|
|
61
|
+
status: string;
|
|
62
|
+
}[];
|
|
52
63
|
} | {
|
|
53
64
|
kind: 'diff';
|
|
54
65
|
title: string;
|
|
@@ -95,6 +106,16 @@ export type ToolCardModel = {
|
|
|
95
106
|
statusCode?: number;
|
|
96
107
|
truncated?: boolean;
|
|
97
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* 展示态路径缩短:>3 段的绝对路径只保末 3 段(≈仓库相对形态,与主区
|
|
111
|
+
* 「Read · dsh-sidenote/scripts/x.sh」同款)。窄栏标题位只够放语义尾部。
|
|
112
|
+
*/
|
|
113
|
+
export declare function shortPath(p: string): string;
|
|
114
|
+
/**
|
|
115
|
+
* 标题整形:「Tool /abs/path」→「Tool · 末3段」(主区同名同款);
|
|
116
|
+
* 裸绝对路径标题 → 末 3 段;不含路径的标题原样。
|
|
117
|
+
*/
|
|
118
|
+
export declare function displayTitle(title: string): string;
|
|
98
119
|
export interface CardModelInput {
|
|
99
120
|
/** 工具名兜底(callView 缺失时的标题)。 */
|
|
100
121
|
toolName: string;
|
|
@@ -119,6 +140,10 @@ export interface NodeCardInput {
|
|
|
119
140
|
meta?: unknown;
|
|
120
141
|
/** 结果正文(已拼 text 块)。 */
|
|
121
142
|
rawText: string;
|
|
143
|
+
/** 失败标记(0.1.2 节点 isError)——失败结果无 meta(presentationMeta 只投
|
|
144
|
+
* 影成功路径),但 write 的空-meta 回退必须先判它,否则失败的 write 也
|
|
145
|
+
* 会被渲染成 diff 卡(官方 diffCardModel 显式 isError → null)。 */
|
|
146
|
+
isError?: boolean | undefined;
|
|
122
147
|
cwdBase?: string | undefined;
|
|
123
148
|
}
|
|
124
149
|
/**
|
|
@@ -26,6 +26,10 @@ export declare const en: {
|
|
|
26
26
|
stopped: string;
|
|
27
27
|
toolLabel: string;
|
|
28
28
|
failed: string;
|
|
29
|
+
todoTitle: string;
|
|
30
|
+
todoDone: string;
|
|
31
|
+
todoDoing: string;
|
|
32
|
+
todoPending: string;
|
|
29
33
|
running: string;
|
|
30
34
|
inputPlaceholder: string;
|
|
31
35
|
modelLabel: string;
|
|
@@ -34,8 +38,18 @@ export declare const en: {
|
|
|
34
38
|
permFullAccess: string;
|
|
35
39
|
pendingNotice: string;
|
|
36
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;
|
|
37
50
|
modelFollowsMain: string;
|
|
38
51
|
stopReply: string;
|
|
52
|
+
sideChatDone: string;
|
|
39
53
|
stopReplyTitle: string;
|
|
40
54
|
send: string;
|
|
41
55
|
attachTitle: string;
|
|
@@ -112,6 +126,7 @@ export declare const en: {
|
|
|
112
126
|
reflowAllDone: string;
|
|
113
127
|
promote: string;
|
|
114
128
|
promoteTitle: string;
|
|
129
|
+
promoteDone: string;
|
|
115
130
|
readWindow: string;
|
|
116
131
|
searchPathsSummary: string;
|
|
117
132
|
searchMatchesSummary: 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",
|
|
@@ -16,13 +16,14 @@ export function FoldCard(props: { count: number; rowKey: string; fold: FoldStore
|
|
|
16
16
|
const { rowKey, fold } = props
|
|
17
17
|
const open = useSyncExternalStore((fn) => fold.subscribe(fn), () => fold.isOpen(rowKey))
|
|
18
18
|
return (
|
|
19
|
-
<div className={css.
|
|
19
|
+
<div className={css.flowRow}>
|
|
20
20
|
<DisclosureRow
|
|
21
21
|
icon={<IconBranchOutline16 size={14} />}
|
|
22
22
|
title={t('inheritedLabel', { n: props.count })}
|
|
23
23
|
open={open}
|
|
24
24
|
expandable
|
|
25
25
|
expandOnRowClick
|
|
26
|
+
previewChevron
|
|
26
27
|
onToggle={() => { fold.toggle(rowKey) }}
|
|
27
28
|
>
|
|
28
29
|
<div className={css.inheritedBody}>{props.children}</div>
|
|
@@ -10,20 +10,32 @@ import type { FoldStore } from './viewState.ts'
|
|
|
10
10
|
import { t } from '../locales.ts'
|
|
11
11
|
import css from '../sidechat/sidechat.module.css'
|
|
12
12
|
|
|
13
|
+
/** 首行非空文本(折叠态预览——主区「Think · 首行摘要」同款)。 */
|
|
14
|
+
function firstLineOf(text: string): string {
|
|
15
|
+
for (const line of text.split('\n')) {
|
|
16
|
+
const s = line.trim()
|
|
17
|
+
if (s !== '') return s
|
|
18
|
+
}
|
|
19
|
+
return ''
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
export function ReasoningRow(props: { text: string; rowKey: string; fold: FoldStore; streaming?: boolean }) {
|
|
14
23
|
const { rowKey, fold } = props
|
|
15
24
|
const touched = useSyncExternalStore((fn) => fold.subscribe(fn), () => fold.has(rowKey))
|
|
16
25
|
const stored = useSyncExternalStore((fn) => fold.subscribe(fn), () => fold.isOpen(rowKey))
|
|
17
26
|
// 流式中且用户未触碰 → 默认展开;其余听 store。
|
|
18
27
|
const open = stored || (props.streaming === true && !touched)
|
|
28
|
+
const preview = firstLineOf(props.text)
|
|
19
29
|
return (
|
|
20
|
-
<div className={css.
|
|
30
|
+
<div className={css.flowRow}>
|
|
21
31
|
<DisclosureRow
|
|
22
32
|
icon={<IconThinkOutline16 size={14} />}
|
|
23
33
|
title={t('thinking')}
|
|
24
34
|
open={open}
|
|
25
35
|
expandable
|
|
26
36
|
expandOnRowClick
|
|
37
|
+
previewChevron
|
|
38
|
+
{...(preview !== '' ? { collapsedContent: <span className={css.rowPreview}>{preview}</span> } : {})}
|
|
27
39
|
onToggle={() => { fold.toggle(rowKey) }}
|
|
28
40
|
>
|
|
29
41
|
<div className={css.reasoningBody}>{props.text}</div>
|
|
@@ -10,6 +10,7 @@ import { useSyncExternalStore, type ReactNode } from 'react'
|
|
|
10
10
|
import {
|
|
11
11
|
DisclosureRow,
|
|
12
12
|
DiffBlock,
|
|
13
|
+
IconChecklistOutline14,
|
|
13
14
|
IconCodeOutline16,
|
|
14
15
|
IconCordisPluginOutline14,
|
|
15
16
|
IconEditOutline16,
|
|
@@ -53,7 +54,7 @@ function useFoldOpen(fold: FoldStore, rowKey: string): boolean {
|
|
|
53
54
|
)
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
export function ToolCard(props: { model: ToolCardModel; rowKey: string; fold: FoldStore; streaming?: boolean }) {
|
|
57
|
+
export function ToolCard(props: { model: ToolCardModel; rowKey: string; fold: FoldStore; streaming?: boolean; error?: boolean }) {
|
|
57
58
|
const { model, rowKey, fold } = props
|
|
58
59
|
const open = useFoldOpen(fold, rowKey)
|
|
59
60
|
const icon = model.kind === 'terminal'
|
|
@@ -62,16 +63,20 @@ export function ToolCard(props: { model: ToolCardModel; rowKey: string; fold: Fo
|
|
|
62
63
|
? <IconEditOutline16 size={14} />
|
|
63
64
|
: model.kind === 'read'
|
|
64
65
|
? <IconCodeOutline16 size={14} />
|
|
65
|
-
: model.kind === '
|
|
66
|
+
: model.kind === 'todo'
|
|
67
|
+
? <IconChecklistOutline14 size={14} />
|
|
68
|
+
: model.kind === 'generic' ? kindIcon(model.icon) : <IconCordisPluginOutline14 size={14} />
|
|
66
69
|
|
|
67
70
|
return (
|
|
68
|
-
<div className={css.
|
|
71
|
+
<div className={css.flowRow}>
|
|
69
72
|
<DisclosureRow
|
|
70
73
|
icon={icon}
|
|
71
74
|
title={model.title}
|
|
72
75
|
open={open}
|
|
73
76
|
expandable
|
|
74
77
|
expandOnRowClick
|
|
78
|
+
previewChevron
|
|
79
|
+
{...(props.error === true ? { titleClassName: css.flowRowError } : {})}
|
|
75
80
|
onToggle={() => { fold.toggle(rowKey) }}
|
|
76
81
|
>
|
|
77
82
|
<ToolCardBody model={model} streaming={props.streaming} />
|
|
@@ -85,7 +90,7 @@ function ToolCardBody({ model, streaming }: { model: ToolCardModel; streaming?:
|
|
|
85
90
|
case 'terminal':
|
|
86
91
|
return (
|
|
87
92
|
<TerminalBlock
|
|
88
|
-
command={model.
|
|
93
|
+
command={model.command}
|
|
89
94
|
labels={terminalLabels()}
|
|
90
95
|
{...(model.cwd !== undefined ? { cwd: model.cwd } : {})}
|
|
91
96
|
{...(model.output !== undefined ? { output: model.output } : {})}
|
|
@@ -94,6 +99,17 @@ function ToolCardBody({ model, streaming }: { model: ToolCardModel; streaming?:
|
|
|
94
99
|
{...(streaming === true ? { running: true } : {})}
|
|
95
100
|
/>
|
|
96
101
|
)
|
|
102
|
+
case 'todo':
|
|
103
|
+
return (
|
|
104
|
+
<div className={css.todoBody}>
|
|
105
|
+
{model.items.map((item, i) => (
|
|
106
|
+
<div key={i} className={css.todoItem}>
|
|
107
|
+
<span className={css.todoDot} data-status={item.status} />
|
|
108
|
+
<span className={css.todoText} data-status={item.status}>{item.content}</span>
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
97
113
|
case 'diff':
|
|
98
114
|
return <DiffBlock {...({ labels: diffLabels() } as object)} diffs={model.diffs.map(d => ({ path: d.path, oldText: d.oldText, newText: d.newText }))} />
|
|
99
115
|
case 'read':
|