@aiwayds/dsh-tui-pi 0.1.1 → 0.3.0
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/README.md +175 -41
- package/lib/agent-manager.d.ts +125 -0
- package/lib/agent-manager.js +298 -0
- package/lib/agent-manager.js.map +1 -0
- package/lib/agents.d.ts +30 -0
- package/lib/agents.js +468 -0
- package/lib/agents.js.map +1 -0
- package/lib/append-system.d.ts +2 -0
- package/lib/append-system.js +1 -1
- package/lib/append-system.js.map +1 -1
- package/lib/dsh-events.d.ts +12 -0
- package/lib/editor.d.ts +58 -0
- package/lib/editor.js +92 -0
- package/lib/editor.js.map +1 -1
- package/lib/footer.d.ts +8 -0
- package/lib/footer.js +8 -0
- package/lib/footer.js.map +1 -1
- package/lib/hotkeys.d.ts +93 -0
- package/lib/hotkeys.js +305 -0
- package/lib/hotkeys.js.map +1 -0
- package/lib/index.js +301 -29
- package/lib/index.js.map +1 -1
- package/lib/keymap.d.ts +177 -0
- package/lib/keymap.js +189 -0
- package/lib/keymap.js.map +1 -0
- package/lib/live-widgets.d.ts +29 -24
- package/lib/live-widgets.js +78 -45
- package/lib/live-widgets.js.map +1 -1
- package/lib/panels.d.ts +177 -0
- package/lib/panels.js +331 -0
- package/lib/panels.js.map +1 -0
- package/lib/selectors.d.ts +12 -1
- package/lib/selectors.js +1 -1
- package/lib/selectors.js.map +1 -1
- package/lib/session.d.ts +25 -0
- package/lib/session.js +137 -10
- package/lib/session.js.map +1 -1
- package/lib/settings.d.ts +1 -2
- package/lib/subagent-policy.d.ts +58 -0
- package/lib/subagent-policy.js +137 -0
- package/lib/subagent-policy.js.map +1 -0
- package/lib/subagent-viewer.d.ts +49 -0
- package/lib/subagent-viewer.js +523 -0
- package/lib/subagent-viewer.js.map +1 -0
- package/lib/theme/index.js +4 -0
- package/lib/theme/index.js.map +1 -1
- package/lib/theme/palette.d.ts +21 -3
- package/lib/theme/palette.js +27 -6
- package/lib/theme/palette.js.map +1 -1
- package/lib/theme-settings.d.ts +39 -4
- package/lib/theme-settings.js +69 -12
- package/lib/theme-settings.js.map +1 -1
- package/lib/tui.d.ts +23 -9
- package/lib/tui.js +99 -8
- package/lib/tui.js.map +1 -1
- package/package.json +3 -4
- package/patches/@earendil-works__pi-tui.patch +206 -2
- package/scripts/dev-install.mjs +87 -0
- package/scripts/link-dsh-closure.mjs +85 -0
package/lib/keymap.d.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-level keybinding decisions — the pi interrupt chain, kept pure.
|
|
3
|
+
*
|
|
4
|
+
* pi (badlogic/pi-mono) routes terminal key presses through an app-level
|
|
5
|
+
* handler before the focused component sees them. This module is the
|
|
6
|
+
* dsh-tui-pi equivalent: `tui.ts` composes the live state and calls
|
|
7
|
+
* `resolveKeyAction`, `index.ts` executes the returned action. Keeping the
|
|
8
|
+
* decision PURE makes the whole chain unit-testable without a terminal.
|
|
9
|
+
*
|
|
10
|
+
* Esc chain (mirrors pi's `app.interrupt`, verified on pi 0.84.2):
|
|
11
|
+
* 1. popup/selector overlay open → let the popup's own Esc handle it —
|
|
12
|
+
* Esc NEVER touches the running task
|
|
13
|
+
* from inside a popup (the subagent
|
|
14
|
+
* viewer, pickers and dialogs close
|
|
15
|
+
* themselves and leave the turn alone)
|
|
16
|
+
* 2. editor autocomplete showing → let the editor close the list only
|
|
17
|
+
* 3. agent mid-turn (running) → 1st press arms the stop window, 2nd
|
|
18
|
+
* press within `DOUBLE_PRESS_MS` cancels
|
|
19
|
+
* the whole task (parent + subagents) —
|
|
20
|
+
* stopping is a deliberate double-press
|
|
21
|
+
* 4. editor non-empty (idle) → NO-OP — pi's anti-misfire core
|
|
22
|
+
* 5. editor empty (idle) → 1st press arms, 2nd press within
|
|
23
|
+
* `DOUBLE_PRESS_MS` triggers the mapped
|
|
24
|
+
* action (pi defaults to /tree; dsh has
|
|
25
|
+
* no /tree, so we map to /session — the
|
|
26
|
+
* closest non-invasive analogue). The
|
|
27
|
+
* running-stop and idle windows own
|
|
28
|
+
* SEPARATE clocks (`lastRunningEscPress`
|
|
29
|
+
* vs `lastEscPress`) so an Esc that armed
|
|
30
|
+
* a stop never pops /session after the
|
|
31
|
+
* turn settles, and vice versa.
|
|
32
|
+
*
|
|
33
|
+
* Ctrl+C chain (dsh convention, windowed): first press cancels a running
|
|
34
|
+
* turn — or clears the editor while idle — and a second press within
|
|
35
|
+
* `DOUBLE_PRESS_MS` quits. This keeps dsh's "first cancels / second quits"
|
|
36
|
+
* while requiring a deliberate double-press before an idle exit. Held-key
|
|
37
|
+
* auto-repeat never counts: repeats inside `MIN_DOUBLE_PRESS_GAP_MS` resolve
|
|
38
|
+
* to `key-repeat` (consumed, inert — and the executor aborts a pending quit
|
|
39
|
+
* confirmation on one, since a repeat betrays a held key; see index.ts).
|
|
40
|
+
*
|
|
41
|
+
* Ctrl+D chain (pi `app.exit`): only quits on an EMPTY editor; with text it
|
|
42
|
+
* is left alone so the pi-tui Editor's existing `tui.editor.deleteCharForward`
|
|
43
|
+
* binding (delete, ctrl+d) deletes a character forward — exactly pi's
|
|
44
|
+
* context distinction.
|
|
45
|
+
*
|
|
46
|
+
* Ctrl+L chain (pi `app.model.select`): opens the model/think picker — the
|
|
47
|
+
* dsh analogue of pi's model selector. Like every app key here it yields to
|
|
48
|
+
* an open overlay first, so a popup that is on screen keeps its own keys.
|
|
49
|
+
*
|
|
50
|
+
* Ctrl+G chain (dsh's subagent viewer): opens the subagent picker only while
|
|
51
|
+
* a subagent runs (`runningAgents > 0`) — idle it falls through untouched.
|
|
52
|
+
* Same overlay-yields-first rule as the other app keys.
|
|
53
|
+
*/
|
|
54
|
+
import { type KeyId } from '@earendil-works/pi-tui';
|
|
55
|
+
/** Double-press window (ms) for ctrl+c quit and empty-editor double-Esc. */
|
|
56
|
+
export declare const DOUBLE_PRESS_MS = 500;
|
|
57
|
+
/**
|
|
58
|
+
* Minimum gap (ms) between two presses of the same key for the second to
|
|
59
|
+
* count as a deliberate press rather than terminal auto-repeat. A held key is
|
|
60
|
+
* re-sent every ~30-50ms after the OS repeat delay (183ms-2s) and terminals
|
|
61
|
+
* report no key-up, so gap alone is the discriminator: anything closer than
|
|
62
|
+
* this floor is auto-repeat (see `key-repeat`). Human double-presses are
|
|
63
|
+
* rarely faster than ~100ms; common repeat rates (20-33/s) sit well under it.
|
|
64
|
+
*/
|
|
65
|
+
export declare const MIN_DOUBLE_PRESS_GAP_MS = 80;
|
|
66
|
+
/**
|
|
67
|
+
* The key identifiers resolved for each app-level binding. Injectable so a
|
|
68
|
+
* future `~/.dsh/keybindings.json` override can remap the app keys without
|
|
69
|
+
* touching the decision logic (see README "Keyboard shortcuts").
|
|
70
|
+
*/
|
|
71
|
+
export interface KeyBindings {
|
|
72
|
+
/** Stop the current task (pi app.interrupt). */
|
|
73
|
+
escape: KeyId;
|
|
74
|
+
/** Cancels the running turn / clears the editor; double-press quits. */
|
|
75
|
+
ctrlC: KeyId;
|
|
76
|
+
/** Quit, but only when the editor is empty (pi app.exit). */
|
|
77
|
+
ctrlD: KeyId;
|
|
78
|
+
/** Open the model picker (pi app.model.select). */
|
|
79
|
+
modelPicker: KeyId;
|
|
80
|
+
/** Open the subagent picker / viewer (running children). */
|
|
81
|
+
subagentViewer: KeyId;
|
|
82
|
+
}
|
|
83
|
+
export declare const DEFAULT_KEYBINDINGS: KeyBindings;
|
|
84
|
+
/** Live snapshot of everything the decision needs — composed by tui.ts. */
|
|
85
|
+
export interface KeyPressState {
|
|
86
|
+
/** Agent mid-turn (`bridge.isRunning()`). */
|
|
87
|
+
running: boolean;
|
|
88
|
+
/** An overlay/popup is open and focused (`tui.hasOverlay()`). */
|
|
89
|
+
overlayOpen: boolean;
|
|
90
|
+
/** Editor buffer is non-empty. */
|
|
91
|
+
editorHasText: boolean;
|
|
92
|
+
/** Editor autocomplete dropdown is currently showing. */
|
|
93
|
+
autocompleteOpen: boolean;
|
|
94
|
+
/** Live (not settled) subagent count; 0 = nothing for ctrl+g to open. */
|
|
95
|
+
runningAgents: number;
|
|
96
|
+
/** Timestamp (ms) of the last handled escape press; 0 = none. */
|
|
97
|
+
lastEscPress: number;
|
|
98
|
+
/** Timestamp (ms) of the last handled escape press that armed the
|
|
99
|
+
* running-stop window (branch 3 above); 0 = none. Own clock: independent
|
|
100
|
+
* of `lastEscPress` so the two Esc windows never bleed into each other. */
|
|
101
|
+
lastRunningEscPress: number;
|
|
102
|
+
/** Timestamp (ms) of the last handled ctrl+c press; 0 = none. */
|
|
103
|
+
lastCtrlCPress: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* One resolved app-level key action. `consumes` mirrors pi-tui's
|
|
107
|
+
* `TuiInputListenerResult.consume`: true stops the key from reaching the
|
|
108
|
+
* focused component, false lets it through (the popup / the editor handle
|
|
109
|
+
* it — e.g. Esc-close, ctrl+d delete-char, drop of an unbound key).
|
|
110
|
+
*/
|
|
111
|
+
export type KeyAction = {
|
|
112
|
+
kind: 'overlay';
|
|
113
|
+
consumes: false;
|
|
114
|
+
} | {
|
|
115
|
+
kind: 'autocomplete-close';
|
|
116
|
+
consumes: false;
|
|
117
|
+
} | {
|
|
118
|
+
kind: 'noop';
|
|
119
|
+
consumes: false;
|
|
120
|
+
} | {
|
|
121
|
+
kind: 'interrupt-arm-stop';
|
|
122
|
+
consumes: true;
|
|
123
|
+
} | {
|
|
124
|
+
kind: 'interrupt-cancel';
|
|
125
|
+
consumes: true;
|
|
126
|
+
} | {
|
|
127
|
+
kind: 'interrupt-arm';
|
|
128
|
+
consumes: true;
|
|
129
|
+
} | {
|
|
130
|
+
kind: 'interrupt-double';
|
|
131
|
+
consumes: true;
|
|
132
|
+
} | {
|
|
133
|
+
kind: 'ctrl-c-cancel';
|
|
134
|
+
consumes: true;
|
|
135
|
+
} | {
|
|
136
|
+
kind: 'ctrl-c-clear';
|
|
137
|
+
consumes: true;
|
|
138
|
+
} | {
|
|
139
|
+
kind: 'ctrl-c-quit';
|
|
140
|
+
consumes: true;
|
|
141
|
+
} | {
|
|
142
|
+
kind: 'ctrl-d-quit';
|
|
143
|
+
consumes: true;
|
|
144
|
+
} | {
|
|
145
|
+
kind: 'model-picker';
|
|
146
|
+
consumes: true;
|
|
147
|
+
} | {
|
|
148
|
+
kind: 'subagent-viewer';
|
|
149
|
+
consumes: true;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Auto-repeat of a windowed key (Ctrl+C / armed Esc) — the key is being
|
|
153
|
+
* held. Consumed so the editor never sees it, but no action fires and the
|
|
154
|
+
* double-press clock does NOT advance (a repeat must never arm or complete
|
|
155
|
+
* a double press). The executor can use the `key` to cancel a pending
|
|
156
|
+
* quit confirmation (a repeat betrays a held key).
|
|
157
|
+
*/
|
|
158
|
+
| {
|
|
159
|
+
kind: 'key-repeat';
|
|
160
|
+
key: 'ctrl-c' | 'escape';
|
|
161
|
+
consumes: true;
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Resolve one raw terminal input sequence into an app-level action.
|
|
165
|
+
* `data` is the single decoded key sequence `addInputListener` receives
|
|
166
|
+
* (e.g. "\x1b" for Escape, "\x03" for Ctrl+C); see keys.js for the
|
|
167
|
+
* normalization: Escape is `\x1b`, and legacy Ctrl+[ collapses to the same
|
|
168
|
+
* byte, so both are recognized as `escape`.
|
|
169
|
+
*
|
|
170
|
+
* `keys` may be a PARTIAL override — every missing binding falls back to the
|
|
171
|
+
* default. That is the shape a future `~/.dsh/keybindings.json` loader will
|
|
172
|
+
* feed in: remap one key, keep the rest.
|
|
173
|
+
*
|
|
174
|
+
* Any key outside the app bindings yields `noop` (not consumed — the focused
|
|
175
|
+
* component, normally the editor, processes it).
|
|
176
|
+
*/
|
|
177
|
+
export declare function resolveKeyAction(data: string, state: KeyPressState, now: number, keys?: Partial<KeyBindings>): KeyAction;
|
package/lib/keymap.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-level keybinding decisions — the pi interrupt chain, kept pure.
|
|
3
|
+
*
|
|
4
|
+
* pi (badlogic/pi-mono) routes terminal key presses through an app-level
|
|
5
|
+
* handler before the focused component sees them. This module is the
|
|
6
|
+
* dsh-tui-pi equivalent: `tui.ts` composes the live state and calls
|
|
7
|
+
* `resolveKeyAction`, `index.ts` executes the returned action. Keeping the
|
|
8
|
+
* decision PURE makes the whole chain unit-testable without a terminal.
|
|
9
|
+
*
|
|
10
|
+
* Esc chain (mirrors pi's `app.interrupt`, verified on pi 0.84.2):
|
|
11
|
+
* 1. popup/selector overlay open → let the popup's own Esc handle it —
|
|
12
|
+
* Esc NEVER touches the running task
|
|
13
|
+
* from inside a popup (the subagent
|
|
14
|
+
* viewer, pickers and dialogs close
|
|
15
|
+
* themselves and leave the turn alone)
|
|
16
|
+
* 2. editor autocomplete showing → let the editor close the list only
|
|
17
|
+
* 3. agent mid-turn (running) → 1st press arms the stop window, 2nd
|
|
18
|
+
* press within `DOUBLE_PRESS_MS` cancels
|
|
19
|
+
* the whole task (parent + subagents) —
|
|
20
|
+
* stopping is a deliberate double-press
|
|
21
|
+
* 4. editor non-empty (idle) → NO-OP — pi's anti-misfire core
|
|
22
|
+
* 5. editor empty (idle) → 1st press arms, 2nd press within
|
|
23
|
+
* `DOUBLE_PRESS_MS` triggers the mapped
|
|
24
|
+
* action (pi defaults to /tree; dsh has
|
|
25
|
+
* no /tree, so we map to /session — the
|
|
26
|
+
* closest non-invasive analogue). The
|
|
27
|
+
* running-stop and idle windows own
|
|
28
|
+
* SEPARATE clocks (`lastRunningEscPress`
|
|
29
|
+
* vs `lastEscPress`) so an Esc that armed
|
|
30
|
+
* a stop never pops /session after the
|
|
31
|
+
* turn settles, and vice versa.
|
|
32
|
+
*
|
|
33
|
+
* Ctrl+C chain (dsh convention, windowed): first press cancels a running
|
|
34
|
+
* turn — or clears the editor while idle — and a second press within
|
|
35
|
+
* `DOUBLE_PRESS_MS` quits. This keeps dsh's "first cancels / second quits"
|
|
36
|
+
* while requiring a deliberate double-press before an idle exit. Held-key
|
|
37
|
+
* auto-repeat never counts: repeats inside `MIN_DOUBLE_PRESS_GAP_MS` resolve
|
|
38
|
+
* to `key-repeat` (consumed, inert — and the executor aborts a pending quit
|
|
39
|
+
* confirmation on one, since a repeat betrays a held key; see index.ts).
|
|
40
|
+
*
|
|
41
|
+
* Ctrl+D chain (pi `app.exit`): only quits on an EMPTY editor; with text it
|
|
42
|
+
* is left alone so the pi-tui Editor's existing `tui.editor.deleteCharForward`
|
|
43
|
+
* binding (delete, ctrl+d) deletes a character forward — exactly pi's
|
|
44
|
+
* context distinction.
|
|
45
|
+
*
|
|
46
|
+
* Ctrl+L chain (pi `app.model.select`): opens the model/think picker — the
|
|
47
|
+
* dsh analogue of pi's model selector. Like every app key here it yields to
|
|
48
|
+
* an open overlay first, so a popup that is on screen keeps its own keys.
|
|
49
|
+
*
|
|
50
|
+
* Ctrl+G chain (dsh's subagent viewer): opens the subagent picker only while
|
|
51
|
+
* a subagent runs (`runningAgents > 0`) — idle it falls through untouched.
|
|
52
|
+
* Same overlay-yields-first rule as the other app keys.
|
|
53
|
+
*/
|
|
54
|
+
import { matchesKey } from '@earendil-works/pi-tui';
|
|
55
|
+
/** Double-press window (ms) for ctrl+c quit and empty-editor double-Esc. */
|
|
56
|
+
export const DOUBLE_PRESS_MS = 500;
|
|
57
|
+
/**
|
|
58
|
+
* Minimum gap (ms) between two presses of the same key for the second to
|
|
59
|
+
* count as a deliberate press rather than terminal auto-repeat. A held key is
|
|
60
|
+
* re-sent every ~30-50ms after the OS repeat delay (183ms-2s) and terminals
|
|
61
|
+
* report no key-up, so gap alone is the discriminator: anything closer than
|
|
62
|
+
* this floor is auto-repeat (see `key-repeat`). Human double-presses are
|
|
63
|
+
* rarely faster than ~100ms; common repeat rates (20-33/s) sit well under it.
|
|
64
|
+
*/
|
|
65
|
+
export const MIN_DOUBLE_PRESS_GAP_MS = 80;
|
|
66
|
+
export const DEFAULT_KEYBINDINGS = {
|
|
67
|
+
escape: 'escape',
|
|
68
|
+
ctrlC: 'ctrl+c',
|
|
69
|
+
ctrlD: 'ctrl+d',
|
|
70
|
+
modelPicker: 'ctrl+l',
|
|
71
|
+
subagentViewer: 'ctrl+g',
|
|
72
|
+
};
|
|
73
|
+
/** True when a second press within the window follows `lastPress`. */
|
|
74
|
+
function isDoublePress(lastPress, now) {
|
|
75
|
+
return lastPress > 0 && now - lastPress <= DOUBLE_PRESS_MS;
|
|
76
|
+
}
|
|
77
|
+
/** True when this press is auto-repeat of a held key (gap under the floor). */
|
|
78
|
+
function isKeyRepeat(lastPress, now) {
|
|
79
|
+
return lastPress > 0 && now - lastPress < MIN_DOUBLE_PRESS_GAP_MS;
|
|
80
|
+
}
|
|
81
|
+
/** The Esc chain — pi app.interrupt, popup and autocomplete first. */
|
|
82
|
+
function resolveEscape(state, now) {
|
|
83
|
+
// 1. An open overlay owns Esc: the popup (subagent viewer, pickers,
|
|
84
|
+
// dialogs) closes itself and the running task is NEVER touched by that
|
|
85
|
+
// press — Esc is a per-popup cancel, not the global stop.
|
|
86
|
+
if (state.overlayOpen)
|
|
87
|
+
return { kind: 'overlay', consumes: false };
|
|
88
|
+
if (state.autocompleteOpen)
|
|
89
|
+
return { kind: 'autocomplete-close', consumes: false };
|
|
90
|
+
// 2. Agent mid-turn: stopping the whole task (parent + subagents) is a
|
|
91
|
+
// deliberate double-press — the first Esc only arms the stop window, so
|
|
92
|
+
// a stray Esc (e.g. one aimed at a just-closed popup) can never kill a
|
|
93
|
+
// running turn. Its clock is the separate `lastRunningEscPress`; held
|
|
94
|
+
// auto-repeat never arms or fires it.
|
|
95
|
+
if (state.running) {
|
|
96
|
+
if (isKeyRepeat(state.lastRunningEscPress, now))
|
|
97
|
+
return { kind: 'key-repeat', key: 'escape', consumes: true };
|
|
98
|
+
return isDoublePress(state.lastRunningEscPress, now)
|
|
99
|
+
? { kind: 'interrupt-cancel', consumes: true }
|
|
100
|
+
: { kind: 'interrupt-arm-stop', consumes: true };
|
|
101
|
+
}
|
|
102
|
+
// 3. Idle: a non-empty editor is untouched (pi's anti-misfire core).
|
|
103
|
+
if (state.editorHasText)
|
|
104
|
+
return { kind: 'noop', consumes: false };
|
|
105
|
+
// 4. Empty editor, idle: first press arms, second within the window fires
|
|
106
|
+
// the double action (/session — dsh has no pi /tree). Auto-repeat of the
|
|
107
|
+
// armed key never fires the double action.
|
|
108
|
+
if (isKeyRepeat(state.lastEscPress, now))
|
|
109
|
+
return { kind: 'key-repeat', key: 'escape', consumes: true };
|
|
110
|
+
return isDoublePress(state.lastEscPress, now)
|
|
111
|
+
? { kind: 'interrupt-double', consumes: true }
|
|
112
|
+
: { kind: 'interrupt-arm', consumes: true };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Ctrl+C — dsh's "first cancels / second quits", windowed against misfires.
|
|
116
|
+
* Auto-repeat (a held key) is swallowed: it neither cancels again nor ever
|
|
117
|
+
* completes the quit double press.
|
|
118
|
+
*/
|
|
119
|
+
function resolveCtrlC(state, now) {
|
|
120
|
+
if (state.overlayOpen)
|
|
121
|
+
return { kind: 'overlay', consumes: false };
|
|
122
|
+
if (isKeyRepeat(state.lastCtrlCPress, now))
|
|
123
|
+
return { kind: 'key-repeat', key: 'ctrl-c', consumes: true };
|
|
124
|
+
if (state.running) {
|
|
125
|
+
if (isDoublePress(state.lastCtrlCPress, now))
|
|
126
|
+
return { kind: 'ctrl-c-quit', consumes: true };
|
|
127
|
+
return { kind: 'ctrl-c-cancel', consumes: true };
|
|
128
|
+
}
|
|
129
|
+
if (isDoublePress(state.lastCtrlCPress, now))
|
|
130
|
+
return { kind: 'ctrl-c-quit', consumes: true };
|
|
131
|
+
return { kind: 'ctrl-c-clear', consumes: true };
|
|
132
|
+
}
|
|
133
|
+
/** Ctrl+D — pi app.exit: quit only on an empty editor, else let it delete. */
|
|
134
|
+
function resolveCtrlD(state) {
|
|
135
|
+
if (state.overlayOpen)
|
|
136
|
+
return { kind: 'overlay', consumes: false };
|
|
137
|
+
if (state.editorHasText)
|
|
138
|
+
return { kind: 'noop', consumes: false };
|
|
139
|
+
return { kind: 'ctrl-d-quit', consumes: true };
|
|
140
|
+
}
|
|
141
|
+
/** Ctrl+L — pi app.model.select: open the model picker (overlay yields). */
|
|
142
|
+
function resolveModelPicker(state) {
|
|
143
|
+
if (state.overlayOpen)
|
|
144
|
+
return { kind: 'overlay', consumes: false };
|
|
145
|
+
return { kind: 'model-picker', consumes: true };
|
|
146
|
+
}
|
|
147
|
+
/** Ctrl+G — open the subagent picker only while children run (overlay yields). */
|
|
148
|
+
function resolveSubagentViewer(state) {
|
|
149
|
+
if (state.overlayOpen)
|
|
150
|
+
return { kind: 'overlay', consumes: false };
|
|
151
|
+
if (state.runningAgents > 0)
|
|
152
|
+
return { kind: 'subagent-viewer', consumes: true };
|
|
153
|
+
// No subagents to watch: let the key fall through to the focused component.
|
|
154
|
+
return { kind: 'noop', consumes: false };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolve one raw terminal input sequence into an app-level action.
|
|
158
|
+
* `data` is the single decoded key sequence `addInputListener` receives
|
|
159
|
+
* (e.g. "\x1b" for Escape, "\x03" for Ctrl+C); see keys.js for the
|
|
160
|
+
* normalization: Escape is `\x1b`, and legacy Ctrl+[ collapses to the same
|
|
161
|
+
* byte, so both are recognized as `escape`.
|
|
162
|
+
*
|
|
163
|
+
* `keys` may be a PARTIAL override — every missing binding falls back to the
|
|
164
|
+
* default. That is the shape a future `~/.dsh/keybindings.json` loader will
|
|
165
|
+
* feed in: remap one key, keep the rest.
|
|
166
|
+
*
|
|
167
|
+
* Any key outside the app bindings yields `noop` (not consumed — the focused
|
|
168
|
+
* component, normally the editor, processes it).
|
|
169
|
+
*/
|
|
170
|
+
export function resolveKeyAction(data, state, now, keys = DEFAULT_KEYBINDINGS) {
|
|
171
|
+
const bindings = { ...DEFAULT_KEYBINDINGS };
|
|
172
|
+
for (const id of Object.keys(keys)) {
|
|
173
|
+
const value = keys[id];
|
|
174
|
+
if (value !== undefined)
|
|
175
|
+
bindings[id] = value;
|
|
176
|
+
}
|
|
177
|
+
if (matchesKey(data, bindings.escape))
|
|
178
|
+
return resolveEscape(state, now);
|
|
179
|
+
if (matchesKey(data, bindings.ctrlC))
|
|
180
|
+
return resolveCtrlC(state, now);
|
|
181
|
+
if (matchesKey(data, bindings.ctrlD))
|
|
182
|
+
return resolveCtrlD(state);
|
|
183
|
+
if (matchesKey(data, bindings.modelPicker))
|
|
184
|
+
return resolveModelPicker(state);
|
|
185
|
+
if (matchesKey(data, bindings.subagentViewer))
|
|
186
|
+
return resolveSubagentViewer(state);
|
|
187
|
+
return { kind: 'noop', consumes: false };
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=keymap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keymap.js","sourceRoot":"","sources":["../src/keymap.ts"],"names":[],"mappings":"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"}
|
package/lib/live-widgets.d.ts
CHANGED
|
@@ -9,13 +9,12 @@
|
|
|
9
9
|
* renders zero rows while empty and grows to its content while the model
|
|
10
10
|
* has todos.
|
|
11
11
|
* - `activityDoc` — the lastRequest container below the editor
|
|
12
|
-
* (`ui.lastRequest`): the `
|
|
12
|
+
* (`ui.lastRequest`): the ` ● <last request>` line (persisting across
|
|
13
13
|
* agent churn) followed by one compact line PER RUNNING agent. NO box
|
|
14
|
-
* chrome, NO `● Agents` header, NO
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Auto height — it
|
|
14
|
+
* chrome, NO `● Agents` header, NO provider — just
|
|
15
|
+
* `├─ ⠋ <name> · ↻N≤M · 21k/1m · 13.6s · <latest output>`, the `├─ ` /
|
|
16
|
+
* `└─ ` prefix in the same column as the todo rows and the request ` ● `
|
|
17
|
+
* (the last running agent closes the list with `└─ `). Auto height — it
|
|
19
18
|
* collapses to zero rows when both the last-request line is cleared and no
|
|
20
19
|
* agent runs.
|
|
21
20
|
*
|
|
@@ -37,6 +36,11 @@ import { type TuiTheme } from './theme/index.ts';
|
|
|
37
36
|
export declare const AGENT_TICK_MS = 100;
|
|
38
37
|
/** Braille spinner cycle; the frame index advances once per tick. */
|
|
39
38
|
export declare const AGENT_SPINNER_FRAMES: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
39
|
+
/**
|
|
40
|
+
* Compact human-readable size: 1_500_000 → `1.5m`, 20_965 → `20k`, 999 → `999`.
|
|
41
|
+
* Millions keep one decimal (a trailing `.0` dropped), thousands floor to `k`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function fmtCompact(n: number): string;
|
|
40
44
|
export declare class LiveWidgets {
|
|
41
45
|
private readonly todosDoc;
|
|
42
46
|
private readonly activityDoc;
|
|
@@ -50,7 +54,7 @@ export declare class LiveWidgets {
|
|
|
50
54
|
private todosText;
|
|
51
55
|
/** The merged running-agent Text in `activityDoc`, replaced on rebuild. */
|
|
52
56
|
private agentsText;
|
|
53
|
-
/** The `
|
|
57
|
+
/** The ` ● <last request>` Text in `activityDoc` (persists across churn). */
|
|
54
58
|
private requestText;
|
|
55
59
|
/** Plain clipped last-request text backing `requestText` (for setTheme). */
|
|
56
60
|
private requestDisplay;
|
|
@@ -59,12 +63,12 @@ export declare class LiveWidgets {
|
|
|
59
63
|
/**
|
|
60
64
|
* @param todosDoc The dock slot ABOVE the chat input — the Todos boxed
|
|
61
65
|
* panel lives here.
|
|
62
|
-
* @param activityDoc The lastRequest container BELOW the editor — the `
|
|
66
|
+
* @param activityDoc The lastRequest container BELOW the editor — the ` ● `
|
|
63
67
|
* last-request line plus the compact running-agent lines live here.
|
|
64
68
|
*/
|
|
65
69
|
constructor(todosDoc: Container, activityDoc: Container, theme: TuiTheme, requestRender: () => void);
|
|
66
70
|
/**
|
|
67
|
-
* Render the `
|
|
71
|
+
* Render the ` ● <text>` last-request line in `activityDoc`, UNDER which the
|
|
68
72
|
* running-agent lines appear. `undefined` (or blank) removes the line. Same
|
|
69
73
|
* styling as before the merge: `fgMuted` prefix, but the text is clipped to
|
|
70
74
|
* the terminal width (`columns - 5`) so it ALWAYS renders on one row and
|
|
@@ -84,21 +88,21 @@ export declare class LiveWidgets {
|
|
|
84
88
|
* — the activity area then holds its final (empty) state.
|
|
85
89
|
*/
|
|
86
90
|
tickLive(): void;
|
|
87
|
-
/** Recolor the widget (Todos panel,
|
|
91
|
+
/** Recolor the widget (Todos panel, ● line and agent lines) under a theme
|
|
88
92
|
* hot-switch (no-op on the same bundle). */
|
|
89
93
|
setTheme(theme: TuiTheme): void;
|
|
90
94
|
/**
|
|
91
95
|
* Drop everything except the last-request echo (`/new`): clears the Todos
|
|
92
|
-
* panel and the running-agent lines, keeps the `
|
|
96
|
+
* panel and the running-agent lines, keeps the ` ● ` line. The bridge also
|
|
93
97
|
* fires `renderAgents([])`.
|
|
94
98
|
*/
|
|
95
99
|
clear(): void;
|
|
96
100
|
/**
|
|
97
101
|
* Rebuild the two live surfaces:
|
|
98
102
|
* - `todosDoc`: one bordered Todos panel (or nothing when all completed).
|
|
99
|
-
* - `activityDoc`: the `
|
|
103
|
+
* - `activityDoc`: the ` ● ` line (managed separately, persists) followed
|
|
100
104
|
* by ONE Text holding the compact running-agent lines joined by '\n'
|
|
101
|
-
* (or nothing when no agent runs — the slot collapses to the
|
|
105
|
+
* (or nothing when no agent runs — the slot collapses to the ● line).
|
|
102
106
|
* Clear-when-done: the Todos panel hides once every todo is completed (the
|
|
103
107
|
* model writes the whole-list snapshot and rarely clears it — an
|
|
104
108
|
* all-completed list is the end-of-work signal); the agent lines hide once
|
|
@@ -117,17 +121,18 @@ export declare class LiveWidgets {
|
|
|
117
121
|
*/
|
|
118
122
|
private todoLines;
|
|
119
123
|
/**
|
|
120
|
-
* One compact running-agent line for the last-request area:
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* the
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
124
|
+
* One compact running-agent line for the last-request area, todo-style:
|
|
125
|
+
* `├─ `/`└─ ` connector (same column as the todo rows and the request
|
|
126
|
+
* ` ● `) + spinner + the agent NAME (`view.label`, matching the boxed
|
|
127
|
+
* panel's main line) + the exact meta that main line showed (`↻retries≤max`,
|
|
128
|
+
* compact `tokens[/contextWindow]`, elapsed) + the child's latest output
|
|
129
|
+
* line when one exists (` · <tail>`, so the user sees it is alive). NO box
|
|
130
|
+
* chrome, NO provider — the name is the prominent element; the last running
|
|
131
|
+
* agent closes the list with `└─ `. The label is the only unbounded field;
|
|
132
|
+
* it is clipped against a budget measured from the terminal width so the
|
|
133
|
+
* whole plain line (prefix included) fits, then the assembled plain line is
|
|
134
|
+
* clipped to the terminal width BEFORE any ANSI is applied (clipToWidth
|
|
135
|
+
* counts SGR fragments as visible columns — style last).
|
|
131
136
|
*/
|
|
132
137
|
private compactAgentLine;
|
|
133
138
|
}
|