From a13623ca19e31159cd40c61185e333a4d90599de Mon Sep 17 00:00:00 2001 From: shuv Date: Wed, 15 Jul 2026 01:40:01 -0700 Subject: [PATCH 1/2] fix(tui): restore fork branding and layout --- packages/tui/src/component/logo.tsx | 8 +++-- packages/tui/src/component/prompt/index.tsx | 18 +++++----- packages/tui/src/config/index.tsx | 3 ++ .../tui/src/feature-plugins/home/footer.tsx | 15 +++++---- packages/tui/src/routes/home.tsx | 23 +++++++++---- packages/tui/src/util/responsive.ts | 28 ++++++++++++++++ packages/tui/test/logo.test.ts | 7 ++++ packages/tui/test/responsive.test.ts | 33 +++++++++++++++++++ 8 files changed, 111 insertions(+), 24 deletions(-) create mode 100644 packages/tui/src/util/responsive.ts create mode 100644 packages/tui/test/logo.test.ts create mode 100644 packages/tui/test/responsive.test.ts diff --git a/packages/tui/src/component/logo.tsx b/packages/tui/src/component/logo.tsx index bc24b3c553f9..34bb3a6af853 100644 --- a/packages/tui/src/component/logo.tsx +++ b/packages/tui/src/component/logo.tsx @@ -2,7 +2,9 @@ import { RGBA, TextAttributes } from "@opentui/core" import { For, type JSX } from "solid-js" import { useTheme } from "../context/theme" import { tint } from "../theme/color" -import { logo } from "../logo" +import { logo } from "../shuv-logo" + +export const defaultLogo = logo export function Logo() { const { theme } = useTheme() @@ -49,11 +51,11 @@ export function Logo() { return ( - + {(line, index) => ( {renderLine(line, theme.textMuted, false)} - {renderLine(logo.right[index()], theme.text, true)} + {renderLine(defaultLogo.right[index()], theme.text, true)} )} diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index fb0a6e2fca87..75df6f96d2b4 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -55,6 +55,7 @@ import { useLocation } from "../../context/location" import { Keymap, type KeymapCommand } from "../../context/keymap" import { contextUsage } from "../../util/session" import { abbreviateHome } from "../../runtime" +import { promptMetadata } from "../../util/responsive" registerOpencodeSpinner() @@ -257,9 +258,10 @@ export function Prompt(props: PromptProps) { ], })) const [cursorVersion, setCursorVersion] = createSignal(0) + const metadata = createMemo(() => promptMetadata(dimensions().width, local.model.parsed().model)) const currentProviderLabel = createMemo(() => local.model.parsed().provider) const connected = useConnected() - const hasRightContent = createMemo(() => Boolean(props.right)) + const hasRightContent = createMemo(() => Boolean(props.right) && !metadata().compact) function promptModelWarning() { toast.show({ @@ -1480,11 +1482,14 @@ export function Prompt(props: PromptProps) { - {local.model.parsed().model} + {metadata().model} - {currentProviderLabel()} - + + {currentProviderLabel()} + + · @@ -1565,10 +1570,7 @@ export function Prompt(props: PromptProps) { - } - > + }> {(location) => ( {location()} diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index 46c649fff14a..bcd26c0962e6 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -96,6 +96,9 @@ export const Info = Schema.Struct({ ).annotate({ description: "Terminal integration settings" }), prompt: Schema.optional( Schema.Struct({ + max_width: Schema.optional( + Schema.Union([Schema.Int.check(Schema.isGreaterThan(0)), Schema.Literal("auto")]), + ).annotate({ description: "Prompt width in columns; 'auto' follows the terminal width" }), editor: Schema.optional(Schema.Boolean).annotate({ description: "Include the active editor file or selection as prompt context", }), diff --git a/packages/tui/src/feature-plugins/home/footer.tsx b/packages/tui/src/feature-plugins/home/footer.tsx index 8a1fc02cb2a8..f62e28ad9d18 100644 --- a/packages/tui/src/feature-plugins/home/footer.tsx +++ b/packages/tui/src/feature-plugins/home/footer.tsx @@ -6,6 +6,7 @@ import { useTuiPaths } from "../../context/runtime" import { useTheme } from "../../context/theme" import { abbreviateHome } from "../../runtime" import { FilePath } from "../../ui/file-path" +import { homeFooterLayout } from "../../util/responsive" function Directory(props: { context: Plugin.Context; maxWidth: number }) { const { theme } = useTheme() @@ -56,6 +57,9 @@ function View(props: { context: Plugin.Context }) { const count = list.filter((item) => item.status.status === "connected").length return Bun.stringWidth(`⊙ ${count} MCP /status`) + 2 }) + const layout = createMemo(() => + homeFooterLayout(dimensions().width, Bun.stringWidth(InstallationVersion), mcpWidth()), + ) return ( - - + + + + {InstallationVersion} diff --git a/packages/tui/src/routes/home.tsx b/packages/tui/src/routes/home.tsx index 92640c56b323..4f92c48059bc 100644 --- a/packages/tui/src/routes/home.tsx +++ b/packages/tui/src/routes/home.tsx @@ -11,6 +11,9 @@ import { useData } from "../context/data" import { useLocation } from "../context/location" import { FormPrompt } from "./session/form" import { PluginSlot } from "../plugin/context" +import { useTerminalDimensions } from "@opentui/solid" +import { useConfig } from "../config" +import { homePromptMaxWidth, showHomeLogo } from "../util/responsive" let once = false const placeholder = { @@ -28,6 +31,10 @@ export function Home() { const editor = useEditorContext() const data = useData() const location = useLocation() + const dimensions = useTerminalDimensions() + const config = useConfig().data + const promptMaxWidth = createMemo(() => homePromptMaxWidth(dimensions().width, config.prompt?.max_width)) + const logoVisible = createMemo(() => showHomeLogo(dimensions().width, dimensions().height)) // Global MCP elicitations can arrive without a session route, so keep them reachable from Home. const forms = createMemo(() => data.session.form.list("global", data.location.default()) ?? []) let sent = false @@ -69,13 +76,15 @@ export function Home() { - - - - - - - + + + + + + + + + = 80 && height >= 24 +} + +export function homeFooterLayout(width: number, versionWidth: number, mcpWidth: number) { + const compact = width < 70 + return { + directoryWidth: Math.max(2, width - 8 - versionWidth - (compact ? 0 : mcpWidth)), + gap: compact ? 1 : 2, + showMcp: !compact, + } +} + +export function promptMetadata(width: number, model: string) { + const compact = width < 70 + return { + compact, + model: compact ? Locale.truncate(model, Math.max(8, width - 28)) : model, + } +} diff --git a/packages/tui/test/logo.test.ts b/packages/tui/test/logo.test.ts new file mode 100644 index 000000000000..d3b3e351f884 --- /dev/null +++ b/packages/tui/test/logo.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from "bun:test" +import { defaultLogo } from "../src/component/logo" +import { logo } from "../src/shuv-logo" + +test("the default home logo uses the shuvcode wordmark", () => { + expect(defaultLogo).toEqual(logo) +}) diff --git a/packages/tui/test/responsive.test.ts b/packages/tui/test/responsive.test.ts new file mode 100644 index 000000000000..e9d549a796f1 --- /dev/null +++ b/packages/tui/test/responsive.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from "bun:test" +import { homeFooterLayout, homePromptMaxWidth, promptMetadata, showHomeLogo } from "../src/util/responsive" + +describe("responsive TUI layout", () => { + test("clamps the home prompt to the terminal content width", () => { + expect(homePromptMaxWidth(60, undefined)).toBe(56) + expect(homePromptMaxWidth(120, undefined)).toBe(75) + expect(homePromptMaxWidth(100, "auto")).toBe(70) + expect(homePromptMaxWidth(30, "auto")).toBe(26) + }) + + test("hides the home logo when either terminal dimension is constrained", () => { + expect(showHomeLogo(80, 24)).toBe(true) + expect(showHomeLogo(79, 24)).toBe(false) + expect(showHomeLogo(80, 23)).toBe(false) + }) + + test("hides MCP status and budgets the footer directory on narrow terminals", () => { + expect(homeFooterLayout(60, 12, 18)).toEqual({ directoryWidth: 40, gap: 1, showMcp: false }) + expect(homeFooterLayout(100, 12, 18)).toEqual({ directoryWidth: 62, gap: 2, showMcp: true }) + }) + + test("compacts long model metadata below 70 columns", () => { + expect(promptMetadata(69, "anthropic/claude-opus-4-very-long-model-name")).toEqual({ + compact: true, + model: "anthropic/claude-opus-4-very-long-model-\u2026", + }) + expect(promptMetadata(70, "anthropic/claude-opus-4-very-long-model-name")).toEqual({ + compact: false, + model: "anthropic/claude-opus-4-very-long-model-name", + }) + }) +}) From e3c74c05aeb0c2e79bddcef1cc0205547797d88b Mon Sep 17 00:00:00 2001 From: shuv Date: Wed, 15 Jul 2026 01:40:07 -0700 Subject: [PATCH 2/2] docs: harden shared service migration --- docs/shared-service.md | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/docs/shared-service.md b/docs/shared-service.md index d0709bc2f524..e884ba23436a 100644 --- a/docs/shared-service.md +++ b/docs/shared-service.md @@ -11,8 +11,11 @@ not create caller-specific Shuvcode configuration domains. - The canonical configuration root is `~/.config/opencode`; neither the unit nor an interactive shell sets `OPENCODE_CONFIG_DIR`. - Shuvcode's normal XDG data and state roots remain canonical for every caller. -- `~/.config/opencode/service.json` is private mode `0600` and contains the - administrator credential used by trusted loopback clients. +- The channel-specific service files under `~/.config/opencode` and + `~/.local/state/opencode` are private mode `0600`. They contain the + administrator credential and managed-service registration used by trusted + loopback clients. Use `shuvcode service` commands instead of selecting a file + by name. - Mobile clients receive independently revocable device credentials through pairing. A bridge may keep its own client-facing credential domain while using the administrator credential on loopback. @@ -39,10 +42,17 @@ to make a reverse-proxy URL reachable. Stop dependent callers, back up both configuration roots, and merge the desired server configuration into `~/.config/opencode`. Preserve the active service -password by moving it into the canonical `service.json`, then update dependent -loopback clients to the same value without printing it. Remove any systemd -drop-in that sets `OPENCODE_CONFIG_DIR`, reload the user manager, and restart -Shuvcode before its dependants. +password through `shuvcode service`, then update dependent loopback clients to +the same value without printing it. Remove any systemd drop-in that sets +`OPENCODE_CONFIG_DIR`, reload the user manager, and restart Shuvcode before its +dependants. + +Migrate plugins as part of the configuration merge. A V1-shaped document keeps +the singular `plugin` field because the V2 loader migrates the whole document; +a native V2 document uses `plugins`. Do not rename only that field in an +otherwise V1 document: it will be ignored during migration. Replace legacy +plugin entrypoints with their V2 server adapters and verify every required +integration method after restart. Provider authentication stored only in a legacy credential file is not a V2 integration connection. Reconnect those providers through the V2 TUI after the @@ -62,5 +72,8 @@ shuvcode pair Verify that interactive TUI sessions and paired mobile sessions appear in the same session list, and that every configured V2 provider appears through the -model endpoint. Logs and verification output must not contain administrator -passwords, invitation tokens, or device credentials. +model endpoint. For providers supplied by plugins, verify the required login +methods too; an Anthropic subscription deployment must advertise an OAuth +method, not only API-key or environment methods. Logs and verification output +must not contain administrator passwords, invitation tokens, or device +credentials.