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,546 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
Theme,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { showOneUiPanel, type TuiPanelRuntime } from "../panel.ts";
|
|
7
|
+
import {
|
|
8
|
+
createPiExtensionPort,
|
|
9
|
+
type PiExtensionPort,
|
|
10
|
+
} from "../host/pi-extension-port.ts";
|
|
11
|
+
import { createPiUiPort, type PiUiPort } from "../host/pi-ui-port.ts";
|
|
12
|
+
import { LayoutRegistry } from "../ownership/layout-registry.ts";
|
|
13
|
+
import registerHeaderLayout from "../../layouts/header/index.ts";
|
|
14
|
+
import { EventCoordinator } from "./event-coordinator.ts";
|
|
15
|
+
import { RenderScheduler } from "./render-scheduler.ts";
|
|
16
|
+
import { RuntimeStateStore } from "./runtime-state.ts";
|
|
17
|
+
import { EditorLayoutController } from "../../layouts/editor/controller.ts";
|
|
18
|
+
import { FooterLayoutController } from "../../layouts/footer/controller.ts";
|
|
19
|
+
import {
|
|
20
|
+
WorkingLineLayoutController,
|
|
21
|
+
type WorkingLineMessage,
|
|
22
|
+
type WorkingLineMessageEnd,
|
|
23
|
+
} from "../../layouts/working-line/controller.ts";
|
|
24
|
+
import registerContext, {
|
|
25
|
+
type ContextExtensionOptions,
|
|
26
|
+
type ContextRuntimeController,
|
|
27
|
+
} from "../../layouts/context/index.ts";
|
|
28
|
+
import type { AccentRailLayoutPatchDiagnostic } from "../../layouts/editor/accent-rail-layout-patch.ts";
|
|
29
|
+
import {
|
|
30
|
+
ensureConfigExists,
|
|
31
|
+
hasUnsupportedComponentStyle,
|
|
32
|
+
loadConfig,
|
|
33
|
+
mergeConfig,
|
|
34
|
+
saveEditorComponentPatch,
|
|
35
|
+
saveFooterComponentPatch,
|
|
36
|
+
saveWorkingLineComponentPatch,
|
|
37
|
+
type PolishedTuiConfig,
|
|
38
|
+
} from "../config/shell.ts";
|
|
39
|
+
import { configStore, type ConfigRecord } from "../config/store.ts";
|
|
40
|
+
import {
|
|
41
|
+
SessionStateService,
|
|
42
|
+
syncState,
|
|
43
|
+
} from "../../services/session-state.ts";
|
|
44
|
+
import {
|
|
45
|
+
renderTurnSummaryEntry,
|
|
46
|
+
TURN_SUMMARY_ENTRY_TYPE,
|
|
47
|
+
} from "../../layouts/working-line/interaction-summary.ts";
|
|
48
|
+
import { LiveContextController } from "../../services/live-context.ts";
|
|
49
|
+
import {
|
|
50
|
+
ProjectRefreshService,
|
|
51
|
+
type ScheduleProjectRefreshOptions,
|
|
52
|
+
} from "../../services/project-refresh.ts";
|
|
53
|
+
import { SelectorController } from "../overlay/selector-controller.ts";
|
|
54
|
+
import { SessionLifecycle } from "./session-lifecycle.ts";
|
|
55
|
+
import { resolveFooterTelemetry } from "../../services/telemetry.ts";
|
|
56
|
+
|
|
57
|
+
export type TuiRuntimeContext = {
|
|
58
|
+
readonly extensions: PiExtensionPort;
|
|
59
|
+
readonly state: RuntimeStateStore;
|
|
60
|
+
readonly render: RenderScheduler;
|
|
61
|
+
readonly layouts: LayoutRegistry;
|
|
62
|
+
readonly ui: () => PiUiPort | undefined;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The single composition root for the plugin. Remaining compatibility glue
|
|
67
|
+
* is mounted behind this seam while Layout ownership is finalized.
|
|
68
|
+
*/
|
|
69
|
+
export class TuiRuntime {
|
|
70
|
+
private readonly pi: ExtensionAPI;
|
|
71
|
+
readonly extensions: PiExtensionPort;
|
|
72
|
+
readonly state = new RuntimeStateStore();
|
|
73
|
+
readonly layouts = new LayoutRegistry();
|
|
74
|
+
|
|
75
|
+
private readonly coordinator: EventCoordinator;
|
|
76
|
+
private readonly renderScheduler: RenderScheduler;
|
|
77
|
+
private activeUi: PiUiPort | undefined;
|
|
78
|
+
private installed = false;
|
|
79
|
+
private editorController: EditorLayoutController | undefined;
|
|
80
|
+
private footerController: FooterLayoutController | undefined;
|
|
81
|
+
private workingLineController: WorkingLineLayoutController | undefined;
|
|
82
|
+
private contextController: ContextRuntimeController | undefined;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Creates the runtime services and coordinates session lifecycle ownership.
|
|
86
|
+
*/
|
|
87
|
+
constructor(pi: ExtensionAPI) {
|
|
88
|
+
this.pi = pi;
|
|
89
|
+
this.extensions = createPiExtensionPort(pi);
|
|
90
|
+
this.renderScheduler = new RenderScheduler((force) =>
|
|
91
|
+
this.activeUi?.requestRender(force),
|
|
92
|
+
);
|
|
93
|
+
this.coordinator = new EventCoordinator({
|
|
94
|
+
on: (event, handler) =>
|
|
95
|
+
this.extensions.on(event as never, handler as never),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Returns the shared render scheduler used by installed layouts.
|
|
101
|
+
*/
|
|
102
|
+
get render(): RenderScheduler {
|
|
103
|
+
return this.renderScheduler;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns the active session UI port, when a TUI session is mounted.
|
|
108
|
+
*/
|
|
109
|
+
ui(): PiUiPort | undefined {
|
|
110
|
+
return this.activeUi;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Exposes the narrow runtime contract consumed by the settings panel.
|
|
115
|
+
*
|
|
116
|
+
* @returns Layout-independent panel operations.
|
|
117
|
+
*/
|
|
118
|
+
private panelRuntime(): TuiPanelRuntime {
|
|
119
|
+
return {
|
|
120
|
+
setEditorComponent: (patch, ctx) =>
|
|
121
|
+
this.editorController?.setComponent(patch, ctx) ?? { applied: false },
|
|
122
|
+
setUserMessagesComponent: (patch, ctx) =>
|
|
123
|
+
this.contextController?.setUserMessagesComponent(patch, ctx),
|
|
124
|
+
setWorkingLineComponent: (patch, ctx) =>
|
|
125
|
+
this.workingLineController?.setComponent(patch, ctx) ?? {
|
|
126
|
+
applied: false,
|
|
127
|
+
reason: "WorkingLine runtime is not available",
|
|
128
|
+
},
|
|
129
|
+
setFooterComponent: (patch, ctx) =>
|
|
130
|
+
this.footerController?.setComponent(patch, ctx),
|
|
131
|
+
setContextMode: (mode, ctx) => this.contextController?.setMode(mode, ctx),
|
|
132
|
+
updateContextConfig: (patch, ctx) =>
|
|
133
|
+
this.contextController?.updateConfig(patch, ctx),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Installs the runtime once and mounts remaining layout lifecycle glue.
|
|
139
|
+
*/
|
|
140
|
+
install(): void {
|
|
141
|
+
if (this.installed) return;
|
|
142
|
+
this.installed = true;
|
|
143
|
+
|
|
144
|
+
const contextOptions: ContextExtensionOptions = {
|
|
145
|
+
onRuntimeController: (controller) => {
|
|
146
|
+
this.contextController = controller;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const layoutBindings = createLayoutRuntime(this.pi, this.coordinator);
|
|
151
|
+
layoutBindings.workingLineController.setSummaryWriterEnabled(false);
|
|
152
|
+
this.editorController = layoutBindings.editorController;
|
|
153
|
+
this.footerController = layoutBindings.footerController;
|
|
154
|
+
this.workingLineController = layoutBindings.workingLineController;
|
|
155
|
+
layoutBindings.installEventHandlers(this.coordinator);
|
|
156
|
+
registerHeaderLayout(this.pi);
|
|
157
|
+
registerContext(this.pi, contextOptions);
|
|
158
|
+
this.coordinator.on("session_start", async (_event, ctx) => {
|
|
159
|
+
this.state.start(ctx.mode);
|
|
160
|
+
if (ctx.mode === "tui" && ctx.hasUI) {
|
|
161
|
+
// Pi exposes redraw through concrete TUI components; layouts will
|
|
162
|
+
// connect that callback when they migrate behind this runtime.
|
|
163
|
+
this.activeUi = createPiUiPort(ctx);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
this.coordinator.on("session_shutdown", (_event, ctx) => {
|
|
167
|
+
this.activeUi = undefined;
|
|
168
|
+
this.state.shutdown();
|
|
169
|
+
});
|
|
170
|
+
this.coordinator.install();
|
|
171
|
+
|
|
172
|
+
this.extensions.registerCommand("oneui", {
|
|
173
|
+
description: "Open pi-one-ui settings",
|
|
174
|
+
handler: async (_args, ctx) => {
|
|
175
|
+
await showOneUiPanel(ctx, { runtime: this.panelRuntime() });
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Creates a single TUI runtime for the plugin composition root.
|
|
183
|
+
*/
|
|
184
|
+
export function createTuiRuntime(pi: ExtensionAPI): TuiRuntime {
|
|
185
|
+
return new TuiRuntime(pi);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Reports whether a context exposes the interactive TUI layout.
|
|
190
|
+
*
|
|
191
|
+
* @param ctx Candidate Pi extension context.
|
|
192
|
+
* @returns Whether the context can install TUI-owned layouts.
|
|
193
|
+
*/
|
|
194
|
+
function isTuiContext(ctx: ExtensionContext): boolean {
|
|
195
|
+
try {
|
|
196
|
+
const mode = ctx.mode;
|
|
197
|
+
return ctx.hasUI && (mode === undefined || mode === "tui");
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Creates the layout controllers and shared services used by TuiRuntime.
|
|
205
|
+
*
|
|
206
|
+
* @param pi Pi extension API.
|
|
207
|
+
* @param eventCoordinator Shared lifecycle event coordinator.
|
|
208
|
+
* @returns Layout controllers and lifecycle registration bindings.
|
|
209
|
+
*/
|
|
210
|
+
function createLayoutRuntime(
|
|
211
|
+
pi: ExtensionAPI,
|
|
212
|
+
eventCoordinator: EventCoordinator,
|
|
213
|
+
) {
|
|
214
|
+
const sessionLifecycle = new SessionLifecycle();
|
|
215
|
+
let currentConfig: PolishedTuiConfig = loadConfig();
|
|
216
|
+
configStore.subscribe((record: ConfigRecord) => {
|
|
217
|
+
currentConfig = mergeConfig(record);
|
|
218
|
+
});
|
|
219
|
+
const sessionState = new SessionStateService({
|
|
220
|
+
getCacheHitIcon: () => currentConfig.icons.cacheHit,
|
|
221
|
+
resolveTelemetry: resolveFooterTelemetry,
|
|
222
|
+
syncState,
|
|
223
|
+
});
|
|
224
|
+
const state = sessionState.state;
|
|
225
|
+
// Keep the capability guard defensive for hosts with incomplete extension APIs.
|
|
226
|
+
if (typeof pi.registerEntryRenderer === "function") {
|
|
227
|
+
pi.registerEntryRenderer(TURN_SUMMARY_ENTRY_TYPE, (entry, options, theme) =>
|
|
228
|
+
renderTurnSummaryEntry(
|
|
229
|
+
entry,
|
|
230
|
+
{
|
|
231
|
+
...options,
|
|
232
|
+
colorSource: currentConfig.components.workingLine.colorSource,
|
|
233
|
+
workingLineHigh: currentConfig.colors.workingLineHigh,
|
|
234
|
+
},
|
|
235
|
+
theme,
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
let activeTheme: Theme | undefined;
|
|
240
|
+
let minimalistProjectRoot: string | undefined;
|
|
241
|
+
let activeTuiContext: ExtensionContext | undefined;
|
|
242
|
+
|
|
243
|
+
const recordAccentRailLayoutPatchDiagnostic = (
|
|
244
|
+
diagnostic: AccentRailLayoutPatchDiagnostic,
|
|
245
|
+
version?: string,
|
|
246
|
+
) => {
|
|
247
|
+
if (process.env.ZENTUI_DEBUG === "1") {
|
|
248
|
+
console.error(
|
|
249
|
+
`[zentui] Accent Rail fullscreen layout patch: ${diagnostic}${version ? ` (Pi TUI ${version})` : ""}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Requests redraws from the Editor controller and shared render seam.
|
|
256
|
+
*/
|
|
257
|
+
const refresh = () => {
|
|
258
|
+
if (!sessionLifecycle.isCurrent()) return;
|
|
259
|
+
editorController.requestRender();
|
|
260
|
+
};
|
|
261
|
+
const liveContext = new LiveContextController(sessionLifecycle, refresh);
|
|
262
|
+
const getActiveTheme = () => activeTheme;
|
|
263
|
+
const getCurrentConfig = () => currentConfig;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Reloads the canonical configuration after an external test-support write.
|
|
267
|
+
*
|
|
268
|
+
* @returns The refreshed normalized configuration snapshot.
|
|
269
|
+
*/
|
|
270
|
+
const reloadConfig = (): PolishedTuiConfig => {
|
|
271
|
+
currentConfig = loadConfig();
|
|
272
|
+
return currentConfig;
|
|
273
|
+
};
|
|
274
|
+
const workingLineController = new WorkingLineLayoutController({
|
|
275
|
+
pi,
|
|
276
|
+
getConfig: getCurrentConfig,
|
|
277
|
+
saveComponent: (patch) => {
|
|
278
|
+
currentConfig = saveWorkingLineComponentPatch(patch);
|
|
279
|
+
return currentConfig;
|
|
280
|
+
},
|
|
281
|
+
getTheme: () => activeTheme as Theme,
|
|
282
|
+
sessionLifecycle,
|
|
283
|
+
refresh,
|
|
284
|
+
onAgentActiveChanged: (active) => {
|
|
285
|
+
editorController.setAgentRunActive(active);
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
const getThinkingLevel = () =>
|
|
289
|
+
sessionLifecycle.isCurrent() ? pi.getThinkingLevel() : ("off" as const);
|
|
290
|
+
const editorController = new EditorLayoutController({
|
|
291
|
+
getConfig: getCurrentConfig,
|
|
292
|
+
saveComponent: (patch) => {
|
|
293
|
+
currentConfig = saveEditorComponentPatch(patch);
|
|
294
|
+
return currentConfig;
|
|
295
|
+
},
|
|
296
|
+
getState: () => state,
|
|
297
|
+
sessionLifecycle,
|
|
298
|
+
render: { request: () => footerController.requestRender() },
|
|
299
|
+
getThinkingLevel,
|
|
300
|
+
getContextWindow: (ctx) =>
|
|
301
|
+
ctx.model?.contextWindow ?? ctx.getContextUsage()?.contextWindow,
|
|
302
|
+
getContextPercent: (ctx) => {
|
|
303
|
+
const usage = ctx.getContextUsage();
|
|
304
|
+
const contextWindow = ctx.model?.contextWindow ?? usage?.contextWindow;
|
|
305
|
+
const live = liveContext.get();
|
|
306
|
+
return live && contextWindow && contextWindow > 0
|
|
307
|
+
? (live.tokens / contextWindow) * 100
|
|
308
|
+
: (usage?.percent ?? undefined);
|
|
309
|
+
},
|
|
310
|
+
getAgentDurationMs: () => workingLineController.duration().elapsedMs() ?? 0,
|
|
311
|
+
isAgentActive: () => workingLineController.isAgentActive(),
|
|
312
|
+
isAgentDurationActive: () => workingLineController.duration().isActive(),
|
|
313
|
+
subscribeAgentDuration: (listener) =>
|
|
314
|
+
workingLineController.duration().subscribe(() => listener()),
|
|
315
|
+
getProjectRoot: () => minimalistProjectRoot,
|
|
316
|
+
onProjectRequirementChanged: () => {
|
|
317
|
+
if (activeTuiContext) projectRefreshService.reconcile(activeTuiContext);
|
|
318
|
+
},
|
|
319
|
+
onModelLabelChanged: (ctx) => syncFooterState(ctx),
|
|
320
|
+
recordLayoutDiagnostic: recordAccentRailLayoutPatchDiagnostic,
|
|
321
|
+
});
|
|
322
|
+
const syncFooterState = (ctx: ExtensionContext) => sessionState.sync(ctx);
|
|
323
|
+
const projectRefreshService = new ProjectRefreshService({
|
|
324
|
+
getConfig: getCurrentConfig,
|
|
325
|
+
state,
|
|
326
|
+
sessionLifecycle,
|
|
327
|
+
getFooterReferences: () => footerController.installedFooterReferences(),
|
|
328
|
+
needsRefresh: () =>
|
|
329
|
+
footerController.needsProjectRefresh() ||
|
|
330
|
+
editorController.needsProjectRefresh(),
|
|
331
|
+
reconcileOwnership: (ctx) => editorController.reconcileOwnership(ctx),
|
|
332
|
+
onProjectRoot: (root) => {
|
|
333
|
+
minimalistProjectRoot = root;
|
|
334
|
+
},
|
|
335
|
+
refresh,
|
|
336
|
+
});
|
|
337
|
+
const scheduleProjectRefresh = (
|
|
338
|
+
ctx: ExtensionContext,
|
|
339
|
+
options?: ScheduleProjectRefreshOptions,
|
|
340
|
+
) => projectRefreshService.schedule(ctx, options);
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Synchronizes shared session state and requests a visible redraw.
|
|
344
|
+
*
|
|
345
|
+
* @param ctx Active Pi extension context.
|
|
346
|
+
* @param project Whether project data should be scheduled.
|
|
347
|
+
*/
|
|
348
|
+
const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
|
|
349
|
+
if (!sessionLifecycle.isCurrent() || !ctx.hasUI) return;
|
|
350
|
+
editorController.reconcileOwnership(ctx);
|
|
351
|
+
syncFooterState(ctx);
|
|
352
|
+
if (project) scheduleProjectRefresh(ctx);
|
|
353
|
+
refresh();
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const selectorController = new SelectorController({
|
|
357
|
+
getConfig: getCurrentConfig,
|
|
358
|
+
});
|
|
359
|
+
const footerController = new FooterLayoutController({
|
|
360
|
+
getConfig: getCurrentConfig,
|
|
361
|
+
saveComponent: (patch) => {
|
|
362
|
+
currentConfig = saveFooterComponentPatch(patch);
|
|
363
|
+
return currentConfig;
|
|
364
|
+
},
|
|
365
|
+
state,
|
|
366
|
+
sessionLifecycle,
|
|
367
|
+
refresh,
|
|
368
|
+
scheduleProjectRefresh: (ctx) => scheduleProjectRefresh(ctx),
|
|
369
|
+
getLiveContext: () => liveContext.get(),
|
|
370
|
+
onProjectRequirementChanged: (ctx, force) =>
|
|
371
|
+
projectRefreshService.reconcile(ctx, force),
|
|
372
|
+
onModelLabelChanged: (ctx) => syncFooterState(ctx),
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const applyConfiguredUi = (ctx: ExtensionContext) => {
|
|
376
|
+
if (!isTuiContext(ctx)) return;
|
|
377
|
+
selectorController.reconcile();
|
|
378
|
+
projectRefreshService.reconcile(ctx);
|
|
379
|
+
footerController.reconcileSessionTimer();
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Installs compatibility layouts around the extracted Editor controller.
|
|
384
|
+
*/
|
|
385
|
+
const installUi = (ctx: ExtensionContext) => {
|
|
386
|
+
if (!isTuiContext(ctx)) return;
|
|
387
|
+
activeTuiContext = ctx;
|
|
388
|
+
activeTheme = ctx.ui.theme;
|
|
389
|
+
ensureConfigExists();
|
|
390
|
+
currentConfig = loadConfig();
|
|
391
|
+
syncFooterState(ctx);
|
|
392
|
+
projectRefreshService.stop();
|
|
393
|
+
|
|
394
|
+
footerController.install(ctx);
|
|
395
|
+
editorController.install(ctx, true);
|
|
396
|
+
selectorController.startSession(ctx);
|
|
397
|
+
applyConfiguredUi(ctx);
|
|
398
|
+
refresh();
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Cleans all session-owned layout resources in dependency order.
|
|
403
|
+
*/
|
|
404
|
+
const cleanupUi = (ctx?: ExtensionContext) => {
|
|
405
|
+
if (!ctx || !sessionLifecycle.isCurrent()) return;
|
|
406
|
+
editorController.cleanup(ctx);
|
|
407
|
+
footerController.cleanup(ctx);
|
|
408
|
+
projectRefreshService.stop();
|
|
409
|
+
selectorController.cleanup();
|
|
410
|
+
activeTheme = undefined;
|
|
411
|
+
activeTuiContext = undefined;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const syncInteractiveState = (_event: unknown, ctx: ExtensionContext) => {
|
|
415
|
+
refreshInteractiveState(ctx);
|
|
416
|
+
};
|
|
417
|
+
const syncInteractiveAndProjectState = (
|
|
418
|
+
_event: unknown,
|
|
419
|
+
ctx: ExtensionContext,
|
|
420
|
+
) => {
|
|
421
|
+
refreshInteractiveState(ctx, true);
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Registers session and runtime effects with the supplied event coordinator.
|
|
426
|
+
*
|
|
427
|
+
* @param coordinator Shared lifecycle event coordinator.
|
|
428
|
+
*/
|
|
429
|
+
const installEventHandlers = (coordinator: EventCoordinator): void => {
|
|
430
|
+
coordinator.on("session_start", async (_event, ctx) => {
|
|
431
|
+
sessionLifecycle.start();
|
|
432
|
+
await editorController.startSession(ctx);
|
|
433
|
+
if (!sessionLifecycle.isCurrent()) return;
|
|
434
|
+
liveContext.startSession();
|
|
435
|
+
sessionState.startSession();
|
|
436
|
+
minimalistProjectRoot = undefined;
|
|
437
|
+
installUi(ctx);
|
|
438
|
+
workingLineController.startSession(ctx);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
coordinator.on("session_shutdown", async (_event, ctx) => {
|
|
442
|
+
liveContext.shutdown();
|
|
443
|
+
workingLineController.dispose(ctx);
|
|
444
|
+
cleanupUi(ctx);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const syncInteractiveAndProjectStateWithUsage = (
|
|
448
|
+
_event: unknown,
|
|
449
|
+
ctx: ExtensionContext,
|
|
450
|
+
) => {
|
|
451
|
+
sessionState.invalidateUsageCache();
|
|
452
|
+
refreshInteractiveState(ctx, true);
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
coordinator.on("agent_start", (event, ctx) => {
|
|
456
|
+
liveContext.clear();
|
|
457
|
+
workingLineController.agentStart(ctx);
|
|
458
|
+
syncInteractiveState(event, ctx);
|
|
459
|
+
});
|
|
460
|
+
coordinator.on("turn_start", (_event, ctx) => {
|
|
461
|
+
workingLineController.turnStart(ctx);
|
|
462
|
+
});
|
|
463
|
+
coordinator.on("agent_end", (event, ctx) => {
|
|
464
|
+
liveContext.clear();
|
|
465
|
+
workingLineController.agentEnd(ctx);
|
|
466
|
+
// Reconcile once more after Pi has persisted the assistant message.
|
|
467
|
+
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
468
|
+
});
|
|
469
|
+
coordinator.on("model_select", (event, ctx) => {
|
|
470
|
+
liveContext.clear();
|
|
471
|
+
syncInteractiveState(event, ctx);
|
|
472
|
+
});
|
|
473
|
+
coordinator.on("thinking_level_select", syncInteractiveState);
|
|
474
|
+
coordinator.on("session_info_changed", syncInteractiveState);
|
|
475
|
+
coordinator.on("message_update", (event, ctx) => {
|
|
476
|
+
const payload = event as {
|
|
477
|
+
message: WorkingLineMessage;
|
|
478
|
+
assistantMessageEvent?: Parameters<
|
|
479
|
+
WorkingLineLayoutController["messageUpdate"]
|
|
480
|
+
>[1];
|
|
481
|
+
};
|
|
482
|
+
liveContext.update(payload.message);
|
|
483
|
+
workingLineController.messageUpdate(
|
|
484
|
+
payload.message,
|
|
485
|
+
payload.assistantMessageEvent,
|
|
486
|
+
ctx,
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
coordinator.on("message_end", (event, ctx) => {
|
|
490
|
+
const payload = event as {
|
|
491
|
+
message: WorkingLineMessageEnd & { stopReason?: string };
|
|
492
|
+
};
|
|
493
|
+
const result = workingLineController.messageEnd(payload.message, ctx);
|
|
494
|
+
// Pi notifies extensions before persisting a successful message, so retain its live
|
|
495
|
+
// context until agent_end; accepted failed messages clear immediately instead of showing
|
|
496
|
+
// stale usage. Rejected and duplicate finals are not authoritative.
|
|
497
|
+
if (
|
|
498
|
+
result.status === "accepted" &&
|
|
499
|
+
payload.message.role === "assistant" &&
|
|
500
|
+
(payload.message.stopReason === "error" ||
|
|
501
|
+
payload.message.stopReason === "aborted")
|
|
502
|
+
) {
|
|
503
|
+
liveContext.clear();
|
|
504
|
+
}
|
|
505
|
+
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
506
|
+
});
|
|
507
|
+
coordinator.on("agent_settled", (_event, ctx) => {
|
|
508
|
+
workingLineController.agentSettled(ctx);
|
|
509
|
+
});
|
|
510
|
+
coordinator.on("tool_execution_start", (event, ctx) => {
|
|
511
|
+
liveContext.clear();
|
|
512
|
+
const payload = event as { toolCallId: string; toolName: string };
|
|
513
|
+
workingLineController.toolStart(
|
|
514
|
+
payload.toolCallId,
|
|
515
|
+
payload.toolName,
|
|
516
|
+
ctx,
|
|
517
|
+
);
|
|
518
|
+
syncInteractiveState(event, ctx);
|
|
519
|
+
});
|
|
520
|
+
coordinator.on("tool_execution_end", (event, ctx) => {
|
|
521
|
+
const payload = event as { toolCallId: string };
|
|
522
|
+
workingLineController.toolEnd(payload.toolCallId, ctx);
|
|
523
|
+
syncInteractiveAndProjectState(event, ctx);
|
|
524
|
+
});
|
|
525
|
+
coordinator.on("session_compact", (event, ctx) => {
|
|
526
|
+
liveContext.clear();
|
|
527
|
+
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
528
|
+
});
|
|
529
|
+
coordinator.on("session_tree", (event, ctx) => {
|
|
530
|
+
liveContext.clear();
|
|
531
|
+
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
532
|
+
});
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
sessionLifecycle,
|
|
537
|
+
getConfig: getCurrentConfig,
|
|
538
|
+
editorController,
|
|
539
|
+
footerController,
|
|
540
|
+
workingLineController,
|
|
541
|
+
selectorController,
|
|
542
|
+
projectRefreshService,
|
|
543
|
+
reloadConfig,
|
|
544
|
+
installEventHandlers,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
@@ -13,8 +13,9 @@ import {
|
|
|
13
13
|
matchesKey,
|
|
14
14
|
visibleWidth,
|
|
15
15
|
} from "@earendil-works/pi-tui";
|
|
16
|
-
import { mouseBaseButton, parseSgrMousePacket } from "
|
|
17
|
-
import { padLine } from "
|
|
16
|
+
import { mouseBaseButton, parseSgrMousePacket } from "../../tools/sgr-mouse.ts";
|
|
17
|
+
import { padLine } from "../../tools/format.ts";
|
|
18
|
+
import { overlayManager } from "../../app/overlay/overlay-manager.ts";
|
|
18
19
|
|
|
19
20
|
export type ContextPart = {
|
|
20
21
|
label: string;
|
|
@@ -53,7 +54,9 @@ function normalizePreviewText(text: string): string {
|
|
|
53
54
|
|
|
54
55
|
export type DialogBounds = { left: number; top: number; width: number };
|
|
55
56
|
|
|
56
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Returns the 1-based terminal hitbox of the dialog title's [esc] button.
|
|
59
|
+
*/
|
|
57
60
|
export function escCloseHitbox(bounds: DialogBounds): {
|
|
58
61
|
row: number;
|
|
59
62
|
startCol: number;
|
|
@@ -66,28 +69,38 @@ export function escCloseHitbox(bounds: DialogBounds): {
|
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Reports whether a plugin-owned overlay is currently active.
|
|
74
|
+
*
|
|
75
|
+
* The legacy name is retained because the mouse implementation imports it.
|
|
76
|
+
*/
|
|
72
77
|
export function hasActiveTextPreview(): boolean {
|
|
73
|
-
return
|
|
78
|
+
return overlayManager.hasActive();
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Re-enables fullscreen mouse motion after Pi opens an overlay.
|
|
83
|
+
*/
|
|
77
84
|
function ensureFullscreenMouseMotion(tui: any): void {
|
|
78
85
|
if (tui.mode === "fullscreen")
|
|
79
86
|
tui.terminal?.write?.("\x1b[?1003h\x1b[?1006h");
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Opens a scrollable overlay for one context preview document.
|
|
91
|
+
*
|
|
92
|
+
* @param ctx Extension command context that provides the custom UI host.
|
|
93
|
+
* @param title Preview title shown in the overlay header.
|
|
94
|
+
* @param rawContent Markdown source displayed in the preview.
|
|
95
|
+
*/
|
|
82
96
|
export async function showTextPreview(
|
|
83
97
|
ctx: Pick<ExtensionCommandContext, "ui">,
|
|
84
98
|
title: string,
|
|
85
99
|
rawContent: string,
|
|
86
100
|
): Promise<void> {
|
|
87
101
|
const content = normalizePreviewText(rawContent);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
await ctx.ui.custom(
|
|
102
|
+
await overlayManager.run(() =>
|
|
103
|
+
ctx.ui.custom(
|
|
91
104
|
(tui, theme, _keybindings, done) => {
|
|
92
105
|
ensureFullscreenMouseMotion(tui);
|
|
93
106
|
let scrollOffset = 0;
|
|
@@ -311,10 +324,8 @@ export async function showTextPreview(
|
|
|
311
324
|
margin: 2,
|
|
312
325
|
},
|
|
313
326
|
},
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
activeContextOverlays--;
|
|
317
|
-
}
|
|
327
|
+
),
|
|
328
|
+
);
|
|
318
329
|
}
|
|
319
330
|
|
|
320
331
|
const tokenEstimate = (value: unknown): number => {
|
|
@@ -621,11 +632,9 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
621
632
|
let selectedPreviewIndex = 0;
|
|
622
633
|
|
|
623
634
|
while (true) {
|
|
624
|
-
// 主弹框也计入活动 overlay,fullscreen 下官方输入链才会把鼠标包放行给行点击。
|
|
625
|
-
activeContextOverlays++;
|
|
626
635
|
let action;
|
|
627
|
-
|
|
628
|
-
|
|
636
|
+
action = await overlayManager.run(() =>
|
|
637
|
+
ctx.ui.custom(
|
|
629
638
|
(tui, theme, _keybindings, done) => {
|
|
630
639
|
ensureFullscreenMouseMotion(tui);
|
|
631
640
|
let previewHitboxes: Array<{
|
|
@@ -841,10 +850,8 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
841
850
|
margin: 1,
|
|
842
851
|
},
|
|
843
852
|
},
|
|
844
|
-
)
|
|
845
|
-
|
|
846
|
-
activeContextOverlays--;
|
|
847
|
-
}
|
|
853
|
+
),
|
|
854
|
+
);
|
|
848
855
|
|
|
849
856
|
if (!action) break;
|
|
850
857
|
const preview = previewByKey.get(action as PreviewKey);
|
|
@@ -4,10 +4,7 @@
|
|
|
4
4
|
* 在 handleBashCommand 收尾补一次 flush。
|
|
5
5
|
*/
|
|
6
6
|
import { InteractiveMode } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import {
|
|
8
|
-
FLUSH_DOCKED_BASH_PATCH,
|
|
9
|
-
patchRegistry,
|
|
10
|
-
} from "../../tools/patch-keys.ts";
|
|
7
|
+
import { FLUSH_DOCKED_BASH_PATCH, patchRegistry } from "../tools/patch-keys.ts";
|
|
11
8
|
|
|
12
9
|
type Patch = {
|
|
13
10
|
active: boolean;
|