pi-quests 0.7.0 → 0.9.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,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.0] - 2026-09-17
6
+
7
+ - feat: quest tool results follow pi's compact view — collapsed rows show a 10-line preview with a `ctrl+o` (`app.tools.expand`) hint, expanded rows show everything
8
+ - fix: RangeError crash when position labels exceed the reserved column width in `formatQuestRow`
9
+ - fix: wrap batch add and split in a `runTransaction` guard to roll back partial state on ID exhaustion
10
+
11
+ ## [0.8.0] - 2026-05-26
12
+
13
+ - feat: migrate to `@earendil-works`
14
+ - feat: remove the nudge-based acknowledgement system
15
+
5
16
  ## [0.7.0] - 2026-04-26
6
17
 
7
18
  - feat: add batch toggle and batch delete support for quests (#2) — @pascal-de-ladurantaye
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-quests
2
2
 
3
- [![version 0.7.0](https://img.shields.io/badge/version-0.7.0-blue)](CHANGELOG.md)
3
+ [![version 0.9.0](https://img.shields.io/badge/version-0.9.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.7.0",
3
+ "version": "0.9.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,15 +32,15 @@
32
32
  "LICENSE.md"
33
33
  ],
34
34
  "peerDependencies": {
35
- "@mariozechner/pi-ai": "*",
36
- "@mariozechner/pi-coding-agent": "*",
37
- "@mariozechner/pi-tui": "*",
35
+ "@earendil-works/pi-ai": "*",
36
+ "@earendil-works/pi-coding-agent": "*",
37
+ "@earendil-works/pi-tui": "*",
38
38
  "typebox": "*"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@biomejs/biome": "^2.4.13",
42
- "@mariozechner/pi-coding-agent": "^0.70.2",
43
- "@mariozechner/pi-tui": "^0.70.2",
42
+ "@earendil-works/pi-coding-agent": "^0.75.5",
43
+ "@earendil-works/pi-tui": "^0.75.5",
44
44
  "typebox": "^1.1.33",
45
45
  "typescript": "^6.0.3",
46
46
  "vitest": "^4.1.5"
@@ -3,7 +3,7 @@ import type {
3
3
  ExtensionAPI,
4
4
  ExtensionCommandContext,
5
5
  ExtensionContext,
6
- } from "@mariozechner/pi-coding-agent";
6
+ } from "@earendil-works/pi-coding-agent";
7
7
  import type { ResolvedConfig } from "../config.js";
8
8
  import { logger } from "../logger.js";
9
9
  import type { QuestAction, QuestLog } from "../quest/dataplane.js";
package/src/config.ts CHANGED
@@ -1,53 +1,17 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
4
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
3
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
5
 
6
6
  export interface ResolvedConfig {
7
7
  ids: { length: number };
8
8
  display: { pageSize: number; progressBarMaxWidth: number; icon: string; showStatus: boolean };
9
- nudges: {
10
- enable: boolean;
11
- toolCallThreshold: number;
12
- hintIntervalMinutes: number;
13
- timeBasedToolCallThreshold: number;
14
- zeroActiveToolCallThreshold: number;
15
- staleProgressToolCallThreshold: number;
16
- stepSuggestionToolCallThreshold: number;
17
- complexTaskKeywords: string[];
18
- };
19
- validation: { fakeDonePattern: string };
20
9
  shortcuts?: { openQuests?: string };
21
10
  }
22
11
 
23
- export const DEFAULT_COMPLEX_TASK_KEYWORDS = [
24
- "implement",
25
- "refactor",
26
- "investigate",
27
- "analyze",
28
- "audit",
29
- "design",
30
- "fix",
31
- "plan",
32
- "build",
33
- ] as const;
34
-
35
- export const DEFAULT_FAKE_DONE_PATTERN = String.raw`\s[-\u2013\u2014]\s*(DONE|COMPLETED|FINISHED)$|\s[([](DONE|COMPLETED|FINISHED)[)\]]$`;
36
-
37
12
  export const DEFAULT_CONFIG: ResolvedConfig = {
38
13
  ids: { length: 2 },
39
14
  display: { pageSize: 10, progressBarMaxWidth: 24, icon: "󰣏", showStatus: true },
40
- nudges: {
41
- enable: true,
42
- toolCallThreshold: 8,
43
- hintIntervalMinutes: 4,
44
- timeBasedToolCallThreshold: 8,
45
- zeroActiveToolCallThreshold: 8,
46
- staleProgressToolCallThreshold: 12,
47
- stepSuggestionToolCallThreshold: 10,
48
- complexTaskKeywords: [...DEFAULT_COMPLEX_TASK_KEYWORDS],
49
- },
50
- validation: { fakeDonePattern: DEFAULT_FAKE_DONE_PATTERN },
51
15
  shortcuts: {},
52
16
  };
53
17
 
@@ -119,30 +83,6 @@ export function getConfig(ctx: Pick<ExtensionContext, "cwd">): ResolvedConfig {
119
83
  icon: user.display?.icon ?? DEFAULT_CONFIG.display.icon,
120
84
  showStatus: user.display?.showStatus ?? DEFAULT_CONFIG.display.showStatus,
121
85
  },
122
- nudges: {
123
- enable: user.nudges?.enable ?? DEFAULT_CONFIG.nudges.enable,
124
- toolCallThreshold: user.nudges?.toolCallThreshold ?? DEFAULT_CONFIG.nudges.toolCallThreshold,
125
- hintIntervalMinutes:
126
- user.nudges?.hintIntervalMinutes ?? DEFAULT_CONFIG.nudges.hintIntervalMinutes,
127
- timeBasedToolCallThreshold:
128
- user.nudges?.timeBasedToolCallThreshold ?? DEFAULT_CONFIG.nudges.timeBasedToolCallThreshold,
129
- zeroActiveToolCallThreshold:
130
- user.nudges?.zeroActiveToolCallThreshold ??
131
- DEFAULT_CONFIG.nudges.zeroActiveToolCallThreshold,
132
- staleProgressToolCallThreshold:
133
- user.nudges?.staleProgressToolCallThreshold ??
134
- DEFAULT_CONFIG.nudges.staleProgressToolCallThreshold,
135
- stepSuggestionToolCallThreshold:
136
- user.nudges?.stepSuggestionToolCallThreshold ??
137
- DEFAULT_CONFIG.nudges.stepSuggestionToolCallThreshold,
138
- complexTaskKeywords: [
139
- ...(user.nudges?.complexTaskKeywords ?? DEFAULT_CONFIG.nudges.complexTaskKeywords),
140
- ],
141
- },
142
- validation: {
143
- fakeDonePattern:
144
- user.validation?.fakeDonePattern ?? DEFAULT_CONFIG.validation.fakeDonePattern,
145
- },
146
86
  shortcuts: {
147
87
  openQuests: user.shortcuts?.openQuests,
148
88
  },
package/src/index.ts CHANGED
@@ -1,6 +1,5 @@
1
- import type { UserMessage } from "@mariozechner/pi-ai";
2
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
- import type { KeyId } from "@mariozechner/pi-tui";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { KeyId } from "@earendil-works/pi-tui";
4
3
  import { createQuestsHandler, openQuestList } from "./commands/handler.js";
5
4
  import { DEFAULT_CONFIG, getConfig, type ResolvedConfig } from "./config.js";
6
5
  import { logger } from "./logger.js";
@@ -24,12 +23,9 @@ import { registerQuestTool } from "./tools/handler.js";
24
23
  const NO_QUESTS_REMINDER =
25
24
  "No active quests. Use the learn_quests tool to understand how to use quests and use quests to track this task. DO THIS NOW and acknowledge it.";
26
25
 
27
- const FAKE_DONE_REMINDER =
28
- "QUEST REMINDER: A quest has a completion marker appended to its description but is not toggled done. Use the list action to find it, then toggle it done. NEVER append completion markers via update.";
29
-
30
26
  export default function (pi: ExtensionAPI): void {
31
27
  let questLog = new QuestLog();
32
- let tracker = new QuestUsageTracker(DEFAULT_CONFIG);
28
+ let tracker = new QuestUsageTracker();
33
29
  let config: ResolvedConfig = DEFAULT_CONFIG;
34
30
  let statusWidget = new QuestStatusWidget(
35
31
  DEFAULT_CONFIG.display.icon,
@@ -56,7 +52,7 @@ export default function (pi: ExtensionAPI): void {
56
52
  pi.on("session_start", async (_event, ctx) => {
57
53
  config = getConfig(ctx);
58
54
  questLog = new QuestLog(config);
59
- tracker = new QuestUsageTracker(config);
55
+ tracker = new QuestUsageTracker();
60
56
  statusWidget = new QuestStatusWidget(config.display.icon, config.display.showStatus);
61
57
  questLog.reconstructFromSession(ctx);
62
58
  if (questLog.getAll().length > 0) tracker.markQuestToolUsed();
@@ -79,8 +75,6 @@ export default function (pi: ExtensionAPI): void {
79
75
  statusWidget.update(questLog, ctx.ui, ctx.ui.theme);
80
76
  });
81
77
 
82
- pi.on("turn_start", async () => tracker.clearTurnNudge());
83
-
84
78
  pi.on("tool_execution_end", async (event, ctx) => {
85
79
  tracker.onToolExecution(event.toolName);
86
80
  if (event.toolName === "quest") {
@@ -88,38 +82,6 @@ export default function (pi: ExtensionAPI): void {
88
82
  }
89
83
  });
90
84
 
91
- pi.on("context", async (event) => {
92
- if (!config.nudges.enable) return undefined;
93
- const latestPrompt = event.messages
94
- .filter((m) => m.role === "user")
95
- .map((m) => (typeof m.content === "string" ? m.content : ""))
96
- .join("\n");
97
-
98
- const allQuests = questLog.getAll();
99
- const activeQuests = allQuests.filter((q) => !q.done);
100
- const activeQuestCount = activeQuests.length;
101
- const activeTopLevel = activeQuests.filter((q) => !(q as { parentId?: string }).parentId);
102
- const hasTopLevelQuestWithoutSubs = activeTopLevel.some(
103
- (q) => !allQuests.some((step) => (step as { parentId?: string }).parentId === q.id),
104
- );
105
- const nudge = tracker.getNudge(activeQuestCount, latestPrompt, hasTopLevelQuestWithoutSubs);
106
-
107
- const fakeDoneRegex = new RegExp(config.validation.fakeDonePattern, "i");
108
- const fakeDone = questLog.getAll().find((q) => !q.done && fakeDoneRegex.test(q.description));
109
- if (!nudge && !fakeDone) return undefined;
110
-
111
- const parts: string[] = [];
112
- if (nudge) parts.push(nudge);
113
- if (fakeDone) parts.push(FAKE_DONE_REMINDER);
114
-
115
- const reminder: UserMessage = {
116
- role: "user",
117
- content: parts.join("\n"),
118
- timestamp: Date.now(),
119
- };
120
- return { messages: [...event.messages, reminder] };
121
- });
122
-
123
85
  pi.on("before_agent_start", async (event) => {
124
86
  if (questLog.getAll().length === 0)
125
87
  return {
@@ -1,4 +1,4 @@
1
- import type { AgentToolResult, ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { DEFAULT_CONFIG, type ResolvedConfig } from "../config.js";
3
3
  import { logger } from "../logger.js";
4
4
  import { getQuestSkillDocument } from "../prompts.js";
@@ -146,6 +146,30 @@ export class QuestLog {
146
146
  this.usedIds.add(id);
147
147
  return id;
148
148
  }
149
+
150
+ /*
151
+ * Runs `fn` and, if it throws, restores all mutable state to the snapshot
152
+ * taken beforehand. Makes multi-step mutations (batch add, split) atomic so a
153
+ * partial failure (e.g. ID exhaustion mid-loop) cannot leave orphaned quests,
154
+ * steps, history, or used IDs behind.
155
+ */
156
+ private runTransaction<T>(fn: () => T): T {
157
+ const quests = [...this.quests];
158
+ const steps = [...this.steps];
159
+ const history = [...this.history];
160
+ const usedIds = new Set(this.usedIds);
161
+ const redoStack = [...this.redoStack];
162
+ try {
163
+ return fn();
164
+ } catch (err) {
165
+ this.quests = quests;
166
+ this.steps = steps;
167
+ this.history = history;
168
+ this.usedIds = usedIds;
169
+ this.redoStack = redoStack;
170
+ throw err;
171
+ }
172
+ }
149
173
  private undoHandlers: {
150
174
  [K in HistoryEntry["type"]]: (entry: Extract<HistoryEntry, { type: K }>) => {
151
175
  success: boolean;
@@ -533,11 +557,14 @@ export class QuestLog {
533
557
  const parent = this.quests.find((q) => q.id === id);
534
558
  if (!parent) throw new Error(formatNotFound(id));
535
559
  if (parent.done) throw new Error(formatParentDoneError(id));
536
- const created: Step[] = [];
537
- for (const desc of descriptions) {
538
- created.push(this.addStep(desc, id));
539
- }
540
- return created;
560
+
561
+ return this.runTransaction(() => {
562
+ const created: Step[] = [];
563
+ for (const desc of descriptions) {
564
+ created.push(this.addStep(desc, id));
565
+ }
566
+ return created;
567
+ });
541
568
  }
542
569
 
543
570
  toggle(id: string): Quest | Step | null | undefined {
@@ -878,12 +905,16 @@ export class QuestLog {
878
905
  };
879
906
  }
880
907
  }
908
+ const descriptions = action.descriptions;
881
909
  try {
882
- const added: { id: string; description: string }[] = [];
883
- for (const desc of action.descriptions) {
884
- const q = this.add(desc);
885
- added.push({ id: q.id, description: q.description });
886
- }
910
+ const added = this.runTransaction(() => {
911
+ const result: { id: string; description: string }[] = [];
912
+ for (const desc of descriptions) {
913
+ const q = this.add(desc);
914
+ result.push({ id: q.id, description: q.description });
915
+ }
916
+ return result;
917
+ });
887
918
 
888
919
  return { success: true, message: formatBatchAddResult(added) };
889
920
  } catch (err) {
@@ -1,34 +1,32 @@
1
- import type { ResolvedConfig } from "../config.js";
2
- import { logger } from "../logger.js";
3
-
4
- const ACKNOWLEDGEMENT = "Update your quest status before continuing.";
5
-
6
- type NudgeCandidate = { index: number; message: string };
1
+ export interface QuestUsageStats {
2
+ readonly totalToolCalls: number;
3
+ readonly questToolCalls: number;
4
+ readonly nonQuestToolCalls: number;
5
+ readonly consecutiveNonQuestToolCalls: number;
6
+ readonly hasUsedQuestTool: boolean;
7
+ readonly lastQuestToolTimestamp: number | undefined;
8
+ }
7
9
 
8
10
  export class QuestUsageTracker {
9
11
  private totalToolCalls = 0;
12
+ private questToolCalls = 0;
13
+ private nonQuestToolCalls = 0;
10
14
  private consecutiveNonQuestToolCalls = 0;
11
15
  private hasEverUsedQuestTool = false;
12
- private lastQuestToolTime = 0;
13
- private nudgedThisTurn = false;
14
- private lastNudgeTime = 0;
15
- private lastNudgeIndex = -1;
16
-
17
- constructor(private readonly config: ResolvedConfig) {}
16
+ private lastQuestToolTimestamp: number | undefined;
18
17
 
19
18
  onToolExecution(toolName: string): void {
20
19
  this.totalToolCalls++;
21
20
  if (toolName === "quest") {
21
+ this.questToolCalls++;
22
22
  this.hasEverUsedQuestTool = true;
23
- this.lastQuestToolTime = Date.now();
23
+ this.lastQuestToolTimestamp = Date.now();
24
24
  this.consecutiveNonQuestToolCalls = 0;
25
- } else {
26
- this.consecutiveNonQuestToolCalls++;
25
+ return;
27
26
  }
28
- }
29
27
 
30
- clearTurnNudge(): void {
31
- this.nudgedThisTurn = false;
28
+ this.nonQuestToolCalls++;
29
+ this.consecutiveNonQuestToolCalls++;
32
30
  }
33
31
 
34
32
  get hasUsedQuestTool(): boolean {
@@ -37,141 +35,17 @@ export class QuestUsageTracker {
37
35
 
38
36
  markQuestToolUsed(): void {
39
37
  this.hasEverUsedQuestTool = true;
40
- this.lastQuestToolTime = Date.now();
41
- }
42
-
43
- getNudge(
44
- activeQuestCount: number,
45
- latestPrompt?: string,
46
- hasTopLevelQuestWithoutSubs?: boolean,
47
- ): string | undefined {
48
- if (this.nudgedThisTurn) {
49
- logger.debug("quests:tracker", "nudge-suppressed", { reason: "turn-limit" });
50
- return undefined;
51
- }
52
-
53
- const now = Date.now();
54
- const cooldownMs = this.config.nudges.hintIntervalMinutes * 60 * 1000;
55
- if (this.lastNudgeTime > 0 && now - this.lastNudgeTime < cooldownMs) {
56
- logger.debug("quests:tracker", "nudge-suppressed", {
57
- reason: "cooldown",
58
- elapsed: now - this.lastNudgeTime,
59
- cooldownMs,
60
- });
61
- return undefined;
62
- }
63
-
64
- const eligible = this.getEligibleNudges(
65
- activeQuestCount,
66
- latestPrompt,
67
- hasTopLevelQuestWithoutSubs,
68
- );
69
-
70
- logger.debug("quests:tracker", "nudge-candidates", {
71
- eligible: eligible.length,
72
- indices: eligible.map((c) => c.index),
73
- });
74
-
75
- // Rotate priority: start checking from the nudge after the last one that fired
76
- const rotated = [
77
- ...eligible.filter((n) => n.index > this.lastNudgeIndex),
78
- ...eligible.filter((n) => n.index <= this.lastNudgeIndex),
79
- ];
80
-
81
- const winner = rotated[0];
82
- if (winner) {
83
- this.nudgedThisTurn = true;
84
- this.lastNudgeTime = now;
85
-
86
- logger.debug("quests:tracker", "nudge-fired", {
87
- winner: winner.index,
88
- rotatedFrom: this.lastNudgeIndex,
89
- });
90
- this.lastNudgeIndex = winner.index;
91
- return winner.message;
92
- }
93
-
94
- return undefined;
95
- }
96
-
97
- private getEligibleNudges(
98
- activeQuestCount: number,
99
- latestPrompt?: string,
100
- hasTopLevelQuestWithoutSubs?: boolean,
101
- ): NudgeCandidate[] {
102
- const candidates: NudgeCandidate[] = [];
103
-
104
- // 0. Initialization nudge
105
- if (this.totalToolCalls >= this.config.nudges.toolCallThreshold && !this.hasEverUsedQuestTool) {
106
- candidates.push({
107
- index: 0,
108
- message: `QUEST REMINDER: You have made multiple 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}`,
109
- });
110
- }
111
-
112
- // 1. Complex-task entrypoint nudge
113
- if (activeQuestCount === 0 && latestPrompt && this.isComplexPrompt(latestPrompt)) {
114
- candidates.push({
115
- index: 1,
116
- message: `QUEST REMINDER: Your latest prompt looks like a complex task, but there are 0 active quests. USE the quest tool to break it into concrete, trackable steps. ${ACKNOWLEDGEMENT}`,
117
- });
118
- }
119
-
120
- // 2. Time-based alignment nudge
121
- if (
122
- this.hasEverUsedQuestTool &&
123
- this.lastQuestToolTime > 0 &&
124
- this.consecutiveNonQuestToolCalls >= this.config.nudges.timeBasedToolCallThreshold &&
125
- Date.now() - this.lastQuestToolTime >= this.config.nudges.hintIntervalMinutes * 60 * 1000
126
- ) {
127
- candidates.push({
128
- index: 2,
129
- message: `QUEST REMINDER: It has been a while since your last quest tool use and many tools have been called since then. ALIGN on quest status before continuing. ${ACKNOWLEDGEMENT}`,
130
- });
131
- }
132
-
133
- // 3. Zero-active sustained-work nudge
134
- if (
135
- this.consecutiveNonQuestToolCalls >= this.config.nudges.zeroActiveToolCallThreshold &&
136
- activeQuestCount === 0
137
- ) {
138
- candidates.push({
139
- index: 3,
140
- message: `QUEST REMINDER: You have made several consecutive tool calls without using the quest tool and there are 0 active quests. TRACK your work with specific, actionable quests. ${ACKNOWLEDGEMENT}`,
141
- });
142
- }
143
-
144
- // 4. Sub-quest suggestion nudge
145
- if (
146
- this.hasEverUsedQuestTool &&
147
- this.consecutiveNonQuestToolCalls >= this.config.nudges.stepSuggestionToolCallThreshold &&
148
- activeQuestCount > 0 &&
149
- hasTopLevelQuestWithoutSubs
150
- ) {
151
- candidates.push({
152
- index: 4,
153
- message: `QUEST REMINDER: You have made several consecutive tool calls without using the quest tool and have active top-level quests without steps. Consider whether decomposing them with the \`split\` action would help track progress. ${ACKNOWLEDGEMENT}`,
154
- });
155
- }
156
-
157
- // 5. Stale-progress sustained-work nudge (with time-gate)
158
- if (
159
- this.consecutiveNonQuestToolCalls >= this.config.nudges.staleProgressToolCallThreshold &&
160
- activeQuestCount > 0 &&
161
- this.lastQuestToolTime > 0 &&
162
- Date.now() - this.lastQuestToolTime >= this.config.nudges.hintIntervalMinutes * 60 * 1000
163
- ) {
164
- candidates.push({
165
- index: 5,
166
- message: `QUEST REMINDER: You have made many consecutive tool calls without using the quest tool despite having active quests. UPDATE your quest progress to reflect current status. ${ACKNOWLEDGEMENT}`,
167
- });
168
- }
169
-
170
- return candidates;
38
+ this.lastQuestToolTimestamp = Date.now();
171
39
  }
172
40
 
173
- private isComplexPrompt(prompt: string): boolean {
174
- const lower = prompt.toLowerCase();
175
- return this.config.nudges.complexTaskKeywords.some((kw) => lower.includes(kw));
41
+ getStats(): QuestUsageStats {
42
+ return {
43
+ totalToolCalls: this.totalToolCalls,
44
+ questToolCalls: this.questToolCalls,
45
+ nonQuestToolCalls: this.nonQuestToolCalls,
46
+ consecutiveNonQuestToolCalls: this.consecutiveNonQuestToolCalls,
47
+ hasUsedQuestTool: this.hasEverUsedQuestTool,
48
+ lastQuestToolTimestamp: this.lastQuestToolTimestamp,
49
+ };
176
50
  }
177
51
  }
@@ -1,7 +1,7 @@
1
- import type { MessageRenderer, MessageRenderOptions, Theme } from "@mariozechner/pi-coding-agent";
2
- import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
3
- import type { Component } from "@mariozechner/pi-tui";
4
- import { Markdown } from "@mariozechner/pi-tui";
1
+ import type { MessageRenderer, MessageRenderOptions, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
3
+ import type { Component } from "@earendil-works/pi-tui";
4
+ import { Markdown } from "@earendil-works/pi-tui";
5
5
  import { logger } from "../logger.js";
6
6
 
7
7
  export const questChangelogRenderer: MessageRenderer<{ content: string }> = (
@@ -1,5 +1,5 @@
1
- import type { Theme } from "@mariozechner/pi-coding-agent";
2
- import { Key, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
3
  import type { ResolvedConfig } from "../config.js";
4
4
  import { logger } from "../logger.js";
5
5
  import type { QuestLog } from "../quest/dataplane.js";
@@ -1,5 +1,5 @@
1
- import type { Theme } from "@mariozechner/pi-coding-agent";
2
- import { visibleWidth } from "@mariozechner/pi-tui";
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { visibleWidth } from "@earendil-works/pi-tui";
3
3
  import type { Quest, Step } from "../quest/types.js";
4
4
 
5
5
  export function formatQuestRow(
@@ -15,8 +15,10 @@ export function formatQuestRow(
15
15
  const posWidth = visibleWidth(posText);
16
16
  const idText = `[${q.id}]`;
17
17
 
18
- // spacing is added to keep text aligned
19
- const spacing = " ".repeat(visibleWidth(`${16 ** idLength}`) - posWidth);
18
+ // Reserve width for the widest position label ("#" + up to `16 ** idLength`)
19
+ // and clamp so an unexpectedly wide label can never crash the renderer.
20
+ const posColWidth = visibleWidth(`#${16 ** idLength}`);
21
+ const spacing = " ".repeat(Math.max(0, posColWidth - posWidth));
20
22
  const idStr = `${theme.fg("muted", idText)} ${theme.fg(q.done ? "dim" : "accent", `${posText}`)}${spacing}`;
21
23
 
22
24
  // for steps, use a different marker and indent
@@ -36,8 +38,9 @@ export function formatQuestRow(
36
38
 
37
39
  export function formatStepSpacerLine(theme: Theme, idLength: number): string {
38
40
  const idTextLength = visibleWidth(`${16 ** idLength}`);
41
+ const posColWidth = visibleWidth(`#${16 ** idLength}`);
39
42
  // spacing for `[id](position string)`
40
- const spacerStr = `${" ".repeat(idTextLength + 2)}${" ".repeat(idTextLength)}`;
43
+ const spacerStr = `${" ".repeat(idTextLength + 2)}${" ".repeat(posColWidth)}`;
41
44
 
42
45
  const marker = theme.fg("muted", " │ ");
43
46
 
@@ -1,4 +1,4 @@
1
- import type { Theme } from "@mariozechner/pi-coding-agent";
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import type { QuestLog } from "../quest/dataplane.js";
3
3
 
4
4
  export class QuestStatusWidget {
@@ -1,5 +1,5 @@
1
- import type { AgentToolResult, Theme } from "@mariozechner/pi-coding-agent";
2
- import { Text } from "@mariozechner/pi-tui";
1
+ import { type AgentToolResult, keyHint, type Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
3
  import type { ResolvedConfig } from "../config.js";
4
4
  import { logger } from "../logger.js";
5
5
  import type { Quest } from "../quest/types.js";
@@ -15,6 +15,28 @@ type QuestArgs = {
15
15
  all?: boolean;
16
16
  };
17
17
 
18
+ /**
19
+ * Maximum number of result lines shown while a tool row is collapsed.
20
+ * Matches pi's default fallback preview so quest output behaves like built-in tools.
21
+ */
22
+ export const COLLAPSED_PREVIEW_LINES = 10;
23
+
24
+ /**
25
+ * Applies pi's standard compact-view treatment: the collapsed view shows a preview and an
26
+ * expand hint (`app.tools.expand`, ctrl+o by default); the expanded view shows every line.
27
+ */
28
+ function collapseLines(lines: string[], expanded: boolean, theme: Theme): string {
29
+ if (expanded || lines.length <= COLLAPSED_PREVIEW_LINES) return lines.join("\n");
30
+
31
+ const displayLines = lines.slice(0, COLLAPSED_PREVIEW_LINES);
32
+ const remaining = lines.length - displayLines.length;
33
+ const hint =
34
+ theme.fg("muted", `... (${remaining} more lines,`) +
35
+ ` ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
36
+
37
+ return `${displayLines.join("\n")}\n${hint}`;
38
+ }
39
+
18
40
  export function renderQuestCall(args: QuestArgs, theme: Theme, _context: unknown): Text {
19
41
  const id = "id" in args ? args.id : undefined;
20
42
  logger.debug("quests:tool", "renderCall", { action: args.action, id });
@@ -88,8 +110,12 @@ export function renderQuestResult(config: ResolvedConfig) {
88
110
  questsToRender.length === 0 ||
89
111
  !Array.isArray(allQuests)
90
112
  ) {
91
- const text = result.content.map((c) => (c.type === "text" ? c.text : "[image]")).join("\n");
92
- return new Text(theme.fg("text", text), 0, 0);
113
+ const textLines = result.content
114
+ .map((c) => (c.type === "text" ? c.text : "[image]"))
115
+ .join("\n")
116
+ .split("\n")
117
+ .map((line) => theme.fg("text", line));
118
+ return new Text(collapseLines(textLines, options.expanded, theme), 0, 0);
93
119
  }
94
120
 
95
121
  const quests = allQuests;
@@ -134,6 +160,6 @@ export function renderQuestResult(config: ResolvedConfig) {
134
160
  }
135
161
  }
136
162
 
137
- return new Text(lines.join("\n"), 0, 0);
163
+ return new Text(collapseLines(lines, options.expanded, theme), 0, 0);
138
164
  };
139
165
  }
@@ -1,4 +1,4 @@
1
- import type { AgentToolResult, ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
1
+ import type { AgentToolResult, ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { type Static, Type } from "typebox";
3
3
  import type { ResolvedConfig } from "../config.js";
4
4
  import { logger } from "../logger.js";
@@ -1,4 +1,4 @@
1
- import { StringEnum } from "@mariozechner/pi-ai";
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
2
  import { Type } from "typebox";
3
3
  import { QUEST_ACTION_VALUES } from "../quest/types.js";
4
4