@aragon-agent/cli 0.6.38 → 0.6.40
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 +263 -0
- package/README.md +544 -11
- package/dist/agent/controller.d.ts +245 -10
- package/dist/agent/controller.js +367 -22
- package/dist/agent/model-profile-settings.js +12 -1
- package/dist/agent/plan-prompt.d.ts +1 -3
- package/dist/agent/plan-prompt.js +13 -7
- package/dist/agent/reducer.d.ts +70 -2
- package/dist/agent/reducer.js +59 -6
- package/dist/agent/skill-triage-prompt.d.ts +42 -0
- package/dist/agent/skill-triage-prompt.js +74 -0
- package/dist/agent/system-prompt.d.ts +28 -2
- package/dist/agent/system-prompt.js +12 -4
- package/dist/cli.js +106 -32
- package/dist/commands/builtins.js +24 -3
- package/dist/commands/keys-command.d.ts +23 -0
- package/dist/commands/keys-command.js +38 -0
- package/dist/commands/registry.d.ts +39 -0
- package/dist/commands/skills.js +86 -1
- package/dist/commands/subagent-command.d.ts +25 -0
- package/dist/commands/subagent-command.js +242 -0
- package/dist/commands/task-command.d.ts +39 -0
- package/dist/commands/task-command.js +102 -0
- package/dist/compaction/command.js +16 -3
- package/dist/compaction/compactor.d.ts +25 -0
- package/dist/compaction/compactor.js +111 -10
- package/dist/compaction/digest.d.ts +24 -1
- package/dist/compaction/digest.js +139 -17
- package/dist/compaction/limits.d.ts +21 -0
- package/dist/compaction/limits.js +21 -0
- package/dist/compaction/reason-text.d.ts +36 -0
- package/dist/compaction/reason-text.js +78 -0
- package/dist/compaction/types.d.ts +82 -14
- package/dist/compaction/wiring.d.ts +13 -8
- package/dist/compaction/wiring.js +1 -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 +55 -3
- package/dist/config/schema.d.ts +125 -10
- package/dist/config/schema.js +138 -14
- package/dist/config/store.js +23 -3
- 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/log-mirror.d.ts +21 -4
- package/dist/proc/log-mirror.js +29 -7
- package/dist/proc/output-streams.d.ts +50 -0
- package/dist/proc/output-streams.js +54 -0
- package/dist/skills/service.d.ts +41 -0
- package/dist/skills/service.js +62 -0
- package/dist/skills/triage.d.ts +194 -0
- package/dist/skills/triage.js +393 -0
- package/dist/team/forced-dispatch.d.ts +95 -0
- package/dist/team/forced-dispatch.js +208 -0
- package/dist/team/limits.d.ts +21 -1
- package/dist/team/limits.js +21 -1
- package/dist/team/normalize.d.ts +15 -0
- package/dist/team/normalize.js +21 -4
- package/dist/team/prompt.d.ts +6 -3
- package/dist/team/prompt.js +9 -5
- 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/team/task-tool.js +11 -7
- package/dist/tools/bash-tool.d.ts +10 -0
- package/dist/tools/bash-tool.js +71 -6
- package/dist/tools/human-input.d.ts +35 -2
- package/dist/tools/human-input.js +38 -0
- package/dist/tools/index.d.ts +93 -1
- package/dist/tools/index.js +112 -3
- package/dist/tools/plan-tools.d.ts +20 -12
- package/dist/tools/plan-tools.js +24 -17
- package/dist/tools/tool-gate.d.ts +100 -0
- package/dist/tools/tool-gate.js +87 -0
- package/dist/ui/App.js +238 -12
- package/dist/ui/Header.d.ts +28 -16
- package/dist/ui/Header.js +21 -19
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/entries/CompactionCard.d.ts +11 -1
- package/dist/ui/entries/CompactionCard.js +65 -4
- 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/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +18 -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/SettingsScreen.js +29 -5
- package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
- package/dist/ui/overlays/ShortcutPanel.js +106 -0
- package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
- package/dist/ui/overlays/TeamFanoutPage.js +78 -0
- package/dist/ui/overlays/model-profile-state.d.ts +19 -1
- package/dist/ui/overlays/model-profile-state.js +12 -0
- package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
- package/dist/ui/overlays/team-fanout-state.js +97 -0
- package/dist/ui/shortcuts.d.ts +81 -0
- package/dist/ui/shortcuts.js +221 -0
- package/package.json +2 -2
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shortcut drawer's geometry and row layout
|
|
3
|
+
* (tui-shortcut-drawer-and-task-summary §4.5).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/ui/**` is inside the glyph scanner's scope, so every symbol
|
|
6
|
+
* this module emits arrives as a `glyphs` field and is never spelled here.
|
|
7
|
+
*
|
|
8
|
+
* PURE AND TOTAL. No Ink, no clock, no terminal: the drawer component renders
|
|
9
|
+
* what this returns, `OverlayFrame` slices the window this function DERIVED, and
|
|
10
|
+
* `shortcut-panel-layout.test.ts` pins the arithmetic directly. That split is the
|
|
11
|
+
* whole defence against P0-1, which was not a rendering bug: v1 hand-fixed the key
|
|
12
|
+
* column at 21 cells, which left six cells for the keyword at 80 columns, so every
|
|
13
|
+
* keyword was truncated to `promp...` while a "does not overflow" assertion stayed
|
|
14
|
+
* green. The column is therefore DERIVED from the table, and a keyword that does
|
|
15
|
+
* not fit makes its row STACK rather than be cut.
|
|
16
|
+
*
|
|
17
|
+
* THERE IS EXACTLY ONE WINDOW AUTHORITY: `cursor`. v1 took both `cursor` and
|
|
18
|
+
* `scrollOffset`, which meant paging could leave the selected row - and the detail
|
|
19
|
+
* line that belongs to it - off screen. Paging now moves the CURSOR by a page and
|
|
20
|
+
* the window is the smallest one containing it.
|
|
21
|
+
*/
|
|
22
|
+
import { type OverlayWindow } from './overlay-window.js';
|
|
23
|
+
import { type ShortcutEntry } from '../shortcuts.js';
|
|
24
|
+
export declare const SHORTCUT_PANEL_LIMITS: {
|
|
25
|
+
/** Below this the drawer is full width: a 30-cell bar on a 40-cell screen is noise. */
|
|
26
|
+
readonly minSplitCols: 64;
|
|
27
|
+
/** Fraction of the terminal the drawer takes when it IS a drawer. */
|
|
28
|
+
readonly widthFraction: 0.34;
|
|
29
|
+
readonly minWidth: 30;
|
|
30
|
+
readonly maxWidth: 46;
|
|
31
|
+
/** Rail (1 cell) + `paddingLeft` (1 cell) that `OverlayFrame` spends below 48 columns. */
|
|
32
|
+
readonly railCells: 2;
|
|
33
|
+
/** Border (2 cells) + `paddingX` (2 cells) that `OverlayFrame` spends at or above 48. */
|
|
34
|
+
readonly boxCells: 4;
|
|
35
|
+
/**
|
|
36
|
+
* The key column is DERIVED: `min(longestKeys, ceil(listCells * 0.55))`.
|
|
37
|
+
*
|
|
38
|
+
* A hard 21 is what turned `prompt library` into `promp...` at 80 columns. The
|
|
39
|
+
* share is a ceiling, not a target: a table of short keys gets a narrow key
|
|
40
|
+
* column and the leftover width goes to the keyword.
|
|
41
|
+
*/
|
|
42
|
+
readonly keyColumnShare: 0.55;
|
|
43
|
+
/** The keyword column must be at least this wide, or the row stacks. */
|
|
44
|
+
readonly keywordMinCells: 8;
|
|
45
|
+
/** Hard ceiling for a keyword string; a longer one is a spec bug, not a width problem. */
|
|
46
|
+
readonly keywordMaxCells: 10;
|
|
47
|
+
/** Gap between the key column and the keyword column. */
|
|
48
|
+
readonly keyGapCells: 1;
|
|
49
|
+
/** Indent for the selected row's detail lines, and for a stacked keyword. */
|
|
50
|
+
readonly detailIndent: 2;
|
|
51
|
+
/** Title + footer rows are chrome and are spent out of the row budget. */
|
|
52
|
+
readonly chromeRows: 2;
|
|
53
|
+
/** The task section is capped so the key list always gets the majority of the panel. */
|
|
54
|
+
readonly taskMaxRows: 2;
|
|
55
|
+
/**
|
|
56
|
+
* ABOVE THIS BODY HEIGHT THE GROUP HEADERS GET THEIR OWN ROWS.
|
|
57
|
+
*
|
|
58
|
+
* Below it they are dropped entirely - not compressed into `---- GROUP ----`
|
|
59
|
+
* separators, which was v2's rule and left FOUR entry rows visible on an
|
|
60
|
+
* 80x24 terminal (title + task + rule + five separators = 9 of 13 body rows).
|
|
61
|
+
* See `spec.md`'s `## 实施过程发现的方案缺陷` note 1: the ordering still runs
|
|
62
|
+
* COMPOSE -> RUN -> PANELS -> SESSION -> VIEW, and the requirement ("list every
|
|
63
|
+
* shortcut from top to bottom") is met by the entries themselves.
|
|
64
|
+
*/
|
|
65
|
+
readonly groupHeaderMinRows: 18;
|
|
66
|
+
/** The longest detail line the selected row may grow to. */
|
|
67
|
+
readonly detailMaxRows: 2;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* The drawer's width in columns.
|
|
71
|
+
*
|
|
72
|
+
* `cols < 64` is FULL WIDTH on purpose: at 40 columns a right-aligned 30-cell bar
|
|
73
|
+
* spends 10 of the user's columns on empty space. The clamp mirrors
|
|
74
|
+
* `rail.ts::todoRailWidth` (same shape, same reason).
|
|
75
|
+
*/
|
|
76
|
+
export declare function shortcutPanelWidth(cols: number): number;
|
|
77
|
+
/**
|
|
78
|
+
* Cells the drawer may draw on INSIDE its frame.
|
|
79
|
+
*
|
|
80
|
+
* THE FORM IS DECIDED BY THE TERMINAL, exactly as `OverlayFrame` decides it
|
|
81
|
+
* (`cols >= BORDERLESS_COLS`), so this function takes the same `cols` the frame
|
|
82
|
+
* receives. Passing the panel's own width instead would make a 46-cell drawer on
|
|
83
|
+
* a 200-column terminal fall back to the rail form for ever, because 46 < 48.
|
|
84
|
+
*/
|
|
85
|
+
export declare function shortcutPanelContentCells(cols: number): number;
|
|
86
|
+
/** One rendered row. `text` already measures at most the panel's content width. */
|
|
87
|
+
export interface ShortcutRow {
|
|
88
|
+
/** `entry` = key + keyword on one row; `stacked` = key alone, keyword below. */
|
|
89
|
+
kind: 'task' | 'rule' | 'group' | 'entry' | 'stacked' | 'keyword' | 'detail';
|
|
90
|
+
text: string;
|
|
91
|
+
/** Which entry a `detail` / `keyword` row belongs to; `null` for every other kind. */
|
|
92
|
+
entryId: string | null;
|
|
93
|
+
tone: 'accent' | 'primary' | 'muted' | 'dim';
|
|
94
|
+
}
|
|
95
|
+
/** The task section's inputs, already flattened by the caller. */
|
|
96
|
+
export interface ShortcutTaskSection {
|
|
97
|
+
/** The label, or `null` when there is none yet. */
|
|
98
|
+
text: string | null;
|
|
99
|
+
/** `model` / `fallback`, so the row can be coloured by provenance. */
|
|
100
|
+
tone: 'model' | 'fallback' | null;
|
|
101
|
+
/** Why it is a fallback, or `null`. */
|
|
102
|
+
reason: string | null;
|
|
103
|
+
/** `provider:model`, or `''`. */
|
|
104
|
+
by: string;
|
|
105
|
+
/** Age in ms, or `null` when unknown. */
|
|
106
|
+
ageMs: number | null;
|
|
107
|
+
/** `false` when `taskSummary.enabled` is off. */
|
|
108
|
+
enabled: boolean;
|
|
109
|
+
/** A call is in flight. */
|
|
110
|
+
pending: boolean;
|
|
111
|
+
}
|
|
112
|
+
export interface BuildShortcutRowsInput {
|
|
113
|
+
cols: number;
|
|
114
|
+
/** Body rows available INSIDE the frame (`OverlayFrame` already subtracted its chrome). */
|
|
115
|
+
rows: number;
|
|
116
|
+
/** Index into the FILTERED entry list. The one authority for the window. */
|
|
117
|
+
cursor: number;
|
|
118
|
+
/**
|
|
119
|
+
* THE ENTRIES ARRIVE FILTERED AND WITH THEIR AVAILABILITY, so this module takes
|
|
120
|
+
* neither a `query` nor a `context` for them (review fix RF-2). It used to accept
|
|
121
|
+
* both - and `detailsExpanded` as well - and read none of them, which is the exact
|
|
122
|
+
* "second source of truth that silently does nothing" its own 0-3 rule forbids: a
|
|
123
|
+
* caller could pass `query: 'git'` and get the unfiltered list with nothing
|
|
124
|
+
* failing.
|
|
125
|
+
*/
|
|
126
|
+
entries: readonly ShortcutEntry[];
|
|
127
|
+
task: ShortcutTaskSection;
|
|
128
|
+
/** Per-entry availability, so an unavailable row renders dim and explains itself. */
|
|
129
|
+
entryAvailable?: (entry: ShortcutEntry) => boolean;
|
|
130
|
+
/** The sentence appended to an unavailable row's detail line. */
|
|
131
|
+
unavailableReason?: string;
|
|
132
|
+
glyphs: {
|
|
133
|
+
ellipsis: string;
|
|
134
|
+
cursor: string;
|
|
135
|
+
hrule: string;
|
|
136
|
+
midDot: string;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export interface BuildShortcutRowsResult {
|
|
140
|
+
rows: ShortcutRow[];
|
|
141
|
+
window: OverlayWindow;
|
|
142
|
+
/** Row index of the selected entry; equal to `window.start + 0` at the top. */
|
|
143
|
+
cursorRow: number;
|
|
144
|
+
/** Total rows the panel holds, which is what the frame's position indicator counts. */
|
|
145
|
+
total: number;
|
|
146
|
+
/** The footer string; `OverlayFrame`'s `hint`, and chrome rather than a row. */
|
|
147
|
+
hint: string;
|
|
148
|
+
}
|
|
149
|
+
/** Column widths for one geometry. Exported so the tests can assert them directly. */
|
|
150
|
+
export interface ShortcutColumns {
|
|
151
|
+
/** The panel's own width. */
|
|
152
|
+
width: number;
|
|
153
|
+
/** Content cells inside the frame. */
|
|
154
|
+
available: number;
|
|
155
|
+
/** Cells left for a list row after the cursor column is spent. */
|
|
156
|
+
listCells: number;
|
|
157
|
+
keyCells: number;
|
|
158
|
+
keywordCells: number;
|
|
159
|
+
/** True when no entry fits the two-column form and every row stacks. */
|
|
160
|
+
stacked: boolean;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Derive the two columns (P0-1).
|
|
164
|
+
*
|
|
165
|
+
* `stacked` is a GLOBAL flag only when the keyword column is too narrow for any
|
|
166
|
+
* keyword at all; otherwise stacking is decided per row, so one long binding
|
|
167
|
+
* (`Shift+Up / Shift+Down`, 20 cells) does not cost the other thirty rows a line.
|
|
168
|
+
*/
|
|
169
|
+
export declare function shortcutColumns(cols: number, entries: readonly ShortcutEntry[], glyphs: {
|
|
170
|
+
cursor: string;
|
|
171
|
+
}): ShortcutColumns;
|
|
172
|
+
/**
|
|
173
|
+
* The footer, built by DROPPING clauses from the right (never by re-wording).
|
|
174
|
+
*
|
|
175
|
+
* `Esc close` is never dropped: it is the one thing a user must be able to find on
|
|
176
|
+
* a terminal too narrow to explain anything else. The rest go in a fixed order so
|
|
177
|
+
* the footer is stable across resizes instead of shuffling.
|
|
178
|
+
*/
|
|
179
|
+
export declare function shortcutFooter(cols: number, glyphs: {
|
|
180
|
+
midDot: string;
|
|
181
|
+
}, opts?: {
|
|
182
|
+
canRefresh: boolean;
|
|
183
|
+
}): string;
|
|
184
|
+
/**
|
|
185
|
+
* Build every row the drawer can show, and derive the window from the cursor.
|
|
186
|
+
*
|
|
187
|
+
* @param input.cols Terminal columns (the frame receives the same number).
|
|
188
|
+
* @param input.rows Body rows inside the frame - `overlayBodyRows(maxRows, cols)`.
|
|
189
|
+
* @param input.cursor Index into `input.entries`; clamped, never trusted.
|
|
190
|
+
*/
|
|
191
|
+
export declare function buildShortcutRows(input: BuildShortcutRowsInput): BuildShortcutRowsResult;
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shortcut drawer's geometry and row layout
|
|
3
|
+
* (tui-shortcut-drawer-and-task-summary §4.5).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/ui/**` is inside the glyph scanner's scope, so every symbol
|
|
6
|
+
* this module emits arrives as a `glyphs` field and is never spelled here.
|
|
7
|
+
*
|
|
8
|
+
* PURE AND TOTAL. No Ink, no clock, no terminal: the drawer component renders
|
|
9
|
+
* what this returns, `OverlayFrame` slices the window this function DERIVED, and
|
|
10
|
+
* `shortcut-panel-layout.test.ts` pins the arithmetic directly. That split is the
|
|
11
|
+
* whole defence against P0-1, which was not a rendering bug: v1 hand-fixed the key
|
|
12
|
+
* column at 21 cells, which left six cells for the keyword at 80 columns, so every
|
|
13
|
+
* keyword was truncated to `promp...` while a "does not overflow" assertion stayed
|
|
14
|
+
* green. The column is therefore DERIVED from the table, and a keyword that does
|
|
15
|
+
* not fit makes its row STACK rather than be cut.
|
|
16
|
+
*
|
|
17
|
+
* THERE IS EXACTLY ONE WINDOW AUTHORITY: `cursor`. v1 took both `cursor` and
|
|
18
|
+
* `scrollOffset`, which meant paging could leave the selected row - and the detail
|
|
19
|
+
* line that belongs to it - off screen. Paging now moves the CURSOR by a page and
|
|
20
|
+
* the window is the smallest one containing it.
|
|
21
|
+
*/
|
|
22
|
+
import stringWidth from 'string-width';
|
|
23
|
+
import { fitCells, padCells, wrapCells } from './cells.js';
|
|
24
|
+
import { sliceWindow } from './overlay-window.js';
|
|
25
|
+
import { SHORTCUT_GROUPS, widestKeysCells } from '../shortcuts.js';
|
|
26
|
+
export const SHORTCUT_PANEL_LIMITS = {
|
|
27
|
+
/** Below this the drawer is full width: a 30-cell bar on a 40-cell screen is noise. */
|
|
28
|
+
minSplitCols: 64,
|
|
29
|
+
/** Fraction of the terminal the drawer takes when it IS a drawer. */
|
|
30
|
+
widthFraction: 0.34,
|
|
31
|
+
minWidth: 30,
|
|
32
|
+
maxWidth: 46,
|
|
33
|
+
/** Rail (1 cell) + `paddingLeft` (1 cell) that `OverlayFrame` spends below 48 columns. */
|
|
34
|
+
railCells: 2,
|
|
35
|
+
/** Border (2 cells) + `paddingX` (2 cells) that `OverlayFrame` spends at or above 48. */
|
|
36
|
+
boxCells: 4,
|
|
37
|
+
/**
|
|
38
|
+
* The key column is DERIVED: `min(longestKeys, ceil(listCells * 0.55))`.
|
|
39
|
+
*
|
|
40
|
+
* A hard 21 is what turned `prompt library` into `promp...` at 80 columns. The
|
|
41
|
+
* share is a ceiling, not a target: a table of short keys gets a narrow key
|
|
42
|
+
* column and the leftover width goes to the keyword.
|
|
43
|
+
*/
|
|
44
|
+
keyColumnShare: 0.55,
|
|
45
|
+
/** The keyword column must be at least this wide, or the row stacks. */
|
|
46
|
+
keywordMinCells: 8,
|
|
47
|
+
/** Hard ceiling for a keyword string; a longer one is a spec bug, not a width problem. */
|
|
48
|
+
keywordMaxCells: 10,
|
|
49
|
+
/** Gap between the key column and the keyword column. */
|
|
50
|
+
keyGapCells: 1,
|
|
51
|
+
/** Indent for the selected row's detail lines, and for a stacked keyword. */
|
|
52
|
+
detailIndent: 2,
|
|
53
|
+
/** Title + footer rows are chrome and are spent out of the row budget. */
|
|
54
|
+
chromeRows: 2,
|
|
55
|
+
/** The task section is capped so the key list always gets the majority of the panel. */
|
|
56
|
+
taskMaxRows: 2,
|
|
57
|
+
/**
|
|
58
|
+
* ABOVE THIS BODY HEIGHT THE GROUP HEADERS GET THEIR OWN ROWS.
|
|
59
|
+
*
|
|
60
|
+
* Below it they are dropped entirely - not compressed into `---- GROUP ----`
|
|
61
|
+
* separators, which was v2's rule and left FOUR entry rows visible on an
|
|
62
|
+
* 80x24 terminal (title + task + rule + five separators = 9 of 13 body rows).
|
|
63
|
+
* See `spec.md`'s `## 实施过程发现的方案缺陷` note 1: the ordering still runs
|
|
64
|
+
* COMPOSE -> RUN -> PANELS -> SESSION -> VIEW, and the requirement ("list every
|
|
65
|
+
* shortcut from top to bottom") is met by the entries themselves.
|
|
66
|
+
*/
|
|
67
|
+
groupHeaderMinRows: 18,
|
|
68
|
+
/** The longest detail line the selected row may grow to. */
|
|
69
|
+
detailMaxRows: 2,
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* The drawer's width in columns.
|
|
73
|
+
*
|
|
74
|
+
* `cols < 64` is FULL WIDTH on purpose: at 40 columns a right-aligned 30-cell bar
|
|
75
|
+
* spends 10 of the user's columns on empty space. The clamp mirrors
|
|
76
|
+
* `rail.ts::todoRailWidth` (same shape, same reason).
|
|
77
|
+
*/
|
|
78
|
+
export function shortcutPanelWidth(cols) {
|
|
79
|
+
const width = Number.isFinite(cols) ? Math.floor(cols) : 80;
|
|
80
|
+
const safe = Math.max(0, width);
|
|
81
|
+
if (safe < SHORTCUT_PANEL_LIMITS.minSplitCols)
|
|
82
|
+
return safe;
|
|
83
|
+
const fraction = Math.round(safe * SHORTCUT_PANEL_LIMITS.widthFraction);
|
|
84
|
+
return Math.min(SHORTCUT_PANEL_LIMITS.maxWidth, Math.max(SHORTCUT_PANEL_LIMITS.minWidth, fraction));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Cells the drawer may draw on INSIDE its frame.
|
|
88
|
+
*
|
|
89
|
+
* THE FORM IS DECIDED BY THE TERMINAL, exactly as `OverlayFrame` decides it
|
|
90
|
+
* (`cols >= BORDERLESS_COLS`), so this function takes the same `cols` the frame
|
|
91
|
+
* receives. Passing the panel's own width instead would make a 46-cell drawer on
|
|
92
|
+
* a 200-column terminal fall back to the rail form for ever, because 46 < 48.
|
|
93
|
+
*/
|
|
94
|
+
export function shortcutPanelContentCells(cols) {
|
|
95
|
+
const width = shortcutPanelWidth(cols);
|
|
96
|
+
const chrome = Math.max(0, Math.floor(cols)) >= 48
|
|
97
|
+
? SHORTCUT_PANEL_LIMITS.boxCells
|
|
98
|
+
: SHORTCUT_PANEL_LIMITS.railCells;
|
|
99
|
+
return Math.max(0, width - chrome);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Derive the two columns (P0-1).
|
|
103
|
+
*
|
|
104
|
+
* `stacked` is a GLOBAL flag only when the keyword column is too narrow for any
|
|
105
|
+
* keyword at all; otherwise stacking is decided per row, so one long binding
|
|
106
|
+
* (`Shift+Up / Shift+Down`, 20 cells) does not cost the other thirty rows a line.
|
|
107
|
+
*/
|
|
108
|
+
export function shortcutColumns(cols, entries, glyphs) {
|
|
109
|
+
const width = shortcutPanelWidth(cols);
|
|
110
|
+
const available = shortcutPanelContentCells(cols);
|
|
111
|
+
// The cursor glyph owns one cell on the selected row, so it comes out of the
|
|
112
|
+
// LIST's budget rather than out of the keyword's.
|
|
113
|
+
const cursorCells = stringWidth(glyphs.cursor) > 0 ? 1 : 0;
|
|
114
|
+
const listCells = Math.max(0, available - cursorCells);
|
|
115
|
+
const longest = widestKeysCells(entries);
|
|
116
|
+
const share = Math.ceil(listCells * SHORTCUT_PANEL_LIMITS.keyColumnShare);
|
|
117
|
+
const keyCells = Math.min(longest, Math.max(0, share));
|
|
118
|
+
const keywordCells = Math.max(0, listCells - keyCells - SHORTCUT_PANEL_LIMITS.keyGapCells);
|
|
119
|
+
return {
|
|
120
|
+
width,
|
|
121
|
+
available,
|
|
122
|
+
listCells,
|
|
123
|
+
keyCells,
|
|
124
|
+
keywordCells,
|
|
125
|
+
stacked: keywordCells < SHORTCUT_PANEL_LIMITS.keywordMinCells,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** `12s ago` / `3m ago` / `1h ago`, without introducing a second clock. */
|
|
129
|
+
function ageLabel(ageMs) {
|
|
130
|
+
if (ageMs === null || !Number.isFinite(ageMs) || ageMs < 0)
|
|
131
|
+
return '';
|
|
132
|
+
const seconds = Math.floor(ageMs / 1000);
|
|
133
|
+
if (seconds < 60)
|
|
134
|
+
return `${seconds}s ago`;
|
|
135
|
+
const minutes = Math.floor(seconds / 60);
|
|
136
|
+
if (minutes < 60)
|
|
137
|
+
return `${minutes}m ago`;
|
|
138
|
+
return `${Math.floor(minutes / 60)}h ago`;
|
|
139
|
+
}
|
|
140
|
+
/** The task section's status line, or the reason there is none. */
|
|
141
|
+
function taskStatusLine(task, glyphs) {
|
|
142
|
+
if (!task.enabled)
|
|
143
|
+
return 'task summary is off (config taskSummary.enabled)';
|
|
144
|
+
if (task.pending)
|
|
145
|
+
return 'summarizing' + ` ${glyphs.midDot} ` + 'r refresh';
|
|
146
|
+
if (task.text === null) {
|
|
147
|
+
const reason = task.reason === null ? '' : ` ${glyphs.midDot} ${task.reason}`;
|
|
148
|
+
return `no summary yet${reason} ${glyphs.midDot} r refresh`;
|
|
149
|
+
}
|
|
150
|
+
const age = ageLabel(task.ageMs);
|
|
151
|
+
const who = task.tone === 'fallback' ? 'fallback' : task.by;
|
|
152
|
+
const parts = [who, age].filter((part) => part.length > 0);
|
|
153
|
+
return `${parts.join(` ${glyphs.midDot} `)} ${glyphs.midDot} r refresh`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The footer, built by DROPPING clauses from the right (never by re-wording).
|
|
157
|
+
*
|
|
158
|
+
* `Esc close` is never dropped: it is the one thing a user must be able to find on
|
|
159
|
+
* a terminal too narrow to explain anything else. The rest go in a fixed order so
|
|
160
|
+
* the footer is stable across resizes instead of shuffling.
|
|
161
|
+
*/
|
|
162
|
+
export function shortcutFooter(cols, glyphs, opts = { canRefresh: true }) {
|
|
163
|
+
const available = shortcutPanelContentCells(cols);
|
|
164
|
+
// SHORT SPELLINGS ON PURPOSE. The drawer is 30 cells wide at 80 columns and 46 at
|
|
165
|
+
// most, so a footer that spelled `Up/Down select · PgUp/PgDn page · d details ·
|
|
166
|
+
// r refresh · Esc close` (67 cells) would lose three clauses at EVERY width - the
|
|
167
|
+
// ladder would never be exercised, only its last rung. These forms fit four of the
|
|
168
|
+
// five at the maximum width and still keep the one that matters at the minimum.
|
|
169
|
+
const clauses = [
|
|
170
|
+
'Up/Dn',
|
|
171
|
+
'PgUp/PgDn',
|
|
172
|
+
'd details',
|
|
173
|
+
...(opts.canRefresh ? ['r refresh'] : []),
|
|
174
|
+
'Esc close',
|
|
175
|
+
];
|
|
176
|
+
// Drop order: the least essential clause first. `Esc close` is never dropped - on a
|
|
177
|
+
// terminal too narrow to explain anything else, that is the one key a user must be
|
|
178
|
+
// able to find.
|
|
179
|
+
const droppable = ['r refresh', 'Up/Dn', 'PgUp/PgDn', 'd details'];
|
|
180
|
+
const sep = ` ${glyphs.midDot} `;
|
|
181
|
+
let kept = [...clauses];
|
|
182
|
+
while (kept.length > 1 && stringWidth(kept.join(sep)) > available) {
|
|
183
|
+
const victim = droppable.find((clause) => kept.includes(clause));
|
|
184
|
+
if (victim === undefined)
|
|
185
|
+
break;
|
|
186
|
+
kept = kept.filter((clause) => clause !== victim);
|
|
187
|
+
}
|
|
188
|
+
return kept.join(sep);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Build every row the drawer can show, and derive the window from the cursor.
|
|
192
|
+
*
|
|
193
|
+
* @param input.cols Terminal columns (the frame receives the same number).
|
|
194
|
+
* @param input.rows Body rows inside the frame - `overlayBodyRows(maxRows, cols)`.
|
|
195
|
+
* @param input.cursor Index into `input.entries`; clamped, never trusted.
|
|
196
|
+
*/
|
|
197
|
+
export function buildShortcutRows(input) {
|
|
198
|
+
const { glyphs } = input;
|
|
199
|
+
const columns = shortcutColumns(input.cols, input.entries, glyphs);
|
|
200
|
+
const rows = [];
|
|
201
|
+
const push = (row) => { rows.push(row); };
|
|
202
|
+
const wrapped = (text, cells, maxRows) => wrapCells(text, cells, maxRows, glyphs.ellipsis).lines;
|
|
203
|
+
// --- title ---------------------------------------------------------------
|
|
204
|
+
// NO TITLE ROW (deviation from appendix A's sketch, recorded in `spec.md`'s
|
|
205
|
+
// implementation-issues note): `OverlayFrame` already draws `title` as chrome,
|
|
206
|
+
// and the `details on|off` marker rides there instead - one string on a row
|
|
207
|
+
// that is already paid for, rather than a whole body row spent on it.
|
|
208
|
+
// --- task section (at most `taskMaxRows`) --------------------------------
|
|
209
|
+
const statusText = () => fitCells(taskStatusLine(input.task, glyphs), columns.available, glyphs.ellipsis);
|
|
210
|
+
if (!input.task.enabled) {
|
|
211
|
+
push({ kind: 'task', text: statusText(), entryId: null, tone: 'dim' });
|
|
212
|
+
}
|
|
213
|
+
else if (input.task.text !== null) {
|
|
214
|
+
for (const line of wrapped(input.task.text, columns.available, 1)) {
|
|
215
|
+
push({
|
|
216
|
+
kind: 'task',
|
|
217
|
+
text: line,
|
|
218
|
+
entryId: null,
|
|
219
|
+
tone: input.task.tone === 'fallback' ? 'dim' : 'primary',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (input.rows >= SHORTCUT_PANEL_LIMITS.groupHeaderMinRows) {
|
|
223
|
+
push({ kind: 'task', text: statusText(), entryId: null, tone: 'muted' });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
push({ kind: 'task', text: statusText(), entryId: null, tone: 'muted' });
|
|
228
|
+
}
|
|
229
|
+
// --- rule ----------------------------------------------------------------
|
|
230
|
+
if (input.rows >= SHORTCUT_PANEL_LIMITS.groupHeaderMinRows) {
|
|
231
|
+
push({
|
|
232
|
+
kind: 'rule',
|
|
233
|
+
text: glyphs.hrule.repeat(columns.available),
|
|
234
|
+
entryId: null,
|
|
235
|
+
tone: 'muted',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// --- entries -------------------------------------------------------------
|
|
239
|
+
const entries = input.entries;
|
|
240
|
+
const total = entries.length;
|
|
241
|
+
const cursor = total === 0 ? 0 : Math.min(total - 1, Math.max(0, Math.floor(input.cursor)));
|
|
242
|
+
/** Indent shared by the detail lines and a stacked keyword. */
|
|
243
|
+
const indent = ' '.repeat(SHORTCUT_PANEL_LIMITS.detailIndent);
|
|
244
|
+
let cursorRow = 0;
|
|
245
|
+
const groupHeaders = input.rows >= SHORTCUT_PANEL_LIMITS.groupHeaderMinRows;
|
|
246
|
+
let currentGroup = '';
|
|
247
|
+
entries.forEach((entry, index) => {
|
|
248
|
+
const selected = index === cursor;
|
|
249
|
+
if (groupHeaders && entry.group !== currentGroup) {
|
|
250
|
+
currentGroup = entry.group;
|
|
251
|
+
const title = SHORTCUT_GROUPS.find((group) => group.id === entry.group)?.title ?? '';
|
|
252
|
+
push({
|
|
253
|
+
kind: 'group',
|
|
254
|
+
text: fitCells(title, columns.available, glyphs.ellipsis),
|
|
255
|
+
entryId: null,
|
|
256
|
+
tone: 'accent',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const usable = input.entryAvailable ? input.entryAvailable(entry) : true;
|
|
260
|
+
const tone = usable ? (selected ? 'primary' : 'muted') : 'dim';
|
|
261
|
+
const lead = selected ? glyphs.cursor : ' ';
|
|
262
|
+
if (selected)
|
|
263
|
+
cursorRow = rows.length;
|
|
264
|
+
const keyRoom = columns.keyCells;
|
|
265
|
+
if (columns.stacked || stringWidth(entry.keys) > keyRoom) {
|
|
266
|
+
push({
|
|
267
|
+
kind: 'stacked',
|
|
268
|
+
text: fitCells(`${lead}${entry.keys}`, columns.listCells, glyphs.ellipsis),
|
|
269
|
+
entryId: entry.id,
|
|
270
|
+
tone,
|
|
271
|
+
});
|
|
272
|
+
push({
|
|
273
|
+
kind: 'keyword',
|
|
274
|
+
text: indent
|
|
275
|
+
+ fitCells(entry.keyword, Math.max(0, columns.available - SHORTCUT_PANEL_LIMITS.detailIndent), glyphs.ellipsis),
|
|
276
|
+
entryId: entry.id,
|
|
277
|
+
tone: 'muted',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
const keyCell = padCells(`${lead}${entry.keys}`, 1 + keyRoom);
|
|
282
|
+
const keyword = fitCells(entry.keyword, columns.keywordCells, glyphs.ellipsis);
|
|
283
|
+
push({
|
|
284
|
+
kind: 'entry',
|
|
285
|
+
text: `${keyCell}${' '.repeat(SHORTCUT_PANEL_LIMITS.keyGapCells)}${keyword}`,
|
|
286
|
+
entryId: entry.id,
|
|
287
|
+
tone,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (!selected)
|
|
291
|
+
return;
|
|
292
|
+
// The detail line belongs to the selected row and is the reason the window is
|
|
293
|
+
// derived from the cursor rather than from an independent offset (P1-7).
|
|
294
|
+
const detailRoom = Math.max(0, columns.available - SHORTCUT_PANEL_LIMITS.detailIndent);
|
|
295
|
+
// THE UNAVAILABILITY SENTENCE COMES FIRST, not last: the detail line is capped at
|
|
296
|
+
// two rows, and a reason appended after a long explanation is the first thing the
|
|
297
|
+
// cap removes - leaving a dim row with no explanation, which is the one outcome
|
|
298
|
+
// this line exists to prevent.
|
|
299
|
+
const detail = usable
|
|
300
|
+
? entry.detail
|
|
301
|
+
: `Unavailable now - ${input.unavailableReason ?? 'not in this mode'}. ${entry.detail}`;
|
|
302
|
+
for (const line of wrapped(detail, detailRoom, SHORTCUT_PANEL_LIMITS.detailMaxRows)) {
|
|
303
|
+
push({ kind: 'detail', text: `${indent}${line}`, entryId: entry.id, tone: 'dim' });
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
// --- the window ----------------------------------------------------------
|
|
307
|
+
const height = Math.max(1, Math.floor(Number.isFinite(input.rows) ? input.rows : 1));
|
|
308
|
+
// THE SMALLEST WINDOW CONTAINING THE CURSOR: the cursor sits on the last visible
|
|
309
|
+
// row while scrolling down and the window never starts past it.
|
|
310
|
+
const desired = Math.max(0, cursorRow - height + 1);
|
|
311
|
+
const window = sliceWindow(rows.length, desired, height);
|
|
312
|
+
return {
|
|
313
|
+
rows,
|
|
314
|
+
window,
|
|
315
|
+
cursorRow,
|
|
316
|
+
total: rows.length,
|
|
317
|
+
hint: shortcutFooter(input.cols, glyphs, {
|
|
318
|
+
canRefresh: input.task.enabled && !input.task.pending,
|
|
319
|
+
}),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
@@ -17,7 +17,11 @@ export function planStatusDetail(input) {
|
|
|
17
17
|
const budget = Math.max(0, Math.floor(status.columns) - 1);
|
|
18
18
|
const escape = escapeClause({ ...hints, escapeAction: status.escapeAction });
|
|
19
19
|
const reserved = [...(escape ? [statusField('escape', escape, 'warning')] : []),
|
|
20
|
-
|
|
20
|
+
// THE TOGGLE HINT NAMES A TWO-KEY PATH NOW (tui-shortcut-drawer-and-task-summary
|
|
21
|
+
// D-6). This row said `^G less` because `Ctrl+G` collapsed it directly; the key
|
|
22
|
+
// opens the drawer instead, and `d` inside the drawer is what collapses this row.
|
|
23
|
+
// Saying `^G less` would be a hint that does nothing.
|
|
24
|
+
statusField('toggle', hints.overlay ? 'Details on' : '^G then d')];
|
|
21
25
|
const fields = [];
|
|
22
26
|
const add = (candidate) => {
|
|
23
27
|
if (statusLinePlan([...fields, candidate, ...reserved]).cells <= budget)
|
|
@@ -31,6 +31,16 @@ export interface StatusLayoutInput {
|
|
|
31
31
|
spinner?: string;
|
|
32
32
|
waitingForConfirmation?: boolean;
|
|
33
33
|
stopping?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Auto-compaction paused itself after repeated no-progress attempts
|
|
36
|
+
* (context-compaction-never-fires C).
|
|
37
|
+
*
|
|
38
|
+
* A SEPARATE FIELD FROM `compacting`, and it renders as a SUFFIX on the context
|
|
39
|
+
* gauge rather than as a phase word: the fact belongs next to the percentage it
|
|
40
|
+
* explains ("Context 95% auto-paused"), and the phase slot must keep answering
|
|
41
|
+
* "is the agent running".
|
|
42
|
+
*/
|
|
43
|
+
compactionPaused?: boolean;
|
|
34
44
|
compacting?: boolean;
|
|
35
45
|
retryActive?: {
|
|
36
46
|
attempt: number;
|
|
@@ -53,6 +53,22 @@ function contextPercent(context) {
|
|
|
53
53
|
const mark = context.source === 'estimate' || context.deltaTokens > 0 ? '~' : '';
|
|
54
54
|
return mark + (context.pct > 999 ? '>999' : Math.round(context.pct)) + '%';
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The words that ride on the gauge when auto-compaction has given up (C).
|
|
58
|
+
*
|
|
59
|
+
* IT RIDES ON THE GAUGE RATHER THAN OCCUPYING ITS OWN FIELD. "Auto-paused" is the
|
|
60
|
+
* only fact that explains a stuck percentage, so the two belong in one field for
|
|
61
|
+
* the same reason `/context` prints them on adjacent lines. A second field would
|
|
62
|
+
* also be the first thing the width ladder drops, and this is precisely the state
|
|
63
|
+
* that must survive a narrow terminal.
|
|
64
|
+
*
|
|
65
|
+
* `paused` at the tight rungs: the full phrase costs twelve cells the row does
|
|
66
|
+
* not have at rung 3 and beyond, and dropping the fact entirely would be worse
|
|
67
|
+
* than abbreviating it.
|
|
68
|
+
*/
|
|
69
|
+
function pausedSuffix(full) {
|
|
70
|
+
return full ? ' auto-paused' : ' paused';
|
|
71
|
+
}
|
|
56
72
|
export function shortDuration(ms) {
|
|
57
73
|
if (!Number.isFinite(ms) || ms <= 0)
|
|
58
74
|
return '0s';
|
|
@@ -208,7 +224,8 @@ export function planPrimaryStatusFields(input) {
|
|
|
208
224
|
statusField('phase', glyph + (rung >= 5 ? '' : ' ') + label, phaseTone),
|
|
209
225
|
...(notice ? [statusField('feedback', notice, feedbackTone)] : []),
|
|
210
226
|
...(modeText ? [statusField('mode', modeText, 'accent')] : []),
|
|
211
|
-
statusField('context', (full ? 'Context ' : 'C') + pct
|
|
227
|
+
statusField('context', (full ? 'Context ' : 'C') + pct
|
|
228
|
+
+ (input.compactionPaused === true ? pausedSuffix(full) : '')),
|
|
212
229
|
statusField('thinking', full ? 'Think ' + (thinkingCodes[input.thinkingLevel] ? input.thinkingLevel : '?')
|
|
213
230
|
: (rung >= 2 ? '' : 'Th:') + code),
|
|
214
231
|
statusField('speed', speed + (full ? ' tok/s' : rung >= 3 ? '/s' : 't/s')),
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The task-summary cell's decisions (tui-shortcut-drawer-and-task-summary §6.3).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/ui/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS IS A SEPARATE MODULE FROM `header-left.ts`: the planner answers "how
|
|
7
|
+
* wide is the cell", this one answers "should there be a cell at all, and what is
|
|
8
|
+
* in it". The second question is the trigger rule (D-7) and it has to be testable
|
|
9
|
+
* without a renderer - `task-summary-app.test.tsx` pins "one call per session" by
|
|
10
|
+
* calling THIS function, not by counting renders.
|
|
11
|
+
*
|
|
12
|
+
* THE TRIGGER IS NARROW ON PURPOSE. A header label is an identity strip, not a
|
|
13
|
+
* live subtitle: refreshing it every turn would flicker, cost money, and teach the
|
|
14
|
+
* user to distrust the one thing on the row that is not a status field.
|
|
15
|
+
*/
|
|
16
|
+
/** Why no summary call was started. Every branch is a branch the tests can name. */
|
|
17
|
+
export type TaskSummarySkipReason = 'already-set' | 'no-anchor' | 'not-user-initiated' | 'disabled' | 'pending';
|
|
18
|
+
/**
|
|
19
|
+
* D-7's trigger rule.
|
|
20
|
+
*
|
|
21
|
+
* FIVE INPUTS, ONE ANSWER. `userInitiated` is the second guard on the same fact
|
|
22
|
+
* `submitMessage` already knows (`opts.userInitiated !== false`): an automatic
|
|
23
|
+
* continuation is not a new task, and paying for a label for it would spend the
|
|
24
|
+
* user's money to describe work they did not start.
|
|
25
|
+
*/
|
|
26
|
+
export declare function shouldSummarize(input: {
|
|
27
|
+
anchor: string;
|
|
28
|
+
hasSummary: boolean;
|
|
29
|
+
pending: boolean;
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
userInitiated: boolean;
|
|
32
|
+
}): {
|
|
33
|
+
start: true;
|
|
34
|
+
} | {
|
|
35
|
+
start: false;
|
|
36
|
+
reason: TaskSummarySkipReason;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The drawer's `Task` section, flattened from the view state so the pure layout
|
|
40
|
+
* module never learns what a `ViewState` is.
|
|
41
|
+
*/
|
|
42
|
+
export declare function taskSectionFrom(input: {
|
|
43
|
+
view: {
|
|
44
|
+
text: string;
|
|
45
|
+
tone: 'model' | 'fallback';
|
|
46
|
+
reason: string | null;
|
|
47
|
+
by: string;
|
|
48
|
+
at: number;
|
|
49
|
+
} | null;
|
|
50
|
+
pending: boolean;
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
now: number;
|
|
53
|
+
}): {
|
|
54
|
+
text: string | null;
|
|
55
|
+
tone: 'model' | 'fallback' | null;
|
|
56
|
+
reason: string | null;
|
|
57
|
+
by: string;
|
|
58
|
+
ageMs: number | null;
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
pending: boolean;
|
|
61
|
+
};
|