pi-quests 0.2.0 → 0.3.0
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/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/commands/handler.ts +10 -2
- package/src/commands/parse-args.ts +18 -2
- package/src/index.ts +18 -9
- package/src/prompts.ts +16 -0
- package/src/quest/dataplane.ts +103 -10
- package/src/quest/formatters.ts +2 -1
- package/src/quest/types.ts +2 -0
- package/src/renderers/commands.ts +4 -2
- package/src/renderers/tools.ts +12 -7
- package/src/tools/handler.ts +11 -10
- package/src/tools/params.ts +12 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-04-11
|
|
6
|
+
|
|
7
|
+
- feat: add reorder action to move quests by position (dataplane, tool, /quests command, renderer, revert support)
|
|
8
|
+
- feat: extend clear action with optional all flag to remove all quests regardless of done state
|
|
9
|
+
- feat: detect appended completion markers (e.g. "- DONE") and inject correction nudge via context hook
|
|
10
|
+
- feat: update list rendering to show 1-based positions instead of raw IDs
|
|
11
|
+
- fix: inline type imports across test files to comply with no-inline-imports rule
|
|
12
|
+
- chore: extract shared prompt strings into prompts.ts to eliminate duplication
|
|
13
|
+
- chore: fix release workflow output mapping in create-release job
|
|
14
|
+
|
|
5
15
|
## [0.2.0] - 2026-04-11
|
|
6
16
|
|
|
7
17
|
- feat: add dynamic quest usage nudges via context hook
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-quests
|
|
2
2
|
|
|
3
|
-
[](CHANGELOG.md)
|
|
4
4
|
[](LICENSE.md)
|
|
5
5
|
[](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent)
|
|
6
6
|
|
package/package.json
CHANGED
package/src/commands/handler.ts
CHANGED
|
@@ -17,6 +17,7 @@ type MutatingCommand = Extract<
|
|
|
17
17
|
| typeof QUEST_ACTIONS.update
|
|
18
18
|
| typeof QUEST_ACTIONS.delete
|
|
19
19
|
| typeof QUEST_ACTIONS.clear
|
|
20
|
+
| typeof QUEST_ACTIONS.reorder
|
|
20
21
|
| typeof QUEST_ACTIONS.revert;
|
|
21
22
|
}
|
|
22
23
|
>["action"];
|
|
@@ -32,7 +33,12 @@ const commandActionBuilders: {
|
|
|
32
33
|
description: p.description,
|
|
33
34
|
}),
|
|
34
35
|
[QUEST_ACTIONS.delete]: (p) => ({ type: QUEST_ACTIONS.delete, id: p.id }),
|
|
35
|
-
[QUEST_ACTIONS.clear]: () => ({ type: QUEST_ACTIONS.clear }),
|
|
36
|
+
[QUEST_ACTIONS.clear]: (p) => ({ type: QUEST_ACTIONS.clear, all: p.all }),
|
|
37
|
+
[QUEST_ACTIONS.reorder]: (p) => ({
|
|
38
|
+
type: QUEST_ACTIONS.reorder,
|
|
39
|
+
id: p.id,
|
|
40
|
+
targetIndex: p.targetIndex,
|
|
41
|
+
}),
|
|
36
42
|
[QUEST_ACTIONS.revert]: () => ({ type: QUEST_ACTIONS.revert }),
|
|
37
43
|
};
|
|
38
44
|
|
|
@@ -101,6 +107,7 @@ export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog) {
|
|
|
101
107
|
case QUEST_ACTIONS.update:
|
|
102
108
|
case QUEST_ACTIONS.delete:
|
|
103
109
|
case QUEST_ACTIONS.clear:
|
|
110
|
+
case QUEST_ACTIONS.reorder:
|
|
104
111
|
case QUEST_ACTIONS.revert: {
|
|
105
112
|
const builder = commandActionBuilders[parsed.action];
|
|
106
113
|
const action = builder(parsed as never);
|
|
@@ -119,8 +126,9 @@ export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog) {
|
|
|
119
126
|
lines.push(" toggle <id> - Toggle quest completion");
|
|
120
127
|
lines.push(" delete <id> - Delete a quest");
|
|
121
128
|
lines.push(" update <id> <desc> - Update a quest description");
|
|
129
|
+
lines.push(" reorder <id> <idx> - Reorder a quest to index");
|
|
122
130
|
lines.push(" revert - Revert the last quest change");
|
|
123
|
-
lines.push(" clear
|
|
131
|
+
lines.push(" clear [all] - Clear completed quests, or optionally all quests");
|
|
124
132
|
lines.push(" version - Show version");
|
|
125
133
|
lines.push(" changelog - Show changelog");
|
|
126
134
|
lines.push(" h, help - Show this help message");
|
|
@@ -7,7 +7,8 @@ export type ParsedArgs =
|
|
|
7
7
|
| { action: typeof QUEST_ACTIONS.toggle; id: number }
|
|
8
8
|
| { action: typeof QUEST_ACTIONS.update; id: number; description: string }
|
|
9
9
|
| { action: typeof QUEST_ACTIONS.delete; id: number }
|
|
10
|
-
| { action: typeof QUEST_ACTIONS.clear }
|
|
10
|
+
| { action: typeof QUEST_ACTIONS.clear; all?: boolean }
|
|
11
|
+
| { action: typeof QUEST_ACTIONS.reorder; id: number; targetIndex: number }
|
|
11
12
|
| { action: typeof QUEST_ACTIONS.revert }
|
|
12
13
|
| { action: "help" }
|
|
13
14
|
| { action: "version" }
|
|
@@ -36,7 +37,7 @@ export function parseQuestArgs(args: string): ParsedArgs {
|
|
|
36
37
|
|
|
37
38
|
// Quest actions without arguments
|
|
38
39
|
if (command === QUEST_ACTIONS.list) return { action: QUEST_ACTIONS.list };
|
|
39
|
-
if (command === QUEST_ACTIONS.clear) return
|
|
40
|
+
if (command === QUEST_ACTIONS.clear) return parseClearArgs(rest);
|
|
40
41
|
if (command === QUEST_ACTIONS.revert) return { action: QUEST_ACTIONS.revert };
|
|
41
42
|
|
|
42
43
|
// Quest actions with arguments
|
|
@@ -44,6 +45,7 @@ export function parseQuestArgs(args: string): ParsedArgs {
|
|
|
44
45
|
if (command === QUEST_ACTIONS.toggle) return parseIdAction(QUEST_ACTIONS.toggle, rest);
|
|
45
46
|
if (command === QUEST_ACTIONS.delete) return parseIdAction(QUEST_ACTIONS.delete, rest);
|
|
46
47
|
if (command === QUEST_ACTIONS.update) return parseUpdateArgs(rest);
|
|
48
|
+
if (command === QUEST_ACTIONS.reorder) return parseReorderArgs(rest);
|
|
47
49
|
|
|
48
50
|
return { error: `Unknown subcommand: ${command}. Use /quests help to see available commands.` };
|
|
49
51
|
}
|
|
@@ -76,3 +78,17 @@ function parseUpdateArgs(tokens: string[]): ParsedArgs {
|
|
|
76
78
|
|
|
77
79
|
return { action: QUEST_ACTIONS.update, id, description };
|
|
78
80
|
}
|
|
81
|
+
|
|
82
|
+
function parseClearArgs(tokens: string[]): ParsedArgs {
|
|
83
|
+
const all = tokens[0] === "all";
|
|
84
|
+
if (tokens.length > 0 && !all) return { error: "Usage: /quests clear [all]" };
|
|
85
|
+
return { action: QUEST_ACTIONS.clear, all };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseReorderArgs(tokens: string[]): ParsedArgs {
|
|
89
|
+
const id = Number(tokens[0]);
|
|
90
|
+
const targetIndex = Number(tokens[1]);
|
|
91
|
+
if (Number.isNaN(id) || Number.isNaN(targetIndex))
|
|
92
|
+
return { error: "Usage: /quests reorder <id> <index>" };
|
|
93
|
+
return { action: QUEST_ACTIONS.reorder, id, targetIndex };
|
|
94
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { UserMessage } from "@mariozechner/pi-ai";
|
|
1
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
3
|
import { createQuestsHandler } from "./commands/handler.js";
|
|
4
|
+
import { QUEST_PROMPT_GATE, QUEST_PROMPT_REMINDER } from "./prompts.js";
|
|
3
5
|
import { QuestLog } from "./quest/dataplane.js";
|
|
4
6
|
import { QuestUsageTracker } from "./quest/tracker.js";
|
|
5
7
|
import { questChangelogRenderer } from "./renderers/changelog.js";
|
|
@@ -36,11 +38,20 @@ export default function (pi: ExtensionAPI): void {
|
|
|
36
38
|
|
|
37
39
|
const activeQuestCount = questLog.getAll().filter((q) => !q.done).length;
|
|
38
40
|
const nudge = tracker.getNudge(activeQuestCount, latestPrompt);
|
|
39
|
-
if (!nudge) return undefined;
|
|
40
41
|
|
|
41
|
-
const
|
|
42
|
+
const fakeDoneRegex =
|
|
43
|
+
/\s[-–—]\s*(DONE|COMPLETED|FINISHED)$|\s[([](DONE|COMPLETED|FINISHED)[)\]]$/i;
|
|
44
|
+
const fakeDone = questLog.getAll().find((q) => !q.done && fakeDoneRegex.test(q.description));
|
|
45
|
+
if (!nudge && !fakeDone) return undefined;
|
|
46
|
+
|
|
47
|
+
let content = nudge ?? "";
|
|
48
|
+
if (fakeDone) {
|
|
49
|
+
content += `\nQUEST REMINDER: Quest #${fakeDone.id} has a completion marker appended to its description but is not toggled done. Use the toggle action to mark it done. NEVER append completion markers to descriptions via the update action.`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const reminder: UserMessage = {
|
|
42
53
|
role: "user",
|
|
43
|
-
content:
|
|
54
|
+
content: content.trim(),
|
|
44
55
|
timestamp: Date.now(),
|
|
45
56
|
};
|
|
46
57
|
return { messages: [...event.messages, reminder] };
|
|
@@ -48,11 +59,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
48
59
|
|
|
49
60
|
pi.on("before_agent_start", async (event) => {
|
|
50
61
|
const quests = questLog.getAll();
|
|
51
|
-
|
|
52
|
-
"# Quest Management\nBefore reading files, running commands, or making edits, ALWAYS ensure the current work is tracked as specific, actionable quests. ALWAYS break broad requests into concrete steps.\n\n";
|
|
53
|
-
|
|
54
|
-
let reminder =
|
|
55
|
-
"## Quest Management\nUse the quest tool VERY frequently to track tasks, plans, and progress throughout the conversation. It is critical that you toggle quests to done as soon as you complete them. NEVER batch up multiple tasks before marking them completed.\n\nNEVER create a single vague quest for broad requests. Analyze the user's intent and break it into specific, independent, actionable quests that each represent a concrete step.";
|
|
62
|
+
let reminder = QUEST_PROMPT_REMINDER.join("\n");
|
|
56
63
|
if (quests.length > 0) {
|
|
57
64
|
const remaining = quests.filter((q) => !q.done).length;
|
|
58
65
|
const list = quests
|
|
@@ -62,7 +69,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
62
69
|
reminder += `\n\nActive quests (${remaining}/${quests.length}):\n${list}`;
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
systemPrompt: `# Quest Management\n${QUEST_PROMPT_GATE}${event.systemPrompt}\n\n## Quest Management\n${reminder}`,
|
|
74
|
+
};
|
|
66
75
|
});
|
|
67
76
|
|
|
68
77
|
registerQuestTool(pi, questLog);
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const QUEST_PROMPT_REMINDER = [
|
|
2
|
+
"Use the quest tool VERY frequently to track tasks, plans, and progress throughout the conversation.",
|
|
3
|
+
"Before reading files, running commands, or making edits, ensure the current work is tracked as specific, actionable quests.",
|
|
4
|
+
"Do not create a single vague quest for broad requests. Break them into concrete, independent steps.",
|
|
5
|
+
"When the user gives a plan or a list of tasks, add them as quests immediately.",
|
|
6
|
+
"When reading a skill file, implementation plan, or protocol document that contains numbered steps or checklists, add those steps as quests immediately so they are tracked, and reorder them as needed in the quest log.",
|
|
7
|
+
"It is critical that you toggle quests to done as soon as you complete them. Do NOT batch completions.",
|
|
8
|
+
"ALWAYS use the toggle action to mark a quest done. NEVER use the update action to append 'DONE', '- DONE', or any completion marker to a quest description.",
|
|
9
|
+
"As work evolves, use the reorder action to reflect changes in priority or sequencing.",
|
|
10
|
+
"Before delegating to a minion, add a quest for the delegated task.",
|
|
11
|
+
"For reorder, targetIndex is 0-based (array index), not a 1-based position. If the user says 'move quest to position 5', use targetIndex 4.",
|
|
12
|
+
"If you are unsure what to do next, use the list action to check active quests.",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export const QUEST_PROMPT_GATE =
|
|
16
|
+
"Before reading files, running commands, or making edits, ALWAYS ensure the current work is tracked as specific, actionable quests. ALWAYS break broad requests into concrete steps.";
|
package/src/quest/dataplane.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { AgentToolResult, ExtensionContext } from "@mariozechner/pi-coding-
|
|
|
2
2
|
import { logger } from "../logger.js";
|
|
3
3
|
import {
|
|
4
4
|
formatBatchAddResult,
|
|
5
|
-
formatClearResult,
|
|
6
5
|
formatDeleteResult,
|
|
7
6
|
formatNotFound,
|
|
8
7
|
formatQuestList,
|
|
@@ -16,7 +15,14 @@ export type HistoryEntry =
|
|
|
16
15
|
| { type: typeof QUEST_ACTIONS.toggle; id: number }
|
|
17
16
|
| { type: typeof QUEST_ACTIONS.update; id: number; previousDescription: string }
|
|
18
17
|
| { type: typeof QUEST_ACTIONS.delete; quest: Quest; index: number }
|
|
19
|
-
| {
|
|
18
|
+
| {
|
|
19
|
+
type: typeof QUEST_ACTIONS.clear;
|
|
20
|
+
previousQuests: Quest[];
|
|
21
|
+
previousNextId: number;
|
|
22
|
+
all: false;
|
|
23
|
+
}
|
|
24
|
+
| { type: typeof QUEST_ACTIONS.clear; quests: Quest[]; nextId: number; all: true }
|
|
25
|
+
| { type: typeof QUEST_ACTIONS.reorder; quest: Quest; oldIndex: number; previousIds: number[] };
|
|
20
26
|
|
|
21
27
|
export type QuestAction =
|
|
22
28
|
| { type: typeof QUEST_ACTIONS.add; descriptions?: string[] }
|
|
@@ -24,7 +30,8 @@ export type QuestAction =
|
|
|
24
30
|
| { type: typeof QUEST_ACTIONS.toggle; id?: number }
|
|
25
31
|
| { type: typeof QUEST_ACTIONS.update; id?: number; description?: string }
|
|
26
32
|
| { type: typeof QUEST_ACTIONS.delete; id?: number }
|
|
27
|
-
| { type: typeof QUEST_ACTIONS.clear }
|
|
33
|
+
| { type: typeof QUEST_ACTIONS.clear; all?: boolean }
|
|
34
|
+
| { type: typeof QUEST_ACTIONS.reorder; id?: number; targetIndex?: number }
|
|
28
35
|
| { type: typeof QUEST_ACTIONS.revert };
|
|
29
36
|
|
|
30
37
|
export type QuestOperationResult = { success: boolean; message: string; quest?: Quest };
|
|
@@ -88,12 +95,35 @@ export class QuestLog {
|
|
|
88
95
|
return { success: true, message: `Reverted delete for quest #${entry.quest.id}` };
|
|
89
96
|
},
|
|
90
97
|
[QUEST_ACTIONS.clear]: (entry) => {
|
|
98
|
+
if ("previousQuests" in entry) {
|
|
99
|
+
const restoredCount = entry.previousQuests.length - this.quests.length;
|
|
100
|
+
this.quests = [...entry.previousQuests];
|
|
101
|
+
this.nextId = entry.previousNextId;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
message: `Reverted clear (${restoredCount} quests restored)`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
91
109
|
this.quests = [...entry.quests];
|
|
92
110
|
this.nextId = entry.nextId;
|
|
93
111
|
|
|
94
|
-
logger.debug("quests:state", "revert-clear", { count: entry.quests.length });
|
|
95
112
|
return { success: true, message: `Reverted clear (${entry.quests.length} quests restored)` };
|
|
96
113
|
},
|
|
114
|
+
[QUEST_ACTIONS.reorder]: (entry) => {
|
|
115
|
+
const currentIndex = this.quests.indexOf(entry.quest);
|
|
116
|
+
if (currentIndex === -1) return { success: false, message: "Reordered quest not found" };
|
|
117
|
+
|
|
118
|
+
this.quests.splice(currentIndex, 1);
|
|
119
|
+
this.quests.splice(entry.oldIndex, 0, entry.quest);
|
|
120
|
+
for (let i = 0; i < this.quests.length; i++) {
|
|
121
|
+
this.quests[i].id = entry.previousIds[i];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.nextId = Math.max(...entry.previousIds, 0) + 1;
|
|
125
|
+
return { success: true, message: `Reverted reorder for quest #${entry.quest.id}` };
|
|
126
|
+
},
|
|
97
127
|
};
|
|
98
128
|
|
|
99
129
|
getAll(): Quest[] {
|
|
@@ -174,16 +204,63 @@ export class QuestLog {
|
|
|
174
204
|
return quest;
|
|
175
205
|
}
|
|
176
206
|
|
|
177
|
-
clear(): number {
|
|
207
|
+
clear(all = false): number {
|
|
208
|
+
if (!all) {
|
|
209
|
+
const done = this.quests.filter((q) => q.done);
|
|
210
|
+
const previousQuests = this.quests.map((q) => ({ ...q }));
|
|
211
|
+
const previousNextId = this.nextId;
|
|
212
|
+
this.quests = this.quests.filter((q) => !q.done);
|
|
213
|
+
for (let i = 0; i < this.quests.length; i++) {
|
|
214
|
+
this.quests[i].id = i + 1;
|
|
215
|
+
}
|
|
216
|
+
this.nextId = this.quests.length + 1;
|
|
217
|
+
this.history.push({
|
|
218
|
+
type: QUEST_ACTIONS.clear,
|
|
219
|
+
previousQuests,
|
|
220
|
+
previousNextId,
|
|
221
|
+
all: false,
|
|
222
|
+
});
|
|
223
|
+
logger.debug("quests:state", QUEST_ACTIONS.clear, { count: done.length, all });
|
|
224
|
+
return done.length;
|
|
225
|
+
}
|
|
178
226
|
const count = this.quests.length;
|
|
179
|
-
this.history.push({
|
|
227
|
+
this.history.push({
|
|
228
|
+
type: QUEST_ACTIONS.clear,
|
|
229
|
+
quests: [...this.quests],
|
|
230
|
+
nextId: this.nextId,
|
|
231
|
+
all: true,
|
|
232
|
+
});
|
|
180
233
|
this.quests = [];
|
|
181
234
|
this.nextId = 1;
|
|
182
|
-
|
|
183
|
-
logger.debug("quests:state", QUEST_ACTIONS.clear, { count });
|
|
235
|
+
logger.debug("quests:state", QUEST_ACTIONS.clear, { count, all });
|
|
184
236
|
return count;
|
|
185
237
|
}
|
|
186
238
|
|
|
239
|
+
reorder(id: number, targetIndex: number): Quest | undefined {
|
|
240
|
+
const idx = this.quests.findIndex((q) => q.id === id);
|
|
241
|
+
if (idx === -1) {
|
|
242
|
+
logger.debug("quests:state", "reorder-not-found", { id });
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const previousIds = this.quests.map((q) => q.id);
|
|
247
|
+
const [quest] = this.quests.splice(idx, 1);
|
|
248
|
+
this.quests.splice(targetIndex, 0, quest);
|
|
249
|
+
for (let i = 0; i < this.quests.length; i++) {
|
|
250
|
+
this.quests[i].id = i + 1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
this.nextId = this.quests.length + 1;
|
|
254
|
+
this.history.push({ type: QUEST_ACTIONS.reorder, quest, oldIndex: idx, previousIds });
|
|
255
|
+
logger.debug("quests:state", QUEST_ACTIONS.reorder, {
|
|
256
|
+
id: quest.id,
|
|
257
|
+
targetIndex,
|
|
258
|
+
total: this.quests.length,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return quest;
|
|
262
|
+
}
|
|
263
|
+
|
|
187
264
|
revert(): QuestOperationResult {
|
|
188
265
|
const entry = this.history.pop();
|
|
189
266
|
if (!entry) {
|
|
@@ -274,8 +351,24 @@ export class QuestLog {
|
|
|
274
351
|
return { success: true, message: formatDeleteResult(q), quest: q };
|
|
275
352
|
}
|
|
276
353
|
case QUEST_ACTIONS.clear: {
|
|
277
|
-
const count = this.clear();
|
|
278
|
-
|
|
354
|
+
const count = this.clear(action.all);
|
|
355
|
+
const message = action.all
|
|
356
|
+
? `Cleared ${count} quests`
|
|
357
|
+
: `Cleared ${count} completed quests`;
|
|
358
|
+
|
|
359
|
+
return { success: true, message };
|
|
360
|
+
}
|
|
361
|
+
case QUEST_ACTIONS.reorder: {
|
|
362
|
+
if (action.id === undefined)
|
|
363
|
+
return { success: false, message: "Error: id is required for reorder action" };
|
|
364
|
+
|
|
365
|
+
if (action.targetIndex === undefined)
|
|
366
|
+
return { success: false, message: "Error: targetIndex is required for reorder action" };
|
|
367
|
+
|
|
368
|
+
const q = this.reorder(action.id, action.targetIndex);
|
|
369
|
+
if (!q) return { success: false, message: formatNotFound(action.id) };
|
|
370
|
+
|
|
371
|
+
return { success: true, message: `Reordered quest #${q.id}: ${q.description}`, quest: q };
|
|
279
372
|
}
|
|
280
373
|
case QUEST_ACTIONS.revert: {
|
|
281
374
|
return this.revert();
|
package/src/quest/formatters.ts
CHANGED
|
@@ -2,7 +2,8 @@ export function formatQuestList(
|
|
|
2
2
|
quests: { id: number; description: string; done: boolean }[],
|
|
3
3
|
): string {
|
|
4
4
|
if (quests.length === 0) return "No quests.";
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
return quests.map((q, i) => `#${i + 1} [${q.done ? "x" : " "}] ${q.description}`).join("\n");
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export function formatAddResult(q: { id: number; description: string }): string {
|
package/src/quest/types.ts
CHANGED
|
@@ -9,6 +9,7 @@ export const QUEST_ACTIONS = {
|
|
|
9
9
|
update: "update",
|
|
10
10
|
delete: "delete",
|
|
11
11
|
clear: "clear",
|
|
12
|
+
reorder: "reorder",
|
|
12
13
|
revert: "revert",
|
|
13
14
|
} as const;
|
|
14
15
|
|
|
@@ -19,6 +20,7 @@ export const QUEST_ACTION_VALUES = [
|
|
|
19
20
|
QUEST_ACTIONS.update,
|
|
20
21
|
QUEST_ACTIONS.delete,
|
|
21
22
|
QUEST_ACTIONS.clear,
|
|
23
|
+
QUEST_ACTIONS.reorder,
|
|
22
24
|
QUEST_ACTIONS.revert,
|
|
23
25
|
] as const;
|
|
24
26
|
|
|
@@ -110,9 +110,11 @@ export class QuestListWidget {
|
|
|
110
110
|
|
|
111
111
|
const start = this.page * this.pageSize;
|
|
112
112
|
const pageQuests = this.quests.slice(start, start + this.pageSize);
|
|
113
|
-
for (
|
|
113
|
+
for (let i = 0; i < pageQuests.length; i++) {
|
|
114
|
+
const q = pageQuests[i];
|
|
115
|
+
const pos = start + i + 1;
|
|
114
116
|
const marker = q.done ? th.fg("success", " ✓ ") : th.fg("muted", " ○ ");
|
|
115
|
-
const idStr = th.fg(q.done ? "dim" : "accent", `#${
|
|
117
|
+
const idStr = th.fg(q.done ? "dim" : "accent", `#${pos}`);
|
|
116
118
|
const desc = q.done
|
|
117
119
|
? th.fg("dim", th.strikethrough(q.description))
|
|
118
120
|
: th.fg("text", q.description);
|
package/src/renderers/tools.ts
CHANGED
|
@@ -46,15 +46,20 @@ export function renderQuestResult(
|
|
|
46
46
|
isPartial: options.isPartial,
|
|
47
47
|
});
|
|
48
48
|
const details = result.details as Record<string, unknown> | undefined;
|
|
49
|
-
const
|
|
49
|
+
const allQuests =
|
|
50
|
+
(details?.quests as Array<{ id: number; description: string; done: boolean }> | undefined) ??
|
|
51
|
+
[];
|
|
52
|
+
const questsToRender = (details?.displayQuests ?? details?.quests) as
|
|
53
|
+
| Array<{ id: number; description: string; done: boolean }>
|
|
54
|
+
| undefined;
|
|
50
55
|
|
|
51
56
|
if (Array.isArray(questsToRender) && questsToRender.length > 0) {
|
|
52
|
-
const lines = (
|
|
53
|
-
(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
},
|
|
57
|
-
);
|
|
57
|
+
const lines = questsToRender.map((q) => {
|
|
58
|
+
const pos = allQuests.findIndex((x) => x.id === q.id) + 1 || 1;
|
|
59
|
+
const marker = q.done ? theme.fg("success", "✓") : theme.fg("dim", "○");
|
|
60
|
+
|
|
61
|
+
return `${marker} ${theme.fg("text", `#${pos}`)} ${theme.fg("muted", q.description)}`;
|
|
62
|
+
});
|
|
58
63
|
|
|
59
64
|
return new Text(lines.join("\n"), 0, 0);
|
|
60
65
|
}
|
package/src/tools/handler.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentToolResult, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import type { Static } from "@sinclair/typebox";
|
|
3
3
|
import { logger } from "../logger.js";
|
|
4
|
+
import { QUEST_PROMPT_GATE, QUEST_PROMPT_REMINDER } from "../prompts.js";
|
|
4
5
|
import { makeToolResult, type QuestAction, type QuestLog } from "../quest/dataplane.js";
|
|
5
6
|
import { QUEST_ACTIONS } from "../quest/types.js";
|
|
6
7
|
import { renderQuestCall, renderQuestResult } from "../renderers/tools.js";
|
|
@@ -37,8 +38,15 @@ const toolHandlers: {
|
|
|
37
38
|
[QUEST_ACTIONS.delete](questLog, params, toolCallId) {
|
|
38
39
|
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.delete, id: params.id });
|
|
39
40
|
},
|
|
40
|
-
[QUEST_ACTIONS.clear](questLog,
|
|
41
|
-
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.clear });
|
|
41
|
+
[QUEST_ACTIONS.clear](questLog, params, toolCallId) {
|
|
42
|
+
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.clear, all: params.all });
|
|
43
|
+
},
|
|
44
|
+
[QUEST_ACTIONS.reorder](questLog, params, toolCallId) {
|
|
45
|
+
return runTool(questLog, toolCallId, {
|
|
46
|
+
type: QUEST_ACTIONS.reorder,
|
|
47
|
+
id: params.id,
|
|
48
|
+
targetIndex: params.targetIndex,
|
|
49
|
+
});
|
|
42
50
|
},
|
|
43
51
|
[QUEST_ACTIONS.revert](questLog, _params, toolCallId) {
|
|
44
52
|
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.revert });
|
|
@@ -83,14 +91,7 @@ export function registerQuestTool(pi: ExtensionAPI, questLog: QuestLog): void {
|
|
|
83
91
|
description:
|
|
84
92
|
"Manage the session quest log. Use this VERY frequently to track tasks, plans, and progress throughout the conversation.",
|
|
85
93
|
promptSnippet: "Add, list, toggle, update, delete, clear, or revert quest items",
|
|
86
|
-
promptGuidelines: [
|
|
87
|
-
"Before reading files, running commands, or making edits, ensure the current work is tracked as specific, actionable quests.",
|
|
88
|
-
"Do not create a single vague quest for broad requests. Break them into concrete, independent steps.",
|
|
89
|
-
"When the user gives a plan or a list of tasks, add them as quests immediately.",
|
|
90
|
-
"It is critical that you toggle quests to done as soon as you complete them. Do NOT batch completions.",
|
|
91
|
-
"Before delegating to a minion, add a quest for the delegated task.",
|
|
92
|
-
"If you are unsure what to do next, use the list action to check active quests.",
|
|
93
|
-
],
|
|
94
|
+
promptGuidelines: [...QUEST_PROMPT_GATE, ...QUEST_PROMPT_REMINDER],
|
|
94
95
|
parameters: QuestParams,
|
|
95
96
|
execute: (toolCallId, params, _signal, _onUpdate, _ctx) =>
|
|
96
97
|
questToolExecute(questLog, toolCallId, params),
|
package/src/tools/params.ts
CHANGED
|
@@ -18,7 +18,18 @@ export const QuestParams = Type.Object({
|
|
|
18
18
|
),
|
|
19
19
|
id: Type.Optional(
|
|
20
20
|
Type.Number({
|
|
21
|
-
description: "Quest ID (required for toggle, update, delete actions)",
|
|
21
|
+
description: "Quest ID (required for toggle, update, delete, reorder actions)",
|
|
22
|
+
}),
|
|
23
|
+
),
|
|
24
|
+
targetIndex: Type.Optional(
|
|
25
|
+
Type.Number({
|
|
26
|
+
description:
|
|
27
|
+
"Target index for reorder action (0-based array index). If moving to the 3rd slot in the list, use 2.",
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
all: Type.Optional(
|
|
31
|
+
Type.Boolean({
|
|
32
|
+
description: "Clear all quests when true (defaults to clearing only completed quests)",
|
|
22
33
|
}),
|
|
23
34
|
),
|
|
24
35
|
});
|