opencode-ccgo-provider/src/tui.tsx

135 lines
4.3 KiB
TypeScript

// opencode TUI plugin exposing /cc-zdr, /cc-debug and /cc-status, plus a sidebar panel
// showing the current toggle state. It flips the shared toggle file (see ./toggles.ts)
// that the provider reads on every request, so a change takes effect without restarting
// opencode.
//
// opencode loads this module's default export (`{ id, tui }`) from a `tui.json` plugin
// entry. It is loaded from source (not dist) so opencode's Bun/Solid transform compiles
// the JSX and maps `solid-js` / `@opentui/solid` to its internal modules; that keeps this
// package free of runtime and build dependencies. The ambient types in ./tui-shims.d.ts
// back the JSX and `createSignal` references for `tsc -p tsconfig.tui.json`.
//
// Note: in this opencode version slash commands and the Ctrl+P palette read the same
// `namespace: "palette"` command list, so these entries appear in both; `hidden: true`
// would remove them from both.
import { createSignal } from "solid-js";
import { readToggles, toggle, type ToggleName, type Toggles } from "./toggles.js";
type ToastVariant = "info" | "success" | "warning" | "error";
type TuiCommand = {
name: string;
title?: string;
desc?: string;
category?: string;
namespace?: string;
slashName?: string;
slashAliases?: string[];
suggested?: boolean;
hidden?: boolean;
enabled?: boolean | (() => boolean);
run: () => void | Promise<void>;
};
type TuiTheme = { current: { text: unknown; textMuted: unknown; success: unknown } };
type TuiSlotHandler = (ctx: { theme: TuiTheme }, props: { session_id: string }) => unknown;
type TuiSlotPlugin = { order?: number; slots: Record<string, TuiSlotHandler> };
type TuiApi = {
keymap: { registerLayer(layer: { commands?: readonly TuiCommand[] }): () => void };
ui: { toast(input: { title?: string; message: string; variant?: ToastVariant; duration?: number }): void };
slots: { register(plugin: TuiSlotPlugin): string };
theme: TuiTheme;
lifecycle: { onDispose(fn: () => void): () => void };
};
const ID = "commandcode-toggles";
const CATEGORY = "CommandCode";
const SIDEBAR_ORDER = 90;
function state(value: boolean | undefined): string {
return value === true ? "on" : "off";
}
function summary(toggles: Toggles = readToggles()): string {
return `zdr=${state(toggles.zdr)}, debug=${state(toggles.debug)}`;
}
export const tui = async (api: TuiApi): Promise<void> => {
const [toggles, setToggles] = createSignal(readToggles());
const refresh = (): void => setToggles(readToggles());
const flip = (name: ToggleName): void => {
const next = toggle(name);
setToggles(next);
api.ui.toast({
title: CATEGORY,
message: `${name} ${state(next[name])} (${summary(next)})`,
variant: next[name] === true ? "success" : "info",
});
};
const Status = () => (
<box flexDirection="column" gap={0}>
<text fg={api.theme.current.text}>
<b>CommandCode</b>
</text>
<text fg={api.theme.current.textMuted}>zdr: {state(toggles().zdr)}</text>
<text fg={api.theme.current.textMuted}>debug: {state(toggles().debug)}</text>
</box>
);
api.slots.register({
order: SIDEBAR_ORDER,
slots: {
sidebar_content() {
return <Status />;
},
},
});
api.keymap.registerLayer({
commands: [
{
name: `${ID}.zdr`,
title: "CommandCode: toggle ZDR header",
desc: "Send the x-cmd-zdr header on requests",
category: CATEGORY,
namespace: "palette",
slashName: "cc-zdr",
run: () => flip("zdr"),
},
{
name: `${ID}.debug`,
title: "CommandCode: toggle debug tracing",
desc: "Write a redacted request and stream trace to a log file",
category: CATEGORY,
namespace: "palette",
slashName: "cc-debug",
run: () => flip("debug"),
},
{
name: `${ID}.status`,
title: "CommandCode: show toggle status",
desc: "Show the current zdr and debug state",
category: CATEGORY,
namespace: "palette",
slashName: "cc-status",
run: () => api.ui.toast({ title: CATEGORY, message: summary(), variant: "info" }),
},
],
});
const timer = setInterval(refresh, 1500);
api.lifecycle.onDispose(() => clearInterval(timer));
};
export const id = ID;
export default { id: ID, tui };