Skip to content

feat(gui): Dashboard workspace view + drop Providers classic toggle#428

Merged
Wibias merged 27 commits into
lidge-jun:devfrom
Wibias:codex/gui-dashboard-workspace
Jul 25, 2026
Merged

feat(gui): Dashboard workspace view + drop Providers classic toggle#428
Wibias merged 27 commits into
lidge-jun:devfrom
Wibias:codex/gui-dashboard-workspace

Conversation

@Wibias

@Wibias Wibias commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add a Dashboard workspace layout (section rail + main pane) with a global Classic/Workspace toggle in the sidebar.
  • Remove the per-tab Classic/Workspace buttons from the Providers page header; view mode stays controlled by the sidebar preference/#providers hash sync.
  • Harden Select menus (viewport-aware portal by default; keep language + update-dialog selects in-tree), restore Codex auto-switch CSS, treat stale startup health as error, and fix the shadow-call toggle race against settings polls.

Test plan

  • Open Dashboard workspace and classic via the sidebar toggle; confirm Overview / Active providers / Available models rail sections render without layout jump on load.
  • Open Providers; confirm no Classic/Workspace button in the page header, and the sidebar toggle still switches #providers / #providers/workspace.
  • Toggle Shadow Call and confirm the switch does not flip back during the next poll window.
  • Open the Dashboard update dialog channel Select and confirm the menu is usable (not clipped under the dialog top layer).
  • Confirm Codex Auth auto-switch card still lays out correctly (toggle + threshold input).
  • bun test ./gui/tests/select-portal.test.tsx ./gui/tests/select-position.test.ts
  • bun run typecheck / GUI smoke against a local proxy

Summary by CodeRabbit

  • New Features
    • Added a shared Workspace/Classic view mode toggle that synchronizes Dashboard and Providers via URL hash.
    • Introduced the workspace-style Dashboard layout with section navigation, model search, and empty-state messaging.
  • Bug Fixes
    • Improved Providers hash/history behavior to prevent unwanted history growth and keep view mode consistent.
    • Hardened shadow-call/help state and refined startup-health status/poll cadence logic.
  • UI Improvements
    • Updated Select to portal by default with improved keyboard navigation and outside-click closing; refined dropdown styling/positioning and workspace styling.
  • Documentation
    • Added “Classic vs Workspace” guide content.
  • Localization
    • Added i18n strings for view mode, workspace labels, and model empty states across locales.
  • Tests
    • Expanded coverage for view mode/hash routing, Select interactions, and related contracts.

Wibias added 17 commits July 22, 2026 13:30
Adds a workspace layout for the Dashboard tab following the Providers workspace DNA. The left rail lists the page sections (Overview, Active providers, Available models); the main pane renders the selected section. Section bodies and the update dialog are extracted into shared blocks so classic and workspace views behave identically. Classic view stays available behind a toggle persisted in localStorage (ocx-dashboard-view). New i18n keys for the Overview section and rail header in all six locales.
Adds a Workspace/Classic toggle to the sidebar footer that switches the whole app's view preference at once. It writes every tab's ocx-*-view localStorage key and bumps a render key so the current page remounts and re-reads its preference immediately. Per-page toggles keep working, and the sidebar label re-reads the preference on navigation so it stays accurate. Future tabs only need to register their key in WORKSPACE_VIEW_KEYS. Also fills in the 10 missing sub.workspace.* keys in ja.ts that the rebase onto dev surfaced. New i18n key app.viewMode in all six locales; reuses existing pws.workspaceToggle/classicToggle labels.
Bring latest upstream GUI features (ErrorBoundary, Startup page,
Select portal, Codex auto-switch) into the solo dashboard branch
while preserving the workspace dashboard layout and global
workspace toggle.
Remove the per-tab Classic/Workspace buttons from Providers (sidebar owns view mode). Default Select menus to viewport-aware portals, keep language and dialog selects in-tree, restore Codex auto-switch CSS, treat stale startup health as error, and bump the shadow-call mutation epoch only after a successful write.
@github-actions github-actions Bot added the enhancement New feature or request label Jul 25, 2026
@coderabbitai

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Wibias added 6 commits July 25, 2026 02:52
Use a canonical ocx-view preference, sync Providers hash on global toggle, remount only Dashboard/Providers, snapshot poll epochs before fetch, keep startup health on its dedicated writer, drop the duplicate project-config poll from the 5s loop, and restore guidance-disabled controls plus docs.
…y, poll ownership

Pass shared Classic/Workspace mode as props instead of remounting pages, use ocx-global-view with deterministic migration, keep Providers hash aligned, seed startup health only while unknown, own project-config diagnostics on a 30s poll, and add keyboard listbox behaviour for Select plus focused regression tests.
…idge-jun#428)

Use history.replaceState for preference/hash reconciliation so Back is not trapped, keep navigateHash for deliberate sidebar/toggle navigation, and expose a select-only combobox ARIA contract with Tab committing the active option.
@Wibias

This comment was marked as outdated.

@coderabbitai

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as resolved.

