@tea-agent/loop-agent 0.39.0-next.2 → 0.39.0-next.20

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 (121) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/dist/build-stamp.json +2 -2
  3. package/dist/commands/init-upgrade.js +97 -19
  4. package/dist/executors/dag-pi-executor.js +42 -4
  5. package/dist/executors/shell-executor.js +269 -15
  6. package/dist/shared/operator/capabilities.js +149 -27
  7. package/dist/task/config-types.js +22 -0
  8. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  9. package/dist/worker/console/chat/pi-runtime.js +165 -83
  10. package/dist/worker/console/chat/repo-walk.js +10 -1
  11. package/dist/worker/console/chat/routes.js +19 -8
  12. package/dist/worker/console/chat/semantic-activity.js +17 -5
  13. package/dist/worker/console/chat/turn-process.js +17 -30
  14. package/dist/worker/console/harness-pi-model-matrix.js +137 -0
  15. package/dist/worker/console/operator-actions.js +128 -4
  16. package/dist/worker/console/operator-user-error.js +19 -0
  17. package/dist/worker/console/pi-readiness.js +8 -1
  18. package/dist/worker/console/security.js +20 -1
  19. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-BmOEx0gA.js} +1 -1
  20. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-BGiVVe8b.js} +1 -1
  21. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-nDdu4rgJ.js} +1 -1
  22. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-Bb5u0BWf.js} +1 -1
  23. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-BlhC0j94.js} +1 -1
  24. package/dist/worker/console/static/assets/channel-Tl-FkUqr.js +1 -0
  25. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-Ai8_qQXI.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-B9gyCjqX.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-Bzbj3zGH.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BT415PvT.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-D_A5_5QL.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-srJkaP3f.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-UYGvEP4l.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-B8luBrrz.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-DN7wXZSq.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-DXKyMuKS.js} +1 -1
  35. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BaehtRk4.js +1 -0
  36. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BaehtRk4.js +1 -0
  37. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-dqvoynXj.js} +1 -1
  38. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2--PcyL34A.js} +1 -1
  39. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DvWs_hTY.js} +1 -1
  40. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-DxeIxbyO.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-DPCG61Of.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-CmuU50-U.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-B61XLD_v.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-BB7HTRRY.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-DvkboAIp.js} +1 -1
  46. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-BdJ_y1fL.js} +1 -1
  47. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-9bUBZTRi.js} +1 -1
  48. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-Dw4L4RBa.js} +1 -1
  49. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-Jr82N_QP.js} +1 -1
  50. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-DAz9wFLr.js} +1 -1
  51. package/dist/worker/console/static/assets/index-B7Y-0g3J.css +1 -0
  52. package/dist/worker/console/static/assets/index-DkCrqaYg.js +325 -0
  53. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-CBB5Np7y.js} +1 -1
  54. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-D8IOp94y.js} +1 -1
  55. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-Dgd3AHGG.js} +1 -1
  56. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-BmYOxLE9.js} +1 -1
  57. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-DfYTEF6J.js} +1 -1
  58. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-BjuFPRqa.js} +5 -5
  59. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-CLyUViGd.js} +1 -1
  60. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-BvX2Di4-.js} +1 -1
  61. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-BFj0_OLv.js} +1 -1
  62. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-BIUrD_hx.js} +1 -1
  63. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-CHBB48fj.js} +1 -1
  64. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-sJrYw2Sf.js} +1 -1
  65. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-BzHH6fPR.js} +1 -1
  66. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-7QCnelCt.js} +1 -1
  67. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-sDYSxn72.js} +1 -1
  68. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-XpiUdwzm.js} +1 -1
  69. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-eEhq2A8Y.js +1 -0
  70. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DvqgNPzD.js} +2 -2
  71. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-la8AC7fo.js +8 -0
  72. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-2TyOYNx0.js} +1 -1
  73. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-B-nM6wM2.js} +1 -1
  74. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-B86wPdbr.js} +1 -1
  75. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-Dr1C4AQz.js} +1 -1
  76. package/dist/worker/console/static/index.html +2 -2
  77. package/dist/worker/console/static-src/app/console-types.js +39 -0
  78. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +44 -21
  79. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  80. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  81. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +15 -0
  82. package/dist/worker/console/static-src/operator-chat/useChatStream.js +17 -4
  83. package/dist/worker/console/static-src/operator-chat/useChatThread.js +28 -22
  84. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  85. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +755 -51
  86. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  87. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  88. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  89. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  90. package/dist/workflows/dag/backend-test-layout.js +133 -0
  91. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  92. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  93. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  94. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  95. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  96. package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
  97. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  98. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  99. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  100. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  101. package/dist/workflows/dag/frontend-implementation-contract.js +386 -0
  102. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  103. package/dist/workflows/dag/frontend-prewrite-gate.js +243 -8
  104. package/dist/workflows/dag/init-hybrid.js +753 -104
  105. package/dist/workflows/dag/node-execution.js +61 -2
  106. package/dist/workflows/dag/retry-policy.js +4 -0
  107. package/dist/workflows/dag/types.js +74 -0
  108. package/docs/templates/backend-test-dag.json +60 -35
  109. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  110. package/harness.json +3 -1
  111. package/package.json +1 -1
  112. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  113. package/skills/frontend-implementation/references/design-spec.md +16 -19
  114. package/skills/frontend-implementation/references/node-contracts.md +9 -6
  115. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  116. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  117. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  118. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  119. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  120. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  121. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Resource-panel diagnostics presentation.
