@timurproko/a1 0.2.5-dev.683 → 0.2.5-dev.704
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/dist/app/session-shell/prompt-suggestion-controller.d.ts +2 -0
- package/dist/app/session-shell/prompt-suggestion-controller.js +10 -0
- package/dist/app/session-shell/session-shell-root.js +15 -2
- package/dist/app/session-shell/session-shell.js +80 -23
- package/dist/app/session-shell/share-operation.d.ts +3 -0
- package/dist/app/session-shell/share-operation.js +11 -0
- package/dist/composition/owned-ui.js +2 -0
- package/dist/composition/settings-route-host.js +1 -1
- package/dist/contracts/presentation/index.d.ts +11 -1
- package/dist/contracts/presentation/index.js +6 -2
- package/dist/features/owned-ui/project-trust-dialog.js +7 -5
- package/dist/features/owned-ui/reference-screen-app.d.ts +2 -2
- package/dist/features/owned-ui/reference-screen-app.js +10 -6
- package/dist/features/owned-ui/settings-app.d.ts +1 -1
- package/dist/features/owned-ui/settings-app.js +342 -66
- package/dist/integrations/pi/components/index.d.ts +3 -2
- package/dist/integrations/pi/components/index.js +2 -1
- package/dist/integrations/pi/components/modal-frame.d.ts +11 -1
- package/dist/integrations/pi/components/modal-frame.js +29 -1
- package/dist/integrations/pi/components/models-dialog.js +19 -19
- package/dist/integrations/pi/components/share-operation-dialog.d.ts +6 -0
- package/dist/integrations/pi/components/share-operation-dialog.js +40 -0
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +55 -8
- package/dist/integrations/pi/components/shell-extension-ui.d.ts +3 -0
- package/dist/integrations/pi/components/shell-extension-ui.js +8 -4
- package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +0 -4
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +56 -34
- package/dist/integrations/pi/components/shell-session-info-reference.js +35 -2
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +3 -1
- package/dist/integrations/pi/components/shell-shared-facade.js +13 -1
- package/dist/integrations/pi/components/skills-dialog.js +21 -8
- package/dist/integrations/pi/components/theme.d.ts +8 -1
- package/dist/integrations/pi/components/theme.js +28 -1
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +20 -6
- package/dist/integrations/pi/components/upstream/components/extension-editor.js +2 -2
- package/dist/integrations/pi/components/upstream/components/extension-input.js +4 -5
- package/dist/integrations/pi/components/upstream/components/extension-selector.js +2 -3
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +4 -2
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +48 -12
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -2
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +4 -15
- package/dist/integrations/pi/components/upstream/components/session-selector.d.ts +2 -0
- package/dist/integrations/pi/components/upstream/components/session-selector.js +137 -110
- package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +9 -5
- package/dist/integrations/pi/components/upstream/components/thinking-selector.js +65 -26
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +31 -21
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +255 -214
- package/dist/integrations/pi/components/upstream/components/trust-selector.js +13 -15
- package/dist/integrations/pi/engine/adapter.js +21 -2
- package/dist/integrations/pi/engine/workflow-runner.js +2 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/apps/host.d.ts +4 -3
- package/dist/ui/apps/host.js +9 -4
- package/dist/ui/components/dialog-panel.d.ts +15 -0
- package/dist/ui/components/dialog-panel.js +55 -9
- package/dist/ui/components/index.d.ts +3 -3
- package/dist/ui/components/index.js +2 -2
- package/dist/ui/components/line-input.d.ts +2 -0
- package/dist/ui/components/line-input.js +2 -2
- package/dist/ui/components/list-view.d.ts +3 -3
- package/dist/ui/components/list-view.js +8 -9
- package/dist/ui/components/prompt-input.d.ts +2 -2
- package/dist/ui/components/prompt-input.js +5 -5
- package/dist/ui/components/shortcut-hints.d.ts +10 -3
- package/dist/ui/components/shortcut-hints.js +42 -6
- package/dist/ui/components/shortcuts.d.ts +1 -1
- package/dist/ui/components/shortcuts.js +4 -0
- package/dist/ui/components/theme.d.ts +1 -1
- package/dist/ui/components/value-menu.js +3 -1
- package/dist/ui/settings/manager.d.ts +1 -0
- package/dist/ui/settings/manager.js +9 -3
- package/package.json +1 -1
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* Provenance: @earendil-works/pi-coding-agent 1.0.4 (MIT), commit 7c10bd4337495ee613f2224843ecdf349b80d1df,
|
|
3
3
|
* packages/coding-agent/src/modes/interactive/components/trust-selector.ts.
|
|
4
4
|
* Modifications: Mechanical source-synchronized trust selector port with injected public
|
|
5
|
-
* ProjectTrustStore-derived options, remapped owned theme imports,
|
|
6
|
-
*
|
|
5
|
+
* ProjectTrustStore-derived options, remapped owned theme imports, the shared bare-A1 modal shortcut
|
|
6
|
+
* row and compact padded modal frame, and standard semantic trust styling with marker-free choice
|
|
7
|
+
* geometry.
|
|
7
8
|
* Deviations: owned-modal-shortcut-hints.
|
|
8
9
|
*/
|
|
9
10
|
import { DynamicBorder } from "../../../startup-public.js";
|
|
10
11
|
import { Container, getKeybindings, Spacer, Text } from "@earendil-works/pi-tui";
|
|
11
12
|
import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
|
|
12
|
-
import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
|
|
13
|
+
import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "../../theme.js";
|
|
13
14
|
function formatDecision(trustPath, decision) {
|
|
14
15
|
if (decision === null)
|
|
15
16
|
return "none";
|
|
@@ -30,11 +31,11 @@ export class TrustSelectorComponent extends Container {
|
|
|
30
31
|
&& options.savedDecision?.decision === option.trusted
|
|
31
32
|
&& options.savedDecision.path === option.savedPath;
|
|
32
33
|
this.selectedIndex = Math.max(0, this.trustOptions.findIndex(isSaved));
|
|
33
|
-
const header = addPiModalHeader(this, new DynamicBorder(), new Text(piTheme().fg("accent", piTheme().bold("Project trust")), 0, 0));
|
|
34
|
+
const header = addPiModalHeader(this, new DynamicBorder(text => piTheme().fg("border", text)), new Text(piTheme().fg("accent", piTheme().bold("Project trust")), 0, 0));
|
|
34
35
|
this.addChild(new Text(piTheme().fg("muted", options.cwd), 0, 0));
|
|
35
36
|
this.addChild(new Spacer(1));
|
|
36
|
-
this.addChild(new Text(piTheme().fg("muted",
|
|
37
|
-
this.addChild(new Text(piTheme().fg("muted",
|
|
37
|
+
this.addChild(new Text(`${piTheme().fg("muted", "Saved decision:")} ${piTheme().fg("text", formatDecision(this.trustOptions[0]?.savedPath, options.savedDecision))}`, 0, 0));
|
|
38
|
+
this.addChild(new Text(`${piTheme().fg("muted", "Current session:")} ${piTheme().fg("text", options.projectTrusted ? "trusted" : "untrusted")}`, 0, 0));
|
|
38
39
|
this.addChild(new Spacer(1));
|
|
39
40
|
this.listContainer = new Container();
|
|
40
41
|
this.addChild(this.listContainer);
|
|
@@ -43,12 +44,11 @@ export class TrustSelectorComponent extends Container {
|
|
|
43
44
|
this.addChild(new Text(renderPiModalShortcutHints([
|
|
44
45
|
{ key: "↑↓", action: "navigate" },
|
|
45
46
|
{ key: keybindings.getKeys("tui.select.confirm").join("/"), action: "save" },
|
|
46
|
-
|
|
47
|
+
DIALOG_CLOSE_SHORTCUT_HINT,
|
|
47
48
|
]), 0, 0));
|
|
48
|
-
this.addChild(new
|
|
49
|
-
this.addChild(new DynamicBorder());
|
|
49
|
+
this.addChild(new DynamicBorder(text => piTheme().fg("border", text)));
|
|
50
50
|
adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
|
|
51
|
-
this.updateList(
|
|
51
|
+
this.updateList();
|
|
52
52
|
this.handleInput = data => {
|
|
53
53
|
const kb = getKeybindings();
|
|
54
54
|
if (kb.matches(data, "tui.select.up") || data === "k")
|
|
@@ -67,21 +67,19 @@ export class TrustSelectorComponent extends Container {
|
|
|
67
67
|
}
|
|
68
68
|
else
|
|
69
69
|
return;
|
|
70
|
-
this.updateList(
|
|
70
|
+
this.updateList();
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
|
-
updateList(
|
|
73
|
+
updateList() {
|
|
74
74
|
this.listContainer.clear();
|
|
75
75
|
for (let index = 0; index < this.trustOptions.length; index += 1) {
|
|
76
76
|
const option = this.trustOptions[index];
|
|
77
77
|
if (!option)
|
|
78
78
|
continue;
|
|
79
79
|
const selected = index === this.selectedIndex;
|
|
80
|
-
const current = option.savedPath !== undefined && savedDecision?.decision === option.trusted && savedDecision.path === option.savedPath;
|
|
81
80
|
const prefix = selected ? piTheme().fg("accent", "→ ") : " ";
|
|
82
81
|
const label = selected ? piTheme().fg("accent", option.label) : piTheme().fg("text", option.label);
|
|
83
|
-
|
|
84
|
-
this.listContainer.addChild(new Text(`${prefix}${currentMarker}${label}`, 0, 0));
|
|
82
|
+
this.listContainer.addChild(new Text(`${prefix}${label}`, 0, 0));
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
85
|
}
|
|
@@ -382,8 +382,12 @@ export class PiEngineAdapter {
|
|
|
382
382
|
reloadBlockedResult() {
|
|
383
383
|
return this.#workflows.reloadBlockedResult();
|
|
384
384
|
}
|
|
385
|
-
executeWorkflow(request) {
|
|
386
|
-
|
|
385
|
+
async executeWorkflow(request) {
|
|
386
|
+
const result = await this.#workflows.executeWorkflow(request);
|
|
387
|
+
if (request.command === "tree" && result.outcome === "completed" && request.selection !== undefined) {
|
|
388
|
+
this.#refreshNavigatedSession(result.detail);
|
|
389
|
+
}
|
|
390
|
+
return result;
|
|
387
391
|
}
|
|
388
392
|
/**
|
|
389
393
|
* The theme the engine is configured with, in the engine's own grammar: a
|
|
@@ -575,6 +579,21 @@ export class PiEngineAdapter {
|
|
|
575
579
|
throw new Error("owned UI state commands belong to the owned UI layer, not the Pi engine adapter");
|
|
576
580
|
}
|
|
577
581
|
}
|
|
582
|
+
// Invariant: in-place tree navigation rebuilds derived content without clearing the editor draft.
|
|
583
|
+
#refreshNavigatedSession(editorText) {
|
|
584
|
+
const session = this.#engine.session;
|
|
585
|
+
if (!session)
|
|
586
|
+
return;
|
|
587
|
+
if (editorText && !this.#editor.text.trim()) {
|
|
588
|
+
this.#editor = { ...this.#editor, text: editorText, selection: null, cursorOffset: editorText.length };
|
|
589
|
+
}
|
|
590
|
+
this.#activeModel = readModel(session.model);
|
|
591
|
+
this.#reconcileActiveModelAvailability();
|
|
592
|
+
this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
|
|
593
|
+
this.#projection.assets.clear();
|
|
594
|
+
this.#setTranscript(this.#projection.rebuild(session.messages, "finalized"));
|
|
595
|
+
this.#emitView();
|
|
596
|
+
}
|
|
578
597
|
// Invariant: the runtime already advanced the generation and subscribed; this rebuilds what the view derives from a session.
|
|
579
598
|
#sessionReplaced(session) {
|
|
580
599
|
this.#delivery.discardObsolete(this.#engine.generation);
|
|
@@ -448,7 +448,8 @@ export class PiWorkflowRunner {
|
|
|
448
448
|
return workflowResult(request.command, "cancelled", "Branch summarization cancelled", undefined, "status");
|
|
449
449
|
if (isRecord(result) && result.cancelled === true)
|
|
450
450
|
return workflowResult(request.command, "cancelled", "Navigation cancelled", undefined, "status");
|
|
451
|
-
|
|
451
|
+
const editorText = isRecord(result) && typeof result.editorText === "string" ? result.editorText : undefined;
|
|
452
|
+
return workflowResult(request.command, "completed", "Navigated to selected point", editorText);
|
|
452
453
|
}
|
|
453
454
|
case "trust": {
|
|
454
455
|
if (!selection)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-10-
|
|
8
|
+
"builtAt": "2026-10-07T15:28:34.230Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "51b0b2dc1c9ae577f2586c4217066929278726c2c15308d9144eb6f538e118ff",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-10-
|
|
8
|
+
"builtAt": "2026-10-07T15:28:19.388Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "ef62679e83bc1fad3d38f9f21675d9256f08d20e67fd4791571032115ca1b0e9",
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-10-
|
|
8
|
+
"builtAt": "2026-10-07T15:29:15.953Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "a8e1e48b23cfa2a33b4821ccf799bf703b984ac5453c08926f8b5928801c07bb",
|
|
12
12
|
"size": 288256
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
package/dist/ui/apps/host.d.ts
CHANGED
|
@@ -22,9 +22,10 @@ export interface AppHostOptions {
|
|
|
22
22
|
readonly theme?: UiTheme;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* Presents at most one app.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* Presents at most one app. An opted-in interrupt closes at the host boundary;
|
|
26
|
+
* other input reaches the presented app first and anything it does not consume
|
|
27
|
+
* continues to the caller. A failure while rendering or handling input closes
|
|
28
|
+
* the app rather than leaving a broken surface on screen.
|
|
28
29
|
*/
|
|
29
30
|
export declare class UiAppHost {
|
|
30
31
|
#private;
|
package/dist/ui/apps/host.js
CHANGED
|
@@ -5,9 +5,10 @@ const INTERRUPT = "";
|
|
|
5
5
|
/** How long the first interrupt of the chord stays armed. */
|
|
6
6
|
const INTERRUPT_CHORD_MS = 1_500;
|
|
7
7
|
/**
|
|
8
|
-
* Presents at most one app.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Presents at most one app. An opted-in interrupt closes at the host boundary;
|
|
9
|
+
* other input reaches the presented app first and anything it does not consume
|
|
10
|
+
* continues to the caller. A failure while rendering or handling input closes
|
|
11
|
+
* the app rather than leaving a broken surface on screen.
|
|
11
12
|
*/
|
|
12
13
|
export class UiAppHost {
|
|
13
14
|
#registry;
|
|
@@ -70,6 +71,10 @@ export class UiAppHost {
|
|
|
70
71
|
const app = this.#app;
|
|
71
72
|
if (app === null)
|
|
72
73
|
return { consumed: false };
|
|
74
|
+
if (data === INTERRUPT && this.#closeOnInterrupt) {
|
|
75
|
+
this.close();
|
|
76
|
+
return { consumed: true, render: true };
|
|
77
|
+
}
|
|
73
78
|
const result = this.#guard(() => app.onInput?.(data, this.#services())) ?? { consumed: false };
|
|
74
79
|
if (this.#app === null)
|
|
75
80
|
return { consumed: true, render: true };
|
|
@@ -91,7 +96,7 @@ export class UiAppHost {
|
|
|
91
96
|
this.render();
|
|
92
97
|
return result;
|
|
93
98
|
}
|
|
94
|
-
// Protocol:
|
|
99
|
+
// Protocol: a non-opted-in host preserves the two-interrupt application exit chord.
|
|
95
100
|
#interrupt() {
|
|
96
101
|
const now = Date.now();
|
|
97
102
|
const armed = this.#interruptArmedAt;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LineInput } from "./line-input.js";
|
|
1
2
|
import { type ShortcutHintEntry } from "./shortcut-hints.js";
|
|
2
3
|
import type { UiTheme } from "./theme.js";
|
|
3
4
|
/**
|
|
@@ -7,11 +8,13 @@ import type { UiTheme } from "./theme.js";
|
|
|
7
8
|
*/
|
|
8
9
|
export interface DialogRow {
|
|
9
10
|
readonly label: string;
|
|
11
|
+
readonly labelSuffix?: string;
|
|
10
12
|
readonly value: string;
|
|
11
13
|
/** What this part does, shown while it is the one in hand. */
|
|
12
14
|
readonly description?: string;
|
|
13
15
|
}
|
|
14
16
|
export interface DialogPanelState {
|
|
17
|
+
readonly title: string;
|
|
15
18
|
readonly rows: readonly DialogRow[];
|
|
16
19
|
/** The row in hand. */
|
|
17
20
|
readonly index: number;
|
|
@@ -26,6 +29,16 @@ export interface DialogPanelFrame {
|
|
|
26
29
|
/** Column the values start at, so a label stays a label. */
|
|
27
30
|
readonly valueColumn: number;
|
|
28
31
|
}
|
|
32
|
+
export interface SteppedDialogPanelState {
|
|
33
|
+
readonly title: string;
|
|
34
|
+
readonly step: number;
|
|
35
|
+
readonly steps: number;
|
|
36
|
+
readonly description: string;
|
|
37
|
+
readonly input?: LineInput;
|
|
38
|
+
readonly rows: readonly DialogRow[];
|
|
39
|
+
readonly index: number;
|
|
40
|
+
readonly hint: readonly ShortcutHintEntry[];
|
|
41
|
+
}
|
|
29
42
|
/** The column values start at: past the widest label, capped so one long name cannot push them off. */
|
|
30
43
|
export declare function dialogValueColumn(rows: readonly DialogRow[]): number;
|
|
31
44
|
/**
|
|
@@ -33,5 +46,7 @@ export declare function dialogValueColumn(rows: readonly DialogRow[]): number;
|
|
|
33
46
|
* one line below the panel's top.
|
|
34
47
|
*/
|
|
35
48
|
export declare function renderDialogPanel(state: DialogPanelState, width: number, theme: UiTheme): readonly string[];
|
|
49
|
+
/** Owned stepped-selector composition for structured settings with nested choices. */
|
|
50
|
+
export declare function renderSteppedDialogPanel(state: SteppedDialogPanelState, width: number, theme: UiTheme): readonly string[];
|
|
36
51
|
/** The row a pointer report lands on, or null when it is not on one. */
|
|
37
52
|
export declare function dialogRowAt(frame: DialogPanelFrame, row: number, column: number, valueWidth: number): number | null;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LineInput, renderInputRow } from "./line-input.js";
|
|
2
|
+
import { renderShortcutHints, renderShortcutHintsWithClose, shortcutHintsText } from "./shortcut-hints.js";
|
|
2
3
|
import { displayWidth, truncateToWidth } from "./text.js";
|
|
3
4
|
const LABEL_COLUMN_CAP = 30;
|
|
5
|
+
const STEPPED_LABEL_COLUMN_CAP = 46;
|
|
4
6
|
/** The column values start at: past the widest label, capped so one long name cannot push them off. */
|
|
5
7
|
export function dialogValueColumn(rows) {
|
|
6
8
|
const widest = Math.min(LABEL_COLUMN_CAP, Math.max(0, ...rows.map(row => displayWidth(row.label))));
|
|
@@ -22,28 +24,72 @@ export function renderDialogPanel(state, width, theme) {
|
|
|
22
24
|
const padded = `${row.label}${" ".repeat(Math.max(0, labelColumn - displayWidth(row.label)))}`;
|
|
23
25
|
const cursor = selected ? "→ " : " ";
|
|
24
26
|
const raw = `${cursor}${padded} ${row.value}`;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const painted = selected
|
|
28
|
-
? `${theme.fg("accent", cursor)}${theme.fg("accent", padded)} ${theme.fg("accent", row.value)}`
|
|
27
|
+
const content = selected
|
|
28
|
+
? `${theme.fg("accent", cursor)}${theme.fg("text", padded)} ${theme.fg("muted", row.value)}`
|
|
29
29
|
: `${cursor}${padded} ${theme.fg("muted", row.value)}`;
|
|
30
|
+
const painted = selected ? theme.highlight(content) : content;
|
|
30
31
|
return contentRow(painted, raw);
|
|
31
32
|
});
|
|
32
33
|
const description = state.rows[state.index]?.description ?? "";
|
|
33
34
|
const hint = typeof state.hint === "string"
|
|
34
35
|
? theme.fg("dim", state.hint)
|
|
35
|
-
:
|
|
36
|
-
const plainHint = typeof state.hint === "string" ? state.hint :
|
|
36
|
+
: renderShortcutHintsWithClose(state.hint, theme, contentWidth, 0);
|
|
37
|
+
const plainHint = typeof state.hint === "string" ? state.hint : " ".repeat(displayWidth(hint));
|
|
37
38
|
return [
|
|
38
39
|
rule,
|
|
39
|
-
|
|
40
|
+
contentRow(theme.bold(theme.fg("accent", state.title)), state.title),
|
|
41
|
+
contentRow(theme.fg("dim", description), description),
|
|
40
42
|
"",
|
|
41
|
-
|
|
43
|
+
...rows,
|
|
42
44
|
"",
|
|
43
45
|
contentRow(hint, plainHint),
|
|
44
46
|
rule,
|
|
45
47
|
];
|
|
46
48
|
}
|
|
49
|
+
/** Owned stepped-selector composition for structured settings with nested choices. */
|
|
50
|
+
export function renderSteppedDialogPanel(state, width, theme) {
|
|
51
|
+
const rule = theme.fg("border", "─".repeat(Math.max(0, width)));
|
|
52
|
+
const contentPadding = Math.min(1, Math.max(0, width));
|
|
53
|
+
const contentWidth = Math.max(0, width - contentPadding);
|
|
54
|
+
const inset = " ".repeat(contentPadding);
|
|
55
|
+
const contentRow = (painted, raw) => `${inset}${pad(truncateToWidth(painted, contentWidth), contentWidth, raw)}`;
|
|
56
|
+
const labelColumn = Math.min(STEPPED_LABEL_COLUMN_CAP, Math.max(0, ...state.rows.map(row => displayWidth(`${row.label}${row.labelSuffix ?? ""}`))));
|
|
57
|
+
const rows = state.rows.map((row, index) => {
|
|
58
|
+
const selected = index === state.index;
|
|
59
|
+
const completeLabel = `${row.label}${row.labelSuffix ?? ""}`;
|
|
60
|
+
const padding = " ".repeat(Math.max(0, labelColumn - displayWidth(completeLabel)));
|
|
61
|
+
const suffix = row.value.length === 0 ? "" : ` ${row.value}`;
|
|
62
|
+
const cursor = selected ? "→ " : " ";
|
|
63
|
+
const raw = `${cursor}${completeLabel}${padding}${suffix}`;
|
|
64
|
+
const label = `${theme.fg("text", row.label)}${theme.fg("muted", row.labelSuffix ?? "")}${padding}`;
|
|
65
|
+
const content = selected
|
|
66
|
+
? `${theme.fg("accent", cursor)}${label}${theme.fg("muted", suffix)}`
|
|
67
|
+
: `${cursor}${label}${theme.fg("muted", suffix)}`;
|
|
68
|
+
return contentRow(selected ? theme.highlight(content) : content, raw);
|
|
69
|
+
});
|
|
70
|
+
const input = state.input === undefined
|
|
71
|
+
? []
|
|
72
|
+
: [...renderInputRow(state.input, width, { ruled: false, promptGlyph: "> ", theme }).lines, ""];
|
|
73
|
+
const step = `(step ${state.step}/${state.steps})`;
|
|
74
|
+
const title = `${state.title} ${step}`;
|
|
75
|
+
const paintedTitle = `${theme.bold(theme.fg("accent", state.title))} ${theme.fg("dim", step)}`;
|
|
76
|
+
return [
|
|
77
|
+
rule,
|
|
78
|
+
contentRow(paintedTitle, title),
|
|
79
|
+
contentRow(theme.fg("dim", state.description), state.description),
|
|
80
|
+
"",
|
|
81
|
+
...input,
|
|
82
|
+
...rows,
|
|
83
|
+
"",
|
|
84
|
+
renderSteppedHint(state.hint, width, theme),
|
|
85
|
+
rule,
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
function renderSteppedHint(entries, width, theme) {
|
|
89
|
+
const raw = shortcutHintsText(entries, 1, " · ");
|
|
90
|
+
const painted = renderShortcutHints(entries, theme, 1, theme.fg("dim", " · "));
|
|
91
|
+
return `${truncateToWidth(painted, width)}${" ".repeat(Math.max(0, width - displayWidth(raw)))}`;
|
|
92
|
+
}
|
|
47
93
|
/** The row a pointer report lands on, or null when it is not on one. */
|
|
48
94
|
export function dialogRowAt(frame, row, column, valueWidth) {
|
|
49
95
|
const at = row - frame.firstRow;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { dialogRowAt, dialogValueColumn, renderDialogPanel } from "./dialog-panel.js";
|
|
2
|
-
export type { DialogPanelFrame, DialogPanelState, DialogRow } from "./dialog-panel.js";
|
|
1
|
+
export { dialogRowAt, dialogValueColumn, renderDialogPanel, renderSteppedDialogPanel } from "./dialog-panel.js";
|
|
2
|
+
export type { DialogPanelFrame, DialogPanelState, DialogRow, SteppedDialogPanelState } from "./dialog-panel.js";
|
|
3
3
|
export { FrameContractError, assertPaneRect, finalizeFrame, validateFrame } from "./frame.js";
|
|
4
4
|
export type { PaneRect } from "./frame.js";
|
|
5
5
|
export { humanizeLabel, humanizeTitle } from "./label.js";
|
|
@@ -23,7 +23,7 @@ export { MAX_COPY_ROWS, MAX_COPY_SOURCE_UNITS, selectionCopyLineContent, selecti
|
|
|
23
23
|
export type { SelectionCopyPrompt, SelectionCopyRow, SelectionCopySnapshot } from "./selection-copy.js";
|
|
24
24
|
export { ScrollbarRails, isThumbRow, scrollForThumbRow, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, scrollbarReservesSpace, scrollbarSelectionRows, scrollbarWheelRows, } from "./scrollbar.js";
|
|
25
25
|
export type { RailPointer, RailPosition, ScrollbarAppearance, ScrollbarGeometry, ScrollbarInput, ScrollbarPresentation, ScrollbarPresentationInput, ScrollbarSpeed, ScrollbarStyle, } from "./scrollbar.js";
|
|
26
|
-
export { renderShortcutHints } from "./shortcut-hints.js";
|
|
26
|
+
export { assertOwnedShortcutHintConventions, renderShortcutHints, renderShortcutHintsWithClose, shortcutHintsText } from "./shortcut-hints.js";
|
|
27
27
|
export type { ShortcutHintEntry } from "./shortcut-hints.js";
|
|
28
28
|
export { GLOBAL_SCOPE, ShortcutRegistry, assembleShortcuts, assertNoShortcutConflicts } from "./shortcuts.js";
|
|
29
29
|
export type { ShortcutConflict, ShortcutDeclaration, ShortcutRegistryResult } from "./shortcuts.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { dialogRowAt, dialogValueColumn, renderDialogPanel } from "./dialog-panel.js";
|
|
1
|
+
export { dialogRowAt, dialogValueColumn, renderDialogPanel, renderSteppedDialogPanel } from "./dialog-panel.js";
|
|
2
2
|
export { FrameContractError, assertPaneRect, finalizeFrame, validateFrame } from "./frame.js";
|
|
3
3
|
export { humanizeLabel, humanizeTitle } from "./label.js";
|
|
4
4
|
export { LineInput, caretCell, handleLineInputKey, renderInputRow, wordLeft, wordRight } from "./line-input.js";
|
|
@@ -11,7 +11,7 @@ export { progressStatusFrame, progressStatusText } from "./progress-status.js";
|
|
|
11
11
|
export { FrameCache, RENDER_REVISION_KINDS, RenderRevisionTracker, ZERO_REVISIONS, normalizeRevisions, revisionsEqual } from "./revision.js";
|
|
12
12
|
export { MAX_COPY_ROWS, MAX_COPY_SOURCE_UNITS, selectionCopyLineContent, selectionCopyRowText } from "./selection-copy.js";
|
|
13
13
|
export { ScrollbarRails, isThumbRow, scrollForThumbRow, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, scrollbarReservesSpace, scrollbarSelectionRows, scrollbarWheelRows, } from "./scrollbar.js";
|
|
14
|
-
export { renderShortcutHints } from "./shortcut-hints.js";
|
|
14
|
+
export { assertOwnedShortcutHintConventions, renderShortcutHints, renderShortcutHintsWithClose, shortcutHintsText } from "./shortcut-hints.js";
|
|
15
15
|
export { GLOBAL_SCOPE, ShortcutRegistry, assembleShortcuts, assertNoShortcutConflicts } from "./shortcuts.js";
|
|
16
16
|
export { backgroundSgrSpan, heldNativeHyperlinkStyle, hyperlinkSgrSpan, hyperlinkTargetAtColumn, nativeHyperlinkStyle, overlaySpan } from "./spans.js";
|
|
17
17
|
export { readVisibleHyperlinks } from "./visible-hyperlinks.js";
|
|
@@ -58,6 +58,8 @@ export interface InputRowOptions {
|
|
|
58
58
|
readonly placeholder?: string;
|
|
59
59
|
/** Rules above and below, in the prompt's own grey. Default true. */
|
|
60
60
|
readonly ruled?: boolean;
|
|
61
|
+
/** Prompt marker including any following spacing. Defaults to the shared prompt glyph. */
|
|
62
|
+
readonly promptGlyph?: string;
|
|
61
63
|
readonly theme?: Pick<UiTheme, "fg">;
|
|
62
64
|
}
|
|
63
65
|
export interface InputRow {
|
|
@@ -209,7 +209,7 @@ export function caretCell(text) {
|
|
|
209
209
|
/** The input row as the reference draws one, padded to exactly the width. */
|
|
210
210
|
export function renderInputRow(input, width, options = {}) {
|
|
211
211
|
const presentation = new PromptInput(options.theme);
|
|
212
|
-
const view = input.view(Math.max(0, width - presentation.geometry(width).prefixWidth));
|
|
212
|
+
const view = input.view(Math.max(0, width - presentation.geometry(width, 0, options.promptGlyph).prefixWidth));
|
|
213
213
|
const placeholder = options.placeholder ?? "";
|
|
214
214
|
const empty = view.text.length === 0 && placeholder.length > 0;
|
|
215
215
|
const before = view.text.slice(0, view.caretColumn);
|
|
@@ -220,5 +220,5 @@ export function renderInputRow(input, width, options = {}) {
|
|
|
220
220
|
const body = empty
|
|
221
221
|
? `${caretCell(placeholder.slice(0, 1))}${faint(placeholder.slice(1))}`
|
|
222
222
|
: `${before}${caretCell(under)}${after}`;
|
|
223
|
-
return { lines: presentation.render(width, () => ({ rows: [body] }), options.ruled) };
|
|
223
|
+
return { lines: presentation.render(width, () => ({ rows: [body] }), options.ruled, 0, options.promptGlyph) };
|
|
224
224
|
}
|
|
@@ -44,9 +44,9 @@ export declare function valueColumnFor(rows: readonly ListViewRow[], indent?: nu
|
|
|
44
44
|
/** A group's name with the shared one-cell section inset used by owned screens. */
|
|
45
45
|
export declare function renderGroupHeader(title: string, width: number, theme: UiTheme): string;
|
|
46
46
|
/**
|
|
47
|
-
* One row, painted. The
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* One row, painted. The selected item keeps an accent cursor, normal-text label,
|
|
48
|
+
* muted value, and item-bounded highlight; the value carries the pointer, and the
|
|
49
|
+
* stepper appears only when the pointer is on the value it belongs to.
|
|
50
50
|
*/
|
|
51
51
|
export declare function renderListRow(row: ListViewRow, state: ListRowState, valueColumn: number, width: number, theme: UiTheme): string;
|
|
52
52
|
/** A line that stands in for rows: why a group has none, or what it is waiting on. */
|
|
@@ -18,9 +18,9 @@ export function renderGroupHeader(title, width, theme) {
|
|
|
18
18
|
return ` ${truncateToWidth(theme.fg("mdHeading", theme.bold(title)), width - 1)}`;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* One row, painted. The
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* One row, painted. The selected item keeps an accent cursor, normal-text label,
|
|
22
|
+
* muted value, and item-bounded highlight; the value carries the pointer, and the
|
|
23
|
+
* stepper appears only when the pointer is on the value it belongs to.
|
|
24
24
|
*/
|
|
25
25
|
export function renderListRow(row, state, valueColumn, width, theme) {
|
|
26
26
|
const cursor = state.selected ? "→ " : " ";
|
|
@@ -28,13 +28,11 @@ export function renderListRow(row, state, valueColumn, width, theme) {
|
|
|
28
28
|
const labelPadded = `${row.label}${" ".repeat(Math.max(0, labelColumn - displayWidth(row.label)))}`;
|
|
29
29
|
const leftRaw = `${cursor}${labelPadded}`;
|
|
30
30
|
const left = state.selected
|
|
31
|
-
? `${theme.fg("accent", cursor)}${theme.fg("
|
|
31
|
+
? `${theme.fg("accent", cursor)}${theme.fg("text", labelPadded)}`
|
|
32
32
|
: `${cursor}${theme.plain(labelPadded)}`;
|
|
33
33
|
const gap = Math.max(2, valueColumn - displayWidth(leftRaw));
|
|
34
|
-
// Rationale:
|
|
35
|
-
//
|
|
36
|
-
// value reads the same on every row, and pointer hover brightens it without moving
|
|
37
|
-
// the keyboard selection.
|
|
34
|
+
// Rationale: the value keeps the same semantic foreground on selected and unselected
|
|
35
|
+
// rows; pointer hover brightens it without moving keyboard selection.
|
|
38
36
|
const valueHovered = state.hovered && state.region !== "label";
|
|
39
37
|
const stepper = row.stepper !== undefined && valueHovered;
|
|
40
38
|
const value = valueHovered ? theme.plain(row.value) : theme.fg("muted", row.value);
|
|
@@ -50,7 +48,8 @@ export function renderListRow(row, state, valueColumn, width, theme) {
|
|
|
50
48
|
: "";
|
|
51
49
|
const indent = Math.max(2, stepper ? gap - STEPPER_RESERVE : gap);
|
|
52
50
|
const suffix = row.suffix === undefined ? "" : theme.fg("dim", ` ${row.suffix}`);
|
|
53
|
-
|
|
51
|
+
const rendered = truncateToWidth(`${left}${" ".repeat(indent)}${minus}${value}${plus}${suffix}`, width);
|
|
52
|
+
return state.selected ? theme.highlight(rendered) : rendered;
|
|
54
53
|
}
|
|
55
54
|
/** A line that stands in for rows: why a group has none, or what it is waiting on. */
|
|
56
55
|
export function renderNote(text, width, theme) {
|
|
@@ -21,12 +21,12 @@ export declare class PromptInput {
|
|
|
21
21
|
readonly metrics: PromptInputMetrics;
|
|
22
22
|
constructor(theme?: Pick<UiTheme, "fg">, metrics?: PromptInputMetrics);
|
|
23
23
|
styleRule(text: string): string;
|
|
24
|
-
geometry(width: number, padding?: number): {
|
|
24
|
+
geometry(width: number, padding?: number, promptGlyph?: string): {
|
|
25
25
|
prefixWidth: number;
|
|
26
26
|
innerWidth: number;
|
|
27
27
|
paddingX: number;
|
|
28
28
|
contentWidth: number;
|
|
29
29
|
layoutWidth: number;
|
|
30
30
|
};
|
|
31
|
-
render(width: number, renderBody: (innerWidth: number) => PromptInputBody, ruled?: boolean, padding?: number): string[];
|
|
31
|
+
render(width: number, renderBody: (innerWidth: number) => PromptInputBody, ruled?: boolean, padding?: number, promptGlyph?: string): string[];
|
|
32
32
|
}
|
|
@@ -17,8 +17,8 @@ export class PromptInput {
|
|
|
17
17
|
metrics;
|
|
18
18
|
constructor(theme = PLAIN_THEME, metrics = { measure: displayWidth, truncate: truncateToWidth }) { this.theme = theme; this.metrics = metrics; }
|
|
19
19
|
styleRule(text) { return promptRuleText(text); }
|
|
20
|
-
geometry(width, padding = 0) {
|
|
21
|
-
const prefixWidth = Math.min(
|
|
20
|
+
geometry(width, padding = 0, promptGlyph = PROMPT_GLYPH) {
|
|
21
|
+
const prefixWidth = Math.min(this.metrics.measure(promptGlyph), Math.max(0, width));
|
|
22
22
|
// Compatibility: editor wrapping needs room for a two-cell grapheme plus its cursor cell.
|
|
23
23
|
// Lay out against a safe virtual width, then clip only at the shared frame boundary.
|
|
24
24
|
const innerWidth = Math.max(3 + padding * 2, width - prefixWidth);
|
|
@@ -26,17 +26,17 @@ export class PromptInput {
|
|
|
26
26
|
const contentWidth = Math.max(1, innerWidth - paddingX * 2);
|
|
27
27
|
return { prefixWidth, innerWidth, paddingX, contentWidth, layoutWidth: Math.max(1, contentWidth - (paddingX ? 0 : 1)) };
|
|
28
28
|
}
|
|
29
|
-
render(width, renderBody, ruled = true, padding = 0) {
|
|
29
|
+
render(width, renderBody, ruled = true, padding = 0, promptGlyph = PROMPT_GLYPH) {
|
|
30
30
|
if (width <= 0)
|
|
31
31
|
return [];
|
|
32
|
-
const { prefixWidth, innerWidth } = this.geometry(width, padding);
|
|
32
|
+
const { prefixWidth, innerWidth } = this.geometry(width, padding, promptGlyph);
|
|
33
33
|
const body = renderBody(innerWidth);
|
|
34
34
|
const fit = (row) => {
|
|
35
35
|
const clipped = this.metrics.truncate(row, width);
|
|
36
36
|
return clipped + " ".repeat(Math.max(0, width - this.metrics.measure(clipped)));
|
|
37
37
|
};
|
|
38
38
|
const rows = body.rows.map((row, index) => fit(`${index === 0
|
|
39
|
-
? promptArrow(
|
|
39
|
+
? promptArrow(promptGlyph, this.theme)
|
|
40
40
|
: " ".repeat(prefixWidth)}${row}`));
|
|
41
41
|
const rule = (fragment) => fragment === undefined
|
|
42
42
|
? promptRule(width)
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { type SemanticShortcutHint } from "../../contracts/presentation/index.js";
|
|
2
2
|
import type { UiTheme } from "./theme.js";
|
|
3
3
|
export type ShortcutHintEntry = SemanticShortcutHint;
|
|
4
|
+
/**
|
|
5
|
+
* Enforces the owned-dialog hint grammar at the shared rendering boundary.
|
|
6
|
+
* Pinned Pi surfaces use their separate adapter and retain upstream wording.
|
|
7
|
+
*/
|
|
8
|
+
export declare function assertOwnedShortcutHintConventions(entries: readonly ShortcutHintEntry[]): void;
|
|
4
9
|
/** Returns an unstyled modal instruction row for width-aware clipping. */
|
|
5
|
-
export declare function shortcutHintsText(entries: readonly ShortcutHintEntry[], indent?: number): string;
|
|
6
|
-
/** Paints one modal instruction row
|
|
7
|
-
export declare function renderShortcutHints(entries: readonly ShortcutHintEntry[], theme: Pick<UiTheme, "fg">, indent?: number): string;
|
|
10
|
+
export declare function shortcutHintsText(entries: readonly ShortcutHintEntry[], indent?: number, separator?: string): string;
|
|
11
|
+
/** Paints one modal instruction row with shared key/action roles and caller-selected spacing. */
|
|
12
|
+
export declare function renderShortcutHints(entries: readonly ShortcutHintEntry[], theme: Pick<UiTheme, "fg">, indent?: number, separator?: string): string;
|
|
13
|
+
/** Clips preceding guidance first so a final canonical close entry remains complete whenever it fits. */
|
|
14
|
+
export declare function renderShortcutHintsWithClose(entries: readonly ShortcutHintEntry[], theme: Pick<UiTheme, "fg">, width: number, indent?: number): string;
|
|
@@ -1,12 +1,48 @@
|
|
|
1
|
-
import { renderSemanticShortcutHints, } from "../../contracts/presentation/index.js";
|
|
1
|
+
import { DIALOG_CLOSE_SHORTCUT_HINT, renderSemanticShortcutHints, } from "../../contracts/presentation/index.js";
|
|
2
|
+
import { displayWidth, truncateToWidth } from "./text.js";
|
|
3
|
+
/**
|
|
4
|
+
* Enforces the owned-dialog hint grammar at the shared rendering boundary.
|
|
5
|
+
* Pinned Pi surfaces use their separate adapter and retain upstream wording.
|
|
6
|
+
*/
|
|
7
|
+
export function assertOwnedShortcutHintConventions(entries) {
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
if (entry.key === undefined || entry.key.trim().length === 0)
|
|
10
|
+
continue;
|
|
11
|
+
if (entry.key !== entry.key.trim())
|
|
12
|
+
throw new TypeError("owned shortcut hint keys must not have surrounding whitespace");
|
|
13
|
+
if (entry.action !== entry.action.trim() || entry.action.length === 0) {
|
|
14
|
+
throw new TypeError("owned shortcut hint actions must be nonempty and have no surrounding whitespace");
|
|
15
|
+
}
|
|
16
|
+
if (/^to(?:\s|$)/iu.test(entry.action)) {
|
|
17
|
+
throw new TypeError(`owned shortcut hint actions omit connective "to": ${entry.key} ${entry.action}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
2
21
|
/** Returns an unstyled modal instruction row for width-aware clipping. */
|
|
3
|
-
export function shortcutHintsText(entries, indent = 0) {
|
|
4
|
-
|
|
22
|
+
export function shortcutHintsText(entries, indent = 0, separator = " ") {
|
|
23
|
+
assertOwnedShortcutHintConventions(entries);
|
|
24
|
+
return renderSemanticShortcutHints(entries, { key: text => text, action: text => text }, indent, separator);
|
|
5
25
|
}
|
|
6
|
-
/** Paints one modal instruction row
|
|
7
|
-
export function renderShortcutHints(entries, theme, indent = 0) {
|
|
26
|
+
/** Paints one modal instruction row with shared key/action roles and caller-selected spacing. */
|
|
27
|
+
export function renderShortcutHints(entries, theme, indent = 0, separator = " ") {
|
|
28
|
+
assertOwnedShortcutHintConventions(entries);
|
|
8
29
|
return renderSemanticShortcutHints(entries, {
|
|
9
30
|
key: text => theme.fg("dim", text),
|
|
10
31
|
action: text => theme.fg("muted", text),
|
|
11
|
-
}, indent);
|
|
32
|
+
}, indent, separator);
|
|
33
|
+
}
|
|
34
|
+
/** Clips preceding guidance first so a final canonical close entry remains complete whenever it fits. */
|
|
35
|
+
export function renderShortcutHintsWithClose(entries, theme, width, indent = 0) {
|
|
36
|
+
const hasClose = entries.at(-1)?.key === DIALOG_CLOSE_SHORTCUT_HINT.key
|
|
37
|
+
&& entries.at(-1)?.action === DIALOG_CLOSE_SHORTCUT_HINT.action;
|
|
38
|
+
if (!hasClose)
|
|
39
|
+
return truncateToWidth(renderShortcutHints(entries, theme, indent), width);
|
|
40
|
+
const withoutClose = entries.slice(0, -1);
|
|
41
|
+
const close = renderShortcutHints([DIALOG_CLOSE_SHORTCUT_HINT], theme);
|
|
42
|
+
const closeWidth = displayWidth(close);
|
|
43
|
+
if (width <= closeWidth)
|
|
44
|
+
return truncateToWidth(close, width);
|
|
45
|
+
const bodyWidth = Math.max(0, width - closeWidth - 2);
|
|
46
|
+
const body = bodyWidth < 3 ? "" : truncateToWidth(renderShortcutHints(withoutClose, theme, indent), bodyWidth);
|
|
47
|
+
return displayWidth(body) === 0 ? close : `${body} ${close}`;
|
|
12
48
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* assembled, and any listing is generated from the same declarations — so a
|
|
5
5
|
* listed shortcut is one dispatch would actually invoke.
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { type ShortcutHintEntry } from "./shortcut-hints.js";
|
|
8
8
|
export interface ShortcutDeclaration {
|
|
9
9
|
/** Key token, for example `up`, `shift+down`, `ctrl+c`, or a literal character. */
|
|
10
10
|
readonly key: string;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* assembled, and any listing is generated from the same declarations — so a
|
|
5
5
|
* listed shortcut is one dispatch would actually invoke.
|
|
6
6
|
*/
|
|
7
|
+
import { assertOwnedShortcutHintConventions } from "./shortcut-hints.js";
|
|
7
8
|
export const GLOBAL_SCOPE = "global";
|
|
8
9
|
function overlaps(left, right) {
|
|
9
10
|
return left === right || left === GLOBAL_SCOPE || right === GLOBAL_SCOPE;
|
|
@@ -45,6 +46,9 @@ export class ShortcutRegistry {
|
|
|
45
46
|
#declarations = [];
|
|
46
47
|
#actions = new Map();
|
|
47
48
|
declare(declaration, action) {
|
|
49
|
+
if (declaration.hint !== undefined) {
|
|
50
|
+
assertOwnedShortcutHintConventions([{ key: declaration.hint.keys, action: declaration.hint.does }]);
|
|
51
|
+
}
|
|
48
52
|
this.#declarations.push(declaration);
|
|
49
53
|
this.#actions.set(`${declaration.scope}\u0000${declaration.key}`, action);
|
|
50
54
|
}
|