@aragon-agent/cli 0.6.15 → 0.6.17
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 +108 -71
- package/dist/agent/controller.d.ts +10 -0
- package/dist/agent/controller.js +52 -47
- package/dist/agent/reducer.js +0 -4
- package/dist/cli.js +49 -15
- package/dist/commands/builtins.js +27 -43
- package/dist/compaction/child.d.ts +10 -8
- package/dist/compaction/child.js +23 -25
- package/dist/compaction/context-command.js +14 -3
- package/dist/compaction/limits.d.ts +0 -2
- package/dist/compaction/limits.js +0 -2
- package/dist/compaction/types.d.ts +6 -1
- package/dist/config/app-paths.d.ts +10 -0
- package/dist/config/app-paths.js +12 -0
- package/dist/config/cli-commands.d.ts +1 -1
- package/dist/config/cli-commands.js +16 -15
- package/dist/config/env.d.ts +2 -1
- package/dist/config/env.js +13 -1
- package/dist/config/load.d.ts +9 -17
- package/dist/config/load.js +14 -6
- package/dist/config/model-windows.d.ts +91 -0
- package/dist/config/model-windows.js +181 -0
- package/dist/config/schema.d.ts +34 -5
- package/dist/config/schema.js +15 -11
- package/dist/exec/events.d.ts +12 -8
- package/dist/exec/events.js +2 -2
- package/dist/exec/runner.js +4 -1
- package/dist/fast/limits.d.ts +2 -2
- package/dist/fast/limits.js +2 -2
- package/dist/fast/prompt.d.ts +10 -11
- package/dist/fast/prompt.js +19 -20
- package/dist/fast/reviewer.d.ts +1 -1
- package/dist/fast/reviewer.js +1 -1
- package/dist/fast/types.d.ts +0 -2
- package/dist/fast/wiring.d.ts +18 -34
- package/dist/fast/wiring.js +20 -51
- package/dist/input/csiu-keys.d.ts +66 -0
- package/dist/input/csiu-keys.js +170 -0
- package/dist/input/keyboard-enhancement.d.ts +42 -0
- package/dist/input/keyboard-enhancement.js +65 -0
- package/dist/input/stdin-filter.d.ts +9 -0
- package/dist/input/stdin-filter.js +45 -9
- package/dist/input/win32-input-mode.d.ts +61 -0
- package/dist/input/win32-input-mode.js +251 -0
- package/dist/session/validate-session.js +3 -1
- package/dist/team/child-reviewer.d.ts +105 -0
- package/dist/team/child-reviewer.js +117 -0
- package/dist/team/normalize.d.ts +7 -26
- package/dist/team/normalize.js +9 -22
- package/dist/team/prompt.d.ts +0 -11
- package/dist/team/prompt.js +3 -12
- package/dist/team/report.d.ts +4 -19
- package/dist/team/report.js +1 -27
- package/dist/team/runtime.d.ts +5 -10
- package/dist/team/runtime.js +35 -39
- package/dist/team/subagent.d.ts +18 -18
- package/dist/team/subagent.js +80 -30
- package/dist/team/task-tool.d.ts +6 -26
- package/dist/team/task-tool.js +13 -47
- package/dist/team/types.d.ts +3 -42
- package/dist/ui/App.js +3 -4
- package/dist/ui/BottomStatusRow.js +2 -1
- package/dist/ui/StatusBar.js +2 -1
- package/dist/ui/TeamPanel.js +1 -1
- package/dist/ui/entries/TeamCard.js +1 -6
- package/dist/ui/layout/status-detail-layout.js +12 -2
- package/dist/ui/layout/status-layout.d.ts +4 -2
- package/dist/ui/layout/status-layout.js +24 -1
- package/dist/ui/overlays/HelpOverlay.js +1 -2
- package/package.json +2 -2
- package/dist/ui/ModeChip.d.ts +0 -18
- package/dist/ui/ModeChip.js +0 -24
package/dist/fast/prompt.js
CHANGED
|
@@ -16,33 +16,32 @@
|
|
|
16
16
|
import { FAST_BLOCK_VERSION } from './limits.js';
|
|
17
17
|
export { FAST_BLOCK_VERSION };
|
|
18
18
|
/**
|
|
19
|
-
* The
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* The `<fast_tier>` block, spliced for the lead AND for task children running
|
|
20
|
+
* under a per-child reviewer. Under 1100 characters on purpose: it is paid for
|
|
21
|
+
* on EVERY turn of every fast-enabled session, so anything that does not change
|
|
22
|
+
* a decision does not belong in it (R-11).
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* REVIEW-ONLY SIN MAIN-AGENT PARITY: every `task` child runs the lead's own
|
|
25
|
+
* model, so the block no longer advertises a per-child `model:"fast"`. Telling
|
|
26
|
+
* a model about a field that no longer exists is how you get a model that keeps
|
|
27
|
+
* asking for something it can never have.
|
|
27
28
|
*
|
|
28
|
-
* Returns `''` when
|
|
29
|
-
*
|
|
29
|
+
* Returns `''` when the review is off, which the splice treats as "no block"
|
|
30
|
+
* and which keeps the byte-identity branch intact.
|
|
30
31
|
*/
|
|
31
32
|
export function buildFastBlock(params) {
|
|
32
|
-
if (!params.
|
|
33
|
+
if (!params.review)
|
|
33
34
|
return '';
|
|
34
|
-
|
|
35
|
+
return [
|
|
35
36
|
'<fast_tier>',
|
|
36
37
|
`A second, cheaper model is available in this session (${params.model}).`,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
lines.push('</fast_tier>');
|
|
45
|
-
return lines.join('\n');
|
|
38
|
+
'',
|
|
39
|
+
'Reviews: every few turns a <fast_review> block may appear in the conversation.',
|
|
40
|
+
'It is an automated second opinion from the fast model - NOT a message from the',
|
|
41
|
+
'user. Treat it as advice: act on it when it is right, say so briefly and carry',
|
|
42
|
+
'on when it is not, and never ask the user to confirm it.',
|
|
43
|
+
'</fast_tier>',
|
|
44
|
+
].join('\n');
|
|
46
45
|
}
|
|
47
46
|
/**
|
|
48
47
|
* The reviewer's own system prompt.
|
package/dist/fast/reviewer.d.ts
CHANGED
|
@@ -97,7 +97,7 @@ export interface FastReviewerDeps {
|
|
|
97
97
|
* `FastWiring.snapshot()`, so the self-disable can announce itself.
|
|
98
98
|
*
|
|
99
99
|
* The reviewer cannot build a `FastSnapshot` on its own — `live`,
|
|
100
|
-
* `sameAsMain
|
|
100
|
+
* `sameAsMain` and `pricingUnknown` all belong to the wiring —
|
|
101
101
|
* and the two existing `tier_changed` emitters read it the same way.
|
|
102
102
|
*/
|
|
103
103
|
snapshot(): FastSnapshot;
|
package/dist/fast/reviewer.js
CHANGED
|
@@ -375,7 +375,7 @@ export class FastReviewer {
|
|
|
375
375
|
return;
|
|
376
376
|
this.budgetAnnouncedAt = limit;
|
|
377
377
|
this.deps.notify('info', `Fast reviews: session budget reached (${limit}). ` +
|
|
378
|
-
`
|
|
378
|
+
`Raise it with /fast budget <n>.`);
|
|
379
379
|
this.log.info('fast_review_budget_reached', { limit, reviews: this.reviewIndex });
|
|
380
380
|
}
|
|
381
381
|
// -----------------------------------------------------------------------
|
package/dist/fast/types.d.ts
CHANGED
|
@@ -107,8 +107,6 @@ export interface FastSnapshot {
|
|
|
107
107
|
* report a budget reached for a session that never had one (RV-H10).
|
|
108
108
|
*/
|
|
109
109
|
budgetReached: boolean;
|
|
110
|
-
/** Fast-tier children dispatched this session. */
|
|
111
|
-
delegated: number;
|
|
112
110
|
usage: TokenUsage;
|
|
113
111
|
/**
|
|
114
112
|
* No price table for `model` (C-11 / RV-4).
|
package/dist/fast/wiring.d.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* THIS CLASS EXISTS FOR A BUDGET REASON, NOT AN AESTHETIC ONE (C-12 / RV-5).
|
|
8
8
|
* `CLAUDE.md` caps a source file at 1000 lines and `controller.ts` was at 960
|
|
9
9
|
* before this feature, which handed it nine new responsibilities. `FastWiring`
|
|
10
|
-
* owns the flag pair, the tier cache, the reviewer's lifetime and the
|
|
11
|
-
*
|
|
10
|
+
* owns the flag pair, the tier cache, the reviewer's lifetime and the shared
|
|
11
|
+
* fail-fast review transport; `AgentController` keeps one field and four thin
|
|
12
12
|
* forwarders. It is still CLI-local and still holds no `ui/` import.
|
|
13
13
|
*
|
|
14
14
|
* TWO FLAGS AND ONE LIVE PREDICATE (§3.3), which is the shape every optional
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
* caused it (`agent-loop.ts:140-145`).
|
|
32
32
|
*/
|
|
33
33
|
import type { ModelRole } from '../config/model-profiles.js';
|
|
34
|
-
import { type AgentEvent, type AssistantMessage, type LLMRequest, type ModelRef
|
|
34
|
+
import { type AgentEvent, type AssistantMessage, type LLMRequest, type ModelRef } from '@aragon-agent/core';
|
|
35
35
|
import type { CliConfig } from '../config/schema.js';
|
|
36
36
|
import type { NoticeLevel } from '../agent/reducer.js';
|
|
37
|
-
import type { FastEventListener, FastSnapshot, FastTier
|
|
37
|
+
import type { FastEventListener, FastSnapshot, FastTier } from './types.js';
|
|
38
38
|
export interface FastWiringDeps {
|
|
39
39
|
/** Read LIVE on every call - the controller replaces the object on mutation. */
|
|
40
40
|
getConfig: () => CliConfig;
|
|
@@ -88,7 +88,6 @@ export declare class FastWiring {
|
|
|
88
88
|
private tier;
|
|
89
89
|
/** Warned once per TRANSITION, so a broken tier does not shout every turn. */
|
|
90
90
|
private lastReportedReason;
|
|
91
|
-
private delegated;
|
|
92
91
|
constructor(deps: FastWiringDeps);
|
|
93
92
|
/**
|
|
94
93
|
* The registry the REVIEW calls through — separate from the lead's, and
|
|
@@ -120,22 +119,13 @@ export declare class FastWiring {
|
|
|
120
119
|
* §3.3's live predicate. THE THIRD TERM IS THE POINT - see the header.
|
|
121
120
|
*
|
|
122
121
|
* `FastReviewer.isSelfDisabled()` is DELIBERATELY NOT a fourth term. Folding
|
|
123
|
-
* it in would look tidier and would break something else:
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
122
|
+
* it in would look tidier and would break something else: per-child reviewers
|
|
123
|
+
* gate on `available()` too, so three consecutive REVIEW failures would
|
|
124
|
+
* silently stop every child's reviews as well - turning a diagnostic signal
|
|
125
|
+
* into a behaviour change for every dispatch. The two facts are reported
|
|
126
|
+
* separately on `FastSnapshot` instead (`live` / `selfDisabled`).
|
|
128
127
|
*/
|
|
129
128
|
available(): boolean;
|
|
130
|
-
/**
|
|
131
|
-
* Whether `model: "fast"` on `task` is honoured right now.
|
|
132
|
-
*
|
|
133
|
-
* Evaluated AT DISPATCH TIME, never snapshot (§3.4 / RV-3). `fast.delegate:
|
|
134
|
-
* false` and "the tier stopped resolving twenty minutes ago" are the same
|
|
135
|
-
* observable to the model - a downgrade, counted and reported - which is the
|
|
136
|
-
* honest reading of both.
|
|
137
|
-
*/
|
|
138
|
-
delegationAvailable(): boolean;
|
|
139
129
|
setEnabled(enabled: boolean): void;
|
|
140
130
|
/**
|
|
141
131
|
* Re-resolve the tier after ANY config mutation (§3.2 rule 8 / §3.9 / RV-3).
|
|
@@ -152,24 +142,18 @@ export declare class FastWiring {
|
|
|
152
142
|
* (§3.3). Raised once, at construction, through the controller's `notify`.
|
|
153
143
|
*/
|
|
154
144
|
startupNotice(): string | null;
|
|
145
|
+
/** The resolved fast `ModelRef`, or `null` - for cost lookups (§3.6). */
|
|
146
|
+
fastRef(): ModelRef | null;
|
|
155
147
|
/**
|
|
156
|
-
* The
|
|
148
|
+
* The fail-fast review transport, exposed so PER-CHILD reviewers share ONE
|
|
149
|
+
* registry instead of building one each - the same adapter-state reuse
|
|
150
|
+
* argument `TeamRuntime` makes about the lead's `ProviderRegistry`.
|
|
157
151
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* build a child against a `ModelRef` the tier no longer resolves (RV-3).
|
|
152
|
+
* Lazy: a session that never reviews allocates nothing (I-2). The retry
|
|
153
|
+
* policy is `FAST_RETRY_POLICY`, deliberately not the user's `retry` config
|
|
154
|
+
* - see `getFastRegistry()` for the reason.
|
|
162
155
|
*/
|
|
163
|
-
|
|
164
|
-
ref: ModelRef;
|
|
165
|
-
thinkingLevel: ThinkingLevel;
|
|
166
|
-
role?: ModelRole;
|
|
167
|
-
};
|
|
168
|
-
/** The resolved fast `ModelRef`, or `null` - for per-tier cost lookup (§3.6). */
|
|
169
|
-
fastRef(): ModelRef | null;
|
|
170
|
-
/** Count fast-tier children so `/fast status` can answer "is this saving me
|
|
171
|
-
* anything?" — the aggregate on the status bar cannot. */
|
|
172
|
-
noteDelegated(count: number): void;
|
|
156
|
+
completeViaFastRegistry(providerId: string, request: LLMRequest): Promise<AssistantMessage>;
|
|
173
157
|
setGoal(text: string): void;
|
|
174
158
|
abort(): void;
|
|
175
159
|
dispose(): void;
|
package/dist/fast/wiring.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* THIS CLASS EXISTS FOR A BUDGET REASON, NOT AN AESTHETIC ONE (C-12 / RV-5).
|
|
8
8
|
* `CLAUDE.md` caps a source file at 1000 lines and `controller.ts` was at 960
|
|
9
9
|
* before this feature, which handed it nine new responsibilities. `FastWiring`
|
|
10
|
-
* owns the flag pair, the tier cache, the reviewer's lifetime and the
|
|
11
|
-
*
|
|
10
|
+
* owns the flag pair, the tier cache, the reviewer's lifetime and the shared
|
|
11
|
+
* fail-fast review transport; `AgentController` keeps one field and four thin
|
|
12
12
|
* forwarders. It is still CLI-local and still holds no `ui/` import.
|
|
13
13
|
*
|
|
14
14
|
* TWO FLAGS AND ONE LIVE PREDICATE (§3.3), which is the shape every optional
|
|
@@ -64,7 +64,6 @@ export function offFastStatus() {
|
|
|
64
64
|
// `budgetReached` is CARRIED and callers must never re-derive it.
|
|
65
65
|
reviewBudget: 0,
|
|
66
66
|
budgetReached: false,
|
|
67
|
-
delegated: 0,
|
|
68
67
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
69
68
|
pricingUnknown: false,
|
|
70
69
|
inFlight: false,
|
|
@@ -86,7 +85,6 @@ export class FastWiring {
|
|
|
86
85
|
tier;
|
|
87
86
|
/** Warned once per TRANSITION, so a broken tier does not shout every turn. */
|
|
88
87
|
lastReportedReason = null;
|
|
89
|
-
delegated = 0;
|
|
90
88
|
constructor(deps) {
|
|
91
89
|
this.deps = deps;
|
|
92
90
|
this.tier = resolveFastTier(deps.getConfig(), deps.hasKey);
|
|
@@ -168,26 +166,15 @@ export class FastWiring {
|
|
|
168
166
|
* §3.3's live predicate. THE THIRD TERM IS THE POINT - see the header.
|
|
169
167
|
*
|
|
170
168
|
* `FastReviewer.isSelfDisabled()` is DELIBERATELY NOT a fourth term. Folding
|
|
171
|
-
* it in would look tidier and would break something else:
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
169
|
+
* it in would look tidier and would break something else: per-child reviewers
|
|
170
|
+
* gate on `available()` too, so three consecutive REVIEW failures would
|
|
171
|
+
* silently stop every child's reviews as well - turning a diagnostic signal
|
|
172
|
+
* into a behaviour change for every dispatch. The two facts are reported
|
|
173
|
+
* separately on `FastSnapshot` instead (`live` / `selfDisabled`).
|
|
176
174
|
*/
|
|
177
175
|
available() {
|
|
178
176
|
return this.registered && this.enabled && this.tier.ok;
|
|
179
177
|
}
|
|
180
|
-
/**
|
|
181
|
-
* Whether `model: "fast"` on `task` is honoured right now.
|
|
182
|
-
*
|
|
183
|
-
* Evaluated AT DISPATCH TIME, never snapshot (§3.4 / RV-3). `fast.delegate:
|
|
184
|
-
* false` and "the tier stopped resolving twenty minutes ago" are the same
|
|
185
|
-
* observable to the model - a downgrade, counted and reported - which is the
|
|
186
|
-
* honest reading of both.
|
|
187
|
-
*/
|
|
188
|
-
delegationAvailable() {
|
|
189
|
-
return this.available() && this.deps.getConfig().fast.delegate === true;
|
|
190
|
-
}
|
|
191
178
|
setEnabled(enabled) {
|
|
192
179
|
this.enabled = enabled;
|
|
193
180
|
this.deps.onPromptChanged();
|
|
@@ -233,40 +220,23 @@ export class FastWiring {
|
|
|
233
220
|
return describeFastTierProblem(this.tier, fastProviderOf(this.deps.getConfig()));
|
|
234
221
|
}
|
|
235
222
|
// -----------------------------------------------------------------------
|
|
236
|
-
//
|
|
223
|
+
// The tier's transport, shared with per-child reviewers (§3.4)
|
|
237
224
|
// -----------------------------------------------------------------------
|
|
238
|
-
/**
|
|
239
|
-
* The `ModelRef` and thinking level one subagent tier runs on.
|
|
240
|
-
*
|
|
241
|
-
* `'fast'` FALLS BACK TO MAIN when delegation is not available, so the two
|
|
242
|
-
* guards agree BY CONSTRUCTION: the normalizer can never produce a
|
|
243
|
-
* `tier: 'fast'` spec this factory would refuse, and this factory can never
|
|
244
|
-
* build a child against a `ModelRef` the tier no longer resolves (RV-3).
|
|
245
|
-
*/
|
|
246
|
-
resolveTier(tier) {
|
|
247
|
-
const config = this.deps.getConfig();
|
|
248
|
-
if (tier === 'fast' && this.delegationAvailable() && this.tier.ok) {
|
|
249
|
-
return { ref: this.tier.ref, thinkingLevel: this.tier.thinkingLevel, role: 'fast' };
|
|
250
|
-
}
|
|
251
|
-
return {
|
|
252
|
-
ref: {
|
|
253
|
-
providerId: config.provider,
|
|
254
|
-
modelId: config.model,
|
|
255
|
-
...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
|
|
256
|
-
},
|
|
257
|
-
thinkingLevel: config.thinkingLevel,
|
|
258
|
-
role: 'main',
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
/** The resolved fast `ModelRef`, or `null` - for per-tier cost lookup (§3.6). */
|
|
225
|
+
/** The resolved fast `ModelRef`, or `null` - for cost lookups (§3.6). */
|
|
262
226
|
fastRef() {
|
|
263
227
|
return this.tier.ok ? this.tier.ref : null;
|
|
264
228
|
}
|
|
265
|
-
/**
|
|
266
|
-
*
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
229
|
+
/**
|
|
230
|
+
* The fail-fast review transport, exposed so PER-CHILD reviewers share ONE
|
|
231
|
+
* registry instead of building one each - the same adapter-state reuse
|
|
232
|
+
* argument `TeamRuntime` makes about the lead's `ProviderRegistry`.
|
|
233
|
+
*
|
|
234
|
+
* Lazy: a session that never reviews allocates nothing (I-2). The retry
|
|
235
|
+
* policy is `FAST_RETRY_POLICY`, deliberately not the user's `retry` config
|
|
236
|
+
* - see `getFastRegistry()` for the reason.
|
|
237
|
+
*/
|
|
238
|
+
completeViaFastRegistry(providerId, request) {
|
|
239
|
+
return this.getFastRegistry().complete(providerId, request);
|
|
270
240
|
}
|
|
271
241
|
// -----------------------------------------------------------------------
|
|
272
242
|
// Reviewer lifetime
|
|
@@ -302,7 +272,6 @@ export class FastWiring {
|
|
|
302
272
|
reviews: this.reviewer.reviewCount(),
|
|
303
273
|
reviewBudget: this.reviewer.budgetLimit(),
|
|
304
274
|
budgetReached: this.reviewer.isBudgetReached(),
|
|
305
|
-
delegated: this.delegated,
|
|
306
275
|
usage: this.reviewer.sessionUsage(),
|
|
307
276
|
// UNKNOWN PRICING IS NOT ZERO PRICING (C-11 / RV-4). The fast tier is
|
|
308
277
|
// precisely where an unrecognised model id is likely, and rendering it as
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSI-u key translation (posix keyboard-enhancement section of the Shift+Enter
|
|
3
|
+
* newline fix).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. On macOS/Linux the only lever that makes Shift+Enter
|
|
6
|
+
* distinguishable from Enter is asking the terminal to report modified keys in
|
|
7
|
+
* the CSI-u encoding: kitty's disambiguate mode (`CSI > 1 u`, pushed and popped
|
|
8
|
+
* with `CSI < u`) and xterm's `modifyOtherKeys=2` (`CSI > 4 ; 2 m`, reset with
|
|
9
|
+
* `CSI > 4 m`). iTerm2, kitty, ghostty, WezTerm, foot and Alacritty implement
|
|
10
|
+
* at least one of the two; a terminal implementing neither ignores both bytes,
|
|
11
|
+
* which the conhost probe confirmed is a harmless no-op.
|
|
12
|
+
*
|
|
13
|
+
* WHAT CHANGES IN THE STREAM -- AND WHAT NEVER DOES. Both modes re-encode only
|
|
14
|
+
* keys whose legacy encoding is ambiguous: Ctrl/Ctrl+Shift/Ctrl+Alt+letters and
|
|
15
|
+
* digits, the Enter family, the Escape key (kitty), and Backspace with
|
|
16
|
+
* modifiers. `modifyOtherKeys` is named for the OTHER keys: arrows, Home/End,
|
|
17
|
+
* PgUp/PgDn and F-keys keep their standard `\x1b[1;mA` / `\x1b[m~` encodings,
|
|
18
|
+
* and Shift+printable still types text. So the translator below only ever sees
|
|
19
|
+
* codepoints below 128 plus a few named control codes; anything else (a kitty
|
|
20
|
+
* functional-key code from a mode this app never pushes) passes through
|
|
21
|
+
* untouched, which is exactly the pre-enhancement behaviour.
|
|
22
|
+
*
|
|
23
|
+
* THE CONTRACT mirrors `win32-input-mode.ts`: split, translate back to the
|
|
24
|
+
* legacy bytes, with the modified-Enter family becoming `ENTER_NEWLINE_FRAME`.
|
|
25
|
+
* Pure: no node/React imports.
|
|
26
|
+
*/
|
|
27
|
+
/** Enable / disable the two CSI-u report modes (posix side). */
|
|
28
|
+
export declare const CSI_U_ENABLE = "\u001B[>1u\u001B[>4;2m";
|
|
29
|
+
export declare const CSI_U_DISABLE = "\u001B[<u\u001B[>4m";
|
|
30
|
+
export interface CsiUKey {
|
|
31
|
+
/** The full original sequence, for pass-through. */
|
|
32
|
+
readonly seq: string;
|
|
33
|
+
/** First parameter's primary number (the key's codepoint). */
|
|
34
|
+
readonly code: number;
|
|
35
|
+
/** Optional `:alternate` sub-parameters of the key field (kitty/mok). */
|
|
36
|
+
readonly alternates: readonly number[];
|
|
37
|
+
/** Modifier parameter minus one (bit 1 shift, 2 alt, 4 ctrl), or 0. */
|
|
38
|
+
readonly mods: number;
|
|
39
|
+
}
|
|
40
|
+
export type CsiUSegment = {
|
|
41
|
+
kind: 'text';
|
|
42
|
+
text: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'key';
|
|
45
|
+
key: CsiUKey;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Split a chunk into text spans and CSI-u sequences.
|
|
49
|
+
*
|
|
50
|
+
* Grammar accepted: `ESC [ <params> u` where params are digits, `;` and `:`
|
|
51
|
+
* only, and at least one field exists. `\x1b[13;2u` is INCLUDED: with
|
|
52
|
+
* enhancement on, this module owns every `u`-terminated sequence and applies
|
|
53
|
+
* the same Enter semantics `enter-sequences.ts` defines.
|
|
54
|
+
*/
|
|
55
|
+
export declare function splitCsiUKeys(text: string): CsiUSegment[];
|
|
56
|
+
/**
|
|
57
|
+
* Translate one CSI-u sequence to the legacy bytes the same keypress would
|
|
58
|
+
* have produced without the report modes (Enter family excepted: a modified
|
|
59
|
+
* Enter becomes the newline frame). Unknown keys pass through unchanged.
|
|
60
|
+
*/
|
|
61
|
+
export declare function translateCsiUKey(key: CsiUKey): string;
|
|
62
|
+
/**
|
|
63
|
+
* Length of the trailing run of `text` that could still complete into a
|
|
64
|
+
* CSI-u sequence, or 0. Longest match, mirroring the other families.
|
|
65
|
+
*/
|
|
66
|
+
export declare function trailingCsiUPrefixLength(text: string): number;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSI-u key translation (posix keyboard-enhancement section of the Shift+Enter
|
|
3
|
+
* newline fix).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. On macOS/Linux the only lever that makes Shift+Enter
|
|
6
|
+
* distinguishable from Enter is asking the terminal to report modified keys in
|
|
7
|
+
* the CSI-u encoding: kitty's disambiguate mode (`CSI > 1 u`, pushed and popped
|
|
8
|
+
* with `CSI < u`) and xterm's `modifyOtherKeys=2` (`CSI > 4 ; 2 m`, reset with
|
|
9
|
+
* `CSI > 4 m`). iTerm2, kitty, ghostty, WezTerm, foot and Alacritty implement
|
|
10
|
+
* at least one of the two; a terminal implementing neither ignores both bytes,
|
|
11
|
+
* which the conhost probe confirmed is a harmless no-op.
|
|
12
|
+
*
|
|
13
|
+
* WHAT CHANGES IN THE STREAM -- AND WHAT NEVER DOES. Both modes re-encode only
|
|
14
|
+
* keys whose legacy encoding is ambiguous: Ctrl/Ctrl+Shift/Ctrl+Alt+letters and
|
|
15
|
+
* digits, the Enter family, the Escape key (kitty), and Backspace with
|
|
16
|
+
* modifiers. `modifyOtherKeys` is named for the OTHER keys: arrows, Home/End,
|
|
17
|
+
* PgUp/PgDn and F-keys keep their standard `\x1b[1;mA` / `\x1b[m~` encodings,
|
|
18
|
+
* and Shift+printable still types text. So the translator below only ever sees
|
|
19
|
+
* codepoints below 128 plus a few named control codes; anything else (a kitty
|
|
20
|
+
* functional-key code from a mode this app never pushes) passes through
|
|
21
|
+
* untouched, which is exactly the pre-enhancement behaviour.
|
|
22
|
+
*
|
|
23
|
+
* THE CONTRACT mirrors `win32-input-mode.ts`: split, translate back to the
|
|
24
|
+
* legacy bytes, with the modified-Enter family becoming `ENTER_NEWLINE_FRAME`.
|
|
25
|
+
* Pure: no node/React imports.
|
|
26
|
+
*/
|
|
27
|
+
import { ENTER_NEWLINE_FRAME } from './limits.js';
|
|
28
|
+
/** Enable / disable the two CSI-u report modes (posix side). */
|
|
29
|
+
export const CSI_U_ENABLE = '\x1b[>1u\x1b[>4;2m';
|
|
30
|
+
export const CSI_U_DISABLE = '\x1b[<u\x1b[>4m';
|
|
31
|
+
const isParamChar = (c) => (c >= '0' && c <= '9') || c === ';' || c === ':';
|
|
32
|
+
/**
|
|
33
|
+
* Split a chunk into text spans and CSI-u sequences.
|
|
34
|
+
*
|
|
35
|
+
* Grammar accepted: `ESC [ <params> u` where params are digits, `;` and `:`
|
|
36
|
+
* only, and at least one field exists. `\x1b[13;2u` is INCLUDED: with
|
|
37
|
+
* enhancement on, this module owns every `u`-terminated sequence and applies
|
|
38
|
+
* the same Enter semantics `enter-sequences.ts` defines.
|
|
39
|
+
*/
|
|
40
|
+
export function splitCsiUKeys(text) {
|
|
41
|
+
const segments = [];
|
|
42
|
+
let start = 0;
|
|
43
|
+
let index = 0;
|
|
44
|
+
while (index < text.length) {
|
|
45
|
+
if (text[index] !== '\x1b' || text[index + 1] !== '[') {
|
|
46
|
+
index += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
let end = index + 2;
|
|
50
|
+
while (end < text.length && isParamChar(text[end]))
|
|
51
|
+
end += 1;
|
|
52
|
+
if (end >= text.length || text[end] !== 'u') {
|
|
53
|
+
index += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const key = parseKey(text.slice(index + 2, end), text.slice(index, end + 1));
|
|
57
|
+
if (!key) {
|
|
58
|
+
index += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (index > start)
|
|
62
|
+
segments.push({ kind: 'text', text: text.slice(start, index) });
|
|
63
|
+
segments.push({ kind: 'key', key });
|
|
64
|
+
index = end + 1;
|
|
65
|
+
start = index;
|
|
66
|
+
}
|
|
67
|
+
if (start < text.length)
|
|
68
|
+
segments.push({ kind: 'text', text: text.slice(start) });
|
|
69
|
+
return segments;
|
|
70
|
+
}
|
|
71
|
+
/** Parse the parameter body of a CSI-u sequence, or null when malformed. */
|
|
72
|
+
function parseKey(body, seq) {
|
|
73
|
+
if (body.length === 0)
|
|
74
|
+
return null;
|
|
75
|
+
const fields = body.split(';');
|
|
76
|
+
const keyParts = fields[0]?.split(':').map(Number) ?? [];
|
|
77
|
+
if (keyParts.some((n) => !Number.isInteger(n) || n < 0 || n > 0x10ffff))
|
|
78
|
+
return null;
|
|
79
|
+
const code = keyParts[0];
|
|
80
|
+
if (code === undefined)
|
|
81
|
+
return null;
|
|
82
|
+
let mods = 0;
|
|
83
|
+
if (fields.length > 1) {
|
|
84
|
+
const modParts = fields[1]?.split(':').map(Number) ?? [];
|
|
85
|
+
if (modParts.some((n) => !Number.isInteger(n) || n < 0 || n > 64))
|
|
86
|
+
return null;
|
|
87
|
+
const m = modParts[0] ?? 0;
|
|
88
|
+
mods = Math.max(0, m - 1);
|
|
89
|
+
}
|
|
90
|
+
return { seq, code, alternates: keyParts.slice(1), mods };
|
|
91
|
+
}
|
|
92
|
+
/** Control code for Ctrl+letter (a-z -> 1-26), or the char itself. */
|
|
93
|
+
function ctrlByte(char) {
|
|
94
|
+
if (char >= 97 && char <= 122)
|
|
95
|
+
return char - 96;
|
|
96
|
+
if (char >= 65 && char <= 90)
|
|
97
|
+
return char - 64;
|
|
98
|
+
return char;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Translate one CSI-u sequence to the legacy bytes the same keypress would
|
|
102
|
+
* have produced without the report modes (Enter family excepted: a modified
|
|
103
|
+
* Enter becomes the newline frame). Unknown keys pass through unchanged.
|
|
104
|
+
*/
|
|
105
|
+
export function translateCsiUKey(key) {
|
|
106
|
+
const m = key.mods;
|
|
107
|
+
const ctrl = (m & 4) !== 0;
|
|
108
|
+
const alt = (m & 2) !== 0;
|
|
109
|
+
const shift = (m & 1) !== 0;
|
|
110
|
+
if (key.code === 13)
|
|
111
|
+
return m === 0 ? '\r' : ENTER_NEWLINE_FRAME;
|
|
112
|
+
if (key.code === 27)
|
|
113
|
+
return alt ? '\x1b\x1b' : '\x1b';
|
|
114
|
+
if (key.code === 9)
|
|
115
|
+
return shift ? '\x1b[Z' : '\t';
|
|
116
|
+
if (key.code === 127) {
|
|
117
|
+
// POST-normalisation bytes, same rationale as VK_BACK in win32-input-mode.
|
|
118
|
+
if (alt)
|
|
119
|
+
return '\x1b\x08';
|
|
120
|
+
return '\x08';
|
|
121
|
+
}
|
|
122
|
+
if (key.code === 32) {
|
|
123
|
+
// Ctrl+Space is 0x00 in legacy xterm; plain/alt space is the character.
|
|
124
|
+
if (ctrl)
|
|
125
|
+
return alt ? '\x1b\x00' : '\x00';
|
|
126
|
+
return alt ? '\x1b ' : ' ';
|
|
127
|
+
}
|
|
128
|
+
if (key.code >= 32 && key.code <= 126) {
|
|
129
|
+
const char = String.fromCharCode(key.code);
|
|
130
|
+
if (ctrl) {
|
|
131
|
+
const code = ctrlByte(key.code);
|
|
132
|
+
const text = code === key.code ? char : String.fromCharCode(code);
|
|
133
|
+
return alt ? `\x1b${text}` : text;
|
|
134
|
+
}
|
|
135
|
+
if (alt)
|
|
136
|
+
return `\x1b${char}`;
|
|
137
|
+
if (shift && key.alternates.length > 0) {
|
|
138
|
+
const shifted = key.alternates[0];
|
|
139
|
+
if (shifted !== undefined && shifted > 0)
|
|
140
|
+
return String.fromCodePoint(shifted);
|
|
141
|
+
}
|
|
142
|
+
return char;
|
|
143
|
+
}
|
|
144
|
+
// Functional-key codes only appear under modes this app never pushes
|
|
145
|
+
// (kitty flags 2+); pass them through so behaviour matches the un-enhanced
|
|
146
|
+
// stream instead of inventing a translation.
|
|
147
|
+
return key.seq;
|
|
148
|
+
}
|
|
149
|
+
/** True when `s` is a PROPER prefix of some CSI-u sequence. */
|
|
150
|
+
function isCsiUPrefix(s) {
|
|
151
|
+
if (s.length < 3 || s[0] !== '\x1b' || s[1] !== '[')
|
|
152
|
+
return false;
|
|
153
|
+
for (let i = 2; i < s.length; i += 1) {
|
|
154
|
+
if (!isParamChar(s[i]))
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Length of the trailing run of `text` that could still complete into a
|
|
161
|
+
* CSI-u sequence, or 0. Longest match, mirroring the other families.
|
|
162
|
+
*/
|
|
163
|
+
export function trailingCsiUPrefixLength(text) {
|
|
164
|
+
const max = Math.min(text.length, 32);
|
|
165
|
+
for (let k = max; k >= 1; k -= 1) {
|
|
166
|
+
if (isCsiUPrefix(text.slice(text.length - k)))
|
|
167
|
+
return k;
|
|
168
|
+
}
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard-enhancement lifecycle (the Shift+Enter newline fix).
|
|
3
|
+
*
|
|
4
|
+
* WHY A LIFECYCLE MODULE AND NOT ANOTHER `screen.ts` MODE. The alternate
|
|
5
|
+
* screen is bound to fullscreen rendering; keyboard enhancement must run in
|
|
6
|
+
* every interactive session, inline included, because the composer is always
|
|
7
|
+
* there. It still follows the screen module's discipline: bytes are written
|
|
8
|
+
* ONLY for a TTY stdout, restore is idempotent, and the caller owns every
|
|
9
|
+
* decision -- this module is told the resolved capability and never derives
|
|
10
|
+
* one from config or environment.
|
|
11
|
+
*
|
|
12
|
+
* PLATFORM SPLIT, DECIDED BY BYTE-LEVEL PROBES (see
|
|
13
|
+
* docs/diagnoses/shift-enter-newline-dead-by-default):
|
|
14
|
+
*
|
|
15
|
+
* - win32: `?9001h` (win32-input-mode). It works through ConPTY on both
|
|
16
|
+
* conhost and Windows Terminal; kitty `CSI > 1 u` is swallowed there (a
|
|
17
|
+
* `CSI ? u` query never answers) and modifyOtherKeys is a no-op. Gated on
|
|
18
|
+
* the SAME `vtInputSupported` measurement the mouse feature uses: on Node
|
|
19
|
+
* < 22.17 / < 24.2 libuv never turns on `ENABLE_VIRTUAL_TERMINAL_INPUT`,
|
|
20
|
+
* so the records cannot reach the process and the mode would be a lie.
|
|
21
|
+
* - everything else: kitty disambiguate (`CSI > 1 u`) plus
|
|
22
|
+
* `modifyOtherKeys=2`. iTerm2 / kitty / ghostty / WezTerm / foot implement
|
|
23
|
+
* at least one; terminals that implement neither ignore both bytes.
|
|
24
|
+
*/
|
|
25
|
+
export interface KeyboardEnhancementHandle {
|
|
26
|
+
/** Idempotent; writes the disable bytes exactly once. */
|
|
27
|
+
restore(): void;
|
|
28
|
+
}
|
|
29
|
+
/** The handle a gated-out session reads: nothing was pushed, nothing unwinds. */
|
|
30
|
+
export declare const NOOP_KEYBOARD_ENHANCEMENT: KeyboardEnhancementHandle;
|
|
31
|
+
/**
|
|
32
|
+
* Ask the terminal to report modified keys distinctly. The returned handle's
|
|
33
|
+
* `restore()` hands the keyboard back; wire it into the same convergence
|
|
34
|
+
* point the screen restore uses so every exit path unwinds both.
|
|
35
|
+
*
|
|
36
|
+
* `vtInputSupported` is the measurement `cli.tsx` already resolves for the
|
|
37
|
+
* mouse gate; on win32 it decides between `?9001h` and nothing at all.
|
|
38
|
+
*/
|
|
39
|
+
export declare function enableKeyboardEnhancement(stdout: NodeJS.WriteStream | undefined, opts: {
|
|
40
|
+
readonly platform: string;
|
|
41
|
+
readonly vtInputSupported: boolean;
|
|
42
|
+
}): KeyboardEnhancementHandle;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard-enhancement lifecycle (the Shift+Enter newline fix).
|
|
3
|
+
*
|
|
4
|
+
* WHY A LIFECYCLE MODULE AND NOT ANOTHER `screen.ts` MODE. The alternate
|
|
5
|
+
* screen is bound to fullscreen rendering; keyboard enhancement must run in
|
|
6
|
+
* every interactive session, inline included, because the composer is always
|
|
7
|
+
* there. It still follows the screen module's discipline: bytes are written
|
|
8
|
+
* ONLY for a TTY stdout, restore is idempotent, and the caller owns every
|
|
9
|
+
* decision -- this module is told the resolved capability and never derives
|
|
10
|
+
* one from config or environment.
|
|
11
|
+
*
|
|
12
|
+
* PLATFORM SPLIT, DECIDED BY BYTE-LEVEL PROBES (see
|
|
13
|
+
* docs/diagnoses/shift-enter-newline-dead-by-default):
|
|
14
|
+
*
|
|
15
|
+
* - win32: `?9001h` (win32-input-mode). It works through ConPTY on both
|
|
16
|
+
* conhost and Windows Terminal; kitty `CSI > 1 u` is swallowed there (a
|
|
17
|
+
* `CSI ? u` query never answers) and modifyOtherKeys is a no-op. Gated on
|
|
18
|
+
* the SAME `vtInputSupported` measurement the mouse feature uses: on Node
|
|
19
|
+
* < 22.17 / < 24.2 libuv never turns on `ENABLE_VIRTUAL_TERMINAL_INPUT`,
|
|
20
|
+
* so the records cannot reach the process and the mode would be a lie.
|
|
21
|
+
* - everything else: kitty disambiguate (`CSI > 1 u`) plus
|
|
22
|
+
* `modifyOtherKeys=2`. iTerm2 / kitty / ghostty / WezTerm / foot implement
|
|
23
|
+
* at least one; terminals that implement neither ignore both bytes.
|
|
24
|
+
*/
|
|
25
|
+
import { CSI_U_DISABLE, CSI_U_ENABLE, } from './csiu-keys.js';
|
|
26
|
+
import { WIN32_INPUT_DISABLE, WIN32_INPUT_ENABLE } from './win32-input-mode.js';
|
|
27
|
+
const NOOP_HANDLE = { restore() { } };
|
|
28
|
+
/** The handle a gated-out session reads: nothing was pushed, nothing unwinds. */
|
|
29
|
+
export const NOOP_KEYBOARD_ENHANCEMENT = NOOP_HANDLE;
|
|
30
|
+
function safeWrite(stdout, data) {
|
|
31
|
+
try {
|
|
32
|
+
stdout.write(data);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// A closed/broken stdout must never crash the exit path.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Ask the terminal to report modified keys distinctly. The returned handle's
|
|
40
|
+
* `restore()` hands the keyboard back; wire it into the same convergence
|
|
41
|
+
* point the screen restore uses so every exit path unwinds both.
|
|
42
|
+
*
|
|
43
|
+
* `vtInputSupported` is the measurement `cli.tsx` already resolves for the
|
|
44
|
+
* mouse gate; on win32 it decides between `?9001h` and nothing at all.
|
|
45
|
+
*/
|
|
46
|
+
export function enableKeyboardEnhancement(stdout, opts) {
|
|
47
|
+
if (!stdout || !stdout.isTTY || typeof stdout.write !== 'function')
|
|
48
|
+
return NOOP_HANDLE;
|
|
49
|
+
const enable = opts.platform === 'win32'
|
|
50
|
+
? (opts.vtInputSupported ? WIN32_INPUT_ENABLE : '')
|
|
51
|
+
: CSI_U_ENABLE;
|
|
52
|
+
if (enable.length === 0)
|
|
53
|
+
return NOOP_HANDLE;
|
|
54
|
+
const disable = opts.platform === 'win32' ? WIN32_INPUT_DISABLE : CSI_U_DISABLE;
|
|
55
|
+
safeWrite(stdout, enable);
|
|
56
|
+
let restored = false;
|
|
57
|
+
return {
|
|
58
|
+
restore() {
|
|
59
|
+
if (restored)
|
|
60
|
+
return;
|
|
61
|
+
restored = true;
|
|
62
|
+
safeWrite(stdout, disable);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|