pi-goal-x 0.32.0 → 0.32.2

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 CHANGED
@@ -6,7 +6,7 @@
6
6
  <a href="https://pi.dev/packages?type=extension" target="_blank" rel="noopener noreferrer">
7
7
  <picture>
8
8
  <source media="(prefers-color-scheme: dark)" srcset="assets/badge-dark.svg">
9
- <img src="assets/badge-light.svg" alt="TOP 0.2% of Pi coding agent extensions: #6 of 3,258 by downloads · Oct 1, 2026 (best recorded rank)" width="480">
9
+ <img src="assets/badge-light.svg" alt="TOP 0.2% of Pi coding agent extensions: #6 of 3,177 by downloads · Oct 2, 2026 (best recorded rank)" width="480">
10
10
  </picture>
11
11
  </a>
12
12
  </div>
@@ -19,8 +19,6 @@ The extension saves goal objectives, tasks, and progress across sessions. You ca
19
19
 
20
20
  ## Install
21
21
 
22
- Supports pi 0.83 through 1.x (`@earendil-works/pi-ai`, `pi-coding-agent`, and `pi-tui` `>=0.83.0 <2.0.0`), developed and verified against 1.0.0.
23
-
24
22
  ```bash
25
23
  pi install npm:pi-goal-x
26
24
  ```
@@ -1,4 +1,4 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.2% of Pi coding agent extensions: #6 of 3,258 by downloads · Oct 1, 2026 (best recorded rank)">
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.2% of Pi coding agent extensions: #6 of 3,177 by downloads · Oct 2, 2026 (best recorded rank)">
2
2
  <!-- Dark mode -->
3
3
  <style>
