pi-quests 0.3.0 → 0.5.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.
@@ -1,18 +1,26 @@
1
1
  import type { AgentToolResult, Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { Text } from "@mariozechner/pi-tui";
3
+ import type { ResolvedConfig } from "../config.js";
3
4
  import { logger } from "../logger.js";
5
+ import type { Quest } from "../quest/types.js";
4
6
  import { QUEST_ACTIONS } from "../quest/types.js";
7
+ import { formatQuestRow, formatStepSpacerLine } from "./quests.js";
5
8
 
6
9
  type QuestArgs = {
7
10
  action: string;
8
11
  descriptions?: string[];
9
- id?: number;
12
+ targetId?: string;
13
+ id?: string;
14
+ all?: boolean;
10
15
  };
11
16
 
12
17
  export function renderQuestCall(args: QuestArgs, theme: Theme, _context: unknown): Text {
13
18
  const id = "id" in args ? args.id : undefined;
14
19
  logger.debug("quests:tool", "renderCall", { action: args.action, id });
15
- const actionText = theme.fg("toolTitle", theme.bold("quest ")) + theme.fg("accent", args.action);
20
+ const actionText =
21
+ theme.fg("toolTitle", theme.bold("quest ")) +
22
+ theme.fg("accent", args.action) +
23
+ theme.fg("muted", args.targetId ? ` ${args.targetId}` : "");
16
24
 
17
25
  if (args.action === QUEST_ACTIONS.add && args.descriptions && args.descriptions.length > 0) {
18
26
  const n = args.descriptions.length;
@@ -29,41 +37,82 @@ export function renderQuestCall(args: QuestArgs, theme: Theme, _context: unknown
29
37
  args.action === QUEST_ACTIONS.delete) &&
30
38
  args.id !== undefined
31
39
  ) {
32
- return new Text(`${actionText} ${theme.fg("muted", `#${args.id}`)}`, 0, 0);
40
+ return new Text(`${actionText} ${theme.fg("muted", `[${args.id}]`)}`, 0, 0);
41
+ }
42
+
43
+ if (args.action === QUEST_ACTIONS.clear) {
44
+ return new Text(`${actionText} ${theme.fg("muted", `[${args.all ? "all" : "done"}]`)}`, 0, 0);
33
45
  }
34
46
 
35
47
  return new Text(actionText, 0, 0);
36
48
  }
37
49
 
38
- export function renderQuestResult(
39
- result: AgentToolResult<unknown>,
40
- options: { expanded: boolean; isPartial: boolean },
41
- theme: Theme,
42
- _context: unknown,
43
- ): Text {
44
- logger.debug("quests:tool", "renderResult", {
45
- expanded: options.expanded,
46
- isPartial: options.isPartial,
47
- });
48
- const details = result.details as Record<string, unknown> | undefined;
49
- const allQuests =
50
- (details?.quests as Array<{ id: number; description: string; done: boolean }> | undefined) ??
51
- [];
52
- const questsToRender = (details?.displayQuests ?? details?.quests) as
53
- | Array<{ id: number; description: string; done: boolean }>
54
- | undefined;
55
-
56
- if (Array.isArray(questsToRender) && questsToRender.length > 0) {
57
- const lines = questsToRender.map((q) => {
58
- const pos = allQuests.findIndex((x) => x.id === q.id) + 1 || 1;
59
- const marker = q.done ? theme.fg("success", "✓") : theme.fg("dim", "○");
60
-
61
- return `${marker} ${theme.fg("text", `#${pos}`)} ${theme.fg("muted", q.description)}`;
50
+ export function renderQuestResult(config: ResolvedConfig) {
51
+ return (
52
+ result: AgentToolResult<unknown>,
53
+ options: { expanded: boolean; isPartial: boolean },
54
+ theme: Theme,
55
+ _context: unknown,
56
+ ): Text => {
57
+ logger.debug("quests:tool", "renderResult", {
58
+ expanded: options.expanded,
59
+ isPartial: options.isPartial,
62
60
  });
61
+ 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;
63
64
 
64
- return new Text(lines.join("\n"), 0, 0);
65
- }
65
+ if (
66
+ !Array.isArray(questsToRender) ||
67
+ questsToRender.length === 0 ||
68
+ !Array.isArray(allQuests)
69
+ ) {
70
+ const text = result.content.map((c) => (c.type === "text" ? c.text : "[image]")).join("\n");
71
+ return new Text(theme.fg("text", text), 0, 0);
72
+ }
73
+
74
+ const quests = allQuests;
75
+ const renderedIds = new Set(questsToRender.map((q) => q.id));
76
+ const parents = quests.filter((q) => !(q as Quest & { parentId?: string }).parentId);
77
+ const lines: string[] = [];
78
+
79
+ function getSteps(parentId: string) {
80
+ return quests.filter((q) => (q as Quest & { parentId?: string }).parentId === parentId);
81
+ }
82
+
83
+ function willRenderLater(idx: number): boolean {
84
+ for (let j = idx + 1; j < parents.length; j++) {
85
+ const p = parents[j];
86
+ if (renderedIds.has(p.id)) return true;
87
+
88
+ const steps = getSteps(p.id).filter((step) => renderedIds.has(step.id));
89
+ if (steps.length > 0) return true;
90
+ }
66
91
 
67
- const text = result.content.map((c) => (c.type === "text" ? c.text : "[image]")).join("\n");
68
- return new Text(theme.fg("text", text), 0, 0);
92
+ return false;
93
+ }
94
+
95
+ for (let i = 0; i < parents.length; i++) {
96
+ const parent = parents[i];
97
+ const parentIncluded = renderedIds.has(parent.id);
98
+ const includedSteps = getSteps(parent.id).filter((step) => renderedIds.has(step.id));
99
+
100
+ if (!parentIncluded && includedSteps.length === 0) continue;
101
+
102
+ if (parentIncluded) {
103
+ lines.push(formatQuestRow(theme, parent, config.ids.length, i + 1));
104
+ if (includedSteps.length > 0) lines.push(formatStepSpacerLine(theme, config.ids.length));
105
+ }
106
+
107
+ for (const step of includedSteps) {
108
+ lines.push(formatQuestRow(theme, step, config.ids.length));
109
+ }
110
+
111
+ if (includedSteps.length > 0 && willRenderLater(i)) {
112
+ lines.push("");
113
+ }
114
+ }
115
+
116
+ return new Text(lines.join("\n"), 0, 0);
117
+ };
69
118
  }
@@ -1,13 +1,23 @@
1
1
  import type { AgentToolResult, ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import type { Static } from "@sinclair/typebox";
3
+ import type { ResolvedConfig } from "../config.js";
3
4
  import { logger } from "../logger.js";
4
5
  import { QUEST_PROMPT_GATE, QUEST_PROMPT_REMINDER } from "../prompts.js";
5
6
  import { makeToolResult, type QuestAction, type QuestLog } from "../quest/dataplane.js";
6
7
  import { QUEST_ACTIONS } from "../quest/types.js";
8
+
9
+ const SPLIT_DISPLAY_ACTIONS = [
10
+ QUEST_ACTIONS.add,
11
+ QUEST_ACTIONS.list,
12
+ QUEST_ACTIONS.split,
13
+ QUEST_ACTIONS.add_step,
14
+ QUEST_ACTIONS.revert,
15
+ ] as const;
16
+
7
17
  import { renderQuestCall, renderQuestResult } from "../renderers/tools.js";
8
- import { QuestParams } from "./params.js";
18
+ import { createQuestParams, type QuestParamsType } from "./params.js";
9
19
 
10
- type QuestToolParams = Static<typeof QuestParams>;
20
+ type QuestToolParams = Static<QuestParamsType>;
11
21
 
12
22
  const toolHandlers: {
13
23
  [K in QuestToolParams["action"]]: (
@@ -22,6 +32,20 @@ const toolHandlers: {
22
32
  descriptions: params.descriptions,
23
33
  });
24
34
  },
35
+ [QUEST_ACTIONS.split](questLog, params, toolCallId) {
36
+ return runTool(questLog, toolCallId, {
37
+ type: QUEST_ACTIONS.split,
38
+ id: params.id,
39
+ descriptions: params.descriptions,
40
+ });
41
+ },
42
+ [QUEST_ACTIONS.add_step](questLog, params, toolCallId) {
43
+ return runTool(questLog, toolCallId, {
44
+ type: QUEST_ACTIONS.add_step,
45
+ id: params.id,
46
+ descriptions: params.descriptions,
47
+ });
48
+ },
25
49
  [QUEST_ACTIONS.list](questLog, _params, toolCallId) {
26
50
  return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.list });
27
51
  },
@@ -45,9 +69,22 @@ const toolHandlers: {
45
69
  return runTool(questLog, toolCallId, {
46
70
  type: QUEST_ACTIONS.reorder,
47
71
  id: params.id,
48
- targetIndex: params.targetIndex,
72
+ targetId: params.targetId,
49
73
  });
50
74
  },
75
+ [QUEST_ACTIONS.reparent](questLog, params, toolCallId) {
76
+ return runTool(questLog, toolCallId, {
77
+ type: QUEST_ACTIONS.reparent,
78
+ id: params.id,
79
+ parentId: params.parentId,
80
+ });
81
+ },
82
+ [QUEST_ACTIONS.rules](questLog, _params, toolCallId) {
83
+ return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.rules });
84
+ },
85
+ [QUEST_ACTIONS.skill](questLog, _params, toolCallId) {
86
+ return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.skill });
87
+ },
51
88
  [QUEST_ACTIONS.revert](questLog, _params, toolCallId) {
52
89
  return runTool(questLog, toolCallId, { type: QUEST_ACTIONS.revert });
53
90
  },
