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

  • Read files fs.readMediumReads files in the listed places.Open workspace files in tabsPlaces: the open workspace
  • Read chats chats.readMediumReads your transcripts and chat lists.Reload the open file when an agent finishes a turn
  • Show panels ui.slotsLowShows views in the listed parts of the window.Draw the tabbed pane beside the chatSlots: detail

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

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <318.8 KB3 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.