Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
f2b86f6
feat(zcode): add opt-in local app-server agent provider
FacuM Sep 11, 2026
63d1348
feat(zcode): connect Desktop and show local usage windows
FacuM Sep 11, 2026
c295914
feat(zcode): show remaining subscription quota through Desktop
FacuM Sep 11, 2026
bb2cef3
fix(zcode): select and validate compatible Desktop Node
FacuM Sep 11, 2026
7df545f
fix(zcode): complete Desktop provider and catalog activation
FacuM Sep 11, 2026
222f9c1
test(zcode): cover activation retries and explain dashboard authoriza…
FacuM Sep 11, 2026
c4d47ad
fix(zcode): resolve cold catalog routes and preflight sandbox policy
FacuM Sep 11, 2026
d686303
merge: sync dev to resolve PR4259 integration conflicts
FacuM Sep 11, 2026
129086d
feat(zcode): run on host by default with optional sandbox
FacuM Sep 11, 2026
a05f7e8
feat(zcode): add manually selected accounts through official OAuth
FacuM Sep 11, 2026
d7544db
fix(zcode): describe image inputs with the configured vision sidecar
FacuM Sep 11, 2026
b880dfe
fix(zcode): address automated review findings
FacuM Sep 11, 2026
737e18a
merge: sync ZCode provider with latest dev
FacuM Sep 11, 2026
fdd475f
fix(zcode): address follow-up hygiene findings
FacuM Sep 11, 2026
f8eef43
Merge remote-tracking branch 'origin/dev' into feat/zcode-app-server
FacuM Sep 11, 2026
2da552a
fix(zcode): expose only supported reasoning levels
FacuM Sep 11, 2026
44efbef
fix(zcode): accept replayed tool history
FacuM Sep 13, 2026
bf0630c
fix(zcode): bound replayed conversation history
FacuM Sep 13, 2026
4d3bd07
fix(zcode): restore native host tool access
FacuM Sep 13, 2026
1352a3d
merge: sync ZCode provider with latest dev
FacuM Sep 13, 2026
2c4bcc0
fix(zcode): launch official runtime without unsupported flags
FacuM Sep 13, 2026
aee750c
merge: sync ZCode provider with latest dev
FacuM Sep 13, 2026
aeb8015
merge: sync ZCode provider with latest dev
FacuM Sep 13, 2026
577877b
test(zcode): scope host bootstrap regression to Linux
FacuM Sep 13, 2026
35bed9b
fix(zcode): close account and disconnect lifecycle gaps
FacuM Sep 13, 2026
f4a1095
fix(zcode): harden model and protocol verification
FacuM Sep 13, 2026
44c5504
fix(zcode): terminate host process trees
FacuM Sep 13, 2026
0ff062c
chore: merge current dev into zcode
FacuM Sep 13, 2026
56a13d0
fix(zcode): refresh providers after account activation
FacuM Sep 13, 2026
29650c6
chore: merge current dev into zcode
FacuM Sep 13, 2026
333f262
fix(zcode): preserve account activation refresh
FacuM Sep 13, 2026
921bb02
fix(zcode): cancel shared refresh waits promptly
FacuM Sep 13, 2026
49a9f73
fix(zcode): prune orphaned reconnect drafts
FacuM Sep 13, 2026
582f434
fix(zcode): keep ready activations successful
FacuM Sep 13, 2026
ccae94f
fix(zcode): finish saved-account review findings
FacuM Sep 13, 2026
a01704c
fix(zcode): close remaining review races
FacuM Sep 13, 2026
d8502d4
chore: merge current dev into zcode branch
FacuM Sep 13, 2026
d342715
fix(zcode): close remaining hygiene findings
FacuM Sep 13, 2026
a28b8d0
fix(zcode): harden compaction and continuation
FacuM Sep 13, 2026
a292405
chore: merge current dev into zcode provider
FacuM Sep 13, 2026
7127726
fix(zcode): close lifecycle and host access gaps
FacuM Sep 13, 2026
dec22ad
fix(zcode): guard routed model aliases on removal
FacuM Sep 13, 2026
8092d5a
fix(zcode): keep profile lock keys stable
FacuM Sep 13, 2026
e169c52
fix(zcode): refresh settings mutations
FacuM Sep 13, 2026
65946f8
fix(zcode): close remaining review gaps
FacuM Sep 13, 2026
1681124
chore: merge current dev into zcode provider
FacuM Sep 13, 2026
22fa7d7
fix(zcode): preserve recovery and refresh fences
FacuM Sep 13, 2026
7564eca
chore: merge current dev into zcode provider
FacuM Sep 13, 2026
a22e5fa
fix(zcode): retain ready account recovery
FacuM Sep 13, 2026
588766b
docs(zcode): describe ready refresh recovery
FacuM Sep 13, 2026
6af00b6
fix(zcode): recover partial account removal
FacuM Sep 13, 2026
8a1dbf3
fix(gui): localize zcode desktop titles
FacuM Sep 13, 2026
66b7c63
fix(zcode): bound setup account side effects
FacuM Sep 13, 2026
1e73599
fix(zcode): reserve pending account capacity
FacuM Sep 13, 2026
e5ed567
fix(zcode): preserve partial setup recovery
FacuM Sep 13, 2026
d3b9ebe
fix(zcode): make desktop host access deterministic
FacuM Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs-site/public/images/zcode-provider.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs-site/public/images/zcode-quota-bars.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs-site/public/images/zcode-usage-flash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ description: Every way opencodex authenticates and talks to an LLM provider —
A **provider** is one upstream LLM endpoint plus how to reach it: an adapter, a base URL, an auth
mode, and an optional model list. Providers live under `providers` in `~/.opencodex/config.json`.

