pi-quests 0.1.0 → 0.2.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.
@@ -0,0 +1,337 @@
1
+ import type { AgentToolResult, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import { logger } from "../logger.js";
3
+ import {
4
+ formatBatchAddResult,
5
+ formatClearResult,
6
+ formatDeleteResult,
7
+ formatNotFound,
8
+ formatQuestList,
9
+ formatToggleResult,
10
+ formatUpdateResult,
11
+ } from "./formatters.js";
12
+ import { QUEST_ACTIONS, type Quest } from "./types.js";
13
+
14
+ export type HistoryEntry =
15
+ | { type: typeof QUEST_ACTIONS.add; id: number }
16
+ | { type: typeof QUEST_ACTIONS.toggle; id: number }
17
+ | { type: typeof QUEST_ACTIONS.update; id: number; previousDescription: string }
18
+ | { type: typeof QUEST_ACTIONS.delete; quest: Quest; index: number }
19
+ | { type: typeof QUEST_ACTIONS.clear; quests: Quest[]; nextId: number };
20
+
21
+ export type QuestAction =
22
+ | { type: typeof QUEST_ACTIONS.add; descriptions?: string[] }
23
+ | { type: typeof QUEST_ACTIONS.list }
24
+ | { type: typeof QUEST_ACTIONS.toggle; id?: number }
25
+ | { type: typeof QUEST_ACTIONS.update; id?: number; description?: string }
26
+ | { type: typeof QUEST_ACTIONS.delete; id?: number }
27
+ | { type: typeof QUEST_ACTIONS.clear }
28
+ | { type: typeof QUEST_ACTIONS.revert };
29
+
30
+ export type QuestOperationResult = { success: boolean; message: string; quest?: Quest };
31
+
32
+ /**
33
+ * In-memory quest data plane.
34
+ *
35
+ * Holds quest state, mutation history, and exposes `execute()` as the
36
+ * single entry point for all adapter layers (commands, tools, etc.).
37
+ */
38
+ export class QuestLog {
39
+ private quests: Quest[] = [];
40
+ private nextId = 1;
41
+ private history: HistoryEntry[] = [];
42
+ private undoHandlers: {
43
+ [K in HistoryEntry["type"]]: (entry: Extract<HistoryEntry, { type: K }>) => {
44
+ success: boolean;
45
+ message: string;
46
+ };
47
+ } = {
48
+ [QUEST_ACTIONS.add]: (entry) => {
49
+ this.quests = this.quests.filter((q) => q.id !== entry.id);
50
+
51
+ const maxId = this.quests.reduce((max, q) => Math.max(max, q.id), 0);
52
+ this.nextId = Math.max(maxId + 1, entry.id);
53
+
54
+ logger.debug("quests:state", "revert-add", { id: entry.id, total: this.quests.length });
55
+ return { success: true, message: `Reverted add quest #${entry.id}` };
56
+ },
57
+ [QUEST_ACTIONS.toggle]: (entry) => {
58
+ const quest = this.quests.find((q) => q.id === entry.id);
59
+
60
+ if (quest) {
61
+ quest.done = !quest.done;
62
+ logger.debug("quests:state", "revert-toggle", { id: entry.id, done: quest.done });
63
+ return { success: true, message: `Reverted toggle for quest #${entry.id}` };
64
+ }
65
+
66
+ logger.debug("quests:state", "revert-toggle-not-found", { id: entry.id });
67
+ return { success: false, message: `Quest #${entry.id} not found` };
68
+ },
69
+ [QUEST_ACTIONS.update]: (entry) => {
70
+ const quest = this.quests.find((q) => q.id === entry.id);
71
+ if (quest) {
72
+ quest.description = entry.previousDescription;
73
+ logger.debug("quests:state", "revert-update", { id: entry.id });
74
+ return { success: true, message: `Reverted update for quest #${entry.id}` };
75
+ }
76
+
77
+ logger.debug("quests:state", "revert-update-not-found", { id: entry.id });
78
+ return { success: false, message: `Quest #${entry.id} not found` };
79
+ },
80
+ [QUEST_ACTIONS.delete]: (entry) => {
81
+ this.quests.splice(entry.index, 0, entry.quest);
82
+
83
+ logger.debug("quests:state", "revert-delete", {
84
+ id: entry.quest.id,
85
+ index: entry.index,
86
+ total: this.quests.length,
87
+ });
88
+ return { success: true, message: `Reverted delete for quest #${entry.quest.id}` };
89
+ },
90
+ [QUEST_ACTIONS.clear]: (entry) => {
91
+ this.quests = [...entry.quests];
92
+ this.nextId = entry.nextId;
93
+
94
+ logger.debug("quests:state", "revert-clear", { count: entry.quests.length });
95
+ return { success: true, message: `Reverted clear (${entry.quests.length} quests restored)` };
96
+ },
97
+ };
98
+
99
+ getAll(): Quest[] {
100
+ return [...this.quests];
101
+ }
102
+
103
+ getNextId(): number {
104
+ return this.nextId;
105
+ }
106
+
107
+ add(description: string, additionalContext?: string): Quest {
108
+ const quest: Quest = {
109
+ id: this.nextId++,
110
+ description,
111
+ additionalContext,
112
+ done: false,
113
+ createdAt: Date.now(),
114
+ };
115
+
116
+ this.quests.push(quest);
117
+ this.history.push({ type: QUEST_ACTIONS.add, id: quest.id });
118
+
119
+ logger.debug("quests:state", QUEST_ACTIONS.add, {
120
+ id: quest.id,
121
+ description,
122
+ total: this.quests.length,
123
+ });
124
+ return quest;
125
+ }
126
+
127
+ toggle(id: number): Quest | undefined {
128
+ const quest = this.quests.find((q) => q.id === id);
129
+ if (!quest) {
130
+ logger.debug("quests:state", "toggle-not-found", { id });
131
+ return undefined;
132
+ }
133
+
134
+ quest.done = !quest.done;
135
+ this.history.push({ type: QUEST_ACTIONS.toggle, id });
136
+
137
+ logger.debug("quests:state", QUEST_ACTIONS.toggle, {
138
+ id,
139
+ done: quest.done,
140
+ total: this.quests.length,
141
+ });
142
+ return quest;
143
+ }
144
+
145
+ update(id: number, description: string): Quest | undefined {
146
+ const quest = this.quests.find((q) => q.id === id);
147
+ if (!quest) {
148
+ logger.debug("quests:state", "update-not-found", { id });
149
+ return undefined;
150
+ }
151
+
152
+ this.history.push({ type: QUEST_ACTIONS.update, id, previousDescription: quest.description });
153
+ quest.description = description;
154
+
155
+ logger.debug("quests:state", QUEST_ACTIONS.update, {
156
+ id,
157
+ description,
158
+ total: this.quests.length,
159
+ });
160
+ return quest;
161
+ }
162
+
163
+ delete(id: number): Quest | undefined {
164
+ const index = this.quests.findIndex((q) => q.id === id);
165
+ if (index === -1) {
166
+ logger.debug("quests:state", "delete-not-found", { id });
167
+ return undefined;
168
+ }
169
+
170
+ const [quest] = this.quests.splice(index, 1);
171
+ this.history.push({ type: QUEST_ACTIONS.delete, quest, index });
172
+
173
+ logger.debug("quests:state", QUEST_ACTIONS.delete, { id, index, total: this.quests.length });
174
+ return quest;
175
+ }
176
+
177
+ clear(): number {
178
+ const count = this.quests.length;
179
+ this.history.push({ type: QUEST_ACTIONS.clear, quests: [...this.quests], nextId: this.nextId });
180
+ this.quests = [];
181
+ this.nextId = 1;
182
+
183
+ logger.debug("quests:state", QUEST_ACTIONS.clear, { count });
184
+ return count;
185
+ }
186
+
187
+ revert(): QuestOperationResult {
188
+ const entry = this.history.pop();
189
+ if (!entry) {
190
+ logger.debug("quests:state", "revert-empty");
191
+ return { success: false, message: "Nothing to revert" };
192
+ }
193
+
194
+ logger.debug("quests:state", "revert", { type: entry.type });
195
+
196
+ const handler = this.undoHandlers[entry.type];
197
+ if (handler) {
198
+ return handler(entry as never);
199
+ }
200
+
201
+ logger.debug("quests:state", "revert-unknown", { type: (entry as { type: string }).type });
202
+ return { success: false, message: "Unknown history entry" };
203
+ }
204
+
205
+ /**
206
+ * Execute a quest action and return a presentation-ready result.
207
+ * This is the primary API for all adapter layers (commands, tools).
208
+ */
209
+ execute(action: QuestAction): QuestOperationResult {
210
+ switch (action.type) {
211
+ case QUEST_ACTIONS.add: {
212
+ if (action.descriptions && action.descriptions.length > 0) {
213
+ if (action.descriptions.some((d) => !d || d.trim().length === 0)) {
214
+ return {
215
+ success: false,
216
+ message: "Error: all descriptions in a batch must be non-empty",
217
+ };
218
+ }
219
+ const added: { id: number; description: string }[] = [];
220
+ for (const desc of action.descriptions) {
221
+ const q = this.add(desc);
222
+ added.push({ id: q.id, description: q.description });
223
+ }
224
+
225
+ return { success: true, message: formatBatchAddResult(added) };
226
+ }
227
+ return {
228
+ success: false,
229
+ message: "Error: at least one description is required for add action",
230
+ };
231
+ }
232
+ case QUEST_ACTIONS.list: {
233
+ const quests = this.getAll();
234
+ return { success: true, message: formatQuestList(quests) };
235
+ }
236
+ case QUEST_ACTIONS.toggle: {
237
+ if (action.id === undefined) {
238
+ return { success: false, message: "Error: id is required for toggle action" };
239
+ }
240
+
241
+ const q = this.toggle(action.id);
242
+ if (!q) {
243
+ return { success: false, message: formatNotFound(action.id) };
244
+ }
245
+
246
+ return { success: true, message: formatToggleResult(action.id, q.done), quest: q };
247
+ }
248
+ case QUEST_ACTIONS.update: {
249
+ if (action.id === undefined) {
250
+ return { success: false, message: "Error: id is required for update action" };
251
+ }
252
+
253
+ if (!action.description) {
254
+ return { success: false, message: "Error: description is required for update action" };
255
+ }
256
+
257
+ const q = this.update(action.id, action.description);
258
+ if (!q) {
259
+ return { success: false, message: formatNotFound(action.id) };
260
+ }
261
+
262
+ return { success: true, message: formatUpdateResult(q), quest: q };
263
+ }
264
+ case QUEST_ACTIONS.delete: {
265
+ if (action.id === undefined) {
266
+ return { success: false, message: "Error: id is required for delete action" };
267
+ }
268
+
269
+ const q = this.delete(action.id);
270
+ if (!q) {
271
+ return { success: false, message: formatNotFound(action.id) };
272
+ }
273
+
274
+ return { success: true, message: formatDeleteResult(q), quest: q };
275
+ }
276
+ case QUEST_ACTIONS.clear: {
277
+ const count = this.clear();
278
+ return { success: true, message: formatClearResult(count) };
279
+ }
280
+ case QUEST_ACTIONS.revert: {
281
+ return this.revert();
282
+ }
283
+ default: {
284
+ return { success: false, message: `Unknown action: ${(action as { type: string }).type}` };
285
+ }
286
+ }
287
+ }
288
+
289
+ reconstructFromSession(ctx: ExtensionContext): void {
290
+ const branch = ctx.sessionManager.getBranch();
291
+ let lastState: Record<string, unknown> | undefined;
292
+ let toolResults = 0;
293
+
294
+ for (const entry of branch) {
295
+ if (
296
+ entry.type === "message" &&
297
+ "message" in entry &&
298
+ entry.message.role === "toolResult" &&
299
+ entry.message.toolName === "quest"
300
+ ) {
301
+ toolResults++;
302
+ lastState = entry.message.details;
303
+ }
304
+ }
305
+
306
+ if (lastState) {
307
+ const quests = lastState.quests;
308
+ const nextId = lastState.nextId;
309
+ const questCount = Array.isArray(quests) ? quests.length : 0;
310
+
311
+ this.quests = Array.isArray(quests) ? [...(quests as Quest[])] : [];
312
+ this.nextId = typeof nextId === "number" ? nextId : 1;
313
+ logger.debug("quests:state", "reconstruct", { toolResults, questCount, nextId: this.nextId });
314
+ } else {
315
+ this.quests = [];
316
+ this.nextId = 1;
317
+ logger.debug("quests:state", "reconstruct-empty", { toolResults });
318
+ }
319
+
320
+ this.history = [];
321
+ }
322
+ }
323
+
324
+ export function makeToolResult(
325
+ text: string,
326
+ questLog: QuestLog,
327
+ displayQuests?: Quest[],
328
+ ): AgentToolResult<unknown> {
329
+ return {
330
+ content: [{ type: "text", text }],
331
+ details: {
332
+ quests: questLog.getAll(),
333
+ nextId: questLog.getNextId(),
334
+ displayQuests,
335
+ },
336
+ };
337
+ }
@@ -0,0 +1,34 @@
1
+ export function formatQuestList(
2
+ quests: { id: number; description: string; done: boolean }[],
3
+ ): string {
4
+ if (quests.length === 0) return "No quests.";
5
+ return quests.map((q) => `#${q.id} [${q.done ? "x" : " "}] ${q.description}`).join("\n");
6
+ }
7
+
8
+ export function formatAddResult(q: { id: number; description: string }): string {
9
+ return `Added quest #${q.id}: ${q.description}`;
10
+ }
11
+
12
+ export function formatBatchAddResult(added: { id: number; description: string }[]): string {
13
+ return `Added ${added.length} quests:\n${added.map((q) => `#${q.id}: ${q.description}`).join("\n")}`;
14
+ }
15
+
16
+ export function formatToggleResult(id: number, done: boolean): string {
17
+ return `Quest #${id} ${done ? "done" : "undone"}`;
18
+ }
19
+
20
+ export function formatUpdateResult(q: { id: number; description: string }): string {
21
+ return `Updated quest #${q.id}: ${q.description}`;
22
+ }
23
+
24
+ export function formatDeleteResult(q: { id: number; description: string }): string {
25
+ return `Deleted quest #${q.id}: ${q.description}`;
26
+ }
27
+
28
+ export function formatClearResult(count: number): string {
29
+ return `Cleared ${count} quests`;
30
+ }
31
+
32
+ export function formatNotFound(id: number): string {
33
+ return `Quest #${id} not found`;
34
+ }
@@ -0,0 +1,87 @@
1
+ export const COMPLEX_TASK_KEYWORDS = [
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;
15
+
16
+ const ACKNOWLEDGEMENT =
17
+ "ALWAYS acknowledge this reminder and create, update, or align on quests before making further tool calls.";
18
+
19
+ export class QuestUsageTracker {
20
+ private totalToolCalls = 0;
21
+ private consecutiveNonQuestToolCalls = 0;
22
+ private hasEverUsedQuestTool = false;
23
+ private lastQuestToolTime = 0;
24
+ private nudgedThisTurn = false;
25
+
26
+ onToolExecution(toolName: string): void {
27
+ this.totalToolCalls++;
28
+ if (toolName === "quest") {
29
+ this.hasEverUsedQuestTool = true;
30
+ this.lastQuestToolTime = Date.now();
31
+ this.consecutiveNonQuestToolCalls = 0;
32
+ } else {
33
+ this.consecutiveNonQuestToolCalls++;
34
+ }
35
+ }
36
+
37
+ clearTurnNudge(): void {
38
+ this.nudgedThisTurn = false;
39
+ }
40
+
41
+ getNudge(activeQuestCount: number, latestPrompt?: string): string | undefined {
42
+ if (this.nudgedThisTurn) return undefined;
43
+
44
+ // 1. Initialization nudge
45
+ if (this.totalToolCalls >= 3 && !this.hasEverUsedQuestTool) {
46
+ this.nudgedThisTurn = true;
47
+ 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
+ }
49
+
50
+ // 2. Complex-task entrypoint nudge
51
+ if (activeQuestCount === 0 && latestPrompt && this.isComplexPrompt(latestPrompt)) {
52
+ this.nudgedThisTurn = true;
53
+ return `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}`;
54
+ }
55
+
56
+ // 3. Time-based alignment nudge
57
+ if (
58
+ this.hasEverUsedQuestTool &&
59
+ this.lastQuestToolTime > 0 &&
60
+ this.consecutiveNonQuestToolCalls >= 3 &&
61
+ // 8 minutes without quest tool use
62
+ Date.now() - this.lastQuestToolTime >= 8 * 60 * 1000
63
+ ) {
64
+ this.nudgedThisTurn = true;
65
+ 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
+ }
67
+
68
+ // 4. Zero-active sustained-work nudge
69
+ if (this.consecutiveNonQuestToolCalls >= 5 && activeQuestCount === 0) {
70
+ this.nudgedThisTurn = true;
71
+ 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
+ }
73
+
74
+ // 5. Stale-progress sustained-work nudge
75
+ if (this.consecutiveNonQuestToolCalls >= 10 && activeQuestCount > 0) {
76
+ this.nudgedThisTurn = true;
77
+ return `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. ${ACKNOWLEDGEMENT}`;
78
+ }
79
+
80
+ return undefined;
81
+ }
82
+
83
+ private isComplexPrompt(prompt: string): boolean {
84
+ const lower = prompt.toLowerCase();
85
+ return COMPLEX_TASK_KEYWORDS.some((kw) => lower.includes(kw));
86
+ }
87
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Core domain types and action constants for the quest system.
3
+ */
4
+
5
+ export const QUEST_ACTIONS = {
6
+ add: "add",
7
+ list: "list",
8
+ toggle: "toggle",
9
+ update: "update",
10
+ delete: "delete",
11
+ clear: "clear",
12
+ revert: "revert",
13
+ } as const;
14
+
15
+ export const QUEST_ACTION_VALUES = [
16
+ QUEST_ACTIONS.add,
17
+ QUEST_ACTIONS.list,
18
+ QUEST_ACTIONS.toggle,
19
+ QUEST_ACTIONS.update,
20
+ QUEST_ACTIONS.delete,
21
+ QUEST_ACTIONS.clear,
22
+ QUEST_ACTIONS.revert,
23
+ ] as const;
24
+
25
+ export type QuestActionType = (typeof QUEST_ACTION_VALUES)[number];
26
+
27
+ export interface Quest {
28
+ id: number;
29
+ description: string;
30
+ additionalContext?: string;
31
+ done: boolean;
32
+ createdAt: number;
33
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { Key, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
3
3
  import { logger } from "../logger.js";
4
- import type { Quest } from "../quests.js";
4
+ import type { Quest } from "../quest/types.js";
5
5
 
6
6
  export class QuestListWidget {
7
7
  private cachedWidth?: number;
@@ -24,10 +24,12 @@ export class QuestListWidget {
24
24
  this.onClose();
25
25
  return;
26
26
  }
27
+
27
28
  if (matchesKey(data, Key.tab)) {
28
29
  this.nextPage();
29
30
  return;
30
31
  }
32
+
31
33
  if (matchesKey(data, Key.shift(Key.tab))) {
32
34
  this.prevPage();
33
35
  return;
@@ -45,6 +47,7 @@ export class QuestListWidget {
45
47
  to: next,
46
48
  totalPages: this.totalPages,
47
49
  });
50
+
48
51
  this.page = next;
49
52
  this.invalidate();
50
53
  }
@@ -56,6 +59,7 @@ export class QuestListWidget {
56
59
  to: prev,
57
60
  totalPages: this.totalPages,
58
61
  });
62
+
59
63
  this.page = prev;
60
64
  this.invalidate();
61
65
  }
@@ -100,6 +104,7 @@ export class QuestListWidget {
100
104
  const filled = Math.round((doneCount / total) * barWidth);
101
105
  const empty = barWidth - filled;
102
106
  const bar = th.fg("success", "█".repeat(filled)) + th.fg("dim", "░".repeat(empty));
107
+
103
108
  lines.push(truncateToWidth(` ${bar} ${th.fg("muted", `${doneCount}/${total}`)}`, width));
104
109
  lines.push("");
105
110
 
@@ -112,12 +117,14 @@ export class QuestListWidget {
112
117
  ? th.fg("dim", th.strikethrough(q.description))
113
118
  : th.fg("text", q.description);
114
119
  const row = `${marker}${idStr} ${desc}`;
120
+
115
121
  lines.push(truncateToWidth(row, width));
116
122
  }
117
123
 
118
124
  if (this.totalPages > 1) {
119
- lines.push("");
120
125
  const pageInfo = ` Page ${this.page + 1}/${this.totalPages} ${th.fg("dim", "· Tab/Shift+Tab to navigate")}`;
126
+
127
+ lines.push("");
121
128
  lines.push(truncateToWidth(pageInfo, width));
122
129
  }
123
130
  }
@@ -1,19 +1,20 @@
1
1
  import type { AgentToolResult, Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { Text } from "@mariozechner/pi-tui";
3
3
  import { logger } from "../logger.js";
4
+ import { QUEST_ACTIONS } from "../quest/types.js";
4
5
 
5
6
  type QuestArgs = {
6
7
  action: string;
7
- description?: string;
8
8
  descriptions?: string[];
9
9
  id?: number;
10
10
  };
11
11
 
12
12
  export function renderQuestCall(args: QuestArgs, theme: Theme, _context: unknown): Text {
13
- logger.debug("quests:tool", "renderCall", { action: args.action, id: args.id });
13
+ const id = "id" in args ? args.id : undefined;
14
+ logger.debug("quests:tool", "renderCall", { action: args.action, id });
14
15
  const actionText = theme.fg("toolTitle", theme.bold("quest ")) + theme.fg("accent", args.action);
15
16
 
16
- if (args.action === "add" && args.descriptions && args.descriptions.length > 0) {
17
+ if (args.action === QUEST_ACTIONS.add && args.descriptions && args.descriptions.length > 0) {
17
18
  const n = args.descriptions.length;
18
19
  return new Text(
19
20
  `${actionText} ${theme.fg("muted", `[${n} quest${n !== 1 ? "s" : ""}]`)}`,
@@ -22,23 +23,15 @@ export function renderQuestCall(args: QuestArgs, theme: Theme, _context: unknown
22
23
  );
23
24
  }
24
25
 
25
- if (args.action === "add" && args.description) {
26
- const preview =
27
- args.description.length > 60 ? `${args.description.slice(0, 60)}…` : args.description;
28
- return new Text(`${actionText} ${theme.fg("muted", preview)}`, 0, 0);
29
- }
30
-
31
26
  if (
32
- (args.action === "toggle" || args.action === "update" || args.action === "delete") &&
27
+ (args.action === QUEST_ACTIONS.toggle ||
28
+ args.action === QUEST_ACTIONS.update ||
29
+ args.action === QUEST_ACTIONS.delete) &&
33
30
  args.id !== undefined
34
31
  ) {
35
32
  return new Text(`${actionText} ${theme.fg("muted", `#${args.id}`)}`, 0, 0);
36
33
  }
37
34
 
38
- if (args.action === "revert") {
39
- return new Text(actionText, 0, 0);
40
- }
41
-
42
35
  return new Text(actionText, 0, 0);
43
36
  }
44
37
 
@@ -52,15 +45,17 @@ export function renderQuestResult(
52
45
  expanded: options.expanded,
53
46
  isPartial: options.isPartial,
54
47
  });
55
- const details = result.details as
56
- | { quests?: Array<{ id: number; description: string; done: boolean }> }
57
- | undefined;
48
+ const details = result.details as Record<string, unknown> | undefined;
49
+ const questsToRender = details?.displayQuests ?? details?.quests;
50
+
51
+ if (Array.isArray(questsToRender) && questsToRender.length > 0) {
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
+ );
58
58
 
59
- if (details?.quests && details.quests.length > 0) {
60
- const lines = details.quests.map((q) => {
61
- const marker = q.done ? theme.fg("success", "✓") : theme.fg("dim", "○");
62
- return `${marker} ${theme.fg("text", `#${q.id}`)} ${theme.fg("muted", q.description)}`;
63
- });
64
59
  return new Text(lines.join("\n"), 0, 0);
65
60
  }
66
61