-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(zcode): add opt-in local app-server agent provider #4259
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
FacuM
wants to merge
56
commits into
lidge-jun:dev
Choose a base branch
from
FacuM:feat/zcode-app-server
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
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 63d1348
feat(zcode): connect Desktop and show local usage windows
FacuM c295914
feat(zcode): show remaining subscription quota through Desktop
FacuM bb2cef3
fix(zcode): select and validate compatible Desktop Node
FacuM 7df545f
fix(zcode): complete Desktop provider and catalog activation
FacuM 222f9c1
test(zcode): cover activation retries and explain dashboard authoriza…
FacuM c4d47ad
fix(zcode): resolve cold catalog routes and preflight sandbox policy
FacuM d686303
merge: sync dev to resolve PR4259 integration conflicts
FacuM 129086d
feat(zcode): run on host by default with optional sandbox
FacuM a05f7e8
feat(zcode): add manually selected accounts through official OAuth
FacuM d7544db
fix(zcode): describe image inputs with the configured vision sidecar
FacuM b880dfe
fix(zcode): address automated review findings
FacuM 737e18a
merge: sync ZCode provider with latest dev
FacuM fdd475f
fix(zcode): address follow-up hygiene findings
FacuM f8eef43
Merge remote-tracking branch 'origin/dev' into feat/zcode-app-server
FacuM 2da552a
fix(zcode): expose only supported reasoning levels
FacuM 44efbef
fix(zcode): accept replayed tool history
FacuM bf0630c
fix(zcode): bound replayed conversation history
FacuM 4d3bd07
fix(zcode): restore native host tool access
FacuM 1352a3d
merge: sync ZCode provider with latest dev
FacuM 2c4bcc0
fix(zcode): launch official runtime without unsupported flags
FacuM aee750c
merge: sync ZCode provider with latest dev
FacuM aeb8015
merge: sync ZCode provider with latest dev
FacuM 577877b
test(zcode): scope host bootstrap regression to Linux
FacuM 35bed9b
fix(zcode): close account and disconnect lifecycle gaps
FacuM f4a1095
fix(zcode): harden model and protocol verification
FacuM 44c5504
fix(zcode): terminate host process trees
FacuM 0ff062c
chore: merge current dev into zcode
FacuM 56a13d0
fix(zcode): refresh providers after account activation
FacuM 29650c6
chore: merge current dev into zcode
FacuM 333f262
fix(zcode): preserve account activation refresh
FacuM 921bb02
fix(zcode): cancel shared refresh waits promptly
FacuM 49a9f73
fix(zcode): prune orphaned reconnect drafts
FacuM 582f434
fix(zcode): keep ready activations successful
FacuM ccae94f
fix(zcode): finish saved-account review findings
FacuM a01704c
fix(zcode): close remaining review races
FacuM d8502d4
chore: merge current dev into zcode branch
FacuM d342715
fix(zcode): close remaining hygiene findings
FacuM a28b8d0
fix(zcode): harden compaction and continuation
FacuM a292405
chore: merge current dev into zcode provider
FacuM 7127726
fix(zcode): close lifecycle and host access gaps
FacuM dec22ad
fix(zcode): guard routed model aliases on removal
FacuM 8092d5a
fix(zcode): keep profile lock keys stable
FacuM e169c52
fix(zcode): refresh settings mutations
FacuM 65946f8
fix(zcode): close remaining review gaps
FacuM 1681124
chore: merge current dev into zcode provider
FacuM 22fa7d7
fix(zcode): preserve recovery and refresh fences
FacuM 7564eca
chore: merge current dev into zcode provider
FacuM a22e5fa
fix(zcode): retain ready account recovery
FacuM 588766b
docs(zcode): describe ready refresh recovery
FacuM 6af00b6
fix(zcode): recover partial account removal
FacuM 8a1dbf3
fix(gui): localize zcode desktop titles
FacuM 66b7c63
fix(zcode): bound setup account side effects
FacuM 1e73599
fix(zcode): reserve pending account capacity
FacuM e5ed567
fix(zcode): preserve partial setup recovery
FacuM d3b9ebe
fix(zcode): make desktop host access deterministic
FacuM File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); } } | ||
| 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?.(); | ||
| } | ||
|
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"> | ||
|
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>; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.