pi-one-ui 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -1
- package/README.en.md +19 -6
- package/README.md +19 -6
- package/extensions/app/config/context.ts +42 -0
- package/extensions/app/config/editor.ts +80 -0
- package/extensions/app/config/footer.ts +291 -0
- package/extensions/app/config/panel.ts +1 -5
- package/extensions/app/config/renderer.ts +1 -6
- package/extensions/app/config/shell.ts +358 -1435
- package/extensions/app/config/store.ts +22 -1
- package/extensions/app/config/values.ts +33 -0
- package/extensions/app/config/working-line.ts +100 -0
- package/extensions/app/overlay/input-router.ts +56 -32
- package/extensions/app/overlay/overlay-manager.ts +183 -21
- package/extensions/app/runtime/event-coordinator.ts +90 -42
- package/extensions/{features → app/runtime}/flush-docked-bash.ts +12 -2
- package/extensions/app/runtime/render-scheduler.ts +35 -31
- package/extensions/app/runtime/tui-runtime.ts +315 -312
- package/extensions/app/settings/controller.ts +202 -0
- package/extensions/app/settings/panel.ts +81 -0
- package/extensions/app/settings/presets.ts +43 -0
- package/extensions/features/context-inspector/index.ts +83 -662
- package/extensions/layouts/context/index.ts +58 -161
- package/extensions/layouts/context/message/user-message.ts +1 -1
- package/extensions/layouts/context/renderer/index.ts +31 -13
- package/extensions/layouts/context/renderer/mouse/interaction.ts +29 -14
- package/extensions/layouts/editor/controller.ts +9 -18
- package/extensions/layouts/editor/factory.ts +3 -3
- package/extensions/layouts/footer/controller.ts +30 -19
- package/extensions/layouts/footer/footer.ts +2 -2
- package/extensions/layouts/overlay/context-inspector.ts +497 -0
- package/extensions/{app → layouts}/overlay/selector-border.ts +5 -5
- package/extensions/{app → layouts}/overlay/selector-controller.ts +1 -1
- package/extensions/layouts/overlay/settings-panel.ts +359 -0
- package/extensions/{app/commands → layouts/overlay}/settings-previews.ts +4 -4
- package/extensions/layouts/working-line/controller.ts +3 -29
- package/extensions/layouts/working-line/working-line.ts +17 -114
- package/extensions/shared/working-line-text.ts +108 -0
- package/package.json +1 -1
- package/extensions/app/host/pi-extension-port.ts +0 -25
- package/extensions/app/host/pi-ui-port.ts +0 -52
- package/extensions/app/host/tui-capabilities.ts +0 -27
- package/extensions/app/ownership/layout-registry.ts +0 -65
- package/extensions/app/ownership.ts +0 -32
- package/extensions/app/panel.ts +0 -770
- package/extensions/app/presets.ts +0 -35
- package/extensions/app/runtime/runtime-state.ts +0 -83
- /package/extensions/{layouts/working-line → app/config}/working-line-messages.ts +0 -0
- /package/extensions/app/{ownership → runtime}/prototype-patch-registry.ts +0 -0
|
@@ -1,20 +1,24 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
2
|
import type {
|
|
2
3
|
ExtensionAPI,
|
|
3
4
|
ExtensionContext,
|
|
4
5
|
Theme,
|
|
5
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import aliases from "../../features/aliases.ts";
|
|
8
|
+
import contextInspector from "../../features/context-inspector/index.ts";
|
|
9
|
+
import effortCommand from "../../features/effort-command.ts";
|
|
10
|
+
import sessionReference from "../../features/session-reference/index.ts";
|
|
11
|
+
import subagentAutocomplete from "../../features/subagent-autocomplete.ts";
|
|
6
12
|
import registerContext, {
|
|
7
|
-
type ContextExtensionOptions,
|
|
8
13
|
type ContextRuntimeController,
|
|
9
14
|
} from "../../layouts/context/index.ts";
|
|
10
|
-
import { EditorLayoutController } from "../../layouts/editor/
|
|
11
|
-
import { FooterLayoutController } from "../../layouts/footer/
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from "../../layouts/working-line/controller.ts";
|
|
15
|
+
import { EditorLayoutController } from "../../layouts/editor/index.ts";
|
|
16
|
+
import { FooterLayoutController } from "../../layouts/footer/index.ts";
|
|
17
|
+
import registerHeader, {
|
|
18
|
+
applyStartupHeader,
|
|
19
|
+
} from "../../layouts/header/index.ts";
|
|
20
|
+
import { SelectorController } from "../../layouts/overlay/selector-controller.ts";
|
|
21
|
+
import { WorkingLineLayoutController } from "../../layouts/working-line/controller.ts";
|
|
18
22
|
import {
|
|
19
23
|
renderTurnSummaryEntry,
|
|
20
24
|
TURN_SUMMARY_ENTRY_TYPE,
|
|
@@ -29,261 +33,202 @@ import {
|
|
|
29
33
|
syncState,
|
|
30
34
|
} from "../../services/session-state.ts";
|
|
31
35
|
import { resolveFooterTelemetry } from "../../services/telemetry.ts";
|
|
36
|
+
import { type Config, config, setConfig } from "../config/renderer.ts";
|
|
32
37
|
import {
|
|
33
|
-
|
|
38
|
+
type EditorComponentConfig,
|
|
34
39
|
hasUnsupportedComponentStyle,
|
|
35
40
|
loadConfig,
|
|
36
|
-
mergeConfig,
|
|
37
41
|
type PolishedTuiConfig,
|
|
38
|
-
saveEditorComponentPatch,
|
|
39
|
-
saveFooterComponentPatch,
|
|
40
|
-
saveWorkingLineComponentPatch,
|
|
41
42
|
} from "../config/shell.ts";
|
|
42
|
-
import {
|
|
43
|
+
import { InputRouter } from "../overlay/input-router.ts";
|
|
44
|
+
import { OverlayManager } from "../overlay/overlay-manager.ts";
|
|
43
45
|
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} from "../
|
|
47
|
-
import {
|
|
48
|
-
import { SelectorController } from "../overlay/selector-controller.ts";
|
|
49
|
-
import { LayoutRegistry } from "../ownership/layout-registry.ts";
|
|
50
|
-
import { showOneUiPanel, type TuiPanelRuntime } from "../panel.ts";
|
|
46
|
+
type SettingsChange,
|
|
47
|
+
SettingsController,
|
|
48
|
+
} from "../settings/controller.ts";
|
|
49
|
+
import { showOneUiPanel } from "../settings/panel.ts";
|
|
51
50
|
import { EventCoordinator } from "./event-coordinator.ts";
|
|
51
|
+
import { installFlushDockedBash } from "./flush-docked-bash.ts";
|
|
52
52
|
import { RenderScheduler } from "./render-scheduler.ts";
|
|
53
|
-
import { RuntimeStateStore } from "./runtime-state.ts";
|
|
54
53
|
import { SessionLifecycle } from "./session-lifecycle.ts";
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
readonly extensions: PiExtensionPort;
|
|
58
|
-
readonly state: RuntimeStateStore;
|
|
59
|
-
readonly render: RenderScheduler;
|
|
60
|
-
readonly layouts: LayoutRegistry;
|
|
61
|
-
readonly ui: () => PiUiPort | undefined;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* The single composition root for the plugin. Remaining compatibility glue
|
|
66
|
-
* is mounted behind this seam while Layout ownership is finalized.
|
|
67
|
-
*/
|
|
55
|
+
/** 产品入口只组织组件、Feature 和共享生命周期。 */
|
|
68
56
|
export class TuiRuntime {
|
|
69
|
-
|
|
70
|
-
readonly
|
|
71
|
-
readonly
|
|
72
|
-
readonly
|
|
73
|
-
|
|
57
|
+
readonly render = new RenderScheduler();
|
|
58
|
+
readonly settings = new SettingsController();
|
|
59
|
+
readonly overlays = new OverlayManager();
|
|
60
|
+
readonly input = new InputRouter();
|
|
74
61
|
private readonly coordinator: EventCoordinator;
|
|
75
|
-
private readonly
|
|
76
|
-
private
|
|
62
|
+
private readonly coordinatedPi: ExtensionAPI;
|
|
63
|
+
private context: ContextRuntimeController | undefined;
|
|
77
64
|
private installed = false;
|
|
78
|
-
private
|
|
79
|
-
private footerController: FooterLayoutController | undefined;
|
|
80
|
-
private workingLineController: WorkingLineLayoutController | undefined;
|
|
81
|
-
private contextController: ContextRuntimeController | undefined;
|
|
65
|
+
private currentContext: ExtensionContext | undefined;
|
|
82
66
|
|
|
83
|
-
/**
|
|
84
|
-
* Creates the runtime services and coordinates session lifecycle ownership.
|
|
85
|
-
*/
|
|
86
67
|
constructor(pi: ExtensionAPI) {
|
|
87
|
-
this.pi = pi;
|
|
88
|
-
this.extensions = createPiExtensionPort(pi);
|
|
89
|
-
this.renderScheduler = new RenderScheduler((force) =>
|
|
90
|
-
this.activeUi?.requestRender(force),
|
|
91
|
-
);
|
|
92
68
|
this.coordinator = new EventCoordinator({
|
|
93
|
-
on: (event, handler) =>
|
|
94
|
-
this.extensions.on(event as never, handler as never),
|
|
69
|
+
on: (event, handler) => pi.on(event as never, handler as never),
|
|
95
70
|
});
|
|
71
|
+
this.coordinatedPi = this.coordinator.coordinate(pi);
|
|
96
72
|
}
|
|
97
73
|
|
|
98
|
-
/**
|
|
99
|
-
* Returns the shared render scheduler used by installed layouts.
|
|
100
|
-
*/
|
|
101
|
-
get render(): RenderScheduler {
|
|
102
|
-
return this.renderScheduler;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Returns the active session UI port, when a TUI session is mounted.
|
|
107
|
-
*/
|
|
108
|
-
ui(): PiUiPort | undefined {
|
|
109
|
-
return this.activeUi;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Exposes the narrow runtime contract consumed by the settings panel.
|
|
114
|
-
*
|
|
115
|
-
* @returns Layout-independent panel operations.
|
|
116
|
-
*/
|
|
117
|
-
private panelRuntime(): TuiPanelRuntime {
|
|
118
|
-
return {
|
|
119
|
-
setEditorComponent: (patch, ctx) =>
|
|
120
|
-
this.editorController?.setComponent(patch, ctx) ?? { applied: false },
|
|
121
|
-
setUserMessagesComponent: (patch, ctx) =>
|
|
122
|
-
this.contextController?.setUserMessagesComponent(patch, ctx),
|
|
123
|
-
setWorkingLineComponent: (patch, ctx) =>
|
|
124
|
-
this.workingLineController?.setComponent(patch, ctx) ?? {
|
|
125
|
-
applied: false,
|
|
126
|
-
reason: "WorkingLine runtime is not available",
|
|
127
|
-
},
|
|
128
|
-
setFooterComponent: (patch, ctx) =>
|
|
129
|
-
this.footerController?.setComponent(patch, ctx),
|
|
130
|
-
setContextMode: (mode, ctx) => this.contextController?.setMode(mode, ctx),
|
|
131
|
-
updateContextConfig: (patch, ctx) =>
|
|
132
|
-
this.contextController?.updateConfig(patch, ctx),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Installs the runtime once and mounts remaining layout lifecycle glue.
|
|
138
|
-
*/
|
|
139
74
|
install(): void {
|
|
140
|
-
if (this.installed)
|
|
75
|
+
if (this.installed) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
141
78
|
this.installed = true;
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
79
|
+
setConfig(this.settings.snapshot().renderer);
|
|
80
|
+
const pi = this.coordinatedPi;
|
|
81
|
+
this.coordinator.on("session_start", (_event, ctx) => {
|
|
82
|
+
this.overlays.startSession();
|
|
83
|
+
this.input.clear();
|
|
84
|
+
this.currentContext = ctx;
|
|
85
|
+
});
|
|
86
|
+
this.coordinator.on("session_shutdown", () => {
|
|
87
|
+
this.overlays.reset();
|
|
88
|
+
this.input.clear();
|
|
89
|
+
});
|
|
90
|
+
const bindings = createLayoutRuntime(pi, {
|
|
91
|
+
render: this.render,
|
|
92
|
+
settings: this.settings,
|
|
93
|
+
onConfigChanged: (previous, ctx) => {
|
|
94
|
+
this.context?.applyConfig(ctx, previous);
|
|
95
|
+
if (previous.showStartupHeader !== config.showStartupHeader) {
|
|
96
|
+
applyStartupHeader(ctx);
|
|
97
|
+
}
|
|
146
98
|
},
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
99
|
+
});
|
|
100
|
+
bindings.workingLineController.setSummaryWriterEnabled(false);
|
|
101
|
+
bindings.installEventHandlers(this.coordinator);
|
|
102
|
+
registerHeader(pi);
|
|
103
|
+
this.context = registerContext(pi, {
|
|
104
|
+
getConfig: bindings.getConfig,
|
|
105
|
+
services: {
|
|
106
|
+
input: this.input,
|
|
107
|
+
overlays: this.overlays,
|
|
108
|
+
render: this.render,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
if (config.enableAliases) {
|
|
112
|
+
aliases(pi);
|
|
113
|
+
}
|
|
114
|
+
if (config.enableEffortCommand) {
|
|
115
|
+
effortCommand(pi);
|
|
116
|
+
}
|
|
117
|
+
if (config.enableContextCommand) {
|
|
118
|
+
contextInspector(pi, this.overlays);
|
|
119
|
+
}
|
|
120
|
+
if (config.enableSessionReference) {
|
|
121
|
+
sessionReference(pi);
|
|
122
|
+
}
|
|
123
|
+
if (config.enableSubagentAutocomplete) {
|
|
124
|
+
subagentAutocomplete(pi);
|
|
125
|
+
}
|
|
126
|
+
let cleanupBash: (() => void) | undefined;
|
|
127
|
+
this.coordinator.on("session_start", (_event, ctx) => {
|
|
159
128
|
if (ctx.mode === "tui" && ctx.hasUI) {
|
|
160
|
-
|
|
161
|
-
// connect that callback when they migrate behind this runtime.
|
|
162
|
-
this.activeUi = createPiUiPort(ctx);
|
|
129
|
+
cleanupBash = installFlushDockedBash();
|
|
163
130
|
}
|
|
164
131
|
});
|
|
165
|
-
this.coordinator.on("session_shutdown", (
|
|
166
|
-
|
|
167
|
-
|
|
132
|
+
this.coordinator.on("session_shutdown", () => {
|
|
133
|
+
cleanupBash?.();
|
|
134
|
+
cleanupBash = undefined;
|
|
135
|
+
this.currentContext = undefined;
|
|
168
136
|
});
|
|
169
137
|
this.coordinator.install();
|
|
170
|
-
|
|
171
|
-
this.extensions.registerCommand("oneui", {
|
|
138
|
+
pi.registerCommand("oneui", {
|
|
172
139
|
description: "Open pi-one-ui settings",
|
|
173
140
|
handler: async (_args, ctx) => {
|
|
174
|
-
await showOneUiPanel(ctx, {
|
|
141
|
+
await showOneUiPanel(ctx, {
|
|
142
|
+
settings: this.settings,
|
|
143
|
+
overlays: this.overlays,
|
|
144
|
+
});
|
|
175
145
|
},
|
|
176
146
|
});
|
|
177
147
|
}
|
|
148
|
+
|
|
149
|
+
async dispose(ctx = this.currentContext): Promise<void> {
|
|
150
|
+
if (ctx && this.currentContext) {
|
|
151
|
+
await this.coordinator.dispatch(
|
|
152
|
+
"session_shutdown",
|
|
153
|
+
{ type: "session_shutdown", reason: "quit" },
|
|
154
|
+
ctx,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
178
158
|
}
|
|
179
159
|
|
|
180
|
-
/**
|
|
181
|
-
* Creates a single TUI runtime for the plugin composition root.
|
|
182
|
-
*/
|
|
183
160
|
export function createTuiRuntime(pi: ExtensionAPI): TuiRuntime {
|
|
184
161
|
return new TuiRuntime(pi);
|
|
185
162
|
}
|
|
186
163
|
|
|
187
|
-
/**
|
|
188
|
-
* Reports whether a context exposes the interactive TUI layout.
|
|
189
|
-
*
|
|
190
|
-
* @param ctx Candidate Pi extension context.
|
|
191
|
-
* @returns Whether the context can install TUI-owned layouts.
|
|
192
|
-
*/
|
|
193
164
|
function isTuiContext(ctx: ExtensionContext): boolean {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return ctx.hasUI && (mode === undefined || mode === "tui");
|
|
197
|
-
} catch {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
165
|
+
const mode = ctx.mode;
|
|
166
|
+
return ctx.hasUI && (mode === undefined || mode === "tui");
|
|
200
167
|
}
|
|
201
168
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
*/
|
|
209
|
-
function createLayoutRuntime(
|
|
169
|
+
type LayoutRuntimeOptions = {
|
|
170
|
+
render?: RenderScheduler;
|
|
171
|
+
settings?: SettingsController;
|
|
172
|
+
onConfigChanged?: (previous: Config, ctx: ExtensionContext) => void;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/** 生产运行时与组件集成测试共同使用的实际装配。 */
|
|
176
|
+
export function createLayoutRuntime(
|
|
210
177
|
pi: ExtensionAPI,
|
|
211
|
-
|
|
178
|
+
options: LayoutRuntimeOptions = {},
|
|
212
179
|
) {
|
|
213
180
|
const sessionLifecycle = new SessionLifecycle();
|
|
181
|
+
const render = options.render ?? new RenderScheduler();
|
|
182
|
+
const settings = options.settings ?? new SettingsController();
|
|
214
183
|
let currentConfig: PolishedTuiConfig = loadConfig();
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
184
|
+
let unsubscribeConfig: (() => void) | undefined;
|
|
185
|
+
let activeTheme: Theme | undefined;
|
|
186
|
+
let minimalistProjectRoot: string | undefined;
|
|
187
|
+
let activeTuiContext: ExtensionContext | undefined;
|
|
218
188
|
const sessionState = new SessionStateService({
|
|
219
189
|
getCacheHitIcon: () => currentConfig.icons.cacheHit,
|
|
220
190
|
resolveTelemetry: resolveFooterTelemetry,
|
|
221
191
|
syncState,
|
|
222
192
|
});
|
|
223
193
|
const state = sessionState.state;
|
|
224
|
-
// Keep the capability guard defensive for hosts with incomplete extension APIs.
|
|
225
194
|
if (typeof pi.registerEntryRenderer === "function") {
|
|
226
|
-
pi.registerEntryRenderer(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
195
|
+
pi.registerEntryRenderer(
|
|
196
|
+
TURN_SUMMARY_ENTRY_TYPE,
|
|
197
|
+
(entry, entryOptions, theme) =>
|
|
198
|
+
renderTurnSummaryEntry(
|
|
199
|
+
entry,
|
|
200
|
+
{
|
|
201
|
+
...entryOptions,
|
|
202
|
+
workingLineHigh: currentConfig.colors.workingLineHigh,
|
|
203
|
+
},
|
|
204
|
+
theme,
|
|
205
|
+
),
|
|
235
206
|
);
|
|
236
207
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Requests redraws from the Editor controller and shared render seam.
|
|
243
|
-
*/
|
|
244
|
-
const refresh = () => {
|
|
245
|
-
if (!sessionLifecycle.isCurrent()) return;
|
|
246
|
-
editorController.requestRender();
|
|
208
|
+
const refresh = (): void => {
|
|
209
|
+
if (sessionLifecycle.isCurrent()) {
|
|
210
|
+
render.request();
|
|
211
|
+
}
|
|
247
212
|
};
|
|
248
213
|
const liveContext = new LiveContextController(sessionLifecycle, refresh);
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Reloads the canonical configuration after an external test-support write.
|
|
254
|
-
*
|
|
255
|
-
* @returns The refreshed normalized configuration snapshot.
|
|
256
|
-
*/
|
|
257
|
-
const reloadConfig = (): PolishedTuiConfig => {
|
|
258
|
-
currentConfig = loadConfig();
|
|
259
|
-
return currentConfig;
|
|
260
|
-
};
|
|
214
|
+
const getConfig = () => currentConfig;
|
|
215
|
+
const syncFooterState = (ctx: ExtensionContext) => sessionState.sync(ctx);
|
|
261
216
|
const workingLineController = new WorkingLineLayoutController({
|
|
262
217
|
pi,
|
|
263
|
-
getConfig
|
|
264
|
-
saveComponent: (patch) => {
|
|
265
|
-
currentConfig = saveWorkingLineComponentPatch(patch);
|
|
266
|
-
return currentConfig;
|
|
267
|
-
},
|
|
218
|
+
getConfig,
|
|
268
219
|
getTheme: () => activeTheme as Theme,
|
|
269
220
|
sessionLifecycle,
|
|
270
221
|
refresh,
|
|
271
|
-
onAgentActiveChanged: (active) =>
|
|
272
|
-
editorController.setAgentRunActive(active)
|
|
273
|
-
},
|
|
222
|
+
onAgentActiveChanged: (active) =>
|
|
223
|
+
editorController.setAgentRunActive(active),
|
|
274
224
|
});
|
|
275
|
-
const getThinkingLevel = () =>
|
|
276
|
-
sessionLifecycle.isCurrent() ? pi.getThinkingLevel() : ("off" as const);
|
|
277
225
|
const editorController = new EditorLayoutController({
|
|
278
|
-
getConfig
|
|
279
|
-
saveComponent: (patch) => {
|
|
280
|
-
currentConfig = saveEditorComponentPatch(patch);
|
|
281
|
-
return currentConfig;
|
|
282
|
-
},
|
|
226
|
+
getConfig,
|
|
283
227
|
getState: () => state,
|
|
284
228
|
sessionLifecycle,
|
|
285
|
-
render
|
|
286
|
-
getThinkingLevel
|
|
229
|
+
render,
|
|
230
|
+
getThinkingLevel: () =>
|
|
231
|
+
sessionLifecycle.isCurrent() ? pi.getThinkingLevel() : "off",
|
|
287
232
|
getAgentDurationMs: () => workingLineController.duration().elapsedMs() ?? 0,
|
|
288
233
|
isAgentActive: () => workingLineController.isAgentActive(),
|
|
289
234
|
isAgentDurationActive: () => workingLineController.duration().isActive(),
|
|
@@ -291,13 +236,14 @@ function createLayoutRuntime(
|
|
|
291
236
|
workingLineController.duration().subscribe(() => listener()),
|
|
292
237
|
getProjectRoot: () => minimalistProjectRoot,
|
|
293
238
|
onProjectRequirementChanged: () => {
|
|
294
|
-
if (activeTuiContext)
|
|
239
|
+
if (activeTuiContext) {
|
|
240
|
+
projectRefreshService.reconcile(activeTuiContext);
|
|
241
|
+
}
|
|
295
242
|
},
|
|
296
|
-
onModelLabelChanged:
|
|
243
|
+
onModelLabelChanged: syncFooterState,
|
|
297
244
|
});
|
|
298
|
-
const syncFooterState = (ctx: ExtensionContext) => sessionState.sync(ctx);
|
|
299
245
|
const projectRefreshService = new ProjectRefreshService({
|
|
300
|
-
getConfig
|
|
246
|
+
getConfig,
|
|
301
247
|
state,
|
|
302
248
|
sessionLifecycle,
|
|
303
249
|
getFooterReferences: () => footerController.installedFooterReferences(),
|
|
@@ -312,133 +258,206 @@ function createLayoutRuntime(
|
|
|
312
258
|
});
|
|
313
259
|
const scheduleProjectRefresh = (
|
|
314
260
|
ctx: ExtensionContext,
|
|
315
|
-
|
|
316
|
-
) => projectRefreshService.schedule(ctx,
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Synchronizes shared session state and requests a visible redraw.
|
|
320
|
-
*
|
|
321
|
-
* @param ctx Active Pi extension context.
|
|
322
|
-
* @param project Whether project data should be scheduled.
|
|
323
|
-
*/
|
|
324
|
-
const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
|
|
325
|
-
if (!sessionLifecycle.isCurrent() || !ctx.hasUI) return;
|
|
326
|
-
editorController.reconcileOwnership(ctx);
|
|
327
|
-
syncFooterState(ctx);
|
|
328
|
-
if (project) scheduleProjectRefresh(ctx);
|
|
329
|
-
refresh();
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
const selectorController = new SelectorController({
|
|
333
|
-
getConfig: getCurrentConfig,
|
|
334
|
-
});
|
|
261
|
+
refreshOptions?: ScheduleProjectRefreshOptions,
|
|
262
|
+
) => projectRefreshService.schedule(ctx, refreshOptions);
|
|
263
|
+
const selectorController = new SelectorController({ getConfig });
|
|
335
264
|
const footerController = new FooterLayoutController({
|
|
336
|
-
getConfig
|
|
337
|
-
saveComponent: (patch) => {
|
|
338
|
-
currentConfig = saveFooterComponentPatch(patch);
|
|
339
|
-
return currentConfig;
|
|
340
|
-
},
|
|
265
|
+
getConfig,
|
|
341
266
|
state,
|
|
342
267
|
sessionLifecycle,
|
|
268
|
+
render,
|
|
343
269
|
refresh,
|
|
344
270
|
scheduleProjectRefresh: (ctx) => scheduleProjectRefresh(ctx),
|
|
345
271
|
getLiveContext: () => liveContext.get(),
|
|
346
272
|
onProjectRequirementChanged: (ctx, force) =>
|
|
347
273
|
projectRefreshService.reconcile(ctx, force),
|
|
348
|
-
onModelLabelChanged:
|
|
274
|
+
onModelLabelChanged: syncFooterState,
|
|
349
275
|
});
|
|
350
276
|
|
|
351
|
-
const
|
|
352
|
-
|
|
277
|
+
const applyConfig = (
|
|
278
|
+
previous: PolishedTuiConfig,
|
|
279
|
+
previousRenderer: Config,
|
|
280
|
+
ctx: ExtensionContext,
|
|
281
|
+
change?: SettingsChange,
|
|
282
|
+
): void => {
|
|
283
|
+
if (!sessionLifecycle.isCurrent() || !isTuiContext(ctx)) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const oldEditor = previous.components.editor;
|
|
287
|
+
const editor = currentConfig.components.editor;
|
|
288
|
+
const patch: Partial<EditorComponentConfig> = {};
|
|
289
|
+
for (const key of [
|
|
290
|
+
"style",
|
|
291
|
+
"modelLabel",
|
|
292
|
+
"borderColorMode",
|
|
293
|
+
"viewportIndicators",
|
|
294
|
+
] as const) {
|
|
295
|
+
if (oldEditor[key] !== editor[key]) {
|
|
296
|
+
Object.assign(patch, { [key]: editor[key] });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const preset = change?.id.startsWith("preset:") ?? false;
|
|
300
|
+
if (
|
|
301
|
+
preset ||
|
|
302
|
+
change?.id === "editorStyle" ||
|
|
303
|
+
hasUnsupportedComponentStyle(previous, "editor") !==
|
|
304
|
+
hasUnsupportedComponentStyle(currentConfig, "editor")
|
|
305
|
+
) {
|
|
306
|
+
patch.style = editor.style;
|
|
307
|
+
}
|
|
308
|
+
const editorChange = editorController.applyConfig(patch, ctx);
|
|
309
|
+
const oldFooter = previous.components.footer;
|
|
310
|
+
const footer = currentConfig.components.footer;
|
|
311
|
+
const footerChange = footerController.applyConfig(
|
|
312
|
+
{
|
|
313
|
+
...(oldFooter.style !== footer.style ||
|
|
314
|
+
preset ||
|
|
315
|
+
change?.id === "footerStyle" ||
|
|
316
|
+
hasUnsupportedComponentStyle(previous, "footer") !==
|
|
317
|
+
hasUnsupportedComponentStyle(currentConfig, "footer")
|
|
318
|
+
? { style: footer.style }
|
|
319
|
+
: {}),
|
|
320
|
+
...(oldFooter.modelLabel !== footer.modelLabel
|
|
321
|
+
? { modelLabel: footer.modelLabel }
|
|
322
|
+
: {}),
|
|
323
|
+
},
|
|
324
|
+
ctx,
|
|
325
|
+
oldFooter.style,
|
|
326
|
+
);
|
|
327
|
+
const workingLineUpdated = Boolean(
|
|
328
|
+
preset ||
|
|
329
|
+
!isDeepStrictEqual(
|
|
330
|
+
previous.components.workingLine,
|
|
331
|
+
currentConfig.components.workingLine,
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
const workingLineChange = workingLineUpdated
|
|
335
|
+
? workingLineController.applyConfig(ctx)
|
|
336
|
+
: undefined;
|
|
353
337
|
selectorController.reconcile();
|
|
354
|
-
|
|
355
|
-
|
|
338
|
+
options.onConfigChanged?.(previousRenderer, ctx);
|
|
339
|
+
refresh();
|
|
340
|
+
const failures = [
|
|
341
|
+
editorChange,
|
|
342
|
+
footerChange,
|
|
343
|
+
...(workingLineChange ? [workingLineChange] : []),
|
|
344
|
+
]
|
|
345
|
+
.filter((result) => !result.applied)
|
|
346
|
+
.map(
|
|
347
|
+
(result) =>
|
|
348
|
+
new Error(
|
|
349
|
+
result.reason ?? "Layout could not apply the configuration",
|
|
350
|
+
),
|
|
351
|
+
);
|
|
352
|
+
if (failures.length > 0) {
|
|
353
|
+
throw new AggregateError(
|
|
354
|
+
failures,
|
|
355
|
+
"Layout configuration could not be applied",
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
const subscribeConfig = (): void => {
|
|
360
|
+
unsubscribeConfig?.();
|
|
361
|
+
unsubscribeConfig = settings.subscribe((snapshot, change) => {
|
|
362
|
+
const previous = currentConfig;
|
|
363
|
+
const previousRenderer = { ...config };
|
|
364
|
+
currentConfig = snapshot.shell;
|
|
365
|
+
setConfig(snapshot.renderer);
|
|
366
|
+
if (activeTuiContext) {
|
|
367
|
+
applyConfig(previous, previousRenderer, activeTuiContext, change);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
356
370
|
};
|
|
357
371
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (!
|
|
372
|
+
const refreshInteractiveState = (
|
|
373
|
+
ctx: ExtensionContext,
|
|
374
|
+
project = false,
|
|
375
|
+
): void => {
|
|
376
|
+
if (!sessionLifecycle.isCurrent() || !ctx.hasUI) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
editorController.reconcileOwnership(ctx);
|
|
380
|
+
syncFooterState(ctx);
|
|
381
|
+
if (project) {
|
|
382
|
+
scheduleProjectRefresh(ctx);
|
|
383
|
+
}
|
|
384
|
+
refresh();
|
|
385
|
+
};
|
|
386
|
+
const reloadConfig = (): PolishedTuiConfig => {
|
|
387
|
+
currentConfig = loadConfig();
|
|
388
|
+
return currentConfig;
|
|
389
|
+
};
|
|
390
|
+
const installUi = (ctx: ExtensionContext): void => {
|
|
391
|
+
if (!isTuiContext(ctx)) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
363
394
|
activeTuiContext = ctx;
|
|
364
395
|
activeTheme = ctx.ui.theme;
|
|
365
|
-
|
|
366
|
-
currentConfig = loadConfig();
|
|
396
|
+
reloadConfig();
|
|
367
397
|
syncFooterState(ctx);
|
|
368
398
|
projectRefreshService.stop();
|
|
369
|
-
|
|
370
399
|
footerController.install(ctx);
|
|
371
400
|
editorController.install(ctx, true);
|
|
372
401
|
selectorController.startSession(ctx);
|
|
373
|
-
|
|
402
|
+
projectRefreshService.reconcile(ctx);
|
|
403
|
+
footerController.reconcileSessionTimer();
|
|
374
404
|
refresh();
|
|
375
405
|
};
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
406
|
+
const dispose = (ctx = activeTuiContext): void => {
|
|
407
|
+
if (!sessionLifecycle.isCurrent()) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
sessionLifecycle.shutdown();
|
|
411
|
+
activeTuiContext = undefined;
|
|
412
|
+
unsubscribeConfig?.();
|
|
413
|
+
unsubscribeConfig = undefined;
|
|
414
|
+
liveContext.shutdown();
|
|
415
|
+
if (ctx) {
|
|
416
|
+
workingLineController.dispose(ctx);
|
|
417
|
+
}
|
|
382
418
|
editorController.cleanup(ctx);
|
|
383
419
|
footerController.cleanup(ctx);
|
|
384
420
|
projectRefreshService.stop();
|
|
385
421
|
selectorController.cleanup();
|
|
386
422
|
activeTheme = undefined;
|
|
387
|
-
|
|
423
|
+
render.dispose();
|
|
388
424
|
};
|
|
389
|
-
|
|
390
|
-
const syncInteractiveState = (_event: unknown, ctx: ExtensionContext) => {
|
|
425
|
+
const syncInteractiveState = (_event: unknown, ctx: ExtensionContext) =>
|
|
391
426
|
refreshInteractiveState(ctx);
|
|
392
|
-
};
|
|
393
427
|
const syncInteractiveAndProjectState = (
|
|
394
428
|
_event: unknown,
|
|
395
429
|
ctx: ExtensionContext,
|
|
396
|
-
) =>
|
|
430
|
+
) => refreshInteractiveState(ctx, true);
|
|
431
|
+
const syncWithUsage = (_event: unknown, ctx: ExtensionContext) => {
|
|
432
|
+
sessionState.invalidateUsageCache();
|
|
397
433
|
refreshInteractiveState(ctx, true);
|
|
398
434
|
};
|
|
399
435
|
|
|
400
|
-
/**
|
|
401
|
-
* Registers session and runtime effects with the supplied event coordinator.
|
|
402
|
-
*
|
|
403
|
-
* @param coordinator Shared lifecycle event coordinator.
|
|
404
|
-
*/
|
|
405
436
|
const installEventHandlers = (coordinator: EventCoordinator): void => {
|
|
406
437
|
coordinator.on("session_start", (_event, ctx) => {
|
|
438
|
+
dispose();
|
|
407
439
|
sessionLifecycle.start();
|
|
440
|
+
render.reset();
|
|
441
|
+
subscribeConfig();
|
|
408
442
|
liveContext.startSession();
|
|
409
443
|
sessionState.startSession();
|
|
410
444
|
minimalistProjectRoot = undefined;
|
|
411
445
|
installUi(ctx);
|
|
412
446
|
workingLineController.startSession(ctx);
|
|
413
447
|
});
|
|
414
|
-
|
|
415
|
-
coordinator.on("session_shutdown", async (_event, ctx) => {
|
|
416
|
-
liveContext.shutdown();
|
|
417
|
-
workingLineController.dispose(ctx);
|
|
418
|
-
cleanupUi(ctx);
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
const syncInteractiveAndProjectStateWithUsage = (
|
|
422
|
-
_event: unknown,
|
|
423
|
-
ctx: ExtensionContext,
|
|
424
|
-
) => {
|
|
425
|
-
sessionState.invalidateUsageCache();
|
|
426
|
-
refreshInteractiveState(ctx, true);
|
|
427
|
-
};
|
|
428
|
-
|
|
448
|
+
coordinator.on("session_shutdown", (_event, ctx) => dispose(ctx));
|
|
429
449
|
coordinator.on("agent_start", (event, ctx) => {
|
|
430
450
|
liveContext.clear();
|
|
431
451
|
workingLineController.agentStart(ctx);
|
|
432
452
|
syncInteractiveState(event, ctx);
|
|
433
453
|
});
|
|
434
|
-
coordinator.on("turn_start", (_event, ctx) =>
|
|
435
|
-
workingLineController.turnStart(ctx)
|
|
436
|
-
|
|
454
|
+
coordinator.on("turn_start", (_event, ctx) =>
|
|
455
|
+
workingLineController.turnStart(ctx),
|
|
456
|
+
);
|
|
437
457
|
coordinator.on("agent_end", (event, ctx) => {
|
|
438
458
|
liveContext.clear();
|
|
439
459
|
workingLineController.agentEnd(ctx);
|
|
440
|
-
|
|
441
|
-
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
460
|
+
syncWithUsage(event, ctx);
|
|
442
461
|
});
|
|
443
462
|
coordinator.on("model_select", (event, ctx) => {
|
|
444
463
|
liveContext.clear();
|
|
@@ -447,68 +466,51 @@ function createLayoutRuntime(
|
|
|
447
466
|
coordinator.on("thinking_level_select", syncInteractiveState);
|
|
448
467
|
coordinator.on("session_info_changed", syncInteractiveState);
|
|
449
468
|
coordinator.on("message_update", (event, ctx) => {
|
|
450
|
-
|
|
451
|
-
message: WorkingLineMessage;
|
|
452
|
-
assistantMessageEvent?: Parameters<
|
|
453
|
-
WorkingLineLayoutController["messageUpdate"]
|
|
454
|
-
>[1];
|
|
455
|
-
};
|
|
456
|
-
liveContext.update(payload.message);
|
|
469
|
+
liveContext.update(event.message);
|
|
457
470
|
workingLineController.messageUpdate(
|
|
458
|
-
|
|
459
|
-
|
|
471
|
+
event.message,
|
|
472
|
+
event.assistantMessageEvent,
|
|
460
473
|
ctx,
|
|
461
474
|
);
|
|
462
475
|
});
|
|
463
476
|
coordinator.on("message_end", (event, ctx) => {
|
|
464
|
-
const
|
|
465
|
-
message: WorkingLineMessageEnd & { stopReason?: string };
|
|
466
|
-
};
|
|
467
|
-
const result = workingLineController.messageEnd(payload.message, ctx);
|
|
468
|
-
// Pi notifies extensions before persisting a successful message, so retain its live
|
|
469
|
-
// context until agent_end; accepted failed messages clear immediately instead of showing
|
|
470
|
-
// stale usage. Rejected and duplicate finals are not authoritative.
|
|
477
|
+
const result = workingLineController.messageEnd(event.message, ctx);
|
|
471
478
|
if (
|
|
472
479
|
result.status === "accepted" &&
|
|
473
|
-
|
|
474
|
-
(
|
|
475
|
-
|
|
480
|
+
event.message.role === "assistant" &&
|
|
481
|
+
(event.message.stopReason === "error" ||
|
|
482
|
+
event.message.stopReason === "aborted")
|
|
476
483
|
) {
|
|
477
484
|
liveContext.clear();
|
|
478
485
|
}
|
|
479
|
-
|
|
480
|
-
});
|
|
481
|
-
coordinator.on("agent_settled", (_event, ctx) => {
|
|
482
|
-
workingLineController.agentSettled(ctx);
|
|
486
|
+
syncWithUsage(event, ctx);
|
|
483
487
|
});
|
|
488
|
+
coordinator.on("agent_settled", (_event, ctx) =>
|
|
489
|
+
workingLineController.agentSettled(ctx),
|
|
490
|
+
);
|
|
484
491
|
coordinator.on("tool_execution_start", (event, ctx) => {
|
|
485
492
|
liveContext.clear();
|
|
486
|
-
|
|
487
|
-
workingLineController.toolStart(
|
|
488
|
-
payload.toolCallId,
|
|
489
|
-
payload.toolName,
|
|
490
|
-
ctx,
|
|
491
|
-
);
|
|
493
|
+
workingLineController.toolStart(event.toolCallId, event.toolName, ctx);
|
|
492
494
|
syncInteractiveState(event, ctx);
|
|
493
495
|
});
|
|
494
496
|
coordinator.on("tool_execution_end", (event, ctx) => {
|
|
495
|
-
|
|
496
|
-
workingLineController.toolEnd(payload.toolCallId, ctx);
|
|
497
|
+
workingLineController.toolEnd(event.toolCallId, ctx);
|
|
497
498
|
syncInteractiveAndProjectState(event, ctx);
|
|
498
499
|
});
|
|
499
500
|
coordinator.on("session_compact", (event, ctx) => {
|
|
500
501
|
liveContext.clear();
|
|
501
|
-
|
|
502
|
+
syncWithUsage(event, ctx);
|
|
502
503
|
});
|
|
503
504
|
coordinator.on("session_tree", (event, ctx) => {
|
|
504
505
|
liveContext.clear();
|
|
505
|
-
|
|
506
|
+
syncWithUsage(event, ctx);
|
|
506
507
|
});
|
|
507
508
|
};
|
|
508
509
|
|
|
509
510
|
return {
|
|
510
511
|
sessionLifecycle,
|
|
511
|
-
|
|
512
|
+
settings,
|
|
513
|
+
getConfig,
|
|
512
514
|
editorController,
|
|
513
515
|
footerController,
|
|
514
516
|
workingLineController,
|
|
@@ -516,5 +518,6 @@ function createLayoutRuntime(
|
|
|
516
518
|
projectRefreshService,
|
|
517
519
|
reloadConfig,
|
|
518
520
|
installEventHandlers,
|
|
521
|
+
dispose,
|
|
519
522
|
};
|
|
520
523
|
}
|