pi-quests 0.6.2 → 0.7.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 +6 -0
- package/README.md +1 -1
- package/package.json +15 -10
- package/src/commands/handler.ts +12 -8
- package/src/commands/parse-args.ts +26 -14
- package/src/prompts/skill.md +4 -2
- package/src/quest/dataplane.ts +441 -114
- package/src/quest/formatters.ts +16 -2
- package/src/quest/types.ts +4 -2
- package/src/renderers/tools.ts +29 -8
- package/src/tools/handler.ts +75 -65
- package/src/tools/params.ts +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.7.0] - 2026-04-26
|
|
6
|
+
|
|
7
|
+
- feat: add batch toggle and batch delete support for quests (#2) — @pascal-de-ladurantaye
|
|
8
|
+
- feat: add redo action and rename revert to undo
|
|
9
|
+
- chore: migrate from taskfile to npm scripts and update dependencies
|
|
10
|
+
|
|
5
11
|
## [0.6.2] - 2026-04-17
|
|
6
12
|
|
|
7
13
|
- fix: system prompt was too weak
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-quests
|
|
2
2
|
|
|
3
|
-
[](CHANGELOG.md)
|
|
4
4
|
[](LICENSE.md)
|
|
5
5
|
[](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent)
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-quests",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "A quest-log for your pi. Keep your agent on track, one quest at a time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -35,22 +35,27 @@
|
|
|
35
35
|
"@mariozechner/pi-ai": "*",
|
|
36
36
|
"@mariozechner/pi-coding-agent": "*",
|
|
37
37
|
"@mariozechner/pi-tui": "*",
|
|
38
|
-
"
|
|
38
|
+
"typebox": "*"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@biomejs/biome": "^2.4.
|
|
42
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
43
|
-
"@mariozechner/pi-tui": "^0.
|
|
44
|
-
"
|
|
45
|
-
"
|
|
41
|
+
"@biomejs/biome": "^2.4.13",
|
|
42
|
+
"@mariozechner/pi-coding-agent": "^0.70.2",
|
|
43
|
+
"@mariozechner/pi-tui": "^0.70.2",
|
|
44
|
+
"typebox": "^1.1.33",
|
|
45
|
+
"typescript": "^6.0.3",
|
|
46
|
+
"vitest": "^4.1.5"
|
|
46
47
|
},
|
|
47
48
|
"scripts": {
|
|
48
49
|
"test": "vitest run",
|
|
49
50
|
"test:watch": "vitest",
|
|
50
51
|
"typecheck": "tsc --noEmit",
|
|
51
|
-
"style": "biome check ./src ./test",
|
|
52
|
-
"style:fix": "biome check --write --unsafe --no-errors-on-unmatched ./src ./test",
|
|
52
|
+
"style": "biome check --error-on-warnings ./src ./test",
|
|
53
|
+
"style:fix": "biome check --write --unsafe --no-errors-on-unmatched --error-on-warnings ./src ./test",
|
|
53
54
|
"format": "biome format --write --no-errors-on-unmatched ./src ./test",
|
|
54
|
-
"prepublishOnly": "npx tsc --noEmit && npx vitest run"
|
|
55
|
+
"prepublishOnly": "npx tsc --noEmit && npx vitest run",
|
|
56
|
+
"prepare": "bash scripts/setup.sh",
|
|
57
|
+
"cleanup": "bash scripts/cleanup.sh",
|
|
58
|
+
"dev": "bash scripts/dev.sh",
|
|
59
|
+
"logs": "bash scripts/logs.sh"
|
|
55
60
|
}
|
|
56
61
|
}
|
package/src/commands/handler.ts
CHANGED
|
@@ -25,7 +25,8 @@ type MutatingCommand = Extract<
|
|
|
25
25
|
| typeof QUEST_ACTIONS.clear
|
|
26
26
|
| typeof QUEST_ACTIONS.reorder
|
|
27
27
|
| typeof QUEST_ACTIONS.reparent
|
|
28
|
-
| typeof QUEST_ACTIONS.
|
|
28
|
+
| typeof QUEST_ACTIONS.undo
|
|
29
|
+
| typeof QUEST_ACTIONS.redo;
|
|
29
30
|
}
|
|
30
31
|
>["action"];
|
|
31
32
|
|
|
@@ -41,13 +42,13 @@ const commandActionBuilders: {
|
|
|
41
42
|
id: p.id,
|
|
42
43
|
descriptions: p.descriptions,
|
|
43
44
|
}),
|
|
44
|
-
[QUEST_ACTIONS.toggle]: (p) => ({ type: QUEST_ACTIONS.toggle,
|
|
45
|
+
[QUEST_ACTIONS.toggle]: (p) => ({ type: QUEST_ACTIONS.toggle, ids: p.ids }),
|
|
45
46
|
[QUEST_ACTIONS.update]: (p) => ({
|
|
46
47
|
type: QUEST_ACTIONS.update,
|
|
47
48
|
id: p.id,
|
|
48
49
|
description: p.description,
|
|
49
50
|
}),
|
|
50
|
-
[QUEST_ACTIONS.delete]: (p) => ({ type: QUEST_ACTIONS.delete,
|
|
51
|
+
[QUEST_ACTIONS.delete]: (p) => ({ type: QUEST_ACTIONS.delete, ids: p.ids }),
|
|
51
52
|
[QUEST_ACTIONS.clear]: (p) => ({ type: QUEST_ACTIONS.clear, all: p.all }),
|
|
52
53
|
[QUEST_ACTIONS.reorder]: (p) => ({
|
|
53
54
|
type: QUEST_ACTIONS.reorder,
|
|
@@ -59,7 +60,8 @@ const commandActionBuilders: {
|
|
|
59
60
|
id: p.id,
|
|
60
61
|
parentId: p.parentId,
|
|
61
62
|
}),
|
|
62
|
-
[QUEST_ACTIONS.
|
|
63
|
+
[QUEST_ACTIONS.undo]: () => ({ type: QUEST_ACTIONS.undo }),
|
|
64
|
+
[QUEST_ACTIONS.redo]: () => ({ type: QUEST_ACTIONS.redo }),
|
|
63
65
|
};
|
|
64
66
|
|
|
65
67
|
export function openQuestList(
|
|
@@ -143,7 +145,8 @@ export function createQuestsHandler(
|
|
|
143
145
|
case QUEST_ACTIONS.clear:
|
|
144
146
|
case QUEST_ACTIONS.reorder:
|
|
145
147
|
case QUEST_ACTIONS.reparent:
|
|
146
|
-
case QUEST_ACTIONS.
|
|
148
|
+
case QUEST_ACTIONS.undo:
|
|
149
|
+
case QUEST_ACTIONS.redo: {
|
|
147
150
|
const builder = commandActionBuilders[parsed.action];
|
|
148
151
|
const action = builder(parsed as never);
|
|
149
152
|
const result = questLog.execute(action);
|
|
@@ -161,12 +164,13 @@ export function createQuestsHandler(
|
|
|
161
164
|
lines.push(" add <description> - Add a new top-level quest");
|
|
162
165
|
lines.push(" add-step <id> <description> - Split a quest into a step");
|
|
163
166
|
lines.push(" list - List all quests");
|
|
164
|
-
lines.push(" toggle <id>
|
|
165
|
-
lines.push(" delete <id>
|
|
167
|
+
lines.push(" toggle <id> [moreIds...] - Toggle one or more quests");
|
|
168
|
+
lines.push(" delete <id> [moreIds...] - Delete one or more quests");
|
|
166
169
|
lines.push(" update <id> <desc> - Update a quest description");
|
|
167
170
|
lines.push(" reparent <id> [parentId] - Reparent a quest/step (omit parentId to promote)");
|
|
168
171
|
lines.push(" reorder <id> <targetId> - Reorder a quest before the target quest");
|
|
169
|
-
lines.push("
|
|
172
|
+
lines.push(" undo - Undo the last quest change");
|
|
173
|
+
lines.push(" redo - Redo the last undone action");
|
|
170
174
|
lines.push(" clear [all] - Clear completed quests, or optionally all quests");
|
|
171
175
|
lines.push(" version - Show version");
|
|
172
176
|
lines.push(" changelog - Show changelog");
|
|
@@ -5,12 +5,13 @@ export type ParsedArgs =
|
|
|
5
5
|
| { action: typeof QUEST_ACTIONS.add; descriptions: string[] }
|
|
6
6
|
| { action: typeof QUEST_ACTIONS.split; id: string; descriptions: string[] }
|
|
7
7
|
| { action: typeof QUEST_ACTIONS.list }
|
|
8
|
-
| { action: typeof QUEST_ACTIONS.toggle;
|
|
8
|
+
| { action: typeof QUEST_ACTIONS.toggle; ids: string[] }
|
|
9
9
|
| { action: typeof QUEST_ACTIONS.update; id: string; description: string }
|
|
10
|
-
| { action: typeof QUEST_ACTIONS.delete;
|
|
10
|
+
| { action: typeof QUEST_ACTIONS.delete; ids: string[] }
|
|
11
11
|
| { action: typeof QUEST_ACTIONS.clear; all?: boolean }
|
|
12
12
|
| { action: typeof QUEST_ACTIONS.reorder; id: string; targetId: string }
|
|
13
|
-
| { action: typeof QUEST_ACTIONS.
|
|
13
|
+
| { action: typeof QUEST_ACTIONS.undo }
|
|
14
|
+
| { action: typeof QUEST_ACTIONS.redo }
|
|
14
15
|
| { action: typeof QUEST_ACTIONS.reparent; id: string; parentId?: string }
|
|
15
16
|
| { action: "help" }
|
|
16
17
|
| { action: "version" }
|
|
@@ -40,13 +41,14 @@ export function parseQuestArgs(args: string, idLength = 2): ParsedArgs {
|
|
|
40
41
|
// Quest actions without arguments
|
|
41
42
|
if (command === QUEST_ACTIONS.list) return { action: QUEST_ACTIONS.list };
|
|
42
43
|
if (command === QUEST_ACTIONS.clear) return parseClearArgs(rest);
|
|
43
|
-
if (command === QUEST_ACTIONS.
|
|
44
|
+
if (command === QUEST_ACTIONS.undo) return { action: QUEST_ACTIONS.undo };
|
|
45
|
+
if (command === QUEST_ACTIONS.redo) return { action: QUEST_ACTIONS.redo };
|
|
44
46
|
|
|
45
47
|
// Quest actions with arguments
|
|
46
48
|
if (command === QUEST_ACTIONS.add) return parseAddArgs(rest);
|
|
47
49
|
if (command === "add-step") return parseAddStepArgs(rest, idLength);
|
|
48
|
-
if (command === QUEST_ACTIONS.toggle) return
|
|
49
|
-
if (command === QUEST_ACTIONS.delete) return
|
|
50
|
+
if (command === QUEST_ACTIONS.toggle) return parseToggleArgs(rest, idLength);
|
|
51
|
+
if (command === QUEST_ACTIONS.delete) return parseDeleteArgs(rest, idLength);
|
|
50
52
|
if (command === QUEST_ACTIONS.update) return parseUpdateArgs(rest, idLength);
|
|
51
53
|
if (command === QUEST_ACTIONS.reorder) return parseReorderArgs(rest, idLength);
|
|
52
54
|
if (command === QUEST_ACTIONS.reparent) return parseReparentArgs(rest, idLength);
|
|
@@ -70,16 +72,26 @@ function parseAddStepArgs(tokens: string[], idLength: number): ParsedArgs {
|
|
|
70
72
|
return { action: QUEST_ACTIONS.split, id, descriptions: [description] };
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
function
|
|
74
|
-
action: typeof QUEST_ACTIONS.toggle | typeof QUEST_ACTIONS.delete,
|
|
75
|
-
tokens: string[],
|
|
76
|
-
idLength: number,
|
|
77
|
-
): ParsedArgs {
|
|
78
|
-
const id = tokens[0] ? tokens[0].toLowerCase() : "";
|
|
75
|
+
function parseToggleArgs(tokens: string[], idLength: number): ParsedArgs {
|
|
79
76
|
const pattern = new RegExp(`^[0-9a-f]{${idLength}}$`);
|
|
80
|
-
|
|
77
|
+
const ids = tokens.map((token) => token.toLowerCase());
|
|
78
|
+
|
|
79
|
+
if (ids.length === 0 || ids.some((id) => !pattern.test(id))) {
|
|
80
|
+
return { error: "Usage: /quests toggle <id> [moreIds...]" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { action: QUEST_ACTIONS.toggle, ids };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseDeleteArgs(tokens: string[], idLength: number): ParsedArgs {
|
|
87
|
+
const pattern = new RegExp(`^[0-9a-f]{${idLength}}$`);
|
|
88
|
+
const ids = tokens.map((token) => token.toLowerCase());
|
|
89
|
+
|
|
90
|
+
if (ids.length === 0 || ids.some((id) => !pattern.test(id))) {
|
|
91
|
+
return { error: "Usage: /quests delete <id> [moreIds...]" };
|
|
92
|
+
}
|
|
81
93
|
|
|
82
|
-
return { action,
|
|
94
|
+
return { action: QUEST_ACTIONS.delete, ids };
|
|
83
95
|
}
|
|
84
96
|
|
|
85
97
|
function parseUpdateArgs(tokens: string[], idLength: number): ParsedArgs {
|
package/src/prompts/skill.md
CHANGED
|
@@ -20,7 +20,8 @@ This skill provides guidelines and best practices for using the quest management
|
|
|
20
20
|
| `clear` | — | Remove completed quests. `all: true` removes everything. |
|
|
21
21
|
| `reorder` | `id`, `targetId` | Move a top-level quest before `targetId`. Steps cannot be reordered. |
|
|
22
22
|
| `reparent` | `id`, `parentId?` | Demote to step under `parentId`, or promote to top-level if `parentId` omitted. |
|
|
23
|
-
| `
|
|
23
|
+
| `undo` | — | Undo the most recent mutating action. One level only. |
|
|
24
|
+
| `redo` | — | Redo the most recently undone action. |
|
|
24
25
|
| `rules` / `skill` | — | Return this document. |
|
|
25
26
|
|
|
26
27
|
## Workflows
|
|
@@ -51,7 +52,8 @@ This skill provides guidelines and best practices for using the quest management
|
|
|
51
52
|
- NEVER try to nest steps. Only top-level quests can be parents.
|
|
52
53
|
- NEVER mark a parent as done if they have incomplete steps.
|
|
53
54
|
- NEVER delete a quest with incomplete steps.
|
|
54
|
-
- Use
|
|
55
|
+
- Use undo if you make a mistake to undo an action.
|
|
56
|
+
- Use redo to replay an action you just undid. Redo is cleared when a new mutation happens.
|
|
55
57
|
- ALWAYS use the hex ID, never the positional number when referring to quests in actions.
|
|
56
58
|
- ALWAYS Use `toggle` for done states. NEVER use `update` to append "DONE" or any completion marker to a description.
|
|
57
59
|
- If a parent toggle is blocked, check that all its steps are done first.
|
package/src/quest/dataplane.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { getQuestSkillDocument } from "../prompts.js";
|
|
|
5
5
|
import {
|
|
6
6
|
formatAddResult,
|
|
7
7
|
formatBatchAddResult,
|
|
8
|
+
formatBatchDeleteResult,
|
|
9
|
+
formatBatchToggleResult,
|
|
8
10
|
formatBlockedBySteps,
|
|
9
11
|
formatDeleteResult,
|
|
10
12
|
formatDescriptionRequiredError,
|
|
@@ -12,7 +14,8 @@ import {
|
|
|
12
14
|
formatIdRequiredError,
|
|
13
15
|
formatMissingDescriptionsError,
|
|
14
16
|
formatNotFound,
|
|
15
|
-
|
|
17
|
+
formatNothingToRedoError,
|
|
18
|
+
formatNothingToUndoError,
|
|
16
19
|
formatParentDoneError,
|
|
17
20
|
formatParentNotFoundError,
|
|
18
21
|
formatQuestList,
|
|
@@ -32,17 +35,17 @@ import {
|
|
|
32
35
|
} from "./formatters.js";
|
|
33
36
|
import { QUEST_ACTIONS, type Quest, type Step } from "./types.js";
|
|
34
37
|
|
|
38
|
+
type DeletedQuestSnapshot = {
|
|
39
|
+
deletedIds: string[];
|
|
40
|
+
previousQuests: Quest[];
|
|
41
|
+
previousSteps: Step[];
|
|
42
|
+
};
|
|
43
|
+
|
|
35
44
|
export type HistoryEntry =
|
|
36
45
|
| { type: typeof QUEST_ACTIONS.add; id: string; parentId?: string }
|
|
37
|
-
| { type: typeof QUEST_ACTIONS.toggle;
|
|
46
|
+
| { type: typeof QUEST_ACTIONS.toggle; ids: string[] }
|
|
38
47
|
| { type: typeof QUEST_ACTIONS.update; id: string; previousDescription: string }
|
|
39
|
-
| {
|
|
40
|
-
type: typeof QUEST_ACTIONS.delete;
|
|
41
|
-
quest: Quest | Step;
|
|
42
|
-
index: number;
|
|
43
|
-
isStep?: boolean;
|
|
44
|
-
cascadeDeletedSteps?: Step[];
|
|
45
|
-
}
|
|
48
|
+
| ({ type: typeof QUEST_ACTIONS.delete; ids: string[] } & DeletedQuestSnapshot)
|
|
46
49
|
| {
|
|
47
50
|
type: typeof QUEST_ACTIONS.clear;
|
|
48
51
|
previousQuests: Quest[];
|
|
@@ -69,17 +72,40 @@ export type QuestAction =
|
|
|
69
72
|
| { type: typeof QUEST_ACTIONS.split; id?: string; descriptions?: string[] }
|
|
70
73
|
| { type: typeof QUEST_ACTIONS.add_step; id?: string; descriptions?: string[] }
|
|
71
74
|
| { type: typeof QUEST_ACTIONS.list }
|
|
72
|
-
| { type: typeof QUEST_ACTIONS.toggle; id?: string }
|
|
75
|
+
| { type: typeof QUEST_ACTIONS.toggle; id?: string; ids?: string[] }
|
|
73
76
|
| { type: typeof QUEST_ACTIONS.update; id?: string; description?: string }
|
|
74
|
-
| { type: typeof QUEST_ACTIONS.delete; id?: string }
|
|
77
|
+
| { type: typeof QUEST_ACTIONS.delete; id?: string; ids?: string[] }
|
|
75
78
|
| { type: typeof QUEST_ACTIONS.clear; all?: boolean }
|
|
76
79
|
| { type: typeof QUEST_ACTIONS.reorder; id?: string; targetId?: string }
|
|
77
|
-
| { type: typeof QUEST_ACTIONS.
|
|
80
|
+
| { type: typeof QUEST_ACTIONS.undo }
|
|
81
|
+
| { type: typeof QUEST_ACTIONS.redo }
|
|
78
82
|
| { type: typeof QUEST_ACTIONS.reparent; id?: string; parentId?: string }
|
|
79
83
|
| { type: typeof QUEST_ACTIONS.rules }
|
|
80
84
|
| { type: typeof QUEST_ACTIONS.skill };
|
|
81
85
|
|
|
82
|
-
export type
|
|
86
|
+
export type RedoEntry =
|
|
87
|
+
| { type: typeof QUEST_ACTIONS.add; quest: Quest | Step }
|
|
88
|
+
| { type: typeof QUEST_ACTIONS.toggle; ids: string[] }
|
|
89
|
+
| { type: typeof QUEST_ACTIONS.update; id: string; description: string }
|
|
90
|
+
| { type: typeof QUEST_ACTIONS.delete; ids: string[] }
|
|
91
|
+
| { type: typeof QUEST_ACTIONS.clear; all: boolean }
|
|
92
|
+
| { type: typeof QUEST_ACTIONS.reorder; id: string; targetId: string }
|
|
93
|
+
| { type: typeof QUEST_ACTIONS.reparent; id: string; parentId?: string };
|
|
94
|
+
|
|
95
|
+
export type QuestOperationResult = {
|
|
96
|
+
success: boolean;
|
|
97
|
+
message: string;
|
|
98
|
+
quest?: Quest;
|
|
99
|
+
quests?: Quest[];
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
type ToggleBatchResult =
|
|
103
|
+
| { quests: (Quest | Step)[]; ids: string[] }
|
|
104
|
+
| { error: "notFound" | "blocked"; id: string };
|
|
105
|
+
|
|
106
|
+
type DeleteBatchResult =
|
|
107
|
+
| { quests: (Quest | Step)[]; ids: string[] }
|
|
108
|
+
| { error: "notFound" | "blocked"; id: string };
|
|
83
109
|
|
|
84
110
|
/**
|
|
85
111
|
* In-memory quest data plane.
|
|
@@ -92,6 +118,8 @@ export class QuestLog {
|
|
|
92
118
|
private steps: Step[] = [];
|
|
93
119
|
private usedIds: Set<string> = new Set();
|
|
94
120
|
private history: HistoryEntry[] = [];
|
|
121
|
+
private redoStack: RedoEntry[] = [];
|
|
122
|
+
private inRedo = false;
|
|
95
123
|
|
|
96
124
|
private readonly ID_LENGTH: number;
|
|
97
125
|
private readonly MAX_IDS: number;
|
|
@@ -136,16 +164,30 @@ export class QuestLog {
|
|
|
136
164
|
return { success: true, message: `Reverted add quest [${entry.id}]` };
|
|
137
165
|
},
|
|
138
166
|
[QUEST_ACTIONS.toggle]: (entry) => {
|
|
139
|
-
const
|
|
167
|
+
const quests = entry.ids.map((id) => this.findById(id));
|
|
168
|
+
const missingId = entry.ids.find((_, index) => !quests[index]);
|
|
140
169
|
|
|
141
|
-
if (
|
|
170
|
+
if (missingId) {
|
|
171
|
+
logger.debug("quests:state", "revert-toggle-not-found", { ids: entry.ids, missingId });
|
|
172
|
+
return { success: false, message: formatNotFound(missingId) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const resolvedQuests = quests.filter((quest): quest is Quest | Step => quest !== undefined);
|
|
176
|
+
for (const quest of resolvedQuests) {
|
|
142
177
|
quest.done = !quest.done;
|
|
143
|
-
logger.debug("quests:state", "revert-toggle", { id: entry.id, done: quest.done });
|
|
144
|
-
return { success: true, message: `Reverted toggle for quest [${entry.id}]` };
|
|
145
178
|
}
|
|
146
179
|
|
|
147
|
-
logger.debug("quests:state", "revert-toggle
|
|
148
|
-
|
|
180
|
+
logger.debug("quests:state", "revert-toggle", {
|
|
181
|
+
ids: entry.ids,
|
|
182
|
+
done: resolvedQuests.map((quest) => quest.done),
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
success: true,
|
|
186
|
+
message:
|
|
187
|
+
entry.ids.length === 1
|
|
188
|
+
? `Reverted toggle for quest [${entry.ids[0]}]`
|
|
189
|
+
: `Reverted toggle for ${entry.ids.length} quests`,
|
|
190
|
+
};
|
|
149
191
|
},
|
|
150
192
|
[QUEST_ACTIONS.update]: (entry) => {
|
|
151
193
|
const quest = this.findById(entry.id);
|
|
@@ -159,25 +201,21 @@ export class QuestLog {
|
|
|
159
201
|
return { success: false, message: formatNotFound(entry.id) };
|
|
160
202
|
},
|
|
161
203
|
[QUEST_ACTIONS.delete]: (entry) => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
this.quests.splice(entry.index, 0, entry.quest);
|
|
166
|
-
if (entry.cascadeDeletedSteps) {
|
|
167
|
-
for (const sub of entry.cascadeDeletedSteps) {
|
|
168
|
-
this.steps.push(sub);
|
|
169
|
-
this.usedIds.add(sub.id);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
this.usedIds.add(entry.quest.id);
|
|
204
|
+
this.quests = [...entry.previousQuests];
|
|
205
|
+
this.steps = [...entry.previousSteps];
|
|
206
|
+
this.usedIds = new Set([...this.quests, ...this.steps].map((quest) => quest.id));
|
|
174
207
|
|
|
175
208
|
logger.debug("quests:state", "revert-delete", {
|
|
176
|
-
|
|
177
|
-
index: entry.index,
|
|
209
|
+
ids: entry.deletedIds,
|
|
178
210
|
total: this.quests.length + this.steps.length,
|
|
179
211
|
});
|
|
180
|
-
return {
|
|
212
|
+
return {
|
|
213
|
+
success: true,
|
|
214
|
+
message:
|
|
215
|
+
entry.ids.length === 1
|
|
216
|
+
? `Reverted delete for quest [${entry.ids[0]}]`
|
|
217
|
+
: `Reverted delete for ${entry.ids.length} tasks`,
|
|
218
|
+
};
|
|
181
219
|
},
|
|
182
220
|
[QUEST_ACTIONS.clear]: (entry) => {
|
|
183
221
|
if ("previousQuests" in entry) {
|
|
@@ -230,6 +268,114 @@ export class QuestLog {
|
|
|
230
268
|
},
|
|
231
269
|
};
|
|
232
270
|
|
|
271
|
+
private redoHandlers: {
|
|
272
|
+
[K in RedoEntry["type"]]: (entry: Extract<RedoEntry, { type: K }>) => {
|
|
273
|
+
success: boolean;
|
|
274
|
+
message: string;
|
|
275
|
+
};
|
|
276
|
+
} = {
|
|
277
|
+
[QUEST_ACTIONS.add]: (entry) => {
|
|
278
|
+
if ("parentId" in entry.quest && entry.quest.parentId) {
|
|
279
|
+
this.steps.push(entry.quest as Step);
|
|
280
|
+
this.history.push({
|
|
281
|
+
type: QUEST_ACTIONS.add,
|
|
282
|
+
id: entry.quest.id,
|
|
283
|
+
parentId: entry.quest.parentId,
|
|
284
|
+
});
|
|
285
|
+
} else {
|
|
286
|
+
this.quests.push(entry.quest);
|
|
287
|
+
this.history.push({ type: QUEST_ACTIONS.add, id: entry.quest.id });
|
|
288
|
+
}
|
|
289
|
+
this.usedIds.add(entry.quest.id);
|
|
290
|
+
return { success: true, message: `Redone add quest [${entry.quest.id}]` };
|
|
291
|
+
},
|
|
292
|
+
[QUEST_ACTIONS.toggle]: (entry) => {
|
|
293
|
+
const result = this.toggleMany(entry.ids);
|
|
294
|
+
if ("error" in result) {
|
|
295
|
+
return {
|
|
296
|
+
success: false,
|
|
297
|
+
message:
|
|
298
|
+
result.error === "blocked"
|
|
299
|
+
? formatBlockedBySteps(result.id)
|
|
300
|
+
: formatNotFound(result.id),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
success: true,
|
|
306
|
+
message:
|
|
307
|
+
entry.ids.length === 1
|
|
308
|
+
? `Redone toggle for quest [${entry.ids[0]}]`
|
|
309
|
+
: `Redone toggle for ${entry.ids.length} quests`,
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
[QUEST_ACTIONS.update]: (entry) => {
|
|
313
|
+
const q = this.update(entry.id, entry.description);
|
|
314
|
+
if (q) return { success: true, message: `Redone update for quest [${entry.id}]` };
|
|
315
|
+
return { success: false, message: formatNotFound(entry.id) };
|
|
316
|
+
},
|
|
317
|
+
[QUEST_ACTIONS.delete]: (entry) => {
|
|
318
|
+
const result = this.deleteMany(entry.ids);
|
|
319
|
+
if ("error" in result) {
|
|
320
|
+
return {
|
|
321
|
+
success: false,
|
|
322
|
+
message:
|
|
323
|
+
result.error === "blocked"
|
|
324
|
+
? formatBlockedBySteps(result.id)
|
|
325
|
+
: formatNotFound(result.id),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const firstQuest = result.quests[0];
|
|
330
|
+
return {
|
|
331
|
+
success: true,
|
|
332
|
+
message:
|
|
333
|
+
entry.ids.length === 1 && firstQuest
|
|
334
|
+
? `Redone delete for quest [${firstQuest.id}]`
|
|
335
|
+
: `Redone delete for ${entry.ids.length} tasks`,
|
|
336
|
+
};
|
|
337
|
+
},
|
|
338
|
+
[QUEST_ACTIONS.clear]: (entry) => {
|
|
339
|
+
const count = this.clear(entry.all);
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
message: `Redone clear (${count} quests ${entry.all ? "" : "completed "}cleared)`,
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
[QUEST_ACTIONS.reorder]: (entry) => {
|
|
346
|
+
const q = this.reorder(entry.id, entry.targetId);
|
|
347
|
+
if (q) return { success: true, message: `Redone reorder for quest [${entry.id}]` };
|
|
348
|
+
return { success: false, message: formatReorderedQuestNotFoundError() };
|
|
349
|
+
},
|
|
350
|
+
[QUEST_ACTIONS.reparent]: (entry) => {
|
|
351
|
+
try {
|
|
352
|
+
const q = this.reparent(entry.id, entry.parentId);
|
|
353
|
+
if (q) return { success: true, message: `Redone reparent for quest [${entry.id}]` };
|
|
354
|
+
return { success: false, message: formatNotFound(entry.id) };
|
|
355
|
+
} catch (err) {
|
|
356
|
+
return {
|
|
357
|
+
success: false,
|
|
358
|
+
message: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
redo(): QuestOperationResult {
|
|
365
|
+
const entry = this.redoStack.pop();
|
|
366
|
+
if (!entry) {
|
|
367
|
+
return { success: false, message: formatNothingToRedoError() };
|
|
368
|
+
}
|
|
369
|
+
const handler = this.redoHandlers[entry.type];
|
|
370
|
+
if (handler) {
|
|
371
|
+
this.inRedo = true;
|
|
372
|
+
const result = handler(entry as never);
|
|
373
|
+
this.inRedo = false;
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
return { success: false, message: "Unknown redo entry" };
|
|
377
|
+
}
|
|
378
|
+
|
|
233
379
|
/*
|
|
234
380
|
* Returns all quests including steps, inserted in order
|
|
235
381
|
* steps are returned immediately after their parent quest
|
|
@@ -265,11 +411,73 @@ export class QuestLog {
|
|
|
265
411
|
return this.quests.find((q) => q.id === id) ?? this.steps.find((step) => step.id === id);
|
|
266
412
|
}
|
|
267
413
|
|
|
414
|
+
private normalizeIds(ids: string[]): string[] {
|
|
415
|
+
return [...new Set(ids)];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private validateToggleBatch(ids: string[]): ToggleBatchResult {
|
|
419
|
+
const normalizedIds = this.normalizeIds(ids);
|
|
420
|
+
const quests: (Quest | Step)[] = [];
|
|
421
|
+
|
|
422
|
+
for (const id of normalizedIds) {
|
|
423
|
+
const quest = this.findById(id);
|
|
424
|
+
if (!quest) {
|
|
425
|
+
logger.debug("quests:state", "toggle-not-found", { id });
|
|
426
|
+
return { error: "notFound", id };
|
|
427
|
+
}
|
|
428
|
+
quests.push(quest);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const toggledIds = new Set(normalizedIds);
|
|
432
|
+
for (const quest of quests) {
|
|
433
|
+
if (quest.done || "parentId" in quest) continue;
|
|
434
|
+
|
|
435
|
+
const steps = this.getSteps(quest.id);
|
|
436
|
+
const hasIncompleteSteps = steps.some((step) => {
|
|
437
|
+
const finalDone = toggledIds.has(step.id) ? !step.done : step.done;
|
|
438
|
+
return !finalDone;
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (hasIncompleteSteps) {
|
|
442
|
+
logger.debug("quests:state", "toggle-blocked-steps", { id: quest.id, ids: normalizedIds });
|
|
443
|
+
return { error: "blocked", id: quest.id };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return { quests, ids: normalizedIds };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private applyToggleBatch(quests: (Quest | Step)[], ids: string[]): Quest[] {
|
|
451
|
+
for (const quest of quests) {
|
|
452
|
+
quest.done = !quest.done;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
this.history.push({ type: QUEST_ACTIONS.toggle, ids });
|
|
456
|
+
logger.debug("quests:state", QUEST_ACTIONS.toggle, {
|
|
457
|
+
ids,
|
|
458
|
+
total: this.quests.length + this.steps.length,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
return quests;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
toggleMany(ids: string[]): ToggleBatchResult {
|
|
465
|
+
if (!this.inRedo) this.redoStack = [];
|
|
466
|
+
const result = this.validateToggleBatch(ids);
|
|
467
|
+
if ("error" in result) return result;
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
ids: result.ids,
|
|
471
|
+
quests: this.applyToggleBatch(result.quests, result.ids),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
268
475
|
getUsedIds(): string[] {
|
|
269
476
|
return Array.from(this.usedIds);
|
|
270
477
|
}
|
|
271
478
|
|
|
272
479
|
add(description: string): Quest {
|
|
480
|
+
if (!this.inRedo) this.redoStack = [];
|
|
273
481
|
const quest: Quest = {
|
|
274
482
|
id: this.generateId(),
|
|
275
483
|
description,
|
|
@@ -289,6 +497,7 @@ export class QuestLog {
|
|
|
289
497
|
}
|
|
290
498
|
|
|
291
499
|
addStep(description: string, parentId: string): Step {
|
|
500
|
+
if (!this.inRedo) this.redoStack = [];
|
|
292
501
|
if (this.steps.some((step) => step.id === parentId)) {
|
|
293
502
|
throw new Error(formatStepCannotHaveSteps(parentId));
|
|
294
503
|
}
|
|
@@ -332,32 +541,16 @@ export class QuestLog {
|
|
|
332
541
|
}
|
|
333
542
|
|
|
334
543
|
toggle(id: string): Quest | Step | null | undefined {
|
|
335
|
-
const
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
return undefined;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (!quest.done && !("parentId" in quest)) {
|
|
342
|
-
const steps = this.getSteps(id);
|
|
343
|
-
if (steps.some((q) => !q.done)) {
|
|
344
|
-
logger.debug("quests:state", "toggle-blocked-steps", { id });
|
|
345
|
-
return null;
|
|
346
|
-
}
|
|
544
|
+
const result = this.toggleMany([id]);
|
|
545
|
+
if ("error" in result) {
|
|
546
|
+
return result.error === "blocked" ? null : undefined;
|
|
347
547
|
}
|
|
348
548
|
|
|
349
|
-
|
|
350
|
-
this.history.push({ type: QUEST_ACTIONS.toggle, id });
|
|
351
|
-
|
|
352
|
-
logger.debug("quests:state", QUEST_ACTIONS.toggle, {
|
|
353
|
-
id,
|
|
354
|
-
done: quest.done,
|
|
355
|
-
total: this.quests.length + this.steps.length,
|
|
356
|
-
});
|
|
357
|
-
return quest;
|
|
549
|
+
return result.quests[0];
|
|
358
550
|
}
|
|
359
551
|
|
|
360
552
|
update(id: string, description: string): Quest | Step | undefined {
|
|
553
|
+
if (!this.inRedo) this.redoStack = [];
|
|
361
554
|
const quest = this.findById(id);
|
|
362
555
|
if (!quest) {
|
|
363
556
|
logger.debug("quests:state", "update-not-found", { id });
|
|
@@ -375,49 +568,91 @@ export class QuestLog {
|
|
|
375
568
|
return quest;
|
|
376
569
|
}
|
|
377
570
|
|
|
571
|
+
private validateDeleteBatch(
|
|
572
|
+
ids: string[],
|
|
573
|
+
): { ids: string[]; deletedIds: string[] } | { error: "notFound" | "blocked"; id: string } {
|
|
574
|
+
const normalizedIds = this.normalizeIds(ids);
|
|
575
|
+
const selectedIds = new Set(normalizedIds);
|
|
576
|
+
const deletedIds = new Set<string>();
|
|
577
|
+
|
|
578
|
+
for (const id of normalizedIds) {
|
|
579
|
+
const quest = this.findById(id);
|
|
580
|
+
if (!quest) {
|
|
581
|
+
logger.debug("quests:state", "delete-not-found", { id });
|
|
582
|
+
return { error: "notFound", id };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if ("parentId" in quest) {
|
|
586
|
+
deletedIds.add(quest.id);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const steps = this.getSteps(quest.id);
|
|
591
|
+
const hasUnselectedIncompleteStep = steps.some(
|
|
592
|
+
(step) => !step.done && !selectedIds.has(step.id),
|
|
593
|
+
);
|
|
594
|
+
if (hasUnselectedIncompleteStep) {
|
|
595
|
+
logger.debug("quests:state", "delete-blocked-steps", { id: quest.id, ids: normalizedIds });
|
|
596
|
+
return { error: "blocked", id: quest.id };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
deletedIds.add(quest.id);
|
|
600
|
+
for (const step of steps) {
|
|
601
|
+
deletedIds.add(step.id);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return { ids: normalizedIds, deletedIds: [...deletedIds] };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private applyDeleteBatch(ids: string[], deletedIds: string[]): Quest[] {
|
|
609
|
+
const deletedIdSet = new Set(deletedIds);
|
|
610
|
+
const previousQuests = [...this.quests];
|
|
611
|
+
const previousSteps = [...this.steps];
|
|
612
|
+
const deletedQuests = this.getAll().filter((quest) => deletedIdSet.has(quest.id));
|
|
613
|
+
|
|
614
|
+
this.quests = this.quests.filter((quest) => !deletedIdSet.has(quest.id));
|
|
615
|
+
this.steps = this.steps.filter((step) => !deletedIdSet.has(step.id));
|
|
616
|
+
this.usedIds = new Set([...this.quests, ...this.steps].map((quest) => quest.id));
|
|
617
|
+
this.history.push({
|
|
618
|
+
type: QUEST_ACTIONS.delete,
|
|
619
|
+
ids,
|
|
620
|
+
deletedIds,
|
|
621
|
+
previousQuests,
|
|
622
|
+
previousSteps,
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
logger.debug("quests:state", QUEST_ACTIONS.delete, {
|
|
626
|
+
ids,
|
|
627
|
+
deletedIds,
|
|
628
|
+
total: this.quests.length + this.steps.length,
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
return deletedQuests;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
deleteMany(ids: string[]): DeleteBatchResult {
|
|
635
|
+
if (!this.inRedo) this.redoStack = [];
|
|
636
|
+
const result = this.validateDeleteBatch(ids);
|
|
637
|
+
if ("error" in result) return result;
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
ids: result.ids,
|
|
641
|
+
quests: this.applyDeleteBatch(result.ids, result.deletedIds),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
378
645
|
delete(id: string): Quest | Step | null | undefined {
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
if (steps.some((q) => !q.done)) {
|
|
383
|
-
logger.debug("quests:state", "delete-blocked-steps", { id });
|
|
384
|
-
return null;
|
|
385
|
-
}
|
|
386
|
-
const [quest] = this.quests.splice(index, 1);
|
|
387
|
-
this.usedIds.delete(quest.id);
|
|
388
|
-
const cascadeDeletedSteps = this.steps.filter((step) => step.parentId === id);
|
|
389
|
-
this.history.push({ type: QUEST_ACTIONS.delete, quest, index, cascadeDeletedSteps });
|
|
390
|
-
this.steps = this.steps.filter((step) => {
|
|
391
|
-
if (step.parentId === id) {
|
|
392
|
-
this.usedIds.delete(step.id);
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
return true;
|
|
396
|
-
});
|
|
397
|
-
logger.debug("quests:state", QUEST_ACTIONS.delete, {
|
|
398
|
-
id,
|
|
399
|
-
index,
|
|
400
|
-
total: this.quests.length + this.steps.length,
|
|
401
|
-
});
|
|
402
|
-
return quest;
|
|
403
|
-
}
|
|
404
|
-
const stepIndex = this.steps.findIndex((step) => step.id === id);
|
|
405
|
-
if (stepIndex !== -1) {
|
|
406
|
-
const [quest] = this.steps.splice(stepIndex, 1);
|
|
407
|
-
this.usedIds.delete(quest.id);
|
|
408
|
-
this.history.push({ type: QUEST_ACTIONS.delete, quest, index: stepIndex, isStep: true });
|
|
409
|
-
logger.debug("quests:state", QUEST_ACTIONS.delete, {
|
|
410
|
-
id,
|
|
411
|
-
index: stepIndex,
|
|
412
|
-
total: this.quests.length + this.steps.length,
|
|
413
|
-
});
|
|
414
|
-
return quest;
|
|
646
|
+
const result = this.deleteMany([id]);
|
|
647
|
+
if ("error" in result) {
|
|
648
|
+
return result.error === "blocked" ? null : undefined;
|
|
415
649
|
}
|
|
416
|
-
|
|
417
|
-
return
|
|
650
|
+
|
|
651
|
+
return result.quests[0];
|
|
418
652
|
}
|
|
419
653
|
|
|
420
654
|
clear(all = false): number {
|
|
655
|
+
if (!this.inRedo) this.redoStack = [];
|
|
421
656
|
if (!all) {
|
|
422
657
|
const doneParentIds = new Set(this.quests.filter((q) => q.done).map((q) => q.id));
|
|
423
658
|
const done = this.quests.filter((q) => q.done);
|
|
@@ -458,6 +693,7 @@ export class QuestLog {
|
|
|
458
693
|
}
|
|
459
694
|
|
|
460
695
|
reorder(id: string, targetId: string): Quest | undefined {
|
|
696
|
+
if (!this.inRedo) this.redoStack = [];
|
|
461
697
|
if (this.steps.some((step) => step.id === id || step.id === targetId)) {
|
|
462
698
|
logger.debug("quests:state", "reorder-step-rejected", { id, targetId });
|
|
463
699
|
return undefined;
|
|
@@ -498,6 +734,7 @@ export class QuestLog {
|
|
|
498
734
|
}
|
|
499
735
|
|
|
500
736
|
reparent(id: string, parentId?: string): Quest | Step | undefined {
|
|
737
|
+
if (!this.inRedo) this.redoStack = [];
|
|
501
738
|
const quest = this.findById(id);
|
|
502
739
|
if (!quest) return undefined;
|
|
503
740
|
|
|
@@ -553,29 +790,72 @@ export class QuestLog {
|
|
|
553
790
|
return quest;
|
|
554
791
|
}
|
|
555
792
|
|
|
556
|
-
|
|
793
|
+
undo(): QuestOperationResult {
|
|
557
794
|
const entry = this.history.pop();
|
|
558
795
|
if (!entry) {
|
|
559
|
-
logger.debug("quests:state", "
|
|
560
|
-
return { success: false, message:
|
|
796
|
+
logger.debug("quests:state", "undo-empty");
|
|
797
|
+
return { success: false, message: formatNothingToUndoError() };
|
|
561
798
|
}
|
|
562
799
|
|
|
563
|
-
logger.debug("quests:state", "
|
|
800
|
+
logger.debug("quests:state", "undo", { type: entry.type });
|
|
801
|
+
this.redoStack.push(this.buildRedoEntry(entry));
|
|
564
802
|
|
|
565
803
|
const handler = this.undoHandlers[entry.type];
|
|
566
804
|
if (handler) {
|
|
567
805
|
return handler(entry as never);
|
|
568
806
|
}
|
|
569
807
|
|
|
570
|
-
logger.debug("quests:state", "
|
|
808
|
+
logger.debug("quests:state", "undo-unknown", { type: (entry as { type: string }).type });
|
|
571
809
|
return { success: false, message: "Unknown history entry" };
|
|
572
810
|
}
|
|
573
811
|
|
|
812
|
+
private buildRedoEntry(entry: HistoryEntry): RedoEntry {
|
|
813
|
+
switch (entry.type) {
|
|
814
|
+
case QUEST_ACTIONS.add: {
|
|
815
|
+
const quest = this.findById(entry.id);
|
|
816
|
+
if (!quest) throw new Error(formatNotFound(entry.id));
|
|
817
|
+
return { type: QUEST_ACTIONS.add, quest };
|
|
818
|
+
}
|
|
819
|
+
case QUEST_ACTIONS.toggle:
|
|
820
|
+
return { type: QUEST_ACTIONS.toggle, ids: entry.ids };
|
|
821
|
+
case QUEST_ACTIONS.update:
|
|
822
|
+
return {
|
|
823
|
+
type: QUEST_ACTIONS.update,
|
|
824
|
+
id: entry.id,
|
|
825
|
+
description: this.findById(entry.id)?.description ?? entry.previousDescription,
|
|
826
|
+
};
|
|
827
|
+
case QUEST_ACTIONS.delete:
|
|
828
|
+
return { type: QUEST_ACTIONS.delete, ids: entry.ids };
|
|
829
|
+
case QUEST_ACTIONS.clear:
|
|
830
|
+
return { type: QUEST_ACTIONS.clear, all: entry.all };
|
|
831
|
+
case QUEST_ACTIONS.reorder:
|
|
832
|
+
return { type: QUEST_ACTIONS.reorder, id: entry.quest.id, targetId: entry.targetId };
|
|
833
|
+
case QUEST_ACTIONS.reparent: {
|
|
834
|
+
const quest = this.findById(entry.id);
|
|
835
|
+
if (!quest) throw new Error(formatNotFound(entry.id));
|
|
836
|
+
return {
|
|
837
|
+
type: QUEST_ACTIONS.reparent,
|
|
838
|
+
id: entry.id,
|
|
839
|
+
parentId: "parentId" in quest ? quest.parentId : undefined,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
574
845
|
/**
|
|
575
846
|
* Execute a quest action and return a presentation-ready result.
|
|
576
847
|
* This is the primary API for all adapter layers (commands, tools).
|
|
577
848
|
*/
|
|
578
849
|
execute(action: QuestAction): QuestOperationResult {
|
|
850
|
+
if (
|
|
851
|
+
action.type !== QUEST_ACTIONS.list &&
|
|
852
|
+
action.type !== QUEST_ACTIONS.rules &&
|
|
853
|
+
action.type !== QUEST_ACTIONS.skill &&
|
|
854
|
+
action.type !== QUEST_ACTIONS.undo &&
|
|
855
|
+
action.type !== QUEST_ACTIONS.redo
|
|
856
|
+
) {
|
|
857
|
+
this.redoStack = [];
|
|
858
|
+
}
|
|
579
859
|
switch (action.type) {
|
|
580
860
|
case QUEST_ACTIONS.add: {
|
|
581
861
|
if (action.descriptions && action.descriptions.length > 0) {
|
|
@@ -623,19 +903,42 @@ export class QuestLog {
|
|
|
623
903
|
return { success: true, message: formatQuestList(quests) };
|
|
624
904
|
}
|
|
625
905
|
case QUEST_ACTIONS.toggle: {
|
|
626
|
-
|
|
906
|
+
const ids = action.ids?.length ? action.ids : action.id ? [action.id] : undefined;
|
|
907
|
+
if (!ids || ids.length === 0) {
|
|
627
908
|
return { success: false, message: formatIdRequiredError("toggle") };
|
|
628
909
|
}
|
|
629
910
|
|
|
630
|
-
const
|
|
631
|
-
if (
|
|
632
|
-
return {
|
|
911
|
+
const result = this.toggleMany(ids);
|
|
912
|
+
if ("error" in result) {
|
|
913
|
+
return {
|
|
914
|
+
success: false,
|
|
915
|
+
message:
|
|
916
|
+
result.error === "blocked"
|
|
917
|
+
? formatBlockedBySteps(result.id)
|
|
918
|
+
: formatNotFound(result.id),
|
|
919
|
+
};
|
|
633
920
|
}
|
|
634
|
-
|
|
635
|
-
|
|
921
|
+
|
|
922
|
+
if (result.quests.length === 1) {
|
|
923
|
+
const quest = result.quests[0];
|
|
924
|
+
if (!quest) {
|
|
925
|
+
return { success: false, message: formatNotFound(result.ids[0]) };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return {
|
|
929
|
+
success: true,
|
|
930
|
+
message: formatToggleResult(result.ids[0], quest.done),
|
|
931
|
+
quest,
|
|
932
|
+
};
|
|
636
933
|
}
|
|
637
934
|
|
|
638
|
-
return {
|
|
935
|
+
return {
|
|
936
|
+
success: true,
|
|
937
|
+
message: formatBatchToggleResult(
|
|
938
|
+
result.quests.map((quest, index) => ({ id: result.ids[index], done: quest.done })),
|
|
939
|
+
),
|
|
940
|
+
quests: result.quests,
|
|
941
|
+
};
|
|
639
942
|
}
|
|
640
943
|
case QUEST_ACTIONS.update: {
|
|
641
944
|
if (action.id === undefined) {
|
|
@@ -654,19 +957,38 @@ export class QuestLog {
|
|
|
654
957
|
return { success: true, message: formatUpdateResult(q), quest: q };
|
|
655
958
|
}
|
|
656
959
|
case QUEST_ACTIONS.delete: {
|
|
657
|
-
|
|
960
|
+
const ids = action.ids?.length ? action.ids : action.id ? [action.id] : undefined;
|
|
961
|
+
if (!ids || ids.length === 0) {
|
|
658
962
|
return { success: false, message: formatIdRequiredError("delete") };
|
|
659
963
|
}
|
|
660
964
|
|
|
661
|
-
const
|
|
662
|
-
if (
|
|
663
|
-
return {
|
|
965
|
+
const result = this.deleteMany(ids);
|
|
966
|
+
if ("error" in result) {
|
|
967
|
+
return {
|
|
968
|
+
success: false,
|
|
969
|
+
message:
|
|
970
|
+
result.error === "blocked"
|
|
971
|
+
? formatBlockedBySteps(result.id)
|
|
972
|
+
: formatNotFound(result.id),
|
|
973
|
+
};
|
|
664
974
|
}
|
|
665
|
-
|
|
666
|
-
|
|
975
|
+
|
|
976
|
+
if (result.ids.length === 1) {
|
|
977
|
+
const quest = result.quests[0];
|
|
978
|
+
if (!quest) {
|
|
979
|
+
return { success: false, message: formatNotFound(result.ids[0]) };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return { success: true, message: formatDeleteResult(quest), quest };
|
|
667
983
|
}
|
|
668
984
|
|
|
669
|
-
return {
|
|
985
|
+
return {
|
|
986
|
+
success: true,
|
|
987
|
+
message: formatBatchDeleteResult(
|
|
988
|
+
result.quests.map((quest) => ({ id: quest.id, description: quest.description })),
|
|
989
|
+
),
|
|
990
|
+
quests: result.quests,
|
|
991
|
+
};
|
|
670
992
|
}
|
|
671
993
|
case QUEST_ACTIONS.clear: {
|
|
672
994
|
const count = this.clear(action.all);
|
|
@@ -730,8 +1052,11 @@ export class QuestLog {
|
|
|
730
1052
|
case QUEST_ACTIONS.skill: {
|
|
731
1053
|
return { success: true, message: getQuestSkillDocument() };
|
|
732
1054
|
}
|
|
733
|
-
case QUEST_ACTIONS.
|
|
734
|
-
return this.
|
|
1055
|
+
case QUEST_ACTIONS.undo: {
|
|
1056
|
+
return this.undo();
|
|
1057
|
+
}
|
|
1058
|
+
case QUEST_ACTIONS.redo: {
|
|
1059
|
+
return this.redo();
|
|
735
1060
|
}
|
|
736
1061
|
default: {
|
|
737
1062
|
return {
|
|
@@ -787,6 +1112,7 @@ export function makeToolResult(
|
|
|
787
1112
|
text: string,
|
|
788
1113
|
questLog: QuestLog,
|
|
789
1114
|
displayQuests?: Quest[],
|
|
1115
|
+
snapshotQuests?: Quest[],
|
|
790
1116
|
): AgentToolResult<unknown> {
|
|
791
1117
|
return {
|
|
792
1118
|
content: [{ type: "text", text }],
|
|
@@ -794,6 +1120,7 @@ export function makeToolResult(
|
|
|
794
1120
|
quests: questLog.getAll(),
|
|
795
1121
|
usedIds: questLog.getUsedIds(),
|
|
796
1122
|
displayQuests,
|
|
1123
|
+
snapshotQuests,
|
|
797
1124
|
},
|
|
798
1125
|
};
|
|
799
1126
|
}
|
package/src/quest/formatters.ts
CHANGED
|
@@ -44,6 +44,12 @@ export function formatToggleResult(id: string, done: boolean): string {
|
|
|
44
44
|
return `Quest [${id}] ${done ? "done" : "undone"}`;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export function formatBatchToggleResult(toggled: { id: string; done: boolean }[]): string {
|
|
48
|
+
return `Toggled ${toggled.length} tasks:\n${toggled
|
|
49
|
+
.map((quest) => `[${quest.id}] ${quest.done ? "done" : "undone"}`)
|
|
50
|
+
.join("\n")}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
export function formatUpdateResult(q: { id: string; description: string }): string {
|
|
48
54
|
return `Updated quest [${q.id}]: ${q.description}`;
|
|
49
55
|
}
|
|
@@ -52,6 +58,10 @@ export function formatDeleteResult(q: { id: string; description: string }): stri
|
|
|
52
58
|
return `Deleted quest [${q.id}]: ${q.description}`;
|
|
53
59
|
}
|
|
54
60
|
|
|
61
|
+
export function formatBatchDeleteResult(deleted: { id: string; description: string }[]): string {
|
|
62
|
+
return `Deleted ${deleted.length} tasks:\n${deleted.map((quest) => `[${quest.id}] ${quest.description}`).join("\n")}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
export function formatNotFound(id: string): string {
|
|
56
66
|
return `Quest [${id}] not found. IDs are random hex strings shown in brackets. Use the list action to see valid IDs.`;
|
|
57
67
|
}
|
|
@@ -100,8 +110,12 @@ export function formatUnknownActionError(action: string): string {
|
|
|
100
110
|
return `Unknown action: ${action}. Use the list action or check the tool schema for supported actions.`;
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
export function
|
|
104
|
-
return `Nothing to
|
|
113
|
+
export function formatNothingToUndoError(): string {
|
|
114
|
+
return `Nothing to undo. The history is empty because no mutating actions have been performed yet.`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function formatNothingToRedoError(): string {
|
|
118
|
+
return `Nothing to redo. The redo stack is empty because no actions have been undone yet.`;
|
|
105
119
|
}
|
|
106
120
|
|
|
107
121
|
export function formatReorderedQuestNotFoundError(): string {
|
package/src/quest/types.ts
CHANGED
|
@@ -10,7 +10,8 @@ export const QUEST_ACTIONS = {
|
|
|
10
10
|
delete: "delete",
|
|
11
11
|
clear: "clear",
|
|
12
12
|
reorder: "reorder",
|
|
13
|
-
|
|
13
|
+
undo: "undo",
|
|
14
|
+
redo: "redo",
|
|
14
15
|
reparent: "reparent",
|
|
15
16
|
rules: "rules",
|
|
16
17
|
skill: "skill",
|
|
@@ -26,7 +27,8 @@ export const QUEST_ACTION_VALUES = [
|
|
|
26
27
|
QUEST_ACTIONS.delete,
|
|
27
28
|
QUEST_ACTIONS.clear,
|
|
28
29
|
QUEST_ACTIONS.reorder,
|
|
29
|
-
QUEST_ACTIONS.
|
|
30
|
+
QUEST_ACTIONS.undo,
|
|
31
|
+
QUEST_ACTIONS.redo,
|
|
30
32
|
QUEST_ACTIONS.reparent,
|
|
31
33
|
QUEST_ACTIONS.rules,
|
|
32
34
|
QUEST_ACTIONS.skill,
|
package/src/renderers/tools.ts
CHANGED
|
@@ -11,6 +11,7 @@ type QuestArgs = {
|
|
|
11
11
|
descriptions?: string[];
|
|
12
12
|
targetId?: string;
|
|
13
13
|
id?: string;
|
|
14
|
+
ids?: string[];
|
|
14
15
|
all?: boolean;
|
|
15
16
|
};
|
|
16
17
|
|
|
@@ -31,12 +32,29 @@ export function renderQuestCall(args: QuestArgs, theme: Theme, _context: unknown
|
|
|
31
32
|
);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
if (
|
|
35
|
-
(args.
|
|
36
|
-
args.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
if (args.action === QUEST_ACTIONS.toggle) {
|
|
36
|
+
if (args.ids && args.ids.length > 1) {
|
|
37
|
+
return new Text(`${actionText} ${theme.fg("muted", `[${args.ids.length} tasks]`)}`, 0, 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const toggleId = args.id ?? args.ids?.[0];
|
|
41
|
+
if (toggleId !== undefined) {
|
|
42
|
+
return new Text(`${actionText} ${theme.fg("muted", `[${toggleId}]`)}`, 0, 0);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (args.action === QUEST_ACTIONS.delete) {
|
|
47
|
+
if (args.ids && args.ids.length > 1) {
|
|
48
|
+
return new Text(`${actionText} ${theme.fg("muted", `[${args.ids.length} tasks]`)}`, 0, 0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const deleteId = args.id ?? args.ids?.[0];
|
|
52
|
+
if (deleteId !== undefined) {
|
|
53
|
+
return new Text(`${actionText} ${theme.fg("muted", `[${deleteId}]`)}`, 0, 0);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (args.action === QUEST_ACTIONS.update && args.id !== undefined) {
|
|
40
58
|
return new Text(`${actionText} ${theme.fg("muted", `[${args.id}]`)}`, 0, 0);
|
|
41
59
|
}
|
|
42
60
|
|
|
@@ -59,8 +77,11 @@ export function renderQuestResult(config: ResolvedConfig) {
|
|
|
59
77
|
isPartial: options.isPartial,
|
|
60
78
|
});
|
|
61
79
|
const details = result.details as Record<string, unknown> | undefined;
|
|
62
|
-
const
|
|
63
|
-
const
|
|
80
|
+
const snapshotQuests = details?.snapshotQuests as Quest[] | undefined;
|
|
81
|
+
const allQuests = (snapshotQuests ?? details?.quests) as Quest[] | undefined;
|
|
82
|
+
const questsToRender = (snapshotQuests ?? details?.displayQuests ?? details?.quests) as
|
|
83
|
+
| Quest[]
|
|
84
|
+
| undefined;
|
|
64
85
|
|
|
65
86
|
if (
|
|
66
87
|
!Array.isArray(questsToRender) ||
|
package/src/tools/handler.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import type { AgentToolResult, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { type Static, Type } from "
|
|
1
|
+
import type { AgentToolResult, ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
3
|
import type { ResolvedConfig } from "../config.js";
|
|
4
4
|
import { logger } from "../logger.js";
|
|
5
5
|
import { QUEST_PROMPT_GATE, QUEST_PROMPT_REMINDER } from "../prompts.js";
|
|
6
6
|
import { makeToolResult, type QuestAction, type QuestLog } from "../quest/dataplane.js";
|
|
7
|
-
import { QUEST_ACTIONS } from "../quest/types.js";
|
|
7
|
+
import { QUEST_ACTIONS, type QuestActionType } from "../quest/types.js";
|
|
8
8
|
|
|
9
9
|
const SPLIT_DISPLAY_ACTIONS = [
|
|
10
10
|
QUEST_ACTIONS.add,
|
|
11
11
|
QUEST_ACTIONS.list,
|
|
12
12
|
QUEST_ACTIONS.split,
|
|
13
13
|
QUEST_ACTIONS.add_step,
|
|
14
|
-
QUEST_ACTIONS.
|
|
14
|
+
QUEST_ACTIONS.undo,
|
|
15
|
+
QUEST_ACTIONS.redo,
|
|
15
16
|
] as const;
|
|
16
17
|
|
|
17
18
|
import { invalidateQuestListWidget } from "../renderers/commands.js";
|
|
@@ -20,75 +21,72 @@ import { createQuestParams, type QuestParamsType } from "./params.js";
|
|
|
20
21
|
|
|
21
22
|
type QuestToolParams = Static<QuestParamsType>;
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
[QUEST_ACTIONS.add](questLog, params, toolCallId)
|
|
31
|
-
|
|
24
|
+
type ToolHandler = (
|
|
25
|
+
questLog: QuestLog,
|
|
26
|
+
params: QuestToolParams,
|
|
27
|
+
toolCallId: string,
|
|
28
|
+
) => AgentToolResult<unknown>;
|
|
29
|
+
|
|
30
|
+
const toolHandlers: Record<QuestActionType, ToolHandler> = {
|
|
31
|
+
[QUEST_ACTIONS.add]: (questLog, params, toolCallId) =>
|
|
32
|
+
runTool(questLog, toolCallId, {
|
|
32
33
|
type: QUEST_ACTIONS.add,
|
|
33
34
|
descriptions: params.descriptions,
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return runTool(questLog, toolCallId, {
|
|
35
|
+
}),
|
|
36
|
+
[QUEST_ACTIONS.split]: (questLog, params, toolCallId) =>
|
|
37
|
+
runTool(questLog, toolCallId, {
|
|
38
38
|
type: QUEST_ACTIONS.split,
|
|
39
39
|
id: params.id,
|
|
40
40
|
descriptions: params.descriptions,
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return runTool(questLog, toolCallId, {
|
|
41
|
+
}),
|
|
42
|
+
[QUEST_ACTIONS.add_step]: (questLog, params, toolCallId) =>
|
|
43
|
+
runTool(questLog, toolCallId, {
|
|
45
44
|
type: QUEST_ACTIONS.add_step,
|
|
46
45
|
id: params.id,
|
|
47
46
|
descriptions: params.descriptions,
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
}),
|
|
48
|
+
[QUEST_ACTIONS.list]: (questLog, _params, toolCallId) =>
|
|
49
|
+
runTool(questLog, toolCallId, { type: QUEST_ACTIONS.list }),
|
|
50
|
+
[QUEST_ACTIONS.toggle]: (questLog, params, toolCallId) =>
|
|
51
|
+
runTool(questLog, toolCallId, {
|
|
52
|
+
type: QUEST_ACTIONS.toggle,
|
|
53
|
+
id: params.id,
|
|
54
|
+
ids: params.ids,
|
|
55
|
+
}),
|
|
56
|
+
[QUEST_ACTIONS.update]: (questLog, params, toolCallId) =>
|
|
57
|
+
runTool(questLog, toolCallId, {
|
|
58
58
|
type: QUEST_ACTIONS.update,
|
|
59
59
|
id: params.id,
|
|
60
60
|
description: params.description,
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
}),
|
|
62
|
+
[QUEST_ACTIONS.delete]: (questLog, params, toolCallId) =>
|
|
63
|
+
runTool(questLog, toolCallId, {
|
|
64
|
+
type: QUEST_ACTIONS.delete,
|
|
65
|
+
id: params.id,
|
|
66
|
+
ids: params.ids,
|
|
67
|
+
}),
|
|
68
|
+
[QUEST_ACTIONS.clear]: (questLog, params, toolCallId) =>
|
|
69
|
+
runTool(questLog, toolCallId, { type: QUEST_ACTIONS.clear, all: params.all }),
|
|
70
|
+
[QUEST_ACTIONS.reorder]: (questLog, params, toolCallId) =>
|
|
71
|
+
runTool(questLog, toolCallId, {
|
|
71
72
|
type: QUEST_ACTIONS.reorder,
|
|
72
73
|
id: params.id,
|
|
73
74
|
targetId: params.targetId,
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return runTool(questLog, toolCallId, {
|
|
75
|
+
}),
|
|
76
|
+
[QUEST_ACTIONS.reparent]: (questLog, params, toolCallId) =>
|
|
77
|
+
runTool(questLog, toolCallId, {
|
|
78
78
|
type: QUEST_ACTIONS.reparent,
|
|
79
79
|
id: params.id,
|
|
80
80
|
parentId: params.parentId,
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
[QUEST_ACTIONS.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.revert });
|
|
91
|
-
},
|
|
81
|
+
}),
|
|
82
|
+
[QUEST_ACTIONS.rules]: (questLog, _params, toolCallId) =>
|
|
83
|
+
runTool(questLog, toolCallId, { type: QUEST_ACTIONS.rules }),
|
|
84
|
+
[QUEST_ACTIONS.skill]: (questLog, _params, toolCallId) =>
|
|
85
|
+
runTool(questLog, toolCallId, { type: QUEST_ACTIONS.skill }),
|
|
86
|
+
[QUEST_ACTIONS.undo]: (questLog, _params, toolCallId) =>
|
|
87
|
+
runTool(questLog, toolCallId, { type: QUEST_ACTIONS.undo }),
|
|
88
|
+
[QUEST_ACTIONS.redo]: (questLog, _params, toolCallId) =>
|
|
89
|
+
runTool(questLog, toolCallId, { type: QUEST_ACTIONS.redo }),
|
|
92
90
|
};
|
|
93
91
|
|
|
94
92
|
function runTool(
|
|
@@ -104,11 +102,10 @@ function runTool(
|
|
|
104
102
|
action.type as (typeof SPLIT_DISPLAY_ACTIONS)[number],
|
|
105
103
|
)
|
|
106
104
|
? questLog.getAll()
|
|
107
|
-
: result.quest
|
|
108
|
-
|
|
109
|
-
: [];
|
|
105
|
+
: (result.quests ?? (result.quest ? [result.quest] : []));
|
|
106
|
+
const snapshotQuests = action.type === QUEST_ACTIONS.delete ? displayQuests : undefined;
|
|
110
107
|
|
|
111
|
-
return makeToolResult(result.message, questLog, displayQuests);
|
|
108
|
+
return makeToolResult(result.message, questLog, displayQuests, snapshotQuests);
|
|
112
109
|
}
|
|
113
110
|
|
|
114
111
|
export async function questToolExecute(
|
|
@@ -116,8 +113,13 @@ export async function questToolExecute(
|
|
|
116
113
|
toolCallId: string,
|
|
117
114
|
params: QuestToolParams,
|
|
118
115
|
): Promise<AgentToolResult<unknown>> {
|
|
119
|
-
logger.debug("quests:tool", "execute", {
|
|
120
|
-
|
|
116
|
+
logger.debug("quests:tool", "execute", {
|
|
117
|
+
toolCallId,
|
|
118
|
+
action: params.action,
|
|
119
|
+
id: params.id,
|
|
120
|
+
ids: params.ids,
|
|
121
|
+
});
|
|
122
|
+
const handler = toolHandlers[params.action as QuestActionType];
|
|
121
123
|
return handler(questLog, params, toolCallId);
|
|
122
124
|
}
|
|
123
125
|
|
|
@@ -140,7 +142,11 @@ export function registerQuestTool(
|
|
|
140
142
|
parameters: createQuestParams(config.ids.length),
|
|
141
143
|
execute: (toolCallId, params, _signal, _onUpdate, _ctx) =>
|
|
142
144
|
questToolExecute(questLog, toolCallId, params),
|
|
143
|
-
renderCall: renderQuestCall
|
|
145
|
+
renderCall: renderQuestCall as unknown as (
|
|
146
|
+
args: object,
|
|
147
|
+
theme: Theme,
|
|
148
|
+
context: unknown,
|
|
149
|
+
) => ReturnType<typeof renderQuestCall>,
|
|
144
150
|
renderResult: renderQuestResult(config),
|
|
145
151
|
});
|
|
146
152
|
|
|
@@ -159,8 +165,12 @@ export function registerQuestTool(
|
|
|
159
165
|
),
|
|
160
166
|
execute: async (_toolCallId, _params, _signal, _onUpdate, _ctx) =>
|
|
161
167
|
questToolExecute(questLog, "learn_quests", { action: QUEST_ACTIONS.skill }),
|
|
162
|
-
renderCall: (_args, theme, context) =>
|
|
163
|
-
renderQuestCall({ action: QUEST_ACTIONS.skill }, theme, context)
|
|
168
|
+
renderCall: ((_args: object, theme: Theme, context: unknown) =>
|
|
169
|
+
renderQuestCall({ action: QUEST_ACTIONS.skill }, theme, context)) as unknown as (
|
|
170
|
+
args: object,
|
|
171
|
+
theme: Theme,
|
|
172
|
+
context: unknown,
|
|
173
|
+
) => ReturnType<typeof renderQuestCall>,
|
|
164
174
|
renderResult: renderQuestResult(config),
|
|
165
175
|
});
|
|
166
176
|
}
|
package/src/tools/params.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StringEnum } from "@mariozechner/pi-ai";
|
|
2
|
-
import { Type } from "
|
|
2
|
+
import { Type } from "typebox";
|
|
3
3
|
import { QUEST_ACTION_VALUES } from "../quest/types.js";
|
|
4
4
|
|
|
5
5
|
export function createQuestParams(idLength: number) {
|
|
@@ -22,7 +22,13 @@ export function createQuestParams(idLength: number) {
|
|
|
22
22
|
id: Type.Optional(
|
|
23
23
|
Type.String({
|
|
24
24
|
pattern,
|
|
25
|
-
description: `Quest ID (required for
|
|
25
|
+
description: `Quest ID (required for update, reorder, and reparent actions. For toggle and delete, provide either id or ids. ALWAYS use the ${idLength}-digit hex ID shown in brackets, never the positional number.`,
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
ids: Type.Optional(
|
|
29
|
+
Type.Array(Type.String({ pattern }), {
|
|
30
|
+
minItems: 1,
|
|
31
|
+
description: `Quest IDs for batch toggle or delete actions. Use this when operating on more than one ${idLength}-digit hex quest ID at once.`,
|
|
26
32
|
}),
|
|
27
33
|
),
|
|
28
34
|
targetId: Type.Optional(
|