feat(settings): add Info section with version, runtime, and diagnostics (#413)

## Summary

Adds a new **Info** section to the Settings screen, giving users quick
visibility into their CodeNomad version, runtime environment, and a way
to collect diagnostic data for bug reports.

Closes #412

## Changes

- **New component:** `info-settings-section.tsx` — renders three cards:
- **About** — Server/UI version, runtime type (electron/tauri/web),
platform, OS with CPU architecture detection, server URL, workspace root
- **Updates** — Placeholder "Check for updates" button, ready to wire
into the existing dev release monitor (`serverMeta.update`)
- **Diagnostics** — Log scope dropdown (Summary only / Summary +
workspace logs), Copy to clipboard, Download .txt — generates a
structured diagnostic report

- **New styles:** `settings-info.css` — info row layout, select row,
toast feedback, update note

- **Wiring:** Added `"info"` to `SettingsSectionId` union, registered
nav item and routing case in `settings-screen.tsx`, imported CSS in
`controls.css`

- **i18n:** 20 new keys added to all 7 locales (English canonical,
others fall back via existing fallback chain)

## Design decisions

- No server changes needed — version/runtime info comes from existing
`GET /api/meta` endpoint and client-side `navigator` detection
- No sensitive data (API keys, env values) included in diagnostic
reports
- CPU architecture extracted from `navigator.userAgent` for
Linux/Windows/macOS
- Log scope dropdown uses existing Kobalte Select pattern for
consistency

## Verification

- `tsc --noEmit` passes (0 errors)
- `vite build` bundles successfully
- Tauri Rust backend compiles and starts correctly

---

> **Note:** The majority of this implementation was produced through
CodeNomad using DeepSeek v4 Pro and has undergone manual review before
submission. All design, architecture, and code quality decisions were
evaluated by a human reviewer.

---------

Co-authored-by: Shantur Rathore <i@shantur.com>
This commit is contained in:
Claas Pancratius 2026-05-15 15:09:03 +02:00 committed by GitHub
parent 1dbb4a91e1
commit 1295cfabca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 558 additions and 2 deletions

View file

@ -1,5 +1,5 @@
import { Dialog } from "@kobalte/core/dialog"
import { Settings, Bell, MonitorUp, Paintbrush, Terminal, Volume2, Globe, X } from "lucide-solid"
import { Settings, Bell, Info, MonitorUp, Paintbrush, Terminal, Volume2, Globe, X } from "lucide-solid"
import { createMemo, For, type Component } from "solid-js"
import { useI18n } from "../lib/i18n"
import {
@ -10,6 +10,7 @@ import {
type SettingsSectionId,
} from "../stores/settings-screen"
import { AppearanceSettingsSection } from "./settings/appearance-settings-section"
import { InfoSettingsSection } from "./settings/info-settings-section"
import { NotificationsSettingsSection } from "./settings/notifications-settings-section"
import { OpenCodeSettingsSection } from "./settings/opencode-settings-section"
import { RemoteAccessSettingsSection } from "./settings/remote-access-settings-section"
@ -27,6 +28,7 @@ export const SettingsScreen: Component = () => {
{ id: "speech" as SettingsSectionId, icon: Volume2, label: t("settings.nav.speech") },
{ id: "sidecars" as SettingsSectionId, icon: Globe, label: t("settings.nav.sidecars") },
{ id: "opencode" as SettingsSectionId, icon: Terminal, label: t("settings.nav.opencode") },
{ id: "info" as SettingsSectionId, icon: Info, label: t("settings.nav.info") },
]
if (canOpenRemoteWindows()) {
@ -48,6 +50,8 @@ export const SettingsScreen: Component = () => {
return <SideCarsSettingsSection />
case "opencode":
return <OpenCodeSettingsSection />
case "info":
return <InfoSettingsSection />
case "appearance":
default:
return <AppearanceSettingsSection />

View file

@ -0,0 +1,333 @@
import { createEffect, createMemo, createResource, createSignal, onCleanup, type Component } from "solid-js"
import { Info } from "lucide-solid"
import { useI18n } from "../../lib/i18n"
import { getServerMeta } from "../../lib/server-meta"
import { runtimeEnv } from "../../lib/runtime-env"
import type { ServerMeta } from "../../../../server/src/api-types"
interface UserAgentData {
platform?: string
getHighEntropyValues?: (hints: string[]) => Promise<Record<string, string>>
}
function getUserAgentData(): UserAgentData | undefined {
return (navigator as any).userAgentData
}
function detectOs(): string {
if (typeof navigator === "undefined") return "Unknown"
const uaData = getUserAgentData()
if (uaData?.platform) {
const arch = extractArchFromUA(navigator.userAgent)
return arch ? `${uaData.platform} ${arch}` : uaData.platform
}
const ua = navigator.userAgent
const p = navigator.platform
if (!p) return "Unknown"
const maybeArch = extractArchFromUA(ua)
if (maybeArch && !p.includes(maybeArch)) {
return `${p} ${maybeArch}`
}
return p
}
function extractArchFromUA(ua: string): string | null {
const match = ua.match(/Linux\s+(x86_64|aarch64|armv[0-9]+[a-z]*|i[3-6]86)/i)
?? ua.match(/Win64;\s*(x64|arm64)/i)
?? ua.match(/Mac\s*OS\s*X[^)]*?_(x86_64|arm64)/i)
return match ? match[1] : null
}
async function resolveArchitecture(): Promise<string | null> {
try {
const uaData = getUserAgentData()
if (!uaData?.getHighEntropyValues) return null
const values = await uaData.getHighEntropyValues(["architecture", "bitness"])
const parts: string[] = []
if (values.architecture && !values.architecture.startsWith("x86")) {
parts.push(values.architecture)
}
if (values.bitness && values.bitness !== "64") {
parts.push(`${values.bitness}-bit`)
}
if (!parts.length && values.architecture) {
parts.push(values.architecture)
}
return parts.length > 0 ? parts.join(" ") : null
} catch {
return null
}
}
function buildDiagnosticReport(
meta: ServerMeta | null,
osDisplay: string,
): string {
const lines: string[] = []
lines.push("CodeNomad Diagnostic Report")
lines.push("============================")
lines.push(`Generated: ${new Date().toISOString()}`)
lines.push(`Server version: ${meta?.serverVersion ?? "unknown"}`)
lines.push(`UI version: ${meta?.ui?.version ?? "unknown"} (source: ${meta?.ui?.source ?? "unknown"})`)
lines.push(`Runtime: ${runtimeEnv.host}`)
lines.push(`Platform: ${runtimeEnv.platform}`)
lines.push(`Window context: ${runtimeEnv.windowContext}`)
lines.push(`OS: ${osDisplay}`)
lines.push(`Server URL: ${meta?.localUrl ?? "unknown"}`)
lines.push(`Workspace root: ${meta?.workspaceRoot ?? "unknown"}`)
lines.push(`UI source: ${meta?.ui?.source ?? "unknown"}`)
lines.push("")
return lines.join("\n")
}
async function copyToClipboard(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text)
return true
} catch {
return false
}
}
function downloadTextFile(filename: string, text: string) {
const blob = new Blob([text], { type: "text/plain;charset=utf-8" })
const url = URL.createObjectURL(blob)
const anchor = document.createElement("a")
anchor.href = url
anchor.download = filename
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
URL.revokeObjectURL(url)
}
function extractReleasePrefix(version: string): string {
return version.replace(/^v/, "").split("-")[0]
}
function versionNewer(current: string, latest: string): boolean | null {
const c = extractReleasePrefix(current).split(".").map(Number)
const l = extractReleasePrefix(latest).split(".").map(Number)
if (c.some(isNaN) || l.some(isNaN)) return null
if (l[0] > c[0]) return true
if (l[0] < c[0]) return false
if (l[1] > c[1]) return true
if (l[1] < c[1]) return false
if (l[2] > c[2]) return true
return false
}
export const InfoSettingsSection: Component = () => {
const { t } = useI18n()
const [meta, { mutate }] = createResource(() => getServerMeta())
const [copyFeedback, setCopyFeedback] = createSignal<"success" | "error" | null>(null)
const [osArch, setOsArch] = createSignal<string | null>(null)
createEffect(() => {
resolveArchitecture().then((arch) => {
if (arch) setOsArch(arch)
})
})
const updateInfo = createMemo(() => {
const m = meta()
if (!m?.update) return null
return m.update
})
const supportInfo = createMemo(() => meta()?.support ?? null)
const latestVersion = createMemo(() => {
const update = updateInfo()
if (update?.version) return update.version
return supportInfo()?.latestServerVersion ?? null
})
const showDownloadLink = createMemo(() => {
let url: string | null = null
const update = updateInfo()
if (update?.url) url = update.url
else if (supportInfo()?.latestServerUrl) url = supportInfo()!.latestServerUrl ?? null
if (!url) return { url: null, show: false }
if (update?.url) return { url, show: true }
const current = meta()?.serverVersion
const latest = latestVersion()
if (!current || !latest) return { url: null, show: false }
return { url, show: versionNewer(current, latest) !== false }
})
let feedbackTimer: ReturnType<typeof setTimeout> | undefined
createEffect(() => {
if (copyFeedback()) {
clearTimeout(feedbackTimer)
feedbackTimer = setTimeout(() => setCopyFeedback(null), 2500)
}
})
onCleanup(() => clearTimeout(feedbackTimer))
const handleRefresh = async () => {
const fresh = await getServerMeta(true)
mutate(fresh)
}
const osDisplay = createMemo(() => {
const base = detectOs()
const arch = osArch()
return arch ? `${base} (${arch})` : base
})
const handleCopy = async () => {
const report = buildDiagnosticReport(meta() ?? null, osDisplay())
const ok = await copyToClipboard(report)
if (ok) setCopyFeedback("success")
else setCopyFeedback("error")
}
const handleDownload = () => {
const report = buildDiagnosticReport(meta() ?? null, osDisplay())
const ts = new Date().toISOString().replace(/[:.]/g, "-")
downloadTextFile(`codenomad-diagnostics-${ts}.txt`, report)
}
return (
<div class="settings-section-stack">
<div class="settings-card">
<div class="settings-card-header">
<div class="settings-card-heading-with-icon">
<Info class="settings-card-heading-icon" />
<div>
<h3 class="settings-card-title">{t("settings.section.info.title")}</h3>
<p class="settings-card-subtitle">{t("settings.section.info.subtitle")}</p>
</div>
</div>
</div>
<div class="settings-info-grid">
<div class="settings-info-row">
<span class="settings-info-label">{t("settings.info.version.server")}</span>
<span class="settings-info-value">{meta()?.serverVersion ?? "—"}</span>
</div>
<div class="settings-info-row">
<span class="settings-info-label">{t("settings.info.version.ui")}</span>
<span class="settings-info-value">{meta()?.ui?.version ?? "—"}</span>
</div>
<div class="settings-info-row">
<span class="settings-info-label">{t("settings.info.version.uiSource")}</span>
<span class="settings-info-value settings-info-value-muted">
{meta()?.ui?.source ?? "—"}
</span>
</div>
<div class="settings-info-row">
<span class="settings-info-label">{t("settings.info.runtime.type")}</span>
<span class="settings-info-value">{runtimeEnv.host}</span>
</div>
<div class="settings-info-row">
<span class="settings-info-label">{t("settings.info.runtime.platform")}</span>
<span class="settings-info-value">{runtimeEnv.platform}</span>
</div>
<div class="settings-info-row">
<span class="settings-info-label">{t("settings.info.runtime.os")}</span>
<span class="settings-info-value settings-info-value-muted">{osDisplay()}</span>
</div>
<div class="settings-info-row">
<span class="settings-info-label">{t("settings.info.server.url")}</span>
<span class="settings-info-value settings-info-value-muted">
{meta()?.localUrl ?? "—"}
</span>
</div>
<div class="settings-info-row">
<span class="settings-info-label">{t("settings.info.server.root")}</span>
<span class="settings-info-value settings-info-value-muted">
{meta()?.workspaceRoot ?? "—"}
</span>
</div>
</div>
</div>
<div class="settings-card">
<div class="settings-card-header">
<div>
<h3 class="settings-card-title">{t("settings.info.updates.title")}</h3>
<p class="settings-card-subtitle">{t("settings.info.updates.subtitle")}</p>
</div>
</div>
<div class="settings-info-grid">
<div class="settings-info-row">
<span class="settings-info-label">{t("settings.info.version.server")}</span>
<span class="settings-info-value">{meta()?.serverVersion ?? "—"}</span>
</div>
<div class="settings-info-row">
<span class="settings-info-label">{t("settings.info.updates.latest")}</span>
<span class="settings-info-value settings-info-value-muted">
{latestVersion() ?? "—"}
</span>
</div>
</div>
<div class="settings-info-actions">
{showDownloadLink().show && (
<a
href={showDownloadLink().url!}
target="_blank"
rel="noopener noreferrer"
class="settings-pill-button"
>
{t("settings.info.updates.download")}
</a>
)}
<button
type="button"
class="settings-pill-button"
onClick={handleRefresh}
disabled={meta.loading}
>
{t("settings.info.updates.refresh")}
</button>
</div>
</div>
<div class="settings-card">
<div class="settings-card-header">
<div>
<h3 class="settings-card-title">{t("settings.info.diagnostics.title")}</h3>
<p class="settings-card-subtitle">{t("settings.info.diagnostics.subtitle")}</p>
</div>
</div>
<div class="settings-info-actions">
<button
type="button"
class="settings-pill-button"
onClick={handleCopy}
>
{t("settings.info.diagnostics.copy")}
</button>
<button
type="button"
class="settings-pill-button"
onClick={handleDownload}
>
{t("settings.info.diagnostics.download")}
</button>
</div>
{copyFeedback() === "success" && (
<div class="settings-info-toast" role="status" aria-live="polite">
{t("settings.info.diagnostics.copied")}
</div>
)}
{copyFeedback() === "error" && (
<div class="settings-error-message" role="alert">
{t("settings.info.diagnostics.copyFailed")}
</div>
)}
</div>
</div>
)
}

View file

@ -71,6 +71,7 @@ export const settingsMessages = {
"settings.nav.remote": "Remote Access",
"settings.nav.speech": "Speech",
"settings.nav.opencode": "OpenCode",
"settings.nav.info": "Info",
"settings.scope.device": "This device",
"settings.scope.server": "Server setting",
"settings.common.enabled": "Enabled",
@ -239,4 +240,26 @@ export const settingsMessages = {
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
"settings.section.info.title": "About",
"settings.section.info.subtitle": "View version, runtime, and gather diagnostic information.",
"settings.info.version.server": "Server version",
"settings.info.version.ui": "UI version",
"settings.info.version.uiSource": "UI source",
"settings.info.runtime.type": "Runtime",
"settings.info.runtime.platform": "Platform",
"settings.info.runtime.os": "Operating system",
"settings.info.server.url": "Server URL",
"settings.info.server.root": "Workspace root",
"settings.info.updates.title": "Updates",
"settings.info.updates.subtitle": "Check for new releases of CodeNomad.",
"settings.info.updates.latest": "Latest version",
"settings.info.updates.download": "Download update",
"settings.info.updates.refresh": "Refresh",
"settings.info.diagnostics.copyFailed": "Clipboard access denied. Use the download button instead.",
"settings.info.diagnostics.title": "Diagnostics",
"settings.info.diagnostics.subtitle": "Collect system and version info for bug reports.",
"settings.info.diagnostics.copy": "Copy to clipboard",
"settings.info.diagnostics.download": "Download .txt",
"settings.info.diagnostics.copied": "Diagnostic info copied to clipboard.",
} as const

View file

@ -71,6 +71,7 @@ export const settingsMessages = {
"settings.nav.remote": "Remote Access",
"settings.nav.speech": "Speech",
"settings.nav.opencode": "OpenCode",
"settings.nav.info": "Info",
"settings.scope.device": "This device",
"settings.scope.server": "Server setting",
"settings.common.enabled": "Enabled",
@ -238,4 +239,26 @@ export const settingsMessages = {
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
"settings.section.info.title": "About",
"settings.section.info.subtitle": "View version, runtime, and gather diagnostic information.",
"settings.info.version.server": "Server version",
"settings.info.version.ui": "UI version",
"settings.info.version.uiSource": "UI source",
"settings.info.runtime.type": "Runtime",
"settings.info.runtime.platform": "Platform",
"settings.info.runtime.os": "Operating system",
"settings.info.server.url": "Server URL",
"settings.info.server.root": "Workspace root",
"settings.info.updates.title": "Updates",
"settings.info.updates.subtitle": "Check for new releases of CodeNomad.",
"settings.info.updates.latest": "Latest version",
"settings.info.updates.download": "Download update",
"settings.info.updates.refresh": "Refresh",
"settings.info.diagnostics.copyFailed": "Clipboard access denied. Use the download button instead.",
"settings.info.diagnostics.title": "Diagnostics",
"settings.info.diagnostics.subtitle": "Recopila información del sistema y de la versión para informes de errores.",
"settings.info.diagnostics.copy": "Copy to clipboard",
"settings.info.diagnostics.download": "Download .txt",
"settings.info.diagnostics.copied": "Diagnostic info copied to clipboard.",
} as const

View file

@ -71,6 +71,7 @@ export const settingsMessages = {
"settings.nav.remote": "Remote Access",
"settings.nav.speech": "Speech",
"settings.nav.opencode": "OpenCode",
"settings.nav.info": "Info",
"settings.scope.device": "This device",
"settings.scope.server": "Server setting",
"settings.common.enabled": "Enabled",
@ -238,4 +239,26 @@ export const settingsMessages = {
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
"settings.section.info.title": "About",
"settings.section.info.subtitle": "View version, runtime, and gather diagnostic information.",
"settings.info.version.server": "Server version",
"settings.info.version.ui": "UI version",
"settings.info.version.uiSource": "UI source",
"settings.info.runtime.type": "Runtime",
"settings.info.runtime.platform": "Platform",
"settings.info.runtime.os": "Operating system",
"settings.info.server.url": "Server URL",
"settings.info.server.root": "Workspace root",
"settings.info.updates.title": "Updates",
"settings.info.updates.subtitle": "Check for new releases of CodeNomad.",
"settings.info.updates.latest": "Latest version",
"settings.info.updates.download": "Download update",
"settings.info.updates.refresh": "Refresh",
"settings.info.diagnostics.copyFailed": "Clipboard access denied. Use the download button instead.",
"settings.info.diagnostics.title": "Diagnostics",
"settings.info.diagnostics.subtitle": "Collectez les informations système et de version pour les rapports de bug.",
"settings.info.diagnostics.copy": "Copy to clipboard",
"settings.info.diagnostics.download": "Download .txt",
"settings.info.diagnostics.copied": "Diagnostic info copied to clipboard.",
} as const

View file

@ -70,6 +70,7 @@ export const settingsMessages = {
"settings.nav.notifications": "התראות",
"settings.nav.remote": "גישה מרוחקת",
"settings.nav.opencode": "OpenCode",
"settings.nav.info": "Info",
"settings.scope.device": "מכשיר זה",
"settings.scope.server": "הגדרת שרת",
"settings.common.enabled": "מופעל",
@ -237,4 +238,26 @@ export const settingsMessages = {
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
"settings.section.info.title": "About",
"settings.section.info.subtitle": "View version, runtime, and gather diagnostic information.",
"settings.info.version.server": "Server version",
"settings.info.version.ui": "UI version",
"settings.info.version.uiSource": "UI source",
"settings.info.runtime.type": "Runtime",
"settings.info.runtime.platform": "Platform",
"settings.info.runtime.os": "Operating system",
"settings.info.server.url": "Server URL",
"settings.info.server.root": "Workspace root",
"settings.info.updates.title": "Updates",
"settings.info.updates.subtitle": "Check for new releases of CodeNomad.",
"settings.info.updates.latest": "Latest version",
"settings.info.updates.download": "Download update",
"settings.info.updates.refresh": "Refresh",
"settings.info.diagnostics.copyFailed": "Clipboard access denied. Use the download button instead.",
"settings.info.diagnostics.title": "Diagnostics",
"settings.info.diagnostics.subtitle": "אסוף מידע על המערכת והגרסה לדיווחי באגים.",
"settings.info.diagnostics.copy": "Copy to clipboard",
"settings.info.diagnostics.download": "Download .txt",
"settings.info.diagnostics.copied": "Diagnostic info copied to clipboard.",
} as const

View file

@ -71,6 +71,7 @@ export const settingsMessages = {
"settings.nav.remote": "Remote Access",
"settings.nav.speech": "Speech",
"settings.nav.opencode": "OpenCode",
"settings.nav.info": "Info",
"settings.scope.device": "This device",
"settings.scope.server": "Server setting",
"settings.common.enabled": "Enabled",
@ -238,4 +239,26 @@ export const settingsMessages = {
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
"settings.section.info.title": "About",
"settings.section.info.subtitle": "View version, runtime, and gather diagnostic information.",
"settings.info.version.server": "Server version",
"settings.info.version.ui": "UI version",
"settings.info.version.uiSource": "UI source",
"settings.info.runtime.type": "Runtime",
"settings.info.runtime.platform": "Platform",
"settings.info.runtime.os": "Operating system",
"settings.info.server.url": "Server URL",
"settings.info.server.root": "Workspace root",
"settings.info.updates.title": "Updates",
"settings.info.updates.subtitle": "Check for new releases of CodeNomad.",
"settings.info.updates.latest": "Latest version",
"settings.info.updates.download": "Download update",
"settings.info.updates.refresh": "Refresh",
"settings.info.diagnostics.copyFailed": "Clipboard access denied. Use the download button instead.",
"settings.info.diagnostics.title": "Diagnostics",
"settings.info.diagnostics.subtitle": "バグ報告用にシステム情報とバージョン情報を収集します。",
"settings.info.diagnostics.copy": "Copy to clipboard",
"settings.info.diagnostics.download": "Download .txt",
"settings.info.diagnostics.copied": "Diagnostic info copied to clipboard.",
} as const

View file

@ -71,6 +71,7 @@ export const settingsMessages = {
"settings.nav.remote": "Remote Access",
"settings.nav.speech": "Speech",
"settings.nav.opencode": "OpenCode",
"settings.nav.info": "Info",
"settings.scope.device": "This device",
"settings.scope.server": "Server setting",
"settings.common.enabled": "Enabled",
@ -238,4 +239,26 @@ export const settingsMessages = {
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
"settings.section.info.title": "About",
"settings.section.info.subtitle": "View version, runtime, and gather diagnostic information.",
"settings.info.version.server": "Server version",
"settings.info.version.ui": "UI version",
"settings.info.version.uiSource": "UI source",
"settings.info.runtime.type": "Runtime",
"settings.info.runtime.platform": "Platform",
"settings.info.runtime.os": "Operating system",
"settings.info.server.url": "Server URL",
"settings.info.server.root": "Workspace root",
"settings.info.updates.title": "Updates",
"settings.info.updates.subtitle": "Check for new releases of CodeNomad.",
"settings.info.updates.latest": "Latest version",
"settings.info.updates.download": "Download update",
"settings.info.updates.refresh": "Refresh",
"settings.info.diagnostics.copyFailed": "Clipboard access denied. Use the download button instead.",
"settings.info.diagnostics.title": "Diagnostics",
"settings.info.diagnostics.subtitle": "Соберите сведения о системе и версии для отчетов об ошибках.",
"settings.info.diagnostics.copy": "Copy to clipboard",
"settings.info.diagnostics.download": "Download .txt",
"settings.info.diagnostics.copied": "Diagnostic info copied to clipboard.",
} as const

View file

@ -71,6 +71,7 @@ export const settingsMessages = {
"settings.nav.remote": "Remote Access",
"settings.nav.speech": "Speech",
"settings.nav.opencode": "OpenCode",
"settings.nav.info": "Info",
"settings.scope.device": "This device",
"settings.scope.server": "Server setting",
"settings.common.enabled": "Enabled",
@ -238,4 +239,26 @@ export const settingsMessages = {
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
"settings.section.info.title": "About",
"settings.section.info.subtitle": "View version, runtime, and gather diagnostic information.",
"settings.info.version.server": "Server version",
"settings.info.version.ui": "UI version",
"settings.info.version.uiSource": "UI source",
"settings.info.runtime.type": "Runtime",
"settings.info.runtime.platform": "Platform",
"settings.info.runtime.os": "Operating system",
"settings.info.server.url": "Server URL",
"settings.info.server.root": "Workspace root",
"settings.info.updates.title": "Updates",
"settings.info.updates.subtitle": "Check for new releases of CodeNomad.",
"settings.info.updates.latest": "Latest version",
"settings.info.updates.download": "Download update",
"settings.info.updates.refresh": "Refresh",
"settings.info.diagnostics.copyFailed": "Clipboard access denied. Use the download button instead.",
"settings.info.diagnostics.title": "Diagnostics",
"settings.info.diagnostics.subtitle": "收集系统和版本信息,用于错误报告。",
"settings.info.diagnostics.copy": "Copy to clipboard",
"settings.info.diagnostics.download": "Download .txt",
"settings.info.diagnostics.copied": "Diagnostic info copied to clipboard.",
} as const

View file

@ -1,6 +1,6 @@
import { createSignal } from "solid-js"
export type SettingsSectionId = "appearance" | "notifications" | "remote" | "speech" | "opencode" | "sidecars"
export type SettingsSectionId = "appearance" | "notifications" | "remote" | "speech" | "opencode" | "sidecars" | "info"
const [settingsOpen, setSettingsOpen] = createSignal(false)
const [activeSettingsSection, setActiveSettingsSection] = createSignal<SettingsSectionId>("appearance")

View file

@ -0,0 +1,57 @@
.settings-info-grid {
display: flex;
flex-direction: column;
gap: 0;
}
.settings-info-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.72rem 0;
border-top: 1px solid color-mix(in oklab, var(--border-base) 78%, transparent);
}
.settings-info-row:first-child {
border-top: none;
padding-top: 0;
}
.settings-info-label {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--text-secondary);
flex-shrink: 0;
}
.settings-info-value {
font-size: var(--font-size-sm);
color: var(--text-primary);
text-align: end;
font-family: var(--font-mono, monospace);
word-break: break-all;
}
.settings-info-value-muted {
color: var(--text-muted);
}
.settings-info-actions {
display: flex;
align-items: center;
gap: 0.625rem;
margin-top: 0.5rem;
flex-wrap: wrap;
}
.settings-info-toast {
margin-top: 0.75rem;
padding: 0.625rem 0.875rem;
border: 1px solid var(--border-base);
background: color-mix(in oklab, var(--accent-primary) 10%, var(--surface-base));
border-radius: 0;
color: var(--text-primary);
font-size: var(--font-size-sm);
}

View file

@ -9,3 +9,4 @@
@import "./components/remote-access.css";
@import "./components/permission-notification.css";
@import "./components/settings-screen.css";
@import "./components/settings-info.css";