dsh-todo-inbox 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/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # DSH 待办插件 dsh-todo-inbox
2
+
3
+ 一个 DeepSeek Harness Web 插件:任何会话的 agent 都能把"等待人类处理"的事(合并 MR、审批工单、评审、发布……)登记到一个全局待办收件箱,你在侧边栏里统一查看和处理,不再因为并行多个会话而漏掉。
4
+
5
+ ## 功能
6
+
7
+ - **侧边栏入口**:宽侧边栏底部显示"待办"区块(未完成数角标),点击标题折叠/展开;窄侧边栏是圆形按钮,点击展开侧边栏。
8
+ - **处理待办**:待办标题若是外部链接则点击打开;详情内容点击跳转到登记它的来源会话;hover 显示 完成 ✓ / 删除 ✕。
9
+ - **跨会话共享**:所有会话的 agent 通过 `inbox_add` / `inbox_done` / `inbox_list` 工具读写同一个收件箱;数据落盘 `~/.dsh/todo-inbox.json`,进程重启不丢失。
10
+ - **detail 支持简单 markdown**:`**粗体**`、`*斜体*`、`` `代码` ``、`[文字](url)`、裸链接自动识别。
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ cd ~/dsh-todo-inbox && pnpm install && pnpm run build
16
+ dsh plugin --profile web add ~/dsh-todo-inbox
17
+ # 重启 dsh web,侧边栏底部出现"待办"区块即成功
18
+ ```
19
+
20
+ ## 使用
21
+
22
+ **给人看**:待办出现在侧边栏(宽态为会话列表下方的区块,窄态为底部圆形按钮),角标是未完成数;hover 条目点 ✓ 完成、✕ 删除,点标题打开关联链接、点详情跳转来源会话。
23
+
24
+ **给 agent 用**:在任何会话里让 agent 登记待办,例如:
25
+
26
+ > "帮我登记一条待办:合并 xxx 仓库的 MR !1234"
27
+
28
+ agent 会调用 `inbox_add` 写入收件箱;你也可以让 agent 用 `inbox_list` 检查当前有几条待办。agent 也可以直接编辑 `~/.dsh/todo-inbox.json`(往 `items` 数组追加一条记录),5 秒内自动同步到界面。
@@ -0,0 +1,20 @@
1
+ # dsh-todo-inbox bundle patch
2
+ #
3
+ # This file is the `dsh.bundle.patch` layer of the package: when the plugin is
4
+ # installed through the official CLI —
5
+ #
6
+ # dsh plugin --profile <name> add <path-or-name>
7
+ #
8
+ # — the command reconciles `dsh.profile.bundles` against installed packages
9
+ # and, seeing this declaration, appends the package to the bundle stack. The
10
+ # profile boot then merges THIS patch (a single `insert` of the plugin row)
11
+ # into the composition: the host half mounts process-wide and registers the
12
+ # inbox_* tools for every session; the browser half is served as
13
+ # /plugins/todo-inbox/client.js and registers the sidebar footer action.
14
+ #
15
+ # Do NOT add a second manual mount line in the profile's own cordis.patch.yml
16
+ # — one mount only (two mounts register /todo-inbox/api twice and fail the
17
+ # whole plugin tree at boot with "duplicate prefix route").
18
+ - insert:
19
+ - id: todo-inbox
20
+ name: 'dsh-todo-inbox'
package/lib/client.js ADDED
@@ -0,0 +1,479 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-todo-inbox",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region \0rolldown/runtime.js
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
+ key = keys[i];
17
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
18
+ get: ((k) => from[k]).bind(null, key),
19
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
+ });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
25
+ value: mod,
26
+ enumerable: true
27
+ }) : target, mod));
28
+ //#endregion
29
+ let react = require("react");
30
+ react = __toESM(react, 1);
31
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
32
+ let react_jsx_runtime = require("react/jsx-runtime");
33
+ //#region src/client/index.tsx
34
+ /**
35
+ * dsh-todo-inbox browser half: the global inbox, embedded in the sidebar.
36
+ *
37
+ * Wide sidebar: a collapsible section stacked above Settings — a header row
38
+ * (checklist glyph + label + count badge) and an inline list of pending
39
+ * items with hover-revealed icon actions. Rail sidebar: the standard 36×36
40
+ * circle button with a corner badge; clicking it expands the sidebar (the
41
+ * rail has no room for a list).
42
+ *
43
+ * All state lives in a module-level store, so the sidebar's wide↔rail
44
+ * crossfade (which remounts the slot occupant) never loses data or
45
+ * expansion. Polling starts once at apply time.
46
+ *
47
+ * `detail` renders a tiny safe markdown subset (bold / italic / inline code /
48
+ * [text](url) / bare URLs / line breaks) as React elements — never raw HTML.
49
+ *
50
+ * The bundle is compiled to a window.__ModuleLoader__ closure; react and the
51
+ * ui-primitives icons resolve through the module table at runtime, so this
52
+ * file must not runtime-import anything else (type-only imports are erased).
53
+ *
54
+ * @module dsh-todo-inbox/client
55
+ */
56
+ const name = "todo-inbox-client";
57
+ const inject = [
58
+ "slots",
59
+ "sessions",
60
+ "layout"
61
+ ];
62
+ const API = "/todo-inbox/api";
63
+ /**
64
+ * Design notes:
65
+ * - The inline section mirrors the sidebar's own geometry: the header row
66
+ * uses the Settings trigger's padding/gap/text (8px gap, 14px label), rows
67
+ * use the 8px hover-radius family, and every color is a semantic alias
68
+ * token with a light-dark() fallback.
69
+ * - Rows show a one-line title; detail (markdown-rendered) clamps to two
70
+ * lines; actions are icon buttons revealed on row hover / focus-within.
71
+ * - The rail-mode panel is a menu surface (--dsw-specific-menu + l4 stroke
72
+ * ring + prominent shadow, the ContextMeter recipe), draggable by its
73
+ * header, with a 200ms rise-and-fade entrance.
74
+ */
75
+ const CSS = `
76
+ .tib-root { position: relative; }
77
+ .tib-root.wide { width: calc(100% + 4px); margin: 4px -2px; }
78
+ .tib-root.rail { width: 36px; margin: 8px 0 10px; }
79
+
80
+ /* ── wide mode: inline section ─────────────────────────────────────── */
81
+ .tib-section { padding-top: 2px; }
82
+ .tib-section-head {
83
+ display: flex; align-items: center; gap: 8px; width: 100%;
84
+ height: 32px; padding: 0 10px 0 8px; box-sizing: border-box;
85
+ border: none; border-radius: 10px; background: transparent; cursor: pointer;
86
+ color: var(--dsw-alias-label-primary, light-dark(#333, #ccc));
87
+ font-family: inherit; font-size: 14px; line-height: 22px;
88
+ }
89
+ .tib-section-head:hover {
90
+ background: var(--dsw-alias-interactive-bg-hover, light-dark(rgba(0,0,0,.05), rgba(255,255,255,.07)));
91
+ }
92
+ .tib-head-icon { flex: none; display: grid; place-items: center; color: var(--dsw-alias-label-secondary, light-dark(#555, #bbb)); }
93
+ .tib-head-label { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
94
+ .tib-chevron { /* unused: header matches the plain icon+label Settings row */ }
95
+ margin-left: auto; flex: none; display: grid; place-items: center;
96
+ color: var(--dsw-alias-label-tertiary, light-dark(#888, #999));
97
+ }
98
+ .tib-badge {
99
+ min-width: 18px; padding: 0 6px; height: 16px; line-height: 16px;
100
+ border-radius: 999px; text-align: center; font-size: 11px; font-weight: 600;
101
+ background: var(--dsw-alias-brand-primary, light-dark(#4259d1, #7a9bff));
102
+ color: var(--dsw-alias-brand-primary-invert, #fff);
103
+ }
104
+ .tib-list-inline {
105
+ max-height: min(260px, 32vh); overflow-y: auto; padding: 2px 2px 4px;
106
+ }
107
+ .tib-empty-inline {
108
+ padding: 8px 10px; font-size: .88em;
109
+ color: var(--dsw-alias-label-tertiary, light-dark(#888, #999));
110
+ }
111
+ .tib-row { padding: 5px 8px 5px 32px; border-radius: 8px; }
112
+ .tib-row:hover {
113
+ background: var(--dsw-alias-interactive-bg-hover, light-dark(rgba(0,0,0,.04), rgba(255,255,255,.06)));
114
+ }
115
+ .tib-row-top { display: flex; align-items: center; gap: 4px; }
116
+ .tib-row-title {
117
+ flex: 1; min-width: 0; font-weight: 500; overflow: hidden;
118
+ text-overflow: ellipsis; white-space: nowrap;
119
+ }
120
+ a.tib-row-title { color: inherit; text-decoration: none; }
121
+ a.tib-row-title:hover { color: var(--dsw-alias-link, light-dark(#1a73e8, #8ab4f8)); }
122
+ .tib-row-actions {
123
+ display: flex; gap: 2px; flex: none;
124
+ opacity: 0; transition: opacity 120ms ease;
125
+ }
126
+ .tib-row:hover .tib-row-actions, .tib-row:focus-within .tib-row-actions { opacity: 1; }
127
+ .tib-icon-btn {
128
+ display: grid; place-items: center; width: 28px; height: 28px; padding: 0;
129
+ border: none; border-radius: 50%; corner-shape: round;
130
+ background: transparent; cursor: pointer;
131
+ color: var(--dsw-alias-label-tertiary, light-dark(#777, #999));
132
+ }
133
+ .tib-icon-btn:hover {
134
+ background: var(--dsw-alias-interactive-bg-hover, light-dark(rgba(0,0,0,.05), rgba(255,255,255,.07)));
135
+ color: var(--dsw-alias-label-primary, light-dark(#222, #eee));
136
+ }
137
+ .tib-icon-btn.danger:hover {
138
+ color: var(--dsw-alias-state-error-primary, light-dark(#d93025, #f28b82));
139
+ background: var(--dsw-alias-interactive-bg-hover-danger, transparent);
140
+ }
141
+ .tib-row-detail {
142
+ margin-top: 1px; font-size: .86em; line-height: 1.45;
143
+ color: var(--dsw-alias-label-secondary, light-dark(#555, #bbb));
144
+ white-space: pre-wrap; word-break: break-word;
145
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
146
+ }
147
+ .tib-row-detail-session { cursor: pointer; }
148
+ .tib-row-detail-session:hover { color: var(--dsw-alias-label-primary, light-dark(#333, #ccc)); }
149
+ .tib-row-meta {
150
+ margin-top: 2px; font-size: .8em; letter-spacing: .01em;
151
+ color: var(--dsw-alias-label-tertiary, light-dark(#999, #888));
152
+ }
153
+ .tib-link { color: var(--dsw-alias-link, light-dark(#1a73e8, #8ab4f8)); text-decoration: none; }
154
+ .tib-link:hover { text-decoration: underline; }
155
+ .tib-code {
156
+ font-family: var(--ds-font-family-code, ui-monospace, monospace);
157
+ font-size: .92em; padding: 0 4px; border-radius: 4px;
158
+ background: var(--dsw-alias-bg-base, light-dark(#f0f0f0, #333));
159
+ }
160
+
161
+ /* ── rail mode: circle button only (click expands the sidebar) ─────── */
162
+ .tib-bell {
163
+ display: flex; align-items: center; justify-content: center;
164
+ width: 36px; height: 36px; padding: 0;
165
+ border: none; border-radius: 50%; corner-shape: round;
166
+ background: transparent; cursor: pointer;
167
+ color: var(--dsw-alias-label-primary, light-dark(#333, #ccc));
168
+ }
169
+ .tib-bell:hover {
170
+ background: var(--dsw-alias-interactive-bg-hover, light-dark(rgba(0,0,0,.05), rgba(255,255,255,.07)));
171
+ }
172
+ .tib-bell .tib-badge {
173
+ position: absolute; top: 1px; right: 1px;
174
+ min-width: 14px; height: 14px; line-height: 14px; padding: 0 4px; font-size: 9px;
175
+ }
176
+ `;
177
+ async function listItems() {
178
+ const response = await fetch(`${API}/items`);
179
+ if (!response.ok) throw new Error(`inbox list failed: ${response.status}`);
180
+ return await response.json();
181
+ }
182
+ async function mutate(method, body) {
183
+ const response = await fetch(`${API}/${method}`, {
184
+ method: "POST",
185
+ headers: { "content-type": "application/json" },
186
+ body: JSON.stringify(body)
187
+ });
188
+ if (!response.ok) throw new Error(`inbox ${method} failed: ${response.status}`);
189
+ }
190
+ function isSafeUrl(href) {
191
+ return /^(https?:|mailto:)/i.test(href);
192
+ }
193
+ /**
194
+ * Render one detail string as React nodes. Supported: `inline code`, **bold**,
195
+ * *italic*, [text](url), bare https?:// URLs, and line breaks (white-space:
196
+ * pre-wrap). Anything unmatched renders as plain text — never HTML.
197
+ * Exported for the smoke test; the loader only consumes name/inject/apply.
198
+ */
199
+ function renderDetail(text, keyBase) {
200
+ const out = [];
201
+ let rest = text;
202
+ let key = 0;
203
+ while (rest.length > 0) {
204
+ const code = /^`([^`]+)`/.exec(rest);
205
+ if (code !== null) {
206
+ out.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
207
+ className: "tib-code",
208
+ children: code[1]
209
+ }, `${keyBase}-${key}`));
210
+ key += 1;
211
+ rest = rest.slice(code[0].length);
212
+ continue;
213
+ }
214
+ const link = /^\[([^\]]+)\]\(([^)\s]+)\)/.exec(rest);
215
+ if (link !== null) {
216
+ const href = link[2] ?? "";
217
+ if (isSafeUrl(href)) out.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
218
+ className: "tib-link",
219
+ href,
220
+ target: "_blank",
221
+ rel: "noreferrer",
222
+ children: link[1]
223
+ }, `${keyBase}-${key}`));
224
+ else out.push(link[0]);
225
+ key += 1;
226
+ rest = rest.slice(link[0].length);
227
+ continue;
228
+ }
229
+ const bold = /^\*\*([^*]+)\*\*/.exec(rest);
230
+ if (bold !== null) {
231
+ out.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: renderDetail(bold[1] ?? "", `${keyBase}-${key}`) }, `${keyBase}-${key}`));
232
+ key += 1;
233
+ rest = rest.slice(bold[0].length);
234
+ continue;
235
+ }
236
+ const italic = /^\*([^*]+)\*/.exec(rest);
237
+ if (italic !== null) {
238
+ out.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", { children: renderDetail(italic[1] ?? "", `${keyBase}-${key}`) }, `${keyBase}-${key}`));
239
+ key += 1;
240
+ rest = rest.slice(italic[0].length);
241
+ continue;
242
+ }
243
+ const url = /^https?:\/\/[^\s<>"')\]]+/.exec(rest);
244
+ if (url !== null) {
245
+ const href = url[0];
246
+ out.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
247
+ className: "tib-link",
248
+ href,
249
+ target: "_blank",
250
+ rel: "noreferrer",
251
+ children: href
252
+ }, `${keyBase}-${key}`));
253
+ key += 1;
254
+ rest = rest.slice(href.length);
255
+ continue;
256
+ }
257
+ const nextSpecial = rest.search(/(`|\*|\[|https?:\/\/)/);
258
+ if (nextSpecial <= 0) {
259
+ out.push(rest.slice(0, 1));
260
+ rest = rest.slice(1);
261
+ } else {
262
+ out.push(rest.slice(0, nextSpecial));
263
+ rest = rest.slice(nextSpecial);
264
+ }
265
+ }
266
+ return out;
267
+ }
268
+ /** Jump to the session that recorded this item (source = session id). */
269
+ function openSourceSession(source) {
270
+ if (source === "" || inboxStore.sessions === null) return;
271
+ try {
272
+ inboxStore.sessions.open(source);
273
+ } catch (error) {
274
+ console.error("[todo-inbox] open source session failed", error);
275
+ }
276
+ }
277
+ const inboxStore = {
278
+ current: {
279
+ state: {
280
+ ok: true,
281
+ pending: 0,
282
+ items: []
283
+ },
284
+ loaded: false,
285
+ wide: true,
286
+ sectionOpen: true
287
+ },
288
+ listeners: /* @__PURE__ */ new Set(),
289
+ sessions: null,
290
+ layout: null
291
+ };
292
+ function storeUpdate(patch) {
293
+ inboxStore.current = {
294
+ ...inboxStore.current,
295
+ ...patch
296
+ };
297
+ for (const listener of inboxStore.listeners) listener();
298
+ }
299
+ function useInboxStore() {
300
+ const [, force] = react.default.useReducer((x) => x + 1, 0);
301
+ react.default.useEffect(() => {
302
+ inboxStore.listeners.add(force);
303
+ return () => {
304
+ inboxStore.listeners.delete(force);
305
+ };
306
+ }, []);
307
+ return inboxStore.current;
308
+ }
309
+ let pollTimer = null;
310
+ async function refreshInbox() {
311
+ try {
312
+ storeUpdate({
313
+ state: await listItems(),
314
+ loaded: true
315
+ });
316
+ } catch (error) {
317
+ console.error("[todo-inbox] list failed", error);
318
+ }
319
+ }
320
+ function startPolling() {
321
+ if (pollTimer !== null) return;
322
+ refreshInbox();
323
+ pollTimer = setInterval(() => {
324
+ refreshInbox();
325
+ }, 1e4);
326
+ }
327
+ async function act(method, id) {
328
+ try {
329
+ await mutate(method, { id });
330
+ } catch (error) {
331
+ console.error(`[todo-inbox] ${method} failed`, error);
332
+ }
333
+ refreshInbox();
334
+ }
335
+ function itemMeta(item) {
336
+ return [
337
+ item.type,
338
+ item.source ? `来自 ${item.source.startsWith("session-") ? item.source.slice(8) : item.source}` : null,
339
+ item.dueAt ? `截止 ${item.dueAt.replace("T", " ").slice(0, 16)}` : null
340
+ ].filter(Boolean).join(" · ");
341
+ }
342
+ function InlineSection() {
343
+ const { state, loaded, sectionOpen } = useInboxStore();
344
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
345
+ className: "tib-section",
346
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
347
+ className: "tib-section-head",
348
+ onClick: () => storeUpdate({ sectionOpen: !sectionOpen }),
349
+ title: "待办",
350
+ children: [
351
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
352
+ className: "tib-head-icon",
353
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChecklistOutline14, { size: 16 })
354
+ }),
355
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
356
+ className: "tib-head-label",
357
+ children: "待办"
358
+ }),
359
+ state.pending > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
360
+ className: "tib-badge",
361
+ children: String(state.pending)
362
+ }) : null
363
+ ]
364
+ }), sectionOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
365
+ className: "tib-list-inline",
366
+ children: !loaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
367
+ className: "tib-empty-inline",
368
+ children: "加载中…"
369
+ }) : state.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
370
+ className: "tib-empty-inline",
371
+ children: "暂无"
372
+ }) : state.items.map((item) => {
373
+ const meta = itemMeta(item);
374
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
375
+ className: "tib-row",
376
+ children: [
377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
378
+ className: "tib-row-top",
379
+ children: [item.link ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
380
+ className: "tib-row-title",
381
+ href: item.link,
382
+ target: "_blank",
383
+ rel: "noreferrer",
384
+ title: item.title,
385
+ children: item.title
386
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
387
+ className: "tib-row-title",
388
+ title: item.title,
389
+ children: item.title
390
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
391
+ className: "tib-row-actions",
392
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
393
+ className: "tib-icon-btn",
394
+ onClick: () => void act("done", item.id),
395
+ title: "完成",
396
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 14 })
397
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
398
+ className: "tib-icon-btn danger",
399
+ onClick: () => void act("remove", item.id),
400
+ title: "删除",
401
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 14 })
402
+ })]
403
+ })]
404
+ }),
405
+ item.detail ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
406
+ className: "tib-row-detail tib-row-detail-session",
407
+ title: "点击跳转到来源会话",
408
+ onClick: () => openSourceSession(item.source),
409
+ children: renderDetail(item.detail, item.id)
410
+ }) : null,
411
+ meta ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
412
+ className: "tib-row-meta",
413
+ children: meta
414
+ }) : null
415
+ ]
416
+ }, item.id);
417
+ })
418
+ }) : null]
419
+ });
420
+ }
421
+ function RailButton() {
422
+ const { state } = useInboxStore();
423
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
424
+ className: "tib-bell",
425
+ onClick: () => inboxStore.layout?.toggleSidebar(),
426
+ title: "待办(点击展开侧边栏)",
427
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChecklistOutline14, { size: 18 }), state.pending > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
428
+ className: "tib-badge",
429
+ children: String(state.pending)
430
+ }) : null]
431
+ });
432
+ }
433
+ /** Slot occupant: pure view over the store; syncs the wide fact into it. */
434
+ function InboxAction(props) {
435
+ const wide = Boolean(props.wide);
436
+ react.default.useEffect(() => {
437
+ if (inboxStore.current.wide !== wide) storeUpdate({ wide });
438
+ }, [wide]);
439
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
440
+ className: `tib-root ${wide ? "wide" : "rail"}`,
441
+ children: wide ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineSection, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RailButton, {})
442
+ });
443
+ }
444
+ function apply(ctx) {
445
+ inboxStore.sessions = ctx.sessions;
446
+ inboxStore.layout = ctx.layout;
447
+ ctx.effect(() => {
448
+ if (typeof document === "undefined") return;
449
+ if (document.querySelector("style[data-plugin-css=\"todo-inbox\"]") !== null) return;
450
+ const tag = document.createElement("style");
451
+ tag.dataset.pluginCss = "todo-inbox";
452
+ tag.textContent = CSS;
453
+ document.head.appendChild(tag);
454
+ });
455
+ ctx.effect(() => {
456
+ if (typeof document === "undefined") return;
457
+ startPolling();
458
+ return () => {
459
+ if (pollTimer !== null) {
460
+ clearInterval(pollTimer);
461
+ pollTimer = null;
462
+ }
463
+ };
464
+ });
465
+ ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
466
+ name: "sidebar.footer.action",
467
+ id: "todo-inbox"
468
+ }, InboxAction));
469
+ }
470
+ //#endregion
471
+ exports.apply = apply;
472
+ exports.inject = inject;
473
+ exports.name = name;
474
+ exports.renderDetail = renderDetail;
475
+ return module.exports;
476
+ }
477
+ });
478
+
479
+ //# sourceMappingURL=client.js.map
package/lib/index.js ADDED
@@ -0,0 +1,437 @@
1
+ import { defineTool } from "@deepseek-ai/dsh-tools";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ //#region src/index.ts
6
+ const name = "todo-inbox";
7
+ const inject = [
8
+ "tools",
9
+ "webServer",
10
+ "systemPrompt"
11
+ ];
12
+ /** API prefix for the browser half (registered as a webserver prefix route). */
13
+ const API_PREFIX = "/todo-inbox/api";
14
+ /**
15
+ * Normalize a session source to the canonical session id `sessions.open()`
16
+ * expects: the client session list uses the persisted `header.id`, which
17
+ * carries the `session-` prefix. Sources recorded before this fix (bare ids)
18
+ * are upgraded to the prefixed form on read.
19
+ */
20
+ function normalizeSource(source) {
21
+ return source === "" || source.startsWith("session-") ? source : `session-${source}`;
22
+ }
23
+ /**
24
+ * System-prompt section registered for every session: the trigger discipline
25
+ * that turns "mentioned in prose" into "recorded in the inbox". Registered
26
+ * in the tool-guidance band (after the read/write tool sections, before the
27
+ * SDK section) so it lands next to the tool catalog it governs.
28
+ */
29
+ const INBOX_PROMPT_SECTION = {
30
+ name: "todo-inbox:discipline",
31
+ order: 1600,
32
+ text: "## Global todo inbox\nWhen your work produces an action that waits on the human — merge an MR, approve a work order, review something, release, roll back after a future event, or any follow-up you name in your reply (\"after X completes, do Y\") — you MUST call `inbox_add` in the same turn. Never leave such an action only in prose: if you wrote it in the reply, it belongs in the inbox. Use `inbox_list` to check what is still pending, and `inbox_done` once the human confirms it is handled."
33
+ };
34
+ function apply(ctx, config) {
35
+ const dataPath = typeof config?.dataPath === "string" && config.dataPath.length > 0 ? config.dataPath : join(homedir(), ".dsh", "todo-inbox.json");
36
+ let items = [];
37
+ let lastSeen = "";
38
+ let chain = Promise.resolve();
39
+ const validItem = (x) => typeof x === "object" && x !== null && typeof x.id === "string" && typeof x.title === "string" && x.title.length > 0;
40
+ async function loadFromDisk() {
41
+ let text;
42
+ try {
43
+ text = await readFile(dataPath, "utf8");
44
+ } catch {
45
+ lastSeen = "";
46
+ items = [];
47
+ return;
48
+ }
49
+ lastSeen = text;
50
+ try {
51
+ const parsed = JSON.parse(text);
52
+ if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed.items)) items = parsed.items.filter(validItem).map((item) => ({
53
+ ...item,
54
+ source: normalizeSource(item.source)
55
+ }));
56
+ else items = [];
57
+ } catch (error) {
58
+ console.error(`[todo-inbox] data file parse failed, skipping: ${dataPath}`, error);
59
+ items = [];
60
+ }
61
+ }
62
+ async function persist() {
63
+ const text = JSON.stringify({
64
+ version: 1,
65
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
66
+ items
67
+ }, null, 2);
68
+ await writeFile(dataPath, text, "utf8");
69
+ lastSeen = text;
70
+ }
71
+ function enqueue(fn) {
72
+ const run = chain.then(fn);
73
+ chain = run.then(() => void 0, () => void 0);
74
+ return run;
75
+ }
76
+ function pendingItems() {
77
+ return items.filter((item) => item.doneAt === null);
78
+ }
79
+ function pendingCount() {
80
+ return pendingItems().length;
81
+ }
82
+ function compact(item) {
83
+ return { ...item };
84
+ }
85
+ function nextId() {
86
+ return `i${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
87
+ }
88
+ function sourceOf(exec) {
89
+ try {
90
+ const agent = exec?.agent;
91
+ return normalizeSource(String(agent?.sessionId ?? agent?.id ?? ""));
92
+ } catch {
93
+ return "";
94
+ }
95
+ }
96
+ const renderJson = (_args, value) => [{
97
+ type: "text",
98
+ text: JSON.stringify(value, null, 2)
99
+ }];
100
+ ctx.effect(() => ctx.systemPrompt.section(INBOX_PROMPT_SECTION));
101
+ ctx.tools.register(defineTool({
102
+ name: "inbox_add",
103
+ description: "Record one item waiting on the human in the GLOBAL todo inbox (merge MR, approve work order, review, release, rollback after a future event...). TRIGGER RULE: whenever your reply names a future human action, call this tool in the same turn — never mention it only in prose. Any session can add; the user sees every pending item in the sidebar inbox section.",
104
+ parameters: {
105
+ type: {
106
+ type: "string",
107
+ enum: [
108
+ "merge-mr",
109
+ "approve-order",
110
+ "review",
111
+ "release",
112
+ "other"
113
+ ],
114
+ description: "Item type"
115
+ },
116
+ title: {
117
+ type: "string",
118
+ required: true,
119
+ description: "One line saying what the human must do, e.g. \"merge MR !1234\""
120
+ },
121
+ detail: {
122
+ type: "string",
123
+ description: "Optional extra context"
124
+ },
125
+ link: {
126
+ type: "string",
127
+ description: "Optional link, e.g. the MR / work-order URL"
128
+ },
129
+ due: {
130
+ type: "string",
131
+ description: "Optional ISO 8601 deadline, e.g. 2026-09-08T10:00:00+08:00"
132
+ }
133
+ },
134
+ output: {
135
+ schema: {
136
+ type: "object",
137
+ additionalProperties: false,
138
+ properties: {
139
+ ok: {
140
+ type: "boolean",
141
+ required: true
142
+ },
143
+ id: { type: "string" },
144
+ pending: {
145
+ type: "integer",
146
+ required: true
147
+ }
148
+ }
149
+ },
150
+ render: renderJson
151
+ },
152
+ execute(args, exec) {
153
+ const title = String(args.title ?? "").trim();
154
+ if (title.length === 0) return Promise.resolve({
155
+ ok: false,
156
+ pending: pendingCount()
157
+ });
158
+ const item = {
159
+ id: nextId(),
160
+ type: args.type ?? "other",
161
+ title,
162
+ detail: args.detail ? String(args.detail) : "",
163
+ link: args.link ? String(args.link) : "",
164
+ dueAt: args.due ? String(args.due) : null,
165
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
166
+ source: sourceOf(exec),
167
+ doneAt: null
168
+ };
169
+ return enqueue(async () => {
170
+ items.push(item);
171
+ await persist();
172
+ return {
173
+ ok: true,
174
+ id: item.id,
175
+ pending: pendingCount()
176
+ };
177
+ });
178
+ },
179
+ presentCall: (args) => ({
180
+ card: "generic",
181
+ title: "Add inbox item",
182
+ kind: "other",
183
+ rawInput: args.title
184
+ })
185
+ }));
186
+ ctx.tools.register(defineTool({
187
+ name: "inbox_done",
188
+ description: "Mark one global inbox item as done.",
189
+ parameters: { id: {
190
+ type: "string",
191
+ required: true,
192
+ description: "Item id (from inbox_list)"
193
+ } },
194
+ output: {
195
+ schema: {
196
+ type: "object",
197
+ additionalProperties: false,
198
+ properties: {
199
+ ok: {
200
+ type: "boolean",
201
+ required: true
202
+ },
203
+ id: { type: "string" },
204
+ pending: {
205
+ type: "integer",
206
+ required: true
207
+ }
208
+ }
209
+ },
210
+ render: renderJson
211
+ },
212
+ execute(args) {
213
+ const id = String(args.id ?? "");
214
+ return enqueue(async () => {
215
+ const item = items.find((candidate) => candidate.id === id);
216
+ if (item === void 0) return {
217
+ ok: false,
218
+ pending: pendingCount()
219
+ };
220
+ item.doneAt = (/* @__PURE__ */ new Date()).toISOString();
221
+ await persist();
222
+ return {
223
+ ok: true,
224
+ id,
225
+ pending: pendingCount()
226
+ };
227
+ });
228
+ },
229
+ presentCall: (args) => ({
230
+ card: "generic",
231
+ title: "Done inbox item",
232
+ kind: "other",
233
+ rawInput: args.id
234
+ })
235
+ }));
236
+ ctx.tools.register(defineTool({
237
+ name: "inbox_list",
238
+ description: "List every pending item in the GLOBAL todo inbox (across all sessions).",
239
+ parameters: {},
240
+ output: {
241
+ schema: {
242
+ type: "object",
243
+ additionalProperties: false,
244
+ properties: {
245
+ ok: {
246
+ type: "boolean",
247
+ required: true
248
+ },
249
+ pending: {
250
+ type: "integer",
251
+ required: true
252
+ },
253
+ items: {
254
+ type: "array",
255
+ items: {
256
+ type: "object",
257
+ additionalProperties: true
258
+ }
259
+ }
260
+ }
261
+ },
262
+ render: renderJson
263
+ },
264
+ execute() {
265
+ return Promise.resolve({
266
+ ok: true,
267
+ pending: pendingCount(),
268
+ items: pendingItems().map((item) => ({
269
+ id: item.id,
270
+ type: item.type,
271
+ title: item.title,
272
+ detail: item.detail,
273
+ link: item.link,
274
+ dueAt: item.dueAt,
275
+ createdAt: item.createdAt,
276
+ source: normalizeSource(item.source),
277
+ doneAt: item.doneAt
278
+ }))
279
+ });
280
+ },
281
+ presentCall: () => ({
282
+ card: "generic",
283
+ title: "List inbox items",
284
+ kind: "other",
285
+ rawInput: "pending"
286
+ })
287
+ }));
288
+ function fence(req) {
289
+ const origin = req.headers.origin;
290
+ if (origin === void 0) return true;
291
+ try {
292
+ return new URL(origin).host === (req.headers.host ?? "");
293
+ } catch {
294
+ return false;
295
+ }
296
+ }
297
+ async function readBody(req) {
298
+ let body = "";
299
+ for await (const chunk of req) body += chunk;
300
+ if (body.length === 0) return {};
301
+ try {
302
+ const parsed = JSON.parse(body);
303
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
304
+ } catch {
305
+ throw new Error("invalid json body");
306
+ }
307
+ }
308
+ function writeJson(res, status, value) {
309
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
310
+ res.end(JSON.stringify(value));
311
+ }
312
+ const apiHandler = async (req, res) => {
313
+ if (!fence(req)) {
314
+ writeJson(res, 403, {
315
+ ok: false,
316
+ error: "forbidden"
317
+ });
318
+ return;
319
+ }
320
+ const method = new URL(req.url ?? "/", "http://dsh.local").pathname.slice(16);
321
+ try {
322
+ if (req.method === "GET" && method === "items") {
323
+ writeJson(res, 200, {
324
+ ok: true,
325
+ pending: pendingCount(),
326
+ items: pendingItems().map(compact),
327
+ path: dataPath
328
+ });
329
+ return;
330
+ }
331
+ if (req.method === "POST" && method === "done") {
332
+ const args = await readBody(req);
333
+ const id = String(args.id ?? "");
334
+ writeJson(res, 200, await enqueue(async () => {
335
+ const item = items.find((candidate) => candidate.id === id);
336
+ if (item === void 0) return {
337
+ ok: false,
338
+ error: "not_found",
339
+ pending: pendingCount()
340
+ };
341
+ item.doneAt = (/* @__PURE__ */ new Date()).toISOString();
342
+ await persist();
343
+ return {
344
+ ok: true,
345
+ id,
346
+ pending: pendingCount()
347
+ };
348
+ }));
349
+ return;
350
+ }
351
+ if (req.method === "POST" && method === "remove") {
352
+ const args = await readBody(req);
353
+ const id = String(args.id ?? "");
354
+ writeJson(res, 200, await enqueue(async () => {
355
+ const before = items.length;
356
+ items = items.filter((candidate) => candidate.id !== id);
357
+ if (items.length === before) return {
358
+ ok: false,
359
+ error: "not_found",
360
+ pending: pendingCount()
361
+ };
362
+ await persist();
363
+ return {
364
+ ok: true,
365
+ id,
366
+ pending: pendingCount()
367
+ };
368
+ }));
369
+ return;
370
+ }
371
+ if (req.method === "POST" && method === "add") {
372
+ const args = await readBody(req);
373
+ const title = String(args.title ?? "").trim();
374
+ if (title.length === 0) {
375
+ writeJson(res, 400, {
376
+ ok: false,
377
+ error: "empty_title",
378
+ pending: pendingCount()
379
+ });
380
+ return;
381
+ }
382
+ const item = {
383
+ id: nextId(),
384
+ type: typeof args.type === "string" ? args.type : "other",
385
+ title,
386
+ detail: typeof args.detail === "string" ? args.detail : "",
387
+ link: typeof args.link === "string" ? args.link : "",
388
+ dueAt: typeof args.due === "string" ? args.due : null,
389
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
390
+ source: "api",
391
+ doneAt: null
392
+ };
393
+ writeJson(res, 200, await enqueue(async () => {
394
+ items.push(item);
395
+ await persist();
396
+ return {
397
+ ok: true,
398
+ id: item.id,
399
+ pending: pendingCount()
400
+ };
401
+ }));
402
+ return;
403
+ }
404
+ writeJson(res, 404, {
405
+ ok: false,
406
+ error: "not_found"
407
+ });
408
+ } catch (error) {
409
+ writeJson(res, 500, {
410
+ ok: false,
411
+ error: String(error)
412
+ });
413
+ }
414
+ };
415
+ ctx.effect(() => ctx.webServer.register({
416
+ kind: "prefix",
417
+ path: API_PREFIX,
418
+ handler: apiHandler
419
+ }));
420
+ ctx.effect(() => {
421
+ const timer = setInterval(() => {
422
+ enqueue(async () => {
423
+ let text;
424
+ try {
425
+ text = await readFile(dataPath, "utf8");
426
+ } catch {
427
+ return;
428
+ }
429
+ if (text !== lastSeen) await loadFromDisk();
430
+ });
431
+ }, 5e3);
432
+ return () => clearInterval(timer);
433
+ });
434
+ chain = chain.then(loadFromDisk);
435
+ }
436
+ //#endregion
437
+ export { apply, inject, name };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * dsh-todo-inbox browser half: the global inbox, embedded in the sidebar.
3
+ *
4
+ * Wide sidebar: a collapsible section stacked above Settings — a header row
5
+ * (checklist glyph + label + count badge) and an inline list of pending
6
+ * items with hover-revealed icon actions. Rail sidebar: the standard 36×36
7
+ * circle button with a corner badge; clicking it expands the sidebar (the
8
+ * rail has no room for a list).
9
+ *
10
+ * All state lives in a module-level store, so the sidebar's wide↔rail
11
+ * crossfade (which remounts the slot occupant) never loses data or
12
+ * expansion. Polling starts once at apply time.
13
+ *
14
+ * `detail` renders a tiny safe markdown subset (bold / italic / inline code /
15
+ * [text](url) / bare URLs / line breaks) as React elements — never raw HTML.
16
+ *
17
+ * The bundle is compiled to a window.__ModuleLoader__ closure; react and the
18
+ * ui-primitives icons resolve through the module table at runtime, so this
19
+ * file must not runtime-import anything else (type-only imports are erased).
20
+ *
21
+ * @module dsh-todo-inbox/client
22
+ */
23
+ import React from 'react';
24
+ export declare const name = "todo-inbox-client";
25
+ export declare const inject: string[];
26
+ /** Structural typing only — the loader supplies the real slot registry. */
27
+ interface SlotBridge {
28
+ inject(slot: string, factory: () => unknown): void;
29
+ register(options: {
30
+ name: string;
31
+ id?: string;
32
+ }, component: unknown): unknown;
33
+ }
34
+ /** Structural typing for the sessions service we use: open a session by id. */
35
+ interface SessionsBridge {
36
+ open(id: string): void;
37
+ }
38
+ /** Structural typing for the layout service we use: expand the sidebar. */
39
+ interface LayoutBridge {
40
+ toggleSidebar(): void;
41
+ }
42
+ interface ClientCtx {
43
+ effect(callback: () => void | (() => void)): void;
44
+ slots: SlotBridge;
45
+ sessions: SessionsBridge;
46
+ layout: LayoutBridge;
47
+ }
48
+ /**
49
+ * Render one detail string as React nodes. Supported: `inline code`, **bold**,
50
+ * *italic*, [text](url), bare https?:// URLs, and line breaks (white-space:
51
+ * pre-wrap). Anything unmatched renders as plain text — never HTML.
52
+ * Exported for the smoke test; the loader only consumes name/inject/apply.
53
+ */
54
+ export declare function renderDetail(text: string, keyBase: string): React.ReactNode[];
55
+ export declare function apply(ctx: ClientCtx): void;
56
+ export {};
@@ -0,0 +1,45 @@
1
+ /**
2
+ * dsh-todo-inbox host half: a process-wide global todo inbox.
3
+ *
4
+ * Any session's agent can record "waiting on human" todos through the
5
+ * inbox_add / inbox_done / inbox_list model tools (registered once at the
6
+ * root realm, visible to every session). Items persist to a JSON file
7
+ * (~/.dsh/todo-inbox.json by default, overridable via row `config.dataPath`)
8
+ * which is the single source of truth: agents may also edit the file
9
+ * directly, and a 5-second poll picks external edits up.
10
+ *
11
+ * The browser half talks to this host through plain HTTP routes under
12
+ * /todo-inbox/api (registered on the shared webserver) — no remote-bridge
13
+ * machinery needed, exactly like other third-party bundles.
14
+ *
15
+ * Persistence uses node:fs directly, NOT the injected `fs` service: the app's
16
+ * fs service applies the workspace sandbox policy (writes to ~/.dsh/... are
17
+ * denied), while a static host plugin runs in the real host process with full
18
+ * Node privileges — same as other static bundles (e.g. dsh-better-sidebar).
19
+ *
20
+ * No Config schema: the row config passes through raw and unknown fields are
21
+ * ignored, so a boot-time schema/version mismatch can never fail the plugin
22
+ * tree.
23
+ *
24
+ * @module dsh-todo-inbox/host
25
+ */
26
+ import type { Context } from '@deepseek-ai/cordis';
27
+ export declare const name = "todo-inbox";
28
+ export declare const inject: string[];
29
+ /** One inbox item (mirrors the data-file record; all scalar fields). */
30
+ export interface InboxItem {
31
+ id: string;
32
+ type: string;
33
+ title: string;
34
+ detail: string;
35
+ link: string;
36
+ dueAt: string | null;
37
+ createdAt: string;
38
+ source: string;
39
+ doneAt: string | null;
40
+ }
41
+ export interface TodoInboxConfig {
42
+ /** Absolute path of the JSON data file; parent directory must exist. */
43
+ dataPath?: string;
44
+ }
45
+ export declare function apply(ctx: Context, config?: TodoInboxConfig): void;
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "dsh-todo-inbox",
3
+ "version": "0.1.0",
4
+ "description": "DSH web static plugin: a global cross-session todo inbox. Any session's agent records human-action todos (merge MR, approve work orders, reviews) via inbox_add/inbox_done/inbox_list; a root-scope sidebar entry shows a count badge and an expandable list to view, open, and complete them. Host persists to ~/.dsh/todo-inbox.json and serves /todo-inbox/api HTTP routes for the browser half.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/lumoping/dsh-todo-inbox.git"
8
+ },
9
+ "homepage": "https://github.com/lumoping/dsh-todo-inbox",
10
+ "type": "module",
11
+ "main": "lib/index.js",
12
+ "types": "lib/types/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./lib/types/index.d.ts",
16
+ "default": "./lib/index.js"
17
+ },
18
+ "./client": {
19
+ "default": "./lib/client.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "packageManager": "pnpm@9.15.9",
27
+ "dsh": {
28
+ "bundle": {
29
+ "patch": "./cordis.patch.yml"
30
+ },
31
+ "client": {
32
+ "inject": [
33
+ "@deepseek-ai/dsh-client-ui-slots",
34
+ "@deepseek-ai/dsh-client-modules"
35
+ ],
36
+ "platform": "web"
37
+ }
38
+ },
39
+ "files": [
40
+ "lib/index.js",
41
+ "lib/client.js",
42
+ "lib/types/**/*.d.ts",
43
+ "cordis.patch.yml",
44
+ "README.md"
45
+ ],
46
+ "scripts": {
47
+ "clean": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\"",
48
+ "build": "npm run clean && tsc -p tsconfig.build.json && tsdown",
49
+ "prepare": "tsdown",
50
+ "typecheck": "tsc --noEmit",
51
+ "test": "node scripts/smoke-test.mjs"
52
+ },
53
+ "peerDependencies": {
54
+ "@deepseek-ai/cordis": "^4.0.2",
55
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.2-rc.1 <0.2.0-0",
56
+ "@deepseek-ai/dsh-fs": ">=0.1.2-rc.1 <0.2.0-0",
57
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.2-rc.1 <0.2.0-0",
58
+ "@deepseek-ai/dsh-system-prompt": ">=0.1.2-rc.1 <0.2.0-0",
59
+ "@deepseek-ai/dsh-tools": ">=0.1.2-rc.1 <0.2.0-0",
60
+ "react": "^18.2.0"
61
+ },
62
+ "devDependencies": {
63
+ "@deepseek-ai/cordis": "4.0.2",
64
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-rc.1",
65
+ "@deepseek-ai/dsh-fs": "0.1.2-rc.1",
66
+ "@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1",
67
+ "@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
68
+ "@deepseek-ai/dsh-tools": "0.1.2-rc.1",
69
+ "@types/node": "^24.0.0",
70
+ "@types/react": "~18.3.1",
71
+ "@types/react-dom": "~18.3.1",
72
+ "react": "^18.2.0",
73
+ "react-dom": "18.2.0",
74
+ "tsdown": "^0.22.2",
75
+ "typescript": "^5.6.0"
76
+ }
77
+ }