@stephen1620/dsh-tomato-board 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,738 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@stephen1620/dsh-tomato-board",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region ../../core/session/lib/types/types.js
11
+ /**
12
+ * Brand a string as a {@link SessionId}.
13
+ * @param id - the raw session id string.
14
+ * @returns the same string, branded (a compile-time cast — no runtime cost).
15
+ */
16
+ function SessionId(id) {
17
+ return id;
18
+ }
19
+ //#endregion
20
+ //#region \0dsh-css:/Users/gengfeng/deepseek-harness/packages/client/tomato-board/src/client/tomato-board.module.css.mjs
21
+ const css = ".Y6kksa_sidebarAction{width:100%;min-height:36px;color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit;white-space:nowrap;transition:background var(--ds-transition-duration-fast) var(--ds-ease-in-out), border-color var(--ds-transition-duration-fast) var(--ds-ease-in-out);background:0 0;border:0;border-radius:10px;align-items:center;gap:9px;margin:0;padding:0 10px;display:flex}.Y6kksa_sidebarAction:hover{background:var(--dsw-alias-interactive-bg-hover)}.Y6kksa_tomatoIcon{width:18px;height:18px;color:var(--dsw-alias-label-primary-inverted);background:var(--dsw-alias-state-error-primary);border-radius:6px;flex:none;place-items:center;font-size:11px;font-weight:750;display:inline-grid}.Y6kksa_transitionTrigger{align-items:center;gap:6px;display:inline-flex}.Y6kksa_transitionCaption{color:var(--dsw-alias-label-secondary);font-size:12px}.Y6kksa_transitionTrigger strong{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:650}.Y6kksa_transitionDivider{background:var(--dsw-alias-border-l2);width:1px;height:14px;margin:0 2px}.Y6kksa_transitionAction{color:var(--dsw-alias-state-business-primary);font-size:12px;font-weight:650}.Y6kksa_transitionChevron{color:var(--dsw-alias-state-business-primary);transition:transform var(--ds-transition-duration-fast) var(--ds-ease-in-out)}.Y6kksa_transitionTrigger[aria-expanded=true] .Y6kksa_transitionChevron{transform:rotate(180deg)}.Y6kksa_workbench{min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);font-family:var(--dsw-font-family);flex-direction:column;flex:1;display:flex;position:relative}.Y6kksa_createDialog{width:min(480px,100%)}.Y6kksa_dialogBody{flex-direction:column;gap:12px;display:flex}.Y6kksa_dialogBody>span{color:var(--dsw-alias-state-error-primary);font-size:12px;font-weight:700}.Y6kksa_dialogBody code{color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;font-size:13px}.Y6kksa_workspaceTrigger{justify-content:space-between;width:100%;display:flex}.Y6kksa_header{border-bottom:1px solid color-mix(in srgb, var(--dsw-alias-border-l2) 72%, transparent);background:color-mix(in srgb, var(--dsw-alias-bg-layer-1) 72%, transparent);backdrop-filter:blur(16px);justify-content:space-between;align-items:center;min-height:52px;padding:8px 16px;display:flex}.Y6kksa_header h1{margin:0;font-size:16px;line-height:22px}.Y6kksa_header p{color:var(--dsw-alias-label-secondary);margin:1px 0 0;font-size:12px;line-height:16px}.Y6kksa_actions{align-items:center;gap:8px;display:flex}.Y6kksa_actions button{border:1px solid var(--dsw-alias-border-l2);min-height:34px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-button-elevated-fill);cursor:pointer;transition:background var(--ds-transition-duration-fast) var(--ds-ease-in-out), border-color var(--ds-transition-duration-fast) var(--ds-ease-in-out);border-radius:10px;padding:0 14px}.Y6kksa_actions button:hover{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-button-floating-hover)}.Y6kksa_actions button:disabled{cursor:wait;opacity:.55}.Y6kksa_actions .Y6kksa_headerIconButton{border-radius:8px;width:34px;min-width:34px;height:34px;min-height:34px;padding:0}.Y6kksa_searchField{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:10px;align-items:center;gap:6px;width:220px;min-height:34px;padding:0 8px;display:flex}.Y6kksa_searchField input{min-width:0;color:inherit;font:inherit;background:0 0;border:0;outline:0;flex:1}.Y6kksa_searchField button{border:0;min-height:24px;padding:0 5px}.Y6kksa_filterMenu{position:relative}.Y6kksa_filterMenu summary{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);cursor:pointer;border-radius:10px;place-items:center;width:34px;height:34px;list-style:none;display:grid;position:relative}.Y6kksa_filterMenu summary::-webkit-details-marker{display:none}.Y6kksa_filterMenu summary i{background:var(--dsw-alias-state-error-primary);border-radius:50%;width:5px;height:5px;position:absolute;top:5px;right:5px}.Y6kksa_filterPopover{z-index:10;border:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb, var(--dsw-alias-bg-layer-2) 94%, transparent);width:min(760px,100vw - 320px);box-shadow:var(--dsw-shadow-lv3);backdrop-filter:blur(18px);border-radius:16px;gap:12px;padding:14px;display:grid;position:absolute;top:calc(100% + 8px);right:0}.Y6kksa_filterRow{align-items:flex-start;gap:12px;display:flex}.Y6kksa_filterRow>span{width:36px;color:var(--dsw-alias-label-secondary);padding-top:7px;font-size:13px}.Y6kksa_filterRow>div{flex-wrap:wrap;flex:1;gap:7px;display:flex}.Y6kksa_filterRow button{min-height:30px;color:var(--dsw-alias-label-secondary);padding:3px 11px}.Y6kksa_filterRow .Y6kksa_selectedFilter{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent)}.Y6kksa_error{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-state-error-secondary);border-radius:12px;margin:16px 24px 0;padding:12px 14px}.Y6kksa_board{flex:1;align-items:flex-start;gap:12px;padding:8px 12px;display:flex;overflow:auto}.Y6kksa_lane{border:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb, var(--dsw-alias-bg-module-platform) 86%, transparent);border-radius:16px;flex-direction:column;width:300px;min-width:300px;max-height:100%;display:flex}.Y6kksa_laneHeader{justify-content:space-between;align-items:center;padding:12px 14px 10px;display:flex}.Y6kksa_laneHeader h2{margin:0;font-size:14px}.Y6kksa_laneHeader span{text-align:center;min-width:22px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-overlay);border-radius:999px;padding:2px 6px;font-size:12px}.Y6kksa_cards{padding:0 8px 8px;overflow-y:auto}.Y6kksa_card{border:1px solid var(--dsw-alias-border-l2);min-width:0;max-width:100%;color:inherit;background:color-mix(in srgb, var(--dsw-alias-bg-layer-1) 96%, transparent);box-shadow:var(--dsw-shadow-lv1);text-align:left;transition:background var(--ds-transition-duration-fast) var(--ds-ease-in-out), border-color var(--ds-transition-duration-fast) var(--ds-ease-in-out), box-shadow var(--ds-transition-duration-fast) var(--ds-ease-in-out);cursor:pointer;border-radius:14px;flex-direction:column;gap:7px;margin-top:8px;padding:12px;text-decoration:none;display:flex;overflow:hidden}.Y6kksa_card:hover{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-button-floating-hover);box-shadow:var(--dsw-shadow-lv2)}.Y6kksa_cardMuted{opacity:.46;filter:saturate(.2);box-shadow:none}.Y6kksa_cardMuted:hover{opacity:.62}.Y6kksa_card:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.Y6kksa_card strong,.Y6kksa_key,.Y6kksa_meta{overflow-wrap:anywhere;word-break:normal;min-width:0}.Y6kksa_cardTopline{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.Y6kksa_cardButtons{align-items:center;gap:2px;display:inline-flex}.Y6kksa_muteButton,.Y6kksa_tomatoLink{border-radius:8px;flex:none;place-items:center;width:28px;min-width:28px;height:28px;min-height:28px;padding:0;line-height:1;display:inline-grid}.Y6kksa_muteButton{color:var(--dsw-alias-label-tertiary);font-size:9px}.Y6kksa_muteButton[aria-pressed=true]{color:var(--dsw-alias-label-primary)}.Y6kksa_card strong{font-size:14px;font-weight:600;line-height:1.45}.Y6kksa_key{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:650}.Y6kksa_meta{color:var(--dsw-alias-label-tertiary);font-size:12px}.Y6kksa_empty{color:var(--dsw-alias-label-tertiary);margin:auto}@media (width<=820px){.Y6kksa_header{align-items:flex-start;gap:10px}.Y6kksa_actions{flex-wrap:wrap;justify-content:flex-end}.Y6kksa_searchField{width:170px}}";
22
+ const tagId = "@stephen1620/dsh-tomato-board/tomato-board.module.css";
23
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
24
+ const tag = document.createElement("style");
25
+ tag.dataset.plugin = "@stephen1620/dsh-tomato-board";
26
+ tag.dataset.pluginCss = tagId;
27
+ tag.textContent = css;
28
+ document.head.appendChild(tag);
29
+ }
30
+ var tomato_board_module_css_default = {
31
+ "actions": "Y6kksa_actions",
32
+ "board": "Y6kksa_board",
33
+ "card": "Y6kksa_card",
34
+ "cardButtons": "Y6kksa_cardButtons",
35
+ "cardMuted": "Y6kksa_cardMuted",
36
+ "cardTopline": "Y6kksa_cardTopline",
37
+ "cards": "Y6kksa_cards",
38
+ "createDialog": "Y6kksa_createDialog",
39
+ "dialogBody": "Y6kksa_dialogBody",
40
+ "empty": "Y6kksa_empty",
41
+ "error": "Y6kksa_error",
42
+ "filterMenu": "Y6kksa_filterMenu",
43
+ "filterPopover": "Y6kksa_filterPopover",
44
+ "filterRow": "Y6kksa_filterRow",
45
+ "header": "Y6kksa_header",
46
+ "headerIconButton": "Y6kksa_headerIconButton",
47
+ "key": "Y6kksa_key",
48
+ "lane": "Y6kksa_lane",
49
+ "laneHeader": "Y6kksa_laneHeader",
50
+ "meta": "Y6kksa_meta",
51
+ "muteButton": "Y6kksa_muteButton",
52
+ "searchField": "Y6kksa_searchField",
53
+ "selectedFilter": "Y6kksa_selectedFilter",
54
+ "sidebarAction": "Y6kksa_sidebarAction",
55
+ "tomatoIcon": "Y6kksa_tomatoIcon",
56
+ "tomatoLink": "Y6kksa_tomatoLink",
57
+ "transitionAction": "Y6kksa_transitionAction",
58
+ "transitionCaption": "Y6kksa_transitionCaption",
59
+ "transitionChevron": "Y6kksa_transitionChevron",
60
+ "transitionDivider": "Y6kksa_transitionDivider",
61
+ "transitionTrigger": "Y6kksa_transitionTrigger",
62
+ "workbench": "Y6kksa_workbench",
63
+ "workspaceTrigger": "Y6kksa_workspaceTrigger"
64
+ };
65
+ //#endregion
66
+ //#region src/client/TomatoBoard.tsx
67
+ let state = {
68
+ open: false,
69
+ loading: false,
70
+ items: [],
71
+ error: null,
72
+ selectedItem: null
73
+ };
74
+ let disposeWorkbench = null;
75
+ const listeners = /* @__PURE__ */ new Set();
76
+ const emit = (patch) => {
77
+ state = {
78
+ ...state,
79
+ ...patch
80
+ };
81
+ for (const listener of listeners) listener();
82
+ };
83
+ const subscribe = (listener) => {
84
+ listeners.add(listener);
85
+ return () => listeners.delete(listener);
86
+ };
87
+ const snapshot = () => state;
88
+ const TOMATO_ITEM_KEY_PATTERN = /^\[([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*-\d+)\]/u;
89
+ const TOMATO_TYPE_OPTIONS = [
90
+ "测试缺陷",
91
+ "缺陷",
92
+ "Bug",
93
+ "EnablerStory",
94
+ "Story",
95
+ "Task"
96
+ ];
97
+ const TOMATO_STATUS_ORDER = [
98
+ "新建",
99
+ "Bugfix",
100
+ "修复中",
101
+ "开发中",
102
+ "待测试",
103
+ "测试中",
104
+ "测试通过",
105
+ "已完成",
106
+ "已取消",
107
+ "延期解决",
108
+ "测试完成",
109
+ "待开发",
110
+ "不修复",
111
+ "已挂起"
112
+ ];
113
+ const TOMATO_FILTER_BLACKLIST_KEY = "taskboard.tomatoFilterBlacklist.v1";
114
+ const TOMATO_SESSION_LINKS_KEY = "taskboard.tomatoSessionLinks.v1";
115
+ const TOMATO_MUTED_ITEMS_KEY = "taskboard.tomatoMutedItems.v1";
116
+ function readSessionLinks() {
117
+ try {
118
+ const value = JSON.parse(window.localStorage.getItem(TOMATO_SESSION_LINKS_KEY) ?? "{}");
119
+ return value && typeof value === "object" ? value : {};
120
+ } catch {
121
+ return {};
122
+ }
123
+ }
124
+ function linkedSessionId(itemKey) {
125
+ const value = readSessionLinks()[itemKey];
126
+ return typeof value === "string" && value ? SessionId(value) : "";
127
+ }
128
+ function saveSessionLink(itemKey, sessionId) {
129
+ const links = readSessionLinks();
130
+ links[itemKey] = sessionId;
131
+ window.localStorage.setItem(TOMATO_SESSION_LINKS_KEY, JSON.stringify(links));
132
+ }
133
+ function readFilterBlacklist() {
134
+ try {
135
+ const value = JSON.parse(window.localStorage.getItem(TOMATO_FILTER_BLACKLIST_KEY) ?? "{}");
136
+ return {
137
+ types: new Set(Array.isArray(value?.types) ? value.types.filter((item) => typeof item === "string") : []),
138
+ statuses: new Set(Array.isArray(value?.statuses) ? value.statuses.filter((item) => typeof item === "string") : [])
139
+ };
140
+ } catch {
141
+ return {
142
+ types: /* @__PURE__ */ new Set(),
143
+ statuses: /* @__PURE__ */ new Set()
144
+ };
145
+ }
146
+ }
147
+ function readMutedItems() {
148
+ try {
149
+ const value = JSON.parse(window.localStorage.getItem(TOMATO_MUTED_ITEMS_KEY) ?? "[]");
150
+ return new Set(Array.isArray(value) ? value.filter((item) => typeof item === "string") : []);
151
+ } catch {
152
+ return /* @__PURE__ */ new Set();
153
+ }
154
+ }
155
+ async function refresh() {
156
+ emit({
157
+ loading: true,
158
+ error: null
159
+ });
160
+ try {
161
+ const response = await fetch("/api/tomato-board/items", { headers: { accept: "application/json" } });
162
+ const body = await response.json();
163
+ if (!response.ok) throw new Error(body.error || `请求失败 (${response.status})`);
164
+ emit({ items: body.items ?? [] });
165
+ } catch (error) {
166
+ emit({ error: error instanceof Error ? error.message : "番茄事项读取失败" });
167
+ } finally {
168
+ emit({ loading: false });
169
+ }
170
+ }
171
+ function closeWorkbench() {
172
+ const dispose = disposeWorkbench;
173
+ disposeWorkbench = null;
174
+ emit({
175
+ open: false,
176
+ selectedItem: null
177
+ });
178
+ dispose?.();
179
+ }
180
+ function TomatoBoardAction({ wide, openWorkbench }) {
181
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
182
+ className: tomato_board_module_css_default.sidebarAction,
183
+ type: "button",
184
+ title: "番茄工作台",
185
+ onClick: openWorkbench,
186
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
187
+ className: tomato_board_module_css_default.tomatoIcon,
188
+ "aria-hidden": "true",
189
+ children: "T"
190
+ }), wide && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "番茄工作台" })]
191
+ });
192
+ }
193
+ function CreateConversationDialog({ ctx, item }) {
194
+ const [workspaceId, setWorkspaceId] = (0, react.useState)("");
195
+ const [workspaceMenuOpen, setWorkspaceMenuOpen] = (0, react.useState)(false);
196
+ const [creating, setCreating] = (0, react.useState)(false);
197
+ const [error, setError] = (0, react.useState)(null);
198
+ const workspaces = (0, react.useSyncExternalStore)((listener) => ctx.workspaces.list.subscribe(listener), () => ctx.workspaces.list.getSnapshot(), () => ctx.workspaces.list.getSnapshot());
199
+ const selectedWorkspace = workspaces.items.find((workspace) => workspace.workspaceId === workspaceId);
200
+ async function createConversation() {
201
+ if (!selectedWorkspace || creating) return;
202
+ setCreating(true);
203
+ setError(null);
204
+ try {
205
+ const createdSessionId = await ctx.sessions.create({ workspaceId: selectedWorkspace.workspaceId });
206
+ const session = ctx.sessions.binding(createdSessionId)?.session;
207
+ if (!session) throw new Error("新建对话未能在 Harness 中加载");
208
+ const title = `[${item.itemKey}] ${item.title}`;
209
+ const renamed = await session.rename(title);
210
+ if (!renamed.ok) throw new Error(`对话标题设置失败:${renamed.error.message}`);
211
+ const prompt = [
212
+ `请处理番茄事项 ${item.itemKey}。`,
213
+ `标题:${item.title}`,
214
+ `类型:${item.itemType || "未设置"}`,
215
+ `状态:${item.status || "未设置"}`,
216
+ `优先级:${item.priority || "未设置"}`
217
+ ].join("\n");
218
+ const prompted = await session.prompt([{
219
+ type: "text",
220
+ text: prompt
221
+ }], "queue");
222
+ if (!prompted.ok) throw new Error(`事项上下文写入失败:${prompted.error.message}`);
223
+ saveSessionLink(item.itemKey, createdSessionId);
224
+ ctx.sessions.open(createdSessionId);
225
+ closeWorkbench();
226
+ } catch (reason) {
227
+ setError(reason instanceof Error ? reason.message : "Harness 对话创建失败");
228
+ } finally {
229
+ setCreating(false);
230
+ }
231
+ }
232
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
233
+ open: true,
234
+ onClose: () => emit({ selectedItem: null }),
235
+ title: "选择处理该事项的仓库",
236
+ closeLabel: "关闭",
237
+ description: item.title,
238
+ className: tomato_board_module_css_default.createDialog,
239
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
240
+ variant: "ghost",
241
+ onClick: () => emit({ selectedItem: null }),
242
+ children: "取消"
243
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
244
+ variant: "primary",
245
+ disabled: !selectedWorkspace || creating,
246
+ onClick: () => void createConversation(),
247
+ children: creating ? "正在创建…" : "创建并进入对话"
248
+ })] }),
249
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
250
+ className: tomato_board_module_css_default.dialogBody,
251
+ children: [
252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.itemKey }),
253
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
254
+ className: tomato_board_module_css_default.error,
255
+ role: "alert",
256
+ children: error
257
+ }) : null,
258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
259
+ open: workspaceMenuOpen,
260
+ portal: true,
261
+ items: workspaces.items.map((workspace) => ({
262
+ id: workspace.workspaceId,
263
+ label: workspace.title
264
+ })),
265
+ selectedId: workspaceId || void 0,
266
+ onSelect: (id) => {
267
+ setWorkspaceId(id);
268
+ setWorkspaceMenuOpen(false);
269
+ },
270
+ onClose: () => setWorkspaceMenuOpen(false),
271
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Button, {
272
+ className: tomato_board_module_css_default.workspaceTrigger,
273
+ variant: "outline",
274
+ "aria-haspopup": "menu",
275
+ "aria-expanded": workspaceMenuOpen,
276
+ onClick: () => setWorkspaceMenuOpen((open) => !open),
277
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: selectedWorkspace?.title ?? "选择 Harness 项目…" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
278
+ "aria-hidden": "true",
279
+ children: "⌄"
280
+ })]
281
+ })
282
+ }),
283
+ selectedWorkspace ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: selectedWorkspace.path }) : null
284
+ ]
285
+ })
286
+ });
287
+ }
288
+ function TomatoBoardPanel({ ctx }) {
289
+ const board = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot);
290
+ const workbenchRef = (0, react.useRef)(null);
291
+ const [search, setSearch] = (0, react.useState)("");
292
+ const [blacklist, setBlacklist] = (0, react.useState)(readFilterBlacklist);
293
+ const [mutedItems, setMutedItems] = (0, react.useState)(readMutedItems);
294
+ const sessions = (0, react.useSyncExternalStore)((listener) => ctx.sessions.list.subscribe(listener), () => ctx.sessions.list.getSnapshot(), () => ctx.sessions.list.getSnapshot());
295
+ (0, react.useEffect)(() => {
296
+ if (board.open && board.items.length === 0 && !board.loading && !board.error) refresh();
297
+ }, [
298
+ board.error,
299
+ board.items.length,
300
+ board.loading,
301
+ board.open
302
+ ]);
303
+ (0, react.useEffect)(() => {
304
+ if (!board.open) return;
305
+ const closeOnOutsideNavigation = (event) => {
306
+ if (!(event.target instanceof Element)) return;
307
+ if (workbenchRef.current?.contains(event.target)) return;
308
+ if (event.target.closest("[role=\"dialog\"], [role=\"menu\"]")) return;
309
+ closeWorkbench();
310
+ };
311
+ document.addEventListener("pointerdown", closeOnOutsideNavigation, true);
312
+ return () => document.removeEventListener("pointerdown", closeOnOutsideNavigation, true);
313
+ }, [board.open]);
314
+ if (!board.open) return null;
315
+ function openItem(item) {
316
+ const stored = linkedSessionId(item.itemKey);
317
+ const titlePrefix = `[${item.itemKey}]`;
318
+ const discovered = sessions.ids.find((id) => {
319
+ const summary = sessions.byId[id];
320
+ return summary?.title?.startsWith(titlePrefix) || summary?.displayTitle.startsWith(titlePrefix);
321
+ });
322
+ const associated = stored && sessions.byId[stored] ? stored : discovered;
323
+ if (associated) {
324
+ saveSessionLink(item.itemKey, associated);
325
+ ctx.sessions.open(associated);
326
+ closeWorkbench();
327
+ return;
328
+ }
329
+ emit({ selectedItem: item });
330
+ }
331
+ const normalizedSearch = search.trim().toLowerCase();
332
+ const filteredItems = board.items.filter((item) => !blacklist.types.has(item.itemType) && !blacklist.statuses.has(item.status) && (!normalizedSearch || [
333
+ item.itemKey,
334
+ item.title,
335
+ item.itemType,
336
+ item.status,
337
+ item.workspace,
338
+ item.creator,
339
+ item.priority
340
+ ].join(" ").toLowerCase().includes(normalizedSearch)));
341
+ const typeOptions = [...new Set([...TOMATO_TYPE_OPTIONS, ...board.items.map((item) => item.itemType).filter(Boolean)])];
342
+ const statusOptions = [...new Set([...TOMATO_STATUS_ORDER, ...board.items.map((item) => item.status).filter((status) => !TOMATO_STATUS_ORDER.includes(status))])];
343
+ const statuses = [...new Set([...TOMATO_STATUS_ORDER.filter((status) => filteredItems.some((item) => item.status === status)), ...filteredItems.map((item) => item.status).filter((status) => !TOMATO_STATUS_ORDER.includes(status))])];
344
+ const toggleBlacklist = (kind, value) => setBlacklist((current) => {
345
+ const nextValues = new Set(current[kind]);
346
+ if (nextValues.has(value)) nextValues.delete(value);
347
+ else nextValues.add(value);
348
+ const next = {
349
+ ...current,
350
+ [kind]: nextValues
351
+ };
352
+ window.localStorage.setItem(TOMATO_FILTER_BLACKLIST_KEY, JSON.stringify({
353
+ types: [...next.types],
354
+ statuses: [...next.statuses]
355
+ }));
356
+ return next;
357
+ });
358
+ const toggleMutedItem = (itemKey) => setMutedItems((current) => {
359
+ const next = new Set(current);
360
+ if (next.has(itemKey)) next.delete(itemKey);
361
+ else next.add(itemKey);
362
+ window.localStorage.setItem(TOMATO_MUTED_ITEMS_KEY, JSON.stringify([...next]));
363
+ return next;
364
+ });
365
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
366
+ ref: workbenchRef,
367
+ className: tomato_board_module_css_default.workbench,
368
+ "aria-label": "番茄工作台",
369
+ children: [
370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
371
+ className: tomato_board_module_css_default.header,
372
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: "番茄工作台" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: board.loading ? "正在读取番茄事项…" : `显示 ${filteredItems.length} / ${board.items.length} 个事项` })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
373
+ className: tomato_board_module_css_default.actions,
374
+ children: [
375
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
376
+ className: tomato_board_module_css_default.searchField,
377
+ children: [
378
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
379
+ "aria-hidden": "true",
380
+ children: "⌕"
381
+ }),
382
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
383
+ type: "search",
384
+ value: search,
385
+ onChange: (event) => setSearch(event.target.value),
386
+ placeholder: "搜索标题或 tag…",
387
+ "aria-label": "搜索标题或 tag"
388
+ }),
389
+ search && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
390
+ type: "button",
391
+ "aria-label": "清空搜索词",
392
+ onClick: () => setSearch(""),
393
+ children: "×"
394
+ })
395
+ ]
396
+ }),
397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
398
+ className: tomato_board_module_css_default.filterMenu,
399
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
400
+ "aria-label": "类型和状态筛选",
401
+ title: "类型和状态筛选",
402
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
403
+ "aria-hidden": "true",
404
+ children: "▽"
405
+ }), (blacklist.types.size > 0 || blacklist.statuses.size > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {})]
406
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
407
+ className: tomato_board_module_css_default.filterPopover,
408
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilterRow, {
409
+ label: "类型",
410
+ options: typeOptions,
411
+ hidden: blacklist.types,
412
+ onToggle: (value) => toggleBlacklist("types", value)
413
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilterRow, {
414
+ label: "状态",
415
+ options: statusOptions,
416
+ hidden: blacklist.statuses,
417
+ onToggle: (value) => toggleBlacklist("statuses", value)
418
+ })]
419
+ })]
420
+ }),
421
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
422
+ variant: "toolbar",
423
+ size: "sm",
424
+ className: tomato_board_module_css_default.headerIconButton,
425
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, {}),
426
+ title: "刷新番茄事项",
427
+ "aria-label": "刷新番茄事项",
428
+ disabled: board.loading,
429
+ onClick: () => void refresh()
430
+ }),
431
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
432
+ variant: "toolbar",
433
+ size: "sm",
434
+ className: tomato_board_module_css_default.headerIconButton,
435
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, {}),
436
+ title: "关闭番茄工作台",
437
+ "aria-label": "关闭番茄工作台",
438
+ onClick: closeWorkbench
439
+ })
440
+ ]
441
+ })]
442
+ }),
443
+ board.error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
444
+ className: tomato_board_module_css_default.error,
445
+ role: "alert",
446
+ children: board.error
447
+ }),
448
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
449
+ className: tomato_board_module_css_default.board,
450
+ children: [statuses.map((status) => {
451
+ const items = filteredItems.filter((item) => item.status === status);
452
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
453
+ className: tomato_board_module_css_default.lane,
454
+ "aria-labelledby": `tomato-lane-${status}`,
455
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
456
+ className: tomato_board_module_css_default.laneHeader,
457
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
458
+ id: `tomato-lane-${status}`,
459
+ children: status
460
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: items.length })]
461
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
462
+ className: tomato_board_module_css_default.cards,
463
+ children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
464
+ className: `${tomato_board_module_css_default.card} ${mutedItems.has(item.itemKey) ? tomato_board_module_css_default.cardMuted : ""}`,
465
+ tabIndex: 0,
466
+ role: "button",
467
+ onClick: () => openItem(item),
468
+ onKeyDown: (event) => {
469
+ if (event.key === "Enter" || event.key === " ") {
470
+ event.preventDefault();
471
+ openItem(item);
472
+ }
473
+ },
474
+ children: [
475
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
476
+ className: tomato_board_module_css_default.cardTopline,
477
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
478
+ className: tomato_board_module_css_default.key,
479
+ children: item.itemKey
480
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
481
+ className: tomato_board_module_css_default.cardButtons,
482
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
483
+ className: tomato_board_module_css_default.muteButton,
484
+ variant: "ghost",
485
+ size: "sm",
486
+ title: mutedItems.has(item.itemKey) ? "取消置灰" : "置灰标记",
487
+ "aria-label": mutedItems.has(item.itemKey) ? `取消置灰 ${item.itemKey}` : `置灰 ${item.itemKey}`,
488
+ "aria-pressed": mutedItems.has(item.itemKey),
489
+ onClick: (event) => {
490
+ event.stopPropagation();
491
+ toggleMutedItem(item.itemKey);
492
+ },
493
+ onKeyDown: (event) => event.stopPropagation(),
494
+ children: "●"
495
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
496
+ className: tomato_board_module_css_default.tomatoLink,
497
+ variant: "ghost",
498
+ size: "sm",
499
+ title: "在番茄中打开事项",
500
+ "aria-label": `在番茄中打开 ${item.itemKey}`,
501
+ onClick: (event) => {
502
+ event.stopPropagation();
503
+ window.open(item.tomatoUrl, "_blank", "noopener,noreferrer");
504
+ },
505
+ onKeyDown: (event) => event.stopPropagation(),
506
+ children: "↗"
507
+ })]
508
+ })]
509
+ }),
510
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: item.title }),
511
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
512
+ className: tomato_board_module_css_default.meta,
513
+ children: [
514
+ item.itemType,
515
+ item.priority,
516
+ item.creator
517
+ ].filter(Boolean).join(" · ")
518
+ })
519
+ ]
520
+ }, item.itemKey))
521
+ })]
522
+ }, status);
523
+ }), !board.loading && !board.error && statuses.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
524
+ className: tomato_board_module_css_default.empty,
525
+ children: "当前没有可显示的番茄事项"
526
+ })]
527
+ }),
528
+ board.selectedItem ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreateConversationDialog, {
529
+ ctx,
530
+ item: board.selectedItem
531
+ }) : null
532
+ ]
533
+ });
534
+ }
535
+ function FilterRow({ label, options, hidden, onToggle }) {
536
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
537
+ className: tomato_board_module_css_default.filterRow,
538
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
539
+ className: hidden.has(option) ? "" : tomato_board_module_css_default.selectedFilter,
540
+ type: "button",
541
+ "aria-pressed": !hidden.has(option),
542
+ onClick: () => onToggle(option),
543
+ children: option
544
+ }, option)) })]
545
+ });
546
+ }
547
+ function TomatoConversationShortcut({ ctx, sessionId, useSessions }) {
548
+ const itemKey = useSessions((state) => {
549
+ const summary = state.byId[sessionId];
550
+ const title = summary?.title ?? summary?.displayTitle ?? "";
551
+ const match = TOMATO_ITEM_KEY_PATTERN.exec(title)?.[1]?.trim();
552
+ if (!match) return "";
553
+ return linkedSessionId(match) === sessionId ? match : "";
554
+ });
555
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
556
+ const [loading, setLoading] = (0, react.useState)(false);
557
+ const [transitioning, setTransitioning] = (0, react.useState)(false);
558
+ const [transitionState, setTransitionState] = (0, react.useState)({
559
+ currentStatus: "",
560
+ tomatoUrl: "",
561
+ transitions: []
562
+ });
563
+ const [transitionError, setTransitionError] = (0, react.useState)(null);
564
+ (0, react.useEffect)(() => {
565
+ if (!itemKey) return;
566
+ const controller = new AbortController();
567
+ setLoading(true);
568
+ setTransitionError(null);
569
+ setTransitionState({
570
+ currentStatus: "",
571
+ tomatoUrl: "",
572
+ transitions: []
573
+ });
574
+ fetch(`/api/tomato-board/transitions/${encodeURIComponent(itemKey)}`, {
575
+ headers: { accept: "application/json" },
576
+ signal: controller.signal
577
+ }).then(async (response) => {
578
+ const body = await response.json();
579
+ if (!response.ok) throw new Error(body.error || `请求失败 (${response.status})`);
580
+ setTransitionState({
581
+ currentStatus: body.currentStatus || "",
582
+ tomatoUrl: body.tomatoUrl || "",
583
+ transitions: body.transitions ?? []
584
+ });
585
+ }).catch((error) => {
586
+ if (error instanceof Error && error.name === "AbortError") return;
587
+ setTransitionError(error instanceof Error ? error.message : "番茄流转状态读取失败");
588
+ }).finally(() => {
589
+ if (!controller.signal.aborted) setLoading(false);
590
+ });
591
+ return () => controller.abort();
592
+ }, [itemKey]);
593
+ if (!itemKey) return null;
594
+ async function transitionTo(transitionName) {
595
+ if (transitioning) return;
596
+ setMenuOpen(false);
597
+ setTransitioning(true);
598
+ setTransitionError(null);
599
+ try {
600
+ const query = new URLSearchParams({ transition: transitionName });
601
+ const response = await fetch(`/api/tomato-board/transition/${encodeURIComponent(itemKey)}?${query}`, {
602
+ method: "POST",
603
+ headers: { accept: "application/json" }
604
+ });
605
+ const body = await response.json();
606
+ if (!response.ok) {
607
+ const selected = transitionState.transitions.find((transition) => transition.transition === transitionName);
608
+ const failure = [
609
+ body.error,
610
+ body.details?.stderr,
611
+ body.details?.stdout
612
+ ].filter(Boolean).join("\n");
613
+ const requiredFieldsMissing = /字段.{0,24}必填|必填.{0,24}字段|流转前需填写/u.test(failure);
614
+ if (selected?.targetStatus === "待测试" && requiredFieldsMissing) {
615
+ const session = ctx.sessions.binding(sessionId)?.session;
616
+ if (!session) throw new Error("当前 Harness 对话未加载,无法交给 AI 继续处理");
617
+ const prompt = [
618
+ `番茄事项 ${itemKey} 流转到「待测试」失败,CLI 提示存在必填字段缺失。`,
619
+ "请先读取番茄事项详情,并结合当前对话和仓库代码进行分析。",
620
+ "基于证据补齐并回读确认以下字段:根因分析、RD引入原因分析、原因描述、修复版本、解决方案。",
621
+ "不要编造业务事实;证据不足时先向我确认。",
622
+ "只有这些字段已经持久化且回读一致后,才能重新执行「修复完成」流转到「待测试」,最后再次回读状态验证。",
623
+ `CLI 失败信息:${failure || "未返回具体原因"}`
624
+ ].join("\n");
625
+ const prompted = await session.prompt([{
626
+ type: "text",
627
+ text: prompt
628
+ }], "queue");
629
+ if (!prompted.ok) throw new Error(`无法把流转任务交给 AI:${prompted.error.message}`);
630
+ setTransitionError("必填字段缺失,已交给当前对话中的 AI 分析并继续处理");
631
+ return;
632
+ }
633
+ throw new Error(failure || `请求失败 (${response.status})`);
634
+ }
635
+ const transitionsResponse = await fetch(`/api/tomato-board/transitions/${encodeURIComponent(itemKey)}`, { headers: { accept: "application/json" } });
636
+ const transitionsBody = await transitionsResponse.json();
637
+ if (!transitionsResponse.ok) throw new Error(transitionsBody.error || `状态刷新失败 (${transitionsResponse.status})`);
638
+ setTransitionState({
639
+ currentStatus: transitionsBody.currentStatus || body.currentStatus || "",
640
+ tomatoUrl: transitionsBody.tomatoUrl || "",
641
+ transitions: transitionsBody.transitions ?? []
642
+ });
643
+ } catch (error) {
644
+ setTransitionError(error instanceof Error ? error.message : "番茄事项流转失败");
645
+ } finally {
646
+ setTransitioning(false);
647
+ }
648
+ }
649
+ const availableTransitions = transitionState.transitions.filter((transition) => !transition.disabled);
650
+ const delegatedToAgent = transitionError?.startsWith("必填字段缺失") === true;
651
+ const transitionTitle = transitionError ? delegatedToAgent ? transitionError : `番茄流转失败:${transitionError}` : loading ? "正在查询番茄事项状态" : availableTransitions.length === 0 ? `当前状态「${transitionState.currentStatus || "未知"}」没有可用流转` : `当前状态:${transitionState.currentStatus || "未知"}`;
652
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
653
+ open: menuOpen,
654
+ portal: true,
655
+ align: "end",
656
+ items: transitionState.transitions.map((transition) => ({
657
+ id: transition.transition,
658
+ label: `流转到 ${transition.targetStatus}`,
659
+ disabled: transition.disabled
660
+ })),
661
+ onSelect: (transitionName) => void transitionTo(transitionName),
662
+ onClose: () => setMenuOpen(false),
663
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
664
+ variant: "toolbar",
665
+ size: "sm",
666
+ className: tomato_board_module_css_default.transitionTrigger,
667
+ title: transitionTitle,
668
+ "aria-label": transitionTitle,
669
+ "aria-haspopup": "menu",
670
+ "aria-expanded": menuOpen,
671
+ disabled: loading || transitioning || availableTransitions.length === 0,
672
+ onClick: () => setMenuOpen((open) => !open),
673
+ children: transitioning || delegatedToAgent || transitionError ? transitioning ? "正在流转…" : delegatedToAgent ? "AI 已接手" : "流转失败" : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
674
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
675
+ className: tomato_board_module_css_default.transitionCaption,
676
+ children: "状态"
677
+ }),
678
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: transitionState.currentStatus || "查询中…" }),
679
+ availableTransitions.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
680
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
681
+ className: tomato_board_module_css_default.transitionDivider,
682
+ "aria-hidden": "true"
683
+ }),
684
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
685
+ className: tomato_board_module_css_default.transitionAction,
686
+ children: "流转"
687
+ }),
688
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: tomato_board_module_css_default.transitionChevron })
689
+ ] }) : null
690
+ ] })
691
+ })
692
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
693
+ variant: "toolbar",
694
+ size: "sm",
695
+ title: "在番茄中打开事项",
696
+ "aria-label": `在番茄中打开 ${itemKey}`,
697
+ disabled: !transitionState.tomatoUrl,
698
+ onClick: () => window.open(transitionState.tomatoUrl, "_blank", "noopener,noreferrer"),
699
+ children: "番茄 ↗"
700
+ })] });
701
+ }
702
+ const inject = [
703
+ "slots",
704
+ "sessions",
705
+ "workspaces"
706
+ ];
707
+ function apply(ctx) {
708
+ ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
709
+ name: "conversation.session.header.actions",
710
+ id: "tomato-shortcut",
711
+ order: 12
712
+ }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TomatoConversationShortcut, {
713
+ ...props,
714
+ ctx
715
+ })));
716
+ ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
717
+ name: "sidebar.footer.action",
718
+ id: "tomato-board"
719
+ }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TomatoBoardAction, {
720
+ ...props,
721
+ openWorkbench: () => {
722
+ if (disposeWorkbench) return;
723
+ emit({ open: true });
724
+ disposeWorkbench = ctx.slots.register({
725
+ name: "conversation",
726
+ priority: -100
727
+ }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TomatoBoardPanel, { ctx }));
728
+ }
729
+ })));
730
+ }
731
+ //#endregion
732
+ exports.apply = apply;
733
+ exports.inject = inject;
734
+ return module.exports;
735
+ }
736
+ });
737
+
738
+ //# sourceMappingURL=client.js.map