pi-ui-extend 0.1.77 → 1.0.1
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 +3 -0
- package/dist/app/rendering/toast-controller.d.ts +21 -6
- package/dist/app/rendering/toast-controller.js +37 -1
- package/dist/app/rendering/toast-renderer.d.ts +2 -2
- package/dist/app/rendering/toast-renderer.js +35 -2
- package/dist/app/runtime.d.ts +2 -1
- package/dist/app/runtime.js +6 -1
- package/dist/app/screen/mouse-controller.d.ts +1 -0
- package/dist/app/screen/mouse-controller.js +5 -0
- package/dist/app/session/queued-message-controller.d.ts +1 -0
- package/dist/app/session/queued-message-controller.js +3 -0
- package/dist/app/session/session-event-controller.d.ts +10 -1
- package/dist/app/session/session-event-controller.js +54 -2
- package/dist/app/types.d.ts +1 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +2 -2
- package/package.json +1 -1
package/dist/app/app.js
CHANGED
|
@@ -387,6 +387,7 @@ export class PiUiExtendApp {
|
|
|
387
387
|
this.sessionEvents = new AppSessionEventController({
|
|
388
388
|
entries: this.entries,
|
|
389
389
|
runtime: () => this.runtime,
|
|
390
|
+
awaitSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
|
|
390
391
|
conversationViewport: () => this.conversationViewport,
|
|
391
392
|
conversationViewportColumns: () => this.terminalColumns(),
|
|
392
393
|
onHistoryWindowPruned: (edge, lineCount) => this.scrollController.adjustForHistoryWindowPrune(edge, lineCount),
|
|
@@ -421,6 +422,7 @@ export class PiUiExtendApp {
|
|
|
421
422
|
this.queuedMessages = new AppQueuedMessageController({
|
|
422
423
|
runtime: () => this.runtime,
|
|
423
424
|
requireRuntime: () => this.requireRuntime(),
|
|
425
|
+
awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
|
|
424
426
|
visibleEntries: () => this.conversationViewport.entries(),
|
|
425
427
|
isRunning: () => this.running,
|
|
426
428
|
render: () => this.render(),
|
|
@@ -605,6 +607,7 @@ export class PiUiExtendApp {
|
|
|
605
607
|
toastEntry: (toastId) => this.toastController.entry(toastId),
|
|
606
608
|
showToast: (message, kind, options) => this.showToast(message, kind, options),
|
|
607
609
|
dismissToast: (toastId) => this.toastController.dismissToast(toastId),
|
|
610
|
+
activateToastAction: (toastId) => this.toastController.activateAction(toastId),
|
|
608
611
|
refreshModelUsageStatus: () => this.refreshModelUsageStatusFromClick(),
|
|
609
612
|
refreshUserMessageJumpMenuItems: () => this.menuItems.refreshUserMessageJumpMenuItems(),
|
|
610
613
|
queueInputFromStatus: () => {
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { type ToastEntry, type ToastKind, type ToastVariant } from "../../ui.js";
|
|
2
|
+
export type AppToastEntry = ToastEntry & {
|
|
3
|
+
action?: {
|
|
4
|
+
label: string;
|
|
5
|
+
};
|
|
6
|
+
};
|
|
7
|
+
export type AppToastAction = {
|
|
8
|
+
label: string;
|
|
9
|
+
onSelect: () => void;
|
|
10
|
+
};
|
|
11
|
+
export type AppToastOptions = {
|
|
12
|
+
durationMs?: number;
|
|
13
|
+
variant?: ToastVariant;
|
|
14
|
+
scopeKey?: string;
|
|
15
|
+
action?: AppToastAction;
|
|
16
|
+
};
|
|
2
17
|
export type AppToastControllerHost = {
|
|
3
18
|
activeScope?(): string | undefined;
|
|
4
19
|
render(): void;
|
|
@@ -7,19 +22,19 @@ export declare class AppToastController {
|
|
|
7
22
|
private readonly host;
|
|
8
23
|
private readonly toastsByScope;
|
|
9
24
|
private readonly timers;
|
|
25
|
+
private readonly actions;
|
|
10
26
|
constructor(host: AppToastControllerHost);
|
|
11
|
-
showToast(message: string, kind?: ToastKind, options?:
|
|
12
|
-
durationMs?: number;
|
|
13
|
-
variant?: ToastVariant;
|
|
14
|
-
scopeKey?: string;
|
|
15
|
-
}): void;
|
|
27
|
+
showToast(message: string, kind?: ToastKind, options?: AppToastOptions): void;
|
|
16
28
|
dismissToast(toastId: number, scopeKey?: string): void;
|
|
29
|
+
activateAction(toastId: number, scopeKey?: string): boolean;
|
|
17
30
|
dismissActiveDialog(scopeKey?: string): boolean;
|
|
18
|
-
visibleStates(scopeKey?: string): readonly
|
|
31
|
+
visibleStates(scopeKey?: string): readonly AppToastEntry[];
|
|
19
32
|
entry(toastId: number, scopeKey?: string): ToastEntry | undefined;
|
|
20
33
|
clearToastTimers(): void;
|
|
21
34
|
private toastForScope;
|
|
22
35
|
private timersForScope;
|
|
36
|
+
private actionsForScope;
|
|
37
|
+
private removeAction;
|
|
23
38
|
private deleteScopeIfEmpty;
|
|
24
39
|
private normalizeScopeKey;
|
|
25
40
|
}
|
|
@@ -4,13 +4,17 @@ export class AppToastController {
|
|
|
4
4
|
host;
|
|
5
5
|
toastsByScope = new Map();
|
|
6
6
|
timers = new Map();
|
|
7
|
+
actions = new Map();
|
|
7
8
|
constructor(host) {
|
|
8
9
|
this.host = host;
|
|
9
10
|
}
|
|
10
11
|
showToast(message, kind = "info", options = {}) {
|
|
11
12
|
const scopeKey = this.normalizeScopeKey(options.scopeKey ?? this.host.activeScope?.());
|
|
12
13
|
const toast = this.toastForScope(scopeKey);
|
|
14
|
+
const action = options.variant === "dialog" ? undefined : options.action;
|
|
13
15
|
const toastId = toast.show(message, kind, options.variant ? { variant: options.variant } : {});
|
|
16
|
+
if (action)
|
|
17
|
+
this.actionsForScope(scopeKey).set(toastId, action);
|
|
14
18
|
if (kind === "error" || options.variant === "dialog") {
|
|
15
19
|
this.host.render();
|
|
16
20
|
return;
|
|
@@ -21,6 +25,7 @@ export class AppToastController {
|
|
|
21
25
|
const timer = setTimeout(() => {
|
|
22
26
|
toast.hide(toastId);
|
|
23
27
|
this.timers.get(scopeKey)?.delete(toastId);
|
|
28
|
+
this.removeAction(scopeKey, toastId);
|
|
24
29
|
this.deleteScopeIfEmpty(scopeKey);
|
|
25
30
|
this.host.render();
|
|
26
31
|
}, durationMs);
|
|
@@ -36,9 +41,18 @@ export class AppToastController {
|
|
|
36
41
|
timers?.delete(toastId);
|
|
37
42
|
}
|
|
38
43
|
this.toastsByScope.get(scopeKey)?.hide(toastId);
|
|
44
|
+
this.removeAction(scopeKey, toastId);
|
|
39
45
|
this.deleteScopeIfEmpty(scopeKey);
|
|
40
46
|
this.host.render();
|
|
41
47
|
}
|
|
48
|
+
activateAction(toastId, scopeKey = this.normalizeScopeKey(this.host.activeScope?.())) {
|
|
49
|
+
const action = this.actions.get(scopeKey)?.get(toastId);
|
|
50
|
+
if (!action)
|
|
51
|
+
return false;
|
|
52
|
+
this.dismissToast(toastId, scopeKey);
|
|
53
|
+
action.onSelect();
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
42
56
|
dismissActiveDialog(scopeKey = this.normalizeScopeKey(this.host.activeScope?.())) {
|
|
43
57
|
const states = this.visibleStates(scopeKey);
|
|
44
58
|
let dialog;
|
|
@@ -57,7 +71,14 @@ export class AppToastController {
|
|
|
57
71
|
return true;
|
|
58
72
|
}
|
|
59
73
|
visibleStates(scopeKey = this.normalizeScopeKey(this.host.activeScope?.())) {
|
|
60
|
-
|
|
74
|
+
const states = this.toastsByScope.get(scopeKey)?.visibleStates ?? [];
|
|
75
|
+
const actions = this.actions.get(scopeKey);
|
|
76
|
+
if (!actions || actions.size === 0)
|
|
77
|
+
return states;
|
|
78
|
+
return states.map((state) => {
|
|
79
|
+
const action = actions.get(state.id);
|
|
80
|
+
return action ? { ...state, action: { label: action.label } } : state;
|
|
81
|
+
});
|
|
61
82
|
}
|
|
62
83
|
entry(toastId, scopeKey = this.normalizeScopeKey(this.host.activeScope?.())) {
|
|
63
84
|
return this.toastsByScope.get(scopeKey)?.entry(toastId);
|
|
@@ -68,6 +89,7 @@ export class AppToastController {
|
|
|
68
89
|
clearTimeout(timer);
|
|
69
90
|
}
|
|
70
91
|
this.timers.clear();
|
|
92
|
+
this.actions.clear();
|
|
71
93
|
for (const toast of this.toastsByScope.values())
|
|
72
94
|
toast.hide();
|
|
73
95
|
this.toastsByScope.clear();
|
|
@@ -88,6 +110,20 @@ export class AppToastController {
|
|
|
88
110
|
}
|
|
89
111
|
return timers;
|
|
90
112
|
}
|
|
113
|
+
actionsForScope(scopeKey) {
|
|
114
|
+
let actions = this.actions.get(scopeKey);
|
|
115
|
+
if (!actions) {
|
|
116
|
+
actions = new Map();
|
|
117
|
+
this.actions.set(scopeKey, actions);
|
|
118
|
+
}
|
|
119
|
+
return actions;
|
|
120
|
+
}
|
|
121
|
+
removeAction(scopeKey, toastId) {
|
|
122
|
+
const actions = this.actions.get(scopeKey);
|
|
123
|
+
actions?.delete(toastId);
|
|
124
|
+
if (actions?.size === 0)
|
|
125
|
+
this.actions.delete(scopeKey);
|
|
126
|
+
}
|
|
91
127
|
deleteScopeIfEmpty(scopeKey) {
|
|
92
128
|
const timers = this.timers.get(scopeKey);
|
|
93
129
|
if (timers && timers.size === 0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Theme } from "../../theme.js";
|
|
2
|
-
import type { ToastEntry } from "../../ui.js";
|
|
3
2
|
import type { ToastLineTarget } from "../types.js";
|
|
3
|
+
import type { AppToastEntry } from "./toast-controller.js";
|
|
4
4
|
export type ToastOverlay = {
|
|
5
5
|
id: number;
|
|
6
6
|
row: number;
|
|
@@ -9,4 +9,4 @@ export type ToastOverlay = {
|
|
|
9
9
|
output: string;
|
|
10
10
|
target?: ToastLineTarget;
|
|
11
11
|
};
|
|
12
|
-
export declare function renderToastOverlays(states: readonly
|
|
12
|
+
export declare function renderToastOverlays(states: readonly AppToastEntry[], width: number, maxRows: number, theme: Theme): ToastOverlay[];
|
|
@@ -15,10 +15,13 @@ export function renderToastOverlays(states, width, maxRows, theme) {
|
|
|
15
15
|
}
|
|
16
16
|
const icon = toastKindIcon(state.kind);
|
|
17
17
|
const lines = toastMessageLines(state.message, icon, Math.max(1, width - 6));
|
|
18
|
-
const
|
|
18
|
+
const availableRows = Math.max(0, maxRows - overlays.length);
|
|
19
|
+
const actionLabel = compactToastActionLabel(state);
|
|
20
|
+
const includeAction = actionLabel !== undefined && availableRows >= 2;
|
|
21
|
+
const visibleLines = lines.slice(0, Math.max(0, availableRows - (includeAction ? 1 : 0)));
|
|
19
22
|
if (visibleLines.length === 0)
|
|
20
23
|
continue;
|
|
21
|
-
const contentWidth = Math.max(...visibleLines.map((line) => stringDisplayWidth(line)));
|
|
24
|
+
const contentWidth = Math.max(...visibleLines.map((line) => stringDisplayWidth(line)), ...(includeAction ? [stringDisplayWidth(actionLabel)] : []));
|
|
22
25
|
const toastWidth = Math.min(Math.max(12, contentWidth + 2), Math.max(1, width - 4));
|
|
23
26
|
const leftWidth = Math.max(0, width - toastWidth - 2);
|
|
24
27
|
const column = leftWidth + 1;
|
|
@@ -39,9 +42,39 @@ export function renderToastOverlays(states, width, maxRows, theme) {
|
|
|
39
42
|
target: { kind: "toast", id: state.id, action: "toast", startColumn: column, endColumn: column + toastWidth },
|
|
40
43
|
});
|
|
41
44
|
}
|
|
45
|
+
if (includeAction) {
|
|
46
|
+
const innerWidth = Math.max(0, toastWidth - 2);
|
|
47
|
+
const renderedLabel = padOrTrimPlain(actionLabel, innerWidth).trimEnd();
|
|
48
|
+
const labelWidth = stringDisplayWidth(renderedLabel);
|
|
49
|
+
if (labelWidth > 0) {
|
|
50
|
+
const leftPadding = Math.max(0, innerWidth - labelWidth);
|
|
51
|
+
const message = ` ${" ".repeat(leftPadding)}${renderedLabel} `;
|
|
52
|
+
const startColumn = column + 1 + leftPadding;
|
|
53
|
+
overlays.push({
|
|
54
|
+
id: state.id,
|
|
55
|
+
row: overlays.length + 1,
|
|
56
|
+
column,
|
|
57
|
+
text: padOrTrimPlain(message, toastWidth),
|
|
58
|
+
output: colorToastLine(message, toastWidth, { ...style, bold: true }),
|
|
59
|
+
target: {
|
|
60
|
+
kind: "toast",
|
|
61
|
+
id: state.id,
|
|
62
|
+
action: "action",
|
|
63
|
+
startColumn,
|
|
64
|
+
endColumn: startColumn + labelWidth,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
42
69
|
}
|
|
43
70
|
return overlays;
|
|
44
71
|
}
|
|
72
|
+
function compactToastActionLabel(state) {
|
|
73
|
+
if (!state.action)
|
|
74
|
+
return undefined;
|
|
75
|
+
const label = stripToastAnsi(sanitizeToastText(state.action.label)).split("\n", 1)[0]?.trim();
|
|
76
|
+
return label ? `[${label}]` : undefined;
|
|
77
|
+
}
|
|
45
78
|
function toastMessageLines(message, icon, maxWidth) {
|
|
46
79
|
const firstPrefix = `${icon} `;
|
|
47
80
|
const continuationPrefix = " ".repeat(stringDisplayWidth(firstPrefix));
|
package/dist/app/runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SessionManager, type EventBus, type AgentSessionRuntime, type LoadExtensionsResult, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { SessionManager, type EventBus, type AgentSessionRuntime, type AgentSessionServices, type LoadExtensionsResult, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type PixConfig } from "../config.js";
|
|
3
3
|
import type { AppOptions, ThinkingLevel } from "./types.js";
|
|
4
4
|
export type PiToolsSuiteInstallAction = "installed" | "already-installed" | "existing-kept" | "missing-source";
|
|
@@ -46,5 +46,6 @@ type RuntimeSessionManagerModelState = Pick<SessionManager, "getEntries" | "getB
|
|
|
46
46
|
export declare function resolvePixRuntimeModelRef(options: Pick<AppOptions, "modelRef">, sessionManager: RuntimeSessionManagerModelState, config?: PixConfig): string | undefined;
|
|
47
47
|
export declare function resolvePixRuntimeInitialThinkingLevel(options: Pick<AppOptions, "modelRef">, sessionManager: RuntimeSessionManagerModelState, config: PixConfig): ThinkingLevel | undefined;
|
|
48
48
|
export declare function resolveSessionModelRefFromTail(entries: readonly SessionEntry[]): string | undefined;
|
|
49
|
+
export declare function refreshPixModelRuntimeForStartup(modelRuntime: Pick<AgentSessionServices["modelRuntime"], "refresh">): Promise<void>;
|
|
49
50
|
export declare function createPixRuntime(options: AppOptions, runtimeOptions?: CreatePixRuntimeOptions): Promise<AgentSessionRuntime>;
|
|
50
51
|
export {};
|
package/dist/app/runtime.js
CHANGED
|
@@ -235,6 +235,11 @@ export function resolveSessionModelRefFromTail(entries) {
|
|
|
235
235
|
return undefined;
|
|
236
236
|
return thinkingLevel ? `${modelRef}:${thinkingLevel}` : modelRef;
|
|
237
237
|
}
|
|
238
|
+
export async function refreshPixModelRuntimeForStartup(modelRuntime) {
|
|
239
|
+
// Startup only needs the locally configured model catalog. Remote catalog
|
|
240
|
+
// refreshes belong to explicit model-management flows and must not block boot.
|
|
241
|
+
await modelRuntime.refresh({ allowNetwork: false });
|
|
242
|
+
}
|
|
238
243
|
export async function createPixRuntime(options, runtimeOptions = {}) {
|
|
239
244
|
const agentDir = getAgentDir();
|
|
240
245
|
const reusableServices = reusableRuntimeServices(runtimeOptions.reuseServicesFrom, options.cwd, agentDir);
|
|
@@ -256,7 +261,7 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
|
|
|
256
261
|
config,
|
|
257
262
|
...(runtimeOptions.eventBus === undefined ? {} : { eventBus: runtimeOptions.eventBus }),
|
|
258
263
|
});
|
|
259
|
-
await services.modelRuntime
|
|
264
|
+
await refreshPixModelRuntimeForStartup(services.modelRuntime);
|
|
260
265
|
const model = parsedModel ? services.modelRuntime.getModel(parsedModel.provider, parsedModel.modelId) : undefined;
|
|
261
266
|
if (parsedModel && !model) {
|
|
262
267
|
throw new Error(`Model not found: ${parsedModel.provider}/${parsedModel.modelId}`);
|
|
@@ -53,6 +53,7 @@ export type AppMouseControllerHost = {
|
|
|
53
53
|
variant?: ToastVariant;
|
|
54
54
|
}): void;
|
|
55
55
|
dismissToast(toastId: number): void;
|
|
56
|
+
activateToastAction(toastId: number): boolean;
|
|
56
57
|
refreshModelUsageStatus(): void | Promise<void>;
|
|
57
58
|
refreshUserMessageJumpMenuItems?(): Promise<void>;
|
|
58
59
|
queueInputFromStatus?(): void | Promise<void>;
|
|
@@ -117,6 +117,11 @@ export class AppMouseController {
|
|
|
117
117
|
return;
|
|
118
118
|
if (target.action === "body")
|
|
119
119
|
return;
|
|
120
|
+
if (target.action === "action") {
|
|
121
|
+
if (this.host.activateToastAction(target.id))
|
|
122
|
+
this.showClickFlashForEvent(event);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
120
125
|
if (this.copyErrorToast(target.id)) {
|
|
121
126
|
this.showClickFlashForEvent(event);
|
|
122
127
|
return;
|
|
@@ -4,6 +4,7 @@ import type { Entry, SessionActivity, SubmittedUserMessage } from "../types.js";
|
|
|
4
4
|
export type AppQueuedMessageControllerHost = {
|
|
5
5
|
runtime(): AgentSessionRuntime | undefined;
|
|
6
6
|
requireRuntime(): AgentSessionRuntime;
|
|
7
|
+
awaitCurrentSessionExtensions(runtime: AgentSessionRuntime): Promise<void>;
|
|
7
8
|
visibleEntries(): readonly Entry[];
|
|
8
9
|
isRunning(): boolean;
|
|
9
10
|
render(): void;
|
|
@@ -59,6 +59,9 @@ export class AppQueuedMessageController {
|
|
|
59
59
|
this.promptSubmissionsInFlight.add(targetSession);
|
|
60
60
|
this.host.setSessionActivity("running");
|
|
61
61
|
try {
|
|
62
|
+
const runtime = this.host.runtime();
|
|
63
|
+
if (runtime?.session === targetSession)
|
|
64
|
+
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
62
65
|
const opts = {};
|
|
63
66
|
if (targetSession.isStreaming)
|
|
64
67
|
opts.streamingBehavior = options.streamingBehavior ?? "steer";
|
|
@@ -29,6 +29,7 @@ export type AppSessionEventControllerState = {
|
|
|
29
29
|
export type AppSessionEventControllerHost = {
|
|
30
30
|
readonly entries: Entry[];
|
|
31
31
|
runtime(): AgentSessionRuntime | undefined;
|
|
32
|
+
awaitSessionExtensions?(runtime: AgentSessionRuntime): Promise<void>;
|
|
32
33
|
conversationViewport(): ConversationViewport;
|
|
33
34
|
conversationViewportColumns?(): number;
|
|
34
35
|
onHistoryWindowPruned?(edge: "top" | "bottom", lineCount: number): void;
|
|
@@ -63,7 +64,12 @@ export type AppSessionEventControllerHost = {
|
|
|
63
64
|
showSnapshot?: boolean;
|
|
64
65
|
}): void;
|
|
65
66
|
observeTodoToolResult(toolName: string, details: unknown, isError?: boolean): void;
|
|
66
|
-
showToast(message: string, kind: "success" | "error" | "warning" | "info"
|
|
67
|
+
showToast(message: string, kind: "success" | "error" | "warning" | "info", options?: {
|
|
68
|
+
action?: {
|
|
69
|
+
label: string;
|
|
70
|
+
onSelect: () => void;
|
|
71
|
+
};
|
|
72
|
+
}): void;
|
|
67
73
|
};
|
|
68
74
|
export declare class AppSessionEventController {
|
|
69
75
|
private readonly host;
|
|
@@ -118,6 +124,9 @@ export declare class AppSessionEventController {
|
|
|
118
124
|
render?: boolean;
|
|
119
125
|
}): Promise<boolean>;
|
|
120
126
|
handleSessionEvent(event: AgentSessionEvent): void;
|
|
127
|
+
private retryFailedTurn;
|
|
128
|
+
private retryFailedTurnAsync;
|
|
129
|
+
private isActiveRuntimeSession;
|
|
121
130
|
addCustomMessageEntry(message: Record<string, unknown>): void;
|
|
122
131
|
findEntry(id: string): Entry | undefined;
|
|
123
132
|
findUserEntry(id: string): Extract<Entry, {
|
|
@@ -328,18 +328,70 @@ export class AppSessionEventController {
|
|
|
328
328
|
this.host.setStatus(`retry ${event.attempt}/${event.maxAttempts}`);
|
|
329
329
|
this.host.emitExtensionEvent(RETRY_ACTIVE_EVENT, { active: true });
|
|
330
330
|
break;
|
|
331
|
-
case "auto_retry_end":
|
|
331
|
+
case "auto_retry_end": {
|
|
332
332
|
this.host.setSessionActivity(this.host.runtime()?.session.isStreaming ? "running" : "idle");
|
|
333
333
|
this.host.restoreSessionStatus();
|
|
334
334
|
this.host.flushAutoUserMessages();
|
|
335
335
|
this.host.emitExtensionEvent(RETRY_ACTIVE_EVENT, { active: false });
|
|
336
|
-
|
|
336
|
+
if (event.success) {
|
|
337
|
+
this.host.showToast("Retry succeeded", "success");
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
const session = this.host.runtime()?.session;
|
|
341
|
+
this.host.showToast(`Retry failed: ${event.finalError}`, "error", session
|
|
342
|
+
? { action: { label: "Retry", onSelect: () => this.retryFailedTurn(session) } }
|
|
343
|
+
: undefined);
|
|
337
344
|
break;
|
|
345
|
+
}
|
|
338
346
|
default:
|
|
339
347
|
break;
|
|
340
348
|
}
|
|
341
349
|
this.host.scheduleRender();
|
|
342
350
|
}
|
|
351
|
+
retryFailedTurn(session) {
|
|
352
|
+
void this.retryFailedTurnAsync(session);
|
|
353
|
+
}
|
|
354
|
+
async retryFailedTurnAsync(session) {
|
|
355
|
+
const runtime = this.host.runtime();
|
|
356
|
+
if (!runtime || !this.isActiveRuntimeSession(runtime, session))
|
|
357
|
+
return;
|
|
358
|
+
if (session.isStreaming || session.isCompacting) {
|
|
359
|
+
this.host.showToast("Retry is unavailable while the session is busy", "warning");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
this.host.setStatus("retrying failed turn");
|
|
363
|
+
this.host.setSessionActivity("running");
|
|
364
|
+
this.host.render();
|
|
365
|
+
try {
|
|
366
|
+
await this.host.awaitSessionExtensions?.(runtime);
|
|
367
|
+
if (!this.isActiveRuntimeSession(runtime, session))
|
|
368
|
+
return;
|
|
369
|
+
if (session.isStreaming || session.isCompacting) {
|
|
370
|
+
this.host.showToast("Retry is unavailable while the session is busy", "warning");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
await session.sendCustomMessage({
|
|
374
|
+
customType: "pix-retry",
|
|
375
|
+
content: "Continue the previous task from where you stopped.",
|
|
376
|
+
display: false,
|
|
377
|
+
}, { triggerTurn: true });
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
if (this.isActiveRuntimeSession(runtime, session)) {
|
|
381
|
+
this.host.showToast(`Retry could not start: ${stringifyUnknown(error)}`, "error");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
if (this.isActiveRuntimeSession(runtime, session)) {
|
|
386
|
+
this.host.setSessionStatus(session);
|
|
387
|
+
this.host.setSessionActivity(session.isStreaming || session.isCompacting ? "running" : "idle");
|
|
388
|
+
this.host.render();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
isActiveRuntimeSession(runtime, session) {
|
|
393
|
+
return this.host.isRunning() && this.host.runtime() === runtime && runtime.session === session;
|
|
394
|
+
}
|
|
343
395
|
addCustomMessageEntry(message) {
|
|
344
396
|
const entry = customMessageEntry(message);
|
|
345
397
|
if (entry)
|
package/dist/app/types.d.ts
CHANGED
|
@@ -221,10 +221,10 @@ export const TODO_TOOL_DESCRIPTION: ToolDescription = {
|
|
|
221
221
|
"Use `todo` for complex work with 3+ steps, explicit user task lists, or new non-trivial requirements. Skip single trivial tasks and purely conversational requests.",
|
|
222
222
|
"For multi-step implementation/debugging plans, include a final user-facing report todo in the initial plan with acceptance criteria for changed files/behavior, verification results, and remaining manual actions; close it immediately before the final response, never via compression.",
|
|
223
223
|
"When create or batch_create already sets the intended status, do not issue a redundant update with the same status; continue the work instead.",
|
|
224
|
-
"Resync before continuing when user/new findings change scope, requirements, safety, feasibility, approach, dependencies, or order
|
|
224
|
+
"Resync before continuing when user/new findings change scope, requirements, safety, feasibility, approach, dependencies, or order. Before creating tasks, review pending/deferred todos; when work resumes, reactivate and reuse an equivalent deferred todo instead of duplicating it.",
|
|
225
225
|
"Update todos when starting, finishing, blocking, splitting, abandoning, or materially changing a step; before planned work mark exactly one in_progress with activeForm and complete it only after verification.",
|
|
226
226
|
"If partial, tests fail, or blocked, keep the task in_progress and add/update a blocker. Never use `clear`, `delete`, or batch deletion to hide unfinished/stale/forgotten todos; delete only on explicit request or creation mistake.",
|
|
227
|
-
"Before a final response
|
|
227
|
+
"Before a final response, list and reconcile all visible todos, including deferred ones: complete any whose outcome was achieved elsewhere, and leave deferred only genuinely unfinished or intentionally out of scope, mentioning each in the response. Do not finish with stale/duplicate deferred todos or a just-finished item in_progress.",
|
|
228
228
|
"Keep subjects short; use parentId/blockers for hierarchy/dependencies. For large explicit plans use batch_create/batch_update but keep exactly one visible in_progress unless asked otherwise; use batch_create replace:true only for superseding plans.",
|
|
229
229
|
"list hides tombstones unless includeDeleted:true; use status/blockedOnly only when needed; export/import for handoff, import replace:true only when explicitly overwriting; when all visible todos complete, state clears automatically.",
|
|
230
230
|
"Persistence: `/todos persist on|off|status` or `/todos-persist on|off|status`; on resume, ask in-scope ids and run `/todos scope <id...>` or `/todos-scope <id...>` so out-of-scope active tasks are deferred.",
|
package/package.json
CHANGED