@stephen1620/dsh-tomato-board 0.1.0 → 0.1.1

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 CHANGED
@@ -115,6 +115,9 @@ npx @deepseek-ai/dsh plugin --profile web remove @stephen1620/dsh-tomato-board
115
115
  | `iql` | 当前用户负责的 Story、Bug 等 | 工作台查询条件 |
116
116
  | `tomatoOrigin` | `https://osc.gitee.work` | 番茄站点地址 |
117
117
  | `tomatoTenant` | `xly-poc` | 番茄租户 |
118
+ | `cacheTtlMs` | `15000` | 事项查询缓存时间(毫秒,最大 300000) |
119
+ | `maxItems` | `5000` | 单次最多读取的事项数(最大 20000) |
120
+ | `excludedStatuses` | `测试通过` 等 | 不在工作台展示的状态列表 |
118
121
 
119
122
  如需覆盖配置,可在 Harness 的用户 patch 中对 `tomato-board` 节点进行修改。最终生效配置可用下面的命令检查:
120
123
 
@@ -164,6 +167,8 @@ npm login
164
167
  pnpm publish --access public
165
168
  ```
166
169
 
170
+ `prepack` 会检查 `lib` 是否完整且客户端产物是否与当前源码一致,不会在独立仓库中重新执行依赖 Harness workspace 的构建。
171
+
167
172
  作用域包首次发布必须使用 `--access public`。发布前应确认 npm 账号拥有 `@stephenlgf` scope;如果没有,需要改用自己可发布的 scope,并同步修改包名、README 安装命令和 Harness 卸载命令。
168
173
 
169
174
  ## License
package/lib/client.js CHANGED
@@ -18,7 +18,7 @@ window.__ModuleLoader__.load({
18
18
  }
19
19
  //#endregion
20
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}}";
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_notice{color:var(--dsw-alias-label-secondary);margin:0;padding:8px 20px;font-size:11px}.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
22
  const tagId = "@stephen1620/dsh-tomato-board/tomato-board.module.css";
23
23
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
24
24
  const tag = document.createElement("style");
@@ -49,6 +49,7 @@ window.__ModuleLoader__.load({
49
49
  "laneHeader": "Y6kksa_laneHeader",
50
50
  "meta": "Y6kksa_meta",
51
51
  "muteButton": "Y6kksa_muteButton",
52
+ "notice": "Y6kksa_notice",
52
53
  "searchField": "Y6kksa_searchField",
53
54
  "selectedFilter": "Y6kksa_selectedFilter",
54
55
  "sidebarAction": "Y6kksa_sidebarAction",
@@ -69,7 +70,8 @@ window.__ModuleLoader__.load({
69
70
  loading: false,
70
71
  items: [],
71
72
  error: null,
72
- selectedItem: null
73
+ selectedItem: null,
74
+ truncated: false
73
75
  };
74
76
  let disposeWorkbench = null;
75
77
  const listeners = /* @__PURE__ */ new Set();
@@ -161,7 +163,10 @@ window.__ModuleLoader__.load({
161
163
  const response = await fetch("/api/tomato-board/items", { headers: { accept: "application/json" } });
162
164
  const body = await response.json();
163
165
  if (!response.ok) throw new Error(body.error || `请求失败 (${response.status})`);
164
- emit({ items: body.items ?? [] });
166
+ emit({
167
+ items: body.items ?? [],
168
+ truncated: body.truncated === true
169
+ });
165
170
  } catch (error) {
166
171
  emit({ error: error instanceof Error ? error.message : "番茄事项读取失败" });
167
172
  } finally {
@@ -445,6 +450,15 @@ window.__ModuleLoader__.load({
445
450
  role: "alert",
446
451
  children: board.error
447
452
  }),
453
+ board.truncated && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
454
+ className: tomato_board_module_css_default.notice,
455
+ role: "status",
456
+ children: [
457
+ "事项数量已达配置上限,当前仅展示前 ",
458
+ board.items.length,
459
+ " 条。"
460
+ ]
461
+ }),
448
462
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
449
463
  className: tomato_board_module_css_default.board,
450
464
  children: [statuses.map((status) => {
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["css","Modal","Button","Menu","IconRefreshOutline16","IconCloseOutline16","IconChevronDownOutline14"],"sources":["../../../packages/core/session/src/types.ts","../../../packages/client/tomato-board/src/client/TomatoBoard.tsx"],"sourcesContent":["import type { Branded } from '@deepseek-ai/dsh-brand'\nimport type {\n AssistantMessage,\n ToolCallId,\n LlmCallConfig,\n LlmCallConfigAdapterDefaults,\n LlmFailure,\n StreamChunk,\n TokenUsage,\n ToolResultMessage,\n ToolSchema,\n UserMessage,\n} from '@deepseek-ai/dsh-llm'\nimport type { JsonValue } from './json.ts'\n\n// The lossless-JSON payload type belongs to this client-safe face too: a wire\n// contract carrying JSON data must not import the root entry, which merges\n// `ctx.sessions` (a Host-only SessionStore) into every consumer's program.\nexport type { JsonValue } from './json.ts'\n\n/** Identifies one session in the store (and its persistence artifacts). */\nexport type SessionId = Branded<'SessionId'>\n\n/**\n * Brand a string as a {@link SessionId}.\n * @param id - the raw session id string.\n * @returns the same string, branded (a compile-time cast — no runtime cost).\n */\nexport function SessionId(id: string): SessionId {\n return id as SessionId\n}\n\n/**\n * The on-disk session format version, stamped into every newly-written {@link SessionHeader}\n * and enforced by every persistence backend on load. The single source of truth for the\n * version — write sites and the load-time check all read it.\n * While the harness is unreleased it is pinned at `0`: no compatibility is\n * implied, incompatible logs are rejected, and no migration is provided.\n *\n * The version is a single monotonic integer with no major/minor split. Whether\n * a bump is needed is decided by what the WRITER emits, never by what a newer\n * reader can accept: bump exactly when an older runtime could no longer handle\n * a new log with full semantic correctness (\"parses without error\" is not\n * correctness — silently skipping content that shapes reconstruction is a\n * wrong read). Only structural changes reach that bar: the header shape, the\n * {@link SessionEvent} envelope, core event semantics, or the surface\n * mechanism (the {@link SurfaceEventType} set and {@link SurfaceOp} variants).\n * Adding an ordinary event type does not bump: the generated known-event guard\n * makes older runtimes refuse logs containing a type they do not understand.\n * When in doubt, bump: a near-identity upgrade step is almost free, a missed\n * bump makes older runtimes read new logs wrong silently. The full mechanism\n * (upgrade-step chain, in-memory view conversion, migrate-on-continue) is\n * recorded in the fail-closed-session-event-vocabulary Agent Note\n * (`.agents/notes/implemented/simplification/2026-08-25-fail-closed-session-event-vocabulary.md`).\n */\nexport const SESSION_FORMAT_VERSION = 0\n\n/**\n * Immutable validated storage metadata, kept outside the conversation event log.\n */\nexport interface SessionHeader {\n /**\n * On-disk format version, stamped from {@link SESSION_FORMAT_VERSION} when the\n * session is created. A persistence backend rejects any other version on load\n * (no migration — see the constant).\n */\n readonly version: number\n /** The session's id (mirrors the {@link Session}'s id). */\n readonly id: SessionId\n /** Non-negative safe-integer Unix epoch milliseconds when the session was created. */\n readonly createdAt: number\n /** Absolute working directory the session was created in (if any). */\n readonly cwd?: string\n /** The session this one was forked from (seed lineage), if any. */\n readonly parentSession?: SessionId\n /**\n * How many leading events were inherited through a seed. Persisting this\n * boundary lets resume and replay distinguish parent history from child work.\n */\n readonly seedLength?: number\n /**\n * Coarse product classification for a session created as a subagent child.\n * This is presentation metadata, not proof that the child is continuable.\n */\n readonly origin?: 'subagent'\n /**\n * Delegation depth: absent (zero) for a top-level session, parent depth + 1\n * for a subagent child. Persisted so a recursion budget survives restart and\n * resume — a runtime-only depth would reset a resumed child to top-level.\n */\n readonly delegationDepth?: number\n /**\n * Id of the agent preset this session's agent was composed from, when the\n * deployment composes per session. Durable because the preset decides the\n * session's tools and prompt: a resume that restored a different composition\n * would replay history the model can no longer act on.\n */\n readonly agentPreset?: string\n}\n\n/**\n * Options for creating a {@link Session} via the store. `seed` replays/forks\n * an existing event log; `meta` carries the caller-supplied storage fields the\n * store folds into a {@link SessionHeader}.\n */\nexport interface CreateSessionOptions {\n /** Initial replay or fork history supplied at construction. */\n readonly seed?: readonly SessionEvent[]\n /**\n * Storage metadata read once before publication. `seedLength` is explicit\n * because a resumed seed contains the full stored log, not only its inherited prefix.\n */\n readonly meta?: {\n readonly cwd?: string\n readonly parentSession?: SessionId\n readonly createdAt?: number\n readonly seedLength?: number\n readonly origin?: 'subagent'\n readonly delegationDepth?: number\n readonly agentPreset?: string\n }\n}\n\n/**\n * Fresh storage values transferred to {@link SessionStore.prepare} without a\n * second serialization copy. Callers retain no mutable aliases.\n */\nexport interface RestoredSessionOptions {\n /** Fresh detached storage events to validate and freeze in place. */\n readonly seed: SessionEvent[]\n /** Fresh detached storage metadata to validate and freeze in place. */\n readonly meta: SessionHeader\n /** Select the persistence ownership-transfer path. */\n readonly seedSource: 'persistence'\n}\n\n/** Inputs accepted while constructing an unpublished Session. */\nexport type PrepareSessionOptions =\n | (CreateSessionOptions & { readonly seedSource?: undefined })\n | RestoredSessionOptions\n\n/** Why an active agent driver was cancelled. */\nexport type AgentCancelCause =\n | { readonly kind: 'user' }\n | { readonly kind: 'parent' }\n | { readonly kind: 'hook'; readonly reason: string }\n | { readonly kind: 'disposed' }\n\n/** Durable cancellation cause, including imports whose original coarse record carried no cause. */\nexport type TurnEndCancelCause = AgentCancelCause | { readonly kind: 'legacy' }\n\n/**\n * Why a turn ended. Merge-extensible sum type.\n */\nexport interface TurnEndReasonMap {\n completed: { kind: 'completed' }\n /** A cancellation request interrupted the live turn. */\n aborted: { kind: 'aborted'; reason: TurnEndCancelCause }\n\n blocked: { kind: 'blocked' }\n /**\n * The turn failed. `error` is always a structured failure: the `LlmError`\n * facts verbatim, or `{ message: errorChain(error), code: 'UNKNOWN' }`\n * flattened from any other error.\n */\n error: { kind: 'error'; error: LlmFailure }\n /** At least one step reached its output-token ceiling, even if a plugin continued the turn. */\n 'max-tokens': { kind: 'max-tokens' }\n /**\n * A persistence backend closed a crash-orphaned turn on reload. The loop never\n * emits this marker, and the events recorded before the crash remain intact.\n */\n interrupted: { kind: 'interrupted' }\n}\n\n/** The union over {@link TurnEndReasonMap} — why a turn ended; plugins extend it by merging variants into the map. */\nexport type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap]\n\n/**\n * Logged request state outside derived history: call config, system prompt, and\n * tools. The latest full `request/header` snapshot reconstructs it; canonical\n * empty optional fields are absent.\n */\nexport interface EpochHeader {\n /** The conversation's call configuration (provider, model, reasoning effort, and sampling scalars). */\n config: LlmCallConfig\n /** Effective config fields materialized from the exact adapter rather than proposed by a caller. */\n adapterDefaults?: LlmCallConfigAdapterDefaults\n /** Rendered system prompt text; absent for a system-less request. */\n system?: string\n /** Assembled tool schemas; absent for a tool-less request. */\n tools?: ToolSchema[]\n}\n\n/** Registration-bound metadata for one resolved model route. */\nexport interface RequestContext {\n /** Registered provider route the metadata belongs to. */\n provider: string\n /** Provider-owned model id the metadata belongs to. */\n model: string\n /** Maximum combined request and response context in tokens, when advertised. */\n contextWindow?: number\n}\n\n/**\n * Why a `request/header` snapshot was appended: `'initial'` — the log's first\n * header (a new conversation); `'resume'` — a loop instance's first request\n * over a log that already has header events (process restart, fork seed);\n * `'change'` — a later request used a different header, with `startsSeries`\n * preserving a coincident series boundary; `'series'` — an unchanged header\n * began an explicitly distinct message series or followed a surface replacement.\n */\nexport type RequestHeaderReason = 'initial' | 'resume' | 'change' | 'series'\n\n/**\n * The merge-extensible, append-only source of truth for an agent interaction.\n * Message history is derived from this log. Every event is lossless JSON and\n * sequence numbers stay contiguous, including raw chunks, so persistence can\n * store the canonical log verbatim.\n */\nexport interface SessionEventMap {\n /**\n * Opens turn `turn` before the loop claims queued input or runs pre-step.\n * Rejection, empty input, cancellation, or failure may close it with no\n * step; otherwise the following identified `user/message` event or batch\n * records the messages entering the step.\n */\n 'turn/start': { turn: number }\n /**\n * Closes turn `turn` with the {@link TurnEndReason} that ended it. A turn\n * with no entered step has no `step/start` or `step/end`. The loop does not await a\n * flush at turn boundaries: `dsh-session-checkpoint-policy` owns the\n * per-request durability checkpoint, and consumers that read storage after\n * `whenIdle()` flush themselves. Success commits the turn; rejection is\n * reported live and does not prevent later work.\n */\n 'turn/end': { turn: number; reason: TurnEndReason }\n /** Opens step `step` of turn `turn` — one model call plus the tool executions it requested. */\n 'step/start': { turn: number; step: number }\n /** Closes step `step` of turn `turn`. */\n 'step/end': { turn: number; step: number }\n /**\n * A user-role message on the model-visible surface: a direct human prompt\n * (the queued message claimed for this turn), a synthetic `agent.inject()`\n * context (file-change notices, subdir AGENTS.md, skill content, cron\n * notifications, …), or an entered goal continuation round. All three\n * project their `content` verbatim; `source` tells them apart.\n */\n 'user/message': UserMessage\n /** Raw stream chunk — token-level replay fidelity. */\n 'assistant/chunk': { turn: number; step: number; chunk: StreamChunk }\n /**\n * Assembled assistant message for one step (derived history uses this).\n * Carries the step's `usage` when the adapter reported token accounting, so\n * the model output and its accounting travel together (there is no separate\n * usage record). `usage` is absent when the adapter reported none. A turn\n * cancelled mid-stream finalizes its delivered text/reasoning prefix as this\n * event with `interrupted: true`; undispatched tool calls are absent. The\n * marker distinguishes that prefix without re-deriving interruption from turn\n * boundaries. An aborted turn with no such event streamed no visible content.\n */\n 'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true }\n /**\n * The model requested one tool invocation: `name` with the raw `arguments`\n * JSON string exactly as the model produced it (unparsed). `callId` pairs the\n * call with its `tool/result`.\n */\n 'tool/call': { turn: number; step: number; callId: ToolCallId; name: string; arguments: string }\n /**\n * A completed tool call's model-facing result, optional internal failure\n * identity, and optional tool-private `meta` presentation payload. `meta` is\n * opaque to the core (the producing tool owns its shape and reads it back in\n * `presentResult`) but MUST be JSON-serializable: `Session.append`\n * runtime-validates all event data with `isJsonValue`, so a non-serializable\n * `meta` is rejected at the source, and the durable log reproduces the\n * identical card on replay. Absent\n * unless the tool attaches one (e.g. `dsh-tool-fs` carries its result-time\n * contextual diff here).\n */\n 'tool/result': {\n turn: number\n step: number\n message: ToolResultMessage\n error?: { name: string; code: string }\n meta?: JsonValue\n }\n /**\n * Full header for the next request, appended inside its step before dispatch.\n * It is log-only; the latest snapshot reconstructs the request header.\n */\n 'request/header': {\n header: EpochHeader\n reason: RequestHeaderReason\n /** A changed header also begins a distinct model-message series. */\n startsSeries?: true\n }\n /**\n * Route metadata for the next request, logged only when the route or capacity\n * changes. It does not participate in request reconstruction or header equality.\n */\n 'request/context': RequestContext\n /**\n * Marks the end of a constructor seed. Events before it have smaller seq\n * values and came from the seed (resume, fork, or replay); this lifecycle\n * produced none of them. This log-only event is the durable projection of\n * {@link Session.firstLiveSeq}. Its payload is empty — position and `time`\n * carry the meaning.\n *\n * Locate the LAST one in stored history. A seed already ending in one is not\n * re-marked, so reopening an untouched session does not grow its log per\n * pickup and the event need not be at the current `firstLiveSeq`.\n *\n * `Session`'s constructor is the only legitimate writer. The invariant\n * companion deliberately constrains nothing here, so a plugin appending one\n * would silently classify every live bracket before it as seed history.\n *\n * An owner of a standalone open/close bracket (`compaction/start` …\n * `compaction/end`) reads it because seed history and live work are otherwise\n * byte-identical: an unmatched opening marker before this event belongs to\n * an ended lifecycle, whatever ended it. NOT a liveness signal about other\n * writers — a concurrently live session holds its own boundary elsewhere,\n * so tolerating concurrent writers needs a signal beyond the log.\n */\n 'session/end-seed': Record<string, never>\n}\n\n/** The appendable event-type keys of {@link SessionEventMap}, plugin-merged extensions included. */\nexport type SessionEventType = keyof SessionEventMap\n\n/**\n * The subset of {@link SessionEventType} values whose events produce LLM\n * messages and are eligible to appear on the ordered surface. Only these\n * event types may carry {@link SurfaceOp} and {@link SessionEvent.sourceEventSeqs}.\n */\nexport type SurfaceEventType =\n | 'user/message'\n | 'assistant/message'\n | 'tool/result'\n\n/**\n * A {@link SessionEvent} that is **on** the ordered surface — its\n * `surfaceOp` is guaranteed present (mandatory), narrowed from a\n * surface-eligible {@link SessionEvent} by checking both `type` and\n * `surfaceOp` at runtime.\n *\n * Use the `isSurfaceEvent` type guard (in `surface.ts`) to narrow a\n * `SessionEvent` to this type.\n */\nexport type SurfaceEvent = SessionEvent<SurfaceEventType> & { surfaceOp: SurfaceOp }\n\n/**\n * How a session event entered the ordered surface. Only valid on\n * {@link SurfaceEventType} events.\n *\n * - `'append'`: added to the tail — normal path for user/assistant/tool\n * messages.\n * - `{ op: 'replace', start, end }`: replaces surface nodes from `start`\n * (inclusive) through `end` (inclusive) with this node. Both must exist as\n * surface nodes in the current surface. `start === end` replaces a single\n * node. The node's {@link SessionEvent.sourceEventSeqs} must include every\n * shadowed surface node. Used by compaction; any surface-replacing producer\n * may use it.\n */\nexport type SurfaceOp =\n | 'append'\n | { op: 'replace'; start: number; end: number }\n\n/**\n * Surface placement and cited source-event seqs for {@link Session.append}. Required on\n * message-producing events and forbidden on log-only events.\n */\nexport interface SurfaceIntent {\n surfaceOp: SurfaceOp\n /**\n * Complete set of known source-event seqs. `assistant/message` may use a\n * present empty array for a known empty provider stream; when the field is\n * absent, the event does not record which earlier events produced the message.\n * Other surface events require a non-empty set when this field is present.\n */\n sourceEventSeqs?: number[]\n}\n\n/**\n * One immutable entry in the session log.\n *\n * A proper discriminated union over `type` (not independent `type`/`data`\n * unions), so `switch (event.type)` narrows `event.data` without casts.\n *\n * The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:\n * they only exist on {@link SurfaceEventType} variants (`user/message`,\n * `assistant/message`, `tool/result`).\n * Non-surface events (boundary markers, chunks, usage, errors) never carry\n * surface metadata — the compiler enforces this at `Session.append()`\n * call sites.\n */\nexport type SessionEvent<T extends SessionEventType = SessionEventType> = {\n [K in SessionEventType]: {\n type: K\n /** Monotonic sequence number within the session. */\n seq: number\n /** Unix epoch milliseconds. */\n time: number\n data: SessionEventMap[K]\n } & (K extends SurfaceEventType ? {\n /**\n * Seq numbers of earlier events that this event cites as sources\n * (e.g. the `assistant/chunk` seqs that built an `assistant/message`,\n * or the surface nodes shadowed by a compaction replace node). An\n * `assistant/message` may carry a present empty array for a known empty\n * provider stream; when the field is absent, the event does not record which\n * earlier events produced the message.\n */\n sourceEventSeqs?: number[]\n /** How this event entered the surface; absent for non-surface events. */\n surfaceOp?: SurfaceOp\n } : object)\n}[T]\n","import { useEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-api-session-controller/client'\nimport type { WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client'\nimport type {} from '@deepseek-ai/dsh-api-workspace-controller/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-layout/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport {\n Button, IconChevronDownOutline14, IconCloseOutline16, IconRefreshOutline16, Menu, Modal,\n} from '@deepseek-ai/dsh-client-ui-primitives'\nimport type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport { SessionId } from '@deepseek-ai/dsh-session/types'\nimport css from './tomato-board.module.css'\n\ninterface TomatoItem {\n itemKey: string\n title: string\n status: string\n itemType: string\n workspace: string\n creator: string\n priority: string\n tomatoUrl: string\n}\n\ninterface TomatoTransition {\n transition: string\n targetStatus: string\n disabled: boolean\n disabledReason?: string\n}\n\ninterface TomatoTransitionState {\n currentStatus: string\n tomatoUrl: string\n transitions: TomatoTransition[]\n}\n\ninterface BoardState {\n open: boolean\n loading: boolean\n items: TomatoItem[]\n error: string | null\n selectedItem: TomatoItem | null\n}\n\nlet state: BoardState = { open: false, loading: false, items: [], error: null, selectedItem: null }\nlet disposeWorkbench: (() => void) | null = null\nconst listeners = new Set<() => void>()\nconst emit = (patch: Partial<BoardState>) => {\n state = { ...state, ...patch }\n for (const listener of listeners) listener()\n}\nconst subscribe = (listener: () => void) => {\n listeners.add(listener)\n return () => listeners.delete(listener)\n}\nconst snapshot = () => state\n\n// 真实番茄事项 itemKey 形如 `Gitee-Test-2026-737`、`Proxima-1116`:字母前缀(可含连字符分段)+ 数字结尾。\n// 只匹配该格式可避免把任意 `[xxx]` 开头的普通对话标题(如 `[分析]`、`[每日复盘]`)误判为番茄事项,\n// 进而错误地在对话头上渲染出状态流转按钮并触发无意义的 CLI 调用。\nconst TOMATO_ITEM_KEY_PATTERN = /^\\[([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*-\\d+)\\]/u\n\nconst TOMATO_TYPE_OPTIONS = ['测试缺陷', '缺陷', 'Bug', 'EnablerStory', 'Story', 'Task']\nconst TOMATO_STATUS_ORDER = [\n '新建', 'Bugfix', '修复中', '开发中', '待测试', '测试中', '测试通过', '已完成',\n '已取消', '延期解决', '测试完成', '待开发', '不修复', '已挂起',\n]\nconst TOMATO_FILTER_BLACKLIST_KEY = 'taskboard.tomatoFilterBlacklist.v1'\nconst TOMATO_SESSION_LINKS_KEY = 'taskboard.tomatoSessionLinks.v1'\nconst TOMATO_MUTED_ITEMS_KEY = 'taskboard.tomatoMutedItems.v1'\n\nfunction readSessionLinks(): Record<string, string> {\n try {\n const value = JSON.parse(window.localStorage.getItem(TOMATO_SESSION_LINKS_KEY) ?? '{}')\n return value && typeof value === 'object' ? value as Record<string, string> : {}\n } catch {\n return {}\n }\n}\n\nfunction linkedSessionId(itemKey: string): SessionId | '' {\n const value = readSessionLinks()[itemKey]\n return typeof value === 'string' && value ? SessionId(value) : ''\n}\n\nfunction saveSessionLink(itemKey: string, sessionId: SessionId) {\n const links = readSessionLinks()\n links[itemKey] = sessionId\n window.localStorage.setItem(TOMATO_SESSION_LINKS_KEY, JSON.stringify(links))\n}\n\nfunction readFilterBlacklist(): { types: Set<string>; statuses: Set<string> } {\n try {\n const value = JSON.parse(window.localStorage.getItem(TOMATO_FILTER_BLACKLIST_KEY) ?? '{}')\n return {\n types: new Set(Array.isArray(value?.types) ? value.types.filter((item: unknown): item is string => typeof item === 'string') : []),\n statuses: new Set(Array.isArray(value?.statuses) ? value.statuses.filter((item: unknown): item is string => typeof item === 'string') : []),\n }\n } catch {\n return { types: new Set(), statuses: new Set() }\n }\n}\n\nfunction readMutedItems(): Set<string> {\n try {\n const value = JSON.parse(window.localStorage.getItem(TOMATO_MUTED_ITEMS_KEY) ?? '[]')\n return new Set(Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [])\n } catch {\n return new Set()\n }\n}\n\nasync function refresh() {\n emit({ loading: true, error: null })\n try {\n const response = await fetch('/api/tomato-board/items', { headers: { accept: 'application/json' } })\n const body = await response.json() as { items?: TomatoItem[]; error?: string }\n if (!response.ok) throw new Error(body.error || `请求失败 (${response.status})`)\n emit({ items: body.items ?? [] })\n } catch (error) {\n emit({ error: error instanceof Error ? error.message : '番茄事项读取失败' })\n } finally {\n emit({ loading: false })\n }\n}\n\nfunction closeWorkbench() {\n const dispose = disposeWorkbench\n disposeWorkbench = null\n emit({ open: false, selectedItem: null })\n dispose?.()\n}\n\nfunction TomatoBoardAction({ wide, openWorkbench }: { wide: boolean; openWorkbench: () => void }) {\n return (\n <button className={css.sidebarAction} type=\"button\" title=\"番茄工作台\" onClick={openWorkbench}>\n <span className={css.tomatoIcon} aria-hidden=\"true\">T</span>\n {wide && <span>番茄工作台</span>}\n </button>\n )\n}\n\nfunction CreateConversationDialog({ ctx, item }: { ctx: Context; item: TomatoItem }) {\n const [workspaceId, setWorkspaceId] = useState('')\n const [workspaceMenuOpen, setWorkspaceMenuOpen] = useState(false)\n const [creating, setCreating] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const workspaces = useSyncExternalStore(\n listener => ctx.workspaces.list.subscribe(listener),\n () => ctx.workspaces.list.getSnapshot(),\n () => ctx.workspaces.list.getSnapshot(),\n )\n const selectedWorkspace = workspaces.items.find(workspace => workspace.workspaceId === workspaceId)\n\n async function createConversation() {\n if (!selectedWorkspace || creating) return\n setCreating(true)\n setError(null)\n try {\n const createdSessionId = await ctx.sessions.create({ workspaceId: selectedWorkspace.workspaceId })\n const session = ctx.sessions.binding(createdSessionId)?.session\n if (!session) throw new Error('新建对话未能在 Harness 中加载')\n const title = `[${item.itemKey}] ${item.title}`\n const renamed = await session.rename(title)\n if (!renamed.ok) throw new Error(`对话标题设置失败:${renamed.error.message}`)\n const prompt = [\n `请处理番茄事项 ${item.itemKey}。`,\n `标题:${item.title}`,\n `类型:${item.itemType || '未设置'}`,\n `状态:${item.status || '未设置'}`,\n `优先级:${item.priority || '未设置'}`,\n ].join('\\n')\n const prompted = await session.prompt([{ type: 'text', text: prompt }], 'queue')\n if (!prompted.ok) throw new Error(`事项上下文写入失败:${prompted.error.message}`)\n saveSessionLink(item.itemKey, createdSessionId)\n ctx.sessions.open(createdSessionId)\n closeWorkbench()\n } catch (reason) {\n setError(reason instanceof Error ? reason.message : 'Harness 对话创建失败')\n } finally {\n setCreating(false)\n }\n }\n\n return (\n <Modal\n open\n onClose={() => emit({ selectedItem: null })}\n title=\"选择处理该事项的仓库\"\n closeLabel=\"关闭\"\n description={item.title}\n className={css.createDialog!}\n footer={(\n <>\n <Button variant=\"ghost\" onClick={() => emit({ selectedItem: null })}>取消</Button>\n <Button variant=\"primary\" disabled={!selectedWorkspace || creating} onClick={() => void createConversation()}>\n {creating ? '正在创建…' : '创建并进入对话'}\n </Button>\n </>\n )}\n >\n <div className={css.dialogBody}>\n <span>{item.itemKey}</span>\n {error ? <div className={css.error} role=\"alert\">{error}</div> : null}\n <Menu\n open={workspaceMenuOpen}\n portal\n items={workspaces.items.map((workspace: WorkspaceView) => ({\n id: workspace.workspaceId,\n label: workspace.title,\n }))}\n selectedId={workspaceId || undefined}\n onSelect={id => {\n setWorkspaceId(id)\n setWorkspaceMenuOpen(false)\n }}\n onClose={() => setWorkspaceMenuOpen(false)}\n anchor={(\n <Button\n className={css.workspaceTrigger}\n variant=\"outline\"\n aria-haspopup=\"menu\"\n aria-expanded={workspaceMenuOpen}\n onClick={() => setWorkspaceMenuOpen(open => !open)}\n >\n <span>{selectedWorkspace?.title ?? '选择 Harness 项目…'}</span>\n <span aria-hidden=\"true\">⌄</span>\n </Button>\n )}\n />\n {selectedWorkspace ? <code>{selectedWorkspace.path}</code> : null}\n </div>\n </Modal>\n )\n}\n\nfunction TomatoBoardPanel({ ctx }: { ctx: Context }) {\n const board = useSyncExternalStore(subscribe, snapshot, snapshot)\n const workbenchRef = useRef<HTMLElement>(null)\n const [search, setSearch] = useState('')\n const [blacklist, setBlacklist] = useState(readFilterBlacklist)\n const [mutedItems, setMutedItems] = useState(readMutedItems)\n const sessions = useSyncExternalStore(\n listener => ctx.sessions.list.subscribe(listener),\n () => ctx.sessions.list.getSnapshot(),\n () => ctx.sessions.list.getSnapshot(),\n )\n\n useEffect(() => {\n if (board.open && board.items.length === 0 && !board.loading && !board.error) void refresh()\n }, [board.error, board.items.length, board.loading, board.open])\n useEffect(() => {\n if (!board.open) return\n const closeOnOutsideNavigation = (event: PointerEvent) => {\n if (!(event.target instanceof Element)) return\n if (workbenchRef.current?.contains(event.target)) return\n if (event.target.closest('[role=\"dialog\"], [role=\"menu\"]')) return\n closeWorkbench()\n }\n document.addEventListener('pointerdown', closeOnOutsideNavigation, true)\n return () => document.removeEventListener('pointerdown', closeOnOutsideNavigation, true)\n }, [board.open])\n if (!board.open) return null\n\n function openItem(item: TomatoItem) {\n const stored = linkedSessionId(item.itemKey)\n const titlePrefix = `[${item.itemKey}]`\n const discovered = sessions.ids.find(id => {\n const summary = sessions.byId[id]\n return summary?.title?.startsWith(titlePrefix) || summary?.displayTitle.startsWith(titlePrefix)\n })\n const associated = stored && sessions.byId[stored] ? stored : discovered\n if (associated) {\n saveSessionLink(item.itemKey, associated)\n ctx.sessions.open(associated)\n closeWorkbench()\n return\n }\n emit({ selectedItem: item })\n }\n\n const normalizedSearch = search.trim().toLowerCase()\n const filteredItems = board.items.filter(item => (\n !blacklist.types.has(item.itemType)\n && !blacklist.statuses.has(item.status)\n && (!normalizedSearch || [\n item.itemKey, item.title, item.itemType, item.status, item.workspace, item.creator, item.priority,\n ].join(' ').toLowerCase().includes(normalizedSearch))\n ))\n const typeOptions = [...new Set([...TOMATO_TYPE_OPTIONS, ...board.items.map(item => item.itemType).filter(Boolean)])]\n const statusOptions = [...new Set([\n ...TOMATO_STATUS_ORDER,\n ...board.items.map(item => item.status).filter(status => !TOMATO_STATUS_ORDER.includes(status)),\n ])]\n const statuses = [...new Set([\n ...TOMATO_STATUS_ORDER.filter(status => filteredItems.some(item => item.status === status)),\n ...filteredItems.map(item => item.status).filter(status => !TOMATO_STATUS_ORDER.includes(status)),\n ])]\n\n const toggleBlacklist = (kind: 'types' | 'statuses', value: string) => setBlacklist(current => {\n const nextValues = new Set(current[kind])\n if (nextValues.has(value)) nextValues.delete(value)\n else nextValues.add(value)\n const next = { ...current, [kind]: nextValues }\n window.localStorage.setItem(TOMATO_FILTER_BLACKLIST_KEY, JSON.stringify({\n types: [...next.types],\n statuses: [...next.statuses],\n }))\n return next\n })\n\n const toggleMutedItem = (itemKey: string) => setMutedItems(current => {\n const next = new Set(current)\n if (next.has(itemKey)) next.delete(itemKey)\n else next.add(itemKey)\n window.localStorage.setItem(TOMATO_MUTED_ITEMS_KEY, JSON.stringify([...next]))\n return next\n })\n\n return (\n <section ref={workbenchRef} className={css.workbench} aria-label=\"番茄工作台\">\n <header className={css.header}>\n <div>\n <h1>番茄工作台</h1>\n <p>{board.loading ? '正在读取番茄事项…' : `显示 ${filteredItems.length} / ${board.items.length} 个事项`}</p>\n </div>\n <div className={css.actions}>\n <label className={css.searchField}>\n <span aria-hidden=\"true\">⌕</span>\n <input\n type=\"search\"\n value={search}\n onChange={event => setSearch(event.target.value)}\n placeholder=\"搜索标题或 tag…\"\n aria-label=\"搜索标题或 tag\"\n />\n {search && <button type=\"button\" aria-label=\"清空搜索词\" onClick={() => setSearch('')}>×</button>}\n </label>\n <details className={css.filterMenu}>\n <summary aria-label=\"类型和状态筛选\" title=\"类型和状态筛选\">\n <span aria-hidden=\"true\">▽</span>\n {(blacklist.types.size > 0 || blacklist.statuses.size > 0) && <i />}\n </summary>\n <div className={css.filterPopover}>\n <FilterRow label=\"类型\" options={typeOptions} hidden={blacklist.types} onToggle={value => toggleBlacklist('types', value)} />\n <FilterRow label=\"状态\" options={statusOptions} hidden={blacklist.statuses} onToggle={value => toggleBlacklist('statuses', value)} />\n </div>\n </details>\n <Button\n variant=\"toolbar\"\n size=\"sm\"\n className={css.headerIconButton}\n icon={<IconRefreshOutline16 />}\n title=\"刷新番茄事项\"\n aria-label=\"刷新番茄事项\"\n disabled={board.loading}\n onClick={() => void refresh()}\n />\n <Button\n variant=\"toolbar\"\n size=\"sm\"\n className={css.headerIconButton}\n icon={<IconCloseOutline16 />}\n title=\"关闭番茄工作台\"\n aria-label=\"关闭番茄工作台\"\n onClick={closeWorkbench}\n />\n </div>\n </header>\n {board.error && <div className={css.error} role=\"alert\">{board.error}</div>}\n <div className={css.board}>\n {statuses.map(status => {\n const items = filteredItems.filter(item => item.status === status)\n return (\n <section className={css.lane} key={status} aria-labelledby={`tomato-lane-${status}`}>\n <header className={css.laneHeader}>\n <h2 id={`tomato-lane-${status}`}>{status}</h2>\n <span>{items.length}</span>\n </header>\n <div className={css.cards}>\n {items.map(item => (\n <article\n className={`${css.card} ${mutedItems.has(item.itemKey) ? css.cardMuted : ''}`}\n key={item.itemKey}\n tabIndex={0}\n role=\"button\"\n onClick={() => openItem(item)}\n onKeyDown={event => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n openItem(item)\n }\n }}\n >\n <div className={css.cardTopline}>\n <span className={css.key}>{item.itemKey}</span>\n <div className={css.cardButtons}>\n <Button\n className={css.muteButton}\n variant=\"ghost\"\n size=\"sm\"\n title={mutedItems.has(item.itemKey) ? '取消置灰' : '置灰标记'}\n aria-label={mutedItems.has(item.itemKey) ? `取消置灰 ${item.itemKey}` : `置灰 ${item.itemKey}`}\n aria-pressed={mutedItems.has(item.itemKey)}\n onClick={event => {\n event.stopPropagation()\n toggleMutedItem(item.itemKey)\n }}\n onKeyDown={event => event.stopPropagation()}\n >\n ●\n </Button>\n <Button\n className={css.tomatoLink}\n variant=\"ghost\"\n size=\"sm\"\n title=\"在番茄中打开事项\"\n aria-label={`在番茄中打开 ${item.itemKey}`}\n onClick={event => {\n event.stopPropagation()\n window.open(item.tomatoUrl, '_blank', 'noopener,noreferrer')\n }}\n onKeyDown={event => event.stopPropagation()}\n >\n ↗\n </Button>\n </div>\n </div>\n <strong>{item.title}</strong>\n <span className={css.meta}>{[item.itemType, item.priority, item.creator].filter(Boolean).join(' · ')}</span>\n </article>\n ))}\n </div>\n </section>\n )\n })}\n {!board.loading && !board.error && statuses.length === 0 && (\n <div className={css.empty}>当前没有可显示的番茄事项</div>\n )}\n </div>\n {board.selectedItem ? <CreateConversationDialog ctx={ctx} item={board.selectedItem} /> : null}\n </section>\n )\n}\n\nfunction FilterRow({ label, options, hidden, onToggle }: {\n label: string\n options: string[]\n hidden: ReadonlySet<string>\n onToggle: (value: string) => void\n}) {\n return (\n <div className={css.filterRow}>\n <span>{label}</span>\n <div>\n {options.map(option => (\n <button\n key={option}\n className={hidden.has(option) ? '' : css.selectedFilter}\n type=\"button\"\n aria-pressed={!hidden.has(option)}\n onClick={() => onToggle(option)}\n >\n {option}\n </button>\n ))}\n </div>\n </div>\n )\n}\n\nfunction TomatoConversationShortcut({ ctx, sessionId, useSessions }: PropsRuntime<'conversation.session.header.actions'> & { ctx: Context }) {\n const itemKey = useSessions(state => {\n const summary = state.byId[sessionId]\n const title = summary?.title ?? summary?.displayTitle ?? ''\n const match = TOMATO_ITEM_KEY_PATTERN.exec(title)?.[1]?.trim()\n if (!match) return ''\n // 仅当该对话确实由番茄工作台创建并记录在本机映射中时,才认定它是番茄事项对话。\n // 这样即便普通新建对话的标题恰好命中 itemKey 格式,也不会被误带上状态流转 UI。\n return linkedSessionId(match) === sessionId ? match : ''\n })\n const [menuOpen, setMenuOpen] = useState(false)\n const [loading, setLoading] = useState(false)\n const [transitioning, setTransitioning] = useState(false)\n const [transitionState, setTransitionState] = useState<TomatoTransitionState>({\n currentStatus: '',\n tomatoUrl: '',\n transitions: [],\n })\n const [transitionError, setTransitionError] = useState<string | null>(null)\n\n useEffect(() => {\n if (!itemKey) return\n const controller = new AbortController()\n setLoading(true)\n setTransitionError(null)\n setTransitionState({ currentStatus: '', tomatoUrl: '', transitions: [] })\n void fetch(`/api/tomato-board/transitions/${encodeURIComponent(itemKey)}`, {\n headers: { accept: 'application/json' },\n signal: controller.signal,\n }).then(async response => {\n const body = await response.json() as TomatoTransitionState & { error?: string }\n if (!response.ok) throw new Error(body.error || `请求失败 (${response.status})`)\n setTransitionState({\n currentStatus: body.currentStatus || '',\n tomatoUrl: body.tomatoUrl || '',\n transitions: body.transitions ?? [],\n })\n }).catch(error => {\n if (error instanceof Error && error.name === 'AbortError') return\n setTransitionError(error instanceof Error ? error.message : '番茄流转状态读取失败')\n }).finally(() => {\n if (!controller.signal.aborted) setLoading(false)\n })\n return () => controller.abort()\n }, [itemKey])\n\n if (!itemKey) return null\n\n async function transitionTo(transitionName: string) {\n if (transitioning) return\n setMenuOpen(false)\n setTransitioning(true)\n setTransitionError(null)\n try {\n const query = new URLSearchParams({ transition: transitionName })\n const response = await fetch(`/api/tomato-board/transition/${encodeURIComponent(itemKey)}?${query}`, {\n method: 'POST',\n headers: { accept: 'application/json' },\n })\n const body = await response.json() as {\n currentStatus?: string\n error?: string\n details?: { stderr?: string; stdout?: string }\n }\n if (!response.ok) {\n const selected = transitionState.transitions.find(transition => transition.transition === transitionName)\n const failure = [body.error, body.details?.stderr, body.details?.stdout].filter(Boolean).join('\\n')\n const requiredFieldsMissing = /字段.{0,24}必填|必填.{0,24}字段|流转前需填写/u.test(failure)\n if (selected?.targetStatus === '待测试' && requiredFieldsMissing) {\n const session = ctx.sessions.binding(sessionId)?.session\n if (!session) throw new Error('当前 Harness 对话未加载,无法交给 AI 继续处理')\n const prompt = [\n `番茄事项 ${itemKey} 流转到「待测试」失败,CLI 提示存在必填字段缺失。`,\n '请先读取番茄事项详情,并结合当前对话和仓库代码进行分析。',\n '基于证据补齐并回读确认以下字段:根因分析、RD引入原因分析、原因描述、修复版本、解决方案。',\n '不要编造业务事实;证据不足时先向我确认。',\n '只有这些字段已经持久化且回读一致后,才能重新执行「修复完成」流转到「待测试」,最后再次回读状态验证。',\n `CLI 失败信息:${failure || '未返回具体原因'}`,\n ].join('\\n')\n const prompted = await session.prompt([{ type: 'text', text: prompt }], 'queue')\n if (!prompted.ok) throw new Error(`无法把流转任务交给 AI:${prompted.error.message}`)\n setTransitionError('必填字段缺失,已交给当前对话中的 AI 分析并继续处理')\n return\n }\n throw new Error(failure || `请求失败 (${response.status})`)\n }\n const transitionsResponse = await fetch(`/api/tomato-board/transitions/${encodeURIComponent(itemKey)}`, {\n headers: { accept: 'application/json' },\n })\n const transitionsBody = await transitionsResponse.json() as TomatoTransitionState & { error?: string }\n if (!transitionsResponse.ok) {\n throw new Error(transitionsBody.error || `状态刷新失败 (${transitionsResponse.status})`)\n }\n setTransitionState({\n currentStatus: transitionsBody.currentStatus || body.currentStatus || '',\n tomatoUrl: transitionsBody.tomatoUrl || '',\n transitions: transitionsBody.transitions ?? [],\n })\n } catch (error) {\n setTransitionError(error instanceof Error ? error.message : '番茄事项流转失败')\n } finally {\n setTransitioning(false)\n }\n }\n\n const availableTransitions = transitionState.transitions.filter(transition => !transition.disabled)\n const delegatedToAgent = transitionError?.startsWith('必填字段缺失') === true\n const transitionTitle = transitionError\n ? delegatedToAgent ? transitionError : `番茄流转失败:${transitionError}`\n : loading\n ? '正在查询番茄事项状态'\n : availableTransitions.length === 0\n ? `当前状态「${transitionState.currentStatus || '未知'}」没有可用流转`\n : `当前状态:${transitionState.currentStatus || '未知'}`\n return (\n <>\n <Menu\n open={menuOpen}\n portal\n align=\"end\"\n items={transitionState.transitions.map(transition => ({\n id: transition.transition,\n label: `流转到 ${transition.targetStatus}`,\n disabled: transition.disabled,\n }))}\n onSelect={transitionName => void transitionTo(transitionName)}\n onClose={() => setMenuOpen(false)}\n anchor={(\n <Button\n variant=\"toolbar\"\n size=\"sm\"\n className={css.transitionTrigger}\n title={transitionTitle}\n aria-label={transitionTitle}\n aria-haspopup=\"menu\"\n aria-expanded={menuOpen}\n disabled={loading || transitioning || availableTransitions.length === 0}\n onClick={() => setMenuOpen(open => !open)}\n >\n {transitioning || delegatedToAgent || transitionError\n ? transitioning ? '正在流转…' : delegatedToAgent ? 'AI 已接手' : '流转失败'\n : (\n <>\n <span className={css.transitionCaption}>状态</span>\n <strong>{transitionState.currentStatus || '查询中…'}</strong>\n {availableTransitions.length > 0 ? (\n <>\n <span className={css.transitionDivider} aria-hidden=\"true\" />\n <span className={css.transitionAction}>流转</span>\n <IconChevronDownOutline14 className={css.transitionChevron} />\n </>\n ) : null}\n </>\n )}\n </Button>\n )}\n />\n <Button\n variant=\"toolbar\"\n size=\"sm\"\n title=\"在番茄中打开事项\"\n aria-label={`在番茄中打开 ${itemKey}`}\n disabled={!transitionState.tomatoUrl}\n onClick={() => window.open(transitionState.tomatoUrl, '_blank', 'noopener,noreferrer')}\n >\n 番茄 ↗\n </Button>\n </>\n )\n}\n\nexport const inject = ['slots', 'sessions', 'workspaces']\n\nexport function apply(ctx: Context): void {\n ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register(\n { name: 'conversation.session.header.actions', id: 'tomato-shortcut', order: 12 },\n props => <TomatoConversationShortcut {...props} ctx={ctx} />,\n ))\n ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register(\n { name: 'sidebar.footer.action', id: 'tomato-board' },\n props => <TomatoBoardAction {...props} openWorkbench={() => {\n if (disposeWorkbench) return\n emit({ open: true })\n disposeWorkbench = ctx.slots.register(\n { name: 'conversation', priority: -100 },\n () => <TomatoBoardPanel ctx={ctx} />,\n )\n }} />,\n ))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["css","Modal","Button","Menu","IconRefreshOutline16","IconCloseOutline16","IconChevronDownOutline14"],"sources":["../../../packages/core/session/src/types.ts","../../../packages/client/tomato-board/src/client/TomatoBoard.tsx"],"sourcesContent":["import type { Branded } from '@deepseek-ai/dsh-brand'\nimport type {\n AssistantMessage,\n ToolCallId,\n LlmCallConfig,\n LlmCallConfigAdapterDefaults,\n LlmFailure,\n StreamChunk,\n TokenUsage,\n ToolResultMessage,\n ToolSchema,\n UserMessage,\n} from '@deepseek-ai/dsh-llm'\nimport type { JsonValue } from './json.ts'\n\n// The lossless-JSON payload type belongs to this client-safe face too: a wire\n// contract carrying JSON data must not import the root entry, which merges\n// `ctx.sessions` (a Host-only SessionStore) into every consumer's program.\nexport type { JsonValue } from './json.ts'\n\n/** Identifies one session in the store (and its persistence artifacts). */\nexport type SessionId = Branded<'SessionId'>\n\n/**\n * Brand a string as a {@link SessionId}.\n * @param id - the raw session id string.\n * @returns the same string, branded (a compile-time cast — no runtime cost).\n */\nexport function SessionId(id: string): SessionId {\n return id as SessionId\n}\n\n/**\n * The on-disk session format version, stamped into every newly-written {@link SessionHeader}\n * and enforced by every persistence backend on load. The single source of truth for the\n * version — write sites and the load-time check all read it.\n * While the harness is unreleased it is pinned at `0`: no compatibility is\n * implied, incompatible logs are rejected, and no migration is provided.\n *\n * The version is a single monotonic integer with no major/minor split. Whether\n * a bump is needed is decided by what the WRITER emits, never by what a newer\n * reader can accept: bump exactly when an older runtime could no longer handle\n * a new log with full semantic correctness (\"parses without error\" is not\n * correctness — silently skipping content that shapes reconstruction is a\n * wrong read). Only structural changes reach that bar: the header shape, the\n * {@link SessionEvent} envelope, core event semantics, or the surface\n * mechanism (the {@link SurfaceEventType} set and {@link SurfaceOp} variants).\n * Adding an ordinary event type does not bump: the generated known-event guard\n * makes older runtimes refuse logs containing a type they do not understand.\n * When in doubt, bump: a near-identity upgrade step is almost free, a missed\n * bump makes older runtimes read new logs wrong silently. The full mechanism\n * (upgrade-step chain, in-memory view conversion, migrate-on-continue) is\n * recorded in the fail-closed-session-event-vocabulary Agent Note\n * (`.agents/notes/implemented/simplification/2026-08-25-fail-closed-session-event-vocabulary.md`).\n */\nexport const SESSION_FORMAT_VERSION = 0\n\n/**\n * Immutable validated storage metadata, kept outside the conversation event log.\n */\nexport interface SessionHeader {\n /**\n * On-disk format version, stamped from {@link SESSION_FORMAT_VERSION} when the\n * session is created. A persistence backend rejects any other version on load\n * (no migration — see the constant).\n */\n readonly version: number\n /** The session's id (mirrors the {@link Session}'s id). */\n readonly id: SessionId\n /** Non-negative safe-integer Unix epoch milliseconds when the session was created. */\n readonly createdAt: number\n /** Absolute working directory the session was created in (if any). */\n readonly cwd?: string\n /** The session this one was forked from (seed lineage), if any. */\n readonly parentSession?: SessionId\n /**\n * How many leading events were inherited through a seed. Persisting this\n * boundary lets resume and replay distinguish parent history from child work.\n */\n readonly seedLength?: number\n /**\n * Coarse product classification for a session created as a subagent child.\n * This is presentation metadata, not proof that the child is continuable.\n */\n readonly origin?: 'subagent'\n /**\n * Delegation depth: absent (zero) for a top-level session, parent depth + 1\n * for a subagent child. Persisted so a recursion budget survives restart and\n * resume — a runtime-only depth would reset a resumed child to top-level.\n */\n readonly delegationDepth?: number\n /**\n * Id of the agent preset this session's agent was composed from, when the\n * deployment composes per session. Durable because the preset decides the\n * session's tools and prompt: a resume that restored a different composition\n * would replay history the model can no longer act on.\n */\n readonly agentPreset?: string\n}\n\n/**\n * Options for creating a {@link Session} via the store. `seed` replays/forks\n * an existing event log; `meta` carries the caller-supplied storage fields the\n * store folds into a {@link SessionHeader}.\n */\nexport interface CreateSessionOptions {\n /** Initial replay or fork history supplied at construction. */\n readonly seed?: readonly SessionEvent[]\n /**\n * Storage metadata read once before publication. `seedLength` is explicit\n * because a resumed seed contains the full stored log, not only its inherited prefix.\n */\n readonly meta?: {\n readonly cwd?: string\n readonly parentSession?: SessionId\n readonly createdAt?: number\n readonly seedLength?: number\n readonly origin?: 'subagent'\n readonly delegationDepth?: number\n readonly agentPreset?: string\n }\n}\n\n/**\n * Fresh storage values transferred to {@link SessionStore.prepare} without a\n * second serialization copy. Callers retain no mutable aliases.\n */\nexport interface RestoredSessionOptions {\n /** Fresh detached storage events to validate and freeze in place. */\n readonly seed: SessionEvent[]\n /** Fresh detached storage metadata to validate and freeze in place. */\n readonly meta: SessionHeader\n /** Select the persistence ownership-transfer path. */\n readonly seedSource: 'persistence'\n}\n\n/** Inputs accepted while constructing an unpublished Session. */\nexport type PrepareSessionOptions =\n | (CreateSessionOptions & { readonly seedSource?: undefined })\n | RestoredSessionOptions\n\n/** Why an active agent driver was cancelled. */\nexport type AgentCancelCause =\n | { readonly kind: 'user' }\n | { readonly kind: 'parent' }\n | { readonly kind: 'hook'; readonly reason: string }\n | { readonly kind: 'disposed' }\n\n/** Durable cancellation cause, including imports whose original coarse record carried no cause. */\nexport type TurnEndCancelCause = AgentCancelCause | { readonly kind: 'legacy' }\n\n/**\n * Why a turn ended. Merge-extensible sum type.\n */\nexport interface TurnEndReasonMap {\n completed: { kind: 'completed' }\n /** A cancellation request interrupted the live turn. */\n aborted: { kind: 'aborted'; reason: TurnEndCancelCause }\n\n blocked: { kind: 'blocked' }\n /**\n * The turn failed. `error` is always a structured failure: the `LlmError`\n * facts verbatim, or `{ message: errorChain(error), code: 'UNKNOWN' }`\n * flattened from any other error.\n */\n error: { kind: 'error'; error: LlmFailure }\n /** At least one step reached its output-token ceiling, even if a plugin continued the turn. */\n 'max-tokens': { kind: 'max-tokens' }\n /**\n * A persistence backend closed a crash-orphaned turn on reload. The loop never\n * emits this marker, and the events recorded before the crash remain intact.\n */\n interrupted: { kind: 'interrupted' }\n}\n\n/** The union over {@link TurnEndReasonMap} — why a turn ended; plugins extend it by merging variants into the map. */\nexport type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap]\n\n/**\n * Logged request state outside derived history: call config, system prompt, and\n * tools. The latest full `request/header` snapshot reconstructs it; canonical\n * empty optional fields are absent.\n */\nexport interface EpochHeader {\n /** The conversation's call configuration (provider, model, reasoning effort, and sampling scalars). */\n config: LlmCallConfig\n /** Effective config fields materialized from the exact adapter rather than proposed by a caller. */\n adapterDefaults?: LlmCallConfigAdapterDefaults\n /** Rendered system prompt text; absent for a system-less request. */\n system?: string\n /** Assembled tool schemas; absent for a tool-less request. */\n tools?: ToolSchema[]\n}\n\n/** Registration-bound metadata for one resolved model route. */\nexport interface RequestContext {\n /** Registered provider route the metadata belongs to. */\n provider: string\n /** Provider-owned model id the metadata belongs to. */\n model: string\n /** Maximum combined request and response context in tokens, when advertised. */\n contextWindow?: number\n}\n\n/**\n * Why a `request/header` snapshot was appended: `'initial'` — the log's first\n * header (a new conversation); `'resume'` — a loop instance's first request\n * over a log that already has header events (process restart, fork seed);\n * `'change'` — a later request used a different header, with `startsSeries`\n * preserving a coincident series boundary; `'series'` — an unchanged header\n * began an explicitly distinct message series or followed a surface replacement.\n */\nexport type RequestHeaderReason = 'initial' | 'resume' | 'change' | 'series'\n\n/**\n * The merge-extensible, append-only source of truth for an agent interaction.\n * Message history is derived from this log. Every event is lossless JSON and\n * sequence numbers stay contiguous, including raw chunks, so persistence can\n * store the canonical log verbatim.\n */\nexport interface SessionEventMap {\n /**\n * Opens turn `turn` before the loop claims queued input or runs pre-step.\n * Rejection, empty input, cancellation, or failure may close it with no\n * step; otherwise the following identified `user/message` event or batch\n * records the messages entering the step.\n */\n 'turn/start': { turn: number }\n /**\n * Closes turn `turn` with the {@link TurnEndReason} that ended it. A turn\n * with no entered step has no `step/start` or `step/end`. The loop does not await a\n * flush at turn boundaries: `dsh-session-checkpoint-policy` owns the\n * per-request durability checkpoint, and consumers that read storage after\n * `whenIdle()` flush themselves. Success commits the turn; rejection is\n * reported live and does not prevent later work.\n */\n 'turn/end': { turn: number; reason: TurnEndReason }\n /** Opens step `step` of turn `turn` — one model call plus the tool executions it requested. */\n 'step/start': { turn: number; step: number }\n /** Closes step `step` of turn `turn`. */\n 'step/end': { turn: number; step: number }\n /**\n * A user-role message on the model-visible surface: a direct human prompt\n * (the queued message claimed for this turn), a synthetic `agent.inject()`\n * context (file-change notices, subdir AGENTS.md, skill content, cron\n * notifications, …), or an entered goal continuation round. All three\n * project their `content` verbatim; `source` tells them apart.\n */\n 'user/message': UserMessage\n /** Raw stream chunk — token-level replay fidelity. */\n 'assistant/chunk': { turn: number; step: number; chunk: StreamChunk }\n /**\n * Assembled assistant message for one step (derived history uses this).\n * Carries the step's `usage` when the adapter reported token accounting, so\n * the model output and its accounting travel together (there is no separate\n * usage record). `usage` is absent when the adapter reported none. A turn\n * cancelled mid-stream finalizes its delivered text/reasoning prefix as this\n * event with `interrupted: true`; undispatched tool calls are absent. The\n * marker distinguishes that prefix without re-deriving interruption from turn\n * boundaries. An aborted turn with no such event streamed no visible content.\n */\n 'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true }\n /**\n * The model requested one tool invocation: `name` with the raw `arguments`\n * JSON string exactly as the model produced it (unparsed). `callId` pairs the\n * call with its `tool/result`.\n */\n 'tool/call': { turn: number; step: number; callId: ToolCallId; name: string; arguments: string }\n /**\n * A completed tool call's model-facing result, optional internal failure\n * identity, and optional tool-private `meta` presentation payload. `meta` is\n * opaque to the core (the producing tool owns its shape and reads it back in\n * `presentResult`) but MUST be JSON-serializable: `Session.append`\n * runtime-validates all event data with `isJsonValue`, so a non-serializable\n * `meta` is rejected at the source, and the durable log reproduces the\n * identical card on replay. Absent\n * unless the tool attaches one (e.g. `dsh-tool-fs` carries its result-time\n * contextual diff here).\n */\n 'tool/result': {\n turn: number\n step: number\n message: ToolResultMessage\n error?: { name: string; code: string }\n meta?: JsonValue\n }\n /**\n * Full header for the next request, appended inside its step before dispatch.\n * It is log-only; the latest snapshot reconstructs the request header.\n */\n 'request/header': {\n header: EpochHeader\n reason: RequestHeaderReason\n /** A changed header also begins a distinct model-message series. */\n startsSeries?: true\n }\n /**\n * Route metadata for the next request, logged only when the route or capacity\n * changes. It does not participate in request reconstruction or header equality.\n */\n 'request/context': RequestContext\n /**\n * Marks the end of a constructor seed. Events before it have smaller seq\n * values and came from the seed (resume, fork, or replay); this lifecycle\n * produced none of them. This log-only event is the durable projection of\n * {@link Session.firstLiveSeq}. Its payload is empty — position and `time`\n * carry the meaning.\n *\n * Locate the LAST one in stored history. A seed already ending in one is not\n * re-marked, so reopening an untouched session does not grow its log per\n * pickup and the event need not be at the current `firstLiveSeq`.\n *\n * `Session`'s constructor is the only legitimate writer. The invariant\n * companion deliberately constrains nothing here, so a plugin appending one\n * would silently classify every live bracket before it as seed history.\n *\n * An owner of a standalone open/close bracket (`compaction/start` …\n * `compaction/end`) reads it because seed history and live work are otherwise\n * byte-identical: an unmatched opening marker before this event belongs to\n * an ended lifecycle, whatever ended it. NOT a liveness signal about other\n * writers — a concurrently live session holds its own boundary elsewhere,\n * so tolerating concurrent writers needs a signal beyond the log.\n */\n 'session/end-seed': Record<string, never>\n}\n\n/** The appendable event-type keys of {@link SessionEventMap}, plugin-merged extensions included. */\nexport type SessionEventType = keyof SessionEventMap\n\n/**\n * The subset of {@link SessionEventType} values whose events produce LLM\n * messages and are eligible to appear on the ordered surface. Only these\n * event types may carry {@link SurfaceOp} and {@link SessionEvent.sourceEventSeqs}.\n */\nexport type SurfaceEventType =\n | 'user/message'\n | 'assistant/message'\n | 'tool/result'\n\n/**\n * A {@link SessionEvent} that is **on** the ordered surface — its\n * `surfaceOp` is guaranteed present (mandatory), narrowed from a\n * surface-eligible {@link SessionEvent} by checking both `type` and\n * `surfaceOp` at runtime.\n *\n * Use the `isSurfaceEvent` type guard (in `surface.ts`) to narrow a\n * `SessionEvent` to this type.\n */\nexport type SurfaceEvent = SessionEvent<SurfaceEventType> & { surfaceOp: SurfaceOp }\n\n/**\n * How a session event entered the ordered surface. Only valid on\n * {@link SurfaceEventType} events.\n *\n * - `'append'`: added to the tail — normal path for user/assistant/tool\n * messages.\n * - `{ op: 'replace', start, end }`: replaces surface nodes from `start`\n * (inclusive) through `end` (inclusive) with this node. Both must exist as\n * surface nodes in the current surface. `start === end` replaces a single\n * node. The node's {@link SessionEvent.sourceEventSeqs} must include every\n * shadowed surface node. Used by compaction; any surface-replacing producer\n * may use it.\n */\nexport type SurfaceOp =\n | 'append'\n | { op: 'replace'; start: number; end: number }\n\n/**\n * Surface placement and cited source-event seqs for {@link Session.append}. Required on\n * message-producing events and forbidden on log-only events.\n */\nexport interface SurfaceIntent {\n surfaceOp: SurfaceOp\n /**\n * Complete set of known source-event seqs. `assistant/message` may use a\n * present empty array for a known empty provider stream; when the field is\n * absent, the event does not record which earlier events produced the message.\n * Other surface events require a non-empty set when this field is present.\n */\n sourceEventSeqs?: number[]\n}\n\n/**\n * One immutable entry in the session log.\n *\n * A proper discriminated union over `type` (not independent `type`/`data`\n * unions), so `switch (event.type)` narrows `event.data` without casts.\n *\n * The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:\n * they only exist on {@link SurfaceEventType} variants (`user/message`,\n * `assistant/message`, `tool/result`).\n * Non-surface events (boundary markers, chunks, usage, errors) never carry\n * surface metadata — the compiler enforces this at `Session.append()`\n * call sites.\n */\nexport type SessionEvent<T extends SessionEventType = SessionEventType> = {\n [K in SessionEventType]: {\n type: K\n /** Monotonic sequence number within the session. */\n seq: number\n /** Unix epoch milliseconds. */\n time: number\n data: SessionEventMap[K]\n } & (K extends SurfaceEventType ? {\n /**\n * Seq numbers of earlier events that this event cites as sources\n * (e.g. the `assistant/chunk` seqs that built an `assistant/message`,\n * or the surface nodes shadowed by a compaction replace node). An\n * `assistant/message` may carry a present empty array for a known empty\n * provider stream; when the field is absent, the event does not record which\n * earlier events produced the message.\n */\n sourceEventSeqs?: number[]\n /** How this event entered the surface; absent for non-surface events. */\n surfaceOp?: SurfaceOp\n } : object)\n}[T]\n","import { useEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-api-session-controller/client'\nimport type { WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client'\nimport type {} from '@deepseek-ai/dsh-api-workspace-controller/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-layout/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport {\n Button, IconChevronDownOutline14, IconCloseOutline16, IconRefreshOutline16, Menu, Modal,\n} from '@deepseek-ai/dsh-client-ui-primitives'\nimport type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport { SessionId } from '@deepseek-ai/dsh-session/types'\nimport css from './tomato-board.module.css'\n\ninterface TomatoItem {\n itemKey: string\n title: string\n status: string\n itemType: string\n workspace: string\n creator: string\n priority: string\n tomatoUrl: string\n}\n\ninterface TomatoTransition {\n transition: string\n targetStatus: string\n disabled: boolean\n disabledReason?: string\n}\n\ninterface TomatoTransitionState {\n currentStatus: string\n tomatoUrl: string\n transitions: TomatoTransition[]\n}\n\ninterface BoardState {\n open: boolean\n loading: boolean\n items: TomatoItem[]\n error: string | null\n selectedItem: TomatoItem | null\n truncated: boolean\n}\n\nlet state: BoardState = { open: false, loading: false, items: [], error: null, selectedItem: null, truncated: false }\nlet disposeWorkbench: (() => void) | null = null\nconst listeners = new Set<() => void>()\nconst emit = (patch: Partial<BoardState>) => {\n state = { ...state, ...patch }\n for (const listener of listeners) listener()\n}\nconst subscribe = (listener: () => void) => {\n listeners.add(listener)\n return () => listeners.delete(listener)\n}\nconst snapshot = () => state\n\n// 真实番茄事项 itemKey 形如 `Gitee-Test-2026-737`、`Proxima-1116`:字母前缀(可含连字符分段)+ 数字结尾。\n// 只匹配该格式可避免把任意 `[xxx]` 开头的普通对话标题(如 `[分析]`、`[每日复盘]`)误判为番茄事项,\n// 进而错误地在对话头上渲染出状态流转按钮并触发无意义的 CLI 调用。\nconst TOMATO_ITEM_KEY_PATTERN = /^\\[([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*-\\d+)\\]/u\n\nconst TOMATO_TYPE_OPTIONS = ['测试缺陷', '缺陷', 'Bug', 'EnablerStory', 'Story', 'Task']\nconst TOMATO_STATUS_ORDER = [\n '新建', 'Bugfix', '修复中', '开发中', '待测试', '测试中', '测试通过', '已完成',\n '已取消', '延期解决', '测试完成', '待开发', '不修复', '已挂起',\n]\nconst TOMATO_FILTER_BLACKLIST_KEY = 'taskboard.tomatoFilterBlacklist.v1'\nconst TOMATO_SESSION_LINKS_KEY = 'taskboard.tomatoSessionLinks.v1'\nconst TOMATO_MUTED_ITEMS_KEY = 'taskboard.tomatoMutedItems.v1'\n\nfunction readSessionLinks(): Record<string, string> {\n try {\n const value = JSON.parse(window.localStorage.getItem(TOMATO_SESSION_LINKS_KEY) ?? '{}')\n return value && typeof value === 'object' ? value as Record<string, string> : {}\n } catch {\n return {}\n }\n}\n\nfunction linkedSessionId(itemKey: string): SessionId | '' {\n const value = readSessionLinks()[itemKey]\n return typeof value === 'string' && value ? SessionId(value) : ''\n}\n\nfunction saveSessionLink(itemKey: string, sessionId: SessionId) {\n const links = readSessionLinks()\n links[itemKey] = sessionId\n window.localStorage.setItem(TOMATO_SESSION_LINKS_KEY, JSON.stringify(links))\n}\n\nfunction readFilterBlacklist(): { types: Set<string>; statuses: Set<string> } {\n try {\n const value = JSON.parse(window.localStorage.getItem(TOMATO_FILTER_BLACKLIST_KEY) ?? '{}')\n return {\n types: new Set(Array.isArray(value?.types) ? value.types.filter((item: unknown): item is string => typeof item === 'string') : []),\n statuses: new Set(Array.isArray(value?.statuses) ? value.statuses.filter((item: unknown): item is string => typeof item === 'string') : []),\n }\n } catch {\n return { types: new Set(), statuses: new Set() }\n }\n}\n\nfunction readMutedItems(): Set<string> {\n try {\n const value = JSON.parse(window.localStorage.getItem(TOMATO_MUTED_ITEMS_KEY) ?? '[]')\n return new Set(Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [])\n } catch {\n return new Set()\n }\n}\n\nasync function refresh() {\n emit({ loading: true, error: null })\n try {\n const response = await fetch('/api/tomato-board/items', { headers: { accept: 'application/json' } })\n const body = await response.json() as { items?: TomatoItem[]; truncated?: boolean; error?: string }\n if (!response.ok) throw new Error(body.error || `请求失败 (${response.status})`)\n emit({ items: body.items ?? [], truncated: body.truncated === true })\n } catch (error) {\n emit({ error: error instanceof Error ? error.message : '番茄事项读取失败' })\n } finally {\n emit({ loading: false })\n }\n}\n\nfunction closeWorkbench() {\n const dispose = disposeWorkbench\n disposeWorkbench = null\n emit({ open: false, selectedItem: null })\n dispose?.()\n}\n\nfunction TomatoBoardAction({ wide, openWorkbench }: { wide: boolean; openWorkbench: () => void }) {\n return (\n <button className={css.sidebarAction} type=\"button\" title=\"番茄工作台\" onClick={openWorkbench}>\n <span className={css.tomatoIcon} aria-hidden=\"true\">T</span>\n {wide && <span>番茄工作台</span>}\n </button>\n )\n}\n\nfunction CreateConversationDialog({ ctx, item }: { ctx: Context; item: TomatoItem }) {\n const [workspaceId, setWorkspaceId] = useState('')\n const [workspaceMenuOpen, setWorkspaceMenuOpen] = useState(false)\n const [creating, setCreating] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const workspaces = useSyncExternalStore(\n listener => ctx.workspaces.list.subscribe(listener),\n () => ctx.workspaces.list.getSnapshot(),\n () => ctx.workspaces.list.getSnapshot(),\n )\n const selectedWorkspace = workspaces.items.find(workspace => workspace.workspaceId === workspaceId)\n\n async function createConversation() {\n if (!selectedWorkspace || creating) return\n setCreating(true)\n setError(null)\n try {\n const createdSessionId = await ctx.sessions.create({ workspaceId: selectedWorkspace.workspaceId })\n const session = ctx.sessions.binding(createdSessionId)?.session\n if (!session) throw new Error('新建对话未能在 Harness 中加载')\n const title = `[${item.itemKey}] ${item.title}`\n const renamed = await session.rename(title)\n if (!renamed.ok) throw new Error(`对话标题设置失败:${renamed.error.message}`)\n const prompt = [\n `请处理番茄事项 ${item.itemKey}。`,\n `标题:${item.title}`,\n `类型:${item.itemType || '未设置'}`,\n `状态:${item.status || '未设置'}`,\n `优先级:${item.priority || '未设置'}`,\n ].join('\\n')\n const prompted = await session.prompt([{ type: 'text', text: prompt }], 'queue')\n if (!prompted.ok) throw new Error(`事项上下文写入失败:${prompted.error.message}`)\n saveSessionLink(item.itemKey, createdSessionId)\n ctx.sessions.open(createdSessionId)\n closeWorkbench()\n } catch (reason) {\n setError(reason instanceof Error ? reason.message : 'Harness 对话创建失败')\n } finally {\n setCreating(false)\n }\n }\n\n return (\n <Modal\n open\n onClose={() => emit({ selectedItem: null })}\n title=\"选择处理该事项的仓库\"\n closeLabel=\"关闭\"\n description={item.title}\n className={css.createDialog!}\n footer={(\n <>\n <Button variant=\"ghost\" onClick={() => emit({ selectedItem: null })}>取消</Button>\n <Button variant=\"primary\" disabled={!selectedWorkspace || creating} onClick={() => void createConversation()}>\n {creating ? '正在创建…' : '创建并进入对话'}\n </Button>\n </>\n )}\n >\n <div className={css.dialogBody}>\n <span>{item.itemKey}</span>\n {error ? <div className={css.error} role=\"alert\">{error}</div> : null}\n <Menu\n open={workspaceMenuOpen}\n portal\n items={workspaces.items.map((workspace: WorkspaceView) => ({\n id: workspace.workspaceId,\n label: workspace.title,\n }))}\n selectedId={workspaceId || undefined}\n onSelect={id => {\n setWorkspaceId(id)\n setWorkspaceMenuOpen(false)\n }}\n onClose={() => setWorkspaceMenuOpen(false)}\n anchor={(\n <Button\n className={css.workspaceTrigger}\n variant=\"outline\"\n aria-haspopup=\"menu\"\n aria-expanded={workspaceMenuOpen}\n onClick={() => setWorkspaceMenuOpen(open => !open)}\n >\n <span>{selectedWorkspace?.title ?? '选择 Harness 项目…'}</span>\n <span aria-hidden=\"true\">⌄</span>\n </Button>\n )}\n />\n {selectedWorkspace ? <code>{selectedWorkspace.path}</code> : null}\n </div>\n </Modal>\n )\n}\n\nfunction TomatoBoardPanel({ ctx }: { ctx: Context }) {\n const board = useSyncExternalStore(subscribe, snapshot, snapshot)\n const workbenchRef = useRef<HTMLElement>(null)\n const [search, setSearch] = useState('')\n const [blacklist, setBlacklist] = useState(readFilterBlacklist)\n const [mutedItems, setMutedItems] = useState(readMutedItems)\n const sessions = useSyncExternalStore(\n listener => ctx.sessions.list.subscribe(listener),\n () => ctx.sessions.list.getSnapshot(),\n () => ctx.sessions.list.getSnapshot(),\n )\n\n useEffect(() => {\n if (board.open && board.items.length === 0 && !board.loading && !board.error) void refresh()\n }, [board.error, board.items.length, board.loading, board.open])\n useEffect(() => {\n if (!board.open) return\n const closeOnOutsideNavigation = (event: PointerEvent) => {\n if (!(event.target instanceof Element)) return\n if (workbenchRef.current?.contains(event.target)) return\n if (event.target.closest('[role=\"dialog\"], [role=\"menu\"]')) return\n closeWorkbench()\n }\n document.addEventListener('pointerdown', closeOnOutsideNavigation, true)\n return () => document.removeEventListener('pointerdown', closeOnOutsideNavigation, true)\n }, [board.open])\n if (!board.open) return null\n\n function openItem(item: TomatoItem) {\n const stored = linkedSessionId(item.itemKey)\n const titlePrefix = `[${item.itemKey}]`\n const discovered = sessions.ids.find(id => {\n const summary = sessions.byId[id]\n return summary?.title?.startsWith(titlePrefix) || summary?.displayTitle.startsWith(titlePrefix)\n })\n const associated = stored && sessions.byId[stored] ? stored : discovered\n if (associated) {\n saveSessionLink(item.itemKey, associated)\n ctx.sessions.open(associated)\n closeWorkbench()\n return\n }\n emit({ selectedItem: item })\n }\n\n const normalizedSearch = search.trim().toLowerCase()\n const filteredItems = board.items.filter(item => (\n !blacklist.types.has(item.itemType)\n && !blacklist.statuses.has(item.status)\n && (!normalizedSearch || [\n item.itemKey, item.title, item.itemType, item.status, item.workspace, item.creator, item.priority,\n ].join(' ').toLowerCase().includes(normalizedSearch))\n ))\n const typeOptions = [...new Set([...TOMATO_TYPE_OPTIONS, ...board.items.map(item => item.itemType).filter(Boolean)])]\n const statusOptions = [...new Set([\n ...TOMATO_STATUS_ORDER,\n ...board.items.map(item => item.status).filter(status => !TOMATO_STATUS_ORDER.includes(status)),\n ])]\n const statuses = [...new Set([\n ...TOMATO_STATUS_ORDER.filter(status => filteredItems.some(item => item.status === status)),\n ...filteredItems.map(item => item.status).filter(status => !TOMATO_STATUS_ORDER.includes(status)),\n ])]\n\n const toggleBlacklist = (kind: 'types' | 'statuses', value: string) => setBlacklist(current => {\n const nextValues = new Set(current[kind])\n if (nextValues.has(value)) nextValues.delete(value)\n else nextValues.add(value)\n const next = { ...current, [kind]: nextValues }\n window.localStorage.setItem(TOMATO_FILTER_BLACKLIST_KEY, JSON.stringify({\n types: [...next.types],\n statuses: [...next.statuses],\n }))\n return next\n })\n\n const toggleMutedItem = (itemKey: string) => setMutedItems(current => {\n const next = new Set(current)\n if (next.has(itemKey)) next.delete(itemKey)\n else next.add(itemKey)\n window.localStorage.setItem(TOMATO_MUTED_ITEMS_KEY, JSON.stringify([...next]))\n return next\n })\n\n return (\n <section ref={workbenchRef} className={css.workbench} aria-label=\"番茄工作台\">\n <header className={css.header}>\n <div>\n <h1>番茄工作台</h1>\n <p>{board.loading ? '正在读取番茄事项…' : `显示 ${filteredItems.length} / ${board.items.length} 个事项`}</p>\n </div>\n <div className={css.actions}>\n <label className={css.searchField}>\n <span aria-hidden=\"true\">⌕</span>\n <input\n type=\"search\"\n value={search}\n onChange={event => setSearch(event.target.value)}\n placeholder=\"搜索标题或 tag…\"\n aria-label=\"搜索标题或 tag\"\n />\n {search && <button type=\"button\" aria-label=\"清空搜索词\" onClick={() => setSearch('')}>×</button>}\n </label>\n <details className={css.filterMenu}>\n <summary aria-label=\"类型和状态筛选\" title=\"类型和状态筛选\">\n <span aria-hidden=\"true\">▽</span>\n {(blacklist.types.size > 0 || blacklist.statuses.size > 0) && <i />}\n </summary>\n <div className={css.filterPopover}>\n <FilterRow label=\"类型\" options={typeOptions} hidden={blacklist.types} onToggle={value => toggleBlacklist('types', value)} />\n <FilterRow label=\"状态\" options={statusOptions} hidden={blacklist.statuses} onToggle={value => toggleBlacklist('statuses', value)} />\n </div>\n </details>\n <Button\n variant=\"toolbar\"\n size=\"sm\"\n className={css.headerIconButton}\n icon={<IconRefreshOutline16 />}\n title=\"刷新番茄事项\"\n aria-label=\"刷新番茄事项\"\n disabled={board.loading}\n onClick={() => void refresh()}\n />\n <Button\n variant=\"toolbar\"\n size=\"sm\"\n className={css.headerIconButton}\n icon={<IconCloseOutline16 />}\n title=\"关闭番茄工作台\"\n aria-label=\"关闭番茄工作台\"\n onClick={closeWorkbench}\n />\n </div>\n </header>\n {board.error && <div className={css.error} role=\"alert\">{board.error}</div>}\n {board.truncated && <p className={css.notice} role=\"status\">事项数量已达配置上限,当前仅展示前 {board.items.length} 条。</p>}\n <div className={css.board}>\n {statuses.map(status => {\n const items = filteredItems.filter(item => item.status === status)\n return (\n <section className={css.lane} key={status} aria-labelledby={`tomato-lane-${status}`}>\n <header className={css.laneHeader}>\n <h2 id={`tomato-lane-${status}`}>{status}</h2>\n <span>{items.length}</span>\n </header>\n <div className={css.cards}>\n {items.map(item => (\n <article\n className={`${css.card} ${mutedItems.has(item.itemKey) ? css.cardMuted : ''}`}\n key={item.itemKey}\n tabIndex={0}\n role=\"button\"\n onClick={() => openItem(item)}\n onKeyDown={event => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n openItem(item)\n }\n }}\n >\n <div className={css.cardTopline}>\n <span className={css.key}>{item.itemKey}</span>\n <div className={css.cardButtons}>\n <Button\n className={css.muteButton}\n variant=\"ghost\"\n size=\"sm\"\n title={mutedItems.has(item.itemKey) ? '取消置灰' : '置灰标记'}\n aria-label={mutedItems.has(item.itemKey) ? `取消置灰 ${item.itemKey}` : `置灰 ${item.itemKey}`}\n aria-pressed={mutedItems.has(item.itemKey)}\n onClick={event => {\n event.stopPropagation()\n toggleMutedItem(item.itemKey)\n }}\n onKeyDown={event => event.stopPropagation()}\n >\n ●\n </Button>\n <Button\n className={css.tomatoLink}\n variant=\"ghost\"\n size=\"sm\"\n title=\"在番茄中打开事项\"\n aria-label={`在番茄中打开 ${item.itemKey}`}\n onClick={event => {\n event.stopPropagation()\n window.open(item.tomatoUrl, '_blank', 'noopener,noreferrer')\n }}\n onKeyDown={event => event.stopPropagation()}\n >\n ↗\n </Button>\n </div>\n </div>\n <strong>{item.title}</strong>\n <span className={css.meta}>{[item.itemType, item.priority, item.creator].filter(Boolean).join(' · ')}</span>\n </article>\n ))}\n </div>\n </section>\n )\n })}\n {!board.loading && !board.error && statuses.length === 0 && (\n <div className={css.empty}>当前没有可显示的番茄事项</div>\n )}\n </div>\n {board.selectedItem ? <CreateConversationDialog ctx={ctx} item={board.selectedItem} /> : null}\n </section>\n )\n}\n\nfunction FilterRow({ label, options, hidden, onToggle }: {\n label: string\n options: string[]\n hidden: ReadonlySet<string>\n onToggle: (value: string) => void\n}) {\n return (\n <div className={css.filterRow}>\n <span>{label}</span>\n <div>\n {options.map(option => (\n <button\n key={option}\n className={hidden.has(option) ? '' : css.selectedFilter}\n type=\"button\"\n aria-pressed={!hidden.has(option)}\n onClick={() => onToggle(option)}\n >\n {option}\n </button>\n ))}\n </div>\n </div>\n )\n}\n\nfunction TomatoConversationShortcut({ ctx, sessionId, useSessions }: PropsRuntime<'conversation.session.header.actions'> & { ctx: Context }) {\n const itemKey = useSessions(state => {\n const summary = state.byId[sessionId]\n const title = summary?.title ?? summary?.displayTitle ?? ''\n const match = TOMATO_ITEM_KEY_PATTERN.exec(title)?.[1]?.trim()\n if (!match) return ''\n // 仅当该对话确实由番茄工作台创建并记录在本机映射中时,才认定它是番茄事项对话。\n // 这样即便普通新建对话的标题恰好命中 itemKey 格式,也不会被误带上状态流转 UI。\n return linkedSessionId(match) === sessionId ? match : ''\n })\n const [menuOpen, setMenuOpen] = useState(false)\n const [loading, setLoading] = useState(false)\n const [transitioning, setTransitioning] = useState(false)\n const [transitionState, setTransitionState] = useState<TomatoTransitionState>({\n currentStatus: '',\n tomatoUrl: '',\n transitions: [],\n })\n const [transitionError, setTransitionError] = useState<string | null>(null)\n\n useEffect(() => {\n if (!itemKey) return\n const controller = new AbortController()\n setLoading(true)\n setTransitionError(null)\n setTransitionState({ currentStatus: '', tomatoUrl: '', transitions: [] })\n void fetch(`/api/tomato-board/transitions/${encodeURIComponent(itemKey)}`, {\n headers: { accept: 'application/json' },\n signal: controller.signal,\n }).then(async response => {\n const body = await response.json() as TomatoTransitionState & { error?: string }\n if (!response.ok) throw new Error(body.error || `请求失败 (${response.status})`)\n setTransitionState({\n currentStatus: body.currentStatus || '',\n tomatoUrl: body.tomatoUrl || '',\n transitions: body.transitions ?? [],\n })\n }).catch(error => {\n if (error instanceof Error && error.name === 'AbortError') return\n setTransitionError(error instanceof Error ? error.message : '番茄流转状态读取失败')\n }).finally(() => {\n if (!controller.signal.aborted) setLoading(false)\n })\n return () => controller.abort()\n }, [itemKey])\n\n if (!itemKey) return null\n\n async function transitionTo(transitionName: string) {\n if (transitioning) return\n setMenuOpen(false)\n setTransitioning(true)\n setTransitionError(null)\n try {\n const query = new URLSearchParams({ transition: transitionName })\n const response = await fetch(`/api/tomato-board/transition/${encodeURIComponent(itemKey)}?${query}`, {\n method: 'POST',\n headers: { accept: 'application/json' },\n })\n const body = await response.json() as {\n currentStatus?: string\n error?: string\n details?: { stderr?: string; stdout?: string }\n }\n if (!response.ok) {\n const selected = transitionState.transitions.find(transition => transition.transition === transitionName)\n const failure = [body.error, body.details?.stderr, body.details?.stdout].filter(Boolean).join('\\n')\n const requiredFieldsMissing = /字段.{0,24}必填|必填.{0,24}字段|流转前需填写/u.test(failure)\n if (selected?.targetStatus === '待测试' && requiredFieldsMissing) {\n const session = ctx.sessions.binding(sessionId)?.session\n if (!session) throw new Error('当前 Harness 对话未加载,无法交给 AI 继续处理')\n const prompt = [\n `番茄事项 ${itemKey} 流转到「待测试」失败,CLI 提示存在必填字段缺失。`,\n '请先读取番茄事项详情,并结合当前对话和仓库代码进行分析。',\n '基于证据补齐并回读确认以下字段:根因分析、RD引入原因分析、原因描述、修复版本、解决方案。',\n '不要编造业务事实;证据不足时先向我确认。',\n '只有这些字段已经持久化且回读一致后,才能重新执行「修复完成」流转到「待测试」,最后再次回读状态验证。',\n `CLI 失败信息:${failure || '未返回具体原因'}`,\n ].join('\\n')\n const prompted = await session.prompt([{ type: 'text', text: prompt }], 'queue')\n if (!prompted.ok) throw new Error(`无法把流转任务交给 AI:${prompted.error.message}`)\n setTransitionError('必填字段缺失,已交给当前对话中的 AI 分析并继续处理')\n return\n }\n throw new Error(failure || `请求失败 (${response.status})`)\n }\n const transitionsResponse = await fetch(`/api/tomato-board/transitions/${encodeURIComponent(itemKey)}`, {\n headers: { accept: 'application/json' },\n })\n const transitionsBody = await transitionsResponse.json() as TomatoTransitionState & { error?: string }\n if (!transitionsResponse.ok) {\n throw new Error(transitionsBody.error || `状态刷新失败 (${transitionsResponse.status})`)\n }\n setTransitionState({\n currentStatus: transitionsBody.currentStatus || body.currentStatus || '',\n tomatoUrl: transitionsBody.tomatoUrl || '',\n transitions: transitionsBody.transitions ?? [],\n })\n } catch (error) {\n setTransitionError(error instanceof Error ? error.message : '番茄事项流转失败')\n } finally {\n setTransitioning(false)\n }\n }\n\n const availableTransitions = transitionState.transitions.filter(transition => !transition.disabled)\n const delegatedToAgent = transitionError?.startsWith('必填字段缺失') === true\n const transitionTitle = transitionError\n ? delegatedToAgent ? transitionError : `番茄流转失败:${transitionError}`\n : loading\n ? '正在查询番茄事项状态'\n : availableTransitions.length === 0\n ? `当前状态「${transitionState.currentStatus || '未知'}」没有可用流转`\n : `当前状态:${transitionState.currentStatus || '未知'}`\n return (\n <>\n <Menu\n open={menuOpen}\n portal\n align=\"end\"\n items={transitionState.transitions.map(transition => ({\n id: transition.transition,\n label: `流转到 ${transition.targetStatus}`,\n disabled: transition.disabled,\n }))}\n onSelect={transitionName => void transitionTo(transitionName)}\n onClose={() => setMenuOpen(false)}\n anchor={(\n <Button\n variant=\"toolbar\"\n size=\"sm\"\n className={css.transitionTrigger}\n title={transitionTitle}\n aria-label={transitionTitle}\n aria-haspopup=\"menu\"\n aria-expanded={menuOpen}\n disabled={loading || transitioning || availableTransitions.length === 0}\n onClick={() => setMenuOpen(open => !open)}\n >\n {transitioning || delegatedToAgent || transitionError\n ? transitioning ? '正在流转…' : delegatedToAgent ? 'AI 已接手' : '流转失败'\n : (\n <>\n <span className={css.transitionCaption}>状态</span>\n <strong>{transitionState.currentStatus || '查询中…'}</strong>\n {availableTransitions.length > 0 ? (\n <>\n <span className={css.transitionDivider} aria-hidden=\"true\" />\n <span className={css.transitionAction}>流转</span>\n <IconChevronDownOutline14 className={css.transitionChevron} />\n </>\n ) : null}\n </>\n )}\n </Button>\n )}\n />\n <Button\n variant=\"toolbar\"\n size=\"sm\"\n title=\"在番茄中打开事项\"\n aria-label={`在番茄中打开 ${itemKey}`}\n disabled={!transitionState.tomatoUrl}\n onClick={() => window.open(transitionState.tomatoUrl, '_blank', 'noopener,noreferrer')}\n >\n 番茄 ↗\n </Button>\n </>\n )\n}\n\nexport const inject = ['slots', 'sessions', 'workspaces']\n\nexport function apply(ctx: Context): void {\n ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register(\n { name: 'conversation.session.header.actions', id: 'tomato-shortcut', order: 12 },\n props => <TomatoConversationShortcut {...props} ctx={ctx} />,\n ))\n ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register(\n { name: 'sidebar.footer.action', id: 'tomato-board' },\n props => <TomatoBoardAction {...props} openWorkbench={() => {\n if (disposeWorkbench) return\n emit({ open: true })\n disposeWorkbench = ctx.slots.register(\n { name: 'conversation', priority: -100 },\n () => <TomatoBoardPanel ctx={ctx} />,\n )\n }} />,\n ))\n}\n"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -14,6 +14,8 @@ const PRIORITY_NAMES = new Map([
14
14
  ["1a3e1092-7d70-42ee-ad38-0e8d953c4c23", "P3"],
15
15
  ["faae52da-28c8-46fc-96dd-db9cdb28b557", "P4"]
16
16
  ]);
17
+ let itemsCache = null;
18
+ let itemsInFlight = null;
17
19
  function firstText(value) {
18
20
  if (typeof value === "string") return value.trim();
19
21
  if (Array.isArray(value)) return value.map(firstText).find(Boolean) ?? "";
@@ -171,7 +173,9 @@ async function loadItems(config) {
171
173
  const iql = config.iql || "负责人 = currentUser() and 所属空间 in ['Gitee-Team', 'Gitee-Test'] and 类型 in ['Story', 'EnablerStory', 'Bug', '测试缺陷']";
172
174
  const rawItems = [];
173
175
  let page = 1;
174
- while (rawItems.length < 5e3) {
176
+ let truncated = false;
177
+ const maxItems = Math.max(1, Math.min(config.maxItems ?? 5e3, 2e4));
178
+ while (rawItems.length < maxItems) {
175
179
  const result = await execFileAsync(executable, [
176
180
  "team",
177
181
  "item",
@@ -197,19 +201,70 @@ async function loadItems(config) {
197
201
  rawItems.push(...pageItems);
198
202
  const record = payload && typeof payload === "object" ? payload : {};
199
203
  const total = Number(record.count ?? record.total ?? record.data?.count);
200
- if (!(record.hasNext === true || record.hasMore === true || Number.isFinite(total) && rawItems.length < total || !Number.isFinite(total) && pageItems.length === 50) || pageItems.length === 0) break;
204
+ const hasMore = record.hasNext === true || record.hasMore === true || Number.isFinite(total) && rawItems.length < total || !Number.isFinite(total) && pageItems.length === 50;
205
+ if (rawItems.length >= maxItems && hasMore) {
206
+ truncated = true;
207
+ break;
208
+ }
209
+ if (!hasMore || pageItems.length === 0) break;
201
210
  page += 1;
202
211
  }
203
- const excludedStatuses = new Set([
212
+ const excludedStatuses = new Set(config.excludedStatuses ?? [
204
213
  "测试通过",
205
214
  "测试完成",
206
215
  "不修复",
207
216
  "已取消"
208
217
  ]);
209
- return rawItems.map(normalizeItem).filter((item) => item.itemKey && item.title && item.status && !excludedStatuses.has(item.status)).map((item) => ({
210
- ...item,
211
- tomatoUrl: tomatoItemUrl(config, item.itemKey)
212
- }));
218
+ return {
219
+ items: rawItems.slice(0, maxItems).map(normalizeItem).filter((item) => item.itemKey && item.title && item.status && !excludedStatuses.has(item.status)).map((item) => ({
220
+ ...item,
221
+ tomatoUrl: tomatoItemUrl(config, item.itemKey)
222
+ })),
223
+ truncated
224
+ };
225
+ }
226
+ function loadItemsCached(config) {
227
+ const ttl = Math.max(0, Math.min(config.cacheTtlMs ?? 15e3, 3e5));
228
+ const key = JSON.stringify([
229
+ cliSettings(config),
230
+ config.iql,
231
+ config.maxItems,
232
+ config.excludedStatuses,
233
+ config.tomatoOrigin,
234
+ config.tomatoTenant
235
+ ]);
236
+ if (itemsCache?.key === key && itemsCache.expiresAt > Date.now()) return Promise.resolve(itemsCache.value);
237
+ if (itemsInFlight?.key === key) return itemsInFlight.promise;
238
+ const promise = loadItems(config).then((value) => {
239
+ itemsCache = {
240
+ key,
241
+ expiresAt: Date.now() + ttl,
242
+ value
243
+ };
244
+ return value;
245
+ }).finally(() => {
246
+ if (itemsInFlight?.promise === promise) itemsInFlight = null;
247
+ });
248
+ itemsInFlight = {
249
+ key,
250
+ promise
251
+ };
252
+ return promise;
253
+ }
254
+ function isCrossSite(request) {
255
+ const header = (name) => {
256
+ const value = request.headers?.[name] ?? request.headers?.[name.toLowerCase()];
257
+ return Array.isArray(value) ? value[0] : value;
258
+ };
259
+ if (header("sec-fetch-site") === "cross-site") return true;
260
+ const origin = header("origin");
261
+ const host = header("host");
262
+ if (!origin || !host) return false;
263
+ try {
264
+ return new URL(origin).host !== host;
265
+ } catch {
266
+ return true;
267
+ }
213
268
  }
214
269
  function sendJson(response, status, body) {
215
270
  response.statusCode = status;
@@ -241,7 +296,7 @@ function apply(ctx, config = {}) {
241
296
  return;
242
297
  }
243
298
  try {
244
- sendJson(response, 200, { items: await loadItems(config) });
299
+ sendJson(response, 200, await loadItemsCached(config));
245
300
  } catch (error) {
246
301
  sendJson(response, 502, { error: error instanceof Error ? error.message : "番茄事项读取失败" });
247
302
  }
@@ -300,6 +355,10 @@ function apply(ctx, config = {}) {
300
355
  sendJson(response, 405, { error: "Method not allowed" });
301
356
  return;
302
357
  }
358
+ if (isCrossSite(request)) {
359
+ sendJson(response, 403, { error: "拒绝跨站状态流转请求" });
360
+ return;
361
+ }
303
362
  const url = new URL(request.url ?? TRANSITION_ROUTE, "http://localhost");
304
363
  const itemKey = itemKeyFromRoute(request.url, TRANSITION_ROUTE);
305
364
  const transition = url.searchParams.get("transition")?.trim() ?? "";
@@ -5,6 +5,9 @@ interface Config {
5
5
  iql?: string;
6
6
  tomatoOrigin?: string;
7
7
  tomatoTenant?: string;
8
+ cacheTtlMs?: number;
9
+ maxItems?: number;
10
+ excludedStatuses?: string[];
8
11
  }
9
12
  export declare const name = "tomato-board";
10
13
  export declare const inject: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stephen1620/dsh-tomato-board",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Gitee Team/Tomato workboard and native conversation launcher for DeepSeek Harness",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -61,7 +61,7 @@
61
61
  },
62
62
  "scripts": {
63
63
  "bundle": "tsdown",
64
- "prepack": "npm run bundle"
64
+ "prepack": "node ../../scripts/verify-package.mjs ."
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@deepseek-ai/cordis": "*",