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.test.ts4.8 KB
// Tab identity and content regressions, using the public pure functions.
import assert from "node:assert/strict";
import { test } from "node:test";
import {
  activeTab,
  closeTab,
  extension,
  fileContent,
  fileResponse,
  initialState,
  LANGUAGES,
  openRequest,
  openTab,
  runFinished,
  readContent,
  tabId,
  tabName,
} from "./tabs.ts";
import type { TabInput } from "./tabs.ts";
const file: TabInput = { kind: "file", workspaceId: "w1", path: "src/README.md" };

test("tab ids distinguish kind, workspace and path", () => {
  assert.equal(tabId(file), "file:w1:src/README.md");
  assert.equal(
    new Set(
      [file, { ...file, kind: "diff" }, { ...file, workspaceId: "w2" }, { ...file, path: "README.md" }].map((input) =>
        tabId(openRequest.omit({ nonce: true }).parse(input)),
      ),
    ).size,
    4,
  );
});
test("opening an existing tab keeps its position and preview, while view metadata updates", () => {
  const state = initialState();
  const first = openTab(state, file);
  first.preview = true;
  openTab(state, { kind: "terminal", workspaceId: "w1", path: "t1" });
  assert.equal(openTab(state, file), first);
  assert.equal(first.preview, true);
  assert.equal(state.tabs.length, 2);
  const view = openTab(state, { kind: "view", workspaceId: "w1", path: "v1", title: "Old", icon: "file", view: 1 });
  assert.equal(openTab(state, { kind: "view", workspaceId: "w1", path: "v1", title: "New", view: 2 }), view);
  assert.equal(view.title, "New");
  assert.equal(view.view, 2);
  assert.equal(view.icon, undefined);
});
test("diff contents replace in place and default to the empty diff", () => {
  const state = initialState();
  const request: TabInput = { ...file, kind: "diff", diff: "first" };
  const tab = openTab(state, request);
  assert.deepEqual(state.contents[tab.id], { diff: "first" });
  openTab(state, { ...request, diff: "second" });
  assert.deepEqual(state.contents[tab.id], { diff: "second" });
  openTab(state, { ...request, diff: undefined });
  assert.deepEqual(state.contents[tab.id], { diff: "" });
});
test("closing selects the next tab, then the previous last tab, and clears content", () => {
  const state = initialState();
  const tabs = ["a", "b", "c"].map((path) => openTab(state, { ...file, path }));
  state.active = tabs[1].id;
  state.contents[tabs[1].id] = { text: "old" };
  assert.equal(closeTab(state, tabs[1].id), tabs[2]);
  assert.equal(state.contents[tabs[1].id], undefined);
  assert.equal(closeTab(state, tabs[2].id), tabs[0]);
  assert.equal(closeTab(state, tabs[0].id), undefined);
  assert.equal(state.active, null);
});
test("closing an inactive or missing tab preserves selection; fallback uses the first tab", () => {
  const state = initialState();
  const first = openTab(state, file);
  const second = openTab(state, { ...file, path: "next" });
  closeTab(state, first.id);
  closeTab(state, "missing");
  assert.equal(state.active, second.id);
  state.active = "missing";
  assert.equal(activeTab(state), second);
});
test("file truncation starts only above two million UTF-16 code units", () => {
  assert.deepEqual(fileContent(""), { text: "" });
  const boundary = "a".repeat(2_000_000);
  assert.equal(fileContent(boundary).text, boundary);
  assert.equal(fileContent(boundary + "extra").text, boundary + "\n… file truncated");
});
test("missing files preserve the runtime error while file responses stay typed", () => {
  assert.deepEqual(readContent(fileResponse.parse(null)), { error: "cannot read property 'text' of null" });
  assert.deepEqual(readContent(fileResponse.parse({ text: "" })), { text: "" });
  assert.deepEqual(readContent(fileResponse.parse({ text: "contents" })), { text: "contents" });
  assert.equal(fileResponse.safeParse({ text: 1 }).success, false);
});
test("tab labels, markdown extension and language aliases preserve defaults", () => {
  const state = initialState();
  assert.equal(tabName(openTab(state, file)), "README.md");
  assert.equal(tabName(openTab(state, { ...file, kind: "terminal" })), "Terminal");
  assert.equal(tabName(openTab(state, { ...file, kind: "view" })), "View");
  assert.equal(tabName(openTab(state, { ...file, kind: "terminal", path: "empty-title", title: "" })), "");
  assert.equal(extension("README.MD"), "md");
  assert.equal(extension("file"), "file");
  assert.equal(LANGUAGES.mts, "typescript");
  assert.equal(LANGUAGES.tsx, "tsx");
  assert.equal(LANGUAGES.markdown, "markdown");
});
test("legacy requests validate and only finished runs match the event selector", () => {
  assert.equal(openRequest.safeParse({ ...file, nonce: 1 }).success, true);
  assert.equal(openRequest.safeParse({ ...file, nonce: "1" }).success, false);
  assert.equal(runFinished({ event: "run_finished", outcome: {} }), true);
  for (const kind of [undefined, null, {}, { event: "text_delta" }]) assert.equal(runFinished(kind), false);
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.