pi-zentui 0.22.0 → 0.22.2
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 +8 -6
- package/extensions/zentui/settings-command.ts +16 -19
- package/extensions/zentui/settings-previews.ts +27 -10
- package/extensions/zentui/thinking-experimental.ts +295 -84
- package/extensions/zentui/thinking-status.ts +69 -0
- package/extensions/zentui/ui.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ pi install git:github.com/lmilojevicc/pi-zentui
|
|
|
89
89
|
|
|
90
90
|
## Configure
|
|
91
91
|
|
|
92
|
-
Run `/zentui` inside Pi to configure Appearance, Editor, User messages, Thinking (Experimental), Working line, Footer, Segments, Git, and Extensions. Use `Tab` and `Shift+Tab` to switch sections. Most changes apply live
|
|
92
|
+
Run `/zentui` inside Pi to configure Appearance, Editor, User messages, Thinking (Experimental), Working line, Footer, Segments, Git, and Extensions. Use `Tab` and `Shift+Tab` to switch sections. Most changes apply live. 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 a live disable require restarting Pi. Configuration is saved to:
|
|
93
93
|
|
|
94
94
|
```text
|
|
95
95
|
~/.pi/agent/zentui.json
|
|
@@ -144,13 +144,13 @@ Useful shortcuts:
|
|
|
144
144
|
/zentui format clear
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
**Thinking (Experimental)** uses one
|
|
147
|
+
**Thinking (Experimental)** uses one private `AssistantMessageComponent` renderer for Rail, Tree, and Streaming, tested against exact Pi versions 0.80.5, 0.82.1, 0.83.0, 0.84.0, and 0.84.4. It is disabled by default and may break after Pi updates. Zentui installs an enabled startup mode before transcript restoration. A healthy installed controller lets active Streaming switch live to Rail or Tree, and lets Rail and Tree switch live between each other, without reinstalling its patch. Entering Streaming from Rail or Tree saves the choice but keeps the active structural mode until restart. Disabling live restores native thinking and releases Streaming resources. First enable and re-enable after a live disable are also restart-gated. Mode changes while disabled only preconfigure the next enable. Startup failures, missing constructors, incompatible private child layouts, parser limits, theme/render/width errors, and displaced patch ownership fail open to complete native thinking. If cleanup throws while leaving Streaming, disabling still restores native thinking and a structural selection still becomes active; the successful change warns that Streaming is unavailable for the rest of the session.
|
|
148
148
|
|
|
149
|
-
Rail shows every parsed label in each native contiguous thinking run (`│ Label`, with only the open final phase shown as `│ • Label`). Tree independently shows the latest five labels in each run (`├─ · Label`, settled `└─ · Label`, open `└─ • Label`); it never aggregates across intervening text or tool blocks. Labels are rendered by fresh host-shaped Pi Markdown instances before cropping, so emphasis, code, links, HTML, LaTeX, custom transforms, and native `thinkingText` styling remain host-controlled. Every label occupies one terminal row: ANSI/OSC/grapheme-aware cropping adds `…` only when needed. Native horizontal padding stays external. Connectors are styled directly with the current theme's `accent` callback on every render, so custom themes control them independently. Hidden native thinking remains hidden and keeps Pi's native hidden label.
|
|
149
|
+
Rail shows every parsed label in each native contiguous thinking run (`│ Label`, with only the open final phase shown as `│ • Label`). Tree independently shows the latest five labels in each run (`├─ · Label`, settled `└─ · Label`, open `└─ • Label`); it never aggregates across intervening text or tool blocks. Rail and Tree follow Pi's thinking visibility, and unsafe or unstructured content remains native. Labels are rendered by fresh host-shaped Pi Markdown instances before cropping, so emphasis, code, links, HTML, LaTeX, custom transforms, and native `thinkingText` styling remain host-controlled. Every label occupies one terminal row: ANSI/OSC/grapheme-aware cropping adds `…` only when needed. Native horizontal padding stays external. Connectors are styled directly with the current theme's `accent` callback on every render, so custom themes control them independently. Hidden native thinking remains hidden and keeps Pi's native hidden label.
|
|
150
150
|
|
|
151
|
-
Streaming retains Pi's host-rendered final five rows under `Thinking 7.1s`, folds completed reasoning under `Thought` or current-session `Thought for 12.3s`, and owns the configured thinking-toggle binding (Ctrl+T by default) only for an
|
|
151
|
+
Streaming retains Pi's host-rendered final five rows under `Thinking 7.1s`, folds completed reasoning under `Thought` or current-session `Thought for 12.3s`, and owns the configured thinking-toggle binding (Ctrl+T by default) only when started in Streaming. Its input listener and timer are acquired only for an enabled Streaming session start; startup acquisition failure uses native thinking and marks Streaming unavailable. Restored completions cannot recover a duration because Pi does not persist the thinking-end timestamp. Expand/refold and lifecycle tracking are bounded to 256 retained assistant components; evicted entries are first restored natively. All modes restore/dispose on shutdown. Thinking (Experimental) never writes the Working line and does not change its existing **Thinking time** option, working text, Footer, Editor, statuses, or model behavior.
|
|
152
152
|
|
|
153
|
-
Pi 0.84 also provides a native fullscreen TUI with a sticky editor and Footer. Zentui does not enable
|
|
153
|
+
Pi 0.84 also provides a native fullscreen TUI with a sticky editor and Footer. Pi 0.84.4 is covered by a fullscreen live-transition PTY smoke in addition to the standard matrix. Zentui does not enable fullscreen automatically; select it from Pi's `/settings`, set `"tuiMode": "fullscreen"` in Pi settings, or launch Pi with `--tui-mode fullscreen`.
|
|
154
154
|
|
|
155
155
|
## Requirements
|
|
156
156
|
|
|
@@ -192,8 +192,10 @@ See [CONTRIBUTING.md](https://github.com/lmilojevicc/pi-zentui/blob/main/CONTRIB
|
|
|
192
192
|
- [Opencode](https://github.com/anomalyco/opencode) — inspiration for the Opencode editor treatment
|
|
193
193
|
- [Oh My Pi (`omp`)](https://github.com/can1357/oh-my-pi) by [Can Bölük](https://github.com/can1357) — visual inspiration for the filled, single-left-rail Accent Rail editor
|
|
194
194
|
- [Pi Custom Input](https://github.com/VinhLe1410/pi-custom-input) by [Vinh Le](https://github.com/VinhLe1410) — visual inspiration for Minimalist's framed, border-embedded session, model, context, Git, and path metadata
|
|
195
|
+
- [Pi Thinking Steps](https://github.com/crustyhacker/pi-thinking-steps) by Marc Mironescu / FluxGear — structural-step parsing and the Rail/Tree visual language; adapted in Thinking (Experimental) under the MIT License
|
|
196
|
+
- [Pi Thinking Fold](https://github.com/99percentpeople/pi-extensions/tree/master/extensions/thinking-fold) by [Zach Yuen](https://github.com/99percentpeople) — native rendered-row folding, timing, expand/refold behavior, and fail-open compatibility patterns; adapted in Thinking (Experimental) under the MIT License
|
|
195
197
|
|
|
196
|
-
Zentui
|
|
198
|
+
Most Zentui implementations are independent; these credits acknowledge product and visual inspiration. Thinking (Experimental) also adapts MIT-licensed implementation work from Pi Thinking Steps and Pi Thinking Fold. Their complete copyright and permission notices are retained in the packaged [`thinking-experimental.ts`](./extensions/zentui/thinking-experimental.ts) source.
|
|
197
199
|
|
|
198
200
|
## License
|
|
199
201
|
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
SETTINGS_PREVIEW_MAX_WIDTH,
|
|
64
64
|
} from "./settings-previews";
|
|
65
65
|
import { EDITOR_BORDER_STYLE, renderChromeBorder, safeThemeFg } from "./style";
|
|
66
|
+
import { formatThinkingStatus, thinkingStatusLabels } from "./thinking-status";
|
|
66
67
|
import {
|
|
67
68
|
buildWorkingLinePreviewFrames,
|
|
68
69
|
normalizeWorkingLineMessages,
|
|
@@ -140,7 +141,11 @@ const thinkingStepsModeLabels: Record<ThinkingStepsMode, string> = {
|
|
|
140
141
|
tree: "Tree",
|
|
141
142
|
streaming: "Streaming",
|
|
142
143
|
};
|
|
143
|
-
const thinkingStepsModeValues =
|
|
144
|
+
const thinkingStepsModeValues = (startupMode: ThinkingStepsMode): string[] => {
|
|
145
|
+
if (startupMode === "streaming") return ["Streaming", "Tree", "Rail"];
|
|
146
|
+
if (startupMode === "rail") return ["Rail", "Tree", "Streaming"];
|
|
147
|
+
return ["Tree", "Rail", "Streaming"];
|
|
148
|
+
};
|
|
144
149
|
|
|
145
150
|
const settingsSections = [
|
|
146
151
|
"appearance",
|
|
@@ -176,6 +181,9 @@ type SettingsOutcome =
|
|
|
176
181
|
|
|
177
182
|
type ThinkingControllerState = Readonly<{
|
|
178
183
|
available: boolean;
|
|
184
|
+
rendererAvailable?: boolean;
|
|
185
|
+
streamingAvailable?: boolean;
|
|
186
|
+
streamingPoisoned?: boolean;
|
|
179
187
|
active: boolean;
|
|
180
188
|
activeMode?: ThinkingStepsMode;
|
|
181
189
|
startup: Readonly<ThinkingStepsComponentConfig>;
|
|
@@ -665,20 +673,8 @@ function buildThinkingStepsItems(
|
|
|
665
673
|
): SettingItem[] {
|
|
666
674
|
const thinkingSteps = config.components.thinkingSteps;
|
|
667
675
|
const controller = experimentalThinkingCapability(capability);
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
: "Active startup: native thinking";
|
|
671
|
-
const savedLabel = thinkingSteps.enabled
|
|
672
|
-
? `Saved: ${thinkingStepsModeLabels[thinkingSteps.mode]}`
|
|
673
|
-
: "Saved: disabled";
|
|
674
|
-
const status = !controller.available
|
|
675
|
-
? `${controller.reason ?? "Private renderer unavailable"}; using native thinking.`
|
|
676
|
-
: controller.restartRequired
|
|
677
|
-
? `${savedLabel}; ${startupLabel}. Restart Pi to apply.`
|
|
678
|
-
: controller.active
|
|
679
|
-
? `${startupLabel}.`
|
|
680
|
-
: `${savedLabel}; using native thinking.`;
|
|
681
|
-
const enabledDescription = `${status} Private renderer; every enable, disable, or mode change requires restart and may break after Pi updates.`;
|
|
676
|
+
const status = formatThinkingStatus(thinkingStatusLabels(thinkingSteps, controller));
|
|
677
|
+
const enabledDescription = `${status} Live switching supports Streaming → Rail/Tree and Rail ↔ Tree. Entering Streaming, first enable, and re-enable after live disable require restart. Private renderer may break after Pi updates.`;
|
|
682
678
|
return [
|
|
683
679
|
{
|
|
684
680
|
id: "thinkingStepsEnabled",
|
|
@@ -690,9 +686,9 @@ function buildThinkingStepsItems(
|
|
|
690
686
|
{
|
|
691
687
|
id: "thinkingStepsMode",
|
|
692
688
|
label: "Mode",
|
|
693
|
-
description: `${status} Rail shows every parsed label; Tree shows the latest five per contiguous run; Streaming folds to the latest five host-rendered rows and owns the configured thinking toggle only when active. Incompatibility uses native thinking.`,
|
|
689
|
+
description: `${status} Rail shows every parsed label; Tree shows the latest five per contiguous run; both follow Pi thinking visibility, and unsafe or unstructured content remains native. Streaming folds to the latest five host-rendered rows and owns the configured thinking toggle only when active. Incompatibility uses native thinking.`,
|
|
694
690
|
currentValue: thinkingStepsModeLabels[thinkingSteps.mode],
|
|
695
|
-
values: thinkingStepsModeValues,
|
|
691
|
+
values: thinkingStepsModeValues(controller.startup.mode),
|
|
696
692
|
},
|
|
697
693
|
];
|
|
698
694
|
}
|
|
@@ -1285,9 +1281,10 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
|
|
|
1285
1281
|
};
|
|
1286
1282
|
const notifyChange = (label: string, value: string, result?: ApplyResult) => {
|
|
1287
1283
|
deps.requestRender();
|
|
1284
|
+
const detail = result?.reason?.trim();
|
|
1288
1285
|
ctx.ui.notify(
|
|
1289
|
-
`${label}: ${value}${result && !result.applied ?
|
|
1290
|
-
"info",
|
|
1286
|
+
`${label}: ${value}${detail ? ` (${detail})` : result && !result.applied ? " (reload Pi to apply this change)" : ""}`,
|
|
1287
|
+
result && (!result.applied || detail) ? "warning" : "info",
|
|
1291
1288
|
);
|
|
1292
1289
|
startPreview();
|
|
1293
1290
|
tui.requestRender();
|
|
@@ -6,6 +6,7 @@ import { sanitizeEditorMetadataText } from "./editor-metadata-format";
|
|
|
6
6
|
import { renderMinimalistFrame } from "./minimalist-editor";
|
|
7
7
|
import { safeThemeFg } from "./style";
|
|
8
8
|
import { createThinkingStepsRows } from "./thinking-experimental";
|
|
9
|
+
import { formatThinkingStatus, thinkingStatusLabels } from "./thinking-status";
|
|
9
10
|
import { parseThinkingSteps } from "./thinking-steps";
|
|
10
11
|
import { renderPolishedEditorFrame } from "./ui";
|
|
11
12
|
import { makeMarkdownTheme, renderUserMessageStyle } from "./user-message-styles";
|
|
@@ -25,6 +26,9 @@ export const THINKING_STEPS_PREVIEW_MARKDOWN = [
|
|
|
25
26
|
].join("\n");
|
|
26
27
|
type ThinkingPreviewState = Readonly<{
|
|
27
28
|
available: boolean;
|
|
29
|
+
rendererAvailable?: boolean;
|
|
30
|
+
streamingAvailable?: boolean;
|
|
31
|
+
streamingPoisoned?: boolean;
|
|
28
32
|
active: boolean;
|
|
29
33
|
activeMode?: "rail" | "tree" | "streaming";
|
|
30
34
|
startup: Readonly<{ enabled: boolean; mode: "rail" | "tree" | "streaming" }>;
|
|
@@ -37,6 +41,23 @@ type ThinkingPreviewCapability =
|
|
|
37
41
|
| Readonly<{ available: boolean }>
|
|
38
42
|
| Readonly<{ readonly state: ThinkingPreviewState }>;
|
|
39
43
|
|
|
44
|
+
function wrapPreviewText(text: string, width: number): string[] {
|
|
45
|
+
if (width <= 0) return [];
|
|
46
|
+
const rows: string[] = [];
|
|
47
|
+
for (const fact of text.split(" · ")) {
|
|
48
|
+
const candidate = rows.length > 0 ? `${rows.at(-1)} · ${fact}` : fact;
|
|
49
|
+
if (visibleWidth(candidate) <= width) {
|
|
50
|
+
if (rows.length > 0) rows[rows.length - 1] = candidate;
|
|
51
|
+
else rows.push(candidate);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (rows.length === 0 || rows.at(-1) !== fact) rows.push(fact);
|
|
55
|
+
}
|
|
56
|
+
return rows.flatMap((row) =>
|
|
57
|
+
visibleWidth(row) <= width ? [row] : [truncateToWidth(row, width, "")],
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
function boundedRows(rows: string[], width: number): string[] {
|
|
41
62
|
const safeWidth = Math.max(0, Math.min(SETTINGS_PREVIEW_MAX_WIDTH, width));
|
|
42
63
|
if (safeWidth <= 0) return [];
|
|
@@ -176,20 +197,17 @@ export function renderThinkingStepsSettingsPreview(
|
|
|
176
197
|
startup: { enabled: false, mode: "tree" },
|
|
177
198
|
restartRequired: false,
|
|
178
199
|
};
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
: state.restartRequired
|
|
184
|
-
? `${saved} · ${active} · restart required`
|
|
185
|
-
: `${saved} · ${active}`;
|
|
200
|
+
const status = formatThinkingStatus(thinkingStatusLabels(thinkingSteps, state));
|
|
201
|
+
const statusRows = wrapPreviewText(status, previewWidth).map((row) =>
|
|
202
|
+
safeThemeFg(theme, "muted", row),
|
|
203
|
+
);
|
|
186
204
|
if (thinkingSteps.mode === "streaming") {
|
|
187
205
|
return boundedRows(
|
|
188
206
|
[
|
|
207
|
+
...statusRows,
|
|
189
208
|
safeThemeFg(theme, "thinkingText", "Thinking 7.1s (configured thinking toggle to expand)"),
|
|
190
209
|
safeThemeFg(theme, "thinkingText", "Inspect the host-rendered reasoning tail."),
|
|
191
210
|
safeThemeFg(theme, "thinkingText", "Preserve native Markdown and wrapping."),
|
|
192
|
-
safeThemeFg(theme, "muted", status),
|
|
193
211
|
],
|
|
194
212
|
previewWidth,
|
|
195
213
|
);
|
|
@@ -223,6 +241,5 @@ export function renderThinkingStepsSettingsPreview(
|
|
|
223
241
|
() => theme,
|
|
224
242
|
).render(previewWidth)
|
|
225
243
|
: native.render(previewWidth);
|
|
226
|
-
|
|
227
|
-
return boundedRows(rows, previewWidth);
|
|
244
|
+
return boundedRows([...statusRows, ...rows], previewWidth);
|
|
228
245
|
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
isPrototypePatchCurrent,
|
|
18
18
|
type PrototypePatchRegistration,
|
|
19
19
|
} from "./prototype-patch-registry";
|
|
20
|
+
import { formatThinkingStatus, thinkingStatusLabels } from "./thinking-status";
|
|
20
21
|
import { parseThinkingSteps, type ThinkingStep } from "./thinking-steps";
|
|
21
22
|
|
|
22
23
|
/*
|
|
@@ -114,7 +115,11 @@ export type ThinkingExperimentalDiagnostics = Readonly<{
|
|
|
114
115
|
}>;
|
|
115
116
|
|
|
116
117
|
export type ThinkingExperimentalState = Readonly<{
|
|
118
|
+
/** Backward-compatible alias for whole renderer availability. */
|
|
117
119
|
available: boolean;
|
|
120
|
+
rendererAvailable: boolean;
|
|
121
|
+
streamingAvailable: boolean;
|
|
122
|
+
streamingPoisoned: boolean;
|
|
118
123
|
active: boolean;
|
|
119
124
|
activeMode?: ThinkingStepsMode;
|
|
120
125
|
startup: Readonly<ThinkingStepsComponentConfig>;
|
|
@@ -123,6 +128,11 @@ export type ThinkingExperimentalState = Readonly<{
|
|
|
123
128
|
reason?: string;
|
|
124
129
|
}>;
|
|
125
130
|
|
|
131
|
+
export type ThinkingExperimentalApplyResult = Readonly<{
|
|
132
|
+
applied: boolean;
|
|
133
|
+
reason?: string;
|
|
134
|
+
}>;
|
|
135
|
+
|
|
126
136
|
function messageTimestamp(message: AssistantMessage): number | undefined {
|
|
127
137
|
return Number.isFinite(message.timestamp) ? message.timestamp : undefined;
|
|
128
138
|
}
|
|
@@ -747,6 +757,11 @@ export class ThinkingExperimentalController {
|
|
|
747
757
|
private displaced = false;
|
|
748
758
|
private restartRequired = false;
|
|
749
759
|
private unavailableReason: string | undefined;
|
|
760
|
+
private streamingUnavailableReason: string | undefined;
|
|
761
|
+
private streamingListenerPoisoned = false;
|
|
762
|
+
private disposed = false;
|
|
763
|
+
private resourceToken = 0;
|
|
764
|
+
private timerToken = 0;
|
|
750
765
|
private patchRegistration: PrototypePatchRegistration | undefined;
|
|
751
766
|
private context: ExtensionContext | undefined;
|
|
752
767
|
private stopInput: (() => void) | undefined;
|
|
@@ -789,12 +804,15 @@ export class ThinkingExperimentalController {
|
|
|
789
804
|
|
|
790
805
|
get state(): ThinkingExperimentalState {
|
|
791
806
|
this.checkDisplacement();
|
|
807
|
+
const rendererAvailable = !this.unavailableReason && !this.displaced;
|
|
792
808
|
const reason = this.displaced
|
|
793
|
-
? "Private renderer patch ownership was displaced
|
|
794
|
-
: (this.unavailableReason ??
|
|
795
|
-
(this.restartRequired ? "Restart Pi to apply saved Thinking changes." : undefined));
|
|
809
|
+
? "Private renderer patch ownership was displaced"
|
|
810
|
+
: (this.unavailableReason ?? this.streamingUnavailableReason);
|
|
796
811
|
return Object.freeze({
|
|
797
|
-
available:
|
|
812
|
+
available: rendererAvailable,
|
|
813
|
+
rendererAvailable,
|
|
814
|
+
streamingAvailable: !this.streamingUnavailableReason,
|
|
815
|
+
streamingPoisoned: this.streamingListenerPoisoned,
|
|
798
816
|
active: this.active && !this.displaced,
|
|
799
817
|
...(this.activeMode ? { activeMode: this.activeMode } : {}),
|
|
800
818
|
startup: Object.freeze({ ...this.startup }),
|
|
@@ -804,6 +822,13 @@ export class ThinkingExperimentalController {
|
|
|
804
822
|
});
|
|
805
823
|
}
|
|
806
824
|
|
|
825
|
+
private failureResult(): ThinkingExperimentalApplyResult {
|
|
826
|
+
return {
|
|
827
|
+
applied: false,
|
|
828
|
+
reason: formatThinkingStatus(thinkingStatusLabels(this.getConfig(), this.state)),
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
807
832
|
get diagnostics(): ThinkingExperimentalDiagnostics {
|
|
808
833
|
return Object.freeze({
|
|
809
834
|
trackedComponents: this.trackedEntries().length,
|
|
@@ -812,56 +837,106 @@ export class ThinkingExperimentalController {
|
|
|
812
837
|
});
|
|
813
838
|
}
|
|
814
839
|
|
|
815
|
-
/** Activates the private renderer
|
|
816
|
-
startSession(ctx: ExtensionContext):
|
|
840
|
+
/** Activates the private renderer during session startup, before transcript restoration. */
|
|
841
|
+
startSession(ctx: ExtensionContext): ThinkingExperimentalApplyResult {
|
|
842
|
+
this.disposed = false;
|
|
843
|
+
this.resourceToken += 1;
|
|
844
|
+
this.streamingUnavailableReason = undefined;
|
|
845
|
+
this.streamingListenerPoisoned = false;
|
|
817
846
|
if (ctx.mode === "tui" && ctx.hasUI) this.context = ctx;
|
|
818
847
|
this.startup = { ...this.getConfig() };
|
|
819
848
|
this.restartRequired = false;
|
|
820
849
|
this.activeMode = undefined;
|
|
821
850
|
if (!this.startup.enabled) return { applied: true };
|
|
822
851
|
if (!this.context) {
|
|
823
|
-
|
|
852
|
+
this.unavailableReason = "Thinking (Experimental) requires a TUI session";
|
|
853
|
+
this.restartRequired = true;
|
|
854
|
+
return this.failureResult();
|
|
855
|
+
}
|
|
856
|
+
if (this.unavailableReason) {
|
|
857
|
+
this.restartRequired = true;
|
|
858
|
+
return this.failureResult();
|
|
824
859
|
}
|
|
825
|
-
if (this.unavailableReason) return { applied: false, reason: this.unavailableReason };
|
|
826
860
|
if (this.startup.mode === "streaming" && !this.resolveToggleInput()) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
reason: this.unavailableReason ?? "Experimental thinking toggle is unavailable",
|
|
830
|
-
};
|
|
861
|
+
this.restartRequired = true;
|
|
862
|
+
return this.failureResult();
|
|
831
863
|
}
|
|
832
864
|
if (!this.install()) {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
this.unavailableReason ?? "Experimental renderer unavailable; using native thinking",
|
|
837
|
-
};
|
|
865
|
+
this.unavailableReason ??= "Experimental renderer unavailable; using native thinking";
|
|
866
|
+
this.restartRequired = true;
|
|
867
|
+
return this.failureResult();
|
|
838
868
|
}
|
|
839
869
|
if (this.checkDisplacement()) {
|
|
840
870
|
this.restartRequired = true;
|
|
841
|
-
return
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
871
|
+
return this.failureResult();
|
|
872
|
+
}
|
|
873
|
+
if (this.startup.mode === "streaming") {
|
|
874
|
+
const acquired = this.acquireStreamingResources();
|
|
875
|
+
if (!acquired.applied) {
|
|
876
|
+
this.restartRequired = true;
|
|
877
|
+
return this.failureResult();
|
|
878
|
+
}
|
|
845
879
|
}
|
|
846
880
|
this.active = true;
|
|
847
881
|
this.activeMode = this.startup.mode;
|
|
848
|
-
if (this.activeMode === "streaming") this.installInputListener();
|
|
849
|
-
if (!this.active) {
|
|
850
|
-
return {
|
|
851
|
-
applied: false,
|
|
852
|
-
reason: this.unavailableReason ?? "Experimental input handling is unavailable",
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
882
|
return { applied: true };
|
|
856
883
|
}
|
|
857
884
|
|
|
858
|
-
/**
|
|
859
|
-
reconcile():
|
|
885
|
+
/** Applies healthy installed transitions while preserving persistence as the authority. */
|
|
886
|
+
reconcile(): ThinkingExperimentalApplyResult {
|
|
860
887
|
const desired = this.getConfig();
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
888
|
+
if (!desired.enabled) {
|
|
889
|
+
this.restartRequired = false;
|
|
890
|
+
return this.active ? this.deactivate() : { applied: true };
|
|
891
|
+
}
|
|
892
|
+
if (this.active && this.activeMode === desired.mode) {
|
|
893
|
+
this.restartRequired = false;
|
|
894
|
+
return { applied: true };
|
|
895
|
+
}
|
|
896
|
+
if (
|
|
897
|
+
!this.active ||
|
|
898
|
+
!this.installed ||
|
|
899
|
+
this.displaced ||
|
|
900
|
+
this.unavailableReason ||
|
|
901
|
+
this.checkDisplacement()
|
|
902
|
+
) {
|
|
903
|
+
this.restartRequired = true;
|
|
904
|
+
return this.failureResult();
|
|
905
|
+
}
|
|
906
|
+
const previousMode = this.activeMode;
|
|
907
|
+
if (!previousMode) {
|
|
908
|
+
this.restartRequired = true;
|
|
909
|
+
return this.failureResult();
|
|
910
|
+
}
|
|
911
|
+
if (desired.mode === "streaming") {
|
|
912
|
+
// Streaming owns terminal input and a timer. Never acquire either from a
|
|
913
|
+
// settings callback: entering Streaming is an explicit restart boundary.
|
|
914
|
+
this.restartRequired = true;
|
|
915
|
+
return this.failureResult();
|
|
916
|
+
}
|
|
917
|
+
const streamingCleanupSucceeded =
|
|
918
|
+
previousMode !== "streaming" || this.releaseStreamingResources();
|
|
919
|
+
this.expanded = false;
|
|
920
|
+
this.activeMode = desired.mode;
|
|
921
|
+
this.restartRequired = false;
|
|
922
|
+
this.rerenderTracked();
|
|
923
|
+
const result = this.liveTransitionResult(desired.mode);
|
|
924
|
+
if (!result.applied || streamingCleanupSucceeded) return result;
|
|
925
|
+
return {
|
|
926
|
+
applied: true,
|
|
927
|
+
reason: formatThinkingStatus(thinkingStatusLabels(this.getConfig(), this.state)),
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private liveTransitionResult(mode: ThinkingStepsMode): ThinkingExperimentalApplyResult {
|
|
932
|
+
if (this.active && this.activeMode === mode && !this.displaced && !this.unavailableReason) {
|
|
933
|
+
return { applied: true };
|
|
934
|
+
}
|
|
935
|
+
this.restartRequired = true;
|
|
936
|
+
if (!this.unavailableReason && !this.streamingUnavailableReason) {
|
|
937
|
+
this.unavailableReason = "Experimental renderer failed during the live transition";
|
|
938
|
+
}
|
|
939
|
+
return this.failureResult();
|
|
865
940
|
}
|
|
866
941
|
|
|
867
942
|
private install(): boolean {
|
|
@@ -952,7 +1027,25 @@ export class ThinkingExperimentalController {
|
|
|
952
1027
|
this.failShape("Pi's private assistant renderer shape is incompatible");
|
|
953
1028
|
return nativeResult;
|
|
954
1029
|
}
|
|
955
|
-
this.track(
|
|
1030
|
+
const trackedState = this.track(
|
|
1031
|
+
component,
|
|
1032
|
+
message,
|
|
1033
|
+
args,
|
|
1034
|
+
predecessor,
|
|
1035
|
+
incomplete,
|
|
1036
|
+
nativeHidden,
|
|
1037
|
+
);
|
|
1038
|
+
// Tracking can synchronously acquire the first Streaming timer. If that
|
|
1039
|
+
// transaction fails, failActiveStreaming has already restored native UI.
|
|
1040
|
+
if (
|
|
1041
|
+
!this.active ||
|
|
1042
|
+
this.activeMode !== mode ||
|
|
1043
|
+
this.displaced ||
|
|
1044
|
+
this.unavailableReason ||
|
|
1045
|
+
this.states.get(component) !== trackedState ||
|
|
1046
|
+
(mode === "streaming" && (this.streamingListenerPoisoned || !this.stopInput))
|
|
1047
|
+
)
|
|
1048
|
+
return nativeResult;
|
|
956
1049
|
if (mode === "streaming" && this.expanded) return nativeResult;
|
|
957
1050
|
if (
|
|
958
1051
|
!replaceThinkingChildren(
|
|
@@ -1000,15 +1093,16 @@ export class ThinkingExperimentalController {
|
|
|
1000
1093
|
predecessor: (this: unknown, ...args: unknown[]) => unknown,
|
|
1001
1094
|
incomplete: boolean,
|
|
1002
1095
|
nativeHidden: HiddenState,
|
|
1003
|
-
):
|
|
1004
|
-
|
|
1096
|
+
): TrackedState {
|
|
1097
|
+
const trackedState: TrackedState = {
|
|
1005
1098
|
message,
|
|
1006
1099
|
args: [...args],
|
|
1007
1100
|
predecessor,
|
|
1008
1101
|
incomplete,
|
|
1009
1102
|
nativeHidden,
|
|
1010
1103
|
folded: false,
|
|
1011
|
-
}
|
|
1104
|
+
};
|
|
1105
|
+
this.states.set(component, trackedState);
|
|
1012
1106
|
let reference = this.references.get(component);
|
|
1013
1107
|
if (!reference) {
|
|
1014
1108
|
reference = new WeakRef(component);
|
|
@@ -1034,6 +1128,7 @@ export class ThinkingExperimentalController {
|
|
|
1034
1128
|
}
|
|
1035
1129
|
this.pruneTimings();
|
|
1036
1130
|
this.reconcileTimer();
|
|
1131
|
+
return trackedState;
|
|
1037
1132
|
}
|
|
1038
1133
|
|
|
1039
1134
|
private headerFor(message: AssistantMessage, incomplete: boolean, hidden: boolean): string {
|
|
@@ -1055,6 +1150,7 @@ export class ThinkingExperimentalController {
|
|
|
1055
1150
|
}
|
|
1056
1151
|
|
|
1057
1152
|
private resolveToggleInput(): boolean {
|
|
1153
|
+
if (this.streamingListenerPoisoned) return false;
|
|
1058
1154
|
if (this.toggleInput) return true;
|
|
1059
1155
|
const action = "app.thinking.toggle";
|
|
1060
1156
|
if (!this.getHostKeybindings) {
|
|
@@ -1131,22 +1227,68 @@ export class ThinkingExperimentalController {
|
|
|
1131
1227
|
}
|
|
1132
1228
|
|
|
1133
1229
|
private failToggleCapability(reason: string): false {
|
|
1134
|
-
this.
|
|
1230
|
+
this.poisonStreamingListener(reason);
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
private poisonStreamingListener(
|
|
1235
|
+
reason = "Pi's terminal input listener cleanup is unavailable",
|
|
1236
|
+
): void {
|
|
1237
|
+
this.streamingListenerPoisoned = true;
|
|
1238
|
+
this.streamingUnavailableReason = reason;
|
|
1239
|
+
this.toggleInput = undefined;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
private cleanupInput(stopInput: (() => void) | undefined): boolean {
|
|
1243
|
+
if (!stopInput) return true;
|
|
1244
|
+
try {
|
|
1245
|
+
stopInput();
|
|
1246
|
+
return true;
|
|
1247
|
+
} catch {
|
|
1248
|
+
this.poisonStreamingListener();
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
private failActiveStreaming(reason: string): void {
|
|
1254
|
+
// Invalidate first so a retained or currently executing callback can never act again.
|
|
1255
|
+
this.resourceToken += 1;
|
|
1256
|
+
this.releaseStreamingResources();
|
|
1257
|
+
this.poisonStreamingListener(reason);
|
|
1135
1258
|
this.active = false;
|
|
1136
|
-
this.
|
|
1137
|
-
this.
|
|
1138
|
-
this.
|
|
1139
|
-
this.rerenderTracked(true);
|
|
1259
|
+
this.activeMode = undefined;
|
|
1260
|
+
this.restartRequired = true;
|
|
1261
|
+
this.rerenderTracked(true, true);
|
|
1140
1262
|
this.clearTrackedSnapshots();
|
|
1141
|
-
this.
|
|
1142
|
-
return false;
|
|
1263
|
+
this.clearLiveState();
|
|
1143
1264
|
}
|
|
1144
1265
|
|
|
1145
|
-
private
|
|
1146
|
-
if (this.stopInput ||
|
|
1266
|
+
private acquireStreamingResources(): { applied: boolean; reason?: string } {
|
|
1267
|
+
if (this.stopInput || this.interval) {
|
|
1268
|
+
return { applied: false, reason: "Experimental input resources are already owned" };
|
|
1269
|
+
}
|
|
1270
|
+
if (this.streamingListenerPoisoned) {
|
|
1271
|
+
return {
|
|
1272
|
+
applied: false,
|
|
1273
|
+
reason:
|
|
1274
|
+
this.streamingUnavailableReason ?? "Pi's terminal input listener cleanup is unavailable",
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
if (!this.context || !this.toggleInput) {
|
|
1278
|
+
return { applied: false, reason: "Experimental input handling is unavailable" };
|
|
1279
|
+
}
|
|
1280
|
+
const token = ++this.resourceToken;
|
|
1281
|
+
let stopInput: unknown;
|
|
1147
1282
|
try {
|
|
1148
|
-
|
|
1149
|
-
if (
|
|
1283
|
+
stopInput = this.context.ui.onTerminalInput((data) => {
|
|
1284
|
+
if (
|
|
1285
|
+
this.disposed ||
|
|
1286
|
+
token !== this.resourceToken ||
|
|
1287
|
+
!this.active ||
|
|
1288
|
+
this.activeMode !== "streaming" ||
|
|
1289
|
+
this.checkDisplacement()
|
|
1290
|
+
)
|
|
1291
|
+
return;
|
|
1150
1292
|
const input = this.toggleInput;
|
|
1151
1293
|
if (!input) return;
|
|
1152
1294
|
let matches = false;
|
|
@@ -1159,7 +1301,7 @@ export class ThinkingExperimentalController {
|
|
|
1159
1301
|
matches = parsed !== undefined && input.keys.includes(parsed);
|
|
1160
1302
|
}
|
|
1161
1303
|
} catch {
|
|
1162
|
-
this.
|
|
1304
|
+
this.failActiveStreaming("Pi's thinking-toggle matcher failed at runtime");
|
|
1163
1305
|
return;
|
|
1164
1306
|
}
|
|
1165
1307
|
if (!matches) return;
|
|
@@ -1169,14 +1311,64 @@ export class ThinkingExperimentalController {
|
|
|
1169
1311
|
}
|
|
1170
1312
|
return { consume: true };
|
|
1171
1313
|
});
|
|
1172
|
-
if (typeof stopInput !== "function") {
|
|
1173
|
-
this.failToggleCapability("Pi's terminal input listener cleanup is unavailable");
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
this.stopInput = stopInput;
|
|
1177
1314
|
} catch {
|
|
1178
|
-
|
|
1315
|
+
// Registration may retain the callback before throwing. Invalidate it and never
|
|
1316
|
+
// attempt another registration in this session because ownership is unknowable.
|
|
1317
|
+
this.resourceToken += 1;
|
|
1318
|
+
this.poisonStreamingListener("Pi's terminal input listener registration is unavailable");
|
|
1319
|
+
return { applied: false, reason: this.streamingUnavailableReason };
|
|
1320
|
+
}
|
|
1321
|
+
if (typeof stopInput !== "function") {
|
|
1322
|
+
// The host may have retained the callback without giving us a release mechanism.
|
|
1323
|
+
// Poison Streaming for this session and leave that callback permanently inert.
|
|
1324
|
+
this.resourceToken += 1;
|
|
1325
|
+
this.poisonStreamingListener();
|
|
1326
|
+
return { applied: false, reason: this.streamingUnavailableReason };
|
|
1179
1327
|
}
|
|
1328
|
+
const releaseInput = stopInput as () => void;
|
|
1329
|
+
let interval: ReturnType<typeof setInterval> | undefined;
|
|
1330
|
+
try {
|
|
1331
|
+
if (this.activeEntries().length > 0) interval = this.createStreamingTimer(token);
|
|
1332
|
+
} catch {
|
|
1333
|
+
// Registration succeeded but the acquisition transaction did not. Invalidate the
|
|
1334
|
+
// candidate callback before cleanup and poison Streaming even when cleanup succeeds.
|
|
1335
|
+
this.resourceToken += 1;
|
|
1336
|
+
this.cleanupInput(releaseInput);
|
|
1337
|
+
this.poisonStreamingListener("Pi's thinking timer is unavailable");
|
|
1338
|
+
return { applied: false, reason: this.streamingUnavailableReason };
|
|
1339
|
+
}
|
|
1340
|
+
this.stopInput = releaseInput;
|
|
1341
|
+
this.interval = interval;
|
|
1342
|
+
return { applied: true };
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
private createStreamingTimer(resourceToken = this.resourceToken): ReturnType<typeof setInterval> {
|
|
1346
|
+
const timerToken = ++this.timerToken;
|
|
1347
|
+
return setInterval(() => {
|
|
1348
|
+
// Timer/resource release invalidates callbacks before attempting host cleanup.
|
|
1349
|
+
// A host that throws from clearInterval may retain this callback, so stale
|
|
1350
|
+
// callbacks must be wholly inert while a later release retries the same handle.
|
|
1351
|
+
if (
|
|
1352
|
+
this.disposed ||
|
|
1353
|
+
resourceToken !== this.resourceToken ||
|
|
1354
|
+
timerToken !== this.timerToken ||
|
|
1355
|
+
!this.active ||
|
|
1356
|
+
this.activeMode !== "streaming"
|
|
1357
|
+
)
|
|
1358
|
+
return;
|
|
1359
|
+
if (this.checkDisplacement()) return;
|
|
1360
|
+
this.rerenderActive();
|
|
1361
|
+
this.reconcileTimer();
|
|
1362
|
+
}, 1000);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
private releaseStreamingResources(): boolean {
|
|
1366
|
+
this.resourceToken += 1;
|
|
1367
|
+
const timerCleanupSucceeded = this.stopTimer();
|
|
1368
|
+
const stopInput = this.stopInput;
|
|
1369
|
+
this.stopInput = undefined;
|
|
1370
|
+
const inputCleanupSucceeded = this.cleanupInput(stopInput);
|
|
1371
|
+
return timerCleanupSucceeded && inputCleanupSucceeded;
|
|
1180
1372
|
}
|
|
1181
1373
|
|
|
1182
1374
|
private trackedEntries(): Array<[object, TrackedState]> {
|
|
@@ -1263,13 +1455,20 @@ export class ThinkingExperimentalController {
|
|
|
1263
1455
|
}
|
|
1264
1456
|
}
|
|
1265
1457
|
|
|
1266
|
-
private restoreNative(
|
|
1458
|
+
private restoreNative(
|
|
1459
|
+
entries: Array<[object, TrackedState]>,
|
|
1460
|
+
preserveVisibleUnfolded = false,
|
|
1461
|
+
): number {
|
|
1267
1462
|
let processed = 0;
|
|
1268
1463
|
for (const [component, state] of entries) {
|
|
1269
1464
|
processed += 1;
|
|
1270
1465
|
const instance = component as PatchableAssistant;
|
|
1271
1466
|
try {
|
|
1272
|
-
|
|
1467
|
+
// A visible native predecessor has already produced the authoritative children
|
|
1468
|
+
// when first-message timer acquisition fails inside track(). Preserve their exact
|
|
1469
|
+
// identity rather than invoking the predecessor a second time.
|
|
1470
|
+
if (!preserveVisibleUnfolded || state.folded || state.nativeHidden.value === true)
|
|
1471
|
+
renderWithHiddenState(instance, state.predecessor, state.args, state.nativeHidden);
|
|
1273
1472
|
} catch {
|
|
1274
1473
|
// A stale transcript component must never break restoration of later components.
|
|
1275
1474
|
} finally {
|
|
@@ -1328,7 +1527,7 @@ export class ThinkingExperimentalController {
|
|
|
1328
1527
|
state.folded = true;
|
|
1329
1528
|
}
|
|
1330
1529
|
} catch {
|
|
1331
|
-
this.
|
|
1530
|
+
this.failShape("Pi's private assistant renderer failed during a live rerender");
|
|
1332
1531
|
}
|
|
1333
1532
|
if (this.shapeFailureDuringRerender) break;
|
|
1334
1533
|
}
|
|
@@ -1345,9 +1544,11 @@ export class ThinkingExperimentalController {
|
|
|
1345
1544
|
return processed;
|
|
1346
1545
|
}
|
|
1347
1546
|
|
|
1348
|
-
private rerenderTracked(nativeOnly = false): void {
|
|
1547
|
+
private rerenderTracked(nativeOnly = false, preserveVisibleUnfolded = false): void {
|
|
1349
1548
|
const entries = this.trackedEntries();
|
|
1350
|
-
const processed =
|
|
1549
|
+
const processed = nativeOnly
|
|
1550
|
+
? this.restoreNative(entries, preserveVisibleUnfolded)
|
|
1551
|
+
: this.rerenderEntries(entries);
|
|
1351
1552
|
if (processed > 0) this.requestHostRender();
|
|
1352
1553
|
}
|
|
1353
1554
|
|
|
@@ -1362,7 +1563,7 @@ export class ThinkingExperimentalController {
|
|
|
1362
1563
|
}
|
|
1363
1564
|
|
|
1364
1565
|
private reconcileTimer(): void {
|
|
1365
|
-
if (!this.active || this.activeMode !== "streaming") {
|
|
1566
|
+
if (!this.active || this.activeMode !== "streaming" || !this.stopInput) {
|
|
1366
1567
|
this.stopTimer();
|
|
1367
1568
|
return;
|
|
1368
1569
|
}
|
|
@@ -1370,21 +1571,29 @@ export class ThinkingExperimentalController {
|
|
|
1370
1571
|
this.stopTimer();
|
|
1371
1572
|
return;
|
|
1372
1573
|
}
|
|
1373
|
-
if (!this.interval) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
this.rerenderActive();
|
|
1380
|
-
this.reconcileTimer();
|
|
1381
|
-
}, 1000);
|
|
1574
|
+
if (!this.interval && !this.streamingListenerPoisoned) {
|
|
1575
|
+
try {
|
|
1576
|
+
this.interval = this.createStreamingTimer();
|
|
1577
|
+
} catch {
|
|
1578
|
+
this.failActiveStreaming("Pi's thinking timer is unavailable");
|
|
1579
|
+
}
|
|
1382
1580
|
}
|
|
1383
1581
|
}
|
|
1384
1582
|
|
|
1385
|
-
private stopTimer():
|
|
1386
|
-
|
|
1387
|
-
|
|
1583
|
+
private stopTimer(): boolean {
|
|
1584
|
+
const interval = this.interval;
|
|
1585
|
+
if (!interval) return true;
|
|
1586
|
+
this.timerToken += 1;
|
|
1587
|
+
try {
|
|
1588
|
+
clearInterval(interval);
|
|
1589
|
+
this.interval = undefined;
|
|
1590
|
+
return true;
|
|
1591
|
+
} catch {
|
|
1592
|
+
// Keep the exact handle so shutdown or another release can retry it. Streaming
|
|
1593
|
+
// remains poisoned, preventing any replacement timer/listener acquisition.
|
|
1594
|
+
this.poisonStreamingListener("Pi's thinking timer cleanup is unavailable");
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1388
1597
|
}
|
|
1389
1598
|
|
|
1390
1599
|
private checkDisplacement(): boolean {
|
|
@@ -1405,9 +1614,7 @@ export class ThinkingExperimentalController {
|
|
|
1405
1614
|
// Restore every component synchronously before relinquishing input/timer ownership.
|
|
1406
1615
|
this.rerenderTracked(true);
|
|
1407
1616
|
this.clearTrackedSnapshots();
|
|
1408
|
-
this.
|
|
1409
|
-
this.stopInput?.();
|
|
1410
|
-
this.stopInput = undefined;
|
|
1617
|
+
this.releaseStreamingResources();
|
|
1411
1618
|
}
|
|
1412
1619
|
}
|
|
1413
1620
|
return this.displaced;
|
|
@@ -1416,9 +1623,9 @@ export class ThinkingExperimentalController {
|
|
|
1416
1623
|
private failShape(reason: string): void {
|
|
1417
1624
|
this.unavailableReason = reason;
|
|
1418
1625
|
this.active = false;
|
|
1419
|
-
this.
|
|
1420
|
-
this.
|
|
1421
|
-
this.
|
|
1626
|
+
this.activeMode = undefined;
|
|
1627
|
+
this.restartRequired = true;
|
|
1628
|
+
this.releaseStreamingResources();
|
|
1422
1629
|
if (this.rerendering) {
|
|
1423
1630
|
this.shapeFailureDuringRerender = reason;
|
|
1424
1631
|
return;
|
|
@@ -1440,19 +1647,22 @@ export class ThinkingExperimentalController {
|
|
|
1440
1647
|
this.currentMessage = undefined;
|
|
1441
1648
|
}
|
|
1442
1649
|
|
|
1443
|
-
private deactivate():
|
|
1650
|
+
private deactivate(): ThinkingExperimentalApplyResult {
|
|
1444
1651
|
this.active = false;
|
|
1445
1652
|
this.activeMode = undefined;
|
|
1446
1653
|
this.expanded = false;
|
|
1447
|
-
this.
|
|
1448
|
-
this.stopInput?.();
|
|
1449
|
-
this.stopInput = undefined;
|
|
1654
|
+
const streamingCleanupSucceeded = this.releaseStreamingResources();
|
|
1450
1655
|
this.rerenderTracked(true);
|
|
1451
1656
|
// Cached host components stay native until Pi next rebuilds/updates them. Dropping
|
|
1452
1657
|
// snapshots is intentional: reactivation must never replay pre-disable content.
|
|
1453
1658
|
this.clearTrackedSnapshots();
|
|
1454
1659
|
this.clearLiveState();
|
|
1455
1660
|
this.toggleInput = undefined;
|
|
1661
|
+
if (streamingCleanupSucceeded) return { applied: true };
|
|
1662
|
+
return {
|
|
1663
|
+
applied: true,
|
|
1664
|
+
reason: formatThinkingStatus(thinkingStatusLabels(this.getConfig(), this.state)),
|
|
1665
|
+
};
|
|
1456
1666
|
}
|
|
1457
1667
|
|
|
1458
1668
|
beginMessage(event: unknown): void {
|
|
@@ -1544,6 +1754,7 @@ export class ThinkingExperimentalController {
|
|
|
1544
1754
|
}
|
|
1545
1755
|
|
|
1546
1756
|
shutdown(): void {
|
|
1757
|
+
this.disposed = true;
|
|
1547
1758
|
this.deactivate();
|
|
1548
1759
|
this.startup = { enabled: false, mode: "tree" };
|
|
1549
1760
|
this.restartRequired = false;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ThinkingStepsComponentConfig, ThinkingStepsMode } from "./config";
|
|
2
|
+
|
|
3
|
+
export type ThinkingStatusState = Readonly<{
|
|
4
|
+
active: boolean;
|
|
5
|
+
activeMode?: ThinkingStepsMode;
|
|
6
|
+
restartRequired: boolean;
|
|
7
|
+
/** Whole private renderer capability, independent of mode-specific resources. */
|
|
8
|
+
rendererAvailable?: boolean;
|
|
9
|
+
/** Streaming input/timer capability for this session. */
|
|
10
|
+
streamingAvailable?: boolean;
|
|
11
|
+
/** Irreversible session-local acquisition/callback poison. */
|
|
12
|
+
streamingPoisoned?: boolean;
|
|
13
|
+
/** Legacy whole-renderer capability accepted by settings adapters. */
|
|
14
|
+
available?: boolean;
|
|
15
|
+
reason?: string;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
export type ThinkingStatusLabels = Readonly<{
|
|
19
|
+
saved: ThinkingStepsMode | "disabled";
|
|
20
|
+
active: ThinkingStepsMode | "native";
|
|
21
|
+
rendererUnavailable: boolean;
|
|
22
|
+
streamingUnavailable: boolean;
|
|
23
|
+
streamingPoisoned: boolean;
|
|
24
|
+
restartRequired: boolean;
|
|
25
|
+
reason?: string;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
function cleanReason(value: string | undefined): string | undefined {
|
|
29
|
+
const reason = value
|
|
30
|
+
?.replace(/(?:[;,·]\s*)?restart required\b[.!]?/gi, "")
|
|
31
|
+
.replace(/^restart pi to apply(?: saved thinking changes)?[.!]?$/i, "")
|
|
32
|
+
.replace(/^[\s;,.·]+|[\s;,.·]+$/g, "")
|
|
33
|
+
.trim();
|
|
34
|
+
if (!reason || /^(?:renderer|streaming) unavailable$/i.test(reason)) return undefined;
|
|
35
|
+
return reason;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Builds independent status facts once so every consumer reports the same state. */
|
|
39
|
+
export function thinkingStatusLabels(
|
|
40
|
+
config: ThinkingStepsComponentConfig,
|
|
41
|
+
state: ThinkingStatusState,
|
|
42
|
+
): ThinkingStatusLabels {
|
|
43
|
+
const rendererAvailable = state.rendererAvailable ?? state.available ?? true;
|
|
44
|
+
const reason = cleanReason(state.reason);
|
|
45
|
+
return {
|
|
46
|
+
saved: config.enabled ? config.mode : "disabled",
|
|
47
|
+
active: state.active && state.activeMode ? state.activeMode : "native",
|
|
48
|
+
rendererUnavailable: !rendererAvailable,
|
|
49
|
+
streamingUnavailable: state.streamingAvailable === false || state.streamingPoisoned === true,
|
|
50
|
+
streamingPoisoned: state.streamingPoisoned === true,
|
|
51
|
+
restartRequired: state.restartRequired,
|
|
52
|
+
...(reason ? { reason } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const modeLabel = (mode: ThinkingStepsMode | "disabled" | "native"): string =>
|
|
57
|
+
`${mode.charAt(0).toUpperCase()}${mode.slice(1)}`;
|
|
58
|
+
|
|
59
|
+
/** Formats the canonical complete status line used by settings, previews, and apply failures. */
|
|
60
|
+
export function formatThinkingStatus(facts: ThinkingStatusLabels): string {
|
|
61
|
+
return [
|
|
62
|
+
`Saved: ${modeLabel(facts.saved)}`,
|
|
63
|
+
`Active: ${modeLabel(facts.active)}`,
|
|
64
|
+
...(facts.rendererUnavailable ? ["Renderer unavailable"] : []),
|
|
65
|
+
...(facts.streamingUnavailable ? ["Streaming unavailable"] : []),
|
|
66
|
+
...(facts.restartRequired ? ["restart required"] : []),
|
|
67
|
+
...(facts.reason ? [facts.reason] : []),
|
|
68
|
+
].join(" · ");
|
|
69
|
+
}
|
package/extensions/zentui/ui.ts
CHANGED
|
@@ -602,7 +602,7 @@ function renderAccentRailFrameFromBase({
|
|
|
602
602
|
above: parsedTop?.count,
|
|
603
603
|
below: parsedBottom?.count,
|
|
604
604
|
};
|
|
605
|
-
const
|
|
605
|
+
const renderedLines = renderAccentRailEditorFrame({
|
|
606
606
|
width,
|
|
607
607
|
editorLines,
|
|
608
608
|
autocompleteLines,
|
|
@@ -610,6 +610,7 @@ function renderAccentRailFrameFromBase({
|
|
|
610
610
|
uiTheme,
|
|
611
611
|
config,
|
|
612
612
|
});
|
|
613
|
+
const lines = renderedLines.length === 1 ? ["", ...renderedLines, ""] : renderedLines;
|
|
613
614
|
POLISHED_FRAME_SPLITS.set(lines, {
|
|
614
615
|
rows: Object.freeze([...lines]),
|
|
615
616
|
split: { editorLines, trailingLines: autocompleteLines, viewport },
|