pi-zentui 0.26.1 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -225,7 +225,8 @@ Reference only—not a starter file. Prefer the minimal overrides above. Optiona
225
225
  "contextThresholds": {
226
226
  "warning": 70,
227
227
  "error": 90
228
- }
228
+ },
229
+ "separator": "dash"
229
230
  }
230
231
  }
231
232
  },
@@ -246,6 +247,7 @@ Reference only—not a starter file. Prefer the minimal overrides above. Optiona
246
247
  },
247
248
  "workingLine": {
248
249
  "enabled": false,
250
+ "placement": "above",
249
251
  "turnSummary": true,
250
252
  "spinner": "star-bloom",
251
253
  "spinnerIntervalMs": 100,
@@ -422,6 +424,7 @@ Reference only—not a starter file. Prefer the minimal overrides above. Optiona
422
424
  - `components.thinkingSteps` independently owns opt-in **Thinking (Experimental)** display. It defaults to `{ "enabled": false, "mode": "tree" }`; canonical modes are `rail | tree | streaming`. The former persisted `streaming-experimental` value is accepted only as a migration alias and is normalized to `streaming` on save.
423
425
  - All three modes decorate Pi's private host renderer and are tested on exact Pi versions 0.80.5, 0.82.1, 0.83.0, 0.84.0, 0.84.4, and 0.85.1. Active Streaming can switch live to Rail or Tree, and Rail and Tree can switch live between each other. Entering Streaming from a structural mode, first enable, and re-enable after live disable require restart; live disable restores native thinking. Disabled mode changes only preconfigure.
424
426
  - `components.workingLine.enabled` is the sole Working-line ownership switch. Thinking (Experimental) never enables, configures, or owns the Working line and leaves the existing **Thinking time** option unchanged.
427
+ - `components.workingLine.placement` (`above | border`) selects the default floating row or the Minimalist/Opencode top border. Accent Rail supports only Above. Disabled, native, unavailable, or unsafe/narrow editors and Pi versions without working-row visibility control fall back to Above without changing saved choices. Legacy `input` normalizes to `above`; the prompt remains available for typing.
425
428
  - `components.selectorBorders` owns selector-border enablement, fixed `zentui` style, and color source. Disable it for native Pi behavior.
426
429
  - `components.footer` owns `native | starship | hidden` style selection, color source, model label, and Starship options. Hidden hides its main segments, retaining only allowed extension statuses; an empty status line occupies no rows.
427
430
  - Starship's package-version segment reads the project manifest and is distinct from the runtime segment, which reports the installed toolchain.
@@ -531,7 +534,7 @@ Set `ZENTUI_DEBUG=1` when launching Pi to log the workaround diagnostic without
531
534
 
532
535
  Set `components.editor.style` to `minimalist` or select it in `/zentui`. The rounded frame places viewport counts, Bash state, current/completed turn duration, and explicit session name at top left; cost, model, thinking, context, and optional latest-prompt cache hit rate at top right; viewport count plus Git at bottom left; and configured path at bottom right. Unnamed sessions add no placeholder.
533
536
 
534
- Path examples are `src` (`compact`), `zentui/src` (`project`), and `~/Projects/zentui/src` (`full`). Context can render as `11%`, `11%/372k`, or, with the gauge enabled and enough room, `[█░░░░] 11%/372k`. Enable `showCacheHit` to append values such as `Cache 98.2%`; it defaults to `false`, omits missing data, yields before context at narrow widths, and remains independent of Footer. The gauge shortens or disappears before the context text at narrow widths. Session name, timer, cost, cache hit rate, and Git can be hidden independently; model, thinking, and context remain structurally stable.
537
+ Path examples are `src` (`compact`), `zentui/src` (`project`), and `~/Projects/zentui/src` (`full`). Context can render as `11%`, `11%/372k`, or, with the gauge enabled and enough room, `[█░░░░] 11%/372k`. Enable `showCacheHit` to append values such as `Cache 98.2%`; it defaults to `false`, omits missing data, yields before context at narrow widths, and remains independent of Footer. The gauge shortens or disappears before the context text at narrow widths. Session name, timer, cost, cache hit rate, and Git can be hidden independently; model, thinking, and context remain structurally stable. Metadata items are joined with dashes by default (`separator: "dash"`) or dots (`separator: "dot"`).
535
538
 
536
539
  Autocomplete stays inside the frame when Pi output can be split safely. Unknown third-party layouts fail open. Footer visibility remains independently controlled by `components.footer.style`; Minimalist does not remove Pi's header.
537
540
 
@@ -54,10 +54,12 @@ export type WorkingLineSpinner =
54
54
  | "claude-inspired"
55
55
  | "pulse";
56
56
  export type WorkingLineTextAnimation = "classic" | "kitt" | "disabled";
57
+ export type WorkingLinePlacement = "above" | "border";
57
58
  export type ThinkingStepsMode = "rail" | "tree" | "streaming";
58
59
  export type ComponentStyleOwner = "editor" | "userMessages" | "selectorBorders" | "footer";
59
60
  export type MinimalistPathDisplayMode = "compact" | "project" | "full";
60
61
  export type MinimalistContextFormat = "percent" | "percent-total";
62
+ export type MinimalistEditorSeparator = "dash" | "dot";
61
63
  export type EditorBorderColorMode = "static" | "adaptive";
62
64
  export type CompletionMenuStyle = "native" | "palette";
63
65
  export type CompactFooterMaxLines = 1 | 2 | 3 | "unlimited";
@@ -142,6 +144,7 @@ export type MinimalistEditorStyleConfig = {
142
144
  showCacheHit: boolean;
143
145
  showGit: boolean;
144
146
  contextThresholds: ContextThresholds;
147
+ separator: MinimalistEditorSeparator;
145
148
  };
146
149
 
147
150
  /** Temporary name retained for existing settings consumers. */
@@ -258,6 +261,7 @@ export type WorkingLineComponentConfig = {
258
261
  colorSource: ColorSource;
259
262
  messages: WorkingLineMessagesConfig;
260
263
  segments: WorkingLineSegmentsConfig;
264
+ placement: WorkingLinePlacement;
261
265
  };
262
266
 
263
267
  export type WorkingLineComponentPatch = Partial<
@@ -498,6 +502,7 @@ const defaultMinimalistStyle: MinimalistEditorStyleConfig = {
498
502
  showCacheHit: false,
499
503
  showGit: true,
500
504
  contextThresholds: { warning: 70, error: 90 },
505
+ separator: "dash",
501
506
  };
502
507
 
503
508
  const defaultStarshipStyle: StarshipFooterStyleConfig = {
@@ -557,6 +562,7 @@ const defaultComponents: ComponentsConfig = {
557
562
  colorSource: "theme",
558
563
  messages: { custom: true, values: [...PI_WORKING_LINE_MESSAGES] },
559
564
  segments: { tool: true, elapsed: true, thought: true, tokens: true },
565
+ placement: "above",
560
566
  },
561
567
  selectorBorders: { enabled: true, style: "zentui", colorSource: "theme" },
562
568
  footer: {
@@ -1405,6 +1411,10 @@ function resolveComponents(config: ConfigRecord): ComponentsConfig {
1405
1411
  showCacheHit: parseBoolean(minimalist.showCacheHit, defaultMinimalistStyle.showCacheHit),
1406
1412
  showGit: parseBoolean(minimalist.showGit, defaultMinimalistStyle.showGit),
1407
1413
  contextThresholds: minimalistThresholds,
1414
+ separator:
1415
+ minimalist.separator === "dash" || minimalist.separator === "dot"
1416
+ ? minimalist.separator
1417
+ : defaultMinimalistStyle.separator,
1408
1418
  },
1409
1419
  },
1410
1420
  },
@@ -1466,6 +1476,10 @@ function resolveComponents(config: ConfigRecord): ComponentsConfig {
1466
1476
  workingLine.textAnimation === "disabled"
1467
1477
  ? workingLine.textAnimation
1468
1478
  : defaultComponents.workingLine.textAnimation,
1479
+ placement:
1480
+ workingLine.placement === "border"
1481
+ ? workingLine.placement
1482
+ : defaultComponents.workingLine.placement,
1469
1483
  colorSource: parseColorSource(
1470
1484
  workingLine.colorSource,
1471
1485
  defaultComponents.workingLine.colorSource,
@@ -1903,6 +1917,7 @@ function applyMinimalistStylePatch(
1903
1917
  if (patch.showCost !== undefined) style.showCost = patch.showCost;
1904
1918
  if (patch.showCacheHit !== undefined) style.showCacheHit = patch.showCacheHit;
1905
1919
  if (patch.showGit !== undefined) style.showGit = patch.showGit;
1920
+ if (patch.separator !== undefined) style.separator = patch.separator;
1906
1921
  if (patch.contextThresholds !== undefined) {
1907
1922
  style.contextThresholds = { ...style.contextThresholds, ...patch.contextThresholds };
1908
1923
  }
@@ -1970,6 +1985,7 @@ export function saveWorkingLineComponentPatch(
1970
1985
  if (patch.textIntervalMs !== undefined) component.textIntervalMs = patch.textIntervalMs;
1971
1986
  if (patch.textAnimation !== undefined) component.textAnimation = patch.textAnimation;
1972
1987
  if (patch.colorSource !== undefined) component.colorSource = patch.colorSource;
1988
+ if (patch.placement !== undefined) component.placement = patch.placement;
1973
1989
  if (patch.messages?.custom !== undefined) component.messages.custom = patch.messages.custom;
1974
1990
  if (patch.messages?.values !== undefined) {
1975
1991
  component.messages.values = normalizeWorkingLineMessages([...patch.messages.values]);
@@ -240,6 +240,13 @@ export default function (pi: ExtensionAPI) {
240
240
  let editorInstalled = false;
241
241
  let editorInstallMode: EditorInstallMode = "none";
242
242
  let installedEditorFactory: EditorFactory | undefined;
243
+ let activeEditor:
244
+ | {
245
+ editor: PolishedEditor | WrappedPolishedEditor;
246
+ factory: EditorFactory;
247
+ generation: number;
248
+ }
249
+ | undefined;
243
250
  let wrappedEditorFactory: EditorFactory | undefined;
244
251
  let stopSessionTimer: () => void = () => {};
245
252
  let stopMinimalistDurationUpdates: () => void = () => {};
@@ -330,7 +337,28 @@ export default function (pi: ExtensionAPI) {
330
337
  Date.now,
331
338
  () => interactionMetrics.currentThought(),
332
339
  () => invalidateWorkingLinePublisherState(),
340
+ undefined,
341
+ (ctx) => {
342
+ if (!activeTuiContext || ctx.ui !== activeTuiContext.ui) return false;
343
+ if (!ownsInstalledEditorFactory()) {
344
+ activeEditor = undefined;
345
+ return false;
346
+ }
347
+ return Boolean(
348
+ activeEditor &&
349
+ sessionLifecycle.isCurrent(activeEditor.generation) &&
350
+ activeEditor.factory === installedEditorFactory &&
351
+ effectiveEditorEnabled() &&
352
+ currentConfig.components.editor.style !== "accent-rail" &&
353
+ activeEditor.editor.canEmbedWorkingLineBorder(),
354
+ );
355
+ },
333
356
  );
357
+ workingLine.setRequestRender(() => {
358
+ if (sessionLifecycle.isCurrent()) {
359
+ requestEditorRender?.();
360
+ }
361
+ });
334
362
  let workingLineSessionReady = false;
335
363
  const workingLineExtensions = new WorkingLineExtensionSegments(
336
364
  pi.events,
@@ -567,6 +595,7 @@ export default function (pi: ExtensionAPI) {
567
595
 
568
596
  const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
569
597
  if (!sessionLifecycle.isCurrent() || !ctx.hasUI) return;
598
+ if (activeTuiContext?.ui === ctx.ui) activeTuiContext = ctx;
570
599
  if (editorInstalled && !ownsInstalledEditorFactory()) reconcileObservedEditorOwnership(ctx);
571
600
  syncFooterState(ctx);
572
601
  if (project && needsProjectRefresh()) scheduleProjectRefresh(ctx);
@@ -768,6 +797,8 @@ export default function (pi: ExtensionAPI) {
768
797
  };
769
798
 
770
799
  const clearEditorOwnership = () => {
800
+ activeEditor = undefined;
801
+ if (activeTuiContext && workingLineSessionReady) workingLine.reconcile(activeTuiContext);
771
802
  setMinimalistDecorationActive(false);
772
803
  requestEditorRender = undefined;
773
804
  wrappedEditorFactory = undefined;
@@ -779,6 +810,7 @@ export default function (pi: ExtensionAPI) {
779
810
 
780
811
  const trackZentuiEditorFactory = (factory: EditorFactory): boolean => {
781
812
  if (!isOwnedEditorFactory(factory)) return false;
813
+ if (activeEditor?.factory !== factory) activeEditor = undefined;
782
814
  const baseFactory = getZentuiEditorBaseFactory(factory);
783
815
  wrappedEditorFactory = baseFactory;
784
816
  installedEditorFactory = factory;
@@ -799,7 +831,11 @@ export default function (pi: ExtensionAPI) {
799
831
 
800
832
  const reconcileObservedEditorOwnership = (ctx: ExtensionContext) => {
801
833
  const observed = observeEditorFactory(ctx);
802
- if (!observed.known) return observed;
834
+ if (!observed.known) {
835
+ activeEditor = undefined;
836
+ if (workingLineSessionReady) workingLine.reconcile(ctx);
837
+ return observed;
838
+ }
803
839
  if (observed.factory && isOwnedEditorFactory(observed.factory)) {
804
840
  trackZentuiEditorFactory(observed.factory);
805
841
  } else {
@@ -820,39 +856,78 @@ export default function (pi: ExtensionAPI) {
820
856
  return editor;
821
857
  };
822
858
 
859
+ const isActiveEditor = (editor: PolishedEditor | WrappedPolishedEditor, generation: number) =>
860
+ sessionLifecycle.isCurrent(generation) && activeEditor?.editor === editor;
861
+
862
+ const editorBorderCapabilityChanged = (
863
+ editor: PolishedEditor | WrappedPolishedEditor,
864
+ generation: number,
865
+ ) => {
866
+ if (!isActiveEditor(editor, generation) || !activeTuiContext || !workingLineSessionReady)
867
+ return;
868
+ workingLine.reconcile(activeTuiContext);
869
+ if (workingLine.isAvailable()) requestEditorRender?.();
870
+ };
871
+
872
+ const editorWorkingLineFrame = (
873
+ editor: PolishedEditor | WrappedPolishedEditor,
874
+ generation: number,
875
+ ) => (isActiveEditor(editor, generation) ? workingLine.currentWorkingLineFrame() : undefined);
876
+
823
877
  const makeEditorFactory = (ctx: ExtensionContext): ZentuiEditorFactory => {
824
878
  const sessionTheme = ctx.ui.theme;
879
+ const generation = sessionLifecycle.currentGeneration();
825
880
  const factory = ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => {
826
- requestEditorRender = () => tui.requestRender();
827
- return markOwnedAccentRailEditor(
881
+ const editor: PolishedEditor = markOwnedAccentRailEditor(
828
882
  new PolishedEditor(
829
883
  tui,
830
884
  theme,
831
885
  keybindings,
832
886
  sessionTheme,
833
887
  getCurrentConfig,
834
- () => getEditorMeta(ctx),
888
+ () => getEditorMeta(activeTuiContext ?? ctx),
835
889
  getThinkingLevel,
836
- () => ({
837
- codexQuota: getEditorQuota(),
838
- cwd: ctx.cwd,
839
- projectRoot: minimalistProjectRoot,
840
- branch: state.branch,
841
- dirty: state.dirty,
842
- ahead: state.ahead,
843
- behind: state.behind,
844
- costLabel: state.costLabel,
845
- modelLabel: modelLabelFor(state, currentConfig.components.editor.modelLabel),
846
- thinkingLevel: getThinkingLevel(),
847
- ...getEditorContextMetadata(ctx),
848
- cacheHitRate: state.usageTotals.latestCacheHitRate,
849
- sessionName: ctx.sessionManager.getSessionName() ?? "",
850
- agentDurationMs: getAgentDurationMs(),
851
- agentActive: agentRunActive,
852
- }),
853
- setMinimalistDecorationActive,
890
+ () => {
891
+ const workingLineFrame = editorWorkingLineFrame(editor, generation);
892
+ if (currentConfig.components.editor.style !== "minimalist") {
893
+ return { cwd: "", workingLineFrame };
894
+ }
895
+ return {
896
+ codexQuota: getEditorQuota(),
897
+ cwd: (activeTuiContext ?? ctx).cwd,
898
+ projectRoot: minimalistProjectRoot,
899
+ branch: state.branch,
900
+ dirty: state.dirty,
901
+ ahead: state.ahead,
902
+ behind: state.behind,
903
+ costLabel: state.costLabel,
904
+ modelLabel: modelLabelFor(state, currentConfig.components.editor.modelLabel),
905
+ thinkingLevel: getThinkingLevel(),
906
+ ...getEditorContextMetadata(activeTuiContext ?? ctx),
907
+ cacheHitRate: state.usageTotals.latestCacheHitRate,
908
+ sessionName: (activeTuiContext ?? ctx).sessionManager.getSessionName() ?? "",
909
+ agentDurationMs: getAgentDurationMs(),
910
+ agentActive: agentRunActive,
911
+ workingLineFrame,
912
+ };
913
+ },
914
+ (active) => {
915
+ if (isActiveEditor(editor, generation)) setMinimalistDecorationActive(active);
916
+ },
917
+ () => editorBorderCapabilityChanged(editor, generation),
854
918
  ),
855
919
  );
920
+ const observed = observeEditorFactory(ctx);
921
+ if (
922
+ sessionLifecycle.isCurrent(generation) &&
923
+ activeTuiContext?.ui === ctx.ui &&
924
+ observed.known &&
925
+ observed.factory === factory
926
+ ) {
927
+ activeEditor = { editor, factory, generation };
928
+ requestEditorRender = () => tui.requestRender();
929
+ }
930
+ return editor;
856
931
  }) as ZentuiEditorFactory;
857
932
  factory[ZENTUI_EDITOR_FACTORY] = true;
858
933
  factory[ZENTUI_EDITOR_OWNER] = editorOwnerToken;
@@ -864,35 +939,56 @@ export default function (pi: ExtensionAPI) {
864
939
  baseFactory: EditorFactory,
865
940
  ): ZentuiEditorFactory => {
866
941
  const sessionTheme = ctx.ui.theme;
942
+ const generation = sessionLifecycle.currentGeneration();
867
943
  const factory = ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => {
868
- requestEditorRender = () => tui.requestRender();
869
- return markOwnedAccentRailEditor(
944
+ const editor: WrappedPolishedEditor = markOwnedAccentRailEditor(
870
945
  new WrappedPolishedEditor(
871
946
  baseFactory(tui, theme, keybindings),
872
947
  sessionTheme,
873
948
  getCurrentConfig,
874
- () => getEditorMeta(ctx),
949
+ () => getEditorMeta(activeTuiContext ?? ctx),
875
950
  getThinkingLevel,
876
- () => ({
877
- codexQuota: getEditorQuota(),
878
- cwd: ctx.cwd,
879
- projectRoot: minimalistProjectRoot,
880
- branch: state.branch,
881
- dirty: state.dirty,
882
- ahead: state.ahead,
883
- behind: state.behind,
884
- costLabel: state.costLabel,
885
- modelLabel: modelLabelFor(state, currentConfig.components.editor.modelLabel),
886
- thinkingLevel: getThinkingLevel(),
887
- ...getEditorContextMetadata(ctx),
888
- cacheHitRate: state.usageTotals.latestCacheHitRate,
889
- sessionName: ctx.sessionManager.getSessionName() ?? "",
890
- agentDurationMs: getAgentDurationMs(),
891
- agentActive: agentRunActive,
892
- }),
893
- setMinimalistDecorationActive,
951
+ () => {
952
+ const workingLineFrame = editorWorkingLineFrame(editor, generation);
953
+ if (currentConfig.components.editor.style !== "minimalist") {
954
+ return { cwd: "", workingLineFrame };
955
+ }
956
+ return {
957
+ codexQuota: getEditorQuota(),
958
+ cwd: (activeTuiContext ?? ctx).cwd,
959
+ projectRoot: minimalistProjectRoot,
960
+ branch: state.branch,
961
+ dirty: state.dirty,
962
+ ahead: state.ahead,
963
+ behind: state.behind,
964
+ costLabel: state.costLabel,
965
+ modelLabel: modelLabelFor(state, currentConfig.components.editor.modelLabel),
966
+ thinkingLevel: getThinkingLevel(),
967
+ ...getEditorContextMetadata(activeTuiContext ?? ctx),
968
+ cacheHitRate: state.usageTotals.latestCacheHitRate,
969
+ sessionName: (activeTuiContext ?? ctx).sessionManager.getSessionName() ?? "",
970
+ agentDurationMs: getAgentDurationMs(),
971
+ agentActive: agentRunActive,
972
+ workingLineFrame,
973
+ };
974
+ },
975
+ (active) => {
976
+ if (isActiveEditor(editor, generation)) setMinimalistDecorationActive(active);
977
+ },
978
+ () => editorBorderCapabilityChanged(editor, generation),
894
979
  ),
895
980
  );
981
+ const observed = observeEditorFactory(ctx);
982
+ if (
983
+ sessionLifecycle.isCurrent(generation) &&
984
+ activeTuiContext?.ui === ctx.ui &&
985
+ observed.known &&
986
+ observed.factory === factory
987
+ ) {
988
+ activeEditor = { editor, factory, generation };
989
+ requestEditorRender = () => tui.requestRender();
990
+ }
991
+ return editor;
896
992
  }) as ZentuiEditorFactory;
897
993
  factory[ZENTUI_EDITOR_FACTORY] = true;
898
994
  factory[ZENTUI_EDITOR_OWNER] = editorOwnerToken;
@@ -1141,6 +1237,8 @@ export default function (pi: ExtensionAPI) {
1141
1237
  ok: false,
1142
1238
  reason: "the editor could not be reconciled safely; reload Pi to apply this change",
1143
1239
  };
1240
+ } finally {
1241
+ if (workingLineSessionReady) workingLine.reconcile(ctx);
1144
1242
  }
1145
1243
  };
1146
1244
 
@@ -1231,6 +1329,7 @@ export default function (pi: ExtensionAPI) {
1231
1329
  const cleanupUi = (ctx?: ExtensionContext) => {
1232
1330
  if (!ctx || !sessionLifecycle.isCurrent()) return;
1233
1331
  sessionLifecycle.shutdown();
1332
+ activeEditor = undefined;
1234
1333
  extensionStatuses.dispose();
1235
1334
  codexQuota.stop();
1236
1335
  stopSessionTimer();
@@ -1290,6 +1389,7 @@ export default function (pi: ExtensionAPI) {
1290
1389
  usageTotals.invalidate();
1291
1390
  footerTelemetry.reset();
1292
1391
  const lifecycleGeneration = sessionLifecycle.start();
1392
+ activeEditor = undefined;
1293
1393
  // A new generation must not expose or route extension segments through the previous
1294
1394
  // session while asynchronous TUI startup is still pending.
1295
1395
  workingLineSessionReady = false;
@@ -1350,6 +1450,7 @@ export default function (pi: ExtensionAPI) {
1350
1450
  if (currentConfig.components.editor.style !== "minimalist") {
1351
1451
  setMinimalistDecorationActive(false);
1352
1452
  }
1453
+ workingLine.reconcile(ctx);
1353
1454
  reconcileUserMessages();
1354
1455
  reconcileFooter(ctx);
1355
1456
  reconcileProjectRefresh(ctx, effectiveFooterStyle() !== previousFooterStyle);
@@ -1404,6 +1505,7 @@ export default function (pi: ExtensionAPI) {
1404
1505
  if (patch.style !== undefined && patch.style !== "minimalist") {
1405
1506
  setMinimalistDecorationActive(false);
1406
1507
  }
1508
+ workingLine.reconcile(ctx);
1407
1509
  syncFooterUsage(ctx);
1408
1510
  if (patch.modelLabel !== undefined) syncFooterState(ctx);
1409
1511
  reconcileProjectRefresh(ctx);
@@ -58,6 +58,8 @@ export type MinimalistEditorMetadata = {
58
58
  sessionName?: string;
59
59
  agentDurationMs?: number;
60
60
  agentActive?: boolean;
61
+ /** Controller-gated border content; never resolve canonical placement in the renderer. */
62
+ workingLineFrame?: string;
61
63
  };
62
64
 
63
65
  export type MinimalistFrameOptions = {
@@ -182,6 +184,7 @@ function renderTopLeft(
182
184
  : safeThemeFg(uiTheme, "muted", duration),
183
185
  );
184
186
  }
187
+ if (metadata.workingLineFrame) parts.push(metadata.workingLineFrame);
185
188
  const sessionName = includeSessionName
186
189
  ? sanitizeEditorMetadataText(metadata.sessionName ?? "")
187
190
  : "";
@@ -209,8 +212,9 @@ function renderTopRight(
209
212
  ): string {
210
213
  const source = config.components.editor.colorSource;
211
214
  const parts: string[] = [];
215
+ const separator = config.components.editor.styles.minimalist.separator === "dot" ? " · " : " – ";
212
216
  const joinParts = (values: string[]) =>
213
- values.map((part, index) => (index > 0 ? `${renderBorder(" – ")}${part}` : part)).join("");
217
+ values.map((part, index) => (index > 0 ? `${renderBorder(separator)}${part}` : part)).join("");
214
218
  const cost = config.components.editor.styles.minimalist.showCost
215
219
  ? sanitizeEditorMetadataText(metadata.costLabel ?? "")
216
220
  : "";
@@ -498,6 +502,7 @@ export function renderMinimalistFrame({
498
502
  const topFallbacks = [
499
503
  joinStyled([topViewport, topOperational], separator),
500
504
  topOperational,
505
+ ...(metadata.workingLineFrame ? [metadata.workingLineFrame] : []),
501
506
  ].filter((value, index, values) => value !== topLeft && values.indexOf(value) === index);
502
507
  const top = renderLabeledBorder({
503
508
  width,
@@ -45,6 +45,7 @@ import {
45
45
  MAX_WORKING_LINE_INTERVAL_MS,
46
46
  MIN_WORKING_LINE_INTERVAL_MS,
47
47
  type MinimalistConfig,
48
+ type MinimalistEditorSeparator,
48
49
  type ModelLabelSource,
49
50
  type PathDisplayConfig,
50
51
  type PolishedCopyFriendlyEditorStyleConfig,
@@ -57,6 +58,7 @@ import {
57
58
  type UserMessageStyle,
58
59
  type UserMessagesComponentConfig,
59
60
  type WorkingLineComponentPatch,
61
+ type WorkingLinePlacement,
60
62
  type WorkingLineSpinner,
61
63
  type WorkingLineTextAnimation,
62
64
  } from "./config";
@@ -126,6 +128,7 @@ const completionMenuValues: CompletionMenuStyle[] = ["palette", "native"];
126
128
  const accentRailSurfaceValues = ["filled", "transparent"];
127
129
  const minimalistPathDisplayValues = ["compact", "project", "full"];
128
130
  const minimalistContextFormatValues = ["percent", "percent-total"];
131
+ const minimalistSeparatorValues: MinimalistEditorSeparator[] = ["dash", "dot"];
129
132
  const editorBorderColorModeValues: EditorBorderColorMode[] = ["static", "adaptive"];
130
133
  const compactFooterMaxLineValues = ["1", "2", "3", "unlimited"];
131
134
  const featureStateValues: FeatureState[] = ["enabled", "disabled"];
@@ -151,6 +154,11 @@ const speedValues = (presets: readonly { label: string }[]) => [
151
154
  ...presets.map(({ label }) => label),
152
155
  "Custom…",
153
156
  ];
157
+ const workingLinePlacementLabels: Record<WorkingLinePlacement, string> = {
158
+ above: "Above",
159
+ border: "Border",
160
+ };
161
+ const workingLinePlacementValues = Object.values(workingLinePlacementLabels);
154
162
  const workingLineTextAnimationValues: WorkingLineTextAnimation[] = ["classic", "kitt", "disabled"];
155
163
  const thinkingStepsModeLabels: Record<ThinkingStepsMode, string> = {
156
164
  rail: "Rail",
@@ -728,6 +736,13 @@ function buildMinimalistEditorStyleItems(config: PolishedTuiConfig): SettingItem
728
736
  currentValue: featureValue(minimalist.showGit),
729
737
  values: featureStateValues,
730
738
  },
739
+ {
740
+ id: "minimalistSeparator",
741
+ label: "Separator",
742
+ description: "Choose dash (–) or dot (·) between editor metadata items.",
743
+ currentValue: minimalist.separator,
744
+ values: minimalistSeparatorValues,
745
+ },
731
746
  ];
732
747
  }
733
748
 
@@ -796,6 +811,16 @@ function buildWorkingLineItems(config: PolishedTuiConfig): SettingItem[] {
796
811
  currentValue: featureValue(workingLine.enabled),
797
812
  values: featureStateValues,
798
813
  },
814
+ ...(config.components.editor.enabled && config.components.editor.style !== "accent-rail"
815
+ ? [
816
+ {
817
+ id: "workingLinePlacement",
818
+ label: "Placement",
819
+ currentValue: workingLinePlacementLabels[workingLine.placement],
820
+ values: workingLinePlacementValues,
821
+ },
822
+ ]
823
+ : []),
799
824
  {
800
825
  id: "workingLineTurnSummary",
801
826
  label: "Turn summary",
@@ -1745,6 +1770,14 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
1745
1770
  { contextFormat: newValue as MinimalistConfig["contextFormat"] },
1746
1771
  ctx,
1747
1772
  );
1773
+ else if (
1774
+ id === "minimalistSeparator" &&
1775
+ (newValue === "dash" || newValue === "dot")
1776
+ )
1777
+ deps.setMinimalist(
1778
+ { separator: newValue as MinimalistEditorSeparator },
1779
+ ctx,
1780
+ );
1748
1781
  else if (enabled !== undefined) {
1749
1782
  const key =
1750
1783
  id === "minimalistContextGauge"
@@ -1817,6 +1850,21 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
1817
1850
  notifyWorkingLineChange("Turn summary", newValue, result, false);
1818
1851
  return;
1819
1852
  }
1853
+ if (id === "workingLinePlacement") {
1854
+ const editor = deps.getConfig().components.editor;
1855
+ if (!editor.enabled || editor.style === "accent-rail") return;
1856
+ const placement = (
1857
+ Object.entries(workingLinePlacementLabels) as Array<
1858
+ [WorkingLinePlacement, string]
1859
+ >
1860
+ ).find(([, label]) => label === newValue)?.[0];
1861
+ if (placement) {
1862
+ const result = deps.setWorkingLineComponent({ placement }, ctx);
1863
+ settingsList.updateValue(id, newValue);
1864
+ notifyWorkingLineChange("Placement", newValue, result);
1865
+ return;
1866
+ }
1867
+ }
1820
1868
  const selectedWorkingLineSpinner =
1821
1869
  id === "workingLineSpinner" ? workingLineSpinnerId(newValue) : undefined;
1822
1870
  if (selectedWorkingLineSpinner) {