@shellraining/dsh-side-chat 0.0.0-stage → 0.1.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/lib/client.js ADDED
@@ -0,0 +1,1849 @@
1
+ // Generated by `pnpm run build`; edit src/, not this file.
2
+ window.__ModuleLoader__.load({
3
+ id: "@shellraining/dsh-side-chat",
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __export = (target, all) => {
12
+ for (var name2 in all)
13
+ __defProp(target, name2, { get: all[name2], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
+
25
+ // src/client/index.tsx
26
+ var index_exports = {};
27
+ __export(index_exports, {
28
+ apply: () => apply,
29
+ inject: () => inject,
30
+ name: () => name
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+ var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives");
34
+ var import_react3 = require("react");
35
+
36
+ // src/shared/protocol.ts
37
+ var PLUGIN_NAME = "dsh-side-chat";
38
+ var TAB_KIND = "side-chat";
39
+ var API_PREFIX = "/dsh-side-chat/api/";
40
+ var EVENTS_ROUTE = "events";
41
+ var BOUNDARY_SOURCE_KIND = "plugin:side-chat";
42
+ var BOUNDARY_FIRST_LINE = "Side conversation boundary.";
43
+
44
+ // src/client/commands.ts
45
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
46
+ var COMMANDS = ["side", "btw"];
47
+ var LEADING_COMMAND = /^\/([a-z0-9_-]+)(?=\s|$)/;
48
+ function commandOf(token) {
49
+ const name2 = LEADING_COMMAND.exec(token)?.[1];
50
+ return COMMANDS.find((command) => command === name2);
51
+ }
52
+ function sideChatSource({ t, eligible, open }) {
53
+ const claim = (session, name2) => ({
54
+ name: name2,
55
+ token: `/${name2} `,
56
+ hint: t("command.hint"),
57
+ submit: (args) => {
58
+ try {
59
+ open(session.sessionId, args.trim());
60
+ return Promise.resolve({ kind: "success" });
61
+ } catch {
62
+ return Promise.resolve({ kind: "error", text: t("command.unavailable") });
63
+ }
64
+ }
65
+ });
66
+ const claimOf = (session, token) => {
67
+ const name2 = commandOf(token);
68
+ return name2 === void 0 || !eligible(session.sessionId) ? void 0 : claim(session, name2);
69
+ };
70
+ const rows = () => [
71
+ { name: "side", label: t("command.side.label"), description: t("command.side.description"), icon: import_dsh_client_ui_primitives.IconNewChatOutlineRegular },
72
+ { name: "btw", label: t("command.btw.label"), description: t("command.btw.description"), icon: import_dsh_client_ui_primitives.IconQuestionOutlineRegular }
73
+ ];
74
+ return {
75
+ trigger: "/",
76
+ name: "side-chat",
77
+ // After the shell's commands, before skills.
78
+ order: 1,
79
+ // The rows read as more commands; a separate group title would only repeat them.
80
+ showGroupTitle: false,
81
+ candidates: (session, request) => {
82
+ if (request.position !== "leading" || !eligible(session.sessionId)) return Promise.resolve([]);
83
+ return Promise.resolve((0, import_dsh_client_ui_primitives.rankByName)(rows(), request.query));
84
+ },
85
+ onPick: ({ candidate, session }) => {
86
+ const found = claimOf(session, `/${candidate.name}`);
87
+ return found === void 0 ? void 0 : { claim: found };
88
+ },
89
+ matchSpace: (session, token) => {
90
+ const found = claimOf(session, token);
91
+ return found === void 0 ? void 0 : { claim: found };
92
+ },
93
+ matchClaim: (session, token) => Promise.resolve(claimOf(session, token)),
94
+ matchEnter: (session, line, _signal, envelope) => {
95
+ const found = claimOf(session, line);
96
+ if (found === void 0) return Promise.resolve(void 0);
97
+ if (envelope.attachments > 0) return Promise.reject(new Error(t("command.attachments")));
98
+ return Promise.resolve({ claim: found });
99
+ }
100
+ };
101
+ }
102
+
103
+ // src/client/labels.ts
104
+ function panelLabels(t) {
105
+ return { markdown: markdownLabels(t), terminal: terminalLabels(t), diff: diffLabels(t) };
106
+ }
107
+ function diffLabels(t) {
108
+ return {
109
+ codeLabel: t("code.title"),
110
+ wrapLabel: t("code.wrap"),
111
+ unwrapLabel: t("code.unwrap"),
112
+ copy: t("code.copy"),
113
+ copied: t("code.copied"),
114
+ collapseAria: t("terminal.collapseAria"),
115
+ collapse: t("terminal.collapse"),
116
+ expandAria: (count) => t("terminal.expandAria", { count }),
117
+ expand: (count) => t("terminal.expand", { count })
118
+ };
119
+ }
120
+ function markdownLabels(t) {
121
+ return {
122
+ code: {
123
+ copyLabel: t("code.copy"),
124
+ copiedLabel: t("code.copied"),
125
+ toolbarLabels: { codeLabel: t("code.title"), wrapLabel: t("code.wrap"), unwrapLabel: t("code.unwrap") }
126
+ },
127
+ footnotes: t("markdown.footnotes")
128
+ };
129
+ }
130
+ function terminalLabels(t) {
131
+ return {
132
+ commandLine: (line) => t("terminal.commandLine", { line }),
133
+ signal: (signal) => t("terminal.signal", { signal }),
134
+ exitCode: (code) => t("terminal.exitCode", { code }),
135
+ noExitCode: t("terminal.noExitCode"),
136
+ running: t("terminal.running"),
137
+ failed: t("terminal.failed"),
138
+ done: t("terminal.done"),
139
+ copy: t("code.copy"),
140
+ copied: t("code.copied"),
141
+ noOutput: t("terminal.noOutput"),
142
+ collapseAria: t("terminal.collapseAria"),
143
+ collapse: t("terminal.collapse"),
144
+ expandAria: (count) => t("terminal.expandAria", { count }),
145
+ expand: (count) => t("terminal.expand", { count })
146
+ };
147
+ }
148
+
149
+ // src/client/locale.ts
150
+ var LOCALE_NS = "dsh-side-chat";
151
+ var ZH = {
152
+ "tab.title": "\u4FA7\u8FB9\u4F1A\u8BDD",
153
+ "tab.titleIndexed": "\u4FA7\u8FB9\u4F1A\u8BDD {index}",
154
+ "tab.new": "\u65B0\u5EFA\u4FA7\u8FB9\u4F1A\u8BDD",
155
+ "guide.title": "\u4FA7\u8FB9\u4F1A\u8BDD",
156
+ "guide.description": "\u6D3E\u751F\u5F53\u524D\u4F1A\u8BDD\u7684\u4E34\u65F6\u5BF9\u8BDD\uFF0C\u53EF\u4FEE\u6539\u6587\u4EF6\uFF0C\u5173\u95ED\u5373\u9500\u6BC1",
157
+ "status.blank": "\u672A\u5F00\u59CB",
158
+ "status.running": "\u8FD0\u884C\u4E2D",
159
+ "status.sending": "\u53D1\u9001\u4E2D",
160
+ "status.idle": "\u7A7A\u95F2",
161
+ "status.queued": "{count} \u6761\u6392\u961F",
162
+ "empty.title": "\u4FA7\u8FB9\u4F1A\u8BDD",
163
+ "empty.blank": "\u53D1\u9001\u7B2C\u4E00\u6761\u6D88\u606F\u65F6\u521B\u5EFA\uFF0C\u7EE7\u627F\u5F53\u524D\u4F1A\u8BDD\u5DF2\u5B8C\u6210\u7684\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u5199\u5165\u4E3B\u4F1A\u8BDD\u3002",
164
+ "empty.created": "\u4FA7\u8FB9\u4F1A\u8BDD\u5DF2\u521B\u5EFA\uFF0C\u7B49\u5F85\u56DE\u590D\u2026",
165
+ "empty.hint": "Enter \u53D1\u9001\uFF0CShift+Enter \u6362\u884C",
166
+ "composer.label": "\u4FA7\u8FB9\u4F1A\u8BDD\u8F93\u5165\u6846",
167
+ "composer.placeholder": "\u7EE7\u7EED\u5728\u4FA7\u8FB9\u4F1A\u8BDD\u4E2D\u63D0\u95EE",
168
+ "composer.placeholderBlank": "\u95EE\u70B9\u4EC0\u4E48\uFF0C\u4E0D\u4F1A\u6253\u6270\u4E3B\u4F1A\u8BDD",
169
+ "composer.send": "\u53D1\u9001",
170
+ "composer.stop": "\u505C\u6B62",
171
+ "thinking": "\u601D\u8003\u4E2D\u2026",
172
+ "reasoning.running": "\u601D\u8003\u4E2D",
173
+ "reasoning.done": "\u5DF2\u601D\u8003",
174
+ "tool.fallback": "\u5DE5\u5177\u8C03\u7528",
175
+ "tool.bash": "\u8FD0\u884C\u547D\u4EE4",
176
+ "tool.read": "\u8BFB\u53D6",
177
+ "tool.write": "\u5199\u5165",
178
+ "tool.edit": "\u7F16\u8F91",
179
+ "tool.glob": "\u67E5\u627E\u6587\u4EF6",
180
+ "tool.grep": "\u641C\u7D22",
181
+ "tool.web_fetch": "\u83B7\u53D6\u7F51\u9875",
182
+ "tool.web_search": "\u641C\u7D22\u7F51\u9875",
183
+ "tool.failed": "\u5931\u8D25",
184
+ "tool.emptyOutput": "\uFF08\u65E0\u8F93\u51FA\uFF09",
185
+ "tool.arguments": "\u53C2\u6570",
186
+ "tool.result": "\u7ED3\u679C",
187
+ "read.window": "\u663E\u793A {shown} / {total} \u884C",
188
+ "composer.gone": "\u4FA7\u8FB9\u4F1A\u8BDD\u5DF2\u7ED3\u675F\uFF0C\u8BF7\u65B0\u5EFA\u4E00\u4E2A\u7EE7\u7EED",
189
+ "notice.dismiss": "\u5173\u95ED\u63D0\u793A",
190
+ "pending.queued": "\u6392\u961F\u4E2D",
191
+ "pending.sending": "\u53D1\u9001\u4E2D",
192
+ "context.skill": "\u6280\u80FD",
193
+ "context.skillNamed": "\u6280\u80FD\uFF1A{name}",
194
+ "context.steering": "\u63D2\u8BDD",
195
+ "context.sessionReference": "\u4F1A\u8BDD\u5F15\u7528",
196
+ "context.agentInstructions": "\u4EE3\u7406\u6307\u4EE4",
197
+ "context.compactCheckpoint": "\u538B\u7F29\u68C0\u67E5\u70B9",
198
+ "context.generic": "\u4E0A\u4E0B\u6587",
199
+ "dock.tokens": "{count} tok",
200
+ "dock.cacheHit": "\u7F13\u5B58\u547D\u4E2D {percent}%",
201
+ "dock.contextAria": "\u4E0A\u4E0B\u6587\u5DF2\u7528 {percent}%",
202
+ "dock.contextTitle": "\u7EA6 {used} / {window} tok",
203
+ "dock.usageTitle": "\u672A\u7F13\u5B58\u8F93\u5165 {input} \xB7 \u7F13\u5B58\u8BFB\u53D6 {cacheRead} \xB7 \u8F93\u51FA {output}",
204
+ "dock.usageTitleWithWrite": "\u672A\u7F13\u5B58\u8F93\u5165 {input} \xB7 \u7F13\u5B58\u8BFB\u53D6 {cacheRead} \xB7 \u7F13\u5B58\u5199\u5165 {cacheWrite} \xB7 \u8F93\u51FA {output}",
205
+ "dock.thousand": "{value}K",
206
+ "dock.million": "{value}M",
207
+ "code.copy": "\u590D\u5236",
208
+ "code.copied": "\u5DF2\u590D\u5236",
209
+ "code.title": "\u4EE3\u7801",
210
+ "code.wrap": "\u81EA\u52A8\u6362\u884C",
211
+ "code.unwrap": "\u53D6\u6D88\u6362\u884C",
212
+ "markdown.footnotes": "\u811A\u6CE8",
213
+ "terminal.commandLine": "\u547D\u4EE4\u7B2C {line} \u884C",
214
+ "terminal.signal": "\u4FE1\u53F7 {signal}",
215
+ "terminal.exitCode": "\u9000\u51FA\u7801 {code}",
216
+ "terminal.noExitCode": "\u65E0\u9000\u51FA\u7801",
217
+ "terminal.running": "\u8FD0\u884C\u4E2D",
218
+ "terminal.failed": "\u5931\u8D25",
219
+ "terminal.done": "\u5B8C\u6210",
220
+ "terminal.noOutput": "\u6CA1\u6709\u8F93\u51FA",
221
+ "terminal.collapse": "\u6536\u8D77",
222
+ "terminal.collapseAria": "\u6536\u8D77\u8F93\u51FA",
223
+ "terminal.expand": "\u5C55\u5F00 {count} \u884C",
224
+ "terminal.expandAria": "\u5C55\u5F00\u9690\u85CF\u7684 {count} \u884C",
225
+ "error.unknown-source": "\u539F\u4F1A\u8BDD\u5DF2\u4E0D\u5728\uFF0C\u65E0\u6CD5\u521B\u5EFA\u4FA7\u8FB9\u4F1A\u8BDD",
226
+ "error.not-found": "\u4FA7\u8FB9\u4F1A\u8BDD\u5DF2\u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u5DF2\u88AB\u9500\u6BC1\uFF09",
227
+ "error.empty-prompt": "\u6D88\u606F\u4E3A\u7A7A",
228
+ "error.create-failed": "\u521B\u5EFA\u4FA7\u8FB9\u4F1A\u8BDD\u5931\u8D25",
229
+ "error.prompt-failed": "\u53D1\u9001\u6D88\u606F\u5931\u8D25",
230
+ "error.dispose-failed": "\u9500\u6BC1\u4FA7\u8FB9\u4F1A\u8BDD\u5931\u8D25",
231
+ "error.internal": "\u4FA7\u8FB9\u4F1A\u8BDD\u5185\u90E8\u9519\u8BEF",
232
+ "error.network": "\u65E0\u6CD5\u8FDE\u63A5\u5230 DSH",
233
+ "error.unknown": "\u51FA\u9519\u4E86",
234
+ "error.agent": "\u4FA7\u8FB9\u4F1A\u8BDD\u8FD0\u884C\u51FA\u9519",
235
+ "command.side.label": "\u4FA7\u8FB9\u4F1A\u8BDD",
236
+ "command.side.description": "\u5728\u53F3\u4FA7\u8FB9\u680F\u65B0\u5F00\u4FA7\u8FB9\u4F1A\u8BDD\uFF0C\u53EF\u76F4\u63A5\u5E26\u4E0A\u95EE\u9898",
237
+ "command.btw.label": "\u987A\u4FBF\u95EE",
238
+ "command.btw.description": "\u5728\u4FA7\u8FB9\u4F1A\u8BDD\u91CC\u63D0\u95EE\uFF0C\u4E0D\u6253\u65AD\u4E3B\u4F1A\u8BDD",
239
+ "command.hint": "[\u95EE\u9898]",
240
+ "command.unavailable": "\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u663E\u793A\u5728\u4E3B\u680F\uFF0C\u65E0\u6CD5\u6253\u5F00\u4FA7\u8FB9\u4F1A\u8BDD",
241
+ "command.attachments": "\u4FA7\u8FB9\u4F1A\u8BDD\u547D\u4EE4\u4E0D\u652F\u6301\u9644\u4EF6"
242
+ };
243
+ var EN = {
244
+ "tab.title": "Side chat",
245
+ "tab.titleIndexed": "Side chat {index}",
246
+ "tab.new": "New side chat",
247
+ "guide.title": "Side chat",
248
+ "guide.description": "A temporary fork of this session that can edit files; closing it destroys it",
249
+ "status.blank": "Not started",
250
+ "status.running": "Running",
251
+ "status.sending": "Sending",
252
+ "status.idle": "Idle",
253
+ "status.queued": "{count} queued",
254
+ "empty.title": "Side chat",
255
+ "empty.blank": "Created on your first message. It inherits the completed context of this session and never writes to it.",
256
+ "empty.created": "Side chat created. Waiting for a reply\u2026",
257
+ "empty.hint": "Enter to send, Shift+Enter for a new line",
258
+ "composer.label": "Side chat input",
259
+ "composer.placeholder": "Ask a follow-up in the side chat",
260
+ "composer.placeholderBlank": "Ask anything without disturbing the main session",
261
+ "composer.send": "Send",
262
+ "composer.stop": "Stop",
263
+ "thinking": "Thinking\u2026",
264
+ "reasoning.running": "Thinking",
265
+ "reasoning.done": "Thought",
266
+ "tool.fallback": "Tool call",
267
+ "tool.bash": "Run command",
268
+ "tool.read": "Read",
269
+ "tool.write": "Write",
270
+ "tool.edit": "Edit",
271
+ "tool.glob": "Find files",
272
+ "tool.grep": "Search",
273
+ "tool.web_fetch": "Fetch page",
274
+ "tool.web_search": "Web search",
275
+ "tool.failed": "Failed",
276
+ "tool.emptyOutput": "(no output)",
277
+ "tool.arguments": "Arguments",
278
+ "tool.result": "Result",
279
+ "read.window": "Showing {shown} of {total} lines",
280
+ "composer.gone": "This side chat has ended; start a new one to continue",
281
+ "notice.dismiss": "Dismiss",
282
+ "pending.queued": "Queued",
283
+ "pending.sending": "Sending",
284
+ "context.skill": "Skill",
285
+ "context.skillNamed": "Skill: {name}",
286
+ "context.steering": "Steering",
287
+ "context.sessionReference": "Session reference",
288
+ "context.agentInstructions": "Agent instructions",
289
+ "context.compactCheckpoint": "Compaction checkpoint",
290
+ "context.generic": "Context",
291
+ "dock.tokens": "{count} tok",
292
+ "dock.cacheHit": "Cache hit {percent}%",
293
+ "dock.contextAria": "{percent}% of context used",
294
+ "dock.contextTitle": "~{used} / {window} tok",
295
+ "dock.usageTitle": "Uncached {input} \xB7 cached {cacheRead} \xB7 output {output}",
296
+ "dock.usageTitleWithWrite": "Uncached {input} \xB7 cached {cacheRead} \xB7 cache write {cacheWrite} \xB7 output {output}",
297
+ "dock.thousand": "{value}K",
298
+ "dock.million": "{value}M",
299
+ "code.copy": "Copy",
300
+ "code.copied": "Copied",
301
+ "code.title": "Code",
302
+ "code.wrap": "Wrap lines",
303
+ "code.unwrap": "Unwrap lines",
304
+ "markdown.footnotes": "Footnotes",
305
+ "terminal.commandLine": "Command line {line}",
306
+ "terminal.signal": "Signal {signal}",
307
+ "terminal.exitCode": "Exit code {code}",
308
+ "terminal.noExitCode": "No exit code",
309
+ "terminal.running": "Running",
310
+ "terminal.failed": "Failed",
311
+ "terminal.done": "Done",
312
+ "terminal.noOutput": "No output",
313
+ "terminal.collapse": "Collapse",
314
+ "terminal.collapseAria": "Collapse output",
315
+ "terminal.expand": "Show {count} more lines",
316
+ "terminal.expandAria": "Show {count} hidden lines",
317
+ "error.unknown-source": "The source session is gone; the side chat cannot be created",
318
+ "error.not-found": "The side chat no longer exists (it may have been destroyed)",
319
+ "error.empty-prompt": "The message is empty",
320
+ "error.create-failed": "Could not create the side chat",
321
+ "error.prompt-failed": "Could not send the message",
322
+ "error.dispose-failed": "Could not destroy the side chat",
323
+ "error.internal": "Side chat internal error",
324
+ "error.network": "Cannot reach DSH",
325
+ "error.unknown": "Something went wrong",
326
+ "error.agent": "The side chat hit an error",
327
+ "command.side.label": "Side chat",
328
+ "command.side.description": "Open a side chat in the right sidebar, optionally with a question",
329
+ "command.btw.label": "By the way",
330
+ "command.btw.description": "Ask in a side chat without interrupting the main session",
331
+ "command.hint": "[question]",
332
+ "command.unavailable": "This session is not on screen; cannot open a side chat",
333
+ "command.attachments": "Side chat commands do not accept attachments"
334
+ };
335
+
336
+ // src/client/model.ts
337
+ var EMPTY = Object.freeze({});
338
+ function record(value) {
339
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : EMPTY;
340
+ }
341
+ function list(value) {
342
+ return Array.isArray(value) ? value : [];
343
+ }
344
+ function str(value) {
345
+ return typeof value === "string" ? value : "";
346
+ }
347
+ function textOf(content) {
348
+ return list(content).map((block) => {
349
+ const item = record(block);
350
+ return item.type === "text" ? str(item.text) : "";
351
+ }).filter((text) => text !== "").join("\n");
352
+ }
353
+ function toolTextOf(content) {
354
+ const text = textOf(content);
355
+ if (text !== "") return text;
356
+ const blocks = list(content).filter((block) => block !== null && typeof block === "object");
357
+ if (blocks.length === 0) return "";
358
+ try {
359
+ return blocks.map((block) => JSON.stringify(block)).join("\n");
360
+ } catch {
361
+ return "";
362
+ }
363
+ }
364
+ function timeOf(value) {
365
+ if (typeof value === "number" && Number.isFinite(value)) return value;
366
+ if (typeof value === "string") {
367
+ const parsed = Date.parse(value);
368
+ return Number.isNaN(parsed) ? null : parsed;
369
+ }
370
+ return null;
371
+ }
372
+ function isBoundary(source, text) {
373
+ const kind = record(source).kind;
374
+ if (kind === BOUNDARY_SOURCE_KIND) return true;
375
+ const firstLine2 = text.split("\n", 1)[0] ?? "";
376
+ return firstLine2.trim() === BOUNDARY_FIRST_LINE;
377
+ }
378
+ function contextOf(source) {
379
+ switch (source.kind) {
380
+ case "skill-invocation":
381
+ return { tag: "skill", name: str(source.name) };
382
+ case "session-reference":
383
+ return { tag: "sessionReference", name: "" };
384
+ case "agent-instructions":
385
+ return { tag: "agentInstructions", name: "" };
386
+ case "compact-checkpoint":
387
+ return { tag: "compactCheckpoint", name: "" };
388
+ case "steering":
389
+ return { tag: "steering", name: "" };
390
+ default:
391
+ return { tag: "generic", name: "" };
392
+ }
393
+ }
394
+ function assistantBlock(value) {
395
+ const block = record(value);
396
+ if (block.type === "text" || block.type === "reasoning") {
397
+ const text = str(block.text);
398
+ return text === "" ? null : { kind: block.type, text };
399
+ }
400
+ if (block.type === "tool-call") {
401
+ return {
402
+ kind: "tool-call",
403
+ callId: block.id === void 0 || block.id === null ? "" : String(block.id),
404
+ name: str(block.name),
405
+ argsRaw: typeof block.arguments === "string" ? block.arguments : JSON.stringify(block.arguments ?? {}),
406
+ result: null
407
+ };
408
+ }
409
+ return null;
410
+ }
411
+ var MAX_CONTEXT_TEXT = 4e3;
412
+ var MAX_TOOL_TEXT = 16e3;
413
+ function foldEntries(entries, floor) {
414
+ const nodes = [];
415
+ const results = /* @__PURE__ */ new Map();
416
+ const sorted = [...entries].sort((a, b) => a.seq - b.seq);
417
+ for (const event of sorted) {
418
+ if (event.seq < floor) continue;
419
+ if (event.surfaceOp !== void 0 && event.surfaceOp !== "append" && (event.type === "user/message" || event.type === "assistant/message")) continue;
420
+ const data = record(event.data);
421
+ if (event.type === "user/message") {
422
+ const text = textOf(data.content);
423
+ if (text === "" || isBoundary(data.source, text)) continue;
424
+ const source = record(data.source);
425
+ if (source.kind === void 0 || source.kind === "user") {
426
+ nodes.push({ kind: "user", seq: event.seq, time: timeOf(event.time), text });
427
+ } else {
428
+ nodes.push({ kind: "context", seq: event.seq, ...contextOf(source), text: text.slice(0, MAX_CONTEXT_TEXT) });
429
+ }
430
+ } else if (event.type === "assistant/message") {
431
+ const blocks = list(record(data.message).content).map(assistantBlock).filter((block) => block !== null);
432
+ if (blocks.length > 0) nodes.push({ kind: "assistant", seq: event.seq, blocks });
433
+ } else if (event.type === "tool/result") {
434
+ const message = record(data.message);
435
+ const id = record(message.source).callId ?? data.callId;
436
+ if (id === void 0 || id === null) continue;
437
+ results.set(String(id), { text: toolTextOf(message.content).slice(0, MAX_TOOL_TEXT), isError: message.isError === true });
438
+ }
439
+ }
440
+ if (results.size === 0) return nodes;
441
+ return nodes.map((node) => node.kind !== "assistant" ? node : {
442
+ ...node,
443
+ blocks: node.blocks.map((block) => block.kind === "tool-call" && results.has(block.callId) ? { ...block, result: results.get(block.callId) ?? null } : block)
444
+ });
445
+ }
446
+ function liveBlocks(live) {
447
+ const attempt = live?.activeAttempt;
448
+ if (!attempt) return [];
449
+ return attempt.blocks.flatMap((block) => {
450
+ if (block.kind === "tool-call") return [{ ...block, result: null }];
451
+ return block.text === "" ? [] : [block];
452
+ });
453
+ }
454
+ function settles(live, entry) {
455
+ const attempt = live?.activeAttempt;
456
+ if (!attempt || entry.type !== "assistant/message" && entry.type !== "assistant/attempt") return false;
457
+ if (attempt.turn === void 0 || attempt.turn === null || attempt.step === void 0 || attempt.step === null) return false;
458
+ const data = record(entry.data);
459
+ return data.turn === attempt.turn && data.step === attempt.step;
460
+ }
461
+ var USAGE_FIELDS = [
462
+ ["inputTokens", "uncachedInputTokens"],
463
+ ["outputTokens", "outputTokens"],
464
+ ["cacheReadTokens", "cacheReadTokens"],
465
+ ["cacheWriteTokens", "cacheWriteTokens"]
466
+ ];
467
+ function bucketsOf(value) {
468
+ const usage = record(value);
469
+ const buckets = { uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
470
+ let recognized = false;
471
+ for (const [field, bucket] of USAGE_FIELDS) {
472
+ const raw = usage[field];
473
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) continue;
474
+ buckets[bucket] = raw;
475
+ recognized = true;
476
+ }
477
+ return recognized ? buckets : null;
478
+ }
479
+ function usageOf(entry) {
480
+ if (entry.type !== "assistant/message" && entry.type !== "assistant/attempt") return null;
481
+ const data = record(entry.data);
482
+ const direct = bucketsOf(data.usage);
483
+ if (direct !== null) return direct;
484
+ const stream = list(data.stream);
485
+ for (let index = stream.length - 1; index >= 0; index -= 1) {
486
+ const frame = record(stream[index]);
487
+ const chunk = record(frame.chunk);
488
+ if (frame.type === "chunk" && chunk.type === "usage") {
489
+ const buckets = bucketsOf(chunk.usage);
490
+ if (buckets !== null) return buckets;
491
+ }
492
+ }
493
+ return null;
494
+ }
495
+ function cacheHitText(cacheRead, billedInput) {
496
+ if (!(billedInput > 0)) return null;
497
+ if (cacheRead >= billedInput) return "100";
498
+ const percent = cacheRead / billedInput * 100;
499
+ const whole = Math.round(percent);
500
+ return whole < 100 ? String(whole) : String(Math.floor(percent * 10) / 10);
501
+ }
502
+ function metricsOf(entries, floor) {
503
+ const totals = { uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
504
+ let last = null;
505
+ let pressureTokens = null;
506
+ let contextWindow = null;
507
+ for (const entry of entries) {
508
+ if (entry.seq < floor) continue;
509
+ const data = record(entry.data);
510
+ if (entry.type === "request/context") {
511
+ const window2 = data.contextWindow;
512
+ if (typeof window2 === "number" && Number.isFinite(window2) && window2 > 0) contextWindow = window2;
513
+ continue;
514
+ }
515
+ if (entry.type === "llm/retry-started") {
516
+ if (last !== null && last.turn === data.turn && last.step === data.step) last = null;
517
+ continue;
518
+ }
519
+ const buckets = usageOf(entry);
520
+ if (buckets === null) continue;
521
+ const keyed = typeof data.turn === "number" && typeof data.step === "number";
522
+ if (keyed && last !== null && last.turn === data.turn && last.step === data.step) {
523
+ for (const [, bucket] of USAGE_FIELDS) totals[bucket] -= last.buckets[bucket];
524
+ }
525
+ for (const [, bucket] of USAGE_FIELDS) totals[bucket] += buckets[bucket];
526
+ last = keyed ? { turn: data.turn, step: data.step, buckets } : null;
527
+ pressureTokens = buckets.uncachedInputTokens + buckets.cacheReadTokens + buckets.cacheWriteTokens;
528
+ }
529
+ const billedInputTokens = totals.uncachedInputTokens + totals.cacheReadTokens + totals.cacheWriteTokens;
530
+ const totalTokens = billedInputTokens + totals.outputTokens;
531
+ if (totalTokens === 0) return null;
532
+ return {
533
+ ...totals,
534
+ billedInputTokens,
535
+ totalTokens,
536
+ pressureTokens,
537
+ cacheHitPercent: cacheHitText(totals.cacheReadTokens, billedInputTokens),
538
+ contextWindow
539
+ };
540
+ }
541
+ function occupancyOf(metrics, baseline) {
542
+ if (metrics === null || metrics.pressureTokens === null) return null;
543
+ const contextWindow = metrics.contextWindow ?? baseline;
544
+ if (contextWindow === null || !(contextWindow > 0)) return null;
545
+ const usedTokens = metrics.pressureTokens;
546
+ return { percent: Math.min(100, Math.round(usedTokens / contextWindow * 100)), usedTokens, contextWindow };
547
+ }
548
+ function createSseParser() {
549
+ let buffer = "";
550
+ function parse(block) {
551
+ let event = "message";
552
+ const data = [];
553
+ for (const line of block.split("\n")) {
554
+ if (line === "" || line.startsWith(":")) continue;
555
+ const colon = line.indexOf(":");
556
+ const field = colon < 0 ? line : line.slice(0, colon);
557
+ let value = colon < 0 ? "" : line.slice(colon + 1);
558
+ if (value.startsWith(" ")) value = value.slice(1);
559
+ if (field === "event") event = value;
560
+ else if (field === "data") data.push(value);
561
+ }
562
+ if (data.length === 0) return null;
563
+ const raw = data.join("\n");
564
+ try {
565
+ return { event, data: JSON.parse(raw) };
566
+ } catch {
567
+ return { event, data: raw };
568
+ }
569
+ }
570
+ return {
571
+ push(chunk) {
572
+ buffer += chunk;
573
+ if (buffer.includes("\r")) buffer = buffer.replace(/\r\n?/g, "\n");
574
+ const frames = [];
575
+ let boundary = buffer.indexOf("\n\n");
576
+ while (boundary >= 0) {
577
+ const frame = parse(buffer.slice(0, boundary));
578
+ buffer = buffer.slice(boundary + 2);
579
+ if (frame !== null) frames.push(frame);
580
+ boundary = buffer.indexOf("\n\n");
581
+ }
582
+ return frames;
583
+ }
584
+ };
585
+ }
586
+
587
+ // src/client/transport.ts
588
+ var HostError = class extends Error {
589
+ constructor(code, message = "") {
590
+ super(message);
591
+ this.code = code;
592
+ this.name = "HostError";
593
+ }
594
+ code;
595
+ };
596
+ var KNOWN_CODES = /* @__PURE__ */ new Set([
597
+ "unknown-source",
598
+ "not-found",
599
+ "empty-prompt",
600
+ "create-failed",
601
+ "prompt-failed",
602
+ "dispose-failed",
603
+ "internal"
604
+ ]);
605
+ function toHostError(error) {
606
+ if (error instanceof HostError) return error;
607
+ if (error instanceof DOMException && error.name === "AbortError") return new HostError("network", "aborted");
608
+ if (error instanceof TypeError) return new HostError("network", error.message);
609
+ return new HostError("unknown", error instanceof Error ? error.message : String(error));
610
+ }
611
+ function failureOf(payload, status) {
612
+ const value = payload !== null && typeof payload === "object" ? payload : {};
613
+ const code = typeof value.code === "string" && KNOWN_CODES.has(value.code) ? value.code : "unknown";
614
+ const message = typeof value.message === "string" ? value.message : status !== void 0 ? `HTTP ${status}` : "";
615
+ return new HostError(code, message);
616
+ }
617
+ function routeUrl(name2) {
618
+ return `${API_PREFIX}${name2}`;
619
+ }
620
+ async function call(name2, body, signal) {
621
+ let response;
622
+ try {
623
+ response = await fetch(routeUrl(name2), {
624
+ method: "POST",
625
+ headers: { "Content-Type": "application/json" },
626
+ body: JSON.stringify(body),
627
+ credentials: "same-origin",
628
+ ...signal === void 0 ? {} : { signal }
629
+ });
630
+ } catch (error) {
631
+ throw toHostError(error);
632
+ }
633
+ let payload = null;
634
+ try {
635
+ payload = await response.json();
636
+ } catch {
637
+ payload = null;
638
+ }
639
+ const ok = payload !== null && typeof payload === "object" && payload.ok === true;
640
+ if (!response.ok || !ok) throw failureOf(payload, response.ok ? void 0 : response.status);
641
+ return payload;
642
+ }
643
+
644
+ // src/client/runtime.ts
645
+ var POLL_RUNNING_MS = 400;
646
+ var POLL_IDLE_MS = 1500;
647
+ var HELLO_TIMEOUT_MS = 2500;
648
+ var RETRY_MS = [500, 1e3, 2e3, 4e3, 8e3];
649
+ var STREAM_FAILURES_BEFORE_POLL = 2;
650
+ function panelError(error) {
651
+ return { code: error.code, message: error.message };
652
+ }
653
+ var Panel = class {
654
+ constructor(key, sourceId, ordinal, doc) {
655
+ this.key = key;
656
+ this.sourceId = sourceId;
657
+ this.doc = doc;
658
+ this.state = {
659
+ key,
660
+ sourceId,
661
+ ordinal,
662
+ sessionId: null,
663
+ gone: false,
664
+ nodes: [],
665
+ live: [],
666
+ metrics: null,
667
+ contextWindow: null,
668
+ running: false,
669
+ sending: false,
670
+ draft: "",
671
+ pending: [],
672
+ error: null,
673
+ agentError: null,
674
+ focusRequest: 0
675
+ };
676
+ doc.addEventListener("visibilitychange", this.onVisibility);
677
+ }
678
+ key;
679
+ sourceId;
680
+ doc;
681
+ state;
682
+ entries = /* @__PURE__ */ new Map();
683
+ floor = 0;
684
+ liveState = null;
685
+ listeners = /* @__PURE__ */ new Set();
686
+ visible = false;
687
+ disposed = false;
688
+ mode = "stream";
689
+ stream = null;
690
+ timer = null;
691
+ streamFailures = 0;
692
+ retryIndex = 0;
693
+ catchingUp = false;
694
+ attachTried = false;
695
+ navigationRevision = 0;
696
+ onVisibility = () => {
697
+ if (this.doc.hidden) this.stop();
698
+ else this.connect();
699
+ };
700
+ subscribe = (listener) => {
701
+ this.listeners.add(listener);
702
+ return () => {
703
+ this.listeners.delete(listener);
704
+ };
705
+ };
706
+ getSnapshot = () => this.state;
707
+ update(patch) {
708
+ if (this.disposed) return;
709
+ this.state = { ...this.state, ...patch };
710
+ for (const listener of [...this.listeners]) {
711
+ try {
712
+ listener();
713
+ } catch {
714
+ }
715
+ }
716
+ }
717
+ get active() {
718
+ return !this.disposed && this.visible && !this.state.gone && this.state.sessionId !== null && !this.doc.hidden;
719
+ }
720
+ cursor() {
721
+ let next = this.floor;
722
+ for (const seq of this.entries.keys()) if (seq + 1 > next) next = seq + 1;
723
+ return next;
724
+ }
725
+ // ---- public actions
726
+ setVisible(visible) {
727
+ if (this.visible === visible) return;
728
+ this.visible = visible;
729
+ if (!visible) {
730
+ this.stop();
731
+ return;
732
+ }
733
+ if (this.state.sessionId === null) void this.attach();
734
+ else this.connect();
735
+ }
736
+ setDraft(draft) {
737
+ this.update({ draft });
738
+ }
739
+ dismissError() {
740
+ this.update({ error: null, agentError: null });
741
+ }
742
+ /**
743
+ * Answer one navigation of this tab once. The tab domain rebuilds
744
+ * occurrences without params after a page refresh, so a question handed
745
+ * over by `/side` or `/btw` is sent exactly once.
746
+ */
747
+ navigate(revision, params) {
748
+ if (this.disposed || revision <= this.navigationRevision) return;
749
+ this.navigationRevision = revision;
750
+ const prompt = params?.prompt?.trim() ?? "";
751
+ if (prompt !== "") void this.deliver(prompt, false);
752
+ else if (params?.focus === true) this.update({ focusRequest: this.state.focusRequest + 1 });
753
+ }
754
+ async send() {
755
+ await this.deliver(this.state.draft.trim(), true);
756
+ }
757
+ async deliver(text, fromDraft) {
758
+ if (text === "" || this.state.sending || this.state.gone || this.disposed) return;
759
+ const seen = /* @__PURE__ */ new Set();
760
+ for (const node of this.state.nodes) if (node.kind === "user") seen.add(node.seq);
761
+ const pending = { id: `p${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`, text, time: Date.now(), seen };
762
+ this.update({ sending: true, error: null, draft: fromDraft ? "" : this.state.draft, pending: [...this.state.pending, pending] });
763
+ try {
764
+ let sessionId = this.state.sessionId;
765
+ if (sessionId === null) {
766
+ const created = await call("create", { sourceId: this.sourceId, instanceKey: this.key });
767
+ if (this.disposed) {
768
+ void call("close", { sessionId: created.sessionId }).catch(() => void 0);
769
+ return;
770
+ }
771
+ sessionId = created.sessionId;
772
+ if (this.state.sessionId !== sessionId) this.adopt(sessionId, created.inheritedEventCount);
773
+ }
774
+ await call("prompt", { sessionId, text });
775
+ this.update({ sending: false });
776
+ if (this.mode === "poll") this.schedule(0);
777
+ } catch (error) {
778
+ if (this.disposed) return;
779
+ const failure = toHostError(error);
780
+ this.update({
781
+ sending: false,
782
+ pending: this.state.pending.filter((item) => item.id !== pending.id),
783
+ draft: this.state.draft === "" ? text : this.state.draft,
784
+ error: panelError(failure)
785
+ });
786
+ if (failure.code === "not-found") this.lost(failure);
787
+ }
788
+ }
789
+ async cancel() {
790
+ const sessionId = this.state.sessionId;
791
+ if (sessionId === null || this.state.gone) return;
792
+ try {
793
+ await call("cancel", { sessionId });
794
+ } catch (error) {
795
+ this.update({ error: panelError(toHostError(error)) });
796
+ }
797
+ }
798
+ /** Tab closed: stop everything and destroy the host side session. */
799
+ async close() {
800
+ const sessionId = this.state.sessionId;
801
+ this.dispose();
802
+ if (sessionId === null) return;
803
+ try {
804
+ await call("close", { sessionId });
805
+ } catch (error) {
806
+ console.warn("[dsh-side-chat] close failed:", toHostError(error).message);
807
+ }
808
+ }
809
+ dispose() {
810
+ if (this.disposed) return;
811
+ this.stop();
812
+ this.doc.removeEventListener("visibilitychange", this.onVisibility);
813
+ this.disposed = true;
814
+ this.listeners.clear();
815
+ }
816
+ // ---- session lifecycle
817
+ /** After a page refresh the host may still own this tab's side session. */
818
+ async attach() {
819
+ if (this.attachTried) return;
820
+ this.attachTried = true;
821
+ try {
822
+ const found = await call("create", { sourceId: this.sourceId, instanceKey: this.key, attachOnly: true });
823
+ if (this.disposed || this.state.sessionId !== null) return;
824
+ this.adopt(found.sessionId, found.inheritedEventCount);
825
+ } catch {
826
+ }
827
+ }
828
+ adopt(sessionId, inheritedEventCount) {
829
+ this.floor = inheritedEventCount;
830
+ this.update({ sessionId });
831
+ this.connect();
832
+ }
833
+ lost(error) {
834
+ this.stop();
835
+ this.liveState = null;
836
+ this.update({ gone: true, running: false, live: [], error: panelError(error) });
837
+ }
838
+ // ---- state folding
839
+ merge(entries) {
840
+ let added = false;
841
+ let settled = false;
842
+ for (const entry of entries) {
843
+ if (typeof entry?.seq !== "number" || this.entries.has(entry.seq)) continue;
844
+ this.entries.set(entry.seq, entry);
845
+ added = true;
846
+ if (settles(this.liveState, entry)) settled = true;
847
+ }
848
+ if (!added) return false;
849
+ if (settled) this.liveState = null;
850
+ const ordered = [...this.entries.values()].sort((a, b) => a.seq - b.seq);
851
+ const nodes = foldEntries(ordered, this.floor);
852
+ this.state = {
853
+ ...this.state,
854
+ nodes,
855
+ metrics: metricsOf(ordered, this.floor),
856
+ pending: this.reconcile(nodes),
857
+ live: liveBlocks(this.liveState)
858
+ };
859
+ return true;
860
+ }
861
+ /** Drop pending rows whose echo arrived as a newer user node with the same text. */
862
+ reconcile(nodes) {
863
+ if (this.state.pending.length === 0) return this.state.pending;
864
+ const used = /* @__PURE__ */ new Set();
865
+ const remaining = this.state.pending.filter((pending) => {
866
+ const match = nodes.find((node) => node.kind === "user" && node.text === pending.text && !used.has(node.seq) && !pending.seen.has(node.seq));
867
+ if (match === void 0) return true;
868
+ used.add(match.seq);
869
+ return false;
870
+ });
871
+ return remaining.length === this.state.pending.length ? this.state.pending : remaining;
872
+ }
873
+ applyBaseline(payload) {
874
+ if (typeof payload.inheritedEventCount === "number") this.floor = payload.inheritedEventCount;
875
+ this.merge(payload.entries ?? []);
876
+ if (payload.live !== void 0) this.liveState = payload.live;
877
+ this.update({
878
+ live: liveBlocks(this.liveState),
879
+ running: payload.running === true || payload.status === "running",
880
+ agentError: payload.lastError ?? null,
881
+ ...typeof payload.contextWindow === "number" && payload.contextWindow > 0 ? { contextWindow: payload.contextWindow } : {},
882
+ ...this.state.error?.code === "network" ? { error: null } : {}
883
+ });
884
+ }
885
+ onFrame(frame) {
886
+ const data = record(frame.data);
887
+ switch (frame.event) {
888
+ case "hello":
889
+ this.applyBaseline(data);
890
+ return;
891
+ case "event": {
892
+ const entry = data.entry;
893
+ if (entry !== void 0 && this.merge([entry])) this.update({});
894
+ return;
895
+ }
896
+ case "live": {
897
+ const hadAttempt = Boolean(this.liveState?.activeAttempt);
898
+ this.liveState = data.live ?? null;
899
+ this.update({ live: liveBlocks(this.liveState) });
900
+ if (hadAttempt && !this.liveState?.activeAttempt) this.catchUp();
901
+ return;
902
+ }
903
+ case "status":
904
+ this.update({ running: data.running === true || data.status === "running" });
905
+ return;
906
+ case "error":
907
+ this.update({ agentError: typeof data.message === "string" ? data.message : null });
908
+ return;
909
+ default:
910
+ return;
911
+ }
912
+ }
913
+ // ---- transport
914
+ stop() {
915
+ if (this.timer !== null) {
916
+ clearTimeout(this.timer);
917
+ this.timer = null;
918
+ }
919
+ const stream = this.stream;
920
+ if (stream !== null) {
921
+ stream.stopped = true;
922
+ this.stream = null;
923
+ stream.abort.abort();
924
+ }
925
+ }
926
+ connect() {
927
+ if (this.stream !== null || this.timer !== null) return;
928
+ if (!this.active) return;
929
+ if (this.mode === "stream") void this.openStream();
930
+ else void this.poll();
931
+ }
932
+ schedule(delay) {
933
+ if (this.timer !== null) clearTimeout(this.timer);
934
+ this.timer = null;
935
+ if (!this.active || this.stream !== null) return;
936
+ this.timer = setTimeout(() => {
937
+ this.timer = null;
938
+ if (this.mode === "stream") void this.openStream();
939
+ else void this.poll();
940
+ }, Math.max(0, delay));
941
+ }
942
+ /** One snapshot pull. Returns false when the panel should stop updating. */
943
+ async fetchSnapshot() {
944
+ const sessionId = this.state.sessionId;
945
+ if (sessionId === null || this.disposed) return false;
946
+ try {
947
+ const payload = await call("snapshot", { sessionId, fromSeq: this.cursor() });
948
+ if (this.disposed || this.state.sessionId !== sessionId) return false;
949
+ this.applyBaseline(payload);
950
+ return true;
951
+ } catch (error) {
952
+ if (this.disposed) return false;
953
+ const failure = toHostError(error);
954
+ if (failure.code === "not-found") {
955
+ this.lost(failure);
956
+ return false;
957
+ }
958
+ this.update({ error: panelError(failure) });
959
+ return true;
960
+ }
961
+ }
962
+ async poll() {
963
+ if (!this.active || this.stream !== null) return;
964
+ const alive = await this.fetchSnapshot();
965
+ if (alive && this.mode === "poll") this.schedule(this.state.running ? POLL_RUNNING_MS : POLL_IDLE_MS);
966
+ }
967
+ catchUp() {
968
+ if (this.catchingUp) return;
969
+ this.catchingUp = true;
970
+ void this.fetchSnapshot().finally(() => {
971
+ this.catchingUp = false;
972
+ });
973
+ }
974
+ degrade() {
975
+ this.mode = "poll";
976
+ this.schedule(0);
977
+ }
978
+ async openStream() {
979
+ const sessionId = this.state.sessionId;
980
+ if (!this.active || this.stream !== null || sessionId === null) return;
981
+ const stream = { abort: new AbortController(), hello: false, stopped: false, timedOut: false };
982
+ this.stream = stream;
983
+ const helloTimer = setTimeout(() => {
984
+ if (stream.hello) return;
985
+ stream.timedOut = true;
986
+ stream.abort.abort();
987
+ }, HELLO_TIMEOUT_MS);
988
+ try {
989
+ const response = await fetch(routeUrl(EVENTS_ROUTE), {
990
+ method: "POST",
991
+ headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
992
+ body: JSON.stringify({ sessionId, fromSeq: this.cursor() }),
993
+ credentials: "same-origin",
994
+ signal: stream.abort.signal
995
+ });
996
+ const type = response.headers.get("content-type") ?? "";
997
+ if (!type.includes("text/event-stream") || response.body === null) {
998
+ const payload = await response.json().catch(() => null);
999
+ const failure = failureOf(payload, response.status);
1000
+ if (stream.stopped) return;
1001
+ this.stream = null;
1002
+ if (failure.code === "not-found") this.lost(failure);
1003
+ else this.degrade();
1004
+ return;
1005
+ }
1006
+ const reader = response.body.getReader();
1007
+ const decoder = new TextDecoder();
1008
+ const parser = createSseParser();
1009
+ for (; ; ) {
1010
+ const { done, value } = await reader.read();
1011
+ if (done || stream.stopped) break;
1012
+ for (const frame of parser.push(decoder.decode(value, { stream: true }))) {
1013
+ if (frame.event === "hello") {
1014
+ stream.hello = true;
1015
+ clearTimeout(helloTimer);
1016
+ this.streamFailures = 0;
1017
+ this.retryIndex = 0;
1018
+ }
1019
+ this.onFrame(frame);
1020
+ if (stream.stopped) break;
1021
+ }
1022
+ }
1023
+ } catch {
1024
+ } finally {
1025
+ clearTimeout(helloTimer);
1026
+ }
1027
+ if (stream.stopped) return;
1028
+ if (this.stream === stream) this.stream = null;
1029
+ if (this.disposed || this.state.gone) return;
1030
+ if (stream.timedOut) {
1031
+ this.degrade();
1032
+ return;
1033
+ }
1034
+ if (stream.hello) {
1035
+ this.catchUp();
1036
+ } else if (++this.streamFailures >= STREAM_FAILURES_BEFORE_POLL) {
1037
+ this.degrade();
1038
+ return;
1039
+ }
1040
+ const delay = RETRY_MS[Math.min(this.retryIndex, RETRY_MS.length - 1)] ?? 1e3;
1041
+ this.retryIndex += 1;
1042
+ this.schedule(delay);
1043
+ }
1044
+ };
1045
+ var Registry = class {
1046
+ constructor(doc) {
1047
+ this.doc = doc;
1048
+ }
1049
+ doc;
1050
+ panels = /* @__PURE__ */ new Map();
1051
+ ordinals = /* @__PURE__ */ new Map();
1052
+ listeners = /* @__PURE__ */ new Set();
1053
+ version = 0;
1054
+ subscribe = (listener) => {
1055
+ this.listeners.add(listener);
1056
+ return () => {
1057
+ this.listeners.delete(listener);
1058
+ };
1059
+ };
1060
+ getVersion = () => this.version;
1061
+ emit() {
1062
+ this.version += 1;
1063
+ for (const listener of [...this.listeners]) {
1064
+ try {
1065
+ listener();
1066
+ } catch {
1067
+ }
1068
+ }
1069
+ }
1070
+ /** Get or create the panel of one tab occurrence. */
1071
+ open(key, sourceId) {
1072
+ const existing = this.panels.get(key);
1073
+ if (existing !== void 0) return existing;
1074
+ const ordinal = (this.ordinals.get(sourceId) ?? 0) + 1;
1075
+ this.ordinals.set(sourceId, ordinal);
1076
+ const panel = new Panel(key, sourceId, ordinal, this.doc);
1077
+ this.panels.set(key, panel);
1078
+ queueMicrotask(() => this.emit());
1079
+ return panel;
1080
+ }
1081
+ get(key) {
1082
+ return this.panels.get(key);
1083
+ }
1084
+ /**
1085
+ * A tab closed. A panel that never mounted in this page (restored tab after
1086
+ * a refresh) still owns a host side session under its key: resolve it
1087
+ * without creating one, then destroy it.
1088
+ */
1089
+ async close(key, sourceId) {
1090
+ const panel = this.panels.get(key);
1091
+ if (panel === void 0) {
1092
+ try {
1093
+ const found = await call("create", { sourceId, instanceKey: key, attachOnly: true });
1094
+ await call("close", { sessionId: found.sessionId });
1095
+ } catch {
1096
+ }
1097
+ return;
1098
+ }
1099
+ this.panels.delete(key);
1100
+ if (![...this.panels.values()].some((other) => other.sourceId === panel.sourceId)) this.ordinals.delete(panel.sourceId);
1101
+ this.emit();
1102
+ await panel.close();
1103
+ }
1104
+ dispose() {
1105
+ for (const panel of this.panels.values()) panel.dispose();
1106
+ this.panels.clear();
1107
+ this.ordinals.clear();
1108
+ this.emit();
1109
+ this.listeners.clear();
1110
+ }
1111
+ };
1112
+ function panelKey(sessionId, tabId) {
1113
+ return tabId === void 0 || tabId === "" ? sessionId : `${sessionId}#${tabId}`;
1114
+ }
1115
+
1116
+ // src/client/styles.ts
1117
+ var STYLE_ID = "dsh-side-chat";
1118
+ var CSS = `
1119
+ [data-side-chat] {
1120
+ --dsc-gutter: 16px;
1121
+ --dsc-indent: calc(22px + var(--dsh-content-font-delta, 0px));
1122
+ --dsc-secondary-size: var(--dsh-content-font-size-secondary, 13px);
1123
+ --dsc-secondary-line: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
1124
+ display: flex;
1125
+ flex-direction: column;
1126
+ width: 100%;
1127
+ height: 100%;
1128
+ min-height: 0;
1129
+ color: var(--dsw-alias-label-primary);
1130
+ font-family: var(--dsw-font-family);
1131
+ font-size: var(--dsh-content-font-size, 14px);
1132
+ line-height: calc(24px + var(--dsh-content-font-delta, 0px));
1133
+ background: transparent;
1134
+ }
1135
+
1136
+ /* Header: run state, queue and the new-tab action. Not sticky, nothing scrolls under it. */
1137
+ .dsc-header {
1138
+ flex: none;
1139
+ display: flex;
1140
+ align-items: center;
1141
+ gap: 8px;
1142
+ min-height: 40px;
1143
+ padding: 4px 8px 4px var(--dsc-gutter);
1144
+ border-bottom: .5px solid var(--dsw-alias-border-l2);
1145
+ color: var(--dsw-alias-label-tertiary);
1146
+ font-size: var(--dsc-secondary-size);
1147
+ line-height: var(--dsc-secondary-line);
1148
+ }
1149
+ .dsc-status { display: inline-flex; align-items: center; gap: 8px; min-width: 0; white-space: nowrap; }
1150
+ .dsc-queued { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-caption); }
1151
+ .dsc-header-actions { display: flex; align-items: center; gap: 4px; margin-left: auto; }
1152
+
1153
+ .dsc-scroll {
1154
+ flex: 1;
1155
+ display: flex;
1156
+ flex-direction: column;
1157
+ min-height: 0;
1158
+ overflow-x: hidden;
1159
+ overflow-y: auto;
1160
+ overscroll-behavior: contain;
1161
+ }
1162
+ .dsc-transcript {
1163
+ display: flex;
1164
+ flex-direction: column;
1165
+ gap: 20px;
1166
+ min-width: 0;
1167
+ padding: 16px var(--dsc-gutter) 20px;
1168
+ }
1169
+
1170
+ /* Empty state */
1171
+ .dsc-empty {
1172
+ margin: auto;
1173
+ max-width: 320px;
1174
+ padding: 40px var(--dsc-gutter);
1175
+ display: flex;
1176
+ flex-direction: column;
1177
+ align-items: center;
1178
+ gap: 8px;
1179
+ text-align: center;
1180
+ color: var(--dsw-alias-label-secondary);
1181
+ font-size: var(--dsc-secondary-size);
1182
+ line-height: var(--dsc-secondary-line);
1183
+ }
1184
+ .dsc-empty-art { color: var(--dsw-alias-label-tertiary); margin-bottom: 4px; }
1185
+ .dsc-empty-title { color: var(--dsw-alias-label-primary); font: var(--dsw-font-markdown-h4, 600 14px / 24px var(--dsw-font-family)); }
1186
+ .dsc-empty-hint { color: var(--dsw-alias-label-caption); font-size: 12px; line-height: 18px; }
1187
+
1188
+ /* User turns mirror the main chat bubble. */
1189
+ .dsc-user { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; min-width: 0; }
1190
+ .dsc-bubble {
1191
+ max-width: min(88%, 560px);
1192
+ padding: 10px 16px;
1193
+ border-radius: var(--dsw-radius-xl);
1194
+ background: var(--dsw-specific-bubble);
1195
+ color: var(--dsw-alias-label-primary);
1196
+ line-height: calc(22px + var(--dsh-content-font-delta, 0px));
1197
+ white-space: pre-wrap;
1198
+ overflow-wrap: anywhere;
1199
+ user-select: text;
1200
+ }
1201
+ .dsc-bubble[data-pending] { opacity: .64; }
1202
+ .dsc-meta { padding: 0 4px; color: var(--dsw-alias-label-caption); font-size: 12px; line-height: 18px; font-variant-numeric: tabular-nums; }
1203
+
1204
+ /* Assistant turns: process rows (reasoning, tools) and Markdown text. */
1205
+ .dsc-assistant { display: flex; flex-direction: column; gap: 12px; min-width: 0; overflow-wrap: anywhere; }
1206
+ .dsc-process { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
1207
+ .dsc-row-summary {
1208
+ min-width: 0;
1209
+ overflow: hidden;
1210
+ text-overflow: ellipsis;
1211
+ white-space: nowrap;
1212
+ color: var(--dsw-alias-label-tertiary);
1213
+ font-size: var(--dsc-secondary-size);
1214
+ }
1215
+ .dsc-row-collapsed { display: flex; align-items: center; gap: 8px; min-width: 0; }
1216
+ /* Title-to-summary separator, as in the main transcript's process rows. */
1217
+ .dsc-sep { flex: none; width: 2px; height: 2px; margin-left: 8px; border-radius: 1px; background: var(--dsw-alias-label-caption); }
1218
+ .dsc-detail { display: flex; flex-direction: column; gap: 8px; min-width: 0; margin: 4px 0 4px var(--dsc-indent); }
1219
+ .dsc-reasoning { color: var(--dsw-alias-label-tertiary); }
1220
+ .dsc-pre {
1221
+ margin: 0;
1222
+ max-height: 240px;
1223
+ overflow: auto;
1224
+ padding: 10px 12px;
1225
+ border-radius: var(--dsw-radius-md);
1226
+ background: var(--dsw-alias-markdown-code-block);
1227
+ color: var(--dsw-alias-label-secondary);
1228
+ font: 400 calc(11px + var(--dsh-code-font-delta, 0px)) / 1.6 var(--ds-font-family-code);
1229
+ white-space: pre-wrap;
1230
+ overflow-wrap: anywhere;
1231
+ tab-size: 2;
1232
+ user-select: text;
1233
+ }
1234
+ .dsc-pre[data-error] { color: var(--dsw-alias-state-error-primary); }
1235
+ .dsc-caption { color: var(--dsw-alias-label-caption); font-size: 12px; line-height: 18px; }
1236
+ .dsc-markdown { min-width: 0; }
1237
+ .dsc-context { display: flex; align-items: center; gap: 6px; min-width: 0; color: var(--dsw-alias-label-tertiary); font-size: var(--dsc-secondary-size); line-height: var(--dsc-secondary-line); }
1238
+ .dsc-context > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1239
+ .dsc-thinking { align-self: flex-start; color: var(--dsw-alias-label-tertiary); font-size: var(--dsc-secondary-size); line-height: var(--dsc-secondary-line); }
1240
+
1241
+ .dsc-notice {
1242
+ display: flex;
1243
+ align-items: flex-start;
1244
+ gap: 8px;
1245
+ padding: 8px 8px 8px 12px;
1246
+ border-radius: var(--dsw-radius-md);
1247
+ background: var(--dsw-alias-interactive-bg-hover-danger, var(--dsw-alias-interactive-bg-hover));
1248
+ color: var(--dsw-alias-state-error-primary);
1249
+ font-size: var(--dsc-secondary-size);
1250
+ line-height: var(--dsc-secondary-line);
1251
+ }
1252
+ .dsc-notice-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
1253
+ .dsc-notice-detail { color: var(--dsw-alias-label-secondary); font: 400 12px / 18px var(--ds-font-family-code); white-space: pre-wrap; overflow-wrap: anywhere; }
1254
+
1255
+ /* Composer: same token recipe as the main input bar card. */
1256
+ .dsc-footer { flex: none; display: flex; flex-direction: column; padding: 0 12px 8px; }
1257
+ .dsc-card {
1258
+ position: relative;
1259
+ display: flex;
1260
+ flex-direction: column;
1261
+ gap: 4px;
1262
+ padding-top: 8px;
1263
+ border: 0;
1264
+ border-radius: var(--dsw-radius-panel);
1265
+ background: var(--dsw-specific-input-major);
1266
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l2);
1267
+ box-shadow: var(--dsw-elevation-soft);
1268
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
1269
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
1270
+ }
1271
+ .dsc-input {
1272
+ display: block;
1273
+ box-sizing: border-box;
1274
+ width: 100%;
1275
+ min-height: 36px;
1276
+ max-height: 40vh;
1277
+ margin: 0;
1278
+ padding: 4px 14px 0;
1279
+ border: 0;
1280
+ outline: none;
1281
+ resize: none;
1282
+ overflow-y: auto;
1283
+ background: transparent;
1284
+ color: var(--dsw-alias-label-primary);
1285
+ caret-color: var(--dsw-alias-state-business-primary);
1286
+ font: inherit;
1287
+ font-family: var(--dsw-font-family);
1288
+ }
1289
+ .dsc-input::placeholder { color: var(--dsw-alias-label-caption); }
1290
+ .dsc-input:disabled { color: var(--dsw-alias-label-tertiary); cursor: not-allowed; }
1291
+ .dsc-composer-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; padding: 2px 8px 8px 14px; }
1292
+ .dsc-hint { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-caption); font-size: 12px; line-height: 18px; }
1293
+ .dsc-primary {
1294
+ flex: none;
1295
+ display: grid;
1296
+ place-items: center;
1297
+ width: 32px;
1298
+ height: 32px;
1299
+ padding: 0;
1300
+ border: 0;
1301
+ border-radius: 999px;
1302
+ corner-shape: round;
1303
+ background: var(--dsw-alias-button-info-fill);
1304
+ color: var(--dsw-static-neutral-00);
1305
+ cursor: pointer;
1306
+ transition: background-color var(--ds-transition-duration-fast, .1s) var(--ds-ease-in-out, ease);
1307
+ }
1308
+ .dsc-primary:hover:not(:disabled) { background: var(--dsw-alias-button-info-hover); }
1309
+ .dsc-primary:disabled { opacity: .4; cursor: default; }
1310
+
1311
+ /* Usage dock under the composer, styled like the main chat's stat pills. */
1312
+ .dsc-dock {
1313
+ display: flex;
1314
+ flex-wrap: wrap;
1315
+ align-items: center;
1316
+ justify-content: center;
1317
+ gap: 2px 4px;
1318
+ padding-top: 4px;
1319
+ color: var(--dsw-alias-label-tertiary);
1320
+ font-size: calc(var(--dsc-secondary-size) - 1px);
1321
+ line-height: var(--dsc-secondary-line);
1322
+ font-variant-numeric: tabular-nums;
1323
+ }
1324
+ .dsc-dock-item { display: inline-flex; align-items: center; gap: 6px; padding: 1px 8px; border-radius: var(--dsw-radius-sm); white-space: nowrap; }
1325
+ .dsc-dock-item:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-secondary); }
1326
+ .dsc-ring-track { fill: none; stroke: var(--dsw-alias-border-l3); stroke-width: 2; }
1327
+ .dsc-ring-fill { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
1328
+
1329
+ .dsc-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
1330
+
1331
+ @media (prefers-reduced-motion: reduce) {
1332
+ .dsc-primary { transition: none; }
1333
+ }
1334
+ @media (forced-colors: active) {
1335
+ .dsc-card { border: 1px solid CanvasText; }
1336
+ .dsc-primary { border: 1px solid ButtonText; }
1337
+ }
1338
+ `;
1339
+ function installStyle(document2) {
1340
+ const node = document2.createElement("style");
1341
+ node.dataset.plugin = STYLE_ID;
1342
+ node.textContent = CSS;
1343
+ document2.head.append(node);
1344
+ return () => node.remove();
1345
+ }
1346
+
1347
+ // src/client/view.tsx
1348
+ var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
1349
+ var import_react2 = require("react");
1350
+
1351
+ // src/client/process.tsx
1352
+ var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
1353
+ var import_react = require("react");
1354
+ var import_jsx_runtime = require("react/jsx-runtime");
1355
+ var VARIANT_ICONS = {
1356
+ bash: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives2.IconApiOutlineRegular, { size: 14 }),
1357
+ read: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives2.IconBrowseOutlineRegular, { size: 14 }),
1358
+ edit: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives2.IconEditOutlineRegular, { size: 14 }),
1359
+ search: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives2.IconSearchOutlineRegular, { size: 14 }),
1360
+ web: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives2.IconGlobeOutlineRegular, { size: 14 }),
1361
+ others: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives2.IconSparkleRegular, { size: 14 })
1362
+ };
1363
+ function variantOf(name2) {
1364
+ const tool = name2.toLowerCase();
1365
+ if (/^(bash|pwsh|shell|terminal)/.test(tool)) return "bash";
1366
+ if (tool === "read" || tool === "read_image") return "read";
1367
+ if (tool === "write" || tool === "edit" || tool.includes("replace")) return "edit";
1368
+ if (tool === "grep" || tool === "glob" || tool.includes("search")) return "search";
1369
+ if (tool.startsWith("web")) return "web";
1370
+ return "others";
1371
+ }
1372
+ var SUMMARY_FIELDS = ["command", "file_path", "path", "pattern", "query", "url", "description", "prompt", "objective"];
1373
+ var TITLED_TOOLS = /* @__PURE__ */ new Set(["bash", "read", "write", "edit", "glob", "grep", "web_fetch", "web_search"]);
1374
+ function parseArgs(raw) {
1375
+ if (raw.trim() === "") return null;
1376
+ try {
1377
+ const value = JSON.parse(raw);
1378
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
1379
+ } catch {
1380
+ return null;
1381
+ }
1382
+ }
1383
+ function firstLine(text, max = 160) {
1384
+ const line = text.trim().split("\n", 1)[0] ?? "";
1385
+ return line.length > max ? `${line.slice(0, max - 1)}\u2026` : line;
1386
+ }
1387
+ function useToggle() {
1388
+ const [open, setOpen] = (0, import_react.useState)(false);
1389
+ const toggle = (0, import_react.useCallback)(() => setOpen((value) => !value), []);
1390
+ return [open, toggle];
1391
+ }
1392
+ var ReasoningRow = (0, import_react.memo)(function ReasoningRow2(props) {
1393
+ const { text, running, labels, t } = props;
1394
+ const [open, toggle] = useToggle();
1395
+ const preview = (0, import_react.useMemo)(() => firstLine(text.replace(/[*_#>`]/g, "")), [text]);
1396
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1397
+ import_dsh_client_ui_primitives2.DisclosureRow,
1398
+ {
1399
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives2.IconThinkOutlineRegular, { size: 14 }),
1400
+ title: running ? t("reasoning.running") : t("reasoning.done"),
1401
+ open,
1402
+ expandable: true,
1403
+ onToggle: toggle,
1404
+ running,
1405
+ expandOnRowClick: true,
1406
+ previewChevron: true,
1407
+ collapsedContent: preview === "" ? void 0 : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsc-row-collapsed", children: [
1408
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsc-sep", "aria-hidden": "true" }),
1409
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsc-row-summary", children: preview })
1410
+ ] }),
1411
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsc-detail dsc-reasoning", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives2.MarkdownText, { text, streaming: running, labels, variant: "compact" }) })
1412
+ }
1413
+ );
1414
+ });
1415
+ var ToolRow = (0, import_react.memo)(function ToolRow2(props) {
1416
+ const { block, running, labels, t } = props;
1417
+ const [open, toggle] = useToggle();
1418
+ const variant = variantOf(block.name);
1419
+ const args = (0, import_react.useMemo)(() => parseArgs(block.argsRaw), [block.argsRaw]);
1420
+ const summary = (0, import_react.useMemo)(() => {
1421
+ for (const field of SUMMARY_FIELDS) {
1422
+ const value = args?.[field];
1423
+ if (typeof value === "string" && value.trim() !== "") return firstLine(value);
1424
+ }
1425
+ return "";
1426
+ }, [args]);
1427
+ const failed = block.result?.isError === true;
1428
+ const title = block.name === "" ? t("tool.fallback") : TITLED_TOOLS.has(block.name) ? t(`tool.${block.name}`) : block.name;
1429
+ const command = typeof args?.command === "string" ? args.command : null;
1430
+ const path = typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
1431
+ const diffs = (0, import_react.useMemo)(() => {
1432
+ if (variant !== "edit" || args === null || path === "") return [];
1433
+ if (typeof args.old_string === "string" && typeof args.new_string === "string") {
1434
+ return [{ path, oldText: args.old_string, newText: args.new_string }];
1435
+ }
1436
+ if (typeof args.content === "string") return [{ path, oldText: null, newText: args.content }];
1437
+ return [];
1438
+ }, [args, path, variant]);
1439
+ const result = block.result === null ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className: "dsc-pre", "data-error": failed ? "" : void 0, children: block.result.text === "" ? t("tool.emptyOutput") : block.result.text });
1440
+ let body;
1441
+ if (variant === "bash" && command !== null) {
1442
+ body = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1443
+ import_dsh_client_ui_primitives2.TerminalBlock,
1444
+ {
1445
+ command,
1446
+ output: block.result?.text,
1447
+ running,
1448
+ maxLines: 16,
1449
+ labels: labels.terminal
1450
+ }
1451
+ );
1452
+ } else if (diffs.length > 0) {
1453
+ body = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1454
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives2.DiffBlock, { diffs, labels: labels.diff, maxLines: 16 }),
1455
+ failed ? result : null
1456
+ ] });
1457
+ } else {
1458
+ const pretty = args === null ? block.argsRaw : JSON.stringify(args, null, 2);
1459
+ body = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1460
+ pretty.trim() === "" || pretty === "{}" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1461
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsc-caption", children: t("tool.arguments") }),
1462
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className: "dsc-pre", children: pretty })
1463
+ ] }),
1464
+ result === null ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1465
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsc-caption", children: t("tool.result") }),
1466
+ result
1467
+ ] })
1468
+ ] });
1469
+ }
1470
+ const collapsed = failed || summary !== "" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsc-row-collapsed", children: [
1471
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsc-sep", "aria-hidden": "true" }),
1472
+ failed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives2.Tag, { tone: "danger", children: t("tool.failed") }) : null,
1473
+ summary === "" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsc-row-summary", children: summary })
1474
+ ] }) : void 0;
1475
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1476
+ import_dsh_client_ui_primitives2.DisclosureRow,
1477
+ {
1478
+ icon: VARIANT_ICONS[variant],
1479
+ title,
1480
+ open,
1481
+ expandable: true,
1482
+ onToggle: toggle,
1483
+ running,
1484
+ expandOnRowClick: true,
1485
+ previewChevron: true,
1486
+ keepContentWhenOpen: true,
1487
+ collapsedContent: collapsed,
1488
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsc-detail", children: body })
1489
+ }
1490
+ );
1491
+ });
1492
+
1493
+ // src/client/view.tsx
1494
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1495
+ var STICK_THRESHOLD_PX = 48;
1496
+ var CONTEXT_LABELS = {
1497
+ steering: "context.steering",
1498
+ sessionReference: "context.sessionReference",
1499
+ agentInstructions: "context.agentInstructions",
1500
+ compactCheckpoint: "context.compactCheckpoint",
1501
+ generic: "context.generic"
1502
+ };
1503
+ function clock(time) {
1504
+ if (time === null) return "";
1505
+ const date = new Date(time);
1506
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false });
1507
+ }
1508
+ function openExternal(href) {
1509
+ window.open(href, "_blank", "noopener,noreferrer");
1510
+ }
1511
+ function PanelView({ panel, t, labels, onNew, openFile }) {
1512
+ const state = (0, import_react2.useSyncExternalStore)(panel.subscribe, panel.getSnapshot);
1513
+ const scrollRef = (0, import_react2.useRef)(null);
1514
+ const stickRef = (0, import_react2.useRef)(true);
1515
+ const onScroll = (0, import_react2.useCallback)(() => {
1516
+ const node = scrollRef.current;
1517
+ if (node !== null) stickRef.current = node.scrollHeight - node.scrollTop - node.clientHeight < STICK_THRESHOLD_PX;
1518
+ }, []);
1519
+ (0, import_react2.useLayoutEffect)(() => {
1520
+ const node = scrollRef.current;
1521
+ if (node !== null && stickRef.current) node.scrollTop = node.scrollHeight;
1522
+ }, [state.nodes, state.live, state.pending, state.running, state.sending, state.error, state.agentError]);
1523
+ const busy = state.running || state.sending;
1524
+ const empty = state.nodes.length === 0 && state.pending.length === 0 && state.live.length === 0;
1525
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "data-side-chat": "", className: "dsc-root", "aria-label": t("tab.title"), children: [
1526
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Header, { state, t, onNew }),
1527
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: scrollRef, className: "dsc-scroll", onScroll, children: empty && state.error === null && state.agentError === null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EmptyState, { created: state.sessionId !== null, t }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.MarkdownDelegateProvider, { openExternalLink: openExternal, openFile, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsc-transcript", role: "log", "aria-live": "polite", "aria-busy": busy, children: [
1528
+ state.nodes.map((node) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(NodeView, { node, state, t, labels }, `${node.kind}-${node.seq}`)),
1529
+ state.live.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AssistantView, { blocks: state.live, streaming: true, running: state.running, t, labels }) : null,
1530
+ state.pending.map((pending) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsc-user", children: [
1531
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsc-bubble", "data-pending": "", children: pending.text }),
1532
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsc-meta", children: busy && !state.sending ? t("pending.queued") : t("pending.sending") })
1533
+ ] }, pending.id)),
1534
+ busy && state.live.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.TextShimmer, { active: true, className: "dsc-thinking", children: t("thinking") }) : null,
1535
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Notice, { error: state.error, agentError: state.agentError, t, onDismiss: () => panel.dismissError() })
1536
+ ] }) }) }),
1537
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("footer", { className: "dsc-footer", children: [
1538
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Composer, { panel, state, t }),
1539
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(UsageDock, { metrics: state.metrics, contextWindow: state.contextWindow, t })
1540
+ ] })
1541
+ ] });
1542
+ }
1543
+ function Header({ state, t, onNew }) {
1544
+ const status = state.sessionId === null ? t("status.blank") : state.running ? t("status.running") : state.sending ? t("status.sending") : t("status.idle");
1545
+ const dot = state.gone || state.agentError !== null ? "error" : state.running || state.sending ? "ongoing" : "idle";
1546
+ const queued = state.running ? state.pending.length : 0;
1547
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { className: "dsc-header", children: [
1548
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dsc-status", role: "status", children: [
1549
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.StateDot, { state: dot }),
1550
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: status })
1551
+ ] }),
1552
+ queued > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsc-queued", children: t("status.queued", { count: queued }) }) : null,
1553
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsc-header-actions", children: onNew === null ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.Tooltip, { label: t("tab.new"), side: "bottom", align: "end", delayMs: 500, portal: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.Button, { variant: "ghost", size: "sm", icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.IconNewChatOutlineRegular, { size: 16 }), "aria-label": t("tab.new"), onClick: onNew }) }) })
1554
+ ] });
1555
+ }
1556
+ function EmptyState({ created, t }) {
1557
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsc-empty", children: [
1558
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.PluginArtworkSubagent, { size: 40, className: "dsc-empty-art" }),
1559
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsc-empty-title", children: t("empty.title") }),
1560
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: created ? t("empty.created") : t("empty.blank") })
1561
+ ] });
1562
+ }
1563
+ var NodeView = (0, import_react2.memo)(function NodeView2({ node, state, t, labels }) {
1564
+ if (node.kind === "user") {
1565
+ const time = clock(node.time);
1566
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsc-user", children: [
1567
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsc-bubble", children: node.text }),
1568
+ time === "" ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsc-meta", children: time })
1569
+ ] });
1570
+ }
1571
+ if (node.kind === "context") {
1572
+ const label = node.tag === "skill" ? node.name === "" ? t("context.skill") : t("context.skillNamed", { name: node.name }) : t(CONTEXT_LABELS[node.tag]);
1573
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsc-context", title: node.text.slice(0, 400), children: [
1574
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.IconContextInjectionOutlineRegular, { size: 14 }),
1575
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: label })
1576
+ ] });
1577
+ }
1578
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AssistantView, { blocks: node.blocks, streaming: false, running: state.running, t, labels });
1579
+ }, (previous, next) => previous.node === next.node && previous.t === next.t && previous.labels === next.labels && (previous.node.kind !== "assistant" || previous.state.running === next.state.running));
1580
+ function AssistantView({ blocks, streaming, running, t, labels }) {
1581
+ const groups = [];
1582
+ let process = [];
1583
+ const flush = (key) => {
1584
+ if (process.length === 0) return;
1585
+ groups.push(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsc-process", children: process }, `p${key}`));
1586
+ process = [];
1587
+ };
1588
+ blocks.forEach((block, index) => {
1589
+ const last = index === blocks.length - 1;
1590
+ if (block.kind === "reasoning") {
1591
+ process.push(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ReasoningRow, { text: block.text, running: streaming && last, labels: labels.markdown, t }, index));
1592
+ } else if (block.kind === "tool-call") {
1593
+ process.push(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ToolRow, { block, running: block.result === null && running, labels, t }, index));
1594
+ } else {
1595
+ flush(index);
1596
+ groups.push(
1597
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsc-markdown", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.MarkdownText, { text: block.text, streaming: streaming && last, labels: labels.markdown }) }, index)
1598
+ );
1599
+ }
1600
+ });
1601
+ flush(blocks.length);
1602
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsc-assistant", children: groups });
1603
+ }
1604
+ function Notice({ error, agentError, t, onDismiss }) {
1605
+ if (error === null && agentError === null) return null;
1606
+ const title = error !== null ? t(`error.${error.code}`) : t("error.agent");
1607
+ const detail = error !== null ? error.message : agentError ?? "";
1608
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsc-notice", role: "alert", children: [
1609
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsc-notice-body", children: [
1610
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: title }),
1611
+ detail === "" || detail === title ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsc-notice-detail", children: detail })
1612
+ ] }),
1613
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.Button, { variant: "ghost", size: "sm", icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.IconCloseOutlineRegular, { size: 14 }), "aria-label": t("notice.dismiss"), onClick: onDismiss })
1614
+ ] });
1615
+ }
1616
+ function isComposing(event) {
1617
+ return event.nativeEvent.isComposing || event.keyCode === 229;
1618
+ }
1619
+ function Composer({ panel, state, t }) {
1620
+ const inputRef = (0, import_react2.useRef)(null);
1621
+ (0, import_react2.useLayoutEffect)(() => {
1622
+ const input = inputRef.current;
1623
+ if (input === null) return;
1624
+ input.style.height = "auto";
1625
+ input.style.height = `${input.scrollHeight}px`;
1626
+ }, [state.draft]);
1627
+ (0, import_react2.useEffect)(() => {
1628
+ if (state.focusRequest === 0) return void 0;
1629
+ const frame = requestAnimationFrame(() => inputRef.current?.focus());
1630
+ return () => cancelAnimationFrame(frame);
1631
+ }, [state.focusRequest]);
1632
+ const canSend = state.draft.trim() !== "" && !state.sending && !state.gone;
1633
+ const stops = !canSend && state.running && !state.gone;
1634
+ const label = stops ? t("composer.stop") : t("composer.send");
1635
+ const placeholder = state.gone ? t("composer.gone") : state.sessionId === null ? t("composer.placeholderBlank") : t("composer.placeholder");
1636
+ const submit = () => {
1637
+ if (canSend) void panel.send();
1638
+ else if (stops) void panel.cancel();
1639
+ };
1640
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsc-card", "data-side-chat-composer": "", children: [
1641
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1642
+ "textarea",
1643
+ {
1644
+ ref: inputRef,
1645
+ className: "dsc-input",
1646
+ "data-side-chat-input": "",
1647
+ rows: 1,
1648
+ value: state.draft,
1649
+ placeholder,
1650
+ "aria-label": t("composer.label"),
1651
+ disabled: state.gone,
1652
+ onChange: (event) => panel.setDraft(event.target.value),
1653
+ onKeyDown: (event) => {
1654
+ if (event.key === "Enter" && !event.shiftKey && !isComposing(event)) {
1655
+ event.preventDefault();
1656
+ if (canSend) void panel.send();
1657
+ } else if (event.key === "Escape" && state.running && state.draft === "") {
1658
+ event.preventDefault();
1659
+ void panel.cancel();
1660
+ }
1661
+ }
1662
+ }
1663
+ ),
1664
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsc-composer-row", children: [
1665
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsc-hint", children: t("empty.hint") }),
1666
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.Tooltip, { label, side: "top", delayMs: 500, disabled: !canSend && !stops, portal: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1667
+ "button",
1668
+ {
1669
+ type: "button",
1670
+ className: "dsc-primary",
1671
+ "aria-label": label,
1672
+ disabled: !canSend && !stops,
1673
+ onMouseDown: (event) => event.preventDefault(),
1674
+ onClick: submit,
1675
+ children: stops ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "3", y: "3", width: "10", height: "10", rx: "3", fill: "currentColor" }) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8.3125 0.980183C8.66767 1.0531 8.97902 1.20418 9.2627 1.43233C9.48724 1.61297 9.73029 1.85793 9.97949 2.10714L14.707 6.83468L13.293 8.24874L9 3.95577V15.0417H7V3.95577L2.70703 8.24874L1.29297 6.83468L6.02051 2.10714C6.26971 1.85793 6.51277 1.61297 6.7373 1.43233C6.97662 1.23986 7.28445 1.04402 7.6875 0.980183C7.8973 0.947006 8.1031 0.95516 8.3125 0.980183Z", fill: "currentColor" }) })
1676
+ }
1677
+ ) })
1678
+ ] })
1679
+ ] });
1680
+ }
1681
+ function compact(t, value) {
1682
+ const scaled = (n) => n >= 100 ? String(Math.round(n)) : String(Math.round(n * 10) / 10);
1683
+ if (!(value > 0)) return "0";
1684
+ if (value < 1e3) return String(Math.round(value));
1685
+ if (value < 1e6) return t("dock.thousand", { value: scaled(value / 1e3) });
1686
+ return t("dock.million", { value: scaled(value / 1e6) });
1687
+ }
1688
+ var exact = (value) => Math.max(0, Math.round(value)).toLocaleString("en-US");
1689
+ function UsageDock({ metrics, contextWindow, t }) {
1690
+ const occupancy = (0, import_react2.useMemo)(() => occupancyOf(metrics, contextWindow), [metrics, contextWindow]);
1691
+ if (metrics === null) return null;
1692
+ const usage = t(metrics.cacheWriteTokens > 0 ? "dock.usageTitleWithWrite" : "dock.usageTitle", {
1693
+ input: exact(metrics.uncachedInputTokens),
1694
+ cacheRead: exact(metrics.cacheReadTokens),
1695
+ cacheWrite: exact(metrics.cacheWriteTokens),
1696
+ output: exact(metrics.outputTokens)
1697
+ });
1698
+ const circumference = 2 * Math.PI * 5.5;
1699
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsc-dock", children: [
1700
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.Tooltip, { label: usage, side: "top", delayMs: 300, portal: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dsc-dock-item", children: [
1701
+ t("dock.tokens", { count: compact(t, metrics.totalTokens) }),
1702
+ metrics.cacheHitPercent === null ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
1703
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "aria-hidden": "true", children: "\xB7" }),
1704
+ t("dock.cacheHit", { percent: metrics.cacheHitPercent })
1705
+ ] })
1706
+ ] }) }),
1707
+ occupancy === null ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives3.Tooltip, { label: t("dock.contextTitle", { used: exact(occupancy.usedTokens), window: exact(occupancy.contextWindow) }), side: "top", delayMs: 300, portal: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dsc-dock-item", role: "img", "aria-label": t("dock.contextAria", { percent: occupancy.percent }), children: [
1708
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { viewBox: "0 0 14 14", width: "13", height: "13", "aria-hidden": "true", children: [
1709
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { className: "dsc-ring-track", cx: "7", cy: "7", r: "5.5" }),
1710
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1711
+ "circle",
1712
+ {
1713
+ className: "dsc-ring-fill",
1714
+ cx: "7",
1715
+ cy: "7",
1716
+ r: "5.5",
1717
+ transform: "rotate(-90 7 7)",
1718
+ strokeDasharray: `${circumference * occupancy.percent / 100} ${circumference}`
1719
+ }
1720
+ )
1721
+ ] }),
1722
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: `${occupancy.percent}%` })
1723
+ ] }) })
1724
+ ] });
1725
+ }
1726
+
1727
+ // src/client/index.tsx
1728
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1729
+ var FILE_ADDRESS_PREFIX = "dsh-resource://file/";
1730
+ var GUIDE_KIND = "guide";
1731
+ function openParamsOf(value) {
1732
+ if (typeof value !== "object" || value === null) return void 0;
1733
+ const { prompt, focus } = value;
1734
+ return {
1735
+ ...typeof prompt === "string" ? { prompt } : {},
1736
+ ...focus === true ? { focus: true } : {}
1737
+ };
1738
+ }
1739
+ function sessionFileAddress(sessionId, path) {
1740
+ const segment = (value) => encodeURIComponent(value).replace(/%3A/gi, ":");
1741
+ const normalized = path.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "");
1742
+ return `${FILE_ADDRESS_PREFIX}session/${segment(sessionId)}/${normalized.split("/").map(segment).join("/")}`;
1743
+ }
1744
+ function useLocaleRevision(locale) {
1745
+ const subscribe = (0, import_react3.useCallback)((listener) => locale.subscribe(listener), [locale]);
1746
+ const revision = (0, import_react3.useCallback)(() => locale.getSnapshot().revision, [locale]);
1747
+ return (0, import_react3.useSyncExternalStore)(subscribe, revision);
1748
+ }
1749
+ function SideChatBody({ registry, sessionId, t, locale, useTabInfo }) {
1750
+ const revision = useLocaleRevision(locale);
1751
+ const info = useTabInfo?.();
1752
+ const tab = info?.tab;
1753
+ const key = panelKey(sessionId, tab?.id);
1754
+ const panel = (0, import_react3.useMemo)(() => key === "" ? null : registry.open(key, sessionId), [registry, key, sessionId]);
1755
+ const visible = tab === void 0 ? true : tab.visible;
1756
+ (0, import_react3.useEffect)(() => {
1757
+ if (panel === null) return void 0;
1758
+ panel.setVisible(visible);
1759
+ return () => panel.setVisible(false);
1760
+ }, [panel, visible]);
1761
+ const navigation = tab?.navigation;
1762
+ (0, import_react3.useEffect)(() => {
1763
+ if (panel !== null && navigation !== void 0) panel.navigate(navigation.revision, openParamsOf(navigation.params));
1764
+ }, [panel, navigation]);
1765
+ const labels = (0, import_react3.useMemo)(() => panelLabels(t), [t, revision]);
1766
+ const actions = tab?.actions;
1767
+ const onNew = (0, import_react3.useMemo)(() => actions === void 0 ? null : () => actions.openTab(TAB_KIND), [actions]);
1768
+ const openFile = (0, import_react3.useMemo)(() => {
1769
+ if (actions === void 0) return void 0;
1770
+ return (path, options) => {
1771
+ const line = options?.line;
1772
+ actions.openResource(sessionFileAddress(sessionId, path), line === void 0 ? void 0 : { params: { line } });
1773
+ };
1774
+ }, [actions, sessionId]);
1775
+ if (panel === null) return null;
1776
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PanelView, { panel, t, labels, onNew, openFile });
1777
+ }
1778
+ function SideChatTitle({ registry, sessionId, t, locale, useTabInfo }) {
1779
+ useLocaleRevision(locale);
1780
+ (0, import_react3.useSyncExternalStore)(registry.subscribe, registry.getVersion);
1781
+ const key = panelKey(sessionId, useTabInfo?.().tab.id);
1782
+ const ordinal = registry.get(key)?.getSnapshot().ordinal ?? 1;
1783
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: ordinal > 1 ? t("tab.titleIndexed", { index: ordinal }) : t("tab.title") });
1784
+ }
1785
+ var name = PLUGIN_NAME;
1786
+ var inject = ["slots", "sidebarRight", "sidebarRightTabs", "locale"];
1787
+ function apply(ctx) {
1788
+ const t = ctx.locale.bind(LOCALE_NS);
1789
+ const registry = new Registry(document);
1790
+ ctx.effect(() => {
1791
+ const disposeZh = ctx.locale.register(LOCALE_NS, "zh", ZH);
1792
+ const disposeEn = ctx.locale.register(LOCALE_NS, "en", EN);
1793
+ return () => {
1794
+ disposeEn();
1795
+ disposeZh();
1796
+ };
1797
+ }, `${PLUGIN_NAME}: copy`);
1798
+ ctx.effect(() => installStyle(document), `${PLUGIN_NAME}: stylesheet`);
1799
+ ctx.effect(() => () => registry.dispose(), `${PLUGIN_NAME}: panels`);
1800
+ ctx.effect(() => ctx.sidebarRightTabs.register({
1801
+ id: PLUGIN_NAME,
1802
+ kind: TAB_KIND,
1803
+ priority: "extension",
1804
+ title: () => t("tab.title"),
1805
+ guide: [{
1806
+ id: "side-chat",
1807
+ order: 10,
1808
+ title: () => t("guide.title"),
1809
+ description: () => t("guide.description"),
1810
+ icon: import_dsh_client_ui_primitives4.PluginArtworkSubagent
1811
+ }],
1812
+ // A visited body stays mounted (keeps scroll and draft); each open mints a new tab.
1813
+ keepMounted: true,
1814
+ multiple: true
1815
+ }), `${PLUGIN_NAME}: tab type`);
1816
+ const seat = (sessionId) => ({ registry, sessionId, t, locale: ctx.locale });
1817
+ ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
1818
+ name: "sidebar.right.pane.tab",
1819
+ key: PLUGIN_NAME,
1820
+ locale: LOCALE_NS,
1821
+ inject: seat
1822
+ }, SideChatBody)), `${PLUGIN_NAME}: tab body`);
1823
+ ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab.title", () => ctx.slots.register({
1824
+ name: "sidebar.right.pane.tab.title",
1825
+ key: PLUGIN_NAME,
1826
+ locale: LOCALE_NS,
1827
+ inject: seat
1828
+ }, SideChatTitle)), `${PLUGIN_NAME}: tab title`);
1829
+ ctx.effect(() => ctx.sidebarRight.registerCloseHandler(TAB_KIND, (sessionId, tab) => {
1830
+ void registry.close(panelKey(sessionId, tab.id), sessionId);
1831
+ }), `${PLUGIN_NAME}: tab close`);
1832
+ const onScreen = (sessionId) => ctx.sidebarRight.mounted.getSnapshot() === sessionId;
1833
+ const open = (sessionId, prompt) => {
1834
+ if (!onScreen(sessionId)) throw new Error(`${PLUGIN_NAME}: session ${sessionId} is not on screen`);
1835
+ const active = ctx.sidebarRight.active();
1836
+ const params = prompt === "" ? { focus: true } : { prompt };
1837
+ ctx.sidebarRight.openTab(TAB_KIND, { params, ...active?.kind === GUIDE_KIND ? { replaceTab: active.id } : {} });
1838
+ };
1839
+ ctx.inject(["inputTriggers"], (scope) => {
1840
+ scope.effect(
1841
+ () => scope.inputTriggers.registerSource(sideChatSource({ t, eligible: onScreen, open })),
1842
+ `${PLUGIN_NAME}: slash commands`
1843
+ );
1844
+ });
1845
+ }
1846
+
1847
+ return module.exports;
1848
+ },
1849
+ });