/** * Frontend mirror of the server's MinQuickCreateCLIVersion gate. The * agent-create flow (Quick Create modal) requires the daemon's bundled * multica CLI to be at least this version — older daemons either * double-create issues on partial CLI failures, drop quick-create attachment * bindings, or mishandle pasted screenshot URLs (see PR #1851 / MUL-1496). * * Both the frontend pre-validation in the modal and the server's * `/api/issues/quick-create` handler enforce this; the server is the * authoritative trust boundary, the frontend just lets us tell the user * "your daemon needs an upgrade" before they hit submit. */ export const MIN_QUICK_CREATE_CLI_VERSION = "0.2.21"; export const MIN_QUICK_CREATE_FIELDS_CLI_VERSION = "0.4.3"; export type CliVersionState = "ok" | "too_old" | "missing"; export interface CliVersionCheck { state: CliVersionState; /** What the daemon reported, or empty if missing/unparsable. */ current: string; /** The hard minimum we gate on. */ min: string; } const SEMVER_RE = /v?(\d+)\.(\d+)\.(\d+)/; // Matches the `git describe --tags --always --dirty` output for a build past // the latest tag, e.g. `v0.2.15-235-gdaf0e935` or `v0.2.15-235-gdaf0e935-dirty`. // Daemons built from source (Makefile `make build` / `make daemon`) report this // shape; tagged releases are bare semver. Treating dev-described daemons as OK // is what keeps `pnpm dev:desktop` + `make daemon` unblocked without weakening // the gate for staging or production users running stale stable releases. const DEV_DESCRIBE_RE = /^v?\d+\.\d+\.\d+-\d+-g[0-9a-fA-F]+/; function parseSemver(raw: string): [number, number, number] | null { const m = SEMVER_RE.exec(raw.trim()); if (!m) return null; return [Number(m[1]), Number(m[2]), Number(m[3])]; } function lessThan(a: [number, number, number], b: [number, number, number]) { if (a[0] !== b[0]) return a[0] < b[0]; if (a[1] !== b[1]) return a[1] < b[1]; return a[2] < b[2]; } /** * Check a daemon-reported CLI version string against the minimum. Returns * `"missing"` for empty/unparsable input (fail closed — same policy as the * server) and `"too_old"` for a parsable version below the threshold. * Dev-built daemons (git-describe shape) are always OK — the version string * itself is the shared signal, so frontend and server agree by construction. */ export function checkQuickCreateCliVersion(detected: string | undefined | null): CliVersionCheck { return checkCliVersion(detected, MIN_QUICK_CREATE_CLI_VERSION); } /** Capability gate for explicit quick-create priority and due-date fields. */ export function checkQuickCreateFieldsCliVersion( detected: string | undefined | null, ): CliVersionCheck { return checkCliVersion(detected, MIN_QUICK_CREATE_FIELDS_CLI_VERSION); } function checkCliVersion( detected: string | undefined | null, minimum: string, ): CliVersionCheck { const current = (detected ?? "").trim(); if (DEV_DESCRIBE_RE.test(current)) { return { state: "ok", current, min: minimum }; } const parsed = current ? parseSemver(current) : null; if (!parsed) { return { state: "missing", current, min: minimum }; } const min = parseSemver(minimum)!; if (lessThan(parsed, min)) { return { state: "too_old", current, min: minimum }; } return { state: "ok", current, min: minimum }; } /** Pull `cli_version` off a runtime row's loosely-typed metadata bag. */ export function readRuntimeCliVersion(metadata: Record | undefined): string { const v = metadata?.cli_version; return typeof v === "string" ? v : ""; } /** * Frontend mirror of the server's `MinHandoffCLIVersion` soft gate * (`server/pkg/agent/version.go`). The assignment handoff note is only rendered * into the run's opening prompt by daemons at or above this multica CLI version * (MUL-3375); older daemons silently drop it. Unlike the quick-create gate this * never blocks the assignment — the UI just grays out the note box and warns. * * Keep in lockstep with the server constant; the two are enforced independently * (the server is authoritative) but must agree so the warning matches reality. */ export const MIN_HANDOFF_CLI_VERSION = "0.3.28"; /** * Whether a daemon-reported CLI version is new enough to render a handoff note. * Mirrors server `agent.HandoffSupported`: missing / unparsable / below-minimum * all degrade to `false`, and dev-built daemons (git-describe shape) always * pass — the version string is the shared signal, so frontend and server agree * by construction. Pure and synchronous, so the note box can settle from the * already-warm runtime cache instead of waiting on the trigger-preview * round-trip, exactly like the quick-create version gate. */ export function handoffSupported(detected: string | undefined | null): boolean { return meetsMinCliVersion(detected, MIN_HANDOFF_CLI_VERSION); } /** * First release whose daemon renders the chat session's project context * (description + resources) into the run brief (PR #5765, ships in v0.4.10). * Older daemons still receive and honor the project's repos — the server * pre-extracts those into the generic `repos` claim field — but silently skip * the Project Context section, so the durable description never reaches the * agent. SOFT gate: selecting a project always works; the UI only warns. * * Frontend-only constant: unlike handoff there is no server preview endpoint * computing this, so there is no server twin to keep in lockstep with. */ export const MIN_CHAT_PROJECT_CONTEXT_CLI_VERSION = "0.4.10"; /** * Whether a daemon-reported CLI version is new enough to inject a chat * session's project description into the run brief. Same degrade rules as * `handoffSupported`: missing / unparsable / below-minimum are `false`, * dev-built daemons (git-describe shape) always pass. */ export function chatProjectContextSupported(detected: string | undefined | null): boolean { return meetsMinCliVersion(detected, MIN_CHAT_PROJECT_CONTEXT_CLI_VERSION); } function meetsMinCliVersion(detected: string | undefined | null, minimum: string): boolean { const current = (detected ?? "").trim(); if (!current) return false; if (DEV_DESCRIBE_RE.test(current)) return true; const parsed = parseSemver(current); if (!parsed) return false; return !lessThan(parsed, parseSemver(minimum)!); }