@iowarp/clio-coder 0.3.1 → 0.3.3
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 +233 -373
- package/CONTRIBUTING.md +23 -23
- package/README.md +284 -613
- package/dist/{acp-FPR54DGL.js → acp-P2AQILE2.js} +43 -53
- package/dist/{agents-OGPIHPJH.js → agents-72W3BI7I.js} +43 -26
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-IC3K6NIZ.js → auth-5TWEIYDN.js} +20 -12
- package/dist/chunk-2DJ2KNFG.js +2095 -0
- package/dist/{chunk-IS3ONKU3.js → chunk-2IR2NMPA.js} +6 -4
- package/dist/chunk-2SFS6XQE.js +122 -0
- package/dist/{chunk-PV4JUBVJ.js → chunk-2TLUCQVG.js} +40 -21
- package/dist/chunk-2VTFPG5O.js +48 -0
- package/dist/chunk-4BJ5BYCE.js +61 -0
- package/dist/{chunk-474KN5II.js → chunk-4BPJXDWC.js} +111 -181
- package/dist/chunk-4VP4KH3K.js +962 -0
- package/dist/chunk-4XUGQOHA.js +797 -0
- package/dist/chunk-4ZG3XFUR.js +77 -0
- package/dist/chunk-5B2AEOW5.js +5407 -0
- package/dist/{chunk-K2ITRMHZ.js → chunk-5TSRNF4G.js} +6 -138
- package/dist/{chunk-4QKXUHSR.js → chunk-5UFT4SUX.js} +70 -20
- package/dist/{chunk-OLBBMFRD.js → chunk-5UUP6MWO.js} +24 -62
- package/dist/chunk-65DEGPJ6.js +52 -0
- package/dist/chunk-6EJMN2Y3.js +17 -0
- package/dist/chunk-6N5PTWMY.js +136 -0
- package/dist/chunk-6SGHMWE3.js +277 -0
- package/dist/chunk-6XLNIQDB.js +27 -0
- package/dist/chunk-7CR24IG7.js +242 -0
- package/dist/chunk-7MNJORFF.js +22 -0
- package/dist/{chunk-KY56HMHH.js → chunk-A3CYT5EX.js} +125 -31
- package/dist/chunk-AGYYIBLL.js +1069 -0
- package/dist/{chunk-GB6QRBXN.js → chunk-APJ265NV.js} +54 -1187
- package/dist/{chunk-673JJUWJ.js → chunk-BMEMKKIT.js} +2 -2
- package/dist/chunk-CBCAPZAA.js +229 -0
- package/dist/chunk-CMZWFGD2.js +352 -0
- package/dist/chunk-COU2UHX6.js +400 -0
- package/dist/chunk-DSELYM6W.js +1077 -0
- package/dist/chunk-DUYJ5IO6.js +644 -0
- package/dist/chunk-ECH6PKUQ.js +39 -0
- package/dist/chunk-ED4KHGC3.js +143 -0
- package/dist/chunk-EKMEHE4H.js +340 -0
- package/dist/chunk-FCSXB6T2.js +338 -0
- package/dist/chunk-FJ3H4MN5.js +48 -0
- package/dist/{chunk-RPTR2H26.js → chunk-FNTMWMX5.js} +21 -15
- package/dist/chunk-FQ4SKYE4.js +29 -0
- package/dist/chunk-G4BMMOKF.js +182 -0
- package/dist/{chunk-ZPY3JZ5E.js → chunk-GGXXDWE4.js} +183 -1233
- package/dist/chunk-HC4CLZ2Y.js +68 -0
- package/dist/{chunk-LU4TK2PR.js → chunk-HFSBBKSQ.js} +5 -56
- package/dist/{chunk-PIUMUEMV.js → chunk-HKIYEGME.js} +10 -6
- package/dist/chunk-I4HZDVNP.js +73 -0
- package/dist/chunk-IKCO5N3L.js +162 -0
- package/dist/chunk-IR4CFBFN.js +56 -0
- package/dist/{chunk-R5KLMSBV.js → chunk-J5Q24KAG.js} +2 -2
- package/dist/chunk-J7CWMCQD.js +255 -0
- package/dist/{chunk-K5XEMXTI.js → chunk-JVCV3ICN.js} +1 -1
- package/dist/chunk-KZWTDYJF.js +217 -0
- package/dist/chunk-LBMZMYH2.js +285 -0
- package/dist/{chunk-G34LV2PF.js → chunk-LM5TQCJZ.js} +84 -170
- package/dist/chunk-LW6DSM3M.js +5135 -0
- package/dist/chunk-LWLEKMDQ.js +3482 -0
- package/dist/{chunk-H6F6BYOH.js → chunk-LZSJBIVT.js} +7003 -7434
- package/dist/{chunk-HQQID6OA.js → chunk-M6SHUN7Q.js} +5 -5
- package/dist/{chunk-FST4FYJB.js → chunk-MFFY33HR.js} +99 -140
- package/dist/{chunk-BSU2YIWB.js → chunk-MVVUPGPW.js} +131 -136
- package/dist/chunk-OAO4GE4M.js +619 -0
- package/dist/{chunk-Q3RUPKEJ.js → chunk-OC7FIQPC.js} +58 -189
- package/dist/chunk-OKGUZO2U.js +34 -0
- package/dist/{chunk-GAEBEQVI.js → chunk-OOJYHWRB.js} +32 -346
- package/dist/{chunk-Q5WJOSJ7.js → chunk-OQ33BKR3.js} +2 -1
- package/dist/chunk-OQE5J4C6.js +73 -0
- package/dist/{chunk-KKNLWXI6.js → chunk-ORBHGJC5.js} +8 -8
- package/dist/{chunk-MAR7Y6HW.js → chunk-PAJK6MAQ.js} +23 -16
- package/dist/{chunk-M5T5VO65.js → chunk-PIWWS5BL.js} +837 -635
- package/dist/chunk-POHLU5DW.js +1186 -0
- package/dist/chunk-QKMUKYO7.js +4961 -0
- package/dist/chunk-SRDMMSEP.js +16405 -0
- package/dist/chunk-SST6Z5JA.js +80 -0
- package/dist/chunk-STBPMHSX.js +2456 -0
- package/dist/chunk-T6YILFSB.js +80 -0
- package/dist/chunk-TZK7PACC.js +174 -0
- package/dist/chunk-TZTZS7QK.js +227 -0
- package/dist/{chunk-ASND7OZK.js → chunk-UFIIWP2H.js} +13 -13
- package/dist/chunk-UOV2BYIW.js +107 -0
- package/dist/{chunk-PFEFKVGL.js → chunk-V6RTAOC2.js} +13 -11
- package/dist/chunk-VAKQQHWR.js +434 -0
- package/dist/chunk-VG7TBQIY.js +128 -0
- package/dist/chunk-VJWL6YS5.js +244 -0
- package/dist/{chunk-EYOKLTMF.js → chunk-VPAYEGVX.js} +17 -3
- package/dist/chunk-WEH5XRJQ.js +32 -0
- package/dist/chunk-X4RCMKVQ.js +641 -0
- package/dist/{chunk-TEKV33Q5.js → chunk-X6IAEBZR.js} +65 -33
- package/dist/chunk-XBXAASKX.js +18 -0
- package/dist/chunk-XN3L4EYL.js +46 -0
- package/dist/{chunk-RDLVBZEO.js → chunk-YCWGATWI.js} +6 -4
- package/dist/chunk-YHZX5GEU.js +193 -0
- package/dist/chunk-YXLYO42X.js +91 -0
- package/dist/{chunk-NMOX6HFD.js → chunk-ZDOOVTXZ.js} +29 -77
- package/dist/chunk-ZI647VB5.js +37 -0
- package/dist/{chunk-C4PTHK7P.js → chunk-ZWLZP4ZT.js} +5 -5
- package/dist/chunk-ZWMF7253.js +1882 -0
- package/dist/cli/index.js +62 -54
- package/dist/clio-JOU4FXVA.js +25 -0
- package/dist/code-nav-7AX6FYE6.js +600 -0
- package/dist/codewiki/build-worker.js +66 -0
- package/dist/compile-cache-CVJMMODC.js +18 -0
- package/dist/{components-DMAOEKFB.js → components-KELWS457.js} +11 -6
- package/dist/{config-IRUQ7SE4.js → config-XCDVKR23.js} +92 -55
- package/dist/configure-4GAP54ZW.js +42 -0
- package/dist/{context-5RADCKTR.js → context-4UOGGLQ5.js} +71 -35
- package/dist/context-5VKGUVJJ.js +866 -0
- package/dist/{context-3KWFLHJG.js → context-77FM5DV5.js} +15 -13
- package/dist/{context-clear-7TSNPAAI.js → context-clear-XXJRLCJJ.js} +54 -28
- package/dist/{context-index-W4RLWOQH.js → context-index-BZ4UYMTC.js} +30 -24
- package/dist/dispatch-runner-QPRDDBDX.js +1997 -0
- package/dist/{docs-5AWSPS37.js → docs-2C2LTVT2.js} +23 -10
- package/dist/{doctor-UC5NAJYQ.js → doctor-HR46URBJ.js} +27 -17
- package/dist/{eval-U6TJHRLX.js → eval-XSSNATB4.js} +29 -16
- package/dist/{evidence-YEGUW4L3.js → evidence-6HG2PY2B.js} +46 -26
- package/dist/{evolve-TXARCTPG.js → evolve-K7YU3NCY.js} +45 -25
- package/dist/{extensions-OZFJ3A3G.js → extensions-QVDOHDGJ.js} +16 -7
- package/dist/{fleet-6G3DHNYE.js → fleet-VY3HHKN6.js} +163 -54
- package/dist/{fleet-preflight-DSNT37JK.js → fleet-preflight-DDN536IT.js} +7 -4
- package/dist/{init-KZ5QTF6M.js → init-JYGXI3FK.js} +69 -32
- package/dist/{memory-73ESV5YC.js → memory-WFZMGYHX.js} +48 -27
- package/dist/{models-A4PVNWJK.js → models-I5QWSEOM.js} +39 -25
- package/dist/monitor-GE4ID3IA.js +661 -0
- package/dist/{chunk-FCIH3BIZ.js → orchestrator-EM5MC3HM.js} +15979 -12407
- package/dist/{paths-C4H6IV77.js → paths-UXLN5YYZ.js} +10 -5
- package/dist/{preload-6WVMHX3A.js → preload-P6DGH2PZ.js} +2 -2
- package/dist/{reset-BGW6OGMV.js → reset-L2FQEE3E.js} +16 -10
- package/dist/{run-YTPEYQOH.js → run-ZU3QMZPZ.js} +101 -61
- package/dist/{share-YIFFV4NQ.js → share-S5BZQC5I.js} +15 -8
- package/dist/{skills-2V6RA3OQ.js → skills-X5VXCRNQ.js} +34 -14
- package/dist/{skills-eval-S2TVJO4F.js → skills-eval-WKIHWTHR.js} +70 -34
- package/dist/steer-GGWFUJUD.js +77 -0
- package/dist/{targets-TYXLPB23.js → targets-SNCPI2NR.js} +43 -27
- package/dist/terminal-lease-BNAHVHBS.js +395 -0
- package/dist/{trace-GGOJ6Q6Z.js → trace-PNCASAXC.js} +41 -16
- package/dist/{chunk-N6F52NLF.js → tree-sitter-HGKH6LG4.js} +28 -2306
- package/dist/{uninstall-LLLT4F4W.js → uninstall-FZCQCDKC.js} +10 -5
- package/dist/{upgrade-33G2LMM5.js → upgrade-JQHHPQ4K.js} +45 -25
- package/dist/{usage-ZAFSXKKG.js → usage-OR4O5SMZ.js} +62 -31
- package/dist/verify-375KUB3Y.js +716 -0
- package/dist/web-fetch-2YHJ3KTG.js +638 -0
- package/dist/{wiki-generate-NUQCVOQ3.js → wiki-generate-UEXP2ARI.js} +74 -34
- package/dist/worker/entry.js +221 -36
- package/dist/workspace-G4ZWUIPR.js +22 -0
- package/docs/README.md +22 -17
- package/docs/acp.md +168 -16
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +136 -7
- package/docs/artifact-versions.md +1 -1
- package/docs/built-in-agents.md +1 -1
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +114 -71
- package/docs/config-knobs-audit.md +1 -3
- package/docs/configuration-and-targets.md +174 -46
- package/docs/context-engine.md +29 -6
- package/docs/development-pipeline.md +26 -1
- package/docs/dispatch-architecture-rationale.md +1 -1
- package/docs/documentation-coverage.md +2 -2
- package/docs/documentation-guide.md +1 -1
- package/docs/environment-variables.md +13 -5
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +1 -1
- package/docs/evidence-and-memory.md +6 -2
- package/docs/evolution.md +2 -2
- package/docs/exit-codes-and-output.md +15 -9
- package/docs/extensions-and-sharing.md +9 -9
- package/docs/fleet-dispatch.md +7 -5
- package/docs/git-commit-provenance.md +120 -0
- package/docs/glossary.md +1 -1
- package/docs/installation-and-lifecycle.md +34 -27
- package/docs/middleware-and-components.md +1 -1
- package/docs/model-catalog.md +45 -14
- package/docs/observability.md +8 -5
- package/docs/performance-methodology.md +491 -0
- package/docs/pi-boundary.md +72 -0
- package/docs/proactive-memory.md +3 -3
- package/docs/prompt-envelope-and-tools.md +24 -3
- package/docs/provider-adapter-cookbook.md +57 -4
- package/docs/release-cut-checklist.md +129 -115
- package/docs/safety-model.md +9 -5
- package/docs/scientific-validation.md +3 -3
- package/docs/session-lifecycle.md +55 -12
- package/docs/skills-marketplace.md +12 -8
- package/docs/time-conventions.md +1 -1
- package/docs/tool-usage.md +3 -3
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +10 -7
- package/docs/tui-design.md +47 -10
- package/docs/worker-dispatch-mechanics.md +1 -1
- package/package.json +19 -22
- package/skills/coding/ast-grep/SKILL.md +136 -0
- package/skills/coding/ast-grep/evals.md +56 -0
- package/skills/coding/ast-grep/references/rule_reference.md +297 -0
- package/skills/coding/coding-standards/SKILL.md +113 -0
- package/skills/coding/coding-standards/evals.md +34 -0
- package/skills/coding/prototype/SKILL.md +86 -0
- package/skills/coding/prototype/evals.md +42 -0
- package/skills/coding/prototype/references/LOGIC.md +67 -0
- package/skills/coding/prototype/references/UI.md +112 -0
- package/skills/coding/tdd/SKILL.md +101 -0
- package/skills/coding/tdd/evals.md +41 -0
- package/skills/coding/tdd/references/mocking.md +59 -0
- package/skills/coding/tdd/references/tests.md +77 -0
- package/skills/context/context-handoff/SKILL.md +126 -0
- package/skills/context/context-handoff/evals.md +57 -0
- package/skills/context/context-handoff/scripts/new-handoff.sh +26 -0
- package/skills/context/context-prime/SKILL.md +95 -0
- package/skills/context/context-prime/evals.md +54 -0
- package/skills/meta/clio-dev/SKILL.md +91 -0
- package/skills/meta/clio-dev/evals.md +45 -0
- package/skills/meta/clio-test/SKILL.md +130 -0
- package/skills/meta/clio-test/evals.md +43 -0
- package/skills/meta/clio-test/references/harness.md +97 -0
- package/skills/meta/clio-test/references/test-map.md +59 -0
- package/skills/meta/credentials/SKILL.md +125 -0
- package/skills/meta/credentials/evals.md +104 -0
- package/skills/meta/find-skills/SKILL.md +72 -0
- package/skills/meta/find-skills/evals.md +47 -0
- package/skills/meta/herdr/SKILL.md +127 -0
- package/skills/meta/herdr/evals.md +38 -0
- package/skills/meta/skill-craft/SKILL.md +102 -0
- package/skills/meta/skill-craft/evals.md +41 -0
- package/skills/planning/architecture/SKILL.md +129 -0
- package/skills/planning/architecture/evals.md +36 -0
- package/skills/planning/backlog/SKILL.md +90 -0
- package/skills/planning/backlog/evals.md +43 -0
- package/skills/planning/prd/SKILL.md +82 -0
- package/skills/planning/prd/evals.md +49 -0
- package/skills/planning/product-intent/SKILL.md +112 -0
- package/skills/planning/product-intent/evals.md +36 -0
- package/skills/planning/tech-spec/SKILL.md +115 -0
- package/skills/planning/tech-spec/evals.md +47 -0
- package/skills/registry.yaml +136 -0
- package/skills/research/arxiv-literature/SKILL.md +104 -0
- package/skills/research/arxiv-literature/evals.md +58 -0
- package/skills/research/experiment-protocol/SKILL.md +122 -0
- package/skills/research/experiment-protocol/evals.md +91 -0
- package/skills/research/scientific-debugging/SKILL.md +119 -0
- package/skills/research/scientific-debugging/evals.md +138 -0
- package/skills/research/scientific-modernization/SKILL.md +138 -0
- package/skills/research/scientific-modernization/evals.md +84 -0
- package/skills/workflow/design-council/SKILL.md +139 -0
- package/skills/workflow/design-council/evals.md +97 -0
- package/skills/workflow/grill-me/SKILL.md +186 -0
- package/skills/workflow/grill-me/evals.md +78 -0
- package/skills/workflow/workflow-distiller/SKILL.md +136 -0
- package/skills/workflow/workflow-distiller/evals.md +107 -0
- package/src/cli/acp.ts +31 -4
- package/src/cli/clio.ts +68 -6
- package/src/cli/config-inspect.ts +28 -22
- package/src/cli/configure.ts +47 -9
- package/src/cli/context-clear.ts +2 -2
- package/src/cli/context-index.ts +21 -23
- package/src/cli/context.ts +13 -8
- package/src/cli/default-target.ts +9 -17
- package/src/cli/docs.ts +11 -5
- package/src/cli/evidence.ts +4 -1
- package/src/cli/extensions.ts +10 -1
- package/src/cli/fleet.ts +47 -6
- package/src/cli/index.ts +55 -26
- package/src/cli/memory.ts +3 -1
- package/src/cli/models.ts +1 -1
- package/src/cli/modes/json-stream.ts +37 -1
- package/src/cli/modes/print.ts +24 -9
- package/src/cli/run.ts +2 -2
- package/src/cli/skills-eval.ts +23 -8
- package/src/cli/skills.ts +19 -4
- package/src/cli/targets.ts +4 -0
- package/src/cli/text-layout.ts +15 -5
- package/src/cli/trace.ts +62 -14
- package/src/cli/upgrade.ts +18 -2
- package/src/cli/usage.ts +10 -3
- package/src/cli/wiki-generate.ts +2 -1
- package/src/core/agent-environment.ts +7 -0
- package/src/core/bash-exec.ts +72 -1
- package/src/core/boot-trace.ts +9 -4
- package/src/core/bus-events.ts +20 -4
- package/src/core/commit-attribution.ts +157 -0
- package/src/core/compile-cache.ts +159 -0
- package/src/core/config.ts +131 -2
- package/src/core/defaults.ts +39 -5
- package/src/core/domain-loader.ts +12 -5
- package/src/core/git-commit-attribution.ts +387 -0
- package/src/core/incomplete-installation.ts +45 -0
- package/src/core/response-schema.ts +1 -1
- package/src/core/safe-exec.ts +13 -1
- package/src/core/settings-layers.ts +155 -21
- package/src/core/skill-activation.ts +1 -1
- package/src/core/startup-timer.ts +3 -3
- package/src/core/state-file-lock.ts +13 -1
- package/src/core/termination.ts +78 -5
- package/src/domains/config/classify.ts +15 -3
- package/src/domains/config/extension.ts +19 -13
- package/src/domains/config/index.ts +10 -0
- package/src/domains/config/keybindings.ts +45 -9
- package/src/domains/context/bootstrap-prompt.ts +1 -1
- package/src/domains/context/bootstrap.ts +111 -18
- package/src/domains/context/clear.ts +16 -11
- package/src/domains/context/clio-md.ts +111 -9
- package/src/domains/context/codewiki/artifact.ts +400 -0
- package/src/domains/context/codewiki/build-worker-protocol.ts +24 -0
- package/src/domains/context/codewiki/build-worker.ts +54 -0
- package/src/domains/context/codewiki/coordinator.ts +182 -0
- package/src/domains/context/codewiki/indexer.ts +59 -144
- package/src/domains/context/codewiki/paths.ts +67 -0
- package/src/domains/context/codewiki/schema.ts +80 -0
- package/src/domains/context/codewiki/tree-sitter.ts +1 -1
- package/src/domains/context/contract.ts +11 -5
- package/src/domains/context/extension.ts +94 -143
- package/src/domains/context/fingerprint.ts +3 -1
- package/src/domains/context/index.ts +12 -22
- package/src/domains/context/project-metadata.ts +19 -0
- package/src/domains/context/prompt-context.ts +9 -10
- package/src/domains/context/refresh.ts +29 -21
- package/src/domains/context/runtime.ts +17 -0
- package/src/domains/context/wiki/generate.ts +39 -34
- package/src/domains/context/wiki/plan.ts +1 -1
- package/src/domains/context/wiki/prompts.ts +21 -8
- package/src/domains/dispatch/code-step.ts +20 -1
- package/src/domains/dispatch/extension.ts +158 -21
- package/src/domains/dispatch/failure-classification.ts +6 -0
- package/src/domains/dispatch/fleet-commit-attribution.ts +56 -0
- package/src/domains/dispatch/orphan-recovery.ts +50 -8
- package/src/domains/dispatch/receipt-integrity.ts +5 -0
- package/src/domains/dispatch/state.ts +31 -6
- package/src/domains/dispatch/transport.ts +2 -1
- package/src/domains/dispatch/types.ts +14 -0
- package/src/domains/dispatch/worker-spawn.ts +21 -2
- package/src/domains/eval/metrics/context.ts +1 -1
- package/src/domains/eval/types.ts +0 -1
- package/src/domains/evidence/build.ts +41 -1
- package/src/domains/lifecycle/migrations/2026-08-18-lmstudio-runtime-id.ts +52 -0
- package/src/domains/lifecycle/migrations/index.ts +24 -4
- package/src/domains/middleware/hooks-io.ts +12 -0
- package/src/domains/middleware/skills-reminder.ts +30 -15
- package/src/domains/prompts/compiler.ts +142 -84
- package/src/domains/prompts/contract.ts +18 -2
- package/src/domains/prompts/extension.ts +39 -7
- package/src/domains/prompts/fragment-loader.ts +0 -1
- package/src/domains/prompts/fragments/identity/clio.md +2 -4
- package/src/domains/prompts/fragments/identity/docs-routing.md +10 -0
- package/src/domains/prompts/fragments/identity/self-awareness.md +1 -45
- package/src/domains/prompts/fragments/operating/contract.md +4 -50
- package/src/domains/prompts/fragments/operating/delegation.md +42 -0
- package/src/domains/prompts/fragments/operating/skills.md +26 -0
- package/src/domains/prompts/fragments/operating/worker.md +16 -0
- package/src/domains/prompts/fragments/safety/auto-edit.md +5 -5
- package/src/domains/prompts/fragments/safety/full-auto.md +3 -3
- package/src/domains/prompts/fragments/safety/read-only.md +4 -4
- package/src/domains/prompts/fragments/safety/suggest.md +2 -2
- package/src/domains/prompts/fragments/wiki/page.md +10 -0
- package/src/domains/prompts/fragments/wiki/plan.md +10 -0
- package/src/domains/prompts/preload.ts +3 -3
- package/src/domains/providers/auth/api-key.ts +1 -1
- package/src/domains/providers/auth/backend-file.ts +20 -10
- package/src/domains/providers/auth/backend-memory.ts +59 -4
- package/src/domains/providers/auth/boot-status.ts +65 -0
- package/src/domains/providers/auth/oauth.ts +2 -1
- package/src/domains/providers/auth/storage.ts +97 -38
- package/src/domains/providers/capabilities.ts +12 -4
- package/src/domains/providers/contract.ts +15 -4
- package/src/domains/providers/extension.ts +18 -6
- package/src/domains/providers/model-runtime-capabilities.ts +15 -4
- package/src/domains/providers/models/local-models/clio-local-coding-targets.yaml +118 -35
- package/src/domains/providers/plugins.ts +5 -3
- package/src/domains/providers/probe/fingerprint.ts +25 -5
- package/src/domains/providers/registry.ts +31 -10
- package/src/domains/providers/runtimes/boot-manifest.ts +55 -0
- package/src/domains/providers/runtimes/builtins.ts +2 -2
- package/src/domains/providers/runtimes/common/lmstudio-http.ts +423 -0
- package/src/domains/providers/runtimes/common/local-synth.ts +6 -7
- package/src/domains/providers/runtimes/local-native/lmstudio.ts +241 -0
- package/src/domains/providers/support.ts +6 -3
- package/src/domains/providers/types/local-model-quirks.ts +7 -9
- package/src/domains/providers/types/runtime-descriptor.ts +12 -1
- package/src/domains/providers/types/target-descriptor.ts +22 -0
- package/src/domains/resources/contract.ts +0 -1
- package/src/domains/resources/extension.ts +1 -3
- package/src/domains/resources/loader.ts +3 -4
- package/src/domains/resources/prompts/loader.ts +16 -2
- package/src/domains/resources/prompts/substitute.ts +1 -65
- package/src/domains/resources/skills/content-hash.ts +2 -0
- package/src/domains/resources/skills/install.ts +17 -0
- package/src/domains/resources/skills/loader.ts +17 -10
- package/src/domains/resources/skills/marketplace.ts +55 -9
- package/src/domains/safety/action-classifier.ts +4 -2
- package/src/domains/safety/audit.ts +8 -2
- package/src/domains/safety/extension.ts +1 -1
- package/src/domains/session/compaction/branch-summary.ts +3 -2
- package/src/domains/session/compaction/cut-point.ts +2 -1
- package/src/domains/session/compaction/tokens.ts +2 -1
- package/src/domains/session/context-ledger.ts +14 -0
- package/src/domains/session/contract.ts +15 -0
- package/src/domains/session/decision-board.ts +190 -0
- package/src/domains/session/entries.ts +66 -3
- package/src/domains/session/extension.ts +93 -12
- package/src/domains/session/retry.ts +10 -18
- package/src/domains/session/session-artifacts.ts +107 -0
- package/src/domains/session/task-board.ts +207 -13
- package/src/domains/session/tree/active-path.ts +44 -5
- package/src/domains/session/tree/fork.ts +26 -27
- package/src/domains/session/tree/preview.ts +2 -2
- package/src/domains/session/workspace/git-probe.ts +17 -11
- package/src/domains/user-tasks/store.ts +297 -0
- package/src/engine/acp/errors.ts +96 -0
- package/src/engine/acp/server.ts +1728 -146
- package/src/engine/acp/transport.ts +135 -14
- package/src/engine/acp/types.ts +26 -0
- package/src/engine/agent.ts +3 -3
- package/src/engine/ai.ts +32 -27
- package/src/engine/alcf-oauth.ts +26 -19
- package/src/engine/api-registry.ts +223 -0
- package/src/engine/apis/index.ts +3 -7
- package/src/engine/apis/llamacpp-residency.ts +49 -9
- package/src/engine/apis/lmstudio-residency.ts +5 -21
- package/src/engine/apis/lmstudio.ts +243 -0
- package/src/engine/apis/ollama-native.ts +24 -3
- package/src/engine/apis/openai-completions.ts +170 -91
- package/src/engine/apis/residency.ts +139 -3
- package/src/engine/apis/types.ts +16 -0
- package/src/engine/env-api-keys.ts +98 -0
- package/src/engine/gemma-channel-filter.ts +223 -0
- package/src/engine/instrumented-tui.ts +192 -0
- package/src/engine/messages.ts +14 -0
- package/src/engine/models.ts +42 -0
- package/src/engine/oauth.ts +16 -12
- package/src/engine/prompt-templates.ts +1 -0
- package/src/engine/provider-payload.ts +16 -59
- package/src/engine/strip-tokenizer-sentinels.ts +1 -1
- package/src/engine/truncate.ts +9 -0
- package/src/engine/tui.ts +17 -9
- package/src/engine/types.ts +3 -6
- package/src/engine/worker-runtime-capabilities.ts +5 -0
- package/src/engine/worker-runtime.ts +1 -1
- package/src/engine/worker-tools.ts +9 -4
- package/src/entry/boot-options.ts +50 -0
- package/src/entry/orchestrator.ts +288 -150
- package/src/interactive/application-controller.ts +89 -2
- package/src/interactive/chat-loop.ts +266 -41
- package/src/interactive/chat-panel.ts +715 -278
- package/src/interactive/chat-renderer.ts +306 -85
- package/src/interactive/clio-editor.ts +3 -8
- package/src/interactive/command-fallbacks.ts +2 -2
- package/src/interactive/context-overlay.ts +27 -1
- package/src/interactive/editor-submit.ts +253 -24
- package/src/interactive/export-html/ansi-to-html.ts +161 -0
- package/src/interactive/export-html/index.ts +51 -0
- package/src/interactive/export-html/template.ts +45 -0
- package/src/interactive/export-html/tool-renderer.ts +54 -0
- package/src/interactive/footer/dashboard.ts +4 -0
- package/src/interactive/footer/notifications.ts +1 -1
- package/src/interactive/footer/widgets.ts +42 -22
- package/src/interactive/footer-panel.ts +8 -3
- package/src/interactive/format-time.ts +14 -2
- package/src/interactive/interactive-application.ts +203 -17
- package/src/interactive/interactive-event-projection.ts +18 -1
- package/src/interactive/interactive-input-runtime.ts +50 -4
- package/src/interactive/interactive-presentation.ts +151 -19
- package/src/interactive/interactive-shell.ts +268 -14
- package/src/interactive/interactive-slash-runtime.ts +184 -117
- package/src/interactive/interactive-tickers.ts +38 -7
- package/src/interactive/keybinding-manager.ts +1 -1
- package/src/interactive/layout.ts +40 -3
- package/src/interactive/overlay-frame.ts +1 -1
- package/src/interactive/overlay-general-openers.ts +58 -1
- package/src/interactive/overlay-key-routing.ts +3 -0
- package/src/interactive/overlay-lifecycle.ts +13 -0
- package/src/interactive/overlay-permission-lifecycle.ts +2 -1
- package/src/interactive/overlay-session-lifecycle.ts +69 -12
- package/src/interactive/overlays/ask-user.ts +146 -24
- package/src/interactive/overlays/decisions.ts +300 -0
- package/src/interactive/overlays/help-reference.ts +15 -10
- package/src/interactive/overlays/model-selector.ts +34 -16
- package/src/interactive/overlays/session-selector.ts +18 -0
- package/src/interactive/overlays/settings.ts +105 -17
- package/src/interactive/overlays/skills-hub.ts +4 -4
- package/src/interactive/overlays/tree-selector.ts +41 -6
- package/src/interactive/render-trace.ts +499 -90
- package/src/interactive/renderers/compaction-summary.ts +2 -2
- package/src/interactive/renderers/diff.ts +115 -104
- package/src/interactive/renderers/mermaid.ts +53 -0
- package/src/interactive/renderers/tool-execution.ts +516 -168
- package/src/interactive/renderers/worker-entry.ts +20 -4
- package/src/interactive/session-switch-settlement.ts +10 -0
- package/src/interactive/slash-autocomplete.ts +6 -114
- package/src/interactive/slash-commands.ts +135 -47
- package/src/interactive/slash-spec.ts +9 -38
- package/src/interactive/status/controller.ts +5 -1
- package/src/interactive/status/index.ts +12 -1
- package/src/interactive/status/reasoning.ts +87 -0
- package/src/interactive/status/summary.ts +13 -2
- package/src/interactive/stdout-backpressure.ts +99 -0
- package/src/interactive/stream-pacer.ts +530 -0
- package/src/interactive/stream-pacing-policy.ts +66 -0
- package/src/interactive/tasks-overlay.ts +368 -14
- package/src/interactive/terminal-lease.ts +485 -0
- package/src/interactive/theme/tokens.ts +1 -1
- package/src/interactive/transcript-detail.ts +120 -0
- package/src/interactive/turn-context.ts +4 -3
- package/src/interactive/turn-persistence.ts +30 -13
- package/src/interactive/turn-queues.ts +12 -0
- package/src/interactive/turn-recovery.ts +25 -8
- package/src/interactive/turn-runtime.ts +79 -12
- package/src/interactive/turn-state.ts +10 -0
- package/src/interactive/view/artifacts.ts +114 -4
- package/src/interactive/view/view-overlay.ts +3 -0
- package/src/interactive/welcome-dashboard.ts +17 -16
- package/src/interactive/worker-receipts.ts +52 -3
- package/src/interactive/worker-stream.ts +5 -1
- package/src/tools/agent-tools.ts +23 -3
- package/src/tools/artifact.ts +2 -2
- package/src/tools/ask-user.ts +23 -13
- package/src/tools/bash.ts +30 -2
- package/src/tools/bootstrap.ts +34 -431
- package/src/tools/builtin-tool-catalog.ts +271 -0
- package/src/tools/codewiki/code-nav-surface.ts +29 -0
- package/src/tools/codewiki/code-nav.ts +8 -22
- package/src/tools/codewiki/shared.ts +41 -38
- package/src/tools/context/docs-engine.ts +14 -3
- package/src/tools/context/index.ts +107 -28
- package/src/tools/context/surface.ts +19 -0
- package/src/tools/core-bootstrap.ts +168 -0
- package/src/tools/credential-present.ts +5 -5
- package/src/tools/dispatch-admission.ts +533 -0
- package/src/tools/dispatch-background.ts +54 -0
- package/src/tools/dispatch-event-text.ts +6 -0
- package/src/tools/dispatch-plan.ts +9 -4
- package/src/tools/dispatch-run-events.ts +238 -0
- package/src/tools/dispatch-runner.ts +2370 -0
- package/src/tools/dispatch-scout-admission.ts +295 -0
- package/src/tools/dispatch-types.ts +77 -0
- package/src/tools/dispatch.ts +67 -3161
- package/src/tools/find.ts +4 -2
- package/src/tools/grep.ts +2 -2
- package/src/tools/lazy-tool.ts +60 -0
- package/src/tools/ledger.ts +3 -3
- package/src/tools/monitor-surface.ts +36 -0
- package/src/tools/monitor.ts +2 -32
- package/src/tools/observers.ts +2 -2
- package/src/tools/presentation.ts +107 -0
- package/src/tools/registry.ts +45 -27
- package/src/tools/safe-exec.ts +2 -2
- package/src/tools/steer-surface.ts +17 -0
- package/src/tools/steer.ts +2 -13
- package/src/tools/tasks.ts +108 -11
- package/src/tools/truncate.ts +25 -184
- package/src/tools/verify/frontend.ts +3 -1
- package/src/tools/verify/index.ts +3 -38
- package/src/tools/verify/surface.ts +46 -0
- package/src/tools/web-fetch-surface.ts +23 -0
- package/src/tools/web-fetch.ts +2 -20
- package/src/tools/write.ts +7 -2
- package/src/worker/entry.ts +39 -2
- package/src/worker/spec-contract.ts +26 -5
- package/dist/chunk-7SS2CTV2.js +0 -61361
- package/dist/chunk-DKGKUHFA.js +0 -924
- package/dist/chunk-GEP36Y4X.js +0 -12796
- package/dist/chunk-XYWBQRDM.js +0 -137
- package/dist/clio-BZVGEUFJ.js +0 -58
- package/dist/configure-S7S6F6CL.js +0 -32
- package/docs/html/agents_blueprint.html +0 -936
- package/docs/html/alcf_blueprint.html +0 -324
- package/docs/html/architecture_blueprint.html +0 -850
- package/docs/html/commands_blueprint.html +0 -939
- package/docs/html/config_knobs_audit_blueprint.html +0 -178
- package/docs/html/configuration_blueprint.html +0 -1080
- package/docs/html/context_blueprint.html +0 -603
- package/docs/html/documentation_blueprint.html +0 -832
- package/docs/html/environment_blueprint.html +0 -404
- package/docs/html/eval_blueprint.html +0 -743
- package/docs/html/evals_internal_blueprint.html +0 -190
- package/docs/html/evolution_blueprint.html +0 -674
- package/docs/html/extensions_blueprint.html +0 -2065
- package/docs/html/fleet_dispatch_blueprint.html +0 -286
- package/docs/html/index.html +0 -919
- package/docs/html/lifecycle_blueprint.html +0 -723
- package/docs/html/memory_blueprint.html +0 -699
- package/docs/html/middleware_blueprint.html +0 -664
- package/docs/html/models_blueprint.html +0 -2366
- package/docs/html/observability_blueprint.html +0 -683
- package/docs/html/provider_adapter_blueprint.html +0 -245
- package/docs/html/safety_blueprint.html +0 -1386
- package/docs/html/shared.css +0 -571
- package/docs/html/shared.js +0 -143
- package/docs/html/skills_blueprint.html +0 -671
- package/docs/html/soak_blueprint.html +0 -182
- package/docs/html/tool_usage_blueprint.html +0 -350
- package/docs/html/tools_blueprint.html +0 -2249
- package/docs/html/trace_blueprint.html +0 -235
- package/docs/html/tui_design_blueprint.html +0 -374
- package/docs/html/validation_blueprint.html +0 -961
- package/docs/html/worker_dispatch_blueprint.html +0 -231
- package/src/core/release.ts +0 -2
- package/src/domains/providers/runtimes/common/lmstudio-logger.ts +0 -32
- package/src/domains/providers/runtimes/local-native/lmstudio-native.ts +0 -491
- package/src/engine/apis/lmstudio-native.ts +0 -1438
- package/src/engine/apis/thinking-replay.ts +0 -11
- package/src/tools/string-enum.ts +0 -15
|
@@ -2,28 +2,52 @@ import { performance } from "node:perf_hooks";
|
|
|
2
2
|
import type { OutputVerbosity } from "../core/defaults.js";
|
|
3
3
|
import { SKILL_SUGGESTION_PREFIX } from "../core/skill-activation.js";
|
|
4
4
|
import { rawDurationMs } from "../core/timers.js";
|
|
5
|
-
import { estimateReasoningTextTokens, extractReasoningTokens } from "../domains/session/context-accounting.js";
|
|
6
5
|
import { type Component, Markdown, truncateToWidth, wrapTextWithAnsi } from "../engine/tui.js";
|
|
7
6
|
import type { AgentMessage } from "../engine/types.js";
|
|
7
|
+
import { toolPresentationPolicy } from "../tools/presentation.js";
|
|
8
8
|
import type { ChatLoopEvent, RetryStatusPayload } from "./chat-loop.js";
|
|
9
9
|
import { extractText, isSelfExplainingAbort } from "./chat-loop-messages.js";
|
|
10
|
+
import type { ApprovalRequestView } from "./permission-overlay.js";
|
|
10
11
|
import { codeInk } from "./renderers/code-ink.js";
|
|
12
|
+
import { createMermaidMarkdownTransform } from "./renderers/mermaid.js";
|
|
11
13
|
import { styleTaggedNotice } from "./renderers/notice.js";
|
|
12
14
|
import { formatRetryStatus } from "./renderers/retry-status.js";
|
|
13
15
|
import {
|
|
14
|
-
classifyResourceRead,
|
|
15
|
-
previewResult,
|
|
16
16
|
renderToolAwaitingApproval,
|
|
17
17
|
renderToolCallHeader,
|
|
18
18
|
renderToolExecution,
|
|
19
19
|
renderToolRunningStatus,
|
|
20
20
|
renderToolStreamingExecution,
|
|
21
21
|
renderToolSubline,
|
|
22
|
-
unwrapResultEnvelope,
|
|
23
22
|
} from "./renderers/tool-execution.js";
|
|
24
23
|
import { renderWorkerEntryLines } from "./renderers/worker-entry.js";
|
|
25
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
compactReasoningTokens,
|
|
26
|
+
emptyRunTally,
|
|
27
|
+
foldMessageIntoRunTally,
|
|
28
|
+
formatReasoningChip,
|
|
29
|
+
formatReasoningLabel,
|
|
30
|
+
INLINE_STATUS_INDENT_COLS,
|
|
31
|
+
type ReasoningTokenProvenance,
|
|
32
|
+
type ReasoningUsageView,
|
|
33
|
+
reasoningFromTally,
|
|
34
|
+
type StatusPhase,
|
|
35
|
+
UNMEASURED_REASONING,
|
|
36
|
+
type VerbRender,
|
|
37
|
+
} from "./status/index.js";
|
|
26
38
|
import { clioTheme, fgSequence, GLYPH, markdownTheme, SGR_DIM, SGR_RESET } from "./theme/index.js";
|
|
39
|
+
import {
|
|
40
|
+
type Fold,
|
|
41
|
+
type FoldOverride,
|
|
42
|
+
policyRunningToolFold,
|
|
43
|
+
policyThinkingFold,
|
|
44
|
+
policyToolFold,
|
|
45
|
+
policyWorkerFold,
|
|
46
|
+
resolveFold,
|
|
47
|
+
type TranscriptDetailPolicy,
|
|
48
|
+
toggledFold,
|
|
49
|
+
transcriptDetail,
|
|
50
|
+
} from "./transcript-detail.js";
|
|
27
51
|
import { type WorkerEntryState, workerAskedByModel } from "./worker-stream.js";
|
|
28
52
|
|
|
29
53
|
// Fenced code reaches the screen through pi-tui's Markdown component, which
|
|
@@ -33,6 +57,13 @@ import { type WorkerEntryState, workerAskedByModel } from "./worker-stream.js";
|
|
|
33
57
|
// indentation, and width behavior stay pi-tui's and nothing post-processes
|
|
34
58
|
// already-rendered output.
|
|
35
59
|
const CHAT_MARKDOWN_THEME = markdownTheme(clioTheme(), (code, lang) => codeInk(lang, code.split("\n")));
|
|
60
|
+
const CHAT_MARKDOWN_OPTIONS = {
|
|
61
|
+
transform: createMermaidMarkdownTransform(clioTheme()),
|
|
62
|
+
renderLatex: true,
|
|
63
|
+
} as const;
|
|
64
|
+
// TuiAltScreen uses Pi's OSC 133 prompt-start marker for semantic prompt
|
|
65
|
+
// navigation. The sequence is zero-width and stripped before terminal output.
|
|
66
|
+
const OSC133_PROMPT_START = "\x1b]133;A\x07";
|
|
36
67
|
|
|
37
68
|
// Prefix and rail SGR constants, previously re-exported by the deleted
|
|
38
69
|
// palette.ts. Composing them from fgSequence/GLYPH here yields byte-identical
|
|
@@ -49,10 +80,10 @@ const USER_GLYPH = GLYPH.user;
|
|
|
49
80
|
|
|
50
81
|
/**
|
|
51
82
|
* An assistant turn is a sequence of text and tool segments interleaved in
|
|
52
|
-
* pi-agent-core event order.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* assistant's pre-tool narration and the post-tool summary. Storing a flat
|
|
83
|
+
* pi-agent-core event order. A tool turn emits assistant `message_update`
|
|
84
|
+
* events carrying `toolcall_*` formation, then `message_end`, then the
|
|
85
|
+
* `tool_execution_*` lifecycle before the next assistant message. Tool calls
|
|
86
|
+
* therefore sit BETWEEN the assistant's pre-tool narration and the post-tool summary. Storing a flat
|
|
56
87
|
* `text` buffer + `tools[]` array (pre-refactor) collapsed that order: all
|
|
57
88
|
* text across the turn concatenated into one line with every tool block
|
|
58
89
|
* appended at the end. The segment list preserves the stream order instead.
|
|
@@ -62,7 +93,7 @@ const USER_GLYPH = GLYPH.user;
|
|
|
62
93
|
* through the Markdown renderer. Partial markdown (unclosed fence, half-typed
|
|
63
94
|
* bullet) would otherwise paint garbage at ~60 fps under streaming.
|
|
64
95
|
*/
|
|
65
|
-
export type ReasoningTokenProvenance
|
|
96
|
+
export type { ReasoningTokenProvenance } from "./status/index.js";
|
|
66
97
|
|
|
67
98
|
export interface ChatPanelTurnUsage {
|
|
68
99
|
inputTokens: number;
|
|
@@ -117,6 +148,10 @@ type ToolSegment = {
|
|
|
117
148
|
result?: unknown;
|
|
118
149
|
/** True once `tool_execution_end` has landed (success or error). */
|
|
119
150
|
finished: boolean;
|
|
151
|
+
/** Pi streamed the call row before execution and later starts this same row. */
|
|
152
|
+
executionStarted: boolean;
|
|
153
|
+
/** The assistant stream closed this call's argument block. */
|
|
154
|
+
argsComplete: boolean;
|
|
120
155
|
/**
|
|
121
156
|
* True when the segment was force-settled without its own end event
|
|
122
157
|
* (blocked at admission, aborted mid-batch, id reused). A late
|
|
@@ -128,8 +163,13 @@ type ToolSegment = {
|
|
|
128
163
|
settledWithoutResult?: boolean | undefined;
|
|
129
164
|
/** True when the finished result was an error. Meaningful only after `finished`. */
|
|
130
165
|
isError: boolean;
|
|
131
|
-
/**
|
|
132
|
-
|
|
166
|
+
/**
|
|
167
|
+
* The operator's explicit fold for this block, or none. The effective state
|
|
168
|
+
* is this override when set, else what the transcript detail policy gives
|
|
169
|
+
* the call (through the tool's presentation once it has finished). Cleared
|
|
170
|
+
* when `/output` changes and when a session is switched in.
|
|
171
|
+
*/
|
|
172
|
+
fold?: FoldOverride;
|
|
133
173
|
/** Wall-clock start time captured by the chat panel for live duration display. */
|
|
134
174
|
startedAtMs?: number;
|
|
135
175
|
/** Completed call duration in milliseconds (event-supplied or measured locally). */
|
|
@@ -137,13 +177,15 @@ type ToolSegment = {
|
|
|
137
177
|
/** Persisted result summary (bytes, truncated, offloadPath, observation) from the chat loop. */
|
|
138
178
|
resultSummary?: Record<string, unknown> | undefined;
|
|
139
179
|
/**
|
|
140
|
-
* Latest cumulative
|
|
180
|
+
* Latest cumulative Pi result from `tool_execution_update`, including the
|
|
181
|
+
* display content and structured progress details. Cleared
|
|
141
182
|
* back to `undefined` on `tool_execution_end` so the finished `result`
|
|
142
|
-
* takes over. Only consumed
|
|
183
|
+
* takes over. Only consumed while the call is in flight and its effective
|
|
184
|
+
* state is expanded. The explicit
|
|
143
185
|
* `| undefined` is required under `exactOptionalPropertyTypes: true` so
|
|
144
186
|
* the clear path can re-assign `undefined` without a `delete`.
|
|
145
187
|
*/
|
|
146
|
-
|
|
188
|
+
partialResult?: unknown;
|
|
147
189
|
/**
|
|
148
190
|
* True while the call is parked at the permission gate. Set/cleared by
|
|
149
191
|
* `tool_approval_state` events and cleared by any settle so a denied or
|
|
@@ -151,6 +193,8 @@ type ToolSegment = {
|
|
|
151
193
|
* while `!finished`.
|
|
152
194
|
*/
|
|
153
195
|
awaitingApproval?: boolean | undefined;
|
|
196
|
+
/** Live, redacted approval facts. Never reconstructed during replay. */
|
|
197
|
+
approvalView?: ApprovalRequestView | undefined;
|
|
154
198
|
settlement?: "blocked" | "aborted" | "orphaned" | undefined;
|
|
155
199
|
/**
|
|
156
200
|
* The admission verdict's short reason, present only on a settlement the
|
|
@@ -158,6 +202,8 @@ type ToolSegment = {
|
|
|
158
202
|
* was refused and leaves the operator no way to learn why.
|
|
159
203
|
*/
|
|
160
204
|
blockReason?: string | undefined;
|
|
205
|
+
/** View-only marker: historical calls render mutation diffs without live color. */
|
|
206
|
+
replayed?: true;
|
|
161
207
|
};
|
|
162
208
|
/**
|
|
163
209
|
* A turn's terminal-error marker (`[error] ...`, `[aborted] ...`,
|
|
@@ -170,8 +216,32 @@ type ErrorSegment = {
|
|
|
170
216
|
kind: "error";
|
|
171
217
|
text: string;
|
|
172
218
|
};
|
|
173
|
-
|
|
174
|
-
|
|
219
|
+
/**
|
|
220
|
+
* One stretch of reasoning, in stream order with the text and tool segments
|
|
221
|
+
* around it. A turn used to hold one `thinking` string, so reasoning could only
|
|
222
|
+
* ever render in one place (above everything or below everything) and a
|
|
223
|
+
* turn that thought, wrote, thought again, called a tool, and thought once more
|
|
224
|
+
* had its reasoning pinned at the tail while the prose streamed in above it.
|
|
225
|
+
* Each stretch is its own segment now: it renders where it happened, and the
|
|
226
|
+
* one still open at the tail is the live indicator.
|
|
227
|
+
*/
|
|
228
|
+
type ThinkingSegment = {
|
|
229
|
+
kind: "thinking";
|
|
230
|
+
text: string;
|
|
231
|
+
/** Closed by the first text, tool, or message_end that follows it. */
|
|
232
|
+
finalized: boolean;
|
|
233
|
+
/** Panel clock when the first delta of this stretch arrived. */
|
|
234
|
+
startedAtMs?: number;
|
|
235
|
+
/** The operator's explicit fold for this stretch, or none. */
|
|
236
|
+
fold?: FoldOverride;
|
|
237
|
+
};
|
|
238
|
+
type AssistantSegment = TextSegment | ToolSegment | ErrorSegment | ThinkingSegment;
|
|
239
|
+
/**
|
|
240
|
+
* A caller-rendered block receives the frame's transcript detail policy so a
|
|
241
|
+
* block that owns fold state (the operator's `!` bash row) resolves it the same
|
|
242
|
+
* way the panel resolves a model call. Blocks that ignore it are unaffected.
|
|
243
|
+
*/
|
|
244
|
+
type ReplayBlockRenderer = (width: number, detail: TranscriptDetailPolicy) => string[];
|
|
175
245
|
type AssistantStatusLine = { phase: StatusPhase; verb: string; toneHint: VerbRender["toneHint"] };
|
|
176
246
|
|
|
177
247
|
type TranscriptEntry =
|
|
@@ -181,23 +251,13 @@ type TranscriptEntry =
|
|
|
181
251
|
role: "assistant";
|
|
182
252
|
segments: AssistantSegment[];
|
|
183
253
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* settles it collapses to a static `Thinking...` marker (folded) or
|
|
190
|
-
* a head-anchored rail (expanded), mirroring the pi-coding-agent
|
|
191
|
-
* reference which streams thinking from the partial message.
|
|
192
|
-
*/
|
|
193
|
-
thinking: string;
|
|
194
|
-
/**
|
|
195
|
-
* Whether the thinking block renders as the full body (true) or
|
|
196
|
-
* the one-line dim marker (false/undefined). Toggled by
|
|
197
|
-
* `toggleLastThinking()`. New thinking inherits the panel-level
|
|
198
|
-
* visibility mode until Ctrl+T toggles it again.
|
|
254
|
+
* `segments.length` when the current model call began, so `message_end`
|
|
255
|
+
* can tell which segments belong to the message it is settling. A
|
|
256
|
+
* provider that delivers thinking only in the final message (no
|
|
257
|
+
* `thinking_delta`) gets its thinking segment inserted here, ahead of
|
|
258
|
+
* the text the same message produced, rather than at the tail.
|
|
199
259
|
*/
|
|
200
|
-
|
|
260
|
+
messageStartSegmentIndex?: number | undefined;
|
|
201
261
|
pending: boolean;
|
|
202
262
|
statusLine?: AssistantStatusLine | null | undefined;
|
|
203
263
|
isError: boolean;
|
|
@@ -205,19 +265,53 @@ type TranscriptEntry =
|
|
|
205
265
|
}
|
|
206
266
|
/**
|
|
207
267
|
* A dispatched worker's attributed block. The panel owns only the fold
|
|
208
|
-
*
|
|
209
|
-
* streaming delta reaches the screen without copying the entry per frame.
|
|
268
|
+
* override; `state` is the live object the worker-stream reducer mutates, so
|
|
269
|
+
* a streaming delta reaches the screen without copying the entry per frame.
|
|
210
270
|
* The panel is told when that happened through `applyWorkerState`.
|
|
211
271
|
*/
|
|
212
|
-
| { role: "worker"; state: WorkerEntryState;
|
|
213
|
-
|
|
272
|
+
| { role: "worker"; state: WorkerEntryState; fold?: FoldOverride }
|
|
273
|
+
/**
|
|
274
|
+
* A block the caller renders itself. Most are settled the moment they are
|
|
275
|
+
* appended, but a few (the operator's `!` bash row) keep mutating the state
|
|
276
|
+
* their closure reads until the work behind them finishes. Such a block
|
|
277
|
+
* declares `isLive`, which keeps it out of the frozen prefix and keeps the
|
|
278
|
+
* panel's time-keyed tick running while it is unsettled.
|
|
279
|
+
*/
|
|
280
|
+
| {
|
|
281
|
+
role: "replayBlock";
|
|
282
|
+
renderBlock: ReplayBlockRenderer;
|
|
283
|
+
isLive?: (() => boolean) | undefined;
|
|
284
|
+
fold?: ReplayBlockFoldControl | undefined;
|
|
285
|
+
};
|
|
214
286
|
|
|
215
287
|
type WorkerTranscriptEntry = Extract<TranscriptEntry, { role: "worker" }>;
|
|
216
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Fold state a caller-rendered block owns. The panel never stores it: the
|
|
291
|
+
* block's closure reads it when rendering, so the panel only needs to resolve
|
|
292
|
+
* and flip it when the operator uses an expand/collapse key, and to clear it
|
|
293
|
+
* when `/output` changes. Same tri-state as a tool segment: an override, or
|
|
294
|
+
* none, over the fold the policy gives the block.
|
|
295
|
+
*/
|
|
296
|
+
export interface ReplayBlockFoldControl {
|
|
297
|
+
/** The fold the policy gives this block when no override is set. */
|
|
298
|
+
policyFold(detail: TranscriptDetailPolicy): Fold;
|
|
299
|
+
/** The operator's override, or none. */
|
|
300
|
+
fold(): FoldOverride;
|
|
301
|
+
setFold(fold: FoldOverride): void;
|
|
302
|
+
}
|
|
303
|
+
|
|
217
304
|
export interface ChatPanel extends Component {
|
|
218
305
|
appendUser(text: string): void;
|
|
219
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Append a caller-rendered block. Pass `isLive` when the closure reads state
|
|
308
|
+
* that keeps changing after the append, so the panel keeps re-rendering it
|
|
309
|
+
* instead of treating the first frame as final.
|
|
310
|
+
*/
|
|
311
|
+
appendReplayBlock(renderBlock: ReplayBlockRenderer, isLive?: () => boolean, fold?: ReplayBlockFoldControl): void;
|
|
220
312
|
applyEvent(event: ChatLoopEvent): void;
|
|
313
|
+
/** Mark a just-rehydrated tool segment so its mutation diff remains plain. */
|
|
314
|
+
markToolReplayed?(toolCallId: string): void;
|
|
221
315
|
/**
|
|
222
316
|
* Place or refresh a worker's block. The first call for an assignment
|
|
223
317
|
* inserts the entry (agent origin nests under the tool segment named by
|
|
@@ -232,23 +326,37 @@ export interface ChatPanel extends Component {
|
|
|
232
326
|
*/
|
|
233
327
|
workerStates(): ReadonlyArray<WorkerEntryState>;
|
|
234
328
|
setStatusLine(line: AssistantStatusLine | null): void;
|
|
329
|
+
/**
|
|
330
|
+
* Publish the live run tally's reasoning projection. The pending entry's
|
|
331
|
+
* tail line reads this; passing null returns it to unmeasured, which is what
|
|
332
|
+
* an idle or just-started turn is.
|
|
333
|
+
*/
|
|
334
|
+
setLiveReasoning(view: ReasoningUsageView | null): void;
|
|
335
|
+
/**
|
|
336
|
+
* Flip the newest foldable block (tool call, worker card, or fold-owning
|
|
337
|
+
* replay block) away from its effective state. The flip is an override
|
|
338
|
+
* over the transcript detail policy: under `/output verbose` it folds an
|
|
339
|
+
* open block, under `/output minimal` it opens a folded one.
|
|
340
|
+
*/
|
|
235
341
|
toggleLastToolExpanded(): boolean;
|
|
342
|
+
/** Set an explicit override on every tool, worker, and fold-owning block at once. */
|
|
236
343
|
toggleAllToolsExpanded(): boolean;
|
|
237
344
|
/**
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
345
|
+
* Drop every operator override so each block returns to what the transcript
|
|
346
|
+
* detail policy gives it. `/output` changes and session switches do this:
|
|
347
|
+
* the operator asked for a new baseline, and what they had opened belonged
|
|
348
|
+
* to the transcript they left. Idempotent.
|
|
242
349
|
*/
|
|
243
|
-
|
|
350
|
+
clearFoldOverrides(): void;
|
|
244
351
|
/**
|
|
245
|
-
* Flip thinking
|
|
246
|
-
* and the full rail-prefixed body
|
|
247
|
-
*
|
|
248
|
-
* like a transcript-level thinking visibility toggle.
|
|
352
|
+
* Flip the newest thinking stretch between the one-line dim marker
|
|
353
|
+
* and the full rail-prefixed body, as an override over the policy. A turn
|
|
354
|
+
* with no thinking yet is left alone; a new stretch inherits the policy.
|
|
249
355
|
*/
|
|
250
356
|
toggleLastThinking(): boolean;
|
|
251
357
|
toggleAllThinking(): boolean;
|
|
358
|
+
/** Whether live thinking would render open this frame, which presentation pacing consults. */
|
|
359
|
+
isThinkingExpanded(): boolean;
|
|
252
360
|
/** Toggle whether expanded live tool bodies include cumulative partial output. */
|
|
253
361
|
toggleLiveToolOutput(): boolean;
|
|
254
362
|
/** Clears the visible transcript. /new uses this after rotating the session. */
|
|
@@ -328,39 +436,42 @@ function extractAssistantThinking(message: unknown): string {
|
|
|
328
436
|
.join("");
|
|
329
437
|
}
|
|
330
438
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
439
|
+
/**
|
|
440
|
+
* The panel's view of one assistant message's spend, folded through the same
|
|
441
|
+
* `foldMessageIntoRunTally` the status machine uses. The panel used to
|
|
442
|
+
* re-derive reasoning here with its own provider-lookup-then-estimate rule, so
|
|
443
|
+
* the transcript receipt and the footer could report the same turn differently.
|
|
444
|
+
*/
|
|
335
445
|
function assistantUsage(message: unknown): ChatPanelTurnUsage | undefined {
|
|
336
446
|
if (!message || typeof message !== "object" || (message as { role?: unknown }).role !== "assistant") return undefined;
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
const thinking = extractAssistantThinking(message);
|
|
340
|
-
const reportedReasoning = usage ? extractReasoningTokens(usage) : null;
|
|
341
|
-
const estimatedReasoning = reportedReasoning === null ? estimateReasoningTextTokens(thinking) : null;
|
|
342
|
-
const inputTokens = finiteToken(usage?.input);
|
|
343
|
-
const outputTokens = finiteToken(usage?.output);
|
|
344
|
-
const cacheReadTokens = finiteToken(usage?.cacheRead);
|
|
345
|
-
const cacheWriteTokens = finiteToken(usage?.cacheWrite);
|
|
447
|
+
const tally = foldMessageIntoRunTally(emptyRunTally(), message as AgentMessage);
|
|
448
|
+
const reasoning = reasoningFromTally(tally);
|
|
346
449
|
if (
|
|
347
|
-
inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens === 0 &&
|
|
348
|
-
|
|
349
|
-
estimatedReasoning === null
|
|
450
|
+
tally.inputTokens + tally.outputTokens + tally.cacheReadTokens + tally.cacheWriteTokens === 0 &&
|
|
451
|
+
reasoning.provenance === "unmeasured"
|
|
350
452
|
) {
|
|
351
453
|
return undefined;
|
|
352
454
|
}
|
|
353
|
-
const turnUsage: ChatPanelTurnUsage = {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
455
|
+
const turnUsage: ChatPanelTurnUsage = {
|
|
456
|
+
inputTokens: tally.inputTokens,
|
|
457
|
+
outputTokens: tally.outputTokens,
|
|
458
|
+
cacheReadTokens: tally.cacheReadTokens,
|
|
459
|
+
cacheWriteTokens: tally.cacheWriteTokens,
|
|
460
|
+
modelCalls: 1,
|
|
461
|
+
};
|
|
462
|
+
if (reasoning.provenance !== "unmeasured") {
|
|
463
|
+
turnUsage.reasoningTokens = reasoning.tokens;
|
|
464
|
+
turnUsage.reasoningTokenProvenance = reasoning.provenance;
|
|
360
465
|
}
|
|
361
466
|
return turnUsage;
|
|
362
467
|
}
|
|
363
468
|
|
|
469
|
+
/** Settled-turn adapter onto the shared projection; the panel's own summary shape. */
|
|
470
|
+
function reasoningFromTurnUsage(usage: ChatPanelTurnUsage | undefined): ReasoningUsageView {
|
|
471
|
+
if (!usage || usage.reasoningTokenProvenance === undefined) return UNMEASURED_REASONING;
|
|
472
|
+
return { tokens: Math.max(0, usage.reasoningTokens ?? 0), provenance: usage.reasoningTokenProvenance };
|
|
473
|
+
}
|
|
474
|
+
|
|
364
475
|
function aggregateAssistantUsage(messages: unknown): ChatPanelTurnUsage | undefined {
|
|
365
476
|
if (!Array.isArray(messages)) return undefined;
|
|
366
477
|
const total: ChatPanelTurnUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
@@ -426,6 +537,24 @@ function scopeTerminalErrorAfterSuccessfulTool(
|
|
|
426
537
|
return `[error] ${modelFailure}${detachedDispatchSucceeded ? "; detached runs continue" : ""}`;
|
|
427
538
|
}
|
|
428
539
|
|
|
540
|
+
function hasThinking(entry: Extract<TranscriptEntry, { role: "assistant" }>): boolean {
|
|
541
|
+
return entry.segments.some((seg) => seg.kind === "thinking" && seg.text.length > 0);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** The thinking segment still receiving deltas, which is always the tail. */
|
|
545
|
+
function openThinkingSegment(entry: Extract<TranscriptEntry, { role: "assistant" }>): ThinkingSegment | null {
|
|
546
|
+
const tail = entry.segments[entry.segments.length - 1];
|
|
547
|
+
return tail?.kind === "thinking" && !tail.finalized ? tail : null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Index of the last thinking segment, which is where a settled turn's count chip rides. */
|
|
551
|
+
function lastThinkingIndex(entry: Extract<TranscriptEntry, { role: "assistant" }>): number {
|
|
552
|
+
for (let index = entry.segments.length - 1; index >= 0; index -= 1) {
|
|
553
|
+
if (entry.segments[index]?.kind === "thinking") return index;
|
|
554
|
+
}
|
|
555
|
+
return -1;
|
|
556
|
+
}
|
|
557
|
+
|
|
429
558
|
function hasVisibleOutput(entry: Extract<TranscriptEntry, { role: "assistant" }>): boolean {
|
|
430
559
|
for (const seg of entry.segments) {
|
|
431
560
|
if (seg.kind === "tool") return true;
|
|
@@ -510,7 +639,7 @@ function renderTextSegmentLines(seg: TextSegment, width: number): string[] {
|
|
|
510
639
|
return wrapped;
|
|
511
640
|
}
|
|
512
641
|
if (!seg.md) {
|
|
513
|
-
seg.md = new Markdown(seg.text, 0, 0, CHAT_MARKDOWN_THEME);
|
|
642
|
+
seg.md = new Markdown(seg.text, 0, 0, CHAT_MARKDOWN_THEME, undefined, CHAT_MARKDOWN_OPTIONS);
|
|
514
643
|
}
|
|
515
644
|
// pi-tui Markdown right-pads lines to the render width. If a long streaming
|
|
516
645
|
// reply has already scrolled, flipping the finalized segment from unpadded
|
|
@@ -558,49 +687,60 @@ function hangProseLines(lines: string[], firstPrefix?: string): string[] {
|
|
|
558
687
|
*/
|
|
559
688
|
const THINKING_HIDDEN_LABEL = `Thinking${GLYPH.ellipsis}`;
|
|
560
689
|
const THINKING_LINE_LIMIT = 12;
|
|
561
|
-
const REASONING_CHARS_PER_TOKEN = 4;
|
|
562
690
|
|
|
563
|
-
function
|
|
564
|
-
return
|
|
691
|
+
function dimLine(text: string, width: number): string {
|
|
692
|
+
return `${DIM}${truncateToWidth(text, Math.max(1, width), GLYPH.ellipsis, false)}${RESET}`;
|
|
565
693
|
}
|
|
566
694
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
695
|
+
/**
|
|
696
|
+
* A closed thinking stretch's folded marker, in place in the segment order. The
|
|
697
|
+
* turn's count chip rides on the last marker of a settled turn (`view` is
|
|
698
|
+
* unmeasured everywhere else), and comes from the settled usage, never from
|
|
699
|
+
* measuring the excerpt the panel happens to be holding.
|
|
700
|
+
*/
|
|
701
|
+
function renderSettledThinkingMarker(view: ReasoningUsageView, width: number): string {
|
|
702
|
+
const chip = formatReasoningChip(view, compactReasoningTokens);
|
|
703
|
+
return dimLine(
|
|
704
|
+
chip === null ? THINKING_HIDDEN_LABEL : `${THINKING_HIDDEN_LABEL} · ${chip} ${formatReasoningLabel(view)}`,
|
|
705
|
+
width,
|
|
706
|
+
);
|
|
575
707
|
}
|
|
576
708
|
|
|
577
709
|
/**
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
710
|
+
* The live turn's one reasoning line, rendered where the open thinking segment
|
|
711
|
+
* sits, which is the tail of the entry by construction: the first text, tool,
|
|
712
|
+
* or message_end closes it. Anchoring reasoning at the head put it above every
|
|
713
|
+
* streamed segment, so on a long turn the only progress indicator scrolled off
|
|
714
|
+
* the top; pinning one line at the tail put it below prose that streamed in
|
|
715
|
+
* after the model had already moved on, so the transcript read out of order.
|
|
716
|
+
*
|
|
717
|
+
* The count is whatever the run tally has folded so far, so between model calls
|
|
718
|
+
* the line states elapsed and nothing else. Visible thinking text is never
|
|
719
|
+
* counted: that number moved with how much reasoning the provider chose to
|
|
720
|
+
* display, not with what the turn spent.
|
|
721
|
+
*/
|
|
722
|
+
function renderLiveReasoningLine(view: ReasoningUsageView, elapsedMs: number | undefined, width: number): string {
|
|
723
|
+
const chip = formatReasoningChip(view, compactReasoningTokens);
|
|
724
|
+
const head = chip === null ? THINKING_HIDDEN_LABEL : `Thinking · ${chip} ${formatReasoningLabel(view)}`;
|
|
725
|
+
const seconds = elapsedMs === undefined ? 0 : Math.floor(Math.max(0, elapsedMs) / 1000);
|
|
726
|
+
return dimLine(seconds > 0 ? `${head} · ${seconds}s` : head, width);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Render the expanded thinking body: the text dimmed behind a dim `│ ` rail,
|
|
731
|
+
* capped at `THINKING_LINE_LIMIT` lines. A streaming turn keeps the tail (the
|
|
732
|
+
* reasoning still arriving); a settled one keeps the head with a
|
|
733
|
+
* `... N more lines hidden` overflow message. Mirrors the tool toggle's
|
|
582
734
|
* lab-notebook minimalism: no colored glyphs, no boxes.
|
|
583
735
|
*/
|
|
584
|
-
function
|
|
585
|
-
thinking: string,
|
|
586
|
-
expanded: boolean,
|
|
587
|
-
width: number,
|
|
588
|
-
streaming: boolean,
|
|
589
|
-
usage?: ChatPanelTurnUsage,
|
|
590
|
-
): string[] {
|
|
736
|
+
function renderThinkingRail(thinking: string, width: number, streaming: boolean, unbounded = false): string[] {
|
|
591
737
|
if (thinking.length === 0) return [];
|
|
592
|
-
const dimWrap = (s: string): string => `${DIM}${s}${RESET}`;
|
|
593
|
-
if (!expanded) {
|
|
594
|
-
const lineBudget = Math.max(1, width);
|
|
595
|
-
const display = reasoningDisplay(usage, thinking);
|
|
596
|
-
const label = streaming
|
|
597
|
-
? `Thinking (${display.tokens} tokens)${display.marker === "provider-reported" ? "" : " ≈ estimated"}…`
|
|
598
|
-
: THINKING_HIDDEN_LABEL;
|
|
599
|
-
return [dimWrap(truncateToWidth(label, lineBudget, GLYPH.ellipsis, false))];
|
|
600
|
-
}
|
|
601
738
|
const splitLines = thinking.split("\n");
|
|
602
739
|
let visible: string[];
|
|
603
|
-
if (
|
|
740
|
+
if (unbounded) {
|
|
741
|
+
// /export reproduces the whole transcript; the live cap is a screen budget.
|
|
742
|
+
visible = splitLines;
|
|
743
|
+
} else if (streaming) {
|
|
604
744
|
if (splitLines.length > THINKING_LINE_LIMIT) {
|
|
605
745
|
const hiddenCount = splitLines.length - THINKING_LINE_LIMIT;
|
|
606
746
|
visible = [`… ${hiddenCount} earlier lines hidden`, ...splitLines.slice(-THINKING_LINE_LIMIT)];
|
|
@@ -632,9 +772,13 @@ function renderThinkingLines(
|
|
|
632
772
|
* reported 717676 input tokens against a 500k window; it was 65 calls of about
|
|
633
773
|
* 11k, which the preceding one-call turns had already shown.
|
|
634
774
|
*/
|
|
635
|
-
function renderTurnUsageLine(
|
|
636
|
-
|
|
637
|
-
|
|
775
|
+
function renderTurnUsageLine(
|
|
776
|
+
usage: ChatPanelTurnUsage,
|
|
777
|
+
width: number,
|
|
778
|
+
receipt: TranscriptDetailPolicy["receipt"],
|
|
779
|
+
): string[] {
|
|
780
|
+
if (receipt === "none") return [];
|
|
781
|
+
if (receipt === "compact") {
|
|
638
782
|
const receipt = truncateToWidth(
|
|
639
783
|
` turn · in ${usage.inputTokens} · out ${usage.outputTokens}`,
|
|
640
784
|
width,
|
|
@@ -654,9 +798,10 @@ function renderTurnUsageLine(usage: ChatPanelTurnUsage, width: number, verbosity
|
|
|
654
798
|
// provenance of zero, and at narrow widths `reasoning 0 provider` orphaned
|
|
655
799
|
// the word `provider` on its own line. Zero suppresses the whole suffix, the
|
|
656
800
|
// same rule the caveat below already follows.
|
|
801
|
+
const view = reasoningFromTurnUsage(usage);
|
|
657
802
|
const reason =
|
|
658
|
-
|
|
659
|
-
? ` reasoning ${
|
|
803
|
+
view.tokens > 0 && view.provenance !== "unmeasured"
|
|
804
|
+
? ` reasoning ${view.provenance === "provider" ? "" : "≈"}${view.tokens} ${formatReasoningLabel(view)}`
|
|
660
805
|
: "";
|
|
661
806
|
const cache =
|
|
662
807
|
usage.cacheReadTokens > 0 || usage.cacheWriteTokens > 0
|
|
@@ -665,10 +810,7 @@ function renderTurnUsageLine(usage: ChatPanelTurnUsage, width: number, verbosity
|
|
|
665
810
|
// The caveat is about reasoning text the panel displayed. A turn that spent
|
|
666
811
|
// no reasoning tokens displayed none, so appending it there warned about
|
|
667
812
|
// something absent and cost a wrapped line per turn at narrow widths.
|
|
668
|
-
const caveat =
|
|
669
|
-
usage.reasoningTokens !== undefined && usage.reasoningTokens > 0
|
|
670
|
-
? " · reasoning text is a UI excerpt, not a verification"
|
|
671
|
-
: "";
|
|
813
|
+
const caveat = view.tokens > 0 ? " · reasoning text is a UI excerpt, not a verification" : "";
|
|
672
814
|
return wrapTextWithAnsi(
|
|
673
815
|
`${DIM} turn · in ${usage.inputTokens}${calls} · out ${usage.outputTokens}${cache}${reason}${caveat}${RESET}`,
|
|
674
816
|
width,
|
|
@@ -685,6 +827,22 @@ function styleStatusVerb(text: string, toneHint: VerbRender["toneHint"]): string
|
|
|
685
827
|
return `${DIM}${text}${RESET}`;
|
|
686
828
|
}
|
|
687
829
|
|
|
830
|
+
/**
|
|
831
|
+
* The fold the policy gives a tool segment this frame: the running-tool rule
|
|
832
|
+
* while in flight, the tool body rule (through the tool's own presentation)
|
|
833
|
+
* once finished, and the error rule for a finished failure.
|
|
834
|
+
*/
|
|
835
|
+
function policySegmentFold(seg: ToolSegment, detail: TranscriptDetailPolicy): Fold {
|
|
836
|
+
if (!seg.finished) return policyRunningToolFold(detail);
|
|
837
|
+
if (seg.isError && detail.errors === "body") return "expanded";
|
|
838
|
+
return policyToolFold(detail, toolPresentationPolicy(seg.name, seg.args));
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/** Effective state of a tool segment: the operator's override, else the policy. */
|
|
842
|
+
function toolSegmentExpanded(seg: ToolSegment, detail: TranscriptDetailPolicy): boolean {
|
|
843
|
+
return resolveFold(seg.fold, policySegmentFold(seg, detail)) === "expanded";
|
|
844
|
+
}
|
|
845
|
+
|
|
688
846
|
function renderToolSegmentLines(
|
|
689
847
|
seg: ToolSegment,
|
|
690
848
|
width: number,
|
|
@@ -692,17 +850,26 @@ function renderToolSegmentLines(
|
|
|
692
850
|
latestHintToolId: string | null,
|
|
693
851
|
nowMs: number,
|
|
694
852
|
unboundedToolBodies: boolean,
|
|
695
|
-
|
|
853
|
+
detail: TranscriptDetailPolicy,
|
|
696
854
|
liveToolOutput: boolean,
|
|
697
855
|
): string[] {
|
|
698
856
|
const hintKey = seg.id === latestHintToolId ? expandKey : undefined;
|
|
699
|
-
const expanded =
|
|
857
|
+
const expanded = toolSegmentExpanded(seg, detail);
|
|
700
858
|
const elapsedMs = seg.startedAtMs !== undefined ? Math.max(0, rawDurationMs(seg.startedAtMs, nowMs)) : undefined;
|
|
859
|
+
const phase: "forming" | "ready" | "running" = seg.executionStarted
|
|
860
|
+
? "running"
|
|
861
|
+
: seg.argsComplete
|
|
862
|
+
? "ready"
|
|
863
|
+
: "forming";
|
|
701
864
|
// A parked call is not executing: the awaiting-approval line replaces the
|
|
702
865
|
// counting elapsed spinner in both collapsed and expanded form (there is no
|
|
703
866
|
// body or partial output to expand while the call sits at the gate).
|
|
704
867
|
if (!seg.finished && seg.awaitingApproval === true) {
|
|
705
|
-
return renderToolAwaitingApproval(
|
|
868
|
+
return renderToolAwaitingApproval(
|
|
869
|
+
{ toolCallId: seg.id, toolName: seg.name, args: seg.args },
|
|
870
|
+
width,
|
|
871
|
+
seg.approvalView,
|
|
872
|
+
);
|
|
706
873
|
}
|
|
707
874
|
if (!expanded) {
|
|
708
875
|
return renderToolSubline(
|
|
@@ -718,20 +885,24 @@ function renderToolSegmentLines(
|
|
|
718
885
|
outcome: seg.settlement,
|
|
719
886
|
blockReason: seg.blockReason,
|
|
720
887
|
}
|
|
721
|
-
: { toolCallId: seg.id, toolName: seg.name, args: seg.args, elapsedMs },
|
|
888
|
+
: { toolCallId: seg.id, toolName: seg.name, args: seg.args, elapsedMs, phase },
|
|
722
889
|
width,
|
|
723
890
|
hintKey,
|
|
891
|
+
{
|
|
892
|
+
diffStyle: seg.replayed === true ? "plain" : "color",
|
|
893
|
+
foldedExtras: detail.toolBody === "folded" ? "none" : "per-tool",
|
|
894
|
+
},
|
|
724
895
|
);
|
|
725
896
|
}
|
|
726
897
|
if (!seg.finished) {
|
|
727
|
-
if (liveToolOutput && seg.
|
|
898
|
+
if (liveToolOutput && seg.partialResult !== undefined) {
|
|
728
899
|
return renderToolStreamingExecution(
|
|
729
|
-
{ toolCallId: seg.id, toolName: seg.name, args: seg.args, elapsedMs },
|
|
900
|
+
{ toolCallId: seg.id, toolName: seg.name, args: seg.args, elapsedMs, phase },
|
|
730
901
|
width,
|
|
731
|
-
seg.
|
|
902
|
+
seg.partialResult,
|
|
732
903
|
);
|
|
733
904
|
}
|
|
734
|
-
const call = { toolCallId: seg.id, toolName: seg.name, args: seg.args, elapsedMs };
|
|
905
|
+
const call = { toolCallId: seg.id, toolName: seg.name, args: seg.args, elapsedMs, phase };
|
|
735
906
|
return liveToolOutput ? renderToolCallHeader(call, width) : renderToolRunningStatus(call, width);
|
|
736
907
|
}
|
|
737
908
|
return renderToolExecution(
|
|
@@ -747,19 +918,22 @@ function renderToolSegmentLines(
|
|
|
747
918
|
blockReason: seg.blockReason,
|
|
748
919
|
},
|
|
749
920
|
width,
|
|
750
|
-
{ unbounded: unboundedToolBodies },
|
|
921
|
+
{ unbounded: unboundedToolBodies, diffStyle: seg.replayed === true ? "plain" : "color" },
|
|
751
922
|
);
|
|
752
923
|
}
|
|
753
924
|
|
|
754
925
|
/**
|
|
755
|
-
* Whether a worker block draws its one-line card this frame
|
|
756
|
-
*
|
|
757
|
-
*
|
|
926
|
+
* Whether a worker block draws its one-line card this frame: the operator's
|
|
927
|
+
* override when set, else the policy's worker rule, which under the balanced
|
|
928
|
+
* level is the origin default (a run the model asked for folds).
|
|
758
929
|
*/
|
|
759
|
-
function workerEntryFolded(entry: WorkerTranscriptEntry,
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
930
|
+
function workerEntryFolded(entry: WorkerTranscriptEntry, detail: TranscriptDetailPolicy): boolean {
|
|
931
|
+
return resolveFold(entry.fold, policyWorkerFold(detail, workerAskedByModel(entry.state))) === "folded";
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** Effective state of one thinking stretch: the operator's override, else the policy. */
|
|
935
|
+
function thinkingExpanded(segment: ThinkingSegment, detail: TranscriptDetailPolicy): boolean {
|
|
936
|
+
return resolveFold(segment.fold, policyThinkingFold(detail)) === "expanded";
|
|
763
937
|
}
|
|
764
938
|
|
|
765
939
|
function renderEntryLines(
|
|
@@ -770,24 +944,27 @@ function renderEntryLines(
|
|
|
770
944
|
latestFoldedWorkerId: string | null,
|
|
771
945
|
nowMs: number,
|
|
772
946
|
unboundedToolBodies: boolean,
|
|
773
|
-
|
|
947
|
+
detail: TranscriptDetailPolicy,
|
|
774
948
|
liveToolOutput: boolean,
|
|
949
|
+
liveReasoning: ReasoningUsageView,
|
|
775
950
|
): string[] {
|
|
776
951
|
if (entry.role === "replayBlock") {
|
|
777
|
-
return entry.renderBlock(width);
|
|
952
|
+
return entry.renderBlock(width, detail);
|
|
778
953
|
}
|
|
779
954
|
if (entry.role === "user") {
|
|
780
955
|
const contentWidth = Math.max(1, width - PROSE_GUTTER_WIDTH);
|
|
781
956
|
const lines: string[] = [];
|
|
782
957
|
for (const sourceLine of entry.text.split("\n")) lines.push(...wrapTextWithAnsi(sourceLine, contentWidth));
|
|
783
|
-
|
|
958
|
+
const rendered = hangProseLines(lines, USER_PREFIX);
|
|
959
|
+
if (rendered[0] !== undefined) rendered[0] = `${OSC133_PROMPT_START}${rendered[0]}`;
|
|
960
|
+
return rendered;
|
|
784
961
|
}
|
|
785
962
|
if (entry.role === "retryStatus") {
|
|
786
963
|
return wrapTextWithAnsi(formatRetryStatus(entry.status), width);
|
|
787
964
|
}
|
|
788
965
|
if (entry.role === "worker") {
|
|
789
966
|
return renderWorkerEntryLines(entry.state, width, {
|
|
790
|
-
folded: workerEntryFolded(entry,
|
|
967
|
+
folded: workerEntryFolded(entry, detail),
|
|
791
968
|
...(expandKey !== undefined && entry.state.assignmentId === latestFoldedWorkerId ? { expandKey } : {}),
|
|
792
969
|
unbounded: unboundedToolBodies,
|
|
793
970
|
});
|
|
@@ -796,38 +973,50 @@ function renderEntryLines(
|
|
|
796
973
|
// A mid-turn notice splits the transcript, so the events after it open a
|
|
797
974
|
// fresh entry that a stopped turn never fills; that entry used to reach the
|
|
798
975
|
// tail below and print a lone agent bubble under the notice.
|
|
799
|
-
if (
|
|
800
|
-
!entry.pending &&
|
|
801
|
-
entry.thinking.length === 0 &&
|
|
802
|
-
entry.turnUsage === undefined &&
|
|
803
|
-
!hasVisibleOutput(entry) &&
|
|
804
|
-
entry.segments.length === 0
|
|
805
|
-
) {
|
|
976
|
+
if (!entry.pending && entry.turnUsage === undefined && !hasVisibleOutput(entry) && entry.segments.length === 0) {
|
|
806
977
|
return [];
|
|
807
978
|
}
|
|
808
979
|
const lines: string[] = [];
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
|
|
815
|
-
// active so only one indicator shows (see `shouldRenderStatus` below).
|
|
816
|
-
if (entry.thinking.length > 0) {
|
|
817
|
-
lines.push(
|
|
818
|
-
...renderThinkingLines(
|
|
819
|
-
entry.thinking,
|
|
820
|
-
verbosity === "verbose" || (verbosity !== "minimal" && entry.expandedThinking === true),
|
|
821
|
-
width,
|
|
822
|
-
entry.pending,
|
|
823
|
-
entry.turnUsage,
|
|
824
|
-
),
|
|
825
|
-
);
|
|
826
|
-
}
|
|
980
|
+
// Reasoning renders in stream order, between the text and tool segments it
|
|
981
|
+
// came between. A closed stretch is a folded marker (or a head-anchored rail
|
|
982
|
+
// when expanded); the stretch still open while the turn is pending is the
|
|
983
|
+
// live indicator, with the tally's count and its own elapsed. The turn's
|
|
984
|
+
// settled count chip rides on the last marker once the turn has settled.
|
|
985
|
+
const chipIndex = entry.pending ? -1 : lastThinkingIndex(entry);
|
|
827
986
|
const clioPrefix = entry.isError ? CLIO_PREFIX_ERROR : CLIO_PREFIX;
|
|
828
987
|
const proseWidth = Math.max(1, width - PROSE_GUTTER_WIDTH);
|
|
829
988
|
let labeled = false;
|
|
830
|
-
|
|
989
|
+
let liveIndicatorShown = false;
|
|
990
|
+
let latestThinkingExpanded = policyThinkingFold(detail) === "expanded";
|
|
991
|
+
for (let segIndex = 0; segIndex < entry.segments.length; segIndex += 1) {
|
|
992
|
+
const seg = entry.segments[segIndex];
|
|
993
|
+
if (seg === undefined) continue;
|
|
994
|
+
if (seg.kind === "thinking") {
|
|
995
|
+
if (seg.text.length === 0) continue;
|
|
996
|
+
const thinkingExpandedNow = thinkingExpanded(seg, detail);
|
|
997
|
+
latestThinkingExpanded = thinkingExpandedNow;
|
|
998
|
+
const live = entry.pending && !seg.finalized;
|
|
999
|
+
if (thinkingExpandedNow) lines.push(...renderThinkingRail(seg.text, width, live, unboundedToolBodies));
|
|
1000
|
+
if (live) {
|
|
1001
|
+
liveIndicatorShown = true;
|
|
1002
|
+
// The bare marker level states that the model is thinking and
|
|
1003
|
+
// nothing else: no count, no elapsed. An operator who opened the
|
|
1004
|
+
// stretch anyway gets the progress line under the rail.
|
|
1005
|
+
lines.push(
|
|
1006
|
+
detail.thinking === "marker" && !thinkingExpandedNow
|
|
1007
|
+
? dimLine(THINKING_HIDDEN_LABEL, width)
|
|
1008
|
+
: renderLiveReasoningLine(
|
|
1009
|
+
liveReasoning,
|
|
1010
|
+
seg.startedAtMs === undefined ? undefined : Math.max(0, nowMs - seg.startedAtMs),
|
|
1011
|
+
width,
|
|
1012
|
+
),
|
|
1013
|
+
);
|
|
1014
|
+
} else if (!thinkingExpandedNow) {
|
|
1015
|
+
const view = segIndex === chipIndex ? reasoningFromTurnUsage(entry.turnUsage) : UNMEASURED_REASONING;
|
|
1016
|
+
lines.push(renderSettledThinkingMarker(view, width));
|
|
1017
|
+
}
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
831
1020
|
if (seg.kind === "tool") {
|
|
832
1021
|
lines.push(
|
|
833
1022
|
...renderToolSegmentLines(
|
|
@@ -837,7 +1026,7 @@ function renderEntryLines(
|
|
|
837
1026
|
latestHintToolId,
|
|
838
1027
|
nowMs,
|
|
839
1028
|
unboundedToolBodies,
|
|
840
|
-
|
|
1029
|
+
detail,
|
|
841
1030
|
liveToolOutput,
|
|
842
1031
|
),
|
|
843
1032
|
);
|
|
@@ -870,21 +1059,30 @@ function renderEntryLines(
|
|
|
870
1059
|
lines.push(...hangProseLines(rendered));
|
|
871
1060
|
}
|
|
872
1061
|
}
|
|
873
|
-
|
|
1062
|
+
// A thinking stretch closes when text or a tool follows it so the historical
|
|
1063
|
+
// marker stays in stream order. The turn-level reasoning projection is still
|
|
1064
|
+
// live, though, and must remain beside the current tail instead of scrolling
|
|
1065
|
+
// away with that marker. An open tail stretch already rendered this line.
|
|
1066
|
+
if (entry.pending && hasThinking(entry) && !liveIndicatorShown) {
|
|
1067
|
+
lines.push(
|
|
1068
|
+
detail.thinking === "marker" && !latestThinkingExpanded
|
|
1069
|
+
? dimLine(THINKING_HIDDEN_LABEL, width)
|
|
1070
|
+
: renderLiveReasoningLine(liveReasoning, undefined, width),
|
|
1071
|
+
);
|
|
1072
|
+
liveIndicatorShown = true;
|
|
1073
|
+
}
|
|
1074
|
+
if (entry.turnUsage && !entry.pending) lines.push(...renderTurnUsageLine(entry.turnUsage, width, detail.receipt));
|
|
1075
|
+
// The open thinking segment's line is the one that speaks for reasoning while
|
|
1076
|
+
// the turn runs. The generic thinking verb is suppressed while it shows so
|
|
1077
|
+
// the entry never carries two indicators for the same thing.
|
|
874
1078
|
const shouldRenderStatus =
|
|
875
1079
|
entry.pending &&
|
|
876
1080
|
entry.statusLine !== null &&
|
|
877
1081
|
entry.statusLine !== undefined &&
|
|
878
1082
|
!(entry.statusLine.phase === "writing" && hasStreamingText(entry)) &&
|
|
879
|
-
!(entry.statusLine.phase === "thinking" &&
|
|
880
|
-
if (!labeled && !hasVisibleOutput(entry))
|
|
881
|
-
|
|
882
|
-
if (shouldRenderStatus) {
|
|
883
|
-
lines.push(
|
|
884
|
-
`${STATUS_INDENT}${styleStatusVerb(entry.statusLine?.verb ?? "", entry.statusLine?.toneHint ?? "muted")}`,
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
} else if (shouldRenderStatus) {
|
|
1083
|
+
!(entry.statusLine.phase === "thinking" && liveIndicatorShown);
|
|
1084
|
+
if (!labeled && !hasVisibleOutput(entry)) lines.push(clioPrefix.trimEnd());
|
|
1085
|
+
if (shouldRenderStatus) {
|
|
888
1086
|
lines.push(`${STATUS_INDENT}${styleStatusVerb(entry.statusLine?.verb ?? "", entry.statusLine?.toneHint ?? "muted")}`);
|
|
889
1087
|
}
|
|
890
1088
|
return lines;
|
|
@@ -898,8 +1096,14 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
898
1096
|
let cachedWidth: number | undefined;
|
|
899
1097
|
let cachedLines: string[] = [];
|
|
900
1098
|
let cachedExpandKey: string | undefined;
|
|
901
|
-
let
|
|
1099
|
+
let cachedDetail: TranscriptDetailPolicy | undefined;
|
|
902
1100
|
let cachedLiveToolOutput: boolean | undefined;
|
|
1101
|
+
/**
|
|
1102
|
+
* The verbosity the last frame rendered under, or null before any frame.
|
|
1103
|
+
* A change between frames is the operator asking for a new baseline
|
|
1104
|
+
* (`/output`, or Settings → Terminal), and every override goes with it.
|
|
1105
|
+
*/
|
|
1106
|
+
let lastVerbosity: OutputVerbosity | undefined | null = null;
|
|
903
1107
|
let cachedTick = 0;
|
|
904
1108
|
/**
|
|
905
1109
|
* Did the last executed render put a counting elapsed line on screen? It is
|
|
@@ -921,18 +1125,23 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
921
1125
|
const entryCacheCapacity = (): number =>
|
|
922
1126
|
Math.max(MIN_ENTRY_RENDER_CACHE, Math.min(MAX_ENTRY_RENDER_CACHE, transcript.length));
|
|
923
1127
|
/**
|
|
924
|
-
*
|
|
1128
|
+
* Regular-screen windowed-tail build: the lines of every settled
|
|
925
1129
|
* leading entry are baked into one frozen prefix, so a dirty frame re-renders
|
|
926
|
-
* only the live tail and re-emits the prefix by reference.
|
|
927
|
-
* receives the full line array, deliberately: the
|
|
1130
|
+
* only the live tail and re-emits the prefix by reference. TuiMainScreen still
|
|
1131
|
+
* receives the full line array, deliberately: the renderer keeps every
|
|
928
1132
|
* line in `previousLines` and full-redraws (clearing scrollback) when the
|
|
929
|
-
* head shrinks
|
|
930
|
-
*
|
|
931
|
-
*
|
|
1133
|
+
* head shrinks. Fullscreen mode instead gives the transcript its own pi-tui
|
|
1134
|
+
* ScrollView. The freeze is dropped whenever a frozen entry is invalidated or
|
|
1135
|
+
* the render key changes.
|
|
932
1136
|
*/
|
|
933
1137
|
let frozen: { lines: string[]; through: number; key: string } | null = null;
|
|
934
|
-
let thinkingExpanded = false;
|
|
935
1138
|
let liveToolOutput = true;
|
|
1139
|
+
/**
|
|
1140
|
+
* The run tally's reasoning, projected. It is panel-level rather than
|
|
1141
|
+
* per-entry because only the pending tail entry ever renders it, and that
|
|
1142
|
+
* entry is by definition unfrozen and uncached.
|
|
1143
|
+
*/
|
|
1144
|
+
let liveReasoning: ReasoningUsageView = UNMEASURED_REASONING;
|
|
936
1145
|
const unboundedToolBodies = options.unboundedToolBodies === true;
|
|
937
1146
|
|
|
938
1147
|
const markDirty = (): void => {
|
|
@@ -964,6 +1173,32 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
964
1173
|
|
|
965
1174
|
const now = (): number => options.now?.() ?? Date.now();
|
|
966
1175
|
|
|
1176
|
+
/** The policy for a frame or a keypress, from whatever the settings say right now. */
|
|
1177
|
+
const currentDetail = (): TranscriptDetailPolicy => transcriptDetail(options.getOutputVerbosity?.());
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Drop every operator override. Shared by the panel method and the
|
|
1181
|
+
* verbosity-change path in render, so both leave the same state behind.
|
|
1182
|
+
*/
|
|
1183
|
+
const dropFoldOverrides = (): void => {
|
|
1184
|
+
for (const entry of transcript) {
|
|
1185
|
+
if (entry.role === "replayBlock") {
|
|
1186
|
+
entry.fold?.setFold(undefined);
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
if (entry.role === "worker") {
|
|
1190
|
+
entry.fold = undefined;
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
if (entry.role !== "assistant") continue;
|
|
1194
|
+
for (const seg of entry.segments) {
|
|
1195
|
+
if (seg.kind === "tool" || seg.kind === "thinking") seg.fold = undefined;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
clearRenderCaches();
|
|
1199
|
+
markDirty();
|
|
1200
|
+
};
|
|
1201
|
+
|
|
967
1202
|
/**
|
|
968
1203
|
* Force an in-flight tool segment to a settled error line. A call blocked at
|
|
969
1204
|
* admission (loop guard, safety) or one whose `tool_execution_end` never
|
|
@@ -983,8 +1218,9 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
983
1218
|
if (seg.result === undefined)
|
|
984
1219
|
seg.result = "(no result: the call did not complete; execution was aborted, blocked, or orphaned)";
|
|
985
1220
|
seg.settlement = settlement;
|
|
986
|
-
seg.
|
|
1221
|
+
seg.partialResult = undefined;
|
|
987
1222
|
seg.awaitingApproval = undefined;
|
|
1223
|
+
seg.approvalView = undefined;
|
|
988
1224
|
};
|
|
989
1225
|
|
|
990
1226
|
/**
|
|
@@ -1025,8 +1261,6 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1025
1261
|
const entry: Extract<TranscriptEntry, { role: "assistant" }> = {
|
|
1026
1262
|
role: "assistant",
|
|
1027
1263
|
segments: [],
|
|
1028
|
-
thinking: "",
|
|
1029
|
-
expandedThinking: thinkingExpanded,
|
|
1030
1264
|
pending: false,
|
|
1031
1265
|
isError: false,
|
|
1032
1266
|
};
|
|
@@ -1034,9 +1268,34 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1034
1268
|
return entry;
|
|
1035
1269
|
};
|
|
1036
1270
|
|
|
1271
|
+
/**
|
|
1272
|
+
* Close the thinking stretch at the tail, if one is open. Anything that
|
|
1273
|
+
* follows reasoning in the stream (text, a tool call, the message settling)
|
|
1274
|
+
* ends that stretch; a later `thinking_delta` opens a new segment after it,
|
|
1275
|
+
* so the transcript keeps the order the model actually worked in.
|
|
1276
|
+
*/
|
|
1277
|
+
const closeOpenThinking = (entry: Extract<TranscriptEntry, { role: "assistant" }>): void => {
|
|
1278
|
+
const open = openThinkingSegment(entry);
|
|
1279
|
+
if (open === null) return;
|
|
1280
|
+
open.finalized = true;
|
|
1281
|
+
invalidateEntryCache(entry);
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
const appendThinkingDelta = (entry: Extract<TranscriptEntry, { role: "assistant" }>, delta: string): void => {
|
|
1285
|
+
if (delta.length === 0) return;
|
|
1286
|
+
invalidateEntryCache(entry);
|
|
1287
|
+
const open = openThinkingSegment(entry);
|
|
1288
|
+
if (open !== null) {
|
|
1289
|
+
open.text += delta;
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
entry.segments.push({ kind: "thinking", text: delta, finalized: false, startedAtMs: now() });
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1037
1295
|
const appendTextDelta = (entry: Extract<TranscriptEntry, { role: "assistant" }>, delta: string): void => {
|
|
1038
1296
|
if (delta.length === 0) return;
|
|
1039
1297
|
invalidateEntryCache(entry);
|
|
1298
|
+
closeOpenThinking(entry);
|
|
1040
1299
|
const tail = entry.segments[entry.segments.length - 1];
|
|
1041
1300
|
if (tail && tail.kind === "text" && !tail.finalized) {
|
|
1042
1301
|
tail.text += delta;
|
|
@@ -1046,15 +1305,22 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1046
1305
|
};
|
|
1047
1306
|
|
|
1048
1307
|
/**
|
|
1049
|
-
* Canonicalize
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1308
|
+
* Canonicalize the streamed text of a completed assistant message.
|
|
1309
|
+
*
|
|
1310
|
+
* The streamed text is wherever this message put it, not necessarily at the
|
|
1311
|
+
* tail: a message that thinks, writes, and thinks again leaves its text
|
|
1312
|
+
* behind a thinking segment. Looking only at the tail appended the message
|
|
1313
|
+
* text a second time under the reasoning marker, so the answer read twice.
|
|
1314
|
+
*
|
|
1315
|
+
* One streamed segment that is a prefix of the final text (the common case)
|
|
1316
|
+
* is overwritten in place and flipped to finalized so the next render pipes
|
|
1317
|
+
* it through Markdown. Several streamed segments (text split by reasoning)
|
|
1318
|
+
* are each finalized where they stand; the deltas already are the text.
|
|
1319
|
+
* When the message arrived fully formed with no deltas (non-streaming
|
|
1320
|
+
* path, synthetic notices, replay), a fresh finalized segment is appended.
|
|
1321
|
+
* `replaceTail` forces the overwrite for messages the chat loop rewrote
|
|
1322
|
+
* after streaming (locked-turn markup sanitation): the streamed text is dead
|
|
1323
|
+
* there, not a prefix.
|
|
1058
1324
|
*/
|
|
1059
1325
|
const canonicalizeMessageText = (
|
|
1060
1326
|
entry: Extract<TranscriptEntry, { role: "assistant" }>,
|
|
@@ -1062,15 +1328,36 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1062
1328
|
replaceTail = false,
|
|
1063
1329
|
): void => {
|
|
1064
1330
|
if (text.length === 0) return;
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1331
|
+
closeOpenThinking(entry);
|
|
1332
|
+
const messageStart = Math.min(entry.messageStartSegmentIndex ?? 0, entry.segments.length);
|
|
1333
|
+
const streamed: TextSegment[] = [];
|
|
1334
|
+
for (let index = messageStart; index < entry.segments.length; index += 1) {
|
|
1335
|
+
const segment = entry.segments[index];
|
|
1336
|
+
if (segment?.kind === "text" && !segment.finalized) streamed.push(segment);
|
|
1337
|
+
}
|
|
1338
|
+
const finalize = (segment: TextSegment, value: string): void => {
|
|
1339
|
+
segment.text = value;
|
|
1340
|
+
segment.finalized = true;
|
|
1069
1341
|
// The streaming wrap cache assumes append-only text. This is the one
|
|
1070
1342
|
// path that rewrites it wholesale, and finalized segments render through
|
|
1071
1343
|
// Markdown instead, so the cache is dead here either way.
|
|
1072
|
-
delete
|
|
1073
|
-
if (
|
|
1344
|
+
delete segment.wrapCache;
|
|
1345
|
+
if (segment.md) segment.md.setText(value);
|
|
1346
|
+
};
|
|
1347
|
+
if (replaceTail && streamed.length > 0) {
|
|
1348
|
+
const [first, ...rest] = streamed;
|
|
1349
|
+
if (first) finalize(first, text);
|
|
1350
|
+
for (const dead of rest) entry.segments.splice(entry.segments.indexOf(dead), 1);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
if (streamed.length === 1 && streamed[0] !== undefined) {
|
|
1354
|
+
const only = streamed[0];
|
|
1355
|
+
if (text.startsWith(only.text)) {
|
|
1356
|
+
finalize(only, text);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
} else if (streamed.length > 1) {
|
|
1360
|
+
for (const segment of streamed) finalize(segment, segment.text);
|
|
1074
1361
|
return;
|
|
1075
1362
|
}
|
|
1076
1363
|
entry.segments.push({ kind: "text", text, finalized: true });
|
|
@@ -1089,25 +1376,30 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1089
1376
|
};
|
|
1090
1377
|
|
|
1091
1378
|
/**
|
|
1092
|
-
* Who advertises the fold key this frame: the newest
|
|
1093
|
-
* the newest finished
|
|
1379
|
+
* Who advertises the fold key this frame: the newest worker card whose
|
|
1380
|
+
* effective state is folded, or the newest finished tool subline whose
|
|
1381
|
+
* effective state is folded with no worker card behind it. Effective means
|
|
1382
|
+
* override-or-policy, so the hint follows the block whatever the verbosity.
|
|
1094
1383
|
* One surface at most, because the key reaches the newest foldable thing of
|
|
1095
1384
|
* either kind, and a chord shown anywhere else would open something the
|
|
1096
1385
|
* operator was not looking at. An already-open newest card advertises
|
|
1097
1386
|
* nothing, since folding it again needs no invitation.
|
|
1098
1387
|
*/
|
|
1099
|
-
const expandHintOwner = (): { toolId: string | null; workerId: string | null } => {
|
|
1388
|
+
const expandHintOwner = (detail: TranscriptDetailPolicy): { toolId: string | null; workerId: string | null } => {
|
|
1100
1389
|
let workerMayOwn = true;
|
|
1101
1390
|
for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
|
|
1102
1391
|
const entry = transcript[entryIndex];
|
|
1103
1392
|
if (entry?.role === "worker") {
|
|
1104
|
-
return {
|
|
1393
|
+
return {
|
|
1394
|
+
toolId: null,
|
|
1395
|
+
workerId: workerMayOwn && workerEntryFolded(entry, detail) ? entry.state.assignmentId : null,
|
|
1396
|
+
};
|
|
1105
1397
|
}
|
|
1106
1398
|
if (entry?.role !== "assistant") continue;
|
|
1107
1399
|
for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
|
|
1108
1400
|
const seg = entry.segments[segIndex];
|
|
1109
1401
|
if (seg?.kind !== "tool") continue;
|
|
1110
|
-
if (seg.finished && !seg
|
|
1402
|
+
if (seg.finished && !toolSegmentExpanded(seg, detail)) return { toolId: seg.id, workerId: null };
|
|
1111
1403
|
workerMayOwn = false;
|
|
1112
1404
|
}
|
|
1113
1405
|
}
|
|
@@ -1142,18 +1434,25 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1142
1434
|
|
|
1143
1435
|
/** True when the entry renders at least one counting elapsed line this frame. */
|
|
1144
1436
|
const entryHasRunningTool = (entry: TranscriptEntry): boolean =>
|
|
1145
|
-
entry.role === "assistant" &&
|
|
1146
|
-
|
|
1437
|
+
(entry.role === "assistant" &&
|
|
1438
|
+
(entry.segments.some(
|
|
1439
|
+
(segment) => segment.kind === "tool" && !segment.finished && segment.startedAtMs !== undefined,
|
|
1440
|
+
// The live reasoning line counts its own elapsed, so a turn that is
|
|
1441
|
+
// only thinking still needs the time-keyed render key.
|
|
1442
|
+
) ||
|
|
1443
|
+
(entry.pending && openThinkingSegment(entry)?.startedAtMs !== undefined))) ||
|
|
1444
|
+
(entry.role === "replayBlock" && entry.isLive?.() === true);
|
|
1147
1445
|
|
|
1148
1446
|
/**
|
|
1149
1447
|
* Settled entries whose render is a pure function of the base key. A live
|
|
1150
1448
|
* worker block is excluded for the same reason a running tool is: the
|
|
1151
1449
|
* reducer mutates its state object in place, so a cached render would keep
|
|
1152
|
-
* serving the answer as it looked several deltas ago.
|
|
1450
|
+
* serving the answer as it looked several deltas ago. A replay block that
|
|
1451
|
+
* declares itself live is excluded on the same grounds.
|
|
1153
1452
|
*/
|
|
1154
1453
|
const entryIsStable = (entry: TranscriptEntry): boolean =>
|
|
1155
1454
|
entry.role === "user" ||
|
|
1156
|
-
entry.role === "replayBlock" ||
|
|
1455
|
+
(entry.role === "replayBlock" && entry.isLive?.() !== true) ||
|
|
1157
1456
|
(entry.role === "worker" && !entry.state.pending) ||
|
|
1158
1457
|
(entry.role === "assistant" &&
|
|
1159
1458
|
!entry.pending &&
|
|
@@ -1162,7 +1461,12 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1162
1461
|
const render = (width: number): string[] => {
|
|
1163
1462
|
const startedAt = performance.now();
|
|
1164
1463
|
const expandKey = resolveExpandKey();
|
|
1165
|
-
const verbosity = options.getOutputVerbosity?.()
|
|
1464
|
+
const verbosity = options.getOutputVerbosity?.();
|
|
1465
|
+
// A new verbosity is a new baseline: the operator's per-block overrides
|
|
1466
|
+
// were answers to the old one, so they go before the frame is built.
|
|
1467
|
+
if (lastVerbosity !== null && verbosity !== lastVerbosity) dropFoldOverrides();
|
|
1468
|
+
lastVerbosity = verbosity;
|
|
1469
|
+
const detail = transcriptDetail(verbosity);
|
|
1166
1470
|
const nowMs = now();
|
|
1167
1471
|
// `dirty` is set on mutation and never on a tick, so without time in the
|
|
1168
1472
|
// key a running tool's elapsed counter advanced only when something
|
|
@@ -1180,14 +1484,14 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1180
1484
|
!dirty &&
|
|
1181
1485
|
cachedWidth === width &&
|
|
1182
1486
|
cachedExpandKey === expandKey &&
|
|
1183
|
-
|
|
1487
|
+
cachedDetail === detail &&
|
|
1184
1488
|
cachedLiveToolOutput === liveToolOutput &&
|
|
1185
1489
|
cachedTick === tick
|
|
1186
1490
|
) {
|
|
1187
1491
|
options.onRenderMetrics?.({ durationMs: performance.now() - startedAt, cacheHit: true, entriesRendered: 0 });
|
|
1188
1492
|
return cachedLines;
|
|
1189
1493
|
}
|
|
1190
|
-
const { toolId: latestHintToolId, workerId: latestFoldedWorkerId } = expandHintOwner();
|
|
1494
|
+
const { toolId: latestHintToolId, workerId: latestFoldedWorkerId } = expandHintOwner(detail);
|
|
1191
1495
|
// The hint id is deliberately NOT part of the shared key: it changes on
|
|
1192
1496
|
// every finished collapsed tool, and keying every entry on it re-rendered
|
|
1193
1497
|
// the entire transcript per tool completion. Only the entry that contains
|
|
@@ -1196,7 +1500,7 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1196
1500
|
// bytes at every tick, and keying it on time would drop the entry cache
|
|
1197
1501
|
// and the frozen prefix ten times a second. Only the panel-level guard
|
|
1198
1502
|
// above is time-keyed, so a tick re-renders the live tail and nothing else.
|
|
1199
|
-
const baseKey = `${width}|${expandKey ?? ""}|${
|
|
1503
|
+
const baseKey = `${width}|${expandKey ?? ""}|${detail.toolBody}:${detail.runningTool}:${detail.thinking}:${detail.worker}:${detail.receipt}:${detail.errors}|${liveToolOutput}`;
|
|
1200
1504
|
const capacity = entryCacheCapacity();
|
|
1201
1505
|
if (frozen !== null && frozen.key !== baseKey) frozen = null;
|
|
1202
1506
|
const out: string[] = frozen === null ? [] : frozen.lines.slice();
|
|
@@ -1222,8 +1526,8 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1222
1526
|
const stacksOnPrevious =
|
|
1223
1527
|
entry.role === "worker" &&
|
|
1224
1528
|
previous?.role === "worker" &&
|
|
1225
|
-
workerEntryFolded(entry,
|
|
1226
|
-
workerEntryFolded(previous,
|
|
1529
|
+
workerEntryFolded(entry, detail) &&
|
|
1530
|
+
workerEntryFolded(previous, detail);
|
|
1227
1531
|
if (i > 0 && !stacksOnPrevious) out.push("");
|
|
1228
1532
|
const containsHint =
|
|
1229
1533
|
entryContainsHint(entry, latestHintToolId) ||
|
|
@@ -1245,8 +1549,9 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1245
1549
|
latestFoldedWorkerId,
|
|
1246
1550
|
nowMs,
|
|
1247
1551
|
unboundedToolBodies,
|
|
1248
|
-
|
|
1552
|
+
detail,
|
|
1249
1553
|
liveToolOutput,
|
|
1554
|
+
liveReasoning,
|
|
1250
1555
|
);
|
|
1251
1556
|
for (const line of renderedEntry) out.push(line);
|
|
1252
1557
|
if (cacheable) {
|
|
@@ -1269,7 +1574,7 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1269
1574
|
cachedLines = out;
|
|
1270
1575
|
cachedWidth = width;
|
|
1271
1576
|
cachedExpandKey = expandKey;
|
|
1272
|
-
|
|
1577
|
+
cachedDetail = detail;
|
|
1273
1578
|
cachedLiveToolOutput = liveToolOutput;
|
|
1274
1579
|
cachedTick = tick;
|
|
1275
1580
|
renderedRunningTool = sawRunningTool;
|
|
@@ -1283,8 +1588,8 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1283
1588
|
transcript.push({ role: "user", text });
|
|
1284
1589
|
markDirty();
|
|
1285
1590
|
},
|
|
1286
|
-
appendReplayBlock(renderBlock: ReplayBlockRenderer): void {
|
|
1287
|
-
transcript.push({ role: "replayBlock", renderBlock });
|
|
1591
|
+
appendReplayBlock(renderBlock: ReplayBlockRenderer, isLive?: () => boolean, fold?: ReplayBlockFoldControl): void {
|
|
1592
|
+
transcript.push({ role: "replayBlock", renderBlock, isLive, fold });
|
|
1288
1593
|
markDirty();
|
|
1289
1594
|
},
|
|
1290
1595
|
applyWorkerState(state: WorkerEntryState): void {
|
|
@@ -1296,7 +1601,7 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1296
1601
|
markDirty();
|
|
1297
1602
|
return;
|
|
1298
1603
|
}
|
|
1299
|
-
const entry: WorkerTranscriptEntry = { role: "worker", state
|
|
1604
|
+
const entry: WorkerTranscriptEntry = { role: "worker", state };
|
|
1300
1605
|
workerEntries.set(state.assignmentId, entry);
|
|
1301
1606
|
const at = workerInsertionIndex(state);
|
|
1302
1607
|
if (at === null) {
|
|
@@ -1313,13 +1618,26 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1313
1618
|
return [...workerEntries.values()].map((entry) => entry.state);
|
|
1314
1619
|
},
|
|
1315
1620
|
toggleLastToolExpanded(): boolean {
|
|
1316
|
-
//
|
|
1621
|
+
// The key owns the newest foldable thing, whichever kind it is. A worker
|
|
1317
1622
|
// block the operator just watched land is what they mean by "expand
|
|
1318
|
-
// that", not the tool call two screens up that spawned it.
|
|
1623
|
+
// that", not the tool call two screens up that spawned it. Every flip
|
|
1624
|
+
// is an override away from the block's effective state, so the same
|
|
1625
|
+
// key opens a folded block under minimal and folds an open one under
|
|
1626
|
+
// verbose.
|
|
1627
|
+
const detail = currentDetail();
|
|
1319
1628
|
for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
|
|
1320
1629
|
const entry = transcript[entryIndex];
|
|
1630
|
+
// A caller-rendered block that owns fold state (the operator's own
|
|
1631
|
+
// `!` bash row) is foldable too, and it is usually the newest thing
|
|
1632
|
+
// on screen when the key is pressed.
|
|
1633
|
+
if (entry?.role === "replayBlock" && entry.fold !== undefined) {
|
|
1634
|
+
entry.fold.setFold(toggledFold(resolveFold(entry.fold.fold(), entry.fold.policyFold(detail))));
|
|
1635
|
+
clearRenderCaches();
|
|
1636
|
+
markDirty();
|
|
1637
|
+
return true;
|
|
1638
|
+
}
|
|
1321
1639
|
if (entry?.role === "worker") {
|
|
1322
|
-
entry.
|
|
1640
|
+
entry.fold = workerEntryFolded(entry, detail) ? "expanded" : "folded";
|
|
1323
1641
|
clearRenderCaches();
|
|
1324
1642
|
markDirty();
|
|
1325
1643
|
return true;
|
|
@@ -1328,7 +1646,7 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1328
1646
|
for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
|
|
1329
1647
|
const seg = entry.segments[segIndex];
|
|
1330
1648
|
if (seg?.kind !== "tool") continue;
|
|
1331
|
-
seg.
|
|
1649
|
+
seg.fold = toolSegmentExpanded(seg, detail) ? "folded" : "expanded";
|
|
1332
1650
|
clearRenderCaches();
|
|
1333
1651
|
markDirty();
|
|
1334
1652
|
return true;
|
|
@@ -1337,9 +1655,15 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1337
1655
|
return false;
|
|
1338
1656
|
},
|
|
1339
1657
|
toggleAllToolsExpanded(): boolean {
|
|
1658
|
+
const detail = currentDetail();
|
|
1340
1659
|
const tools: ToolSegment[] = [];
|
|
1341
1660
|
const workers: WorkerTranscriptEntry[] = [];
|
|
1661
|
+
const blocks: ReplayBlockFoldControl[] = [];
|
|
1342
1662
|
for (const entry of transcript) {
|
|
1663
|
+
if (entry.role === "replayBlock") {
|
|
1664
|
+
if (entry.fold !== undefined) blocks.push(entry.fold);
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1343
1667
|
if (entry.role === "worker") {
|
|
1344
1668
|
workers.push(entry);
|
|
1345
1669
|
continue;
|
|
@@ -1349,61 +1673,70 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1349
1673
|
if (seg.kind === "tool") tools.push(seg);
|
|
1350
1674
|
}
|
|
1351
1675
|
}
|
|
1352
|
-
if (tools.length === 0 && workers.length === 0) return false;
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1676
|
+
if (tools.length === 0 && workers.length === 0 && blocks.length === 0) return false;
|
|
1677
|
+
// One folded block anywhere means "open everything"; otherwise fold
|
|
1678
|
+
// everything. Either way every block gets an explicit override.
|
|
1679
|
+
const expand =
|
|
1680
|
+
tools.some((seg) => !toolSegmentExpanded(seg, detail)) ||
|
|
1681
|
+
workers.some((entry) => workerEntryFolded(entry, detail)) ||
|
|
1682
|
+
blocks.some((fold) => resolveFold(fold.fold(), fold.policyFold(detail)) === "folded");
|
|
1683
|
+
const next: Fold = expand ? "expanded" : "folded";
|
|
1684
|
+
for (const seg of tools) seg.fold = next;
|
|
1685
|
+
for (const entry of workers) entry.fold = next;
|
|
1686
|
+
for (const fold of blocks) fold.setFold(next);
|
|
1356
1687
|
clearRenderCaches();
|
|
1357
1688
|
markDirty();
|
|
1358
1689
|
return true;
|
|
1359
1690
|
},
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
if (entry.role === "worker") {
|
|
1363
|
-
// The settled view for a worker is its origin default, not
|
|
1364
|
-
// universally folded: the operator's own run is the one block
|
|
1365
|
-
// /run exists to show them.
|
|
1366
|
-
entry.folded = workerAskedByModel(entry.state);
|
|
1367
|
-
continue;
|
|
1368
|
-
}
|
|
1369
|
-
if (entry.role !== "assistant") continue;
|
|
1370
|
-
for (const seg of entry.segments) {
|
|
1371
|
-
if (seg.kind === "tool") seg.expanded = false;
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
clearRenderCaches();
|
|
1375
|
-
markDirty();
|
|
1691
|
+
clearFoldOverrides(): void {
|
|
1692
|
+
dropFoldOverrides();
|
|
1376
1693
|
},
|
|
1377
1694
|
toggleLastThinking(): boolean {
|
|
1695
|
+
const detail = currentDetail();
|
|
1378
1696
|
for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
|
|
1379
1697
|
const entry = transcript[entryIndex];
|
|
1380
1698
|
if (entry?.role !== "assistant") continue;
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1699
|
+
for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
|
|
1700
|
+
const segment = entry.segments[segIndex];
|
|
1701
|
+
if (segment?.kind !== "thinking" || segment.text.length === 0) continue;
|
|
1702
|
+
segment.fold = thinkingExpanded(segment, detail) ? "folded" : "expanded";
|
|
1703
|
+
clearRenderCaches();
|
|
1704
|
+
markDirty();
|
|
1705
|
+
return true;
|
|
1706
|
+
}
|
|
1387
1707
|
}
|
|
1388
|
-
|
|
1389
|
-
return true;
|
|
1708
|
+
return false;
|
|
1390
1709
|
},
|
|
1391
1710
|
toggleAllThinking(): boolean {
|
|
1392
|
-
const
|
|
1711
|
+
const detail = currentDetail();
|
|
1712
|
+
const segments: ThinkingSegment[] = [];
|
|
1393
1713
|
for (const entry of transcript) {
|
|
1394
|
-
if (entry.role
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
return true;
|
|
1714
|
+
if (entry.role !== "assistant") continue;
|
|
1715
|
+
for (const segment of entry.segments) {
|
|
1716
|
+
if (segment.kind === "thinking" && segment.text.length > 0) segments.push(segment);
|
|
1717
|
+
}
|
|
1399
1718
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1719
|
+
if (segments.length === 0) return false;
|
|
1720
|
+
const expand = segments.some((segment) => !thinkingExpanded(segment, detail));
|
|
1721
|
+
for (const segment of segments) segment.fold = expand ? "expanded" : "folded";
|
|
1403
1722
|
clearRenderCaches();
|
|
1404
1723
|
markDirty();
|
|
1405
1724
|
return true;
|
|
1406
1725
|
},
|
|
1726
|
+
isThinkingExpanded(): boolean {
|
|
1727
|
+
// The live stretch lives on the newest assistant entry; its own override,
|
|
1728
|
+
// if any, is the one that applies. Before a stretch arrives, the policy answers.
|
|
1729
|
+
const detail = currentDetail();
|
|
1730
|
+
const index = lastAssistantIndex(transcript);
|
|
1731
|
+
const entry = index === null ? undefined : transcript[index];
|
|
1732
|
+
if (entry?.role === "assistant") {
|
|
1733
|
+
for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
|
|
1734
|
+
const segment = entry.segments[segIndex];
|
|
1735
|
+
if (segment?.kind === "thinking" && segment.text.length > 0) return thinkingExpanded(segment, detail);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return policyThinkingFold(detail) === "expanded";
|
|
1739
|
+
},
|
|
1407
1740
|
toggleLiveToolOutput(): boolean {
|
|
1408
1741
|
liveToolOutput = !liveToolOutput;
|
|
1409
1742
|
markDirty();
|
|
@@ -1412,9 +1745,17 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1412
1745
|
reset(): void {
|
|
1413
1746
|
transcript.length = 0;
|
|
1414
1747
|
workerEntries.clear();
|
|
1748
|
+
liveReasoning = UNMEASURED_REASONING;
|
|
1415
1749
|
clearRenderCaches();
|
|
1416
1750
|
markDirty();
|
|
1417
1751
|
},
|
|
1752
|
+
markToolReplayed(toolCallId: string): void {
|
|
1753
|
+
const owner = findToolSegmentOwner(toolCallId);
|
|
1754
|
+
if (!owner || owner.segment.finished) return;
|
|
1755
|
+
invalidateEntryCache(owner.entry);
|
|
1756
|
+
owner.segment.replayed = true;
|
|
1757
|
+
markDirty();
|
|
1758
|
+
},
|
|
1418
1759
|
applyEvent(event: ChatLoopEvent): void {
|
|
1419
1760
|
if (event.type === "agent_status") {
|
|
1420
1761
|
return;
|
|
@@ -1439,6 +1780,50 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1439
1780
|
markDirty();
|
|
1440
1781
|
return;
|
|
1441
1782
|
}
|
|
1783
|
+
if (event.type === "message_update") {
|
|
1784
|
+
const assistantEvent = event.assistantMessageEvent as {
|
|
1785
|
+
type?: unknown;
|
|
1786
|
+
contentIndex?: unknown;
|
|
1787
|
+
partial?: { content?: unknown };
|
|
1788
|
+
};
|
|
1789
|
+
if (
|
|
1790
|
+
assistantEvent.type !== "toolcall_start" &&
|
|
1791
|
+
assistantEvent.type !== "toolcall_delta" &&
|
|
1792
|
+
assistantEvent.type !== "toolcall_end"
|
|
1793
|
+
) {
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
const index = typeof assistantEvent.contentIndex === "number" ? assistantEvent.contentIndex : -1;
|
|
1797
|
+
const content = Array.isArray(assistantEvent.partial?.content) ? assistantEvent.partial.content : [];
|
|
1798
|
+
const block = content[index];
|
|
1799
|
+
if (block === null || typeof block !== "object" || Array.isArray(block)) return;
|
|
1800
|
+
const streamed = block as { type?: unknown; id?: unknown; name?: unknown; arguments?: unknown };
|
|
1801
|
+
if (streamed.type !== "toolCall" || typeof streamed.id !== "string" || streamed.id.length === 0) return;
|
|
1802
|
+
const owner = findToolSegmentOwner(streamed.id);
|
|
1803
|
+
const existing = owner?.segment;
|
|
1804
|
+
if (existing && !existing.finished && !existing.executionStarted) {
|
|
1805
|
+
if (owner) invalidateEntryCache(owner.entry);
|
|
1806
|
+
existing.name = typeof streamed.name === "string" && streamed.name.length > 0 ? streamed.name : existing.name;
|
|
1807
|
+
existing.args = streamed.arguments ?? existing.args;
|
|
1808
|
+
existing.argsComplete = assistantEvent.type === "toolcall_end";
|
|
1809
|
+
} else if (existing === undefined) {
|
|
1810
|
+
const assistant = ensureAssistant();
|
|
1811
|
+
assistant.pending = true;
|
|
1812
|
+
closeOpenThinking(assistant);
|
|
1813
|
+
assistant.segments.push({
|
|
1814
|
+
kind: "tool",
|
|
1815
|
+
id: streamed.id,
|
|
1816
|
+
name: typeof streamed.name === "string" && streamed.name.length > 0 ? streamed.name : "tool",
|
|
1817
|
+
args: streamed.arguments ?? {},
|
|
1818
|
+
finished: false,
|
|
1819
|
+
executionStarted: false,
|
|
1820
|
+
argsComplete: assistantEvent.type === "toolcall_end",
|
|
1821
|
+
isError: false,
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
markDirty();
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1442
1827
|
if (event.type === "text_delta") {
|
|
1443
1828
|
const assistant = ensureAssistant();
|
|
1444
1829
|
assistant.pending = true;
|
|
@@ -1447,20 +1832,34 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1447
1832
|
return;
|
|
1448
1833
|
}
|
|
1449
1834
|
if (event.type === "thinking_delta") {
|
|
1450
|
-
//
|
|
1835
|
+
// The text itself is never rendered unless the operator expands it;
|
|
1836
|
+
// the segment is what keeps reasoning in its place in the stream.
|
|
1451
1837
|
const assistant = ensureAssistant();
|
|
1452
1838
|
assistant.pending = true;
|
|
1453
|
-
assistant
|
|
1454
|
-
assistant.expandedThinking = thinkingExpanded;
|
|
1839
|
+
appendThinkingDelta(assistant, event.delta);
|
|
1455
1840
|
markDirty();
|
|
1456
1841
|
return;
|
|
1457
1842
|
}
|
|
1458
1843
|
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
1459
|
-
ensureAssistant()
|
|
1844
|
+
const assistant = ensureAssistant();
|
|
1845
|
+
assistant.pending = true;
|
|
1846
|
+
assistant.messageStartSegmentIndex = assistant.segments.length;
|
|
1460
1847
|
markDirty();
|
|
1461
1848
|
return;
|
|
1462
1849
|
}
|
|
1463
1850
|
if (event.type === "tool_execution_start") {
|
|
1851
|
+
const streamedOwner = findToolSegmentOwner(event.toolCallId);
|
|
1852
|
+
if (streamedOwner !== undefined && !streamedOwner.segment.finished && !streamedOwner.segment.executionStarted) {
|
|
1853
|
+
invalidateEntryCache(streamedOwner.entry);
|
|
1854
|
+
const streamed = streamedOwner.segment;
|
|
1855
|
+
streamed.name = event.toolName;
|
|
1856
|
+
streamed.args = event.args;
|
|
1857
|
+
streamed.executionStarted = true;
|
|
1858
|
+
streamed.argsComplete = true;
|
|
1859
|
+
streamed.startedAtMs = now();
|
|
1860
|
+
markDirty();
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1464
1863
|
// A model that reuses a tool-call id across calls would leave the
|
|
1465
1864
|
// prior same-id segment unsettled (its end matches the first segment
|
|
1466
1865
|
// on lookup). Settle any such orphan now, wherever it lives, so it
|
|
@@ -1472,18 +1871,20 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1472
1871
|
}
|
|
1473
1872
|
}
|
|
1474
1873
|
const assistant = ensureAssistant();
|
|
1475
|
-
//
|
|
1476
|
-
//
|
|
1477
|
-
|
|
1874
|
+
// No fold is stored here: the segment's effective state is resolved
|
|
1875
|
+
// per frame from the transcript detail policy and the tool's
|
|
1876
|
+
// registered presentation, with the operator's override on top.
|
|
1478
1877
|
assistant.pending = true;
|
|
1878
|
+
closeOpenThinking(assistant);
|
|
1479
1879
|
assistant.segments.push({
|
|
1480
1880
|
kind: "tool",
|
|
1481
1881
|
id: event.toolCallId,
|
|
1482
1882
|
name: event.toolName,
|
|
1483
1883
|
args: event.args,
|
|
1484
1884
|
finished: false,
|
|
1885
|
+
executionStarted: true,
|
|
1886
|
+
argsComplete: true,
|
|
1485
1887
|
isError: false,
|
|
1486
|
-
expanded,
|
|
1487
1888
|
startedAtMs: now(),
|
|
1488
1889
|
});
|
|
1489
1890
|
markDirty();
|
|
@@ -1499,24 +1900,22 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1499
1900
|
const tool = owner?.segment;
|
|
1500
1901
|
if (tool && !tool.finished) {
|
|
1501
1902
|
tool.awaitingApproval = event.state === "awaiting-approval" ? true : undefined;
|
|
1903
|
+
tool.approvalView = event.state === "awaiting-approval" ? event.view : undefined;
|
|
1502
1904
|
markDirty();
|
|
1503
1905
|
}
|
|
1504
1906
|
return;
|
|
1505
1907
|
}
|
|
1506
1908
|
if (event.type === "tool_execution_update") {
|
|
1507
|
-
// pi-agent emits `partialResult` as a cumulative
|
|
1508
|
-
//
|
|
1509
|
-
//
|
|
1510
|
-
//
|
|
1511
|
-
//
|
|
1512
|
-
// snapshot. Render dispatch picks up the new buffer on the next
|
|
1513
|
-
// frame via `renderToolSegmentLines`.
|
|
1909
|
+
// pi-agent emits `partialResult` as a cumulative AgentToolResult.
|
|
1910
|
+
// Preserve that full envelope so the renderer can use structured
|
|
1911
|
+
// progress details as well as content. Replace rather than append:
|
|
1912
|
+
// Pi's update semantics are cumulative, and appending would duplicate
|
|
1913
|
+
// every earlier snapshot.
|
|
1514
1914
|
const owner = findToolSegmentOwner(event.toolCallId);
|
|
1515
1915
|
if (owner) invalidateEntryCache(owner.entry);
|
|
1516
1916
|
const tool = owner?.segment;
|
|
1517
1917
|
if (tool && !tool.finished) {
|
|
1518
|
-
|
|
1519
|
-
tool.partialOutput = typeof unwrapped === "string" ? unwrapped : previewResult(unwrapped);
|
|
1918
|
+
tool.partialResult = event.partialResult;
|
|
1520
1919
|
}
|
|
1521
1920
|
markDirty();
|
|
1522
1921
|
return;
|
|
@@ -1568,32 +1967,57 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1568
1967
|
// expanded render switches to `renderToolExecution` and stays
|
|
1569
1968
|
// stable instead of churning through partial-frame layout. A
|
|
1570
1969
|
// denied park settles here too, so the awaiting styling must go.
|
|
1571
|
-
tool.
|
|
1970
|
+
tool.partialResult = undefined;
|
|
1572
1971
|
tool.awaitingApproval = undefined;
|
|
1972
|
+
tool.approvalView = undefined;
|
|
1573
1973
|
}
|
|
1574
1974
|
markDirty();
|
|
1575
1975
|
return;
|
|
1576
1976
|
}
|
|
1577
1977
|
if (event.type === "message_end") {
|
|
1978
|
+
const current = transcript[transcript.length - 1];
|
|
1979
|
+
let completedStreamedArgs = false;
|
|
1980
|
+
if (current?.role === "assistant") {
|
|
1981
|
+
for (const segment of current.segments) {
|
|
1982
|
+
if (segment.kind !== "tool" || segment.finished || segment.executionStarted || segment.argsComplete) continue;
|
|
1983
|
+
segment.argsComplete = true;
|
|
1984
|
+
completedStreamedArgs = true;
|
|
1985
|
+
}
|
|
1986
|
+
if (completedStreamedArgs) invalidateEntryCache(current);
|
|
1987
|
+
}
|
|
1578
1988
|
const text = extractAssistantText(event.message);
|
|
1579
1989
|
const thinking = extractAssistantThinking(event.message);
|
|
1580
1990
|
const extractedTerminalError = extractAssistantTerminalError(event.message);
|
|
1581
|
-
const current = transcript[transcript.length - 1];
|
|
1582
1991
|
const terminalError =
|
|
1583
1992
|
current?.role === "assistant"
|
|
1584
1993
|
? scopeTerminalErrorAfterSuccessfulTool(current, extractedTerminalError)
|
|
1585
1994
|
: extractedTerminalError;
|
|
1586
1995
|
const usage = assistantUsage(event.message);
|
|
1587
|
-
if (text.length === 0 && thinking.length === 0 && terminalError.length === 0 && usage === undefined)
|
|
1996
|
+
if (text.length === 0 && thinking.length === 0 && terminalError.length === 0 && usage === undefined) {
|
|
1997
|
+
if (completedStreamedArgs) markDirty();
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
1588
2000
|
const assistant = ensureAssistant();
|
|
1589
2001
|
// message_end rewrites exactly one entry: the assistant it lands on.
|
|
1590
2002
|
invalidateEntryCache(assistant);
|
|
1591
2003
|
if (usage !== undefined) assistant.turnUsage = usage;
|
|
1592
2004
|
if (terminalError.length > 0) assistant.isError = true;
|
|
2005
|
+
// The message is settled, so whatever reasoning it streamed is closed.
|
|
2006
|
+
// A message that carried thinking the panel never saw as deltas (a
|
|
2007
|
+
// non-streaming provider, a replayed message) gets one segment at the
|
|
2008
|
+
// point this message began, ahead of the text the same message
|
|
2009
|
+
// produced, rather than a marker dangling after the answer.
|
|
2010
|
+
closeOpenThinking(assistant);
|
|
1593
2011
|
if (thinking.length > 0) {
|
|
1594
|
-
assistant.
|
|
1595
|
-
|
|
2012
|
+
const messageStart = Math.min(assistant.messageStartSegmentIndex ?? 0, assistant.segments.length);
|
|
2013
|
+
const streamedThisMessage = assistant.segments
|
|
2014
|
+
.slice(messageStart)
|
|
2015
|
+
.some((segment) => segment.kind === "thinking" && segment.text.length > 0);
|
|
2016
|
+
if (!streamedThisMessage) {
|
|
2017
|
+
assistant.segments.splice(messageStart, 0, { kind: "thinking", text: thinking, finalized: true });
|
|
2018
|
+
}
|
|
1596
2019
|
}
|
|
2020
|
+
assistant.messageStartSegmentIndex = undefined;
|
|
1597
2021
|
// The chat loop marks messages it sanitized after streaming (dead
|
|
1598
2022
|
// tool-call markup on a synthesis-locked turn); the streamed tail
|
|
1599
2023
|
// must be replaced, not kept alongside a duplicate segment.
|
|
@@ -1664,13 +2088,26 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1664
2088
|
}
|
|
1665
2089
|
if (entry.pending) {
|
|
1666
2090
|
invalidateEntryCache(entry);
|
|
2091
|
+
closeOpenThinking(entry);
|
|
1667
2092
|
entry.pending = false;
|
|
1668
2093
|
entry.statusLine = null;
|
|
2094
|
+
entry.messageStartSegmentIndex = undefined;
|
|
1669
2095
|
}
|
|
1670
2096
|
}
|
|
1671
2097
|
markDirty();
|
|
1672
2098
|
}
|
|
1673
2099
|
},
|
|
2100
|
+
setLiveReasoning(view: ReasoningUsageView | null): void {
|
|
2101
|
+
const next = view ?? UNMEASURED_REASONING;
|
|
2102
|
+
if (next.tokens === liveReasoning.tokens && next.provenance === liveReasoning.provenance) return;
|
|
2103
|
+
liveReasoning = next;
|
|
2104
|
+
// The line lives on the pending tail entry, which the freeze may already
|
|
2105
|
+
// cover if nothing has mutated since the last settle.
|
|
2106
|
+
unfreezeTail();
|
|
2107
|
+
const last = transcript[transcript.length - 1];
|
|
2108
|
+
if (last !== undefined) invalidateEntryCache(last);
|
|
2109
|
+
markDirty();
|
|
2110
|
+
},
|
|
1674
2111
|
setStatusLine(line): void {
|
|
1675
2112
|
if (line) {
|
|
1676
2113
|
const assistant = ensureAssistant();
|