pi-cockpit 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cockpit",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Responsive Maestro operations sidebar, fallback status widgets, and footer for Pi.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -18,7 +18,7 @@
18
18
  "README.md"
19
19
  ],
20
20
  "scripts": {
21
- "test": "node --test --experimental-transform-types tests/config.test.ts tests/public-events.test.ts tests/maestro-store.test.ts tests/split-pane.test.ts tests/editor-bottom.test.ts tests/sidebar-render.test.ts tests/sidebar-controller.test.ts tests/viewport-stability.test.ts tests/quiet-tools.test.ts tests/edit-guard.test.ts tests/agents-store.test.ts tests/endpoint-store.test.ts tests/session-ui-state.test.ts tests/session-tabs.test.ts tests/agent-bar.test.ts tests/supervision-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/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",
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/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
22
  "typecheck": "tsc --noEmit",
23
23
  "prepack": "node ../../scripts/bundle-settings-core.mjs",
24
24
  "postpack": "node ../../scripts/bundle-settings-core.mjs --clean"
@@ -72,7 +72,7 @@
72
72
  "@earendil-works/pi-coding-agent": "0.83.0",
73
73
  "@earendil-works/pi-tui": "0.83.0",
74
74
  "@types/node": "^22.0.0",
75
- "pi-maestro-teammate": "1.12.0",
75
+ "pi-maestro-teammate": "1.13.0",
76
76
  "typebox": "1.3.7",
77
77
  "typescript": "^5.5.0"
78
78
  },
@@ -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
  });
@@ -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 { AgentRow, AgentStatus } from "./types.ts";
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":
@@ -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/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,6 +48,7 @@ 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,
@@ -133,6 +135,7 @@ export function mergeConfigDocument(raw: unknown, config: CockpitConfig): Record
133
135
  agentsMode: config.agentsMode,
134
136
  todoMode: config.todoMode,
135
137
  todoExpanded: config.todoExpanded,
138
+ stackStyle: config.stackStyle,
136
139
  hideNativeAgents: config.hideNativeAgents,
137
140
  pinEditorBottom: config.pinEditorBottom,
138
141
  doubleEscapeClearInput: config.doubleEscapeClearInput,
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@ import { SupervisionStore } from "./supervision-store.ts";
13
13
  import { MaestroStore } from "./maestro-store.ts";
14
14
  import { createSidebarController, type SidebarController } from "./sidebar-controller.ts";
15
15
  import { COCKPIT_SPLIT_PANE_MARKER } from "./split-pane.ts";
