дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
feat(plugins): let plugin code open streaming log tabs (#2082)
views.open-log-tab(key, title, process-log) (SDK host::open_log_tab) opens the read-only log tab a web view opens with open-log-tab, from a command or event of a window, reusing the Services pane's process-log. The host checks the process grant and limits when called and remembers what each key runs in that window; the tab starts it by key (plugin_log_tab_spawn) through the web-view PTY checks: pinned program, trusted project, session cap, lifecycle and grant revocation. Remote windows report that these tabs aren't available there yet.
Цей коміт міститься в:
1 джерело
ec171c3188
коміт
2bf412ff0d
92 files changed
+496
-50
No files matched your search
@@ -9,11 +9,11 @@ import { usePlugins } from "../store";
|
||||
import { startViewUi } from "../uiPoints";
|
||||
import { registerLogActions } from "../views/logActions";
|
||||
import { PluginLogEditor } from "../views/logEditor";
|
||||
import { boundedLog, logTabData } from "../views/logTab";
|
||||
import { webPtyBackend } from "../views/ptyBackend";
|
||||
import { boundedLog, logTabData, type PluginLogEditorData } from "../views/logTab";
|
||||
import { logTabPtyBackend, webPtyBackend } from "../views/ptyBackend";
|
||||
import type { PtyBackend } from "../views/ptyBridge";
|
||||
|
||||
vi.mock("../views/ptyBackend", () => ({ webPtyBackend: vi.fn() }));
|
||||
vi.mock("../views/ptyBackend", () => ({ webPtyBackend: vi.fn(), logTabPtyBackend: vi.fn() }));
|
||||
|
||||
const plugin = { id: "probe", enabled: true, capabilities: [{ id: "process:bash", granted: true }] } as PluginInfo;
|
||||
const spec = {
|
||||
@@ -36,7 +36,7 @@ afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function setup() {
|
||||
function setup(extra: Partial<PluginLogEditorData> = {}) {
|
||||
usePlugins.setState({ plugins: [plugin] });
|
||||
let output: (text: string) => void = () => {};
|
||||
let exit: Parameters<PtyBackend["onExit"]>[0] = () => {};
|
||||
@@ -54,7 +54,8 @@ function setup() {
|
||||
},
|
||||
};
|
||||
vi.mocked(webPtyBackend).mockReturnValue(backend);
|
||||
const data = logTabData("probe", "logs", spec, 100);
|
||||
vi.mocked(logTabPtyBackend).mockReturnValue(backend);
|
||||
const data = { ...logTabData("probe", "logs", spec, 100), ...extra };
|
||||
const input = { kind: "pluginLog", key: "test-log", data };
|
||||
const rendered = render(<PluginLogEditor input={input} />);
|
||||
const content = () => rendered.container.querySelector(".cm-content")!;
|
||||
@@ -122,6 +123,16 @@ describe("plugin streaming log tabs", () => {
|
||||
expect(screen.getByRole("textbox", { name: "Find" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("starts a tab opened by plugin code by its key, with the toggles that are on", async () => {
|
||||
const f = setup({ view: "", key: "code", origin: "code" });
|
||||
expect(webPtyBackend).not.toHaveBeenCalled();
|
||||
expect(logTabPtyBackend).toHaveBeenLastCalledWith("probe", "code", []);
|
||||
await act(async () => {});
|
||||
fireEvent.click(screen.getByRole("switch", { name: "Verbose" }));
|
||||
await waitFor(() => expect(f.backend.kill).toHaveBeenCalledWith(42));
|
||||
expect(logTabPtyBackend).toHaveBeenLastCalledWith("probe", "code", ["verbose"]);
|
||||
});
|
||||
|
||||
it("kills the previous process on toggle and every lifecycle boundary", async () => {
|
||||
for (const boundary of ["unmount", "disable", "project", "window"] as const) {
|
||||
const f = setup();
|
||||
|
||||
@@ -31,6 +31,8 @@ import { confirm } from "@/workbench/messages";
|
||||
import { removeTask, upsertTask, useTasks } from "@/workbench/tasks";
|
||||
import { pluginsBackend } from "./backend";
|
||||
import { usePluginT } from "./pluginText";
|
||||
import { PLUGIN_LOG_EDITOR_KIND } from "./views/logEditor";
|
||||
import type { PluginLogEditorData } from "./views/logTab";
|
||||
import { bumpView, deliverViewMessage } from "./views/viewStore";
|
||||
import { revealWindowView } from "./views/viewWindows";
|
||||
import { runPluginCommand, usePlugins } from "./store";
|
||||
@@ -420,6 +422,22 @@ export function handleRuntimeEvent(e: PluginEvent): boolean {
|
||||
// The program was approved (`process:`) and verified by the host.
|
||||
void executeAction("terminal.runCommand", { program: e.program, args: e.args, name: e.title, cwd: e.cwd });
|
||||
return true;
|
||||
case "openLogTab":
|
||||
void maybeEditorService()?.openInput({
|
||||
kind: PLUGIN_LOG_EDITOR_KIND,
|
||||
key: `pluginLog:${JSON.stringify([e.pluginId, "", e.key])}`,
|
||||
data: {
|
||||
pluginId: e.pluginId,
|
||||
view: "",
|
||||
key: e.key,
|
||||
title: e.title,
|
||||
program: e.log.program,
|
||||
args: e.log.args,
|
||||
toggles: e.log.toggles,
|
||||
origin: "code",
|
||||
} satisfies PluginLogEditorData,
|
||||
});
|
||||
return true;
|
||||
case "openLocation":
|
||||
void maybeEditorService()?.openFile(e.path, { selection: { line: e.line, col: e.column }, focus: true });
|
||||
return true;
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { EditorInput } from "@/workbench/contributions";
|
||||
import { usePlugins } from "../store";
|
||||
import { bindLogControls } from "./logActions";
|
||||
import { boundedLog, type PluginLogEditorData } from "./logTab";
|
||||
import { webPtyBackend } from "./ptyBackend";
|
||||
import { logTabPtyBackend, webPtyBackend } from "./ptyBackend";
|
||||
import { ptyBridge } from "./ptyBridge";
|
||||
|
||||
export const PLUGIN_LOG_EDITOR_KIND = "pluginLog";
|
||||
@@ -371,7 +371,11 @@ function PluginLogEditorBody({ input, data }: { input: EditorInput; data: Plugin
|
||||
sequence = undefined;
|
||||
bridge.handle(JSON.stringify({ op: "pty-ack", sessionId: "log", sequence: consumed }));
|
||||
};
|
||||
const bridge = ptyBridge(data.pluginId, webPtyBackend(data.pluginId, data.view), (message) => {
|
||||
const backend =
|
||||
data.origin === "code"
|
||||
? logTabPtyBackend(data.pluginId, data.key ?? "", [...on])
|
||||
: webPtyBackend(data.pluginId, data.view);
|
||||
const bridge = ptyBridge(data.pluginId, backend, (message) => {
|
||||
const m = message as Record<string, unknown>;
|
||||
if (stopped) return;
|
||||
if (m.op === "pty-data") {
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface PluginLogEditorData {
|
||||
program: string;
|
||||
args: string[];
|
||||
toggles: PluginLogToggleSpec[];
|
||||
/** Opened by the plugin's code (`views.open-log-tab`): the host starts what it approved for `key`. */
|
||||
origin?: "code";
|
||||
}
|
||||
|
||||
const text = (v: unknown, max: number): v is string =>
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
import { Channel } from "@tauri-apps/api/core";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import type { TerminalInfo, WebPtyCommand } from "@/bindings";
|
||||
import { call, commands, isTauri, onIdeEvent } from "@/platform/ipc";
|
||||
import { isRemoteWindow } from "@/platform/remote";
|
||||
import { usePlugins } from "../store";
|
||||
import type { PtyBackend } from "./ptyBridge";
|
||||
|
||||
/** Both frame terminals and log tabs use the backend's exact, pinned process grant. */
|
||||
export function webPtyBackend(pluginId: string, view: string): PtyBackend {
|
||||
return pluginPtyBackend(pluginId, (command, channel) =>
|
||||
call(commands.pluginWebPtySpawn(pluginId, view, command, channel)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A log tab the plugin's code opened: the host runs what it approved for `key`,
|
||||
* with the toggles in `on`; the command the tab builds only lends its size.
|
||||
*/
|
||||
export function logTabPtyBackend(pluginId: string, key: string, on: string[]): PtyBackend {
|
||||
return pluginPtyBackend(pluginId, (command, channel) => {
|
||||
if (isRemoteWindow())
|
||||
throw new Error(tx("Log tabs opened by plugin code are not available in remote projects yet."));
|
||||
return call(commands.pluginLogTabSpawn(pluginId, key, on, command.cols, command.rows, channel));
|
||||
});
|
||||
}
|
||||
|
||||
function pluginPtyBackend(
|
||||
pluginId: string,
|
||||
start: (command: WebPtyCommand, channel: Channel<string>) => Promise<TerminalInfo>,
|
||||
): PtyBackend {
|
||||
return {
|
||||
spawn: async (command, output) => {
|
||||
const current = usePlugins.getState().plugins.find((p) => p.id === pluginId);
|
||||
@@ -17,7 +40,7 @@ export function webPtyBackend(pluginId: string, view: string): PtyBackend {
|
||||
throw new Error(tx`Cannot start PTY: process:${command.program} capability not granted.`);
|
||||
const channel = new Channel<string>();
|
||||
channel.onmessage = output;
|
||||
return call(commands.pluginWebPtySpawn(pluginId, view, command, channel));
|
||||
return start(command, channel);
|
||||
},
|
||||
write: (id, data) => call(commands.termWrite(id, data)),
|
||||
resize: (id, cols, rows) => call(commands.termResize(id, cols, rows)),
|
||||
|
||||
Reference in new issue
Заблокувати користувача