pi-ui-extend 0.1.13 → 0.1.15
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 +1 -1
- package/dist/app/app.d.ts +5 -0
- package/dist/app/app.js +82 -12
- package/dist/app/commands/command-controller.js +1 -0
- package/dist/app/commands/command-host.d.ts +3 -0
- package/dist/app/commands/command-model-actions.d.ts +2 -0
- package/dist/app/commands/command-model-actions.js +40 -4
- package/dist/app/commands/command-navigation-actions.js +3 -0
- package/dist/app/commands/command-registry.d.ts +1 -0
- package/dist/app/commands/command-registry.js +8 -0
- package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
- package/dist/app/extensions/extension-ui-controller.js +99 -61
- package/dist/app/input/input-action-controller.d.ts +1 -0
- package/dist/app/input/input-action-controller.js +8 -2
- package/dist/app/logger.d.ts +25 -0
- package/dist/app/logger.js +90 -0
- package/dist/app/model/model-usage-status.js +30 -15
- package/dist/app/popup/menu-items-controller.d.ts +2 -0
- package/dist/app/popup/menu-items-controller.js +45 -6
- package/dist/app/popup/popup-action-controller.d.ts +2 -1
- package/dist/app/popup/popup-action-controller.js +7 -4
- package/dist/app/popup/popup-menu-controller.d.ts +36 -23
- package/dist/app/popup/popup-menu-controller.js +68 -322
- package/dist/app/rendering/conversation-entry-renderer.js +3 -3
- package/dist/app/rendering/conversation-viewport.d.ts +10 -2
- package/dist/app/rendering/conversation-viewport.js +157 -16
- package/dist/app/rendering/editor-panels.js +4 -2
- package/dist/app/rendering/popup-menu-renderer.d.ts +50 -0
- package/dist/app/rendering/popup-menu-renderer.js +307 -0
- package/dist/app/rendering/render-controller.js +5 -13
- package/dist/app/rendering/status-line-renderer.d.ts +1 -1
- package/dist/app/rendering/status-line-renderer.js +27 -24
- package/dist/app/rendering/toast-controller.d.ts +11 -3
- package/dist/app/rendering/toast-controller.js +53 -12
- package/dist/app/runtime.d.ts +2 -1
- package/dist/app/runtime.js +20 -10
- package/dist/app/screen/mouse-controller.d.ts +2 -2
- package/dist/app/screen/mouse-controller.js +27 -48
- package/dist/app/screen/screen-styler.d.ts +1 -1
- package/dist/app/screen/screen-styler.js +9 -7
- package/dist/app/screen/scroll-controller.d.ts +11 -9
- package/dist/app/screen/scroll-controller.js +50 -45
- package/dist/app/session/lazy-session-manager.d.ts +11 -0
- package/dist/app/session/lazy-session-manager.js +539 -0
- package/dist/app/session/pix-system-message.d.ts +16 -0
- package/dist/app/session/pix-system-message.js +64 -0
- package/dist/app/session/session-event-controller.d.ts +11 -0
- package/dist/app/session/session-event-controller.js +58 -2
- package/dist/app/session/session-history.d.ts +18 -0
- package/dist/app/session/session-history.js +72 -3
- package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
- package/dist/app/session/session-lifecycle-controller.js +7 -2
- package/dist/app/session/tabs-controller.d.ts +13 -1
- package/dist/app/session/tabs-controller.js +248 -27
- package/dist/app/todo/todo-model.d.ts +3 -1
- package/dist/app/todo/todo-model.js +14 -2
- package/dist/app/types.d.ts +5 -2
- package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
- package/dist/app/workspace/workspace-actions-controller.js +12 -0
- package/dist/config.d.ts +5 -1
- package/dist/config.js +73 -25
- package/dist/default-pix-config.js +2 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
- package/dist/schemas/pi-tools-suite-schema.js +1 -0
- package/dist/schemas/pix-schema.d.ts +2 -1
- package/dist/schemas/pix-schema.js +5 -4
- package/dist/terminal-width.d.ts +2 -0
- package/dist/terminal-width.js +64 -3
- package/external/pi-tools-suite/README.md +1 -0
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +12 -3
- package/external/pi-tools-suite/src/antigravity-auth/commands.ts +2 -4
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +2 -2
- package/external/pi-tools-suite/src/antigravity-auth/index.ts +8 -2
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +102 -50
- package/external/pi-tools-suite/src/antigravity-auth/status.ts +81 -2
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +29 -8
- package/external/pi-tools-suite/src/config.ts +8 -0
- package/external/pi-tools-suite/src/dcp/index.ts +16 -1
- package/external/pi-tools-suite/src/dcp/state.ts +35 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
- package/external/pi-tools-suite/src/todo/index.ts +181 -11
- package/external/pi-tools-suite/src/todo/state/state-reducer.ts +23 -10
- package/external/pi-tools-suite/src/todo/todo.ts +10 -5
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +33 -6
- package/external/pi-tools-suite/src/todo/tool/types.ts +9 -1
- package/external/pi-tools-suite/src/todo/view/format.ts +2 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +2 -1
- package/external/pi-tools-suite/src/usage/index.ts +5 -2
- package/external/pi-tools-suite/src/usage/lib/google.ts +6 -13
- package/package.json +1 -1
- package/schemas/pi-tools-suite.json +4 -0
- package/schemas/pix.json +6 -2
|
@@ -1,16 +1,46 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { loadPiToolsSuiteConfig } from "../config.js";
|
|
2
3
|
import { autoClearCompletedTodos } from "./state/auto-clear.js";
|
|
3
4
|
import { loadPersistedPlan, syncPersistedPlan } from "./state/persistence.js";
|
|
4
5
|
import { replayFromBranch } from "./state/replay.js";
|
|
5
6
|
import { ACTIVE_STATUSES, isTaskBlocked, selectVisibleTasks } from "./state/selectors.js";
|
|
6
7
|
import { getState, replaceState } from "./state/store.js";
|
|
7
|
-
import { publishTodoState, registerTodosCommand, registerTodoTool } from "./todo.js";
|
|
8
|
+
import { DEFAULT_PROMPT_GUIDELINES, DEFAULT_PROMPT_SNIPPET, publishTodoState, registerTodosCommand, registerTodoTool } from "./todo.js";
|
|
9
|
+
import type { TaskMutationParams } from "./tool/types.js";
|
|
8
10
|
|
|
9
11
|
const TODO_NUDGE_LIMIT = 8;
|
|
10
12
|
const TODO_NUDGE_INITIAL_DELAY_MS = 5_000;
|
|
11
13
|
const TODO_NUDGE_IDLE_RETRY_DELAY_MS = 100;
|
|
12
14
|
const TODO_NUDGE_MAX_IDLE_ATTEMPTS = 40;
|
|
13
15
|
const ASK_USER_TOOL_NAMES = new Set(["ask_user", "ask_user_question", "question"]);
|
|
16
|
+
const TODO_RECONCILE_ACTIONS = new Set(["list"]);
|
|
17
|
+
const TODO_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
18
|
+
|
|
19
|
+
type TodoThinkingLevel = (typeof TODO_THINKING_LEVELS)[number];
|
|
20
|
+
type ModelLike = { reasoning?: boolean; thinkingLevelMap?: Partial<Record<TodoThinkingLevel, unknown | null>> };
|
|
21
|
+
|
|
22
|
+
function isTodoThinkingLevel(value: unknown): value is TodoThinkingLevel {
|
|
23
|
+
return TODO_THINKING_LEVELS.includes(value as TodoThinkingLevel);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getAvailableTodoThinkingLevels(model: unknown): TodoThinkingLevel[] {
|
|
27
|
+
const m = model as ModelLike | undefined;
|
|
28
|
+
if (!m?.reasoning) return ["off"];
|
|
29
|
+
const map = m.thinkingLevelMap;
|
|
30
|
+
return TODO_THINKING_LEVELS.filter((level) => level === "off" || map?.[level] !== null);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildThinkingPromptParts(model: unknown): { promptSnippet?: string; promptGuidelines?: string[] } {
|
|
34
|
+
const levels = getAvailableTodoThinkingLevels(model);
|
|
35
|
+
if (levels.length <= 1) return {};
|
|
36
|
+
return {
|
|
37
|
+
promptSnippet: `${DEFAULT_PROMPT_SNIPPET} Optional per-item thinking: ${levels.join("|")}.`.trim(),
|
|
38
|
+
promptGuidelines: [
|
|
39
|
+
...DEFAULT_PROMPT_GUIDELINES,
|
|
40
|
+
`If todoThinking is enabled, set task \`thinking\` only when useful; choose from ${levels.join(", ")}. Split todos to change thinking by work type (e.g. final report can use off; retry/hard debugging can use higher thinking).`,
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
14
44
|
|
|
15
45
|
function isAskUserToolName(toolName: string): boolean {
|
|
16
46
|
return ASK_USER_TOOL_NAMES.has(toolName);
|
|
@@ -54,6 +84,39 @@ function getUnfinishedTodoNudge(): { signature: string; message: string } | unde
|
|
|
54
84
|
};
|
|
55
85
|
}
|
|
56
86
|
|
|
87
|
+
function getTodoFinalGuardPrompt(): { signature: string; message: string } | undefined {
|
|
88
|
+
const nudge = getUnfinishedTodoNudge();
|
|
89
|
+
if (!nudge) return undefined;
|
|
90
|
+
return {
|
|
91
|
+
signature: nudge.signature,
|
|
92
|
+
message: nudge.message.replace(
|
|
93
|
+
"Todo auto-nudge: unfinished todo items remain after your last response.",
|
|
94
|
+
"Todo final reconciliation required before answering the user.",
|
|
95
|
+
).replace(
|
|
96
|
+
"Continue working on them now. Pick exactly one pending/in_progress item, mark it in_progress if needed, make concrete progress, and update or complete todos immediately as work changes.",
|
|
97
|
+
"Run `todo list` first, reconcile the visible plan, then answer only after completed work is not left pending/in_progress/deferred by accident.",
|
|
98
|
+
),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isFinalAssistantMessage(message: unknown): boolean {
|
|
103
|
+
if (!message || typeof message !== "object") return false;
|
|
104
|
+
const record = message as Record<string, unknown>;
|
|
105
|
+
if (record.role !== "assistant" || record.stopReason !== "stop") return false;
|
|
106
|
+
const content = record.content;
|
|
107
|
+
if (typeof content === "string") return content.trim().length > 0;
|
|
108
|
+
if (!Array.isArray(content)) return false;
|
|
109
|
+
|
|
110
|
+
let hasText = false;
|
|
111
|
+
for (const block of content) {
|
|
112
|
+
if (!block || typeof block !== "object") continue;
|
|
113
|
+
const part = block as Record<string, unknown>;
|
|
114
|
+
if (part.type === "toolCall" || part.type === "tool-call") return false;
|
|
115
|
+
if (typeof part.text === "string" && part.text.trim().length > 0) hasText = true;
|
|
116
|
+
}
|
|
117
|
+
return hasText;
|
|
118
|
+
}
|
|
119
|
+
|
|
57
120
|
function getPersistedPlanPrompt(path: string): string | undefined {
|
|
58
121
|
const unfinished = selectVisibleTasks(getState()).filter((task) => task.status !== "completed");
|
|
59
122
|
if (unfinished.length === 0) return undefined;
|
|
@@ -84,9 +147,95 @@ function emitPersistedPlanPrompt(pi: ExtensionAPI, ctx: ExtensionContext, prompt
|
|
|
84
147
|
}
|
|
85
148
|
|
|
86
149
|
export default function (pi: ExtensionAPI) {
|
|
150
|
+
let currentModel: unknown;
|
|
151
|
+
const todoThinkingEnabled = loadPiToolsSuiteConfig(["todo"]).todoThinking;
|
|
152
|
+
const rememberedThinkingByTaskId = new Map<number, TodoThinkingLevel>();
|
|
87
153
|
let lastNudgedSignature: string | undefined;
|
|
154
|
+
let lastFinalGuardSignature: string | undefined;
|
|
155
|
+
let todoDirtySinceReconcile = false;
|
|
88
156
|
let nudgeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
89
157
|
const pendingAskUserToolCallIds = new Set<string>();
|
|
158
|
+
let suppressNextNudgeForThinkingSwitch = false;
|
|
159
|
+
|
|
160
|
+
function registerTodoToolWithCurrentPrompt(): void {
|
|
161
|
+
const thinkingPrompt = todoThinkingEnabled ? buildThinkingPromptParts(currentModel) : {};
|
|
162
|
+
registerTodoTool(pi, {
|
|
163
|
+
...thinkingPrompt,
|
|
164
|
+
afterCommit: async (state, ctx, info) => {
|
|
165
|
+
if (todoThinkingEnabled) applyTodoThinkingAfterCommit(state, info);
|
|
166
|
+
|
|
167
|
+
if (TODO_RECONCILE_ACTIONS.has(info.action)) {
|
|
168
|
+
todoDirtySinceReconcile = false;
|
|
169
|
+
lastFinalGuardSignature = undefined;
|
|
170
|
+
} else if (info.action !== "get" && info.action !== "export") {
|
|
171
|
+
todoDirtySinceReconcile = true;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const sync = syncPersistedPlan(ctx.cwd, state);
|
|
175
|
+
if (sync?.completed) console.log(`rpiv-todo: completed persisted plan and removed ${sync.path}`);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.warn(`rpiv-todo: failed to sync persisted plan — ${(err as Error).message}`);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function applyTodoThinkingAfterCommit(state: ReturnType<typeof getState>, info: { action: string; params: TaskMutationParams }): void {
|
|
184
|
+
const mutations = getTodoThinkingMutations(info.action, info.params);
|
|
185
|
+
for (const mutation of mutations) {
|
|
186
|
+
if (mutation.id === undefined || mutation.status === "in_progress") continue;
|
|
187
|
+
restoreTaskThinking(mutation.id);
|
|
188
|
+
}
|
|
189
|
+
restoreInactiveTodoThinking(state);
|
|
190
|
+
for (const mutation of mutations) {
|
|
191
|
+
if (mutation.id === undefined) continue;
|
|
192
|
+
const task = state.tasks.find((item) => item.id === mutation.id);
|
|
193
|
+
if (!task || task.status !== "in_progress" || !task.thinking) continue;
|
|
194
|
+
if (mutation.status !== "in_progress" && mutation.thinking === undefined) continue;
|
|
195
|
+
switchToTaskThinking(task.id, task.thinking);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getTodoThinkingMutations(action: string, params: TaskMutationParams): TaskMutationParams[] {
|
|
200
|
+
if (action === "update") return [params];
|
|
201
|
+
if (action === "batch_update") return params.items ?? [];
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getCurrentThinkingLevel(): TodoThinkingLevel | undefined {
|
|
206
|
+
const level = (pi as { getThinkingLevel?: () => unknown }).getThinkingLevel?.();
|
|
207
|
+
return isTodoThinkingLevel(level) ? level : undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function switchToTaskThinking(taskId: number, level: TodoThinkingLevel): void {
|
|
211
|
+
if (!getAvailableTodoThinkingLevels(currentModel).includes(level)) return;
|
|
212
|
+
const current = getCurrentThinkingLevel();
|
|
213
|
+
if (!current) return;
|
|
214
|
+
if (!rememberedThinkingByTaskId.has(taskId)) rememberedThinkingByTaskId.set(taskId, current);
|
|
215
|
+
if (current !== level) setTodoThinkingLevel(level);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function restoreTaskThinking(taskId: number): void {
|
|
219
|
+
const previous = rememberedThinkingByTaskId.get(taskId);
|
|
220
|
+
if (!previous) return;
|
|
221
|
+
rememberedThinkingByTaskId.delete(taskId);
|
|
222
|
+
if (getCurrentThinkingLevel() !== previous) setTodoThinkingLevel(previous);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function restoreInactiveTodoThinking(state: ReturnType<typeof getState>): void {
|
|
226
|
+
for (const taskId of [...rememberedThinkingByTaskId.keys()]) {
|
|
227
|
+
const task = state.tasks.find((item) => item.id === taskId);
|
|
228
|
+
if (task?.status === "in_progress") continue;
|
|
229
|
+
restoreTaskThinking(taskId);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function setTodoThinkingLevel(level: TodoThinkingLevel): void {
|
|
234
|
+
const setter = (pi as { setThinkingLevel?: (level: TodoThinkingLevel) => void }).setThinkingLevel;
|
|
235
|
+
if (!setter) return;
|
|
236
|
+
suppressNextNudgeForThinkingSwitch = true;
|
|
237
|
+
setter.call(pi, level);
|
|
238
|
+
}
|
|
90
239
|
|
|
91
240
|
function clearNudgeTimer(): void {
|
|
92
241
|
if (!nudgeTimer) return;
|
|
@@ -127,19 +276,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
127
276
|
}, delayMs);
|
|
128
277
|
}
|
|
129
278
|
|
|
130
|
-
|
|
131
|
-
afterCommit: (state, ctx) => {
|
|
132
|
-
try {
|
|
133
|
-
const sync = syncPersistedPlan(ctx.cwd, state);
|
|
134
|
-
if (sync?.completed) console.log(`rpiv-todo: completed persisted plan and removed ${sync.path}`);
|
|
135
|
-
} catch (err) {
|
|
136
|
-
console.warn(`rpiv-todo: failed to sync persisted plan — ${(err as Error).message}`);
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
});
|
|
279
|
+
registerTodoToolWithCurrentPrompt();
|
|
140
280
|
registerTodosCommand(pi);
|
|
141
281
|
|
|
142
282
|
pi.on("session_start", async (_event, ctx) => {
|
|
283
|
+
currentModel = ctx.model;
|
|
284
|
+
registerTodoToolWithCurrentPrompt();
|
|
143
285
|
const persisted = loadPersistedPlan(ctx.cwd);
|
|
144
286
|
const loaded = autoClearCompletedTodos(persisted?.state ?? replayFromBranch(ctx));
|
|
145
287
|
replaceState(loaded.state);
|
|
@@ -171,6 +313,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
171
313
|
clearNudgeTimer();
|
|
172
314
|
});
|
|
173
315
|
|
|
316
|
+
pi.on("model_select", async (event) => {
|
|
317
|
+
currentModel = event.model;
|
|
318
|
+
if (todoThinkingEnabled) registerTodoToolWithCurrentPrompt();
|
|
319
|
+
});
|
|
320
|
+
|
|
174
321
|
// Reads getTodos() at render time; do NOT call replayFromBranch here
|
|
175
322
|
// (branch is stale — message_end runs after tool_execution_end).
|
|
176
323
|
pi.on("tool_execution_start", async (event) => {
|
|
@@ -185,9 +332,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
185
332
|
|
|
186
333
|
pi.on("agent_start", async () => {
|
|
187
334
|
pendingAskUserToolCallIds.clear();
|
|
335
|
+
todoDirtySinceReconcile = false;
|
|
336
|
+
lastFinalGuardSignature = undefined;
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
pi.on("message_end", async (event) => {
|
|
340
|
+
if (!todoDirtySinceReconcile) return;
|
|
341
|
+
if (pendingAskUserToolCallIds.size > 0) return;
|
|
342
|
+
if (!isFinalAssistantMessage(event.message)) return;
|
|
343
|
+
|
|
344
|
+
const guard = getTodoFinalGuardPrompt();
|
|
345
|
+
if (!guard) return;
|
|
346
|
+
if (guard.signature === lastFinalGuardSignature) return;
|
|
347
|
+
|
|
348
|
+
lastFinalGuardSignature = guard.signature;
|
|
349
|
+
lastNudgedSignature = guard.signature;
|
|
350
|
+
clearNudgeTimer();
|
|
351
|
+
pi.sendUserMessage(guard.message, { deliverAs: "followUp" });
|
|
188
352
|
});
|
|
189
353
|
|
|
190
354
|
pi.on("agent_end", async (_event, ctx) => {
|
|
355
|
+
if (suppressNextNudgeForThinkingSwitch) {
|
|
356
|
+
suppressNextNudgeForThinkingSwitch = false;
|
|
357
|
+
clearNudgeTimer();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
191
361
|
if (pendingAskUserToolCallIds.size > 0) {
|
|
192
362
|
clearNudgeTimer();
|
|
193
363
|
return;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { Task, TaskAction, TaskMutationParams, TaskPriority, TaskStatus } from "../tool/types.js";
|
|
1
|
+
import type { Task, TaskAction, TaskMutationParams, TaskPriority, TaskStatus, TodoThinkingLevel } from "../tool/types.js";
|
|
2
2
|
import { isTransitionValid } from "./invariants.js";
|
|
3
3
|
import type { TaskState } from "./state.js";
|
|
4
4
|
import { detectCycle } from "./task-graph.js";
|
|
5
5
|
|
|
6
6
|
export type Op =
|
|
7
|
-
| { kind: "create"; taskId: number }
|
|
7
|
+
| { kind: "create"; taskId: number; replacedCount?: number }
|
|
8
8
|
| { kind: "update"; id: number; fromStatus: TaskStatus; toStatus: TaskStatus }
|
|
9
|
-
| { kind: "batch_create"; ids: number[] }
|
|
9
|
+
| { kind: "batch_create"; ids: number[]; replacedCount?: number }
|
|
10
10
|
| { kind: "batch_update"; ids: number[] }
|
|
11
11
|
| { kind: "delete"; id: number; subject: string }
|
|
12
12
|
| { kind: "list"; statusFilter?: TaskStatus; priorityFilter?: TaskPriority; tagFilter?: string; blockedOnly: boolean; includeDeleted: boolean }
|
|
@@ -34,6 +34,10 @@ function normalizeTags(tags: string[] | undefined): string[] | undefined {
|
|
|
34
34
|
return normalized.length ? normalized : undefined;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function isTodoThinkingLevel(value: unknown): value is TodoThinkingLevel {
|
|
38
|
+
return value === "off" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh";
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
function findTask(state: TaskState, id: number): Task | undefined {
|
|
38
42
|
return state.tasks.find((task) => task.id === id);
|
|
39
43
|
}
|
|
@@ -88,6 +92,7 @@ function coerceTask(value: unknown, fallbackId: number): Task | undefined {
|
|
|
88
92
|
if (typeof v.description === "string" && v.description) task.description = v.description;
|
|
89
93
|
if (typeof v.activeForm === "string" && v.activeForm) task.activeForm = v.activeForm;
|
|
90
94
|
if (v.priority === "low" || v.priority === "medium" || v.priority === "high" || v.priority === "urgent") task.priority = v.priority;
|
|
95
|
+
if (isTodoThinkingLevel(v.thinking)) task.thinking = v.thinking;
|
|
91
96
|
if (typeof v.parentId === "number" && Number.isFinite(v.parentId)) task.parentId = v.parentId;
|
|
92
97
|
const blockedBy = Array.isArray(v.blockedBy) ? uniqueNumbers(v.blockedBy as number[]) : undefined;
|
|
93
98
|
if (blockedBy?.length) task.blockedBy = blockedBy;
|
|
@@ -165,18 +170,21 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
|
|
|
165
170
|
switch (action) {
|
|
166
171
|
case "create": {
|
|
167
172
|
if (!params.subject?.trim()) return errorResult(state, "subject required for create");
|
|
173
|
+
const replacedCount = params.replace === true ? state.tasks.length : 0;
|
|
174
|
+
const baseState = params.replace === true ? { tasks: [], nextId: 1 } : state;
|
|
168
175
|
if (params.parentId !== undefined && params.parentId !== null) {
|
|
169
|
-
const err = validateLiveReference(
|
|
176
|
+
const err = validateLiveReference(baseState, "parentId", params.parentId);
|
|
170
177
|
if (err) return errorResult(state, err);
|
|
171
178
|
}
|
|
172
179
|
for (const dep of uniqueNumbers(params.blockedBy)) {
|
|
173
|
-
const err = validateLiveReference(
|
|
180
|
+
const err = validateLiveReference(baseState, "blockedBy", dep);
|
|
174
181
|
if (err) return errorResult(state, err);
|
|
175
182
|
}
|
|
176
|
-
const newTask: Task = { id:
|
|
183
|
+
const newTask: Task = { id: baseState.nextId, subject: params.subject.trim(), status: "pending" };
|
|
177
184
|
if (params.description) newTask.description = params.description;
|
|
178
185
|
if (params.activeForm) newTask.activeForm = params.activeForm;
|
|
179
186
|
if (params.priority) newTask.priority = params.priority;
|
|
187
|
+
if (params.thinking) newTask.thinking = params.thinking;
|
|
180
188
|
if (params.parentId !== undefined && params.parentId !== null) newTask.parentId = params.parentId;
|
|
181
189
|
const blockedBy = uniqueNumbers(params.blockedBy);
|
|
182
190
|
if (blockedBy.length) newTask.blockedBy = blockedBy;
|
|
@@ -184,7 +192,10 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
|
|
|
184
192
|
if (tags) newTask.tags = tags;
|
|
185
193
|
if (params.owner) newTask.owner = params.owner;
|
|
186
194
|
if (params.metadata) newTask.metadata = { ...params.metadata };
|
|
187
|
-
return {
|
|
195
|
+
return {
|
|
196
|
+
state: { tasks: [...baseState.tasks, newTask], nextId: baseState.nextId + 1 },
|
|
197
|
+
op: { kind: "create", taskId: newTask.id, ...(replacedCount > 0 ? { replacedCount } : {}) },
|
|
198
|
+
};
|
|
188
199
|
}
|
|
189
200
|
|
|
190
201
|
case "update": {
|
|
@@ -194,7 +205,7 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
|
|
|
194
205
|
const current = state.tasks[idx];
|
|
195
206
|
const hasMutation =
|
|
196
207
|
params.subject !== undefined || params.description !== undefined || params.activeForm !== undefined || params.status !== undefined ||
|
|
197
|
-
params.priority !== undefined || params.parentId !== undefined || params.clearParent === true || params.owner !== undefined ||
|
|
208
|
+
params.priority !== undefined || params.thinking !== undefined || params.parentId !== undefined || params.clearParent === true || params.owner !== undefined ||
|
|
198
209
|
params.metadata !== undefined || params.tags !== undefined || (params.addTags?.length ?? 0) > 0 || (params.removeTags?.length ?? 0) > 0 ||
|
|
199
210
|
(params.addBlockedBy?.length ?? 0) > 0 || (params.removeBlockedBy?.length ?? 0) > 0;
|
|
200
211
|
if (!hasMutation) return errorResult(state, "update requires at least one mutable field");
|
|
@@ -249,6 +260,7 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
|
|
|
249
260
|
if (params.description !== undefined) updated.description = params.description;
|
|
250
261
|
if (params.activeForm !== undefined) updated.activeForm = params.activeForm;
|
|
251
262
|
if (params.priority !== undefined) updated.priority = params.priority;
|
|
263
|
+
if (params.thinking !== undefined) updated.thinking = params.thinking;
|
|
252
264
|
if (params.owner !== undefined) updated.owner = params.owner;
|
|
253
265
|
if (newParentId === undefined) delete updated.parentId;
|
|
254
266
|
else updated.parentId = newParentId;
|
|
@@ -266,7 +278,8 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
|
|
|
266
278
|
|
|
267
279
|
case "batch_create": {
|
|
268
280
|
if (!params.items?.length) return errorResult(state, "items required for batch_create");
|
|
269
|
-
|
|
281
|
+
const replacedCount = params.replace === true ? state.tasks.length : 0;
|
|
282
|
+
let working = params.replace === true ? { tasks: [], nextId: 1 } : state;
|
|
270
283
|
const ids: number[] = [];
|
|
271
284
|
for (let i = 0; i < params.items.length; i++) {
|
|
272
285
|
const result = applyTaskMutation(working, "create", { ...params.items[i], action: "create" });
|
|
@@ -274,7 +287,7 @@ export function applyTaskMutation(state: TaskState, action: TaskAction, params:
|
|
|
274
287
|
if (result.op.kind === "create") ids.push(result.op.taskId);
|
|
275
288
|
working = result.state;
|
|
276
289
|
}
|
|
277
|
-
return { state: working, op: { kind: "batch_create", ids } };
|
|
290
|
+
return { state: working, op: { kind: "batch_create", ids, ...(replacedCount > 0 ? { replacedCount } : {}) } };
|
|
278
291
|
}
|
|
279
292
|
|
|
280
293
|
case "batch_update": {
|
|
@@ -78,7 +78,12 @@ const PERSIST_ARGUMENT_COMPLETIONS: CommandCompletion[] = [
|
|
|
78
78
|
];
|
|
79
79
|
|
|
80
80
|
interface TodoToolHooks {
|
|
81
|
-
afterCommit?: (state: ReturnType<typeof getState>, ctx: ExtensionContext) => void | Promise<void>;
|
|
81
|
+
afterCommit?: (state: ReturnType<typeof getState>, ctx: ExtensionContext, info: { action: TaskAction; params: TaskMutationParams }) => void | Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface TodoToolRegistrationOptions extends TodoToolHooks {
|
|
85
|
+
promptSnippet?: string;
|
|
86
|
+
promptGuidelines?: string[];
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
type TodoStateEventContext = { sessionManager?: { getSessionFile?: () => unknown } };
|
|
@@ -362,13 +367,13 @@ export function reconstructTodoState(ctx: Parameters<typeof replayFromBranch>[0]
|
|
|
362
367
|
export const DEFAULT_PROMPT_SNIPPET = TODO_TOOL_DESCRIPTION.promptSnippet ?? "";
|
|
363
368
|
export const DEFAULT_PROMPT_GUIDELINES: string[] = TODO_TOOL_DESCRIPTION.promptGuidelines ?? [];
|
|
364
369
|
|
|
365
|
-
export function registerTodoTool(pi: ExtensionAPI, hooks:
|
|
370
|
+
export function registerTodoTool(pi: ExtensionAPI, hooks: TodoToolRegistrationOptions = {}): void {
|
|
366
371
|
pi.registerTool({
|
|
367
372
|
...TODO_TOOL_DESCRIPTION,
|
|
368
373
|
name: TOOL_NAME,
|
|
369
374
|
label: TOOL_LABEL,
|
|
370
|
-
promptSnippet: DEFAULT_PROMPT_SNIPPET,
|
|
371
|
-
promptGuidelines: DEFAULT_PROMPT_GUIDELINES,
|
|
375
|
+
promptSnippet: hooks.promptSnippet ?? DEFAULT_PROMPT_SNIPPET,
|
|
376
|
+
promptGuidelines: hooks.promptGuidelines ?? DEFAULT_PROMPT_GUIDELINES,
|
|
372
377
|
parameters: TodoParamsSchema,
|
|
373
378
|
|
|
374
379
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
@@ -379,7 +384,7 @@ export function registerTodoTool(pi: ExtensionAPI, hooks: TodoToolHooks = {}): v
|
|
|
379
384
|
const autoClear = autoClearCompletedTodos(result.state);
|
|
380
385
|
commitState(autoClear.state);
|
|
381
386
|
publishTodoState(pi as TodoStateEventEmitter, _ctx, params.action, params as Record<string, unknown>);
|
|
382
|
-
await hooks.afterCommit?.(autoClear.state, _ctx as ExtensionContext);
|
|
387
|
+
await hooks.afterCommit?.(autoClear.state, _ctx as ExtensionContext, { action: params.action, params: params as TaskMutationParams });
|
|
383
388
|
const toolResult = buildToolResult(params.action, params as TaskMutationParams, autoClear.state, result.op);
|
|
384
389
|
if (!autoClear.cleared) return toolResult;
|
|
385
390
|
return {
|
|
@@ -11,9 +11,10 @@ function formatListLine(t: Task): string {
|
|
|
11
11
|
const block = t.blockedBy?.length ? ` ⛓ ${t.blockedBy.map((id) => `#${id}`).join(",")}` : "";
|
|
12
12
|
const parent = t.parentId !== undefined ? ` ↳ #${t.parentId}` : "";
|
|
13
13
|
const priority = t.priority ? ` (${t.priority})` : "";
|
|
14
|
+
const thinking = t.thinking ? ` {thinking:${t.thinking}}` : "";
|
|
14
15
|
const tags = t.tags?.length ? ` ${t.tags.map((tag) => `#${tag}`).join(" ")}` : "";
|
|
15
16
|
const form = t.status === "in_progress" && t.activeForm ? ` (${t.activeForm})` : "";
|
|
16
|
-
return `[${t.status}] #${t.id} ${t.subject}${priority}${form}${parent}${block}${tags}`;
|
|
17
|
+
return `[${t.status}] #${t.id} ${t.subject}${priority}${thinking}${form}${parent}${block}${tags}`;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -26,6 +27,7 @@ function formatGetLines(task: Task, state: TaskState): string {
|
|
|
26
27
|
if (task.description) lines.push(` description: ${task.description}`);
|
|
27
28
|
if (task.activeForm) lines.push(` activeForm: ${task.activeForm}`);
|
|
28
29
|
if (task.priority) lines.push(` priority: ${task.priority}`);
|
|
30
|
+
if (task.thinking) lines.push(` thinking: ${task.thinking}`);
|
|
29
31
|
if (task.parentId !== undefined) lines.push(` parentId: #${task.parentId}`);
|
|
30
32
|
if (task.blockedBy?.length) {
|
|
31
33
|
lines.push(` blockedBy: ${task.blockedBy.map((id) => `#${id}`).join(", ")}`);
|
|
@@ -49,6 +51,11 @@ function filterTasks(op: Extract<Op, { kind: "list" | "export" }>, state: TaskSt
|
|
|
49
51
|
return view;
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function formatReplacePrefix(replacedCount: number | undefined): string {
|
|
55
|
+
if (!replacedCount) return "";
|
|
56
|
+
return `Replaced ${replacedCount} existing todo item${replacedCount === 1 ? "" : "s"}; `;
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
function formatMarkdownExport(tasks: readonly Task[]): string {
|
|
53
60
|
const byParent = new Map<number | undefined, Task[]>();
|
|
54
61
|
for (const task of tasks) {
|
|
@@ -85,15 +92,15 @@ export function formatContent(op: Op, state: TaskState): string {
|
|
|
85
92
|
case "create": {
|
|
86
93
|
const t = state.tasks.find((x) => x.id === op.taskId);
|
|
87
94
|
// Defensive — `op.taskId` always resolves on success path.
|
|
88
|
-
if (!t) return
|
|
89
|
-
return
|
|
95
|
+
if (!t) return `${formatReplacePrefix(op.replacedCount)}Created #${op.taskId}`;
|
|
96
|
+
return `${formatReplacePrefix(op.replacedCount)}Created #${t.id}: ${t.subject} (pending)`;
|
|
90
97
|
}
|
|
91
98
|
case "update": {
|
|
92
99
|
const transition = op.fromStatus !== op.toStatus ? ` (${op.fromStatus} → ${op.toStatus})` : "";
|
|
93
100
|
return `Updated #${op.id}${transition}`;
|
|
94
101
|
}
|
|
95
102
|
case "batch_create":
|
|
96
|
-
return
|
|
103
|
+
return `${formatReplacePrefix(op.replacedCount)}Created ${op.ids.length} tasks: ${op.ids.map((id) => `#${id}`).join(", ")}`;
|
|
97
104
|
case "batch_update":
|
|
98
105
|
return `Updated ${op.ids.length} tasks: ${op.ids.map((id) => `#${id}`).join(", ")}`;
|
|
99
106
|
case "delete":
|
|
@@ -144,8 +151,28 @@ export function buildToolResult(
|
|
|
144
151
|
|
|
145
152
|
function appendWorkflowReminder(text: string, op: Op, state: TaskState): string {
|
|
146
153
|
if (op.kind === "error" || op.kind === "export") return text;
|
|
154
|
+
const lines = [text];
|
|
155
|
+
if (op.kind === "create" || op.kind === "batch_create") {
|
|
156
|
+
lines.push(
|
|
157
|
+
"Reminder: if this is a multi-step task, include a final todo item for the user-facing final report before completion. Give that final-report todo an explicit description/acceptance criteria: summarize changed files and behavior, list verification commands/results, mention any remaining manual action, and never replace the user-facing report with a compression/housekeeping note.",
|
|
158
|
+
);
|
|
159
|
+
const createdIds = new Set(op.kind === "create" ? [op.taskId] : op.ids);
|
|
160
|
+
const hasOlderUnfinished = !op.replacedCount && state.tasks.some((task) => {
|
|
161
|
+
if (createdIds.has(task.id)) return false;
|
|
162
|
+
return task.status !== "completed" && task.status !== "deleted";
|
|
163
|
+
});
|
|
164
|
+
if (hasOlderUnfinished) {
|
|
165
|
+
lines.push(
|
|
166
|
+
"Reminder: existing unfinished todos are still present. If this is a new plan that supersedes them, use batch_create with replace:true or explicitly update/defer/delete obsolete tasks.",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
147
170
|
const hasPending = state.tasks.some((task) => task.status === "pending");
|
|
148
171
|
const hasInProgress = state.tasks.some((task) => task.status === "in_progress");
|
|
149
|
-
if (
|
|
150
|
-
|
|
172
|
+
if (hasPending && !hasInProgress) {
|
|
173
|
+
lines.push(
|
|
174
|
+
"Reminder: pending todos exist but none is in_progress. Before starting work, call todo update on exactly one task with status in_progress and activeForm.",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return lines.join("\n\n");
|
|
151
178
|
}
|
|
@@ -25,6 +25,7 @@ export const MSG_NO_TODOS = "No todos yet. Ask the agent to add some!";
|
|
|
25
25
|
|
|
26
26
|
export type TaskStatus = "pending" | "in_progress" | "deferred" | "completed" | "deleted";
|
|
27
27
|
export type TaskPriority = "low" | "medium" | "high" | "urgent";
|
|
28
|
+
export type TodoThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
28
29
|
|
|
29
30
|
export type TaskAction = "create" | "update" | "batch_create" | "batch_update" | "list" | "get" | "delete" | "clear" | "export" | "import";
|
|
30
31
|
|
|
@@ -35,6 +36,7 @@ export interface Task {
|
|
|
35
36
|
activeForm?: string;
|
|
36
37
|
status: TaskStatus;
|
|
37
38
|
priority?: TaskPriority;
|
|
39
|
+
thinking?: TodoThinkingLevel;
|
|
38
40
|
parentId?: number;
|
|
39
41
|
blockedBy?: number[];
|
|
40
42
|
tags?: string[];
|
|
@@ -68,6 +70,7 @@ export interface TaskMutationParams {
|
|
|
68
70
|
activeForm?: string;
|
|
69
71
|
status?: TaskStatus;
|
|
70
72
|
priority?: TaskPriority;
|
|
73
|
+
thinking?: TodoThinkingLevel;
|
|
71
74
|
parentId?: number | null;
|
|
72
75
|
clearParent?: boolean;
|
|
73
76
|
blockedBy?: number[];
|
|
@@ -113,6 +116,11 @@ export const TodoParamsSchema = Type.Object({
|
|
|
113
116
|
description: "Task priority (create/update) or list/export filter (list/export)",
|
|
114
117
|
}),
|
|
115
118
|
),
|
|
119
|
+
thinking: Type.Optional(
|
|
120
|
+
StringEnum(["off", "minimal", "low", "medium", "high", "xhigh"] as const, {
|
|
121
|
+
description: "Per-task thinking level used when todoThinking is enabled and this task is in_progress",
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
116
124
|
parentId: Type.Optional(
|
|
117
125
|
Type.Number({
|
|
118
126
|
description: "Parent task id for hierarchy/subtasks (create/update); must refer to a non-deleted task",
|
|
@@ -182,7 +190,7 @@ export const TodoParamsSchema = Type.Object({
|
|
|
182
190
|
}),
|
|
183
191
|
),
|
|
184
192
|
content: Type.Optional(Type.String({ description: "Import content for action=import" })),
|
|
185
|
-
replace: Type.Optional(Type.Boolean({ description: "For import, replace existing tasks instead of appending. Default: false." })),
|
|
193
|
+
replace: Type.Optional(Type.Boolean({ description: "For import/create/batch_create, replace existing tasks instead of appending. Use batch_create with replace:true when starting a new plan that supersedes old unfinished todos. Default: false." })),
|
|
186
194
|
});
|
|
187
195
|
|
|
188
196
|
export type TodoParams = Static<typeof TodoParamsSchema>;
|
|
@@ -11,8 +11,9 @@ export { formatStatusLabel };
|
|
|
11
11
|
export function formatCommandTaskLine(t: Task, glyph: string): string {
|
|
12
12
|
const form = t.status === "in_progress" && t.activeForm ? ` (${t.activeForm})` : "";
|
|
13
13
|
const priority = t.priority ? ` (${t.priority})` : "";
|
|
14
|
+
const thinking = t.thinking ? ` {thinking:${t.thinking}}` : "";
|
|
14
15
|
const parent = t.parentId !== undefined ? ` ↳ #${t.parentId}` : "";
|
|
15
16
|
const block = t.blockedBy?.length ? ` ⛓ ${t.blockedBy.map((id) => `#${id}`).join(",")}` : "";
|
|
16
17
|
const tags = t.tags?.length ? ` ${t.tags.map((tag) => `#${tag}`).join(" ")}` : "";
|
|
17
|
-
return ` ${glyph} #${t.id} ${t.subject}${priority}${form}${parent}${block}${tags}`;
|
|
18
|
+
return ` ${glyph} #${t.id} ${t.subject}${priority}${thinking}${form}${parent}${block}${tags}`;
|
|
18
19
|
}
|
|
@@ -249,7 +249,7 @@ export const REPO_DISCOVERY_TOOL_NAMES = REPO_DISCOVERY_TOOLS.map((tool) => tool
|
|
|
249
249
|
export const TODO_TOOL_DESCRIPTION: ToolDescription = {
|
|
250
250
|
name: "todo",
|
|
251
251
|
label: "Todo",
|
|
252
|
-
description: "Track and keep in sync non-trivial multi-step work as todos. Actions: create, update, batch_create, batch_update, list, get, delete, clear, export, import. Supports priorities, tags, parent/subtask hierarchy, blockers, deferred out-of-scope items, and
|
|
252
|
+
description: "Track and keep in sync non-trivial multi-step work as todos. Actions: create, update, batch_create, batch_update, list, get, delete, clear, export, import. Supports priorities, tags, parent/subtask hierarchy, blockers, deferred out-of-scope items, dependencies, and replace:true on create/batch_create/import for intentionally replacing an obsolete plan; skip trivial or chat-only requests. Resynchronize the plan when requirements are added, canceled, or become obsolete, whether from user input or discovered facts. Keep exactly one task in_progress and complete it only after verification.",
|
|
253
253
|
promptSnippet: "Track/sync non-trivial multi-step work; resync when requirements or discoveries change the plan; keep one task in_progress",
|
|
254
254
|
promptGuidelines: [
|
|
255
255
|
"Use `todo` for complex work with 3+ steps, explicit user task lists, or new non-trivial requirements. Skip single trivial tasks and purely conversational requests.",
|
|
@@ -262,6 +262,7 @@ export const TODO_TOOL_DESCRIPTION: ToolDescription = {
|
|
|
262
262
|
"Before giving a final response for work that used todos, ensure every visible todo is completed, deferred, or intentionally still in_progress with a blocker/explanation.",
|
|
263
263
|
"Keep subjects short and imperative; put details in description only when needed. Use priority, tags, and parentId for large plans; use blockedBy on create and addBlockedBy/removeBlockedBy on update for dependencies.",
|
|
264
264
|
"Use batch_create/batch_update for large explicit plans, but still keep exactly one visible task in_progress unless the user asks otherwise.",
|
|
265
|
+
"When starting a new plan that supersedes existing unfinished todos, use batch_create with replace:true instead of appending; only omit replace when intentionally extending the current plan.",
|
|
265
266
|
"list hides deleted tombstones unless includeDeleted:true; pass status, priority, tag, or blockedOnly only when you need a filtered list.",
|
|
266
267
|
"Use export/import for handoff or plan migration; import with replace:true only when the user explicitly wants to overwrite the current todo state.",
|
|
267
268
|
"When every visible todo is completed, todo state clears automatically; do not call clear afterward just to remove completed tasks.",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -37,7 +37,10 @@ async function readOpenCodeAuth(): Promise<{ authData: AuthData; error?: string
|
|
|
37
37
|
|
|
38
38
|
async function readPiAuth(): Promise<PiAuthData> {
|
|
39
39
|
try {
|
|
40
|
-
const
|
|
40
|
+
const authPath = process.env.NODE_ENV === "test" && process.env.PI_TOOLS_SUITE_TEST_AUTH_PATH
|
|
41
|
+
? process.env.PI_TOOLS_SUITE_TEST_AUTH_PATH
|
|
42
|
+
: join(homedir(), ".pi", "agent", "auth.json");
|
|
43
|
+
const content = await readFile(authPath, "utf-8");
|
|
41
44
|
return JSON.parse(content) as PiAuthData;
|
|
42
45
|
} catch {
|
|
43
46
|
return {};
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* [同步]: usage.ts, types.ts, utils.ts
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
11
10
|
import { readFile } from "node:fs/promises";
|
|
12
11
|
import { homedir } from "node:os";
|
|
13
12
|
import { join } from "node:path";
|
|
@@ -97,7 +96,9 @@ function getAntigravityAccountsPath(): string {
|
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
function getPiAuthPath(): string {
|
|
100
|
-
return
|
|
99
|
+
return process.env.NODE_ENV === "test" && process.env.PI_TOOLS_SUITE_TEST_AUTH_PATH
|
|
100
|
+
? process.env.PI_TOOLS_SUITE_TEST_AUTH_PATH
|
|
101
|
+
: join(homedir(), ".pi", "agent", "auth.json");
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
function splitPiRefresh(refresh: string): AntigravityAccount | null {
|
|
@@ -156,14 +157,8 @@ async function readAntigravityAccounts(): Promise<AntigravityAccount[]> {
|
|
|
156
157
|
|
|
157
158
|
const GOOGLE_TOKEN_REFRESH_URL = "https://oauth2.googleapis.com/token";
|
|
158
159
|
|
|
159
|
-
const GOOGLE_CLIENT_ID =
|
|
160
|
-
|
|
161
|
-
process.env.ANTIGRAVITY_GOOGLE_CLIENT_ID ??
|
|
162
|
-
"";
|
|
163
|
-
const GOOGLE_CLIENT_SECRET =
|
|
164
|
-
process.env.PIX_ANTIGRAVITY_GOOGLE_CLIENT_SECRET ??
|
|
165
|
-
process.env.ANTIGRAVITY_GOOGLE_CLIENT_SECRET ??
|
|
166
|
-
"";
|
|
160
|
+
const GOOGLE_CLIENT_ID = "";
|
|
161
|
+
const GOOGLE_CLIENT_SECRET = "";
|
|
167
162
|
|
|
168
163
|
// ============================================================================
|
|
169
164
|
// 工具函数
|
|
@@ -258,9 +253,7 @@ async function refreshAccessToken(
|
|
|
258
253
|
refreshToken: string,
|
|
259
254
|
): Promise<{ access_token: string; expires_in: number }> {
|
|
260
255
|
if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) {
|
|
261
|
-
throw new Error(
|
|
262
|
-
"Antigravity Google OAuth credentials are not configured; set PIX_ANTIGRAVITY_GOOGLE_CLIENT_ID and PIX_ANTIGRAVITY_GOOGLE_CLIENT_SECRET.",
|
|
263
|
-
);
|
|
256
|
+
throw new Error("Antigravity Google OAuth credentials are not bundled.");
|
|
264
257
|
}
|
|
265
258
|
|
|
266
259
|
const params = new URLSearchParams({
|
package/package.json
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
},
|
|
17
17
|
"description": "List of disabled module names (e.g. ['lsp', 'prompt-commands'])."
|
|
18
18
|
},
|
|
19
|
+
"todoThinking": {
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"description": "Enable per-todo thinking levels and automatic thinking switch/restore when tasks become in-progress/completed."
|
|
22
|
+
},
|
|
19
23
|
"terminalBell": {
|
|
20
24
|
"type": "object",
|
|
21
25
|
"properties": {
|