/** * Serializable configuration and defaults for the sidebar host half. Loader * schema validation normally fills defaults; {@link resolveSidebarConfig} * applies the same defaults for direct callers that bypass the Loader. * @module dsh-better-sidebar/config */ import z from 'schemastery' import { TERMINAL_FONT_SIZE_DEFAULT, TERMINAL_FONT_SIZE_MAX, TERMINAL_FONT_SIZE_MIN, TITLE_BAR_STRIP_DEFAULT, TITLE_BAR_STRIP_MAX, TITLE_BAR_STRIP_MIN, type SidebarPrefs, } from './prefs-shared.ts' export { SIDEBAR_PREFS_DEFAULTS, SIDEBAR_PREFS_NS, TERMINAL_FONT_SIZE_DEFAULT, TERMINAL_FONT_SIZE_MAX, TERMINAL_FONT_SIZE_MIN, TITLE_BAR_STRIP_DEFAULT, TITLE_BAR_STRIP_MAX, TITLE_BAR_STRIP_MIN, type SidebarPrefs, } from './prefs-shared.ts' /** Tunable sidebar host limits (every field optional; defaults fill in). */ export interface SidebarConfig { /** Read cap of one text file (bytes); larger files return truncated. */ readLimit?: number /** Media route cap (bytes); larger binaries are refused. */ mediaLimit?: number /** Upload route cap (bytes); larger files are refused without touching disk. */ uploadLimit?: number /** Explorer row bound of one level. */ listLimit?: number /** Terminals per session. */ terminalsPerSession?: number /** How long a disconnected terminal process survives awaiting a reconnect. */ reconnectGraceMs?: number /** * Terminal shell (absolute path or bare executable name) for BOTH the UI * terminal tabs and the model-facing `terminal_*` tools. Empty = auto: * POSIX follows `$SHELL` then the account login shell; Windows follows * `DSH_SIDEBAR_SHELL`, then probes for `pwsh.exe`, then falls back to the * inbox `powershell.exe` (5.1). Set it from `cordis.patch.yml` / profile * plugin config, e.g. `config: { shell: /bin/zsh }`. */ shell?: string /** * Optional arguments passed to the shell executable. When non-empty these * REPLACE the automatic platform defaults (POSIX `-l` / Windows none), so * the deployment has full control over how the shell starts. When omitted * the existing default behavior is kept. */ shellArgs?: string[] } /** Schemastery schema for the plugin configuration. */ export const Config: z = z.object({ readLimit: z.number().step(1).min(1).default(512 * 1024), mediaLimit: z.number().step(1).min(1).default(20 * 1024 * 1024), uploadLimit: z.number().step(1).min(1).default(128 * 1024 * 1024), listLimit: z.number().step(1).min(1).default(1000), terminalsPerSession: z.number().step(1).min(1).default(3), reconnectGraceMs: z.number().step(1).min(0).default(30_000), shell: z.string().default(''), shellArgs: z.array(z.string()).default([]), }) /** Fully defaulted sidebar host settings. */ export interface ResolvedSidebarConfig { readLimit: number mediaLimit: number uploadLimit: number listLimit: number terminalsPerSession: number reconnectGraceMs: number /** The configured terminal shell; empty means the host auto-resolves it. */ shell: string /** Explicit shell arguments; empty means use the platform defaults. */ shellArgs: string[] } /** * Apply direct-call defaults after Loader schema validation has normally run. * * @param config - Deployment-provided sidebar host settings. * @returns Complete settings consumed by the host half. */ export function resolveSidebarConfig(config: SidebarConfig | undefined): ResolvedSidebarConfig { return { readLimit: config?.readLimit ?? 512 * 1024, mediaLimit: config?.mediaLimit ?? 20 * 1024 * 1024, uploadLimit: config?.uploadLimit ?? 128 * 1024 * 1024, listLimit: config?.listLimit ?? 1000, terminalsPerSession: config?.terminalsPerSession ?? 3, reconnectGraceMs: config?.reconnectGraceMs ?? 30_000, shell: config?.shell?.trim() ?? '', shellArgs: config?.shellArgs ?? [], } } // ── User-facing "Side card" preferences ───────────────────────────────────── /** Schemastery schema for the user-facing preferences (validated by the settings service). */ export const PrefsSchema: z = z.object({ autoOpenSubagent: z.boolean().default(true), autoOpenJobs: z.boolean().default(true), agentTerminalTools: z.boolean().default(false), agentOpenTools: z.boolean().default(false), bottomPanelAutoTerminal: z.boolean().default(true), terminalFontFamily: z.string().default(''), terminalFontSize: z.number().step(1).min(TERMINAL_FONT_SIZE_MIN).max(TERMINAL_FONT_SIZE_MAX).default(TERMINAL_FONT_SIZE_DEFAULT), editorExplorer: z.boolean().default(false), workspaceFence: z.boolean().default(true), terminalShell: z.string().default(''), terminalShellArgs: z.string().default(''), titleBarScheme: z.union([z.const('auto'), z.const('web'), z.const('preset'), z.const('custom')]), titleBarPresetId: z.string(), customCss: z.string(), titleBarCompat: z.boolean().default(false), titleBarStripPx: z.number().step(1).min(TITLE_BAR_STRIP_MIN).max(TITLE_BAR_STRIP_MAX).default(TITLE_BAR_STRIP_DEFAULT), htmlViewerNoSandbox: z.boolean().default(false), htmlViewerDefaultUnsafe: z.boolean().default(false), browserNoSandbox: z.boolean().default(false), browserInterceptLinks: z.boolean().default(true), browserInterceptHttp: z.boolean().default(true), browserInterceptHttps: z.boolean().default(false), browserAllowedLoopback: z.string().default(''), // Per-feature enable switches are OPEN maps (any tab/viewer id, built-in or // external): an absent key means enabled, so old documents resolve to {} // (everything on) with no migration. Non-boolean values fail validation. tabsEnabled: z.dict(z.boolean()).default({}), viewersEnabled: z.dict(z.boolean()).default({}), // Plugin-owned settings blobs (v0.12.0+) are an OPEN nested map: any // descriptor id may carry any JSON-serializable values. This is the // "settings seam" opening — without it the seam would drop third-party // keys as unknown schema fields. pluginSettings: z.dict(z.dict(z.any())).default({}), })