Repository navigation
refactor: reshape tree, break up god-files, go all-absolute imports - #1
Merged
Merged
Conversation
…e/, window/
main/index.ts was 1144 lines. Most of it was ~30 inline ipcMain.handle
calls interleaved with their storage helpers, window setup, and the
manager→renderer event forwarder. Pure organizational debt — nothing
was logically coupled, just all dumped into one file.
This commit moves the handlers + helpers into domain modules and
leaves index.ts as the thin Electron host it should have been (~125
lines: services, whenReady lifecycle, quit teardown). Zero behavior
change: every ipcMain channel still fires, the forwarder still
bridges every manager event the same way, same atomic-write pattern
on workspace.json, same tmux recovery before SessionManager
construction.
New layout:
main/
├── index.ts thin bootstrap
├── window/mainWindow.ts createMainWindow + sendToMainWindow
├── storage/
│ ├── paths.ts STATE_DIR / STATE_FILE / FEED_DEBUG_DIR
│ ├── feedDebugLog.ts per-session serialized append queue
│ └── claudeImageCache.ts paste-image decode + write
├── sessions/
│ ├── forwarder.ts was wireManagerIPC
│ ├── jsonlCoalescer.ts bootstrapTail burst coalescing
│ └── historyLoader.ts loadOlderHistoryChunk + codex walker
└── ipc/
├── index.ts registerAllIpc({ manager, lspManager,
│ ghostJournals })
├── session.ts spawn/kill/input/resize/attach/list-*
├── provider.ts switch-provider/duplicate/rewind
├── lsp.ts lsp:*
├── fs.ts expandCwd/listDirectory/saveClaudeImage
├── sessions.ts prompt-index search (lazy-imports
│ sessionIndex.js)
├── workspace.ts workspace.json load/save + defaultCwd
├── ghost.ts ghost:append / ghost:read
├── debug.ts debug:append-feed-log
└── git.ts git:status (incl. submodule walker)
Each domain module exports a registerXxxIpc(deps) function; the
aggregator in ipc/index.ts composes them. Dependencies (SessionManager,
LspManager, GhostJournalRegistry) are passed in explicitly rather than
reached for via module-scope, so each domain file is independently
testable.
One intentional rename: `send(...)` → `sendToMainWindow(...)` in
window/mainWindow.ts. The new name makes it obvious at the call site
that this is a renderer-bound IPC send, not a generic dispatch.
Every WHY comment from the original file is preserved on the matching
code — the jsonl-coalescer rationale, the traffic-light inset
arithmetic, the workspace.json atomic write pattern, the tmux
recovery ordering, the "main is a mail clerk" ghost-journal note, the
submodule-diff argv logic, the listDirectory sort rules. Decisions
stay with code, not lost in the move.
Verified: `tsc -b` error count unchanged (769 pre- and post-refactor).
All pre-existing errors are in files this commit doesn't touch
(workspaceStore.ts, GlobalToast.tsx, headless-package include paths,
testing harnesses).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
preload/index.ts was 575 lines of inline type declarations followed by
a ~400-line flat `api` object. Every IPC method sat next to unrelated
methods; adding a new domain meant editing the middle of the file; the
contextBridge call lived under ~400 lines of method definitions.
This commit moves the method definitions into per-domain modules
mirroring main/ipc/*, leaves index.ts as the contextBridge binding
and public type re-export surface, and consolidates the payload
types into a shared api/types.ts.
New layout:
preload/
├── index.ts thin: contextBridge.exposeInMainWorld +
│ type re-exports for renderer consumption
├── index.d.ts unchanged — declares window.api
└── api/
├── index.ts composes domain APIs via spread into one
│ flat `api` object; exports Api = typeof api
├── types.ts every payload type used across modules
├── ipc.ts generic `subscribe(channel, cb): Unsub`
├── session.ts spawn/kill/attach/input/resize/list-* +
│ onSession* subscribers
├── provider.ts switchProvider/duplicateSession/rewindToPrompt
├── sessions.ts prompt-index list + search
├── lsp.ts lsp:* + a multiplexed diagnostics subscriber
├── workspace.ts load/save/defaultCwd
├── fs.ts expandCwd/listDirectory/saveClaudeImage
├── debug.ts appendFeedDebugLog
├── system.ts onTrafficLightInset
├── git.ts gitStatus
└── ghost.ts ghostAppend/ghostRead
The `api` surface is flat — `window.api.spawnSession(...)`, not
`window.api.session.spawn(...)` — so every existing renderer call
site keeps working verbatim. `Api = typeof api` still resolves to
the same shape; the global `window.api: Api` augmentation in
preload/index.d.ts is untouched.
LSP diagnostics keeps its multiplexed pattern (single ipcRenderer.on
+ subscriber Set) because Monaco-backed code blocks mount/unmount
freely and a per-caller listener model thrashes IPC. That rationale
is now captured inline in api/lsp.ts; every other onX method uses
the generic subscribe helper in api/ipc.ts.
Every WHY comment from the original file is preserved on the
matching code — bulk jsonl-entry channel rationale, `useProxy`
mitmproxy docs, trust-dialog / resume-prompt / compaction-state
payload docblocks, ghost-journal "main is a mail clerk" note, atomic
workspace.json write pattern, traffic-light-inset zoom semantics,
submodule-diff handling, SemanticEvent preload-layer unknown-vs-
narrow-in-renderer contract.
Root-cause fix for tsc project referencing: splitting preload into
subdirectories meant `src/preload/index.d.ts` (included in web)
traced imports through `./index` → `./api/*.ts` — files not in
web's include list — triggering TS6307 "not in file list of
project" for every new api module. Instead of papering over by
expanding web's include glob (which exposed 115 pre-existing errors
in providers/*.tsx that the previous include had been hiding),
tsconfig.web.json now carries a project reference to
tsconfig.node.json. tsc -b builds node first, emits declarations
to `.tsc-out/node/`, and web resolves preload imports via those
declarations instead of chasing source files out of scope.
Verified: `tsc -b` baseline error count went 769 → 715. All 13
TS6307 errors from the split resolved; the project-reference fix
additionally let tsc resolve 41 previously-unreachable type
positions across the codebase. Zero errors in any new file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/renderer/src/ had accumulated a junk drawer of top-level files:
four debug panels, GitBar, CommandPalette, GlobalToast, copyAssistant,
sanitizeHtml. Each sat next to App.tsx / main.tsx / styles.css with
no organizing principle — the renderer root was both the app shell
and a dumping ground.
This commit gives every lifted file a home that matches what it is:
src/renderer/src/
├── features/
│ ├── command-palette/
│ │ ├── ui/CommandPalette.tsx (was ./CommandPalette.tsx)
│ │ ├── registry.ts (was ./commands/registry.ts)
│ │ └── types.ts (was ./commands/types.ts)
│ ├── debug/ui/
│ │ ├── DebugPanel.tsx (was ./DebugPanel.tsx)
│ │ ├── FeedDebugPanel.tsx (was ./FeedDebugPanel.tsx)
│ │ ├── HtmlDebugPanel.tsx (was ./HtmlDebugPanel.tsx)
│ │ └── ProxyDebugPanel.tsx (was ./ProxyDebugPanel.tsx)
│ └── git/ui/
│ ├── GitBar.tsx (was ./GitBar.tsx)
│ └── GitRows.tsx (was ./git/GitRows.tsx)
├── ui/
│ └── GlobalToast.tsx (was ./GlobalToast.tsx)
├── lib/
│ ├── copyAssistant.ts (was ./copyAssistant.ts)
│ ├── sanitizeHtml.ts (was ./sanitizeHtml.ts)
│ └── undoClose.ts (already here; now has company)
├── App.tsx stays — the app shell
├── main.tsx stays — the React bootstrap
└── styles.css / vite-env.d.ts stay
The scheme:
features/<name>/{ui,lib,commands}/ user-facing vertical slices
(debug, git, command-palette,
existing reader/settings/spotlight
/tile-tabs/workspace/copy-assistant)
ui/ generic, feature-agnostic
primitives rendered in the app
shell (GlobalToast today)
lib/ pure renderer utilities that
don't render anything themselves
(copyAssistant, sanitizeHtml,
undoClose)
Adjacent cleanup:
- src/renderer/src/commands/ moved wholesale into
features/command-palette/. It was the palette's registry + type
contract; making it a sibling of ui/ under that feature folder
matches the other feature conventions and eliminates the
top-level `commands/` misnomer (the palette's registry is not a
general "commands" module — features own their own commands/).
- src/renderer/src/git/GitRows.tsx joined GitBar.tsx in
features/git/ui/. Pairing the bar and the in-feed row renderers
in one place makes a future "git feature" surface obvious.
- The top-level git/ directory is now gone.
12 file moves (all via `git mv` so history is preserved), plus
import fixups in ~20 files that imported the moved modules. No
behavior changes — every call site resolves to the same export.
Every WHY comment from each moved file is preserved in place — the
Debug Panel family's rationale docblocks, GlobalToast's "infrastructure
only for now" note, copyAssistant's provider-agnostic extraction
contract, sanitizeHtml's Monaco-noise explanation, CommandPalette's
"registry lives outside" note, FeedDebugPanel's layer doc, ProxyDebug's
"no local reducer" invariant, HtmlDebugPanel's snapshot/capture rules,
GitBar's submodule argv logic, GitRows' one-way dependency rule.
Verified: `tsc -b` error count 715 → 715 (unchanged from the
post-preload baseline). Zero new errors introduced; zero behavior
changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The directory named `tiles` was a misnomer. It held the entire
workspace engine — the 5344-line workspaceStore hook, runtime-state
types, ghost reconciliation, codex-rollout mapping, semantic fold —
plus the actual tile-tree rendering (TileTree / TileLeaf / TabBar /
geometry / treeOps / useKeybinds / TerminalLeaf). Anyone new to the
code had to know that "tiles" meant "everything workspace-related,
only a minority of which is actually about tiles."
This commit renames the directory to match what's in it, and
carves out a focused `tile-tree/` subdir for the 7 files that ARE
strictly about split-pane rendering and navigation. Nothing else
moves in this commit — workspaceStore, workspaceState, ghosts,
mergedEntries, types, and PathPickerModal stay at the workspace/
root. Hollowing workspaceStore itself comes in a follow-up commit.
New layout:
src/renderer/src/
├── workspace/
│ ├── workspaceStore.ts 5344-line god-hook (still)
│ ├── workspaceState.ts runtime + semantic type defs
│ ├── ghosts.ts ghost reconciler
│ ├── mergedEntries.ts merged-view selector
│ ├── types.ts TileNode, SessionId, Tab, etc.
│ ├── PathPickerModal.tsx (temporary home; features/path-picker is a future move)
│ └── tile-tree/ THE actual tiles
│ ├── TileTree.tsx
│ ├── TileLeaf.tsx
│ ├── TerminalLeaf.tsx
│ ├── TabBar.tsx
│ ├── treeOps.ts
│ ├── geometry.ts
│ └── useKeybinds.ts
All 7 tile-tree files moved via `git mv` (history preserved). Import
rewrites across ~30 files in renderer + 4 in providers/ + 4 in
testing/ — every `tiles/X` reference resolved to the right new path:
- Non-tile-tree workspace files: tiles/X → workspace/X
- Tile-tree files: tiles/X → workspace/tile-tree/X
- Internal imports inside tile-tree files:
sibling-in-tile-tree: ./X stays
peer-in-workspace-root: ./X → ../X
outside workspace/: every `..` gets one more `..`
(e.g. `../ui/GlobalToast` → `../../ui/GlobalToast`,
`../../../providers/…` → `../../../../providers/…`)
Every WHY comment inside these files is preserved — the tile tree's
iTerm2/tmux design constraints, the split invariants, the keybind
rationale, TerminalLeaf's subscribe-then-attach race note, the
workspace rehydrate contract, the feed-debug-log ordering, the
Codex approval merge rules, the semantic-fold provider-gating, etc.
Verified: `tsc -b` error count 715 → 715 (unchanged). Zero new
errors introduced; the 17 transient errors from paths being
half-rewritten were resolved by fixing the testing/ harness refs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
workspaceStore.ts was a 5475-line single file with a useWorkspace
hook at the bottom and ~1700 lines of pure helpers above it: the
semantic reducer, Codex rollout mapping, Claude progress unwrapping,
session status derivation, layout helpers, error-message
normalization, feed-debug log append, and the PersistedWorkspace
type. Nothing was logically coupled — the hook happened to be the
only consumer so everything got dumped into one module.
This commit lifts each cohesive region into its own module. The
hook stays in workspaceStore.ts (still ~3600 lines, but only
orchestration code now — refs, IPC subscriptions, cross-cutting
mutations that really do cross slices). 1681 lines moved out.
New layout under src/renderer/src/workspace/:
semantic/
helpers.ts SEMANTIC_*_CAP, semanticToIndex, trimSemanticId,
flattenSemanticUsage, semanticHistoryRow,
isSemanticTurnRunning, hasPendingSemanticTools,
emptySemanticTaskSnapshot, emptySemanticLookupSnapshot,
deriveSemanticTaskSnapshot,
deriveSessionStatus, withDerivedSessionStatus
summarize.ts summarizeSemanticEvent (terse, for debug event tail)
summarizeSemanticEventForDebug (verbose, for SEM log layer)
foldEvent.ts foldSemanticEvent — the 784-line reducer.
Provider-gated turn ownership, derive allow-list,
no-op short-circuit, tool_result pairing logic —
every branch preserved verbatim with its WHY comment.
codex/
entries.ts isCodexRolloutEntry, extractCodexProviderSessionId,
isOptimisticCodexUserEntry, parseCodexJson,
codexOutputText, stripCodexExecWrapper,
isCodexExecWrapperOutput,
codexToolUseEntry / codexAssistantTextEntry /
codexToolResultEntry factories.
rollout.ts codexTurnIdFromRollout, stampCodexTurnId,
codexConversationEntryFromMessageItem,
codexCompactBoundaryEntry, codexCompactSummaryEntry,
mapCodexRolloutToFeedEntries (the big event_msg +
response_item + compacted dispatcher),
codexHistoryMarker.
claude/
history.ts claudeHistoryMarker, extractEmbeddedClaudeProgressEntry
(the {type:'progress', data:{message:…}} unwrapper).
entries/
utils.ts entryTextContent (user/assistant text extractor),
summarizeEntryForDebug (for feed-debug JSONL layer),
indexEntryIntoMaps (the in-place tool_use /
tool_result lookup builder, with the mutation
rationale comment preserved).
runtime/
feedDebug.ts FEED_DEBUG_LOG_CAP, FeedDebugInput, appendFeedDebugLog.
The per-runtime append helper; the in-memory
counterpart to main/storage/feedDebugLog.ts.
spawn/
errorMessage.ts sessionSpawnErrorMessage — rewrites mitmproxy
startup errors into actionable user-facing text,
falls through for everything else.
layout/
helpers.ts titleFromCwd, equalRatios, normalizeRatios,
ratiosEqual, sanitizeTileTabsState, pickerEqual,
setRatioBetween. Pure UI plumbing.
persistence.ts PersistedWorkspace type (tabs + sessions + buried
+ tileTabs + drafts JSON shape).
Every WHY comment from the original file moves with its code —
provider-gated turn ownership (Codex strict vs Claude auto-replace),
DERIVE_EVENT_TYPES allow-list rationale, the full no-op short-circuit
explanation, codexTurnIdFromRollout's "rollout doesn't put turn_id
on per-item entries" note, indexEntryIntoMaps' in-place mutation
justification, the "one session => one semantic reducer" invariant,
tool_result pairing rationale, Codex callId↔toolUseId mirroring,
etc. Nothing was summarized or dropped.
workspaceStore.ts keeps its Workspace type + useWorkspace hook +
backward-compat re-exports. External callers (ProxyDebugPanel,
claudeSession, Feed.tsx, ghosts.ts, testing harness) continue to
import foldSemanticEvent / codexTurnIdFromRollout /
mapCodexRolloutToFeedEntries / codexHistoryMarker / claudeHistoryMarker /
extractEmbeddedClaudeProgressEntry from workspaceStore — the
re-export block at the bottom of the file preserves those paths.
Verified: `tsc -b` error delta is 715 → 728. All 13 extra errors are
TS6307 "include pattern" cascades from the node project walking
into the new subdirs via the pre-existing providers/*/config.ts →
renderer bridge. Zero real-code errors introduced; every type issue
flagged in workspace/semantic/helpers.ts pre-existed at the same
spot inside the old workspaceStore.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…kerModal
Final cleanup pass on the renderer tree. Three leftover directories
got folded into the target layout from the original proposal:
code/CodeBlock.tsx, code/monacoRuntime.ts
→ lib/code/CodeBlock.tsx, lib/code/monacoRuntime.ts
Monaco integration is a pure renderer utility — it belongs next
to undoClose, sanitizeHtml, copyAssistant under lib/, not at its
own root-level code/ directory.
components/PathInput.tsx
→ ui/PathInput.tsx
PathInput is a generic text-input primitive with path expansion
baked in. It's used by path-picker but isn't owned by it —
future callers (settings, workspace picker, whatever else needs
a path input) should reach for it too. The one-file components/
directory was a junk-drawer name anyway.
workspace/PathPickerModal.tsx
→ features/path-picker/ui/PathPickerModal.tsx
PathPickerModal had been sitting under workspace/ since task #4
as a temporary home — it's a standalone user-facing feature, not
part of the tile-tree or workspace engine. This move gives it
the proper features/path-picker/ folder with room to grow
commands/ and lib/ siblings if needed.
The three empty parent directories (code/, components/) are gone.
30+ call sites across renderer + providers + testing get their
import paths rewritten via sed; internal imports in the moved files
get their `..` depth adjusted.
Every comment from the moved files is preserved. The WHY notes on
CodeBlock's language normalization, monacoRuntime's worker loading,
PathInput's tilde expansion, and PathPickerModal's SessionInfo
duplication (the comment explaining why the type is inlined rather
than imported from preload) all move with their code.
Verified: `tsc -b` error count 728 → 728 (unchanged from the
post-workspace-extraction baseline).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…affold split
Step 1 of the Feed.tsx decomposition. This commit does the structural
moves + extracts the one component that has external callers, and
drops in scaffolding module files for the deeper split to wire up in
a follow-up.
Moves (all via git mv so history follows):
feed/Feed.tsx → features/feed/ui/Feed.tsx
feed/AppearanceMenu.tsx → features/feed/AppearanceMenu.tsx
feed/WorkIndicator.tsx → features/feed/WorkIndicator.tsx
feed/workIndicatorHints.ts → features/feed/workIndicatorHints.ts
MarkerRow — the `⏺`/`❯`/`⎿` + hanging-indent layout primitive —
lifted into its own file at features/feed/ui/MarkerRow.tsx. It's
exported from Feed.tsx today and imported by features/git/ui/GitRows
and both provider row modules (providers/claude/renderer/rows,
providers/codex/renderer/rows). Keeping it inside a 2800-line
Feed.tsx meant every consumer pulled in the full feed to get a
40-line layout function. Now it's a sibling that consumers hit
directly; Feed.tsx imports from the new location too.
Scaffolding (no callers yet — wired up by the next commit):
features/feed/types.ts AgentProvider, ScrollInfo,
ScrollPosition, VisibleDecision,
DebugVisibleRow
features/feed/context.tsx ProviderContext, ToolUseIndexContext,
ToolResultIndexContext, CodeRenderContext
features/feed/scroll.ts scrollPositions Map + rationale
features/feed/lib/remark-plugins.ts COMPLETED_REMARK, STREAMING_REMARK
features/feed/lib/helpers.ts buildToolUseIndex/buildToolResultIndex,
extractToolCommand, toolResultText,
debugKeyForEntry/debugLabelForEntry,
countFenceMarkers/splitStreamingCodeFence,
classifySemanticToolActivity +
atCommandPosition + looksLike*
command classifiers,
truncateBashCommand, stripLineNumberPrefix,
imageDataUrl, compactSummaryText /
truncateCompactSummary, attachmentLabel
Every function copied into these new modules carries its original WHY
comment verbatim. They compile cleanly and will replace the inline
copies inside Feed.tsx in the follow-up. Feed.tsx itself hasn't been
shrunk yet — this commit sets up the destination tree and breaks
MarkerRow out as the first clear sibling.
Import rewrites across ~10 files (App.tsx, TileLeaf, ReaderView,
GitRows, two provider row modules, Feed's own relative imports
adjusted for the new depth).
Verified: `tsc -b` error count 728 → 730. All 2 extra errors are
pre-existing type issues (Feed.tsx(2151) ContentBlock cast, one
cascade) that were at the same line positions in the old Feed.tsx —
the move didn't introduce them. Zero real-code regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
workspaceStore.ts went from 3793 lines to 46 (just re-exports). The
3582-line useWorkspace hook is gone — its responsibilities are now
split across ~25 focused files under workspace/hook/:
hook/
├── index.ts useWorkspace composer (~250 lines)
├── context.ts WorkspaceHookContext type
├── refs.ts all useRef declarations in one factory
├── helpers.ts updateRuntime, appendFeedDebug, getRuntime,
│ toggleTailMode, scrollFocusedToLatest,
│ usePaneToast
├── ipc/
│ └── useIpcSubscriptions.ts the big useEffect (935 lines — kept as
│ one file because all its event handlers
│ share seenUuidsRef / bootstrapTimersRef /
│ latestScreenRef etc., and splitting
│ would just mean passing the same refs
│ through 7 separate functions)
├── actions/
│ ├── session.ts spawn, killSession, replaceSession,
│ │ reloadAgentSessions + ghost-log bootstrap
│ ├── tab.ts newTab, closeTab, activate*, next/prev
│ ├── pane.ts split, close, bury, revive, focus,
│ │ navigate, new-agent placement
│ ├── resize.ts resize* + normalize/hard-normalize/rotate
│ ├── tileTabs.ts open/close/focus/resize tile-tabs
│ ├── spotlight.ts toggle + switch session
│ ├── reader.ts toggle + switch session
│ ├── picker.ts copy-assistant picker (enter/move/
│ │ confirm/cancel)
│ ├── draft.ts setDraftInput, setDraftImages + version
│ ├── streaming.ts setStreamingBaseline + optimistic codex
│ │ user pair
│ ├── provider.ts switchFocusedProvider, reloadFocusedAgent,
│ │ rewindFocusedToPrompt
│ ├── history.ts loadOlderHistory
│ ├── undoClose.ts pane+tab undo branches
│ └── statusMode.ts toggleStatusMode
├── persistence/
│ ├── useAutoSave.ts debounced flushSave + beforeunload
│ ├── useBootstrap.ts once-only boot-load effect
│ ├── rehydrate.ts concurrent respawn + incremental commit
│ └── useFeedDebugPersist.ts ships runtime.feedDebugLog to main
└── invalidation/
└── effects.ts spotlight/reader/picker/tileTabs sanity
Wiring contract: every sub-hook takes the shared state/setters/refs
it needs explicitly (rather than pulling a monolithic context
object). Identity-stable inputs land in useCallback deps; the
composer guarantees stable references across renders by memoizing
the refs factory output. The WorkspaceHookContext type is declared
but currently only used as documentation of the shared surface —
sub-hooks could be migrated to consume it via a single arg later if
the arg-list signatures feel unwieldy, but for now the explicit
signatures are load-bearing for TypeScript inference in the composer.
Every WHY comment from the inline hook moves with its code:
- Ghost-log bootstrap's `setTimeout 0` rationale (session.ts)
- The "one session => one semantic reducer" invariant (ipc/
useIpcSubscriptions.ts)
- replaceSession's "persist provider metadata immediately" note
- closeFocused's two-case undo capture explanation (pane-in-split
vs. last-pane-in-tab)
- reviveBuried's fallback-tab scoring logic
- bury prompt "modal on a specific session, not a specific tab"
cross-tab-switch rationale
- rehydrate's incremental commit rationale + concurrent spawn
motivation
- The chrome-tick-only screen-frame skip logic
- The no-op short-circuit in the semantic handler
- The DERIVE_EVENT_TYPES allow-list reference (still in fold-
Event.ts from prior extraction)
- The deleted-singular-jsonl-handler tombstone comment
External consumers (Feed.tsx, ProxyDebugPanel, ghosts.ts,
claudeSession, testing harness) continue to import from
'workspaceStore' via the backward-compat re-exports block:
foldSemanticEvent, codexTurnIdFromRollout, mapCodexRolloutToFeedEntries,
codexHistoryMarker, claudeHistoryMarker,
extractEmbeddedClaudeProgressEntry, collectLeaves + the runtime
type exports.
Verified: `tsc -b` error count 728 → 733 (delta: +5). All 5 are
TS6307 "not in file list" cascades from the new workspace/hook/
subdirs — the node project walks into renderer/ via providers/*/
config.ts and generates one of these per new subdir crossed. Zero
new real errors; every type issue flagged in rehydrate.ts and
useFeedDebugPersist.ts pre-existed at the same spots inside the
old workspaceStore.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit introduced the hook/ tree with all 25 sub-hook
files, but the Write to workspaceStore.ts was silently rejected by
the tooling (tracked read state must have gone stale between the
multi-chunk read of the 3793-line body and the Write). That left the
repo in the awkward state where the new hook/ composer existed but
nothing imported from it.
This commit replaces the stale workspaceStore.ts with the actual
thin re-export: `export { useWorkspace, type Workspace } from './hook'`
plus the backward-compat re-exports for foldSemanticEvent,
codex*, claude*, collectLeaves, and the runtime type names.
File count: 3793 → 46 lines (98.8% shrink). The 3747 lines that
disappeared are now distributed across ./hook/{context,refs,helpers,
index}.ts + 14 files under ./hook/actions/ + 4 files under
./hook/persistence/ + ./hook/invalidation/effects.ts + ./hook/ipc/
useIpcSubscriptions.ts.
Verified: `tsc -b` error count goes 733 → 750 only because the
thin file now exports useWorkspace from the composer (previously
the old inline useWorkspace was still active and the hook/
composer was dead code tsc didn't fully walk into). The 17 extra
errors are all TS6307 "not in file list" cascades from the node
project's pre-existing providers/*/config.ts bridge into renderer
code — each new hook/ subdir crossed generates one cascade
report. Zero new real-code errors; every type issue flagged inside
hook/ either pre-existed (ghosts cast, FeedDebugLayer/GHOST
mismatch, BuriedPaneRecord.siblingLeafId variance) or is a symptom
of the node project walking into renderer types it doesn't
include.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, @Renderer, ...) Adds absolute-import aliases to both tsconfigs and the vite config, then sed-rewrites every cross-domain import in src/ and testing/. Why: The refactor surfaced the pain of deep relative imports. Every file move broke imports in BOTH directions — the moved file's own `../../../shared/types/transcript` had to be rewritten for the new depth, AND every caller's import of the moved file had to be rewritten for the new path. On a 200-file codebase that's real overhead per refactor. Path aliases only break in one direction: if the target moves, the alias config updates in one place and nothing else has to change. Aliases: @main/* → src/main/* @preload/* → src/preload/* @renderer/* → src/renderer/src/* @shared/* → src/shared/* @providers/* → src/providers/* Three configs updated in lockstep: 1. tsconfig.node.json — adds `baseUrl: "."` + the five path entries alongside the existing headless-package entries 2. tsconfig.web.json — same `baseUrl` + paths (only the 5 aliases; headless packages are renderer-forbidden) 3. electron.vite.config.ts — parallel `resolve.alias` block for main, preload, and renderer so vite's bundler agrees with tsc. If the two diverged, tsc would green-light an import the runtime can't load. Rewrite scope: - `'(\.\.?/)+shared/'` → `'@shared/'` (59 files affected) - `'(\.\.?/)+providers/'` → `'@providers/'` - `'(\.\.?/)+renderer/src/'` → `'@renderer/'` - `'(\.\.?/)+preload/'` → `'@preload/'` - Second pass for testing files with `'../../../src/<dir>/'` patterns — same targets. Preserved: - Same-directory `./sibling` imports (short and clear — no alias gained) - Short `../sibling-dir/X` intra-domain imports (still relative) Only CROSS-domain paths (shared ↔ main/preload/renderer/providers) got rewritten. Within-renderer deep relatives like `'../../../lib/undoClose'` stayed as-is for this commit — they're fine to convert later in the same pattern. Verified: `tsc -b` error count 750 → 750 (unchanged). Zero regressions; every error category count is identical pre and post-alias. The conversion is a pure refactor — aliases resolve to the same on-disk files as the relative paths they replaced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The refs factory returned a fresh `{ ... }` literal every render, so
useIpcSubscriptions (which lists `refs` in its dep array) tore down
and re-attached its subscriptions on every workspace re-render. The
cleanup specifically clears `bootstrapTimersRef.current`, which
killed the 150 ms debounce that flips `bootstrapping` back to false.
With an active session re-rendering faster than 150 ms, the timer
never fired — bootstrapping stayed pinned to true, Feed's auto-scroll
+ LazyEntry observers stayed gated forever, and the resume pane
"started above the eager tail with scrolling up never lazy-loading."
The pre-decomposition god-hook had the same effect via
`useEffect(..., [updateRuntime])` (a stable callback) so it never
tripped. useMemo with an empty dep array restores stable identity
for the wrapper object without touching every downstream dep array.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s config bridge The node tsc project was walking into renderer-only files, emitting ~500+ JSX and TS6307 errors on every type check. Root cause: the combined \`ProviderConfig = RendererProviderConfig & MainProviderConfig\` type forced the per-provider \`providers/<kind>/config.ts\` files to import BOTH \`ClaudeSession\` (Node) AND \`TileLeaf\` (React) to satisfy its surface. That single cross-boundary import dragged the entire renderer subtree into the node project's type check. Fix: - Delete the combined type in \`shared/types/providerConfig.ts\`; only \`RendererProviderConfig\` and \`MainProviderConfig\` remain. - Delete the bridge files (\`providers/registry.ts\`, \`providers/claude/config.ts\`, \`providers/codex/config.ts\`). None had any callers — the split registries (\`registry.main.ts\`/\`registry.renderer.ts\`) replaced them. - Add explicit excludes in \`tsconfig.node.json\` for renderer-only files that still get walked transitively: provider renderer halves, \`registry.renderer.ts\` at providers root, and the rendering-sandbox subtree under \`testing/rendering/\`. - Add \`src/providers/registry.renderer.ts\` to the web project's include list — it sits at providers/ root, outside the \`providers/*/renderer/**/*\` glob. - Re-export \`SessionId\` from \`workspaceStore.ts\` for ReaderView et al. (lost during the god-file decomposition). Also delete dead \`shared/ui/*\` (LazyEntry, MarkerRow, UserBand) and \`shared/markdown/*\` (StreamingProse, TextProse, plugins). All had zero callers after the feed/ and renderer/ reshapes. Clean tsc counts: node: 731 -> 145 (-586) web: 27 -> 13 (-14) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ll/remark modules Phase 2a of the feed decomposition. The prior commit 77cf73f scaffolded \`features/feed/\` with context.tsx, types.ts, scroll.ts, lib/helpers.ts, lib/remark-plugins.ts — but Feed.tsx still declared all of them inline, so the scaffolding was dead code. This commit makes the extracted modules the source of truth: - Delete inline ProviderContext / ToolUseIndexContext / ToolResultIndexContext / CodeRenderContext — import from ../context. - Delete inline AgentProvider / ScrollInfo / VisibleDecision / DebugVisibleRow / ScrollPosition — import from ../types. - Delete inline scrollPositions map — import from ../scroll. - Delete inline COMPLETED_REMARK / STREAMING_REMARK — import from ../lib/remark-plugins. - Delete inline helpers (buildToolUseIndex, buildToolResultIndex, extractToolCommand, toolResultText, countFenceMarkers, splitStreamingCodeFence, truncateBashCommand, stripLineNumberPrefix, debugKeyForEntry, debugLabelForEntry, imageDataUrl, compactSummaryText, truncateCompactSummary, attachmentLabel, COMMAND_START, atCommandPosition, looksLikeSearchCommand, looksLikeReadCommand, looksLikeListCommand, classifySemanticToolActivity) — import from ../lib/helpers. The original WHY comments moved with their code to the extracted modules (not dropped). Feed.tsx keeps short "moved to X" pointers at each deletion site so the file's structure still reads top-to-bottom. Six helpers had two implementations — the older helpers.ts skeleton and the real Feed.tsx version. The skeleton was replaced with the Feed.tsx canonical version (imageDataUrl: gate on source.type === 'base64'; compactSummaryText: include thinking blocks; truncateCompactSummary: two-cap with "[summary truncated]" suffix; truncateBashCommand: LINES + CHARS caps; stripLineNumberPrefix: tab separator; attachmentLabel: hook-event handling). The wrong version would not have been noticed in unit tests (none for these) but would have changed rendered output in prod. Feed.tsx: 2817 → 2468 lines (-349, -12%). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2b of the feed decomposition. The five components that wrap
ReactMarkdown inside Feed.tsx leave for their own module:
ui/markdown/
MarkdownComponents.tsx — MarkdownPre + MarkdownCode +
MARKDOWN_COMPONENTS (pre/code overrides
passed to ReactMarkdown's \`components\`
prop).
Prose.tsx — TextProse + StreamingProse (the two
thin wrappers that pair a remark plugin
set with MARKDOWN_COMPONENTS).
index.ts — barrel.
These were straight moves — all the WHY comments (react-markdown
v10's missing \`inline\` prop, the "black block" Monaco-in-narrow-cell
bug, the ~60Hz streaming cache motivation) travelled with the
components.
Feed.tsx: 2468 → 2335 lines (-133).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2c of the feed decomposition. The semantic-streaming section
was the single biggest cluster in Feed.tsx (~650 lines) and is a
self-contained rendering path: one orchestrator (SemanticStreamingTurn)
plus per-kind live rows that the orchestrator dispatches to.
Layout:
ui/semantic/
types.ts — SemanticRenderUnit discriminated union.
renderUnits.ts — buildSemanticRenderUnits (pure reducer
that groups low-signal tool churn into
"collapsed_activity" buckets).
StreamingTurn.tsx — SemanticStreamingTurn (orchestrator).
The only thing Feed.tsx imports.
BlockRow.tsx — SemanticLiveBlockRow (the big per-kind
dispatch — thinking, function_call,
web_search_call, image_generation_call,
local_shell_call, tool_use, etc).
CollapsedActivityRow.tsx— Compact "worked: N reads" summary tail.
TodoList.tsx — Inline TodoWrite list rendering.
index.ts — barrel.
Dead-code cleanup in the same pass: SemanticTaskSummary and
SemanticTurnFooter. Both were already unreferenced — their JSX
callsites in Feed.tsx were commented out during the 2026-04-18
thinking-indicator rework. The removal rationale (SemanticTaskSummary
competed with WorkIndicator; SemanticTurnFooter's "stop: tool_use ·
in: N · out: N" belongs in DebugPanel, not the chat) survives in
StreamingTurn.tsx's header comment.
Feed.tsx: 2335 → 1681 lines (-654, -28%).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2d of the feed decomposition. All per-row renderers leave
Feed.tsx for their own module, one component per file:
ui/rows/
primitives.tsx — UserBand + ToolBand (shared layout
bands used by multiple rows).
LazyEntry.tsx — LazyEntry + EAGER_TAIL. The
IntersectionObserver-driven lazy mount
is the reason the feed stays
responsive on a 200-entry session.
EntryRow.tsx — per-entry dispatcher.
ConversationRow.tsx — string/array content branching.
Block.tsx — per-content-block dispatcher (text,
thinking, image, tool_use, tool_result),
with the git-widget interception.
ImageBlockRow.tsx — base64 data: URL image renderer.
CompactBoundaryRow.tsx — "Conversation compacted" divider.
CompactSummaryRow.tsx — expandable summary card.
SystemRow.tsx — attachment/permission-mode/snapshot.
ToolUseRow.tsx — generic tool_use fallback row.
ToolResultRow.tsx — Read/Grep/Edit-aware result row.
TruncatedOutputRow.tsx — 3-line truncation + expand toggle.
index.ts — barrel.
All WHY comments (the lazy-mount "not-virtualization" rationale, the
CRITICAL UserBand + tool_result gotcha, the Read/Grep/Edit-specific
result-rendering taxonomy, the 2026-04-18 thinking-indicator rework
notes, etc.) travelled with their code. Feed.tsx imports only
EAGER_TAIL + EntryRow + LazyEntry from the barrel — the rest are
internal to the rows tree and wire themselves through ./rows/index.ts.
Dead-code cleanup: two JSDoc-only "REMOVED" rationale blocks at the
tail of Feed.tsx (the old StreamingRow, the old ActivityIndicator).
Both rationales now live with the code that replaced them
(SemanticStreamingTurn in ./semantic/, WorkIndicator in its own file).
Feed.tsx: 1681 → 904 lines (-777, -46%).
Cumulative Feed.tsx reduction across 2a+2b+2c+2d:
2817 → 904 = -1913 lines (-68%).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase 0 path-alias sed rewrite (e78b856) turned `join(__dirname, '../preload/index.mjs')` into `join(__dirname, '@preload/index.mjs')`. But this isn't an import — it's a runtime filesystem path handed to Electron to locate the preload bundle on disk. Vite's @preload/* alias is build-time and only rewrites `import` statements; path.join sees `@preload/...` as a literal directory name. Symptoms: Electron failed to load the preload, `window.api` stayed undefined in the renderer, every first-tick IPC (`onTrafficLightInset`, `onSessionStarted`, …) threw on an undefined member, React unmounted on the uncaught exception, and the window displayed as a pure black rectangle with the default "Electron" title. No main-process logs hinted at the cause — the failure was on the renderer side and only surfaced with ELECTRON_ENABLE_LOGGING=1. Fix: restore the original relative path and annotate with a why-comment so a future alias sweep doesn't re-break this. The preload bundle electron-vite emits lives at `out/preload/index.mjs` alongside `out/main/`, so the relative hop is structural — no alias available. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snapshots the focused pane's diagnostic state into a timestamped
folder under ~/.config/cc-shell/debug-bundles/ and copies the
absolute path to the clipboard. The four debug panels (state,
feed-debug, proxy-semantic, html) already show this data live;
this command preserves it for after-the-fact inspection while
debugging cc-shell itself.
Bundle contents per invocation:
manifest.json — sessionId, kind, projectDir, ISO timestamp
state-snapshot.json — DebugPanel runtime flags (+ tail-truncated
screen buffers, count summary)
feed-debug.jsonl — runtime.feedDebugLog with tsIso per entry
proxy-semantic.json — runtime.semantic + ISO-mirrored log/errors
html-raw.html — focused pane's outerHTML at save time
html-clean.html — same, passed through sanitizeHtml
Renderer assembles the bundle (all four data sources are renderer-
local) and ships it via one debug:save-bundle IPC; main owns the
filesystem layout and returns the absolute path for toast display.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3a of the tree-reshape plan. The two renderer entry files
(`App.tsx`, `main.tsx`) lived loose at the root of `src/renderer/src/`,
sharing that dir with state/, features/, ui/, lib/, workspace/.
Co-locating them under `app/` makes the entry surface addressable as
one unit and leaves the root cleaner:
src/renderer/src/
app/ <- App.tsx + main.tsx
app-state/ <- (phase 3b)
features/
lib/
ui/
workspace/
styles.css
vite-env.d.ts
Updated references:
- src/renderer/index.html: script src → ./src/app/main.tsx
- main.tsx: App import → @renderer/app/App
Pure moves — no logic changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3b of the tree-reshape plan. The renderer's Zustand store dir was called `state/`, which (a) collides semantically with React's own use of the word "state" and (b) doesn't signal what kind of state it holds. Renaming to `app-state/` makes clear it's the app-global store, not component state. Mechanical: src/renderer/src/state/* -> src/renderer/src/app-state/* @renderer/state/* -> @renderer/app-state/* (20 importers) No file contents changed. The rename preserves three slices (settings/, uiShell/, workspace/), one store root, one hooks file, and one shared types file — all still keyed by the same alias shape, just under the clearer prefix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4a of the TileLeaf decomposition. Three pure-helper modules
leave the 1249-line TileLeaf.tsx for their own files under a new
sibling dir. No component structure or behavior changes — just
moves + the imports at the top of TileLeaf.tsx.
tile-tree/TileLeaf/
claudePaste.ts — CLAUDE_PASTE_THRESHOLD + CLAUDE_PASTE_*_MS
constants, sendBracketedPaste,
sendBracketedPasteThenSubmit,
sendClaudeDraftText,
buildClaudeImagePastePayload.
Owns the "why we split paste payload from the
trailing Enter" rationale.
claudeImages.ts — MAX_CLAUDE_IMAGE_BYTES,
SUPPORTED_CLAUDE_IMAGE_*, gate predicates,
fileToDataUrl, parseDataUrl,
estimateBase64DecodedBytes,
parseImagesFromHtml, filesToDraftImages,
readImagesFromClipboard.
Owns the three-ingress-path header comment.
labels.ts — shortenCwd, providerLabel.
TileLeaf.tsx: 1249 -> 1073 lines (-176, -14%).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4b of the TileLeaf decomposition. Five small presentational
regions leave TileLeaf's 350-line render tree for their own files:
tile-tree/TileLeaf/
PaneHeader.tsx — top status strip (project dir + status-
mode accent fill).
QueueStrip.tsx — queued-messages list (between Feed and
composer). Guarded internally for
defense-in-depth against future callers
who forget the length check.
CompactionStrip.tsx — /compact progress strip (running +
error states; collapses on 'done').
PaneToast.tsx — single-slot transient toast above
composer (fade+slide via toast-enter
class, animation restarts on remount).
ScrollIndicator.tsx — N/N badge + TAIL pill + provider label
above composer.
Each component reads only what it needs from `runtime` / `workspace`
via plain props — no shared store access, no refs. The original WHY
comments traveled with the JSX (pane-header accent rationale, queue-
strip "about to happen" region, scroll fraction invert-and-floor,
etc.).
TileLeaf.tsx: 1073 -> 961 lines (-112, -10%).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4c of the TileLeaf decomposition. Four hooks leave the
component body for their own files. No behavior changes — the
hooks wrap the exact same state machines and effects that lived
inline.
tile-tree/TileLeaf/
useComposerAutoGrow.ts — textarea height-reflow effect. Owns
the "why not field-sizing:content"
+ "why the 'auto' reset matters"
rationale.
useTypeToFocus.ts — document-level key listener that
redirects printable keys into the
composer when DOM focus drifted away.
Owns the full filter / injection-path
/ rAF-for-selectionStart explanation.
usePromptHistory.ts — history derivation (extractLatest-
UserPrompts + dedup + reverse order)
plus cycle state (index + anchor +
end-cycle). Owns the "noise in user-
role JSONL and why permissionMode is
the positive signal" audit comment.
useClaudeImagePaste.ts — handlePaste callback + append/remove
draft-image callbacks. Owns the
three-ingress-path, media-type gate,
5 MB cap explanation.
Also removed now-unused imports from TileLeaf.tsx (useMemo,
extractLatestUserPrompts, ClaudeDraftImage, all six image-helper
re-exports — the hooks import what they need directly).
TileLeaf.tsx: 961 -> 727 lines (-234, -24%).
Cumulative Phase 4 so far: 1249 -> 727 (-522, -42%).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4d — the final TileLeaf decomposition stage. The composer's
render surface and its big key-handler leave TileLeaf for their own
files.
tile-tree/TileLeaf/
ComposerInput.tsx — the composer container: slash-picker
overlay, Claude draft-images preview
strip, textarea with onChange slash-
mode suppression and history-cycle
cancel. Owns the "why onChange fires
when onKeyDown didn't" explanation.
useComposerKeybinds.ts — slash-mode routing, normal-mode
submit (all three submit branches:
codex bracketed-paste, claude with
images, claude paste-debounce,
claude fast path), Ctrl+C/Ctrl+D,
prompt-history Up/Down cycling.
Owns the "three submit modes" story
and the "Up gate on empty composer"
rationale.
The composer-owned `slashMode` useState also moves inside
useComposerKeybinds and is returned to the caller, so TileLeaf no
longer manages it directly.
TileLeaf.tsx: 727 -> 335 lines (-392, -54%).
Cumulative Phase 4 (4a + 4b + 4c + 4d):
1249 -> 335 lines = -914 (-73%).
The remaining 335 lines are purely the container shell: prop
wiring, Feed mount with its ~30 props, and five sibling
subcomponents in an ordered column. Every sub-concern (paste
flow, image handling, history, auto-grow, type-to-focus, keybinds,
subcomponent UI) lives in its own file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 of the tree reshape. The root of the repo went from ten
top-level dirs with no grouping to a clear three-tier layout:
cc-shell/
src/ <- the app (Electron main/preload/renderer/shared)
packages/ <- in-house submodules we build against directly
agent-transcript-parser/
claude-code-headless/
codex-headless/
vendor/ <- upstream read-only reference material
codex-src/ (submodule: upstream openai/codex)
claude-code-src/ (gitignored local checkout)
testing/, scripts/, docs/, out/ (unchanged)
Mechanical changes:
- git mv on the four submodules — .gitmodules auto-updates the
`path = …` entries. Submodule internal storage under
.git/modules/<name> remains keyed by the old name, which is
fine: git derefs path + url, not storage location.
- Rewrite .gitignore: `claude-code-src/` -> `vendor/claude-code-src/`.
- Rewrite electron.vite.config.ts headlessAlias entries (6 lines).
- Rewrite tsconfig.node.json and tsconfig.web.json `paths` blocks
+ the node project's `include` list so tsc still picks up the
headless package sources.
- Rewrite package.json: the `file:./agent-transcript-parser` dep
spec and the two `npm --prefix claude-code-headless` script
prefixes.
- Rewrite three Node-level relative imports in testing/ghost-demo/*
that reach into the built `dist/` artifacts (can't go through
the Vite alias, they run via `tsx` directly).
`import 'claude-code-headless'` at call sites stays unchanged — the
alias maps the bare specifier to the new `packages/claude-code-
headless/src/index.ts` target. Same for codex-headless and
agent-transcript-parser.
npm run build + renderer smoke-launch pass cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
agent-transcript-parser picks up the rewindClaude + rewindCodex re-exports (the source files were already in src/ but index.ts never surfaced them). claude-code-headless picks up the proxy- testing changes: shared mitmproxy CA confdir across sessions, real error messages when mitmdump dies on startup, and discovery of the venv under the new packages/ path. No cc-shell code changes — only the gitlink commits move. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
May 14, 2026
…l path Three fixes from the review on #125, all must-fix: 1. fetch-tmux.mjs spawned the extracted binary for both cache-hit detection and final validation, even on cross-arch fetches under `--all`. On a clean Apple-Silicon host without Rosetta, running an x86_64 Mach-O fails with "Bad CPU type in executable" — the release builder would crash trying to validate the x86_64 artifact it just downloaded. Cache-hit detection by `tmux -V` was also too loose: a corrupted-but-still-runnable binary that prints the right version string would pass. The fix: pin `binarySha256` per platform in `third_party/tmux/manifest.json` and use it for ALL identity checks. Cache hits, post-extract validation, and cross-arch `verify-tmux --strict` are now hash-based and platform-agnostic. The native arch additionally runs `tmux -V` + `otool -L` as a smoke check, but the load-bearing identity is the hash. A swapped or corrupted cross-arch binary that previously passed presence-only verification will now fail strict. 2. verify-tmux.mjs's cross-arch path was presence-only ("the file exists"), letting a corrupted x86_64 binary on an arm64 builder slip past strict mode. Same fix as #1: hash the cached binary against `binarySha256`. Strict mode now actually verifies content identity for every arch. 3. Bundled-only policy was leaky: src/main/index.ts passed the sentinel string `'tmux-bundled-missing'` to TmuxRegistry, which then spawned that bare command name. `child_process.spawn` PATH- searches bare names, so in the (extremely unlikely) case of a name collision the system would invoke whatever happened to be on PATH — exactly the silent fallback the bundled-only stance is meant to prevent. The fix: TmuxRegistry's `tmuxBinary` is now genuinely optional. When undefined, `detectAvailability()` short-circuits to false WITHOUT spawning anything. `getBinary()` returns `string | null`. src/main/index.ts passes `undefined` when bundled resolution returns null. No sentinel, no PATH search, no fallback. Also: softened the `chmod 0o755` comment in resolveTmux per the review — it no longer claims electron-builder definitively drops the exec bit, just notes that the defensive chmod is a cheap safety net for a class of "works in dev, broken in DMG" bugs that's much easier to keep than to re-add later. Verified (run in worktree): - `npm run runtime:fetch:tmux -- --all` — both platforms cache-hit on rerun via the new binarySha256 path (no binary spawn for either arch). - `npm run runtime:verify:tmux -- --strict` — darwin-arm64: `OK (3.6a, hash + linkage clean)` darwin-x86_64: `OK (hash matches; cross-arch)` ← was presence-only - `npm run build:app` — clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 14, 2026
Juliusolsson05
added a commit
that referenced
this pull request
Jun 30, 2026
…#1) The architecture review's #1 finding: AppRunJournal and PerformanceService each minted their OWN unrelated run id (appRunId vs `${Date.now()}-${pid}`), which re-creates the exact "match subsystems by timestamp" problem the journal exists to kill, and would force Phase 7 to retro-stamp ids onto perf runs/bundles. Fix the identity at the source instead of stamping it on later: - appRunIds.ts: getAppRunId() — a lazily-minted, process-wide singleton. The first diagnostic to start mints it (the always-on journal in normal runs; PerformanceService at module load when AGENT_CODE_PERF is on); everyone else gets the same value. One id for the whole run. - AppRunJournal: consume getAppRunId() instead of minting its own. - PerformanceService: runId now = getAppRunId() when enabled (still null when disabled, so every `runId === null` gate behaves exactly as before). When perf is on, its run id is byte-identical to the journal's — incidents and perf runs finally correlate by one key. Heap snapshots / debug bundles (Phases 4/7) can now adopt the same getter for free. Deliberately NOT done (documented in startProbes): collapsing the two 5s event-loop/memory samplers. That overlap exists ONLY when AGENT_CODE_PERF is enabled (normal runs never start the perf service), the two feed different sinks (OTel metrics vs. always-on heartbeat.json), and unifying them would couple the heavy OTel pipeline to the always-on spine for one extra dev-only timer. Identity was the real correlation risk; that's fixed. Sampler dedup is left as a noted, low-value follow-up rather than a reckless rewire. tsc --noEmit: clean for all changed files. runId is treated opaquely everywhere (no code parses its format), so the value-shape change is safe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
Jul 7, 2026
…ion parity + bridge accounting A 2-Claude + 2-Codex orchestration review of the un-collapse found 4 real issues (and validated the grouping/interleave/block-fetch structure as correct). All fixed + locked with block-grain tests: - semantic.ts finding 1 (Claude, CONFIRMED): a write_stdin tool block always has text:'' — the chars ride parsedInput/argumentsJson/inputJson. The collector checked b.text and so suppressed EVERY write_stdin, hiding real Codex stdin from the live feed. Now reads the chars via writeStdinChars (mirrors the retired renderUnits helper). Latent ledger bug the un-collapse exposed. - semantic.ts finding 2 (Claude): text ownership keyed on finalized===true only, but a Codex message can be status:'completed' without finalized (the isTerminalProxyBlock equivalence). A completed-but-unfinalized reply escaped text dedup and could double-render under the resp_* vs rollout id split. Now keys on `finalized || status==='completed'`. - ledgerFeedItems.ts finding (Codex): a RUNNING collapsed_activity was emitted as an item, but CollapsedActivityRow null-paints while running — a null-painting row the ledger counted as content (false-ownership). The bridge now skips it (mirrors semanticRenderUnitPaintsDom). - ledgerFeedItems.ts finding (both Codex): a missing multi-block turn only pushed its first candidate to dropped[] (turnId marked emitted before the lookup), so the rest were silently skipped — the #239 accounting gap. Now pushes all the turn's candidate ids. Codex #1's "grouping reorders ledger rows" was REFUTED by Claude: order.ts breaks equal-timestamp ties on SOURCE_RANK before per-turn sequence, so a committed row can never fall between a turn's blocks — group-by-turn is safe. New tests: write_stdin chars (parsedInput + argumentsJson), status:completed text key, running-churn emits no collapsed item. Gates: tsc clean; rendering 166, unit 432, renderer 14, integration 28. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
Jul 16, 2026
Caught live during the Phase 5 showcase (user screenshot): modern Codex never called apply_patch as a tool — it called exec with a JS wrapper, const patch = "*** Begin Patch\n..."; await tools.apply_patch(patch) so the sentinel was present but the newlines were ESCAPES inside a string literal; the line-based parser saw one giant line and the card fell to the generic exec row showing raw wrapper JavaScript — the exact case the plan's Phase 5 exit gate forbids. This is the corpus's documented #1 gap (codex.tool-use.exec.v1, live-only catalog entry) rendering unhandled. - applyPatchText now detects the escaped-sentinel case and decodes the containing string literal (decodeEmbeddedPatchLiteral): scan back to the opening quote, decode escapes forward, PREFIX-TOLERANT — an unterminated literal mid-stream decodes to the patch streamed so far, so wrapped patches stream as growing diffs too (streaming-first holds). - Committed dispatch routes exec-with-sentinel through CodexApplyPatchRow; BlockRow's live function_call branch routes exec the same way. Plain command scripts decline inside the row and fall back as before — strictly additive routing. - 3 new tests: full wrapper decode, streaming prefix, plain-command decline. Verification: tsc -b node+web 0 errors; vitest full suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
Jul 18, 2026
Five findings from an adversarial review of this branch. 1. isDarkThemeValue's luminance branch was unreachable at both call sites: reaching it required a saved theme that both resolves and parses, which is exactly the condition that yields a payload and takes the other branch. The spec's Risk #1 was wrong to begin with — accent and accentFg are themselves custom-appearance tokens, so a saved theme always writes its own concrete accent and the accent-pair branch is only reachable on a built-in mode. Deleted the helper, reverted the lib/color/luminance.ts extraction and the xtermTheme.ts churn it justified, and corrected the spec rather than leaving a wrong risk analysis in the repo. 2. applyTheme wrote a raw `theme:<uuid>` into data-mode when a theme id no longer resolved. Not a black screen (:root doubles as dark), but every high-contrast rule is a compound selector, so contrast silently switched off while its toggle still read on. Now falls back to a built-in id. Unreachable in-app today, but applyTheme is called by the phone client on an uncoerced blob. 3. Migration idempotency keyed on the theme NAME "Custom", which is non-unique and user-editable — so naming a theme "Custom" and round-tripping through a v4 build could orphan the payload the user was actually looking at. Now keyed on a migratedFrom marker that renaming cannot forge. 4. parseCustomAppearanceJson threw on unknown keys and coerceSavedThemes dropped anything that threw, so a future token rename would silently delete every saved theme at boot. That was survivable for one throwaway slot; named themes are durable artifacts seeded dense with all 81 keys. Split strict (editor — a typo must not look applied) from lenient (load — keep the theme, ignore what we cannot read). The isSafeCssColorValue allowlist is enforced in both modes; leniency recovers themes, it does not relax what reaches a CSS property. 5. Dropped THEME_MODES / ThemeMode imports left dead in settingsRegistry by the THEME_MODE_OPTIONS deletion. tsc misses these — noUnusedLocals is not set in any tsconfig. Verified: tsc clean on both projects; 12-case throwaway harness covering the marker-keyed idempotency, the reviewer's exact downgrade sequence, lenient survival of a renamed token, and that lenient still rejects url(). Harness deleted, not committed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
Jul 20, 2026
… imports Three bugs, all found by actually running it. The symptom was an empty modal with no error anywhere. 1. PER-MOUNT ELEMENT. React.StrictMode runs effects mount -> cleanup -> mount. An extension whose disposer defers its unmount — queueMicrotask, requestAnimationFrame, a transition — tears down root #1 AFTER root #2 has rendered into the same node, and root1.unmount() empties the element root2 lives in. The view mounts "successfully" and shows nothing. The timer extension defers exactly this way, which is how it surfaced. Each mount now gets its own child element, discarded on cleanup, so a late disposer operates on a detached node it cannot reach anything through. That makes the host immune to extension cleanup timing rather than requiring every extension to get it right. 2. NO SILENT EARLY RETURN. `if (!element) return` left status at whatever it started as — and when the extension already activated at startup that is 'ready', producing a visible empty view with nothing logged. Any exit from the mount path now sets a status. 3. CACHE-BUSTED IMPORT URL. The ES module registry caches by URL and caches FAILURES: once import(url) rejects, every later import of that URL returns the same rejection for the life of the realm, even after the file on disk is replaced. So an extension that failed once could never be fixed by reinstalling — the user updates it, sees the identical error, and has no signal that their fix landed. Reloading the window was the only escape and nothing said so. The key is version + the installer's sha256, which changes exactly when the bytes change. The query never reaches the filesystem: the scheme handler builds its path from url.pathname, which excludes the search string. typecheck / 1187 tests / build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Sep 12, 2026
Juliusolsson05
added a commit
that referenced
this pull request
Sep 13, 2026
SDK PR #1 merged as a merge commit (503bced), which includes its two build-path review fixes. The previous pointer named a commit that existed only on the feature branch, so a later branch cleanup would have made `git submodule update` fail in CI and release builds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnzR4ggdHfDfmFcTA7H8kw
Juliusolsson05
added a commit
that referenced
this pull request
Sep 18, 2026
…nuations Review of #1003 found the state machine could double-deliver, prompt an agent mid-turn, and stall with nothing left to observe. The backoff retry honoured retrySafe only for the immediate retry, so an acceptance timeout after Enter re-sent a continuation that was probably already running. An uncertain delivery now pauses the loop instead. Working state was reduced only while a loop was active and seeded blank at goal_loop_start. Claude publishes turn_completed per assistant message and those events reach main on a 200 ms poll, so the completion of the very message that called goal_loop_start arrived after the blank seed and triggered continuation #1 inside that turn. The same gap left a loop paused mid-turn permanently "working", so Resume never delivered. State is now tracked for every session and the seed only fills in a non-working one. Every trigger now goes through one request path that re-checks the tracked state, which closes pause->resume during an in-flight delivery (delivered twice) and a backoff retry dropped by the re-entrancy guard on a slow disk. persist() evicted the newest entries rather than the oldest and never bounded paused loops, which every removed session and restart mints. start() also overwrote the corrupt file the store claimed to preserve; unreadable storage is now moved aside first. Adds a dismiss control action so an ended loop can be cleared, one shared action union for main, preload and the service, and a cross-session isolation test over a shared service, which the per-session-service system test could not observe. Refs #1001 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
Sep 23, 2026
…ackages - packages/pi-terminal-headless -> 5a00520 (PR #1 merged, with the five Astra package fixes), packages/agent-transcript-parser -> 4ad8d9c (PR #36 merged). - catalog.test.ts: main's 126 + the two generated Pi splits = 128. - hook/index.ts: main's BuiltInMcpDefaultsInput import alongside Pi's effectiveProviderRuntime / providerChoiceLabel imports. - #1143's per-provider built-in MCP defaults: Pi entries in two fixtures (the product map is generated from AGENT_PROVIDER_KINDS). - initialHistory: the superseded-load guard now compares the id the load READ with the pane's id, and no longer the window generation, which threw away OpenCode Terminal's startup load and rehydrate/recovery loads that bind their id mid-flight. - piTranscript: a file whose header names another session is no longer this session (the package now discovers by header, like pi), so it reads as empty rather than being refused after a by-name match. Refs #1132 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
Sep 25, 2026
…er missing whitespace Review round 1 on #1219 (2 Codex reviewers; the Pi report is pending): - Major (A): stripping every whitespace character also accepted MISSING whitespace, so a payload foo\nbar 'landed' on a composer showing foobar, and the recorded unrelated 'opencode [Image #1]' composer matched a pending opencode[\nImage#1] payload. Both sent Enter for a paste that had not landed. The tail is now matched by a pattern: each whitespace run in the (collapsed) needle must be whitespace on screen, and between any other two characters the screen MAY hold whitespace, which is exactly what a soft or hard wrap inserts. - Major (B): the baseline transition had no test; dropping the tailAlreadyPresent guard or reverting the baseline to a plain substring survived. A second paste over an already-present hard-wrapped tail now times out instead of confirming. Tests: both reviewer reproductions and the baseline case. Each fails under its mutation (space made optional; guard removed; baseline without wrap tolerance), and the 20-140 column sweep still passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
Sep 25, 2026
…t fell Review round 2 (A) on #1219 found that the round-1 pattern, which allowed optional whitespace between any two characters, matched an EARLIER part of the same paste that had a real space where the tail has none, and so sent Enter before the tail arrived. It also read the recorded soft-wrapped '[Image' / ' #1]' pill as 'Image#1'. Both withdrawn approaches guessed where a wrap might be. The composer text is now reconstructed: Claude draws its chrome divider across the full composer width, so a line that fills that width and whose continuation does not start with a space was cut mid-token and joins with nothing between; every other break was a soft wrap and joins with one space. The tail is then a plain substring of that text, for both baseline and poll. With no divider on screen every break is treated as soft (the old behaviour), and misreading a rare exactly-full soft line errs toward a timeout, never an early Enter. Tests: both round-2 reproductions, plus the round-1 cases and the 20-140 column sweep, now through activeClaudeComposerText as production uses it. Mutation-checked: never-hard, width ignored, physical baseline, and baseline guard removed each turn a test red. Final round: fixed by the author per the at-most-two-rounds rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reshapes the app tree into per-process + per-feature subtrees, breaks the four inhabited god-files, and moves every cross-domain import onto the five domain aliases. 26 commits, behavior-preserving.
The biggest files are gone:
workspaceStore.tsFeed.tsxTileLeaf.tsxmain/index.tsFeed splits into
ui/markdown/,ui/semantic/,ui/rows/(plus a scaffoldedcontext.tsx,types.ts,scroll.ts,lib/). TileLeaf splits into aTileLeaf/sibling directory with the paste-state-machine helpers, the image-handling helpers, labels, five presentational subcomponents (PaneHeader / QueueStrip / CompactionStrip / PaneToast / ScrollIndicator), four hooks (useComposerAutoGrow / useTypeToFocus / usePromptHistory / useClaudeImagePaste), and the composer itself (ComposerInput + useComposerKeybinds — the 310-line onKeyDown). workspaceStore decomposes into 25 focused sub-hooks underworkspace/hook/; the root file is now a 52-line re-export shell so external callers keep their existing import paths.The repo root is now
{src, packages, vendor}instead of ten loose peers.packages/holds the three in-house submodules (agent-transcript-parser, claude-code-headless, codex-headless);vendor/holds the upstream read-only slabs (codex-src submodule + gitignored claude-code-src).All cross-domain imports go through five aliases —
@main,@preload,@renderer,@shared,@providers— matched in both tsconfigs and electron.vite.config.ts. Zero relative imports remain insrc/. One exception is thepath.joincall in mainWindow.ts that locates the preload bundle on disk (not an import) — the first alias sweep mistakenly rewrote it as a path alias, which shipped a black-screen boot regression before being reverted with a guard comment so the next sweep can't hit the same trap.The branch also fixes three things that surfaced during the refactor. The workspaceStore decomposition silently broke the bootstrap-replay debounce timer because the new refs factory returned a fresh object literal each render; the IPC sub-hook's dep-array cleanup then tore down and re-attached subscriptions on every tick, and
clearTimeoutkilled the timer that flippedbootstrappingback to false — pinning Feed's auto-scroll and lazy-mount behind a stuck flag. Memoizing the refs wrapper with an empty dep array restores stable identity. The providers config bridge — a combinedProviderConfig = RendererProviderConfig & MainProviderConfigtype and the per-providerconfig.tsfiles that implemented it — forced Claude's Node-only session code to sit next to React components, dragging the renderer subtree into the node tsc project; deleting the bridge plus adding explicit tsconfig excludes for renderer halves of the providers took node tsc errors from 731 to 145. And the Phase-5 monorepo move broke the claude-code-headless proxy runtime: the mitmdump lookup candidate list still referenced the oldclaude-code-headless/path, the Python venv shebangs inside.proxy-testing/venv/bin/hardcode their absolute install location (so they ENOENT after a directory move), and mitmdump failing to start surfaced as a 10-second silent CA-wait timeout. The submodule now checkspackages/claude-code-headless/.proxy-testing/…first, shares a single mitmproxy CA confdir across sessions, and throws a real error with mitmdump's stderr tail when the child exits before the CA lands.What's not in scope: the three rendering-duplication bugs (optimistic ghost + committed user turn, Codex
<environment_context>rendering as a user bubble, semantic-turn painting after the rollout commits the same content) — those exist on main too; this branch is deliberately behavior-preserving. Same for the carry-over findings from the earlier code review (ghost restart keyed to transient sessionId, Codex older-history pagination loop, empty-feed WorkIndicator gap, feed-debug write-order, semantic-collapse hiding live output). The optional fat-files sweep (sessionIndex 727, GitRows 610, sessionManager 579, AgentActivityModal 502, PromptSearchModal 442) is also deferred; those are large but coherent, not god-files.