pi-zentui 0.26.1 → 0.27.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/docs/configuration.md +5 -2
- package/extensions/zentui/config.ts +16 -0
- package/extensions/zentui/index.ts +145 -43
- package/extensions/zentui/minimalist-editor.ts +6 -1
- package/extensions/zentui/settings-command.ts +48 -0
- package/extensions/zentui/ui.ts +141 -28
- package/extensions/zentui/working-line.ts +141 -6
- package/package.json +1 -1
package/docs/configuration.md
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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(
|
|
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) {
|
package/extensions/zentui/ui.ts
CHANGED
|
@@ -35,6 +35,9 @@ import {
|
|
|
35
35
|
safeThemeFg,
|
|
36
36
|
} from "./style";
|
|
37
37
|
|
|
38
|
+
// Reserve at least four label cells beside Minimalist metadata.
|
|
39
|
+
const MIN_WORKING_LINE_BORDER_WIDTH = 16;
|
|
40
|
+
|
|
38
41
|
const LEGACY_SPLIT_POLISHED_FRAME = Symbol.for("pi-zentui.polished-frame");
|
|
39
42
|
|
|
40
43
|
export type ViewportCounts = {
|
|
@@ -117,6 +120,7 @@ export type PolishedEditorFrameOptions = {
|
|
|
117
120
|
modelMeta: EditorMeta;
|
|
118
121
|
thinkingLevel?: string;
|
|
119
122
|
rightStatus?: string;
|
|
123
|
+
workingLineFrame?: string;
|
|
120
124
|
shellMode?: boolean;
|
|
121
125
|
borderColor?: (text: string) => string;
|
|
122
126
|
};
|
|
@@ -134,6 +138,8 @@ type PolishedFrameOptions = {
|
|
|
134
138
|
shellMode?: boolean;
|
|
135
139
|
ownedFrame?: PolishedFrameSplit;
|
|
136
140
|
trustedBaseFrame?: boolean;
|
|
141
|
+
getWorkingLineFrame: () => string | undefined;
|
|
142
|
+
reportWorkingLineBorder: (active: boolean) => void;
|
|
137
143
|
borderColor?: (text: string) => string;
|
|
138
144
|
};
|
|
139
145
|
|
|
@@ -162,7 +168,8 @@ type MinimalistFrameAdapterOptions = {
|
|
|
162
168
|
uiTheme: Theme;
|
|
163
169
|
config: ZentuiConfig;
|
|
164
170
|
inputText: string;
|
|
165
|
-
|
|
171
|
+
getMetadata: () => MinimalistEditorMetadata;
|
|
172
|
+
reportWorkingLineBorder: (active: boolean) => void;
|
|
166
173
|
ownedFrame?: PolishedFrameSplit;
|
|
167
174
|
trustedBaseFrame?: boolean;
|
|
168
175
|
borderColor?: (text: string) => string;
|
|
@@ -694,7 +701,8 @@ function renderMinimalistFrameFromBase({
|
|
|
694
701
|
uiTheme,
|
|
695
702
|
config,
|
|
696
703
|
inputText,
|
|
697
|
-
|
|
704
|
+
getMetadata,
|
|
705
|
+
reportWorkingLineBorder,
|
|
698
706
|
ownedFrame,
|
|
699
707
|
trustedBaseFrame = false,
|
|
700
708
|
borderColor,
|
|
@@ -733,6 +741,16 @@ function renderMinimalistFrameFromBase({
|
|
|
733
741
|
below: parsedBottom?.count,
|
|
734
742
|
};
|
|
735
743
|
const editorLines = ownedFrame?.editorLines ?? editorFrame.slice(1, -1);
|
|
744
|
+
// Publish safe geometry before reading the controller-gated Working frame.
|
|
745
|
+
reportWorkingLineBorder(
|
|
746
|
+
width >= MIN_WORKING_LINE_BORDER_WIDTH &&
|
|
747
|
+
Boolean(
|
|
748
|
+
ownedFrame ||
|
|
749
|
+
(typeof autocompleteSource.isShowingAutocomplete === "function" &&
|
|
750
|
+
autocompleteCapture?.compatible),
|
|
751
|
+
),
|
|
752
|
+
);
|
|
753
|
+
const metadata = getMetadata();
|
|
736
754
|
const lines = renderMinimalistFrame({
|
|
737
755
|
width,
|
|
738
756
|
editorLines,
|
|
@@ -764,6 +782,8 @@ function renderPolishedFrame({
|
|
|
764
782
|
shellMode = false,
|
|
765
783
|
ownedFrame,
|
|
766
784
|
trustedBaseFrame = false,
|
|
785
|
+
getWorkingLineFrame,
|
|
786
|
+
reportWorkingLineBorder,
|
|
767
787
|
borderColor,
|
|
768
788
|
}: PolishedFrameOptions): PolishedFrameResult {
|
|
769
789
|
if (width <= 2) return { lines: clampRenderedLines(baseRendered, width), decorated: false };
|
|
@@ -804,6 +824,16 @@ function renderPolishedFrame({
|
|
|
804
824
|
above: parsedTop?.count,
|
|
805
825
|
below: parsedBottom?.count,
|
|
806
826
|
};
|
|
827
|
+
// Publish safe geometry before reading the controller-gated Working frame.
|
|
828
|
+
reportWorkingLineBorder(
|
|
829
|
+
width >= MIN_WORKING_LINE_BORDER_WIDTH &&
|
|
830
|
+
Boolean(
|
|
831
|
+
ownedFrame ||
|
|
832
|
+
(typeof autocompleteSource.isShowingAutocomplete === "function" &&
|
|
833
|
+
autocompleteCapture?.compatible),
|
|
834
|
+
),
|
|
835
|
+
);
|
|
836
|
+
const workingLineFrame = getWorkingLineFrame();
|
|
807
837
|
const lines = renderPolishedEditorFrame({
|
|
808
838
|
width,
|
|
809
839
|
editorLines,
|
|
@@ -814,6 +844,7 @@ function renderPolishedFrame({
|
|
|
814
844
|
modelMeta,
|
|
815
845
|
thinkingLevel,
|
|
816
846
|
rightStatus,
|
|
847
|
+
workingLineFrame,
|
|
817
848
|
shellMode,
|
|
818
849
|
borderColor,
|
|
819
850
|
});
|
|
@@ -848,6 +879,7 @@ export function renderPolishedEditorFrame({
|
|
|
848
879
|
modelMeta,
|
|
849
880
|
thinkingLevel,
|
|
850
881
|
rightStatus,
|
|
882
|
+
workingLineFrame,
|
|
851
883
|
shellMode = false,
|
|
852
884
|
borderColor,
|
|
853
885
|
}: PolishedEditorFrameOptions): string[] {
|
|
@@ -920,13 +952,23 @@ export function renderPolishedEditorFrame({
|
|
|
920
952
|
return renderStaticBorder(text);
|
|
921
953
|
}
|
|
922
954
|
};
|
|
923
|
-
|
|
955
|
+
let top = renderBorder(
|
|
924
956
|
renderEditorBorder(
|
|
925
957
|
width,
|
|
926
958
|
"above",
|
|
927
959
|
config.components.editor.viewportIndicators ? viewport.above : undefined,
|
|
928
960
|
),
|
|
929
961
|
);
|
|
962
|
+
if (workingLineFrame && width >= MIN_WORKING_LINE_BORDER_WIDTH) {
|
|
963
|
+
const label = truncateToWidth(workingLineFrame, width - 6, "…");
|
|
964
|
+
top = `${renderBorder("── ")}${label}${renderBorder(
|
|
965
|
+
` ${renderEditorBorder(
|
|
966
|
+
width - visibleWidth(label) - 4,
|
|
967
|
+
"above",
|
|
968
|
+
config.components.editor.viewportIndicators ? viewport.above : undefined,
|
|
969
|
+
)}`,
|
|
970
|
+
)}`;
|
|
971
|
+
}
|
|
930
972
|
const bottom = renderBorder(
|
|
931
973
|
renderEditorBorder(
|
|
932
974
|
width,
|
|
@@ -977,6 +1019,7 @@ export class PolishedEditor extends CustomEditor {
|
|
|
977
1019
|
private readonly uiTheme: Theme;
|
|
978
1020
|
|
|
979
1021
|
private readonly mouse = new EditorMouseForwarder();
|
|
1022
|
+
private workingLineBorderCapable = false;
|
|
980
1023
|
|
|
981
1024
|
constructor(
|
|
982
1025
|
tui: TUI,
|
|
@@ -988,6 +1031,7 @@ export class PolishedEditor extends CustomEditor {
|
|
|
988
1031
|
getThinkingLevel: () => string | undefined,
|
|
989
1032
|
getMinimalistMetadata: () => MinimalistEditorMetadata = () => ({ cwd: "" }),
|
|
990
1033
|
onMinimalistDecorationChange: (active: boolean) => void = () => {},
|
|
1034
|
+
private readonly onWorkingLineBorderCapabilityChange: () => void = () => {},
|
|
991
1035
|
) {
|
|
992
1036
|
super(tui, theme, keybindings, { paddingX: 0 });
|
|
993
1037
|
this.borderColor = (text: string) => safeThemeFg(uiTheme, "border", text);
|
|
@@ -997,6 +1041,18 @@ export class PolishedEditor extends CustomEditor {
|
|
|
997
1041
|
this.getThinkingLevel = getThinkingLevel;
|
|
998
1042
|
this.getMinimalistMetadata = getMinimalistMetadata;
|
|
999
1043
|
this.onMinimalistDecorationChange = onMinimalistDecorationChange;
|
|
1044
|
+
// Pi defines focused as an instance field; observe its public focus lifecycle
|
|
1045
|
+
// without shadowing the native cursor's value with a prototype accessor.
|
|
1046
|
+
let focused = this.focused;
|
|
1047
|
+
Object.defineProperty(this, "focused", {
|
|
1048
|
+
configurable: true,
|
|
1049
|
+
enumerable: true,
|
|
1050
|
+
get: () => focused,
|
|
1051
|
+
set: (value: boolean) => {
|
|
1052
|
+
focused = value;
|
|
1053
|
+
if (!value) this.reportWorkingLineBorder(false);
|
|
1054
|
+
},
|
|
1055
|
+
});
|
|
1000
1056
|
// Resolve subclass prototype overrides before installing the instance adapter.
|
|
1001
1057
|
// Their super call reaches the native prototype directly, so translate once.
|
|
1002
1058
|
const handleMouse = (this as unknown as { handleMouse?: EditorMouseHandler }).handleMouse;
|
|
@@ -1018,20 +1074,42 @@ export class PolishedEditor extends CustomEditor {
|
|
|
1018
1074
|
return this.mouse.baseRendered(width, super.render(width));
|
|
1019
1075
|
}
|
|
1020
1076
|
|
|
1077
|
+
canEmbedWorkingLineBorder(): boolean {
|
|
1078
|
+
return this.focused && this.workingLineBorderCapable;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
private reportWorkingLineBorder = (active: boolean): void => {
|
|
1082
|
+
active = active && this.focused;
|
|
1083
|
+
if (this.workingLineBorderCapable === active) return;
|
|
1084
|
+
this.workingLineBorderCapable = active;
|
|
1085
|
+
this.onWorkingLineBorderCapabilityChange();
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
private renderFallback(lines: string[], width: number): string[] {
|
|
1089
|
+
this.reportWorkingLineBorder(false);
|
|
1090
|
+
return clampRenderedLines(lines, width);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1021
1093
|
render(width: number): string[] {
|
|
1022
|
-
|
|
1094
|
+
try {
|
|
1095
|
+
return this.mouse.rendered(this.renderDecorated(width));
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
this.reportWorkingLineBorder(false);
|
|
1098
|
+
throw error;
|
|
1099
|
+
}
|
|
1023
1100
|
}
|
|
1024
1101
|
|
|
1025
1102
|
private renderDecorated(width: number): string[] {
|
|
1026
1103
|
const config = this.getConfig();
|
|
1027
1104
|
if (!config.components.editor.enabled) {
|
|
1028
1105
|
this.reportMinimalistDecoration(false);
|
|
1029
|
-
return
|
|
1106
|
+
return this.renderFallback(this.renderBase(width), width);
|
|
1030
1107
|
}
|
|
1031
1108
|
if (config.components.editor.style === "accent-rail") {
|
|
1109
|
+
this.reportWorkingLineBorder(false);
|
|
1032
1110
|
this.reportMinimalistDecoration(false);
|
|
1033
1111
|
if (width < ACCENT_RAIL_CHROME_WIDTH + 1) {
|
|
1034
|
-
return
|
|
1112
|
+
return this.renderFallback(this.renderBase(width), width);
|
|
1035
1113
|
}
|
|
1036
1114
|
let captured: { value: string[]; capture?: AutocompleteCapture };
|
|
1037
1115
|
try {
|
|
@@ -1040,7 +1118,7 @@ export class PolishedEditor extends CustomEditor {
|
|
|
1040
1118
|
() => this.renderBase(width - ACCENT_RAIL_CHROME_WIDTH),
|
|
1041
1119
|
);
|
|
1042
1120
|
} catch {
|
|
1043
|
-
return
|
|
1121
|
+
return this.renderFallback(this.renderBase(width), width);
|
|
1044
1122
|
}
|
|
1045
1123
|
try {
|
|
1046
1124
|
const result = renderAccentRailFrameFromBase({
|
|
@@ -1057,12 +1135,12 @@ export class PolishedEditor extends CustomEditor {
|
|
|
1057
1135
|
} catch {
|
|
1058
1136
|
// Decoration is optional; preserve the completed same-render rows below.
|
|
1059
1137
|
}
|
|
1060
|
-
return
|
|
1138
|
+
return this.renderFallback(captured.value, width);
|
|
1061
1139
|
}
|
|
1062
1140
|
if (config.components.editor.style === "minimalist") {
|
|
1063
1141
|
if (width <= 4) {
|
|
1064
1142
|
this.reportMinimalistDecoration(false);
|
|
1065
|
-
return
|
|
1143
|
+
return this.renderFallback(this.renderBase(width), width);
|
|
1066
1144
|
}
|
|
1067
1145
|
let captured: { value: string[]; capture?: AutocompleteCapture };
|
|
1068
1146
|
try {
|
|
@@ -1072,7 +1150,7 @@ export class PolishedEditor extends CustomEditor {
|
|
|
1072
1150
|
);
|
|
1073
1151
|
} catch {
|
|
1074
1152
|
this.reportMinimalistDecoration(false);
|
|
1075
|
-
return
|
|
1153
|
+
return this.renderFallback(this.renderBase(width), width);
|
|
1076
1154
|
}
|
|
1077
1155
|
try {
|
|
1078
1156
|
const result = renderMinimalistFrameFromBase({
|
|
@@ -1083,20 +1161,22 @@ export class PolishedEditor extends CustomEditor {
|
|
|
1083
1161
|
uiTheme: this.uiTheme,
|
|
1084
1162
|
config,
|
|
1085
1163
|
inputText: this.getText(),
|
|
1086
|
-
|
|
1164
|
+
getMetadata: this.getMinimalistMetadata,
|
|
1165
|
+
reportWorkingLineBorder: this.reportWorkingLineBorder,
|
|
1087
1166
|
trustedBaseFrame: true,
|
|
1088
1167
|
borderColor: this.borderColor,
|
|
1089
1168
|
});
|
|
1090
1169
|
this.reportMinimalistDecoration(result.decorated);
|
|
1170
|
+
if (!result.decorated) this.reportWorkingLineBorder(false);
|
|
1091
1171
|
return result.lines;
|
|
1092
1172
|
} catch {
|
|
1093
1173
|
this.reportMinimalistDecoration(false);
|
|
1094
|
-
return
|
|
1174
|
+
return this.renderFallback(captured.value, width);
|
|
1095
1175
|
}
|
|
1096
1176
|
}
|
|
1097
1177
|
this.reportMinimalistDecoration(false);
|
|
1098
1178
|
if (width <= 2) {
|
|
1099
|
-
return
|
|
1179
|
+
return this.renderFallback(this.renderBase(width), width);
|
|
1100
1180
|
}
|
|
1101
1181
|
|
|
1102
1182
|
const shellMode = isShellModeInput(this.getText());
|
|
@@ -1108,7 +1188,7 @@ export class PolishedEditor extends CustomEditor {
|
|
|
1108
1188
|
this.renderBase(innerWidth),
|
|
1109
1189
|
);
|
|
1110
1190
|
} catch {
|
|
1111
|
-
return
|
|
1191
|
+
return this.renderFallback(this.renderBase(width), width);
|
|
1112
1192
|
}
|
|
1113
1193
|
try {
|
|
1114
1194
|
const result = renderPolishedFrame({
|
|
@@ -1119,6 +1199,8 @@ export class PolishedEditor extends CustomEditor {
|
|
|
1119
1199
|
uiTheme: this.uiTheme,
|
|
1120
1200
|
config,
|
|
1121
1201
|
modelMeta: this.getModelMeta(),
|
|
1202
|
+
getWorkingLineFrame: () => this.getMinimalistMetadata().workingLineFrame,
|
|
1203
|
+
reportWorkingLineBorder: this.reportWorkingLineBorder,
|
|
1122
1204
|
thinkingLevel: this.getThinkingLevel(),
|
|
1123
1205
|
shellMode,
|
|
1124
1206
|
trustedBaseFrame: true,
|
|
@@ -1128,7 +1210,7 @@ export class PolishedEditor extends CustomEditor {
|
|
|
1128
1210
|
} catch {
|
|
1129
1211
|
// Decoration is optional; preserve the completed same-render rows below.
|
|
1130
1212
|
}
|
|
1131
|
-
return
|
|
1213
|
+
return this.renderFallback(captured.value, width);
|
|
1132
1214
|
}
|
|
1133
1215
|
}
|
|
1134
1216
|
|
|
@@ -1141,6 +1223,7 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
1141
1223
|
declare readonly setAutocompleteMaxVisible?: (maxVisible: number) => void;
|
|
1142
1224
|
|
|
1143
1225
|
private readonly mouse = new EditorMouseForwarder();
|
|
1226
|
+
private workingLineBorderCapable = false;
|
|
1144
1227
|
|
|
1145
1228
|
constructor(
|
|
1146
1229
|
private readonly base: WrappedEditor,
|
|
@@ -1150,6 +1233,7 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
1150
1233
|
private readonly getThinkingLevel: () => string | undefined,
|
|
1151
1234
|
private readonly getMinimalistMetadata: () => MinimalistEditorMetadata = () => ({ cwd: "" }),
|
|
1152
1235
|
private readonly onMinimalistDecorationChange: (active: boolean) => void = () => {},
|
|
1236
|
+
private readonly onWorkingLineBorderCapabilityChange: () => void = () => {},
|
|
1153
1237
|
) {
|
|
1154
1238
|
if (typeof base.handleMouse === "function") {
|
|
1155
1239
|
this.handleMouse = (event) => {
|
|
@@ -1178,7 +1262,11 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
1178
1262
|
return Boolean(this.base.focused);
|
|
1179
1263
|
}
|
|
1180
1264
|
set focused(value: boolean) {
|
|
1181
|
-
|
|
1265
|
+
try {
|
|
1266
|
+
this.base.focused = value;
|
|
1267
|
+
} finally {
|
|
1268
|
+
if (!value) this.reportWorkingLineBorder(false);
|
|
1269
|
+
}
|
|
1182
1270
|
}
|
|
1183
1271
|
|
|
1184
1272
|
get borderColor(): ((str: string) => string) | undefined {
|
|
@@ -1259,20 +1347,42 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
1259
1347
|
return this.mouse.baseRendered(width, this.base.render(width));
|
|
1260
1348
|
}
|
|
1261
1349
|
|
|
1350
|
+
canEmbedWorkingLineBorder(): boolean {
|
|
1351
|
+
return this.focused && this.workingLineBorderCapable;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
private reportWorkingLineBorder = (active: boolean): void => {
|
|
1355
|
+
active = active && this.focused;
|
|
1356
|
+
if (this.workingLineBorderCapable === active) return;
|
|
1357
|
+
this.workingLineBorderCapable = active;
|
|
1358
|
+
this.onWorkingLineBorderCapabilityChange();
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
private renderFallback(lines: string[], width: number): string[] {
|
|
1362
|
+
this.reportWorkingLineBorder(false);
|
|
1363
|
+
return clampRenderedLines(lines, width);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1262
1366
|
render(width: number): string[] {
|
|
1263
|
-
|
|
1367
|
+
try {
|
|
1368
|
+
return this.mouse.rendered(this.renderDecorated(width));
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
this.reportWorkingLineBorder(false);
|
|
1371
|
+
throw error;
|
|
1372
|
+
}
|
|
1264
1373
|
}
|
|
1265
1374
|
|
|
1266
1375
|
private renderDecorated(width: number): string[] {
|
|
1267
1376
|
const config = this.getConfig();
|
|
1268
1377
|
if (!config.components.editor.enabled) {
|
|
1269
1378
|
this.reportMinimalistDecoration(false);
|
|
1270
|
-
return
|
|
1379
|
+
return this.renderFallback(this.renderBase(width), width);
|
|
1271
1380
|
}
|
|
1272
1381
|
if (config.components.editor.style === "accent-rail") {
|
|
1382
|
+
this.reportWorkingLineBorder(false);
|
|
1273
1383
|
this.reportMinimalistDecoration(false);
|
|
1274
1384
|
if (width < ACCENT_RAIL_CHROME_WIDTH + 1) {
|
|
1275
|
-
return
|
|
1385
|
+
return this.renderFallback(this.renderBase(width), width);
|
|
1276
1386
|
}
|
|
1277
1387
|
let captured: { value: string[]; capture?: AutocompleteCapture };
|
|
1278
1388
|
try {
|
|
@@ -1280,7 +1390,7 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
1280
1390
|
this.renderBase(width - ACCENT_RAIL_CHROME_WIDTH),
|
|
1281
1391
|
);
|
|
1282
1392
|
} catch {
|
|
1283
|
-
return
|
|
1393
|
+
return this.renderFallback(this.renderBase(width), width);
|
|
1284
1394
|
}
|
|
1285
1395
|
try {
|
|
1286
1396
|
const provenance = inspectPolishedFrameProvenance(
|
|
@@ -1305,12 +1415,12 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
1305
1415
|
} catch {
|
|
1306
1416
|
// Decoration is optional; preserve the completed same-render rows below.
|
|
1307
1417
|
}
|
|
1308
|
-
return
|
|
1418
|
+
return this.renderFallback(captured.value, width);
|
|
1309
1419
|
}
|
|
1310
1420
|
if (config.components.editor.style === "minimalist") {
|
|
1311
1421
|
if (width <= 4) {
|
|
1312
1422
|
this.reportMinimalistDecoration(false);
|
|
1313
|
-
return
|
|
1423
|
+
return this.renderFallback(this.renderBase(width), width);
|
|
1314
1424
|
}
|
|
1315
1425
|
let captured: { value: string[]; capture?: AutocompleteCapture };
|
|
1316
1426
|
try {
|
|
@@ -1319,7 +1429,7 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
1319
1429
|
);
|
|
1320
1430
|
} catch {
|
|
1321
1431
|
this.reportMinimalistDecoration(false);
|
|
1322
|
-
return
|
|
1432
|
+
return this.renderFallback(this.renderBase(width), width);
|
|
1323
1433
|
}
|
|
1324
1434
|
try {
|
|
1325
1435
|
const provenance = inspectPolishedFrameProvenance(
|
|
@@ -1337,7 +1447,8 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
1337
1447
|
uiTheme: this.uiTheme,
|
|
1338
1448
|
config,
|
|
1339
1449
|
inputText: this.base.getText(),
|
|
1340
|
-
|
|
1450
|
+
getMetadata: this.getMinimalistMetadata,
|
|
1451
|
+
reportWorkingLineBorder: this.reportWorkingLineBorder,
|
|
1341
1452
|
ownedFrame: provenance.ownedFrame,
|
|
1342
1453
|
borderColor: this.borderColor,
|
|
1343
1454
|
});
|
|
@@ -1350,10 +1461,10 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
1350
1461
|
// Decoration is optional; preserve the completed same-render rows below.
|
|
1351
1462
|
}
|
|
1352
1463
|
this.reportMinimalistDecoration(false);
|
|
1353
|
-
return
|
|
1464
|
+
return this.renderFallback(captured.value, width);
|
|
1354
1465
|
}
|
|
1355
1466
|
this.reportMinimalistDecoration(false);
|
|
1356
|
-
if (width <= 2) return
|
|
1467
|
+
if (width <= 2) return this.renderFallback(this.renderBase(width), width);
|
|
1357
1468
|
|
|
1358
1469
|
const shellMode = isShellModeInput(this.base.getText());
|
|
1359
1470
|
const { railWidth } = getEditorChromeWidths(config, this.uiTheme, "\x1b[0m", shellMode);
|
|
@@ -1362,7 +1473,7 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
1362
1473
|
try {
|
|
1363
1474
|
captured = renderWithAutocompleteCapture(this.base, () => this.renderBase(innerWidth));
|
|
1364
1475
|
} catch {
|
|
1365
|
-
return
|
|
1476
|
+
return this.renderFallback(this.renderBase(width), width);
|
|
1366
1477
|
}
|
|
1367
1478
|
let result: PolishedFrameResult | undefined;
|
|
1368
1479
|
try {
|
|
@@ -1381,6 +1492,8 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
1381
1492
|
uiTheme: this.uiTheme,
|
|
1382
1493
|
config,
|
|
1383
1494
|
modelMeta: this.getModelMeta(),
|
|
1495
|
+
getWorkingLineFrame: () => this.getMinimalistMetadata().workingLineFrame,
|
|
1496
|
+
reportWorkingLineBorder: this.reportWorkingLineBorder,
|
|
1384
1497
|
thinkingLevel: this.getThinkingLevel(),
|
|
1385
1498
|
rightStatus: readVimStatus(this.base, this.uiTheme),
|
|
1386
1499
|
shellMode,
|
|
@@ -1392,7 +1505,7 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
1392
1505
|
// Decoration is optional; preserve the completed same-render rows below.
|
|
1393
1506
|
}
|
|
1394
1507
|
if (result?.decorated) return result.lines;
|
|
1395
|
-
return
|
|
1508
|
+
return this.renderFallback(captured.value, width);
|
|
1396
1509
|
}
|
|
1397
1510
|
|
|
1398
1511
|
invalidate(): void {
|
|
@@ -59,6 +59,7 @@ type GraphemeCell = { text: string; start: number; width: number };
|
|
|
59
59
|
type WorkingLineUi = {
|
|
60
60
|
setWorkingMessage(message?: string): void;
|
|
61
61
|
setWorkingIndicator(options?: { frames?: string[]; intervalMs?: number }): void;
|
|
62
|
+
setWorkingVisible?(visible: boolean): void;
|
|
62
63
|
};
|
|
63
64
|
|
|
64
65
|
type WorkingLineContext = {
|
|
@@ -1144,6 +1145,13 @@ export class WorkingLineController {
|
|
|
1144
1145
|
private ownsIndicator = false;
|
|
1145
1146
|
private ownsMessage = false;
|
|
1146
1147
|
private agentActive = false;
|
|
1148
|
+
private animationTimer?: ReturnType<typeof setInterval>;
|
|
1149
|
+
private animationIntervalMs: number | undefined;
|
|
1150
|
+
private hiddenWorkingVisible = false;
|
|
1151
|
+
private restoreWorkingVisible: (() => void) | undefined;
|
|
1152
|
+
private installedUi: WorkingLineUi | undefined;
|
|
1153
|
+
private placementContext: WorkingLineContext | undefined;
|
|
1154
|
+
private requestRender: () => void = () => {};
|
|
1147
1155
|
private selectedMessage: string | undefined;
|
|
1148
1156
|
private frameKey: string | undefined;
|
|
1149
1157
|
private installedPhase: InstalledAnimationPhase | undefined;
|
|
@@ -1172,9 +1180,11 @@ export class WorkingLineController {
|
|
|
1172
1180
|
private readonly getThought: () => WorkingLineRuntimeSegments["thought"] = () => this.thought,
|
|
1173
1181
|
private readonly onUnavailable: () => void = () => {},
|
|
1174
1182
|
private readonly metricScheduler: WorkingLineMetricScheduler = defaultMetricScheduler,
|
|
1183
|
+
private readonly canEmbedBorder: (ctx: WorkingLineContext) => boolean = () => false,
|
|
1175
1184
|
) {}
|
|
1176
1185
|
|
|
1177
1186
|
startSession(ctx: WorkingLineContext): WorkingLineReconcileResult {
|
|
1187
|
+
this.reset(ctx);
|
|
1178
1188
|
this.clearRuntime();
|
|
1179
1189
|
this.selectedMessage = undefined;
|
|
1180
1190
|
this.installedPhase = undefined;
|
|
@@ -1190,6 +1200,7 @@ export class WorkingLineController {
|
|
|
1190
1200
|
// stored indicator so the subsequently constructed Loader begins at intended frame zero.
|
|
1191
1201
|
this.install(ctx, true, true);
|
|
1192
1202
|
}
|
|
1203
|
+
this.reconcilePlacement(ctx);
|
|
1193
1204
|
this.reconcileElapsedUpdates(ctx);
|
|
1194
1205
|
}
|
|
1195
1206
|
|
|
@@ -1254,9 +1265,12 @@ export class WorkingLineController {
|
|
|
1254
1265
|
|
|
1255
1266
|
finishAgent(ctx: WorkingLineContext): void {
|
|
1256
1267
|
this.agentActive = false;
|
|
1268
|
+
this.stopAnimationTicks();
|
|
1269
|
+
this.releaseWorkingVisibility();
|
|
1257
1270
|
this.activeTools.clear();
|
|
1258
1271
|
this.deactivateElapsedUpdates();
|
|
1259
1272
|
this.updateIndicator(ctx);
|
|
1273
|
+
this.requestRender();
|
|
1260
1274
|
}
|
|
1261
1275
|
|
|
1262
1276
|
flushMetrics(
|
|
@@ -1303,6 +1317,26 @@ export class WorkingLineController {
|
|
|
1303
1317
|
return this.selectedMessage;
|
|
1304
1318
|
}
|
|
1305
1319
|
|
|
1320
|
+
setRequestRender(requestRender: () => void): void {
|
|
1321
|
+
this.requestRender = requestRender;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/** Only the currently capable, owned border renderer may consume this frame. */
|
|
1325
|
+
currentWorkingLineFrame(): string | undefined {
|
|
1326
|
+
if (!this.placementContext || !this.reconcilePlacement(this.placementContext)) return undefined;
|
|
1327
|
+
const frames = this.installedIndicatorOptions?.frames;
|
|
1328
|
+
if (!frames || frames.length === 0) {
|
|
1329
|
+
return this.selectedMessage;
|
|
1330
|
+
}
|
|
1331
|
+
if (!this.installedPhase || this.installedPhase.frameStates.length === 0) {
|
|
1332
|
+
return frames[0];
|
|
1333
|
+
}
|
|
1334
|
+
const interval = this.installedPhase.intervalMs > 0 ? this.installedPhase.intervalMs : 100;
|
|
1335
|
+
const elapsedMs = Math.max(0, this.now() - this.installedPhase.frameEpochMs);
|
|
1336
|
+
const frameIndex = Math.max(0, Math.floor(elapsedMs / interval)) % frames.length;
|
|
1337
|
+
return frames[frameIndex] ?? this.selectedMessage;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1306
1340
|
/** Whether this controller currently claims both required public Working-row surfaces. */
|
|
1307
1341
|
isAvailable(): boolean {
|
|
1308
1342
|
return (
|
|
@@ -1321,12 +1355,14 @@ export class WorkingLineController {
|
|
|
1321
1355
|
): WorkingLineReconcileResult {
|
|
1322
1356
|
this.cancelMetricIndicator();
|
|
1323
1357
|
const ui = workingLineUi(ctx);
|
|
1358
|
+
if (this.installedUi && this.installedUi !== ui) this.reset(ctx);
|
|
1324
1359
|
if (!ui) {
|
|
1325
|
-
this.
|
|
1326
|
-
this.deactivateElapsedUpdates();
|
|
1360
|
+
this.reset(ctx);
|
|
1327
1361
|
this.invalidateUnavailableExtensionSegments();
|
|
1328
1362
|
return { applied: false, reason: "Working line requires a newer Pi TUI" };
|
|
1329
1363
|
}
|
|
1364
|
+
this.installedUi = ui;
|
|
1365
|
+
this.placementContext = ctx;
|
|
1330
1366
|
const rootConfig = this.getConfig();
|
|
1331
1367
|
const config = rootConfig.components.workingLine;
|
|
1332
1368
|
const nextMessage =
|
|
@@ -1339,6 +1375,7 @@ export class WorkingLineController {
|
|
|
1339
1375
|
}
|
|
1340
1376
|
this.applyIndicator(ui, rootConfig, nextMessage, forceIndicator, rebasePhase);
|
|
1341
1377
|
this.selectedMessage = nextMessage;
|
|
1378
|
+
this.reconcilePlacement(ctx);
|
|
1342
1379
|
this.reconcileElapsedUpdates(ctx);
|
|
1343
1380
|
return { applied: true };
|
|
1344
1381
|
} catch {
|
|
@@ -1471,12 +1508,22 @@ export class WorkingLineController {
|
|
|
1471
1508
|
private updateIndicator(ctx: WorkingLineContext): boolean {
|
|
1472
1509
|
this.cancelMetricIndicator();
|
|
1473
1510
|
const rootConfig = this.getConfig();
|
|
1474
|
-
if (!rootConfig.components.workingLine.enabled
|
|
1511
|
+
if (!rootConfig.components.workingLine.enabled) {
|
|
1512
|
+
this.reset(ctx);
|
|
1513
|
+
return false;
|
|
1514
|
+
}
|
|
1475
1515
|
const ui = workingLineUi(ctx);
|
|
1476
|
-
if (!ui)
|
|
1516
|
+
if (!ui || this.installedUi !== ui) {
|
|
1517
|
+
this.reset(ctx);
|
|
1518
|
+
this.invalidateUnavailableExtensionSegments();
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1521
|
+
if (!this.installed) return false;
|
|
1522
|
+
this.placementContext = ctx;
|
|
1477
1523
|
const snapshot = this.installationSnapshot();
|
|
1478
1524
|
try {
|
|
1479
1525
|
this.applyIndicator(ui, rootConfig);
|
|
1526
|
+
this.reconcilePlacement(ctx);
|
|
1480
1527
|
return !this.extensionSegmentsDirty;
|
|
1481
1528
|
} catch {
|
|
1482
1529
|
// A transient last-writer/public-API failure must not break the agent turn.
|
|
@@ -1527,7 +1574,86 @@ export class WorkingLineController {
|
|
|
1527
1574
|
this.metricUpdateScheduled = false;
|
|
1528
1575
|
this.metricUpdateHandle = undefined;
|
|
1529
1576
|
}
|
|
1577
|
+
/** Placement is independent of the cached row contents and never changes user choices. */
|
|
1578
|
+
private reconcilePlacement(ctx: WorkingLineContext): boolean {
|
|
1579
|
+
let canEmbed = false;
|
|
1580
|
+
const ui = this.installedUi;
|
|
1581
|
+
try {
|
|
1582
|
+
canEmbed = Boolean(
|
|
1583
|
+
this.agentActive &&
|
|
1584
|
+
this.isAvailable() &&
|
|
1585
|
+
this.getConfig().components.workingLine.placement === "border" &&
|
|
1586
|
+
ui &&
|
|
1587
|
+
workingLineUi(ctx) === ui &&
|
|
1588
|
+
typeof ui.setWorkingVisible === "function" &&
|
|
1589
|
+
this.canEmbedBorder(ctx),
|
|
1590
|
+
);
|
|
1591
|
+
} catch {
|
|
1592
|
+
// Capability probes, including predecessor editor getters, must fail open.
|
|
1593
|
+
}
|
|
1594
|
+
if (!canEmbed || !ui) {
|
|
1595
|
+
this.stopAnimationTicks();
|
|
1596
|
+
this.releaseWorkingVisibility();
|
|
1597
|
+
return false;
|
|
1598
|
+
}
|
|
1599
|
+
if (!this.hiddenWorkingVisible) {
|
|
1600
|
+
// A failed restoration remains attached to its original UI, even after reset.
|
|
1601
|
+
this.releaseWorkingVisibility();
|
|
1602
|
+
if (this.restoreWorkingVisible) return false;
|
|
1603
|
+
try {
|
|
1604
|
+
const setVisible = ui.setWorkingVisible;
|
|
1605
|
+
if (typeof setVisible !== "function") return false;
|
|
1606
|
+
this.restoreWorkingVisible = () => setVisible.call(ui, true);
|
|
1607
|
+
setVisible.call(ui, false);
|
|
1608
|
+
this.hiddenWorkingVisible = true;
|
|
1609
|
+
} catch {
|
|
1610
|
+
// A throwing setter may already have hidden the row. Restore without claiming it.
|
|
1611
|
+
this.stopAnimationTicks();
|
|
1612
|
+
this.releaseWorkingVisibility();
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
this.startAnimationTicks();
|
|
1617
|
+
return true;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
private releaseWorkingVisibility(): void {
|
|
1621
|
+
this.hiddenWorkingVisible = false;
|
|
1622
|
+
if (!this.restoreWorkingVisible) return;
|
|
1623
|
+
try {
|
|
1624
|
+
this.restoreWorkingVisible();
|
|
1625
|
+
this.restoreWorkingVisible = undefined;
|
|
1626
|
+
} catch {
|
|
1627
|
+
// Keep the original setter for a later cleanup attempt, even without row ownership.
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
private startAnimationTicks(): void {
|
|
1632
|
+
const interval = this.installedPhase?.intervalMs;
|
|
1633
|
+
if (!interval || interval <= 0) {
|
|
1634
|
+
this.stopAnimationTicks();
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
if (this.animationTimer !== undefined && this.animationIntervalMs === interval) return;
|
|
1638
|
+
this.stopAnimationTicks();
|
|
1639
|
+
this.animationIntervalMs = interval;
|
|
1640
|
+
this.animationTimer = setInterval(() => {
|
|
1641
|
+
const ctx = this.placementContext;
|
|
1642
|
+
if (!ctx || !this.reconcilePlacement(ctx)) {
|
|
1643
|
+
this.stopAnimationTicks();
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
this.requestRender();
|
|
1647
|
+
}, interval);
|
|
1648
|
+
}
|
|
1530
1649
|
|
|
1650
|
+
private stopAnimationTicks(): void {
|
|
1651
|
+
if (this.animationTimer !== undefined) {
|
|
1652
|
+
clearInterval(this.animationTimer);
|
|
1653
|
+
this.animationTimer = undefined;
|
|
1654
|
+
}
|
|
1655
|
+
this.animationIntervalMs = undefined;
|
|
1656
|
+
}
|
|
1531
1657
|
private reconcileElapsedUpdates(ctx: WorkingLineContext): void {
|
|
1532
1658
|
this.elapsedUpdatesContext = ctx;
|
|
1533
1659
|
const config = this.getConfig().components.workingLine;
|
|
@@ -1594,6 +1720,7 @@ export class WorkingLineController {
|
|
|
1594
1720
|
try {
|
|
1595
1721
|
ui.setWorkingIndicator(snapshot.indicatorOptions);
|
|
1596
1722
|
if (snapshot.ownsMessage) ui.setWorkingMessage("");
|
|
1723
|
+
if (this.placementContext) this.reconcilePlacement(this.placementContext);
|
|
1597
1724
|
} catch {
|
|
1598
1725
|
// Recovery is deliberately direct rather than recursive. If either setter cannot
|
|
1599
1726
|
// restore the last successful public state, release both unkeyed surfaces.
|
|
@@ -1611,7 +1738,10 @@ export class WorkingLineController {
|
|
|
1611
1738
|
}
|
|
1612
1739
|
|
|
1613
1740
|
private releaseAfterFailure(ui: WorkingLineUi): void {
|
|
1741
|
+
this.cancelMetricIndicator();
|
|
1614
1742
|
this.deactivateElapsedUpdates();
|
|
1743
|
+
this.stopAnimationTicks();
|
|
1744
|
+
this.releaseWorkingVisibility();
|
|
1615
1745
|
if (this.ownsIndicator) {
|
|
1616
1746
|
try {
|
|
1617
1747
|
ui.setWorkingIndicator();
|
|
@@ -1632,14 +1762,17 @@ export class WorkingLineController {
|
|
|
1632
1762
|
this.frameKey = undefined;
|
|
1633
1763
|
this.installedPhase = undefined;
|
|
1634
1764
|
this.installedIndicatorOptions = undefined;
|
|
1765
|
+
this.installedUi = undefined;
|
|
1766
|
+
this.placementContext = undefined;
|
|
1635
1767
|
this.invalidateUnavailableExtensionSegments();
|
|
1636
1768
|
}
|
|
1637
1769
|
|
|
1638
1770
|
private reset(ctx: WorkingLineContext): void {
|
|
1639
1771
|
this.cancelMetricIndicator();
|
|
1640
1772
|
this.deactivateElapsedUpdates();
|
|
1641
|
-
|
|
1642
|
-
|
|
1773
|
+
this.stopAnimationTicks();
|
|
1774
|
+
this.releaseWorkingVisibility();
|
|
1775
|
+
const ui = this.installedUi ?? workingLineUi(ctx);
|
|
1643
1776
|
if (ui && this.ownsIndicator) {
|
|
1644
1777
|
try {
|
|
1645
1778
|
ui.setWorkingIndicator();
|
|
@@ -1660,6 +1793,8 @@ export class WorkingLineController {
|
|
|
1660
1793
|
this.frameKey = undefined;
|
|
1661
1794
|
this.installedPhase = undefined;
|
|
1662
1795
|
this.installedIndicatorOptions = undefined;
|
|
1796
|
+
this.installedUi = undefined;
|
|
1797
|
+
this.placementContext = undefined;
|
|
1663
1798
|
}
|
|
1664
1799
|
|
|
1665
1800
|
private clearRuntime(): void {
|