@aragon-agent/cli 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +216 -0
- package/README.md +202 -0
- package/dist/agent/controller.d.ts +74 -1
- package/dist/agent/controller.js +200 -1
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +47 -0
- package/dist/agent/reducer.d.ts +120 -0
- package/dist/agent/reducer.js +201 -2
- package/dist/cli.js +224 -5
- package/dist/commands/builtins.js +73 -23
- package/dist/commands/registry.d.ts +27 -0
- package/dist/compaction/archive.d.ts +175 -0
- package/dist/compaction/archive.js +267 -0
- package/dist/compaction/child.d.ts +97 -0
- package/dist/compaction/child.js +113 -0
- package/dist/compaction/command.d.ts +41 -0
- package/dist/compaction/command.js +342 -0
- package/dist/compaction/compactor.d.ts +268 -0
- package/dist/compaction/compactor.js +689 -0
- package/dist/compaction/digest.d.ts +62 -0
- package/dist/compaction/digest.js +164 -0
- package/dist/compaction/limits.d.ts +165 -0
- package/dist/compaction/limits.js +172 -0
- package/dist/compaction/pressure.d.ts +135 -0
- package/dist/compaction/pressure.js +167 -0
- package/dist/compaction/summarize-call.d.ts +70 -0
- package/dist/compaction/summarize-call.js +86 -0
- package/dist/compaction/summary-prompt.d.ts +85 -0
- package/dist/compaction/summary-prompt.js +197 -0
- package/dist/compaction/tail-budget.d.ts +52 -0
- package/dist/compaction/tail-budget.js +67 -0
- package/dist/compaction/types.d.ts +179 -0
- package/dist/compaction/types.js +22 -0
- package/dist/compaction/wiring.d.ts +255 -0
- package/dist/compaction/wiring.js +556 -0
- package/dist/config/app-paths.d.ts +14 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/cli-commands.d.ts +41 -0
- package/dist/config/cli-commands.js +77 -1
- package/dist/config/env.d.ts +4 -1
- package/dist/config/env.js +92 -2
- package/dist/config/load.d.ts +31 -0
- package/dist/config/load.js +63 -1
- package/dist/config/schema.d.ts +159 -0
- package/dist/config/schema.js +137 -0
- package/dist/config/store.js +17 -1
- package/dist/config/ui-state.d.ts +28 -1
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +50 -1
- package/dist/exec/runner.js +42 -0
- package/dist/input/mouse-events.d.ts +31 -5
- package/dist/input/mouse-events.js +86 -27
- package/dist/input/stdin-mouse-filter.d.ts +8 -3
- package/dist/logging/install.d.ts +51 -1
- package/dist/logging/install.js +28 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.js +19 -0
- package/dist/team/report.js +7 -1
- package/dist/team/runtime.d.ts +10 -0
- package/dist/team/runtime.js +7 -0
- package/dist/team/subagent.d.ts +31 -1
- package/dist/team/subagent.js +42 -15
- package/dist/team/types.d.ts +8 -0
- package/dist/ui/ActivityLine.d.ts +19 -1
- package/dist/ui/ActivityLine.js +10 -2
- package/dist/ui/App.d.ts +48 -1
- package/dist/ui/App.js +294 -13
- package/dist/ui/Composer.d.ts +13 -1
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/PromptInput.d.ts +34 -1
- package/dist/ui/PromptInput.js +54 -3
- package/dist/ui/StatusBar.d.ts +40 -0
- package/dist/ui/StatusBar.js +13 -4
- package/dist/ui/TeamPanel.js +7 -1
- package/dist/ui/Transcript.d.ts +21 -1
- package/dist/ui/Transcript.js +32 -2
- package/dist/ui/clipboard.d.ts +52 -0
- package/dist/ui/clipboard.js +91 -0
- package/dist/ui/entries/CompactionCard.d.ts +75 -0
- package/dist/ui/entries/CompactionCard.js +158 -0
- package/dist/ui/frame-differ.d.ts +46 -0
- package/dist/ui/frame-differ.js +94 -10
- package/dist/ui/gauge.d.ts +16 -1
- package/dist/ui/gauge.js +25 -6
- package/dist/ui/glyphs.d.ts +11 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/ScrollViewport.d.ts +43 -6
- package/dist/ui/layout/ScrollViewport.js +128 -21
- package/dist/ui/layout/budget.d.ts +10 -3
- package/dist/ui/layout/budget.js +10 -3
- package/dist/ui/layout/follow-state.d.ts +137 -0
- package/dist/ui/layout/follow-state.js +150 -0
- package/dist/ui/layout/virtual-window.d.ts +11 -0
- package/dist/ui/layout/virtual-window.js +34 -0
- package/dist/ui/overlays/HelpOverlay.js +6 -1
- package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
- package/dist/ui/overlays/SettingsScreen.js +60 -1
- package/dist/ui/palettes.d.ts +16 -0
- package/dist/ui/palettes.js +6 -0
- package/dist/ui/screen.d.ts +22 -0
- package/dist/ui/screen.js +70 -8
- package/dist/ui/selection/highlight.d.ts +39 -0
- package/dist/ui/selection/highlight.js +64 -0
- package/dist/ui/selection/screen-mirror.d.ts +24 -0
- package/dist/ui/selection/screen-mirror.js +35 -0
- package/dist/ui/selection/selection-controller.d.ts +88 -0
- package/dist/ui/selection/selection-controller.js +282 -0
- package/dist/ui/selection/selection.d.ts +90 -0
- package/dist/ui/selection/selection.js +223 -0
- package/dist/ui/stdout-frame-writer.d.ts +33 -0
- package/dist/ui/stdout-frame-writer.js +15 -0
- package/dist/ui/theme.d.ts +11 -0
- package/dist/ui/theme.js +2 -0
- package/dist/ui/transcript-text.js +23 -0
- package/dist/ui/use-startup-notices.d.ts +27 -1
- package/dist/ui/use-startup-notices.js +43 -11
- package/dist/ui/use-wheel-routing.js +8 -0
- package/package.json +4 -2
package/dist/ui/App.js
CHANGED
|
@@ -11,14 +11,15 @@ import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
|
11
11
|
import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js';
|
|
12
12
|
import { mergeDeltas } from '../agent/coalesce.js';
|
|
13
13
|
import { computeCost } from '../agent/usage.js';
|
|
14
|
+
import { isApproximate } from '../compaction/pressure.js';
|
|
14
15
|
import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
|
|
15
16
|
import { updatePersistedConfig } from '../config/store.js';
|
|
16
17
|
import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
|
|
17
|
-
import { bumpSubmitCount,
|
|
18
|
+
import { bumpSubmitCount, setMouseNoticeVersion, setVtInputNoticeVersion, } from '../config/ui-state.js';
|
|
18
19
|
import { getLogger } from '../logging/logger.js';
|
|
19
20
|
import { registerSecret } from '../logging/secret-registry.js';
|
|
20
21
|
import { detectCapabilities } from './capabilities.js';
|
|
21
|
-
import { useStartupNotices, VT_INPUT_NOTICE_VERSION } from './use-startup-notices.js';
|
|
22
|
+
import { useStartupNotices, MOUSE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
|
|
22
23
|
import { getTheme } from './theme.js';
|
|
23
24
|
import { pickGlyphs } from './glyphs.js';
|
|
24
25
|
import { Header } from './Header.js';
|
|
@@ -60,7 +61,7 @@ import { publishExitSnapshot } from './exit-snapshot.js';
|
|
|
60
61
|
import { OverlayFrame } from './layout/OverlayFrame.js';
|
|
61
62
|
import { helpRows } from './overlays/HelpOverlay.js';
|
|
62
63
|
import { ModelPicker } from './overlays/ModelPicker.js';
|
|
63
|
-
import { SettingsScreen, fastSettingsFrom, readFastSettings, } from './overlays/SettingsScreen.js';
|
|
64
|
+
import { SettingsScreen, compactionSettingsFrom, fastSettingsFrom, readCompactionSettings, readFastSettings, } from './overlays/SettingsScreen.js';
|
|
64
65
|
import { ConfirmDialog } from './overlays/ConfirmDialog.js';
|
|
65
66
|
import { QuestionOverlay } from './overlays/QuestionOverlay.js';
|
|
66
67
|
import { PlanReviewOverlay } from './overlays/PlanReviewOverlay.js';
|
|
@@ -68,6 +69,7 @@ import { CommandRegistry, runSlashInput } from '../commands/registry.js';
|
|
|
68
69
|
import { registerBuiltinCommands } from '../commands/builtins.js';
|
|
69
70
|
import { makeSkillsCommand, registerSkillCommands } from '../commands/skills.js';
|
|
70
71
|
import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
|
|
72
|
+
import { emptyTailState } from './layout/follow-state.js';
|
|
71
73
|
/**
|
|
72
74
|
* Decide what the `Max tokens` field means, WITHOUT letting a bad value in it
|
|
73
75
|
* discard the rest of the form.
|
|
@@ -110,7 +112,7 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
110
112
|
function seedViewState(thinkingVisible) {
|
|
111
113
|
return initialViewState({ thinkingVisible });
|
|
112
114
|
}
|
|
113
|
-
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, }) {
|
|
115
|
+
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, terminal, }) {
|
|
114
116
|
const { exit } = useApp();
|
|
115
117
|
const { stdout } = useStdout();
|
|
116
118
|
const fullscreen = mode === 'fullscreen';
|
|
@@ -133,6 +135,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
133
135
|
// function reference.
|
|
134
136
|
const [state, dispatch] = useReducer(viewReducer, cfg.showThinking, seedViewState);
|
|
135
137
|
const [elapsedMs, setElapsedMs] = useState(0);
|
|
138
|
+
/**
|
|
139
|
+
* Forces the render that re-reads `Date.now()` for the live compaction card
|
|
140
|
+
* while the run is IDLE (hardening W5). The value itself is never read - the
|
|
141
|
+
* card's elapsed figure is derived from `nowSec` like the running tool card's.
|
|
142
|
+
*/
|
|
143
|
+
const [, setCompactionClock] = useState(0);
|
|
136
144
|
const [confirmState, setConfirmState] = useState(null);
|
|
137
145
|
/**
|
|
138
146
|
* The updater's latest snapshot, or `null` while there is no service
|
|
@@ -152,6 +160,15 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
152
160
|
const [scrollIntent, setScrollIntent] = useState();
|
|
153
161
|
const [pinToBottomNonce, setPinToBottomNonce] = useState(0);
|
|
154
162
|
const [scrolledLines, setScrolledLines] = useState(0);
|
|
163
|
+
/**
|
|
164
|
+
* True while a selection drag holds the viewport
|
|
165
|
+
* (tui-selection-and-scroll-follow §4.3.1 / §4.4).
|
|
166
|
+
*
|
|
167
|
+
* REACT STATE RATHER THAN A REF, because `ScrollViewport` has to re-arm its
|
|
168
|
+
* resume timer on both edges and a ref cannot drive an effect. The controller
|
|
169
|
+
* is the owner; this is a mirror, pushed by `onHoldChange`.
|
|
170
|
+
*/
|
|
171
|
+
const [selectionHold, setSelectionHold] = useState(false);
|
|
155
172
|
// I-5 (§4.13): Ctrl+L cannot repaint by writing escapes — Ink dedupes
|
|
156
173
|
// identical output twice over — so it bumps this instead, and StatusBar turns
|
|
157
174
|
// it into a one-byte, zero-width change that both dedupe gates let through.
|
|
@@ -211,6 +228,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
211
228
|
* would make reading the render budget cost a render.
|
|
212
229
|
*/
|
|
213
230
|
const mountedCount = useRef(0);
|
|
231
|
+
/**
|
|
232
|
+
* Rows the transcript has appended AT ITS TAIL — Rule A's only legal input
|
|
233
|
+
* (tui-selection-and-scroll-follow §4.3.1a).
|
|
234
|
+
*
|
|
235
|
+
* Owned here because it has TWO readers one level down each: `TranscriptList`
|
|
236
|
+
* writes it during its render, and `ScrollViewport` reads it in the layout
|
|
237
|
+
* effect that runs afterwards. A ref rather than state, exactly like
|
|
238
|
+
* `mountedCount` above and for the same reason.
|
|
239
|
+
*/
|
|
240
|
+
const tailSink = useRef(emptyTailState());
|
|
214
241
|
// --- Follow-through (todo-plan-followthrough §3.4). ---------------------
|
|
215
242
|
//
|
|
216
243
|
// THREE REFS AND NO VIEW STATE, DELIBERATELY (§5.2). Every field added to
|
|
@@ -604,6 +631,97 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
604
631
|
}
|
|
605
632
|
});
|
|
606
633
|
}, [controller]);
|
|
634
|
+
// --- Compaction event stream (context-auto-compaction §5.2). -------------
|
|
635
|
+
//
|
|
636
|
+
// A SIXTH SUBSCRIPTION, for the reason the team, todo and fast ones record:
|
|
637
|
+
// `CompactionEvent` is deliberately not a member of core's `AgentEvent` union,
|
|
638
|
+
// so it cannot arrive through `controller.subscribe`.
|
|
639
|
+
//
|
|
640
|
+
// AND IT IS THE **SOLE** DISPATCHER OF THE FIVE COMPACTION ACTIONS (D-20 /
|
|
641
|
+
// P1-6). `reduceEvent` gains no cases for core's `compaction_start` /
|
|
642
|
+
// `compaction_end` — its `default: return []` is already correct for them —
|
|
643
|
+
// because one compaction arriving through two channels is two transcript cards
|
|
644
|
+
// and a doubled `tokensReclaimed`, with nothing naming the authority. This
|
|
645
|
+
// stream is the right authority because it is strictly richer: the wiring IS
|
|
646
|
+
// the `ContextManager`, so it knows the summarizer's model, its cost and the
|
|
647
|
+
// session totals, and it learns `applied` by subscribing to the core event one
|
|
648
|
+
// layer earlier.
|
|
649
|
+
//
|
|
650
|
+
// `subscribeCompaction` returns a no-op unsubscribe when compaction was off at
|
|
651
|
+
// construction, so this can subscribe unconditionally and an ordinary session
|
|
652
|
+
// pays one closure for the whole run.
|
|
653
|
+
useEffect(() => {
|
|
654
|
+
// Seed the chip and the gauge marks from the snapshot resolved at
|
|
655
|
+
// construction, so a compaction-enabled session shows them on the FIRST
|
|
656
|
+
// frame rather than after the first turn.
|
|
657
|
+
if (controller.isCompactionRegistered()) {
|
|
658
|
+
dispatch({ type: 'compactionSnapshot', snapshot: controller.getCompactionSnapshot() });
|
|
659
|
+
}
|
|
660
|
+
return controller.subscribeCompaction((event) => {
|
|
661
|
+
switch (event.type) {
|
|
662
|
+
case 'compaction_start':
|
|
663
|
+
dispatch({
|
|
664
|
+
type: 'compactionStart',
|
|
665
|
+
index: event.index,
|
|
666
|
+
trigger: event.trigger,
|
|
667
|
+
model: event.model,
|
|
668
|
+
});
|
|
669
|
+
break;
|
|
670
|
+
case 'compaction_end': {
|
|
671
|
+
dispatch({ type: 'compactionEnd', record: event.record });
|
|
672
|
+
// THE GAUGE MUST FALL IMMEDIATELY (§6.2 / AC-5), and it must fall to a
|
|
673
|
+
// number in the SAME UNIT as the one it replaces (P1-11). The value
|
|
674
|
+
// being overwritten came from `occupiedTokens(usage)` — a provider
|
|
675
|
+
// measurement that includes the tool schemas — while
|
|
676
|
+
// `record.tokensAfter` comes from core's `estimatePromptTokens`, which
|
|
677
|
+
// never counts them. Writing the raw estimate makes the bar fall too
|
|
678
|
+
// far and then visibly climb again on the next `turn_end` for no reason
|
|
679
|
+
// the user can see, which is the same is-this-thing-working doubt the
|
|
680
|
+
// immediate drop exists to prevent, one turn later.
|
|
681
|
+
if (event.record.applied) {
|
|
682
|
+
const offset = controller.getCompactionSnapshot().pressure.estimateOffset ?? 0;
|
|
683
|
+
dispatch({ type: 'contextTokensEstimated', tokens: event.record.tokensAfter + offset });
|
|
684
|
+
}
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
case 'usage': {
|
|
688
|
+
// Compaction spend is REAL SPEND at the SUMMARIZER's price (§6.4 /
|
|
689
|
+
// AC-15), which may be neither the lead's nor the fast tier's — so the
|
|
690
|
+
// ref is ASKED FOR rather than assumed. `getModelInfo()` here would
|
|
691
|
+
// price a Haiku summarization at the lead's Opus rates, which is the
|
|
692
|
+
// same class of lie as the `$0.00` `pricingUnknown` exists to prevent,
|
|
693
|
+
// just in the other direction.
|
|
694
|
+
const snapshot = controller.getCompactionSnapshot();
|
|
695
|
+
const ref = controller.getCompactionSummarizerRef();
|
|
696
|
+
const costDelta = snapshot.pricingUnknown || !ref
|
|
697
|
+
? 0
|
|
698
|
+
: computeCost(event.usage, controller.getModelInfoFor(ref).cost);
|
|
699
|
+
dispatch({ type: 'compactionUsage', usage: event.usage, costDelta });
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
case 'snapshot':
|
|
703
|
+
dispatch({ type: 'compactionSnapshot', snapshot: event.snapshot });
|
|
704
|
+
// ONE NUMBER ON SCREEN AND IN THE TRIGGER (R-11 / hardening §3.2.4).
|
|
705
|
+
// The checkpoint's occupancy is a measured base PLUS an estimate of
|
|
706
|
+
// what was appended after it, and without this the gauge would still
|
|
707
|
+
// show the bare base from the last `turn_end` - so a user watching
|
|
708
|
+
// 76 % would see compaction fire and conclude the feature is broken.
|
|
709
|
+
//
|
|
710
|
+
// `contextTokensEstimated` IS REUSED RATHER THAN A SECOND WRITER ADDED:
|
|
711
|
+
// it is the one existing action for "this figure is derived", and it
|
|
712
|
+
// already sets the `~`. Gated on `isApproximate`, which is precisely
|
|
713
|
+
// the case it was built for - a wholly measured figure belongs to
|
|
714
|
+
// `turnEnd`.
|
|
715
|
+
if (isApproximate(event.snapshot.pressure)) {
|
|
716
|
+
dispatch({
|
|
717
|
+
type: 'contextTokensEstimated',
|
|
718
|
+
tokens: event.snapshot.pressure.occupied,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
}, [controller]);
|
|
607
725
|
// --- Live tool output (agent-activity-presentation-live §3.3.2). --------
|
|
608
726
|
//
|
|
609
727
|
// A FIFTH SUBSCRIPTION, and THE ONE THAT DOES GO THROUGH THE STREAMING
|
|
@@ -647,6 +765,17 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
647
765
|
const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 200);
|
|
648
766
|
return () => clearInterval(id);
|
|
649
767
|
}, [state.status]);
|
|
768
|
+
// --- Live compaction elapsed tick (hardening §3.6 / W5). ---------------
|
|
769
|
+
//
|
|
770
|
+
// ONLY WHILE A CARD IS LIVE AND THE RUN IS NOT, so an ordinary session has no
|
|
771
|
+
// timer and an in-loop compaction reuses the 200 ms elapsed ticker rather than
|
|
772
|
+
// adding a second one. 1 Hz, because the card shows whole seconds.
|
|
773
|
+
useEffect(() => {
|
|
774
|
+
if (state.compactionEntryId === undefined || state.status === 'running')
|
|
775
|
+
return undefined;
|
|
776
|
+
const id = setInterval(() => setCompactionClock(Date.now()), 1000);
|
|
777
|
+
return () => clearInterval(id);
|
|
778
|
+
}, [state.compactionEntryId, state.status]);
|
|
650
779
|
// --- Retry countdown tick (llm-api-retry-backoff §6.5). ----------------
|
|
651
780
|
//
|
|
652
781
|
// ONLY WHILE A BACKOFF IS ACTUALLY WAITING, and the effect's teardown clears it —
|
|
@@ -697,9 +826,17 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
697
826
|
// so on a console that reports nothing there is no source and no advice.
|
|
698
827
|
useStartupNotices(dispatch, {
|
|
699
828
|
enabled: !!mouseSource,
|
|
829
|
+
// NOT re-derived here (see `MouseNoticeOptions`): `cli.tsx` computes
|
|
830
|
+
// `mouseFilter !== null && cfg.mouseSelect` once and four things read it,
|
|
831
|
+
// so a second derivation is a second chance for them to disagree.
|
|
832
|
+
selectEnabled: terminal?.mouseSelect ?? false,
|
|
700
833
|
// `state.json`, not `config.json`: whether a one-shot notice has been shown
|
|
701
834
|
// is bookkeeping, and the comment on the old persisted key said so.
|
|
702
|
-
|
|
835
|
+
//
|
|
836
|
+
// THE VERSION, NOT `true` (P1-8). The gate compares against
|
|
837
|
+
// `MOUSE_NOTICE_VERSION`, so storing anything else either replays the
|
|
838
|
+
// notice forever or silences a future revision of the text.
|
|
839
|
+
onSeen: () => setMouseNoticeVersion(MOUSE_NOTICE_VERSION),
|
|
703
840
|
},
|
|
704
841
|
// Present only when `loadConfig` had to raise the user's own
|
|
705
842
|
// `transcriptRetain` to `transcriptWindow` (tui-render-performance §5.1).
|
|
@@ -888,6 +1025,83 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
888
1025
|
// every render and defeat the ref guard's purpose rather than its mechanism.
|
|
889
1026
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
890
1027
|
}, [updateSnapshot?.phase, updateSnapshot?.latestVersion]);
|
|
1028
|
+
// --- Selection (tui-selection-and-scroll-follow §4.4). -------------------
|
|
1029
|
+
//
|
|
1030
|
+
// The controller is built by `cli.tsx` before `render()`, so what it is
|
|
1031
|
+
// missing when it starts is exactly the two things that only exist once this
|
|
1032
|
+
// component is mounted: somewhere to put a toast, and a way to ask for a React
|
|
1033
|
+
// redraw when the frame differ has no cache to repaint from. Both are assigned
|
|
1034
|
+
// here and cleared on unmount, the same shape `updateBridge` uses.
|
|
1035
|
+
const selectionBridge = terminal?.selection;
|
|
1036
|
+
useEffect(() => {
|
|
1037
|
+
if (!selectionBridge)
|
|
1038
|
+
return undefined;
|
|
1039
|
+
selectionBridge.onCopied = (via, lines, chars) => {
|
|
1040
|
+
// IT NAMES THE MECHANISM RATHER THAN CLAIMING SUCCESS (R-7). Neither OSC 52
|
|
1041
|
+
// nor a spawned `xclip` is detectable: tmux without `set -g set-clipboard
|
|
1042
|
+
// on` swallows the first silently, and the second is best-effort by
|
|
1043
|
+
// construction. A toast that said "copied successfully" would be guessing.
|
|
1044
|
+
const noun = lines === 1 ? 'line' : 'lines';
|
|
1045
|
+
if (via === 'none') {
|
|
1046
|
+
toast('warn', 'Nothing could be copied - no clipboard mechanism is available.');
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const how = via === 'osc52' ? 'terminal clipboard' : 'system clipboard';
|
|
1050
|
+
toast('success', `Sent ${lines} ${noun} (${chars} chars) to the ${how}.`);
|
|
1051
|
+
};
|
|
1052
|
+
// I-5 of `frame-differ.ts`: writing escapes here would be a foreign write.
|
|
1053
|
+
// The nonce is the carrier Ctrl+L already uses (§4.13).
|
|
1054
|
+
selectionBridge.requestRedraw = () => setRedrawNonce((n) => n + 1);
|
|
1055
|
+
const unsubscribe = selectionBridge.controller?.onHoldChange(setSelectionHold);
|
|
1056
|
+
return () => {
|
|
1057
|
+
selectionBridge.onCopied = null;
|
|
1058
|
+
selectionBridge.requestRedraw = null;
|
|
1059
|
+
unsubscribe?.();
|
|
1060
|
+
};
|
|
1061
|
+
// `toast` is a stable dispatch wrapper; depending on it would rebuild this
|
|
1062
|
+
// subscription on every render.
|
|
1063
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1064
|
+
}, [selectionBridge]);
|
|
1065
|
+
const selectionController = selectionBridge?.controller ?? null;
|
|
1066
|
+
/**
|
|
1067
|
+
* I-9, the DERIVED half — and the one that matters (D-14 / P1-4).
|
|
1068
|
+
*
|
|
1069
|
+
* `shiftUp` changes exactly when the rows on screen move: a wheel notch, a key,
|
|
1070
|
+
* PgDn, the bottom snap, `pinToBottomNonce`, the idle auto-resume, a height
|
|
1071
|
+
* correction above the reading position. An enumerated list of gestures was
|
|
1072
|
+
* already incomplete on the day it was written — it missed this feature's OWN
|
|
1073
|
+
* auto-resume, which would have left a five-second-old highlight sitting over
|
|
1074
|
+
* rows that had scrolled out from under it — and a derived condition cannot be
|
|
1075
|
+
* forgotten by the next person who adds a way to move the viewport.
|
|
1076
|
+
*
|
|
1077
|
+
* Rule A holds `shiftUp` CONSTANT while a drag is in progress, which is why a
|
|
1078
|
+
* selection survives output streaming in below it and only that.
|
|
1079
|
+
*
|
|
1080
|
+
* Referentially stable, because `ScrollViewport` publishes it from an effect
|
|
1081
|
+
* keyed on the callback.
|
|
1082
|
+
*/
|
|
1083
|
+
const onViewportShiftChange = useCallback(() => {
|
|
1084
|
+
selectionController?.clear();
|
|
1085
|
+
}, [selectionController]);
|
|
1086
|
+
// I-9, the ENUMERATED half. `shiftUp` covers every movement of the rows and is
|
|
1087
|
+
// wired straight into `ScrollViewport` below; these three are the cases that
|
|
1088
|
+
// change the screen while leaving `shiftUp` untouched — a key that scrolls
|
|
1089
|
+
// nothing, an overlay swap, and a re-wrap (P1-7).
|
|
1090
|
+
useEffect(() => {
|
|
1091
|
+
if (!selectionController)
|
|
1092
|
+
return;
|
|
1093
|
+
// Screen coordinates stop meaning anything the moment every line re-wraps,
|
|
1094
|
+
// and a resize is exactly that (N6).
|
|
1095
|
+
selectionController.clear();
|
|
1096
|
+
}, [selectionController, rows, cols]);
|
|
1097
|
+
useEffect(() => {
|
|
1098
|
+
if (!selectionController)
|
|
1099
|
+
return;
|
|
1100
|
+
// An overlay replaces the viewport, so a selection over it would be a
|
|
1101
|
+
// highlight over rows that are no longer there (N4).
|
|
1102
|
+
selectionController.setEnabled(state.overlay === null);
|
|
1103
|
+
selectionController.clear();
|
|
1104
|
+
}, [selectionController, state.overlay]);
|
|
891
1105
|
const persistConfig = (patch) => {
|
|
892
1106
|
try {
|
|
893
1107
|
updatePersistedConfig(patch);
|
|
@@ -951,6 +1165,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
951
1165
|
// cache, and `/update` would report "disabled" on a session where it is on.
|
|
952
1166
|
// `?? undefined` because the bridge holds `null` and the port is optional.
|
|
953
1167
|
update: updateBridge?.service ?? undefined,
|
|
1168
|
+
// Read at command time so `/mouse` reports the LIVE state rather than
|
|
1169
|
+
// whatever it was when this closure was first built — `/mouse off` followed
|
|
1170
|
+
// by a bare `/mouse` has to say "off".
|
|
1171
|
+
...(terminal?.setMouseCapture
|
|
1172
|
+
? {
|
|
1173
|
+
mouse: {
|
|
1174
|
+
captured: () => terminal.isMouseCaptured?.() ?? true,
|
|
1175
|
+
selectEnabled: () => terminal.mouseSelect,
|
|
1176
|
+
setCapture: (on) => terminal.setMouseCapture?.(on),
|
|
1177
|
+
},
|
|
1178
|
+
}
|
|
1179
|
+
: {}),
|
|
1180
|
+
...(terminal?.writeForeign ? { writeForeign: terminal.writeForeign } : {}),
|
|
954
1181
|
});
|
|
955
1182
|
/**
|
|
956
1183
|
* Submitting a prompt no longer touches `config.json` at all — it appends one
|
|
@@ -1079,6 +1306,23 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1079
1306
|
const fastPatch = readFastSettings(values);
|
|
1080
1307
|
controller.setFastConfig(fastPatch);
|
|
1081
1308
|
patch.fast = fastPatch;
|
|
1309
|
+
// --- Context compaction (context-auto-compaction §4.5) ------------------
|
|
1310
|
+
//
|
|
1311
|
+
// THE SAME SETTER-AND-PERSIST PAIR, and the same reason. `setCompactionConfig`
|
|
1312
|
+
// additionally re-emits the snapshot, which is what moves the status chip and
|
|
1313
|
+
// the gauge's marks without waiting for the next turn.
|
|
1314
|
+
//
|
|
1315
|
+
// `enabled` here flips the LIVE switch only. Whether the wiring exists at all
|
|
1316
|
+
// was decided at construction and cannot be undone in this session (§3.2), so
|
|
1317
|
+
// a user who turns it ON in a `--no-compaction` session gets the persisted
|
|
1318
|
+
// value for next launch and nothing this session — which is exactly what
|
|
1319
|
+
// `/compact on` reports in words.
|
|
1320
|
+
const compactionPatch = readCompactionSettings(values);
|
|
1321
|
+
controller.setCompactionConfig(compactionPatch);
|
|
1322
|
+
if (compactionPatch.enabled !== undefined) {
|
|
1323
|
+
controller.setCompactionEnabled(compactionPatch.enabled);
|
|
1324
|
+
}
|
|
1325
|
+
patch.compaction = compactionPatch;
|
|
1082
1326
|
persistConfig(patch);
|
|
1083
1327
|
getLogger().reconfigure({ ...controller.getConfig().log, level: values.logLevel });
|
|
1084
1328
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
@@ -1152,6 +1396,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1152
1396
|
}, []);
|
|
1153
1397
|
// --- Global keys -------------------------------------------------------
|
|
1154
1398
|
useInput((input, key) => {
|
|
1399
|
+
// I-9, the third clear trigger, and FIRST IN THE HANDLER so no `return`
|
|
1400
|
+
// below can skip it. Any key at all drops the selection: most of them move
|
|
1401
|
+
// the rows, and the ones that do not have still ended the gesture. It is
|
|
1402
|
+
// also the way out of a `hold` that lost its release (I-11), alongside the
|
|
1403
|
+
// watchdog.
|
|
1404
|
+
//
|
|
1405
|
+
// `Esc` IS DELIBERATELY NOT GIVEN A SECOND MEANING (§4.4.6). It aborts a
|
|
1406
|
+
// run, and an emergency exit is the last key that should silently absorb a
|
|
1407
|
+
// press; it clears the selection here like every other key and nothing more.
|
|
1408
|
+
selectionController?.clear();
|
|
1155
1409
|
if (key.ctrl && (input === 'c' || input === 'C')) {
|
|
1156
1410
|
if (ctrlCArmed.current) {
|
|
1157
1411
|
doExit();
|
|
@@ -1266,13 +1520,21 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1266
1520
|
// `team` joins `tool` here (team-subagents §6.3): a dispatch card collapses
|
|
1267
1521
|
// its per-agent summaries behind the same key, and `toggleExpand` is keyed
|
|
1268
1522
|
// on entry id so it needs no change.
|
|
1523
|
+
// `compaction` joins `tool` and `team` here (context-auto-compaction §6.3 /
|
|
1524
|
+
// C-15 / P1-8). `expandedToolIds` is keyed by entry id so the STORAGE was
|
|
1525
|
+
// already generic — but both surfaces that CHOOSE a target filter by kind,
|
|
1526
|
+
// and without this the compaction card renders its own `ctrl+o to expand`
|
|
1527
|
+
// hint, the keystroke silently expands an older tool card instead, and
|
|
1528
|
+
// nothing errors. AC-21 is the assertion that the hint does not lie.
|
|
1269
1529
|
const last = [...stateRef.current.entries]
|
|
1270
1530
|
.reverse()
|
|
1271
|
-
.find((e) => e.kind === 'tool' || e.kind === 'team');
|
|
1531
|
+
.find((e) => e.kind === 'tool' || e.kind === 'team' || e.kind === 'compaction');
|
|
1272
1532
|
if (last)
|
|
1273
1533
|
dispatch({ type: 'toggleExpand', id: last.id });
|
|
1534
|
+
// WIDENED FROM `No tool output to expand.`: with three expandable kinds the
|
|
1535
|
+
// old wording would be wrong in two of the three empty cases.
|
|
1274
1536
|
else
|
|
1275
|
-
toast('info', '
|
|
1537
|
+
toast('info', 'Nothing to expand.');
|
|
1276
1538
|
return;
|
|
1277
1539
|
}
|
|
1278
1540
|
if (key.escape) {
|
|
@@ -1332,6 +1594,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1332
1594
|
// every single turn, which is the one moment this feature exists to make feel
|
|
1333
1595
|
// calm.
|
|
1334
1596
|
const running = state.status === 'running';
|
|
1597
|
+
// The live compaction card's clock (hardening W5). While the run is going the
|
|
1598
|
+
// 200 ms elapsed ticker already causes the render that re-reads this; an IDLE
|
|
1599
|
+
// `/compact` has no ticker at all, which is what `compactionClock` is for.
|
|
1600
|
+
const compactionLive = state.compactionEntryId !== undefined;
|
|
1335
1601
|
if (running && runStartedAt.current === 0)
|
|
1336
1602
|
runStartedAt.current = Date.now();
|
|
1337
1603
|
if (!running && runStartedAt.current !== 0)
|
|
@@ -1357,7 +1623,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1357
1623
|
// that re-reads it — so no timer is added (D-36 / E-15). Seconds rather than
|
|
1358
1624
|
// milliseconds because `ToolCard` is `React.memo`'d with the default
|
|
1359
1625
|
// comparator; `Transcript` passes it on only to running tool entries.
|
|
1360
|
-
const nowSec = running ? Math.floor(Date.now() / 1000) : undefined;
|
|
1626
|
+
const nowSec = running || compactionLive ? Math.floor(Date.now() / 1000) : undefined;
|
|
1361
1627
|
// Row budget for the viewport and everything sized against it (§4.2a). The
|
|
1362
1628
|
// inline path has no fixed frame, hence no height to fit into: `Infinity`
|
|
1363
1629
|
// tells `OverlayFrame` to render everything and show no position indicator.
|
|
@@ -1425,6 +1691,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1425
1691
|
// The four fast rows, seeded through the pure mapper so the screen and
|
|
1426
1692
|
// the save handler cannot disagree about what `(inherit)` means.
|
|
1427
1693
|
...fastSettingsFrom(cfg.fast),
|
|
1694
|
+
...compactionSettingsFrom(cfg.compaction),
|
|
1428
1695
|
}, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave,
|
|
1429
1696
|
// Read from `cfg` (render scope, re-read every frame), so `/retry off`
|
|
1430
1697
|
// while the screen is open shows the new value on the next paint.
|
|
@@ -1500,7 +1767,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1500
1767
|
viewportRows: fullscreen ? viewportBudget : rows,
|
|
1501
1768
|
offset: scrolledLines,
|
|
1502
1769
|
};
|
|
1503
|
-
const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps }), overlayNode] }));
|
|
1770
|
+
const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, onViewportShiftChange: onViewportShiftChange, tailRowsRef: tailSink, hold: selectionHold, resumeMs: cfg.scrollResumeMs, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount, tailSink: tailSink })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps }), overlayNode] }));
|
|
1504
1771
|
// The completion popup lives inside the bottom chrome and can add up to 9
|
|
1505
1772
|
// rows; unbounded, it pushes the status bar past the frame height where
|
|
1506
1773
|
// `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
|
|
@@ -1515,7 +1782,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1515
1782
|
// Both keys always WORK (they share one branch in the input handler); this
|
|
1516
1783
|
// only decides which one the user is taught.
|
|
1517
1784
|
const modeToggleKey = vtInputWarning ? MODE_TOGGLE_KEYS.fallback : MODE_TOGGLE_KEYS.primary;
|
|
1518
|
-
const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
|
|
1785
|
+
const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, scrolledLines: scrolledLines, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
|
|
1519
1786
|
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, header: header, viewport: viewport, team: state.team ? (_jsx(TeamPanel, { snapshot: state.team, rows: rows, cols: cols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null, rail: showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null, strip: showStrip && state.todos ? (_jsx(TodoStrip, { snapshot: state.todos, cols: cols, theme: theme, caps: caps })) : null, toast: _jsx(BottomStatusRow, { mode: mode, toasts: state.toasts,
|
|
1520
1787
|
// `!overlayNode` suppresses the line while an overlay owns the screen:
|
|
1521
1788
|
// the full-screen branch replaces the whole viewport with the overlay,
|
|
@@ -1528,7 +1795,13 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1528
1795
|
// SIGNAL IS DERIVED FROM (D-4): inlining it here again is how the two
|
|
1529
1796
|
// drift apart, and the drift is silent in whichever branch is not
|
|
1530
1797
|
// edited.
|
|
1531
|
-
activity: activityVisible ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool,
|
|
1798
|
+
activity: activityVisible ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool,
|
|
1799
|
+
// THE ROW MUST NEVER SAY SOMETHING FALSE (§6.1). Compaction runs
|
|
1800
|
+
// BEFORE `turn_start`, so `status` is `'running'` with no tool in
|
|
1801
|
+
// flight — precisely the window in which the rotating phrase
|
|
1802
|
+
// would claim the model is thinking while it is actually waiting
|
|
1803
|
+
// on a 30k-token summarization.
|
|
1804
|
+
compacting: state.compaction?.inFlight === true, theme: theme, caps: caps })) : null,
|
|
1532
1805
|
// THE FORM THE LIFE SIGNAL TAKES WHEN A TOAST HAS THE ROW
|
|
1533
1806
|
// (`activity-spinner-vanishes-behind-toast`). Passed UNCONDITIONALLY:
|
|
1534
1807
|
// `BottomStatusRow` reads it only when `activity` above is non-null,
|
|
@@ -1548,7 +1821,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1548
1821
|
// branch replaces the whole viewport with the overlay, and an update
|
|
1549
1822
|
// notice under a settings screen is noise attached to a surface it is
|
|
1550
1823
|
// not visible on.
|
|
1551
|
-
update: updateSnapshot && !overlayNode && shouldRenderUpdateLine(updateSnapshot) ? (_jsx(UpdateLine, { snapshot: updateSnapshot, compact: cols < UPDATE_LIMITS.statusCompactCols, theme: theme, caps: caps })) : null, theme: theme }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ecoRung: governor.rung, ...(state.team
|
|
1824
|
+
update: updateSnapshot && !overlayNode && shouldRenderUpdateLine(updateSnapshot) ? (_jsx(UpdateLine, { snapshot: updateSnapshot, compact: cols < UPDATE_LIMITS.statusCompactCols, theme: theme, caps: caps })) : null, theme: theme }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, contextEstimated: state.contextTokensEstimated, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ecoRung: governor.rung, ...(state.team
|
|
1552
1825
|
? {
|
|
1553
1826
|
teamActive: {
|
|
1554
1827
|
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
@@ -1558,7 +1831,15 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1558
1831
|
}
|
|
1559
1832
|
: {}), ...(state.todos && !showRail
|
|
1560
1833
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
1561
|
-
: {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.
|
|
1834
|
+
: {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.compaction?.live
|
|
1835
|
+
? {
|
|
1836
|
+
compactionActive: { inFlight: state.compaction.inFlight },
|
|
1837
|
+
gaugeMarks: {
|
|
1838
|
+
warn: cfg.compaction.warnThreshold * 100,
|
|
1839
|
+
high: cfg.compaction.threshold * 100,
|
|
1840
|
+
},
|
|
1841
|
+
}
|
|
1842
|
+
: {}), ...(state.retry
|
|
1562
1843
|
? {
|
|
1563
1844
|
retryActive: {
|
|
1564
1845
|
attempt: state.retry.attempt,
|
package/dist/ui/Composer.d.ts
CHANGED
|
@@ -48,9 +48,21 @@ export interface ComposerProps {
|
|
|
48
48
|
*/
|
|
49
49
|
modeToggleKey?: string;
|
|
50
50
|
popupMaxRows?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Rows of newer output below the viewport, forwarded to the input row's chip
|
|
53
|
+
* (tui-selection-and-scroll-follow §4.2 / G7).
|
|
54
|
+
*
|
|
55
|
+
* IT IS STATE, NOT A FADEABLE HINT, and it therefore does NOT pass through the
|
|
56
|
+
* `showHint` / `hintsEnabled` gate below. Those two govern the teaching row —
|
|
57
|
+
* which retires keys the user has demonstrably learned and is dropped outright
|
|
58
|
+
* on a short terminal — and "you are 12 rows behind the newest output" is
|
|
59
|
+
* neither learnable nor optional. Same argument that keeps `esc abort`
|
|
60
|
+
* un-faded in `hintText`.
|
|
61
|
+
*/
|
|
62
|
+
scrolledLines?: number;
|
|
51
63
|
theme: Theme;
|
|
52
64
|
caps: TermCapabilities;
|
|
53
65
|
onSubmit: (text: string) => void;
|
|
54
66
|
onHelp?: () => void;
|
|
55
67
|
}
|
|
56
|
-
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, modeToggleKey, popupMaxRows, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
|
68
|
+
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, modeToggleKey, popupMaxRows, scrolledLines, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
package/dist/ui/Composer.js
CHANGED
|
@@ -56,7 +56,7 @@ function hintText(running, submitCount, glyphs, mode, toggleKey) {
|
|
|
56
56
|
'? help',
|
|
57
57
|
].join(dot);
|
|
58
58
|
}
|
|
59
|
-
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, modeToggleKey = MODE_TOGGLE_KEYS.primary, popupMaxRows, theme, caps, onSubmit, onHelp, }) {
|
|
59
|
+
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, modeToggleKey = MODE_TOGGLE_KEYS.primary, popupMaxRows, scrolledLines = 0, theme, caps, onSubmit, onHelp, }) {
|
|
60
60
|
const [hasDraft, setHasDraft] = useState(false);
|
|
61
61
|
const onDraftChange = useCallback((next) => setHasDraft(next), []);
|
|
62
62
|
// Plan mode inserts ONE branch, above `draft` and below `running`. The
|
|
@@ -73,7 +73,7 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
|
|
|
73
73
|
: theme.idleBorder ?? theme.border;
|
|
74
74
|
const glyphs = pickGlyphs(caps);
|
|
75
75
|
const visible = showHint && hintsEnabled;
|
|
76
|
-
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange }), visible && (
|
|
76
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, scrolledLines: scrolledLines }), visible && (
|
|
77
77
|
// The chip rides the EXISTING hint row rather than adding one, so it
|
|
78
78
|
// costs zero rows on a 24-row terminal.
|
|
79
79
|
_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText(running, submitCount, glyphs, agentMode, modeToggleKey) })] }))] }));
|
package/dist/ui/PromptInput.d.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import React from 'react';
|
|
22
22
|
import type { Theme } from './theme.js';
|
|
23
23
|
import type { TermCapabilities } from './capabilities.js';
|
|
24
|
+
import { pickGlyphs } from './glyphs.js';
|
|
24
25
|
import { type EditIntent } from '../input/keymap.js';
|
|
25
26
|
import { type Suggestion } from './AutocompletePopup.js';
|
|
26
27
|
import type { AgentMode } from '../agent/agent-mode.js';
|
|
@@ -65,7 +66,39 @@ interface PromptInputProps {
|
|
|
65
66
|
};
|
|
66
67
|
/** Fires only when the draft flips empty ⇄ non-empty, for the border color. */
|
|
67
68
|
onDraftChange?: (hasDraft: boolean) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Rows of newer output hidden BELOW the viewport — `ScrollViewport`'s offset
|
|
71
|
+
* (tui-selection-and-scroll-follow §4.2 / G7).
|
|
72
|
+
*
|
|
73
|
+
* Rendered as a right-aligned chip ON THE INPUT ROW, INSIDE THE BORDER, which
|
|
74
|
+
* is what the requirement asks for (输入框内部) and what makes the hint cost
|
|
75
|
+
* zero rows. It used to take the viewport's own last row, which spent a line of
|
|
76
|
+
* the user's content exactly when the transcript was longest AND fed the number
|
|
77
|
+
* back into its own derivation (§3.3).
|
|
78
|
+
*
|
|
79
|
+
* IT IS STATE, NOT A HINT. It deliberately ignores `showHint` / `hintsEnabled`
|
|
80
|
+
* — those govern the teaching row BELOW the box, which fades with experience
|
|
81
|
+
* and is dropped on a short terminal. "You are 12 rows behind the newest
|
|
82
|
+
* output" must survive both (T-23).
|
|
83
|
+
*
|
|
84
|
+
* Only meaningful in full-screen mode, so it renders only when `bordered` is
|
|
85
|
+
* set: inline mode has no self-drawn viewport and this is always 0 there.
|
|
86
|
+
*/
|
|
87
|
+
scrolledLines?: number;
|
|
68
88
|
}
|
|
89
|
+
export interface ScrollChip {
|
|
90
|
+
text: string;
|
|
91
|
+
/** FIXED width, so the number gaining a digit cannot re-wrap the draft (P2-4). */
|
|
92
|
+
cells: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Build the chip, or `null` when the viewport is pinned to the newest output.
|
|
96
|
+
*
|
|
97
|
+
* Every glyph comes from `pickGlyphs` and none from a literal (I-5): on a legacy
|
|
98
|
+
* console this renders `v12` rather than mojibake, and `glyphs.test.ts`'s static
|
|
99
|
+
* scan fails the build if that rule is broken here.
|
|
100
|
+
*/
|
|
101
|
+
export declare function scrollChip(scrolledLines: number, cols: number, glyphs: ReturnType<typeof pickGlyphs>): ScrollChip | null;
|
|
69
102
|
/** Apply an edit intent as a pure buffer/cursor transform. */
|
|
70
103
|
export declare function applyEdit(buffer: string, cursor: number, intent: EditIntent): {
|
|
71
104
|
buffer: string;
|
|
@@ -94,5 +127,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
|
|
|
94
127
|
end: number;
|
|
95
128
|
query: string;
|
|
96
129
|
} | null;
|
|
97
|
-
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }: PromptInputProps): React.ReactElement;
|
|
130
|
+
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, scrolledLines, }: PromptInputProps): React.ReactElement;
|
|
98
131
|
export {};
|