pi-quests 0.6.1 → 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.
@@ -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 formatNothingToRevertError(): string {
104
- return `Nothing to revert. The history is empty because no mutating actions have been performed yet.`;
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 {
@@ -10,7 +10,8 @@ export const QUEST_ACTIONS = {
10
10
  delete: "delete",
11
11
  clear: "clear",
12
12
  reorder: "reorder",
13
- revert: "revert",
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.revert,
30
+ QUEST_ACTIONS.undo,
31
+ QUEST_ACTIONS.redo,
30
32
  QUEST_ACTIONS.reparent,
31
33
  QUEST_ACTIONS.rules,
32
34
  QUEST_ACTIONS.skill,
@@ -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.action === QUEST_ACTIONS.toggle ||
36
- args.action === QUEST_ACTIONS.update ||
37
- args.action === QUEST_ACTIONS.delete) &&
38
- args.id !== undefined
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 allQuests = details?.quests as Quest[] | undefined;
63
- const questsToRender = (details?.displayQuests ?? details?.quests) as Quest[] | undefined;
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) ||
@@ -1,17 +1,18 @@
1
- import type { AgentToolResult, ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import type { Static } from "@sinclair/typebox";
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.revert,
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
- const toolHandlers: {
24
- [K in QuestToolParams["action"]]: (
25
- questLog: QuestLog,
26
- params: QuestToolParams,
27
- toolCallId: string,
28
- ) => AgentToolResult<unknown>;
29
- } = {
30
- [QUEST_ACTIONS.add](questLog, params, toolCallId) {
31
- return runTool(questLog, toolCallId, {
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
- [QUEST_ACTIONS.split](questLog, params, toolCallId) {
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
- [QUEST_ACTIONS.add_step](questLog, params, toolCallId) {
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
- [QUEST_ACTIONS.list](questLog, _params, toolCallId) {
51
- return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.list });
52
- },
53
- [QUEST_ACTIONS.toggle](questLog, params, toolCallId) {
54
- return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.toggle, id: params.id });
55
- },
56
- [QUEST_ACTIONS.update](questLog, params, toolCallId) {
57
- return runTool(questLog, toolCallId, {
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
- [QUEST_ACTIONS.delete](questLog, params, toolCallId) {
64
- return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.delete, id: params.id });
65
- },
66
- [QUEST_ACTIONS.clear](questLog, params, toolCallId) {
67
- return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.clear, all: params.all });
68
- },
69
- [QUEST_ACTIONS.reorder](questLog, params, toolCallId) {
70
- return runTool(questLog, toolCallId, {
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
- [QUEST_ACTIONS.reparent](questLog, params, toolCallId) {
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
- [QUEST_ACTIONS.rules](questLog, _params, toolCallId) {
84
- return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.rules });
85
- },
86
- [QUEST_ACTIONS.skill](questLog, _params, toolCallId) {
87
- return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.skill });
88
- },
89
- [QUEST_ACTIONS.revert](questLog, _params, toolCallId) {
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
- ? [result.quest]
109
- : undefined;
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", { toolCallId, action: params.action, id: params.id });
120
- const handler = toolHandlers[params.action];
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
 
@@ -132,15 +134,43 @@ export function registerQuestTool(
132
134
  label: "Quest",
133
135
  description:
134
136
  "Manage the session quest log and retrieve the complete quest system documentation. " +
135
- "Use this VERY frequently to track tasks, plans, and progress. " +
136
137
  "When you need to understand quests, steps, rules, or best practices, use action: 'skill' or action: 'rules'.",
137
138
  promptSnippet:
138
- "Manage quests and steps, or retrieve quest rules and best practices via skill/rules",
139
+ "Manage quests and steps, or retrieve quest rules and best practices via skill/rules. " +
140
+ "Use this VERY frequently to track tasks, plans, and progress. ",
139
141
  promptGuidelines: [QUEST_PROMPT_GATE, ...QUEST_PROMPT_REMINDER],
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>,
150
+ renderResult: renderQuestResult(config),
151
+ });
152
+
153
+ pi.registerTool({
154
+ name: "learn_quests",
155
+ label: "Learn Quests",
156
+ description: "Learn about the quest system and how to use it effectively.",
157
+ promptSnippet: "Use this tool to learn about the quest system and how to use it effectively.",
158
+ promptGuidelines: [
159
+ "Use this tool when on a session to understand how and when to use quests.",
160
+ "ALWAYS run this tool when on a new session before starting any work.",
161
+ ],
162
+ parameters: Type.Object(
163
+ {},
164
+ { description: "No parameters required for learning about quests" },
165
+ ),
166
+ execute: async (_toolCallId, _params, _signal, _onUpdate, _ctx) =>
167
+ questToolExecute(questLog, "learn_quests", { action: QUEST_ACTIONS.skill }),
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>,
144
174
  renderResult: renderQuestResult(config),
145
175
  });
146
176
  }
@@ -1,5 +1,5 @@
1
1
  import { StringEnum } from "@mariozechner/pi-ai";
2
- import { Type } from "@sinclair/typebox";
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 toggle, update, delete, reorder actions). ALWAYS use the ${idLength}-digit hex ID shown in brackets, never the positional number.`,
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(