pi-cockpit 0.22.1 → 0.23.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/README.md CHANGED
@@ -62,8 +62,8 @@ Cockpit has no package dependency on `pi-maestro-flow`. It observes optional pro
62
62
  ```json
63
63
  {
64
64
  "enabled": true,
65
- "quietMode": false,
66
- "quietSymbols": "check",
65
+ "quietMode": true,
66
+ "quietSymbols": "dot",
67
67
  "agentsMode": "list",
68
68
  "todoMode": "list",
69
69
  "todoExpanded": false,
@@ -71,21 +71,21 @@ Cockpit has no package dependency on `pi-maestro-flow`. It observes optional pro
71
71
  "hideNativeAgents": true,
72
72
  "icons": { "mode": "auto" },
73
73
  "sidebar": {
74
- "mode": "auto",
74
+ "mode": "off",
75
75
  "width": 40,
76
76
  "density": "comfortable"
77
77
  },
78
78
  "title": {
79
79
  "enabled": true,
80
80
  "showSession": true,
81
- "showCwd": false,
82
- "showModel": false,
83
- "showThinking": false,
84
- "showGit": false,
85
- "showMaestro": false,
81
+ "showCwd": true,
82
+ "showModel": true,
83
+ "showThinking": true,
84
+ "showGit": true,
85
+ "showMaestro": true,
86
86
  "maxLength": 80
87
87
  },
88
- "theme": "",
88
+ "theme": "cockpit-zen",
89
89
  "usage": {
90
90
  "enabled": true,
91
91
  "footer": true,
@@ -108,16 +108,16 @@ Cockpit has no package dependency on `pi-maestro-flow`. It observes optional pro
108
108
  - `icons.mode`: `"auto"` (detect Nerd Font), `"nerd"`, or `"ascii"`.
109
109
  - `title.enabled`: master switch for the terminal tab title (session summary + working state + opt-in tags). On by default.
110
110
  - `title.showSession`: include the session summary (the `session_info` name, else a short session id) right after `pi`. On by default.
111
- - `title.showCwd`: include the working directory after the session. Off by default — the title stays short, since the tab strip has no room for a wall of tags.
112
- - `title.showModel`: include the active model tag (`m:gpt-5.6-sol`). Off by default.
113
- - `title.showThinking`: include the thinking level tag (`t:high`); skipped while off. Off by default.
114
- - `title.showGit`: include the git branch tag (`git:main`, `git:detached`); read synchronously from `.git/HEAD`, so it costs no process spawn. Off by default.
115
- - `title.showMaestro`: include the Maestro workflow status tag (`wf:running`, `wf:done`). Off by default.
111
+ - `title.showCwd`: include the working directory after the session. On by default.
112
+ - `title.showModel`: include the active model tag (`m:gpt-5.6-sol`). On by default.
113
+ - `title.showThinking`: include the thinking level tag (`t:high`); skipped while off. On by default.
114
+ - `title.showGit`: include the git branch tag (`git:main`, `git:detached`); read synchronously from `.git/HEAD`, so it costs no process spawn. On by default.
115
+ - `title.showMaestro`: include the Maestro workflow status tag (`wf:running`, `wf:done`). On by default.
116
116
  - `title.maxLength`: hard cap on the composed title; the middle is ellided to keep the head and the working-state tail. Clamped to `20..200`; default `80`.
117
117
  - `title.generationModel`: `"provider/model"` (e.g. `"maestro-qwen/qwen3.8-max"`) of a model registered with the `/api-manager` command (or any other provider pi resolves). When set, the session title is generated by that model over the first completed turn (user prompt + assistant reply) through its OpenAI-compatible endpoint, with a 10s timeout. Empty (default) uses the offline rule-based extractor instead. Generation is best-effort — on any failure the title silently falls back to the rule-based one. Editable from the `/cockpit` settings overlay (`z` or cursor to the `title gen model` row, Enter, type, Enter to save; empty clears back to rule-based).
118
118
 
119
119
  The tab title is `frame + pi - <session> - <working state>`. The session part follows Claude Code's chain (`sessionTitle ?? agentTitle ?? haikuTitle ?? default`): the `/session name` title wins, otherwise the generated title, otherwise a short session id. The frame is Claude Code's title chrome: `⠂`/`⠐` braille spinner while a turn runs, static `✳` when idle; failure replaces it with `✗`. On quit, Cockpit clears the title so no stale tab label lingers (Claude Code's `CLEAR_TERMINAL_TITLE`).
120
- - `theme`: named theme override; empty string follows the Pi session theme.
120
+ - `theme`: named theme override; defaults to `"cockpit-zen"`. An empty string follows the Pi session theme.
121
121
  - `usage.enabled`: master switch for the usage bars (quota, balance, spend). On by default; off hides the footer segment and disables the `/usage` command. Ports the [`hknet/pi-usage-bars`](https://github.com/hknet/pi-usage-bars) extension.
122
122
  - `usage.footer`: when `true`, render the live quota bar on a dedicated footer line. The `/usage` command works regardless of this toggle. On by default.
123
123
  - `usage.pollIntervalMs`: how often to refresh usage data, in milliseconds. Clamped to `30000`..`1800000` (30s..30min); `0` switches to manual refresh — no background polling and no footer bar; the `/usage` overlay fetches on open and re-fetches with `r`. Default `120000` (2min). Lower values hit provider APIs more often.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cockpit",
3
- "version": "0.22.1",
3
+ "version": "0.23.0",
4
4
  "description": "Responsive Maestro operations sidebar, fallback status widgets, and footer for Pi.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -18,7 +18,7 @@
18
18
  "README.md"
19
19
  ],
20
20
  "scripts": {
21
- "test": "node --test --experimental-transform-types tests/config.test.ts tests/public-events.test.ts tests/maestro-store.test.ts tests/split-pane.test.ts tests/editor-bottom.test.ts tests/sidebar-render.test.ts tests/sidebar-controller.test.ts tests/viewport-stability.test.ts tests/quiet-tools.test.ts tests/edit-guard.test.ts tests/agents-store.test.ts tests/endpoint-store.test.ts tests/session-ui-state.test.ts tests/session-tabs.test.ts tests/agent-bar.test.ts tests/supervision-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/todo-overlay.test.ts tests/agent-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/agent-scroll.test.ts tests/session-bar.test.ts tests/session-detail.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/teammate-message.test.ts tests/input-routing.test.ts tests/input-history-store.test.ts tests/unified-editor.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/zen-render.test.ts tests/zen-browse.test.ts tests/zen-sheet.test.ts tests/settings-view.test.ts tests/thinking-fold.test.ts tests/thinking-timer.test.ts tests/tick-policy.test.ts tests/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts tests/model-picker.test.ts tests/title-gen.test.ts tests/title-llm.test.ts tests/settings-registry.test.ts tests/settings-coordinator.test.ts tests/settings-locale.test.ts tests/tui-i18n.test.ts tests/settings-shell.test.ts tests/settings-cockpit-provider.test.ts tests/settings-cross-package.test.ts tests/ui-primitives.test.ts tests/settings-shell-secret.test.ts tests/settings-shell-listcrud.test.ts tests/capturing-overlay.test.ts tests/claude-editor.test.ts tests/fullscreen-controller.test.ts tests/transcript-selection.test.ts tests/terminal-cleanup.test.ts tests/terminal-capability.test.ts tests/usage-core.test.ts tests/usage-extension.test.ts tests/usage-history.test.ts tests/usage-sparkline.test.ts",
21
+ "test": "node --test --experimental-transform-types tests/config.test.ts tests/public-events.test.ts tests/maestro-store.test.ts tests/split-pane.test.ts tests/editor-bottom.test.ts tests/sidebar-render.test.ts tests/sidebar-controller.test.ts tests/viewport-stability.test.ts tests/compaction-style.test.ts tests/quiet-tools.test.ts tests/edit-guard.test.ts tests/agents-store.test.ts tests/endpoint-store.test.ts tests/session-ui-state.test.ts tests/session-tabs.test.ts tests/agent-bar.test.ts tests/supervision-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/todo-overlay.test.ts tests/agent-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/agent-scroll.test.ts tests/session-bar.test.ts tests/session-detail.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/teammate-message.test.ts tests/input-routing.test.ts tests/input-history-store.test.ts tests/unified-editor.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/zen-render.test.ts tests/zen-browse.test.ts tests/zen-sheet.test.ts tests/settings-view.test.ts tests/thinking-fold.test.ts tests/thinking-timer.test.ts tests/tick-policy.test.ts tests/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts tests/model-picker.test.ts tests/title-gen.test.ts tests/title-llm.test.ts tests/settings-registry.test.ts tests/settings-coordinator.test.ts tests/settings-locale.test.ts tests/tui-i18n.test.ts tests/settings-shell.test.ts tests/settings-cockpit-provider.test.ts tests/settings-cross-package.test.ts tests/ui-primitives.test.ts tests/settings-shell-secret.test.ts tests/settings-shell-listcrud.test.ts tests/capturing-overlay.test.ts tests/claude-editor.test.ts tests/fullscreen-controller.test.ts tests/transcript-selection.test.ts tests/terminal-cleanup.test.ts tests/terminal-capability.test.ts tests/usage-core.test.ts tests/usage-extension.test.ts tests/usage-history.test.ts tests/usage-sparkline.test.ts",
22
22
  "typecheck": "tsc --noEmit",
23
23
  "prepack": "node ../../scripts/bundle-settings-core.mjs",
24
24
  "postpack": "node ../../scripts/bundle-settings-core.mjs --clean"
@@ -72,7 +72,7 @@
72
72
  "@earendil-works/pi-coding-agent": "0.84.4",
73
73
  "@earendil-works/pi-tui": "0.84.4",
74
74
  "@types/node": "^22.0.0",
75
- "pi-maestro-teammate": "2.5.0",
75
+ "pi-maestro-teammate": "2.6.0",
76
76
  "typebox": "1.3.7",
77
77
  "typescript": "^5.5.0"
78
78
  },
@@ -0,0 +1,233 @@
1
+ import {
2
+ CompactionSummaryMessageComponent,
3
+ keyText,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import {
6
+ truncateToWidth,
7
+ type Component,
8
+ visibleWidth,
9
+ wrapTextWithAnsi,
10
+ } from "@earendil-works/pi-tui";
11
+ import type { IconGlyphs } from "./icons.ts";
12
+
13
+ const COMPACTION_STYLE_MARKER = Symbol.for("pi-cockpit.compaction-style");
14
+ const NEW_CONTEXT_CAPSULE = "Maestro New Context Recovery Capsule";
15
+
16
+ interface CompactionSummaryLike {
17
+ summary: string;
18
+ tokensBefore: number;
19
+ timestamp: number;
20
+ }
21
+
22
+ interface CompactionComponentInternals {
23
+ expanded: boolean;
24
+ message: CompactionSummaryLike;
25
+ clear(): void;
26
+ addChild(component: Component): void;
27
+ }
28
+
29
+ interface CompactionTheme {
30
+ fg(role: string, text: string): string;
31
+ bold(text: string): string;
32
+ }
33
+
34
+ interface StyleProvider {
35
+ isEnabled(): boolean;
36
+ getTheme(): CompactionTheme | undefined;
37
+ getGlyphs(): IconGlyphs;
38
+ }
39
+
40
+ const PLAIN_THEME: CompactionTheme = {
41
+ fg: (_role, text) => text,
42
+ bold: (text) => text,
43
+ };
44
+
45
+ type UpdateDisplay = (this: CompactionComponentInternals) => void;
46
+
47
+ interface CompactionStyleMarker {
48
+ original: UpdateDisplay;
49
+ retain(provider: StyleProvider): () => void;
50
+ }
51
+
52
+ export interface CompactionStylePatch {
53
+ active: boolean;
54
+ detach(): void;
55
+ }
56
+
57
+ export interface CompactionSummaryDetails {
58
+ kind: "new-context" | "compaction";
59
+ checkpoint?: string;
60
+ todo?: string;
61
+ nextTask?: string;
62
+ lineCount: number;
63
+ byteCount: number;
64
+ }
65
+
66
+ function once(action: () => void): () => void {
67
+ let called = false;
68
+ return () => {
69
+ if (called) return;
70
+ called = true;
71
+ action();
72
+ };
73
+ }
74
+
75
+ function clean(value: string, max = 180): string {
76
+ const safe = value
77
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
78
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/g, " ")
79
+ .replace(/\s+/g, " ")
80
+ .trim();
81
+ return safe.length > max ? `${safe.slice(0, max - 1)}…` : safe;
82
+ }
83
+
84
+ function field(summary: string, label: string): string | undefined {
85
+ const line = summary.split("\n").find((candidate) => candidate.trimStart().startsWith(`- ${label}:`));
86
+ if (!line) return undefined;
87
+ const value = clean(line.slice(line.indexOf(":") + 1));
88
+ return value || undefined;
89
+ }
90
+
91
+ function nextTask(summary: string): string | undefined {
92
+ const section = summary.match(/## (?:Active Todo Tasks|Runnable Pending Frontier)\s*\n- \[#([^\]]+)\] ([^\n]+)/);
93
+ if (!section) return undefined;
94
+ const subject = clean(section[2].replace(/ \((?:in_progress|pending|blocked|completed|deleted); assignee=.*$/, ""));
95
+ return clean(`#${section[1]} ${subject}`);
96
+ }
97
+
98
+ export function compactionSummaryDetails(summary: string): CompactionSummaryDetails {
99
+ const normalized = typeof summary === "string" ? summary : "";
100
+ return {
101
+ kind: normalized.includes(NEW_CONTEXT_CAPSULE) || normalized.startsWith("<recovery_capsule")
102
+ ? "new-context"
103
+ : "compaction",
104
+ checkpoint: field(normalized, "Checkpoint ID"),
105
+ todo: field(normalized, "Todo"),
106
+ nextTask: nextTask(normalized),
107
+ lineCount: normalized === "" ? 0 : normalized.split("\n").length,
108
+ byteCount: Buffer.byteLength(normalized, "utf8"),
109
+ };
110
+ }
111
+
112
+ function formatBytes(bytes: number): string {
113
+ if (bytes < 1_024) return `${bytes}B`;
114
+ const kib = bytes / 1_024;
115
+ return `${kib >= 10 ? Math.round(kib) : kib.toFixed(1)}KB`;
116
+ }
117
+
118
+ function card(message: CompactionSummaryLike, provider: StyleProvider): Component {
119
+ return {
120
+ render(width: number): string[] {
121
+ const safeWidth = Math.max(1, width);
122
+ if (safeWidth <= 1) return [];
123
+ const theme = provider.getTheme() ?? PLAIN_THEME;
124
+ const glyphs = provider.getGlyphs();
125
+ const expandHint = keyText("app.tools.expand") || "ctrl+o";
126
+ const tokenText = message.tokensBefore.toLocaleString("en-US");
127
+ if (!provider.isEnabled()) {
128
+ return [
129
+ truncateToWidth(theme.fg("customMessageLabel", theme.bold("[compaction]")), safeWidth, glyphs.ellipsis),
130
+ "",
131
+ truncateToWidth(
132
+ theme.fg("customMessageText", `Compacted from ${tokenText} tokens (`)
133
+ + theme.fg("dim", expandHint)
134
+ + theme.fg("customMessageText", " to expand)"),
135
+ safeWidth,
136
+ glyphs.ellipsis,
137
+ ),
138
+ ];
139
+ }
140
+
141
+ const details = compactionSummaryDetails(message.summary);
142
+ const title = details.kind === "new-context" ? "new context" : "compaction";
143
+ const summaryKind = details.kind === "new-context" ? "deterministic recovery capsule" : "model summary";
144
+ const rows = [`${theme.fg("success", `${glyphs.check} complete`)} ${summaryKind}`];
145
+ if (details.todo) rows.push(`${theme.fg("accent", `${glyphs.pending} todo`)} ${details.todo}`);
146
+ if (details.nextTask) rows.push(`${theme.fg("warning", `${glyphs.arrow} next`)} ${details.nextTask}`);
147
+ rows.push(theme.fg("dim", [
148
+ details.checkpoint ? `checkpoint ${details.checkpoint}` : "summary",
149
+ `${details.lineCount} lines`,
150
+ formatBytes(details.byteCount),
151
+ `${expandHint} expand`,
152
+ ].join(" · ")));
153
+
154
+ const cardWidth = safeWidth - 1;
155
+ const label = `${theme.fg("success", glyphs.check)} ${theme.fg("toolTitle", theme.bold(title))} ${theme.fg("dim", `· ${tokenText} tokens`)}`;
156
+ if (cardWidth < 6) return [truncateToWidth(label, cardWidth, glyphs.ellipsis)];
157
+
158
+ const innerWidth = cardWidth - 2;
159
+ const contentWidth = Math.max(1, innerWidth - 2);
160
+ const fit = (text: string, target: number): string => {
161
+ const clipped = truncateToWidth(text, target, glyphs.ellipsis);
162
+ return `${clipped}${" ".repeat(Math.max(0, target - visibleWidth(clipped)))}`;
163
+ };
164
+ const header = truncateToWidth(` ${label} `, innerWidth, glyphs.ellipsis);
165
+ const body = rows.flatMap((row) => wrapTextWithAnsi(row, contentWidth));
166
+ return [
167
+ `${theme.fg("dim", glyphs.box.topLeft)}${header}${theme.fg("dim", `${glyphs.box.horizontal.repeat(Math.max(0, innerWidth - visibleWidth(header)))}${glyphs.box.topRight}`)}`,
168
+ ...body.map((row) => `${theme.fg("dim", glyphs.box.vertical)} ${fit(row, contentWidth)} ${theme.fg("dim", glyphs.box.vertical)}`),
169
+ theme.fg("dim", `${glyphs.box.bottomLeft}${glyphs.box.horizontal.repeat(innerWidth)}${glyphs.box.bottomRight}`),
170
+ ];
171
+ },
172
+ invalidate(): void {},
173
+ };
174
+ }
175
+
176
+ function markerOf(value: unknown): CompactionStyleMarker | undefined {
177
+ if (typeof value !== "function") return undefined;
178
+ return (value as UpdateDisplay & Record<symbol, CompactionStyleMarker | undefined>)[COMPACTION_STYLE_MARKER];
179
+ }
180
+
181
+ /**
182
+ * Pi does not expose a renderer hook for its built-in compaction message. Patch
183
+ * the exported component's stable method while Cockpit is active, and retain the
184
+ * native expanded Markdown view and a reversible fallback for incompatible hosts.
185
+ */
186
+ export function attachCompactionStyle(provider: StyleProvider): CompactionStylePatch {
187
+ try {
188
+ const prototype = CompactionSummaryMessageComponent.prototype as unknown as object;
189
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, "updateDisplay");
190
+ if (!descriptor || typeof descriptor.value !== "function" || descriptor.writable !== true) {
191
+ return { active: false, detach() {} };
192
+ }
193
+ const existing = markerOf(descriptor.value);
194
+ if (existing) return { active: true, detach: existing.retain(provider) };
195
+
196
+ const original = descriptor.value as UpdateDisplay;
197
+ const providers: StyleProvider[] = [provider];
198
+ const wrapped: UpdateDisplay = function () {
199
+ const active = providers.at(-1);
200
+ if (!active || this.expanded || typeof this.message?.summary !== "string") {
201
+ original.call(this);
202
+ return;
203
+ }
204
+ try {
205
+ this.clear();
206
+ this.addChild(card(this.message, active));
207
+ } catch {
208
+ original.call(this);
209
+ }
210
+ };
211
+ const release = (retained: StyleProvider): void => {
212
+ const index = providers.lastIndexOf(retained);
213
+ if (index >= 0) providers.splice(index, 1);
214
+ if (providers.length === 0 && Object.getOwnPropertyDescriptor(prototype, "updateDisplay")?.value === wrapped) {
215
+ Object.defineProperty(prototype, "updateDisplay", descriptor);
216
+ }
217
+ };
218
+ const retain = (retained: StyleProvider): (() => void) => {
219
+ providers.push(retained);
220
+ return once(() => release(retained));
221
+ };
222
+ Object.defineProperty(wrapped, COMPACTION_STYLE_MARKER, {
223
+ value: { original, retain } satisfies CompactionStyleMarker,
224
+ });
225
+ Object.defineProperty(prototype, "updateDisplay", { ...descriptor, value: wrapped });
226
+ if (Object.getOwnPropertyDescriptor(prototype, "updateDisplay")?.value !== wrapped) {
227
+ return { active: false, detach() {} };
228
+ }
229
+ return { active: true, detach: once(() => release(provider)) };
230
+ } catch {
231
+ return { active: false, detach() {} };
232
+ }
233
+ }
package/src/footer.ts CHANGED
@@ -238,8 +238,11 @@ function alignRight(left: string, right: string, width: number, measure: WidthUt
238
238
  }
