pi-quests 0.5.0 → 0.6.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 +9 -0
- package/README.md +1 -1
- package/package.json +5 -3
- package/src/commands/handler.ts +9 -2
- package/src/config.ts +7 -9
- package/src/index.ts +24 -6
- package/src/prompts.ts +10 -16
- package/src/quest/tracker.ts +45 -7
- package/src/renderers/commands.ts +9 -1
- package/src/renderers/status.ts +34 -0
- package/src/tools/handler.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.6.0] - 2026-04-17
|
|
6
|
+
|
|
7
|
+
- feat: add footer quest progress indicator with configurable icon
|
|
8
|
+
- fix: quest list widget cache invalidation
|
|
9
|
+
|
|
10
|
+
## [0.5.1] - 2026-04-17
|
|
11
|
+
|
|
12
|
+
- fix: add context signals and calibrate nudge thresholds
|
|
13
|
+
|
|
5
14
|
## [0.5.0] - 2026-04-14
|
|
6
15
|
|
|
7
16
|
- feat: add reparent action to promote, demote, or move quests and steps with validation and revert support
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-quests",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A quest-log for your pi. Keep your agent on track, one quest at a time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -32,8 +32,10 @@
|
|
|
32
32
|
"LICENSE.md"
|
|
33
33
|
],
|
|
34
34
|
"peerDependencies": {
|
|
35
|
+
"@mariozechner/pi-ai": "*",
|
|
35
36
|
"@mariozechner/pi-coding-agent": "*",
|
|
36
|
-
"@mariozechner/pi-tui": "*"
|
|
37
|
+
"@mariozechner/pi-tui": "*",
|
|
38
|
+
"@sinclair/typebox": "*"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@biomejs/biome": "^2.4.10",
|
|
@@ -49,6 +51,6 @@
|
|
|
49
51
|
"style": "biome check ./src ./test",
|
|
50
52
|
"style:fix": "biome check --write --unsafe --no-errors-on-unmatched ./src ./test",
|
|
51
53
|
"format": "biome format --write --no-errors-on-unmatched ./src ./test",
|
|
52
|
-
"prepublishOnly": "
|
|
54
|
+
"prepublishOnly": "npx tsc --noEmit && npx vitest run"
|
|
53
55
|
}
|
|
54
56
|
}
|
package/src/commands/handler.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { ResolvedConfig } from "../config.js";
|
|
|
8
8
|
import { logger } from "../logger.js";
|
|
9
9
|
import type { QuestAction, QuestLog } from "../quest/dataplane.js";
|
|
10
10
|
import { QUEST_ACTIONS } from "../quest/types.js";
|
|
11
|
-
import { QuestListWidget } from "../renderers/commands.js";
|
|
11
|
+
import { invalidateQuestListWidget, QuestListWidget } from "../renderers/commands.js";
|
|
12
12
|
import { CHANGELOG_PATH, getVersion } from "../version.js";
|
|
13
13
|
import { reverseChangelog } from "./changelog.js";
|
|
14
14
|
import { type ParsedArgs, parseQuestArgs } from "./parse-args.js";
|
|
@@ -73,7 +73,12 @@ export function openQuestList(
|
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export function createQuestsHandler(
|
|
76
|
+
export function createQuestsHandler(
|
|
77
|
+
pi: ExtensionAPI,
|
|
78
|
+
questLog: QuestLog,
|
|
79
|
+
config: ResolvedConfig,
|
|
80
|
+
onMutate?: (ctx: ExtensionCommandContext) => void,
|
|
81
|
+
) {
|
|
77
82
|
return async function handler(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
78
83
|
logger.debug("quests:cmd", "handler", { args, hasUI: ctx.hasUI });
|
|
79
84
|
|
|
@@ -144,6 +149,8 @@ export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog, config
|
|
|
144
149
|
const result = questLog.execute(action);
|
|
145
150
|
|
|
146
151
|
logger.debug("quests:cmd", parsed.action, { success: result.success });
|
|
152
|
+
invalidateQuestListWidget();
|
|
153
|
+
onMutate?.(ctx);
|
|
147
154
|
ctx.ui.notify(result.message, result.success ? "info" : "error");
|
|
148
155
|
return;
|
|
149
156
|
}
|
package/src/config.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
|
5
5
|
|
|
6
6
|
export interface ResolvedConfig {
|
|
7
7
|
ids: { length: number };
|
|
8
|
-
display: { pageSize: number; progressBarMaxWidth: number };
|
|
8
|
+
display: { pageSize: number; progressBarMaxWidth: number; icon: string };
|
|
9
9
|
nudges: {
|
|
10
10
|
toolCallThreshold: number;
|
|
11
11
|
hintIntervalMinutes: number;
|
|
@@ -23,28 +23,25 @@ export const DEFAULT_COMPLEX_TASK_KEYWORDS = [
|
|
|
23
23
|
"implement",
|
|
24
24
|
"refactor",
|
|
25
25
|
"investigate",
|
|
26
|
-
"review",
|
|
27
26
|
"analyze",
|
|
28
27
|
"audit",
|
|
29
|
-
"plan",
|
|
30
28
|
"design",
|
|
31
|
-
"create",
|
|
32
|
-
"build",
|
|
33
|
-
"write",
|
|
34
29
|
"fix",
|
|
30
|
+
"plan",
|
|
31
|
+
"build",
|
|
35
32
|
] as const;
|
|
36
33
|
|
|
37
34
|
export const DEFAULT_FAKE_DONE_PATTERN = String.raw`\s[-\u2013\u2014]\s*(DONE|COMPLETED|FINISHED)$|\s[([](DONE|COMPLETED|FINISHED)[)\]]$`;
|
|
38
35
|
|
|
39
36
|
export const DEFAULT_CONFIG: ResolvedConfig = {
|
|
40
37
|
ids: { length: 2 },
|
|
41
|
-
display: { pageSize: 10, progressBarMaxWidth: 24 },
|
|
38
|
+
display: { pageSize: 10, progressBarMaxWidth: 24, icon: "" },
|
|
42
39
|
nudges: {
|
|
43
40
|
toolCallThreshold: 8,
|
|
44
41
|
hintIntervalMinutes: 4,
|
|
45
|
-
timeBasedToolCallThreshold:
|
|
42
|
+
timeBasedToolCallThreshold: 8,
|
|
46
43
|
zeroActiveToolCallThreshold: 8,
|
|
47
|
-
staleProgressToolCallThreshold:
|
|
44
|
+
staleProgressToolCallThreshold: 12,
|
|
48
45
|
stepSuggestionToolCallThreshold: 10,
|
|
49
46
|
complexTaskKeywords: [...DEFAULT_COMPLEX_TASK_KEYWORDS],
|
|
50
47
|
},
|
|
@@ -117,6 +114,7 @@ export function getConfig(ctx: Pick<ExtensionContext, "cwd">): ResolvedConfig {
|
|
|
117
114
|
pageSize: user.display?.pageSize ?? DEFAULT_CONFIG.display.pageSize,
|
|
118
115
|
progressBarMaxWidth:
|
|
119
116
|
user.display?.progressBarMaxWidth ?? DEFAULT_CONFIG.display.progressBarMaxWidth,
|
|
117
|
+
icon: user.display?.icon ?? DEFAULT_CONFIG.display.icon,
|
|
120
118
|
},
|
|
121
119
|
nudges: {
|
|
122
120
|
toolCallThreshold: user.nudges?.toolCallThreshold ?? DEFAULT_CONFIG.nudges.toolCallThreshold,
|
package/src/index.ts
CHANGED
|
@@ -4,11 +4,12 @@ import type { KeyId } from "@mariozechner/pi-tui";
|
|
|
4
4
|
import { createQuestsHandler, openQuestList } from "./commands/handler.js";
|
|
5
5
|
import { DEFAULT_CONFIG, getConfig, type ResolvedConfig } from "./config.js";
|
|
6
6
|
import { logger } from "./logger.js";
|
|
7
|
-
import { QUEST_PROMPT_GATE
|
|
7
|
+
import { QUEST_PROMPT_GATE } from "./prompts.js";
|
|
8
8
|
import { QuestLog } from "./quest/dataplane.js";
|
|
9
9
|
import { formatQuestList } from "./quest/formatters.js";
|
|
10
10
|
import { QuestUsageTracker } from "./quest/tracker.js";
|
|
11
11
|
import { questChangelogRenderer } from "./renderers/changelog.js";
|
|
12
|
+
import { QuestStatusWidget } from "./renderers/status.js";
|
|
12
13
|
import { registerQuestTool } from "./tools/handler.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -25,6 +26,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
25
26
|
let questLog = new QuestLog();
|
|
26
27
|
let tracker = new QuestUsageTracker(DEFAULT_CONFIG);
|
|
27
28
|
let config: ResolvedConfig = DEFAULT_CONFIG;
|
|
29
|
+
let statusWidget = new QuestStatusWidget(DEFAULT_CONFIG.display.icon);
|
|
28
30
|
|
|
29
31
|
const shortcutKey = getConfig({ cwd: process.cwd() }).shortcuts?.openQuests ?? "ctrl+shift+l";
|
|
30
32
|
logger.debug("quests:shortcut", "register", { key: shortcutKey });
|
|
@@ -47,23 +49,33 @@ export default function (pi: ExtensionAPI): void {
|
|
|
47
49
|
config = getConfig(ctx);
|
|
48
50
|
questLog = new QuestLog(config);
|
|
49
51
|
tracker = new QuestUsageTracker(config);
|
|
52
|
+
statusWidget = new QuestStatusWidget(config.display.icon);
|
|
50
53
|
questLog.reconstructFromSession(ctx);
|
|
54
|
+
statusWidget.update(questLog, ctx.ui, ctx.ui.theme);
|
|
51
55
|
|
|
52
56
|
registerQuestTool(pi, questLog, config);
|
|
53
57
|
|
|
54
|
-
const questsHandler = createQuestsHandler(pi, questLog, config)
|
|
58
|
+
const questsHandler = createQuestsHandler(pi, questLog, config, (ctx) => {
|
|
59
|
+
statusWidget.update(questLog, ctx.ui, ctx.ui.theme);
|
|
60
|
+
});
|
|
55
61
|
pi.registerCommand("quests", {
|
|
56
62
|
description: "Quest commands: /quests [help] to see usage",
|
|
57
63
|
handler: questsHandler,
|
|
58
64
|
});
|
|
59
65
|
});
|
|
60
66
|
|
|
61
|
-
pi.on("session_tree", async (_event, ctx) =>
|
|
67
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
68
|
+
questLog.reconstructFromSession(ctx);
|
|
69
|
+
statusWidget.update(questLog, ctx.ui, ctx.ui.theme);
|
|
70
|
+
});
|
|
62
71
|
|
|
63
72
|
pi.on("turn_start", async () => tracker.clearTurnNudge());
|
|
64
73
|
|
|
65
|
-
pi.on("tool_execution_end", async (event) => {
|
|
74
|
+
pi.on("tool_execution_end", async (event, ctx) => {
|
|
66
75
|
tracker.onToolExecution(event.toolName);
|
|
76
|
+
if (event.toolName === "quest") {
|
|
77
|
+
statusWidget.update(questLog, ctx.ui, ctx.ui.theme);
|
|
78
|
+
}
|
|
67
79
|
});
|
|
68
80
|
|
|
69
81
|
pi.on("context", async (event) => {
|
|
@@ -79,7 +91,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
79
91
|
const hasTopLevelQuestWithoutSubs = activeTopLevel.some(
|
|
80
92
|
(q) => !allQuests.some((step) => (step as { parentId?: string }).parentId === q.id),
|
|
81
93
|
);
|
|
82
|
-
const nudge = tracker.getNudge(
|
|
94
|
+
const nudge = tracker.getNudge(
|
|
95
|
+
activeQuestCount,
|
|
96
|
+
allQuests,
|
|
97
|
+
latestPrompt,
|
|
98
|
+
hasTopLevelQuestWithoutSubs,
|
|
99
|
+
);
|
|
83
100
|
|
|
84
101
|
const fakeDoneRegex = new RegExp(config.validation.fakeDonePattern, "i");
|
|
85
102
|
const fakeDone = questLog.getAll().find((q) => !q.done && fakeDoneRegex.test(q.description));
|
|
@@ -106,7 +123,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
106
123
|
const list = formatQuestList(quests);
|
|
107
124
|
reminder = `Active quests (${remaining}/${quests.length}):\n${list}\n\nKeep quest progress updated as you work.`;
|
|
108
125
|
} else {
|
|
109
|
-
reminder =
|
|
126
|
+
reminder =
|
|
127
|
+
"No active quests. Use the quest tool to track your work. Use action: 'skill' for usage documentation.";
|
|
110
128
|
}
|
|
111
129
|
|
|
112
130
|
return {
|
package/src/prompts.ts
CHANGED
|
@@ -9,22 +9,16 @@ export function getQuestSkillDocument(): string {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export const QUEST_PROMPT_REMINDER = [
|
|
12
|
-
"Use the quest tool VERY frequently to track tasks, plans, and progress
|
|
13
|
-
"Before reading files
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"Before delegating to a minion, add a quest for the delegated task.",
|
|
22
|
-
"As work evolves, use the reorder action to reflect changes in priority",
|
|
23
|
-
"For reorder, provide the targetId (the hex ID of the quest to insert before).",
|
|
24
|
-
"If you are unsure what to do next, use the list action to check active quests.",
|
|
25
|
-
"Always use the hex ID shown in brackets (e.g. 0a, ff, 44e1, f712a) for toggle, update, delete, and reorder actions.",
|
|
26
|
-
"When you need to understand the quest system, rules, or best practices, use the quest tool with action: 'skill' or action: 'rules' rather than reading documentation files.",
|
|
12
|
+
"Use the quest tool VERY frequently to track tasks, plans, and progress.",
|
|
13
|
+
"Before reading files or making edits, ensure work is tracked as specific, actionable quests.",
|
|
14
|
+
"Break broad requests into concrete, independent quests. Use split to break quests into steps for multi-step tasks. When the user gives a list of tasks, add them as quests immediately.",
|
|
15
|
+
"Toggle quests done as you complete them. Do NOT batch completions.",
|
|
16
|
+
"ALWAYS use the toggle action for completion. NEVER append completion markers via update.",
|
|
17
|
+
"A parent quest cannot be toggled done until all its steps are complete.",
|
|
18
|
+
"Always use the hex ID shown in brackets (e.g. 0a, ff) for actions, never the positional number.",
|
|
19
|
+
"When reading a skill file, plan, or protocol with numbered steps, add those steps as quests immediately.",
|
|
20
|
+
"Use the quest tool with action: 'skill' for complete usage documentation, patterns, and best practices.",
|
|
27
21
|
] as const;
|
|
28
22
|
|
|
29
23
|
export const QUEST_PROMPT_GATE =
|
|
30
|
-
"Before reading files, running commands, or making edits, ALWAYS ensure the current work is tracked as specific, actionable quests
|
|
24
|
+
"Before reading files, running commands, or making edits, ALWAYS ensure the current work is tracked as specific, actionable quests.\nALWAYS breakdown broad quests into smaller steps.";
|
package/src/quest/tracker.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import type { ResolvedConfig } from "../config.js";
|
|
2
|
+
import { logger } from "../logger.js";
|
|
3
|
+
import type { Quest } from "./types.js";
|
|
2
4
|
|
|
3
5
|
const ACKNOWLEDGEMENT = "Update your quest status before continuing.";
|
|
4
6
|
|
|
7
|
+
function formatActiveQuests(allQuests: Quest[], limit = 3): string {
|
|
8
|
+
const active = allQuests.filter((q) => !q.done);
|
|
9
|
+
if (active.length === 0) return "";
|
|
10
|
+
|
|
11
|
+
const shown = active.slice(0, limit);
|
|
12
|
+
const lines = shown.map((q) => ` [${q.id}]: ${q.description}`);
|
|
13
|
+
if (active.length > limit) lines.push(` ... and ${active.length - limit} more`);
|
|
14
|
+
|
|
15
|
+
return `\nActive quests:\n${lines.join("\n")}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
type NudgeCandidate = { index: number; message: string };
|
|
6
19
|
|
|
7
20
|
export class QuestUsageTracker {
|
|
@@ -32,23 +45,38 @@ export class QuestUsageTracker {
|
|
|
32
45
|
|
|
33
46
|
getNudge(
|
|
34
47
|
activeQuestCount: number,
|
|
48
|
+
allQuests: Quest[],
|
|
35
49
|
latestPrompt?: string,
|
|
36
50
|
hasTopLevelQuestWithoutSubs?: boolean,
|
|
37
51
|
): string | undefined {
|
|
38
|
-
if (this.nudgedThisTurn)
|
|
52
|
+
if (this.nudgedThisTurn) {
|
|
53
|
+
logger.debug("quests:tracker", "nudge-suppressed", { reason: "turn-limit" });
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
39
56
|
|
|
40
57
|
const now = Date.now();
|
|
41
58
|
const cooldownMs = this.config.nudges.hintIntervalMinutes * 60 * 1000;
|
|
42
59
|
if (this.lastNudgeTime > 0 && now - this.lastNudgeTime < cooldownMs) {
|
|
60
|
+
logger.debug("quests:tracker", "nudge-suppressed", {
|
|
61
|
+
reason: "cooldown",
|
|
62
|
+
elapsed: now - this.lastNudgeTime,
|
|
63
|
+
cooldownMs,
|
|
64
|
+
});
|
|
43
65
|
return undefined;
|
|
44
66
|
}
|
|
45
67
|
|
|
46
68
|
const eligible = this.getEligibleNudges(
|
|
47
69
|
activeQuestCount,
|
|
70
|
+
allQuests,
|
|
48
71
|
latestPrompt,
|
|
49
72
|
hasTopLevelQuestWithoutSubs,
|
|
50
73
|
);
|
|
51
74
|
|
|
75
|
+
logger.debug("quests:tracker", "nudge-candidates", {
|
|
76
|
+
eligible: eligible.length,
|
|
77
|
+
indices: eligible.map((c) => c.index),
|
|
78
|
+
});
|
|
79
|
+
|
|
52
80
|
// Rotate priority: start checking from the nudge after the last one that fired
|
|
53
81
|
const rotated = [
|
|
54
82
|
...eligible.filter((n) => n.index > this.lastNudgeIndex),
|
|
@@ -59,6 +87,11 @@ export class QuestUsageTracker {
|
|
|
59
87
|
if (winner) {
|
|
60
88
|
this.nudgedThisTurn = true;
|
|
61
89
|
this.lastNudgeTime = now;
|
|
90
|
+
|
|
91
|
+
logger.debug("quests:tracker", "nudge-fired", {
|
|
92
|
+
winner: winner.index,
|
|
93
|
+
rotatedFrom: this.lastNudgeIndex,
|
|
94
|
+
});
|
|
62
95
|
this.lastNudgeIndex = winner.index;
|
|
63
96
|
return winner.message;
|
|
64
97
|
}
|
|
@@ -68,6 +101,7 @@ export class QuestUsageTracker {
|
|
|
68
101
|
|
|
69
102
|
private getEligibleNudges(
|
|
70
103
|
activeQuestCount: number,
|
|
104
|
+
allQuests: Quest[],
|
|
71
105
|
latestPrompt?: string,
|
|
72
106
|
hasTopLevelQuestWithoutSubs?: boolean,
|
|
73
107
|
): NudgeCandidate[] {
|
|
@@ -85,7 +119,7 @@ export class QuestUsageTracker {
|
|
|
85
119
|
if (activeQuestCount === 0 && latestPrompt && this.isComplexPrompt(latestPrompt)) {
|
|
86
120
|
candidates.push({
|
|
87
121
|
index: 1,
|
|
88
|
-
message: `QUEST REMINDER: Your latest prompt
|
|
122
|
+
message: `QUEST REMINDER: Your latest prompt is a complex task, but there are 0 active quests. USE the quest tool to break this into concrete, trackable steps. ${ACKNOWLEDGEMENT}`,
|
|
89
123
|
});
|
|
90
124
|
}
|
|
91
125
|
|
|
@@ -96,9 +130,10 @@ export class QuestUsageTracker {
|
|
|
96
130
|
this.consecutiveNonQuestToolCalls >= this.config.nudges.timeBasedToolCallThreshold &&
|
|
97
131
|
Date.now() - this.lastQuestToolTime >= this.config.nudges.hintIntervalMinutes * 60 * 1000
|
|
98
132
|
) {
|
|
133
|
+
const questContext = formatActiveQuests(allQuests);
|
|
99
134
|
candidates.push({
|
|
100
135
|
index: 2,
|
|
101
|
-
message: `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
|
|
136
|
+
message: `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.${questContext} ${ACKNOWLEDGEMENT}`,
|
|
102
137
|
});
|
|
103
138
|
}
|
|
104
139
|
|
|
@@ -122,18 +157,21 @@ export class QuestUsageTracker {
|
|
|
122
157
|
) {
|
|
123
158
|
candidates.push({
|
|
124
159
|
index: 4,
|
|
125
|
-
message: `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool and have active top-level quests without steps.
|
|
160
|
+
message: `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool and have active top-level quests without steps. Consider the quest complexity and use the \`split\` action to break them into smaller steps and track progress. ${ACKNOWLEDGEMENT}`,
|
|
126
161
|
});
|
|
127
162
|
}
|
|
128
163
|
|
|
129
|
-
// 5. Stale-progress sustained-work nudge
|
|
164
|
+
// 5. Stale-progress sustained-work nudge (with time-gate)
|
|
130
165
|
if (
|
|
131
166
|
this.consecutiveNonQuestToolCalls >= this.config.nudges.staleProgressToolCallThreshold &&
|
|
132
|
-
activeQuestCount > 0
|
|
167
|
+
activeQuestCount > 0 &&
|
|
168
|
+
this.lastQuestToolTime > 0 &&
|
|
169
|
+
Date.now() - this.lastQuestToolTime >= this.config.nudges.hintIntervalMinutes * 60 * 1000
|
|
133
170
|
) {
|
|
171
|
+
const questContext = formatActiveQuests(allQuests);
|
|
134
172
|
candidates.push({
|
|
135
173
|
index: 5,
|
|
136
|
-
message: `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool despite having active quests
|
|
174
|
+
message: `QUEST REMINDER: You have made ${this.consecutiveNonQuestToolCalls} consecutive tool calls without using the quest tool despite having active quests. UPDATE your quest progress to reflect current status.${questContext} ${ACKNOWLEDGEMENT}`,
|
|
137
175
|
});
|
|
138
176
|
}
|
|
139
177
|
|
|
@@ -5,6 +5,12 @@ import { logger } from "../logger.js";
|
|
|
5
5
|
import type { QuestLog } from "../quest/dataplane.js";
|
|
6
6
|
import { formatQuestRow, formatStepSpacerLine } from "./quests.js";
|
|
7
7
|
|
|
8
|
+
let activeQuestListWidget: QuestListWidget | undefined;
|
|
9
|
+
|
|
10
|
+
export function invalidateQuestListWidget(): void {
|
|
11
|
+
activeQuestListWidget?.invalidate();
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
export class QuestListWidget {
|
|
9
15
|
private cachedWidth?: number;
|
|
10
16
|
private cachedLines?: string[];
|
|
@@ -17,12 +23,14 @@ export class QuestListWidget {
|
|
|
17
23
|
private readonly config: ResolvedConfig,
|
|
18
24
|
) {
|
|
19
25
|
logger.debug("quests:widget", "create", { questCount: questLog.getAll().length });
|
|
26
|
+
activeQuestListWidget = this;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
handleInput(data: string): void {
|
|
23
30
|
logger.debug("quests:widget", "handleInput", { data });
|
|
24
31
|
if (matchesKey(data, Key.escape) || data === "q" || data === "Q") {
|
|
25
32
|
logger.debug("quests:widget", "close");
|
|
33
|
+
activeQuestListWidget = undefined;
|
|
26
34
|
this.onClose();
|
|
27
35
|
return;
|
|
28
36
|
}
|
|
@@ -134,7 +142,7 @@ export class QuestListWidget {
|
|
|
134
142
|
const barWidth = Math.min(width - 4, this.config.display.progressBarMaxWidth);
|
|
135
143
|
const filled = Math.round((doneCount / total) * barWidth);
|
|
136
144
|
const empty = barWidth - filled;
|
|
137
|
-
const bar = th.fg("success", "
|
|
145
|
+
const bar = th.fg("success", "▰".repeat(filled)) + th.fg("dim", "▱".repeat(empty));
|
|
138
146
|
|
|
139
147
|
lines.push(truncateToWidth(` ${bar} ${th.fg("muted", `${doneCount}/${total}`)}`, width));
|
|
140
148
|
lines.push("");
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { QuestLog } from "../quest/dataplane.js";
|
|
3
|
+
|
|
4
|
+
export class QuestStatusWidget {
|
|
5
|
+
private readonly key = "pi-quests";
|
|
6
|
+
private readonly barWidth = 5;
|
|
7
|
+
|
|
8
|
+
constructor(private readonly icon: string) {}
|
|
9
|
+
|
|
10
|
+
update(
|
|
11
|
+
questLog: QuestLog,
|
|
12
|
+
ui: { setStatus(key: string, text: string | undefined): void },
|
|
13
|
+
theme: Theme,
|
|
14
|
+
): void {
|
|
15
|
+
const all = questLog.getAll();
|
|
16
|
+
const total = all.length;
|
|
17
|
+
const done = all.filter((q) => q.done).length;
|
|
18
|
+
|
|
19
|
+
const text = this.formatStatus(total, done, theme);
|
|
20
|
+
ui.setStatus(this.key, text);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private formatStatus(total: number, done: number, theme: Theme): string | undefined {
|
|
24
|
+
if (total === 0) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const filled = Math.round((done / total) * this.barWidth);
|
|
29
|
+
const empty = this.barWidth - filled;
|
|
30
|
+
const bar = theme.fg("success", "▰".repeat(filled)) + theme.fg("muted", "▱".repeat(empty));
|
|
31
|
+
|
|
32
|
+
return `${theme.fg("accent", this.icon)} ${bar} ${theme.fg("dim", `${done}/${total}`)}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/tools/handler.ts
CHANGED
|
@@ -14,6 +14,7 @@ const SPLIT_DISPLAY_ACTIONS = [
|
|
|
14
14
|
QUEST_ACTIONS.revert,
|
|
15
15
|
] as const;
|
|
16
16
|
|
|
17
|
+
import { invalidateQuestListWidget } from "../renderers/commands.js";
|
|
17
18
|
import { renderQuestCall, renderQuestResult } from "../renderers/tools.js";
|
|
18
19
|
import { createQuestParams, type QuestParamsType } from "./params.js";
|
|
19
20
|
|
|
@@ -96,6 +97,7 @@ function runTool(
|
|
|
96
97
|
action: QuestAction,
|
|
97
98
|
): AgentToolResult<unknown> {
|
|
98
99
|
const result = questLog.execute(action);
|
|
100
|
+
invalidateQuestListWidget();
|
|
99
101
|
logger.debug("quests:tool", "execute-complete", { toolCallId, success: result.success });
|
|
100
102
|
|
|
101
103
|
const displayQuests = SPLIT_DISPLAY_ACTIONS.includes(
|
|
@@ -134,7 +136,7 @@ export function registerQuestTool(
|
|
|
134
136
|
"When you need to understand quests, steps, rules, or best practices, use action: 'skill' or action: 'rules'.",
|
|
135
137
|
promptSnippet:
|
|
136
138
|
"Manage quests and steps, or retrieve quest rules and best practices via skill/rules",
|
|
137
|
-
promptGuidelines: [
|
|
139
|
+
promptGuidelines: [QUEST_PROMPT_GATE, ...QUEST_PROMPT_REMINDER],
|
|
138
140
|
parameters: createQuestParams(config.ids.length),
|
|
139
141
|
execute: (toolCallId, params, _signal, _onUpdate, _ctx) =>
|
|
140
142
|
questToolExecute(questLog, toolCallId, params),
|