4
4
  .frame { fill: #14181f; stroke: #2a3040; stroke-width: 1; }
@@ -13,6 +13,6 @@
13
13
  <text class="rank" x="18" y="34">TOP 0.2%</text>
14
14
  <line class="divider" x1="128" y1="13" x2="128" y2="41"/>
15
15
  <text class="label" x="143" y="25">of Pi coding agent extensions</text>
16
- <text class="meta" x="143" y="41">#6 of 3,258 by downloads · Oct 1, 2026</text>
16
+ <text class="meta" x="143" y="41">#6 of 3,177 by downloads · Oct 2, 2026</text>
17
17
  </a>
18
18
  </svg>
@@ -1,4 +1,4 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.2% of Pi coding agent extensions: #6 of 3,258 by downloads · Oct 1, 2026 (best recorded rank)">
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="480" height="54" viewBox="0 0 480 54" role="img" aria-label="TOP 0.2% of Pi coding agent extensions: #6 of 3,177 by downloads · Oct 2, 2026 (best recorded rank)">
2
2
  <!-- Light mode -->
3
3
  <style>
4
4
  .frame { fill: #f6f8fa; stroke: #d0d7de; stroke-width: 1; }
@@ -13,6 +13,6 @@
13
13
  <text class="rank" x="18" y="34">TOP 0.2%</text>
14
14
  <line class="divider" x1="128" y1="13" x2="128" y2="41"/>
15
15
  <text class="label" x="143" y="25">of Pi coding agent extensions</text>
16
- <text class="meta" x="143" y="41">#6 of 3,258 by downloads · Oct 1, 2026</text>
16
+ <text class="meta" x="143" y="41">#6 of 3,177 by downloads · Oct 2, 2026</text>
17
17
  </a>
18
18
  </svg>
@@ -229,14 +229,15 @@ export function isErrorAssistantMessage(message: unknown): boolean {
229
229
  /**
230
230
  * Transient provider failures are safe for goal-level recovery backoff:
231
231
  * Pi-declared network errors plus HTTP 429/5xx-style outages surfaced by
232
- * providers (429 rate limits, 503 server_error, "Endpoint is unavailable",
233
- * overloaded, gateway failures). Deliberately excludes auth and
232
+ * providers (429 rate limits, 503 server_error, HTTP/2 PROTOCOL_ERROR,
233
+ * "Endpoint is unavailable", overloaded, gateway failures). Deliberately excludes auth and
234
234
  * malformed-request failures (and quota/billing text), which retrying
235
235
  * cannot fix.
236
236
  */
237
237
  const TRANSIENT_PROVIDER_ERROR_RE = new RegExp(
238
238
  [
239
239
  "\\bnetwork[_\\s-]?error\\b",
240
+ "\\bprotocol[_\\s-]?error\\b",
240
241
  "\\bserver[_\\s]?error\\b",
241
242
  "\\b(?:429|502|503|504|529)\\b",
242
243
  "\\brate[_\\s-]?limited\\b",
@@ -681,6 +681,7 @@ export function createGoalCore(
681
681
  getExpanded: () => dashboardExpanded,
682
682
  getLedgerEvents: () => state.goal ? goalActivityEvents(storage, state.goal.id) : [],
683
683
  getAuditResult: () => auditResult,
684
+ componentRef: goalWidgetComponentRef,
684
685
  }),
685
686
  { placement: "aboveEditor" },
686
687
  );
@@ -711,6 +712,7 @@ export function createGoalCore(
711
712
  getExpanded: () => dashboardExpanded,
712
713
  getLedgerEvents: () => state.goal ? goalActivityEvents(storage, state.goal.id) : [],
713
714
  getAuditResult: () => auditResult,
715
+ componentRef: goalWidgetComponentRef,
714
716
  }),
715
717
  { placement: "aboveEditor" },
716
718
  );
@@ -9,6 +9,10 @@ import type { GoalCore } from "./goal-state.ts";
9
9
 
10
10
  const DEBUG_GOALS_DIR = ".pi/goals/debug";
11
11
 
12
+ function isKittyRepeatOrRelease(data: string): boolean {
13
+ return data.startsWith("\x1b[") && /^[0-9:;]+:[23][u~ABCDHF]$/.test(data.slice(2));
14
+ }
15
+
12
16
  /**
13
17
  * Terminal input keybindings (Escape pause/abort-audit, Ctrl+Shift+T dashboard
14
18
  * overlay, and the hidden debug-mode bindings) plus the debug goal/task/audit
@@ -111,8 +115,7 @@ export async function toggleTaskViaService(core: GoalCore, ctx: ExtensionContext
111
115
  export function syncTerminalInputPause(core: GoalCore, ctx: ExtensionContext): void {
112
116
  if (!ctx.hasUI) return;
113
117
  core.terminalInputUnsubscribe?.();
114
- const settings = loadGoalSettings(typeof ctx.cwd === "string" ? ctx.cwd : process.cwd());
115
- const keybindings = settings.keybindings?.dashboard ?? DEFAULT_GOAL_KEYBINDINGS.dashboard;
118
+ const goalCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
116
119
  core.terminalInputUnsubscribe = ctx.ui.onTerminalInput((data) => {
117
120
  // If an audit is running, Escape aborts the audit instead of pausing.
118
121
  // Must return { consume: true } so the TUI doesn't also process the key
@@ -128,6 +131,15 @@ export function syncTerminalInputPause(core: GoalCore, ctx: ExtensionContext): v
128
131
  // before the dialog could process it (bn-l pattern). Depth counter so
129
132
  // nested goal modals remain guarded.
130
133
  if (core.goalModalDepth > 0) return undefined;
134
+ const keybindings = loadGoalSettings(goalCwd).keybindings?.dashboard ?? DEFAULT_GOAL_KEYBINDINGS.dashboard;
135
+ if (isKittyRepeatOrRelease(data)) {
136
+ const goalShortcut = matchesKey(data, keybindings.toggleExpand) || matchesKey(data, "ctrl+shift+a") ||
137
+ (matchesKey(data, "escape") && (core.isDashboardExpanded() || core.state.goal !== null)) ||
138
+ (core.isDashboardExpanded() && (["up", "down", "pageUp", "pageDown", "home", "end"] as const).some(key => matchesKey(data, key))) ||
139
+ (!core.isDashboardExpanded() && core.goalWidgetComponentRef.current !== null &&
140
+ ([keybindings.scrollUp, keybindings.scrollDown, "ctrl+shift+pageUp", "ctrl+shift+pageDown", "ctrl+shift+home", "ctrl+shift+end"] as const).some(key => matchesKey(data, key)));
141
+ return goalShortcut ? { consume: true } : undefined;
142
+ }
131
143
  if (matchesKey(data, "escape") && core.auditProgress) {
132
144
  core.abortAudit(ctx);
133
145
  return { consume: true };
@@ -82,20 +82,26 @@ export function makeGoalWidgetFactory(opts: {
82
82
  getExpanded?: () => boolean;
83
83
  getLedgerEvents?: () => GoalLedgerEvent[];
84
84
  getAuditResult?: () => AuditResultView | null;
85
+ /** Receives the live widget so terminal shortcuts can scroll its viewports. */
86
+ componentRef?: { current: GoalWidgetComponent | null };
85
87
  }) {
86
- return (tui: TUI, theme: Theme) => new GoalWidgetComponent({
87
- tui,
88
- theme,
89
- getGoal: opts.getGoal,
90
- getOpenGoalCount: opts.getOpenGoalCount,
91
- getAuditorProgress: opts.getAuditorProgress,
92
- getSettings: opts.getSettings,
93
- getDebugMode: opts.getDebugMode,
94
- getStalled: opts.getStalled,
95
- getExpanded: opts.getExpanded,
96
- getLedgerEvents: opts.getLedgerEvents,
97
- getAuditResult: opts.getAuditResult,
98
- });
88
+ return (tui: TUI, theme: Theme) => {
89
+ const component = new GoalWidgetComponent({
90
+ tui,
91
+ theme,
92
+ getGoal: opts.getGoal,
93
+ getOpenGoalCount: opts.getOpenGoalCount,
94
+ getAuditorProgress: opts.getAuditorProgress,
95
+ getSettings: opts.getSettings,
96
+ getDebugMode: opts.getDebugMode,
97
+ getStalled: opts.getStalled,
98
+ getExpanded: opts.getExpanded,
99
+ getLedgerEvents: opts.getLedgerEvents,
100
+ getAuditResult: opts.getAuditResult,
101
+ });
102
+ if (opts.componentRef) opts.componentRef.current = component;
103
+ return component;
104
+ };
99
105
  }
