pi-one-ui 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -39
- package/extensions/{shell → app/commands}/settings-previews.ts +7 -7
- package/extensions/app/config/renderer.ts +13 -63
- package/extensions/app/config/shell.ts +24 -138
- package/extensions/app/config/store.ts +290 -0
- package/extensions/app/host/pi-extension-port.ts +25 -0
- package/extensions/app/host/pi-ui-port.ts +52 -0
- package/extensions/app/host/tui-capabilities.ts +27 -0
- package/extensions/app/overlay/input-router.ts +63 -0
- package/extensions/app/overlay/overlay-manager.ts +39 -0
- package/extensions/{shell → app/overlay}/selector-border.ts +3 -3
- package/extensions/app/overlay/selector-controller.ts +104 -0
- package/extensions/app/ownership/layout-registry.ts +65 -0
- package/extensions/app/ownership.ts +21 -7
- package/extensions/app/panel.ts +261 -140
- package/extensions/app/runtime/event-coordinator.ts +84 -0
- package/extensions/app/runtime/render-scheduler.ts +56 -0
- package/extensions/app/runtime/runtime-state.ts +83 -0
- package/extensions/app/runtime/tui-runtime.ts +546 -0
- package/extensions/features/{context.ts → context-inspector/index.ts} +30 -23
- package/extensions/features/{shell/flush-docked-bash.ts → flush-docked-bash.ts} +1 -4
- package/extensions/index.ts +2 -48
- package/extensions/layouts/context/index.ts +188 -0
- package/extensions/{shell → layouts/context/message}/user-message-styles.ts +2 -2
- package/extensions/{shell → layouts/context/message}/user-message.ts +2 -2
- package/extensions/{transcript → layouts/context}/renderer/compact-mode.ts +17 -13
- package/extensions/layouts/context/renderer/context-refresh.ts +79 -0
- package/extensions/{transcript → layouts/context}/renderer/default-mode.ts +3 -3
- package/extensions/{transcript → layouts/context}/renderer/index.ts +14 -14
- package/extensions/{transcript → layouts/context}/renderer/mouse/hover.ts +2 -2
- package/extensions/{transcript → layouts/context}/renderer/mouse/interaction.ts +26 -7
- package/extensions/{transcript → layouts/context}/renderer/mouse/layout.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/mouse/packets.ts +3 -3
- package/extensions/{transcript → layouts/context}/renderer/mouse/scroll.ts +12 -7
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-component.ts +2 -2
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-edit-render.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-highlight.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-layout.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-limits.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-palette.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-presentation.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-renderer.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-write-render.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/index.ts +2 -2
- package/extensions/{transcript → layouts/context}/renderer/tool/grouping.ts +4 -4
- package/extensions/{transcript → layouts/context}/renderer/tool/message-display.ts +6 -4
- package/extensions/{transcript → layouts/context}/renderer/tool/names.ts +2 -2
- package/extensions/{transcript → layouts/context}/renderer/tool/result.ts +3 -3
- package/extensions/{features/agent-summary → layouts/context/summary}/core.ts +1 -1
- package/extensions/{features → layouts/context/thinking}/compact-thinking.ts +7 -7
- package/extensions/{shell → layouts/editor}/accent-rail-editor.ts +8 -8
- package/extensions/{shell → layouts/editor}/completion-menu.ts +3 -6
- package/extensions/layouts/editor/controller.ts +613 -0
- package/extensions/{shell → layouts/editor}/editor-metadata-format.ts +6 -3
- package/extensions/layouts/editor/factory.ts +159 -0
- package/extensions/layouts/editor/index.ts +16 -0
- package/extensions/{shell → layouts/editor}/minimalist-editor.ts +4 -4
- package/extensions/layouts/editor/ownership.ts +103 -0
- package/extensions/{shell → layouts/editor}/ui.ts +2 -2
- package/extensions/layouts/footer/controller.ts +392 -0
- package/extensions/layouts/footer/data.ts +41 -0
- package/extensions/{shell → layouts/footer}/extension-status.ts +2 -2
- package/extensions/{shell → layouts/footer}/footer-layout.ts +1 -1
- package/extensions/{shell → layouts/footer}/footer.ts +11 -8
- package/extensions/layouts/footer/index.ts +24 -0
- package/extensions/layouts/header/index.ts +5 -0
- package/extensions/{features/shell → layouts/header}/startup-header.ts +105 -19
- package/extensions/layouts/working-line/controller.ts +291 -0
- package/extensions/{shell → layouts/working-line}/interaction-summary.ts +3 -3
- package/extensions/{shell → layouts/working-line}/working-line-spinners.ts +1 -1
- package/extensions/{shell → layouts/working-line}/working-line.ts +4 -4
- package/extensions/{shell → services}/live-context.ts +29 -1
- package/extensions/services/project-refresh.ts +374 -0
- package/extensions/{shell → services}/project-state.ts +5 -5
- package/extensions/{shell/state.ts → services/session-state.ts} +75 -5
- package/extensions/{shell → shared}/format.ts +4 -4
- package/extensions/tools/component-tree.ts +6 -6
- package/package.json +4 -4
- package/extensions/shell/index.ts +0 -1659
- package/extensions/shell/project-refresh.ts +0 -130
- package/extensions/shell/settings-command.ts +0 -2144
- package/extensions/transcript/index.ts +0 -65
- package/extensions/transcript/renderer/transcript-refresh.ts +0 -79
- /package/extensions/{shell → app/ownership}/prototype-patch-registry.ts +0 -0
- /package/extensions/{shell → app/runtime}/session-lifecycle.ts +0 -0
- /package/extensions/features/{shell/aliases.ts → aliases.ts} +0 -0
- /package/extensions/features/{shell → legacy}/working-message.ts +0 -0
- /package/extensions/features/{reference → session-reference}/index.ts +0 -0
- /package/extensions/features/{reference → session-reference}/session.ts +0 -0
- /package/extensions/features/{reference/subagent.ts → subagent-autocomplete.ts} +0 -0
- /package/extensions/{shell → layouts/context/message}/user-message-osc.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/markdown-enhance.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/ATTRIBUTION.md +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/ansi-utils.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-header.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-inline.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-parse.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-text.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/line-width-safety.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/shiki-highlight.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/write-execution.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/show-more-hint.ts +0 -0
- /package/extensions/{features/agent-summary → layouts/context/summary}/index.ts +0 -0
- /package/extensions/{shell → layouts/editor}/accent-rail-layout-patch.ts +0 -0
- /package/extensions/{shell → layouts/editor}/editor-transfer.ts +0 -0
- /package/extensions/{shell → layouts/footer}/footer-format.ts +0 -0
- /package/extensions/{shell → layouts/working-line}/working-line-messages.ts +0 -0
- /package/extensions/{shell/git.ts → services/git-data.ts} +0 -0
- /package/extensions/{shell/package-version.ts → services/package-data.ts} +0 -0
- /package/extensions/{shell/runtime.ts → services/runtime-data.ts} +0 -0
- /package/extensions/{shell → services}/telemetry.ts +0 -0
- /package/extensions/{shell → shared}/icons.ts +0 -0
- /package/extensions/{shell → shared}/style.ts +0 -0
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
hasUnsupportedComponentStyle,
|
|
7
|
+
type PolishedTuiConfig,
|
|
8
|
+
} from "../../app/config/shell.ts";
|
|
9
|
+
import { SessionLifecycle } from "../../app/runtime/session-lifecycle.ts";
|
|
10
|
+
import type { RenderScheduler } from "../../app/runtime/render-scheduler.ts";
|
|
11
|
+
import type { FooterState } from "../footer/index.ts";
|
|
12
|
+
import {
|
|
13
|
+
installHostAccentRailLayoutPatch,
|
|
14
|
+
retainAccentRailLayoutPatchInstallation,
|
|
15
|
+
type AccentRailLayoutPatchDiagnostic,
|
|
16
|
+
} from "./accent-rail-layout-patch.ts";
|
|
17
|
+
import {
|
|
18
|
+
replaceEditorComponentWithExpandedText,
|
|
19
|
+
type EditorTransferFailureReason,
|
|
20
|
+
} from "./editor-transfer.ts";
|
|
21
|
+
import {
|
|
22
|
+
createEditorFactory,
|
|
23
|
+
createWrappedEditorFactory,
|
|
24
|
+
type EditorFactoryRuntime,
|
|
25
|
+
} from "./factory.ts";
|
|
26
|
+
import {
|
|
27
|
+
getZentuiEditorBaseFactory,
|
|
28
|
+
isOwnedEditorFactory,
|
|
29
|
+
isZentuiEditorFactory,
|
|
30
|
+
type EditorFactory,
|
|
31
|
+
} from "./ownership.ts";
|
|
32
|
+
|
|
33
|
+
export type EditorChangeResult = { ok: true } | { ok: false; reason: string };
|
|
34
|
+
|
|
35
|
+
export type EditorLayoutControllerContext = {
|
|
36
|
+
readonly getConfig: () => PolishedTuiConfig;
|
|
37
|
+
readonly saveComponent: (
|
|
38
|
+
patch: Partial<PolishedTuiConfig["components"]["editor"]>,
|
|
39
|
+
) => PolishedTuiConfig;
|
|
40
|
+
readonly getState: () => FooterState;
|
|
41
|
+
readonly sessionLifecycle: SessionLifecycle;
|
|
42
|
+
readonly render: Pick<RenderScheduler, "request">;
|
|
43
|
+
readonly getThinkingLevel: () => ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
44
|
+
readonly getContextWindow: (ctx: ExtensionContext) => number | undefined;
|
|
45
|
+
readonly getContextPercent: (ctx: ExtensionContext) => number | undefined;
|
|
46
|
+
readonly getAgentDurationMs: () => number;
|
|
47
|
+
readonly isAgentActive: () => boolean;
|
|
48
|
+
readonly isAgentDurationActive: () => boolean;
|
|
49
|
+
readonly subscribeAgentDuration: (listener: () => void) => () => void;
|
|
50
|
+
readonly getProjectRoot: () => string | undefined;
|
|
51
|
+
readonly onProjectRequirementChanged: () => void;
|
|
52
|
+
readonly onModelLabelChanged: (ctx: ExtensionContext) => void;
|
|
53
|
+
readonly recordLayoutDiagnostic: (
|
|
54
|
+
diagnostic: AccentRailLayoutPatchDiagnostic,
|
|
55
|
+
version?: string,
|
|
56
|
+
) => void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type EditorInstallMode = "none" | "standalone" | "wrapper";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Owns the Pi editor factory and all Editor-specific lifecycle state.
|
|
63
|
+
*/
|
|
64
|
+
export class EditorLayoutController {
|
|
65
|
+
private readonly context: EditorLayoutControllerContext;
|
|
66
|
+
private readonly ownerToken = Symbol("pi-one-ui-editor-owner");
|
|
67
|
+
private activeTuiContext: ExtensionContext | undefined;
|
|
68
|
+
private requestEditorRender: (() => void) | undefined;
|
|
69
|
+
private editorInstalled = false;
|
|
70
|
+
private editorInstallMode: EditorInstallMode = "none";
|
|
71
|
+
private installedEditorFactory: EditorFactory | undefined;
|
|
72
|
+
private wrappedEditorFactory: EditorFactory | undefined;
|
|
73
|
+
private minimalistDecorationActive = false;
|
|
74
|
+
private minimalistDurationUpdatesActive = false;
|
|
75
|
+
private stopMinimalistDurationUpdates: () => void = () => {};
|
|
76
|
+
private accentRailLayoutPatchCleanup: () => void = () => {};
|
|
77
|
+
private accentRailLayoutPatchInstallSerial = 0;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates an Editor controller with shared runtime selectors.
|
|
81
|
+
*
|
|
82
|
+
* @param context Runtime services and selectors exposed to the Editor.
|
|
83
|
+
*/
|
|
84
|
+
constructor(context: EditorLayoutControllerContext) {
|
|
85
|
+
this.context = context;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Installs the host Accent Rail patch for a new session generation.
|
|
90
|
+
*
|
|
91
|
+
* @param ctx Active Pi extension context.
|
|
92
|
+
* @returns A promise that settles after patch retention is attempted.
|
|
93
|
+
*/
|
|
94
|
+
async startSession(ctx: ExtensionContext): Promise<void> {
|
|
95
|
+
const lifecycleGeneration =
|
|
96
|
+
this.context.sessionLifecycle.currentGeneration();
|
|
97
|
+
const installSerial = ++this.accentRailLayoutPatchInstallSerial;
|
|
98
|
+
this.accentRailLayoutPatchCleanup();
|
|
99
|
+
this.accentRailLayoutPatchCleanup = () => {};
|
|
100
|
+
if (!this.isTuiContext(ctx)) return;
|
|
101
|
+
|
|
102
|
+
const result = await retainAccentRailLayoutPatchInstallation(
|
|
103
|
+
() => installHostAccentRailLayoutPatch(this.ownerToken),
|
|
104
|
+
() =>
|
|
105
|
+
this.context.sessionLifecycle.isCurrent(lifecycleGeneration) &&
|
|
106
|
+
installSerial === this.accentRailLayoutPatchInstallSerial,
|
|
107
|
+
(layoutPatch) => {
|
|
108
|
+
this.accentRailLayoutPatchCleanup = layoutPatch.cleanup;
|
|
109
|
+
this.context.recordLayoutDiagnostic(
|
|
110
|
+
layoutPatch.diagnostic,
|
|
111
|
+
layoutPatch.version,
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
if (result === "failed")
|
|
116
|
+
this.context.recordLayoutDiagnostic("host-module-unavailable");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Installs or reconciles the configured Editor for an active session.
|
|
121
|
+
*
|
|
122
|
+
* @param ctx Active Pi extension context.
|
|
123
|
+
* @param allowStaleZentui Whether a stale predecessor may be replaced.
|
|
124
|
+
* @returns The replacement result when a host factory changed.
|
|
125
|
+
*/
|
|
126
|
+
install(
|
|
127
|
+
ctx: ExtensionContext,
|
|
128
|
+
allowStaleZentui = true,
|
|
129
|
+
): EditorChangeResult | undefined {
|
|
130
|
+
if (!this.isTuiContext(ctx)) return;
|
|
131
|
+
this.activeTuiContext = ctx;
|
|
132
|
+
if (this.isEditorEnabled()) this.clearEditorOwnership();
|
|
133
|
+
else this.uninstall(ctx, { allowStaleZentui });
|
|
134
|
+
const result = this.reconcile(ctx, { allowStaleZentui });
|
|
135
|
+
this.scheduleReconciliation(ctx);
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reconciles the Editor with the latest Editor component configuration.
|
|
141
|
+
*
|
|
142
|
+
* @param ctx Active Pi extension context.
|
|
143
|
+
* @param options Ownership options for reload transitions.
|
|
144
|
+
* @returns The replacement result when a host factory changed.
|
|
145
|
+
*/
|
|
146
|
+
reconcile(
|
|
147
|
+
ctx: ExtensionContext,
|
|
148
|
+
options: { allowStaleZentui?: boolean } = {},
|
|
149
|
+
): EditorChangeResult | undefined {
|
|
150
|
+
try {
|
|
151
|
+
if (this.isEditorEnabled()) {
|
|
152
|
+
const currentFactory = ctx.ui.getEditorComponent();
|
|
153
|
+
if (
|
|
154
|
+
isZentuiEditorFactory(currentFactory) &&
|
|
155
|
+
!this.ownsFactory(currentFactory) &&
|
|
156
|
+
!options.allowStaleZentui
|
|
157
|
+
) {
|
|
158
|
+
this.clearEditorOwnership();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!this.editorInstalled || !this.ownsFactory(currentFactory))
|
|
162
|
+
return this.installEditor(ctx);
|
|
163
|
+
} else {
|
|
164
|
+
const currentFactory = ctx.ui.getEditorComponent();
|
|
165
|
+
if (this.editorInstalled || this.ownsFactory(currentFactory))
|
|
166
|
+
return this.uninstall(ctx);
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
reason:
|
|
172
|
+
"the editor could not be reconciled safely; reload Pi to apply this change",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Applies an Editor component configuration patch and reconciles the host.
|
|
179
|
+
*
|
|
180
|
+
* @param patch Editor configuration changes to persist.
|
|
181
|
+
* @param ctx Active Pi extension context.
|
|
182
|
+
* @returns Whether the runtime applied the requested replacement.
|
|
183
|
+
*/
|
|
184
|
+
setComponent(
|
|
185
|
+
patch: Partial<PolishedTuiConfig["components"]["editor"]>,
|
|
186
|
+
ctx: ExtensionContext,
|
|
187
|
+
): { applied: boolean; reason?: string } {
|
|
188
|
+
this.context.saveComponent(patch);
|
|
189
|
+
let result: EditorChangeResult | undefined;
|
|
190
|
+
if (patch.enabled !== undefined && this.isTuiContext(ctx))
|
|
191
|
+
result = this.reconcile(ctx);
|
|
192
|
+
if (patch.style !== undefined && patch.style !== "minimalist")
|
|
193
|
+
this.setMinimalistDecorationActive(false);
|
|
194
|
+
if (patch.modelLabel !== undefined) this.context.onModelLabelChanged(ctx);
|
|
195
|
+
this.context.onProjectRequirementChanged();
|
|
196
|
+
this.requestRender();
|
|
197
|
+
return {
|
|
198
|
+
applied: !result || result.ok,
|
|
199
|
+
reason: result && !result.ok ? result.reason : undefined,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Reports whether the Editor currently owns the host editor factory.
|
|
205
|
+
*
|
|
206
|
+
* @param ctx Active Pi extension context.
|
|
207
|
+
* @returns Whether the installed factory is still active and owned.
|
|
208
|
+
*/
|
|
209
|
+
ownsActiveFactory(ctx: ExtensionContext): boolean {
|
|
210
|
+
if (!this.editorInstalled || !this.installedEditorFactory) return false;
|
|
211
|
+
try {
|
|
212
|
+
return (
|
|
213
|
+
this.ownsFactory(this.installedEditorFactory) &&
|
|
214
|
+
ctx.ui.getEditorComponent() === this.installedEditorFactory
|
|
215
|
+
);
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Reconciles the controller's local ownership with the host factory.
|
|
223
|
+
*
|
|
224
|
+
* @param ctx Active Pi extension context.
|
|
225
|
+
*/
|
|
226
|
+
reconcileOwnership(ctx: ExtensionContext): void {
|
|
227
|
+
this.reconcileObservedOwnership(ctx);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Reports whether the Editor requires project metadata refreshes.
|
|
232
|
+
*
|
|
233
|
+
* @returns Whether a project selector is currently visible.
|
|
234
|
+
*/
|
|
235
|
+
needsProjectRefresh(): boolean {
|
|
236
|
+
const editor = this.context.getConfig().components.editor;
|
|
237
|
+
const minimalist = editor.styles.minimalist;
|
|
238
|
+
return (
|
|
239
|
+
this.isEditorEnabled() &&
|
|
240
|
+
Boolean(
|
|
241
|
+
this.activeTuiContext && this.ownsActiveFactory(this.activeTuiContext),
|
|
242
|
+
) &&
|
|
243
|
+
editor.style === "minimalist" &&
|
|
244
|
+
(minimalist.showGit || minimalist.pathDisplay === "project")
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Marks the Editor decoration timer as active or inactive.
|
|
250
|
+
*
|
|
251
|
+
* @param active Whether the mounted Editor requested timer decoration.
|
|
252
|
+
*/
|
|
253
|
+
setMinimalistDecorationActive(active: boolean): void {
|
|
254
|
+
this.minimalistDecorationActive =
|
|
255
|
+
this.context.sessionLifecycle.isCurrent() &&
|
|
256
|
+
active &&
|
|
257
|
+
Boolean(
|
|
258
|
+
this.activeTuiContext && this.ownsActiveFactory(this.activeTuiContext),
|
|
259
|
+
);
|
|
260
|
+
this.reconcileAgentTimer();
|
|
261
|
+
this.context.onProjectRequirementChanged();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Updates whether an agent turn is currently active.
|
|
266
|
+
*
|
|
267
|
+
* @param active Whether the agent is running.
|
|
268
|
+
*/
|
|
269
|
+
setAgentRunActive(active: boolean): void {
|
|
270
|
+
this.reconcileAgentTimer();
|
|
271
|
+
if (this.context.isAgentActive() !== active)
|
|
272
|
+
this.context.onProjectRequirementChanged();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Stops Editor duration decoration and clears its session-local state.
|
|
277
|
+
*/
|
|
278
|
+
resetAgentTimer(): void {
|
|
279
|
+
this.stopMinimalistDurationUpdates();
|
|
280
|
+
this.stopMinimalistDurationUpdates = () => {};
|
|
281
|
+
this.minimalistDurationUpdatesActive = false;
|
|
282
|
+
this.minimalistDecorationActive = false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Reconciles timer decoration after a style or lifecycle update.
|
|
287
|
+
*/
|
|
288
|
+
reconcileTimers(): void {
|
|
289
|
+
this.reconcileAgentTimer();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Removes the Editor and restores the retained third-party factory.
|
|
294
|
+
*
|
|
295
|
+
* @param ctx Active Pi extension context.
|
|
296
|
+
* @param options Ownership options for stale reload cleanup.
|
|
297
|
+
* @returns Whether restoration succeeded.
|
|
298
|
+
*/
|
|
299
|
+
uninstall(
|
|
300
|
+
ctx: ExtensionContext,
|
|
301
|
+
options: { allowStaleZentui?: boolean } = {},
|
|
302
|
+
): EditorChangeResult {
|
|
303
|
+
let currentFactory: EditorFactory | undefined;
|
|
304
|
+
try {
|
|
305
|
+
currentFactory = ctx.ui.getEditorComponent();
|
|
306
|
+
} catch {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
reason:
|
|
310
|
+
"the current editor factory could not be observed safely; reload Pi to apply this change",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (!currentFactory || !isZentuiEditorFactory(currentFactory)) {
|
|
314
|
+
this.clearEditorOwnership();
|
|
315
|
+
return { ok: true };
|
|
316
|
+
}
|
|
317
|
+
if (!this.ownsFactory(currentFactory) && !options.allowStaleZentui) {
|
|
318
|
+
this.clearEditorOwnership();
|
|
319
|
+
return { ok: true };
|
|
320
|
+
}
|
|
321
|
+
const result = this.replace(
|
|
322
|
+
ctx,
|
|
323
|
+
getZentuiEditorBaseFactory(currentFactory) ??
|
|
324
|
+
(this.editorInstallMode === "wrapper"
|
|
325
|
+
? this.wrappedEditorFactory
|
|
326
|
+
: undefined),
|
|
327
|
+
);
|
|
328
|
+
if (!result.ok) return result;
|
|
329
|
+
this.clearEditorOwnership();
|
|
330
|
+
return { ok: true };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Cleans Editor ownership, timers, deferred reconciliation and host patches.
|
|
335
|
+
*
|
|
336
|
+
* @param ctx Active Pi extension context, when available.
|
|
337
|
+
*/
|
|
338
|
+
cleanup(ctx?: ExtensionContext): void {
|
|
339
|
+
this.resetAgentTimer();
|
|
340
|
+
this.context.sessionLifecycle.shutdown();
|
|
341
|
+
if (ctx && this.isTuiContext(ctx)) {
|
|
342
|
+
try {
|
|
343
|
+
const currentFactory = ctx.ui.getEditorComponent();
|
|
344
|
+
if (currentFactory && this.ownsFactory(currentFactory)) {
|
|
345
|
+
this.replace(
|
|
346
|
+
ctx,
|
|
347
|
+
getZentuiEditorBaseFactory(currentFactory) ??
|
|
348
|
+
(this.editorInstallMode === "wrapper"
|
|
349
|
+
? this.wrappedEditorFactory
|
|
350
|
+
: undefined),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
} catch {
|
|
354
|
+
// Continue cleanup even when Pi cannot expose the current factory.
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
this.clearEditorOwnership();
|
|
358
|
+
++this.accentRailLayoutPatchInstallSerial;
|
|
359
|
+
this.accentRailLayoutPatchCleanup();
|
|
360
|
+
this.accentRailLayoutPatchCleanup = () => {};
|
|
361
|
+
this.activeTuiContext = undefined;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Starts or stops the Editor's one-second duration decoration subscription.
|
|
366
|
+
*/
|
|
367
|
+
private reconcileAgentTimer(): void {
|
|
368
|
+
const ctx = this.activeTuiContext;
|
|
369
|
+
const config = this.context.getConfig().components.editor;
|
|
370
|
+
const needed = Boolean(
|
|
371
|
+
ctx &&
|
|
372
|
+
this.context.sessionLifecycle.isCurrent() &&
|
|
373
|
+
this.context.isAgentActive() &&
|
|
374
|
+
this.context.isAgentDurationActive() &&
|
|
375
|
+
this.minimalistDecorationActive &&
|
|
376
|
+
this.isEditorEnabled() &&
|
|
377
|
+
this.ownsActiveFactory(ctx) &&
|
|
378
|
+
config.style === "minimalist" &&
|
|
379
|
+
config.styles.minimalist.showTimer,
|
|
380
|
+
);
|
|
381
|
+
if (!needed) {
|
|
382
|
+
this.stopMinimalistDurationUpdates();
|
|
383
|
+
this.stopMinimalistDurationUpdates = () => {};
|
|
384
|
+
this.minimalistDurationUpdatesActive = false;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (this.minimalistDurationUpdatesActive) return;
|
|
388
|
+
this.minimalistDurationUpdatesActive = true;
|
|
389
|
+
this.stopMinimalistDurationUpdates = this.context.subscribeAgentDuration(
|
|
390
|
+
() => {
|
|
391
|
+
if (this.activeTuiContext)
|
|
392
|
+
this.reconcileObservedOwnership(this.activeTuiContext);
|
|
393
|
+
this.reconcileAgentTimer();
|
|
394
|
+
if (this.minimalistDurationUpdatesActive) this.requestRender();
|
|
395
|
+
},
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Reports whether a context can safely install the Editor layout.
|
|
401
|
+
*
|
|
402
|
+
* @param ctx Candidate Pi extension context.
|
|
403
|
+
* @returns Whether the context is an interactive TUI context.
|
|
404
|
+
*/
|
|
405
|
+
private isTuiContext(ctx: ExtensionContext): boolean {
|
|
406
|
+
try {
|
|
407
|
+
const mode = (ctx as ExtensionContext & { mode?: string }).mode;
|
|
408
|
+
return ctx.hasUI && (mode === undefined || mode === "tui");
|
|
409
|
+
} catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Returns whether the configured Editor style can be installed.
|
|
416
|
+
*
|
|
417
|
+
* @returns Whether the Editor is enabled and supported.
|
|
418
|
+
*/
|
|
419
|
+
private isEditorEnabled(): boolean {
|
|
420
|
+
const editor = this.context.getConfig().components.editor;
|
|
421
|
+
return (
|
|
422
|
+
editor.enabled &&
|
|
423
|
+
!hasUnsupportedComponentStyle(this.context.getConfig(), "editor")
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Tests whether a factory belongs to this controller.
|
|
429
|
+
*
|
|
430
|
+
* @param factory Candidate host factory.
|
|
431
|
+
* @returns Whether this controller owns the factory.
|
|
432
|
+
*/
|
|
433
|
+
private ownsFactory(factory: EditorFactory | undefined): boolean {
|
|
434
|
+
return isOwnedEditorFactory(factory, this.ownerToken);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Records the currently installed owned factory.
|
|
439
|
+
*
|
|
440
|
+
* @param factory Factory created by this controller.
|
|
441
|
+
*/
|
|
442
|
+
private trackFactory(factory: EditorFactory): void {
|
|
443
|
+
const baseFactory = getZentuiEditorBaseFactory(factory);
|
|
444
|
+
this.wrappedEditorFactory = baseFactory;
|
|
445
|
+
this.installedEditorFactory = factory;
|
|
446
|
+
this.editorInstallMode = baseFactory ? "wrapper" : "standalone";
|
|
447
|
+
this.editorInstalled = true;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Clears local factory ownership and decoration callbacks.
|
|
452
|
+
*/
|
|
453
|
+
private clearEditorOwnership(): void {
|
|
454
|
+
this.setMinimalistDecorationActive(false);
|
|
455
|
+
this.requestEditorRender = undefined;
|
|
456
|
+
this.wrappedEditorFactory = undefined;
|
|
457
|
+
this.installedEditorFactory = undefined;
|
|
458
|
+
this.editorInstallMode = "none";
|
|
459
|
+
this.editorInstalled = false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Replaces the host Editor factory while preserving expanded text.
|
|
464
|
+
*
|
|
465
|
+
* @param ctx Active Pi extension context.
|
|
466
|
+
* @param factory Replacement factory, or undefined for native restoration.
|
|
467
|
+
* @returns A normalized replacement result.
|
|
468
|
+
*/
|
|
469
|
+
private replace(
|
|
470
|
+
ctx: ExtensionContext,
|
|
471
|
+
factory: EditorFactory | undefined,
|
|
472
|
+
): EditorChangeResult {
|
|
473
|
+
const result = replaceEditorComponentWithExpandedText(ctx.ui, factory);
|
|
474
|
+
return result.ok
|
|
475
|
+
? result
|
|
476
|
+
: { ok: false, reason: editorTransferFailureMessage(result.reason) };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Installs a native or third-party-wrapping Editor factory.
|
|
481
|
+
*
|
|
482
|
+
* @param ctx Active Pi extension context.
|
|
483
|
+
* @returns A normalized replacement result.
|
|
484
|
+
*/
|
|
485
|
+
private installEditor(ctx: ExtensionContext): EditorChangeResult {
|
|
486
|
+
const currentFactory = ctx.ui.getEditorComponent();
|
|
487
|
+
if (currentFactory && currentFactory === this.installedEditorFactory) {
|
|
488
|
+
this.editorInstalled = true;
|
|
489
|
+
return { ok: true };
|
|
490
|
+
}
|
|
491
|
+
const baseFactory =
|
|
492
|
+
getZentuiEditorBaseFactory(currentFactory) ??
|
|
493
|
+
(currentFactory && !isZentuiEditorFactory(currentFactory)
|
|
494
|
+
? currentFactory
|
|
495
|
+
: undefined);
|
|
496
|
+
const runtime = this.factoryRuntime(ctx);
|
|
497
|
+
const nextFactory = baseFactory
|
|
498
|
+
? createWrappedEditorFactory(ctx, baseFactory, runtime)
|
|
499
|
+
: createEditorFactory(ctx, runtime);
|
|
500
|
+
const result = this.replace(ctx, nextFactory);
|
|
501
|
+
if (!result.ok) return result;
|
|
502
|
+
this.trackFactory(nextFactory);
|
|
503
|
+
return { ok: true };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Builds the narrow selector set consumed by Editor factories.
|
|
508
|
+
*
|
|
509
|
+
* @param ctx Active Pi extension context.
|
|
510
|
+
* @returns Factory selectors and callbacks.
|
|
511
|
+
*/
|
|
512
|
+
private factoryRuntime(ctx: ExtensionContext): EditorFactoryRuntime {
|
|
513
|
+
return {
|
|
514
|
+
ownerToken: this.ownerToken,
|
|
515
|
+
sessionTheme: ctx.ui.theme,
|
|
516
|
+
getConfig: this.context.getConfig,
|
|
517
|
+
getState: this.context.getState,
|
|
518
|
+
getThinkingLevel: this.context.getThinkingLevel,
|
|
519
|
+
getContextWindow: this.context.getContextWindow,
|
|
520
|
+
getContextPercent: this.context.getContextPercent,
|
|
521
|
+
getAgentDurationMs: this.context.getAgentDurationMs,
|
|
522
|
+
isAgentActive: this.context.isAgentActive,
|
|
523
|
+
getProjectRoot: this.context.getProjectRoot,
|
|
524
|
+
onRender: (requestRender) => {
|
|
525
|
+
this.requestEditorRender = requestRender;
|
|
526
|
+
},
|
|
527
|
+
onDecorationActive: (active) =>
|
|
528
|
+
this.setMinimalistDecorationActive(active),
|
|
529
|
+
isAccentRailActive: () =>
|
|
530
|
+
this.context.sessionLifecycle.isCurrent() &&
|
|
531
|
+
this.ownsActiveFactory(ctx) &&
|
|
532
|
+
this.isEditorEnabled() &&
|
|
533
|
+
this.context.getConfig().components.editor.style === "accent-rail",
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Reconciles ownership after Pi or another extension replaces the factory.
|
|
539
|
+
*
|
|
540
|
+
* @param ctx Active Pi extension context.
|
|
541
|
+
*/
|
|
542
|
+
private reconcileObservedOwnership(ctx: ExtensionContext): void {
|
|
543
|
+
let currentFactory: EditorFactory | undefined;
|
|
544
|
+
try {
|
|
545
|
+
currentFactory = ctx.ui.getEditorComponent();
|
|
546
|
+
} catch {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (currentFactory && this.ownsFactory(currentFactory)) {
|
|
550
|
+
this.trackFactory(currentFactory);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
this.clearEditorOwnership();
|
|
554
|
+
this.context.onProjectRequirementChanged();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Defers one ownership check until the host has finished mounting its UI.
|
|
559
|
+
*
|
|
560
|
+
* @param ctx Active Pi extension context.
|
|
561
|
+
*/
|
|
562
|
+
private scheduleReconciliation(ctx: ExtensionContext): void {
|
|
563
|
+
this.context.sessionLifecycle.defer(() => {
|
|
564
|
+
if (!this.isEditorEnabled()) return;
|
|
565
|
+
try {
|
|
566
|
+
const currentFactory = ctx.ui.getEditorComponent();
|
|
567
|
+
if (currentFactory === this.installedEditorFactory) {
|
|
568
|
+
this.context.onProjectRequirementChanged();
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
} catch {
|
|
572
|
+
this.scheduleReconciliation(ctx);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
this.reconcileObservedOwnership(ctx);
|
|
576
|
+
this.context.onProjectRequirementChanged();
|
|
577
|
+
this.requestRender();
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Requests both the mounted editor redraw and the shared runtime redraw.
|
|
583
|
+
*/
|
|
584
|
+
requestRender(): void {
|
|
585
|
+
this.requestEditorRender?.();
|
|
586
|
+
this.context.render.request();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Converts a transfer failure into a user-facing Editor transition message.
|
|
592
|
+
*
|
|
593
|
+
* @param reason Failure reported by the expanded-text transfer helper.
|
|
594
|
+
* @returns Stable message suitable for settings notifications.
|
|
595
|
+
*/
|
|
596
|
+
function editorTransferFailureMessage(
|
|
597
|
+
reason: EditorTransferFailureReason,
|
|
598
|
+
): string {
|
|
599
|
+
switch (reason) {
|
|
600
|
+
case "unsupported-transfer-api":
|
|
601
|
+
return "this Pi version cannot safely transfer expanded editor text; reload Pi to apply this change";
|
|
602
|
+
case "editor-factory-snapshot-failed":
|
|
603
|
+
return "the current editor factory could not be read safely; reload Pi to apply this change";
|
|
604
|
+
case "editor-text-snapshot-failed":
|
|
605
|
+
return "expanded editor text could not be read safely; reload Pi to apply this change";
|
|
606
|
+
case "editor-text-preparation-failed":
|
|
607
|
+
return "expanded editor text could not be prepared safely; reload Pi to apply this change";
|
|
608
|
+
case "editor-replacement-failed-with-rollback":
|
|
609
|
+
return "the editor replacement failed; the previous factory was reapplied, but editor instance identity is not guaranteed";
|
|
610
|
+
case "editor-replacement-rollback-failed":
|
|
611
|
+
return "the editor replacement and previous-factory rollback both failed; reload Pi before editing";
|
|
612
|
+
}
|
|
613
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { ZentuiConfig } from "
|
|
3
|
-
import {
|
|
2
|
+
import type { ZentuiConfig } from "../../app/config/shell.ts";
|
|
3
|
+
import {
|
|
4
|
+
type FormatToken,
|
|
5
|
+
parseFooterFormat,
|
|
6
|
+
} from "../footer/footer-format.ts";
|
|
4
7
|
import {
|
|
5
8
|
EDITOR_ACCENT_FALLBACK,
|
|
6
9
|
renderStyleForSourceOrFallback,
|
|
7
10
|
safeThemeFg,
|
|
8
|
-
} from "
|
|
11
|
+
} from "../../shared/style.ts";
|
|
9
12
|
|
|
10
13
|
export type EditorMetadataValues = {
|
|
11
14
|
model: string;
|