@sassywares/pi-extensions 0.3.21 → 0.3.22

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
@@ -151,7 +151,7 @@ Writes do not fail quietly and they are not matched by pattern. `rm`, `sed -i`,
151
151
 
152
152
  A backend is only trusted after it proves both halves of the contract: reads inside the working directory must succeed **and** a probe write there must fail. A backend that is installed but cannot enforce (for example bubblewrap on a distribution that blocks unprivileged user namespaces) is discarded, and ask mode continues without bash rather than falling back to a command allowlist. The working directory always wins over the temp allowance: if the project itself lives under the temp directory, temp writes are denied too and `/modes` reports `temp dir is read-only`.
153
153
 
154
- `/modes` shows the verdict, and the footer status line shows `⏸ ask · ro(sandbox-exec)`, `⏸ ask · bash off`, or `▶ work`.
154
+ `/modes` shows the verdict. The mode badge sits on a footer line of its own, aligned to the right edge: `▶ work`, `⏸ ask · ro(sandbox-exec)`, or `⏸ ask · bash off`. To place it there without reimplementing pi's footer, the extension delegates to pi's own `FooterComponent` and appends the badge line, so directory, session name, token stats, model, and context usage stay exactly as pi renders them. If a future pi release changes what that component reads, the extension restores the native footer and falls back to a left-aligned status line.
155
155
 
156
156
  - `--ask` starts a session in ask mode; the mode persists across resume.
