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.
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-08 10:01:35 +08:00
1 джерело ec171c3188
коміт 2bf412ff0d
92 files changed
+496 -50

No files matched your search

+16 -5
Переглянути файл
@@ -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();