feat: shared chat composer context with @file mentions and slash commands#67
feat: shared chat composer context with @file mentions and slash commands#67Cheezeiii365 wants to merge 12 commits intomainfrom
Conversation
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>
📝 WalkthroughWalkthroughIntroduces 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
💡 Codex Review
aIDE/packages/main/src/index.ts
Lines 779 to 781 in c46fb4b
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".
There was a problem hiding this comment.
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 | 🟡 MinorReset the title before loading another conversation.
When
chatGetHistory()returnsnull, orconversationGet()does not find metadata, the previous conversation's title remains visible. ResetconversationTitleto'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 | 🟠 MajorKeep theme palettes under
[data-theme], not global:root.Moving these values to
:rootmakes 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 withdata-themeattribute 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 | 🟡 MinorHydrating an existing session never restores persisted totals.
This path repopulates messages/model/backend state, but
totalCostUsdandtotalTokensstay 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 | 🟡 MinorAvoid hardcoding the assistant label to a single provider.
At Line 47,
"Claude"will mislabel messages ifMessageBubbleis reused across different LLM backends, since theChatMessagetype 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 | 🟡 MinorAdd
aria-labelfor explicit accessible name on icon-only button.The
titleattribute alone is insufficient for accessible naming on icon-only buttons per WAI-ARIA guidance. Icon-only buttons require an explicit accessible name (viaaria-label,aria-labelledby, or text content). Addaria-labelhere: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 | 🟡 MinorCount reasoning tokens when deciding whether to render this badge.
hasTokensignorestokens.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 | 🟡 MinorAdd an explicit accessible name to the kebab trigger.
Icon-only buttons are announced as the glyph in some AT, and
titleis not a reliable accessible name. Addaria-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 | 🟡 MinorOnly render auth flows the provider actually supports.
Once
authMethodsis 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 | 🟡 MinorCount tool toggles as session overrides.
hasOverridesignoresstate.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 | 🟡 MinorRemove 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 | 🟡 MinorTrack the caret in state; DOM reads alone won't keep autocomplete in sync.
selectionStartchanges when the user clicks or arrows around inside the textarea, but those cursor moves do not rerender this component. That leavestriggercomputed 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 | 🟡 MinorEscape does not actually dismiss the suggestions list.
setValue((current) => current)is a no-op for React state, so pressing Escape here keeps the sametriggerand 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 | 🟡 MinorEmit token usage in the structured
resultevent too.
useCliAgentupdates the session badge from result payloads, not from the rendered"Completed in ..."string. Right now Codex computesoutputTokenshere 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 | 🟡 MinorAutocomplete and submit disagree on indented slash commands.
buildComposerSubmission()trims before parsing, so" /plan"is still submitted as a command, butgetComposerTrigger()suppresses autocomplete unless the/is literally at index0. 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 nullAlso 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
useEffectsimilar 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 importingBadgeVariantfrom the Badge component.The
BadgeVarianttype 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 unusedviimport.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 exportingButtonVariantandButtonSizetypes.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 memoizingthemePickerItemscomputation.The
themePickerItemsarray 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
globMatchimplementation 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(...)/tomatovalues 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 withdata-themeattribute 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 toExternalCliBackend.The adapter layer introduced in this PR is explicitly external-backend-only, but
WindowApi.cliAgentStart/cliAgentSwitchBackendstill acceptAgentBackend, 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 matchingWindowApimethods still surfaceunknown. 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-themeattribute 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:richPayloadis created but unused.The
richPayloadobject 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (89)
docs/IDE_BUILD_PLAN.mddocs/THEME_FILES.mddocs/cli-agent-backend-hotswap-report.mdelectron-builder.ymlelectron.vite.config.tspackage.jsonpackages/main/package.jsonpackages/main/src/chat/agentManager.tspackages/main/src/chat/approvalRouter.tspackages/main/src/chat/chatComposerContext.tspackages/main/src/chat/cliAdapters/claudeCodeAdapter.tspackages/main/src/chat/cliAdapters/codexAdapter.tspackages/main/src/chat/cliAdapters/openCodeAdapter.tspackages/main/src/chat/cliAdapters/openCodePartConverter.tspackages/main/src/chat/cliAdapters/openCodePermissionBridge.tspackages/main/src/chat/cliAdapters/types.tspackages/main/src/chat/cliAgentManager.tspackages/main/src/chat/conversationStore.tspackages/main/src/chat/openCodeServerHost.tspackages/main/src/chat/permissionMatching.tspackages/main/src/index.tspackages/main/src/preload.tspackages/main/src/themes/builtins.tspackages/main/src/themes/themeRegistry.tspackages/main/src/workspace/WorkspaceRuntime.tspackages/main/src/workspace/runtimeTypes.tspackages/main/src/workspace/settingsResolver.tspackages/renderer/src/commands/context.tspackages/renderer/src/commands/domains/agent.tspackages/renderer/src/commands/domains/theme.tspackages/renderer/src/commands/registerAppCommands.tspackages/renderer/src/components/chat/ChatInput.tsxpackages/renderer/src/components/chat/MessageBubble.tsxpackages/renderer/src/components/chat/MessageList.tsxpackages/renderer/src/components/chat/ModeSelector.tsxpackages/renderer/src/components/chat/PermissionTierBadge.tsxpackages/renderer/src/components/chat/ToolCallCard.tsxpackages/renderer/src/components/chat/WorkingSetPicker.tsxpackages/renderer/src/components/cliAgent/CostTokenBadge.tsxpackages/renderer/src/components/cliAgent/RichPartRenderer.tsxpackages/renderer/src/components/cliAgent/SessionMenu.tsxpackages/renderer/src/components/cliAgent/SessionSettingsPopover.tsxpackages/renderer/src/components/layout/AppShell.tsxpackages/renderer/src/components/layout/DockviewContainer.tsxpackages/renderer/src/components/layout/ThemeToggle.tsxpackages/renderer/src/components/panes/ChatHistoryPane.tsxpackages/renderer/src/components/panes/ChatPane.tsxpackages/renderer/src/components/panes/CliAgentPane.tsxpackages/renderer/src/components/panes/OpenCodeToolsPane.tsxpackages/renderer/src/components/panes/SettingsPane.tsxpackages/renderer/src/components/settings/OpenCodeProvidersTab.tsxpackages/renderer/src/components/settings/SettingsContent.tsxpackages/renderer/src/components/ui/Badge.tsxpackages/renderer/src/components/ui/Button.tsxpackages/renderer/src/components/ui/SegmentedControl.tsxpackages/renderer/src/hooks/useChat.tspackages/renderer/src/hooks/useCliAgent.tspackages/renderer/src/hooks/useTheme.tspackages/renderer/src/lib/agentBackend.tspackages/renderer/src/lib/chatComposer.tspackages/renderer/src/lib/editor/editorTheme.tspackages/renderer/src/lib/settingsSchema.tspackages/renderer/src/lib/workspace/workspaceSwitcher.tspackages/renderer/src/main.tsxpackages/renderer/src/styles/chat-history-pane.csspackages/renderer/src/styles/chat-pane.csspackages/renderer/src/styles/cli-agent-pane.csspackages/renderer/src/styles/cli-agent-settings.csspackages/renderer/src/styles/global.csspackages/renderer/src/styles/inline-diff.csspackages/renderer/src/styles/settings-pane.csspackages/renderer/src/styles/themes.csspackages/renderer/src/styles/ui.csspackages/shared/src/agentTypes.tspackages/shared/src/cliAgentTypes.tspackages/shared/src/index.tspackages/shared/src/themes.tstests/unit/agentManager.test.tstests/unit/app.test.tsxtests/unit/chatComposer.test.tstests/unit/chatComposerContext.test.tstests/unit/cliAgentApprovalRouter.test.tstests/unit/cliAgentManager.test.tstests/unit/editorTheme.test.tstests/unit/openCodeAdapter.test.tstests/unit/openCodePartConverter.test.tstests/unit/openCodePermissionBridge.test.tstests/unit/sharedIndex.test.tstests/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.tstests/unit/sharedIndex.test.tstests/unit/useCliAgent.test.tsxtests/unit/app.test.tsxtests/unit/agentManager.test.tstests/unit/cliAgentApprovalRouter.test.tstests/unit/chatComposer.test.tstests/unit/chatComposerContext.test.tstests/unit/openCodeAdapter.test.tstests/unit/openCodePartConverter.test.tstests/unit/cliAgentManager.test.tstests/unit/openCodePermissionBridge.test.ts
**/*.{css,scss,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use CSS variable token system with
data-themeattribute for theming, supporting dark and light mode
Files:
packages/renderer/src/components/layout/DockviewContainer.tsxtests/unit/useCliAgent.test.tsxpackages/renderer/src/styles/inline-diff.csspackages/renderer/src/components/panes/ChatPane.tsxpackages/renderer/src/components/chat/MessageList.tsxpackages/renderer/src/main.tsxpackages/renderer/src/components/chat/ModeSelector.tsxtests/unit/app.test.tsxpackages/renderer/src/components/chat/MessageBubble.tsxpackages/renderer/src/components/chat/ToolCallCard.tsxpackages/renderer/src/styles/ui.csspackages/renderer/src/components/chat/WorkingSetPicker.tsxpackages/renderer/src/components/panes/SettingsPane.tsxpackages/renderer/src/components/ui/Button.tsxpackages/renderer/src/components/ui/SegmentedControl.tsxpackages/renderer/src/components/panes/ChatHistoryPane.tsxpackages/renderer/src/components/layout/ThemeToggle.tsxpackages/renderer/src/components/ui/Badge.tsxpackages/renderer/src/components/settings/OpenCodeProvidersTab.tsxpackages/renderer/src/styles/chat-history-pane.csspackages/renderer/src/components/cliAgent/SessionSettingsPopover.tsxpackages/renderer/src/styles/cli-agent-settings.csspackages/renderer/src/components/cliAgent/SessionMenu.tsxpackages/renderer/src/styles/settings-pane.csspackages/renderer/src/styles/themes.csspackages/renderer/src/styles/cli-agent-pane.csspackages/renderer/src/styles/global.csspackages/renderer/src/components/chat/PermissionTierBadge.tsxpackages/renderer/src/components/layout/AppShell.tsxpackages/renderer/src/components/panes/CliAgentPane.tsxpackages/renderer/src/components/cliAgent/RichPartRenderer.tsxpackages/renderer/src/components/settings/SettingsContent.tsxpackages/renderer/src/components/chat/ChatInput.tsxpackages/renderer/src/components/cliAgent/CostTokenBadge.tsxpackages/renderer/src/styles/chat-pane.csspackages/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.tselectron-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.tspackages/renderer/src/components/layout/DockviewContainer.tsxpackages/renderer/src/commands/context.tspackages/renderer/src/commands/registerAppCommands.tspackages/renderer/src/components/panes/ChatPane.tsxpackages/renderer/src/components/chat/MessageList.tsxpackages/renderer/src/main.tsxpackages/renderer/src/components/chat/ModeSelector.tsxpackages/main/src/workspace/WorkspaceRuntime.tspackages/renderer/src/components/chat/MessageBubble.tsxpackages/renderer/src/lib/workspace/workspaceSwitcher.tspackages/renderer/src/components/chat/ToolCallCard.tsxpackages/main/src/themes/builtins.tspackages/shared/src/agentTypes.tspackages/main/src/chat/chatComposerContext.tspackages/renderer/src/components/chat/WorkingSetPicker.tsxpackages/renderer/src/components/panes/SettingsPane.tsxpackages/renderer/src/components/ui/Button.tsxpackages/renderer/src/components/ui/SegmentedControl.tsxpackages/renderer/src/components/panes/ChatHistoryPane.tsxpackages/renderer/src/components/layout/ThemeToggle.tsxpackages/renderer/src/components/ui/Badge.tsxpackages/renderer/src/components/settings/OpenCodeProvidersTab.tsxpackages/renderer/src/commands/domains/theme.tspackages/renderer/src/lib/agentBackend.tspackages/renderer/src/lib/editor/editorTheme.tspackages/renderer/src/components/cliAgent/SessionSettingsPopover.tsxpackages/renderer/src/hooks/useChat.tspackages/renderer/src/commands/domains/agent.tspackages/renderer/src/components/cliAgent/SessionMenu.tsxpackages/shared/src/themes.tspackages/main/src/chat/approvalRouter.tspackages/main/src/chat/cliAdapters/claudeCodeAdapter.tspackages/renderer/src/components/chat/PermissionTierBadge.tsxpackages/main/src/chat/permissionMatching.tspackages/main/src/chat/cliAdapters/openCodeAdapter.tspackages/renderer/src/components/layout/AppShell.tsxpackages/main/src/chat/agentManager.tspackages/main/src/chat/conversationStore.tspackages/renderer/src/components/panes/CliAgentPane.tsxpackages/main/src/chat/cliAdapters/codexAdapter.tspackages/renderer/src/components/cliAgent/RichPartRenderer.tsxpackages/renderer/src/components/settings/SettingsContent.tsxpackages/main/src/themes/themeRegistry.tspackages/renderer/src/hooks/useTheme.tspackages/main/src/workspace/settingsResolver.tspackages/renderer/src/components/chat/ChatInput.tsxpackages/renderer/src/components/cliAgent/CostTokenBadge.tsxpackages/main/src/chat/openCodeServerHost.tspackages/renderer/src/hooks/useCliAgent.tspackages/main/src/index.tspackages/renderer/src/lib/chatComposer.tspackages/main/src/chat/cliAdapters/types.tspackages/main/src/chat/cliAdapters/openCodePartConverter.tspackages/renderer/src/lib/settingsSchema.tspackages/main/src/chat/cliAdapters/openCodePermissionBridge.tspackages/shared/src/cliAgentTypes.tspackages/renderer/src/components/panes/OpenCodeToolsPane.tsxpackages/main/src/preload.tspackages/shared/src/index.tspackages/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.tspackages/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.tsxpackages/renderer/src/components/panes/ChatPane.tsxpackages/renderer/src/lib/workspace/workspaceSwitcher.tspackages/renderer/src/components/panes/SettingsPane.tsxpackages/renderer/src/components/layout/AppShell.tsxpackages/renderer/src/components/panes/CliAgentPane.tsxpackages/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.cssdocs/THEME_FILES.mdpackages/main/src/themes/builtins.tspackages/renderer/src/components/layout/ThemeToggle.tsxpackages/renderer/src/lib/editor/editorTheme.tspackages/renderer/src/styles/themes.csspackages/shared/src/themes.tspackages/renderer/src/styles/global.cssdocs/IDE_BUILD_PLAN.mdpackages/renderer/src/components/settings/SettingsContent.tsxpackages/main/src/themes/themeRegistry.tspackages/renderer/src/hooks/useTheme.tspackages/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.tsxpackages/renderer/src/components/ui/Button.tsxpackages/renderer/src/components/ui/SegmentedControl.tsxpackages/renderer/src/components/cliAgent/RichPartRenderer.tsxpackages/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.mdpackages/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)
| // 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, | ||
| }) |
There was a problem hiding this comment.
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.
| 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' |
There was a problem hiding this comment.
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
| 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} |
There was a problem hiding this comment.
🧩 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 viaaria-labeloraria-labelledby. [1] - Each tab has
role="tab"and is a child of thetablist. [1] - Each content panel has
role="tabpanel". [1] - Each
tabpoints to its panel witharia-controls="panel-id". [1] - The active tab has
aria-selected="true"; all other tabs havearia-selected="false". [1] - Each
tabpanelpoints back to its tab witharia-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 aretabindex="-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 Arrowbehave like left/right. [1]
- Horizontal:
SpaceorEnter: 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
tabpanelhas no focusable content, APG notes settingtabindex="0"on thetabpanelso 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-pressedbutton 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.tsxRepository: 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.
| // 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() | ||
| } |
There was a problem hiding this comment.
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.
| // 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // 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.
| 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 |
There was a problem hiding this comment.
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.
| 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') }, | ||
| ]) | ||
| } |
There was a problem hiding this comment.
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.
Summary
chatComposer,chatComposerContext) used by both Claude Agent SDK and OpenCode CLI panes@filecontext mentions and slash-command autocomplete to ChatInputTest plan
Summary by CodeRabbit
New Features
Documentation