Skip to content

feat: shared chat composer context with @file mentions and slash commands#67

Open
Cheezeiii365 wants to merge 12 commits intomainfrom
feat/agentsdk
Open

feat: shared chat composer context with @file mentions and slash commands#67
Cheezeiii365 wants to merge 12 commits intomainfrom
feat/agentsdk

Conversation

@Cheezeiii365
Copy link
Copy Markdown
Owner

@Cheezeiii365 Cheezeiii365 commented Apr 8, 2026

Summary

  • Introduce shared chat composer pipeline (chatComposer, chatComposerContext) used by both Claude Agent SDK and OpenCode CLI panes
  • Add @file context mentions and slash-command autocomplete to ChatInput
  • Wire composer submissions through agentManager / cliAgentManager with new tests

Test plan

  • pnpm test

Summary by CodeRabbit

  • New Features

    • Added multi-backend AI agent support (Claude Code, OpenCode, Codex) with hot-swapping between backends.
    • Introduced chat composition with @file mentions, /slash commands, and contextual file inclusion.
    • Implemented customizable theme system with user-defined JSON themes, appearance-based defaults, and runtime reloading.
    • Added OpenCode workspace tools pane with file browser, search, shell execution, and provider management.
    • Enhanced CLI agent sessions with per-backend configuration overrides and cost/token tracking.
  • Documentation

    • Added theme file format documentation and IDE build plan updates.

Cheezeiii365 and others added 12 commits April 8, 2026 15:49
Resolve the unpacked @anthropic-ai/claude-agent-sdk cli.js before falling
back to the legacy claude-code path, fixing agent startup failures in
packaged apps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce reusable Button and related UI components and migrate chat pane
elements to use them for a more consistent look and feel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds opencode as a first-class CLI backend across the renderer, introduces
a backend switcher in CliAgentPane, and renders per-message backend badges
for mixed-backend transcripts. Centralizes backend label/CLI-detection
helpers in lib/agentBackend.ts so routing, history, and settings stop
hardcoding claude-code/codex checks. Forward-compatible with the upcoming
cliAgentSwitchBackend IPC: falls back to start() when unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts:
#	packages/shared/src/cliAgentTypes.ts
#	packages/shared/src/index.ts
Replace hardcoded one-dark/one-light checks with a manifest-based theme
registry. Built-in and user-installed themes share the same shape and
load from the Electron user data themes folder. Settings persist active
theme plus default dark/light ids, and the command palette exposes
selection, default switching, registry reload, and folder open actions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Promote OpenCode from a minimal per-turn shell to a first-class backend
that exposes the SDK's session, provider, permission, and rich-part
surfaces while reusing the IDE's existing permission tier and per-
workspace cost tracking. Persistent OpenCodeServerHost owns one server
per workspace with a fanned-out SSE pump; an ApprovalRouter routes
CHAT_TOOL_* IPC to whichever manager owns the toolCallId so OpenCode
permission prompts surface in the built-in approval UI. Adds rich-part
rendering, session settings pickers, diagnostics, TUI controls, an
OpenCode tools pane, and provider auth UI.

Also fixes the SSE unwrap so OpenCode chats actually reply (subscribe
to /event instead of /global/event), wires the session settings pickers
end-to-end (hook now exposes backendState with optimistic updates;
listOpenCodeTools query field names + Model shape extraction corrected),
and hides the cost badge for the Claude Code CLI harness since it bills
via subscription rather than per-token.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the three stacked disclosures (session settings / diagnostics /
TUI controls) above every OpenCode chat with a single gear-anchored
popover in the header. Diagnostics, init AGENTS.md, and the TUI controls
move into the OpenCodeToolsPane Status tab; the kebab menu trims to
Share / Summarize / Delete remote.

Add a new agent.opencode.* settings namespace (defaultProvider,
defaultModel, defaultAgent, defaultMode, defaultSystemPrompt,
defaultToolToggles) under Settings → Agent → OpenCode Defaults. New
sessions seed their backendStates['opencode'] from these once on first
touch; per-session overrides set from the popover never write back. The
keys are added to SENSITIVE_AGENT_KEYS so workspace .aide/settings.json
files can't override the user's choice. CliAgentManager exposes
updateOpencodeDefaults() and the settings-changed listener calls it so
edits apply live.

Refined terminal-minimal styling for all OpenCode UI surfaces in a new
cli-agent-settings.css: hairline borders, 2px radii, 9px all-caps mono
labels, transparent inputs with focus underlines, dot-grid popover
backdrop, bottom-border tab indicators.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ands

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

Introduces OpenCode as a first-class CLI agent backend alongside Claude Code and Codex with a multi-backend adapter architecture. Replaces hardcoded theme system with registry-backed manifest support. Expands chat composition with file mentions and slash commands. Implements shared tool-approval routing and extensive OpenCode operations IPC surface.

Changes

Cohort / File(s) Summary
Theme System Refactoring
packages/main/src/themes/*, packages/renderer/src/hooks/useTheme.ts, packages/renderer/src/lib/editor/editorTheme.ts, packages/renderer/src/components/layout/ThemeToggle.tsx, packages/renderer/src/components/panes/SettingsPane.tsx, packages/renderer/src/styles/themes.css
Replaced hardcoded one-dark/one-light with dynamic ThemeRegistry supporting built-in and user-defined JSON themes. Introduced theme manifests, token-based CSS variables, persistence of active and default dark/light themes, and async theme reload/directory operations.
OpenCode SDK Integration
packages/main/src/chat/cliAdapters/openCodeAdapter.ts, packages/main/src/chat/cliAdapters/openCodePartConverter.ts, packages/main/src/chat/cliAdapters/openCodePermissionBridge.ts, packages/main/src/chat/openCodeServerHost.ts, packages/shared/src/cliAgentTypes.ts
New OpenCode backend adapter supporting session lifecycle, streaming parts (text, reasoning, tool, patch, step, etc.), permission request bridging to IDE approval system, and comprehensive OpenCode SDK client wrapping for providers/agents/modes/tools and workspace operations.
CLI Backend Architecture
packages/main/src/chat/cliAdapters/types.ts, packages/main/src/chat/cliAdapters/claudeCodeAdapter.ts, packages/main/src/chat/cliAdapters/codexAdapter.ts, packages/main/src/chat/cliAgentManager.ts, packages/main/src/chat/cliAgentManager.ts
Abstracted Claude-specific agent manager into generic multi-backend architecture with pluggable adapters. Added backend state persistence per backend, session resumption, and explicit backend switching with transcript preservation across hot swaps.
Approval Routing & Permission System
packages/main/src/chat/approvalRouter.ts, packages/main/src/chat/permissionMatching.ts, packages/main/src/workspace/WorkspaceRuntime.ts, packages/main/src/workspace/runtimeTypes.ts
Introduced ApprovalRouter registry for routing tool approvals across built-in and CLI managers. Centralized permission tier/autoApprove evaluation in permissionMatching with support for pattern-based tool permission configs.
Chat Composition & Context
packages/main/src/chat/agentManager.ts, packages/main/src/chat/chatComposerContext.ts, packages/main/src/chat/conversationStore.ts, packages/shared/src/agentTypes.ts, packages/renderer/src/lib/chatComposer.ts
Extended chat messages with contextualContent, mentionedFiles, and commandId. Implemented composer context builder that sanitizes file mentions, applies slash commands, and embeds referenced files in prompts. Updated ChatComposerSubmission as unified input type.
OpenCode Renderer Components & Panes
packages/renderer/src/components/cliAgent/*, packages/renderer/src/components/panes/OpenCodeToolsPane.tsx, packages/renderer/src/components/panes/CliAgentPane.tsx, packages/renderer/src/components/settings/OpenCodeProvidersTab.tsx
Added rich-part rendering for OpenCode messages (reasoning, patches, steps, snapshots). New OpenCodeToolsPane with multi-tab workspace/file/search/shell/LSP/status/provider UI. Per-session settings popover for backend overrides. Session menu for share/summarize/delete operations.
UI Components & Theme Foundation
packages/renderer/src/components/ui/*, packages/renderer/src/styles/ui.css, packages/renderer/src/styles/global.css, packages/renderer/src/styles/cli-agent-settings.css
New shared Badge, Button, SegmentedControl components with variants. Added global CSS token variables (fonts, spacing, radii, motion). New theming-focused command context methods for theme picker and reload.
Chat & Input UI Redesign
packages/renderer/src/components/chat/ChatInput.tsx, packages/renderer/src/components/chat/MessageBubble.tsx, packages/renderer/src/components/chat/MessageList.tsx, packages/renderer/src/components/chat/ModeSelector.tsx, packages/renderer/src/components/chat/ToolCallCard.tsx, packages/renderer/src/components/chat/WorkingSetPicker.tsx, packages/renderer/src/styles/chat-pane.css
Chat input now supports @file mentions and /command autocomplete with dropdown suggestions. Message bubbles refactored to use role labels. Tool call cards simplified. Working set picker updated with label and improved accessibility. CSS redesigned with theme tokens and centered max-width layout.
IPC Surface & Main Process
packages/main/src/index.ts, packages/main/src/preload.ts, packages/shared/src/index.ts
Massive expansion of IPC channels and window API surface for theme management (list, set default, reload, open directory), CLI agent backend switching, per-session config, OpenCode provider/agent/mode/tool/session/workspace/shell/LSP/formatter/config/auth operations, and TUI controls.
Settings & Workspace Configuration
packages/main/src/workspace/settingsResolver.ts, packages/renderer/src/lib/settingsSchema.ts, packages/renderer/src/components/settings/SettingsContent.tsx, electron.vite.config.ts, package.json, packages/main/package.json
Added agent.opencodePath, agent.opencode.default* settings for OpenCode session seeding. Updated electron builder config and vite bundling for @opencode-ai/sdk dependency. Settings UI updated for dynamic theme selection with dark/light defaults.
CLI Backend Helpers & Build Config
packages/renderer/src/lib/agentBackend.ts, docs/IDE_BUILD_PLAN.md, docs/THEME_FILES.md, docs/cli-agent-backend-hotswap-report.md, electron-builder.yml
Added isCliBackend, backendLabel, backendBadgeLabel helpers. Updated build plan and added architecture documentation for theme system and backend hot-swapping. Build config updated to unpack @opencode-ai/sdk dependency.
Tests & Test Utilities
tests/unit/chatComposer.test.ts, tests/unit/chatComposerContext.test.ts, tests/unit/cliAgentApprovalRouter.test.ts, tests/unit/cliAgentManager.test.ts, tests/unit/openCodeAdapter.test.ts, tests/unit/openCodePartConverter.test.ts, tests/unit/openCodePermissionBridge.test.ts, tests/unit/agentManager.test.ts, tests/unit/app.test.tsx, tests/unit/editorTheme.test.ts, tests/unit/sharedIndex.test.ts, tests/unit/useCliAgent.test.tsx
Comprehensive test coverage for new modules: chat composer triggers/submission building, context builder with file sanitization/path traversal prevention, approval routing and permission bridge logic, OpenCode adapter events/state/permissions, token extraction/summing, chat message preference for contextual content, theme state handling.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Renderer<br/>(CLI Agent Pane)
    participant Manager as CliAgentManager
    participant Adapter as OpenCode<br/>Adapter
    participant Host as OpenCode<br/>ServerHost
    participant SDK as OpenCode<br/>SDK Client

    Client->>Manager: send(sessionId, submission)
    activate Manager
    Manager->>Manager: buildComposerContext(submission)
    Manager->>Adapter: startTurn(context, emit)
    activate Adapter
    
    Adapter->>Host: getClient()
    activate Host
    Host-->>Adapter: OpencodeClient
    deactivate Host
    
    Adapter->>Adapter: resolve/create session
    alt Session doesn't exist
        Adapter->>SDK: session.create()
        SDK-->>Adapter: sessionId
        Adapter->>Manager: emit('backend-state' patch)
    end
    
    Adapter->>SDK: session.promptAsync(prompt)
    activate SDK
    
    loop Stream OpenCode Events
        SDK-->>Adapter: part event (text/tool/step)
        Adapter->>Adapter: convertOpenCodePart()
        alt Permission Event
            Adapter->>Adapter: permission.updated
            Adapter->>Manager: emit('permission-request')
            Manager->>Manager: route to approval
            Manager->>Adapter: resolve(response)
            Adapter->>Host: respondPermission()
        else Normal Part
            Adapter->>Manager: emit('message'/'stream-delta')
        end
    end
    
    SDK-->>Adapter: session.idle / session.error
    deactivate SDK
    Adapter->>Manager: emit('result' with cost/tokens)
    deactivate Adapter
    
    Manager->>Client: onResult, notify workload
    deactivate Manager
Loading
sequenceDiagram
    participant User as User
    participant App as App Shell<br/>(Theme Context)
    participant Registry as Theme<br/>Registry
    participant Store as Electron<br/>Store
    participant Filesystem as Filesystem

    User->>App: openThemePicker('active')
    activate App
    App->>Registry: getSnapshot()
    activate Registry
    Registry->>Store: load activeThemeId,<br/>defaultDarkThemeId
    Registry->>Filesystem: read *.json from<br/>themes directory
    Filesystem-->>Registry: user theme manifests
    Registry->>Registry: resolve tokens with<br/>fallback built-ins
    Registry-->>App: ThemeStateSnapshot
    deactivate Registry
    App->>App: render theme picker modal
    User->>App: select themeId
    App->>Registry: setActiveTheme(themeId)
    activate Registry
    Registry->>Store: persist activeThemeId
    Registry->>App: emit onChanged(snapshot)
    deactivate Registry
    App->>App: apply theme tokens to CSS
    App->>User: theme visibly changes
    deactivate App
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Poem

🐰 A rabbit's ode to sweeping change:
From hardcoded hues to themes so free,
OpenCode joins the chat with glee,
Files @mentioned in every plea,
Approvals routed, tokens counted—three,
UI buttons bloom on every spree! 🎨✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/agentsdk

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

const agentManager = getAgentManager(rt)
if (!agentManager) continue
out.push(...agentManager.listPendingToolApprovals())

P1 Badge Include CLI approvals in pending-approval hydration list

This handler only pulls pending approvals from AgentManager, but useRuntimeGlobalNotifications hydrates the global inbox from CHAT_PENDING_TOOL_APPROVALS_LIST after renderer reloads. OpenCode permissions are tracked in CliAgentManager.pendingPermissions and are emitted as one-shot CHAT_TOOL_CALL events, so after a reload those pending CLI approvals disappear from the UI and the run can remain blocked with no way to approve/reject from the inbox.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/renderer/src/lib/chatComposer.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (3)
packages/renderer/src/hooks/useChat.ts (1)

49-78: ⚠️ Potential issue | 🟡 Minor

Reset the title before loading another conversation.

When chatGetHistory() returns null, or conversationGet() does not find metadata, the previous conversation's title remains visible. Reset conversationTitle to 'New Chat' before starting this async load so tab/workspace switches don't show stale headers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/hooks/useChat.ts` around lines 49 - 78, Before calling
window.api.chatGetHistory in the useEffect, reset conversationTitle to 'New
Chat' to avoid showing stale titles while async loads complete; specifically,
inside the useEffect (which depends on workspaceId, conversationId, tick) call
setConversationTitle('New Chat') immediately after the workspaceId check and
before invoking window.api.chatGetHistory, and also ensure that when
chatGetHistory resolves with null or when conversationGet returns no meta you do
not preserve the old title (leave it as 'New Chat'); update the logic around
session handling (sessionIdRef, messagesRef, setModeState, setWorkingSetState,
setStatus, tick) so it only runs when a non-null session is returned.
packages/renderer/src/styles/themes.css (1)

1-38: ⚠️ Potential issue | 🟠 Major

Keep theme palettes under [data-theme], not global :root.

Moving these values to :root makes the One Dark palette the app-wide default, so light and user themes can no longer switch these tokens cleanly. The new merge-diff colors inherit the same problem.

As per coding guidelines, **/*.{css,scss,tsx} must "Use CSS variable token system with data-theme attribute for theming, supporting dark and light mode".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/styles/themes.css` around lines 1 - 38, The theme
tokens were declared on :root, making One Dark the global default; move all
palette variables (e.g. --bg-base, --bg-elevated, --bg-hover, --text-primary,
--accent, --accent-rgb, --syntax-*, --merge-*-bg/gutter/char, etc.) out of :root
and declare them instead under a data-theme selector for the One Dark theme
(e.g. [data-theme="one-dark"]) so they apply only when that theme is active;
ensure other themes (light, user) define their own sets of the same variable
names under their respective [data-theme="..."] selectors so theme switching
works correctly and remove duplicate palette declarations from :root.
packages/renderer/src/hooks/useCliAgent.ts (1)

102-112: ⚠️ Potential issue | 🟡 Minor

Hydrating an existing session never restores persisted totals.

This path repopulates messages/model/backend state, but totalCostUsd and totalTokens stay at their initial values. Reopening a conversation will therefore show a zeroed cost/token badge until another turn finishes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/hooks/useCliAgent.ts` around lines 102 - 112, When
hydrating an existing session in useCliAgent (inside the block that checks
session.id === conversationId) the persisted totals aren't being restored; after
restoring messages/model/backend state call the state setters for the persisted
totals (e.g. setTotalCostUsd(session.totalCostUsd ?? 0) and
setTotalTokens(session.totalTokens ?? 0) or the equivalent total/state updater
used to render the cost/token badge) so the UI shows the correct values on
reopen; place these calls alongside setModel, setLastError, setActiveBackend and
setBackendState to ensure totals are hydrated with the rest of the session.
🟡 Minor comments (11)
packages/renderer/src/components/chat/MessageBubble.tsx-47-47 (1)

47-47: ⚠️ Potential issue | 🟡 Minor

Avoid hardcoding the assistant label to a single provider.

At Line 47, "Claude" will mislabel messages if MessageBubble is reused across different LLM backends, since the ChatMessage type carries no provider metadata. Use a neutral label (Assistant) instead.

Suggested fix
-      <div className="chat-msg__role">Claude</div>
+      <div className="chat-msg__role">Assistant</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/chat/MessageBubble.tsx` at line 47, The
component MessageBubble currently hardcodes the assistant label as "Claude" in
the JSX (the div with className "chat-msg__role"); change this to a neutral
label such as "Assistant" or derive the label from message metadata (e.g., use
message.role or a provider prop passed into MessageBubble) so messages are not
misnamed for other LLM backends; update the JSX to reference the neutral/derived
label instead of the literal "Claude" and ensure any tests or usages expecting
"Claude" are updated accordingly.
packages/renderer/src/components/layout/ThemeToggle.tsx-37-42 (1)

37-42: ⚠️ Potential issue | 🟡 Minor

Add aria-label for explicit accessible name on icon-only button.

The title attribute alone is insufficient for accessible naming on icon-only buttons per WAI-ARIA guidance. Icon-only buttons require an explicit accessible name (via aria-label, aria-labelledby, or text content). Add aria-label here:

Suggested patch
     <button
       type="button"
       className="ribbon-icon-btn"
       onClick={() => void toggleTheme()}
+      aria-label={`Switch to ${nextLabel} theme`}
       title={`Switch to ${nextLabel} theme`}
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/layout/ThemeToggle.tsx` around lines 37 -
42, The icon-only button in the ThemeToggle component lacks an explicit
accessible name; update the button element (className "ribbon-icon-btn", onClick
toggleTheme) to include an aria-label that conveys the action (e.g., use
nextLabel to build the string like `Switch to ${nextLabel} theme`) so screen
readers get a clear name in addition to the title attribute. Ensure the
aria-label is kept in sync with nextLabel wherever that variable is
defined/updated within ThemeToggle.
packages/renderer/src/components/cliAgent/CostTokenBadge.tsx-19-20 (1)

19-20: ⚠️ Potential issue | 🟡 Minor

Count reasoning tokens when deciding whether to render this badge.

hasTokens ignores tokens.reasoning, so a turn that only reports reasoning usage renders nothing even though the tooltip includes those tokens. Include reasoning in the total here, and ideally in the visible summary too, so usage badges don't disappear for those responses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/cliAgent/CostTokenBadge.tsx` around lines 19
- 20, The badge currently skips tokens.reasoning when computing hasTokens, so
responses that only consumed reasoning tokens are hidden; update the logic in
CostTokenBadge (the hasTokens computation and the visible summary generation) to
include tokens.reasoning in the summed total (i.e., add tokens.reasoning
alongside tokens.input, tokens.output, tokens.cacheRead, tokens.cacheWrite) and
ensure any UI string that shows the token total uses the same sum so the badge
is shown and the tooltip/summary reflect reasoning usage.
packages/renderer/src/components/cliAgent/SessionMenu.tsx-79-88 (1)

79-88: ⚠️ Potential issue | 🟡 Minor

Add an explicit accessible name to the kebab trigger.

Icon-only buttons are announced as the glyph in some AT, and title is not a reliable accessible name. Add aria-label="Session actions" here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/cliAgent/SessionMenu.tsx` around lines 79 -
88, The kebab button (the element using triggerRef, onClick toggling setOpen,
and className "oc-kebab__trigger") lacks an explicit accessible name; add
aria-label="Session actions" to the button element (in addition to the existing
title) so screen readers get a reliable name for this icon-only trigger, keeping
existing props like disabled and aria-expanded unchanged.
packages/renderer/src/components/settings/OpenCodeProvidersTab.tsx-120-181 (1)

120-181: ⚠️ Potential issue | 🟡 Minor

Only render auth flows the provider actually supports.

Once authMethods is loaded, this card still shows both the API-key and OAuth controls unconditionally. For providers that advertise only one method, that turns normal clicks into avoidable IPC errors. Gate each control on the corresponding auth method type.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/settings/OpenCodeProvidersTab.tsx` around
lines 120 - 181, The provider card renders both API-key and OAuth UI
unconditionally; update OpenCodeProvidersTab to only show the API-key
input/button when authMethods includes a method with type 'apiKey' (or the
provider's API-key id) and only show the OAuth sign-in/input/buttons when
authMethods includes a method with type 'oauth' (or matching id), so clicks
don't invoke window.api.cliAgentAuthSet, cliAgentProviderOauthAuthorize or
cliAgentProviderOauthCallback for unsupported flows; use the existing
authMethods array and provider.id/sessionId/state setters (apiKey, oauthCode,
setAuthStatus) to conditionally render each control block.
packages/renderer/src/components/cliAgent/SessionSettingsPopover.tsx-57-60 (1)

57-60: ⚠️ Potential issue | 🟡 Minor

Count tool toggles as session overrides.

hasOverrides ignores state.toolToggles, so the dot disappears when tool enablement is the only override. Check for a non-empty toggle map here too.

Suggested fix
  const hasOverrides =
-    !!(state.providerID || state.modelID || state.agent || state.mode || state.systemPromptOverride)
+    !!(
+      state.providerID ||
+      state.modelID ||
+      state.agent ||
+      state.mode ||
+      state.systemPromptOverride ||
+      Object.keys(state.toolToggles ?? {}).length > 0
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/cliAgent/SessionSettingsPopover.tsx` around
lines 57 - 60, The visual dot logic in the SessionSettingsPopover computes
hasOverrides but omits state.toolToggles, so add a check for a non-empty
toolToggles collection when computing hasOverrides; update the hasOverrides
expression (referencing hasOverrides and state.toolToggles) to return true if
state.toolToggles exists and contains any entries (e.g.,
Object.keys(state.toolToggles || {}).length > 0 or a values().some(Boolean)
check depending on whether toolToggles is a map or record) in addition to the
existing providerID/modelID/agent/mode/systemPromptOverride checks.
packages/renderer/src/styles/cli-agent-pane.css-372-372 (1)

372-372: ⚠️ Potential issue | 🟡 Minor

Remove unnecessary quotes around font family name.

Stylelint flags "Charter" as unnecessarily quoted. Single-word font family names don't require quotes.

🔧 Proposed fix
-  font-family: ui-serif, 'New York', 'Charter', 'Source Serif Pro', 'Hoefler Text', serif;
+  font-family: ui-serif, 'New York', Charter, 'Source Serif Pro', 'Hoefler Text', serif;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/styles/cli-agent-pane.css` at line 372, The font-family
declaration contains an unnecessary quoted single-word family "Charter"; update
the font-family list in the rule that sets font-family (the line with
font-family: ui-serif, 'New York', 'Charter', 'Source Serif Pro', 'Hoefler
Text', serif;) by removing the quotes around Charter (leave quotes only for
multi-word families like 'New York' and 'Source Serif Pro') so the entry becomes
Charter unquoted to satisfy stylelint.
packages/renderer/src/components/chat/ChatInput.tsx-56-57 (1)

56-57: ⚠️ Potential issue | 🟡 Minor

Track the caret in state; DOM reads alone won't keep autocomplete in sync.

selectionStart changes when the user clicks or arrows around inside the textarea, but those cursor moves do not rerender this component. That leaves trigger computed from a stale caret position until the text changes again, so file/slash suggestions can appear for the wrong location.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/chat/ChatInput.tsx` around lines 56 - 57,
The code reads textareaRef.current?.selectionStart into a local cursor variable
so trigger (computed via useMemo(getComposerTrigger(value, cursor))) can become
stale when the user moves the caret without changing text; fix by tracking the
caret position in React state (e.g., caret or cursor state) and update it from
the textarea event handlers (onSelect/onKeyUp/onClick or onChange) so the
component re-renders on caret moves; replace the local cursor usage with the
state variable and include that state in the useMemo dependency list for trigger
to ensure file/slash suggestions stay in sync with the actual caret.
packages/renderer/src/components/chat/ChatInput.tsx-172-175 (1)

172-175: ⚠️ Potential issue | 🟡 Minor

Escape does not actually dismiss the suggestions list.

setValue((current) => current) is a no-op for React state, so pressing Escape here keeps the same trigger and the same suggestions visible. This needs its own dismissal state, or another state change that invalidates the current trigger.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/chat/ChatInput.tsx` around lines 172 - 175,
The Escape key handler in ChatInput.tsx currently does nothing because
setValue((current) => current) is a no-op; update the onKeyDown logic to clear
or invalidate the active suggestion trigger instead of leaving state unchanged —
e.g., reset the trigger state (the variable/state that holds the current
autocomplete trigger), or set a separate dismissal flag (e.g., suggestionVisible
false) so the suggestions list hides when e.key === 'Escape'; modify the handler
that currently calls setValue to call the trigger-clearing function or
setSuggestionVisible(false) and ensure the suggestions rendering uses that state
to hide the list.
packages/main/src/chat/cliAdapters/codexAdapter.ts-111-131 (1)

111-131: ⚠️ Potential issue | 🟡 Minor

Emit token usage in the structured result event too.

useCliAgent updates the session badge from result payloads, not from the rendered "Completed in ..." string. Right now Codex computes outputTokens here but never includes them in the structured event, so Codex sessions won't accumulate token totals in the UI/persisted session state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/main/src/chat/cliAdapters/codexAdapter.ts` around lines 111 - 131,
In the 'turn.completed' handler in codexAdapter (where sawResult, startedAt and
emit are used), include the computed outputTokens in the structured result event
so the CLI agent can aggregate token totals; modify the second emit call (emit({
type: 'result', durationMs: ..., totalCostUsd: ..., isSuccess: true })) to add a
field such as outputTokens: outputTokens (or output_tokens) populated from the
earlier computed outputTokens variable so token usage flows into session/result
payloads.
packages/renderer/src/lib/chatComposer.ts-62-73 (1)

62-73: ⚠️ Potential issue | 🟡 Minor

Autocomplete and submit disagree on indented slash commands.

buildComposerSubmission() trims before parsing, so " /plan" is still submitted as a command, but getComposerTrigger() suppresses autocomplete unless the / is literally at index 0. That makes slash-command discovery depend on leading whitespace rather than intent.

Suggested fix
-  if (marker === '/' && start !== 0) return null
+  if (marker === '/' && beforeCursor.slice(0, start).trim().length > 0) return null

Also applies to: 116-119

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/lib/chatComposer.ts` around lines 62 - 73,
getComposerTrigger currently blocks slash-command triggers unless the '/' is at
index 0 (start !== 0), which conflicts with buildComposerSubmission that trims
leading whitespace; update the check in getComposerTrigger (and the similar
logic at the other occurrence) to allow a '/' if all characters before the
computed start are only whitespace (e.g., replace the start !== 0 check with a
test like text.slice(0, start).trim() === ''), ensuring marker '/' still rejects
embedded slashes (the existing marker === '/' && query.includes('/') check
remains).
🧹 Nitpick comments (13)
packages/renderer/src/components/panes/ChatHistoryPane.tsx (1)

99-118: Dropdown doesn't close on outside click.

The new-conversation dropdown menu uses e.stopPropagation() to prevent bubbling but lacks a document-level click handler to close when clicking outside (like the context menu has). Users must click the caret again to dismiss.

♻️ Suggested approach

Add a useEffect similar to the context menu handler:

useEffect(() => {
  if (!newMenuOpen) return
  const handler = () => setNewMenuOpen(false)
  document.addEventListener('click', handler)
  return () => document.removeEventListener('click', handler)
}, [newMenuOpen])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/panes/ChatHistoryPane.tsx` around lines 99 -
118, The new-conversation dropdown (controlled by newMenuOpen and toggled via
setNewMenuOpen in ChatHistoryPane) never closes when clicking outside; add a
useEffect in the ChatHistoryPane component that watches newMenuOpen and, when
true, registers a document 'click' listener which calls setNewMenuOpen(false)
and removes the listener on cleanup so outside clicks close the menu (keep the
existing onClick={(e) => e.stopPropagation()} on the menu div and leave
handleNewChat usage unchanged).
packages/renderer/src/components/chat/ToolCallCard.tsx (1)

23-24: Consider importing BadgeVariant from the Badge component.

The BadgeVariant type is defined locally here, but it's likely already exported (or should be) from ../ui/Badge. Importing from the source of truth avoids drift if the Badge component's variants change.

♻️ Suggested change
-import { Badge } from '../ui/Badge'
+import { Badge, type BadgeVariant } from '../ui/Badge'
 import { Button } from '../ui/Button'

 // ...

-type BadgeVariant = 'neutral' | 'success' | 'warning' | 'error' | 'info'
-
 const STATUS_META: Record<
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/chat/ToolCallCard.tsx` around lines 23 - 24,
Replace the local type alias BadgeVariant in ToolCallCard with an import from
the Badge component to avoid type drift: remove the local "type BadgeVariant =
'neutral' | 'success' | 'warning' | 'error' | 'info'" and import the exported
BadgeVariant (or equivalent named export) from the Badge module used by this
component (e.g., from "../ui/Badge"); update any references in ToolCallCard
(props, variables, or state) to use the imported BadgeVariant type. Ensure the
Badge component actually exports that type (add an export in the Badge file if
necessary) so ToolCallCard consumes the source-of-truth variant type.
tests/unit/cliAgentApprovalRouter.test.ts (1)

1-1: Remove unused vi import.

The vi (Vitest mock utilities) import isn't used in this test file.

♻️ Suggested change
-import { describe, it, expect, vi } from 'vitest'
+import { describe, it, expect } from 'vitest'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/cliAgentApprovalRouter.test.ts` at line 1, The import list
includes an unused symbol "vi" from vitest; remove "vi" from the import
statement (i.e., change the import that currently lists describe, it, expect, vi
to only import describe, it, expect) so there are no unused imports in
cliAgentApprovalRouter.test.ts and the test linter stays clean.
packages/renderer/src/components/ui/Button.tsx (1)

4-5: Consider exporting ButtonVariant and ButtonSize types.

Consumers may need these types to dynamically pass variants or annotate props in parent components.

♻️ Suggested change
-type ButtonVariant = 'ghost' | 'outline' | 'accent' | 'danger'
-type ButtonSize = 'sm' | 'md'
+export type ButtonVariant = 'ghost' | 'outline' | 'accent' | 'danger'
+export type ButtonSize = 'sm' | 'md'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/ui/Button.tsx` around lines 4 - 5, Export
the ButtonVariant and ButtonSize type aliases from
packages/renderer/src/components/ui/Button.tsx so consumers can import and reuse
them; locate the type declarations named ButtonVariant and ButtonSize and add
exports (e.g., export type ButtonVariant = ...; export type ButtonSize = ...;)
so parent components can reference these types when annotating props or
dynamically passing variants.
packages/renderer/src/components/layout/AppShell.tsx (1)

589-611: Consider memoizing themePickerItems computation.

The themePickerItems array is recomputed on every render when the picker is open. While the computation is lightweight, memoization would be more idiomatic React.

♻️ Optional: Use useMemo for theme picker items
+const themePickerItems: SearchPanelItem[] = useMemo(() => {
+  if (themePickerMode === null) return []
+  const filtered =
+    themePickerMode === 'dark'
+      ? themes.filter((theme) => theme.appearance === 'dark')
+      : themePickerMode === 'light'
+        ? themes.filter((theme) => theme.appearance === 'light')
+        : themes
+
+  const selectedThemeId =
+    themePickerMode === 'dark'
+      ? defaultDarkThemeId
+      : themePickerMode === 'light'
+        ? defaultLightThemeId
+        : activeThemeId
+
+  return filtered.map((theme) => ({
+    id: theme.id,
+    label: theme.label,
+    description: `${theme.appearance}${theme.id === selectedThemeId ? ' • current' : ''}`,
+    searchText: `${theme.label} ${theme.id} ${theme.appearance} ${theme.source}`,
+  }))
+}, [themePickerMode, themes, defaultDarkThemeId, defaultLightThemeId, activeThemeId])
-const themePickerItems: SearchPanelItem[] =
-  themePickerMode === null
-    ? []
-    : (themePickerMode === 'dark'
-        ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/layout/AppShell.tsx` around lines 589 - 611,
The themePickerItems array is recreated on every render; wrap its computation in
React's useMemo to memoize it and avoid unnecessary recalculation—compute
themePickerItems using useMemo(() => { ... }) and keep the same
filtering/mapping logic but list dependencies: themePickerMode, themes,
defaultDarkThemeId, defaultLightThemeId, and activeThemeId so it updates only
when those change; reference the existing symbols themePickerItems,
themePickerMode, themes, defaultDarkThemeId, defaultLightThemeId, and
activeThemeId when locating where to apply the change.
packages/renderer/src/components/cliAgent/RichPartRenderer.tsx (1)

58-247: Consider extracting inline styles to CSS classes.

The bubble components use extensive inline styles. While functional, extracting these to CSS classes (e.g., .cli-agent-msg--patch__header, .cli-agent-msg--step__indicator) would improve maintainability and enable theming via CSS variables per the project's theming guidelines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/cliAgent/RichPartRenderer.tsx` around lines
58 - 247, The inline style blocks on the bubble components (PatchBubble,
StepBubble, SnapshotBubble, RetryBubble, CompactionBubble, AgentChangeBubble,
SubtaskBubble, FileAttachmentBubble) should be moved to CSS classes: create
semantic class names (e.g., .cli-agent-msg--patch,
.cli-agent-msg--patch__header, .cli-agent-msg--step,
.cli-agent-msg--step__indicator, etc.) or a CSS/SCSS module and replace the
style props with className references; port static style values to CSS rules
using project CSS variables for colors/spacing, keep only truly dynamic styles
as minimal inline styles (e.g., computed widths or runtime colors), and update
the JSX to remove the large style objects and use the new classes to enable
theming and easier maintenance.
packages/main/src/chat/permissionMatching.ts (1)

124-129: Glob pattern may not handle ? wildcard.

The current globMatch implementation only handles * wildcards. If users expect standard glob semantics, ? (single character match) won't work. This is likely acceptable for the current use case (command/path patterns), but worth noting if pattern flexibility expands later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/main/src/chat/permissionMatching.ts` around lines 124 - 129,
globMatch currently only converts '*' to '.*' so '?' in patterns won't match a
single character; update globMatch to treat '?' as a single-character wildcard
by not escaping it and converting it to '.' in the generated regex. Concretely,
change the escape logic in globMatch so it doesn't escape '*' and '?' (e.g. only
escape other regex metacharacters), then replace '*' with '.*' and '?' with '.'
before constructing the RegExp (retain the '^' and '$' anchors and use
regex.test(text)). This preserves existing behavior while adding support for '?'
single-character matches.
packages/renderer/src/components/panes/CliAgentPane.tsx (1)

229-243: Move the toast styling into tokenized CSS instead of hardcoded colors.

The inline rgba(...) / tomato values won't track the active theme, so this toast can look out of place or lose contrast in some themes. A class-based style using existing CSS variables keeps it aligned with the rest of the pane.

As per coding guidelines, **/*.{css,scss,tsx}: Use CSS variable token system with data-theme attribute for theming, supporting dark and light mode.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/panes/CliAgentPane.tsx` around lines 229 -
243, The inline toast styles in CliAgentPane.tsx (the JSX block that renders
{toast && (...)}) should be moved to tokenized CSS: create CSS rules (e.g.,
.cli-agent-toast and modifier .cli-agent-toast--error) that use theme CSS
variables (like --toast-bg, --toast-bg-error, --toast-color, etc.) and
data-theme selectors for light/dark, then replace the inline style object with
className="cli-agent-toast cli-agent-toast--error" (or conditional modifier
based on toast.variant) and keep the existing structure/props (toast.text).
Ensure the new CSS lives in the component stylesheet (.css/.scss used by the
pane) and the class names match the JSX conditional variant handling.
packages/shared/src/index.ts (2)

1175-1184: Narrow the CLI backend parameters to ExternalCliBackend.

The adapter layer introduced in this PR is explicitly external-backend-only, but WindowApi.cliAgentStart / cliAgentSwitchBackend still accept AgentBackend, which includes 'built-in'. That makes an invalid selection compile and fail only at runtime.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared/src/index.ts` around lines 1175 - 1184, Change the CLI
methods to only accept the external-only backend type: update the parameter
types for cliAgentStart and cliAgentSwitchBackend in the Window API declaration
(the functions named cliAgentStart and cliAgentSwitchBackend in this file) from
AgentBackend to ExternalCliBackend so the API cannot be called with the
`'built-in'` option; ensure any callers are updated to pass an
ExternalCliBackend or are type-guarded accordingly.

1196-1287: The new CLI/OpenCode API is still throwing away its own types.

This file exports CliAgentWorkspaceCostSummary, OpenCodeProviderSummary, OpenCodeFileEntry, OpenCodeShellResult, OpenCodeServerInfo, etc., but the matching WindowApi methods still surface unknown. That forces every renderer caller to cast and makes IPC drift invisible to TypeScript right where this contract is supposed to prevent it.

Example of the direction
onCliAgentWorkspaceCost: (
  callback: (summary: CliAgentWorkspaceCostSummary) => void,
) => () => void

cliAgentListProviders: (sessionId: string) => Promise<OpenCodeProviderSummary[]>
cliAgentListAgents: (sessionId: string) => Promise<OpenCodeAgentSummary[]>
cliAgentListTools: (
  sessionId: string,
  providerID: string,
  modelID: string,
) => Promise<OpenCodeToolSummary[]>

cliAgentFileList: (
  sessionId: string,
  path: string,
) => Promise<{ entries?: OpenCodeFileEntry[]; error?: string }>

cliAgentShellRun: (
  sessionId: string,
  command: string,
) => Promise<{ result?: OpenCodeShellResult; error?: string }>

cliAgentServerInfo: (workspaceId: string) => Promise<OpenCodeServerInfo>

You'll also need to import the corresponding DTO types into this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared/src/index.ts` around lines 1196 - 1287, The WindowApi methods
for the new CLI/OpenCode feature are typed as unknowns and should use the
exported DTOs; update signatures such as onCliAgentWorkspaceCost,
cliAgentListProviders, cliAgentListAgents, cliAgentListTools, cliAgentFileList,
cliAgentFileRead, cliAgentShellRun, cliAgentServerInfo, etc. to return or accept
the proper types (e.g., CliAgentWorkspaceCostSummary, OpenCodeProviderSummary[],
OpenCodeAgentSummary[], OpenCodeToolSummary[], { entries?: OpenCodeFileEntry[];
error?: string }, { result?: OpenCodeShellResult; error?: string },
Promise<OpenCodeServerInfo>, etc.), and add imports for those DTOs at the top of
the file so the IPC contract is strongly typed end-to-end. Ensure callback
signatures (onCliAgentWorkspaceCost) use the concrete DTO instead of unknown and
keep the same function names when changing types.
packages/renderer/src/styles/chat-pane.css (1)

236-249: Use a theme token for the dropdown shadow.

This overlay hardcodes a black alpha shadow while the rest of the pane is tokenized, so it will drift on light/custom themes. Reusing the existing shadow token keeps the dropdown consistent with the theme system.

Suggested fix
-  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
+  box-shadow: var(--shadow-lg);

As per coding guidelines "Use CSS variable token system with data-theme attribute for theming, supporting dark and light mode".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/styles/chat-pane.css` around lines 236 - 249, The
dropdown CSS currently hardcodes box-shadow on .chat-working-set__dropdown
(box-shadow: 0 6px 18px rgba(0,0,0,0.35)), which breaks theming; replace that
hardcoded value with the theme shadow token (e.g., use var(--shadow-md) or the
repository's shadow token) so the dropdown uses the same tokenized shadow as
other components, and if the token is missing ensure it is defined in the theme
token set referenced by data-theme.
packages/main/src/chat/cliAgentManager.ts (2)

1510-1524: Dead code: richPayload is created but unused.

The richPayload object at lines 1512-1522 is constructed and immediately voided. While the comment indicates it's reserved for a future channel, creating and discarding objects is unnecessary overhead.

🧹 Remove unused code or guard with a TODO

Either remove the dead code entirely:

     this.getWebContents()?.send(IpcChannels.CHAT_TOOL_CALL, payload)
-
-    // Also emit a structured CLI agent permission request payload for any
-    // surface that wants the rich form (badges, metadata).
-    const richPayload: CliAgentPermissionRequest = {
-      workspaceId: session.workspaceId,
-      sessionId: session.id,
-      toolCallId,
-      backend: 'opencode',
-      title: request.title,
-      category: request.category,
-      pattern: request.pattern,
-      metadata: request.metadata,
-      timestamp: Date.now(),
-    }
-    void richPayload // (channel reserved for future granular UI; CHAT_TOOL_CALL is the active surface)
     this.notifyWorkloadChanged()

Or add a clear TODO if this is planned:

-    void richPayload // (channel reserved for future granular UI; CHAT_TOOL_CALL is the active surface)
+    // TODO(phase-N): Emit richPayload on CLI_AGENT_PERMISSION_REQUEST channel for granular UI
+    void richPayload
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/main/src/chat/cliAgentManager.ts` around lines 1510 - 1524, The
constructed but unused richPayload (type CliAgentPermissionRequest) is dead
code—either remove the object construction and the subsequent void richPayload
line entirely, or if this is intended for future use, keep a minimal placeholder
with a clear TODO comment referencing why it’s retained and where it will be
used (e.g., to be emitted on CHAT_TOOL_CALL); update the surrounding code that
builds richPayload (references: richPayload, CliAgentPermissionRequest, session,
toolCallId, notifyWorkloadChanged) accordingly so no unused object is allocated.

579-593: Approval always grants permanent permission.

approveToolCall() resolves with 'always', but the resolve signature allows 'once' as well. If "approve once" semantics are needed for granular permission control, this would require a separate method or parameter.

If this is intentional (approve = permanently trust this category), the current design is fine. Consider documenting this behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/main/src/chat/cliAgentManager.ts` around lines 579 - 593,
approveToolCall currently always grants permanent permission by resolving
pending.resolve('always'); if you need "approve once" semantics make the
approval explicit: either change approveToolCall(sessionId, toolCallId, mode =
'always') to accept a mode parameter ('always' | 'once') and resolve
pending.resolve(mode), or add a new method approveToolCallOnce(sessionId,
toolCallId) that resolves pending.resolve('once'); update callers to pass the
desired mode or call the new method and document the behavior; keep the
delete(this.pendingPermissions) and notifyWorkloadChanged() logic unchanged and
use the existing pendingPermissions map and pending.resolve to implement this.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9fa32358-187b-4d7f-bcac-af9c014636ca

📥 Commits

Reviewing files that changed from the base of the PR and between caa6788 and c46fb4b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (89)
  • docs/IDE_BUILD_PLAN.md
  • docs/THEME_FILES.md
  • docs/cli-agent-backend-hotswap-report.md
  • electron-builder.yml
  • electron.vite.config.ts
  • package.json
  • packages/main/package.json
  • packages/main/src/chat/agentManager.ts
  • packages/main/src/chat/approvalRouter.ts
  • packages/main/src/chat/chatComposerContext.ts
  • packages/main/src/chat/cliAdapters/claudeCodeAdapter.ts
  • packages/main/src/chat/cliAdapters/codexAdapter.ts
  • packages/main/src/chat/cliAdapters/openCodeAdapter.ts
  • packages/main/src/chat/cliAdapters/openCodePartConverter.ts
  • packages/main/src/chat/cliAdapters/openCodePermissionBridge.ts
  • packages/main/src/chat/cliAdapters/types.ts
  • packages/main/src/chat/cliAgentManager.ts
  • packages/main/src/chat/conversationStore.ts
  • packages/main/src/chat/openCodeServerHost.ts
  • packages/main/src/chat/permissionMatching.ts
  • packages/main/src/index.ts
  • packages/main/src/preload.ts
  • packages/main/src/themes/builtins.ts
  • packages/main/src/themes/themeRegistry.ts
  • packages/main/src/workspace/WorkspaceRuntime.ts
  • packages/main/src/workspace/runtimeTypes.ts
  • packages/main/src/workspace/settingsResolver.ts
  • packages/renderer/src/commands/context.ts
  • packages/renderer/src/commands/domains/agent.ts
  • packages/renderer/src/commands/domains/theme.ts
  • packages/renderer/src/commands/registerAppCommands.ts
  • packages/renderer/src/components/chat/ChatInput.tsx
  • packages/renderer/src/components/chat/MessageBubble.tsx
  • packages/renderer/src/components/chat/MessageList.tsx
  • packages/renderer/src/components/chat/ModeSelector.tsx
  • packages/renderer/src/components/chat/PermissionTierBadge.tsx
  • packages/renderer/src/components/chat/ToolCallCard.tsx
  • packages/renderer/src/components/chat/WorkingSetPicker.tsx
  • packages/renderer/src/components/cliAgent/CostTokenBadge.tsx
  • packages/renderer/src/components/cliAgent/RichPartRenderer.tsx
  • packages/renderer/src/components/cliAgent/SessionMenu.tsx
  • packages/renderer/src/components/cliAgent/SessionSettingsPopover.tsx
  • packages/renderer/src/components/layout/AppShell.tsx
  • packages/renderer/src/components/layout/DockviewContainer.tsx
  • packages/renderer/src/components/layout/ThemeToggle.tsx
  • packages/renderer/src/components/panes/ChatHistoryPane.tsx
  • packages/renderer/src/components/panes/ChatPane.tsx
  • packages/renderer/src/components/panes/CliAgentPane.tsx
  • packages/renderer/src/components/panes/OpenCodeToolsPane.tsx
  • packages/renderer/src/components/panes/SettingsPane.tsx
  • packages/renderer/src/components/settings/OpenCodeProvidersTab.tsx
  • packages/renderer/src/components/settings/SettingsContent.tsx
  • packages/renderer/src/components/ui/Badge.tsx
  • packages/renderer/src/components/ui/Button.tsx
  • packages/renderer/src/components/ui/SegmentedControl.tsx
  • packages/renderer/src/hooks/useChat.ts
  • packages/renderer/src/hooks/useCliAgent.ts
  • packages/renderer/src/hooks/useTheme.ts
  • packages/renderer/src/lib/agentBackend.ts
  • packages/renderer/src/lib/chatComposer.ts
  • packages/renderer/src/lib/editor/editorTheme.ts
  • packages/renderer/src/lib/settingsSchema.ts
  • packages/renderer/src/lib/workspace/workspaceSwitcher.ts
  • packages/renderer/src/main.tsx
  • packages/renderer/src/styles/chat-history-pane.css
  • packages/renderer/src/styles/chat-pane.css
  • packages/renderer/src/styles/cli-agent-pane.css
  • packages/renderer/src/styles/cli-agent-settings.css
  • packages/renderer/src/styles/global.css
  • packages/renderer/src/styles/inline-diff.css
  • packages/renderer/src/styles/settings-pane.css
  • packages/renderer/src/styles/themes.css
  • packages/renderer/src/styles/ui.css
  • packages/shared/src/agentTypes.ts
  • packages/shared/src/cliAgentTypes.ts
  • packages/shared/src/index.ts
  • packages/shared/src/themes.ts
  • tests/unit/agentManager.test.ts
  • tests/unit/app.test.tsx
  • tests/unit/chatComposer.test.ts
  • tests/unit/chatComposerContext.test.ts
  • tests/unit/cliAgentApprovalRouter.test.ts
  • tests/unit/cliAgentManager.test.ts
  • tests/unit/editorTheme.test.ts
  • tests/unit/openCodeAdapter.test.ts
  • tests/unit/openCodePartConverter.test.ts
  • tests/unit/openCodePermissionBridge.test.ts
  • tests/unit/sharedIndex.test.ts
  • tests/unit/useCliAgent.test.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (3)
electron.vite.config.*

📄 CodeRabbit inference engine (CLAUDE.md)

Do not build into a dmg unless explicitly told to do so

Files:

  • electron.vite.config.ts
**/tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place all new tests in the /tests directory

Files:

  • tests/unit/editorTheme.test.ts
  • tests/unit/sharedIndex.test.ts
  • tests/unit/useCliAgent.test.tsx
  • tests/unit/app.test.tsx
  • tests/unit/agentManager.test.ts
  • tests/unit/cliAgentApprovalRouter.test.ts
  • tests/unit/chatComposer.test.ts
  • tests/unit/chatComposerContext.test.ts
  • tests/unit/openCodeAdapter.test.ts
  • tests/unit/openCodePartConverter.test.ts
  • tests/unit/cliAgentManager.test.ts
  • tests/unit/openCodePermissionBridge.test.ts
**/*.{css,scss,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use CSS variable token system with data-theme attribute for theming, supporting dark and light mode

Files:

  • packages/renderer/src/components/layout/DockviewContainer.tsx
  • tests/unit/useCliAgent.test.tsx
  • packages/renderer/src/styles/inline-diff.css
  • packages/renderer/src/components/panes/ChatPane.tsx
  • packages/renderer/src/components/chat/MessageList.tsx
  • packages/renderer/src/main.tsx
  • packages/renderer/src/components/chat/ModeSelector.tsx
  • tests/unit/app.test.tsx
  • packages/renderer/src/components/chat/MessageBubble.tsx
  • packages/renderer/src/components/chat/ToolCallCard.tsx
  • packages/renderer/src/styles/ui.css
  • packages/renderer/src/components/chat/WorkingSetPicker.tsx
  • packages/renderer/src/components/panes/SettingsPane.tsx
  • packages/renderer/src/components/ui/Button.tsx
  • packages/renderer/src/components/ui/SegmentedControl.tsx
  • packages/renderer/src/components/panes/ChatHistoryPane.tsx
  • packages/renderer/src/components/layout/ThemeToggle.tsx
  • packages/renderer/src/components/ui/Badge.tsx
  • packages/renderer/src/components/settings/OpenCodeProvidersTab.tsx
  • packages/renderer/src/styles/chat-history-pane.css
  • packages/renderer/src/components/cliAgent/SessionSettingsPopover.tsx
  • packages/renderer/src/styles/cli-agent-settings.css
  • packages/renderer/src/components/cliAgent/SessionMenu.tsx
  • packages/renderer/src/styles/settings-pane.css
  • packages/renderer/src/styles/themes.css
  • packages/renderer/src/styles/cli-agent-pane.css
  • packages/renderer/src/styles/global.css
  • packages/renderer/src/components/chat/PermissionTierBadge.tsx
  • packages/renderer/src/components/layout/AppShell.tsx
  • packages/renderer/src/components/panes/CliAgentPane.tsx
  • packages/renderer/src/components/cliAgent/RichPartRenderer.tsx
  • packages/renderer/src/components/settings/SettingsContent.tsx
  • packages/renderer/src/components/chat/ChatInput.tsx
  • packages/renderer/src/components/cliAgent/CostTokenBadge.tsx
  • packages/renderer/src/styles/chat-pane.css
  • packages/renderer/src/components/panes/OpenCodeToolsPane.tsx
🧠 Learnings (8)
📚 Learning: 2026-03-29T20:00:59.311Z
Learnt from: CR
Repo: Cheezeiii365/aIDE PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-29T20:00:59.311Z
Learning: Applies to electron.vite.config.* : Do not build into a dmg unless explicitly told to do so

Applied to files:

  • electron.vite.config.ts
  • electron-builder.yml
📚 Learning: 2026-03-25T11:00:59.388Z
Learnt from: Cheezeiii365
Repo: Cheezeiii365/aIDE PR: 4
File: packages/renderer/src/components/FileTree/ContextMenu.tsx:16-20
Timestamp: 2026-03-25T11:00:59.388Z
Learning: Since aIDE does not support Windows, path-handling utilities and related code should assume POSIX-style separators (`/`) (e.g., helpers like `dirname`/`basename` that split on `/`). During review, do not require Windows-style backslash (`\\`) support or `path.sep`/cross-platform normalization unless the project explicitly adds Windows support.

Applied to files:

  • packages/main/src/workspace/runtimeTypes.ts
  • packages/renderer/src/components/layout/DockviewContainer.tsx
  • packages/renderer/src/commands/context.ts
  • packages/renderer/src/commands/registerAppCommands.ts
  • packages/renderer/src/components/panes/ChatPane.tsx
  • packages/renderer/src/components/chat/MessageList.tsx
  • packages/renderer/src/main.tsx
  • packages/renderer/src/components/chat/ModeSelector.tsx
  • packages/main/src/workspace/WorkspaceRuntime.ts
  • packages/renderer/src/components/chat/MessageBubble.tsx
  • packages/renderer/src/lib/workspace/workspaceSwitcher.ts
  • packages/renderer/src/components/chat/ToolCallCard.tsx
  • packages/main/src/themes/builtins.ts
  • packages/shared/src/agentTypes.ts
  • packages/main/src/chat/chatComposerContext.ts
  • packages/renderer/src/components/chat/WorkingSetPicker.tsx
  • packages/renderer/src/components/panes/SettingsPane.tsx
  • packages/renderer/src/components/ui/Button.tsx
  • packages/renderer/src/components/ui/SegmentedControl.tsx
  • packages/renderer/src/components/panes/ChatHistoryPane.tsx
  • packages/renderer/src/components/layout/ThemeToggle.tsx
  • packages/renderer/src/components/ui/Badge.tsx
  • packages/renderer/src/components/settings/OpenCodeProvidersTab.tsx
  • packages/renderer/src/commands/domains/theme.ts
  • packages/renderer/src/lib/agentBackend.ts
  • packages/renderer/src/lib/editor/editorTheme.ts
  • packages/renderer/src/components/cliAgent/SessionSettingsPopover.tsx
  • packages/renderer/src/hooks/useChat.ts
  • packages/renderer/src/commands/domains/agent.ts
  • packages/renderer/src/components/cliAgent/SessionMenu.tsx
  • packages/shared/src/themes.ts
  • packages/main/src/chat/approvalRouter.ts
  • packages/main/src/chat/cliAdapters/claudeCodeAdapter.ts
  • packages/renderer/src/components/chat/PermissionTierBadge.tsx
  • packages/main/src/chat/permissionMatching.ts
  • packages/main/src/chat/cliAdapters/openCodeAdapter.ts
  • packages/renderer/src/components/layout/AppShell.tsx
  • packages/main/src/chat/agentManager.ts
  • packages/main/src/chat/conversationStore.ts
  • packages/renderer/src/components/panes/CliAgentPane.tsx
  • packages/main/src/chat/cliAdapters/codexAdapter.ts
  • packages/renderer/src/components/cliAgent/RichPartRenderer.tsx
  • packages/renderer/src/components/settings/SettingsContent.tsx
  • packages/main/src/themes/themeRegistry.ts
  • packages/renderer/src/hooks/useTheme.ts
  • packages/main/src/workspace/settingsResolver.ts
  • packages/renderer/src/components/chat/ChatInput.tsx
  • packages/renderer/src/components/cliAgent/CostTokenBadge.tsx
  • packages/main/src/chat/openCodeServerHost.ts
  • packages/renderer/src/hooks/useCliAgent.ts
  • packages/main/src/index.ts
  • packages/renderer/src/lib/chatComposer.ts
  • packages/main/src/chat/cliAdapters/types.ts
  • packages/main/src/chat/cliAdapters/openCodePartConverter.ts
  • packages/renderer/src/lib/settingsSchema.ts
  • packages/main/src/chat/cliAdapters/openCodePermissionBridge.ts
  • packages/shared/src/cliAgentTypes.ts
  • packages/renderer/src/components/panes/OpenCodeToolsPane.tsx
  • packages/main/src/preload.ts
  • packages/shared/src/index.ts
  • packages/main/src/chat/cliAgentManager.ts
📚 Learning: 2026-03-29T20:00:59.311Z
Learnt from: CR
Repo: Cheezeiii365/aIDE PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-29T20:00:59.311Z
Learning: Applies to renderer/**/*editor*.{ts,tsx} : Use CodeMirror 6 for editor implementation (not Monaco)

Applied to files:

  • tests/unit/editorTheme.test.ts
  • packages/renderer/src/lib/editor/editorTheme.ts
📚 Learning: 2026-03-29T20:00:59.311Z
Learnt from: CR
Repo: Cheezeiii365/aIDE PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-29T20:00:59.311Z
Learning: Applies to renderer/**/*layout*.{ts,tsx} : Use Dockview 5.x for layout management of tiling panes

Applied to files:

  • packages/renderer/src/components/layout/DockviewContainer.tsx
  • packages/renderer/src/components/panes/ChatPane.tsx
  • packages/renderer/src/lib/workspace/workspaceSwitcher.ts
  • packages/renderer/src/components/panes/SettingsPane.tsx
  • packages/renderer/src/components/layout/AppShell.tsx
  • packages/renderer/src/components/panes/CliAgentPane.tsx
  • packages/renderer/src/components/panes/OpenCodeToolsPane.tsx
📚 Learning: 2026-03-29T20:00:59.311Z
Learnt from: CR
Repo: Cheezeiii365/aIDE PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-29T20:00:59.311Z
Learning: Applies to **/*.{css,scss,tsx} : Use CSS variable token system with `data-theme` attribute for theming, supporting dark and light mode

Applied to files:

  • packages/renderer/src/styles/inline-diff.css
  • docs/THEME_FILES.md
  • packages/main/src/themes/builtins.ts
  • packages/renderer/src/components/layout/ThemeToggle.tsx
  • packages/renderer/src/lib/editor/editorTheme.ts
  • packages/renderer/src/styles/themes.css
  • packages/shared/src/themes.ts
  • packages/renderer/src/styles/global.css
  • docs/IDE_BUILD_PLAN.md
  • packages/renderer/src/components/settings/SettingsContent.tsx
  • packages/main/src/themes/themeRegistry.ts
  • packages/renderer/src/hooks/useTheme.ts
  • packages/shared/src/index.ts
📚 Learning: 2026-03-29T20:00:59.311Z
Learnt from: CR
Repo: Cheezeiii365/aIDE PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-29T20:00:59.311Z
Learning: Applies to renderer/**/*.{ts,tsx} : Use React 19 and TypeScript for UI components

Applied to files:

  • packages/renderer/src/main.tsx
  • packages/renderer/src/components/ui/Button.tsx
  • packages/renderer/src/components/ui/SegmentedControl.tsx
  • packages/renderer/src/components/cliAgent/RichPartRenderer.tsx
  • packages/renderer/src/hooks/useTheme.ts
📚 Learning: 2026-03-29T20:00:59.311Z
Learnt from: CR
Repo: Cheezeiii365/aIDE PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-29T20:00:59.311Z
Learning: Always update IDE_BUILD_PLAN.md when making changes to the codebase

Applied to files:

  • docs/IDE_BUILD_PLAN.md
📚 Learning: 2026-03-29T20:00:59.311Z
Learnt from: CR
Repo: Cheezeiii365/aIDE PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-29T20:00:59.311Z
Learning: Applies to renderer/**/*terminal*.{ts,tsx} : Use xterm.js with node-pty for terminal implementation

Applied to files:

  • docs/IDE_BUILD_PLAN.md
  • packages/renderer/src/components/cliAgent/RichPartRenderer.tsx
🪛 ast-grep (0.42.1)
packages/renderer/src/components/cliAgent/RichPartRenderer.tsx

[warning] 51-51: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 LanguageTool
docs/THEME_FILES.md

[style] ~164-~164: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...to the configured default dark theme. - If a configured default dark/light theme n...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 Stylelint (17.6.0)
packages/renderer/src/styles/cli-agent-pane.css

[error] 372-372: Unexpected quotes around "Charter" (font-family-name-quotes)

(font-family-name-quotes)

packages/renderer/src/styles/global.css

[error] 3-3: Expected "BlinkMacSystemFont" to be "blinkmacsystemfont" (value-keyword-case)

(value-keyword-case)


[error] 4-4: Expected "Menlo" to be "menlo" (value-keyword-case)

(value-keyword-case)


[error] 4-4: Expected "Monaco" to be "monaco" (value-keyword-case)

(value-keyword-case)

Comment thread packages/main/src/chat/chatComposerContext.ts
Comment thread packages/main/src/chat/cliAdapters/openCodeAdapter.ts
Comment on lines +143 to +201
// Wait for session.idle (or session.error which sets failedError).
await idlePromise

// Emit any final assistant text accumulators that weren't already emitted.
for (const [messageId, text] of textByMessageId) {
if (!text) continue
if (emittedAssistantIds.has(messageId)) continue
if (seenPartFinalIds.has(messageId)) continue
emittedAssistantIds.add(messageId)
const message: Omit<CliAgentMessage, 'backend'> = {
id: messageId,
type: 'assistant',
content: text,
timestamp: Date.now(),
tokens: tokensByMessageId.get(messageId),
costUsd: costByMessageId.get(messageId),
}
emit({ type: 'message', message })
}

if (failedError) {
emit({
type: 'message',
message: {
id: randomUUID(),
type: 'error',
content: failedError,
timestamp: Date.now(),
},
})
emit({
type: 'result',
durationMs: Date.now() - startedAt,
totalCostUsd,
tokens: totalTokens,
isSuccess: false,
})
return
}

emit({
type: 'message',
message: {
id: randomUUID(),
type: 'result',
content: `Completed in ${((Date.now() - startedAt) / 1000).toFixed(1)}s`,
timestamp: Date.now(),
totalCostUsd,
tokens: totalTokens,
isSuccess: true,
},
})
emit({
type: 'result',
durationMs: Date.now() - startedAt,
totalCostUsd,
tokens: totalTokens,
isSuccess: true,
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Stopping a run can still append a fake success result.

close() resolves idlePromise, but the completion path never checks closed after the wait. So a user-initiated stop can still flush buffered assistant text and emit the synthetic "Completed in ..." success result.

Suggested fix
     // Wait for session.idle (or session.error which sets failedError).
     await idlePromise
+    if (closed) return
 
     // Emit any final assistant text accumulators that weren't already emitted.

Also applies to: 339-355

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/main/src/chat/cliAdapters/openCodeAdapter.ts` around lines 143 -
201, After awaiting idlePromise the completion path must check the adapter stop
flag (closed) before flushing buffered assistant text or emitting the synthetic
success result; modify the code in openCodeAdapter.ts so that immediately after
"await idlePromise" you return early if closed is true (do not emit any messages
or the success "Completed in ..." result), and keep the existing failedError
handling intact; apply the same closed check in the other completion block that
mirrors this logic (the block that emits final assistant text and the result) so
that a user-initiated close never produces post-stop assistant messages or a
success result.

Comment thread packages/main/src/chat/cliAdapters/openCodePartConverter.ts
Comment on lines +127 to +138
export function decideOpenCodePermission(
input: DecideInput,
tier: PermissionTier,
autoApprove: Record<string, boolean | ToolPermissionConfig>,
): OpenCodePermissionResponse | 'prompt' {
const ideTool = ideToolNameForCategory(input.category)
const matchInput = buildMatchInput(input)

const decision = evaluatePermission(ideTool, matchInput, tier, autoApprove)
if (decision === 'allow') return 'always'
if (decision === 'deny') return 'reject'
return 'prompt'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don't collapse multi-command bash approvals into one string.

Joining ['cmd1', 'cmd2'] into "cmd1 && cmd2" turns per-command policy into one glob match. A permissive rule for the first command can end up auto-approving a dangerous second command in the same batch. Evaluate each command separately and only return 'always' when all commands are allowed.

Suggested fix
 export function decideOpenCodePermission(
   input: DecideInput,
   tier: PermissionTier,
   autoApprove: Record<string, boolean | ToolPermissionConfig>,
 ): OpenCodePermissionResponse | 'prompt' {
   const ideTool = ideToolNameForCategory(input.category)
+
+  if (input.category === 'bash' && Array.isArray(input.pattern)) {
+    const decisions = input.pattern.map((command) =>
+      evaluatePermission(ideTool, { command }, tier, autoApprove),
+    )
+    if (decisions.every((decision) => decision === 'allow')) return 'always'
+    if (decisions.some((decision) => decision === 'deny')) return 'reject'
+    return 'prompt'
+  }
+
   const matchInput = buildMatchInput(input)
 
   const decision = evaluatePermission(ideTool, matchInput, tier, autoApprove)
   if (decision === 'allow') return 'always'
   if (decision === 'deny') return 'reject'

Also applies to: 141-150

Comment on lines +24 to +35
role="tablist"
aria-label={ariaLabel}
>
{options.map((opt) => {
const active = opt.value === value
return (
<button
key={opt.value}
type="button"
role="tab"
aria-selected={active}
disabled={disabled}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What are the required ARIA semantics and keyboard interactions for tablist/tabpatterns, and when shouldaria-pressed button groups be used instead?

💡 Result:

tablist / tab / tabpanel: required ARIA semantics (APG)

A tabs widget is specifically a set of tabs that each reveals one associated tab panel (only one panel shown at a time). The core required semantics are:

  • Container has role="tablist" and an accessible name via aria-label or aria-labelledby. [1]
  • Each tab has role="tab" and is a child of the tablist. [1]
  • Each content panel has role="tabpanel". [1]
  • Each tab points to its panel with aria-controls="panel-id". [1]
  • The active tab has aria-selected="true"; all other tabs have aria-selected="false". [1]
  • Each tabpanel points back to its tab with aria-labelledby="tab-id". [1]
  • If the tablist is vertical, set aria-orientation="vertical" (default is horizontal). [1]
  • Focus management is typically “roving tabindex”: only one tab is in the Tab order (tabindex="0"), the rest are tabindex="-1" (commonly aligned with the selected tab). (This is consistent with APG’s keyboard model and common implementations.) [1]

Tabs keyboard interaction (APG)

When focus is within the tablist:

  • Tab
    • Entering the tablist: focus lands on the active tab.
    • Tabbing away: moves to the next element after the tablist (typically the active tabpanel, unless the first meaningful content in the panel is already focusable). [1]
  • Arrow keys move focus between tabs (wrapping):
    • Horizontal: Left Arrow / Right Arrow. [1]
    • Vertical (when aria-orientation="vertical"): Up Arrow / Down Arrow behave like left/right. [1]
  • Space or Enter: activates the focused tab (needed for “manual activation”; optional if you use “automatic activation on focus”). [1]
  • Optional keys:
    • Home / End: move focus to first/last tab (optionally also activate). [1]
    • Shift+F10: opens an associated popup menu (if present). [1]
    • Delete: closes a tab if your UI supports closable tabs. [1]
  • If the active tabpanel has no focusable content, APG notes setting tabindex="0" on the tabpanel so it can receive focus. [1]

When to use aria-pressed button groups instead (and not tabs)

Use toggle buttons (<button aria-pressed="true|false">) when the control represents an on/off pressed state of the button itself, not “select a tab to reveal its tabpanel”. The APG button pattern defines toggle buttons as buttons that expose state via aria-pressed. [2] MDN also notes aria-pressed is for toggle buttons, and it’s most appropriate when the label stays the same while the pressed state changes. [3]

Practical rule:

  • Use tabs when you’re switching between distinct panels/sections that behave like a tabbed interface (one panel shown at a time, each tab labels a panel). [1]
  • Use an aria-pressed button group (often a “segmented control”) when you’re toggling modes/options (e.g., show/hide overlays, formatting toggles like Bold/Italic, “Mute” on/off) and there isn’t a real tabpanel relationship that users should navigate as “tabs”. [2][3]

Important: If your “button group” is mutually exclusive selection (exactly one option selected), that often maps better to a radio group pattern than aria-pressed—but if it’s truly a set of toggleable buttons (including possibly multiple pressed), aria-pressed is the right state.

Sources: Tabs Pattern (APG) [1]; Button Pattern (APG) [2]; MDN aria-pressed reference [3].

[1] Tabs Pattern | APG | WAI | W3C
[2] Button Pattern | APG | WAI | W3C
[3] MDN: aria-pressed attribute


🏁 Script executed:

cat packages/renderer/src/components/ui/SegmentedControl.tsx

Repository: Cheezeiii365/aIDE

Length of output: 1097


Use aria-pressed button-group semantics instead of incomplete tabs pattern.

This SegmentedControl component uses role="tablist" and role="tab" but lacks the required ARIA semantics for a true tabs widget: no aria-controls attributes, no tabpanel elements, no keyboard handling (arrow keys), and no roving tabindex.

Since the component implements mutually exclusive option selection (not tab-panel switching), it should use button-group semantics with aria-pressed="true|false" instead.

Suggested patch
     <div
       className={`ui-segmented${disabled ? ' ui-segmented--disabled' : ''}`}
-      role="tablist"
+      role="group"
       aria-label={ariaLabel}
     >
       {options.map((opt) => {
         const active = opt.value === value
         return (
           <button
             key={opt.value}
             type="button"
-            role="tab"
-            aria-selected={active}
+            aria-pressed={active}
             disabled={disabled}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/components/ui/SegmentedControl.tsx` around lines 24 -
35, The SegmentedControl currently uses role="tablist" and role="tab" without
tab semantics; change it to expose button-group semantics by removing
role="tablist"/role="tab" and instead set aria-pressed on each button (true when
opt.value === value, false otherwise), keep aria-label, preserve disabled
handling, and ensure each button has a unique key (opt.value) and type="button";
update any tests/consumers expecting tabs to use aria-pressed boolean semantics
rather than tab/tabpanel attributes and remove expectations of
aria-controls/tabpanel/roving tabindex/arrow-key handling in SegmentedControl.

Comment on lines +199 to +219
// Optimistic: add user message locally
const userMsg: ChatMessage = {
id: `local-${Date.now()}`,
role: 'user',
content: payload.rawText?.trim() || payload.text.trim(),
contextualContent: payload.text.trim(),
mentionedFiles: payload.mentionedFiles,
commandId: payload.commandId,
timestamp: Date.now(),
}
messagesRef.current = [...messagesRef.current, userMsg]
setStatus('thinking')
streamingContentRef.current = ''
setStreamingContent('')
tick()
}
}, [tick])

const result = await window.api.chatSendMessage(sid, payload)
if ('error' in result) {
setStatus('idle')
tick()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Rollback the optimistic user message when the send fails.

If chatSendMessage() returns an error, the locally appended userMsg stays in messagesRef.current even though the main process never stored it. That leaves ghost messages in the transcript until reload and makes retries easy to duplicate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/hooks/useChat.ts` around lines 199 - 219, The
optimistic user message (userMsg) is appended to messagesRef.current before
sending but never removed on failure; update the error branch after awaiting
window.api.chatSendMessage(sid, payload) to remove that message (e.g., filter
out the message by its id `local-...` from messagesRef.current and call
setMessages/state update accordingly), reset streamingContentRef.current and
setStreamingContent(''), setStatus('idle') and call tick() so the UI and refs
match the real state; locate this logic around the userMsg creation and the
result/error handling for chatSendMessage to implement the rollback.

Comment on lines 82 to 88
// No workspace-wide session lookup: multiple unsaved CLI tabs must not share the first in-memory match.
if (!conversationId) {
setHistoryHydrated(true)
return () => { cancelled = true }
return () => {
cancelled = true
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reset session-scoped state when switching to a new draft.

When conversationId becomes falsy, this branch only flips historyHydrated. The previous sessionIdRef, transcript, title, error, model, totals, and backend state all survive until a later start(). If the prior conversation ended in error, the pane's auto-start guard can skip the restart entirely, and send() will still target the old session id.

Suggested fix
     if (!conversationId) {
+      sessionIdRef.current = null
+      messagesRef.current = []
+      setProcessStatus('stopped')
+      setModel(null)
+      setLastError(null)
+      setConversationTitle('New Chat')
+      setActiveBackend(options.backend ?? null)
+      setBackendState(null)
+      setTotalCostUsd(0)
+      setTotalTokens(null)
+      setStreamingMessageId(null)
+      streamingContentRef.current = ''
+      setStreamingContent('')
       setHistoryHydrated(true)
       return () => {
         cancelled = true
       }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// No workspace-wide session lookup: multiple unsaved CLI tabs must not share the first in-memory match.
if (!conversationId) {
setHistoryHydrated(true)
return () => { cancelled = true }
return () => {
cancelled = true
}
}
// No workspace-wide session lookup: multiple unsaved CLI tabs must not share the first in-memory match.
if (!conversationId) {
sessionIdRef.current = null
messagesRef.current = []
setProcessStatus('stopped')
setModel(null)
setLastError(null)
setConversationTitle('New Chat')
setActiveBackend(options.backend ?? null)
setBackendState(null)
setTotalCostUsd(0)
setTotalTokens(null)
setStreamingMessageId(null)
streamingContentRef.current = ''
setStreamingContent('')
setHistoryHydrated(true)
return () => {
cancelled = true
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/hooks/useCliAgent.ts` around lines 82 - 88, When
conversationId becomes falsy, reset all session-scoped state so a new draft
doesn't reuse the previous session: inside the branch where you call
setHistoryHydrated(true) (the block that currently just sets cancelled = true),
also clear sessionIdRef.current (set to undefined/null), reset the transcript,
title, error, model, totals, and backend state by calling their respective
setter functions or initializers, and ensure any auto-start guard is cleared;
keep the cancelled = true and return cleanup as before so start() and send()
will target a fresh session when invoked.

Comment on lines +319 to +329
const switchBackend = useCallback(async (backend: AgentBackend): Promise<boolean> => {
const sid = sessionIdRef.current
if (!sid) return false
const result = await window.api.cliAgentSwitchBackend(sid, backend)
if ('error' in result) {
setLastError(result.error)
return false
}
}, [workspaceId, conversationId, worktreePath, tick])
setActiveBackend(backend)
setLastError(null)
return true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Backend hot-swap leaves stale per-backend UI state behind.

After a successful switch, only activeBackend changes. backendState and model still point at the previous backend until something else refreshes them, so the settings popover can render and edit the wrong config immediately after a hot-swap.

Suggested fix
     setActiveBackend(backend)
+    setBackendState(null)
+    setModel(null)
     setLastError(null)
     return true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/hooks/useCliAgent.ts` around lines 319 - 329,
switchBackend currently only updates activeBackend, leaving backendState and
model pointing at the previous backend; update those after a successful hot-swap
by applying the new backend's state returned from the API (or resetting to
defaults) so the UI reflects the new backend immediately. In the switchBackend
function, after the API call succeeds and before setLastError(null), call the
appropriate setters (e.g., setBackendState(...) and setModel(...)) with the
payload from window.api.cliAgentSwitchBackend (or fallback empty/default state)
and then setActiveBackend(backend); ensure you reference switchBackend,
setBackendState, setModel and setActiveBackend when making the change.

Comment on lines +69 to +133
function createHighlighting(theme: ThemeDefinition): HighlightStyle {
return HighlightStyle.define([
{ tag: tags.keyword, color: token(theme, '--syntax-keyword', '#c678dd') },
{
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
color: token(theme, '--syntax-tag', '#e06c75'),
},
'.cm-content': { caretColor: '#526fff' },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: '#526fff' },
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
{ backgroundColor: 'rgba(56, 113, 220, 0.12)' },
'.cm-searchMatch': { backgroundColor: '#e2e8f0', outline: '1px solid #cbd5e1' },
'.cm-searchMatch.cm-searchMatch-selected': { backgroundColor: '#bfdbfe' },
'.cm-activeLine': { backgroundColor: 'rgba(0, 0, 0, 0.03)' },
'.cm-selectionMatch': { backgroundColor: 'rgba(56, 113, 220, 0.08)' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: 'rgba(56, 113, 220, 0.15)',
{
tag: [tags.function(tags.variableName), tags.labelName],
color: token(theme, '--syntax-fn', '#61afef'),
},
'.cm-gutters': {
backgroundColor: '#f0f0f1',
color: '#a0a1a7',
border: 'none',
{
tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)],
color: token(theme, '--syntax-number', '#d19a66'),
},
'.cm-activeLineGutter': { backgroundColor: 'rgba(0, 0, 0, 0.04)' },
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#a0a1a7',
{
tag: [tags.definition(tags.name), tags.separator],
color: token(theme, '--text-primary', '#abb2bf'),
},
'.cm-tooltip': {
border: '1px solid #d4d4d5',
backgroundColor: '#f0f0f1',
{
tag: [
tags.typeName,
tags.className,
tags.number,
tags.changed,
tags.annotation,
tags.modifier,
tags.self,
tags.namespace,
],
color: token(theme, '--syntax-number', '#d19a66'),
},
'.cm-tooltip .cm-tooltip-arrow:before': { borderTopColor: '#d4d4d5', borderBottomColor: '#d4d4d5' },
'.cm-tooltip .cm-tooltip-arrow:after': { borderTopColor: '#f0f0f1', borderBottomColor: '#f0f0f1' },
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': { backgroundColor: 'rgba(56, 113, 220, 0.12)', color: '#383a42' },
{
tag: [
tags.operator,
tags.operatorKeyword,
tags.url,
tags.escape,
tags.regexp,
tags.link,
tags.special(tags.string),
],
color: token(theme, '--syntax-attr', '#528bff'),
},
},
{ dark: false },
)
{ tag: [tags.meta, tags.comment], color: token(theme, '--syntax-comment', '#5c6370') },
{ tag: tags.strong, fontWeight: 'bold' },
{ tag: tags.emphasis, fontStyle: 'italic' },
{ tag: tags.strikethrough, textDecoration: 'line-through' },
{
tag: tags.link,
color: token(theme, '--syntax-attr', '#528bff'),
textDecoration: 'underline',
},
{ tag: tags.heading, fontWeight: 'bold', color: token(theme, '--syntax-tag', '#e06c75') },
{
tag: [tags.atom, tags.bool, tags.special(tags.variableName)],
color: token(theme, '--syntax-number', '#d19a66'),
},
{
tag: [tags.processingInstruction, tags.string, tags.inserted],
color: token(theme, '--syntax-string', '#98c379'),
},
{ tag: tags.invalid, color: token(theme, '--text-error', '#ff6b6b') },
])
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep separate light/dark fallback palettes here.

All of these fallbacks are from the dark palette. If a light theme manifest omits some editor or syntax tokens, the editor will now inherit dark gutters/highlighting on a light surface. The previous implementation had distinct light/dark defaults, so the fallback branch should still key off theme.appearance.

Also applies to: 135-203

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/lib/editor/editorTheme.ts` around lines 69 - 133, The
theme fallback colors currently hardcode dark-palette defaults inside
createHighlighting (via token(theme, '--...', '#...')) causing light themes to
inherit dark colors; update the token fallback logic so it returns a light or
dark default based on theme.appearance (e.g., check theme.appearance === 'light'
? lightFallback : darkFallback) and replace the inline '#...' defaults in
createHighlighting (and the similar block around lines 135-203) with calls that
select the appropriate contrast color for each CSS variable; ensure all
references to token(...) in createHighlighting and the subsequent highlighting
blocks use the new appearance-aware fallback function so light manifests get
light defaults and dark manifests keep dark defaults.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant