@tea-agent/loop-agent 0.39.0-next.3 → 0.39.0-next.30
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/AGENTS.md +4 -1
- package/CHANGELOG.md +267 -65
- package/README.md +11 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/application/task-lifecycle/advance.js +4 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +428 -27
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +118 -14
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +8 -1
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +533 -29
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/operator/capabilities.js +181 -27
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +22 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/project.js +5 -0
- package/dist/task/contract/schema.js +6 -1
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/mcp-inventory.js +221 -0
- package/dist/worker/console/chat/pi-runtime.js +165 -83
- package/dist/worker/console/chat/repo-walk.js +10 -1
- package/dist/worker/console/chat/routes.js +478 -55
- package/dist/worker/console/chat/semantic-activity.js +20 -8
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/chat/turn-process.js +17 -30
- package/dist/worker/console/harness-pi-model-matrix.js +137 -0
- package/dist/worker/console/operator-actions.js +244 -4
- package/dist/worker/console/operator-user-error.js +19 -0
- package/dist/worker/console/pi-readiness.js +12 -5
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +83 -10
- package/dist/worker/console/server.js +42 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-CiGOV5eG.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-Ds9Gyfd8.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-C6V6uC98.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-DezF7Ol9.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-BCWk3gCT.js} +1 -1
- package/dist/worker/console/static/assets/channel-DMvu1nmi.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-DbYr-Cxy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-XSlusW4G.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-BtcKAj9i.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BF57lhXj.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-DJIlR25o.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-DklNc7dm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-fZNtEdQZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-BBgLqV4h.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-Dl9e5n_J.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-B0JK51NT.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVzM-02m.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVzM-02m.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-Ch-ajv4O.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-xn_NxcfB.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DRD0fyga.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-COtnKp4B.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-BQp_CAqG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-x0y-vNgq.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-N2ezwL31.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-CSaxRH6Q.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-6Ro7Kpbd.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-CrIIQEAI.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-DivKcq8g.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-BXrfDo8y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-LTTQmMRJ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-DP3o_5qd.js} +1 -1
- package/dist/worker/console/static/assets/index-C2jmYBfu.js +404 -0
- package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-wVfSY1WH.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-BRKfFL0I.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DEml3jLb.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-DTzBmOqN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-Cpy1d873.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-C1cSavOy.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-BEBSKvYM.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-BrLs4Oac.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-B47AZcLa.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-s0dVeYR2.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-lW8aJ7Q6.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-BcHJPf6Q.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-DdegpUUf.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-BxFWqNv8.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-cOdUDPbR.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-DpxlYl8P.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BwecGAkM.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DQOfurE0.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D5WW_LbN.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-CIp3tC6V.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-C-adIPKn.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-CiYrar9M.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-WHrdTV0-.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +39 -0
- package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
- package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
- package/dist/workflows/dag/backend-test-intake-context.js +32 -0
- package/dist/workflows/dag/backend-test-layout.js +133 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
- package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +3 -1
- package/dist/workflows/dag/convergence/controller.js +141 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +493 -2
- package/dist/workflows/dag/frontend-plan-render.js +90 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +221 -31
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +10 -18
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/init-hybrid.js +805 -113
- package/dist/workflows/dag/node-execution.js +194 -6
- package/dist/workflows/dag/prompt.js +60 -1
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +224 -27
- package/dist/workflows/dag/rerun-run.js +61 -6
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +113 -0
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +157 -1
- package/dist/workflows/dag/validate.js +41 -1
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +1 -5
- package/docs/skills/vetted-skill-registry.md +14 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/frontend-implementation-contract.schema.json +9 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -1
- package/package.json +12 -4
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/analyze-product-requirements/SKILL.md +8 -5
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
- package/skills/analyze-product-requirements/references/example.md +50 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
- package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/frontend-design-review/references/review-checklist.md +2 -0
- package/skills/frontend-implementation/references/design-spec.md +16 -19
- package/skills/frontend-implementation/references/node-contracts.md +9 -6
- package/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/improve-codebase-architecture/deepening.md +37 -0
- package/skills/improve-codebase-architecture/html-report.md +123 -0
- package/skills/improve-codebase-architecture/interface-design.md +44 -0
- package/skills/improve-codebase-architecture/language.md +53 -0
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
- package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect } from "react";
|
|
2
2
|
import { formatOperatorUserError } from "../../operator-user-error.js";
|
|
3
3
|
import { confirmationToken, requestId, sleep } from "./format.js";
|
|
4
|
-
import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
|
|
5
|
-
import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, } from "./turn-stream-controller.js";
|
|
4
|
+
import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, decideSubmissionRoute, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcileDeferredSubmissionFromState, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
|
|
5
|
+
import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, } from "./turn-stream-controller.js";
|
|
6
6
|
/** Project ChatRefs into the executable ownership bag used by the controller. */
|
|
7
7
|
function ownershipBagFromRefs(refs) {
|
|
8
8
|
return {
|
|
@@ -57,7 +57,7 @@ function clearEmptyAssistant(setMessages, assistantId) {
|
|
|
57
57
|
* detached Turn resource only; it never opens a second inline SSE consumer. */
|
|
58
58
|
export function useChatStream(params) {
|
|
59
59
|
const { origin, refs, session, thread, composer, setError, setAutoScroll, setNotice, recoveryVersion = 0, watchSessionTitle, } = params;
|
|
60
|
-
const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, } = thread;
|
|
60
|
+
const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, } = thread;
|
|
61
61
|
const { setInput, setPendingImages } = composer;
|
|
62
62
|
const showNotice = useCallback((message) => {
|
|
63
63
|
if (!setNotice)
|
|
@@ -78,9 +78,10 @@ export function useChatStream(params) {
|
|
|
78
78
|
refs.assistantIdRef.current = null;
|
|
79
79
|
refs.stoppingTurnIdRef.current = null;
|
|
80
80
|
setStreaming(false);
|
|
81
|
+
setTurnPhase("idle");
|
|
81
82
|
setStreamingAssistantId(null);
|
|
82
83
|
clearEmptyAssistant(setMessages, assistantId);
|
|
83
|
-
}, [refs, setStreaming, setStreamingAssistantId, setMessages]);
|
|
84
|
+
}, [refs, setStreaming, setTurnPhase, setStreamingAssistantId, setMessages]);
|
|
84
85
|
/** Follow one session's event stream until it is explicitly aborted. The
|
|
85
86
|
* server's `stream_ready` marker separates replay from live events; this
|
|
86
87
|
* means replay cannot render duplicate messages while live text remains
|
|
@@ -242,9 +243,13 @@ export function useChatStream(params) {
|
|
|
242
243
|
/** Issue real POST /abort for a known turnId (Stop never disposes Session). */
|
|
243
244
|
const postAbortForTurn = useCallback(async (sessionId, generation, turnId) => {
|
|
244
245
|
refs.stoppingTurnIdRef.current = turnId;
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
246
|
+
// Optimistic visual stop: remove the running spinner immediately, but
|
|
247
|
+
// retain ownership as `stopping` until the server proves a terminal state.
|
|
248
|
+
// This keeps the composer responsive without falsely advertising idle.
|
|
249
|
+
refs.streamingRef.current = false;
|
|
250
|
+
setStreaming(false);
|
|
251
|
+
setTurnPhase("stopping");
|
|
252
|
+
setStreamingAssistantId(null);
|
|
248
253
|
try {
|
|
249
254
|
const res = await fetch(operatorChatAbortUrl(origin, sessionId, turnId), {
|
|
250
255
|
method: "POST",
|
|
@@ -278,6 +283,7 @@ export function useChatStream(params) {
|
|
|
278
283
|
markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
|
|
279
284
|
});
|
|
280
285
|
applyOwnershipBagToRefs(refs, bag);
|
|
286
|
+
setTurnPhase(turnPhaseFromOwnership(bag));
|
|
281
287
|
return;
|
|
282
288
|
}
|
|
283
289
|
applyAbortDecision(bag, decision, noticeSink(), {
|
|
@@ -285,6 +291,11 @@ export function useChatStream(params) {
|
|
|
285
291
|
markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
|
|
286
292
|
});
|
|
287
293
|
applyOwnershipBagToRefs(refs, bag);
|
|
294
|
+
setTurnPhase(turnPhaseFromOwnership(bag));
|
|
295
|
+
// Sync the post-decision busy state back to React: optimistic stop
|
|
296
|
+
// flipped it idle up front; pending/keep-busy outcomes restore it.
|
|
297
|
+
if (bag.streaming)
|
|
298
|
+
setStreaming(true);
|
|
288
299
|
}
|
|
289
300
|
catch {
|
|
290
301
|
// Abort network failure: keep busy (UIS-abort-network-fail).
|
|
@@ -297,11 +308,17 @@ export function useChatStream(params) {
|
|
|
297
308
|
networkError: true,
|
|
298
309
|
}), noticeSink());
|
|
299
310
|
applyOwnershipBagToRefs(refs, bag);
|
|
311
|
+
// Network failure: the optimistic idle was premature — restore busy
|
|
312
|
+
// so the turn is not silently dropped; /state reconcile converges.
|
|
313
|
+
refs.streamingRef.current = true;
|
|
314
|
+
setStreaming(true);
|
|
315
|
+
setTurnPhase("stopping");
|
|
300
316
|
}
|
|
301
317
|
}, [
|
|
302
318
|
origin,
|
|
303
319
|
refs,
|
|
304
320
|
setStreaming,
|
|
321
|
+
setTurnPhase,
|
|
305
322
|
markIdleIfCurrent,
|
|
306
323
|
fetchSessionState,
|
|
307
324
|
noticeSink,
|
|
@@ -351,10 +368,12 @@ export function useChatStream(params) {
|
|
|
351
368
|
},
|
|
352
369
|
});
|
|
353
370
|
applyOwnershipBagToRefs(refs, bag);
|
|
371
|
+
setTurnPhase(turnPhaseFromOwnership(bag));
|
|
354
372
|
}, [
|
|
355
373
|
refs,
|
|
356
374
|
noticeSink,
|
|
357
375
|
setStreaming,
|
|
376
|
+
setTurnPhase,
|
|
358
377
|
setStreamingAssistantId,
|
|
359
378
|
setPendingImages,
|
|
360
379
|
watchSessionTitle,
|
|
@@ -363,20 +382,199 @@ export function useChatStream(params) {
|
|
|
363
382
|
setError,
|
|
364
383
|
postAbortForTurn,
|
|
365
384
|
]);
|
|
385
|
+
const queueDeferredTurn = useCallback(async (input) => {
|
|
386
|
+
const activeSession = refs.sessionRef.current;
|
|
387
|
+
if (!activeSession || (!input.text.trim() && input.images.length === 0))
|
|
388
|
+
return { action: "handled" };
|
|
389
|
+
const existing = thread.deferredTurn;
|
|
390
|
+
showNotice("正在排队…");
|
|
391
|
+
const acceptDeferred = (deferredTurn) => {
|
|
392
|
+
setDeferredTurn(deferredTurn);
|
|
393
|
+
setInput("");
|
|
394
|
+
setPendingImages([]);
|
|
395
|
+
setError(null);
|
|
396
|
+
showNotice(existing ? "已更新停止后发送的消息" : "已排队,将在上一轮停止后自动发送");
|
|
397
|
+
};
|
|
398
|
+
const acceptDispatched = () => {
|
|
399
|
+
setDeferredTurn(null);
|
|
400
|
+
setInput("");
|
|
401
|
+
setPendingImages([]);
|
|
402
|
+
setError(null);
|
|
403
|
+
showNotice("消息已进入发送");
|
|
404
|
+
};
|
|
405
|
+
const putOnce = async () => {
|
|
406
|
+
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn`, {
|
|
407
|
+
method: "PUT",
|
|
408
|
+
headers: {
|
|
409
|
+
"Content-Type": "application/json",
|
|
410
|
+
"x-loop-console-confirmation": confirmationToken(),
|
|
411
|
+
},
|
|
412
|
+
credentials: "include",
|
|
413
|
+
body: JSON.stringify({
|
|
414
|
+
clientRequestId: input.clientRequestId,
|
|
415
|
+
blockedByTurnId: input.blockedByTurnId,
|
|
416
|
+
text: input.text,
|
|
417
|
+
images: input.images,
|
|
418
|
+
...(existing ? { expectedRevision: existing.revision } : {}),
|
|
419
|
+
}),
|
|
420
|
+
});
|
|
421
|
+
const body = (await response.json().catch(() => ({})));
|
|
422
|
+
return { response, body };
|
|
423
|
+
};
|
|
424
|
+
try {
|
|
425
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
426
|
+
const { response, body } = await putOnce();
|
|
427
|
+
if (response.ok && body.accepted && body.deferredTurn) {
|
|
428
|
+
acceptDeferred(body.deferredTurn);
|
|
429
|
+
return { action: "handled" };
|
|
430
|
+
}
|
|
431
|
+
if (body.deferredTurn)
|
|
432
|
+
setDeferredTurn(body.deferredTurn);
|
|
433
|
+
if (body.error?.code !== "TURN_NOT_STOPPING") {
|
|
434
|
+
setError(body.error?.message ??
|
|
435
|
+
`待发送消息未被服务端接受(HTTP ${response.status})`);
|
|
436
|
+
return { action: "handled" };
|
|
437
|
+
}
|
|
438
|
+
const canonical = await fetchSessionState(activeSession.sessionId);
|
|
439
|
+
const decision = reconcileDeferredSubmissionFromState({
|
|
440
|
+
clientRequestId: input.clientRequestId,
|
|
441
|
+
blockedByTurnId: input.blockedByTurnId,
|
|
442
|
+
state: canonical ?? undefined,
|
|
443
|
+
});
|
|
444
|
+
if (decision.action === "deferred") {
|
|
445
|
+
acceptDeferred(decision.deferredTurn);
|
|
446
|
+
return { action: "handled" };
|
|
447
|
+
}
|
|
448
|
+
if (decision.action === "accepted") {
|
|
449
|
+
acceptDispatched();
|
|
450
|
+
return { action: "handled" };
|
|
451
|
+
}
|
|
452
|
+
if (decision.action === "retry-deferred" && attempt === 0)
|
|
453
|
+
continue;
|
|
454
|
+
if (decision.action === "send-now" && !existing) {
|
|
455
|
+
showNotice("上一轮已停止,正在发送当前消息…");
|
|
456
|
+
return { action: "send-now" };
|
|
457
|
+
}
|
|
458
|
+
setError(body.error?.message ?? "停止状态已变化,当前消息仍保留在输入框");
|
|
459
|
+
return { action: "handled" };
|
|
460
|
+
}
|
|
461
|
+
return { action: "handled" };
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
const canonical = await fetchSessionState(activeSession.sessionId);
|
|
465
|
+
const decision = reconcileDeferredSubmissionFromState({
|
|
466
|
+
clientRequestId: input.clientRequestId,
|
|
467
|
+
blockedByTurnId: input.blockedByTurnId,
|
|
468
|
+
state: canonical ?? undefined,
|
|
469
|
+
});
|
|
470
|
+
if (decision.action === "deferred") {
|
|
471
|
+
acceptDeferred(decision.deferredTurn);
|
|
472
|
+
return { action: "handled" };
|
|
473
|
+
}
|
|
474
|
+
if (decision.action === "accepted") {
|
|
475
|
+
acceptDispatched();
|
|
476
|
+
return { action: "handled" };
|
|
477
|
+
}
|
|
478
|
+
if (decision.action === "send-now" && !existing) {
|
|
479
|
+
showNotice("上一轮已停止,正在发送当前消息…");
|
|
480
|
+
return { action: "send-now" };
|
|
481
|
+
}
|
|
482
|
+
setError(error instanceof Error
|
|
483
|
+
? `无法确认消息是否已排队:${error.message}`
|
|
484
|
+
: "无法确认消息是否已排队,请刷新后查看服务端状态");
|
|
485
|
+
return { action: "handled" };
|
|
486
|
+
}
|
|
487
|
+
}, [
|
|
488
|
+
origin,
|
|
489
|
+
refs,
|
|
490
|
+
thread.deferredTurn,
|
|
491
|
+
setDeferredTurn,
|
|
492
|
+
setInput,
|
|
493
|
+
setPendingImages,
|
|
494
|
+
setError,
|
|
495
|
+
showNotice,
|
|
496
|
+
fetchSessionState,
|
|
497
|
+
]);
|
|
498
|
+
const cancelDeferredTurn = useCallback(async () => {
|
|
499
|
+
const activeSession = refs.sessionRef.current;
|
|
500
|
+
const deferred = thread.deferredTurn;
|
|
501
|
+
if (!activeSession || !deferred)
|
|
502
|
+
return;
|
|
503
|
+
try {
|
|
504
|
+
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn`, {
|
|
505
|
+
method: "DELETE",
|
|
506
|
+
headers: {
|
|
507
|
+
"Content-Type": "application/json",
|
|
508
|
+
"x-loop-console-confirmation": confirmationToken(),
|
|
509
|
+
},
|
|
510
|
+
credentials: "include",
|
|
511
|
+
body: JSON.stringify({
|
|
512
|
+
clientRequestId: deferred.clientRequestId,
|
|
513
|
+
expectedRevision: deferred.revision,
|
|
514
|
+
}),
|
|
515
|
+
});
|
|
516
|
+
const body = (await response.json().catch(() => ({})));
|
|
517
|
+
if (!response.ok || !body.cancelled) {
|
|
518
|
+
setError(body.error?.message ?? "待发送消息撤回失败");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
setDeferredTurn(null);
|
|
522
|
+
setInput(body.deferredTurn?.text ?? "");
|
|
523
|
+
setPendingImages(body.deferredTurn?.images ?? []);
|
|
524
|
+
setError(null);
|
|
525
|
+
showNotice("待发送消息已移回输入框");
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
529
|
+
}
|
|
530
|
+
}, [
|
|
531
|
+
origin,
|
|
532
|
+
refs,
|
|
533
|
+
thread.deferredTurn,
|
|
534
|
+
setDeferredTurn,
|
|
535
|
+
setInput,
|
|
536
|
+
setPendingImages,
|
|
537
|
+
setError,
|
|
538
|
+
showNotice,
|
|
539
|
+
]);
|
|
366
540
|
const sendPrompt = useCallback(async (text, images) => {
|
|
541
|
+
const initialSession = refs.sessionRef.current;
|
|
542
|
+
if (!initialSession || (!text.trim() && images.length === 0))
|
|
543
|
+
return;
|
|
544
|
+
const ownership = ownershipBagFromRefs(refs);
|
|
545
|
+
const route = decideSubmissionRoute({
|
|
546
|
+
turnPhase: turnPhaseFromOwnership(ownership),
|
|
547
|
+
blockedByTurnId: ownership.stoppingTurnId ?? ownership.activeTurnId,
|
|
548
|
+
});
|
|
549
|
+
const clientRequestId = requestId("chat-submit");
|
|
550
|
+
if (route.action === "hold") {
|
|
551
|
+
if (turnPhaseFromOwnership(ownership) === "stopping")
|
|
552
|
+
showNotice("正在确认上一轮停止状态,当前消息已保留");
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (route.action === "queue-deferred") {
|
|
556
|
+
const deferred = await queueDeferredTurn({
|
|
557
|
+
text,
|
|
558
|
+
images,
|
|
559
|
+
clientRequestId,
|
|
560
|
+
blockedByTurnId: route.blockedByTurnId,
|
|
561
|
+
});
|
|
562
|
+
if (deferred.action !== "send-now")
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
367
565
|
const activeSession = refs.sessionRef.current;
|
|
368
566
|
const generation = refs.sessionGenerationRef.current;
|
|
369
567
|
if (!activeSession ||
|
|
568
|
+
activeSession.sessionId !== initialSession.sessionId ||
|
|
370
569
|
(!text.trim() && images.length === 0) ||
|
|
371
570
|
refs.streamingRef.current ||
|
|
372
571
|
refs.submitInFlightRef.current ||
|
|
373
572
|
// AC-R5-002: ownership lease blocks send until terminal release.
|
|
374
|
-
|
|
573
|
+
refs.pendingSubmissionRef.current)
|
|
375
574
|
return;
|
|
376
575
|
// AC-001: acquire synchronous submit ownership before the first await.
|
|
377
576
|
refs.submitInFlightRef.current = true;
|
|
378
577
|
const submitGeneration = ++refs.submitGenerationRef.current;
|
|
379
|
-
const clientRequestId = requestId("chat-submit");
|
|
380
578
|
const userMessageId = requestId("u");
|
|
381
579
|
const assistantId = requestId("a");
|
|
382
580
|
const draftText = text.trim();
|
|
@@ -424,6 +622,7 @@ export function useChatStream(params) {
|
|
|
424
622
|
setAutoScroll(true);
|
|
425
623
|
setError(null);
|
|
426
624
|
refs.streamingRef.current = true;
|
|
625
|
+
setTurnPhase("running");
|
|
427
626
|
refs.assistantIdRef.current = assistantId;
|
|
428
627
|
setStreaming(true);
|
|
429
628
|
setStreamingAssistantId(assistantId);
|
|
@@ -519,7 +718,9 @@ export function useChatStream(params) {
|
|
|
519
718
|
setAutoScroll,
|
|
520
719
|
setError,
|
|
521
720
|
setStreaming,
|
|
721
|
+
setTurnPhase,
|
|
522
722
|
setStreamingAssistantId,
|
|
723
|
+
queueDeferredTurn,
|
|
523
724
|
]);
|
|
524
725
|
const handleStop = useCallback(async () => {
|
|
525
726
|
// AC-R5-001: durable stopRequested is set synchronously before any await so
|
|
@@ -535,6 +736,7 @@ export function useChatStream(params) {
|
|
|
535
736
|
notice: noticeSink(),
|
|
536
737
|
});
|
|
537
738
|
applyOwnershipBagToRefs(refs, bag);
|
|
739
|
+
setTurnPhase("stopping");
|
|
538
740
|
// Immediate abort already scheduled for a known turnId — coalesce via
|
|
539
741
|
// abortPostedTurnIds; stopInFlight only gates concurrent in-flight POSTs.
|
|
540
742
|
if (stopResult.abortedTurnId) {
|
|
@@ -643,6 +845,9 @@ export function useChatStream(params) {
|
|
|
643
845
|
},
|
|
644
846
|
});
|
|
645
847
|
applyOwnershipBagToRefs(refs, bag);
|
|
848
|
+
setTurnPhase(turnPhaseFromOwnership(bag));
|
|
849
|
+
if (state && "deferredTurn" in state)
|
|
850
|
+
setDeferredTurn(state.deferredTurn ?? null);
|
|
646
851
|
if (result === "bound") {
|
|
647
852
|
// US-BUSY-ACTIVE-BOUND: keep streaming/busy with same id.
|
|
648
853
|
setStreaming(true);
|
|
@@ -742,6 +947,7 @@ export function useChatStream(params) {
|
|
|
742
947
|
refs,
|
|
743
948
|
session,
|
|
744
949
|
streaming,
|
|
950
|
+
setTurnPhase,
|
|
745
951
|
markIdleIfCurrent,
|
|
746
952
|
releaseSubmitOwnership,
|
|
747
953
|
postAbortForTurn,
|
|
@@ -763,5 +969,5 @@ export function useChatStream(params) {
|
|
|
763
969
|
}
|
|
764
970
|
};
|
|
765
971
|
}, [session, recoveryVersion, refs, startEventsStream]);
|
|
766
|
-
return { sendPrompt, handleStop, openEventsStream };
|
|
972
|
+
return { sendPrompt, handleStop, cancelDeferredTurn, openEventsStream };
|
|
767
973
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
-
import {
|
|
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 = {
|
|
@@ -32,6 +31,8 @@ export function useChatThread(params) {
|
|
|
32
31
|
const [contextLoading, setContextLoading] = useState(false);
|
|
33
32
|
const [usage, setUsage] = useState(EMPTY_USAGE);
|
|
34
33
|
const [streaming, setStreaming] = useState(false);
|
|
34
|
+
const [turnPhase, setTurnPhase] = useState("idle");
|
|
35
|
+
const [deferredTurn, setDeferredTurn] = useState(null);
|
|
35
36
|
/** Assistant bubble id for the in-flight turn; only this row shows the streaming cursor. */
|
|
36
37
|
const [streamingAssistantId, setStreamingAssistantId] = useState(null);
|
|
37
38
|
const [showSystemInMessages] = useState(false);
|
|
@@ -45,6 +46,8 @@ export function useChatThread(params) {
|
|
|
45
46
|
setHumanGateCards,
|
|
46
47
|
setArtifactCards,
|
|
47
48
|
setStreaming,
|
|
49
|
+
setTurnPhase,
|
|
50
|
+
setDeferredTurn,
|
|
48
51
|
setStreamingAssistantId,
|
|
49
52
|
setUnread,
|
|
50
53
|
setOperationCards,
|
|
@@ -121,9 +124,7 @@ export function useChatThread(params) {
|
|
|
121
124
|
}, 350);
|
|
122
125
|
return () => window.clearTimeout(timer);
|
|
123
126
|
}, [sessionId, operationCards, humanGateCards, interview, loadTaskContext]);
|
|
124
|
-
const
|
|
125
|
-
.filter((m) => m.role !== "system" || showSystemInMessages)
|
|
126
|
-
.slice(-historyLimit), [messages, showSystemInMessages, historyLimit]);
|
|
127
|
+
const timelineMessages = useMemo(() => messages.filter((m) => m.role !== "system" || showSystemInMessages), [messages, showSystemInMessages]);
|
|
127
128
|
useEffect(() => {
|
|
128
129
|
const onVisibility = () => {
|
|
129
130
|
if (!document.hidden)
|
|
@@ -158,12 +159,30 @@ export function useChatThread(params) {
|
|
|
158
159
|
statusKind,
|
|
159
160
|
};
|
|
160
161
|
}, [toolCalls]);
|
|
161
|
-
const turnGroups = useMemo(() =>
|
|
162
|
-
|
|
162
|
+
const turnGroups = useMemo(() => {
|
|
163
|
+
const all = buildTurnProcessGroups({
|
|
164
|
+
messages: timelineMessages,
|
|
165
|
+
toolCalls,
|
|
166
|
+
streaming,
|
|
167
|
+
activeAssistantId: streamingAssistantId,
|
|
168
|
+
});
|
|
169
|
+
return selectRecentTurnGroups(all, historyLimit);
|
|
170
|
+
}, [
|
|
171
|
+
timelineMessages,
|
|
163
172
|
toolCalls,
|
|
164
173
|
streaming,
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
streamingAssistantId,
|
|
175
|
+
historyLimit,
|
|
176
|
+
]);
|
|
177
|
+
const visibleMessages = useMemo(() => {
|
|
178
|
+
const next = [];
|
|
179
|
+
for (const group of turnGroups) {
|
|
180
|
+
if (group.user)
|
|
181
|
+
next.push(group.user);
|
|
182
|
+
next.push(...group.assistants);
|
|
183
|
+
}
|
|
184
|
+
return next;
|
|
185
|
+
}, [turnGroups]);
|
|
167
186
|
/** Reset all session-scoped projections before adopting a fresh session. */
|
|
168
187
|
const resetForNewSession = useCallback(() => {
|
|
169
188
|
setMessages([]);
|
|
@@ -175,6 +194,8 @@ export function useChatThread(params) {
|
|
|
175
194
|
setTaskContext(null);
|
|
176
195
|
setUsage(EMPTY_USAGE);
|
|
177
196
|
setStreaming(false);
|
|
197
|
+
setTurnPhase("idle");
|
|
198
|
+
setDeferredTurn(null);
|
|
178
199
|
setStreamingAssistantId(null);
|
|
179
200
|
setUnread(0);
|
|
180
201
|
setHistoryLimit(50);
|
|
@@ -192,6 +213,8 @@ export function useChatThread(params) {
|
|
|
192
213
|
setUsage(EMPTY_USAGE);
|
|
193
214
|
refs.usageResponseKeysRef.current = new Set();
|
|
194
215
|
setStreaming(next.streaming);
|
|
216
|
+
setTurnPhase(next.turnPhase ?? (next.streaming ? "running" : "idle"));
|
|
217
|
+
setDeferredTurn(next.deferredTurn ?? null);
|
|
195
218
|
setStreamingAssistantId(null);
|
|
196
219
|
setUnread(0);
|
|
197
220
|
setHistoryLimit(50);
|
|
@@ -213,6 +236,8 @@ export function useChatThread(params) {
|
|
|
213
236
|
const activeTurnId = body.session.activeTurnId ?? null;
|
|
214
237
|
refs.activeTurnIdRef.current = activeTurnId;
|
|
215
238
|
refs.streamingRef.current = Boolean(activeTurnId);
|
|
239
|
+
setTurnPhase(body.session.turnPhase ?? (activeTurnId ? "running" : "idle"));
|
|
240
|
+
setDeferredTurn(body.session.deferredTurn ?? null);
|
|
216
241
|
// Resume the events stream from the snapshot tail so the next
|
|
217
242
|
// reconnect never immediately re-expires (AC-003). The refs below
|
|
218
243
|
// are reset so any follow-up tail replay re-applies cleanly.
|
|
@@ -220,18 +245,9 @@ export function useChatThread(params) {
|
|
|
220
245
|
refs.seenEventIdsRef.current = new Set();
|
|
221
246
|
refs.turnAssistantIdsRef.current = new Map();
|
|
222
247
|
refs.sealedAssistantIdsRef.current = new Set();
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
}));
|
|
248
|
+
const hydrated = hydrateSessionThread(body.session.messages ?? []);
|
|
249
|
+
setMessages(hydrated.messages);
|
|
250
|
+
setToolCalls(hydrated.toolCalls);
|
|
235
251
|
setStreamingAssistantId(null);
|
|
236
252
|
return true;
|
|
237
253
|
}
|
|
@@ -240,7 +256,15 @@ export function useChatThread(params) {
|
|
|
240
256
|
// trigger another reconcile and retry (best-effort recovery).
|
|
241
257
|
return false;
|
|
242
258
|
}
|
|
243
|
-
}, [
|
|
259
|
+
}, [
|
|
260
|
+
origin,
|
|
261
|
+
refs,
|
|
262
|
+
setMessages,
|
|
263
|
+
setToolCalls,
|
|
264
|
+
setStreamingAssistantId,
|
|
265
|
+
setTurnPhase,
|
|
266
|
+
setDeferredTurn,
|
|
267
|
+
]);
|
|
244
268
|
return {
|
|
245
269
|
messages,
|
|
246
270
|
setMessages,
|
|
@@ -259,6 +283,10 @@ export function useChatThread(params) {
|
|
|
259
283
|
usage,
|
|
260
284
|
streaming,
|
|
261
285
|
setStreaming,
|
|
286
|
+
turnPhase,
|
|
287
|
+
setTurnPhase,
|
|
288
|
+
deferredTurn,
|
|
289
|
+
setDeferredTurn,
|
|
262
290
|
streamingAssistantId,
|
|
263
291
|
setStreamingAssistantId,
|
|
264
292
|
visibleMessages,
|
|
@@ -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,
|
|
@@ -2,7 +2,8 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
|
|
4
4
|
import { resolveDagNodeSkills } from "../../workflows/dag/skills.js";
|
|
5
|
-
import { resolveDagNodePromptRedacted, resolveDagRunJson, } from "./dag-run-artifacts.js";
|
|
5
|
+
import { resolveDagNodePromptRedacted, resolveDagRunArtifact, resolveDagRunJson, } from "./dag-run-artifacts.js";
|
|
6
|
+
import { namedShellHandlers } from "./shell-handler-keys.js";
|
|
6
7
|
export const NODE_INPUT_SCHEMA_VERSION = 1;
|
|
7
8
|
export const NODE_INPUT_PREVIEW_MAX_BYTES = 64 * 1024;
|
|
8
9
|
const SHELL_GATE_KEYS = [
|
|
@@ -33,6 +34,7 @@ function unavailableBody(dagRunId, nodeId, reason, warnings = []) {
|
|
|
33
34
|
executorInput: null,
|
|
34
35
|
boundaries: null,
|
|
35
36
|
promptFingerprint: null,
|
|
37
|
+
piExtensionRunFacts: null,
|
|
36
38
|
warnings,
|
|
37
39
|
};
|
|
38
40
|
}
|
|
@@ -137,7 +139,7 @@ function resolveModelHint(spec, task) {
|
|
|
137
139
|
}
|
|
138
140
|
return null;
|
|
139
141
|
}
|
|
140
|
-
function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warnings) {
|
|
142
|
+
function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, piExtensionRunFacts, warnings) {
|
|
141
143
|
const defaults = spec.defaults && typeof spec.defaults === "object"
|
|
142
144
|
? spec.defaults
|
|
143
145
|
: undefined;
|
|
@@ -216,6 +218,7 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warni
|
|
|
216
218
|
: null,
|
|
217
219
|
failFast: typeof shellRaw.failFast === "boolean" ? shellRaw.failFast : null,
|
|
218
220
|
gates: shellGateNames(shellRaw),
|
|
221
|
+
handlers: namedShellHandlers(shellRaw),
|
|
219
222
|
}
|
|
220
223
|
: null;
|
|
221
224
|
const staticPreview = staticRaw && typeof staticRaw.resultMarkdown === "string"
|
|
@@ -268,6 +271,7 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warni
|
|
|
268
271
|
kind,
|
|
269
272
|
skills,
|
|
270
273
|
modelHint: resolveModelHint(spec, task),
|
|
274
|
+
piExtensions: redactStringList(task.piExtensions),
|
|
271
275
|
shell,
|
|
272
276
|
static: staticBlock,
|
|
273
277
|
},
|
|
@@ -284,9 +288,72 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warni
|
|
|
284
288
|
: null,
|
|
285
289
|
},
|
|
286
290
|
promptFingerprint: fingerprint,
|
|
291
|
+
piExtensionRunFacts,
|
|
287
292
|
warnings,
|
|
288
293
|
};
|
|
289
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Load run-time Pi extension facts from <node>/pi-extensions.json (plan
|
|
297
|
+
* 2026-08-22 R2). Returns null when the node declares no piExtensions
|
|
298
|
+
* (no expected artifact). Never throws: unreadable/corrupt artifacts degrade
|
|
299
|
+
* to parseError stubs so the node-input endpoint stays 200.
|
|
300
|
+
*/
|
|
301
|
+
async function loadPiExtensionRunFacts(repoRoot, dagRunId, nodeId, declared, warnings) {
|
|
302
|
+
if (declared.length === 0)
|
|
303
|
+
return null;
|
|
304
|
+
const artifact = path.posix.join(nodeId, "pi-extensions.json");
|
|
305
|
+
const resolved = resolveDagRunArtifact(repoRoot, dagRunId, [
|
|
306
|
+
nodeId,
|
|
307
|
+
"pi-extensions.json",
|
|
308
|
+
]);
|
|
309
|
+
if (!resolved.ok) {
|
|
310
|
+
if (resolved.reason === "ambiguous") {
|
|
311
|
+
warnings.push("Ambiguous pi-extensions.json lifecycle copies.");
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
artifact,
|
|
315
|
+
exists: false,
|
|
316
|
+
requested: [...declared],
|
|
317
|
+
resolved: [],
|
|
318
|
+
missing: [],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const empty = {
|
|
322
|
+
artifact,
|
|
323
|
+
exists: false,
|
|
324
|
+
requested: [...declared],
|
|
325
|
+
resolved: [],
|
|
326
|
+
missing: [],
|
|
327
|
+
};
|
|
328
|
+
try {
|
|
329
|
+
const raw = JSON.parse(await readFile(resolved.result.absolutePath, "utf-8"));
|
|
330
|
+
const resolvedList = Array.isArray(raw.resolved) ? raw.resolved : [];
|
|
331
|
+
const missingList = Array.isArray(raw.missing) ? raw.missing : [];
|
|
332
|
+
const projected = resolvedList
|
|
333
|
+
.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry))
|
|
334
|
+
.map((entry) => ({
|
|
335
|
+
id: redactSecrets(String(entry.id ?? "")),
|
|
336
|
+
packageName: redactSecrets(String(entry.packageName ?? "")),
|
|
337
|
+
version: redactSecrets(String(entry.version ?? "")),
|
|
338
|
+
}));
|
|
339
|
+
const missingProjected = missingList
|
|
340
|
+
.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry))
|
|
341
|
+
.map((entry) => ({
|
|
342
|
+
id: redactSecrets(String(entry.id ?? "")),
|
|
343
|
+
reason: redactSecrets(String(entry.reason ?? "")),
|
|
344
|
+
}));
|
|
345
|
+
return {
|
|
346
|
+
artifact,
|
|
347
|
+
exists: true,
|
|
348
|
+
requested: redactStringList(raw.requested ?? declared),
|
|
349
|
+
resolved: projected,
|
|
350
|
+
missing: missingProjected,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return { ...empty, parseError: "read-failed" };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
290
357
|
async function loadPromptFingerprint(repoRoot, dagRunId, nodeId, warnings) {
|
|
291
358
|
const resolved = resolveDagNodePromptRedacted(repoRoot, dagRunId, nodeId);
|
|
292
359
|
if (!resolved.ok) {
|
|
@@ -415,8 +482,12 @@ export async function buildDagNodeInput(repoRoot, dagRunId, nodeId) {
|
|
|
415
482
|
}
|
|
416
483
|
const warnings = [];
|
|
417
484
|
const fingerprint = await loadPromptFingerprint(repoRoot, dagRunId, nodeId, warnings);
|
|
485
|
+
const declaredPiExtensions = Array.isArray(task.piExtensions)
|
|
486
|
+
? task.piExtensions.filter((item) => typeof item === "string")
|
|
487
|
+
: [];
|
|
488
|
+
const piExtensionRunFacts = await loadPiExtensionRunFacts(repoRoot, dagRunId, nodeId, declaredPiExtensions, warnings);
|
|
418
489
|
// Reject unknown nested leakage by projecting only through allowlist helpers.
|
|
419
|
-
const body = projectTask(dagRunId, nodeId, runResolved.result.lifecycle, spec, task, fingerprint, warnings);
|
|
490
|
+
const body = projectTask(dagRunId, nodeId, runResolved.result.lifecycle, spec, task, fingerprint, piExtensionRunFacts, warnings);
|
|
420
491
|
// Defense-in-depth: ensure raw task never sneaks in.
|
|
421
492
|
if ("task" in body) {
|
|
422
493
|
return {
|