pi-ui-extend 1.0.24 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/app/app.d.ts +1 -0
- package/dist/app/app.js +12 -0
- package/dist/app/icons.d.ts +1 -0
- package/dist/app/icons.js +2 -0
- package/dist/app/input/input-controller.d.ts +2 -0
- package/dist/app/input/input-controller.js +8 -5
- package/dist/app/rendering/render-controller.js +2 -0
- package/dist/app/rendering/status-line-renderer.d.ts +4 -1
- package/dist/app/rendering/status-line-renderer.js +12 -0
- package/dist/app/screen/mouse-controller.d.ts +4 -1
- package/dist/app/screen/mouse-controller.js +9 -0
- package/dist/app/session/agent-pause-controller.d.ts +30 -0
- package/dist/app/session/agent-pause-controller.js +178 -0
- package/dist/app/session/session-event-controller.d.ts +1 -0
- package/dist/app/session/session-event-controller.js +22 -15
- package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
- package/dist/app/session/session-lifecycle-controller.js +1 -0
- package/dist/app/types.d.ts +10 -0
- package/dist/config.js +1 -1
- package/dist/default-pix-config.js +2 -2
- package/external/pi-tools-suite/README.md +1 -1
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +34 -2
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +24 -18
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +14 -3
- package/external/pi-tools-suite/src/async-subagents/core/process.ts +19 -0
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +97 -17
- package/external/pi-tools-suite/src/async-subagents/core/stop.ts +4 -2
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/SKILL.md +108 -13
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +80 -0
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-flow.example.jsonc +54 -1
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/scripts/browser-qa-runner.mjs +952 -76
- package/external/pi-tools-suite/src/dcp/index.ts +21 -0
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +92 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +21 -16
- package/external/pi-tools-suite/src/todo/index.ts +86 -46
- package/external/pi-tools-suite/src/todo/todo.ts +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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-
|
|
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.
|
|
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.
|
|
308
|
+
"model": "openai-codex/gpt-5.6-luna",
|
|
306
309
|
"fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"],
|
|
307
|
-
"thinking": "
|
|
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": "
|
|
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 {
|
|
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
|
|
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,
|
|
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(
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
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
|
|
309
|
-
if (!
|
|
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]:
|
|
365
|
+
[TODO_THINKING_RESTORE_METADATA_KEY]: baselineThinking,
|
|
315
366
|
},
|
|
316
367
|
};
|
|
317
368
|
}
|
|
318
369
|
|
|
319
|
-
function
|
|
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
|
|
336
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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