Conversation
…d circuit-bird theme
- session-info-panel.tsx: added isMaster signal for immediate UI feedback after becomeMaster/leaveBridge - bridge.tsx: fixed bridge poll to include x-opencode-directory header so server reads from correct instance - Added profile list/switch endpoints and UI integration - Enhanced session info panel with bridge controls and session ID display
- Add DialogBecomeFriend modal component for joining bridge as friend by entering master session ID - Add "Become Friend" button in Bridge section of session info panel - Implement logic fixes: - Guard "Become Friend" button with `!bridge.role` condition to prevent conflicts - Disable button when session ID or directory is missing - Use trailing-slash safe slug computation with filter(Boolean) in both components - Track friend state independently and update Leave button visibility accordingly
…rived functions Replaced local createSignal state (isMaster, isFriend) with derived functions that read directly from bridge.role, ensuring bridge status reflects actual server state per directory and fixing stale UI when switching projects.
…f local store The panel was maintaining a redundant local `bridgeInfo` store with its own `createEffect` fetch. On remount (navigating away and back to the same project), the local store reset to null and SolidJS did not re-run the effect because `sdk.directory` hadn't changed value — leaving the bridge role stuck at null. Fix: remove the local store entirely. The panel now reads directly from `bridge.state` (the singleton from `useBridge()`), kept fresh by the 5-second background poller in `bridge.tsx` which is mounted above the router and is never destroyed on navigation. Also included in this commit (from previous sessions): - bridge.tsx: correct directory decoding (base64+URI), navigation-aware polling with opencode-navigate custom event, selfRole from server - bridge/index.ts: export sessionID() - server/routes/bridge.ts: include selfRole + selfNodeID in GET /info - session-header.tsx: use bridge.state.role / bridge.state.sessionID - dialog-become-friend.tsx: add onSuccess callback prop
Contributor
There was a problem hiding this comment.
Pull request overview
This PR makes bridge role/session identity persistent across in-app navigation by moving bridge state into a global provider with polling, and updates both server and client to use server-authoritative bridge role. It also adds profile switching endpoints + UI wiring, plus introduces a new “Circuit Bird” theme.
Changes:
- Add a global
BridgeProviderwith navigation-aware polling; update session UI to read frombridge.staterather than a component-local store. - Extend
GET /bridge/inforesponse withselfRole/selfNodeIDand add a new/profileroute group (list/active/switch) with client bootstrap + UI selector. - Add new Circuit Bird theme assets and register them as defaults.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/ui/src/theme/themes/circuit-bird.json | Adds a new built-in desktop theme definition. |
| packages/ui/src/theme/default-themes.ts | Registers the new “circuit-bird” theme in the default theme map. |
| packages/opencode/src/server/server.ts | Mounts new /profile routes on the server. |
| packages/opencode/src/server/routes/profile.ts | Introduces profile list/active/switch endpoints backed by VuHitra settings. |
| packages/opencode/src/server/routes/bridge.ts | Extends /bridge/info payload with server-authoritative self role/session identity. |
| packages/opencode/src/project/profiles.ts | Ensures default profile is always present in profile listings. |
| packages/opencode/src/cli/cmd/web.ts | Attempts to raise inotify watch limit before starting the web server. |
| packages/opencode/src/bridge/index.ts | Exposes Bridge.sessionID() accessor for API responses. |
| packages/app/src/pages/session/session-info-panel.tsx | New session side panel UI; bridge actions + memory/biblion/indexer status display. |
| packages/app/src/pages/session/dialog-become-friend.tsx | Adds onSuccess callback and performs “become friend” request. |
| packages/app/src/pages/session.tsx | Mounts the new session info side panel with resize handle. |
| packages/app/src/context/layout.tsx | Adds layout state + actions for toggling/resizing the session info panel. |
| packages/app/src/context/global-sync/types.ts | Adds global sync fields for memory/biblion/indexer status + profiles. |
| packages/app/src/context/global-sync/event-reducer.ts | Handles new status/profile events into the store. |
| packages/app/src/context/global-sync/bootstrap.ts | Bootstraps memory/biblion/indexer status and fetches profile list/active. |
| packages/app/src/context/global-sync.tsx | Passes server URL into directory bootstrap for profile fetches. |
| packages/app/src/context/bridge.tsx | New global bridge store with polling + navigation detection. |
| packages/app/src/components/session/session-header.tsx | Adds bridge “master” indicator/copy button + session info panel toggle button. |
| packages/app/src/components/prompt-input.tsx | Adds profile selector UI and profile switch request. |
| packages/app/src/app.tsx | Mounts BridgeProvider above the router so state persists across navigations. |
| .opencode/themes/circuit-bird.json | Adds a theme file for the .opencode theme format. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
Comment on lines
+82
to
+85
| const res = await fetch(`${sdk.url}/bridge/leave`, { | ||
| method: "POST", | ||
| headers: { "x-opencode-directory": dir }, | ||
| }) |
Comment on lines
+47
to
+59
| const res = await fetch(`${sdk.url}/bridge/set-master`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "x-opencode-directory": dir, | ||
| }, | ||
| body: JSON.stringify({ | ||
| sessionID: id, | ||
| slug: title, | ||
| title, | ||
| directory: dir, | ||
| }), | ||
| }) |
Comment on lines
+122
to
+131
| const res = await fetch(`${globalSDK.url}/profile/switch`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "x-opencode-directory": dir, | ||
| }, | ||
| body: JSON.stringify({ name, directory: dir }), | ||
| }) | ||
| if (!res.ok) throw new Error(await res.text()) | ||
| showToast({ variant: "success", title: `Switched to profile "${name}"` }) |
| onClick={() => view().sessionPanel.toggle()} | ||
| aria-label="Session Info" | ||
| aria-expanded={view().sessionPanel.opened()} | ||
| aria-controls="session-info-panel" |
| UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") | ||
| } | ||
| const opts = await resolveNetworkOptions(args) | ||
| await Bun.write("/proc/sys/fs/inotify/max_user_watches", "524288\n").catch(() => {}) |
Comment on lines
+119
to
+129
| async function switchProfile(name: string) { | ||
| const dir = sync.data.path.directory | ||
| try { | ||
| const res = await fetch(`${globalSDK.url}/profile/switch`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "x-opencode-directory": dir, | ||
| }, | ||
| body: JSON.stringify({ name, directory: dir }), | ||
| }) |
Comment on lines
+164
to
+183
| fetch(`${input.url}/profile/list?directory=${encodeURIComponent(input.directory)}`) | ||
| .then((r) => { | ||
| if (!r.ok) throw new Error(`profile/list ${r.status}`) | ||
| return r.json() | ||
| }) | ||
| .then((profiles) => { | ||
| if (!Array.isArray(profiles) || profiles.some((p) => typeof p !== "string")) return | ||
| input.setStore("profiles", profiles) | ||
| }) | ||
| .catch((e) => console.error("[profile/list] fetch failed", e)), | ||
| fetch(`${input.url}/profile/active?directory=${encodeURIComponent(input.directory)}`) | ||
| .then((r) => { | ||
| if (!r.ok) throw new Error(`profile/active ${r.status}`) | ||
| return r.json() | ||
| }) | ||
| .then((active) => { | ||
| if (typeof active !== "string") return | ||
| input.setStore("active_profile", active) | ||
| }) | ||
| .catch((e) => console.error("[profile/active] fetch failed", e)), |
Comment on lines
+547
to
+565
| <Show when={bridge.state.role === "master"}> | ||
| <Button | ||
| variant="ghost" | ||
| class="titlebar-icon w-8 h-6 p-0 box-border" | ||
| title="Bridge master · click to copy session ID" | ||
| aria-label="Bridge master · click to copy session ID" | ||
| onClick={() => | ||
| navigator.clipboard | ||
| .writeText(bridge.state.sessionID ?? "") | ||
| .then(() => | ||
| showToast({ | ||
| variant: "success", | ||
| icon: "circle-check", | ||
| title: "Copied bridge session ID", | ||
| description: bridge.state.sessionID ?? "", | ||
| }), | ||
| ) | ||
| .catch((err: unknown) => showRequestError(language, err)) | ||
| } |
Comment on lines
31
to
+52
| describeRoute({ | ||
| summary: "Get bridge info", | ||
| operationId: "bridge.info", | ||
| responses: { | ||
| 200: { | ||
| description: "Current bridge info or null if not active", | ||
| content: { | ||
| "application/json": { | ||
| schema: resolver(Bridge.Info.nullable()), | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }), | ||
| async (c) => { | ||
| return c.json(Bridge.info()) | ||
| const info = Bridge.info() | ||
| if (!info) return c.json(null) | ||
| return c.json({ | ||
| ...info, | ||
| selfRole: Bridge.role(), | ||
| selfNodeID: Bridge.sessionID(), | ||
| }) |
Comment on lines
+65
to
+67
| await VuHitraSettings.setActiveProfile(name, directory).catch((err: unknown) => { | ||
| throw err | ||
| }) |
- Add Unicode-safe auth headers (TextEncoder btoa) to all raw fetch
calls in bridge.tsx and session-info-panel.tsx
- Clear state when /bridge/info returns null (prevent stale state)
- Fix /bridge/leave to send Content-Type: application/json + body {}
- Remove broken aria-controls from session panel toggle button
- Guard copy button visibility with bridge.state.sessionID truthiness
- Add selfRole/selfNodeID to GET /bridge/info OpenAPI schema
- Use defensive const-sid guard in click handlers instead of ! assertion
- Chain .then()/.catch() on clipboard.writeText promise
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
SessionInfoPanelmaintained a localbridgeInfostore (reset on every component mount) with acreateEffectthat only re-fetched whensdk.directorychanged. On navigating away and back to the same project, the component remounted but SolidJS saw no directory change → effect didn't re-run → role stuck atnull.bridge.state— the global singleton fromBridgeProvider, mounted above the router, kept fresh by a 5-second background poller. It is never reset by component mount/unmount cycles.bridge.tsx,selfRole/selfNodeIDfields added toGET /bridge/inforesponse so clients get the server-authoritative role (not a broken heuristic), navigation-aware polling via customopencode-navigateevent.Files changed
packages/app/src/pages/session/session-info-panel.tsxbridge.statepackages/app/src/context/bridge.tsxselfRolepackages/app/src/components/session/session-header.tsxbridge.state.role/bridge.state.sessionIDpackages/app/src/pages/session/dialog-become-friend.tsxonSuccesscallback proppackages/opencode/src/bridge/index.tssessionID()packages/opencode/src/server/routes/bridge.tsselfRole+selfNodeIDin/bridge/info