Skip to content

chore: add pastoralist override metadata#1

Open
yowainwright wants to merge 201 commits into
mainfrom
pastoralist/reapply-2026-05-25
Open

chore: add pastoralist override metadata#1
yowainwright wants to merge 201 commits into
mainfrom
pastoralist/reapply-2026-05-25

Conversation

@yowainwright
Copy link
Copy Markdown
Owner

Adds Pastoralist override metadata from a clean current-upstream run.

Metrics from the run:

  • Upstream ref: nimbalyst/nimbalyst@main
  • Override/resolution entries: 4
  • Package manifests scanned: 2144
  • Appendix entries added: 4

Note: this PR targets the yowainwright fork, not the upstream project.

omartelo and others added 30 commits May 11, 2026 16:39
* feat: multi-project rail with keep-warm switching (issue nimbalyst#155)

Add a Discord-style vertical rail that lets a single Electron window host
several workspace projects side-by-side. Switching between them is
instant and keeps inactive projects warm — AI sessions keep streaming,
file watchers keep firing, transcripts keep updating, tabs and panel
layouts are preserved per project. The rail is opt-in via Settings →
Advanced → General → "Multi-project Mode"; the legacy one-window-per-
project flow stays as a fallback.

Architecture
------------
* Renderer state moved off module-level singletons into per-workspace
  atom families (agent layout, navigation history, sidebar / AI-chat
  widths, tabs, collab providers, git status / commits / staging /
  commit-message). Bare exports are read+write proxies that resolve to
  the active workspace's slot, so existing call sites keep working
  unchanged.
* New `openProjects.ts` exposes `multiProjectModeAtom`,
  `openProjectsAtom`, `activeWorkspacePathAtom`, and add/close write
  atoms. State persists to app-settings: `multiProjectMode`,
  `openProjects[]`, `activeProjectPath`.
* New `ProjectRail` component (56px rail, hover-morph icons, badges for
  streaming / unread sessions, X to close, `+` to add via folder
  picker, right-click context menu via `@floating-ui/react` with
  "Open in new window", "Reveal in Finder/Show in Explorer", "Close
  project").
* `App.tsx` mirrors `activeWorkspacePathAtom` into the legacy
  `workspacePath` useState so the existing tree re-renders for the new
  project on a rail switch.
* `Cmd/Ctrl+1..9` activates the Nth rail project; `Cmd/Ctrl+Shift+W`
  closes the active project.

Main process
------------
* `WindowState` extended with `activeWorkspacePath?` and
  `additionalWorkspacePaths?`. Helper `windowReferencesWorkspace` /
  `anyWindowReferencesWorkspace` in `windowState.ts` so service-cleanup
  refcounts cover both the primary path and rail-warm additional paths.
* `serviceRegistry.ts` extracted so the new IPC handlers can register
  and free `ElectronFileSystemService` without importing the whole
  `WindowManager`.
* New `MultiProjectRailHandlers.ts` with three IPC handlers:
  - `workspace:register-additional` — start `DocumentService`,
    `FileSystemService`, `WorkspaceEventBus` subscription, MCP config
    watcher, and navigation history for a path warm in this window.
  - `workspace:unregister-additional` — release services only when no
    other window still references the path.
  - `workspace:set-active` — flip `activeWorkspacePath` without
    spawning a new BrowserWindow.
* `WindowManager.ts` cleanup-on-close considers both primary and
  additional paths. `findWindowByWorkspace` prefers windows where the
  path is the active one and falls back to any reference, so MCP
  routing does not leak across rail projects.
* `WorkspaceWatcher.ts` `stopWorkspaceWatcher` checks additional paths
  before stopping `ProjectFileSync`.

Bug fixes shaken out by dogfooding
----------------------------------
* `setExtensionWorkspacePath` now re-registers MCP tools on every new
  workspace, not just the first non-null assignment.
* `PanelContainer` keys the rendered panel on `workspacePath` so the
  git extension and other panels remount on a rail switch instead of
  caching previous-project state.
* `gitOperations.ts` git status / commits / staging / commit-message /
  is-committing atoms are now per-workspace — no more flash of the
  previous project's git state after a switch.
* `ai-session-state:subscribe` accepts `string | string[]`, and
  `sessionStateListeners.ts` resubscribes with every warm rail path
  whenever `multiProjectModeAtom` / `openProjectsAtom` change. Fixes
  the UI getting stuck on "Thinking…" when a session in an inactive
  rail project completed (its `session:completed` event was being
  filtered out of the previous single-path subscription).

Verification
------------
* `tsc --noEmit` in `packages/electron`: clean.
* `npm run test:unit`: 2077 passing, 26 skipped (baseline preserved).
* Manual: open multiple projects, switch via rail click and Cmd+1..9,
  verify tabs / sidebar widths / agent layout per project, streaming
  sessions keep going for inactive projects, badges show pending work,
  right-click menu opens, settings toggle hides rail.

Refs nimbalyst#155

* fix(rail): drop renderer-side workspace filter on session events

Session lifecycle events (session:streaming/completed/etc.) emitted
from the main process are now subscribed to with the full set of warm
rail paths, so any event that reaches the renderer is intended for
this window. The previous renderer filter still rejected events when
`sessionListWorkspaceAtom` (driven by the visible project) didn't
match the event's workspace, which meant a session that completed
while its project was hidden in the rail never flipped
`sessionProcessingAtom` to false — the UI stayed on "Thinking…" even
after the LLM had finished and chunks had arrived.

Drop the renderer filter; the main-process subscription is already
the authoritative scope.

* fix(rail): drop renderer-side filter on ai:message-logged too

Same pattern as the previous fix. `handleMessageLogged` was guarding
against cross-window leakage by comparing the session's workspaceId
to the visible project's path, dropping reload triggers for sessions
in inactive rail projects. The main-process subscription is now
authoritative on scope, so this filter is redundant and harmful in
multi-project mode.

* fix(rail): include workspacePath in ai:message-logged + batch payloads

When the agent finishes a tool-driven turn (last assistant message
arrives after a tool call) and the user has switched the rail to a
different project, the renderer never reloaded the session's data —
the final assistant text stayed missing until the user manually
cancelled the request, which forced a reload via a different path.

Root cause: `ai:message-logged` and `ai:messages-logged-batch` payloads
did not carry the session's workspacePath. The renderer's resolver
fell back to `sessionListWorkspaceAtom` (the visible project's path),
so `aiLoadSession(sessionId, wrongWorkspacePath)` either no-op'd or
loaded against the wrong project context.

Fix: main now stamps both events with the session's workspacePath
(taken from `effectiveWorkspacePath` for per-row events and from
`SessionStateManager.getSessionState(sessionId)?.workspacePath` for
the batch firehose). The renderer prefers that path, then falls back
to the registry, then to the visible project.

This unblocks the "Thinking…" indicator that was sticking after a
tool-finished turn for sessions in inactive rail projects.

* feat(rail): "restore last session" toggle + recents in add-project menu

Two UX changes for the multi-project rail launch flow:

1. By default the rail no longer rehydrates with every project that was
   warm at last app close. When the user picks a project from the launch
   screen, only that project opens; the rail is empty otherwise. Adding
   more projects is explicit (rail `+` button).

   New app-setting `restorePreviousProjectsOnLaunch` (default `false`)
   restores the previous behavior for users who liked the rehydrate.
   `initOpenProjects` now gates the `openProjects` rehydrate behind that
   flag. Toggle lives next to "Multi-project Mode" in
   Settings → Advanced → General.

2. Clicking the rail `+` button now opens a dropdown anchored to the
   button (via `@floating-ui/react`) with:
     - "Open folder…" — same folder picker as before
     - "Recent projects" list (up to 8) populated from
       `settings:get-recent-projects`, filtered to exclude already-open
       rail projects
   Clicking a recent project registers it via
   `workspace:register-additional` and activates it without spawning a
   new window — same flow as the folder picker, just skips the dialog.

Backend: new IPC handlers `app:get-restore-previous-projects` /
`app:set-restore-previous-projects` plus accessors in `store.ts`.
Renderer: new `restorePreviousProjectsAtom` mirrored to disk, toggle
component, and new state for the add-menu in `ProjectRail`.

* fix(rail): scope watcher + FS service to active path; add test suite

Address Copilot review on PR nimbalyst#188 and add the test coverage the rail
feature lacked:

* `register-additional` no longer starts the workspace watcher and no
  longer flips the runtime-global `FileSystemService`. Both belonged to
  the active path of a window and got clobbered every time another
  project was added to the rail (`OptimizedWorkspaceWatcher.start`
  unconditionally calls `stop(windowId)`; the FS getter is a singleton).
* `unregister-additional` only stops the watcher and clears the FS
  global when the closed path was the active one. Per-workspace
  service teardown is unchanged.
* `set-active` is now the single place that transitions the watcher
  (stop previous, start new) and flips the FS global; idempotent when
  the requested path is already active.
* The renderer dispatches `workspace:set-active` from a single
  subscriber on `activeWorkspacePathAtom`, so close-fallback,
  rail-click and restore-on-launch all flow through one path. Direct
  IPC calls in `ProjectRail.tsx` are removed.

Test coverage (62 new vitest cases):

* `openProjects.test.ts`: add/dedup/cap, close fall-through, derived
  atoms.
* `workspaceLayout.test.ts`: per-path atom-family isolation +
  defaults.
* `windowState.test.ts`: `windowReferencesWorkspace`,
  `anyWindowReferencesWorkspace` (incl. `excludeWindowId`),
  `resolveActiveWorkspacePath` fallback.
* `MultiProjectRailHandlers.test.ts`: register/unregister/set-active
  side effects with regression guards for the watcher + FS-global
  fixes.
* `OptimizedWorkspaceWatcher.test.ts`: lifecycle invariants and
  watched-folder bounds checking.
* `multi-project-rail.spec.ts` (Playwright): rail rendering,
  registering a second project, switching, closing the active project,
  cap-at-8.

* fix(rail): address remaining Copilot review items

UX limitations:
* Cross-workspace activity tracker (`globalSessionActivityAtom`) maintained
  by sessionStateListeners. Rail badges and the close-with-streaming
  confirm now reflect every warm project, not only the currently visible
  one. Replaces the previous `projectActivitySummaryAtom` that iterated
  `sessionRegistryAtom` and missed inactive workspaces.

A11y:
* `ProjectRailIcon` no longer nests a button inside a button. The wrapper
  is a non-interactive `div`; activate and close are sibling buttons. CSS
  splits the avatar styling onto `.project-rail-item-main` while keeping
  the active-bar `::before` on the wrapper.

Reliability:
* `initSharedDocuments` now caches the team-sync provider only after
  `connect()` succeeds. A failed connect destroys the provider instead of
  leaving a dead entry in `providersByPath` that blocked retries.

Memory:
* `closeOpenProjectAtom` clears the workspace's activity slot. A new
  `workspaceStatePruner` subscriber drops every per-workspace atom-family
  entry (tabs slot, sidebar/AI-chat layout, agent-mode layout, navigation
  history, git operations, file-mention search, collab documents) when a
  project is removed from the rail. Per-atom prune helpers live next to
  the families they manage; the coordinator stays in its own module to
  avoid the cycle that openProjects would create with the atom files.

Tests: 14 new vitest cases (sessionActivity reducers, prune helpers).
Total suite: 766 PASS.

* fix(rail): keep team-sync provider warm across rail switches

Multi-project rail re-renders CollabMode whenever the visible
`workspacePath` changes (App.tsx mirrors the rail's active path into the
legacy useState that drives the editor tree). The `useEffect([workspacePath])`
cleanup was destroying the team-sync provider on every switch, contradicting
the keep-warm promise: pending writes were lost and TeamSync had to
reconnect every time the user picked another project.

Provider lifecycle now flows through the rail close path only:
`closeOpenProjectAtom` → `workspaceStatePruner` →
`pruneCollabDocumentsWorkspaceState` already calls `provider.destroy()`
when the project actually leaves the rail.

* feat(rail): tint rail items with per-project accent color

Each rail item now reads its accent from `generateWorkspaceAccentColor(path)`
— the same deterministic hash → HSL helper that drives the colored bar in
the workspace summary header and SessionHistory entries. Result: a project's
rail icon, its workspace header bar, and its session list entries share
one identifying color.

* Inline `--rail-item-accent` CSS variable on the rail-item wrapper.
* Inactive: neutral background, colored initials so each project is
  identifiable at a glance without overwhelming the rail chrome.
* Hover and active: solid accent background + the active-state accent bar
  (`::before`) also picks up the per-project color.

* fix(rail): re-subscribe TabBar when slot switches across workspaces

Multi-project keep-warm moved tab listeners into a per-workspace slot,
but `subscribe`/`getSnapshot` were `useCallback([])` and captured
`slotRef.current` at call time. `useSyncExternalStore` never
re-subscribed across workspace switches, so TabBar stayed bound to the
previous project's listener set: notify on the new slot did not fire
TabManager's callback, freezing activeTabId on the old project even
though the editor and breadcrumb (which read the snapshot imperatively)
followed the active file.

Tie the deps to `slot` so subscribe/getSnapshot identity changes when
the workspace switches, forcing useSyncExternalStore to drop the old
slot's listener and attach to the new one.

* fix(rail): light-mode legibility, title sync in agent mode, richer hover tooltip

ProjectRail.css referenced non-canonical `--nim-fg`, `--nim-fg-muted`,
`--nim-fg-on-accent`, `--nim-accent`, and `--nim-danger` variables that
the theme system never defines. In dark mode the dark fallbacks
(`#e6e6e6`, `#5b8cff`, etc.) happened to look fine; in light mode they
collapsed onto light backgrounds, leaving the context menu items
("Open in new window", "Reveal in Finder", "Close project") essentially
invisible. Switch to the canonical names from UI_PATTERNS.md
(`--nim-text`, `--nim-text-muted`, `--nim-primary`, `--nim-error`) with
light-friendly fallbacks so the rail reads correctly in both themes.

The window-title effect short-circuited in agent mode because an old
comment claimed AgenticPanel set the title itself; nothing does today,
so switching projects via the rail while in agent mode left the title
bar stuck on the previous workspace's name. Drop the early return.

Tooltip now stacks the project name above the absolute path so users
with multiple two-letter-collision projects can disambiguate them on
hover without clicking in.

* fix(rail): keep pending-prompt indicator across streaming chunks

Karl reported (PR nimbalyst#188 review) that interactive prompt indicators
(AskUserQuestion / ExitPlanMode / ToolPermission / GitCommitProposal)
do not surface as the warning "contact_support" icon — sessions stay
on the generic "Thinking…" spinner — once the multi-project rail is
enabled. Reproduces consistently for sessions in inactive rail
projects and intermittently in the active project.

Root cause: `session:streaming` in the renderer's sessionStateListeners
was clearing `sessionHasPendingInteractivePromptAtom` on every event
("AI resumed streaming - no longer waiting for user input"). That is
the wrong signal — `session:streaming` fires for any token chunk the
provider emits, including tail-end chunks that arrive after a tool_use
for a durable prompt. In single-project mode pre-rail, the global
subscription was scoped to the active workspace so streaming events
for warm-but-inactive sessions never reached this window. With the
multi-project rail, the subscription now covers every open project,
so those tail chunks reach the renderer and clear the flag mid-prompt.
Inactive-project transcripts are not mounted, so
refreshPendingPromptsAtom never re-derives the flag from messages —
the indicator stays wrong until the user opens the session.

Fix:

- sessionStateListeners.ts: drop the clear from the `session:streaming`
  case. The pending flag is the responsibility of the explicit
  resolve events (`ai:askUserQuestionAnswered`, `ai:exitPlanModeResolved`,
  `ai:toolPermissionResolved`, `ai:gitCommitProposalResolved`,
  `ai:sessionCancelled`) and the terminal lifecycle events
  (`session:completed/error/interrupted`), which all clear it correctly.

- MessageStreamingHandler.ts + interactiveToolHandlers.ts: stamp
  `workspacePath` on the four prompt event payloads that did not carry
  it (`ai:exitPlanModeConfirm`, `ai:askUserQuestion`,
  `ai:askUserQuestionAnswered`, `ai:toolPermissionResolved`,
  `ai:gitCommitProposal`, `ai:gitCommitProposalResolved`). Aligns with
  the pattern already in `ai:toolPermission` and `ai:message-logged`
  (commit 795e429). Defensive — opens a path for future multi-window
  routing without changing renderer behavior today.

- New unit suite `sessionStateListeners.test.ts` with 15 cases:
  regression guards on `session:streaming` not clearing the pending
  flag, verifies the resolve / terminal lifecycle events still clear
  it, and exercises set/clear semantics for AskUserQuestion,
  ExitPlanMode, ToolPermission, GitCommitProposal.

* fix(rail): close window when the last project is closed

Closing the last project from the rail (icon button, context menu, or
Cmd+Shift+W) left the window stuck on the just-closed project. Add a
`workspace:close-rail-window` IPC and invoke it from the rail's close
paths when no projects remain so the window closes and the app falls
back to its initial project-selection flow.

* fix(rail): prevent stale activeSessionIdAtom across workspace switch

Adding a brand-new project to the multi-project rail (or switching to a
project whose `selectedWorkstreamAtom` is null) used to leak the previous
workspace's session id through the global `activeSessionIdAtom`. The
renderer then sent that stale id with the new workspace's path to
`ai:sendMessage`, and SessionManager rejected it as
`Session <uuid> not found`. The agent panel also kept rendering the
previous workspace's tab and transcript until the user manually
selected a new session.

Centralize the cross-workspace state hygiene in a subscriber on
`activeWorkspacePathAtom` (`attachWorkspaceSwitchCleanup`) attached
from `initOpenProjects`. The subscriber clears the global session id
on every flip; AgentMode's mount effect then repopulates it from the
new workspace's `selectedWorkstreamAtom` when the workspace has a
prior selection. Drop AgentMode's `if (actualActiveSessionId)` guard
so the global also follows transitions to `null`. Pre-warm
`initAgentModeLayout` and `initSessionList` from App.tsx's rail-switch
effect so the new workspace's layout / session registry are loaded
before any reader fires.

Cover the regression with three jotai-only unit tests for
`attachWorkspaceSwitchCleanup` and an E2E scenario that asserts the
agent empty state after switching to a fresh rail entry.

* fix(rail): emit exitPlanMode:resolved so Thinking indicator updates

In multi-project rail mode, denying an ExitPlanMode prompt left the AGENT
panel without the "Thinking..." spinner until the user switched away and
back to the project. Root cause: ExitPlanMode confirmation was missing
the symmetric "resolved" semantic event that AskUserQuestion and
ToolPermission already have. Without it, SessionStateManager kept the
session at 'waiting_for_input' and no session:streaming was emitted on
resume, so subscribed renderer windows never got a fresh transition to
re-assert sessionProcessingAtom for the active session.

ClaudeCodeProvider.resolveExitPlanModeConfirmation now emits
'exitPlanMode:resolved' after deleting the pending entry.
MessageStreamingHandler registers a paired listener that flips the
session status back to running with isStreaming=true, mirroring the
askUserQuestion:answered and toolPermission:resolved handlers.

The existing ai:exitPlanModeResolved IPC send in AIService stays — it
drives renderer prompt teardown and is independent of state-manager
lifecycle. Companion refactor of MessageStreamingHandler's
removeAllListeners idiom is tracked in nimbalyst#225.

* fix(rail): render hover tooltip via FloatingPortal so it escapes overflow clip

The hover tooltip on rail project icons (and the add-project `+` button)
was already in the codebase but never visible: the rail container has
`overflow-x: hidden`, which silently clipped the tooltip span positioned
at `left: 100%` outside the rail. The previous "fix" only adjusted CSS
typography — visibility was still gated by `:hover > .project-rail-tooltip`
on a clipped element. The earlier "richer hover tooltip" change shipped
without the tooltip actually rendering on screen.

Refactor both tooltips to use @floating-ui/react (matches CLAUDE.md's
floating-ui rule):

- ProjectRailIcon and the `+` button each get a dedicated useFloating +
  useHover + useInteractions instance.
- Tooltip body renders inside FloatingPortal so it escapes the rail's
  `overflow: hidden`. Position is owned by floating-ui (`offset(12) +
  flip + shift`) instead of CSS `position: absolute; left: 100%`.
- 200ms open delay so quick mouse passes don't flash the tooltip.

CSS keeps appearance rules only; positioning, opacity transitions, and
the parent-hover gate are removed. Box shadow added so the portaled
tooltip reads as floating UI rather than inline content.

* fix(rail): address PR review concerns 1-3 (fileTools routing, e2e hot-path, transient null)

Three pre-merge concerns from the architectural review:

1. **fileTools dispatch routes to the wrong workspace's FileSystemService.**
   `runtime/ai/tools/fileTools.ts` (searchFiles, listFiles, readFile)
   used the no-arg `getFileSystemService()` global, which the rail flips
   to the visible workspace on switch. A session running in an inactive
   rail project would dispatch its file tools through whichever project
   was on top, leaking writes/reads across workspaces. Add a per-path
   registry (`setFileSystemServiceFor` / `getFileSystemServiceFor` /
   `clearFileSystemServiceFor`) in `runtime/core/FileSystemService.ts`,
   widen `ToolDefinition.handler` to receive a `ToolContext` carrying
   `workspacePath`, and have the electron-side ToolExecutor pass its
   `workspaceId` through. WindowManager and MultiProjectRailHandlers
   mirror their existing electron-side `fileSystemServices` map into
   the new runtime registry on register / unregister. fileTools
   resolves via the per-path map first and falls back to the global
   only when the dispatcher has no workspace context (preserves
   single-project legacy callers).

2. **E2E suite never exercises the in-process rail switch.** Existing
   tests in `multi-project-rail.spec.ts` reload the renderer between
   assertions, so they validate the IPC + boot cycle but not the hot
   path users actually hit. Add `rail click updates workspace context
   in-process (no reload)` — clicks between two rail icons and asserts
   `.workspace-summary-header-path` follows the click, plus the agent
   panel renders its empty state for the newly active workspace,
   without any `page.reload()`.

3. **`activeSessionIdAtom` transient-null window on rail switch.**
   `attachWorkspaceSwitchCleanup` previously cleared the global atom
   and left AgentMode's mount effect to repopulate it. Between the
   subscriber firing and the React commit, any reader (notification
   listeners, voice mode, VoiceModeButton) saw `null`. Rewrite the
   subscriber to derive the new value synchronously by reading
   `selectedWorkstreamAtom(newPath)` and `workstreamActiveChildAtom`,
   matching AgentMode's own derivation in
   `AgentMode.tsx:151-158`. AgentMode's later mount-effect write
   converges on the same value. Adds two new unit tests in
   `openProjects.test.ts`.

All three concerns are addressed without expanding global state:
`activeSessionIdAtom` stays as a single global (atomFamily promotion
deferred — see PR follow-up note); workspace-scoped registries are the
new primitive only where they actually fix a routing bug.

* test(rail): mock new runtime per-path FS exports in MultiProjectRailHandlers test

Adds setFileSystemServiceFor / clearFileSystemServiceFor to the
@nimbalyst/runtime vi.mock so the existing 13 regression tests keep
passing after the multi-project rail per-workspace dispatch fix
introduced those exports.

---------

Co-authored-by: marcelo-filho_snk <marcelo.filho@sankhya.com.br>
…enderDirectoryNode (nimbalyst#235)

* fix(commit-widget): sort files and subdirectories alphabetically in renderDirectoryNode

Files inside a directory rendered in the order the model emitted them in
filesToStage, which made it hard to scan a commit with many files in one
folder (the issue example: .claude/commands/ rendered analyze-code.md,
roadmap.md, bug-report.md, design.md, posthog-analysis.md instead of
alphabetical order).

Sort subdirectories by displayPath and files by basename inside
renderDirectoryNode. Folders-before-files convention is preserved by the
existing render order. Used at both render sites (root + expanded
subdirectory) so the tree renders deterministically.

Fixes nimbalyst#233.

* test(commit-widget): extract sort comparators and add helper tests

Extract the two inline sort callbacks from renderDirectoryNode into
named module-level exports (compareSubdirectoriesByDisplayPath and
compareFilesByBasename) and add a focused helper test that covers the
issue nimbalyst#233 example, basename-vs-full-path semantics, root-level paths,
and collapsed compound displayPath nodes.

Also export the DirectoryNode interface so the test can build
fixture nodes without re-declaring the shape.

Corrects my prior claim that vitest + jsdom test infrastructure
was not in place: it is (vitest.config.ts environment: 'jsdom',
jsdom@26.1.0 in package.json, sibling __tests__/TranscriptWidgets.test.tsx).

---------

Co-authored-by: Andrew Demczuk <5212682+ademczuk@users.noreply.github.com>
When a session running inside a worktree called spawn_session, the new
child was created with worktree_id=null and ran in the project root.
Edits the child made landed in the wrong tree -- the caller looked like
it was working in the worktree, but its spawned helpers were silently
mutating main.

- Read parent.worktreeId in MetaAgentService.spawnSession and pass it
  through as the child's worktreeId when the caller did not request a
  brand-new worktree (useWorktree=true)
- Update the spawn_session MCP tool description so the meta-agent
  understands the default is "inherit caller's working directory" and
  useWorktree=true is "create a NEW worktree"
- Mirror the same wording in the /launch-new-session command doc

Fixes nimbalyst#229
Refs nimbalyst#37
A Codex orchestrator session running in the parent project (or in
one worktree) could not write to sibling `<project>_worktrees/<name>`
checkouts, and `git rebase --continue` failed from inside a worktree
because the shared `.git` common dir sat outside the sandbox. The SDK
supports `additionalDirectories`, but Nimbalyst never populated it,
and the workspace permissions UI only feeds Claude Code -- so the
spawned `codex` CLI never received any `--add-dir` flags.

- Wire `OpenAICodexProvider.setAdditionalDirectoriesLoader` mirroring
  the existing Claude Code path; both providers now share one loader.
- Forward `raw.additionalDirectories` through `CodexSDKProtocol` so
  the SDK passes them to the CLI.
- Extend `getAdditionalDirectoriesForWorkspace` to enumerate sibling
  worktrees from the filesystem and include the parent project root
  when the cwd is itself a worktree.

Fixes nimbalyst#230
- route worktree session state through the parent workspace
- restore running and waiting indicators for launched siblings

fixes nimbalyst#231

# Conflicts:
#	packages/electron/src/main/ipc/SessionStateHandlers.ts
#	packages/electron/src/renderer/store/sessionStateListeners.ts
…ions

- derive command descriptions from markdown body text when frontmatter is missing
- cover Codex skill export so generated workflows stay triggerable
- add explicit descriptions to project Claude command files
- cover prompt/export behavior so generated Codex skills stay triggerable
The slash typeahead inserted bare skill names like /excalidraw while
the Claude Agent SDK registers plugin skills as plugin-name:skill-name,
so the inserted command did not match anything the SDK could route.
Issue nimbalyst#234.

- Apply the plugin namespace to skills as well as commands in
  AgentWorkflowService.toDescriptor so the typeahead inserts the real
  wire name (no after-the-fact prompt rewriting).
- Drop the cosmetic nimbalyst- prefix from the eight bundled
  plugin.json names so wire names are short (/excalidraw:excalidraw,
  /planning:design, /feedback:bug-report, etc.).
- Add a plugin.json to the automations extension so its namespace is
  "automations" instead of the directory-basename fallback.
- Resolve cmd+skill name collisions: deleted the redundant excalidraw
  command (skill is canonical) and renamed the feedback bug-report
  skill to feedback-intake so it no longer clashes with the
  bug-report command.
- Update FeedbackIntakeDialog and the /implement mode interceptors to
  emit and match the new names; the implement regex still accepts the
  legacy /nimbalyst-planning:implement form so old session draft
  buffers keep mode-switching behavior.
- Add POST /admin/cleanup-do that enumerates a DO namespace via the
  Cloudflare management API and purges instances flagged as orphaned,
  stale, or carrying residual storage. Gated by Cloudflare Access:
  the worker validates Cf-Access-Jwt-Assertion against the team JWKS
  and the per-application AUD with no shared bearer fallback, so
  removing Access disables the endpoint instead of falling back to
  a static secret.
- Add scripts/cleanup-orphan-dos.mjs driver that threads the cursor
  across worker invocations until done. Auths via an Access service
  token, intended to run under `op run` so credentials never live
  in shell env.
- Switch handleDeleteAccount to state.storage.deleteAll() across
  SessionRoom, IndexRoom, ProjectSyncRoom, and TeamRoom. The prior
  per-table DELETE FROM hit the DO storage-operation timeout on
  large rooms, resetting the DO mid-delete and stranding partial
  state. TrackerRoom and DocumentRoom were already correct.
- Parallelize per-DO probes/purges (concurrency=25) and floor the
  CF API page-size to satisfy the list-objects minimum.
A worktree IS the workstream -- the worktrees row is the container and
every session inside it is a flat sibling keyed by worktree_id. Older
spawn_session and convert-to-workstream paths instead minted a
session_type='workstream' row inside the worktree, producing a forbidden
three-layer hierarchy (worktree -> workstream -> session) that hid the
workstream's children from the left pane: they got filtered out of
sessionListRootAtom by parent_session_id != NULL but never re-surfaced
in worktreeGroupsData (which only sees root rows). Users saw "4
sessions" in a worktree that actually held 10.

- resolveOrCreateWorkstream now hard-returns null when the parent is in
  a worktree, so the new child lands as a flat sibling.
- convertToWorkstreamAtom refuses to run on a session with worktreeId
  and no longer sets worktreeId on the workstream row it creates.
- One-time migration deletes accidental worktree-attached workstream
  rows (guarded by NOT EXISTS on ai_agent_messages so any row with
  real content is left alone). Children auto-unparent via the FK's
  ON DELETE SET NULL.
- New docs/SESSION_HIERARCHY.md documents the two-layer invariant,
  legal (session_type, parent_session_id, worktree_id) combinations,
  and the code paths that enforce them. Linked from CLAUDE.md and
  cross-referenced from DATABASE_SCHEMA.md.
Implicit any on the onLaunch destructure broke typecheck after the
multi-project rail merge widened the surrounding callable graph.
Import FeedbackIntakeLaunchOptions and apply it to the callback.
- Multi-project rail with keep-warm switching. Opt-in via Settings > Advanced > General > "Multi-project Mode" adds a Discord-style vertical rail to the left of the window that lets a single Electron instance host several workspace projects side-by-side. Switching between them is instant and keeps inactive projects warm: AI sessions keep streaming, file watchers keep firing, transcripts keep updating, tabs / panel layouts / agent layouts / git status are preserved per project. Renderer state moved off module-level singletons into per-workspace atom families (agent layout, navigation history, sidebar / AI-chat widths, tabs, collab providers, git status / commits / staging / commit-message). `Cmd/Ctrl+1..9` activates the Nth rail project; `Cmd/Ctrl+Shift+W` closes the active project. Right-click on a rail icon offers "Open in new window", "Reveal in Finder/Show in Explorer", and "Close project". Main-process `serviceRegistry` reference-counts `ElectronFileSystemService` / `DocumentService` / MCP config watcher per warm path, so `findWindowByWorkspace` prefers the active rail project and falls back to any reference (no MCP routing leaks across rail projects). `ai-session-state:subscribe` now accepts `string | string[]` and resubscribes to every warm path so sessions completing in inactive rail projects no longer leave the UI stuck on "Thinking...". Legacy one-window-per-project flow stays as a fallback. Refs nimbalyst#155.
- Streamlined feedback intake flow with separate bug-report and feature-request entry points and a new dialog mockup. The `/feedback:bug-report` and `/feedback:feature-request` slash skills now route through a single `FeedbackIntakeDialog` that gathers reproduction steps, expected/actual behavior, and environment info before posting to the public Nimbalyst GitHub repo via the existing `feedback_open_github_issue` MCP tool.
- Orphan Durable Object cleanup endpoint and driver in collabv3. `POST /admin/cleanup-do` enumerates a DO namespace via the Cloudflare management API and purges instances flagged as orphaned, stale, or carrying residual storage. The endpoint is gated by Cloudflare Access: the worker validates `Cf-Access-Jwt-Assertion` against the team JWKS and the per-application AUD with no shared bearer fallback, so removing Access disables the endpoint instead of falling back to a static secret. New `scripts/cleanup-orphan-dos.mjs` driver threads the cursor across worker invocations until done; auths via an Access service token, intended to run under `op run` so credentials never live in shell env. Also switches `handleDeleteAccount` to `state.storage.deleteAll()` across SessionRoom, IndexRoom, ProjectSyncRoom, and TeamRoom (per-table `DELETE FROM` was hitting the DO storage-operation timeout on large rooms and stranding partial state). Parallelizes per-DO probes/purges (concurrency=25) and floors the CF API page-size to satisfy the list-objects minimum.
- Stop creating workstream rows inside worktrees. A worktree IS the workstream -- the worktrees row is the container and every session inside it is a flat sibling keyed by `worktree_id`. Older `spawn_session` and convert-to-workstream paths instead minted a `session_type='workstream'` row inside the worktree, producing a forbidden three-layer hierarchy (worktree -> workstream -> session) that hid the workstream's children from the left pane: they got filtered out of `sessionListRootAtom` by `parent_session_id != NULL` but never re-surfaced in `worktreeGroupsData` (which only sees root rows). Users saw "4 sessions" in a worktree that actually held 10. `resolveOrCreateWorkstream` now hard-returns `null` when the parent is in a worktree, `convertToWorkstreamAtom` refuses to run on a session with `worktreeId`, and a one-time migration deletes accidental worktree-attached workstream rows (guarded by `NOT EXISTS` on `ai_agent_messages` so any row with real content is left alone; children auto-unparent via the FK's `ON DELETE SET NULL`). New `docs/SESSION_HIERARCHY.md` documents the two-layer invariant and the legal `(session_type, parent_session_id, worktree_id)` combinations.
- Worktree child sessions now stay in sync with the parent workspace. Worktree session state was routed through the worktree instead of the parent workspace, so running and waiting indicators went missing for launched siblings. State now flows through the parent workspace and indicators are restored. Fixes nimbalyst#231.
- Codex `workspace-write` sandbox can now reach sibling worktrees. A Codex orchestrator session running in the parent project (or in one worktree) could not write to sibling `<project>_worktrees/<name>` checkouts, and `git rebase --continue` failed from inside a worktree because the shared `.git` common dir sat outside the sandbox. The SDK supported `additionalDirectories`, but Nimbalyst never populated it, and the workspace permissions UI only fed Claude Code -- so the spawned `codex` CLI never received any `--add-dir` flags. `OpenAICodexProvider.setAdditionalDirectoriesLoader` now mirrors the Claude Code path (both providers share one loader), `CodexSDKProtocol` forwards `raw.additionalDirectories` through to the SDK, and `getAdditionalDirectoriesForWorkspace` enumerates sibling worktrees from the filesystem and includes the parent project root when the cwd is itself a worktree. Fixes nimbalyst#230.
- `spawn_session` now inherits the caller's worktree. When a session running inside a worktree called `spawn_session`, the new child was created with `worktree_id=null` and ran in the project root -- edits the child made landed in the wrong tree, and the caller looked like it was working in the worktree while its spawned helpers were silently mutating main. `MetaAgentService.spawnSession` now reads `parent.worktreeId` and passes it through as the child's `worktreeId` when the caller did not request `useWorktree=true`. The `spawn_session` MCP tool description and `/launch-new-session` command doc now say the default is "inherit caller's working directory" and `useWorktree=true` is "create a NEW worktree". Fixes nimbalyst#229. Refs nimbalyst#37.
- Resolve diff peek paths for worktree files. The diff-peek viewer was building paths relative to the parent project root for files that lived inside a sibling worktree, so opening a peek on a worktree-only edit pointed at a nonexistent path in the parent. Paths now resolve against the worktree's own checkout.
- Namespace plugin slash skills consistently with commands. The slash typeahead inserted bare skill names like `/excalidraw` while the Claude Agent SDK registers plugin skills as `plugin-name:skill-name`, so the inserted command did not match anything the SDK could route. `AgentWorkflowService.toDescriptor` now applies the plugin namespace to skills as well as commands (no after-the-fact prompt rewriting). Drops the cosmetic `nimbalyst-` prefix from the eight bundled `plugin.json` names so wire names are short (`/excalidraw:excalidraw`, `/planning:design`, `/feedback:bug-report`, etc.). Resolves cmd+skill name collisions: deletes the redundant `excalidraw` command (skill is canonical) and renames the feedback `bug-report` skill to `feedback-intake`. The `/implement` mode regex still accepts the legacy `/nimbalyst-planning:implement` form so old session draft buffers keep mode-switching behavior. Fixes nimbalyst#234.
- Preserve Codex command instructions when frontmatter is missing a description. The Codex skill export was dropping command bodies when no frontmatter description was set; descriptions now derive from the markdown body text so generated workflows stay triggerable. Companion fix adds explicit descriptions to project Claude command files to cover prompt/export behavior for generated Codex skills.
- Annotate `FeedbackIntakeDialog` `onLaunch` parameter to fix a typecheck regression that surfaced after the multi-project rail merge widened the surrounding callable graph. The implicit `any` on the destructured callback now uses the imported `FeedbackIntakeLaunchOptions` type.
- Commit proposal widget: sort files alphabetically by basename and subdirectories by displayPath within each directory node. Files inside a directory used to render in the order the model emitted them in `filesToStage`, which made it hard to scan a commit with many files in one folder. Folders-before-files convention is preserved. Fixes nimbalyst#233.
…imbalyst#118 (nimbalyst#222)

The original showToolCalls field is gated dev-mode-only (default false,
toggle hidden behind isDevelopment in AgentFeaturesPanel). Naively making
the chat view honour it would silently hide tool rows for every production
user after upgrade, since their stored value is the default false.

Add a separate user-facing chatShowToolCalls boolean (default true) that
the chat transcript routes through. Wire it through the main-process
electron-store schema, the renderer AIDebugSettings atom, the IPC
get/save/getEffectiveSettings handlers, and a new toggle in the Agent
Features panel outside the isDevelopment block.

SessionTranscript reads chatShowToolCallsAtom and passes it as
initialSettings.showToolCalls into RichTranscriptView. The renderer
guards the two previously-unguarded paths that call renderToolCard:
- Orphan tool cards (tool ran but no following assistant message)
- toolMessagesBefore cards rendered alongside the next assistant turn

Interactive widgets (ToolPermission, ExitPlanMode, AskUserQuestion,
GitCommitProposal) always render so the user can act on prompts even
with tool rows hidden. INTERACTIVE_WIDGET_TOOLS is lifted from inline
to module scope and reused at all three guard sites.

Defaults preserve UX: every production user who hasn't manually set
chatShowToolCalls keeps seeing tool rows. Power users who set false
in ai-settings.json (the reporter on nimbalyst#118) get tool rows hidden.

Verified: tsc --noEmit clean across runtime + electron, 57 widget tests
pass. Pre-existing dnd-kit module resolution errors are unrelated.

Fixes nimbalyst#118.

Co-authored-by: Andrew Demczuk <5212682+ademczuk@users.noreply.github.com>
…ed (nimbalyst#255)

The renderer's gitCommit host in SessionTranscript.tsx was sending
action='cancelled' for every result.success===false outcome, including
hook rejection, no-staged-changes errors, and IPC throws. The widget's
completedState reader short-circuits on action='cancelled' before
reaching the error check, so users saw the cancelled state instead of
the actual failure reason.

Changes:
- SessionTranscript.tsx: send action='error' (with the underlying error
  string) when result.success is false or the IPC throws. action='cancelled'
  is now reserved for the explicit user-cancel path.
- GitCommitConfirmationWidget.tsx: add handler for action='error' and
  action='failed' so the widget renders the error state and surfaces the
  error message to the user.
- interactiveToolHandlers.ts: widen the CommitResult action type from
  "committed" | "cancelled" to also include "error".

This is the renderer-side partial fix from the three-site fix scope
discussed in nimbalyst#202. The auto-commit handler outer catch and the simple-git
error-detection rewrite are separate concerns and will be follow-ups once
the maintainer signals scope.

Refs nimbalyst#202

Co-authored-by: Andrew Demczuk <5212682+ademczuk@users.noreply.github.com>
…imbalyst#256)

The marketplace install handler at ExtensionMarketplaceHandlers.ts:370
was logging an info-level message when an installed-from-GitHub repo
did not include a built dist/ directory, then falling through to
addMarketplaceInstall + notifyExtensionsChanged and returning
{ success: true, extensionId } anyway. The user saw a success toast,
but the extension loader later failed silently when it tried to import
manifest.main and the file did not exist.

The change returns { success: false, error: "..." } with a clear
explanation that the repo must be built locally first (the message
distinguishes the "has package.json, run npm install + npm run build"
case from the "no package.json at all, repo may be malformed" case).
It also cleans up the partially-installed extension directory so the
user can retry from a fresh state.

Auto-building on install is intentionally deferred (slow, error-prone,
runs arbitrary npm scripts) and is out of scope here. This PR is the
surface-the-error path; auto-build can be a follow-up once direction
is confirmed.

Closes nimbalyst#247

Co-authored-by: Andrew Demczuk <5212682+ademczuk@users.noreply.github.com>
Co-authored-by: Greg Hinkle <greghinkle@gmail.com>
* refactor(markdown): upgrade Lexical to 0.44 and de-fork markdown import

Stop forking @lexical/markdown's importer. Round real Nimbalyst plan
documents through upstream $convertFromMarkdownString without losing
emphasis spans by encoding literal `*` and `_` in non-code text as HTML
numeric character references (&nimbalyst#42;, &nimbalyst#95;) instead of backslash escapes.
NCRs are inert to upstream's CommonMark emphasis scanner -- which
classifies `\` as non-punctuation and so dropped escape-flanked emphasis
on re-import -- and unescape back to literal characters.

- Upgrade lexical / @lexical/* to 0.44.0 across the runtime package
- Route $convertFromEnhancedMarkdownString through upstream import,
  with a small pass that collapses upstream TabNodes back to plain
  text so the diff plugin's tree matcher keeps working
- Always use the custom export traversal so the NCR encoding applies
  to full-document export, not only to diff/single-node paths
- Delete LexicalMarkdownImport.ts and the three forked text-transformer
  helper files; delete the orphaned utils.ts copy of upstream's
  pre-extension MarkdownCriteria system
- Slim MarkdownTransformers.ts: re-export upstream text-format
  constants and Transformer types verbatim; keep HEADING/QUOTE/CODE
  /LINK local because of Nimbalyst-specific tweaks
- Keep ListTransformers.ts as the 2-space house-style fork until
  upstream makes LIST_INDENT_SIZE configurable
- Add a round-trip corpus test covering the tracker-integration-strategy
  excerpt that motivated this work
- Add @lexical/extension and a pluginPackageToExtension adapter
  (+tests, README) as scaffolding for future incremental migration to
  LexicalExtensionComposer; no plugin migrations land here
- Block direct \$convertFromMarkdownString / \$convertToMarkdownString
  imports in the electron renderer via no-restricted-imports; rewrite
  FORKED_MARKDOWN_IMPORT.md and the markdown README around the
  Enhanced wrappers as the supported entry points

* refactor(editor): adopt LexicalExtensionComposer, open extension API

- Switch the editor shell from LexicalComposer to LexicalExtensionComposer;
  root extension lives in editor/extensions/NimbalystEditorExtensions.ts
- Replace 8 React-mounted plugins with upstream Lexical extensions: Clear,
  HorizontalRule, TabIndentation, List, CheckList, History (omitted in
  collab mode), Link, plus the HorizontalRuleNode class swap to the
  canonical @lexical/extension class
- Move 7 Nimbalyst-internal headless plugins into editor/extensions/
  builtin/: AutoLink (fork with the base64/data-URL filter),
  CollabAssetLink, AssetGc, MarkdownPaste, MarkdownCopy, DragDropPaste,
  TabFocus. Each was previously a React component mounted purely to
  register commands or listeners
- Extensions can now ship Lexical plugins: new
  contributions.lexicalExtensions manifest field + module.lexicalExtensions
  record, ExtensionLoader.getLexicalExtensions(), runtime store
  (extensionLexicalExtensionsStore) wired through ExtensionPluginBridge.
  NimbalystEditor reads the store and includes contributions in the root
  extension's dependencies; toggling an extension rebuilds the editor
- Drop dead code: CommentPlugin (was commented out in Editor.tsx) and the
  old in-editor SearchReplacePlugin (the active fixed-tab-header
  SearchReplace at runtime/plugins/SearchReplace stays)
- ESLint guardrails forbid re-importing retired @lexical/react/Lexical*
  Plugin paths (History, List, CheckList, TabIndentation, HorizontalRule,
  ClearEditor, Link) and the React HorizontalRuleNode subclass
- Integration test exercises the extension-contributed lexical-extension
  API end-to-end (publish/subscribe semantics, register() against a live
  editor, configExtension overrides at the contribution boundary)

Phases 7.1-7.3, 7.6, and partial 7.7 of the lexical-upgrade-and-defork
plan. Remaining: 7.4 (split UI plugins), 7.5 (retire PluginPackage /
PluginRegistry / PluginManager), 7.7 documentation updates.

* refactor(editor): retire PluginPackage/PluginRegistry/PluginManager

Every built-in editor plugin (Mermaid, Images, PageBreak, Collapsible,
Layout, Kanban, Diff, Table, Emoji) is now a LexicalExtension declared in
NimbalystEditorExtensions.ts. The Nimbalyst-specific plugin system
(PluginPackage, PluginRegistry, PluginManager, registerBuiltinPlugins,
pluginPackageToExtension, ExtensionCommandsPlugin) is deleted.

- New extensionContributionsStore for userCommands, markdown transformers,
  and dynamic options previously carried by pluginRegistry
- New extensionEditorComponentsStore for React UI surfaces that must mount
  inside LexicalExtensionComposer (replaces PluginManager)
- extensionLexicalExtensionsStore upgraded to per-source keyed entries so
  tracker, mockup, document-link can publish independently of the loader
- Four electron register*Plugin files rewritten on the new APIs;
  trackerPluginPackage replaced by TrackerLexicalExtension +
  TRACKER_USER_COMMANDS; MockupPlugin () => null replaced by
  MockupLexicalExtension
- ComponentPicker, getEditorTransformers, ExtensionPluginBridge rewired
  onto the new stores
- ESLint guard added to block pluginRegistry/PluginPackage/PluginManager
  imports from @nimbalyst/runtime
- Docs: rewrote editor/extensions/README.md to describe the live shell;
  added Contributing Lexical Extensions section to EXTENSION_ARCHITECTURE

* test(editor): lock in extension contract and node-set snapshot

Two regression guards for the post-Phase-7 editor shell, plus a stale
architecture doc removed.

- extensionContract.test.ts: a fixture extension publishes a node, a
  markdown transformer, and a slash-picker entry through the runtime
  contribution APIs; the test walks the full path that on-disk Nimbalyst
  extensions rely on. Breaking this trips the SDK contract.
- editorNodeSnapshot.test.ts: asserts the exact set of node types
  registered on the editor in standalone and collaboration modes.
  Catches accidental drops from the dependency graph that would only
  surface at runtime as "unknown node type" import errors.
- PLUGIN_ANALYSIS.md: pre-Phase-7 design note describing an architecture
  that no longer exists. The live shell is documented under
  editor/extensions/README.md.

* fix(editor): track in-tree SelectionAlwaysOnDisplayPlugin

The runtime imports `SelectionAlwaysOnDisplay` from
`plugins/SelectionAlwaysOnDisplayPlugin/`, but the file was never
checked in. Cold-clone or CI builds would fail with a missing-module
error. This is the in-tree replacement for upstream's
`@lexical/react/LexicalSelectionAlwaysOnDisplay` -- needed because
upstream's `markSelection` calls `editorState.read(cb)` without binding
an active editor, which throws "Unable to find an active editor" in
0.44 when a selection endpoint is on an empty paragraph.

* fix(markdown): preserve emphasis across whitespace-only text nodes

Triple-nested formatting spans (e.g. ~~strike *italic **bold** text*
inside~~) corrupted on export when the diff plugin left a
whitespace-only text node between formatted siblings. The opening-tag
continuity check used a helper that filtered whitespace-only siblings
as if they had no format, causing the exporter to re-open already-open
tags and produce duplicate unclosedTags entries; the close loop then
popped extras, emitting stray ** and ~~ around the trailing run.

- Use raw format-bit continuity (hasTextFormat) for the opening-marker
  check; whitespace flanking is still handled per-node via the
  existing isWhitespaceOnly branch
- Remove the now-unused shouldTrackAsFormattedSibling helper
- Switch the triple-nest coverage test to the enhanced exporter
  (production path); upstream's $convertToMarkdownString carries the
  same bug but is not used in production

* fix: dark mode logo issue

* fix(agent-mode): conflict dialogs overflowed short viewports

- cap dialog at viewport height with internal body scroll so the
  header and Close/Resolve buttons stay reachable
- widen max-width from 520px to 760px so the conflict file list
  and two-column commit grid have room to breathe

* fix(editor): restore collaboration context for Lexical 0.44

Lexical 0.44 removed the implicit global CollaborationContext;
useCollaborationContext now throws when no LexicalCollaboration
provider is in the tree, breaking every collab document load.
Wrap the editor tree with LexicalCollaboration so CollaborationPlugin
can resolve the context.

* test(e2e): repair editor specs to match current behavior

- image-paste: assert assets land in <docDir>/assets/ (the path
  introduced in nimbalyst#146) and that ImageComponent resolves through the
  nim-asset:// protocol; dedup check counts unique blue-rect SVGs
  so siblings tests don't poison the total
- history: restore persists through saveFile, so the tab is not
  dirty afterwards -- poll the on-disk content for the restored
  marker instead of expecting a dirty indicator
- autosave-deleted-file-recreate: raise describe timeout to 25s
  so the deliberate 13s wait past the recentlyDeletedFiles TTL
  fits the budget
- files.spec debounce: drop the brittle mtime equality during a
  rapid-typing burst (per repo guidance to prefer content-based
  assertions); verify final on-disk content reflects every keystroke
- files.spec tree-scroll/breadcrumb: fix wrong openFileFromTree
  paths (target.md -> zzz-deep/target.md, app.ts -> src/app.ts),
  drop directory-toggle clicks that collapsed state left by an
  earlier test, and use an exact regex so filter-app.ts no longer
  shadows app.ts
- excalidraw: migrate off the legacy per-extension window globals
  (window.__excalidraw_getEditorAPI) to the registry-backed
  __testHelpers.getExtensionEditorAPI exposed from App.tsx
- theme: rename a duplicate `const editor` declaration that was a
  syntax error blocking the entire suite from discovering tests

* fix(editor): scroll to in-document headings for #anchor links

Reimplementation of nimbalyst#254 due to incompatible Lexical upgrade
(Thanks @ademczuk)

- Add HeadingAnchorExtension that assigns GitHub-style slug ids to
  rendered heading elements via a HeadingNode mutation listener,
  scoped to each editor's root so multiple open files stay isolated
- Extend the global link-click handler in App.tsx with a #fragment
  branch that resolves the id inside the active .editor-scroller and
  smooth-scrolls to it, falling back to default behaviour on no match
- Add slugify helper plus 12 unit tests covering punctuation,
  numbers, unicode letters, and duplicate handling

Fixes nimbalyst#248
TrackerSchemaService.refreshWorkspaceSchemasIfCurrent bailed out
whenever currentWorkspacePath was null, so custom tracker types defined
in .nimbalyst/trackers/ were never registered in memory — only the
CLI-arg workspace open path ever called updateTrackerSchemaWorkspace.

- Fix null guard in refreshWorkspaceSchemasIfCurrent so it loads when
  no workspace has been set yet (covers tracker_define_type called
  before any window opens)
- Call updateTrackerSchemaWorkspace in session restore (SessionState.ts)
- Call updateTrackerSchemaWorkspace in UI workspace open
  (WorkspaceManagerWindow.ts)
- Call updateTrackerSchemaWorkspace in file-open workspace creation
  paths (index.ts)
Add a regression test that streams a multi-block markdown body through
MarkdownStreamProcessor in 5-character chunks and asserts the exported
result matches a one-shot import of the same source. Locks in the
streaming path now that $convertFromEnhancedMarkdownString routes
through upstream's importer in Lexical 0.44.
"Restart and Install" left no workspaces open after the relaunch
because performQuitAndInstall removed the before-quit handler that
saves session state, and the resulting window-close cascade then
overwrote the persisted list with `{ windows: [] }`.

Save session state and mark the app as restarting (so window-close
handlers skip their own save) before tearing listeners down,
mirroring the MCP restart path.

Fixes nimbalyst#232
- ios-transcript-tests: drop packages/runtime/** from the PR paths
  filter so the macos-15 simulator job (10x billed minutes) only
  runs on PRs that touch packages/ios/** directly. Push-to-main
  still runs the full job for both ios/** and runtime/** changes.
- ci: add a Transcript Bundle job that builds the iOS transcript
  vite bundle on Ubuntu, covering runtime-only PRs at ~1/100 the
  cost of the macOS job.
- ci: cache node_modules keyed on package-lock.json hash to
  short-circuit `npm ci` (~70s/job) on warm cache.
- ci: cache packages/runtime/dist keyed on runtime + extension-sdk
  sources so typecheck skips the ~46s vite build when sources are
  unchanged.
…#265)

The GitCommitConfirmationWidget did not appear in the transcript
until the user cancelled the AI input. The SDK's assistant chunk
carrying the developer_git_commit_proposal tool_use sat in the
AgentMessageWriteQueue buffer, and once the MCP handler blocked
waiting for the user's response no further chunks arrived to
drive the queue's 200ms idle flush -- so the canonical
tool_call_started event was never produced.

- interactiveToolHandlers: write an awaited synthetic
  nimbalyst_tool_use row keyed by the SDK's toolUseId before
  notifying the renderer, mirroring the AskUserQuestion pattern
- ClaudeCodeRawParser: make parseToolUse async and add a
  findByProviderToolCallId cross-batch dedup check so the SDK's
  later assistant tool_use chunk does not create a duplicate
  canonical event

Fixes nimbalyst#265
- Honour a new user-facing `chatShowToolCalls` setting in `ai-settings.json` (default `true`) so the AI chat view can hide tool-call rows entirely instead of just collapsing them. The original `showToolCalls` field stays a developer-mode-only toggle (default `false`, gated behind `isDevelopment` in `AgentFeaturesPanel`), so production users get a discoverable "Show Tool Calls in Chat" toggle in Agent Features that defaults to on. The renderer's tool-row guards in `RichTranscriptView` (orphan-tool path and `toolMessagesBefore` path) consume the new value flowing through `initialSettings.showToolCalls`. Interactive tool widgets (`ToolPermission`, `ExitPlanMode`, `AskUserQuestion`, `GitCommitProposal`) always render so the user can still act on prompts even when tool rows are hidden. Default `true` preserves UX for everyone who has not manually set the value. Fixes nimbalyst#118.
- Open the editor extension API: extensions can now ship Lexical extensions via the new `contributions.lexicalExtensions` manifest field plus a `module.lexicalExtensions` record. The editor shell switched from `LexicalComposer` to `LexicalExtensionComposer`, the root extension lives in `editor/extensions/NimbalystEditorExtensions.ts`, and `ExtensionPluginBridge` wires per-source contributions through `extensionLexicalExtensionsStore`. Toggling an extension rebuilds the editor so the new in-process plugins register their nodes, transformers, and commands. The legacy `PluginPackage` / `PluginRegistry` / `PluginManager` system is retired: every bundled built-in (Mermaid, Images, PageBreak, Collapsible, Layout, Kanban, Diff, Table, Emoji, AutoLink, MarkdownPaste/Copy, DragDropPaste, TabFocus, Tracker, Mockup) is now declared as a `LexicalExtension`. New `extensionContributionsStore` carries `userCommands`, markdown transformers, and dynamic options previously owned by `pluginRegistry`; new `extensionEditorComponentsStore` carries React UI surfaces that must mount inside `LexicalExtensionComposer`. ESLint guardrails block re-importing the retired plugin paths and the old `@lexical/react/Lexical*Plugin` modules.
- Upgrade Lexical and `@lexical/*` packages to 0.44.0 across the runtime. Stops forking `@lexical/markdown`'s importer by routing `$convertFromEnhancedMarkdownString` through upstream import, encoding literal `*` and `_` in non-code text as HTML numeric character references (`&nimbalyst#42;`, `&nimbalyst#95;`) instead of backslash escapes -- NCRs are inert to upstream's CommonMark emphasis scanner (which classifies `\` as non-punctuation and so dropped escape-flanked emphasis on re-import). Deletes `LexicalMarkdownImport.ts`, the three forked text-transformer helpers, and the orphaned `utils.ts` copy of upstream's pre-extension `MarkdownCriteria` system. Keeps `ListTransformers.ts` as the 2-space house-style fork until upstream makes `LIST_INDENT_SIZE` configurable. Round-trip corpus test added covering a real-world tracker-integration-strategy excerpt that motivated the work, plus an extension-contract test that walks the full path on-disk Nimbalyst extensions rely on and a node-set snapshot test for standalone and collaboration editor modes.
- Cut per-commit GitHub Actions runtime: drop `packages/runtime/**` from the `ios-transcript-tests` PR-paths filter so the `macos-15` simulator job (10x billed minutes) only runs on PRs that touch `packages/ios/**` directly; add a Transcript Bundle job that builds the iOS transcript Vite bundle on Ubuntu at ~1/100 the cost; cache `node_modules` keyed on `package-lock.json` hash to short-circuit `npm ci` (~70s/job) on warm cache; cache `packages/runtime/dist` keyed on runtime + extension-sdk sources so typecheck skips the ~46s Vite build when sources are unchanged. Push-to-main still runs the full macOS simulator job for both `ios/**` and `runtime/**` changes.
- Markdown anchor links now scroll to in-document headings in the Lexical editor. A new headless `HeadingAnchorExtension` (registered via `NimbalystEditorExtensions.ts`) walks `HeadingNode` mutations and assigns each rendered heading element a GitHub-style slug id (`# Hero` becomes `id="hero"`), with duplicate-suffix handling for repeat slugs. The global link-click handler in `App.tsx` was also extended to detect `href="#..."` links, find the matching id inside the active editor scroll container (so multiple open files do not interfere), and scroll to it; it falls back to default behaviour if no match is found. (nimbalyst#248)
- Marketplace install from a GitHub URL no longer reports success and then silently fails to load when the repo does not include a built `dist/` directory. The install handler now returns a clear error explaining that the repo must be built before installing (or noting if the repo lacks a `package.json` entirely) and cleans up the partially-installed extension directory so the user can retry from a fresh state. (nimbalyst#247)
- Commit-widget failure path now renders the error state instead of the cancelled state. The renderer's `gitCommit` host was collapsing every `result.success === false` outcome (including hook rejection, no-staged-changes errors, and IPC throws) into `action: 'cancelled'`, which the widget's reader at `GitCommitConfirmationWidget.tsx:452` short-circuits to the cancelled state before reaching the error check. Real failures are now sent as `action: 'error'` with the underlying error string, and the widget gains an `error`/`failed` action handler so the user sees the actual failure reason. `'cancelled'` stays reserved for the explicit user-cancel path. (Partial fix for nimbalyst#202.)
- Commit-widget proposal renders before the SDK chunk flush. The `GitCommitConfirmationWidget` failed to appear in the transcript until the user cancelled the AI input because the SDK assistant chunk carrying the `developer_git_commit_proposal` tool_use sat in the `AgentMessageWriteQueue` buffer, and once the MCP handler blocked waiting for the user's response no further chunks arrived to drive the queue's 200ms idle flush -- so the canonical `tool_call_started` event was never produced. `interactiveToolHandlers` now writes an awaited synthetic `nimbalyst_tool_use` row keyed by the SDK's `toolUseId` before notifying the renderer (mirroring the `AskUserQuestion` pattern), and `ClaudeCodeRawParser.parseToolUse` is now async with a `findByProviderToolCallId` cross-batch dedup check so the SDK's later assistant tool_use chunk does not create a duplicate canonical event. Fixes nimbalyst#265.
- Restore open workspaces after an auto-update relaunch. "Restart and Install" left no workspaces open after the relaunch because `performQuitAndInstall` removed the before-quit handler that saves session state, and the resulting window-close cascade then overwrote the persisted list with `{ windows: [] }`. Save session state and mark the app as restarting (so window-close handlers skip their own save) before tearing listeners down, mirroring the MCP restart path. Fixes nimbalyst#232.
- Load workspace YAML tracker schemas on every workspace-open path. `TrackerSchemaService.refreshWorkspaceSchemasIfCurrent` bailed out whenever `currentWorkspacePath` was null, so custom tracker types defined in `.nimbalyst/trackers/` were never registered in memory -- only the CLI-arg workspace open path ever called `updateTrackerSchemaWorkspace`. The null guard is fixed and `updateTrackerSchemaWorkspace` is now called from session restore (`SessionState.ts`), UI workspace open (`WorkspaceManagerWindow.ts`), and file-open workspace creation paths (`index.ts`) so workspace-defined tracker types appear on every entry path.
- Agent-mode conflict dialogs no longer overflow short viewports. The dialog now caps at viewport height with an internal body scroll so the header and Close/Resolve buttons stay reachable, and the max-width widens from 520px to 760px so the conflict file list and two-column commit grid have room to breathe.
- Triple-nested emphasis (e.g. `~~strike *italic **bold** text* inside~~`) corrupted on export when the diff plugin left a whitespace-only text node between formatted siblings. The opening-tag continuity check now uses raw format-bit continuity (`hasTextFormat`) so whitespace siblings stop tripping the duplicate `unclosedTags` path; per-node whitespace flanking still goes through the existing `isWhitespaceOnly` branch.
- Restore Lexical collaboration context for Lexical 0.44. Upstream removed the implicit global `CollaborationContext`, so `useCollaborationContext` started throwing without a `LexicalCollaboration` provider in the tree -- breaking every collab document load. The editor tree is wrapped so `CollaborationPlugin` can resolve the context.
- Track an in-tree `SelectionAlwaysOnDisplayPlugin`. The runtime imported `SelectionAlwaysOnDisplay` from `plugins/SelectionAlwaysOnDisplayPlugin/`, but the file was never checked in -- cold-clone or CI builds would fail with a missing-module error. This is the in-tree replacement for upstream's `@lexical/react/LexicalSelectionAlwaysOnDisplay`, needed because upstream's `markSelection` calls `editorState.read(cb)` without binding an active editor and throws "Unable to find an active editor" in 0.44 when a selection endpoint is on an empty paragraph.
- Dark mode logo asset rendered incorrectly in the editor shell after the Lexical upgrade.
- Ignore .agents/skills/.nimbalyst-generated/ and
  .claude/plugins/.nimbalyst-generated/; Nimbalyst regenerates
  these on launch, so they should not be tracked.
- Ignore packages/electron/e2e/smoke/screenshots/ and
  packages/electron/e2e/walkthroughs/screenshots/; these are
  test output regenerated on every run.
- Untrack the existing manifest.json and screenshot files so
  they stop appearing as constant churn in git status.
Paragraph-isolated markdown links to extension-edited files now render
as live embeds (Excalidraw canvas, mockup, datamodel, csv, sqlite)
inside the host document, with a chrome bar, resize handles, and a
view/edit toggle that autosaves through the extension's EditorHost.

- New EmbedExtension auto-upgrades paragraph-isolated links to
  EmbeddedFileNode when the URL extension is registered as embeddable.
  Tab toggles between link and embed forms; embed=false title attr
  records explicit user opt-out so the auto-upgrade respects it.
- Embeddable extension set is driven by the pluggable custom-editor
  registry rather than a hardcoded list, on both main and renderer.
- DocumentReferenceTransformer hands off embeddable paths to LinkNode
  so the embed auto-upgrade path runs (previously it short-circuited
  to an inline DocumentReferenceNode and links read back as plain
  file references).
- All five built-in extension editors (excalidraw, mockuplm,
  datamodellm, csv-spreadsheet, sqlite-browser) honor reactive
  host.readOnly so view mode hides editing UI and edit mode writes
  back via host.saveContent on a 2s autosave cadence.
- Click-to-select gate: embed shield catches the first click to set
  Lexical NodeSelection, editor wrapper uses isolation+inert so wheel
  events bubble past the embed to the host scroller until the user
  selects it (fixes Excalidraw eating trackpad scroll).
- make embed headers match the main editor breadcrumb toolbar
- preserve folder and file navigation from embedded editors
- archive lingering worktree sessions during archive recovery
- hide archived worktree sessions and super loops from sidebar lists
- archive linked super loop metadata when a worktree is archived
ghinkle and others added 27 commits May 21, 2026 02:21
- Shareable deep links for tracker items. "Copy Link" in the tracker item detail header and in the context menus on the kanban board and the tracker table produces a `nimbalyst://tracker/{id}?orgId={orgId}` URL. The main process resolves the orgId against open and recent workspaces, focuses or creates a window, queues the payload for the renderer; the renderer switches workspace, switches to tracker mode, and opens the detail panel via `setTrackerModeLayoutAtom` (selectedType `all` so the item is visible regardless of its primaryType). Menu entries are single-selection only and hidden when the workspace has no team configured. Fallback notification distinguishes "not signed in" from "no matching workspace".
- Shareable deep links for team documents. "Copy Link" in the shared-document context menu produces a `nimbalyst://doc/{id}?orgId={orgId}` URL that can be pasted in Slack/email and opens the doc in the recipient's app. Main process resolves orgId against open windows (active and rail-warm) and recent workspaces, queues the link payload per workspace so a freshly opened renderer can drain it on listener init, focuses an existing window or creates one for the matching workspace. Renderer switches to collab mode, switches active workspace if needed, and opens the doc by id without waiting for the team's shared-docs list to sync (title backfills automatically).
- Programmable action prompts can now launch a new sibling session in the current workstream instead of prefilling the current input. Per-action config in `ai-actions.md` picks the model, foreground/background, and auto-submit vs prefill behavior. Parser extracts an optional `key:value` config block under each `##` heading (`launch`, `model`, `foreground`, `autoSubmit`, `worktree`); actions without a known leading key keep current behavior. Model field validates via `ModelIdentifier.tryParse` so every model the app accepts is accepted here too, including slash-bearing IDs like `opencode:vendor/name`. Dropdown shows an open-in-new icon next to launcher entries and branches on config; SessionTranscript builds the prompt with the originating-session reference appended via the full UUID. IPC + `MetaAgentService.launchActionSession` spawn the sibling, queuing the prompt when `autoSubmit` is true or leaving the new session's draft prefilled when false. Central listener focuses the new session by driving `selectedWorkstreamAtom` with worktree- and workstream-aware branches.
- File-based tracker items unified with database tracker items. Frontmatter-backed plan files (`design/trackers/*.md`) now show up through the same tracker read model as DB-backed items; plan status and kanban ordering persist through canonical file IDs. MCP visibility and plan mutation regressions are covered by tests.
- Agent transcript flat-list code path removed. Both desktop and iOS now run VList unconditionally; `LazyMount`, `wrapHeavy`, `flatListRef`, `flatBottomSentinelRef`, the flat-list scroll handler and ResizeObserver effects, and the `.rich-transcript-flat-list` / `.scroll-container.flat` CSS are gone. The mobile-vs-desktop VList `bufferSize` difference is preserved.
- `nimbalyst-session-naming` MCP server set to `alwaysLoad: true` so `update_session_meta` stays in the prompt instead of being deferred behind `ToolSearch`. Audit of recent sessions showed ~56% burned their first turn on a `ToolSearch` lookup for the naming tool before they could set the session title and phase.
- Three layered defenses against file-watcher attribution-queue overflows that froze the renderer under multi-session load: hardcoded build-artifact directories (`.build`, `dist`, `node_modules`, etc.) filtered at `WorkspaceEventBus`, `ingestWatcherEvent`, and the bash file-path extractor; per-session cap (`SessionEditQuota`) of 500 distinct edited files hydrated from `session_files` so it survives restart; per-workspace burst throttle (`WorkspaceAttributionThrottle`) token bucket of 20 events with 20/sec refill applied at `ingestWatcherEvent` so codegen/build dumps never reach the queue. Works in pre-git-init workspaces because the throttle is gitignore-independent. (nimbalyst#352, nimbalyst#365)
- Stytch B2B auth recovery made resilient to JWKS key rotation and stale tokens. JWT refresh now uses the actual `exp` claim instead of a 60s "last refresh" throttle, and already-expired JWTs are rejected at the `getJwt` boundary so the WebSocket reconnect loop stops feeding stale tokens to the worker. WebSocket close code/reason/wasClean logged on both index and session rooms so server-side auth rejections stop logging as `[object Object]`. Auth-callback URL params logged from the deep-link handler with sensitive tokens redacted; worker-reported `error` / `error_description` / `stytch_error_type` params surface verbatim instead of silently falling through the missing-`session_token` branch. Stytch auth state centralized in a single atom + IPC listener so consumers stop re-subscribing to `onAuthStateChange`; gutter user icon flips to `no_accounts` in warning color when sync is enabled but the user is signed out. iOS Settings danger zone gets a Sign Out action plus banner/escalation hooks for sustained auth-failure recovery.
- High-volume low-value PostHog events cut to remove ~970k events/week of retry-storm noise. `file_save_blocked_after_delete` and `file_conflict_detected` analytics emits removed (save-block / conflict-detect logic unchanged; ~563k/wk, 1500+ per affected user). `ai_response_streamed` merged into `ai_response_received` and the 1:1 duplicate emit deleted (~106k/wk). `update_toast_shown` deduped to once per distinct `newVersion` per app run (prior guard only suppressed while state stayed `available`). `update_error` background path deduped on `(stage, error_type)` per app run, resetting on a successful check so a recurring error after the network heals is still captured; manual download-failure branch unchanged. `POSTHOG_EVENTS.md`, `FILE_WATCHING_AND_CHANGE_TRACKING.md`, and `WEEKLY_DASHBOARD.md` updated to match.
- System addendum's Interactive User Input section rewritten to nudge agents toward the `PromptForUserInput` widget over chat-based questions: trigger on "about to write a question, list, or draft", inline field-type cheatsheet, and a directive to combine multi-turn questions into one multi-field prompt.
- CLAUDE.md refactored into path-scoped rules and dedicated docs. Repeated guidance from root, electron, and runtime CLAUDE.md extracted into focused docs under `docs/` and `packages/electron/`; path-scoped `.claude/rules/` entries added so each rule loads only where it applies (error handling, naming, database, main process init, end-to-end verification). The three CLAUDE.md files trimmed to high-signal critical rules plus a documentation reference table.
- PGLite lock-staleness check now surfaces an "ambiguous" branch and asks the user via dialog instead of guessing. Follow-up to the closed PR nimbalyst#316: previously the EPERM-with-fresh-timestamp case (lock < 60s old, lock holder PID unsignalable from us) was treated as a live sibling and the launch was refused. On a slow-disk machine where the original Nimbalyst wrote the lock less than 60s before crashing, that produced a false-positive DATABASE_LOCKED that the user could only clear by deleting the lock file from the filesystem. `decideLockIsRunning` now returns a ternary `decision: 'running' | 'stale' | 'ambiguous'` and exposes `lockPid` / `lockAgeMs` for dialog rendering. The 'running' and 'stale' branches behave as before. The 'ambiguous' branch raises a distinct `DATABASE_LOCKED_AMBIGUOUS` error code; `PGLiteDatabaseWorker` catches it and shows a dialog explaining the two scenarios (live sibling vs fast PID reuse) with "Cancel" and "Open Anyway (clear lock)" buttons. The legacy `isRunning` boolean stays on the return shape for backwards compatibility (true for both 'running' and 'ambiguous'). Per @ghinkle's review on the closed PR nimbalyst#316. (nimbalyst#272 follow-up)
- Ambiguous database lock recovery restored end-to-end as a follow-up to PR nimbalyst#325. Worker error metadata is now preserved across the thread boundary so the ambiguous-lock dialog and force-unlock path actually fire instead of being silently downgraded to a generic `DATABASE_LOCKED`. Focused coverage added for worker error round-tripping.
- AI edits to a file open in a custom diff-mode editor (csv-spreadsheet) no longer skip the red/green pending-review diff. Custom editors get file changes through `EditorHost.subscribeToFileChanges`, and that path did not carry the in-flight-diff guard the built-in Lexical/Monaco file-change handler already applies. So the AI edit's own file-watcher echo reached the editor's external-change handler and discarded the pending diff before it could render, and the pre-edit review tag flipped to reviewed within milliseconds. The custom-editor file-change subscription now mirrors the built-in guard and suppresses the raw change while a diff is being applied or a pending AI-edit tag is tracked; the modified content still arrives via the diff request path and the final content via diff resolution. (nimbalyst#328)
- Auto-update error toast no longer fires on transient DNS failures during background polls. electron-updater runs through Electron's Chromium net stack, which reports connectivity failures as `net::ERR_*` strings (most commonly `net::ERR_NAME_NOT_RESOLVED`) that `classifyUpdateError` did not recognise as network errors. Because the nimbalyst#223 background-poll suppression is gated on `errorType === 'network'`, it never fired for these, so a single failed hourly check (e.g. a DNS blip while the machine wakes) left an "Update Error" toast on screen all day. The classifier now matches the `net::ERR_*` connectivity family (DNS resolution, connection, network-state, proxy, address, and timeout failures) while keeping `net::ERR_CERT_*` / `net::ERR_SSL_*` in the signature bucket. Follow-up to nimbalyst#223 / nimbalyst#56.
- Tracker transcript "Tracker Updated" widget no longer crashes on legacy tag value shapes, and now renders all field changes (not just `status` / `priority` / `title` / `owner` / `archived` / `progress` / `tags`). Updates to custom tracker types (incident severity, vendor, description, etc.) previously showed a header with an empty body; the widget now renders a generic from-to row for any unhandled change key. `description` is special-cased to a chars-only summary so entire documents don't render in the transcript. String-shaped tag diffs are covered by a transcript widget test.
- Dragging a file from the file tree or files-edited sidebar into the AI input now inserts `[name](/absolute/path)` instead of `@<workspace-relative-path>`. Codex previously had to guess sibling directories before falling back to cwd-relative, costing multiple shell commands per dropped reference; absolute-path links resolve on first try for any agent.
- Shared tracker bodies now sync end-to-end through the collab Y.Doc, not just local PGLite cache. End-to-end test added covering the shared tracker body sync path against a live collab room; collab sync URL construction centralized in a shared utility to keep tracker and document sync paths aligned.
- Pasted HTML clipboard images stored as assets. Inline `data:image` HTML paste content now rewrites to asset refs so Google Docs-style image pastes don't bloat the document with base64 payloads. Regression coverage added.
- iOS Codex sessions on the app-server transport now display messages. Mobile transcript projection now routes through `CodexRawParserDispatcher` so newer (app-server) sessions get the right parser; previously the SDK parser threw on every output message and the catch swallowed it, leaving only user prompts visible on iOS. Regression test added covering `agentMessage` + `mcpToolCall`. The iOS pre-build script also stops checking the package-local `node_modules/.bin/vite` (npm workspaces hoist it to the root), invokes the workspace-root binary directly, and fails loudly instead of silently shipping a stale transcript bundle.
- Transcript rerenders from other sessions reduced. `SessionTranscript` now subscribes only to its own phase; registry reads remain on-demand for session mention expansion; selection churn from unrelated session activity no longer triggers redraws.
- AIService shutdown no longer dereferences null on late-arriving IPC. `AIService.destroy` stops nulling `sessionManager` / `settingsStore`, which were being read by late IPC invocations during quit.
- E2E selectors unstuck: dropped the dead `.agentic-panel--agent` worktree locator (developer worktrees + blitz alpha enabled in the first describe block so the gated buttons render); datamodellm spec targets `[data-testid="agent-mode-chat-input"]` instead of a multi-match selector that hit both mode textareas; core spec matches `test.md` exactly so the file-tree click no longer collides with `files-mode-test.md` / `agent-mode-test.md`.
- Typecheck fixed: bogus `Promise<TrackerItem[]>` cast on an already-awaited IPC result in `trackerSyncListeners` dropped; `TrackerItem -> Record<string, unknown>` coercion routed through `unknown` so tsc accepts the field-lookup helper; tracker `DocumentService` mocks widened to `Promise<any>` so `mockResolvedValueOnce` accepts TrackerItem fixtures; stale `prompt.test` assertions updated to match the rewritten Interactive User Input section.
- `buildSdkOptions` env tests extended to assert the base flags composed onto every spawned session env (tool-search mode and the client entrypoint label); `CLAUDE_CODE_ENTRYPOINT` restored in `afterEach` alongside the API-key cleanup so the suite leaves the environment as it found it. Test-only.
…used (nimbalyst#395)

Imported Claude Code sessions always showed Sonnet. The importer never set
a model on the new ai_sessions row, so the renderer fell back to a hardcoded
claude-code:sonnet and every imported session displayed Sonnet regardless of
the model that was actually used (nimbalyst#394).

Read the per-turn model recorded on the JSONL assistant entries (e.g.
claude-opus-4-7), map it to the matching claude-code variant, and store it on
the session. When no entry carries a model, fall back to the real claude-code
default (claude-code:opus-1m) rather than Sonnet.

Co-authored-by: Andrew Demczuk <5212682+ademczuk@users.noreply.github.com>
…eport anonymizer (nimbalyst#396)

The bug-report anonymizer is documented to redact known workspace paths to
<WORKSPACE>, but the production caller passed only homeDir, so the workspace
redaction never fired. Workspaces commonly live outside the home directory
(e.g. C:\Projects\...), so those paths went verbatim into the prefilled public
GitHub issue.

The home-directory redaction also used exact-string matching against the value
os.homedir() returns. On Windows the same path appears in logs as forward-slash
(C:/Users/name), Git Bash (/c/Users/name), WSL (/mnt/c/Users/name), and
JSON-escaped (C:\Users\name) forms, all of which bypassed the match and leaked
the OS username.

Thread the reporting session's workspace path through to the anonymizer, and
expand each configured path into its common Windows/POSIX variants, matched
case-insensitively. Pure-function change, additive only. Adds Windows path-form
test coverage (the suite previously only exercised Unix paths).

Co-authored-by: Andrew Demczuk <5212682+ademczuk@users.noreply.github.com>
Co-authored-by: Greg Hinkle <greghinkle@gmail.com>
- show out-of-repo session files in All Session Edits only
- prevent external files from being selectable or staged
- add path boundary coverage for workspace filtering
Opening a ~280 KB markdown file while an AI session held a pre-edit
history tag for the larger pre-trim snapshot pinned the renderer for
~57 seconds before throwing a swallowed "Map maximum size exceeded"
from the TOPT tree-matcher. Electron's unresponsive signal fired and
users saw the app freeze.

- Skip Lexical diff-on-mount when either side of the pending diff
  exceeds 200 KB; the approval bar still appears so the user can
  accept/reject, they just lose inline diff highlighting for that
  one file
- Add e2e/editors/large-markdown.spec.ts repro (CHANGELOG-shaped
  280 KB vs 413 KB fixture, asserts open completes within budget
  and renderer stays responsive)
- Follow-up to remove the guard tracked in
  nimbalyst-local/plans/lexical-diff-size-guard.md (window TOPT's
  PC grid, hash-based guideposts, bound pairMemo)

  Not mentioned in nimbalyst#365 but may be relevant in some hang conditions
- move transcript, model picker, session history, and shared editor
  dropdowns onto floating-ui portals
- prevent panel clipping and stale manual menu positioning across
  these shared surfaces
- keep workstream and worktree metadata in index updates
- route session naming and metadata fields through shared sync paths
…imbalyst#393)

- Drop sessionUpdatedAtAtom subscription from SessionTranscript so
  streaming sessions no longer rerender ~10x/s purely from updatedAt
  ticks; nothing downstream actually consumes that value.
- Extract SessionAIInput wrapper that owns the draftInput and
  draftAttachments subscriptions plus the debounced PGLite save, so
  typing only re-renders the textarea instead of the full transcript,
  banners, and prompt queue.
- Narrow sessions.ts into per-field derived atoms (sessionMessages,
  sessionStatus, sessionPhase, sessionCurrentTeammates, etc.) so
  consumers subscribe to one slice instead of the whole session blob.
- Preserve per-element identity in preserveReloadIdentity: when only
  one message in a reloaded array differs (optimistic to persisted),
  unchanged entries keep their refs so virtualized rows bail out.
- Stabilize teammates / todos / tokenUsage / pendingReviewFiles refs
  on reload via deep-equality so AgentTranscriptPanel's memo holds.
- Memoize AgentSessionPanel and gate ChatSidebar init on isActive so
  background modes do not initialize and rerender.
- Add RenderTrace dev logging in SessionTranscript, AgentTranscript
  Panel.memo, and RichTranscriptView for future regression hunts.
- Tests for preserveReloadIdentity and pending-review-files identity
  preservation.
- seed ai-actions.md with launcher examples for planning and worktree flows
- cover the default template with a parser regression test
- reuse the shared session context menu for child sessions in workstreams
  and blitz worktrees
- refresh transcript reparses without faking new output or mutating
  unread state
- preserve spawned session launches and generic tool-like items
- keep app-server todos and live tool cards aligned with Claude Code
- defer shared markdown bootstrap until server sync to prevent duplicated content
- preserve full custom-editor suffixes when sharing collaborative docs
- add a true table grid with sortable, resizable columns
- migrate legacy tracker view state and share row interactions across views
The `@CHANGELOG.md` reference in CLAUDE.md triggered Claude Code's
file-import behavior, pulling the entire changelog (~100k tokens)
into every session's context. Backtick the filename so the
instruction still reads naturally without the auto-load.
Co-authored-by: Andrew Demczuk <5212682+ademczuk@users.noreply.github.com>
Co-authored-by: Greg Hinkle <greghinkle@gmail.com>
…inks

- treat custom-editor links as the default visual output
- reserve screenshots for verification or explicit requests
Refuse to open the index WebSocket when the JWT subject does
not match the configured userId -- the server would reject
every attempt anyway, and the prior flat 2s pre-open retry
caused server-side throttling.

- Add AUTH_MISMATCH error and indexAuthBlocked latch so
  reconnect scheduling stops until an explicit reconnect.
- Ramp pre-open backoff to 2s x3, then 5s/10s/30s/60s/120s/300s
  instead of staying flat at 2s forever.
- Rate-limit the mismatch warning to once per 60s.
- Clear the latch and counters on reconnectIndex() and
  disconnectAll() so legitimate signals (network change,
  settings update, auth refresh) get another shot.
- use the shared floating tooltip for Alpha badges instead of browser titles
- explain incomplete alpha features, shared-data loss risk, and future Team pricing
- add share-to-team destination picker and local source bindings
- fix shared-doc find/close commands and HMR reconnect cleanup
Active agent turns streamed ~10 messages/sec, and each one drove
MessageSyncHandler.onMessageCreated -> connect() -> ensureFreshJwt(),
which threw AUTH_MISMATCH and logged. One mobile-build session put
1686 of 4986 main.log lines into that single warning and froze the
renderer.

- Per-session connect() now short-circuits on the indexAuthBlocked
  latch (no JWT fetch, no WebSocket alloc) and sets the latch on its
  own AUTH_MISMATCH path so the very first failure is durable.
- SyncProvider exposes isAuthMismatched(); MessageSyncHandler skips
  auto-connect when the latch is set instead of retrying on every
  message.
- Rate-limit "Failed to connect session" to once per minute per
  session so a future regression cannot flood main.log.
- New PLAYWRIGHT-gated IPC repro + spec
  (e2e/sync/collabv3-jwt-mismatch-hang.spec.ts) fires 100 agent
  messages with a mismatched JWT and asserts connect attempts and
  flood logs both stay near zero.
- send durable cancel responses from iOS and Android commit proposal widgets
- remap Codex transcript prompt ids to canonical proposal ids so blocked sessions resume correctly
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 25, 2026

Required label not found on this PR.

@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​lexical/​overflow@​0.34.0 ⏵ 0.44.0100 +110067 +1100100
Updated@​lexical/​file@​0.34.0 ⏵ 0.44.010010068100100
Updated@​lexical/​dragon@​0.34.0 ⏵ 0.44.0100 +110068 +1100100
Updated@​lexical/​code@​0.34.0 ⏵ 0.44.0100 +110069 -4100100
Updated@​lexical/​plain-text@​0.34.0 ⏵ 0.44.09910070 +1100100
Updated@​lexical/​mark@​0.34.0 ⏵ 0.44.0100 +110070 +1100100
Updated@​lexical/​text@​0.34.0 ⏵ 0.44.0100 +110070 +1100100
Updated@​lexical/​hashtag@​0.34.0 ⏵ 0.44.010010070 +3100100
Updated@​lexical/​clipboard@​0.34.0 ⏵ 0.44.09910072 +1100100
Updated@​lexical/​rich-text@​0.34.0 ⏵ 0.44.09910072 +1100100
Updated@​lexical/​selection@​0.34.0 ⏵ 0.44.09910072 +1100100
Updated@​lexical/​utils@​0.34.0 ⏵ 0.44.099 +110073100100
Updated@​lexical/​link@​0.34.0 ⏵ 0.44.098 -110073 +2100100
Added@​lexical/​extension@​0.44.09910074100100
Updated@​lexical/​yjs@​0.34.0 ⏵ 0.44.09910074 +1100100
Updated@​lexical/​code-shiki@​0.34.0 ⏵ 0.44.099 +710074 -2100100
Updated@​openai/​codex-sdk@​0.128.0 ⏵ 0.130.074100100100100
Updated@​nimbalyst/​extension-sdk@​0.1.5 ⏵ 0.2.077100100 +190 +2100
Updated@​lexical/​history@​0.34.0 ⏵ 0.44.010010079 +1100100
Addedrehype-katex@​7.0.110010010083100
Addedremark-math@​6.0.010010010083100
Updated@​lexical/​headless@​0.34.0 ⏵ 0.44.0100 +110085 +2100 +1100
Addedfractional-indexing@​3.2.01001009485100
Updated@​lexical/​table@​0.34.0 ⏵ 0.44.09710087 +1100100
Addedkatex@​0.16.479210010099100
Updatedlexical@​0.34.0 ⏵ 0.44.092 -210099 +1100 +2100
Updated@​lexical/​list@​0.34.0 ⏵ 0.44.099 +110096 +1100100
Updated@​lexical/​markdown@​0.34.0 ⏵ 0.44.096 -110098 +1100100
Updated@​lexical/​react@​0.34.0 ⏵ 0.44.09910098100100
Added@​nimbalyst/​collab-protocol@​0.1.0100100100100100

View full report

@socket-security
Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm happy-dom is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package-lock.jsonnpm/vitest@4.1.5npm/@lexical/headless@0.44.0npm/vitest@3.2.4npm/happy-dom@20.9.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/happy-dom@20.9.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants