pi-ui-extend 1.0.24 → 1.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +5 -1
  2. package/dist/app/app.d.ts +1 -0
  3. package/dist/app/app.js +12 -0
  4. package/dist/app/icons.d.ts +1 -0
  5. package/dist/app/icons.js +2 -0
  6. package/dist/app/input/input-controller.d.ts +2 -0
  7. package/dist/app/input/input-controller.js +8 -5
  8. package/dist/app/rendering/render-controller.js +2 -0
  9. package/dist/app/rendering/status-line-renderer.d.ts +4 -1
  10. package/dist/app/rendering/status-line-renderer.js +12 -0
  11. package/dist/app/screen/mouse-controller.d.ts +4 -1
  12. package/dist/app/screen/mouse-controller.js +9 -0
  13. package/dist/app/session/agent-pause-controller.d.ts +30 -0
  14. package/dist/app/session/agent-pause-controller.js +178 -0
  15. package/dist/app/session/session-event-controller.d.ts +1 -0
  16. package/dist/app/session/session-event-controller.js +22 -15
  17. package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
  18. package/dist/app/session/session-lifecycle-controller.js +1 -0
  19. package/dist/app/types.d.ts +10 -0
  20. package/dist/config.js +1 -1
  21. package/dist/default-pix-config.js +2 -2
  22. package/external/pi-tools-suite/README.md +1 -1
  23. package/external/pi-tools-suite/docs/browser-qa-subagent.md +34 -2
  24. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +24 -18
  25. package/external/pi-tools-suite/src/async-subagents/core/config.ts +14 -3
  26. package/external/pi-tools-suite/src/async-subagents/core/process.ts +19 -0
  27. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +97 -17
  28. package/external/pi-tools-suite/src/async-subagents/core/stop.ts +4 -2
  29. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/SKILL.md +108 -13
  30. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +80 -0
  31. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-flow.example.jsonc +54 -1
  32. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/scripts/browser-qa-runner.mjs +952 -76
  33. package/external/pi-tools-suite/src/dcp/index.ts +21 -0
  34. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +92 -0
  35. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +21 -16
  36. package/external/pi-tools-suite/src/todo/index.ts +86 -46
  37. package/external/pi-tools-suite/src/todo/todo.ts +1 -1
  38. package/package.json +1 -1
@@ -63,6 +63,7 @@ import {
63
63
  collectProviderToolResultEvidence,
64
64
  providerPayloadIncludesToolResult,
65
65
  } from "./provider-tool-results.js"
66
+ import { reconcileInheritedCompressionBlocks } from "./pruner-compression-blocks.js"
66
67
 
67
68
  // ---------------------------------------------------------------------------
68
69
  // Helpers
@@ -232,6 +233,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
232
233
 
233
234
  // ── 2. Create state ───────────────────────────────────────────────────────
234
235
  const state = createState()
236
+ let pendingInheritedBlockReconciliation = false
235
237
  const pendingProviderToolIds = new Set<string>()
