From e2a360b106453f2c6992a926d1bc6cd015524259 Mon Sep 17 00:00:00 2001 From: filipeforattini Date: Fri, 21 Aug 2026 16:19:14 -0300 Subject: [PATCH] fix(tui): remove global statusline --- .changeset/remove-statusline.md | 5 + README.md | 4 +- packages/opencode/src/plugin/tui/runtime.ts | 13 -- .../instance/httpapi/handlers/redskilled.ts | 16 +- packages/opencode/test/fixture/tui-plugin.ts | 3 - .../opencode/test/redskilled/client.test.ts | 1 - packages/plugin/src/tui.ts | 34 ---- packages/schema/src/redskilled.ts | 6 - packages/sdk/js/src/v2/gen/types.gen.ts | 44 +---- packages/tui/src/app.tsx | 4 - packages/tui/src/component/statusline.tsx | 184 ------------------ packages/tui/src/config/index.tsx | 13 +- packages/tui/src/plugin/adapters.tsx | 5 - packages/tui/src/plugin/runtime.tsx | 33 +--- packages/tui/src/routes/workers.tsx | 4 +- packages/tui/src/statusline.ts | 46 ----- packages/tui/test/config.test.tsx | 5 - packages/tui/test/statusline.test.ts | 45 ----- 18 files changed, 15 insertions(+), 450 deletions(-) create mode 100644 .changeset/remove-statusline.md delete mode 100644 packages/tui/src/component/statusline.tsx delete mode 100644 packages/tui/src/statusline.ts delete mode 100644 packages/tui/test/statusline.test.ts diff --git a/.changeset/remove-statusline.md b/.changeset/remove-statusline.md new file mode 100644 index 000000000000..aa8d52c887f8 --- /dev/null +++ b/.changeset/remove-statusline.md @@ -0,0 +1,5 @@ +--- +"opencode": patch +--- + +Remove the experimental global TUI statusline, its configuration, and its plugin extension API. diff --git a/README.md b/README.md index 46ff9dfc8d40..1c7124e1e1db 100644 --- a/README.md +++ b/README.md @@ -158,8 +158,8 @@ The **Workers** view is a live, project-scoped console over the daemon's public Redcode stores no separate consent, registration, or Project-control state. Drain intent and policy remain daemon-owned, and status comes from the Project projection reached through the supported -`red-skills-redskilled acp` stdio adapter. Host-wide statusline details are not presented because the -public ACP session deliberately binds each connection to one Project. +`red-skills-redskilled acp` stdio adapter. Host-wide details are not presented because the public ACP +session deliberately binds each connection to one Project. ## Install diff --git a/packages/opencode/src/plugin/tui/runtime.ts b/packages/opencode/src/plugin/tui/runtime.ts index 3ded685dafcf..2a88ad3d5934 100644 --- a/packages/opencode/src/plugin/tui/runtime.ts +++ b/packages/opencode/src/plugin/tui/runtime.ts @@ -609,18 +609,6 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop }, } - const statusline: TuiPluginApi["statusline"] = { - register(contribution) { - const handle = runtime.view.registerStatusline(base, contribution) - return { - update(next) { - handle.update(next) - }, - dispose: scope.track(() => handle.dispose()), - } - }, - } - return { app: api.app, attention: createScopedAttention(api.attention, scope, load.plugin_root), @@ -641,7 +629,6 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop event, renderer: api.renderer, slots, - statusline, plugins: { list() { return listPluginStatus(runtime) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/redskilled.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/redskilled.ts index fedc5e433a9a..cc711599c125 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/redskilled.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/redskilled.ts @@ -6,10 +6,7 @@ import { HttpApiBuilder } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../api" import { RedskilledApiError } from "../groups/redskilled" -const lastGood = new Map< - string, - { payload: Redskilled.Payload; render: NonNullable; at: string } ->() +const lastGood = new Map() export const redskilledHandlers = HttpApiBuilder.group(InstanceHttpApi, "redskilled", (handlers) => Effect.gen(function* () { @@ -149,13 +146,7 @@ function project(snapshot: Snapshot, key: string, scope: Redskilled.Scope): Reds registered_projects: registered ? [snapshot.state.project_label] : [], workers, } - const render = { - line: `${workers.length} Worker${workers.length === 1 ? "" : "s"} · ${snapshot.state.project_label}`, - degraded: true, - stale: true, - generated_at: now, - } - lastGood.set(key, { payload, render, at: now }) + lastGood.set(key, { payload, at: now }) const consent: Redskilled.Consent = registered ? "accepted" : "unknown" return { lifecycle: "degraded", @@ -168,7 +159,6 @@ function project(snapshot: Snapshot, key: string, scope: Redskilled.Scope): Reds runner: "ACP", }, payload, - render, last_success_at: now, } } @@ -180,7 +170,7 @@ function unavailable(key: string, scope: Redskilled.Scope, message: string): Red consent: "unknown", scope, native: true, - ...(cached ? { payload: cached.payload, render: cached.render, last_success_at: cached.at } : {}), + ...(cached ? { payload: cached.payload, last_success_at: cached.at } : {}), error: message, } } diff --git a/packages/opencode/test/fixture/tui-plugin.ts b/packages/opencode/test/fixture/tui-plugin.ts index c1ee6152b835..5d93139f002b 100644 --- a/packages/opencode/test/fixture/tui-plugin.ts +++ b/packages/opencode/test/fixture/tui-plugin.ts @@ -221,9 +221,6 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi { slots: { register: () => "fixture-slot", }, - statusline: { - register: () => ({ update() {}, dispose() {} }), - }, plugins: { list: () => [], activate: async () => false, diff --git a/packages/opencode/test/redskilled/client.test.ts b/packages/opencode/test/redskilled/client.test.ts index 9520bcafe94d..a408dc144978 100644 --- a/packages/opencode/test/redskilled/client.test.ts +++ b/packages/opencode/test/redskilled/client.test.ts @@ -309,7 +309,6 @@ describe("public redskilled ACP client", () => { "../../src/server/routes/instance/httpapi/handlers/redskilled.ts", "../../../schema/src/redskilled.ts", "../../../tui/src/app.tsx", - "../../../tui/src/component/statusline.tsx", "../../../tui/src/context/redskilled.tsx", "../../../tui/src/routes/workers.tsx", "../../../tui/src/routes/workers/history.ts", diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index b91cfd245d1c..0500704257c6 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -522,39 +522,6 @@ export type TuiSlots = { } } -export type TuiStatuslineTone = "default" | "muted" | "info" | "success" | "warning" | "error" - -export type TuiStatuslineImportance = "required" | "normal" | "optional" - -export type TuiStatuslineSegment = { - id: string - text: string - short?: string - tone?: TuiStatuslineTone - order?: number - importance?: TuiStatuslineImportance -} - -export type TuiStatuslineRow = { - id: string - segments: ReadonlyArray - order?: number -} - -export type TuiStatuslineContribution = { - segments?: ReadonlyArray - rows?: ReadonlyArray -} - -export type TuiStatuslineHandle = { - update: (contribution: TuiStatuslineContribution) => void - dispose: () => void -} - -export type TuiStatuslineApi = { - register: (contribution: TuiStatuslineContribution) => TuiStatuslineHandle -} - export type TuiEventBus = { on: (type: Type, handler: (event: Extract) => void) => () => void } @@ -654,7 +621,6 @@ export type TuiPluginApi = { event: TuiEventBus renderer: CliRenderer slots: TuiSlots - statusline: TuiStatuslineApi plugins: { list: () => ReadonlyArray activate: (id: string) => Promise diff --git a/packages/schema/src/redskilled.ts b/packages/schema/src/redskilled.ts index ec778381a340..9210734d1528 100644 --- a/packages/schema/src/redskilled.ts +++ b/packages/schema/src/redskilled.ts @@ -125,12 +125,6 @@ export const Status = Schema.Struct({ native: Schema.Literal(true), activation: Activation.pipe(optional), payload: Payload.pipe(optional), - render: Schema.Struct({ - line: Schema.String, - degraded: Schema.Boolean, - stale: Schema.Boolean, - generated_at: Schema.String, - }).pipe(optional), last_success_at: Schema.String.pipe(optional), error: Schema.String.pipe(optional), }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9c7af3b508f4..4c839f50d5ea 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1976,7 +1976,7 @@ export type Config = { } } /** - * Enable or configure LSP servers. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides. + * Configure LSP servers. Omit or set to true to enable built-ins, set to false to disable, or use an object for overrides. */ lsp?: | boolean @@ -11348,12 +11348,6 @@ export type RedskilledStatusResponses = { } }> } - render?: { - line: string - degraded: boolean - stale: boolean - generated_at: string - } last_success_at?: string error?: string } @@ -11479,12 +11473,6 @@ export type RedskilledConsentResponses = { } }> } - render?: { - line: string - degraded: boolean - stale: boolean - generated_at: string - } last_success_at?: string error?: string } @@ -11610,12 +11598,6 @@ export type RedskilledProjectResizeResponses = { } }> } - render?: { - line: string - degraded: boolean - stale: boolean - generated_at: string - } last_success_at?: string error?: string } @@ -11739,12 +11721,6 @@ export type RedskilledProjectStopResponses = { } }> } - render?: { - line: string - degraded: boolean - stale: boolean - generated_at: string - } last_success_at?: string error?: string } @@ -11870,12 +11846,6 @@ export type RedskilledWorkerStopResponses = { } }> } - render?: { - line: string - degraded: boolean - stale: boolean - generated_at: string - } last_success_at?: string error?: string } @@ -12001,12 +11971,6 @@ export type RedskilledWorkerRecycleResponses = { } }> } - render?: { - line: string - degraded: boolean - stale: boolean - generated_at: string - } last_success_at?: string error?: string } @@ -12133,12 +12097,6 @@ export type RedskilledWorkerSteerResponses = { } }> } - render?: { - line: string - degraded: boolean - stale: boolean - generated_at: string - } last_success_at?: string error?: string } diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index e6de59f45399..97adee43c2e9 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -70,7 +70,6 @@ import { TuiConfigProvider, useTuiConfig, type TuiConfig } from "./config" import { createTuiApiAdapters } from "./plugin/adapters" import { createTuiApi } from "./plugin/api" import { createPluginRuntime, PluginRuntimeProvider, usePluginRuntime, type TuiPluginHost } from "./plugin/runtime" -import { Statusline } from "./component/statusline" import { RedskilledProvider, useRedskilled } from "./context/redskilled" import { Workers } from "./routes/workers" import { CommandPaletteDialog } from "./component/command-palette" @@ -1188,9 +1187,6 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi {plugin()} - - - diff --git a/packages/tui/src/component/statusline.tsx b/packages/tui/src/component/statusline.tsx deleted file mode 100644 index 8f2c63bbadd7..000000000000 --- a/packages/tui/src/component/statusline.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import type { AssistantMessage } from "@opencode-ai/sdk/v2" -import type { TuiStatuslineSegment, TuiStatuslineTone } from "@opencode-ai/plugin/tui" -import { useTerminalDimensions } from "@opentui/solid" -import { createMemo, For, Show } from "solid-js" -import path from "path" -import { useRoute } from "../context/route" -import { useSync } from "../context/sync" -import { useTheme } from "../context/theme" -import { usePluginRuntime } from "../plugin/runtime" -import { fitStatuslineSegments, mergeStatuslineSegments } from "../statusline" -import { useRedskilled } from "../context/redskilled" - -function Segment(props: { segment: TuiStatuslineSegment }) { - const theme = useTheme().theme - const colors: Record = { - default: theme.text, - muted: theme.textMuted, - info: theme.info, - success: theme.success, - warning: theme.warning, - error: theme.error, - } - return {props.segment.text} -} - -export function Statusline(props: { version: string }) { - const dimensions = useTerminalDimensions() - const route = useRoute() - const sync = useSync() - const runtime = usePluginRuntime() - const theme = useTheme().theme - const redskilled = useRedskilled() - - const base = createMemo(() => { - const sessionID = route.data.type === "session" ? route.data.sessionID : undefined - const messages = sessionID ? (sync.data.message[sessionID] ?? []) : [] - const last = messages.findLast( - (item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0, - ) - const tokens = last - ? last.tokens.input + - last.tokens.output + - last.tokens.reasoning + - last.tokens.cache.read + - last.tokens.cache.write - : 0 - const model = last - ? sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID] - : undefined - const diff = sessionID ? (sync.data.session_diff[sessionID] ?? []) : [] - const additions = diff.reduce((total, item) => total + (item.additions ?? 0), 0) - const deletions = diff.reduce((total, item) => total + (item.deletions ?? 0), 0) - const mcp = Object.values(sync.data.mcp) - const connected = mcp.filter((item) => item.status === "connected").length - const failed = mcp.some((item) => item.status === "failed") - return [ - { - id: "project", - text: `${path.basename(sync.path.directory)}${sync.data.vcs?.branch ? ` (${sync.data.vcs.branch})` : ""}`, - short: path.basename(sync.path.directory), - tone: "muted", - importance: "required", - order: 10, - }, - { - id: "version", - text: `v${props.version}`, - tone: "muted", - importance: "optional", - order: 20, - }, - ...(last - ? [ - { - id: "model", - text: last.modelID, - short: last.modelID.split("/").at(-1), - importance: "normal" as const, - order: 30, - }, - ] - : []), - ...(tokens && model?.limit.context - ? [ - { - id: "context", - text: `ctx ${Math.round((tokens / model.limit.context) * 100)}%`, - importance: "normal" as const, - order: 40, - }, - ] - : []), - ...(additions || deletions - ? [ - { - id: "diff", - text: `+${additions} -${deletions}`, - tone: "warning" as const, - importance: "normal" as const, - order: 50, - }, - ] - : []), - ...(mcp.length - ? [ - { - id: "mcp", - text: `${failed ? "!" : ""}${connected} MCP`, - tone: failed ? ("error" as const) : ("success" as const), - importance: "optional" as const, - order: 90, - }, - ] - : []), - ...(redskilled.status() - ? [ - { - id: "redskilled", - text: redskilled.status()?.render?.line || `rsk ${redskilled.status()?.payload?.host.worker_count ?? 0}w`, - short: `rsk ${redskilled.status()?.payload?.host.worker_count ?? 0}`, - tone: - redskilled.status()?.lifecycle === "live" - ? ("success" as const) - : redskilled.status()?.lifecycle === "degraded" - ? ("warning" as const) - : ("muted" as const), - importance: "normal" as const, - order: 80, - }, - ] - : []), - ] satisfies TuiStatuslineSegment[] - }) - - const header = createMemo(() => - fitStatuslineSegments( - mergeStatuslineSegments( - base(), - runtime.statusline().map((item) => item.contribution.segments ?? []), - ), - Math.max(1, dimensions().width - 4), - ), - ) - - const rows = createMemo(() => - runtime - .statusline() - .flatMap((item) => item.contribution.rows ?? []) - .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0)), - ) - - return ( - - - - {(item, index) => ( - <> - 0}> - · - - - - )} - - - - {(row) => ( - - - {(item, index) => ( - <> - 0}> - · - - - - )} - - - )} - - - ) -} diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index 8856dc376278..bde0bfb0718a 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -58,10 +58,6 @@ export const Prompt = Schema.Struct({ }), }).annotate({ description: "Prompt size settings" }) -export const Statusline = Schema.Struct({ - enabled: Schema.optional(Schema.Boolean).annotate({ description: "Show the global TUI statusline" }), -}).annotate({ description: "Global statusline settings" }) - export const Info = Schema.Struct({ $schema: Schema.optional(Schema.String), theme: Schema.optional(Schema.String), @@ -71,7 +67,6 @@ export const Info = Schema.Struct({ leader_timeout: Schema.optional(LeaderTimeout), attention: Schema.optional(Attention), prompt: Schema.optional(Prompt), - statusline: Schema.optional(Statusline), scroll_speed: Schema.optional(ScrollSpeed).annotate({ description: "TUI scroll speed" }), scroll_acceleration: Schema.optional(ScrollAcceleration), diff_style: Schema.optional(DiffStyle), @@ -80,7 +75,7 @@ export const Info = Schema.Struct({ }) export type Info = Schema.Schema.Type -export type Resolved = Omit & { +export type Resolved = Omit & { attention: { enabled: boolean notifications: boolean @@ -92,9 +87,6 @@ export type Resolved = Omit(emptyCommands) const [status, setStatus] = createSignal>([]) - const [statusline, setStatusline] = createSignal< - ReadonlyArray<{ id: string; contribution: TuiStatuslineContribution }> - >([]) const slots = createSlots() return { @@ -24,26 +14,6 @@ export function createPluginRuntime() { routes: createPluginRoutes(), commands, status, - statusline, - registerStatusline(id: string, contribution: TuiStatuslineContribution) { - const key = `${id}:${statuslineCount++}` - let active = true - const replace = (next: TuiStatuslineContribution) => { - setStatusline((items) => [...items.filter((item) => item.id !== key), { id: key, contribution: next }]) - } - replace(contribution) - return { - update(next: TuiStatuslineContribution) { - if (!active) return - replace(next) - }, - dispose() { - if (!active) return - active = false - setStatusline((items) => items.filter((item) => item.id !== key)) - }, - } - }, update(input: { commands?: PluginRuntimeCommands; status?: ReadonlyArray }) { if (input.commands) setCommands(input.commands) if (input.status) setStatus(input.status) @@ -51,7 +21,6 @@ export function createPluginRuntime() { clear() { setCommands(emptyCommands) setStatus([]) - setStatusline([]) slots.clear() }, setupSlots(api: TuiPluginApi): HostSlots { diff --git a/packages/tui/src/routes/workers.tsx b/packages/tui/src/routes/workers.tsx index 0cb8094cb35b..3520af6566d6 100644 --- a/packages/tui/src/routes/workers.tsx +++ b/packages/tui/src/routes/workers.tsx @@ -36,10 +36,10 @@ const HEARTBEAT = { warn: 30_000, error: 120_000 } * OpenTUI overlaps cells when a flex subtree asks for more rows than the terminal has, * so this page never relies on wrapping: every section is a fixed count of single lines * and both panes get explicit sizes. CHROME_ROWS is what the page spends outside the - * panes (tab bar, two header lines, gaps, hint line, statusline); DETAIL_ROWS is what + * panes (tab bar, two header lines, gaps, and hint line); DETAIL_ROWS is what * the detail pane always draws around its activity feed. */ -const CHROME_ROWS = 10 +const CHROME_ROWS = 7 const DETAIL_ROWS = 12 export function Workers() { diff --git a/packages/tui/src/statusline.ts b/packages/tui/src/statusline.ts deleted file mode 100644 index c2e897ab4827..000000000000 --- a/packages/tui/src/statusline.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { TuiStatuslineImportance, TuiStatuslineSegment } from "@opencode-ai/plugin/tui" -import { Locale } from "./util/locale" - -const importance = (value: TuiStatuslineImportance | undefined) => { - if (value === "required") return 2 - if (value === "normal") return 1 - return 0 -} - -const width = (segments: ReadonlyArray) => - segments.reduce((total, item) => total + Bun.stringWidth(item.text), 0) + Math.max(0, segments.length - 1) * 3 - -export function fitStatuslineSegments(input: ReadonlyArray, maxWidth: number) { - const ordered = input.toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0)) - const shortened = ordered.map((item) => ({ ...item, text: item.short ?? item.text })) - const initial = width(ordered) <= maxWidth ? ordered : shortened - const fitted = ["optional", "normal"].reduce((items, level) => { - if (width(items) <= maxWidth) return items - return items - .toReversed() - .reduce((state, item) => { - if (width(state) <= maxWidth) return state - if ((item.importance ?? "optional") !== level) return state - const index = state.findIndex((candidate) => candidate.id === item.id) - return state.toSpliced(index, 1) - }, items) - .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0)) - }, initial) - if (width(fitted) <= maxWidth) return fitted - if (!fitted.length) return fitted - const available = Math.max(1, maxWidth - width(fitted.slice(0, -1)) - (fitted.length > 1 ? 3 : 0)) - return fitted.map((item, index) => - index === fitted.length - 1 ? { ...item, text: Locale.truncate(item.text, available) } : item, - ) -} - -export function mergeStatuslineSegments( - base: ReadonlyArray, - contributions: ReadonlyArray>, -) { - return Array.from( - [...base, ...contributions.flat()] - .reduce((items, item) => items.set(item.id, item), new Map()) - .values(), - ).toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0) || importance(b.importance) - importance(a.importance)) -} diff --git a/packages/tui/test/config.test.tsx b/packages/tui/test/config.test.tsx index b261ce9a667a..37fc0033e561 100644 --- a/packages/tui/test/config.test.tsx +++ b/packages/tui/test/config.test.tsx @@ -29,7 +29,6 @@ test("validates config constraints", () => { leader_timeout: 250, attention: { volume: 1, sounds: { done: "done.wav" } }, prompt: { max_height: 10, max_width: "auto" }, - statusline: { enabled: false }, scroll_speed: 0.001, diff_style: "stacked", cursor: { blinking: false }, @@ -40,7 +39,6 @@ test("validates config constraints", () => { attention: { volume: 1 }, diff_style: "stacked", cursor: { blinking: false }, - statusline: { enabled: false }, }) expect(() => decodeInfo({ leader_timeout: 0 })).toThrow() expect(() => decodeInfo({ attention: { volume: 1.1 } })).toThrow() @@ -63,7 +61,6 @@ test("resolves host-neutral defaults", () => { }) expect(config.leader_timeout).toBe(LeaderTimeoutDefault) expect(config.mouse).toBe(true) - expect(config.statusline.enabled).toBe(true) expect(config.keybinds.has("terminal.suspend")).toBe(true) expect(config.keybinds.has("session.list")).toBe(true) expect(config.cursor).toBeUndefined() @@ -84,7 +81,6 @@ test("resolves overrides without mutating input", () => { }, keybinds: { session_list: "ctrl+l" }, cursor: { blinking: false }, - statusline: { enabled: false }, } const config = resolve(input, { terminalSuspend: true }) @@ -94,7 +90,6 @@ test("resolves overrides without mutating input", () => { leader_timeout: 750, attention: input.attention, cursor: { style: "block", blinking: false }, - statusline: { enabled: false }, }) expect(config.keybinds.get("session.list")).toHaveLength(1) expect(input.keybinds).toEqual({ session_list: "ctrl+l" }) diff --git a/packages/tui/test/statusline.test.ts b/packages/tui/test/statusline.test.ts deleted file mode 100644 index ff7ade28ee82..000000000000 --- a/packages/tui/test/statusline.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { createPluginRuntime } from "../src/plugin/runtime" -import { fitStatuslineSegments, mergeStatuslineSegments } from "../src/statusline" - -describe("statusline", () => { - test("replaces base segments by stable id", () => { - expect( - mergeStatuslineSegments( - [ - { id: "project", text: "opencode", order: 10 }, - { id: "model", text: "sonnet", order: 20 }, - ], - [[{ id: "project", text: "red-code", order: 10 }]], - ), - ).toEqual([ - { id: "project", text: "red-code", order: 10 }, - { id: "model", text: "sonnet", order: 20 }, - ]) - }) - - test("shortens then drops optional segments before required ones", () => { - const segments = [ - { id: "project", text: "long-project", short: "repo", importance: "required" as const, order: 10 }, - { id: "model", text: "claude-opus", short: "opus", importance: "normal" as const, order: 20 }, - { id: "version", text: "v1.18.18", importance: "optional" as const, order: 30 }, - ] - - expect(fitStatuslineSegments(segments, 16).map((item) => item.text)).toEqual(["repo", "opus"]) - expect(fitStatuslineSegments(segments, 5).map((item) => item.text)).toEqual(["repo"]) - }) - - test("updates and disposes plugin contributions independently", () => { - const runtime = createPluginRuntime() - const first = runtime.registerStatusline("dev", { segments: [{ id: "one", text: "one" }] }) - const second = runtime.registerStatusline("dev", { segments: [{ id: "two", text: "two" }] }) - - first.update({ segments: [{ id: "one", text: "updated" }] }) - expect(runtime.statusline().map((item) => item.contribution.segments?.[0]?.text)).toEqual(["two", "updated"]) - - first.dispose() - expect(runtime.statusline().map((item) => item.contribution.segments?.[0]?.text)).toEqual(["two"]) - second.dispose() - expect(runtime.statusline()).toEqual([]) - }) -})