pi-quests 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +29 -12
- package/package.json +1 -1
- package/src/commands/changelog.ts +9 -0
- package/src/commands/handler.ts +134 -0
- package/src/commands/parse-args.ts +78 -0
- package/src/index.ts +48 -3
- package/src/logger.ts +11 -2
- package/src/quest/dataplane.ts +337 -0
- package/src/quest/formatters.ts +34 -0
- package/src/quest/tracker.ts +87 -0
- package/src/quest/types.ts +33 -0
- package/src/renderers/commands.ts +9 -2
- package/src/renderers/tools.ts +17 -22
- package/src/tools/handler.ts +100 -0
- package/src/tools/params.ts +24 -0
- package/src/version.ts +9 -2
- package/src/commands/quests.ts +0 -220
- package/src/quests.ts +0 -222
- package/src/tools/quest.ts +0 -136
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { AgentToolResult, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { Static } from "@sinclair/typebox";
|
|
3
|
+
import { logger } from "../logger.js";
|
|
4
|
+
import { makeToolResult, type QuestAction, type QuestLog } from "../quest/dataplane.js";
|
|
5
|
+
import { QUEST_ACTIONS } from "../quest/types.js";
|
|
6
|
+
import { renderQuestCall, renderQuestResult } from "../renderers/tools.js";
|
|
7
|
+
import { QuestParams } from "./params.js";
|
|
8
|
+
|
|
9
|
+
type QuestToolParams = Static<typeof QuestParams>;
|
|
10
|
+
|
|
11
|
+
const toolHandlers: {
|
|
12
|
+
[K in QuestToolParams["action"]]: (
|
|
13
|
+
questLog: QuestLog,
|
|
14
|
+
params: QuestToolParams,
|
|
15
|
+
toolCallId: string,
|
|
16
|
+
) => AgentToolResult<unknown>;
|
|
17
|
+
} = {
|
|
18
|
+
[QUEST_ACTIONS.add](questLog, params, toolCallId) {
|
|
19
|
+
return runTool(questLog, toolCallId, {
|
|
20
|
+
type: QUEST_ACTIONS.add,
|
|
21
|
+
descriptions: params.descriptions,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
[QUEST_ACTIONS.list](questLog, _params, toolCallId) {
|
|
25
|
+
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.list });
|
|
26
|
+
},
|
|
27
|
+
[QUEST_ACTIONS.toggle](questLog, params, toolCallId) {
|
|
28
|
+
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.toggle, id: params.id });
|
|
29
|
+
},
|
|
30
|
+
[QUEST_ACTIONS.update](questLog, params, toolCallId) {
|
|
31
|
+
return runTool(questLog, toolCallId, {
|
|
32
|
+
type: QUEST_ACTIONS.update,
|
|
33
|
+
id: params.id,
|
|
34
|
+
description: params.description,
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
[QUEST_ACTIONS.delete](questLog, params, toolCallId) {
|
|
38
|
+
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.delete, id: params.id });
|
|
39
|
+
},
|
|
40
|
+
[QUEST_ACTIONS.clear](questLog, _params, toolCallId) {
|
|
41
|
+
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.clear });
|
|
42
|
+
},
|
|
43
|
+
[QUEST_ACTIONS.revert](questLog, _params, toolCallId) {
|
|
44
|
+
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.revert });
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function runTool(
|
|
49
|
+
questLog: QuestLog,
|
|
50
|
+
toolCallId: string,
|
|
51
|
+
action: QuestAction,
|
|
52
|
+
): AgentToolResult<unknown> {
|
|
53
|
+
const result = questLog.execute(action);
|
|
54
|
+
logger.debug("quests:tool", "execute-complete", { toolCallId, success: result.success });
|
|
55
|
+
|
|
56
|
+
const displayQuests =
|
|
57
|
+
action.type === QUEST_ACTIONS.add ||
|
|
58
|
+
action.type === QUEST_ACTIONS.list ||
|
|
59
|
+
action.type === QUEST_ACTIONS.revert
|
|
60
|
+
? questLog.getAll()
|
|
61
|
+
: result.quest
|
|
62
|
+
? [result.quest]
|
|
63
|
+
: undefined;
|
|
64
|
+
|
|
65
|
+
return makeToolResult(result.message, questLog, displayQuests);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function questToolExecute(
|
|
69
|
+
questLog: QuestLog,
|
|
70
|
+
toolCallId: string,
|
|
71
|
+
params: QuestToolParams,
|
|
72
|
+
): Promise<AgentToolResult<unknown>> {
|
|
73
|
+
logger.debug("quests:tool", "execute", { toolCallId, action: params.action, id: params.id });
|
|
74
|
+
const handler = toolHandlers[params.action];
|
|
75
|
+
return handler(questLog, params, toolCallId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function registerQuestTool(pi: ExtensionAPI, questLog: QuestLog): void {
|
|
79
|
+
logger.debug("quests:tool", "register");
|
|
80
|
+
pi.registerTool({
|
|
81
|
+
name: "quest",
|
|
82
|
+
label: "Quest",
|
|
83
|
+
description:
|
|
84
|
+
"Manage the session quest log. Use this VERY frequently to track tasks, plans, and progress throughout the conversation.",
|
|
85
|
+
promptSnippet: "Add, list, toggle, update, delete, clear, or revert quest items",
|
|
86
|
+
promptGuidelines: [
|
|
87
|
+
"Before reading files, running commands, or making edits, ensure the current work is tracked as specific, actionable quests.",
|
|
88
|
+
"Do not create a single vague quest for broad requests. Break them into concrete, independent steps.",
|
|
89
|
+
"When the user gives a plan or a list of tasks, add them as quests immediately.",
|
|
90
|
+
"It is critical that you toggle quests to done as soon as you complete them. Do NOT batch completions.",
|
|
91
|
+
"Before delegating to a minion, add a quest for the delegated task.",
|
|
92
|
+
"If you are unsure what to do next, use the list action to check active quests.",
|
|
93
|
+
],
|
|
94
|
+
parameters: QuestParams,
|
|
95
|
+
execute: (toolCallId, params, _signal, _onUpdate, _ctx) =>
|
|
96
|
+
questToolExecute(questLog, toolCallId, params),
|
|
97
|
+
renderCall: renderQuestCall,
|
|
98
|
+
renderResult: renderQuestResult,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { QUEST_ACTION_VALUES } from "../quest/types.js";
|
|
4
|
+
|
|
5
|
+
export const QuestParams = Type.Object({
|
|
6
|
+
action: StringEnum(QUEST_ACTION_VALUES, {
|
|
7
|
+
description: "The quest action to perform",
|
|
8
|
+
}),
|
|
9
|
+
descriptions: Type.Optional(
|
|
10
|
+
Type.Array(Type.String(), {
|
|
11
|
+
description: "Array of quest descriptions (required for add action)",
|
|
12
|
+
}),
|
|
13
|
+
),
|
|
14
|
+
description: Type.Optional(
|
|
15
|
+
Type.String({
|
|
16
|
+
description: "New description (required for update action)",
|
|
17
|
+
}),
|
|
18
|
+
),
|
|
19
|
+
id: Type.Optional(
|
|
20
|
+
Type.Number({
|
|
21
|
+
description: "Quest ID (required for toggle, update, delete actions)",
|
|
22
|
+
}),
|
|
23
|
+
),
|
|
24
|
+
});
|
package/src/version.ts
CHANGED
|
@@ -5,7 +5,14 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
|
|
7
7
|
const packageJsonPath = resolve(__dirname, "../package.json");
|
|
8
|
-
|
|
8
|
+
let cachedVersion: string | undefined;
|
|
9
|
+
|
|
10
|
+
export function getVersion(): string {
|
|
11
|
+
if (cachedVersion === undefined) {
|
|
12
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
13
|
+
cachedVersion = packageJson.version as string;
|
|
14
|
+
}
|
|
15
|
+
return cachedVersion;
|
|
16
|
+
}
|
|
9
17
|
|
|
10
|
-
export const VERSION: string = packageJson.version;
|
|
11
18
|
export const CHANGELOG_PATH: string = resolve(__dirname, "../CHANGELOG.md");
|
package/src/commands/quests.ts
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
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/quests.ts
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
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
|
-
}
|