100
106
 
101
107
  export interface AuditorWidgetProgress {
@@ -623,6 +629,7 @@ export class GoalWidgetComponent implements Component {
623
629
  * list. Returns true when the key was consumed.
624
630
  */
625
631
  handleCompactScrollKey(key: "up" | "down" | "pageUp" | "pageDown" | "home" | "end"): boolean {
632
+ if (this.getExpanded()) return false;
626
633
  const settings = this.getSettings();
627
634
  const goal = this.getGoal();
628
635
  const model = goal ? deriveGoalDashboardModel(goal as GoalRecord | null, {
@@ -632,6 +639,7 @@ export class GoalWidgetComponent implements Component {
632
639
  tasksDisabled: settings.disableTasks === true,
633
640
  maxAutonomousRuns: settings.maxAutonomousRuns,
634
641
  }) : null;
642
+ this.maybeReanchor(model);
635
643
  const list = model?.taskTree.filter((n) => n.depth === 0) ?? [];
636
644
  const rows = compactTaskViewportRows(this.lastRenderWidth);
637
645
  if (list.length <= rows) return false;
@@ -667,11 +675,12 @@ export class GoalWidgetComponent implements Component {
667
675
  tasksDisabled: settings.disableTasks === true,
668
676
  maxAutonomousRuns: settings.maxAutonomousRuns,
669
677
  }) : null;
678
+ this.maybeReanchor(model);
670
679
  const list = model?.taskTree ?? [];
671
680
  if (list.length === 0) return false;
672
681
  const rows = expandedTaskViewportRows(this.lastRenderWidth);
673
682
  const maxO = maxScrollOffset(list.length, rows);
674
- if (maxO <= 0) return false;
683
+ if (maxO <= 0) return true;
675
684
  let offset = clampScrollOffset(this.expandedScrollOffset, list.length, rows);
676
685
  if (key === "up") offset -= 1;
677
686
  else if (key === "down") offset += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-x",
3
- "version": "0.32.0",
3
+ "version": "0.32.2",
4
4
  "description": "Adds /goal to pi: conversational goal planning, flexible or ordered goals, persistent progress, and an independent completion auditor.",
5
5
  "license": "MIT",
6
6
  "author": "pi-goal-x contributors",