renderer-components
Skill9 starsChanged 4 days agoMITView on GitHub
Reference of the existing renderer components, reusable CSS classes and UI patterns (sidebar views, tree rows, inline inputs, overlays, notifications, dialogs, shortcuts). Check it BEFORE creating a new component in src/renderer to reuse what exists.
In this repository17 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- app-testingSkill
- claude-panelSkill
- commands-settingsSkill
- containersSkill
- debuggerSkill
- files-git-searchSkill
- i18nSkill
- lspSkill
- main-ipcSkill
- main-processesSkill
- packaging-releaseSkill
- renderer-editorSkill
- renderer-storeSkill
- renderer-stylesSkill
- renderer-terminalSkill
- workspacesSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 120
Sidebar views: only the Explorer stays mounted (virtualized.
- Very specific: line 124
Never hide with inline style.display: set hidden and give the class an explicit .x[hidden] { display: none } (a class that sets display …
- Very specific: line 136
All UI text comes from the i18n catalogs (i18n skill): const t = useT() and t.<namespace>.<key>, with pt-BR, English and Spanish written …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Renderer Components (src/renderer/src/components)
Structure
components/
common/ shared primitives and hooks: Button, IconButton, Modal, Select, MenuList, FileTreeRow, use*.ts
editor/ CodeEditor, DiffEditor, MarkdownPreview, ImageViewer, EditorArea/EditorGroup (tab bar), setup.ts, LSP extensions
sidebar/ Explorer, SearchView, GitView (+ CommitBox, GitHistory, GitStashes) — one view per file
quickInput/ QuickInput overlay, item builders, pickers, use*.ts for the file list, symbols and clone URL
claude/ Claude panel: ClaudePanel, Timeline (+ `timeline/` parts), PromptBox (+ `usePromptText`, `usePromptMenus`), cards, ToolRow, AgentsStrip, use*.ts; mcp/ = the /mcp manager
layout/ SidebarSlot, PanelSlot, ClaudeSlot, OverlayHost: the slots of `App`, each owning its selectors
settings/ SettingsModal, SettingItem, DraftField, ShortcutsList
containers/ ContainersView, ContainerGroup, ContainerRow, ContainerDetail
ActivityBar · WorkspaceBar · Welcome · AddWorkspaceModal · RecentRow · StatusBar · BottomPanel · PanelGroupRow ·
ProblemsPanel · ReferencesPanel · OutputPanel · TerminalPanel · TerminalFind · Toasts · Resizer (+ terminalLinks.ts, terminalRegistry.ts)
lib/ pure utilities (path, language, terminalBus…) — no React, testableQuick Index — components
| Component | File | Purpose | ||||
Resizer | components/Resizer.tsx | Drag handle (axis="x" | "y", onDrag(delta)) for panels | ||||
SidebarSlot, PanelSlot, ClaudeSlot, OverlayHost | components/layout/ | The slots App composes. Each subscribes to its own size/visibility (sidebarWidth, panelHeight/panelWidth/panelPosition, claudeWidth) and renders its Resizer, so a drag re-renders only that slot, never App; a new resizable region follows the same pattern. OverlayHost switches on overlay.kind and lazy-loads the heavy modals (a new modal = a new case). App keeps only takeover, viewActive and the editor slot | ||||
Toasts | components/Toasts.tsx | Toast stack at the bottom-right of .main (.toasts > .toast.toast-<tone>, .toast-time bar pauses on hover) — call notify(text, kind) or showToast({...}) from the store, never render toasts manually | ||||
InlineInput | sidebar/Explorer.tsx (local) | Inline text input for create/rename (Enter submits, Esc/blur cancels) | ||||
useExplorerDnd, usePendingRowFocus | sidebar/useExplorerDnd.ts, sidebar/usePendingRowFocus.ts | Explorer hooks. useExplorerDnd(root) → { dropDir, handlers }: drag of a row (move) and of OS files (copy), drop highlight and auto-expand of a collapsed folder under the cursor; spread handlers on the .tree. usePendingRowFocus(treeRef, rows, scrollToRow) → stable focusRow(path): focuses the row once it is in the virtualized DOM (scrolls to it meanwhile, gives up after 3 s or if focus went elsewhere). Explorer keeps rowsRef because onKey must stay a stable callback for the memoized rows | ||||
Icon, FileIcon, Logo, StatusDot | components/common/ | Design-system icon set (name, plain), file/folder icon theme by name (lib/fileIcons.ts resolves: exact name → compound suffix → extension), the Walph mark (variant, size), Claude status dot (`idle | running | done | attention | error`, pulse/ping in CSS) |
IconButton | components/common/IconButton.tsx | The only icon button (.icon-button, 24px). label (required, aria-label) is also the tooltip unless title overrides it (title={null} removes it); shortcut (keys from useShortcutLabel) renders common.withShortcut(label, keys); icon from glyph (design-system) or icon (codicon), or children; active, pressed, busy (aria-disabled + click blocked, focus kept); spreads button attributes and ref (data-testid, disabled…). Never hand-write <button className="icon-button"> | ||||
Button | components/common/Button.tsx | The only text button: variant (primary, secondary (default), ghost, danger), size (md 28px, sm 24px, xs 22px caption), icon (design-system name), busy, type="button" by default, button attributes and ref. Extra className is for placement only (toast-action, recent-open-many) | ||||
CopyButton | components/common/CopyButton.tsx | IconButton that copies text (copyText from clipboard.ts) and shows a check for 1.5 s (label is the idle name; className for placement) | ||||
Section | components/common/ | Collapsible section whose header is a button.section-toggle (aria-expanded, chevron + title + count) with optional hover actions as a sibling (uncontrolled, or controlled via open/onToggle; className for the container's layout). Used by the Git History header and the Outline | ||||
SectionRow | components/common/SectionRow.tsx | The same section header as the group row of a useGroupedTree (treeitem, aria-level 1, aria-expanded, .section-header, chevron + title + actions + count-badge; aria-label = common.withCount(title, n) so the actions revealed by focus stay out of its name). Git Staged / Changes / Stashes | ||||
FileTreeRow, Chevron | components/common/FileTreeRow.tsx, components/common/Chevron.tsx | FileTreeRow: the file row of Git, commit files, Search and Problems/References groups (.tree-row + optional Chevron via open, FileIcon unless icon={false}, name, dir from root or dir/dirClassName, hover actions, trailing badge; extra props such as treeRowProps, role, title, onClick go to the row). Chevron: decorative design-system chevron (.tree-chevron, open rotates it, placeholder reserves the space for files); never swap codicon-chevron-right/down, the expanded state stays on aria-expanded | ||||
FileRow | sidebar/GitView.tsx (local) | Git file row (memo treeitem; open file / discard / stage, or unstage) | ||||
CommitBox, GitHistory, GitStashes | sidebar/ | Commit message + Amend; paged history with expandable commit files (opens read-only commit diffs); stash list with apply/pop/drop | ||||
| Git view keyboard | sidebar/GitView.tsx, GitStashes.tsx, GitHistory.tsx | Three role="tree" lists on useGroupedTree (each one Tab stop): Staged + Changes (groups SectionRow, items FileRow; Enter/Space/click opens the diff), Stashes (one SectionRow group; Enter does nothing, actions by Tab) and the History commits (groups = commits as treeitem .commit-row with aria-expanded, items = the commit files, loaded on expand; Enter/Space/click toggles a commit, opens a file's read-only diff). ↑/↓/Home/End move, ←/→ collapse/expand or go to the group, Tab from a row enters its .row-actions (revealed by :focus-within). Row and group actions use IconButton busy (never disabled={busy}) so the focus survives the operation; when the row leaves (stage, unstage, drop) the hook focuses the next surviving row. The History section header is still Section and "Carregar mais" a plain button after the tree | ||||
| git gutter | editor/gitGutter.ts (+ lib/gitGutter.ts) | StateField with the index baseline (setDocumentBaseline), debounced line marks, cm-git-gutter; clicking a mark opens the hunk menu (stage/revert, bindHunkHost from gitHunkActions.ts) | ||||
| merge conflicts, blame, diff hunks | editor/mergeConflicts.ts, editor/gitBlame.ts, editor/diffHunks.ts (styles in styles/git-editor.css) | Conflict line colors + action bar widget (bindMergeHost); blame field (mapped marks, inline annotation + hover, file gutter, blameAtLine for the status bar; bindBlameHost); per-chunk buttons in the diff's right side and the registry of open diff views (registerDiffViews) | ||||
| Debugger UI | components/debug/ (DebugView, DebugSections, ValueTree, DebugToolbar, DebugConsole) | "Run and Debug" sidebar view (view === 'debug', unmounted when hidden; section open state in debugExpanded under section:<id>), Variables/Watch/Call Stack/Breakpoints sections, floating toolbar in .main (only while debugSessions is non-empty), Debug Console panel view (PanelView 'debugConsole'). ValueRow/ChildValues render any name/value node whose children load lazily (loadVariables(ref) from the effect of an open node) and whose expansion is keyed by a NAME path (scope:Locals/obj/x), so it survives steps. onTreeKeyDown (debug/treeKeys.ts) moves focus between [data-debug-row] rows | ||||
| breakpoint gutter | editor/breakpointGutter.ts (+ editor/debugHost.ts, lib/debugBreakpoints.ts) | cm-breakpoint-gutter left of the line numbers: click toggles, right-click opens the inline condition widget (expression / hit count / log message), stopped-line decoration + codicon-debug-stackframe arrow. Marks enter by setBreakpointMarks/setStoppedLine (fed by debugBreakpointActions.ts through dispatchEffects) and new states read them from debugHost() in create | ||||
Toggle | components/common/Toggle.tsx | On/off button (.toggle, .on, aria-pressed): active, label, glyph/icon, optional children (a text label beside the icon), disabled. For an action (not a state) use IconButton | ||||
QuickInput | components/quickInput/QuickInput.tsx | Top-center overlay for Quick Open (files), command palette (>) and go-to-line (:): combobox/listbox, ↑/↓/Enter/Esc, focus trap/restore, status region for empty/hint messages. Helpers: buildItems.tsx (item builders per mode), highlight.tsx, useProjectFiles.ts (file list from projectFiles.ts, shared with Claude mentions); pure logic in lib/fuzzyPath.ts, lib/quickOpenQuery.ts, lib/quickOpenFiles.ts, lib/recentFiles.ts. Commands come from commands/registry.ts | ||||
EditorArea, EditorGroup | editor/EditorArea.tsx, editor/EditorGroup.tsx | EditorArea renders 1 to 8 EditorGroups as a flat list positioned by the split tree (editorLayout), with a Resizer on every boundary; each group = its own tab row (TabBar/TabItem, local) + editor-actions (Markdown preview, split (Alt = down); the Claude toggle only in the top-right group) + CodeEditor / DiffEditor / MarkdownPreview / Welcome. Clicking or focusing a group makes it active (.editor-group.active; the inactive groups' active tab loses the brand line). Tabs drag between groups (TAB_DRAG_TYPE + GROUP_DRAG_TYPE: tab bar, editor body center = move, body edge = split that side) | ||||
TabBar, TabItem | editor/EditorGroup.tsx (local) | Tab bar with HTML5 drag-to-reorder (drop indicator, moveTab with the group), tab item (memo: the group's tabs are selected with useShallow and the callbacks are stable, so editing one tab re-renders only it; preview/deleted/dirty states, Markdown preview tab, native context menu via showTabMenu, double-click pins) | ||||
MarkdownPreview | editor/MarkdownPreview.tsx (+ lib/markdownPreview.ts) | Preview tab: renders the source tab's buffer (currentState, subscribeDocChanges, 150 ms debounce; disk read when the source isn't loaded) with marked + a private DOMPurify instance (no script/iframe/form/style/on*; href/src become data-raw-*), code fences highlighted by lib/codeHighlight.ts. Links: resolveLink (#x scrolls inside, relative opens the file in the editor group, http(s)/mailto → window.open, the rest ignored); images: resolveImage + readCachedImage/knownImage from fileViewActions.ts (cache per path); disk reads and fileRevs[path] (watcher revision, 150 ms debounce when the source isn't loaded) instead of window.api/workspace.onChange, remote → .md-image-missing placeholder. Styles .markdown-preview, .markdown-body | ||||
BottomPanel | components/BottomPanel.tsx | Bottom panel: tabs Problems (count badge) | Output | Terminal (+ terminal instance tabs .panel-tab.sub on the Terminal view), actions per view, close. The views stay mounted in .panel-pane (one per entry of the PANES registry in BottomPanel.tsx; a new view = a PanelView, a PANES entry and a tab) and are hidden with hidden (xterm and the log must not remount) | ||||
ProblemsPanel | components/ProblemsPanel.tsx | LSP diagnostics grouped by file (collapsible .problem-group, .problem-item rows with severity icon, message, source(code), [Ln, Col]); keyboard via common/useGroupedTree.ts (shared with ReferencesPanel and SearchView; spread listProps on the list, which delegates focus/keys/click by data-row, and treeRowProps(row.key, row.key === tabStop) on each memo row so moving the cursor re-renders only 2 rows; group header PanelGroupRow; collapsible groups, roving tabindex, ↑/↓/Home/End/←/→, Enter/Space; when the focused row, or a control in it, disappears and focus falls to the body, the next surviving row (or the previous one) takes it; also used by the Git view); click → openProblem; hints are not listed. Pure logic in lib/problems.ts | ||||
OutputPanel | components/OutputPanel.tsx | Read-only language-server log (.output-line.<level>, memoized lines keyed by a sequential id, appends batched per frame, 2000 cap, local "Limpar", sticks to the bottom). Pure logic in lib/lspLog.ts | ||||
| editor LSP | editor/lspSync.ts, editor/diagnostics.ts | lspSync: didOpen/didChange/didSave/didClose per tab with versions (fed by documentStore.ts, paths injected by bindLspPaths); diagnostics: LSP → @codemirror/lint setDiagnostics per EditorState (no linter()); position math in src/shared/lspPosition.ts (shared with main tests). the editor uses baseSetup (editor/setup.ts, basicSetup minus lintKeymap) so CodeMirror's lint panel never competes with view.problems | ||||
| LSP editor features | editor/lspExtensions.ts (+ lspCompletion, lspHover, lspSignatureHelp, lspCodeActions, lspRename, lspLinks) | Per-tab CodeMirror extensions, placed BEFORE baseSetup in extensionsFor. They never import the store: lspHost.ts (bindLspHost, bound by lspActions.ts) provides server state, go-to-definition, code action apply and notifications; paths come from lspSync.lspDocPath. Completion is added through EditorState.languageData (never override), icons through completionIcon in baseSetup. Floating widgets (signature, quick fix menu, rename input) are tooltips whose dom is the .cm-tooltip; hidden tabs close them in hideDocument. Markdown docs go through lib/lspMarkdown.ts (marked + DOMPurify, links open only http(s) via window.open); code fences are highlighted after sanitizing by lib/codeHighlight.ts (Lezer + the active theme's HighlightStyle, parser loaded on demand). The lightbulb gutter lives in lightbulbSlot (Compartment), enabled only for files with a ready server (refreshLspGutters) | ||||
ReferencesPanel | components/ReferencesPanel.tsx | References view (reuses the Problems classes): groups by file, preview line with <mark>, click → jumpToItem (records navigation) | ||||
Welcome | components/Welcome.tsx | No root: .welcome-actions (.welcome-action[data-action]: Open Folder, Clone Repository, Terminal, New Window), "Start with Claude" (.welcome-claude-input, Enter sends, Shift+Enter newline, folder = first selected recent or main's dialog) and .welcome-recent (ul.recent-list > li.recent-item[data-selected] > input.recent-check / button.recent-open / button.recent-remove; ⌘/Ctrl-click or Space selects, plain click opens, button.recent-open-many = "Open N Workspaces"/"Open All"; branch, "opened X ago", .recent-live StatusDot when open in another workspace; pure logic in lib/welcomeRecents.ts); with a root and no tab: shortcut list | ||||
WorkspaceBar | components/WorkspaceBar.tsx | Top bar and title bar (36px, -webkit-app-region: drag, left inset --titlebar-inset = 78px on macOS via html[data-platform]): .workspace-tab (216px, dot, name, branch, unread counter with ping, effective workspace.selectN hint, close), +, HTML5 drag-to-reorder, roving tabindex with arrows. Toasts of other workspaces carry origin (.toast-origin card) | ||||
Modal | components/common/Modal.tsx | Dialog in a portal on body (role="dialog" aria-modal, aria-labelledby): #root goes inert while open, Tab wraps, Esc/backdrop close unless dismissible={false}, tone="danger" focuses [data-modal-safe], initial focus [data-autofocus], focus returns to the opener; closing blurs first so a pending DraftField commits. Only for Settings and destructive confirmations, one at a time. Toasts render in a portal above it (.toasts.over-modal). Classes .modal-backdrop, .modal (+ .danger), .modal-header, .modal-title, .modal-body, .modal-footer | ||||
Select, Switch | components/common/ | Select: combobox button + listbox portal on body (focus stays on the button, aria-activedescendant, arrows/Home/End/Enter/Esc, first-letter type-ahead via lib/typeAhead.ts) — replaces native <select>; classes .select, .select-list, .select-option. Switch: role="switch" (.switch, .on) | ||||
SettingRow | components/common/SettingRow.tsx | Settings row: label + description + note left, control + fixed .setting-action slot right; data-setting-key. Classes .setting-row (+ .disabled), .setting-label, .setting-description, .setting-note, .setting-control, .setting-badge, .setting-link, .setting-summary, .setting-error | ||||
SettingsModal, SettingItem, DraftField, ShortcutsList | components/settings/ | Settings modal (overlay.kind === 'settings'): section tablist + scope .segmented (.segmented-item); SettingItem picks the control from shared/settingsUi.ts; DraftField keeps a draft keyed by the stored value, commits on Enter/blur (normalized, invalid → inline error); ShortcutsList reads listAllCommands() (no when) + effective keybindings (.shortcuts*, .shortcut-*) | ||||
MenuList, MenuEntry | components/common/MenuList.tsx | The single item list of every popup menu (DropdownMenu, StatusMenu, Claude ChipMenu bodies): one MenuEntry type (id → data-action, or data-mode/data-model via idAttr; label, hint, description, icon, danger, checked + checkKind → menuitemradio/menuitemcheckbox with boolean aria-checked, disabled, separator), variant menu (compact row) or option (title + description, trailing check). Items carry data-nav, which useMenuPopup navigates. Never render a menu item by hand | ||||
DropdownMenu | components/common/DropdownMenu.tsx | Button + accessible menu popup rendered through MenuList (items: MenuEntry[]); arrows/Home/End/Esc/Tab, focus returns to the button before the action runs. Used by the Claude panel ⋯ menu (.dropdown, .dropdown-menu, .dropdown-item) | ||||
| Claude sessions | claude/ClaudePanel.tsx, Timeline.tsx, AgentsStrip.tsx | Tab rename input (data-rename-input), star (.claude-star, aria-pressed), timeline/PinnedPrompt (sticky last user message + IntersectionObserver sentinel, .claude-pinned-prompt), timeline/TurnSummary ([data-turn-summary], rows data-path/added/removed), AgentsStrip ([data-agents], rows data-agent-status; at most 2 neon-live rows). Pure logic in lib/claudeTurnSummary.ts (line diff), lib/claudeAgents.ts, lib/claudeSessions.ts | ||||
useFocusRegion, useFocusTarget, useRequest | common/useFocusRegion.ts, common/useFocusTarget.ts, common/useRequest.ts | How a component takes part in focus and one-shot requests without actions touching the DOM: ref={useFocusRegion('tree')} (focus option for a custom focus, intoRef to keep an existing ref) registers its element as a region of focusBridge.ts; useFocusTarget('terminal') makes it the target of requestFocus and returns a counter for the focusing effect; useRequest(kind, handle, match?) serves a pending one-shot request from the requests slice. Which one to use: renderer-store, "One-shot requests". Regions in use: editorSlot (App), tree (Explorer), panelContainer (layout/PanelSlot.tsx), panelTab (active PanelTab), claudePanel/claudeNewConversation (ClaudePanel), claudeCard:<requestId> (permission, question and elicitation cards), diffEditor (DiffEditor, focus = last .cm-content), containersView/containersSearch/containerDetail | ||||
useAsync | common/useAsync.ts | useAsync(fn, deps, { enabled, delayMs, keepPrevious, onError }) → { status: 'idle' | 'loading' | 'success' | 'error', value, error, reload }: the only way to write a plain async load in a component or hook, never a hand-rolled let cancelled/alive effect. The result is keyed by the deps it answers, so a late answer of old deps never renders and loading is derived (no setState in the effect). enabled: false skips the load without dropping a result already obtained for the same deps; delayMs debounces typing; keepPrevious keeps the last non-null value while loading (menus that reload on each open, image reload); onError runs once for a still-current failure (toasts); fn is read through useEffectEvent, so deps list only what identifies the load; deps must be stable values (primitives or references from props/store), never objects, arrays or closures created during render (they are compared with Object.is during render and would loop). Not for loads entangled with a subscription, an imperative CodeMirror/xterm lifecycle or a debounced stream (OutputPanel, useShellCwd, MarkdownPreview, CodeEditor, DiffEditor) | ||||
useShortcutLabel, useConversationField | common/useShortcutLabel.ts, claude/useConversation.ts | Hooks for the effective shortcut text of a command and for one field of a conversation (details in renderer-store, "Shared selectors") | ||||
useNow, useRestoreFocus, useRovingFocus | common/useNow.ts, common/useRestoreFocus.ts, common/useRovingFocus.ts | useNow(intervalMs, enabled = true, until?): clock in ms that ticks while enabled and, with until (a timestamp), stops after the tick that reaches it (status bar, retry countdown, agents strip). useRestoreFocus(): remembers the focused element on mount, returns restore() (focus it now) and on unmount refocuses it only if focus was lost (Modal, QuickInput); call it after any effect whose cleanup removes inert. useRovingFocus({ orientation, selector, activate }): the onKeyDown of a group (arrows per orientation horizontal / vertical / both, Home/End, wraps, skips disabled items; activate clicks the item it moves to) for tablists, radiogroups and button lists (WorkspaceBar, BottomPanel, Segmented, Welcome recents, QuestionCard tabs) | ||||
useCardAutoFocus, useConversationOverlay | claude/useCardAutoFocus.ts, claude/useConversationOverlay.ts | useCardAutoFocus(ref, conversationId, trigger?, initial?): focus of a permission-style card (question, elicitation): on mount only when the session is visible and focus is free (body or the Claude panel) and nobody is typing; when trigger changes, back to the card only if the returned noteIntent() saw focus inside it and it fell on the body. The claudeCard:<requestId> region stays with useFocusRegion. useConversationOverlay('mcp' | 'usage', close): conversation id of a per-conversation modal, null when closed; closes the modal if the conversation ended | ||||
Breadcrumbs | editor/Breadcrumbs.tsx | Bar above each group's editor (nav.breadcrumbs, breadcrumbs.enabled): .breadcrumb-item buttons (roving: only the last is a Tab stop, ←/→ move, Enter/Space/↓ open, Esc → editor) with useMenuPopup + MenuList popups (.dropdown-menu.breadcrumb-menu): folder listing (FileIcon, current file checked, folders navigate inside the menu) or sibling symbols (SymbolIcon). Served by the breadcrumbsFocus request (editor.focusBreadcrumbs) | ||||
OutlineView | sidebar/OutlineView.tsx | "Estrutura" section at the foot of the Explorer slot (layout/SidebarSlot.tsx; Section + Resizer above it): symbol tree of the active file tab (useDocumentSymbols, only while the Explorer is shown and the section open), virtualized role="tree" rows (.tree-row.outline-row, SymbolIcon, detail), follow-cursor .active row (activeRowKey), click/Enter reveals (revealDocumentSymbol), typing opens .outline-filter, Collapse All and sort by position/name/category (DropdownMenu). Pure logic in lib/symbolTree.ts | ||||
SymbolIcon | common/SymbolIcon.tsx | Codicon of an LSP SymbolKind colored by group (.symbol-icon.kind-function/variable/type, lib/symbolKinds.ts) | ||||
ConflictBar | editor/CodeEditor.tsx (local) | Bar above the editor when the file changed on disk while the tab is dirty: "Reload from Disk" / "Keep My Version" |
Ranking and labels for quick-pick lists: rankByLabel(entries, query, labelOf, { sort, tiebreak? }) (lib/rankByLabel.ts) is the only fuzzy filter/sort over a label (empty query keeps everything in order); pathLabel(name, dir, matches, offset, highlightDir?) (components/quickInput/highlight.tsx) renders the "name + folder" label of file, folder, symbol and workspace rows. Never re-implement either in a build*Items function.
Badge counters that cap (99+) use capCount(n, max = 99) from lib/capCount.ts (built on formatNumber); never hand-write n > 99 ? '99+' : n.
Strict typing (exactOptionalPropertyTypes, noUncheckedIndexedAccess; tests keep the first, not the second):
prop?: Tdoes not accept an explicitundefined. Add| undefinedonly on a prop/field that really receives a possibly-undefined value forwarded from another optional (MenuEntry.hint,QuickItem.tag,IconButton.shortcut,Command.when); otherwise omit the key. For an API that must not receive the key (CodeMirror configs,window.apioptions, shared contracts) spreadomitUndefined({ … })from@shared/omitUndefinedor...(cond && { key }); neverkey: maybeUndefined.array[i]/record[key]/regex.exec(…)[n]/ typed arrays areT | undefined: guard (const x = arr[i]; if (!x) return),.at(-1)+ check, a destructuring default (const [a = '', b = ''] = s.split('\n')), orstring.charAt(i)for a character loop;!only when the same expression makes it impossible. A non-empty fixed list is typed as a tuple (readonly [ModeInfo, ...ModeInfo[]]).JSON.stringifyis typedstringbut returnsundefinedforundefined/functions: usejsonText(value, space?)(lib/jsonText.ts).- A flag assigned inside a callback (
set((s) => { changed = true })) is seen by TS as constant: return the value from the callback or compare results instead of reading the flag afterwards.
Rule: a local component needed in a second place moves to components/common/<name>.tsx and gets exported —
never copy-paste it.
Quick Index — CSS classes (styles/*.css)
| Class | Use |
.sidebar-view / .sidebar-title (+ .actions) | Root and 35px header of a sidebar view |
.tree-row (+ .selected, .active, .editing) / .tree-label | Any 22px list row (explorer, search, git) |
.row-actions | Actions revealed on row hover or when the row has focus (:focus-within), so Tab reaches them |
.button + .button-primary (--grad-brand) / .button-secondary / .button-ghost / .button-danger (+ .button-sm) / .icon-button / .toggle (+ .on) | Buttons (styles/controls.css, + .button-xs), always rendered through Button/IconButton/Toggle |
.neon / .neon-live | Static gradient ring / neon comet tracing the border while the AI works (needs --neon-fill) |
.status-dot (+ status) / .tree-chevron (+ .open) / .wicon / .ficon | Status dot, rotating chevron, icon sizing |
.count-badge / .git-badge / .git-letter-* / .git-<status> | Badges and git decorations |
.empty-state / .empty-line / .muted / .ellipsis | Empty states and secondary text |
.palette-backdrop / .palette / .palette-input / .palette-list / .palette-item (+ .selected, .disabled) / .palette-label / .palette-category / .palette-recent / .palette-kbd / .palette-icon / .palette-file-dir / .palette-hint / .palette-empty | Quick-input overlay (Quick Open, command palette, go-to-line, @ file symbols, # workspace symbols) |
.tab (+ .active, .dirty, .preview italic label, .deleted struck-through label, .drop-before / .drop-after drag indicator) | Editor tabs |
.editor-area (+ .split), .editor-group-slot, .editor-group (+ .active), .editor-content.drop-target | Editor groups |
.conflict-bar / .conflict-text / .conflict-button (+ .primary) | External-change conflict bar |
.panel, .panel-header, .panel-tab (+ .active, .sub), .panel-tab-separator, .panel-body, .panel-pane (+ [hidden]), .terminal-views | Bottom panel and its views |
.problems, .problem-group, .problem-item, .problem-icon.<severity>, .problem-dir, .problem-source, .problem-position | Problems view (reuses .tree-row, .tree-label, .count-badge, .empty-line) |
.output-panel, .output-line (+ level), .output-time, .output-server, .output-text, .output-toolbar, .output-empty | Output view |
.breadcrumbs, .breadcrumb, .breadcrumb-item (+ .open), .breadcrumb-separator, .breadcrumb-menu, .breadcrumb-empty | Editor breadcrumbs (styles/editor.css) |
.cm-sticky-scroll, .cm-sticky-line, .cm-sticky-text, .cm-sticky-gutter | Sticky scroll strip drawn by editor/stickyScroll.ts |
.outline (+ .open), .outline-body, .outline-tree, .outline-row, .outline-filter, .outline-detail, .symbol-icon | Outline section and symbol icons (styles/workbench.css) |
.status-item.lsp-error | Language server in error on the status bar |
Patterns
- No IPC in components. Components call actions (
explorerFsActions,gitActions,lspActions,terminalActions,fileViewActions,projectFiles…), neverwindow.api.*orworkspace.onChange;window.api.platform/homeDirare constants and fine. Editor infrastructure undercomponents/editor(CodeMirror extensions,documentStore.ts/groupViews.ts,lspSync.ts, the load inCodeEditor/DiffEditor) still talks towindow.apidirectly. - Sidebar views: only the Explorer stays mounted (virtualized; keeps scroll and selection) and is hidden with the
hiddenattribute. Search and Git unmount when not visible (layout/SidebarSlot.tsx) and keep their state (results, commit message) in the store, so a hidden view never re-renders on watcher events. A new view keeps its state in the store, not in the component. Never hide with inlinestyle.display: sethiddenand give the class an explicit.x[hidden] { display: none }(a class that setsdisplaybeats the user-agent[hidden]);.sidebar,.claude-sidebar,.panel-containerand.code-editoralready do. - Overlays (quick input: Quick Open / command palette / go-to-line) are one component at the top of
Appdriven by store state. - Dialogs:
window.prompt()does not exist in Electron → inline input or overlay. Unsaved-changes questions use the native dialog (confirmAndSave→api.dialog.confirmUnsaved); every other question (discard, publish, trash) goes throughconfirmDialog(saveActions.ts→api.dialog.confirm); neverwindow.confirm. - Context menus are native, built in main (see
fs:contextMenu). - Shortcuts: a new shortcut = an entry in
defaultKeys(+defaultWhenif scoped) read bydefaultKeybindings(src/shared/commands.ts, plus the id inCommandId)- a menu item in
src/main/menu.tsif it belongs to a menu +registerCommandin the renderer registry. Never add a loose globalkeydownlistener.
- a menu item in
UI copy
- All UI text comes from the i18n catalogs (
i18nskill):const t = useT()andt.<namespace>.<key>, with pt-BR, English and Spanish written together. Short and direct ("Open Folder…", "No changes."). Established technical terms stay in English (commit, stage, branch, push). - Shortcut labels follow the OS:
window.api.platform === 'darwin'→⌘, otherwiseCtrl+.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

