pi-goal-x 0.18.2 → 0.18.4
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.
|
@@ -314,10 +314,13 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
314
314
|
* Wraps a pipe-prefixed line and prepends "│ " to continuation lines
|
|
315
315
|
* so wrapped content stays within the ASCII box.
|
|
316
316
|
*/
|
|
317
|
+
const PIPE_PREFIX = "│ ";
|
|
318
|
+
const PIPE_WIDTH = visibleWidth(PIPE_PREFIX);
|
|
317
319
|
const addWrappedPipe = (styledLine: string) => {
|
|
318
|
-
const
|
|
320
|
+
const wrapWidth = Math.max(1, safeWidth - PIPE_WIDTH);
|
|
321
|
+
const wrapped = wrapTextWithAnsi(styledLine, wrapWidth);
|
|
319
322
|
for (let i = 0; i < wrapped.length; i++) {
|
|
320
|
-
lines.push(i === 0 ? wrapped[i] :
|
|
323
|
+
lines.push(i === 0 ? wrapped[i] : PIPE_PREFIX + wrapped[i]);
|
|
321
324
|
}
|
|
322
325
|
};
|
|
323
326
|
|
|
@@ -488,6 +491,12 @@ export async function runGoalQuestionnaire(ctx: ExtensionContext, rawQuestions:
|
|
|
488
491
|
add(theme.fg("dim", isMulti ? " Tab/←→ navigate • ↑↓ select • Enter confirm • Esc cancel" + auditorHint : " ↑↓ navigate • Enter select • Esc cancel" + auditorHint));
|
|
489
492
|
}
|
|
490
493
|
add(theme.fg("accent", "─".repeat(safeWidth)));
|
|
494
|
+
// Safety net: ensure no returned line exceeds the terminal width
|
|
495
|
+
for (let i = 0; i < lines.length; i++) {
|
|
496
|
+
if (lines[i] && visibleWidth(lines[i]) > safeWidth) {
|
|
497
|
+
lines[i] = truncateToWidth(lines[i], safeWidth);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
491
500
|
cachedLines = lines;
|
|
492
501
|
return lines;
|
|
493
502
|
}
|
package/extensions/goal.ts
CHANGED
|
@@ -112,6 +112,7 @@ import {
|
|
|
112
112
|
import { buildGoalRunningNotification } from "./widgets/goal-notifications.ts";
|
|
113
113
|
import { GoalWidgetComponent, type AuditorWidgetProgress } from "./widgets/goal-widget.ts";
|
|
114
114
|
import { showEscapeDialog, type EscapeDialogResult } from "./widgets/goal-escape-dialog.ts";
|
|
115
|
+
import { showTaskListOverlay } from "./widgets/task-list-overlay.ts";
|
|
115
116
|
|
|
116
117
|
import {
|
|
117
118
|
abortGoalCommandMessage,
|
|
@@ -445,7 +446,12 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
445
446
|
|
|
446
447
|
function syncGoalTools(): void {
|
|
447
448
|
try {
|
|
448
|
-
const
|
|
449
|
+
const initialTools = pi.getActiveTools();
|
|
450
|
+
if (!Array.isArray(initialTools)) {
|
|
451
|
+
console.error("[pi-goal] syncGoalTools: pi.getActiveTools() did not return an array, got", typeof initialTools);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const active = new Set(initialTools);
|
|
449
455
|
for (const name of goalExecutionWorkTools) active.add(name);
|
|
450
456
|
active.delete(QUESTION_TOOL_NAME);
|
|
451
457
|
active.delete(QUESTIONNAIRE_TOOL_NAME);
|
|
@@ -468,7 +474,7 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
468
474
|
if (!state.goal || state.goal.status === "complete") {
|
|
469
475
|
active.delete(PROPOSE_TWEAK_TOOL_NAME);
|
|
470
476
|
}
|
|
471
|
-
|
|
477
|
+
|
|
472
478
|
// Keep the commit tool available and let its validator enforce that a
|
|
473
479
|
// drafting flow is active. This avoids fragile hidden-tool drift after
|
|
474
480
|
// question turns, compaction, or active-tool resync.
|
|
@@ -484,7 +490,9 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
484
490
|
active.add(QUESTIONNAIRE_TOOL_NAME);
|
|
485
491
|
}
|
|
486
492
|
pi.setActiveTools(Array.from(active));
|
|
487
|
-
} catch {
|
|
493
|
+
} catch (err) {
|
|
494
|
+
console.error("[pi-goal] syncGoalTools error:", err instanceof Error ? err.message : String(err));
|
|
495
|
+
}
|
|
488
496
|
}
|
|
489
497
|
|
|
490
498
|
function stopAuditAnimation(): void {
|
|
@@ -983,6 +991,12 @@ export default function goalExtension(pi: ExtensionAPI): void {
|
|
|
983
991
|
return { consume: true };
|
|
984
992
|
}
|
|
985
993
|
|
|
994
|
+
// Ctrl+Shift+T — show task list overlay for all open goals
|
|
995
|
+
if (matchesKey(data, "ctrl+shift+t")) {
|
|
996
|
+
showTaskListOverlay(ctx, goalsById, focusedGoalId);
|
|
997
|
+
return { consume: true };
|
|
998
|
+
}
|
|
999
|
+
|
|
986
1000
|
// ── Debug mode keybindings (hidden from normal view) ────────────────
|
|
987
1001
|
|
|
988
1002
|
// Ctrl+Shift+X — toggle debug mode on/off
|
|
@@ -77,7 +77,8 @@ export async function showEscapeDialog(
|
|
|
77
77
|
|
|
78
78
|
// ── Header ────────────────────────────────────────────────
|
|
79
79
|
lines.push(accent(`┌${horizLine}┐`));
|
|
80
|
-
|
|
80
|
+
const headerContent = p + theme.bold("Audit interrupted by Escape") + dim(" (continue = default)");
|
|
81
|
+
lines.push(line(truncateToWidth(headerContent, innerWidth, "…")));
|
|
81
82
|
const truncatedObjective = truncateToWidth(goalObjective, innerWidth - 14, "…");
|
|
82
83
|
lines.push(line(p + dim("Goal: ") + dim(truncatedObjective)));
|
|
83
84
|
lines.push(accent(`├${horizLine}┤`));
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } 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
|
+
import { displayObjectiveTitle, statusLabel } from "../goal-core.ts";
|
|
5
|
+
import { openGoalsFromPool } from "../goal-pool.ts";
|
|
6
|
+
import type { GoalRecord, GoalTask } from "../goal-record.ts";
|
|
7
|
+
|
|
8
|
+
// ── Structured line entries ──────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
type LineEntry =
|
|
11
|
+
| { type: "separator" }
|
|
12
|
+
| { type: "goal-header"; icon: string; title: string; status: string }
|
|
13
|
+
| { type: "task-summary"; text: string }
|
|
14
|
+
| { type: "task"; prefix: string; title: string }
|
|
15
|
+
| { type: "empty-message"; text: string };
|
|
16
|
+
|
|
17
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Show a scrollable modal overlay displaying the current goal's task list.
|
|
21
|
+
* Press 'a' to toggle between the current goal and all open goals.
|
|
22
|
+
* Triggered by Ctrl+Shift+T. Dismisses on Escape.
|
|
23
|
+
*/
|
|
24
|
+
export async function showTaskListOverlay(
|
|
25
|
+
ctx: ExtensionContext,
|
|
26
|
+
goalsById: Map<string, GoalRecord>,
|
|
27
|
+
focusedGoalId: string | null,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
if (!ctx.hasUI) return;
|
|
30
|
+
|
|
31
|
+
await ctx.ui.custom<void>(
|
|
32
|
+
(tui: TUI, theme: Theme, _keybindings: unknown, done: () => void): Component => {
|
|
33
|
+
// ── Theme helpers ─────────────────────────────────────────────
|
|
34
|
+
const accent = (s: string) => theme.fg("accent", s);
|
|
35
|
+
const dim = (s: string) => theme.fg("dim", s);
|
|
36
|
+
const success = (s: string) => theme.fg("success", s);
|
|
37
|
+
const muted = (s: string) => theme.fg("muted", s);
|
|
38
|
+
const bold = (s: string) => theme.bold(s);
|
|
39
|
+
|
|
40
|
+
// ── State ─────────────────────────────────────────────────────
|
|
41
|
+
let showAllGoals = false;
|
|
42
|
+
let entries: LineEntry[] = [];
|
|
43
|
+
let totalOpenGoals = 0;
|
|
44
|
+
let totalTaskCount = 0;
|
|
45
|
+
|
|
46
|
+
function rebuildEntries(): void {
|
|
47
|
+
entries = [];
|
|
48
|
+
totalOpenGoals = 0;
|
|
49
|
+
totalTaskCount = 0;
|
|
50
|
+
|
|
51
|
+
const openGoals = openGoalsFromPool(goalsById);
|
|
52
|
+
const targetGoals = showAllGoals
|
|
53
|
+
? openGoals
|
|
54
|
+
: openGoals.filter((g) => g.id === focusedGoalId);
|
|
55
|
+
|
|
56
|
+
if (targetGoals.length === 0) {
|
|
57
|
+
entries.push({ type: "empty-message", text: "No open goals to display." });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const [gIdx, goal] of targetGoals.entries()) {
|
|
62
|
+
if (goal.taskList) {
|
|
63
|
+
totalTaskCount += countAllTasks(goal.taskList.tasks);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (gIdx > 0) {
|
|
67
|
+
entries.push({ type: "separator" });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Goal header
|
|
71
|
+
const icon = goal.status === "paused" ? "⏸"
|
|
72
|
+
: goal.sisyphus ? "◆" : "●";
|
|
73
|
+
const title = displayObjectiveTitle(goal.objective);
|
|
74
|
+
const label = statusLabel(goal);
|
|
75
|
+
entries.push({ type: "goal-header", icon, title, status: label });
|
|
76
|
+
|
|
77
|
+
// Task summary & task list
|
|
78
|
+
if (goal.taskList && goal.taskList.tasks.length > 0) {
|
|
79
|
+
const { total, complete, skipped } = countAllWithStatus(goal.taskList.tasks);
|
|
80
|
+
const summary = `${complete}/${total} done${skipped > 0 ? ` (${skipped} skipped)` : ""}`;
|
|
81
|
+
entries.push({ type: "task-summary", text: summary });
|
|
82
|
+
|
|
83
|
+
const tasks = goal.taskList.tasks;
|
|
84
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
85
|
+
const isLast = i === tasks.length - 1;
|
|
86
|
+
collectTaskEntries(tasks[i], 1, isLast, entries);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
entries.push({ type: "empty-message", text: "(no tasks)" });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
totalOpenGoals++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Build initial entries
|
|
97
|
+
rebuildEntries();
|
|
98
|
+
|
|
99
|
+
// ── Render helpers ────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/** Render a single line entry into styled wrapped lines */
|
|
102
|
+
function renderEntry(entry: LineEntry, innerWidth: number): string[] {
|
|
103
|
+
switch (entry.type) {
|
|
104
|
+
case "separator":
|
|
105
|
+
return [dim("·")];
|
|
106
|
+
|
|
107
|
+
case "goal-header": {
|
|
108
|
+
const icon = entry.icon === "⏸" ? muted("⏸") : entry.icon === "◆" ? accent("◆") : accent("●");
|
|
109
|
+
const status = dim(entry.status);
|
|
110
|
+
const prefix = `${icon} ${bold("")}`; // placeholder, we'll build below
|
|
111
|
+
const rawPrefix = `${entry.icon} `; // raw width: icon + 2 spaces
|
|
112
|
+
const prefixWidth = visibleWidth(rawPrefix);
|
|
113
|
+
const available = innerWidth - prefixWidth;
|
|
114
|
+
const wrappedTitle = wrapTextWithAnsi(bold(entry.title), Math.max(1, available));
|
|
115
|
+
const lines: string[] = [];
|
|
116
|
+
wrappedTitle.forEach((segment, i) => {
|
|
117
|
+
if (i === 0) {
|
|
118
|
+
lines.push(`${icon} ${segment} ${status}`);
|
|
119
|
+
} else {
|
|
120
|
+
lines.push(` ${" ".repeat(rawPrefix.length - 3)}${segment}`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
// If title alone fits, status needs to be on same line
|
|
124
|
+
// But we already handle it above for i=0. The status might overflow.
|
|
125
|
+
// If status doesn't fit on first line after title, put it on its own line
|
|
126
|
+
const firstLine = lines[0];
|
|
127
|
+
const allOnFirst = `${icon} ${entry.title} ${entry.status}`;
|
|
128
|
+
if (visibleWidth(firstLine) > innerWidth) {
|
|
129
|
+
// Status overflowed — put status on a separate dim line
|
|
130
|
+
lines[0] = `${icon} ${wrappedTitle[0]}`;
|
|
131
|
+
if (wrappedTitle.length === 1) {
|
|
132
|
+
lines.push(` ${dim(entry.status)}`);
|
|
133
|
+
} else {
|
|
134
|
+
// insert status after last wrapped title segment
|
|
135
|
+
const lastLine = wrappedTitle[wrappedTitle.length - 1];
|
|
136
|
+
lines[lines.length - 1] = ` ${" ".repeat(rawPrefix.length - 3)}${lastLine}`;
|
|
137
|
+
lines.push(` ${dim(entry.status)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Re-truncate any over-long lines just in case
|
|
141
|
+
return lines.map((l) => visibleWidth(l) > innerWidth ? truncateToWidth(l, innerWidth, "…") : l);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case "task-summary": {
|
|
145
|
+
const content = dim(entry.text);
|
|
146
|
+
return [visibleWidth(content) > innerWidth ? truncateToWidth(content, innerWidth, "…") : content];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
case "task": {
|
|
150
|
+
const prefixWidth = visibleWidth(entry.prefix);
|
|
151
|
+
const available = innerWidth - prefixWidth;
|
|
152
|
+
const wrappedTitle = wrapTextWithAnsi(entry.title, Math.max(1, available));
|
|
153
|
+
const lines: string[] = [];
|
|
154
|
+
wrappedTitle.forEach((segment, i) => {
|
|
155
|
+
if (i === 0) {
|
|
156
|
+
lines.push(`${dim(entry.prefix)} ${segment}`);
|
|
157
|
+
} else {
|
|
158
|
+
lines.push(`${" ".repeat(prefixWidth + 1)}${segment}`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return lines.map((l) => visibleWidth(l) > innerWidth ? truncateToWidth(l, innerWidth, "…") : l);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case "empty-message": {
|
|
165
|
+
const content = dim(entry.text);
|
|
166
|
+
return [visibleWidth(content) > innerWidth ? truncateToWidth(content, innerWidth, "…") : content];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
default:
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Scroll state ──────────────────────────────────────────────
|
|
175
|
+
let scrollOffset = 0;
|
|
176
|
+
let lastRenderWidth = 80;
|
|
177
|
+
|
|
178
|
+
// The "logical line count" is the number of entries (before wrapping).
|
|
179
|
+
// We store rendered line count too since wrapped entries expand.
|
|
180
|
+
let renderedLineCount = 0;
|
|
181
|
+
|
|
182
|
+
function computeVisibleHeight(innerWidth: number): number {
|
|
183
|
+
return Math.max(8, Math.floor(innerWidth / 2.8));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const wasHardwareCursorShown = tui.getShowHardwareCursor();
|
|
187
|
+
tui.setShowHardwareCursor(false);
|
|
188
|
+
|
|
189
|
+
// ── Component ─────────────────────────────────────────────────
|
|
190
|
+
const component: Component & { dispose?(): void } = {
|
|
191
|
+
dispose() {
|
|
192
|
+
tui.setShowHardwareCursor(wasHardwareCursorShown);
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
invalidate(): void {},
|
|
196
|
+
|
|
197
|
+
render(width: number): string[] {
|
|
198
|
+
lastRenderWidth = width;
|
|
199
|
+
|
|
200
|
+
const termWidth = Math.min(width, 100);
|
|
201
|
+
const innerWidth = Math.min(termWidth, 90) - 2;
|
|
202
|
+
const p = " ";
|
|
203
|
+
|
|
204
|
+
function line(content: string): string {
|
|
205
|
+
const vis = visibleWidth(content);
|
|
206
|
+
const fill = innerWidth - vis;
|
|
207
|
+
return accent("│") + content + (fill > 0 ? " ".repeat(fill) : "") + accent("│");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Render all entries into flat text lines ────────────
|
|
211
|
+
const renderedLines: string[] = [];
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
const wrapped = renderEntry(entry, innerWidth);
|
|
214
|
+
renderedLines.push(...wrapped);
|
|
215
|
+
}
|
|
216
|
+
renderedLineCount = renderedLines.length;
|
|
217
|
+
|
|
218
|
+
const horiz = "─".repeat(innerWidth);
|
|
219
|
+
const out: string[] = [];
|
|
220
|
+
|
|
221
|
+
out.push(accent(`┌${horiz}┐`));
|
|
222
|
+
|
|
223
|
+
// Header
|
|
224
|
+
const viewLabel = showAllGoals
|
|
225
|
+
? `${totalOpenGoals} ${totalOpenGoals === 1 ? "goal" : "goals"}`
|
|
226
|
+
: "current goal";
|
|
227
|
+
const taskWord = totalTaskCount === 1 ? "task" : "tasks";
|
|
228
|
+
const h = bold(` Tasks (${viewLabel}, ${totalTaskCount} ${taskWord})`);
|
|
229
|
+
out.push(line(p + h));
|
|
230
|
+
out.push(accent(`├${horiz}┤`));
|
|
231
|
+
|
|
232
|
+
// Content with scrolling
|
|
233
|
+
const visibleHeight = computeVisibleHeight(innerWidth);
|
|
234
|
+
const maxOffset = Math.max(0, renderedLineCount - visibleHeight);
|
|
235
|
+
if (scrollOffset > maxOffset) scrollOffset = maxOffset;
|
|
236
|
+
|
|
237
|
+
const canScrollUp = scrollOffset > 0;
|
|
238
|
+
const canScrollDown = scrollOffset < maxOffset;
|
|
239
|
+
|
|
240
|
+
if (canScrollUp) {
|
|
241
|
+
out.push(line(p + dim(`▴ ${scrollOffset}/${renderedLineCount} lines`)));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const end = Math.min(scrollOffset + visibleHeight, renderedLineCount);
|
|
245
|
+
for (let i = scrollOffset; i < end; i++) {
|
|
246
|
+
const raw = renderedLines[i];
|
|
247
|
+
// Each rendered line from renderEntry already wraps/truncates,
|
|
248
|
+
// but double-check for safety
|
|
249
|
+
const safe = visibleWidth(raw) > innerWidth
|
|
250
|
+
? truncateToWidth(raw, innerWidth, "…")
|
|
251
|
+
: raw;
|
|
252
|
+
out.push(line(p + safe));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (canScrollDown) {
|
|
256
|
+
out.push(line(p + dim(`▾ ${renderedLineCount - end} more lines`)));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (renderedLineCount === 0) {
|
|
260
|
+
out.push(line(p + dim("No tasks to display.")));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
out.push(accent(`├${horiz}┤`));
|
|
264
|
+
const toggleHint = showAllGoals ? "show current" : "show all";
|
|
265
|
+
const footer = dim(`↑↓/jk scroll · 'a' to ${toggleHint} · Esc close`);
|
|
266
|
+
out.push(line(p + footer));
|
|
267
|
+
out.push(accent(`└${horiz}┘`));
|
|
268
|
+
|
|
269
|
+
return out;
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
handleInput(data: string): void {
|
|
273
|
+
const tw = Math.min(lastRenderWidth, 100);
|
|
274
|
+
const innerW = Math.min(tw, 90) - 2;
|
|
275
|
+
const visibleH = computeVisibleHeight(innerW);
|
|
276
|
+
const maxO = Math.max(0, renderedLineCount - visibleH);
|
|
277
|
+
|
|
278
|
+
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
279
|
+
scrollOffset = Math.max(0, scrollOffset - 1);
|
|
280
|
+
tui.requestRender();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (matchesKey(data, "down") || matchesKey(data, "j")) {
|
|
284
|
+
scrollOffset = Math.min(maxO, scrollOffset + 1);
|
|
285
|
+
tui.requestRender();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (matchesKey(data, "pageUp")) {
|
|
289
|
+
scrollOffset = Math.max(0, scrollOffset - visibleH);
|
|
290
|
+
tui.requestRender();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (matchesKey(data, "pageDown")) {
|
|
294
|
+
scrollOffset = Math.min(maxO, scrollOffset + visibleH);
|
|
295
|
+
tui.requestRender();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (matchesKey(data, "home")) {
|
|
299
|
+
scrollOffset = 0;
|
|
300
|
+
tui.requestRender();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (matchesKey(data, "end")) {
|
|
304
|
+
scrollOffset = maxO;
|
|
305
|
+
tui.requestRender();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
// Toggle view
|
|
309
|
+
if (matchesKey(data, "a")) {
|
|
310
|
+
showAllGoals = !showAllGoals;
|
|
311
|
+
rebuildEntries();
|
|
312
|
+
scrollOffset = 0;
|
|
313
|
+
tui.requestRender();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (matchesKey(data, "escape") || matchesKey(data, "enter")) {
|
|
317
|
+
done();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
return component;
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
overlay: true,
|
|
327
|
+
overlayOptions: {
|
|
328
|
+
anchor: "center",
|
|
329
|
+
width: "80%",
|
|
330
|
+
minWidth: 60,
|
|
331
|
+
maxHeight: "80%",
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── Task counting ─────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
function countAllTasks(tasks: GoalTask[]): number {
|
|
340
|
+
let n = 0;
|
|
341
|
+
for (const t of tasks) {
|
|
342
|
+
n += 1 + countAllTasks(t.subtasks ?? []);
|
|
343
|
+
}
|
|
344
|
+
return n;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function countAllWithStatus(tasks: GoalTask[]): { total: number; complete: number; skipped: number; pending: number } {
|
|
348
|
+
let total = 0;
|
|
349
|
+
let complete = 0;
|
|
350
|
+
let skipped = 0;
|
|
351
|
+
for (const t of tasks) {
|
|
352
|
+
total++;
|
|
353
|
+
if (t.status === "complete") complete++;
|
|
354
|
+
else if (t.status === "skipped") skipped++;
|
|
355
|
+
if (t.subtasks) {
|
|
356
|
+
const child = countAllWithStatus(t.subtasks);
|
|
357
|
+
total += child.total;
|
|
358
|
+
complete += child.complete;
|
|
359
|
+
skipped += child.skipped;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return { total, complete, skipped, pending: total - complete - skipped };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── Tree entry collection ─────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
const STATUS_ICONS = { complete: "✓", skipped: "—", pending: "◌" } as const;
|
|
368
|
+
const BRANCH = "├─";
|
|
369
|
+
const BRANCH_LAST = "└─";
|
|
370
|
+
|
|
371
|
+
function collectTaskEntries(
|
|
372
|
+
task: GoalTask,
|
|
373
|
+
depth: number,
|
|
374
|
+
isLast: boolean,
|
|
375
|
+
entries: LineEntry[],
|
|
376
|
+
): void {
|
|
377
|
+
const branch = isLast ? BRANCH_LAST : BRANCH;
|
|
378
|
+
const statusIcon = STATUS_ICONS[task.status] ?? STATUS_ICONS.pending;
|
|
379
|
+
|
|
380
|
+
const indent = " " + " ".repeat(depth - 1);
|
|
381
|
+
const prefix = `${indent} ${branch} ${statusIcon}`;
|
|
382
|
+
entries.push({ type: "task", prefix, title: task.title });
|
|
383
|
+
|
|
384
|
+
if (task.subtasks && task.subtasks.length > 0) {
|
|
385
|
+
for (let i = 0; i < task.subtasks.length; i++) {
|
|
386
|
+
collectTaskEntries(task.subtasks[i], depth + 1, i === task.subtasks.length - 1, entries);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-x",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.4",
|
|
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",
|