Wibias added 2 commits July 25, 2026 04:26
Keep optimistic shadow-call UI when a poll JSON parse races an in-flight save, dedupe legacy view-mode key literals, and cover option-click selection.
Replace Providers view-mode hashes in place so Back returns to the prior page, keep page+viewMode updates atomic, and test production useAppRouteState with real history.back/forward.
@Wibias

This comment was marked as outdated.

@coderabbitai

This comment was marked as outdated.

…extract

Assert the exact providers/workspace hash ownership against app-routing and useAppRouteState instead of scraped App.tsx literals.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gui/src/pages/Dashboard.tsx (1)

325-354: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Duplicated epoch-gate check between the success and catch branches — extract to avoid future divergence.

The same settingsPollMayCommit({...}, {...}) object literal is repeated verbatim in the try (Line 330-337) and catch (Line 344-351) branches. This gate exists precisely to protect a race (parse failure vs. in-flight save/stale poll identity); if it's edited in only one branch during future maintenance, the two paths silently diverge and the original optimistic-UI-clobbering bug can reappear in whichever branch was missed.

♻️ Suggested fix: single shared gate closure
+        const shadowCallMayCommit = () => settingsPollMayCommit(
+          { request: shadowRequestEpoch, mutation: shadowMutationEpoch },
+          {
+            request: shadowCallRequestEpochRef.current,
+            mutation: shadowCallMutationEpochRef.current,
+            mutationInFlight: shadowCallMutationInFlightRef.current,
+          },
+        );
         try {
           if (shRes.ok) {
             const nextShadow = await shRes.json() as ShadowCallData;
-            if (settingsPollMayCommit(
-              { request: shadowRequestEpoch, mutation: shadowMutationEpoch },
-              {
-                request: shadowCallRequestEpochRef.current,
-                mutation: shadowCallMutationEpochRef.current,
-                mutationInFlight: shadowCallMutationInFlightRef.current,
-              },
-            )) {
-              setShadowCall(nextShadow);
-            }
+            if (shadowCallMayCommit()) setShadowCall(nextShadow);
           }
         } catch {
-          if (settingsPollMayCommit(
-            { request: shadowRequestEpoch, mutation: shadowMutationEpoch },
-            {
-              request: shadowCallRequestEpochRef.current,
-              mutation: shadowCallMutationEpochRef.current,
-              mutationInFlight: shadowCallMutationInFlightRef.current,
-            },
-          )) {
-            setShadowCall(null);
-          }
+          if (shadowCallMayCommit()) setShadowCall(null);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/pages/Dashboard.tsx` around lines 325 - 354, Extract the duplicated
settingsPollMayCommit call in the surrounding polling logic into one shared gate
value or closure before the try/catch, using the existing shadow request and
mutation epoch refs. Reuse that single gate for both the successful JSON path
and the parse-failure path so setShadowCall updates remain protected by
identical race checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gui/src/use-app-route-state.ts`:
- Around line 91-98: Remove setPageState from the public return object of
useAppRouteState, while retaining page, viewMode, toggleGlobalWorkspace, and
navigateToPage. Update any related hook typings or consumers if needed so the
exposed API no longer permits direct route-state mutation.

---

Outside diff comments:
In `@gui/src/pages/Dashboard.tsx`:
- Around line 325-354: Extract the duplicated settingsPollMayCommit call in the
surrounding polling logic into one shared gate value or closure before the
try/catch, using the existing shadow request and mutation epoch refs. Reuse that
single gate for both the successful JSON path and the parse-failure path so
setShadowCall updates remain protected by identical race checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b2f37eeb-675e-4ad6-afb8-f8566b22ba80

📥 Commits

Reviewing files that changed from the base of the PR and between e691b76 and 9e66467.

📒 Files selected for processing (9)
  • gui/src/App.tsx
  • gui/src/app-routing.ts
  • gui/src/pages/Dashboard.tsx
  • gui/src/use-app-route-state.ts
  • gui/src/view-mode.ts
  • gui/tests/dashboard-contracts.test.ts
  • gui/tests/providers-hash-history.test.tsx
  • gui/tests/select-keyboard.test.tsx
  • gui/tests/view-mode.test.ts

Comment on lines +91 to +98
return {
page,
viewMode,
setPageState,
toggleGlobalWorkspace,
navigateToPage,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm no other file destructures/uses setPageState from useAppRouteState.
rg -nP '\bsetPageState\b' gui/src gui/tests

Repository: lidge-jun/opencodex

Length of output: 429


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== use-app-route-state.ts =="
cat -n gui/src/use-app-route-state.ts | sed -n '1,140p'

echo
echo "== direct consumers of useAppRouteState =="
rg -n "useAppRouteState|setPageState" gui/src gui/tests

Repository: lidge-jun/opencodex

Length of output: 4954


Remove setPageState from useAppRouteState. gui/src/App.tsx and gui/tests/providers-hash-history.test.tsx only use page, viewMode, toggleGlobalWorkspace, and navigateToPage; keeping the raw setter in the hook’s public return value leaves a footgun that can desync page from the hash/view mode. Drop it from gui/src/use-app-route-state.ts:91-98.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/use-app-route-state.ts` around lines 91 - 98, Remove setPageState
from the public return object of useAppRouteState, while retaining page,
viewMode, toggleGlobalWorkspace, and navigateToPage. Update any related hook
typings or consumers if needed so the exposed API no longer permits direct
route-state mutation.

…t storage

Style select-option-active so keyboard navigation is visible without hover, and drive Providers hashes from in-memory viewMode so a localStorage failure cannot revert a live Classic/Workspace toggle.
@Wibias

Wibias commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
gui/src/use-app-route-state.ts (1)

102-109: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

setPageState is still exposed on the public hook API.

Calling it directly bypasses navigateHash/providersHashForViewMode, letting page desync from the URL hash and viewMode. Only page, viewMode, toggleGlobalWorkspace, and navigateToPage are consumed by App.tsx and the test harness.

♻️ Proposed fix
   return {
     page,
     viewMode,
-    setPageState,
     toggleGlobalWorkspace,
     navigateToPage,
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/use-app-route-state.ts` around lines 102 - 109, Remove setPageState
from the object returned by the useAppRouteState hook, while retaining its
internal use where needed. Keep page, viewMode, toggleGlobalWorkspace, and
navigateToPage exposed so callers use navigateToPage and preserve URL
hash/viewMode synchronization.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gui/tests/dashboard-contracts.test.ts`:
- Around line 19-32: Replace the source-text assertions in the workspace
landmark and multi-agent guidance tests with rendered Dashboard.tsx behavior
tests, mocking network dependencies as needed. Use DOM queries to verify the
workspace section’s accessible label and landmark structure, and render both
enabled and disabled guidance states to assert the actual controls’ disabled
visibility/gating behavior. Remove the brittle string and regex checks for
dashboard-workspace-main, dash.workspace.sections, multiAgentGuidanceEnabled,
and models.v2Mode_.

---

Duplicate comments:
In `@gui/src/use-app-route-state.ts`:
- Around line 102-109: Remove setPageState from the object returned by the
useAppRouteState hook, while retaining its internal use where needed. Keep page,
viewMode, toggleGlobalWorkspace, and navigateToPage exposed so callers use
navigateToPage and preserve URL hash/viewMode synchronization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9f3a5a05-6091-4afc-a154-772ec73fa0e9

📥 Commits

Reviewing files that changed from the base of the PR and between 9e66467 and 421eb44.

📒 Files selected for processing (6)
  • gui/src/styles.css
  • gui/src/use-app-route-state.ts
  • gui/tests/dashboard-contracts.test.ts
  • gui/tests/providers-hash-history.test.tsx
  • gui/tests/select-keyboard.test.tsx
  • tests/provider-workspace-rail.test.ts

Comment on lines +19 to +32
test("Dashboard workspace pane is a labelled section, not a nested main landmark", async () => {
const src = await Bun.file(new URL("../src/pages/Dashboard.tsx", import.meta.url)).text();
expect(src).toContain("dashboard-workspace-main");
expect(src).toContain("dash.workspace.sections");
expect(src).not.toMatch(/<main\b[^>]*dashboard-workspace-main/);
expect(src).toMatch(/<(section)\b[^>]*dashboard-workspace-main/);
});

test("multi-agent guidance gates injection controls and Active badge on the enabled flag", async () => {
const src = await Bun.file(new URL("../src/pages/Dashboard.tsx", import.meta.url)).text();
expect(src).toContain("!multiAgentGuidanceEnabled");
expect(src).toContain("multiAgentGuidanceEnabled &&");
expect(src).toContain("models.v2Mode_");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two more contract tests added via raw source-text scraping instead of runtime behavior.

Past reviews on this file already flagged this pattern for the diagnostics/project-config test: asserting on literal substrings/regex of Dashboard.tsx's source text is brittle (a cosmetic rename/reformat fails it) and can miss real regressions (a bug that happens to retain the matched substring passes). Lines 19-25 and 27-32 add two more instances of the same anti-pattern for the workspace landmark and multi-agent gating contracts.

Prefer rendering Dashboard.tsx (mocking network) and asserting on the actual DOM: query for the <section> landmark's aria-label/role instead of regexing the JSX source, and render the guidance controls to assert the real disabled/gating wiring.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/tests/dashboard-contracts.test.ts` around lines 19 - 32, Replace the
source-text assertions in the workspace landmark and multi-agent guidance tests
with rendered Dashboard.tsx behavior tests, mocking network dependencies as
needed. Use DOM queries to verify the workspace section’s accessible label and
landmark structure, and render both enabled and disabled guidance states to
assert the actual controls’ disabled visibility/gating behavior. Remove the
brittle string and regex checks for dashboard-workspace-main,
dash.workspace.sections, multiAgentGuidanceEnabled, and models.v2Mode_.

@Wibias
Wibias merged commit daa2e9d into lidge-jun:dev Jul 25, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant