@aragon-agent/cli 0.5.0 → 0.5.2
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 +158 -0
- package/README.md +254 -7
- package/dist/agent/agent-mode.d.ts +53 -0
- package/dist/agent/agent-mode.js +69 -0
- package/dist/agent/controller.d.ts +69 -0
- package/dist/agent/controller.js +154 -1
- package/dist/agent/plan-prompt.d.ts +18 -0
- package/dist/agent/plan-prompt.js +93 -0
- package/dist/agent/reducer.d.ts +14 -1
- package/dist/agent/reducer.js +9 -0
- package/dist/agent/system-prompt.d.ts +15 -0
- package/dist/agent/system-prompt.js +8 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +247 -23
- package/dist/commands/builtins.js +60 -0
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +71 -4
- package/dist/config/app-paths.js +139 -8
- package/dist/config/cli-commands.d.ts +43 -0
- package/dist/config/cli-commands.js +196 -0
- package/dist/config/env.d.ts +7 -1
- package/dist/config/env.js +53 -1
- package/dist/config/load.d.ts +10 -0
- package/dist/config/load.js +82 -2
- package/dist/config/migrate-home.d.ts +63 -0
- package/dist/config/migrate-home.js +165 -0
- package/dist/config/migrate-legacy-state.d.ts +10 -0
- package/dist/config/migrate-legacy-state.js +15 -6
- package/dist/config/schema.d.ts +81 -0
- package/dist/config/schema.js +85 -0
- package/dist/config/store.d.ts +30 -12
- package/dist/config/store.js +47 -35
- package/dist/input/mouse-events.d.ts +61 -0
- package/dist/input/mouse-events.js +153 -0
- package/dist/input/stdin-mouse-filter.d.ts +42 -0
- package/dist/input/stdin-mouse-filter.js +153 -0
- package/dist/logging/cli-commands.d.ts +25 -0
- package/dist/logging/cli-commands.js +221 -0
- package/dist/logging/file-sink.d.ts +114 -0
- package/dist/logging/file-sink.js +326 -0
- package/dist/logging/install.d.ts +163 -0
- package/dist/logging/install.js +302 -0
- package/dist/logging/levels.d.ts +40 -0
- package/dist/logging/levels.js +59 -0
- package/dist/logging/logger.d.ts +110 -0
- package/dist/logging/logger.js +237 -0
- package/dist/logging/redact.d.ts +28 -0
- package/dist/logging/redact.js +90 -0
- package/dist/logging/secret-registry.d.ts +30 -0
- package/dist/logging/secret-registry.js +55 -0
- package/dist/skills/paths.d.ts +2 -4
- package/dist/skills/paths.js +7 -9
- package/dist/tools/human-input.d.ts +179 -0
- package/dist/tools/human-input.js +227 -0
- package/dist/tools/index.d.ts +35 -3
- package/dist/tools/index.js +96 -9
- package/dist/tools/plan-tools.d.ts +64 -0
- package/dist/tools/plan-tools.js +198 -0
- package/dist/ui/App.d.ts +12 -1
- package/dist/ui/App.js +223 -17
- package/dist/ui/Composer.d.ts +9 -1
- package/dist/ui/Composer.js +26 -7
- package/dist/ui/ModeChip.d.ts +29 -0
- package/dist/ui/ModeChip.js +15 -0
- package/dist/ui/PromptInput.d.ts +22 -1
- package/dist/ui/PromptInput.js +49 -2
- package/dist/ui/StatusBar.d.ts +15 -0
- package/dist/ui/StatusBar.js +10 -2
- package/dist/ui/entries/ToolPreview.d.ts +2 -0
- package/dist/ui/entries/ToolPreview.js +64 -0
- package/dist/ui/glyphs.d.ts +21 -0
- package/dist/ui/glyphs.js +12 -0
- package/dist/ui/layout/AppShell.d.ts +10 -1
- package/dist/ui/layout/AppShell.js +38 -3
- package/dist/ui/layout/ScrollIndicator.d.ts +31 -0
- package/dist/ui/layout/ScrollIndicator.js +15 -0
- package/dist/ui/layout/ScrollViewport.d.ts +8 -2
- package/dist/ui/layout/ScrollViewport.js +24 -4
- package/dist/ui/layout/regions.d.ts +55 -0
- package/dist/ui/layout/regions.js +52 -0
- package/dist/ui/layout/scroll-indicator.d.ts +27 -0
- package/dist/ui/layout/scroll-indicator.js +35 -0
- package/dist/ui/layout/scroll.d.ts +9 -0
- package/dist/ui/layout/scroll.js +23 -0
- package/dist/ui/layout/wrap-rows.d.ts +24 -0
- package/dist/ui/layout/wrap-rows.js +74 -0
- package/dist/ui/overlays/HelpOverlay.js +15 -0
- package/dist/ui/overlays/PlanReviewOverlay.d.ts +50 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +117 -0
- package/dist/ui/overlays/QuestionOverlay.d.ts +30 -0
- package/dist/ui/overlays/QuestionOverlay.js +193 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
- package/dist/ui/overlays/SettingsScreen.js +8 -1
- package/dist/ui/screen.d.ts +14 -1
- package/dist/ui/screen.js +37 -3
- package/dist/ui/use-startup-notices.d.ts +25 -0
- package/dist/ui/use-startup-notices.js +64 -0
- package/dist/ui/use-wheel-routing.d.ts +68 -0
- package/dist/ui/use-wheel-routing.js +139 -0
- package/package.json +3 -2
- package/runtime/insecure-tls-warning.cjs +62 -0
- package/runtime/insecure-tls-warning.d.cts +5 -0
|
@@ -17,17 +17,23 @@ import type { Theme } from '../theme.js';
|
|
|
17
17
|
import type { TermCapabilities } from '../capabilities.js';
|
|
18
18
|
import { type ScrollIntent } from './scroll.js';
|
|
19
19
|
export interface ScrollViewportProps {
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Scroll intent. `nonce` is monotonic so pressing the same key twice re-fires.
|
|
22
|
+
* `repeat` folds a coalesced wheel burst into one state update (defaults 1).
|
|
23
|
+
*/
|
|
21
24
|
intent?: {
|
|
22
25
|
kind: ScrollIntent;
|
|
23
26
|
nonce: number;
|
|
27
|
+
repeat?: number;
|
|
24
28
|
};
|
|
25
29
|
/** Bumped by the parent to force a pin to the newest output (e.g. on submit). */
|
|
26
30
|
pinToBottomNonce?: number;
|
|
27
31
|
/** Derived display value for the status bar's `↑N`. Must be referentially stable. */
|
|
28
32
|
onScrolledLinesChange?: (lines: number) => void;
|
|
33
|
+
/** Reserve the right-edge scroll-position rail (mouse-wheel §4.8). */
|
|
34
|
+
showScrollIndicator?: boolean;
|
|
29
35
|
theme: Theme;
|
|
30
36
|
caps: TermCapabilities;
|
|
31
37
|
children: React.ReactNode;
|
|
32
38
|
}
|
|
33
|
-
export declare function ScrollViewport({ intent, pinToBottomNonce, onScrolledLinesChange, theme, caps, children, }: ScrollViewportProps): React.ReactElement;
|
|
39
|
+
export declare function ScrollViewport({ intent, pinToBottomNonce, onScrolledLinesChange, showScrollIndicator, theme, caps, children, }: ScrollViewportProps): React.ReactElement;
|
|
@@ -16,10 +16,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
16
16
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
17
17
|
import { Box, Text, measureElement, useStdout } from 'ink';
|
|
18
18
|
import { pickGlyphs } from '../glyphs.js';
|
|
19
|
-
import {
|
|
19
|
+
import { applyScrollTimes, clampScroll, } from './scroll.js';
|
|
20
|
+
import { thumbRange } from './scroll-indicator.js';
|
|
21
|
+
import { MIN_INDICATOR_COLS, ScrollIndicator } from './ScrollIndicator.js';
|
|
20
22
|
/** Below this the full hint sentence wraps and eats a second viewport row. */
|
|
21
23
|
const TERSE_HINT_COLS = 42;
|
|
22
|
-
export function ScrollViewport({ intent, pinToBottomNonce = 0, onScrolledLinesChange, theme, caps, children, }) {
|
|
24
|
+
export function ScrollViewport({ intent, pinToBottomNonce = 0, onScrolledLinesChange, showScrollIndicator = false, theme, caps, children, }) {
|
|
23
25
|
const { stdout } = useStdout();
|
|
24
26
|
const cols = stdout?.columns ?? 80;
|
|
25
27
|
const glyphs = pickGlyphs(caps);
|
|
@@ -38,12 +40,26 @@ export function ScrollViewport({ intent, pinToBottomNonce = 0, onScrolledLinesCh
|
|
|
38
40
|
const content = innerRef.current ? measureElement(innerRef.current).height : 0;
|
|
39
41
|
setMetrics((prev) => (prev.viewport === viewport && prev.content === content ? prev : { viewport, content }));
|
|
40
42
|
});
|
|
43
|
+
// I-9: seeded from the INCOMING nonce during the first render, so the effect
|
|
44
|
+
// below cannot apply an intent on the mount that first observes it. A
|
|
45
|
+
// `useEffect([nonce])` fires on mount as well as on change, and this
|
|
46
|
+
// component is unmounted whenever an overlay opens (`App.tsx` swaps it out) —
|
|
47
|
+
// without the guard, closing an overlay replays whatever wheel gesture was in
|
|
48
|
+
// flight when it opened, and the transcript jumps a page on its own. That is
|
|
49
|
+
// R-P1-7, which this codebase has already paid for once through the keyboard
|
|
50
|
+
// path; the overlay-swallowing rule in `App`'s key handler becomes a SECOND
|
|
51
|
+
// line of defence rather than the only one.
|
|
52
|
+
const lastAppliedNonce = useRef(intent?.nonce);
|
|
41
53
|
const intentNonce = intent?.nonce ?? 0;
|
|
42
54
|
useEffect(() => {
|
|
43
55
|
const current = intentRef.current;
|
|
44
56
|
if (!current)
|
|
45
57
|
return;
|
|
46
|
-
|
|
58
|
+
if (current.nonce === lastAppliedNonce.current)
|
|
59
|
+
return;
|
|
60
|
+
lastAppliedNonce.current = current.nonce;
|
|
61
|
+
const repeat = current.repeat ?? 1;
|
|
62
|
+
setScroll((prev) => applyScrollTimes(prev, current.kind, metricsRef.current, repeat));
|
|
47
63
|
// Keyed on the nonce alone: `intent` is a fresh object on every parent
|
|
48
64
|
// render, so depending on it directly would re-scroll on every keystroke.
|
|
49
65
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -61,7 +77,11 @@ export function ScrollViewport({ intent, pinToBottomNonce = 0, onScrolledLinesCh
|
|
|
61
77
|
// what "N new lines you have not scrolled down to" means.
|
|
62
78
|
const hiddenBelow = offset;
|
|
63
79
|
const showHint = hiddenBelow > 0;
|
|
64
|
-
|
|
80
|
+
// Suppressed below `MIN_INDICATOR_COLS`, where a column of content is worth
|
|
81
|
+
// more than the affordance. The routing never reads this flag, which is what
|
|
82
|
+
// keeps the indicator independently revertible (§13-Q3).
|
|
83
|
+
const indicatorVisible = showScrollIndicator && cols >= MIN_INDICATOR_COLS;
|
|
84
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { ref: clipRef, flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Box, { ref: innerRef, flexDirection: "column", flexShrink: 0, marginTop: -shiftUp, children: children }) }), indicatorVisible && (_jsx(ScrollIndicator, { rows: metrics.viewport, thumb: thumbRange(metrics.viewport, metrics.content, offset), theme: theme, caps: caps }))] }), showHint && (
|
|
65
85
|
// Not an overlay — Ink has no absolute positioning or z-index. This takes
|
|
66
86
|
// the viewport's last row, trading one line of content for a hint that
|
|
67
87
|
// cannot drift. Cheaper than stacking with negative margins.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wheel band hit-testing (mouse-wheel-region-routing §4.5) — pure, React-free.
|
|
3
|
+
*
|
|
4
|
+
* The requirement is stated in terms of REGIONS ("scrolling over the agent area
|
|
5
|
+
* scrolls the transcript; scrolling over the input box steps prompt history"),
|
|
6
|
+
* so the router needs to turn a reported terminal row into one of two bands.
|
|
7
|
+
*
|
|
8
|
+
* The boundary is MEASURED, never computed from `budget.ts`. That arithmetic
|
|
9
|
+
* describes the PLANNED chrome, but the real bottom chrome grows and shrinks at
|
|
10
|
+
* run time: the toast row comes and goes, the hint row is dropped below
|
|
11
|
+
* `HINT_MIN_ROWS`, and the autocomplete popup adds up to nine rows. Routing off
|
|
12
|
+
* the plan would put the boundary in the wrong place exactly when the popup is
|
|
13
|
+
* open — the moment precision matters most (D-5 / D-16).
|
|
14
|
+
*/
|
|
15
|
+
export type WheelBand = 'content' | 'composer';
|
|
16
|
+
export interface FrameGeometry {
|
|
17
|
+
/**
|
|
18
|
+
* 1-based row of the frame's first row. 1 in the alternate screen.
|
|
19
|
+
*
|
|
20
|
+
* This is DERIVED, not assumed, and it has a named dependency: it holds
|
|
21
|
+
* because the full-screen transcript renders no `<Static>`
|
|
22
|
+
* (`Transcript.tsx`), so Ink's `log-update` writes `frameHeight(rows) = rows-1`
|
|
23
|
+
* lines plus a newline into a `rows`-row buffer — nothing scrolls, and the
|
|
24
|
+
* frame sits at rows `1 … rows-1` with the cursor parked on the undrawn last
|
|
25
|
+
* row. Reintroducing `<Static>` into the full-screen path pushes the frame
|
|
26
|
+
* down the screen and silently biases every hit test; whoever does that has
|
|
27
|
+
* to change `top` here at the same time.
|
|
28
|
+
*/
|
|
29
|
+
top: number;
|
|
30
|
+
/** Rows the frame occupies — `frameHeight(rows)`. */
|
|
31
|
+
height: number;
|
|
32
|
+
/** Measured rows of the bottom chrome (toast + composer + status). */
|
|
33
|
+
bottom: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The "I do not know where the frame is" value.
|
|
37
|
+
*
|
|
38
|
+
* `App` writes it into the geometry ref on every render path that does NOT
|
|
39
|
+
* render the full-screen `AppShell` — the terminal-too-small placeholder and
|
|
40
|
+
* inline mode — so a stale measurement can never outlive the frame it
|
|
41
|
+
* described. There is deliberately no arithmetic fallback: an unmeasured frame
|
|
42
|
+
* routes to `'content'`, which is the band that cannot destroy anything, and
|
|
43
|
+
* one invisible frame after a resize is a better failure than a wrong band
|
|
44
|
+
* (§4.5, P1-5 / D-16).
|
|
45
|
+
*/
|
|
46
|
+
export declare const UNMEASURED_GEOMETRY: FrameGeometry;
|
|
47
|
+
/**
|
|
48
|
+
* Which band does terminal row `y` belong to?
|
|
49
|
+
*
|
|
50
|
+
* Fails toward `'content'`: the whole point of this feature is that a wheel
|
|
51
|
+
* notch must never edit the user's draft (I-3), so every degenerate input —
|
|
52
|
+
* nothing measured yet, a row outside the frame, a chrome measurement that
|
|
53
|
+
* swallowed the screen — resolves to the harmless direction.
|
|
54
|
+
*/
|
|
55
|
+
export declare function hitTestWheel(y: number, geometry: FrameGeometry): WheelBand;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wheel band hit-testing (mouse-wheel-region-routing §4.5) — pure, React-free.
|
|
3
|
+
*
|
|
4
|
+
* The requirement is stated in terms of REGIONS ("scrolling over the agent area
|
|
5
|
+
* scrolls the transcript; scrolling over the input box steps prompt history"),
|
|
6
|
+
* so the router needs to turn a reported terminal row into one of two bands.
|
|
7
|
+
*
|
|
8
|
+
* The boundary is MEASURED, never computed from `budget.ts`. That arithmetic
|
|
9
|
+
* describes the PLANNED chrome, but the real bottom chrome grows and shrinks at
|
|
10
|
+
* run time: the toast row comes and goes, the hint row is dropped below
|
|
11
|
+
* `HINT_MIN_ROWS`, and the autocomplete popup adds up to nine rows. Routing off
|
|
12
|
+
* the plan would put the boundary in the wrong place exactly when the popup is
|
|
13
|
+
* open — the moment precision matters most (D-5 / D-16).
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The "I do not know where the frame is" value.
|
|
17
|
+
*
|
|
18
|
+
* `App` writes it into the geometry ref on every render path that does NOT
|
|
19
|
+
* render the full-screen `AppShell` — the terminal-too-small placeholder and
|
|
20
|
+
* inline mode — so a stale measurement can never outlive the frame it
|
|
21
|
+
* described. There is deliberately no arithmetic fallback: an unmeasured frame
|
|
22
|
+
* routes to `'content'`, which is the band that cannot destroy anything, and
|
|
23
|
+
* one invisible frame after a resize is a better failure than a wrong band
|
|
24
|
+
* (§4.5, P1-5 / D-16).
|
|
25
|
+
*/
|
|
26
|
+
export const UNMEASURED_GEOMETRY = { top: 1, height: 0, bottom: 0 };
|
|
27
|
+
/**
|
|
28
|
+
* Which band does terminal row `y` belong to?
|
|
29
|
+
*
|
|
30
|
+
* Fails toward `'content'`: the whole point of this feature is that a wheel
|
|
31
|
+
* notch must never edit the user's draft (I-3), so every degenerate input —
|
|
32
|
+
* nothing measured yet, a row outside the frame, a chrome measurement that
|
|
33
|
+
* swallowed the screen — resolves to the harmless direction.
|
|
34
|
+
*/
|
|
35
|
+
export function hitTestWheel(y, geometry) {
|
|
36
|
+
const { top, height } = geometry;
|
|
37
|
+
if (!Number.isFinite(height) || height <= 0)
|
|
38
|
+
return 'content';
|
|
39
|
+
if (!Number.isFinite(y))
|
|
40
|
+
return 'content';
|
|
41
|
+
// Clamp into the frame so the one undrawn bottom row — and any off-by-one
|
|
42
|
+
// from a terminal that reports 0-based rows — lands on a real band instead of
|
|
43
|
+
// being dropped.
|
|
44
|
+
const row = Math.min(Math.max(Math.floor(y), top), top + height - 1);
|
|
45
|
+
// I-6: at least one content row always survives. This clamp is a backstop
|
|
46
|
+
// that should never fire, and that is checkable rather than hopeful: the
|
|
47
|
+
// completion popup is capped at `viewportRows(rows) - 4`, so the measured
|
|
48
|
+
// chrome tops out at `rows - 6` against a frame of `rows - 1` — five content
|
|
49
|
+
// rows survive the worst case the layout can produce.
|
|
50
|
+
const bottom = Math.min(Math.max(Math.floor(geometry.bottom) || 0, 0), height - 1);
|
|
51
|
+
return row >= top + height - bottom ? 'composer' : 'content';
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll-thumb geometry (mouse-wheel-region-routing §4.8) — pure, React-free.
|
|
3
|
+
*
|
|
4
|
+
* Wheel scrolling without a position indicator is a half-finished affordance:
|
|
5
|
+
* the user learns THAT they moved but not HOW FAR or how much is left. The
|
|
6
|
+
* status bar's `↑N` answers neither.
|
|
7
|
+
*
|
|
8
|
+
* OFFSET COUNTS FROM THE BOTTOM, exactly as `scroll.ts` documents — `0` is
|
|
9
|
+
* pinned to the newest output and `overflow` is parked at the very top. The
|
|
10
|
+
* rows hidden ABOVE are therefore `overflow - offset`, and that inversion is
|
|
11
|
+
* precisely what gets done by accident, so it is computed here once and named.
|
|
12
|
+
*/
|
|
13
|
+
export interface ThumbRange {
|
|
14
|
+
/** 0-based row of the thumb's first row, within the viewport. */
|
|
15
|
+
start: number;
|
|
16
|
+
/** Thumb height in rows; always >= 1 when a range is returned. */
|
|
17
|
+
size: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Where the thumb sits, or `null` when the content fits and there is nothing to
|
|
21
|
+
* indicate.
|
|
22
|
+
*
|
|
23
|
+
* `size` never rounds to zero: a 10-row window over a 5000-row transcript still
|
|
24
|
+
* gets a one-row thumb, because an indicator that vanishes at exactly the
|
|
25
|
+
* moment the transcript is longest is worse than no indicator at all.
|
|
26
|
+
*/
|
|
27
|
+
export declare function thumbRange(viewportRows: number, contentRows: number, offsetFromBottom: number): ThumbRange | null;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll-thumb geometry (mouse-wheel-region-routing §4.8) — pure, React-free.
|
|
3
|
+
*
|
|
4
|
+
* Wheel scrolling without a position indicator is a half-finished affordance:
|
|
5
|
+
* the user learns THAT they moved but not HOW FAR or how much is left. The
|
|
6
|
+
* status bar's `↑N` answers neither.
|
|
7
|
+
*
|
|
8
|
+
* OFFSET COUNTS FROM THE BOTTOM, exactly as `scroll.ts` documents — `0` is
|
|
9
|
+
* pinned to the newest output and `overflow` is parked at the very top. The
|
|
10
|
+
* rows hidden ABOVE are therefore `overflow - offset`, and that inversion is
|
|
11
|
+
* precisely what gets done by accident, so it is computed here once and named.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Where the thumb sits, or `null` when the content fits and there is nothing to
|
|
15
|
+
* indicate.
|
|
16
|
+
*
|
|
17
|
+
* `size` never rounds to zero: a 10-row window over a 5000-row transcript still
|
|
18
|
+
* gets a one-row thumb, because an indicator that vanishes at exactly the
|
|
19
|
+
* moment the transcript is longest is worse than no indicator at all.
|
|
20
|
+
*/
|
|
21
|
+
export function thumbRange(viewportRows, contentRows, offsetFromBottom) {
|
|
22
|
+
const viewport = Math.floor(viewportRows);
|
|
23
|
+
const content = Math.floor(contentRows);
|
|
24
|
+
if (!Number.isFinite(viewport) || !Number.isFinite(content))
|
|
25
|
+
return null;
|
|
26
|
+
if (viewport <= 0 || content <= viewport)
|
|
27
|
+
return null;
|
|
28
|
+
const overflow = content - viewport;
|
|
29
|
+
const offset = Math.min(Math.max(Math.floor(offsetFromBottom) || 0, 0), overflow);
|
|
30
|
+
const size = Math.min(viewport, Math.max(1, Math.round((viewport * viewport) / content)));
|
|
31
|
+
const travel = viewport - size;
|
|
32
|
+
const hiddenAbove = overflow - offset;
|
|
33
|
+
const start = travel <= 0 ? 0 : Math.round((hiddenAbove / overflow) * travel);
|
|
34
|
+
return { start: Math.min(Math.max(start, 0), travel), size };
|
|
35
|
+
}
|
|
@@ -26,3 +26,12 @@ export declare function clampScroll(offset: number, overflowLines: number): numb
|
|
|
26
26
|
export declare function pageSize(viewportHeight: number): number;
|
|
27
27
|
export declare function isPinnedToBottom(state: ScrollState): boolean;
|
|
28
28
|
export declare function applyScroll(state: ScrollState, intent: ScrollIntent, metrics: ViewportMetrics): ScrollState;
|
|
29
|
+
/**
|
|
30
|
+
* Fold `applyScroll` `times` times — one `setState`, one render, however fast
|
|
31
|
+
* the wheel spins (mouse-wheel-region-routing §5.5).
|
|
32
|
+
*
|
|
33
|
+
* Folding rather than multiplying is deliberate: `applyScroll` owns the
|
|
34
|
+
* downward bottom-snap and the clamp, and reimplementing either here would give
|
|
35
|
+
* the wheel a different notion of "pinned to the bottom" than the keyboard has.
|
|
36
|
+
*/
|
|
37
|
+
export declare function applyScrollTimes(state: ScrollState, intent: ScrollIntent, metrics: ViewportMetrics, times: number): ScrollState;
|
package/dist/ui/layout/scroll.js
CHANGED
|
@@ -74,3 +74,26 @@ export function applyScroll(state, intent, metrics) {
|
|
|
74
74
|
}
|
|
75
75
|
return { offset: clamped };
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Upper bound on one folded burst. `repeat` arrives from the wheel router's
|
|
79
|
+
* coalescing window, and an unbounded fold is a free CPU-burn primitive for a
|
|
80
|
+
* high-resolution / free-spinning wheel (R-10).
|
|
81
|
+
*/
|
|
82
|
+
const MAX_SCROLL_REPEAT = 100;
|
|
83
|
+
/**
|
|
84
|
+
* Fold `applyScroll` `times` times — one `setState`, one render, however fast
|
|
85
|
+
* the wheel spins (mouse-wheel-region-routing §5.5).
|
|
86
|
+
*
|
|
87
|
+
* Folding rather than multiplying is deliberate: `applyScroll` owns the
|
|
88
|
+
* downward bottom-snap and the clamp, and reimplementing either here would give
|
|
89
|
+
* the wheel a different notion of "pinned to the bottom" than the keyboard has.
|
|
90
|
+
*/
|
|
91
|
+
export function applyScrollTimes(state, intent, metrics, times) {
|
|
92
|
+
const n = Number.isFinite(times)
|
|
93
|
+
? Math.min(MAX_SCROLL_REPEAT, Math.max(1, Math.floor(times)))
|
|
94
|
+
: 1;
|
|
95
|
+
let next = state;
|
|
96
|
+
for (let i = 0; i < n; i += 1)
|
|
97
|
+
next = applyScroll(next, intent, metrics);
|
|
98
|
+
return next;
|
|
99
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wrapToRows` — the prose wrapper for `OverlayFrame`'s controlled (mode A)
|
|
3
|
+
* content (plan-mode §6.6).
|
|
4
|
+
*
|
|
5
|
+
* Mode A's contract is *one element per row, each already `wrap="truncate"`*,
|
|
6
|
+
* and `sliceWindow` counts ELEMENTS, not lines. Help and Settings satisfy that
|
|
7
|
+
* naturally because their content is a list of short rows. A plan is not: its
|
|
8
|
+
* summary runs to 600 characters and a step detail to 400, so handing either
|
|
9
|
+
* over as a single `<Text>` renders the whole paragraph as one truncated line —
|
|
10
|
+
* the single worst failure available to a plan review card, and one that also
|
|
11
|
+
* makes the frame's `1-14/22` position indicator a false statement.
|
|
12
|
+
*
|
|
13
|
+
* The repo had no wrapper before this (no `wrapText`, no `wordWrap`), so here is
|
|
14
|
+
* one: pure, no Ink, unit-tested.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Break `text` into lines of at most `width` columns.
|
|
18
|
+
*
|
|
19
|
+
* Prefers spaces; hard-breaks any single token longer than `width` (a URL or a
|
|
20
|
+
* long path, which must not be dropped and cannot be wrapped politely).
|
|
21
|
+
* Existing newlines are honoured as paragraph breaks. Returns `[]` for empty
|
|
22
|
+
* input so callers can splice the result without an emptiness branch.
|
|
23
|
+
*/
|
|
24
|
+
export declare function wrapToRows(text: string, width: number): string[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wrapToRows` — the prose wrapper for `OverlayFrame`'s controlled (mode A)
|
|
3
|
+
* content (plan-mode §6.6).
|
|
4
|
+
*
|
|
5
|
+
* Mode A's contract is *one element per row, each already `wrap="truncate"`*,
|
|
6
|
+
* and `sliceWindow` counts ELEMENTS, not lines. Help and Settings satisfy that
|
|
7
|
+
* naturally because their content is a list of short rows. A plan is not: its
|
|
8
|
+
* summary runs to 600 characters and a step detail to 400, so handing either
|
|
9
|
+
* over as a single `<Text>` renders the whole paragraph as one truncated line —
|
|
10
|
+
* the single worst failure available to a plan review card, and one that also
|
|
11
|
+
* makes the frame's `1-14/22` position indicator a false statement.
|
|
12
|
+
*
|
|
13
|
+
* The repo had no wrapper before this (no `wrapText`, no `wordWrap`), so here is
|
|
14
|
+
* one: pure, no Ink, unit-tested.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Break `text` into lines of at most `width` columns.
|
|
18
|
+
*
|
|
19
|
+
* Prefers spaces; hard-breaks any single token longer than `width` (a URL or a
|
|
20
|
+
* long path, which must not be dropped and cannot be wrapped politely).
|
|
21
|
+
* Existing newlines are honoured as paragraph breaks. Returns `[]` for empty
|
|
22
|
+
* input so callers can splice the result without an emptiness branch.
|
|
23
|
+
*/
|
|
24
|
+
export function wrapToRows(text, width) {
|
|
25
|
+
if (typeof text !== 'string' || text.length === 0)
|
|
26
|
+
return [];
|
|
27
|
+
const limit = Math.max(1, Math.floor(width));
|
|
28
|
+
const rows = [];
|
|
29
|
+
for (const paragraph of text.split('\n')) {
|
|
30
|
+
if (paragraph.trim().length === 0) {
|
|
31
|
+
// A blank line in the source is a deliberate separator; keep it as a row
|
|
32
|
+
// rather than collapsing it, or the summary and the steps run together.
|
|
33
|
+
rows.push('');
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
rows.push(...wrapParagraph(paragraph, limit));
|
|
37
|
+
}
|
|
38
|
+
return rows;
|
|
39
|
+
}
|
|
40
|
+
function wrapParagraph(paragraph, limit) {
|
|
41
|
+
const rows = [];
|
|
42
|
+
let current = '';
|
|
43
|
+
for (const word of paragraph.split(/\s+/).filter((w) => w.length > 0)) {
|
|
44
|
+
if (word.length > limit) {
|
|
45
|
+
// Flush what we have, then chop the over-long token into full-width
|
|
46
|
+
// pieces. Its tail becomes the new `current` so the next word can still
|
|
47
|
+
// join it.
|
|
48
|
+
if (current.length > 0) {
|
|
49
|
+
rows.push(current);
|
|
50
|
+
current = '';
|
|
51
|
+
}
|
|
52
|
+
let rest = word;
|
|
53
|
+
while (rest.length > limit) {
|
|
54
|
+
rows.push(rest.slice(0, limit));
|
|
55
|
+
rest = rest.slice(limit);
|
|
56
|
+
}
|
|
57
|
+
current = rest;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (current.length === 0) {
|
|
61
|
+
current = word;
|
|
62
|
+
}
|
|
63
|
+
else if (current.length + 1 + word.length <= limit) {
|
|
64
|
+
current = `${current} ${word}`;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
rows.push(current);
|
|
68
|
+
current = word;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (current.length > 0)
|
|
72
|
+
rows.push(current);
|
|
73
|
+
return rows;
|
|
74
|
+
}
|
|
@@ -7,11 +7,14 @@ function keyRows(times) {
|
|
|
7
7
|
['Enter', 'Submit (idle) / queue steering (running); re-pins to the newest output'],
|
|
8
8
|
['Alt+Enter / Shift+Enter', 'Insert newline'],
|
|
9
9
|
['Esc', 'Abort run / close overlay'],
|
|
10
|
+
['Shift+Tab', 'Toggle plan mode (same as /plan)'],
|
|
10
11
|
[`Ctrl+C ${times}2`, 'Exit'],
|
|
11
12
|
['Ctrl+L', 'Redraw the frame'],
|
|
12
13
|
['Ctrl+T', 'Toggle thinking blocks'],
|
|
13
14
|
['PgUp / PgDn', 'Scroll a page (transcript, or this overlay)'],
|
|
14
15
|
['Shift+Up / Shift+Down', 'Scroll the transcript a line (full-screen mode)'],
|
|
16
|
+
['Wheel', 'Scroll the transcript (over the input box: prompt history)'],
|
|
17
|
+
['Shift+Wheel', 'Scroll the transcript a page'],
|
|
15
18
|
['Up / Down', 'Prompt history (empty input)'],
|
|
16
19
|
];
|
|
17
20
|
}
|
|
@@ -19,6 +22,7 @@ const COMMANDS = [
|
|
|
19
22
|
['/help', 'Show this help'],
|
|
20
23
|
['/model', 'Open the model picker'],
|
|
21
24
|
['/settings', 'Open the settings screen'],
|
|
25
|
+
['/plan [on|off|status]', 'Toggle plan mode (same as Shift+Tab)'],
|
|
22
26
|
['/theme <name>', 'auto | warm | cool | light'],
|
|
23
27
|
['/thinking <level>', 'Set thinking level'],
|
|
24
28
|
['/tools', 'List active tools'],
|
|
@@ -41,6 +45,14 @@ const SKILL_ROWS = [
|
|
|
41
45
|
['/<skill-name> [args]', 'Run a skill directly ($ARGUMENTS / $1..$9)'],
|
|
42
46
|
['skill / skill_install', 'Tools the model uses to load and install skills'],
|
|
43
47
|
];
|
|
48
|
+
const PLAN_ROWS = [
|
|
49
|
+
['Shift+Tab / /plan', 'Switch between BUILD (do it now) and PLAN (research first)'],
|
|
50
|
+
['In PLAN mode', 'write_file, edit_file, bash, skill_install and skill_create are refused'],
|
|
51
|
+
['bash', 'Refused in full, including git status - use read_file / glob / grep'],
|
|
52
|
+
['ask_user', 'The agent asks 1-5 multiple-choice questions; Enter takes the recommendation'],
|
|
53
|
+
['submit_plan', 'The agent submits a plan: a approve, r revise, Esc dismiss'],
|
|
54
|
+
['--plan / --no-plan', 'Start a session in a given mode (also ARAGON_PLAN=1)'],
|
|
55
|
+
];
|
|
44
56
|
/** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
|
|
45
57
|
export function helpRows(theme, caps) {
|
|
46
58
|
const glyphs = pickGlyphs(caps);
|
|
@@ -53,6 +65,9 @@ export function helpRows(theme, caps) {
|
|
|
53
65
|
rows.push(section('Slash commands'));
|
|
54
66
|
for (const [c, d] of COMMANDS)
|
|
55
67
|
rows.push(pair(c, d));
|
|
68
|
+
rows.push(section('Plan mode'));
|
|
69
|
+
for (const [c, d] of PLAN_ROWS)
|
|
70
|
+
rows.push(pair(c, d));
|
|
56
71
|
rows.push(section('Skills'));
|
|
57
72
|
for (const [c, d] of SKILL_ROWS)
|
|
58
73
|
rows.push(pair(c, d));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlanReviewOverlay — the reviewable card `submit_plan` renders (plan-mode §6.6).
|
|
3
|
+
*
|
|
4
|
+
* `OverlayFrame` MODE A (`rows`), because a 20-step plan must scroll and mode A
|
|
5
|
+
* is the only mode with a position indicator.
|
|
6
|
+
*
|
|
7
|
+
* MODE A SLICES BY ELEMENT, SO THE PROSE IS PRE-WRAPPED. `sliceWindow` counts
|
|
8
|
+
* elements, not lines, and each element must be exactly one row. The summary
|
|
9
|
+
* runs to 600 characters and a step detail to 400 — handing either over as a
|
|
10
|
+
* single `<Text>` would render the whole paragraph as one truncated line and
|
|
11
|
+
* make the frame's `1-14/22` a false statement. `wrapToRows` is what keeps the
|
|
12
|
+
* count honest.
|
|
13
|
+
*
|
|
14
|
+
* ACTIONS LIVE IN THE FRAME HINT, NOT IN `rows`. Mode A slices `rows`; an action
|
|
15
|
+
* row placed there would scroll off exactly when a long plan needs it most.
|
|
16
|
+
* Single-key actions also avoid a second selection cursor competing with the
|
|
17
|
+
* scroll cursor.
|
|
18
|
+
*
|
|
19
|
+
* `Esc` IS NOT HANDLED HERE, and neither is any resolution of the pending
|
|
20
|
+
* request: `App` owns Esc, and the bridge's `settle()` is the only place a human
|
|
21
|
+
* request is answered (R-P12). That holds in the revise sub-state too, which is
|
|
22
|
+
* why the way back from the feedback field is `<-` rather than `Esc` (R2-P1-2).
|
|
23
|
+
*/
|
|
24
|
+
import React from 'react';
|
|
25
|
+
import type { Theme } from '../theme.js';
|
|
26
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
27
|
+
import type { NormalizedPlan } from '../../tools/human-input.js';
|
|
28
|
+
export type PlanVerdict = {
|
|
29
|
+
decision: 'approved' | 'revise';
|
|
30
|
+
feedback: string;
|
|
31
|
+
};
|
|
32
|
+
interface PlanReviewOverlayProps {
|
|
33
|
+
plan: NormalizedPlan;
|
|
34
|
+
maxRows: number;
|
|
35
|
+
cols: number;
|
|
36
|
+
scrollOffset: number;
|
|
37
|
+
onScrollClamp: (offset: number) => void;
|
|
38
|
+
theme: Theme;
|
|
39
|
+
caps: TermCapabilities;
|
|
40
|
+
onVerdict: (verdict: PlanVerdict) => void;
|
|
41
|
+
}
|
|
42
|
+
export declare function PlanReviewOverlay({ plan, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onVerdict, }: PlanReviewOverlayProps): React.ReactElement;
|
|
43
|
+
/**
|
|
44
|
+
* Flatten the plan into one-row elements.
|
|
45
|
+
*
|
|
46
|
+
* Exported for `plan-mode.test.ts`, which asserts that a 600-character summary
|
|
47
|
+
* really does become many rows rather than one truncated line (P1-3).
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildPlanRows(plan: NormalizedPlan, width: number, theme: Theme, bullet: string): React.ReactElement[];
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* PlanReviewOverlay — the reviewable card `submit_plan` renders (plan-mode §6.6).
|
|
4
|
+
*
|
|
5
|
+
* `OverlayFrame` MODE A (`rows`), because a 20-step plan must scroll and mode A
|
|
6
|
+
* is the only mode with a position indicator.
|
|
7
|
+
*
|
|
8
|
+
* MODE A SLICES BY ELEMENT, SO THE PROSE IS PRE-WRAPPED. `sliceWindow` counts
|
|
9
|
+
* elements, not lines, and each element must be exactly one row. The summary
|
|
10
|
+
* runs to 600 characters and a step detail to 400 — handing either over as a
|
|
11
|
+
* single `<Text>` would render the whole paragraph as one truncated line and
|
|
12
|
+
* make the frame's `1-14/22` a false statement. `wrapToRows` is what keeps the
|
|
13
|
+
* count honest.
|
|
14
|
+
*
|
|
15
|
+
* ACTIONS LIVE IN THE FRAME HINT, NOT IN `rows`. Mode A slices `rows`; an action
|
|
16
|
+
* row placed there would scroll off exactly when a long plan needs it most.
|
|
17
|
+
* Single-key actions also avoid a second selection cursor competing with the
|
|
18
|
+
* scroll cursor.
|
|
19
|
+
*
|
|
20
|
+
* `Esc` IS NOT HANDLED HERE, and neither is any resolution of the pending
|
|
21
|
+
* request: `App` owns Esc, and the bridge's `settle()` is the only place a human
|
|
22
|
+
* request is answered (R-P12). That holds in the revise sub-state too, which is
|
|
23
|
+
* why the way back from the feedback field is `<-` rather than `Esc` (R2-P1-2).
|
|
24
|
+
*/
|
|
25
|
+
import { useState } from 'react';
|
|
26
|
+
import { Text, useInput } from 'ink';
|
|
27
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
28
|
+
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
29
|
+
import { wrapToRows } from '../layout/wrap-rows.js';
|
|
30
|
+
/** The frame's border plus its one-column padding on each side. */
|
|
31
|
+
const FRAME_CHROME_COLS = 4;
|
|
32
|
+
export function PlanReviewOverlay({ plan, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onVerdict, }) {
|
|
33
|
+
const glyphs = pickGlyphs(caps);
|
|
34
|
+
const [feedback, setFeedback] = useState(null);
|
|
35
|
+
useInput((input, key) => {
|
|
36
|
+
if (key.escape)
|
|
37
|
+
return; // App owns dismiss.
|
|
38
|
+
if (feedback !== null) {
|
|
39
|
+
if (key.return) {
|
|
40
|
+
onVerdict({ decision: 'revise', feedback: feedback.trim() });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// THE WAY BACK TO THE CARD, and the reason it is `<-` and not `Esc`:
|
|
44
|
+
// `App` owns Esc here and settles the pending request with it, so a second
|
|
45
|
+
// Esc consumer in this overlay would be the `ConfirmDialog` double-resolve
|
|
46
|
+
// pattern R-P12 exists to keep out. `QuestionOverlay` already spends `<-`
|
|
47
|
+
// on "back", so this costs no key ownership. Without it the field is a
|
|
48
|
+
// one-way door — it consumes printable input and Backspace, and the only
|
|
49
|
+
// other exit throws the whole review away (R2-P1-2).
|
|
50
|
+
if (key.leftArrow) {
|
|
51
|
+
setFeedback(null);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (key.backspace || key.delete) {
|
|
55
|
+
setFeedback(feedback.slice(0, -1));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (input && !key.ctrl && !key.meta && !key.tab)
|
|
59
|
+
setFeedback(feedback + input);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (input === 'a' || input === 'A')
|
|
63
|
+
onVerdict({ decision: 'approved', feedback: '' });
|
|
64
|
+
else if (input === 'r' || input === 'R')
|
|
65
|
+
setFeedback('');
|
|
66
|
+
});
|
|
67
|
+
const width = Math.max(20, cols - FRAME_CHROME_COLS);
|
|
68
|
+
const rows = buildPlanRows(plan, width, theme, glyphs.bullet);
|
|
69
|
+
if (feedback !== null) {
|
|
70
|
+
// `esc dismiss`, NOT "esc back to the plan": `App` owns Esc for this overlay
|
|
71
|
+
// (R-P12) and settles the pending request with `null`, so Esc throws the
|
|
72
|
+
// whole review away from either sub-state. `<-` is the route back to the
|
|
73
|
+
// card, which is why it is named here too.
|
|
74
|
+
const feedbackHint = `${glyphs.enterKey} send ${glyphs.midDot} ${glyphs.arrowLeft} back ${glyphs.midDot} esc dismiss`;
|
|
75
|
+
return (_jsxs(OverlayFrame, { title: "What should change?", hint: feedbackHint, maxRows: maxRows, cols: cols, theme: theme, caps: caps, children: [_jsx(Text, { wrap: "truncate", color: theme.muted, children: "The agent revises the plan and submits it again." }), _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: theme.primary, bold: true, children: [glyphs.caret, ' '] }), feedback.length > 0 ? (_jsx(Text, { children: feedback })) : (_jsx(Text, { color: theme.muted, children: "e.g. use the existing session store instead" })), _jsx(Text, { inverse: true, children: " " })] })] }));
|
|
76
|
+
}
|
|
77
|
+
return (_jsx(OverlayFrame, { title: "Review plan", hint: `a approve ${glyphs.midDot} r revise ${glyphs.midDot} esc dismiss ${glyphs.midDot} ${glyphs.arrowUp}${glyphs.arrowDown} scroll`, maxRows: maxRows, cols: cols, rows: rows, scrollOffset: scrollOffset, onScrollClamp: onScrollClamp, theme: theme, caps: caps }));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Flatten the plan into one-row elements.
|
|
81
|
+
*
|
|
82
|
+
* Exported for `plan-mode.test.ts`, which asserts that a 600-character summary
|
|
83
|
+
* really does become many rows rather than one truncated line (P1-3).
|
|
84
|
+
*/
|
|
85
|
+
export function buildPlanRows(plan, width, theme, bullet) {
|
|
86
|
+
const rows = [];
|
|
87
|
+
let key = 0;
|
|
88
|
+
const push = (node, color, bold) => {
|
|
89
|
+
rows.push(_jsx(Text, { wrap: "truncate", color: color, bold: bold, children: node }, `p${key++}`));
|
|
90
|
+
};
|
|
91
|
+
push(plan.title, theme.assistant, true);
|
|
92
|
+
for (const line of wrapToRows(plan.summary, width))
|
|
93
|
+
push(line || ' ', theme.muted);
|
|
94
|
+
push(' ');
|
|
95
|
+
push('Steps', theme.accent, true);
|
|
96
|
+
plan.steps.forEach((step, i) => {
|
|
97
|
+
const number = String(i + 1).padStart(2, ' ');
|
|
98
|
+
push(`${number} ${step.title}`);
|
|
99
|
+
for (const line of wrapToRows(step.detail, Math.max(10, width - 4))) {
|
|
100
|
+
push(` ${line}`, theme.muted);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
const section = (label, items) => {
|
|
104
|
+
if (items.length === 0)
|
|
105
|
+
return;
|
|
106
|
+
push(' ');
|
|
107
|
+
push(label, theme.accent, true);
|
|
108
|
+
for (const item of items) {
|
|
109
|
+
for (const line of wrapToRows(`${bullet} ${item}`, width))
|
|
110
|
+
push(line, theme.muted);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
section('Files', plan.filesTouched);
|
|
114
|
+
section('Risks', plan.risks);
|
|
115
|
+
section('Open questions', plan.openQuestions);
|
|
116
|
+
return rows;
|
|
117
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuestionOverlay — the keyboard-driven multiple-choice wizard `ask_user`
|
|
3
|
+
* renders (plan-mode §6.5).
|
|
4
|
+
*
|
|
5
|
+
* `OverlayFrame` MODE B (self-managed): the content is a small state machine,
|
|
6
|
+
* not a list of rows, and slicing it by element would be meaningless.
|
|
7
|
+
*
|
|
8
|
+
* THE SINGLE HIGHEST-VALUE DECISION HERE is that the recommended option is the
|
|
9
|
+
* initial highlight for every question, so `Enter Enter Enter` is a correct,
|
|
10
|
+
* deliberate "use your judgement" path rather than an accident.
|
|
11
|
+
*
|
|
12
|
+
* `Esc` IS NOT HANDLED HERE. `App` owns it, and the bridge's `settle()` is the
|
|
13
|
+
* only place a human request is ever answered. `ConfirmDialog` does the
|
|
14
|
+
* opposite — both it and `App` call `resolve` — which is harmless there only by
|
|
15
|
+
* accident of promise semantics, and is not a pattern to copy (R-P12).
|
|
16
|
+
*/
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import type { Theme } from '../theme.js';
|
|
19
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
20
|
+
import type { Answer, NormalizedQuestion } from '../../tools/human-input.js';
|
|
21
|
+
interface QuestionOverlayProps {
|
|
22
|
+
questions: NormalizedQuestion[];
|
|
23
|
+
maxRows: number;
|
|
24
|
+
cols: number;
|
|
25
|
+
theme: Theme;
|
|
26
|
+
caps: TermCapabilities;
|
|
27
|
+
onSubmit: (answers: Answer[]) => void;
|
|
28
|
+
}
|
|
29
|
+
export declare function QuestionOverlay({ questions, maxRows, cols, theme, caps, onSubmit, }: QuestionOverlayProps): React.ReactElement;
|
|
30
|
+
export {};
|