@tea-agent/loop-agent 0.40.0-next.4 → 0.40.0-next.6

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.
Files changed (134) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/shell-executor.js +4 -1
  4. package/dist/infrastructure/harness/atomic-write.js +23 -0
  5. package/dist/worker/console/chat/chat-event-store.js +62 -4
  6. package/dist/worker/console/chat/context-panel.js +7 -0
  7. package/dist/worker/console/chat/deferred-turn.js +12 -0
  8. package/dist/worker/console/chat/pi-runtime.js +264 -16
  9. package/dist/worker/console/chat/provider-error.js +98 -0
  10. package/dist/worker/console/chat/routes.js +292 -29
  11. package/dist/worker/console/chat/session-stats.js +179 -0
  12. package/dist/worker/console/chat/session-store.js +93 -63
  13. package/dist/worker/console/chat/todos.js +115 -0
  14. package/dist/worker/console/chat/tool-preview.js +42 -0
  15. package/dist/worker/console/chat/turn-process.js +20 -55
  16. package/dist/worker/console/chat/user-questions.js +266 -0
  17. package/dist/worker/console/chat/workspace-landing.js +14 -0
  18. package/dist/worker/console/native-directory-picker.js +13 -0
  19. package/dist/worker/console/routes.js +309 -1
  20. package/dist/worker/console/security.js +4 -4
  21. package/dist/worker/console/server.js +529 -186
  22. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-DGt5TRsk.js} +1 -1
  23. package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DKBSJsXm.js} +1 -1
  24. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-BnhooRBs.js} +1 -1
  25. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-Bm9qkrJP.js} +1 -1
  26. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-B2cFh4bO.js} +1 -1
  27. package/dist/worker/console/static/assets/channel-DSgFX07x.js +1 -0
  28. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DhvWRI03.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-chzTP_2w.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-SdWIR12m.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-C2-8Eo_A.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-DvbE1yQW.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-CEenyPTz.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-CcaZrq0k.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-FGngOTto.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-jT_yX3sf.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-CjE9LQOM.js} +1 -1
  38. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-unJObRls.js +1 -0
  39. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-unJObRls.js +1 -0
  40. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-GQWns1tb.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CGRpxWNJ.js} +1 -1
  42. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-Cd3uFGE4.js} +1 -1
  43. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-DmDHuUW0.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-kAr--hF3.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-woahL2Oh.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-7syNVaNf.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-BELPXiaY.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-CrPa4aXi.js} +1 -1
  49. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-B48u2VVe.js} +1 -1
  50. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-DhBTfwTF.js} +1 -1
  51. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-CAeZ5nDU.js} +1 -1
  52. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CkIFeFLN.js} +1 -1
  53. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-t0HGYDel.js} +1 -1
  54. package/dist/worker/console/static/assets/index-B4l5ag79.js +407 -0
  55. package/dist/worker/console/static/assets/index-CwVHWB71.css +1 -0
  56. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-WM0HZhe4.js} +1 -1
  57. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-CV5byHe0.js} +1 -1
  58. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-lIyWkgnw.js} +1 -1
  59. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-BIH4YNRY.js} +1 -1
  60. package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-lQ0ZGrWc.js} +1 -1
  61. package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-BYdXtke8.js} +5 -5
  62. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-k93vkIa4.js} +1 -1
  63. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-Bm6AqvSy.js} +1 -1
  64. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-DnJW6Xpf.js} +1 -1
  65. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-CMzm4ObL.js} +1 -1
  66. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-fwFF5dFQ.js} +1 -1
  67. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-tzl9VDUc.js} +1 -1
  68. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-B26V09DD.js} +1 -1
  69. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-D2npyCeP.js} +1 -1
  70. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-CTJtkRU2.js} +1 -1
  71. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-B8lk4Lzu.js} +1 -1
  72. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BmOcMTJE.js +1 -0
  73. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-C3yP53hH.js} +2 -2
  74. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Beah-dq0.js +8 -0
  75. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-Bck7xjhe.js} +1 -1
  76. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CCntr1w7.js} +1 -1
  77. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-DsCqCBZz.js} +1 -1
  78. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-Br2l90OM.js} +1 -1
  79. package/dist/worker/console/static/index.html +2 -2
  80. package/dist/worker/console/static-src/app/console-types.js +29 -4
  81. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  82. package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
  83. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  84. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  85. package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
  86. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  87. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  88. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  89. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  90. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  91. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  92. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  93. package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -17
  94. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  95. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  96. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  97. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  98. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  99. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  100. package/dist/worker/console/workspace-context.js +234 -0
  101. package/dist/worker/console/workspace-registry.js +214 -0
  102. package/dist/worker/observe/server.js +4 -0
  103. package/dist/worker/observe/static/api.js +26 -6
  104. package/dist/worker/observe/static/app.js +18 -70
  105. package/dist/worker/observe/static/index.html +1 -1
  106. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  107. package/dist/worker/observe/static/operator-chrome.css +145 -20
  108. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  109. package/dist/worker/observe/static/operator-chrome.js +330 -100
  110. package/dist/worker/observe/static/router.d.ts +46 -0
  111. package/dist/worker/observe/static/router.js +25 -1
  112. package/dist/worker/observe/static/state.d.ts +50 -0
  113. package/dist/worker/observe/static/state.js +172 -0
  114. package/dist/worker/observe/static/views/dag-inspector.js +3 -2
  115. package/dist/worker/observe/static/views/dag.js +2 -1
  116. package/dist/worker/observe/static/views/dashboard.js +6 -4
  117. package/dist/worker/observe/static/views/night.js +2 -2
  118. package/dist/worker/observe/static/views/pool.js +1 -0
  119. package/dist/worker/observe/static/views/run.js +3 -1
  120. package/dist/worker/observe/static/views/task.js +1 -1
  121. package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
  122. package/dist/workflows/dag/interrupt-request.js +3 -3
  123. package/dist/workflows/dag/lifecycle.js +3 -3
  124. package/docs/README.md +1 -0
  125. package/docs/operations/local-development-environment.md +21 -0
  126. package/harness.json +3 -3
  127. package/package.json +3 -1
  128. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
  129. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
  130. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
  131. package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
  132. package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
  133. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
  134. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
