@aragon-agent/cli 0.6.38 → 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/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/shortcuts.d.ts +81 -0
- package/dist/ui/shortcuts.js +221 -0
- package/package.json +2 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-call approval gate: one wrapper, one shared denial text, no I/O.
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/tools/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* WHY A WRAPPER AND NOT A CORE HOOK (headless-host-bridge section 2.3.3). A
|
|
7
|
+
* `beforeExecute` callback on Core's `ToolExecutor` would put a human wait in
|
|
8
|
+
* front of the plan gate's refusal, freeze a new field into the public API that
|
|
9
|
+
* `public-api.test.ts` guards, and teach the engine about a host shape it has no
|
|
10
|
+
* business knowing. The `{...tool, execute}` shape below is the one already used
|
|
11
|
+
* three times in `tools/index.ts` (confirmation, skill ceiling, plan gate), so
|
|
12
|
+
* nothing in the engine has to learn that an approval exists.
|
|
13
|
+
*
|
|
14
|
+
* WHERE IT SITS IN THE STACK IS PART OF THE DESIGN, not an accident of import
|
|
15
|
+
* order: `tools/index.ts` applies it AFTER every append (so `skill_create` is
|
|
16
|
+
* reachable) and BEFORE the `toolPolicy` / plan-gate wrappers are added, so at
|
|
17
|
+
* call time those two refusals - which need no I/O - happen first and a tool that
|
|
18
|
+
* policy would refuse never interrupts a human.
|
|
19
|
+
*/
|
|
20
|
+
import { errorResult } from '@aragon-agent/core';
|
|
21
|
+
/**
|
|
22
|
+
* THE ONE PLACE A DENIAL IS WORDED. Shared by the wrapper below and by the tests,
|
|
23
|
+
* so "the model is told what happened and what to do instead" is a single string
|
|
24
|
+
* rather than a paragraph repeated in two modules.
|
|
25
|
+
*
|
|
26
|
+
* The last sentence is not politeness. A bare "denied" makes a model retry the
|
|
27
|
+
* same call; the observed failure mode is a paid loop that burns a budget on one
|
|
28
|
+
* forbidden command. Naming the tool and the reason is what lets it route around
|
|
29
|
+
* the refusal instead.
|
|
30
|
+
*/
|
|
31
|
+
export function denialMessage(tool, reason) {
|
|
32
|
+
const why = reason && reason.trim().length > 0 ? ` Reason: ${reason.trim()}` : '';
|
|
33
|
+
return (`The caller denied this tool call (${tool}).${why} ` +
|
|
34
|
+
'Do not retry the same call. Explain what you were trying to do, or take a different approach.');
|
|
35
|
+
}
|
|
36
|
+
/** The text a timed-out decision produces. Exported for the same reason. */
|
|
37
|
+
export function timeoutMessage(tool, timeoutMs) {
|
|
38
|
+
return (`The caller did not answer within ${timeoutMs} ms. The tool was not run. ` +
|
|
39
|
+
`Do not retry ${tool} without asking; explain what you needed it for instead.`);
|
|
40
|
+
}
|
|
41
|
+
/** The text an aborted turn produces, in the middle of a decision wait. */
|
|
42
|
+
export function interruptedMessage(tool) {
|
|
43
|
+
return `The turn was interrupted before the caller answered, so ${tool} was not run.`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Wrap a tool behind the gate. Same `{...tool, execute}` shape as every other
|
|
47
|
+
* wrapper in this package, and the same `errorResult` discipline: a refusal is a
|
|
48
|
+
* TOOL RESULT, never a throw, because the engine pushes whatever this returns
|
|
49
|
+
* into history - and a `tool_use` with no `tool_result` is an HTTP 400 on the next
|
|
50
|
+
* request.
|
|
51
|
+
*/
|
|
52
|
+
export function withToolApproval(tool, gate, options = {}) {
|
|
53
|
+
const original = tool.execute;
|
|
54
|
+
return {
|
|
55
|
+
...tool,
|
|
56
|
+
async execute(id, params, ctx) {
|
|
57
|
+
const request = {
|
|
58
|
+
tool: tool.name,
|
|
59
|
+
callId: id,
|
|
60
|
+
input: (params ?? {}),
|
|
61
|
+
summary: summarize(tool.name, (params ?? {})),
|
|
62
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
63
|
+
};
|
|
64
|
+
const ask = () => gate.decide(request);
|
|
65
|
+
const verdict = options.withHumanWait ? await options.withHumanWait(ask) : await ask();
|
|
66
|
+
if (verdict.allow)
|
|
67
|
+
return original(id, params, ctx);
|
|
68
|
+
return errorResult(verdict.message ?? denialMessage(tool.name, verdict.reason));
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A local copy of the one-line summary, for the same reason `tools/index.ts` keeps
|
|
74
|
+
* its own: this file must stay importable by a test without dragging in the whole
|
|
75
|
+
* builtin tool set, and the two call sites describe the same three tools.
|
|
76
|
+
*/
|
|
77
|
+
function summarize(toolName, params) {
|
|
78
|
+
if (toolName === 'bash')
|
|
79
|
+
return `Run: ${String(params.command ?? '')}`;
|
|
80
|
+
if (toolName === 'write_file')
|
|
81
|
+
return `Write: ${String(params.path ?? '')}`;
|
|
82
|
+
if (toolName === 'edit_file')
|
|
83
|
+
return `Edit: ${String(params.path ?? '')}`;
|
|
84
|
+
if (toolName === 'skill_create')
|
|
85
|
+
return `Create skill: ${String(params.name ?? '')}`;
|
|
86
|
+
return toolName;
|
|
87
|
+
}
|
package/dist/ui/App.js
CHANGED
|
@@ -87,6 +87,12 @@ import { createClipboardCoordinator } from './clipboard-task.js';
|
|
|
87
87
|
import { QueuePanel } from './QueuePanel.js';
|
|
88
88
|
import { QueueOverlay } from './overlays/QueueOverlay.js';
|
|
89
89
|
import { AgentsBoardOverlay } from './overlays/AgentsBoardOverlay.js';
|
|
90
|
+
// THE KEYBOARD DRAWER AND THE HEADER LABEL (tui-shortcut-drawer-and-task-summary).
|
|
91
|
+
// Both are thin here on purpose: the drawer's geometry lives in
|
|
92
|
+
// `layout/shortcut-panel-layout.ts` and the trigger rule in
|
|
93
|
+
// `layout/task-summary-cell.ts`, so this file only routes state into them.
|
|
94
|
+
import { ShortcutPanel } from './overlays/ShortcutPanel.js';
|
|
95
|
+
import { shouldSummarize, taskSectionFrom } from './layout/task-summary-cell.js';
|
|
90
96
|
import { buildQueueLayout } from './layout/queue-layout.js';
|
|
91
97
|
import { interactionCopy } from './interaction-copy.js';
|
|
92
98
|
import { createRunMetrics, updateRunMetrics, freezeRunMetrics, projectRunMetrics } from './run-metrics.js';
|
|
@@ -170,6 +176,36 @@ function applyPromptMutation(action) {
|
|
|
170
176
|
return movePrompt(action.id, action.delta);
|
|
171
177
|
return setPromptFavorite(action.id, action.favorite);
|
|
172
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* The first USER message in a transcript, or `''`.
|
|
181
|
+
*
|
|
182
|
+
* Used by the `/resume` rebuild, which must describe the conversation it is
|
|
183
|
+
* ADOPTING rather than the one on screen (D-13). A restored session can open with a
|
|
184
|
+
* tool card or a settled system notice, so "entries[0]" is not the answer.
|
|
185
|
+
*/
|
|
186
|
+
function firstUserText(entries) {
|
|
187
|
+
const entry = entries.find((candidate) => candidate.kind === 'user');
|
|
188
|
+
return entry && entry.kind === 'user' ? entry.text : '';
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The overlays that hold an unresolved promise to the agent (review fix RF-1).
|
|
192
|
+
*
|
|
193
|
+
* NOT PANELS: `confirm` is a tool waiting for approval, `question` and `plan` are
|
|
194
|
+
* human-input requests, and each one is settled ONLY by the Escape branch of the
|
|
195
|
+
* global key handler. An overlay that can be dismissed without settling it strands
|
|
196
|
+
* the run, so `Ctrl+G` refuses to take the keyboard from these three - the same
|
|
197
|
+
* shape of refusal as `tooSmall` - and the header keeps naming `Esc close`.
|
|
198
|
+
*/
|
|
199
|
+
const DECISION_OVERLAYS = new Set(['confirm', 'question', 'plan']);
|
|
200
|
+
/** The LAST user message in a transcript, or `''` — the anchor `/task refresh` uses. */
|
|
201
|
+
function lastUserText(entries) {
|
|
202
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
203
|
+
const entry = entries[index];
|
|
204
|
+
if (entry && entry.kind === 'user')
|
|
205
|
+
return entry.text;
|
|
206
|
+
}
|
|
207
|
+
return '';
|
|
208
|
+
}
|
|
173
209
|
export function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, scrollMode = 'app', scrollback, initialEntries, session, sessionBridge, sessionNotice, }) {
|
|
174
210
|
const native = scrollMode === 'native' && scrollback !== undefined;
|
|
175
211
|
/** Set when the native writer swallowed repeated tall frames; cleared by a resize. */
|
|
@@ -201,6 +237,14 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
201
237
|
const [draftClearedByCtrlC, setDraftClearedByCtrlC] = useState(false);
|
|
202
238
|
const [popupRows, setPopupRows] = useState(0);
|
|
203
239
|
const [statusExpanded, setStatusExpanded] = useState(false);
|
|
240
|
+
/**
|
|
241
|
+
* The `/keys <query>` pre-filter for the drawer.
|
|
242
|
+
*
|
|
243
|
+
* STATE RATHER THAN A PROP OF THE COMMAND, because the command and the panel are
|
|
244
|
+
* two moments: `/keys git` sets the filter, `Ctrl+G` opens with whatever was set
|
|
245
|
+
* last, and the panel itself never types (F-2: a text field would fight `Esc`).
|
|
246
|
+
*/
|
|
247
|
+
const [shortcutQuery, setShortcutQuery] = useState('');
|
|
204
248
|
const dimensions = buildFrameBudget({ ...size, draftRows, popupRows, statusExpanded });
|
|
205
249
|
const { rows, cols } = dimensions;
|
|
206
250
|
/**
|
|
@@ -304,6 +348,17 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
304
348
|
const reducedMotion = cfg.reducedMotion ?? false;
|
|
305
349
|
const stateRef = useRef(state);
|
|
306
350
|
stateRef.current = state;
|
|
351
|
+
/**
|
|
352
|
+
* The task label's two guards (tui-shortcut-drawer-and-task-summary D-10).
|
|
353
|
+
*
|
|
354
|
+
* `taskSummaryEpoch` IS THE CORRECTNESS GUARD and the `AbortController` is the
|
|
355
|
+
* economy one: the epoch decides whether a late result may still be adopted, the
|
|
356
|
+
* abort releases the connection the moment its answer stopped mattering. Neither
|
|
357
|
+
* substitutes for the other, so both are bumped/cancelled on every new-task
|
|
358
|
+
* boundary - `/clear`, `/resume`, and a new first submit.
|
|
359
|
+
*/
|
|
360
|
+
const taskSummaryEpoch = useRef(0);
|
|
361
|
+
const taskSummaryAbort = useRef(null);
|
|
307
362
|
const startedAt = useRef(Date.now());
|
|
308
363
|
const confirmRef = useRef(null);
|
|
309
364
|
confirmRef.current = confirmState;
|
|
@@ -1628,6 +1683,74 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1628
1683
|
}
|
|
1629
1684
|
return applied;
|
|
1630
1685
|
};
|
|
1686
|
+
/**
|
|
1687
|
+
* Cancel any in-flight label call and make its result unadoptable.
|
|
1688
|
+
*
|
|
1689
|
+
* ONE FUNCTION FOR BOTH BOUNDARIES: the epoch is incremented BEFORE the abort so a
|
|
1690
|
+
* resolution that is already sitting in the microtask queue cannot slip through
|
|
1691
|
+
* between the two statements.
|
|
1692
|
+
*/
|
|
1693
|
+
const cancelTaskSummary = () => {
|
|
1694
|
+
taskSummaryEpoch.current += 1;
|
|
1695
|
+
taskSummaryAbort.current?.abort();
|
|
1696
|
+
taskSummaryAbort.current = null;
|
|
1697
|
+
};
|
|
1698
|
+
/**
|
|
1699
|
+
* Start one label call, if the trigger rule allows it (D-7).
|
|
1700
|
+
*
|
|
1701
|
+
* FIRE AND FORGET, AND NEVER AWAITED: the caller (`submitMessage`) starts
|
|
1702
|
+
* `controller.prompt()` immediately after this returns, so the summary can never
|
|
1703
|
+
* delay the first token (AC-13). `summarizeTask` never rejects, so there is no
|
|
1704
|
+
* `.catch` here and no unhandled rejection can reach a TUI with nowhere to show it.
|
|
1705
|
+
*/
|
|
1706
|
+
const ensureTaskSummary = (anchor, opts = {}) => {
|
|
1707
|
+
const decision = shouldSummarize({
|
|
1708
|
+
anchor,
|
|
1709
|
+
// `force` IS THE `/resume` REBUILD. The reducer is about to replace the label
|
|
1710
|
+
// with `null`, so reading the CURRENT state here would see the old
|
|
1711
|
+
// conversation's label still in place and skip the rebuild - a race the
|
|
1712
|
+
// wrapper cannot win by ordering alone, because React applies the action on
|
|
1713
|
+
// the next render. An explicit instruction outranks the automatic rule; the
|
|
1714
|
+
// `enabled` and anchor checks still apply.
|
|
1715
|
+
hasSummary: opts.force === true ? false : stateRef.current.taskSummary !== null,
|
|
1716
|
+
pending: opts.force === true ? false : stateRef.current.taskSummaryPending,
|
|
1717
|
+
enabled: controller.getConfig().taskSummary?.enabled !== false,
|
|
1718
|
+
userInitiated: opts.userInitiated !== false,
|
|
1719
|
+
});
|
|
1720
|
+
if (!decision.start)
|
|
1721
|
+
return;
|
|
1722
|
+
taskSummaryEpoch.current += 1;
|
|
1723
|
+
const epoch = taskSummaryEpoch.current;
|
|
1724
|
+
taskSummaryAbort.current?.abort();
|
|
1725
|
+
const abort = new AbortController();
|
|
1726
|
+
taskSummaryAbort.current = abort;
|
|
1727
|
+
dispatch({ type: 'taskSummaryPending' });
|
|
1728
|
+
// `?.` FOR THE SAME REASON `isCompactionBusy?.()` AND `isFastRegistered?.()` USE IT:
|
|
1729
|
+
// a host that supplies a partial controller (every App test double, and any
|
|
1730
|
+
// embedding that never wanted this feature) must not crash on a header
|
|
1731
|
+
// decoration. A missing method means "no label machinery here", which is exactly
|
|
1732
|
+
// `view: null`.
|
|
1733
|
+
const call = controller.summarizeTask?.(anchor, {
|
|
1734
|
+
signal: abort.signal,
|
|
1735
|
+
ellipsis: glyphs.ellipsis,
|
|
1736
|
+
});
|
|
1737
|
+
if (!call) {
|
|
1738
|
+
// Nothing was started, so the latch must come back down or every later task
|
|
1739
|
+
// would be skipped as `pending`.
|
|
1740
|
+
dispatch({ type: 'taskSummaryReady', view: null });
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
void call.then((outcome) => {
|
|
1744
|
+
if (!mounted.current)
|
|
1745
|
+
return;
|
|
1746
|
+
// THE EPOCH CHECK IS THE ADOPTION RULE, not a nicety: `/clear` while a call was
|
|
1747
|
+
// in flight would otherwise paint the previous task's label onto an empty
|
|
1748
|
+
// session, which is the one thing a label must never do.
|
|
1749
|
+
if (epoch !== taskSummaryEpoch.current)
|
|
1750
|
+
return;
|
|
1751
|
+
dispatch({ type: 'taskSummaryReady', view: outcome.view });
|
|
1752
|
+
});
|
|
1753
|
+
};
|
|
1631
1754
|
const makeCtx = (args) => ({
|
|
1632
1755
|
args,
|
|
1633
1756
|
requestCopy: copyCoordinator.requestCopy,
|
|
@@ -1678,6 +1801,24 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1678
1801
|
dispatch({ type: 'abortMark' });
|
|
1679
1802
|
dispatch({ type: 'runEnd' });
|
|
1680
1803
|
}
|
|
1804
|
+
// A NEW-TASK BOUNDARY INVALIDATES THE LABEL IN FLIGHT (D-10 / D-13). The
|
|
1805
|
+
// reducer clears the stored view in both branches; this cancels the call
|
|
1806
|
+
// whose answer belongs to the conversation that just ended, and it runs
|
|
1807
|
+
// BEFORE `dispatch(action)` so no late result can be adopted in between.
|
|
1808
|
+
cancelTaskSummary();
|
|
1809
|
+
if (action.type === 'restoreEntries') {
|
|
1810
|
+
// `/resume` REBUILDS ONE LABEL FROM THE ENTRIES IT JUST ADOPTED.
|
|
1811
|
+
//
|
|
1812
|
+
// FROM `action.entries`, NOT `state.entries`: this wrapper runs before the
|
|
1813
|
+
// reducer applies the action, so the state snapshot is still the
|
|
1814
|
+
// conversation being replaced. Reading the reducer's input is also the
|
|
1815
|
+
// only way this works for the `/resume` path at all - and the App's
|
|
1816
|
+
// initial seed (`seedViewState`) calls `viewReducer` DIRECTLY, so
|
|
1817
|
+
// launching with `--resume` costs nothing (AC-26).
|
|
1818
|
+
const anchor = firstUserText(action.entries);
|
|
1819
|
+
if (anchor !== '')
|
|
1820
|
+
ensureTaskSummary(anchor, { userInitiated: true, force: true });
|
|
1821
|
+
}
|
|
1681
1822
|
}
|
|
1682
1823
|
dispatch(action);
|
|
1683
1824
|
},
|
|
@@ -1692,6 +1833,27 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1692
1833
|
resolve: (ref) => resolvePromptRef(loadPromptLibrary(), ref),
|
|
1693
1834
|
},
|
|
1694
1835
|
setOverlay: (overlay) => dispatch({ type: 'setOverlay', overlay }),
|
|
1836
|
+
// THE TASK LABEL PORT (`/keys`, `/task`; §6.4). Reads go through `stateRef` so a
|
|
1837
|
+
// command always sees the LATEST label rather than the one from the render that
|
|
1838
|
+
// built this context - the same rule `transcriptEntries` above records.
|
|
1839
|
+
taskSummary: {
|
|
1840
|
+
openPanel: (query) => {
|
|
1841
|
+
setShortcutQuery(query ?? '');
|
|
1842
|
+
dispatch({ type: 'setOverlay', overlay: 'shortcuts' });
|
|
1843
|
+
},
|
|
1844
|
+
read: () => ({
|
|
1845
|
+
view: stateRef.current.taskSummary,
|
|
1846
|
+
pending: stateRef.current.taskSummaryPending,
|
|
1847
|
+
enabled: controller.getConfig().taskSummary?.enabled !== false,
|
|
1848
|
+
}),
|
|
1849
|
+
refresh: () => {
|
|
1850
|
+
const anchor = lastUserText(stateRef.current.entries);
|
|
1851
|
+
if (anchor === '')
|
|
1852
|
+
return '';
|
|
1853
|
+
ensureTaskSummary(anchor, { force: true, userInitiated: true });
|
|
1854
|
+
return anchor;
|
|
1855
|
+
},
|
|
1856
|
+
},
|
|
1695
1857
|
notify,
|
|
1696
1858
|
toast,
|
|
1697
1859
|
persistConfig,
|
|
@@ -1805,6 +1967,15 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1805
1967
|
return { accepted: true };
|
|
1806
1968
|
}
|
|
1807
1969
|
// Fire-and-forget: events drive the UI. prompt() never rejects.
|
|
1970
|
+
//
|
|
1971
|
+
// THE LABEL HOOK SITS HERE AND NOWHERE ELSE (P1-1): after `preflight()` said
|
|
1972
|
+
// yes, before `prompt()` is started. Above this line the two `return`s are a
|
|
1973
|
+
// restart notice and a run already starting; between `dispatch({type:'submit'})`
|
|
1974
|
+
// and `preflight()` is the path where a rejected submission would pay for a
|
|
1975
|
+
// model call that describes a turn nobody ran; and below, in the queueing
|
|
1976
|
+
// branch, the message is steering rather than a new task.
|
|
1977
|
+
if (opts.userInitiated !== false)
|
|
1978
|
+
ensureTaskSummary(message);
|
|
1808
1979
|
void controller.prompt(message, { todoPolicy: opts.todoPolicy ?? 'new-task' }).then((outcome) => {
|
|
1809
1980
|
if (!mounted.current || sequence !== submissionSequence.current)
|
|
1810
1981
|
return;
|
|
@@ -2170,10 +2341,36 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2170
2341
|
}, 1500);
|
|
2171
2342
|
return;
|
|
2172
2343
|
}
|
|
2344
|
+
// Ctrl+G: THE KEYBOARD DRAWER (tui-shortcut-drawer-and-task-summary D-1).
|
|
2345
|
+
//
|
|
2346
|
+
// THREE STATES, NOT ONE GUARD (P2-3). It used to require "no overlay open" and
|
|
2347
|
+
// then flipped the status detail row; both halves are gone:
|
|
2348
|
+
// - `shortcuts` open -> close it (the `Ctrl+B` precedent);
|
|
2349
|
+
// - nothing open -> open it;
|
|
2350
|
+
// - some OTHER overlay -> TAKE OVER. Swallowing the key would leave the user
|
|
2351
|
+
// pressing a key whose only on-screen hint is `Esc close` and getting
|
|
2352
|
+
// nothing, and what they asked for was this panel.
|
|
2353
|
+
//
|
|
2354
|
+
// THE DECISION OVERLAYS ARE THE ONE EXCEPTION, AND IT IS NOT A NICETY (review fix
|
|
2355
|
+
// RF-1). `confirm`, `question` and `plan` are not panels: each holds an
|
|
2356
|
+
// unresolved promise to the agent - a tool waiting for approval, a question
|
|
2357
|
+
// waiting for an answer - and the Escape branch further down is the ONLY path
|
|
2358
|
+
// that settles one (`cancelConfirmation` / `cancelPendingHuman`). Taking the
|
|
2359
|
+
// keyboard away from such a dialog would hide it and leave that promise pending
|
|
2360
|
+
// for ever: a run that never resumes, and a screen that cannot say why. A
|
|
2361
|
+
// read-only reference panel is not worth stranding a run, so the key stays inert
|
|
2362
|
+
// here and the header keeps naming the key that does work.
|
|
2363
|
+
//
|
|
2364
|
+
// THE DETAIL-ROW TOGGLE MOVED INSIDE THE DRAWER (`d`, D-6): `HelpOverlay` and
|
|
2365
|
+
// `CHANGELOG` both name the new home, and the panel's title line carries the
|
|
2366
|
+
// `details on|off` state.
|
|
2173
2367
|
if (key.ctrl && input.toLowerCase() === 'g') {
|
|
2174
|
-
if (tooSmall
|
|
2368
|
+
if (tooSmall)
|
|
2369
|
+
return;
|
|
2370
|
+
const open = stateRef.current.overlay;
|
|
2371
|
+
if (DECISION_OVERLAYS.has(open))
|
|
2175
2372
|
return;
|
|
2176
|
-
|
|
2373
|
+
dispatch({ type: 'setOverlay', overlay: open === 'shortcuts' ? null : 'shortcuts' });
|
|
2177
2374
|
return;
|
|
2178
2375
|
}
|
|
2179
2376
|
// Ctrl+B: the Agents board (tui-subagent-panel D-6 / §6.5).
|
|
@@ -2652,14 +2849,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2652
2849
|
// ALWAYS CALLED, INERT WHEN `enabled` IS FALSE: hooks cannot be conditional, and
|
|
2653
2850
|
// an inert hook is what keeps `scrollMode: 'app'` byte-identical (I-S9).
|
|
2654
2851
|
const viewCols = native ? Math.max(1, cols - 1) : cols;
|
|
2655
|
-
const header = (_jsx(Header, { columns: viewCols,
|
|
2656
|
-
//
|
|
2657
|
-
// `
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
// still works (AC-3).
|
|
2662
|
-
promptsHint: cfg.prompts?.hint !== false }));
|
|
2852
|
+
const header = (_jsx(Header, { columns: viewCols,
|
|
2853
|
+
// DERIVED IN EXACTLY ONE PLACE (0-3 (d)): both overlay slots render
|
|
2854
|
+
// `overlayNode`, and both read this same three-state value from here.
|
|
2855
|
+
panel: overlay === null ? 'none' : overlay === 'shortcuts' ? 'shortcuts' : 'other', scrolledLines: scrolledLines, taskSummary: state.taskSummary === null
|
|
2856
|
+
? null
|
|
2857
|
+
: { text: state.taskSummary.text, tone: state.taskSummary.tone }, summaryEnabled: cfg.taskSummary?.enabled !== false, cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: pickHeaderVariant(cols), theme: theme, caps: caps }));
|
|
2663
2858
|
// The header and the opener are SLOTS OF THE SHELL, not printed bytes (D3/D4): at
|
|
2664
2859
|
// `H = rows - 1` a printed row is pushed off the screen by the very next frame, so
|
|
2665
2860
|
// `printBanner()` was a way of making the banner invisible. Neither is passed here.
|
|
@@ -2710,7 +2905,25 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2710
2905
|
const openerCols = nativeBudget ? nativeBudget.previewCols : contentCols;
|
|
2711
2906
|
const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(openerRows, openerCols), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: openerRows, theme: theme, caps: caps })) : null;
|
|
2712
2907
|
const overlayMaxRows = nativeBudget ? nativeBudget.overlayRows : viewportBudget;
|
|
2713
|
-
const overlayNode =
|
|
2908
|
+
const overlayNode =
|
|
2909
|
+
// THE DRAWER (tui-shortcut-drawer-and-task-summary D-4): it rides the SAME
|
|
2910
|
+
// overlay slot as `/help` and `/agents`, so it needs no new frame shape, no
|
|
2911
|
+
// second scroll authority and no geometry change in `ScrollViewport`.
|
|
2912
|
+
overlay === 'shortcuts' ? (_jsx(ShortcutPanel, { isActive: !tooSmall, cols: viewCols, maxRows: overlayMaxRows, theme: theme, caps: caps, native: native, running: running, query: shortcutQuery, task: taskSectionFrom({
|
|
2913
|
+
view: state.taskSummary,
|
|
2914
|
+
pending: state.taskSummaryPending,
|
|
2915
|
+
enabled: cfg.taskSummary?.enabled !== false,
|
|
2916
|
+
now: Date.now(),
|
|
2917
|
+
}), detailsExpanded: statusExpanded, onToggleDetails: () => setStatusExpanded((value) => !value), onRefresh: () => {
|
|
2918
|
+
// THE MANUAL OVERRIDE (D-7): the last thing the user asked for. An empty
|
|
2919
|
+
// conversation has nothing to describe, so it says so instead of calling.
|
|
2920
|
+
const anchor = lastUserText(state.entries);
|
|
2921
|
+
if (anchor === '') {
|
|
2922
|
+
toast('info', 'No prompt to summarise yet.');
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
ensureTaskSummary(anchor, { force: true, userInitiated: true });
|
|
2926
|
+
} })) : overlay === 'queue' ? (_jsx(QueueOverlay, { pending: state.pendingSteering, cols: viewCols, maxRows: overlayMaxRows, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'help' ? (_jsx(OverlayFrame, { title: "Help", hint: `Esc close ${glyphs.midDot} PgUp/PgDn scroll`, maxRows: overlayMaxRows, cols: viewCols, rows: helpRows(theme, caps, native), scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'model' ? (_jsx(ModelPicker, { isActive: !tooSmall, registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: viewCols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'kit' ? (_jsx(KitPicker, { isActive: !tooSmall, library: cfg.modelProfiles, config: cfg, ...(kitDiskConfig ? { diskConfig: kitDiskConfig } : {}), active: activeKit,
|
|
2714
2927
|
// ONLY THE APPLY IS BLOCKED while work is in flight (§4.7): a list a
|
|
2715
2928
|
// user cannot even OPEN reads as a broken key.
|
|
2716
2929
|
busy: controller.isModelSettingsBusy(), maxRows: overlayMaxRows, cols: viewCols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onChoose: handleKitChoose, onClose: () => dispatch({ type: 'setOverlay', overlay: null }) })) : overlay === 'prompts' ? (_jsx(PromptLibraryOverlay, { isActive: !tooSmall, library: promptLibrary, ...(promptIntent ? { intent: promptIntent } : {}), cols: viewCols, maxRows: overlayMaxRows, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onMutate: handlePromptMutate, onNotice: notify, onClose: () => dispatch({ type: 'setOverlay', overlay: null }) })) : overlay === 'prompt-pick' ? (_jsx(PromptPicker, { isActive: !tooSmall, library: promptLibrary, cols: viewCols, maxRows: overlayMaxRows, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onFill: handlePromptFill, onManage: (entry) => openPromptLibrary(entry), onClose: closePromptPicker })) : overlay === 'settings' ? (_jsx(SettingsScreen, { isActive: !tooSmall, initial: {
|
package/dist/ui/Header.d.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* One-row brand bar: word mark, identity, working directory, task summary and a
|
|
3
|
+
* fixed-width hint column (tui-shortcut-drawer-and-task-summary §4.2 / §4.3).
|
|
4
|
+
*
|
|
5
|
+
* THIS COMPONENT COMPUTES NO WIDTHS ANY MORE. Both halves come from pure
|
|
6
|
+
* planners - `planHeaderHints` for the right column and `planHeaderLeft` for the
|
|
7
|
+
* clause ladder - so the only arithmetic left here is "join what I was given".
|
|
8
|
+
* The old shape carried a `statusExpanded` flag, a kit switch and a prompts
|
|
9
|
+
* switch, all of which existed to choose between header clauses; the clauses are
|
|
10
|
+
* gone (the drawer lists the keys instead), so the props are gone with them.
|
|
11
|
+
*/
|
|
2
12
|
import React from 'react';
|
|
3
13
|
import type { Theme } from './theme.js';
|
|
4
14
|
import type { TermCapabilities } from './capabilities.js';
|
|
5
15
|
import { type HeaderVariant } from './Logo.js';
|
|
16
|
+
import { type HeaderPanelState } from './layout/header-hints.js';
|
|
6
17
|
interface HeaderProps {
|
|
7
18
|
cwd: string;
|
|
8
19
|
provider: string;
|
|
@@ -12,25 +23,26 @@ interface HeaderProps {
|
|
|
12
23
|
theme: Theme;
|
|
13
24
|
caps: TermCapabilities;
|
|
14
25
|
columns?: number;
|
|
15
|
-
statusExpanded?: boolean;
|
|
16
|
-
scrolledLines?: number;
|
|
17
|
-
overlayOpen?: boolean;
|
|
18
26
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* DEFAULTS TO TRUE because that is the requirement - `ctrl+G` must reveal the
|
|
22
|
-
* kit key - and because a default of `false` would leave the one caller that
|
|
23
|
-
* forgets the prop silently teaching nothing.
|
|
27
|
+
* What the right column says. Derived in ONE place - the line that renders
|
|
28
|
+
* `<Header>` - so the two overlay slots cannot disagree about it.
|
|
24
29
|
*/
|
|
25
|
-
|
|
30
|
+
panel?: HeaderPanelState;
|
|
31
|
+
scrolledLines?: number;
|
|
26
32
|
/**
|
|
27
|
-
*
|
|
33
|
+
* The session's task label, or `null` while there is none.
|
|
28
34
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* the
|
|
35
|
+
* `null` WHILE PENDING TOO: a placeholder here would spend eight columns of the
|
|
36
|
+
* row to draw an ellipsis and then move everything back when the label lands,
|
|
37
|
+
* so the first submit visibly jogged the header. The label now appears once,
|
|
38
|
+
* complete (P2-10).
|
|
32
39
|
*/
|
|
33
|
-
|
|
40
|
+
taskSummary?: {
|
|
41
|
+
text: string;
|
|
42
|
+
tone: 'model' | 'fallback';
|
|
43
|
+
} | null;
|
|
44
|
+
/** `config.taskSummary.enabled`; `false` removes the cell without removing the words. */
|
|
45
|
+
summaryEnabled?: boolean;
|
|
34
46
|
}
|
|
35
|
-
export declare function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns,
|
|
47
|
+
export declare function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns, panel, scrolledLines, taskSummary, summaryEnabled, }: HeaderProps): React.ReactElement;
|
|
36
48
|
export {};
|
package/dist/ui/Header.js
CHANGED
|
@@ -2,24 +2,26 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { Logo } from './Logo.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
import { pickGlyphs } from './glyphs.js';
|
|
6
|
+
import { planHeaderHints, HEADER_HINT_LIMITS, } from './layout/header-hints.js';
|
|
7
|
+
import { planHeaderLeft } from './layout/header-left.js';
|
|
8
|
+
export function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns = 80, panel = 'none', scrolledLines = 0, taskSummary = null, summaryEnabled = true, }) {
|
|
9
|
+
const glyphs = pickGlyphs(caps);
|
|
10
|
+
// THE RIGHT COLUMN FIRST: it is the only clause that may never be dropped, so
|
|
11
|
+
// the left half is planned against what is left after it (defect 12).
|
|
12
|
+
const plan = planHeaderHints({ columns, panel, scrolledLines });
|
|
13
|
+
const leftCells = Math.max(0, columns - plan.cells - HEADER_HINT_LIMITS.separatorCells);
|
|
14
|
+
const left = planHeaderLeft({
|
|
15
|
+
leftCells,
|
|
16
|
+
provider,
|
|
17
|
+
model,
|
|
18
|
+
cwd: basename(cwd) || cwd,
|
|
19
|
+
hasKey,
|
|
20
|
+
summary: taskSummary,
|
|
21
|
+
summaryEnabled,
|
|
22
|
+
ellipsis: glyphs.ellipsis,
|
|
18
23
|
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// `aragon...Details off` as one string. The blank column comes out of the left
|
|
23
|
-
// half's width, so the row still spans exactly `columns`.
|
|
24
|
-
return _jsxs(Box, { flexDirection: "row", height: 1, width: columns, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: Math.max(0, columns - plan.cells - HEADER_HINT_LIMITS.separatorCells), flexShrink: 0, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Logo, { variant: variant === 'mini' ? 'mini' : 'bar', theme: theme, caps: caps }), variant !== 'mini' && _jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.accent, children: [' ', provider, ":", model] }), _jsxs(Text, { color: theme.muted, children: [' ', basename(cwd) || cwd] }), _jsxs(Text, { color: hasKey ? theme.toolDone : theme.toolRunning, children: [' ', hasKey ? theme.symbols.keyOn : theme.symbols.keyOff] })] })] }) }), _jsx(Box, { width: HEADER_HINT_LIMITS.separatorCells, flexShrink: 0 }), _jsx(Box, { width: plan.cells, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { color: theme.muted, wrap: "truncate", children: plan.text }) })] });
|
|
24
|
+
return _jsxs(Box, { flexDirection: "row", height: 1, width: columns, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: leftCells, flexShrink: 0, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Logo, { variant: variant === 'mini' ? 'mini' : 'bar', theme: theme, caps: caps }), variant !== 'mini' && _jsxs(_Fragment, { children: [left.text.model && _jsxs(Text, { color: theme.accent, children: [' ', left.text.model] }), left.text.cwd && _jsxs(Text, { color: theme.muted, children: [' ', left.text.cwd] }), left.text.summary && _jsxs(Text, { color: theme.muted, children: [' ', glyphs.midDot, ' '] }), left.text.summary && (taskSummary?.tone === 'fallback'
|
|
25
|
+
? _jsx(Text, { color: theme.muted, dimColor: true, children: left.text.summary })
|
|
26
|
+
: _jsx(Text, { color: theme.primary, children: left.text.summary })), _jsxs(Text, { color: hasKey ? theme.toolDone : theme.toolRunning, children: [' ', hasKey ? theme.symbols.keyOn : theme.symbols.keyOff] })] })] }) }), _jsx(Box, { width: HEADER_HINT_LIMITS.separatorCells, flexShrink: 0 }), _jsx(Box, { width: plan.cells, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { color: theme.muted, wrap: "truncate", children: plan.text }) })] });
|
|
25
27
|
}
|
|
@@ -29,6 +29,12 @@ export function actionCandidates(input) {
|
|
|
29
29
|
if (input.overlay) {
|
|
30
30
|
if (input.overlay === 'confirm')
|
|
31
31
|
return [copy.approveTool, copy.rejectTool];
|
|
32
|
+
// THE DRAWER SELECTS, IT DOES NOT SCROLL (tui-shortcut-drawer-and-task-summary
|
|
33
|
+
// §5). It is a list with a cursor, so the generic `copy.scroll` fallback would
|
|
34
|
+
// describe the wrong interaction - and the drawer's own footer already names
|
|
35
|
+
// every key, so one clause is the whole line.
|
|
36
|
+
if (input.overlay === 'shortcuts')
|
|
37
|
+
return [copy.select];
|
|
32
38
|
const selects = ['question', 'model', 'session', 'settings'].includes(input.overlay);
|
|
33
39
|
return [selects ? copy.select : copy.scroll,
|
|
34
40
|
...(selects ? [input.overlay === 'settings' ? copy.saveSettings : copy.confirm] : [])];
|
|
@@ -10,6 +10,24 @@ interface OverlayFrameBase {
|
|
|
10
10
|
cols?: number;
|
|
11
11
|
theme: Theme;
|
|
12
12
|
caps: TermCapabilities;
|
|
13
|
+
/**
|
|
14
|
+
* Where the frame sits on the row when `width` is given.
|
|
15
|
+
*
|
|
16
|
+
* DEFAULT `'left'` AND NO WRAPPER BOX: with neither new prop set, the render
|
|
17
|
+
* tree is byte-identical to the pre-`shortcut-drawer` one, which is what keeps
|
|
18
|
+
* the eight existing overlays' layout assertions green (`R-16`).
|
|
19
|
+
*/
|
|
20
|
+
align?: 'left' | 'right';
|
|
21
|
+
/**
|
|
22
|
+
* An explicit width for a frame that is NOT the full terminal width - the
|
|
23
|
+
* shortcut drawer's right-hand bar.
|
|
24
|
+
*
|
|
25
|
+
* THE FRAME STILL DECIDES ITS OWN FORM from `cols` (`BORDERLESS_COLS`), so the
|
|
26
|
+
* caller passes the TERMINAL width here and the bar's width separately. Passing
|
|
27
|
+
* the bar's width as `cols` would put a 46-cell drawer on a 200-column terminal
|
|
28
|
+
* into the rail form for ever.
|
|
29
|
+
*/
|
|
30
|
+
width?: number;
|
|
13
31
|
}
|
|
14
32
|
interface OverlayFrameControlled extends OverlayFrameBase {
|
|
15
33
|
/** One element per row, each already `wrap="truncate"`. */
|
|
@@ -60,14 +60,29 @@ export function OverlayFrame(props) {
|
|
|
60
60
|
onScrollClamp?.(overflowOffset);
|
|
61
61
|
}, [overflowOffset, onScrollClamp]);
|
|
62
62
|
const inner = (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "row", height: 1, flexShrink: 0, children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { wrap: "truncate", color: theme.primary, bold: true, children: title }) }), position && _jsx(Box, { width: stringWidth(position), flexShrink: 0, children: _jsx(Text, { color: theme.muted, children: position }) })] }), _jsx(Box, { flexDirection: "column", flexShrink: 0, height: unbounded ? undefined : Math.max(1, maxRows - (compact ? 2 : bordered ? 5 : 3)), overflowY: "hidden", children: body }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hint })] }));
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// THE NEW PROPS ARE OPTIONAL AND THEIR DEFAULT IS TODAY'S BEHAVIOUR (R-16): the
|
|
64
|
+
// three branches below return exactly what they always returned unless the
|
|
65
|
+
// caller asked for a width or a right-hand placement. When it did, the SAME box
|
|
66
|
+
// is built and then placed inside one row-wide wrapper, which is the only way
|
|
67
|
+
// Ink can push a fixed-width child to the right edge (a `marginLeft: 'auto'`
|
|
68
|
+
// does not exist here).
|
|
69
|
+
const width = props.width;
|
|
70
|
+
const align = props.align ?? 'left';
|
|
71
|
+
const placed = (node) => {
|
|
72
|
+
if (width === undefined && align === 'left')
|
|
73
|
+
return node;
|
|
74
|
+
return (_jsx(Box, { width: cols, flexDirection: "row", flexShrink: 0, justifyContent: align === 'right' ? 'flex-end' : 'flex-start', children: node }));
|
|
75
|
+
};
|
|
76
|
+
const boxWidth = width === undefined ? {} : { width };
|
|
77
|
+
if (compact) {
|
|
78
|
+
return placed(_jsx(Box, { ...boxWidth, width: width ?? cols, height: Math.max(0, maxRows), flexDirection: "column", flexShrink: 0, overflowY: "hidden", children: inner }));
|
|
79
|
+
}
|
|
65
80
|
if (!bordered) {
|
|
66
81
|
// Narrow terminals trade the box for a rail: same grouping, 2 fewer rows
|
|
67
82
|
// and 4 fewer columns (§5.2).
|
|
68
|
-
return (_jsx(Box, { flexDirection: "column", flexShrink: 0, marginTop: 1, children: _jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: railBorderStyle, borderTop: false, borderBottom: false, borderRight: false, borderColor: theme.primary, paddingLeft: 1, children: inner }) }));
|
|
83
|
+
return placed(_jsx(Box, { flexDirection: "column", flexShrink: 0, marginTop: 1, ...boxWidth, children: _jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: railBorderStyle, borderTop: false, borderBottom: false, borderRight: false, borderColor: theme.primary, paddingLeft: 1, children: inner }) }));
|
|
69
84
|
}
|
|
70
|
-
return (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: theme.primary, paddingX: 1, marginTop: 1, children: inner }));
|
|
85
|
+
return placed(_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: theme.primary, paddingX: 1, marginTop: 1, ...boxWidth, children: inner }));
|
|
71
86
|
}
|
|
72
87
|
/**
|
|
73
88
|
* Rows one PgUp/PgDn - and one Shift+wheel notch - turns inside a controlled
|