pi-quests 0.3.0 → 0.5.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 +16 -0
- package/README.md +20 -1
- package/package.json +1 -1
- package/src/commands/handler.ts +43 -11
- package/src/commands/parse-args.ts +51 -22
- package/src/config.ts +148 -0
- package/src/index.ts +55 -23
- package/src/prompts/skill.md +60 -0
- package/src/prompts.ts +16 -2
- package/src/quest/dataplane.ts +473 -104
- package/src/quest/formatters.ts +119 -16
- package/src/quest/tracker.ts +99 -39
- package/src/quest/types.ts +15 -2
- package/src/renderers/commands.ts +46 -24
- package/src/renderers/quests.ts +45 -0
- package/src/renderers/tools.ts +80 -31
- package/src/tools/handler.ts +59 -16
- package/src/tools/params.ts +44 -30
package/src/quest/formatters.ts
CHANGED
|
@@ -1,35 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error message design guidelines
|
|
3
|
+
*
|
|
4
|
+
* All error messages should follow a consistent three-part pattern:
|
|
5
|
+
* 1. PROBLEM — state what went wrong clearly and concisely
|
|
6
|
+
* 2. RULE — explain the constraint or invariant that was violated
|
|
7
|
+
* 3. RECOVERY HINT — suggest the next valid action the caller can take
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* "Quest [01] has incomplete steps. A parent quest can only be marked
|
|
11
|
+
* done after all its steps are complete. Try toggling the steps first."
|
|
12
|
+
*
|
|
13
|
+
* Keep messages actionable. Agents should never be left guessing why something
|
|
14
|
+
* failed or what to do next.
|
|
15
|
+
*/
|
|
16
|
+
|
|
1
17
|
export function formatQuestList(
|
|
2
|
-
quests: { id:
|
|
18
|
+
quests: { id: string; description: string; done: boolean; parentId?: string }[],
|
|
3
19
|
): string {
|
|
4
20
|
if (quests.length === 0) return "No quests.";
|
|
5
21
|
|
|
6
|
-
|
|
22
|
+
const lines: string[] = [];
|
|
23
|
+
let pos = 1;
|
|
24
|
+
for (const q of quests) {
|
|
25
|
+
if (q.parentId) {
|
|
26
|
+
lines.push(` [${q.id}] [${q.done ? "x" : " "}] ${q.description}`);
|
|
27
|
+
} else {
|
|
28
|
+
lines.push(`#${pos} [${q.id}] [${q.done ? "x" : " "}] ${q.description}`);
|
|
29
|
+
pos++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatAddResult(q: { id: string; description: string }): string {
|
|
36
|
+
return `Added quest [${q.id}]: ${q.description}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatBatchAddResult(added: { id: string; description: string }[]): string {
|
|
40
|
+
return `Added ${added.length} quests:\n${added.map((q) => `[${q.id}]: ${q.description}`).join("\n")}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatToggleResult(id: string, done: boolean): string {
|
|
44
|
+
return `Quest [${id}] ${done ? "done" : "undone"}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatUpdateResult(q: { id: string; description: string }): string {
|
|
48
|
+
return `Updated quest [${q.id}]: ${q.description}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatDeleteResult(q: { id: string; description: string }): string {
|
|
52
|
+
return `Deleted quest [${q.id}]: ${q.description}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatNotFound(id: string): string {
|
|
56
|
+
return `Quest [${id}] not found. IDs are random hex strings shown in brackets. Use the list action to see valid IDs.`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatBlockedBySteps(id: string): string {
|
|
60
|
+
return `Quest [${id}] has incomplete steps. A parent quest can only be marked done or deleted after all its steps are complete. Toggle the steps first, or delete them if they are no longer needed.`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatStepCannotHaveSteps(id: string): string {
|
|
64
|
+
return `Step [${id}] cannot have nested steps. The quest system only supports one level of nesting. Use a top-level quest as the parent instead.`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function formatEmptyDescriptionsError(): string {
|
|
68
|
+
return `Every description in a batch must be non-empty. Remove empty strings or split the batch into separate add calls.`;
|
|
7
69
|
}
|
|
8
70
|
|
|
9
|
-
export function
|
|
10
|
-
return `
|
|
71
|
+
export function formatMissingDescriptionsError(action = "add"): string {
|
|
72
|
+
return `At least one description is required for the ${action} action. Provide a description string to create a quest.`;
|
|
11
73
|
}
|
|
12
74
|
|
|
13
|
-
export function
|
|
14
|
-
return `
|
|
75
|
+
export function formatIdRequiredError(action: string): string {
|
|
76
|
+
return `An id is required for the ${action} action. Use the list action to see valid quest IDs. IDs are the hex strings shown in square brackets.`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatDescriptionRequiredError(): string {
|
|
80
|
+
return `A description is required for the update action. Provide a non-empty string to update the quest.`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function formatTargetIdRequiredError(): string {
|
|
84
|
+
return `A targetId is required for the reorder action. Provide the hex ID of the quest to insert before.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatReorderNotFoundError(): string {
|
|
88
|
+
return `Quest not found or is a step. Reorder only works on top-level quests. Use the list action to verify the ID and ensure it is a top-level quest.`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatParentNotFoundError(id: string): string {
|
|
92
|
+
return `Parent quest [${id}] not found. Use the list action to see valid parent IDs.`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function formatParentDoneError(id: string): string {
|
|
96
|
+
return `Cannot add a step to completed parent quest [${id}]. Steps can only be added to open parents. Reopen the parent first if you need to add more work.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function formatUnknownActionError(action: string): string {
|
|
100
|
+
return `Unknown action: ${action}. Use the list action or check the tool schema for supported actions.`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function formatNothingToRevertError(): string {
|
|
104
|
+
return `Nothing to revert. The history is empty because no mutating actions have been performed yet.`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function formatReorderedQuestNotFoundError(): string {
|
|
108
|
+
return `Reordered quest not found. It may have been deleted or cleared since the reorder was recorded.`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function formatReparentResult(
|
|
112
|
+
q: { id: string; description: string },
|
|
113
|
+
parentId?: string,
|
|
114
|
+
): string {
|
|
115
|
+
return parentId
|
|
116
|
+
? `Moved quest [${q.id}] under [${parentId}]: ${q.description}`
|
|
117
|
+
: `Promoted quest [${q.id}] to top-level: ${q.description}`;
|
|
15
118
|
}
|
|
16
119
|
|
|
17
|
-
export function
|
|
18
|
-
return `
|
|
120
|
+
export function formatReparentTargetNotFoundError(id: string): string {
|
|
121
|
+
return `Target quest [${id}] not found. Use the list action to see valid parent IDs.`;
|
|
19
122
|
}
|
|
20
123
|
|
|
21
|
-
export function
|
|
22
|
-
return `
|
|
124
|
+
export function formatReparentTargetIsStepError(id: string): string {
|
|
125
|
+
return `Step [${id}] cannot be a parent. The quest system only supports one level of nesting. Use a top-level quest as the parent instead.`;
|
|
23
126
|
}
|
|
24
127
|
|
|
25
|
-
export function
|
|
26
|
-
return `
|
|
128
|
+
export function formatReparentTargetDoneError(id: string): string {
|
|
129
|
+
return `Cannot reparent under completed parent [${id}]. Steps can only be added to open parents.`;
|
|
27
130
|
}
|
|
28
131
|
|
|
29
|
-
export function
|
|
30
|
-
return `
|
|
132
|
+
export function formatReparentDemoteHasStepsError(id: string): string {
|
|
133
|
+
return `Quest [${id}] has steps and cannot be demoted to a step. Delete or reparent its steps first.`;
|
|
31
134
|
}
|
|
32
135
|
|
|
33
|
-
export function
|
|
34
|
-
return `Quest
|
|
136
|
+
export function formatReparentSelfParentError(id: string): string {
|
|
137
|
+
return `Quest [${id}] cannot be its own parent. Choose a different parent ID.`;
|
|
35
138
|
}
|
package/src/quest/tracker.ts
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.";
|
|
1
|
+
import type { ResolvedConfig } from "../config.js";
|
|
2
|
+
|
|
3
|
+
const ACKNOWLEDGEMENT = "Update your quest status before continuing.";
|
|
4
|
+
|
|
5
|
+
type NudgeCandidate = { index: number; message: string };
|
|
18
6
|
|
|
19
7
|
export class QuestUsageTracker {
|
|
20
8
|
private totalToolCalls = 0;
|
|
@@ -22,6 +10,10 @@ export class QuestUsageTracker {
|
|
|
22
10
|
private hasEverUsedQuestTool = false;
|
|
23
11
|
private lastQuestToolTime = 0;
|
|
24
12
|
private nudgedThisTurn = false;
|
|
13
|
+
private lastNudgeTime = 0;
|
|
14
|
+
private lastNudgeIndex = -1;
|
|
15
|
+
|
|
16
|
+
constructor(private readonly config: ResolvedConfig) {}
|
|
25
17
|
|
|
26
18
|
onToolExecution(toolName: string): void {
|
|
27
19
|
this.totalToolCalls++;
|
|
@@ -38,50 +30,118 @@ export class QuestUsageTracker {
|
|
|
38
30
|
this.nudgedThisTurn = false;
|
|
39
31
|
}
|
|
40
32
|
|
|
41
|
-
getNudge(
|
|
33
|
+
getNudge(
|
|
34
|
+
activeQuestCount: number,
|
|
35
|
+
latestPrompt?: string,
|
|
36
|
+
hasTopLevelQuestWithoutSubs?: boolean,
|
|
37
|
+
): string | undefined {
|
|
42
38
|
if (this.nudgedThisTurn) return undefined;
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const cooldownMs = this.config.nudges.hintIntervalMinutes * 60 * 1000;
|
|
42
|
+
if (this.lastNudgeTime > 0 && now - this.lastNudgeTime < cooldownMs) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const eligible = this.getEligibleNudges(
|
|
47
|
+
activeQuestCount,
|
|
48
|
+
latestPrompt,
|
|
49
|
+
hasTopLevelQuestWithoutSubs,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Rotate priority: start checking from the nudge after the last one that fired
|
|
53
|
+
const rotated = [
|
|
54
|
+
...eligible.filter((n) => n.index > this.lastNudgeIndex),
|
|
55
|
+
...eligible.filter((n) => n.index <= this.lastNudgeIndex),
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const winner = rotated[0];
|
|
59
|
+
if (winner) {
|
|
46
60
|
this.nudgedThisTurn = true;
|
|
47
|
-
|
|
61
|
+
this.lastNudgeTime = now;
|
|
62
|
+
this.lastNudgeIndex = winner.index;
|
|
63
|
+
return winner.message;
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private getEligibleNudges(
|
|
70
|
+
activeQuestCount: number,
|
|
71
|
+
latestPrompt?: string,
|
|
72
|
+
hasTopLevelQuestWithoutSubs?: boolean,
|
|
73
|
+
): NudgeCandidate[] {
|
|
74
|
+
const candidates: NudgeCandidate[] = [];
|
|
75
|
+
|
|
76
|
+
// 0. Initialization nudge
|
|
77
|
+
if (this.totalToolCalls >= this.config.nudges.toolCallThreshold && !this.hasEverUsedQuestTool) {
|
|
78
|
+
candidates.push({
|
|
79
|
+
index: 0,
|
|
80
|
+
message: `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}`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 1. Complex-task entrypoint nudge
|
|
51
85
|
if (activeQuestCount === 0 && latestPrompt && this.isComplexPrompt(latestPrompt)) {
|
|
52
|
-
|
|
53
|
-
|
|
86
|
+
candidates.push({
|
|
87
|
+
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}`,
|
|
89
|
+
});
|
|
54
90
|
}
|
|
55
91
|
|
|
56
|
-
//
|
|
92
|
+
// 2. Time-based alignment nudge
|
|
57
93
|
if (
|
|
58
94
|
this.hasEverUsedQuestTool &&
|
|
59
95
|
this.lastQuestToolTime > 0 &&
|
|
60
|
-
this.consecutiveNonQuestToolCalls >=
|
|
61
|
-
|
|
62
|
-
Date.now() - this.lastQuestToolTime >= 8 * 60 * 1000
|
|
96
|
+
this.consecutiveNonQuestToolCalls >= this.config.nudges.timeBasedToolCallThreshold &&
|
|
97
|
+
Date.now() - this.lastQuestToolTime >= this.config.nudges.hintIntervalMinutes * 60 * 1000
|
|
63
98
|
) {
|
|
64
|
-
|
|
65
|
-
|
|
99
|
+
candidates.push({
|
|
100
|
+
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}`,
|
|
102
|
+
});
|
|
66
103
|
}
|
|
67
104
|
|
|
68
|
-
//
|
|
69
|
-
if (
|
|
70
|
-
this.
|
|
71
|
-
|
|
105
|
+
// 3. Zero-active sustained-work nudge
|
|
106
|
+
if (
|
|
107
|
+
this.consecutiveNonQuestToolCalls >= this.config.nudges.zeroActiveToolCallThreshold &&
|
|
108
|
+
activeQuestCount === 0
|
|
109
|
+
) {
|
|
110
|
+
candidates.push({
|
|
111
|
+
index: 3,
|
|
112
|
+
message: `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}`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 4. Sub-quest suggestion nudge
|
|
117
|
+
if (
|
|
118
|
+
this.hasEverUsedQuestTool &&
|
|
119
|
+
this.consecutiveNonQuestToolCalls >= this.config.nudges.stepSuggestionToolCallThreshold &&
|
|
120
|
+
activeQuestCount > 0 &&
|
|
121
|
+
hasTopLevelQuestWithoutSubs
|
|
122
|
+
) {
|
|
123
|
+
candidates.push({
|
|
124
|
+
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}`,
|
|
126
|
+
});
|
|
72
127
|
}
|
|
73
128
|
|
|
74
129
|
// 5. Stale-progress sustained-work nudge
|
|
75
|
-
if (
|
|
76
|
-
this.
|
|
77
|
-
|
|
130
|
+
if (
|
|
131
|
+
this.consecutiveNonQuestToolCalls >= this.config.nudges.staleProgressToolCallThreshold &&
|
|
132
|
+
activeQuestCount > 0
|
|
133
|
+
) {
|
|
134
|
+
candidates.push({
|
|
135
|
+
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}`,
|
|
137
|
+
});
|
|
78
138
|
}
|
|
79
139
|
|
|
80
|
-
return
|
|
140
|
+
return candidates;
|
|
81
141
|
}
|
|
82
142
|
|
|
83
143
|
private isComplexPrompt(prompt: string): boolean {
|
|
84
144
|
const lower = prompt.toLowerCase();
|
|
85
|
-
return
|
|
145
|
+
return this.config.nudges.complexTaskKeywords.some((kw) => lower.includes(kw));
|
|
86
146
|
}
|
|
87
147
|
}
|
package/src/quest/types.ts
CHANGED
|
@@ -11,6 +11,11 @@ export const QUEST_ACTIONS = {
|
|
|
11
11
|
clear: "clear",
|
|
12
12
|
reorder: "reorder",
|
|
13
13
|
revert: "revert",
|
|
14
|
+
reparent: "reparent",
|
|
15
|
+
rules: "rules",
|
|
16
|
+
skill: "skill",
|
|
17
|
+
split: "split",
|
|
18
|
+
add_step: "add_step",
|
|
14
19
|
} as const;
|
|
15
20
|
|
|
16
21
|
export const QUEST_ACTION_VALUES = [
|
|
@@ -22,14 +27,22 @@ export const QUEST_ACTION_VALUES = [
|
|
|
22
27
|
QUEST_ACTIONS.clear,
|
|
23
28
|
QUEST_ACTIONS.reorder,
|
|
24
29
|
QUEST_ACTIONS.revert,
|
|
30
|
+
QUEST_ACTIONS.reparent,
|
|
31
|
+
QUEST_ACTIONS.rules,
|
|
32
|
+
QUEST_ACTIONS.skill,
|
|
33
|
+
QUEST_ACTIONS.split,
|
|
34
|
+
QUEST_ACTIONS.add_step,
|
|
25
35
|
] as const;
|
|
26
36
|
|
|
27
37
|
export type QuestActionType = (typeof QUEST_ACTION_VALUES)[number];
|
|
28
38
|
|
|
29
39
|
export interface Quest {
|
|
30
|
-
id:
|
|
40
|
+
id: string;
|
|
31
41
|
description: string;
|
|
32
|
-
additionalContext?: string;
|
|
33
42
|
done: boolean;
|
|
34
43
|
createdAt: number;
|
|
35
44
|
}
|
|
45
|
+
|
|
46
|
+
export interface Step extends Quest {
|
|
47
|
+
parentId: string;
|
|
48
|
+
}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { Key, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
3
|
+
import type { ResolvedConfig } from "../config.js";
|
|
3
4
|
import { logger } from "../logger.js";
|
|
4
|
-
import type {
|
|
5
|
+
import type { QuestLog } from "../quest/dataplane.js";
|
|
6
|
+
import { formatQuestRow, formatStepSpacerLine } from "./quests.js";
|
|
5
7
|
|
|
6
8
|
export class QuestListWidget {
|
|
7
9
|
private cachedWidth?: number;
|
|
8
10
|
private cachedLines?: string[];
|
|
9
11
|
private page = 0;
|
|
10
|
-
private readonly pageSize = 10;
|
|
11
12
|
|
|
12
13
|
constructor(
|
|
13
|
-
private
|
|
14
|
+
private questLog: QuestLog,
|
|
14
15
|
private theme: Theme,
|
|
15
16
|
private onClose: () => void,
|
|
17
|
+
private readonly config: ResolvedConfig,
|
|
16
18
|
) {
|
|
17
|
-
logger.debug("quests:widget", "create", { questCount:
|
|
19
|
+
logger.debug("quests:widget", "create", { questCount: questLog.getAll().length });
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
handleInput(data: string): void {
|
|
@@ -37,7 +39,35 @@ export class QuestListWidget {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
private get totalPages(): number {
|
|
40
|
-
|
|
42
|
+
const totalLines = this.buildQuestLines(0).length;
|
|
43
|
+
return Math.max(1, Math.ceil(totalLines / this.config.display.pageSize));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private buildQuestLines(width: number): string[] {
|
|
47
|
+
const th = this.theme;
|
|
48
|
+
const parents = this.questLog.getQuests();
|
|
49
|
+
const lines: string[] = [];
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < parents.length; i++) {
|
|
52
|
+
const parent = parents[i];
|
|
53
|
+
const steps = this.questLog.getSteps(parent.id);
|
|
54
|
+
|
|
55
|
+
const row = formatQuestRow(th, parent, this.config.ids.length, i + 1);
|
|
56
|
+
lines.push(width > 0 ? truncateToWidth(row, width) : row);
|
|
57
|
+
|
|
58
|
+
if (steps.length > 0) {
|
|
59
|
+
lines.push(formatStepSpacerLine(th, this.config.ids.length));
|
|
60
|
+
|
|
61
|
+
for (const step of steps) {
|
|
62
|
+
const stepRow = formatQuestRow(th, step, this.config.ids.length);
|
|
63
|
+
lines.push(width > 0 ? truncateToWidth(stepRow, width) : stepRow);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
lines.push("");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return lines;
|
|
41
71
|
}
|
|
42
72
|
|
|
43
73
|
private nextPage(): void {
|
|
@@ -72,13 +102,14 @@ export class QuestListWidget {
|
|
|
72
102
|
width,
|
|
73
103
|
page: this.page,
|
|
74
104
|
totalPages: this.totalPages,
|
|
75
|
-
totalQuests: this.
|
|
105
|
+
totalQuests: this.questLog.getAll().length,
|
|
76
106
|
});
|
|
77
107
|
|
|
78
108
|
const th = this.theme;
|
|
79
109
|
const lines: string[] = [];
|
|
80
|
-
const
|
|
81
|
-
const
|
|
110
|
+
const allQuests = this.questLog.getAll();
|
|
111
|
+
const total = allQuests.length;
|
|
112
|
+
const doneCount = allQuests.filter((q) => q.done).length;
|
|
82
113
|
|
|
83
114
|
// Top accent border
|
|
84
115
|
lines.push(th.fg("accent", "─".repeat(width)));
|
|
@@ -91,7 +122,7 @@ export class QuestListWidget {
|
|
|
91
122
|
|
|
92
123
|
lines.push("");
|
|
93
124
|
|
|
94
|
-
if (
|
|
125
|
+
if (total === 0) {
|
|
95
126
|
lines.push(
|
|
96
127
|
truncateToWidth(
|
|
97
128
|
` ${th.fg("dim", "No active quests. Add one with /quests add <description>")}`,
|
|
@@ -100,7 +131,7 @@ export class QuestListWidget {
|
|
|
100
131
|
);
|
|
101
132
|
} else {
|
|
102
133
|
// Mini progress bar
|
|
103
|
-
const barWidth = Math.min(width - 4,
|
|
134
|
+
const barWidth = Math.min(width - 4, this.config.display.progressBarMaxWidth);
|
|
104
135
|
const filled = Math.round((doneCount / total) * barWidth);
|
|
105
136
|
const empty = barWidth - filled;
|
|
106
137
|
const bar = th.fg("success", "█".repeat(filled)) + th.fg("dim", "░".repeat(empty));
|
|
@@ -108,20 +139,11 @@ export class QuestListWidget {
|
|
|
108
139
|
lines.push(truncateToWidth(` ${bar} ${th.fg("muted", `${doneCount}/${total}`)}`, width));
|
|
109
140
|
lines.push("");
|
|
110
141
|
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const marker = q.done ? th.fg("success", " ✓ ") : th.fg("muted", " ○ ");
|
|
117
|
-
const idStr = th.fg(q.done ? "dim" : "accent", `#${pos}`);
|
|
118
|
-
const desc = q.done
|
|
119
|
-
? th.fg("dim", th.strikethrough(q.description))
|
|
120
|
-
: th.fg("text", q.description);
|
|
121
|
-
const row = `${marker}${idStr} ${desc}`;
|
|
122
|
-
|
|
123
|
-
lines.push(truncateToWidth(row, width));
|
|
124
|
-
}
|
|
142
|
+
const questLines = this.buildQuestLines(width);
|
|
143
|
+
const start = this.page * this.config.display.pageSize;
|
|
144
|
+
const pageLines = questLines.slice(start, start + this.config.display.pageSize);
|
|
145
|
+
|
|
146
|
+
lines.push(...pageLines);
|
|
125
147
|
|
|
126
148
|
if (this.totalPages > 1) {
|
|
127
149
|
const pageInfo = ` Page ${this.page + 1}/${this.totalPages} ${th.fg("dim", "· Tab/Shift+Tab to navigate")}`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { visibleWidth } from "@mariozechner/pi-tui";
|
|
3
|
+
import type { Quest, Step } from "../quest/types.js";
|
|
4
|
+
|
|
5
|
+
export function formatQuestRow(
|
|
6
|
+
theme: Theme,
|
|
7
|
+
q: Quest | Step,
|
|
8
|
+
idLength: number,
|
|
9
|
+
pos?: number,
|
|
10
|
+
): string {
|
|
11
|
+
const isStep = "parentId" in q && q.parentId;
|
|
12
|
+
|
|
13
|
+
// position test is only relevant for quests, not steps
|
|
14
|
+
const posText = isStep ? "" : pos !== undefined ? `#${pos}` : "";
|
|
15
|
+
const posWidth = visibleWidth(posText);
|
|
16
|
+
const idText = `[${q.id}]`;
|
|
17
|
+
|
|
18
|
+
// spacing is added to keep text aligned
|
|
19
|
+
const spacing = " ".repeat(visibleWidth(`${16 ** idLength}`) - posWidth);
|
|
20
|
+
const idStr = `${theme.fg("muted", idText)} ${theme.fg(q.done ? "dim" : "accent", `${posText}`)}${spacing}`;
|
|
21
|
+
|
|
22
|
+
// for steps, use a different marker and indent
|
|
23
|
+
const markerNotDone = isStep ? theme.fg("muted", " └── ○ ") : theme.fg("muted", " ○ ");
|
|
24
|
+
const markerDone = isStep ? theme.fg("muted", " └── ✓ ") : theme.fg("success", " ✓ ");
|
|
25
|
+
const marker = q.done ? markerDone : markerNotDone;
|
|
26
|
+
|
|
27
|
+
// for steps, use dim text to contrast with the parent quests
|
|
28
|
+
const descColor = q.done || isStep ? "dim" : "text";
|
|
29
|
+
const desc = q.done
|
|
30
|
+
? theme.fg(descColor, theme.strikethrough(q.description))
|
|
31
|
+
: theme.fg(descColor, q.description);
|
|
32
|
+
|
|
33
|
+
const line = ` ${idStr}${marker} ${desc}`;
|
|
34
|
+
return line;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function formatStepSpacerLine(theme: Theme, idLength: number): string {
|
|
38
|
+
const idTextLength = visibleWidth(`${16 ** idLength}`);
|
|
39
|
+
// spacing for `[id](position string)`
|
|
40
|
+
const spacerStr = `${" ".repeat(idTextLength + 2)}${" ".repeat(idTextLength)}`;
|
|
41
|
+
|
|
42
|
+
const marker = theme.fg("muted", " │ ");
|
|
43
|
+
|
|
44
|
+
return ` ${spacerStr}${marker}`;
|
|
45
|
+
}
|