@@ -61,14 +98,13 @@ function runTool(
61
98
  const result = questLog.execute(action);
62
99
  logger.debug("quests:tool", "execute-complete", { toolCallId, success: result.success });
63
100
 
64
- const displayQuests =
65
- action.type === QUEST_ACTIONS.add ||
66
- action.type === QUEST_ACTIONS.list ||
67
- action.type === QUEST_ACTIONS.revert
68
- ? questLog.getAll()
69
- : result.quest
70
- ? [result.quest]
71
- : undefined;
101
+ const displayQuests = SPLIT_DISPLAY_ACTIONS.includes(
102
+ action.type as (typeof SPLIT_DISPLAY_ACTIONS)[number],
103
+ )
104
+ ? questLog.getAll()
105
+ : result.quest
106
+ ? [result.quest]
107
+ : undefined;
72
108
 
73
109
  return makeToolResult(result.message, questLog, displayQuests);
74
110
  }
@@ -83,19 +119,26 @@ export async function questToolExecute(
83
119
  return handler(questLog, params, toolCallId);
84
120
  }
85
121
 
86
- export function registerQuestTool(pi: ExtensionAPI, questLog: QuestLog): void {
122
+ export function registerQuestTool(
123
+ pi: ExtensionAPI,
124
+ questLog: QuestLog,
125
+ config: ResolvedConfig,
126
+ ): void {
87
127
  logger.debug("quests:tool", "register");
88
128
  pi.registerTool({
89
129
  name: "quest",
90
130
  label: "Quest",
91
131
  description:
92
- "Manage the session quest log. Use this VERY frequently to track tasks, plans, and progress throughout the conversation.",
93
- promptSnippet: "Add, list, toggle, update, delete, clear, or revert quest items",
132
+ "Manage the session quest log and retrieve the complete quest system documentation. " +
133
+ "Use this VERY frequently to track tasks, plans, and progress. " +
134
+ "When you need to understand quests, steps, rules, or best practices, use action: 'skill' or action: 'rules'.",
135
+ promptSnippet:
136
+ "Manage quests and steps, or retrieve quest rules and best practices via skill/rules",
94
137
  promptGuidelines: [...QUEST_PROMPT_GATE, ...QUEST_PROMPT_REMINDER],
95
- parameters: QuestParams,
138
+ parameters: createQuestParams(config.ids.length),
96
139
  execute: (toolCallId, params, _signal, _onUpdate, _ctx) =>
97
140
  questToolExecute(questLog, toolCallId, params),
98
141
  renderCall: renderQuestCall,
99
- renderResult: renderQuestResult,
142
+ renderResult: renderQuestResult(config),
100
143
  });
101
144
  }
@@ -2,34 +2,48 @@ import { StringEnum } from "@mariozechner/pi-ai";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import { QUEST_ACTION_VALUES } from "../quest/types.js";
4
4
 
5
- export const QuestParams = Type.Object({
6
- action: StringEnum(QUEST_ACTION_VALUES, {
7
- description: "The quest action to perform",
8
- }),
9
- descriptions: Type.Optional(
10
- Type.Array(Type.String(), {
11
- description: "Array of quest descriptions (required for add action)",
5
+ export function createQuestParams(idLength: number) {
6
+ const pattern = `^[0-9a-f]{${idLength}}$`;
7
+ return Type.Object({
8
+ action: StringEnum(QUEST_ACTION_VALUES, {
9
+ description: "The quest action to perform",
12
10
  }),
13
- ),
14
- description: Type.Optional(
15
- Type.String({
16
- description: "New description (required for update action)",
17
- }),
18
- ),
19
- id: Type.Optional(
20
- Type.Number({
21
- description: "Quest ID (required for toggle, update, delete, reorder actions)",
22
- }),
23
- ),
24
- targetIndex: Type.Optional(
25
- Type.Number({
26
- description:
27
- "Target index for reorder action (0-based array index). If moving to the 3rd slot in the list, use 2.",
28
- }),
29
- ),
30
- all: Type.Optional(
31
- Type.Boolean({
32
- description: "Clear all quests when true (defaults to clearing only completed quests)",
33
- }),
34
- ),
35
- });
11
+ descriptions: Type.Optional(
12
+ Type.Array(Type.String(), {
13
+ description: "Array of quest descriptions (required for add action)",
14
+ }),
15
+ ),
16
+ description: Type.Optional(
17
+ Type.String({
18
+ description: "New description (required for update action)",
19
+ }),
20
+ ),
21
+
22
+ id: Type.Optional(
23
+ Type.String({
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.`,
26
+ }),
27
+ ),
28
+ targetId: Type.Optional(
29
+ Type.String({
30
+ pattern,
31
+ description:
32
+ "Target quest ID for reorder action. The quest will be moved to just before the target quest.",
33
+ }),
34
+ ),
35
+ parentId: Type.Optional(
36
+ Type.String({
37
+ pattern,
38
+ description: "Parent quest ID for reparent action. Omit to promote to top-level.",
39
+ }),
40
+ ),
41
+ all: Type.Optional(
42
+ Type.Boolean({
43
+ description: "Clear all quests when true (defaults to clearing only completed quests)",
44
+ }),
45
+ ),
46
+ });
47
+ }
48
+
49
+ export type QuestParamsType = ReturnType<typeof createQuestParams>;