pi-cockpit 0.9.1 → 0.10.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 +40 -0
- package/package.json +80 -80
- package/src/agent-overlay.ts +337 -0
- package/src/agent-scroll.ts +10 -5
- package/src/agents-store.ts +6 -1
- package/src/ambient.ts +3 -0
- package/src/autocomplete-probe.ts +26 -0
- package/src/bash-bg-overlay.ts +31 -6
- package/src/capturing-overlay.ts +77 -0
- package/src/claude-editor.ts +158 -0
- package/src/config.ts +6 -0
- package/src/footer.ts +9 -9
- package/src/fullscreen-controller.ts +359 -0
- package/src/index.ts +367 -48
- package/src/public/v1/events.ts +13 -0
- package/src/render.ts +18 -10
- package/src/session-bar.ts +41 -6
- package/src/session-detail.ts +4 -3
- package/src/settings/cockpit-provider.ts +52 -4
- package/src/settings-view.ts +27 -0
- package/src/sidebar-controller.ts +6 -0
- package/src/split-pane.ts +18 -2
- package/src/stack-widget.ts +31 -11
- package/src/terminal-capability.ts +25 -0
- package/src/terminal-cleanup.ts +120 -0
- package/src/thinking-fold.ts +67 -11
- package/src/thinking-timer.ts +32 -9
- package/src/todo-store.ts +14 -1
- package/src/transcript-selection.ts +247 -0
- package/src/types.ts +27 -0
- package/src/viewport.ts +28 -0
package/README.md
CHANGED
|
@@ -137,6 +137,46 @@ The original mockup was a browser page; a terminal is an ANSI stream with no DOM
|
|
|
137
137
|
| Scanlines / logo light-up animation | no overlay/animation layer | a braille spinner frame, advanced on each redraw |
|
|
138
138
|
| In-stream thinking-collapse / edit progress bar / colored bash stdout | built-in message & built-in tool rendering is **not** replaceable by extensions (`renderCall`/`renderResult` only apply to tools *you* register) | the conversation stream is left to Pi's native renderer — it is context, not a cockpit deliverable |
|
|
139
139
|
|
|
140
|
+
## Claude Code-style interactions (all opt-in, all default off)
|
|
141
|
+
|
|
142
|
+
Three independent settings, each stored in `cockpit.json` and exposed bilingually in `/cockpit` and `/maestro-settings`:
|
|
143
|
+
|
|
144
|
+
| Setting | Default | Applies | What it does |
|
|
145
|
+
|---|---|---|---|
|
|
146
|
+
| `doubleEscapeClearInput` | off | after `/reload` | Press Escape twice quickly to clear a non-empty input draft. The first Escape keeps its native meaning; an empty-draft double-Escape stays pi's rewind/tree action. Requires the Cockpit custom editor (`/reload`). |
|
|
147
|
+
| `fullscreenInput` | off | after `/reload` | Alternate screen with the editor fixed at the bottom and an application-scrolled transcript. Wheel scrolls history while the editor stays put; `↑ n new · click to bottom` appears when new output arrives while you are scrolled up. Replaces terminal-native scrollback/search inside fullscreen. Requires the Cockpit custom editor (`/reload`). |
|
|
148
|
+
| `copyOnSelect` | off | live | Drag inside the fullscreen transcript to select; the visible text is copied to the clipboard on release. Effective only while `fullscreenInput` is active. |
|
|
149
|
+
|
|
150
|
+
Interaction rules:
|
|
151
|
+
|
|
152
|
+
- The three settings are independent; `copyOnSelect` is inert without `fullscreenInput`.
|
|
153
|
+
- The legacy `pinEditorBottom` keeps working in normal mode and is ignored inside fullscreen.
|
|
154
|
+
- If another extension already owns a custom editor, `doubleEscapeClearInput` and `fullscreenInput` fail closed with one warning (they share the Cockpit custom editor) and never overwrite it.
|
|
155
|
+
- A clipboard failure shows a warning and keeps the selection so you can retry.
|
|
156
|
+
|
|
157
|
+
### Terminal capability matrix
|
|
158
|
+
|
|
159
|
+
Fullscreen needs an alternate screen (`?1049`) and SGR mouse reporting (`1006`/`1002`); copy-on-select additionally needs drag selection to be application-owned. No universal support is claimed — this is best effort, opt-in, and `TERM=dumb`/unset is refused with a warning.
|
|
160
|
+
|
|
161
|
+
| Terminal | Alternate screen | SGR mouse | Copy-on-select | Verified |
|
|
162
|
+
|---|---|---|---|---|
|
|
163
|
+
| iTerm2 | ✓ | ✓ | ✓ | expected |
|
|
164
|
+
| Ghostty | ✓ | ✓ | ✓ | expected |
|
|
165
|
+
| WezTerm | ✓ | ✓ | ✓ | expected |
|
|
166
|
+
| kitty | ✓ | ✓ | ✓ | expected |
|
|
167
|
+
| Windows Terminal | ✓ | ✓ | ✓ | expected |
|
|
168
|
+
| VS Code integrated terminal | ✓ | ✓ | ✓ | expected |
|
|
169
|
+
| tmux (no `-T` config) | depends | depends | depends | not claimed |
|
|
170
|
+
| `TERM=dumb` | ✗ | ✗ | ✗ | refused with a warning |
|
|
171
|
+
|
|
172
|
+
### Manual smoke checklist
|
|
173
|
+
|
|
174
|
+
1. `/maestro-settings` → Cockpit → enable `doubleEscapeClearInput`, then `/reload`; type a draft and press Escape twice — the draft clears; a single Escape does nothing; with an empty draft two Escapes still open the tree selector.
|
|
175
|
+
2. Enable `fullscreenInput`, `/reload`; scroll the transcript with the mouse wheel — the editor stays fixed at the bottom; while scrolled up, new output shows the `↑ n new` hint; clicking it returns to the bottom.
|
|
176
|
+
3. Enable `copyOnSelect` (live, no reload); drag across transcript lines — the visible text lands on the clipboard (verify with `pbpaste` on macOS); a plain click does not copy; a copy failure (e.g. headless) shows a warning and keeps the selection.
|
|
177
|
+
4. Toggle everything off and `/reload` — behavior returns to stock pi.
|
|
178
|
+
5. If a terminal ever gets stuck in a blank alternate screen (crash while fullscreen), run `reset` to restore the normal screen.
|
|
179
|
+
|
|
140
180
|
## Local development
|
|
141
181
|
|
|
142
182
|
```bash
|
package/package.json
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-cockpit",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Responsive Maestro operations sidebar, fallback status widgets, and footer for Pi.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"tui",
|
|
11
|
+
"teammate",
|
|
12
|
+
"todo"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"files": [
|
|
16
|
+
"src/",
|
|
17
|
+
"themes/",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
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/supervision-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-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/input-routing.test.ts tests/integration-contract.test.ts tests/stack-widget.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/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",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"pi": {
|
|
25
|
+
"extensions": [
|
|
26
|
+
"./src/extension/index.ts"
|
|
27
|
+
],
|
|
28
|
+
"themes": [
|
|
29
|
+
"./themes"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"pi-maestro-settings-core": "0.1.0",
|
|
34
|
+
"proper-lockfile": "4.1.2"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@earendil-works/pi-agent-core": "*",
|
|
38
|
+
"@earendil-works/pi-ai": "*",
|
|
39
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
40
|
+
"@earendil-works/pi-tui": "*",
|
|
41
|
+
"pi-maestro-teammate": "^1.6.0",
|
|
42
|
+
"typebox": "*"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"@earendil-works/pi-agent-core": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"@earendil-works/pi-ai": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"@earendil-works/pi-coding-agent": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"@earendil-works/pi-tui": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"pi-maestro-teammate": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"typebox": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@earendil-works/pi-agent-core": "0.83.0",
|
|
66
|
+
"@earendil-works/pi-ai": "0.83.0",
|
|
67
|
+
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
68
|
+
"@earendil-works/pi-tui": "0.83.0",
|
|
69
|
+
"@types/node": "^22.0.0",
|
|
70
|
+
"pi-maestro-teammate": "1.8.0",
|
|
71
|
+
"typebox": "1.3.7",
|
|
72
|
+
"typescript": "^5.5.0"
|
|
73
|
+
},
|
|
74
|
+
"main": "./src/index.ts",
|
|
75
|
+
"exports": {
|
|
76
|
+
".": "./src/index.ts",
|
|
77
|
+
"./v1/events": "./src/public/v1/events.ts",
|
|
78
|
+
"./src/*": "./src/*"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
Key,
|
|
4
|
+
type Component,
|
|
5
|
+
type Focusable,
|
|
6
|
+
matchesKey,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
visibleWidth,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
10
|
+
import { effectiveAgentStatus, type AgentDisplayStatus } from "./agents-store.ts";
|
|
11
|
+
import { scrollBy, type AgentScrollState } from "./agent-scroll.ts";
|
|
12
|
+
import type { IconGlyphs } from "./icons.ts";
|
|
13
|
+
import { visibleStart } from "./layout.ts";
|
|
14
|
+
import { buildAgentTree } from "./render.ts";
|
|
15
|
+
import {
|
|
16
|
+
renderSessionDetail,
|
|
17
|
+
sessionDetailBodyLength,
|
|
18
|
+
sessionDetailWindowRows,
|
|
19
|
+
} from "./session-detail.ts";
|
|
20
|
+
import { visibleAgentRows } from "./stack-widget.ts";
|
|
21
|
+
import type { AgentRow } from "./types.ts";
|
|
22
|
+
import { overlayListRows } from "./viewport.ts";
|
|
23
|
+
|
|
24
|
+
export interface AgentOverlayParams {
|
|
25
|
+
getAgents: () => readonly AgentRow[];
|
|
26
|
+
getViewingId: () => string | undefined;
|
|
27
|
+
onSelect: (correlationId: string) => void;
|
|
28
|
+
onCommand: (correlationId: string, action: "interrupt" | "steer", message?: string) => void;
|
|
29
|
+
requestRender: () => void;
|
|
30
|
+
close: () => void;
|
|
31
|
+
theme: Theme;
|
|
32
|
+
glyphs: IconGlyphs;
|
|
33
|
+
getTerminalRows?: () => number | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CARD_CHROME_ROWS = 5;
|
|
37
|
+
const COMMAND_ACK_MS = 1_500;
|
|
38
|
+
|
|
39
|
+
/** Plain letters reserved for command keys stay out of the draft. */
|
|
40
|
+
function isDraftInput(data: string): boolean {
|
|
41
|
+
return data.length === 1 && data >= " ";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class AgentOverlay implements Component, Focusable {
|
|
45
|
+
focused = false;
|
|
46
|
+
private selectedId: string | undefined;
|
|
47
|
+
private outputScroll: AgentScrollState = { offset: 0, following: true };
|
|
48
|
+
private detailWidth = 80;
|
|
49
|
+
private detailRows = 8;
|
|
50
|
+
private compose: { targetId: string; draft: string } | null = null;
|
|
51
|
+
private lastAck: { text: string; at: number; error?: boolean } | null = null;
|
|
52
|
+
private ackTimer: ReturnType<typeof setTimeout> | undefined;
|
|
53
|
+
|
|
54
|
+
constructor(private readonly params: AgentOverlayParams) {
|
|
55
|
+
this.selectedId = params.getViewingId();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
invalidate(): void {}
|
|
59
|
+
dispose(): void {
|
|
60
|
+
if (this.ackTimer) clearTimeout(this.ackTimer);
|
|
61
|
+
this.ackTimer = undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
handleInput(data: string): void {
|
|
65
|
+
if (this.compose) {
|
|
66
|
+
if (matchesKey(data, Key.escape)) {
|
|
67
|
+
this.compose = null;
|
|
68
|
+
} else if (data === "\r" || data === "\n") {
|
|
69
|
+
const targetId = this.compose.targetId;
|
|
70
|
+
const draft = this.compose.draft.trim();
|
|
71
|
+
this.compose = null;
|
|
72
|
+
// The compose target is frozen: the steer must never silently
|
|
73
|
+
// re-target another agent because the roster selection moved.
|
|
74
|
+
const target = this.entries().find((entry) => entry.row.correlationId === targetId)?.row;
|
|
75
|
+
if (!target) {
|
|
76
|
+
this.ack(`steer aborted: @${this.agentName(targetId)} is gone`, true);
|
|
77
|
+
} else if (draft) {
|
|
78
|
+
this.params.onCommand(targetId, "steer", draft);
|
|
79
|
+
this.ack(`steer @${target.name || target.role || target.agent || targetId}: ${draft}`);
|
|
80
|
+
}
|
|
81
|
+
} else if (data === "\x7f" || data === "\x08") {
|
|
82
|
+
this.compose.draft = this.compose.draft.slice(0, -1);
|
|
83
|
+
} else if (isDraftInput(data)) {
|
|
84
|
+
this.compose.draft += data;
|
|
85
|
+
}
|
|
86
|
+
this.params.requestRender();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (matchesKey(data, Key.escape)) {
|
|
90
|
+
this.params.close();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (matchesKey(data, Key.up)) {
|
|
94
|
+
this.moveSelection(-1);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (matchesKey(data, Key.down)) {
|
|
98
|
+
this.moveSelection(1);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (matchesKey(data, Key.home)) {
|
|
102
|
+
this.selectIndex(0);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (matchesKey(data, Key.end)) {
|
|
106
|
+
this.selectIndex(this.entries().length - 1);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
110
|
+
this.scrollOutput(-Math.max(1, this.detailRows - 2));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
114
|
+
this.scrollOutput(Math.max(1, this.detailRows - 2));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (data === "i" && this.selectedId) {
|
|
118
|
+
// Interrupt (打断): abort the current turn/tool; the agent stays alive
|
|
119
|
+
// and is told to report and continue.
|
|
120
|
+
this.params.onCommand(this.selectedId, "interrupt");
|
|
121
|
+
this.ack(`interrupt @${this.agentName(this.selectedId)}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (data === "s" && this.selectedId) {
|
|
125
|
+
// Steer (引导): interrupt + inject the drafted message. The target is
|
|
126
|
+
// frozen at compose time so later roster changes cannot re-target it.
|
|
127
|
+
this.compose = { targetId: this.selectedId, draft: "" };
|
|
128
|
+
this.params.requestRender();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private ack(text: string, error = false): void {
|
|
133
|
+
this.lastAck = { text, at: Date.now(), ...(error ? { error: true } : {}) };
|
|
134
|
+
if (this.ackTimer) clearTimeout(this.ackTimer);
|
|
135
|
+
this.ackTimer = setTimeout(() => {
|
|
136
|
+
this.ackTimer = undefined;
|
|
137
|
+
this.lastAck = null;
|
|
138
|
+
this.params.requestRender();
|
|
139
|
+
}, COMMAND_ACK_MS);
|
|
140
|
+
this.ackTimer.unref?.();
|
|
141
|
+
this.params.requestRender();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private agentName(correlationId: string): string {
|
|
145
|
+
const row = this.entries().find((entry) => entry.row.correlationId === correlationId)?.row;
|
|
146
|
+
return row?.name || row?.role || row?.agent || correlationId.slice(0, 8);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
render(width: number): string[] {
|
|
150
|
+
const safeWidth = Math.max(1, Math.min(width, 150));
|
|
151
|
+
const entries = this.entries();
|
|
152
|
+
this.reconcileSelection(entries.map((entry) => entry.row));
|
|
153
|
+
const terminalHeight = this.params.getTerminalRows?.();
|
|
154
|
+
const heightIsCompact = terminalHeight !== undefined && Math.floor(terminalHeight * 0.9) < CARD_CHROME_ROWS + 1;
|
|
155
|
+
if (safeWidth < 20 || heightIsCompact) {
|
|
156
|
+
if (this.compose) {
|
|
157
|
+
const target = this.agentName(this.compose.targetId);
|
|
158
|
+
return [fit(`steer @${target}: ${this.compose.draft}_ · Enter · Esc`, safeWidth)];
|
|
159
|
+
}
|
|
160
|
+
const selected = entries.find((entry) => entry.row.correlationId === this.selectedId)?.row;
|
|
161
|
+
return [fit(selected ? `Agents ${entries.length} · ${agentLabel(selected)} · Esc` : "Agents · none · Esc", safeWidth)];
|
|
162
|
+
}
|
|
163
|
+
return safeWidth < 88
|
|
164
|
+
? this.renderNarrow(safeWidth, entries.map((entry) => entry.row))
|
|
165
|
+
: this.renderWide(safeWidth, entries.map((entry) => entry.row));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private renderWide(width: number, rows: AgentRow[]): string[] {
|
|
169
|
+
const inner = width - 2;
|
|
170
|
+
const leftWidth = Math.max(30, Math.floor((inner - 3) * 0.38));
|
|
171
|
+
const rightWidth = inner - leftWidth - 3;
|
|
172
|
+
const contentRows = overlayListRows(this.params.getTerminalRows?.(), CARD_CHROME_ROWS);
|
|
173
|
+
const list = this.agentLines(leftWidth, contentRows);
|
|
174
|
+
const detail = this.detailLines(rows, rightWidth, contentRows);
|
|
175
|
+
const bodyRows = Math.max(list.length, detail.length, 1);
|
|
176
|
+
const body: string[] = [this.header(inner), this.rule(inner)];
|
|
177
|
+
for (let index = 0; index < bodyRows; index++) {
|
|
178
|
+
body.push(`${pad(list[index] ?? "", leftWidth)} ${this.params.glyphs.box.vertical} ${pad(detail[index] ?? "", rightWidth)}`);
|
|
179
|
+
}
|
|
180
|
+
body.push(this.help(inner));
|
|
181
|
+
return this.card(body, width);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private renderNarrow(width: number, rows: AgentRow[]): string[] {
|
|
185
|
+
const inner = width - 2;
|
|
186
|
+
const contentRows = overlayListRows(this.params.getTerminalRows?.(), CARD_CHROME_ROWS);
|
|
187
|
+
const listRows = Math.max(1, Math.min(4, Math.floor(contentRows * 0.35)));
|
|
188
|
+
const list = this.agentLines(inner, listRows);
|
|
189
|
+
const detailRows = Math.max(2, contentRows - list.length - 1);
|
|
190
|
+
const detail = this.detailLines(rows, inner, detailRows);
|
|
191
|
+
return this.card([
|
|
192
|
+
this.header(inner),
|
|
193
|
+
this.rule(inner),
|
|
194
|
+
...list,
|
|
195
|
+
this.rule(inner),
|
|
196
|
+
...detail,
|
|
197
|
+
this.help(inner),
|
|
198
|
+
], width);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private entries() {
|
|
202
|
+
return buildAgentTree(visibleAgentRows([...this.params.getAgents()]), this.params.glyphs);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private agentLines(width: number, maxRows: number): string[] {
|
|
206
|
+
const entries = this.entries();
|
|
207
|
+
if (entries.length === 0) return [this.params.theme.fg("dim", fit("No agents", width))];
|
|
208
|
+
const selected = Math.max(0, entries.findIndex((entry) => entry.row.correlationId === this.selectedId));
|
|
209
|
+
const start = visibleStart(selected, entries.length, maxRows);
|
|
210
|
+
return entries.slice(start, start + maxRows).map(({ row, prefix }) => {
|
|
211
|
+
const status = effectiveAgentStatus(row);
|
|
212
|
+
const visual = agentVisual(status, this.params.glyphs);
|
|
213
|
+
const marker = row.correlationId === this.selectedId ? this.params.glyphs.selectMarker : " ";
|
|
214
|
+
const task = row.task ? ` · ${row.task}` : "";
|
|
215
|
+
return fit(
|
|
216
|
+
`${prefix}${marker} ${this.params.theme.fg(visual.color, visual.glyph)} ${this.params.theme.bold(agentLabel(row))}${task}`,
|
|
217
|
+
width,
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private detailLines(rows: AgentRow[], width: number, maxRows: number): string[] {
|
|
223
|
+
this.detailWidth = width;
|
|
224
|
+
this.detailRows = maxRows;
|
|
225
|
+
if (!this.selectedId) return [this.params.theme.fg("dim", fit("No agent selected", width))];
|
|
226
|
+
return renderSessionDetail(
|
|
227
|
+
rows,
|
|
228
|
+
this.selectedId,
|
|
229
|
+
width,
|
|
230
|
+
this.params.theme,
|
|
231
|
+
maxRows,
|
|
232
|
+
this.outputScroll,
|
|
233
|
+
"PgUp/PgDn scroll output",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private header(width: number): string {
|
|
238
|
+
if (this.compose) {
|
|
239
|
+
const target = this.agentName(this.compose.targetId);
|
|
240
|
+
return fit(`steer @${target}: ${this.compose.draft}_`, width);
|
|
241
|
+
}
|
|
242
|
+
const rows = this.entries().map((entry) => entry.row);
|
|
243
|
+
const statuses = rows.map((row) => effectiveAgentStatus(row));
|
|
244
|
+
const active = statuses.filter((status) => status === "running" || status === "retrying").length;
|
|
245
|
+
const failed = statuses.filter((status) => status === "failed" || status === "stalled").length;
|
|
246
|
+
const ack = this.lastAck && Date.now() - this.lastAck.at < COMMAND_ACK_MS ? this.lastAck : null;
|
|
247
|
+
return fit([
|
|
248
|
+
this.params.theme.bold("Agents"),
|
|
249
|
+
`${rows.length} total`,
|
|
250
|
+
active ? `${active} active` : "",
|
|
251
|
+
failed ? `${failed} failed` : "",
|
|
252
|
+
ack
|
|
253
|
+
? this.params.theme.fg(ack.error ? "error" : "success", ack.error ? `✗ ${ack.text}` : `${this.params.glyphs.check} ${ack.text}`)
|
|
254
|
+
: "",
|
|
255
|
+
].filter(Boolean).join(" · "), width);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private help(width: number): string {
|
|
259
|
+
if (this.compose) {
|
|
260
|
+
return this.params.theme.fg("dim", fit("Enter send · Esc cancel", width));
|
|
261
|
+
}
|
|
262
|
+
return this.params.theme.fg("dim", fit("Esc close · ↑↓ agent · Home/End jump · PgUp/PgDn output · i interrupt · s steer", width));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private rule(width: number): string {
|
|
266
|
+
return this.params.theme.fg("borderMuted", this.params.glyphs.box.horizontal.repeat(Math.max(1, width)));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private card(rows: string[], width: number): string[] {
|
|
270
|
+
const box = this.params.glyphs.box;
|
|
271
|
+
const edge = box.horizontal.repeat(Math.max(0, width - 2));
|
|
272
|
+
const background = (value: string) => this.params.theme.bg("customMessageBg", value);
|
|
273
|
+
return [
|
|
274
|
+
background(this.params.theme.fg("borderMuted", `${box.topLeft}${edge}${box.topRight}`)),
|
|
275
|
+
...rows.map((row) => background(pad(` ${row}`, width))),
|
|
276
|
+
background(this.params.theme.fg("borderMuted", `${box.bottomLeft}${edge}${box.bottomRight}`)),
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private moveSelection(delta: number): void {
|
|
281
|
+
const rows = this.entries().map((entry) => entry.row);
|
|
282
|
+
if (rows.length === 0) return;
|
|
283
|
+
const current = Math.max(0, rows.findIndex((row) => row.correlationId === this.selectedId));
|
|
284
|
+
this.selectRow(rows[(current + delta + rows.length) % rows.length]);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private selectIndex(index: number): void {
|
|
288
|
+
const rows = this.entries().map((entry) => entry.row);
|
|
289
|
+
if (rows.length === 0) return;
|
|
290
|
+
this.selectRow(rows[Math.max(0, Math.min(index, rows.length - 1))]);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private selectRow(row: AgentRow): void {
|
|
294
|
+
this.selectedId = row.correlationId;
|
|
295
|
+
this.outputScroll = { offset: 0, following: true };
|
|
296
|
+
this.params.onSelect(row.correlationId);
|
|
297
|
+
this.params.requestRender();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private reconcileSelection(rows: AgentRow[]): void {
|
|
301
|
+
if (rows.some((row) => row.correlationId === this.selectedId)) return;
|
|
302
|
+
this.selectedId = rows[0]?.correlationId;
|
|
303
|
+
this.outputScroll = { offset: 0, following: true };
|
|
304
|
+
if (this.selectedId) this.params.onSelect(this.selectedId);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private scrollOutput(delta: number): void {
|
|
308
|
+
if (!this.selectedId) return;
|
|
309
|
+
const rows = this.entries().map((entry) => entry.row);
|
|
310
|
+
const total = sessionDetailBodyLength(rows, this.selectedId, this.detailWidth);
|
|
311
|
+
const budget = Math.max(1, sessionDetailWindowRows(total, this.detailRows));
|
|
312
|
+
this.outputScroll = scrollBy(this.outputScroll, delta, total, budget);
|
|
313
|
+
this.params.requestRender();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function agentLabel(row: AgentRow): string {
|
|
318
|
+
return `@${row.name || row.role || row.agent || "agent"}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function agentVisual(status: AgentDisplayStatus, glyphs: IconGlyphs): { glyph: string; color: ThemeColor } {
|
|
322
|
+
if (status === "failed" || status === "stalled") return { glyph: glyphs.cross, color: "error" };
|
|
323
|
+
if (status === "terminated") return { glyph: glyphs.cross, color: "warning" };
|
|
324
|
+
if (status === "done" || status === "result-ready") return { glyph: glyphs.check, color: "success" };
|
|
325
|
+
if (status === "pending") return { glyph: glyphs.pending, color: "dim" };
|
|
326
|
+
if (status === "sleeping") return { glyph: glyphs.dotIdle, color: "warning" };
|
|
327
|
+
return { glyph: glyphs.dotRunning, color: "accent" };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function fit(value: string, width: number): string {
|
|
331
|
+
return truncateToWidth(value, Math.max(1, width), "…");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function pad(value: string, width: number): string {
|
|
335
|
+
const fitted = fit(value, width);
|
|
336
|
+
return `${fitted}${" ".repeat(Math.max(0, width - visibleWidth(fitted)))}`;
|
|
337
|
+
}
|
package/src/agent-scroll.ts
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* resumes it. Pure state math, no UI dependencies.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
AGENT_NARROW_ROW_CAP,
|
|
13
|
+
AGENT_WIDE_ROW_CAP,
|
|
14
|
+
agentPanelRows,
|
|
15
|
+
} from "./viewport.ts";
|
|
12
16
|
|
|
13
17
|
export interface AgentScrollState {
|
|
14
18
|
/** Window start over the activity-ordered roster. */
|
|
@@ -44,16 +48,17 @@ export function followWindow(total: number, budget: number): AgentScrollState {
|
|
|
44
48
|
|
|
45
49
|
/**
|
|
46
50
|
* How many agent rows the panel may render. Mirrors the roster widget's own
|
|
47
|
-
* budget:
|
|
48
|
-
*
|
|
51
|
+
* budget: eight wide / four narrow rows plus one marker row, bounded by the
|
|
52
|
+
* active-agent share unless the caller supplies a shared-surface budget.
|
|
49
53
|
*/
|
|
50
54
|
export function agentListWindowRows(
|
|
51
55
|
terminalColumns: number | undefined,
|
|
52
56
|
terminalRows: number | undefined,
|
|
53
57
|
total?: number,
|
|
58
|
+
panelOverride?: number,
|
|
54
59
|
): number {
|
|
55
|
-
const widthCap = (terminalColumns ?? 80) < 40 ?
|
|
56
|
-
const panel =
|
|
60
|
+
const widthCap = (terminalColumns ?? 80) < 40 ? AGENT_NARROW_ROW_CAP : AGENT_WIDE_ROW_CAP;
|
|
61
|
+
const panel = panelOverride ?? agentPanelRows(terminalRows);
|
|
57
62
|
const panelBudget = Math.min(widthCap + 1, panel ?? widthCap + 1);
|
|
58
63
|
const bodyBudget = Math.max(1, panelBudget - 1); // roster header owns one row
|
|
59
64
|
// One combined overflow marker is paid from the body budget, preserving at
|
package/src/agents-store.ts
CHANGED
|
@@ -154,7 +154,12 @@ function latestTool(tools: Array<string | { name?: string; status?: string }> |
|
|
|
154
154
|
const name = tool?.name?.trim();
|
|
155
155
|
if (!name) return undefined;
|
|
156
156
|
const status = tool.status?.trim();
|
|
157
|
-
|
|
157
|
+
// The active tool is by definition the one in flight, so "bash (running)"
|
|
158
|
+
// only repeats the row's own live state. Keep the suffix only for
|
|
159
|
+
// anomalies (failed/error) that the row state does not already convey.
|
|
160
|
+
return truncateStatusText(
|
|
161
|
+
status && status !== "completed" && status !== "running" ? `${name} (${status})` : name,
|
|
162
|
+
);
|
|
158
163
|
}
|
|
159
164
|
|
|
160
165
|
// Self-accumulating roster. The teammate extension only broadcasts deltas
|
package/src/ambient.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface AmbientState {
|
|
|
31
31
|
gitBranch?: string;
|
|
32
32
|
/** Maestro workflow status (run status when a run is active). */
|
|
33
33
|
maestro?: string;
|
|
34
|
+
/** Maestro knowledge evolution summary (consumed/pending/review). */
|
|
35
|
+
maestroKnowledge?: string;
|
|
34
36
|
/**
|
|
35
37
|
* Leading title glyph: spinner frames while a turn runs, a static marker
|
|
36
38
|
* when idle (Claude Code's `✳` / braille frames). Failure keeps its own
|
|
@@ -109,6 +111,7 @@ export function titleFor(
|
|
|
109
111
|
if (state.thinking) tags.push(`t:${state.thinking}`);
|
|
110
112
|
if (state.gitBranch) tags.push(`git:${state.gitBranch}`);
|
|
111
113
|
if (state.maestro) tags.push(`wf:${state.maestro}`);
|
|
114
|
+
if (state.maestroKnowledge) tags.push(`k:${state.maestroKnowledge}`);
|
|
112
115
|
if (tags.length > 0) title = `${title}${sep}${tags.join(sep)}`;
|
|
113
116
|
if (opts?.maxLength !== undefined && title.length > opts.maxLength) {
|
|
114
117
|
const cap = opts.maxLength;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single isolation point for "is the autocomplete popup active" knowledge.
|
|
3
|
+
*
|
|
4
|
+
* Pi 0.83 exposes `isShowingAutocomplete()` as a public method on the Editor
|
|
5
|
+
* class (not on the EditorComponent contract). CockpitClaudeEditor extends
|
|
6
|
+
* CustomEditor, so it can call it directly — no reflection is needed. Keeping
|
|
7
|
+
* the knowledge here means a future Pi that renames or removes the method
|
|
8
|
+
* fails at compile time in one place, and the double-Escape feature fails open
|
|
9
|
+
* (never clears) instead of mis-detecting.
|
|
10
|
+
*/
|
|
11
|
+
export interface AutocompleteProbeResult {
|
|
12
|
+
/** true: autocomplete is open; an Escape must cancel it, not arm double-Escape. */
|
|
13
|
+
active: boolean;
|
|
14
|
+
/** true: the probe could not read autocomplete state, so double-Escape must not clear. */
|
|
15
|
+
unknown: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function readAutocompleteState(editor: { isShowingAutocomplete?(): boolean }): AutocompleteProbeResult {
|
|
19
|
+
if (typeof editor.isShowingAutocomplete !== "function") return { active: false, unknown: true };
|
|
20
|
+
try {
|
|
21
|
+
return { active: editor.isShowingAutocomplete(), unknown: false };
|
|
22
|
+
} catch {
|
|
23
|
+
// A broken probe must never clear user input: fail open to native behaviour.
|
|
24
|
+
return { active: false, unknown: true };
|
|
25
|
+
}
|
|
26
|
+
}
|