pi-ui-extend 0.1.38 → 0.1.41
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.d.ts +0 -1
- package/dist/app/app.js +28 -21
- package/dist/app/constants.js +1 -1
- package/dist/app/input/input-action-controller.d.ts +1 -0
- package/dist/app/input/input-action-controller.js +3 -0
- package/dist/app/input/input-controller.d.ts +1 -0
- package/dist/app/input/input-controller.js +40 -12
- package/dist/app/model/model-usage-status.js +4 -2
- package/dist/app/process.js +11 -0
- package/dist/app/rendering/conversation-tool-renderer.js +4 -6
- package/dist/app/session/request-history.js +2 -0
- package/dist/app/session/session-event-controller.d.ts +13 -0
- package/dist/app/session/session-event-controller.js +27 -0
- package/dist/app/session/tabs-controller.d.ts +8 -0
- package/dist/app/session/tabs-controller.js +37 -6
- package/dist/app/workspace/workspace-actions-controller.d.ts +1 -0
- package/dist/app/workspace/workspace-actions-controller.js +2 -1
- package/dist/bundled-extensions/terminal-bell/index.js +55 -1
- package/dist/config.js +1 -1
- package/dist/default-pix-config.js +1 -1
- package/dist/markdown-format.js +14 -25
- package/dist/terminal-width.d.ts +14 -0
- package/dist/terminal-width.js +31 -2
- package/dist/theme.js +2 -2
- package/external/pi-tools-suite/README.md +34 -9
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +35 -21
- package/external/pi-tools-suite/src/async-subagents/commands.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +2 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +70 -12
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/types.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/index.ts +6 -6
- package/external/pi-tools-suite/src/async-subagents/lib.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +4 -2
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +2 -2
- package/external/pi-tools-suite/src/{glm-coding-discipline → coding-discipline}/index.ts +17 -8
- package/external/pi-tools-suite/src/config.ts +1 -1
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +368 -0
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +3 -0
- package/external/pi-tools-suite/src/dcp/config.ts +23 -0
- package/external/pi-tools-suite/src/dcp/index.ts +112 -7
- package/external/pi-tools-suite/src/dcp/prompts.ts +8 -0
- package/external/pi-tools-suite/src/dcp/state.ts +41 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +30 -22
- package/external/pi-tools-suite/src/index.ts +2 -1
- package/external/pi-tools-suite/src/session-name/index.ts +37 -0
- package/external/pi-tools-suite/src/tool-descriptions.ts +16 -4
- package/package.json +4 -4
- package/skills/skill-creator/SKILL.md +36 -40
- package/skills/skill-creator/eval-viewer/viewer.html +2 -2
- package/skills/skill-creator/references/schemas.md +1 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/aggregate_benchmark.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/package_skill.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/generate_report.py +1 -1
- package/skills/skill-creator/scripts/improve_description.py +14 -24
- package/skills/skill-creator/scripts/run_eval.py +89 -82
package/dist/app/app.d.ts
CHANGED
|
@@ -94,7 +94,6 @@ export declare class PiUiExtendApp {
|
|
|
94
94
|
private loadSessionHistory;
|
|
95
95
|
private openSearchResultInNewTab;
|
|
96
96
|
private scrollToUserMessageJumpTarget;
|
|
97
|
-
private findUserEntryBySessionEntryId;
|
|
98
97
|
private findUserEntryByJumpText;
|
|
99
98
|
private loadSessionHistoryAsync;
|
|
100
99
|
private handleSessionEvent;
|
package/dist/app/app.js
CHANGED
|
@@ -360,6 +360,7 @@ export class PiUiExtendApp {
|
|
|
360
360
|
});
|
|
361
361
|
this.workspaceActions = new AppWorkspaceActionsController({
|
|
362
362
|
entries: this.entries,
|
|
363
|
+
allEntries: () => this.sessionEvents.allEntries(),
|
|
363
364
|
runtime: () => this.runtime,
|
|
364
365
|
awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
|
|
365
366
|
findUserEntry: (entryId) => this.findUserEntry(entryId),
|
|
@@ -665,6 +666,12 @@ export class PiUiExtendApp {
|
|
|
665
666
|
setSessionActivity: (activity) => this.setSessionActivity(activity),
|
|
666
667
|
addEntry: (entry) => this.addEntry(entry),
|
|
667
668
|
addSessionAbortedEntry: () => this.sessionEvents.addSessionAbortedEntry(),
|
|
669
|
+
emitSessionAborted: () => {
|
|
670
|
+
const runtime = this.runtime;
|
|
671
|
+
if (!runtime)
|
|
672
|
+
return;
|
|
673
|
+
this.extensionEventBusByRuntime.get(runtime)?.emit("pix:session-aborted", { aborted: true });
|
|
674
|
+
},
|
|
668
675
|
showToast: (message, kind) => this.showToast(message, kind),
|
|
669
676
|
dismissActiveDialog: () => this.toastController.dismissActiveDialog(),
|
|
670
677
|
stopVoiceInput: () => this.voiceController.stopRecording(),
|
|
@@ -999,39 +1006,39 @@ export class PiUiExtendApp {
|
|
|
999
1006
|
return true;
|
|
1000
1007
|
this.workspaceActions.syncUserSessionEntryMetadata();
|
|
1001
1008
|
if (target.sessionEntryId) {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
const loaded = await this.sessionEvents.loadOlderSessionHistory({ render: false });
|
|
1005
|
-
if (!loaded)
|
|
1006
|
-
break;
|
|
1007
|
-
entry = this.findUserEntryBySessionEntryId(target.sessionEntryId);
|
|
1008
|
-
}
|
|
1009
|
-
if (entry && this.scrollController.scrollToConversationEntry(entry.id))
|
|
1009
|
+
const entryId = this.sessionEvents.revealHistoryEntryForSessionEntryId(target.sessionEntryId);
|
|
1010
|
+
if (entryId && this.scrollController.scrollToConversationEntry(entryId))
|
|
1010
1011
|
return true;
|
|
1011
1012
|
}
|
|
1012
1013
|
const fallbackEntry = this.findUserEntryByJumpText(target);
|
|
1013
1014
|
return fallbackEntry ? this.scrollController.scrollToConversationEntry(fallbackEntry.id) : false;
|
|
1014
1015
|
}
|
|
1015
|
-
findUserEntryBySessionEntryId(sessionEntryId) {
|
|
1016
|
-
return this.entries.find((entry) => entry.kind === "user" && entry.sessionEntryId === sessionEntryId);
|
|
1017
|
-
}
|
|
1018
1016
|
findUserEntryByJumpText(target) {
|
|
1019
1017
|
if (!target.text)
|
|
1020
1018
|
return undefined;
|
|
1021
|
-
const userEntries = this.
|
|
1019
|
+
const userEntries = this.sessionEvents.allEntries().filter((entry) => entry.kind === "user");
|
|
1020
|
+
let matched;
|
|
1022
1021
|
if (target.userIndex !== undefined && target.userCount !== undefined) {
|
|
1023
|
-
const
|
|
1024
|
-
const entry = userEntries[visibleIndex];
|
|
1022
|
+
const entry = userEntries[target.userIndex];
|
|
1025
1023
|
if (entry && normalizeJumpTargetText(entry.text) === normalizeJumpTargetText(target.text))
|
|
1026
|
-
|
|
1024
|
+
matched = entry;
|
|
1025
|
+
}
|
|
1026
|
+
if (!matched) {
|
|
1027
|
+
const normalizedTargetText = normalizeJumpTargetText(target.text);
|
|
1028
|
+
for (let index = userEntries.length - 1; index >= 0; index -= 1) {
|
|
1029
|
+
const entry = userEntries[index];
|
|
1030
|
+
if (entry && normalizeJumpTargetText(entry.text) === normalizedTargetText) {
|
|
1031
|
+
matched = entry;
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1027
1035
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
return entry;
|
|
1036
|
+
if (matched?.sessionEntryId) {
|
|
1037
|
+
// Shift the sliding window onto the matched entry so it is present in the
|
|
1038
|
+
// viewport for the subsequent scrollToConversationEntry call.
|
|
1039
|
+
this.sessionEvents.revealHistoryEntryForSessionEntryId(matched.sessionEntryId);
|
|
1033
1040
|
}
|
|
1034
|
-
return
|
|
1041
|
+
return matched;
|
|
1035
1042
|
}
|
|
1036
1043
|
async loadSessionHistoryAsync(options) {
|
|
1037
1044
|
return this.sessionEvents.loadSessionHistoryAsync(options);
|
package/dist/app/constants.js
CHANGED
|
@@ -78,7 +78,7 @@ export const SUBAGENTS_WIDGET_MAX_ROWS = 8;
|
|
|
78
78
|
export const DEFAULT_THINKING_TOOL_RULE = {
|
|
79
79
|
previewLines: 0,
|
|
80
80
|
direction: "head",
|
|
81
|
-
color: "
|
|
81
|
+
color: "assistantForeground",
|
|
82
82
|
};
|
|
83
83
|
export const TERMINAL_COMMAND_MODIFIER_FLAG = 8;
|
|
84
84
|
export const GIT_BRANCH_CACHE_MS = 30_000;
|
|
@@ -18,6 +18,7 @@ export type AppInputActionControllerHost = {
|
|
|
18
18
|
setSessionActivity(activity: SessionActivity): void;
|
|
19
19
|
addEntry(entry: Entry): void;
|
|
20
20
|
addSessionAbortedEntry(): void;
|
|
21
|
+
emitSessionAborted(): void;
|
|
21
22
|
showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
|
|
22
23
|
dismissActiveDialog?(): boolean;
|
|
23
24
|
stopVoiceInput(): Promise<void>;
|
|
@@ -88,6 +88,9 @@ export class AppInputActionController {
|
|
|
88
88
|
}
|
|
89
89
|
async abortStreamingSession(runtime, options) {
|
|
90
90
|
const session = runtime.session;
|
|
91
|
+
// Relay the user-initiated abort to extensions (e.g. the terminal-bell
|
|
92
|
+
// extension) so they can suppress the attention bell for this turn.
|
|
93
|
+
this.host.emitSessionAborted();
|
|
91
94
|
if (this.abortInFlight) {
|
|
92
95
|
session.agent.abort();
|
|
93
96
|
if (options.stopIfAlreadyAborting)
|
|
@@ -35,6 +35,7 @@ export declare class AppInputController {
|
|
|
35
35
|
private readonly pasteHandler;
|
|
36
36
|
constructor(host: InputControllerHost);
|
|
37
37
|
handleChunk(chunk: Buffer): void;
|
|
38
|
+
private consumeBufferedSharedEditorInput;
|
|
38
39
|
private consumeSharedEditorInput;
|
|
39
40
|
private drainInputBuffer;
|
|
40
41
|
private consumeBracketedPastePayload;
|
|
@@ -12,13 +12,23 @@ export class AppInputController {
|
|
|
12
12
|
}
|
|
13
13
|
handleChunk(chunk) {
|
|
14
14
|
let data = chunk.toString("utf8");
|
|
15
|
+
const bufferedSharedEditorInput = this.consumeBufferedSharedEditorInput(data);
|
|
16
|
+
if (bufferedSharedEditorInput.kind === "consumed" || bufferedSharedEditorInput.kind === "pending")
|
|
17
|
+
return;
|
|
18
|
+
if (bufferedSharedEditorInput.kind === "passthrough")
|
|
19
|
+
data = bufferedSharedEditorInput.data;
|
|
15
20
|
if (this.inputBuffer.startsWith("\x1b[<") || data.startsWith("\x1b[<")) {
|
|
16
21
|
this.inputBuffer += data;
|
|
17
22
|
this.drainInputBuffer();
|
|
18
23
|
return;
|
|
19
24
|
}
|
|
20
|
-
|
|
25
|
+
const sharedEditorInput = this.consumeSharedEditorInput(data);
|
|
26
|
+
if (sharedEditorInput === "consumed")
|
|
27
|
+
return;
|
|
28
|
+
if (sharedEditorInput === "pending") {
|
|
29
|
+
this.inputBuffer = data;
|
|
21
30
|
return;
|
|
31
|
+
}
|
|
22
32
|
const extensionInput = this.host.handleExtensionTerminalInput(data);
|
|
23
33
|
if (extensionInput.consume)
|
|
24
34
|
return;
|
|
@@ -29,41 +39,59 @@ export class AppInputController {
|
|
|
29
39
|
this.inputBuffer += data;
|
|
30
40
|
this.drainInputBuffer();
|
|
31
41
|
}
|
|
42
|
+
consumeBufferedSharedEditorInput(data) {
|
|
43
|
+
if (this.host.extensionInputUsesEditor?.() !== true)
|
|
44
|
+
return { kind: "none" };
|
|
45
|
+
if (this.inputBuffer.length === 0)
|
|
46
|
+
return { kind: "none" };
|
|
47
|
+
const buffered = `${this.inputBuffer}${data}`;
|
|
48
|
+
const result = this.consumeSharedEditorInput(buffered);
|
|
49
|
+
if (result === "pending") {
|
|
50
|
+
this.inputBuffer = buffered;
|
|
51
|
+
return { kind: "pending" };
|
|
52
|
+
}
|
|
53
|
+
this.inputBuffer = "";
|
|
54
|
+
if (result === "consumed")
|
|
55
|
+
return { kind: "consumed" };
|
|
56
|
+
return { kind: "passthrough", data: buffered };
|
|
57
|
+
}
|
|
32
58
|
consumeSharedEditorInput(data) {
|
|
33
59
|
if (this.host.extensionInputUsesEditor?.() !== true)
|
|
34
|
-
return
|
|
60
|
+
return "none";
|
|
35
61
|
if (this.host.inputEditor.isInBracketedPaste)
|
|
36
|
-
return
|
|
62
|
+
return "none";
|
|
37
63
|
if (data === "\n") {
|
|
38
64
|
this.insertInputNewline();
|
|
39
|
-
return
|
|
65
|
+
return "consumed";
|
|
40
66
|
}
|
|
41
67
|
if (data === "\r" && this.isShiftPressed()) {
|
|
42
68
|
this.insertInputNewline();
|
|
43
|
-
return
|
|
69
|
+
return "consumed";
|
|
44
70
|
}
|
|
45
71
|
if (SHIFT_ENTER_ESCAPE_SEQUENCES.includes(data)) {
|
|
46
72
|
this.insertInputNewline();
|
|
47
|
-
return
|
|
73
|
+
return "consumed";
|
|
48
74
|
}
|
|
49
75
|
if (data === "\x16") {
|
|
50
76
|
void this.pasteHandler.handleClipboardImagePaste();
|
|
51
|
-
return
|
|
77
|
+
return "consumed";
|
|
52
78
|
}
|
|
53
79
|
const modifiedKey = parseTerminalModifiedKeySequence(data);
|
|
80
|
+
if (modifiedKey.kind === "pending")
|
|
81
|
+
return "pending";
|
|
54
82
|
if (modifiedKey.kind !== "key")
|
|
55
|
-
return
|
|
83
|
+
return "none";
|
|
56
84
|
if (terminalKeyShouldIgnore(modifiedKey.key))
|
|
57
|
-
return
|
|
85
|
+
return "consumed";
|
|
58
86
|
if (terminalKeyIsShiftEnter(modifiedKey.key)) {
|
|
59
87
|
this.insertInputNewline();
|
|
60
|
-
return
|
|
88
|
+
return "consumed";
|
|
61
89
|
}
|
|
62
90
|
if (terminalKeyIsClipboardImagePaste(modifiedKey.key)) {
|
|
63
91
|
void this.pasteHandler.handleClipboardImagePaste();
|
|
64
|
-
return
|
|
92
|
+
return "consumed";
|
|
65
93
|
}
|
|
66
|
-
return
|
|
94
|
+
return "none";
|
|
67
95
|
}
|
|
68
96
|
drainInputBuffer() {
|
|
69
97
|
while (this.inputBuffer.length > 0) {
|
|
@@ -13,6 +13,8 @@ const GOOGLE_ANTIGRAVITY_USER_AGENT = "antigravity/1.11.9 windows/amd64";
|
|
|
13
13
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
14
14
|
const DAY_SECONDS = 86_400;
|
|
15
15
|
const HOUR_SECONDS = 3_600;
|
|
16
|
+
const MODEL_USAGE_WARNING_MIN_USED_PERCENT = 5;
|
|
17
|
+
const MODEL_USAGE_WARNING_MIN_ELAPSED_SECONDS = 6 * HOUR_SECONDS;
|
|
16
18
|
const PI_AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");
|
|
17
19
|
const DEFAULT_ANTIGRAVITY_PROJECT_ID = "rising-fact-p41fc";
|
|
18
20
|
function getPiAuthPath() {
|
|
@@ -929,11 +931,11 @@ function modelUsageWindowWillExhaustBeforeReset(window, now) {
|
|
|
929
931
|
return false;
|
|
930
932
|
const timeUntilResetSeconds = Math.max(0, (window.resetAt - now) / 1000);
|
|
931
933
|
const elapsedSeconds = Math.max(0, window.windowSeconds - timeUntilResetSeconds);
|
|
932
|
-
if (elapsedSeconds
|
|
934
|
+
if (elapsedSeconds < MODEL_USAGE_WARNING_MIN_ELAPSED_SECONDS)
|
|
933
935
|
return false;
|
|
934
936
|
const total = 100;
|
|
935
937
|
const used = total - window.remainingPercent;
|
|
936
|
-
if (used
|
|
938
|
+
if (used < MODEL_USAGE_WARNING_MIN_USED_PERCENT)
|
|
937
939
|
return false;
|
|
938
940
|
const remaining = total - used;
|
|
939
941
|
const averageRate = used / elapsedSeconds;
|
package/dist/app/process.js
CHANGED
|
@@ -37,6 +37,17 @@ export async function runProcess(command, args = [], options = {}) {
|
|
|
37
37
|
child.once("error", (err) => {
|
|
38
38
|
error = err;
|
|
39
39
|
});
|
|
40
|
+
// Writing to stdin after the child has closed it raises EPIPE. This is
|
|
41
|
+
// common with clipboard helpers (xclip/xsel/wl-copy) that exit once they
|
|
42
|
+
// have read enough, or when a candidate command exits early. The child's
|
|
43
|
+
// exit status is still captured by the "close" handler, so treat EPIPE as
|
|
44
|
+
// benign and never let it surface as an unhandled "error" event.
|
|
45
|
+
child.stdin?.once("error", (err) => {
|
|
46
|
+
if (err?.code === "EPIPE")
|
|
47
|
+
return;
|
|
48
|
+
if (error === undefined)
|
|
49
|
+
error = err;
|
|
50
|
+
});
|
|
40
51
|
child.once("close", (status, signal) => {
|
|
41
52
|
if (timer)
|
|
42
53
|
clearTimeout(timer);
|
|
@@ -7,7 +7,6 @@ import { formatStructuredText } from "./message-content.js";
|
|
|
7
7
|
import { formatSubagentTimestamp, isSubagentRunRenderDetails, isSubagentsToolName, subagentRunName, subagentStatusIcon, taskPreviewMap, } from "../subagents/subagents-model.js";
|
|
8
8
|
import { formatTodoTaskLine, isTodoDetails, visibleTodoTasks } from "../todo/todo-model.js";
|
|
9
9
|
import { renderToolBlock } from "./tool-block-renderer.js";
|
|
10
|
-
import { thinkingLevelThemeColor } from "./status-line-renderer.js";
|
|
11
10
|
export function renderConversationToolEntry(entry, width, options) {
|
|
12
11
|
const todoLines = renderTodoToolEntry(entry, width, options);
|
|
13
12
|
if (todoLines)
|
|
@@ -53,14 +52,14 @@ export function renderThinkingEntry(entry, width, options) {
|
|
|
53
52
|
const forceExpanded = Boolean(options.allThinkingExpanded);
|
|
54
53
|
const compactExpandedText = options.superCompactTools && forceExpanded ? removeBlankLines(expandedText) : expandedText;
|
|
55
54
|
const expanded = forceExpanded || (entry.expanded && expandedText.trim().length > 0);
|
|
56
|
-
const headerColorOverride = entry.level
|
|
57
|
-
? thinkingLevelThemeColor(entry.level, options.colors, options.availableThinkingLevels)
|
|
58
|
-
: undefined;
|
|
59
55
|
const elapsed = thinkingElapsedText(entry, options.currentTimeMs ?? Date.now());
|
|
56
|
+
const headerArgs = [entry.level ? `(${entry.level})` : undefined, elapsed]
|
|
57
|
+
.filter((part) => part !== undefined)
|
|
58
|
+
.join(" ");
|
|
60
59
|
return renderToolBlock({
|
|
61
60
|
id: entry.id,
|
|
62
61
|
toolName: THINKING_TOOL_NAME,
|
|
63
|
-
...(
|
|
62
|
+
...(headerArgs === "" ? {} : { headerArgs }),
|
|
64
63
|
expanded,
|
|
65
64
|
status: entry.status,
|
|
66
65
|
isError: false,
|
|
@@ -73,7 +72,6 @@ export function renderThinkingEntry(entry, width, options) {
|
|
|
73
72
|
superCompact: Boolean(options.superCompactTools && !forceExpanded),
|
|
74
73
|
backgroundOverride: options.colors.thinkingMessageBackground,
|
|
75
74
|
showGutter: true,
|
|
76
|
-
...(headerColorOverride === undefined ? {} : { headerColorOverride }),
|
|
77
75
|
});
|
|
78
76
|
}
|
|
79
77
|
function thinkingElapsedText(entry, currentTimeMs) {
|
|
@@ -61,6 +61,8 @@ export class AppRequestHistory {
|
|
|
61
61
|
if (this.cursor === undefined) {
|
|
62
62
|
if (direction > 0)
|
|
63
63
|
return false;
|
|
64
|
+
if (this.host.getInput().length > 0)
|
|
65
|
+
return false;
|
|
64
66
|
this.draft = this.host.getInput();
|
|
65
67
|
this.cursor = this.entries.length - 1;
|
|
66
68
|
const entry = this.entries[this.cursor];
|
|
@@ -90,6 +90,7 @@ export declare class AppSessionEventController {
|
|
|
90
90
|
snapshotState(): AppSessionEventControllerState;
|
|
91
91
|
restoreState(state: AppSessionEventControllerState): void;
|
|
92
92
|
reset(): void;
|
|
93
|
+
allEntries(): readonly Entry[];
|
|
93
94
|
loadSessionHistory(): void;
|
|
94
95
|
loadSessionHistoryAsync(options: {
|
|
95
96
|
isCancelled: () => boolean;
|
|
@@ -99,6 +100,18 @@ export declare class AppSessionEventController {
|
|
|
99
100
|
hasOlderSessionHistory(): boolean;
|
|
100
101
|
isLoadingOlderSessionHistory(): boolean;
|
|
101
102
|
loadOlderSessionHistory(options?: LoadOlderSessionHistoryOptions): Promise<boolean>;
|
|
103
|
+
/**
|
|
104
|
+
* Reveal the conversation entry for a session entry id (e.g. a user message
|
|
105
|
+
* selected from the jump menu) by shifting the sliding window directly onto
|
|
106
|
+
* it, then return its local entry id so the caller can scroll to it.
|
|
107
|
+
*
|
|
108
|
+
* Unlike paging older history in fixed increments, this works for any branch
|
|
109
|
+
* position in a single synchronous step and does not race with concurrent
|
|
110
|
+
* window re-anchors (e.g. while the agent is streaming). Returns undefined
|
|
111
|
+
* when the entry is neither in the full history window nor in the live
|
|
112
|
+
* entries (e.g. older than the loaded branch).
|
|
113
|
+
*/
|
|
114
|
+
revealHistoryEntryForSessionEntryId(sessionEntryId: string): string | undefined;
|
|
102
115
|
hasNewerSessionHistory(): boolean;
|
|
103
116
|
isLoadingNewerSessionHistory(): boolean;
|
|
104
117
|
loadNewerSessionHistory(options?: {
|
|
@@ -109,6 +109,9 @@ export class AppSessionEventController {
|
|
|
109
109
|
this.assistantTextBuffer = "";
|
|
110
110
|
this.olderHistoryLoader = undefined;
|
|
111
111
|
}
|
|
112
|
+
allEntries() {
|
|
113
|
+
return this.historyEntries.length > 0 ? this.historyEntries : this.host.entries;
|
|
114
|
+
}
|
|
112
115
|
loadSessionHistory() {
|
|
113
116
|
const runtime = this.host.runtime();
|
|
114
117
|
if (!runtime)
|
|
@@ -165,6 +168,30 @@ export class AppSessionEventController {
|
|
|
165
168
|
return this.shiftHistoryWindow(-HISTORY_WINDOW_SHIFT_ENTRIES, options);
|
|
166
169
|
return this.olderHistoryLoader?.loadOlder(options) ?? false;
|
|
167
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Reveal the conversation entry for a session entry id (e.g. a user message
|
|
173
|
+
* selected from the jump menu) by shifting the sliding window directly onto
|
|
174
|
+
* it, then return its local entry id so the caller can scroll to it.
|
|
175
|
+
*
|
|
176
|
+
* Unlike paging older history in fixed increments, this works for any branch
|
|
177
|
+
* position in a single synchronous step and does not race with concurrent
|
|
178
|
+
* window re-anchors (e.g. while the agent is streaming). Returns undefined
|
|
179
|
+
* when the entry is neither in the full history window nor in the live
|
|
180
|
+
* entries (e.g. older than the loaded branch).
|
|
181
|
+
*/
|
|
182
|
+
revealHistoryEntryForSessionEntryId(sessionEntryId) {
|
|
183
|
+
if (this.historyEntries.length > 0) {
|
|
184
|
+
const index = this.historyEntries.findIndex((entry) => entry.kind === "user" && entry.sessionEntryId === sessionEntryId);
|
|
185
|
+
if (index === -1)
|
|
186
|
+
return undefined;
|
|
187
|
+
const targetStart = Math.max(0, Math.min(this.maxHistoryWindowStart(), index - Math.floor(this.historyWindowSize() / 2)));
|
|
188
|
+
if (targetStart !== this.historyWindowStart)
|
|
189
|
+
this.setHistoryWindowStart(targetStart);
|
|
190
|
+
return this.historyEntries[index]?.id;
|
|
191
|
+
}
|
|
192
|
+
const entry = this.host.entries.find((entry) => entry.kind === "user" && entry.sessionEntryId === sessionEntryId);
|
|
193
|
+
return entry?.id;
|
|
194
|
+
}
|
|
168
195
|
hasNewerSessionHistory() {
|
|
169
196
|
return this.historyEntries.length > 0 && this.historyWindowStart < this.maxHistoryWindowStart();
|
|
170
197
|
}
|
|
@@ -155,6 +155,14 @@ export declare class AppTabsController {
|
|
|
155
155
|
private scheduleTabPrewarm;
|
|
156
156
|
private prewarmTabs;
|
|
157
157
|
private cleanupOldProjectSessions;
|
|
158
|
+
/**
|
|
159
|
+
* Unlink a project session file and, best-effort, its DCP sidecar state.
|
|
160
|
+
* The sidecar path is derived from the session id in the first line of the
|
|
161
|
+
* `.jsonl` (mirrors the DCP module's `safeSessionFileName`), so retention
|
|
162
|
+
* never leaves orphan sidecars behind. Everything here is best-effort:
|
|
163
|
+
* session retention must never interrupt the terminal UI.
|
|
164
|
+
*/
|
|
165
|
+
private unlinkSessionAndDcpSidecar;
|
|
158
166
|
private preservedSessionPaths;
|
|
159
167
|
private maxProjectSessions;
|
|
160
168
|
}
|
|
@@ -1543,12 +1543,7 @@ export class AppTabsController {
|
|
|
1543
1543
|
for (const session of sessions) {
|
|
1544
1544
|
if (keep.has(session.path))
|
|
1545
1545
|
continue;
|
|
1546
|
-
|
|
1547
|
-
await unlink(session.path);
|
|
1548
|
-
}
|
|
1549
|
-
catch {
|
|
1550
|
-
// Session retention must never interrupt the terminal UI.
|
|
1551
|
-
}
|
|
1546
|
+
await this.unlinkSessionAndDcpSidecar(session.path);
|
|
1552
1547
|
}
|
|
1553
1548
|
}
|
|
1554
1549
|
catch {
|
|
@@ -1558,6 +1553,42 @@ export class AppTabsController {
|
|
|
1558
1553
|
this.retentionCleanupRunning = false;
|
|
1559
1554
|
}
|
|
1560
1555
|
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Unlink a project session file and, best-effort, its DCP sidecar state.
|
|
1558
|
+
* The sidecar path is derived from the session id in the first line of the
|
|
1559
|
+
* `.jsonl` (mirrors the DCP module's `safeSessionFileName`), so retention
|
|
1560
|
+
* never leaves orphan sidecars behind. Everything here is best-effort:
|
|
1561
|
+
* session retention must never interrupt the terminal UI.
|
|
1562
|
+
*/
|
|
1563
|
+
async unlinkSessionAndDcpSidecar(sessionPath) {
|
|
1564
|
+
let sidecarPath;
|
|
1565
|
+
try {
|
|
1566
|
+
const firstLine = (await readFile(sessionPath, "utf8")).split("\n", 1)[0]?.trim();
|
|
1567
|
+
if (firstLine) {
|
|
1568
|
+
const parsed = JSON.parse(firstLine);
|
|
1569
|
+
if (parsed.type === "session" && typeof parsed.id === "string" && parsed.id) {
|
|
1570
|
+
sidecarPath = join(dirname(sessionPath), "dcp-state", parsed.id.replace(/[^a-zA-Z0-9._-]/g, "_") + ".json");
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
catch {
|
|
1575
|
+
// Reading the session id is best-effort; proceed to unlink the file.
|
|
1576
|
+
}
|
|
1577
|
+
try {
|
|
1578
|
+
await unlink(sessionPath);
|
|
1579
|
+
}
|
|
1580
|
+
catch {
|
|
1581
|
+
// Session retention must never interrupt the terminal UI.
|
|
1582
|
+
}
|
|
1583
|
+
if (sidecarPath) {
|
|
1584
|
+
try {
|
|
1585
|
+
await unlink(sidecarPath);
|
|
1586
|
+
}
|
|
1587
|
+
catch {
|
|
1588
|
+
// Sidecar removal is best-effort; never interrupt the terminal UI.
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1561
1592
|
preservedSessionPaths() {
|
|
1562
1593
|
const preserved = new Set();
|
|
1563
1594
|
const add = (sessionPath) => {
|
|
@@ -3,6 +3,7 @@ import type { Entry } from "../types.js";
|
|
|
3
3
|
import { type WorkspaceMutation, type WorkspaceMutationFromToolInput, type WorkspaceMutationPreparation } from "./workspace-undo.js";
|
|
4
4
|
export type AppWorkspaceActionsControllerHost = {
|
|
5
5
|
readonly entries: Entry[];
|
|
6
|
+
allEntries?(): readonly Entry[];
|
|
6
7
|
runtime(): AgentSessionRuntime | undefined;
|
|
7
8
|
awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
|
|
8
9
|
findUserEntry(entryId: string): Extract<Entry, {
|
|
@@ -48,8 +48,9 @@ export class AppWorkspaceActionsController {
|
|
|
48
48
|
});
|
|
49
49
|
if (branchUserEntries.length === 0)
|
|
50
50
|
return;
|
|
51
|
+
const loadedEntries = this.host.allEntries?.() ?? this.host.entries;
|
|
51
52
|
let branchIndex = 0;
|
|
52
|
-
for (const entry of
|
|
53
|
+
for (const entry of loadedEntries) {
|
|
53
54
|
if (entry.kind !== "user")
|
|
54
55
|
continue;
|
|
55
56
|
const sessionEntry = branchUserEntries[branchIndex];
|
|
@@ -15,7 +15,14 @@ const TERMINAL_BELL_ATTENTION_EVENT = "pix:terminal-bell:attention";
|
|
|
15
15
|
* extensions, so the renderer emits this on the extension event bus.
|
|
16
16
|
*/
|
|
17
17
|
const RETRY_ACTIVE_EVENT = "pix:retry-active";
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Renderer-relayed signal that the user interrupted the session (Esc/Ctrl-C).
|
|
20
|
+
* Payload: `{ aborted: boolean }`. Aborting the SDK stream during tool
|
|
21
|
+
* execution does not always produce an aborted `message_update`, so the
|
|
22
|
+
* renderer relays the abort here to reliably suppress the attention bell.
|
|
23
|
+
*/
|
|
24
|
+
const SESSION_ABORTED_EVENT = "pix:session-aborted";
|
|
25
|
+
const DEFAULT_COMPLETION_NOTIFICATION_TITLE = "Pix - complete";
|
|
19
26
|
const DEFAULT_ERROR_NOTIFICATION_TITLE = "Pix - error";
|
|
20
27
|
const DEFAULT_QUESTION_NOTIFICATION_TITLE = "Pix - question";
|
|
21
28
|
const DEFAULT_NOTIFICATION_MESSAGE = "{sessionName}";
|
|
@@ -220,9 +227,16 @@ function notificationTitleTemplate(defaultTitle) {
|
|
|
220
227
|
function willRetryAfterAgentEnd(event) {
|
|
221
228
|
return event.willRetry === true;
|
|
222
229
|
}
|
|
230
|
+
function isAbortedMessageUpdate(event) {
|
|
231
|
+
return event.type === "error" && event.reason === "aborted";
|
|
232
|
+
}
|
|
223
233
|
function failureReasonFromMessageUpdate(event) {
|
|
224
234
|
if (event.type !== "error")
|
|
225
235
|
return undefined;
|
|
236
|
+
// The SDK reports a user-initiated interrupt as `{ type: "error", reason: "aborted" }`.
|
|
237
|
+
// That is not a failure the bell should announce, so treat it as "no reason".
|
|
238
|
+
if (event.reason === "aborted")
|
|
239
|
+
return undefined;
|
|
226
240
|
const reason = event.error?.errorMessage;
|
|
227
241
|
return typeof reason === "string" ? trimmed(reason) : undefined;
|
|
228
242
|
}
|
|
@@ -387,6 +401,10 @@ export default function terminalBell(pi) {
|
|
|
387
401
|
let deferredUntilSubagentsFinish = false;
|
|
388
402
|
let liveSubagentCount = 0;
|
|
389
403
|
let lastFailureReason;
|
|
404
|
+
// True when the user interrupted the session this turn (Esc/Ctrl-C). The
|
|
405
|
+
// attention bell should never ring for a user-initiated abort, so this flag
|
|
406
|
+
// suppresses any queued/pending bell until the next agent_start resets it.
|
|
407
|
+
let userAborted = false;
|
|
390
408
|
// True while the session is in an auto-retry cycle (relayed via the
|
|
391
409
|
// extension event bus). Suppresses the failure bell on intermediate retry
|
|
392
410
|
// attempts; the final exhausted failure still rings because no retry-start
|
|
@@ -426,6 +444,13 @@ export default function terminalBell(pi) {
|
|
|
426
444
|
// queued this bell and the timer firing, suppress the bell entirely.
|
|
427
445
|
if (retryActive)
|
|
428
446
|
return;
|
|
447
|
+
// Safety net: if the user aborted after the bell was queued (e.g. an
|
|
448
|
+
// aborted agent_end with no aborted message_update), suppress it.
|
|
449
|
+
if (userAborted) {
|
|
450
|
+
pendingBell = undefined;
|
|
451
|
+
deferredUntilSubagentsFinish = false;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
429
454
|
try {
|
|
430
455
|
if (!ctx.isIdle()) {
|
|
431
456
|
if (attempt < MAX_IDLE_RETRIES)
|
|
@@ -514,15 +539,39 @@ export default function terminalBell(pi) {
|
|
|
514
539
|
deferredUntilSubagentsFinish = false;
|
|
515
540
|
}
|
|
516
541
|
});
|
|
542
|
+
pi.events.on(SESSION_ABORTED_EVENT, (data) => {
|
|
543
|
+
const aborted = data != null && typeof data === "object" && data.aborted === true;
|
|
544
|
+
if (!aborted)
|
|
545
|
+
return;
|
|
546
|
+
// The user interrupted the session. Aborting during tool execution does
|
|
547
|
+
// not always produce an aborted `message_update`, so the renderer relays
|
|
548
|
+
// the interrupt here. Suppress any pending bell until the next agent_start.
|
|
549
|
+
userAborted = true;
|
|
550
|
+
lastFailureReason = undefined;
|
|
551
|
+
clearTimer();
|
|
552
|
+
pendingBell = undefined;
|
|
553
|
+
deferredUntilSubagentsFinish = false;
|
|
554
|
+
});
|
|
517
555
|
pi.on("agent_start", async () => {
|
|
518
556
|
clearTimer();
|
|
519
557
|
deferredUntilSubagentsFinish = false;
|
|
520
558
|
lastFailureReason = undefined;
|
|
559
|
+
userAborted = false;
|
|
521
560
|
retryActive = false;
|
|
522
561
|
activeSubagentWaitToolCallIds.clear();
|
|
523
562
|
notifiedAskUserToolCallIds.clear();
|
|
524
563
|
});
|
|
525
564
|
pi.on("message_update", async (event) => {
|
|
565
|
+
if (isAbortedMessageUpdate(event.assistantMessageEvent)) {
|
|
566
|
+
// The user interrupted the stream. Suppress any pending bell until
|
|
567
|
+
// the next agent_start.
|
|
568
|
+
userAborted = true;
|
|
569
|
+
lastFailureReason = undefined;
|
|
570
|
+
clearTimer();
|
|
571
|
+
pendingBell = undefined;
|
|
572
|
+
deferredUntilSubagentsFinish = false;
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
526
575
|
const reason = failureReasonFromMessageUpdate(event.assistantMessageEvent);
|
|
527
576
|
if (reason) {
|
|
528
577
|
lastFailureReason = reason;
|
|
@@ -554,6 +603,10 @@ export default function terminalBell(pi) {
|
|
|
554
603
|
clearTimer();
|
|
555
604
|
return;
|
|
556
605
|
}
|
|
606
|
+
if (userAborted) {
|
|
607
|
+
clearTimer();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
557
610
|
if (lastFailureReason) {
|
|
558
611
|
scheduleBell(ctx, idleDelayMs, 0, renderNotificationTemplate(retryFailureMessageTemplate(), {
|
|
559
612
|
...buildNotificationTemplateValues(ctx, pi),
|
|
@@ -569,6 +622,7 @@ export default function terminalBell(pi) {
|
|
|
569
622
|
deferredUntilSubagentsFinish = false;
|
|
570
623
|
liveSubagentCount = 0;
|
|
571
624
|
lastFailureReason = undefined;
|
|
625
|
+
userAborted = false;
|
|
572
626
|
retryActive = false;
|
|
573
627
|
activeSubagentWaitToolCallIds.clear();
|
|
574
628
|
notifiedAskUserToolCallIds.clear();
|
package/dist/config.js
CHANGED
|
@@ -19,7 +19,7 @@ const DEFAULT_TOOL_RENDERER = {
|
|
|
19
19
|
color: "toolTitle",
|
|
20
20
|
},
|
|
21
21
|
tools: {
|
|
22
|
-
thinking: { previewLines: 0, direction: "head", color: "
|
|
22
|
+
thinking: { previewLines: 0, direction: "head", color: "assistantForeground" },
|
|
23
23
|
bash: { previewLines: 6, direction: "tail", color: "warning" },
|
|
24
24
|
Bash: { previewLines: 6, direction: "tail", color: "warning" },
|
|
25
25
|
shell: { previewLines: 6, direction: "tail", color: "warning" },
|
|
@@ -10,7 +10,7 @@ export const DEFAULT_PIX_CONFIG_JSONC = String.raw `{
|
|
|
10
10
|
"toolRenderer": {
|
|
11
11
|
"default": { "previewLines": 0, "direction": "head", "color": "toolTitle" },
|
|
12
12
|
"tools": {
|
|
13
|
-
"thinking": { "previewLines": 0, "direction": "head", "color": "
|
|
13
|
+
"thinking": { "previewLines": 0, "direction": "head", "color": "assistantForeground" },
|
|
14
14
|
"bash": { "previewLines": 6, "direction": "tail", "color": "warning" },
|
|
15
15
|
"Bash": { "previewLines": 6, "direction": "tail", "color": "warning" },
|
|
16
16
|
"shell": { "previewLines": 6, "direction": "tail", "color": "warning" },
|