@iowarp/clio-coder 0.3.2 → 0.3.4
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 +269 -458
- package/CONTRIBUTING.md +1 -1
- package/README.md +3 -3
- package/dist/{acp-BIYHVZIM.js → acp-S5R4RR5B.js} +7 -6
- package/dist/{agents-YT6SSRIT.js → agents-P6DMMVZY.js} +24 -21
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-5TWEIYDN.js → auth-2XCZLPKS.js} +12 -8
- package/dist/{chunk-GGXXDWE4.js → chunk-22NAGB7X.js} +2 -2
- package/dist/{chunk-WMSVI4G2.js → chunk-2LZI5CAG.js} +133 -13
- package/dist/{chunk-OAO4GE4M.js → chunk-2TZWSW76.js} +2 -2
- package/dist/{chunk-OOJYHWRB.js → chunk-34475P3I.js} +2 -2
- package/dist/{chunk-WVO7V2QY.js → chunk-35MKKU5R.js} +4 -4
- package/dist/{chunk-LBNRH5WM.js → chunk-3HZ5RWN2.js} +5 -5
- package/dist/{chunk-AGYYIBLL.js → chunk-3JLKSKD7.js} +2 -2
- package/dist/{chunk-MBS4V7ZP.js → chunk-4JUF2NNX.js} +7 -7
- package/dist/{chunk-ZDOOVTXZ.js → chunk-4OC57DA6.js} +27 -4
- package/dist/chunk-5M54SPOL.js +926 -0
- package/dist/{chunk-STBPMHSX.js → chunk-7RXG6QRZ.js} +51 -11
- package/dist/{chunk-77VKQEHF.js → chunk-A2GZF7DC.js} +5 -5
- package/dist/{chunk-A3CYT5EX.js → chunk-AD2SYQYC.js} +55 -2
- package/dist/chunk-AOCYTWAV.js +449 -0
- package/dist/chunk-BEY543CS.js +258 -0
- package/dist/{chunk-6N5PTWMY.js → chunk-BP4OYD6A.js} +32 -13
- package/dist/chunk-BPGS2WCQ.js +612 -0
- package/dist/{chunk-J5HN4RYU.js → chunk-BRXQQJFP.js} +8 -8
- package/dist/chunk-CFGTUFWB.js +67 -0
- package/dist/{chunk-CBCAPZAA.js → chunk-E25LMLRW.js} +2 -2
- package/dist/{chunk-G2DE3C7R.js → chunk-EDRHSCIE.js} +4 -4
- package/dist/{chunk-4KLWL3UC.js → chunk-EFADSJET.js} +2 -2
- package/dist/{chunk-M6SHUN7Q.js → chunk-FO5ZOVUY.js} +2 -2
- package/dist/chunk-FYYLNIL5.js +313 -0
- package/dist/{chunk-EPVUXGXG.js → chunk-HV5X7OR2.js} +14 -12
- package/dist/{chunk-TZTZS7QK.js → chunk-HXG4IURW.js} +5 -3
- package/dist/{chunk-IGLFWIYI.js → chunk-K6WL7QZT.js} +3 -3
- package/dist/chunk-K7VKOLQQ.js +15 -0
- package/dist/{chunk-BMEMKKIT.js → chunk-KOHPCX4K.js} +2 -2
- package/dist/{chunk-V4RXGQ5Q.js → chunk-KRPY7NTG.js} +10 -7
- package/dist/chunk-LL4KHSZI.js +22 -0
- package/dist/{chunk-KJ5LWLOE.js → chunk-MEQ45TQ4.js} +15 -9
- package/dist/{chunk-5UUP6MWO.js → chunk-MV3K5QF2.js} +5 -436
- package/dist/{chunk-AO4RKG4M.js → chunk-N4CZJQRK.js} +5 -5
- package/dist/{chunk-ARBGF5F7.js → chunk-NILBFAPG.js} +14 -8
- package/dist/chunk-OZNBF4L3.js +23 -0
- package/dist/{verify-G6V4D2G7.js → chunk-PCZJO5TI.js} +127 -42
- package/dist/chunk-QQK64KLB.js +1360 -0
- package/dist/{chunk-6EJV5X2W.js → chunk-QQL5RT5M.js} +979 -1619
- package/dist/{chunk-LZSJBIVT.js → chunk-QWU7ZBO7.js} +70 -720
- package/dist/{chunk-2EHAIA3X.js → chunk-RD5U66HV.js} +3 -3
- package/dist/{chunk-OKGUZO2U.js → chunk-SPULKLCF.js} +4 -3
- package/dist/{chunk-OQ33BKR3.js → chunk-TTNYS3EA.js} +3 -60
- package/dist/chunk-TW3WDMVS.js +677 -0
- package/dist/chunk-TZSKNMZG.js +434 -0
- package/dist/{chunk-7MNJORFF.js → chunk-UL3WSD3F.js} +6 -1
- package/dist/{chunk-7EYHLWU7.js → chunk-UZHIZC5S.js} +7 -7
- package/dist/{chunk-QTYWRVRA.js → chunk-VAWWTKDP.js} +8 -8
- package/dist/{chunk-X75S7HFS.js → chunk-VEZEGCGW.js} +214 -20
- package/dist/{chunk-OHHN2SO4.js → chunk-VMNQ6OZA.js} +98 -202
- package/dist/chunk-VSNATDE6.js +122 -0
- package/dist/chunk-W6GROXXM.js +69 -0
- package/dist/chunk-WPQLXFOZ.js +375 -0
- package/dist/{chunk-ORBHGJC5.js → chunk-WR67VIZY.js} +3 -3
- package/dist/{chunk-3ZXDFGR5.js → chunk-X6COSD2O.js} +5 -5
- package/dist/chunk-ZGVHUX3M.js +66 -0
- package/dist/{chunk-MAW544W2.js → chunk-ZWMF7253.js} +4 -4
- package/dist/{chunk-MQSRRFWA.js → chunk-ZYKPLLNQ.js} +563 -546
- package/dist/cli/index.js +27 -23
- package/dist/{clio-4LY5K2AC.js → clio-J5JIOIDS.js} +7 -6
- package/dist/{code-nav-7AX6FYE6.js → code-nav-AXCXSBHX.js} +5 -3
- package/dist/{config-GTLUW2PR.js → config-OEBMIN2U.js} +37 -27
- package/dist/{configure-R6A64DHX.js → configure-PUQOSIXQ.js} +16 -13
- package/dist/{context-5VKGUVJJ.js → context-EKDCKUUZ.js} +82 -7
- package/dist/{context-RW5HC47S.js → context-MGSE4Z2T.js} +33 -23
- package/dist/{context-JFZEJ7W5.js → context-URSXPBCK.js} +17 -9
- package/dist/{context-clear-6ZHBAZZT.js → context-clear-KDAJRNUK.js} +33 -23
- package/dist/context-working-set-SBKMPPI2.js +1552 -0
- package/dist/{dispatch-runner-VKBRCWQC.js → dispatch-runner-MSWN72NK.js} +43 -29
- package/dist/{doctor-KI767GSN.js → doctor-7BSE27PJ.js} +10 -10
- package/dist/{eval-XSSNATB4.js → eval-IZGDOO4H.js} +9 -8
- package/dist/{evidence-UA6AWDQQ.js → evidence-SR7WXB5B.js} +51 -23
- package/dist/{evolve-QNTFGV6Z.js → evolve-K7VE2CBX.js} +30 -20
- package/dist/{fleet-Q7UOMUSG.js → fleet-7XMJNQNF.js} +48 -38
- package/dist/{fleet-preflight-DDN536IT.js → fleet-preflight-AQNAH644.js} +3 -3
- package/dist/{init-WBB65ZHQ.js → init-JGNPAYXT.js} +41 -31
- package/dist/{memory-MD3O64RI.js → memory-4ALKDJ4Q.js} +32 -22
- package/dist/{models-BZU34YWD.js → models-ZMMLFJNN.js} +22 -19
- package/dist/{monitor-MEQA5C3I.js → monitor-2F3T5KHP.js} +55 -43
- package/dist/{orchestrator-CGFKEP27.js → orchestrator-ORHT43JB.js} +2507 -1896
- package/dist/{reset-L2FQEE3E.js → reset-NXGTYNUO.js} +4 -3
- package/dist/{run-IV4Q6RLN.js → run-RF4WJGMT.js} +51 -41
- package/dist/{share-S5BZQC5I.js → share-UT3W6E4M.js} +5 -4
- package/dist/{skills-LQEKRDTN.js → skills-PSACKC5Q.js} +2 -2
- package/dist/{skills-eval-3DC4HEWS.js → skills-eval-WJSI55RZ.js} +34 -24
- package/dist/{targets-C4SSGQOB.js → targets-PIIRAOYS.js} +23 -20
- package/dist/{terminal-lease-IT5JW2NR.js → terminal-lease-ULWXWNVY.js} +5 -4
- package/dist/{upgrade-7TT7SQ3G.js → upgrade-346TZ6AV.js} +18 -17
- package/dist/{usage-GV4PKT3M.js → usage-6KKXR32N.js} +34 -24
- package/dist/verifiers-4UUM6TEE.js +1214 -0
- package/dist/verify-X5HDROLA.js +25 -0
- package/dist/{wiki-generate-DQF6Z66B.js → wiki-generate-7STOCIFZ.js} +42 -31
- package/dist/worker/entry.js +33 -24
- package/docs/README.md +8 -7
- package/docs/acp.md +1 -1
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +2 -2
- 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 +53 -21
- package/docs/config-knobs-audit.md +1 -2
- package/docs/configuration-and-targets.md +15 -1
- package/docs/context-engine.md +64 -12
- package/docs/context-working-set.md +194 -0
- package/docs/development-pipeline.md +1 -1
- package/docs/documentation-coverage.md +5 -5
- package/docs/documentation-guide.md +6 -5
- package/docs/environment-variables.md +2 -1
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +14 -1
- package/docs/evidence-and-memory.md +74 -2
- package/docs/evolution.md +1 -1
- package/docs/exit-codes-and-output.md +1 -1
- package/docs/extensions-and-sharing.md +2 -2
- package/docs/fleet-dispatch.md +22 -7
- package/docs/glossary.md +21 -1
- package/docs/installation-and-lifecycle.md +6 -6
- package/docs/middleware-and-components.md +1 -1
- package/docs/model-catalog.md +7 -9
- package/docs/observability.md +4 -4
- package/docs/performance-methodology.md +2 -2
- package/docs/proactive-memory.md +1 -1
- package/docs/prompt-envelope-and-tools.md +4 -4
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +35 -35
- package/docs/safety-model.md +23 -4
- package/docs/scientific-validation.md +21 -3
- package/docs/session-lifecycle.md +3 -3
- package/docs/skills-marketplace.md +1 -1
- package/docs/tool-usage.md +79 -12
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/docs/tui-design.md +2 -2
- package/docs/worker-dispatch-mechanics.md +11 -1
- package/package.json +8 -11
- package/skills/meta/clio-test/SKILL.md +20 -17
- package/skills/meta/clio-test/evals.md +3 -3
- package/skills/meta/clio-test/references/harness.md +35 -6
- package/skills/meta/clio-test/references/test-map.md +20 -10
- package/skills/registry.yaml +2 -2
- package/skills/skill-marketplace.json +1 -1
- package/src/cli/context-working-set.ts +513 -0
- package/src/cli/context.ts +8 -0
- package/src/cli/evidence.ts +20 -2
- package/src/cli/index.ts +4 -0
- package/src/cli/verifiers.ts +325 -0
- package/src/core/bash-exec.ts +39 -14
- package/src/core/bus-events.ts +19 -4
- package/src/core/config.ts +54 -0
- package/src/core/defaults.ts +50 -3
- package/src/core/git-commit-attribution.ts +46 -21
- package/src/core/verification-scripts.ts +6 -0
- package/src/domains/agents/builtins/verifier.md +3 -0
- package/src/domains/config/classify.ts +1 -0
- package/src/domains/config/keybindings.ts +3 -3
- package/src/domains/context/working-set/contract.ts +161 -0
- package/src/domains/context/working-set/defaults.ts +28 -0
- package/src/domains/context/working-set/engine.ts +203 -0
- package/src/domains/context/working-set/fold.ts +62 -0
- package/src/domains/context/working-set/horizon.ts +38 -0
- package/src/domains/context/working-set/marker.ts +103 -0
- package/src/domains/context/working-set/path-index.ts +436 -0
- package/src/domains/context/working-set/payload.ts +152 -0
- package/src/domains/context/working-set/policies/age-horizon.ts +55 -0
- package/src/domains/context/working-set/policies/index.ts +21 -0
- package/src/domains/context/working-set/policies/structural.ts +160 -0
- package/src/domains/context/working-set/project.ts +132 -0
- package/src/domains/context/working-set/protect.ts +109 -0
- package/src/domains/context/working-set/recall.ts +177 -0
- package/src/domains/context/working-set/replay/controls.ts +112 -0
- package/src/domains/context/working-set/replay/load-clio.ts +199 -0
- package/src/domains/context/working-set/replay/metrics.ts +185 -0
- package/src/domains/context/working-set/replay/reference-graph.ts +79 -0
- package/src/domains/context/working-set/replay/report.ts +139 -0
- package/src/domains/context/working-set/replay/runner.ts +325 -0
- package/src/domains/context/working-set/replay/synthetic.ts +422 -0
- package/src/domains/context/working-set/replay/trace.ts +21 -0
- package/src/domains/context/working-set/visible.ts +54 -0
- package/src/domains/evidence/build.ts +112 -45
- package/src/domains/evidence/eval.ts +24 -7
- package/src/domains/evidence/index.ts +53 -0
- package/src/domains/evidence/ordering.ts +12 -0
- package/src/domains/evidence/run-trust.ts +221 -0
- package/src/domains/evidence/store.ts +46 -6
- package/src/domains/evidence/trust-status.ts +854 -0
- package/src/domains/evidence/types.ts +26 -0
- package/src/domains/middleware/memory-intervention.ts +3 -0
- package/src/domains/middleware/stalled-turn.ts +165 -4
- package/src/domains/safety/autonomy.ts +1 -1
- package/src/domains/safety/default-path-policy.ts +8 -0
- package/src/domains/safety/finish-contract.ts +4 -3
- package/src/domains/safety/policy-engine.ts +48 -6
- package/src/domains/session/compaction/compact.ts +23 -1
- package/src/domains/session/compaction/cut-point.ts +2 -0
- package/src/domains/session/compaction/tokens.ts +16 -1
- package/src/domains/session/context-ledger.ts +2 -0
- package/src/domains/session/entries.ts +107 -1
- package/src/domains/session/manager.ts +9 -2
- package/src/domains/session/migrations/index.ts +22 -3
- package/src/engine/acp/server.ts +3 -0
- package/src/engine/agent.ts +18 -1
- package/src/engine/session.ts +9 -3
- package/src/entry/orchestrator.ts +16 -4
- package/src/interactive/chat-loop-messages.ts +18 -6
- package/src/interactive/chat-panel.ts +571 -244
- package/src/interactive/chat-renderer.ts +79 -39
- package/src/interactive/context-meter.ts +10 -0
- package/src/interactive/context-overlay.ts +81 -6
- package/src/interactive/context-recall-command.ts +110 -0
- package/src/interactive/editor-submit.ts +26 -1
- package/src/interactive/footer/widgets.ts +22 -20
- package/src/interactive/footer-panel.ts +6 -1
- package/src/interactive/interactive-application.ts +2 -0
- package/src/interactive/interactive-event-projection.ts +12 -0
- package/src/interactive/interactive-slash-runtime.ts +49 -8
- package/src/interactive/model-session-replay.ts +21 -0
- package/src/interactive/overlay-general-openers.ts +6 -0
- package/src/interactive/overlay-session-lifecycle.ts +8 -4
- package/src/interactive/overlays/ask-user.ts +146 -24
- package/src/interactive/renderers/tool-execution.ts +167 -56
- package/src/interactive/session-transcript.ts +2 -2
- package/src/interactive/slash-commands.ts +29 -2
- 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/transcript-detail.ts +120 -0
- package/src/interactive/turn-context.ts +238 -88
- package/src/interactive/turn-middleware.ts +6 -6
- package/src/tools/agent-tools.ts +11 -4
- package/src/tools/bash.ts +144 -82
- package/src/tools/builtin-tool-catalog.ts +18 -6
- package/src/tools/context/index.ts +105 -3
- package/src/tools/context/surface.ts +3 -2
- package/src/tools/core-bootstrap.ts +21 -0
- package/src/tools/dispatch-runner.ts +9 -7
- package/src/tools/monitor.ts +28 -20
- package/src/tools/presentation.ts +107 -0
- package/src/tools/registry.ts +65 -7
- package/src/tools/result-disposition.ts +550 -0
- package/src/tools/result-shaping.ts +262 -19
- package/src/tools/safe-exec.ts +2 -0
- package/src/tools/verify/authoring.ts +1119 -0
- package/src/tools/verify/catalog.ts +346 -0
- package/src/tools/verify/index.ts +13 -3
- package/src/tools/verify/scripts.ts +135 -37
- package/src/tools/verify/surface.ts +9 -5
- package/src/tools/worker-evidence.ts +35 -12
- package/dist/chunk-MNA4JGU4.js +0 -255
- package/dist/chunk-SRF2PJNW.js +0 -184
- package/dist/chunk-T6YILFSB.js +0 -80
- package/dist/chunk-VAKQQHWR.js +0 -434
|
@@ -2,9 +2,10 @@ 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
|
+
import { toolResultPresentationPolicy } from "../tools/result-disposition.js";
|
|
8
9
|
import type { ChatLoopEvent, RetryStatusPayload } from "./chat-loop.js";
|
|
9
10
|
import { extractText, isSelfExplainingAbort } from "./chat-loop-messages.js";
|
|
10
11
|
import type { ApprovalRequestView } from "./permission-overlay.js";
|
|
@@ -13,7 +14,6 @@ import { createMermaidMarkdownTransform } from "./renderers/mermaid.js";
|
|
|
13
14
|
import { styleTaggedNotice } from "./renderers/notice.js";
|
|
14
15
|
import { formatRetryStatus } from "./renderers/retry-status.js";
|
|
15
16
|
import {
|
|
16
|
-
classifyResourceRead,
|
|
17
17
|
renderToolAwaitingApproval,
|
|
18
18
|
renderToolCallHeader,
|
|
19
19
|
renderToolExecution,
|
|
@@ -22,8 +22,33 @@ import {
|
|
|
22
22
|
renderToolSubline,
|
|
23
23
|
} from "./renderers/tool-execution.js";
|
|
24
24
|
import { renderWorkerEntryLines } from "./renderers/worker-entry.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
compactReasoningTokens,
|
|
27
|
+
emptyRunTally,
|
|
28
|
+
foldMessageIntoRunTally,
|
|
29
|
+
formatReasoningChip,
|
|
30
|
+
formatReasoningLabel,
|
|
31
|
+
INLINE_STATUS_INDENT_COLS,
|
|
32
|
+
type ReasoningTokenProvenance,
|
|
33
|
+
type ReasoningUsageView,
|
|
34
|
+
reasoningFromTally,
|
|
35
|
+
type StatusPhase,
|
|
36
|
+
UNMEASURED_REASONING,
|
|
37
|
+
type VerbRender,
|
|
38
|
+
} from "./status/index.js";
|
|
26
39
|
import { clioTheme, fgSequence, GLYPH, markdownTheme, SGR_DIM, SGR_RESET } from "./theme/index.js";
|
|
40
|
+
import {
|
|
41
|
+
type Fold,
|
|
42
|
+
type FoldOverride,
|
|
43
|
+
policyRunningToolFold,
|
|
44
|
+
policyThinkingFold,
|
|
45
|
+
policyToolFold,
|
|
46
|
+
policyWorkerFold,
|
|
47
|
+
resolveFold,
|
|
48
|
+
type TranscriptDetailPolicy,
|
|
49
|
+
toggledFold,
|
|
50
|
+
transcriptDetail,
|
|
51
|
+
} from "./transcript-detail.js";
|
|
27
52
|
import { type WorkerEntryState, workerAskedByModel } from "./worker-stream.js";
|
|
28
53
|
|
|
29
54
|
// Fenced code reaches the screen through pi-tui's Markdown component, which
|
|
@@ -69,7 +94,7 @@ const USER_GLYPH = GLYPH.user;
|
|
|
69
94
|
* through the Markdown renderer. Partial markdown (unclosed fence, half-typed
|
|
70
95
|
* bullet) would otherwise paint garbage at ~60 fps under streaming.
|
|
71
96
|
*/
|
|
72
|
-
export type ReasoningTokenProvenance
|
|
97
|
+
export type { ReasoningTokenProvenance } from "./status/index.js";
|
|
73
98
|
|
|
74
99
|
export interface ChatPanelTurnUsage {
|
|
75
100
|
inputTokens: number;
|
|
@@ -139,8 +164,13 @@ type ToolSegment = {
|
|
|
139
164
|
settledWithoutResult?: boolean | undefined;
|
|
140
165
|
/** True when the finished result was an error. Meaningful only after `finished`. */
|
|
141
166
|
isError: boolean;
|
|
142
|
-
/**
|
|
143
|
-
|
|
167
|
+
/**
|
|
168
|
+
* The operator's explicit fold for this block, or none. The effective state
|
|
169
|
+
* is this override when set, else what the transcript detail policy gives
|
|
170
|
+
* the call (through the tool's presentation once it has finished). Cleared
|
|
171
|
+
* when `/output` changes and when a session is switched in.
|
|
172
|
+
*/
|
|
173
|
+
fold?: FoldOverride;
|
|
144
174
|
/** Wall-clock start time captured by the chat panel for live duration display. */
|
|
145
175
|
startedAtMs?: number;
|
|
146
176
|
/** Completed call duration in milliseconds (event-supplied or measured locally). */
|
|
@@ -151,7 +181,8 @@ type ToolSegment = {
|
|
|
151
181
|
* Latest cumulative Pi result from `tool_execution_update`, including the
|
|
152
182
|
* display content and structured progress details. Cleared
|
|
153
183
|
* back to `undefined` on `tool_execution_end` so the finished `result`
|
|
154
|
-
* takes over. Only consumed
|
|
184
|
+
* takes over. Only consumed while the call is in flight and its effective
|
|
185
|
+
* state is expanded. The explicit
|
|
155
186
|
* `| undefined` is required under `exactOptionalPropertyTypes: true` so
|
|
156
187
|
* the clear path can re-assign `undefined` without a `delete`.
|
|
157
188
|
*/
|
|
@@ -172,6 +203,12 @@ type ToolSegment = {
|
|
|
172
203
|
* was refused and leaves the operator no way to learn why.
|
|
173
204
|
*/
|
|
174
205
|
blockReason?: string | undefined;
|
|
206
|
+
/**
|
|
207
|
+
* Working-set eviction reason for a replayed result whose body the
|
|
208
|
+
* projection has replaced. Set only on rehydrate, from the folded ledger;
|
|
209
|
+
* a live call is never evicted while it is still being rendered.
|
|
210
|
+
*/
|
|
211
|
+
evictedReason?: string | undefined;
|
|
175
212
|
/** View-only marker: historical calls render mutation diffs without live color. */
|
|
176
213
|
replayed?: true;
|
|
177
214
|
};
|
|
@@ -186,8 +223,32 @@ type ErrorSegment = {
|
|
|
186
223
|
kind: "error";
|
|
187
224
|
text: string;
|
|
188
225
|
};
|
|
189
|
-
|
|
190
|
-
|
|
226
|
+
/**
|
|
227
|
+
* One stretch of reasoning, in stream order with the text and tool segments
|
|
228
|
+
* around it. A turn used to hold one `thinking` string, so reasoning could only
|
|
229
|
+
* ever render in one place (above everything or below everything) and a
|
|
230
|
+
* turn that thought, wrote, thought again, called a tool, and thought once more
|
|
231
|
+
* had its reasoning pinned at the tail while the prose streamed in above it.
|
|
232
|
+
* Each stretch is its own segment now: it renders where it happened, and the
|
|
233
|
+
* one still open at the tail is the live indicator.
|
|
234
|
+
*/
|
|
235
|
+
type ThinkingSegment = {
|
|
236
|
+
kind: "thinking";
|
|
237
|
+
text: string;
|
|
238
|
+
/** Closed by the first text, tool, or message_end that follows it. */
|
|
239
|
+
finalized: boolean;
|
|
240
|
+
/** Panel clock when the first delta of this stretch arrived. */
|
|
241
|
+
startedAtMs?: number;
|
|
242
|
+
/** The operator's explicit fold for this stretch, or none. */
|
|
243
|
+
fold?: FoldOverride;
|
|
244
|
+
};
|
|
245
|
+
type AssistantSegment = TextSegment | ToolSegment | ErrorSegment | ThinkingSegment;
|
|
246
|
+
/**
|
|
247
|
+
* A caller-rendered block receives the frame's transcript detail policy so a
|
|
248
|
+
* block that owns fold state (the operator's `!` bash row) resolves it the same
|
|
249
|
+
* way the panel resolves a model call. Blocks that ignore it are unaffected.
|
|
250
|
+
*/
|
|
251
|
+
type ReplayBlockRenderer = (width: number, detail: TranscriptDetailPolicy) => string[];
|
|
191
252
|
type AssistantStatusLine = { phase: StatusPhase; verb: string; toneHint: VerbRender["toneHint"] };
|
|
192
253
|
|
|
193
254
|
type TranscriptEntry =
|
|
@@ -197,23 +258,13 @@ type TranscriptEntry =
|
|
|
197
258
|
role: "assistant";
|
|
198
259
|
segments: AssistantSegment[];
|
|
199
260
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* settles it collapses to a static `Thinking...` marker (folded) or
|
|
206
|
-
* a head-anchored rail (expanded), mirroring the pi-coding-agent
|
|
207
|
-
* reference which streams thinking from the partial message.
|
|
208
|
-
*/
|
|
209
|
-
thinking: string;
|
|
210
|
-
/**
|
|
211
|
-
* Whether the thinking block renders as the full body (true) or
|
|
212
|
-
* the one-line dim marker (false/undefined). Toggled by
|
|
213
|
-
* `toggleLastThinking()`. New thinking inherits the panel-level
|
|
214
|
-
* visibility mode until Ctrl+T toggles it again.
|
|
261
|
+
* `segments.length` when the current model call began, so `message_end`
|
|
262
|
+
* can tell which segments belong to the message it is settling. A
|
|
263
|
+
* provider that delivers thinking only in the final message (no
|
|
264
|
+
* `thinking_delta`) gets its thinking segment inserted here, ahead of
|
|
265
|
+
* the text the same message produced, rather than at the tail.
|
|
215
266
|
*/
|
|
216
|
-
|
|
267
|
+
messageStartSegmentIndex?: number | undefined;
|
|
217
268
|
pending: boolean;
|
|
218
269
|
statusLine?: AssistantStatusLine | null | undefined;
|
|
219
270
|
isError: boolean;
|
|
@@ -221,11 +272,11 @@ type TranscriptEntry =
|
|
|
221
272
|
}
|
|
222
273
|
/**
|
|
223
274
|
* A dispatched worker's attributed block. The panel owns only the fold
|
|
224
|
-
*
|
|
225
|
-
* streaming delta reaches the screen without copying the entry per frame.
|
|
275
|
+
* override; `state` is the live object the worker-stream reducer mutates, so
|
|
276
|
+
* a streaming delta reaches the screen without copying the entry per frame.
|
|
226
277
|
* The panel is told when that happened through `applyWorkerState`.
|
|
227
278
|
*/
|
|
228
|
-
| { role: "worker"; state: WorkerEntryState;
|
|
279
|
+
| { role: "worker"; state: WorkerEntryState; fold?: FoldOverride }
|
|
229
280
|
/**
|
|
230
281
|
* A block the caller renders itself. Most are settled the moment they are
|
|
231
282
|
* appended, but a few (the operator's `!` bash row) keep mutating the state
|
|
@@ -233,10 +284,30 @@ type TranscriptEntry =
|
|
|
233
284
|
* declares `isLive`, which keeps it out of the frozen prefix and keeps the
|
|
234
285
|
* panel's time-keyed tick running while it is unsettled.
|
|
235
286
|
*/
|
|
236
|
-
| {
|
|
287
|
+
| {
|
|
288
|
+
role: "replayBlock";
|
|
289
|
+
renderBlock: ReplayBlockRenderer;
|
|
290
|
+
isLive?: (() => boolean) | undefined;
|
|
291
|
+
fold?: ReplayBlockFoldControl | undefined;
|
|
292
|
+
};
|
|
237
293
|
|
|
238
294
|
type WorkerTranscriptEntry = Extract<TranscriptEntry, { role: "worker" }>;
|
|
239
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Fold state a caller-rendered block owns. The panel never stores it: the
|
|
298
|
+
* block's closure reads it when rendering, so the panel only needs to resolve
|
|
299
|
+
* and flip it when the operator uses an expand/collapse key, and to clear it
|
|
300
|
+
* when `/output` changes. Same tri-state as a tool segment: an override, or
|
|
301
|
+
* none, over the fold the policy gives the block.
|
|
302
|
+
*/
|
|
303
|
+
export interface ReplayBlockFoldControl {
|
|
304
|
+
/** The fold the policy gives this block when no override is set. */
|
|
305
|
+
policyFold(detail: TranscriptDetailPolicy): Fold;
|
|
306
|
+
/** The operator's override, or none. */
|
|
307
|
+
fold(): FoldOverride;
|
|
308
|
+
setFold(fold: FoldOverride): void;
|
|
309
|
+
}
|
|
310
|
+
|
|
240
311
|
export interface ChatPanel extends Component {
|
|
241
312
|
appendUser(text: string): void;
|
|
242
313
|
/**
|
|
@@ -244,7 +315,7 @@ export interface ChatPanel extends Component {
|
|
|
244
315
|
* that keeps changing after the append, so the panel keeps re-rendering it
|
|
245
316
|
* instead of treating the first frame as final.
|
|
246
317
|
*/
|
|
247
|
-
appendReplayBlock(renderBlock: ReplayBlockRenderer, isLive?: () => boolean): void;
|
|
318
|
+
appendReplayBlock(renderBlock: ReplayBlockRenderer, isLive?: () => boolean, fold?: ReplayBlockFoldControl): void;
|
|
248
319
|
applyEvent(event: ChatLoopEvent): void;
|
|
249
320
|
/** Mark a just-rehydrated tool segment so its mutation diff remains plain. */
|
|
250
321
|
markToolReplayed?(toolCallId: string): void;
|
|
@@ -262,24 +333,36 @@ export interface ChatPanel extends Component {
|
|
|
262
333
|
*/
|
|
263
334
|
workerStates(): ReadonlyArray<WorkerEntryState>;
|
|
264
335
|
setStatusLine(line: AssistantStatusLine | null): void;
|
|
336
|
+
/**
|
|
337
|
+
* Publish the live run tally's reasoning projection. The pending entry's
|
|
338
|
+
* tail line reads this; passing null returns it to unmeasured, which is what
|
|
339
|
+
* an idle or just-started turn is.
|
|
340
|
+
*/
|
|
341
|
+
setLiveReasoning(view: ReasoningUsageView | null): void;
|
|
342
|
+
/**
|
|
343
|
+
* Flip the newest foldable block (tool call, worker card, or fold-owning
|
|
344
|
+
* replay block) away from its effective state. The flip is an override
|
|
345
|
+
* over the transcript detail policy: under `/output verbose` it folds an
|
|
346
|
+
* open block, under `/output minimal` it opens a folded one.
|
|
347
|
+
*/
|
|
265
348
|
toggleLastToolExpanded(): boolean;
|
|
349
|
+
/** Set an explicit override on every tool, worker, and fold-owning block at once. */
|
|
266
350
|
toggleAllToolsExpanded(): boolean;
|
|
267
351
|
/**
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
352
|
+
* Drop every operator override so each block returns to what the transcript
|
|
353
|
+
* detail policy gives it. `/output` changes and session switches do this:
|
|
354
|
+
* the operator asked for a new baseline, and what they had opened belonged
|
|
355
|
+
* to the transcript they left. Idempotent.
|
|
272
356
|
*/
|
|
273
|
-
|
|
357
|
+
clearFoldOverrides(): void;
|
|
274
358
|
/**
|
|
275
|
-
* Flip thinking
|
|
276
|
-
* and the full rail-prefixed body
|
|
277
|
-
*
|
|
278
|
-
* like a transcript-level thinking visibility toggle.
|
|
359
|
+
* Flip the newest thinking stretch between the one-line dim marker
|
|
360
|
+
* and the full rail-prefixed body, as an override over the policy. A turn
|
|
361
|
+
* with no thinking yet is left alone; a new stretch inherits the policy.
|
|
279
362
|
*/
|
|
280
363
|
toggleLastThinking(): boolean;
|
|
281
364
|
toggleAllThinking(): boolean;
|
|
282
|
-
/**
|
|
365
|
+
/** Whether live thinking would render open this frame, which presentation pacing consults. */
|
|
283
366
|
isThinkingExpanded(): boolean;
|
|
284
367
|
/** Toggle whether expanded live tool bodies include cumulative partial output. */
|
|
285
368
|
toggleLiveToolOutput(): boolean;
|
|
@@ -360,39 +443,42 @@ function extractAssistantThinking(message: unknown): string {
|
|
|
360
443
|
.join("");
|
|
361
444
|
}
|
|
362
445
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
446
|
+
/**
|
|
447
|
+
* The panel's view of one assistant message's spend, folded through the same
|
|
448
|
+
* `foldMessageIntoRunTally` the status machine uses. The panel used to
|
|
449
|
+
* re-derive reasoning here with its own provider-lookup-then-estimate rule, so
|
|
450
|
+
* the transcript receipt and the footer could report the same turn differently.
|
|
451
|
+
*/
|
|
367
452
|
function assistantUsage(message: unknown): ChatPanelTurnUsage | undefined {
|
|
368
453
|
if (!message || typeof message !== "object" || (message as { role?: unknown }).role !== "assistant") return undefined;
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
const thinking = extractAssistantThinking(message);
|
|
372
|
-
const reportedReasoning = usage ? extractReasoningTokens(usage) : null;
|
|
373
|
-
const estimatedReasoning = reportedReasoning === null ? estimateReasoningTextTokens(thinking) : null;
|
|
374
|
-
const inputTokens = finiteToken(usage?.input);
|
|
375
|
-
const outputTokens = finiteToken(usage?.output);
|
|
376
|
-
const cacheReadTokens = finiteToken(usage?.cacheRead);
|
|
377
|
-
const cacheWriteTokens = finiteToken(usage?.cacheWrite);
|
|
454
|
+
const tally = foldMessageIntoRunTally(emptyRunTally(), message as AgentMessage);
|
|
455
|
+
const reasoning = reasoningFromTally(tally);
|
|
378
456
|
if (
|
|
379
|
-
inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens === 0 &&
|
|
380
|
-
|
|
381
|
-
estimatedReasoning === null
|
|
457
|
+
tally.inputTokens + tally.outputTokens + tally.cacheReadTokens + tally.cacheWriteTokens === 0 &&
|
|
458
|
+
reasoning.provenance === "unmeasured"
|
|
382
459
|
) {
|
|
383
460
|
return undefined;
|
|
384
461
|
}
|
|
385
|
-
const turnUsage: ChatPanelTurnUsage = {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
462
|
+
const turnUsage: ChatPanelTurnUsage = {
|
|
463
|
+
inputTokens: tally.inputTokens,
|
|
464
|
+
outputTokens: tally.outputTokens,
|
|
465
|
+
cacheReadTokens: tally.cacheReadTokens,
|
|
466
|
+
cacheWriteTokens: tally.cacheWriteTokens,
|
|
467
|
+
modelCalls: 1,
|
|
468
|
+
};
|
|
469
|
+
if (reasoning.provenance !== "unmeasured") {
|
|
470
|
+
turnUsage.reasoningTokens = reasoning.tokens;
|
|
471
|
+
turnUsage.reasoningTokenProvenance = reasoning.provenance;
|
|
392
472
|
}
|
|
393
473
|
return turnUsage;
|
|
394
474
|
}
|
|
395
475
|
|
|
476
|
+
/** Settled-turn adapter onto the shared projection; the panel's own summary shape. */
|
|
477
|
+
function reasoningFromTurnUsage(usage: ChatPanelTurnUsage | undefined): ReasoningUsageView {
|
|
478
|
+
if (!usage || usage.reasoningTokenProvenance === undefined) return UNMEASURED_REASONING;
|
|
479
|
+
return { tokens: Math.max(0, usage.reasoningTokens ?? 0), provenance: usage.reasoningTokenProvenance };
|
|
480
|
+
}
|
|
481
|
+
|
|
396
482
|
function aggregateAssistantUsage(messages: unknown): ChatPanelTurnUsage | undefined {
|
|
397
483
|
if (!Array.isArray(messages)) return undefined;
|
|
398
484
|
const total: ChatPanelTurnUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
@@ -458,6 +544,24 @@ function scopeTerminalErrorAfterSuccessfulTool(
|
|
|
458
544
|
return `[error] ${modelFailure}${detachedDispatchSucceeded ? "; detached runs continue" : ""}`;
|
|
459
545
|
}
|
|
460
546
|
|
|
547
|
+
function hasThinking(entry: Extract<TranscriptEntry, { role: "assistant" }>): boolean {
|
|
548
|
+
return entry.segments.some((seg) => seg.kind === "thinking" && seg.text.length > 0);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** The thinking segment still receiving deltas, which is always the tail. */
|
|
552
|
+
function openThinkingSegment(entry: Extract<TranscriptEntry, { role: "assistant" }>): ThinkingSegment | null {
|
|
553
|
+
const tail = entry.segments[entry.segments.length - 1];
|
|
554
|
+
return tail?.kind === "thinking" && !tail.finalized ? tail : null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Index of the last thinking segment, which is where a settled turn's count chip rides. */
|
|
558
|
+
function lastThinkingIndex(entry: Extract<TranscriptEntry, { role: "assistant" }>): number {
|
|
559
|
+
for (let index = entry.segments.length - 1; index >= 0; index -= 1) {
|
|
560
|
+
if (entry.segments[index]?.kind === "thinking") return index;
|
|
561
|
+
}
|
|
562
|
+
return -1;
|
|
563
|
+
}
|
|
564
|
+
|
|
461
565
|
function hasVisibleOutput(entry: Extract<TranscriptEntry, { role: "assistant" }>): boolean {
|
|
462
566
|
for (const seg of entry.segments) {
|
|
463
567
|
if (seg.kind === "tool") return true;
|
|
@@ -590,49 +694,60 @@ function hangProseLines(lines: string[], firstPrefix?: string): string[] {
|
|
|
590
694
|
*/
|
|
591
695
|
const THINKING_HIDDEN_LABEL = `Thinking${GLYPH.ellipsis}`;
|
|
592
696
|
const THINKING_LINE_LIMIT = 12;
|
|
593
|
-
const REASONING_CHARS_PER_TOKEN = 4;
|
|
594
697
|
|
|
595
|
-
function
|
|
596
|
-
return
|
|
698
|
+
function dimLine(text: string, width: number): string {
|
|
699
|
+
return `${DIM}${truncateToWidth(text, Math.max(1, width), GLYPH.ellipsis, false)}${RESET}`;
|
|
597
700
|
}
|
|
598
701
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
702
|
+
/**
|
|
703
|
+
* A closed thinking stretch's folded marker, in place in the segment order. The
|
|
704
|
+
* turn's count chip rides on the last marker of a settled turn (`view` is
|
|
705
|
+
* unmeasured everywhere else), and comes from the settled usage, never from
|
|
706
|
+
* measuring the excerpt the panel happens to be holding.
|
|
707
|
+
*/
|
|
708
|
+
function renderSettledThinkingMarker(view: ReasoningUsageView, width: number): string {
|
|
709
|
+
const chip = formatReasoningChip(view, compactReasoningTokens);
|
|
710
|
+
return dimLine(
|
|
711
|
+
chip === null ? THINKING_HIDDEN_LABEL : `${THINKING_HIDDEN_LABEL} · ${chip} ${formatReasoningLabel(view)}`,
|
|
712
|
+
width,
|
|
713
|
+
);
|
|
607
714
|
}
|
|
608
715
|
|
|
609
716
|
/**
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
717
|
+
* The live turn's one reasoning line, rendered where the open thinking segment
|
|
718
|
+
* sits, which is the tail of the entry by construction: the first text, tool,
|
|
719
|
+
* or message_end closes it. Anchoring reasoning at the head put it above every
|
|
720
|
+
* streamed segment, so on a long turn the only progress indicator scrolled off
|
|
721
|
+
* the top; pinning one line at the tail put it below prose that streamed in
|
|
722
|
+
* after the model had already moved on, so the transcript read out of order.
|
|
723
|
+
*
|
|
724
|
+
* The count is whatever the run tally has folded so far, so between model calls
|
|
725
|
+
* the line states elapsed and nothing else. Visible thinking text is never
|
|
726
|
+
* counted: that number moved with how much reasoning the provider chose to
|
|
727
|
+
* display, not with what the turn spent.
|
|
728
|
+
*/
|
|
729
|
+
function renderLiveReasoningLine(view: ReasoningUsageView, elapsedMs: number | undefined, width: number): string {
|
|
730
|
+
const chip = formatReasoningChip(view, compactReasoningTokens);
|
|
731
|
+
const head = chip === null ? THINKING_HIDDEN_LABEL : `Thinking · ${chip} ${formatReasoningLabel(view)}`;
|
|
732
|
+
const seconds = elapsedMs === undefined ? 0 : Math.floor(Math.max(0, elapsedMs) / 1000);
|
|
733
|
+
return dimLine(seconds > 0 ? `${head} · ${seconds}s` : head, width);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Render the expanded thinking body: the text dimmed behind a dim `│ ` rail,
|
|
738
|
+
* capped at `THINKING_LINE_LIMIT` lines. A streaming turn keeps the tail (the
|
|
739
|
+
* reasoning still arriving); a settled one keeps the head with a
|
|
740
|
+
* `... N more lines hidden` overflow message. Mirrors the tool toggle's
|
|
614
741
|
* lab-notebook minimalism: no colored glyphs, no boxes.
|
|
615
742
|
*/
|
|
616
|
-
function
|
|
617
|
-
thinking: string,
|
|
618
|
-
expanded: boolean,
|
|
619
|
-
width: number,
|
|
620
|
-
streaming: boolean,
|
|
621
|
-
usage?: ChatPanelTurnUsage,
|
|
622
|
-
): string[] {
|
|
743
|
+
function renderThinkingRail(thinking: string, width: number, streaming: boolean, unbounded = false): string[] {
|
|
623
744
|
if (thinking.length === 0) return [];
|
|
624
|
-
const dimWrap = (s: string): string => `${DIM}${s}${RESET}`;
|
|
625
|
-
if (!expanded) {
|
|
626
|
-
const lineBudget = Math.max(1, width);
|
|
627
|
-
const display = reasoningDisplay(usage, thinking);
|
|
628
|
-
const label = streaming
|
|
629
|
-
? `Thinking (${display.tokens} tokens)${display.marker === "provider-reported" ? "" : " ≈ estimated"}…`
|
|
630
|
-
: THINKING_HIDDEN_LABEL;
|
|
631
|
-
return [dimWrap(truncateToWidth(label, lineBudget, GLYPH.ellipsis, false))];
|
|
632
|
-
}
|
|
633
745
|
const splitLines = thinking.split("\n");
|
|
634
746
|
let visible: string[];
|
|
635
|
-
if (
|
|
747
|
+
if (unbounded) {
|
|
748
|
+
// /export reproduces the whole transcript; the live cap is a screen budget.
|
|
749
|
+
visible = splitLines;
|
|
750
|
+
} else if (streaming) {
|
|
636
751
|
if (splitLines.length > THINKING_LINE_LIMIT) {
|
|
637
752
|
const hiddenCount = splitLines.length - THINKING_LINE_LIMIT;
|
|
638
753
|
visible = [`… ${hiddenCount} earlier lines hidden`, ...splitLines.slice(-THINKING_LINE_LIMIT)];
|
|
@@ -664,9 +779,13 @@ function renderThinkingLines(
|
|
|
664
779
|
* reported 717676 input tokens against a 500k window; it was 65 calls of about
|
|
665
780
|
* 11k, which the preceding one-call turns had already shown.
|
|
666
781
|
*/
|
|
667
|
-
function renderTurnUsageLine(
|
|
668
|
-
|
|
669
|
-
|
|
782
|
+
function renderTurnUsageLine(
|
|
783
|
+
usage: ChatPanelTurnUsage,
|
|
784
|
+
width: number,
|
|
785
|
+
receipt: TranscriptDetailPolicy["receipt"],
|
|
786
|
+
): string[] {
|
|
787
|
+
if (receipt === "none") return [];
|
|
788
|
+
if (receipt === "compact") {
|
|
670
789
|
const receipt = truncateToWidth(
|
|
671
790
|
` turn · in ${usage.inputTokens} · out ${usage.outputTokens}`,
|
|
672
791
|
width,
|
|
@@ -686,9 +805,10 @@ function renderTurnUsageLine(usage: ChatPanelTurnUsage, width: number, verbosity
|
|
|
686
805
|
// provenance of zero, and at narrow widths `reasoning 0 provider` orphaned
|
|
687
806
|
// the word `provider` on its own line. Zero suppresses the whole suffix, the
|
|
688
807
|
// same rule the caveat below already follows.
|
|
808
|
+
const view = reasoningFromTurnUsage(usage);
|
|
689
809
|
const reason =
|
|
690
|
-
|
|
691
|
-
? ` reasoning ${
|
|
810
|
+
view.tokens > 0 && view.provenance !== "unmeasured"
|
|
811
|
+
? ` reasoning ${view.provenance === "provider" ? "" : "≈"}${view.tokens} ${formatReasoningLabel(view)}`
|
|
692
812
|
: "";
|
|
693
813
|
const cache =
|
|
694
814
|
usage.cacheReadTokens > 0 || usage.cacheWriteTokens > 0
|
|
@@ -697,10 +817,7 @@ function renderTurnUsageLine(usage: ChatPanelTurnUsage, width: number, verbosity
|
|
|
697
817
|
// The caveat is about reasoning text the panel displayed. A turn that spent
|
|
698
818
|
// no reasoning tokens displayed none, so appending it there warned about
|
|
699
819
|
// something absent and cost a wrapped line per turn at narrow widths.
|
|
700
|
-
const caveat =
|
|
701
|
-
usage.reasoningTokens !== undefined && usage.reasoningTokens > 0
|
|
702
|
-
? " · reasoning text is a UI excerpt, not a verification"
|
|
703
|
-
: "";
|
|
820
|
+
const caveat = view.tokens > 0 ? " · reasoning text is a UI excerpt, not a verification" : "";
|
|
704
821
|
return wrapTextWithAnsi(
|
|
705
822
|
`${DIM} turn · in ${usage.inputTokens}${calls} · out ${usage.outputTokens}${cache}${reason}${caveat}${RESET}`,
|
|
706
823
|
width,
|
|
@@ -717,6 +834,22 @@ function styleStatusVerb(text: string, toneHint: VerbRender["toneHint"]): string
|
|
|
717
834
|
return `${DIM}${text}${RESET}`;
|
|
718
835
|
}
|
|
719
836
|
|
|
837
|
+
/**
|
|
838
|
+
* The fold the policy gives a tool segment this frame: the running-tool rule
|
|
839
|
+
* while in flight, the tool body rule (through the tool's own presentation)
|
|
840
|
+
* once finished, and the error rule for a finished failure.
|
|
841
|
+
*/
|
|
842
|
+
function policySegmentFold(seg: ToolSegment, detail: TranscriptDetailPolicy): Fold {
|
|
843
|
+
if (!seg.finished) return policyRunningToolFold(detail);
|
|
844
|
+
if (seg.isError && detail.errors === "body") return "expanded";
|
|
845
|
+
return policyToolFold(detail, toolResultPresentationPolicy(seg.result) ?? toolPresentationPolicy(seg.name, seg.args));
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** Effective state of a tool segment: the operator's override, else the policy. */
|
|
849
|
+
function toolSegmentExpanded(seg: ToolSegment, detail: TranscriptDetailPolicy): boolean {
|
|
850
|
+
return resolveFold(seg.fold, policySegmentFold(seg, detail)) === "expanded";
|
|
851
|
+
}
|
|
852
|
+
|
|
720
853
|
function renderToolSegmentLines(
|
|
721
854
|
seg: ToolSegment,
|
|
722
855
|
width: number,
|
|
@@ -724,11 +857,11 @@ function renderToolSegmentLines(
|
|
|
724
857
|
latestHintToolId: string | null,
|
|
725
858
|
nowMs: number,
|
|
726
859
|
unboundedToolBodies: boolean,
|
|
727
|
-
|
|
860
|
+
detail: TranscriptDetailPolicy,
|
|
728
861
|
liveToolOutput: boolean,
|
|
729
862
|
): string[] {
|
|
730
863
|
const hintKey = seg.id === latestHintToolId ? expandKey : undefined;
|
|
731
|
-
const expanded =
|
|
864
|
+
const expanded = toolSegmentExpanded(seg, detail);
|
|
732
865
|
const elapsedMs = seg.startedAtMs !== undefined ? Math.max(0, rawDurationMs(seg.startedAtMs, nowMs)) : undefined;
|
|
733
866
|
const phase: "forming" | "ready" | "running" = seg.executionStarted
|
|
734
867
|
? "running"
|
|
@@ -758,10 +891,15 @@ function renderToolSegmentLines(
|
|
|
758
891
|
resultSummary: seg.resultSummary,
|
|
759
892
|
outcome: seg.settlement,
|
|
760
893
|
blockReason: seg.blockReason,
|
|
894
|
+
evictedReason: seg.evictedReason,
|
|
761
895
|
}
|
|
762
896
|
: { toolCallId: seg.id, toolName: seg.name, args: seg.args, elapsedMs, phase },
|
|
763
897
|
width,
|
|
764
898
|
hintKey,
|
|
899
|
+
{
|
|
900
|
+
diffStyle: seg.replayed === true ? "plain" : "color",
|
|
901
|
+
foldedExtras: detail.toolBody === "folded" ? "none" : "per-tool",
|
|
902
|
+
},
|
|
765
903
|
);
|
|
766
904
|
}
|
|
767
905
|
if (!seg.finished) {
|
|
@@ -786,6 +924,7 @@ function renderToolSegmentLines(
|
|
|
786
924
|
resultSummary: seg.resultSummary,
|
|
787
925
|
outcome: seg.settlement,
|
|
788
926
|
blockReason: seg.blockReason,
|
|
927
|
+
evictedReason: seg.evictedReason,
|
|
789
928
|
},
|
|
790
929
|
width,
|
|
791
930
|
{ unbounded: unboundedToolBodies, diffStyle: seg.replayed === true ? "plain" : "color" },
|
|
@@ -793,14 +932,17 @@ function renderToolSegmentLines(
|
|
|
793
932
|
}
|
|
794
933
|
|
|
795
934
|
/**
|
|
796
|
-
* Whether a worker block draws its one-line card this frame
|
|
797
|
-
*
|
|
798
|
-
*
|
|
935
|
+
* Whether a worker block draws its one-line card this frame: the operator's
|
|
936
|
+
* override when set, else the policy's worker rule, which under the balanced
|
|
937
|
+
* level is the origin default (a run the model asked for folds).
|
|
799
938
|
*/
|
|
800
|
-
function workerEntryFolded(entry: WorkerTranscriptEntry,
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
939
|
+
function workerEntryFolded(entry: WorkerTranscriptEntry, detail: TranscriptDetailPolicy): boolean {
|
|
940
|
+
return resolveFold(entry.fold, policyWorkerFold(detail, workerAskedByModel(entry.state))) === "folded";
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** Effective state of one thinking stretch: the operator's override, else the policy. */
|
|
944
|
+
function thinkingExpanded(segment: ThinkingSegment, detail: TranscriptDetailPolicy): boolean {
|
|
945
|
+
return resolveFold(segment.fold, policyThinkingFold(detail)) === "expanded";
|
|
804
946
|
}
|
|
805
947
|
|
|
806
948
|
function renderEntryLines(
|
|
@@ -811,11 +953,12 @@ function renderEntryLines(
|
|
|
811
953
|
latestFoldedWorkerId: string | null,
|
|
812
954
|
nowMs: number,
|
|
813
955
|
unboundedToolBodies: boolean,
|
|
814
|
-
|
|
956
|
+
detail: TranscriptDetailPolicy,
|
|
815
957
|
liveToolOutput: boolean,
|
|
958
|
+
liveReasoning: ReasoningUsageView,
|
|
816
959
|
): string[] {
|
|
817
960
|
if (entry.role === "replayBlock") {
|
|
818
|
-
return entry.renderBlock(width);
|
|
961
|
+
return entry.renderBlock(width, detail);
|
|
819
962
|
}
|
|
820
963
|
if (entry.role === "user") {
|
|
821
964
|
const contentWidth = Math.max(1, width - PROSE_GUTTER_WIDTH);
|
|
@@ -830,7 +973,7 @@ function renderEntryLines(
|
|
|
830
973
|
}
|
|
831
974
|
if (entry.role === "worker") {
|
|
832
975
|
return renderWorkerEntryLines(entry.state, width, {
|
|
833
|
-
folded: workerEntryFolded(entry,
|
|
976
|
+
folded: workerEntryFolded(entry, detail),
|
|
834
977
|
...(expandKey !== undefined && entry.state.assignmentId === latestFoldedWorkerId ? { expandKey } : {}),
|
|
835
978
|
unbounded: unboundedToolBodies,
|
|
836
979
|
});
|
|
@@ -839,38 +982,50 @@ function renderEntryLines(
|
|
|
839
982
|
// A mid-turn notice splits the transcript, so the events after it open a
|
|
840
983
|
// fresh entry that a stopped turn never fills; that entry used to reach the
|
|
841
984
|
// tail below and print a lone agent bubble under the notice.
|
|
842
|
-
if (
|
|
843
|
-
!entry.pending &&
|
|
844
|
-
entry.thinking.length === 0 &&
|
|
845
|
-
entry.turnUsage === undefined &&
|
|
846
|
-
!hasVisibleOutput(entry) &&
|
|
847
|
-
entry.segments.length === 0
|
|
848
|
-
) {
|
|
985
|
+
if (!entry.pending && entry.turnUsage === undefined && !hasVisibleOutput(entry) && entry.segments.length === 0) {
|
|
849
986
|
return [];
|
|
850
987
|
}
|
|
851
988
|
const lines: string[] = [];
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
//
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
// active so only one indicator shows (see `shouldRenderStatus` below).
|
|
859
|
-
if (entry.thinking.length > 0) {
|
|
860
|
-
lines.push(
|
|
861
|
-
...renderThinkingLines(
|
|
862
|
-
entry.thinking,
|
|
863
|
-
verbosity === "verbose" || (verbosity !== "minimal" && entry.expandedThinking === true),
|
|
864
|
-
width,
|
|
865
|
-
entry.pending,
|
|
866
|
-
entry.turnUsage,
|
|
867
|
-
),
|
|
868
|
-
);
|
|
869
|
-
}
|
|
989
|
+
// Reasoning renders in stream order, between the text and tool segments it
|
|
990
|
+
// came between. A closed stretch is a folded marker (or a head-anchored rail
|
|
991
|
+
// when expanded); the stretch still open while the turn is pending is the
|
|
992
|
+
// live indicator, with the tally's count and its own elapsed. The turn's
|
|
993
|
+
// settled count chip rides on the last marker once the turn has settled.
|
|
994
|
+
const chipIndex = entry.pending ? -1 : lastThinkingIndex(entry);
|
|
870
995
|
const clioPrefix = entry.isError ? CLIO_PREFIX_ERROR : CLIO_PREFIX;
|
|
871
996
|
const proseWidth = Math.max(1, width - PROSE_GUTTER_WIDTH);
|
|
872
997
|
let labeled = false;
|
|
873
|
-
|
|
998
|
+
let liveIndicatorShown = false;
|
|
999
|
+
let latestThinkingExpanded = policyThinkingFold(detail) === "expanded";
|
|
1000
|
+
for (let segIndex = 0; segIndex < entry.segments.length; segIndex += 1) {
|
|
1001
|
+
const seg = entry.segments[segIndex];
|
|
1002
|
+
if (seg === undefined) continue;
|
|
1003
|
+
if (seg.kind === "thinking") {
|
|
1004
|
+
if (seg.text.length === 0) continue;
|
|
1005
|
+
const thinkingExpandedNow = thinkingExpanded(seg, detail);
|
|
1006
|
+
latestThinkingExpanded = thinkingExpandedNow;
|
|
1007
|
+
const live = entry.pending && !seg.finalized;
|
|
1008
|
+
if (thinkingExpandedNow) lines.push(...renderThinkingRail(seg.text, width, live, unboundedToolBodies));
|
|
1009
|
+
if (live) {
|
|
1010
|
+
liveIndicatorShown = true;
|
|
1011
|
+
// The bare marker level states that the model is thinking and
|
|
1012
|
+
// nothing else: no count, no elapsed. An operator who opened the
|
|
1013
|
+
// stretch anyway gets the progress line under the rail.
|
|
1014
|
+
lines.push(
|
|
1015
|
+
detail.thinking === "marker" && !thinkingExpandedNow
|
|
1016
|
+
? dimLine(THINKING_HIDDEN_LABEL, width)
|
|
1017
|
+
: renderLiveReasoningLine(
|
|
1018
|
+
liveReasoning,
|
|
1019
|
+
seg.startedAtMs === undefined ? undefined : Math.max(0, nowMs - seg.startedAtMs),
|
|
1020
|
+
width,
|
|
1021
|
+
),
|
|
1022
|
+
);
|
|
1023
|
+
} else if (!thinkingExpandedNow) {
|
|
1024
|
+
const view = segIndex === chipIndex ? reasoningFromTurnUsage(entry.turnUsage) : UNMEASURED_REASONING;
|
|
1025
|
+
lines.push(renderSettledThinkingMarker(view, width));
|
|
1026
|
+
}
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
874
1029
|
if (seg.kind === "tool") {
|
|
875
1030
|
lines.push(
|
|
876
1031
|
...renderToolSegmentLines(
|
|
@@ -880,7 +1035,7 @@ function renderEntryLines(
|
|
|
880
1035
|
latestHintToolId,
|
|
881
1036
|
nowMs,
|
|
882
1037
|
unboundedToolBodies,
|
|
883
|
-
|
|
1038
|
+
detail,
|
|
884
1039
|
liveToolOutput,
|
|
885
1040
|
),
|
|
886
1041
|
);
|
|
@@ -913,21 +1068,30 @@ function renderEntryLines(
|
|
|
913
1068
|
lines.push(...hangProseLines(rendered));
|
|
914
1069
|
}
|
|
915
1070
|
}
|
|
916
|
-
|
|
1071
|
+
// A thinking stretch closes when text or a tool follows it so the historical
|
|
1072
|
+
// marker stays in stream order. The turn-level reasoning projection is still
|
|
1073
|
+
// live, though, and must remain beside the current tail instead of scrolling
|
|
1074
|
+
// away with that marker. An open tail stretch already rendered this line.
|
|
1075
|
+
if (entry.pending && hasThinking(entry) && !liveIndicatorShown) {
|
|
1076
|
+
lines.push(
|
|
1077
|
+
detail.thinking === "marker" && !latestThinkingExpanded
|
|
1078
|
+
? dimLine(THINKING_HIDDEN_LABEL, width)
|
|
1079
|
+
: renderLiveReasoningLine(liveReasoning, undefined, width),
|
|
1080
|
+
);
|
|
1081
|
+
liveIndicatorShown = true;
|
|
1082
|
+
}
|
|
1083
|
+
if (entry.turnUsage && !entry.pending) lines.push(...renderTurnUsageLine(entry.turnUsage, width, detail.receipt));
|
|
1084
|
+
// The open thinking segment's line is the one that speaks for reasoning while
|
|
1085
|
+
// the turn runs. The generic thinking verb is suppressed while it shows so
|
|
1086
|
+
// the entry never carries two indicators for the same thing.
|
|
917
1087
|
const shouldRenderStatus =
|
|
918
1088
|
entry.pending &&
|
|
919
1089
|
entry.statusLine !== null &&
|
|
920
1090
|
entry.statusLine !== undefined &&
|
|
921
1091
|
!(entry.statusLine.phase === "writing" && hasStreamingText(entry)) &&
|
|
922
|
-
!(entry.statusLine.phase === "thinking" &&
|
|
923
|
-
if (!labeled && !hasVisibleOutput(entry))
|
|
924
|
-
|
|
925
|
-
if (shouldRenderStatus) {
|
|
926
|
-
lines.push(
|
|
927
|
-
`${STATUS_INDENT}${styleStatusVerb(entry.statusLine?.verb ?? "", entry.statusLine?.toneHint ?? "muted")}`,
|
|
928
|
-
);
|
|
929
|
-
}
|
|
930
|
-
} else if (shouldRenderStatus) {
|
|
1092
|
+
!(entry.statusLine.phase === "thinking" && liveIndicatorShown);
|
|
1093
|
+
if (!labeled && !hasVisibleOutput(entry)) lines.push(clioPrefix.trimEnd());
|
|
1094
|
+
if (shouldRenderStatus) {
|
|
931
1095
|
lines.push(`${STATUS_INDENT}${styleStatusVerb(entry.statusLine?.verb ?? "", entry.statusLine?.toneHint ?? "muted")}`);
|
|
932
1096
|
}
|
|
933
1097
|
return lines;
|
|
@@ -941,8 +1105,14 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
941
1105
|
let cachedWidth: number | undefined;
|
|
942
1106
|
let cachedLines: string[] = [];
|
|
943
1107
|
let cachedExpandKey: string | undefined;
|
|
944
|
-
let
|
|
1108
|
+
let cachedDetail: TranscriptDetailPolicy | undefined;
|
|
945
1109
|
let cachedLiveToolOutput: boolean | undefined;
|
|
1110
|
+
/**
|
|
1111
|
+
* The verbosity the last frame rendered under, or null before any frame.
|
|
1112
|
+
* A change between frames is the operator asking for a new baseline
|
|
1113
|
+
* (`/output`, or Settings → Terminal), and every override goes with it.
|
|
1114
|
+
*/
|
|
1115
|
+
let lastVerbosity: OutputVerbosity | undefined | null = null;
|
|
946
1116
|
let cachedTick = 0;
|
|
947
1117
|
/**
|
|
948
1118
|
* Did the last executed render put a counting elapsed line on screen? It is
|
|
@@ -974,8 +1144,13 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
974
1144
|
* the render key changes.
|
|
975
1145
|
*/
|
|
976
1146
|
let frozen: { lines: string[]; through: number; key: string } | null = null;
|
|
977
|
-
let thinkingExpanded = false;
|
|
978
1147
|
let liveToolOutput = true;
|
|
1148
|
+
/**
|
|
1149
|
+
* The run tally's reasoning, projected. It is panel-level rather than
|
|
1150
|
+
* per-entry because only the pending tail entry ever renders it, and that
|
|
1151
|
+
* entry is by definition unfrozen and uncached.
|
|
1152
|
+
*/
|
|
1153
|
+
let liveReasoning: ReasoningUsageView = UNMEASURED_REASONING;
|
|
979
1154
|
const unboundedToolBodies = options.unboundedToolBodies === true;
|
|
980
1155
|
|
|
981
1156
|
const markDirty = (): void => {
|
|
@@ -1007,6 +1182,32 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1007
1182
|
|
|
1008
1183
|
const now = (): number => options.now?.() ?? Date.now();
|
|
1009
1184
|
|
|
1185
|
+
/** The policy for a frame or a keypress, from whatever the settings say right now. */
|
|
1186
|
+
const currentDetail = (): TranscriptDetailPolicy => transcriptDetail(options.getOutputVerbosity?.());
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Drop every operator override. Shared by the panel method and the
|
|
1190
|
+
* verbosity-change path in render, so both leave the same state behind.
|
|
1191
|
+
*/
|
|
1192
|
+
const dropFoldOverrides = (): void => {
|
|
1193
|
+
for (const entry of transcript) {
|
|
1194
|
+
if (entry.role === "replayBlock") {
|
|
1195
|
+
entry.fold?.setFold(undefined);
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (entry.role === "worker") {
|
|
1199
|
+
entry.fold = undefined;
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
if (entry.role !== "assistant") continue;
|
|
1203
|
+
for (const seg of entry.segments) {
|
|
1204
|
+
if (seg.kind === "tool" || seg.kind === "thinking") seg.fold = undefined;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
clearRenderCaches();
|
|
1208
|
+
markDirty();
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1010
1211
|
/**
|
|
1011
1212
|
* Force an in-flight tool segment to a settled error line. A call blocked at
|
|
1012
1213
|
* admission (loop guard, safety) or one whose `tool_execution_end` never
|
|
@@ -1069,8 +1270,6 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1069
1270
|
const entry: Extract<TranscriptEntry, { role: "assistant" }> = {
|
|
1070
1271
|
role: "assistant",
|
|
1071
1272
|
segments: [],
|
|
1072
|
-
thinking: "",
|
|
1073
|
-
expandedThinking: thinkingExpanded,
|
|
1074
1273
|
pending: false,
|
|
1075
1274
|
isError: false,
|
|
1076
1275
|
};
|
|
@@ -1078,9 +1277,34 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1078
1277
|
return entry;
|
|
1079
1278
|
};
|
|
1080
1279
|
|
|
1280
|
+
/**
|
|
1281
|
+
* Close the thinking stretch at the tail, if one is open. Anything that
|
|
1282
|
+
* follows reasoning in the stream (text, a tool call, the message settling)
|
|
1283
|
+
* ends that stretch; a later `thinking_delta` opens a new segment after it,
|
|
1284
|
+
* so the transcript keeps the order the model actually worked in.
|
|
1285
|
+
*/
|
|
1286
|
+
const closeOpenThinking = (entry: Extract<TranscriptEntry, { role: "assistant" }>): void => {
|
|
1287
|
+
const open = openThinkingSegment(entry);
|
|
1288
|
+
if (open === null) return;
|
|
1289
|
+
open.finalized = true;
|
|
1290
|
+
invalidateEntryCache(entry);
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
const appendThinkingDelta = (entry: Extract<TranscriptEntry, { role: "assistant" }>, delta: string): void => {
|
|
1294
|
+
if (delta.length === 0) return;
|
|
1295
|
+
invalidateEntryCache(entry);
|
|
1296
|
+
const open = openThinkingSegment(entry);
|
|
1297
|
+
if (open !== null) {
|
|
1298
|
+
open.text += delta;
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
entry.segments.push({ kind: "thinking", text: delta, finalized: false, startedAtMs: now() });
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1081
1304
|
const appendTextDelta = (entry: Extract<TranscriptEntry, { role: "assistant" }>, delta: string): void => {
|
|
1082
1305
|
if (delta.length === 0) return;
|
|
1083
1306
|
invalidateEntryCache(entry);
|
|
1307
|
+
closeOpenThinking(entry);
|
|
1084
1308
|
const tail = entry.segments[entry.segments.length - 1];
|
|
1085
1309
|
if (tail && tail.kind === "text" && !tail.finalized) {
|
|
1086
1310
|
tail.text += delta;
|
|
@@ -1090,15 +1314,22 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1090
1314
|
};
|
|
1091
1315
|
|
|
1092
1316
|
/**
|
|
1093
|
-
* Canonicalize
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1317
|
+
* Canonicalize the streamed text of a completed assistant message.
|
|
1318
|
+
*
|
|
1319
|
+
* The streamed text is wherever this message put it, not necessarily at the
|
|
1320
|
+
* tail: a message that thinks, writes, and thinks again leaves its text
|
|
1321
|
+
* behind a thinking segment. Looking only at the tail appended the message
|
|
1322
|
+
* text a second time under the reasoning marker, so the answer read twice.
|
|
1323
|
+
*
|
|
1324
|
+
* One streamed segment that is a prefix of the final text (the common case)
|
|
1325
|
+
* is overwritten in place and flipped to finalized so the next render pipes
|
|
1326
|
+
* it through Markdown. Several streamed segments (text split by reasoning)
|
|
1327
|
+
* are each finalized where they stand; the deltas already are the text.
|
|
1328
|
+
* When the message arrived fully formed with no deltas (non-streaming
|
|
1329
|
+
* path, synthetic notices, replay), a fresh finalized segment is appended.
|
|
1330
|
+
* `replaceTail` forces the overwrite for messages the chat loop rewrote
|
|
1331
|
+
* after streaming (locked-turn markup sanitation): the streamed text is dead
|
|
1332
|
+
* there, not a prefix.
|
|
1102
1333
|
*/
|
|
1103
1334
|
const canonicalizeMessageText = (
|
|
1104
1335
|
entry: Extract<TranscriptEntry, { role: "assistant" }>,
|
|
@@ -1106,15 +1337,36 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1106
1337
|
replaceTail = false,
|
|
1107
1338
|
): void => {
|
|
1108
1339
|
if (text.length === 0) return;
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1340
|
+
closeOpenThinking(entry);
|
|
1341
|
+
const messageStart = Math.min(entry.messageStartSegmentIndex ?? 0, entry.segments.length);
|
|
1342
|
+
const streamed: TextSegment[] = [];
|
|
1343
|
+
for (let index = messageStart; index < entry.segments.length; index += 1) {
|
|
1344
|
+
const segment = entry.segments[index];
|
|
1345
|
+
if (segment?.kind === "text" && !segment.finalized) streamed.push(segment);
|
|
1346
|
+
}
|
|
1347
|
+
const finalize = (segment: TextSegment, value: string): void => {
|
|
1348
|
+
segment.text = value;
|
|
1349
|
+
segment.finalized = true;
|
|
1113
1350
|
// The streaming wrap cache assumes append-only text. This is the one
|
|
1114
1351
|
// path that rewrites it wholesale, and finalized segments render through
|
|
1115
1352
|
// Markdown instead, so the cache is dead here either way.
|
|
1116
|
-
delete
|
|
1117
|
-
if (
|
|
1353
|
+
delete segment.wrapCache;
|
|
1354
|
+
if (segment.md) segment.md.setText(value);
|
|
1355
|
+
};
|
|
1356
|
+
if (replaceTail && streamed.length > 0) {
|
|
1357
|
+
const [first, ...rest] = streamed;
|
|
1358
|
+
if (first) finalize(first, text);
|
|
1359
|
+
for (const dead of rest) entry.segments.splice(entry.segments.indexOf(dead), 1);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
if (streamed.length === 1 && streamed[0] !== undefined) {
|
|
1363
|
+
const only = streamed[0];
|
|
1364
|
+
if (text.startsWith(only.text)) {
|
|
1365
|
+
finalize(only, text);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
} else if (streamed.length > 1) {
|
|
1369
|
+
for (const segment of streamed) finalize(segment, segment.text);
|
|
1118
1370
|
return;
|
|
1119
1371
|
}
|
|
1120
1372
|
entry.segments.push({ kind: "text", text, finalized: true });
|
|
@@ -1133,25 +1385,30 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1133
1385
|
};
|
|
1134
1386
|
|
|
1135
1387
|
/**
|
|
1136
|
-
* Who advertises the fold key this frame: the newest
|
|
1137
|
-
* the newest finished
|
|
1388
|
+
* Who advertises the fold key this frame: the newest worker card whose
|
|
1389
|
+
* effective state is folded, or the newest finished tool subline whose
|
|
1390
|
+
* effective state is folded with no worker card behind it. Effective means
|
|
1391
|
+
* override-or-policy, so the hint follows the block whatever the verbosity.
|
|
1138
1392
|
* One surface at most, because the key reaches the newest foldable thing of
|
|
1139
1393
|
* either kind, and a chord shown anywhere else would open something the
|
|
1140
1394
|
* operator was not looking at. An already-open newest card advertises
|
|
1141
1395
|
* nothing, since folding it again needs no invitation.
|
|
1142
1396
|
*/
|
|
1143
|
-
const expandHintOwner = (): { toolId: string | null; workerId: string | null } => {
|
|
1397
|
+
const expandHintOwner = (detail: TranscriptDetailPolicy): { toolId: string | null; workerId: string | null } => {
|
|
1144
1398
|
let workerMayOwn = true;
|
|
1145
1399
|
for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
|
|
1146
1400
|
const entry = transcript[entryIndex];
|
|
1147
1401
|
if (entry?.role === "worker") {
|
|
1148
|
-
return {
|
|
1402
|
+
return {
|
|
1403
|
+
toolId: null,
|
|
1404
|
+
workerId: workerMayOwn && workerEntryFolded(entry, detail) ? entry.state.assignmentId : null,
|
|
1405
|
+
};
|
|
1149
1406
|
}
|
|
1150
1407
|
if (entry?.role !== "assistant") continue;
|
|
1151
1408
|
for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
|
|
1152
1409
|
const seg = entry.segments[segIndex];
|
|
1153
1410
|
if (seg?.kind !== "tool") continue;
|
|
1154
|
-
if (seg.finished && !seg
|
|
1411
|
+
if (seg.finished && !toolSegmentExpanded(seg, detail)) return { toolId: seg.id, workerId: null };
|
|
1155
1412
|
workerMayOwn = false;
|
|
1156
1413
|
}
|
|
1157
1414
|
}
|
|
@@ -1187,9 +1444,12 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1187
1444
|
/** True when the entry renders at least one counting elapsed line this frame. */
|
|
1188
1445
|
const entryHasRunningTool = (entry: TranscriptEntry): boolean =>
|
|
1189
1446
|
(entry.role === "assistant" &&
|
|
1190
|
-
entry.segments.some(
|
|
1447
|
+
(entry.segments.some(
|
|
1191
1448
|
(segment) => segment.kind === "tool" && !segment.finished && segment.startedAtMs !== undefined,
|
|
1192
|
-
|
|
1449
|
+
// The live reasoning line counts its own elapsed, so a turn that is
|
|
1450
|
+
// only thinking still needs the time-keyed render key.
|
|
1451
|
+
) ||
|
|
1452
|
+
(entry.pending && openThinkingSegment(entry)?.startedAtMs !== undefined))) ||
|
|
1193
1453
|
(entry.role === "replayBlock" && entry.isLive?.() === true);
|
|
1194
1454
|
|
|
1195
1455
|
/**
|
|
@@ -1210,7 +1470,12 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1210
1470
|
const render = (width: number): string[] => {
|
|
1211
1471
|
const startedAt = performance.now();
|
|
1212
1472
|
const expandKey = resolveExpandKey();
|
|
1213
|
-
const verbosity = options.getOutputVerbosity?.()
|
|
1473
|
+
const verbosity = options.getOutputVerbosity?.();
|
|
1474
|
+
// A new verbosity is a new baseline: the operator's per-block overrides
|
|
1475
|
+
// were answers to the old one, so they go before the frame is built.
|
|
1476
|
+
if (lastVerbosity !== null && verbosity !== lastVerbosity) dropFoldOverrides();
|
|
1477
|
+
lastVerbosity = verbosity;
|
|
1478
|
+
const detail = transcriptDetail(verbosity);
|
|
1214
1479
|
const nowMs = now();
|
|
1215
1480
|
// `dirty` is set on mutation and never on a tick, so without time in the
|
|
1216
1481
|
// key a running tool's elapsed counter advanced only when something
|
|
@@ -1228,14 +1493,14 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1228
1493
|
!dirty &&
|
|
1229
1494
|
cachedWidth === width &&
|
|
1230
1495
|
cachedExpandKey === expandKey &&
|
|
1231
|
-
|
|
1496
|
+
cachedDetail === detail &&
|
|
1232
1497
|
cachedLiveToolOutput === liveToolOutput &&
|
|
1233
1498
|
cachedTick === tick
|
|
1234
1499
|
) {
|
|
1235
1500
|
options.onRenderMetrics?.({ durationMs: performance.now() - startedAt, cacheHit: true, entriesRendered: 0 });
|
|
1236
1501
|
return cachedLines;
|
|
1237
1502
|
}
|
|
1238
|
-
const { toolId: latestHintToolId, workerId: latestFoldedWorkerId } = expandHintOwner();
|
|
1503
|
+
const { toolId: latestHintToolId, workerId: latestFoldedWorkerId } = expandHintOwner(detail);
|
|
1239
1504
|
// The hint id is deliberately NOT part of the shared key: it changes on
|
|
1240
1505
|
// every finished collapsed tool, and keying every entry on it re-rendered
|
|
1241
1506
|
// the entire transcript per tool completion. Only the entry that contains
|
|
@@ -1244,7 +1509,7 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1244
1509
|
// bytes at every tick, and keying it on time would drop the entry cache
|
|
1245
1510
|
// and the frozen prefix ten times a second. Only the panel-level guard
|
|
1246
1511
|
// above is time-keyed, so a tick re-renders the live tail and nothing else.
|
|
1247
|
-
const baseKey = `${width}|${expandKey ?? ""}|${
|
|
1512
|
+
const baseKey = `${width}|${expandKey ?? ""}|${detail.toolBody}:${detail.runningTool}:${detail.thinking}:${detail.worker}:${detail.receipt}:${detail.errors}|${liveToolOutput}`;
|
|
1248
1513
|
const capacity = entryCacheCapacity();
|
|
1249
1514
|
if (frozen !== null && frozen.key !== baseKey) frozen = null;
|
|
1250
1515
|
const out: string[] = frozen === null ? [] : frozen.lines.slice();
|
|
@@ -1270,8 +1535,8 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1270
1535
|
const stacksOnPrevious =
|
|
1271
1536
|
entry.role === "worker" &&
|
|
1272
1537
|
previous?.role === "worker" &&
|
|
1273
|
-
workerEntryFolded(entry,
|
|
1274
|
-
workerEntryFolded(previous,
|
|
1538
|
+
workerEntryFolded(entry, detail) &&
|
|
1539
|
+
workerEntryFolded(previous, detail);
|
|
1275
1540
|
if (i > 0 && !stacksOnPrevious) out.push("");
|
|
1276
1541
|
const containsHint =
|
|
1277
1542
|
entryContainsHint(entry, latestHintToolId) ||
|
|
@@ -1293,8 +1558,9 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1293
1558
|
latestFoldedWorkerId,
|
|
1294
1559
|
nowMs,
|
|
1295
1560
|
unboundedToolBodies,
|
|
1296
|
-
|
|
1561
|
+
detail,
|
|
1297
1562
|
liveToolOutput,
|
|
1563
|
+
liveReasoning,
|
|
1298
1564
|
);
|
|
1299
1565
|
for (const line of renderedEntry) out.push(line);
|
|
1300
1566
|
if (cacheable) {
|
|
@@ -1317,7 +1583,7 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1317
1583
|
cachedLines = out;
|
|
1318
1584
|
cachedWidth = width;
|
|
1319
1585
|
cachedExpandKey = expandKey;
|
|
1320
|
-
|
|
1586
|
+
cachedDetail = detail;
|
|
1321
1587
|
cachedLiveToolOutput = liveToolOutput;
|
|
1322
1588
|
cachedTick = tick;
|
|
1323
1589
|
renderedRunningTool = sawRunningTool;
|
|
@@ -1331,8 +1597,8 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1331
1597
|
transcript.push({ role: "user", text });
|
|
1332
1598
|
markDirty();
|
|
1333
1599
|
},
|
|
1334
|
-
appendReplayBlock(renderBlock: ReplayBlockRenderer, isLive?: () => boolean): void {
|
|
1335
|
-
transcript.push({ role: "replayBlock", renderBlock, isLive });
|
|
1600
|
+
appendReplayBlock(renderBlock: ReplayBlockRenderer, isLive?: () => boolean, fold?: ReplayBlockFoldControl): void {
|
|
1601
|
+
transcript.push({ role: "replayBlock", renderBlock, isLive, fold });
|
|
1336
1602
|
markDirty();
|
|
1337
1603
|
},
|
|
1338
1604
|
applyWorkerState(state: WorkerEntryState): void {
|
|
@@ -1344,7 +1610,7 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1344
1610
|
markDirty();
|
|
1345
1611
|
return;
|
|
1346
1612
|
}
|
|
1347
|
-
const entry: WorkerTranscriptEntry = { role: "worker", state
|
|
1613
|
+
const entry: WorkerTranscriptEntry = { role: "worker", state };
|
|
1348
1614
|
workerEntries.set(state.assignmentId, entry);
|
|
1349
1615
|
const at = workerInsertionIndex(state);
|
|
1350
1616
|
if (at === null) {
|
|
@@ -1361,13 +1627,26 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1361
1627
|
return [...workerEntries.values()].map((entry) => entry.state);
|
|
1362
1628
|
},
|
|
1363
1629
|
toggleLastToolExpanded(): boolean {
|
|
1364
|
-
//
|
|
1630
|
+
// The key owns the newest foldable thing, whichever kind it is. A worker
|
|
1365
1631
|
// block the operator just watched land is what they mean by "expand
|
|
1366
|
-
// that", not the tool call two screens up that spawned it.
|
|
1632
|
+
// that", not the tool call two screens up that spawned it. Every flip
|
|
1633
|
+
// is an override away from the block's effective state, so the same
|
|
1634
|
+
// key opens a folded block under minimal and folds an open one under
|
|
1635
|
+
// verbose.
|
|
1636
|
+
const detail = currentDetail();
|
|
1367
1637
|
for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
|
|
1368
1638
|
const entry = transcript[entryIndex];
|
|
1639
|
+
// A caller-rendered block that owns fold state (the operator's own
|
|
1640
|
+
// `!` bash row) is foldable too, and it is usually the newest thing
|
|
1641
|
+
// on screen when the key is pressed.
|
|
1642
|
+
if (entry?.role === "replayBlock" && entry.fold !== undefined) {
|
|
1643
|
+
entry.fold.setFold(toggledFold(resolveFold(entry.fold.fold(), entry.fold.policyFold(detail))));
|
|
1644
|
+
clearRenderCaches();
|
|
1645
|
+
markDirty();
|
|
1646
|
+
return true;
|
|
1647
|
+
}
|
|
1369
1648
|
if (entry?.role === "worker") {
|
|
1370
|
-
entry.
|
|
1649
|
+
entry.fold = workerEntryFolded(entry, detail) ? "expanded" : "folded";
|
|
1371
1650
|
clearRenderCaches();
|
|
1372
1651
|
markDirty();
|
|
1373
1652
|
return true;
|
|
@@ -1376,7 +1655,7 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1376
1655
|
for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
|
|
1377
1656
|
const seg = entry.segments[segIndex];
|
|
1378
1657
|
if (seg?.kind !== "tool") continue;
|
|
1379
|
-
seg.
|
|
1658
|
+
seg.fold = toolSegmentExpanded(seg, detail) ? "folded" : "expanded";
|
|
1380
1659
|
clearRenderCaches();
|
|
1381
1660
|
markDirty();
|
|
1382
1661
|
return true;
|
|
@@ -1385,9 +1664,15 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1385
1664
|
return false;
|
|
1386
1665
|
},
|
|
1387
1666
|
toggleAllToolsExpanded(): boolean {
|
|
1667
|
+
const detail = currentDetail();
|
|
1388
1668
|
const tools: ToolSegment[] = [];
|
|
1389
1669
|
const workers: WorkerTranscriptEntry[] = [];
|
|
1670
|
+
const blocks: ReplayBlockFoldControl[] = [];
|
|
1390
1671
|
for (const entry of transcript) {
|
|
1672
|
+
if (entry.role === "replayBlock") {
|
|
1673
|
+
if (entry.fold !== undefined) blocks.push(entry.fold);
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1391
1676
|
if (entry.role === "worker") {
|
|
1392
1677
|
workers.push(entry);
|
|
1393
1678
|
continue;
|
|
@@ -1397,62 +1682,70 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1397
1682
|
if (seg.kind === "tool") tools.push(seg);
|
|
1398
1683
|
}
|
|
1399
1684
|
}
|
|
1400
|
-
if (tools.length === 0 && workers.length === 0) return false;
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1685
|
+
if (tools.length === 0 && workers.length === 0 && blocks.length === 0) return false;
|
|
1686
|
+
// One folded block anywhere means "open everything"; otherwise fold
|
|
1687
|
+
// everything. Either way every block gets an explicit override.
|
|
1688
|
+
const expand =
|
|
1689
|
+
tools.some((seg) => !toolSegmentExpanded(seg, detail)) ||
|
|
1690
|
+
workers.some((entry) => workerEntryFolded(entry, detail)) ||
|
|
1691
|
+
blocks.some((fold) => resolveFold(fold.fold(), fold.policyFold(detail)) === "folded");
|
|
1692
|
+
const next: Fold = expand ? "expanded" : "folded";
|
|
1693
|
+
for (const seg of tools) seg.fold = next;
|
|
1694
|
+
for (const entry of workers) entry.fold = next;
|
|
1695
|
+
for (const fold of blocks) fold.setFold(next);
|
|
1404
1696
|
clearRenderCaches();
|
|
1405
1697
|
markDirty();
|
|
1406
1698
|
return true;
|
|
1407
1699
|
},
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
if (entry.role === "worker") {
|
|
1411
|
-
// The settled view for a worker is its origin default, not
|
|
1412
|
-
// universally folded: the operator's own run is the one block
|
|
1413
|
-
// /run exists to show them.
|
|
1414
|
-
entry.folded = workerAskedByModel(entry.state);
|
|
1415
|
-
continue;
|
|
1416
|
-
}
|
|
1417
|
-
if (entry.role !== "assistant") continue;
|
|
1418
|
-
for (const seg of entry.segments) {
|
|
1419
|
-
if (seg.kind === "tool") seg.expanded = false;
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
clearRenderCaches();
|
|
1423
|
-
markDirty();
|
|
1700
|
+
clearFoldOverrides(): void {
|
|
1701
|
+
dropFoldOverrides();
|
|
1424
1702
|
},
|
|
1425
1703
|
toggleLastThinking(): boolean {
|
|
1704
|
+
const detail = currentDetail();
|
|
1426
1705
|
for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
|
|
1427
1706
|
const entry = transcript[entryIndex];
|
|
1428
1707
|
if (entry?.role !== "assistant") continue;
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1708
|
+
for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
|
|
1709
|
+
const segment = entry.segments[segIndex];
|
|
1710
|
+
if (segment?.kind !== "thinking" || segment.text.length === 0) continue;
|
|
1711
|
+
segment.fold = thinkingExpanded(segment, detail) ? "folded" : "expanded";
|
|
1712
|
+
clearRenderCaches();
|
|
1713
|
+
markDirty();
|
|
1714
|
+
return true;
|
|
1715
|
+
}
|
|
1435
1716
|
}
|
|
1436
|
-
|
|
1437
|
-
return true;
|
|
1717
|
+
return false;
|
|
1438
1718
|
},
|
|
1439
1719
|
toggleAllThinking(): boolean {
|
|
1440
|
-
const
|
|
1720
|
+
const detail = currentDetail();
|
|
1721
|
+
const segments: ThinkingSegment[] = [];
|
|
1441
1722
|
for (const entry of transcript) {
|
|
1442
|
-
if (entry.role
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
return true;
|
|
1723
|
+
if (entry.role !== "assistant") continue;
|
|
1724
|
+
for (const segment of entry.segments) {
|
|
1725
|
+
if (segment.kind === "thinking" && segment.text.length > 0) segments.push(segment);
|
|
1726
|
+
}
|
|
1447
1727
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1728
|
+
if (segments.length === 0) return false;
|
|
1729
|
+
const expand = segments.some((segment) => !thinkingExpanded(segment, detail));
|
|
1730
|
+
for (const segment of segments) segment.fold = expand ? "expanded" : "folded";
|
|
1451
1731
|
clearRenderCaches();
|
|
1452
1732
|
markDirty();
|
|
1453
1733
|
return true;
|
|
1454
1734
|
},
|
|
1455
|
-
isThinkingExpanded
|
|
1735
|
+
isThinkingExpanded(): boolean {
|
|
1736
|
+
// The live stretch lives on the newest assistant entry; its own override,
|
|
1737
|
+
// if any, is the one that applies. Before a stretch arrives, the policy answers.
|
|
1738
|
+
const detail = currentDetail();
|
|
1739
|
+
const index = lastAssistantIndex(transcript);
|
|
1740
|
+
const entry = index === null ? undefined : transcript[index];
|
|
1741
|
+
if (entry?.role === "assistant") {
|
|
1742
|
+
for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
|
|
1743
|
+
const segment = entry.segments[segIndex];
|
|
1744
|
+
if (segment?.kind === "thinking" && segment.text.length > 0) return thinkingExpanded(segment, detail);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return policyThinkingFold(detail) === "expanded";
|
|
1748
|
+
},
|
|
1456
1749
|
toggleLiveToolOutput(): boolean {
|
|
1457
1750
|
liveToolOutput = !liveToolOutput;
|
|
1458
1751
|
markDirty();
|
|
@@ -1461,6 +1754,7 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1461
1754
|
reset(): void {
|
|
1462
1755
|
transcript.length = 0;
|
|
1463
1756
|
workerEntries.clear();
|
|
1757
|
+
liveReasoning = UNMEASURED_REASONING;
|
|
1464
1758
|
clearRenderCaches();
|
|
1465
1759
|
markDirty();
|
|
1466
1760
|
},
|
|
@@ -1524,6 +1818,7 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1524
1818
|
} else if (existing === undefined) {
|
|
1525
1819
|
const assistant = ensureAssistant();
|
|
1526
1820
|
assistant.pending = true;
|
|
1821
|
+
closeOpenThinking(assistant);
|
|
1527
1822
|
assistant.segments.push({
|
|
1528
1823
|
kind: "tool",
|
|
1529
1824
|
id: streamed.id,
|
|
@@ -1533,7 +1828,6 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1533
1828
|
executionStarted: false,
|
|
1534
1829
|
argsComplete: assistantEvent.type === "toolcall_end",
|
|
1535
1830
|
isError: false,
|
|
1536
|
-
expanded: false,
|
|
1537
1831
|
});
|
|
1538
1832
|
}
|
|
1539
1833
|
markDirty();
|
|
@@ -1547,16 +1841,18 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1547
1841
|
return;
|
|
1548
1842
|
}
|
|
1549
1843
|
if (event.type === "thinking_delta") {
|
|
1550
|
-
//
|
|
1844
|
+
// The text itself is never rendered unless the operator expands it;
|
|
1845
|
+
// the segment is what keeps reasoning in its place in the stream.
|
|
1551
1846
|
const assistant = ensureAssistant();
|
|
1552
1847
|
assistant.pending = true;
|
|
1553
|
-
assistant
|
|
1554
|
-
assistant.expandedThinking = thinkingExpanded;
|
|
1848
|
+
appendThinkingDelta(assistant, event.delta);
|
|
1555
1849
|
markDirty();
|
|
1556
1850
|
return;
|
|
1557
1851
|
}
|
|
1558
1852
|
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
1559
|
-
ensureAssistant()
|
|
1853
|
+
const assistant = ensureAssistant();
|
|
1854
|
+
assistant.pending = true;
|
|
1855
|
+
assistant.messageStartSegmentIndex = assistant.segments.length;
|
|
1560
1856
|
markDirty();
|
|
1561
1857
|
return;
|
|
1562
1858
|
}
|
|
@@ -1570,7 +1866,6 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1570
1866
|
streamed.executionStarted = true;
|
|
1571
1867
|
streamed.argsComplete = true;
|
|
1572
1868
|
streamed.startedAtMs = now();
|
|
1573
|
-
streamed.expanded = classifyResourceRead(event.toolName, event.args) === null;
|
|
1574
1869
|
markDirty();
|
|
1575
1870
|
return;
|
|
1576
1871
|
}
|
|
@@ -1585,10 +1880,11 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1585
1880
|
}
|
|
1586
1881
|
}
|
|
1587
1882
|
const assistant = ensureAssistant();
|
|
1588
|
-
//
|
|
1589
|
-
//
|
|
1590
|
-
|
|
1883
|
+
// No fold is stored here: the segment's effective state is resolved
|
|
1884
|
+
// per frame from the transcript detail policy and the tool's
|
|
1885
|
+
// registered presentation, with the operator's override on top.
|
|
1591
1886
|
assistant.pending = true;
|
|
1887
|
+
closeOpenThinking(assistant);
|
|
1592
1888
|
assistant.segments.push({
|
|
1593
1889
|
kind: "tool",
|
|
1594
1890
|
id: event.toolCallId,
|
|
@@ -1598,7 +1894,6 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1598
1894
|
executionStarted: true,
|
|
1599
1895
|
argsComplete: true,
|
|
1600
1896
|
isError: false,
|
|
1601
|
-
expanded,
|
|
1602
1897
|
startedAtMs: now(),
|
|
1603
1898
|
});
|
|
1604
1899
|
markDirty();
|
|
@@ -1650,7 +1945,14 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1650
1945
|
resultSummary?: unknown;
|
|
1651
1946
|
outcome?: unknown;
|
|
1652
1947
|
blockReason?: unknown;
|
|
1948
|
+
evictedReason?: unknown;
|
|
1653
1949
|
};
|
|
1950
|
+
// Replay-only: the rehydrator reads the working-set fold and tags
|
|
1951
|
+
// the rows whose bodies the projection has replaced for the model.
|
|
1952
|
+
tool.evictedReason =
|
|
1953
|
+
typeof enriched.evictedReason === "string" && enriched.evictedReason.length > 0
|
|
1954
|
+
? enriched.evictedReason
|
|
1955
|
+
: undefined;
|
|
1654
1956
|
// Settlement is that verdict, never an inference from result text.
|
|
1655
1957
|
// The text of a tool result is the tool's own output: `node --test`
|
|
1656
1958
|
// prints `cancelled 0` on every run and a linter can print
|
|
@@ -1716,10 +2018,22 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1716
2018
|
invalidateEntryCache(assistant);
|
|
1717
2019
|
if (usage !== undefined) assistant.turnUsage = usage;
|
|
1718
2020
|
if (terminalError.length > 0) assistant.isError = true;
|
|
2021
|
+
// The message is settled, so whatever reasoning it streamed is closed.
|
|
2022
|
+
// A message that carried thinking the panel never saw as deltas (a
|
|
2023
|
+
// non-streaming provider, a replayed message) gets one segment at the
|
|
2024
|
+
// point this message began, ahead of the text the same message
|
|
2025
|
+
// produced, rather than a marker dangling after the answer.
|
|
2026
|
+
closeOpenThinking(assistant);
|
|
1719
2027
|
if (thinking.length > 0) {
|
|
1720
|
-
assistant.
|
|
1721
|
-
|
|
2028
|
+
const messageStart = Math.min(assistant.messageStartSegmentIndex ?? 0, assistant.segments.length);
|
|
2029
|
+
const streamedThisMessage = assistant.segments
|
|
2030
|
+
.slice(messageStart)
|
|
2031
|
+
.some((segment) => segment.kind === "thinking" && segment.text.length > 0);
|
|
2032
|
+
if (!streamedThisMessage) {
|
|
2033
|
+
assistant.segments.splice(messageStart, 0, { kind: "thinking", text: thinking, finalized: true });
|
|
2034
|
+
}
|
|
1722
2035
|
}
|
|
2036
|
+
assistant.messageStartSegmentIndex = undefined;
|
|
1723
2037
|
// The chat loop marks messages it sanitized after streaming (dead
|
|
1724
2038
|
// tool-call markup on a synthesis-locked turn); the streamed tail
|
|
1725
2039
|
// must be replaced, not kept alongside a duplicate segment.
|
|
@@ -1790,13 +2104,26 @@ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
|
|
|
1790
2104
|
}
|
|
1791
2105
|
if (entry.pending) {
|
|
1792
2106
|
invalidateEntryCache(entry);
|
|
2107
|
+
closeOpenThinking(entry);
|
|
1793
2108
|
entry.pending = false;
|
|
1794
2109
|
entry.statusLine = null;
|
|
2110
|
+
entry.messageStartSegmentIndex = undefined;
|
|
1795
2111
|
}
|
|
1796
2112
|
}
|
|
1797
2113
|
markDirty();
|
|
1798
2114
|
}
|
|
1799
2115
|
},
|
|
2116
|
+
setLiveReasoning(view: ReasoningUsageView | null): void {
|
|
2117
|
+
const next = view ?? UNMEASURED_REASONING;
|
|
2118
|
+
if (next.tokens === liveReasoning.tokens && next.provenance === liveReasoning.provenance) return;
|
|
2119
|
+
liveReasoning = next;
|
|
2120
|
+
// The line lives on the pending tail entry, which the freeze may already
|
|
2121
|
+
// cover if nothing has mutated since the last settle.
|
|
2122
|
+
unfreezeTail();
|
|
2123
|
+
const last = transcript[transcript.length - 1];
|
|
2124
|
+
if (last !== undefined) invalidateEntryCache(last);
|
|
2125
|
+
markDirty();
|
|
2126
|
+
},
|
|
1800
2127
|
setStatusLine(line): void {
|
|
1801
2128
|
if (line) {
|
|
1802
2129
|
const assistant = ensureAssistant();
|