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,
|
theme,
|
||||||
} from "../theme/theme.js";
|
} 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 {
|
export function createExtensionUIContext(host: any): ExtensionUIContext {
|
||||||
return {
|
return {
|
||||||
select: (title, options, opts) =>
|
select: (title, options, opts) =>
|
||||||
|
|
@ -17,7 +44,7 @@ export function createExtensionUIContext(host: any): ExtensionUIContext {
|
||||||
host.showExtensionConfirm(title, message, opts),
|
host.showExtensionConfirm(title, message, opts),
|
||||||
input: (title, placeholder, opts) =>
|
input: (title, placeholder, opts) =>
|
||||||
host.showExtensionInput(title, placeholder, opts),
|
host.showExtensionInput(title, placeholder, opts),
|
||||||
notify: (message, type) => host.showExtensionNotify(message, type),
|
notify: (message, type) => notifyHost(host, message, type),
|
||||||
onTerminalInput: (handler) =>
|
onTerminalInput: (handler) =>
|
||||||
host.addExtensionTerminalInputListener(handler),
|
host.addExtensionTerminalInputListener(handler),
|
||||||
setStatus: (key, text) => host.setExtensionStatus(key, text),
|
setStatus: (key, text) => host.setExtensionStatus(key, text),
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ import type {
|
||||||
ExtensionRunner,
|
ExtensionRunner,
|
||||||
ExtensionUIContext,
|
ExtensionUIContext,
|
||||||
ExtensionUIDialogOptions,
|
ExtensionUIDialogOptions,
|
||||||
|
ExtensionWidgetOptions,
|
||||||
} from "../../core/extensions/index.js";
|
} from "../../core/extensions/index.js";
|
||||||
import {
|
import {
|
||||||
FooterDataProvider,
|
FooterDataProvider,
|
||||||
|
|
@ -1543,6 +1544,49 @@ export class InteractiveMode {
|
||||||
this.ui.requestRender();
|
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 {
|
private clearExtensionWidgets(): void {
|
||||||
for (const widget of this.extensionWidgetsAbove.values()) {
|
for (const widget of this.extensionWidgetsAbove.values()) {
|
||||||
widget.dispose?.();
|
widget.dispose?.();
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"tsBuildInfoFile": "dist/.tsbuildinfo/tsconfig.tsbuildinfo",
|
"tsBuildInfoFile": "dist/.tsbuildinfo/tsconfig.root.tsbuildinfo",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
"outDir": "dist/resources",
|
"outDir": "dist/resources",
|
||||||
"declaration": false,
|
"declaration": false,
|
||||||
"declarationMap": false,
|
"declarationMap": false,
|
||||||
"sourceMap": false
|
"sourceMap": false,
|
||||||
|
"tsBuildInfoFile": "dist/.tsbuildinfo/tsconfig.resources.tsbuildinfo"
|
||||||
},
|
},
|
||||||
"include": ["src/resources/**/*.ts"],
|
"include": ["src/resources/**/*.ts"],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue