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.
@@ -5,4 +5,5 @@ export declare function ToolCard(props: {
5
5
  rowKey: string;
6
6
  fold: FoldStore;
7
7
  streaming?: boolean;
8
+ error?: boolean;
8
9
  }): import("react").JSX.Element;
@@ -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,4 @@
1
+ import type { PendingItem } from './pending.ts';
2
+ export declare function PendingCardList({ items }: {
3
+ items: readonly PendingItem[];
4
+ }): import("react").JSX.Element;
@@ -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 {};
@@ -0,0 +1,5 @@
1
+ import type { Context } from '../host/contracts.ts';
2
+ /** 全局瞬态通知入口(任何模块可调;未挂载宿主时静默丢弃)。 */
3
+ export declare function showToast(text: string): void;
4
+ /** 挂载全局 toast 宿主(registerSideChat 里调用一次)。 */
5
+ export declare function registerToastHost(ctx: Context): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sidenote",
3
- "version": "0.3.0",
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.toolCardV2}>
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.toolCardV2}>
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 === 'generic' ? kindIcon(model.icon) : <IconCordisPluginOutline14 size={14} />
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.toolCardV2}>
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.title}
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':