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.
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",
+ })
+ })
+})