pi-better-harness 0.3.4 → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-background-tasks",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "Pi extension for durable background shell tasks, watchers, logs, and status inspection.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -97,7 +97,6 @@ type NavigatorState = {
97
97
  };
98
98
 
99
99
  const GLOBAL_KEY = Symbol.for("pi-better-harness.navigator.state");
100
- const PLAN_NAVIGATION_KEY = Symbol.for("pi-better-harness.plan-navigation.state");
101
100
  const FACTORY_MARK = "__piBetterHarnessNavigatorFactory";
102
101
  const FACTORY_REFRESH = "__piBetterHarnessNavigatorRefresh";
103
102
 
@@ -121,23 +120,6 @@ function state(): NavigatorState {
121
120
  return g[GLOBAL_KEY]!;
122
121
  }
123
122
 
124
- type PlanNavigationState = {
125
- visible: boolean;
126
- progressLabel?: string;
127
- releaseWorkFocus?: () => void;
128
- refreshNavigationHint?: () => void;
129
- };
130
-
131
- function planNavigationState(): PlanNavigationState {
132
- const global = globalThis as typeof globalThis & { [PLAN_NAVIGATION_KEY]?: PlanNavigationState };
133
- if (!global[PLAN_NAVIGATION_KEY]) global[PLAN_NAVIGATION_KEY] = { visible: false };
134
- return global[PLAN_NAVIGATION_KEY]!;
135
- }
136
-
137
- function refreshPlanNavigationHint(): void {
138
- refreshBackgroundWorkNavigator();
139
- }
140
-
141
123
  export function registerBackgroundWorkProvider(provider: BackgroundWorkProvider): () => void {
142
124
  const s = state();
143
125
  const previousUnsub = s.unsubscribers.get(provider.id);
@@ -179,12 +161,7 @@ export function isNavigatorUiAvailable(ctx: ExtensionContext | undefined): boole
179
161
  }
180
162
 
181
163
  export function navigatorFooterHint(count: number): string | null {
182
- const work = count > 0 ? `← work · ${count}` : null;
183
- const navigation = planNavigationState();
184
- const plan = navigation.visible
185
- ? `→ plan${navigation.progressLabel ? ` · ${navigation.progressLabel}` : ""}`
186
- : null;
187
- return [work, plan].filter((hint): hint is string => hint !== null).join(" ") || null;
164
+ return count > 0 ? `← work · ${count}` : null;
188
165
  }
189
166
 
