Files
openidea/src/features/plugins/views/logTab.ts
T
l0ng-ai 2bf412ff0d 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.
2026-11-08 10:01:35 +08:00

62 lines
2.4 KiB
TypeScript

import { tx } from "@/platform/i18n";
import { usePlugins } from "../store";
export interface PluginLogToggleSpec {
id: string;
label: string;
args: string[];
}
export interface PluginLogEditorData {
pluginId: string;
view: string;
key?: string;
title: string;
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 =>
typeof v === "string" && v.length > 0 && v.length <= max && !/[\0\r\n]/.test(v);
const args = (v: unknown): v is string[] =>
Array.isArray(v) && v.length <= 256 && v.every((a) => typeof a === "string" && a.length <= 4096 && !a.includes("\0"));
/** Reject malformed requests instead of silently running a different command. */
export function logTabData(pluginId: string, view: string, value: object, length: number): PluginLogEditorData {
const m = value as Record<string, unknown>;
const { program, args: command = [], toggles = [], key, title = program } = m;
if (
length > 2 << 20 ||
!text(program, 256) ||
program.trim() !== program ||
!text(title, 256) ||
(key !== undefined && !text(key, 128)) ||
!args(command) ||
!Array.isArray(toggles) ||
toggles.length > 32 ||
toggles.some((t) => !t || !text(t.id, 64) || !text(t.label, 128) || !args(t.args)) ||
new Set(toggles.map((t) => t.id)).size !== toggles.length ||
command.length + toggles.reduce((n, t) => n + t.args.length, 0) > 256 ||
"cwd" in m ||
"env" in m ||
"baseArgs" in m
)
throw new Error(tx("Invalid log stream configuration"));
const plugin = usePlugins.getState().plugins.find((p) => p.id === pluginId);
if (!plugin?.enabled || !plugin.capabilities.some((c) => c.id === `process:${program}` && c.granted))
throw new Error(tx`Cannot start PTY: process:${program} capability not granted.`);
return { pluginId, view, key: key as string | undefined, title, program, args: command, toggles };
}
/** Bound both line count and text size, including output with no newlines. */
export function boundedLog(text: string): string {
let result = text.slice(-(1 << 20));
if (/^[\uDC00-\uDFFF]/.test(result)) result = result.slice(1);
const lines = result.split("\n");
if (lines.length > 5000) result = lines.slice(-5000).join("\n");
return result;
}