Official

files

Built-in files UI (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/files@0.2.0

Permissions in 0.2.0

  • Read files fs.readMediumReads files in the listed places.List the files of the workspacePlaces: the open workspace
  • Read chats chats.readMediumReads your transcripts and chat lists.Refresh the file list when an agent finishes a turn
  • Show panels ui.slotsLowShows views in the listed parts of the window.Show the files button and panelSlots: railright

Files

views.ts2.8 KB
// Files rail and panel trees. Rendering only reads state; handlers run effects.
// HTM whitespace is significant; keep these templates out of Prettier's HTML formatter.
import { html as htm } from "convergence";
import type { EffectViewContext } from "convergence/effect";
import type { Files } from "./controller.ts";
import { isOpen, selectedPath, visiblePaths } from "./panel.ts";

// Layout for the templates below: a flex column, a row that centres its
// children across (the old `ui.row`), and a child that takes the free
// space and may shrink below its content.
const column = { display: "flex", flexDirection: "column" };
const row = { display: "flex", flexDirection: "row", alignItems: "center" };
const fill = { flexGrow: 1, flexShrink: 1, flexBasis: "0%", minWidth: 0, minHeight: 0 };
const full = { width: "100%", height: "100%" };

export function renderRail(ctx: EffectViewContext<object>, actions: Files["Service"]) {
  return htm`
    <button id="rail-files" icon="files" variant="ghost" active=${ctx.app.shared?.rightPanel === "files"} tooltip="Files"
      onClick=${() => ctx.run(actions.toggle(ctx.app))} />`;
}

export function renderPanel(ctx: EffectViewContext<object>, actions: Files["Service"]) {
  const { state } = actions;
  if (!isOpen(ctx.app)) return null;
  const workspaceId = ctx.app.workspaceId;
  if (!workspaceId) {
    return htm`<div ...${column} gap="small" padding="medium"><text style="muted">Select a workspace to browse files.</text></div>`;
  }
  const paths = visiblePaths(state.paths, state.query);
  return htm`
      <div ...${column} ...${fill} ...${full} padding="small" paddingX="small">
        <div ...${row} gap="small" padding="small" paddingY="small">
          <text style="body" size=${13.125} weight=${600}>Files</text>
          <spacer />
          <button id="files-refresh" icon="rotate-cw" variant="ghost" small tooltip="Refresh" onClick=${() => ctx.run(actions.refresh(workspaceId))} />
          <button id="files-close" icon="x" variant="ghost" small tooltip="Close" onClick=${() => ctx.run(actions.close)} />
        </div>
        <input id="files-search" value=${state.query} placeholder="Search files..."
          onChange=${(e: { value: string }) => {
            state.query = e.value;
            ctx.update();
          }} />
        ${!state.loaded ? htm`<div ...${column} gap="small" padding="medium"><text style="muted">Loading files…</text></div>` : null}
        <div ...${column} ...${fill} gap="small" padding="small" paddingY="small">
          <tree id=${`files-${workspaceId}`} files=${paths.map((path) => ({ path }))} selected=${selectedPath(state.activePath)}
            onSelect=${(e: { id: string; folder: boolean }) => {
              if (!e.folder) ctx.run(actions.openFile(workspaceId, e.id));
            }} />
        </div>
      </div>`;
}

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.