pi-quests 0.3.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 +8 -0
- package/README.md +20 -1
- package/package.json +1 -1
- package/src/commands/handler.ts +28 -11
- package/src/commands/parse-args.ts +42 -28
- package/src/config.ts +148 -0
- package/src/index.ts +52 -21
- package/src/prompts.ts +5 -2
- package/src/quest/dataplane.ts +322 -94
- package/src/quest/formatters.ts +30 -16
- package/src/quest/tracker.ts +34 -25
- package/src/quest/types.ts +5 -2
- package/src/renderers/commands.ts +46 -24
- package/src/renderers/quests.ts +45 -0
- package/src/renderers/tools.ts +80 -31
- package/src/tools/handler.ts +15 -8
- package/src/tools/params.ts +44 -30
package/src/quest/dataplane.ts
CHANGED
|
@@ -1,37 +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,
|
|
7
|
+
formatBlockedBySubQuests,
|
|
5
8
|
formatDeleteResult,
|
|
6
9
|
formatNotFound,
|
|
7
10
|
formatQuestList,
|
|
11
|
+
formatSubQuestCannotHaveSubQuests,
|
|
8
12
|
formatToggleResult,
|
|
9
13
|
formatUpdateResult,
|
|
10
14
|
} from "./formatters.js";
|
|
11
|
-
import { QUEST_ACTIONS, type Quest } from "./types.js";
|
|
15
|
+
import { QUEST_ACTIONS, type Quest, type SubQuest } from "./types.js";
|
|
12
16
|
|
|
13
17
|
export type HistoryEntry =
|
|
14
|
-
| { type: typeof QUEST_ACTIONS.add; id:
|
|
15
|
-
| { type: typeof QUEST_ACTIONS.toggle; id:
|
|
16
|
-
| { type: typeof QUEST_ACTIONS.update; id:
|
|
17
|
-
| {
|
|
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
|
+
}
|
|
18
28
|
| {
|
|
19
29
|
type: typeof QUEST_ACTIONS.clear;
|
|
20
30
|
previousQuests: Quest[];
|
|
21
|
-
|
|
31
|
+
previousSubQuests?: SubQuest[];
|
|
22
32
|
all: false;
|
|
23
33
|
}
|
|
24
|
-
| { type: typeof QUEST_ACTIONS.clear; quests: Quest[];
|
|
25
|
-
| {
|
|
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
|
+
};
|
|
26
42
|
|
|
27
43
|
export type QuestAction =
|
|
28
|
-
| { type: typeof QUEST_ACTIONS.add; descriptions?: string[] }
|
|
44
|
+
| { type: typeof QUEST_ACTIONS.add; descriptions?: string[]; parentId?: string }
|
|
29
45
|
| { type: typeof QUEST_ACTIONS.list }
|
|
30
|
-
| { type: typeof QUEST_ACTIONS.toggle; id?:
|
|
31
|
-
| { type: typeof QUEST_ACTIONS.update; id?:
|
|
32
|
-
| { type: typeof QUEST_ACTIONS.delete; id?:
|
|
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 }
|
|
33
49
|
| { type: typeof QUEST_ACTIONS.clear; all?: boolean }
|
|
34
|
-
| { type: typeof QUEST_ACTIONS.reorder; id?:
|
|
50
|
+
| { type: typeof QUEST_ACTIONS.reorder; id?: string; targetId?: string }
|
|
35
51
|
| { type: typeof QUEST_ACTIONS.revert };
|
|
36
52
|
|
|
37
53
|
export type QuestOperationResult = { success: boolean; message: string; quest?: Quest };
|
|
@@ -44,8 +60,35 @@ export type QuestOperationResult = { success: boolean; message: string; quest?:
|
|
|
44
60
|
*/
|
|
45
61
|
export class QuestLog {
|
|
46
62
|
private quests: Quest[] = [];
|
|
47
|
-
private
|
|
63
|
+
private subQuests: SubQuest[] = [];
|
|
64
|
+
private usedIds: Set<string> = new Set();
|
|
48
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
|
+
}
|
|
49
92
|
private undoHandlers: {
|
|
50
93
|
[K in HistoryEntry["type"]]: (entry: Extract<HistoryEntry, { type: K }>) => {
|
|
51
94
|
success: boolean;
|
|
@@ -54,51 +97,68 @@ export class QuestLog {
|
|
|
54
97
|
} = {
|
|
55
98
|
[QUEST_ACTIONS.add]: (entry) => {
|
|
56
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);
|
|
57
102
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
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}]` };
|
|
63
108
|
},
|
|
64
109
|
[QUEST_ACTIONS.toggle]: (entry) => {
|
|
65
|
-
const quest = this.
|
|
110
|
+
const quest = this.findById(entry.id);
|
|
66
111
|
|
|
67
112
|
if (quest) {
|
|
68
113
|
quest.done = !quest.done;
|
|
69
114
|
logger.debug("quests:state", "revert-toggle", { id: entry.id, done: quest.done });
|
|
70
|
-
return { success: true, message: `Reverted toggle for quest
|
|
115
|
+
return { success: true, message: `Reverted toggle for quest [${entry.id}]` };
|
|
71
116
|
}
|
|
72
117
|
|
|
73
118
|
logger.debug("quests:state", "revert-toggle-not-found", { id: entry.id });
|
|
74
|
-
return { success: false, message: `Quest
|
|
119
|
+
return { success: false, message: `Quest [${entry.id}] not found` };
|
|
75
120
|
},
|
|
76
121
|
[QUEST_ACTIONS.update]: (entry) => {
|
|
77
|
-
const quest = this.
|
|
122
|
+
const quest = this.findById(entry.id);
|
|
78
123
|
if (quest) {
|
|
79
124
|
quest.description = entry.previousDescription;
|
|
80
125
|
logger.debug("quests:state", "revert-update", { id: entry.id });
|
|
81
|
-
return { success: true, message: `Reverted update for quest
|
|
126
|
+
return { success: true, message: `Reverted update for quest [${entry.id}]` };
|
|
82
127
|
}
|
|
83
128
|
|
|
84
129
|
logger.debug("quests:state", "revert-update-not-found", { id: entry.id });
|
|
85
|
-
return { success: false, message: `Quest
|
|
130
|
+
return { success: false, message: `Quest [${entry.id}] not found` };
|
|
86
131
|
},
|
|
87
132
|
[QUEST_ACTIONS.delete]: (entry) => {
|
|
88
|
-
|
|
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);
|
|
89
145
|
|
|
90
146
|
logger.debug("quests:state", "revert-delete", {
|
|
91
147
|
id: entry.quest.id,
|
|
92
148
|
index: entry.index,
|
|
93
|
-
total: this.quests.length,
|
|
149
|
+
total: this.quests.length + this.subQuests.length,
|
|
94
150
|
});
|
|
95
|
-
return { success: true, message: `Reverted delete for quest
|
|
151
|
+
return { success: true, message: `Reverted delete for quest [${entry.quest.id}]` };
|
|
96
152
|
},
|
|
97
153
|
[QUEST_ACTIONS.clear]: (entry) => {
|
|
98
154
|
if ("previousQuests" in entry) {
|
|
99
|
-
const restoredCount =
|
|
155
|
+
const restoredCount =
|
|
156
|
+
entry.previousQuests.length +
|
|
157
|
+
(entry.previousSubQuests?.length ?? 0) -
|
|
158
|
+
(this.quests.length + this.subQuests.length);
|
|
100
159
|
this.quests = [...entry.previousQuests];
|
|
101
|
-
this.
|
|
160
|
+
this.subQuests = [...(entry.previousSubQuests ?? [])];
|
|
161
|
+
this.usedIds = new Set([...this.quests, ...this.subQuests].map((q) => q.id));
|
|
102
162
|
|
|
103
163
|
return {
|
|
104
164
|
success: true,
|
|
@@ -107,7 +167,8 @@ export class QuestLog {
|
|
|
107
167
|
}
|
|
108
168
|
|
|
109
169
|
this.quests = [...entry.quests];
|
|
110
|
-
this.
|
|
170
|
+
this.subQuests = [...(entry.subQuests ?? [])];
|
|
171
|
+
this.usedIds = new Set([...this.quests, ...this.subQuests].map((q) => q.id));
|
|
111
172
|
|
|
112
173
|
return { success: true, message: `Reverted clear (${entry.quests.length} quests restored)` };
|
|
113
174
|
},
|
|
@@ -121,24 +182,53 @@ export class QuestLog {
|
|
|
121
182
|
this.quests[i].id = entry.previousIds[i];
|
|
122
183
|
}
|
|
123
184
|
|
|
124
|
-
|
|
125
|
-
return { success: true, message: `Reverted reorder for quest #${entry.quest.id}` };
|
|
185
|
+
return { success: true, message: `Reverted reorder for quest [${entry.quest.id}]` };
|
|
126
186
|
},
|
|
127
187
|
};
|
|
128
188
|
|
|
189
|
+
/*
|
|
190
|
+
* Returns all quests including sub-quests, inserted in order
|
|
191
|
+
* sub-quests are returned immediately after their parent quest
|
|
192
|
+
*/
|
|
129
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[] {
|
|
130
206
|
return [...this.quests];
|
|
131
207
|
}
|
|
132
208
|
|
|
133
|
-
|
|
134
|
-
|
|
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);
|
|
135
218
|
}
|
|
136
219
|
|
|
137
|
-
|
|
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 {
|
|
138
229
|
const quest: Quest = {
|
|
139
|
-
id: this.
|
|
230
|
+
id: this.generateId(),
|
|
140
231
|
description,
|
|
141
|
-
additionalContext,
|
|
142
232
|
done: false,
|
|
143
233
|
createdAt: Date.now(),
|
|
144
234
|
};
|
|
@@ -154,26 +244,63 @@ export class QuestLog {
|
|
|
154
244
|
return quest;
|
|
155
245
|
}
|
|
156
246
|
|
|
157
|
-
|
|
158
|
-
|
|
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);
|
|
159
278
|
if (!quest) {
|
|
160
279
|
logger.debug("quests:state", "toggle-not-found", { id });
|
|
161
280
|
return undefined;
|
|
162
281
|
}
|
|
163
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
|
+
|
|
164
291
|
quest.done = !quest.done;
|
|
165
292
|
this.history.push({ type: QUEST_ACTIONS.toggle, id });
|
|
166
293
|
|
|
167
294
|
logger.debug("quests:state", QUEST_ACTIONS.toggle, {
|
|
168
295
|
id,
|
|
169
296
|
done: quest.done,
|
|
170
|
-
total: this.quests.length,
|
|
297
|
+
total: this.quests.length + this.subQuests.length,
|
|
171
298
|
});
|
|
172
299
|
return quest;
|
|
173
300
|
}
|
|
174
301
|
|
|
175
|
-
update(id:
|
|
176
|
-
const quest = this.
|
|
302
|
+
update(id: string, description: string): Quest | SubQuest | undefined {
|
|
303
|
+
const quest = this.findById(id);
|
|
177
304
|
if (!quest) {
|
|
178
305
|
logger.debug("quests:state", "update-not-found", { id });
|
|
179
306
|
return undefined;
|
|
@@ -185,77 +312,124 @@ export class QuestLog {
|
|
|
185
312
|
logger.debug("quests:state", QUEST_ACTIONS.update, {
|
|
186
313
|
id,
|
|
187
314
|
description,
|
|
188
|
-
total: this.quests.length,
|
|
315
|
+
total: this.quests.length + this.subQuests.length,
|
|
189
316
|
});
|
|
190
317
|
return quest;
|
|
191
318
|
}
|
|
192
319
|
|
|
193
|
-
delete(id:
|
|
320
|
+
delete(id: string): Quest | SubQuest | null | undefined {
|
|
194
321
|
const index = this.quests.findIndex((q) => q.id === id);
|
|
195
|
-
if (index
|
|
196
|
-
|
|
197
|
-
|
|
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;
|
|
198
345
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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;
|
|
205
360
|
}
|
|
206
361
|
|
|
207
362
|
clear(all = false): number {
|
|
208
363
|
if (!all) {
|
|
364
|
+
const doneParentIds = new Set(this.quests.filter((q) => q.done).map((q) => q.id));
|
|
209
365
|
const done = this.quests.filter((q) => q.done);
|
|
210
|
-
const
|
|
211
|
-
const
|
|
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);
|
|
212
372
|
this.quests = this.quests.filter((q) => !q.done);
|
|
213
|
-
|
|
214
|
-
this.quests[i].id = i + 1;
|
|
215
|
-
}
|
|
216
|
-
this.nextId = this.quests.length + 1;
|
|
373
|
+
this.subQuests = keptSubs;
|
|
217
374
|
this.history.push({
|
|
218
375
|
type: QUEST_ACTIONS.clear,
|
|
219
376
|
previousQuests,
|
|
220
|
-
|
|
377
|
+
previousSubQuests,
|
|
221
378
|
all: false,
|
|
222
379
|
});
|
|
223
|
-
|
|
224
|
-
|
|
380
|
+
const count = done.length + removedSubs.length;
|
|
381
|
+
logger.debug("quests:state", QUEST_ACTIONS.clear, { count, all });
|
|
382
|
+
return count;
|
|
225
383
|
}
|
|
226
|
-
const count = this.quests.length;
|
|
384
|
+
const count = this.quests.length + this.subQuests.length;
|
|
227
385
|
this.history.push({
|
|
228
386
|
type: QUEST_ACTIONS.clear,
|
|
229
387
|
quests: [...this.quests],
|
|
230
|
-
|
|
388
|
+
subQuests: [...this.subQuests],
|
|
231
389
|
all: true,
|
|
232
390
|
});
|
|
233
391
|
this.quests = [];
|
|
234
|
-
this.
|
|
392
|
+
this.subQuests = [];
|
|
393
|
+
this.usedIds.clear();
|
|
235
394
|
logger.debug("quests:state", QUEST_ACTIONS.clear, { count, all });
|
|
236
395
|
return count;
|
|
237
396
|
}
|
|
238
397
|
|
|
239
|
-
reorder(id:
|
|
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
|
+
}
|
|
240
403
|
const idx = this.quests.findIndex((q) => q.id === id);
|
|
241
404
|
if (idx === -1) {
|
|
242
405
|
logger.debug("quests:state", "reorder-not-found", { id });
|
|
243
406
|
return undefined;
|
|
244
407
|
}
|
|
245
408
|
|
|
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
|
+
}
|
|
414
|
+
|
|
415
|
+
if (idx === targetIdx) {
|
|
416
|
+
return this.quests[idx];
|
|
417
|
+
}
|
|
418
|
+
|
|
246
419
|
const previousIds = this.quests.map((q) => q.id);
|
|
247
420
|
const [quest] = this.quests.splice(idx, 1);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
421
|
+
|
|
422
|
+
let insertIndex = targetIdx;
|
|
423
|
+
if (idx < targetIdx) {
|
|
424
|
+
insertIndex = targetIdx - 1;
|
|
251
425
|
}
|
|
252
426
|
|
|
253
|
-
this.
|
|
254
|
-
this.history.push({ type: QUEST_ACTIONS.reorder, quest, oldIndex: idx, previousIds });
|
|
427
|
+
this.quests.splice(insertIndex, 0, quest);
|
|
428
|
+
this.history.push({ type: QUEST_ACTIONS.reorder, quest, oldIndex: idx, previousIds, targetId });
|
|
255
429
|
logger.debug("quests:state", QUEST_ACTIONS.reorder, {
|
|
256
430
|
id: quest.id,
|
|
257
|
-
|
|
258
|
-
total: this.quests.length,
|
|
431
|
+
targetId,
|
|
432
|
+
total: this.quests.length + this.subQuests.length,
|
|
259
433
|
});
|
|
260
434
|
|
|
261
435
|
return quest;
|
|
@@ -293,13 +467,53 @@ export class QuestLog {
|
|
|
293
467
|
message: "Error: all descriptions in a batch must be non-empty",
|
|
294
468
|
};
|
|
295
469
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
};
|
|
300
516
|
}
|
|
301
|
-
|
|
302
|
-
return { success: true, message: formatBatchAddResult(added) };
|
|
303
517
|
}
|
|
304
518
|
return {
|
|
305
519
|
success: false,
|
|
@@ -316,9 +530,12 @@ export class QuestLog {
|
|
|
316
530
|
}
|
|
317
531
|
|
|
318
532
|
const q = this.toggle(action.id);
|
|
319
|
-
if (
|
|
533
|
+
if (q === undefined) {
|
|
320
534
|
return { success: false, message: formatNotFound(action.id) };
|
|
321
535
|
}
|
|
536
|
+
if (q === null) {
|
|
537
|
+
return { success: false, message: formatBlockedBySubQuests(action.id) };
|
|
538
|
+
}
|
|
322
539
|
|
|
323
540
|
return { success: true, message: formatToggleResult(action.id, q.done), quest: q };
|
|
324
541
|
}
|
|
@@ -327,7 +544,7 @@ export class QuestLog {
|
|
|
327
544
|
return { success: false, message: "Error: id is required for update action" };
|
|
328
545
|
}
|
|
329
546
|
|
|
330
|
-
if (!action.description) {
|
|
547
|
+
if (!action.description || action.description.trim().length === 0) {
|
|
331
548
|
return { success: false, message: "Error: description is required for update action" };
|
|
332
549
|
}
|
|
333
550
|
|
|
@@ -344,9 +561,12 @@ export class QuestLog {
|
|
|
344
561
|
}
|
|
345
562
|
|
|
346
563
|
const q = this.delete(action.id);
|
|
347
|
-
if (
|
|
564
|
+
if (q === undefined) {
|
|
348
565
|
return { success: false, message: formatNotFound(action.id) };
|
|
349
566
|
}
|
|
567
|
+
if (q === null) {
|
|
568
|
+
return { success: false, message: formatBlockedBySubQuests(action.id) };
|
|
569
|
+
}
|
|
350
570
|
|
|
351
571
|
return { success: true, message: formatDeleteResult(q), quest: q };
|
|
352
572
|
}
|
|
@@ -362,13 +582,13 @@ export class QuestLog {
|
|
|
362
582
|
if (action.id === undefined)
|
|
363
583
|
return { success: false, message: "Error: id is required for reorder action" };
|
|
364
584
|
|
|
365
|
-
if (action.
|
|
366
|
-
return { success: false, message: "Error:
|
|
585
|
+
if (action.targetId === undefined)
|
|
586
|
+
return { success: false, message: "Error: targetId is required for reorder action" };
|
|
367
587
|
|
|
368
|
-
const q = this.reorder(action.id, action.
|
|
369
|
-
if (!q) return { success: false, message:
|
|
588
|
+
const q = this.reorder(action.id, action.targetId);
|
|
589
|
+
if (!q) return { success: false, message: "Quest not found or is a sub-quest" };
|
|
370
590
|
|
|
371
|
-
return { success: true, message: `Reordered quest
|
|
591
|
+
return { success: true, message: `Reordered quest [${q.id}]: ${q.description}`, quest: q };
|
|
372
592
|
}
|
|
373
593
|
case QUEST_ACTIONS.revert: {
|
|
374
594
|
return this.revert();
|
|
@@ -398,15 +618,23 @@ export class QuestLog {
|
|
|
398
618
|
|
|
399
619
|
if (lastState) {
|
|
400
620
|
const quests = lastState.quests;
|
|
401
|
-
const nextId = lastState.nextId;
|
|
402
621
|
const questCount = Array.isArray(quests) ? quests.length : 0;
|
|
403
622
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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 });
|
|
407
634
|
} else {
|
|
408
635
|
this.quests = [];
|
|
409
|
-
this.
|
|
636
|
+
this.subQuests = [];
|
|
637
|
+
this.usedIds.clear();
|
|
410
638
|
logger.debug("quests:state", "reconstruct-empty", { toolResults });
|
|
411
639
|
}
|
|
412
640
|
|
|
@@ -423,7 +651,7 @@ export function makeToolResult(
|
|
|
423
651
|
content: [{ type: "text", text }],
|
|
424
652
|
details: {
|
|
425
653
|
quests: questLog.getAll(),
|
|
426
|
-
|
|
654
|
+
usedIds: questLog.getUsedIds(),
|
|
427
655
|
displayQuests,
|
|
428
656
|
},
|
|
429
657
|
};
|