pi-goal-x 0.15.1 → 0.16.1
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.
|
@@ -202,7 +202,8 @@ export function validateTaskSkip(args: {
|
|
|
202
202
|
const task = args.goal.taskList.tasks.find((t) => t.id === args.taskId);
|
|
203
203
|
if (!task) return { ok: false, message: `Task "${args.taskId}" not found.` };
|
|
204
204
|
if (task.status === "complete") return { ok: false, message: `Task "${args.taskId}" is already complete.` };
|
|
205
|
-
|
|
205
|
+
// Skipped tasks toggle via the executor; reason is only required for first-time skips.
|
|
206
|
+
if (task.status === "skipped") return { ok: true };
|
|
206
207
|
if (!args.reason.trim()) return { ok: false, message: "skip_task requires a non-empty reason." };
|
|
207
208
|
return { ok: true };
|
|
208
209
|
}
|
package/extensions/goal.ts
CHANGED
|
@@ -105,6 +105,7 @@ import {
|
|
|
105
105
|
} from "./prompts/goal-prompts.ts";
|
|
106
106
|
import { buildGoalRunningNotification } from "./widgets/goal-notifications.ts";
|
|
107
107
|
import { GoalWidgetComponent, type AuditorWidgetProgress } from "./widgets/goal-widget.ts";
|
|
108
|
+
import { showEscapeDialog, type EscapeDialogResult } from "./widgets/goal-escape-dialog.ts";
|
|
108
109
|
|
|
109
110
|
import {
|
|
110
111
|
abortGoalCommandMessage,
|
|
@@ -407,6 +408,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
407
408
|
let auditProgress: AuditorWidgetProgress | null = null;
|
|
408
409
|
let auditAnimationTimer: ReturnType<typeof setInterval> | null = null;
|
|
409
410
|
let auditAbortController: AbortController | null = null;
|
|
411
|
+
let showingEscapeDialog = false;
|
|
410
412
|
|
|
411
413
|
|
|
412
414
|
// Per-turn flags reset in turn_start (#4, C9 fix).
|
|
@@ -959,6 +961,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
959
961
|
// Must return { consume: true } so the TUI doesn't also process the key
|
|
960
962
|
// and abort the running tool execution, which would cascade into pausing
|
|
961
963
|
// the entire goal (agent_end sees ctx.signal?.aborted and calls pauseActiveGoal).
|
|
964
|
+
if (showingEscapeDialog) return undefined;
|
|
962
965
|
if (matchesKey(data, "escape") && auditProgress) {
|
|
963
966
|
abortAudit(ctx);
|
|
964
967
|
return { consume: true };
|
|
@@ -2192,41 +2195,88 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
2192
2195
|
// Clear auditor progress display
|
|
2193
2196
|
stopAuditAnimation();
|
|
2194
2197
|
|
|
2195
|
-
// If the audit was aborted by the user (Esc),
|
|
2196
|
-
//
|
|
2198
|
+
// If the audit was aborted by the user (Esc), show a TUI dialog letting
|
|
2199
|
+
// the user choose: mark complete without audit, or continue working.
|
|
2197
2200
|
if (auditor.error === "Auditor aborted.") {
|
|
2198
|
-
pi.sendMessage<GoalAuditEventDetails>({
|
|
2199
|
-
customType: GOAL_AUDIT_ENTRY,
|
|
2200
|
-
content: [
|
|
2201
|
-
`Goal audit skipped — auditor bypassed (user pressed Escape during audit).`,
|
|
2202
|
-
`Goal remains ${statusLabel(auditTarget)}.`,
|
|
2203
|
-
"",
|
|
2204
|
-
"Use goal_question to ask the user whether to:",
|
|
2205
|
-
" - Mark the goal complete anyway (call complete_goal again)",
|
|
2206
|
-
" - Give feedback on the work so far",
|
|
2207
|
-
" - Continue working toward the goal",
|
|
2208
|
-
].join("\n"),
|
|
2209
|
-
display: true,
|
|
2210
|
-
details: { phase: "skipped", goalId: auditTarget.id, auditor: auditorLabel },
|
|
2211
|
-
});
|
|
2212
2201
|
auditProgress = null;
|
|
2213
2202
|
goalWidgetComponent?.invalidate();
|
|
2214
|
-
syncGoalTools();
|
|
2215
2203
|
updateUI(ctx);
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2204
|
+
|
|
2205
|
+
showingEscapeDialog = true;
|
|
2206
|
+
const userChoice: EscapeDialogResult = await showEscapeDialog(ctx, auditTarget.objective);
|
|
2207
|
+
showingEscapeDialog = false;
|
|
2208
|
+
|
|
2209
|
+
if (userChoice === "complete_without_audit") {
|
|
2210
|
+
// ── Mark complete without audit ────────────────────────────
|
|
2211
|
+
pi.sendMessage<GoalAuditEventDetails>({
|
|
2212
|
+
customType: GOAL_AUDIT_ENTRY,
|
|
2213
|
+
content: `Goal completed — user bypassed audit via Escape.`,
|
|
2214
|
+
display: true,
|
|
2215
|
+
details: { phase: "skipped", goalId: auditTarget.id, auditor: auditorLabel },
|
|
2216
|
+
});
|
|
2217
|
+
try {
|
|
2218
|
+
appendGoalEvent(ctx, {
|
|
2219
|
+
type: "audit_skipped",
|
|
2220
|
+
goalId: auditTarget.id,
|
|
2221
|
+
reason: "user_aborted",
|
|
2222
|
+
provider: settings.provider,
|
|
2223
|
+
model: settings.model,
|
|
2224
|
+
thinkingLevel: settings.thinkingLevel,
|
|
2225
|
+
at: nowIso(),
|
|
2226
|
+
});
|
|
2227
|
+
} catch {
|
|
2228
|
+
// Ledger append failure should not block completion
|
|
2229
|
+
}
|
|
2230
|
+
// Set goal complete in memory (defer archival to turn_end)
|
|
2231
|
+
accountProgress(ctx);
|
|
2232
|
+
state.goal = {
|
|
2233
|
+
...auditTarget,
|
|
2234
|
+
status: "complete",
|
|
2235
|
+
stopReason: "agent",
|
|
2236
|
+
updatedAt: nowIso(),
|
|
2237
|
+
};
|
|
2238
|
+
state.goal = writeActiveGoalFile(ctx, state.goal);
|
|
2239
|
+
pi.appendEntry(STATE_ENTRY, goalDetails(state.goal));
|
|
2240
|
+
turnStoppedFor = state.goal?.id ?? null;
|
|
2241
|
+
resetGetGoalNudgeState(state.goal?.id);
|
|
2242
|
+
syncGoalTools();
|
|
2243
|
+
updateUI(ctx);
|
|
2244
|
+
return {
|
|
2245
|
+
content: [{
|
|
2246
|
+
type: "text",
|
|
2247
|
+
text: [
|
|
2248
|
+
"User chose to mark the goal complete (bypassed audit via Escape).",
|
|
2249
|
+
"",
|
|
2250
|
+
"The goal is complete. Provide a final summary of what was accomplished.",
|
|
2251
|
+
].join("\n"),
|
|
2252
|
+
}],
|
|
2253
|
+
details: goalDetails(state.goal),
|
|
2254
|
+
};
|
|
2255
|
+
} else {
|
|
2256
|
+
// ── Continue working ────────────────────────────────────
|
|
2257
|
+
pi.sendMessage<GoalAuditEventDetails>({
|
|
2258
|
+
customType: GOAL_AUDIT_ENTRY,
|
|
2259
|
+
content: [
|
|
2260
|
+
`Goal audit skipped — user chose to continue working.`,
|
|
2261
|
+
`Goal remains ${statusLabel(auditTarget)}.`,
|
|
2226
2262
|
].join("\n"),
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2263
|
+
display: true,
|
|
2264
|
+
details: { phase: "skipped", goalId: auditTarget.id, auditor: auditorLabel },
|
|
2265
|
+
});
|
|
2266
|
+
syncGoalTools();
|
|
2267
|
+
updateUI(ctx);
|
|
2268
|
+
return {
|
|
2269
|
+
content: [{
|
|
2270
|
+
type: "text",
|
|
2271
|
+
text: [
|
|
2272
|
+
"User chose to continue working (bypassed audit via Escape).",
|
|
2273
|
+
"",
|
|
2274
|
+
"Resume working toward the goal.",
|
|
2275
|
+
].join("\n"),
|
|
2276
|
+
}],
|
|
2277
|
+
details: goalDetails(state.goal),
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2230
2280
|
}
|
|
2231
2281
|
|
|
2232
2282
|
// Show final audit output briefly before clearing
|
|
@@ -2505,6 +2555,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
2505
2555
|
promptGuidelines: [
|
|
2506
2556
|
"Use propose_task_list after a goal is confirmed, on the first continuation turn, if the objective naturally decomposes into trackable milestones.",
|
|
2507
2557
|
"Do not add a task list for simple, single-step goals.",
|
|
2558
|
+
"If a task list already exists on the goal, only call propose_task_list to restructure it when (a) the user explicitly asks for a change, or (b) the goal objective or requirements have structurally changed. Do not restructure the task list autonomously.",
|
|
2508
2559
|
"Existing tasks with matching IDs preserve their status/evidence/timestamps; new IDs start as pending; removed IDs are gone.",
|
|
2509
2560
|
"After confirmation the turn stops; the next continuation will arrive automatically.",
|
|
2510
2561
|
"You may optionally specify a verificationContract per task to define what verification evidence is required before completing that task.",
|
|
@@ -2761,9 +2812,12 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
2761
2812
|
label: "Skip Task",
|
|
2762
2813
|
description: "Skip a pending task in the current goal's task list. Does NOT stop the turn — the agent can continue working.",
|
|
2763
2814
|
promptSnippet: "Skip a task with a reason. Does not stop the turn.",
|
|
2764
|
-
|
|
2815
|
+
promptGuidelines: [
|
|
2765
2816
|
"Use skip_task to mark a task as skipped with a required reason.",
|
|
2766
2817
|
"The turn does NOT stop after skip_task — you may continue with other work.",
|
|
2818
|
+
"Only skip a task when the user explicitly asks you to, or when the task directly contradicts a hard constraint (e.g. an impossible requirement).",
|
|
2819
|
+
"Do NOT autonomously skip tasks to avoid work, or because they look optional, inconvenient, or out of scope. When in doubt, ask the user first.",
|
|
2820
|
+
"If skip_task is called on an already-skipped task, it toggles back to pending (unskip).",
|
|
2767
2821
|
],
|
|
2768
2822
|
parameters: Type.Object({
|
|
2769
2823
|
taskId: Type.String({ description: "Task id to skip" }),
|
|
@@ -2787,8 +2841,15 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
2787
2841
|
}
|
|
2788
2842
|
if (!state.goal?.taskList) throw new Error("Task list disappeared during task skip.");
|
|
2789
2843
|
const now = nowIso();
|
|
2844
|
+
const wasAlreadySkipped = findTaskInTree(state.goal.taskList.tasks, params.taskId)?.status === "skipped";
|
|
2845
|
+
|
|
2790
2846
|
const updatedTasks = updateTaskInTree(state.goal.taskList.tasks, params.taskId, (t) => {
|
|
2791
|
-
|
|
2847
|
+
if (t.status === "skipped") {
|
|
2848
|
+
// Toggle back to pending — clear skip state, do NOT cascade to subtasks
|
|
2849
|
+
const { skippedAt, skipReason, ...rest } = t;
|
|
2850
|
+
return { ...rest, status: "pending" as const };
|
|
2851
|
+
}
|
|
2852
|
+
// First-time skip: cascade to all subtasks (full subtasks only)
|
|
2792
2853
|
const base = { ...t, status: "skipped" as const, skippedAt: now, skipReason: params.reason.trim() };
|
|
2793
2854
|
if (t.subtasks && t.subtasks.length > 0 && !t.lightweightSubtasks) {
|
|
2794
2855
|
return skipAllSubtasks(base, now, params.reason.trim());
|
|
@@ -2806,22 +2867,33 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
2806
2867
|
syncGoalTools();
|
|
2807
2868
|
updateUI(ctx);
|
|
2808
2869
|
|
|
2809
|
-
|
|
2870
|
+
// Append ledger event
|
|
2810
2871
|
try {
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2872
|
+
if (wasAlreadySkipped) {
|
|
2873
|
+
appendGoalEvent(ctx, {
|
|
2874
|
+
type: "task_skipped",
|
|
2875
|
+
goalId: state.goal.id,
|
|
2876
|
+
taskId: params.taskId,
|
|
2877
|
+
reason: "unskipped (toggle via skip_task)",
|
|
2878
|
+
at: now,
|
|
2879
|
+
});
|
|
2880
|
+
} else {
|
|
2881
|
+
appendGoalEvent(ctx, {
|
|
2882
|
+
type: "task_skipped",
|
|
2883
|
+
goalId: state.goal.id,
|
|
2884
|
+
taskId: params.taskId,
|
|
2885
|
+
reason: params.reason.trim(),
|
|
2886
|
+
at: now,
|
|
2887
|
+
});
|
|
2888
|
+
}
|
|
2818
2889
|
} catch {
|
|
2819
2890
|
// Ledger failure should not block task skip
|
|
2820
2891
|
}
|
|
2821
2892
|
|
|
2822
2893
|
const taskSummary = buildTaskSummary(state.goal.taskList!);
|
|
2894
|
+
const action = wasAlreadySkipped ? "unsikpped" : "skipped";
|
|
2823
2895
|
return {
|
|
2824
|
-
content: [{ type: "text", text: `${params.taskId}
|
|
2896
|
+
content: [{ type: "text", text: `${params.taskId} ${action}. ${taskSummary}.` }],
|
|
2825
2897
|
details: goalDetails(state.goal),
|
|
2826
2898
|
};
|
|
2827
2899
|
},
|
|
@@ -130,17 +130,19 @@ ${untrustedObjectiveBlock(goal)}
|
|
|
130
130
|
|
|
131
131
|
Available work tools for pursuing the active goal include write, read, bash, and edit. Use those tools directly for file and shell work; do not call get_goal repeatedly to discover tools.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
After goal confirmation, you may call propose_task_list once to set up an initial task list if the objective decomposes into trackable milestones. If a task list already exists, only restructure it when the user asks or the goal structurally changes — do not restructure autonomously. Do not add a task list for simple, single-step goals.
|
|
134
134
|
|
|
135
135
|
To ask the user a structured question (e.g. when the user's spec changes and you need to clarify before updating the goal), use goal_question. It opens a question dialog and returns the user's answer as tool output. Use plain conversation for simple clarifications.
|
|
136
136
|
|
|
137
|
+
Task skipping restrictions: Only skip a task when the user explicitly asks you to, or when the task directly contradicts a hard constraint (e.g. an impossible requirement). Do NOT autonomously skip tasks to avoid work, or because they look optional, inconvenient, or out of scope. When in doubt, ask the user first. Calling skip_task on an already-skipped task toggles it back to pending (unskip).
|
|
138
|
+
|
|
137
139
|
Keep this goal in force until it is actually achieved. Do not pause for confirmation just because a phase, chapter, file, or checklist item is finished. At each natural stopping point, compare every explicit requirement with concrete evidence from the workspace/session. If the objective is complete, call complete_goal with status=complete and provide a verificationSummary; complete_goal will launch an independent pi auditor agent and only archive if that auditor returns <approved/>. If it is not complete, choose the next concrete action and do it.
|
|
138
140
|
|
|
139
141
|
The completion auditor is independent and semantic, not a paperwork checklist. It may inspect files and command output, and it will reject scaffold-only, alpha, template, proxy-metric, or weakly verified completions with <disapproved/>.
|
|
140
142
|
|
|
141
143
|
Before marking any sub-item as complete (including ✅ checkmarks in your output), verify thoroughly against the goal's success criteria and any verification contract. Only mark items as done when you have concrete evidence — not intent or partial progress.
|
|
142
144
|
|
|
143
|
-
If the user presses Escape
|
|
145
|
+
If the user presses Escape during a completion audit, a TUI dialog appears with "Mark complete without audit" or "Continue working". You will receive a structured message with the user's choice.
|
|
144
146
|
|
|
145
147
|
If you hit a real blocker that you cannot resolve with one more reasonable next step (missing credentials, contradictory spec, file/permission you cannot access, dangerous operation pending user approval, or an unclear Sisyphus-style ordered plan), the CORRECT action is to call pause_goal({reason, suggestedAction?}) with a structured, non-empty reason. pause_goal IS the channel for handing control back to the user — do not substitute a conversational "blocked, please help" summary in your final message and skip the tool call. Without pause_goal, the goal stays "active" and the UI cannot show the blocker. After pause_goal returns, you may add one short user-facing summary, but the tool call comes first.
|
|
146
148
|
|
|
@@ -168,7 +170,9 @@ export function continuationPrompt(goal: GoalRecord, settings?: GoalSettings): s
|
|
|
168
170
|
"",
|
|
169
171
|
"Available work tools for pursuing the active goal include write, read, bash, and edit. Use those tools directly for file and shell work; do not call get_goal repeatedly to discover tools.",
|
|
170
172
|
"",
|
|
171
|
-
|
|
173
|
+
"To ask the user a structured question (e.g. when the user's spec changes and you need to clarify before updating the goal), use goal_question. It opens a question dialog and returns the user's answer as tool output. Use plain conversation for simple clarifications.",
|
|
174
|
+
"",
|
|
175
|
+
"Task skipping restrictions: Only skip a task when the user explicitly asks you to, or when the task directly contradicts a hard constraint (e.g. an impossible requirement). Do NOT autonomously skip tasks to avoid work, or because they look optional, inconvenient, or out of scope. When in doubt, ask the user first. Calling skip_task on an already-skipped task toggles it back to pending (unskip).",
|
|
172
176
|
"",
|
|
173
177
|
"Avoid repeating work that is already done. Choose the next concrete action toward the objective.",
|
|
174
178
|
"",
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Result of the Escape dialog during audit.
|
|
7
|
+
*/
|
|
8
|
+
export type EscapeDialogResult = "complete_without_audit" | "continue_working";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Show a TUI confirmation dialog when the user presses Escape during a completion audit.
|
|
12
|
+
*
|
|
13
|
+
* Presents two choices:
|
|
14
|
+
* - Mark complete without audit (skips auditor, marks goal complete immediately)
|
|
15
|
+
* - Continue working (returns to agent, goal stays active)
|
|
16
|
+
*
|
|
17
|
+
* Returns the user's choice. Escape or Enter on a focused option submits the selection.
|
|
18
|
+
*/
|
|
19
|
+
export async function showEscapeDialog(
|
|
20
|
+
ctx: ExtensionContext,
|
|
21
|
+
goalObjective: string,
|
|
22
|
+
): Promise<EscapeDialogResult> {
|
|
23
|
+
if (!ctx.hasUI) {
|
|
24
|
+
// Fallback for headless/RPC mode — return "continue" as the safe default
|
|
25
|
+
return "continue_working";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return await ctx.ui.custom<EscapeDialogResult>(
|
|
29
|
+
(tui: TUI, theme: Theme, _keybindings: unknown, done: (result: EscapeDialogResult) => void): Component => {
|
|
30
|
+
const wasHardwareCursorShown = tui.getShowHardwareCursor();
|
|
31
|
+
tui.setShowHardwareCursor(false);
|
|
32
|
+
|
|
33
|
+
let selectedIndex = 1; // Default: "Continue working" (index 1)
|
|
34
|
+
let cancelled = false;
|
|
35
|
+
|
|
36
|
+
const OPTIONS: Array<{ label: string; value: EscapeDialogResult; description: string }> = [
|
|
37
|
+
{
|
|
38
|
+
label: "Mark complete without audit",
|
|
39
|
+
value: "complete_without_audit",
|
|
40
|
+
description: "Bypass the auditor and mark the goal complete now.",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "Continue working",
|
|
44
|
+
value: "continue_working",
|
|
45
|
+
description: "Resume work on the goal. The audit will not run this turn.",
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const accent = (s: string) => theme.fg("accent", s);
|
|
50
|
+
const dim = (s: string) => theme.fg("dim", s);
|
|
51
|
+
const warning = (s: string) => theme.fg("warning", s);
|
|
52
|
+
|
|
53
|
+
// ── Component ────────────────────────────────────────────────────
|
|
54
|
+
const component: Component & { dispose?(): void } = {
|
|
55
|
+
dispose() {
|
|
56
|
+
tui.setShowHardwareCursor(wasHardwareCursorShown);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
invalidate(): void {
|
|
60
|
+
// No cached state to invalidate
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
render(width: number): string[] {
|
|
64
|
+
const termWidth = Math.min(width, 80);
|
|
65
|
+
const innerWidth = Math.min(termWidth, 64) - 2; // inner content width between ││
|
|
66
|
+
|
|
67
|
+
/** Build a bordered line: fits exactly `innerWidth` visible chars between ││ */
|
|
68
|
+
function line(leftContent: string): string {
|
|
69
|
+
const vis = visibleWidth(leftContent);
|
|
70
|
+
const fill = innerWidth - vis;
|
|
71
|
+
return accent("│") + leftContent + (fill > 0 ? " ".repeat(fill) : "") + accent("│");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const horizLine = "─".repeat(innerWidth);
|
|
75
|
+
const lines: string[] = [];
|
|
76
|
+
const p = " "; // left padding inside the border
|
|
77
|
+
|
|
78
|
+
// ── Header ────────────────────────────────────────────────
|
|
79
|
+
lines.push(accent(`┌${horizLine}┐`));
|
|
80
|
+
lines.push(line(p + theme.bold("Audit interrupted by Escape") + dim(" (continue = default)")));
|
|
81
|
+
const truncatedObjective = truncateToWidth(goalObjective, innerWidth - 14, "…");
|
|
82
|
+
lines.push(line(p + dim("Goal: ") + dim(truncatedObjective)));
|
|
83
|
+
lines.push(accent(`├${horizLine}┤`));
|
|
84
|
+
|
|
85
|
+
// ── Options ─────────────────────────────────────────────
|
|
86
|
+
OPTIONS.forEach((opt, i) => {
|
|
87
|
+
const isSelected = i === selectedIndex && !cancelled;
|
|
88
|
+
const marker = isSelected ? "▸ " : " ";
|
|
89
|
+
const label = isSelected ? warning(opt.label) : opt.label;
|
|
90
|
+
const truncLabel = truncateToWidth(label, innerWidth - 6, "…");
|
|
91
|
+
lines.push(line(p + marker + truncLabel));
|
|
92
|
+
if (isSelected && opt.description) {
|
|
93
|
+
const desc = dim(opt.description);
|
|
94
|
+
const truncDesc = truncateToWidth(desc, innerWidth - 10, "…");
|
|
95
|
+
lines.push(line(p + " ".repeat(4) + truncDesc));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ── Footer ───────────────────────────────────────────────
|
|
100
|
+
lines.push(accent(`├${horizLine}┤`));
|
|
101
|
+
const footerText = dim("Enter to select · ↑↓ to navigate · Esc = continue working");
|
|
102
|
+
const truncFooter = truncateToWidth(footerText, innerWidth - 2, "…");
|
|
103
|
+
lines.push(line(p + truncFooter));
|
|
104
|
+
lines.push(accent(`└${horizLine}┘`));
|
|
105
|
+
|
|
106
|
+
return lines;
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
handleInput(data: string): void {
|
|
110
|
+
if (matchesKey(data, "up")) {
|
|
111
|
+
selectedIndex = (selectedIndex - 1 + OPTIONS.length) % OPTIONS.length;
|
|
112
|
+
tui.requestRender();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (matchesKey(data, "down")) {
|
|
116
|
+
selectedIndex = (selectedIndex + 1) % OPTIONS.length;
|
|
117
|
+
tui.requestRender();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (matchesKey(data, "enter")) {
|
|
121
|
+
cancelled = false;
|
|
122
|
+
done(OPTIONS[selectedIndex].value);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (matchesKey(data, "escape")) {
|
|
126
|
+
cancelled = true;
|
|
127
|
+
done("continue_working");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return component;
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
overlay: true,
|
|
137
|
+
overlayOptions: {
|
|
138
|
+
anchor: "center",
|
|
139
|
+
width: "70%",
|
|
140
|
+
minWidth: 50,
|
|
141
|
+
maxHeight: "50%",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-x",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Goal mode extension for pi: persistent long-running objectives, /goal-set drafting, Sisyphus prompt style, autoContinue, and an above-editor status overlay. Fork of @capyup/pi-goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "pi-goal-x contributors",
|