dsh-plugin-core 0.34.0 → 0.35.0
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/dist/adv/session/SessionViewModal.d.ts +20 -2
- package/dist/adv/session/SessionViewModal.d.ts.map +1 -1
- package/dist/adv/session/SessionViewModal.js +186 -73
- package/dist/adv/session/SessionViewModal.js.map +1 -1
- package/dist/adv/session/faces.d.ts +30 -7
- package/dist/adv/session/faces.d.ts.map +1 -1
- package/dist/adv/session/index.d.ts +1 -0
- package/dist/adv/session/index.d.ts.map +1 -1
- package/dist/adv/session/index.js +1 -0
- package/dist/adv/session/index.js.map +1 -1
- package/dist/adv/session/mirror/BashRow.d.ts.map +1 -1
- package/dist/adv/session/mirror/BashRow.js +11 -2
- package/dist/adv/session/mirror/BashRow.js.map +1 -1
- package/dist/adv/session/mirror/ChatView.d.ts +8 -0
- package/dist/adv/session/mirror/ChatView.d.ts.map +1 -1
- package/dist/adv/session/mirror/ChatView.js +8 -0
- package/dist/adv/session/mirror/ChatView.js.map +1 -1
- package/dist/adv/session/mirror/ContextBody.d.ts +24 -0
- package/dist/adv/session/mirror/ContextBody.d.ts.map +1 -0
- package/dist/adv/session/mirror/ContextBody.js +249 -0
- package/dist/adv/session/mirror/ContextBody.js.map +1 -0
- package/dist/adv/session/mirror/ContextInjectionRow.d.ts +21 -1
- package/dist/adv/session/mirror/ContextInjectionRow.d.ts.map +1 -1
- package/dist/adv/session/mirror/ContextInjectionRow.js +57 -29
- package/dist/adv/session/mirror/ContextInjectionRow.js.map +1 -1
- package/dist/adv/session/mirror/GenericCommandCard.d.ts +20 -0
- package/dist/adv/session/mirror/GenericCommandCard.d.ts.map +1 -1
- package/dist/adv/session/mirror/GenericCommandCard.js +89 -27
- package/dist/adv/session/mirror/GenericCommandCard.js.map +1 -1
- package/dist/adv/session/mirror/MessageIconActions.js +1 -1
- package/dist/adv/session/mirror/MessageIconActions.js.map +1 -1
- package/dist/adv/session/mirror/MessageImage.d.ts +23 -0
- package/dist/adv/session/mirror/MessageImage.d.ts.map +1 -0
- package/dist/adv/session/mirror/MessageImage.js +107 -0
- package/dist/adv/session/mirror/MessageImage.js.map +1 -0
- package/dist/adv/session/mirror/MessageItem.d.ts +11 -0
- package/dist/adv/session/mirror/MessageItem.d.ts.map +1 -1
- package/dist/adv/session/mirror/MessageItem.js +12 -2
- package/dist/adv/session/mirror/MessageItem.js.map +1 -1
- package/dist/adv/session/official-classes.d.ts +2 -0
- package/dist/adv/session/official-classes.d.ts.map +1 -1
- package/dist/adv/session/official-classes.js +3 -0
- package/dist/adv/session/official-classes.js.map +1 -1
- package/dist/adv/session/open-session-view.d.ts +6 -1
- package/dist/adv/session/open-session-view.d.ts.map +1 -1
- package/dist/adv/session/open-session-view.js +8 -0
- package/dist/adv/session/open-session-view.js.map +1 -1
- package/dist/adv/session/session-css.generated.d.ts +1 -1
- package/dist/adv/session/session-css.generated.d.ts.map +1 -1
- package/dist/adv/session/session-css.generated.js +1 -1
- package/dist/adv/session/session-css.generated.js.map +1 -1
- package/dist/design/tokens-css.generated.d.ts +1 -1
- package/dist/design/tokens-css.generated.d.ts.map +1 -1
- package/dist/design/tokens-css.generated.js +1 -1
- package/dist/design/tokens-css.generated.js.map +1 -1
- package/dist/ui/feedback/Modal.d.ts +7 -0
- package/dist/ui/feedback/Modal.d.ts.map +1 -1
- package/dist/ui/feedback/Modal.js +2 -0
- package/dist/ui/feedback/Modal.js.map +1 -1
- package/dist/ui/infra/overlay-css.generated.d.ts +1 -1
- package/dist/ui/infra/overlay-css.generated.d.ts.map +1 -1
- package/dist/ui/infra/overlay-css.generated.js +1 -1
- package/dist/ui/infra/overlay-css.generated.js.map +1 -1
- package/dist/ui/infra/overlay.d.ts +7 -0
- package/dist/ui/infra/overlay.d.ts.map +1 -1
- package/dist/ui/infra/overlay.js +6 -1
- package/dist/ui/infra/overlay.js.map +1 -1
- package/dist/ui/infra/panel-css.generated.d.ts +1 -1
- package/dist/ui/infra/panel-css.generated.d.ts.map +1 -1
- package/dist/ui/infra/panel-css.generated.js +1 -1
- package/dist/ui/infra/panel-css.generated.js.map +1 -1
- package/dist/ui/inputs/DateTimePicker.js +1 -1
- package/dist/ui/inputs/DateTimePicker.js.map +1 -1
- package/dist/ui/inputs/Picker.d.ts.map +1 -1
- package/dist/ui/inputs/Picker.js +5 -3
- package/dist/ui/inputs/Picker.js.map +1 -1
- package/dist/ui/inputs/datetime-css.generated.d.ts +1 -1
- package/dist/ui/inputs/datetime-css.generated.d.ts.map +1 -1
- package/dist/ui/inputs/datetime-css.generated.js +1 -1
- package/dist/ui/inputs/datetime-css.generated.js.map +1 -1
- package/dist/ui/inputs/picker-css.generated.d.ts +1 -1
- package/dist/ui/inputs/picker-css.generated.d.ts.map +1 -1
- package/dist/ui/inputs/picker-css.generated.js +1 -1
- package/dist/ui/inputs/picker-css.generated.js.map +1 -1
- package/dist/ui/layout/panel-dock-css.generated.d.ts +1 -1
- package/dist/ui/layout/panel-dock-css.generated.d.ts.map +1 -1
- package/dist/ui/layout/panel-dock-css.generated.js +1 -1
- package/dist/ui/layout/panel-dock-css.generated.js.map +1 -1
- package/dist/utils/i18n.d.ts +2 -2
- package/dist/utils/i18n.d.ts.map +1 -1
- package/dist/utils/i18n.js +54 -0
- package/dist/utils/i18n.js.map +1 -1
- package/docs/design/external/dsh-host.md +22 -0
- package/docs/design/style.md +38 -10
- package/docs/help/session.doc.ts +19 -2
- package/docs/help/session.md +19 -2
- package/docs/help/ui.doc.ts +3 -3
- package/docs/help/ui.md +3 -3
- package/package.json +1 -1
|
@@ -1,11 +1,29 @@
|
|
|
1
|
-
import { createElement as h } from 'react';
|
|
1
|
+
import { createElement as h, type ReactNode } from 'react';
|
|
2
2
|
import { type Translate } from '../../utils/i18n.js';
|
|
3
3
|
import type { SessionViewTarget } from './open-session-view.js';
|
|
4
4
|
import { type ChangedFileReviewLocator } from './mirror/ChangedFilesCard.js';
|
|
5
5
|
export interface SessionViewModalProps {
|
|
6
6
|
t?: Translate;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* 标题行(第一级):核心画在标题位,下方 sessionId 副标题行与归档徽标不受影响。
|
|
9
|
+
* 想在标题后跟标签 / 按钮 / 自己的一段节点,塞进这里。
|
|
10
|
+
* 缺省 = `heading`,再缺省 =「会话预览」。
|
|
11
|
+
*/
|
|
12
|
+
title?: ReactNode;
|
|
13
|
+
/** @deprecated 0.34 的旧名(只收字符串),等价 `title`;两者都给时以 `title` 为准。 */
|
|
8
14
|
heading?: string;
|
|
15
|
+
/**
|
|
16
|
+
* 整块替换左侧标题区(第二级:标题行 + sessionId 行 + 归档徽标)。传了则 `title` / `heading` 不生效;
|
|
17
|
+
* 归档徽标改挂到右侧动作区最前(归档是数据事实,不能因为自定义标题就消失)。
|
|
18
|
+
* 右侧「继续对话」与关闭 ✕ 不受影响。
|
|
19
|
+
*/
|
|
20
|
+
header?: ReactNode;
|
|
21
|
+
/** 头部右侧追加动作:排在核心「继续对话」之后、关闭 ✕ 之前。 */
|
|
22
|
+
headerActions?: ReactNode;
|
|
23
|
+
/** 会话内容最顶端(滚动容器内、所有节点与「加载更早」之前);随会话一起滚。 */
|
|
24
|
+
bodyHeader?: ReactNode;
|
|
25
|
+
/** 会话内容最底端(滚动容器内、最后一条内容与运行中行之后);随会话一起滚。 */
|
|
26
|
+
bodyFooter?: ReactNode;
|
|
9
27
|
sessionId: string;
|
|
10
28
|
view: SessionViewTarget;
|
|
11
29
|
onClose: () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionViewModal.d.ts","sourceRoot":"","sources":["../../../src/adv/session/SessionViewModal.ts"],"names":[],"mappings":"AAUA,OAAO,EAAY,aAAa,IAAI,CAAC,EAA4F,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"SessionViewModal.d.ts","sourceRoot":"","sources":["../../../src/adv/session/SessionViewModal.ts"],"names":[],"mappings":"AAUA,OAAO,EAAY,aAAa,IAAI,CAAC,EAA4F,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAI9J,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAYhE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAiB/D,OAAO,EAAoB,KAAK,wBAAwB,EAAE,MAAM,8BAA8B,CAAA;AAguB9F,MAAM,WAAW,qBAAqB;IACpC,CAAC,CAAC,EAAE,SAAS,CAAA;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,qCAAqC;IACrC,aAAa,CAAC,EAAE,SAAS,CAAA;IACzB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,iBAAiB,CAAA;IACvB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACpE,uDAAuD;IACvD,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7C;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C;;qDAEiD;IACjD,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IACnF;mEAC+D;IAC/D,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,IAAI,CAAA;CAC3D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CA4enF"}
|
|
@@ -13,11 +13,12 @@ import { IconArchiveOutlineRegular, IconBranchOutlineRegular, IconNewChatOutline
|
|
|
13
13
|
import { Modal } from '../../ui/feedback/Modal.js';
|
|
14
14
|
import { Button } from '../../ui/actions/Button.js';
|
|
15
15
|
import { t as coreT } from '../../utils/i18n.js';
|
|
16
|
-
import { ChatHint, ChatNodeListMirror, ChatOlderButton, ChatViewFrame } from './mirror/ChatView.js';
|
|
17
|
-
import { GenericCommandCard } from './mirror/GenericCommandCard.js';
|
|
16
|
+
import { ChatCallRow, ChatHint, ChatNodeListMirror, ChatOlderButton, ChatOpenError, ChatViewFrame } from './mirror/ChatView.js';
|
|
17
|
+
import { GenericCommandCard, imageCardFace } from './mirror/GenericCommandCard.js';
|
|
18
18
|
import { CommandCard } from './mirror/CommandCard.js';
|
|
19
19
|
import { MessageIconActionsMirror } from './mirror/MessageIconActions.js';
|
|
20
|
-
import { AssistantMarkdown, CompactionItemMirror, ModelRetryItemMirror, TurnErrorItemMirror, TurnMaxTokensItemMirror, UserMessage } from './mirror/MessageItem.js';
|
|
20
|
+
import { AssistantMarkdown, AssistantStoppedBadge, CompactionItemMirror, ModelRetryItemMirror, TurnErrorItemMirror, TurnMaxTokensItemMirror, UserMessage } from './mirror/MessageItem.js';
|
|
21
|
+
import { ImageGalleryMirror } from './mirror/MessageImage.js';
|
|
21
22
|
import { BashRowMirror } from './mirror/BashRow.js';
|
|
22
23
|
import { SkillRowMirror } from './mirror/SkillRow.js';
|
|
23
24
|
import { ContextInjectionRowMirror } from './mirror/ContextInjectionRow.js';
|
|
@@ -55,19 +56,39 @@ function safeJson(value) {
|
|
|
55
56
|
return String(value);
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
|
-
/**
|
|
59
|
-
|
|
59
|
+
/**
|
|
60
|
+
* 提取内容块的可读文本:text 拼接、图片占位(官方 ContentBlock 是 merge-extensible map)。
|
|
61
|
+
* @param imagePlaceholder 图片块是否退化成「[图片]」文本:图片能真渲染时传 false(官方文本里没有图片)。
|
|
62
|
+
*/
|
|
63
|
+
function contentText(blocks, imagePlaceholder = true) {
|
|
60
64
|
if (blocks === undefined)
|
|
61
65
|
return '';
|
|
62
66
|
const parts = blocks.map((block) => {
|
|
63
67
|
if (block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string')
|
|
64
68
|
return block.text;
|
|
65
|
-
if (block !== null && typeof block === 'object' && block.type === 'image')
|
|
69
|
+
if (imagePlaceholder && block !== null && typeof block === 'object' && block.type === 'image')
|
|
66
70
|
return coreT('sessionImage');
|
|
67
71
|
return '';
|
|
68
72
|
}).filter(part => part !== '');
|
|
69
73
|
return parts.join('\n');
|
|
70
74
|
}
|
|
75
|
+
/** 内容块里的官方 image 块(`{ type:'image', attachment: ImageAttachmentRef }`)→ 图片组。 */
|
|
76
|
+
function contentImages(blocks) {
|
|
77
|
+
if (blocks === undefined)
|
|
78
|
+
return [];
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const block of blocks) {
|
|
81
|
+
if (block === null || typeof block !== 'object' || block.type !== 'image')
|
|
82
|
+
continue;
|
|
83
|
+
const attachment = block.attachment;
|
|
84
|
+
if (typeof attachment !== 'object' || attachment === null)
|
|
85
|
+
continue;
|
|
86
|
+
if (typeof attachment.attachmentId !== 'string' || attachment.attachmentId === '')
|
|
87
|
+
continue;
|
|
88
|
+
out.push({ attachment });
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
71
92
|
/**
|
|
72
93
|
* 提取内容块里的**官方 file 块**(2026-10-03 核实):随附文件以官方附件形式发进会话后,
|
|
73
94
|
* 会话快照的 user 节点 content 里就是它 ⇒ 弹窗里照样渲染成附件卡(与官方页一致)。
|
|
@@ -119,24 +140,31 @@ function blocksOf(value) {
|
|
|
119
140
|
* 官方 ToolCallBlock(uic contract/records.d.ts:140)→ 工具卡 props。
|
|
120
141
|
* running 半截(phase: preparing/start)只有 name/argsRaw;settled(kind: tool-result)带输出与错误。
|
|
121
142
|
*/
|
|
122
|
-
function toolCallCard(node, t, onOpenFile) {
|
|
143
|
+
function toolCallCard(node, t, onOpenFile, loadImage) {
|
|
123
144
|
const root = dataOf(node).root;
|
|
124
145
|
if (root === undefined || root === null)
|
|
125
146
|
return null;
|
|
126
147
|
const settled = root.kind === 'tool-result';
|
|
127
148
|
const call = settled ? root.call : root;
|
|
128
149
|
const error = root.error;
|
|
150
|
+
const name = typeof call?.name === 'string' ? call.name : 'tool';
|
|
151
|
+
const argsRaw = typeof call?.argsRaw === 'string' ? call.argsRaw : '';
|
|
152
|
+
const isError = root.isError === true;
|
|
153
|
+
const content = settled ? root.content : undefined;
|
|
129
154
|
return {
|
|
130
|
-
name
|
|
131
|
-
argsRaw
|
|
132
|
-
output: settled ? contentText(
|
|
133
|
-
isError
|
|
155
|
+
name,
|
|
156
|
+
argsRaw,
|
|
157
|
+
output: settled ? contentText(content, false) : '',
|
|
158
|
+
isError,
|
|
134
159
|
errorName: error?.name,
|
|
135
160
|
meta: root.meta,
|
|
136
161
|
settled,
|
|
137
162
|
phase: settled ? undefined : root.phase === 'preparing' ? 'preparing' : 'start',
|
|
138
163
|
interrupted: error?.code === 'interrupted',
|
|
139
164
|
errorCode: error?.code,
|
|
165
|
+
// 官方 read_image 专属卡:图片来自结果内容块的附件引用,不是文本。
|
|
166
|
+
image: imageCardFace(name, argsRaw, content, root.meta, isError, settled) ?? undefined,
|
|
167
|
+
loadImage,
|
|
140
168
|
onOpenFile,
|
|
141
169
|
t,
|
|
142
170
|
};
|
|
@@ -204,7 +232,13 @@ sessionId,
|
|
|
204
232
|
/** 变更文件点击出口(官方 openReview 等价):带改动身份;undefined = 变更卡不可点。 */
|
|
205
233
|
onOpenReview,
|
|
206
234
|
/** 官方 endsWithResponse / hasLaterChatNode(尾部操作行常显与分支可用性)。 */
|
|
207
|
-
tailFacts
|
|
235
|
+
tailFacts,
|
|
236
|
+
/** 图片 loader(view.loadImage);缺席 = 图片块退回「[图片]」文本。 */
|
|
237
|
+
loadImage,
|
|
238
|
+
/** 底端插槽:挂在最后一条 tail 之前(复制 / 分支 / 用量操作行之上)。 */
|
|
239
|
+
footerSlot,
|
|
240
|
+
/** 该挂到哪个节点的 key 上(最后一条带回复的 turn-tail;undefined = 不挂)。 */
|
|
241
|
+
footerKey) {
|
|
208
242
|
switch (node.kind) {
|
|
209
243
|
case 'turn-trigger':
|
|
210
244
|
return h(TurnTriggerNodeViewMirror, { data: node.data, t });
|
|
@@ -230,13 +264,14 @@ tailFacts) {
|
|
|
230
264
|
const changesCard = atLastTail && changesSeq !== undefined && sessionId !== undefined
|
|
231
265
|
? h(ChangedFilesCard, { sessionId, seq: changesSeq, onOpenReview, t })
|
|
232
266
|
: null;
|
|
233
|
-
|
|
267
|
+
// 底端插槽 = tailSlot 的最后一项 ⇒ 在交付文件 / 变更卡**下面**、复制/分支/用量操作行**上面**。
|
|
268
|
+
const atFooter = footerKey !== undefined && node.key === footerKey;
|
|
269
|
+
const tailSlot = changesCard === null && !showGrid && !atFooter
|
|
234
270
|
? null
|
|
235
|
-
: h(Fragment, null, changesCard, showGrid ? h(DeliverablesGridMirror, { files: deliverFiles, onOpen: fileOpen?.open, t }) : null);
|
|
236
|
-
|
|
271
|
+
: h(Fragment, null, changesCard, showGrid ? h(DeliverablesGridMirror, { files: deliverFiles, onOpen: fileOpen?.open, t }) : null, atFooter ? footerSlot : null);
|
|
272
|
+
return data === undefined || data.closing === null || data.closing === undefined
|
|
237
273
|
? null
|
|
238
274
|
: h(TurnTailNodeViewMirror, { data, onBranchAt, tailSlot, facts: tailFacts, t });
|
|
239
|
-
return tail;
|
|
240
275
|
}
|
|
241
276
|
case 'assistant-step': {
|
|
242
277
|
// 官方块渲染器(lib/client.js:5818-5871):
|
|
@@ -256,7 +291,12 @@ tailFacts) {
|
|
|
256
291
|
: groupPart === 'response'
|
|
257
292
|
? contentBlocks.filter(block => block.kind !== 'reasoning')
|
|
258
293
|
: contentBlocks;
|
|
259
|
-
const parts = assistantBlocks(visible, t, fileOpen?.mentions, streaming);
|
|
294
|
+
const parts = assistantBlocks(visible, t, fileOpen?.mentions, streaming, loadImage);
|
|
295
|
+
// 官方 AssistantMarkdown.stopped:被中断的整步末尾一枚「已停止」;只画思考块的组内不重复画。
|
|
296
|
+
const stopped = interrupted
|
|
297
|
+
&& (groupPart !== 'reasoning' || !blocks.some(block => block.kind !== 'reasoning' && block.kind !== 'tool-call'));
|
|
298
|
+
if (stopped)
|
|
299
|
+
parts.push(h(AssistantStoppedBadge, { key: 'stopped', t }));
|
|
260
300
|
return parts.length === 0 ? null : h('div', { className: 'dshpc-sv-assistant' }, parts);
|
|
261
301
|
}
|
|
262
302
|
case 'tool-call': {
|
|
@@ -265,7 +305,7 @@ tailFacts) {
|
|
|
265
305
|
const root = dataOf(node).root;
|
|
266
306
|
if (isPresentRoot(root))
|
|
267
307
|
return h(PresentRowMirror, { block: root, t });
|
|
268
|
-
const card = toolCallCard(node, t, fileOpen?.open);
|
|
308
|
+
const card = toolCallCard(node, t, fileOpen?.open, loadImage);
|
|
269
309
|
if (card === null)
|
|
270
310
|
return null;
|
|
271
311
|
// 官方 tool.call.toolview 槽位按工具名分派:bash / pwsh 走专属 BashRow(独立卡片,无 DisclosureRow);
|
|
@@ -280,19 +320,23 @@ tailFacts) {
|
|
|
280
320
|
// 官方 CommandNodeView → GenericCommandCard(ui-chat/chat/GenericCommandCard.tsx 原样复刻)。
|
|
281
321
|
// 斜杠命令生命周期节点(records.d.ts:name/args/outcome);走 CommandCard:标题=裸命令名、
|
|
282
322
|
// 摘要=outcome 落定 text、仅多行才展开。之前漏接 ⇒ 落 default 显示「未支持的节点类型: command」。
|
|
283
|
-
return h(CommandCard, { node: dataOf(node), t });
|
|
323
|
+
return h(ChatCallRow, null, h(CommandCard, { node: dataOf(node), t }));
|
|
284
324
|
}
|
|
285
325
|
case 'user':
|
|
286
326
|
case 'steering': {
|
|
287
327
|
const blocks = dataOf(node).content;
|
|
288
|
-
const
|
|
328
|
+
const images = loadImage === undefined ? [] : contentImages(blocks);
|
|
329
|
+
// 官方文本里没有图片:能真出图时不再塞「[图片]」。
|
|
330
|
+
const text = contentText(blocks, loadImage === undefined || images.length === 0);
|
|
289
331
|
// 随附文件(官方 file 块):没有文本但有附件时**照样渲染**(不能整条消失)。
|
|
290
332
|
const { files, hadFiles } = contentFiles(blocks);
|
|
291
|
-
if (text === '' && !hadFiles)
|
|
333
|
+
if (text === '' && !hadFiles && images.length === 0)
|
|
292
334
|
return null;
|
|
293
335
|
return h(UserMessage, {
|
|
294
336
|
text,
|
|
295
337
|
files,
|
|
338
|
+
images,
|
|
339
|
+
loadImage,
|
|
296
340
|
referenceLabels: dataOf(node).referenceLabels,
|
|
297
341
|
skillNames: dataOf(node).skillNames,
|
|
298
342
|
t,
|
|
@@ -318,15 +362,18 @@ tailFacts) {
|
|
|
318
362
|
// 决策 28:context(系统注入)/ unknown 仍默认过滤;
|
|
319
363
|
// 0.2.x compaction 节点走官方 CompactionItem(此前漏渲染 ⇒ 压缩段整段消失)。
|
|
320
364
|
case 'compaction':
|
|
321
|
-
case 'manual-compaction':
|
|
322
365
|
return h(CompactionItemMirror, { node: dataOf(node), t });
|
|
366
|
+
case 'manual-compaction':
|
|
367
|
+
// 官方 ManualCompactionNodeView 同样套 callRow 外壳。
|
|
368
|
+
return h(ChatCallRow, null, h(CompactionItemMirror, { node: dataOf(node), t }));
|
|
323
369
|
case 'context':
|
|
324
370
|
return h(ContextInjectionRowMirror, { node: dataOf(node), t });
|
|
325
371
|
case 'unknown':
|
|
326
372
|
return null;
|
|
327
373
|
default:
|
|
328
374
|
// 决策 28:不认识的 kind 一律 fallback(折叠原文),升级不白屏。
|
|
329
|
-
|
|
375
|
+
// 外壳 = 官方 MessageItem.contextRow(官方 UnknownNodeView 的同一个容器类)。
|
|
376
|
+
return h('div', { className: officialClass('MessageItem', 'contextRow') ?? '' }, h('details', { className: 'dshpc-sv-tool' }, h('summary', { className: 'dshpc-sv-notice' }, `${t('sessionUnknownKind')} ${node.kind}`), h('pre', null, safeJson(node.data))));
|
|
330
377
|
}
|
|
331
378
|
}
|
|
332
379
|
/**
|
|
@@ -337,7 +384,9 @@ tailFacts) {
|
|
|
337
384
|
*/
|
|
338
385
|
function assistantBlocks(blocks, t, fileMentions,
|
|
339
386
|
/** 官方 streaming(节点 data.status === 'running'):正文按流式尾渲染、末块思考行走 shimmer。 */
|
|
340
|
-
streaming = false
|
|
387
|
+
streaming = false,
|
|
388
|
+
/** 图片 loader;缺席 = 图片块退回「[图片]」文本占位。 */
|
|
389
|
+
loadImage) {
|
|
341
390
|
if (blocks === undefined)
|
|
342
391
|
return [];
|
|
343
392
|
const parts = [];
|
|
@@ -355,9 +404,28 @@ streaming = false) {
|
|
|
355
404
|
parts.push(h(ReasoningRowMirror, { key: `r${index}`, text: block.text, running: streaming && index === last, t }));
|
|
356
405
|
}
|
|
357
406
|
break;
|
|
358
|
-
case 'image':
|
|
359
|
-
|
|
407
|
+
case 'image': {
|
|
408
|
+
// 官方:连续 image 块并成一组推一次(conversation.chat.message.images,align='start')。
|
|
409
|
+
const previous = blocks[index - 1];
|
|
410
|
+
if (previous !== undefined && previous.kind === 'image')
|
|
411
|
+
break;
|
|
412
|
+
const images = [];
|
|
413
|
+
for (const candidate of blocks.slice(index)) {
|
|
414
|
+
if (candidate.kind !== 'image')
|
|
415
|
+
break;
|
|
416
|
+
const attachment = candidate.attachment;
|
|
417
|
+
if (attachment !== null && typeof attachment === 'object' && typeof attachment.attachmentId === 'string') {
|
|
418
|
+
images.push({ attachment });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (loadImage !== undefined && images.length > 0) {
|
|
422
|
+
parts.push(h(ImageGalleryMirror, { key: `i${index}`, images, load: loadImage, align: 'start', t }));
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
parts.push(h('div', { key: `i${index}`, className: 'dshpc-sv-image' }, t('sessionImage')));
|
|
426
|
+
}
|
|
360
427
|
break;
|
|
428
|
+
}
|
|
361
429
|
case 'tool-call':
|
|
362
430
|
break;
|
|
363
431
|
default:
|
|
@@ -376,6 +444,7 @@ function renderLegacyNode(node, t, fileOpen) {
|
|
|
376
444
|
switch (node.kind) {
|
|
377
445
|
case 'user':
|
|
378
446
|
case 'steering': {
|
|
447
|
+
// shortcut: legacy 兜底路径不带图片 loader(图片仍是「[图片]」占位);keyed 主路已出真图。
|
|
379
448
|
const text = contentText(node.content);
|
|
380
449
|
const { files, hadFiles } = contentFiles(node.content);
|
|
381
450
|
if (text === '' && !hadFiles)
|
|
@@ -695,11 +764,13 @@ export function SessionViewModal(props) {
|
|
|
695
764
|
const isArchived = useSyncExternalStore(workspaceList.subscribe, archivedNow, NOT_ARCHIVED);
|
|
696
765
|
const subscribe = useMemo(() => (onChange) => view.target.subscribe(onChange), [view]);
|
|
697
766
|
const getSnapshot = useMemo(() => () => view.target.getSnapshot(), [view]);
|
|
698
|
-
|
|
767
|
+
// 第三参 = 同一个快照函数:React 要求 SSR 必给,缺了直接抛错。这里的数据挂在 props 的 view 上
|
|
768
|
+
// (不是全局 store),服务端读同一个快照不会串数据;纯全局 store 才需要单独的空快照。
|
|
769
|
+
const chat = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
699
770
|
// 会话生命周期(加载态 / 向前翻页按钮),同样只读订阅。
|
|
700
771
|
const sessionSub = useMemo(() => (onChange) => view.session.subscribe(onChange), [view]);
|
|
701
772
|
const sessionGet = useMemo(() => () => view.session.getSnapshot(), [view]);
|
|
702
|
-
const sessionSnap = useSyncExternalStore(sessionSub, sessionGet);
|
|
773
|
+
const sessionSnap = useSyncExternalStore(sessionSub, sessionGet, sessionGet);
|
|
703
774
|
// host `inbox` 投影(faceOf('inbox')):首轮 durable 事件落盘前用户消息的权威来源,
|
|
704
775
|
// 冷读 / 新打开预览时也拿得到;本地回显被 retire 后靠它继续画气泡。
|
|
705
776
|
const inboxSub = useMemo(() => (onChange) => view.inbox.subscribe(onChange), [view]);
|
|
@@ -729,6 +800,8 @@ export function SessionViewModal(props) {
|
|
|
729
800
|
const [forkTarget, setForkTarget] = useState(null);
|
|
730
801
|
const [forking, setForking] = useState(false);
|
|
731
802
|
const [forkErr, setForkErr] = useState(null);
|
|
803
|
+
/** 官方 ChatView.modalAction(弹窗脚按钮 min-width 72px);官方样式未注入时为 undefined。 */
|
|
804
|
+
const modalActionClass = officialClass('ChatView', 'modalAction') ?? undefined;
|
|
732
805
|
const aliveRef = useRef(true);
|
|
733
806
|
useEffect(() => () => { aliveRef.current = false; }, []);
|
|
734
807
|
// 取消归档时收掉「因为归档才弹的」fork 确认框(atSeq 缺省 = 头部按钮触发);
|
|
@@ -988,7 +1061,28 @@ export function SessionViewModal(props) {
|
|
|
988
1061
|
const tailFacts = useMemo(() => (keyed && store !== undefined
|
|
989
1062
|
? buildTurnTailFacts({ order, store, lastTurn: lastTailTurn })
|
|
990
1063
|
: undefined), [keyed, order, store, lastTailTurn]);
|
|
991
|
-
|
|
1064
|
+
// 底端插槽挂点:末尾是**带回复的 turn-tail** 时挂在它之前(最后那条回复之后、复制 / 分支 /
|
|
1065
|
+
// 用量操作行之上);没有回复(closing 为空)或末尾不是 tail ⇒ 退回流的末尾。
|
|
1066
|
+
const footerAnchorKey = useMemo(() => {
|
|
1067
|
+
if (!keyed || store === undefined)
|
|
1068
|
+
return undefined;
|
|
1069
|
+
const lastKey = order[order.length - 1];
|
|
1070
|
+
const last = lastKey === undefined ? undefined : store.get(lastKey);
|
|
1071
|
+
if (last?.kind !== 'turn-tail')
|
|
1072
|
+
return undefined;
|
|
1073
|
+
const closing = last.data?.closing;
|
|
1074
|
+
return closing === null || closing === undefined ? undefined : lastKey;
|
|
1075
|
+
}, [keyed, order, store]);
|
|
1076
|
+
// 内容两端插槽(调用方自绘):不传 = 不渲染元素(少一层空容器多吃一个 gap);四态都渲染。
|
|
1077
|
+
// 顶端槽的**下边距由自己的类固定**(见 session.css):不依赖相邻项自己的 margin,
|
|
1078
|
+
// 否则相邻项换一种(turn-trigger / 用户气泡 / 提示条)间距就跟着变。
|
|
1079
|
+
const bodyHeaderRow = props.bodyHeader === undefined
|
|
1080
|
+
? null
|
|
1081
|
+
: h('div', { key: 'bodyHeader', className: 'dshpc-sv-body-header' }, props.bodyHeader);
|
|
1082
|
+
const bodyFooterRow = props.bodyFooter === undefined
|
|
1083
|
+
? null
|
|
1084
|
+
: h('div', { key: 'bodyFooter', className: 'dshpc-sv-body-footer' }, props.bodyFooter);
|
|
1085
|
+
const renderNode = useCallback((node, turnProcess, groupPart) => renderKeyedNode(node, turnProcess, tt, onBranchAt, fileOpen, groupPart, deliverFiles, lastTailTurn, lastTailChangesSeq, sessionId, props.onOpenReview, tailFacts, props.view.loadImage, bodyFooterRow, footerAnchorKey), [tt, onBranchAt, fileOpen, deliverFiles, lastTailTurn, lastTailChangesSeq, sessionId, props.onOpenReview, tailFacts, props.view.loadImage, bodyFooterRow, footerAnchorKey]);
|
|
992
1086
|
// 官方 grouped('chat'):把 keyed 流切成「独立条目 + 过程分组」(二级收折)。
|
|
993
1087
|
const isTurnClosed = useCallback((turn) => (turns?.get(turn) ?? turns?.get(String(turn)))?.status !== 'open', [turns]);
|
|
994
1088
|
const groupedView = useMemo(() => keyed ? buildProcessGroups(order, key => store?.get(key), isTurnClosed) : undefined, [keyed, order, store, isTurnClosed]);
|
|
@@ -1036,64 +1130,82 @@ export function SessionViewModal(props) {
|
|
|
1036
1130
|
: h(RunningStatusMirror, { key: 'running', startTime: runningStartTime, t: tt });
|
|
1037
1131
|
const body = rendered.length === 0 && pendingRows.length === 0
|
|
1038
1132
|
? [
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1133
|
+
bodyHeaderRow,
|
|
1134
|
+
// 官方三态各走各的类:error = openError(错误色),loading / 空 = hint。
|
|
1135
|
+
openState === 'error'
|
|
1136
|
+
? h(ChatOpenError, { key: 'hint', text: tt('sessionFailed') })
|
|
1137
|
+
: h(ChatHint, {
|
|
1138
|
+
key: 'hint',
|
|
1139
|
+
text: openState === 'loading' || openState === 'cold' ? tt('sessionLoading') : tt('sessionEmpty'),
|
|
1140
|
+
}),
|
|
1045
1141
|
runningRow,
|
|
1142
|
+
// 已挂在最后一条 tail 上的话这里不再画(否则同一个槽出现两次)。
|
|
1143
|
+
footerAnchorKey === undefined ? bodyFooterRow : null,
|
|
1046
1144
|
]
|
|
1047
1145
|
: [
|
|
1146
|
+
bodyHeaderRow,
|
|
1048
1147
|
showLoadOlder
|
|
1049
1148
|
? h(ChatOlderButton, { key: 'older', label: tt('sessionLoadOlder'), onClick: () => { view.loadOlder(); } })
|
|
1050
1149
|
: null,
|
|
1051
1150
|
...rendered,
|
|
1052
1151
|
...pendingRows,
|
|
1053
1152
|
runningRow,
|
|
1153
|
+
footerAnchorKey === undefined ? bodyFooterRow : null,
|
|
1054
1154
|
];
|
|
1055
1155
|
const flowChildren = Array.isArray(body) ? body : [body];
|
|
1156
|
+
// 头部两槽:标题区整块替换(`header`)与右侧动作追加(`headerActions`)。
|
|
1157
|
+
// 自定义标题区时归档徽标挪到动作区最前 —— 归档是数据事实,不能因为自定义标题就消失。
|
|
1158
|
+
const customHeader = props.header !== undefined;
|
|
1159
|
+
const archivedBadge = isArchived
|
|
1160
|
+
? h('span', { className: 'dshpc-sv-archived', title: tt('sessionArchived') }, h(IconArchiveOutlineRegular, { size: 12 }), tt('sessionArchived'))
|
|
1161
|
+
: undefined;
|
|
1162
|
+
// 「继续对话」:已归档 = 开分支图标 + 确认框;未归档 = 会话图标直跳官方会话页。
|
|
1163
|
+
const continueAction = showContinue
|
|
1164
|
+
? h(Button, {
|
|
1165
|
+
variant: 'outline',
|
|
1166
|
+
size: 'sm',
|
|
1167
|
+
disabled: forking,
|
|
1168
|
+
title: tt('continueBranch'),
|
|
1169
|
+
icon: isArchived
|
|
1170
|
+
? h(IconBranchOutlineRegular, { size: 14 })
|
|
1171
|
+
: h(IconNewChatOutlineRegular, { size: 14 }),
|
|
1172
|
+
onClick: () => {
|
|
1173
|
+
// 官方 openForkSource 判据:动作前读权威快照(client 内存,零远端 IO),
|
|
1174
|
+
// 避免渲染与快照之间的竞态把已归档会话直开。
|
|
1175
|
+
if (archivedNow()) {
|
|
1176
|
+
setForkErr(null);
|
|
1177
|
+
setForkTarget({});
|
|
1178
|
+
}
|
|
1179
|
+
else if (openHostSession !== undefined) {
|
|
1180
|
+
onClose();
|
|
1181
|
+
openHostSession(sessionId);
|
|
1182
|
+
}
|
|
1183
|
+
},
|
|
1184
|
+
}, tt('continueBranch'))
|
|
1185
|
+
: undefined;
|
|
1186
|
+
// 动作区顺序(都靠右,「继续对话」与 ✕ 恒在最右):归档徽标 → 调用方动作 → 继续对话 → ✕。
|
|
1187
|
+
const headActions = continueAction === undefined && props.headerActions === undefined && !(customHeader && archivedBadge !== undefined)
|
|
1188
|
+
? undefined
|
|
1189
|
+
: h(Fragment, null, customHeader ? archivedBadge : null, props.headerActions, continueAction);
|
|
1056
1190
|
// 外壳 = **核心 Modal**(size lg:min(1120px,100vw-32px) × calc(100%-80px),兄弟项目大弹窗同值);
|
|
1057
1191
|
// 「继续对话」与确认框按钮一律**核心 Button**,不就地自绘(用户 2026-10-22 拍板)。
|
|
1058
1192
|
// 会话中间部分 = mirror/ 官方 1:1 复刻(ChatViewFrame:官方类优先,缺失回退自绘)。
|
|
1059
1193
|
return h(Fragment, null, h(Modal, {
|
|
1060
1194
|
size: 'lg',
|
|
1061
|
-
//
|
|
1062
|
-
//
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
headerActions:
|
|
1075
|
-
? h(Button, {
|
|
1076
|
-
variant: 'outline',
|
|
1077
|
-
size: 'sm',
|
|
1078
|
-
disabled: forking,
|
|
1079
|
-
title: tt('continueBranch'),
|
|
1080
|
-
icon: isArchived
|
|
1081
|
-
? h(IconBranchOutlineRegular, { size: 14 })
|
|
1082
|
-
: h(IconNewChatOutlineRegular, { size: 14 }),
|
|
1083
|
-
onClick: () => {
|
|
1084
|
-
// 官方 openForkSource 判据:动作前读权威快照(client 内存,零远端 IO),
|
|
1085
|
-
// 避免渲染与快照之间的竞态把已归档会话直开。
|
|
1086
|
-
if (archivedNow()) {
|
|
1087
|
-
setForkErr(null);
|
|
1088
|
-
setForkTarget({});
|
|
1089
|
-
}
|
|
1090
|
-
else if (openHostSession !== undefined) {
|
|
1091
|
-
onClose();
|
|
1092
|
-
openHostSession(sessionId);
|
|
1093
|
-
}
|
|
1094
|
-
},
|
|
1095
|
-
}, tt('continueBranch'))
|
|
1096
|
-
: undefined,
|
|
1195
|
+
// 二级:整块标题区交给调用方(含 sessionId 行);一级:只换标题行,副标题仍由核心画。
|
|
1196
|
+
// 会话 id 是**标识**,放标题下方副标题行,不占正文(用户 2026-10-22:一串 MD5 不该出现在正文里)。
|
|
1197
|
+
header: props.header,
|
|
1198
|
+
title: customHeader ? undefined : (props.title ?? heading ?? tt('sessionHeading')),
|
|
1199
|
+
subtitle: customHeader
|
|
1200
|
+
? undefined
|
|
1201
|
+
: h(Fragment, null, h('span', null, sessionId),
|
|
1202
|
+
// 可见探针:官方样式未命中时直接显示(省得翻控制台)。命中则不显示。
|
|
1203
|
+
officialCount === 0
|
|
1204
|
+
? h('span', { className: 'dshpc-sv-probe' }, ' ⚠ ', tt('sessionProbeStyle'))
|
|
1205
|
+
: null),
|
|
1206
|
+
// 归档标识(官方归档图标 + 「已归档」)走头部右侧小块;自定义标题区时改挂动作区最前。
|
|
1207
|
+
headerExtra: customHeader ? undefined : archivedBadge,
|
|
1208
|
+
headerActions: headActions,
|
|
1097
1209
|
onClose,
|
|
1098
1210
|
translate: tt,
|
|
1099
1211
|
// 底部暂存输入框走 footer(不随会话区滚动):打字 → 确定 → onContinue 带话开分支跳官方视图。
|
|
@@ -1127,9 +1239,10 @@ export function SessionViewModal(props) {
|
|
|
1127
1239
|
setForkTarget(null); },
|
|
1128
1240
|
title: tt('forkConfirmTitle'),
|
|
1129
1241
|
translate: tt,
|
|
1242
|
+
// 弹窗脚按钮宽度与官方对话框对齐(官方 ChatView.modalAction:min-width 72px)。
|
|
1130
1243
|
footer: [
|
|
1131
|
-
h(Button, { key: 'cancel', variant: 'outline', disabled: forking, onClick: () => { setForkTarget(null); } }, tt('cancel')),
|
|
1132
|
-
h(Button, { key: 'accept', variant: 'primary', disabled: forking || !canFork, onClick: onForkAccept }, forking ? tt('forkWorking') : tt('forkConfirmAccept')),
|
|
1244
|
+
h(Button, { key: 'cancel', variant: 'outline', className: modalActionClass, disabled: forking, onClick: () => { setForkTarget(null); } }, tt('cancel')),
|
|
1245
|
+
h(Button, { key: 'accept', variant: 'primary', className: modalActionClass, disabled: forking || !canFork, onClick: onForkAccept }, forking ? tt('forkWorking') : tt('forkConfirmAccept')),
|
|
1133
1246
|
],
|
|
1134
1247
|
}, h('p', { className: 'dshpc-sv-forktext' }, tt('forkConfirmText')), canFork ? null : h('p', { className: 'dshpc-sv-forkerr' }, tt('forkUnavailable')), forkErr !== null ? h('p', { className: 'dshpc-sv-forkerr' }, tt('forkFailed', { error: forkErr })) : null));
|
|
1135
1248
|
}
|