3
+ *
4
+ * Pi ResourceLoader emits machine English (`warning` / `collision` + full
5
+ * Windows paths). The Skills / Packages / Models panels must not dump that
6
+ * block above the inventory — it pushes the list off-screen. Classify here,
7
+ * then render a header chip + grouped popover with natural Chinese copy.
8
+ */
9
+ const COLLISION_NAME = /name\s+"([^"]+)"\s+collision/i;
10
+ const INVALID_CHARS = /invalid characters|must be lowercase/i;
11
+ const INVALID_EDGE_HYPHEN = /must not start or end with a hyphen/i;
12
+ const INVALID_DOUBLE_HYPHEN = /consecutive hyphens/i;
13
+ const GROUP_COPY = {
14
+ "load-failed": {
15
+ heading: "需要处理",
16
+ hint: "有些清单没读完整,下面的列表可能不全。",
17
+ },
18
+ "invalid-name": {
19
+ heading: "名称不规范",
20
+ hint: "技能已经加载,还能用。名称请改成小写字母、数字和连字符,例如 sdd-requirement-analysis。",
21
+ },
22
+ collision: {
23
+ heading: "同名未生效",
24
+ hint: "同名技能只保留先扫到的那一份,后面的不会进当前会话。",
25
+ },
26
+ other: {
27
+ heading: "其他提示",
28
+ hint: "不影响你浏览当前列表。",
29
+ },
30
+ };
31
+ const GROUP_ORDER = [
32
+ "load-failed",
33
+ "invalid-name",
34
+ "collision",
35
+ "other",
36
+ ];
37
+ export function shortResourcePath(path) {
38
+ if (!path)
39
+ return undefined;
40
+ const parts = path.replace(/\\/g, "/").split("/").filter(Boolean);
41
+ if (parts.length === 0)
42
+ return path;
43
+ const file = parts.at(-1);
44
+ if (!file)
45
+ return path;
46
+ const parent = parts.at(-2);
47
+ return parent ? `${parent}/${file}` : file;
48
+ }
49
+ export function skillNameFromPath(path) {
50
+ if (!path)
51
+ return undefined;
52
+ const parts = path.replace(/\\/g, "/").split("/").filter(Boolean);
53
+ if (parts.length === 0)
54
+ return undefined;
55
+ const last = parts.at(-1);
56
+ if (!last)
57
+ return undefined;
58
+ if (/^skill\.md$/i.test(last) && parts.length >= 2) {
59
+ return parts.at(-2);
60
+ }
61
+ return last.replace(/\.md$/i, "");
62
+ }
63
+ export function diagnosticMatchesSkill(item, skill) {
64
+ if (item.path) {
65
+ const left = item.path.replace(/\\/g, "/").toLowerCase();
66
+ const right = skill.path.replace(/\\/g, "/").toLowerCase();
67
+ if (left === right || left.endsWith(right) || right.endsWith(left)) {
68
+ return true;
69
+ }
70
+ }
71
+ return Boolean(item.skillName && item.skillName === skill.name);
72
+ }
73
+ function shortenMessage(message) {
74
+ const trimmed = message.trim();
75
+ if (trimmed.length <= 80)
76
+ return trimmed;
77
+ return `${trimmed.slice(0, 79)}…`;
78
+ }
79
+ function titleForLoadFailed(diag) {
80
+ const hay = `${diag.code ?? ""} ${diag.message}`.toLowerCase();
81
+ if (hay.includes("preference"))
82
+ return "自动使用偏好读不出来";
83
+ if (hay.includes("package"))
84
+ return "软件包清单没读完整";
85
+ if (hay.includes("skill"))
86
+ return "技能清单没读完整";
87
+ if (hay.includes("extension"))
88
+ return "扩展没读完整";
89
+ if (hay.includes("prompt"))
90
+ return "提示词没读完整";
91
+ if (hay.includes("theme"))
92
+ return "主题没读完整";
93
+ if (hay.includes("context"))
94
+ return "上下文文件没读完整";
95
+ return "有些内容没读完整";
96
+ }
97
+ function detailForInvalidName(message) {
98
+ if (INVALID_DOUBLE_HYPHEN.test(message)) {
99
+ return "名称里不要连用两个连字符。现在这样也能用。";
100
+ }
101
+ if (INVALID_EDGE_HYPHEN.test(message)) {
102
+ return "名称开头和结尾不要用连字符。现在这样也能用。";
103
+ }
104
+ return "名称请只用小写字母、数字和连字符。现在这样也能用。";
105
+ }
106
+ export function classifyResourceDiagnostic(diag) {
107
+ const type = (diag.type ?? "warning").toLowerCase();
108
+ const message = diag.message ?? "";
109
+ const collisionMatch = message.match(COLLISION_NAME) ??
110
+ (type === "collision" ? message.match(/"([^"]+)"/) : null);
111
+ if (type === "collision" || collisionMatch) {
112
+ const skillName = collisionMatch?.[1] ?? skillNameFromPath(diag.path);
113
+ return {
114
+ kind: "collision",
115
+ tone: "warning",
116
+ title: skillName ? `「${skillName}」有多份` : "有一份同名技能没生效",
117
+ detail: "同名只保留先扫到的那一份,这份被盖掉了。",
118
+ path: diag.path,
119
+ shortPath: shortResourcePath(diag.path),
120
+ skillName,
121
+ retryable: false,
122
+ };
123
+ }
124
+ if (INVALID_CHARS.test(message) ||
125
+ INVALID_EDGE_HYPHEN.test(message) ||
126
+ INVALID_DOUBLE_HYPHEN.test(message)) {
127
+ const skillName = skillNameFromPath(diag.path);
128
+ return {
129
+ kind: "invalid-name",
130
+ tone: "warning",
131
+ title: skillName ?? "有个技能名称不合规范",
132
+ detail: detailForInvalidName(message),
133
+ path: diag.path,
134
+ shortPath: shortResourcePath(diag.path),
135
+ skillName,
136
+ retryable: false,
137
+ };
138
+ }
139
+ const isLoadFailed = type === "error" ||
140
+ diag.retryable === true ||
141
+ diag.code === "RESOURCE_LOAD_FAILED" ||
142
+ diag.code === "PACKAGE_INVENTORY_FAILED" ||
143
+ diag.code === "PI_RESOURCE_PREFERENCES_MALFORMED";
144
+ if (isLoadFailed) {
145
+ return {
146
+ kind: "load-failed",
147
+ tone: "error",
148
+ title: titleForLoadFailed(diag),
149
+ detail: shortenMessage(message),
150
+ path: diag.path,
151
+ shortPath: shortResourcePath(diag.path),
152
+ retryable: diag.retryable === true,
153
+ };
154
+ }
155
+ return {
156
+ kind: "other",
157
+ tone: type === "error" ? "error" : "warning",
158
+ title: shortenMessage(message) || "一条提示",
159
+ path: diag.path,
160
+ shortPath: shortResourcePath(diag.path),
161
+ retryable: false,
162
+ };
163
+ }
164
+ export function summarizeResourceDiagnostics(diagnostics) {
165
+ const items = diagnostics.map(classifyResourceDiagnostic);
166
+ const buckets = new Map();
167
+ for (const item of items) {
168
+ const list = buckets.get(item.kind) ?? [];
169
+ list.push(item);
170
+ buckets.set(item.kind, list);
171
+ }
172
+ const groups = GROUP_ORDER.flatMap((kind) => {
173
+ const groupItems = buckets.get(kind);
174
+ if (!groupItems?.length)
175
+ return [];
176
+ return [{ kind, ...GROUP_COPY[kind], items: groupItems }];
177
+ });
178
+ const errorCount = items.filter((item) => item.tone === "error").length;
179
+ const warningCount = items.length - errorCount;
180
+ return {
181
+ total: items.length,
182
+ errorCount,
183
+ warningCount,
184
+ chipLabel: chipLabel(errorCount, warningCount),
185
+ groups,
186
+ };
187
+ }
188
+ function chipLabel(errorCount, warningCount) {
189
+ if (errorCount > 0 && warningCount > 0) {
190
+ return `${errorCount} 个问题 · ${warningCount} 条提示`;
191
+ }
192
+ if (errorCount > 0) {
193
+ return errorCount === 1 ? "1 个问题" : `${errorCount} 个问题`;
194
+ }
195
+ if (warningCount === 1)
196
+ return "1 条提示";
197
+ return `${warningCount} 条提示`;
198
+ }
199
+ export function skillHasNameWarning(skill, items) {
200
+ return items.some((item) => item.kind === "invalid-name" && diagnosticMatchesSkill(item, skill));
201
+ }
202
+ export function noticesForSkill(skill, items) {
203
+ return items.filter((item) => item.kind !== "collision" && diagnosticMatchesSkill(item, skill));
204
+ }
@@ -5,6 +5,7 @@ import { hydrateSessionThread } from "../../chat/turn-process.js";
5
5
  import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
