pi-quests 0.2.0 → 0.4.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 +18 -0
- package/README.md +20 -1
- package/package.json +1 -1
- package/src/commands/handler.ts +36 -11
- package/src/commands/parse-args.ts +52 -22
- package/src/config.ts +148 -0
- package/src/index.ts +66 -26
- package/src/prompts.ts +19 -0
- package/src/quest/dataplane.ts +393 -72
- package/src/quest/formatters.ts +31 -16
- package/src/quest/tracker.ts +34 -25
- package/src/quest/types.ts +7 -2
- package/src/renderers/commands.ts +46 -22
- package/src/renderers/quests.ts +45 -0
- package/src/renderers/tools.ts +81 -27
- package/src/tools/handler.ts +25 -17
- package/src/tools/params.ts +44 -19
package/src/quest/formatters.ts
CHANGED
|
@@ -1,34 +1,49 @@
|
|
|
1
1
|
export function formatQuestList(
|
|
2
|
-
quests: { id:
|
|
2
|
+
quests: { id: string; description: string; done: boolean; parentId?: string }[],
|
|
3
3
|
): string {
|
|
4
4
|
if (quests.length === 0) return "No quests.";
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
const lines: string[] = [];
|
|
7
|
+
let pos = 1;
|
|
8
|
+
for (const q of quests) {
|
|
9
|
+
if (q.parentId) {
|
|
10
|
+
lines.push(` [${q.id}] [${q.done ? "x" : " "}] ${q.description}`);
|
|
11
|
+
} else {
|
|
12
|
+
lines.push(`#${pos} [${q.id}] [${q.done ? "x" : " "}] ${q.description}`);
|
|
13
|
+
pos++;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return lines.join("\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatAddResult(q: { id: string; description: string }): string {
|
|
20
|
+
return `Added quest [${q.id}]: ${q.description}`;
|
|
6
21
|
}
|
|
7
22
|
|
|
8
|
-
export function
|
|
9
|
-
return `Added
|
|
23
|
+
export function formatBatchAddResult(added: { id: string; description: string }[]): string {
|
|
24
|
+
return `Added ${added.length} quests:\n${added.map((q) => `[${q.id}]: ${q.description}`).join("\n")}`;
|
|
10
25
|
}
|
|
11
26
|
|
|
12
|
-
export function
|
|
13
|
-
return `
|
|
27
|
+
export function formatToggleResult(id: string, done: boolean): string {
|
|
28
|
+
return `Quest [${id}] ${done ? "done" : "undone"}`;
|
|
14
29
|
}
|
|
15
30
|
|
|
16
|
-
export function
|
|
17
|
-
return `
|
|
31
|
+
export function formatUpdateResult(q: { id: string; description: string }): string {
|
|
32
|
+
return `Updated quest [${q.id}]: ${q.description}`;
|
|
18
33
|
}
|
|
19
34
|
|
|
20
|
-
export function
|
|
21
|
-
return `
|
|
35
|
+
export function formatDeleteResult(q: { id: string; description: string }): string {
|
|
36
|
+
return `Deleted quest [${q.id}]: ${q.description}`;
|
|
22
37
|
}
|
|
23
38
|
|
|
24
|
-
export function
|
|
25
|
-
return `
|
|
39
|
+
export function formatNotFound(id: string): string {
|
|
40
|
+
return `Quest [${id}] not found`;
|
|
26
41
|
}
|
|
27
42
|
|
|
28
|
-
export function
|
|
29
|
-
return `
|
|
43
|
+
export function formatBlockedBySubQuests(id: string): string {
|
|
44
|
+
return `Quest [${id}] has incomplete sub-quests`;
|
|
30
45
|
}
|
|
31
46
|
|
|
32
|
-
export function
|
|
33
|
-
return `
|
|
47
|
+
export function formatSubQuestCannotHaveSubQuests(id: string): string {
|
|
48
|
+
return `Sub-quest [${id}] cannot have nested sub-quests`;
|
|
34
49
|
}
|
package/src/quest/tracker.ts
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
"implement",
|
|
3
|
-
"refactor",
|
|
4
|
-
"investigate",
|
|
5
|
-
"review",
|
|
6
|
-
"analyze",
|
|
7
|
-
"audit",
|
|
8
|
-
"plan",
|
|
9
|
-
"design",
|
|
10
|
-
"create",
|
|
11
|
-
"build",
|
|
12
|
-
"write",
|
|
13
|
-
"fix",
|
|
14
|
-
] as const;
|
|
1
|
+
import type { ResolvedConfig } from "../config.js";
|
|
15
2
|
|
|
16
3
|
const ACKNOWLEDGEMENT =
|
|
17
|
-
"ALWAYS acknowledge this reminder and create, update, or align on quests before making further tool calls.";
|
|
4
|
+
"ALWAYS acknowledge this reminder immediately and create, update, or align on quests before making further tool calls. DO NOT add this acknowledgement as another quest.";
|
|
18
5
|
|
|
19
6
|
export class QuestUsageTracker {
|
|
20
7
|
private totalToolCalls = 0;
|
|
@@ -23,6 +10,8 @@ export class QuestUsageTracker {
|
|
|
23
10
|
private lastQuestToolTime = 0;
|
|
24
11
|
private nudgedThisTurn = false;
|
|
25
12
|
|
|
13
|
+
constructor(private readonly config: ResolvedConfig) {}
|
|
14
|
+
|
|
26
15
|
onToolExecution(toolName: string): void {
|
|
27
16
|
this.totalToolCalls++;
|
|
28
17
|
if (toolName === "quest") {
|
|
@@ -38,11 +27,15 @@ export class QuestUsageTracker {
|
|
|
38
27
|
this.nudgedThisTurn = false;
|
|
39
28
|
}
|
|
40
29
|
|
|
41
|
-
getNudge(
|
|
30
|
+
getNudge(
|
|
31
|
+
activeQuestCount: number,
|
|
32
|
+
latestPrompt?: string,
|
|
33
|
+
hasTopLevelQuestWithoutSubs?: boolean,
|
|
34
|
+
): string | undefined {
|
|
42
35
|
if (this.nudgedThisTurn) return undefined;
|
|
43
36
|
|
|
44
37
|
// 1. Initialization nudge
|
|
45
|
-
if (this.totalToolCalls >=
|
|
38
|
+
if (this.totalToolCalls >= this.config.nudges.toolCallThreshold && !this.hasEverUsedQuestTool) {
|
|
46
39
|
this.nudgedThisTurn = true;
|
|
47
40
|
return `QUEST REMINDER: You have made ${this.totalToolCalls} tool calls but have NEVER used the quest tool this session. USE the quest tool to initialize tracking and break your work into concrete steps. ${ACKNOWLEDGEMENT}`;
|
|
48
41
|
}
|
|
@@ -57,24 +50,40 @@ export class QuestUsageTracker {
|
|
|
57
50
|
if (
|
|
58
51
|
this.hasEverUsedQuestTool &&
|
|
59
52
|
this.lastQuestToolTime > 0 &&
|
|
60
|
-
this.consecutiveNonQuestToolCalls >=
|
|
61
|
-
|
|
62
|
-
Date.now() - this.lastQuestToolTime >= 8 * 60 * 1000
|
|
53
|
+
this.consecutiveNonQuestToolCalls >= this.config.nudges.timeBasedToolCallThreshold &&
|
|
54
|
+
Date.now() - this.lastQuestToolTime >= this.config.nudges.hintIntervalMinutes * 60 * 1000
|
|
63
55
|
) {
|
|
64
56
|
this.nudgedThisTurn = true;
|
|
65
57
|
return `QUEST REMINDER: It has been a while since your last quest tool use and ${this.consecutiveNonQuestToolCalls} tools have been called since then. ALIGN on quest status before continuing. ${ACKNOWLEDGEMENT}`;
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
// 4. Zero-active sustained-work nudge
|
|
69
|
-
if (
|
|
61
|
+
if (
|
|
62
|
+
this.consecutiveNonQuestToolCalls >= this.config.nudges.zeroActiveToolCallThreshold &&
|
|
63
|
+
activeQuestCount === 0
|
|
64
|
+
) {
|
|
70
65
|
this.nudgedThisTurn = true;
|
|
71
66
|
return `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool and there are 0 active quests. TRACK your work with specific, actionable quests. ${ACKNOWLEDGEMENT}`;
|
|
72
67
|
}
|
|
73
68
|
|
|
74
|
-
// 5.
|
|
75
|
-
if (
|
|
69
|
+
// 5. Sub-quest suggestion nudge
|
|
70
|
+
if (
|
|
71
|
+
this.hasEverUsedQuestTool &&
|
|
72
|
+
this.consecutiveNonQuestToolCalls >= this.config.nudges.subQuestSuggestionToolCallThreshold &&
|
|
73
|
+
activeQuestCount > 0 &&
|
|
74
|
+
hasTopLevelQuestWithoutSubs
|
|
75
|
+
) {
|
|
76
|
+
this.nudgedThisTurn = true;
|
|
77
|
+
return `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool and have active top-level quests without sub-quests. Use the \`add\` action with \`parentId\` to break down complex tasks into smaller, trackable steps. ${ACKNOWLEDGEMENT}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 6. Stale-progress sustained-work nudge
|
|
81
|
+
if (
|
|
82
|
+
this.consecutiveNonQuestToolCalls >= this.config.nudges.staleProgressToolCallThreshold &&
|
|
83
|
+
activeQuestCount > 0
|
|
84
|
+
) {
|
|
76
85
|
this.nudgedThisTurn = true;
|
|
77
|
-
return `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool despite having active quests
|
|
86
|
+
return `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool despite having active quests.\nUPDATE your quest progress to reflect current status.\nALWAYS use sub quests to break down a quest into smaller steps, and to group related tasks together. ${ACKNOWLEDGEMENT}`;
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
return undefined;
|
|
@@ -82,6 +91,6 @@ export class QuestUsageTracker {
|
|
|
82
91
|
|
|
83
92
|
private isComplexPrompt(prompt: string): boolean {
|
|
84
93
|
const lower = prompt.toLowerCase();
|
|
85
|
-
return
|
|
94
|
+
return this.config.nudges.complexTaskKeywords.some((kw) => lower.includes(kw));
|
|
86
95
|
}
|
|
87
96
|
}
|
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,15 +20,19 @@ 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
|
|
|
25
27
|
export type QuestActionType = (typeof QUEST_ACTION_VALUES)[number];
|
|
26
28
|
|
|
27
29
|
export interface Quest {
|
|
28
|
-
id:
|
|
30
|
+
id: string;
|
|
29
31
|
description: string;
|
|
30
|
-
additionalContext?: string;
|
|
31
32
|
done: boolean;
|
|
32
33
|
createdAt: number;
|
|
33
34
|
}
|
|
35
|
+
|
|
36
|
+
export interface SubQuest extends Quest {
|
|
37
|
+
parentId: string;
|
|
38
|
+
}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { Key, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
3
|
+
import type { ResolvedConfig } from "../config.js";
|
|
3
4
|
import { logger } from "../logger.js";
|
|
4
|
-
import type {
|
|
5
|
+
import type { QuestLog } from "../quest/dataplane.js";
|
|
6
|
+
import { formatQuestRow, formatSubQuestSpacerLine } from "./quests.js";
|
|
5
7
|
|
|
6
8
|
export class QuestListWidget {
|
|
7
9
|
private cachedWidth?: number;
|
|
8
10
|
private cachedLines?: string[];
|
|
9
11
|
private page = 0;
|
|
10
|
-
private readonly pageSize = 10;
|
|
11
12
|
|
|
12
13
|
constructor(
|
|
13
|
-
private
|
|
14
|
+
private questLog: QuestLog,
|
|
14
15
|
private theme: Theme,
|
|
15
16
|
private onClose: () => void,
|
|
17
|
+
private readonly config: ResolvedConfig,
|
|
16
18
|
) {
|
|
17
|
-
logger.debug("quests:widget", "create", { questCount:
|
|
19
|
+
logger.debug("quests:widget", "create", { questCount: questLog.getAll().length });
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
handleInput(data: string): void {
|
|
@@ -37,7 +39,35 @@ export class QuestListWidget {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
private get totalPages(): number {
|
|
40
|
-
|
|
42
|
+
const totalLines = this.buildQuestLines(0).length;
|
|
43
|
+
return Math.max(1, Math.ceil(totalLines / this.config.display.pageSize));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private buildQuestLines(width: number): string[] {
|
|
47
|
+
const th = this.theme;
|
|
48
|
+
const parents = this.questLog.getQuests();
|
|
49
|
+
const lines: string[] = [];
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < parents.length; i++) {
|
|
52
|
+
const parent = parents[i];
|
|
53
|
+
const subs = this.questLog.getSubQuests(parent.id);
|
|
54
|
+
|
|
55
|
+
const row = formatQuestRow(th, parent, this.config.ids.length, i + 1);
|
|
56
|
+
lines.push(width > 0 ? truncateToWidth(row, width) : row);
|
|
57
|
+
|
|
58
|
+
if (subs.length > 0) {
|
|
59
|
+
lines.push(formatSubQuestSpacerLine(th, this.config.ids.length));
|
|
60
|
+
|
|
61
|
+
for (const sub of subs) {
|
|
62
|
+
const subRow = formatQuestRow(th, sub, this.config.ids.length);
|
|
63
|
+
lines.push(width > 0 ? truncateToWidth(subRow, width) : subRow);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
lines.push("");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return lines;
|
|
41
71
|
}
|
|
42
72
|
|
|
43
73
|
private nextPage(): void {
|
|
@@ -72,13 +102,14 @@ export class QuestListWidget {
|
|
|
72
102
|
width,
|
|
73
103
|
page: this.page,
|
|
74
104
|
totalPages: this.totalPages,
|
|
75
|
-
totalQuests: this.
|
|
105
|
+
totalQuests: this.questLog.getAll().length,
|
|
76
106
|
});
|
|
77
107
|
|
|
78
108
|
const th = this.theme;
|
|
79
109
|
const lines: string[] = [];
|
|
80
|
-
const
|
|
81
|
-
const
|
|
110
|
+
const allQuests = this.questLog.getAll();
|
|
111
|
+
const total = allQuests.length;
|
|
112
|
+
const doneCount = allQuests.filter((q) => q.done).length;
|
|
82
113
|
|
|
83
114
|
// Top accent border
|
|
84
115
|
lines.push(th.fg("accent", "─".repeat(width)));
|
|
@@ -91,7 +122,7 @@ export class QuestListWidget {
|
|
|
91
122
|
|
|
92
123
|
lines.push("");
|
|
93
124
|
|
|
94
|
-
if (
|
|
125
|
+
if (total === 0) {
|
|
95
126
|
lines.push(
|
|
96
127
|
truncateToWidth(
|
|
97
128
|
` ${th.fg("dim", "No active quests. Add one with /quests add <description>")}`,
|
|
@@ -100,7 +131,7 @@ export class QuestListWidget {
|
|
|
100
131
|
);
|
|
101
132
|
} else {
|
|
102
133
|
// Mini progress bar
|
|
103
|
-
const barWidth = Math.min(width - 4,
|
|
134
|
+
const barWidth = Math.min(width - 4, this.config.display.progressBarMaxWidth);
|
|
104
135
|
const filled = Math.round((doneCount / total) * barWidth);
|
|
105
136
|
const empty = barWidth - filled;
|
|
106
137
|
const bar = th.fg("success", "█".repeat(filled)) + th.fg("dim", "░".repeat(empty));
|
|
@@ -108,18 +139,11 @@ export class QuestListWidget {
|
|
|
108
139
|
lines.push(truncateToWidth(` ${bar} ${th.fg("muted", `${doneCount}/${total}`)}`, width));
|
|
109
140
|
lines.push("");
|
|
110
141
|
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const desc = q.done
|
|
117
|
-
? th.fg("dim", th.strikethrough(q.description))
|
|
118
|
-
: th.fg("text", q.description);
|
|
119
|
-
const row = `${marker}${idStr} ${desc}`;
|
|
120
|
-
|
|
121
|
-
lines.push(truncateToWidth(row, width));
|
|
122
|
-
}
|
|
142
|
+
const questLines = this.buildQuestLines(width);
|
|
143
|
+
const start = this.page * this.config.display.pageSize;
|
|
144
|
+
const pageLines = questLines.slice(start, start + this.config.display.pageSize);
|
|
145
|
+
|
|
146
|
+
lines.push(...pageLines);
|
|
123
147
|
|
|
124
148
|
if (this.totalPages > 1) {
|
|
125
149
|
const pageInfo = ` Page ${this.page + 1}/${this.totalPages} ${th.fg("dim", "· Tab/Shift+Tab to navigate")}`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { visibleWidth } from "@mariozechner/pi-tui";
|
|
3
|
+
import type { Quest, SubQuest } from "../quest/types.js";
|
|
4
|
+
|
|
5
|
+
export function formatQuestRow(
|
|
6
|
+
theme: Theme,
|
|
7
|
+
q: Quest | SubQuest,
|
|
8
|
+
idLength: number,
|
|
9
|
+
pos?: number,
|
|
10
|
+
): string {
|
|
11
|
+
const isSub = "parentId" in q && q.parentId;
|
|
12
|
+
|
|
13
|
+
// position test is only relevant for quests, not subquests
|
|
14
|
+
const posText = isSub ? "" : pos !== undefined ? `#${pos}` : "";
|
|
15
|
+
const posWidth = visibleWidth(posText);
|
|
16
|
+
const idText = `[${q.id}]`;
|
|
17
|
+
|
|
18
|
+
// spacing is added to keep text aligned
|
|
19
|
+
const spacing = " ".repeat(visibleWidth(`${16 ** idLength}`) - posWidth);
|
|
20
|
+
const idStr = `${theme.fg("muted", idText)} ${theme.fg(q.done ? "dim" : "accent", `${posText}`)}${spacing}`;
|
|
21
|
+
|
|
22
|
+
// for subquests, use a different marker and indent
|
|
23
|
+
const markerNotDone = isSub ? theme.fg("muted", " └── ○ ") : theme.fg("muted", " ○ ");
|
|
24
|
+
const markerDone = isSub ? theme.fg("muted", " └── ✓ ") : theme.fg("success", " ✓ ");
|
|
25
|
+
const marker = q.done ? markerDone : markerNotDone;
|
|
26
|
+
|
|
27
|
+
// for subquests, use dim text to contrast with the parent quests
|
|
28
|
+
const descColor = q.done || isSub ? "dim" : "text";
|
|
29
|
+
const desc = q.done
|
|
30
|
+
? theme.fg(descColor, theme.strikethrough(q.description))
|
|
31
|
+
: theme.fg(descColor, q.description);
|
|
32
|
+
|
|
33
|
+
const line = ` ${idStr}${marker} ${desc}`;
|
|
34
|
+
return line;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function formatSubQuestSpacerLine(theme: Theme, idLength: number): string {
|
|
38
|
+
const idTextLength = visibleWidth(`${16 ** idLength}`);
|
|
39
|
+
// spacing for `[id](position string)`
|
|
40
|
+
const spacerStr = `${" ".repeat(idTextLength + 2)}${" ".repeat(idTextLength)}`;
|
|
41
|
+
|
|
42
|
+
const marker = theme.fg("muted", " │ ");
|
|
43
|
+
|
|
44
|
+
return ` ${spacerStr}${marker}`;
|
|
45
|
+
}
|
package/src/renderers/tools.ts
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import type { AgentToolResult, Theme } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { Text } from "@mariozechner/pi-tui";
|
|
3
|
+
import type { ResolvedConfig } from "../config.js";
|
|
3
4
|
import { logger } from "../logger.js";
|
|
5
|
+
import type { Quest } from "../quest/types.js";
|
|
4
6
|
import { QUEST_ACTIONS } from "../quest/types.js";
|
|
7
|
+
import { formatQuestRow, formatSubQuestSpacerLine } from "./quests.js";
|
|
5
8
|
|
|
6
9
|
type QuestArgs = {
|
|
7
10
|
action: string;
|
|
8
11
|
descriptions?: string[];
|
|
9
|
-
|
|
12
|
+
targetId?: string;
|
|
13
|
+
id?: string;
|
|
14
|
+
all?: boolean;
|
|
10
15
|
};
|
|
11
16
|
|
|
12
17
|
export function renderQuestCall(args: QuestArgs, theme: Theme, _context: unknown): Text {
|
|
13
18
|
const id = "id" in args ? args.id : undefined;
|
|
14
19
|
logger.debug("quests:tool", "renderCall", { action: args.action, id });
|
|
15
|
-
const actionText =
|
|
20
|
+
const actionText =
|
|
21
|
+
theme.fg("toolTitle", theme.bold("quest ")) +
|
|
22
|
+
theme.fg("accent", args.action) +
|
|
23
|
+
theme.fg("muted", args.targetId ? ` ${args.targetId}` : "");
|
|
16
24
|
|
|
17
25
|
if (args.action === QUEST_ACTIONS.add && args.descriptions && args.descriptions.length > 0) {
|
|
18
26
|
const n = args.descriptions.length;
|
|
@@ -29,36 +37,82 @@ export function renderQuestCall(args: QuestArgs, theme: Theme, _context: unknown
|
|
|
29
37
|
args.action === QUEST_ACTIONS.delete) &&
|
|
30
38
|
args.id !== undefined
|
|
31
39
|
) {
|
|
32
|
-
return new Text(`${actionText} ${theme.fg("muted",
|
|
40
|
+
return new Text(`${actionText} ${theme.fg("muted", `[${args.id}]`)}`, 0, 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (args.action === QUEST_ACTIONS.clear) {
|
|
44
|
+
return new Text(`${actionText} ${theme.fg("muted", `[${args.all ? "all" : "done"}]`)}`, 0, 0);
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
return new Text(actionText, 0, 0);
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
export function renderQuestResult(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const lines = (questsToRender as Array<{ id: number; description: string; done: boolean }>).map(
|
|
53
|
-
(q) => {
|
|
54
|
-
const marker = q.done ? theme.fg("success", "✓") : theme.fg("dim", "○");
|
|
55
|
-
return `${marker} ${theme.fg("text", `#${q.id}`)} ${theme.fg("muted", q.description)}`;
|
|
56
|
-
},
|
|
57
|
-
);
|
|
50
|
+
export function renderQuestResult(config: ResolvedConfig) {
|
|
51
|
+
return (
|
|
52
|
+
result: AgentToolResult<unknown>,
|
|
53
|
+
options: { expanded: boolean; isPartial: boolean },
|
|
54
|
+
theme: Theme,
|
|
55
|
+
_context: unknown,
|
|
56
|
+
): Text => {
|
|
57
|
+
logger.debug("quests:tool", "renderResult", {
|
|
58
|
+
expanded: options.expanded,
|
|
59
|
+
isPartial: options.isPartial,
|
|
60
|
+
});
|
|
61
|
+
const details = result.details as Record<string, unknown> | undefined;
|
|
62
|
+
const allQuests = details?.quests as Quest[] | undefined;
|
|
63
|
+
const questsToRender = (details?.displayQuests ?? details?.quests) as Quest[] | undefined;
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
if (
|
|
66
|
+
!Array.isArray(questsToRender) ||
|
|
67
|
+
questsToRender.length === 0 ||
|
|
68
|
+
!Array.isArray(allQuests)
|
|
69
|
+
) {
|
|
70
|
+
const text = result.content.map((c) => (c.type === "text" ? c.text : "[image]")).join("\n");
|
|
71
|
+
return new Text(theme.fg("text", text), 0, 0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const quests = allQuests;
|
|
75
|
+
const renderedIds = new Set(questsToRender.map((q) => q.id));
|
|
76
|
+
const parents = quests.filter((q) => !(q as Quest & { parentId?: string }).parentId);
|
|
77
|
+
const lines: string[] = [];
|
|
78
|
+
|
|
79
|
+
function getSubQuests(parentId: string) {
|
|
80
|
+
return quests.filter((q) => (q as Quest & { parentId?: string }).parentId === parentId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function willRenderLater(idx: number): boolean {
|
|
84
|
+
for (let j = idx + 1; j < parents.length; j++) {
|
|
85
|
+
const p = parents[j];
|
|
86
|
+
if (renderedIds.has(p.id)) return true;
|
|
61
87
|
|
|
62
|
-
|
|
63
|
-
|
|
88
|
+
const subs = getSubQuests(p.id).filter((sq) => renderedIds.has(sq.id));
|
|
89
|
+
if (subs.length > 0) return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < parents.length; i++) {
|
|
96
|
+
const parent = parents[i];
|
|
97
|
+
const parentIncluded = renderedIds.has(parent.id);
|
|
98
|
+
const includedSubs = getSubQuests(parent.id).filter((sq) => renderedIds.has(sq.id));
|
|
99
|
+
|
|
100
|
+
if (!parentIncluded && includedSubs.length === 0) continue;
|
|
101
|
+
|
|
102
|
+
if (parentIncluded) {
|
|
103
|
+
lines.push(formatQuestRow(theme, parent, config.ids.length, i + 1));
|
|
104
|
+
if (includedSubs.length > 0) lines.push(formatSubQuestSpacerLine(theme, config.ids.length));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const sub of includedSubs) {
|
|
108
|
+
lines.push(formatQuestRow(theme, sub, config.ids.length));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (includedSubs.length > 0 && willRenderLater(i)) {
|
|
112
|
+
lines.push("");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
117
|
+
};
|
|
64
118
|
}
|
package/src/tools/handler.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { AgentToolResult, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import type { Static } from "@sinclair/typebox";
|
|
3
|
+
import type { ResolvedConfig } from "../config.js";
|
|
3
4
|
import { logger } from "../logger.js";
|
|
5
|
+
import { QUEST_PROMPT_GATE, QUEST_PROMPT_REMINDER } from "../prompts.js";
|
|
4
6
|
import { makeToolResult, type QuestAction, type QuestLog } from "../quest/dataplane.js";
|
|
5
7
|
import { QUEST_ACTIONS } from "../quest/types.js";
|
|
6
8
|
import { renderQuestCall, renderQuestResult } from "../renderers/tools.js";
|
|
7
|
-
import {
|
|
9
|
+
import { createQuestParams, type QuestParamsType } from "./params.js";
|
|
8
10
|
|
|
9
|
-
type QuestToolParams = Static<
|
|
11
|
+
type QuestToolParams = Static<QuestParamsType>;
|
|
10
12
|
|
|
11
13
|
const toolHandlers: {
|
|
12
14
|
[K in QuestToolParams["action"]]: (
|
|
@@ -19,6 +21,7 @@ const toolHandlers: {
|
|
|
19
21
|
return runTool(questLog, toolCallId, {
|
|
20
22
|
type: QUEST_ACTIONS.add,
|
|
21
23
|
descriptions: params.descriptions,
|
|
24
|
+
parentId: params.parentId,
|
|
22
25
|
});
|
|
23
26
|
},
|
|
24
27
|
[QUEST_ACTIONS.list](questLog, _params, toolCallId) {
|
|
@@ -37,8 +40,15 @@ const toolHandlers: {
|
|
|
37
40
|
[QUEST_ACTIONS.delete](questLog, params, toolCallId) {
|
|
38
41
|
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.delete, id: params.id });
|
|
39
42
|
},
|
|
40
|
-
[QUEST_ACTIONS.clear](questLog,
|
|
41
|
-
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.clear });
|
|
43
|
+
[QUEST_ACTIONS.clear](questLog, params, toolCallId) {
|
|
44
|
+
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.clear, all: params.all });
|
|
45
|
+
},
|
|
46
|
+
[QUEST_ACTIONS.reorder](questLog, params, toolCallId) {
|
|
47
|
+
return runTool(questLog, toolCallId, {
|
|
48
|
+
type: QUEST_ACTIONS.reorder,
|
|
49
|
+
id: params.id,
|
|
50
|
+
targetId: params.targetId,
|
|
51
|
+
});
|
|
42
52
|
},
|
|
43
53
|
[QUEST_ACTIONS.revert](questLog, _params, toolCallId) {
|
|
44
54
|
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.revert });
|
|
@@ -75,26 +85,24 @@ export async function questToolExecute(
|
|
|
75
85
|
return handler(questLog, params, toolCallId);
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
export function registerQuestTool(
|
|
88
|
+
export function registerQuestTool(
|
|
89
|
+
pi: ExtensionAPI,
|
|
90
|
+
questLog: QuestLog,
|
|
91
|
+
config: ResolvedConfig,
|
|
92
|
+
): void {
|
|
79
93
|
logger.debug("quests:tool", "register");
|
|
80
94
|
pi.registerTool({
|
|
81
95
|
name: "quest",
|
|
82
96
|
label: "Quest",
|
|
83
97
|
description:
|
|
84
|
-
"Manage the session quest log. Use this VERY frequently to track tasks, plans, and progress throughout the conversation.",
|
|
85
|
-
promptSnippet:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
parameters: QuestParams,
|
|
98
|
+
"Manage the session quest log including top-level quests and sub-quests. Use this VERY frequently to track tasks, plans, and progress throughout the conversation.",
|
|
99
|
+
promptSnippet:
|
|
100
|
+
"Add (with optional parentId for sub-quests), list, toggle, update, delete, clear, or revert quest items",
|
|
101
|
+
promptGuidelines: [...QUEST_PROMPT_GATE, ...QUEST_PROMPT_REMINDER],
|
|
102
|
+
parameters: createQuestParams(config.ids.length),
|
|
95
103
|
execute: (toolCallId, params, _signal, _onUpdate, _ctx) =>
|
|
96
104
|
questToolExecute(questLog, toolCallId, params),
|
|
97
105
|
renderCall: renderQuestCall,
|
|
98
|
-
renderResult: renderQuestResult,
|
|
106
|
+
renderResult: renderQuestResult(config),
|
|
99
107
|
});
|
|
100
108
|
}
|
package/src/tools/params.ts
CHANGED
|
@@ -2,23 +2,48 @@ import { StringEnum } from "@mariozechner/pi-ai";
|
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import { QUEST_ACTION_VALUES } from "../quest/types.js";
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Type.Array(Type.String(), {
|
|
11
|
-
description: "Array of quest descriptions (required for add action)",
|
|
5
|
+
export function createQuestParams(idLength: number) {
|
|
6
|
+
const pattern = `^[0-9a-f]{${idLength}}$`;
|
|
7
|
+
return Type.Object({
|
|
8
|
+
action: StringEnum(QUEST_ACTION_VALUES, {
|
|
9
|
+
description: "The quest action to perform",
|
|
12
10
|
}),
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
descriptions: Type.Optional(
|
|
12
|
+
Type.Array(Type.String(), {
|
|
13
|
+
description: "Array of quest descriptions (required for add action)",
|
|
14
|
+
}),
|
|
15
|
+
),
|
|
16
|
+
description: Type.Optional(
|
|
17
|
+
Type.String({
|
|
18
|
+
description: "New description (required for update action)",
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
parentId: Type.Optional(
|
|
22
|
+
Type.String({
|
|
23
|
+
pattern,
|
|
24
|
+
description:
|
|
25
|
+
"Parent quest hex ID. Use this to break a large task into smaller, trackable steps. When provided, the added quest becomes a sub-quest under the parent.",
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
id: Type.Optional(
|
|
29
|
+
Type.String({
|
|
30
|
+
pattern,
|
|
31
|
+
description: `Quest ID (required for toggle, update, delete, reorder actions). ALWAYS use the ${idLength}-digit hex ID shown in brackets, never the positional number.`,
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
targetId: Type.Optional(
|
|
35
|
+
Type.String({
|
|
36
|
+
pattern,
|
|
37
|
+
description:
|
|
38
|
+
"Target quest ID for reorder action. The quest will be moved to just before the target quest.",
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
|
+
all: Type.Optional(
|
|
42
|
+
Type.Boolean({
|
|
43
|
+
description: "Clear all quests when true (defaults to clearing only completed quests)",
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type QuestParamsType = ReturnType<typeof createQuestParams>;
|