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
Files
controller.ts4.2 KB
// Scoped file-panel effects: host reads and the convergence/tabs client.
// A layer owns one state instance for both registered views.
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Stream from "effect/Stream";
import { App, Host, Notify, Services } from "convergence/effect";
import type { EffectClient } from "convergence/effect";
import type { AppState, ViewHandle } from "convergence";
import type { Tabs } from "../tabs/contract.ts";
import { initialState, isOpen } from "./panel.ts";
const makeFiles = Effect.fn("Files.make")(function* () {
const host = yield* Host;
const app = yield* App;
const notify = yield* Notify;
const services = yield* Services;
const scope = yield* Effect.scope;
const state = initialState();
const views: ViewHandle[] = [];
const refresh = Effect.fn("Files.refresh")(function* (workspaceId: string) {
state.workspaceId = workspaceId;
yield* host.call("host/fs.search", { workspaceId, limit: 100000 }).pipe(
Effect.tap((result) =>
Effect.sync(() => {
state.paths = result.paths;
}),
),
Effect.catchTag(["HostCallFailed", "NeedsReview", "PermissionNotGranted"], (error) =>
notify.warning(`Files unavailable: ${error.message}`),
),
);
state.loaded = true;
for (const view of views) view.update();
});
const followApp = Effect.fn("Files.followApp")(function* (next: AppState) {
if (isOpen(next) && next.workspaceId && next.workspaceId !== state.workspaceId) yield* refresh(next.workspaceId);
});
const openFile = Effect.fn("Files.openFile")(function* (workspaceId: string, path: string) {
const client = state.tabs;
// Without the tabs plugin there is no detail pane; the shared request
// the baseline wrote would have had no reader either.
if (!client) return;
yield* client.open({ kind: "file", workspaceId, path }).pipe(
// A call that loses its provider on the way behaves like no reader.
Effect.ignore,
);
});
const toggle = Effect.fn("Files.toggle")((next: AppState) =>
app.update({
shared: {
rightPanel: next.shared?.rightPanel === "files" ? null : "files",
},
}),
);
// The open file follows the visible tab while the tabs plugin runs. One
// episode per provider: the previous follow ends when the provider goes.
const followTabs = Effect.fn("Files.followTabs")(function* () {
yield* services.when<typeof Tabs>("convergence/tabs").pipe(
Stream.runForEach((option) =>
Effect.gen(function* () {
if (state.follow) {
yield* Fiber.interrupt(state.follow);
state.follow = undefined;
}
if (Option.isNone(option)) {
state.tabs = undefined;
return;
}
const client: EffectClient<typeof Tabs> = option.value;
state.tabs = client;
const shown = (tab: { kind: string; path: string } | null) => {
state.activePath = tab?.kind === "file" ? tab.path : null;
};
state.follow = yield* Effect.forkIn(
client.on("activeChanged").pipe(
Stream.runForEach((tab) =>
Effect.sync(() => {
shown(tab);
for (const view of views) view.update();
}),
),
),
scope,
{ startImmediately: true },
);
yield* client.active({}).pipe(
Effect.tap((tab) =>
Effect.sync(() => {
shown(tab);
for (const view of views) view.update();
}),
),
// A provider that goes away mid-read leaves the last selection.
Effect.ignore,
);
}),
),
Effect.forkIn(scope, { startImmediately: true }),
);
});
yield* followTabs();
return { state, views, refresh, followApp, openFile, toggle, close: app.update({ shared: { rightPanel: null } }) };
});
export class Files extends Context.Service<Files, Effect.Success<ReturnType<typeof makeFiles>>>()("files/Files") {
static readonly layer = Layer.effect(Files, makeFiles());
}Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 6.4 KB | 3 permissions | Listed |
No comments yet.