pi-ui-extend 1.0.24 → 1.0.28
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 +5 -1
- package/dist/app/app.d.ts +1 -0
- package/dist/app/app.js +12 -0
- package/dist/app/icons.d.ts +1 -0
- package/dist/app/icons.js +2 -0
- package/dist/app/input/input-controller.d.ts +2 -0
- package/dist/app/input/input-controller.js +8 -5
- package/dist/app/rendering/render-controller.js +2 -0
- package/dist/app/rendering/status-line-renderer.d.ts +4 -1
- package/dist/app/rendering/status-line-renderer.js +12 -0
- package/dist/app/screen/mouse-controller.d.ts +4 -1
- package/dist/app/screen/mouse-controller.js +9 -0
- package/dist/app/session/agent-pause-controller.d.ts +30 -0
- package/dist/app/session/agent-pause-controller.js +178 -0
- package/dist/app/session/session-event-controller.d.ts +1 -0
- package/dist/app/session/session-event-controller.js +22 -15
- package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
- package/dist/app/session/session-lifecycle-controller.js +1 -0
- package/dist/app/types.d.ts +10 -0
- package/dist/config.js +1 -1
- package/dist/default-pix-config.js +2 -2
- package/external/pi-tools-suite/README.md +1 -1
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +34 -2
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +24 -18
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +14 -3
- package/external/pi-tools-suite/src/async-subagents/core/process.ts +19 -0
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +97 -17
- package/external/pi-tools-suite/src/async-subagents/core/stop.ts +4 -2
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/SKILL.md +108 -13
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +80 -0
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-flow.example.jsonc +54 -1
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/scripts/browser-qa-runner.mjs +952 -76
- package/external/pi-tools-suite/src/dcp/index.ts +21 -0
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +92 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +21 -16
- package/external/pi-tools-suite/src/todo/index.ts +86 -46
- package/external/pi-tools-suite/src/todo/todo.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -371,12 +371,16 @@ Both support the published schema:
|
|
|
371
371
|
"apply_patch": { "defaultExpanded": true }
|
|
372
372
|
}
|
|
373
373
|
},
|
|
374
|
-
"promptEnhancer": { "modelRef": "
|
|
374
|
+
"promptEnhancer": { "modelRef": "openai-codex/gpt-5.6-luna" },
|
|
375
375
|
"autocomplete": {
|
|
376
376
|
"modelRef": "zai/glm-5-turbo",
|
|
377
377
|
"debounceMs": 350,
|
|
378
378
|
"timeoutMs": 3000
|
|
379
379
|
},
|
|
380
|
+
"sessionTitle": {
|
|
381
|
+
"modelRef": "openai-codex/gpt-5.6-luna",
|
|
382
|
+
"fallbackModels": ["zai/glm-5-turbo"]
|
|
383
|
+
},
|
|
380
384
|
"dictation": { "language": "en" },
|
|
381
385
|
"ignoreContextFiles": false,
|
|
382
386
|
"maxProjectSessions": 0
|
package/dist/app/app.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export declare class PiUiExtendApp {
|
|
|
36
36
|
private readonly popupMenus;
|
|
37
37
|
private readonly voiceController;
|
|
38
38
|
private readonly sessionEvents;
|
|
39
|
+
private readonly agentPauseController;
|
|
39
40
|
private readonly shellController;
|
|
40
41
|
private readonly queuedMessages;
|
|
41
42
|
private readonly workspaceActions;
|
package/dist/app/app.js
CHANGED
|
@@ -31,6 +31,7 @@ import { AppSessionLifecycleController } from "./session/session-lifecycle-contr
|
|
|
31
31
|
import { AppShellController } from "./commands/shell-controller.js";
|
|
32
32
|
import { runInteractiveShellCommand } from "./commands/shell-command.js";
|
|
33
33
|
import { AppSessionEventController } from "./session/session-event-controller.js";
|
|
34
|
+
import { AgentPauseController } from "./session/agent-pause-controller.js";
|
|
34
35
|
import { AppStatusController } from "./screen/status-controller.js";
|
|
35
36
|
import { StatusLineRenderer } from "./rendering/status-line-renderer.js";
|
|
36
37
|
import { AppModelUsageController } from "./model/model-usage-controller.js";
|
|
@@ -91,6 +92,7 @@ export class PiUiExtendApp {
|
|
|
91
92
|
popupMenus;
|
|
92
93
|
voiceController;
|
|
93
94
|
sessionEvents;
|
|
95
|
+
agentPauseController;
|
|
94
96
|
shellController;
|
|
95
97
|
queuedMessages;
|
|
96
98
|
workspaceActions;
|
|
@@ -167,6 +169,11 @@ export class PiUiExtendApp {
|
|
|
167
169
|
runtimeSession: () => this.runtime?.session,
|
|
168
170
|
render: () => this.scheduleRender(),
|
|
169
171
|
});
|
|
172
|
+
this.agentPauseController = new AgentPauseController({
|
|
173
|
+
showToast: (message, kind) => this.showToast(message, kind),
|
|
174
|
+
render: () => this.scheduleRender(),
|
|
175
|
+
isCurrentSession: (session) => this.runtime?.session === session,
|
|
176
|
+
});
|
|
170
177
|
this.modelUsageController = new AppModelUsageController({
|
|
171
178
|
runtimeSession: () => this.runtime?.session,
|
|
172
179
|
render: () => this.render(),
|
|
@@ -308,6 +315,8 @@ export class PiUiExtendApp {
|
|
|
308
315
|
promptEnhancerStatusWidgetEnabled: () => this.promptEnhancer.statusWidgetEnabled(),
|
|
309
316
|
terminalBellSoundStatusWidgetText: () => this.terminalBellSoundController.statusWidgetText(),
|
|
310
317
|
terminalBellSoundStatusWidgetEnabled: () => this.terminalBellSoundController.isEnabled(),
|
|
318
|
+
agentPauseStatusWidgetText: () => this.agentPauseController.statusWidgetText(this.runtime?.session),
|
|
319
|
+
agentPauseStatusWidgetActive: () => this.agentPauseController.statusWidgetActive(this.runtime?.session),
|
|
311
320
|
voiceStatusWidgetText: () => this.voiceController.statusWidgetText(),
|
|
312
321
|
voiceStatusWidgetActive: () => this.voiceController.statusWidgetActive(),
|
|
313
322
|
conversationQuickScrollDirections: () => this.conversationQuickScrollDirections(),
|
|
@@ -633,6 +642,7 @@ export class PiUiExtendApp {
|
|
|
633
642
|
this.render();
|
|
634
643
|
},
|
|
635
644
|
toggleTerminalBellSound: () => this.toggleTerminalBellSound(),
|
|
645
|
+
toggleAgentPause: () => this.agentPauseController.toggle(this.runtime?.session),
|
|
636
646
|
handleExtensionInputMouse: (event) => this.extensionUiController.handleCustomUiMouse(event),
|
|
637
647
|
render: () => this.render(),
|
|
638
648
|
}, this.popupMenus, this.popupActions, this.scrollController, this.commandController);
|
|
@@ -783,6 +793,7 @@ export class PiUiExtendApp {
|
|
|
783
793
|
createExtensionCommandContextActions: (runtime) => this.extensionActions.createCommandContextActions(runtime),
|
|
784
794
|
handleExtensionError: (error) => this.extensionActions.handleExtensionError(error),
|
|
785
795
|
handleSessionEvent: (event) => this.handleSessionEvent(event),
|
|
796
|
+
bindAgentPause: (session) => this.agentPauseController.bind(session),
|
|
786
797
|
addEntry: (entry) => this.addEntry(entry),
|
|
787
798
|
setStatus: (status) => this.setStatus(status),
|
|
788
799
|
showToast: (message, kind) => this.showToast(message, kind),
|
|
@@ -809,6 +820,7 @@ export class PiUiExtendApp {
|
|
|
809
820
|
this.mouseController.statusTerminalBellSoundTarget = undefined;
|
|
810
821
|
this.mouseController.statusSessionTarget = undefined;
|
|
811
822
|
this.mouseController.statusPromptEnhancerTarget = undefined;
|
|
823
|
+
this.mouseController.statusAgentPauseTarget = undefined;
|
|
812
824
|
this.mouseController.statusVoiceMicTarget = undefined;
|
|
813
825
|
this.mouseController.statusVoiceLanguageTarget = undefined;
|
|
814
826
|
this.mouseController.tabLineTargets.length = 0;
|
package/dist/app/icons.d.ts
CHANGED
package/dist/app/icons.js
CHANGED
|
@@ -28,6 +28,7 @@ const NERD_FONT_ICONS = {
|
|
|
28
28
|
microphone: "\u{f036c}",
|
|
29
29
|
plus: "\u{f0415}",
|
|
30
30
|
pause: "\u{f03e4}",
|
|
31
|
+
play: "\u{f040a}",
|
|
31
32
|
record: "\u{f044a}",
|
|
32
33
|
refresh: "\u{f0450}",
|
|
33
34
|
volumeHigh: "\u{f057e}",
|
|
@@ -61,6 +62,7 @@ const FALLBACK_ICONS = {
|
|
|
61
62
|
microphone: "m",
|
|
62
63
|
plus: "+",
|
|
63
64
|
pause: "⏸",
|
|
65
|
+
play: "▶",
|
|
64
66
|
record: "●",
|
|
65
67
|
refresh: "↻",
|
|
66
68
|
volumeHigh: "♪",
|
|
@@ -9,6 +9,7 @@ export type InputControllerHost = {
|
|
|
9
9
|
handleExtensionTerminalInput(data: string): ExtensionTerminalInputResult;
|
|
10
10
|
extensionInputUsesEditor?(): boolean;
|
|
11
11
|
isShiftPressed?(): boolean;
|
|
12
|
+
isCommandPressed?(): boolean;
|
|
12
13
|
getInput(): string;
|
|
13
14
|
getDirectPopupMenu(): DirectPopupMenu | undefined;
|
|
14
15
|
resetRequestHistoryNavigation(): void;
|
|
@@ -68,5 +69,6 @@ export declare class AppInputController {
|
|
|
68
69
|
private deleteCurrentInputLine;
|
|
69
70
|
private handleChar;
|
|
70
71
|
private isShiftPressed;
|
|
72
|
+
private isCommandPressed;
|
|
71
73
|
}
|
|
72
74
|
export {};
|
|
@@ -179,7 +179,7 @@ export class AppInputController {
|
|
|
179
179
|
break;
|
|
180
180
|
// Native undo/redo shortcuts must retain their individual key behavior.
|
|
181
181
|
const lower = char.toLowerCase();
|
|
182
|
-
if ((lower === "z" || lower === "y") &&
|
|
182
|
+
if ((lower === "z" || lower === "y") && this.isCommandPressed())
|
|
183
183
|
break;
|
|
184
184
|
length += 1;
|
|
185
185
|
}
|
|
@@ -417,7 +417,7 @@ export class AppInputController {
|
|
|
417
417
|
return "consumed";
|
|
418
418
|
}
|
|
419
419
|
handleArrowUp() {
|
|
420
|
-
if (
|
|
420
|
+
if (this.isCommandPressed()) {
|
|
421
421
|
this.host.scrollByPage(-1);
|
|
422
422
|
return;
|
|
423
423
|
}
|
|
@@ -438,7 +438,7 @@ export class AppInputController {
|
|
|
438
438
|
this.host.scrollByLines(-1);
|
|
439
439
|
}
|
|
440
440
|
handleArrowDown() {
|
|
441
|
-
if (
|
|
441
|
+
if (this.isCommandPressed()) {
|
|
442
442
|
this.host.scrollByPage(1);
|
|
443
443
|
return;
|
|
444
444
|
}
|
|
@@ -517,7 +517,7 @@ export class AppInputController {
|
|
|
517
517
|
this.redoInput();
|
|
518
518
|
return true;
|
|
519
519
|
}
|
|
520
|
-
if (!
|
|
520
|
+
if (!this.isCommandPressed())
|
|
521
521
|
return false;
|
|
522
522
|
const lower = char.toLowerCase();
|
|
523
523
|
if (lower === "z") {
|
|
@@ -595,7 +595,7 @@ export class AppInputController {
|
|
|
595
595
|
this.host.handleEnter();
|
|
596
596
|
return;
|
|
597
597
|
}
|
|
598
|
-
if (char === "\u0015" || ((char === "\u007f" || char === "\b") &&
|
|
598
|
+
if (char === "\u0015" || ((char === "\u007f" || char === "\b") && this.isCommandPressed())) {
|
|
599
599
|
this.deleteCurrentInputLine();
|
|
600
600
|
return;
|
|
601
601
|
}
|
|
@@ -625,6 +625,9 @@ export class AppInputController {
|
|
|
625
625
|
isShiftPressed() {
|
|
626
626
|
return this.host.isShiftPressed?.() ?? isNativeShiftPressed();
|
|
627
627
|
}
|
|
628
|
+
isCommandPressed() {
|
|
629
|
+
return this.host.isCommandPressed?.() ?? isNativeCommandPressed();
|
|
630
|
+
}
|
|
628
631
|
}
|
|
629
632
|
function normalizeBracketedPastePayload(payload) {
|
|
630
633
|
return payload.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
@@ -73,6 +73,7 @@ export class AppRenderController {
|
|
|
73
73
|
this.deps.mouseController.statusTerminalBellSoundTarget = undefined;
|
|
74
74
|
this.deps.mouseController.statusSessionTarget = undefined;
|
|
75
75
|
this.deps.mouseController.statusPromptEnhancerTarget = undefined;
|
|
76
|
+
this.deps.mouseController.statusAgentPauseTarget = undefined;
|
|
76
77
|
this.deps.mouseController.statusVoiceMicTarget = undefined;
|
|
77
78
|
this.deps.mouseController.statusVoiceLanguageTarget = undefined;
|
|
78
79
|
this.deps.mouseController.tabLineTargets.length = 0;
|
|
@@ -284,6 +285,7 @@ export class AppRenderController {
|
|
|
284
285
|
this.deps.mouseController.statusTerminalBellSoundTarget = this.deps.statusLineRenderer.terminalBellSoundTarget?.(statusLayout, statusRow);
|
|
285
286
|
this.deps.mouseController.statusSessionTarget = this.deps.statusLineRenderer.sessionTarget(statusLayout.text, statusRow, statusLayout.sessionLabel, statusLayout.workspaceLabel);
|
|
286
287
|
this.deps.mouseController.statusPromptEnhancerTarget = this.deps.statusLineRenderer.promptEnhancerTarget(statusLayout, statusRow);
|
|
288
|
+
this.deps.mouseController.statusAgentPauseTarget = this.deps.statusLineRenderer.agentPauseTarget?.(statusLayout, statusRow);
|
|
287
289
|
this.deps.mouseController.statusVoiceMicTarget = this.deps.statusLineRenderer.voiceMicTarget(statusLayout, statusRow);
|
|
288
290
|
this.deps.mouseController.statusVoiceLanguageTarget = this.deps.statusLineRenderer.voiceLanguageTarget(statusLayout, statusRow);
|
|
289
291
|
this.deps.mouseController.renderedRowTexts.set(statusRow, statusLayout.text);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Theme } from "../../theme.js";
|
|
3
|
-
import type { SessionActivity, StatusCompactToolsTarget, StatusContextTarget, StatusDraftQueueTarget, StatusInternalClipboardTarget, StatusLineLayout, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusQuickScrollTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
|
|
3
|
+
import type { SessionActivity, StatusAgentPauseTarget, StatusCompactToolsTarget, StatusContextTarget, StatusDraftQueueTarget, StatusInternalClipboardTarget, StatusLineLayout, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusQuickScrollTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
|
|
4
4
|
import type { ScreenStyler } from "../screen/screen-styler.js";
|
|
5
5
|
import { type ModelColorsConfig } from "../../config.js";
|
|
6
6
|
export type StatusLineRendererHost = {
|
|
@@ -24,6 +24,8 @@ export type StatusLineRendererHost = {
|
|
|
24
24
|
promptEnhancerStatusWidgetEnabled(): boolean;
|
|
25
25
|
terminalBellSoundStatusWidgetText(): string;
|
|
26
26
|
terminalBellSoundStatusWidgetEnabled(): boolean;
|
|
27
|
+
agentPauseStatusWidgetText?(): string;
|
|
28
|
+
agentPauseStatusWidgetActive?(): boolean;
|
|
27
29
|
voiceStatusWidgetText(): string;
|
|
28
30
|
voiceStatusWidgetActive(): boolean;
|
|
29
31
|
conversationQuickScrollDirections?(): {
|
|
@@ -49,6 +51,7 @@ export declare class StatusLineRenderer {
|
|
|
49
51
|
contextTarget(statusText: string, row: number, layout: StatusLineLayout): StatusContextTarget | undefined;
|
|
50
52
|
modelUsageTarget(statusText: string, row: number, layout: StatusLineLayout): StatusModelUsageTarget | undefined;
|
|
51
53
|
promptEnhancerTarget(layout: StatusLineLayout, row: number): StatusPromptEnhancerTarget | undefined;
|
|
54
|
+
agentPauseTarget(layout: StatusLineLayout, row: number): StatusAgentPauseTarget | undefined;
|
|
52
55
|
voiceMicTarget(layout: StatusLineLayout, row: number): StatusVoiceMicTarget | undefined;
|
|
53
56
|
voiceLanguageTarget(layout: StatusLineLayout, row: number): StatusVoiceLanguageTarget | undefined;
|
|
54
57
|
userJumpTarget(layout: StatusLineLayout, row: number): StatusUserJumpTarget | undefined;
|
|
@@ -91,6 +91,10 @@ export class StatusLineRenderer {
|
|
|
91
91
|
appendWidget(quickScrollDirections.down ? this.iconButtonText(APP_ICONS.down) : "", (column, text) => {
|
|
92
92
|
layout.quickScrollDownWidget = this.widgetLayout(column, text);
|
|
93
93
|
});
|
|
94
|
+
const agentPauseWidgetText = this.host.agentPauseStatusWidgetText?.() ?? "";
|
|
95
|
+
appendWidget(agentPauseWidgetText ? this.iconButtonText(agentPauseWidgetText) : "", (column, text) => {
|
|
96
|
+
layout.agentPauseWidget = this.widgetLayout(column, text);
|
|
97
|
+
});
|
|
94
98
|
const voiceWidgetText = this.host.voiceStatusWidgetText();
|
|
95
99
|
appendWidget(this.voiceBorderWidgetText(voiceWidgetText), (column, text) => {
|
|
96
100
|
layout.voiceWidget = this.voiceWidgetLayout(column, voiceWidgetText, text);
|
|
@@ -156,6 +160,7 @@ export class StatusLineRenderer {
|
|
|
156
160
|
pushWidgetSegment(layout.terminalBellSoundWidget, this.host.terminalBellSoundStatusWidgetEnabled() ? colors.info : colors.muted);
|
|
157
161
|
pushWidgetSegment(layout.thinkingExpandWidget, this.host.allThinkingExpandedActive?.() ? colors.info : colors.muted);
|
|
158
162
|
pushWidgetSegment(layout.compactToolsWidget, this.host.superCompactToolsActive?.() ? colors.info : colors.muted);
|
|
163
|
+
pushWidgetSegment(layout.agentPauseWidget, this.host.agentPauseStatusWidgetActive?.() ? colors.info : colors.muted);
|
|
159
164
|
const voiceWidget = layout.voiceWidget;
|
|
160
165
|
if (voiceWidget) {
|
|
161
166
|
segments.push({
|
|
@@ -229,6 +234,12 @@ export class StatusLineRenderer {
|
|
|
229
234
|
return undefined;
|
|
230
235
|
return { row, startColumn: widget.startColumn, endColumn: widget.endColumn };
|
|
231
236
|
}
|
|
237
|
+
agentPauseTarget(layout, row) {
|
|
238
|
+
const widget = layout.agentPauseWidget;
|
|
239
|
+
if (!widget)
|
|
240
|
+
return undefined;
|
|
241
|
+
return { row, startColumn: widget.startColumn, endColumn: widget.endColumn };
|
|
242
|
+
}
|
|
232
243
|
voiceMicTarget(layout, row) {
|
|
233
244
|
const voiceWidget = layout.voiceWidget;
|
|
234
245
|
if (!voiceWidget)
|
|
@@ -359,6 +370,7 @@ export class StatusLineRenderer {
|
|
|
359
370
|
{ widget: layout.terminalBellSoundWidget, foreground: this.host.terminalBellSoundStatusWidgetEnabled() ? colors.info : colors.muted },
|
|
360
371
|
{ widget: layout.thinkingExpandWidget, foreground: this.host.allThinkingExpandedActive?.() ? colors.info : colors.muted },
|
|
361
372
|
{ widget: layout.compactToolsWidget, foreground: this.host.superCompactToolsActive?.() ? colors.info : colors.muted },
|
|
373
|
+
{ widget: layout.agentPauseWidget, foreground: this.host.agentPauseStatusWidgetActive?.() ? colors.info : colors.muted },
|
|
362
374
|
].filter((entry) => Boolean(entry.widget));
|
|
363
375
|
for (const { widget, foreground } of widgets) {
|
|
364
376
|
segments.push({
|
|
@@ -6,7 +6,7 @@ import type { ToastEntry, ToastVariant } from "../../ui.js";
|
|
|
6
6
|
import type { AppPopupActionController } from "../popup/popup-action-controller.js";
|
|
7
7
|
import type { AppPopupMenuController } from "../popup/popup-menu-controller.js";
|
|
8
8
|
import type { AppScrollController } from "./scroll-controller.js";
|
|
9
|
-
import type { Entry, ImageClickTarget, MouseEvent, MouseSelection, StatusContextTarget, StatusCompactToolsTarget, StatusDraftQueueTarget, StatusInternalClipboardTarget, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusQuickScrollTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, TabLineMouseTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
|
|
9
|
+
import type { Entry, ImageClickTarget, MouseEvent, MouseSelection, StatusAgentPauseTarget, StatusContextTarget, StatusCompactToolsTarget, StatusDraftQueueTarget, StatusInternalClipboardTarget, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusQuickScrollTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, TabLineMouseTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
|
|
10
10
|
import { type RenderedLink } from "./file-links.js";
|
|
11
11
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
type ClickFlash = {
|
|
@@ -63,6 +63,7 @@ export type AppMouseControllerHost = {
|
|
|
63
63
|
toggleAllThinkingExpanded?(): void;
|
|
64
64
|
toggleSuperCompactTools?(): void;
|
|
65
65
|
toggleTerminalBellSound?(): void;
|
|
66
|
+
toggleAgentPause?(): void | Promise<void>;
|
|
66
67
|
copyTextToClipboard?(text: string): void | Promise<void>;
|
|
67
68
|
handleExtensionInputMouse(event: MouseEvent & {
|
|
68
69
|
localRow: number;
|
|
@@ -114,6 +115,7 @@ export declare class AppMouseController {
|
|
|
114
115
|
statusTerminalBellSoundTarget: StatusTerminalBellSoundTarget | undefined;
|
|
115
116
|
statusSessionTarget: StatusSessionTarget | undefined;
|
|
116
117
|
statusPromptEnhancerTarget: StatusPromptEnhancerTarget | undefined;
|
|
118
|
+
statusAgentPauseTarget: StatusAgentPauseTarget | undefined;
|
|
117
119
|
statusVoiceMicTarget: StatusVoiceMicTarget | undefined;
|
|
118
120
|
statusVoiceLanguageTarget: StatusVoiceLanguageTarget | undefined;
|
|
119
121
|
readonly tabLineTargets: TabLineMouseTarget[];
|
|
@@ -172,6 +174,7 @@ export declare class AppMouseController {
|
|
|
172
174
|
private handleStatusTerminalBellSoundClick;
|
|
173
175
|
private handleStatusSessionClick;
|
|
174
176
|
private handleStatusPromptEnhancerClick;
|
|
177
|
+
private handleStatusAgentPauseClick;
|
|
175
178
|
private handleStatusVoiceMicClick;
|
|
176
179
|
private handleStatusVoiceLanguageClick;
|
|
177
180
|
private handleStatusQuickScrollClick;
|
|
@@ -35,6 +35,7 @@ export class AppMouseController {
|
|
|
35
35
|
statusTerminalBellSoundTarget;
|
|
36
36
|
statusSessionTarget;
|
|
37
37
|
statusPromptEnhancerTarget;
|
|
38
|
+
statusAgentPauseTarget;
|
|
38
39
|
statusVoiceMicTarget;
|
|
39
40
|
statusVoiceLanguageTarget;
|
|
40
41
|
tabLineTargets = [];
|
|
@@ -333,6 +334,7 @@ export class AppMouseController {
|
|
|
333
334
|
this.statusTerminalBellSoundTarget,
|
|
334
335
|
this.statusSessionTarget,
|
|
335
336
|
this.statusPromptEnhancerTarget,
|
|
337
|
+
this.statusAgentPauseTarget,
|
|
336
338
|
this.statusVoiceMicTarget,
|
|
337
339
|
this.statusVoiceLanguageTarget,
|
|
338
340
|
].find((candidate) => !!candidate
|
|
@@ -510,6 +512,7 @@ export class AppMouseController {
|
|
|
510
512
|
|| this.handleStatusCompactToolsClick(event)
|
|
511
513
|
|| this.handleStatusTerminalBellSoundClick(event)
|
|
512
514
|
|| this.handleStatusPromptEnhancerClick(event)
|
|
515
|
+
|| this.handleStatusAgentPauseClick(event)
|
|
513
516
|
|| this.handleStatusVoiceMicClick(event)
|
|
514
517
|
|| this.handleStatusVoiceLanguageClick(event);
|
|
515
518
|
}
|
|
@@ -570,6 +573,12 @@ export class AppMouseController {
|
|
|
570
573
|
void this.host.enhancePrompt();
|
|
571
574
|
return true;
|
|
572
575
|
}
|
|
576
|
+
handleStatusAgentPauseClick(event) {
|
|
577
|
+
if (!this.statusTargetContains(this.statusAgentPauseTarget, event))
|
|
578
|
+
return false;
|
|
579
|
+
void this.host.toggleAgentPause?.();
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
573
582
|
handleStatusVoiceMicClick(event) {
|
|
574
583
|
if (!this.statusTargetContains(this.statusVoiceMicTarget, event))
|
|
575
584
|
return false;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export type AgentPauseState = "idle" | "pause-requested" | "paused" | "resuming";
|
|
3
|
+
export type AgentPauseControllerHost = {
|
|
4
|
+
showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
|
|
5
|
+
render(): void;
|
|
6
|
+
isCurrentSession(session: AgentSession): boolean;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Implements a turn-boundary pause without interrupting an active tool batch.
|
|
10
|
+
*
|
|
11
|
+
* The pinned SDK exposes Agent.shouldStopAfterTurn and Agent.continue(), but not a
|
|
12
|
+
* session-level continuation method. The small private adapter below mirrors the
|
|
13
|
+
* bookkeeping in AgentSession._runAgentPrompt so resumed runs still get retries,
|
|
14
|
+
* compaction, queue draining, isStreaming, and agent_settled behavior.
|
|
15
|
+
*/
|
|
16
|
+
export declare class AgentPauseController {
|
|
17
|
+
private readonly host;
|
|
18
|
+
private readonly records;
|
|
19
|
+
constructor(host: AgentPauseControllerHost);
|
|
20
|
+
bind(session: AgentSession): void;
|
|
21
|
+
state(session: AgentSession | undefined): AgentPauseState;
|
|
22
|
+
statusWidgetText(session: AgentSession | undefined): string;
|
|
23
|
+
statusWidgetActive(session: AgentSession | undefined): boolean;
|
|
24
|
+
toggle(session: AgentSession | undefined): Promise<void>;
|
|
25
|
+
private resume;
|
|
26
|
+
private setState;
|
|
27
|
+
private finishSettlement;
|
|
28
|
+
private canContinue;
|
|
29
|
+
private sessionInternals;
|
|
30
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { APP_ICONS } from "../icons.js";
|
|
2
|
+
/**
|
|
3
|
+
* Implements a turn-boundary pause without interrupting an active tool batch.
|
|
4
|
+
*
|
|
5
|
+
* The pinned SDK exposes Agent.shouldStopAfterTurn and Agent.continue(), but not a
|
|
6
|
+
* session-level continuation method. The small private adapter below mirrors the
|
|
7
|
+
* bookkeeping in AgentSession._runAgentPrompt so resumed runs still get retries,
|
|
8
|
+
* compaction, queue draining, isStreaming, and agent_settled behavior.
|
|
9
|
+
*/
|
|
10
|
+
export class AgentPauseController {
|
|
11
|
+
host;
|
|
12
|
+
records = new WeakMap();
|
|
13
|
+
constructor(host) {
|
|
14
|
+
this.host = host;
|
|
15
|
+
}
|
|
16
|
+
bind(session) {
|
|
17
|
+
if (this.records.has(session))
|
|
18
|
+
return;
|
|
19
|
+
const internals = this.sessionInternals(session);
|
|
20
|
+
const record = {
|
|
21
|
+
state: "idle",
|
|
22
|
+
pauseBoundaryReached: false,
|
|
23
|
+
originalHandlePostAgentRun: internals._handlePostAgentRun.bind(session),
|
|
24
|
+
};
|
|
25
|
+
this.records.set(session, record);
|
|
26
|
+
session.subscribe((event) => {
|
|
27
|
+
if (event.type === "agent_start" && record.state === "paused") {
|
|
28
|
+
// A new prompt supersedes a paused continuation.
|
|
29
|
+
this.setState(record, "idle");
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const originalEmitAgentSettled = internals._emitAgentSettled.bind(session);
|
|
33
|
+
internals._emitAgentSettled = async () => {
|
|
34
|
+
try {
|
|
35
|
+
await originalEmitAgentSettled();
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
this.finishSettlement(record);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const originalShouldStopAfterTurn = session.agent.shouldStopAfterTurn;
|
|
42
|
+
const shouldStopAfterTurn = async (context, signal) => {
|
|
43
|
+
if (await originalShouldStopAfterTurn?.(context, signal)) {
|
|
44
|
+
if (record.state === "pause-requested") {
|
|
45
|
+
record.pauseBoundaryReached = false;
|
|
46
|
+
this.setState(record, "idle");
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (record.state !== "pause-requested")
|
|
51
|
+
return false;
|
|
52
|
+
record.pauseBoundaryReached = true;
|
|
53
|
+
return true;
|
|
54
|
+
};
|
|
55
|
+
session.agent.shouldStopAfterTurn = shouldStopAfterTurn;
|
|
56
|
+
internals._handlePostAgentRun = async () => {
|
|
57
|
+
if (record.state === "paused")
|
|
58
|
+
return false;
|
|
59
|
+
const pauseBoundaryReached = record.pauseBoundaryReached;
|
|
60
|
+
record.pauseBoundaryReached = false;
|
|
61
|
+
const shouldContinue = await record.originalHandlePostAgentRun();
|
|
62
|
+
if (pauseBoundaryReached || record.state === "pause-requested") {
|
|
63
|
+
if (shouldContinue || this.canContinue(session)) {
|
|
64
|
+
record.pauseSettled = new Promise((resolve) => {
|
|
65
|
+
record.resolvePauseSettled = resolve;
|
|
66
|
+
});
|
|
67
|
+
this.setState(record, "paused");
|
|
68
|
+
this.host.showToast("Agent paused after the current turn", "success");
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
this.setState(record, "idle");
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return shouldContinue;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
state(session) {
|
|
78
|
+
if (!session)
|
|
79
|
+
return "idle";
|
|
80
|
+
return this.records.get(session)?.state ?? "idle";
|
|
81
|
+
}
|
|
82
|
+
statusWidgetText(session) {
|
|
83
|
+
return this.state(session) === "paused" ? APP_ICONS.play : APP_ICONS.pause;
|
|
84
|
+
}
|
|
85
|
+
statusWidgetActive(session) {
|
|
86
|
+
if (!session)
|
|
87
|
+
return false;
|
|
88
|
+
return this.state(session) !== "idle" || session.isStreaming || session.agent.state.isStreaming;
|
|
89
|
+
}
|
|
90
|
+
async toggle(session) {
|
|
91
|
+
if (!session) {
|
|
92
|
+
this.host.showToast("No active agent session", "info");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.bind(session);
|
|
96
|
+
const record = this.records.get(session);
|
|
97
|
+
if (record.state === "paused") {
|
|
98
|
+
await this.resume(session, record);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (record.state === "pause-requested" || record.state === "resuming")
|
|
102
|
+
return;
|
|
103
|
+
if (!session.isStreaming && !session.agent.state.isStreaming) {
|
|
104
|
+
this.host.showToast("Agent is not running", "info");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.setState(record, "pause-requested");
|
|
108
|
+
this.host.showToast("Pause requested; waiting for the current turn to finish", "info");
|
|
109
|
+
}
|
|
110
|
+
async resume(session, record) {
|
|
111
|
+
this.setState(record, "resuming");
|
|
112
|
+
this.host.showToast("Continuing agent", "info");
|
|
113
|
+
try {
|
|
114
|
+
// isIdle becomes true before async agent_settled extension handlers finish,
|
|
115
|
+
// so wait for the event itself rather than relying on waitForIdle().
|
|
116
|
+
await record.pauseSettled;
|
|
117
|
+
if (!this.host.isCurrentSession(session)) {
|
|
118
|
+
throw new Error("agent session changed before it could continue");
|
|
119
|
+
}
|
|
120
|
+
const internals = this.sessionInternals(session);
|
|
121
|
+
internals._isAgentRunActive = true;
|
|
122
|
+
try {
|
|
123
|
+
// Continue exactly as AgentSession._runAgentPrompt would.
|
|
124
|
+
await session.agent.continue();
|
|
125
|
+
while (this.host.isCurrentSession(session)) {
|
|
126
|
+
const shouldContinue = await record.originalHandlePostAgentRun();
|
|
127
|
+
if (!shouldContinue || !this.host.isCurrentSession(session))
|
|
128
|
+
break;
|
|
129
|
+
await session.agent.continue();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
internals._systemPromptOverride = undefined;
|
|
134
|
+
internals._flushPendingBashMessages();
|
|
135
|
+
await internals._emitAgentSettled();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
this.setState(record, "paused");
|
|
140
|
+
this.host.showToast(`Could not continue agent: ${errorMessage(error)}`, "error");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.setState(record, "idle");
|
|
144
|
+
}
|
|
145
|
+
setState(record, state) {
|
|
146
|
+
if (record.state === state)
|
|
147
|
+
return;
|
|
148
|
+
record.state = state;
|
|
149
|
+
this.host.render();
|
|
150
|
+
}
|
|
151
|
+
finishSettlement(record) {
|
|
152
|
+
record.resolvePauseSettled?.();
|
|
153
|
+
delete record.resolvePauseSettled;
|
|
154
|
+
if (record.state === "pause-requested") {
|
|
155
|
+
record.pauseBoundaryReached = false;
|
|
156
|
+
this.setState(record, "idle");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
canContinue(session) {
|
|
160
|
+
const messages = session.agent.state.messages;
|
|
161
|
+
const lastMessage = messages[messages.length - 1];
|
|
162
|
+
return Boolean(lastMessage && lastMessage.role !== "assistant") || session.agent.hasQueuedMessages();
|
|
163
|
+
}
|
|
164
|
+
sessionInternals(session) {
|
|
165
|
+
const internals = session;
|
|
166
|
+
if (typeof internals._handlePostAgentRun !== "function"
|
|
167
|
+
|| typeof internals._flushPendingBashMessages !== "function"
|
|
168
|
+
|| typeof internals._emitAgentSettled !== "function"
|
|
169
|
+
|| typeof internals._isAgentRunActive !== "boolean"
|
|
170
|
+
|| !("_systemPromptOverride" in internals)) {
|
|
171
|
+
throw new Error("Agent pause is incompatible with this pi SDK version");
|
|
172
|
+
}
|
|
173
|
+
return internals;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function errorMessage(error) {
|
|
177
|
+
return error instanceof Error ? error.message : String(error);
|
|
178
|
+
}
|
|
@@ -144,6 +144,7 @@ export declare class AppSessionEventController {
|
|
|
144
144
|
touchEntry(entry: Entry): void;
|
|
145
145
|
addEntry(entry: Entry): void;
|
|
146
146
|
prependEntries(entries: readonly Entry[]): void;
|
|
147
|
+
private prependLoadedHistoryEntries;
|
|
147
148
|
private shiftHistoryWindow;
|
|
148
149
|
private setHistoryWindowStart;
|
|
149
150
|
private historyWindowSize;
|
|
@@ -134,7 +134,8 @@ export class AppSessionEventController {
|
|
|
134
134
|
this.historyEntries = [];
|
|
135
135
|
this.historyWindowStart = 0;
|
|
136
136
|
const lazyOlderHistory = options.lazyOlderHistory === true;
|
|
137
|
-
const historyEntries =
|
|
137
|
+
const historyEntries = [];
|
|
138
|
+
let historyAttached = false;
|
|
138
139
|
const messages = lazyOlderHistory
|
|
139
140
|
? sessionHistoryDisplayMessages(runtime.session)
|
|
140
141
|
: sessionHistoryDisplayMessagesFromEntries(await sessionHistoryFullBranchEntries(runtime.session));
|
|
@@ -142,24 +143,22 @@ export class AppSessionEventController {
|
|
|
142
143
|
return false;
|
|
143
144
|
const loaded = await loadSessionHistoryEntriesAsync({
|
|
144
145
|
messages,
|
|
145
|
-
addEntry: (entry) =>
|
|
146
|
-
if (historyEntries)
|
|
147
|
-
historyEntries.push(entry);
|
|
148
|
-
else
|
|
149
|
-
this.addEntry(entry);
|
|
150
|
-
},
|
|
146
|
+
addEntry: (entry) => historyEntries.push(entry),
|
|
151
147
|
prependEntries: (entries) => {
|
|
152
|
-
if (
|
|
153
|
-
|
|
148
|
+
if (historyAttached)
|
|
149
|
+
this.prependLoadedHistoryEntries(entries);
|
|
154
150
|
else
|
|
155
|
-
|
|
151
|
+
historyEntries.unshift(...entries);
|
|
156
152
|
},
|
|
157
153
|
setToolEntryId: (toolCallId, entryId) => this.toolEntryIdsByCallId.set(toolCallId, entryId),
|
|
158
154
|
toolDefaultExpanded: (toolName) => this.host.toolDefaultExpanded(toolName),
|
|
159
155
|
observeSubagentsToolResult: (toolName, details, options) => this.host.observeSubagentsToolResult(toolName, details, options),
|
|
160
156
|
observeTodoToolResult: (toolName, details, isError) => this.host.observeTodoToolResult(toolName, details, isError),
|
|
161
157
|
isCancelled: options.isCancelled,
|
|
162
|
-
render:
|
|
158
|
+
render: () => {
|
|
159
|
+
if (historyAttached)
|
|
160
|
+
options.render();
|
|
161
|
+
},
|
|
163
162
|
lazyOlderHistory,
|
|
164
163
|
olderMessagesReader: lazyOlderHistory ? sessionHistoryOlderMessagesReader(runtime.session) : undefined,
|
|
165
164
|
onOlderLoaderReady: (loader) => {
|
|
@@ -169,24 +168,24 @@ export class AppSessionEventController {
|
|
|
169
168
|
});
|
|
170
169
|
if (!loaded)
|
|
171
170
|
return false;
|
|
172
|
-
if (!historyEntries)
|
|
173
|
-
return !options.isCancelled();
|
|
174
171
|
this.historyEntries = historyEntries;
|
|
172
|
+
historyAttached = true;
|
|
175
173
|
this.setHistoryWindowStart(this.maxHistoryWindowStart());
|
|
176
174
|
options.render();
|
|
177
175
|
return !options.isCancelled();
|
|
178
176
|
}
|
|
179
177
|
hasOlderSessionHistory() {
|
|
180
178
|
if (this.historyEntries.length > 0)
|
|
181
|
-
return this.historyWindowStart > 0;
|
|
179
|
+
return this.historyWindowStart > 0 || this.olderHistoryLoader?.hasOlder() === true;
|
|
182
180
|
return this.olderHistoryLoader?.hasOlder() === true;
|
|
183
181
|
}
|
|
184
182
|
isLoadingOlderSessionHistory() {
|
|
185
183
|
return this.olderHistoryLoader?.isLoading() === true;
|
|
186
184
|
}
|
|
187
185
|
async loadOlderSessionHistory(options = {}) {
|
|
188
|
-
if (this.historyEntries.length > 0)
|
|
186
|
+
if (this.historyEntries.length > 0 && this.historyWindowStart > 0) {
|
|
189
187
|
return this.shiftHistoryWindow(-HISTORY_WINDOW_SHIFT_ENTRIES, options);
|
|
188
|
+
}
|
|
190
189
|
return this.olderHistoryLoader?.loadOlder(options) ?? false;
|
|
191
190
|
}
|
|
192
191
|
/**
|
|
@@ -447,6 +446,14 @@ export class AppSessionEventController {
|
|
|
447
446
|
this.registerEntry(entry);
|
|
448
447
|
this.pruneHistoryWindow("bottom");
|
|
449
448
|
}
|
|
449
|
+
prependLoadedHistoryEntries(entries) {
|
|
450
|
+
if (entries.length === 0)
|
|
451
|
+
return;
|
|
452
|
+
const previousStart = this.historyWindowStart;
|
|
453
|
+
this.historyEntries.unshift(...entries);
|
|
454
|
+
const nextStart = previousStart === 0 ? 0 : previousStart + entries.length;
|
|
455
|
+
this.setHistoryWindowStart(nextStart);
|
|
456
|
+
}
|
|
450
457
|
shiftHistoryWindow(delta, options = {}) {
|
|
451
458
|
if (this.historyEntries.length === 0)
|
|
452
459
|
return false;
|
|
@@ -26,6 +26,7 @@ export type AppSessionLifecycleHost = {
|
|
|
26
26
|
createExtensionCommandContextActions(runtime: AgentSessionRuntime): ExtensionCommandContextActions;
|
|
27
27
|
handleExtensionError(error: ExtensionError): void;
|
|
28
28
|
handleSessionEvent(event: AgentSessionEvent): void;
|
|
29
|
+
bindAgentPause(session: AgentSession): void;
|
|
29
30
|
addEntry(entry: Entry): void;
|
|
30
31
|
setStatus(status: string): void;
|
|
31
32
|
showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
|
|
@@ -73,6 +73,7 @@ export class AppSessionLifecycleController {
|
|
|
73
73
|
async bindCurrentSession(options = {}) {
|
|
74
74
|
const runtime = this.requireRuntime();
|
|
75
75
|
const session = runtime.session;
|
|
76
|
+
this.host.bindAgentPause(session);
|
|
76
77
|
const ownershipGeneration = this.advanceOwnership(runtime, session);
|
|
77
78
|
const subscriptionGeneration = ++this.subscriptionGeneration;
|
|
78
79
|
this.replacementHistoryGeneration += 1;
|