pi-quests 0.1.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 +14 -0
- package/LICENSE.md +21 -0
- package/README.md +45 -0
- package/package.json +54 -0
- package/src/commands/quests.ts +220 -0
- package/src/index.ts +33 -0
- package/src/logger.ts +15 -0
- package/src/quests.ts +222 -0
- package/src/renderers/changelog.ts +26 -0
- package/src/renderers/commands.ts +141 -0
- package/src/renderers/tools.ts +69 -0
- package/src/tools/quest.ts +136 -0
- package/src/version.ts +11 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# pi-quests Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-04-10
|
|
6
|
+
|
|
7
|
+
- feat: add session-scoped quest log with tools, commands, and TUI widgets
|
|
8
|
+
- add `QuestLog` with history stack, revert, and session reconstruction
|
|
9
|
+
- expose `quest` tool with add, list, toggle, update, delete, clear, and revert
|
|
10
|
+
- add `/quests` command namespace with interactive list widget and pagination
|
|
11
|
+
- split renderers into dedicated tools, commands, and changelog modules
|
|
12
|
+
- include `AGENTS.md` and concise docs for architecture, patterns, and reference
|
|
13
|
+
- chore: setup initial pi extension boilerplate
|
|
14
|
+
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kalindu De Costa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# pi-quests
|
|
2
|
+
|
|
3
|
+
[](CHANGELOG.md)
|
|
4
|
+
[](LICENSE.md)
|
|
5
|
+
[](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent)
|
|
6
|
+
|
|
7
|
+
A quest-log for your [pi](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent). Keep your agent on track, one quest at a time.
|
|
8
|
+
|
|
9
|
+
## Why pi-quests?
|
|
10
|
+
|
|
11
|
+
Long agent sessions drift. Goals get lost in tool calls, side quests multiply, and the original task is forgotten under a pile of yak hair.
|
|
12
|
+
|
|
13
|
+
pi-quests gives your agent a persistent quest log — a living TODO list for the current session. Each quest is a checkpoint the agent can create, complete, and optionally roll back to if things go sideways.
|
|
14
|
+
|
|
15
|
+
- **Stay focused** — the quest log keeps the original goal visible no matter how deep the rabbit hole goes
|
|
16
|
+
- **Checkpoint progress** — mark milestones as quests and capture lightweight session snapshots
|
|
17
|
+
- **Rollback safety** — revert to a previous quest snapshot when exploration goes off track
|
|
18
|
+
- **Session-scoped** — quests live with the session, vanish when you start fresh
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
TBD
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
**See the extension version:**
|
|
27
|
+
```bash
|
|
28
|
+
/quests version
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**View the changelog:**
|
|
32
|
+
```bash
|
|
33
|
+
/quests changelog
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Documentation
|
|
37
|
+
|
|
38
|
+
| Doc | Description |
|
|
39
|
+
|-----|-------------|
|
|
40
|
+
| [Roadmap](docs/roadmap.md) | Feature backlog and progress tracker |
|
|
41
|
+
| [Changelog](CHANGELOG.md) | Version history |
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT — see [LICENSE.md](LICENSE.md)
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-quests",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A quest-log for your pi. Keep your agent on track, one quest at a time.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"quests",
|
|
11
|
+
"snapshot",
|
|
12
|
+
"rollback",
|
|
13
|
+
"todo"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/kalindudc/pi-quests.git"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./src/index.ts"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src/",
|
|
30
|
+
"README.md",
|
|
31
|
+
"CHANGELOG.md",
|
|
32
|
+
"LICENSE.md"
|
|
33
|
+
],
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
36
|
+
"@mariozechner/pi-tui": "*"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@biomejs/biome": "^2.4.10",
|
|
40
|
+
"@mariozechner/pi-coding-agent": "^0.63.2",
|
|
41
|
+
"@mariozechner/pi-tui": "^0.63.0",
|
|
42
|
+
"typescript": "^5.7.0",
|
|
43
|
+
"vitest": "^3.0.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"style": "biome check ./src ./test",
|
|
50
|
+
"style:fix": "biome check --write --unsafe --no-errors-on-unmatched ./src ./test",
|
|
51
|
+
"format": "biome format --write --no-errors-on-unmatched ./src ./test",
|
|
52
|
+
"prepublishOnly": "npm run typecheck && npm test"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { logger } from "../logger.js";
|
|
4
|
+
import {
|
|
5
|
+
formatClearResult,
|
|
6
|
+
formatDeleteResult,
|
|
7
|
+
formatNotFound,
|
|
8
|
+
formatToggleResult,
|
|
9
|
+
formatUpdateResult,
|
|
10
|
+
type QuestLog,
|
|
11
|
+
} from "../quests.js";
|
|
12
|
+
import { QuestListWidget } from "../renderers/commands.js";
|
|
13
|
+
import { CHANGELOG_PATH, VERSION } from "../version.js";
|
|
14
|
+
|
|
15
|
+
// Helper function to reverse changelog sections so newest appears first
|
|
16
|
+
function reverseChangelog(content: string): string {
|
|
17
|
+
const lines = content.split("\n");
|
|
18
|
+
const sections: string[][] = [];
|
|
19
|
+
let currentSection: string[] = [];
|
|
20
|
+
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
if (line.startsWith("## [")) {
|
|
23
|
+
if (currentSection.length > 0) {
|
|
24
|
+
sections.push(currentSection);
|
|
25
|
+
}
|
|
26
|
+
currentSection = [line];
|
|
27
|
+
} else {
|
|
28
|
+
currentSection.push(line);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (currentSection.length > 0) {
|
|
33
|
+
sections.push(currentSection);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Reverse sections and flatten
|
|
37
|
+
const reversed = sections.reverse().flat();
|
|
38
|
+
return reversed.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ParsedArgs =
|
|
42
|
+
| { action: "add"; description: string }
|
|
43
|
+
| { action: "list" }
|
|
44
|
+
| { action: "toggle"; id: number }
|
|
45
|
+
| { action: "update"; id: number; description: string }
|
|
46
|
+
| { action: "delete"; id: number }
|
|
47
|
+
| { action: "clear" }
|
|
48
|
+
| { action: "revert" }
|
|
49
|
+
| { action: "help" }
|
|
50
|
+
| { action: "version" }
|
|
51
|
+
| { action: "changelog" }
|
|
52
|
+
| { error: string };
|
|
53
|
+
|
|
54
|
+
export function parseQuestArgs(args: string): ParsedArgs {
|
|
55
|
+
logger.debug("quests:cmd", "parse-args", { args });
|
|
56
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
57
|
+
if (tokens.length === 0) {
|
|
58
|
+
logger.debug("quests:cmd", "parse-args-empty", { action: "list" });
|
|
59
|
+
return { action: "list" };
|
|
60
|
+
}
|
|
61
|
+
const action = tokens[0];
|
|
62
|
+
if (action === "version") return { action: "version" };
|
|
63
|
+
if (action === "changelog") return { action: "changelog" };
|
|
64
|
+
if (action === "help" || action === "h") return { action: "help" };
|
|
65
|
+
if (action === "list") return { action: "list" };
|
|
66
|
+
if (action === "clear") return { action: "clear" };
|
|
67
|
+
if (action === "revert") return { action: "revert" };
|
|
68
|
+
if (action === "add") {
|
|
69
|
+
const description = tokens.slice(1).join(" ").trim();
|
|
70
|
+
if (!description) return { error: "Usage: /quests add <description>" };
|
|
71
|
+
return { action: "add", description };
|
|
72
|
+
}
|
|
73
|
+
if (action === "toggle") {
|
|
74
|
+
const idStr = tokens[1];
|
|
75
|
+
const id = idStr ? Number(idStr) : NaN;
|
|
76
|
+
if (Number.isNaN(id)) return { error: "Usage: /quests toggle <id>" };
|
|
77
|
+
return { action: "toggle", id };
|
|
78
|
+
}
|
|
79
|
+
if (action === "delete") {
|
|
80
|
+
const idStr = tokens[1];
|
|
81
|
+
const id = idStr ? Number(idStr) : NaN;
|
|
82
|
+
if (Number.isNaN(id)) return { error: "Usage: /quests delete <id>" };
|
|
83
|
+
return { action: "delete", id };
|
|
84
|
+
}
|
|
85
|
+
if (action === "update") {
|
|
86
|
+
const idStr = tokens[1];
|
|
87
|
+
const id = idStr ? Number(idStr) : NaN;
|
|
88
|
+
if (Number.isNaN(id)) return { error: "Usage: /quests update <id> <description>" };
|
|
89
|
+
const description = tokens.slice(2).join(" ").trim();
|
|
90
|
+
if (!description) return { error: "Usage: /quests update <id> <description>" };
|
|
91
|
+
return { action: "update", id, description };
|
|
92
|
+
}
|
|
93
|
+
return { error: `Unknown subcommand: ${action}. Use /quests help to see available commands.` };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createQuestsHandler(pi: ExtensionAPI, questLog: QuestLog) {
|
|
97
|
+
return async function handler(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
98
|
+
logger.debug("quests:cmd", "handler", { args, hasUI: ctx.hasUI });
|
|
99
|
+
const parsed = parseQuestArgs(args);
|
|
100
|
+
if ("error" in parsed) {
|
|
101
|
+
logger.debug("quests:cmd", "handler-error", { error: parsed.error });
|
|
102
|
+
ctx.ui.notify(parsed.error, "error");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
switch (parsed.action) {
|
|
107
|
+
case "version": {
|
|
108
|
+
logger.debug("quests:cmd", "version", { version: VERSION });
|
|
109
|
+
ctx.ui.notify(`pi-quests v${VERSION}`, "info");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
case "changelog": {
|
|
113
|
+
logger.debug("quests:cmd", "changelog", { changelogPath: CHANGELOG_PATH });
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const content = readFileSync(CHANGELOG_PATH, "utf-8");
|
|
117
|
+
logger.debug("quests:cmd", "changelog-read", { contentLength: content.length });
|
|
118
|
+
const reversedContent = reverseChangelog(content);
|
|
119
|
+
logger.debug("quests:cmd", "changelog-reversed");
|
|
120
|
+
|
|
121
|
+
pi.sendMessage({
|
|
122
|
+
customType: "quest-changelog",
|
|
123
|
+
content: "",
|
|
124
|
+
display: true,
|
|
125
|
+
details: { content: reversedContent },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
logger.debug("quests:cmd", "changelog-sent");
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
131
|
+
logger.debug("quests:cmd", "changelog-error", { error: errorMessage });
|
|
132
|
+
ctx.ui.notify(`Failed to read changelog: ${errorMessage}`, "error");
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
case "add": {
|
|
137
|
+
const q = questLog.add(parsed.description);
|
|
138
|
+
logger.debug("quests:cmd", "add", { id: q.id });
|
|
139
|
+
ctx.ui.notify(`Added quest #${q.id}`, "info");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
case "list": {
|
|
143
|
+
if (!ctx.hasUI) {
|
|
144
|
+
logger.debug("quests:cmd", "list-no-ui");
|
|
145
|
+
ctx.ui.notify("Interactive mode required", "error");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
logger.debug("quests:cmd", "list-open-widget", { count: questLog.getAll().length });
|
|
149
|
+
await ctx.ui.custom(
|
|
150
|
+
(_, theme, __, done) =>
|
|
151
|
+
new QuestListWidget(questLog.getAll(), theme, () => done(undefined)),
|
|
152
|
+
);
|
|
153
|
+
logger.debug("quests:cmd", "list-widget-closed");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
case "toggle": {
|
|
157
|
+
const q = questLog.toggle(parsed.id);
|
|
158
|
+
if (!q) {
|
|
159
|
+
logger.debug("quests:cmd", "toggle-not-found", { id: parsed.id });
|
|
160
|
+
ctx.ui.notify(formatNotFound(parsed.id), "error");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
logger.debug("quests:cmd", "toggle", { id: parsed.id, state: q.done });
|
|
164
|
+
ctx.ui.notify(formatToggleResult(parsed.id, q.done), "info");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
case "update": {
|
|
168
|
+
const q = questLog.update(parsed.id, parsed.description);
|
|
169
|
+
if (!q) {
|
|
170
|
+
logger.debug("quests:cmd", "update-not-found", { id: parsed.id });
|
|
171
|
+
ctx.ui.notify(formatNotFound(parsed.id), "error");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
logger.debug("quests:cmd", "update", { id: parsed.id });
|
|
175
|
+
ctx.ui.notify(formatUpdateResult(q), "info");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
case "delete": {
|
|
179
|
+
const q = questLog.delete(parsed.id);
|
|
180
|
+
if (!q) {
|
|
181
|
+
logger.debug("quests:cmd", "delete-not-found", { id: parsed.id });
|
|
182
|
+
ctx.ui.notify(formatNotFound(parsed.id), "error");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
logger.debug("quests:cmd", "delete", { id: parsed.id });
|
|
186
|
+
ctx.ui.notify(formatDeleteResult(q), "info");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
case "clear": {
|
|
190
|
+
const count = questLog.clear();
|
|
191
|
+
logger.debug("quests:cmd", "clear", { count });
|
|
192
|
+
ctx.ui.notify(formatClearResult(count), "info");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
case "revert": {
|
|
196
|
+
const result = questLog.revert();
|
|
197
|
+
logger.debug("quests:cmd", "revert", { success: result.success });
|
|
198
|
+
ctx.ui.notify(result.message, result.success ? "info" : "error");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
case "help": {
|
|
202
|
+
logger.debug("quests:cmd", "help");
|
|
203
|
+
const lines = ["Available /quests subcommands:"];
|
|
204
|
+
lines.push(" add <description> - Add a new quest");
|
|
205
|
+
lines.push(" list - List all quests");
|
|
206
|
+
lines.push(" toggle <id> - Toggle quest completion");
|
|
207
|
+
lines.push(" delete <id> - Delete a quest");
|
|
208
|
+
lines.push(" update <id> <desc> - Update a quest description");
|
|
209
|
+
lines.push(" revert - Revert the last quest change");
|
|
210
|
+
lines.push(" clear - Clear all quests");
|
|
211
|
+
lines.push(" version - Show version");
|
|
212
|
+
lines.push(" changelog - Show changelog");
|
|
213
|
+
lines.push(" h, help - Show this help message");
|
|
214
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
215
|
+
logger.debug("quests:cmd", "help-complete");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { createQuestsHandler } from "./commands/quests.js";
|
|
3
|
+
import { QuestLog } from "./quests.js";
|
|
4
|
+
import { questChangelogRenderer } from "./renderers/changelog.js";
|
|
5
|
+
import { registerQuestTool } from "./tools/quest.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* pi-quests
|
|
9
|
+
*
|
|
10
|
+
* A quest-log extension for pi-coding-agent.
|
|
11
|
+
*
|
|
12
|
+
* Roadmap:
|
|
13
|
+
* - Register tools for agents to add, list, and manage quest items.
|
|
14
|
+
* - Snapshot relevant session state at each quest milestone.
|
|
15
|
+
* - Provide rollback support to restore a previous snapshot.
|
|
16
|
+
*/
|
|
17
|
+
export default function (pi: ExtensionAPI): void {
|
|
18
|
+
const questLog = new QuestLog();
|
|
19
|
+
|
|
20
|
+
pi.on("session_start", async (_event, ctx) => questLog.reconstructFromSession(ctx));
|
|
21
|
+
pi.on("session_tree", async (_event, ctx) => questLog.reconstructFromSession(ctx));
|
|
22
|
+
|
|
23
|
+
registerQuestTool(pi, questLog);
|
|
24
|
+
|
|
25
|
+
// Register custom message renderers
|
|
26
|
+
pi.registerMessageRenderer("quest-changelog", questChangelogRenderer);
|
|
27
|
+
|
|
28
|
+
// Register the top-level /quests command dispatcher.
|
|
29
|
+
pi.registerCommand("quests", {
|
|
30
|
+
description: "Quest commands: /quests [help] to see usage",
|
|
31
|
+
handler: createQuestsHandler(pi, questLog),
|
|
32
|
+
});
|
|
33
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createWriteStream, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const LOG_FILE = "/tmp/logs/pi-quests/debug.log";
|
|
5
|
+
|
|
6
|
+
mkdirSync(dirname(LOG_FILE), { recursive: true });
|
|
7
|
+
|
|
8
|
+
export const logger = {
|
|
9
|
+
debug: (namespace: string, event: string, meta?: Record<string, unknown>) => {
|
|
10
|
+
const timestamp = new Date().toISOString();
|
|
11
|
+
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
12
|
+
const line = `[${timestamp}] [${namespace}] ${event}${metaStr}\n`;
|
|
13
|
+
createWriteStream(LOG_FILE, { flags: "a" }).write(line);
|
|
14
|
+
},
|
|
15
|
+
};
|
package/src/quests.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { AgentToolResult, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
|
|
4
|
+
export interface Quest {
|
|
5
|
+
id: number;
|
|
6
|
+
description: string;
|
|
7
|
+
additionalContext?: string;
|
|
8
|
+
done: boolean;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type HistoryEntry =
|
|
13
|
+
| { type: "add"; id: number }
|
|
14
|
+
| { type: "toggle"; id: number }
|
|
15
|
+
| { type: "update"; id: number; previousDescription: string }
|
|
16
|
+
| { type: "delete"; quest: Quest; index: number }
|
|
17
|
+
| { type: "clear"; quests: Quest[]; nextId: number };
|
|
18
|
+
|
|
19
|
+
export class QuestLog {
|
|
20
|
+
private quests: Quest[] = [];
|
|
21
|
+
private nextId = 1;
|
|
22
|
+
private history: HistoryEntry[] = [];
|
|
23
|
+
|
|
24
|
+
getAll(): Quest[] {
|
|
25
|
+
return [...this.quests];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getNextId(): number {
|
|
29
|
+
return this.nextId;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
add(description: string, additionalContext?: string): Quest {
|
|
33
|
+
const quest: Quest = {
|
|
34
|
+
id: this.nextId++,
|
|
35
|
+
description,
|
|
36
|
+
additionalContext,
|
|
37
|
+
done: false,
|
|
38
|
+
createdAt: Date.now(),
|
|
39
|
+
};
|
|
40
|
+
this.quests.push(quest);
|
|
41
|
+
this.history.push({ type: "add", id: quest.id });
|
|
42
|
+
logger.debug("quests:state", "add", { id: quest.id, description, total: this.quests.length });
|
|
43
|
+
return quest;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
toggle(id: number): Quest | undefined {
|
|
47
|
+
const quest = this.quests.find((q) => q.id === id);
|
|
48
|
+
if (!quest) {
|
|
49
|
+
logger.debug("quests:state", "toggle-not-found", { id });
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
quest.done = !quest.done;
|
|
53
|
+
this.history.push({ type: "toggle", id });
|
|
54
|
+
logger.debug("quests:state", "toggle", { id, done: quest.done, total: this.quests.length });
|
|
55
|
+
return quest;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
update(id: number, description: string): Quest | undefined {
|
|
59
|
+
const quest = this.quests.find((q) => q.id === id);
|
|
60
|
+
if (!quest) {
|
|
61
|
+
logger.debug("quests:state", "update-not-found", { id });
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
this.history.push({ type: "update", id, previousDescription: quest.description });
|
|
65
|
+
quest.description = description;
|
|
66
|
+
logger.debug("quests:state", "update", { id, description, total: this.quests.length });
|
|
67
|
+
return quest;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
delete(id: number): Quest | undefined {
|
|
71
|
+
const index = this.quests.findIndex((q) => q.id === id);
|
|
72
|
+
if (index === -1) {
|
|
73
|
+
logger.debug("quests:state", "delete-not-found", { id });
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const [quest] = this.quests.splice(index, 1);
|
|
77
|
+
this.history.push({ type: "delete", quest, index });
|
|
78
|
+
logger.debug("quests:state", "delete", { id, index, total: this.quests.length });
|
|
79
|
+
return quest;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
clear(): number {
|
|
83
|
+
const count = this.quests.length;
|
|
84
|
+
this.history.push({ type: "clear", quests: [...this.quests], nextId: this.nextId });
|
|
85
|
+
this.quests = [];
|
|
86
|
+
this.nextId = 1;
|
|
87
|
+
logger.debug("quests:state", "clear", { count });
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
revert(): { success: boolean; message: string } {
|
|
92
|
+
const entry = this.history.pop();
|
|
93
|
+
if (!entry) {
|
|
94
|
+
logger.debug("quests:state", "revert-empty");
|
|
95
|
+
return { success: false, message: "Nothing to revert" };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
logger.debug("quests:state", "revert", { type: entry.type });
|
|
99
|
+
|
|
100
|
+
if (entry.type === "add") {
|
|
101
|
+
this.quests = this.quests.filter((q) => q.id !== entry.id);
|
|
102
|
+
const maxId = this.quests.reduce((max, q) => Math.max(max, q.id), 0);
|
|
103
|
+
this.nextId = Math.max(maxId + 1, entry.id);
|
|
104
|
+
logger.debug("quests:state", "revert-add", { id: entry.id, total: this.quests.length });
|
|
105
|
+
return { success: true, message: `Reverted add quest #${entry.id}` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (entry.type === "toggle") {
|
|
109
|
+
const quest = this.quests.find((q) => q.id === entry.id);
|
|
110
|
+
if (quest) {
|
|
111
|
+
quest.done = !quest.done;
|
|
112
|
+
logger.debug("quests:state", "revert-toggle", { id: entry.id, done: quest.done });
|
|
113
|
+
return { success: true, message: `Reverted toggle for quest #${entry.id}` };
|
|
114
|
+
}
|
|
115
|
+
logger.debug("quests:state", "revert-toggle-not-found", { id: entry.id });
|
|
116
|
+
return { success: false, message: `Quest #${entry.id} not found` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (entry.type === "update") {
|
|
120
|
+
const quest = this.quests.find((q) => q.id === entry.id);
|
|
121
|
+
if (quest) {
|
|
122
|
+
quest.description = entry.previousDescription;
|
|
123
|
+
logger.debug("quests:state", "revert-update", { id: entry.id });
|
|
124
|
+
return { success: true, message: `Reverted update for quest #${entry.id}` };
|
|
125
|
+
}
|
|
126
|
+
logger.debug("quests:state", "revert-update-not-found", { id: entry.id });
|
|
127
|
+
return { success: false, message: `Quest #${entry.id} not found` };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (entry.type === "delete") {
|
|
131
|
+
this.quests.splice(entry.index, 0, entry.quest);
|
|
132
|
+
logger.debug("quests:state", "revert-delete", {
|
|
133
|
+
id: entry.quest.id,
|
|
134
|
+
index: entry.index,
|
|
135
|
+
total: this.quests.length,
|
|
136
|
+
});
|
|
137
|
+
return { success: true, message: `Reverted delete for quest #${entry.quest.id}` };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (entry.type === "clear") {
|
|
141
|
+
this.quests = [...entry.quests];
|
|
142
|
+
this.nextId = entry.nextId;
|
|
143
|
+
logger.debug("quests:state", "revert-clear", { count: entry.quests.length });
|
|
144
|
+
return { success: true, message: `Reverted clear (${entry.quests.length} quests restored)` };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
logger.debug("quests:state", "revert-unknown", { type: (entry as { type: string }).type });
|
|
148
|
+
return { success: false, message: "Unknown history entry" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
reconstructFromSession(ctx: ExtensionContext): void {
|
|
152
|
+
const branch = ctx.sessionManager.getBranch();
|
|
153
|
+
let lastState: { quests?: Quest[]; nextId?: number } | undefined;
|
|
154
|
+
let toolResults = 0;
|
|
155
|
+
|
|
156
|
+
for (const entry of branch) {
|
|
157
|
+
if (
|
|
158
|
+
entry.type === "message" &&
|
|
159
|
+
"message" in entry &&
|
|
160
|
+
entry.message.role === "toolResult" &&
|
|
161
|
+
entry.message.toolName === "quest"
|
|
162
|
+
) {
|
|
163
|
+
toolResults++;
|
|
164
|
+
lastState = entry.message.details as { quests?: Quest[]; nextId?: number } | undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (lastState) {
|
|
169
|
+
const questCount = Array.isArray(lastState.quests) ? lastState.quests.length : 0;
|
|
170
|
+
this.quests = Array.isArray(lastState.quests) ? [...lastState.quests] : [];
|
|
171
|
+
this.nextId = typeof lastState.nextId === "number" ? lastState.nextId : 1;
|
|
172
|
+
logger.debug("quests:state", "reconstruct", { toolResults, questCount, nextId: this.nextId });
|
|
173
|
+
} else {
|
|
174
|
+
this.quests = [];
|
|
175
|
+
this.nextId = 1;
|
|
176
|
+
logger.debug("quests:state", "reconstruct-empty", { toolResults });
|
|
177
|
+
}
|
|
178
|
+
this.history = [];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function makeToolResult(text: string, questLog: QuestLog): AgentToolResult<unknown> {
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text }],
|
|
185
|
+
details: { quests: questLog.getAll(), nextId: questLog.getNextId() },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function formatQuestList(
|
|
190
|
+
quests: { id: number; description: string; done: boolean }[],
|
|
191
|
+
): string {
|
|
192
|
+
if (quests.length === 0) return "No quests.";
|
|
193
|
+
return quests.map((q) => `#${q.id} [${q.done ? "x" : " "}] ${q.description}`).join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function formatAddResult(q: { id: number; description: string }): string {
|
|
197
|
+
return `Added quest #${q.id}: ${q.description}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function formatBatchAddResult(added: { id: number; description: string }[]): string {
|
|
201
|
+
return `Added ${added.length} quests:\n${added.map((q) => `#${q.id}: ${q.description}`).join("\n")}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function formatToggleResult(id: number, done: boolean): string {
|
|
205
|
+
return `Quest #${id} ${done ? "done" : "undone"}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function formatUpdateResult(q: { id: number; description: string }): string {
|
|
209
|
+
return `Updated quest #${q.id}: ${q.description}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function formatDeleteResult(q: { id: number; description: string }): string {
|
|
213
|
+
return `Deleted quest #${q.id}: ${q.description}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function formatClearResult(count: number): string {
|
|
217
|
+
return `Cleared ${count} quests`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function formatNotFound(id: number): string {
|
|
221
|
+
return `Quest #${id} not found`;
|
|
222
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
|
|
7
|
+
export const questChangelogRenderer: MessageRenderer<{ content: string }> = (
|
|
8
|
+
message,
|
|
9
|
+
_options: MessageRenderOptions,
|
|
10
|
+
_theme: Theme,
|
|
11
|
+
): Component | undefined => {
|
|
12
|
+
logger.debug("quest-changelog:renderer", "render-called", {
|
|
13
|
+
hasDetails: !!message.details,
|
|
14
|
+
hasContent: !!message.details?.content,
|
|
15
|
+
contentLength: message.details?.content?.length ?? 0,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const data = message.details;
|
|
19
|
+
if (!data?.content) {
|
|
20
|
+
logger.debug("quest-changelog:renderer", "no-content-returning-undefined");
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
logger.debug("quest-changelog:renderer", "creating-markdown");
|
|
25
|
+
return new Markdown(data.content.trim(), 1, 1, getMarkdownTheme());
|
|
26
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Key, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
3
|
+
import { logger } from "../logger.js";
|
|
4
|
+
import type { Quest } from "../quests.js";
|
|
5
|
+
|
|
6
|
+
export class QuestListWidget {
|
|
7
|
+
private cachedWidth?: number;
|
|
8
|
+
private cachedLines?: string[];
|
|
9
|
+
private page = 0;
|
|
10
|
+
private readonly pageSize = 10;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private quests: Quest[],
|
|
14
|
+
private theme: Theme,
|
|
15
|
+
private onClose: () => void,
|
|
16
|
+
) {
|
|
17
|
+
logger.debug("quests:widget", "create", { questCount: quests.length });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
handleInput(data: string): void {
|
|
21
|
+
logger.debug("quests:widget", "handleInput", { data });
|
|
22
|
+
if (matchesKey(data, Key.escape) || data === "q" || data === "Q") {
|
|
23
|
+
logger.debug("quests:widget", "close");
|
|
24
|
+
this.onClose();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (matchesKey(data, Key.tab)) {
|
|
28
|
+
this.nextPage();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (matchesKey(data, Key.shift(Key.tab))) {
|
|
32
|
+
this.prevPage();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private get totalPages(): number {
|
|
38
|
+
return Math.max(1, Math.ceil(this.quests.length / this.pageSize));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private nextPage(): void {
|
|
42
|
+
const next = (this.page + 1) % this.totalPages;
|
|
43
|
+
logger.debug("quests:widget", "nextPage", {
|
|
44
|
+
from: this.page,
|
|
45
|
+
to: next,
|
|
46
|
+
totalPages: this.totalPages,
|
|
47
|
+
});
|
|
48
|
+
this.page = next;
|
|
49
|
+
this.invalidate();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private prevPage(): void {
|
|
53
|
+
const prev = (this.page - 1 + this.totalPages) % this.totalPages;
|
|
54
|
+
logger.debug("quests:widget", "prevPage", {
|
|
55
|
+
from: this.page,
|
|
56
|
+
to: prev,
|
|
57
|
+
totalPages: this.totalPages,
|
|
58
|
+
});
|
|
59
|
+
this.page = prev;
|
|
60
|
+
this.invalidate();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
render(width: number): string[] {
|
|
64
|
+
if (this.cachedWidth === width && this.cachedLines) {
|
|
65
|
+
return this.cachedLines;
|
|
66
|
+
}
|
|
67
|
+
logger.debug("quests:widget", "render", {
|
|
68
|
+
width,
|
|
69
|
+
page: this.page,
|
|
70
|
+
totalPages: this.totalPages,
|
|
71
|
+
totalQuests: this.quests.length,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const th = this.theme;
|
|
75
|
+
const lines: string[] = [];
|
|
76
|
+
const total = this.quests.length;
|
|
77
|
+
const doneCount = this.quests.filter((q) => q.done).length;
|
|
78
|
+
|
|
79
|
+
// Top accent border
|
|
80
|
+
lines.push(th.fg("accent", "─".repeat(width)));
|
|
81
|
+
|
|
82
|
+
// Header with title and completion stats
|
|
83
|
+
const title = th.fg("accent", th.bold(" Quest Log "));
|
|
84
|
+
const stats = total > 0 ? th.fg("muted", `${doneCount}/${total} completed `) : "";
|
|
85
|
+
const headerGap = Math.max(0, width - visibleWidth(title) - visibleWidth(stats));
|
|
86
|
+
lines.push(truncateToWidth(title + " ".repeat(headerGap) + stats, width));
|
|
87
|
+
|
|
88
|
+
lines.push("");
|
|
89
|
+
|
|
90
|
+
if (this.quests.length === 0) {
|
|
91
|
+
lines.push(
|
|
92
|
+
truncateToWidth(
|
|
93
|
+
` ${th.fg("dim", "No active quests. Add one with /quests add <description>")}`,
|
|
94
|
+
width,
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
// Mini progress bar
|
|
99
|
+
const barWidth = Math.min(width - 4, 24);
|
|
100
|
+
const filled = Math.round((doneCount / total) * barWidth);
|
|
101
|
+
const empty = barWidth - filled;
|
|
102
|
+
const bar = th.fg("success", "█".repeat(filled)) + th.fg("dim", "░".repeat(empty));
|
|
103
|
+
lines.push(truncateToWidth(` ${bar} ${th.fg("muted", `${doneCount}/${total}`)}`, width));
|
|
104
|
+
lines.push("");
|
|
105
|
+
|
|
106
|
+
const start = this.page * this.pageSize;
|
|
107
|
+
const pageQuests = this.quests.slice(start, start + this.pageSize);
|
|
108
|
+
for (const q of pageQuests) {
|
|
109
|
+
const marker = q.done ? th.fg("success", " ✓ ") : th.fg("muted", " ○ ");
|
|
110
|
+
const idStr = th.fg(q.done ? "dim" : "accent", `#${q.id}`);
|
|
111
|
+
const desc = q.done
|
|
112
|
+
? th.fg("dim", th.strikethrough(q.description))
|
|
113
|
+
: th.fg("text", q.description);
|
|
114
|
+
const row = `${marker}${idStr} ${desc}`;
|
|
115
|
+
lines.push(truncateToWidth(row, width));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (this.totalPages > 1) {
|
|
119
|
+
lines.push("");
|
|
120
|
+
const pageInfo = ` Page ${this.page + 1}/${this.totalPages} ${th.fg("dim", "· Tab/Shift+Tab to navigate")}`;
|
|
121
|
+
lines.push(truncateToWidth(pageInfo, width));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
lines.push("");
|
|
126
|
+
lines.push(truncateToWidth(` ${th.fg("dim", "Press q or Esc to close")}`, width));
|
|
127
|
+
|
|
128
|
+
// Bottom accent border
|
|
129
|
+
lines.push(th.fg("accent", "─".repeat(width)));
|
|
130
|
+
|
|
131
|
+
this.cachedWidth = width;
|
|
132
|
+
this.cachedLines = lines;
|
|
133
|
+
return lines;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
invalidate(): void {
|
|
137
|
+
logger.debug("quests:widget", "invalidate");
|
|
138
|
+
this.cachedWidth = undefined;
|
|
139
|
+
this.cachedLines = undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { AgentToolResult, Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
3
|
+
import { logger } from "../logger.js";
|
|
4
|
+
|
|
5
|
+
type QuestArgs = {
|
|
6
|
+
action: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
descriptions?: string[];
|
|
9
|
+
id?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function renderQuestCall(args: QuestArgs, theme: Theme, _context: unknown): Text {
|
|
13
|
+
logger.debug("quests:tool", "renderCall", { action: args.action, id: args.id });
|
|
14
|
+
const actionText = theme.fg("toolTitle", theme.bold("quest ")) + theme.fg("accent", args.action);
|
|
15
|
+
|
|
16
|
+
if (args.action === "add" && args.descriptions && args.descriptions.length > 0) {
|
|
17
|
+
const n = args.descriptions.length;
|
|
18
|
+
return new Text(
|
|
19
|
+
`${actionText} ${theme.fg("muted", `[${n} quest${n !== 1 ? "s" : ""}]`)}`,
|
|
20
|
+
0,
|
|
21
|
+
0,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
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
|
+
if (
|
|
32
|
+
(args.action === "toggle" || args.action === "update" || args.action === "delete") &&
|
|
33
|
+
args.id !== undefined
|
|
34
|
+
) {
|
|
35
|
+
return new Text(`${actionText} ${theme.fg("muted", `#${args.id}`)}`, 0, 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (args.action === "revert") {
|
|
39
|
+
return new Text(actionText, 0, 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Text(actionText, 0, 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function renderQuestResult(
|
|
46
|
+
result: AgentToolResult<unknown>,
|
|
47
|
+
options: { expanded: boolean; isPartial: boolean },
|
|
48
|
+
theme: Theme,
|
|
49
|
+
_context: unknown,
|
|
50
|
+
): Text {
|
|
51
|
+
logger.debug("quests:tool", "renderResult", {
|
|
52
|
+
expanded: options.expanded,
|
|
53
|
+
isPartial: options.isPartial,
|
|
54
|
+
});
|
|
55
|
+
const details = result.details as
|
|
56
|
+
| { quests?: Array<{ id: number; description: string; done: boolean }> }
|
|
57
|
+
| undefined;
|
|
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
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const text = result.content.map((c) => (c.type === "text" ? c.text : "[image]")).join("\n");
|
|
68
|
+
return new Text(theme.fg("text", text), 0, 0);
|
|
69
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
2
|
+
import type { AgentToolResult, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
+
import { logger } from "../logger.js";
|
|
5
|
+
import {
|
|
6
|
+
formatBatchAddResult,
|
|
7
|
+
formatClearResult,
|
|
8
|
+
formatDeleteResult,
|
|
9
|
+
formatNotFound,
|
|
10
|
+
formatQuestList,
|
|
11
|
+
formatToggleResult,
|
|
12
|
+
formatUpdateResult,
|
|
13
|
+
makeToolResult,
|
|
14
|
+
type QuestLog,
|
|
15
|
+
} from "../quests.js";
|
|
16
|
+
import { renderQuestCall, renderQuestResult } from "../renderers/tools.js";
|
|
17
|
+
|
|
18
|
+
export const QuestParams = Type.Object({
|
|
19
|
+
action: StringEnum(["add", "list", "toggle", "update", "delete", "clear", "revert"] as const),
|
|
20
|
+
description: Type.Optional(Type.String()),
|
|
21
|
+
descriptions: Type.Optional(Type.Array(Type.String())),
|
|
22
|
+
id: Type.Optional(Type.Number()),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export async function questToolExecute(
|
|
26
|
+
questLog: QuestLog,
|
|
27
|
+
toolCallId: string,
|
|
28
|
+
params: Static<typeof QuestParams>,
|
|
29
|
+
): Promise<AgentToolResult<unknown>> {
|
|
30
|
+
logger.debug("quests:tool", "execute", { toolCallId, action: params.action, id: params.id });
|
|
31
|
+
|
|
32
|
+
switch (params.action) {
|
|
33
|
+
case "add": {
|
|
34
|
+
if (params.descriptions && params.descriptions.length > 0) {
|
|
35
|
+
logger.debug("quests:tool", "add-batch", { count: params.descriptions.length, toolCallId });
|
|
36
|
+
const added: { id: number; description: string }[] = [];
|
|
37
|
+
for (const desc of params.descriptions) {
|
|
38
|
+
const q = questLog.add(desc);
|
|
39
|
+
added.push({ id: q.id, description: q.description });
|
|
40
|
+
}
|
|
41
|
+
logger.debug("quests:tool", "add-batch-complete", { toolCallId, added: added.length });
|
|
42
|
+
return makeToolResult(formatBatchAddResult(added), questLog);
|
|
43
|
+
}
|
|
44
|
+
if (!params.description) {
|
|
45
|
+
logger.debug("quests:tool", "add-missing-description", { toolCallId });
|
|
46
|
+
return makeToolResult("Error: description is required for add action", questLog);
|
|
47
|
+
}
|
|
48
|
+
const q = questLog.add(params.description);
|
|
49
|
+
logger.debug("quests:tool", "add-complete", { toolCallId, id: q.id });
|
|
50
|
+
return makeToolResult(`Added quest #${q.id}: ${q.description}`, questLog);
|
|
51
|
+
}
|
|
52
|
+
case "list": {
|
|
53
|
+
const quests = questLog.getAll();
|
|
54
|
+
logger.debug("quests:tool", "list", { toolCallId, count: quests.length });
|
|
55
|
+
return makeToolResult(formatQuestList(quests), questLog);
|
|
56
|
+
}
|
|
57
|
+
case "toggle": {
|
|
58
|
+
if (params.id === undefined) {
|
|
59
|
+
logger.debug("quests:tool", "toggle-missing-id", { toolCallId });
|
|
60
|
+
return makeToolResult("Error: id is required for toggle action", questLog);
|
|
61
|
+
}
|
|
62
|
+
const q = questLog.toggle(params.id);
|
|
63
|
+
if (!q) {
|
|
64
|
+
logger.debug("quests:tool", "toggle-not-found", { toolCallId, id: params.id });
|
|
65
|
+
return makeToolResult(formatNotFound(params.id), questLog);
|
|
66
|
+
}
|
|
67
|
+
logger.debug("quests:tool", "toggle-complete", {
|
|
68
|
+
toolCallId,
|
|
69
|
+
id: params.id,
|
|
70
|
+
state: q.done,
|
|
71
|
+
});
|
|
72
|
+
return makeToolResult(formatToggleResult(params.id, q.done), questLog);
|
|
73
|
+
}
|
|
74
|
+
case "update": {
|
|
75
|
+
if (params.id === undefined) {
|
|
76
|
+
logger.debug("quests:tool", "update-missing-id", { toolCallId });
|
|
77
|
+
return makeToolResult("Error: id is required for update action", questLog);
|
|
78
|
+
}
|
|
79
|
+
if (!params.description) {
|
|
80
|
+
logger.debug("quests:tool", "update-missing-description", { toolCallId, id: params.id });
|
|
81
|
+
return makeToolResult("Error: description is required for update action", questLog);
|
|
82
|
+
}
|
|
83
|
+
const q = questLog.update(params.id, params.description);
|
|
84
|
+
if (!q) {
|
|
85
|
+
logger.debug("quests:tool", "update-not-found", { toolCallId, id: params.id });
|
|
86
|
+
return makeToolResult(formatNotFound(params.id), questLog);
|
|
87
|
+
}
|
|
88
|
+
logger.debug("quests:tool", "update-complete", { toolCallId, id: params.id });
|
|
89
|
+
return makeToolResult(formatUpdateResult(q), questLog);
|
|
90
|
+
}
|
|
91
|
+
case "delete": {
|
|
92
|
+
if (params.id === undefined) {
|
|
93
|
+
logger.debug("quests:tool", "delete-missing-id", { toolCallId });
|
|
94
|
+
return makeToolResult("Error: id is required for delete action", questLog);
|
|
95
|
+
}
|
|
96
|
+
const q = questLog.delete(params.id);
|
|
97
|
+
if (!q) {
|
|
98
|
+
logger.debug("quests:tool", "delete-not-found", { toolCallId, id: params.id });
|
|
99
|
+
return makeToolResult(formatNotFound(params.id), questLog);
|
|
100
|
+
}
|
|
101
|
+
logger.debug("quests:tool", "delete-complete", { toolCallId, id: params.id });
|
|
102
|
+
return makeToolResult(formatDeleteResult(q), questLog);
|
|
103
|
+
}
|
|
104
|
+
case "clear": {
|
|
105
|
+
const count = questLog.clear();
|
|
106
|
+
logger.debug("quests:tool", "clear-complete", { toolCallId, count });
|
|
107
|
+
return makeToolResult(formatClearResult(count), questLog);
|
|
108
|
+
}
|
|
109
|
+
case "revert": {
|
|
110
|
+
const result = questLog.revert();
|
|
111
|
+
logger.debug("quests:tool", "revert-complete", { toolCallId, success: result.success });
|
|
112
|
+
return makeToolResult(result.message, questLog);
|
|
113
|
+
}
|
|
114
|
+
default: {
|
|
115
|
+
logger.debug("quests:tool", "unknown-action", {
|
|
116
|
+
toolCallId,
|
|
117
|
+
action: (params as { action: string }).action,
|
|
118
|
+
});
|
|
119
|
+
return makeToolResult(`Unknown action: ${(params as { action: string }).action}`, questLog);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function registerQuestTool(pi: ExtensionAPI, questLog: QuestLog): void {
|
|
125
|
+
logger.debug("quests:tool", "register");
|
|
126
|
+
pi.registerTool({
|
|
127
|
+
name: "quest",
|
|
128
|
+
label: "Quest",
|
|
129
|
+
description: "Manage the session quest log",
|
|
130
|
+
parameters: QuestParams,
|
|
131
|
+
execute: (toolCallId, params, _signal, _onUpdate, _ctx) =>
|
|
132
|
+
questToolExecute(questLog, toolCallId, params),
|
|
133
|
+
renderCall: renderQuestCall,
|
|
134
|
+
renderResult: renderQuestResult,
|
|
135
|
+
});
|
|
136
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
const packageJsonPath = resolve(__dirname, "../package.json");
|
|
8
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
9
|
+
|
|
10
|
+
export const VERSION: string = packageJson.version;
|
|
11
|
+
export const CHANGELOG_PATH: string = resolve(__dirname, "../CHANGELOG.md");
|