@aragon-agent/cli 0.6.24 → 0.6.26
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 +1924 -1786
- package/README.md +2730 -2562
- package/dist/agent/controller.d.ts +49 -3
- package/dist/agent/controller.js +78 -3
- package/dist/agent/conversation-reset.d.ts +91 -0
- package/dist/agent/conversation-reset.js +66 -0
- package/dist/agent/model-probe-limits.d.ts +39 -0
- package/dist/agent/model-probe-limits.js +25 -0
- package/dist/agent/model-probe.d.ts +104 -0
- package/dist/agent/model-probe.js +391 -0
- package/dist/agent/reducer.d.ts +22 -0
- package/dist/agent/reducer.js +49 -14
- package/dist/cli.js +214 -2
- package/dist/commands/branch-command.d.ts +61 -0
- package/dist/commands/branch-command.js +117 -0
- package/dist/commands/builtins.js +62 -40
- package/dist/commands/clear-command.d.ts +27 -0
- package/dist/commands/clear-command.js +107 -0
- package/dist/commands/registry.d.ts +48 -1
- package/dist/commands/session-command.d.ts +54 -0
- package/dist/commands/session-command.js +109 -0
- package/dist/config/load.d.ts +15 -0
- package/dist/config/model-profiles.d.ts +8 -0
- package/dist/config/model-profiles.js +10 -0
- package/dist/exec/index.js +10 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/terminal-plan.d.ts +155 -0
- package/dist/proc/terminal-plan.js +360 -0
- package/dist/session/branch-invocation.d.ts +83 -0
- package/dist/session/branch-invocation.js +109 -0
- package/dist/session/branch-launch.d.ts +70 -0
- package/dist/session/branch-launch.js +248 -0
- package/dist/session/checkpoint.d.ts +257 -0
- package/dist/session/checkpoint.js +512 -0
- package/dist/session/clear-archive.d.ts +51 -0
- package/dist/session/clear-archive.js +160 -0
- package/dist/session/cli-commands.d.ts +2 -0
- package/dist/session/cli-commands.js +36 -5
- package/dist/session/identity.d.ts +72 -0
- package/dist/session/identity.js +148 -0
- package/dist/session/limits.d.ts +66 -0
- package/dist/session/limits.js +66 -0
- package/dist/session/persist.d.ts +30 -5
- package/dist/session/startup-session.d.ts +107 -0
- package/dist/session/startup-session.js +315 -0
- package/dist/session/store.d.ts +60 -1
- package/dist/session/store.js +95 -6
- package/dist/todo/types.d.ts +5 -3
- package/dist/ui/App.d.ts +54 -2
- package/dist/ui/App.js +154 -28
- package/dist/ui/Header.js +8 -16
- package/dist/ui/StatusBar.d.ts +8 -0
- package/dist/ui/StatusBar.js +12 -3
- package/dist/ui/entries/CompactionCard.d.ts +5 -4
- package/dist/ui/entries/CompactionCard.js +5 -4
- package/dist/ui/layout/header-hints.d.ts +68 -0
- package/dist/ui/layout/header-hints.js +138 -0
- package/dist/ui/layout/session-chip.d.ts +52 -0
- package/dist/ui/layout/session-chip.js +64 -0
- package/dist/ui/layout/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +8 -1
- package/dist/ui/overlays/HelpOverlay.js +17 -5
- package/dist/ui/overlays/ModelDiscoveryList.d.ts +23 -0
- package/dist/ui/overlays/ModelDiscoveryList.js +161 -0
- package/dist/ui/overlays/ModelProfileEditor.d.ts +18 -1
- package/dist/ui/overlays/ModelProfileEditor.js +117 -68
- package/dist/ui/overlays/SettingsScreen.d.ts +8 -0
- package/dist/ui/overlays/SettingsScreen.js +34 -2
- package/dist/ui/overlays/model-probe-view.d.ts +128 -0
- package/dist/ui/overlays/model-probe-view.js +225 -0
- package/dist/ui/overlays/model-profile-state.d.ts +9 -1
- package/dist/ui/overlays/model-profile-state.js +10 -0
- package/dist/ui/overlays/use-model-probe.d.ts +30 -0
- package/dist/ui/overlays/use-model-probe.js +109 -0
- package/dist/ui/session-bridge.d.ts +26 -0
- package/dist/ui/session-bridge.js +32 -0
- package/package.json +2 -2
package/dist/ui/StatusBar.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Box, Text } from 'ink';
|
|
|
5
5
|
import { pickGlyphs } from './glyphs.js';
|
|
6
6
|
import { liveSpinner } from './ActivityLine.js';
|
|
7
7
|
import { planPrimaryStatusFields } from './layout/status-layout.js';
|
|
8
|
+
import { planSessionChip } from './layout/session-chip.js';
|
|
8
9
|
/** An invisible leading byte change forces Ink to repaint on Ctrl+L. */
|
|
9
10
|
const redrawChar = (nonce) => nonce % 2 ? String.fromCharCode(0x00a0) : ' ';
|
|
10
11
|
export function StatusBar(props) {
|
|
@@ -13,7 +14,15 @@ export function StatusBar(props) {
|
|
|
13
14
|
const active = phase !== 'idle' || !!props.compactionActive?.inFlight;
|
|
14
15
|
const glyph = pickGlyphs(props.caps).spinnerStill;
|
|
15
16
|
const spinner = active ? liveSpinner(props.reducedMotion ?? false, props.caps) : null;
|
|
17
|
+
// THE CHIP IS PLANNED FIRST, and the plan then feeds the field budget: the two
|
|
18
|
+
// cannot disagree about how wide the terminal is if only one of them is asked.
|
|
19
|
+
const chip = planSessionChip({
|
|
20
|
+
columns: cols,
|
|
21
|
+
...(props.sessionId ? { sessionId: props.sessionId } : {}),
|
|
22
|
+
});
|
|
23
|
+
const chipCells = chip.visible ? chip.cells + chip.gap : 0;
|
|
16
24
|
const plan = planPrimaryStatusFields({
|
|
25
|
+
reserveRight: chipCells,
|
|
17
26
|
columns: cols, phase, pendingCount: props.pendingSteering?.length ?? 0,
|
|
18
27
|
context: props.context, usageTotal: props.usageTotal,
|
|
19
28
|
thinkingLevel: props.thinkingLevel, elapsedMs: props.elapsedMs, tokPerSec: props.tokPerSec,
|
|
@@ -29,7 +38,7 @@ export function StatusBar(props) {
|
|
|
29
38
|
const tones = { normal: props.theme.primary, muted: props.theme.muted,
|
|
30
39
|
warning: props.theme.noticeWarn, error: props.theme.noticeError,
|
|
31
40
|
accent: props.theme.accent };
|
|
32
|
-
return _jsxs(Box, { width: cols, height: 1, flexShrink: 0, flexDirection: "row", overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(props.redrawNonce ?? 0) }) }), plan.fields.map((item, index) => _jsxs(React.Fragment, { children: [index > 0 && _jsx(Box, { width: plan.separatorCells, flexShrink: 0, children: _jsx(Text, { color: props.theme.muted, children: plan.separator }) }), _jsx(Box, { width: item.cells, flexShrink: 0, children: _jsx(Text, { color: tones[item.tone], wrap: "truncate", children: item.id === 'phase' && active
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
return _jsxs(Box, { width: cols, height: 1, flexShrink: 0, flexDirection: "row", overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(props.redrawNonce ?? 0) }) }), _jsx(Box, { width: Math.max(0, cols - 1 - chipCells), flexShrink: 0, flexDirection: "row", overflow: "hidden", children: plan.fields.map((item, index) => _jsxs(React.Fragment, { children: [index > 0 && _jsx(Box, { width: plan.separatorCells, flexShrink: 0, children: _jsx(Text, { color: props.theme.muted, children: plan.separator }) }), _jsx(Box, { width: item.cells, flexShrink: 0, children: _jsx(Text, { color: tones[item.tone], wrap: "truncate", children: item.id === 'phase' && active
|
|
42
|
+
? _jsxs(_Fragment, { children: [spinner ?? glyph, item.text.slice(glyph.length)] })
|
|
43
|
+
: item.text }) })] }, item.id)) }), chip.visible && _jsx(Box, { width: chipCells, flexShrink: 0, justifyContent: "flex-end", overflow: "hidden", children: _jsx(Text, { color: props.theme.muted, wrap: "truncate", children: chip.text }) })] });
|
|
35
44
|
}
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
* kind — without it the card renders a hint that does nothing and expands an
|
|
15
15
|
* older tool card instead (C-15 / P1-8 / AC-21).
|
|
16
16
|
*
|
|
17
|
-
* EVERY STRING DESCRIBES THE HISTORY, NEVER THE SCREEN (§6.4 / P2-9).
|
|
18
|
-
* empties the transcript view but not `messages`, so this card
|
|
19
|
-
*
|
|
20
|
-
* "above" or "earlier in this transcript".
|
|
17
|
+
* EVERY STRING DESCRIBES THE HISTORY, NEVER THE SCREEN (§6.4 / P2-9).
|
|
18
|
+
* `/clear screen` empties the transcript view but not `messages`, so this card
|
|
19
|
+
* can legitimately report a message count the visible rows do not add up to.
|
|
20
|
+
* Nothing here says "above" or "earlier in this transcript". `/clear` itself is
|
|
21
|
+
* the conversation boundary now: it drops the history and this card with it.
|
|
21
22
|
*/
|
|
22
23
|
import React from 'react';
|
|
23
24
|
import type { Theme } from '../theme.js';
|
|
@@ -15,10 +15,11 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
15
15
|
* kind — without it the card renders a hint that does nothing and expands an
|
|
16
16
|
* older tool card instead (C-15 / P1-8 / AC-21).
|
|
17
17
|
*
|
|
18
|
-
* EVERY STRING DESCRIBES THE HISTORY, NEVER THE SCREEN (§6.4 / P2-9).
|
|
19
|
-
* empties the transcript view but not `messages`, so this card
|
|
20
|
-
*
|
|
21
|
-
* "above" or "earlier in this transcript".
|
|
18
|
+
* EVERY STRING DESCRIBES THE HISTORY, NEVER THE SCREEN (§6.4 / P2-9).
|
|
19
|
+
* `/clear screen` empties the transcript view but not `messages`, so this card
|
|
20
|
+
* can legitimately report a message count the visible rows do not add up to.
|
|
21
|
+
* Nothing here says "above" or "earlier in this transcript". `/clear` itself is
|
|
22
|
+
* the conversation boundary now: it drops the history and this card with it.
|
|
22
23
|
*/
|
|
23
24
|
import React from 'react';
|
|
24
25
|
import { Box, Text } from 'ink';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The right-hand hint budget for the header row (tui-branch-terminal-shortcut 3.8).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS IS A PLANNER AND NOT ANOTHER TERNARY IN THE JSX (R-0-4): the header's
|
|
7
|
+
* right side used to be one hand-computed width constant (`detailCells = indexHint
|
|
8
|
+
* ? 32 : 18`). Adding a third affordance to that arithmetic produces either a
|
|
9
|
+
* clipped hint or a pushed-out word mark on a 72-column terminal, and NO TEST
|
|
10
|
+
* WOULD SEE IT - the only thing lost is trailing characters. `ui/layout/session-chip.ts`
|
|
11
|
+
* already set the precedent for the same problem: the chip has to participate in
|
|
12
|
+
* one width budget rather than be appended and hoped for.
|
|
13
|
+
*
|
|
14
|
+
* THE RULE IS ONE COMPARISON. `budget = columns - minLeftCells`, then take the
|
|
15
|
+
* first ladder rung whose `cells` fits. There is no second magic constant, and the
|
|
16
|
+
* threshold table in the spec is what that comparison WORKS OUT TO, not an extra
|
|
17
|
+
* branch.
|
|
18
|
+
*
|
|
19
|
+
* WHY `minLeftCells` IS 24: the word mark is 13 cells and `provider:model` + cwd +
|
|
20
|
+
* the key symbol need at least 11 more to stay readable. It is lower than
|
|
21
|
+
* `status-layout`'s 34 because the top-left half has a brand to lose while the
|
|
22
|
+
* bottom-left half has a phase that must not disappear.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Bounds for the hint budget.
|
|
26
|
+
*
|
|
27
|
+
* WHY THEY LIVE IN THIS FILE (R-21, accepted): `CLAUDE.md`'s general rule is that
|
|
28
|
+
* structural bounds live in the subsystem's `limits.ts`, and `ui/layout/` has none
|
|
29
|
+
* - `budget.ts` keeps `HINT_MIN_ROWS` beside its own module for the same reason.
|
|
30
|
+
* This module has exactly one consumer. If `ui/layout/` ever grows a real
|
|
31
|
+
* `limits.ts`, these three constants should move there together; this comment is
|
|
32
|
+
* that future migration's note.
|
|
33
|
+
*/
|
|
34
|
+
export declare const HEADER_HINT_LIMITS: {
|
|
35
|
+
/** Cells the LEFT half of the header must keep (`AragonAgent` is 13 of them). */
|
|
36
|
+
readonly minLeftCells: 24;
|
|
37
|
+
/** Fixed spacing between two hint clauses. */
|
|
38
|
+
readonly gapCells: 2;
|
|
39
|
+
};
|
|
40
|
+
export type HeaderHintId = 'branch' | 'index' | 'detail';
|
|
41
|
+
export interface HeaderHintClause {
|
|
42
|
+
id: HeaderHintId;
|
|
43
|
+
text: string;
|
|
44
|
+
}
|
|
45
|
+
export interface HeaderHintPlan {
|
|
46
|
+
/** Clauses left to right (including the overlay's `Details on/off` replacement). */
|
|
47
|
+
clauses: HeaderHintClause[];
|
|
48
|
+
/** The whole right-hand string, scroll chip included with its trailing spacing. */
|
|
49
|
+
text: string;
|
|
50
|
+
/** Total cells of the right column; `Header` uses it to size the left one. */
|
|
51
|
+
cells: number;
|
|
52
|
+
/** Cells taken by the scroll chip (0 when there is none). */
|
|
53
|
+
scrollCells: number;
|
|
54
|
+
/** True while an overlay owns the keyboard: only the detail word remains. */
|
|
55
|
+
detailsOnly: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Plan the right-hand hints.
|
|
59
|
+
*
|
|
60
|
+
* The four inputs are exactly the props `Header` already receives - the call site
|
|
61
|
+
* does not change, only the arithmetic behind it (spec 3.11 step 5).
|
|
62
|
+
*/
|
|
63
|
+
export declare function planHeaderHints(input: {
|
|
64
|
+
columns: number;
|
|
65
|
+
statusExpanded: boolean;
|
|
66
|
+
overlayOpen: boolean;
|
|
67
|
+
scrolledLines: number;
|
|
68
|
+
}): HeaderHintPlan;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The right-hand hint budget for the header row (tui-branch-terminal-shortcut 3.8).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS IS A PLANNER AND NOT ANOTHER TERNARY IN THE JSX (R-0-4): the header's
|
|
7
|
+
* right side used to be one hand-computed width constant (`detailCells = indexHint
|
|
8
|
+
* ? 32 : 18`). Adding a third affordance to that arithmetic produces either a
|
|
9
|
+
* clipped hint or a pushed-out word mark on a 72-column terminal, and NO TEST
|
|
10
|
+
* WOULD SEE IT - the only thing lost is trailing characters. `ui/layout/session-chip.ts`
|
|
11
|
+
* already set the precedent for the same problem: the chip has to participate in
|
|
12
|
+
* one width budget rather than be appended and hoped for.
|
|
13
|
+
*
|
|
14
|
+
* THE RULE IS ONE COMPARISON. `budget = columns - minLeftCells`, then take the
|
|
15
|
+
* first ladder rung whose `cells` fits. There is no second magic constant, and the
|
|
16
|
+
* threshold table in the spec is what that comparison WORKS OUT TO, not an extra
|
|
17
|
+
* branch.
|
|
18
|
+
*
|
|
19
|
+
* WHY `minLeftCells` IS 24: the word mark is 13 cells and `provider:model` + cwd +
|
|
20
|
+
* the key symbol need at least 11 more to stay readable. It is lower than
|
|
21
|
+
* `status-layout`'s 34 because the top-left half has a brand to lose while the
|
|
22
|
+
* bottom-left half has a phase that must not disappear.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Bounds for the hint budget.
|
|
26
|
+
*
|
|
27
|
+
* WHY THEY LIVE IN THIS FILE (R-21, accepted): `CLAUDE.md`'s general rule is that
|
|
28
|
+
* structural bounds live in the subsystem's `limits.ts`, and `ui/layout/` has none
|
|
29
|
+
* - `budget.ts` keeps `HINT_MIN_ROWS` beside its own module for the same reason.
|
|
30
|
+
* This module has exactly one consumer. If `ui/layout/` ever grows a real
|
|
31
|
+
* `limits.ts`, these three constants should move there together; this comment is
|
|
32
|
+
* that future migration's note.
|
|
33
|
+
*/
|
|
34
|
+
export const HEADER_HINT_LIMITS = {
|
|
35
|
+
/** Cells the LEFT half of the header must keep (`AragonAgent` is 13 of them). */
|
|
36
|
+
minLeftCells: 24,
|
|
37
|
+
/** Fixed spacing between two hint clauses. */
|
|
38
|
+
gapCells: 2,
|
|
39
|
+
};
|
|
40
|
+
/** The verbatim clause text (spec 3.8, appendix A). */
|
|
41
|
+
const BRANCH_CLAUSE = { collapsed: '^L branch', expanded: '^L new branch' };
|
|
42
|
+
const INDEX_CLAUSE = { collapsed: '^I Index', expanded: '^I build index' };
|
|
43
|
+
const DETAIL_CLAUSE = { collapsed: '^G more', expanded: '^G less' };
|
|
44
|
+
/** L3 gives the key name and nothing else: discoverability at the last width. */
|
|
45
|
+
const BRANCH_KEY_ONLY = '^L';
|
|
46
|
+
/** Overlay wording, kept exactly as `Header.tsx` had it. */
|
|
47
|
+
const DETAILS_ON = 'Details on';
|
|
48
|
+
const DETAILS_OFF = 'Details off';
|
|
49
|
+
/** Above this, the chip stops counting and saturates (`^9999+`), as it always has. */
|
|
50
|
+
const SCROLL_CHIP_MAX = 9999;
|
|
51
|
+
/**
|
|
52
|
+
* Plan the right-hand hints.
|
|
53
|
+
*
|
|
54
|
+
* The four inputs are exactly the props `Header` already receives - the call site
|
|
55
|
+
* does not change, only the arithmetic behind it (spec 3.11 step 5).
|
|
56
|
+
*/
|
|
57
|
+
export function planHeaderHints(input) {
|
|
58
|
+
// NORMALISED HERE, NOT TRUSTED (R-14). `Header` defaults to 80, but a test or a
|
|
59
|
+
// future caller may pass anything; `budget.ts::dimension()` already set the rule
|
|
60
|
+
// (non-finite or < 1 falls back, everything else floors) and this follows it.
|
|
61
|
+
const columns = normalizeDimension(input.columns, 80);
|
|
62
|
+
const scrolledLines = Math.max(0, Math.floor(normalizeDimension(input.scrolledLines, 0)));
|
|
63
|
+
const scrollCells = scrollChip(scrolledLines, input.overlayOpen);
|
|
64
|
+
if (input.overlayOpen) {
|
|
65
|
+
// THE OVERLAY OWNS THE KEYBOARD, so every key hint disappears (D-14's sibling
|
|
66
|
+
// rule, unchanged from the old JSX). The scroll chip stays: it is a position
|
|
67
|
+
// indicator, not an affordance.
|
|
68
|
+
const detail = input.statusExpanded ? DETAILS_ON : DETAILS_OFF;
|
|
69
|
+
return buildPlan([{ id: 'detail', text: detail }], scrollCells, true);
|
|
70
|
+
}
|
|
71
|
+
const ladder = ladderFor(input.statusExpanded);
|
|
72
|
+
const budget = columns - HEADER_HINT_LIMITS.minLeftCells;
|
|
73
|
+
for (const rung of ladder) {
|
|
74
|
+
const plan = buildPlan(rung, scrollCells, false);
|
|
75
|
+
if (plan.cells <= budget)
|
|
76
|
+
return plan;
|
|
77
|
+
}
|
|
78
|
+
// L4 IS UNCONDITIONAL: `^G` is the only entry in this table that may never be
|
|
79
|
+
// dropped - it is how a user who hid the status detail fields gets them back.
|
|
80
|
+
// A header narrower than the last rung still renders; the LEFT column is what
|
|
81
|
+
// shrinks (to zero), and `overflow="hidden"` keeps that from overflowing.
|
|
82
|
+
return buildPlan(ladder[ladder.length - 1] ?? [], scrollCells, false);
|
|
83
|
+
}
|
|
84
|
+
/** The four rungs, widest first. Built per call because the widths are text-derived. */
|
|
85
|
+
function ladderFor(statusExpanded) {
|
|
86
|
+
const branch = {
|
|
87
|
+
id: 'branch',
|
|
88
|
+
text: statusExpanded ? BRANCH_CLAUSE.expanded : BRANCH_CLAUSE.collapsed,
|
|
89
|
+
};
|
|
90
|
+
const index = {
|
|
91
|
+
id: 'index',
|
|
92
|
+
text: statusExpanded ? INDEX_CLAUSE.expanded : INDEX_CLAUSE.collapsed,
|
|
93
|
+
};
|
|
94
|
+
const detail = {
|
|
95
|
+
id: 'detail',
|
|
96
|
+
text: statusExpanded ? DETAIL_CLAUSE.expanded : DETAIL_CLAUSE.collapsed,
|
|
97
|
+
};
|
|
98
|
+
return [
|
|
99
|
+
[branch, index, detail],
|
|
100
|
+
[branch, detail],
|
|
101
|
+
[{ id: 'branch', text: BRANCH_KEY_ONLY }, detail],
|
|
102
|
+
[detail],
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Compose the final string. The chip carries its OWN trailing spacing and is the
|
|
107
|
+
* only source of it (R-8): one space while an overlay is open, two otherwise -
|
|
108
|
+
* copied byte for byte from the expression the header already used.
|
|
109
|
+
*/
|
|
110
|
+
function buildPlan(clauses, scrollChipText, detailsOnly) {
|
|
111
|
+
const body = clauses.map((clause) => clause.text).join(' '.repeat(HEADER_HINT_LIMITS.gapCells));
|
|
112
|
+
const text = scrollChipText.length > 0 ? `${scrollChipText}${body}` : body;
|
|
113
|
+
return {
|
|
114
|
+
clauses,
|
|
115
|
+
text,
|
|
116
|
+
cells: text.length,
|
|
117
|
+
scrollCells: scrollChipText.length,
|
|
118
|
+
detailsOnly,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* `^<n>` plus its trailing spacing, or `''`.
|
|
123
|
+
*
|
|
124
|
+
* THE SPACING IS PART OF THE CHIP, not something `Header` adds afterwards: with two
|
|
125
|
+
* sources of spacing a reviewer has to read two files to answer "how wide is the
|
|
126
|
+
* chip", and the overlay's single-space case lived in the JSX as a ternary nobody
|
|
127
|
+
* would think to check.
|
|
128
|
+
*/
|
|
129
|
+
function scrollChip(scrolledLines, overlayOpen) {
|
|
130
|
+
if (scrolledLines <= 0)
|
|
131
|
+
return '';
|
|
132
|
+
const label = scrolledLines > SCROLL_CHIP_MAX ? `^${SCROLL_CHIP_MAX}+` : `^${scrolledLines}`;
|
|
133
|
+
return label + (overlayOpen ? ' ' : ' ');
|
|
134
|
+
}
|
|
135
|
+
/** The `budget.ts::dimension()` rule, local because that helper is not exported. */
|
|
136
|
+
function normalizeDimension(value, fallback) {
|
|
137
|
+
return Number.isFinite(value) && value >= 1 ? Math.floor(value) : fallback;
|
|
138
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session-id chip: pure width planning, no JSX
|
|
3
|
+
* (tui-session-id-resume §3.9 / D-11 / D-12).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* WHY A PLANNER AND NOT A STYLED `<Text>`: the status bar is a cell-by-cell
|
|
8
|
+
* measured row with a six-rung degradation ladder (`status-layout.ts`), not a
|
|
9
|
+
* flex layout.
|
|
10
|
+
* Anything positioned "just to the right" pushes the left half over or covers
|
|
11
|
+
* `phase`, and `phase` is the field a user needs while a run is in flight. The
|
|
12
|
+
* chip therefore spends cells out of the SAME budget as every other field.
|
|
13
|
+
*
|
|
14
|
+
* THE ID IS NEVER TRUNCATED (D-12). A truncated id produces a `--resume` command
|
|
15
|
+
* that is guaranteed to fail, which is strictly worse than showing nothing: the
|
|
16
|
+
* rungs below are, in order, "drop the label" and only then "hide the whole
|
|
17
|
+
* chip", and `/session` reports the full id at any width.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SESSION_CHIP_LIMITS: {
|
|
20
|
+
/** The same threshold `Header.tsx` uses for its `^I Index` hint. */
|
|
21
|
+
readonly labelMinCols: 72;
|
|
22
|
+
/** Blank cells between the last status field and the chip. */
|
|
23
|
+
readonly gapCells: 2;
|
|
24
|
+
/**
|
|
25
|
+
* The tightest rung's worst-case width for the fields on the LEFT, summed:
|
|
26
|
+
* phase glyph(1)+label(4)=5, sep 1 -> 6, deferred mode 'C>B' 3 -> 9,
|
|
27
|
+
* sep 1 -> 10, 'C'+'~999%' 6 -> 16, sep 1 -> 17, thinking code 1 -> 18,
|
|
28
|
+
* sep 1 -> 19, '>99M/s' 6 -> 25, sep 1 -> 26, '>999d' 5 -> 31.
|
|
29
|
+
*
|
|
30
|
+
* 34 IS A FLOOR ROUNDED UP, not a measurement: over-estimating hides the chip
|
|
31
|
+
* one column early, which is invisible; under-estimating lets the chip eat the
|
|
32
|
+
* phase field, which is not.
|
|
33
|
+
*/
|
|
34
|
+
readonly minStatusCells: 34;
|
|
35
|
+
};
|
|
36
|
+
export interface SessionChipPlan {
|
|
37
|
+
visible: boolean;
|
|
38
|
+
text: string;
|
|
39
|
+
cells: number;
|
|
40
|
+
gap: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Plan the chip for `columns` and this session's id.
|
|
44
|
+
*
|
|
45
|
+
* Inputs are the same ones the status row already has, so the two plans cannot
|
|
46
|
+
* disagree about the terminal: `StatusBar` computes `chip` first and hands
|
|
47
|
+
* `chip.cells + chip.gap` to `planPrimaryStatusFields` as `reserveRight`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function planSessionChip(input: {
|
|
50
|
+
columns: number;
|
|
51
|
+
sessionId?: string | null;
|
|
52
|
+
}): SessionChipPlan;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session-id chip: pure width planning, no JSX
|
|
3
|
+
* (tui-session-id-resume §3.9 / D-11 / D-12).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* WHY A PLANNER AND NOT A STYLED `<Text>`: the status bar is a cell-by-cell
|
|
8
|
+
* measured row with a six-rung degradation ladder (`status-layout.ts`), not a
|
|
9
|
+
* flex layout.
|
|
10
|
+
* Anything positioned "just to the right" pushes the left half over or covers
|
|
11
|
+
* `phase`, and `phase` is the field a user needs while a run is in flight. The
|
|
12
|
+
* chip therefore spends cells out of the SAME budget as every other field.
|
|
13
|
+
*
|
|
14
|
+
* THE ID IS NEVER TRUNCATED (D-12). A truncated id produces a `--resume` command
|
|
15
|
+
* that is guaranteed to fail, which is strictly worse than showing nothing: the
|
|
16
|
+
* rungs below are, in order, "drop the label" and only then "hide the whole
|
|
17
|
+
* chip", and `/session` reports the full id at any width.
|
|
18
|
+
*/
|
|
19
|
+
import stringWidth from 'string-width';
|
|
20
|
+
export const SESSION_CHIP_LIMITS = {
|
|
21
|
+
/** The same threshold `Header.tsx` uses for its `^I Index` hint. */
|
|
22
|
+
labelMinCols: 72,
|
|
23
|
+
/** Blank cells between the last status field and the chip. */
|
|
24
|
+
gapCells: 2,
|
|
25
|
+
/**
|
|
26
|
+
* The tightest rung's worst-case width for the fields on the LEFT, summed:
|
|
27
|
+
* phase glyph(1)+label(4)=5, sep 1 -> 6, deferred mode 'C>B' 3 -> 9,
|
|
28
|
+
* sep 1 -> 10, 'C'+'~999%' 6 -> 16, sep 1 -> 17, thinking code 1 -> 18,
|
|
29
|
+
* sep 1 -> 19, '>99M/s' 6 -> 25, sep 1 -> 26, '>999d' 5 -> 31.
|
|
30
|
+
*
|
|
31
|
+
* 34 IS A FLOOR ROUNDED UP, not a measurement: over-estimating hides the chip
|
|
32
|
+
* one column early, which is invisible; under-estimating lets the chip eat the
|
|
33
|
+
* phase field, which is not.
|
|
34
|
+
*/
|
|
35
|
+
minStatusCells: 34,
|
|
36
|
+
};
|
|
37
|
+
/** `{visible:false}` - the shape every caller can cheaply branch on. */
|
|
38
|
+
const HIDDEN = { visible: false, text: '', cells: 0, gap: 0 };
|
|
39
|
+
/**
|
|
40
|
+
* Plan the chip for `columns` and this session's id.
|
|
41
|
+
*
|
|
42
|
+
* Inputs are the same ones the status row already has, so the two plans cannot
|
|
43
|
+
* disagree about the terminal: `StatusBar` computes `chip` first and hands
|
|
44
|
+
* `chip.cells + chip.gap` to `planPrimaryStatusFields` as `reserveRight`.
|
|
45
|
+
*/
|
|
46
|
+
export function planSessionChip(input) {
|
|
47
|
+
// No identity (the default build, a test, or a host with no session) is the
|
|
48
|
+
// `undefined` case, and it must cost nothing: reserveRight stays 0 and the
|
|
49
|
+
// status row's output is byte-identical to the pre-feature build (I-6).
|
|
50
|
+
if (!input.sessionId)
|
|
51
|
+
return HIDDEN;
|
|
52
|
+
const columns = Number.isFinite(input.columns) ? Math.floor(input.columns) : 80;
|
|
53
|
+
const id = input.sessionId;
|
|
54
|
+
const gap = SESSION_CHIP_LIMITS.gapCells;
|
|
55
|
+
const fits = (text) => columns - 1 - gap - stringWidth(text) >= SESSION_CHIP_LIMITS.minStatusCells;
|
|
56
|
+
// Widest form first, then drop the label, then hide: the order is the decision.
|
|
57
|
+
const labelled = `id ${id}`;
|
|
58
|
+
if (columns >= SESSION_CHIP_LIMITS.labelMinCols && fits(labelled)) {
|
|
59
|
+
return { visible: true, text: labelled, cells: stringWidth(labelled), gap };
|
|
60
|
+
}
|
|
61
|
+
if (fits(id))
|
|
62
|
+
return { visible: true, text: id, cells: stringWidth(id), gap };
|
|
63
|
+
return HIDDEN;
|
|
64
|
+
}
|
|
@@ -44,6 +44,16 @@ export interface StatusLayoutInput {
|
|
|
44
44
|
inFlight: boolean;
|
|
45
45
|
};
|
|
46
46
|
scrolledLines?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Cells already spent at the RIGHT end of this same row
|
|
49
|
+
* (tui-session-id-resume 3.10 / I-6).
|
|
50
|
+
*
|
|
51
|
+
* The session-id chip is not a layer above this row: it takes its cells out of
|
|
52
|
+
* the SAME budget, because the row is measured rather than laid out and
|
|
53
|
+
* anything overlapping it eats `phase` first. ABSENT MEANS 0, which is what
|
|
54
|
+
* keeps every existing caller's plan byte-identical (I-6).
|
|
55
|
+
*/
|
|
56
|
+
reserveRight?: number;
|
|
47
57
|
}
|
|
48
58
|
export interface StatusPrimaryInput extends StatusLayoutInput {
|
|
49
59
|
feedback?: StatusFeedback;
|
|
@@ -113,7 +113,14 @@ function modeFieldText(mode, pendingMode, full) {
|
|
|
113
113
|
}
|
|
114
114
|
/** Every field survives each width rung; only the redraw carrier is outside this plan. */
|
|
115
115
|
export function planPrimaryStatusFields(input) {
|
|
116
|
-
const
|
|
116
|
+
const columns = Number.isFinite(input.columns) ? Math.floor(input.columns) : 80;
|
|
117
|
+
// GUARDED LIKE `columns` ABOVE, and for the same reason: `Math.max(0, NaN)`
|
|
118
|
+
// is `NaN`, which would make every `cells > budget` comparison false and
|
|
119
|
+
// silently pin this row at its widest rung - the opposite of reserving space.
|
|
120
|
+
const reserved = typeof input.reserveRight === 'number' && Number.isFinite(input.reserveRight)
|
|
121
|
+
? Math.max(0, Math.floor(input.reserveRight))
|
|
122
|
+
: 0;
|
|
123
|
+
const budget = Math.max(0, columns - 1 - reserved);
|
|
117
124
|
const labels = phaseLabels(input);
|
|
118
125
|
const glyphText = cleanStatusText(input.spinner ?? '-');
|
|
119
126
|
const glyph = stringWidth(glyphText) === 1 ? glyphText : '-';
|
|
@@ -21,7 +21,11 @@ function keyRows(times) {
|
|
|
21
21
|
// with a draft x2 clears the input and x3 exits; an empty input
|
|
22
22
|
// keeps the previous x2 exit.
|
|
23
23
|
['Ctrl+C', `Draft: ${times}2 clear input, ${times}3 exit. Empty: ${times}2 exit`],
|
|
24
|
-
|
|
24
|
+
// Ctrl+L IS THE BRANCH KEY NOW (tui-branch-terminal-shortcut D-1) and the
|
|
25
|
+
// redraw moved to Ctrl+R. Both rows are listed, because the migration is the
|
|
26
|
+
// one thing an existing user will press by muscle memory and be surprised by.
|
|
27
|
+
['Ctrl+L', 'Open a new terminal on this session (the branch gets its own id)'],
|
|
28
|
+
['Ctrl+R', 'Redraw the frame'],
|
|
25
29
|
['Ctrl+T', 'Show/hide thinking (off by default)'],
|
|
26
30
|
['Ctrl+I', 'Build the project index (bundled skill; asks first; needs keyboard enhancement)'],
|
|
27
31
|
['Ctrl+G', 'Expand/collapse status details; disabled while an overlay is open'],
|
|
@@ -68,11 +72,19 @@ const COMMANDS = [
|
|
|
68
72
|
'Todo planning: status, switches, the rail, follow-through',
|
|
69
73
|
],
|
|
70
74
|
['/tools', 'List active tools'],
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
// `/clear` IS NOT "CLEAR THE SCREEN" ANY MORE (clear-command-context-reset D-1).
|
|
76
|
+
// Two rows, not one, because the intentions differ: `/clear` starts a NEW
|
|
77
|
+
// CONVERSATION (interrupt the run, archive the old one, drop the history), while
|
|
78
|
+
// `/clear screen` only wipes what is on screen. A user who wants the second has
|
|
79
|
+
// to be able to see that it exists before reaching for the first.
|
|
80
|
+
['/clear', 'Start a new conversation: archive, then clear context and transcript'],
|
|
81
|
+
['/clear screen', 'Clear the visible transcript only; keeps the conversation'],
|
|
82
|
+
['/reset', 'Alias of /clear'],
|
|
73
83
|
['/cwd [dir]', 'Show or change the tool working directory'],
|
|
74
|
-
['/save [file]', '
|
|
75
|
-
['/resume [file]', 'Load a saved session'],
|
|
84
|
+
['/save [file]', 'Export the session to JSON (a file you keep)'],
|
|
85
|
+
['/resume [file]', 'Load a saved session; this session moves to a new id'],
|
|
86
|
+
['/session [copy|pin]', 'Show this session id and file; copy the resume command; pin it'],
|
|
87
|
+
['/branch', 'Same as Ctrl+L: branch into a new terminal'],
|
|
76
88
|
['/copy', 'Copy last answer; native completion confirmed, OSC 52 request unconfirmed'],
|
|
77
89
|
['/queue', 'Read every pending message in full; no replay or queue mutation'],
|
|
78
90
|
['/mouse [on|off]', 'Release the mouse to your terminal, or take it back'],
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type ProfileFrameProps } from './ModelProfilePicker.js';
|
|
3
|
+
import { type DiscoveryView } from './model-probe-view.js';
|
|
4
|
+
interface ModelDiscoveryListProps extends ProfileFrameProps {
|
|
5
|
+
provider: string;
|
|
6
|
+
/** The draft's current model: the cursor starts on it when the list has it. */
|
|
7
|
+
currentModel: string;
|
|
8
|
+
view: DiscoveryView;
|
|
9
|
+
busy: boolean;
|
|
10
|
+
onChoose: (model: string) => void;
|
|
11
|
+
onRetry: () => void;
|
|
12
|
+
onBack: () => void;
|
|
13
|
+
onCancelProbe?: () => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The discovered-model page (model-profile-connection-test §3.5).
|
|
17
|
+
*
|
|
18
|
+
* Pure display plus input: it receives the discovery outcome and three intents and
|
|
19
|
+
* issues no request itself. No spinner - the census of animated sites is a closed
|
|
20
|
+
* list, and this overlay can be open while a run is in flight.
|
|
21
|
+
*/
|
|
22
|
+
export declare function ModelDiscoveryList(props: ModelDiscoveryListProps): React.ReactElement;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Text, useInput } from 'ink';
|
|
4
|
+
import { PROBE_LIMITS } from '../../agent/model-probe-limits.js';
|
|
5
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
6
|
+
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
7
|
+
import { stripPasteFrames } from '../paste-frames.js';
|
|
8
|
+
import { stripEnterFrames } from '../enter-frames.js';
|
|
9
|
+
import { clipProfileText, useProfileScroll } from './ModelProfilePicker.js';
|
|
10
|
+
import { rankCandidates, windowLabel, clipName, LIST_ID_CHARS } from './model-probe-view.js';
|
|
11
|
+
function matches(candidate, query) {
|
|
12
|
+
const needle = query.toLocaleLowerCase();
|
|
13
|
+
return candidate.id.toLocaleLowerCase().includes(needle)
|
|
14
|
+
|| candidate.name.toLocaleLowerCase().includes(needle);
|
|
15
|
+
}
|
|
16
|
+
/** One row: id column, name column, window right-aligned inside the frame budget. */
|
|
17
|
+
function listRowText(candidate, cols) {
|
|
18
|
+
const width = Math.max(20, cols - 8);
|
|
19
|
+
const label = windowLabel(candidate.contextWindow);
|
|
20
|
+
const idWidth = Math.max(10, Math.min(LIST_ID_CHARS, width - label.length - 14));
|
|
21
|
+
const nameWidth = Math.max(4, width - idWidth - label.length - 2);
|
|
22
|
+
const id = [...candidate.id].slice(0, idWidth).join('').padEnd(idWidth);
|
|
23
|
+
// `clipName` caps the NAME COLUMN itself, so a vendor that ships a paragraph in
|
|
24
|
+
// `name` cannot push the window label out of a wide frame either.
|
|
25
|
+
const name = [...clipName(candidate.name)].slice(0, nameWidth).join('').padEnd(nameWidth);
|
|
26
|
+
return clipProfileText(`${id} ${name} ${label}`, width);
|
|
27
|
+
}
|
|
28
|
+
/** What the page says when there is no row to select. */
|
|
29
|
+
function emptyLines(busy, provider, view) {
|
|
30
|
+
if (busy)
|
|
31
|
+
return [`Discovering ${provider} models...`];
|
|
32
|
+
if (view.kind === 'discovery' && !view.outcome.ok) {
|
|
33
|
+
return [view.outcome.detail ?? 'The model list could not be fetched.',
|
|
34
|
+
'Fix: check the base URL and key, then press r to retry.'];
|
|
35
|
+
}
|
|
36
|
+
if (view.kind === 'discovery') {
|
|
37
|
+
return ['No models returned. Press r to retry, Esc to go back.',
|
|
38
|
+
'Test the connection for a precise reason.'];
|
|
39
|
+
}
|
|
40
|
+
return ['No models discovered yet. Press r to retry, Esc to go back.'];
|
|
41
|
+
}
|
|
42
|
+
function buildRows(props, view, glyphs) {
|
|
43
|
+
const rows = [];
|
|
44
|
+
if (view.searchable) {
|
|
45
|
+
rows.push(_jsxs(Text, { wrap: "truncate", color: props.theme.primary, children: [view.searching ? `${glyphs.caret} ` : ' ', "Search: ", view.query || '(/ to search)'] }, "search"));
|
|
46
|
+
}
|
|
47
|
+
if (view.shown.length === 0) {
|
|
48
|
+
const lines = view.query.length > 0
|
|
49
|
+
? [`No model matches "${view.query}". Esc clears the search.`]
|
|
50
|
+
: emptyLines(props.busy, props.provider, props.view);
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: line }, `empty-${rows.length}`));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
view.shown.forEach((candidate, at) => {
|
|
56
|
+
const focused = at === view.cursor && !view.searching;
|
|
57
|
+
rows.push(_jsxs(Text, { wrap: "truncate", color: at === view.cursor ? props.theme.accent : props.theme.muted, children: [focused ? `${glyphs.caret} ` : ' ', listRowText(candidate, props.cols)] }, candidate.id));
|
|
58
|
+
});
|
|
59
|
+
if (view.truncated) {
|
|
60
|
+
const total = props.view.kind === 'discovery'
|
|
61
|
+
? props.view.outcome.total ?? view.shown.length : view.shown.length;
|
|
62
|
+
rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: `Showing first ${PROBE_LIMITS.maxListedModels} of ${total}. Narrow with search.` }, "truncated"));
|
|
63
|
+
}
|
|
64
|
+
return rows;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The discovered-model page (model-profile-connection-test §3.5).
|
|
68
|
+
*
|
|
69
|
+
* Pure display plus input: it receives the discovery outcome and three intents and
|
|
70
|
+
* issues no request itself. No spinner - the census of animated sites is a closed
|
|
71
|
+
* list, and this overlay can be open while a run is in flight.
|
|
72
|
+
*/
|
|
73
|
+
export function ModelDiscoveryList(props) {
|
|
74
|
+
const [index, setIndex] = useState(0);
|
|
75
|
+
const [query, setQuery] = useState('');
|
|
76
|
+
const [searching, setSearching] = useState(false);
|
|
77
|
+
const glyphs = pickGlyphs(props.caps);
|
|
78
|
+
const candidates = props.view.kind === 'discovery'
|
|
79
|
+
? props.view.outcome.models : [];
|
|
80
|
+
const ranked = rankCandidates(candidates, props.currentModel);
|
|
81
|
+
const shown = query.length > 0 ? ranked.filter((item) => matches(item, query)) : ranked;
|
|
82
|
+
const searchable = ranked.length > PROBE_LIMITS.searchThreshold;
|
|
83
|
+
const cursor = shown.length > 0 ? Math.min(index, shown.length - 1) : 0;
|
|
84
|
+
// The truncation notice is one row BELOW the last candidate, and `useProfileScroll`
|
|
85
|
+
// only guarantees the FOCUSED row is visible - the same blind spot that hid the
|
|
86
|
+
// editor's result rows (RV-P1-3). Standing on the last row therefore widens the
|
|
87
|
+
// window by one, or `Showing first 400 of ...` could never be read at all.
|
|
88
|
+
const truncatedNotice = props.view.kind === 'discovery' && props.view.outcome.truncated
|
|
89
|
+
&& shown.length > 0;
|
|
90
|
+
const revealNotice = truncatedNotice && cursor >= shown.length - 1;
|
|
91
|
+
const offset = useProfileScroll(props, cursor + (searchable ? 1 : 0) + (revealNotice ? 1 : 0));
|
|
92
|
+
// Arrival, not every render: when the outcome lands, put the cursor on the model the
|
|
93
|
+
// draft already names, so "which one am I on" is answerable without scrolling.
|
|
94
|
+
const arrival = props.view.kind === 'discovery'
|
|
95
|
+
? `${props.view.outcome.connectionId}|${ranked.length}` : 'none';
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const at = ranked.findIndex((item) => item.id === props.currentModel.trim());
|
|
98
|
+
setIndex(at > 0 ? at : 0);
|
|
99
|
+
}, [arrival]);
|
|
100
|
+
const move = (delta) => {
|
|
101
|
+
if (shown.length > 0)
|
|
102
|
+
setIndex((cursor + delta + shown.length) % shown.length);
|
|
103
|
+
};
|
|
104
|
+
useInput((input, key) => {
|
|
105
|
+
if (props.isActive === false)
|
|
106
|
+
return;
|
|
107
|
+
if (key.escape) {
|
|
108
|
+
if (searching) {
|
|
109
|
+
setSearching(false);
|
|
110
|
+
setQuery('');
|
|
111
|
+
setIndex(0);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (props.busy) {
|
|
115
|
+
props.onCancelProbe?.();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
props.onBack();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (key.upArrow || (key.tab && key.shift)) {
|
|
122
|
+
move(-1);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (key.downArrow || key.tab) {
|
|
126
|
+
move(1);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (key.return) {
|
|
130
|
+
const picked = shown[cursor];
|
|
131
|
+
if (picked)
|
|
132
|
+
props.onChoose(picked.id);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (searching) {
|
|
136
|
+
if (key.backspace || key.delete) {
|
|
137
|
+
setQuery((value) => [...value].slice(0, -1).join(''));
|
|
138
|
+
setIndex(0);
|
|
139
|
+
}
|
|
140
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
141
|
+
setQuery((value) => value + stripPasteFrames(stripEnterFrames(input)));
|
|
142
|
+
setIndex(0);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (input === '/' && searchable) {
|
|
147
|
+
setSearching(true);
|
|
148
|
+
setIndex(0);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// `r` retries ONLY outside the search state: sharing the key with the query would
|
|
152
|
+
// turn one typo into a network request (§3.5 / D-16).
|
|
153
|
+
if (!key.ctrl && !key.meta && input === 'r')
|
|
154
|
+
props.onRetry();
|
|
155
|
+
});
|
|
156
|
+
const hint = props.busy ? 'Esc cancel'
|
|
157
|
+
: searching ? 'Type search | Enter choose | Esc back'
|
|
158
|
+
: `Enter choose${searchable ? ' | / search' : ''} | r retry | Esc back`;
|
|
159
|
+
return _jsx(OverlayFrame, { ...props, scrollOffset: offset, hint: hint, rows: buildRows(props, { query, shown, cursor, searchable, searching,
|
|
160
|
+
truncated: truncatedNotice }, glyphs), title: `Models | ${props.provider} (${shown.length})` });
|
|
161
|
+
}
|