@tea-agent/loop-agent 0.40.0-next.3 → 0.40.0-next.5

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 (139) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/dag-pi-executor.js +46 -5
  4. package/dist/executors/shell-executor.js +28 -0
  5. package/dist/executors/workspace-write-snapshot.js +68 -0
  6. package/dist/infrastructure/harness/atomic-write.js +23 -0
  7. package/dist/task/config-types.js +10 -0
  8. package/dist/worker/console/chat/chat-event-store.js +62 -4
  9. package/dist/worker/console/chat/context-panel.js +7 -0
  10. package/dist/worker/console/chat/deferred-turn.js +12 -0
  11. package/dist/worker/console/chat/pi-runtime.js +264 -16
  12. package/dist/worker/console/chat/provider-error.js +98 -0
  13. package/dist/worker/console/chat/routes.js +292 -29
  14. package/dist/worker/console/chat/session-stats.js +179 -0
  15. package/dist/worker/console/chat/session-store.js +93 -63
  16. package/dist/worker/console/chat/todos.js +115 -0
  17. package/dist/worker/console/chat/tool-preview.js +42 -0
  18. package/dist/worker/console/chat/turn-process.js +20 -55
  19. package/dist/worker/console/chat/user-questions.js +266 -0
  20. package/dist/worker/console/chat/workspace-landing.js +14 -0
  21. package/dist/worker/console/native-directory-picker.js +13 -0
  22. package/dist/worker/console/routes.js +309 -1
  23. package/dist/worker/console/security.js +4 -4
  24. package/dist/worker/console/server.js +529 -186
  25. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CGZrpj0K.js} +1 -1
  26. package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-CwEyA8iq.js} +1 -1
  27. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-DLTkrEQ3.js} +1 -1
  28. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-4w12zR3t.js} +1 -1
  29. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-BppqLAjk.js} +1 -1
  30. package/dist/worker/console/static/assets/channel-TzuwQha5.js +1 -0
  31. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DO6U26Pm.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-DC7YEdOy.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtMYVQBw.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-C5Ed-57N.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-BUY4oIzT.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-VnWHALIV.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-COV-RQk4.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-dSnLtUiz.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-CX6FDzWh.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D3mEgb6W.js} +1 -1
  41. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DCu69b3S.js +1 -0
  42. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DCu69b3S.js +1 -0
  43. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-QQBMUnAf.js} +1 -1
  44. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-BxSD-LVV.js} +1 -1
  45. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-DxHNiNbk.js} +1 -1
  46. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-C83bYy8E.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-BhtZu2YY.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-K0P_ntRE.js} +1 -1
  49. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-CXYy1Nib.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-D-CtIFkU.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BEK9-_7c.js} +1 -1
  52. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-L0EX-9wn.js} +1 -1
  53. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-9pt0fc_5.js} +1 -1
  54. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-CZVeM_n1.js} +1 -1
  55. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-DqUKkAZP.js} +1 -1
  56. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-BYUuXEgp.js} +1 -1
  57. package/dist/worker/console/static/assets/index-B0jGzQj6.js +407 -0
  58. package/dist/worker/console/static/assets/index-BiYbPiTw.css +1 -0
  59. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-DNzKk7ah.js} +1 -1
  60. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-Clbyg5IP.js} +1 -1
  61. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-zDO_gZNO.js} +1 -1
  62. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-x3LpRYek.js} +1 -1
  63. package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CTpwZqKR.js} +1 -1
  64. package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-D_WXK6C7.js} +5 -5
  65. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-CWK-OYP5.js} +1 -1
  66. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-B9gtUPsM.js} +1 -1
  67. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-Dj2lzceR.js} +1 -1
  68. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-Nwljut7m.js} +1 -1
  69. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-BY-KNt93.js} +1 -1
  70. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Vx0wvpPy.js} +1 -1
  71. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-B6grNXUk.js} +1 -1
  72. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-CteidBvh.js} +1 -1
  73. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Du4W8mFK.js} +1 -1
  74. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-jyI6Yh81.js} +1 -1
  75. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyQblISl.js +1 -0
  76. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-DYWs_fRv.js} +2 -2
  77. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DiygmgYP.js +8 -0
  78. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DO5LLy47.js} +1 -1
  79. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CLF2p1_L.js} +1 -1
  80. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-AwTaYBxA.js} +1 -1
  81. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-DzbblQRw.js} +1 -1
  82. package/dist/worker/console/static/index.html +2 -2
  83. package/dist/worker/console/static-src/app/console-types.js +29 -4
  84. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  85. package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
  86. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  87. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  88. package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
  89. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  90. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  91. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  92. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  93. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  94. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  95. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  96. package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -17
  97. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  98. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  99. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  100. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  101. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  102. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  103. package/dist/worker/console/workspace-context.js +234 -0
  104. package/dist/worker/console/workspace-registry.js +214 -0
  105. package/dist/worker/observe/server.js +4 -0
  106. package/dist/worker/observe/static/api.js +26 -6
  107. package/dist/worker/observe/static/app.js +18 -70
  108. package/dist/worker/observe/static/index.html +1 -1
  109. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  110. package/dist/worker/observe/static/operator-chrome.css +145 -20
  111. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  112. package/dist/worker/observe/static/operator-chrome.js +330 -100
  113. package/dist/worker/observe/static/router.d.ts +46 -0
  114. package/dist/worker/observe/static/router.js +25 -1
  115. package/dist/worker/observe/static/state.d.ts +50 -0
  116. package/dist/worker/observe/static/state.js +172 -0
  117. package/dist/worker/observe/static/styles.css +20 -1
  118. package/dist/worker/observe/static/views/dag-inspector.js +4 -3
  119. package/dist/worker/observe/static/views/dag.js +2 -1
  120. package/dist/worker/observe/static/views/dashboard.js +6 -4
  121. package/dist/worker/observe/static/views/night.js +2 -2
  122. package/dist/worker/observe/static/views/pool.js +1 -0
  123. package/dist/worker/observe/static/views/run.js +3 -1
  124. package/dist/worker/observe/static/views/task.js +1 -1
  125. package/dist/workflows/dag/init-hybrid.js +19 -0
  126. package/dist/workflows/dag/interrupt-request.js +3 -3
  127. package/dist/workflows/dag/lifecycle.js +3 -3
  128. package/dist/workflows/dag/types.js +3 -1
  129. package/docs/README.md +1 -0
  130. package/docs/operations/local-development-environment.md +21 -0
  131. package/harness.json +3 -3
  132. package/package.json +3 -1
  133. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
  134. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
  135. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
  136. package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
  137. package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
  138. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
  139. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Hosted Inspect workspace adapter.