236
238
  const appendNudgeTelemetry = (
237
239
  event: "emitted" | "upgraded" | "reapplied",
@@ -275,6 +277,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
275
277
  pi.on("session_start", async (event, ctx) => {
276
278
  // Reset to a clean slate first.
277
279
  resetState(state)
280
+ pendingInheritedBlockReconciliation = false
278
281
 
279
282
  // Reset dedup hash before loading the sidecar state for this session.
280
283
  resetDcpPersistenceDedup()
@@ -290,6 +293,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
290
293
  // Cleanup is opportunistic; stale sidecars must not block session startup.
291
294
  })
292
295
  restoreState(state, await loadDcpState(ctx))
296
+ pendingInheritedBlockReconciliation = state.compressionBlocks.length > 0
293
297
 
294
298
  // fork/resume/new sessions inherit the source conversation but get a fresh
295
299
  // sidecar; inherit the previous session's compression blocks so they are
@@ -299,6 +303,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
299
303
  const inherited = await loadDcpStateFromSessionFile(event.previousSessionFile)
300
304
  const added = inheritCompressionBlocks(state, inherited)
301
305
  if (added > 0) {
306
+ pendingInheritedBlockReconciliation = true
302
307
  writeDcpDebugLog(configForContext(ctx), "session_start.inherited_blocks", {
303
308
  reason: event.reason,
304
309
  previousSessionFile: event.previousSessionFile,
@@ -433,6 +438,22 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
433
438
  return { messages: contextMessages }
434
439
  }
435
440
  annotateMessagesWithBranchEntryIds(contextMessages, ctx)
441
+ if (pendingInheritedBlockReconciliation) {
442
+ pendingInheritedBlockReconciliation = false
443
+ const reconciliation = reconcileInheritedCompressionBlocks(contextMessages, state)
444
+ writeDcpDebugLog(
445
+ effectiveConfig,
446
+ "context.reconciled_inherited_blocks",
447
+ { ...reconciliation },
448
+ ctx,
449
+ )
450
+ if (
451
+ reconciliation.activatedBlockIds.length > 0 ||
452
+ reconciliation.deactivatedBlockIds.length > 0
453
+ ) {
454
+ await saveDcpState(ctx, state)
455
+ }
456
+ }
436
457
  let prunedMessages = applyPruning(contextMessages, state, effectiveConfig)
437
458
  let candidate = null as ReturnType<typeof detectCompressionCandidate>
438
459
  let messageCandidates = [] as ReturnType<typeof detectMessageCompressionCandidates>
@@ -13,6 +13,98 @@ function findBoundaryIndex(messages: any[], stableId: string | undefined, timest
13
13
  return messages.findIndex((m, index) => messageMatchesBoundary(m, index, stableId, timestamp));
14
14
  }
15
15
 
16
+ export interface ReconcileInheritedBlocksResult {
17
+ fittingBlockIds: number[];
18
+ activatedBlockIds: number[];
19
+ deactivatedBlockIds: number[];
20
+ }
21
+
22
+ /**
23
+ * Reconcile inherited compression state with the branch that is actually
24
+ * being sent to the provider.
25
+ *
26
+ * A fork can branch before the source session's newest roll-up block ends.
27
+ * That roll-up remains active in the source sidecar, while the older block it
28
+ * covered is inactive. Since the roll-up's end boundary is absent on the
29
+ * fork, neither block would otherwise be applied and the whole history would
30
+ * be sent raw. Select the widest fitting block(s) instead, while preserving an
31
+ * explicit user decompression for the block and its covered descendants.
32
+ */
33
+ export function reconcileInheritedCompressionBlocks(
34
+ messages: any[],
35
+ state: DcpState,
36
+ ): ReconcileInheritedBlocksResult {
37
+ const byId = new Map(state.compressionBlocks.map((block) => [block.id, block]));
38
+ const fittingBlockIds = new Set<number>();
39
+ const descendantCache = new Map<number, Set<number>>();
40
+
41
+ for (const block of state.compressionBlocks) {
42
+ if (!Number.isFinite(block.startTimestamp) || !Number.isFinite(block.endTimestamp)) continue;
43
+ const startFound = findBoundaryIndex(messages, block.startMessageId, block.startTimestamp) !== -1;
44
+ const endFound = findBoundaryIndex(messages, block.endMessageId, block.endTimestamp) !== -1;
45
+ if (startFound && endFound) fittingBlockIds.add(block.id);
46
+ }
47
+
48
+ function descendantsOf(rootId: number): Set<number> {
49
+ const cached = descendantCache.get(rootId);
50
+ if (cached) return cached;
51
+
52
+ const descendants = new Set<number>();
53
+ const pending = [...(byId.get(rootId)?.coveredBlockIds ?? [])];
54
+ while (pending.length > 0) {
55
+ const id = pending.pop()!;
56
+ if (id === rootId || descendants.has(id)) continue;
57
+ descendants.add(id);
58
+ pending.push(...(byId.get(id)?.coveredBlockIds ?? []));
59
+ }
60
+ descendantCache.set(rootId, descendants);
61
+ return descendants;
62
+ }
63
+
64
+ // Decompressing a roll-up intentionally exposes its whole range. Do not
65
+ // unexpectedly reactivate one of its older nested summaries after a fork.
66
+ const suppressedByUser = new Set<number>();
67
+ for (const block of state.compressionBlocks) {
68
+ if (!block.deactivatedByUser) continue;
69
+ suppressedByUser.add(block.id);
70
+ for (const id of descendantsOf(block.id)) suppressedByUser.add(id);
71
+ }
72
+
73
+ const eligibleBlockIds = new Set(
74
+ [...fittingBlockIds].filter((id) => !suppressedByUser.has(id)),
75
+ );
76
+ const coveredByFittingBlock = new Set<number>();
77
+ for (const id of eligibleBlockIds) {
78
+ for (const coveredId of descendantsOf(id)) coveredByFittingBlock.add(coveredId);
79
+ }
80
+
81
+ const activeBlockIds = new Set(
82
+ [...eligibleBlockIds].filter((id) => !coveredByFittingBlock.has(id)),
83
+ );
84
+ const activatedBlockIds: number[] = [];
85
+ const deactivatedBlockIds: number[] = [];
86
+
87
+ for (const block of state.compressionBlocks) {
88
+ const shouldBeActive = activeBlockIds.has(block.id);
89
+ if (block.active !== shouldBeActive) {
90
+ if (shouldBeActive) activatedBlockIds.push(block.id);
91
+ else deactivatedBlockIds.push(block.id);
92
+ }
93
+ block.active = shouldBeActive;
94
+
95
+ if (block.deactivatedByUser) continue;
96
+ block.deactivatedReason = fittingBlockIds.has(block.id)
97
+ ? undefined
98
+ : "outside-inherited-branch";
99
+ }
100
+
101
+ return {
102
+ fittingBlockIds: [...fittingBlockIds].sort((a, b) => a - b),
103
+ activatedBlockIds: activatedBlockIds.sort((a, b) => a - b),
104
+ deactivatedBlockIds: deactivatedBlockIds.sort((a, b) => a - b),
105
+ };
106
+ }
107
+
16
108
 
17
109
  export function syncCompressionBlocks(messages: any[], state: DcpState, config: DcpConfig): void {
18
110
  if (state.compressionBlocks.length === 0) return;
@@ -23,7 +23,7 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
23
23
  },
24
24
  // Vision-capable model used by the coding-discipline lookup tool for blind-model
25
25
  // screenshot/image questions. Remove or set to null to disable lookup.
26
- "lookupModel": "openai-codex/gpt-5.4-mini",
26
+ "lookupModel": "openai-codex/gpt-5.6-luna",
27
27
  // coding-discipline working-state strictness.
28
28
  // "lenient" (default): batch independent read-only tool calls; brief reasoning
29
29
  // text is acceptable when thinking is off. Text only counts as chatter when a
@@ -148,7 +148,7 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
148
148
  },
149
149
  "asyncSubagents": {
150
150
  "defaultType": "quick",
151
- "routing": { "enabled": true, "model": "zai/glm-4.5-air", "maxTaskChars": 1200, "maxTokens": 512, "maxRetries": 1, "timeoutMs": 12000, "debug": false },
151
+ "routing": { "enabled": true, "model": "zai/glm-4.5-air", "fallbackModels": ["openai-codex/gpt-5.6-luna"], "maxTaskChars": 1200, "maxTokens": 512, "maxRetries": 1, "timeoutMs": 12000, "debug": false },
152
152
  "presets": {
153
153
  "cheap": {
154
154
  "description": "Use cheap GLM/Gemini Flash models for text/code roles.",
@@ -222,33 +222,33 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
222
222
  "description": "Use only enabled strong models, mixing GPT, Claude, and Gemini by role.",
223
223
  "types": {
224
224
  "quick": {
225
- "model": "openai-codex/gpt-5.4-mini",
225
+ "model": "openai-codex/gpt-5.6-luna",
226
226
  "fallbackModels": ["zai/glm-4.5-air"],
227
227
  "thinking": "low"
228
228
  },
229
229
  "scan": {
230
230
  "model": "antigravity/gemini-3-flash-preview",
231
- "fallbackModels": ["openai-codex/gpt-5.3-codex-spark", "zai/glm-4.5-air"],
231
+ "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-4.5-air"],
232
232
  "thinking": "off"
233
233
  },
234
234
  "research": {
235
235
  "model": "antigravity/gemini-3.1-pro-preview",
236
- "fallbackModels": ["openai-codex/gpt-5.4-mini", "zai/glm-5-turbo"],
236
+ "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-5-turbo"],
237
237
  "thinking": "medium"
238
238
  },
239
239
  "docs": {
240
240
  "model": "antigravity/gemini-2.5-flash",
241
- "fallbackModels": ["openai-codex/gpt-5.3-codex-spark", "zai/glm-4.5-air"],
241
+ "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-4.5-air"],
242
242
  "thinking": "medium"
243
243
  },
244
244
  "frontend": {
245
245
  "model": "antigravity/gemini-3.1-pro-preview-customtools",
246
- "fallbackModels": ["zai/glm-5.3"],
246
+ "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-5.3"],
247
247
  "thinking": "low"
248
248
  },
249
249
  "tests": {
250
250
  "model": "antigravity/antigravity-claude-sonnet-4-6",
251
- "fallbackModels": ["openai-codex/gpt-5.4-mini", "zai/glm-5-turbo"],
251
+ "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-5-turbo"],
252
252
  "thinking": "high"
253
253
  },
254
254
  "review": {
@@ -272,26 +272,29 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
272
272
  "types": {
273
273
  "quick": {
274
274
  "description": "Use for tiny cheap tasks: answer a simple question, inspect one known file, or verify one fact. Not for broad repo search.",
275
- "model": "zai/glm-5-turbo",
275
+ "model": "openai-codex/gpt-5.6-luna",
276
+ "fallbackModels": ["zai/glm-4.5-air"],
276
277
  "thinking": "off"
277
278
  },
278
279
  "scan": {
279
280
  "description": "Use for finding files, symbols, text, or inventory across a repo. Return paths/facts; do not judge code quality.",
280
- "model": "zai/glm-5-turbo",
281
+ "model": "openai-codex/gpt-5.6-luna",
282
+ "fallbackModels": ["zai/glm-4.5-air"],
281
283
  "thinking": "off",
282
284
  "tools": ["read", "grep"]
283
285
  },
284
286
  "research": {
285
287
  "description": "Use for multi-file codebase research: read several files and explain how something works. No edits.",
286
- "model": "zai/glm-5-turbo",
288
+ "model": "openai-codex/gpt-5.6-terra",
289
+ "fallbackModels": ["zai/glm-5-turbo"],
287
290
  "thinking": "low",
288
291
  "tools": ["read", "grep"]
289
292
  },
290
- "docs": { "description": "Use for documentation work: README/API docs review, docs gaps, changelog, migration notes, examples.", "model": "zai/glm-5-turbo", "thinking": "low" },
293
+ "docs": { "description": "Use for documentation work: README/API docs review, docs gaps, changelog, migration notes, examples.", "model": "openai-codex/gpt-5.6-luna", "fallbackModels": ["zai/glm-4.5-air"], "thinking": "low" },
291
294
  "frontend": {
292
295
  "description": "Use for frontend UI/UX visual work: styling, layout, typography, animation, responsive states, component polish, accessibility. Avoid backend/business logic unless needed for UI behavior.",
293
296
  "model": "antigravity/gemini-3-flash-preview",
294
- "fallbackModels": ["openai-codex/gpt-5.4-mini", "zai/glm-5.3"],
297
+ "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-5.3"],
295
298
  "thinking": "medium",
296
299
  "promptAppend": [
297
300
  "Act as a frontend UI/UX engineer for visual and product-facing work.",
@@ -302,14 +305,16 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
302
305
  },
303
306
  "browser-qa": {
304
307
  "description": "Use for browser-based visual QA: reproduce UI bugs and verify fixes with deterministic assertions, screenshots, video, and traces.",
305
- "model": "openai-codex/gpt-5.4-mini",
308
+ "model": "openai-codex/gpt-5.6-luna",
306
309
  "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"],
307
- "thinking": "medium",
310
+ "thinking": "low",
311
+ "timeoutMs": 120000,
308
312
  "tools": ["read", "grep", "bash"]
309
313
  },
310
314
  "tests": {
311
315
  "description": "Use for tests: locate coverage, find gaps, run/check targeted test commands, diagnose failing tests.",
312
- "model": "zai/glm-5-turbo",
316
+ "model": "openai-codex/gpt-5.6-terra",
317
+ "fallbackModels": ["zai/glm-5-turbo"],
313
318
  "thinking": "medium",
314
319
  "tools": ["read", "grep", "bash"]
315
320
  },
@@ -7,7 +7,15 @@ import { replayFromBranch } from "./state/replay.js";
7
7
  import { ACTIVE_STATUSES, isTaskBlocked, selectVisibleTasks } from "./state/selectors.js";
8
8
  import { applyTaskMutation } from "./state/state-reducer.js";
9
9
  import { getState, replaceState } from "./state/store.js";
10
- import { activateTodoStateScope, DEFAULT_PROMPT_GUIDELINES, DEFAULT_PROMPT_SNIPPET, publishTodoState, registerTodosCommand, registerTodoTool } from "./todo.js";
10
+ import {
11
+ activateTodoStateScope,
12
+ DEFAULT_PROMPT_GUIDELINES,
13
+ DEFAULT_PROMPT_SNIPPET,
14
+ publishTodoState,
15
+ registerTodosCommand,
16
+ registerTodoTool,
17
+ todoStateScopeFromContext,
18
+ } from "./todo.js";
11
19
  import {
12
20
  TODO_THINKING_LEVEL_VALUES,
13
21
  todoParamsSchemaForThinkingLevels,
@@ -216,7 +224,7 @@ export default function (pi: ExtensionAPI) {
216
224
  const todoConfig = loadPiToolsSuiteConfig(["todo"]);
217
225
  const todoThinkingEnabled = todoConfig.todoThinking;
218
226
  const todoThinkingOverrides = todoConfig.todoThinkingOverrides;
219
- const rememberedThinkingByTaskId = new Map<number, TodoThinkingLevel>();
227
+ const baselineThinkingByScope = new Map<string, TodoThinkingLevel>();
220
228
  let lastNudgedSignature: string | undefined;
221
229
  let nudgeTimer: ReturnType<typeof setTimeout> | undefined;
222
230
  const pendingAskUserToolCallIds = new Set<string>();
@@ -233,19 +241,19 @@ export default function (pi: ExtensionAPI) {
233
241
  registerTodoTool(pi, {
234
242
  ...thinkingPrompt,
235
243
  ...(availableThinkingLevels ? { parameters: todoParamsSchemaForThinkingLevels(availableThinkingLevels) } : {}),
236
- prepareMutation: (state, _ctx, info) => {
244
+ prepareMutation: (state, ctx, info) => {
237
245
  if (!todoThinkingEnabled) return info.params;
238
- if (info.action === "create" || info.action === "update") return prepareTodoThinkingMutation(state, info.params);
246
+ if (info.action === "create" || info.action === "update") return prepareTodoThinkingMutation(state, ctx, info.params);
239
247
  if (info.action === "batch_create" || info.action === "batch_update") {
240
248
  return {
241
249
  ...info.params,
242
- items: (info.params.items ?? []).map((item) => prepareTodoThinkingMutation(state, item)),
250
+ items: (info.params.items ?? []).map((item) => prepareTodoThinkingMutation(state, ctx, item)),
243
251
  };
244
252
  }
245
253
  return info.params;
246
254
  },
247
255
  afterCommit: async (state, ctx, info) => {
248
- if (todoThinkingEnabled) applyTodoThinkingAfterCommit(state, info);
256
+ if (todoThinkingEnabled) applyTodoThinkingAfterCommit(state, ctx, info);
249
257
  try {
250
258
  const sync = syncPersistedPlan(ctx.cwd, info.committedState);
251
259
  if (sync?.completed) console.log(`rpiv-todo: completed persisted plan and removed ${sync.path}`);
@@ -256,20 +264,27 @@ export default function (pi: ExtensionAPI) {
256
264
  });
257
265
  }
258
266
 
259
- function applyTodoThinkingAfterCommit(state: ReturnType<typeof getState>, info: { action: string; params: TaskMutationParams }): void {
267
+ function applyTodoThinkingAfterCommit(
268
+ state: ReturnType<typeof getState>,
269
+ ctx: ExtensionContext,
270
+ info: { action: string; params: TaskMutationParams },
271
+ ): void {
260
272
  const mutations = getTodoThinkingMutations(info.action, info.params);
261
- for (const mutation of mutations) {
262
- if (mutation.id === undefined || mutation.status === "in_progress") continue;
263
- restoreTaskThinking(mutation.id, state);
264
- }
265
- restoreInactiveTodoThinking(state);
266
- for (const mutation of mutations) {
267
- if (mutation.id === undefined) continue;
268
- const task = state.tasks.find((item) => item.id === mutation.id);
269
- if (!task || task.status !== "in_progress" || !task.thinking) continue;
270
- if (mutation.status !== "in_progress" && mutation.thinking === undefined) continue;
271
- switchToTaskThinking(task.id, task.thinking);
273
+ const activeTasks = state.tasks.filter((task) => task.status === "in_progress" && task.thinking !== undefined);
274
+ if (activeTasks.length === 0) {
275
+ restoreTodoThinkingBaseline(state, ctx);
276
+ return;
272
277
  }
278
+
279
+ rememberTodoThinkingBaseline(state, ctx);
280
+ const explicitlyActivated = [...mutations].reverse().find((mutation) => {
281
+ if (mutation.id === undefined || (mutation.status !== "in_progress" && mutation.thinking === undefined)) return false;
282
+ return activeTasks.some((task) => task.id === mutation.id);
283
+ });
284
+ let target: Task | undefined;
285
+ if (explicitlyActivated) target = activeTasks.find((task) => task.id === explicitlyActivated.id);
286
+ else if (activeTasks.length === 1) [target] = activeTasks;
287
+ if (target?.thinking) switchToTaskThinking(target.thinking);
273
288
  }
274
289
 
275
290
  function getTodoThinkingMutations(action: string, params: TaskMutationParams): TaskMutationParams[] {
@@ -283,7 +298,43 @@ export default function (pi: ExtensionAPI) {
283
298
  return isTodoThinkingLevel(level) ? level : undefined;
284
299
  }
285
300
 
286
- function prepareTodoThinkingMutation(state: ReturnType<typeof getState>, params: TaskMutationParams): TaskMutationParams {
301
+ function thinkingScopeKey(ctx: ExtensionContext): string {
302
+ return todoStateScopeFromContext(ctx) ?? "__default__";
303
+ }
304
+
305
+ function storedRestoreThinking(task: Task): TodoThinkingLevel | undefined {
306
+ const stored = task.metadata?.[TODO_THINKING_RESTORE_METADATA_KEY];
307
+ return isTodoThinkingLevel(stored) ? stored : undefined;
308
+ }
309
+
310
+ function storedTodoThinkingBaseline(state: ReturnType<typeof getState>, activeOnly: boolean): TodoThinkingLevel | undefined {
311
+ for (const task of state.tasks) {
312
+ if (activeOnly && task.status !== "in_progress") continue;
313
+ const stored = storedRestoreThinking(task);
314
+ if (stored) return stored;
315
+ }
316
+ return undefined;
317
+ }
318
+
319
+ function getTodoThinkingBaseline(state: ReturnType<typeof getState>, ctx: ExtensionContext): TodoThinkingLevel | undefined {
320
+ return baselineThinkingByScope.get(thinkingScopeKey(ctx)) ?? storedTodoThinkingBaseline(state, true);
321
+ }
322
+
323
+ function rememberTodoThinkingBaseline(state: ReturnType<typeof getState>, ctx: ExtensionContext): void {
324
+ const scopeKey = thinkingScopeKey(ctx);
325
+ if (baselineThinkingByScope.has(scopeKey)) return;
326
+ const baseline = storedTodoThinkingBaseline(state, true);
327
+ if (baseline) baselineThinkingByScope.set(scopeKey, baseline);
328
+ }
329
+
330
+ function syncTodoThinkingBaseline(state: ReturnType<typeof getState>, ctx: ExtensionContext): void {
331
+ const scopeKey = thinkingScopeKey(ctx);
332
+ const baseline = storedTodoThinkingBaseline(state, true);
333
+ if (baseline) baselineThinkingByScope.set(scopeKey, baseline);
334
+ else baselineThinkingByScope.delete(scopeKey);
335
+ }
336
+
337
+ function prepareTodoThinkingMutation(state: ReturnType<typeof getState>, ctx: ExtensionContext, params: TaskMutationParams): TaskMutationParams {
287
338
  let nextParams = params;
288
339
  const configuredOverride = resolveTodoThinkingOverride(currentModel, todoThinkingOverrides);
289
340
  if (configuredOverride !== undefined) {
@@ -305,49 +356,33 @@ export default function (pi: ExtensionAPI) {
305
356
  const shouldCapturePreviousThinking =
306
357
  nextStatus === "in_progress" && nextThinking !== undefined && (current.status !== "in_progress" || nextParams.thinking !== undefined);
307
358
  if (!shouldCapturePreviousThinking) return nextParams;
308
- const currentThinking = getCurrentThinkingLevel();
309
- if (!currentThinking) return nextParams;
359
+ const baselineThinking = getTodoThinkingBaseline(state, ctx) ?? getCurrentThinkingLevel();
360
+ if (!baselineThinking) return nextParams;
310
361
  return {
311
362
  ...nextParams,
312
363
  metadata: {
313
364
  ...(nextParams.metadata ?? {}),
314
- [TODO_THINKING_RESTORE_METADATA_KEY]: currentThinking,
365
+ [TODO_THINKING_RESTORE_METADATA_KEY]: baselineThinking,
315
366
  },
316
367
  };
317
368
  }
318
369
 
319
- function getRememberedThinking(taskId: number, state: ReturnType<typeof getState>): TodoThinkingLevel | undefined {
320
- const remembered = rememberedThinkingByTaskId.get(taskId);
321
- if (remembered) return remembered;
322
- const task = state.tasks.find((item) => item.id === taskId);
323
- const stored = task?.metadata?.[TODO_THINKING_RESTORE_METADATA_KEY];
324
- return isTodoThinkingLevel(stored) ? stored : undefined;
325
- }
326
-
327
- function switchToTaskThinking(taskId: number, level: TodoThinkingLevel): void {
370
+ function switchToTaskThinking(level: TodoThinkingLevel): void {
328
371
  if (!getAvailableTodoThinkingLevels(currentModel).includes(level)) return;
329
372
  const current = getCurrentThinkingLevel();
330
373
  if (!current) return;
331
- if (!rememberedThinkingByTaskId.has(taskId)) rememberedThinkingByTaskId.set(taskId, current);
332
374
  if (current !== level) setTodoThinkingLevel(level);
333
375
  }
334
376
 
335
- function restoreTaskThinking(taskId: number, state: ReturnType<typeof getState>): void {
336
- const previous = getRememberedThinking(taskId, state);
377
+ function restoreTodoThinkingBaseline(state: ReturnType<typeof getState>, ctx: ExtensionContext): void {
378
+ const scopeKey = thinkingScopeKey(ctx);
379
+ const previous = baselineThinkingByScope.get(scopeKey) ?? storedTodoThinkingBaseline(state, false);
337
380
  if (!previous) return;
338
- rememberedThinkingByTaskId.delete(taskId);
381
+ baselineThinkingByScope.delete(scopeKey);
339
382
  const restored = normalizeTodoThinkingLevelForModel(currentModel, previous);
340
383
  if (getCurrentThinkingLevel() !== restored) setTodoThinkingLevel(restored);
341
384
  }
342
385
 
343
- function restoreInactiveTodoThinking(state: ReturnType<typeof getState>): void {
344
- for (const taskId of [...rememberedThinkingByTaskId.keys()]) {
345
- const task = state.tasks.find((item) => item.id === taskId);
346
- if (task?.status === "in_progress") continue;
347
- restoreTaskThinking(taskId, state);
348
- }
349
- }
350
-
351
386
  function setTodoThinkingLevel(level: TodoThinkingLevel): void {
352
387
  const setter = (pi as { setThinkingLevel?: (level: TodoThinkingLevel) => void }).setThinkingLevel;
353
388
  if (!setter) return;
@@ -401,7 +436,7 @@ export default function (pi: ExtensionAPI) {
401
436
  const autoClear = autoClearCompletedTodos(result.state);
402
437
  replaceState(autoClear.state);
403
438
  publishTodoState(pi as any, ctx, action, params as Record<string, unknown>);
404
- if (todoThinkingEnabled) applyTodoThinkingAfterCommit(result.state, { action, params });
439
+ if (todoThinkingEnabled) applyTodoThinkingAfterCommit(result.state, ctx, { action, params });
405
440
  try {
406
441
  const sync = syncPersistedPlan(ctx.cwd, autoClear.state);
407
442
  if (sync?.completed) console.log(`rpiv-todo: completed persisted plan and removed ${sync.path}`);
@@ -473,6 +508,7 @@ export default function (pi: ExtensionAPI) {
473
508
  const persisted = loadPersistedPlan(ctx.cwd);
474
509
  const loaded = autoClearCompletedTodos(persisted?.state ?? replayFromBranch(ctx));
475
510
  replaceState(loaded.state);
511
+ if (todoThinkingEnabled) syncTodoThinkingBaseline(loaded.state, ctx);
476
512
  publishTodoState(pi as any, ctx);
477
513
  if (persisted && loaded.cleared) syncPersistedPlan(ctx.cwd, loaded.state);
478
514
  lastNudgedSignature = undefined;
@@ -487,14 +523,18 @@ export default function (pi: ExtensionAPI) {
487
523
 
488
524
  pi.on("session_compact", async (_event, ctx) => {
489
525
  activateTodoStateScope(ctx);
490
- replaceState(autoClearCompletedTodos(loadPersistedPlan(ctx.cwd)?.state ?? replayFromBranch(ctx)).state);
526
+ const state = autoClearCompletedTodos(loadPersistedPlan(ctx.cwd)?.state ?? replayFromBranch(ctx)).state;
527
+ replaceState(state);
528
+ if (todoThinkingEnabled) syncTodoThinkingBaseline(state, ctx);
491
529
  publishTodoState(pi as any, ctx);
492
530
  lastNudgedSignature = undefined;
493
531
  });
494
532
 
495
533
  pi.on("session_tree", async (_event, ctx) => {
496
534
  activateTodoStateScope(ctx);
497
- replaceState(autoClearCompletedTodos(loadPersistedPlan(ctx.cwd)?.state ?? replayFromBranch(ctx)).state);
535
+ const state = autoClearCompletedTodos(loadPersistedPlan(ctx.cwd)?.state ?? replayFromBranch(ctx)).state;
536
+ replaceState(state);
537
+ if (todoThinkingEnabled) syncTodoThinkingBaseline(state, ctx);
498
538
  publishTodoState(pi as any, ctx);
499
539
  lastNudgedSignature = undefined;
500
540
  });
@@ -202,7 +202,7 @@ function sessionIdFromContext(ctx: unknown): string | undefined {
202
202
  return typeof sessionId === "string" && sessionId.trim() ? sessionId : undefined;
203
203
  }
204
204
 
205
- function todoStateScopeFromContext(ctx: unknown): string | undefined {
205
+ export function todoStateScopeFromContext(ctx: unknown): string | undefined {
206
206
  const sessionFile = sessionFileFromContext(ctx);
207
207
  if (sessionFile) return `file:${resolve(sessionFile)}`;
208
208
  const sessionId = sessionIdFromContext(ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "1.0.24",
3
+ "version": "1.0.28",
4
4
  "description": "Pix: a workspace-first terminal UI for Pi with tabs, readable tool activity, voice input, and bundled agent tools.",
5
5
  "private": false,
6
6
  "repository": {