@tea-agent/loop-agent 0.39.3 → 0.40.0-beta.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/CHANGELOG.md +133 -92
- package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
- package/dist/adapters/context-transfer/pi-session.js +61 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +12 -0
- package/dist/cli/program.js +23 -1
- package/dist/commands/dag-artifact.js +284 -0
- package/dist/commands/dag-context.js +184 -0
- package/dist/commands/dag-rerun.js +206 -1
- package/dist/executors/dag-pi-executor.js +108 -19
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +49 -1
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +42 -2
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/{worker → infrastructure}/console/app-data.js +4 -0
- package/dist/infrastructure/console/artifact-revision-store.js +430 -0
- package/dist/infrastructure/console/context-export-store.js +160 -0
- package/dist/infrastructure/console/dir-lock.js +132 -0
- package/dist/infrastructure/harness/artifact-store.js +10 -1
- package/dist/infrastructure/harness/atomic-write.js +35 -2
- package/dist/shared/context-transfer/artifact-revision.js +172 -0
- package/dist/shared/context-transfer.js +418 -0
- package/dist/shared/operator/capabilities.js +14 -0
- package/dist/shared/operator/safe-run-summary.js +1 -0
- package/dist/shared/path-safety.js +93 -0
- package/dist/shared/preview.js +28 -4
- package/dist/task/config-types.js +10 -0
- package/dist/task/source-prepare/fragment-inventory.js +1 -14
- package/dist/worker/console/chat/chat-event-store.js +77 -7
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/resource-preferences-store.js +1 -1
- package/dist/worker/console/chat/routes.js +389 -56
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +94 -64
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +79 -0
- package/dist/worker/console/console-update-runtime.js +1 -1
- package/dist/worker/console/context-transfer-diagnostics.js +198 -0
- package/dist/worker/console/dag-confirmation.js +1 -1
- package/dist/worker/console/dag-execution-receipt.js +1 -1
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/draft-store.js +1 -1
- package/dist/worker/console/human-gate-token.js +1 -1
- package/dist/worker/console/index.js +3 -3
- package/dist/worker/console/interview/assessment.js +1 -1
- package/dist/worker/console/interview/session.js +1 -1
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-runner.js +45 -4
- package/dist/worker/console/operation-sse.js +1 -1
- package/dist/worker/console/operation-wait.js +1 -1
- package/dist/worker/console/operator-actions.js +104 -3
- package/dist/worker/console/operator-surface-health.js +1 -1
- package/dist/worker/console/operator-user-error.js +128 -0
- package/dist/worker/console/routes.js +927 -8
- package/dist/worker/console/security.js +39 -4
- package/dist/worker/console/server.js +532 -187
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CpWvwARc.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DctXgO-g.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-D4mIkoQl.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-ClG0--KO.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-CwdLk78T.js} +1 -1
- package/dist/worker/console/static/assets/channel-C9VQZ-ru.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DKczcu-O.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-BV6F-vBI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtojVnvA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-BjDQRGnO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-I0fV6ngU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-BM8C-mmC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-BEK-a15e.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-CQsg_fSR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-B-BcaUm3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D1Bu08fg.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-q8nwtxBR.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CruhpURw.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-CJne6iYF.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-CbeLvBU5.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-DmCxH1Pe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-DJ5OYhtC.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-B-UN-hln.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-CcEro8Ey.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BBBzDden.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-xKEyH-oo.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-C7xl63VC.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-nthQQ9nR.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CWN5mE33.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-Wn86tQ6N.js} +1 -1
- package/dist/worker/console/static/assets/index-D-0NVyMk.css +1 -0
- package/dist/worker/console/static/assets/index-gwyOxQiS.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-_WQymGtW.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-COBeivBm.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-CwGOJl3Q.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-Ci0CJ5Vi.js} +1 -1
- package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CPjMq6vN.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-Z0wGoe0b.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-dISked0d.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-BJxgof7Y.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-CIblU8eW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-vwYiqh8E.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-_iOsVeFm.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Dk0MLxO_.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-Dx7HtLVW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-DRuWjlpL.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Bhy9DQAA.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-DAx9twnU.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dmdf8cGy.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-okMlvVrM.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-W1IcAFff.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DXqCKe37.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CeIUkwBh.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-BP76BKTN.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-BpVI0OpF.js} +1 -1
- package/dist/worker/console/static/index.html +7 -2
- package/dist/worker/console/static-src/app/console-types.js +29 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
- package/dist/worker/console/workspace-context.js +260 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/continuation/worker-continuation.js +278 -0
- package/dist/worker/observe/health.js +1 -1
- package/dist/worker/observe/node-transparency.js +572 -0
- package/dist/worker/observe/routes.js +51 -1
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +94 -10
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
- package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +175 -1
- package/dist/worker/observe/static/styles.css +1039 -128
- package/dist/worker/observe/static/views/dag-inspector.js +1218 -60
- package/dist/worker/observe/static/views/dag.js +2 -1
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/session-timeline.js +75 -10
- package/dist/worker/observe/static/views/task.js +1 -1
- package/dist/worker/pool/attempt-lease.js +97 -66
- package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
- package/dist/worker/pool/reconcile.js +46 -1
- package/dist/worker/pool/run-store.js +2 -0
- package/dist/worker/pool/state-projection.js +2 -0
- package/dist/workflows/dag/artifact-bindings.js +149 -0
- package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
- package/dist/workflows/dag/backend-test-writer-completeness.js +7 -12
- package/dist/workflows/dag/context-receipt.js +305 -0
- package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
- package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
- package/dist/workflows/dag/context-transfer/renderers.js +93 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +1 -89
- package/dist/workflows/dag/init-hybrid.js +178 -47
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +112 -27
- package/dist/workflows/dag/path-safety.js +1 -0
- package/dist/workflows/dag/prompt.js +29 -4
- package/dist/workflows/dag/rerun-plan.js +272 -0
- package/dist/workflows/dag/rerun-run.js +390 -12
- package/dist/workflows/dag/skill-snapshot.js +1 -0
- package/dist/workflows/dag/types.js +203 -8
- package/docs/README.md +1 -0
- package/docs/architecture/runtime-boundaries.md +6 -6
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/agent-dag.schema.json +50 -0
- package/docs/templates/backend-test-dag.json +265 -237
- package/harness.json +3 -3
- package/package.json +5 -1
- package/skills/loop-agent/references/command-reference.md +6 -0
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
- /package/dist/{worker → infrastructure}/console/operation-store.js +0 -0
- /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, } from "../../chat/turn-process.js";
|
|
3
|
+
import { mergeOperationCardState, } from "../../chat/operation-card.js";
|
|
3
4
|
import { createChatSseEventApplier, } from "./chat-sse-events.js";
|
|
4
5
|
import { taskContextUnchanged } from "./format.js";
|
|
6
|
+
import { upsertCompactionMessage } from "./compaction-message.js";
|
|
5
7
|
const EMPTY_USAGE = {
|
|
6
8
|
inputTokens: 0,
|
|
7
9
|
outputTokens: 0,
|
|
@@ -9,13 +11,105 @@ const EMPTY_USAGE = {
|
|
|
9
11
|
cacheWriteTokens: 0,
|
|
10
12
|
totalTokens: 0,
|
|
11
13
|
};
|
|
14
|
+
const EMPTY_SESSION_STATS = {
|
|
15
|
+
turns: 0,
|
|
16
|
+
steps: 0,
|
|
17
|
+
llmMs: 0,
|
|
18
|
+
toolMs: 0,
|
|
19
|
+
ttftMs: 0,
|
|
20
|
+
ttftSteps: 0,
|
|
21
|
+
decodeMs: 0,
|
|
22
|
+
decodeTokens: 0,
|
|
23
|
+
};
|
|
24
|
+
/** R8 AC-R8-003: project a raw GET /operations/:id fact (the same shape the
|
|
25
|
+
* server persists) into a browser OperationCardState. Field-validated; any
|
|
26
|
+
* malformed fact returns undefined (no fabricated cards). */
|
|
27
|
+
function operationCardFromOperationFact(op) {
|
|
28
|
+
const str = (key) => {
|
|
29
|
+
const value = op[key];
|
|
30
|
+
return typeof value === "string" && value ? value : undefined;
|
|
31
|
+
};
|
|
32
|
+
const operationId = str("operationId");
|
|
33
|
+
const action = str("action");
|
|
34
|
+
const state = str("state");
|
|
35
|
+
const createdAt = str("createdAt");
|
|
36
|
+
if (!operationId || !action || !state || !createdAt)
|
|
37
|
+
return undefined;
|
|
38
|
+
const updatedAt = str("updatedAt") ?? str("finishedAt") ?? str("startedAt") ?? createdAt;
|
|
39
|
+
const dagRunId = str("dagRunId");
|
|
40
|
+
return {
|
|
41
|
+
schemaVersion: 1,
|
|
42
|
+
operationId,
|
|
43
|
+
action,
|
|
44
|
+
state,
|
|
45
|
+
createdAt,
|
|
46
|
+
updatedAt,
|
|
47
|
+
inspectHref: `/inspect/?operationId=${encodeURIComponent(operationId)}#/`,
|
|
48
|
+
...(str("taskId") ? { taskId: str("taskId") } : {}),
|
|
49
|
+
...(dagRunId ? { dagRunId } : {}),
|
|
50
|
+
...(str("errorCode") ? { errorCode: str("errorCode") } : {}),
|
|
51
|
+
activityRef: `operation:${operationId}`,
|
|
52
|
+
previewHash: `${operationId}:${state}:${updatedAt}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Apply one settled operation-link mutation to the visible card projection.
|
|
57
|
+
*
|
|
58
|
+
* The mutation identity is captured before the request starts. Both the
|
|
59
|
+
* session and workspace are fenced before and after the follow-up read so a
|
|
60
|
+
* late response can never leak into a newly selected workspace/session.
|
|
61
|
+
*/
|
|
62
|
+
export async function applyOperationLinkMutationProjection(input) {
|
|
63
|
+
const { identity } = input;
|
|
64
|
+
const stillOwnsProjection = () => {
|
|
65
|
+
const current = input.currentOwner();
|
|
66
|
+
return (current.sessionId === identity.sessionId &&
|
|
67
|
+
current.workspaceId === identity.workspaceId);
|
|
68
|
+
};
|
|
69
|
+
if (!identity.workspaceId || !stillOwnsProjection())
|
|
70
|
+
return false;
|
|
71
|
+
if (identity.action === "remove") {
|
|
72
|
+
input.setOperationCards((current) => current.filter((card) => card.operationId !== identity.operationId));
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const workspaceQuery = new URLSearchParams({
|
|
77
|
+
workspaceId: identity.workspaceId,
|
|
78
|
+
});
|
|
79
|
+
const fetchImpl = input.fetchFn ?? fetch;
|
|
80
|
+
const res = await fetchImpl(`${input.origin}/api/operator/v1/operations/${encodeURIComponent(identity.operationId)}?${workspaceQuery.toString()}`, { credentials: "include" });
|
|
81
|
+
if (!res.ok || !stillOwnsProjection())
|
|
82
|
+
return false;
|
|
83
|
+
const body = (await res.json());
|
|
84
|
+
const operation = body.operation;
|
|
85
|
+
if (body.workspaceId !== identity.workspaceId ||
|
|
86
|
+
!operation ||
|
|
87
|
+
operation.workspaceId !== identity.workspaceId ||
|
|
88
|
+
operation.operationId !== identity.operationId ||
|
|
89
|
+
!stillOwnsProjection())
|
|
90
|
+
return false;
|
|
91
|
+
const card = operationCardFromOperationFact(operation);
|
|
92
|
+
if (!card)
|
|
93
|
+
return false;
|
|
94
|
+
input.setOperationCards((current) => stillOwnsProjection()
|
|
95
|
+
? mergeOperationCardState(current, card)
|
|
96
|
+
: current);
|
|
97
|
+
return stillOwnsProjection();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function mergeCompactionMessages(messages, compactions) {
|
|
104
|
+
return (compactions ?? []).reduce((result, item) => upsertCompactionMessage(result, item), messages);
|
|
105
|
+
}
|
|
12
106
|
/**
|
|
13
107
|
* Thread state + SSE block parsing. The event-application logic itself lives
|
|
14
108
|
* in chat-sse-events.ts (createChatSseEventApplier); this hook owns the state
|
|
15
109
|
* slices it patches and the derived timeline projections.
|
|
16
110
|
*/
|
|
17
111
|
export function useChatThread(params) {
|
|
18
|
-
const { origin, refs, sessionId, setError, onReconcile } = params;
|
|
112
|
+
const { origin, refs, sessionId, workspaceId, setError, onReconcile } = params;
|
|
19
113
|
// setProcessOpen remains on the hook API for session lifecycle close/reset
|
|
20
114
|
// (UI-08); the SSE applier must not open the process panel.
|
|
21
115
|
void params.setProcessOpen;
|
|
@@ -30,12 +124,36 @@ export function useChatThread(params) {
|
|
|
30
124
|
const [taskContext, setTaskContext] = useState(null);
|
|
31
125
|
const [contextLoading, setContextLoading] = useState(false);
|
|
32
126
|
const [usage, setUsage] = useState(EMPTY_USAGE);
|
|
127
|
+
const [sessionStats, setSessionStats] = useState(EMPTY_SESSION_STATS);
|
|
33
128
|
const [streaming, setStreaming] = useState(false);
|
|
34
129
|
const [turnPhase, setTurnPhase] = useState("idle");
|
|
35
130
|
const [deferredTurn, setDeferredTurn] = useState(null);
|
|
131
|
+
const [deferredTurns, setDeferredTurns] = useState([]);
|
|
132
|
+
const [pendingQuestion, setPendingQuestion] = useState(null);
|
|
133
|
+
const [todos, setTodos] = useState([]);
|
|
36
134
|
/** Assistant bubble id for the in-flight turn; only this row shows the streaming cursor. */
|
|
37
135
|
const [streamingAssistantId, setStreamingAssistantId] = useState(null);
|
|
38
136
|
const [showSystemInMessages] = useState(false);
|
|
137
|
+
const refreshSessionTelemetry = useCallback(async () => {
|
|
138
|
+
const targetSessionId = refs.sessionRef.current?.sessionId;
|
|
139
|
+
if (!targetSessionId)
|
|
140
|
+
return;
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(targetSessionId)}`, { credentials: "include" });
|
|
143
|
+
if (!res.ok)
|
|
144
|
+
return;
|
|
145
|
+
const body = (await res.json());
|
|
146
|
+
if (refs.sessionRef.current?.sessionId !== targetSessionId)
|
|
147
|
+
return;
|
|
148
|
+
if (body.sessionStats)
|
|
149
|
+
setSessionStats(body.sessionStats);
|
|
150
|
+
if (body.usage)
|
|
151
|
+
setUsage(body.usage);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Telemetry is supplementary; the normal event stream remains authoritative.
|
|
155
|
+
}
|
|
156
|
+
}, [origin, refs]);
|
|
39
157
|
// All inputs are stable (state setters + the memoized refs bag), so the
|
|
40
158
|
// applier identity is stable across renders.
|
|
41
159
|
const applyChatSseEvent = useMemo(() => createChatSseEventApplier({
|
|
@@ -48,13 +166,17 @@ export function useChatThread(params) {
|
|
|
48
166
|
setStreaming,
|
|
49
167
|
setTurnPhase,
|
|
50
168
|
setDeferredTurn,
|
|
169
|
+
setDeferredTurns,
|
|
170
|
+
setPendingQuestion,
|
|
171
|
+
setTodos,
|
|
51
172
|
setStreamingAssistantId,
|
|
52
173
|
setUnread,
|
|
53
174
|
setOperationCards,
|
|
54
175
|
setUsage,
|
|
176
|
+
onTelemetryRefresh: () => void refreshSessionTelemetry(),
|
|
55
177
|
setMessages,
|
|
56
178
|
setToolCalls,
|
|
57
|
-
}), [refs, onReconcile, setError]);
|
|
179
|
+
}), [refs, onReconcile, setError, refreshSessionTelemetry]);
|
|
58
180
|
/** Parse + apply one raw SSE block (frames separated by \n\n). Returns the
|
|
59
181
|
* eventId if the block carried an `id:` line (for Last-Event-ID tracking). */
|
|
60
182
|
const applySseBlock = useCallback((block, assistantId, options) => {
|
|
@@ -124,6 +246,21 @@ export function useChatThread(params) {
|
|
|
124
246
|
}, 350);
|
|
125
247
|
return () => window.clearTimeout(timer);
|
|
126
248
|
}, [sessionId, operationCards, humanGateCards, interview, loadTaskContext]);
|
|
249
|
+
/** R8 AC-R8-003: deterministic mutation-driven operationCards updates.
|
|
250
|
+
* Binds to the identity captured when the real POST/DELETE was issued:
|
|
251
|
+
* - add → consume server truth for that operation (GET operation) and
|
|
252
|
+
* upsert the card when the captured session still matches.
|
|
253
|
+
* - remove → drop the captured operation's card immediately (the server
|
|
254
|
+
* tombstone guarantees it never resurrects on replay). */
|
|
255
|
+
const applyOperationLinkMutation = useCallback((identity) => applyOperationLinkMutationProjection({
|
|
256
|
+
origin,
|
|
257
|
+
identity,
|
|
258
|
+
currentOwner: () => ({
|
|
259
|
+
sessionId: refs.sessionRef.current?.sessionId,
|
|
260
|
+
workspaceId,
|
|
261
|
+
}),
|
|
262
|
+
setOperationCards,
|
|
263
|
+
}), [origin, refs, workspaceId]);
|
|
127
264
|
const timelineMessages = useMemo(() => messages.filter((m) => m.role !== "system" || showSystemInMessages), [messages, showSystemInMessages]);
|
|
128
265
|
useEffect(() => {
|
|
129
266
|
const onVisibility = () => {
|
|
@@ -193,9 +330,13 @@ export function useChatThread(params) {
|
|
|
193
330
|
setInterview(null);
|
|
194
331
|
setTaskContext(null);
|
|
195
332
|
setUsage(EMPTY_USAGE);
|
|
333
|
+
setSessionStats(EMPTY_SESSION_STATS);
|
|
196
334
|
setStreaming(false);
|
|
197
335
|
setTurnPhase("idle");
|
|
198
336
|
setDeferredTurn(null);
|
|
337
|
+
setDeferredTurns([]);
|
|
338
|
+
setPendingQuestion(null);
|
|
339
|
+
setTodos([]);
|
|
199
340
|
setStreamingAssistantId(null);
|
|
200
341
|
setUnread(0);
|
|
201
342
|
setHistoryLimit(50);
|
|
@@ -203,6 +344,7 @@ export function useChatThread(params) {
|
|
|
203
344
|
}, [refs]);
|
|
204
345
|
/** Adopt a freshly activated session's hydrated thread + streaming flags. */
|
|
205
346
|
const applyActivatedSession = useCallback((next) => {
|
|
347
|
+
const visuallyStopped = next.turnPhase === "stopping";
|
|
206
348
|
setMessages(next.messages);
|
|
207
349
|
setToolCalls(next.toolCalls);
|
|
208
350
|
setOperationCards([]);
|
|
@@ -210,11 +352,17 @@ export function useChatThread(params) {
|
|
|
210
352
|
setArtifactCards([]);
|
|
211
353
|
setTaskContext(null);
|
|
212
354
|
setInterview(null);
|
|
213
|
-
setUsage(EMPTY_USAGE);
|
|
355
|
+
setUsage(next.usage ?? EMPTY_USAGE);
|
|
356
|
+
setSessionStats(next.sessionStats ?? EMPTY_SESSION_STATS);
|
|
214
357
|
refs.usageResponseKeysRef.current = new Set();
|
|
215
|
-
setStreaming(next.streaming);
|
|
216
|
-
setTurnPhase(
|
|
358
|
+
setStreaming(visuallyStopped ? false : next.streaming);
|
|
359
|
+
setTurnPhase(visuallyStopped
|
|
360
|
+
? "idle"
|
|
361
|
+
: (next.turnPhase ?? (next.streaming ? "running" : "idle")));
|
|
217
362
|
setDeferredTurn(next.deferredTurn ?? null);
|
|
363
|
+
setDeferredTurns(next.deferredTurns ?? (next.deferredTurn ? [next.deferredTurn] : []));
|
|
364
|
+
setPendingQuestion(next.pendingQuestion ?? null);
|
|
365
|
+
setTodos(next.todos ?? []);
|
|
218
366
|
setStreamingAssistantId(null);
|
|
219
367
|
setUnread(0);
|
|
220
368
|
setHistoryLimit(50);
|
|
@@ -234,10 +382,21 @@ export function useChatThread(params) {
|
|
|
234
382
|
refs.sessionRef.current?.sessionId !== targetSessionId)
|
|
235
383
|
return false;
|
|
236
384
|
const activeTurnId = body.session.activeTurnId ?? null;
|
|
385
|
+
const visuallyStopped = body.session.turnPhase === "stopping";
|
|
237
386
|
refs.activeTurnIdRef.current = activeTurnId;
|
|
238
|
-
refs.streamingRef.current = Boolean(activeTurnId);
|
|
239
|
-
|
|
387
|
+
refs.streamingRef.current = visuallyStopped ? false : Boolean(activeTurnId);
|
|
388
|
+
setStreaming(visuallyStopped ? false : Boolean(activeTurnId));
|
|
389
|
+
setTurnPhase(visuallyStopped
|
|
390
|
+
? "idle"
|
|
391
|
+
: (body.session.turnPhase ?? (activeTurnId ? "running" : "idle")));
|
|
240
392
|
setDeferredTurn(body.session.deferredTurn ?? null);
|
|
393
|
+
setDeferredTurns(body.session.deferredTurns ?? (body.session.deferredTurn ? [body.session.deferredTurn] : []));
|
|
394
|
+
setPendingQuestion(body.session.pendingQuestion ?? null);
|
|
395
|
+
setTodos(body.session.todos ?? []);
|
|
396
|
+
if (body.sessionStats)
|
|
397
|
+
setSessionStats(body.sessionStats);
|
|
398
|
+
if (body.usage)
|
|
399
|
+
setUsage(body.usage);
|
|
241
400
|
// Resume the events stream from the snapshot tail so the next
|
|
242
401
|
// reconnect never immediately re-expires (AC-003). The refs below
|
|
243
402
|
// are reset so any follow-up tail replay re-applies cleanly.
|
|
@@ -246,7 +405,7 @@ export function useChatThread(params) {
|
|
|
246
405
|
refs.turnAssistantIdsRef.current = new Map();
|
|
247
406
|
refs.sealedAssistantIdsRef.current = new Set();
|
|
248
407
|
const hydrated = hydrateSessionThread(body.session.messages ?? []);
|
|
249
|
-
setMessages(hydrated.messages);
|
|
408
|
+
setMessages(mergeCompactionMessages(hydrated.messages, body.compactions));
|
|
250
409
|
setToolCalls(hydrated.toolCalls);
|
|
251
410
|
setStreamingAssistantId(null);
|
|
252
411
|
return true;
|
|
@@ -262,8 +421,11 @@ export function useChatThread(params) {
|
|
|
262
421
|
setMessages,
|
|
263
422
|
setToolCalls,
|
|
264
423
|
setStreamingAssistantId,
|
|
424
|
+
setStreaming,
|
|
265
425
|
setTurnPhase,
|
|
266
426
|
setDeferredTurn,
|
|
427
|
+
setPendingQuestion,
|
|
428
|
+
setTodos,
|
|
267
429
|
]);
|
|
268
430
|
return {
|
|
269
431
|
messages,
|
|
@@ -281,12 +443,18 @@ export function useChatThread(params) {
|
|
|
281
443
|
taskContext,
|
|
282
444
|
contextLoading,
|
|
283
445
|
usage,
|
|
446
|
+
sessionStats,
|
|
284
447
|
streaming,
|
|
285
448
|
setStreaming,
|
|
286
449
|
turnPhase,
|
|
287
450
|
setTurnPhase,
|
|
288
451
|
deferredTurn,
|
|
289
452
|
setDeferredTurn,
|
|
453
|
+
deferredTurns,
|
|
454
|
+
setDeferredTurns,
|
|
455
|
+
pendingQuestion,
|
|
456
|
+
setPendingQuestion,
|
|
457
|
+
todos,
|
|
290
458
|
streamingAssistantId,
|
|
291
459
|
setStreamingAssistantId,
|
|
292
460
|
visibleMessages,
|
|
@@ -295,6 +463,7 @@ export function useChatThread(params) {
|
|
|
295
463
|
applyChatSseEvent,
|
|
296
464
|
applySseBlock,
|
|
297
465
|
loadTaskContext,
|
|
466
|
+
applyOperationLinkMutation,
|
|
298
467
|
resetForNewSession,
|
|
299
468
|
applyActivatedSession,
|
|
300
469
|
recoverSession,
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
|
|
3
|
-
/**
|
|
3
|
+
/** Details-rail repo tree + file preview tabs state. */
|
|
4
4
|
export function useRepoBrowser(params) {
|
|
5
|
-
const { origin, refs, sessionId, setError } = params;
|
|
5
|
+
const { origin, refs, sessionId, workspaceId, blocked, setError } = params;
|
|
6
6
|
const [tree, setTree] = useState({});
|
|
7
7
|
const [expandedDirs, setExpandedDirs] = useState(new Set());
|
|
8
8
|
const [treeError, setTreeError] = useState(null);
|
|
9
9
|
const [previews, setPreviews] = useState([]);
|
|
10
10
|
const [activePreviewPath, setActivePreviewPath] = useState(null);
|
|
11
11
|
const [previewLoading, setPreviewLoading] = useState(false);
|
|
12
|
+
const workspaceScopeRef = useRef(workspaceId ?? null);
|
|
13
|
+
workspaceScopeRef.current = workspaceId ?? null;
|
|
14
|
+
const autoPreviewedWorkspaceRef = useRef(new Set());
|
|
12
15
|
const expandedDirsRef = useRef(new Set());
|
|
13
16
|
expandedDirsRef.current = expandedDirs;
|
|
17
|
+
const workspaceQuery = workspaceId
|
|
18
|
+
? `&workspaceId=${encodeURIComponent(workspaceId)}`
|
|
19
|
+
: "";
|
|
14
20
|
const loadTree = useCallback(async (directory = "") => {
|
|
21
|
+
if (blocked)
|
|
22
|
+
return; // AC-R5-002: fail-closed, zero repo requests
|
|
23
|
+
const requestWorkspaceId = workspaceId ?? null;
|
|
15
24
|
try {
|
|
16
25
|
setTreeError(null);
|
|
17
|
-
const res = await fetch(`${origin}/api/operator/v1/chat/repo/tree?path=${encodeURIComponent(directory)}`, { credentials: "include" });
|
|
26
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/repo/tree?path=${encodeURIComponent(directory)}${workspaceQuery}`, { credentials: "include" });
|
|
18
27
|
const body = (await res.json());
|
|
28
|
+
if (workspaceScopeRef.current !== requestWorkspaceId)
|
|
29
|
+
return;
|
|
19
30
|
if (!res.ok)
|
|
20
31
|
throw new Error(body.error?.message ?? "无法读取仓库目录");
|
|
21
32
|
setTree((current) => ({
|
|
@@ -24,9 +35,11 @@ export function useRepoBrowser(params) {
|
|
|
24
35
|
}));
|
|
25
36
|
}
|
|
26
37
|
catch (error) {
|
|
38
|
+
if (workspaceScopeRef.current !== requestWorkspaceId)
|
|
39
|
+
return;
|
|
27
40
|
setTreeError(error instanceof Error ? error.message : String(error));
|
|
28
41
|
}
|
|
29
|
-
}, [origin]);
|
|
42
|
+
}, [origin, workspaceQuery, workspaceId, blocked]);
|
|
30
43
|
useEffect(() => {
|
|
31
44
|
void loadTree();
|
|
32
45
|
}, [loadTree]);
|
|
@@ -53,10 +66,15 @@ export function useRepoBrowser(params) {
|
|
|
53
66
|
return () => document.removeEventListener("visibilitychange", onVisibility);
|
|
54
67
|
}, [refreshTree]);
|
|
55
68
|
const openPreview = useCallback(async (file) => {
|
|
69
|
+
if (blocked)
|
|
70
|
+
return; // AC-R5-002: fail-closed
|
|
71
|
+
const requestWorkspaceId = workspaceId ?? null;
|
|
56
72
|
setPreviewLoading(true);
|
|
57
73
|
try {
|
|
58
|
-
const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}`, { credentials: "include" });
|
|
74
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}${workspaceQuery}`, { credentials: "include" });
|
|
59
75
|
const body = (await res.json());
|
|
76
|
+
if (workspaceScopeRef.current !== requestWorkspaceId)
|
|
77
|
+
return;
|
|
60
78
|
if (!res.ok || !body.data)
|
|
61
79
|
throw new Error(body.error?.message ?? "无法预览文件");
|
|
62
80
|
setPreviews((current) => {
|
|
@@ -72,12 +90,48 @@ export function useRepoBrowser(params) {
|
|
|
72
90
|
setActivePreviewPath(body.data.path);
|
|
73
91
|
}
|
|
74
92
|
catch (error) {
|
|
93
|
+
if (workspaceScopeRef.current !== requestWorkspaceId)
|
|
94
|
+
return;
|
|
75
95
|
setError(error instanceof Error ? error.message : String(error));
|
|
76
96
|
}
|
|
77
97
|
finally {
|
|
78
|
-
|
|
98
|
+
if (workspaceScopeRef.current === requestWorkspaceId)
|
|
99
|
+
setPreviewLoading(false);
|
|
79
100
|
}
|
|
80
|
-
}, [origin, refs, setError]);
|
|
101
|
+
}, [origin, workspaceQuery, workspaceId, refs, setError, blocked]);
|
|
102
|
+
// A workspace switch must immediately remove the previous workspace's tree
|
|
103
|
+
// and file contents while the newly scoped requests are in flight.
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
setTree({});
|
|
106
|
+
setExpandedDirs(new Set());
|
|
107
|
+
setTreeError(null);
|
|
108
|
+
setPreviews([]);
|
|
109
|
+
setActivePreviewPath(null);
|
|
110
|
+
setPreviewLoading(false);
|
|
111
|
+
}, [workspaceId]);
|
|
112
|
+
// DSH parity: Details opens as a real tree + preview split. Select a real
|
|
113
|
+
// root file once per workspace (README first), never placeholder content.
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (blocked || activePreviewPath || previews.length > 0)
|
|
116
|
+
return;
|
|
117
|
+
const workspaceKey = workspaceId ?? "__default__";
|
|
118
|
+
if (autoPreviewedWorkspaceRef.current.has(workspaceKey))
|
|
119
|
+
return;
|
|
120
|
+
const rootFiles = (tree[""] ?? []).filter((entry) => entry.kind === "file");
|
|
121
|
+
if (rootFiles.length === 0)
|
|
122
|
+
return;
|
|
123
|
+
const initial = rootFiles.find((entry) => /^readme(?:\.[^.]+)?$/i.test(entry.name)) ??
|
|
124
|
+
rootFiles[0];
|
|
125
|
+
autoPreviewedWorkspaceRef.current.add(workspaceKey);
|
|
126
|
+
void openPreview(initial.path);
|
|
127
|
+
}, [
|
|
128
|
+
activePreviewPath,
|
|
129
|
+
blocked,
|
|
130
|
+
openPreview,
|
|
131
|
+
previews.length,
|
|
132
|
+
tree,
|
|
133
|
+
workspaceId,
|
|
134
|
+
]);
|
|
81
135
|
useEffect(() => {
|
|
82
136
|
if (!sessionId)
|
|
83
137
|
return;
|
|
@@ -95,7 +149,7 @@ export function useRepoBrowser(params) {
|
|
|
95
149
|
if (!paths.length)
|
|
96
150
|
return;
|
|
97
151
|
void Promise.all(paths.map(async (file) => {
|
|
98
|
-
const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}`, { credentials: "include" });
|
|
152
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}${workspaceQuery}`, { credentials: "include" });
|
|
99
153
|
if (!res.ok)
|
|
100
154
|
return undefined;
|
|
101
155
|
return (await res.json()).data;
|
|
@@ -106,7 +160,7 @@ export function useRepoBrowser(params) {
|
|
|
106
160
|
setPreviews(available);
|
|
107
161
|
setActivePreviewPath(available.at(-1)?.path ?? null);
|
|
108
162
|
});
|
|
109
|
-
}, [origin, sessionId, refs]);
|
|
163
|
+
}, [origin, workspaceQuery, sessionId, refs]);
|
|
110
164
|
const toggleDir = useCallback((dir) => {
|
|
111
165
|
setExpandedDirs((current) => {
|
|
112
166
|
const next = new Set(current);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/** Workspace layout modes resolved from measured Chat container width. */
|
|
2
2
|
/** Width thresholds in CSS px against `.oc-workspace` (not window/UA). */
|
|
3
3
|
export const WORKSPACE_LAYOUT_THRESHOLDS = {
|
|
4
|
-
comfortableMin:
|
|
5
|
-
compactMin:
|
|
4
|
+
comfortableMin: 1200,
|
|
5
|
+
compactMin: 720,
|
|
6
6
|
};
|
|
7
7
|
/**
|
|
8
8
|
* Resolve the three-band workspace mode from a measured container width.
|
|
9
|
-
* Boundaries: comfortable >=
|
|
9
|
+
* Boundaries: comfortable >=1200, compact 720-1199, narrow <720.
|
|
10
10
|
*/
|
|
11
11
|
export function resolveWorkspaceLayoutMode(widthPx) {
|
|
12
12
|
if (!Number.isFinite(widthPx) || widthPx < 0)
|