157
157
  - `shift+tab` is not used, because pi binds it to the thinking level. To match an editor that uses it for modes, bind `app.thinking.cycle` to something else in `~/.pi/agent/keybindings.json` and add a shortcut there.
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Right-aligned mode badge.
3
+ *
4
+ * `ctx.ui.setStatus()` has no placement: pi appends status texts as a line of
5
+ * their own, left-aligned, and its sanitizer collapses runs of spaces, so a
6
+ * badge cannot be pushed to the right edge from there.
7
+ *
8
+ * This footer keeps pi's own rendering (it delegates to the exported
9
+ * `FooterComponent`) and appends one extra line with the badge aligned to the
10
+ * right edge. Nothing about the directory, session name, token stats, or model
11
+ * line is reimplemented here, so it stays correct as pi's footer evolves.
12
+ *
13
+ * If a future pi version changes what `FooterComponent` reads from its session
14
+ * argument, the render below throws; `onError` then tells the extension to
15
+ * restore the native footer and fall back to the plain status line.
16
+ */
17
+
18
+ import type { Component } from "@earendil-works/pi-tui";
19
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
20
+ import type { ExtensionContext, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
21
+ import { FooterComponent } from "@earendil-works/pi-coding-agent";
22
+
23
+ export interface ModeBadge {
24
+ text: string;
25
+ tone: "warning" | "error" | "accent";
26
+ }
27
+
28
+ export interface ModesFooterOptions {
29
+ /** Session auto-compaction, shown by the footer as the `(auto)` suffix. */
30
+ autoCompact: boolean;
31
+ /** Called once if pi's footer cannot be rendered through this shim. */
32
+ onError: () => void;
33
+ }
34
+
35
+ /**
36
+ * The subset of `AgentSession` that `FooterComponent` reads. Everything except
37
+ * a few computed values is already on the extension context.
38
+ */
39
+ function sessionShim(ctx: ExtensionContext) {
40
+ return {
41
+ model: ctx.model,
42
+ state: { model: ctx.model, thinkingLevel: ctx.thinkingLevel },
43
+ routedModel: undefined,
44
+ getContextUsage: () => ctx.getContextUsage(),
45
+ modelRuntime: {
46
+ // Extensions cannot observe subscription backing; the provider name is
47
+ // the only signal available.
48
+ isUsingSubscription: (provider: string) => provider === "kimi-coding",
49
+ },
50
+ sessionManager: ctx.sessionManager,
51
+ } as unknown as ConstructorParameters<typeof FooterComponent>[0];
52
+ }
53
+
54
+ export function createModesFooter(
55
+ ctx: ExtensionContext,
56
+ theme: Theme,
57
+ footerData: ReadonlyFooterDataProvider,
58
+ badge: () => ModeBadge,
59
+ options: ModesFooterOptions,
60
+ ): Component & { dispose?(): void } {
61
+ const inner = new FooterComponent(sessionShim(ctx), footerData);
62
+ inner.setAutoCompactEnabled(options.autoCompact);
63
+ let failed = false;
64
+
65
+ function badgeLine(width: number): string {
66
+ const current = badge();
67
+ const text = current.text;
68
+ if (visibleWidth(text) >= width) return theme.fg(current.tone, truncateToWidth(text, width, "…"));
69
+ const padding = Math.max(1, width - visibleWidth(text));
70
+ return " ".repeat(padding) + theme.fg(current.tone, text);
71
+ }
72
+
73
+ return {
74
+ invalidate() {
75
+ inner.invalidate();
76
+ },
77
+ render(width: number): string[] {
78
+ if (failed) return [badgeLine(width)];
79
+ try {
80
+ return [...inner.render(width), badgeLine(width)];
81
+ } catch {
82
+ failed = true;
83
+ options.onError();
84
+ return [badgeLine(width)];
85
+ }
86
+ },
87
+ };
88
+ }
@@ -17,6 +17,7 @@
17
17
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
18
18
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
19
19
  import { createWrappedOperations, detectBackend, type SandboxBackend } from "./backends.ts";
20
+ import { createModesFooter, type ModeBadge } from "./footer.ts";
20
21
 
21
22
  type Mode = "work" | "ask";
22
23
 
@@ -51,6 +52,8 @@ export default function modesExtension(pi: ExtensionAPI): void {
51
52
  let backendReason = "not detected yet";
52
53
  let backendChecked = false;
53
54
  let workTools: string[] | undefined;
55
+ let footerStatus: "none" | "custom" | "fallback" = "none";
56
+ let footerTui: { requestRender(): void } | undefined;
54
57
  const wrappedCalls = new Set<string>();
55
58
 
56
59
  function configuredAskTools(): string[] {
@@ -71,13 +74,55 @@ export default function modesExtension(pi: ExtensionAPI): void {
71
74
  pi.appendEntry(STATE_CUSTOM_TYPE, { mode } satisfies PersistedState);
72
75
  }
73
76
 
77
+ function badgeSpec(): ModeBadge {
78
+ if (mode === "work") return { text: "▶ work", tone: "accent" };
79
+ if (backend) return { text: `⏸ ask · ro(${backend.id})`, tone: "warning" };
80
+ return { text: "⏸ ask · bash off", tone: "error" };
81
+ }
82
+
74
83
  function updateStatus(ctx: ExtensionContext): void {
75
- if (mode === "work") {
76
- ctx.ui.setStatus("modes", ctx.ui.theme.fg("accent", "▶ work"));
84
+ // The custom footer draws the badge itself, right-aligned.
85
+ if (footerStatus === "custom") {
86
+ ctx.ui.setStatus("modes", undefined);
87
+ footerTui?.requestRender();
77
88
  return;
78
89
  }
79
- const text = backend ? `⏸ ask · ro(${backend.id})` : "⏸ ask · bash off";
80
- ctx.ui.setStatus("modes", ctx.ui.theme.fg(backend ? "warning" : "error", text));
90
+ const badge = badgeSpec();
91
+ ctx.ui.setStatus("modes", ctx.ui.theme.fg(badge.tone, badge.text));
92
+ }
93
+
94
+ /**
95
+ * Install a footer that keeps pi's rendering and appends the badge aligned to
96
+ * the right edge. Falls back to the native footer plus a status line if pi's
97
+ * footer cannot be driven from here.
98
+ */
99
+ function installFooter(ctx: ExtensionContext): void {
100
+ if (ctx.mode !== "tui" || footerStatus === "custom") return;
101
+ const autoCompact = pi.getSettings().compaction?.enabled !== false;
102
+
103
+ ctx.ui.setFooter((tui, theme, footerData) => {
104
+ footerTui = tui;
105
+ const unsubscribe = footerData.onBranchChange(() => tui.requestRender());
106
+ return {
107
+ ...createModesFooter(ctx, theme, footerData, badgeSpec, {
108
+ autoCompact,
109
+ onError: () => {
110
+ footerStatus = "fallback";
111
+ footerTui = undefined;
112
+ queueMicrotask(() => {
113
+ ctx.ui.setFooter(undefined);
114
+ updateStatus(ctx);
115
+ });
116
+ },
117
+ }),
118
+ dispose() {
119
+ unsubscribe();
120
+ footerTui = undefined;
121
+ },
122
+ };
123
+ });
124
+ footerStatus = "custom";
125
+ ctx.ui.setStatus("modes", undefined);
81
126
  }
82
127
 
83
128
  function describe(): string {
@@ -240,6 +285,7 @@ export default function modesExtension(pi: ExtensionAPI): void {
240
285
  workTools = pi.getActiveTools();
241
286
  pi.setActiveTools(askToolset());
242
287
  }
288
+ installFooter(ctx);
243
289
  updateStatus(ctx);
244
290
  });
245
291
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sassywares/pi-extensions",
3
- "version": "0.3.21",
3
+ "version": "0.3.22",
4
4
  "description": "A zesty power pack for pi: auto-titled sessions, real web access, clean handoffs, isolated subagents, and ask/work modes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,6 +36,7 @@
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-coding-agent": "*",
39
+ "@earendil-works/pi-tui": "*",
39
40
  "typebox": "*"
40
41
  },
41
42
  "pi": {