16
- import { attachViewportStability } from "./viewport-stability.ts";
16
+ import { attachViewportStability, type ViewportStabilityPatch } from "./viewport-stability.ts";
17
17
  import {
18
18
  COCKPIT_EDITOR_BOTTOM_MARKER,
19
19
  EDITOR_BOTTOM_WIDGET_KEY,
@@ -54,7 +54,17 @@ import { SessionUiState } from "./session-ui-state.ts";
54
54
  import { nextSessionTabId } from "./session-tabs.ts";
55
55
  import { renderWindowBar, windowSessionColor } from "./window-bar.ts";
56
56
  import { makeWindowThreadWidget } from "./window-thread-view.ts";
57
- import { agentPanelRows } from "./viewport.ts";
57
+ import { agentPanelRows, panelRows } from "./viewport.ts";
58
+ import { createZenBrowseController, type ZenBrowseController } from "./zen-browse.ts";
59
+ import { enumerateZenNavRows, renderZenStack } from "./zen-render.ts";
60
+ import {
61
+ ZenSheet,
62
+ buildZenMissionSheet,
63
+ buildZenRunSheet,
64
+ buildZenSwarmSheet,
65
+ buildZenTaskSheet,
66
+ type ZenSheetDocument,
67
+ } from "./zen-sheet.ts";
58
68
  import { routeAgentInput } from "./input-routing.ts";
59
69
  import { activeThemeName, ThemePicker } from "./theme-picker.ts";
60
70
  import { ModelPicker, type ModelPickerEntry } from "./model-picker.ts";
@@ -323,6 +333,7 @@ export default function (pi: ExtensionAPI): void {
323
333
  // Independent of the split-pane attach: the sidebar can be off while the
324
334
  // thinking label / teammate tree still stream above the visible viewport.
325
335
  let stabilityTui: TUI | undefined;
336
+ let viewportStabilityPatch: ViewportStabilityPatch | undefined;
326
337
  // True while Cockpit owns a capturing overlay (bash jobs, theme picker,
327
338
  // settings panel). Split-pane resize must yield to the overlay's focus and
328
339
  // refuse to start while one is open; otherwise the resize listener — a
@@ -331,6 +342,7 @@ export default function (pi: ExtensionAPI): void {
331
342
  /** Extra repaint target while the Agent modal is consuming the live store. */
332
343
  let activeAgentOverlayRender: (() => void) | undefined;
333
344
  let activeAgentOverlay: { finalize(): void } | undefined;
345
+ let activeZenSheet: { finalize(): void } | undefined;
334
346
  let activeBashBgOverlay: BashBgOverlay | undefined;
335
347
  /** Agent activity temporarily wins vertical space without rewriting Todo preference. */
336
348
  let agentPriorityActive = false;
@@ -340,6 +352,8 @@ export default function (pi: ExtensionAPI): void {
340
352
  // component.dispose() (MW-2).
341
353
  let activeSettingsOverlay: { finalize(): void } | undefined;
342
354
  let sidebarController: SidebarController | undefined;
355
+ let zenBrowseController: ZenBrowseController | undefined;
356
+ let zenNavRows: string[] = [];
343
357
  let editorBottomController: EditorBottomController | undefined;
344
358
  // Whether Cockpit installed its custom editor this session (reload-gated).
345
359
  // Tracks ownership so teardown restores the default editor only when ours.
@@ -676,14 +690,17 @@ export default function (pi: ExtensionAPI): void {
676
690
 
677
691
  const ensureViewportStability = (tui: TUI): void => {
678
692
  if (stabilityTui === tui) return;
693
+ viewportStabilityPatch?.detach();
694
+ viewportStabilityPatch = attachViewportStability(tui);
679
695
  stabilityTui = tui;
680
- attachViewportStability(tui);
681
696
  };
682
697
 
683
698
  const sessionListOverlayActive = (): boolean =>
684
699
  capturingOverlayActive || ambientKeysShouldYield(capturedTui);
685
700
 
686
701
  const clearWidgets = (ctx: ExtensionContext): void => {
702
+ zenBrowseController?.end();
703
+ zenNavRows = [];
687
704
  ctx.ui.setWidget(STACK_WIDGET_KEY, undefined);
688
705
  ctx.ui.setWidget(AGENT_WIDGET_KEY, undefined);
689
706
  };
@@ -926,7 +943,10 @@ export default function (pi: ExtensionAPI): void {
926
943
  capturedTui = tui;
927
944
  ensureViewportStability(tui);
928
945
  return makeAgentWidget({
929
- getAgents: () => sessionUi.mode === "window" ? [] : agents.snapshot(),
946
+ // The Zen stack absorbs the roster into its ACTORS section; an empty
947
+ // roster makes makeAgentWidget render zero rows, so the widget slot
948
+ // stays registered (placement bookkeeping) without duplicating rows.
949
+ getAgents: () => config.stackStyle === "zen" || sessionUi.mode === "window" ? [] : agents.snapshot(),
930
950
  getConfig: () => config,
931
951
  isRunning: () => running,
932
952
  isAnimating,
@@ -1019,18 +1039,63 @@ export default function (pi: ExtensionAPI): void {
1019
1039
  );
1020
1040
  };
1021
1041
 
1042
+ const ensureZenBrowseController = (ctx: ExtensionContext): ZenBrowseController => {
1043
+ if (zenBrowseController) return zenBrowseController;
1044
+ zenBrowseController = createZenBrowseController({
1045
+ getNavRows: () => zenNavRows,
1046
+ subscribeInput: (handler) => ctx.ui.onTerminalInput(handler),
1047
+ requestRender: () => req(),
1048
+ onActivate: (id) => {
1049
+ void activateZenRow(ctx, id).catch((error) => {
1050
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
1051
+ });
1052
+ },
1053
+ shouldYield: () => capturingOverlayActive || ambientKeysShouldYield(capturedTui),
1054
+ onWarning: (message) => ctx.ui.notify(message, "warning"),
1055
+ emptyNotice: () => tuiT("notice.sidebarNothing"),
1056
+ });
1057
+ return zenBrowseController;
1058
+ };
1059
+
1022
1060
  const installWidgets = (ctx: ExtensionContext): void => {
1061
+ const browseController = ensureZenBrowseController(ctx);
1023
1062
  ctx.ui.setWidget(
1024
1063
  STACK_WIDGET_KEY,
1025
1064
  (tui, theme) => {
1026
1065
  capturedTui = tui;
1027
1066
  ensureViewportStability(tui);
1028
- return makeTodoWidget({
1067
+ const todoWidget = makeTodoWidget({
1029
1068
  getTodos: () => todos.snapshot(),
1030
1069
  getConfig: () => config,
1031
1070
  getExpanded: effectiveTodoExpanded,
1032
1071
  isAnimating,
1033
1072
  })(tui, theme);
1073
+ // stackStyle is dispatched per render, not per install, so a settings
1074
+ // toggle switches the projection live without re-registering widgets.
1075
+ return {
1076
+ render(width: number): string[] {
1077
+ if (config.stackStyle !== "zen") {
1078
+ zenNavRows = [];
1079
+ return todoWidget.render(width);
1080
+ }
1081
+ const input = {
1082
+ maestro: maestro.snapshot(),
1083
+ todos: todos.snapshot(),
1084
+ agents: sessionUi.mode === "window" ? [] : visibleAgentRows(agents.snapshot()),
1085
+ jobs: bashBg.snapshot(),
1086
+ config: { ...config, todoExpanded: effectiveTodoExpanded() },
1087
+ width,
1088
+ theme,
1089
+ now: Date.now(),
1090
+ maxRows: panelRows(terminalRows(tui)),
1091
+ browse: browseController.state(),
1092
+ };
1093
+ zenNavRows = enumerateZenNavRows(input);
1094
+ return renderZenStack(input);
1095
+ },
1096
+ invalidate(): void { todoWidget.invalidate(); },
1097
+ dispose(): void { todoWidget.dispose(); },
1098
+ };
1034
1099
  },
1035
1100
  { placement: "aboveEditor" },
1036
1101
  );
@@ -1135,6 +1200,9 @@ export default function (pi: ExtensionAPI): void {
1135
1200
  sidebarController?.dispose();
1136
1201
  editorBottomController?.dispose();
1137
1202
  }
1203
+ zenBrowseController?.dispose();
1204
+ zenBrowseController = undefined;
1205
+ zenNavRows = [];
1138
1206
  sidebarController = undefined;
1139
1207
  editorBottomController = undefined;
1140
1208
  fullscreenController = undefined;
@@ -1148,6 +1216,12 @@ export default function (pi: ExtensionAPI): void {
1148
1216
  }
1149
1217
  activeAgentOverlay = undefined;
1150
1218
  activeAgentOverlayRender = undefined;
1219
+ try {
1220
+ activeZenSheet?.finalize();
1221
+ } catch {
1222
+ // best effort
1223
+ }
1224
+ activeZenSheet = undefined;
1151
1225
  dockEffectiveVisible = false;
1152
1226
  clearClaudeEditor(ctx);
1153
1227
  clearFullscreen(ctx);
@@ -1172,6 +1246,9 @@ export default function (pi: ExtensionAPI): void {
1172
1246
  // ambient surfaces are best-effort
1173
1247
  }
1174
1248
  stopTick();
1249
+ viewportStabilityPatch?.detach();
1250
+ viewportStabilityPatch = undefined;
1251
+ stabilityTui = undefined;
1175
1252
  lastPublishedInputTarget = "@main";
1176
1253
  pi.events.emit(COCKPIT_INPUT_TARGET_EVENT, { version: 1 } satisfies CockpitInputTargetV1);
1177
1254
  capturedTui = undefined;
@@ -1500,15 +1577,14 @@ export default function (pi: ExtensionAPI): void {
1500
1577
  }),
1501
1578
  pi.events.on(COCKPIT_PREEMPT_RESIZE_EVENT, () => {
1502
1579
  // A capturing overlay opened by another extension (teammate attach,
1503
- // control center, model mapping) must preempt an active sidebar resize
1504
- // or browse mode: both are global terminal-input hooks that would
1505
- // otherwise swallow the overlay's first arrow/Enter/Esc.
1580
+ // control center, model mapping) must preempt every global input hook.
1506
1581
  try {
1507
1582
  sidebarController?.cancelResize();
1508
1583
  sidebarController?.endFocus();
1509
- } catch {
1510
- // best effort
1511
- }
1584
+ zenBrowseController?.end();
1585
+ } catch {
1586
+ // best effort
1587
+ }
1512
1588
  }),
1513
1589
  );
1514
1590
  };
@@ -1832,6 +1908,7 @@ export default function (pi: ExtensionAPI): void {
1832
1908
 
1833
1909
  const enterCapturingOverlay = (): void => {
1834
1910
  capturingOverlayActive = true;
1911
+ zenBrowseController?.end();
1835
1912
  req();
1836
1913
  try {
1837
1914
  sidebarController?.cancelResize();
@@ -1844,7 +1921,71 @@ export default function (pi: ExtensionAPI): void {
1844
1921
  req();
1845
1922
  };
1846
1923
 
1847
- const openAgentOverlay = async (ctx: ExtensionContext): Promise<void> => {
1924
+ const openZenSheet = async (
1925
+ ctx: ExtensionContext,
1926
+ getDocument: () => ZenSheetDocument | undefined,
1927
+ ): Promise<void> => {
1928
+ if (!ctx.hasUI || capturingOverlayActive) return;
1929
+ let ownedSheet: { finalize(): void } | undefined;
1930
+ enterCapturingOverlay();
1931
+ try {
1932
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => {
1933
+ let settled = false;
1934
+ const finalize = (): void => {
1935
+ if (settled) return;
1936
+ settled = true;
1937
+ done(undefined);
1938
+ };
1939
+ ownedSheet = { finalize };
1940
+ activeZenSheet = ownedSheet;
1941
+ return new ZenSheet({
1942
+ getDocument,
1943
+ requestRender: () => tui.requestRender(),
1944
+ close: finalize,
1945
+ theme,
1946
+ glyphs: resolveGlyphs(config.icons.mode),
1947
+ getTerminalRows: () => terminalRows(tui),
1948
+ });
1949
+ }, {
1950
+ overlay: true,
1951
+ overlayOptions: { anchor: "center", width: "72%", maxHeight: "85%" },
1952
+ });
1953
+ } finally {
1954
+ if (activeZenSheet === ownedSheet) activeZenSheet = undefined;
1955
+ exitCapturingOverlay();
1956
+ }
1957
+ };
1958
+
1959
+ const activateZenRow = async (ctx: ExtensionContext, id: string): Promise<void> => {
1960
+ if (id === "mission") {
1961
+ await openZenSheet(ctx, () => buildZenMissionSheet(maestro.snapshot()));
1962
+ return;
1963
+ }
1964
+ if (id === "run") {
1965
+ await openZenSheet(ctx, () => buildZenRunSheet(maestro.snapshot()));
1966
+ return;
1967
+ }
1968
+ if (id === "swarm") {
1969
+ await openZenSheet(ctx, () => buildZenSwarmSheet(maestro.snapshot()));
1970
+ return;
1971
+ }
1972
+ if (id.startsWith("task:")) {
1973
+ const taskId = id.slice("task:".length);
1974
+ await openZenSheet(ctx, () => buildZenTaskSheet(todos.snapshot().find((item) => item.id === taskId)));
1975
+ return;
1976
+ }
1977
+ if (id.startsWith("agent:")) {
1978
+ const correlationId = id.slice("agent:".length);
1979
+ selectAgent(correlationId);
1980
+ await openAgentOverlay(ctx, correlationId);
1981
+ return;
1982
+ }
1983
+ if (id.startsWith("job:")) {
1984
+ await openBashBgOverlay(ctx, id.slice("job:".length));
1985
+ }
1986
+ };
1987
+
1988
+ const openAgentOverlay = async (ctx: ExtensionContext, initialCorrelationId?: string): Promise<void> => {
1848
1989
  if (!ctx.hasUI) return;
1849
1990
  if (capturingOverlayActive) {
1850
1991
  ctx.ui.notify(tuiT("notice.closeOverlayAgents"), "warning");
@@ -1854,6 +1995,7 @@ export default function (pi: ExtensionAPI): void {
1854
1995
  ctx.ui.notify(tuiT("notice.noAgents"), "info");
1855
1996
  return;
1856
1997
  }
1998
+ if (initialCorrelationId) selectAgent(initialCorrelationId);
1857
1999
  let ownedOverlay: { finalize(): void } | undefined;
1858
2000
  let ownedRender: (() => void) | undefined;
1859
2001
  enterCapturingOverlay();
@@ -1871,8 +2013,9 @@ export default function (pi: ExtensionAPI): void {
1871
2013
  activeAgentOverlayRender = ownedRender;
1872
2014
  return new AgentOverlay({
1873
2015
  getAgents: () => agents.snapshot(),
1874
- getViewingId: selectedAgentCorrelationId,
2016
+ getViewingId: () => initialCorrelationId ?? selectedAgentCorrelationId(),
1875
2017
  onSelect: (correlationId) => selectAgent(correlationId),
2018
+ onTarget: (correlationId) => selectAgent(correlationId),
1876
2019
  onCommand: (correlationId, action, message) => {
1877
2020
  pi.events.emit(TEAMMATE_AGENT_COMMAND_EVENT, { correlationId, action, ...(message !== undefined ? { message } : {}) });
1878
2021
  },
@@ -1955,7 +2098,7 @@ export default function (pi: ExtensionAPI): void {
1955
2098
  });
1956
2099
  });
1957
2100
 
1958
- const openBashBgOverlay = async (ctx: ExtensionContext): Promise<void> => {
2101
+ const openBashBgOverlay = async (ctx: ExtensionContext, initialJobId?: string): Promise<void> => {
1959
2102
  if (!ctx.hasUI) return;
1960
2103
  let ownedOverlay: BashBgOverlay | undefined;
1961
2104
  enterCapturingOverlay();
@@ -1970,6 +2113,7 @@ export default function (pi: ExtensionAPI): void {
1970
2113
  glyphs: resolveGlyphs(config.icons.mode),
1971
2114
  now: Date.now(),
1972
2115
  hideLiveDuration: config.staticMode,
2116
+ ...(initialJobId ? { initialJobId } : {}),
1973
2117
  getTerminalRows: () => terminalRows(tui),
1974
2118
  });
1975
2119
  activeBashBgOverlay = ownedOverlay;
@@ -2145,6 +2289,10 @@ export default function (pi: ExtensionAPI): void {
2145
2289
  else clearEditorBottom(ctx);
2146
2290
  }
2147
2291
  if (changedKeys.some((key) => key.startsWith("sidebar."))) syncSidebarMode(ctx);
2292
+ if (previous.stackStyle !== nextConfig.stackStyle && nextConfig.stackStyle !== "zen") {
2293
+ zenBrowseController?.end();
2294
+ zenNavRows = [];
2295
+ }
2148
2296
  publishUiOwnership();
2149
2297
  req();
2150
2298
  },
@@ -2231,7 +2379,7 @@ export default function (pi: ExtensionAPI): void {
2231
2379
  });
2232
2380
 
2233
2381
  pi.registerShortcut(SIDEBAR_FOCUS_KEY, {
2234
- description: "Browse the Cockpit sidebar with the keyboard (↑↓/j/k/PageUp/PageDown/Home/End, Esc to leave)",
2382
+ description: "Browse the visible Cockpit surface with the keyboard (↑↓/j/k/Home/End, Enter, Esc)",
2235
2383
  handler(ctx) {
2236
2384
  if (!config.enabled) {
2237
2385
  ctx.ui.notify(tuiT("notice.cockpitDisabled"), "warning");
@@ -2241,6 +2389,10 @@ export default function (pi: ExtensionAPI): void {
2241
2389
  ctx.ui.notify(tuiT("notice.closeOverlayBrowse"), "warning");
2242
2390
  return;
2243
2391
  }
2392
+ if (config.stackStyle === "zen" && surfaceState === "widgets") {
2393
+ ensureZenBrowseController(ctx).begin();
2394
+ return;
2395
+ }
2244
2396
  sidebarController?.beginFocus();
2245
2397
  },
2246
2398
  });
package/src/render.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
2
- import { effectiveAgentStatus, type AgentDisplayStatus } from "./agents-store.ts";
2
+ import { effectiveAgentStatus, isExpertLeader, type AgentDisplayStatus } from "./agents-store.ts";
3
3
  import type { AgentRow, TodoItem, ViewMode } from "./types.ts";
4
4
  import type { IconGlyphs } from "./icons.ts";
5
5
  import { composeByPriority, fitLineByPriority, type PriorityGroup, type PrioritizedSegment, type WidthUtils } from "./layout.ts";
@@ -237,7 +237,9 @@ export function renderAgents(
237
237
  + (stalled ? theme.fg("error", `${g.separator}${tuiT("common.stalled", { count: stalled })}`) : "");
238
238
  const tails = rows
239
239
  .filter((r) => r.tail)
240
- .map((r) => `${theme.fg(roleColor(r.role), r.role)}${theme.fg("dim", ":")} ${theme.fg("dim", r.tail)}`);
240
+ .map((r) => isExpertLeader(r)
241
+ ? `${theme.fg("accent", tuiT("widget.agent.expert"))}${theme.fg("dim", ":")} ${theme.fg("dim", r.tail)}`
242
+ : `${theme.fg(roleColor(r.role), r.role)}${theme.fg("dim", ":")} ${theme.fg("dim", r.tail)}`);
241
243
  const parts = opts.withHead === false ? tails : [head, ...tails];
242
244
  if (parts.length === 0) return [];
243
245
  return [utils.clip(parts.join(theme.fg("dim", g.separator)), width, ell)];
@@ -292,6 +294,11 @@ export function renderAgents(
292
294
  { text: theme.fg(status.color, status.glyph), priority: 99, clippable: false },
293
295
  ];
294
296
  if (status.label) segs.push({ text: theme.fg(status.color, status.label), priority: 95, clippable: false });
297
+ // Expert mode marks its workflow Leader row; the row keeps the same
298
+ // tree/dependency presentation as parallel and DAG dispatches.
299
+ if (isExpertLeader(r)) {
300
+ segs.push({ text: theme.fg("accent", tuiT("widget.agent.expert")), priority: 93, clippable: false });
301
+ }
295
302
  if (r.phase && !(r.phase === "tool-execution" && r.activeTool)) {
296
303
  segs.push({ text: theme.fg("dim", r.phase), priority: 92, minWidth: 6 });
297
304
  }
@@ -67,6 +67,7 @@ const CONFIG_KEYS = [
67
67
  "agentsMode",
68
68
  "todoMode",
69
69
  "todoExpanded",
70
+ "stackStyle",
70
71
  "hideNativeAgents",
71
72
  "toolPalette",
72
73
  "icons.mode",
@@ -155,6 +156,7 @@ const CATALOGS = {
155
156
  "cockpit.agentsMode": "Agent display",
156
157
  "cockpit.todoMode": "Todo display",
157
158
  "cockpit.todoExpanded": "Todo expanded",
159
+ "cockpit.stackStyle": "Stack projection",
158
160
  "cockpit.hideNativeAgents": "Hide native agent widget",
159
161
  "cockpit.icons.mode": "Icon mode",
160
162
  "cockpit.sidebar.mode": "Sidebar mode",
@@ -175,6 +177,7 @@ const CATALOGS = {
175
177
  "cockpit.title.maxLength": "Title max length",
176
178
  "cockpit.title.maxLength.description": "Hard cap on the composed tab title (the middle is ellided).",
177
179
  "cockpit.option.classic": "Classic",
180
+ "cockpit.option.zen": "Zen",
178
181
  "cockpit.option.family": "Family",
179
182
  "cockpit.option.readwrite": "Read/Write",
180
183
  "cockpit.option.search": "Search",
@@ -225,6 +228,7 @@ const CATALOGS = {
225
228
  "cockpit.agentsMode": "Agent 显示",
226
229
  "cockpit.todoMode": "Todo 显示",
227
230
  "cockpit.todoExpanded": "展开 Todo",
231
+ "cockpit.stackStyle": "栈投影",
228
232
  "cockpit.hideNativeAgents": "隐藏原生 Agent 面板",
229
233
  "cockpit.icons.mode": "图标模式",
230
234
  "cockpit.sidebar.mode": "侧边栏模式",
@@ -245,6 +249,7 @@ const CATALOGS = {
245
249
  "cockpit.title.maxLength": "标题最大长度",
246
250
  "cockpit.title.maxLength.description": "组合标签页标题的硬上限(中间省略)。",
247
251
  "cockpit.option.classic": "经典",
252
+ "cockpit.option.zen": "Zen",
248
253
  "cockpit.option.family": "族",
249
254
  "cockpit.option.readwrite": "读写",
250
255
  "cockpit.option.search": "搜索",
@@ -322,6 +327,7 @@ const DEFINITIONS: readonly SettingDefinition[] = [
322
327
  enumDefinition("agentsMode", "cockpit.group.panels", 0, "cockpit.agentsMode", ["list", "compact"], "live"),
323
328
  enumDefinition("todoMode", "cockpit.group.panels", 1, "cockpit.todoMode", ["list", "compact"], "live"),
324
329
  booleanDefinition("todoExpanded", "cockpit.group.panels", 2, "cockpit.todoExpanded", "live"),
330
+ enumDefinition("stackStyle", "cockpit.group.layout", 4, "cockpit.stackStyle", ["classic", "zen"], "live"),
325
331
  booleanDefinition("hideNativeAgents", "cockpit.group.panels", 3, "cockpit.hideNativeAgents", "live"),
326
332
  enumDefinition("sidebar.mode", "cockpit.group.sidebar", 0, "cockpit.sidebar.mode", ["auto", "on", "off"], "live"),
327
333
  {
@@ -688,6 +694,7 @@ function validValue(key: CockpitSettingKey, value: JsonValue): boolean {
688
694
  if (key === "title.generationModel") return typeof value === "string";
689
695
  if (key.startsWith("title.")) return typeof value === "boolean";
690
696
  if (key === "quietSymbols") return value === "check" || value === "dot";
697
+ if (key === "stackStyle") return value === "classic" || value === "zen";
691
698
  if (key === "toolPalette") return ["classic", "family", "readwrite", "search", "mono"].includes(String(value));
692
699
  if (key === "agentsMode" || key === "todoMode") return value === "list" || value === "compact";
693
700
  if (key === "icons.mode") return value === "auto" || value === "nerd" || value === "ascii";
@@ -12,6 +12,7 @@ import type {
12
12
  QuietSymbolMode,
13
13
  SidebarDensity,
14
14
  SidebarMode,
15
+ StackStyle,
15
16
  ViewMode,
16
17
  } from "./types.ts";
17
18
  import { tuiT } from "./tui-i18n.ts";
@@ -39,6 +40,7 @@ const QUIET_SYMBOL_MODES: QuietSymbolMode[] = ["check", "dot"];
39
40
  const ICON_MODES: IconMode[] = ["auto", "nerd", "ascii"];
40
41
  const SIDEBAR_MODES: SidebarMode[] = ["auto", "on", "off"];
41
42
  const SIDEBAR_DENSITIES: SidebarDensity[] = ["comfortable", "compact"];
43
+ const STACK_STYLES: StackStyle[] = ["classic", "zen"];
42
44
 
43
45
  function cycle<T>(values: readonly T[], current: T): T {
44
46
  const index = values.indexOf(current);
@@ -65,6 +67,8 @@ const LEGACY_VALUE_KEYS: Readonly<Record<string, string>> = {
65
67
  compact: "common.value.compact",
66
68
  auto: "common.value.auto",
67
69
  comfortable: "common.value.comfortable",
70
+ classic: "common.value.classic",
71
+ zen: "common.value.zen",
68
72
  "picker…": "common.value.picker",
69
73
  [NO_THEME_LABEL]: "legacy.piSettings",
70
74
  "(rule-based)": "common.ruleBased",
@@ -170,6 +174,13 @@ export function buildRows(config: CockpitConfig, live?: LiveRowState): SettingsR
170
174
  value: config.todoExpanded ? "yes" : "no",
171
175
  next: config.todoExpanded ? "no" : "yes",
172
176
  },
177
+ {
178
+ key: "stackStyle",
179
+ accel: "p",
180
+ label: "stack style",
181
+ value: config.stackStyle,
182
+ next: cycle(STACK_STYLES, config.stackStyle),
183
+ },
173
184
  {
174
185
  key: "hideNativeAgents",
175
186
  accel: "n",
@@ -314,6 +325,8 @@ export function applyRow(config: CockpitConfig, key: string, textValue?: string)
314
325
  return { ...config, todoMode: cycle(VIEW_MODES, config.todoMode) };
315
326
  case "todoExpanded":
316
327
  return { ...config, todoExpanded: !config.todoExpanded };
328
+ case "stackStyle":
329
+ return { ...config, stackStyle: cycle(STACK_STYLES, config.stackStyle) };
317
330
  case "hideNativeAgents":
318
331
  return { ...config, hideNativeAgents: !config.hideNativeAgents };
319
332
  case "sidebarMode":