190
167
  export function applyNavigatorFooter(ui: { setStatus(key: string, value: string | undefined): void }, count: number): string | null {
@@ -208,10 +185,6 @@ export function ensureBackgroundWorkNavigator(ctx: ExtensionContext, deps: HostD
208
185
  s.mainListRequestRender = undefined;
209
186
  s.mainListDeadlineScheduler?.dispose();
210
187
  s.mainListDeadlineScheduler = createRenderScheduler(() => refreshMainListWidget());
211
- const planNavigation = planNavigationState();
212
- planNavigation.releaseWorkFocus = unfocusMainList;
213
- planNavigation.refreshNavigationHint = refreshPlanNavigationHint;
214
- try { (ctx.ui as any).setStatus?.("pi-better-plan-nav", undefined); } catch { /* ignore */ }
215
188
  installNavigatorEditor(ctx.ui as any, deps);
216
189
  s.lastHint = undefined;
217
190
  refreshBackgroundWorkNavigator(ctx);
@@ -240,11 +213,6 @@ export function disposeBackgroundWorkNavigator(ctx?: ExtensionContext): void {
240
213
  s.mainListRequestRender = undefined;
241
214
  s.editorComponent = undefined;
242
215
  s.detailOverlayRows = undefined;
243
- const planNavigation = planNavigationState();
244
- planNavigation.releaseWorkFocus = undefined;
245
- if (planNavigation.refreshNavigationHint === refreshPlanNavigationHint) {
246
- planNavigation.refreshNavigationHint = undefined;
247
- }
248
216
  s.mainListSelectedId = undefined;
249
217
  s.mainListFocused = false;
250
218
  }
@@ -492,10 +460,9 @@ function buildMainListLines(
492
460
  }
493
461
 
494
462
  function shortcutsLine(focused: boolean, fg: (color: string, value: string) => string): string {
495
- const plan = planNavigationState().visible ? " · → plan" : "";
496
463
  const keys = focused
497
- ? `↑↓ switch · Enter detail · x stop${plan} · Esc unfocus`
498
- : `← work navigator${plan}`;
464
+ ? "↑↓ switch · Enter detail · x stop · Esc unfocus"
465
+ : "← work navigator";
499
466
  return dim(keys, fg);
500
467
  }
501
468
 
@@ -6,7 +6,6 @@
6
6
 
7
7
  - Gives models `update_plan` and `get_plan` tools for atomic, explicit progress updates.
8
8
  - Shows the complete checklist of completed, active, pending, and blocked steps above the editor.
9
- - Uses `→` from an empty editor to focus the plan; normal cursor movement is preserved when the editor contains text.
10
9
  - Persists plan state and display preferences on the active Pi session branch.
11
10
  - Opens the complete plan with `/plan`.
12
11
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-plan",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Structured execution plans with persistent progress for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
- import { CustomEditor, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { Key, matchesKey, Text } from "@earendil-works/pi-tui";
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { Text } from "@earendil-works/pi-tui";
4
4
  import { Type } from "typebox";
5
5
 
6
6
  import {
@@ -36,74 +36,32 @@ const UpdatePlanSchema = Type.Object({
36
36
  plan: Type.Array(PlanStepSchema, { minItems: 1, maxItems: 50 }),
37
37
  });
38
38
 
39
- const PLAN_NAV_STATUS_KEY = "pi-better-plan-nav";
40
- const PLAN_EDITOR_FACTORY_MARK = "__piBetterPlanFactory";
41
- const PLAN_EDITOR_FACTORY_REFRESH = "__piBetterPlanRefresh";
42
- const PLAN_NAVIGATION_KEY = Symbol.for("pi-better-harness.plan-navigation.state");
43
-
44
- interface SharedPlanNavigationState {
45
- visible: boolean;
46
- progressLabel?: string;
47
- releaseWorkFocus?: () => void;
48
- refreshNavigationHint?: () => void;
49
- }
50
-
51
- function sharedPlanNavigationState(): SharedPlanNavigationState {
52
- const global = globalThis as typeof globalThis & { [PLAN_NAVIGATION_KEY]?: SharedPlanNavigationState };
53
- if (!global[PLAN_NAVIGATION_KEY]) global[PLAN_NAVIGATION_KEY] = { visible: false };
54
- return global[PLAN_NAVIGATION_KEY]!;
55
- }
39
+ const LEGACY_PLAN_NAV_STATUS_KEY = "pi-better-plan-nav";
56
40
 
57
41
  export default function planExtension(pi: ExtensionAPI): void {
58
42
  let currentPlan: PlanSnapshot | null = null;
59
43
  let displayMode: PlanDisplayMode = "auto";
60
- let selectedIndex = 0;
61
- let focused = false;
62
44
  let refreshWidget: ((force?: boolean) => void) | undefined;
63
- let currentCtx: ExtensionContext | undefined;
64
45
 
65
46
  const refresh = (force = false): void => refreshWidget?.(force);
66
47
 
67
- const updateNavigationHint = (ctx = currentCtx): void => {
68
- if (!ctx?.hasUI) return;
69
- const progress = currentPlan ? planProgress(currentPlan) : null;
70
- const visible = currentPlan !== null && displayMode !== "hidden";
71
- const navigation = sharedPlanNavigationState();
72
- navigation.visible = visible;
73
- const suffix = progress?.blocked ? "blocked" : progress ? `${progress.completed}/${progress.total}` : "";
74
- if (suffix) navigation.progressLabel = suffix;
75
- else delete navigation.progressLabel;
76
- if (navigation.refreshNavigationHint) {
77
- ctx.ui.setStatus(PLAN_NAV_STATUS_KEY, undefined);
78
- navigation.refreshNavigationHint();
79
- } else {
80
- ctx.ui.setStatus(PLAN_NAV_STATUS_KEY, visible ? `→ plan · ${suffix}` : undefined);
81
- }
82
- };
83
-
84
48
  const restore = (ctx: ExtensionContext): void => {
85
49
  const state = reconstructPlanState(ctx.sessionManager.getBranch());
86
50
  currentPlan = state.plan;
87
51
  displayMode = state.displayMode;
88
- selectedIndex = preferredIndex(currentPlan);
89
- focused = false;
90
- updateNavigationHint(ctx);
52
+ if (ctx.hasUI) ctx.ui.setStatus(LEGACY_PLAN_NAV_STATUS_KEY, undefined);
91
53
  refresh(true);
92
54
  };
93
55
 
94
56
  const persistPlan = (plan: PlanSnapshot): void => {
95
57
  currentPlan = plan;
96
- selectedIndex = preferredIndex(plan);
97
- focused = false;
98
58
  pi.appendEntry(EXTENSION_NAME, planSetEntry(plan));
99
- updateNavigationHint();
100
59
  refresh(true);
101
60
  };
102
61
 
103
62
  const setDisplayMode = (mode: PlanDisplayMode): void => {
104
63
  displayMode = mode;
105
64
  pi.appendEntry(EXTENSION_NAME, planDisplayEntry(mode));
106
- updateNavigationHint();
107
65
  refresh(true);
108
66
  };
109
67
 
@@ -117,7 +75,7 @@ export default function planExtension(pi: ExtensionAPI): void {
117
75
  return {
118
76
  render(width: number): string[] {
119
77
  if (!currentPlan || displayMode === "hidden") return [];
120
- return renderCompactPlan(currentPlan, width, theme as never, { focused, selectedIndex });
78
+ return renderCompactPlan(currentPlan, width, theme as never);
121
79
  },
122
80
  invalidate() {},
123
81
  dispose() {
@@ -139,8 +97,6 @@ export default function planExtension(pi: ExtensionAPI): void {
139
97
  return;
140
98
  }
141
99
  const snapshot = currentPlan;
142
- focused = false;
143
- refresh();
144
100
  await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
145
101
  const component = createFullPlanComponent(snapshot, theme as never, () => done());
146
102
  return {
@@ -154,80 +110,6 @@ export default function planExtension(pi: ExtensionAPI): void {
154
110
  });
155
111
  };
156
112
 
157
- const handlePlanNavigation = (data: string, ctx: ExtensionContext): boolean => {
158
- if (!currentPlan || displayMode === "hidden") return false;
159
- if (!focused) {
160
- if (!matchesKey(data, Key.right)) return false;
161
- sharedPlanNavigationState().releaseWorkFocus?.();
162
- focused = true;
163
- selectedIndex = preferredIndex(currentPlan);
164
- refresh();
165
- return true;
166
- }
167
- if (matchesKey(data, Key.up)) {
168
- selectedIndex = Math.max(0, selectedIndex - 1);
169
- refresh();
170
- return true;
171
- }
172
- if (matchesKey(data, Key.down)) {
173
- selectedIndex = Math.min(currentPlan.steps.length - 1, selectedIndex + 1);
174
- refresh();
175
- return true;
176
- }
177
- if (matchesKey(data, Key.enter) || matchesKey(data, Key.right)) {
178
- void showFullPlan(ctx);
179
- return true;
180
- }
181
- if (matchesKey(data, Key.left) || matchesKey(data, Key.escape)) {
182
- focused = false;
183
- refresh();
184
- return true;
185
- }
186
- focused = false;
187
- refresh();
188
- return false;
189
- };
190
-
191
- const installEditorNavigation = (ctx: ExtensionContext): void => {
192
- if (ctx.mode !== "tui") return;
193
- const ui = ctx.ui as typeof ctx.ui & {
194
- getEditorComponent?(): ((tui: unknown, theme: unknown, keybindings: unknown) => unknown) | undefined;
195
- setEditorComponent(factory: (tui: unknown, theme: unknown, keybindings: unknown) => unknown): void;
196
- };
197
- const previous = ui.getEditorComponent?.() as
198
- | (((tui: unknown, theme: unknown, keybindings: unknown) => unknown) & Record<string, unknown>)
199
- | undefined;
200
- const handlers = { handle: (data: string) => handlePlanNavigation(data, ctx) };
201
- if (previous?.[PLAN_EDITOR_FACTORY_MARK] === true) {
202
- const replaceHandlers = previous[PLAN_EDITOR_FACTORY_REFRESH];
203
- if (typeof replaceHandlers === "function") replaceHandlers(handlers);
204
- return;
205
- }
206
- let currentHandlers = handlers;
207
- const factory = ((tui: unknown, theme: unknown, keybindings: unknown) => {
208
- const inner = (previous
209
- ? previous(tui, theme, keybindings)
210
- : new CustomEditor(tui as never, theme as never, keybindings as never)) as {
211
- getText?(): string;
212
- handleInput(data: string): void;
213
- };
214
- return new Proxy(inner, {
215
- get(target, property) {
216
- if (property === "handleInput") {
217
- return (data: string) => {
218
- if (target.getText?.() === "" && currentHandlers.handle(data)) return;
219
- target.handleInput(data);
220
- };
221
- }
222
- const value = Reflect.get(target, property);
223
- return typeof value === "function" ? value.bind(target) : value;
224
- },
225
- });
226
- }) as ((tui: unknown, theme: unknown, keybindings: unknown) => unknown) & Record<string, unknown>;
227
- factory[PLAN_EDITOR_FACTORY_MARK] = true;
228
- factory[PLAN_EDITOR_FACTORY_REFRESH] = (next: typeof handlers) => { currentHandlers = next; };
229
- ui.setEditorComponent(factory);
230
- };
231
113
 
232
114
  pi.registerTool({
233
115
  name: "update_plan",
@@ -290,9 +172,7 @@ export default function planExtension(pi: ExtensionAPI): void {
290
172
  if (!input) return showFullPlan(ctx);
291
173
  if (input === "clear") {
292
174
  currentPlan = null;
293
- focused = false;
294
175
  pi.appendEntry(EXTENSION_NAME, planClearEntry());
295
- updateNavigationHint(ctx);
296
176
  refresh(true);
297
177
  ctx.ui.notify("Plan cleared.", "info");
298
178
  return;
@@ -316,13 +196,10 @@ export default function planExtension(pi: ExtensionAPI): void {
316
196
  });
317
197
 
318
198
  pi.on("session_start", async (_event, ctx) => {
319
- currentCtx = ctx;
320
199
  installWidget(ctx);
321
- installEditorNavigation(ctx);
322
200
  restore(ctx);
323
201
  });
324
202
  pi.on("session_tree", async (_event, ctx) => {
325
- currentCtx = ctx;
326
203
  restore(ctx);
327
204
  });
328
205
  pi.on("before_agent_start", async (event) => {
@@ -333,26 +210,13 @@ export default function planExtension(pi: ExtensionAPI): void {
333
210
  });
334
211
  pi.on("session_shutdown", async (_event, ctx) => {
335
212
  refreshWidget = undefined;
336
- currentCtx = undefined;
337
- const navigation = sharedPlanNavigationState();
338
- navigation.visible = false;
339
- delete navigation.progressLabel;
340
- navigation.refreshNavigationHint?.();
341
213
  try {
342
- ctx.ui.setStatus(PLAN_NAV_STATUS_KEY, undefined);
214
+ ctx.ui.setStatus(LEGACY_PLAN_NAV_STATUS_KEY, undefined);
343
215
  ctx.ui.setWidget(EXTENSION_NAME, undefined);
344
216
  } catch { /* best-effort cleanup */ }
345
217
  });
346
218
  }
347
219
 
348
- function preferredIndex(plan: PlanSnapshot | null): number {
349
- if (!plan) return 0;
350
- const progress = planProgress(plan);
351
- if (progress.activeIndex !== null) return progress.activeIndex;
352
- const next = plan.steps.findIndex((item) => item.status !== "completed");
353
- return next >= 0 ? next : Math.max(0, plan.steps.length - 1);
354
- }
355
-
356
220
  function formatProgress(plan: PlanSnapshot): string {
357
221
  const progress = planProgress(plan);
358
222
  const current = progress.activeIndex === null ? null : plan.steps[progress.activeIndex];
@@ -12,10 +12,8 @@ export function renderCompactPlan(
12
12
  plan: PlanSnapshot,
13
13
  width: number,
14
14
  theme: PlanRenderTheme,
15
- options: { focused?: boolean; selectedIndex?: number } = {},
16
15
  ): string[] {
17
16
  const progress = planProgress(plan);
18
- const selected = clampIndex(options.selectedIndex ?? progress.activeIndex ?? firstIncompleteIndex(plan), plan.steps.length);
19
17
  const stateLabel = progress.blocked > 0
20
18
  ? `${progress.blocked} blocked`
21
19
  : progress.state === "complete"
@@ -28,8 +26,7 @@ export function renderCompactPlan(
28
26
 
29
27
  for (let index = 0; index < plan.steps.length; index += 1) {
30
28
  const item = plan.steps[index]!;
31
- const selectedPrefix = options.focused && index === selected ? theme.fg("accent", "› ") : " ";
32
- lines.push(`${selectedPrefix}${stepGlyph(item, theme)} ${index + 1} ${stepText(item, theme)}`);
29
+ lines.push(` ${stepGlyph(item, theme)} ${index + 1} ${stepText(item, theme)}`);
33
30
  }
34
31
  lines.push("");
35
32
  return lines.map((line) => truncateToWidth(line, Math.max(1, width)));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-subagents",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Pi extension for detached, sandboxed subagent runs that keep the foreground session free.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -97,7 +97,6 @@ type NavigatorState = {
97
97
  };
98
98
 
99
99
  const GLOBAL_KEY = Symbol.for("pi-better-harness.navigator.state");
100
- const PLAN_NAVIGATION_KEY = Symbol.for("pi-better-harness.plan-navigation.state");
101
100
  const FACTORY_MARK = "__piBetterHarnessNavigatorFactory";
102
101
  const FACTORY_REFRESH = "__piBetterHarnessNavigatorRefresh";
103
102
 
@@ -121,23 +120,6 @@ function state(): NavigatorState {
121
120
  return g[GLOBAL_KEY]!;
122
121
  }
123
122
 
124
- type PlanNavigationState = {
125
- visible: boolean;
126
- progressLabel?: string;
127
- releaseWorkFocus?: () => void;
128
- refreshNavigationHint?: () => void;
129
- };
130
-
131
- function planNavigationState(): PlanNavigationState {
132
- const global = globalThis as typeof globalThis & { [PLAN_NAVIGATION_KEY]?: PlanNavigationState };
133
- if (!global[PLAN_NAVIGATION_KEY]) global[PLAN_NAVIGATION_KEY] = { visible: false };
134
- return global[PLAN_NAVIGATION_KEY]!;
135
- }
136
-
137
- function refreshPlanNavigationHint(): void {
138
- refreshBackgroundWorkNavigator();
139
- }
140
-
141
123
  export function registerBackgroundWorkProvider(provider: BackgroundWorkProvider): () => void {
142
124
  const s = state();
143
125
  const previousUnsub = s.unsubscribers.get(provider.id);
@@ -179,12 +161,7 @@ export function isNavigatorUiAvailable(ctx: ExtensionContext | undefined): boole
179
161
  }
180
162
 
181
163
  export function navigatorFooterHint(count: number): string | null {
182
- const work = count > 0 ? `← work · ${count}` : null;
183
- const navigation = planNavigationState();
184
- const plan = navigation.visible
185
- ? `→ plan${navigation.progressLabel ? ` · ${navigation.progressLabel}` : ""}`
186
- : null;
187
- return [work, plan].filter((hint): hint is string => hint !== null).join(" ") || null;
164
+ return count > 0 ? `← work · ${count}` : null;
188
165
  }
189
166
 
190
167
  export function applyNavigatorFooter(ui: { setStatus(key: string, value: string | undefined): void }, count: number): string | null {
@@ -208,10 +185,6 @@ export function ensureBackgroundWorkNavigator(ctx: ExtensionContext, deps: HostD
208
185
  s.mainListRequestRender = undefined;
209
186
  s.mainListDeadlineScheduler?.dispose();
210
187
  s.mainListDeadlineScheduler = createRenderScheduler(() => refreshMainListWidget());
211
- const planNavigation = planNavigationState();
212
- planNavigation.releaseWorkFocus = unfocusMainList;
213
- planNavigation.refreshNavigationHint = refreshPlanNavigationHint;
214
- try { (ctx.ui as any).setStatus?.("pi-better-plan-nav", undefined); } catch { /* ignore */ }
215
188
  installNavigatorEditor(ctx.ui as any, deps);
216
189
  s.lastHint = undefined;
217
190
  refreshBackgroundWorkNavigator(ctx);
@@ -240,11 +213,6 @@ export function disposeBackgroundWorkNavigator(ctx?: ExtensionContext): void {
240
213
  s.mainListRequestRender = undefined;
241
214
  s.editorComponent = undefined;
242
215
  s.detailOverlayRows = undefined;
243
- const planNavigation = planNavigationState();
244
- planNavigation.releaseWorkFocus = undefined;
245
- if (planNavigation.refreshNavigationHint === refreshPlanNavigationHint) {
246
- planNavigation.refreshNavigationHint = undefined;
247
- }
248
216
  s.mainListSelectedId = undefined;
249
217
  s.mainListFocused = false;
250
218
  }
@@ -492,10 +460,9 @@ function buildMainListLines(
492
460
  }
493
461
 
494
462
  function shortcutsLine(focused: boolean, fg: (color: string, value: string) => string): string {
495
- const plan = planNavigationState().visible ? " · → plan" : "";
496
463
  const keys = focused
497
- ? `↑↓ switch · Enter detail · x stop${plan} · Esc unfocus`
498
- : `← work navigator${plan}`;
464
+ ? "↑↓ switch · Enter detail · x stop · Esc unfocus"
465
+ : "← work navigator";
499
466
  return dim(keys, fg);
500
467
  }
501
468
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-harness",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, and structured plans.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -50,12 +50,12 @@
50
50
  "test": "node --test test/*.test.mjs"
51
51
  },
52
52
  "dependencies": {
53
- "pi-better-background-tasks": "0.2.10",
53
+ "pi-better-background-tasks": "0.2.11",
54
54
  "pi-better-goal": "0.2.1",
55
- "pi-better-plan": "0.1.2",
55
+ "pi-better-plan": "0.1.3",
56
56
  "pi-better-sandbox": "0.3.0",
57
57
  "pi-better-ssh": "0.1.1",
58
- "pi-better-subagents": "0.1.25"
58
+ "pi-better-subagents": "0.1.26"
59
59
  },
60
60
  "bundledDependencies": [
61
61
  "pi-better-background-tasks",