For a tool-executing local agent rather than an HTTP model endpoint, see the opt-in
[ZCode app-server provider](/guides/zcode-agent/). It is separate from exporting models to ZCode Desktop.

The dashboard provider Overview separates connection details, account usage and editable notes.
Notes appear once, below the connection and authentication sections. Supported sponsor presets
also show a short introduction, a Sponsor label and links to the provider's site or console.
Expand Down
437 changes: 437 additions & 0 deletions docs-site/src/content/docs/guides/zcode-agent.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions gui/.eslint/i18n-allowlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const BRAND_LITERALS = new Set([
"Anthropic",
"GitHub",
"Codex",
"ZCode Desktop",
"OpenRouter",
"Ollama",
"xAI",
Expand Down
11 changes: 9 additions & 2 deletions gui/src/components/AddProviderModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import type { CatalogLoginHint } from "./provider-catalog/login-hint-visibility"
import { baseUrlForChoice, matchChoiceId, resolvedBaseUrlForChoice } from "../base-url-choice";
import { AddProviderOAuthPane } from "./add-provider-oauth-pane";
import { AddProviderFormPane } from "./add-provider-form-pane";
import ZcodeDesktopPane from "./ZcodeDesktopPane";
import { useAddProviderOAuth } from "./use-add-provider-oauth";
import {
addProviderModalReducer,
createInitialAddProviderState,
} from "./add-provider-modal-reducer";
import type { ProviderAdditionMetadata } from "../provider-addition";

export type ProviderConfig = ProviderPayload;

Expand All @@ -34,11 +36,12 @@ export default function AddProviderModal({
apiBase, existingNames, onClose, onAdded, initialTier, initialCustom = false,
accountRows, accountStatus, accountBusy, accountLoginHint = null,
onAccountLogin, onAccountCancelLogin, onAccountLogout, onAccountManage, onOpen,
onProviderStateMutation,
}: {
apiBase: string;
existingNames: string[];
onClose: () => void;
onAdded: (name: string) => void;
onAdded: (name: string, metadata?: ProviderAdditionMetadata) => void;
initialTier?: "accounts" | "free" | "paid";
initialCustom?: boolean;
accountRows?: AccountLoginRow[];
Expand All @@ -51,6 +54,7 @@ export default function AddProviderModal({
onAccountLogout?: (provider: string) => void;
onAccountManage?: (provider: string) => void;
onOpen?: () => void;
onProviderStateMutation?: () => void;
}) {
const t = useT();
const fallbackPresets = useMemo<Preset[]>(() => [
Expand Down Expand Up @@ -301,7 +305,10 @@ export default function AddProviderModal({
}}
/>
) : form && (
preset.auth === "oauth" && form.authMode === "oauth" ? (
preset.id === "zcode" ? (
<ZcodeDesktopPane apiBase={apiBase} onConnected={onAdded}
onProviderStateMutation={onProviderStateMutation} onBack={() => dispatch({ type: "back" })} />
) : preset.auth === "oauth" && form.authMode === "oauth" ? (
<AddProviderOAuthPane
preset={preset}
oauthSupported={oauthSupported}
Expand Down
16 changes: 12 additions & 4 deletions gui/src/components/QuotaBars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export default function QuotaBars({
incompleteWindowKeys,
incompleteCustomWindowLabels,
observedAt,
showRemaining = false,
}: {
quota: AccountQuota | null;
plan?: string | null;
Expand All @@ -262,6 +263,8 @@ export default function QuotaBars({
* look live.
*/
observedAt?: number;
/** Display remaining balance for native subscription snapshots; consumption stays canonical. */
showRemaining?: boolean;
}) {
const { locale } = useI18n();
const rows = buildQuotaRows(quota, plan, t);
Expand Down Expand Up @@ -320,6 +323,7 @@ export default function QuotaBars({
{observedLine}
{rows.map(row => (
<StackedQuotaRow
showRemaining={showRemaining}
key={row.limitLabel}
row={row}
threshold={threshold}
Expand Down Expand Up @@ -389,7 +393,8 @@ function QuotaRow({ credits, label, percent, resetAt, threshold, t, locale }: {
);
}

function StackedQuotaRow({ row, threshold, t, locale, incomplete }: {
function StackedQuotaRow({ row, threshold, t, locale, incomplete, showRemaining = false }: {
showRemaining?: boolean;
row: QuotaBarRow;
threshold: number;
t: TFn;
Expand All @@ -400,6 +405,8 @@ function StackedQuotaRow({ row, threshold, t, locale, incomplete }: {
const warn = isQuotaWarn(row.percent, threshold);
const color = quotaBarTone(row.percent, threshold);
const resetText = formatResetFuture(row.resetAt, t, locale);
const displayPercent = showRemaining ? 100 - row.percent : row.percent;
const remainingText = t("quota.remainingPercent", { pct: new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(displayPercent) });
return (
<div className={`quota-stacked-row${warn ? " quota-stacked-row--warn" : ""}${exhausted ? " quota-stacked-row--exhausted" : ""}`}>
<div className="quota-stacked-head">
Expand All @@ -419,11 +426,12 @@ function StackedQuotaRow({ row, threshold, t, locale, incomplete }: {
<span className="quota-stacked-reset muted">{resetText}</span>
</div>
<div className="quota-stacked-bar-row">
<div className="bar quota-stacked-bar">
<div className={`bar-fill ${color}`} style={barFillStyle(row.percent)} />
<div className="bar quota-stacked-bar" role="progressbar" aria-label={row.limitLabel} aria-valuemin={0} aria-valuemax={100} aria-valuenow={displayPercent}
aria-valuetext={showRemaining ? remainingText : t("quota.usedPercent", { pct: Math.round(row.percent) })}>
<div className={`bar-fill ${color}`} style={barFillStyle(displayPercent)} />
</div>
<span className={`quota-stacked-used${warn ? " quota-stacked-used--warn" : ""}`}>
{t("quota.usedPercent", { pct: Math.round(row.percent) })}
{showRemaining ? remainingText : t("quota.usedPercent", { pct: Math.round(row.percent) })}
</span>
</div>
{exhausted && (
Expand Down
174 changes: 174 additions & 0 deletions gui/src/components/ZcodeAccountsPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useT } from "../i18n/shared";

type Account = { id: string; label: string; activation: string; busy: boolean; providerName?: string };
type Job = { jobId: string; accountId: string; phase: string; url?: string; error?: string };
type Activation = { activation?: string; providerName?: string };
export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProviderActivated, onProviderStateMutation }: {
apiBase: string;
runtime: string;
workspace: string;
onProviderActivated?: (name: string) => void;
onProviderStateMutation?: () => void;
}) {
const t = useT();
const [accounts, setAccounts] = useState<Account[]>([]);
const [label, setLabel] = useState("");
const [consented, setConsented] = useState<string | null>(null);
const consentScope = JSON.stringify([apiBase, runtime, workspace]);
const consent = consented === consentScope;
const [job, setJob] = useState<Job | null>(null);
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
const onProviderActivatedRef = useRef(onProviderActivated);
const onProviderStateMutationRef = useRef(onProviderStateMutation);
useEffect(() => { onProviderActivatedRef.current = onProviderActivated; }, [onProviderActivated]);
useEffect(() => { onProviderStateMutationRef.current = onProviderStateMutation; }, [onProviderStateMutation]);
const read = useCallback(async (path: string, body?: Record<string, unknown>) => {
const response = await fetch(apiBase + "/api/zcode-accounts" + path, body ? {
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...body, consent: true }),
} : undefined);
const value = await response.json();
if (!response.ok) throw new Error(value.error || "native_oauth_failed");
return value;
}, [apiBase]);
const refresh = useCallback(async () => {
const value = await read("");
setAccounts(value.accounts ?? []);
}, [read]);
const completeJob = useCallback(async (current: Job, stopped: () => boolean = () => false) => {
let result: Activation;
try {
result = await read("/complete", { jobId: current.jobId }) as Activation;
} catch (e) {
if (!stopped()) {
setError(e instanceof Error ? e.message : "native_oauth_failed");
// A recovery retry already owns a finished, idempotent job. Keep that state so a
// transient /complete failure cannot restart OAuth polling or hide Retry activation.
setJob({ ...current, phase: current.phase === "recovery" ? "recovery" : "authenticated", url: undefined });
}
return;
}
if (stopped()) return;
onProviderStateMutationRef.current?.();
const ready = result.activation === "ready";
let refreshFailed = false;
try { await refresh(); } catch { refreshFailed = true; }
if (stopped()) return;
setError(!ready ? "catalog_update_failed" : refreshFailed ? "account_refresh_failed" : "");
// Server completion is idempotent. Retain its finished job id whenever the account row could
// not be refreshed so the user can retry /complete directly without repeating OAuth. This
// also covers a ready provider whose new account row is not visible in existing Settings.
setJob({ ...current, phase: refreshFailed ? "recovery" : "finished", url: undefined });
if (ready && result.providerName && onProviderActivatedRef.current) {
onProviderActivatedRef.current(result.providerName);
}
}, [read, refresh]);
useEffect(() => {
let stopped = false;
void read("").then(value => { if (!stopped) setAccounts(value.accounts ?? []); })
.catch(e => { if (!stopped) setError(e.message); });
return () => { stopped = true; };
}, [read]);
const jobId = job?.jobId, jobPhase = job?.phase;
useEffect(() => {
if (!jobId || !consent || !["waiting", "authenticated"].includes(jobPhase ?? "")) return;
let stopped = false, pending = false;
const poll = async () => {
if (pending) return;
pending = true;
try {
const next: Job = await read("/login?jobId=" + encodeURIComponent(jobId));
if (stopped) return;
if (next.phase === "authenticated") {
await completeJob(next, () => stopped);
} else { setJob(next); if (next.phase === "failed") { setError(next.error || "native_oauth_failed"); await refresh(); } }
} catch (e) { if (!stopped) { setError(e instanceof Error ? e.message : "native_oauth_failed"); setJob(j => j ? { ...j, phase: "failed", url: undefined } : null); } }
Comment thread
FacuM marked this conversation as resolved.
finally { pending = false; }
};
void poll();
const timer = setInterval(() => void poll(), 2000);
return () => { stopped = true; clearInterval(timer); };
}, [jobId, jobPhase, consent, read, refresh, completeJob]);
const action = async (path: string, body: Record<string, unknown>) => {
if ((!consent && path !== "/cancel") || busy) return;
setBusy(true); setError("");
try {
const result = await read(path, body);
if (path === "/login") setJob(result);
else if (path === "/cancel") setJob(null);
else if (result.activation && result.activation !== "ready") setError("catalog_update_failed");
if (path === "/activate" || path === "/rename" || path === "/remove") {
onProviderStateMutationRef.current?.();
}
Comment thread
FacuM marked this conversation as resolved.
const ready = path === "/activate" && result.activation === "ready";
const activation = ready && result.providerName && onProviderActivatedRef.current
? { name: result.providerName, notify: onProviderActivatedRef.current } : undefined;
try { await refresh(); } catch (error) { if (path !== "/activate") throw error; }
if (activation) activation.notify(activation.name);
} catch (e) {
const code = e instanceof Error ? e.message : "native_oauth_failed";
// Removal revokes and persists provider state before catalog convergence. Its bounded
// partial error must still invalidate the parent and refresh this account list.
if (path === "/remove" && ["account_removal_partial", "catalog_update_failed"].includes(code)) {
onProviderStateMutationRef.current?.();
try { await refresh(); } catch { /* The mutation callback remains authoritative. */ }
}
setError(code);
}
finally { setBusy(false); }
};
const retryCompletion = async () => {
if (!job || job.phase !== "recovery" || !consent || busy) return;
setBusy(true); setError("");
try { await completeJob(job); } finally { setBusy(false); }
};
// Failed jobs retain their hidden profile until Cancel, so they must also block another Add or
// Reconnect attempt. The server applies the same ownership rule and the ten-minute expiry is a
// final cleanup bound rather than the ordinary retry path.
const loggingIn = !!job && ["waiting", "authenticated", "failed"].includes(job.phase);
return <section style={{ display: "grid", gap: 8 }}>
<h3>{t("zcodeAccounts.title")}</h3>
<p className="muted text-label">{t("zcodeAccounts.help")}</p>
<label className="modal-field"><span>{t("zcodeAccounts.name")}</span>
<input className="input" maxLength={80} value={label} onChange={e => setLabel(e.target.value)} />
</label>
<label style={{ display: "flex", gap: 8 }}><input type="checkbox" checked={consent} onChange={e => setConsented(e.target.checked ? consentScope : null)} />
<span>{t("zcodeAccounts.consent")}</span>
</label>
<button type="button" className="btn" disabled={!consent || busy || loggingIn || !label.trim() || !runtime || !workspace}
onClick={() => void action("/login", { label, runtime, workspace })}>{t("zcodeAccounts.add")}</button>
{job && ["waiting", "authenticated"].includes(job.phase) && <div role="status">
Comment thread
FacuM marked this conversation as resolved.
<p>{t("zcodeAccounts.loginHint")}</p>
{job.url && <a href={job.url} target="_blank" rel="noopener noreferrer">{t("zcodeAccounts.login")}</a>}
<button type="button" className="btn btn-ghost" disabled={busy} onClick={() => void action("/cancel", { jobId: job.jobId })}>{t("common.cancel")}</button>
</div>}
{job?.phase === "failed" && <button type="button" className="btn btn-ghost" disabled={busy}
onClick={() => void action("/cancel", { jobId: job.jobId })}>{t("common.cancel")}</button>}
{job?.phase === "recovery" && <div role="status">
<p>{t("zcodeAccounts.pending")}</p>
<button type="button" className="btn btn-primary" disabled={!consent || busy}
onClick={() => void retryCompletion()}>{t("zcodeDesktop.retryActivation")}</button>
</div>}
{error && <p role="alert">{t("zcodeAccounts.failed")} <code>{error}</code></p>}
{accounts.some(account => account.activation === "ready") && <p className="muted text-label">{t("zcodeDesktop.restartNotice")}</p>}
{accounts.map(account => <div key={account.id} style={{ display: "grid", gap: 4 }}>
<strong>{account.label}</strong>
<span>{account.activation === "ready" ? t("zcodeDesktop.connected") : t("zcodeAccounts.pending")}</span>
{account.providerName && <code>{account.providerName}</code>}
<div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
<button type="button" className="btn" disabled={!consent || busy || account.busy}
onClick={() => void action("/activate", { accountId: account.id })}>{t("zcodeDesktop.retryActivation")}</button>
<button type="button" className="btn" disabled={!consent || busy || account.busy || loggingIn || !runtime || !workspace}
onClick={() => void action("/login", { accountId: account.id, label: account.label, runtime, workspace })}>{t("zcodeAccounts.reconnect")}</button>
<button type="button" className="btn" disabled={!consent || busy || account.busy} onClick={() => {
const name = window.prompt(t("zcodeAccounts.name"), account.label);
if (name?.trim()) void action("/rename", { accountId: account.id, label: name });
}}>{t("zcodeAccounts.rename")}</button>
<button type="button" className="btn btn-ghost" disabled={!consent || busy || account.busy} onClick={() => {
if (window.confirm(t("zcodeAccounts.removeConfirm"))) void action("/remove", { accountId: account.id });
}}>{t("zcodeAccounts.remove")}</button>
</div>
</div>)}
</section>;
}
Loading
Loading