pi-ui-extend 0.1.34 → 0.1.36
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 +20 -0
- package/dist/app/app.d.ts +1 -0
- package/dist/app/app.js +12 -2
- package/dist/app/commands/command-host.d.ts +1 -0
- package/dist/app/commands/command-model-actions.d.ts +1 -0
- package/dist/app/commands/command-model-actions.js +32 -0
- package/dist/app/commands/command-navigation-actions.js +3 -0
- package/dist/app/commands/command-session-actions.js +2 -0
- package/dist/app/constants.d.ts +2 -1
- package/dist/app/constants.js +6 -1
- package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
- package/dist/app/extensions/extension-actions-controller.js +4 -0
- package/dist/app/input/input-controller.d.ts +5 -1
- package/dist/app/input/input-controller.js +122 -16
- package/dist/app/input/input-paste-handler.js +3 -1
- package/dist/app/input/terminal-edit-shortcuts.d.ts +21 -0
- package/dist/app/input/terminal-edit-shortcuts.js +92 -16
- package/dist/app/popup/popup-action-controller.d.ts +1 -0
- package/dist/app/popup/popup-action-controller.js +1 -0
- package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
- package/dist/app/rendering/conversation-entry-renderer.js +1 -1
- package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
- package/dist/app/rendering/conversation-tool-renderer.js +21 -0
- package/dist/app/rendering/conversation-viewport.d.ts +3 -0
- package/dist/app/rendering/conversation-viewport.js +41 -5
- package/dist/app/rendering/editor-layout-renderer.js +3 -2
- package/dist/app/rendering/editor-panels.js +27 -10
- package/dist/app/runtime.d.ts +1 -0
- package/dist/app/runtime.js +33 -14
- package/dist/app/session/session-event-controller.d.ts +7 -0
- package/dist/app/session/session-event-controller.js +78 -0
- package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
- package/dist/app/session/session-lifecycle-controller.js +7 -0
- package/dist/app/session/tabs-controller.d.ts +1 -0
- package/dist/app/session/tabs-controller.js +4 -1
- package/dist/app/subagents/subagents-widget-controller.d.ts +10 -2
- package/dist/app/subagents/subagents-widget-controller.js +141 -70
- package/dist/app/terminal/terminal-controller.d.ts +10 -0
- package/dist/app/terminal/terminal-controller.js +91 -2
- package/dist/app/todo/todo-model.js +2 -0
- package/dist/app/todo/todo-widget-controller.d.ts +2 -0
- package/dist/app/todo/todo-widget-controller.js +17 -7
- package/dist/app/types.d.ts +4 -0
- package/dist/app/workspace/workspace-actions-controller.d.ts +1 -0
- package/dist/app/workspace/workspace-actions-controller.js +1 -0
- package/dist/bundled-extensions/question/tui.js +8 -1
- package/dist/bundled-extensions/session-title/index.js +65 -14
- package/dist/input-editor-files.js +23 -4
- package/dist/markdown-format.d.ts +4 -1
- package/dist/markdown-format.js +76 -9
- package/external/pi-tools-suite/README.md +71 -1
- package/external/pi-tools-suite/package.json +5 -5
- package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -6
- package/external/pi-tools-suite/src/async-subagents/index.ts +133 -37
- package/external/pi-tools-suite/src/context-usage.ts +6 -1
- package/external/pi-tools-suite/src/dcp/commands.ts +3 -2
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +9 -4
- package/external/pi-tools-suite/src/dcp/config.ts +142 -6
- package/external/pi-tools-suite/src/dcp/index.ts +20 -8
- package/external/pi-tools-suite/src/dcp/prompts.ts +17 -9
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +59 -15
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +6 -8
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +51 -1
- package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +16 -11
- package/external/pi-tools-suite/src/model-tools/index.ts +24 -12
- package/external/pi-tools-suite/src/prompt-commands/index.ts +11 -2
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +66 -27
- package/external/pi-tools-suite/src/todo/index.ts +87 -16
- package/external/pi-tools-suite/src/todo/state/store.ts +41 -10
- package/external/pi-tools-suite/src/todo/todo.ts +49 -6
- package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
- package/package.json +7 -5
|
@@ -5,6 +5,7 @@ import type { Api, AssistantMessage, ImageContent, Model, TextContent } from "@e
|
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
|
|
7
7
|
import { loadPiToolsSuiteConfig } from "../config.js";
|
|
8
|
+
import { ignoreStaleExtensionContextError } from "../context-usage.js";
|
|
8
9
|
|
|
9
10
|
type ExtensionAPI = any;
|
|
10
11
|
|
|
@@ -189,20 +190,24 @@ export default function glmCodingDiscipline(pi: ExtensionAPI) {
|
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
function syncLookupToolAvailability(modelRef: string | undefined, cwd?: string): void {
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
try {
|
|
194
|
+
const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : undefined;
|
|
195
|
+
if (!Array.isArray(activeTools)) return;
|
|
194
196
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
const lookupEnabled = Boolean(lookupModelFromConfig(cwd));
|
|
198
|
+
const shouldExposeLookup = lookupEnabled && isGlmModel(modelRef);
|
|
199
|
+
const hasLookup = activeTools.includes(LOOKUP_TOOL_NAME);
|
|
198
200
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
if (shouldExposeLookup === hasLookup) return;
|
|
202
|
+
if (typeof pi.setActiveTools !== "function") return;
|
|
201
203
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
const nextTools = shouldExposeLookup
|
|
205
|
+
? [...activeTools, LOOKUP_TOOL_NAME]
|
|
206
|
+
: activeTools.filter((tool: unknown) => tool !== LOOKUP_TOOL_NAME);
|
|
207
|
+
pi.setActiveTools([...new Set(nextTools)]);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
ignoreStaleExtensionContextError(error);
|
|
210
|
+
}
|
|
206
211
|
}
|
|
207
212
|
|
|
208
213
|
maybeRegisterLookupTool(process.cwd());
|
|
@@ -40,6 +40,14 @@ const MANAGED_TOOLS = new Set([...CLAUDE_ALIAS_TOOLS, ...CODEX_ALIAS_TOOLS, ...B
|
|
|
40
40
|
const REPO_DISCOVERY_TOOL_NAME_SET = new Set(REPO_DISCOVERY_TOOL_NAMES);
|
|
41
41
|
const MAX_BUILTIN_DEFINITIONS = 64;
|
|
42
42
|
|
|
43
|
+
function isStaleExtensionContextError(error: unknown): boolean {
|
|
44
|
+
return error instanceof Error && /ctx is stale|stale ctx|stale after session replacement|stale after.*reload/i.test(error.message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ignoreStaleExtensionContextError(error: unknown): void {
|
|
48
|
+
if (!isStaleExtensionContextError(error)) throw error;
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
type ShellAliasInput = {
|
|
44
52
|
command?: string;
|
|
45
53
|
description?: string;
|
|
@@ -391,18 +399,22 @@ function sameTools(left: string[], right: string[]): boolean {
|
|
|
391
399
|
}
|
|
392
400
|
|
|
393
401
|
function applyToolProfile(pi: ExtensionAPI, model: unknown, baseTools: string[]): void {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
402
|
+
try {
|
|
403
|
+
const targetTools = toolsForProfile(detectModelProfile(model));
|
|
404
|
+
const preserveSelection = shouldPreserveSelection();
|
|
405
|
+
const active = pi.getActiveTools();
|
|
406
|
+
const repoTools = activeRepoDiscoveryTools(active, baseTools);
|
|
407
|
+
const preserved = active.filter((tool) => !MANAGED_TOOLS.has(tool) && !REPO_DISCOVERY_TOOL_NAME_SET.has(tool));
|
|
408
|
+
const baseWithoutRepoTools = baseTools.filter((tool) => !REPO_DISCOVERY_TOOL_NAME_SET.has(tool));
|
|
409
|
+
const selectedTargetTools = preserveSelection
|
|
410
|
+
? selectSuitableToolsForModel(model, active.filter((tool) => MANAGED_TOOLS.has(tool)))
|
|
411
|
+
: targetTools;
|
|
412
|
+
const next = selectedTargetTools ? [...repoTools, ...preserved, ...selectedTargetTools] : [...repoTools, ...preserved, ...baseWithoutRepoTools];
|
|
413
|
+
const nextTools = [...new Set(next)];
|
|
414
|
+
if (!sameTools(active, nextTools)) pi.setActiveTools(nextTools);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
ignoreStaleExtensionContextError(error);
|
|
417
|
+
}
|
|
406
418
|
}
|
|
407
419
|
|
|
408
420
|
function shouldPreserveSelection(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
@@ -3,6 +3,7 @@ import { dirname } from "node:path";
|
|
|
3
3
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser";
|
|
5
5
|
import { ensurePiToolsSuiteUserConfig, getPiToolsSuiteUserConfigPath } from "../config";
|
|
6
|
+
import { ignoreStaleExtensionContextError } from "../context-usage";
|
|
6
7
|
import { publishStartupSection } from "../startup-section";
|
|
7
8
|
|
|
8
9
|
type PromptCommand = {
|
|
@@ -166,12 +167,20 @@ async function runPromptCommand(pi: ExtensionAPI, ctx: ExtensionCommandContext,
|
|
|
166
167
|
if (!prompt) return notify(ctx, `/${name} has an empty prompt.`, "error");
|
|
167
168
|
|
|
168
169
|
await ctx.waitForIdle();
|
|
169
|
-
|
|
170
|
+
try {
|
|
171
|
+
pi.sendUserMessage(prompt);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
ignoreStaleExtensionContextError(error);
|
|
174
|
+
}
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
async function reloadAfterConfigChange(ctx: ExtensionCommandContext, message: string): Promise<void> {
|
|
173
178
|
notify(ctx, `${message}\nReloading commands from ${getConfigPath()}…`);
|
|
174
|
-
|
|
179
|
+
try {
|
|
180
|
+
await ctx.reload();
|
|
181
|
+
} catch (error) {
|
|
182
|
+
ignoreStaleExtensionContextError(error);
|
|
183
|
+
}
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
async function selectCommand(ctx: ExtensionContext, title: string, commands: Record<string, PromptCommand>): Promise<string | undefined> {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
37
37
|
import { loadTelegramMirrorConfig } from "../config.js";
|
|
38
|
+
import { ignoreStaleExtensionContextError, isAgentBusyRaceError } from "../context-usage.js";
|
|
38
39
|
import { TelegramBot } from "./bot.js";
|
|
39
40
|
import { captureAbortableContext, registerPixEventHandlers, type PixMirrorHooks, type RendererSink } from "./events.js";
|
|
40
41
|
import { TurnRenderer, type RendererEvent } from "./renderer.js";
|
|
@@ -104,11 +105,32 @@ export default function telegramMirror(pi: ExtensionAPI): void {
|
|
|
104
105
|
console.error(`[telegram-mirror] ${message}`);
|
|
105
106
|
};
|
|
106
107
|
|
|
108
|
+
function staleSafe<T>(callback: () => T, fallback?: T): T | undefined {
|
|
109
|
+
try {
|
|
110
|
+
return callback();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
ignoreStaleExtensionContextError(error);
|
|
113
|
+
return fallback;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sendUserMessageSafely(text: string): void {
|
|
118
|
+
try {
|
|
119
|
+
pi.sendUserMessage(text);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (isAgentBusyRaceError(error)) {
|
|
122
|
+
pi.sendUserMessage(text, { deliverAs: "followUp" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
107
129
|
// Dispatch the leader uses to execute commands on its own pi session.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
130
|
+
const localDispatch: LocalDispatch = {
|
|
131
|
+
sendUserMessage(text) {
|
|
132
|
+
staleSafe(() => sendUserMessageSafely(text));
|
|
133
|
+
},
|
|
112
134
|
currentDialog() {
|
|
113
135
|
return mirrorCtx?.currentDialog();
|
|
114
136
|
},
|
|
@@ -119,8 +141,12 @@ export default function telegramMirror(pi: ExtensionAPI): void {
|
|
|
119
141
|
mirrorCtx?.compact();
|
|
120
142
|
},
|
|
121
143
|
status() {
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
const ctx = mirrorCtx;
|
|
145
|
+
if (!ctx) return undefined;
|
|
146
|
+
return {
|
|
147
|
+
idle: staleSafe(() => ctx.isIdle(), true) ?? true,
|
|
148
|
+
hasPending: staleSafe(() => ctx.hasPendingMessages(), false) ?? false,
|
|
149
|
+
};
|
|
124
150
|
},
|
|
125
151
|
};
|
|
126
152
|
|
|
@@ -336,7 +362,7 @@ export default function telegramMirror(pi: ExtensionAPI): void {
|
|
|
336
362
|
try {
|
|
337
363
|
switch (command) {
|
|
338
364
|
case "sendUserMessage":
|
|
339
|
-
|
|
365
|
+
sendUserMessageSafely(((args as { text?: string } | undefined)?.text ?? ""));
|
|
340
366
|
break;
|
|
341
367
|
case "abort":
|
|
342
368
|
mirrorCtx?.abort();
|
|
@@ -477,22 +503,30 @@ export default function telegramMirror(pi: ExtensionAPI): void {
|
|
|
477
503
|
function refreshCtx(ctx: ExtensionContext | undefined): void {
|
|
478
504
|
if (!ctx) return;
|
|
479
505
|
if (!mirrorCtx) {
|
|
480
|
-
|
|
481
|
-
abort: () =>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
506
|
+
mirrorCtx = makeCtx({
|
|
507
|
+
abort: () => {
|
|
508
|
+
staleSafe(() => ctx.abort());
|
|
509
|
+
},
|
|
510
|
+
isIdle: () => staleSafe(() => ctx.isIdle(), true) ?? true,
|
|
511
|
+
hasPendingMessages: () => staleSafe(() => ctx.hasPendingMessages(), false) ?? false,
|
|
512
|
+
compact: () => {
|
|
513
|
+
staleSafe(() => ctx.compact());
|
|
514
|
+
},
|
|
485
515
|
currentDialog: () => currentDialogFromContext(ctx),
|
|
486
516
|
});
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
const m = mirrorCtx as Mutable<MirrorContext>;
|
|
490
|
-
m.abort = () => ctx.abort();
|
|
491
|
-
m.isIdle = () => ctx.isIdle();
|
|
492
|
-
m.hasPendingMessages = () => ctx.hasPendingMessages();
|
|
493
|
-
m.compact = () => ctx.compact();
|
|
494
|
-
m.currentDialog = () => currentDialogFromContext(ctx);
|
|
517
|
+
return;
|
|
495
518
|
}
|
|
519
|
+
const m = mirrorCtx as Mutable<MirrorContext>;
|
|
520
|
+
m.abort = () => {
|
|
521
|
+
staleSafe(() => ctx.abort());
|
|
522
|
+
};
|
|
523
|
+
m.isIdle = () => staleSafe(() => ctx.isIdle(), true) ?? true;
|
|
524
|
+
m.hasPendingMessages = () => staleSafe(() => ctx.hasPendingMessages(), false) ?? false;
|
|
525
|
+
m.compact = () => {
|
|
526
|
+
staleSafe(() => ctx.compact());
|
|
527
|
+
};
|
|
528
|
+
m.currentDialog = () => currentDialogFromContext(ctx);
|
|
529
|
+
}
|
|
496
530
|
|
|
497
531
|
function refreshSelfInfo(ctx: ExtensionContext | undefined): void {
|
|
498
532
|
const snapshot = sessionSnapshot(ctx);
|
|
@@ -611,13 +645,18 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
611
645
|
|
|
612
646
|
function sessionSnapshot(ctx: ExtensionContext | undefined): SessionSnapshot | undefined {
|
|
613
647
|
if (!ctx) return undefined;
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
648
|
+
try {
|
|
649
|
+
const manager = ctx.sessionManager;
|
|
650
|
+
return {
|
|
651
|
+
cwd: manager.getCwd?.() ?? ctx.cwd,
|
|
652
|
+
...(manager.getSessionId?.() ? { sessionId: manager.getSessionId() } : {}),
|
|
653
|
+
...(manager.getSessionFile?.() ? { sessionFile: manager.getSessionFile() } : {}),
|
|
654
|
+
...(manager.getSessionName?.() ? { sessionName: manager.getSessionName() } : {}),
|
|
655
|
+
};
|
|
656
|
+
} catch (error) {
|
|
657
|
+
ignoreStaleExtensionContextError(error);
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
621
660
|
}
|
|
622
661
|
|
|
623
662
|
function notify(ctx: { hasUI?: boolean; ui?: { notify?: (message: string, type?: "info" | "warning" | "error") => void } }, message: string, type: "info" | "warning" | "error" = "info"): void {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { loadPiToolsSuiteConfig } from "../config.js";
|
|
3
|
+
import { isAgentBusyRaceError } from "../context-usage.js";
|
|
3
4
|
import { autoClearCompletedTodos } from "./state/auto-clear.js";
|
|
4
5
|
import { loadPersistedPlan, syncPersistedPlan } from "./state/persistence.js";
|
|
5
6
|
import { replayFromBranch } from "./state/replay.js";
|
|
6
7
|
import { ACTIVE_STATUSES, isTaskBlocked, selectVisibleTasks } from "./state/selectors.js";
|
|
7
8
|
import { applyTaskMutation } from "./state/state-reducer.js";
|
|
8
9
|
import { getState, replaceState } from "./state/store.js";
|
|
9
|
-
import { DEFAULT_PROMPT_GUIDELINES, DEFAULT_PROMPT_SNIPPET, publishTodoState, registerTodosCommand, registerTodoTool } from "./todo.js";
|
|
10
|
+
import { activateTodoStateScope, DEFAULT_PROMPT_GUIDELINES, DEFAULT_PROMPT_SNIPPET, publishTodoState, registerTodosCommand, registerTodoTool } from "./todo.js";
|
|
10
11
|
import type { Task, TaskMutationParams } from "./tool/types.js";
|
|
11
12
|
|
|
12
13
|
type AgentMessageLike = { role?: unknown; stopReason?: unknown; content?: unknown };
|
|
@@ -17,6 +18,11 @@ const TODO_NUDGE_IDLE_RETRY_DELAY_MS = 100;
|
|
|
17
18
|
const TODO_NUDGE_MAX_IDLE_ATTEMPTS = 40;
|
|
18
19
|
const ASK_USER_TOOL_NAMES = new Set(["ask_user", "ask_user_question", "question"]);
|
|
19
20
|
const TODO_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
21
|
+
const TODO_THINKING_RESTORE_METADATA_KEY = "__piTodoRestoreThinking";
|
|
22
|
+
|
|
23
|
+
function isStaleExtensionContextError(error: unknown): boolean {
|
|
24
|
+
return error instanceof Error && /ctx is stale|stale ctx|stale after session replacement|stale after.*reload/i.test(error.message);
|
|
25
|
+
}
|
|
20
26
|
|
|
21
27
|
type TodoThinkingLevel = (typeof TODO_THINKING_LEVELS)[number];
|
|
22
28
|
type ModelLike = { reasoning?: boolean; thinkingLevelMap?: Partial<Record<TodoThinkingLevel, unknown | null>> };
|
|
@@ -108,7 +114,12 @@ function getPersistedPlanPrompt(path: string): string | undefined {
|
|
|
108
114
|
|
|
109
115
|
function emitPersistedPlanPrompt(pi: ExtensionAPI, ctx: ExtensionContext, prompt: string): void {
|
|
110
116
|
if (ctx.hasUI) {
|
|
111
|
-
|
|
117
|
+
try {
|
|
118
|
+
pi.sendUserMessage(prompt);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (isStaleExtensionContextError(error)) return;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
112
123
|
return;
|
|
113
124
|
}
|
|
114
125
|
console.log(prompt);
|
|
@@ -128,10 +139,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
128
139
|
const thinkingPrompt = todoThinkingEnabled ? buildThinkingPromptParts(currentModel) : {};
|
|
129
140
|
registerTodoTool(pi, {
|
|
130
141
|
...thinkingPrompt,
|
|
142
|
+
prepareMutation: (state, _ctx, info) => {
|
|
143
|
+
if (!todoThinkingEnabled) return info.params;
|
|
144
|
+
if (info.action === "update") return prepareTodoThinkingMutation(state, info.params);
|
|
145
|
+
if (info.action === "batch_update") {
|
|
146
|
+
return {
|
|
147
|
+
...info.params,
|
|
148
|
+
items: (info.params.items ?? []).map((item) => prepareTodoThinkingMutation(state, item)),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return info.params;
|
|
152
|
+
},
|
|
131
153
|
afterCommit: async (state, ctx, info) => {
|
|
132
154
|
if (todoThinkingEnabled) applyTodoThinkingAfterCommit(state, info);
|
|
133
155
|
try {
|
|
134
|
-
const sync = syncPersistedPlan(ctx.cwd,
|
|
156
|
+
const sync = syncPersistedPlan(ctx.cwd, info.committedState);
|
|
135
157
|
if (sync?.completed) console.log(`rpiv-todo: completed persisted plan and removed ${sync.path}`);
|
|
136
158
|
} catch (err) {
|
|
137
159
|
console.warn(`rpiv-todo: failed to sync persisted plan — ${(err as Error).message}`);
|
|
@@ -144,7 +166,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
144
166
|
const mutations = getTodoThinkingMutations(info.action, info.params);
|
|
145
167
|
for (const mutation of mutations) {
|
|
146
168
|
if (mutation.id === undefined || mutation.status === "in_progress") continue;
|
|
147
|
-
restoreTaskThinking(mutation.id);
|
|
169
|
+
restoreTaskThinking(mutation.id, state);
|
|
148
170
|
}
|
|
149
171
|
restoreInactiveTodoThinking(state);
|
|
150
172
|
for (const mutation of mutations) {
|
|
@@ -167,6 +189,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
167
189
|
return isTodoThinkingLevel(level) ? level : undefined;
|
|
168
190
|
}
|
|
169
191
|
|
|
192
|
+
function prepareTodoThinkingMutation(state: ReturnType<typeof getState>, params: TaskMutationParams): TaskMutationParams {
|
|
193
|
+
if (params.id === undefined) return params;
|
|
194
|
+
const current = state.tasks.find((task) => task.id === params.id);
|
|
195
|
+
if (!current) return params;
|
|
196
|
+
const nextStatus = params.status ?? current.status;
|
|
197
|
+
const nextThinking = params.thinking ?? current.thinking;
|
|
198
|
+
const shouldCapturePreviousThinking =
|
|
199
|
+
nextStatus === "in_progress" && nextThinking !== undefined && (current.status !== "in_progress" || params.thinking !== undefined);
|
|
200
|
+
if (!shouldCapturePreviousThinking) return params;
|
|
201
|
+
const currentThinking = getCurrentThinkingLevel();
|
|
202
|
+
if (!currentThinking) return params;
|
|
203
|
+
return {
|
|
204
|
+
...params,
|
|
205
|
+
metadata: {
|
|
206
|
+
...(params.metadata ?? {}),
|
|
207
|
+
[TODO_THINKING_RESTORE_METADATA_KEY]: currentThinking,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getRememberedThinking(taskId: number, state: ReturnType<typeof getState>): TodoThinkingLevel | undefined {
|
|
213
|
+
const remembered = rememberedThinkingByTaskId.get(taskId);
|
|
214
|
+
if (remembered) return remembered;
|
|
215
|
+
const task = state.tasks.find((item) => item.id === taskId);
|
|
216
|
+
const stored = task?.metadata?.[TODO_THINKING_RESTORE_METADATA_KEY];
|
|
217
|
+
return isTodoThinkingLevel(stored) ? stored : undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
170
220
|
function switchToTaskThinking(taskId: number, level: TodoThinkingLevel): void {
|
|
171
221
|
if (!getAvailableTodoThinkingLevels(currentModel).includes(level)) return;
|
|
172
222
|
const current = getCurrentThinkingLevel();
|
|
@@ -175,8 +225,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
175
225
|
if (current !== level) setTodoThinkingLevel(level);
|
|
176
226
|
}
|
|
177
227
|
|
|
178
|
-
function restoreTaskThinking(taskId: number): void {
|
|
179
|
-
const previous =
|
|
228
|
+
function restoreTaskThinking(taskId: number, state: ReturnType<typeof getState>): void {
|
|
229
|
+
const previous = getRememberedThinking(taskId, state);
|
|
180
230
|
if (!previous) return;
|
|
181
231
|
rememberedThinkingByTaskId.delete(taskId);
|
|
182
232
|
if (getCurrentThinkingLevel() !== previous) setTodoThinkingLevel(previous);
|
|
@@ -186,7 +236,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
186
236
|
for (const taskId of [...rememberedThinkingByTaskId.keys()]) {
|
|
187
237
|
const task = state.tasks.find((item) => item.id === taskId);
|
|
188
238
|
if (task?.status === "in_progress") continue;
|
|
189
|
-
restoreTaskThinking(taskId);
|
|
239
|
+
restoreTaskThinking(taskId, state);
|
|
190
240
|
}
|
|
191
241
|
}
|
|
192
242
|
|
|
@@ -197,13 +247,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
197
247
|
setter.call(pi, level);
|
|
198
248
|
}
|
|
199
249
|
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
250
|
+
function hasVisibleAssistantContent(content: unknown): boolean {
|
|
251
|
+
if (typeof content === "string") return content.trim().length > 0;
|
|
252
|
+
if (!Array.isArray(content)) return false;
|
|
253
|
+
return content.some((block) => {
|
|
254
|
+
const candidate = block as { type?: unknown; text?: unknown };
|
|
255
|
+
return candidate.type === "text" && typeof candidate.text === "string" && candidate.text.trim().length > 0;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function isCompletedAssistantReply(message: AgentMessageLike | undefined): boolean {
|
|
260
|
+
if (message?.role !== "assistant") return false;
|
|
261
|
+
if (message.stopReason === "aborted" || message.stopReason === "error" || message.stopReason === "length" || message.stopReason === "toolUse") return false;
|
|
262
|
+
return hasVisibleAssistantContent(message.content);
|
|
263
|
+
}
|
|
207
264
|
|
|
208
265
|
function hasCompletedAssistantReply(messages: readonly unknown[] | undefined): boolean {
|
|
209
266
|
if (!Array.isArray(messages)) return false;
|
|
@@ -227,6 +284,7 @@ function isCompletedAssistantReply(message: AgentMessageLike | undefined): boole
|
|
|
227
284
|
}
|
|
228
285
|
|
|
229
286
|
function applyInternalTodoMutation(action: "update", params: TaskMutationParams, ctx: ExtensionContext): boolean {
|
|
287
|
+
activateTodoStateScope(ctx);
|
|
230
288
|
const result = applyTaskMutation(getState(), action, params);
|
|
231
289
|
if (result.op.kind === "error") {
|
|
232
290
|
console.warn(`rpiv-todo: failed internal ${action} mutation — ${result.op.message}`);
|
|
@@ -235,7 +293,7 @@ function isCompletedAssistantReply(message: AgentMessageLike | undefined): boole
|
|
|
235
293
|
const autoClear = autoClearCompletedTodos(result.state);
|
|
236
294
|
replaceState(autoClear.state);
|
|
237
295
|
publishTodoState(pi as any, ctx, action, params as Record<string, unknown>);
|
|
238
|
-
if (todoThinkingEnabled) applyTodoThinkingAfterCommit(
|
|
296
|
+
if (todoThinkingEnabled) applyTodoThinkingAfterCommit(result.state, { action, params });
|
|
239
297
|
try {
|
|
240
298
|
const sync = syncPersistedPlan(ctx.cwd, autoClear.state);
|
|
241
299
|
if (sync?.completed) console.log(`rpiv-todo: completed persisted plan and removed ${sync.path}`);
|
|
@@ -246,6 +304,7 @@ function isCompletedAssistantReply(message: AgentMessageLike | undefined): boole
|
|
|
246
304
|
}
|
|
247
305
|
|
|
248
306
|
function maybeRecoverCompletedCurrentTask(messages: readonly unknown[] | undefined, ctx: ExtensionContext): boolean {
|
|
307
|
+
activateTodoStateScope(ctx);
|
|
249
308
|
if (!hasCompletedAssistantReply(messages)) return false;
|
|
250
309
|
const task = findOptimisticallyCompletableTask(getState().tasks);
|
|
251
310
|
if (!task) return false;
|
|
@@ -264,6 +323,7 @@ function isCompletedAssistantReply(message: AgentMessageLike | undefined): boole
|
|
|
264
323
|
nudgeTimer = setTimeout(() => {
|
|
265
324
|
nudgeTimer = undefined;
|
|
266
325
|
try {
|
|
326
|
+
activateTodoStateScope(ctx);
|
|
267
327
|
if (!ctx.isIdle()) {
|
|
268
328
|
if (attempt < TODO_NUDGE_MAX_IDLE_ATTEMPTS) scheduleTodoNudge(ctx, attempt + 1);
|
|
269
329
|
return;
|
|
@@ -286,6 +346,11 @@ function isCompletedAssistantReply(message: AgentMessageLike | undefined): boole
|
|
|
286
346
|
// queueing followUp from inside agent_end can be too late to be drained.
|
|
287
347
|
pi.sendUserMessage(nudge.message);
|
|
288
348
|
} catch (err) {
|
|
349
|
+
if (isAgentBusyRaceError(err)) {
|
|
350
|
+
if (attempt < TODO_NUDGE_MAX_IDLE_ATTEMPTS) scheduleTodoNudge(ctx, attempt + 1);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (isStaleExtensionContextError(err)) return;
|
|
289
354
|
console.warn(`rpiv-todo: failed to auto-nudge unfinished todos — ${(err as Error).message}`);
|
|
290
355
|
}
|
|
291
356
|
}, delayMs);
|
|
@@ -295,6 +360,7 @@ function isCompletedAssistantReply(message: AgentMessageLike | undefined): boole
|
|
|
295
360
|
registerTodosCommand(pi);
|
|
296
361
|
|
|
297
362
|
pi.on("session_start", async (_event, ctx) => {
|
|
363
|
+
activateTodoStateScope(ctx);
|
|
298
364
|
currentModel = ctx.model;
|
|
299
365
|
registerTodoToolWithCurrentPrompt();
|
|
300
366
|
const persisted = loadPersistedPlan(ctx.cwd);
|
|
@@ -313,12 +379,14 @@ function isCompletedAssistantReply(message: AgentMessageLike | undefined): boole
|
|
|
313
379
|
});
|
|
314
380
|
|
|
315
381
|
pi.on("session_compact", async (_event, ctx) => {
|
|
382
|
+
activateTodoStateScope(ctx);
|
|
316
383
|
replaceState(autoClearCompletedTodos(loadPersistedPlan(ctx.cwd)?.state ?? replayFromBranch(ctx)).state);
|
|
317
384
|
publishTodoState(pi as any, ctx);
|
|
318
385
|
lastNudgedSignature = undefined;
|
|
319
386
|
});
|
|
320
387
|
|
|
321
388
|
pi.on("session_tree", async (_event, ctx) => {
|
|
389
|
+
activateTodoStateScope(ctx);
|
|
322
390
|
replaceState(autoClearCompletedTodos(loadPersistedPlan(ctx.cwd)?.state ?? replayFromBranch(ctx)).state);
|
|
323
391
|
publishTodoState(pi as any, ctx);
|
|
324
392
|
lastNudgedSignature = undefined;
|
|
@@ -345,12 +413,14 @@ function isCompletedAssistantReply(message: AgentMessageLike | undefined): boole
|
|
|
345
413
|
if (isAskUserToolName(event.toolName)) pendingAskUserToolCallIds.delete(event.toolCallId);
|
|
346
414
|
});
|
|
347
415
|
|
|
348
|
-
pi.on("agent_start", async () => {
|
|
416
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
417
|
+
activateTodoStateScope(ctx);
|
|
349
418
|
pendingAskUserToolCallIds.clear();
|
|
350
419
|
inProgressAtAgentStart = new Set(selectVisibleTasks(getState()).filter((task) => task.status === "in_progress").map((task) => task.id));
|
|
351
420
|
});
|
|
352
421
|
|
|
353
422
|
pi.on("message_end", async (event, ctx) => {
|
|
423
|
+
activateTodoStateScope(ctx);
|
|
354
424
|
if (!isCompletedAssistantReply((event as { message?: AgentMessageLike } | undefined)?.message)) return;
|
|
355
425
|
if (maybeRecoverCompletedCurrentTask([(event as { message?: AgentMessageLike }).message], ctx)) {
|
|
356
426
|
lastNudgedSignature = undefined;
|
|
@@ -359,6 +429,7 @@ function isCompletedAssistantReply(message: AgentMessageLike | undefined): boole
|
|
|
359
429
|
});
|
|
360
430
|
|
|
361
431
|
pi.on("agent_end", async (event, ctx) => {
|
|
432
|
+
activateTodoStateScope(ctx);
|
|
362
433
|
const completedAssistantReply = hasCompletedAssistantReply((event as { messages?: readonly unknown[] } | undefined)?.messages);
|
|
363
434
|
|
|
364
435
|
if (suppressNextNudgeForThinkingSwitch) {
|
|
@@ -1,28 +1,57 @@
|
|
|
1
1
|
import type { Task } from "../tool/types.js";
|
|
2
2
|
import { EMPTY_STATE, type TaskState } from "./state.js";
|
|
3
3
|
|
|
4
|
+
const DEFAULT_SCOPE_KEY = "__default__";
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
|
-
* Module-level live state
|
|
6
|
-
*
|
|
7
|
-
* single
|
|
7
|
+
* Module-level live state cells, keyed by the active Pi session identity.
|
|
8
|
+
* Pi can keep multiple SDK runtimes/tabs alive in one Node process, so a
|
|
9
|
+
* single module-level todo cell leaks tasks across sessions. Keep the legacy
|
|
10
|
+
* getState()/commitState() API, but make it resolve through the current scope.
|
|
8
11
|
*/
|
|
9
|
-
let
|
|
12
|
+
let activeScopeKey = DEFAULT_SCOPE_KEY;
|
|
13
|
+
const statesByScopeKey = new Map<string, TaskState>([
|
|
14
|
+
[DEFAULT_SCOPE_KEY, { tasks: [...EMPTY_STATE.tasks], nextId: EMPTY_STATE.nextId }],
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function emptyState(): TaskState {
|
|
18
|
+
return { tasks: [...EMPTY_STATE.tasks], nextId: EMPTY_STATE.nextId };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizedScopeKey(scopeKey: string | undefined): string {
|
|
22
|
+
const trimmed = scopeKey?.trim();
|
|
23
|
+
return trimmed ? trimmed : DEFAULT_SCOPE_KEY;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function activeState(): TaskState {
|
|
27
|
+
let state = statesByScopeKey.get(activeScopeKey);
|
|
28
|
+
if (!state) {
|
|
29
|
+
state = emptyState();
|
|
30
|
+
statesByScopeKey.set(activeScopeKey, state);
|
|
31
|
+
}
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function activateStateScope(scopeKey: string | undefined): void {
|
|
36
|
+
activeScopeKey = normalizedScopeKey(scopeKey);
|
|
37
|
+
activeState();
|
|
38
|
+
}
|
|
10
39
|
|
|
11
40
|
/**
|
|
12
41
|
* Live tasks accessor. Returned `readonly Task[]` so callers cannot mutate the
|
|
13
42
|
* live cell. Consumers must not cast back.
|
|
14
43
|
*/
|
|
15
44
|
export function getTodos(): readonly Task[] {
|
|
16
|
-
return
|
|
45
|
+
return activeState().tasks;
|
|
17
46
|
}
|
|
18
47
|
|
|
19
48
|
export function getNextId(): number {
|
|
20
|
-
return
|
|
49
|
+
return activeState().nextId;
|
|
21
50
|
}
|
|
22
51
|
|
|
23
52
|
/** Snapshot accessor used by reducer callers to pass canonical state in. */
|
|
24
53
|
export function getState(): TaskState {
|
|
25
|
-
return
|
|
54
|
+
return activeState();
|
|
26
55
|
}
|
|
27
56
|
|
|
28
57
|
/**
|
|
@@ -31,7 +60,7 @@ export function getState(): TaskState {
|
|
|
31
60
|
* `replayFromBranch` decodes the latest snapshot.
|
|
32
61
|
*/
|
|
33
62
|
export function replaceState(next: TaskState): void {
|
|
34
|
-
|
|
63
|
+
statesByScopeKey.set(activeScopeKey, next);
|
|
35
64
|
}
|
|
36
65
|
|
|
37
66
|
/**
|
|
@@ -39,7 +68,7 @@ export function replaceState(next: TaskState): void {
|
|
|
39
68
|
* `state` output to publish the new canonical state to live readers.
|
|
40
69
|
*/
|
|
41
70
|
export function commitState(next: TaskState): void {
|
|
42
|
-
|
|
71
|
+
statesByScopeKey.set(activeScopeKey, next);
|
|
43
72
|
}
|
|
44
73
|
|
|
45
74
|
/**
|
|
@@ -48,5 +77,7 @@ export function commitState(next: TaskState): void {
|
|
|
48
77
|
* Plan §Decisions §Decision 7.
|
|
49
78
|
*/
|
|
50
79
|
export function __resetState(): void {
|
|
51
|
-
|
|
80
|
+
activeScopeKey = DEFAULT_SCOPE_KEY;
|
|
81
|
+
statesByScopeKey.clear();
|
|
82
|
+
statesByScopeKey.set(DEFAULT_SCOPE_KEY, emptyState());
|
|
52
83
|
}
|