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 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
- [![version 0.5.0](https://img.shields.io/badge/version-0.5.0-blue)](CHANGELOG.md)
3
+ [![version 0.6.0](https://img.shields.io/badge/version-0.6.0-blue)](CHANGELOG.md)
4
4
  [![MIT license](https://img.shields.io/badge/license-MIT-green)](LICENSE.md)
5
5
  [![pi extension](https://img.shields.io/badge/pi-extension-purple)](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.5.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": "npm run typecheck && npm test"
54
+ "prepublishOnly": "npx tsc --noEmit && npx vitest run"
53
55
  }
54
56
  }
@@ -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(pi: ExtensionAPI, questLog: QuestLog, config: ResolvedConfig) {
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: 5,
42
+ timeBasedToolCallThreshold: 8,
46
43
  zeroActiveToolCallThreshold: 8,
47
- staleProgressToolCallThreshold: 16,
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, QUEST_PROMPT_REMINDER } from "./prompts.js";
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) => questLog.reconstructFromSession(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(activeQuestCount, latestPrompt, hasTopLevelQuestWithoutSubs);
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 = `${QUEST_PROMPT_REMINDER.join("\n")}\n\nBefore adding any independent quests, clear any previously completed quests from the log to keep it focused on current work.`;
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 throughout the conversation.",
13
- "Before reading files, running commands, or making edits, ensure the current work is tracked as specific, actionable quests.",
14
- "Do not create a single vague quest for broad requests. Break them into concrete, independent steps.",
15
- "When the user gives a plan or a list of tasks, add them as quests immediately.",
16
- "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.",
17
- "It is critical that you toggle quests to done as soon as you complete them. Do NOT batch completions.",
18
- "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.",
19
- "ALWAYS use steps to break down a complex quest into smaller steps. To create a step, use the `split` action with the parent quest's hex ID. Use steps for multi-step tasks, minion delegations, or when a quest has more than one distinct deliverable.",
20
- "A parent quest cannot be toggled done until all of its steps are completed. Steps cannot be reordered independently.",
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. ALWAYS break broad requests into concrete steps.";
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.";
@@ -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) return undefined;
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 appears to be a complex task, but there are 0 active quests. USE the quest tool to break this into concrete, trackable steps. ${ACKNOWLEDGEMENT}`,
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. ${ACKNOWLEDGEMENT}`,
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. Use the \`split\` action to break down complex tasks into smaller, trackable steps. ${ACKNOWLEDGEMENT}`,
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.\nUPDATE your quest progress to reflect current status.\nALWAYS use steps to break down a quest into smaller steps, and to group related tasks together. ${ACKNOWLEDGEMENT}`,
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", "".repeat(filled)) + th.fg("dim", "".repeat(empty));
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
+ }
@@ -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: [...QUEST_PROMPT_GATE, ...QUEST_PROMPT_REMINDER],
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),