Official
terminal
Built-in integrated terminal (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/terminal@0.2.0
Permissions in 0.2.0
Take care. This plugin asks for permissions that can do anything your account can. The app asks you to hold Enable for two seconds or to type the plugin's name before it turns on.
Files
state.ts3 KB
// Workspace-scoped terminal selection is independent of the views and kernel effects.
// The state layer gives each activation its own session cache and creation guards.
import * as Context from "effect/Context";
import * as Layer from "effect/Layer";
import * as z from "zod";
import type { EffectClient } from "convergence/effect";
import type * as Fiber from "effect/Fiber";
import type { Terminal, ViewHandle, Workspace } from "convergence";
import type { Tabs } from "../tabs/contract.ts";
export interface State {
terminals: readonly Terminal[];
activeByWorkspace: Record<string, string | null>;
paths: Record<string, string>;
menu: string | null;
renaming: string | null;
creating: Set<string>;
ready: boolean;
flyOut?: ViewHandle;
rail?: ViewHandle;
// The convergence/tabs client while its provider runs, the tab it last
// announced, and the fiber following it.
tabs: EffectClient<typeof Tabs> | undefined;
activeTab: { kind: string; path: unknown } | null;
follow: Fiber.Fiber<void> | undefined;
}
export interface State {
terminals: readonly Terminal[];
activeByWorkspace: Record<string, string | null>;
paths: Record<string, string>;
menu: string | null;
renaming: string | null;
creating: Set<string>;
ready: boolean;
flyOut?: ViewHandle;
rail?: ViewHandle;
}
export class TerminalState extends Context.Service<TerminalState, State>()("terminal/State") {
static readonly layer = Layer.sync(TerminalState, () => ({
terminals: [],
activeByWorkspace: {},
paths: {},
menu: null,
renaming: null,
creating: new Set<string>(),
ready: false,
tabs: undefined,
activeTab: null,
follow: undefined,
}));
}
export function forWorkspace(terminals: readonly Terminal[], workspaceId: string | null | undefined): Terminal[] {
return terminals.filter((terminal) => terminal.workspaceId === workspaceId);
}
export function activeTerminal(
state: Pick<State, "terminals" | "activeByWorkspace">,
workspaceId: string | undefined,
): Terminal | null {
const mine = forWorkspace(state.terminals, workspaceId);
return (
mine.find((terminal) => terminal.id === state.activeByWorkspace[String(workspaceId)]) ??
mine[mine.length - 1] ??
null
);
}
export function nextTerminal(terminals: readonly Terminal[], terminal: Terminal): Terminal | null {
const mine = forWorkspace(terminals, terminal.workspaceId);
const index = mine.findIndex((session) => session.id === terminal.id);
return mine[index + 1] ?? mine[index - 1] ?? null;
}
// The detail pane owns the payload; this plugin only compares its identity and tests its truthiness.
const tab = z.object({ kind: z.string(), path: z.unknown().optional() });
export function terminalInPane(active: unknown): unknown {
const parsed = tab.safeParse(active);
return parsed.success && parsed.data.kind === "terminal" ? parsed.data.path : null;
}
export function pathsByWorkspace(workspaces: readonly Workspace[]): Record<string, string> {
return Object.fromEntries(workspaces.map((workspace) => [workspace.id, workspace.path]));
}Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 8.7 KB | 3 permissions | Listed |
No comments yet.