239
239
 
240
240
  export function renderFooter(p: FooterParts): string[] {
241
- const { width, theme, glyphs: g, utils } = p;
242
- if (width <= 0) return [""];
241
+ const { width: viewportWidth, theme, glyphs: g, utils } = p;
242
+ if (viewportWidth <= 0) return [""];
243
+ // Footer values repaint on usage, Git, workflow and background-job updates.
244
+ // Reserve the last terminal column so those rewrites cannot trigger auto-wrap.
245
+ const width = Math.max(1, viewportWidth - 1);
243
246
  const ell = theme.fg("dim", g.ellipsis);
244
247
  const sep = theme.fg("dim", g.separator.trim());
245
248
 
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ import { MaestroStore } from "./maestro-store.ts";
25
25
  import { createSidebarController, type SidebarController } from "./sidebar-controller.ts";
26
26
  import { COCKPIT_SPLIT_PANE_MARKER } from "./split-pane.ts";
27
27
  import { attachViewportStability, type ViewportStabilityPatch } from "./viewport-stability.ts";
28
+ import { attachCompactionStyle, type CompactionStylePatch } from "./compaction-style.ts";
28
29
  import {
29
30
  COCKPIT_EDITOR_BOTTOM_MARKER,
30
31
  EDITOR_BOTTOM_WIDGET_KEY,
@@ -86,7 +87,7 @@ import {
86
87
  buildZenTaskSheet,
87
88
  type ZenSheetDocument,
88
89
  } from "./zen-sheet.ts";
89
- import { isLegacyTodoOverlayInput, routeAgentInput } from "./input-routing.ts";
90
+ import { isLegacyTodoOverlayInput, isLocalInputText, routeAgentInput } from "./input-routing.ts";
90
91
  import { activeThemeName, ThemePicker } from "./theme-picker.ts";
91
92
  import { ModelPicker, type ModelPickerEntry } from "./model-picker.ts";
92
93
  import { getUsageTotals, invalidateUsageCache, renderFooter, setUsageThrottle, type PaintTheme, type WidthUtils } from "./footer.ts";
@@ -131,6 +132,13 @@ import {
131
132
  type CockpitConfig,
132
133
  } from "./types.ts";
133
134
  import type { MailboxHostRegistry } from "pi-maestro-teammate/v1/mailbox";
135
+ import {
136
+ buildCockpitTargetCatalogue,
137
+ createTargetAutocompleteProvider,
138
+ formatPendingTargetContext,
139
+ routeCanonicalTargetInput,
140
+ type PendingTargetReference,
141
+ } from "./target-integration.ts";
134
142
 
135
143
  export {
136
144
  EndpointStore,
@@ -153,6 +161,16 @@ export { makeWindowThreadWidget, renderWindowThreadView, windowThreadBody } from
153
161
 
154
162
  const MAILBOX_REGISTRY_KEY = Symbol.for("pi-maestro-teammate.mailbox-registry");
155
163
 
164
+ async function listSshHostPickerEntries() {
165
+ const sshHosts = await import("pi-maestro-teammate/v1/ssh-hosts");
166
+ return sshHosts.listSshHostPickerEntries();
167
+ }
168
+
169
+ async function activateSshHost(hostId: string): Promise<void> {
170
+ const sshHosts = await import("pi-maestro-teammate/v1/ssh-hosts");
171
+ await sshHosts.activateSshHost(hostId);
172
+ }
173
+
156
174
  const FOOTER_UTILS: WidthUtils = { measure: visibleWidth, clip: truncateToWidth };
157
175
  const BASH_BG_OVERLAY_KEY = "alt+j";
158
176
  const TODO_OVERLAY_KEY = "alt+shift+t";
@@ -361,6 +379,7 @@ export default function (pi: ExtensionAPI): void {
361
379
  // thinking label / teammate tree still stream above the visible viewport.
362
380
  let stabilityTui: TUI | undefined;
363
381
  let viewportStabilityPatch: ViewportStabilityPatch | undefined;
382
+ let compactionStylePatch: CompactionStylePatch | undefined;
364
383
  const ensureViewportStability = (tui: TUI): void => {
365
384
  // Native fullscreen owns a fixed application viewport and has no main-screen
366
385
  // applyLineResets hook. Do not cache that expected miss: the same dynamic TUI
@@ -433,6 +452,7 @@ export default function (pi: ExtensionAPI): void {
433
452
  // stand-in for Claude Code's Haiku title). Cleared per session.
434
453
  let aiTitle: string | undefined;
435
454
  let firstUserText: string | undefined;
455
+ let pendingTargetReference: PendingTargetReference | undefined;
436
456
  let titleRequested = false;
437
457
  let titleFrameIndex = 0;
438
458
  let mainOutputRevision = 0;
@@ -465,6 +485,11 @@ export default function (pi: ExtensionAPI): void {
465
485
  // AgentSession construction — before renderInitialMessages runs.
466
486
  ensureConfigExists();
467
487
  config = loadConfig();
488
+ compactionStylePatch = attachCompactionStyle({
489
+ isEnabled: () => config.enabled,
490
+ getTheme: () => lastCtx?.ui.theme,
491
+ getGlyphs: () => resolveGlyphs(config.icons.mode),
492
+ });
468
493
  // Guarded edit replaces the built-in edit (same name, same execution, plus a
469
494
  // UTF-8 gate): editing a non-UTF-8 file would otherwise corrupt its bytes.
470
495
  registerGuardedEditTool(pi);
@@ -912,6 +937,13 @@ export default function (pi: ExtensionAPI): void {
912
937
  current: tuiT("window.autocompleteCurrent"),
913
938
  peer: tuiT("window.autocompletePeer"),
914
939
  });
940
+ const targetCatalogue = async () => {
941
+ try {
942
+ return buildCockpitTargetCatalogue(endpoints.snapshot(), await listSshHostPickerEntries(), true);
943
+ } catch {
944
+ return buildCockpitTargetCatalogue(endpoints.snapshot(), [], false);
945
+ }
946
+ };
915
947
 
916
948
  const publishInputTarget = (force = false): void => {
917
949
  const target = sessionUi.mode === "window" ? selectedWindowInputTarget() : selectedAgentTarget();
@@ -1841,6 +1873,7 @@ export default function (pi: ExtensionAPI): void {
1841
1873
  maestro.clear();
1842
1874
  aiTitle = undefined;
1843
1875
  firstUserText = undefined;
1876
+ pendingTargetReference = undefined;
1844
1877
  titleRequested = false;
1845
1878
  titleFrameIndex = 0;
1846
1879
  titleGeneration += 1;
@@ -1855,10 +1888,13 @@ export default function (pi: ExtensionAPI): void {
1855
1888
  settingsLocale.reload();
1856
1889
  settingsRegistry.emitLocale(settingsLocale.locale);
1857
1890
  selectedHashWindowTargets.clear();
1858
- ctx.ui.addAutocompleteProvider((current) => createWindowAutocompleteProvider(
1859
- current,
1860
- () => config.enabled ? windowAutocompleteTargets() : [],
1861
- (target) => selectedHashWindowTargets.set(target.token.toLocaleLowerCase("en"), target),
1891
+ ctx.ui.addAutocompleteProvider((current) => createTargetAutocompleteProvider(
1892
+ createWindowAutocompleteProvider(
1893
+ current,
1894
+ () => config.enabled ? windowAutocompleteTargets() : [],
1895
+ (target) => selectedHashWindowTargets.set(target.token.toLocaleLowerCase("en"), target),
1896
+ ),
1897
+ async () => config.enabled ? targetCatalogue() : buildCockpitTargetCatalogue(endpoints.snapshot(), [], false),
1862
1898
  ));
1863
1899
  invalidateUsageCache();
1864
1900
  // Quiet mode: register compact tool renderers and fold thinking blocks.
@@ -1952,7 +1988,10 @@ export default function (pi: ExtensionAPI): void {
1952
1988
  }
1953
1989
  settingsProviderDisposer = undefined;
1954
1990
  endpoints.disconnect();
1991
+ compactionStylePatch?.detach();
1992
+ compactionStylePatch = undefined;
1955
1993
  lastCtx = undefined;
1994
+ pendingTargetReference = undefined;
1956
1995
  running = false;
1957
1996
  runningStartedAt = undefined;
1958
1997
  uiPromptDepth = 0;
@@ -1971,6 +2010,13 @@ export default function (pi: ExtensionAPI): void {
1971
2010
  usageSubsystem?.stop(ctx);
1972
2011
  });
1973
2012
 
2013
+ pi.on("before_agent_start", async (event) => {
2014
+ const reference = pendingTargetReference;
2015
+ pendingTargetReference = undefined;
2016
+ if (!reference) return undefined;
2017
+ const context = formatPendingTargetContext(reference, await targetCatalogue());
2018
+ return context ? { systemPrompt: `${event.systemPrompt}\n\n${context}` } : undefined;
2019
+ });
1974
2020
  pi.on("agent_start", () => {
1975
2021
  running = true;
1976
2022
  runningStartedAt = Date.now();
@@ -1978,6 +2024,7 @@ export default function (pi: ExtensionAPI): void {
1978
2024
  req();
1979
2025
  });
1980
2026
  pi.on("agent_end", () => {
2027
+ pendingTargetReference = undefined;
1981
2028
  running = false;
1982
2029
  runningStartedAt = undefined;
1983
2030
  activeTools.clear();
@@ -2038,7 +2085,41 @@ export default function (pi: ExtensionAPI): void {
2038
2085
  const mailboxRegistry = globals[MAILBOX_REGISTRY_KEY] as MailboxHostRegistry | undefined;
2039
2086
  const hasImages = (e.images?.length ?? 0) > 0;
2040
2087
  const interactiveText = e.source === "interactive" && e.text.trim().length > 0;
2041
- const isSynthetic = e.text.startsWith("/") || e.text.startsWith("!");
2088
+
2089
+ if (config.enabled && e.source === "interactive") {
2090
+ pendingTargetReference = undefined;
2091
+ const canonical = await routeCanonicalTargetInput({
2092
+ text: e.text,
2093
+ hasImages,
2094
+ getCatalogue: targetCatalogue,
2095
+ activateSsh: activateSshHost,
2096
+ send: async (request) => {
2097
+ const liveRegistry = sessionRegistry() ?? endpoints.registry;
2098
+ const delivery = await (liveRegistry?.send?.({
2099
+ ...request,
2100
+ source: "user",
2101
+ }) ?? liveRegistry?.router?.route({
2102
+ ...request,
2103
+ source: "user",
2104
+ }));
2105
+ return delivery ?? { delivered: false, error: tuiT("notice.deliveryRegistryUnavailable") };
2106
+ },
2107
+ notify: (message, type) => ctx.ui.notify(message, type),
2108
+ restore: (text) => {
2109
+ const selectedId = sessionUi.selectedId(sessionUi.mode);
2110
+ if (selectedId) sessionUi.setDraft(selectedId, text);
2111
+ ctx.ui.setEditorText(text);
2112
+ },
2113
+ });
2114
+ if (canonical?.action === "handled") return { action: "handled" as const };
2115
+ if (canonical?.action === "transform") {
2116
+ pendingTargetReference = canonical.reference;
2117
+ if (!firstUserText) firstUserText = canonical.text;
2118
+ return { action: "transform" as const, text: canonical.text };
2119
+ }
2120
+ }
2121
+
2122
+ const isSynthetic = isLocalInputText(e.text);
2042
2123
  const hashWindowRoute = config.enabled && e.source === "interactive" && !isSynthetic
2043
2124
  ? resolveWindowRouteInput(e.text, windowAutocompleteTargets(), selectedHashWindowTargets)
2044
2125
  : undefined;
@@ -38,6 +38,11 @@ export function isLegacyTodoOverlayInput(data: string): boolean {
38
38
  return data === "\x1bT";
39
39
  }
40
40
 
41
+ /** Inputs owned by the local Pi host rather than the selected Agent/Window route. */
42
+ export function isLocalInputText(text: string): boolean {
43
+ return text.startsWith("/") || text.startsWith("!") || text.trim().toLocaleLowerCase("en") === "#ssh";
44
+ }
45
+
41
46
  function inputRegistries(provider: AgentInputRegistryProvider | undefined): AgentInputRegistries {
42
47
  if (provider && "deliverAgentMessage" in provider) return { mailbox: provider };
43
48
  return provider ?? {};
@@ -56,8 +61,7 @@ export async function routeAgentInput(
56
61
  ): Promise<"continue" | "handled"> {
57
62
  const hasImages = (event.images?.length ?? 0) > 0;
58
63
  const interactive = event.source === "interactive" && (event.text.trim().length > 0 || hasImages);
59
- const synthetic = event.text.startsWith("/") || event.text.startsWith("!");
60
- if (!interactive || synthetic || !target) return "continue";
64
+ if (!interactive || isLocalInputText(event.text) || !target) return "continue";
61
65
 
62
66
  if (hasImages) {
63
67
  ui.notify(tuiT("notice.agentImage", { label: target.label }), "warning");
@@ -226,7 +226,7 @@ export function makeSessionDetailWidget(deps: SessionDetailDeps) {
226
226
  return renderSessionDetail(
227
227
  deps.getAgents(),
228
228
  deps.getViewingId(),
229
- width,
229
+ Math.max(1, width - 1),
230
230
  theme,
231
231
  maxRows,
232
232
  deps.getScroll?.(),
@@ -72,6 +72,9 @@ export function makeTodoWidget(deps: TodoWidgetDeps) {
72
72
  const cfg = deps.getConfig();
73
73
  const todos = deps.getTodos();
74
74
  if (todos.length === 0) return [];
75
+ // Todo rows repaint as task state changes. Keep the terminal's final
76
+ // column empty so auto-wrap cannot desynchronize pi-tui's cursor model.
77
+ const liveWidth = Math.max(1, width - 1);
75
78
  const expanded = deps.getExpanded?.() ?? cfg.todoExpanded;
76
79
  const g = resolveGlyphs(cfg.icons.mode);
77
80
  // Todo row glyphs come from the glyph table (a hollow rectangle for
@@ -81,7 +84,7 @@ export function makeTodoWidget(deps: TodoWidgetDeps) {
81
84
  expanded,
82
85
  maxRows: panelRows(terminalRows(tui)),
83
86
  };
84
- return renderTodos(todos, expanded ? "list" : cfg.todoMode, width, paint, UTILS, opts);
87
+ return renderTodos(todos, expanded ? "list" : cfg.todoMode, liveWidth, paint, UTILS, opts);
85
88
  },
86
89
  invalidate(): void {},
87
90
  dispose(): void {},