@preapexis/piexis 0.2.2 → 0.2.5
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 +85 -131
- package/apps/server/src/index.ts +97 -0
- package/apps/server/src/session-manager.ts +95 -0
- package/apps/server/src/workspace.ts +39 -0
- package/{tsconfig.json → apps/server/tsconfig.json} +3 -3
- package/apps/web/dist/assets/index-D4n3_lAw.js +10 -0
- package/apps/web/dist/assets/index-igiK0FPY.css +1 -0
- package/apps/web/dist/index.html +3 -0
- package/bin/piexis-web.mjs +24 -0
- package/extensions/agents.ts +205 -0
- package/extensions/ask-question.ts +54 -95
- package/extensions/recommended-tools.ts +62 -0
- package/extensions/ui.ts +204 -58
- package/extensions/webui.ts +45 -0
- package/lib/agent-config.ts +55 -0
- package/lib/agents.ts +208 -0
- package/lib/question-model.ts +98 -0
- package/lib/question-ui.ts +214 -0
- package/lib/recommended-tools.ts +119 -0
- package/lib/subagent-runner.ts +146 -0
- package/lib/ui-tips.json +41 -0
- package/package.json +41 -14
- package/packages/pi-integration/src/rpc-client.ts +99 -0
- package/packages/shared/src/index.ts +27 -0
- package/prompts/init.md +10 -2
- package/prompts/summary.md +7 -6
- package/schemas/agent.schema.json +48 -0
- package/skills/{claude-api/LICENSE.txt → caveman/LICENSE} +2 -2
- package/skills/caveman/LICENSE-MIT +21 -0
- package/skills/caveman/NOTICE +10 -0
- package/skills/caveman/SKILL.md +89 -0
- package/skills/find-skills/SKILL.md +141 -0
- package/skills/ponytail/LICENSE +21 -0
- package/skills/ponytail/NOTICE +9 -0
- package/skills/ponytail/SKILL.md +52 -0
- package/themes/midnight.json +81 -0
- package/.github/workflows/publish.yml +0 -35
- package/AGENTS.md +0 -58
- package/eval/README.md +0 -72
- package/eval/permission-cases.json +0 -28
- package/eval/permissions.mjs +0 -103
- package/extensions/background-tasks.ts +0 -433
- package/extensions/background-worker.ts +0 -50
- package/extensions/glob.ts +0 -32
- package/extensions/mode.ts +0 -397
- package/lib/bash-approvals.ts +0 -113
- package/lib/bash-policy.ts +0 -164
- package/lib/mode-policy.ts +0 -174
- package/lib/path-policy.ts +0 -182
- package/lib/permission-classifier.ts +0 -128
- package/lib/permission-config.ts +0 -91
- package/lib/permission-denials.ts +0 -32
- package/lib/permission-intent.ts +0 -128
- package/lib/permission-taxonomy.ts +0 -15
- package/lib/task-broker.ts +0 -65
- package/lib/task-checkout.ts +0 -39
- package/lib/task-git.ts +0 -44
- package/lib/task-patch.ts +0 -106
- package/lib/task-permissions.ts +0 -51
- package/lib/task-report.ts +0 -62
- package/lib/task-worktree-policy.ts +0 -56
- package/scripts/release.mjs +0 -32
- package/skills/claude-api/SKILL.md +0 -572
- package/skills/claude-api/csharp/claude-api/README.md +0 -362
- package/skills/claude-api/csharp/claude-api/batches.md +0 -14
- package/skills/claude-api/csharp/claude-api/files-api.md +0 -25
- package/skills/claude-api/csharp/claude-api/streaming.md +0 -28
- package/skills/claude-api/csharp/claude-api/tool-use.md +0 -164
- package/skills/claude-api/curl/examples.md +0 -256
- package/skills/claude-api/curl/managed-agents.md +0 -371
- package/skills/claude-api/go/claude-api/README.md +0 -188
- package/skills/claude-api/go/claude-api/files-api.md +0 -23
- package/skills/claude-api/go/claude-api/streaming.md +0 -43
- package/skills/claude-api/go/claude-api/tool-use.md +0 -220
- package/skills/claude-api/go/managed-agents/README.md +0 -564
- package/skills/claude-api/java/claude-api/README.md +0 -238
- package/skills/claude-api/java/claude-api/files-api.md +0 -27
- package/skills/claude-api/java/claude-api/streaming.md +0 -24
- package/skills/claude-api/java/claude-api/tool-use.md +0 -228
- package/skills/claude-api/java/managed-agents/README.md +0 -443
- package/skills/claude-api/php/claude-api/README.md +0 -174
- package/skills/claude-api/php/claude-api/batches.md +0 -15
- package/skills/claude-api/php/claude-api/files-api.md +0 -13
- package/skills/claude-api/php/claude-api/streaming.md +0 -27
- package/skills/claude-api/php/claude-api/tool-use.md +0 -253
- package/skills/claude-api/php/managed-agents/README.md +0 -443
- package/skills/claude-api/python/claude-api/README.md +0 -565
- package/skills/claude-api/python/claude-api/batches.md +0 -198
- package/skills/claude-api/python/claude-api/files-api.md +0 -170
- package/skills/claude-api/python/claude-api/sdk-upgrade.md +0 -286
- package/skills/claude-api/python/claude-api/streaming.md +0 -179
- package/skills/claude-api/python/claude-api/tool-use.md +0 -627
- package/skills/claude-api/python/managed-agents/README.md +0 -335
- package/skills/claude-api/ruby/claude-api/README.md +0 -134
- package/skills/claude-api/ruby/claude-api/streaming.md +0 -16
- package/skills/claude-api/ruby/claude-api/tool-use.md +0 -41
- package/skills/claude-api/ruby/managed-agents/README.md +0 -394
- package/skills/claude-api/shared/admin-api.md +0 -179
- package/skills/claude-api/shared/agent-design.md +0 -101
- package/skills/claude-api/shared/anthropic-cli.md +0 -269
- package/skills/claude-api/shared/claude-platform-on-aws.md +0 -59
- package/skills/claude-api/shared/cost-optimization.md +0 -233
- package/skills/claude-api/shared/error-codes.md +0 -262
- package/skills/claude-api/shared/live-sources.md +0 -166
- package/skills/claude-api/shared/managed-agents-api-reference.md +0 -455
- package/skills/claude-api/shared/managed-agents-client-patterns.md +0 -220
- package/skills/claude-api/shared/managed-agents-core.md +0 -359
- package/skills/claude-api/shared/managed-agents-environments.md +0 -226
- package/skills/claude-api/shared/managed-agents-events.md +0 -290
- package/skills/claude-api/shared/managed-agents-memory.md +0 -201
- package/skills/claude-api/shared/managed-agents-multiagent.md +0 -260
- package/skills/claude-api/shared/managed-agents-onboarding.md +0 -82
- package/skills/claude-api/shared/managed-agents-outcomes.md +0 -108
- package/skills/claude-api/shared/managed-agents-overview.md +0 -77
- package/skills/claude-api/shared/managed-agents-scheduled-deployments.md +0 -156
- package/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +0 -282
- package/skills/claude-api/shared/managed-agents-tools.md +0 -504
- package/skills/claude-api/shared/managed-agents-webhooks.md +0 -143
- package/skills/claude-api/shared/model-migration.md +0 -1872
- package/skills/claude-api/shared/models.md +0 -142
- package/skills/claude-api/shared/platform-availability.md +0 -53
- package/skills/claude-api/shared/prompt-audit.md +0 -223
- package/skills/claude-api/shared/prompt-caching.md +0 -292
- package/skills/claude-api/shared/token-counting.md +0 -56
- package/skills/claude-api/shared/tool-use-concepts.md +0 -526
- package/skills/claude-api/typescript/claude-api/README.md +0 -405
- package/skills/claude-api/typescript/claude-api/batches.md +0 -106
- package/skills/claude-api/typescript/claude-api/files-api.md +0 -98
- package/skills/claude-api/typescript/claude-api/streaming.md +0 -178
- package/skills/claude-api/typescript/claude-api/tool-use.md +0 -586
- package/skills/claude-api/typescript/managed-agents/README.md +0 -359
- package/skills-lock.json +0 -359
- package/tests/auto-permissions.test.mjs +0 -315
- package/tests/background-tasks.test.mjs +0 -27
- package/tests/background-worker.test.mjs +0 -60
- package/tests/bash-permissions.test.mjs +0 -276
- package/tests/glob.test.mjs +0 -31
- package/tests/mode.test.mjs +0 -62
- package/tests/permission-evaluation.test.mjs +0 -70
- package/tests/permission-foundation.test.mjs +0 -194
- package/tests/permission-policy.test.mjs +0 -158
- package/tests/runtime/README.md +0 -51
- package/tests/runtime/harness.mjs +0 -190
- package/tests/runtime/permissions.test.mjs +0 -192
- package/tests/runtime/tui.test.mjs +0 -60
- package/tests/skills-routing.test.mjs +0 -51
- package/tests/task-integration.test.mjs +0 -324
- package/tests/task-permissions.test.mjs +0 -178
- package/tests/task-worktree-policy.test.mjs +0 -53
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
|
|
3
|
+
const Option = Type.Object({
|
|
4
|
+
id: Type.Optional(Type.String({ minLength: 1 })),
|
|
5
|
+
label: Type.String({ minLength: 1 }),
|
|
6
|
+
description: Type.Optional(Type.String())
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const AskQuestionParams = Type.Object({
|
|
10
|
+
questions: Type.Array(Type.Object({
|
|
11
|
+
id: Type.Optional(Type.String({ minLength: 1 })),
|
|
12
|
+
question: Type.String({ minLength: 1 }),
|
|
13
|
+
type: Type.Optional(Type.Union([Type.Literal("single"), Type.Literal("multi"), Type.Literal("text")])),
|
|
14
|
+
options: Type.Optional(Type.Array(Option)),
|
|
15
|
+
multiSelect: Type.Optional(Type.Boolean()),
|
|
16
|
+
allowOther: Type.Optional(Type.Boolean()),
|
|
17
|
+
required: Type.Optional(Type.Boolean()),
|
|
18
|
+
defaultSelection: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
|
|
19
|
+
minSelections: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
20
|
+
maxSelections: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
21
|
+
maxLength: Type.Optional(Type.Integer({ minimum: 0 }))
|
|
22
|
+
}), { minItems: 1, description: "Related questions, answered together in the terminal" })
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type Params = Static<typeof AskQuestionParams>;
|
|
26
|
+
export type Question = Omit<Params["questions"][number], "id" | "type" | "options" | "required"> & {
|
|
27
|
+
id: string;
|
|
28
|
+
type: "single" | "multi" | "text";
|
|
29
|
+
options: { id: string; label: string; description?: string }[];
|
|
30
|
+
required: boolean;
|
|
31
|
+
};
|
|
32
|
+
export type Draft = { indices: number[]; text: string; custom: boolean };
|
|
33
|
+
export type Answer = {
|
|
34
|
+
id: string;
|
|
35
|
+
question: string;
|
|
36
|
+
answer: string | string[] | null;
|
|
37
|
+
optionIds: string[];
|
|
38
|
+
custom: boolean;
|
|
39
|
+
};
|
|
40
|
+
export type Outcome = "answered" | "cancelled" | "aborted" | "requires_interaction" | "error";
|
|
41
|
+
export type Result = { status: Outcome; answers: Answer[]; cancelled: boolean; error?: string };
|
|
42
|
+
|
|
43
|
+
export function normalizeQuestions(params: Params): Question[] {
|
|
44
|
+
if (!params.questions.length) throw new Error("At least one question is required.");
|
|
45
|
+
const questions = params.questions.map((item, index): Question => {
|
|
46
|
+
const type = item.type ?? (item.options?.length ? item.multiSelect ? "multi" : "single" : "text");
|
|
47
|
+
const options = (item.options ?? []).map((option, i) => ({ ...option, id: option.id ?? `option-${i + 1}` }));
|
|
48
|
+
if (!item.question.trim()) throw new Error("Question text cannot be blank.");
|
|
49
|
+
if (new Set(options.map((option) => option.id)).size !== options.length) throw new Error("Option IDs must be unique within each question.");
|
|
50
|
+
if (options.some((option) => !option.label.trim())) throw new Error("Option labels cannot be blank.");
|
|
51
|
+
if (item.type && item.multiSelect && item.type !== "multi") throw new Error("type conflicts with multiSelect.");
|
|
52
|
+
if (type === "text" && options.length) throw new Error("Text questions cannot have options.");
|
|
53
|
+
if (!options.length && type !== "text") throw new Error("Selection questions require options.");
|
|
54
|
+
// Legacy optionless questions still require explicit permission for text input.
|
|
55
|
+
if (!item.type && !options.length && !item.allowOther) throw new Error("AskQuestion requires options or allowOther=true.");
|
|
56
|
+
const required = item.required !== false;
|
|
57
|
+
const min = Math.max(item.minSelections ?? 0, required ? 1 : 0);
|
|
58
|
+
const max = item.maxSelections ?? options.length;
|
|
59
|
+
if (type !== "multi" && (item.minSelections !== undefined || item.maxSelections !== undefined)) throw new Error("Selection limits require a multi question.");
|
|
60
|
+
if (type === "multi" && (min > max || max > options.length)) throw new Error("Invalid selection limits.");
|
|
61
|
+
const q = { ...item, id: item.id ?? `question-${index + 1}`, type, options, required };
|
|
62
|
+
const defaults = item.defaultSelection === undefined ? [] : typeof item.defaultSelection === "string" ? [item.defaultSelection] : item.defaultSelection;
|
|
63
|
+
if (type === "text" && defaults.length) throw new Error("Text questions do not support defaultSelection.");
|
|
64
|
+
if (type === "single" && defaults.length > 1) throw new Error("Single questions accept only one default selection.");
|
|
65
|
+
if (defaults.some((id) => !options.some((option) => option.id === id))) throw new Error("Unknown default option ID.");
|
|
66
|
+
return q;
|
|
67
|
+
});
|
|
68
|
+
if (new Set(questions.map((q) => q.id)).size !== questions.length) throw new Error("Question IDs must be unique.");
|
|
69
|
+
return questions;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function initialDraft(q: Question): Draft {
|
|
73
|
+
const defaults = q.defaultSelection === undefined ? [] : typeof q.defaultSelection === "string" ? [q.defaultSelection] : q.defaultSelection;
|
|
74
|
+
return { indices: q.options.flatMap((option, i) => defaults.includes(option.id) ? [i] : []), text: "", custom: q.type === "text" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Shared by submission and final review; never filters away invalid selections. */
|
|
78
|
+
export function validateAnswer(q: Question, draft: Draft): Answer {
|
|
79
|
+
if (draft.custom) {
|
|
80
|
+
if (q.type !== "text" && !q.allowOther) throw new Error("Custom answers are not enabled.");
|
|
81
|
+
if (q.required && !draft.text.trim()) throw new Error("An answer is required.");
|
|
82
|
+
if (q.maxLength !== undefined && Array.from(draft.text).length > q.maxLength) throw new Error(`Use at most ${q.maxLength} characters.`);
|
|
83
|
+
return { id: q.id, question: q.question, answer: draft.text, optionIds: [], custom: true };
|
|
84
|
+
}
|
|
85
|
+
if (q.type === "text") throw new Error("Enter a text answer.");
|
|
86
|
+
if (draft.indices.some((i) => !Number.isInteger(i) || i < 0 || i >= q.options.length)) throw new Error("Choose only listed options.");
|
|
87
|
+
const indices = [...new Set(draft.indices)].sort((a, b) => a - b);
|
|
88
|
+
const min = q.type === "multi" ? Math.max(q.minSelections ?? 0, q.required ? 1 : 0) : q.required ? 1 : 0;
|
|
89
|
+
const max = q.type === "multi" ? q.maxSelections ?? q.options.length : 1;
|
|
90
|
+
if (indices.length < min) throw new Error(`Choose at least ${min} option${min === 1 ? "" : "s"}.`);
|
|
91
|
+
if (indices.length > max) throw new Error(`Choose at most ${max} option${max === 1 ? "" : "s"}.`);
|
|
92
|
+
const options = indices.map((i) => q.options[i]!);
|
|
93
|
+
return { id: q.id, question: q.question, answer: q.type === "multi" ? options.map((o) => o.label) : options[0]?.label ?? "", optionIds: options.map((o) => o.id), custom: false };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function outcome(status: Outcome, answers: Answer[] = [], error?: string): Result {
|
|
97
|
+
return { status, answers, cancelled: status !== "answered", ...(error ? { error } : {}) };
|
|
98
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Input, Key, matchesKey, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { initialDraft, outcome, validateAnswer, type Answer, type Draft, type Outcome, type Question, type Result } from "./question-model.ts";
|
|
4
|
+
|
|
5
|
+
// Model-provided labels are plain text, not terminal escape sequences.
|
|
6
|
+
const plain = (text: string) => text.replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
|
|
7
|
+
const message = (error: unknown) => error instanceof Error ? error.message : String(error);
|
|
8
|
+
|
|
9
|
+
export class QuestionScreen {
|
|
10
|
+
private current = 0;
|
|
11
|
+
private review = false;
|
|
12
|
+
private error = "";
|
|
13
|
+
private settled = false;
|
|
14
|
+
private hasFocus = false;
|
|
15
|
+
private readonly drafts: Draft[];
|
|
16
|
+
private readonly inputs: Input[];
|
|
17
|
+
private readonly focus: number[];
|
|
18
|
+
private readonly confirmed = new Set<number>();
|
|
19
|
+
private readonly abort = () => this.finish("aborted");
|
|
20
|
+
private readonly eof = () => this.finish("error", "Terminal input closed before submission.");
|
|
21
|
+
private readonly inputError = (error: Error) => this.finish("error", `Terminal input failed: ${message(error)}`);
|
|
22
|
+
|
|
23
|
+
private readonly questions: Question[];
|
|
24
|
+
private readonly tui: Pick<TUI, "requestRender" | "terminal">;
|
|
25
|
+
private readonly theme: Pick<Theme, "fg">;
|
|
26
|
+
private readonly done: (result: Result) => void;
|
|
27
|
+
private readonly signal?: AbortSignal;
|
|
28
|
+
private readonly stream: Pick<NodeJS.ReadStream, "on" | "off">;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
questions: Question[],
|
|
32
|
+
tui: Pick<TUI, "requestRender" | "terminal">,
|
|
33
|
+
theme: Pick<Theme, "fg">,
|
|
34
|
+
done: (result: Result) => void,
|
|
35
|
+
signal?: AbortSignal,
|
|
36
|
+
stream: Pick<NodeJS.ReadStream, "on" | "off"> = process.stdin
|
|
37
|
+
) {
|
|
38
|
+
this.questions = questions;
|
|
39
|
+
this.tui = tui;
|
|
40
|
+
this.theme = theme;
|
|
41
|
+
this.done = done;
|
|
42
|
+
this.signal = signal;
|
|
43
|
+
this.stream = stream;
|
|
44
|
+
this.drafts = questions.map(initialDraft);
|
|
45
|
+
this.focus = this.drafts.map((draft) => draft.indices[0] ?? 0);
|
|
46
|
+
this.inputs = questions.map(() => new Input({ prompt: "> " }));
|
|
47
|
+
signal?.addEventListener("abort", this.abort, { once: true });
|
|
48
|
+
stream.on("end", this.eof);
|
|
49
|
+
stream.on("close", this.eof);
|
|
50
|
+
stream.on("error", this.inputError);
|
|
51
|
+
if (signal?.aborted) this.abort();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get focused() { return this.hasFocus; }
|
|
55
|
+
set focused(value: boolean) {
|
|
56
|
+
this.hasFocus = value;
|
|
57
|
+
this.inputs.forEach((input, i) => { input.focused = value && !this.review && i === this.current && this.drafts[i]!.custom; });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
dispose() {
|
|
61
|
+
this.signal?.removeEventListener("abort", this.abort);
|
|
62
|
+
this.stream.off("end", this.eof);
|
|
63
|
+
this.stream.off("close", this.eof);
|
|
64
|
+
this.stream.off("error", this.inputError);
|
|
65
|
+
this.inputs.forEach((input) => { input.focused = false; });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private answers(): Answer[] {
|
|
69
|
+
return this.questions.flatMap((q, i) => this.confirmed.has(i) ? [validateAnswer(q, this.drafts[i]!)] : []);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private finish(status: Outcome, error?: string) {
|
|
73
|
+
if (this.settled) return;
|
|
74
|
+
this.settled = true;
|
|
75
|
+
this.dispose();
|
|
76
|
+
this.done(outcome(status, this.answers(), error));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private refresh() {
|
|
80
|
+
this.focused = this.hasFocus;
|
|
81
|
+
this.tui.requestRender();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private save() {
|
|
85
|
+
try {
|
|
86
|
+
validateAnswer(this.questions[this.current]!, this.drafts[this.current]!);
|
|
87
|
+
this.confirmed.add(this.current);
|
|
88
|
+
this.error = "";
|
|
89
|
+
if (this.questions.length === 1) this.finish("answered");
|
|
90
|
+
else if (this.current + 1 < this.questions.length) this.current++;
|
|
91
|
+
else this.review = true;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.error = message(error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
handleInput(data: string) {
|
|
98
|
+
if (this.settled) return;
|
|
99
|
+
try {
|
|
100
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
101
|
+
this.finish("cancelled");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (this.questions.length > 1 && (matchesKey(data, Key.tab) || matchesKey(data, Key.shift("tab")))) {
|
|
105
|
+
const position = this.review ? this.questions.length : this.current;
|
|
106
|
+
const direction = matchesKey(data, Key.tab) ? 1 : -1;
|
|
107
|
+
const next = (position + direction + this.questions.length + 1) % (this.questions.length + 1);
|
|
108
|
+
this.review = next === this.questions.length;
|
|
109
|
+
if (!this.review) this.current = next;
|
|
110
|
+
this.error = "";
|
|
111
|
+
} else if (this.review) {
|
|
112
|
+
if (matchesKey(data, Key.enter)) {
|
|
113
|
+
const missing = this.questions.findIndex((_, i) => !this.confirmed.has(i));
|
|
114
|
+
if (missing !== -1) {
|
|
115
|
+
this.current = missing;
|
|
116
|
+
this.review = false;
|
|
117
|
+
this.error = "Confirm this question before submitting.";
|
|
118
|
+
} else {
|
|
119
|
+
this.questions.forEach((q, i) => validateAnswer(q, this.drafts[i]!));
|
|
120
|
+
this.finish("answered");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
const q = this.questions[this.current]!;
|
|
125
|
+
const draft = this.drafts[this.current]!;
|
|
126
|
+
const input = this.inputs[this.current]!;
|
|
127
|
+
if (draft.custom) {
|
|
128
|
+
if (q.type !== "text" && matchesKey(data, Key.up)) {
|
|
129
|
+
draft.custom = false;
|
|
130
|
+
this.confirmed.delete(this.current);
|
|
131
|
+
} else if (matchesKey(data, Key.enter)) {
|
|
132
|
+
this.save();
|
|
133
|
+
} else {
|
|
134
|
+
input.handleInput(data);
|
|
135
|
+
draft.text = input.getValue();
|
|
136
|
+
this.confirmed.delete(this.current);
|
|
137
|
+
this.error = "";
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// The synthetic action lives at a separate index, never a label or option ID.
|
|
141
|
+
const count = q.options.length + (q.allowOther ? 1 : 0) + (!q.required && q.type === "single" ? 1 : 0);
|
|
142
|
+
let focus = this.focus[this.current]!;
|
|
143
|
+
if (matchesKey(data, Key.up)) this.focus[this.current] = (focus - 1 + count) % count;
|
|
144
|
+
else if (matchesKey(data, Key.down)) this.focus[this.current] = (focus + 1) % count;
|
|
145
|
+
else if (q.type === "multi" && matchesKey(data, Key.space) && focus < q.options.length) {
|
|
146
|
+
draft.indices = draft.indices.includes(focus) ? draft.indices.filter((i) => i !== focus) : [...draft.indices, focus];
|
|
147
|
+
this.confirmed.delete(this.current);
|
|
148
|
+
this.error = "";
|
|
149
|
+
} else if (matchesKey(data, Key.enter) || (q.type === "multi" && matchesKey(data, Key.space))) {
|
|
150
|
+
if (q.allowOther && focus === q.options.length) {
|
|
151
|
+
draft.custom = true;
|
|
152
|
+
this.confirmed.delete(this.current);
|
|
153
|
+
} else {
|
|
154
|
+
if (q.type === "single") draft.indices = focus < q.options.length ? [focus] : [];
|
|
155
|
+
this.save();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
this.refresh();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
this.finish("error", message(error));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
invalidate() { this.inputs.forEach((input) => input.invalidate()); }
|
|
167
|
+
|
|
168
|
+
render(width: number): string[] {
|
|
169
|
+
if (this.settled) return [];
|
|
170
|
+
try {
|
|
171
|
+
width = Math.max(1, width);
|
|
172
|
+
const lines: string[] = [];
|
|
173
|
+
const add = (text: string, color: Parameters<Theme["fg"]>[0] = "text") => lines.push(truncateToWidth(this.theme.fg(color, plain(text)), width));
|
|
174
|
+
add(this.review ? "✓ Review answers · Enter to submit" : `? Question ${this.current + 1}/${this.questions.length}`, "accent");
|
|
175
|
+
if (this.review) {
|
|
176
|
+
this.questions.forEach((q, i) => {
|
|
177
|
+
const value = this.confirmed.has(i) ? validateAnswer(q, this.drafts[i]!).answer : "(not confirmed)";
|
|
178
|
+
add(`${q.question}: ${Array.isArray(value) ? value.join(", ") : value}`);
|
|
179
|
+
});
|
|
180
|
+
} else {
|
|
181
|
+
const q = this.questions[this.current]!;
|
|
182
|
+
const draft = this.drafts[this.current]!;
|
|
183
|
+
add(q.question);
|
|
184
|
+
if (draft.custom) {
|
|
185
|
+
add(q.type === "text" ? "Your answer:" : "Your answer · ↑ returns to options", "muted");
|
|
186
|
+
lines.push(...this.inputs[this.current]!.render(width));
|
|
187
|
+
} else {
|
|
188
|
+
const choices = q.options.map((option, i) => ({ label: option.label, description: option.description, selected: draft.indices.includes(i) }));
|
|
189
|
+
if (q.allowOther) choices.push({ label: "Type something else", description: undefined, selected: false });
|
|
190
|
+
if (!q.required && q.type === "single") choices.push({ label: "Skip (no answer)", description: undefined, selected: false });
|
|
191
|
+
// Bound menu height to the terminal and keep the focused choice visible.
|
|
192
|
+
const capacity = Math.max(1, Math.floor((this.tui.terminal.rows - 9) / 2));
|
|
193
|
+
const start = Math.max(0, Math.min(this.focus[this.current]! - Math.floor(capacity / 2), choices.length - capacity));
|
|
194
|
+
choices.slice(start, start + capacity).forEach((choice, offset) => {
|
|
195
|
+
const i = start + offset;
|
|
196
|
+
const focused = i === this.focus[this.current];
|
|
197
|
+
add(`${focused ? "❯" : " "} ${q.type === "multi" ? choice.selected ? "[x] " : "[ ] " : ""}${i + 1}. ${choice.label}`, focused ? "accent" : "text");
|
|
198
|
+
if (choice.description) add(` ${choice.description}`, "muted");
|
|
199
|
+
});
|
|
200
|
+
if (choices.length > capacity) add(`${start + 1}–${Math.min(start + capacity, choices.length)} of ${choices.length}`, "dim");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (this.error) add(this.error, "error");
|
|
204
|
+
const multi = !this.review && this.questions[this.current]!.type === "multi" && !this.drafts[this.current]!.custom;
|
|
205
|
+
const textMode = !this.review && this.drafts[this.current]!.custom;
|
|
206
|
+
add(`${this.review || textMode ? "" : multi ? "↑↓ Navigate · Space Toggle · " : "↑↓ Navigate · "}Enter Confirm · Esc/Ctrl+C Cancel`, "dim");
|
|
207
|
+
if (this.questions.length > 1) add("Tab/Shift+Tab Questions · Answers preserved", "dim");
|
|
208
|
+
return lines;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
this.finish("error", message(error));
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { getAgentDir, SettingsManager, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { chmod, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const recommendedTools = ["grep", "find", "ls"];
|
|
7
|
+
const defaultTools = ["read", "bash", "edit", "write"];
|
|
8
|
+
// Older Pi releases treat +name as a literal tool name.
|
|
9
|
+
export const supportsAdditiveTools = SettingsManager.inMemory({ defaultTools: ["+find"] }).getDefaultTools()?.includes("find") === true;
|
|
10
|
+
type Settings = Record<string, unknown>;
|
|
11
|
+
type Snapshot = { text: string | undefined; settings: Settings; mode: number };
|
|
12
|
+
export type SetupSettings = { projectPath: string; globalPath: string; project: Snapshot; global: Snapshot };
|
|
13
|
+
|
|
14
|
+
function object(value: unknown): value is Settings {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function snapshot(path: string): Promise<Snapshot> {
|
|
19
|
+
try {
|
|
20
|
+
const stat = await lstat(path);
|
|
21
|
+
if (!stat.isFile()) throw new Error(`Not a regular settings file: ${path}`);
|
|
22
|
+
const text = await readFile(path, "utf8");
|
|
23
|
+
const settings: unknown = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
24
|
+
if (!object(settings)) throw new Error(`Settings must be a JSON object: ${path}`);
|
|
25
|
+
if (settings.defaultTools !== undefined && (!Array.isArray(settings.defaultTools) || settings.defaultTools.some((entry) => typeof entry !== "string" || !entry))) {
|
|
26
|
+
throw new Error(`Invalid defaultTools in ${path}`);
|
|
27
|
+
}
|
|
28
|
+
if (settings.piexis !== undefined && !object(settings.piexis)) throw new Error(`Invalid piexis settings in ${path}`);
|
|
29
|
+
return { text, settings, mode: stat.mode & 0o777 };
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { text: undefined, settings: {}, mode: 0o600 };
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function readSetupSettings(cwd: string, agentDir = getAgentDir()): Promise<SetupSettings> {
|
|
37
|
+
const projectPath = join(cwd, ".pi", "settings.json");
|
|
38
|
+
const globalPath = join(agentDir, "settings.json");
|
|
39
|
+
const directories = await Promise.all([dirname(projectPath), dirname(globalPath)].map(async (path) => {
|
|
40
|
+
try { return await realpath(path); }
|
|
41
|
+
catch (error) {
|
|
42
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return resolve(path);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}));
|
|
46
|
+
if (directories[0] === directories[1]) throw new Error("Project and user settings share a path; project-only setup is unavailable.");
|
|
47
|
+
const [project, global] = await Promise.all([snapshot(projectPath), snapshot(globalPath)]);
|
|
48
|
+
return { projectPath, globalPath, project, global };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function setupChoice(settings: SetupSettings, feature: "recommendedTools" | "agentSetup" = "recommendedTools"): unknown {
|
|
52
|
+
const piexis = settings.project.settings.piexis;
|
|
53
|
+
return object(piexis) ? piexis[feature] : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function toolsToOffer(settings: SetupSettings, available: string[], active: string[], additive = supportsAdditiveTools): string[] {
|
|
57
|
+
if (setupChoice(settings) === "enabled" || setupChoice(settings) === "disabled") return [];
|
|
58
|
+
const layers = [settings.global.settings.defaultTools, settings.project.settings.defaultTools] as (string[] | undefined)[];
|
|
59
|
+
// A plain-name list (including []) is an intentional allowlist, not an invitation to expand it.
|
|
60
|
+
if (layers.some((list) => list !== undefined && (!list.length || list.some((name) => !/^[+-].+/.test(name))))) return [];
|
|
61
|
+
// Do not reinterpret additive configurations on a Pi release that does not support them.
|
|
62
|
+
if (!additive && layers.some((list) => list !== undefined)) return [];
|
|
63
|
+
const excluded = new Set<string>();
|
|
64
|
+
for (const list of layers) {
|
|
65
|
+
for (const entry of list ?? []) {
|
|
66
|
+
if (entry.startsWith("-")) excluded.add(entry.slice(1));
|
|
67
|
+
else excluded.delete(entry.slice(1));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return recommendedTools.filter((name) => available.includes(name) && !active.includes(name) && !excluded.has(name));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Skip CLI selection overrides instead of trying to infer or undo the user's policy. */
|
|
74
|
+
export function hasToolOverrides(argv: string[]): boolean {
|
|
75
|
+
for (const arg of argv) {
|
|
76
|
+
if (arg === "--") break;
|
|
77
|
+
if (/^(?:--(?:tools|exclude-tools|no-tools|no-builtin-tools)|-t|-xt|-nt|-nbt)(?:=|$)/.test(arg)) return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function saveSetupChoice(settings: SetupSettings, choice: "enabled" | "disabled", tools: string[], additive = supportsAdditiveTools, signal?: AbortSignal, agentSetup?: "enabled" | "disabled"): Promise<void> {
|
|
83
|
+
await withFileMutationQueue(settings.projectPath, async () => {
|
|
84
|
+
signal?.throwIfAborted();
|
|
85
|
+
await mkdir(dirname(settings.projectPath), { recursive: true });
|
|
86
|
+
// Use Pi's settings lock-directory convention. Never remove another writer's lock.
|
|
87
|
+
const lock = `${settings.projectPath}.lock`;
|
|
88
|
+
await mkdir(lock);
|
|
89
|
+
const temporary = join(dirname(settings.projectPath), `.settings.json.piexis-${randomUUID()}.tmp`);
|
|
90
|
+
try {
|
|
91
|
+
const { project, global } = await readSetupSettings(dirname(dirname(settings.projectPath)), dirname(settings.globalPath));
|
|
92
|
+
if (project.text !== settings.project.text || global.text !== settings.global.text) {
|
|
93
|
+
throw new Error("Settings changed while the setup prompt was open. Nothing was overwritten; try again next startup.");
|
|
94
|
+
}
|
|
95
|
+
signal?.throwIfAborted();
|
|
96
|
+
const next = { ...project.settings };
|
|
97
|
+
const piexis = object(next.piexis) ? next.piexis : {};
|
|
98
|
+
next.piexis = { ...piexis,
|
|
99
|
+
...(tools.length ? { recommendedTools: choice } : {}),
|
|
100
|
+
...(agentSetup ? { agentSetup } : {}),
|
|
101
|
+
};
|
|
102
|
+
if (choice === "enabled" && tools.length) {
|
|
103
|
+
next.defaultTools = additive
|
|
104
|
+
? [...(project.settings.defaultTools as string[] | undefined ?? []), ...tools.map((name) => `+${name}`)]
|
|
105
|
+
: [...defaultTools, ...tools];
|
|
106
|
+
}
|
|
107
|
+
await writeFile(temporary, `${JSON.stringify(next, null, 2)}\n`, { flag: "wx", mode: project.mode });
|
|
108
|
+
await chmod(temporary, project.mode);
|
|
109
|
+
signal?.throwIfAborted();
|
|
110
|
+
await rename(temporary, settings.projectPath);
|
|
111
|
+
} finally {
|
|
112
|
+
try {
|
|
113
|
+
await rm(temporary, { force: true });
|
|
114
|
+
} finally {
|
|
115
|
+
await rm(lock, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
type Usage = NonNullable<Awaited<ReturnType<ToolDefinition["execute"]>>["usage"]>;
|
|
8
|
+
|
|
9
|
+
export function emptyUsage(): Usage {
|
|
10
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0,
|
|
11
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function addUsage(total: Usage, value?: Partial<Usage>): void {
|
|
15
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"] as const) {
|
|
16
|
+
if (typeof value?.[key] === "number" && Number.isFinite(value[key])) total[key] += value[key];
|
|
17
|
+
}
|
|
18
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"] as const) {
|
|
19
|
+
if (typeof value?.cost?.[key] === "number" && Number.isFinite(value.cost[key])) total.cost[key] += value.cost[key];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function piInvocation(): { command: string; args: string[] } {
|
|
24
|
+
const script = process.argv[1];
|
|
25
|
+
if (script && /(?:^|[\\/])(?:cli|pi)\.(?:m?js|ts)$/.test(script) && existsSync(script)) {
|
|
26
|
+
return { command: process.execPath, args: [script] };
|
|
27
|
+
}
|
|
28
|
+
if (!/^(node|bun)(\.exe)?$/i.test(basename(process.execPath))) return { command: process.execPath, args: [] };
|
|
29
|
+
const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
30
|
+
const cli = join(dirname(entry), "bundle", "cli.js");
|
|
31
|
+
return existsSync(cli) ? { command: process.execPath, args: [cli] } : { command: "pi", args: [] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ChildResult {
|
|
35
|
+
output: string;
|
|
36
|
+
exitCode: number;
|
|
37
|
+
stopReason?: string;
|
|
38
|
+
error?: string;
|
|
39
|
+
stderr: string;
|
|
40
|
+
usage: Usage;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function limitOutput(text: string, limit = 50_000): string {
|
|
44
|
+
return text.length > limit ? `${text.slice(0, limit)}\n[Output truncated; full output is in subagent tool details (up to 1 MB).]` : text;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runSubagent(options: {
|
|
48
|
+
invocation: { command: string; args: string[] };
|
|
49
|
+
args: string[];
|
|
50
|
+
cwd: string;
|
|
51
|
+
depth: number;
|
|
52
|
+
task: string;
|
|
53
|
+
signal: AbortSignal;
|
|
54
|
+
onProgress?: (text: string) => void;
|
|
55
|
+
}): Promise<ChildResult> {
|
|
56
|
+
options.signal.throwIfAborted();
|
|
57
|
+
const result: ChildResult = { output: "", exitCode: -1, stderr: "", usage: emptyUsage() };
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const grouped = process.platform !== "win32" && options.depth === 1;
|
|
60
|
+
const child = spawn(options.invocation.command, [...options.invocation.args, ...options.args], {
|
|
61
|
+
cwd: options.cwd, shell: false, detached: grouped,
|
|
62
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
63
|
+
env: { ...process.env, PIEXIS_SUBAGENT_DEPTH: String(options.depth) },
|
|
64
|
+
});
|
|
65
|
+
let buffer = "", bytes = 0, timer: NodeJS.Timeout | undefined, failure: Error | undefined;
|
|
66
|
+
let closed = false;
|
|
67
|
+
const kill = (signal: NodeJS.Signals) => {
|
|
68
|
+
try {
|
|
69
|
+
if (grouped && child.pid) process.kill(-child.pid, signal);
|
|
70
|
+
else child.kill(signal);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if ((error as NodeJS.ErrnoException).code !== "ESRCH") failure ??= error as Error;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const stop = () => {
|
|
76
|
+
kill("SIGTERM");
|
|
77
|
+
timer ??= setTimeout(() => kill("SIGKILL"), 1000);
|
|
78
|
+
};
|
|
79
|
+
const abort = () => stop();
|
|
80
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
81
|
+
if (options.signal.aborted) abort();
|
|
82
|
+
|
|
83
|
+
const line = (text: string) => {
|
|
84
|
+
if (!text.trim()) return;
|
|
85
|
+
let event;
|
|
86
|
+
try { event = JSON.parse(text); }
|
|
87
|
+
catch { failure ??= new Error("Invalid JSON from subagent stdout"); stop(); return; }
|
|
88
|
+
if (event.type === "message_end" && event.message) {
|
|
89
|
+
const message = event.message;
|
|
90
|
+
if (message.role === "assistant" || message.role === "toolResult") addUsage(result.usage, message.usage);
|
|
91
|
+
if (message.role === "assistant") {
|
|
92
|
+
result.output = (Array.isArray(message.content) ? message.content : [])
|
|
93
|
+
.filter((part: { type: string }) => part.type === "text")
|
|
94
|
+
.map((part: { text: string }) => part.text).join("\n");
|
|
95
|
+
if (result.output.length > 1_000_000) result.output = `${result.output.slice(0, 1_000_000)}\n[Output exceeded the 1 MB retention limit.]`;
|
|
96
|
+
result.stopReason = message.stopReason;
|
|
97
|
+
result.error = message.errorMessage;
|
|
98
|
+
options.onProgress?.(limitOutput(result.output, 2000) || "Working…");
|
|
99
|
+
}
|
|
100
|
+
} else if (event.type === "tool_execution_start") {
|
|
101
|
+
options.onProgress?.(`Running ${event.toolName}…`);
|
|
102
|
+
} else if (event.type === "agent_settled" && event.aborted) {
|
|
103
|
+
result.stopReason = "aborted";
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
child.stdout.setEncoding("utf8");
|
|
107
|
+
child.stdout.on("data", (data: string) => {
|
|
108
|
+
if (failure) return;
|
|
109
|
+
bytes += Buffer.byteLength(data);
|
|
110
|
+
buffer += data;
|
|
111
|
+
if (bytes > 64 * 1024 * 1024 || buffer.length > 8 * 1024 * 1024) {
|
|
112
|
+
failure = new Error("Subagent event stream exceeded its limit"); stop(); return;
|
|
113
|
+
}
|
|
114
|
+
let end: number;
|
|
115
|
+
while ((end = buffer.indexOf("\n")) >= 0) {
|
|
116
|
+
line(buffer.slice(0, end));
|
|
117
|
+
buffer = buffer.slice(end + 1);
|
|
118
|
+
if (failure) break;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
child.stderr.setEncoding("utf8");
|
|
122
|
+
child.stderr.on("data", (text: string) => { result.stderr = (result.stderr + text).slice(-32_000); });
|
|
123
|
+
child.stdin.on("error", (error: NodeJS.ErrnoException) => {
|
|
124
|
+
if (error.code !== "EPIPE") { failure = error; stop(); }
|
|
125
|
+
});
|
|
126
|
+
child.on("error", (error) => { failure = error; });
|
|
127
|
+
child.on("close", (code, signal) => {
|
|
128
|
+
closed = true;
|
|
129
|
+
options.signal.removeEventListener("abort", abort);
|
|
130
|
+
if (!failure && buffer.trim()) line(buffer);
|
|
131
|
+
if (timer) clearTimeout(timer);
|
|
132
|
+
// A root cancellation must also terminate descendants that outlive the Pi process.
|
|
133
|
+
if (grouped && (options.signal.aborted || failure || code !== 0 || signal)) kill("SIGKILL");
|
|
134
|
+
if (options.signal.aborted) reject(new Error("Subagent aborted", { cause: options.signal.reason }));
|
|
135
|
+
else if (failure) reject(failure);
|
|
136
|
+
else {
|
|
137
|
+
result.exitCode = code ?? 1;
|
|
138
|
+
if (signal) result.error ??= `Subagent terminated by ${signal}`;
|
|
139
|
+
if (!result.output && !result.error) result.error = result.stderr || "Subagent exited without a final text response";
|
|
140
|
+
resolve(result);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// stdin avoids command-line size limits and keeps delegated context out of process arguments.
|
|
144
|
+
if (!closed) child.stdin.end(`Task:\n${options.task}\n`);
|
|
145
|
+
});
|
|
146
|
+
}
|
package/lib/ui-tips.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "category": "Getting started", "text": "Use /init to initialize project guidance in AGENTS.md." },
|
|
3
|
+
{ "category": "Coding", "text": "For ponytail, say: \"Use ponytail: make the smallest complete change.\"" },
|
|
4
|
+
{ "category": "Style", "text": "For caveman, say: \"Be brief\" or \"caveman mode\"; say \"normal mode\" to stop." },
|
|
5
|
+
{ "category": "Planning", "text": "For grilling, ask: \"Turn this idea into a spec and clarify the unknowns.\"" },
|
|
6
|
+
{ "category": "Coding", "text": "For lean-build, ask: \"Implement this feature with these acceptance criteria.\"" },
|
|
7
|
+
{ "category": "Coding", "text": "For diagnosing-bugs, ask: \"Find the cause of this bug before fixing it.\"" },
|
|
8
|
+
{ "category": "Coding", "text": "For code-review, ask: \"Review this diff for correctness.\"" },
|
|
9
|
+
{ "category": "Coding", "text": "For safe-refactor, ask: \"Refactor this code without changing its behavior.\"" },
|
|
10
|
+
{ "category": "Coding", "text": "For tdd, ask: \"Use test-first development: red, green, refactor.\"" },
|
|
11
|
+
{ "category": "Coding", "text": "For verify-and-stop, ask: \"Verify this work against the acceptance criteria; don't expand scope.\"" },
|
|
12
|
+
{ "category": "Planning", "text": "For codebase-design, ask: \"Design module boundaries and interfaces for this service.\"" },
|
|
13
|
+
{ "category": "Coding", "text": "For migration, ask: \"Implement this migration with compatibility checks and rollback.\"" },
|
|
14
|
+
{ "category": "Design & integrations", "text": "For frontend-design, ask: \"Design a responsive, accessible web interface.\"" },
|
|
15
|
+
{ "category": "Design & integrations", "text": "For webapp-testing, ask: \"Test this local web app with Playwright.\"" },
|
|
16
|
+
{ "category": "Documents", "text": "For docx, ask: \"Create a Word document with headings and a table of contents.\"" },
|
|
17
|
+
{ "category": "Documents", "text": "For pdf, ask: \"Extract the text and tables from this PDF.\"" },
|
|
18
|
+
{ "category": "Documents", "text": "For xlsx, ask: \"Create an Excel spreadsheet with these formulas.\"" },
|
|
19
|
+
{ "category": "Documents", "text": "For pptx, ask: \"Create a presentation deck from this outline.\"" },
|
|
20
|
+
{ "category": "Design & integrations", "text": "For mcp-builder, ask: \"Build an MCP server for this API.\"" },
|
|
21
|
+
{ "category": "Planning", "text": "For writing-for-agents, ask: \"Improve the routing and structure of this AGENTS.md.\"" },
|
|
22
|
+
{ "category": "Getting started", "text": "For find-skills, ask: \"Find an installable skill for this task.\"" },
|
|
23
|
+
{ "category": "Workflow", "text": "Use /summary for a concise summary of work, decisions, and next steps." },
|
|
24
|
+
{ "category": "Workflow", "text": "Use /commit to write a commit message from your Git changes; it does not commit them." },
|
|
25
|
+
{ "category": "Planning", "text": "Name relevant files, constraints, and expected behavior in your prompt." },
|
|
26
|
+
{ "category": "Getting started", "text": "Choose Enable if grep/find/ls setup is offered to save your project preference." },
|
|
27
|
+
{ "category": "Planning", "text": "Give acceptance criteria when asking for a feature so the agent knows when to stop." },
|
|
28
|
+
{ "category": "Getting started", "text": "Use /help to discover Pi commands and keyboard shortcuts." },
|
|
29
|
+
{ "category": "Getting started", "text": "Use /reload after updating PieXis resources." },
|
|
30
|
+
{ "category": "Interaction", "text": "In AskQuestion, use arrows to navigate, Space to toggle, Enter to confirm, and Esc to cancel." },
|
|
31
|
+
{ "category": "Interaction", "text": "In AskQuestion, Tab revisits related questions without losing your drafts." },
|
|
32
|
+
{ "category": "Session stats", "text": "Footer: the chat bubble counts user messages across the whole session." },
|
|
33
|
+
{ "category": "Session stats", "text": "Footer: the lightning icon shows the cache-hit percentage for the current branch." },
|
|
34
|
+
{ "category": "Session stats", "text": "Footer: cost is an estimate, not your provider's billing total." },
|
|
35
|
+
{ "category": "Session stats", "text": "Context: the bar turns yellow at 70% and red at 90%." },
|
|
36
|
+
{ "category": "Session stats", "text": "Token and cost totals follow the current conversation branch." },
|
|
37
|
+
{ "category": "Getting started", "text": "Tips change on startup or reload, not on every screen redraw." },
|
|
38
|
+
{ "category": "Getting started", "text": "Describe your task naturally; matching available skills can be selected automatically, but selection is not guaranteed." },
|
|
39
|
+
{ "category": "Getting started", "text": "Say \"use the code-review skill\" to request a skill by name without a slash command." },
|
|
40
|
+
{ "category": "Getting started", "text": "Only skills marked disable-model-invocation: true require explicit commands; none bundled here have that flag." }
|
|
41
|
+
]
|