@gavin-hu/my-pi-agent 0.11.4 → 0.13.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/README.md +38 -26
- package/README.zh-CN.md +33 -23
- package/extensions/bytefly/README.md +381 -225
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +52 -6
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/register.ts +13 -2
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +30 -2
- package/extensions/bytefly/footer/types.ts +12 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +15 -4
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +37 -13
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +293 -48
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/cache.ts +73 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +87 -0
- package/extensions/bytefly/update/register.ts +158 -0
- package/extensions/bytefly/update/state.ts +40 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +24 -14
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +14 -5
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +45 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/schema.ts +6 -2
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +56 -14
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +83 -36
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +84 -19
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +91 -2
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +246 -0
- package/extensions/plan/runtime.ts +73 -24
- package/extensions/plan/steps.ts +60 -39
- package/extensions/plan/todos.ts +29 -60
- package/extensions/plan/tools.ts +188 -244
- package/extensions/plan/tui.ts +18 -31
- package/extensions/plan/types.ts +12 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +7 -2
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +106 -41
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -3
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/watch/README.md +8 -3
- package/extensions/watch/index.ts +19 -0
- package/extensions/web-access/README.md +46 -16
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +20 -21
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +14 -3
- package/extensions/web-access/text.ts +50 -11
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/extensions/worktree/README.md +74 -15
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/exit.ts +35 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +12 -2
- package/extensions/worktree/runtime.ts +20 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +12 -9
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +340 -46
- package/lib/todo-state.ts +62 -0
- package/lib/tui/README.md +91 -0
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/bytefly.schema.json +0 -785
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question-cancelled.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory-long-subject.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc-running.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch-rejected.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Host orchestration for the goal comprehension step: the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Host orchestration for the goal comprehension step: the clarifier call, its
|
|
3
|
+
* cancellable TUI loader, the in-command questionnaire, and the confirm/edit
|
|
4
|
+
* dialog.
|
|
5
5
|
*
|
|
6
|
-
* The loop's target is understanding what the user wants.
|
|
7
|
-
*
|
|
8
|
-
* the command asks those questions through host dialogs
|
|
9
|
-
* answers into the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* The loop's target is understanding what the user wants. One call asks the
|
|
7
|
+
* model whether it understands the goal and, when it cannot yet, what it needs
|
|
8
|
+
* to know; the command asks those questions through host dialogs and carries
|
|
9
|
+
* the answers into the editor, where the user confirms or corrects the goal.
|
|
10
|
+
* The flow never throws and never half-applies: a failed, timed-out, cancelled,
|
|
11
|
+
* or unusable comprehension falls back to the text the user typed, and a
|
|
12
|
+
* dismissed dialog cancels the whole command.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { BorderedLoader, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import type { UserMessage } from "@earendil-works/pi-ai";
|
|
17
17
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
18
18
|
import { type Clarification, type ClarifyAnswer, type ClarifyQuestion, parseClarification } from "./clarify-parse.ts";
|
|
19
|
-
import { buildClarifyMessages
|
|
19
|
+
import { buildClarifyMessages } from "./clarify-prompt.ts";
|
|
20
20
|
|
|
21
|
-
/** Bound on
|
|
21
|
+
/** Bound on the clarifier model call, in milliseconds. */
|
|
22
22
|
const CLARIFY_TIMEOUT_MS = 30_000;
|
|
23
23
|
|
|
24
24
|
/** The slice of the context the clarify step reads. */
|
|
@@ -27,9 +27,6 @@ export type ClarifyContext = Pick<ExtensionContext, "mode" | "ui" | "signal" | "
|
|
|
27
27
|
/** Loader text shown while the comprehension call runs. */
|
|
28
28
|
const LOADER_MESSAGE = "Understanding goal…";
|
|
29
29
|
|
|
30
|
-
/** Loader text shown while the goal is restated from the answers. */
|
|
31
|
-
const REFINE_LOADER_MESSAGE = "Updating the goal…";
|
|
32
|
-
|
|
33
30
|
/** The confirm/edit dialog title, reading as a confirmation of understanding. */
|
|
34
31
|
const EDITOR_TITLE = "Confirm the goal — Enter to save, Esc to cancel";
|
|
35
32
|
|
|
@@ -79,74 +76,54 @@ function assistantText(response: { content?: unknown }): string {
|
|
|
79
76
|
return parts.join("\n");
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
/** Runs a nested operation, optionally behind the TUI loader. */
|
|
83
|
-
type CompleterRun = <T>(fn: (signal: AbortSignal | undefined) => Promise<T>) => Promise<T | undefined>;
|
|
84
|
-
|
|
85
|
-
/** The context slice the completer needs. */
|
|
86
|
-
type CompleterContext = Pick<ExtensionContext, "model" | "modelRegistry" | "signal">;
|
|
87
|
-
|
|
88
79
|
/**
|
|
89
|
-
*
|
|
80
|
+
* One clarifier model call, behind the TUI loader.
|
|
90
81
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
82
|
+
* The call is aborted on timeout, on the session signal, or when the loader is
|
|
83
|
+
* cancelled, and any failure becomes `undefined` so a provider error never
|
|
84
|
+
* blocks the command. Nothing from the conversation is sent: the model must not
|
|
85
|
+
* invent scope the user did not state.
|
|
95
86
|
*/
|
|
96
|
-
function
|
|
97
|
-
ctx: CompleterContext,
|
|
98
|
-
timeoutMs: number,
|
|
99
|
-
run: CompleterRun,
|
|
100
|
-
buildMessages: () => { systemPrompt: string; messages: UserMessage[] },
|
|
101
|
-
): () => Promise<string | undefined> {
|
|
102
|
-
return async () => {
|
|
103
|
-
const model = ctx.model;
|
|
104
|
-
if (!model) return undefined;
|
|
105
|
-
|
|
106
|
-
const { systemPrompt, messages } = buildMessages();
|
|
107
|
-
const controller = new AbortController();
|
|
108
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
109
|
-
const onAbort = () => controller.abort();
|
|
110
|
-
ctx.signal?.addEventListener("abort", onAbort, { once: true });
|
|
111
|
-
try {
|
|
112
|
-
return await run(async (signal) => {
|
|
113
|
-
if (signal) signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
114
|
-
const response = await ctx.modelRegistry.complete(
|
|
115
|
-
model,
|
|
116
|
-
{ systemPrompt, messages },
|
|
117
|
-
{ signal: controller.signal, cacheRetention: "none" },
|
|
118
|
-
);
|
|
119
|
-
if (response.stopReason === "aborted") return undefined;
|
|
120
|
-
return assistantText(response);
|
|
121
|
-
});
|
|
122
|
-
} catch {
|
|
123
|
-
return undefined;
|
|
124
|
-
} finally {
|
|
125
|
-
clearTimeout(timer);
|
|
126
|
-
ctx.signal?.removeEventListener("abort", onAbort);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Run one clarifier call behind its loader and return the raw reply. */
|
|
132
|
-
async function completeOnce(
|
|
87
|
+
async function askClarifier(
|
|
133
88
|
ctx: ClarifyContext,
|
|
134
|
-
loaderMessage: string,
|
|
135
89
|
buildMessages: () => { systemPrompt: string; messages: UserMessage[] },
|
|
136
90
|
): Promise<string | undefined> {
|
|
137
|
-
|
|
91
|
+
const model = ctx.model;
|
|
92
|
+
if (!model) return undefined;
|
|
93
|
+
|
|
94
|
+
const { systemPrompt, messages } = buildMessages();
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
const timer = setTimeout(() => controller.abort(), CLARIFY_TIMEOUT_MS);
|
|
97
|
+
const onAbort = () => controller.abort();
|
|
98
|
+
ctx.signal?.addEventListener("abort", onAbort, { once: true });
|
|
99
|
+
try {
|
|
100
|
+
return await withLoader(ctx, LOADER_MESSAGE, async (signal) => {
|
|
101
|
+
if (signal) signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
102
|
+
const response = await ctx.modelRegistry.complete(
|
|
103
|
+
model,
|
|
104
|
+
{ systemPrompt, messages },
|
|
105
|
+
{ signal: controller.signal, cacheRetention: "none" },
|
|
106
|
+
);
|
|
107
|
+
if (response.stopReason === "aborted") return undefined;
|
|
108
|
+
return assistantText(response);
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
ctx.signal?.removeEventListener("abort", onAbort);
|
|
115
|
+
}
|
|
138
116
|
}
|
|
139
117
|
|
|
140
118
|
/**
|
|
141
|
-
*
|
|
142
|
-
* {@link Clarification}.
|
|
119
|
+
* The comprehension call, parsed into a {@link Clarification}.
|
|
143
120
|
*
|
|
144
121
|
* Returns `undefined` when there is no dialog UI or no model, or when the call
|
|
145
122
|
* fails, times out, is cancelled, or returns nothing usable.
|
|
146
123
|
*/
|
|
147
124
|
export async function clarifyGoalPrompt(ctx: ClarifyContext, text: string): Promise<Clarification | undefined> {
|
|
148
125
|
if (!ctx.hasUI || !ctx.model) return undefined;
|
|
149
|
-
return parseClarification(await
|
|
126
|
+
return parseClarification(await askClarifier(ctx, () => buildClarifyMessages(text)));
|
|
150
127
|
}
|
|
151
128
|
|
|
152
129
|
/**
|
|
@@ -155,7 +132,7 @@ export async function clarifyGoalPrompt(ctx: ClarifyContext, text: string): Prom
|
|
|
155
132
|
* A question with options uses a selector; a free-form question uses a text
|
|
156
133
|
* input. Resolves `undefined` when any dialog is dismissed, so the caller cancels
|
|
157
134
|
* the whole command; an empty answer is skipped. The answer is sanitized to one
|
|
158
|
-
* line, since it is folded into
|
|
135
|
+
* line, since it is folded into the goal the editor holds.
|
|
159
136
|
*/
|
|
160
137
|
async function askQuestions(
|
|
161
138
|
ctx: Pick<ClarifyContext, "ui">,
|
|
@@ -178,19 +155,15 @@ async function askQuestions(
|
|
|
178
155
|
}
|
|
179
156
|
|
|
180
157
|
/**
|
|
181
|
-
*
|
|
158
|
+
* Carry the answers into the editor as notes under the restatement.
|
|
182
159
|
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
160
|
+
* No model call rewrites the goal from the answers, so they are shown verbatim
|
|
161
|
+
* with the question that asked for them: the user folds them into the sentence
|
|
162
|
+
* or deletes them, and whatever the editor holds is what gets recorded.
|
|
185
163
|
*/
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
goal: string,
|
|
190
|
-
answers: readonly ClarifyAnswer[],
|
|
191
|
-
): Promise<string | undefined> {
|
|
192
|
-
const reply = await completeOnce(ctx, REFINE_LOADER_MESSAGE, () => buildRefineMessages(original, goal, answers));
|
|
193
|
-
return parseClarification(reply)?.goal || undefined;
|
|
164
|
+
function withAnswers(goal: string, answers: readonly ClarifyAnswer[]): string {
|
|
165
|
+
if (answers.length === 0) return goal;
|
|
166
|
+
return [goal, "", ...answers.map((answer) => `Also: ${answer.question} → ${answer.answer}`)].join("\n");
|
|
194
167
|
}
|
|
195
168
|
|
|
196
169
|
/** Open the confirm/edit dialog, or cancel when it is dismissed. */
|
|
@@ -201,7 +174,7 @@ async function editOrCancel(ctx: ClarifyContext, prefill: string): Promise<GoalP
|
|
|
201
174
|
|
|
202
175
|
/**
|
|
203
176
|
* Run the understanding loop for `/goal <prompt>`: comprehend, ask what is
|
|
204
|
-
* missing,
|
|
177
|
+
* missing, then let the user confirm or correct the goal.
|
|
205
178
|
*
|
|
206
179
|
* Without a dialog UI or a model the typed text is used unchanged, so the
|
|
207
180
|
* command never blocks on the loop in print or JSON mode. A failed comprehension
|
|
@@ -224,11 +197,7 @@ export async function prepareGoalPrompt(ctx: ClarifyContext, typed: string): Pro
|
|
|
224
197
|
ctx.ui.notify("Goal not set.", "info");
|
|
225
198
|
return { action: "cancel" };
|
|
226
199
|
}
|
|
227
|
-
|
|
228
|
-
const refined = await refineWithAnswers(ctx, typed, proposed, answers);
|
|
229
|
-
if (refined) proposed = refined;
|
|
230
|
-
else ctx.ui.notify("Could not update the goal from your answers; edit it below.", "info");
|
|
231
|
-
}
|
|
200
|
+
proposed = withAnswers(proposed, answers);
|
|
232
201
|
}
|
|
233
202
|
return editOrCancel(ctx, proposed);
|
|
234
203
|
}
|
|
@@ -23,23 +23,37 @@ import { GOAL_ENTRY_TYPE, type Goal } from "./types.ts";
|
|
|
23
23
|
/** Title of the record-only edit dialog, spelling out both exits. */
|
|
24
24
|
const EDIT_TITLE = "Goal — Enter to save, Esc to cancel";
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
{
|
|
33
|
-
{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
/**
|
|
27
|
+
* One reserved `/goal` verb: the word, what a bare occurrence means, and whether
|
|
28
|
+
* it takes the rest of the line as its argument.
|
|
29
|
+
*/
|
|
30
|
+
type GoalVerb =
|
|
31
|
+
| { word: string; kind: "usage" | "clear" | "done"; argument: "none" }
|
|
32
|
+
| { word: string; kind: "raw"; argument: "required" }
|
|
33
|
+
| { word: string; kind: "record"; argument: "required" | "optional" };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The single list behind the parser, the completions, and the help text, so a
|
|
37
|
+
* new verb cannot land in one of them and be missing from another.
|
|
38
|
+
* `set`/`edit` share the `record` kind; the two words are interchangeable.
|
|
39
|
+
*/
|
|
40
|
+
export const GOAL_VERBS: readonly GoalVerb[] = [
|
|
41
|
+
{ word: "set", kind: "record", argument: "optional" },
|
|
42
|
+
{ word: "edit", kind: "record", argument: "optional" },
|
|
43
|
+
{ word: "raw", kind: "raw", argument: "required" },
|
|
44
|
+
{ word: "clear", kind: "clear", argument: "none" },
|
|
45
|
+
{ word: "done", kind: "done", argument: "none" },
|
|
46
|
+
{ word: "achieved", kind: "done", argument: "none" },
|
|
47
|
+
{ word: "help", kind: "usage", argument: "none" },
|
|
48
|
+
{ word: "-h", kind: "usage", argument: "none" },
|
|
49
|
+
{ word: "--help", kind: "usage", argument: "none" },
|
|
37
50
|
];
|
|
38
51
|
|
|
39
|
-
/**
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
/** Completion values: a verb that takes an argument completes with a space. */
|
|
53
|
+
const GOAL_COMPLETIONS = GOAL_VERBS.map((verb) => ({
|
|
54
|
+
value: verb.argument === "none" ? verb.word : `${verb.word} `,
|
|
55
|
+
label: verb.word,
|
|
56
|
+
}));
|
|
43
57
|
|
|
44
58
|
/** One parsed `/goal` invocation. */
|
|
45
59
|
export type GoalCommand =
|
|
@@ -55,30 +69,22 @@ export type GoalCommand =
|
|
|
55
69
|
/**
|
|
56
70
|
* Parse `/goal`'s argument into one command.
|
|
57
71
|
*
|
|
58
|
-
*
|
|
59
|
-
* not malformed keywords
|
|
60
|
-
*
|
|
72
|
+
* Only the first word is looked up, so a keyword matches a whole word: `setfoo`
|
|
73
|
+
* and `edit-foo` are free-form prompts, not malformed keywords, and a verb that
|
|
74
|
+
* takes no argument leaves the whole line a prompt (`clear the cache`). Anything
|
|
75
|
+
* unlisted is a prompt.
|
|
61
76
|
*/
|
|
62
77
|
export function parseGoalCommand(input: string): GoalCommand {
|
|
63
78
|
const text = input.trim();
|
|
64
79
|
if (!text) return { kind: "show" };
|
|
65
80
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const objective = (record[2] ?? "").trim();
|
|
74
|
-
return objective ? { kind: "record", text: objective } : { kind: "editor" };
|
|
75
|
-
}
|
|
76
|
-
const raw = RAW_PATTERN.exec(text);
|
|
77
|
-
if (raw) {
|
|
78
|
-
const objective = (raw[1] ?? "").trim();
|
|
79
|
-
return objective ? { kind: "raw", text: objective } : { kind: "usage" };
|
|
80
|
-
}
|
|
81
|
-
return { kind: "prompt", text };
|
|
81
|
+
const head = text.split(/\s+/, 1)[0] ?? "";
|
|
82
|
+
const rest = text.slice(head.length).trim();
|
|
83
|
+
const verb = GOAL_VERBS.find((candidate) => candidate.word === head.toLowerCase());
|
|
84
|
+
if (!verb) return { kind: "prompt", text };
|
|
85
|
+
if (verb.argument === "none") return rest ? { kind: "prompt", text } : { kind: verb.kind };
|
|
86
|
+
if (verb.kind === "raw") return rest ? { kind: "raw", text: rest } : { kind: "usage" };
|
|
87
|
+
return rest ? { kind: "record", text: rest } : { kind: "editor" };
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
/**
|
|
@@ -87,7 +93,7 @@ export function parseGoalCommand(input: string): GoalCommand {
|
|
|
87
93
|
* once the prefix contains a space (or the start of the prompt does not look
|
|
88
94
|
* like a keyword) the argument is left to the user.
|
|
89
95
|
*/
|
|
90
|
-
function completeGoalArg(prefix: string): Array<{ value: string; label: string }> | null {
|
|
96
|
+
export function completeGoalArg(prefix: string): Array<{ value: string; label: string }> | null {
|
|
91
97
|
const text = prefix.trimStart();
|
|
92
98
|
if (text.includes(" ")) return null;
|
|
93
99
|
const items = GOAL_COMPLETIONS.filter((item) => item.label.startsWith(text));
|
|
@@ -51,10 +51,18 @@ export function goalObjective(goal: Goal, theme: Theme): string {
|
|
|
51
51
|
return goal.status === "achieved" ? paint(theme, "faint", goal.objective) : paint(theme, "label", goal.objective);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Model-facing result text. `openTodos` counts the todo items the branch still
|
|
56
|
+
* has open, so marking a goal achieved can point at work the list still holds.
|
|
57
|
+
*/
|
|
58
|
+
export function formatGoalText(goal: Goal | null, openTodos = 0): string {
|
|
56
59
|
if (goal === null) return "Goal cleared.";
|
|
57
|
-
if (goal.status === "achieved")
|
|
60
|
+
if (goal.status === "achieved") {
|
|
61
|
+
const text = `Goal achieved: ${goal.objective}`;
|
|
62
|
+
if (openTodos <= 0) return text;
|
|
63
|
+
const items = openTodos === 1 ? "1 todo item is" : `${openTodos} todo items are`;
|
|
64
|
+
return `${text}\n${items} still open; close or clear the list if that work is done.`;
|
|
65
|
+
}
|
|
58
66
|
return `Goal: ${goal.objective}\nKeep this objective in mind as you work, and mark it achieved with the goal tool when it is done.`;
|
|
59
67
|
}
|
|
60
68
|
|
|
@@ -72,7 +80,7 @@ export const GOAL_HELP = [
|
|
|
72
80
|
"Set a goal with /goal <what you want to accomplish> and start working on it.",
|
|
73
81
|
"/goal raw <prompt> records your words verbatim, without confirming them first.",
|
|
74
82
|
"/goal set <prompt> records the goal without starting work; /goal edit [<prompt>] changes it.",
|
|
75
|
-
"/goal done · /goal clear · /goal help",
|
|
83
|
+
"/goal done (or achieved) · /goal clear · /goal help (or -h / --help)",
|
|
76
84
|
].join("\n");
|
|
77
85
|
|
|
78
86
|
/** Truncate an objective to `max` display columns, appending `…` when cut. */
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
10
10
|
import { Type, type Static } from "typebox";
|
|
11
11
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
12
|
+
import { MAX_GOAL_OBJECTIVE } from "../../lib/goal-state.ts";
|
|
12
13
|
import { GOAL_ACTIONS, GOAL_STATUSES, type Goal, type GoalStatus } from "./types.ts";
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Maximum length of the objective. Owned by `lib/goal-state.ts`, so a reader of
|
|
17
|
+
* the published state applies the same limit as this validator.
|
|
18
|
+
*/
|
|
19
|
+
export const MAX_OBJECTIVE = MAX_GOAL_OBJECTIVE;
|
|
16
20
|
|
|
17
21
|
const GoalStatusEnum = StringEnum(GOAL_STATUSES, {
|
|
18
22
|
description: 'Whether the goal is still being pursued. Defaults to "active".',
|
package/extensions/goal/tools.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { JsonValue } from "@earendil-works/pi-ai";
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
13
13
|
import { GOAL_TOOL } from "../../lib/tool-names.ts";
|
|
14
|
+
import { lastTodos } from "../../lib/todo-state.ts";
|
|
14
15
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
15
16
|
import { rejectionLine, reuseCallText, toolCallTitle } from "../../lib/tui/tool.ts";
|
|
16
17
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
@@ -18,7 +19,7 @@ import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
|
18
19
|
import { formatCallText, formatGoalText } from "./format.ts";
|
|
19
20
|
import type { GoalRuntime } from "./runtime.ts";
|
|
20
21
|
import { GoalParams, GoalResult, normalizeGoal, type GoalArgs } from "./schema.ts";
|
|
21
|
-
import {
|
|
22
|
+
import { GoalHeader } from "./tui.ts";
|
|
22
23
|
import type { GoalAction, GoalDetails } from "./types.ts";
|
|
23
24
|
|
|
24
25
|
export const TOOL_NAME = GOAL_TOOL;
|
|
@@ -45,13 +46,30 @@ function toStructuredContent(details: GoalDetails): Record<string, JsonValue> {
|
|
|
45
46
|
return content;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* How many items the branch's todo list still has open.
|
|
51
|
+
*
|
|
52
|
+
* `todo` is the *how* to the goal's *what*, so completing one while the other
|
|
53
|
+
* still holds work is worth saying out loud. The list is read from the branch
|
|
54
|
+
* (`lib/todo-state.ts`), so no todo module is imported.
|
|
55
|
+
*/
|
|
56
|
+
function openTodoCount(entries: Iterable<unknown>): number {
|
|
57
|
+
return lastTodos(entries).filter((todo) => todo.status !== "completed").length;
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
/**
|
|
49
61
|
* Build the tool result for one call. A rejected call (`error` set) carries the
|
|
50
62
|
* unchanged goal so the model can retry, and is marked `isError`.
|
|
51
63
|
*/
|
|
52
|
-
function goalResult(details: GoalDetails, error?: string) {
|
|
64
|
+
function goalResult(details: GoalDetails, options: { error?: string; openTodos?: number } = {}) {
|
|
65
|
+
const { error, openTodos = 0 } = options;
|
|
53
66
|
return {
|
|
54
|
-
content: [
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: "text" as const,
|
|
70
|
+
text: error === undefined ? formatGoalText(details.goal, openTodos) : `Error: ${error}`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
55
73
|
details,
|
|
56
74
|
structuredContent: toStructuredContent(details),
|
|
57
75
|
...(error === undefined ? {} : { isError: true as const }),
|
|
@@ -82,10 +100,14 @@ export function registerTools(pi: ExtensionAPI, runtime: GoalRuntime): void {
|
|
|
82
100
|
const goal = normalizeGoal(args);
|
|
83
101
|
const action = goal === null ? "clear" : goal.status === "achieved" ? "achieve" : "set";
|
|
84
102
|
runtime.setGoal(goal, ctx);
|
|
85
|
-
|
|
103
|
+
const openTodos = action === "achieve" ? openTodoCount(ctx.sessionManager.getBranch()) : 0;
|
|
104
|
+
return goalResult({ goal, action }, { openTodos });
|
|
86
105
|
} catch (error) {
|
|
87
106
|
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
-
return goalResult(
|
|
107
|
+
return goalResult(
|
|
108
|
+
{ goal: runtime.getGoal(), action: attemptedAction(args), error: message },
|
|
109
|
+
{ error: message },
|
|
110
|
+
);
|
|
89
111
|
}
|
|
90
112
|
},
|
|
91
113
|
|
|
@@ -115,9 +137,7 @@ export function registerTools(pi: ExtensionAPI, runtime: GoalRuntime): void {
|
|
|
115
137
|
return text;
|
|
116
138
|
}
|
|
117
139
|
const view =
|
|
118
|
-
context.lastComponent instanceof
|
|
119
|
-
? context.lastComponent
|
|
120
|
-
: new GoalResultView(details.goal, theme);
|
|
140
|
+
context.lastComponent instanceof GoalHeader ? context.lastComponent : new GoalHeader(details.goal, theme);
|
|
121
141
|
view.update(details.goal, theme);
|
|
122
142
|
return view;
|
|
123
143
|
},
|
package/extensions/goal/tui.ts
CHANGED
|
@@ -44,7 +44,7 @@ export class GoalWidget implements Component {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** Transcript result block: the status header, separated from the call line. */
|
|
47
|
-
export class
|
|
47
|
+
export class GoalHeader implements Component {
|
|
48
48
|
private goal: Goal;
|
|
49
49
|
private theme: Theme;
|
|
50
50
|
|
package/extensions/goal/types.ts
CHANGED
|
@@ -15,8 +15,12 @@ export const GOAL_ACTIONS = ["set", "achieve", "clear"] as const;
|
|
|
15
15
|
|
|
16
16
|
export type GoalAction = (typeof GOAL_ACTIONS)[number];
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Custom-entry type used to persist a goal set outside the tool (the `/goal`
|
|
20
|
+
* command). Owned by `lib/goal-state.ts` because another extension's reader
|
|
21
|
+
* dispatches on it; re-exported here so goal's own modules keep one import.
|
|
22
|
+
*/
|
|
23
|
+
export { GOAL_ENTRY_TYPE } from "../../lib/goal-state.ts";
|
|
20
24
|
|
|
21
25
|
/** How an achieved goal renders in the persistent widget. */
|
|
22
26
|
export type AchievedStyle = "show" | "hide";
|
package/extensions/job/README.md
CHANGED
|
@@ -38,10 +38,11 @@ How the `job` call and its result look in the transcript — the call line as a
|
|
|
38
38
|
killing, and clearing.
|
|
39
39
|
- Reports finished jobs to the model at the start of the next turn, or — for
|
|
40
40
|
jobs started with `wake: true` — by triggering one turn.
|
|
41
|
-
- Kills session-owned jobs on shutdown so no process is left behind;
|
|
42
|
-
job with `detached: true` to keep it running and reattach next session.
|
|
43
|
-
detached
|
|
44
|
-
logging after this process ends.
|
|
41
|
+
- Kills session-owned jobs on shutdown so no process is left behind; on POSIX,
|
|
42
|
+
start a job with `detached: true` to keep it running and reattach next session.
|
|
43
|
+
A detached job points its own output straight at its log file, so it keeps
|
|
44
|
+
logging after this process ends. Windows has neither a process group to leave
|
|
45
|
+
nor a shell `EXIT` trap, so it refuses the detach instead of faking one.
|
|
45
46
|
|
|
46
47
|
## Tool
|
|
47
48
|
|
|
@@ -53,7 +54,7 @@ How the `job` call and its result look in the transcript — the call line as a
|
|
|
53
54
|
| `cwd` | Working directory (`start`); defaults to the session cwd. |
|
|
54
55
|
| `label` | Short display label (`start`); defaults to the command. |
|
|
55
56
|
| `wake` | Wake the agent with one triggered turn when it finishes (`start`). |
|
|
56
|
-
| `detached` | Leave it running when the session ends (`start`). |
|
|
57
|
+
| `detached` | Leave it running when the session ends (`start`); POSIX only, refused on Windows. |
|
|
57
58
|
| `stdin` | Give the job a stdin pipe so `send` can write to it (`start`); not with `detached`. |
|
|
58
59
|
| `id` | Job id such as `j1` (`status`/`logs`/`send`/`kill`/`wait`/`clear`). |
|
|
59
60
|
| `lines` | Log lines to return (`logs`, 1–2000). |
|
|
@@ -191,7 +192,7 @@ document, project wins):
|
|
|
191
192
|
| `enabled` | `true` | Track jobs at all. |
|
|
192
193
|
| `showStatus` | `true` | Show the `▸ N` / `✗ N` status chips. |
|
|
193
194
|
| `wakeOnFinish` | `false` | Wake the agent when any job finishes. |
|
|
194
|
-
| `detachedByDefault` | `false` | Leave jobs running when the session ends. |
|
|
195
|
+
| `detachedByDefault` | `false` | Leave jobs running when the session ends; inert on Windows. |
|
|
195
196
|
| `maxJobs` | `20` | Finished jobs kept before the oldest are pruned. |
|
|
196
197
|
| `maxLogLines` | `100` | Log lines returned by `logs`. |
|
|
197
198
|
| `maxLogBytes` | `8388608` | Bytes a log segment may reach before it rotates to `<id>.log.1`; `0` keeps logs unbounded. |
|
|
@@ -214,8 +215,8 @@ rewritten.
|
|
|
214
215
|
- **`unknown` still has no exit code on some paths.** A POSIX job is wrapped in
|
|
215
216
|
an `EXIT` trap that writes `$?` to a per-job `.status` file, so a job
|
|
216
217
|
reattached after its session died reports its real exit code. `exec cmd`
|
|
217
|
-
replaces the shell and bypasses the trap, and Windows has no equivalent,
|
|
218
|
-
those
|
|
218
|
+
replaces the shell and bypasses the trap, and a Windows job has no equivalent,
|
|
219
|
+
so those settle as `unknown`.
|
|
219
220
|
- **Another session's jobs are shown, but not managed here.** Records are merged
|
|
220
221
|
from every session file so ids stay unique and a peer's job is not reaped, and
|
|
221
222
|
`list`/`/jobs` show them tagged `peer` (a dead session's records are adopted
|
|
@@ -236,10 +237,13 @@ rewritten.
|
|
|
236
237
|
keep appending to the moved inode, or lose everything. Its log therefore grows
|
|
237
238
|
without the `maxLogBytes` cap; `logs` still reads a bounded tail, and the file
|
|
238
239
|
can be emptied by hand.
|
|
239
|
-
- **Windows
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
- **Windows cannot detach a job.** There is no process group to leave and no
|
|
241
|
+
shell `EXIT` trap, so `detached: true` is refused with the three alternatives
|
|
242
|
+
(`detached: false`, WSL, a `subagent`) rather than starting a job that would
|
|
243
|
+
die with the session while claiming to outlive it. `job.detachedByDefault` is
|
|
244
|
+
inert there instead of failing every start with a config set on another
|
|
245
|
+
machine, and `doctor` names both facts. Tree-kill is still `taskkill /T`, and a
|
|
246
|
+
job reported only after its process is gone settles as `unknown`.
|
|
243
247
|
|
|
244
248
|
## Non-goals
|
|
245
249
|
|
|
@@ -127,6 +127,8 @@ export interface RuntimeOptions {
|
|
|
127
127
|
killTree?: KillTreeFn;
|
|
128
128
|
startToken?: StartTokenFn;
|
|
129
129
|
now?: () => number;
|
|
130
|
+
/** Platform seam: whether this is Windows. Defaults to the running platform. */
|
|
131
|
+
isWindows?: () => boolean;
|
|
130
132
|
/** Fixed config, for tests; production loads it per cwd. */
|
|
131
133
|
config?: JobsConfig;
|
|
132
134
|
}
|
|
@@ -202,6 +204,7 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
202
204
|
const killTree = options.killTree ?? defaultKillTree;
|
|
203
205
|
const startToken = options.startToken ?? defaultStartToken;
|
|
204
206
|
const now = options.now ?? (() => Date.now());
|
|
207
|
+
const isWindows = options.isWindows ?? (() => process.platform === "win32");
|
|
205
208
|
|
|
206
209
|
const jobs = new Map<string, Job>();
|
|
207
210
|
const handles = new Map<string, Handle>();
|
|
@@ -717,7 +720,20 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
717
720
|
if (!config.enabled) throw new Error("jobs are disabled.");
|
|
718
721
|
ui.attach(ctx);
|
|
719
722
|
const wantsStdin = startOptions.stdin === true;
|
|
720
|
-
|
|
723
|
+
// Windows has no process group to leave and no shell `EXIT` trap to record an
|
|
724
|
+
// exit code, so a job there cannot outlive the session: refusing an explicit
|
|
725
|
+
// detach is the honest answer, and it happens before any id is reserved.
|
|
726
|
+
// `detachedByDefault` stays inert instead of failing every start with a config
|
|
727
|
+
// the user set on another machine; `doctor` names that.
|
|
728
|
+
const windows = isWindows();
|
|
729
|
+
if (windows && startOptions.detached === true) {
|
|
730
|
+
throw new Error(
|
|
731
|
+
"A detached job cannot outlive the session on Windows: there is no process group to leave " +
|
|
732
|
+
"and no shell EXIT trap to record its exit code. Start it with `detached: false`, " +
|
|
733
|
+
"run it under WSL, or move the work into a `subagent`.",
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
const detached = windows ? false : (startOptions.detached ?? config.detachedByDefault);
|
|
721
737
|
// A detached job outlives this process, so a stdin pipe owned by this session
|
|
722
738
|
// would hand it EOF the moment the session ends; refuse instead of silently
|
|
723
739
|
// dropping the pipe (or the detach).
|
|
@@ -732,14 +748,15 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
732
748
|
// so a peer that advanced the shared counter cannot hand us a live job's id.
|
|
733
749
|
const id = store.nextId(jobs.keys());
|
|
734
750
|
const logPath = store.logPath(id);
|
|
735
|
-
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
const
|
|
751
|
+
// Only a POSIX shell has an `EXIT` trap to record a status, and only a detached
|
|
752
|
+
// job points its own output at the log so it keeps logging after this process
|
|
753
|
+
// ends (which on Windows is never: `detached` is false there).
|
|
754
|
+
const statusPath = windows ? null : store.statusPath(id);
|
|
755
|
+
const detachOutput = detached;
|
|
739
756
|
|
|
740
757
|
// Wrap the command so a POSIX shell records its exit status; the path travels
|
|
741
758
|
// in the environment so the shell never has to quote it.
|
|
742
|
-
const program = withExitTrap(startOptions.command,
|
|
759
|
+
const program = withExitTrap(startOptions.command, windows, detachOutput);
|
|
743
760
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
744
761
|
if (statusPath) env[STATUS_ENV] = statusPath;
|
|
745
762
|
if (detachOutput) env[LOG_ENV] = logPath;
|
package/extensions/job/status.ts
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* shell never has to quote it). A later session reads that file when it finds a
|
|
8
8
|
* dead pid and reports `exited`/`failed` instead of `unknown`.
|
|
9
9
|
*
|
|
10
|
-
* Windows shells have no equivalent trap
|
|
11
|
-
*
|
|
10
|
+
* Windows shells have no equivalent trap. A Windows job is never detached (the
|
|
11
|
+
* runtime refuses it), so the missing trap is not a second limitation: a job
|
|
12
|
+
* reported only after its process is gone still settles as `unknown`, which
|
|
13
|
+
* `doctor` names.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
import { closeSync, existsSync, openSync, readSync, statSync } from "node:fs";
|