pi-ui-extend 0.1.70 → 0.1.71
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/app.js +22 -1
- package/dist/app/commands/command-host.d.ts +7 -0
- package/dist/app/commands/command-host.js +10 -1
- package/dist/app/commands/command-model-actions.d.ts +1 -1
- package/dist/app/commands/command-model-actions.js +36 -2
- package/dist/app/commands/command-navigation-actions.d.ts +1 -1
- package/dist/app/commands/command-navigation-actions.js +40 -6
- package/dist/app/commands/command-session-actions.d.ts +1 -1
- package/dist/app/commands/command-session-actions.js +56 -0
- package/dist/app/commands/shell-controller.d.ts +9 -2
- package/dist/app/commands/shell-controller.js +32 -21
- package/dist/app/extensions/extension-actions-controller.d.ts +3 -0
- package/dist/app/extensions/extension-actions-controller.js +25 -7
- package/dist/app/extensions/extension-ui-controller.d.ts +5 -1
- package/dist/app/extensions/extension-ui-controller.js +104 -64
- package/dist/app/input/input-action-controller.d.ts +4 -1
- package/dist/app/input/input-action-controller.js +68 -27
- package/dist/app/input/input-controller.d.ts +3 -0
- package/dist/app/input/input-controller.js +45 -1
- package/dist/app/input/input-paste-handler.d.ts +3 -0
- package/dist/app/input/input-paste-handler.js +21 -12
- package/dist/app/input/prompt-enhancer-controller.d.ts +1 -0
- package/dist/app/input/prompt-enhancer-controller.js +11 -5
- package/dist/app/input/voice-controller.d.ts +4 -0
- package/dist/app/input/voice-controller.js +76 -35
- package/dist/app/popup/popup-action-controller.d.ts +3 -0
- package/dist/app/popup/popup-action-controller.js +43 -8
- package/dist/app/session/queued-message-controller.d.ts +5 -4
- package/dist/app/session/queued-message-controller.js +32 -27
- package/dist/app/session/request-history.d.ts +2 -0
- package/dist/app/session/request-history.js +22 -15
- package/dist/app/session/session-event-controller.js +28 -8
- package/dist/app/session/session-history.js +8 -2
- package/dist/app/session/session-lifecycle-controller.d.ts +7 -0
- package/dist/app/session/session-lifecycle-controller.js +91 -19
- package/dist/app/session/tabs-controller.d.ts +26 -0
- package/dist/app/session/tabs-controller.js +469 -96
- package/dist/app/terminal/terminal-controller.d.ts +4 -0
- package/dist/app/terminal/terminal-controller.js +38 -11
- package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
- package/dist/app/workspace/workspace-actions-controller.js +37 -9
- package/dist/input-editor.d.ts +2 -0
- package/dist/input-editor.js +17 -0
- package/docs/concurrency.md +76 -0
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +29 -14
- package/external/pi-tools-suite/src/todo/index.ts +2 -2
- package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
- package/package.json +4 -4
|
@@ -3,6 +3,7 @@ import { mkdir, readFile, rm } from "node:fs/promises";
|
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { captureCommandScope, isCommandRuntimeActive, isCommandScopeActive, } from "./command-host.js";
|
|
6
7
|
import { getIdleRuntime, getRuntime, parsePathArgument } from "./command-runtime.js";
|
|
7
8
|
import { createId } from "../id.js";
|
|
8
9
|
import { runProcess } from "../process.js";
|
|
@@ -22,6 +23,7 @@ export class SessionCommandActions {
|
|
|
22
23
|
const runtime = getIdleRuntime(this.host, "export");
|
|
23
24
|
if (!runtime)
|
|
24
25
|
return;
|
|
26
|
+
const scope = captureCommandScope(this.host);
|
|
25
27
|
const outputPath = parsePathArgument(argumentsText);
|
|
26
28
|
const resolvedOutputPath = outputPath ? resolve(runtime.cwd, outputPath) : undefined;
|
|
27
29
|
this.host.setStatus("exporting session");
|
|
@@ -29,6 +31,8 @@ export class SessionCommandActions {
|
|
|
29
31
|
const filePath = resolvedOutputPath?.endsWith(".jsonl")
|
|
30
32
|
? runtime.session.exportToJsonl(resolvedOutputPath)
|
|
31
33
|
: await runtime.session.exportToHtml(resolvedOutputPath);
|
|
34
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
35
|
+
return;
|
|
32
36
|
this.host.addEntry({ id: createId("system"), kind: "system", text: `Session exported to: ${filePath}` });
|
|
33
37
|
this.host.setSessionStatus(runtime.session);
|
|
34
38
|
this.host.toast.success("Session exported");
|
|
@@ -44,6 +48,8 @@ export class SessionCommandActions {
|
|
|
44
48
|
this.host.setStatus("importing session");
|
|
45
49
|
this.host.render();
|
|
46
50
|
const result = await runtime.importFromJsonl(resolvedInputPath);
|
|
51
|
+
if (!isCommandRuntimeActive(this.host, runtime))
|
|
52
|
+
return;
|
|
47
53
|
if (result.cancelled) {
|
|
48
54
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "Import cancelled." });
|
|
49
55
|
this.host.setSessionStatus(runtime.session);
|
|
@@ -55,17 +61,26 @@ export class SessionCommandActions {
|
|
|
55
61
|
const runtime = getIdleRuntime(this.host, "share");
|
|
56
62
|
if (!runtime)
|
|
57
63
|
return;
|
|
64
|
+
const scope = captureCommandScope(this.host);
|
|
58
65
|
const authResult = await runProcess("gh", ["auth", "status"], { maxBufferBytes: 32 * 1024 });
|
|
66
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
67
|
+
return;
|
|
59
68
|
if (authResult.status !== 0)
|
|
60
69
|
throw new Error("GitHub CLI is not installed or is not logged in. Run `gh auth login` first.");
|
|
61
70
|
const shareDir = join(getAgentDir(), "pix");
|
|
62
71
|
await mkdir(shareDir, { recursive: true });
|
|
72
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
73
|
+
return;
|
|
63
74
|
const tmpFile = join(shareDir, `session-share-${randomUUID()}.html`);
|
|
64
75
|
try {
|
|
65
76
|
this.host.setStatus("creating share gist");
|
|
66
77
|
this.host.render();
|
|
67
78
|
await runtime.session.exportToHtml(tmpFile);
|
|
79
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
80
|
+
return;
|
|
68
81
|
const gistResult = await runProcess("gh", ["gist", "create", "--public=false", tmpFile], { maxBufferBytes: 64 * 1024 });
|
|
82
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
83
|
+
return;
|
|
69
84
|
if (gistResult.status !== 0)
|
|
70
85
|
throw new Error(gistResult.stderr?.trim() || "Failed to create gist");
|
|
71
86
|
const gistUrl = gistResult.stdout.trim();
|
|
@@ -81,10 +96,13 @@ export class SessionCommandActions {
|
|
|
81
96
|
const runtime = getRuntime(this.host, "copy");
|
|
82
97
|
if (!runtime)
|
|
83
98
|
return;
|
|
99
|
+
const scope = captureCommandScope(this.host);
|
|
84
100
|
const text = runtime.session.getLastAssistantText();
|
|
85
101
|
if (!text)
|
|
86
102
|
throw new Error("No agent messages to copy yet");
|
|
87
103
|
await copyTextToClipboard(text);
|
|
104
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
105
|
+
return;
|
|
88
106
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "Copied last agent message to clipboard." });
|
|
89
107
|
this.host.setSessionStatus(runtime.session);
|
|
90
108
|
this.host.toast.success("Copied last agent message");
|
|
@@ -103,6 +121,7 @@ export class SessionCommandActions {
|
|
|
103
121
|
return;
|
|
104
122
|
const name = argumentsText.trim();
|
|
105
123
|
if (!name) {
|
|
124
|
+
const scope = captureCommandScope(this.host);
|
|
106
125
|
const branch = runtime.session.sessionManager.getBranch();
|
|
107
126
|
const firstPrompt = firstUserMessageText(branch);
|
|
108
127
|
if (!firstPrompt)
|
|
@@ -117,6 +136,8 @@ export class SessionCommandActions {
|
|
|
117
136
|
for (const modelRef of modelRefs) {
|
|
118
137
|
for (let attempt = 0; attempt < config.generationAttempts; attempt++) {
|
|
119
138
|
generatedName = await generateSessionTitleWithRuntime(firstPrompt.slice(0, config.maxInputChars).trim(), runtime.services.modelRuntime, config, modelRef, AbortSignal.timeout(config.timeoutMs));
|
|
139
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
140
|
+
return;
|
|
120
141
|
if (generatedName)
|
|
121
142
|
break;
|
|
122
143
|
}
|
|
@@ -142,7 +163,10 @@ export class SessionCommandActions {
|
|
|
142
163
|
const runtime = getRuntime(this.host, "session");
|
|
143
164
|
if (!runtime)
|
|
144
165
|
return;
|
|
166
|
+
const scope = captureCommandScope(this.host);
|
|
145
167
|
const stats = await getCompleteSessionStats(runtime.session);
|
|
168
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
169
|
+
return;
|
|
146
170
|
const lines = [
|
|
147
171
|
createStartupInfoMessage(runtime),
|
|
148
172
|
"",
|
|
@@ -173,9 +197,12 @@ export class SessionCommandActions {
|
|
|
173
197
|
const runtime = getRuntime(this.host, "usage");
|
|
174
198
|
if (!runtime)
|
|
175
199
|
return;
|
|
200
|
+
const scope = captureCommandScope(this.host);
|
|
176
201
|
this.host.setStatus("loading usage");
|
|
177
202
|
this.host.render();
|
|
178
203
|
const accountReport = await queryAccountUsageReport();
|
|
204
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
205
|
+
return;
|
|
179
206
|
const accountUsage = formatAccountUsageReport(accountReport);
|
|
180
207
|
if (accountUsage) {
|
|
181
208
|
this.host.addEntry({ id: createId("system"), kind: "system", text: accountUsage });
|
|
@@ -183,6 +210,8 @@ export class SessionCommandActions {
|
|
|
183
210
|
return;
|
|
184
211
|
}
|
|
185
212
|
const stats = await getCompleteSessionStats(runtime.session);
|
|
213
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
214
|
+
return;
|
|
186
215
|
const contextUsage = stats.contextUsage ?? runtime.session.getContextUsage();
|
|
187
216
|
const model = runtime.session.model ? this.host.modelRef(runtime.session.model) : "not selected";
|
|
188
217
|
const lines = [
|
|
@@ -204,8 +233,11 @@ export class SessionCommandActions {
|
|
|
204
233
|
this.host.setSessionStatus(runtime.session);
|
|
205
234
|
}
|
|
206
235
|
async runChangelogCommand() {
|
|
236
|
+
const scope = captureCommandScope(this.host);
|
|
207
237
|
const changelogPath = join(this.piPackageRoot(), "CHANGELOG.md");
|
|
208
238
|
const raw = await readFile(changelogPath, "utf8");
|
|
239
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
240
|
+
return;
|
|
209
241
|
const lines = raw.trim().split(/\r?\n/).slice(0, 140);
|
|
210
242
|
this.host.addEntry({ id: createId("system"), kind: "system", text: lines.join("\n") });
|
|
211
243
|
this.host.setSessionStatus(this.host.runtime()?.session);
|
|
@@ -214,6 +246,7 @@ export class SessionCommandActions {
|
|
|
214
246
|
const runtime = getRuntime(this.host, "update");
|
|
215
247
|
if (!runtime)
|
|
216
248
|
return;
|
|
249
|
+
const scope = captureCommandScope(this.host);
|
|
217
250
|
const options = parsePixUpdateArgs(splitUpdateArguments(argumentsText));
|
|
218
251
|
if (options.help) {
|
|
219
252
|
this.host.addEntry({ id: createId("system"), kind: "system", text: pixUpdateUsage() });
|
|
@@ -223,6 +256,8 @@ export class SessionCommandActions {
|
|
|
223
256
|
this.host.setStatus("checking updates");
|
|
224
257
|
this.host.render();
|
|
225
258
|
const result = await checkPixUpdate();
|
|
259
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
260
|
+
return;
|
|
226
261
|
const globalPiResult = checkGlobalPiInstall(result.packageRoot);
|
|
227
262
|
const forceHint = options.force ? "\n\n/update is check-only. To force a reinstall, run `pix update --force` in your shell and restart Pix." : "";
|
|
228
263
|
this.host.addEntry({
|
|
@@ -263,16 +298,23 @@ export class SessionCommandActions {
|
|
|
263
298
|
const runtime = getIdleRuntime(this.host, "reload");
|
|
264
299
|
if (!runtime)
|
|
265
300
|
return;
|
|
301
|
+
const scope = captureCommandScope(this.host);
|
|
266
302
|
this.host.setStatus("reloading");
|
|
267
303
|
this.host.render();
|
|
268
304
|
try {
|
|
269
305
|
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
306
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
307
|
+
return;
|
|
270
308
|
await runtime.session.reload();
|
|
309
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
310
|
+
return;
|
|
271
311
|
this.host.setSessionStatus(runtime.session);
|
|
272
312
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "Reloaded keybindings, extensions, skills, prompts, themes" });
|
|
273
313
|
this.host.toast.success("Reloaded resources");
|
|
274
314
|
}
|
|
275
315
|
catch (error) {
|
|
316
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
317
|
+
return;
|
|
276
318
|
this.host.setSessionStatus(runtime.session);
|
|
277
319
|
this.host.addEntry({ id: createId("error"), kind: "error", text: `Reload failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
278
320
|
this.host.toast.error("Reload failed");
|
|
@@ -282,6 +324,7 @@ export class SessionCommandActions {
|
|
|
282
324
|
const runtime = getIdleRuntime(this.host, "delete");
|
|
283
325
|
if (!runtime)
|
|
284
326
|
return;
|
|
327
|
+
const scope = captureCommandScope(this.host);
|
|
285
328
|
const sessionManager = runtime.session.sessionManager;
|
|
286
329
|
const currentSessionFile = sessionManager.getSessionFile();
|
|
287
330
|
const currentSessionId = sessionManager.getSessionId();
|
|
@@ -304,6 +347,8 @@ export class SessionCommandActions {
|
|
|
304
347
|
},
|
|
305
348
|
{ value: false, label: "Cancel" },
|
|
306
349
|
], { title: "Delete session?", searchable: false, preserveStatus: true });
|
|
350
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
351
|
+
return;
|
|
307
352
|
if (confirm !== true) {
|
|
308
353
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "Delete cancelled." });
|
|
309
354
|
this.host.setSessionStatus(runtime.session);
|
|
@@ -317,6 +362,8 @@ export class SessionCommandActions {
|
|
|
317
362
|
if (deleteCurrent) {
|
|
318
363
|
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
319
364
|
const result = await runtime.newSession();
|
|
365
|
+
if (!isCommandRuntimeActive(this.host, runtime))
|
|
366
|
+
return;
|
|
320
367
|
if (result.cancelled) {
|
|
321
368
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "Delete succeeded, but new session was cancelled." });
|
|
322
369
|
this.host.setSessionStatus(runtime.session);
|
|
@@ -334,6 +381,8 @@ export class SessionCommandActions {
|
|
|
334
381
|
this.host.setSessionStatus(runtime.session);
|
|
335
382
|
}
|
|
336
383
|
else {
|
|
384
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
385
|
+
return;
|
|
337
386
|
this.host.addEntry({
|
|
338
387
|
id: createId("system"),
|
|
339
388
|
kind: "system",
|
|
@@ -358,7 +407,11 @@ export class SessionCommandActions {
|
|
|
358
407
|
this.host.setStatus("starting new session");
|
|
359
408
|
this.host.render();
|
|
360
409
|
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
410
|
+
if (!isCommandRuntimeActive(this.host, runtime))
|
|
411
|
+
return;
|
|
361
412
|
const result = await runtime.newSession();
|
|
413
|
+
if (!isCommandRuntimeActive(this.host, runtime))
|
|
414
|
+
return;
|
|
362
415
|
if (result.cancelled) {
|
|
363
416
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "New session cancelled." });
|
|
364
417
|
this.host.setSessionStatus(runtime.session);
|
|
@@ -378,6 +431,7 @@ export class SessionCommandActions {
|
|
|
378
431
|
const runtime = getRuntime(this.host, "compact");
|
|
379
432
|
if (!runtime)
|
|
380
433
|
return;
|
|
434
|
+
const scope = captureCommandScope(this.host);
|
|
381
435
|
if (runtime.session.isCompacting) {
|
|
382
436
|
this.host.toast.warning("Compaction already running");
|
|
383
437
|
return;
|
|
@@ -385,6 +439,8 @@ export class SessionCommandActions {
|
|
|
385
439
|
this.host.setStatus(customInstructions ? "compacting with instructions" : "compacting");
|
|
386
440
|
this.host.render();
|
|
387
441
|
const result = await runtime.session.compact(customInstructions);
|
|
442
|
+
if (!isCommandScopeActive(this.host, scope))
|
|
443
|
+
return;
|
|
388
444
|
this.host.addEntry({
|
|
389
445
|
id: createId("system"),
|
|
390
446
|
kind: "system",
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { type InteractiveShellCommandResult } from "./shell-command.js";
|
|
1
|
+
import { type ChatShellCommandHandlers, type InteractiveShellCommandResult, type RunningChatShellCommand } from "./shell-command.js";
|
|
2
2
|
import type { Entry, SessionActivity } from "../types.js";
|
|
3
|
+
type AppShellControllerDeps = {
|
|
4
|
+
runChatShellCommand(command: string, cwd: string, handlers: ChatShellCommandHandlers): RunningChatShellCommand;
|
|
5
|
+
};
|
|
3
6
|
export type AppShellControllerHost = {
|
|
4
7
|
readonly cwd: string;
|
|
5
8
|
isRunning(): boolean;
|
|
9
|
+
activeScopeKey(): string | undefined;
|
|
6
10
|
addEntry(entry: Entry): void;
|
|
7
11
|
touchEntry(entry: Entry): void;
|
|
8
12
|
setStatus(status: string): void;
|
|
@@ -13,9 +17,10 @@ export type AppShellControllerHost = {
|
|
|
13
17
|
};
|
|
14
18
|
export declare class AppShellController {
|
|
15
19
|
private readonly host;
|
|
20
|
+
private readonly deps;
|
|
16
21
|
private activeRun;
|
|
17
22
|
private renderTimer;
|
|
18
|
-
constructor(host: AppShellControllerHost);
|
|
23
|
+
constructor(host: AppShellControllerHost, deps?: AppShellControllerDeps);
|
|
19
24
|
isRunning(): boolean;
|
|
20
25
|
run(command: string): Promise<InteractiveShellCommandResult>;
|
|
21
26
|
sendInput(text: string): boolean;
|
|
@@ -26,4 +31,6 @@ export declare class AppShellController {
|
|
|
26
31
|
private finishEntry;
|
|
27
32
|
private touchAndScheduleRender;
|
|
28
33
|
private flushRender;
|
|
34
|
+
private isScopeActive;
|
|
29
35
|
}
|
|
36
|
+
export {};
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { createId } from "../id.js";
|
|
2
|
-
import { runChatShellCommand } from "./shell-command.js";
|
|
2
|
+
import { runChatShellCommand, } from "./shell-command.js";
|
|
3
3
|
const SHELL_RENDER_THROTTLE_MS = 33;
|
|
4
|
+
const defaultDeps = { runChatShellCommand };
|
|
4
5
|
export class AppShellController {
|
|
5
6
|
host;
|
|
7
|
+
deps;
|
|
6
8
|
activeRun;
|
|
7
9
|
renderTimer;
|
|
8
|
-
constructor(host) {
|
|
10
|
+
constructor(host, deps = defaultDeps) {
|
|
9
11
|
this.host = host;
|
|
12
|
+
this.deps = deps;
|
|
10
13
|
}
|
|
11
14
|
isRunning() {
|
|
12
|
-
return this.activeRun !== undefined;
|
|
15
|
+
return this.activeRun !== undefined && this.isScopeActive(this.activeRun.scopeKey);
|
|
13
16
|
}
|
|
14
17
|
async run(command) {
|
|
15
18
|
if (this.activeRun)
|
|
16
19
|
throw new Error("A shell command is already running");
|
|
20
|
+
const scopeKey = this.host.activeScopeKey();
|
|
17
21
|
const entry = {
|
|
18
22
|
id: createId("shell"),
|
|
19
23
|
kind: "shell",
|
|
@@ -26,11 +30,11 @@ export class AppShellController {
|
|
|
26
30
|
this.host.setStatus(`shell: ${command}`);
|
|
27
31
|
this.host.setSessionActivity("running");
|
|
28
32
|
this.host.render();
|
|
29
|
-
const runningCommand = runChatShellCommand(command, this.host.cwd, {
|
|
30
|
-
onOutput: (chunk) => this.appendOutput(entry, chunk),
|
|
31
|
-
onSettled: (result) => this.finishEntry(entry, result),
|
|
33
|
+
const runningCommand = this.deps.runChatShellCommand(command, this.host.cwd, {
|
|
34
|
+
onOutput: (chunk) => this.appendOutput(entry, chunk, scopeKey),
|
|
35
|
+
onSettled: (result) => this.finishEntry(entry, result, scopeKey),
|
|
32
36
|
});
|
|
33
|
-
this.activeRun = { entry, command: runningCommand };
|
|
37
|
+
this.activeRun = { entry, command: runningCommand, scopeKey };
|
|
34
38
|
try {
|
|
35
39
|
return await runningCommand.done;
|
|
36
40
|
}
|
|
@@ -38,24 +42,26 @@ export class AppShellController {
|
|
|
38
42
|
if (this.activeRun?.entry === entry)
|
|
39
43
|
this.activeRun = undefined;
|
|
40
44
|
this.flushRender();
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
this.host.
|
|
45
|
+
if (this.isScopeActive(scopeKey)) {
|
|
46
|
+
this.host.restoreSessionStatus();
|
|
47
|
+
if (this.host.isRunning())
|
|
48
|
+
this.host.render();
|
|
49
|
+
}
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
sendInput(text) {
|
|
47
53
|
const activeRun = this.activeRun;
|
|
48
|
-
if (!activeRun)
|
|
54
|
+
if (!activeRun || !this.isScopeActive(activeRun.scopeKey))
|
|
49
55
|
return false;
|
|
50
56
|
const input = text.endsWith("\n") ? text : `${text}\n`;
|
|
51
|
-
this.appendInputEcho(activeRun.entry, input);
|
|
57
|
+
this.appendInputEcho(activeRun.entry, input, activeRun.scopeKey);
|
|
52
58
|
return activeRun.command.writeInput(input);
|
|
53
59
|
}
|
|
54
60
|
interrupt() {
|
|
55
61
|
const activeRun = this.activeRun;
|
|
56
|
-
if (!activeRun)
|
|
62
|
+
if (!activeRun || !this.isScopeActive(activeRun.scopeKey))
|
|
57
63
|
return false;
|
|
58
|
-
this.appendOutput(activeRun.entry, "\n^C\n");
|
|
64
|
+
this.appendOutput(activeRun.entry, "\n^C\n", activeRun.scopeKey);
|
|
59
65
|
return activeRun.command.interrupt();
|
|
60
66
|
}
|
|
61
67
|
dispose() {
|
|
@@ -66,23 +72,25 @@ export class AppShellController {
|
|
|
66
72
|
this.activeRun = undefined;
|
|
67
73
|
activeRun?.command.kill("SIGTERM");
|
|
68
74
|
}
|
|
69
|
-
appendOutput(entry, chunk) {
|
|
75
|
+
appendOutput(entry, chunk, scopeKey) {
|
|
70
76
|
entry.output = appendTerminalChunk(entry.output, chunk);
|
|
71
|
-
this.touchAndScheduleRender(entry);
|
|
77
|
+
this.touchAndScheduleRender(entry, scopeKey);
|
|
72
78
|
}
|
|
73
|
-
appendInputEcho(entry, input) {
|
|
79
|
+
appendInputEcho(entry, input, scopeKey) {
|
|
74
80
|
entry.output = appendTerminalChunk(entry.output, formatInputEcho(input));
|
|
75
|
-
this.touchAndScheduleRender(entry);
|
|
81
|
+
this.touchAndScheduleRender(entry, scopeKey);
|
|
76
82
|
}
|
|
77
|
-
finishEntry(entry, result) {
|
|
83
|
+
finishEntry(entry, result, scopeKey) {
|
|
78
84
|
entry.status = "done";
|
|
79
85
|
entry.exitCode = result.exitCode;
|
|
80
86
|
entry.signal = result.signal;
|
|
81
87
|
if (result.error)
|
|
82
88
|
entry.error = result.error;
|
|
83
|
-
this.touchAndScheduleRender(entry);
|
|
89
|
+
this.touchAndScheduleRender(entry, scopeKey);
|
|
84
90
|
}
|
|
85
|
-
touchAndScheduleRender(entry) {
|
|
91
|
+
touchAndScheduleRender(entry, scopeKey) {
|
|
92
|
+
if (!this.isScopeActive(scopeKey))
|
|
93
|
+
return;
|
|
86
94
|
this.host.touchEntry(entry);
|
|
87
95
|
if (!this.host.isRunning() || this.renderTimer)
|
|
88
96
|
return;
|
|
@@ -99,6 +107,9 @@ export class AppShellController {
|
|
|
99
107
|
clearTimeout(this.renderTimer);
|
|
100
108
|
this.renderTimer = undefined;
|
|
101
109
|
}
|
|
110
|
+
isScopeActive(scopeKey) {
|
|
111
|
+
return this.host.activeScopeKey() === scopeKey;
|
|
112
|
+
}
|
|
102
113
|
}
|
|
103
114
|
function appendTerminalChunk(current, chunk) {
|
|
104
115
|
let next = current;
|
|
@@ -3,6 +3,7 @@ import { type PixLogDetails, type PixLogLevel } from "../logger.js";
|
|
|
3
3
|
import type { Entry } from "../types.js";
|
|
4
4
|
export type AppExtensionActionsHost = {
|
|
5
5
|
isRunning(): boolean;
|
|
6
|
+
runtime(): AgentSessionRuntime | undefined;
|
|
6
7
|
getInput(): string;
|
|
7
8
|
setInput(value: string): void;
|
|
8
9
|
awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
|
|
@@ -22,5 +23,7 @@ export declare class AppExtensionActionsController {
|
|
|
22
23
|
constructor(host: AppExtensionActionsHost, logExtensionError?: ExtensionErrorLogger);
|
|
23
24
|
createCommandContextActions(runtime: AgentSessionRuntime): ExtensionCommandContextActions;
|
|
24
25
|
waitForSessionIdle(runtime: AgentSessionRuntime): Promise<void>;
|
|
26
|
+
private isRuntimeActive;
|
|
27
|
+
private isSessionActive;
|
|
25
28
|
handleExtensionError(error: ExtensionError): void;
|
|
26
29
|
}
|
|
@@ -13,21 +13,26 @@ export class AppExtensionActionsController {
|
|
|
13
13
|
waitForIdle: () => this.waitForSessionIdle(runtime),
|
|
14
14
|
newSession: async (options) => {
|
|
15
15
|
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
16
|
+
if (!this.isRuntimeActive(runtime))
|
|
17
|
+
return { cancelled: true };
|
|
16
18
|
const result = await runtime.newSession(options);
|
|
17
|
-
if (!result.cancelled)
|
|
19
|
+
if (!result.cancelled && this.isRuntimeActive(runtime))
|
|
18
20
|
this.host.afterSessionReplacement("Started a new session.");
|
|
19
21
|
return result;
|
|
20
22
|
},
|
|
21
23
|
fork: async (entryId, options) => {
|
|
22
24
|
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
25
|
+
if (!this.isRuntimeActive(runtime))
|
|
26
|
+
return { cancelled: true };
|
|
23
27
|
const result = await runtime.fork(entryId, options);
|
|
24
|
-
if (!result.cancelled)
|
|
28
|
+
if (!result.cancelled && this.isRuntimeActive(runtime))
|
|
25
29
|
this.host.afterSessionReplacement("Forked to a new session.");
|
|
26
30
|
return result;
|
|
27
31
|
},
|
|
28
32
|
navigateTree: async (targetId, options) => {
|
|
29
|
-
const
|
|
30
|
-
|
|
33
|
+
const session = runtime.session;
|
|
34
|
+
const result = await session.navigateTree(targetId, options);
|
|
35
|
+
if (!result.cancelled && !result.aborted && this.isSessionActive(runtime, session)) {
|
|
31
36
|
this.host.resetSessionView();
|
|
32
37
|
this.host.loadSessionHistory();
|
|
33
38
|
if (result.editorText && !this.host.getInput().trim())
|
|
@@ -39,15 +44,22 @@ export class AppExtensionActionsController {
|
|
|
39
44
|
},
|
|
40
45
|
switchSession: async (sessionPath, options) => {
|
|
41
46
|
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
47
|
+
if (!this.isRuntimeActive(runtime))
|
|
48
|
+
return { cancelled: true };
|
|
42
49
|
const result = await runtime.switchSession(sessionPath, options);
|
|
43
|
-
if (!result.cancelled)
|
|
50
|
+
if (!result.cancelled && this.isRuntimeActive(runtime))
|
|
44
51
|
this.host.afterSessionReplacement(`Switched session: ${sessionPath}`);
|
|
45
52
|
return result;
|
|
46
53
|
},
|
|
47
54
|
reload: async () => {
|
|
55
|
+
const session = runtime.session;
|
|
48
56
|
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
if (!this.isSessionActive(runtime, session))
|
|
58
|
+
return;
|
|
59
|
+
await session.reload();
|
|
60
|
+
if (!this.isSessionActive(runtime, session))
|
|
61
|
+
return;
|
|
62
|
+
this.host.setSessionStatus(session);
|
|
51
63
|
this.host.showToast("Reloaded resources", "success");
|
|
52
64
|
this.host.render();
|
|
53
65
|
},
|
|
@@ -56,6 +68,12 @@ export class AppExtensionActionsController {
|
|
|
56
68
|
async waitForSessionIdle(runtime) {
|
|
57
69
|
await runtime.session.waitForIdle();
|
|
58
70
|
}
|
|
71
|
+
isRuntimeActive(runtime) {
|
|
72
|
+
return this.host.isRunning() && this.host.runtime() === runtime;
|
|
73
|
+
}
|
|
74
|
+
isSessionActive(runtime, session) {
|
|
75
|
+
return this.isRuntimeActive(runtime) && runtime.session === session;
|
|
76
|
+
}
|
|
59
77
|
handleExtensionError(error) {
|
|
60
78
|
const sourceText = formatExtensionErrorSource(error.extensionPath);
|
|
61
79
|
const pathText = error.extensionPath ? ` (${error.extensionPath})` : "";
|
|
@@ -8,6 +8,7 @@ export type ExtensionTerminalInputResult = {
|
|
|
8
8
|
export type ExtensionUiControllerHost = {
|
|
9
9
|
readonly theme: Theme;
|
|
10
10
|
activeExtensionUiScope?(): string | undefined;
|
|
11
|
+
isExtensionUiScopeActive?(scopeKey: string): boolean;
|
|
11
12
|
isRunning(): boolean;
|
|
12
13
|
render(): void;
|
|
13
14
|
showToast(message: string, kind?: ToastKind, options?: {
|
|
@@ -38,12 +39,13 @@ export declare class ExtensionUiController {
|
|
|
38
39
|
clearWidgets(scopeKey?: string, options?: {
|
|
39
40
|
cancelCustomUi?: boolean;
|
|
40
41
|
}): void;
|
|
42
|
+
cancelCustomUi(scopeKey?: string): void;
|
|
41
43
|
suppressWidget(key: string): void;
|
|
42
44
|
handleTerminalInput(data: string): ExtensionTerminalInputResult;
|
|
43
45
|
renderActiveCustomUi(width: number): string[] | undefined;
|
|
44
46
|
activeCustomUiUsesEditor(): boolean;
|
|
45
47
|
handleCustomUiMouse(event: ExtensionInputMouseEvent): boolean;
|
|
46
|
-
widgetTuiHandle(): WidgetTuiHandle;
|
|
48
|
+
widgetTuiHandle(scopeKey?: string): WidgetTuiHandle;
|
|
47
49
|
createExtensionUIContext(scopeKey?: string): PixExtensionUIContext;
|
|
48
50
|
private setAboveInputWidget;
|
|
49
51
|
private clearAboveInputWidget;
|
|
@@ -60,6 +62,8 @@ export declare class ExtensionUiController {
|
|
|
60
62
|
private rejectActiveCustomUi;
|
|
61
63
|
private finishActiveCustomUi;
|
|
62
64
|
private activeCustomUiForActiveScope;
|
|
65
|
+
private isScopeActive;
|
|
66
|
+
private scopedMenuController;
|
|
63
67
|
private activeScopeKey;
|
|
64
68
|
private normalizeScopeKey;
|
|
65
69
|
private scopedWidgetKey;
|