fix: stabilize interactive extension startup
This commit is contained in:
parent
0d440bed7a
commit
861c4b6cf6
5 changed files with 128 additions and 3 deletions
|
|
@ -0,0 +1,53 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
|
||||
import { createExtensionUIContext } from "./extension-ui-controller.js";
|
||||
|
||||
test("notify_when_host_has_extension_notify_uses_dedicated_handler", () => {
|
||||
const calls: unknown[][] = [];
|
||||
const ui = createExtensionUIContext({
|
||||
showExtensionNotify(message: string, type: string) {
|
||||
calls.push([message, type]);
|
||||
},
|
||||
});
|
||||
|
||||
ui.notify("Ready", "success");
|
||||
|
||||
assert.deepEqual(calls, [["Ready", "success"]]);
|
||||
});
|
||||
|
||||
test("notify_when_extension_notify_missing_routes_errors_and_warnings_to_existing_host_methods", () => {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
const ui = createExtensionUIContext({
|
||||
showError(message: string) {
|
||||
errors.push(message);
|
||||
},
|
||||
showWarning(message: string) {
|
||||
warnings.push(message);
|
||||
},
|
||||
});
|
||||
|
||||
ui.notify("Failed", "error");
|
||||
ui.notify("Careful", "warning");
|
||||
|
||||
assert.deepEqual(errors, ["Failed"]);
|
||||
assert.deepEqual(warnings, ["Careful"]);
|
||||
});
|
||||
|
||||
test("notify_when_extension_notify_missing_routes_info_and_success_to_status", () => {
|
||||
const statuses: unknown[][] = [];
|
||||
const ui = createExtensionUIContext({
|
||||
showStatus(message: string, options?: unknown) {
|
||||
statuses.push([message, options]);
|
||||
},
|
||||
});
|
||||
|
||||
ui.notify("Started", "info");
|
||||
ui.notify("Done", "success");
|
||||
|
||||
assert.deepEqual(statuses, [
|
||||
["Started", { append: false }],
|
||||
["Done", { append: true }],
|
||||
]);
|
||||
});
|
||||
|
|
@ -9,6 +9,33 @@ import {
|
|||
theme,
|
||||
} from "../theme/theme.js";
|
||||
|
||||
type ExtensionNotifyType = "info" | "warning" | "error" | "success";
|
||||
|
||||
function notifyHost(
|
||||
host: any,
|
||||
message: string,
|
||||
type: ExtensionNotifyType = "info",
|
||||
): void {
|
||||
if (typeof host.showExtensionNotify === "function") {
|
||||
host.showExtensionNotify(message, type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "error" && typeof host.showError === "function") {
|
||||
host.showError(message);
|
||||
return;
|
||||
}
|
||||
if (type === "warning" && typeof host.showWarning === "function") {
|
||||
host.showWarning(message);
|
||||
return;
|
||||
}
|
||||
if (typeof host.showStatus === "function") {
|
||||
host.showStatus(message, { append: type === "success" });
|
||||
return;
|
||||
}
|
||||
host.ui?.requestRender?.();
|
||||
}
|
||||
|
||||
export function createExtensionUIContext(host: any): ExtensionUIContext {
|
||||
return {
|
||||
select: (title, options, opts) =>
|
||||
|
|
@ -17,7 +44,7 @@ export function createExtensionUIContext(host: any): ExtensionUIContext {
|
|||
host.showExtensionConfirm(title, message, opts),
|
||||
input: (title, placeholder, opts) =>
|
||||
host.showExtensionInput(title, placeholder, opts),
|
||||
notify: (message, type) => host.showExtensionNotify(message, type),
|
||||
notify: (message, type) => notifyHost(host, message, type),
|
||||
onTerminalInput: (handler) =>
|
||||
host.addExtensionTerminalInputListener(handler),
|
||||
setStatus: (key, text) => host.setExtensionStatus(key, text),
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import type {
|
|||
ExtensionRunner,
|
||||
ExtensionUIContext,
|
||||
ExtensionUIDialogOptions,
|
||||
ExtensionWidgetOptions,
|
||||
} from "../../core/extensions/index.js";
|
||||
import {
|
||||
FooterDataProvider,
|
||||
|
|
@ -1543,6 +1544,49 @@ export class InteractiveMode {
|
|||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render or clear an extension-owned widget.
|
||||
*
|
||||
* Purpose: let extensions expose compact live UI near the editor without
|
||||
* depending on private TUI container internals.
|
||||
*
|
||||
* Consumer: extension-ui-controller.ts for ctx.ui.setWidget().
|
||||
*/
|
||||
setExtensionWidget(
|
||||
key: string,
|
||||
content:
|
||||
| string[]
|
||||
| ((tui: TUI, theme: Theme) => Component & { dispose?(): void })
|
||||
| undefined,
|
||||
options?: ExtensionWidgetOptions,
|
||||
): void {
|
||||
const target =
|
||||
options?.placement === "belowEditor"
|
||||
? this.extensionWidgetsBelow
|
||||
: this.extensionWidgetsAbove;
|
||||
const other =
|
||||
target === this.extensionWidgetsBelow
|
||||
? this.extensionWidgetsAbove
|
||||
: this.extensionWidgetsBelow;
|
||||
target.get(key)?.dispose?.();
|
||||
other.get(key)?.dispose?.();
|
||||
target.delete(key);
|
||||
other.delete(key);
|
||||
|
||||
if (content !== undefined) {
|
||||
const widget =
|
||||
typeof content === "function"
|
||||
? content(this.ui, theme)
|
||||
: {
|
||||
render: () => content,
|
||||
invalidate: () => {},
|
||||
};
|
||||
target.set(key, widget);
|
||||
}
|
||||
|
||||
this.renderWidgets();
|
||||
}
|
||||
|
||||
private clearExtensionWidgets(): void {
|
||||
for (const widget of this.extensionWidgetsAbove.values()) {
|
||||
widget.dispose?.();
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"strict": true,
|
||||
"declaration": true,
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo/tsconfig.tsbuildinfo",
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo/tsconfig.root.tsbuildinfo",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"outDir": "dist/resources",
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"sourceMap": false
|
||||
"sourceMap": false,
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo/tsconfig.resources.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/resources/**/*.ts"],
|
||||
"exclude": [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue