AGENTS.md@packages/client/src/components/editor-pane · git:20261024.6f2ef1c · 2026-10-24 · sha256 7be28a86e08c4670
AGENTS.md@packages/client/src/components/editor-pane git:20261024.6f2ef1cA
Immutable. This exact content never changes and is served at /api/v1/blob/7be28a86e08c4670.
# DOX — packages/client/src/components/editor-pane
Files in this directory. One row per source file.
| File | Purpose |
|------|---------|
| `BinaryWarn.tsx` | Binary-file tab. Shows a "binary file" notice (path only). No content fetched. Native-editor buttons / `fetchEditors` removed. See change: add-internal-monaco-editor-pane, remove-external-editor-integration. D11 A4: reads `useFileModeOffers()`; matching enabled file-handler claims → "It can be opened with a plugin:" + one button per claim (`binary-mode-<pluginId>/<claimId>`, claim label, same `selectMode` as the switcher); key `editor.binaryOpenWith`. See change: extend-craft-web-to-all-apps. |
| `BulkCloseConfirm.tsx` | Bulk-close summary `Dialog` (`bulk-close-confirm`): lists terminals + unsaved handler tabs with reasons; buttons Cancel (`bulk-close-cancel`, focused via ref after the focus trap), keep (`bulk-close-keep`, "all but these N"), discard (`bulk-close-discard`). See change: scope-anchor-editor-pane-controls. |
| `CappedViewer.tsx` | Large-file cap gate wrapping the rich viewers: resolves the component from `viewerRegistry` (half a) and, for every non-`monaco` kind, reads `size` from `/api/file` metadata, mounting `TooLargePreview` above `MAX_PREVIEW_BYTES`. `monaco` bypasses (own large-file handling). `viewer` narrowed to `OpenPathViewer`; pseudo-tabs bypass this gate (their probe always missed → removal is a no-op). No casts at call sites. See change: cleanup-import-cycles. See change: surface-denial-remedy-in-previews — `/api/file` size probe ALWAYS `fetchWithoutGrantPrompt` (an Allow once spent on it would starve the viewer). Gate = `useEditorLimits().bytes` (not fixed 10 MiB); waits for config hydration, default after 3 s; re-gates live. See change: gate-changed-on-disk-by-content. The `/api/file` metadata probe now runs for EVERY kind (monaco renders immediately meanwhile): a 404 renders `editor-file-missing` (`File no longer exists: <path>`) instead of the viewer; refusals/network errors still mount it. See change: composer-dock-collapse-and-file-open. A missing `openspec/changes/<name>/…` file reads the folder archive (`useArchiveEntries`, only then) and, on a match, extends `editor-file-missing` with `editor.changeArchived` + `editor-open-archived` (`openInSplit` archived path then `closeByPath` stale). The `File no longer exists: <path>` line stays. See change: fix-archived-session-view-header-and-stale-artifact-tab (D2, D3). |
| `ChangedOnDiskBanner.tsx` | Per-tab changed-on-disk banner. Refresh re-fetches; Dismiss keeps stale view. No auto-reload. See change: split-editor-workspace. |
| `ChangesRailSection.tsx` | Slim summary bar atop the rail: `Changes (N) · +X −Y`, `summed` badge (non-git), `this session only` toggle. Props `{sessionOnly, onSessionOnlyChange}` (rail-local, owned by `EditorPane`, D3 — not context; lifting breaks `FileDiffView` takeover). No per-file list / `DiffFileTree`. Reads `SessionDiffContext`; null when no `files`+`otherChanges`. See change: add-change-summary-table, detect-tool-created-files, collapse-diff-file-tree. |
| `DiffViewer.tsx` | `diff` viewer tab. Strips `diff:` prefix from virtual path, reads file `gitDiff` from `SessionDiffContext`, delegates render to `DiffPanel`. See change: add-change-summary-table. |
| `DocumentBar.tsx` | Document bar (`doc-bar`): file → breadcrumb (`doc-breadcrumb`, folder click = expand-only reveal, outside cwd = inert absolute path), inline mode switcher, Reload (`tab-reload`), `TabActions`, `⋯` (`doc-more-menu`: Show in file tree, Copy full / relative / name); url → Open in system browser; diff/live/terminal/plugin → title line (`doc-title`); none → hidden. `narrow` moves Open/Reveal into `⋯`. See change: scope-anchor-editor-pane-controls. |
| `EditablePreviewTab.tsx` | Generic Preview/Edit tab for `editable` text kinds (`.csv`, `.adoc`). `EditablePreviewTab({cwd,path,preview,testIdPrefix})`. Preview = caller node; Edit = lazy `MarkdownEditor` over `/api/file` `content` (loaded on first Edit). Save → `POST /api/file/write` w/ loaded mtime; 409 → `ChangedOnDiskBanner`. Body `min-h-0 flex-1 overflow-auto` = scroll container. testids `<prefix>-preview-toggle`/`-edit-toggle`/`-save-btn`/`-dirty-dot`. See change: surface-denial-remedy-in-previews — `/api/file` load via `usePreviewFetch()` (opted out unless operator provenance); `/api/file/write` unchanged. Save posts via `useTrackedFileWrite` (own save rebaselines tab). See change: gate-changed-on-disk-by-content. |
| `EditableSpreadsheetTab.tsx` | `.csv` tab: thin `EditablePreviewTab` wrapper, preview=`SpreadsheetPreview`, prefix `csv`. See change: open-view-command-in-editor-pane (D4). |
| `EditorFileTree.tsx` | Lazy file-tree rail rooted at session cwd. Expands one level per click. Lists `{name,isDir}` from single `GET /api/file/tree` (hidden included) — replaces the `/api/file`+`/api/browse` merge that mislabelled `.git`/`.pi` as files (#1). File rows show per-kind icon via `fileIcon()`. Clicking file calls `onOpenFile(rel, viewer)` via shared `fileKind`. `treeOpenRoots` persisted. Each row (file+dir) gains hover-revealed `RowCopyAffordance` copy glyph → anchored popup: Copy full path (`absOf`), relative (`rel`), file name (basename); `copyText` (never rejects), ✓ ~1.5s; popup portaled via `LayerPortal`, `fixed` + `z-popover`, placed by `usePopoverFlip` (viewport-bound, flips vertical + horizontal, `maxWidth`) — escapes rail `overflow-auto` clip (#728); dismiss on outside-click/rail-scroll/Escape (Escape refocuses glyph); glyph `stopPropagation` — no open/toggle. Consumes `useOptionalSessionDiff()` (`buildDiffIndex`): changed `FileRow` shows per-kind icon + status `●`/`+` (`statusIndicator`; edit/tool/mixed→●), `CountBadges`, hover `diff` chip (`onOpenDiff`→`diff:` tab), >1-event `✏️/📝` expander; changed-descendant dirs show `folder-dot` (path-prefix, no fetch); NO auto-expand. `OtherChangesGroup` (muted, `<button>` rows) renders `otherChanges` at rail bottom unless `sessionOnly`. Null diff → plain tree. See change: add-internal-monaco-editor-pane, improve-content-editor, copy-file-path, collapse-diff-file-tree. See change: surface-denial-remedy-in-previews — `listDir` → `fetchWithoutGrantPrompt` (tree never raises the grant dialog). Fix: GH #728. `RowCopyAffordance` copies via `copyText` instead of a guarded `navigator.clipboard` (non-secure LAN origin safe). See change: add-lan-knock-pairing. |
| `EditorPane.tsx` | Editor pane shell: pane bar (Files · tabs · `+` · `⌄` · `✕`), `DocumentBar`, rail (Files / Search views) + resizable divider, active viewer (`viewerRegistry`/`pseudoTabRegistry`), keep-alive `TerminalPaneLayer`, status footer; bulk-close runner + Search-tabs / context menus. State via `useEditorPaneState` + `SplitWorkspaceContext`. → see `EditorPane.tsx.AGENTS.md` See change: scope-anchor-editor-pane-controls. |
| `EditorSearchPanel.tsx` | Dual-mode search. Filenames=bridge walk, Contents=`GET /api/grep`. Regexp toggle, min-3-char + debounce, keyboard nav ↑↓/↵/Esc. Injected search fns. See change: split-editor-workspace. Now the rail Search view: mode controlled by the pane (`mode`/`onModeChange`), `prefill {query?, nonce}` focuses + replaces the query; Escape handled by the rail container, not the input; vertical rail layout. See change: scope-anchor-editor-pane-controls. |
| `EditorTabs.tsx` | Horizontal tab strip. Click activate, `×`/middle-click/Ctrl+Cmd-W close, drag reorder. Per-kind icon via `fileIcon()` + label = basename, tooltip = rel path. Tags `diff` tab kind. `terminal` tabs (`term:<id>`) render a console icon + `terminalTitle(id)` label (shell/title) via optional `terminalTitle` prop. Every tab carries `data-testid="editor-tab"` + `data-tab-path={file.path}` (stable active-tab assertion for browser tests). See change: add-internal-monaco-editor-pane. See change: improve-content-editor. See change: add-change-summary-table. See change: terminals-in-tabbed-panes. See change: fix-terminals-action-opens-terminal. `pluginLabel(path)` prop renders the claim label component for `viewer:"plugin"` tabs (every tab, active or not); default = prefix; puzzle icon. See change: add-browser-editor-pane-tab. Pane-bar tablist: `min-w-0 overflow-x-auto` flexible item; `×` 24 px, visible on keyboard focus; `onContextMenuTab(index, anchor)` from secondary click / ContextMenu / Shift+F10; `closeShortcutSuspended` makes Cmd/Ctrl-W inert; labels from `tab-labels.ts`. See change: scope-anchor-editor-pane-controls. |
| `MarkdownEditor.tsx` | Controlled editable Monaco buffer. Props `{value,onChange,readOnly}`. language=markdown, wordWrap on, minimap off. Lazy-mounts behind Suspense in InstructionsPage. Theme via shared `useThemeContext()` (recolors live on theme switch). See change: directory-settings-page-and-scoped-md-editing. See change: improve-content-editor. |
| `LiveServerViewer.tsx` | Live-server-preview tab (viewer kind `live-server`). Allowlist picker (`GET /api/live-server/list`) + confirmed URL entry → `startLiveServer` (client+server loopback SSRF check) → iframe proxied `/live/<id>/` on MAIN origin with `sandbox="allow-scripts allow-forms allow-popups allow-downloads"` (NO `allow-same-origin` — opaque origin D7; NO `allow-downloads-without-user-activation`; `allow-downloads` is not gesture-gated in Chrome). Remote hosts refused. Preset `ViewerProps.path` of form `live:<loopback-url>` auto-launches on mount (skips picker) and preserves the URL's `pathname+search` into the iframe `src` (deep link); `live:preview`/empty → picker. See change: improve-content-editor; open-loopback-links-in-split-viewer; live-viewer-allow-downloads. Frames the minted capability path `/live/<id>/~c/<token>/` (`mintLiveCapability`) — mint before framing, on every reopen, and 10 min before `expiresAt` (retry 30 s on failure); sandbox tokens unchanged; token never displayed; refused mint ⇒ `live-error`, never the bare path. See change: retire-trusted-networks-bypass. Orchestrator only: picker (`live/TargetPicker`) vs framed (`live/LiveFrame`). Presets `live:<url>`→`startLiveServer({addedBy:"preset"})`, `static:<abs file>`→`startStaticTarget`; picker launch = operator (open egress). Policy/mint/address/toggle/exposure/banner live in `live/`. See change: improve-canvas-html-and-url-ux. |
| `menu-keys.ts` | Shared `role="menu"` keyboard model: `enabledMenuItems`, `nextMenuIndex`, `handleMenuKeyDown` (↑/↓ wrap, Home/End, Escape/Tab close, disabled skipped). See change: scope-anchor-editor-pane-controls. |
| `MenuPopover.tsx` | `MenuPopover` (`Popover` + `role="menu"` + `menu-keys`) and `MenuItem`. First enabled item focused; Escape closes + refocuses the trigger. See change: scope-anchor-editor-pane-controls. |
| `MermaidViewer.tsx` | Mermaid tab. Fetches `.mmd`/`.mermaid` source via `/api/file/raw`, renders through shared `MermaidBlock` (theme-aware, zoom/pan). Loading/error states. See change: improve-content-editor. See change: surface-denial-remedy-in-previews — Source fetch via `usePreviewFetch()` (opted out unless operator provenance). Provides `MermaidSourceContext` (kind mmd, ordinal 0) + reload nonce for apply-fix. See change: add-preview-apply-fix-to-file. |
| `MarkdownViewer.tsx` | Markdown tab, Preview (default) / Edit. Fetches content+mtime via `/api/file`. Preview→`MarkdownContent` (frontmatter=properties, `pi-asset:` via `SessionAssetsContext`). Preview passes `imageBase={{cwd, dir: absOf(cwd, dirname(path))}}` so local images resolve. Edit (gated on `fileKind.editable` `.md`/`.mdx`)→controlled `MarkdownEditor`; Save→`POST /api/file/write` w/ loaded mtime, 200 clears dirty, 409→`ChangedOnDiskBanner`. See change: add-internal-monaco-editor-pane. See change: improve-content-editor. See change: fix-markdown-preview-relative-images. See change: surface-denial-remedy-in-previews — Load via `usePreviewFetch` + `denialFetch`; refused load → `DenialNotice` (Ask for access re-runs once eligibly); save errors keep `error`. `imageBase` memoised on cwd/path. See change: fix-markdown-remount-storm. Passes `mermaidSource` (kind md, `isDirty: dirty`, `reload`=`load`) to preview `MarkdownContent`. See change: add-preview-apply-fix-to-file. Passes `staticSource` to `MarkdownContent`. See change: add-md-adoc-markup-repair. Save posts via `useTrackedFileWrite` (own save rebaselines tab). See change: gate-changed-on-disk-by-content. |
| `NewTabMenu.tsx` | Pane-bar `+` (`new-tab-menu-toggle`): Terminal (`new-terminal-launch`), Live preview (`live-preview-launch`), Open file… (`open-file-launch`, only with file search). See change: scope-anchor-editor-pane-controls. |
| `PluginTabHost.tsx` | Host for a plugin-owned tab (`viewer:"plugin"`, path `<prefix>:<rest>`): resolves the enabled `editor-pane-tab` claim by prefix via `EditorPaneTabSlot`; with no enabled claim renders `PluginTabUnavailable` ("Tab unavailable ({prefix})" + Close). Never reaches `CappedViewer`, so no `/api/file`. Exports `PluginTabHost`, `PluginTabUnavailable`, `pluginTabPrefix`. See change: add-browser-editor-pane-tab. |
| `tab-labels.ts` | `basename`, `prefixOf`, `tabLabelText`, `tabHaystack`: one label/location text for the strip, title line and `matchTabs`. See change: scope-anchor-editor-pane-controls. |
| `TabActions.tsx` | System-open actions: file → Open in default app (`tab-open-in-app`) + Reveal (`tab-reveal-in-file-manager`, label from server `platform`: Finder / Explorer / file manager), gated on `systemOpen`; url → `tab-open-in-browser`. `variant="menu"` renders them as menu rows (narrow fold). See change: scope-anchor-editor-pane-controls. New-context opens go through `openExternalUrl` (`classifyUrl`; `noopener,noreferrer`). See change: harden-client-sinks. |
| `TabContextMenu.tsx` | Tab context menu (`tab-context-menu`): Close, Close other tabs, Close tabs to the right (disabled on last), Close all tabs, Show in file tree, Copy path (file tabs). See change: scope-anchor-editor-pane-controls. |
| `TabSearchPopover.tsx` | Search tabs popover (`tab-search-popover`): combobox `tab-search-input` + listbox over open tabs via `matchTabs`, highlights, unsaved dot (confirmed `dirty===true` only), Delete closes selected, row ✕ outside the option, bulk footer, no-match Enter → rail Search prefilled or "No open tab matches". See change: scope-anchor-editor-pane-controls. |
| `TooLargePreview.tsx` | Too-large notice + **Open raw** (`/api/file/raw`) mounted instead of a rich viewer. Props `{cwd, path, size?, cap?}`; `cap` defaults to `MAX_PREVIEW_BYTES`, office previews pass `OFFICE_SIZE_CAPS.<kind>` on HTTP 413. See change: open-view-command-in-editor-pane, harden-untrusted-content-ingestion. Default `cap` = configured editor limit (`useEditorLimits`); office callers still pass fixed caps. See change: gate-changed-on-disk-by-content. Variable link targets render via `SafeLink` (`classifyUrl`; blocked → inert text). See change: harden-client-sinks. |
| `monaco-setup.ts` | Shared Monaco bootstrap. Worker `?worker` imports + MonacoEnvironment + loader.config. Side-effect import by MonacoBuffer + MarkdownEditor. See change: directory-settings-page-and-scoped-md-editing. |
| `MonacoBuffer.tsx` | Heavy lazy Monaco chunk. Imports `monaco-editor` + `@monaco-editor/react`. Bundles editor/json/css/html workers via `?worker`; ts.worker omitted (read-only, no LSP); ts/js diagnostics disabled. Read-only. Theme via `buildMonacoTheme`, recolors on theme/mode change. Fetches content via `/api/file`. Scrolls to line. Theme via shared `useThemeContext()` (recolors live on theme switch). See change: add-internal-monaco-editor-pane. Worker bootstrap moved to monaco-setup.ts. See change: directory-settings-page-and-scoped-md-editing. See change: improve-content-editor. See change: surface-denial-remedy-in-previews — `/api/file` read via `usePreviewFetch()` (opted out unless operator provenance). |
| `pseudo-tab-registry.tsx` | Half (b) of the registry split: `pseudoTabRegistry` for diff/url/live-server/terminal. Owns the `DiffViewer` import, so only `EditorPane` may import it. `DiffViewer` is a `React.lazy` wrapper (D4). See change: cleanup-import-cycles. See change: add-lazy-terminal-diff-bootstrap. Typed `Record<RegistryPseudoTabViewer,…>`: `plugin` is NOT a key (rendered by `PluginTabHost`). See change: add-browser-editor-pane-tab. |
| `TerminalPaneLayer.tsx` | Keep-alive terminal layer. Mounts one `TerminalView` per open `term:<id>` tab, visibility toggled by the active tab — switching to a file tab and back does NOT tear down + reconnect the xterm WS (single-mount-per-id contract, avoids fix-terminal-half-height-dual-mount). Fills the file-viewer body region; hidden when a file tab active. Reads `paneState`+`terminal` from `useSplitWorkspace`. Exports `TerminalPaneLayer`. See change: terminals-in-tabbed-panes. |
| `types.ts` | `ViewerProps` contract `{ cwd, path, kind, mimeType, size, line? }`. Shared by all viewers. See change: add-internal-monaco-editor-pane. `ViewerProps` adds `reloadNonce?`, `updatedByAgent?`. See change: improve-canvas-html-and-url-ux. |
| `viewer-kinds.ts` | Component-free leaf partitioning `ViewerKind` (20) into `OPEN_PATH_VIEWERS` (15) + `PSEUDO_TAB_VIEWERS` (5) with guard `isPseudoTabViewer`. Compile-time totality via `_AssertNever` (NOT `const _x: T[] = []`, which is vacuous). Must import no viewer. See change: cleanup-import-cycles. `PSEUDO_TAB_VIEWERS` gains `plugin` (5); `RegistryPseudoTabViewer` = pseudo kinds minus `plugin`; union total 20. See change: add-browser-editor-pane-tab. |
| `viewer-registry.tsx` | `viewerRegistry` maps `ViewerKind`→component. monaco=`React.lazy(MonacoBuffer)` boundary. Rich kinds delegate to shared `preview/*` (thin `ViewerProps`→`{target}` adapters): pdf→`PdfPreview` (pdfjs, replaces broken `<object>`), html→`HtmlPreview` (sandboxed, no scripts), video→`VideoPreview`, image→`ImagePreview` variant=full (pan/zoom), audio→`AudioPreview`, mermaid→`MermaidViewer`, markdown/binary-warn eager. `line` reaches Monaco only. Since the D3 split it maps ONLY the 15 `fileKind()`-returnable kinds; `diff`/`terminal`/`url`/`live-server` moved to `pseudo-tab-registry.tsx`. See change: add-internal-monaco-editor-pane. See change: improve-content-editor. See change: add-change-summary-table. See change: terminals-in-tabbed-panes. **Split (half a):** now `Record<OpenPathViewer, …>` over the 14 `fileKind()`-returnable kinds only; diff/terminal/url/live-server moved to `pseudo-tab-registry.tsx`. Do NOT import a pseudo-tab viewer here — keeping `DiffViewer` out is what breaks the cycle. See change: cleanup-import-cycles. asciidoc→`EditablePreviewTab` (prefix `adoc`, preview=`AsciiDocPreview` in `p-4`); tab body is the scroller (editor pane gives none; overlay does). docx→`DocxPreview` wrapped in `h-full min-h-0 overflow-auto` scroller (editor pane gives none). |