Official

palette

Built-in command palette (⌘⇧P) and chat search (⌘P).

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/palette@0.2.0

Permissions in 0.2.0

  • Run other plugins' commands commands.runHighRuns commands of other plugins.Run the command you pick, whichever plugin it belongs to
  • Read chats chats.readMediumReads your transcripts and chat lists.Search your chats by title and workspace, and open the one you pick
  • Show panels ui.slotsLowShows views in the listed parts of the window.Hold the palette, which opens over the windowSlots: title

Files

settings.test.ts7.6 KB
// Assign opens Keyboard Shortcuts through the convergence/settings
// service. Real activation with a canned settings client: Assign calls the
// service, and without a provider it only closes the palette.
import assert from "node:assert/strict";
import { test } from "node:test";
import * as z from "zod";
import type {
  Api,
  AppState,
  AppStatePatch,
  HostEvent,
  RenderResult,
  SlotOptions,
  ViewContext,
} from "convergence";
import { ui } from "convergence";
import { testLayer } from "convergence/effect/testing";
import { activate } from "./main.ts";

const tick = () => new Promise<void>((resolve) => setImmediate(resolve));
const stable = (value: unknown) =>
  JSON.stringify(value, (_key, item: unknown) => (typeof item === "function" ? "@handler" : item));

function fakeApi(withSettings: boolean) {
  let state: AppState = {};
  const appListeners = new Set<(state: AppState) => void>();
  const views: { render: (ctx: ViewContext<object>) => RenderResult; active: boolean }[] = [];
  const settingsCalls: { method: string; params: unknown }[] = [];
  const focused: string[] = [];
  let dirty = true;
  const update = (patch: AppStatePatch) => {
    state = {
      ...state,
      ...patch,
      workspaceId: patch.workspaceId === null ? undefined : (patch.workspaceId ?? state.workspaceId),
      chatId: patch.chatId === null ? undefined : (patch.chatId ?? state.chatId),
      route: patch.route ?? state.route,
      shared: { ...state.shared, ...patch.shared },
    };
    dirty = true;
    for (const listener of appListeners) listener(state);
  };
  const commands = [
    {
      id: "a.open",
      title: "Open",
      category: "A",
      plugin: "a",
      pluginTitle: "A",
      official: true,
      labels: [],
      source: "default",
      conflict: { key: "cmd+x", label: "⌘X", with: "b.other", withTitle: "Other" },
      problem: null,
      kernel: false,
      enabled: true,
      registered: true,
    },
  ];
  const kernelCall = async (method: string, params: unknown) => {
    if (method === "commands") return { commands };
    if (method === "palette.providers") return { providers: [] };
    throw new Error(`Unexpected kernel method ${method} ${JSON.stringify(params)}`);
  };
  const hostCall = async (method: string, params: unknown) => {
    if (method === "host/storage.get") return { value: null };
    if (method === "host/storage.set") return {};
    throw new Error(`Unexpected host method ${method} ${JSON.stringify(params)}`);
  };
  const settingsClient = {
    showShortcuts: Object.assign(async () => null, {
      // The Effect adapter calls through `result`, never the direct call.
      result: async (params: unknown) => {
        settingsCalls.push({ method: "showShortcuts", params });
        return { ok: true as const, value: null };
      },
    }),
    on: () => () => {},
  };
  const broker = testLayer();
  const registered = new Map<string, () => unknown>();
  // This assertion describes a local fake; unused API methods fail rather than impersonating external replies.
  const api = new Proxy(
    {
      ...broker.api,
      plugin: "palette",
      id: "convergence/palette",
      ui,
      services: {
        provide: async () => ({ emit: () => {}, remove: () => {} }),
        get: (name: string) => {
          if (name === "convergence/settings" && withSettings) return settingsClient;
          throw new Error(`no provider for ${name}`);
        },
        available: (name: string) => name === "convergence/settings" && withSettings,
        when: (name: string, fn: (client: unknown) => void) => {
          fn(name === "convergence/settings" && withSettings ? settingsClient : undefined);
          return () => {};
        },
      },
      host: { ...broker.api.host, call: hostCall, kernel: kernelCall },
      app: {
        state: () => state,
        update,
        share: (key: string, value: unknown) => update({ shared: { [key]: value } }),
        onChange: (fn: (state: AppState) => void) => {
          appListeners.add(fn);
          return () => {
            appListeners.delete(fn);
          };
        },
      },
      slot: (slot: string, render: (ctx: ViewContext<object>) => RenderResult, _options?: SlotOptions) => {
        const view = { render, active: true };
        views.push(view);
        dirty = true;
        return {
          id: views.length,
          update: () => {
            dirty = true;
          },
          remove: () => {
            view.active = false;
          },
        };
      },
      on: () => () => {},
      command: ({ id, run }: { id: string; run: () => unknown }) => {
        registered.set(id, run);
        return { remove: () => {} };
      },
      notify: () => {},
      focus: (id: string) => {
        focused.push(id);
      },
    },
    {
      get(target, key, receiver) {
        if (Reflect.has(target, key)) return Reflect.get(target, key, receiver);
        throw new Error(`Unexpected API ${String(key)}`);
      },
    },
  ) as unknown as Api;
  const render = () =>
    views
      .filter((view) => view.active)
      .map((view) =>
        view.render({
          state: {},
          app: state,
          plugin: "palette",
          update: () => {
            dirty = true;
          },
        }),
      );
  let trees: RenderResult[] = [];
  const settle = async () => {
    for (let round = 0; round < 50; round++) {
      await tick();
      if (!dirty) {
        await tick();
        if (!dirty) return;
      }
      dirty = false;
      trees = render();
    }
    throw new Error("palette did not settle");
  };
  const nodeSchema = z.object({ id: z.string().optional(), children: z.array(z.unknown()).optional() }).passthrough();
  function found(tree: unknown): Record<string, unknown>[] {
    if (Array.isArray(tree)) return tree.flatMap(found);
    const parsed = nodeSchema.safeParse(tree);
    if (!parsed.success) return [];
    return [
      parsed.data,
      ...(parsed.data.children ?? []).flatMap(found),
      ...(Array.isArray(parsed.data.items) ? parsed.data.items.flatMap(found) : []),
    ];
  }
  const invoke = (id: string, event = "onClick", payload: unknown = {}) => {
    const node = trees.flatMap((view) => found(view)).find((node) => node.id === id);
    assert.ok(node, `Missing ${id}`);
    const handler = z.custom<(payload: unknown) => unknown>((value) => typeof value === "function").parse(node[event]);
    handler(payload);
  };
  return {
    api,
    registered,
    settingsCalls,
    settle,
    invoke,
    trees: () => stable(trees),
    state: () => state,
  };
}

test("Assign on a conflicting key calls the settings service", async () => {
  const fake = fakeApi(true);
  const dispose = activate(fake.api);
  try {
    await fake.settle();
    fake.registered.get("palette.commands")?.();
    await fake.settle();
    assert.ok(fake.trees().includes("palette-assign-a.open"));
    fake.invoke("palette-assign-a.open");
    await fake.settle();
    assert.deepEqual(fake.settingsCalls, [{ method: "showShortcuts", params: { query: "a.open" } }]);
    assert.equal(fake.state().shared?.showShortcuts, undefined);
  } finally {
    await dispose();
  }
});

test("without the settings plugin Assign only closes the palette", async () => {
  const fake = fakeApi(false);
  const dispose = activate(fake.api);
  try {
    await fake.settle();
    fake.registered.get("palette.commands")?.();
    await fake.settle();
    assert.ok(fake.trees().includes("palette-assign-a.open"));
    fake.invoke("palette-assign-a.open");
    await fake.settle();
    assert.deepEqual(fake.settingsCalls, []);
    assert.equal(fake.state().shared?.showShortcuts, undefined);
    assert.ok(!fake.trees().includes("palette-assign-a.open"));
  } finally {
    await dispose();
  }
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.