6
6
  import { CHAT_SESSION_LIST_LIMIT } from "../chat-view-types.js";
7
7
  import { confirmationToken } from "./format.js";
8
+ import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
8
9
  import { SessionTitleWatcher } from "./session-title-watcher.js";
9
10
  export async function createSessionAndPrimeTitleWatcher(input) {
10
11
  if (input.creatingRef?.current) {
@@ -69,6 +70,8 @@ export function useChatSessions(params) {
69
70
  // Shared busy gate for sidebar + Landing create entries (one render feedback).
70
71
  const [creatingSession, setCreatingSession] = useState(false);
71
72
  const creatingSessionRef = useRef(false);
73
+ /** One-shot guard so a stale-token failure schedules a single page reload. */
74
+ const compactReloadScheduledRef = useRef(false);
72
75
  // Only the latest activate/create/restore intent may commit active-session
73
76
  // state. This prevents slow responses from an older selection overwriting a
74
77
  // newer user choice.
@@ -396,6 +399,18 @@ export function useChatSessions(params) {
396
399
  });
397
400
  const body = (await response.json().catch(() => ({})));
398
401
  if (!response.ok) {
402
+ const gateCode = body.error?.code;
403
+ if (isMutationGateFailureCode(gateCode)) {
404
+ // Stale boot tokens: the page predates the current server boot.
405
+ // Auto-reload once so the fresh boot page re-injects tokens; keep
406
+ // an error line up while reloading in case reload is blocked.
407
+ setError(describeMutationGateFailure(gateCode));
408
+ if (isStaleTokenFailure(gateCode) && !compactReloadScheduledRef.current) {
409
+ compactReloadScheduledRef.current = true;
410
+ window.setTimeout(() => window.location.reload(), 1200);
411
+ }
412
+ return;
413
+ }
399
414
  setError(body.error?.message ?? "上下文压缩失败");
400
415
  return;
401
416
  }
@@ -242,9 +242,14 @@ export function useChatStream(params) {
242
242
  /** Issue real POST /abort for a known turnId (Stop never disposes Session). */
243
243
  const postAbortForTurn = useCallback(async (sessionId, generation, turnId) => {
244
244
  refs.stoppingTurnIdRef.current = turnId;
245
- // Keep streaming/busy while abort is in flight (UIS-stopping).
246
- refs.streamingRef.current = true;
247
- setStreaming(true);
245
+ // Optimistic stop (pi-web parity): the abort signal kills the SDK
246
+ // process tree promptly, so flip the UI idle NOW instead of holding
247
+ // the busy spinner through the whole POST /abort round-trip. If the
248
+ // server later reports the turn still running (TURN_ABORT_PENDING
249
+ // or owner-lost reconcile), applyAbortDecision restores busy state.
250
+ refs.streamingRef.current = false;
251
+ setStreaming(false);
252
+ setStreamingAssistantId(null);
248
253
  try {
249
254
  const res = await fetch(operatorChatAbortUrl(origin, sessionId, turnId), {
250
255
  method: "POST",
@@ -285,6 +290,10 @@ export function useChatStream(params) {
285
290
  markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
286
291
  });
287
292
  applyOwnershipBagToRefs(refs, bag);
293
+ // Sync the post-decision busy state back to React: optimistic stop
294
+ // flipped it idle up front; pending/keep-busy outcomes restore it.
295
+ if (bag.streaming)
296
+ setStreaming(true);
288
297
  }
289
298
  catch {
290
299
  // Abort network failure: keep busy (UIS-abort-network-fail).
@@ -297,6 +306,10 @@ export function useChatStream(params) {
297
306
  networkError: true,
298
307
  }), noticeSink());
299
308
  applyOwnershipBagToRefs(refs, bag);
309
+ // Network failure: the optimistic idle was premature — restore busy
310
+ // so the turn is not silently dropped; /state reconcile converges.
311
+ refs.streamingRef.current = true;
312
+ setStreaming(true);
300
313
  }
301
314
  }, [
302
315
  origin,
@@ -371,7 +384,7 @@ export function useChatStream(params) {
371
384
  refs.streamingRef.current ||
372
385
  refs.submitInFlightRef.current ||
373
386
  // AC-R5-002: ownership lease blocks send until terminal release.
374
- Boolean(refs.pendingSubmissionRef.current))
387
+ refs.pendingSubmissionRef.current)
375
388
  return;
376
389
  // AC-001: acquire synchronous submit ownership before the first await.
377
390
  refs.submitInFlightRef.current = true;
@@ -1,6 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
- import { parseAssistantContent } from "../../chat/assistant-content.js";
3
- import { buildTurnProcessGroups } from "../../chat/turn-process.js";
2
+ import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, } from "../../chat/turn-process.js";
4
3
  import { createChatSseEventApplier, } from "./chat-sse-events.js";
5
4
  import { taskContextUnchanged } from "./format.js";
6
5
  const EMPTY_USAGE = {
@@ -121,9 +120,7 @@ export function useChatThread(params) {
121
120
  }, 350);
122
121
  return () => window.clearTimeout(timer);
123
122
  }, [sessionId, operationCards, humanGateCards, interview, loadTaskContext]);
