@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TokenThroughputSnapshot, UsageBreakdown } from "../domains/observability/index.js";
|
|
2
2
|
import { type Text, truncateToWidth, visibleWidth } from "../engine/tui.js";
|
|
3
3
|
import type { DispatchBoardRow, DispatchBoardStatus } from "./dispatch-board.js";
|
|
4
|
+
import { formatReasoningChip } from "./status/reasoning.js";
|
|
4
5
|
import { type ClioTheme, formatCompactMs, formatContextPercent, GLYPH } from "./theme/index.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -37,7 +38,11 @@ export function tokensSegment(usage: UsageBreakdown | null | undefined): string
|
|
|
37
38
|
const reasoning = Math.max(0, usage.reasoningTokens ?? 0);
|
|
38
39
|
const total = Math.max(0, usage.totalTokens ?? input + output);
|
|
39
40
|
if (input + output + reasoning + total === 0) return null;
|
|
40
|
-
|
|
41
|
+
// Session totals are summed from provider usage payloads, so the chip carries
|
|
42
|
+
// no `≈`. It is built by the same formatter every other reasoning surface
|
|
43
|
+
// uses rather than by a fourth copy of the marker rule.
|
|
44
|
+
const reasoningChip = formatReasoningChip({ tokens: reasoning, provenance: "provider" }, formatFooterTokens);
|
|
45
|
+
const reasoningPart = reasoningChip === null ? "" : ` ${reasoningChip}`;
|
|
41
46
|
const totalPart = total > 0 ? ` Σ${formatFooterTokens(total)}` : "";
|
|
42
47
|
return `${GLYPH.up}${formatFooterTokens(input)} ${GLYPH.down}${formatFooterTokens(output)}${reasoningPart}${totalPart}`;
|
|
43
48
|
}
|
|
@@ -560,6 +560,7 @@ export async function createInteractiveApplication(deps: InteractiveDeps): Promi
|
|
|
560
560
|
recordToolEnd: (_toolName, toolCallId, isError, truncated) =>
|
|
561
561
|
presentation.recordToolEnd({ toolCallId, isError, truncated }),
|
|
562
562
|
setStatusLine: (line) => chatPanel.setStatusLine(line),
|
|
563
|
+
setLiveReasoning: (view) => chatPanel.setLiveReasoning(view),
|
|
563
564
|
setLastTurnSummary: (summary) => presentation.setLastTurnSummary(summary),
|
|
564
565
|
startTerminalProgress: () => agentProgress.start(),
|
|
565
566
|
stopTerminalProgress: () => agentProgress.stop(),
|
|
@@ -591,6 +592,7 @@ export async function createInteractiveApplication(deps: InteractiveDeps): Promi
|
|
|
591
592
|
chatPanel: {
|
|
592
593
|
appendReplayBlock: (...args) => chatRenderer.mutate(() => chatPanel.appendReplayBlock(...args), "slash-output"),
|
|
593
594
|
appendUser: (text) => chatRenderer.mutate(() => chatPanel.appendUser(text), "user-submit"),
|
|
595
|
+
clearFoldOverrides: () => chatRenderer.mutate(() => chatPanel.clearFoldOverrides(), "output-detail"),
|
|
594
596
|
},
|
|
595
597
|
beforeSemanticSubmit: () => chatRenderer.flush(),
|
|
596
598
|
settleVisibleFrame: (reason) => chatRenderer.flushAndCommit(reason),
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
import {
|
|
29
29
|
type AgentStatus,
|
|
30
30
|
INLINE_STATUS_INDENT_COLS,
|
|
31
|
+
type ReasoningUsageView,
|
|
32
|
+
reasoningFromTally,
|
|
31
33
|
resolveInlineVerb,
|
|
32
34
|
type StatusPhase,
|
|
33
35
|
spinnerFrame,
|
|
@@ -67,6 +69,11 @@ export interface InteractiveEventProjectionDeps {
|
|
|
67
69
|
recordToolStart: (toolName: string, toolCallId: string) => void;
|
|
68
70
|
recordToolEnd: (toolName: string, toolCallId: string, isError: boolean, truncated: boolean) => void;
|
|
69
71
|
setStatusLine: (line: InteractiveStatusLine | null) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Publish the live turn's reasoning projection to the transcript. Optional so
|
|
74
|
+
* a host without a chat panel still gets every other projection.
|
|
75
|
+
*/
|
|
76
|
+
setLiveReasoning?: (view: ReasoningUsageView | null) => void;
|
|
70
77
|
setLastTurnSummary: (summary: TurnSummary) => void;
|
|
71
78
|
startTerminalProgress: () => void;
|
|
72
79
|
stopTerminalProgress: () => void;
|
|
@@ -177,6 +184,11 @@ export function createInteractiveEventProjection(deps: InteractiveEventProjectio
|
|
|
177
184
|
let statusInlineFrame = 0;
|
|
178
185
|
primaryUnsubscribers.push(
|
|
179
186
|
deps.status.subscribe((status) => {
|
|
187
|
+
// One projection of the run tally reaches the transcript, so the live
|
|
188
|
+
// line, the turn receipt, and the footer state the same number.
|
|
189
|
+
deps.setLiveReasoning?.(
|
|
190
|
+
status.phase === "idle" || status.phase === "ended" ? null : reasoningFromTally(status.runTally),
|
|
191
|
+
);
|
|
180
192
|
if (status.phase === "idle") {
|
|
181
193
|
deps.setStatusLine(null);
|
|
182
194
|
} else if (status.phase === "ended") {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { BusChannels } from "../core/bus-events.js";
|
|
3
4
|
import type { ClioSettings } from "../core/config.js";
|
|
4
5
|
import type { SafeEventBus } from "../core/event-bus.js";
|
|
5
6
|
import type { PendingSkillRequest } from "../core/skill-activation.js";
|
|
@@ -28,6 +29,7 @@ import { type ChatPanel, createChatPanel } from "./chat-panel.js";
|
|
|
28
29
|
import { rehydrateChatPanelFromTurns } from "./chat-renderer.js";
|
|
29
30
|
import { runCompactWithNotice } from "./command-fallbacks.js";
|
|
30
31
|
import { appendNotice, appendOperatorCommand } from "./command-output.js";
|
|
32
|
+
import { runOperatorRecall } from "./context-recall-command.js";
|
|
31
33
|
import { renderSessionHtml } from "./export-html/index.js";
|
|
32
34
|
import { dateLocal } from "./format-time.js";
|
|
33
35
|
import type { SettingsCenterRowId, SettingsSectionId } from "./overlays/settings.js";
|
|
@@ -54,7 +56,7 @@ export interface InteractiveSlashSubmitExpansion {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
type SlashChat = Pick<ChatLoop, "getSessionId" | "isStreaming" | "submit">;
|
|
57
|
-
type SlashChatPanel = Pick<ChatPanel, "appendReplayBlock" | "appendUser">;
|
|
59
|
+
type SlashChatPanel = Pick<ChatPanel, "appendReplayBlock" | "appendUser" | "clearFoldOverrides">;
|
|
58
60
|
type SlashResources = Pick<ResourcesContract, "prompts" | "expandPromptTemplate" | "reload">;
|
|
59
61
|
type SlashExtensions = Pick<ExtensionsContract, "list">;
|
|
60
62
|
type SlashAgents = Pick<AgentsContract, "getSpec" | "listSpecs">;
|
|
@@ -100,8 +102,10 @@ export interface InteractiveSlashRuntimeDeps {
|
|
|
100
102
|
* Leaf lookup for `/export`, so the export follows the branch the session
|
|
101
103
|
* is actually on. current.jsonl still holds the abandoned turns after a
|
|
102
104
|
* `/tree` pin, and an unscoped rehydrate reproduced them (issue #109).
|
|
105
|
+
* `/context recall` uses the same leaf for its fold, plus `appendEntry` for
|
|
106
|
+
* the `contextRecall` record.
|
|
103
107
|
*/
|
|
104
|
-
session?: Pick<SessionContract, "tree">;
|
|
108
|
+
session?: Pick<SessionContract, "tree" | "current" | "appendEntry">;
|
|
105
109
|
expandSubmit: (text: string) => Promise<InteractiveSlashSubmitExpansion>;
|
|
106
110
|
openAskUser: AskUserHandler;
|
|
107
111
|
openSkillsHub: () => void;
|
|
@@ -398,6 +402,11 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
|
|
|
398
402
|
// alone. Saving it as the default is what Settings → Terminal is for.
|
|
399
403
|
if (deps.commitSetting) deps.commitSetting("terminal.outputVerbosity", next, "session");
|
|
400
404
|
else deps.writeSettings?.(next);
|
|
405
|
+
// `/output` is also the explicit reset action for transcript folds. Do
|
|
406
|
+
// this even when the requested level already matches the current one;
|
|
407
|
+
// the render-time change detector cannot observe a same-value command.
|
|
408
|
+
deps.chatPanel.clearFoldOverrides();
|
|
409
|
+
deps.refreshFooter();
|
|
401
410
|
return { status: "applied", verbosity: match };
|
|
402
411
|
},
|
|
403
412
|
openModel: deps.openModel,
|
|
@@ -434,11 +443,12 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
|
|
|
434
443
|
try {
|
|
435
444
|
const turns = deps.readStructuredEntries(sessionId);
|
|
436
445
|
// Same pure render pipeline as the live panel and /resume replay: a
|
|
437
|
-
// throwaway panel is rehydrated from the ledger
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
|
|
446
|
+
// throwaway panel is rehydrated from the ledger under the verbose
|
|
447
|
+
// transcript detail policy (every body open, full receipts) with no
|
|
448
|
+
// operator overrides, and rendered at a stable width. HTML converts
|
|
449
|
+
// the resulting ANSI presentation to inline styles; Markdown keeps the
|
|
450
|
+
// prior plain-text fenced transcript.
|
|
451
|
+
const exportPanel = createChatPanel({ unboundedToolBodies: true, getOutputVerbosity: () => "verbose" });
|
|
442
452
|
// Scoped to the leaf the session is on, the same way /resume replays
|
|
443
453
|
// (issue #107): with a /tree pin persisted and not yet extended, the
|
|
444
454
|
// file still holds the abandoned branch after the pin, and an unscoped
|
|
@@ -449,7 +459,6 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
|
|
|
449
459
|
unboundedToolBodies: true,
|
|
450
460
|
...(leafTurnId ? { activeLeafTurnId: leafTurnId } : {}),
|
|
451
461
|
});
|
|
452
|
-
exportPanel.toggleAllToolsExpanded();
|
|
453
462
|
const ansiLines = exportPanel.render(EXPORT_RENDER_WIDTH);
|
|
454
463
|
// The operator names this file by the day they ran the export, so the
|
|
455
464
|
// date in it is their calendar date. The header below keeps the ISO
|
|
@@ -507,6 +516,38 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
|
|
|
507
516
|
}
|
|
508
517
|
deps.openContextReset();
|
|
509
518
|
},
|
|
519
|
+
...(deps.session
|
|
520
|
+
? {
|
|
521
|
+
runContextRecall: (ref: string) => {
|
|
522
|
+
const session = deps.session;
|
|
523
|
+
if (!session) return;
|
|
524
|
+
const outcome = runOperatorRecall(ref, {
|
|
525
|
+
hasSession: () => session.current() !== null,
|
|
526
|
+
readEntries: () => {
|
|
527
|
+
const sessionId = deps.chat.getSessionId();
|
|
528
|
+
return sessionId ? deps.readStructuredEntries(sessionId) : [];
|
|
529
|
+
},
|
|
530
|
+
activeLeafTurnId: () => {
|
|
531
|
+
const meta = session.current();
|
|
532
|
+
return meta ? (session.tree(meta.id).leafId ?? undefined) : undefined;
|
|
533
|
+
},
|
|
534
|
+
appendEntry: (entry) => session.appendEntry(entry),
|
|
535
|
+
onRecalled: (payload) => deps.bus.emit(BusChannels.ContextRecalled, payload),
|
|
536
|
+
...(deps.now ? { now: () => (deps.now?.() ?? new Date()).getTime() } : {}),
|
|
537
|
+
});
|
|
538
|
+
if (!outcome.ok) {
|
|
539
|
+
appendCommandNotice("error", outcome.message);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
appendCommandNotice("success", outcome.headline);
|
|
543
|
+
// The body is transcript output, not a notice: notices collapse
|
|
544
|
+
// newlines, and a recalled build log is worth nothing on one line.
|
|
545
|
+
// It is a replay block either way, so it never enters model context.
|
|
546
|
+
deps.io.stdout(`${outcome.body}\n`);
|
|
547
|
+
deps.requestRender();
|
|
548
|
+
},
|
|
549
|
+
}
|
|
550
|
+
: {}),
|
|
510
551
|
runContextRefresh: () => {
|
|
511
552
|
if (!deps.onContextRefresh) {
|
|
512
553
|
deps.io.stderr("[/context refresh] context refresh not wired; pass onContextRefresh to startInteractive\n");
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { foldWorkingSet } from "../domains/context/working-set/fold.js";
|
|
2
|
+
import { projectWorkingSet } from "../domains/context/working-set/project.js";
|
|
3
|
+
import type { SessionEntry } from "../domains/session/entries.js";
|
|
4
|
+
import type { AgentMessage } from "../engine/types.js";
|
|
5
|
+
import { buildReplayAgentMessagesFromTurns, type RehydrateChatPanelOptions } from "./chat-renderer.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build provider-facing replay messages from the durable session ledger.
|
|
9
|
+
* Projection always honors existing eviction and recall entries; the enabled
|
|
10
|
+
* setting gates creation of new evictions, not replay of durable state.
|
|
11
|
+
* Visible transcript and export callers intentionally keep using the raw
|
|
12
|
+
* rehydration helpers so eviction remains a model projection, not data loss.
|
|
13
|
+
*/
|
|
14
|
+
export function buildModelReplayAgentMessagesFromTurns(
|
|
15
|
+
entries: ReadonlyArray<SessionEntry>,
|
|
16
|
+
options: RehydrateChatPanelOptions = {},
|
|
17
|
+
): AgentMessage[] {
|
|
18
|
+
const activeLeafTurnId = options.activeLeafTurnId ?? options.uptoTurnId;
|
|
19
|
+
const projected = projectWorkingSet(entries, foldWorkingSet(entries, activeLeafTurnId));
|
|
20
|
+
return buildReplayAgentMessagesFromTurns(projected, options);
|
|
21
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SafeEventBus } from "../core/event-bus.js";
|
|
2
|
+
import { foldWorkingSet } from "../domains/context/working-set/fold.js";
|
|
2
3
|
import type { DispatchContract } from "../domains/dispatch/index.js";
|
|
3
4
|
import { loadMemoryRecordsSync, type MemoryRecord } from "../domains/memory/index.js";
|
|
4
5
|
import type { ObservabilityContract } from "../domains/observability/index.js";
|
|
@@ -107,6 +108,11 @@ export function createOverlayGeneralOpeners(deps: OverlayGeneralOpenersDeps): Ov
|
|
|
107
108
|
deps.transitions.handle = openContextOverlayFactory(deps.tui, deps.getContextLedger, {
|
|
108
109
|
bus: deps.bus,
|
|
109
110
|
chat: deps.contextChat,
|
|
111
|
+
getWorkingSet: () => {
|
|
112
|
+
const readSessionEntries = deps.readSessionEntries;
|
|
113
|
+
if (!readSessionEntries) return null;
|
|
114
|
+
return foldWorkingSet(readSessionEntries(), deps.getSessionMeta()?.pinnedLeafTurnId ?? undefined);
|
|
115
|
+
},
|
|
110
116
|
});
|
|
111
117
|
deps.requestRender();
|
|
112
118
|
};
|
|
@@ -3,9 +3,10 @@ import type { SessionContract, SessionEntry } from "../domains/session/index.js"
|
|
|
3
3
|
import type { TUI } from "../engine/tui.js";
|
|
4
4
|
import type { ChatLoop } from "./chat-loop.js";
|
|
5
5
|
import type { ChatPanel } from "./chat-panel.js";
|
|
6
|
-
import {
|
|
6
|
+
import { rehydrateChatPanelFromTurns } from "./chat-renderer.js";
|
|
7
7
|
import { emitCommandNotice } from "./command-fallbacks.js";
|
|
8
8
|
import type { InteractiveNoticeLevel } from "./interactive-subscriptions.js";
|
|
9
|
+
import { buildModelReplayAgentMessagesFromTurns } from "./model-session-replay.js";
|
|
9
10
|
import type { OverlayTransitions } from "./overlay-transitions.js";
|
|
10
11
|
import { openCwdFallbackOverlay } from "./overlays/cwd-fallback.js";
|
|
11
12
|
import { openMessagePickerOverlay } from "./overlays/message-picker.js";
|
|
@@ -159,7 +160,7 @@ export function createOverlaySessionLifecycle(deps: OverlaySessionLifecycleDeps)
|
|
|
159
160
|
const replayOptions = leafTurnId ? { activeLeafTurnId: leafTurnId } : {};
|
|
160
161
|
deps.resetTranscript();
|
|
161
162
|
rehydrateChatPanelFromTurns(deps.chatPanel, turns, replayOptions);
|
|
162
|
-
const replayMessages =
|
|
163
|
+
const replayMessages = buildModelReplayAgentMessagesFromTurns(turns, replayOptions);
|
|
163
164
|
deps.chat.resetForSession(leafTurnId, replayMessages);
|
|
164
165
|
rescopeToBranch(session, turns, leafTurnId);
|
|
165
166
|
} catch (error) {
|
|
@@ -209,7 +210,7 @@ export function createOverlaySessionLifecycle(deps: OverlaySessionLifecycleDeps)
|
|
|
209
210
|
const turns = deps.readStructuredEntries(sessionId);
|
|
210
211
|
deps.resetTranscript();
|
|
211
212
|
rehydrateChatPanelFromTurns(deps.chatPanel, turns, { uptoTurnId: turnId });
|
|
212
|
-
const replayMessages =
|
|
213
|
+
const replayMessages = buildModelReplayAgentMessagesFromTurns(turns, { uptoTurnId: turnId });
|
|
213
214
|
deps.chat.resetForSession(turnId, replayMessages);
|
|
214
215
|
// The same branch the transcript above was just scoped to. Without the
|
|
215
216
|
// leaf, /cost, the footer Σ, and the last-turn line kept reporting the
|
|
@@ -287,8 +288,11 @@ export function createOverlaySessionLifecycle(deps: OverlaySessionLifecycleDeps)
|
|
|
287
288
|
try {
|
|
288
289
|
const turns = deps.readStructuredEntries(forkedSessionId);
|
|
289
290
|
rehydrateChatPanelFromTurns(deps.chatPanel, turns);
|
|
290
|
-
const replayMessages = buildReplayAgentMessagesFromTurns(turns);
|
|
291
291
|
const leafTurnId = session.tree(forkedSessionId).leafId ?? parentTurnId;
|
|
292
|
+
const replayMessages = buildModelReplayAgentMessagesFromTurns(
|
|
293
|
+
turns,
|
|
294
|
+
leafTurnId ? { activeLeafTurnId: leafTurnId } : {},
|
|
295
|
+
);
|
|
292
296
|
deps.chat.resetForSession(leafTurnId, replayMessages);
|
|
293
297
|
rescopeToBranch(session, turns, leafTurnId);
|
|
294
298
|
} catch (error) {
|
|
@@ -13,8 +13,9 @@ import {
|
|
|
13
13
|
} from "../../engine/tui.js";
|
|
14
14
|
import type { AskUserAnswer, AskUserQuestion, AskUserResult } from "../../tools/ask-user.js";
|
|
15
15
|
import { ASK_USER_OTHER_LABEL, cancelledAskUserResult } from "../../tools/ask-user.js";
|
|
16
|
-
import { buildHint, DEFAULT_SELECT_THEME, showClioOverlayFrame } from "../overlay-frame.js";
|
|
16
|
+
import { buildHint, DEFAULT_SELECT_THEME, type HintEntry, showClioOverlayFrame } from "../overlay-frame.js";
|
|
17
17
|
import { type ClioToken, clioTheme, dotSep, GLYPH, screenTitle } from "../theme/index.js";
|
|
18
|
+
import { nextContentScrollOffset, type ViewScrollAction } from "../view/view-overlay.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* The border and title token while a decision is pending.
|
|
@@ -54,7 +55,7 @@ export const ASK_USER_DEFAULT_SURFACE: AskUserSurface = "panel";
|
|
|
54
55
|
interface AskUserSurfaceGeometry {
|
|
55
56
|
/** Box width in columns; zero fills the row. The frame clamps to the terminal. */
|
|
56
57
|
width: number;
|
|
57
|
-
/** Rows the body may use, before the borders. */
|
|
58
|
+
/** Rows the body may use, before the borders. Zero means every row the frame has. */
|
|
58
59
|
maxInnerRows: number;
|
|
59
60
|
anchor: NonNullable<OverlayOptions["anchor"]>;
|
|
60
61
|
margin: NonNullable<OverlayOptions["margin"]>;
|
|
@@ -74,12 +75,18 @@ interface AskUserSurfaceGeometry {
|
|
|
74
75
|
export const ASK_USER_SURFACE_GEOMETRY: Record<AskUserSurface, AskUserSurfaceGeometry> = {
|
|
75
76
|
compact: { width: 68, maxInnerRows: 10, anchor: "bottom-center", margin: { top: 1, right: 2, bottom: 1, left: 2 } },
|
|
76
77
|
panel: { width: 88, maxInnerRows: 18, anchor: "center", margin: 1 },
|
|
77
|
-
interview
|
|
78
|
+
// The interview is a workspace, not a modal. A cap of 42 turned a 66-row
|
|
79
|
+
// terminal into a centered box with twenty blank rows around it, so the
|
|
80
|
+
// interview takes every row the frame has and lays its own chrome out inside
|
|
81
|
+
// them. The one-row margin keeps the border off the top row and the footer.
|
|
82
|
+
interview: { width: 0, maxInnerRows: 0, anchor: "center", margin: 1 },
|
|
78
83
|
};
|
|
79
84
|
|
|
80
85
|
const ASK_USER_FRAME_ROWS = 2;
|
|
81
86
|
const ASK_USER_FRAME_AND_MARGIN_ROWS = 4;
|
|
82
87
|
const MIN_VISIBLE_OPTIONS = 3;
|
|
88
|
+
/** Interview rows spent on chrome before an option row: strip, header, blanks, status, one question row. */
|
|
89
|
+
const INTERVIEW_CHROME_ROWS = 7;
|
|
83
90
|
const MAX_VISIBLE_OPTIONS = 12;
|
|
84
91
|
const ELLIPSIS = "…";
|
|
85
92
|
|
|
@@ -227,6 +234,14 @@ class AskUserOverlayView implements Component {
|
|
|
227
234
|
private list: SelectList | null = null;
|
|
228
235
|
private input: Input | null = null;
|
|
229
236
|
private resolveCurrent: ((result: AskUserResult) => void) | null = null;
|
|
237
|
+
/** Rows of the interview's scrollable middle already scrolled past. */
|
|
238
|
+
private contentScrollOffset = 0;
|
|
239
|
+
/** Whether the last interview render had more content than the region held. */
|
|
240
|
+
private contentOverflows = false;
|
|
241
|
+
/** Rows the scroll region drew on the last render, for a page-sized jump. */
|
|
242
|
+
private contentRegionHeight = 1;
|
|
243
|
+
/** Rows the scroll region wanted on the last render. */
|
|
244
|
+
private contentTotalRows = 0;
|
|
230
245
|
|
|
231
246
|
constructor(private readonly deps: AskUserOverlayViewDeps) {}
|
|
232
247
|
|
|
@@ -240,6 +255,7 @@ class AskUserOverlayView implements Component {
|
|
|
240
255
|
this.states = this.questions.map((question) => createQuestionState(question));
|
|
241
256
|
this.list = null;
|
|
242
257
|
this.input = null;
|
|
258
|
+
this.resetContentScroll();
|
|
243
259
|
// Before the controls, because how many option rows fit is a property of
|
|
244
260
|
// the box this round is about to be drawn in.
|
|
245
261
|
this.setSurface(askUserSurface(this.questions, this.history.length));
|
|
@@ -283,6 +299,10 @@ class AskUserOverlayView implements Component {
|
|
|
283
299
|
const state = this.currentState();
|
|
284
300
|
if (!question || !state) return;
|
|
285
301
|
|
|
302
|
+
// Scrolling the transcript must not cost the operator their place in the
|
|
303
|
+
// options, so the region has keys of its own that neither control claims.
|
|
304
|
+
if (this.handleScrollInput(data, state.mode)) return;
|
|
305
|
+
|
|
286
306
|
if (state.mode === "text") {
|
|
287
307
|
if (this.isTextModePreviousKey(data)) {
|
|
288
308
|
this.goToRelativeQuestion(-1);
|
|
@@ -331,21 +351,106 @@ class AskUserOverlayView implements Component {
|
|
|
331
351
|
const question = this.currentQuestion();
|
|
332
352
|
if (!question) return [clioTheme().fg("muted", "No questions.")];
|
|
333
353
|
|
|
334
|
-
|
|
354
|
+
if (this.surface === "interview") return this.renderInterview(question, safeWidth, maxRows);
|
|
355
|
+
|
|
356
|
+
this.contentOverflows = false;
|
|
335
357
|
const controlLines = this.renderControlLines(safeWidth);
|
|
336
|
-
const ledgerLines = interview ? this.renderAnswerLedger(safeWidth) : [];
|
|
337
358
|
const statusLines = this.status.length > 0 ? ["", fitLine(clioTheme().fg("dim", this.status), safeWidth)] : [];
|
|
338
|
-
const stripLines = interview ? [this.renderQuestionStrip(safeWidth), ""] : [];
|
|
339
359
|
const headerLine = this.renderQuestionHeader(question, safeWidth);
|
|
340
360
|
const headerLines = headerLine.length > 0 ? [headerLine] : [];
|
|
341
|
-
const spent =
|
|
342
|
-
stripLines.length + headerLines.length + statusLines.length + 1 + controlLines.length + ledgerLines.length;
|
|
361
|
+
const spent = headerLines.length + statusLines.length + 1 + controlLines.length;
|
|
343
362
|
const questionLines = wrapTextWithAnsi(question.question, safeWidth).slice(0, Math.max(1, maxRows - spent));
|
|
344
363
|
|
|
345
|
-
return [...
|
|
346
|
-
|
|
347
|
-
|
|
364
|
+
return [...headerLines, ...questionLines, ...statusLines, "", ...controlLines].slice(0, maxRows);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The interview, as a workspace with fixed chrome and a scrolling middle.
|
|
369
|
+
*
|
|
370
|
+
* The round strip and the question header hold the top; the status line and
|
|
371
|
+
* the active control hold the bottom, at their full row cost, so the thing the
|
|
372
|
+
* operator has to act on is never the thing that gets clipped. The question
|
|
373
|
+
* text and the answer ledger share whatever is left and scroll through it.
|
|
374
|
+
* Everything is re-derived per render, so a resize is just the next frame.
|
|
375
|
+
*/
|
|
376
|
+
private renderInterview(question: AskUserQuestion, width: number, maxRows: number): string[] {
|
|
377
|
+
const theme = clioTheme();
|
|
378
|
+
const controlLines = this.renderControlLines(width);
|
|
379
|
+
const statusLines = this.status.length > 0 ? ["", fitLine(theme.fg("dim", this.status), width)] : [];
|
|
380
|
+
const headerLine = this.renderQuestionHeader(question, width);
|
|
381
|
+
const headerLines = headerLine.length > 0 ? [headerLine] : [];
|
|
382
|
+
const stripLines = [this.renderQuestionStrip(width), ""];
|
|
383
|
+
const content = [...wrapTextWithAnsi(question.question, width), ...this.renderAnswerLedger(width)];
|
|
384
|
+
|
|
385
|
+
// Chrome sheds from the outside in when the terminal is too short to carry
|
|
386
|
+
// all of it: the round strip first, then the header, then the status, and
|
|
387
|
+
// the control only if a terminal is short enough that nothing else is left.
|
|
388
|
+
let top = [...stripLines, ...headerLines];
|
|
389
|
+
let bottom = [...statusLines, "", ...controlLines];
|
|
390
|
+
const region = (): number => maxRows - top.length - bottom.length;
|
|
391
|
+
if (region() < 1) top = [...headerLines];
|
|
392
|
+
if (region() < 1) top = [];
|
|
393
|
+
if (region() < 1) bottom = ["", ...controlLines];
|
|
394
|
+
if (region() < 1) bottom = [...controlLines];
|
|
395
|
+
const regionRows = Math.max(1, region());
|
|
396
|
+
|
|
397
|
+
const overflows = content.length > regionRows;
|
|
398
|
+
this.contentOverflows = overflows;
|
|
399
|
+
this.contentTotalRows = content.length;
|
|
400
|
+
// One row of the region pays for the indicator when there is more than the
|
|
401
|
+
// region holds, so the marker never covers a row it is reporting on.
|
|
402
|
+
const viewRows = overflows ? Math.max(1, regionRows - 1) : regionRows;
|
|
403
|
+
this.contentRegionHeight = viewRows;
|
|
404
|
+
this.contentScrollOffset = Math.max(0, Math.min(this.contentScrollOffset, Math.max(0, content.length - viewRows)));
|
|
405
|
+
|
|
406
|
+
const visible = content.slice(this.contentScrollOffset, this.contentScrollOffset + viewRows);
|
|
407
|
+
const middle = [...visible];
|
|
408
|
+
if (overflows) middle.push(fitLine(theme.fg("dim", this.scrollIndicator(content.length, viewRows)), width));
|
|
409
|
+
while (middle.length < regionRows) middle.push("");
|
|
410
|
+
|
|
411
|
+
return [...top, ...middle, ...bottom].slice(0, maxRows);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private scrollIndicator(total: number, viewRows: number): string {
|
|
415
|
+
const above = this.contentScrollOffset;
|
|
416
|
+
const below = Math.max(0, total - viewRows - above);
|
|
417
|
+
const parts: string[] = [];
|
|
418
|
+
if (above > 0) parts.push(`↑ ${above} more`);
|
|
419
|
+
if (below > 0) parts.push(`↓ ${below} more`);
|
|
420
|
+
parts.push(`(${Math.min(total, above + viewRows)}/${total})`);
|
|
421
|
+
return parts.join(" ");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** True when the key belonged to the scroll region rather than to a control. */
|
|
425
|
+
private handleScrollInput(data: string, mode: Mode): boolean {
|
|
426
|
+
if (this.surface !== "interview") return false;
|
|
427
|
+
const action = this.scrollAction(data, mode);
|
|
428
|
+
if (!action) return false;
|
|
429
|
+
const next = nextContentScrollOffset(
|
|
430
|
+
this.contentScrollOffset,
|
|
431
|
+
this.contentTotalRows,
|
|
432
|
+
this.contentRegionHeight,
|
|
433
|
+
action,
|
|
348
434
|
);
|
|
435
|
+
if (next !== this.contentScrollOffset) {
|
|
436
|
+
this.contentScrollOffset = next;
|
|
437
|
+
this.deps.requestRender();
|
|
438
|
+
}
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private scrollAction(data: string, mode: Mode): ViewScrollAction | null {
|
|
443
|
+
if (matchesKey(data, "pageUp")) return "page-up";
|
|
444
|
+
if (matchesKey(data, "pageDown")) return "page-down";
|
|
445
|
+
// Ctrl+U and Ctrl+D are line-kill and half-page in a text field, so they
|
|
446
|
+
// stay with the input while one is focused.
|
|
447
|
+
if (mode === "select" && matchesKey(data, "ctrl+u")) return "half-up";
|
|
448
|
+
if (mode === "select" && matchesKey(data, "ctrl+d")) return "half-down";
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private resetContentScroll(): void {
|
|
453
|
+
this.contentScrollOffset = 0;
|
|
349
454
|
}
|
|
350
455
|
|
|
351
456
|
footerHint(): string {
|
|
@@ -357,11 +462,11 @@ class AskUserOverlayView implements Component {
|
|
|
357
462
|
if (!question || !state) return buildHint([]);
|
|
358
463
|
if (state.mode === "text") {
|
|
359
464
|
return this.questions.length > 1
|
|
360
|
-
?
|
|
465
|
+
? this.withScrollHint([
|
|
361
466
|
{ key: "Enter", verb: "submit" },
|
|
362
467
|
{ key: "Alt+Left/Right", verb: "question" },
|
|
363
468
|
])
|
|
364
|
-
:
|
|
469
|
+
: this.withScrollHint([{ key: "Enter", verb: "submit" }]);
|
|
365
470
|
}
|
|
366
471
|
// `accept` rather than `select` or `commit`: the footer of a decision prompt
|
|
367
472
|
// has one job, which is to name the key that answers it. Esc keeps the
|
|
@@ -369,22 +474,32 @@ class AskUserOverlayView implements Component {
|
|
|
369
474
|
// interview together.
|
|
370
475
|
if (question.multi_select === true) {
|
|
371
476
|
return this.questions.length > 1
|
|
372
|
-
?
|
|
477
|
+
? this.withScrollHint([
|
|
373
478
|
{ key: "Left/Right", verb: "question" },
|
|
374
479
|
{ key: "Space", verb: "toggle" },
|
|
375
480
|
{ key: "Enter", verb: "accept" },
|
|
376
481
|
])
|
|
377
|
-
:
|
|
482
|
+
: this.withScrollHint([
|
|
378
483
|
{ key: "Space", verb: "toggle" },
|
|
379
484
|
{ key: "Enter", verb: "accept" },
|
|
380
485
|
]);
|
|
381
486
|
}
|
|
382
487
|
return this.questions.length > 1
|
|
383
|
-
?
|
|
488
|
+
? this.withScrollHint([
|
|
384
489
|
{ key: "Left/Right", verb: "question" },
|
|
385
490
|
{ key: "Enter", verb: "accept" },
|
|
386
491
|
])
|
|
387
|
-
:
|
|
492
|
+
: this.withScrollHint([{ key: "Enter", verb: "accept" }]);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* The keys for this question, plus the scroll keys when there is something to
|
|
497
|
+
* scroll. A footer that always advertised PgUp would be advertising a key that
|
|
498
|
+
* does nothing on most rounds.
|
|
499
|
+
*/
|
|
500
|
+
private withScrollHint(entries: ReadonlyArray<HintEntry>): string {
|
|
501
|
+
if (this.surface !== "interview" || !this.contentOverflows) return buildHint(entries);
|
|
502
|
+
return buildHint([...entries, { key: "PgUp/PgDn", verb: "scroll" }]);
|
|
388
503
|
}
|
|
389
504
|
|
|
390
505
|
private setSurface(next: AskUserSurface): void {
|
|
@@ -443,17 +558,20 @@ class AskUserOverlayView implements Component {
|
|
|
443
558
|
private maxInnerRows(): number {
|
|
444
559
|
const cap = ASK_USER_SURFACE_GEOMETRY[this.surface].maxInnerRows;
|
|
445
560
|
const rows = this.deps.getTerminalRows();
|
|
446
|
-
|
|
447
|
-
|
|
561
|
+
// A zero cap is the interview's "every row the frame has", which is the
|
|
562
|
+
// same number the frame budgets itself when the mount passes no maxHeight.
|
|
563
|
+
if (!Number.isFinite(rows) || rows <= 0) return cap > 0 ? cap : 1;
|
|
564
|
+
const available = Math.max(1, Math.floor(rows) - ASK_USER_FRAME_AND_MARGIN_ROWS);
|
|
565
|
+
return cap > 0 ? Math.min(cap, available) : available;
|
|
448
566
|
}
|
|
449
567
|
|
|
450
568
|
private maxVisibleOptions(): number {
|
|
451
569
|
const inner = this.maxInnerRows();
|
|
452
|
-
// The interview
|
|
453
|
-
//
|
|
454
|
-
//
|
|
570
|
+
// The interview's option list is budgeted off the live row count rather
|
|
571
|
+
// than a fixed heuristic: the strip, header, blank, status, blank and one
|
|
572
|
+
// row of question are what the control shares the workspace with.
|
|
455
573
|
if (this.surface === "interview") {
|
|
456
|
-
return Math.max(MIN_VISIBLE_OPTIONS, Math.min(MAX_VISIBLE_OPTIONS, inner -
|
|
574
|
+
return Math.max(MIN_VISIBLE_OPTIONS, Math.min(MAX_VISIBLE_OPTIONS, inner - INTERVIEW_CHROME_ROWS));
|
|
457
575
|
}
|
|
458
576
|
return Math.max(1, Math.min(MAX_VISIBLE_OPTIONS, inner - 4));
|
|
459
577
|
}
|
|
@@ -809,6 +927,7 @@ class AskUserOverlayView implements Component {
|
|
|
809
927
|
const next = this.nextUnansweredIndex();
|
|
810
928
|
if (next !== null) {
|
|
811
929
|
this.index = next;
|
|
930
|
+
this.resetContentScroll();
|
|
812
931
|
const nextState = this.currentState();
|
|
813
932
|
const nextQuestion = this.currentQuestion();
|
|
814
933
|
if (nextState && nextQuestion && !nextState.answer.trim()) nextState.mode = initialMode(nextQuestion);
|
|
@@ -844,6 +963,7 @@ class AskUserOverlayView implements Component {
|
|
|
844
963
|
this.syncActiveControl();
|
|
845
964
|
this.index = (this.index + delta + this.questions.length) % this.questions.length;
|
|
846
965
|
this.status = "";
|
|
966
|
+
this.resetContentScroll();
|
|
847
967
|
this.rebuildControl();
|
|
848
968
|
this.deps.requestRender();
|
|
849
969
|
}
|
|
@@ -888,7 +1008,9 @@ export function openAskUserOverlay(tui: TUI, deps: OpenAskUserOverlayDeps): AskU
|
|
|
888
1008
|
handle = showClioOverlayFrame(tui, view, {
|
|
889
1009
|
anchor: geometry.anchor,
|
|
890
1010
|
width: geometry.width,
|
|
891
|
-
|
|
1011
|
+
// A zero cap means the frame keeps its own budget, which is every row
|
|
1012
|
+
// the terminal has left after the margins.
|
|
1013
|
+
...(geometry.maxInnerRows > 0 ? { maxHeight: geometry.maxInnerRows + ASK_USER_FRAME_ROWS } : {}),
|
|
892
1014
|
margin: geometry.margin,
|
|
893
1015
|
title: () => (view.isDecisionPending() ? ASK_USER_DECISION_TITLE : ASK_USER_WAITING_TITLE),
|
|
894
1016
|
tone: () => (view.isDecisionPending() ? ASK_USER_DECISION_TONE : undefined),
|