pi-cockpit 0.19.0 → 0.21.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/package.json +85 -85
- package/src/agent-bar.ts +15 -5
- package/src/ambient.ts +22 -4
- package/src/config.ts +2 -0
- package/src/endpoint-store.ts +1 -1
- package/src/index.ts +145 -23
- package/src/input-routing.ts +4 -0
- package/src/maestro-store.ts +18 -0
- package/src/public/v1/events.ts +12 -0
- package/src/render.ts +11 -0
- package/src/settings/cockpit-provider.ts +8 -2
- package/src/stack-widget.ts +1 -1
- package/src/tui-i18n.ts +18 -0
- package/src/types.ts +3 -0
- package/src/viewport-stability.ts +20 -18
- package/src/window-autocomplete.ts +192 -0
- package/src/window-bar.ts +6 -4
package/package.json
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-cockpit",
|
|
3
|
-
"version": "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/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
|
-
"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.1",
|
|
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.
|
|
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.
|
|
76
|
-
"typebox": "1.3.7",
|
|
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.21.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/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
|
+
"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.1",
|
|
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.4.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.84.4",
|
|
71
|
+
"@earendil-works/pi-ai": "0.84.4",
|
|
72
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
73
|
+
"@earendil-works/pi-tui": "0.84.4",
|
|
74
|
+
"@types/node": "^22.0.0",
|
|
75
|
+
"pi-maestro-teammate": "2.4.0",
|
|
76
|
+
"typebox": "1.3.7",
|
|
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
|
@@ -15,6 +15,7 @@ import type { AgentRow } from "./types.ts";
|
|
|
15
15
|
import { effectiveAgentStatus, isCliAgent, type AgentDisplayStatus } from "./agents-store.ts";
|
|
16
16
|
import { visibleAgentRows } from "./stack-widget.ts";
|
|
17
17
|
import { tuiT } from "./tui-i18n.ts";
|
|
18
|
+
import { ownerDisplayToken } from "./window-autocomplete.ts";
|
|
18
19
|
|
|
19
20
|
export const SESSION_BAR_WIDGET_KEY = "cockpit-session-bar";
|
|
20
21
|
export const AGENT_BAR_WIDGET_KEY = SESSION_BAR_WIDGET_KEY;
|
|
@@ -37,12 +38,19 @@ export interface SessionBarDeps {
|
|
|
37
38
|
getCurrentSession?: () => { label: string; color: ThemeColor } | undefined;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
export interface SessionBarHint {
|
|
42
|
+
text: string;
|
|
43
|
+
color?: ThemeColor;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type SessionBarHintValue = string | SessionBarHint;
|
|
47
|
+
|
|
40
48
|
export interface AgentBarDeps {
|
|
41
49
|
getEndpoints: () => readonly CockpitEndpoint[];
|
|
42
50
|
getState: () => SessionUiState;
|
|
43
51
|
getNow: () => number;
|
|
44
52
|
isMainRunning?: () => boolean;
|
|
45
|
-
getShortcutHint?: () =>
|
|
53
|
+
getShortcutHint?: () => SessionBarHintValue | undefined;
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
export const AGENT_SESSION_COLORS = [
|
|
@@ -154,7 +162,7 @@ function endpointColor(endpoint: CockpitEndpoint, status: AgentBarStatus, now: n
|
|
|
154
162
|
export interface AgentBarRenderOptions {
|
|
155
163
|
mainRunning?: boolean;
|
|
156
164
|
now?: number;
|
|
157
|
-
shortcutHint?:
|
|
165
|
+
shortcutHint?: SessionBarHintValue;
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
/** Chip-to-chip separator in the agent bar. */
|
|
@@ -231,11 +239,12 @@ export function renderSessionBarLine(
|
|
|
231
239
|
renderContent: (width: number) => string,
|
|
232
240
|
width: number,
|
|
233
241
|
theme: Theme,
|
|
234
|
-
shortcutHint?:
|
|
242
|
+
shortcutHint?: SessionBarHintValue,
|
|
235
243
|
): string {
|
|
236
244
|
const w = Math.max(1, width);
|
|
237
245
|
if (!shortcutHint) return renderContent(w);
|
|
238
|
-
const
|
|
246
|
+
const hintSpec = typeof shortcutHint === "string" ? { text: shortcutHint, color: "dim" as const } : shortcutHint;
|
|
247
|
+
const hint = theme.fg(hintSpec.color ?? "dim", hintSpec.text);
|
|
239
248
|
const hintWidth = visibleWidth(hint);
|
|
240
249
|
const gap = 2;
|
|
241
250
|
if (w < hintWidth + gap + 8) return renderContent(w);
|
|
@@ -269,9 +278,10 @@ export function renderAgentBar(
|
|
|
269
278
|
const outcome = status === "done" || status === "failed" || status === "terminated" || status === "sleeping"
|
|
270
279
|
? endpoint.agentRow?.lastOutcome
|
|
271
280
|
: undefined;
|
|
281
|
+
const ownerId = endpoint.kind === "root" ? endpoint.registryEndpoint?.ownerId : undefined;
|
|
272
282
|
return {
|
|
273
283
|
id: endpoint.id,
|
|
274
|
-
label: endpoint.label,
|
|
284
|
+
label: ownerId ? ownerDisplayToken(endpoint.label, ownerId) : endpoint.label,
|
|
275
285
|
ordinal: endpoint.ordinal,
|
|
276
286
|
unread: state.endpoint(endpoint.id).unread,
|
|
277
287
|
endpoint,
|
package/src/ambient.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface AmbientState {
|
|
|
16
16
|
agents: readonly AgentRow[];
|
|
17
17
|
jobs: readonly BashBgJob[];
|
|
18
18
|
running: boolean;
|
|
19
|
+
waitingForInput?: boolean;
|
|
19
20
|
cwd?: string;
|
|
20
21
|
activeTool?: string;
|
|
21
22
|
workingStartedAt?: number;
|
|
@@ -54,17 +55,32 @@ function failedJobs(jobs: readonly BashBgJob[]): BashBgJob[] {
|
|
|
54
55
|
return jobs.filter((job) => job.status === "failed" || (job.exitCode !== null && job.exitCode !== 0));
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
export function nextUiPromptDepth(depth: number, event: "start" | "end"): number {
|
|
59
|
+
return event === "start" ? depth + 1 : Math.max(0, depth - 1);
|
|
60
|
+
}
|
|
61
|
+
|
|
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
|
+
|
|
57
71
|
/**
|
|
58
72
|
* The streaming working line.
|
|
59
73
|
*
|
|
60
|
-
* Cockpit hides the host indicator and renders the active state
|
|
61
|
-
* elapsed value
|
|
74
|
+
* Cockpit hides the host indicator and renders the active state. Fullscreen may
|
|
75
|
+
* add a live elapsed value; the regular main screen keeps this row stable.
|
|
62
76
|
*/
|
|
63
77
|
export function workingMessage(state: AmbientState, now = Date.now()): string | undefined {
|
|
64
|
-
const label = state.
|
|
78
|
+
const label = state.waitingForInput
|
|
79
|
+
? tuiT("ambient.waitingForInput")
|
|
80
|
+
: state.activeTool ?? (state.running ? tuiT("ambient.working") : undefined);
|
|
65
81
|
if (!label) return undefined;
|
|
66
82
|
const sep = state.separator ?? " ";
|
|
67
|
-
const text = (state.hideLiveDuration || state.workingStartedAt === undefined)
|
|
83
|
+
const text = (state.waitingForInput || state.hideLiveDuration || state.workingStartedAt === undefined)
|
|
68
84
|
? label
|
|
69
85
|
: `${label}${sep}${formatThinkingDuration(now - state.workingStartedAt)}`;
|
|
70
86
|
return `\x1b[3m${text}\x1b[23m`;
|
|
@@ -100,6 +116,8 @@ export function titleFor(
|
|
|
100
116
|
let title: string;
|
|
101
117
|
if (broken > 0) {
|
|
102
118
|
title = `${marks.fail} ${base}${sep}${tuiT("ambient.failed", { count: broken })}`;
|
|
119
|
+
} else if (state.waitingForInput) {
|
|
120
|
+
title = `${prefix}${base}${sep}${tuiT("ambient.waitingForInput")}`;
|
|
103
121
|
} else if (state.running) {
|
|
104
122
|
const live = liveAgents(state.agents).length;
|
|
105
123
|
title = live > 0
|
package/src/config.ts
CHANGED
|
@@ -76,6 +76,7 @@ const isCurrencyMode = (v: unknown): v is CurrencyMode => v === "usd" || v === "
|
|
|
76
76
|
agentsMode: isMode(o.agentsMode) ? o.agentsMode : base.agentsMode,
|
|
77
77
|
todoMode: isMode(o.todoMode) ? o.todoMode : base.todoMode,
|
|
78
78
|
todoExpanded: typeof o.todoExpanded === "boolean" ? o.todoExpanded : base.todoExpanded,
|
|
79
|
+
todoDurationChart: typeof o.todoDurationChart === "boolean" ? o.todoDurationChart : base.todoDurationChart,
|
|
79
80
|
stackStyle: isStackStyle(o.stackStyle) ? o.stackStyle : base.stackStyle,
|
|
80
81
|
hideNativeAgents: typeof o.hideNativeAgents === "boolean" ? o.hideNativeAgents : base.hideNativeAgents,
|
|
81
82
|
pinEditorBottom: typeof o.pinEditorBottom === "boolean" ? o.pinEditorBottom : base.pinEditorBottom,
|
|
@@ -168,6 +169,7 @@ export function mergeConfigDocument(raw: unknown, config: CockpitConfig): Record
|
|
|
168
169
|
agentsMode: config.agentsMode,
|
|
169
170
|
todoMode: config.todoMode,
|
|
170
171
|
todoExpanded: config.todoExpanded,
|
|
172
|
+
todoDurationChart: config.todoDurationChart,
|
|
171
173
|
stackStyle: config.stackStyle,
|
|
172
174
|
hideNativeAgents: config.hideNativeAgents,
|
|
173
175
|
pinEditorBottom: config.pinEditorBottom,
|
package/src/endpoint-store.ts
CHANGED
|
@@ -507,7 +507,7 @@ export class EndpointStore {
|
|
|
507
507
|
id: `${MONITOR_CONTROL_ENDPOINT_PREFIX}${encodeURIComponent(root.id)}`,
|
|
508
508
|
logicalKey: `monitor-control:${root.ownerId}:${root.ownerNonce}`,
|
|
509
509
|
kind: "window",
|
|
510
|
-
label:
|
|
510
|
+
label: `control·${root.ownerId.slice(0, 6)}`,
|
|
511
511
|
ordinal: Number.MIN_SAFE_INTEGER,
|
|
512
512
|
status: root.status,
|
|
513
513
|
contentRevision: revisionOf([root.contentRevision, controlActivityRevision]),
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { TUI } from "@earendil-works/pi-tui";
|
|
|
5
5
|
import { ambientKeysShouldYield, capturingOverlayVisible } from "./capturing-overlay.ts";
|
|
6
6
|
import { Key, decodeKittyPrintable, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
7
7
|
import { AgentReadStoreRouter, effectiveAgentStatus, type CompletePayload, type MessagePayload, type StartedPayload } from "./agents-store.ts";
|
|
8
|
-
import { AmbientSurfaceCache, statusText, titleFor, workingMessage, type AmbientState } from "./ambient.ts";
|
|
8
|
+
import { AmbientSurfaceCache, nextUiPromptDepth, shouldHideWorkingDuration, statusText, titleFor, workingMessage, type AmbientState } from "./ambient.ts";
|
|
9
9
|
import { generateTitleWithModel } from "./title-llm.ts";
|
|
10
10
|
import { suggestTitle } from "./title-gen.ts";
|
|
11
11
|
import { BashBgStore } from "./bash-bg-store.ts";
|
|
@@ -56,6 +56,13 @@ import {
|
|
|
56
56
|
import { SessionUiState } from "./session-ui-state.ts";
|
|
57
57
|
import { nextSessionTabId } from "./session-tabs.ts";
|
|
58
58
|
import { renderWindowBar, windowSessionColor } from "./window-bar.ts";
|
|
59
|
+
import {
|
|
60
|
+
buildWindowAutocompleteTargets,
|
|
61
|
+
createWindowAutocompleteProvider,
|
|
62
|
+
ownerDisplayToken,
|
|
63
|
+
resolveWindowRouteInput,
|
|
64
|
+
type WindowAutocompleteTarget,
|
|
65
|
+
} from "./window-autocomplete.ts";
|
|
59
66
|
import { makeWindowThreadWidget } from "./window-thread-view.ts";
|
|
60
67
|
import { agentPanelRows, panelRows } from "./viewport.ts";
|
|
61
68
|
import { createZenBrowseController, type ZenBrowseController } from "./zen-browse.ts";
|
|
@@ -68,7 +75,7 @@ import {
|
|
|
68
75
|
buildZenTaskSheet,
|
|
69
76
|
type ZenSheetDocument,
|
|
70
77
|
} from "./zen-sheet.ts";
|
|
71
|
-
import { routeAgentInput } from "./input-routing.ts";
|
|
78
|
+
import { isLegacyTodoOverlayInput, routeAgentInput } from "./input-routing.ts";
|
|
72
79
|
import { activeThemeName, ThemePicker } from "./theme-picker.ts";
|
|
73
80
|
import { ModelPicker, type ModelPickerEntry } from "./model-picker.ts";
|
|
74
81
|
import { getUsageTotals, invalidateUsageCache, renderFooter, setUsageThrottle, type PaintTheme, type WidthUtils } from "./footer.ts";
|
|
@@ -350,6 +357,27 @@ export default function (pi: ExtensionAPI): void {
|
|
|
350
357
|
// thinking label / teammate tree still stream above the visible viewport.
|
|
351
358
|
let stabilityTui: TUI | undefined;
|
|
352
359
|
let viewportStabilityPatch: ViewportStabilityPatch | undefined;
|
|
360
|
+
const ensureViewportStability = (tui: TUI): void => {
|
|
361
|
+
// Native fullscreen owns a fixed application viewport and has no main-screen
|
|
362
|
+
// applyLineResets hook. Do not cache that expected miss: the same dynamic TUI
|
|
363
|
+
// reference can later dispatch to the regular renderer.
|
|
364
|
+
if (tui.mode === "fullscreen") return;
|
|
365
|
+
if (stabilityTui === tui && viewportStabilityPatch?.active) return;
|
|
366
|
+
viewportStabilityPatch?.detach();
|
|
367
|
+
const patch = attachViewportStability(tui);
|
|
368
|
+
viewportStabilityPatch = patch;
|
|
369
|
+
stabilityTui = patch.active ? tui : undefined;
|
|
370
|
+
};
|
|
371
|
+
const requestCapturedRender = (): void => {
|
|
372
|
+
try {
|
|
373
|
+
const tui = capturedTui;
|
|
374
|
+
if (!tui) return;
|
|
375
|
+
ensureViewportStability(tui);
|
|
376
|
+
tui.requestRender();
|
|
377
|
+
} catch {
|
|
378
|
+
// tui may be gone between sessions
|
|
379
|
+
}
|
|
380
|
+
};
|
|
353
381
|
// True while Cockpit owns a capturing overlay (bash jobs, theme picker,
|
|
354
382
|
// settings panel). Split-pane resize must yield to the overlay's focus and
|
|
355
383
|
// refuse to start while one is open; otherwise the resize listener — a
|
|
@@ -383,6 +411,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
383
411
|
let sessionTheme: Theme | undefined;
|
|
384
412
|
// Fullscreen (alternate-screen fixed editor) controller, reload-gated.
|
|
385
413
|
let fullscreenController: FullscreenController | undefined;
|
|
414
|
+
/** Disposer for the legacy Alt+Shift+T compatibility hook (per applyUi). */
|
|
415
|
+
let todoOverlayShortcutDisposer: (() => void) | undefined;
|
|
386
416
|
/** Disposer for the session ←/→ navigation hook (per applyUi). */
|
|
387
417
|
let sessionBarNavDisposer: (() => void) | undefined;
|
|
388
418
|
/** Disposer for the agent-list Shift+↑/↓ scroll hook (per applyUi). */
|
|
@@ -394,6 +424,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
394
424
|
let surfaceState: CockpitSurfaceState = "disabled";
|
|
395
425
|
let running = false;
|
|
396
426
|
let runningStartedAt: number | undefined;
|
|
427
|
+
let uiPromptDepth = 0;
|
|
397
428
|
// Auto-generated session title from the first turn (the rule-based / LLM
|
|
398
429
|
// stand-in for Claude Code's Haiku title). Cleared per session.
|
|
399
430
|
let aiTitle: string | undefined;
|
|
@@ -445,13 +476,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
445
476
|
// while the model thinks, then the settled duration. Pi owns the fold.
|
|
446
477
|
const thinkingTimer = new ThinkingFoldTimer({
|
|
447
478
|
getTui: () => capturedTui,
|
|
448
|
-
requestRender:
|
|
449
|
-
try {
|
|
450
|
-
capturedTui?.requestRender();
|
|
451
|
-
} catch {
|
|
452
|
-
// tui may be gone between sessions
|
|
453
|
-
}
|
|
454
|
-
},
|
|
479
|
+
requestRender: requestCapturedRender,
|
|
455
480
|
getBaseLabel: () => (config.quietMode ? quietThinkingLabel() : undefined),
|
|
456
481
|
getGlyphs: () => resolveGlyphs(config.icons.mode),
|
|
457
482
|
isThinkingHidden: () => (lastCtx ? readHideThinkingBlock(lastCtx.cwd) : false),
|
|
@@ -496,10 +521,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
496
521
|
agents: agents.snapshot(),
|
|
497
522
|
jobs: bashBg.snapshot(),
|
|
498
523
|
running,
|
|
524
|
+
waitingForInput: uiPromptDepth > 0,
|
|
499
525
|
cwd: title.showCwd ? formatCwd(ctx.sessionManager.getCwd()) : undefined,
|
|
500
526
|
activeTool: activeTool?.name,
|
|
501
527
|
workingStartedAt: activeTool?.startedAt ?? runningStartedAt,
|
|
502
|
-
hideLiveDuration: config.staticMode,
|
|
528
|
+
hideLiveDuration: shouldHideWorkingDuration(capturedTui?.mode, config.staticMode),
|
|
503
529
|
separator: ` ${g.separator} `,
|
|
504
530
|
};
|
|
505
531
|
ambientSurfaces.setWorkingMessage((message) => ctx.ui.setWorkingMessage(message), workingMessage(state, now));
|
|
@@ -512,7 +538,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
512
538
|
state.maestroKnowledge = title.showMaestro
|
|
513
539
|
? maestroKnowledgeTag(maestro.snapshot()?.workflow?.knowledge)
|
|
514
540
|
: undefined;
|
|
515
|
-
state.frame = titleFrame(running);
|
|
541
|
+
state.frame = titleFrame(running && uiPromptDepth === 0);
|
|
516
542
|
ambientSurfaces.setTitle(
|
|
517
543
|
(value) => ctx.ui.setTitle(value),
|
|
518
544
|
titleFor(state, { ok: g.check, fail: g.cross }, g.separator, { maxLength: title.maxLength }),
|
|
@@ -590,7 +616,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
590
616
|
if (refreshAmbientSurface) refreshAmbient(now);
|
|
591
617
|
if (agentPriorityChanged) publishUiOwnership();
|
|
592
618
|
try {
|
|
593
|
-
|
|
619
|
+
requestCapturedRender();
|
|
594
620
|
activeAgentOverlayRender?.();
|
|
595
621
|
sidebarController?.requestRender();
|
|
596
622
|
} catch {
|
|
@@ -680,6 +706,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
680
706
|
sidebar: ownsDock,
|
|
681
707
|
goal: ownsDock,
|
|
682
708
|
todoExpanded: effectiveTodoExpanded(),
|
|
709
|
+
todoDurationChart: config.todoDurationChart,
|
|
683
710
|
quiet: config.enabled && config.quietMode,
|
|
684
711
|
quietSymbols: config.quietSymbols,
|
|
685
712
|
static: config.staticMode,
|
|
@@ -736,13 +763,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
736
763
|
};
|
|
737
764
|
|
|
738
765
|
|
|
739
|
-
const ensureViewportStability = (tui: TUI): void => {
|
|
740
|
-
if (stabilityTui === tui) return;
|
|
741
|
-
viewportStabilityPatch?.detach();
|
|
742
|
-
viewportStabilityPatch = attachViewportStability(tui);
|
|
743
|
-
stabilityTui = tui;
|
|
744
|
-
};
|
|
745
|
-
|
|
746
766
|
const sessionListOverlayActive = (): boolean =>
|
|
747
767
|
capturingOverlayActive || ambientKeysShouldYield(capturedTui);
|
|
748
768
|
|
|
@@ -883,6 +903,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
883
903
|
};
|
|
884
904
|
};
|
|
885
905
|
|
|
906
|
+
const selectedHashWindowTargets = new Map<string, WindowAutocompleteTarget>();
|
|
907
|
+
const windowAutocompleteTargets = () => buildWindowAutocompleteTargets(endpoints.snapshot(), {
|
|
908
|
+
current: tuiT("window.autocompleteCurrent"),
|
|
909
|
+
peer: tuiT("window.autocompletePeer"),
|
|
910
|
+
});
|
|
911
|
+
|
|
886
912
|
const publishInputTarget = (force = false): void => {
|
|
887
913
|
const target = sessionUi.mode === "window" ? selectedWindowInputTarget() : selectedAgentTarget();
|
|
888
914
|
const sigil = target && "sigil" in target ? target.sigil : "@";
|
|
@@ -1082,6 +1108,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1082
1108
|
if (config.enabled) syncAgentWidgetPlacement(ctx, "belowEditor");
|
|
1083
1109
|
};
|
|
1084
1110
|
|
|
1111
|
+
const sessionBarHint = () => {
|
|
1112
|
+
if (sessionListOverlayActive()) return undefined;
|
|
1113
|
+
return maestro.snapshot()?.artifact?.available
|
|
1114
|
+
? { text: tuiT("artifact.hint"), color: "accent" as const }
|
|
1115
|
+
: tuiT(sessionUi.mode === "agent" ? "session.agentListHint" : "session.listHint");
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1085
1118
|
const installSessionBar = (ctx: ExtensionContext): void => {
|
|
1086
1119
|
ctx.ui.setWidget(
|
|
1087
1120
|
SESSION_BAR_WIDGET_KEY,
|
|
@@ -1093,7 +1126,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1093
1126
|
getState: () => sessionUi,
|
|
1094
1127
|
getNow: () => nowSnapshot,
|
|
1095
1128
|
isMainRunning: () => running,
|
|
1096
|
-
getShortcutHint:
|
|
1129
|
+
getShortcutHint: sessionBarHint,
|
|
1097
1130
|
})(tui, theme);
|
|
1098
1131
|
return {
|
|
1099
1132
|
render(width: number): string[] {
|
|
@@ -1104,7 +1137,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1104
1137
|
sessionUi,
|
|
1105
1138
|
width,
|
|
1106
1139
|
theme,
|
|
1107
|
-
{ shortcutHint:
|
|
1140
|
+
{ shortcutHint: sessionBarHint() },
|
|
1108
1141
|
)
|
|
1109
1142
|
: agentWidget.render(width);
|
|
1110
1143
|
},
|
|
@@ -1329,6 +1362,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1329
1362
|
clearWidgets(ctx);
|
|
1330
1363
|
ctx.ui.setWidget(SESSION_BAR_WIDGET_KEY, undefined);
|
|
1331
1364
|
ctx.ui.setWidget(SESSION_DETAIL_WIDGET_KEY, undefined);
|
|
1365
|
+
todoOverlayShortcutDisposer?.();
|
|
1366
|
+
todoOverlayShortcutDisposer = undefined;
|
|
1332
1367
|
sessionBarNavDisposer?.();
|
|
1333
1368
|
sessionBarNavDisposer = undefined;
|
|
1334
1369
|
agentScrollDisposer?.();
|
|
@@ -1495,6 +1530,16 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1495
1530
|
// window mode swaps in the window bar. Installed on every surface (dock
|
|
1496
1531
|
// and widgets), inserted last so it sits closest to the editor.
|
|
1497
1532
|
installSessionBar(ctx);
|
|
1533
|
+
// Legacy terminals encode Alt+Shift+T as ESC + uppercase T, which the
|
|
1534
|
+
// host shortcut matcher cannot distinguish without enhanced key reporting.
|
|
1535
|
+
todoOverlayShortcutDisposer?.();
|
|
1536
|
+
todoOverlayShortcutDisposer = ctx.ui.onTerminalInput((data) => {
|
|
1537
|
+
if (!isLegacyTodoOverlayInput(data) || ambientKeysShouldYield(capturedTui)) return undefined;
|
|
1538
|
+
void openTodoOverlay(ctx).catch((error) => {
|
|
1539
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
|
|
1540
|
+
});
|
|
1541
|
+
return { consume: true };
|
|
1542
|
+
});
|
|
1498
1543
|
// ←/→ cycles tabs only with an empty composer. Window mode additionally
|
|
1499
1544
|
// accepts Alt+←/→ so drafts can be switched without losing cursor arrows.
|
|
1500
1545
|
// A capturing modal overlay owns navigation while it is open.
|
|
@@ -1766,6 +1811,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1766
1811
|
// --- session + agent lifecycle ---
|
|
1767
1812
|
pi.on("session_start", (_e, ctx) => {
|
|
1768
1813
|
lastCtx = ctx;
|
|
1814
|
+
uiPromptDepth = 0;
|
|
1769
1815
|
ambientSurfaces.reset();
|
|
1770
1816
|
agentReads.bindSession(ctx.sessionManager.getSessionId());
|
|
1771
1817
|
agents = agentReads.current;
|
|
@@ -1804,6 +1850,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1804
1850
|
});
|
|
1805
1851
|
settingsLocale.reload();
|
|
1806
1852
|
settingsRegistry.emitLocale(settingsLocale.locale);
|
|
1853
|
+
selectedHashWindowTargets.clear();
|
|
1854
|
+
ctx.ui.addAutocompleteProvider((current) => createWindowAutocompleteProvider(
|
|
1855
|
+
current,
|
|
1856
|
+
() => config.enabled ? windowAutocompleteTargets() : [],
|
|
1857
|
+
(target) => selectedHashWindowTargets.set(target.token.toLocaleLowerCase("en"), target),
|
|
1858
|
+
));
|
|
1807
1859
|
invalidateUsageCache();
|
|
1808
1860
|
// Quiet mode: register compact tool renderers and fold thinking blocks.
|
|
1809
1861
|
// Tools are normally registered at extension load time (above) so they
|
|
@@ -1873,6 +1925,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1873
1925
|
sidebar: false,
|
|
1874
1926
|
goal: false,
|
|
1875
1927
|
todoExpanded: config.todoExpanded,
|
|
1928
|
+
todoDurationChart: config.todoDurationChart,
|
|
1876
1929
|
quiet: false,
|
|
1877
1930
|
quietSymbols: config.quietSymbols,
|
|
1878
1931
|
static: config.staticMode,
|
|
@@ -1898,6 +1951,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1898
1951
|
lastCtx = undefined;
|
|
1899
1952
|
running = false;
|
|
1900
1953
|
runningStartedAt = undefined;
|
|
1954
|
+
uiPromptDepth = 0;
|
|
1901
1955
|
activeTools.clear();
|
|
1902
1956
|
thinkingTimer.reset();
|
|
1903
1957
|
invalidateUsageCache();
|
|
@@ -1927,6 +1981,16 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1927
1981
|
syncTick();
|
|
1928
1982
|
req();
|
|
1929
1983
|
});
|
|
1984
|
+
pi.on("ui_prompt_start", () => {
|
|
1985
|
+
uiPromptDepth = nextUiPromptDepth(uiPromptDepth, "start");
|
|
1986
|
+
req();
|
|
1987
|
+
});
|
|
1988
|
+
pi.on("ui_prompt_end", () => {
|
|
1989
|
+
const nextDepth = nextUiPromptDepth(uiPromptDepth, "end");
|
|
1990
|
+
if (nextDepth === uiPromptDepth) return;
|
|
1991
|
+
uiPromptDepth = nextDepth;
|
|
1992
|
+
req();
|
|
1993
|
+
});
|
|
1930
1994
|
|
|
1931
1995
|
// --- live elapsed for folded thinking rows ---
|
|
1932
1996
|
pi.on("message_start", (e) => {
|
|
@@ -1971,6 +2035,60 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1971
2035
|
const hasImages = (e.images?.length ?? 0) > 0;
|
|
1972
2036
|
const interactiveText = e.source === "interactive" && e.text.trim().length > 0;
|
|
1973
2037
|
const isSynthetic = e.text.startsWith("/") || e.text.startsWith("!");
|
|
2038
|
+
const hashWindowRoute = config.enabled && e.source === "interactive" && !isSynthetic
|
|
2039
|
+
? resolveWindowRouteInput(e.text, windowAutocompleteTargets(), selectedHashWindowTargets)
|
|
2040
|
+
: undefined;
|
|
2041
|
+
|
|
2042
|
+
if (hashWindowRoute) {
|
|
2043
|
+
if (hashWindowRoute.code !== "resolved") {
|
|
2044
|
+
ctx.ui.notify(tuiT(hashWindowRoute.code === "stale"
|
|
2045
|
+
? "notice.windowTargetUnavailable"
|
|
2046
|
+
: "notice.windowTargetAmbiguous"), "warning");
|
|
2047
|
+
ctx.ui.setEditorText(e.text);
|
|
2048
|
+
return { action: "handled" as const };
|
|
2049
|
+
}
|
|
2050
|
+
if (!hashWindowRoute.message) {
|
|
2051
|
+
ctx.ui.notify(tuiT("notice.windowMessageRequired"), "warning");
|
|
2052
|
+
ctx.ui.setEditorText(e.text);
|
|
2053
|
+
return { action: "handled" as const };
|
|
2054
|
+
}
|
|
2055
|
+
if (hashWindowRoute.target.current) {
|
|
2056
|
+
selectedHashWindowTargets.delete(hashWindowRoute.token);
|
|
2057
|
+
if (!firstUserText) firstUserText = hashWindowRoute.message;
|
|
2058
|
+
return { action: "transform" as const, text: hashWindowRoute.message };
|
|
2059
|
+
}
|
|
2060
|
+
if (hasImages) {
|
|
2061
|
+
ctx.ui.notify(tuiT("notice.imagePeer"), "warning");
|
|
2062
|
+
ctx.ui.setEditorText(e.text);
|
|
2063
|
+
return { action: "handled" as const };
|
|
2064
|
+
}
|
|
2065
|
+
if (!registry) {
|
|
2066
|
+
ctx.ui.notify(tuiT("notice.peerDeliveryUnavailable"), "warning");
|
|
2067
|
+
ctx.ui.setEditorText(e.text);
|
|
2068
|
+
return { action: "handled" as const };
|
|
2069
|
+
}
|
|
2070
|
+
const delivery = await (registry.send?.({
|
|
2071
|
+
selector: hashWindowRoute.target.routeSelector,
|
|
2072
|
+
message: hashWindowRoute.message,
|
|
2073
|
+
mode: e.streamingBehavior === "steer" ? "steer" : "follow_up",
|
|
2074
|
+
source: "user",
|
|
2075
|
+
}) ?? registry.router?.route({
|
|
2076
|
+
selector: hashWindowRoute.target.routeSelector,
|
|
2077
|
+
message: hashWindowRoute.message,
|
|
2078
|
+
mode: e.streamingBehavior === "steer" ? "steer" : "follow_up",
|
|
2079
|
+
source: "user",
|
|
2080
|
+
}));
|
|
2081
|
+
if (!delivery?.delivered) {
|
|
2082
|
+
ctx.ui.notify(tuiT("notice.peerMessageFailed", {
|
|
2083
|
+
label: hashWindowRoute.target.label,
|
|
2084
|
+
message: delivery?.error ?? tuiT("notice.deliveryRegistryUnavailable"),
|
|
2085
|
+
}), "error");
|
|
2086
|
+
ctx.ui.setEditorText(e.text);
|
|
2087
|
+
} else {
|
|
2088
|
+
selectedHashWindowTargets.delete(hashWindowRoute.token);
|
|
2089
|
+
}
|
|
2090
|
+
return { action: "handled" as const };
|
|
2091
|
+
}
|
|
1974
2092
|
|
|
1975
2093
|
if (e.source === "interactive" && !hasImages && e.text.trim() === "monitor") {
|
|
1976
2094
|
if (!registry?.requestWindowMode) {
|
|
@@ -1981,7 +2099,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1981
2099
|
return { action: "handled" as const };
|
|
1982
2100
|
}
|
|
1983
2101
|
|
|
1984
|
-
if (sessionUi.mode === "window" && (interactiveText || hasImages) && !isSynthetic) {
|
|
2102
|
+
if (config.enabled && sessionUi.mode === "window" && (interactiveText || hasImages) && !isSynthetic) {
|
|
1985
2103
|
const target = selectedWindowEndpoint();
|
|
1986
2104
|
if (!target) {
|
|
1987
2105
|
ctx.ui.notify(tuiT("notice.noMonitorWindow"), "warning");
|
|
@@ -2221,7 +2339,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2221
2339
|
: "";
|
|
2222
2340
|
const detail = [current, tuiStatus(endpoint.status), agentCount].filter(Boolean).join(" · ");
|
|
2223
2341
|
const sigil = mode === "window" ? "#" : "@";
|
|
2224
|
-
|
|
2342
|
+
const showOwner = mode === "window" || endpoint.kind === "root";
|
|
2343
|
+
const label = showOwner && endpoint.registryEndpoint
|
|
2344
|
+
? ownerDisplayToken(endpoint.label, endpoint.registryEndpoint.ownerId)
|
|
2345
|
+
: endpoint.label;
|
|
2346
|
+
return `${sigil}${label}${detail ? ` · ${detail}` : ""}`;
|
|
2225
2347
|
});
|
|
2226
2348
|
let previewAgent = false;
|
|
2227
2349
|
enterCapturingOverlay();
|
package/src/input-routing.ts
CHANGED
|
@@ -34,6 +34,10 @@ export interface AgentInputRegistries {
|
|
|
34
34
|
|
|
35
35
|
export type AgentInputRegistryProvider = MailboxHostRegistry | AgentInputRegistries;
|
|
36
36
|
|
|
37
|
+
export function isLegacyTodoOverlayInput(data: string): boolean {
|
|
38
|
+
return data === "\x1bT";
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
function inputRegistries(provider: AgentInputRegistryProvider | undefined): AgentInputRegistries {
|
|
38
42
|
if (provider && "deliverAgentMessage" in provider) return { mailbox: provider };
|
|
39
43
|
return provider ?? {};
|
package/src/maestro-store.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sanitizeExtensionStatusText } from "./extension-status.ts";
|
|
2
2
|
import {
|
|
3
3
|
MAESTRO_UI_SNAPSHOT_VERSION,
|
|
4
|
+
type MaestroArtifactV1,
|
|
4
5
|
type MaestroGoalV1,
|
|
5
6
|
type MaestroModeV1,
|
|
6
7
|
type MaestroSwarmBestV1,
|
|
@@ -87,6 +88,8 @@ function parseSnapshot(value: unknown): MaestroUiSnapshotV1 | undefined {
|
|
|
87
88
|
}
|
|
88
89
|
const swarm = value.swarm === null ? null : parseSwarm(value.swarm);
|
|
89
90
|
if (swarm === undefined) return undefined;
|
|
91
|
+
const artifact = value.artifact === undefined ? undefined : parseArtifact(value.artifact);
|
|
92
|
+
if (value.artifact !== undefined && artifact === undefined) return undefined;
|
|
90
93
|
const mode = parseMode(value.mode);
|
|
91
94
|
if (mode === undefined) return undefined;
|
|
92
95
|
if (value.currentGoalId !== undefined && !isOpaqueId(value.currentGoalId)) return undefined;
|
|
@@ -98,6 +101,7 @@ function parseSnapshot(value: unknown): MaestroUiSnapshotV1 | undefined {
|
|
|
98
101
|
goals,
|
|
99
102
|
...(value.currentGoalId === undefined ? {} : { currentGoalId: value.currentGoalId }),
|
|
100
103
|
swarm,
|
|
104
|
+
...(artifact === undefined ? {} : { artifact }),
|
|
101
105
|
mode,
|
|
102
106
|
};
|
|
103
107
|
}
|
|
@@ -239,6 +243,20 @@ function parseSwarmBest(value: unknown): MaestroSwarmBestV1 | undefined {
|
|
|
239
243
|
};
|
|
240
244
|
}
|
|
241
245
|
|
|
246
|
+
function parseArtifact(value: unknown): MaestroArtifactV1 | undefined {
|
|
247
|
+
if (!isRecord(value) || typeof value.available !== "boolean") return undefined;
|
|
248
|
+
if (value.planRevision !== undefined && !isNonNegativeInteger(value.planRevision)) return undefined;
|
|
249
|
+
const planStatus = value.planStatus === undefined ? undefined : displayString(value.planStatus);
|
|
250
|
+
if (value.planStatus !== undefined && planStatus === undefined) return undefined;
|
|
251
|
+
if (value.knowledgeSessionId !== undefined && !isOpaqueId(value.knowledgeSessionId)) return undefined;
|
|
252
|
+
return {
|
|
253
|
+
available: value.available,
|
|
254
|
+
...(value.planRevision === undefined ? {} : { planRevision: value.planRevision }),
|
|
255
|
+
...(planStatus === undefined ? {} : { planStatus }),
|
|
256
|
+
...(value.knowledgeSessionId === undefined ? {} : { knowledgeSessionId: value.knowledgeSessionId }),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
242
260
|
function parseMode(value: unknown): MaestroModeV1 | undefined {
|
|
243
261
|
if (typeof value === "string") return sanitizeExtensionStatusText(value);
|
|
244
262
|
if (!isRecord(value)) return undefined;
|
package/src/public/v1/events.ts
CHANGED
|
@@ -138,6 +138,14 @@ export interface MaestroSwarmV1 {
|
|
|
138
138
|
updatedAt: number;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/** Session-bound documents exposed through `/artifact`. */
|
|
142
|
+
export interface MaestroArtifactV1 {
|
|
143
|
+
available: boolean;
|
|
144
|
+
planRevision?: number;
|
|
145
|
+
planStatus?: string;
|
|
146
|
+
knowledgeSessionId?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
141
149
|
/**
|
|
142
150
|
* The active Maestro projection. String values are deliberately open so adding
|
|
143
151
|
* a producer mode does not require a protocol revision.
|
|
@@ -164,6 +172,8 @@ export interface MaestroUiStateSnapshotV1 extends MaestroUiEnvelopeV1 {
|
|
|
164
172
|
goals: MaestroGoalV1[];
|
|
165
173
|
currentGoalId?: string;
|
|
166
174
|
swarm: MaestroSwarmV1 | null;
|
|
175
|
+
/** Optional for compatibility with older Maestro producers. */
|
|
176
|
+
artifact?: MaestroArtifactV1;
|
|
167
177
|
mode: MaestroModeV1;
|
|
168
178
|
}
|
|
169
179
|
|
|
@@ -185,6 +195,8 @@ export interface CockpitUiOwnershipV1 {
|
|
|
185
195
|
/** True only while the dock renders Maestro Goal state. */
|
|
186
196
|
goal: boolean;
|
|
187
197
|
todoExpanded: boolean;
|
|
198
|
+
/** Whether Flow should render the duration chart after a terminal todo advance. */
|
|
199
|
+
todoDurationChart?: boolean;
|
|
188
200
|
quiet: boolean;
|
|
189
201
|
quietSymbols: "check" | "dot";
|
|
190
202
|
/** Static mode: per-second churn (elapsed ticks, spinners) is suppressed. */
|
package/src/render.ts
CHANGED
|
@@ -63,6 +63,8 @@ export interface RenderOpts {
|
|
|
63
63
|
* owner overrides this with the always-available `/cockpit todo`.
|
|
64
64
|
*/
|
|
65
65
|
toggleHint?: string;
|
|
66
|
+
/** How the user opens the full Todo overlay. Cockpit registers this shortcut itself. */
|
|
67
|
+
detailsHint?: string;
|
|
66
68
|
/**
|
|
67
69
|
* Compact agent mode prints its own roster count. When the widget already
|
|
68
70
|
* renders an "Agents N running" header, that would be the same sentence twice
|
|
@@ -80,6 +82,7 @@ export interface RenderOpts {
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export const DEFAULT_TOGGLE_HINT = altKey("T");
|
|
85
|
+
export const DEFAULT_TODO_DETAILS_HINT = altKey("Shift+T");
|
|
83
86
|
|
|
84
87
|
// One cell per task stops scaling once the plan outgrows the terminal, so beyond
|
|
85
88
|
// that the bar switches to a proportional summary instead of eating the whole line.
|
|
@@ -469,6 +472,7 @@ export function renderTodos(
|
|
|
469
472
|
const ell = theme.fg("dim", g.ellipsis);
|
|
470
473
|
const expanded = opts.expanded !== false;
|
|
471
474
|
const toggleHint = opts.toggleHint ?? DEFAULT_TOGGLE_HINT;
|
|
475
|
+
const detailsHint = opts.detailsHint ?? DEFAULT_TODO_DETAILS_HINT;
|
|
472
476
|
|
|
473
477
|
if (mode === "compact") {
|
|
474
478
|
const total = items.length;
|
|
@@ -522,6 +526,13 @@ export function renderTodos(
|
|
|
522
526
|
const ntext = `${g.arrow} ${theme.fg(np.glyphColor, np.glyph)}${nactor ? ` ${theme.fg("mdLink", nactor)}` : ""} ${todoSubject(np, next.subject, theme)}`;
|
|
523
527
|
summarySegs.push({ text: ntext, priority: 20, clippable: false });
|
|
524
528
|
}
|
|
529
|
+
if (expanded) {
|
|
530
|
+
summarySegs.push({
|
|
531
|
+
text: theme.fg("dim", tuiT("widget.todo.details", { shortcut: detailsHint })),
|
|
532
|
+
priority: 35,
|
|
533
|
+
clippable: false,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
525
536
|
summarySegs.push({
|
|
526
537
|
text: theme.fg("dim", `(${toggleHint} ${tuiT(expanded ? "widget.todo.collapse" : "widget.todo.expandAction")})`),
|
|
527
538
|
priority: 10,
|
|
@@ -68,6 +68,7 @@ const CONFIG_KEYS = [
|
|
|
68
68
|
"agentsMode",
|
|
69
69
|
"todoMode",
|
|
70
70
|
"todoExpanded",
|
|
71
|
+
"todoDurationChart",
|
|
71
72
|
"stackStyle",
|
|
72
73
|
"hideNativeAgents",
|
|
73
74
|
"toolPalette",
|
|
@@ -164,6 +165,8 @@ const CATALOGS = {
|
|
|
164
165
|
"cockpit.agentsMode": "Agent display",
|
|
165
166
|
"cockpit.todoMode": "Todo display",
|
|
166
167
|
"cockpit.todoExpanded": "Todo expanded",
|
|
168
|
+
"cockpit.todoDurationChart": "Advance duration chart",
|
|
169
|
+
"cockpit.todoDurationChart.description": "Show a task-duration chart after advance completes the entire Todo list.",
|
|
167
170
|
"cockpit.stackStyle": "Stack projection",
|
|
168
171
|
"cockpit.hideNativeAgents": "Hide native agent widget",
|
|
169
172
|
"cockpit.icons.mode": "Icon mode",
|
|
@@ -249,6 +252,8 @@ const CATALOGS = {
|
|
|
249
252
|
"cockpit.agentsMode": "Agent 显示",
|
|
250
253
|
"cockpit.todoMode": "Todo 显示",
|
|
251
254
|
"cockpit.todoExpanded": "展开 Todo",
|
|
255
|
+
"cockpit.todoDurationChart": "Advance 耗时图",
|
|
256
|
+
"cockpit.todoDurationChart.description": "当 advance 完成整个 Todo 列表后显示任务耗时统计图。",
|
|
252
257
|
"cockpit.stackStyle": "栈投影",
|
|
253
258
|
"cockpit.hideNativeAgents": "隐藏原生 Agent 面板",
|
|
254
259
|
"cockpit.icons.mode": "图标模式",
|
|
@@ -367,8 +372,9 @@ const DEFINITIONS: readonly SettingDefinition[] = [
|
|
|
367
372
|
enumDefinition("agentsMode", "cockpit.group.panels", 0, "cockpit.agentsMode", ["list", "compact"], "live"),
|
|
368
373
|
enumDefinition("todoMode", "cockpit.group.panels", 1, "cockpit.todoMode", ["list", "compact"], "live"),
|
|
369
374
|
booleanDefinition("todoExpanded", "cockpit.group.panels", 2, "cockpit.todoExpanded", "live"),
|
|
375
|
+
booleanDefinition("todoDurationChart", "cockpit.group.panels", 3, "cockpit.todoDurationChart", "live", "cockpit.todoDurationChart.description"),
|
|
370
376
|
enumDefinition("stackStyle", "cockpit.group.layout", 4, "cockpit.stackStyle", ["classic", "zen"], "live"),
|
|
371
|
-
booleanDefinition("hideNativeAgents", "cockpit.group.panels",
|
|
377
|
+
booleanDefinition("hideNativeAgents", "cockpit.group.panels", 4, "cockpit.hideNativeAgents", "live"),
|
|
372
378
|
enumDefinition("sidebar.mode", "cockpit.group.sidebar", 0, "cockpit.sidebar.mode", ["auto", "on", "off"], "live"),
|
|
373
379
|
{
|
|
374
380
|
key: "sidebar.width",
|
|
@@ -789,7 +795,7 @@ function setUsageValue(usage: CockpitConfig["usage"], field: string, value: Json
|
|
|
789
795
|
}
|
|
790
796
|
|
|
791
797
|
function validValue(key: CockpitSettingKey, value: JsonValue): boolean {
|
|
792
|
-
if (["enabled", "staticMode", "pinEditorBottom", "doubleEscapeClearInput", "fullscreenInput", "copyOnSelect", "historyEnabled", "quietMode", "todoExpanded", "hideNativeAgents"].includes(key)) return typeof value === "boolean";
|
|
798
|
+
if (["enabled", "staticMode", "pinEditorBottom", "doubleEscapeClearInput", "fullscreenInput", "copyOnSelect", "historyEnabled", "quietMode", "todoExpanded", "todoDurationChart", "hideNativeAgents"].includes(key)) return typeof value === "boolean";
|
|
793
799
|
if (key === "sidebar.width") return typeof value === "number" && Number.isSafeInteger(value) && value >= 32 && value <= 56;
|
|
794
800
|
if (key === "title.maxLength") return typeof value === "number" && Number.isSafeInteger(value) && value >= 20 && value <= 200;
|
|
795
801
|
if (key === "title.generationModel") return typeof value === "string";
|
package/src/stack-widget.ts
CHANGED
|
@@ -141,7 +141,7 @@ export function makeAgentWidget(deps: AgentWidgetDeps) {
|
|
|
141
141
|
// Focused-session priority: while a selected session's detail block is
|
|
142
142
|
// open, it owns the Agent height allowance and the roster collapses to
|
|
143
143
|
// this one-line summary. The per-agent rows stay reachable through the
|
|
144
|
-
// session
|
|
144
|
+
// session bar and its Alt+R selection flow, so monitoring is preserved
|
|
145
145
|
// while the session content gains the released rows.
|
|
146
146
|
if (deps.hasSessionDetail?.()) return [headerLine];
|
|
147
147
|
|
package/src/tui-i18n.ts
CHANGED
|
@@ -68,6 +68,8 @@ export const COCKPIT_TUI_CATALOGS: TranslationCatalogs = {
|
|
|
68
68
|
"common.ruleBased": "(rule-based)",
|
|
69
69
|
"common.customRef": "custom ref…",
|
|
70
70
|
"session.listHint": `${altKey("R")} list`,
|
|
71
|
+
"session.agentListHint": `${altKey("R")} Agent`,
|
|
72
|
+
"artifact.hint": "/artifact",
|
|
71
73
|
"session.detailHint": `${altKey("R")} preview · ${altKey("E")} hide`,
|
|
72
74
|
"session.previewHint": "PgUp/PgDn scroll output",
|
|
73
75
|
"session.thinking": "thinking…",
|
|
@@ -76,6 +78,8 @@ export const COCKPIT_TUI_CATALOGS: TranslationCatalogs = {
|
|
|
76
78
|
"window.empty": "Windows · no peer sessions",
|
|
77
79
|
"window.title": "Windows",
|
|
78
80
|
"window.context": "context {percent}%",
|
|
81
|
+
"window.autocompleteCurrent": "current window",
|
|
82
|
+
"window.autocompletePeer": "peer window",
|
|
79
83
|
"window.currentCoordination": "Current session · monitor coordination",
|
|
80
84
|
"window.noMessages": "No messages in this window thread.",
|
|
81
85
|
"window.messages": "Messages",
|
|
@@ -151,6 +155,7 @@ export const COCKPIT_TUI_CATALOGS: TranslationCatalogs = {
|
|
|
151
155
|
"widget.agents.noun": "agents",
|
|
152
156
|
"widget.todo.title": "Todo",
|
|
153
157
|
"widget.todo.expand": "({shortcut} expand)",
|
|
158
|
+
"widget.todo.details": "{shortcut} full list",
|
|
154
159
|
"widget.todo.collapse": "collapse",
|
|
155
160
|
"widget.todo.expandAction": "expand",
|
|
156
161
|
"widget.agent.lastFailed": "last failed",
|
|
@@ -235,6 +240,7 @@ export const COCKPIT_TUI_CATALOGS: TranslationCatalogs = {
|
|
|
235
240
|
"sidebar.unavailable": "Cockpit sidebar unavailable",
|
|
236
241
|
"sidebar.requiresTui": "Cockpit sidebar requires TUI mode",
|
|
237
242
|
"ambient.working": "working",
|
|
243
|
+
"ambient.waitingForInput": "WAITING FOR INPUT",
|
|
238
244
|
"ambient.failed": "{count} failed",
|
|
239
245
|
"ambient.agents": "{count} agents",
|
|
240
246
|
"legacy.label.enabled": "enabled",
|
|
@@ -280,6 +286,9 @@ export const COCKPIT_TUI_CATALOGS: TranslationCatalogs = {
|
|
|
280
286
|
"notice.noMonitorWindow": "No monitor window is selected.",
|
|
281
287
|
"notice.monitorUnavailable": "Window monitor mode is unavailable in this session.",
|
|
282
288
|
"notice.imagePeer": "Image input cannot be routed to a peer window; reattach it in #control or after leaving monitor mode.",
|
|
289
|
+
"notice.windowMessageRequired": "Enter a message after the selected #window.",
|
|
290
|
+
"notice.windowTargetUnavailable": "The selected #window is no longer available; the message was not sent.",
|
|
291
|
+
"notice.windowTargetAmbiguous": "The selected #window is ambiguous; choose it again before sending.",
|
|
283
292
|
"notice.peerDeliveryUnavailable": "Peer window delivery is unavailable.",
|
|
284
293
|
"notice.peerMessageFailed": "Message to #{label} was not sent: {message}",
|
|
285
294
|
"notice.deliveryRegistryUnavailable": "delivery registry unavailable",
|
|
@@ -387,6 +396,8 @@ export const COCKPIT_TUI_CATALOGS: TranslationCatalogs = {
|
|
|
387
396
|
"common.ruleBased": "(规则生成)",
|
|
388
397
|
"common.customRef": "自定义引用…",
|
|
389
398
|
"session.listHint": `${altKey("R")} 列表`,
|
|
399
|
+
"session.agentListHint": `${altKey("R")} Agent`,
|
|
400
|
+
"artifact.hint": "/artifact",
|
|
390
401
|
"session.detailHint": `${altKey("R")} 预览 · ${altKey("E")} 隐藏`,
|
|
391
402
|
"session.previewHint": "PgUp/PgDn 滚动输出",
|
|
392
403
|
"session.thinking": "思考中…",
|
|
@@ -395,6 +406,8 @@ export const COCKPIT_TUI_CATALOGS: TranslationCatalogs = {
|
|
|
395
406
|
"window.empty": "窗口 · 没有对等会话",
|
|
396
407
|
"window.title": "窗口",
|
|
397
408
|
"window.context": "上下文 {percent}%",
|
|
409
|
+
"window.autocompleteCurrent": "当前窗口",
|
|
410
|
+
"window.autocompletePeer": "对等窗口",
|
|
398
411
|
"window.currentCoordination": "当前会话 · Monitor 协调",
|
|
399
412
|
"window.noMessages": "此窗口线程中没有消息。",
|
|
400
413
|
"window.messages": "消息",
|
|
@@ -470,6 +483,7 @@ export const COCKPIT_TUI_CATALOGS: TranslationCatalogs = {
|
|
|
470
483
|
"widget.agents.noun": "Agent",
|
|
471
484
|
"widget.todo.title": "Todo",
|
|
472
485
|
"widget.todo.expand": "({shortcut} 展开)",
|
|
486
|
+
"widget.todo.details": "{shortcut} 详细列表",
|
|
473
487
|
"widget.todo.collapse": "收起",
|
|
474
488
|
"widget.todo.expandAction": "展开",
|
|
475
489
|
"widget.agent.lastFailed": "上次失败",
|
|
@@ -554,6 +568,7 @@ export const COCKPIT_TUI_CATALOGS: TranslationCatalogs = {
|
|
|
554
568
|
"sidebar.unavailable": "Cockpit 侧边栏不可用",
|
|
555
569
|
"sidebar.requiresTui": "Cockpit 侧边栏需要 TUI 模式",
|
|
556
570
|
"ambient.working": "工作中",
|
|
571
|
+
"ambient.waitingForInput": "等待输入",
|
|
557
572
|
"ambient.failed": "{count} 项失败",
|
|
558
573
|
"ambient.agents": "{count} 个 Agent",
|
|
559
574
|
"legacy.label.enabled": "启用",
|
|
@@ -599,6 +614,9 @@ export const COCKPIT_TUI_CATALOGS: TranslationCatalogs = {
|
|
|
599
614
|
"notice.noMonitorWindow": "未选择 Monitor 窗口。",
|
|
600
615
|
"notice.monitorUnavailable": "当前会话不支持窗口监控模式。",
|
|
601
616
|
"notice.imagePeer": "无法将图片路由到对等窗口;请在 #control 中或离开 Monitor 模式后重新附加。",
|
|
617
|
+
"notice.windowMessageRequired": "请在选中的 #窗口 后输入消息。",
|
|
618
|
+
"notice.windowTargetUnavailable": "选中的 #窗口 已不可用;消息未发送。",
|
|
619
|
+
"notice.windowTargetAmbiguous": "选中的 #窗口 存在歧义;请重新选择后再发送。",
|
|
602
620
|
"notice.peerDeliveryUnavailable": "对等窗口消息发送不可用。",
|
|
603
621
|
"notice.peerMessageFailed": "发送到 #{label} 的消息失败:{message}",
|
|
604
622
|
"notice.deliveryRegistryUnavailable": "发送注册表不可用",
|
package/src/types.ts
CHANGED
|
@@ -260,6 +260,8 @@ export interface CockpitConfig {
|
|
|
260
260
|
agentsMode: ViewMode;
|
|
261
261
|
todoMode: ViewMode;
|
|
262
262
|
todoExpanded: boolean;
|
|
263
|
+
/** Show the duration chart after a terminal todo advance. */
|
|
264
|
+
todoDurationChart: boolean;
|
|
263
265
|
/** Above-editor stack projection: classic Todo/Agents widgets or the unified Zen stack. */
|
|
264
266
|
stackStyle: StackStyle;
|
|
265
267
|
hideNativeAgents: boolean;
|
|
@@ -318,6 +320,7 @@ export const DEFAULT_CONFIG: CockpitConfig = {
|
|
|
318
320
|
agentsMode: "list",
|
|
319
321
|
todoMode: "list",
|
|
320
322
|
todoExpanded: false,
|
|
323
|
+
todoDurationChart: true,
|
|
321
324
|
stackStyle: "classic",
|
|
322
325
|
hideNativeAgents: true,
|
|
323
326
|
pinEditorBottom: false,
|
|
@@ -10,7 +10,8 @@ interface ViewportTuiInternals {
|
|
|
10
10
|
previousLines?: string[];
|
|
11
11
|
previousViewportTop?: number;
|
|
12
12
|
previousHeight?: number;
|
|
13
|
-
|
|
13
|
+
previousWidth?: number;
|
|
14
|
+
terminal?: { columns: number; rows: number };
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
interface ViewportStabilityMarker {
|
|
@@ -98,15 +99,17 @@ function resolveApplyLineResetsSlot(internals: ViewportTuiInternals): ApplyLineR
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* pi-tui
|
|
104
|
-
*
|
|
105
|
-
* cannot be updated in terminal scrollback and should only advance the diff
|
|
106
|
-
* baseline. Visible changes are then rendered normally from viewportTop.
|
|
102
|
+
* Treats the hidden viewport prefix as immutable scrollback while frame size is
|
|
103
|
+
* stable. The next frame keeps the terminal's actual hidden text, so visible
|
|
104
|
+
* changes can use pi-tui's differential path instead of clearing and replaying
|
|
105
|
+
* the screen. Structural, resize and image changes retain the native path.
|
|
107
106
|
*/
|
|
108
107
|
export function attachViewportStability(tui: TUI): ViewportStabilityPatch {
|
|
109
108
|
try {
|
|
109
|
+
// Native fullscreen has an application-owned fixed viewport. Its inherited
|
|
110
|
+
// applyLineResets method is not part of that renderer's diff path, so patching
|
|
111
|
+
// it would report a false-positive attachment on the shared TUI base class.
|
|
112
|
+
if (tui.mode === "fullscreen") return { active: false, detach() {} };
|
|
110
113
|
const internals = tui as unknown as ViewportTuiInternals;
|
|
111
114
|
const slot = resolveApplyLineResetsSlot(internals);
|
|
112
115
|
if (!slot) return { active: false, detach() {} };
|
|
@@ -120,24 +123,23 @@ export function attachViewportStability(tui: TUI): ViewportStabilityPatch {
|
|
|
120
123
|
const nextLines = original.call(this, lines);
|
|
121
124
|
const previousLines = this.previousLines;
|
|
122
125
|
const viewportTop = this.previousViewportTop;
|
|
123
|
-
const previousHeight = this.previousHeight;
|
|
124
|
-
const currentHeight = this.terminal?.rows;
|
|
125
126
|
if (
|
|
126
127
|
Array.isArray(previousLines)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
&& Number.isFinite(previousHeight)
|
|
134
|
-
&& previousHeight === currentHeight
|
|
128
|
+
&& previousLines.length === nextLines.length
|
|
129
|
+
&& typeof viewportTop === "number"
|
|
130
|
+
&& Number.isFinite(viewportTop)
|
|
131
|
+
&& viewportTop > 0
|
|
132
|
+
&& this.previousHeight === this.terminal?.rows
|
|
133
|
+
&& this.previousWidth === this.terminal?.columns
|
|
135
134
|
) {
|
|
136
135
|
const hiddenEnd = Math.min(previousLines.length, Math.trunc(viewportTop));
|
|
137
136
|
const hiddenHasKittyImage = previousLines.slice(0, hiddenEnd).some(isKittyImageLine)
|
|
138
137
|
|| nextLines.slice(0, hiddenEnd).some(isKittyImageLine);
|
|
139
138
|
if (!hiddenHasKittyImage) {
|
|
140
|
-
for (let index = 0; index < hiddenEnd; index
|
|
139
|
+
for (let index = 0; index < hiddenEnd; index += 1) {
|
|
140
|
+
const previous = previousLines[index];
|
|
141
|
+
if (previous !== undefined) nextLines[index] = previous;
|
|
142
|
+
}
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
145
|
return nextLines;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AutocompleteItem,
|
|
3
|
+
AutocompleteProvider,
|
|
4
|
+
AutocompleteSuggestions,
|
|
5
|
+
} from "@earendil-works/pi-tui";
|
|
6
|
+
import type { CockpitEndpoint, EndpointStoreSnapshot } from "./endpoint-store.ts";
|
|
7
|
+
|
|
8
|
+
const OWNER_LABEL_LENGTH = 6;
|
|
9
|
+
const MAX_WINDOW_SUGGESTIONS = 20;
|
|
10
|
+
|
|
11
|
+
export interface WindowAutocompleteLabels {
|
|
12
|
+
current: string;
|
|
13
|
+
peer: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WindowAutocompleteTarget {
|
|
17
|
+
token: string;
|
|
18
|
+
label: string;
|
|
19
|
+
description: string;
|
|
20
|
+
routeSelector: string;
|
|
21
|
+
ownerId: string;
|
|
22
|
+
ownerNonce: string;
|
|
23
|
+
current: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type WindowRouteResolution = {
|
|
27
|
+
code: "resolved";
|
|
28
|
+
token: string;
|
|
29
|
+
target: WindowAutocompleteTarget;
|
|
30
|
+
message: string;
|
|
31
|
+
} | {
|
|
32
|
+
code: "stale" | "ambiguous";
|
|
33
|
+
token: string;
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function ownerDisplayToken(label: string, ownerId: string): string {
|
|
38
|
+
const normalized = label
|
|
39
|
+
.replace(/[\s#]+/g, "-")
|
|
40
|
+
.replace(/^-+|-+$/g, "") || "window";
|
|
41
|
+
const suffix = `·${ownerId.slice(0, OWNER_LABEL_LENGTH)}`;
|
|
42
|
+
return normalized.endsWith(suffix) ? normalized : `${normalized}${suffix}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function targetFromEndpoint(
|
|
46
|
+
endpoint: CockpitEndpoint,
|
|
47
|
+
current: boolean,
|
|
48
|
+
labels: WindowAutocompleteLabels,
|
|
49
|
+
): WindowAutocompleteTarget | undefined {
|
|
50
|
+
const registryEndpoint = endpoint.registryEndpoint;
|
|
51
|
+
if (!registryEndpoint || registryEndpoint.kind !== "root") return undefined;
|
|
52
|
+
const ownerTarget = `owner:${registryEndpoint.ownerId}`;
|
|
53
|
+
const displayLabel = current ? "control" : endpoint.label;
|
|
54
|
+
return {
|
|
55
|
+
token: ownerDisplayToken(displayLabel, registryEndpoint.ownerId),
|
|
56
|
+
label: displayLabel,
|
|
57
|
+
description: [
|
|
58
|
+
current ? labels.current : labels.peer,
|
|
59
|
+
ownerTarget,
|
|
60
|
+
`incarnation:${registryEndpoint.ownerNonce}`,
|
|
61
|
+
registryEndpoint.sessionId ? `session:${registryEndpoint.sessionId}` : undefined,
|
|
62
|
+
].filter(Boolean).join(" · "),
|
|
63
|
+
routeSelector: endpoint.routeSelector,
|
|
64
|
+
ownerId: registryEndpoint.ownerId,
|
|
65
|
+
ownerNonce: registryEndpoint.ownerNonce,
|
|
66
|
+
current,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildWindowAutocompleteTargets(
|
|
71
|
+
snapshot: EndpointStoreSnapshot,
|
|
72
|
+
labels: WindowAutocompleteLabels,
|
|
73
|
+
): WindowAutocompleteTarget[] {
|
|
74
|
+
const currentRoot = snapshot.endpoints.find((endpoint) =>
|
|
75
|
+
endpoint.kind === "root" && endpoint.registryEndpoint?.scope === "local"
|
|
76
|
+
);
|
|
77
|
+
const targets: WindowAutocompleteTarget[] = [];
|
|
78
|
+
if (currentRoot) {
|
|
79
|
+
const current = targetFromEndpoint(currentRoot, true, labels);
|
|
80
|
+
if (current) targets.push(current);
|
|
81
|
+
}
|
|
82
|
+
for (const endpoint of snapshot.windows) {
|
|
83
|
+
if (endpoint.registryEndpoint?.scope !== "workspace-peer") continue;
|
|
84
|
+
const target = targetFromEndpoint(endpoint, false, labels);
|
|
85
|
+
if (target) targets.push(target);
|
|
86
|
+
}
|
|
87
|
+
return targets;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function extractWindowCompletionQuery(textBeforeCursor: string): string | undefined {
|
|
91
|
+
return /^[ \t]*#([^\s#]*)$/.exec(textBeforeCursor)?.[1];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function matchingTargets(
|
|
95
|
+
targets: readonly WindowAutocompleteTarget[],
|
|
96
|
+
query: string,
|
|
97
|
+
): WindowAutocompleteTarget[] {
|
|
98
|
+
const normalized = query.trim().toLocaleLowerCase("en");
|
|
99
|
+
if (!normalized) return targets.slice(0, MAX_WINDOW_SUGGESTIONS);
|
|
100
|
+
return targets.filter((target) => [
|
|
101
|
+
target.token,
|
|
102
|
+
target.label,
|
|
103
|
+
target.ownerId,
|
|
104
|
+
target.description,
|
|
105
|
+
].some((value) => value.toLocaleLowerCase("en").includes(normalized)))
|
|
106
|
+
.slice(0, MAX_WINDOW_SUGGESTIONS);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function completionItem(target: WindowAutocompleteTarget): AutocompleteItem {
|
|
110
|
+
return {
|
|
111
|
+
value: `#${target.token}`,
|
|
112
|
+
label: `#${target.token}`,
|
|
113
|
+
description: target.description,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function createWindowAutocompleteProvider(
|
|
118
|
+
current: AutocompleteProvider,
|
|
119
|
+
getTargets: () => readonly WindowAutocompleteTarget[],
|
|
120
|
+
onSelect: (target: WindowAutocompleteTarget) => void,
|
|
121
|
+
): AutocompleteProvider {
|
|
122
|
+
const suggestedTargets = new Map<string, WindowAutocompleteTarget>();
|
|
123
|
+
const suggestionKey = (item: AutocompleteItem): string => `${item.value}\0${item.description ?? ""}`;
|
|
124
|
+
return {
|
|
125
|
+
triggerCharacters: [...new Set([...(current.triggerCharacters ?? []), "#"])],
|
|
126
|
+
async getSuggestions(lines, cursorLine, cursorCol, options): Promise<AutocompleteSuggestions | null> {
|
|
127
|
+
if (cursorLine !== 0) return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
128
|
+
const currentLine = lines[cursorLine] ?? "";
|
|
129
|
+
const query = extractWindowCompletionQuery(currentLine.slice(0, cursorCol));
|
|
130
|
+
if (query === undefined) return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
131
|
+
const targets = matchingTargets(getTargets(), query);
|
|
132
|
+
if (targets.length === 0) return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
133
|
+
const items = targets.map(completionItem);
|
|
134
|
+
for (let index = 0; index < items.length; index++) {
|
|
135
|
+
suggestedTargets.set(suggestionKey(items[index]!), targets[index]!);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
items,
|
|
139
|
+
prefix: `#${query}`,
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
143
|
+
const selected = suggestedTargets.get(suggestionKey(item));
|
|
144
|
+
const currentLine = lines[cursorLine] ?? "";
|
|
145
|
+
const beforePrefix = currentLine.slice(0, cursorCol - prefix.length);
|
|
146
|
+
if (!selected || !prefix.startsWith("#") || beforePrefix.trim()) {
|
|
147
|
+
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
148
|
+
}
|
|
149
|
+
onSelect(selected);
|
|
150
|
+
const afterCursor = currentLine.slice(cursorCol);
|
|
151
|
+
const separator = afterCursor.length === 0 || !/^\s/.test(afterCursor) ? " " : "";
|
|
152
|
+
const nextLine = `${beforePrefix}${item.value}${separator}${afterCursor}`;
|
|
153
|
+
const nextLines = [...lines];
|
|
154
|
+
nextLines[cursorLine] = nextLine;
|
|
155
|
+
return {
|
|
156
|
+
lines: nextLines,
|
|
157
|
+
cursorLine,
|
|
158
|
+
cursorCol: beforePrefix.length + item.value.length + separator.length,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
162
|
+
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function resolveWindowRouteInput(
|
|
168
|
+
text: string,
|
|
169
|
+
targets: readonly WindowAutocompleteTarget[],
|
|
170
|
+
selectedTargets: ReadonlyMap<string, WindowAutocompleteTarget> = new Map(),
|
|
171
|
+
): WindowRouteResolution | undefined {
|
|
172
|
+
const match = /^[ \t]*#([^\s#]+)(?:\s+([\s\S]*))?$/.exec(text);
|
|
173
|
+
if (!match) return undefined;
|
|
174
|
+
const rawToken = match[1]!;
|
|
175
|
+
const token = rawToken.toLocaleLowerCase("en");
|
|
176
|
+
const message = (match[2] ?? "").trim();
|
|
177
|
+
const selected = selectedTargets.get(token);
|
|
178
|
+
if (selected) {
|
|
179
|
+
const live = targets.filter((candidate) =>
|
|
180
|
+
candidate.routeSelector === selected.routeSelector
|
|
181
|
+
&& candidate.ownerId === selected.ownerId
|
|
182
|
+
&& candidate.ownerNonce === selected.ownerNonce
|
|
183
|
+
);
|
|
184
|
+
return live.length === 1
|
|
185
|
+
? { code: "resolved", token, target: live[0]!, message }
|
|
186
|
+
: { code: "stale", token: rawToken, message };
|
|
187
|
+
}
|
|
188
|
+
const matches = targets.filter((candidate) => candidate.token.toLocaleLowerCase("en") === token);
|
|
189
|
+
if (matches.length > 1) return { code: "ambiguous", token: rawToken, message };
|
|
190
|
+
if (matches.length === 0) return undefined;
|
|
191
|
+
return { code: "resolved", token, target: matches[0]!, message };
|
|
192
|
+
}
|
package/src/window-bar.ts
CHANGED
|
@@ -3,18 +3,19 @@ import { truncateToWidth, type TUI } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import type { CockpitEndpoint } from "./endpoint-store.ts";
|
|
4
4
|
import { isMonitorControlEndpoint } from "./endpoint-store.ts";
|
|
5
5
|
import type { SessionUiState } from "./session-ui-state.ts";
|
|
6
|
-
import { assignedAgentColor, renderSessionBarLine } from "./agent-bar.ts";
|
|
6
|
+
import { assignedAgentColor, renderSessionBarLine, type SessionBarHintValue } from "./agent-bar.ts";
|
|
7
7
|
import { tuiT } from "./tui-i18n.ts";
|
|
8
8
|
import { formatUnreadCount, renderSessionTabLine, type SessionTab } from "./session-tabs.ts";
|
|
9
|
+
import { ownerDisplayToken } from "./window-autocomplete.ts";
|
|
9
10
|
|
|
10
11
|
export interface WindowBarDeps {
|
|
11
12
|
getWindows: () => readonly CockpitEndpoint[];
|
|
12
13
|
getState: () => SessionUiState;
|
|
13
|
-
getShortcutHint?: () =>
|
|
14
|
+
getShortcutHint?: () => SessionBarHintValue | undefined;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface WindowBarRenderOptions {
|
|
17
|
-
shortcutHint?:
|
|
18
|
+
shortcutHint?: SessionBarHintValue;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
interface WindowTab extends SessionTab {
|
|
@@ -36,7 +37,8 @@ function renderTab(tab: WindowTab, selected: boolean, theme: Theme): string {
|
|
|
36
37
|
? theme.fg("accent", "◆")
|
|
37
38
|
: theme.fg("muted", "○");
|
|
38
39
|
const unread = (tab.unread ?? 0) > 0 ? theme.fg("warning", ` ${formatUnreadCount(tab.unread ?? 0)}`) : "";
|
|
39
|
-
const
|
|
40
|
+
const ownerId = tab.endpoint.registryEndpoint?.ownerId;
|
|
41
|
+
const label = `#${ownerId ? ownerDisplayToken(tab.label, ownerId) : tab.label}`;
|
|
40
42
|
return selected
|
|
41
43
|
? `${marker} ${theme.fg(color, theme.bold(`▸ ${label}`))}${unread}`
|
|
42
44
|
: `${marker} ${theme.fg(color, label)}${unread}`;
|