/** @jsxImportSource @opentui/solid */ /** * opencode-lastact v2 TUI entrypoint — the `{ id, setup(context) }` plugin * shape the OpenCode v2 TUI host loads from the package's `./tui` export. * * JUST one thing: a very short view of the last-used time of the current * session. Which footer carries the line is config (`slots`: "prompt" | * "sidebar", default "prompt") — both surfaces are session-bound on the * current host. * * The activity-ledger model: subscriptions (per the `refreshOn` families, * plus the always-observed `session.execution.*` terminals) do nothing but * touch a per-session ledger cell; the view's fixed-cadence ticker is the * one bucket that derives the two clocks (last prompt, last LLM part) and * paints per the `view` mode. Every claim is additive `append`, never * `replace`. Setup returns a cleanup that disposes every claim and event * subscription. */ import { ActivityTracker } from "../activity/tracker.ts"; import { SLOT_PATHS, resolveConfig, surfaceGate, type Config, type SlotSurface, type SurfaceGate, } from "../config/config.ts"; import { LastactStatusLine } from "./status.tsx"; import { activityFromEvent, settleFromEvent, subscriptionTypes } from "./events.ts"; import type { SlotMap, TuiContext, TuiV2Event } from "./context.ts"; export const PLUGIN_ID = "opencode-lastact"; // Drift check (compile-time): every configured surface must map to a path in // the locally-typed published SlotMap, or typecheck fails here. const slotPathsArePublished: Record = SLOT_PATHS; void slotPathsArePublished; /** Claim one published path; the line itself gates on session kind. */ function claimPath( context: TuiContext, tracker: ActivityTracker, config: Config, path: (typeof SLOT_PATHS)[SlotSurface], gate: SurfaceGate, ): () => void { return context.ui.slot({ append: path, // every claimed path publishes a session id (optional on prompt.footer) render: (input: { readonly sessionID?: string }) => ( ), }); } export function setup(context: TuiContext): (() => void) | void { const config = resolveConfig(context.options ?? {}); if (!config.enabled) return; const tracker = new ActivityTracker(); // The event path stays dumb: route the event to a ledger cell and touch // it. All deriving happens in the view's bucket (the per-surface ticker). const onEvent = (event: TuiV2Event): void => { const settle = settleFromEvent(event); if (settle) { tracker.settle(settle); return; } const touch = activityFromEvent(event, config.refreshOn); if (touch) tracker.touch(touch.sessionID, touch.clock, touch.atMs); }; const unsubscribers = subscriptionTypes(config.refreshOn).map((type) => context.data.on(type, onEvent)); const disposers = [ // One claim per unique published path across both surface lists; each // line paints only for the session kind(s) that asked for the path. // Children matter: the host suppresses prompt/sidebar in a child pane, // and `session.composer.top` (the default child surface) mounts in // regular panes too — without the gate the line would paint twice. ...new Set([...config.slots, ...config.childSlots].map((surface) => SLOT_PATHS[surface])), ].map((path) => claimPath(context, tracker, config, path, surfaceGate(config, path))); return () => { for (const dispose of disposers) dispose(); for (const unsubscribe of unsubscribers) unsubscribe(); }; } const plugin = { id: PLUGIN_ID, setup }; export default plugin;