124
- const visibleMessages = useMemo(() => messages
125
- .filter((m) => m.role !== "system" || showSystemInMessages)
126
- .slice(-historyLimit), [messages, showSystemInMessages, historyLimit]);
123
+ const timelineMessages = useMemo(() => messages.filter((m) => m.role !== "system" || showSystemInMessages), [messages, showSystemInMessages]);
127
124
  useEffect(() => {
128
125
  const onVisibility = () => {
129
126
  if (!document.hidden)
@@ -158,12 +155,30 @@ export function useChatThread(params) {
158
155
  statusKind,
159
156
  };
160
157
  }, [toolCalls]);
161
- const turnGroups = useMemo(() => buildTurnProcessGroups({
162
- messages: visibleMessages,
158
+ const turnGroups = useMemo(() => {
159
+ const all = buildTurnProcessGroups({
160
+ messages: timelineMessages,
161
+ toolCalls,
162
+ streaming,
163
+ activeAssistantId: streamingAssistantId,
164
+ });
165
+ return selectRecentTurnGroups(all, historyLimit);
166
+ }, [
167
+ timelineMessages,
163
168
  toolCalls,
164
169
  streaming,
165
- activeAssistantId: streamingAssistantId,
166
- }), [visibleMessages, toolCalls, streaming, streamingAssistantId]);
170
+ streamingAssistantId,
171
+ historyLimit,
172
+ ]);
173
+ const visibleMessages = useMemo(() => {
174
+ const next = [];
175
+ for (const group of turnGroups) {
176
+ if (group.user)
177
+ next.push(group.user);
178
+ next.push(...group.assistants);
179
+ }
180
+ return next;
181
+ }, [turnGroups]);
167
182
  /** Reset all session-scoped projections before adopting a fresh session. */
168
183
  const resetForNewSession = useCallback(() => {
169
184
  setMessages([]);
@@ -220,18 +235,9 @@ export function useChatThread(params) {
220
235
  refs.seenEventIdsRef.current = new Set();
221
236
  refs.turnAssistantIdsRef.current = new Map();
222
237
  refs.sealedAssistantIdsRef.current = new Set();
223
- setMessages((body.session.messages ?? [])
224
- .filter((message) => message.role === "user" || message.role === "assistant")
225
- .map((message) => {
226
- const content = parseAssistantContent(message.content);
227
- return {
228
- id: message.id,
229
- role: message.role,
230
- text: message.text,
231
- createdAt: Date.parse(message.at) || Date.now(),
232
- ...(content && content.length > 0 ? { content } : {}),
233
- };
234
- }));
238
+ const hydrated = hydrateSessionThread(body.session.messages ?? []);
239
+ setMessages(hydrated.messages);
240
+ setToolCalls(hydrated.toolCalls);
235
241
  setStreamingAssistantId(null);
236
242
  return true;
237
243
  }
@@ -240,7 +246,7 @@ export function useChatThread(params) {
240
246
  // trigger another reconcile and retry (best-effort recovery).
241
247
  return false;
242
248
  }
243
- }, [origin, refs, setMessages, setStreamingAssistantId]);
249
+ }, [origin, refs, setMessages, setToolCalls, setStreamingAssistantId]);
244
250
  return {
245
251
  messages,
246
252
  setMessages,
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
3
3
  /** Left-rail repo tree + file preview tabs state. */
4
4
  export function useRepoBrowser(params) {
@@ -9,6 +9,8 @@ export function useRepoBrowser(params) {
9
9
  const [previews, setPreviews] = useState([]);
10
10
  const [activePreviewPath, setActivePreviewPath] = useState(null);
11
11
  const [previewLoading, setPreviewLoading] = useState(false);
12
+ const expandedDirsRef = useRef(new Set());
13
+ expandedDirsRef.current = expandedDirs;
12
14
  const loadTree = useCallback(async (directory = "") => {
13
15
  try {
14
16
  setTreeError(null);
@@ -28,6 +30,28 @@ export function useRepoBrowser(params) {
28
30
  useEffect(() => {
29
31
  void loadTree();
30
32
  }, [loadTree]);
33
+ /** pi-web parity: re-fetch the root + every expanded directory. Bump
34
+ * `refreshTick` to trigger; cached entries are NOT reused, so newly
35
+ * created/deleted files appear without a manual refresh. */
36
+ const [refreshTick, setRefreshTick] = useState(0);
37
+ const refreshTree = useCallback(() => {
38
+ setRefreshTick((tick) => tick + 1);
39
+ }, []);
40
+ useEffect(() => {
41
+ const dirs = ["", ...expandedDirsRef.current];
42
+ for (const dir of dirs)
43
+ void loadTree(dir);
44
+ }, [loadTree, refreshTick]);
45
+ // Also refresh when the tab regains focus: external tools (editor, git,
46
+ // CLI) may have changed the tree while the console was hidden.
47
+ useEffect(() => {
48
+ const onVisibility = () => {
49
+ if (!document.hidden)
50
+ refreshTree();
51
+ };
52
+ document.addEventListener("visibilitychange", onVisibility);
53
+ return () => document.removeEventListener("visibilitychange", onVisibility);
54
+ }, [refreshTree]);
31
55
  const openPreview = useCallback(async (file) => {
32
56
  setPreviewLoading(true);
33
57
  try {
@@ -116,6 +140,7 @@ export function useRepoBrowser(params) {
116
140
  previewLoading,
117
141
  setPreviews,
118
142
  loadTree,
143
+ refreshTree,
119
144
  openPreview,
120
145
  closePreview,
121
146
  toggleDir,