@aragon-agent/cli 0.6.37 → 0.6.39
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 +2523 -2326
- package/README.md +3614 -3173
- package/dist/agent/controller.d.ts +142 -1
- package/dist/agent/controller.js +171 -3
- package/dist/agent/reducer.d.ts +41 -1
- package/dist/agent/reducer.js +25 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +5 -0
- package/dist/cli.js +85 -27
- package/dist/commands/builtins.js +13 -1
- package/dist/commands/keys-command.d.ts +23 -0
- package/dist/commands/keys-command.js +38 -0
- package/dist/commands/perf.d.ts +7 -1
- package/dist/commands/perf.js +2 -2
- package/dist/commands/registry.d.ts +39 -0
- package/dist/commands/task-command.d.ts +39 -0
- package/dist/commands/task-command.js +102 -0
- package/dist/config/cli-commands.d.ts +23 -0
- package/dist/config/cli-commands.js +36 -0
- package/dist/config/env.js +17 -4
- package/dist/config/load.js +6 -1
- package/dist/config/schema.d.ts +43 -4
- package/dist/config/schema.js +31 -5
- package/dist/config/store.js +17 -2
- package/dist/diagnostics/doctor.d.ts +9 -0
- package/dist/diagnostics/doctor.js +34 -0
- package/dist/diagnostics/info.d.ts +71 -0
- package/dist/diagnostics/info.js +91 -5
- package/dist/exec/bridge/caller-tools.d.ts +96 -0
- package/dist/exec/bridge/caller-tools.js +213 -0
- package/dist/exec/bridge/decisions.d.ts +78 -0
- package/dist/exec/bridge/decisions.js +116 -0
- package/dist/exec/bridge/human-answers.d.ts +38 -0
- package/dist/exec/bridge/human-answers.js +164 -0
- package/dist/exec/bridge/limits.d.ts +68 -0
- package/dist/exec/bridge/limits.js +68 -0
- package/dist/exec/bridge/ndjson.d.ts +17 -0
- package/dist/exec/bridge/ndjson.js +83 -0
- package/dist/exec/bridge/pending.d.ts +70 -0
- package/dist/exec/bridge/pending.js +132 -0
- package/dist/exec/bridge/protocol.d.ts +346 -0
- package/dist/exec/bridge/protocol.js +256 -0
- package/dist/exec/bridge/server.d.ts +142 -0
- package/dist/exec/bridge/server.js +502 -0
- package/dist/exec/bridge/source.d.ts +58 -0
- package/dist/exec/bridge/source.js +145 -0
- package/dist/exec/bridge/writer.d.ts +26 -0
- package/dist/exec/bridge/writer.js +156 -0
- package/dist/exec/cli-commands.js +30 -2
- package/dist/exec/emitter.d.ts +16 -2
- package/dist/exec/emitter.js +26 -6
- package/dist/exec/events.d.ts +465 -10
- package/dist/exec/events.js +47 -1
- package/dist/exec/hooks/dispatch.d.ts +76 -0
- package/dist/exec/hooks/dispatch.js +169 -0
- package/dist/exec/hooks/index.d.ts +83 -0
- package/dist/exec/hooks/index.js +205 -0
- package/dist/exec/hooks/limits.d.ts +56 -0
- package/dist/exec/hooks/limits.js +49 -0
- package/dist/exec/hooks/registry.d.ts +49 -0
- package/dist/exec/hooks/registry.js +91 -0
- package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
- package/dist/exec/hooks/tool-wrapper.js +154 -0
- package/dist/exec/hooks/turn.d.ts +61 -0
- package/dist/exec/hooks/turn.js +59 -0
- package/dist/exec/hooks/validate.d.ts +85 -0
- package/dist/exec/hooks/validate.js +310 -0
- package/dist/exec/index.d.ts +130 -4
- package/dist/exec/index.js +501 -10
- package/dist/exec/options.d.ts +124 -2
- package/dist/exec/options.js +413 -6
- package/dist/exec/permission.d.ts +9 -1
- package/dist/exec/permission.js +7 -1
- package/dist/exec/request/limits.d.ts +51 -0
- package/dist/exec/request/limits.js +49 -0
- package/dist/exec/request/scope.d.ts +106 -0
- package/dist/exec/request/scope.js +149 -0
- package/dist/exec/result/limits.d.ts +74 -0
- package/dist/exec/result/limits.js +72 -0
- package/dist/exec/result/prompt.d.ts +28 -0
- package/dist/exec/result/prompt.js +74 -0
- package/dist/exec/result/schema.d.ts +99 -0
- package/dist/exec/result/schema.js +464 -0
- package/dist/exec/result/sink.d.ts +56 -0
- package/dist/exec/result/sink.js +50 -0
- package/dist/exec/result/tool.d.ts +37 -0
- package/dist/exec/result/tool.js +118 -0
- package/dist/exec/runner.d.ts +395 -1
- package/dist/exec/runner.js +860 -8
- package/dist/exec/threads/aggregate.d.ts +34 -0
- package/dist/exec/threads/aggregate.js +30 -0
- package/dist/exec/threads/capacity.d.ts +23 -0
- package/dist/exec/threads/capacity.js +38 -0
- package/dist/exec/threads/dispatch-source.d.ts +60 -0
- package/dist/exec/threads/dispatch-source.js +106 -0
- package/dist/exec/threads/execute-threads.d.ts +53 -0
- package/dist/exec/threads/execute-threads.js +234 -0
- package/dist/exec/threads/gate.d.ts +42 -0
- package/dist/exec/threads/gate.js +81 -0
- package/dist/exec/threads/identity.d.ts +36 -0
- package/dist/exec/threads/identity.js +67 -0
- package/dist/exec/threads/interrupt.d.ts +29 -0
- package/dist/exec/threads/interrupt.js +79 -0
- package/dist/exec/threads/limits.d.ts +58 -0
- package/dist/exec/threads/limits.js +35 -0
- package/dist/exec/threads/outcome.d.ts +47 -0
- package/dist/exec/threads/outcome.js +26 -0
- package/dist/exec/threads/registry.d.ts +81 -0
- package/dist/exec/threads/registry.js +427 -0
- package/dist/exec/threads/runtime.d.ts +107 -0
- package/dist/exec/threads/runtime.js +523 -0
- package/dist/exec/threads/thread-source.d.ts +20 -0
- package/dist/exec/threads/thread-source.js +134 -0
- package/dist/fast/limits.d.ts +54 -0
- package/dist/fast/limits.js +54 -0
- package/dist/fast/summary-call.d.ts +72 -0
- package/dist/fast/summary-call.js +133 -0
- package/dist/fast/summary-prompt.d.ts +106 -0
- package/dist/fast/summary-prompt.js +320 -0
- package/dist/fast/task-summary-types.d.ts +66 -0
- package/dist/fast/task-summary-types.js +17 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/limits.d.ts +7 -0
- package/dist/proc/limits.js +7 -0
- package/dist/proc/output-streams.d.ts +50 -0
- package/dist/proc/output-streams.js +54 -0
- package/dist/team/runtime.d.ts +9 -0
- package/dist/team/runtime.js +8 -0
- package/dist/team/subagent.d.ts +15 -0
- package/dist/team/subagent.js +45 -1
- package/dist/tools/bash-tool.d.ts +10 -0
- package/dist/tools/bash-tool.js +71 -6
- package/dist/tools/index.d.ts +93 -1
- package/dist/tools/index.js +112 -3
- package/dist/tools/tool-gate.d.ts +100 -0
- package/dist/tools/tool-gate.js +87 -0
- package/dist/ui/App.js +224 -11
- package/dist/ui/Header.d.ts +28 -16
- package/dist/ui/Header.js +21 -19
- package/dist/ui/interaction-hints.js +6 -0
- package/dist/ui/layout/OverlayFrame.d.ts +18 -0
- package/dist/ui/layout/OverlayFrame.js +19 -4
- package/dist/ui/layout/header-hints.d.ts +55 -62
- package/dist/ui/layout/header-hints.js +59 -166
- package/dist/ui/layout/header-left.d.ts +115 -0
- package/dist/ui/layout/header-left.js +206 -0
- package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
- package/dist/ui/layout/shortcut-panel-layout.js +321 -0
- package/dist/ui/layout/status-detail-layout.js +5 -1
- package/dist/ui/layout/task-summary-cell.d.ts +61 -0
- package/dist/ui/layout/task-summary-cell.js +65 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
- package/dist/ui/overlays/HelpOverlay.js +29 -2
- package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
- package/dist/ui/overlays/ShortcutPanel.js +106 -0
- package/dist/ui/scrollback/canvas.d.ts +3 -2
- package/dist/ui/scrollback/canvas.js +3 -2
- package/dist/ui/scrollback/scrollback-writer.d.ts +13 -1
- package/dist/ui/scrollback/scrollback-writer.js +109 -10
- package/dist/ui/shortcuts.d.ts +81 -0
- package/dist/ui/shortcuts.js +221 -0
- package/package.json +2 -2
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The keyboard drawer (tui-shortcut-drawer-and-task-summary §4.5).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* IT OWNS NO CHROME AND NO ARITHMETIC. `OverlayFrame` draws the frame, the
|
|
8
|
+
* window and the position indicator; `shortcut-panel-layout.ts` decides the
|
|
9
|
+
* columns, the rows and which window contains the cursor. This component adds two
|
|
10
|
+
* things and nothing else: its own key handling and the text-run colours. v1
|
|
11
|
+
* planned 260 lines that re-implemented the frame's windowing and clamping - a
|
|
12
|
+
* second source of truth for the one thing overlays have already been rewritten
|
|
13
|
+
* once to unify.
|
|
14
|
+
*
|
|
15
|
+
* IT DOES NOT REGISTER `Esc` AND DOES NOT TOUCH ANY `ctrl`/`meta` COMBINATION:
|
|
16
|
+
* `Esc` belongs to `App` (it closes every overlay and cancels the pending
|
|
17
|
+
* question/plan), and `Ctrl+G` is handled there too. Two listeners both acting on
|
|
18
|
+
* one byte is how a panel closes twice.
|
|
19
|
+
*/
|
|
20
|
+
import { useMemo, useState } from 'react';
|
|
21
|
+
import { Text, useInput } from 'ink';
|
|
22
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
23
|
+
import { OverlayFrame, overlayBodyRows, overlayPageSize } from '../layout/OverlayFrame.js';
|
|
24
|
+
import { buildShortcutRows, shortcutPanelWidth, } from '../layout/shortcut-panel-layout.js';
|
|
25
|
+
import { filterShortcuts, isAvailable, SHORTCUTS } from '../shortcuts.js';
|
|
26
|
+
/** Colour per tone. `dim` is `muted` plus the dim attribute, never a second colour. */
|
|
27
|
+
function toneOf(row, theme) {
|
|
28
|
+
if (row.tone === 'accent')
|
|
29
|
+
return { color: theme.accent };
|
|
30
|
+
if (row.tone === 'primary')
|
|
31
|
+
return { color: theme.primary };
|
|
32
|
+
if (row.tone === 'dim')
|
|
33
|
+
return { color: theme.muted, dimColor: true };
|
|
34
|
+
return { color: theme.muted };
|
|
35
|
+
}
|
|
36
|
+
export function ShortcutPanel(props) {
|
|
37
|
+
const { cols, maxRows, theme, caps, isActive, native, running } = props;
|
|
38
|
+
const glyphs = pickGlyphs(caps);
|
|
39
|
+
const [cursor, setCursor] = useState(0);
|
|
40
|
+
const entries = useMemo(() => filterShortcuts(SHORTCUTS, props.query ?? ''), [props.query]);
|
|
41
|
+
const bodyRows = overlayBodyRows(maxRows, cols);
|
|
42
|
+
const page = overlayPageSize(maxRows, cols);
|
|
43
|
+
const result = useMemo(() => buildShortcutRows({
|
|
44
|
+
cols,
|
|
45
|
+
rows: bodyRows,
|
|
46
|
+
cursor,
|
|
47
|
+
entries,
|
|
48
|
+
task: props.task,
|
|
49
|
+
entryAvailable: (entry) => isAvailable(entry, { native, running }),
|
|
50
|
+
unavailableReason: native ? 'this mode leaves it to the terminal' : 'a run is in flight',
|
|
51
|
+
glyphs: {
|
|
52
|
+
ellipsis: glyphs.ellipsis,
|
|
53
|
+
cursor: glyphs.cursor,
|
|
54
|
+
hrule: glyphs.hrule,
|
|
55
|
+
midDot: glyphs.midDot,
|
|
56
|
+
},
|
|
57
|
+
}), [cols, bodyRows, cursor, entries, props.task, native, running,
|
|
58
|
+
glyphs.ellipsis, glyphs.cursor, glyphs.hrule, glyphs.midDot]);
|
|
59
|
+
const last = Math.max(0, entries.length - 1);
|
|
60
|
+
useInput((input, key) => {
|
|
61
|
+
// `ctrl`/`meta` ARE NOT OURS: `Ctrl+G` closes this panel and `Ctrl+C` owns the
|
|
62
|
+
// exit ladder, both in `App`. Consuming them here would double-handle a byte.
|
|
63
|
+
if (key.ctrl || key.meta)
|
|
64
|
+
return;
|
|
65
|
+
if (key.upArrow || input === 'k') {
|
|
66
|
+
setCursor((value) => Math.max(0, value - 1));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (key.downArrow || input === 'j') {
|
|
70
|
+
setCursor((value) => Math.min(last, value + 1));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// PAGING MOVES THE CURSOR, one page minus one row of overlap - the same rule
|
|
74
|
+
// `overlayPageSize` uses for every other overlay, so a page turn keeps a row of
|
|
75
|
+
// context. An independent offset would let the selected row - and the detail
|
|
76
|
+
// line that explains it - slide off screen (P1-7).
|
|
77
|
+
if (key.pageUp) {
|
|
78
|
+
setCursor((value) => Math.max(0, value - Math.max(1, page - 1)));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (key.pageDown) {
|
|
82
|
+
setCursor((value) => Math.min(last, value + Math.max(1, page - 1)));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (input === 'd') {
|
|
86
|
+
props.onToggleDetails();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (input === 'r') {
|
|
90
|
+
props.onRefresh();
|
|
91
|
+
}
|
|
92
|
+
}, { isActive });
|
|
93
|
+
const rows = result.rows.map((row, index) => (_jsx(Text, { wrap: "truncate", ...toneOf(row, theme), children: row.text }, `${row.kind}-${row.entryId ?? 'x'}-${index}`)));
|
|
94
|
+
// THE `d` MARKER LIVES ON THE FRAME'S OWN TITLE LINE (D-6), which is chrome that is
|
|
95
|
+
// already paid for. It is spelled `d:on` / `d:off` rather than `details on|off`
|
|
96
|
+
// because the title row also carries the frame's position indicator (`v 1-13/37`,
|
|
97
|
+
// 13 cells) and the drawer's content is only 26 cells wide at 80 columns: the long
|
|
98
|
+
// form is truncated to `details...`, which is a marker that says nothing. The
|
|
99
|
+
// footer's `d details` clause names the key, so the pair reads as one sentence.
|
|
100
|
+
const title = `Keys d:${props.detailsExpanded ? 'on' : 'off'}`;
|
|
101
|
+
return (_jsx(OverlayFrame, { title: title, hint: result.hint, maxRows: maxRows, cols: cols, width: shortcutPanelWidth(cols), align: "right", rows: rows,
|
|
102
|
+
// THE WINDOW IS DERIVED FROM THE CURSOR, so the offset handed to the frame is
|
|
103
|
+
// a conclusion rather than state. `onScrollClamp` is deliberately not passed:
|
|
104
|
+
// there is nothing here that could drift out of step with it.
|
|
105
|
+
scrollOffset: result.window.start, theme: theme, caps: caps }));
|
|
106
|
+
}
|
|
@@ -78,8 +78,9 @@ export declare function screenCapacity(rows: number | undefined): number;
|
|
|
78
78
|
export declare function clampSpaceBelow(spaceBelow: number, rows: number | undefined): number;
|
|
79
79
|
/**
|
|
80
80
|
* A terminal that grew adds rows BELOW the cursor, so the capacity grows by the
|
|
81
|
-
* exact difference (D6). Shrinking is NOT handled here:
|
|
82
|
-
*
|
|
81
|
+
* exact difference (D6). Shrinking is NOT handled here either: the writer keeps the
|
|
82
|
+
* rows the screen still has and re-pins them (I-B17) instead of guessing a smaller
|
|
83
|
+
* value, which would only hide a lost measurement (D5).
|
|
83
84
|
*/
|
|
84
85
|
export declare function applyResizeGrowth(capacity: number, rowsPrev: number, rowsNow: number): number;
|
|
85
86
|
/**
|
|
@@ -48,8 +48,9 @@ export function clampSpaceBelow(spaceBelow, rows) {
|
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* A terminal that grew adds rows BELOW the cursor, so the capacity grows by the
|
|
51
|
-
* exact difference (D6). Shrinking is NOT handled here:
|
|
52
|
-
*
|
|
51
|
+
* exact difference (D6). Shrinking is NOT handled here either: the writer keeps the
|
|
52
|
+
* rows the screen still has and re-pins them (I-B17) instead of guessing a smaller
|
|
53
|
+
* value, which would only hide a lost measurement (D5).
|
|
53
54
|
*/
|
|
54
55
|
export function applyResizeGrowth(capacity, rowsPrev, rowsNow) {
|
|
55
56
|
// No capacity to grow: a broken number must not become a canvas by arithmetic.
|
|
@@ -54,6 +54,16 @@
|
|
|
54
54
|
* - I-B15 · every batch starts by reconciling `top + capacity` with the live
|
|
55
55
|
* `rows - 1` (a resize event can arrive one frame late), and a column change
|
|
56
56
|
* resets before it writes.
|
|
57
|
+
* - I-B17 · a SHRINK re-pins the canvas, it never drops it. The canvas keeps
|
|
58
|
+
* `min(canvasRows, screenCapacity(rows))` rows — the screen keeps the rows nearest
|
|
59
|
+
* the parked cursor, so the survivors are its SUFFIX — and its top moves down to
|
|
60
|
+
* `capacity - kept`, which preserves LB-3. Dropping the cache instead made the next
|
|
61
|
+
* batch a SEED, and a seed assumes nothing is below the cursor
|
|
62
|
+
* (`writer-shapes.ts:60-64`); after a shrink that is false, so the batch overflowed
|
|
63
|
+
* the screen and the previous frame — status row included — went into the
|
|
64
|
+
* scrollback: the same row in two places. For the same reason a frame TALLER than
|
|
65
|
+
* the screen gives way at its TOP instead of being swallowed whole: the composer
|
|
66
|
+
* and the status line are the two rows a human is interacting with.
|
|
57
67
|
* - I-A9 · the writer never writes a probe byte; it only asks the host, which owns
|
|
58
68
|
* deferral, coalescing and the death switch.
|
|
59
69
|
*/
|
|
@@ -102,6 +112,8 @@ export interface ScrollbackWriterStats {
|
|
|
102
112
|
guardHits: number;
|
|
103
113
|
/** The subset NOT explained by a recent resize: the real defect signal. */
|
|
104
114
|
guardHitsOutsideResize: number;
|
|
115
|
+
/** Frames taller than the screen, drawn after giving way at their TOP (I-B17). */
|
|
116
|
+
tallFramesTrimmed: number;
|
|
105
117
|
clippedRows: number;
|
|
106
118
|
elidedEntries: number;
|
|
107
119
|
resizes: number;
|
|
@@ -116,7 +128,7 @@ export interface ScrollbackWriterStats {
|
|
|
116
128
|
anchorGapRows: number;
|
|
117
129
|
/** Rows this session pushed into the scrollback (the "is it scrolling" signal). */
|
|
118
130
|
anchorScrollRows: number;
|
|
119
|
-
/** Times the geometry was dropped on purpose (column change,
|
|
131
|
+
/** Times the geometry was dropped on purpose (column change, nothing left to pin). */
|
|
120
132
|
anchorResets: number;
|
|
121
133
|
/** Measurements accepted AFTER the first one — the re-probe count (not resize events). */
|
|
122
134
|
anchorReprobes: number;
|
|
@@ -54,6 +54,16 @@
|
|
|
54
54
|
* - I-B15 · every batch starts by reconciling `top + capacity` with the live
|
|
55
55
|
* `rows - 1` (a resize event can arrive one frame late), and a column change
|
|
56
56
|
* resets before it writes.
|
|
57
|
+
* - I-B17 · a SHRINK re-pins the canvas, it never drops it. The canvas keeps
|
|
58
|
+
* `min(canvasRows, screenCapacity(rows))` rows — the screen keeps the rows nearest
|
|
59
|
+
* the parked cursor, so the survivors are its SUFFIX — and its top moves down to
|
|
60
|
+
* `capacity - kept`, which preserves LB-3. Dropping the cache instead made the next
|
|
61
|
+
* batch a SEED, and a seed assumes nothing is below the cursor
|
|
62
|
+
* (`writer-shapes.ts:60-64`); after a shrink that is false, so the batch overflowed
|
|
63
|
+
* the screen and the previous frame — status row included — went into the
|
|
64
|
+
* scrollback: the same row in two places. For the same reason a frame TALLER than
|
|
65
|
+
* the screen gives way at its TOP instead of being swallowed whole: the composer
|
|
66
|
+
* and the status line are the two rows a human is interacting with.
|
|
57
67
|
* - I-A9 · the writer never writes a probe byte; it only asks the host, which owns
|
|
58
68
|
* deferral, coalescing and the death switch.
|
|
59
69
|
*/
|
|
@@ -110,6 +120,7 @@ export function createScrollbackWriter(o) {
|
|
|
110
120
|
fallbacks: 0, guardHits: 0, guardHitsOutsideResize: 0, clippedRows: 0, elidedEntries: 0,
|
|
111
121
|
resizes: 0, repaints: 0, anchorSource: 'none', anchorCanvasRows: 0, anchorHeldRows: 0,
|
|
112
122
|
anchorGapRows: 0, anchorScrollRows: 0, anchorResets: 0, anchorReprobes: 0,
|
|
123
|
+
tallFramesTrimmed: 0,
|
|
113
124
|
};
|
|
114
125
|
const liveCols = () => {
|
|
115
126
|
const cols = o.cols();
|
|
@@ -205,6 +216,68 @@ export function createScrollbackWriter(o) {
|
|
|
205
216
|
stats.bytesWritten += bytes.length;
|
|
206
217
|
return bytes;
|
|
207
218
|
};
|
|
219
|
+
/**
|
|
220
|
+
* Drop from the canvas cache the rows a shorter screen no longer has (I-B17).
|
|
221
|
+
*
|
|
222
|
+
* A shrink deletes rows ABOVE the parked cursor, so what survives is the canvas's
|
|
223
|
+
* SUFFIX: the gap goes first, then the oldest content rows. Trimming the cache is
|
|
224
|
+
* what lets the next batch be a DIFF against what is really on screen. Dropping the
|
|
225
|
+
* cache (`reset()`) made it a SEED instead, and a seed assumes nothing is below the
|
|
226
|
+
* cursor (`writer-shapes.ts:60-64`) — after a shrink that is false, so the batch
|
|
227
|
+
* overflowed and the previous frame, status row included, was pushed into the
|
|
228
|
+
* scrollback instead of being erased.
|
|
229
|
+
*/
|
|
230
|
+
const trimCanvasCache = (capacity) => {
|
|
231
|
+
const lost = canvasRows - capacity;
|
|
232
|
+
if (lost <= 0)
|
|
233
|
+
return;
|
|
234
|
+
const lostFromGap = Math.min(lost, gapRows);
|
|
235
|
+
gapRows -= lostFromGap;
|
|
236
|
+
held = held.slice(Math.min(lost - lostFromGap, held.length));
|
|
237
|
+
prev = prev === null ? null : prev.slice(lost);
|
|
238
|
+
canvasRows = capacity;
|
|
239
|
+
// A drastic shrink can eat into the region itself (E2). `frameRows` is the height
|
|
240
|
+
// the next diff is aligned against, so it may never claim rows that are gone.
|
|
241
|
+
frameRows = Math.min(frameRows, Math.max(0, capacity - gapRows - held.length));
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Re-pin a canvas the terminal just got SHORTER, instead of dropping it (I-B17).
|
|
245
|
+
*
|
|
246
|
+
* `top` moves DOWN (`capacity - kept`) because the rows the screen lost were the ones
|
|
247
|
+
* above the cursor: the parked row is still `rows - 1`, so LB-3 holds for the same
|
|
248
|
+
* reason it always did.
|
|
249
|
+
*
|
|
250
|
+
* `capacity < 1` keeps the old answer (E3): a terminal with no row above the parked one
|
|
251
|
+
* has no canvas left to address, and re-measuring is then the only honest move. Columns
|
|
252
|
+
* are deliberately NOT handled here: a width change re-flows the rows themselves, so
|
|
253
|
+
* the cache describes nothing and still resets (D5 / E4).
|
|
254
|
+
*/
|
|
255
|
+
const repinShrunk = (rows) => {
|
|
256
|
+
const room = screenCapacity(rows);
|
|
257
|
+
// Only a MEASURED canvas may be re-pinned. `unknown` means nothing ever established
|
|
258
|
+
// that the cursor sits on the last row, and the arithmetic below starts from exactly
|
|
259
|
+
// that assumption; the append path erases nothing, so it stays the honest answer and
|
|
260
|
+
// keeps its bytes exactly as they were (I-B7).
|
|
261
|
+
const capacity = anchor.kind === 'pinned' ? Math.min(canvasRows, room) : 0;
|
|
262
|
+
if (capacity < 1) {
|
|
263
|
+
reset();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
trimCanvasCache(capacity);
|
|
267
|
+
anchor = { kind: 'pinned', top: room - capacity, capacity };
|
|
268
|
+
seedDownToTop = 0;
|
|
269
|
+
lastRows = rows;
|
|
270
|
+
// The re-draw this answers is the host's, deferred and coalesced (P0-2 / P0-3):
|
|
271
|
+
// `anchorDirty` is what lets a `repaint()` land while the feed is idle.
|
|
272
|
+
anchorDirty = true;
|
|
273
|
+
publishAnchorStats();
|
|
274
|
+
// Re-measure anyway (I-B14). Keeping the canvas is a bet that the parked row is still
|
|
275
|
+
// `rows - 1`, and only a measurement can confirm the terminal agrees; dropping the
|
|
276
|
+
// probe here would leave a wrong geometry with nothing left to correct it. A drag
|
|
277
|
+
// that fires dozens of resize events still costs one probe: the host coalesces them
|
|
278
|
+
// (P1-3 / E6), and `anchorReprobes` counts measurements, never requests.
|
|
279
|
+
requestProbe();
|
|
280
|
+
};
|
|
208
281
|
/**
|
|
209
282
|
* Bring `top + capacity` back in line with the live `rows - 1` BEFORE the batch
|
|
210
283
|
* reads either of them (§6.11, [P0-2]).
|
|
@@ -235,7 +308,10 @@ export function createScrollbackWriter(o) {
|
|
|
235
308
|
lastRows = rows;
|
|
236
309
|
return;
|
|
237
310
|
}
|
|
238
|
-
|
|
311
|
+
// The `resize` event can arrive a whole frame late, and on a terminal that never
|
|
312
|
+
// answers CPR it never arrives at all: this batch is then the last chance to keep
|
|
313
|
+
// the canvas instead of dropping it (I-B17, W6).
|
|
314
|
+
repinShrunk(rows);
|
|
239
315
|
};
|
|
240
316
|
const afterCanvasBatch = (layout, heldNext, frame, raw, cols, rows) => {
|
|
241
317
|
prev = [...new Array(layout.gap).fill(''), ...heldNext, ...frame];
|
|
@@ -402,6 +478,23 @@ export function createScrollbackWriter(o) {
|
|
|
402
478
|
canvasRows = lines.length;
|
|
403
479
|
gapRows = 0;
|
|
404
480
|
};
|
|
481
|
+
/**
|
|
482
|
+
* Give way at the TOP of a frame taller than the screen (I-B17 / W4).
|
|
483
|
+
*
|
|
484
|
+
* Such a frame cannot be addressed as ONE block, and swallowing it left the PREVIOUS
|
|
485
|
+
* frame on screen — which is how the status row came to be printed twice. The region's
|
|
486
|
+
* last rows are the composer and the status line, the two rows a human is interacting
|
|
487
|
+
* with, so the oldest rows go first and the next (shorter) frame restores the rest.
|
|
488
|
+
*
|
|
489
|
+
* BOTH entry points that draw a frame need this: `repaint()` replays the frame it kept,
|
|
490
|
+
* and that frame can predate a shrink that has since made the screen shorter.
|
|
491
|
+
*/
|
|
492
|
+
const giveWayAtTop = (lines, rows) => {
|
|
493
|
+
if (lines.length + 1 <= rows)
|
|
494
|
+
return lines;
|
|
495
|
+
stats.tallFramesTrimmed += 1;
|
|
496
|
+
return lines.slice(lines.length - (rows - 1));
|
|
497
|
+
};
|
|
405
498
|
const transform = (chunk) => {
|
|
406
499
|
if (finalized)
|
|
407
500
|
return '';
|
|
@@ -419,12 +512,7 @@ export function createScrollbackWriter(o) {
|
|
|
419
512
|
}
|
|
420
513
|
if (lines === null)
|
|
421
514
|
return countFallback(!firstChunk);
|
|
422
|
-
|
|
423
|
-
// Unreachable by construction (I-S1); never erase on a guess.
|
|
424
|
-
reset();
|
|
425
|
-
return '';
|
|
426
|
-
}
|
|
427
|
-
return assemble(lines, rows, liveCols(), 'frame');
|
|
515
|
+
return assemble(giveWayAtTop(lines, rows), rows, liveCols(), 'frame');
|
|
428
516
|
};
|
|
429
517
|
const repaint = () => {
|
|
430
518
|
repaintQueued = false;
|
|
@@ -437,7 +525,10 @@ export function createScrollbackWriter(o) {
|
|
|
437
525
|
const rows = liveRows();
|
|
438
526
|
if (rows === undefined || rows < 2)
|
|
439
527
|
return '';
|
|
440
|
-
|
|
528
|
+
// The kept frame may predate a shrink, so a replay gets the same top give-way as a
|
|
529
|
+
// live frame (W4): drawing it whole would overflow the new screen and push this
|
|
530
|
+
// session's own rows into the scrollback through the repaint door.
|
|
531
|
+
return assemble(giveWayAtTop(prevRawFrame, rows), rows, liveCols(), 'repaint');
|
|
441
532
|
};
|
|
442
533
|
const finalize = (summary) => {
|
|
443
534
|
if (finalized)
|
|
@@ -478,8 +569,10 @@ export function createScrollbackWriter(o) {
|
|
|
478
569
|
}
|
|
479
570
|
if (rowsNow !== undefined && lastRows !== undefined) {
|
|
480
571
|
if (rowsNow < lastRows) {
|
|
481
|
-
//
|
|
482
|
-
reset
|
|
572
|
+
// I-B17: the screen kept the rows nearest the cursor, so re-pin the canvas onto
|
|
573
|
+
// them instead of dropping it. A reset made the next batch a seed over a screen
|
|
574
|
+
// that is not empty, which is what left the old status row in the scrollback.
|
|
575
|
+
repinShrunk(rowsNow);
|
|
483
576
|
return;
|
|
484
577
|
}
|
|
485
578
|
if (anchor.kind === 'pinned') {
|
|
@@ -503,6 +596,12 @@ export function createScrollbackWriter(o) {
|
|
|
503
596
|
return;
|
|
504
597
|
const rows = liveRows();
|
|
505
598
|
const capacity = screenCapacity(rows);
|
|
599
|
+
// I-B17: a measurement can arrive after the terminal got shorter without ever
|
|
600
|
+
// announcing it (`rows` is read live, the `resize` event is optional). The cache
|
|
601
|
+
// must then stop describing rows the screen has already dropped, or the reach
|
|
602
|
+
// below would hand the next batch an erase distance longer than the canvas left.
|
|
603
|
+
if (rows !== undefined)
|
|
604
|
+
trimCanvasCache(capacity);
|
|
506
605
|
// I-B14: the report is "rows BELOW the cursor", and the canvas also still owns
|
|
507
606
|
// `canvasRows` rows ABOVE it, so the reachable height is their sum. Keeping only
|
|
508
607
|
// the first term collapses the canvas onto the parked row, and the next batch
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE shortcut table (tui-shortcut-drawer-and-task-summary §4.6 / appendix B).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY, AND THAT IS A HARD CONSTRAINT RATHER THAN A STYLE CHOICE: this file
|
|
5
|
+
* is inside `glyphs.test.ts::inScope` (`ui/` prefix), whose `A-1` case fails on a
|
|
6
|
+
* single non-ASCII byte. Every `keyword` and `detail` below is English on
|
|
7
|
+
* purpose; translating them would turn the scanner red, and the only escape
|
|
8
|
+
* hatch - adding this file to the exemptions - is the thing that helper's own
|
|
9
|
+
* comment argues against.
|
|
10
|
+
*
|
|
11
|
+
* THIS IS THE ONLY TRUTH ABOUT SHORTCUTS. The drawer, `/keys` and the tests all
|
|
12
|
+
* derive from it, and `shortcut-table.test.ts` holds it in a BIDIRECTIONAL set
|
|
13
|
+
* relation with `HelpOverlay`'s key column (D-8). The reverse direction is the
|
|
14
|
+
* one that matters: without it `Alt+K`, `Esc twice` and `Esc again` could sit in
|
|
15
|
+
* `/help` forever and never appear in the panel whose entire requirement is
|
|
16
|
+
* "list ALL of them".
|
|
17
|
+
*
|
|
18
|
+
* `keyword` IS CAPPED AT 10 CELLS (`SHORTCUT_PANEL_LIMITS.keywordMaxCells`). A
|
|
19
|
+
* longer one is a spec error, not a width problem: at 80 columns the keyword
|
|
20
|
+
* column is 11 cells wide, and the only remaining remedy would be truncating it -
|
|
21
|
+
* which is the exact defect (a column of `promp...`) this table exists to avoid.
|
|
22
|
+
*/
|
|
23
|
+
export type ShortcutGroupId = 'compose' | 'run' | 'panels' | 'session' | 'view';
|
|
24
|
+
/**
|
|
25
|
+
* When a row can actually be used. Unavailable rows are STILL LISTED - the user
|
|
26
|
+
* needs to know the key exists - but they render dim and explain themselves in
|
|
27
|
+
* the detail line when selected.
|
|
28
|
+
*/
|
|
29
|
+
export type ShortcutAvailability = 'always' | 'idle' | 'running' | 'app' | 'native';
|
|
30
|
+
export interface ShortcutEntry {
|
|
31
|
+
/** Stable id used by tests and by `/keys <query>`. */
|
|
32
|
+
id: string;
|
|
33
|
+
/** The primary binding, spelled in full the way `HelpOverlay` spells it. */
|
|
34
|
+
keys: string;
|
|
35
|
+
/** One to two words, ASCII only, at most 10 columns. The requirement's second column. */
|
|
36
|
+
keyword: string;
|
|
37
|
+
/** One ASCII sentence, shown when the row is selected. */
|
|
38
|
+
detail: string;
|
|
39
|
+
group: ShortcutGroupId;
|
|
40
|
+
availability: ShortcutAvailability;
|
|
41
|
+
/** Extra spellings that must also resolve in `/keys <query>`. */
|
|
42
|
+
aliases?: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
export declare const SHORTCUT_GROUPS: readonly {
|
|
45
|
+
id: ShortcutGroupId;
|
|
46
|
+
title: string;
|
|
47
|
+
}[];
|
|
48
|
+
/**
|
|
49
|
+
* The 31 rows, in reading order. Group order is fixed by `SHORTCUT_GROUPS`, and
|
|
50
|
+
* within a group the order is "how often a user reaches for it", not alphabetical:
|
|
51
|
+
* a reference panel is scanned from the top.
|
|
52
|
+
*/
|
|
53
|
+
export declare const SHORTCUTS: readonly ShortcutEntry[];
|
|
54
|
+
/**
|
|
55
|
+
* The longest `keys` string in cells - the input the column derivation needs.
|
|
56
|
+
*
|
|
57
|
+
* DERIVED, NEVER HARD-CODED: v1 fixed the key column at 21 cells, which left six
|
|
58
|
+
* for the keyword at 80 columns and turned `prompt library` into `promp...`. A
|
|
59
|
+
* derived value cannot go stale the day someone adds a longer binding.
|
|
60
|
+
*/
|
|
61
|
+
export declare function widestKeysCells(entries: readonly ShortcutEntry[]): number;
|
|
62
|
+
/**
|
|
63
|
+
* Filter for `/keys <query>`: case-insensitive substring over id, keys, keyword
|
|
64
|
+
* and aliases.
|
|
65
|
+
*
|
|
66
|
+
* NO KEYBOARD SEARCH IN THE PANEL ITSELF (§11 F-2): typing would fight `Esc` for
|
|
67
|
+
* "clear the filter or close the panel", and `Esc` is App-owned. The command is
|
|
68
|
+
* the zero-conflict way to reach the same filter.
|
|
69
|
+
*/
|
|
70
|
+
export declare function filterShortcuts(entries: readonly ShortcutEntry[], query: string): ShortcutEntry[];
|
|
71
|
+
/**
|
|
72
|
+
* Whether a row can be used right now.
|
|
73
|
+
*
|
|
74
|
+
* `running` MEANS "ONLY WHILE RUNNING", not "unavailable while running" - the two
|
|
75
|
+
* halves of the table would otherwise need two different predicates, which is the
|
|
76
|
+
* kind of asymmetry a later reader gets backwards.
|
|
77
|
+
*/
|
|
78
|
+
export declare function isAvailable(entry: ShortcutEntry, context: {
|
|
79
|
+
native: boolean;
|
|
80
|
+
running: boolean;
|
|
81
|
+
}): boolean;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE shortcut table (tui-shortcut-drawer-and-task-summary §4.6 / appendix B).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY, AND THAT IS A HARD CONSTRAINT RATHER THAN A STYLE CHOICE: this file
|
|
5
|
+
* is inside `glyphs.test.ts::inScope` (`ui/` prefix), whose `A-1` case fails on a
|
|
6
|
+
* single non-ASCII byte. Every `keyword` and `detail` below is English on
|
|
7
|
+
* purpose; translating them would turn the scanner red, and the only escape
|
|
8
|
+
* hatch - adding this file to the exemptions - is the thing that helper's own
|
|
9
|
+
* comment argues against.
|
|
10
|
+
*
|
|
11
|
+
* THIS IS THE ONLY TRUTH ABOUT SHORTCUTS. The drawer, `/keys` and the tests all
|
|
12
|
+
* derive from it, and `shortcut-table.test.ts` holds it in a BIDIRECTIONAL set
|
|
13
|
+
* relation with `HelpOverlay`'s key column (D-8). The reverse direction is the
|
|
14
|
+
* one that matters: without it `Alt+K`, `Esc twice` and `Esc again` could sit in
|
|
15
|
+
* `/help` forever and never appear in the panel whose entire requirement is
|
|
16
|
+
* "list ALL of them".
|
|
17
|
+
*
|
|
18
|
+
* `keyword` IS CAPPED AT 10 CELLS (`SHORTCUT_PANEL_LIMITS.keywordMaxCells`). A
|
|
19
|
+
* longer one is a spec error, not a width problem: at 80 columns the keyword
|
|
20
|
+
* column is 11 cells wide, and the only remaining remedy would be truncating it -
|
|
21
|
+
* which is the exact defect (a column of `promp...`) this table exists to avoid.
|
|
22
|
+
*/
|
|
23
|
+
import { cellWidth } from './layout/cells.js';
|
|
24
|
+
export const SHORTCUT_GROUPS = [
|
|
25
|
+
{ id: 'compose', title: 'COMPOSE' },
|
|
26
|
+
{ id: 'run', title: 'RUN' },
|
|
27
|
+
{ id: 'panels', title: 'PANELS' },
|
|
28
|
+
{ id: 'session', title: 'SESSION' },
|
|
29
|
+
{ id: 'view', title: 'VIEW' },
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* The 31 rows, in reading order. Group order is fixed by `SHORTCUT_GROUPS`, and
|
|
33
|
+
* within a group the order is "how often a user reaches for it", not alphabetical:
|
|
34
|
+
* a reference panel is scanned from the top.
|
|
35
|
+
*/
|
|
36
|
+
export const SHORTCUTS = [
|
|
37
|
+
// --- COMPOSE ---------------------------------------------------------------
|
|
38
|
+
{
|
|
39
|
+
id: 'enter', keys: 'Enter', keyword: 'send', group: 'compose', availability: 'always',
|
|
40
|
+
detail: 'Submits when idle; queues as steering while running;'
|
|
41
|
+
+ ' re-pins the view to the newest output.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'alt-enter', keys: 'Alt+Enter', keyword: 'newline', group: 'compose',
|
|
45
|
+
availability: 'always',
|
|
46
|
+
detail: 'Multiline input; the host may intercept this combination.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'shift-enter', keys: 'Shift+Enter', keyword: 'newline', group: 'compose',
|
|
50
|
+
availability: 'always',
|
|
51
|
+
detail: 'Needs keyboard enhancement; /terminal-setup explains how to bind it.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'ctrl-j', keys: 'Ctrl+J', keyword: 'newline', group: 'compose', availability: 'always',
|
|
55
|
+
detail: 'Newline fallback that does not depend on keyboard enhancement.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'history', keys: 'Up / Down', keyword: 'history', group: 'compose', availability: 'always',
|
|
59
|
+
detail: 'Recalls earlier prompts when the input box is empty.',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'paste', keys: 'Paste', keyword: 'paste', group: 'compose', availability: 'always',
|
|
63
|
+
detail: 'Up to 6 lines inserted as-is; larger becomes [Pasted text #N]; --no-paste disables.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'ctrl-o', keys: 'Ctrl+O', keyword: 'expand', group: 'compose', availability: 'always',
|
|
67
|
+
detail: 'Expands the newest tool output; in native mode it reprints it as a new block.',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'ctrl-c', keys: 'Ctrl+C', keyword: 'copy/exit', group: 'compose', availability: 'always',
|
|
71
|
+
aliases: ['Ctrl+C (with a selection)'],
|
|
72
|
+
detail: 'Copies a pending selection; with a draft x2 clears and x3 exits;'
|
|
73
|
+
+ ' empty input x2 exits.',
|
|
74
|
+
},
|
|
75
|
+
// --- RUN -------------------------------------------------------------------
|
|
76
|
+
{
|
|
77
|
+
id: 'esc', keys: 'Esc', keyword: 'close', group: 'run', availability: 'always',
|
|
78
|
+
detail: 'Closes the open overlay or menu; with nothing open it arms the interrupt ladder.',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'esc-twice', keys: 'Esc twice', keyword: 'interrupt', group: 'run', availability: 'running',
|
|
82
|
+
aliases: ['Esc again'],
|
|
83
|
+
detail: 'Requests an interrupt within 1.5s; the next press forces the stop.',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'shift-tab', keys: 'Shift+Tab', keyword: 'plan mode', group: 'run', availability: 'always',
|
|
87
|
+
detail: 'Toggles BUILD and PLAN; writes are refused and the agent only investigates.',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'ctrl-n', keys: 'Ctrl+N', keyword: 'plan mode', group: 'run', availability: 'always',
|
|
91
|
+
detail: 'The same toggle, for Windows consoles that cannot deliver Shift+Tab.',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'ctrl-t', keys: 'Ctrl+T', keyword: 'thinking', group: 'run', availability: 'always',
|
|
95
|
+
detail: "Shows or hides the model's thinking (off by default).",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'ctrl-r', keys: 'Ctrl+R', keyword: 'redraw', group: 'run', availability: 'always',
|
|
99
|
+
detail: 'Repaints the whole frame; use it when the terminal is garbled.',
|
|
100
|
+
},
|
|
101
|
+
// --- PANELS ----------------------------------------------------------------
|
|
102
|
+
{
|
|
103
|
+
id: 'ctrl-g', keys: 'Ctrl+G', keyword: 'all keys', group: 'panels', availability: 'always',
|
|
104
|
+
detail: 'Opens this panel; a second Ctrl+G closes it. Every shortcut is listed here.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'ctrl-b', keys: 'Ctrl+B', keyword: 'agents', group: 'panels', availability: 'always',
|
|
108
|
+
detail: 'Read-only subagent board: every child, its facts and its timeline.',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'ctrl-p', keys: 'Ctrl+P', keyword: 'prompts', group: 'panels', availability: 'always',
|
|
112
|
+
detail: 'Quick prompt library: pick, edit, reorder.',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'ctrl-k', keys: 'Ctrl+K', keyword: 'kits', group: 'panels', availability: 'always',
|
|
116
|
+
detail: 'Switch the main / fast / subagent model kit for this session.',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'ctrl-i', keys: 'Ctrl+I', keyword: 'index', group: 'panels', availability: 'idle',
|
|
120
|
+
detail: 'Unavailable while running; asks first, then runs the project-indexer skill.',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'alt-k', keys: 'Alt+K', keyword: 'kill line', group: 'panels', availability: 'always',
|
|
124
|
+
detail: 'Deletes from the cursor to the end of the line'
|
|
125
|
+
+ ' (this was Ctrl+K before the kit picker).',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'help', keys: '/help', keyword: 'reference', group: 'panels', availability: 'always',
|
|
129
|
+
detail: 'Full reference: keys, slash commands, plan mode, team, skills.',
|
|
130
|
+
},
|
|
131
|
+
// --- SESSION ---------------------------------------------------------------
|
|
132
|
+
{
|
|
133
|
+
id: 'ctrl-l', keys: 'Ctrl+L', keyword: 'branch', group: 'session', availability: 'always',
|
|
134
|
+
detail: 'Opens this session in a new terminal window with its own session id.',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'clear', keys: '/clear', keyword: 'new task', group: 'session', availability: 'always',
|
|
138
|
+
detail: 'Archives the old conversation, clears the context;'
|
|
139
|
+
+ ' the task summary is cleared with it.',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'resume', keys: '/resume', keyword: 'switch', group: 'session', availability: 'always',
|
|
143
|
+
detail: 'Loads a saved session; the current session moves to a new id.',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'session', keys: '/session', keyword: 'identity', group: 'session', availability: 'always',
|
|
147
|
+
detail: 'Shows the session id and file, copies the resume command, pins the id.',
|
|
148
|
+
},
|
|
149
|
+
// --- VIEW ------------------------------------------------------------------
|
|
150
|
+
{
|
|
151
|
+
id: 'page', keys: 'PgUp / PgDn', keyword: 'page', group: 'view', availability: 'always',
|
|
152
|
+
detail: 'Pages the transcript, or the overlay that currently owns the keyboard.',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'wheel', keys: 'Wheel', keyword: 'scroll', group: 'view', availability: 'app',
|
|
156
|
+
detail: 'Scrolls the transcript in app mode; in native mode the terminal owns this.',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'shift-wheel', keys: 'Shift+Wheel', keyword: 'page', group: 'view', availability: 'app',
|
|
160
|
+
detail: 'Scrolls a whole page.',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'shift-arrows', keys: 'Shift+Up / Shift+Down', keyword: 'line', group: 'view',
|
|
164
|
+
availability: 'app',
|
|
165
|
+
detail: 'Scrolls one line; native mode leaves this to the terminal.',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'drag-edge', keys: 'Drag right edge', keyword: 'scrollbar', group: 'view',
|
|
169
|
+
availability: 'app',
|
|
170
|
+
detail: 'Drags the right-edge scrollbar; the input stays fixed.',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'drag-text', keys: 'Drag text (left button)', keyword: 'select', group: 'view',
|
|
174
|
+
availability: 'app',
|
|
175
|
+
detail: 'Drags to select on-screen text; the highlight waits for a copy.',
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
/**
|
|
179
|
+
* The longest `keys` string in cells - the input the column derivation needs.
|
|
180
|
+
*
|
|
181
|
+
* DERIVED, NEVER HARD-CODED: v1 fixed the key column at 21 cells, which left six
|
|
182
|
+
* for the keyword at 80 columns and turned `prompt library` into `promp...`. A
|
|
183
|
+
* derived value cannot go stale the day someone adds a longer binding.
|
|
184
|
+
*/
|
|
185
|
+
export function widestKeysCells(entries) {
|
|
186
|
+
return entries.reduce((widest, entry) => Math.max(widest, cellWidth(entry.keys)), 0);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Filter for `/keys <query>`: case-insensitive substring over id, keys, keyword
|
|
190
|
+
* and aliases.
|
|
191
|
+
*
|
|
192
|
+
* NO KEYBOARD SEARCH IN THE PANEL ITSELF (§11 F-2): typing would fight `Esc` for
|
|
193
|
+
* "clear the filter or close the panel", and `Esc` is App-owned. The command is
|
|
194
|
+
* the zero-conflict way to reach the same filter.
|
|
195
|
+
*/
|
|
196
|
+
export function filterShortcuts(entries, query) {
|
|
197
|
+
const needle = query.trim().toLowerCase();
|
|
198
|
+
if (needle.length === 0)
|
|
199
|
+
return [...entries];
|
|
200
|
+
return entries.filter((entry) => {
|
|
201
|
+
const haystack = [entry.id, entry.keys, entry.keyword, ...(entry.aliases ?? [])];
|
|
202
|
+
return haystack.some((field) => field.toLowerCase().includes(needle));
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Whether a row can be used right now.
|
|
207
|
+
*
|
|
208
|
+
* `running` MEANS "ONLY WHILE RUNNING", not "unavailable while running" - the two
|
|
209
|
+
* halves of the table would otherwise need two different predicates, which is the
|
|
210
|
+
* kind of asymmetry a later reader gets backwards.
|
|
211
|
+
*/
|
|
212
|
+
export function isAvailable(entry, context) {
|
|
213
|
+
switch (entry.availability) {
|
|
214
|
+
case 'always': return true;
|
|
215
|
+
case 'idle': return !context.running;
|
|
216
|
+
case 'running': return context.running;
|
|
217
|
+
case 'app': return !context.native;
|
|
218
|
+
case 'native': return context.native;
|
|
219
|
+
default: return true;
|
|
220
|
+
}
|
|
221
|
+
}
|