@quantmind-br/pi-goal-x 0.0.0-stage → 0.35.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/LICENSE +21 -0
- package/README.md +131 -2
- package/assets/badge-dark.svg +18 -0
- package/assets/badge-light.svg +18 -0
- package/docs/advanced-usage.md +106 -0
- package/docs/agent-flow-design.md +306 -0
- package/docs/agentic-runtime-prd.md +773 -0
- package/docs/architecture.md +368 -0
- package/docs/controls.md +140 -0
- package/docs/pi-autoresearch-survey.md +50 -0
- package/docs/pi-goal-x-unified-dashboard-plan.md +1667 -0
- package/docs/prompt-caching.md +31 -0
- package/docs/unified-dashboard.md +286 -0
- package/extensions/auditor-selector.ts +74 -0
- package/extensions/goal-accounting.ts +107 -0
- package/extensions/goal-activity.ts +202 -0
- package/extensions/goal-auditor.ts +480 -0
- package/extensions/goal-commands.ts +866 -0
- package/extensions/goal-compaction.ts +197 -0
- package/extensions/goal-completion.ts +173 -0
- package/extensions/goal-context.ts +12 -0
- package/extensions/goal-contract.ts +58 -0
- package/extensions/goal-controls.ts +202 -0
- package/extensions/goal-core-tools.ts +549 -0
- package/extensions/goal-core.ts +78 -0
- package/extensions/goal-detail.ts +65 -0
- package/extensions/goal-dialog-controls.ts +31 -0
- package/extensions/goal-draft.ts +244 -0
- package/extensions/goal-drafting.ts +535 -0
- package/extensions/goal-events.ts +692 -0
- package/extensions/goal-format.ts +376 -0
- package/extensions/goal-interruption.ts +53 -0
- package/extensions/goal-ledger-index.ts +68 -0
- package/extensions/goal-ledger.ts +1007 -0
- package/extensions/goal-live-retention.ts +219 -0
- package/extensions/goal-oracle.ts +346 -0
- package/extensions/goal-policy.ts +370 -0
- package/extensions/goal-pool.ts +103 -0
- package/extensions/goal-prompt-cache.ts +80 -0
- package/extensions/goal-questionnaire.ts +1073 -0
- package/extensions/goal-record.ts +388 -0
- package/extensions/goal-recovery.ts +256 -0
- package/extensions/goal-runtime.ts +277 -0
- package/extensions/goal-scheduler-state.ts +100 -0
- package/extensions/goal-scheduler.ts +436 -0
- package/extensions/goal-service.ts +843 -0
- package/extensions/goal-session-health.ts +130 -0
- package/extensions/goal-session-safety.ts +65 -0
- package/extensions/goal-settings.ts +1370 -0
- package/extensions/goal-state.ts +1069 -0
- package/extensions/goal-status.ts +305 -0
- package/extensions/goal-task-confirmation.ts +174 -0
- package/extensions/goal-task-count.ts +59 -0
- package/extensions/goal-task-derive.ts +46 -0
- package/extensions/goal-task-index.ts +57 -0
- package/extensions/goal-task-tools.ts +595 -0
- package/extensions/goal-tool-inputs.ts +102 -0
- package/extensions/goal-tool-names.ts +73 -0
- package/extensions/goal-tools.ts +16 -0
- package/extensions/goal-widget.ts +455 -0
- package/extensions/goal.ts +42 -0
- package/extensions/network-error-backoff.ts +55 -0
- package/extensions/prompts/goal-prompts.ts +321 -0
- package/extensions/storage/atomic-rename.ts +52 -0
- package/extensions/storage/goal-files.ts +695 -0
- package/extensions/storage/goal-lock.ts +131 -0
- package/extensions/storage/goal-root.ts +95 -0
- package/extensions/widgets/auditor-dashboard-model.ts +174 -0
- package/extensions/widgets/dialog-scaffold.ts +26 -0
- package/extensions/widgets/goal-controls-dialog.ts +329 -0
- package/extensions/widgets/goal-dashboard-model.ts +530 -0
- package/extensions/widgets/goal-dashboard-renderer.ts +674 -0
- package/extensions/widgets/goal-escape-dialog.ts +148 -0
- package/extensions/widgets/goal-notifications.ts +9 -0
- package/extensions/widgets/goal-widget.ts +723 -0
- package/extensions/widgets/task-list-overlay.ts +409 -0
- package/extensions/widgets/text-cache.ts +30 -0
- package/package.json +117 -4
- package/scripts/recover-session-checkpoints.mjs +289 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { truncateToWidth, wrapTextWithAnsi } from "./text-cache.ts";
|
|
3
|
+
import { borderedLine, dialogInnerWidth, horizontalRule } from "./dialog-scaffold.ts";
|
|
4
|
+
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
5
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { displayObjectiveTitle, statusLabel } from "../goal-core.ts";
|
|
7
|
+
import { openGoalsFromPool } from "../goal-pool.ts";
|
|
8
|
+
import type { GoalRecord, GoalTask } from "../goal-record.ts";
|
|
9
|
+
import { countAllTasks, countTaskSubtree } from "../goal-task-count.ts";
|
|
10
|
+
|
|
11
|
+
// ── Structured line entries ──────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
type LineEntry =
|
|
14
|
+
| { type: "separator" }
|
|
15
|
+
| { type: "goal-header"; icon: string; title: string; status: string }
|
|
16
|
+
| { type: "task-summary"; text: string }
|
|
17
|
+
| { type: "task"; goalId: string; taskId: string; status: "pending" | "complete" | "skipped"; prefix: string; title: string }
|
|
18
|
+
| { type: "empty-message"; text: string };
|
|
19
|
+
|
|
20
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Show a scrollable modal overlay displaying the current goal's task list.
|
|
24
|
+
* Press 'a' to toggle between the current goal and all open goals.
|
|
25
|
+
* Triggered by Ctrl+Shift+T. Dismisses on Escape.
|
|
26
|
+
*/
|
|
27
|
+
export async function showTaskListOverlay(
|
|
28
|
+
ctx: ExtensionContext,
|
|
29
|
+
goalsById: Map<string, GoalRecord>,
|
|
30
|
+
focusedGoalId: string | null,
|
|
31
|
+
opts: { onToggleTask?: (goalId: string, taskId: string) => Promise<{ ok: boolean; message?: string }> } = {},
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
if (!ctx.hasUI) return;
|
|
34
|
+
|
|
35
|
+
await ctx.ui.custom<void>(
|
|
36
|
+
(tui: TUI, theme: Theme, _keybindings: unknown, done: () => void): Component => {
|
|
37
|
+
// ── Theme helpers ─────────────────────────────────────────────
|
|
38
|
+
const accent = (s: string) => theme.fg("accent", s);
|
|
39
|
+
const dim = (s: string) => theme.fg("dim", s);
|
|
40
|
+
const success = (s: string) => theme.fg("success", s);
|
|
41
|
+
const muted = (s: string) => theme.fg("muted", s);
|
|
42
|
+
const bold = (s: string) => theme.bold(s);
|
|
43
|
+
|
|
44
|
+
// ── State ─────────────────────────────────────────────────────
|
|
45
|
+
let showAllGoals = false;
|
|
46
|
+
let entries: LineEntry[] = [];
|
|
47
|
+
let totalOpenGoals = 0;
|
|
48
|
+
let totalTaskCount = 0;
|
|
49
|
+
// F3: interactive cursor over task rows; Enter toggles the task.
|
|
50
|
+
let cursorEntryIndex = -1;
|
|
51
|
+
let cursorMessage = "";
|
|
52
|
+
|
|
53
|
+
function rebuildEntries(): void {
|
|
54
|
+
entries = [];
|
|
55
|
+
totalOpenGoals = 0;
|
|
56
|
+
totalTaskCount = 0;
|
|
57
|
+
|
|
58
|
+
const openGoals = openGoalsFromPool(goalsById);
|
|
59
|
+
const targetGoals = showAllGoals
|
|
60
|
+
? openGoals
|
|
61
|
+
: openGoals.filter((g) => g.id === focusedGoalId);
|
|
62
|
+
|
|
63
|
+
if (targetGoals.length === 0) {
|
|
64
|
+
entries.push({ type: "empty-message", text: "No open goals to display." });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const [gIdx, goal] of targetGoals.entries()) {
|
|
69
|
+
if (goal.taskList) {
|
|
70
|
+
totalTaskCount += countAllTasks(goal.taskList.tasks);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (gIdx > 0) {
|
|
74
|
+
entries.push({ type: "separator" });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Goal header
|
|
78
|
+
const icon = goal.status === "paused" ? "⏸"
|
|
79
|
+
: goal.sisyphus ? "◆" : "●";
|
|
80
|
+
const title = displayObjectiveTitle(goal.objective);
|
|
81
|
+
const label = statusLabel(goal);
|
|
82
|
+
entries.push({ type: "goal-header", icon, title, status: label });
|
|
83
|
+
|
|
84
|
+
// Task summary & task list
|
|
85
|
+
if (goal.taskList && goal.taskList.tasks.length > 0) {
|
|
86
|
+
const { total, complete, skipped } = countTaskSubtree(goal.taskList.tasks);
|
|
87
|
+
const summary = `${complete}/${total} done${skipped > 0 ? ` (${skipped} skipped)` : ""}`;
|
|
88
|
+
entries.push({ type: "task-summary", text: summary });
|
|
89
|
+
|
|
90
|
+
const tasks = goal.taskList.tasks;
|
|
91
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
92
|
+
const isLast = i === tasks.length - 1;
|
|
93
|
+
collectTaskEntries(tasks[i]!, 1, isLast, entries, goal.id);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
entries.push({ type: "empty-message", text: "(no tasks)" });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
totalOpenGoals++;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// F3: reset the cursor to the first task row after a rebuild.
|
|
103
|
+
cursorEntryIndex = entries.findIndex((e) => e.type === "task");
|
|
104
|
+
cursorMessage = "";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Build initial entries
|
|
108
|
+
rebuildEntries();
|
|
109
|
+
|
|
110
|
+
// ── Render helpers ────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/** Render a single line entry into styled wrapped lines */
|
|
113
|
+
function renderEntry(entry: LineEntry, innerWidth: number): string[] {
|
|
114
|
+
switch (entry.type) {
|
|
115
|
+
case "separator":
|
|
116
|
+
return [dim("·")];
|
|
117
|
+
|
|
118
|
+
case "goal-header": {
|
|
119
|
+
const icon = entry.icon === "⏸" ? muted("⏸") : entry.icon === "◆" ? accent("◆") : accent("●");
|
|
120
|
+
const status = dim(entry.status);
|
|
121
|
+
const prefix = `${icon} ${bold("")}`; // placeholder, we'll build below
|
|
122
|
+
const rawPrefix = `${entry.icon} `; // raw width: icon + 2 spaces
|
|
123
|
+
const prefixWidth = visibleWidth(rawPrefix);
|
|
124
|
+
const available = innerWidth - prefixWidth;
|
|
125
|
+
const wrappedTitle = wrapTextWithAnsi(bold(entry.title), Math.max(1, available));
|
|
126
|
+
const lines: string[] = [];
|
|
127
|
+
wrappedTitle.forEach((segment, i) => {
|
|
128
|
+
if (i === 0) {
|
|
129
|
+
lines.push(`${icon} ${segment} ${status}`);
|
|
130
|
+
} else {
|
|
131
|
+
lines.push(` ${" ".repeat(rawPrefix.length - 3)}${segment}`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// If title alone fits, status needs to be on same line
|
|
135
|
+
// But we already handle it above for i=0. The status might overflow.
|
|
136
|
+
// If status doesn't fit on first line after title, put it on its own line
|
|
137
|
+
const firstLine = lines[0]!;
|
|
138
|
+
const allOnFirst = `${icon} ${entry.title} ${entry.status}`;
|
|
139
|
+
if (visibleWidth(firstLine) > innerWidth) {
|
|
140
|
+
// Status overflowed — put status on a separate dim line
|
|
141
|
+
lines[0] = `${icon} ${wrappedTitle[0]}`;
|
|
142
|
+
if (wrappedTitle.length === 1) {
|
|
143
|
+
lines.push(` ${dim(entry.status)}`);
|
|
144
|
+
} else {
|
|
145
|
+
// insert status after last wrapped title segment
|
|
146
|
+
const lastLine = wrappedTitle[wrappedTitle.length - 1];
|
|
147
|
+
lines[lines.length - 1] = ` ${" ".repeat(rawPrefix.length - 3)}${lastLine}`;
|
|
148
|
+
lines.push(` ${dim(entry.status)}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Re-truncate any over-long lines just in case
|
|
152
|
+
return lines.map((l) => visibleWidth(l) > innerWidth ? truncateToWidth(l, innerWidth, "…") : l);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case "task-summary": {
|
|
156
|
+
const content = dim(entry.text);
|
|
157
|
+
return [visibleWidth(content) > innerWidth ? truncateToWidth(content, innerWidth, "…") : content];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
case "task": {
|
|
161
|
+
const prefixWidth = visibleWidth(entry.prefix);
|
|
162
|
+
const available = innerWidth - prefixWidth;
|
|
163
|
+
const wrappedTitle = wrapTextWithAnsi(entry.title, Math.max(1, available));
|
|
164
|
+
const lines: string[] = [];
|
|
165
|
+
wrappedTitle.forEach((segment, i) => {
|
|
166
|
+
if (i === 0) {
|
|
167
|
+
lines.push(`${dim(entry.prefix)} ${segment}`);
|
|
168
|
+
} else {
|
|
169
|
+
lines.push(`${" ".repeat(prefixWidth + 1)}${segment}`);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return lines.map((l) => visibleWidth(l) > innerWidth ? truncateToWidth(l, innerWidth, "…") : l);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case "empty-message": {
|
|
176
|
+
const content = dim(entry.text);
|
|
177
|
+
return [visibleWidth(content) > innerWidth ? truncateToWidth(content, innerWidth, "…") : content];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
default:
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Scroll state ──────────────────────────────────────────────
|
|
186
|
+
let scrollOffset = 0;
|
|
187
|
+
let lastRenderWidth = 80;
|
|
188
|
+
|
|
189
|
+
// The "logical line count" is the number of entries (before wrapping).
|
|
190
|
+
// We store rendered line count too since wrapped entries expand.
|
|
191
|
+
let renderedLineCount = 0;
|
|
192
|
+
|
|
193
|
+
function computeVisibleHeight(innerWidth: number): number {
|
|
194
|
+
return Math.max(8, Math.floor(innerWidth / 2.8));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const wasHardwareCursorShown = tui.getShowHardwareCursor();
|
|
198
|
+
tui.setShowHardwareCursor(false);
|
|
199
|
+
|
|
200
|
+
// ── Component ─────────────────────────────────────────────────
|
|
201
|
+
const component: Component & { dispose?(): void } = {
|
|
202
|
+
dispose() {
|
|
203
|
+
tui.setShowHardwareCursor(wasHardwareCursorShown);
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
invalidate(): void {},
|
|
207
|
+
|
|
208
|
+
render(width: number): string[] {
|
|
209
|
+
lastRenderWidth = width;
|
|
210
|
+
|
|
211
|
+
const termWidth = Math.min(width, 100);
|
|
212
|
+
const innerWidth = dialogInnerWidth(termWidth, 90);
|
|
213
|
+
const p = " ";
|
|
214
|
+
|
|
215
|
+
const line = (content: string): string => borderedLine(accent, innerWidth, content);
|
|
216
|
+
|
|
217
|
+
// ── Render all entries into flat text lines ────────────
|
|
218
|
+
const renderedLines: string[] = [];
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
const wrapped = renderEntry(entry, innerWidth);
|
|
221
|
+
renderedLines.push(...wrapped);
|
|
222
|
+
}
|
|
223
|
+
renderedLineCount = renderedLines.length;
|
|
224
|
+
|
|
225
|
+
const horiz = horizontalRule(innerWidth);
|
|
226
|
+
const out: string[] = [];
|
|
227
|
+
|
|
228
|
+
out.push(accent(`┌${horiz}┐`));
|
|
229
|
+
|
|
230
|
+
// Header
|
|
231
|
+
const viewLabel = showAllGoals
|
|
232
|
+
? `${totalOpenGoals} ${totalOpenGoals === 1 ? "goal" : "goals"}`
|
|
233
|
+
: "current goal";
|
|
234
|
+
const taskWord = totalTaskCount === 1 ? "task" : "tasks";
|
|
235
|
+
const h = bold(` Tasks (${viewLabel}, ${totalTaskCount} ${taskWord})`);
|
|
236
|
+
out.push(line(p + h));
|
|
237
|
+
out.push(accent(`├${horiz}┤`));
|
|
238
|
+
|
|
239
|
+
// Content with scrolling
|
|
240
|
+
const visibleHeight = computeVisibleHeight(innerWidth);
|
|
241
|
+
const maxOffset = Math.max(0, renderedLineCount - visibleHeight);
|
|
242
|
+
if (scrollOffset > maxOffset) scrollOffset = maxOffset;
|
|
243
|
+
|
|
244
|
+
const canScrollUp = scrollOffset > 0;
|
|
245
|
+
const canScrollDown = scrollOffset < maxOffset;
|
|
246
|
+
|
|
247
|
+
if (canScrollUp) {
|
|
248
|
+
out.push(line(p + dim(`▴ ${scrollOffset}/${renderedLineCount} lines`)));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const end = Math.min(scrollOffset + visibleHeight, renderedLineCount);
|
|
252
|
+
for (let i = scrollOffset; i < end; i++) {
|
|
253
|
+
const raw = renderedLines[i]!;
|
|
254
|
+
// Each rendered line from renderEntry already wraps/truncates,
|
|
255
|
+
// but double-check for safety
|
|
256
|
+
const safe = visibleWidth(raw) > innerWidth
|
|
257
|
+
? truncateToWidth(raw, innerWidth, "…")
|
|
258
|
+
: raw;
|
|
259
|
+
out.push(line(p + safe));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (canScrollDown) {
|
|
263
|
+
out.push(line(p + dim(`▾ ${renderedLineCount - end} more lines`)));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (renderedLineCount === 0) {
|
|
267
|
+
out.push(line(p + dim("No tasks to display.")));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
out.push(accent(`├${horiz}┤`));
|
|
271
|
+
const toggleHint = showAllGoals ? "show current" : "show all";
|
|
272
|
+
const interactive = opts.onToggleTask ? " · Enter toggle task" : "";
|
|
273
|
+
const footer = dim(`↑↓/jk scroll · 'a' to ${toggleHint} · Esc close${interactive}`);
|
|
274
|
+
out.push(line(p + footer));
|
|
275
|
+
if (cursorMessage) {
|
|
276
|
+
out.push(line(p + theme.fg("warning", cursorMessage)));
|
|
277
|
+
}
|
|
278
|
+
out.push(accent(`└${horiz}┘`));
|
|
279
|
+
|
|
280
|
+
return out;
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
handleInput(data: string): void {
|
|
284
|
+
const tw = Math.min(lastRenderWidth, 100);
|
|
285
|
+
const innerW = Math.min(tw, 90) - 2;
|
|
286
|
+
const visibleH = computeVisibleHeight(innerW);
|
|
287
|
+
const maxO = Math.max(0, renderedLineCount - visibleH);
|
|
288
|
+
|
|
289
|
+
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
290
|
+
scrollOffset = Math.max(0, scrollOffset - 1);
|
|
291
|
+
if (opts.onToggleTask) {
|
|
292
|
+
let prev = -1;
|
|
293
|
+
for (let i = cursorEntryIndex - 1; i >= 0; i--) {
|
|
294
|
+
if (entries[i]?.type === "task") { prev = i; break; }
|
|
295
|
+
}
|
|
296
|
+
if (prev >= 0) cursorEntryIndex = prev;
|
|
297
|
+
}
|
|
298
|
+
tui.requestRender();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (matchesKey(data, "down") || matchesKey(data, "j")) {
|
|
302
|
+
scrollOffset = Math.min(maxO, scrollOffset + 1);
|
|
303
|
+
if (opts.onToggleTask) {
|
|
304
|
+
for (let i = cursorEntryIndex + 1; i < entries.length; i++) {
|
|
305
|
+
if (entries[i]?.type === "task") { cursorEntryIndex = i; break; }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
tui.requestRender();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (matchesKey(data, "pageUp")) {
|
|
312
|
+
scrollOffset = Math.max(0, scrollOffset - visibleH);
|
|
313
|
+
tui.requestRender();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (matchesKey(data, "pageDown")) {
|
|
317
|
+
scrollOffset = Math.min(maxO, scrollOffset + visibleH);
|
|
318
|
+
tui.requestRender();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (matchesKey(data, "home")) {
|
|
322
|
+
scrollOffset = 0;
|
|
323
|
+
tui.requestRender();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (matchesKey(data, "end")) {
|
|
327
|
+
scrollOffset = maxO;
|
|
328
|
+
tui.requestRender();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// Toggle view
|
|
332
|
+
if (matchesKey(data, "a")) {
|
|
333
|
+
showAllGoals = !showAllGoals;
|
|
334
|
+
rebuildEntries();
|
|
335
|
+
scrollOffset = 0;
|
|
336
|
+
tui.requestRender();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (matchesKey(data, "enter") && opts.onToggleTask) {
|
|
340
|
+
const cursorEntry = entries[cursorEntryIndex];
|
|
341
|
+
if (cursorEntry && cursorEntry.type === "task") {
|
|
342
|
+
void (async () => {
|
|
343
|
+
const result = await opts.onToggleTask!(cursorEntry.goalId, cursorEntry.taskId);
|
|
344
|
+
if (result.ok) {
|
|
345
|
+
rebuildEntries();
|
|
346
|
+
tui.requestRender();
|
|
347
|
+
} else {
|
|
348
|
+
cursorMessage = result.message ?? "Toggle failed.";
|
|
349
|
+
tui.requestRender();
|
|
350
|
+
setTimeout(() => {
|
|
351
|
+
cursorMessage = "";
|
|
352
|
+
tui.requestRender();
|
|
353
|
+
}, 2500);
|
|
354
|
+
}
|
|
355
|
+
})();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
done();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (matchesKey(data, "escape") || matchesKey(data, "enter")) {
|
|
362
|
+
done();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
return component;
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
overlay: true,
|
|
372
|
+
overlayOptions: {
|
|
373
|
+
anchor: "center",
|
|
374
|
+
width: "80%",
|
|
375
|
+
minWidth: 60,
|
|
376
|
+
maxHeight: "80%",
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── Task counting ─────────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
// ── Tree entry collection ─────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
const STATUS_ICONS = { complete: "✓", skipped: "—", pending: "◌" } as const;
|
|
387
|
+
const BRANCH = "├─";
|
|
388
|
+
const BRANCH_LAST = "└─";
|
|
389
|
+
|
|
390
|
+
function collectTaskEntries(
|
|
391
|
+
task: GoalTask,
|
|
392
|
+
depth: number,
|
|
393
|
+
isLast: boolean,
|
|
394
|
+
entries: LineEntry[],
|
|
395
|
+
goalId: string,
|
|
396
|
+
): void {
|
|
397
|
+
const branch = isLast ? BRANCH_LAST : BRANCH;
|
|
398
|
+
const statusIcon = STATUS_ICONS[task.status] ?? STATUS_ICONS.pending;
|
|
399
|
+
|
|
400
|
+
const indent = " " + " ".repeat(depth - 1);
|
|
401
|
+
const prefix = `${indent} ${branch} ${statusIcon}`;
|
|
402
|
+
entries.push({ type: "task", goalId, taskId: task.id, status: task.status, prefix, title: task.title });
|
|
403
|
+
|
|
404
|
+
if (task.subtasks && task.subtasks.length > 0) {
|
|
405
|
+
for (let i = 0; i < task.subtasks.length; i++) {
|
|
406
|
+
collectTaskEntries(task.subtasks[i]!, depth + 1, i === task.subtasks.length - 1, entries, goalId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { truncateToWidth as sdkTruncate, wrapTextWithAnsi as sdkWrap } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
// Cache pure SDK text transforms, keyed by complete ANSI content and layout arguments.
|
|
4
|
+
// Theme changes naturally miss. visibleWidth already has a cache in the supported SDK.
|
|
5
|
+
const cache = new Map<string, {value: string | string[]; chars: number}>();
|
|
6
|
+
let retainedChars = 0;
|
|
7
|
+
function memo<T extends string | string[]>(key: string, build: () => T): T {
|
|
8
|
+
const cached = cache.get(key);
|
|
9
|
+
if (cached) return cached.value as T;
|
|
10
|
+
const value = build();
|
|
11
|
+
const chars = key.length + (typeof value === "string" ? value.length : value.reduce((n, line) => n + line.length, 0));
|
|
12
|
+
if (chars <= 500_000) {
|
|
13
|
+
while (cache.size >= 1024 || retainedChars + chars > 500_000) {
|
|
14
|
+
const oldest = cache.keys().next().value!;
|
|
15
|
+
retainedChars -= cache.get(oldest)!.chars;
|
|
16
|
+
cache.delete(oldest);
|
|
17
|
+
}
|
|
18
|
+
cache.set(key, {value, chars}); retainedChars += chars;
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function truncateToWidth(text: string, width: number, ellipsis = "...", pad = false): string {
|
|
24
|
+
return memo(`t:${width}:${pad}:${ellipsis.length}:${ellipsis}${text}`, () => sdkTruncate(text, width, ellipsis, pad));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function wrapTextWithAnsi(text: string, width: number): string[] {
|
|
28
|
+
// Callers may edit the returned rows; cached rows stay privately owned.
|
|
29
|
+
return memo(`w:${width}:${text}`, () => sdkWrap(text, width)).slice();
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,119 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quantmind-br/pi-goal-x",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.35.0",
|
|
4
|
+
"description": "Fork of pi-goal-x with a fixed, cache-stable goal tool profile. Adds /goal to pi: conversational goal planning, flexible or ordered goals, persistent progress, and an independent completion auditor.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "pi-goal-x contributors",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"pi-goal-x-recover": "scripts/recover-session-checkpoints.mjs"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"pi-package",
|
|
13
|
+
"pi",
|
|
14
|
+
"pi-extension",
|
|
15
|
+
"goal",
|
|
16
|
+
"sisyphus",
|
|
17
|
+
"autoresearch",
|
|
18
|
+
"agent-loop",
|
|
19
|
+
"long-running"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/quantforge-br/pi-goal-x.git"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/quantforge-br/pi-goal-x#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/quantforge-br/pi-goal-x/issues"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"extensions",
|
|
31
|
+
"scripts/recover-session-checkpoints.mjs",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"docs/agent-flow-design.md",
|
|
35
|
+
"docs/agentic-runtime-prd.md",
|
|
36
|
+
"docs/architecture.md",
|
|
37
|
+
"docs/prompt-caching.md",
|
|
38
|
+
"docs/advanced-usage.md",
|
|
39
|
+
"docs/pi-autoresearch-survey.md",
|
|
40
|
+
"docs/pi-goal-x-unified-dashboard-plan.md",
|
|
41
|
+
"docs/unified-dashboard.md",
|
|
42
|
+
"assets/badge-light.svg",
|
|
43
|
+
"assets/badge-dark.svg",
|
|
44
|
+
"docs/controls.md"
|
|
45
|
+
],
|
|
46
|
+
"pi": {
|
|
47
|
+
"extensions": [
|
|
48
|
+
"extensions/goal.ts"
|
|
49
|
+
],
|
|
50
|
+
"image": "https://github.com/quantforge-br/pi-goal-x/raw/main/pi-goal-x.png"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"check": "tsc --noEmit",
|
|
54
|
+
"test": "node scripts/run-unit-tests.mjs",
|
|
55
|
+
"test:serial": "node --experimental-strip-types --test --test-concurrency=1 tests/*.test.ts",
|
|
56
|
+
"test:unit": "node scripts/run-unit-tests.mjs",
|
|
57
|
+
"test:integration": "node scripts/run-unit-tests.mjs integration",
|
|
58
|
+
"test:all": "node scripts/run-unit-tests.mjs all",
|
|
59
|
+
"test:selfcheck": "node scripts/run-unit-tests.mjs --selfcheck",
|
|
60
|
+
"bench": "node --import ./experiments/bench/bench-adapter-hooks.mjs --experimental-strip-types ./experiments/bench/run-bench.mjs",
|
|
61
|
+
"bench:gate": "node --experimental-strip-types ./experiments/bench/b6-gate.mjs",
|
|
62
|
+
"bench:naf": "node --import ./experiments/bench/bench-adapter-hooks.mjs --experimental-strip-types ./experiments/bench/run-bench.mjs",
|
|
63
|
+
"bench:gate:naf": "node --experimental-strip-types ./experiments/bench/b6-gate.mjs naf",
|
|
64
|
+
"test:e2e": "node scripts/run-unit-tests.mjs e2e",
|
|
65
|
+
"test:checkpoint-recovery": "node --experimental-strip-types --test tests/session-checkpoint-growth.test.ts tests/session-checkpoint-recovery.test.ts tests/goal-session-health.test.ts",
|
|
66
|
+
"test:settings-race": "node --experimental-strip-types --test tests/goal-settings-race.test.ts",
|
|
67
|
+
"lint": "eslint .",
|
|
68
|
+
"context:measure": "node experiments/context/run-measure.mjs",
|
|
69
|
+
"context:gate": "node experiments/context/run-gate.mjs",
|
|
70
|
+
"context:provider-check": "node --experimental-strip-types experiments/context/provider-crosscheck.mjs",
|
|
71
|
+
"bench:runtime-token": "node --import ./experiments/bench/bench-adapter-hooks.mjs --expose-gc --experimental-strip-types experiments/bench/runtime-token.mjs",
|
|
72
|
+
"bench:gate:runtime-token": "node experiments/bench/runtime-token-gate.mjs",
|
|
73
|
+
"test:oracle": "node --experimental-strip-types --test tests/goal-oracle.test.ts",
|
|
74
|
+
"bench:comprehensive": "node --import ./experiments/bench/bench-adapter-hooks.mjs --expose-gc --experimental-strip-types experiments/bench/comprehensive.mjs",
|
|
75
|
+
"bench:gate:comprehensive": "node experiments/bench/comprehensive-gate.mjs",
|
|
76
|
+
"test:controls-cli": "node experiments/controls/cli-walkthrough.mjs",
|
|
77
|
+
"context:cache-report": "node --experimental-strip-types experiments/context/cache-report.mjs",
|
|
78
|
+
"context:cache-gate": "node --experimental-strip-types experiments/context/cache-gate.mjs"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"@earendil-works/pi-ai": ">=1.4.0 <2.0.0",
|
|
82
|
+
"@earendil-works/pi-coding-agent": ">=1.4.0 <2.0.0",
|
|
83
|
+
"@earendil-works/pi-tui": ">=1.4.0 <2.0.0",
|
|
84
|
+
"typebox": "^1.3.11"
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@earendil-works/pi-ai": "1.4.0",
|
|
88
|
+
"@earendil-works/pi-coding-agent": "1.4.0",
|
|
89
|
+
"@earendil-works/pi-tui": "1.4.0",
|
|
90
|
+
"@eslint/js": "^9.39.5",
|
|
91
|
+
"@xterm/headless": "^6.0.0",
|
|
92
|
+
"eslint": "^9.39.5",
|
|
93
|
+
"globals": "^15.15.0",
|
|
94
|
+
"typebox": "^1.3.11",
|
|
95
|
+
"typescript": "^5.9.3",
|
|
96
|
+
"typescript-eslint": "^8.66.0"
|
|
97
|
+
},
|
|
98
|
+
"publishConfig": {
|
|
99
|
+
"access": "public"
|
|
100
|
+
},
|
|
101
|
+
"overrides": {
|
|
102
|
+
"@earendil-works/pi-coding-agent": {
|
|
103
|
+
"undici": "8.12.2",
|
|
104
|
+
"minimatch": {
|
|
105
|
+
".": "10.2.6",
|
|
106
|
+
"brace-expansion": "5.0.12"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"@earendil-works/pi-ai": {
|
|
110
|
+
"undici": "8.12.2"
|
|
111
|
+
},
|
|
112
|
+
"minimatch": {
|
|
113
|
+
"brace-expansion": "5.0.12"
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"engines": {
|
|
117
|
+
"node": ">=22.19.0"
|
|
118
|
+
}
|
|
119
|
+
}
|