banso-dsh-ui 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tindada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,19 @@
1
- # Temporary Holding Version
1
+ # Banso 聊天 UI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `banso-dsh-ui` 是 DSH 浏览器插件,配合 `banso-dsh-web` 使用,向官方 renderer 的 `root` 插槽注册 Banso 页面。
4
+
5
+ ## 集成要求
6
+
7
+ 接口对齐 DSH `0.2.0-rc.2`;需要官方 `@deepseek-ai/dsh-api-session-controller` 和 `@deepseek-ai/dsh-client-ui-renderer` 客户端服务,由 Web bundle 配置依赖。宿主入口不提供独立业务服务,无需单独启动 UI。不要同时加载其他占用根页面的聊天 UI。
8
+
9
+ ## 展示与限制
10
+
11
+ 支持单会话文字聊天、连续追问、流式回答、停止和错误提示。不显示推理或注入上下文,不提供资料面板。
12
+
13
+ 执行期间分别显示每次搜索、阅读的进行中、完成、失败或取消状态;可展开查看关键词、关注点、URL 和错误详情,并保留本轮先前的失败记录。仅展示整次调用,不区分下载与提取阶段。本轮完成、失败或停止后隐藏全部工具记录,服务端日志仍保留。
14
+
15
+ 伴随工具调用的模型文字临时显示,当前步骤结束后消失;普通回答保留。
16
+
17
+ 当前会话 ID 仅保留在页面内存中,没有历史列表和刷新恢复;刷新后下一次发送会创建新会话。刷新或关闭页面不保证停止服务端任务。文本由 React 转义,不解析 HTML 或 Markdown。
18
+
19
+ 安装和启动见 [Web 聊天使用说明](https://github.com/Tindada/banso-dsh/blob/main/README.md#web-聊天源码安装)。运行配置和存储说明见 [Web bundle](https://github.com/Tindada/banso-dsh/blob/main/packages/web/README.md)。源码构建、消息投影和测试见 [开发文档](https://github.com/Tindada/banso-dsh/blob/main/docs/development.md)。
package/lib/client.js ADDED
@@ -0,0 +1,308 @@
1
+ window.__ModuleLoader__.load({ id: "banso-dsh-ui", factory: (require) => { var module = { exports: {} }; var exports = module.exports;
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/client/index.tsx
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ apply: () => apply,
25
+ inject: () => inject
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var import_react = require("react");
29
+
30
+ // src/client/messages.ts
31
+ var textOf = (content) => content.flatMap((block) => block.type === "text" ? [block.text] : []).join("");
32
+ var hasToolCall = (content) => content.some((block) => block.type === "tool-call");
33
+ function messagesOf(entries) {
34
+ const messages = [];
35
+ const live = /* @__PURE__ */ new Map();
36
+ const settledSteps = /* @__PURE__ */ new Map();
37
+ for (const { event } of entries) {
38
+ if (event.type === "assistant/message" && !hasToolCall(event.data.message.content)) {
39
+ const key = `${event.data.turn}:${event.data.step}`;
40
+ settledSteps.set(key, Math.max(settledSteps.get(key) ?? -1, event.seq));
41
+ }
42
+ }
43
+ for (const { event } of entries) {
44
+ if (event.type === "user/message" && event.data.source.kind === "user") {
45
+ messages.push({ id: `user-${event.seq}`, seq: event.seq, role: "user", text: textOf(event.data.content) });
46
+ } else if (event.type === "assistant/message") {
47
+ if (hasToolCall(event.data.message.content)) continue;
48
+ const text = textOf(event.data.message.content);
49
+ if (text) messages.push({ id: `assistant-${event.seq}`, seq: event.seq, role: "assistant", text });
50
+ } else if (event.type === "assistant/live-chunk" && event.data.chunk.type === "text-delta") {
51
+ const settledSeq = settledSteps.get(`${event.data.turn}:${event.data.step}`);
52
+ if (settledSeq !== void 0 && event.seq < settledSeq) continue;
53
+ const id = event.data.attemptId;
54
+ let message = live.get(id);
55
+ if (!message) {
56
+ message = { id: `live-${id}`, seq: event.seq, role: "assistant", text: "" };
57
+ live.set(id, message);
58
+ messages.push(message);
59
+ }
60
+ message.text += event.data.chunk.text;
61
+ }
62
+ }
63
+ return messages;
64
+ }
65
+
66
+ // src/client/activity.ts
67
+ var object = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
68
+ var string = (value) => typeof value === "string" ? value : "";
69
+ function argumentsOf(value) {
70
+ try {
71
+ return object(JSON.parse(value));
72
+ } catch {
73
+ return {};
74
+ }
75
+ }
76
+ function failureNote(error) {
77
+ if (error.startsWith("Extraction failed:")) {
78
+ return error.includes("Extraction did not complete: max-tokens") ? "\u5185\u5BB9\u63D0\u53D6\u8FBE\u5230\u8F93\u51FA\u4E0A\u9650" : "\u5185\u5BB9\u63D0\u53D6\u5931\u8D25";
79
+ }
80
+ return error.startsWith("Fetch failed:") ? "\u7F51\u9875\u6293\u53D6\u5931\u8D25" : "\u6267\u884C\u5931\u8D25";
81
+ }
82
+ function activitySummary(item) {
83
+ const status = { running: `\u6B63\u5728${item.label}`, completed: `${item.label}\u5B8C\u6210`, failed: `${item.label}\u5931\u8D25`, cancelled: `${item.label}\u5DF2\u53D6\u6D88` }[item.status];
84
+ return `${status}${item.target ? `\uFF1A${item.target}` : ""}${item.note ? ` \u2014 ${item.note}` : ""}`;
85
+ }
86
+ function timelineOf(entries) {
87
+ const items = messagesOf(entries).map((message) => ({ ...message, kind: "message" }));
88
+ const calls = /* @__PURE__ */ new Map();
89
+ const endedTurns = /* @__PURE__ */ new Set();
90
+ for (const { event } of entries) {
91
+ if (event.type === "tool/call") {
92
+ const { turn, step, callId, name } = event.data;
93
+ if (name !== "web_search" && name !== "web_read") continue;
94
+ const key = `${turn}:${step}:${callId}`;
95
+ if (calls.has(key)) continue;
96
+ const args = argumentsOf(event.data.arguments);
97
+ const queries = Array.isArray(args.queries) && args.queries.every((q) => typeof q === "string") ? args.queries.join("\uFF1B") : "";
98
+ const item = {
99
+ id: `tool-${event.seq}`,
100
+ seq: event.seq,
101
+ turn,
102
+ kind: "tool",
103
+ name,
104
+ status: "running",
105
+ label: name === "web_search" ? "\u641C\u7D22" : "\u9605\u8BFB",
106
+ target: name === "web_search" ? queries : string(args.target),
107
+ detail: string(args.focus),
108
+ note: "",
109
+ error: ""
110
+ };
111
+ calls.set(key, item);
112
+ items.push(item);
113
+ } else if (event.type === "tool/result") {
114
+ const { turn, step, message, error, meta } = event.data;
115
+ const item = calls.get(`${turn}:${step}:${message.toolCallId}`);
116
+ if (!item) continue;
117
+ const value = object(meta);
118
+ item.note = "";
119
+ item.error = "";
120
+ if (error?.code === "ABORTED" || error?.code === "ABORTED_BEFORE_DISPATCH") {
121
+ item.status = "cancelled";
122
+ } else if (message.isError || item.name === "web_read" && typeof value.error === "string") {
123
+ item.status = "failed";
124
+ item.error = message.isError ? error?.reason || message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n") : string(value.error);
125
+ item.note = failureNote(item.error);
126
+ } else {
127
+ item.status = "completed";
128
+ if (item.name === "web_search" && Array.isArray(value.sources)) item.note = `\u8FD4\u56DE ${value.sources.length} \u4E2A\u6765\u6E90`;
129
+ if (item.name === "web_read" && typeof value.evidence === "string") item.note = value.evidence === "" ? "\u672A\u627E\u5230\u76F8\u5173\u8BC1\u636E" : "\u5DF2\u63D0\u53D6\u76F8\u5173\u8BC1\u636E";
130
+ }
131
+ const details = [item.detail];
132
+ if (typeof value.requestUrl === "string") details.push(`URL\uFF1A${value.requestUrl}`);
133
+ if (value.truncated === true) details.push("\u7ED3\u679C\u5DF2\u622A\u65AD");
134
+ item.detail = details.filter(Boolean).join("\n");
135
+ } else if (event.type === "turn/end") {
136
+ endedTurns.add(event.data.turn);
137
+ }
138
+ }
139
+ return items.filter((item) => item.kind !== "tool" || !endedTurns.has(item.turn)).sort((a, b) => a.seq - b.seq);
140
+ }
141
+
142
+ // src/client/index.tsx
143
+ var import_jsx_runtime = require("react/jsx-runtime");
144
+ var inject = ["slots", "sessions", "connection"];
145
+ function Conversation({ binding }) {
146
+ const events = (0, import_react.useSyncExternalStore)((listener) => binding.eventSource.subscribe(listener), () => binding.eventSource.getSnapshot());
147
+ const state = (0, import_react.useSyncExternalStore)((listener) => binding.session.subscribe(listener), () => binding.session.getSnapshot());
148
+ const end = (0, import_react.useRef)(null);
149
+ (0, import_react.useEffect)(() => {
150
+ end.current?.scrollIntoView({ block: "end" });
151
+ }, [events]);
152
+ const messages = timelineOf(events.entries);
153
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
154
+ messages.map((message) => message.kind === "tool" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { className: `banso-tool ${message.status}`, children: [
155
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("summary", { children: [
156
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: { running: "\u25CC", completed: "\u2713", failed: "\u2717", cancelled: "\u25CB" }[message.status] }),
157
+ " ",
158
+ activitySummary(message)
159
+ ] }),
160
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
161
+ message.target && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [
162
+ message.name === "web_search" ? "\u5173\u952E\u8BCD" : "\u9605\u8BFB\u5BF9\u8C61",
163
+ "\uFF1A",
164
+ message.target
165
+ ] }),
166
+ message.detail && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: message.detail }),
167
+ message.error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [
168
+ "\u9519\u8BEF\u8BE6\u60C5\uFF1A",
169
+ message.error
170
+ ] }),
171
+ !message.target && !message.detail && !message.error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "\u6CA1\u6709\u66F4\u591A\u8BE6\u60C5" })
172
+ ] })
173
+ ] }, message.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("article", { className: `banso-message ${message.role}`, children: [
174
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: message.role === "user" ? "\u4F60" : "Banso" }),
175
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: message.text })
176
+ ] }, message.id)),
177
+ state.pendingSubmissions.map((pending) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("article", { className: "banso-message user", children: [
178
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "\u4F60 \xB7 \u53D1\u9001\u4E2D" }),
179
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: pending.text })
180
+ ] }, pending.requestId)),
181
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: end })
182
+ ] });
183
+ }
184
+ function App({ ctx }) {
185
+ const [binding, setBinding] = (0, import_react.useState)();
186
+ const [input, setInput] = (0, import_react.useState)("");
187
+ const [busy, setBusy] = (0, import_react.useState)(false);
188
+ const [stopping, setStopping] = (0, import_react.useState)(false);
189
+ const [error, setError] = (0, import_react.useState)("");
190
+ const [running, setRunning] = (0, import_react.useState)(false);
191
+ const reference = (0, import_react.useRef)();
192
+ const alive = (0, import_react.useRef)(true);
193
+ const inFlight = (0, import_react.useRef)(false);
194
+ const connection = ctx.get("connection");
195
+ const generation = (0, import_react.useSyncExternalStore)((listener) => connection.generation.subscribe(listener), () => connection.generation.getSnapshot());
196
+ (0, import_react.useEffect)(() => () => {
197
+ alive.current = false;
198
+ reference.current?.release();
199
+ }, []);
200
+ (0, import_react.useEffect)(() => {
201
+ if (!binding) return;
202
+ const update = () => {
203
+ const state = binding.session.getSnapshot();
204
+ setRunning(state.running || state.pendingSubmissions.length > 0 || state.awaitingFirstTurn);
205
+ if (!state.running) setStopping(false);
206
+ const failure = state.promptError?.error.message ?? state.openError?.message ?? state.lastAgentError;
207
+ if (failure) setError(failure);
208
+ };
209
+ update();
210
+ return binding.session.subscribe(update);
211
+ }, [binding]);
212
+ async function send() {
213
+ const text = input.trim();
214
+ if (!text || inFlight.current || running || !generation) return;
215
+ inFlight.current = true;
216
+ setBusy(true);
217
+ setError("");
218
+ try {
219
+ let current = binding;
220
+ if (!current) {
221
+ const id = await ctx.sessions.create();
222
+ if (!alive.current) return;
223
+ const held = ctx.sessions.retain(id, { source: "bansoChat" });
224
+ reference.current = held;
225
+ try {
226
+ current = await held.ready;
227
+ } catch (error2) {
228
+ held.release();
229
+ reference.current = void 0;
230
+ throw error2;
231
+ }
232
+ if (!alive.current) return;
233
+ setBinding(current);
234
+ }
235
+ const submission = current.session.beginSubmission({ mode: "queue", text, attachments: [] });
236
+ const result = await current.session.prompt([{ type: "text", text }], "queue", void 0, submission.requestId);
237
+ if (!result.ok) throw new Error(result.error.message);
238
+ if (alive.current) setInput("");
239
+ } catch (error2) {
240
+ if (alive.current) setError(error2 instanceof Error ? error2.message : String(error2));
241
+ } finally {
242
+ inFlight.current = false;
243
+ if (alive.current) setBusy(false);
244
+ }
245
+ }
246
+ async function stop() {
247
+ if (!binding || stopping) return;
248
+ setStopping(true);
249
+ setError("");
250
+ try {
251
+ const result = await binding.session.cancel();
252
+ if (!result.ok) throw new Error(result.error.message);
253
+ } catch (error2) {
254
+ if (alive.current) {
255
+ setStopping(false);
256
+ setError(error2 instanceof Error ? error2.message : String(error2));
257
+ }
258
+ }
259
+ }
260
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("main", { className: "banso-app", children: [
261
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { children: [
262
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { children: "Banso" }),
263
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: !generation ? "\u8FDE\u63A5\u4E2D\u2026" : stopping ? "\u6B63\u5728\u505C\u6B62\u2026" : running ? "\u6B63\u5728\u7814\u7A76\u2026" : "\u65B0\u95FB\u7814\u7A76\u52A9\u624B" })
264
+ ] }),
265
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("section", { className: "banso-messages", "aria-label": "\u804A\u5929\u6D88\u606F", children: binding ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Conversation, { binding }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "banso-empty", children: [
266
+ "\u8F93\u5165\u4E00\u4E2A\u95EE\u9898\uFF0C\u5F00\u59CB\u7814\u7A76\u3002",
267
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: "\u6B64\u7248\u672C\u5237\u65B0\u540E\u4F1A\u5F00\u59CB\u65B0\u5BF9\u8BDD\u3002" })
268
+ ] }) }),
269
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: "banso-error", children: error }),
270
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: (event) => {
271
+ event.preventDefault();
272
+ void send();
273
+ }, children: [
274
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "banso-input", children: "\u4F60\u7684\u95EE\u9898" }),
275
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: "banso-input", placeholder: "\u60F3\u4E86\u89E3\u4EC0\u4E48\uFF1F", rows: 3, value: input, onChange: (event) => setInput(event.target.value), disabled: busy || running }),
276
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "banso-actions", children: [
277
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: "\u7EAF\u6587\u672C\u804A\u5929" }),
278
+ running && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: stopping || !generation, onClick: () => void stop(), children: "\u505C\u6B62" }),
279
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", disabled: !input.trim() || busy || running || !generation, children: busy ? "\u53D1\u9001\u4E2D\u2026" : "\u53D1\u9001" })
280
+ ] })
281
+ ] })
282
+ ] });
283
+ }
284
+ var css = `
285
+ html,body,#root{height:100%;margin:0}body{background:#f7f8fa;color:#20252c;font:16px/1.65 system-ui,sans-serif}
286
+ .banso-app{box-sizing:border-box;max-width:900px;height:100dvh;margin:auto;padding:20px 24px;display:flex;flex-direction:column}
287
+ .banso-app header{display:flex;align-items:center;gap:20px;border-bottom:1px solid #dfe3e8;padding-bottom:12px}.banso-app h1{font-size:24px;margin:0}.banso-app header span,.banso-app small{color:#647080;font-size:13px}
288
+ .banso-messages{flex:1;overflow:auto;min-height:0;padding:24px 0}.banso-message{padding:14px 18px;margin:0 0 18px;border-radius:12px;background:white;overflow-wrap:anywhere}.banso-message.user{background:#e9f0fa}.banso-message strong{font-size:13px}.banso-message div{white-space:pre-wrap}.banso-empty{color:#647080;text-align:center;margin-top:15vh}.banso-empty small{display:block}
289
+ .banso-tool{margin:0 0 12px;padding:8px 14px;border-left:3px solid #bdc7d4;font-size:14px;overflow-wrap:anywhere}.banso-tool summary{cursor:pointer}.banso-tool p{white-space:pre-wrap;margin:8px 0}.banso-tool.failed{border-color:#a52d2d}.banso-tool.running{border-color:#245ba0}.banso-tool.completed{border-color:#38714b}
290
+ .banso-app label{display:block;font-size:13px;margin-bottom:6px}.banso-app textarea{box-sizing:border-box;width:100%;resize:vertical;max-height:30vh;border:1px solid #bdc7d4;border-radius:10px;padding:12px;font:inherit}.banso-app textarea:focus{outline:2px solid #5285c5}.banso-actions{display:flex;align-items:center;gap:12px;margin-top:10px}.banso-actions small{flex:1}.banso-app button{border:0;border-radius:8px;background:#245ba0;color:white;padding:9px 20px;font:inherit;cursor:pointer}.banso-app button:disabled{opacity:.5;cursor:default}.banso-error{color:#9f2525;background:#ffeded;padding:10px;overflow-wrap:anywhere;margin:8px 0}@media(max-width:600px){.banso-app{padding:12px}.banso-actions small{font-size:11px}}
291
+ `;
292
+ function apply(ctx) {
293
+ ctx.effect(() => {
294
+ const style = document.createElement("style");
295
+ style.textContent = css;
296
+ document.head.append(style);
297
+ const previousTitle = document.title;
298
+ document.title = "Banso";
299
+ const dispose = ctx.slots.register({ name: "root" }, () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(App, { ctx }));
300
+ return () => {
301
+ dispose();
302
+ style.remove();
303
+ document.title = previousTitle;
304
+ };
305
+ });
306
+ }
307
+ return module.exports; } });
308
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/client/index.tsx", "../src/client/messages.ts", "../src/client/activity.ts"],
4
+ "sourcesContent": ["import type { Context } from '@deepseek-ai/cordis';\nimport type { SessionBinding, SessionReference } from '@deepseek-ai/dsh-api-session-controller/client';\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client';\nimport type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client';\nimport { useEffect, useRef, useState, useSyncExternalStore } from 'react';\nimport { timelineOf, activitySummary } from './activity.js';\n\n// Lifetime label for the one visible conversation; not a persisted session property.\ndeclare module '@deepseek-ai/dsh-api-session-controller/client' {\n interface SessionReferenceSourceMap { bansoChat: unknown }\n}\nexport const inject = ['slots', 'sessions', 'connection'];\n\nfunction Conversation({ binding }: { binding: SessionBinding }) {\n const events = useSyncExternalStore(listener => binding.eventSource.subscribe(listener), () => binding.eventSource.getSnapshot());\n const state = useSyncExternalStore(listener => binding.session.subscribe(listener), () => binding.session.getSnapshot());\n const end = useRef<HTMLDivElement>(null);\n useEffect(() => { end.current?.scrollIntoView({ block: 'end' }); }, [events]);\n const messages = timelineOf(events.entries);\n return <>\n {messages.map(message => message.kind === 'tool' ? <details className={`banso-tool ${message.status}`} key={message.id}>\n <summary><span aria-hidden=\"true\">{{ running: '\u25CC', completed: '\u2713', failed: '\u2717', cancelled: '\u25CB' }[message.status]}</span> {activitySummary(message)}</summary>\n <div>{message.target && <p>{message.name === 'web_search' ? '\u5173\u952E\u8BCD' : '\u9605\u8BFB\u5BF9\u8C61'}\uFF1A{message.target}</p>}\n {message.detail && <p>{message.detail}</p>}{message.error && <p>\u9519\u8BEF\u8BE6\u60C5\uFF1A{message.error}</p>}\n {!message.target && !message.detail && !message.error && <p>\u6CA1\u6709\u66F4\u591A\u8BE6\u60C5</p>}</div>\n </details> : <article className={`banso-message ${message.role}`} key={message.id}>\n <strong>{message.role === 'user' ? '\u4F60' : 'Banso'}</strong><div>{message.text}</div>\n </article>)}\n {state.pendingSubmissions.map(pending => <article className=\"banso-message user\" key={pending.requestId}><strong>\u4F60 \u00B7 \u53D1\u9001\u4E2D</strong><div>{pending.text}</div></article>)}\n <div ref={end} />\n </>;\n}\n\nfunction App({ ctx }: { ctx: Context }) {\n const [binding, setBinding] = useState<SessionBinding>();\n const [input, setInput] = useState('');\n const [busy, setBusy] = useState(false);\n const [stopping, setStopping] = useState(false);\n const [error, setError] = useState('');\n const [running, setRunning] = useState(false);\n const reference = useRef<SessionReference>();\n const alive = useRef(true);\n const inFlight = useRef(false);\n const connection = ctx.get('connection') as ConnectionHandle;\n const generation = useSyncExternalStore(listener => connection.generation.subscribe(listener), () => connection.generation.getSnapshot());\n useEffect(() => () => { alive.current = false; reference.current?.release(); }, []);\n useEffect(() => {\n if (!binding) return;\n const update = () => {\n const state = binding.session.getSnapshot();\n setRunning(state.running || state.pendingSubmissions.length > 0 || state.awaitingFirstTurn);\n if (!state.running) setStopping(false);\n const failure = state.promptError?.error.message ?? state.openError?.message ?? state.lastAgentError;\n if (failure) setError(failure);\n };\n update();\n return binding.session.subscribe(update);\n }, [binding]);\n async function send() {\n const text = input.trim();\n if (!text || inFlight.current || running || !generation) return;\n inFlight.current = true;\n setBusy(true); setError('');\n try {\n let current = binding;\n if (!current) {\n const id = await ctx.sessions.create();\n if (!alive.current) return;\n const held = ctx.sessions.retain(id, { source: 'bansoChat' });\n reference.current = held;\n try { current = await held.ready; }\n catch (error) { held.release(); reference.current = undefined; throw error; }\n if (!alive.current) return;\n setBinding(current);\n }\n const submission = current.session.beginSubmission({ mode: 'queue', text, attachments: [] });\n const result = await current.session.prompt([{ type: 'text', text }], 'queue', undefined, submission.requestId);\n if (!result.ok) throw new Error(result.error.message);\n if (alive.current) setInput('');\n } catch (error) {\n if (alive.current) setError(error instanceof Error ? error.message : String(error));\n } finally {\n inFlight.current = false;\n if (alive.current) setBusy(false);\n }\n }\n async function stop() {\n if (!binding || stopping) return;\n setStopping(true); setError('');\n try {\n const result = await binding.session.cancel();\n if (!result.ok) throw new Error(result.error.message);\n } catch (error) {\n if (alive.current) { setStopping(false); setError(error instanceof Error ? error.message : String(error)); }\n }\n }\n return <main className=\"banso-app\">\n <header><h1>Banso</h1><span>{!generation ? '\u8FDE\u63A5\u4E2D\u2026' : stopping ? '\u6B63\u5728\u505C\u6B62\u2026' : running ? '\u6B63\u5728\u7814\u7A76\u2026' : '\u65B0\u95FB\u7814\u7A76\u52A9\u624B'}</span></header>\n <section className=\"banso-messages\" aria-label=\"\u804A\u5929\u6D88\u606F\">\n {binding ? <Conversation binding={binding} /> : <p className=\"banso-empty\">\u8F93\u5165\u4E00\u4E2A\u95EE\u9898\uFF0C\u5F00\u59CB\u7814\u7A76\u3002<small>\u6B64\u7248\u672C\u5237\u65B0\u540E\u4F1A\u5F00\u59CB\u65B0\u5BF9\u8BDD\u3002</small></p>}\n </section>\n {error && <p role=\"alert\" className=\"banso-error\">{error}</p>}\n <form onSubmit={event => { event.preventDefault(); void send(); }}>\n <label htmlFor=\"banso-input\">\u4F60\u7684\u95EE\u9898</label>\n <textarea id=\"banso-input\" placeholder=\"\u60F3\u4E86\u89E3\u4EC0\u4E48\uFF1F\" rows={3} value={input} onChange={event => setInput(event.target.value)} disabled={busy || running} />\n <div className=\"banso-actions\"><small>\u7EAF\u6587\u672C\u804A\u5929</small>\n {running && <button type=\"button\" disabled={stopping || !generation} onClick={() => void stop()}>\u505C\u6B62</button>}\n <button type=\"submit\" disabled={!input.trim() || busy || running || !generation}>{busy ? '\u53D1\u9001\u4E2D\u2026' : '\u53D1\u9001'}</button>\n </div>\n </form>\n </main>;\n}\n\nconst css = `\nhtml,body,#root{height:100%;margin:0}body{background:#f7f8fa;color:#20252c;font:16px/1.65 system-ui,sans-serif}\n.banso-app{box-sizing:border-box;max-width:900px;height:100dvh;margin:auto;padding:20px 24px;display:flex;flex-direction:column}\n.banso-app header{display:flex;align-items:center;gap:20px;border-bottom:1px solid #dfe3e8;padding-bottom:12px}.banso-app h1{font-size:24px;margin:0}.banso-app header span,.banso-app small{color:#647080;font-size:13px}\n.banso-messages{flex:1;overflow:auto;min-height:0;padding:24px 0}.banso-message{padding:14px 18px;margin:0 0 18px;border-radius:12px;background:white;overflow-wrap:anywhere}.banso-message.user{background:#e9f0fa}.banso-message strong{font-size:13px}.banso-message div{white-space:pre-wrap}.banso-empty{color:#647080;text-align:center;margin-top:15vh}.banso-empty small{display:block}\n.banso-tool{margin:0 0 12px;padding:8px 14px;border-left:3px solid #bdc7d4;font-size:14px;overflow-wrap:anywhere}.banso-tool summary{cursor:pointer}.banso-tool p{white-space:pre-wrap;margin:8px 0}.banso-tool.failed{border-color:#a52d2d}.banso-tool.running{border-color:#245ba0}.banso-tool.completed{border-color:#38714b}\n.banso-app label{display:block;font-size:13px;margin-bottom:6px}.banso-app textarea{box-sizing:border-box;width:100%;resize:vertical;max-height:30vh;border:1px solid #bdc7d4;border-radius:10px;padding:12px;font:inherit}.banso-app textarea:focus{outline:2px solid #5285c5}.banso-actions{display:flex;align-items:center;gap:12px;margin-top:10px}.banso-actions small{flex:1}.banso-app button{border:0;border-radius:8px;background:#245ba0;color:white;padding:9px 20px;font:inherit;cursor:pointer}.banso-app button:disabled{opacity:.5;cursor:default}.banso-error{color:#9f2525;background:#ffeded;padding:10px;overflow-wrap:anywhere;margin:8px 0}@media(max-width:600px){.banso-app{padding:12px}.banso-actions small{font-size:11px}}\n`;\nexport function apply(ctx: Context): void {\n ctx.effect(() => {\n const style = document.createElement('style'); style.textContent = css; document.head.append(style);\n const previousTitle = document.title; document.title = 'Banso';\n const dispose = ctx.slots.register({ name: 'root' }, () => <App ctx={ctx} />);\n return () => { dispose(); style.remove(); document.title = previousTitle; };\n });\n}\n", "import type { ContentBlock } from '@deepseek-ai/dsh-llm';\nimport type { SessionEventLikeEntry } from '@deepseek-ai/dsh-api-session-controller/client';\n\nexport interface ChatMessage { id: string; seq: number; role: 'user' | 'assistant'; text: string }\nconst textOf = (content: readonly ContentBlock[]) => content.flatMap(block => block.type === 'text' ? [block.text] : []).join('');\n\nconst hasToolCall = (content: readonly ContentBlock[]) => content.some(block => block.type === 'tool-call');\n\n/** Tool preambles use retained live rows until step/end; ordinary answers persist. */\nexport function messagesOf(entries: readonly SessionEventLikeEntry[]): ChatMessage[] {\n const messages: ChatMessage[] = [];\n const live = new Map<string, ChatMessage>();\n const settledSteps = new Map<string, number>();\n for (const { event } of entries) {\n if (event.type === 'assistant/message' && !hasToolCall(event.data.message.content)) {\n const key = `${event.data.turn}:${event.data.step}`;\n settledSteps.set(key, Math.max(settledSteps.get(key) ?? -1, event.seq));\n }\n }\n for (const { event } of entries) {\n if (event.type === 'user/message' && event.data.source.kind === 'user') {\n messages.push({ id: `user-${event.seq}`, seq: event.seq, role: 'user', text: textOf(event.data.content) });\n } else if (event.type === 'assistant/message') {\n if (hasToolCall(event.data.message.content)) continue;\n const text = textOf(event.data.message.content);\n if (text) messages.push({ id: `assistant-${event.seq}`, seq: event.seq, role: 'assistant', text });\n } else if (event.type === 'assistant/live-chunk' && event.data.chunk.type === 'text-delta') {\n const settledSeq = settledSteps.get(`${event.data.turn}:${event.data.step}`);\n if (settledSeq !== undefined && event.seq < settledSeq) continue;\n const id = event.data.attemptId;\n let message = live.get(id);\n if (!message) {\n message = { id: `live-${id}`, seq: event.seq, role: 'assistant', text: '' };\n live.set(id, message);\n messages.push(message);\n }\n message.text += event.data.chunk.text;\n }\n }\n return messages;\n}\n", "import type { SessionEventLikeEntry } from '@deepseek-ai/dsh-api-session-controller/client';\nimport { messagesOf, type ChatMessage } from './messages.js';\n\nexport interface ToolActivity {\n id: string; seq: number; turn: number; kind: 'tool'; name: string;\n status: 'running' | 'completed' | 'failed' | 'cancelled';\n label: string; target: string; detail: string; note: string; error: string;\n}\nexport type TimelineItem = (ChatMessage & { kind: 'message' }) | ToolActivity;\nconst object = (value: unknown): Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};\nconst string = (value: unknown): string => typeof value === 'string' ? value : '';\nfunction argumentsOf(value: string): Record<string, unknown> {\n try { return object(JSON.parse(value)); } catch { return {}; }\n}\nfunction failureNote(error: string): string {\n if (error.startsWith('Extraction failed:')) {\n return error.includes('Extraction did not complete: max-tokens') ? '\u5185\u5BB9\u63D0\u53D6\u8FBE\u5230\u8F93\u51FA\u4E0A\u9650' : '\u5185\u5BB9\u63D0\u53D6\u5931\u8D25';\n }\n return error.startsWith('Fetch failed:') ? '\u7F51\u9875\u6293\u53D6\u5931\u8D25' : '\u6267\u884C\u5931\u8D25';\n}\nexport function activitySummary(item: ToolActivity): string {\n const status = { running: `\u6B63\u5728${item.label}`, completed: `${item.label}\u5B8C\u6210`, failed: `${item.label}\u5931\u8D25`, cancelled: `${item.label}\u5DF2\u53D6\u6D88` }[item.status];\n return `${status}${item.target ? `\uFF1A${item.target}` : ''}${item.note ? ` \u2014 ${item.note}` : ''}`;\n}\n\n/** Rebuild from the official snapshot so replay and live updates share one path. */\nexport function timelineOf(entries: readonly SessionEventLikeEntry[]): TimelineItem[] {\n const items: TimelineItem[] = messagesOf(entries).map(message => ({ ...message, kind: 'message' }));\n const calls = new Map<string, ToolActivity>();\n const endedTurns = new Set<number>();\n for (const { event } of entries) {\n if (event.type === 'tool/call') {\n const { turn, step, callId, name } = event.data;\n if (name !== 'web_search' && name !== 'web_read') continue;\n const key = `${turn}:${step}:${callId}`;\n if (calls.has(key)) continue;\n const args = argumentsOf(event.data.arguments);\n const queries = Array.isArray(args.queries) && args.queries.every(q => typeof q === 'string') ? args.queries.join('\uFF1B') : '';\n const item: ToolActivity = {\n id: `tool-${event.seq}`, seq: event.seq, turn, kind: 'tool', name, status: 'running',\n label: name === 'web_search' ? '\u641C\u7D22' : '\u9605\u8BFB', target: name === 'web_search' ? queries : string(args.target),\n detail: string(args.focus), note: '', error: '',\n };\n calls.set(key, item); items.push(item);\n } else if (event.type === 'tool/result') {\n const { turn, step, message, error, meta } = event.data;\n const item = calls.get(`${turn}:${step}:${message.toolCallId}`);\n if (!item) continue;\n const value = object(meta);\n item.note = ''; item.error = '';\n if (error?.code === 'ABORTED' || error?.code === 'ABORTED_BEFORE_DISPATCH') {\n item.status = 'cancelled';\n } else if (message.isError || (item.name === 'web_read' && typeof value.error === 'string')) {\n item.status = 'failed';\n item.error = message.isError ? error?.reason || message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\\n') : string(value.error);\n item.note = failureNote(item.error);\n } else {\n item.status = 'completed';\n if (item.name === 'web_search' && Array.isArray(value.sources)) item.note = `\u8FD4\u56DE ${value.sources.length} \u4E2A\u6765\u6E90`;\n if (item.name === 'web_read' && typeof value.evidence === 'string') item.note = value.evidence === '' ? '\u672A\u627E\u5230\u76F8\u5173\u8BC1\u636E' : '\u5DF2\u63D0\u53D6\u76F8\u5173\u8BC1\u636E';\n }\n const details = [item.detail];\n if (typeof value.requestUrl === 'string') details.push(`URL\uFF1A${value.requestUrl}`);\n if (value.truncated === true) details.push('\u7ED3\u679C\u5DF2\u622A\u65AD');\n item.detail = details.filter(Boolean).join('\\n');\n } else if (event.type === 'turn/end') {\n endedTurns.add(event.data.turn);\n }\n }\n return items.filter(item => item.kind !== 'tool' || !endedTurns.has(item.turn)).sort((a, b) => a.seq - b.seq);\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["error"]
7
+ }
package/lib/index.js ADDED
@@ -0,0 +1,2 @@
1
+ /** Host loader entry; the interface runs in the browser. */
2
+ export function apply() { }
@@ -0,0 +1,21 @@
1
+ import type { SessionEventLikeEntry } from '@deepseek-ai/dsh-api-session-controller/client';
2
+ import { type ChatMessage } from './messages.js';
3
+ export interface ToolActivity {
4
+ id: string;
5
+ seq: number;
6
+ turn: number;
7
+ kind: 'tool';
8
+ name: string;
9
+ status: 'running' | 'completed' | 'failed' | 'cancelled';
10
+ label: string;
11
+ target: string;
12
+ detail: string;
13
+ note: string;
14
+ error: string;
15
+ }
16
+ export type TimelineItem = (ChatMessage & {
17
+ kind: 'message';
18
+ }) | ToolActivity;
19
+ export declare function activitySummary(item: ToolActivity): string;
20
+ /** Rebuild from the official snapshot so replay and live updates share one path. */
21
+ export declare function timelineOf(entries: readonly SessionEventLikeEntry[]): TimelineItem[];
@@ -0,0 +1,80 @@
1
+ import { messagesOf } from './messages.js';
2
+ const object = (value) => typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {};
3
+ const string = (value) => typeof value === 'string' ? value : '';
4
+ function argumentsOf(value) {
5
+ try {
6
+ return object(JSON.parse(value));
7
+ }
8
+ catch {
9
+ return {};
10
+ }
11
+ }
12
+ function failureNote(error) {
13
+ if (error.startsWith('Extraction failed:')) {
14
+ return error.includes('Extraction did not complete: max-tokens') ? '内容提取达到输出上限' : '内容提取失败';
15
+ }
16
+ return error.startsWith('Fetch failed:') ? '网页抓取失败' : '执行失败';
17
+ }
18
+ export function activitySummary(item) {
19
+ const status = { running: `正在${item.label}`, completed: `${item.label}完成`, failed: `${item.label}失败`, cancelled: `${item.label}已取消` }[item.status];
20
+ return `${status}${item.target ? `:${item.target}` : ''}${item.note ? ` — ${item.note}` : ''}`;
21
+ }
22
+ /** Rebuild from the official snapshot so replay and live updates share one path. */
23
+ export function timelineOf(entries) {
24
+ const items = messagesOf(entries).map(message => ({ ...message, kind: 'message' }));
25
+ const calls = new Map();
26
+ const endedTurns = new Set();
27
+ for (const { event } of entries) {
28
+ if (event.type === 'tool/call') {
29
+ const { turn, step, callId, name } = event.data;
30
+ if (name !== 'web_search' && name !== 'web_read')
31
+ continue;
32
+ const key = `${turn}:${step}:${callId}`;
33
+ if (calls.has(key))
34
+ continue;
35
+ const args = argumentsOf(event.data.arguments);
36
+ const queries = Array.isArray(args.queries) && args.queries.every(q => typeof q === 'string') ? args.queries.join(';') : '';
37
+ const item = {
38
+ id: `tool-${event.seq}`, seq: event.seq, turn, kind: 'tool', name, status: 'running',
39
+ label: name === 'web_search' ? '搜索' : '阅读', target: name === 'web_search' ? queries : string(args.target),
40
+ detail: string(args.focus), note: '', error: '',
41
+ };
42
+ calls.set(key, item);
43
+ items.push(item);
44
+ }
45
+ else if (event.type === 'tool/result') {
46
+ const { turn, step, message, error, meta } = event.data;
47
+ const item = calls.get(`${turn}:${step}:${message.toolCallId}`);
48
+ if (!item)
49
+ continue;
50
+ const value = object(meta);
51
+ item.note = '';
52
+ item.error = '';
53
+ if (error?.code === 'ABORTED' || error?.code === 'ABORTED_BEFORE_DISPATCH') {
54
+ item.status = 'cancelled';
55
+ }
56
+ else if (message.isError || (item.name === 'web_read' && typeof value.error === 'string')) {
57
+ item.status = 'failed';
58
+ item.error = message.isError ? error?.reason || message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n') : string(value.error);
59
+ item.note = failureNote(item.error);
60
+ }
61
+ else {
62
+ item.status = 'completed';
63
+ if (item.name === 'web_search' && Array.isArray(value.sources))
64
+ item.note = `返回 ${value.sources.length} 个来源`;
65
+ if (item.name === 'web_read' && typeof value.evidence === 'string')
66
+ item.note = value.evidence === '' ? '未找到相关证据' : '已提取相关证据';
67
+ }
68
+ const details = [item.detail];
69
+ if (typeof value.requestUrl === 'string')
70
+ details.push(`URL:${value.requestUrl}`);
71
+ if (value.truncated === true)
72
+ details.push('结果已截断');
73
+ item.detail = details.filter(Boolean).join('\n');
74
+ }
75
+ else if (event.type === 'turn/end') {
76
+ endedTurns.add(event.data.turn);
77
+ }
78
+ }
79
+ return items.filter(item => item.kind !== 'tool' || !endedTurns.has(item.turn)).sort((a, b) => a.seq - b.seq);
80
+ }
@@ -0,0 +1,8 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ declare module '@deepseek-ai/dsh-api-session-controller/client' {
3
+ interface SessionReferenceSourceMap {
4
+ bansoChat: unknown;
5
+ }
6
+ }
7
+ export declare const inject: string[];
8
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,122 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
3
+ import { timelineOf, activitySummary } from './activity.js';
4
+ export const inject = ['slots', 'sessions', 'connection'];
5
+ function Conversation({ binding }) {
6
+ const events = useSyncExternalStore(listener => binding.eventSource.subscribe(listener), () => binding.eventSource.getSnapshot());
7
+ const state = useSyncExternalStore(listener => binding.session.subscribe(listener), () => binding.session.getSnapshot());
8
+ const end = useRef(null);
9
+ useEffect(() => { end.current?.scrollIntoView({ block: 'end' }); }, [events]);
10
+ const messages = timelineOf(events.entries);
11
+ return _jsxs(_Fragment, { children: [messages.map(message => message.kind === 'tool' ? _jsxs("details", { className: `banso-tool ${message.status}`, children: [_jsxs("summary", { children: [_jsx("span", { "aria-hidden": "true", children: { running: '◌', completed: '✓', failed: '✗', cancelled: '○' }[message.status] }), " ", activitySummary(message)] }), _jsxs("div", { children: [message.target && _jsxs("p", { children: [message.name === 'web_search' ? '关键词' : '阅读对象', "\uFF1A", message.target] }), message.detail && _jsx("p", { children: message.detail }), message.error && _jsxs("p", { children: ["\u9519\u8BEF\u8BE6\u60C5\uFF1A", message.error] }), !message.target && !message.detail && !message.error && _jsx("p", { children: "\u6CA1\u6709\u66F4\u591A\u8BE6\u60C5" })] })] }, message.id) : _jsxs("article", { className: `banso-message ${message.role}`, children: [_jsx("strong", { children: message.role === 'user' ? '你' : 'Banso' }), _jsx("div", { children: message.text })] }, message.id)), state.pendingSubmissions.map(pending => _jsxs("article", { className: "banso-message user", children: [_jsx("strong", { children: "\u4F60 \u00B7 \u53D1\u9001\u4E2D" }), _jsx("div", { children: pending.text })] }, pending.requestId)), _jsx("div", { ref: end })] });
12
+ }
13
+ function App({ ctx }) {
14
+ const [binding, setBinding] = useState();
15
+ const [input, setInput] = useState('');
16
+ const [busy, setBusy] = useState(false);
17
+ const [stopping, setStopping] = useState(false);
18
+ const [error, setError] = useState('');
19
+ const [running, setRunning] = useState(false);
20
+ const reference = useRef();
21
+ const alive = useRef(true);
22
+ const inFlight = useRef(false);
23
+ const connection = ctx.get('connection');
24
+ const generation = useSyncExternalStore(listener => connection.generation.subscribe(listener), () => connection.generation.getSnapshot());
25
+ useEffect(() => () => { alive.current = false; reference.current?.release(); }, []);
26
+ useEffect(() => {
27
+ if (!binding)
28
+ return;
29
+ const update = () => {
30
+ const state = binding.session.getSnapshot();
31
+ setRunning(state.running || state.pendingSubmissions.length > 0 || state.awaitingFirstTurn);
32
+ if (!state.running)
33
+ setStopping(false);
34
+ const failure = state.promptError?.error.message ?? state.openError?.message ?? state.lastAgentError;
35
+ if (failure)
36
+ setError(failure);
37
+ };
38
+ update();
39
+ return binding.session.subscribe(update);
40
+ }, [binding]);
41
+ async function send() {
42
+ const text = input.trim();
43
+ if (!text || inFlight.current || running || !generation)
44
+ return;
45
+ inFlight.current = true;
46
+ setBusy(true);
47
+ setError('');
48
+ try {
49
+ let current = binding;
50
+ if (!current) {
51
+ const id = await ctx.sessions.create();
52
+ if (!alive.current)
53
+ return;
54
+ const held = ctx.sessions.retain(id, { source: 'bansoChat' });
55
+ reference.current = held;
56
+ try {
57
+ current = await held.ready;
58
+ }
59
+ catch (error) {
60
+ held.release();
61
+ reference.current = undefined;
62
+ throw error;
63
+ }
64
+ if (!alive.current)
65
+ return;
66
+ setBinding(current);
67
+ }
68
+ const submission = current.session.beginSubmission({ mode: 'queue', text, attachments: [] });
69
+ const result = await current.session.prompt([{ type: 'text', text }], 'queue', undefined, submission.requestId);
70
+ if (!result.ok)
71
+ throw new Error(result.error.message);
72
+ if (alive.current)
73
+ setInput('');
74
+ }
75
+ catch (error) {
76
+ if (alive.current)
77
+ setError(error instanceof Error ? error.message : String(error));
78
+ }
79
+ finally {
80
+ inFlight.current = false;
81
+ if (alive.current)
82
+ setBusy(false);
83
+ }
84
+ }
85
+ async function stop() {
86
+ if (!binding || stopping)
87
+ return;
88
+ setStopping(true);
89
+ setError('');
90
+ try {
91
+ const result = await binding.session.cancel();
92
+ if (!result.ok)
93
+ throw new Error(result.error.message);
94
+ }
95
+ catch (error) {
96
+ if (alive.current) {
97
+ setStopping(false);
98
+ setError(error instanceof Error ? error.message : String(error));
99
+ }
100
+ }
101
+ }
102
+ return _jsxs("main", { className: "banso-app", children: [_jsxs("header", { children: [_jsx("h1", { children: "Banso" }), _jsx("span", { children: !generation ? '连接中…' : stopping ? '正在停止…' : running ? '正在研究…' : '新闻研究助手' })] }), _jsx("section", { className: "banso-messages", "aria-label": "\u804A\u5929\u6D88\u606F", children: binding ? _jsx(Conversation, { binding: binding }) : _jsxs("p", { className: "banso-empty", children: ["\u8F93\u5165\u4E00\u4E2A\u95EE\u9898\uFF0C\u5F00\u59CB\u7814\u7A76\u3002", _jsx("small", { children: "\u6B64\u7248\u672C\u5237\u65B0\u540E\u4F1A\u5F00\u59CB\u65B0\u5BF9\u8BDD\u3002" })] }) }), error && _jsx("p", { role: "alert", className: "banso-error", children: error }), _jsxs("form", { onSubmit: event => { event.preventDefault(); void send(); }, children: [_jsx("label", { htmlFor: "banso-input", children: "\u4F60\u7684\u95EE\u9898" }), _jsx("textarea", { id: "banso-input", placeholder: "\u60F3\u4E86\u89E3\u4EC0\u4E48\uFF1F", rows: 3, value: input, onChange: event => setInput(event.target.value), disabled: busy || running }), _jsxs("div", { className: "banso-actions", children: [_jsx("small", { children: "\u7EAF\u6587\u672C\u804A\u5929" }), running && _jsx("button", { type: "button", disabled: stopping || !generation, onClick: () => void stop(), children: "\u505C\u6B62" }), _jsx("button", { type: "submit", disabled: !input.trim() || busy || running || !generation, children: busy ? '发送中…' : '发送' })] })] })] });
103
+ }
104
+ const css = `
105
+ html,body,#root{height:100%;margin:0}body{background:#f7f8fa;color:#20252c;font:16px/1.65 system-ui,sans-serif}
106
+ .banso-app{box-sizing:border-box;max-width:900px;height:100dvh;margin:auto;padding:20px 24px;display:flex;flex-direction:column}
107
+ .banso-app header{display:flex;align-items:center;gap:20px;border-bottom:1px solid #dfe3e8;padding-bottom:12px}.banso-app h1{font-size:24px;margin:0}.banso-app header span,.banso-app small{color:#647080;font-size:13px}
108
+ .banso-messages{flex:1;overflow:auto;min-height:0;padding:24px 0}.banso-message{padding:14px 18px;margin:0 0 18px;border-radius:12px;background:white;overflow-wrap:anywhere}.banso-message.user{background:#e9f0fa}.banso-message strong{font-size:13px}.banso-message div{white-space:pre-wrap}.banso-empty{color:#647080;text-align:center;margin-top:15vh}.banso-empty small{display:block}
109
+ .banso-tool{margin:0 0 12px;padding:8px 14px;border-left:3px solid #bdc7d4;font-size:14px;overflow-wrap:anywhere}.banso-tool summary{cursor:pointer}.banso-tool p{white-space:pre-wrap;margin:8px 0}.banso-tool.failed{border-color:#a52d2d}.banso-tool.running{border-color:#245ba0}.banso-tool.completed{border-color:#38714b}
110
+ .banso-app label{display:block;font-size:13px;margin-bottom:6px}.banso-app textarea{box-sizing:border-box;width:100%;resize:vertical;max-height:30vh;border:1px solid #bdc7d4;border-radius:10px;padding:12px;font:inherit}.banso-app textarea:focus{outline:2px solid #5285c5}.banso-actions{display:flex;align-items:center;gap:12px;margin-top:10px}.banso-actions small{flex:1}.banso-app button{border:0;border-radius:8px;background:#245ba0;color:white;padding:9px 20px;font:inherit;cursor:pointer}.banso-app button:disabled{opacity:.5;cursor:default}.banso-error{color:#9f2525;background:#ffeded;padding:10px;overflow-wrap:anywhere;margin:8px 0}@media(max-width:600px){.banso-app{padding:12px}.banso-actions small{font-size:11px}}
111
+ `;
112
+ export function apply(ctx) {
113
+ ctx.effect(() => {
114
+ const style = document.createElement('style');
115
+ style.textContent = css;
116
+ document.head.append(style);
117
+ const previousTitle = document.title;
118
+ document.title = 'Banso';
119
+ const dispose = ctx.slots.register({ name: 'root' }, () => _jsx(App, { ctx: ctx }));
120
+ return () => { dispose(); style.remove(); document.title = previousTitle; };
121
+ });
122
+ }
@@ -0,0 +1,9 @@
1
+ import type { SessionEventLikeEntry } from '@deepseek-ai/dsh-api-session-controller/client';
2
+ export interface ChatMessage {
3
+ id: string;
4
+ seq: number;
5
+ role: 'user' | 'assistant';
6
+ text: string;
7
+ }
8
+ /** Tool preambles use retained live rows until step/end; ordinary answers persist. */
9
+ export declare function messagesOf(entries: readonly SessionEventLikeEntry[]): ChatMessage[];
@@ -0,0 +1,40 @@
1
+ const textOf = (content) => content.flatMap(block => block.type === 'text' ? [block.text] : []).join('');
2
+ const hasToolCall = (content) => content.some(block => block.type === 'tool-call');
3
+ /** Tool preambles use retained live rows until step/end; ordinary answers persist. */
4
+ export function messagesOf(entries) {
5
+ const messages = [];
6
+ const live = new Map();
7
+ const settledSteps = new Map();
8
+ for (const { event } of entries) {
9
+ if (event.type === 'assistant/message' && !hasToolCall(event.data.message.content)) {
10
+ const key = `${event.data.turn}:${event.data.step}`;
11
+ settledSteps.set(key, Math.max(settledSteps.get(key) ?? -1, event.seq));
12
+ }
13
+ }
14
+ for (const { event } of entries) {
15
+ if (event.type === 'user/message' && event.data.source.kind === 'user') {
16
+ messages.push({ id: `user-${event.seq}`, seq: event.seq, role: 'user', text: textOf(event.data.content) });
17
+ }
18
+ else if (event.type === 'assistant/message') {
19
+ if (hasToolCall(event.data.message.content))
20
+ continue;
21
+ const text = textOf(event.data.message.content);
22
+ if (text)
23
+ messages.push({ id: `assistant-${event.seq}`, seq: event.seq, role: 'assistant', text });
24
+ }
25
+ else if (event.type === 'assistant/live-chunk' && event.data.chunk.type === 'text-delta') {
26
+ const settledSeq = settledSteps.get(`${event.data.turn}:${event.data.step}`);
27
+ if (settledSeq !== undefined && event.seq < settledSeq)
28
+ continue;
29
+ const id = event.data.attemptId;
30
+ let message = live.get(id);
31
+ if (!message) {
32
+ message = { id: `live-${id}`, seq: event.seq, role: 'assistant', text: '' };
33
+ live.set(id, message);
34
+ messages.push(message);
35
+ }
36
+ message.text += event.data.chunk.text;
37
+ }
38
+ }
39
+ return messages;
40
+ }
@@ -0,0 +1,2 @@
1
+ /** Host loader entry; the interface runs in the browser. */
2
+ export declare function apply(): void;
@@ -0,0 +1,2 @@
1
+ /** Host loader entry; the interface runs in the browser. */
2
+ export function apply() { }
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "banso-dsh-ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "exports": {
7
+ ".": "./lib/index.js",
8
+ "./client": {
9
+ "types": "./lib/types/client/index.d.ts",
10
+ "default": "./lib/client.js"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "lib"
16
+ ],
17
+ "dsh": {
18
+ "client": {
19
+ "platform": "web",
20
+ "immediately": true,
21
+ "inject": [
22
+ "@deepseek-ai/dsh-api-session-controller",
23
+ "@deepseek-ai/dsh-client-ui-renderer"
24
+ ]
25
+ }
26
+ },
27
+ "peerDependencies": {
28
+ "@deepseek-ai/cordis": "^4.0.4"
29
+ },
30
+ "devDependencies": {
31
+ "@deepseek-ai/cordis": "^4.0.4",
32
+ "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2",
33
+ "@deepseek-ai/dsh-api-remotes": "0.2.0-rc.2",
34
+ "@deepseek-ai/dsh-client-connection": "0.2.0-rc.2",
35
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
36
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
37
+ "@deepseek-ai/dsh-session": "0.2.0-rc.2",
38
+ "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
39
+ "@types/react": "^18.3.0",
40
+ "@types/node": "^24.19.0",
41
+ "react": "^18.2.0",
42
+ "typescript": "^7.0.2",
43
+ "esbuild": "^0.25.0"
44
+ },
45
+ "description": "Banso chat frontend plugin for DeepSeek Harness",
46
+ "scripts": {
47
+ "build": "tsc -p tsconfig.json && node scripts/build.mjs",
48
+ "typecheck": "tsc -p tsconfig.json --noEmit",
49
+ "test": "node --test tests/*.test.mjs",
50
+ "clean": "node -e \"require('node:fs').rmSync('lib', { recursive: true, force: true })\""
51
+ }
6
52
  }