Official
tabs
The tabbed detail pane beside the chat: opened files and diffs (JavaScript UI plugin).
The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/tabs@0.2.0
Permissions in 0.2.0
Files
tabs.ts3.8 KB
// Tab identity, selection and content handling, independent of host calls.
// Legacy requests are parsed at the app-state boundary rather than cast.
import * as z from "zod";
export const openRequest = z.object({
kind: z.enum(["file", "diff", "terminal", "view"]),
workspaceId: z.string(),
path: z.string(),
nonce: z.number(),
title: z.string().optional(),
icon: z.string().optional(),
view: z.number().optional(),
diff: z.string().optional(),
});
export const closeRequest = z.object({ kind: openRequest.shape.kind, path: z.string(), nonce: z.number() });
export type TabInput = Omit<z.infer<typeof openRequest>, "nonce">;
export interface Tab extends Omit<TabInput, "diff"> {
id: string;
preview: boolean;
}
export interface Content {
text?: string;
diff?: string;
error?: string;
}
export interface TabsState {
tabs: Tab[];
active: string | null;
handled: number;
handledClose: number;
contents: Record<string, Content>;
}
export const initialState = (): TabsState => ({ tabs: [], active: null, handled: 0, handledClose: 0, contents: {} });
export const tabId = (request: TabInput): string => `${request.kind}:${request.workspaceId}:${request.path}`;
export const extension = (path: string): string => (path.split(".").pop() ?? "").toLowerCase();
export const activeTab = (state: TabsState): Tab | undefined =>
state.tabs.find((tab) => tab.id === state.active) ?? state.tabs[0];
export const fileContent = (text: string): Content => ({
text: text.length > 2_000_000 ? `${text.slice(0, 2_000_000)}\n… file truncated` : text,
});
export const fileResponse = z.object({ text: z.string() }).nullable();
export const readContent = (result: z.infer<typeof fileResponse>): Content =>
// The host returns null for a missing file; preserve the original runtime's error text.
result === null ? { error: "cannot read property 'text' of null" } : fileContent(result.text);
export function openTab(state: TabsState, request: TabInput): Tab {
const id = tabId(request);
let tab = state.tabs.find((tab) => tab.id === id);
if (!tab) {
tab = {
id,
kind: request.kind,
workspaceId: request.workspaceId,
path: request.path,
title: request.title,
icon: request.icon,
view: request.view,
preview: false,
};
state.tabs.push(tab);
}
if (request.kind === "view") Object.assign(tab, { title: request.title, icon: request.icon, view: request.view });
if (request.kind === "diff") state.contents[id] = { diff: request.diff ?? "" };
state.active = id;
return tab;
}
export function closeTab(state: TabsState, id: string): Tab | undefined {
const index = state.tabs.findIndex((tab) => tab.id === id);
state.tabs = state.tabs.filter((tab) => tab.id !== id);
delete state.contents[id];
if (state.active === id) state.active = state.tabs[Math.min(index, state.tabs.length - 1)]?.id ?? null;
return state.tabs.find((tab) => tab.id === state.active);
}
export function tabName(tab: Tab): string {
if (tab.kind === "terminal") return tab.title ?? "Terminal";
if (tab.kind === "view") return tab.title ?? "View";
return tab.path.split("/").pop() ?? "";
}
export const LANGUAGES: Readonly<Record<string, string>> = {
rs: "rust",
js: "javascript",
mjs: "javascript",
cjs: "javascript",
jsx: "javascript",
ts: "typescript",
mts: "typescript",
cts: "typescript",
tsx: "tsx",
json: "json",
jsonc: "json",
md: "markdown",
markdown: "markdown",
py: "python",
sh: "bash",
bash: "bash",
zsh: "bash",
fish: "bash",
toml: "toml",
yml: "yaml",
yaml: "yaml",
go: "go",
html: "html",
htm: "html",
css: "css",
diff: "diff",
patch: "diff",
};
const finished = z.object({ event: z.literal("run_finished") });
export const runFinished = (kind: unknown): boolean => finished.safeParse(kind).success;Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 18.8 KB | 3 permissions | Listed |
No comments yet.