pi-cockpit 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/package.json +85 -85
- package/src/agent-overlay.ts +12 -2
- package/src/agents-store.ts +11 -1
- package/src/bash-bg-overlay.ts +3 -0
- package/src/claude-editor.ts +158 -10
- package/src/config.ts +6 -1
- package/src/editor/input-history-store.ts +173 -0
- package/src/index.ts +214 -17
- package/src/render.ts +9 -2
- package/src/settings/cockpit-provider.ts +22 -2
- package/src/settings-view.ts +13 -0
- package/src/sidebar-render.ts +5 -2
- package/src/tui-i18n.ts +729 -617
- package/src/types.ts +300 -275
- package/src/viewport-stability.ts +173 -99
- package/src/zen-browse.ts +178 -0
- package/src/zen-render.ts +499 -0
- package/src/zen-sheet.ts +254 -0
- package/themes/cockpit-zen.json +24 -0
package/README.md
CHANGED
|
@@ -66,6 +66,7 @@ Cockpit has no package dependency on `pi-maestro-flow`. It observes optional pro
|
|
|
66
66
|
"agentsMode": "list",
|
|
67
67
|
"todoMode": "list",
|
|
68
68
|
"todoExpanded": false,
|
|
69
|
+
"stackStyle": "classic",
|
|
69
70
|
"hideNativeAgents": true,
|
|
70
71
|
"icons": { "mode": "auto" },
|
|
71
72
|
"sidebar": {
|
|
@@ -91,6 +92,7 @@ Cockpit has no package dependency on `pi-maestro-flow`. It observes optional pro
|
|
|
91
92
|
- `quietSymbols`: `"check"` (✓/✗/⋯) or `"dot"` (●/○/◌) lifecycle glyphs for quiet tool rows.
|
|
92
93
|
- `agentsMode` / `todoMode`: `"list"` or `"compact"`.
|
|
93
94
|
- `todoExpanded`: when `true`, expands the todo widget by default.
|
|
95
|
+
- `stackStyle`: `"classic"` keeps the separate Todo/Agent widgets; `"zen"` projects MISSION, WORK, and ACTORS into one borderless stack. The default remains `"classic"`.
|
|
94
96
|
- `hideNativeAgents`: when `true`, clears the teammate extension's own `teammate-agents` widget (it draws a similar list *below* the editor) so the two don't duplicate. On by default.
|
|
95
97
|
- `sidebar.mode`: `"auto"` or `"on"` enables the dock when at least 72 main columns plus 32 sidebar columns fit; `"off"` always uses widgets.
|
|
96
98
|
- `sidebar.width`: persisted dock width, rounded and clamped to `32..56`; default `40`.
|
|
@@ -114,11 +116,12 @@ The tab title is `frame + pi - <session> - <working state>`. The session part fo
|
|
|
114
116
|
- `/cockpit` — opens the settings overlay.
|
|
115
117
|
- `/maestro-settings` — opens the unified settings shell (Cockpit, Flow, Teammate and integrations).
|
|
116
118
|
- `Alt+J` — opens the background Bash jobs overlay (live status, command, cwd, duration, output tail).
|
|
119
|
+
- `Alt+L` — browses the visible Cockpit surface. In Zen widget mode, arrows or `j/k` select rows, first Enter expands inline details, second Enter opens the entity sheet/overlay, and Esc steps back. In the Agent overlay, `m` makes the selected agent the editor input target.
|
|
117
120
|
- `/cockpit sidebar` — reports the current sidebar mode, width, and density.
|
|
118
121
|
- `/cockpit sidebar auto|on|off` — selects dock behavior.
|
|
119
122
|
- `/cockpit sidebar resize` or `Ctrl+Shift+R` — enters temporary Resize mode. Left/Right adjusts one column, Shift+Left/Shift+Right adjusts four, Enter accepts, and Escape rolls back. Mouse reporting is active only during Resize mode.
|
|
120
123
|
- `/cockpit quiet` — toggles quiet mode; `/cockpit bg` shows background jobs.
|
|
121
|
-
- `/theme` — switch theme with live preview; `/theme <name>` applies directly. Pi ships no standalone theme command; cockpit provides one.
|
|
124
|
+
- `/theme` — switch theme with live preview; `/theme <name>` applies directly. `cockpit-zen` is the restrained warm-gold theme designed for the Zen stack; selecting it does not change `stackStyle`. Pi ships no standalone theme command; cockpit provides one.
|
|
122
125
|
|
|
123
126
|
## Sidebar compatibility
|
|
124
127
|
|
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/agent-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/agent-scroll.test.ts tests/session-bar.test.ts tests/session-detail.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/input-routing.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/settings-view.test.ts tests/thinking-fold.test.ts tests/thinking-timer.test.ts tests/tick-policy.test.ts tests/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts tests/model-picker.test.ts tests/title-gen.test.ts tests/title-llm.test.ts tests/settings-registry.test.ts tests/settings-coordinator.test.ts tests/settings-locale.test.ts tests/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",
|
|
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.1.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": "^1.6.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.83.0",
|
|
71
|
-
"@earendil-works/pi-ai": "0.83.0",
|
|
72
|
-
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
73
|
-
"@earendil-works/pi-tui": "0.83.0",
|
|
74
|
-
"@types/node": "^22.0.0",
|
|
75
|
-
"pi-maestro-teammate": "1.
|
|
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.16.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/agent-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/agent-scroll.test.ts tests/session-bar.test.ts tests/session-detail.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/input-routing.test.ts tests/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",
|
|
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.1.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": "^1.6.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.83.0",
|
|
71
|
+
"@earendil-works/pi-ai": "0.83.0",
|
|
72
|
+
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
73
|
+
"@earendil-works/pi-tui": "0.83.0",
|
|
74
|
+
"@types/node": "^22.0.0",
|
|
75
|
+
"pi-maestro-teammate": "1.13.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-overlay.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
truncateToWidth,
|
|
10
10
|
visibleWidth,
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
|
-
import { effectiveAgentStatus, type AgentDisplayStatus } from "./agents-store.ts";
|
|
12
|
+
import { effectiveAgentStatus, isExpertLeader, type AgentDisplayStatus } from "./agents-store.ts";
|
|
13
13
|
import { scrollBy, type AgentScrollState } from "./agent-scroll.ts";
|
|
14
14
|
import type { IconGlyphs } from "./icons.ts";
|
|
15
15
|
import { visibleStart } from "./layout.ts";
|
|
@@ -28,6 +28,8 @@ export interface AgentOverlayParams {
|
|
|
28
28
|
getAgents: () => readonly AgentRow[];
|
|
29
29
|
getViewingId: () => string | undefined;
|
|
30
30
|
onSelect: (correlationId: string) => void;
|
|
31
|
+
/** Set the selected agent as the editor input target without closing the overlay. */
|
|
32
|
+
onTarget?: (correlationId: string) => void;
|
|
31
33
|
onCommand: (correlationId: string, action: "interrupt" | "steer", message?: string) => void;
|
|
32
34
|
requestRender: () => void;
|
|
33
35
|
close: () => void;
|
|
@@ -289,6 +291,11 @@ export class AgentOverlay implements Component, Focusable {
|
|
|
289
291
|
this.scrollOutput(Math.max(1, this.detailRows - 2));
|
|
290
292
|
return;
|
|
291
293
|
}
|
|
294
|
+
if (commandInput === "m" && this.selectedId && this.params.onTarget) {
|
|
295
|
+
this.params.onTarget(this.selectedId);
|
|
296
|
+
this.ack(tuiT("overlay.agents.targetAck", { agent: this.agentName(this.selectedId) }));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
292
299
|
if (commandInput === "i" && this.selectedId) {
|
|
293
300
|
// Interrupt (打断): abort the current turn/tool; the agent stays alive
|
|
294
301
|
// and is told to report and continue.
|
|
@@ -444,8 +451,11 @@ export class AgentOverlay implements Component, Focusable {
|
|
|
444
451
|
const visual = agentVisual(status, this.params.glyphs);
|
|
445
452
|
const marker = row.correlationId === this.selectedId ? this.params.glyphs.selectMarker : " ";
|
|
446
453
|
const task = row.task ? ` · ${row.task}` : "";
|
|
454
|
+
const expertTag = isExpertLeader(row)
|
|
455
|
+
? `${this.params.theme.fg("accent", tuiT("widget.agent.expert"))} `
|
|
456
|
+
: "";
|
|
447
457
|
return fit(
|
|
448
|
-
`${prefix}${marker} ${this.params.theme.fg(visual.color, visual.glyph)} ${this.params.theme.bold(agentLabel(row))}${task}`,
|
|
458
|
+
`${prefix}${marker} ${this.params.theme.fg(visual.color, visual.glyph)} ${expertTag}${this.params.theme.bold(agentLabel(row))}${task}`,
|
|
449
459
|
width,
|
|
450
460
|
);
|
|
451
461
|
});
|
package/src/agents-store.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
} from "pi-maestro-teammate/v1/events";
|
|
6
6
|
import type { AgentProgressSnapshot } from "pi-maestro-teammate/v1/types";
|
|
7
7
|
import { sanitizeExtensionStatusText } from "./extension-status.ts";
|
|
8
|
-
import type
|
|
8
|
+
import { EXPERT_LEADER_NAME, type AgentRow, type AgentStatus } from "./types.ts";
|
|
9
9
|
|
|
10
10
|
const STATUS_TEXT_MAX = 48;
|
|
11
11
|
const TEAMMATE_STALL_TIMEOUT_MS = 30_000;
|
|
@@ -99,6 +99,16 @@ function deriveRole(agent: string | undefined, name: string | undefined): string
|
|
|
99
99
|
return clean(name) || "agent";
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* True when the row is the workflow Leader of an expert-mode dispatch
|
|
104
|
+
* (pi-maestro-teammate EXPERT_MODE_LEADER_NAME). The Leader keeps the same
|
|
105
|
+
* tree + dependency-arrow presentation as parallel/DAG runs; this only adds
|
|
106
|
+
* its strategy marker.
|
|
107
|
+
*/
|
|
108
|
+
export function isExpertLeader(row: { name: string | undefined }): boolean {
|
|
109
|
+
return row.name === EXPERT_LEADER_NAME;
|
|
110
|
+
}
|
|
111
|
+
|
|
102
112
|
export function mapAgentStatus(status: unknown): AgentStatus {
|
|
103
113
|
switch (status) {
|
|
104
114
|
case "pending":
|
package/src/bash-bg-overlay.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface BashBgOverlayParams {
|
|
|
25
25
|
close: () => void;
|
|
26
26
|
theme: Theme;
|
|
27
27
|
glyphs: IconGlyphs;
|
|
28
|
+
/** Select this job when the overlay first renders. */
|
|
29
|
+
initialJobId?: string;
|
|
28
30
|
/** Wall-clock snapshot supplied by the owner and advanced through tick(). */
|
|
29
31
|
now: number;
|
|
30
32
|
/** Live terminal height, so the card can use the space it already reserves. */
|
|
@@ -57,6 +59,7 @@ export class BashBgOverlay implements Component, Focusable {
|
|
|
57
59
|
constructor(private readonly params: BashBgOverlayParams) {
|
|
58
60
|
this.now = params.now;
|
|
59
61
|
this.liveTickKey = this.tickKey(params.now);
|
|
62
|
+
this.selectedId = params.initialJobId;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
invalidate(): void {}
|
package/src/claude-editor.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CustomEditor, type KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { isKeyRelease, isKeyRepeat, matchesKey, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
|
|
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 { tuiT } from "./tui-i18n.ts";
|
|
4
5
|
|
|
5
6
|
/** Invisible editor start marker consumed by the fullscreen controller (fullscreenInput only). */
|
|
6
7
|
export const EDITOR_START_SENTINEL = "\u2063\u2064\u2063\u2064cockpit:editor:start";
|
|
@@ -9,6 +10,15 @@ export const EDITOR_END_SENTINEL = "\u2063\u2064\u2063\u2064cockpit:editor:end";
|
|
|
9
10
|
|
|
10
11
|
export const DEFAULT_DOUBLE_ESCAPE_WINDOW_MS = 500;
|
|
11
12
|
|
|
13
|
+
/** Cursor state when the user is typing rather than browsing history. */
|
|
14
|
+
const NOT_BROWSING = -1;
|
|
15
|
+
|
|
16
|
+
export interface CockpitEditorRouteTarget {
|
|
17
|
+
label: string;
|
|
18
|
+
sigil?: "@" | "#";
|
|
19
|
+
paint: (text: string) => string;
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
export interface CockpitClaudeEditorOptions {
|
|
13
23
|
/** Enable the double bare-Escape clear-input state machine. */
|
|
14
24
|
doubleEscapeClearInput: boolean;
|
|
@@ -22,6 +32,14 @@ export interface CockpitClaudeEditorOptions {
|
|
|
22
32
|
onError?: (error: unknown) => void;
|
|
23
33
|
/** Double-Escape window in ms (default 500). */
|
|
24
34
|
doubleEscapeWindowMs?: number;
|
|
35
|
+
/** Persistent prompt history, newest first. Read on every keystroke. */
|
|
36
|
+
getEntries?: () => readonly string[];
|
|
37
|
+
/** Called for every submitted prompt (including slash commands and `!` lines). */
|
|
38
|
+
record?: (text: string) => void;
|
|
39
|
+
/** Immutable route prefix painted inside the editor, never included in text. */
|
|
40
|
+
getRouteTarget?: () => CockpitEditorRouteTarget | undefined;
|
|
41
|
+
/** Called with the constructed editor so the host can push route updates. */
|
|
42
|
+
onEditor?: (editor: CockpitClaudeEditor) => void;
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
export interface DoubleEscapeDecision {
|
|
@@ -85,13 +103,18 @@ export class DoubleEscapeGate {
|
|
|
85
103
|
}
|
|
86
104
|
|
|
87
105
|
/**
|
|
88
|
-
* Cockpit's CustomEditor: owns the double-Escape state machine
|
|
89
|
-
* markers the fullscreen controller uses
|
|
90
|
-
* Installed at session start
|
|
106
|
+
* Cockpit's unified CustomEditor: owns the double-Escape state machine, the
|
|
107
|
+
* editor markers the fullscreen controller uses, the persistent ↑/↓ prompt
|
|
108
|
+
* history, and the input route-target prefix. Installed at session start
|
|
109
|
+
* (reload-gated) via createCockpitClaudeEditorFactory.
|
|
91
110
|
*/
|
|
92
111
|
export class CockpitClaudeEditor extends CustomEditor {
|
|
93
112
|
private readonly editorOptions: CockpitClaudeEditorOptions;
|
|
94
113
|
private readonly doubleEscapeGate = new DoubleEscapeGate();
|
|
114
|
+
/** History browsing cursor: NOT_BROWSING while typing, else an index into getEntries(). */
|
|
115
|
+
private index = NOT_BROWSING;
|
|
116
|
+
/** What the user had typed before browsing started, restored on the way back down. */
|
|
117
|
+
private draft = "";
|
|
95
118
|
|
|
96
119
|
constructor(
|
|
97
120
|
tui: TUI,
|
|
@@ -102,17 +125,39 @@ export class CockpitClaudeEditor extends CustomEditor {
|
|
|
102
125
|
super(tui, theme, keybindings);
|
|
103
126
|
this.editorOptions = options;
|
|
104
127
|
if (options.doubleEscapeWindowMs !== undefined) this.doubleEscapeGate.windowMs = options.doubleEscapeWindowMs;
|
|
128
|
+
options.onEditor?.(this);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Submitted prompts go to the persistent store when enabled, else to the base in-memory history. */
|
|
132
|
+
override addToHistory(text: string): void {
|
|
133
|
+
if (this.editorOptions.record) this.editorOptions.record(text);
|
|
134
|
+
else super.addToHistory(text);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Programmatic writes (submit clears, `ui.setEditorText`) end history browsing. */
|
|
138
|
+
override setText(text: string): void {
|
|
139
|
+
this.index = NOT_BROWSING;
|
|
140
|
+
super.setText(text);
|
|
105
141
|
}
|
|
106
142
|
|
|
107
143
|
override render(width: number): string[] {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
144
|
+
const target = this.editorOptions.getRouteTarget?.();
|
|
145
|
+
const lines = target ? this.renderWithRouteTarget(width, target) : super.render(width);
|
|
146
|
+
const total = this.editorOptions.getEntries?.().length ?? 0;
|
|
147
|
+
const banner = this.browsing() && total > 0
|
|
148
|
+
? historyBanner(this.index + 1, total, width, this.getPaddingX(), this.borderColor)
|
|
149
|
+
: undefined;
|
|
150
|
+
if (!this.editorOptions.emitEditorMarkers) {
|
|
151
|
+
return banner ? [...lines, banner] : lines;
|
|
152
|
+
}
|
|
153
|
+
// The fullscreen controller treats everything between the sentinels as the
|
|
154
|
+
// editor region, so the banner must sit before the END marker to stay visible.
|
|
155
|
+
return [EDITOR_START_SENTINEL, ...lines, ...(banner ? [banner] : []), EDITOR_END_SENTINEL];
|
|
111
156
|
}
|
|
112
157
|
|
|
113
158
|
override handleInput(data: string): void {
|
|
114
|
-
const enabled = this.editorOptions.doubleEscapeClearInput;
|
|
115
159
|
if (this.isBareEscape(data)) {
|
|
160
|
+
const enabled = this.editorOptions.doubleEscapeClearInput;
|
|
116
161
|
if (enabled) {
|
|
117
162
|
const probe = readAutocompleteState(this);
|
|
118
163
|
const autocomplete: DoubleEscapeGateInput["autocomplete"] = probe.unknown
|
|
@@ -133,8 +178,30 @@ export class CockpitClaudeEditor extends CustomEditor {
|
|
|
133
178
|
super.handleInput(data);
|
|
134
179
|
return;
|
|
135
180
|
}
|
|
136
|
-
if (
|
|
181
|
+
if (this.editorOptions.doubleEscapeClearInput) this.doubleEscapeGate.onAnyOtherInput();
|
|
182
|
+
const keys = getKeybindings();
|
|
183
|
+
// History browsing only exists when persistent history is enabled; without
|
|
184
|
+
// getEntries, Up/Down keep their plain cursor-move meaning.
|
|
185
|
+
if (this.editorOptions.getEntries) {
|
|
186
|
+
if (keys.matches(data, "tui.editor.cursorUp") && this.canBrowseOlder()) {
|
|
187
|
+
this.step(1);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (keys.matches(data, "tui.editor.cursorDown") && this.canBrowseNewer()) {
|
|
191
|
+
this.step(-1);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
137
195
|
super.handleInput(data);
|
|
196
|
+
// Editing the recalled text means the user left history behind.
|
|
197
|
+
if (this.browsing() && this.getText() !== this.editorOptions.getEntries?.()[this.index]) {
|
|
198
|
+
this.index = NOT_BROWSING;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Repaint after a cross-extension input-target change. */
|
|
203
|
+
refreshRouteTarget(): void {
|
|
204
|
+
this.tui.requestRender();
|
|
138
205
|
}
|
|
139
206
|
|
|
140
207
|
private isBareEscape(data: string): boolean {
|
|
@@ -149,9 +216,90 @@ export class CockpitClaudeEditor extends CustomEditor {
|
|
|
149
216
|
this.editorOptions.onError?.(error);
|
|
150
217
|
}
|
|
151
218
|
}
|
|
219
|
+
|
|
220
|
+
private renderWithRouteTarget(width: number, target: CockpitEditorRouteTarget): string[] {
|
|
221
|
+
const paddingX = Math.min(this.getPaddingX(), Math.max(0, Math.floor((width - 1) / 2)));
|
|
222
|
+
const contentWidth = Math.max(1, width - paddingX * 2);
|
|
223
|
+
const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1));
|
|
224
|
+
const token = truncateToWidth(`${target.sigil ?? "@"}${target.label}:`, Math.max(1, layoutWidth - 1), "…");
|
|
225
|
+
const injected = `${token}${visibleWidth(token) < layoutWidth ? " " : ""}`;
|
|
226
|
+
|
|
227
|
+
// Editor has no render-prefix API. Temporarily project the immutable target
|
|
228
|
+
// into its layout state so native wrapping and cursor placement stay correct,
|
|
229
|
+
// then restore the real editable state before returning.
|
|
230
|
+
const state = (this as unknown as EditorWithRenderState).state;
|
|
231
|
+
const originalLines = state.lines;
|
|
232
|
+
const originalCursorCol = state.cursorCol;
|
|
233
|
+
state.lines = [`${injected}${originalLines[0] ?? ""}`, ...originalLines.slice(1)];
|
|
234
|
+
if (state.cursorLine === 0) state.cursorCol += injected.length;
|
|
235
|
+
try {
|
|
236
|
+
const rendered = super.render(width);
|
|
237
|
+
const routeLine = rendered.findIndex((line) => line.includes(token));
|
|
238
|
+
if (routeLine >= 0) {
|
|
239
|
+
rendered[routeLine] = rendered[routeLine]!.replace(token, target.paint(token));
|
|
240
|
+
}
|
|
241
|
+
return rendered.map((line) => truncateToWidth(line, Math.max(1, width), ""));
|
|
242
|
+
} finally {
|
|
243
|
+
state.lines = originalLines;
|
|
244
|
+
state.cursorCol = originalCursorCol;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private browsing(): boolean {
|
|
249
|
+
return this.index > NOT_BROWSING;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Mirrors the base editor's own gating: on the first line, and either already
|
|
254
|
+
* browsing or with the cursor at the very start (which includes an empty editor) —
|
|
255
|
+
* anywhere else Up is a plain cursor move.
|
|
256
|
+
*/
|
|
257
|
+
private canBrowseOlder(): boolean {
|
|
258
|
+
if (this.isShowingAutocomplete() || this.getCursor().line !== 0) return false;
|
|
259
|
+
return this.browsing() || this.getCursor().col === 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private canBrowseNewer(): boolean {
|
|
263
|
+
if (!this.browsing() || this.isShowingAutocomplete()) return false;
|
|
264
|
+
return this.getCursor().line === this.getLines().length - 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private step(delta: number): void {
|
|
268
|
+
const entries = this.editorOptions.getEntries?.() ?? [];
|
|
269
|
+
const next = this.index + delta;
|
|
270
|
+
if (next < NOT_BROWSING || next >= entries.length) return;
|
|
271
|
+
if (!this.browsing()) this.draft = this.getText();
|
|
272
|
+
// super.setText, so the index we are about to record survives.
|
|
273
|
+
super.setText(next === NOT_BROWSING ? this.draft : entries[next] ?? "");
|
|
274
|
+
this.index = next;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Compact history position label, indented to the editor's content. */
|
|
279
|
+
export function historyBanner(
|
|
280
|
+
position: number,
|
|
281
|
+
total: number,
|
|
282
|
+
width: number,
|
|
283
|
+
paddingX: number,
|
|
284
|
+
paint: (value: string) => string,
|
|
285
|
+
): string {
|
|
286
|
+
const indent = " ".repeat(Math.max(0, paddingX));
|
|
287
|
+
const inner = Math.max(1, width - indent.length * 2);
|
|
288
|
+
const label = tuiT("editor.historyBanner", { position, total });
|
|
289
|
+
return indent + paint(truncateToWidth(label, inner, "…"));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface EditorRenderState {
|
|
293
|
+
lines: string[];
|
|
294
|
+
cursorLine: number;
|
|
295
|
+
cursorCol: number;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
interface EditorWithRenderState {
|
|
299
|
+
state: EditorRenderState;
|
|
152
300
|
}
|
|
153
301
|
|
|
154
|
-
/** Build the editor factory Cockpit installs
|
|
302
|
+
/** Build the editor factory Cockpit installs for its gated editor features. */
|
|
155
303
|
export function createCockpitClaudeEditorFactory(options: CockpitClaudeEditorOptions) {
|
|
156
304
|
const factory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) =>
|
|
157
305
|
new CockpitClaudeEditor(tui, theme, keybindings, options);
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { type CockpitConfig, type CurrencyMode, DEFAULT_CONFIG } from "./types.ts";
|
|
4
|
+
import { type CockpitConfig, type CurrencyMode, DEFAULT_CONFIG, type StackStyle } from "./types.ts";
|
|
5
5
|
|
|
6
6
|
const SIDEBAR_MIN_WIDTH = 32;
|
|
7
7
|
const SIDEBAR_MAX_WIDTH = 56;
|
|
@@ -19,6 +19,7 @@ export function mergeConfig(base: CockpitConfig, over: unknown): CockpitConfig {
|
|
|
19
19
|
const isToolPaletteMode = (v: unknown): v is "classic" | "family" | "readwrite" | "search" | "mono" =>
|
|
20
20
|
v === "classic" || v === "family" || v === "readwrite" || v === "search" || v === "mono";
|
|
21
21
|
const isIconMode = (v: unknown): v is "auto" | "nerd" | "ascii" => v === "auto" || v === "nerd" || v === "ascii";
|
|
22
|
+
const isStackStyle = (v: unknown): v is StackStyle => v === "classic" || v === "zen";
|
|
22
23
|
const isCurrencyMode = (v: unknown): v is CurrencyMode => v === "usd" || v === "cny";
|
|
23
24
|
const isSidebarMode = (v: unknown): v is "auto" | "on" | "off" => v === "auto" || v === "on" || v === "off";
|
|
24
25
|
const isSidebarDensity = (v: unknown): v is "comfortable" | "compact" => v === "comfortable" || v === "compact";
|
|
@@ -47,11 +48,13 @@ const isCurrencyMode = (v: unknown): v is CurrencyMode => v === "usd" || v === "
|
|
|
47
48
|
agentsMode: isMode(o.agentsMode) ? o.agentsMode : base.agentsMode,
|
|
48
49
|
todoMode: isMode(o.todoMode) ? o.todoMode : base.todoMode,
|
|
49
50
|
todoExpanded: typeof o.todoExpanded === "boolean" ? o.todoExpanded : base.todoExpanded,
|
|
51
|
+
stackStyle: isStackStyle(o.stackStyle) ? o.stackStyle : base.stackStyle,
|
|
50
52
|
hideNativeAgents: typeof o.hideNativeAgents === "boolean" ? o.hideNativeAgents : base.hideNativeAgents,
|
|
51
53
|
pinEditorBottom: typeof o.pinEditorBottom === "boolean" ? o.pinEditorBottom : base.pinEditorBottom,
|
|
52
54
|
doubleEscapeClearInput: typeof o.doubleEscapeClearInput === "boolean" ? o.doubleEscapeClearInput : base.doubleEscapeClearInput,
|
|
53
55
|
fullscreenInput: typeof o.fullscreenInput === "boolean" ? o.fullscreenInput : base.fullscreenInput,
|
|
54
56
|
copyOnSelect: typeof o.copyOnSelect === "boolean" ? o.copyOnSelect : base.copyOnSelect,
|
|
57
|
+
historyEnabled: typeof o.historyEnabled === "boolean" ? o.historyEnabled : base.historyEnabled,
|
|
55
58
|
currency: isCurrencyMode(o.currency) ? o.currency : base.currency,
|
|
56
59
|
currencyRate: typeof o.currencyRate === "number" && Number.isFinite(o.currencyRate) && o.currencyRate > 0
|
|
57
60
|
? Math.min(100, Math.max(0.01, Math.round(o.currencyRate * 100) / 100))
|
|
@@ -133,11 +136,13 @@ export function mergeConfigDocument(raw: unknown, config: CockpitConfig): Record
|
|
|
133
136
|
agentsMode: config.agentsMode,
|
|
134
137
|
todoMode: config.todoMode,
|
|
135
138
|
todoExpanded: config.todoExpanded,
|
|
139
|
+
stackStyle: config.stackStyle,
|
|
136
140
|
hideNativeAgents: config.hideNativeAgents,
|
|
137
141
|
pinEditorBottom: config.pinEditorBottom,
|
|
138
142
|
doubleEscapeClearInput: config.doubleEscapeClearInput,
|
|
139
143
|
fullscreenInput: config.fullscreenInput,
|
|
140
144
|
copyOnSelect: config.copyOnSelect,
|
|
145
|
+
historyEnabled: config.historyEnabled,
|
|
141
146
|
currency: config.currency,
|
|
142
147
|
currencyRate: config.currencyRate,
|
|
143
148
|
icons,
|