3
+ *
4
+ * Standalone `/inspect/` documents canonicalize into Console `/#/inspect/...`.
5
+ * Console hosts a single mountInspectWorkspace instance and pauses polls via
6
+ * setActive(false) without destroying selection/scroll.
7
+ */
8
+ import {
9
+ parseHashRouteWithFilters,
10
+ parseRoute,
11
+ setNavigateHandler,
12
+ inspectPathToCanonicalHref,
13
+ } from "./router.js";
14
+ import {
15
+ isPageVisible,
16
+ pauseForegroundTimers,
17
+ setWorkspaceActive,
18
+ setWorkspaceScope,
19
+ applyWorkspaceScopeSwitch,
20
+ stopTimers,
21
+ uiState,
22
+ workspaceScope,
23
+ } from "./state.js";
24
+ import { renderFailures } from "./views/failures.js";
25
+ import { startPoolView } from "./views/pool.js";
26
+ import { startDashboardPolling } from "./views/dashboard.js";
27
+ import { startDagsView } from "./views/dags.js";
28
+ import { renderFeatureDetail } from "./views/feature.js";
29
+ import { renderBatch } from "./views/batch.js";
30
+ import { startTaskPolling } from "./views/task.js";
31
+ import { startRunPolling } from "./views/run.js";
32
+ import { startDagPolling } from "./views/dag.js";
33
+ import { renderNightJobs } from "./views/night.js";
34
+ import { closeDagInspectorOnOutsideClick } from "./views/dag-inspector.js";
35
+ import { mountOperatorChrome } from "./operator-chrome.js";
36
+
37
+ let instance = null;
38
+ let lastInspectHash = "";
39
+ let listenersBound = false;
40
+
41
+ export function getInspectWorkspaceInstance() {
42
+ return instance && !instance.destroyed ? instance : null;
43
+ }
44
+
45
+ export function inspectCanonicalFromStandalone(locationLike) {
46
+ const loc = locationLike ?? (typeof location !== "undefined" ? location : null);
47
+ if (!loc) return null;
48
+ const pathname = String(loc.pathname ?? "/").replace(/\/+$/, "") || "/";
49
+ if (pathname !== "/inspect") return null;
50
+ return inspectPathToCanonicalHref(loc.hash || "#/");
51
+ }
52
+
53
+ export function tryCanonicalizeStandaloneInspect(locationLike) {
54
+ const loc = locationLike ?? (typeof location !== "undefined" ? location : null);
55
+ const canonical = inspectCanonicalFromStandalone(loc);
56
+ if (!canonical || !loc) return false;
57
+ if (typeof loc.replace === "function") loc.replace(canonical);
58
+ else if (typeof location !== "undefined" && typeof location.replace === "function") {
59
+ location.replace(canonical);
60
+ }
61
+ return true;
62
+ }
63
+
64
+ export function navigateInspectPath(path) {
65
+ const href = inspectPathToCanonicalHref(path);
66
+ if (instance && typeof instance.onNavigate === "function") {
67
+ instance.onNavigate(href);
68
+ return href;
69
+ }
70
+ if (typeof location !== "undefined") {
71
+ location.hash = href.startsWith("/#") ? href.slice(1) : href;
72
+ }
73
+ return href;
74
+ }
75
+
76
+ function startView(routeValue) {
77
+ if (routeValue.view === "dashboard") {
78
+ startDashboardPolling(routeValue.scrollTo);
79
+ } else if (routeValue.view === "dags") {
80
+ void startDagsView();
81
+ } else if (routeValue.view === "batch") {
82
+ void renderBatch(routeValue.batchRunId);
83
+ } else if (routeValue.view === "run") {
84
+ startRunPolling(routeValue.workerRunId);
85
+ } else if (routeValue.view === "dag") {
86
+ startDagPolling(routeValue.dagRunId);
87
+ } else if (routeValue.view === "failures") {
88
+ void renderFailures();
89
+ } else if (routeValue.view === "pool") {
90
+ startPoolView();
91
+ } else if (routeValue.view === "night") {
92
+ void renderNightJobs(routeValue.scheduleId);
93
+ } else if (routeValue.view === "task") {
94
+ startTaskPolling(routeValue.featureId, routeValue.taskId);
95
+ } else if (routeValue.view === "feature") {
96
+ void renderFeatureDetail(routeValue.featureId);
97
+ }
98
+ }
99
+
100
+ export function route(hashValue) {
101
+ stopTimers();
102
+ const parsed = hashValue
103
+ ? parseHashRouteWithFilters(hashValue)
104
+ : parseRoute();
105
+ startView(parsed);
106
+ }
107
+
108
+ function resumeView(hashValue) {
109
+ const parsed = hashValue
110
+ ? parseHashRouteWithFilters(hashValue)
111
+ : parseRoute();
112
+ startView(parsed);
113
+ }
114
+
115
+ function ensureStylesheet(href) {
116
+ if (typeof document === "undefined") return;
117
+ if (document.querySelector(`link[href="${href}"]`)) return;
118
+ const link = document.createElement("link");
119
+ link.rel = "stylesheet";
120
+ link.href = href;
121
+ document.head.appendChild(link);
122
+ }
123
+
124
+ export async function prepareInspectWorkspace(root) {
125
+ if (!root || typeof document === "undefined") return;
126
+ if (root.querySelector("#main-content")) return;
127
+ ensureStylesheet("/inspect/styles.css");
128
+ ensureStylesheet("/inspect/operator-chrome.css");
129
+ const res = await fetch("/inspect/", { credentials: "same-origin" });
130
+ if (!res.ok) {
131
+ throw new Error(`Inspect markup HTTP ${res.status}`);
132
+ }
133
+ const html = await res.text();
134
+ const doc = new DOMParser().parseFromString(html, "text/html");
135
+ const app = doc.getElementById("app");
136
+ if (!app) throw new Error("Inspect markup missing #app");
137
+ root.appendChild(document.importNode(app, true));
138
+ }
139
+
140
+ function bindStandaloneListeners() {
141
+ if (listenersBound || typeof window === "undefined") return;
142
+ listenersBound = true;
143
+ window.addEventListener("hashchange", () => route());
144
+ window.addEventListener("load", () => route());
145
+ document.addEventListener("pointerdown", closeDagInspectorOnOutsideClick);
146
+ document.addEventListener("visibilitychange", () => {
147
+ if (!isPageVisible()) return;
148
+ const r = parseRoute();
149
+ if (r.view === "dashboard") startDashboardPolling();
150
+ if (r.view === "run") startRunPolling(r.workerRunId, false);
151
+ if (r.view === "dag") startDagPolling(r.dagRunId, false);
152
+ if (r.view === "pool" && uiState.poolAutoRefresh) startPoolView();
153
+ if (r.view === "task") startTaskPolling(r.featureId, r.taskId);
154
+ });
155
+ }
156
+
157
+ export function startStandaloneInspect() {
158
+ if (typeof window === "undefined") return;
159
+ // Multi-workspace (AC-003): sync scope from the URL query before any fetch.
160
+ syncWorkspaceScopeFromLocation();
161
+ window.addEventListener("hashchange", syncWorkspaceScopeFromLocation);
162
+ const chromeHost = document.getElementById("operator-chrome");
163
+ if (chromeHost) {
164
+ mountOperatorChrome(chromeHost, {
165
+ mode: "inspect",
166
+ liveLocation: true,
167
+ healthUrl: "/api/health",
168
+ inspectHref: "/#/inspect",
169
+ operateHref: "/#/operate/tasks",
170
+ chatHref: "/#/chat",
171
+ readinessHref: "/#/operate/models",
172
+ readinessTitle:
173
+ "Pi 状态(只读展示)。模型与认证管理在操作侧,点击进入。",
174
+ onNavigate: (href) => {
175
+ navigateInspectPath(href);
176
+ },
177
+ });
178
+ }
179
+ setNavigateHandler((href) => {
180
+ if (href.startsWith("/#/operate") || href.startsWith("/#/chat")) {
181
+ window.location.assign(href);
182
+ return;
183
+ }
184
+ window.location.hash = href.startsWith("/#") ? href.slice(1) : href;
185
+ });
186
+ bindStandaloneListeners();
187
+ route();
188
+ }
189
+
190
+ export function mountInspectWorkspace(root, options = {}) {
191
+ const existing = getInspectWorkspaceInstance();
192
+ if (existing) {
193
+ if (typeof options.onNavigate === "function") {
194
+ existing.onNavigate = options.onNavigate;
195
+ setNavigateHandler(options.onNavigate);
196
+ }
197
+ if (options.hash) existing.updateRoute(options.hash);
198
+ if (options.active === false) existing.setActive(false);
199
+ else if (options.active === true) existing.setActive(true);
200
+ return existing;
201
+ }
202
+
203
+ // Multi-workspace (AC-002/AC-003): hosted mode receives the parent shell's
204
+ // workspaceId via mount options/hash and postMessage updates; every observe
205
+ // GET is then scoped via scopedApiUrl.
206
+ setWorkspaceScope(options.workspaceId ?? workspaceIdFromHash(options.hash));
207
+ window.addEventListener("message", onWorkspaceScopeMessage);
208
+
209
+ const hosted = options.hosted === true || options.hosted !== false;
210
+ const onNavigate =
211
+ typeof options.onNavigate === "function"
212
+ ? options.onNavigate
213
+ : (href) => navigateInspectPath(href);
214
+
215
+ setNavigateHandler(onNavigate);
216
+ lastInspectHash = options.hash || (typeof location !== "undefined" ? location.hash : "");
217
+ setWorkspaceActive(options.active !== false);
218
+
219
+ const host = {
220
+ root,
221
+ hosted,
222
+ destroyed: false,
223
+ onNavigate,
224
+ updateRoute(hashValue) {
225
+ if (host.destroyed) return;
226
+ lastInspectHash = hashValue || lastInspectHash;
227
+ if (!uiState.workspaceActive) return;
228
+ // AC-006: a hosted route update that carries a DIFFERENT workspaceId
229
+ // must not route under the old scope. Swap atomically:
230
+ // abort → clear → set scope → route new.
231
+ const nextScope = workspaceIdFromHash(lastInspectHash);
232
+ if (nextScope && nextScope !== workspaceScope()) {
233
+ applyWorkspaceScopeSwitch(nextScope, () => route(lastInspectHash));
234
+ return;
235
+ }
236
+ route(lastInspectHash);
237
+ },
238
+ setActive(active) {
239
+ if (host.destroyed) return;
240
+ const next = Boolean(active);
241
+ const wasActive = uiState.workspaceActive !== false;
242
+ setWorkspaceActive(next);
243
+ if (!next) {
244
+ pauseForegroundTimers();
245
+ return;
246
+ }
247
+ if (!wasActive) resumeView(lastInspectHash);
248
+ },
249
+ destroy() {
250
+ host.destroyed = true;
251
+ pauseForegroundTimers();
252
+ setNavigateHandler(null);
253
+ if (instance === host) instance = null;
254
+ },
255
+ };
256
+ instance = host;
257
+ if (options.autoStart !== false && !hosted) {
258
+ bindStandaloneListeners();
259
+ route(lastInspectHash);
260
+ }
261
+ return host;
262
+ }
263
+
264
+ /** Extract ?workspaceId= from a canonical console hash (e.g. #/inspect?workspaceId=ws-a). */
265
+ function workspaceIdFromHash(hashValue) {
266
+ const raw = String(hashValue ?? "").replace(/^#/, "");
267
+ const qIndex = raw.indexOf("?");
268
+ if (qIndex < 0) return null;
269
+ const id = new URLSearchParams(raw.slice(qIndex + 1)).get("workspaceId");
270
+ return id?.trim() || null;
271
+ }
272
+
273
+ function syncWorkspaceScopeFromLocation() {
274
+ if (typeof window === "undefined") return;
275
+ const next =
276
+ workspaceIdFromHash(window.location.hash) ??
277
+ workspaceIdFromSearch(window.location.search);
278
+ // Atomic scope swap (AC-006): abort old timers → clear old state → set
279
+ // scope → re-route with the new scope. A no-op change (same scope) only
280
+ // publishes the scope without tearing the live view down.
281
+ if (next === workspaceScope()) {
282
+ setWorkspaceScope(next);
283
+ return;
284
+ }
285
+ applyWorkspaceScopeSwitch(next, () => route());
286
+ }
287
+
288
+ function workspaceIdFromSearch(search) {
289
+ const params = new URLSearchParams(String(search ?? ""));
290
+ const id = params.get("workspaceId");
291
+ return id?.trim() || null;
292
+ }
293
+
294
+ function onWorkspaceScopeMessage(event) {
295
+ const data = event?.data;
296
+ if (!data || typeof data !== "object") return;
297
+ if (data.type !== "loop-console:workspace-scope") return;
298
+ const next = typeof data.workspaceId === "string" ? data.workspaceId : null;
299
+ if (next === workspaceScope()) {
300
+ setWorkspaceScope(next);
301
+ return;
302
+ }
303
+ // Atomic scope swap (AC-006): abort → clear → set → re-route. The hosted
304
+ // view is re-routed (remounted from the new scope) so no observe GET can
305
+ // fire with the previous workspaceId after the message lands.
306
+ applyWorkspaceScopeSwitch(next, () => route());
307
+ }
@@ -9,15 +9,15 @@
9
9
  */
10
10
 
11
11
  .och {
12
- --och-canvas: var(--canvas, #f7f7f4);
13
- --och-surface: var(--surface, #ffffff);
14
- --och-surface-muted: var(--surface-muted, #f0efe9);
15
- --och-ink: var(--ink, #26251e);
16
- --och-body: var(--body, #5a5852);
17
- --och-muted: var(--muted, #807d72);
18
- --och-muted-soft: var(--muted-soft, #a09c92);
19
- --och-hairline: var(--hairline, #e6e5e0);
20
- --och-hairline-strong: var(--hairline-strong, #cfcdc4);
12
+ --och-canvas: var(--canvas, #f3f4f6);
13
+ --och-surface: var(--surface, #fbfcfd);
14
+ --och-surface-muted: var(--surface-muted, #eceef1);
15
+ --och-ink: var(--ink, #202124);
16
+ --och-body: var(--body, #50545b);
17
+ --och-muted: var(--muted, #767b84);
18
+ --och-muted-soft: var(--muted-soft, #9ca1aa);
19
+ --och-hairline: var(--hairline, #e1e3e7);
20
+ --och-hairline-strong: var(--hairline-strong, #cfd2d8);
21
21
  --och-orange: var(--orange, #f54e00);
22
22
  --och-orange-active: var(--orange-active, #d04200);
23
23
  --och-green: var(--green, #1f8a65);
@@ -40,14 +40,16 @@
40
40
  top: 0;
41
41
  z-index: 50;
42
42
  border-bottom: 1px solid var(--och-hairline);
43
- background: color-mix(in srgb, var(--och-canvas) 88%, transparent);
43
+ background: var(--canvas-soft, var(--och-surface));
44
44
  backdrop-filter: blur(12px);
45
45
  -webkit-backdrop-filter: blur(12px);
46
46
  font-family: inherit;
47
47
  }
48
48
 
49
49
  .och-inner {
50
- width: min(100% - 48px, var(--och-shell-max));
50
+ box-sizing: border-box;
51
+ width: 100%;
52
+ padding-inline: 14px;
51
53
  margin: 0 auto;
52
54
  }
53
55
 
@@ -57,7 +59,8 @@
57
59
  display: flex;
58
60
  align-items: center;
59
61
  gap: 12px;
60
- height: 54px;
62
+ /* DSH 2026-08-23:约 44px 与内容同色的压缩标题栏。 */
63
+ height: 44px;
61
64
  }
62
65
 
63
66
  .och-brand {
@@ -70,6 +73,7 @@
70
73
  color: var(--och-ink);
71
74
  text-decoration: none;
72
75
  white-space: nowrap;
76
+ flex-shrink: 0;
73
77
  }
74
78
  .och-brand-mark {
75
79
  display: block;
@@ -114,6 +118,19 @@
114
118
  color: var(--och-muted);
115
119
  }
116
120
 
121
+ .och-workspace-picker {
122
+ position: relative;
123
+ display: inline-flex;
124
+ min-width: 0;
125
+ flex-shrink: 0;
126
+ }
127
+
128
+ .och-workspace-trigger .och-repo-name {
129
+ max-width: 180px;
130
+ overflow: hidden;
131
+ text-overflow: ellipsis;
132
+ }
133
+
117
134
  .och-pop {
118
135
  position: absolute;
119
136
  top: 50px;
@@ -155,21 +172,122 @@
155
172
  word-break: break-all;
156
173
  }
157
174
 
175
+ .och-workspace-picker .och-pop {
176
+ top: 36px;
177
+ left: 0;
178
+ width: min(340px, calc(100vw - 28px));
179
+ min-width: 260px;
180
+ padding: 8px;
181
+ }
182
+ .och-workspace-picker .och-pop-title {
183
+ padding: 5px 7px 7px;
184
+ margin: 0;
185
+ font-size: 11px;
186
+ font-weight: 700;
187
+ letter-spacing: 0.06em;
188
+ color: var(--och-muted);
189
+ }
190
+ .och-workspace-option {
191
+ appearance: none;
192
+ display: flex;
193
+ align-items: center;
194
+ justify-content: space-between;
195
+ gap: 12px;
196
+ width: 100%;
197
+ min-height: 48px;
198
+ padding: 7px 8px;
199
+ border: 0;
200
+ border-radius: 9px;
201
+ background: transparent;
202
+ color: var(--och-body);
203
+ font: inherit;
204
+ text-align: left;
205
+ cursor: pointer;
206
+ }
207
+ .och-workspace-option:hover,
208
+ .och-workspace-option:focus-visible,
209
+ .och-workspace-option[aria-checked="true"] {
210
+ background: color-mix(in srgb, var(--och-ink) 7%, transparent);
211
+ color: var(--och-ink);
212
+ }
213
+ .och-workspace-option-copy {
214
+ display: grid;
215
+ min-width: 0;
216
+ gap: 2px;
217
+ }
218
+ .och-workspace-option strong,
219
+ .och-workspace-option small {
220
+ overflow: hidden;
221
+ text-overflow: ellipsis;
222
+ white-space: nowrap;
223
+ }
224
+ .och-workspace-option strong {
225
+ font-size: 13px;
226
+ font-weight: 650;
227
+ }
228
+ .och-workspace-option small {
229
+ font-size: 11px;
230
+ color: var(--och-muted);
231
+ }
232
+ .och-workspace-option > .och-icon {
233
+ color: var(--och-orange-active);
234
+ flex-shrink: 0;
235
+ }
236
+ .och-workspace-empty {
237
+ margin: 0;
238
+ padding: 12px 8px;
239
+ color: var(--och-muted);
240
+ }
241
+
158
242
  /* 模式分段控件:全局唯一模式入口(重语法:ink 实底) */
159
243
  .och-mode {
160
- display: inline-flex;
244
+ --och-mode-item-width: 92px;
245
+ position: relative;
246
+ display: grid;
247
+ grid-template-columns: repeat(3, var(--och-mode-item-width));
161
248
  padding: 3px;
162
249
  border: 1px solid var(--och-hairline);
163
250
  border-radius: 999px;
164
251
  background: var(--och-surface-muted);
165
252
  flex-shrink: 0;
166
253
  }
254
+
255
+ /* The product-mode switch is viewport-centered in Chat, Operate and Inspect;
256
+ * changing the active workspace must never move this global navigation. */
257
+ @media (min-width: 720px) {
258
+ .och-mode {
259
+ position: fixed;
260
+ top: 3px;
261
+ left: 50vw;
262
+ transform: translateX(-50%);
263
+ }
264
+ }
265
+ /* DSH 2026-08-23:共享椭圆滑块,160-220ms 移动到选中椭圆。 */
266
+ .och-mode-thumb {
267
+ position: absolute;
268
+ left: 0;
269
+ top: 0;
270
+ border-radius: 999px;
271
+ background: var(--och-ink);
272
+ box-shadow: 0 2px 8px rgba(38, 37, 30, 0.25);
273
+ transition: none;
274
+ pointer-events: none;
275
+ }
276
+ .och-mode-thumb.is-ready {
277
+ transition:
278
+ transform 190ms cubic-bezier(0.22, 1, 0.36, 1),
279
+ width 190ms cubic-bezier(0.22, 1, 0.36, 1),
280
+ height 190ms cubic-bezier(0.22, 1, 0.36, 1),
281
+ opacity 160ms ease;
282
+ }
167
283
  .och-mode-btn {
168
284
  display: inline-flex;
169
285
  align-items: center;
170
286
  justify-content: center;
171
287
  gap: 6px;
172
- min-width: 92px;
288
+ box-sizing: border-box;
289
+ width: var(--och-mode-item-width);
290
+ min-width: var(--och-mode-item-width);
173
291
  height: 30px;
174
292
  padding: 0 14px;
175
293
  border-radius: 999px;
@@ -190,9 +308,14 @@
190
308
  }
191
309
  .och-mode-btn.och-on,
192
310
  .och-mode-btn.och-on:hover {
193
- background: var(--och-ink);
311
+ /* 滑块提供选中背景;文字反色保持可读。 */
194
312
  color: #fff;
195
- box-shadow: 0 2px 8px rgba(38, 37, 30, 0.25);
313
+ background: transparent;
314
+ box-shadow: none;
315
+ }
316
+ .och-mode-btn {
317
+ position: relative;
318
+ z-index: 1;
196
319
  }
197
320
 
198
321
  /* 对象上下文 chip:跨模式选中连续性 + 显式返回路径 */
@@ -452,9 +575,6 @@
452
575
  .och-repo-fp {
453
576
  display: none;
454
577
  }
455
- .och-mode-btn {
456
- min-width: 0;
457
- }
458
578
  .och-workspace {
459
579
  overflow-x: auto;
460
580
  }
@@ -481,7 +601,7 @@
481
601
 
482
602
  @media (max-width: 760px) {
483
603
  .och-inner {
484
- width: min(100% - 24px, var(--och-shell-max));
604
+ padding-inline: 12px;
485
605
  }
486
606
  .och-global {
487
607
  gap: 6px;
@@ -502,6 +622,9 @@
502
622
  .och-mode-btn {
503
623
  padding-inline: 10px;
504
624
  }
625
+ .och-mode {
626
+ --och-mode-item-width: 78px;
627
+ }
505
628
  .och-tab {
506
629
  padding-inline: 10px;
507
630
  }
@@ -509,6 +632,8 @@
509
632
 
510
633
  @media (prefers-reduced-motion: reduce) {
511
634
  .och-mode-btn,
635
+ .och-mode-thumb,
636
+ .och-mode-thumb.is-ready,
512
637
  .och-tab,
513
638
  .och-repo,
514
639
  .och-pi {
@@ -25,20 +25,33 @@ export type ChromeRepoInfo = {
25
25
  inspectReady?: boolean;
26
26
  };
27
27
 
28
+ export type ChromeWorkspaceSwitcher = {
29
+ activeId: string | null;
30
+ loading: boolean;
31
+ items: Array<{
32
+ workspaceId: string;
33
+ name: string;
34
+ displayPath: string;
35
+ }>;
36
+ };
37
+
28
38
  export type ChromeModel = {
29
- mode: "operate" | "inspect";
39
+ mode: "chat" | "operate" | "inspect";
30
40
  homeHref: string;
31
41
  brand: string;
32
42
  repo: ChromeRepoInfo | null;
43
+ workspaceSwitcher: ChromeWorkspaceSwitcher | null;
33
44
  modeButtons: Array<{
34
- id: "operate" | "inspect";
45
+ id: "chat" | "operate" | "inspect";
35
46
  label: string;
36
47
  href: string;
37
48
  active: boolean;
38
49
  }>;
39
50
  context: InspectContext | null;
40
51
  freshness: boolean;
52
+ statusVisible: boolean;
41
53
  pi: { state: string; label: string; href: string; title: string };
54
+ workspaceTabsVisible: boolean;
42
55
  tabs: Array<OperatorChromeTab & { active: boolean }>;
43
56
  tabsAriaLabel: string;
44
57
  };
@@ -63,16 +76,20 @@ export function chromeStateFromHealth(body: unknown): {
63
76
  repo: ChromeRepoInfo;
64
77
  };
65
78
  export function buildOperatorChromeModel(config: {
66
- mode: "operate" | "inspect";
79
+ mode: "chat" | "operate" | "inspect";
67
80
  activeTabHref?: string | null;
81
+ chatHref?: string;
68
82
  operateHref?: string;
69
83
  inspectHref?: string;
70
84
  readinessState?: string;
71
85
  readinessHref?: string;
72
86
  readinessTitle?: string;
73
87
  repo?: ChromeRepoInfo | null;
88
+ workspaceSwitcher?: ChromeWorkspaceSwitcher | null;
89
+ statusVisible?: boolean;
74
90
  freshness?: boolean;
75
91
  context?: InspectContext | null;
92
+ onSelectWorkspace?: (workspaceId: string) => void;
76
93
  }): ChromeModel;
77
94
  export function mountOperatorChrome(
78
95
  root: HTMLElement,