@@ -53,6 +53,9 @@ export function hydrateSessionThread(raw) {
53
53
  role: "assistant",
54
54
  text: item.text,
55
55
  createdAt,
56
+ ...(item.kind === "provider-error" && item.providerError
57
+ ? { kind: item.kind, providerError: item.providerError }
58
+ : {}),
56
59
  ...(content && content.length > 0 ? { content } : {}),
57
60
  });
58
61
  flushPendingToAssistant(item.id);
@@ -154,9 +157,23 @@ function appendAssistantUnmatchedTools(group, assistant, processItems, usedToolI
154
157
  function fillProcessDetails(group) {
155
158
  const usedToolIds = new Set();
156
159
  const processItems = [];
157
- const prior = group.assistants.slice(0, -1);
158
- const finalAssistant = group.assistants.at(-1);
159
- for (const assistant of prior) {
160
+ const lastAssistant = group.assistants.at(-1);
161
+ const finalAssistant = lastAssistant?.kind === "provider-error" ? undefined : lastAssistant;
162
+ const finalIndex = finalAssistant
163
+ ? group.assistants.lastIndexOf(finalAssistant)
164
+ : -1;
165
+ for (const [index, assistant] of group.assistants.entries()) {
166
+ if (index === finalIndex)
167
+ continue;
168
+ if (assistant.kind === "provider-error" && assistant.providerError) {
169
+ emitProcessBlocks(getDisplayableAssistantBlocks(assistant.content ?? []), group, processItems, usedToolIds);
170
+ appendAssistantUnmatchedTools(group, assistant, processItems, usedToolIds);
171
+ processItems.push({
172
+ kind: "provider-error",
173
+ error: assistant.providerError,
174
+ });
175
+ continue;
176
+ }
160
177
  emitProcessBlocks(getDisplayableAssistantBlocks(assistant.content ?? []), group, processItems, usedToolIds);
161
178
  appendAssistantUnmatchedTools(group, assistant, processItems, usedToolIds);
162
179
  }
@@ -292,55 +309,3 @@ export function selectRecentTurnGroups(groups, limit) {
292
309
  return [...groups];
293
310
  return groups.slice(-limit);
294
311
  }
295
- export function processGroupSummary(tools, options) {
296
- const count = tools.length;
297
- const thinkingCount = options?.thinkingCount ?? 0;
298
- const textCount = options?.textCount ?? 0;
299
- const activityCount = options?.activityCount ?? 0;
300
- const stepCount = thinkingCount + textCount + activityCount + count;
301
- const errorCount = tools.filter((t) => t.status === "error").length +
302
- (options?.activityErrorCount ?? 0);
303
- const runningCount = tools.filter((t) => t.status === "running").length +
304
- (options?.activityRunningCount ?? 0);
305
- const parts = [`处理详情 · ${stepCount} 步`];
306
- if (count > 0)
307
- parts.push(`${count} 工具`);
308
- if (activityCount > 0)
309
- parts.push(`${activityCount} 进展`);
310
- if (errorCount > 0)
311
- parts.push(`${errorCount} 出错`);
312
- else if (runningCount > 0)
313
- parts.push("运行中");
314
- return { count, errorCount, runningCount, stepCount, label: parts.join(" · ") };
315
- }
316
- /** Summary over rendered process items. */
317
- export function summarizeProcessItems(items) {
318
- const tools = [];
319
- let thinkingCount = 0;
320
- let textCount = 0;
321
- let activityCount = 0;
322
- let activityErrorCount = 0;
323
- let activityRunningCount = 0;
324
- for (const item of items) {
325
- if (item.kind === "thinking")
326
- thinkingCount += 1;
327
- else if (item.kind === "text")
328
- textCount += 1;
329
- else if (item.kind === "activity") {
330
- activityCount += 1;
331
- if (item.activity.kind === "error")
332
- activityErrorCount += 1;
333
- else if (item.activity.kind === "progress")
334
- activityRunningCount += 1;
335
- }
336
- else
337
- tools.push(item.tool);
338
- }
339
- return processGroupSummary(tools, {
340
- thinkingCount,
341
- textCount,
342
- activityCount,
343
- activityErrorCount,
344
- activityRunningCount,
345
- });
346
- }
@@ -0,0 +1,266 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import path from "node:path";
3
+ import { readJsonIfExists, writeSecureJson } from "../app-data.js";
4
+ function questionFile(root, sessionId) {
5
+ const safe = sessionId.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 180);
6
+ return path.join(root, `${safe}.json`);
7
+ }
8
+ function isRecord(value) {
9
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
10
+ }
11
+ function normalizeQuestions(value) {
12
+ if (!Array.isArray(value) || value.length === 0 || value.length > 20) {
13
+ throw new Error("ask_user_question requires 1-20 questions");
14
+ }
15
+ const ids = new Set();
16
+ return value.map((raw, index) => {
17
+ if (!isRecord(raw))
18
+ throw new Error(`question ${index + 1} must be an object`);
19
+ const id = typeof raw.id === "string" ? raw.id.trim() : "";
20
+ const question = typeof raw.question === "string" ? raw.question.trim() : "";
21
+ if (!id || !question)
22
+ throw new Error(`question ${index + 1} requires id and question`);
23
+ if (ids.has(id))
24
+ throw new Error(`duplicate question id: ${id}`);
25
+ ids.add(id);
26
+ const options = raw.options === undefined ? undefined : normalizeOptions(raw.options, id);
27
+ return {
28
+ id,
29
+ question,
30
+ ...(typeof raw.header === "string" && raw.header.trim()
31
+ ? { header: raw.header.trim().slice(0, 80) }
32
+ : {}),
33
+ ...(typeof raw.detail === "string" && raw.detail.trim()
34
+ ? { detail: raw.detail.trim().slice(0, 4000) }
35
+ : {}),
36
+ ...(options ? { options } : {}),
37
+ ...(raw.multi_select === true ? { multiSelect: true } : {}),
38
+ };
39
+ });
40
+ }
41
+ function normalizeOptions(value, questionId) {
42
+ if (!Array.isArray(value) || value.length === 0 || value.length > 12) {
43
+ throw new Error(`question ${questionId} options must contain 1-12 items`);
44
+ }
45
+ const labels = new Set();
46
+ return value.map((raw, index) => {
47
+ if (!isRecord(raw) || typeof raw.label !== "string" || !raw.label.trim()) {
48
+ throw new Error(`question ${questionId} option ${index + 1} requires a label`);
49
+ }
50
+ const label = raw.label.trim();
51
+ if (labels.has(label))
52
+ throw new Error(`duplicate option label: ${label}`);
53
+ labels.add(label);
54
+ return {
55
+ label,
56
+ ...(typeof raw.description === "string" && raw.description.trim()
57
+ ? { description: raw.description.trim().slice(0, 300) }
58
+ : {}),
59
+ };
60
+ });
61
+ }
62
+ function normalizeAnswer(value, questions) {
63
+ if (!isRecord(value) || !Array.isArray(value.answers))
64
+ throw new Error("answers must be an array");
65
+ const byId = new Map(questions.map((question) => [question.id, question]));
66
+ const seen = new Set();
67
+ const answers = value.answers.map((raw) => {
68
+ if (!isRecord(raw) || typeof raw.id !== "string")
69
+ throw new Error("answer id is required");
70
+ const id = raw.id.trim();
71
+ const question = byId.get(id);
72
+ if (!question || seen.has(id))
73
+ throw new Error(`answer does not match question: ${id}`);
74
+ seen.add(id);
75
+ const selected = Array.isArray(raw.selected)
76
+ ? raw.selected.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)
77
+ : [];
78
+ const allowed = new Set((question.options ?? []).map((option) => option.label));
79
+ if (selected.some((item) => !allowed.has(item)))
80
+ throw new Error(`answer contains an unknown option for ${id}`);
81
+ if (!question.multiSelect && selected.length > 1)
82
+ throw new Error(`question ${id} accepts one option`);
83
+ const custom = typeof raw.custom === "string" ? raw.custom.trim().slice(0, 4000) : "";
84
+ return { id, selected, ...(custom ? { custom } : {}) };
85
+ });
86
+ if (seen.size !== questions.length)
87
+ throw new Error("every question requires an answer or skip entry");
88
+ return { answers };
89
+ }
90
+ export class UserQuestionRegistry {
91
+ root;
92
+ pending = new Map();
93
+ /** Serialize first-write and resolve/cancel writes for one session. */
94
+ persistence = new Map();
95
+ waiters = new Map();
96
+ turnIds = new Map();
97
+ listeners = new Set();
98
+ constructor(root) {
99
+ this.root = root;
100
+ }
101
+ setTurn(sessionId, turnId) {
102
+ this.turnIds.set(sessionId, turnId);
103
+ }
104
+ clearTurn(sessionId) {
105
+ this.turnIds.delete(sessionId);
106
+ }
107
+ subscribe(listener) {
108
+ this.listeners.add(listener);
109
+ return () => this.listeners.delete(listener);
110
+ }
111
+ async get(sessionId) {
112
+ const memory = this.pending.get(sessionId);
113
+ if (memory?.status === "pending")
114
+ return structuredClone(memory);
115
+ const disk = await readJsonIfExists(questionFile(this.root, sessionId));
116
+ if (disk?.status === "pending") {
117
+ this.pending.set(sessionId, disk);
118
+ return structuredClone(disk);
119
+ }
120
+ return undefined;
121
+ }
122
+ async ask(input) {
123
+ const questions = normalizeQuestions(input.questions);
124
+ if (input.signal?.aborted)
125
+ throw new Error("USER_QUESTION_ABORTED");
126
+ if (await this.get(input.sessionId))
127
+ throw new Error("USER_QUESTION_ALREADY_PENDING");
128
+ // Two tool calls can finish the disk lookup in the same turn. Re-check the
129
+ // in-memory lease after that await so only the first one can publish.
130
+ if (this.pending.get(input.sessionId)?.status === "pending") {
131
+ throw new Error("USER_QUESTION_ALREADY_PENDING");
132
+ }
133
+ const now = new Date().toISOString();
134
+ const question = {
135
+ schemaVersion: 1,
136
+ requestId: `question_${randomBytes(12).toString("hex")}`,
137
+ sessionId: input.sessionId,
138
+ toolCallId: input.toolCallId,
139
+ ...(this.turnIds.get(input.sessionId) ? { turnId: this.turnIds.get(input.sessionId) } : {}),
140
+ questions,
141
+ createdAt: now,
142
+ updatedAt: now,
143
+ status: "pending",
144
+ version: 1,
145
+ };
146
+ this.pending.set(input.sessionId, question);
147
+ // Register the waiter before the first persistence await. A turn abort can
148
+ // arrive while the request is being written; registering afterwards leaves
149
+ // cancel() with nobody to reject and the tool promise would hang forever.
150
+ let persisted = false;
151
+ let abortRequested = false;
152
+ const waiting = new Promise((resolve, reject) => {
153
+ const onAbort = () => {
154
+ abortRequested = true;
155
+ if (persisted) {
156
+ void this.cancel(input.sessionId, question.requestId, "TURN_ABORTED").catch(reject);
157
+ }
158
+ };
159
+ this.waiters.set(input.sessionId, {
160
+ resolve: (answer) => {
161
+ input.signal?.removeEventListener("abort", onAbort);
162
+ resolve(answer);
163
+ },
164
+ reject: (error) => {
165
+ input.signal?.removeEventListener("abort", onAbort);
166
+ reject(error);
167
+ },
168
+ });
169
+ if (input.signal?.aborted)
170
+ onAbort();
171
+ else
172
+ input.signal?.addEventListener("abort", onAbort, { once: true });
173
+ });
174
+ try {
175
+ await this.persist(input.sessionId, question);
176
+ persisted = true;
177
+ if (abortRequested) {
178
+ await this.cancel(input.sessionId, question.requestId, "TURN_ABORTED");
179
+ return waiting;
180
+ }
181
+ this.emit({ type: "requested", question });
182
+ }
183
+ catch (error) {
184
+ const waiter = this.waiters.get(input.sessionId);
185
+ this.pending.delete(input.sessionId);
186
+ this.waiters.delete(input.sessionId);
187
+ const failure = error instanceof Error ? error : new Error(String(error));
188
+ if (waiter) {
189
+ waiter.reject(failure);
190
+ return waiting;
191
+ }
192
+ throw failure;
193
+ }
194
+ return waiting;
195
+ }
196
+ async answer(sessionId, requestId, rawAnswer) {
197
+ await this.persistence.get(sessionId);
198
+ const current = await this.get(sessionId);
199
+ if (!current)
200
+ throw new Error("USER_QUESTION_NOT_FOUND");
201
+ if (current.requestId !== requestId)
202
+ throw new Error("USER_QUESTION_STALE");
203
+ const answer = normalizeAnswer(rawAnswer, current.questions);
204
+ const resolved = {
205
+ ...current,
206
+ status: "resolved",
207
+ answer,
208
+ updatedAt: new Date().toISOString(),
209
+ version: current.version + 1,
210
+ };
211
+ this.pending.delete(sessionId);
212
+ await this.persist(sessionId, resolved);
213
+ this.emit({ type: "resolved", question: resolved });
214
+ const waiter = this.waiters.get(sessionId);
215
+ this.waiters.delete(sessionId);
216
+ waiter?.resolve(answer);
217
+ return resolved;
218
+ }
219
+ async cancel(sessionId, requestId, reason = "USER_CANCELLED") {
220
+ await this.persistence.get(sessionId);
221
+ const current = await this.get(sessionId);
222
+ if (!current)
223
+ throw new Error("USER_QUESTION_NOT_FOUND");
224
+ if (current.requestId !== requestId)
225
+ throw new Error("USER_QUESTION_STALE");
226
+ const cancelled = {
227
+ ...current,
228
+ status: "cancelled",
229
+ cancelReason: reason,
230
+ updatedAt: new Date().toISOString(),
231
+ version: current.version + 1,
232
+ };
233
+ this.pending.delete(sessionId);
234
+ await this.persist(sessionId, cancelled);
235
+ this.emit({ type: "cancelled", question: cancelled });
236
+ const waiter = this.waiters.get(sessionId);
237
+ this.waiters.delete(sessionId);
238
+ waiter?.reject(new Error(reason));
239
+ return cancelled;
240
+ }
241
+ async cancelForSession(sessionId, reason = "SESSION_DISPOSED") {
242
+ const current = await this.get(sessionId);
243
+ if (current)
244
+ await this.cancel(sessionId, current.requestId, reason);
245
+ }
246
+ emit(change) {
247
+ for (const listener of this.listeners) {
248
+ try {
249
+ listener(change);
250
+ }
251
+ catch { /* observers never break the waiter */ }
252
+ }
253
+ }
254
+ async persist(sessionId, question) {
255
+ const write = writeSecureJson(questionFile(this.root, sessionId), question);
256
+ this.persistence.set(sessionId, write);
257
+ try {
258
+ await write;
259
+ }
260
+ finally {
261
+ if (this.persistence.get(sessionId) === write) {
262
+ this.persistence.delete(sessionId);
263
+ }
264
+ }
265
+ }
266
+ }
@@ -28,6 +28,20 @@ function readWorkspaceParams(query) {
28
28
  ...(read("fromNodeId") ? { fromNodeId: read("fromNodeId") } : {}),
29
29
  ...(read("featureId") ? { featureId: read("featureId") } : {}),
30
30
  ...(read("intent") ? { intent: read("intent") } : {}),
31
+ ...(read("workspaceId") ? { workspaceId: read("workspaceId") } : {}),
32
+ ...(read("sessionId") ? { sessionId: read("sessionId") } : {}),
33
+ };
34
+ }
35
+ export function resolveLegacyWorkspaceSelection(location, knownWorkspaceIds) {
36
+ const parsed = parseConsoleRoute(location);
37
+ const requested = parsed?.workspaceId?.trim();
38
+ if (parsed && requested && !knownWorkspaceIds.includes(requested)) {
39
+ return { status: "unknown-workspace", workspaceId: requested };
40
+ }
41
+ return {
42
+ status: "ok",
43
+ workspace: parsed?.workspace ?? resolveDefaultLanding({ location }),
44
+ params: parsed ?? {},
31
45
  };
32
46
  }
33
47
  /** Parse `#/tasks?taskId=…` hash routes (Observe-style virtual paths). */
@@ -0,0 +1,13 @@
1
+ /**
2
+ * DSH's native directory-picker adapter.
3
+ *
4
+ * The published DSH host package uses the modern Win32 IFileOpenDialog in a
5
+ * dedicated child process (plus osascript/Zenity/KDialog on other platforms),
6
+ * so the Console does not pay PowerShell startup cost or lose the chooser
7
+ * behind the browser window.
8
+ */
9
+ import { pickNativeDirectory, } from "@deepseek-ai/dsh-host-directory-picker-native";
10
+ /** Open the host OS single-folder chooser. Cancellation is a null result. */
11
+ export async function pickNativeWorkspaceDirectory(signal, internals = {}) {
12
+ return pickNativeDirectory(signal, internals);
13
+ }