agentleFS
filipe-camposfilipe-campos/walph/.claude/skills/renderer-components/SKILL.md

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.

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
  1. Very specific:

    Sidebar views: only the Explorer stays mounted (virtualized.

    line 120
  2. Very specific:

    Never hide with inline style.display: set hidden and give the class an explicit .x[hidden] { display: none } (a class that sets display …

    line 124
  3. Very specific:

    All UI text comes from the i18n catalogs (i18n skill): const t = useT() and t.<namespace>.<key>, with pt-BR, English and Spanish written …

    line 136

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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, testable

Quick Index — components

ComponentFilePurpose
Resizercomponents/Resizer.tsxDrag handle (axis="x" | "y", onDrag(delta)) for panels
SidebarSlot, PanelSlot, ClaudeSlot, OverlayHostcomponents/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
Toastscomponents/Toasts.tsxToast 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
InlineInputsidebar/Explorer.tsx (local)Inline text input for create/rename (Enter submits, Esc/blur cancels)
useExplorerDnd, usePendingRowFocussidebar/useExplorerDnd.ts, sidebar/usePendingRowFocus.tsExplorer 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, StatusDotcomponents/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 (`idlerunningdoneattentionerror`, pulse/ping in CSS)
IconButtoncomponents/common/IconButton.tsxThe 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">
Buttoncomponents/common/Button.tsxThe 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)
CopyButtoncomponents/common/CopyButton.tsxIconButton that copies text (copyText from clipboard.ts) and shows a check for 1.5 s (label is the idle name; className for placement)
Sectioncomponents/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
SectionRowcomponents/common/SectionRow.tsxThe 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, Chevroncomponents/common/FileTreeRow.tsx, components/common/Chevron.tsxFileTreeRow: 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
FileRowsidebar/GitView.tsx (local)Git file row (memo treeitem; open file / discard / stage, or unstage)
CommitBox, GitHistory, GitStashessidebar/Commit message + Amend; paged history with expandable commit files (opens read-only commit diffs); stash list with apply/pop/drop
Git view keyboardsidebar/GitView.tsx, GitStashes.tsx, GitHistory.tsxThree 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 guttereditor/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 hunkseditor/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 UIcomponents/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 guttereditor/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
Togglecomponents/common/Toggle.tsxOn/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
QuickInputcomponents/quickInput/QuickInput.tsxTop-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, EditorGroupeditor/EditorArea.tsx, editor/EditorGroup.tsxEditorArea 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, TabItemeditor/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)
MarkdownPrevieweditor/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
BottomPanelcomponents/BottomPanel.tsxBottom 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)
ProblemsPanelcomponents/ProblemsPanel.tsxLSP 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
OutputPanelcomponents/OutputPanel.tsxRead-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 LSPeditor/lspSync.ts, editor/diagnostics.tslspSync: 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 featureseditor/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)
ReferencesPanelcomponents/ReferencesPanel.tsxReferences view (reuses the Problems classes): groups by file, preview line with <mark>, click → jumpToItem (records navigation)
Welcomecomponents/Welcome.tsxNo 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
WorkspaceBarcomponents/WorkspaceBar.tsxTop 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)
Modalcomponents/common/Modal.tsxDialog 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, Switchcomponents/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)
SettingRowcomponents/common/SettingRow.tsxSettings 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, ShortcutsListcomponents/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, MenuEntrycomponents/common/MenuList.tsxThe 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
DropdownMenucomponents/common/DropdownMenu.tsxButton + 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 sessionsclaude/ClaudePanel.tsx, Timeline.tsx, AgentsStrip.tsxTab 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, useRequestcommon/useFocusRegion.ts, common/useFocusTarget.ts, common/useRequest.tsHow 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
useAsynccommon/useAsync.tsuseAsync(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, useConversationFieldcommon/useShortcutLabel.ts, claude/useConversation.tsHooks for the effective shortcut text of a command and for one field of a conversation (details in renderer-store, "Shared selectors")
useNow, useRestoreFocus, useRovingFocuscommon/useNow.ts, common/useRestoreFocus.ts, common/useRovingFocus.tsuseNow(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, useConversationOverlayclaude/useCardAutoFocus.ts, claude/useConversationOverlay.tsuseCardAutoFocus(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
Breadcrumbseditor/Breadcrumbs.tsxBar 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)
OutlineViewsidebar/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
SymbolIconcommon/SymbolIcon.tsxCodicon of an LSP SymbolKind colored by group (.symbol-icon.kind-function/variable/type, lib/symbolKinds.ts)
ConflictBareditor/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?: T does not accept an explicit undefined. Add | undefined only 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.api options, shared contracts) spread omitUndefined({ … }) from @shared/omitUndefined or ...(cond && { key }); never key: maybeUndefined.
  • array[i] / record[key] / regex.exec(…)[n] / typed arrays are T | undefined: guard (const x = arr[i]; if (!x) return), .at(-1) + check, a destructuring default (const [a = '', b = ''] = s.split('\n')), or string.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.stringify is typed string but returns undefined for undefined/functions: use jsonText(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)

ClassUse
.sidebar-view / .sidebar-title (+ .actions)Root and 35px header of a sidebar view
.tree-row (+ .selected, .active, .editing) / .tree-labelAny 22px list row (explorer, search, git)
.row-actionsActions 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-liveStatic gradient ring / neon comet tracing the border while the AI works (needs --neon-fill)
.status-dot (+ status) / .tree-chevron (+ .open) / .wicon / .ficonStatus dot, rotating chevron, icon sizing
.count-badge / .git-badge / .git-letter-* / .git-<status>Badges and git decorations
.empty-state / .empty-line / .muted / .ellipsisEmpty 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-emptyQuick-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-targetEditor 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-viewsBottom panel and its views
.problems, .problem-group, .problem-item, .problem-icon.<severity>, .problem-dir, .problem-source, .problem-positionProblems view (reuses .tree-row, .tree-label, .count-badge, .empty-line)
.output-panel, .output-line (+ level), .output-time, .output-server, .output-text, .output-toolbar, .output-emptyOutput view
.breadcrumbs, .breadcrumb, .breadcrumb-item (+ .open), .breadcrumb-separator, .breadcrumb-menu, .breadcrumb-emptyEditor breadcrumbs (styles/editor.css)
.cm-sticky-scroll, .cm-sticky-line, .cm-sticky-text, .cm-sticky-gutterSticky scroll strip drawn by editor/stickyScroll.ts
.outline (+ .open), .outline-body, .outline-tree, .outline-row, .outline-filter, .outline-detail, .symbol-iconOutline section and symbol icons (styles/workbench.css)
.status-item.lsp-errorLanguage server in error on the status bar

Patterns

  • No IPC in components. Components call actions (explorerFsActions, gitActions, lspActions, terminalActions, fileViewActions, projectFiles…), never window.api.* or workspace.onChange; window.api.platform/homeDir are constants and fine. Editor infrastructure under components/editor (CodeMirror extensions, documentStore.ts/groupViews.ts, lspSync.ts, the load in CodeEditor/DiffEditor) still talks to window.api directly.
  • Sidebar views: only the Explorer stays mounted (virtualized; keeps scroll and selection) and is hidden with the hidden attribute. 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 inline style.display: set hidden and give the class an explicit .x[hidden] { display: none } (a class that sets display beats the user-agent [hidden]); .sidebar, .claude-sidebar, .panel-container and .code-editor already do.
  • Overlays (quick input: Quick Open / command palette / go-to-line) are one component at the top of App driven 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 through confirmDialog (saveActions.ts → api.dialog.confirm); never window.confirm.
  • Context menus are native, built in main (see fs:contextMenu).
  • Shortcuts: a new shortcut = an entry in defaultKeys (+ defaultWhen if scoped) read by defaultKeybindings (src/shared/commands.ts, plus the id in CommandId)
    • a menu item in src/main/menu.ts if it belongs to a menu + registerCommand in the renderer registry. Never add a loose global keydown listener.

UI copy

  • All UI text comes from the i18n catalogs (i18n skill): const t = useT() and t.<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' → ⌘, otherwise Ctrl+.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.