@jwilger/pi-development-system 0.10.6 → 0.12.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 +9 -1
- package/agents/advisor.md +40 -0
- package/agents/architect.md +66 -0
- package/agents/coder.md +57 -0
- package/agents/implementer.md +47 -0
- package/agents/lens-cagan.md +54 -0
- package/agents/lens-perri.md +54 -0
- package/agents/lens-pichler.md +54 -0
- package/agents/lens-rumelt.md +54 -0
- package/agents/lens-torres.md +54 -0
- package/agents/researcher.md +70 -0
- package/agents/reviewer.md +57 -0
- package/agents/tasker.md +58 -0
- package/agents/writer.md +39 -0
- package/extensions/development-system.ts +4 -0
- package/package.json +5 -3
- package/skills/delegation/SKILL.md +66 -0
- package/src/core/models.ts +18 -0
- package/src/core/routing.ts +67 -0
- package/src/core/spawn-overrides.ts +59 -0
- package/src/jev/questions/route.ts +93 -0
- package/src/state/config.ts +2 -4
- package/src/state/route-task-tool.ts +74 -0
- package/src/subagents/VENDORED.md +40 -0
- package/src/subagents/index.ts +544 -0
- package/src/subagents/orch/inherited-tools.ts +85 -0
- package/src/subagents/orch/mailbox.ts +262 -0
- package/src/subagents/orch/manager.ts +778 -0
- package/src/subagents/orch/paths.ts +74 -0
- package/src/subagents/orch/prompt.ts +25 -0
- package/src/subagents/orch/runtime.ts +791 -0
- package/src/subagents/orch/tools.ts +177 -0
- package/src/subagents/orch/transcript.ts +187 -0
- package/src/subagents/prefs/agent-import.ts +246 -0
- package/src/subagents/prefs/config.ts +1029 -0
- package/src/subagents/prefs/import-discovery.ts +644 -0
- package/src/subagents/prefs/import-instructions.ts +157 -0
- package/src/subagents/prefs/models.ts +85 -0
- package/src/subagents/prefs/settings.ts +371 -0
- package/src/subagents/types.ts +209 -0
- package/src/subagents/ui/agent-navigation-editor.ts +210 -0
- package/src/subagents/ui/dialog.ts +651 -0
- package/src/subagents/ui/import-picker.ts +232 -0
- package/src/subagents/ui/live-agent-view.ts +572 -0
- package/src/subagents/ui/model-picker.ts +452 -0
- package/src/subagents/ui/settings-ui.ts +329 -0
- package/src/subagents/ui/status-ui.ts +545 -0
- package/src/subagents/ui/thread-tree.ts +135 -0
- package/src/subagents/ui/ui.ts +1507 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
6
|
+
export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
7
|
+
export interface ResolvedAgentSettings {
|
|
8
|
+
model: string;
|
|
9
|
+
thinkingLevel: ThinkingLevel;
|
|
10
|
+
}
|
|
11
|
+
export interface AgentType {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
models?: string[];
|
|
15
|
+
/** @deprecated Use ordered models instead. */
|
|
16
|
+
model?: string;
|
|
17
|
+
/** Advisory display names. Not provider/model pins and never used for selection. */
|
|
18
|
+
modelSuggestions?: string[];
|
|
19
|
+
thinkingLevel?: ThinkingLevel;
|
|
20
|
+
/** devsys: per-spawn pin set by the coordinator via agent_spawn; beats preferences and inheritance. */
|
|
21
|
+
spawnOverrides?: { model?: string; thinkingLevel?: ThinkingLevel };
|
|
22
|
+
/** Pi semantic token for the type pill background and task path foreground. */
|
|
23
|
+
color?: string;
|
|
24
|
+
/** Optional literal Nerd Font glyph; displayed only when the labs setting is enabled. */
|
|
25
|
+
icon?: string;
|
|
26
|
+
tools?: { allow?: string[]; block?: string[] };
|
|
27
|
+
systemPrompt: string;
|
|
28
|
+
filePath?: string;
|
|
29
|
+
source?: "bundled" | "user" | "project";
|
|
30
|
+
/**
|
|
31
|
+
* How this definition is customized relative to its base.
|
|
32
|
+
* - fork: a full `<name>.md` copy (owns the system prompt).
|
|
33
|
+
* - override: a settings-only `<name>.yml` merged on top of the base
|
|
34
|
+
* (no Markdown body; unset fields follow the base).
|
|
35
|
+
* Absent for pure bundled definitions and brand-new drafts.
|
|
36
|
+
*/
|
|
37
|
+
customization?: {
|
|
38
|
+
kind: "fork" | "override";
|
|
39
|
+
scope: "user" | "project";
|
|
40
|
+
filePath: string;
|
|
41
|
+
};
|
|
42
|
+
/** Source of the base definition under the customization, if any. */
|
|
43
|
+
baseSource?: "bundled" | "user" | "project";
|
|
44
|
+
/** File path of the base definition under the customization, if any. */
|
|
45
|
+
baseFilePath?: string;
|
|
46
|
+
}
|
|
47
|
+
export type ThreadState = "starting" | "running" | "paused" | "completed" | "failed" | "stopped";
|
|
48
|
+
export interface ThreadView {
|
|
49
|
+
path: string;
|
|
50
|
+
parent: string | null;
|
|
51
|
+
owner: string;
|
|
52
|
+
type: string;
|
|
53
|
+
color?: string;
|
|
54
|
+
icon?: string;
|
|
55
|
+
state: ThreadState;
|
|
56
|
+
task: string;
|
|
57
|
+
status: string;
|
|
58
|
+
output?: string;
|
|
59
|
+
error?: string;
|
|
60
|
+
createdAt: number;
|
|
61
|
+
updatedAt: number;
|
|
62
|
+
sessionFile?: string;
|
|
63
|
+
/** Frozen active time. Renderer adds `Date.now() - startedAt` while a run is active. */
|
|
64
|
+
elapsedMs?: number;
|
|
65
|
+
/** Current run start. Live only; omitted from saved views so reloads do not count offline time. */
|
|
66
|
+
startedAt?: number;
|
|
67
|
+
/** Cumulative input tokens, including cache read/write. Live views include the in-progress message. */
|
|
68
|
+
inputTokens?: number;
|
|
69
|
+
/** Cumulative output tokens. Live views include the in-progress message. */
|
|
70
|
+
outputTokens?: number;
|
|
71
|
+
}
|
|
72
|
+
export type DriverEvent =
|
|
73
|
+
| { kind: "activity" | "error"; text: string }
|
|
74
|
+
| {
|
|
75
|
+
kind: "checkpoint";
|
|
76
|
+
text: string;
|
|
77
|
+
sessionFile?: string;
|
|
78
|
+
sessionLeafId?: string | null;
|
|
79
|
+
}
|
|
80
|
+
| {
|
|
81
|
+
kind: "usage";
|
|
82
|
+
/** Cumulative own-assistant tokens for this driver instance, excluding inherited history. */
|
|
83
|
+
inputTokens: number;
|
|
84
|
+
outputTokens: number;
|
|
85
|
+
/** Streaming message_update snapshot. Omit for authoritative message_end. */
|
|
86
|
+
partial?: boolean;
|
|
87
|
+
};
|
|
88
|
+
/** Ephemeral read-only transcript state; never part of registry/delivery events. */
|
|
89
|
+
export interface TranscriptToolState {
|
|
90
|
+
toolCallId: string;
|
|
91
|
+
toolName: string;
|
|
92
|
+
parentToolCallId?: string;
|
|
93
|
+
args: unknown;
|
|
94
|
+
result?: unknown;
|
|
95
|
+
isError?: boolean;
|
|
96
|
+
state: "running" | "completed";
|
|
97
|
+
}
|
|
98
|
+
export interface TranscriptSnapshot {
|
|
99
|
+
revision: number;
|
|
100
|
+
/** Changes when a driver is replaced; revisions remain monotonic within the manager. */
|
|
101
|
+
generation: number;
|
|
102
|
+
messages: readonly AgentMessage[];
|
|
103
|
+
assistant: AgentMessage | null;
|
|
104
|
+
tools: readonly TranscriptToolState[];
|
|
105
|
+
inheritedCount: number;
|
|
106
|
+
thread?: ThreadView;
|
|
107
|
+
error?: string;
|
|
108
|
+
}
|
|
109
|
+
export interface TranscriptObservation {
|
|
110
|
+
snapshot: TranscriptSnapshot;
|
|
111
|
+
unsubscribe(): void;
|
|
112
|
+
}
|
|
113
|
+
export type TranscriptListener = (snapshot: TranscriptSnapshot) => void;
|
|
114
|
+
|
|
115
|
+
export interface AgentDriver {
|
|
116
|
+
prompt(message: string): Promise<void>;
|
|
117
|
+
steer(message: string): Promise<void>;
|
|
118
|
+
snapshot(): AgentMessage[];
|
|
119
|
+
observeTranscript?(listener: TranscriptListener): TranscriptObservation;
|
|
120
|
+
output(): string;
|
|
121
|
+
abort(): Promise<void>;
|
|
122
|
+
dispose(): void;
|
|
123
|
+
sendUpdate(content: string): Promise<void> | void;
|
|
124
|
+
sessionFile?: string;
|
|
125
|
+
sessionLeafId?: string | null;
|
|
126
|
+
}
|
|
127
|
+
export interface DriverOptions {
|
|
128
|
+
path: string;
|
|
129
|
+
type: AgentType;
|
|
130
|
+
inherited: AgentMessage[];
|
|
131
|
+
tools: ToolDefinition[];
|
|
132
|
+
onEvent(event: DriverEvent): void;
|
|
133
|
+
shouldPause(): boolean;
|
|
134
|
+
parentPath: string | null;
|
|
135
|
+
sessionFile?: string;
|
|
136
|
+
/** Retained transcript of a nested lexical parent. Absent for /root and independent roots. */
|
|
137
|
+
parentSessionFile?: string;
|
|
138
|
+
sessionLeafId?: string | null;
|
|
139
|
+
signal: AbortSignal;
|
|
140
|
+
}
|
|
141
|
+
export interface SavedThreadView {
|
|
142
|
+
path: string;
|
|
143
|
+
parent: string | null;
|
|
144
|
+
owner: string;
|
|
145
|
+
type: string;
|
|
146
|
+
color?: string;
|
|
147
|
+
icon?: string;
|
|
148
|
+
state: ThreadState;
|
|
149
|
+
task: string;
|
|
150
|
+
status: string;
|
|
151
|
+
output?: string;
|
|
152
|
+
error?: string;
|
|
153
|
+
createdAt: number;
|
|
154
|
+
sessionFile?: string;
|
|
155
|
+
sessionLeafId?: string | null;
|
|
156
|
+
/** Frozen cumulative active time. `startedAt` is intentionally not persisted. */
|
|
157
|
+
elapsedMs?: number;
|
|
158
|
+
inputTokens?: number;
|
|
159
|
+
outputTokens?: number;
|
|
160
|
+
}
|
|
161
|
+
export interface SavedThread {
|
|
162
|
+
view: SavedThreadView;
|
|
163
|
+
definition: AgentType;
|
|
164
|
+
/** Reserved threads may not have a transcript yet; preserve their initial context. */
|
|
165
|
+
inherited?: AgentMessage[];
|
|
166
|
+
}
|
|
167
|
+
export interface ThreadService {
|
|
168
|
+
list(): ThreadView[];
|
|
169
|
+
get(path: string): ThreadView;
|
|
170
|
+
output(path: string): string;
|
|
171
|
+
transcript(path: string): Promise<string>;
|
|
172
|
+
/** Optional for portable adapters; the runtime manager always implements this. */
|
|
173
|
+
observeTranscript?(path: string, listener: TranscriptListener): Promise<TranscriptObservation>;
|
|
174
|
+
spawn(
|
|
175
|
+
args: {
|
|
176
|
+
path: string;
|
|
177
|
+
type: string;
|
|
178
|
+
task: string;
|
|
179
|
+
wait?: boolean;
|
|
180
|
+
model?: string;
|
|
181
|
+
thinkingLevel?: string;
|
|
182
|
+
},
|
|
183
|
+
signal?: AbortSignal,
|
|
184
|
+
): Promise<ThreadView>;
|
|
185
|
+
steer(path: string, message: string): Promise<ThreadView>;
|
|
186
|
+
wait(path: string, timeoutMs?: number, signal?: AbortSignal): Promise<ThreadView>;
|
|
187
|
+
update(message: string): ThreadView;
|
|
188
|
+
pause(reason: string): ThreadView;
|
|
189
|
+
stop(path: string): Promise<ThreadView>;
|
|
190
|
+
}
|
|
191
|
+
export type DriverFactory = (options: DriverOptions) => Promise<AgentDriver>;
|
|
192
|
+
export type ThreadEvent =
|
|
193
|
+
| { kind: "change"; thread: ThreadView }
|
|
194
|
+
| { kind: "metrics"; thread: ThreadView }
|
|
195
|
+
| { kind: "update"; thread: ThreadView; message: string; recipient: string }
|
|
196
|
+
| { kind: "settled"; thread: ThreadView; recipient: string };
|
|
197
|
+
export interface ManagerOptions {
|
|
198
|
+
createDriver: DriverFactory;
|
|
199
|
+
rootSnapshot(): AgentMessage[];
|
|
200
|
+
getType(name: string): AgentType;
|
|
201
|
+
toolsFor(path: string): ToolDefinition[];
|
|
202
|
+
onEvent?(event: ThreadEvent): void;
|
|
203
|
+
/** Maximum levels including the main conversation as L1; defaults to 3. */
|
|
204
|
+
maxLevels?: number;
|
|
205
|
+
/** @deprecated Internal legacy path-depth override; prefer maxLevels. */
|
|
206
|
+
maxDepth?: number;
|
|
207
|
+
maxThreads?: number;
|
|
208
|
+
maxConcurrent?: number;
|
|
209
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
|
+
import {
|
|
3
|
+
CustomEditor,
|
|
4
|
+
type CustomEditorOptions,
|
|
5
|
+
type KeybindingsManager,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
getKeybindings,
|
|
9
|
+
Key,
|
|
10
|
+
matchesKey,
|
|
11
|
+
type EditorTheme,
|
|
12
|
+
type Keybinding,
|
|
13
|
+
type TUI,
|
|
14
|
+
} from "@earendil-works/pi-tui";
|
|
15
|
+
|
|
16
|
+
export interface AgentNavigationEditorOptions {
|
|
17
|
+
/** Must return the whole navigation interaction, so repeated Left cannot reopen it. */
|
|
18
|
+
openTree(): void | Promise<void>;
|
|
19
|
+
canOpen(): boolean;
|
|
20
|
+
/** Session-local widget collapse; never saves the configured display mode. */
|
|
21
|
+
collapseWidget?(): void | Promise<void>;
|
|
22
|
+
canCollapse?(): boolean;
|
|
23
|
+
/** Root ownership token; changing it invalidates queued transitions. */
|
|
24
|
+
generation(): unknown;
|
|
25
|
+
onError?(error: unknown): void;
|
|
26
|
+
/**
|
|
27
|
+
* Chronological user prompts for reload/new/resume/fork. Omit on startup:
|
|
28
|
+
* Pi hydrates the installed editor after the startup session_start event.
|
|
29
|
+
*/
|
|
30
|
+
initialHistory?: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Physical arrow boundary adapter. It owns no session/history internals and never
|
|
35
|
+
* changes the draft. Install once per root UI lifecycle, not for tree navigation.
|
|
36
|
+
* Another custom editor occupies the same host slot; skip installation rather than stack.
|
|
37
|
+
*/
|
|
38
|
+
export class AgentNavigationEditor extends CustomEditor {
|
|
39
|
+
private pending: "left" | "right" | undefined;
|
|
40
|
+
private disposed = false;
|
|
41
|
+
private dispatchDepth = 0;
|
|
42
|
+
private inputEpoch = 0;
|
|
43
|
+
private pastePending = false;
|
|
44
|
+
private jumpPending = false;
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
tui: TUI,
|
|
48
|
+
theme: EditorTheme,
|
|
49
|
+
private navigationKeys: KeybindingsManager,
|
|
50
|
+
private navigation: AgentNavigationEditorOptions,
|
|
51
|
+
options?: CustomEditorOptions,
|
|
52
|
+
) {
|
|
53
|
+
super(tui, theme, navigationKeys, options);
|
|
54
|
+
// SessionStartEvent.reason distinguishes startup (host hydrates AFTER
|
|
55
|
+
// installation) from reload/replacement (explicit public hydration needed).
|
|
56
|
+
// Never infer replay from prompt text: intentional repetitions are valid.
|
|
57
|
+
for (const text of navigation.initialHistory ?? []) this.addToHistory(text);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Invalidates queued work without resetting a competing extension's editor. */
|
|
61
|
+
dispose(): void {
|
|
62
|
+
this.disposed = true;
|
|
63
|
+
this.inputEpoch++;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private ordinaryArrow(data: string, direction: "left" | "right"): boolean {
|
|
67
|
+
if (!matchesKey(data, direction === "left" ? Key.left : Key.right)) return false;
|
|
68
|
+
const action = direction === "left" ? "tui.editor.cursorLeft" : "tui.editor.cursorRight";
|
|
69
|
+
// Editor uses the global TUI bindings; CustomEditor uses the injected app
|
|
70
|
+
// manager. Both must agree, and a conflicting remap wins conservatively.
|
|
71
|
+
for (const keys of [this.navigationKeys, getKeybindings()]) {
|
|
72
|
+
if (!keys.matches(data, action)) return false;
|
|
73
|
+
for (const other of Object.keys(keys.getResolvedBindings())) {
|
|
74
|
+
if (
|
|
75
|
+
other !== action &&
|
|
76
|
+
(other.startsWith("tui.editor.") ||
|
|
77
|
+
other.startsWith("tui.input.") ||
|
|
78
|
+
other.startsWith("tui.altScreen.") ||
|
|
79
|
+
other.startsWith("app.")) &&
|
|
80
|
+
keys.matches(data, other as Keybinding)
|
|
81
|
+
)
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
override handleInput(data: string): void {
|
|
89
|
+
const outer = this.dispatchDepth === 0;
|
|
90
|
+
const direction = this.ordinaryArrow(data, "left") ? "left"
|
|
91
|
+
: this.ordinaryArrow(data, "right") ? "right" : undefined;
|
|
92
|
+
if (outer && (!direction || (this.pending && this.pending !== direction))) this.inputEpoch++;
|
|
93
|
+
const previousPaste = this.pastePending;
|
|
94
|
+
const wasPaste = previousPaste || data.includes("\x1b[200~");
|
|
95
|
+
// Update before dispatch: Pi recursively sends a suffix after the end
|
|
96
|
+
// marker through this.handleInput, and that suffix is no longer paste.
|
|
97
|
+
if (data.includes("\x1b[200~")) this.pastePending = true;
|
|
98
|
+
if (wasPaste && data.includes("\x1b[201~")) this.pastePending = false;
|
|
99
|
+
const wasJump = this.jumpPending;
|
|
100
|
+
const cursor = this.getCursor();
|
|
101
|
+
const text = this.getText();
|
|
102
|
+
const lines = text.split("\n");
|
|
103
|
+
const atBoundary = direction === "left"
|
|
104
|
+
? cursor.line === 0 && cursor.col === 0
|
|
105
|
+
: direction === "right" && cursor.line === lines.length - 1 &&
|
|
106
|
+
cursor.col === lines.at(-1)!.length;
|
|
107
|
+
const eligible =
|
|
108
|
+
outer &&
|
|
109
|
+
direction !== undefined &&
|
|
110
|
+
this.focused &&
|
|
111
|
+
!this.disposed &&
|
|
112
|
+
!wasPaste &&
|
|
113
|
+
!wasJump &&
|
|
114
|
+
!this.isShowingAutocomplete() &&
|
|
115
|
+
atBoundary;
|
|
116
|
+
const generation = eligible ? this.navigation.generation() : undefined;
|
|
117
|
+
let changed = false;
|
|
118
|
+
let consumed = false;
|
|
119
|
+
const originalChange = this.onChange;
|
|
120
|
+
const originalShortcut = this.onExtensionShortcut;
|
|
121
|
+
const observeChange = (value: string) => {
|
|
122
|
+
changed = true;
|
|
123
|
+
originalChange?.call(this, value);
|
|
124
|
+
};
|
|
125
|
+
const observeShortcut = (value: string) => {
|
|
126
|
+
const handled = originalShortcut?.call(this, value) ?? false;
|
|
127
|
+
consumed ||= handled && value === data;
|
|
128
|
+
return handled;
|
|
129
|
+
};
|
|
130
|
+
this.onChange = observeChange;
|
|
131
|
+
this.onExtensionShortcut = observeShortcut;
|
|
132
|
+
this.dispatchDepth++;
|
|
133
|
+
try {
|
|
134
|
+
// Exactly one stock dispatch, preserving host-wired application controls.
|
|
135
|
+
super.handleInput(data);
|
|
136
|
+
} finally {
|
|
137
|
+
this.dispatchDepth--;
|
|
138
|
+
if (this.onChange === observeChange) this.onChange = originalChange;
|
|
139
|
+
if (this.onExtensionShortcut === observeShortcut) this.onExtensionShortcut = originalShortcut;
|
|
140
|
+
if (consumed) this.pastePending = previousPaste;
|
|
141
|
+
if (!consumed) {
|
|
142
|
+
// Track only public input; over-suppress when precedence is ambiguous.
|
|
143
|
+
// Recursive paste suffix dispatch updates guards, but never opens UI.
|
|
144
|
+
if (!wasPaste) {
|
|
145
|
+
const keys = getKeybindings();
|
|
146
|
+
const jump =
|
|
147
|
+
keys.matches(data, "tui.editor.jumpForward") ||
|
|
148
|
+
keys.matches(data, "tui.editor.jumpBackward");
|
|
149
|
+
this.jumpPending = !wasJump && jump;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const after = this.getCursor();
|
|
154
|
+
if (
|
|
155
|
+
eligible &&
|
|
156
|
+
!consumed &&
|
|
157
|
+
!changed &&
|
|
158
|
+
!this.isShowingAutocomplete() &&
|
|
159
|
+
this.getText() === text &&
|
|
160
|
+
after.line === cursor.line &&
|
|
161
|
+
after.col === cursor.col
|
|
162
|
+
) {
|
|
163
|
+
this.queueNavigation(text, cursor, generation, direction!);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private queueNavigation(
|
|
168
|
+
text: string,
|
|
169
|
+
cursor: { line: number; col: number },
|
|
170
|
+
generation: unknown,
|
|
171
|
+
direction: "left" | "right",
|
|
172
|
+
): void {
|
|
173
|
+
const allowed = () => direction === "left"
|
|
174
|
+
? this.navigation.canOpen()
|
|
175
|
+
: !!this.navigation.collapseWidget && (this.navigation.canCollapse?.() ?? false);
|
|
176
|
+
if (this.pending || !allowed()) return;
|
|
177
|
+
this.pending = direction;
|
|
178
|
+
const epoch = this.inputEpoch;
|
|
179
|
+
queueMicrotask(() => {
|
|
180
|
+
const after = this.getCursor();
|
|
181
|
+
if (
|
|
182
|
+
this.disposed ||
|
|
183
|
+
!this.focused ||
|
|
184
|
+
this.inputEpoch !== epoch ||
|
|
185
|
+
this.navigation.generation() !== generation ||
|
|
186
|
+
!allowed() ||
|
|
187
|
+
this.isShowingAutocomplete() ||
|
|
188
|
+
this.getText() !== text ||
|
|
189
|
+
after.line !== cursor.line ||
|
|
190
|
+
after.col !== cursor.col
|
|
191
|
+
) {
|
|
192
|
+
this.pending = undefined;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
// Call within the checked microtask: an extra Promise.then would leave
|
|
197
|
+
// a gap in which root ownership could change after validation.
|
|
198
|
+
void Promise.resolve(direction === "left"
|
|
199
|
+
? this.navigation.openTree() : this.navigation.collapseWidget!())
|
|
200
|
+
.catch((error: unknown) => this.navigation.onError?.(error))
|
|
201
|
+
.finally(() => {
|
|
202
|
+
this.pending = undefined;
|
|
203
|
+
});
|
|
204
|
+
} catch (error) {
|
|
205
|
+
this.pending = undefined;
|
|
206
|
+
this.navigation.onError?.(error);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|