pi-cockpit 0.24.0 → 0.24.1
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/node_modules/pi-maestro-settings-core/package.json +1 -1
- package/node_modules/pi-maestro-settings-core/src/pi-host.ts +22 -0
- package/node_modules/pi-maestro-settings-core/src/public/v1/index.ts +1 -0
- package/package.json +85 -85
- package/src/agent-bar.ts +72 -83
- package/src/ambient.ts +14 -15
- package/src/claude-editor.ts +28 -3
- package/src/edit-guard.ts +40 -6
- package/src/footer.ts +0 -4
- package/src/fullscreen-controller.ts +1 -1
- package/src/host-ui.ts +14 -0
- package/src/index.ts +280 -230
- package/src/quiet-tools.ts +143 -40
- package/src/render-memo.ts +17 -5
- package/src/render.ts +6 -2
- package/src/session-detail.ts +21 -6
- package/src/settings/cockpit-provider.ts +14 -5
- package/src/stack-widget.ts +95 -4
- package/src/thinking-timer.ts +1 -1
- package/src/tick-policy.ts +10 -10
- package/src/transcript-selection.ts +1 -1
- package/src/tui-i18n.ts +2 -0
- package/src/usage/core.ts +18 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Classify only the supplied host version; prereleases and unsupported hosts are unknown. */
|
|
2
|
+
export function getPiHostMode(version: unknown): "native" | "legacy" | "unknown" {
|
|
3
|
+
if (typeof version !== "string") return "unknown";
|
|
4
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version);
|
|
5
|
+
if (!match || match[0] !== version) return "unknown";
|
|
6
|
+
|
|
7
|
+
const [major, minor, patch] = match.slice(1, 4).map(Number);
|
|
8
|
+
if (![major, minor, patch].every(Number.isSafeInteger)) return "unknown";
|
|
9
|
+
if (major > 0 || minor >= 99) return "native";
|
|
10
|
+
if (minor >= 87) return "legacy";
|
|
11
|
+
return "unknown";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Missing native APIs never authorize legacy fallback on a native or unknown host. */
|
|
15
|
+
export function getPiFeatureOwner(
|
|
16
|
+
version: unknown,
|
|
17
|
+
apiAvailable: boolean,
|
|
18
|
+
): "native" | "legacy" | "unavailable" {
|
|
19
|
+
const mode = getPiHostMode(version);
|
|
20
|
+
if (mode === "legacy") return "legacy";
|
|
21
|
+
return mode === "native" && apiAvailable ? "native" : "unavailable";
|
|
22
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./events.ts";
|
|
2
2
|
export * from "./provider.ts";
|
|
3
3
|
export * from "./schema.ts";
|
|
4
|
+
export { getPiHostMode, getPiFeatureOwner } from "../../pi-host.ts";
|
|
4
5
|
|
|
5
6
|
// The keyboard-shortcut helpers live in the shared UI primitives module so
|
|
6
7
|
// Cockpit, Flow, and Teammate all import the same implementation. Keep the
|
package/package.json
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-cockpit",
|
|
3
|
-
"version": "0.24.
|
|
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/ui-ownership-runtime.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 tests/patch-health.test.ts",
|
|
22
|
-
"typecheck": "tsc --noEmit",
|
|
23
|
-
"prepack": "node ../../scripts/bundle-settings-core.mjs",
|
|
24
|
-
"postpack": "node ../../scripts/bundle-settings-core.mjs --clean"
|
|
25
|
-
},
|
|
26
|
-
"pi": {
|
|
27
|
-
"extensions": [
|
|
28
|
-
"./src/extension/index.ts"
|
|
29
|
-
],
|
|
30
|
-
"themes": [
|
|
31
|
-
"./themes"
|
|
32
|
-
]
|
|
33
|
-
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"pi-maestro-settings-core": "0.2.
|
|
36
|
-
"proper-lockfile": "4.1.2"
|
|
37
|
-
},
|
|
38
|
-
"bundledDependencies": [
|
|
39
|
-
"pi-maestro-settings-core"
|
|
40
|
-
],
|
|
41
|
-
"peerDependencies": {
|
|
42
|
-
"@earendil-works/pi-agent-core": "*",
|
|
43
|
-
"@earendil-works/pi-ai": "*",
|
|
44
|
-
"@earendil-works/pi-coding-agent": "*",
|
|
45
|
-
"@earendil-works/pi-tui": "*",
|
|
46
|
-
"pi-maestro-teammate": "^2.5.0",
|
|
47
|
-
"typebox": "*"
|
|
48
|
-
},
|
|
49
|
-
"peerDependenciesMeta": {
|
|
50
|
-
"@earendil-works/pi-agent-core": {
|
|
51
|
-
"optional": true
|
|
52
|
-
},
|
|
53
|
-
"@earendil-works/pi-ai": {
|
|
54
|
-
"optional": true
|
|
55
|
-
},
|
|
56
|
-
"@earendil-works/pi-coding-agent": {
|
|
57
|
-
"optional": true
|
|
58
|
-
},
|
|
59
|
-
"@earendil-works/pi-tui": {
|
|
60
|
-
"optional": true
|
|
61
|
-
},
|
|
62
|
-
"pi-maestro-teammate": {
|
|
63
|
-
"optional": true
|
|
64
|
-
},
|
|
65
|
-
"typebox": {
|
|
66
|
-
"optional": true
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
"devDependencies": {
|
|
70
|
-
"@earendil-works/pi-agent-core": "0.
|
|
71
|
-
"@earendil-works/pi-ai": "0.
|
|
72
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
73
|
-
"@earendil-works/pi-tui": "0.
|
|
74
|
-
"@types/node": "^22.0.0",
|
|
75
|
-
"pi-maestro-teammate": "2.7.
|
|
76
|
-
"typebox": "1.3.
|
|
77
|
-
"typescript": "^5.5.0"
|
|
78
|
-
},
|
|
79
|
-
"main": "./src/index.ts",
|
|
80
|
-
"exports": {
|
|
81
|
-
".": "./src/index.ts",
|
|
82
|
-
"./v1/events": "./src/public/v1/events.ts",
|
|
83
|
-
"./src/*": "./src/*"
|
|
84
|
-
}
|
|
85
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-cockpit",
|
|
3
|
+
"version": "0.24.1",
|
|
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/ui-ownership-runtime.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/native-host.test.ts tests/native-host-entry.test.ts tests/native-host-quiet.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 tests/patch-health.test.ts",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"prepack": "node ../../scripts/bundle-settings-core.mjs",
|
|
24
|
+
"postpack": "node ../../scripts/bundle-settings-core.mjs --clean"
|
|
25
|
+
},
|
|
26
|
+
"pi": {
|
|
27
|
+
"extensions": [
|
|
28
|
+
"./src/extension/index.ts"
|
|
29
|
+
],
|
|
30
|
+
"themes": [
|
|
31
|
+
"./themes"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"pi-maestro-settings-core": "0.2.3",
|
|
36
|
+
"proper-lockfile": "4.1.2"
|
|
37
|
+
},
|
|
38
|
+
"bundledDependencies": [
|
|
39
|
+
"pi-maestro-settings-core"
|
|
40
|
+
],
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@earendil-works/pi-agent-core": "*",
|
|
43
|
+
"@earendil-works/pi-ai": "*",
|
|
44
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
45
|
+
"@earendil-works/pi-tui": "*",
|
|
46
|
+
"pi-maestro-teammate": "^2.5.0",
|
|
47
|
+
"typebox": "*"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@earendil-works/pi-agent-core": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@earendil-works/pi-ai": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"@earendil-works/pi-coding-agent": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"@earendil-works/pi-tui": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"pi-maestro-teammate": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"typebox": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@earendil-works/pi-agent-core": "0.99.0",
|
|
71
|
+
"@earendil-works/pi-ai": "0.99.0",
|
|
72
|
+
"@earendil-works/pi-coding-agent": "0.99.0",
|
|
73
|
+
"@earendil-works/pi-tui": "0.99.0",
|
|
74
|
+
"@types/node": "^22.0.0",
|
|
75
|
+
"pi-maestro-teammate": "2.7.2",
|
|
76
|
+
"typebox": "1.3.27",
|
|
77
|
+
"typescript": "^5.5.0"
|
|
78
|
+
},
|
|
79
|
+
"main": "./src/index.ts",
|
|
80
|
+
"exports": {
|
|
81
|
+
".": "./src/index.ts",
|
|
82
|
+
"./v1/events": "./src/public/v1/events.ts",
|
|
83
|
+
"./src/*": "./src/*"
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/agent-bar.ts
CHANGED
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { truncateToWidth, visibleWidth, type TUI } from "@earendil-works/pi-tui";
|
|
10
10
|
import type { CockpitEndpoint } from "./endpoint-store.ts";
|
|
11
|
-
import { formatAgentMetric } from "./render.ts";
|
|
12
11
|
import type { SessionUiState } from "./session-ui-state.ts";
|
|
13
12
|
import { formatUnreadCount, type SessionTab } from "./session-tabs.ts";
|
|
14
13
|
import type { AgentRow } from "./types.ts";
|
|
15
14
|
import { effectiveAgentStatus, isCliAgent, type AgentDisplayStatus } from "./agents-store.ts";
|
|
16
15
|
import { visibleAgentRows } from "./stack-widget.ts";
|
|
17
|
-
import { tuiT } from "./tui-i18n.ts";
|
|
16
|
+
import { tuiStatus, tuiT } from "./tui-i18n.ts";
|
|
18
17
|
import { ownerDisplayToken } from "./window-autocomplete.ts";
|
|
19
18
|
|
|
20
19
|
export const SESSION_BAR_WIDGET_KEY = "cockpit-session-bar";
|
|
@@ -98,46 +97,23 @@ export function statusColor(status: AgentDisplayStatus): ThemeColor {
|
|
|
98
97
|
}
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
active: boolean,
|
|
104
|
-
color: ThemeColor,
|
|
105
|
-
theme: Theme,
|
|
106
|
-
unread = 0,
|
|
107
|
-
activity?: string,
|
|
108
|
-
attention = false,
|
|
109
|
-
outcome?: { status: "completed" | "failed" | "terminated"; message?: string },
|
|
110
|
-
cli = false,
|
|
111
|
-
): string {
|
|
100
|
+
/** Original chip shape retained exclusively for the legacy Session Bar facade. */
|
|
101
|
+
function legacyChip(label: string, active: boolean, color: ThemeColor, theme: Theme): string {
|
|
112
102
|
const text = `@${label}`;
|
|
113
|
-
const cliBadge = cli ? theme.fg("accent", ` ${tuiT("widget.agent.cli")}`) : "";
|
|
114
|
-
const badge = unread > 0 ? theme.fg("warning", ` •${formatUnreadCount(unread)}`) : "";
|
|
115
|
-
const activitySuffix = activity ? theme.fg("dim", ` · ${activity}`) : "";
|
|
116
|
-
const attentionPrefix = attention ? theme.fg("error", "!") : "";
|
|
117
|
-
const outcomeSuffix = outcome
|
|
118
|
-
? outcome.status === "failed"
|
|
119
|
-
? theme.fg("error", ` ✗${outcome.message ? ` ${outcome.message}` : ""}`)
|
|
120
|
-
: outcome.status === "terminated"
|
|
121
|
-
? theme.fg("warning", " ✗")
|
|
122
|
-
: theme.fg("success", " ✓")
|
|
123
|
-
: "";
|
|
124
103
|
return active
|
|
125
|
-
? `${
|
|
126
|
-
:
|
|
104
|
+
? `${theme.fg(color, "▸")} ${theme.fg(color, theme.bold(text))}`
|
|
105
|
+
: theme.fg(color, text);
|
|
127
106
|
}
|
|
128
107
|
|
|
129
108
|
export type AgentBarStatus = AgentDisplayStatus | "idle";
|
|
130
109
|
|
|
131
110
|
interface AgentBarTab extends SessionTab {
|
|
132
|
-
endpoint: CockpitEndpoint;
|
|
133
111
|
status: AgentBarStatus;
|
|
134
112
|
color: ThemeColor;
|
|
135
|
-
/**
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
|
|
139
|
-
/** Terminal outcome badge: ✓ completed, ✗ failed (with reason) or terminated. */
|
|
140
|
-
outcome?: { status: "completed" | "failed" | "terminated"; message?: string };
|
|
113
|
+
/** The rail has one suffix slot: live tool name when present, otherwise localized state. */
|
|
114
|
+
suffix: string;
|
|
115
|
+
/** Last terminal outcome selects the status glyph, but never exposes its error message. */
|
|
116
|
+
outcome?: "completed" | "failed" | "terminated";
|
|
141
117
|
/** External CLI backend (ACP `cli/<tool>` route): shown as an accent badge after the label. */
|
|
142
118
|
cli?: boolean;
|
|
143
119
|
}
|
|
@@ -151,8 +127,7 @@ function endpointStatus(endpoint: CockpitEndpoint, now: number, mainRunning: boo
|
|
|
151
127
|
return "running";
|
|
152
128
|
}
|
|
153
129
|
|
|
154
|
-
function endpointColor(endpoint: CockpitEndpoint, status: AgentBarStatus, now: number
|
|
155
|
-
if (selected) return "accent";
|
|
130
|
+
function endpointColor(endpoint: CockpitEndpoint, status: AgentBarStatus, now: number): ThemeColor {
|
|
156
131
|
if (endpoint.kind === "root") return status === "running" ? "warning" : "muted";
|
|
157
132
|
if (endpoint.agentRow) return agentSessionColor(endpoint.agentRow, now, status === "idle" ? undefined : status);
|
|
158
133
|
if (status === "done" || status === "sleeping" || status === "terminated") return "muted";
|
|
@@ -160,6 +135,35 @@ function endpointColor(endpoint: CockpitEndpoint, status: AgentBarStatus, now: n
|
|
|
160
135
|
return assignedAgentColor(endpoint.correlationId ?? endpoint.id);
|
|
161
136
|
}
|
|
162
137
|
|
|
138
|
+
function statusVisual(
|
|
139
|
+
status: AgentBarStatus,
|
|
140
|
+
outcome?: AgentBarTab["outcome"],
|
|
141
|
+
): { glyph: string; color: ThemeColor } {
|
|
142
|
+
if (outcome === "completed") return { glyph: "✓", color: "success" };
|
|
143
|
+
if (outcome === "failed") return { glyph: "✗", color: "error" };
|
|
144
|
+
if (outcome === "terminated") return { glyph: "✗", color: "warning" };
|
|
145
|
+
if (status === "failed") return { glyph: "✗", color: "error" };
|
|
146
|
+
if (status === "stalled") return { glyph: "!", color: "error" };
|
|
147
|
+
if (status === "terminated") return { glyph: "✗", color: "warning" };
|
|
148
|
+
if (status === "done" || status === "result-ready") return { glyph: "✓", color: "success" };
|
|
149
|
+
if (status === "retrying") return { glyph: "↻", color: "warning" };
|
|
150
|
+
if (status === "running") return { glyph: "●", color: "warning" };
|
|
151
|
+
if (status === "pending") return { glyph: "○", color: "dim" };
|
|
152
|
+
return { glyph: "○", color: "muted" };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function railChip(tab: AgentBarTab, active: boolean, theme: Theme): string {
|
|
156
|
+
const visual = statusVisual(tab.status, tab.outcome);
|
|
157
|
+
const selected = active ? `${theme.fg("accent", "▸")} ` : "";
|
|
158
|
+
const status = theme.fg(visual.color, visual.glyph);
|
|
159
|
+
const identity = theme.fg(tab.color, active ? theme.bold(`@${tab.label}`) : `@${tab.label}`);
|
|
160
|
+
const cli = tab.cli ? theme.fg("accent", ` ${tuiT("widget.agent.cli")}`) : "";
|
|
161
|
+
const unreadCount = tab.unread ?? 0;
|
|
162
|
+
const unread = unreadCount > 0 ? theme.fg("warning", ` •${formatUnreadCount(unreadCount)}`) : "";
|
|
163
|
+
const suffix = tab.suffix ? theme.fg("dim", ` · ${tab.suffix}`) : "";
|
|
164
|
+
return `${selected}${status} ${identity}${cli}${unread}${suffix}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
163
167
|
export interface AgentBarRenderOptions {
|
|
164
168
|
mainRunning?: boolean;
|
|
165
169
|
now?: number;
|
|
@@ -177,21 +181,10 @@ const CHIP_SEPARATOR = " ";
|
|
|
177
181
|
* and its context visible no matter how many agents exist.
|
|
178
182
|
*/
|
|
179
183
|
function renderAgentChipLine(tabs: AgentBarTab[], selectedId: string, width: number, theme: Theme): string {
|
|
180
|
-
|
|
184
|
+
if (width <= 0 || tabs.length === 0) return "";
|
|
185
|
+
const w = width;
|
|
181
186
|
const selectedIndex = Math.max(0, tabs.findIndex((tab) => tab.id === selectedId));
|
|
182
|
-
const chips = tabs.map((tab) =>
|
|
183
|
-
tab.label,
|
|
184
|
-
tab.id === selectedId,
|
|
185
|
-
tab.color,
|
|
186
|
-
theme,
|
|
187
|
-
tab.unread,
|
|
188
|
-
tab.activity,
|
|
189
|
-
tab.attention,
|
|
190
|
-
tab.outcome
|
|
191
|
-
? { status: tab.outcome.status, message: tab.outcome.message ? truncateToWidth(tab.outcome.message, 24, "…") : undefined }
|
|
192
|
-
: undefined,
|
|
193
|
-
tab.cli,
|
|
194
|
-
));
|
|
187
|
+
const chips = tabs.map((tab) => railChip(tab, tab.id === selectedId, theme));
|
|
195
188
|
const chipWidths = chips.map((text) => visibleWidth(text));
|
|
196
189
|
const sepWidth = visibleWidth(CHIP_SEPARATOR);
|
|
197
190
|
const totalWidth = chipWidths.reduce((sum, chipWidth) => sum + chipWidth + sepWidth, 0) - sepWidth;
|
|
@@ -244,7 +237,8 @@ export function renderSessionBarLine(
|
|
|
244
237
|
): string {
|
|
245
238
|
// A live main-screen row must not write the terminal's final column: doing so
|
|
246
239
|
// arms auto-wrap, while pi-tui still tracks the cursor on the original row.
|
|
247
|
-
const w = Math.max(
|
|
240
|
+
const w = Math.max(0, width - 1);
|
|
241
|
+
if (w === 0) return "";
|
|
248
242
|
if (!shortcutHint) return renderContent(w);
|
|
249
243
|
const hintSpec = typeof shortcutHint === "string" ? { text: shortcutHint, color: "dim" as const } : shortcutHint;
|
|
250
244
|
const hint = theme.fg(hintSpec.color ?? "dim", hintSpec.text);
|
|
@@ -265,7 +259,24 @@ export function renderAgentBar(
|
|
|
265
259
|
theme: Theme,
|
|
266
260
|
options: AgentBarRenderOptions = {},
|
|
267
261
|
): string[] {
|
|
268
|
-
if (endpoints.length === 0)
|
|
262
|
+
if (endpoints.length === 0) {
|
|
263
|
+
const status: AgentBarStatus = options.mainRunning ? "running" : "idle";
|
|
264
|
+
const fallback: AgentBarTab = {
|
|
265
|
+
id: "root",
|
|
266
|
+
label: MAIN_SESSION_LABEL,
|
|
267
|
+
ordinal: 0,
|
|
268
|
+
unread: 0,
|
|
269
|
+
status,
|
|
270
|
+
color: status === "running" ? "warning" : "muted",
|
|
271
|
+
suffix: tuiStatus(status),
|
|
272
|
+
};
|
|
273
|
+
return [renderSessionBarLine(
|
|
274
|
+
(availableWidth) => truncateToWidth(railChip(fallback, true, theme), availableWidth, "…"),
|
|
275
|
+
width,
|
|
276
|
+
theme,
|
|
277
|
+
options.shortcutHint,
|
|
278
|
+
)];
|
|
279
|
+
}
|
|
269
280
|
const now = options.now ?? 0;
|
|
270
281
|
// A stale selection (id no longer in the endpoint list) must not leave the
|
|
271
282
|
// bar without a highlighted chip: fall back to root / first endpoint.
|
|
@@ -275,11 +286,11 @@ export function renderAgentBar(
|
|
|
275
286
|
? requestedId
|
|
276
287
|
: fallbackId;
|
|
277
288
|
const tabs: AgentBarTab[] = endpoints.map((endpoint) => {
|
|
278
|
-
const selected = endpoint.id === selectedId;
|
|
279
289
|
const status = endpointStatus(endpoint, now, options.mainRunning === true);
|
|
280
|
-
const
|
|
290
|
+
const liveAgent = endpoint.kind === "agent" && (status === "running" || status === "retrying");
|
|
291
|
+
const activeTool = liveAgent ? endpoint.agentRow?.activeTool ?? endpoint.externalAgent?.activeTool : undefined;
|
|
281
292
|
const outcome = status === "done" || status === "failed" || status === "terminated" || status === "sleeping"
|
|
282
|
-
? endpoint.agentRow?.lastOutcome
|
|
293
|
+
? endpoint.agentRow?.lastOutcome?.status
|
|
283
294
|
: undefined;
|
|
284
295
|
const ownerId = endpoint.kind === "root" ? endpoint.registryEndpoint?.ownerId : undefined;
|
|
285
296
|
return {
|
|
@@ -287,39 +298,17 @@ export function renderAgentBar(
|
|
|
287
298
|
label: ownerId ? ownerDisplayToken(endpoint.label, ownerId) : endpoint.label,
|
|
288
299
|
ordinal: endpoint.ordinal,
|
|
289
300
|
unread: state.endpoint(endpoint.id).unread,
|
|
290
|
-
endpoint,
|
|
291
301
|
status,
|
|
292
|
-
color: endpointColor(endpoint, status, now
|
|
293
|
-
|
|
294
|
-
?
|
|
295
|
-
|
|
296
|
-
? endpoint.agentRow.activeToolArgs
|
|
297
|
-
? `${endpoint.agentRow.activeTool} ${endpoint.agentRow.activeToolArgs}`
|
|
298
|
-
: endpoint.agentRow.activeTool
|
|
299
|
-
: endpoint.externalAgent!.activeToolArgs
|
|
300
|
-
? `${endpoint.externalAgent!.activeTool} ${endpoint.externalAgent!.activeToolArgs}`
|
|
301
|
-
: endpoint.externalAgent!.activeTool,
|
|
302
|
-
}
|
|
303
|
-
: {}),
|
|
304
|
-
...(status === "stalled" ? { attention: true } : {}),
|
|
302
|
+
color: endpointColor(endpoint, status, now),
|
|
303
|
+
suffix: liveAgent
|
|
304
|
+
? activeTool ? truncateToWidth(activeTool, 18, "…") : ""
|
|
305
|
+
: tuiStatus(status),
|
|
305
306
|
...(outcome ? { outcome } : {}),
|
|
306
307
|
...(endpoint.agentRow && isCliAgent(endpoint.agentRow) ? { cli: true } : {}),
|
|
307
308
|
};
|
|
308
309
|
});
|
|
309
|
-
// Selected-session metrics summary, appended only when the chip line leaves
|
|
310
|
-
// room for it: telemetry must never squeeze the chips that carry identity.
|
|
311
|
-
const selectedEndpoint = tabs.find((tab) => tab.id === selectedId)?.endpoint;
|
|
312
|
-
const selectedMetrics = selectedEndpoint?.agentRow ?? selectedEndpoint?.externalAgent?.metrics;
|
|
313
|
-
const metricParts: string[] = [];
|
|
314
|
-
if (selectedMetrics?.toolCount !== undefined) metricParts.push(tuiT("common.tools", { count: selectedMetrics.toolCount }));
|
|
315
|
-
if (selectedMetrics?.tokens !== undefined) metricParts.push(tuiT("widget.agent.tokens", { count: formatAgentMetric(selectedMetrics.tokens) }));
|
|
316
|
-
const metrics = metricParts.length > 0 ? theme.fg("muted", ` · ${metricParts.join(" · ")}`) : "";
|
|
317
310
|
return [renderSessionBarLine(
|
|
318
|
-
(availableWidth) =>
|
|
319
|
-
const chipsLine = renderAgentChipLine(tabs, selectedId, availableWidth, theme);
|
|
320
|
-
if (!metrics || visibleWidth(chipsLine) + visibleWidth(metrics) > availableWidth) return chipsLine;
|
|
321
|
-
return chipsLine + metrics;
|
|
322
|
-
},
|
|
311
|
+
(availableWidth) => renderAgentChipLine(tabs, selectedId, availableWidth, theme),
|
|
323
312
|
width,
|
|
324
313
|
theme,
|
|
325
314
|
options.shortcutHint,
|
|
@@ -364,7 +353,7 @@ export function renderSessionBar(
|
|
|
364
353
|
}
|
|
365
354
|
}
|
|
366
355
|
const chips = [
|
|
367
|
-
|
|
356
|
+
legacyChip(
|
|
368
357
|
MAIN_SESSION_LABEL,
|
|
369
358
|
viewingId === undefined,
|
|
370
359
|
viewingId === undefined ? "accent" : opts.mainRunning ? "warning" : "muted",
|
|
@@ -377,7 +366,7 @@ export function renderSessionBar(
|
|
|
377
366
|
const stableStatus = previousStatus === nextStatus ? previousStatus : nextStatus;
|
|
378
367
|
opts.stableStatuses?.set(row.correlationId, stableStatus);
|
|
379
368
|
const color = agentSessionColor(row, now, stableStatus);
|
|
380
|
-
return
|
|
369
|
+
return legacyChip(label, row.correlationId === viewingId, color, theme);
|
|
381
370
|
}),
|
|
382
371
|
];
|
|
383
372
|
const w = Math.max(1, width);
|
package/src/ambient.ts
CHANGED
|
@@ -23,6 +23,10 @@ export interface AmbientState {
|
|
|
23
23
|
hideLiveDuration?: boolean;
|
|
24
24
|
/** Separator glyph between label and elapsed time (e.g. " · "). */
|
|
25
25
|
separator?: string;
|
|
26
|
+
/** Parenthesized suffix appended after the elapsed (e.g. an interrupt hint). */
|
|
27
|
+
hint?: string;
|
|
28
|
+
/** Live output-token rate label (e.g. "45 tok/s"), shown while streaming. */
|
|
29
|
+
tokenRate?: string;
|
|
26
30
|
/** Session summary — the session_info name, else a short session id. */
|
|
27
31
|
session?: string;
|
|
28
32
|
/** Active model short id. */
|
|
@@ -59,20 +63,11 @@ export function nextUiPromptDepth(depth: number, event: "start" | "end"): number
|
|
|
59
63
|
return event === "start" ? depth + 1 : Math.max(0, depth - 1);
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
export function shouldHideWorkingDuration(
|
|
63
|
-
mode: "regular" | "fullscreen" | undefined,
|
|
64
|
-
staticMode: boolean,
|
|
65
|
-
): boolean {
|
|
66
|
-
// Pi's regular renderer shares scrollback between the transcript and status
|
|
67
|
-
// row, so a moving live timer can be preserved as transcript height changes.
|
|
68
|
-
return staticMode || mode !== "fullscreen";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
66
|
/**
|
|
72
67
|
* The streaming working line.
|
|
73
68
|
*
|
|
74
|
-
* Cockpit hides the host indicator and renders the active state
|
|
75
|
-
*
|
|
69
|
+
* Cockpit hides the host indicator and renders the active state with a live
|
|
70
|
+
* elapsed value. Static mode keeps the row stable by omitting the timer.
|
|
76
71
|
*/
|
|
77
72
|
export function workingMessage(state: AmbientState, now = Date.now()): string | undefined {
|
|
78
73
|
const label = state.waitingForInput
|
|
@@ -80,10 +75,14 @@ export function workingMessage(state: AmbientState, now = Date.now()): string |
|
|
|
80
75
|
: state.activeTool ?? (state.running ? tuiT("ambient.working") : undefined);
|
|
81
76
|
if (!label) return undefined;
|
|
82
77
|
const sep = state.separator ?? " ";
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
const parts = [label];
|
|
79
|
+
if (!state.waitingForInput && !state.hideLiveDuration && state.workingStartedAt !== undefined) {
|
|
80
|
+
parts.push(formatThinkingDuration(now - state.workingStartedAt));
|
|
81
|
+
if (state.tokenRate) parts.push(state.tokenRate);
|
|
82
|
+
}
|
|
83
|
+
let text = parts.join(sep);
|
|
84
|
+
if (state.hint) text += ` (${state.hint})`;
|
|
85
|
+
return text;
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
/** Composition options for titleFor. */
|
package/src/claude-editor.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CustomEditor, type KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { isKeyRelease, isKeyRepeat, matchesKey, getKeybindings, truncateToWidth, visibleWidth, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
|
|
3
3
|
import { readAutocompleteState } from "./autocomplete-probe.ts";
|
|
4
|
+
import { stripAnsi } from "./transcript-selection.ts";
|
|
4
5
|
import { tuiT } from "./tui-i18n.ts";
|
|
5
6
|
|
|
6
7
|
/** Invisible editor start marker consumed by the fullscreen controller (fullscreenInput only). */
|
|
@@ -22,8 +23,8 @@ export interface CockpitEditorRouteTarget {
|
|
|
22
23
|
export interface CockpitClaudeEditorOptions {
|
|
23
24
|
/** Enable the double bare-Escape clear-input state machine. */
|
|
24
25
|
doubleEscapeClearInput: boolean;
|
|
25
|
-
/** Emit editor start/end markers
|
|
26
|
-
emitEditorMarkers: boolean;
|
|
26
|
+
/** Emit editor start/end markers while the fullscreen controller is active. */
|
|
27
|
+
emitEditorMarkers: boolean | (() => boolean);
|
|
27
28
|
/** Guard: true while pi is streaming or a capturing overlay is open (never arm). */
|
|
28
29
|
isBusy?: () => boolean;
|
|
29
30
|
/** Called after a double-Escape clears a non-empty draft. */
|
|
@@ -40,6 +41,12 @@ export interface CockpitClaudeEditorOptions {
|
|
|
40
41
|
getRouteTarget?: () => CockpitEditorRouteTarget | undefined;
|
|
41
42
|
/** Called with the constructed editor so the host can push route updates. */
|
|
42
43
|
onEditor?: (editor: CockpitClaudeEditor) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Repaints the embedded working-status row with a fixed color. The host
|
|
46
|
+
* derives that row's color from the thinking-level border tint; Cockpit
|
|
47
|
+
* keeps it neutral instead.
|
|
48
|
+
*/
|
|
49
|
+
workingStatusPaint?: (text: string) => string;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
export interface DoubleEscapeDecision {
|
|
@@ -147,7 +154,10 @@ export class CockpitClaudeEditor extends CustomEditor {
|
|
|
147
154
|
const banner = this.browsing() && total > 0
|
|
148
155
|
? historyBanner(this.index + 1, total, width, this.getPaddingX(), this.borderColor)
|
|
149
156
|
: undefined;
|
|
150
|
-
|
|
157
|
+
const emitEditorMarkers = typeof this.editorOptions.emitEditorMarkers === "function"
|
|
158
|
+
? this.editorOptions.emitEditorMarkers()
|
|
159
|
+
: this.editorOptions.emitEditorMarkers;
|
|
160
|
+
if (!emitEditorMarkers) {
|
|
151
161
|
return banner ? [...lines, banner] : lines;
|
|
152
162
|
}
|
|
153
163
|
// The fullscreen controller treats everything between the sentinels as the
|
|
@@ -204,6 +214,21 @@ export class CockpitClaudeEditor extends CustomEditor {
|
|
|
204
214
|
this.tui.requestRender();
|
|
205
215
|
}
|
|
206
216
|
|
|
217
|
+
/** Status row embedded in the top border keeps a constant color, never the thinking tint. */
|
|
218
|
+
override setWorkingStatusIndicator(indicator: {
|
|
219
|
+
renderInBorder(width: number): string;
|
|
220
|
+
renderSpinnerInBorder(width: number): string;
|
|
221
|
+
} | undefined): void {
|
|
222
|
+
const paint = this.editorOptions.workingStatusPaint;
|
|
223
|
+
if (indicator && paint) {
|
|
224
|
+
const render = indicator.renderInBorder.bind(indicator);
|
|
225
|
+
const renderSpinner = indicator.renderSpinnerInBorder.bind(indicator);
|
|
226
|
+
indicator.renderInBorder = (width: number) => paint(stripAnsi(render(width)));
|
|
227
|
+
indicator.renderSpinnerInBorder = (width: number) => paint(stripAnsi(renderSpinner(width)));
|
|
228
|
+
}
|
|
229
|
+
super.setWorkingStatusIndicator(indicator as Parameters<CustomEditor["setWorkingStatusIndicator"]>[0]);
|
|
230
|
+
}
|
|
231
|
+
|
|
207
232
|
private isBareEscape(data: string): boolean {
|
|
208
233
|
return matchesKey(data, "escape") && !isKeyRelease(data) && !isKeyRepeat(data);
|
|
209
234
|
}
|