zidane 9.0.1 → 9.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{auto-update-restart-Di-QOmbc.js → auto-update-restart-D30Th3Or.js} +2 -2
- package/dist/{auto-update-restart-Di-QOmbc.js.map → auto-update-restart-D30Th3Or.js.map} +1 -1
- package/dist/chat.js +3 -3
- package/dist/daemon-cli.js +2 -2
- package/dist/{engine-gnOWE8_z.js → engine-D9gwgitb.js} +2 -2
- package/dist/{engine-gnOWE8_z.js.map → engine-D9gwgitb.js.map} +1 -1
- package/dist/{executor-D8qIXJK9.js → executor-QgXGkX7V.js} +2 -2
- package/dist/{executor-D8qIXJK9.js.map → executor-QgXGkX7V.js.map} +1 -1
- package/dist/{extensions-BlizVmBJ.js → extensions-D2Ae4by5.js} +2 -2
- package/dist/{extensions-BlizVmBJ.js.map → extensions-D2Ae4by5.js.map} +1 -1
- package/dist/extensions.js +1 -1
- package/dist/{host-signals-Bod6f9lW.js → host-signals-DEYsDUTY.js} +5 -5
- package/dist/{host-signals-Bod6f9lW.js.map → host-signals-DEYsDUTY.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/ipc/remote.js +3 -3
- package/dist/{ipc-CxU525LK.js → ipc-mJcMKWb-.js} +4 -4
- package/dist/{ipc-CxU525LK.js.map → ipc-mJcMKWb-.js.map} +1 -1
- package/dist/ipc.js +4 -4
- package/dist/{links-vCRfuwcf.js → links-DZDovXfh.js} +6 -6
- package/dist/{links-vCRfuwcf.js.map → links-DZDovXfh.js.map} +1 -1
- package/dist/{memory-D_IXaDph.js → memory-CAi-4CJs.js} +7 -7
- package/dist/{memory-D_IXaDph.js.map → memory-CAi-4CJs.js.map} +1 -1
- package/dist/{path-display-DKLe4XMx.js → path-display-ngOL00wI.js} +3 -3
- package/dist/{path-display-DKLe4XMx.js.map → path-display-ngOL00wI.js.map} +1 -1
- package/dist/{remote-hosts-CrWY_6M4.js → remote-hosts-z6lSKQIV.js} +4 -4
- package/dist/{remote-hosts-CrWY_6M4.js.map → remote-hosts-z6lSKQIV.js.map} +1 -1
- package/dist/{settings-keys-D17NBeft.js → settings-keys-EtuJbE_K.js} +2 -2
- package/dist/{settings-keys-D17NBeft.js.map → settings-keys-EtuJbE_K.js.map} +1 -1
- package/dist/tui.js +8 -8
- package/dist/{version-4D2cCm14.js → version-B57fToQH.js} +2 -2
- package/dist/{version-4D2cCm14.js.map → version-B57fToQH.js.map} +1 -1
- package/dist/{ws-B5dFRL2T.js → ws-VPYC0cXG.js} +3 -3
- package/dist/{ws-B5dFRL2T.js.map → ws-VPYC0cXG.js.map} +1 -1
- package/dist/{ws-client-KorHNu6H.js → ws-client-BDfucozp.js} +2 -2
- package/dist/{ws-client-KorHNu6H.js.map → ws-client-BDfucozp.js.map} +1 -1
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as artifactKindLabel, u as renderArtifactRow } from "./builtin-artifact-kinds-uu16KcYX.js";
|
|
2
|
-
import { L as formatBindingForDisplay } from "./extensions-
|
|
3
|
-
import { _ as performSelfUpdate, b as updateChecksOptedOut, c as findInstalledCompanion, d as compareSemver, h as parseSemver, l as checkForUpdate, o as watchInstalledVersion, p as detectPackageManager, s as writeUpdateAppliedMarker, u as checkTarballAvailable, v as reinstallHint } from "./auto-update-restart-
|
|
2
|
+
import { L as formatBindingForDisplay } from "./extensions-D2Ae4by5.js";
|
|
3
|
+
import { _ as performSelfUpdate, b as updateChecksOptedOut, c as findInstalledCompanion, d as compareSemver, h as parseSemver, l as checkForUpdate, o as watchInstalledVersion, p as detectPackageManager, s as writeUpdateAppliedMarker, u as checkTarballAvailable, v as reinstallHint } from "./auto-update-restart-D30Th3Or.js";
|
|
4
4
|
import { isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { linkSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { hostname } from "node:os";
|
|
@@ -1014,4 +1014,4 @@ function formatPathForCwd(projectRelativePath, projectRoot, cwd) {
|
|
|
1014
1014
|
//#endregion
|
|
1015
1015
|
export { updateLockPath as C, readUpdateLock as S, summarizeArtifactCounts as T, UPDATE_LOCK_FILE as _, buildUpdateHint as a, describeUpdateLockHolder as b, useUpdateCheck as c, clearFailedUpdate as d, isAutoInstallCandidate as f, upgradeInstalledPackages as g, writeFailedUpdate as h, buildRestartHint as i, UPDATE_FAILED_FILE as l, upgradeError as m, buildHints as n, useAutoInstallUpdate as o, readFailedUpdate as p, shortChord as r, useRestartRequired as s, formatPathForCwd as t, autoInstallUpdate as u, UPDATE_LOCK_STALE_MS as v, groupArtifacts as w, isUpdateLockStale as x, acquireUpdateLock as y };
|
|
1016
1016
|
|
|
1017
|
-
//# sourceMappingURL=path-display-
|
|
1017
|
+
//# sourceMappingURL=path-display-ngOL00wI.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-display-DKLe4XMx.js","names":["osHostname"],"sources":["../src/chat/artifacts-view.ts","../src/chat/auto-update-lock.ts","../src/chat/auto-update-service.ts","../src/chat/auto-update-auto-install.ts","../src/chat/auto-update-hook.ts","../src/chat/footer-hints.ts","../src/chat/path-display.ts"],"sourcesContent":["/**\n * Renderer-agnostic shaping of session artifacts for display — grouping\n * and row presentation. Shared by the TUI rail + modal and the GUI rail, so\n * every surface agrees on what an artifact looks like.\n */\n\nimport type { ExtensionArtifactRow, ExtensionRegistry } from '../extensions'\nimport type { SessionArtifact } from '../session'\nimport { artifactKindLabel, renderArtifactRow } from '../extensions/builtin-artifact-kinds'\n\n/** Whether artifacts are listed for the current session or the whole project. */\nexport type ArtifactScope = 'session' | 'project'\n\n/** One artifact paired with its rendered row. */\nexport interface ArtifactViewRow {\n /** The session artifact. */\n artifact: SessionArtifact\n /** Rendered row for the artifact. */\n row: ExtensionArtifactRow\n}\n\n/** Artifacts of one kind, ready to display. */\nexport interface ArtifactViewGroup {\n /** Artifact kind id. */\n kind: string\n /** Human kind label (`Pull request`), raw kind when unregistered. */\n label: string\n /** Rows of this kind, in the store's order. */\n rows: ArtifactViewRow[]\n}\n\ntype KindRegistry = Pick<ExtensionRegistry, 'artifactKinds'> | undefined\n\n/** Display order for the built-in kinds; anything else follows alphabetically. */\nconst KIND_ORDER = ['pull_request', 'branch', 'commit', 'file']\n\n/**\n * Group by kind (built-ins first in a fixed order, then extension kinds\n * alphabetically); rows inside a group keep the store's newest-first order.\n *\n * @param artifacts - Session artifacts to group.\n * @param registry - Extension registry that supplies kind labels and row renderers.\n */\nexport function groupArtifacts(artifacts: readonly SessionArtifact[], registry?: KindRegistry): ArtifactViewGroup[] {\n const byKind = new Map<string, ArtifactViewRow[]>()\n for (const artifact of artifacts) {\n const list = byKind.get(artifact.kind) ?? []\n list.push({ artifact, row: renderArtifactRow(artifact, registry) })\n byKind.set(artifact.kind, list)\n }\n const kinds = [...byKind.keys()].sort((a, b) => {\n const ai = KIND_ORDER.indexOf(a)\n const bi = KIND_ORDER.indexOf(b)\n if (ai !== -1 || bi !== -1)\n return (ai === -1 ? KIND_ORDER.length : ai) - (bi === -1 ? KIND_ORDER.length : bi)\n return a.localeCompare(b)\n })\n return kinds.map(kind => ({ kind, label: artifactKindLabel(kind, registry), rows: byKind.get(kind)! }))\n}\n\n/**\n * One count per kind, a badge each: `['3 pull requests', '2 files']`; empty\n * for none.\n *\n * @param artifacts - Session artifacts to count.\n * @param registry - Extension registry that supplies kind labels.\n */\nexport function summarizeArtifactCounts(artifacts: readonly SessionArtifact[], registry?: KindRegistry): string[] {\n return groupArtifacts(artifacts, registry)\n .map(g => `${g.rows.length} ${pluralizeKind(g.label, g.rows.length)}`)\n}\n\nfunction pluralizeKind(label: string, count: number): string {\n const lower = label.toLowerCase()\n if (count === 1)\n return lower\n return /(?:s|x|ch|sh)$/.test(lower) ? `${lower}es` : `${lower}s`\n}\n","// ---------------------------------------------------------------------------\n// One install at a time across the GUI, the TUI and `zidane upgrade`.\n//\n// `<cacheDir>/update.lock` holds who runs the install. It is created with an\n// exclusive create (`wx`), so two processes never both think they own it. A\n// lock goes stale when its process is gone on this host, when a live holder\n// on this host is older than {@link UPDATE_LOCK_LIVE_STALE_MS}, or when a\n// holder whose pid means nothing here is older than\n// {@link UPDATE_LOCK_STALE_MS}: a crashed install must not block every later\n// one. A stale lock is replaced.\n// ---------------------------------------------------------------------------\n\nimport { linkSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { hostname as osHostname } from 'node:os'\nimport { join } from 'node:path'\n\n/** File name of the install lock inside the cache directory. */\nexport const UPDATE_LOCK_FILE = 'update.lock'\n\n/** A lock older than this is stale when its holder can't be probed (another host, or no pid). */\nexport const UPDATE_LOCK_STALE_MS = 15 * 60_000\n\n/**\n * A lock held by a live process on this host goes stale only past this: a slow\n * install keeps its lock, and a recycled pid can't hold one forever.\n */\nexport const UPDATE_LOCK_LIVE_STALE_MS = 2 * 60 * 60_000\n\n/** What the lock file records about the install that holds it. */\nexport interface UpdateLockHolder {\n /** Process id of the install. */\n pid: number\n /** Host the process runs on. A pid only means something on its own host. */\n hostname: string\n /** ms since epoch. */\n startedAt: number\n /** Who runs the install, e.g. `'zidane upgrade'`, `'desktop app'`, `'terminal app'`. */\n by: string\n}\n\n/** Outcome of {@link acquireUpdateLock}. */\nexport type UpdateLockResult\n = | { acquired: true, holder: UpdateLockHolder, release: () => void }\n | { acquired: false, holder: UpdateLockHolder }\n\n/** Injected clocks and probes, for tests. */\nexport interface UpdateLockOptions {\n /** Defaults to `Date.now`. */\n now?: () => number\n /** Whether `pid` runs on this host. Defaults to `process.kill(pid, 0)`. */\n isAlive?: (pid: number) => boolean\n /** Defaults to `os.hostname()`. */\n hostname?: string\n /** Defaults to `process.pid`. */\n pid?: number\n}\n\n/**\n * Returns the path of the install lock file.\n *\n * @param cacheDir - Directory that holds the lock.\n */\nexport function updateLockPath(cacheDir: string): string {\n return join(cacheDir, UPDATE_LOCK_FILE)\n}\n\n/**\n * Whether `pid` is a live process on this host. `EPERM` means it exists but\n * belongs to another user, so it counts as alive.\n *\n * @param pid - Process id to probe.\n */\nexport function isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0)\n return true\n }\n catch (err) {\n return (err as NodeJS.ErrnoException).code !== 'ESRCH'\n }\n}\n\n/**\n * Reads the lock file. Returns `null` when there is none. A file that does\n * not parse (another process is still writing it, or garbage) yields a holder\n * with `pid: 0`, dated by the file's mtime, so only age makes it stale.\n *\n * @param cacheDir - Directory that holds the lock.\n */\nexport function readUpdateLock(cacheDir: string): UpdateLockHolder | null {\n return readHolder(updateLockPath(cacheDir))\n}\n\n/**\n * Whether a lock no longer protects a running install: its process is gone\n * on this host, a live one is older than {@link UPDATE_LOCK_LIVE_STALE_MS},\n * or a holder that can't be probed is older than {@link UPDATE_LOCK_STALE_MS}.\n *\n * @param holder - The recorded holder.\n * @param options - Injected clock, liveness probe and host name.\n */\nexport function isUpdateLockStale(holder: UpdateLockHolder, options: UpdateLockOptions = {}): boolean {\n const age = (options.now ?? Date.now)() - holder.startedAt\n const host = options.hostname ?? osHostname()\n if (holder.pid > 0 && holder.hostname === host)\n return age > UPDATE_LOCK_LIVE_STALE_MS || !(options.isAlive ?? isProcessAlive)(holder.pid)\n return age > UPDATE_LOCK_STALE_MS\n}\n\n/**\n * Takes the install lock, replacing a stale one. Never throws: a cache dir\n * that cannot be written yields `acquired: true` with a no-op release, since\n * a lock that cannot be created protects nothing and must not block the\n * install.\n *\n * @param cacheDir - Directory that holds the lock. Created when missing.\n * @param by - Who runs the install, shown to anyone who finds the lock held.\n * @param options - Injected clock, liveness probe, host name and pid.\n */\nexport function acquireUpdateLock(cacheDir: string, by: string, options: UpdateLockOptions = {}): UpdateLockResult {\n const path = updateLockPath(cacheDir)\n const mine: UpdateLockHolder = {\n pid: options.pid ?? process.pid,\n hostname: options.hostname ?? osHostname(),\n startedAt: (options.now ?? Date.now)(),\n by,\n }\n const acquired = (): UpdateLockResult => ({ acquired: true, holder: mine, release: () => releaseIfMine(path, mine) })\n try {\n mkdirSync(cacheDir, { recursive: true })\n }\n catch {\n return { acquired: true, holder: mine, release: () => {} }\n }\n // Two rounds: the second runs after a stale lock was moved away.\n for (let attempt = 0; attempt < 2; attempt++) {\n const created = create(path, mine)\n if (created === 'ok')\n return acquired()\n if (created === 'error')\n return { acquired: true, holder: mine, release: () => {} }\n const held = readHolder(path)\n if (!held)\n continue // Released between our create and our read.\n if (!isUpdateLockStale(held, options))\n return { acquired: false, holder: held }\n const taken = takeStale(path, mine, options)\n if (taken)\n return { acquired: false, holder: taken }\n }\n const created = create(path, mine)\n if (created === 'exists') {\n const held = readHolder(path)\n if (held)\n return { acquired: false, holder: held }\n }\n return created === 'ok' ? acquired() : { acquired: true, holder: mine, release: () => {} }\n}\n\n/**\n * One line for people: `the desktop app (pid 4242, started 3 min ago)`.\n *\n * @param holder - The recorded holder.\n * @param now - Current time in ms. Defaults to `Date.now()`.\n */\nexport function describeUpdateLockHolder(holder: UpdateLockHolder, now: number = Date.now()): string {\n const minutes = Math.max(0, Math.round((now - holder.startedAt) / 60_000))\n const age = minutes < 1 ? 'just now' : `${minutes} min ago`\n const pid = holder.pid > 0 ? `pid ${holder.pid}, ` : ''\n return `${holder.by} (${pid}started ${age})`\n}\n\nfunction create(path: string, holder: UpdateLockHolder): 'ok' | 'exists' | 'error' {\n try {\n writeFileSync(path, `${JSON.stringify(holder)}\\n`, { flag: 'wx' })\n return 'ok'\n }\n catch (err) {\n return (err as NodeJS.ErrnoException).code === 'EEXIST' ? 'exists' : 'error'\n }\n}\n\n/**\n * Move a stale lock aside so only one process replaces it. Returns the holder\n * when the file we moved turned out to be a fresh lock (another process\n * replaced the stale one first): it goes back, and the lock stays held.\n */\nfunction takeStale(path: string, mine: UpdateLockHolder, options: UpdateLockOptions): UpdateLockHolder | null {\n const aside = `${path}.${mine.pid}.${mine.startedAt}.stale`\n try {\n renameSync(path, aside)\n }\n catch {\n return null // Gone already; the next create decides.\n }\n const moved = readHolder(aside)\n if (moved && !isUpdateLockStale(moved, options)) {\n try {\n linkSync(aside, path)\n }\n catch {\n // Someone created a lock meanwhile; theirs stands.\n }\n removeQuietly(aside)\n return readHolder(path) ?? moved\n }\n removeQuietly(aside)\n return null\n}\n\nfunction releaseIfMine(path: string, mine: UpdateLockHolder): void {\n const held = readHolder(path)\n // Never remove a lock another process took after ours went stale.\n if (held && held.pid === mine.pid && held.startedAt === mine.startedAt && held.hostname === mine.hostname)\n removeQuietly(path)\n}\n\nfunction readHolder(path: string): UpdateLockHolder | null {\n let raw: string\n try {\n raw = readFileSync(path, 'utf8')\n }\n catch {\n return null\n }\n try {\n const parsed = JSON.parse(raw) as Partial<UpdateLockHolder>\n if (typeof parsed.pid === 'number' && typeof parsed.hostname === 'string' && typeof parsed.startedAt === 'number' && typeof parsed.by === 'string')\n return { pid: parsed.pid, hostname: parsed.hostname, startedAt: parsed.startedAt, by: parsed.by }\n }\n catch {\n // Half-written or garbage: dated by the file itself below.\n }\n try {\n return { pid: 0, hostname: '', startedAt: statSync(path).mtimeMs, by: 'another install' }\n }\n catch {\n return null\n }\n}\n\nfunction removeQuietly(path: string): void {\n try {\n unlinkSync(path)\n }\n catch {\n // Already gone.\n }\n}\n","// ---------------------------------------------------------------------------\n// Structured, host-agnostic upgrade of every INSTALLED zidane package.\n//\n// `runUpdateCommand` is the terminal-facing ledger for `zidane upgrade`; it\n// knows the TUI is the primary and detects that install's package manager\n// from `process.execPath`. A GUI cannot reuse it — its `execPath` is\n// Electron's — and wants results, not text. This module is the shared\n// engine both can build on: every target is located by its bin on PATH\n// (`findInstalledCompanion`), checked against the registry, installed through\n// the package manager that owns THAT bin, and verified on disk afterwards\n// (a PM exiting 0 is not proof; an optional platform binary that 404s is\n// silently skipped).\n//\n// On success the applied marker is written so running surfaces can offer a\n// restart — see `auto-update-restart.ts`.\n// ---------------------------------------------------------------------------\n\nimport type { PackageManagerCommand, UpdateStatus } from './auto-update'\nimport type { CompanionSpec, InstalledCompanion } from './auto-update-companion'\nimport { Writable } from 'node:stream'\nimport {\n checkForUpdate,\n checkTarballAvailable,\n compareSemver,\n detectPackageManager,\n performSelfUpdate,\n reinstallHint,\n} from './auto-update'\nimport { findInstalledCompanion } from './auto-update-companion'\nimport { writeUpdateAppliedMarker } from './auto-update-restart'\n\n/** Outcome of upgrading one package. */\nexport interface UpgradePackageResult {\n /** npm package name. */\n packageName: string\n /** Version on disk before the run; `null` when not installed. */\n from: string | null\n /** Version the registry advertises; `null` when the check failed. */\n to: string | null\n /**\n * - `'updated'` — installed and verified on disk.\n * - `'up-to-date'` — nothing to do.\n * - `'not-installed'` — bin not on PATH; skipped silently.\n * - `'unavailable'` — published but the tarball is not downloadable yet.\n * - `'failed'` — registry error, PM failure, or the install did not land; see `error`.\n */\n status: 'updated' | 'up-to-date' | 'not-installed' | 'unavailable' | 'failed'\n /** The PM command that ran (or would run). */\n command?: string[]\n /** Why the upgrade did not complete. Set for `failed` and `unavailable`. */\n error?: string\n}\n\n/** Outcome of {@link upgradeInstalledPackages}. */\nexport interface UpgradeReport {\n /** One result per target package, in order. */\n results: UpgradePackageResult[]\n /** No `failed` / `unavailable` entries. */\n ok: boolean\n /** At least one package was actually installed — a restart is now pending. */\n installedAny: boolean\n}\n\n/** Progress event emitted while packages are upgraded: a version check, an install start, a line of package manager output, or a finished package. */\nexport type UpgradeProgressEvent\n = | { kind: 'check', packageName: string }\n | { kind: 'install', packageName: string, from: string, to: string, command: string[] }\n | { kind: 'log', packageName: string, stream: 'stdout' | 'stderr', line: string }\n | { kind: 'done', result: UpgradePackageResult }\n\n/** Options for {@link upgradeInstalledPackages}. */\nexport interface UpgradeInstalledPackagesOptions {\n /** Packages to keep in step, e.g. `[{ packageName: 'zidane-gui' }, { packageName: 'zidane-tui', binName: 'zidane' }]`. */\n targets: readonly CompanionSpec[]\n /** dist-tag to install. Defaults to `'latest'`. */\n channel?: string\n /** Registry base URL. Defaults to `https://registry.npmjs.org`. */\n registry?: string\n /** Where the registry cache and the applied marker live. */\n cacheDir?: string\n /** Install even when the registry check failed or reports up-to-date. */\n force?: boolean\n /** Abort signal. Targets not yet started are skipped once it aborts. */\n signal?: AbortSignal\n /** Called with check, install, log and done events as the upgrade runs. */\n onProgress?: (event: UpgradeProgressEvent) => void\n /** Injected for tests. */\n findInstalled?: typeof findInstalledCompanion\n /** Injected for tests. Defaults to {@link performSelfUpdate}. */\n performSelfUpdate?: typeof performSelfUpdate\n /** Injected for tests. Defaults to {@link checkTarballAvailable}. */\n checkTarballAvailable?: typeof checkTarballAvailable\n /** Injected for tests. Defaults to {@link checkForUpdate}. */\n checkForUpdate?: typeof checkForUpdate\n /** Injected for tests. Defaults to {@link detectPackageManager}. */\n detectPackageManager?: typeof detectPackageManager\n /** Injected for tests. Defaults to {@link writeUpdateAppliedMarker}. */\n writeMarker?: typeof writeUpdateAppliedMarker\n /** Injected for tests. Defaults to `Date.now`. */\n now?: () => number\n}\n\n/**\n * Upgrades each installed target package through its own package manager.\n * Skips targets whose bin is not on PATH, checks the registry for each one, reports `unavailable` instead of installing when the tarball is not downloadable yet, runs the install, and verifies the new version on disk. Writes the applied marker when something was installed and `cacheDir` is set.\n *\n * @param options - Target packages, registry settings and progress callback.\n * @example\n * ```ts\n * import { upgradeInstalledPackages } from 'zidane/chat'\n *\n * const report = await upgradeInstalledPackages({\n * targets: [{ packageName: 'zidane-tui', binName: 'zidane' }],\n * onProgress: event => console.log(event.kind),\n * })\n * if (!report.ok)\n * console.error(report.results.filter(r => r.status === 'failed'))\n * ```\n */\nexport async function upgradeInstalledPackages(options: UpgradeInstalledPackagesOptions): Promise<UpgradeReport> {\n const channel = options.channel ?? 'latest'\n const find = options.findInstalled ?? findInstalledCompanion\n const check = options.checkForUpdate ?? checkForUpdate\n const available = options.checkTarballAvailable ?? checkTarballAvailable\n const install = options.performSelfUpdate ?? performSelfUpdate\n const detect = options.detectPackageManager ?? detectPackageManager\n const progress = options.onProgress ?? (() => {})\n\n const results: UpgradePackageResult[] = []\n for (const spec of options.targets) {\n if (options.signal?.aborted)\n break\n const installed = find(spec)\n if (!installed) {\n results.push({ packageName: spec.packageName, from: null, to: null, status: 'not-installed' })\n continue\n }\n progress({ kind: 'check', packageName: spec.packageName })\n const status = await check({\n packageName: installed.packageName,\n currentVersion: installed.version,\n channel,\n registry: options.registry,\n cacheDir: options.cacheDir,\n force: true,\n timeoutMs: 8000,\n signal: options.signal,\n })\n const result = await upgradeOne(installed, status)\n progress({ kind: 'done', result })\n results.push(result)\n }\n\n const installedAny = results.some(r => r.status === 'updated')\n if (installedAny && options.cacheDir) {\n const writeMarker = options.writeMarker ?? writeUpdateAppliedMarker\n writeMarker(options.cacheDir, {\n at: (options.now ?? Date.now)(),\n packages: results\n .filter(r => r.status === 'updated' && r.to)\n .map(r => ({ packageName: r.packageName, version: r.to! })),\n })\n }\n return {\n results,\n ok: results.every(r => r.status !== 'failed' && r.status !== 'unavailable'),\n installedAny,\n }\n\n async function upgradeOne(installed: InstalledCompanion, status: UpdateStatus): Promise<UpgradePackageResult> {\n const base = { packageName: installed.packageName, from: installed.version, to: status.latest }\n if (status.source === 'error' && !options.force)\n return { ...base, status: 'failed', error: `registry check failed: ${status.error ?? 'unknown error'}` }\n const outdated = status.latest !== null && compareSemver(status.latest, installed.version) > 0\n if (!outdated && !options.force)\n return { ...base, status: 'up-to-date' }\n if (status.latest && !options.force) {\n const availability = await available({ packageName: installed.packageName, version: status.latest, registry: options.registry })\n if (availability === 'missing')\n return { ...base, status: 'unavailable', error: `${installed.packageName} ${status.latest} is published but not downloadable yet. Try again in a few minutes.` }\n }\n // The PM that owns THIS bin — npm/pnpm/yarn/bun/Volta differ per install.\n const command: PackageManagerCommand = detect({ packageName: installed.packageName, channel, binaryPath: installed.binaryPath })\n progress({ kind: 'install', packageName: installed.packageName, from: installed.version, to: status.latest ?? channel, command: command.argv })\n\n const outcome = await install({\n packageName: installed.packageName,\n channel,\n packageManager: command.id,\n signal: options.signal,\n stdout: lineSink(installed.packageName, 'stdout'),\n stderr: lineSink(installed.packageName, 'stderr'),\n // Quiet PM output: the UI shows one live line, not a progress bar dump.\n env: { npm_config_loglevel: 'error', npm_config_progress: 'false' },\n })\n if (outcome.spawnError)\n return { ...base, status: 'failed', command: command.argv, error: `could not run ${command.argv[0]}: ${outcome.spawnError}` }\n if (outcome.exitCode !== 0)\n return { ...base, status: 'failed', command: command.argv, error: `${command.argv[0]} exited with ${outcome.exitCode ?? 'unknown'}` }\n\n // Verify: what is on disk now vs. what we asked for.\n const after = find(installed)\n if (status.latest && after && compareSemver(after.version, status.latest) < 0) {\n return {\n ...base,\n status: 'failed',\n command: command.argv,\n error: `install finished but ${after.version} is on disk, expected ${status.latest}. Reinstall: ${reinstallHint(command, installed.packageName, channel)}`,\n }\n }\n return { ...base, to: after?.version ?? status.latest, status: 'updated', command: command.argv }\n }\n\n /** Split a PM's byte stream into trimmed lines for the progress sink. */\n function lineSink(packageName: string, stream: 'stdout' | 'stderr'): NodeJS.WritableStream {\n let buffer = ''\n const flush = (final: boolean) => {\n const parts = buffer.split(/\\r?\\n/)\n buffer = final ? '' : parts.pop() ?? ''\n for (const part of parts) {\n const line = part.trim()\n if (line)\n progress({ kind: 'log', packageName, stream, line })\n }\n }\n return new Writable({\n write(chunk: Uint8Array | string, _enc, cb) {\n buffer += chunk.toString()\n flush(false)\n cb()\n },\n final(cb) {\n flush(true)\n cb()\n },\n })\n }\n}\n","// ---------------------------------------------------------------------------\n// Automatic installs (`Settings.autoInstallUpdates`).\n//\n// When a background check finds a newer stable release, the host calls\n// `autoInstallUpdate`. It runs the same engine as the GUI's Install button\n// and `zidane upgrade` (`upgradeInstalledPackages`), under the shared install\n// lock (`auto-update-lock.ts`), and never restarts anything: the applied\n// marker it writes wakes the restart chip.\n//\n// A failed automatic install records its version in\n// `<cacheDir>/update-failed.json`. That version is never retried on its own;\n// a newer release, or a manual install (which clears the record), is.\n// ---------------------------------------------------------------------------\n\nimport type { UpdateStatus } from './auto-update'\nimport type { CompanionSpec } from './auto-update-companion'\nimport type { UpdateLockHolder, UpdateLockOptions } from './auto-update-lock'\nimport type { UpgradeInstalledPackagesOptions, UpgradeReport } from './auto-update-service'\nimport { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { parseSemver, updateChecksOptedOut } from './auto-update'\nimport { acquireUpdateLock } from './auto-update-lock'\nimport { upgradeInstalledPackages } from './auto-update-service'\n\n/** File name of the failed-install record inside the cache directory. */\nexport const UPDATE_FAILED_FILE = 'update-failed.json'\n\n/** The automatic install that failed last. */\nexport interface FailedUpdateRecord {\n /** Release the install tried to reach. */\n version: string\n /** Why it failed, one package per line. */\n error: string\n /** ms since epoch. */\n at: number\n}\n\n/**\n * Reads the failed-install record. `null` when there is none or it does not parse.\n *\n * @param cacheDir - Directory that holds the record.\n */\nexport function readFailedUpdate(cacheDir: string): FailedUpdateRecord | null {\n try {\n const raw = JSON.parse(readFileSync(join(cacheDir, UPDATE_FAILED_FILE), 'utf8')) as Partial<FailedUpdateRecord>\n if (typeof raw.version === 'string' && typeof raw.error === 'string' && typeof raw.at === 'number')\n return { version: raw.version, error: raw.error, at: raw.at }\n return null\n }\n catch {\n return null\n }\n}\n\n/**\n * Records a failed automatic install. Best-effort.\n *\n * @param cacheDir - Directory that holds the record. Created when missing.\n * @param record - What failed.\n */\nexport function writeFailedUpdate(cacheDir: string, record: FailedUpdateRecord): void {\n try {\n mkdirSync(cacheDir, { recursive: true })\n writeFileSync(join(cacheDir, UPDATE_FAILED_FILE), `${JSON.stringify(record, null, 2)}\\n`)\n }\n catch {\n // A missing record only means one more attempt later.\n }\n}\n\n/**\n * Forgets the failed install, so the next check may try again. Called by every\n * manual install.\n *\n * @param cacheDir - Directory that holds the record.\n */\nexport function clearFailedUpdate(cacheDir: string): void {\n try {\n unlinkSync(join(cacheDir, UPDATE_FAILED_FILE))\n }\n catch {\n // Nothing to clear.\n }\n}\n\n/** Why {@link autoInstallUpdate} did nothing. */\nexport type AutoInstallSkipReason\n /** No newer stable release on `latest`, or the env opts out. */\n = | 'not-eligible'\n /** This version failed before; wait for a newer one or a manual install. */\n | 'failed-before'\n /** Another install holds the lock. The installed-version watcher sees its result. */\n | 'locked'\n\n/** Outcome of {@link autoInstallUpdate}. */\nexport type AutoInstallOutcome\n = | { kind: 'installed', version: string, report: UpgradeReport }\n | { kind: 'skipped', reason: AutoInstallSkipReason, holder?: UpdateLockHolder }\n | { kind: 'failed', version: string, error: string }\n\n/** Options for {@link isAutoInstallCandidate}. */\nexport interface AutoInstallCandidateOptions {\n /** Directory that holds the failed-install record. */\n cacheDir: string\n /** Defaults to `process.env`. */\n env?: NodeJS.ProcessEnv\n}\n\n/**\n * Whether a check result is one an automatic install may act on: a newer\n * release on the `latest` dist-tag, not a prerelease, not opted out by env,\n * and not the version that failed last time.\n *\n * @param status - Result of the background check.\n * @param options - Cache directory and environment.\n * @returns `true`, or why not.\n */\nexport function isAutoInstallCandidate(status: UpdateStatus | null, options: AutoInstallCandidateOptions): true | AutoInstallSkipReason {\n if (!status?.hasUpdate || !status.latest || status.channel !== 'latest')\n return 'not-eligible'\n if (status.source === 'skipped' || status.source === 'error' || updateChecksOptedOut(options.env))\n return 'not-eligible'\n const semver = parseSemver(status.latest)\n if (!semver || semver.prerelease.length > 0)\n return 'not-eligible'\n if (readFailedUpdate(options.cacheDir)?.version === status.latest)\n return 'failed-before'\n return true\n}\n\n/** Options for {@link autoInstallUpdate}. */\nexport interface AutoInstallUpdateOptions {\n /** Result of the background check that found the release. */\n status: UpdateStatus\n /** Where the lock, the failed record, the registry cache and the applied marker live. */\n cacheDir: string\n /** Packages to keep in step. */\n targets: readonly CompanionSpec[]\n /** Who runs the install, recorded in the lock: `'desktop app'`, `'terminal app'`. */\n by: string\n /** Passed through to {@link upgradeInstalledPackages} (progress, PATH-aware probes). */\n upgradeOptions?: Omit<UpgradeInstalledPackagesOptions, 'targets' | 'cacheDir' | 'channel' | 'force'>\n /** Defaults to `process.env`. */\n env?: NodeJS.ProcessEnv\n /** Injected for tests. Defaults to {@link upgradeInstalledPackages}. */\n upgrade?: typeof upgradeInstalledPackages\n /** Injected for tests. */\n lock?: UpdateLockOptions\n /** Injected for tests. Defaults to `Date.now`. */\n now?: () => number\n}\n\n/**\n * Installs the release a background check found, unless something says not\n * to (see {@link isAutoInstallCandidate}) or another install holds the lock.\n * Never throws. Records a failure so the same version is not retried, and\n * clears the record on success.\n *\n * @param options - Check result, cache directory, targets and test seams.\n * @example\n * ```ts\n * const outcome = await autoInstallUpdate({ status, cacheDir, targets, by: 'terminal app' })\n * if (outcome.kind === 'failed')\n * log.warn('automatic update failed', { error: outcome.error })\n * ```\n */\nexport async function autoInstallUpdate(options: AutoInstallUpdateOptions): Promise<AutoInstallOutcome> {\n const { status, cacheDir } = options\n const now = options.now ?? Date.now\n const candidate = isAutoInstallCandidate(status, { cacheDir, ...(options.env ? { env: options.env } : {}) })\n if (candidate !== true)\n return { kind: 'skipped', reason: candidate }\n const version = status.latest!\n const lock = acquireUpdateLock(cacheDir, options.by, options.lock)\n if (!lock.acquired)\n return { kind: 'skipped', reason: 'locked', holder: lock.holder }\n try {\n const report = await (options.upgrade ?? upgradeInstalledPackages)({\n ...options.upgradeOptions,\n targets: options.targets,\n cacheDir,\n channel: 'latest',\n })\n const error = upgradeError(report)\n if (error) {\n writeFailedUpdate(cacheDir, { version, error, at: now() })\n return { kind: 'failed', version, error }\n }\n clearFailedUpdate(cacheDir)\n return { kind: 'installed', version, report }\n }\n catch (err) {\n const error = err instanceof Error ? err.message : String(err)\n writeFailedUpdate(cacheDir, { version, error, at: now() })\n return { kind: 'failed', version, error }\n }\n finally {\n lock.release()\n }\n}\n\n/**\n * Why an upgrade run did not land, one package per line; `null` when it did.\n * A run that found no package on PATH failed too: nothing was installed.\n *\n * @param report - Result of {@link upgradeInstalledPackages}.\n */\nexport function upgradeError(report: UpgradeReport): string | null {\n const failures = report.results.filter(r => r.status === 'failed' || r.status === 'unavailable')\n if (failures.length > 0)\n return failures.map(f => `${f.packageName}: ${f.error ?? f.status}`).join('\\n')\n if (report.results.every(r => r.status === 'not-installed'))\n return 'no zidane packages found on PATH'\n return null\n}\n","// ---------------------------------------------------------------------------\n// `useUpdateCheck` — renderer-agnostic React hook driving the auto-update\n// footer chip + any banner a host wants to wire.\n//\n// Lives in `zidane/chat` (not `zidane/tui`) so a GUI shell can consume the\n// same status. Pure React + the renderer-agnostic core in\n// `./auto-update`; no OpenTUI imports.\n// ---------------------------------------------------------------------------\n\nimport type { UpdateStatus } from './auto-update'\nimport type { AutoInstallOutcome } from './auto-update-auto-install'\nimport type { CompanionSpec } from './auto-update-companion'\nimport type { InstalledVersionState } from './auto-update-restart'\nimport type { Hint } from './hints'\nimport { useEffect, useRef, useState } from 'react'\nimport { checkForUpdate } from './auto-update'\nimport { autoInstallUpdate } from './auto-update-auto-install'\nimport { watchInstalledVersion } from './auto-update-restart'\n\n/** Options for {@link useUpdateCheck}. */\nexport interface UseUpdateCheckOptions {\n /** npm package name. e.g. `'zidane-tui'`. */\n packageName: string\n /** Inlined at build time. Skips the check if empty/falsy. */\n currentVersion: string\n /**\n * Master enable flag — usually `Settings.checkForUpdates`. When `false`\n * the hook short-circuits without scheduling any work AND clears the\n * last-known status so a stale chip doesn't linger after the user\n * disabled the feature.\n */\n enabled: boolean\n /** Directory under which the on-disk cache lives. Usually `paths.userDir`. */\n cacheDir?: string\n /** Registry base URL. Defaults to `https://registry.npmjs.org`. */\n registry?: string\n /** dist-tag to read. Defaults to `'latest'`. */\n channel?: string\n /**\n * Cache lifetime for the read-through. **Defaults to `0`** (always\n * fetch fresh) — the TUI chip is the user's source of truth for \"what\n * version of zidane is on npm right now\"; we'd rather pay one HTTPS\n * call per session than tell them yesterday's news for up to 24 hours.\n *\n * The on-disk cache is still WRITTEN with each fresh result (so\n * `zidane upgrade` and other tooling reads benefit) and still READ to\n * feed the registry's ETag (so the cold path can short-circuit on 304\n * — meaningful bandwidth win when nothing changed). Pass a positive\n * value (e.g. `3_600_000` for 1 h) to enable read-through caching for\n * a GUI shell that mounts the hook in many places.\n */\n cacheTtlMs?: number\n /**\n * Wall-clock delay before the first check fires. Defaults to 1 500 ms so\n * we never compete with the boot path's first paint. The check itself\n * is async, so this only affects WHEN the chip first appears — never\n * blocks anything else.\n */\n delayMs?: number\n /** Injected for tests. */\n now?: () => number\n /** Injected for tests. */\n fetcher?: typeof fetch\n}\n\n/**\n * Returns the current update status. The check fires:\n * - Once per mount (every TUI launch).\n * - Every time `enabled` flips from `false` → `true` (user re-enabling\n * the setting expects an immediate refresh).\n *\n * The check is async and deferred by `delayMs` so it never competes with\n * first paint. Result is `null` until the first check resolves; flipping\n * `enabled` to `false` resets to `null` so any chip rendered against\n * `hasUpdate` disappears immediately.\n *\n * The hook owns its own AbortController so an unmount cancels any in-flight\n * registry request cleanly.\n *\n * @param options - Package, current version, enable flag and cache settings for the check.\n */\nexport function useUpdateCheck(options: UseUpdateCheckOptions): UpdateStatus | null {\n const [status, setStatus] = useState<UpdateStatus | null>(null)\n // Ref-mirror so the effect doesn't re-run on every keystroke when the\n // host wraps the result into derived hint state.\n const optsRef = useRef(options)\n optsRef.current = options\n\n useEffect(() => {\n if (!options.enabled || !options.currentVersion) {\n // Disabling the setting must hide a chip that was already painted\n // — otherwise the user sees a stale `↑ vX.Y.Z` linger until the\n // next launch. React bails on identical-state updates so the\n // initial-mount case (status already null) costs nothing.\n setStatus(null)\n return\n }\n const controller = new AbortController()\n const timer = setTimeout(() => {\n void checkForUpdate({\n packageName: optsRef.current.packageName,\n currentVersion: optsRef.current.currentVersion,\n cacheDir: optsRef.current.cacheDir,\n registry: optsRef.current.registry,\n channel: optsRef.current.channel,\n // Default to TTL=0 (always fetch fresh). See `cacheTtlMs` docs.\n cacheTtlMs: optsRef.current.cacheTtlMs ?? 0,\n signal: controller.signal,\n now: optsRef.current.now,\n fetcher: optsRef.current.fetcher,\n }).then((s) => {\n if (!controller.signal.aborted)\n setStatus(s)\n })\n }, options.delayMs ?? 1500)\n return () => {\n clearTimeout(timer)\n controller.abort()\n }\n // Re-run when the master enable / package / version flip. Other\n // option changes stay inside `optsRef.current`; re-running on every\n // option mutation would cause render storms when the host wraps\n // the result into derived hint state.\n }, [options.enabled, options.packageName, options.currentVersion])\n\n return status\n}\n\n/** Options for {@link useAutoInstallUpdate}. */\nexport interface UseAutoInstallUpdateOptions {\n /** Result of {@link useUpdateCheck}. */\n status: UpdateStatus | null\n /** `Settings.checkForUpdates && Settings.autoInstallUpdates`, and the host allows background installs. */\n enabled: boolean\n /** Where the lock, the failed record and the applied marker live. Nothing runs without it. */\n cacheDir?: string\n /** Packages to keep in step. */\n targets: readonly CompanionSpec[]\n /** Who runs the install, recorded in the lock. */\n by: string\n /** Called once per attempt with what happened. */\n onOutcome?: (outcome: AutoInstallOutcome) => void\n /** Injected for tests. Defaults to {@link autoInstallUpdate}. */\n install?: typeof autoInstallUpdate\n}\n\n/**\n * Installs the release {@link useUpdateCheck} found, in the background, when\n * the user turned `autoInstallUpdates` on. One attempt per version per mount;\n * the check itself runs after first paint, so this never competes with it.\n * Never restarts anything: the applied marker wakes `useRestartRequired`.\n * Returns the last outcome, `null` before the first attempt.\n *\n * @param options - Check result, switches, cache directory and targets.\n */\nexport function useAutoInstallUpdate(options: UseAutoInstallUpdateOptions): AutoInstallOutcome | null {\n const [outcome, setOutcome] = useState<AutoInstallOutcome | null>(null)\n const optsRef = useRef(options)\n optsRef.current = options\n const tried = useRef(new Set<string>())\n const latest = options.status?.hasUpdate ? options.status.latest : null\n\n useEffect(() => {\n const { status, cacheDir } = optsRef.current\n if (!options.enabled || !cacheDir || !status || !latest || tried.current.has(latest))\n return\n tried.current.add(latest)\n let live = true\n // Not aborted on unmount: killing a package manager mid-install can\n // leave a broken global install behind.\n void (optsRef.current.install ?? autoInstallUpdate)({\n status,\n cacheDir,\n targets: optsRef.current.targets,\n by: optsRef.current.by,\n }).then((next) => {\n optsRef.current.onOutcome?.(next)\n if (live)\n setOutcome(next)\n })\n return () => {\n live = false\n }\n }, [options.enabled, options.cacheDir, latest])\n\n return outcome\n}\n\n// ---------------------------------------------------------------------------\n// Footer chip helper — pure function so any renderer (TUI Footer, future\n// GUI status bar) can compose against the same `Hint`. The chip is\n// suppressed when there's no update; callers spread the result into their\n// hint array unconditionally.\n//\n// Color contract: the chip rides whatever accent the host passes (typically\n// `theme.colors.accent`). Renderers that want a \"fresh check\" badge can\n// inspect `status.source === 'fresh'` themselves — this helper sticks to\n// the one-line discoverability surface.\n// ---------------------------------------------------------------------------\n\n/** Display options for the update and restart hints. */\nexport interface BuildUpdateHintOptions {\n /** Foreground color for the `↑` glyph + version label. */\n color: string\n /**\n * When true (default), the chip renders the literal version (`↑ v5.6.0`).\n * When false, just the glyph + `update` label — useful when the row is\n * tight and the version would push higher-priority chips off the right.\n */\n showVersion?: boolean\n /** Override the leading glyph. Defaults to `'↑'`. */\n glyph?: string\n}\n\n/**\n * Builds the `↑ vX.Y.Z` hint for an available update.\n * Returns `null` when there is no newer version.\n *\n * @param status - Result of an update check, or `null` before the first check.\n * @param options - Color and label settings for the hint.\n */\nexport function buildUpdateHint(status: UpdateStatus | null, options: BuildUpdateHintOptions): Hint | null {\n if (!status?.hasUpdate || !status.latest)\n return null\n const label = options.showVersion === false\n ? 'update'\n : `v${stripVPrefix(status.latest)}`\n return {\n key: options.glyph ?? '↑',\n keyColor: options.color,\n label,\n labelColor: options.color,\n }\n}\n\n/**\n * `↻ v7.0.31` — a newer build is on disk than the one running (this\n * process is stale; see `auto-update-restart.ts`). Takes precedence over\n * the `↑` chip: once the release is installed, \"available\" is old news and\n * \"restart\" is the one action left. Same color contract as `buildUpdateHint`.\n *\n * @param state - Installed-version state, or `null` when unknown.\n * @param options - Color and label settings for the hint.\n */\nexport function buildRestartHint(\n state: { restartRequired: boolean, installed: string | null } | null,\n options: BuildUpdateHintOptions,\n): Hint | null {\n if (!state?.restartRequired || !state.installed)\n return null\n return {\n key: options.glyph ?? '↻',\n keyColor: options.color,\n label: options.showVersion === false ? 'restart' : `v${stripVPrefix(state.installed)}`,\n labelColor: options.color,\n }\n}\n\nfunction stripVPrefix(v: string): string {\n return v.startsWith('v') ? v.slice(1) : v\n}\n\n// ---------------------------------------------------------------------------\n// `useRestartRequired` — the TUI's consumer of the installed-version watcher\n// (`./auto-update-restart`). The GUI runs the watcher in its main process and\n// forwards the state over IPC instead.\n// ---------------------------------------------------------------------------\n\n/** Options for {@link useRestartRequired}. */\nexport interface UseRestartRequiredOptions {\n /** The running package. `null` disables the hook. */\n spec: CompanionSpec | null\n /** Version the process was built with. */\n currentVersion: string\n /** Directory that holds the applied marker. Omit to rely on polling alone. */\n cacheDir?: string\n /** Master switch — mirrors `Settings.checkForUpdates`. */\n enabled: boolean\n /** Poll interval in milliseconds. Defaults to 5 minutes. `0` disables polling. */\n pollMs?: number\n}\n\n/**\n * `null` until the first probe (or when disabled / no spec), then the live\n * state. Flipping `enabled` off tears the watcher down and clears the state\n * so no stale \"restart\" chip lingers.\n *\n * @param options - Package spec, running version, cache directory, poll interval and the master `enabled` switch.\n */\nexport function useRestartRequired(options: UseRestartRequiredOptions): InstalledVersionState | null {\n const [state, setState] = useState<InstalledVersionState | null>(null)\n const { spec, currentVersion, cacheDir, enabled, pollMs } = options\n const packageName = spec?.packageName\n const binName = spec?.binName\n\n useEffect(() => {\n if (!enabled || !packageName || !currentVersion) {\n setState(null)\n return\n }\n const watcher = watchInstalledVersion({\n spec: binName ? { packageName, binName } : { packageName },\n currentVersion,\n cacheDir,\n pollMs,\n onChange: setState,\n })\n setState(watcher.current())\n return () => watcher.close()\n }, [enabled, packageName, binName, currentVersion, cacheDir, pollMs])\n\n return state\n}\n","// ---------------------------------------------------------------------------\n// Footer hint composition.\n//\n// Pure presentation logic — given the current screen, run state, and a\n// resolved keybinding map, produce the ordered list of {@link Hint}s\n// the bottom-bar Footer should render. Lives in `zidane/chat` (not\n// `zidane/tui`) because it's renderer-agnostic: the only types involved\n// are `Hint`, `Screen`, `SessionMeta`, `Settings['uiMode']`, and `KeyBindings`. A GUI\n// shell can consume the same list and paint it however it likes.\n//\n// Priority order is intentional and load-bearing:\n//\n// pending approval > live pending interaction > resumed pending\n// interaction > busy (streaming) > auth screen > onboarding screen (no\n// hints) > sessions screen > chat screen (idle).\n//\n// The first four return state-specific hint sets unconditionally —\n// `uiMode` is ignored there because those affordances (approve / abort\n// / navigate) are not the noise the minimal mode is targeting. Minimal\n// mode only collapses the chat-idle row.\n// ---------------------------------------------------------------------------\n\nimport type { Hint } from './hints'\nimport type { KeyBindings } from './keybindings'\nimport type { Screen, SessionMeta, Settings } from './types'\nimport { formatBindingForDisplay } from './keybindings'\n\n/**\n * Options bag for {@link buildHints}. Flat shape so call sites read as a\n * checklist of \"what state do I have to surface\" — easy to thread\n * through React `useMemo` deps and to construct fresh in tests.\n */\nexport interface BuildHintsOptions {\n /** Screen the footer is shown on. */\n screen: Screen\n /** True while the agent is running a turn. */\n busy: boolean\n /** True while a safe-mode approval prompt is pending. */\n pending: boolean\n /**\n * True when a LIVE pending interaction is on screen (agent is mid-run,\n * awaiting the tool's Promise). Same nav hints as the safe-mode\n * approval picker — `abortRun` aborts the run.\n */\n pendingInteractionLive: boolean\n /**\n * True when the chat screen is showing a resumed (not live) pending\n * interaction. The agent isn't running; esc leaves the form without\n * persisting a response, so the hint reads \"leave for later\" — the\n * session re-enqueues the same interaction on next activation.\n */\n pendingInteractionResumed: boolean\n /** Active session, or `null` when none is open. */\n currentSession: SessionMeta | null\n /** True when more than one agent profile is available, so the agent switch hint is useful. */\n hasMultipleAgents: boolean\n /**\n * Chat-screen chrome density — `'minimal'` collapses the idle chat\n * hint row to just `agent / model / settings`. Busy and pending\n * states keep their full hint sets because those\n * affordances are load-bearing (abort, navigate, approve).\n */\n uiMode: Settings['uiMode']\n /** Label of the active model, or `null` when none is selected. */\n modelLabel: string | null\n /** Foreground color for the model label. */\n modelColor: string\n /** Current reasoning effort label, or `null` when the model has no reasoning knob. */\n effortLabel: string | null\n /** Foreground color for the effort label. */\n effortColor: string\n /**\n * Foreground for the `/l` chord that introduces the effort label. Unset, it\n * takes the color of the model key it extends, so the pair reads as one binding.\n */\n effortKeyColor?: string\n /**\n * Comma-joined labels of the enabled model options (e.g. `'fast'`), or `null`\n * when none are on. Appended after the effort label in the model chord — when\n * the model has no reasoning knob, it rides the chord on its own.\n */\n modelOptionsLabel?: string | null\n /** Label of the active agent profile. */\n agentLabel: string\n /** Foreground color for the agent label. */\n agentColor: string\n /**\n * Resolved keybindings — every hint key string flows through this so\n * a user-level rebind (e.g. `\"openSettings\": \"ctrl+p\"`) shows up live\n * in the bottom bar without us hard-coding the default `'ctrl+o'`.\n */\n keybindings: KeyBindings\n /**\n * Number of currently-dispatching tool calls. Drives the\n * `<cancelToolCall> cancel` chip in the `busy` hint row and the\n * `cancel task` chip on the idle chat row. Without\n * it, the binding stays inert and there's no point advertising the\n * shortcut. Falls back to \"no chip\" when no tools are in flight\n * (model is mid-stream but hasn't dispatched yet).\n */\n inFlightToolCount: number\n /**\n * Number of currently-active skills. Drives the footer's\n * `✦ N skill(s)` chip on the chat screen — purely informational,\n * a passive surface for \"tool restrictions are in effect\". Hidden\n * when `0` so the row stays uncluttered for the common case.\n */\n activeSkillCount: number\n /** Foreground for the skills chip (typically `COLOR.brand`). */\n skillsChipColor: string\n /**\n * Progress label for the todo chip (`\"2/5\"` from `todoProgressLabel`), or\n * `null` to hide it.\n *\n * As a `Hint` it inherits the row's click-to-keypress\n * contract for free (`dispatchHintKey`), so clicking it opens the list the\n * same way `ctrl+t` does — and it sits below the prompt like every other\n * control.\n */\n todoProgress: string | null\n /**\n * Pre-built `↑ vX.Y.Z` chip from `buildUpdateHint(useUpdateCheck())`,\n * or `null` when no update is available. Always last in the chat-screen\n * row so it's the first to drop when the terminal narrows.\n */\n updateHint: Hint | null\n /**\n * Pre-built prompt-cache toast, non-null only for a few seconds after a\n * `'cache:break'` hook fired (the host owns the visibility timer and the\n * severity split): warn-colored `⚡ cache break in <culprit>: +Nk tok, ~$0.48`\n * for attributable breaks, dim `⚡ cache expired: +Nk tok, ~$0.48` for\n * byte-stable TTL expiry — routine across human think-time, so it must not\n * cry wolf. Sits beside `updateHint` at the droppable end of the row: a\n * transient cost signal, not an action.\n */\n cacheBreakHint?: Hint | null\n /**\n * Sessions-screen list scope — `true` when the list spans all\n * projects. Drives the `tab` hint's label, which advertises the scope\n * the toggle switches TO. Optional so non-sessions callers can omit it.\n */\n sessionsScopeAll?: boolean\n /** Sessions screen: plain ←/→ switch spaces (Spaces on, 2+ spaces). Adds the `←→ space` hint. */\n sessionsSpaceSwitch?: boolean\n}\n\n/**\n * Build the footer's shortcut hints for the current screen. On the chat\n * screen the model id rides next to its `ctrl+m` shortcut and the agent\n * label rides next to `shift+tab`, each in its accent color — the bar\n * doubles as the status display without needing separate badges. When\n * the active model exposes reasoning, the `ctrl+m` hint grows a\n * secondary `/l` chord with the current effort label, surfacing the\n * effort picker as a discoverable, in-place affordance.\n *\n * @param options - Screen, run state, labels, colors, and keybindings that decide which hints appear.\n */\nexport function buildHints(options: BuildHintsOptions): Hint[] {\n const {\n screen,\n busy,\n pending,\n pendingInteractionLive,\n pendingInteractionResumed,\n currentSession,\n hasMultipleAgents,\n uiMode,\n modelLabel,\n modelColor,\n effortLabel,\n effortColor,\n effortKeyColor,\n modelOptionsLabel,\n agentLabel,\n agentColor,\n keybindings,\n inFlightToolCount,\n activeSkillCount,\n skillsChipColor,\n todoProgress,\n updateHint,\n cacheBreakHint = null,\n sessionsScopeAll,\n sessionsSpaceSwitch,\n } = options\n\n // Todo progress chip. Built BEFORE the priority branches below because it is\n // STATUS, not a shortcut: it has to survive every one of them. It reports\n // todo progress independent of `busy`, and an `in_progress` todo is set\n // precisely while the agent is working and calling `todowrite`. Building\n // it after the early returns would hide it for the whole window it exists\n // to cover.\n //\n // Appended LAST in each branch so `clipHintsToWidth` (which drops from the\n // right) sheds it before the abort/navigate affordances, which must stay.\n const todosChip: Hint | null = todoProgress !== null\n ? { key: keybindings.openTodos, label: `${todoProgress} todos` }\n : null\n const withTodos = (hints: Hint[]): Hint[] => todosChip ? [...hints, todosChip] : hints\n\n // Priority: safe-mode approval > live interaction > resumed\n // interaction > plain streaming. All three pending shapes share the\n // ↑↓ / ↵ nav prefix; only the esc semantics differ.\n if (pending)\n return withTodos([{ key: '↑↓', label: 'navigate' }, { key: '↵', label: 'select' }, { key: formatBindingForDisplay(keybindings.abortRun), label: 'abort run' }])\n if (pendingInteractionLive)\n return withTodos([{ key: '↑↓', label: 'navigate' }, { key: '↵', label: 'select' }, { key: formatBindingForDisplay(keybindings.abortRun), label: 'abort run' }])\n if (pendingInteractionResumed)\n return withTodos([{ key: '↑↓', label: 'navigate' }, { key: '↵', label: 'select' }, { key: 'esc', label: 'leave for later' }])\n if (busy) {\n // The run-wide abort, then the way out. The per-call cancel\n // (`cancelToolCall`) stays bound but unadvertised mid-run: the abort\n // covers the common case, and the chip only crowded the row.\n const baseBusyHints: Hint[] = []\n baseBusyHints.push({ key: formatBindingForDisplay(keybindings.abortRun), label: 'abort' })\n baseBusyHints.push({ key: 'esc', label: 'sessions' })\n // The model stays in view during the run it serves (a fallback shows in\n // its warn colour exactly then); no key, since the picker waits for idle.\n if (modelLabel)\n baseBusyHints.push({ key: '', label: modelLabel, labelColor: modelColor })\n return withTodos(baseBusyHints)\n }\n if (screen === 'auth')\n return [{ key: '↑↓', label: 'navigate' }, { key: '↵', label: 'select' }, { key: 'esc', label: 'exit' }]\n // The setup wizard shows the keys of each step inside its frame.\n if (screen === 'onboarding')\n return []\n if (screen === 'sessions') {\n return [\n { key: '↑↓', label: 'navigate' },\n { key: '↵', label: 'open' },\n { key: 'tab', label: sessionsScopeAll ? 'current project' : 'all projects' },\n ...(sessionsSpaceSwitch ? [{ key: '←→', label: 'space' }] : []),\n { key: keybindings.openSessionDetails, label: 'session' },\n { key: keybindings.openSettings, label: 'settings' },\n { key: 'esc', label: currentSession ? 'back' : 'exit' },\n ]\n }\n // Chat screen. Prompt-input shortcuts (`↵ send`, `shift+↵ newline`,\n // `↑↓ history`) live in the prompt box's overlay title — the bottom\n // bar only carries the most common agent / model / global affordances. The\n // `cycleAgent` binding shows the active label inline; the\n // `openModelPicker` binding shows the active model id inline; etc.\n // `cycleAgent` is suppressed when there's only one profile and\n // `openSessionDetails` only renders once a session exists.\n //\n // Minimal UI mode: strip the chat-idle bottom bar to just `agent`\n // (when there's >1 profile), `model/effort`, and `settings`. The\n // full keybinding catalog stays reachable from inside settings.\n // Busy / pending / select-turn / queue branches above already exited\n // before this point — minimal only affects the resting chat row.\n //\n // Compose the model hint. When the active model supports reasoning,\n // the primary `<openModelPicker> model` pair grows a secondary\n // `/l effort` chord that doubles as the discoverability hint for the\n // effort picker (`openEffortPicker`) and the status display for the\n // current effort. The chord shares the model accent so the pair\n // reads as one keyboard binding across every theme.\n // The `/l` chord (from `openEffortPicker`) carries reasoning effort plus any enabled model options,\n // all space-separated so they read as one model-info run (e.g.\n // `claude-opus-4-8 medium fast`). When the model has no reasoning knob, the\n // options ride the chord on their own.\n const chordLabel = [effortLabel, modelOptionsLabel].filter(Boolean).join(' ') || null\n const modelHint: Hint | null = modelLabel\n ? {\n key: keybindings.openModelPicker,\n label: modelLabel,\n labelColor: modelColor,\n ...(chordLabel\n ? { extra: { key: shortChord(keybindings.openEffortPicker), keySpec: keybindings.openEffortPicker, ...(effortKeyColor ? { keyColor: effortKeyColor } : {}), label: chordLabel, labelColor: effortColor } }\n : {}),\n }\n : null\n // Skills chip — purely informational, no shortcut wired. We re-use\n // the Hint shape's `key` slot to render the glyph (`✦`) so the row\n // layout (key + label, padded breakpoints) stays consistent with\n // every other entry. The chip is omitted when no skills are active\n // so the row stays uncluttered for the common case.\n const skillsChip: Hint | null = activeSkillCount > 0\n ? {\n key: '✦',\n keyColor: skillsChipColor,\n label: activeSkillCount === 1 ? '1 skill' : `${activeSkillCount} skills`,\n labelColor: skillsChipColor,\n }\n : null\n // Background-task chip — fires when the user is idle (not `busy`)\n // but a task is still running. Gives them a discoverable way to\n // reach `ctrl+k` for kill. The `busy` branch above already covers\n // the dispatching case; this one is for \"agent finished its turn,\n // task still chugging\".\n const cancelTaskChip: Hint | null = inFlightToolCount > 0\n ? {\n key: keybindings.cancelToolCall,\n label: inFlightToolCount === 1 ? 'cancel task' : `cancel task (${inFlightToolCount})`,\n }\n : null\n // Minimal mode short-circuits the full composition. Same agent / model\n // hints as the full row (so a multi-agent setup still cycles, and the\n // model + effort chord stays visible), plus `settings` as the\n // discoverability anchor for everything we just dropped (keybindings\n // included — they have a tab in there).\n if (uiMode === 'minimal') {\n return [\n ...(hasMultipleAgents ? [{ key: keybindings.cycleAgent, label: agentLabel, labelColor: agentColor }] : []),\n ...(modelHint ? [modelHint] : []),\n { key: keybindings.openSettings, label: 'settings' },\n ]\n }\n return [\n ...(hasMultipleAgents ? [{ key: keybindings.cycleAgent, label: agentLabel, labelColor: agentColor }] : []),\n ...(modelHint ? [modelHint] : []),\n ...(skillsChip ? [skillsChip] : []),\n ...(todosChip ? [todosChip] : []),\n ...(cancelTaskChip ? [cancelTaskChip] : []),\n ...(currentSession ? [{ key: keybindings.openSessionDetails, label: 'session' }] : []),\n { key: keybindings.openSettings, label: 'settings' },\n { key: 'esc', label: 'sessions' },\n // Update chip lives at the right-most position so `clipHintsToWidth`\n // drops it first on narrow terminals — discoverability is nice but\n // the action shortcuts must stay.\n ...(cacheBreakHint ? [cacheBreakHint] : []),\n ...(updateHint ? [updateHint] : []),\n ]\n}\n\n/**\n * Shorten a binding spec for display as a \"chord continuation\" — the\n * `/l` after `ctrl+m model` in the chat footer. We only strip the\n * `ctrl+` prefix so user-customized chords (`alt+n`, `meta+shift+n`)\n * still render in full. Falls back to the verbatim spec when no\n * `ctrl+` prefix is found, which keeps the visual contract honest:\n * the rendered key always matches the bound trigger.\n *\n * @param spec - Binding spec, such as `ctrl+n`.\n */\nexport function shortChord(spec: string): string {\n return spec.startsWith('ctrl+') ? `/${spec.slice('ctrl+'.length)}` : spec\n}\n","/**\n * @-completion path display formatter.\n *\n * The `@`-popover walks the project from the git root so the user sees\n * the whole tree regardless of where they launched the TUI. The\n * agent's CWD-resolving tools (`read_file`, `glob`, `edit`, …) read\n * relative paths against `process.cwd()`, which is frequently a\n * subdirectory of that root. Emitting the discovery's raw\n * project-root-relative path into the prompt produces a `<cwd>/<rel>`\n * miss — the bug this helper exists to close.\n *\n * Rule:\n *\n * - File at `cwd` itself → `.`.\n * - File under `cwd` (subtree) → forward-slashed CWD-relative,\n * no `..`.\n * - File above `cwd` but still inside the project → CWD-relative\n * with `..` traversal (e.g. `../EDIT_THIS.md`). Shorter and more\n * scannable than absolute, and the agent's tools resolve both.\n * - File outside the project (input was passed absolute) → returned\n * verbatim. Inputs that arrive as project-root-relative strings\n * are always inside the project by construction, so this branch\n * only kicks in when a direct caller hands the function an\n * absolute target.\n *\n * Pure, no I/O, deterministic.\n */\n\nimport { isAbsolute, posix, relative, resolve, sep } from 'node:path'\n\n/**\n * Convert a project-root-relative path (as emitted by `listProjectFiles`)\n * into the form the agent's tools — and the user — will actually find\n * given the current `cwd`.\n *\n * Inputs:\n * - `projectRelativePath` — forward-slashed, no leading slash. Returned\n * verbatim when it's empty or already absolute (defensive — the\n * absolute-input branch is the de-facto \"outside the project\"\n * escape; the catalog itself never emits absolute paths).\n * - `projectRoot` — absolute path of the discovery anchor.\n * - `cwd` — absolute path of `process.cwd()` at the moment of\n * insertion.\n *\n * Output is always either an absolute path (only when the input arrived\n * absolute) OR a CWD-relative path. The relative form may include\n * `../` segments when the target sits above `cwd` within the project,\n * which keeps cross-tree references short and scannable\n * (e.g. `../EDIT_THIS.md` instead of `/Users/.../zidane/EDIT_THIS.md`).\n *\n * @param projectRelativePath - Forward-slashed path relative to `projectRoot`.\n * @param projectRoot - Absolute path of the discovery anchor.\n * @param cwd - Absolute working directory the result is relative to.\n */\nexport function formatPathForCwd(\n projectRelativePath: string,\n projectRoot: string,\n cwd: string,\n): string {\n if (projectRelativePath.length === 0)\n return projectRelativePath\n if (isAbsolute(projectRelativePath))\n return projectRelativePath\n const target = resolve(projectRoot, projectRelativePath)\n const here = resolve(cwd)\n if (target === here)\n return '.'\n const rel = relative(here, target)\n if (rel.length === 0)\n return '.'\n return sep === '/' ? rel : rel.split(sep).join(posix.sep)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"path-display-ngOL00wI.js","names":["osHostname"],"sources":["../src/chat/artifacts-view.ts","../src/chat/auto-update-lock.ts","../src/chat/auto-update-service.ts","../src/chat/auto-update-auto-install.ts","../src/chat/auto-update-hook.ts","../src/chat/footer-hints.ts","../src/chat/path-display.ts"],"sourcesContent":["/**\n * Renderer-agnostic shaping of session artifacts for display — grouping\n * and row presentation. Shared by the TUI rail + modal and the GUI rail, so\n * every surface agrees on what an artifact looks like.\n */\n\nimport type { ExtensionArtifactRow, ExtensionRegistry } from '../extensions'\nimport type { SessionArtifact } from '../session'\nimport { artifactKindLabel, renderArtifactRow } from '../extensions/builtin-artifact-kinds'\n\n/** Whether artifacts are listed for the current session or the whole project. */\nexport type ArtifactScope = 'session' | 'project'\n\n/** One artifact paired with its rendered row. */\nexport interface ArtifactViewRow {\n /** The session artifact. */\n artifact: SessionArtifact\n /** Rendered row for the artifact. */\n row: ExtensionArtifactRow\n}\n\n/** Artifacts of one kind, ready to display. */\nexport interface ArtifactViewGroup {\n /** Artifact kind id. */\n kind: string\n /** Human kind label (`Pull request`), raw kind when unregistered. */\n label: string\n /** Rows of this kind, in the store's order. */\n rows: ArtifactViewRow[]\n}\n\ntype KindRegistry = Pick<ExtensionRegistry, 'artifactKinds'> | undefined\n\n/** Display order for the built-in kinds; anything else follows alphabetically. */\nconst KIND_ORDER = ['pull_request', 'branch', 'commit', 'file']\n\n/**\n * Group by kind (built-ins first in a fixed order, then extension kinds\n * alphabetically); rows inside a group keep the store's newest-first order.\n *\n * @param artifacts - Session artifacts to group.\n * @param registry - Extension registry that supplies kind labels and row renderers.\n */\nexport function groupArtifacts(artifacts: readonly SessionArtifact[], registry?: KindRegistry): ArtifactViewGroup[] {\n const byKind = new Map<string, ArtifactViewRow[]>()\n for (const artifact of artifacts) {\n const list = byKind.get(artifact.kind) ?? []\n list.push({ artifact, row: renderArtifactRow(artifact, registry) })\n byKind.set(artifact.kind, list)\n }\n const kinds = [...byKind.keys()].sort((a, b) => {\n const ai = KIND_ORDER.indexOf(a)\n const bi = KIND_ORDER.indexOf(b)\n if (ai !== -1 || bi !== -1)\n return (ai === -1 ? KIND_ORDER.length : ai) - (bi === -1 ? KIND_ORDER.length : bi)\n return a.localeCompare(b)\n })\n return kinds.map(kind => ({ kind, label: artifactKindLabel(kind, registry), rows: byKind.get(kind)! }))\n}\n\n/**\n * One count per kind, a badge each: `['3 pull requests', '2 files']`; empty\n * for none.\n *\n * @param artifacts - Session artifacts to count.\n * @param registry - Extension registry that supplies kind labels.\n */\nexport function summarizeArtifactCounts(artifacts: readonly SessionArtifact[], registry?: KindRegistry): string[] {\n return groupArtifacts(artifacts, registry)\n .map(g => `${g.rows.length} ${pluralizeKind(g.label, g.rows.length)}`)\n}\n\nfunction pluralizeKind(label: string, count: number): string {\n const lower = label.toLowerCase()\n if (count === 1)\n return lower\n return /(?:s|x|ch|sh)$/.test(lower) ? `${lower}es` : `${lower}s`\n}\n","// ---------------------------------------------------------------------------\n// One install at a time across the GUI, the TUI and `zidane upgrade`.\n//\n// `<cacheDir>/update.lock` holds who runs the install. It is created with an\n// exclusive create (`wx`), so two processes never both think they own it. A\n// lock goes stale when its process is gone on this host, when a live holder\n// on this host is older than {@link UPDATE_LOCK_LIVE_STALE_MS}, or when a\n// holder whose pid means nothing here is older than\n// {@link UPDATE_LOCK_STALE_MS}: a crashed install must not block every later\n// one. A stale lock is replaced.\n// ---------------------------------------------------------------------------\n\nimport { linkSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { hostname as osHostname } from 'node:os'\nimport { join } from 'node:path'\n\n/** File name of the install lock inside the cache directory. */\nexport const UPDATE_LOCK_FILE = 'update.lock'\n\n/** A lock older than this is stale when its holder can't be probed (another host, or no pid). */\nexport const UPDATE_LOCK_STALE_MS = 15 * 60_000\n\n/**\n * A lock held by a live process on this host goes stale only past this: a slow\n * install keeps its lock, and a recycled pid can't hold one forever.\n */\nexport const UPDATE_LOCK_LIVE_STALE_MS = 2 * 60 * 60_000\n\n/** What the lock file records about the install that holds it. */\nexport interface UpdateLockHolder {\n /** Process id of the install. */\n pid: number\n /** Host the process runs on. A pid only means something on its own host. */\n hostname: string\n /** ms since epoch. */\n startedAt: number\n /** Who runs the install, e.g. `'zidane upgrade'`, `'desktop app'`, `'terminal app'`. */\n by: string\n}\n\n/** Outcome of {@link acquireUpdateLock}. */\nexport type UpdateLockResult\n = | { acquired: true, holder: UpdateLockHolder, release: () => void }\n | { acquired: false, holder: UpdateLockHolder }\n\n/** Injected clocks and probes, for tests. */\nexport interface UpdateLockOptions {\n /** Defaults to `Date.now`. */\n now?: () => number\n /** Whether `pid` runs on this host. Defaults to `process.kill(pid, 0)`. */\n isAlive?: (pid: number) => boolean\n /** Defaults to `os.hostname()`. */\n hostname?: string\n /** Defaults to `process.pid`. */\n pid?: number\n}\n\n/**\n * Returns the path of the install lock file.\n *\n * @param cacheDir - Directory that holds the lock.\n */\nexport function updateLockPath(cacheDir: string): string {\n return join(cacheDir, UPDATE_LOCK_FILE)\n}\n\n/**\n * Whether `pid` is a live process on this host. `EPERM` means it exists but\n * belongs to another user, so it counts as alive.\n *\n * @param pid - Process id to probe.\n */\nexport function isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0)\n return true\n }\n catch (err) {\n return (err as NodeJS.ErrnoException).code !== 'ESRCH'\n }\n}\n\n/**\n * Reads the lock file. Returns `null` when there is none. A file that does\n * not parse (another process is still writing it, or garbage) yields a holder\n * with `pid: 0`, dated by the file's mtime, so only age makes it stale.\n *\n * @param cacheDir - Directory that holds the lock.\n */\nexport function readUpdateLock(cacheDir: string): UpdateLockHolder | null {\n return readHolder(updateLockPath(cacheDir))\n}\n\n/**\n * Whether a lock no longer protects a running install: its process is gone\n * on this host, a live one is older than {@link UPDATE_LOCK_LIVE_STALE_MS},\n * or a holder that can't be probed is older than {@link UPDATE_LOCK_STALE_MS}.\n *\n * @param holder - The recorded holder.\n * @param options - Injected clock, liveness probe and host name.\n */\nexport function isUpdateLockStale(holder: UpdateLockHolder, options: UpdateLockOptions = {}): boolean {\n const age = (options.now ?? Date.now)() - holder.startedAt\n const host = options.hostname ?? osHostname()\n if (holder.pid > 0 && holder.hostname === host)\n return age > UPDATE_LOCK_LIVE_STALE_MS || !(options.isAlive ?? isProcessAlive)(holder.pid)\n return age > UPDATE_LOCK_STALE_MS\n}\n\n/**\n * Takes the install lock, replacing a stale one. Never throws: a cache dir\n * that cannot be written yields `acquired: true` with a no-op release, since\n * a lock that cannot be created protects nothing and must not block the\n * install.\n *\n * @param cacheDir - Directory that holds the lock. Created when missing.\n * @param by - Who runs the install, shown to anyone who finds the lock held.\n * @param options - Injected clock, liveness probe, host name and pid.\n */\nexport function acquireUpdateLock(cacheDir: string, by: string, options: UpdateLockOptions = {}): UpdateLockResult {\n const path = updateLockPath(cacheDir)\n const mine: UpdateLockHolder = {\n pid: options.pid ?? process.pid,\n hostname: options.hostname ?? osHostname(),\n startedAt: (options.now ?? Date.now)(),\n by,\n }\n const acquired = (): UpdateLockResult => ({ acquired: true, holder: mine, release: () => releaseIfMine(path, mine) })\n try {\n mkdirSync(cacheDir, { recursive: true })\n }\n catch {\n return { acquired: true, holder: mine, release: () => {} }\n }\n // Two rounds: the second runs after a stale lock was moved away.\n for (let attempt = 0; attempt < 2; attempt++) {\n const created = create(path, mine)\n if (created === 'ok')\n return acquired()\n if (created === 'error')\n return { acquired: true, holder: mine, release: () => {} }\n const held = readHolder(path)\n if (!held)\n continue // Released between our create and our read.\n if (!isUpdateLockStale(held, options))\n return { acquired: false, holder: held }\n const taken = takeStale(path, mine, options)\n if (taken)\n return { acquired: false, holder: taken }\n }\n const created = create(path, mine)\n if (created === 'exists') {\n const held = readHolder(path)\n if (held)\n return { acquired: false, holder: held }\n }\n return created === 'ok' ? acquired() : { acquired: true, holder: mine, release: () => {} }\n}\n\n/**\n * One line for people: `the desktop app (pid 4242, started 3 min ago)`.\n *\n * @param holder - The recorded holder.\n * @param now - Current time in ms. Defaults to `Date.now()`.\n */\nexport function describeUpdateLockHolder(holder: UpdateLockHolder, now: number = Date.now()): string {\n const minutes = Math.max(0, Math.round((now - holder.startedAt) / 60_000))\n const age = minutes < 1 ? 'just now' : `${minutes} min ago`\n const pid = holder.pid > 0 ? `pid ${holder.pid}, ` : ''\n return `${holder.by} (${pid}started ${age})`\n}\n\nfunction create(path: string, holder: UpdateLockHolder): 'ok' | 'exists' | 'error' {\n try {\n writeFileSync(path, `${JSON.stringify(holder)}\\n`, { flag: 'wx' })\n return 'ok'\n }\n catch (err) {\n return (err as NodeJS.ErrnoException).code === 'EEXIST' ? 'exists' : 'error'\n }\n}\n\n/**\n * Move a stale lock aside so only one process replaces it. Returns the holder\n * when the file we moved turned out to be a fresh lock (another process\n * replaced the stale one first): it goes back, and the lock stays held.\n */\nfunction takeStale(path: string, mine: UpdateLockHolder, options: UpdateLockOptions): UpdateLockHolder | null {\n const aside = `${path}.${mine.pid}.${mine.startedAt}.stale`\n try {\n renameSync(path, aside)\n }\n catch {\n return null // Gone already; the next create decides.\n }\n const moved = readHolder(aside)\n if (moved && !isUpdateLockStale(moved, options)) {\n try {\n linkSync(aside, path)\n }\n catch {\n // Someone created a lock meanwhile; theirs stands.\n }\n removeQuietly(aside)\n return readHolder(path) ?? moved\n }\n removeQuietly(aside)\n return null\n}\n\nfunction releaseIfMine(path: string, mine: UpdateLockHolder): void {\n const held = readHolder(path)\n // Never remove a lock another process took after ours went stale.\n if (held && held.pid === mine.pid && held.startedAt === mine.startedAt && held.hostname === mine.hostname)\n removeQuietly(path)\n}\n\nfunction readHolder(path: string): UpdateLockHolder | null {\n let raw: string\n try {\n raw = readFileSync(path, 'utf8')\n }\n catch {\n return null\n }\n try {\n const parsed = JSON.parse(raw) as Partial<UpdateLockHolder>\n if (typeof parsed.pid === 'number' && typeof parsed.hostname === 'string' && typeof parsed.startedAt === 'number' && typeof parsed.by === 'string')\n return { pid: parsed.pid, hostname: parsed.hostname, startedAt: parsed.startedAt, by: parsed.by }\n }\n catch {\n // Half-written or garbage: dated by the file itself below.\n }\n try {\n return { pid: 0, hostname: '', startedAt: statSync(path).mtimeMs, by: 'another install' }\n }\n catch {\n return null\n }\n}\n\nfunction removeQuietly(path: string): void {\n try {\n unlinkSync(path)\n }\n catch {\n // Already gone.\n }\n}\n","// ---------------------------------------------------------------------------\n// Structured, host-agnostic upgrade of every INSTALLED zidane package.\n//\n// `runUpdateCommand` is the terminal-facing ledger for `zidane upgrade`; it\n// knows the TUI is the primary and detects that install's package manager\n// from `process.execPath`. A GUI cannot reuse it — its `execPath` is\n// Electron's — and wants results, not text. This module is the shared\n// engine both can build on: every target is located by its bin on PATH\n// (`findInstalledCompanion`), checked against the registry, installed through\n// the package manager that owns THAT bin, and verified on disk afterwards\n// (a PM exiting 0 is not proof; an optional platform binary that 404s is\n// silently skipped).\n//\n// On success the applied marker is written so running surfaces can offer a\n// restart — see `auto-update-restart.ts`.\n// ---------------------------------------------------------------------------\n\nimport type { PackageManagerCommand, UpdateStatus } from './auto-update'\nimport type { CompanionSpec, InstalledCompanion } from './auto-update-companion'\nimport { Writable } from 'node:stream'\nimport {\n checkForUpdate,\n checkTarballAvailable,\n compareSemver,\n detectPackageManager,\n performSelfUpdate,\n reinstallHint,\n} from './auto-update'\nimport { findInstalledCompanion } from './auto-update-companion'\nimport { writeUpdateAppliedMarker } from './auto-update-restart'\n\n/** Outcome of upgrading one package. */\nexport interface UpgradePackageResult {\n /** npm package name. */\n packageName: string\n /** Version on disk before the run; `null` when not installed. */\n from: string | null\n /** Version the registry advertises; `null` when the check failed. */\n to: string | null\n /**\n * - `'updated'` — installed and verified on disk.\n * - `'up-to-date'` — nothing to do.\n * - `'not-installed'` — bin not on PATH; skipped silently.\n * - `'unavailable'` — published but the tarball is not downloadable yet.\n * - `'failed'` — registry error, PM failure, or the install did not land; see `error`.\n */\n status: 'updated' | 'up-to-date' | 'not-installed' | 'unavailable' | 'failed'\n /** The PM command that ran (or would run). */\n command?: string[]\n /** Why the upgrade did not complete. Set for `failed` and `unavailable`. */\n error?: string\n}\n\n/** Outcome of {@link upgradeInstalledPackages}. */\nexport interface UpgradeReport {\n /** One result per target package, in order. */\n results: UpgradePackageResult[]\n /** No `failed` / `unavailable` entries. */\n ok: boolean\n /** At least one package was actually installed — a restart is now pending. */\n installedAny: boolean\n}\n\n/** Progress event emitted while packages are upgraded: a version check, an install start, a line of package manager output, or a finished package. */\nexport type UpgradeProgressEvent\n = | { kind: 'check', packageName: string }\n | { kind: 'install', packageName: string, from: string, to: string, command: string[] }\n | { kind: 'log', packageName: string, stream: 'stdout' | 'stderr', line: string }\n | { kind: 'done', result: UpgradePackageResult }\n\n/** Options for {@link upgradeInstalledPackages}. */\nexport interface UpgradeInstalledPackagesOptions {\n /** Packages to keep in step, e.g. `[{ packageName: 'zidane-gui' }, { packageName: 'zidane-tui', binName: 'zidane' }]`. */\n targets: readonly CompanionSpec[]\n /** dist-tag to install. Defaults to `'latest'`. */\n channel?: string\n /** Registry base URL. Defaults to `https://registry.npmjs.org`. */\n registry?: string\n /** Where the registry cache and the applied marker live. */\n cacheDir?: string\n /** Install even when the registry check failed or reports up-to-date. */\n force?: boolean\n /** Abort signal. Targets not yet started are skipped once it aborts. */\n signal?: AbortSignal\n /** Called with check, install, log and done events as the upgrade runs. */\n onProgress?: (event: UpgradeProgressEvent) => void\n /** Injected for tests. */\n findInstalled?: typeof findInstalledCompanion\n /** Injected for tests. Defaults to {@link performSelfUpdate}. */\n performSelfUpdate?: typeof performSelfUpdate\n /** Injected for tests. Defaults to {@link checkTarballAvailable}. */\n checkTarballAvailable?: typeof checkTarballAvailable\n /** Injected for tests. Defaults to {@link checkForUpdate}. */\n checkForUpdate?: typeof checkForUpdate\n /** Injected for tests. Defaults to {@link detectPackageManager}. */\n detectPackageManager?: typeof detectPackageManager\n /** Injected for tests. Defaults to {@link writeUpdateAppliedMarker}. */\n writeMarker?: typeof writeUpdateAppliedMarker\n /** Injected for tests. Defaults to `Date.now`. */\n now?: () => number\n}\n\n/**\n * Upgrades each installed target package through its own package manager.\n * Skips targets whose bin is not on PATH, checks the registry for each one, reports `unavailable` instead of installing when the tarball is not downloadable yet, runs the install, and verifies the new version on disk. Writes the applied marker when something was installed and `cacheDir` is set.\n *\n * @param options - Target packages, registry settings and progress callback.\n * @example\n * ```ts\n * import { upgradeInstalledPackages } from 'zidane/chat'\n *\n * const report = await upgradeInstalledPackages({\n * targets: [{ packageName: 'zidane-tui', binName: 'zidane' }],\n * onProgress: event => console.log(event.kind),\n * })\n * if (!report.ok)\n * console.error(report.results.filter(r => r.status === 'failed'))\n * ```\n */\nexport async function upgradeInstalledPackages(options: UpgradeInstalledPackagesOptions): Promise<UpgradeReport> {\n const channel = options.channel ?? 'latest'\n const find = options.findInstalled ?? findInstalledCompanion\n const check = options.checkForUpdate ?? checkForUpdate\n const available = options.checkTarballAvailable ?? checkTarballAvailable\n const install = options.performSelfUpdate ?? performSelfUpdate\n const detect = options.detectPackageManager ?? detectPackageManager\n const progress = options.onProgress ?? (() => {})\n\n const results: UpgradePackageResult[] = []\n for (const spec of options.targets) {\n if (options.signal?.aborted)\n break\n const installed = find(spec)\n if (!installed) {\n results.push({ packageName: spec.packageName, from: null, to: null, status: 'not-installed' })\n continue\n }\n progress({ kind: 'check', packageName: spec.packageName })\n const status = await check({\n packageName: installed.packageName,\n currentVersion: installed.version,\n channel,\n registry: options.registry,\n cacheDir: options.cacheDir,\n force: true,\n timeoutMs: 8000,\n signal: options.signal,\n })\n const result = await upgradeOne(installed, status)\n progress({ kind: 'done', result })\n results.push(result)\n }\n\n const installedAny = results.some(r => r.status === 'updated')\n if (installedAny && options.cacheDir) {\n const writeMarker = options.writeMarker ?? writeUpdateAppliedMarker\n writeMarker(options.cacheDir, {\n at: (options.now ?? Date.now)(),\n packages: results\n .filter(r => r.status === 'updated' && r.to)\n .map(r => ({ packageName: r.packageName, version: r.to! })),\n })\n }\n return {\n results,\n ok: results.every(r => r.status !== 'failed' && r.status !== 'unavailable'),\n installedAny,\n }\n\n async function upgradeOne(installed: InstalledCompanion, status: UpdateStatus): Promise<UpgradePackageResult> {\n const base = { packageName: installed.packageName, from: installed.version, to: status.latest }\n if (status.source === 'error' && !options.force)\n return { ...base, status: 'failed', error: `registry check failed: ${status.error ?? 'unknown error'}` }\n const outdated = status.latest !== null && compareSemver(status.latest, installed.version) > 0\n if (!outdated && !options.force)\n return { ...base, status: 'up-to-date' }\n if (status.latest && !options.force) {\n const availability = await available({ packageName: installed.packageName, version: status.latest, registry: options.registry })\n if (availability === 'missing')\n return { ...base, status: 'unavailable', error: `${installed.packageName} ${status.latest} is published but not downloadable yet. Try again in a few minutes.` }\n }\n // The PM that owns THIS bin — npm/pnpm/yarn/bun/Volta differ per install.\n const command: PackageManagerCommand = detect({ packageName: installed.packageName, channel, binaryPath: installed.binaryPath })\n progress({ kind: 'install', packageName: installed.packageName, from: installed.version, to: status.latest ?? channel, command: command.argv })\n\n const outcome = await install({\n packageName: installed.packageName,\n channel,\n packageManager: command.id,\n signal: options.signal,\n stdout: lineSink(installed.packageName, 'stdout'),\n stderr: lineSink(installed.packageName, 'stderr'),\n // Quiet PM output: the UI shows one live line, not a progress bar dump.\n env: { npm_config_loglevel: 'error', npm_config_progress: 'false' },\n })\n if (outcome.spawnError)\n return { ...base, status: 'failed', command: command.argv, error: `could not run ${command.argv[0]}: ${outcome.spawnError}` }\n if (outcome.exitCode !== 0)\n return { ...base, status: 'failed', command: command.argv, error: `${command.argv[0]} exited with ${outcome.exitCode ?? 'unknown'}` }\n\n // Verify: what is on disk now vs. what we asked for.\n const after = find(installed)\n if (status.latest && after && compareSemver(after.version, status.latest) < 0) {\n return {\n ...base,\n status: 'failed',\n command: command.argv,\n error: `install finished but ${after.version} is on disk, expected ${status.latest}. Reinstall: ${reinstallHint(command, installed.packageName, channel)}`,\n }\n }\n return { ...base, to: after?.version ?? status.latest, status: 'updated', command: command.argv }\n }\n\n /** Split a PM's byte stream into trimmed lines for the progress sink. */\n function lineSink(packageName: string, stream: 'stdout' | 'stderr'): NodeJS.WritableStream {\n let buffer = ''\n const flush = (final: boolean) => {\n const parts = buffer.split(/\\r?\\n/)\n buffer = final ? '' : parts.pop() ?? ''\n for (const part of parts) {\n const line = part.trim()\n if (line)\n progress({ kind: 'log', packageName, stream, line })\n }\n }\n return new Writable({\n write(chunk: Uint8Array | string, _enc, cb) {\n buffer += chunk.toString()\n flush(false)\n cb()\n },\n final(cb) {\n flush(true)\n cb()\n },\n })\n }\n}\n","// ---------------------------------------------------------------------------\n// Automatic installs (`Settings.autoInstallUpdates`).\n//\n// When a background check finds a newer stable release, the host calls\n// `autoInstallUpdate`. It runs the same engine as the GUI's Install button\n// and `zidane upgrade` (`upgradeInstalledPackages`), under the shared install\n// lock (`auto-update-lock.ts`), and never restarts anything: the applied\n// marker it writes wakes the restart chip.\n//\n// A failed automatic install records its version in\n// `<cacheDir>/update-failed.json`. That version is never retried on its own;\n// a newer release, or a manual install (which clears the record), is.\n// ---------------------------------------------------------------------------\n\nimport type { UpdateStatus } from './auto-update'\nimport type { CompanionSpec } from './auto-update-companion'\nimport type { UpdateLockHolder, UpdateLockOptions } from './auto-update-lock'\nimport type { UpgradeInstalledPackagesOptions, UpgradeReport } from './auto-update-service'\nimport { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { parseSemver, updateChecksOptedOut } from './auto-update'\nimport { acquireUpdateLock } from './auto-update-lock'\nimport { upgradeInstalledPackages } from './auto-update-service'\n\n/** File name of the failed-install record inside the cache directory. */\nexport const UPDATE_FAILED_FILE = 'update-failed.json'\n\n/** The automatic install that failed last. */\nexport interface FailedUpdateRecord {\n /** Release the install tried to reach. */\n version: string\n /** Why it failed, one package per line. */\n error: string\n /** ms since epoch. */\n at: number\n}\n\n/**\n * Reads the failed-install record. `null` when there is none or it does not parse.\n *\n * @param cacheDir - Directory that holds the record.\n */\nexport function readFailedUpdate(cacheDir: string): FailedUpdateRecord | null {\n try {\n const raw = JSON.parse(readFileSync(join(cacheDir, UPDATE_FAILED_FILE), 'utf8')) as Partial<FailedUpdateRecord>\n if (typeof raw.version === 'string' && typeof raw.error === 'string' && typeof raw.at === 'number')\n return { version: raw.version, error: raw.error, at: raw.at }\n return null\n }\n catch {\n return null\n }\n}\n\n/**\n * Records a failed automatic install. Best-effort.\n *\n * @param cacheDir - Directory that holds the record. Created when missing.\n * @param record - What failed.\n */\nexport function writeFailedUpdate(cacheDir: string, record: FailedUpdateRecord): void {\n try {\n mkdirSync(cacheDir, { recursive: true })\n writeFileSync(join(cacheDir, UPDATE_FAILED_FILE), `${JSON.stringify(record, null, 2)}\\n`)\n }\n catch {\n // A missing record only means one more attempt later.\n }\n}\n\n/**\n * Forgets the failed install, so the next check may try again. Called by every\n * manual install.\n *\n * @param cacheDir - Directory that holds the record.\n */\nexport function clearFailedUpdate(cacheDir: string): void {\n try {\n unlinkSync(join(cacheDir, UPDATE_FAILED_FILE))\n }\n catch {\n // Nothing to clear.\n }\n}\n\n/** Why {@link autoInstallUpdate} did nothing. */\nexport type AutoInstallSkipReason\n /** No newer stable release on `latest`, or the env opts out. */\n = | 'not-eligible'\n /** This version failed before; wait for a newer one or a manual install. */\n | 'failed-before'\n /** Another install holds the lock. The installed-version watcher sees its result. */\n | 'locked'\n\n/** Outcome of {@link autoInstallUpdate}. */\nexport type AutoInstallOutcome\n = | { kind: 'installed', version: string, report: UpgradeReport }\n | { kind: 'skipped', reason: AutoInstallSkipReason, holder?: UpdateLockHolder }\n | { kind: 'failed', version: string, error: string }\n\n/** Options for {@link isAutoInstallCandidate}. */\nexport interface AutoInstallCandidateOptions {\n /** Directory that holds the failed-install record. */\n cacheDir: string\n /** Defaults to `process.env`. */\n env?: NodeJS.ProcessEnv\n}\n\n/**\n * Whether a check result is one an automatic install may act on: a newer\n * release on the `latest` dist-tag, not a prerelease, not opted out by env,\n * and not the version that failed last time.\n *\n * @param status - Result of the background check.\n * @param options - Cache directory and environment.\n * @returns `true`, or why not.\n */\nexport function isAutoInstallCandidate(status: UpdateStatus | null, options: AutoInstallCandidateOptions): true | AutoInstallSkipReason {\n if (!status?.hasUpdate || !status.latest || status.channel !== 'latest')\n return 'not-eligible'\n if (status.source === 'skipped' || status.source === 'error' || updateChecksOptedOut(options.env))\n return 'not-eligible'\n const semver = parseSemver(status.latest)\n if (!semver || semver.prerelease.length > 0)\n return 'not-eligible'\n if (readFailedUpdate(options.cacheDir)?.version === status.latest)\n return 'failed-before'\n return true\n}\n\n/** Options for {@link autoInstallUpdate}. */\nexport interface AutoInstallUpdateOptions {\n /** Result of the background check that found the release. */\n status: UpdateStatus\n /** Where the lock, the failed record, the registry cache and the applied marker live. */\n cacheDir: string\n /** Packages to keep in step. */\n targets: readonly CompanionSpec[]\n /** Who runs the install, recorded in the lock: `'desktop app'`, `'terminal app'`. */\n by: string\n /** Passed through to {@link upgradeInstalledPackages} (progress, PATH-aware probes). */\n upgradeOptions?: Omit<UpgradeInstalledPackagesOptions, 'targets' | 'cacheDir' | 'channel' | 'force'>\n /** Defaults to `process.env`. */\n env?: NodeJS.ProcessEnv\n /** Injected for tests. Defaults to {@link upgradeInstalledPackages}. */\n upgrade?: typeof upgradeInstalledPackages\n /** Injected for tests. */\n lock?: UpdateLockOptions\n /** Injected for tests. Defaults to `Date.now`. */\n now?: () => number\n}\n\n/**\n * Installs the release a background check found, unless something says not\n * to (see {@link isAutoInstallCandidate}) or another install holds the lock.\n * Never throws. Records a failure so the same version is not retried, and\n * clears the record on success.\n *\n * @param options - Check result, cache directory, targets and test seams.\n * @example\n * ```ts\n * const outcome = await autoInstallUpdate({ status, cacheDir, targets, by: 'terminal app' })\n * if (outcome.kind === 'failed')\n * log.warn('automatic update failed', { error: outcome.error })\n * ```\n */\nexport async function autoInstallUpdate(options: AutoInstallUpdateOptions): Promise<AutoInstallOutcome> {\n const { status, cacheDir } = options\n const now = options.now ?? Date.now\n const candidate = isAutoInstallCandidate(status, { cacheDir, ...(options.env ? { env: options.env } : {}) })\n if (candidate !== true)\n return { kind: 'skipped', reason: candidate }\n const version = status.latest!\n const lock = acquireUpdateLock(cacheDir, options.by, options.lock)\n if (!lock.acquired)\n return { kind: 'skipped', reason: 'locked', holder: lock.holder }\n try {\n const report = await (options.upgrade ?? upgradeInstalledPackages)({\n ...options.upgradeOptions,\n targets: options.targets,\n cacheDir,\n channel: 'latest',\n })\n const error = upgradeError(report)\n if (error) {\n writeFailedUpdate(cacheDir, { version, error, at: now() })\n return { kind: 'failed', version, error }\n }\n clearFailedUpdate(cacheDir)\n return { kind: 'installed', version, report }\n }\n catch (err) {\n const error = err instanceof Error ? err.message : String(err)\n writeFailedUpdate(cacheDir, { version, error, at: now() })\n return { kind: 'failed', version, error }\n }\n finally {\n lock.release()\n }\n}\n\n/**\n * Why an upgrade run did not land, one package per line; `null` when it did.\n * A run that found no package on PATH failed too: nothing was installed.\n *\n * @param report - Result of {@link upgradeInstalledPackages}.\n */\nexport function upgradeError(report: UpgradeReport): string | null {\n const failures = report.results.filter(r => r.status === 'failed' || r.status === 'unavailable')\n if (failures.length > 0)\n return failures.map(f => `${f.packageName}: ${f.error ?? f.status}`).join('\\n')\n if (report.results.every(r => r.status === 'not-installed'))\n return 'no zidane packages found on PATH'\n return null\n}\n","// ---------------------------------------------------------------------------\n// `useUpdateCheck` — renderer-agnostic React hook driving the auto-update\n// footer chip + any banner a host wants to wire.\n//\n// Lives in `zidane/chat` (not `zidane/tui`) so a GUI shell can consume the\n// same status. Pure React + the renderer-agnostic core in\n// `./auto-update`; no OpenTUI imports.\n// ---------------------------------------------------------------------------\n\nimport type { UpdateStatus } from './auto-update'\nimport type { AutoInstallOutcome } from './auto-update-auto-install'\nimport type { CompanionSpec } from './auto-update-companion'\nimport type { InstalledVersionState } from './auto-update-restart'\nimport type { Hint } from './hints'\nimport { useEffect, useRef, useState } from 'react'\nimport { checkForUpdate } from './auto-update'\nimport { autoInstallUpdate } from './auto-update-auto-install'\nimport { watchInstalledVersion } from './auto-update-restart'\n\n/** Options for {@link useUpdateCheck}. */\nexport interface UseUpdateCheckOptions {\n /** npm package name. e.g. `'zidane-tui'`. */\n packageName: string\n /** Inlined at build time. Skips the check if empty/falsy. */\n currentVersion: string\n /**\n * Master enable flag — usually `Settings.checkForUpdates`. When `false`\n * the hook short-circuits without scheduling any work AND clears the\n * last-known status so a stale chip doesn't linger after the user\n * disabled the feature.\n */\n enabled: boolean\n /** Directory under which the on-disk cache lives. Usually `paths.userDir`. */\n cacheDir?: string\n /** Registry base URL. Defaults to `https://registry.npmjs.org`. */\n registry?: string\n /** dist-tag to read. Defaults to `'latest'`. */\n channel?: string\n /**\n * Cache lifetime for the read-through. **Defaults to `0`** (always\n * fetch fresh) — the TUI chip is the user's source of truth for \"what\n * version of zidane is on npm right now\"; we'd rather pay one HTTPS\n * call per session than tell them yesterday's news for up to 24 hours.\n *\n * The on-disk cache is still WRITTEN with each fresh result (so\n * `zidane upgrade` and other tooling reads benefit) and still READ to\n * feed the registry's ETag (so the cold path can short-circuit on 304\n * — meaningful bandwidth win when nothing changed). Pass a positive\n * value (e.g. `3_600_000` for 1 h) to enable read-through caching for\n * a GUI shell that mounts the hook in many places.\n */\n cacheTtlMs?: number\n /**\n * Wall-clock delay before the first check fires. Defaults to 1 500 ms so\n * we never compete with the boot path's first paint. The check itself\n * is async, so this only affects WHEN the chip first appears — never\n * blocks anything else.\n */\n delayMs?: number\n /** Injected for tests. */\n now?: () => number\n /** Injected for tests. */\n fetcher?: typeof fetch\n}\n\n/**\n * Returns the current update status. The check fires:\n * - Once per mount (every TUI launch).\n * - Every time `enabled` flips from `false` → `true` (user re-enabling\n * the setting expects an immediate refresh).\n *\n * The check is async and deferred by `delayMs` so it never competes with\n * first paint. Result is `null` until the first check resolves; flipping\n * `enabled` to `false` resets to `null` so any chip rendered against\n * `hasUpdate` disappears immediately.\n *\n * The hook owns its own AbortController so an unmount cancels any in-flight\n * registry request cleanly.\n *\n * @param options - Package, current version, enable flag and cache settings for the check.\n */\nexport function useUpdateCheck(options: UseUpdateCheckOptions): UpdateStatus | null {\n const [status, setStatus] = useState<UpdateStatus | null>(null)\n // Ref-mirror so the effect doesn't re-run on every keystroke when the\n // host wraps the result into derived hint state.\n const optsRef = useRef(options)\n optsRef.current = options\n\n useEffect(() => {\n if (!options.enabled || !options.currentVersion) {\n // Disabling the setting must hide a chip that was already painted\n // — otherwise the user sees a stale `↑ vX.Y.Z` linger until the\n // next launch. React bails on identical-state updates so the\n // initial-mount case (status already null) costs nothing.\n setStatus(null)\n return\n }\n const controller = new AbortController()\n const timer = setTimeout(() => {\n void checkForUpdate({\n packageName: optsRef.current.packageName,\n currentVersion: optsRef.current.currentVersion,\n cacheDir: optsRef.current.cacheDir,\n registry: optsRef.current.registry,\n channel: optsRef.current.channel,\n // Default to TTL=0 (always fetch fresh). See `cacheTtlMs` docs.\n cacheTtlMs: optsRef.current.cacheTtlMs ?? 0,\n signal: controller.signal,\n now: optsRef.current.now,\n fetcher: optsRef.current.fetcher,\n }).then((s) => {\n if (!controller.signal.aborted)\n setStatus(s)\n })\n }, options.delayMs ?? 1500)\n return () => {\n clearTimeout(timer)\n controller.abort()\n }\n // Re-run when the master enable / package / version flip. Other\n // option changes stay inside `optsRef.current`; re-running on every\n // option mutation would cause render storms when the host wraps\n // the result into derived hint state.\n }, [options.enabled, options.packageName, options.currentVersion])\n\n return status\n}\n\n/** Options for {@link useAutoInstallUpdate}. */\nexport interface UseAutoInstallUpdateOptions {\n /** Result of {@link useUpdateCheck}. */\n status: UpdateStatus | null\n /** `Settings.checkForUpdates && Settings.autoInstallUpdates`, and the host allows background installs. */\n enabled: boolean\n /** Where the lock, the failed record and the applied marker live. Nothing runs without it. */\n cacheDir?: string\n /** Packages to keep in step. */\n targets: readonly CompanionSpec[]\n /** Who runs the install, recorded in the lock. */\n by: string\n /** Called once per attempt with what happened. */\n onOutcome?: (outcome: AutoInstallOutcome) => void\n /** Injected for tests. Defaults to {@link autoInstallUpdate}. */\n install?: typeof autoInstallUpdate\n}\n\n/**\n * Installs the release {@link useUpdateCheck} found, in the background, when\n * the user turned `autoInstallUpdates` on. One attempt per version per mount;\n * the check itself runs after first paint, so this never competes with it.\n * Never restarts anything: the applied marker wakes `useRestartRequired`.\n * Returns the last outcome, `null` before the first attempt.\n *\n * @param options - Check result, switches, cache directory and targets.\n */\nexport function useAutoInstallUpdate(options: UseAutoInstallUpdateOptions): AutoInstallOutcome | null {\n const [outcome, setOutcome] = useState<AutoInstallOutcome | null>(null)\n const optsRef = useRef(options)\n optsRef.current = options\n const tried = useRef(new Set<string>())\n const latest = options.status?.hasUpdate ? options.status.latest : null\n\n useEffect(() => {\n const { status, cacheDir } = optsRef.current\n if (!options.enabled || !cacheDir || !status || !latest || tried.current.has(latest))\n return\n tried.current.add(latest)\n let live = true\n // Not aborted on unmount: killing a package manager mid-install can\n // leave a broken global install behind.\n void (optsRef.current.install ?? autoInstallUpdate)({\n status,\n cacheDir,\n targets: optsRef.current.targets,\n by: optsRef.current.by,\n }).then((next) => {\n optsRef.current.onOutcome?.(next)\n if (live)\n setOutcome(next)\n })\n return () => {\n live = false\n }\n }, [options.enabled, options.cacheDir, latest])\n\n return outcome\n}\n\n// ---------------------------------------------------------------------------\n// Footer chip helper — pure function so any renderer (TUI Footer, future\n// GUI status bar) can compose against the same `Hint`. The chip is\n// suppressed when there's no update; callers spread the result into their\n// hint array unconditionally.\n//\n// Color contract: the chip rides whatever accent the host passes (typically\n// `theme.colors.accent`). Renderers that want a \"fresh check\" badge can\n// inspect `status.source === 'fresh'` themselves — this helper sticks to\n// the one-line discoverability surface.\n// ---------------------------------------------------------------------------\n\n/** Display options for the update and restart hints. */\nexport interface BuildUpdateHintOptions {\n /** Foreground color for the `↑` glyph + version label. */\n color: string\n /**\n * When true (default), the chip renders the literal version (`↑ v5.6.0`).\n * When false, just the glyph + `update` label — useful when the row is\n * tight and the version would push higher-priority chips off the right.\n */\n showVersion?: boolean\n /** Override the leading glyph. Defaults to `'↑'`. */\n glyph?: string\n}\n\n/**\n * Builds the `↑ vX.Y.Z` hint for an available update.\n * Returns `null` when there is no newer version.\n *\n * @param status - Result of an update check, or `null` before the first check.\n * @param options - Color and label settings for the hint.\n */\nexport function buildUpdateHint(status: UpdateStatus | null, options: BuildUpdateHintOptions): Hint | null {\n if (!status?.hasUpdate || !status.latest)\n return null\n const label = options.showVersion === false\n ? 'update'\n : `v${stripVPrefix(status.latest)}`\n return {\n key: options.glyph ?? '↑',\n keyColor: options.color,\n label,\n labelColor: options.color,\n }\n}\n\n/**\n * `↻ v7.0.31` — a newer build is on disk than the one running (this\n * process is stale; see `auto-update-restart.ts`). Takes precedence over\n * the `↑` chip: once the release is installed, \"available\" is old news and\n * \"restart\" is the one action left. Same color contract as `buildUpdateHint`.\n *\n * @param state - Installed-version state, or `null` when unknown.\n * @param options - Color and label settings for the hint.\n */\nexport function buildRestartHint(\n state: { restartRequired: boolean, installed: string | null } | null,\n options: BuildUpdateHintOptions,\n): Hint | null {\n if (!state?.restartRequired || !state.installed)\n return null\n return {\n key: options.glyph ?? '↻',\n keyColor: options.color,\n label: options.showVersion === false ? 'restart' : `v${stripVPrefix(state.installed)}`,\n labelColor: options.color,\n }\n}\n\nfunction stripVPrefix(v: string): string {\n return v.startsWith('v') ? v.slice(1) : v\n}\n\n// ---------------------------------------------------------------------------\n// `useRestartRequired` — the TUI's consumer of the installed-version watcher\n// (`./auto-update-restart`). The GUI runs the watcher in its main process and\n// forwards the state over IPC instead.\n// ---------------------------------------------------------------------------\n\n/** Options for {@link useRestartRequired}. */\nexport interface UseRestartRequiredOptions {\n /** The running package. `null` disables the hook. */\n spec: CompanionSpec | null\n /** Version the process was built with. */\n currentVersion: string\n /** Directory that holds the applied marker. Omit to rely on polling alone. */\n cacheDir?: string\n /** Master switch — mirrors `Settings.checkForUpdates`. */\n enabled: boolean\n /** Poll interval in milliseconds. Defaults to 5 minutes. `0` disables polling. */\n pollMs?: number\n}\n\n/**\n * `null` until the first probe (or when disabled / no spec), then the live\n * state. Flipping `enabled` off tears the watcher down and clears the state\n * so no stale \"restart\" chip lingers.\n *\n * @param options - Package spec, running version, cache directory, poll interval and the master `enabled` switch.\n */\nexport function useRestartRequired(options: UseRestartRequiredOptions): InstalledVersionState | null {\n const [state, setState] = useState<InstalledVersionState | null>(null)\n const { spec, currentVersion, cacheDir, enabled, pollMs } = options\n const packageName = spec?.packageName\n const binName = spec?.binName\n\n useEffect(() => {\n if (!enabled || !packageName || !currentVersion) {\n setState(null)\n return\n }\n const watcher = watchInstalledVersion({\n spec: binName ? { packageName, binName } : { packageName },\n currentVersion,\n cacheDir,\n pollMs,\n onChange: setState,\n })\n setState(watcher.current())\n return () => watcher.close()\n }, [enabled, packageName, binName, currentVersion, cacheDir, pollMs])\n\n return state\n}\n","// ---------------------------------------------------------------------------\n// Footer hint composition.\n//\n// Pure presentation logic — given the current screen, run state, and a\n// resolved keybinding map, produce the ordered list of {@link Hint}s\n// the bottom-bar Footer should render. Lives in `zidane/chat` (not\n// `zidane/tui`) because it's renderer-agnostic: the only types involved\n// are `Hint`, `Screen`, `SessionMeta`, `Settings['uiMode']`, and `KeyBindings`. A GUI\n// shell can consume the same list and paint it however it likes.\n//\n// Priority order is intentional and load-bearing:\n//\n// pending approval > live pending interaction > resumed pending\n// interaction > busy (streaming) > auth screen > onboarding screen (no\n// hints) > sessions screen > chat screen (idle).\n//\n// The first four return state-specific hint sets unconditionally —\n// `uiMode` is ignored there because those affordances (approve / abort\n// / navigate) are not the noise the minimal mode is targeting. Minimal\n// mode only collapses the chat-idle row.\n// ---------------------------------------------------------------------------\n\nimport type { Hint } from './hints'\nimport type { KeyBindings } from './keybindings'\nimport type { Screen, SessionMeta, Settings } from './types'\nimport { formatBindingForDisplay } from './keybindings'\n\n/**\n * Options bag for {@link buildHints}. Flat shape so call sites read as a\n * checklist of \"what state do I have to surface\" — easy to thread\n * through React `useMemo` deps and to construct fresh in tests.\n */\nexport interface BuildHintsOptions {\n /** Screen the footer is shown on. */\n screen: Screen\n /** True while the agent is running a turn. */\n busy: boolean\n /** True while a safe-mode approval prompt is pending. */\n pending: boolean\n /**\n * True when a LIVE pending interaction is on screen (agent is mid-run,\n * awaiting the tool's Promise). Same nav hints as the safe-mode\n * approval picker — `abortRun` aborts the run.\n */\n pendingInteractionLive: boolean\n /**\n * True when the chat screen is showing a resumed (not live) pending\n * interaction. The agent isn't running; esc leaves the form without\n * persisting a response, so the hint reads \"leave for later\" — the\n * session re-enqueues the same interaction on next activation.\n */\n pendingInteractionResumed: boolean\n /** Active session, or `null` when none is open. */\n currentSession: SessionMeta | null\n /** True when more than one agent profile is available, so the agent switch hint is useful. */\n hasMultipleAgents: boolean\n /**\n * Chat-screen chrome density — `'minimal'` collapses the idle chat\n * hint row to just `agent / model / settings`. Busy and pending\n * states keep their full hint sets because those\n * affordances are load-bearing (abort, navigate, approve).\n */\n uiMode: Settings['uiMode']\n /** Label of the active model, or `null` when none is selected. */\n modelLabel: string | null\n /** Foreground color for the model label. */\n modelColor: string\n /** Current reasoning effort label, or `null` when the model has no reasoning knob. */\n effortLabel: string | null\n /** Foreground color for the effort label. */\n effortColor: string\n /**\n * Foreground for the `/l` chord that introduces the effort label. Unset, it\n * takes the color of the model key it extends, so the pair reads as one binding.\n */\n effortKeyColor?: string\n /**\n * Comma-joined labels of the enabled model options (e.g. `'fast'`), or `null`\n * when none are on. Appended after the effort label in the model chord — when\n * the model has no reasoning knob, it rides the chord on its own.\n */\n modelOptionsLabel?: string | null\n /** Label of the active agent profile. */\n agentLabel: string\n /** Foreground color for the agent label. */\n agentColor: string\n /**\n * Resolved keybindings — every hint key string flows through this so\n * a user-level rebind (e.g. `\"openSettings\": \"ctrl+p\"`) shows up live\n * in the bottom bar without us hard-coding the default `'ctrl+o'`.\n */\n keybindings: KeyBindings\n /**\n * Number of currently-dispatching tool calls. Drives the\n * `<cancelToolCall> cancel` chip in the `busy` hint row and the\n * `cancel task` chip on the idle chat row. Without\n * it, the binding stays inert and there's no point advertising the\n * shortcut. Falls back to \"no chip\" when no tools are in flight\n * (model is mid-stream but hasn't dispatched yet).\n */\n inFlightToolCount: number\n /**\n * Number of currently-active skills. Drives the footer's\n * `✦ N skill(s)` chip on the chat screen — purely informational,\n * a passive surface for \"tool restrictions are in effect\". Hidden\n * when `0` so the row stays uncluttered for the common case.\n */\n activeSkillCount: number\n /** Foreground for the skills chip (typically `COLOR.brand`). */\n skillsChipColor: string\n /**\n * Progress label for the todo chip (`\"2/5\"` from `todoProgressLabel`), or\n * `null` to hide it.\n *\n * As a `Hint` it inherits the row's click-to-keypress\n * contract for free (`dispatchHintKey`), so clicking it opens the list the\n * same way `ctrl+t` does — and it sits below the prompt like every other\n * control.\n */\n todoProgress: string | null\n /**\n * Pre-built `↑ vX.Y.Z` chip from `buildUpdateHint(useUpdateCheck())`,\n * or `null` when no update is available. Always last in the chat-screen\n * row so it's the first to drop when the terminal narrows.\n */\n updateHint: Hint | null\n /**\n * Pre-built prompt-cache toast, non-null only for a few seconds after a\n * `'cache:break'` hook fired (the host owns the visibility timer and the\n * severity split): warn-colored `⚡ cache break in <culprit>: +Nk tok, ~$0.48`\n * for attributable breaks, dim `⚡ cache expired: +Nk tok, ~$0.48` for\n * byte-stable TTL expiry — routine across human think-time, so it must not\n * cry wolf. Sits beside `updateHint` at the droppable end of the row: a\n * transient cost signal, not an action.\n */\n cacheBreakHint?: Hint | null\n /**\n * Sessions-screen list scope — `true` when the list spans all\n * projects. Drives the `tab` hint's label, which advertises the scope\n * the toggle switches TO. Optional so non-sessions callers can omit it.\n */\n sessionsScopeAll?: boolean\n /** Sessions screen: plain ←/→ switch spaces (Spaces on, 2+ spaces). Adds the `←→ space` hint. */\n sessionsSpaceSwitch?: boolean\n}\n\n/**\n * Build the footer's shortcut hints for the current screen. On the chat\n * screen the model id rides next to its `ctrl+m` shortcut and the agent\n * label rides next to `shift+tab`, each in its accent color — the bar\n * doubles as the status display without needing separate badges. When\n * the active model exposes reasoning, the `ctrl+m` hint grows a\n * secondary `/l` chord with the current effort label, surfacing the\n * effort picker as a discoverable, in-place affordance.\n *\n * @param options - Screen, run state, labels, colors, and keybindings that decide which hints appear.\n */\nexport function buildHints(options: BuildHintsOptions): Hint[] {\n const {\n screen,\n busy,\n pending,\n pendingInteractionLive,\n pendingInteractionResumed,\n currentSession,\n hasMultipleAgents,\n uiMode,\n modelLabel,\n modelColor,\n effortLabel,\n effortColor,\n effortKeyColor,\n modelOptionsLabel,\n agentLabel,\n agentColor,\n keybindings,\n inFlightToolCount,\n activeSkillCount,\n skillsChipColor,\n todoProgress,\n updateHint,\n cacheBreakHint = null,\n sessionsScopeAll,\n sessionsSpaceSwitch,\n } = options\n\n // Todo progress chip. Built BEFORE the priority branches below because it is\n // STATUS, not a shortcut: it has to survive every one of them. It reports\n // todo progress independent of `busy`, and an `in_progress` todo is set\n // precisely while the agent is working and calling `todowrite`. Building\n // it after the early returns would hide it for the whole window it exists\n // to cover.\n //\n // Appended LAST in each branch so `clipHintsToWidth` (which drops from the\n // right) sheds it before the abort/navigate affordances, which must stay.\n const todosChip: Hint | null = todoProgress !== null\n ? { key: keybindings.openTodos, label: `${todoProgress} todos` }\n : null\n const withTodos = (hints: Hint[]): Hint[] => todosChip ? [...hints, todosChip] : hints\n\n // Priority: safe-mode approval > live interaction > resumed\n // interaction > plain streaming. All three pending shapes share the\n // ↑↓ / ↵ nav prefix; only the esc semantics differ.\n if (pending)\n return withTodos([{ key: '↑↓', label: 'navigate' }, { key: '↵', label: 'select' }, { key: formatBindingForDisplay(keybindings.abortRun), label: 'abort run' }])\n if (pendingInteractionLive)\n return withTodos([{ key: '↑↓', label: 'navigate' }, { key: '↵', label: 'select' }, { key: formatBindingForDisplay(keybindings.abortRun), label: 'abort run' }])\n if (pendingInteractionResumed)\n return withTodos([{ key: '↑↓', label: 'navigate' }, { key: '↵', label: 'select' }, { key: 'esc', label: 'leave for later' }])\n if (busy) {\n // The run-wide abort, then the way out. The per-call cancel\n // (`cancelToolCall`) stays bound but unadvertised mid-run: the abort\n // covers the common case, and the chip only crowded the row.\n const baseBusyHints: Hint[] = []\n baseBusyHints.push({ key: formatBindingForDisplay(keybindings.abortRun), label: 'abort' })\n baseBusyHints.push({ key: 'esc', label: 'sessions' })\n // The model stays in view during the run it serves (a fallback shows in\n // its warn colour exactly then); no key, since the picker waits for idle.\n if (modelLabel)\n baseBusyHints.push({ key: '', label: modelLabel, labelColor: modelColor })\n return withTodos(baseBusyHints)\n }\n if (screen === 'auth')\n return [{ key: '↑↓', label: 'navigate' }, { key: '↵', label: 'select' }, { key: 'esc', label: 'exit' }]\n // The setup wizard shows the keys of each step inside its frame.\n if (screen === 'onboarding')\n return []\n if (screen === 'sessions') {\n return [\n { key: '↑↓', label: 'navigate' },\n { key: '↵', label: 'open' },\n { key: 'tab', label: sessionsScopeAll ? 'current project' : 'all projects' },\n ...(sessionsSpaceSwitch ? [{ key: '←→', label: 'space' }] : []),\n { key: keybindings.openSessionDetails, label: 'session' },\n { key: keybindings.openSettings, label: 'settings' },\n { key: 'esc', label: currentSession ? 'back' : 'exit' },\n ]\n }\n // Chat screen. Prompt-input shortcuts (`↵ send`, `shift+↵ newline`,\n // `↑↓ history`) live in the prompt box's overlay title — the bottom\n // bar only carries the most common agent / model / global affordances. The\n // `cycleAgent` binding shows the active label inline; the\n // `openModelPicker` binding shows the active model id inline; etc.\n // `cycleAgent` is suppressed when there's only one profile and\n // `openSessionDetails` only renders once a session exists.\n //\n // Minimal UI mode: strip the chat-idle bottom bar to just `agent`\n // (when there's >1 profile), `model/effort`, and `settings`. The\n // full keybinding catalog stays reachable from inside settings.\n // Busy / pending / select-turn / queue branches above already exited\n // before this point — minimal only affects the resting chat row.\n //\n // Compose the model hint. When the active model supports reasoning,\n // the primary `<openModelPicker> model` pair grows a secondary\n // `/l effort` chord that doubles as the discoverability hint for the\n // effort picker (`openEffortPicker`) and the status display for the\n // current effort. The chord shares the model accent so the pair\n // reads as one keyboard binding across every theme.\n // The `/l` chord (from `openEffortPicker`) carries reasoning effort plus any enabled model options,\n // all space-separated so they read as one model-info run (e.g.\n // `claude-opus-4-8 medium fast`). When the model has no reasoning knob, the\n // options ride the chord on their own.\n const chordLabel = [effortLabel, modelOptionsLabel].filter(Boolean).join(' ') || null\n const modelHint: Hint | null = modelLabel\n ? {\n key: keybindings.openModelPicker,\n label: modelLabel,\n labelColor: modelColor,\n ...(chordLabel\n ? { extra: { key: shortChord(keybindings.openEffortPicker), keySpec: keybindings.openEffortPicker, ...(effortKeyColor ? { keyColor: effortKeyColor } : {}), label: chordLabel, labelColor: effortColor } }\n : {}),\n }\n : null\n // Skills chip — purely informational, no shortcut wired. We re-use\n // the Hint shape's `key` slot to render the glyph (`✦`) so the row\n // layout (key + label, padded breakpoints) stays consistent with\n // every other entry. The chip is omitted when no skills are active\n // so the row stays uncluttered for the common case.\n const skillsChip: Hint | null = activeSkillCount > 0\n ? {\n key: '✦',\n keyColor: skillsChipColor,\n label: activeSkillCount === 1 ? '1 skill' : `${activeSkillCount} skills`,\n labelColor: skillsChipColor,\n }\n : null\n // Background-task chip — fires when the user is idle (not `busy`)\n // but a task is still running. Gives them a discoverable way to\n // reach `ctrl+k` for kill. The `busy` branch above already covers\n // the dispatching case; this one is for \"agent finished its turn,\n // task still chugging\".\n const cancelTaskChip: Hint | null = inFlightToolCount > 0\n ? {\n key: keybindings.cancelToolCall,\n label: inFlightToolCount === 1 ? 'cancel task' : `cancel task (${inFlightToolCount})`,\n }\n : null\n // Minimal mode short-circuits the full composition. Same agent / model\n // hints as the full row (so a multi-agent setup still cycles, and the\n // model + effort chord stays visible), plus `settings` as the\n // discoverability anchor for everything we just dropped (keybindings\n // included — they have a tab in there).\n if (uiMode === 'minimal') {\n return [\n ...(hasMultipleAgents ? [{ key: keybindings.cycleAgent, label: agentLabel, labelColor: agentColor }] : []),\n ...(modelHint ? [modelHint] : []),\n { key: keybindings.openSettings, label: 'settings' },\n ]\n }\n return [\n ...(hasMultipleAgents ? [{ key: keybindings.cycleAgent, label: agentLabel, labelColor: agentColor }] : []),\n ...(modelHint ? [modelHint] : []),\n ...(skillsChip ? [skillsChip] : []),\n ...(todosChip ? [todosChip] : []),\n ...(cancelTaskChip ? [cancelTaskChip] : []),\n ...(currentSession ? [{ key: keybindings.openSessionDetails, label: 'session' }] : []),\n { key: keybindings.openSettings, label: 'settings' },\n { key: 'esc', label: 'sessions' },\n // Update chip lives at the right-most position so `clipHintsToWidth`\n // drops it first on narrow terminals — discoverability is nice but\n // the action shortcuts must stay.\n ...(cacheBreakHint ? [cacheBreakHint] : []),\n ...(updateHint ? [updateHint] : []),\n ]\n}\n\n/**\n * Shorten a binding spec for display as a \"chord continuation\" — the\n * `/l` after `ctrl+m model` in the chat footer. We only strip the\n * `ctrl+` prefix so user-customized chords (`alt+n`, `meta+shift+n`)\n * still render in full. Falls back to the verbatim spec when no\n * `ctrl+` prefix is found, which keeps the visual contract honest:\n * the rendered key always matches the bound trigger.\n *\n * @param spec - Binding spec, such as `ctrl+n`.\n */\nexport function shortChord(spec: string): string {\n return spec.startsWith('ctrl+') ? `/${spec.slice('ctrl+'.length)}` : spec\n}\n","/**\n * @-completion path display formatter.\n *\n * The `@`-popover walks the project from the git root so the user sees\n * the whole tree regardless of where they launched the TUI. The\n * agent's CWD-resolving tools (`read_file`, `glob`, `edit`, …) read\n * relative paths against `process.cwd()`, which is frequently a\n * subdirectory of that root. Emitting the discovery's raw\n * project-root-relative path into the prompt produces a `<cwd>/<rel>`\n * miss — the bug this helper exists to close.\n *\n * Rule:\n *\n * - File at `cwd` itself → `.`.\n * - File under `cwd` (subtree) → forward-slashed CWD-relative,\n * no `..`.\n * - File above `cwd` but still inside the project → CWD-relative\n * with `..` traversal (e.g. `../EDIT_THIS.md`). Shorter and more\n * scannable than absolute, and the agent's tools resolve both.\n * - File outside the project (input was passed absolute) → returned\n * verbatim. Inputs that arrive as project-root-relative strings\n * are always inside the project by construction, so this branch\n * only kicks in when a direct caller hands the function an\n * absolute target.\n *\n * Pure, no I/O, deterministic.\n */\n\nimport { isAbsolute, posix, relative, resolve, sep } from 'node:path'\n\n/**\n * Convert a project-root-relative path (as emitted by `listProjectFiles`)\n * into the form the agent's tools — and the user — will actually find\n * given the current `cwd`.\n *\n * Inputs:\n * - `projectRelativePath` — forward-slashed, no leading slash. Returned\n * verbatim when it's empty or already absolute (defensive — the\n * absolute-input branch is the de-facto \"outside the project\"\n * escape; the catalog itself never emits absolute paths).\n * - `projectRoot` — absolute path of the discovery anchor.\n * - `cwd` — absolute path of `process.cwd()` at the moment of\n * insertion.\n *\n * Output is always either an absolute path (only when the input arrived\n * absolute) OR a CWD-relative path. The relative form may include\n * `../` segments when the target sits above `cwd` within the project,\n * which keeps cross-tree references short and scannable\n * (e.g. `../EDIT_THIS.md` instead of `/Users/.../zidane/EDIT_THIS.md`).\n *\n * @param projectRelativePath - Forward-slashed path relative to `projectRoot`.\n * @param projectRoot - Absolute path of the discovery anchor.\n * @param cwd - Absolute working directory the result is relative to.\n */\nexport function formatPathForCwd(\n projectRelativePath: string,\n projectRoot: string,\n cwd: string,\n): string {\n if (projectRelativePath.length === 0)\n return projectRelativePath\n if (isAbsolute(projectRelativePath))\n return projectRelativePath\n const target = resolve(projectRoot, projectRelativePath)\n const here = resolve(cwd)\n if (target === here)\n return '.'\n const rel = relative(here, target)\n if (rel.length === 0)\n return '.'\n return sep === '/' ? rel : rel.split(sep).join(posix.sep)\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as createSyncEngine } from "./engine-
|
|
2
|
-
import { r as startWsSavedHosts } from "./ws-
|
|
3
|
-
import { i as wsTransport, t as hostSignalHandlers } from "./host-signals-
|
|
1
|
+
import { t as createSyncEngine } from "./engine-D9gwgitb.js";
|
|
2
|
+
import { r as startWsSavedHosts } from "./ws-VPYC0cXG.js";
|
|
3
|
+
import { i as wsTransport, t as hostSignalHandlers } from "./host-signals-DEYsDUTY.js";
|
|
4
4
|
//#region src/tui/host/remote-hosts.ts
|
|
5
5
|
/** The TUI's directory, as the shared saved-hosts sync sees it. */
|
|
6
6
|
function savedHostsDirectory(directory) {
|
|
@@ -95,4 +95,4 @@ function startRemoteHosts(options, registry) {
|
|
|
95
95
|
//#endregion
|
|
96
96
|
export { startRemoteHosts };
|
|
97
97
|
|
|
98
|
-
//# sourceMappingURL=remote-hosts-
|
|
98
|
+
//# sourceMappingURL=remote-hosts-z6lSKQIV.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-hosts-
|
|
1
|
+
{"version":3,"file":"remote-hosts-z6lSKQIV.js","names":[],"sources":["../src/tui/host/remote-hosts.ts"],"sourcesContent":["/**\n * Saved servers in the window's host directory: loaded only when the\n * `remoteHosts` setting is on (`use-remote-hosts` imports this module\n * dynamically), so a window with it off never reads `hosts.json`, never\n * loads the WebSocket client and never dials anything.\n *\n * The registry ↔ directory sync and the connection loops are the shared\n * ones (`startWsSavedHosts`, `src/ipc/hosts`, the GUI's too); this module is\n * the TUI's glue: a directory entry per server carries a WebSocket\n * transport (the session controllers and session data dial through it),\n * and the server's presence and session changes feed the window.\n */\nimport type { HostEntry, HostRegistry } from '../../chat/hosts'\nimport type { SyncEngine, SyncEngineOptions } from '../../chat/sync/engine'\nimport type { SavedHostsController, SavedHostsDirectory } from '../../ipc/hosts'\nimport type { SessionPresence } from '../../ipc/index'\nimport type { HostDirectory } from './host-directory'\nimport { createSyncEngine } from '../../chat/sync/engine'\nimport { startWsSavedHosts } from '../../ipc/hosts'\nimport { wsTransport } from '../runtime/transport'\nimport { hostSignalHandlers } from './host-signals'\n\nexport interface RemoteHostsOptions {\n dataDir: string\n directory: HostDirectory\n onPresence?: (key: string, rows: readonly SessionPresence[]) => void\n /** A server (re)connected: its sessions can be listed. */\n onConnected?: (key: string) => void\n /** A server's sessions changed (another window created, renamed or deleted one). */\n onSessionsChanged?: (key: string) => void\n /** Settings sync to the saved servers (`src/chat/sync`): this computer's files. Absent: no sync. */\n sync?: Omit<SyncEngineOptions, 'dataDir'>\n}\n\nexport type RemoteHostsController = SavedHostsController & {\n /** Settings sync to each saved server (Settings → Hosts → a server → Sync). */\n sync?: SyncEngine | null\n}\n\n/** The TUI's directory, as the shared saved-hosts sync sees it. */\nfunction savedHostsDirectory(directory: HostDirectory): SavedHostsDirectory {\n return {\n saved: () => directory.servers().filter(host => !host.adHoc).map(host => ({ key: host.key, url: host.url ?? '' })),\n upsert: (entry: HostEntry, token) => {\n // Every dial through it (the connection loop, session data, controllers) checks it is still this server.\n const transport = wsTransport({ url: entry.url, expectHostId: entry.id, ...(token ? { token } : {}) })\n const known = directory.get(entry.id)\n if (known) {\n // In place: the entry (and the sessions routed to it) stays.\n directory.update(entry.id, { name: entry.name, url: entry.url, transport, client: null, status: 'connecting', error: undefined })\n return\n }\n directory.put({\n key: entry.id,\n kind: 'server',\n name: entry.name,\n url: entry.url,\n transport,\n client: null,\n status: 'connecting',\n ...(entry.serverName ? { serverName: entry.serverName } : {}),\n ...(entry.serverVersion ? { serverVersion: entry.serverVersion } : {}),\n })\n },\n update: (key, patch) => directory.update(key, patch),\n remove: key => directory.remove(key),\n }\n}\n\nexport function startRemoteHosts(options: RemoteHostsOptions, registry?: HostRegistry): RemoteHostsController {\n const { directory } = options\n /** `--host` already names this server: its saved entry would be a second connection to it. */\n const adHocUrl = directory.servers().find(host => host.adHoc)?.url\n const sync = options.sync ? createSyncEngine({ dataDir: options.dataDir, ...options.sync }) : null\n const handlers = hostSignalHandlers(options)\n const controller = startWsSavedHosts({\n dataDir: options.dataDir,\n ...(registry ? { registry } : {}),\n directory: savedHostsDirectory(directory),\n skip: entry => entry.url === adHocUrl,\n connect: (entry, _token, attempt) => {\n const host = directory.get(entry.id)\n if (!host)\n return Promise.reject(new Error(`${entry.name} is no longer in this window's hosts.`))\n return host.transport.connect({ appVersion: 'tui-hosts', passive: true, ...attempt })\n },\n onSignal: (key, signal) => {\n handlers.onSignal(key, signal)\n if (signal.kind === 'sync:changed')\n sync?.serverChanged(key)\n },\n onConnected: (key, client) => {\n handlers.onConnected(key, client)\n sync?.connected(key, client)\n },\n onDisconnected: (key) => {\n handlers.onDisconnected(key)\n sync?.disconnected(key)\n },\n })\n return {\n ...controller,\n // Removing a server keeps what it has; this computer stops syncing to it.\n remove: (id) => {\n const removed = controller.remove(id)\n if (removed)\n sync?.removed(id)\n return removed\n },\n stop: () => {\n sync?.stop()\n controller.stop()\n },\n sync,\n }\n}\n"],"mappings":";;;;;AAwCA,SAAS,oBAAoB,WAA+C;CAC1E,OAAO;EACL,aAAa,UAAU,QAAQ,CAAC,CAAC,QAAO,SAAQ,CAAC,KAAK,KAAK,CAAC,CAAC,KAAI,UAAS;GAAE,KAAK,KAAK;GAAK,KAAK,KAAK,OAAO;EAAG,EAAE;EACjH,SAAS,OAAkB,UAAU;GAEnC,MAAM,YAAY,YAAY;IAAE,KAAK,MAAM;IAAK,cAAc,MAAM;IAAI,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;GAAG,CAAC;GAErG,IADc,UAAU,IAAI,MAAM,EAC1B,GAAG;IAET,UAAU,OAAO,MAAM,IAAI;KAAE,MAAM,MAAM;KAAM,KAAK,MAAM;KAAK;KAAW,QAAQ;KAAM,QAAQ;KAAc,OAAO,KAAA;IAAU,CAAC;IAChI;GACF;GACA,UAAU,IAAI;IACZ,KAAK,MAAM;IACX,MAAM;IACN,MAAM,MAAM;IACZ,KAAK,MAAM;IACX;IACA,QAAQ;IACR,QAAQ;IACR,GAAI,MAAM,aAAa,EAAE,YAAY,MAAM,WAAW,IAAI,CAAC;IAC3D,GAAI,MAAM,gBAAgB,EAAE,eAAe,MAAM,cAAc,IAAI,CAAC;GACtE,CAAC;EACH;EACA,SAAS,KAAK,UAAU,UAAU,OAAO,KAAK,KAAK;EACnD,SAAQ,QAAO,UAAU,OAAO,GAAG;CACrC;AACF;AAEA,SAAgB,iBAAiB,SAA6B,UAAgD;CAC5G,MAAM,EAAE,cAAc;;CAEtB,MAAM,WAAW,UAAU,QAAQ,CAAC,CAAC,MAAK,SAAQ,KAAK,KAAK,CAAC,EAAE;CAC/D,MAAM,OAAO,QAAQ,OAAO,iBAAiB;EAAE,SAAS,QAAQ;EAAS,GAAG,QAAQ;CAAK,CAAC,IAAI;CAC9F,MAAM,WAAW,mBAAmB,OAAO;CAC3C,MAAM,aAAa,kBAAkB;EACnC,SAAS,QAAQ;EACjB,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAC/B,WAAW,oBAAoB,SAAS;EACxC,OAAM,UAAS,MAAM,QAAQ;EAC7B,UAAU,OAAO,QAAQ,YAAY;GACnC,MAAM,OAAO,UAAU,IAAI,MAAM,EAAE;GACnC,IAAI,CAAC,MACH,OAAO,QAAQ,uBAAO,IAAI,MAAM,GAAG,MAAM,KAAK,sCAAsC,CAAC;GACvF,OAAO,KAAK,UAAU,QAAQ;IAAE,YAAY;IAAa,SAAS;IAAM,GAAG;GAAQ,CAAC;EACtF;EACA,WAAW,KAAK,WAAW;GACzB,SAAS,SAAS,KAAK,MAAM;GAC7B,IAAI,OAAO,SAAS,gBAClB,MAAM,cAAc,GAAG;EAC3B;EACA,cAAc,KAAK,WAAW;GAC5B,SAAS,YAAY,KAAK,MAAM;GAChC,MAAM,UAAU,KAAK,MAAM;EAC7B;EACA,iBAAiB,QAAQ;GACvB,SAAS,eAAe,GAAG;GAC3B,MAAM,aAAa,GAAG;EACxB;CACF,CAAC;CACD,OAAO;EACL,GAAG;EAEH,SAAS,OAAO;GACd,MAAM,UAAU,WAAW,OAAO,EAAE;GACpC,IAAI,SACF,MAAM,QAAQ,EAAE;GAClB,OAAO;EACT;EACA,YAAY;GACV,MAAM,KAAK;GACX,WAAW,KAAK;EAClB;EACA;CACF;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as compareVersions, t as ZIDANE_VERSION } from "./version-
|
|
1
|
+
import { n as compareVersions, t as ZIDANE_VERSION } from "./version-B57fToQH.js";
|
|
2
2
|
import { t as resolveStorageDirs } from "./xdg-DFSAQQG8.js";
|
|
3
3
|
import { n as JsonRpcRemoteError, t as JsonRpcConnectionClosedError } from "./json-rpc-G0rBfEar.js";
|
|
4
4
|
import { r as isWindowOnlySetting, t as HOST_ENABLED_LISTS } from "./host-scoped-settings-CLPvpPwd.js";
|
|
@@ -395,4 +395,4 @@ function isSyncableSetting(key) {
|
|
|
395
395
|
//#endregion
|
|
396
396
|
export { createSignalListeners as a, daemonSocketName as c, legacySocketPath as d, probeSocket as f, IpcVersionMismatchError as i, defaultSocketPath as l, hashValue as n, openIpcClient as o, selfDaemonSpawnCommand as p, isRecord as r, IPC_BUILD_ID as s, isSyncableSetting as t, ensureDaemonRunning as u };
|
|
397
397
|
|
|
398
|
-
//# sourceMappingURL=settings-keys-
|
|
398
|
+
//# sourceMappingURL=settings-keys-EtuJbE_K.js.map
|