feat: popup / side panel display mode toggle#51
Conversation
Let users choose between the toolbar pop-up and Chrome side panel via Settings > Display mode. Side panel reuses the same React app with fluid layout; dApp approvals route to the panel with popup-window fallback. Co-authored-by: Cursor <cursoragent@cursor.com>
When display mode is side panel, always notify the panel and never fall back to approval popup windows if sidePanel.open() fails. Co-authored-by: Cursor <cursoragent@cursor.com>
Covers the case where an approval arrived while the panel was hidden or the runtime message was missed before the panel remounted. Co-authored-by: Cursor <cursoragent@cursor.com>
Open the side panel on the user-gesture chain before provider requests that may need approval. Persist connect/sign/tx pending state to chrome.storage.session so the side panel can restore after SW restarts. Also restyle the display mode settings icon to match other settings icons. Co-authored-by: Cursor <cursoragent@cursor.com>
Use the same solid/hollow evenodd split as the theme icon — a window frame with a cut-out side panel — instead of two thin filled blocks. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep fluid w-full h-full layout on SendReviewScreen from the side panel work. Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes the failing typecheck-and-format CI check. Co-authored-by: Cursor <cursoragent@cursor.com>
chrome.sidePanel and chrome.tabs are unavailable in content scripts, so the previous content-script open call never ran. Open the panel synchronously in the onMessage listener instead, where Chrome preserves the dApp click gesture (any await consumes it), using an in-memory display mode cache. Co-authored-by: Cursor <cursoragent@cursor.com>
- Replace the 15 numbered wallet icons with a new 29-icon pack: 5 kept classics (styles 1, 2, 3, 8, 14 - default unchanged) plus 24 new iris-themed icons, normalised to the shared tintable-SVG convention (var(--fill-0) placeholder, container-sized root). - Add shared/walletStyles.ts as the single source of truth: icon registry with visual families (picker groups similar icons together), a 17-colour palette (7 existing + 10 new in the same poppy scheme), and a deterministic assignment sequence that round-robins across icon families and strides the colour rainbow so consecutive wallets always look distinct; combinations only repeat after all 493 are used. - Switch persisted iconStyleId to stable string ids with transparent migration of legacy numeric ids (retired styles fall back to default). - Drive AccountIcon and WalletStylingScreen from the shared registry. https://claude.ai/code/session_01EPxCTHkLN8QM5iuNR5qqrn
feat: replace wallet icon pack and rework style assignment logic
Greptile SummaryThis PR adds a MetaMask-style display mode toggle (popup ↔ side panel) to the Iris wallet extension, introducing a new
Confidence Score: 3/5The core popup path is unchanged and safe; the new side panel routing works for the happy path but has a gap in approval-queue restoration after a service worker restart. When a sign-raw-tx approval is the active request at the moment the service worker restarts, any other approval requests queued behind it are written to session storage but never promoted to the active slot on restore. GET_PENDING_APPROVAL returns null, so those approvals are silently unreachable until the user re-triggers them from the dApp. extension/shared/pending-approvals-session.ts and the restorePendingApprovalsSession function in extension/background/index.ts need a queue-promotion step when currentRequestId is null after restore.
|
| Filename | Overview |
|---|---|
| extension/background/index.ts | Core routing logic for popup vs side panel; adds display mode cache, gesture hook, session sync, and new message handlers — nock_signRawTx is hardcoded outside APPROVAL_PROVIDER_METHODS |
| extension/shared/pending-approvals-session.ts | New session-persistence module for approvals across SW restarts; sign-raw-tx is correctly excluded (WASM non-serializable) but queued persistable requests are never promoted when the current request is non-persistable |
| extension/popup/hooks/useApprovalDetection.ts | Refactored to support both URL-hash (popup) and runtime-message/poll (side panel) approval detection; ref-based deferred dispatch pattern handles pre-init messages correctly |
| extension/popup/screens/DisplayModeScreen.tsx | New settings screen for popup/side-panel toggle; sidePanel.open() after an await may lose the user gesture but the error is caught and the panel still opens via toolbar action |
| extension/shared/walletStyles.ts | New single source of truth for icon styles and colors; adds normalizeIconStyleId for legacy numeric-to-string migration with clean fallback logic |
| extension/popup/screens/SettingsScreen.tsx | Adds Display mode menu entry (side panel gated); minor duplicate flex-1 class introduced |
| extension/popup/utils/displayContext.ts | New utility providing isSidePanel, isSidePanelSupported, and closeAfterApproval helpers; straightforward and correctly used across approval screens |
| extension/sidepanel/index.tsx | New side panel entry point that reuses the existing Popup component with ThemeProvider; adds sidepanel class to root for CSS differentiation |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant dApp as dApp (content script)
participant BG as Background SW
participant SP as Side Panel UI
dApp->>BG: provider request (e.g. eth_requestAccounts)
Note over BG: maybeOpenSidePanelOnGesture()<br/>(sync, preserves user gesture)
BG->>SP: "chrome.sidePanel.open({tabId})"
BG->>BG: await initPromise / createApprovalPopup()
BG->>BG: persistPendingApprovalSession()
BG->>SP: runtime.sendMessage(APPROVAL_PENDING)
SP->>BG: GET_PENDING_APPROVAL
BG->>SP: "{requestId, approvalType}"
SP->>BG: GET_PENDING_CONNECTION / GET_PENDING_TRANSACTION / ...
SP->>SP: navigate to approval screen
SP->>BG: APPROVE_CONNECTION / APPROVE_TRANSACTION / ...
BG->>dApp: provider response
BG->>BG: syncPendingApprovalsSession() (clear)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant dApp as dApp (content script)
participant BG as Background SW
participant SP as Side Panel UI
dApp->>BG: provider request (e.g. eth_requestAccounts)
Note over BG: maybeOpenSidePanelOnGesture()<br/>(sync, preserves user gesture)
BG->>SP: "chrome.sidePanel.open({tabId})"
BG->>BG: await initPromise / createApprovalPopup()
BG->>BG: persistPendingApprovalSession()
BG->>SP: runtime.sendMessage(APPROVAL_PENDING)
SP->>BG: GET_PENDING_APPROVAL
BG->>SP: "{requestId, approvalType}"
SP->>BG: GET_PENDING_CONNECTION / GET_PENDING_TRANSACTION / ...
SP->>SP: navigate to approval screen
SP->>BG: APPROVE_CONNECTION / APPROVE_TRANSACTION / ...
BG->>dApp: provider response
BG->>BG: syncPendingApprovalsSession() (clear)
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
extension/shared/pending-approvals-session.ts:77-103
**Queued persistable approvals stuck after SW restart when `sign-raw-tx` is active**
When a `sign-raw-tx` is the active request at SW restart time, `buildPendingApprovalSessionSnapshot` cannot include it (WASM objects aren't serializable) so it sets `currentRequestId: null` in the snapshot. If there are queued persistable approvals (e.g., a `connect` or `transaction`), they _are_ written to session storage in `pending` and `queue` — but `restorePendingApprovalsSession` only calls `notifyApprovalPending` when `currentRequestId` is non-null after restore. There is no code path that promotes the head of the restored queue to `currentRequestId`, so those queued requests are permanently stuck: `GET_PENDING_APPROVAL` will return `null`, and the user never sees them after reloading the panel.
### Issue 2 of 3
extension/background/index.ts:974-980
`nock_signRawTx` is hardcoded as a string literal here instead of being added to `APPROVAL_PROVIDER_METHODS` in `side-panel.ts`. If this method name ever changes, or if `APPROVAL_PROVIDER_METHODS` is reused in another check, the raw-tx case will silently diverge. Add `nock_signRawTx` to the set so the background can reference `APPROVAL_PROVIDER_METHODS` exclusively.
```suggestion
const method = msg?.payload?.method;
if (typeof method !== 'string' || !APPROVAL_PROVIDER_METHODS.has(method)) {
return;
}
```
### Issue 3 of 3
extension/popup/screens/SettingsScreen.tsx:99
Duplicate `flex-1` Tailwind class — only one is needed.
```suggestion
<div className="flex flex-col justify-between flex-1 min-h-0">
```
Reviews (1): Last reviewed commit: "Merge pull request #53 from nockbox/clau..." | Re-trigger Greptile
| if (!sessionStorage) { | ||
| return; | ||
| } | ||
|
|
||
| if (!snapshot) { | ||
| await sessionStorage.remove(SESSION_STORAGE_KEYS.PENDING_APPROVALS); | ||
| return; | ||
| } | ||
|
|
||
| await sessionStorage.set({ | ||
| [SESSION_STORAGE_KEYS.PENDING_APPROVALS]: snapshot, | ||
| }); | ||
| } | ||
|
|
||
| export async function loadPendingApprovalSession(): Promise<PendingApprovalSessionSnapshot | null> { | ||
| const sessionStorage = chrome.storage.session; | ||
| if (!sessionStorage) { | ||
| return null; | ||
| } | ||
|
|
||
| const stored = await sessionStorage.get([SESSION_STORAGE_KEYS.PENDING_APPROVALS]); | ||
| const snapshot = stored[SESSION_STORAGE_KEYS.PENDING_APPROVALS] as | ||
| | PendingApprovalSessionSnapshot | ||
| | undefined; | ||
|
|
||
| return snapshot ?? null; | ||
| } |
There was a problem hiding this comment.
Queued persistable approvals stuck after SW restart when
sign-raw-tx is active
When a sign-raw-tx is the active request at SW restart time, buildPendingApprovalSessionSnapshot cannot include it (WASM objects aren't serializable) so it sets currentRequestId: null in the snapshot. If there are queued persistable approvals (e.g., a connect or transaction), they are written to session storage in pending and queue — but restorePendingApprovalsSession only calls notifyApprovalPending when currentRequestId is non-null after restore. There is no code path that promotes the head of the restored queue to currentRequestId, so those queued requests are permanently stuck: GET_PENDING_APPROVAL will return null, and the user never sees them after reloading the panel.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extension/shared/pending-approvals-session.ts
Line: 77-103
Comment:
**Queued persistable approvals stuck after SW restart when `sign-raw-tx` is active**
When a `sign-raw-tx` is the active request at SW restart time, `buildPendingApprovalSessionSnapshot` cannot include it (WASM objects aren't serializable) so it sets `currentRequestId: null` in the snapshot. If there are queued persistable approvals (e.g., a `connect` or `transaction`), they _are_ written to session storage in `pending` and `queue` — but `restorePendingApprovalsSession` only calls `notifyApprovalPending` when `currentRequestId` is non-null after restore. There is no code path that promotes the head of the restored queue to `currentRequestId`, so those queued requests are permanently stuck: `GET_PENDING_APPROVAL` will return `null`, and the user never sees them after reloading the panel.
How can I resolve this? If you propose a fix, please make it concise.| const method = msg?.payload?.method; | ||
| if ( | ||
| typeof method !== 'string' || | ||
| (!APPROVAL_PROVIDER_METHODS.has(method) && method !== 'nock_signRawTx') | ||
| ) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
nock_signRawTx is hardcoded as a string literal here instead of being added to APPROVAL_PROVIDER_METHODS in side-panel.ts. If this method name ever changes, or if APPROVAL_PROVIDER_METHODS is reused in another check, the raw-tx case will silently diverge. Add nock_signRawTx to the set so the background can reference APPROVAL_PROVIDER_METHODS exclusively.
| const method = msg?.payload?.method; | |
| if ( | |
| typeof method !== 'string' || | |
| (!APPROVAL_PROVIDER_METHODS.has(method) && method !== 'nock_signRawTx') | |
| ) { | |
| return; | |
| } | |
| const method = msg?.payload?.method; | |
| if (typeof method !== 'string' || !APPROVAL_PROVIDER_METHODS.has(method)) { | |
| return; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extension/background/index.ts
Line: 974-980
Comment:
`nock_signRawTx` is hardcoded as a string literal here instead of being added to `APPROVAL_PROVIDER_METHODS` in `side-panel.ts`. If this method name ever changes, or if `APPROVAL_PROVIDER_METHODS` is reused in another check, the raw-tx case will silently diverge. Add `nock_signRawTx` to the set so the background can reference `APPROVAL_PROVIDER_METHODS` exclusively.
```suggestion
const method = msg?.payload?.method;
if (typeof method !== 'string' || !APPROVAL_PROVIDER_METHODS.has(method)) {
return;
}
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
|
||
| {/* Content */} | ||
| <div className="flex flex-col justify-between flex-1 h-[536px]"> | ||
| <div className="flex flex-col justify-between flex-1 flex-1 min-h-0"> |
There was a problem hiding this comment.
Duplicate
flex-1 Tailwind class — only one is needed.
| <div className="flex flex-col justify-between flex-1 flex-1 min-h-0"> | |
| <div className="flex flex-col justify-between flex-1 min-h-0"> |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extension/popup/screens/SettingsScreen.tsx
Line: 99
Comment:
Duplicate `flex-1` Tailwind class — only one is needed.
```suggestion
<div className="flex flex-col justify-between flex-1 min-h-0">
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
Most extensions default to the sidebar view these days: maybe we should to? |

Summary
chrome.storage.sessionacross service worker restarts, and re-check pending approvals when the panel becomes visible again.Test plan
dist/in Chrome 114+Made with Cursor