pi-quests 0.2.0 → 0.4.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 +18 -0
- package/README.md +20 -1
- package/package.json +1 -1
- package/src/commands/handler.ts +36 -11
- package/src/commands/parse-args.ts +52 -22
- package/src/config.ts +148 -0
- package/src/index.ts +66 -26
- package/src/prompts.ts +19 -0
- package/src/quest/dataplane.ts +393 -72
- package/src/quest/formatters.ts +31 -16
- package/src/quest/tracker.ts +34 -25
- package/src/quest/types.ts +7 -2
- package/src/renderers/commands.ts +46 -22
- package/src/renderers/quests.ts +45 -0
- package/src/renderers/tools.ts +81 -27
- package/src/tools/handler.ts +25 -17
- package/src/tools/params.ts +44 -19
package/src/quest/dataplane.ts
CHANGED
|
@@ -1,30 +1,53 @@
|
|
|
1
1
|
import type { AgentToolResult, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { DEFAULT_CONFIG, type ResolvedConfig } from "../config.js";
|
|
2
3
|
import { logger } from "../logger.js";
|
|
3
4
|
import {
|
|
5
|
+
formatAddResult,
|
|
4
6
|
formatBatchAddResult,
|
|
5
|
-
|
|
7
|
+
formatBlockedBySubQuests,
|
|
6
8
|
formatDeleteResult,
|
|
7
9
|
formatNotFound,
|
|
8
10
|
formatQuestList,
|
|
11
|
+
formatSubQuestCannotHaveSubQuests,
|
|
9
12
|
formatToggleResult,
|
|
10
13
|
formatUpdateResult,
|
|
11
14
|
} from "./formatters.js";
|
|
12
|
-
import { QUEST_ACTIONS, type Quest } from "./types.js";
|
|
15
|
+
import { QUEST_ACTIONS, type Quest, type SubQuest } from "./types.js";
|
|
13
16
|
|
|
14
17
|
export type HistoryEntry =
|
|
15
|
-
| { type: typeof QUEST_ACTIONS.add; id:
|
|
16
|
-
| { type: typeof QUEST_ACTIONS.toggle; id:
|
|
17
|
-
| { type: typeof QUEST_ACTIONS.update; id:
|
|
18
|
-
| {
|
|
19
|
-
|
|
18
|
+
| { type: typeof QUEST_ACTIONS.add; id: string; parentId?: string }
|
|
19
|
+
| { type: typeof QUEST_ACTIONS.toggle; id: string }
|
|
20
|
+
| { type: typeof QUEST_ACTIONS.update; id: string; previousDescription: string }
|
|
21
|
+
| {
|
|
22
|
+
type: typeof QUEST_ACTIONS.delete;
|
|
23
|
+
quest: Quest | SubQuest;
|
|
24
|
+
index: number;
|
|
25
|
+
isSubQuest?: boolean;
|
|
26
|
+
cascadeDeletedSubs?: SubQuest[];
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
type: typeof QUEST_ACTIONS.clear;
|
|
30
|
+
previousQuests: Quest[];
|
|
31
|
+
previousSubQuests?: SubQuest[];
|
|
32
|
+
all: false;
|
|
33
|
+
}
|
|
34
|
+
| { type: typeof QUEST_ACTIONS.clear; quests: Quest[]; subQuests?: SubQuest[]; all: true }
|
|
35
|
+
| {
|
|
36
|
+
type: typeof QUEST_ACTIONS.reorder;
|
|
37
|
+
quest: Quest;
|
|
38
|
+
oldIndex: number;
|
|
39
|
+
previousIds: string[];
|
|
40
|
+
targetId: string;
|
|
41
|
+
};
|
|
20
42
|
|
|
21
43
|
export type QuestAction =
|
|
22
|
-
| { type: typeof QUEST_ACTIONS.add; descriptions?: string[] }
|
|
44
|
+
| { type: typeof QUEST_ACTIONS.add; descriptions?: string[]; parentId?: string }
|
|
23
45
|
| { type: typeof QUEST_ACTIONS.list }
|
|
24
|
-
| { type: typeof QUEST_ACTIONS.toggle; id?:
|
|
25
|
-
| { type: typeof QUEST_ACTIONS.update; id?:
|
|
26
|
-
| { type: typeof QUEST_ACTIONS.delete; id?:
|
|
27
|
-
| { type: typeof QUEST_ACTIONS.clear }
|
|
46
|
+
| { type: typeof QUEST_ACTIONS.toggle; id?: string }
|
|
47
|
+
| { type: typeof QUEST_ACTIONS.update; id?: string; description?: string }
|
|
48
|
+
| { type: typeof QUEST_ACTIONS.delete; id?: string }
|
|
49
|
+
| { type: typeof QUEST_ACTIONS.clear; all?: boolean }
|
|
50
|
+
| { type: typeof QUEST_ACTIONS.reorder; id?: string; targetId?: string }
|
|
28
51
|
| { type: typeof QUEST_ACTIONS.revert };
|
|
29
52
|
|
|
30
53
|
export type QuestOperationResult = { success: boolean; message: string; quest?: Quest };
|
|
@@ -37,8 +60,35 @@ export type QuestOperationResult = { success: boolean; message: string; quest?:
|
|
|
37
60
|
*/
|
|
38
61
|
export class QuestLog {
|
|
39
62
|
private quests: Quest[] = [];
|
|
40
|
-
private
|
|
63
|
+
private subQuests: SubQuest[] = [];
|
|
64
|
+
private usedIds: Set<string> = new Set();
|
|
41
65
|
private history: HistoryEntry[] = [];
|
|
66
|
+
|
|
67
|
+
private readonly ID_LENGTH: number;
|
|
68
|
+
private readonly MAX_IDS: number;
|
|
69
|
+
|
|
70
|
+
constructor(config: ResolvedConfig = DEFAULT_CONFIG) {
|
|
71
|
+
this.ID_LENGTH = config.ids.length;
|
|
72
|
+
this.MAX_IDS = 16 ** this.ID_LENGTH;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private generateId(): string {
|
|
76
|
+
if (this.usedIds.size >= this.MAX_IDS) {
|
|
77
|
+
logger.debug("quests:state", "generate-id-exhausted", { usedIds: this.usedIds.size });
|
|
78
|
+
throw new Error("No available quest IDs. Clear done or all quests to free up IDs.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let id: string;
|
|
82
|
+
do {
|
|
83
|
+
id = Math.floor(Math.random() * this.MAX_IDS)
|
|
84
|
+
.toString(16)
|
|
85
|
+
.padStart(this.ID_LENGTH, "0")
|
|
86
|
+
.toLowerCase();
|
|
87
|
+
} while (this.usedIds.has(id));
|
|
88
|
+
|
|
89
|
+
this.usedIds.add(id);
|
|
90
|
+
return id;
|
|
91
|
+
}
|
|
42
92
|
private undoHandlers: {
|
|
43
93
|
[K in HistoryEntry["type"]]: (entry: Extract<HistoryEntry, { type: K }>) => {
|
|
44
94
|
success: boolean;
|
|
@@ -47,68 +97,138 @@ export class QuestLog {
|
|
|
47
97
|
} = {
|
|
48
98
|
[QUEST_ACTIONS.add]: (entry) => {
|
|
49
99
|
this.quests = this.quests.filter((q) => q.id !== entry.id);
|
|
100
|
+
this.subQuests = this.subQuests.filter((q) => q.id !== entry.id);
|
|
101
|
+
this.usedIds.delete(entry.id);
|
|
50
102
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return { success: true, message: `Reverted add quest
|
|
103
|
+
logger.debug("quests:state", "revert-add", {
|
|
104
|
+
id: entry.id,
|
|
105
|
+
total: this.quests.length + this.subQuests.length,
|
|
106
|
+
});
|
|
107
|
+
return { success: true, message: `Reverted add quest [${entry.id}]` };
|
|
56
108
|
},
|
|
57
109
|
[QUEST_ACTIONS.toggle]: (entry) => {
|
|
58
|
-
const quest = this.
|
|
110
|
+
const quest = this.findById(entry.id);
|
|
59
111
|
|
|
60
112
|
if (quest) {
|
|
61
113
|
quest.done = !quest.done;
|
|
62
114
|
logger.debug("quests:state", "revert-toggle", { id: entry.id, done: quest.done });
|
|
63
|
-
return { success: true, message: `Reverted toggle for quest
|
|
115
|
+
return { success: true, message: `Reverted toggle for quest [${entry.id}]` };
|
|
64
116
|
}
|
|
65
117
|
|
|
66
118
|
logger.debug("quests:state", "revert-toggle-not-found", { id: entry.id });
|
|
67
|
-
return { success: false, message: `Quest
|
|
119
|
+
return { success: false, message: `Quest [${entry.id}] not found` };
|
|
68
120
|
},
|
|
69
121
|
[QUEST_ACTIONS.update]: (entry) => {
|
|
70
|
-
const quest = this.
|
|
122
|
+
const quest = this.findById(entry.id);
|
|
71
123
|
if (quest) {
|
|
72
124
|
quest.description = entry.previousDescription;
|
|
73
125
|
logger.debug("quests:state", "revert-update", { id: entry.id });
|
|
74
|
-
return { success: true, message: `Reverted update for quest
|
|
126
|
+
return { success: true, message: `Reverted update for quest [${entry.id}]` };
|
|
75
127
|
}
|
|
76
128
|
|
|
77
129
|
logger.debug("quests:state", "revert-update-not-found", { id: entry.id });
|
|
78
|
-
return { success: false, message: `Quest
|
|
130
|
+
return { success: false, message: `Quest [${entry.id}] not found` };
|
|
79
131
|
},
|
|
80
132
|
[QUEST_ACTIONS.delete]: (entry) => {
|
|
81
|
-
|
|
133
|
+
if (entry.isSubQuest) {
|
|
134
|
+
this.subQuests.splice(entry.index, 0, entry.quest as unknown as SubQuest);
|
|
135
|
+
} else {
|
|
136
|
+
this.quests.splice(entry.index, 0, entry.quest);
|
|
137
|
+
if (entry.cascadeDeletedSubs) {
|
|
138
|
+
for (const sub of entry.cascadeDeletedSubs) {
|
|
139
|
+
this.subQuests.push(sub);
|
|
140
|
+
this.usedIds.add(sub.id);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this.usedIds.add(entry.quest.id);
|
|
82
145
|
|
|
83
146
|
logger.debug("quests:state", "revert-delete", {
|
|
84
147
|
id: entry.quest.id,
|
|
85
148
|
index: entry.index,
|
|
86
|
-
total: this.quests.length,
|
|
149
|
+
total: this.quests.length + this.subQuests.length,
|
|
87
150
|
});
|
|
88
|
-
return { success: true, message: `Reverted delete for quest
|
|
151
|
+
return { success: true, message: `Reverted delete for quest [${entry.quest.id}]` };
|
|
89
152
|
},
|
|
90
153
|
[QUEST_ACTIONS.clear]: (entry) => {
|
|
154
|
+
if ("previousQuests" in entry) {
|
|
155
|
+
const restoredCount =
|
|
156
|
+
entry.previousQuests.length +
|
|
157
|
+
(entry.previousSubQuests?.length ?? 0) -
|
|
158
|
+
(this.quests.length + this.subQuests.length);
|
|
159
|
+
this.quests = [...entry.previousQuests];
|
|
160
|
+
this.subQuests = [...(entry.previousSubQuests ?? [])];
|
|
161
|
+
this.usedIds = new Set([...this.quests, ...this.subQuests].map((q) => q.id));
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
success: true,
|
|
165
|
+
message: `Reverted clear (${restoredCount} quests restored)`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
91
169
|
this.quests = [...entry.quests];
|
|
92
|
-
this.
|
|
170
|
+
this.subQuests = [...(entry.subQuests ?? [])];
|
|
171
|
+
this.usedIds = new Set([...this.quests, ...this.subQuests].map((q) => q.id));
|
|
93
172
|
|
|
94
|
-
logger.debug("quests:state", "revert-clear", { count: entry.quests.length });
|
|
95
173
|
return { success: true, message: `Reverted clear (${entry.quests.length} quests restored)` };
|
|
96
174
|
},
|
|
175
|
+
[QUEST_ACTIONS.reorder]: (entry) => {
|
|
176
|
+
const currentIndex = this.quests.indexOf(entry.quest);
|
|
177
|
+
if (currentIndex === -1) return { success: false, message: "Reordered quest not found" };
|
|
178
|
+
|
|
179
|
+
this.quests.splice(currentIndex, 1);
|
|
180
|
+
this.quests.splice(entry.oldIndex, 0, entry.quest);
|
|
181
|
+
for (let i = 0; i < this.quests.length; i++) {
|
|
182
|
+
this.quests[i].id = entry.previousIds[i];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { success: true, message: `Reverted reorder for quest [${entry.quest.id}]` };
|
|
186
|
+
},
|
|
97
187
|
};
|
|
98
188
|
|
|
189
|
+
/*
|
|
190
|
+
* Returns all quests including sub-quests, inserted in order
|
|
191
|
+
* sub-quests are returned immediately after their parent quest
|
|
192
|
+
*/
|
|
99
193
|
getAll(): Quest[] {
|
|
194
|
+
const result: Quest[] = [];
|
|
195
|
+
for (const q of this.quests) {
|
|
196
|
+
result.push(q);
|
|
197
|
+
result.push(...this.subQuests.filter((sq) => sq.parentId === q.id));
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/*
|
|
203
|
+
* Returns only top-level quests. Sub-quests should be accessed via `getSubQuests()`.
|
|
204
|
+
*/
|
|
205
|
+
getQuests(): Quest[] {
|
|
100
206
|
return [...this.quests];
|
|
101
207
|
}
|
|
102
208
|
|
|
103
|
-
|
|
104
|
-
|
|
209
|
+
/*
|
|
210
|
+
* Returns sub-quests for a given parent quest ID. Sub-quests are not included in `getQuests()`.
|
|
211
|
+
*/
|
|
212
|
+
getSubQuests(parentId: string): SubQuest[] {
|
|
213
|
+
return this.subQuests.filter((sq) => sq.parentId === parentId);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getParentQuest(subQuest: SubQuest): Quest | undefined {
|
|
217
|
+
return this.quests.find((q) => q.id === subQuest.parentId);
|
|
105
218
|
}
|
|
106
219
|
|
|
107
|
-
|
|
220
|
+
private findById(id: string): Quest | SubQuest | undefined {
|
|
221
|
+
return this.quests.find((q) => q.id === id) ?? this.subQuests.find((sq) => sq.id === id);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getUsedIds(): string[] {
|
|
225
|
+
return Array.from(this.usedIds);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
add(description: string): Quest {
|
|
108
229
|
const quest: Quest = {
|
|
109
|
-
id: this.
|
|
230
|
+
id: this.generateId(),
|
|
110
231
|
description,
|
|
111
|
-
additionalContext,
|
|
112
232
|
done: false,
|
|
113
233
|
createdAt: Date.now(),
|
|
114
234
|
};
|
|
@@ -124,26 +244,63 @@ export class QuestLog {
|
|
|
124
244
|
return quest;
|
|
125
245
|
}
|
|
126
246
|
|
|
127
|
-
|
|
128
|
-
|
|
247
|
+
addSubQuest(description: string, parentId: string): SubQuest {
|
|
248
|
+
if (this.subQuests.some((sq) => sq.id === parentId)) {
|
|
249
|
+
throw new Error(formatSubQuestCannotHaveSubQuests(parentId));
|
|
250
|
+
}
|
|
251
|
+
const parent = this.quests.find((q) => q.id === parentId);
|
|
252
|
+
if (!parent) {
|
|
253
|
+
throw new Error(`Parent quest [${parentId}] not found`);
|
|
254
|
+
}
|
|
255
|
+
if (parent.done) {
|
|
256
|
+
throw new Error(`Cannot add sub-quest to completed parent quest [${parentId}]`);
|
|
257
|
+
}
|
|
258
|
+
const subQuest: SubQuest = {
|
|
259
|
+
id: this.generateId(),
|
|
260
|
+
description,
|
|
261
|
+
done: false,
|
|
262
|
+
createdAt: Date.now(),
|
|
263
|
+
parentId,
|
|
264
|
+
};
|
|
265
|
+
this.subQuests.push(subQuest);
|
|
266
|
+
this.history.push({ type: QUEST_ACTIONS.add, id: subQuest.id, parentId });
|
|
267
|
+
logger.debug("quests:state", "add-subquest", {
|
|
268
|
+
id: subQuest.id,
|
|
269
|
+
parentId,
|
|
270
|
+
description,
|
|
271
|
+
totalSubQuests: this.subQuests.length,
|
|
272
|
+
});
|
|
273
|
+
return subQuest;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
toggle(id: string): Quest | SubQuest | null | undefined {
|
|
277
|
+
const quest = this.findById(id);
|
|
129
278
|
if (!quest) {
|
|
130
279
|
logger.debug("quests:state", "toggle-not-found", { id });
|
|
131
280
|
return undefined;
|
|
132
281
|
}
|
|
133
282
|
|
|
283
|
+
if (!quest.done && !("parentId" in quest)) {
|
|
284
|
+
const subs = this.getSubQuests(id);
|
|
285
|
+
if (subs.some((q) => !q.done)) {
|
|
286
|
+
logger.debug("quests:state", "toggle-blocked-subquests", { id });
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
134
291
|
quest.done = !quest.done;
|
|
135
292
|
this.history.push({ type: QUEST_ACTIONS.toggle, id });
|
|
136
293
|
|
|
137
294
|
logger.debug("quests:state", QUEST_ACTIONS.toggle, {
|
|
138
295
|
id,
|
|
139
296
|
done: quest.done,
|
|
140
|
-
total: this.quests.length,
|
|
297
|
+
total: this.quests.length + this.subQuests.length,
|
|
141
298
|
});
|
|
142
299
|
return quest;
|
|
143
300
|
}
|
|
144
301
|
|
|
145
|
-
update(id:
|
|
146
|
-
const quest = this.
|
|
302
|
+
update(id: string, description: string): Quest | SubQuest | undefined {
|
|
303
|
+
const quest = this.findById(id);
|
|
147
304
|
if (!quest) {
|
|
148
305
|
logger.debug("quests:state", "update-not-found", { id });
|
|
149
306
|
return undefined;
|
|
@@ -155,33 +312,127 @@ export class QuestLog {
|
|
|
155
312
|
logger.debug("quests:state", QUEST_ACTIONS.update, {
|
|
156
313
|
id,
|
|
157
314
|
description,
|
|
158
|
-
total: this.quests.length,
|
|
315
|
+
total: this.quests.length + this.subQuests.length,
|
|
159
316
|
});
|
|
160
317
|
return quest;
|
|
161
318
|
}
|
|
162
319
|
|
|
163
|
-
delete(id:
|
|
320
|
+
delete(id: string): Quest | SubQuest | null | undefined {
|
|
164
321
|
const index = this.quests.findIndex((q) => q.id === id);
|
|
165
|
-
if (index
|
|
166
|
-
|
|
322
|
+
if (index !== -1) {
|
|
323
|
+
const subs = this.getSubQuests(id);
|
|
324
|
+
if (subs.some((q) => !q.done)) {
|
|
325
|
+
logger.debug("quests:state", "delete-blocked-subquests", { id });
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const [quest] = this.quests.splice(index, 1);
|
|
329
|
+
this.usedIds.delete(quest.id);
|
|
330
|
+
const cascadeDeletedSubs = this.subQuests.filter((sq) => sq.parentId === id);
|
|
331
|
+
this.history.push({ type: QUEST_ACTIONS.delete, quest, index, cascadeDeletedSubs });
|
|
332
|
+
this.subQuests = this.subQuests.filter((sq) => {
|
|
333
|
+
if (sq.parentId === id) {
|
|
334
|
+
this.usedIds.delete(sq.id);
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
return true;
|
|
338
|
+
});
|
|
339
|
+
logger.debug("quests:state", QUEST_ACTIONS.delete, {
|
|
340
|
+
id,
|
|
341
|
+
index,
|
|
342
|
+
total: this.quests.length + this.subQuests.length,
|
|
343
|
+
});
|
|
344
|
+
return quest;
|
|
345
|
+
}
|
|
346
|
+
const subIndex = this.subQuests.findIndex((sq) => sq.id === id);
|
|
347
|
+
if (subIndex !== -1) {
|
|
348
|
+
const [quest] = this.subQuests.splice(subIndex, 1);
|
|
349
|
+
this.usedIds.delete(quest.id);
|
|
350
|
+
this.history.push({ type: QUEST_ACTIONS.delete, quest, index: subIndex, isSubQuest: true });
|
|
351
|
+
logger.debug("quests:state", QUEST_ACTIONS.delete, {
|
|
352
|
+
id,
|
|
353
|
+
index: subIndex,
|
|
354
|
+
total: this.quests.length + this.subQuests.length,
|
|
355
|
+
});
|
|
356
|
+
return quest;
|
|
357
|
+
}
|
|
358
|
+
logger.debug("quests:state", "delete-not-found", { id });
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
clear(all = false): number {
|
|
363
|
+
if (!all) {
|
|
364
|
+
const doneParentIds = new Set(this.quests.filter((q) => q.done).map((q) => q.id));
|
|
365
|
+
const done = this.quests.filter((q) => q.done);
|
|
366
|
+
const removedSubs = this.subQuests.filter((sq) => sq.done || doneParentIds.has(sq.parentId));
|
|
367
|
+
const keptSubs = this.subQuests.filter((sq) => !sq.done && !doneParentIds.has(sq.parentId));
|
|
368
|
+
const previousQuests = [...this.quests];
|
|
369
|
+
const previousSubQuests = [...this.subQuests];
|
|
370
|
+
for (const q of done) this.usedIds.delete(q.id);
|
|
371
|
+
for (const q of removedSubs) this.usedIds.delete(q.id);
|
|
372
|
+
this.quests = this.quests.filter((q) => !q.done);
|
|
373
|
+
this.subQuests = keptSubs;
|
|
374
|
+
this.history.push({
|
|
375
|
+
type: QUEST_ACTIONS.clear,
|
|
376
|
+
previousQuests,
|
|
377
|
+
previousSubQuests,
|
|
378
|
+
all: false,
|
|
379
|
+
});
|
|
380
|
+
const count = done.length + removedSubs.length;
|
|
381
|
+
logger.debug("quests:state", QUEST_ACTIONS.clear, { count, all });
|
|
382
|
+
return count;
|
|
383
|
+
}
|
|
384
|
+
const count = this.quests.length + this.subQuests.length;
|
|
385
|
+
this.history.push({
|
|
386
|
+
type: QUEST_ACTIONS.clear,
|
|
387
|
+
quests: [...this.quests],
|
|
388
|
+
subQuests: [...this.subQuests],
|
|
389
|
+
all: true,
|
|
390
|
+
});
|
|
391
|
+
this.quests = [];
|
|
392
|
+
this.subQuests = [];
|
|
393
|
+
this.usedIds.clear();
|
|
394
|
+
logger.debug("quests:state", QUEST_ACTIONS.clear, { count, all });
|
|
395
|
+
return count;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
reorder(id: string, targetId: string): Quest | undefined {
|
|
399
|
+
if (this.subQuests.some((sq) => sq.id === id || sq.id === targetId)) {
|
|
400
|
+
logger.debug("quests:state", "reorder-subquest-rejected", { id, targetId });
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
const idx = this.quests.findIndex((q) => q.id === id);
|
|
404
|
+
if (idx === -1) {
|
|
405
|
+
logger.debug("quests:state", "reorder-not-found", { id });
|
|
167
406
|
return undefined;
|
|
168
407
|
}
|
|
169
408
|
|
|
170
|
-
const
|
|
171
|
-
|
|
409
|
+
const targetIdx = this.quests.findIndex((q) => q.id === targetId);
|
|
410
|
+
if (targetIdx === -1) {
|
|
411
|
+
logger.debug("quests:state", "reorder-target-not-found", { id, targetId });
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
172
414
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
415
|
+
if (idx === targetIdx) {
|
|
416
|
+
return this.quests[idx];
|
|
417
|
+
}
|
|
176
418
|
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
this.history.push({ type: QUEST_ACTIONS.clear, quests: [...this.quests], nextId: this.nextId });
|
|
180
|
-
this.quests = [];
|
|
181
|
-
this.nextId = 1;
|
|
419
|
+
const previousIds = this.quests.map((q) => q.id);
|
|
420
|
+
const [quest] = this.quests.splice(idx, 1);
|
|
182
421
|
|
|
183
|
-
|
|
184
|
-
|
|
422
|
+
let insertIndex = targetIdx;
|
|
423
|
+
if (idx < targetIdx) {
|
|
424
|
+
insertIndex = targetIdx - 1;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
this.quests.splice(insertIndex, 0, quest);
|
|
428
|
+
this.history.push({ type: QUEST_ACTIONS.reorder, quest, oldIndex: idx, previousIds, targetId });
|
|
429
|
+
logger.debug("quests:state", QUEST_ACTIONS.reorder, {
|
|
430
|
+
id: quest.id,
|
|
431
|
+
targetId,
|
|
432
|
+
total: this.quests.length + this.subQuests.length,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
return quest;
|
|
185
436
|
}
|
|
186
437
|
|
|
187
438
|
revert(): QuestOperationResult {
|
|
@@ -216,13 +467,53 @@ export class QuestLog {
|
|
|
216
467
|
message: "Error: all descriptions in a batch must be non-empty",
|
|
217
468
|
};
|
|
218
469
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
470
|
+
if (action.parentId) {
|
|
471
|
+
if (this.subQuests.some((sq) => sq.id === action.parentId)) {
|
|
472
|
+
return {
|
|
473
|
+
success: false,
|
|
474
|
+
message: formatSubQuestCannotHaveSubQuests(action.parentId),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const parent = this.quests.find((q) => q.id === action.parentId);
|
|
478
|
+
if (!parent) {
|
|
479
|
+
return { success: false, message: formatNotFound(action.parentId) };
|
|
480
|
+
}
|
|
481
|
+
if (parent.done) {
|
|
482
|
+
return {
|
|
483
|
+
success: false,
|
|
484
|
+
message: `Error: cannot add sub-quest to completed parent quest [${action.parentId}]`,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (action.descriptions.length === 1) {
|
|
489
|
+
try {
|
|
490
|
+
const [firstDesc] = action.descriptions;
|
|
491
|
+
const q = action.parentId
|
|
492
|
+
? this.addSubQuest(firstDesc, action.parentId)
|
|
493
|
+
: this.add(firstDesc);
|
|
494
|
+
|
|
495
|
+
return { success: true, message: formatAddResult(q) };
|
|
496
|
+
} catch (err) {
|
|
497
|
+
return {
|
|
498
|
+
success: false,
|
|
499
|
+
message: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
const added: { id: string; description: string }[] = [];
|
|
505
|
+
for (const desc of action.descriptions) {
|
|
506
|
+
const q = action.parentId ? this.addSubQuest(desc, action.parentId) : this.add(desc);
|
|
507
|
+
added.push({ id: q.id, description: q.description });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return { success: true, message: formatBatchAddResult(added) };
|
|
511
|
+
} catch (err) {
|
|
512
|
+
return {
|
|
513
|
+
success: false,
|
|
514
|
+
message: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
515
|
+
};
|
|
223
516
|
}
|
|
224
|
-
|
|
225
|
-
return { success: true, message: formatBatchAddResult(added) };
|
|
226
517
|
}
|
|
227
518
|
return {
|
|
228
519
|
success: false,
|
|
@@ -239,9 +530,12 @@ export class QuestLog {
|
|
|
239
530
|
}
|
|
240
531
|
|
|
241
532
|
const q = this.toggle(action.id);
|
|
242
|
-
if (
|
|
533
|
+
if (q === undefined) {
|
|
243
534
|
return { success: false, message: formatNotFound(action.id) };
|
|
244
535
|
}
|
|
536
|
+
if (q === null) {
|
|
537
|
+
return { success: false, message: formatBlockedBySubQuests(action.id) };
|
|
538
|
+
}
|
|
245
539
|
|
|
246
540
|
return { success: true, message: formatToggleResult(action.id, q.done), quest: q };
|
|
247
541
|
}
|
|
@@ -250,7 +544,7 @@ export class QuestLog {
|
|
|
250
544
|
return { success: false, message: "Error: id is required for update action" };
|
|
251
545
|
}
|
|
252
546
|
|
|
253
|
-
if (!action.description) {
|
|
547
|
+
if (!action.description || action.description.trim().length === 0) {
|
|
254
548
|
return { success: false, message: "Error: description is required for update action" };
|
|
255
549
|
}
|
|
256
550
|
|
|
@@ -267,15 +561,34 @@ export class QuestLog {
|
|
|
267
561
|
}
|
|
268
562
|
|
|
269
563
|
const q = this.delete(action.id);
|
|
270
|
-
if (
|
|
564
|
+
if (q === undefined) {
|
|
271
565
|
return { success: false, message: formatNotFound(action.id) };
|
|
272
566
|
}
|
|
567
|
+
if (q === null) {
|
|
568
|
+
return { success: false, message: formatBlockedBySubQuests(action.id) };
|
|
569
|
+
}
|
|
273
570
|
|
|
274
571
|
return { success: true, message: formatDeleteResult(q), quest: q };
|
|
275
572
|
}
|
|
276
573
|
case QUEST_ACTIONS.clear: {
|
|
277
|
-
const count = this.clear();
|
|
278
|
-
|
|
574
|
+
const count = this.clear(action.all);
|
|
575
|
+
const message = action.all
|
|
576
|
+
? `Cleared ${count} quests`
|
|
577
|
+
: `Cleared ${count} completed quests`;
|
|
578
|
+
|
|
579
|
+
return { success: true, message };
|
|
580
|
+
}
|
|
581
|
+
case QUEST_ACTIONS.reorder: {
|
|
582
|
+
if (action.id === undefined)
|
|
583
|
+
return { success: false, message: "Error: id is required for reorder action" };
|
|
584
|
+
|
|
585
|
+
if (action.targetId === undefined)
|
|
586
|
+
return { success: false, message: "Error: targetId is required for reorder action" };
|
|
587
|
+
|
|
588
|
+
const q = this.reorder(action.id, action.targetId);
|
|
589
|
+
if (!q) return { success: false, message: "Quest not found or is a sub-quest" };
|
|
590
|
+
|
|
591
|
+
return { success: true, message: `Reordered quest [${q.id}]: ${q.description}`, quest: q };
|
|
279
592
|
}
|
|
280
593
|
case QUEST_ACTIONS.revert: {
|
|
281
594
|
return this.revert();
|
|
@@ -305,15 +618,23 @@ export class QuestLog {
|
|
|
305
618
|
|
|
306
619
|
if (lastState) {
|
|
307
620
|
const quests = lastState.quests;
|
|
308
|
-
const nextId = lastState.nextId;
|
|
309
621
|
const questCount = Array.isArray(quests) ? quests.length : 0;
|
|
310
622
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
623
|
+
if (Array.isArray(quests)) {
|
|
624
|
+
this.quests = (quests as Quest[]).filter((q) => !("parentId" in q && q.parentId));
|
|
625
|
+
this.subQuests = (quests as Quest[]).filter(
|
|
626
|
+
(q) => "parentId" in q && q.parentId,
|
|
627
|
+
) as unknown as SubQuest[];
|
|
628
|
+
} else {
|
|
629
|
+
this.quests = [];
|
|
630
|
+
this.subQuests = [];
|
|
631
|
+
}
|
|
632
|
+
this.usedIds = new Set([...this.quests, ...this.subQuests].map((q) => q.id));
|
|
633
|
+
logger.debug("quests:state", "reconstruct", { toolResults, questCount });
|
|
314
634
|
} else {
|
|
315
635
|
this.quests = [];
|
|
316
|
-
this.
|
|
636
|
+
this.subQuests = [];
|
|
637
|
+
this.usedIds.clear();
|
|
317
638
|
logger.debug("quests:state", "reconstruct-empty", { toolResults });
|
|
318
639
|
}
|
|
319
640
|
|
|
@@ -330,7 +651,7 @@ export function makeToolResult(
|
|
|
330
651
|
content: [{ type: "text", text }],
|
|
331
652
|
details: {
|
|
332
653
|
quests: questLog.getAll(),
|
|
333
|
-
|
|
654
|
+
usedIds: questLog.getUsedIds(),
|
|
334
655
|
displayQuests,
|
|
335
656
|
},
|
|
336
657
|
};
|