-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix: release 2.68.0 blockers and bring the menu bar patches to the Windows/Linux tray #6052
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # 030 — done: Codex App visualization references for routed models | ||
|
|
||
| ## Outcome | ||
|
|
||
| Any routed model can now show a Codex App inline visualization. #6040 (`a1285fc648`) stopped the | ||
| citation filter from deleting non-citation directives; #6045 (`dc784d3e6f`) rewrites the private-use | ||
| `visualize` reference into the app's own `::codex-inline-vis{…}` directive in the text routed models | ||
| read, so a model whose provider drops private-use characters still reads and writes a form the app renders. | ||
|
|
||
| ## Evidence | ||
|
|
||
| - App bundle 26.924.22138: `f2`, `Err` and `i3e` (see 001) render the ASCII directive directly. | ||
| - Local app rebuilt from `dc784d3e6f` with a forced standalone sidecar; the installed `ocx` reports | ||
| 2.68.0 and contains `codex-inline-vis`; `codesign --verify --deep --strict` passes. | ||
| - Live, through the installed proxy: `anthropic/claude-opus-5-5` and `cursor/claude-opus-5-5` answered a | ||
| private-use reference with `::codex-inline-vis{path="/tmp/demo-chart.html"}`; `gpt-6-luna` returned the | ||
| private-use form unchanged; `xai/grok-4.7` received and quoted the private-use form. | ||
| - Render: a Claude-routed final answer carrying `::codex-inline-vis{…}` rendered as an interactive widget in | ||
| Codex App; the widget reported `route: Claude, version: #6045, outcome: renders` back to the thread. | ||
| - Reviews: architect Fermat, auditor Lovelace, reviewer Archimedes (132,715 `f2` parity cases), and two | ||
| release regression lanes over #6040 and #6045 found no regression. | ||
|
|
||
| ## What did not go to plan | ||
|
|
||
| - `desktop/scripts/prepare-sidecar.ts` reuses `dist/standalone/*/ocx` when it exists, so the first rebuilt | ||
| app shipped a stale 2.61.0 proxy. The standalone build has to be forced before `prepare-sidecar`. | ||
| - A commentary message is recorded as a reasoning summary on this route, which the app does not render | ||
| as markdown directives; the render check needed a final answer. | ||
| - #6045 merged by admin at the owner's instruction before its PR CI finished; the post-merge lane=all run | ||
| on `dc784d3e6f` is the CI evidence for the merged tree. | ||
|
|
||
| ## Follow-ups | ||
|
|
||
| - Make `prepare-sidecar` rebuild when the source is newer than the cached standalone binary. | ||
| - Replies already stored in the bare `visualize{…}` form are not repaired. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # 260927 release 2.68.0 — plan | ||
|
|
||
| ## Reader summary | ||
|
|
||
| The owner asked (2026-09-27) for a main..dev regression review with astra reviewers, for the | ||
| Windows/Linux tray to gain the menu bar patches the macOS panel received, and for a full 2.68.0 | ||
| release. `origin/main` is v2.67.0 (`4bc92294aa`); `origin/dev` (`dc784d3e6f`) carries 86 commits | ||
| beyond it. Procedure follows [the 2.67.0 round](../../_fin/260926_release_2670/020_wp3_release.md); | ||
| only values differ. The owner asked for CI to be judged heuristically: a failure is a blocker only | ||
| when it reproduces or is tied to a change in the range. | ||
|
|
||
| ## Work-phase map | ||
|
|
||
| | wp | Doc | Change | | ||
| |---|---|---| | ||
| | wp4 | [010](010_wp4_blockers_and_tray.md) | release blockers from the review, Windows tray parity | | ||
| | wp5 | [020](020_wp5_release.md) | candidate CI, pre-move to 2.69.0, promotion, publish, verify | | ||
|
|
||
| ## Review lanes (astra, read-only) | ||
|
|
||
| | Lane | Range | Verdict | | ||
| |---|---|---| | ||
| | #6040 citation filter | `a1285fc648` | OK (700,168 comparisons) | | ||
| | #6045 visualization references | `dc784d3e6f` | OK | | ||
| | dev sanity + CI classification | whole range | OK; Devin test mock leak (test-only) | | ||
| | merge trains 1-5 | #5901..#5909 | OK | | ||
| | desktop, batches 6-8 | #5910..#5957 | BLOCK: native tray `switchFailed` cached | | ||
| | Kiro series | #5967..#6016 | BLOCK: first discovery failure never backs off | | ||
| | batches 9-10 | #5984..#6031 | BLOCK: Home-initiated Remote Link answers 503 | | ||
|
|
||
| Owner disposition for the Remote Link finding (2026-09-27): keep 2.67.0 behaviour for Home-initiated | ||
| links only; Child-initiated links keep the new ownership proof. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # 010 — wp4: release blockers and Windows tray parity | ||
|
|
||
| ## Fixes (one PR to dev) | ||
|
|
||
| | Finding | Files | Change | Proof | | ||
| |---|---|---|---| | ||
| | Kiro discovery failure without a last good list retried on every request | `src/providers/kiro-model-catalog.ts` | `failedUntil` map carries the 60 s retry per account identity; cleared on success and by `clearKiroAccountModels` | new case in `tests/providers/kiro/kiro-model-catalog.test.ts` fails before (2 calls), passes after (1) | | ||
| | Native tray `switchFailed` cached and settling later switches | `desktop/src-tauri/src/native_tray.rs` | `cached()` drops the one-shot flag from the snapshot every refresh starts from | `cargo test --lib native_tray` | | ||
| | Home-initiated Child relays answer 503 | `src/client/link-relay.ts`, `src/client/runtime.ts` | explicit `HOME_INITIATED_LINK_TUNNEL` gate for a link-mode runtime without a sidecar; a relay with no gate still refuses | new case in `tests/clients/client-link-relay.test.ts`; the lane's repro passes | | ||
| | Devin preflight test leaks its adapter mock | `tests/responses/responses-grok-devin-preflight.test.ts` | restore the module in `afterAll` | 3-file run 82 pass (was 55/27) | | ||
|
|
||
| ## Windows/Linux tray parity (web tray `gui/src/pages/Tray.tsx`) | ||
|
|
||
| | macOS patch | Web tray before | Change | | ||
| |---|---|---| | ||
| | #5920 windows that report data | present | none | | ||
| | #5922 provider marks, severity bars | missing | `ProviderIcon` in headings; `quotaSeverity` classes on bars (70/90) | | ||
| | #5931 switch the active account | missing | `switchState`/`exhausted` in `parseAccounts`, `accountSwitchRequest` routes, "Use this account" on hover/focus, pending and failure states | | ||
| | #5921 widget reload | not applicable (macOS widget) | none | | ||
|
|
||
| The popup sends the switch with the dashboard session (`window.fetch` is session-wrapped by | ||
| `gui/src/api.ts`), not the desktop capability the native panel uses. | ||
|
|
||
| ## Verification | ||
|
|
||
| `bun run typecheck`, GUI `tsc` and lint, `bun run structure:check`, `bun run privacy:scan`, the focused | ||
| test files above, `gui/tests/tray-data.test.ts`, and a Playwright capture of the web tray against the | ||
| running proxy. Full suite: PR CI. | ||
|
|
||
| ## Audit round 1 (Carver, astra) — FAIL, dispositions | ||
|
|
||
| 1. GUI build: `providerSources` `flatMap` inferred only `'codex'` — folded (`flatMap<TrayProviderSource>`; `tsc -p tsconfig.app.json` exit 0). | ||
| 2. A Child-initiated link whose sidecar was deleted took the Home-initiated gate — folded. A join now | ||
| writes `link/child-initiated.json` with the link id; the runtime uses the Home gate only when neither | ||
| the sidecar nor a matching marker exists, and an unreadable marker counts as present. The auditor's | ||
| repro now answers 503 with no send for deleted and corrupt sidecars and for hub transport. | ||
| Residual: a 2.67.0 join made before this marker existed, with its sidecar later deleted, is treated as | ||
| Home-initiated, which is the 2.67.0 behaviour for every link. | ||
| 3. Switch settled before the reload — folded: the row stays pending until the reload the switch started completes. | ||
| 4. Unbounded PUT — folded: `createBoundedFetch(20 s)`; a timeout reports the switch failure. | ||
| Note (not folded): the web tray does not print `blockedReason`; a blocked account simply offers no "Use". | ||
| 5. Round 2: pre-marker joins — partly folded. The runtime records the marker at start whenever the | ||
| sidecar is intact (`recordChildInitiatedLink`), so a pre-marker join that starts once on 2.68.0 is | ||
| protected from then on. Rebutted for the remaining case, a pre-marker join whose sidecar was deleted | ||
| before its first 2.68.0 start: that state is indistinguishable from a 2.67.0 Home-initiated link, and | ||
| failing it closed would break every existing Home-initiated link, which the owner chose to keep working. | ||
| It keeps exactly the 2.67.0 behaviour, the owner-accepted baseline. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # 020 — wp5: release 2.68.0 | ||
|
|
||
| Values for the 2.67.0 procedure: `CAND` = `origin/dev` after wp4 merges; `PV=2.68.0-preview.20260927`; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Correct the version label to 2.68.0. Line 3 identifies these inputs as belonging to the 2.67.0 procedure, but this runbook and its release values specify 2.68.0. This conflict can lead an operator to apply or verify the wrong release procedure. Change 🤖 Prompt for AI Agents |
||
| pre-move `dev-version-bump.yml --ref main -f intended-version=2.68.0 -f mode=pre-move` (dev → 2.69.0); | ||
| promotion branches `codex/260927-release-preview-2.68.0` and `codex/260927-release-main-2.68.0` built | ||
| with `git merge -s ours` and `scripts/release-version-sources.ts`; merge commits (never squash); push-event | ||
| CI and Service lifecycle at both promotion SHAs; `release.yml` preview first, then stable; verify npm | ||
| dist-tags, both GitHub releases' assets, and `latest.json` signatures; fast-forward local branches. | ||
|
|
||
| Heuristic CI rule (owner): a failing job blocks only when it reproduces on rerun or its log points at a | ||
| change in main..dev. Runner-stall signatures get one job rerun. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,19 @@ | ||
| import { useEffect, useState } from 'react'; | ||
| import { useEffect, useRef, useState } from 'react'; | ||
| import { createBoundedFetch } from '../bounded-fetch'; | ||
| import { useI18n } from '../i18n/shared'; | ||
| import { formatTokens } from '../format-tokens'; | ||
| import { formatProviderDisplayName } from '../provider-icons'; | ||
| import { ProviderIcon } from '../components/provider-workspace/ProviderRail'; | ||
| import { quotaSeverity } from '../quota-summary'; | ||
| import { UsageCompanionChart } from './usage-companion-chart'; | ||
| import { companionTimelineQuery, companionTimelineProjection, type CompanionSettings, type CompanionSettingsResponse, type UsageTimeline } from './usage-companion-utils'; | ||
| import { fetchTrayJson, parseTrayUsage, filterUsage, measuredTotals, finite, parseAccounts, providerSources, quotaWindows, relativeReset, type TrayProvider, type TrayTotals, type TrayUsage } from './tray-data'; | ||
| import { accountSwitchRequest, fetchTrayJson, parseTrayUsage, filterUsage, measuredTotals, finite, parseAccounts, providerSources, quotaWindows, relativeReset, type TrayProvider, type TraySwitchKind, type TrayTotals, type TrayUsage } from './tray-data'; | ||
|
|
||
| declare global { interface Window { __OPENCODEX_TRAY_VISIBLE__?: boolean } } | ||
|
|
||
| /** A switch that has not answered by then is reported as failed rather than left spinning. */ | ||
| const SWITCH_TIMEOUT_MS = 20_000; | ||
|
|
||
| const incomplete = (data: TrayUsage | null | undefined) => data?.usageIncomplete || data?.historyTruncated || data?.entriesTruncated; | ||
|
|
||
| export default function Tray() { | ||
|
|
@@ -23,7 +29,30 @@ export default function Tray() { | |
| const [updatedAt, setUpdatedAt] = useState<number | null>(null); | ||
| const [refreshing, setRefreshing] = useState(false); | ||
| const [revision, setRevision] = useState(0); | ||
| const [switching, setSwitching] = useState<string | null>(null); | ||
| const [switchError, setSwitchError] = useState<string | null>(null); | ||
| const retry = () => setRevision(value => value + 1); | ||
| // Set after a successful switch: the pending row stays busy until the reload it started lands. | ||
| const awaitingRefresh = useRef(false); | ||
| // The same route and body the native panel sends; the dashboard session supplies the | ||
| // credentials. The switch settles only when the reload shows the runtime's own selection. | ||
| const switchAccount = async (provider: string, kind: TraySwitchKind, accountId: string) => { | ||
| setSwitching(`${provider}:${accountId}`); | ||
| setSwitchError(null); | ||
| const bounded = createBoundedFetch(SWITCH_TIMEOUT_MS); | ||
| try { | ||
| const request = accountSwitchRequest(provider, kind, accountId); | ||
| const response = await fetch(request.path, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request.body), signal: bounded.signal }); | ||
| if (!response.ok) throw new Error(String(response.status)); | ||
| awaitingRefresh.current = true; | ||
| retry(); | ||
| } catch { | ||
| setSwitching(null); | ||
| setSwitchError(t(kind === 'codex' ? 'codexAuth.switchFailed' : 'prov.accountSwitchFail')); | ||
| } finally { | ||
| bounded.clear(); | ||
| } | ||
| }; | ||
|
|
||
| // Every post-await state write checks both effect disposal and the request's AbortSignal. | ||
| // react-doctor-disable-next-line react-doctor/no-set-state-after-await-in-effect | ||
|
|
@@ -49,7 +78,7 @@ export default function Tray() { | |
| try { | ||
| const sources = providerSources(await json<unknown>('/api/config')); | ||
| const rows = await Promise.all(sources.map(async source => { | ||
| if (!source.path) return { name: source.name, accounts: [] }; | ||
| if (!source.path) return { name: source.name, switchKind: source.switchKind, accounts: [] }; | ||
| try { | ||
| const payload = await json<Record<string, unknown>>(source.path); | ||
| if (source.name === 'openai') { | ||
|
|
@@ -58,9 +87,9 @@ export default function Tray() { | |
| payload.activeCodexAccountId = selection.activeCodexAccountId ?? '__main__'; | ||
| } catch { /* Missing selection is unknown, never inferred from quota. */ } | ||
| } | ||
| return { name: source.name, accounts: parseAccounts(payload) }; | ||
| return { name: source.name, switchKind: source.switchKind, accounts: parseAccounts(payload) }; | ||
| } | ||
| catch { return { name: source.name, accounts: [], unavailable: true }; } | ||
| catch { return { name: source.name, switchKind: source.switchKind, accounts: [], unavailable: true }; } | ||
| })); | ||
| if (active()) { setProviders(rows); setQuotaError(false); hadSuccess = true; } | ||
| } catch { if (active()) { setProviders([]); setQuotaError(true); } } | ||
|
|
@@ -95,7 +124,11 @@ export default function Tray() { | |
| await Promise.allSettled([quotas, metrics]); | ||
| } finally { | ||
| busy = false; | ||
| if (active()) { setRefreshing(false); if (hadSuccess) setUpdatedAt(Date.now()); } | ||
| if (active()) { | ||
| setRefreshing(false); | ||
| if (hadSuccess) setUpdatedAt(Date.now()); | ||
| if (awaitingRefresh.current) { awaitingRefresh.current = false; setSwitching(null); } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '25,155p' gui/src/pages/Tray.tsx
sed -n '180,225p' gui/src/pages/Tray.tsxRepository: lidge-jun/opencodex Length of output: 12016 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Tray imports, helpers, and switch/load setup ---'
sed -n '1,70p' gui/src/pages/Tray.tsx
printf '%s\n' '--- Tray effect completion and dependency ---'
sed -n '115,180p' gui/src/pages/Tray.tsx
printf '%s\n' '--- Account parser and provider types ---'
rg -n -A80 -B20 'function parseAccounts|const parseAccounts|type TrayProvider|interface TrayProvider|activeCodexAccountId|switchState' gui/src/pages/Tray.tsx gui/src
printf '%s\n' '--- Changed file diff against supplied merge base ---'
git diff --unified=35 dc784d3e6fd10045d376832ac99cc429b20ba928 3523505cf6198e3d926a81e37e760458a775245 -- gui/src/pages/Tray.tsxRepository: lidge-jun/opencodex Length of output: 42033 Clear switch pending only after the refreshed roster confirms the selection. The effect aborts disposed loads, but The tray can stop showing the switch as pending while it displays stale or unavailable data. If accounts remain available, it can enable another switch. Track the requested provider, account, and refresh generation. Clear 🤖 Prompt for AI Agents |
||
| } | ||
| if (!disposed && current.signal.aborted && visible()) void load(); | ||
| } | ||
| }; | ||
|
|
@@ -152,11 +185,23 @@ export default function Tray() { | |
| </section>} | ||
| {(settings?.showAccounts ?? true) && <section className="tray-providers" aria-label={t('usage.section.providers')}> | ||
| {quotaError && <p role="alert" className="tray-error">{t('startup.tray.unavailable')} <button onClick={retry}>{t('common.retry')}</button></p>} | ||
| {switchError && <p role="alert" className="tray-error">{switchError}</p>} | ||
| {providers.filter(provider => !hiddenProviders.has(provider.name)).map(provider => <div className="tray-provider" key={provider.name}> | ||
| <h2>{formatProviderDisplayName(provider.name, t)}</h2> | ||
| <h2><ProviderIcon name={provider.name} cls="provider-icon tray-provider-icon" />{formatProviderDisplayName(provider.name, t)}</h2> | ||
| {!provider.accounts.length && <div className="tray-missing">{t(provider.unavailable ? 'startup.tray.unavailable' : 'pws.dashboard.noQuota')}</div>} | ||
| {provider.accounts.map(account => <div className="tray-account" key={account.id}> | ||
| <div className="tray-account-name" title={account.label}>{account.label}<span className="tray-account-meta">{account.plan}{account.active && <span title={t('prov.activeBadge')} aria-label={t('prov.activeBadge')}> ●</span>}</span></div> | ||
| {provider.accounts.map(account => { | ||
| const kind = provider.switchKind; | ||
| const pending = switching === `${provider.name}:${account.id}`; | ||
| return <div className="tray-account" key={account.id}> | ||
| <div className="tray-account-name"> | ||
| <span className="tray-account-label" title={account.label}>{account.label}{account.exhausted && <span className="tray-exhausted" title={t('quota.limitReached')} aria-label={t('quota.limitReached')}> ⚠</span>}</span> | ||
| <span className="tray-account-meta"> | ||
| {pending && <span role="status">{t('pws.accountSwitching')}</span>} | ||
| {!pending && kind && account.switchState === 'available' && <button type="button" className="tray-use" disabled={switching !== null} onClick={() => void switchAccount(provider.name, kind, account.id)} title={t('prov.accountSwitchTitle')} aria-label={`${t('prov.accountSwitchTitle')}: ${account.label}`}>{t('prov.accountSwitchTitle')}</button>} | ||
| {account.plan} | ||
| {account.active && <span className="tray-active" title={t('prov.activeBadge')} aria-label={t('prov.activeBadge')}> ✓</span>} | ||
| </span> | ||
| </div> | ||
| {account.email && account.email !== account.label && <div className="tray-account-email">{account.email}</div>} | ||
| {!quotaWindows(account.quota).length && <div className="tray-missing">{t(account.unavailable ? 'startup.tray.unavailable' : 'pws.dashboard.noQuota')}</div>} | ||
| {quotaWindows(account.quota).map(window => { | ||
|
|
@@ -165,11 +210,12 @@ export default function Tray() { | |
| const percent = finite(window.percent) ? Math.min(100, window.percent) : null; | ||
| return <div className="tray-quota" key={window.id}> | ||
| <span title={label}>{label}</span><span>{percent === null ? '—' : `${Math.round(percent)}%`}</span> | ||
| <span className="tray-bar" role={percent === null ? 'img' : 'meter'} aria-label={percent === null ? `${label}: ${t('pws.dashboard.noQuota')}` : label} aria-valuemin={percent === null ? undefined : 0} aria-valuemax={percent === null ? undefined : 100} aria-valuenow={percent ?? undefined} aria-valuetext={percent === null ? undefined : `${Math.round(percent)}%`}><i style={{ width: percent === null ? '0%' : `${percent}%` }} /></span> | ||
| <time title={reset.exact}>{reset.text}</time> | ||
| </div>; | ||
| })} | ||
| </div>)} | ||
| <span className={`tray-bar tray-bar--${quotaSeverity(percent ?? undefined)}`} role={percent === null ? 'img' : 'meter'} aria-label={percent === null ? `${label}: ${t('pws.dashboard.noQuota')}` : label} aria-valuemin={percent === null ? undefined : 0} aria-valuemax={percent === null ? undefined : 100} aria-valuenow={percent ?? undefined} aria-valuetext={percent === null ? undefined : `${Math.round(percent)}%`}><i style={{ width: percent === null ? '0%' : `${percent}%` }} /></span> | ||
| <time title={reset.exact}>{reset.text}</time> | ||
| </div>; | ||
| })} | ||
| </div>; | ||
| })} | ||
| </div>)} | ||
| </section>} | ||
| <div className="tray-refresh"><button type="button" onClick={retry} disabled={refreshing}>{t('startup.refresh')}</button><span role="status">{t('tray.updated', { time: updatedAt === null ? '—' : new Date(updatedAt).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }) })}</span></div> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 45649
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 42158
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 42132
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 41878
Narrow the scope to context-built routes.
normalizeVisualizationContextrewrites only parsedcontext. Native passthrough serializes the unchanged_rawBody, so a raw-body route can still send the private-use reference unchanged. The plan explicitly excludes raw-body passthrough.Suggested documentation fix
📝 Committable suggestion
🤖 Prompt for AI Agents