Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 40 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,22 +190,40 @@ black for primary content. When adding a new surface, step up the gray ramp

---

## 4b. App shell layout (Codex-style)
## 4b. App shell layout (floating app shell)

The shell lives in [`src/renderer/app/AppShell.tsx`](src/renderer/app/AppShell.tsx),
composed by [`src/renderer/App.tsx`](src/renderer/App.tsx). It is a single column
with a custom title bar on top and a horizontal row of resizable regions below it:
composed by [`src/renderer/App.tsx`](src/renderer/App.tsx). It is a **two-layer
floating app shell**: persistent chrome sits on the pure-black root background,
and the workspace floats above it as one detached card:

```
┌──────────────────────────────────────────────────────────────┐
│ TitleBar (frameless, draggable) [search][_][□][x] │
├────────┬──────────────────────────────────────────┬──────────┤
│Sessions│ header / conversation / Composer │ drawer │▐ rail
└────────┴──────────────────────────────────────────┴──────────┘
│ TitleBar (root bg, frameless, draggable) [search][_][□][x] │
│ ╷ ┌────────────────────────────────────────┐ │
│Sessions│ │ header / conversation / Composer │drawer│ ▐ rail │
│(root bg)│ │ floating card (bg-surface) │ (root bg)│
│ ╵ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
Comment on lines 200 to 208

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Label the ASCII diagram fence.

This is still an unlabeled fenced block, so the markdownlint warning will keep firing. Mark it as text (or another explicit language) to satisfy the doc linter.

Fix
-```
+```text
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
┌──────────────────────────────────────────────────────────────┐
│ TitleBar (frameless, draggable) [search][_][][x]
├────────┬──────────────────────────────────────────┬──────────┤
│Sessions│ header / conversation / Composer │ drawer │▐ rail
└────────┴──────────────────────────────────────────┴──────────┘
│ TitleBar (root bg, frameless, draggable) [search][_][][x]
│ ╷ ┌────────────────────────────────────────┐ │
│Sessions│ │ header / conversation / Composer │drawer│ ▐ rail │
│(root bg)│ │ floating card (bg-surface) │ (root bg)│
│ ╵ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 200-200: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 200 - 208, The ASCII diagram in the documentation is
still an unlabeled fenced block, which will keep triggering the markdown lint
warning. Update the fenced block around the diagram in CLAUDE.md to use an
explicit language tag such as text, and keep the diagram content unchanged so
the doc linter recognizes it correctly.

Source: Linters/SAST tools


- **Root background layer** (`bg-base`, borderless): the TitleBar, the Sessions
sidebar (+ its collapsed rail), and the right `ActivityRail` icon strip. They
visually merge with the black canvas — architectural, not content.
- **Floating workspace card**: the center column **and the right `ActivityDrawer`
together** — `bg-surface`, `border-line`, **`rounded-md` (strictly 6px)**,
`overflow-hidden`, framed by an 8px side gutter and a 16px bottom gutter
(`px-2 pb-4`, `pt-1` under the title bar) so it never touches the window
edges. Separation comes from the gutter +
border + surface step, not shadows (invisible on `#000` anyway).
- The sidebar↔card gutter doubles as the resize grab area (`ResizeHandle ghost`);
the card↔drawer divider stays a 1px `bg-line` handle **inside** the card, so
the drawer (and the full-bleed Terminal/Git/Memory panels) carry no `border-l`
of their own.
- **Left + right columns are full height**; the **Composer lives only inside the
center column** (it does not span the whole window).
center column** (it does not span the whole window). Center-column surfaces sit
on `bg-surface` (the card), not `bg-base` — e.g. the composer fade is
`from-surface`.
- **Right side = a fixed ~48px icon rail (`ActivityRail`) + a collapsible drawer
(`ActivityDrawer`)**. Tabs: Files / Changes / Tasks / Activity. Clicking the
active tab toggles the drawer closed (`activeTab` in the layout store, nullable);
Expand Down Expand Up @@ -484,8 +502,17 @@ the real (no-mock) UI. Each owns one responsibility:
`@electron/rebuild` accordingly. `createForCommand` runs one command in a
PTY (`origin: 'hook' | 'service'`) with an exit callback; terminal `cwd` is
the session's effective root.
- **File System Layer** (`managers/FileSystemManager.ts`) — `chokidar` watch +
tree index + guarded reads; pushes live git status into sessions.
- **File System Layer** (`managers/FileSystemManager.ts`) — the single gateway
for workspace file operations: `chokidar` watch + tree index + guarded reads
(`fs/reader.ts`) **+ guarded writes** (`fs/writer.ts`: atomic write / create
file / create dir / delete / rename+move / copy — workspace-boundary,
symlink-escape, and `.git`-segment protected, bounded by `FS_LIMITS`);
records mutations in the File History ring and pushes live git status into
sessions. Watcher bursts are batched (`WatchBatch`) so small file changes
reindex search **incrementally** (`SearchManager.indexFiles`) while
structural/large batches fall back to a coalesced full pass. The Files tree
has per-language icons (`renderer/lib/fileIcons.tsx`) and a right-click File
Writer context menu (`FileTreeMenu.tsx`).
- **Agent Manager** (`managers/AgentManager.ts`) — drives `@anthropic-ai/claude-
agent-sdk` (plan/implement modes), risk-gated `canUseTool`, path-guarded to the
workspace, persists transcript/activity/diagnostics, resumes SDK sessions.
Expand Down Expand Up @@ -562,9 +589,9 @@ ones at query time (memory, git, sessions, commands).

**Still open / future** — Repository clone/track UI, a dedicated Permission System
beyond the agent's `canUseTool`, merge-conflict resolution UI, remote management, and
stash. True per-file incremental search indexing (v1 does a coalesced full reindex on
change) and local vector embeddings on top of BM25 (both Memory and Search rankings
are already fusion-ready) are natural follow-ups.
stash. Local vector embeddings on top of BM25 (both Memory and Search rankings are
already fusion-ready) and recording File Writer mutations into the session activity
timeline (today they land in the in-memory File History ring) are natural follow-ups.

---

Expand Down
11 changes: 9 additions & 2 deletions docs/reference/window-limboo-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,18 @@ Coding-agent orchestration and the structured event stream.

## fs

File System Layer: read, watch, index.
File System Layer: read, write, watch, index.

- `index(workspaceId)`, `getTree(workspaceId)`, `readFile(workspaceId, relPath)`
- `getHistory(workspaceId)`, `reveal(workspaceId, relPath?)`
- `onIndexProgress(cb)`, `onTreeChanged(cb)` — subscriptions.
- `writeFile(workspaceId, relPath, content, opts?)`, `createFile(workspaceId, relPath)`,
`createDir(workspaceId, relPath)` — guarded File Writer mutations (atomic writes,
workspace-boundary + symlink + `.git` protection in main)
- `remove(workspaceId, relPath, opts?)` (non-empty dirs need `{ recursive: true }`),
`rename(workspaceId, fromRel, toRel, opts?)` (rename AND move),
`copy(workspaceId, fromRel, toRel, opts?)`
- `onIndexProgress(cb)`, `onTreeChanged(cb)` — subscriptions (mutations surface
through `onTreeChanged`; no dedicated mutation events).

## terminal

Expand Down
106 changes: 96 additions & 10 deletions src/main/ipc/fsHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
/**
* IPC handlers for the File System Manager. Registered through `handle()`, so
* every call inherits sender-origin validation. Inputs are validated here in the
* main process (CLAUDE.md §6): workspace ids are type/length-checked and the
* `fs:readFile` relative path is constrained to a bounded, NUL-free, relative,
* non-`..`-traversing string before it ever reaches the reader — so a malicious
* renderer can never widen a read outside the workspace root.
* main process (CLAUDE.md §6): workspace ids are type/length-checked and every
* relative path (read AND write, source AND destination) is constrained to a
* bounded, NUL-free, relative, non-`..`-traversing string before it ever reaches
* the reader/writer — so a malicious renderer can never widen an operation
* outside the workspace root. Mutation options are re-built from known boolean
* flags only (never spread) so renderer objects cannot smuggle keys.
*/
import { IpcChannels } from '@shared/ipc-channels';
import { FS_LIMITS } from '@shared/constants';
import type { FileHistoryEntry, FileReadResult, FileTree } from '@shared/types';
import type {
FileHistoryEntry,
FileReadResult,
FileTree,
FileWriteResult,
FsMutationOptions,
} from '@shared/types';
import { handle } from './registry';
import type { FileSystemManager } from '../managers/FileSystemManager';
import { logger } from '../logger';
Expand All @@ -21,24 +29,41 @@ function assertValidId(id: unknown): asserts id is string {

/**
* Validate a renderer-supplied relative file path before it is joined to a
* workspace root and read. Mirrors the ignored-dirs guard in workspaceHandlers:
* workspace root. Mirrors the ignored-dirs guard in workspaceHandlers:
* bounded, NUL-free, relative only, and no upward (`..`) traversal segment.
*/
function assertValidRelPath(p: unknown): asserts p is string {
if (typeof p !== 'string' || p.length === 0 || p.length > FS_LIMITS.relPathMax) {
throw new Error('fs:readFile invalid path');
throw new Error('fs: invalid path');
}
if (p.includes('\0')) {
throw new Error('fs:readFile path contains an invalid character');
throw new Error('fs: path contains an invalid character');
}
if (p.startsWith('/') || p.startsWith('\\') || /^[a-zA-Z]:/.test(p)) {
throw new Error('fs:readFile path must be relative');
throw new Error('fs: path must be relative');
}
if (p.split(/[/\\]/).some((seg) => seg === '..')) {
throw new Error('fs:readFile path must not traverse upward');
throw new Error('fs: path must not traverse upward');
}
}

/** Validate write content: a string within the File Writer's byte cap. */
function assertValidContent(c: unknown): asserts c is string {
if (typeof c !== 'string') throw new Error('fs: invalid content');
if (Buffer.byteLength(c, 'utf8') > FS_LIMITS.maxWriteBytes) {
throw new Error('fs: content too large');
}
}

/**
* Rebuild mutation options from known boolean flags ONLY — the renderer object
* is never spread or merged (prototype-pollution guard, CLAUDE.md §6).
*/
function sanitizeOpts(o: unknown): FsMutationOptions {
const v = (o ?? {}) as Record<string, unknown>;
return { overwrite: v.overwrite === true, recursive: v.recursive === true };
}

export function registerFsHandlers(fs: FileSystemManager): void {
handle<[string], FileTree>(IpcChannels.fsIndex, async (_e, id) => {
assertValidId(id);
Expand Down Expand Up @@ -74,4 +99,65 @@ export function registerFsHandlers(fs: FileSystemManager): void {
if (relPath !== undefined && relPath !== '') assertValidRelPath(relPath);
await fs.reveal(id, relPath);
});

// --- File Writer mutations. Every path below is re-validated by the writer's
// own containment/symlink/.git guards; this layer rejects malformed input
// before it ever reaches the manager.

handle<[string, string, string, unknown], FileWriteResult>(
IpcChannels.fsWriteFile,
async (_e, id, relPath, content, opts) => {
assertValidId(id);
assertValidRelPath(relPath);
assertValidContent(content);
return fs.writeFile(id, relPath, content, sanitizeOpts(opts));
},
);

handle<[string, string], FileWriteResult>(
IpcChannels.fsCreateFile,
async (_e, id, relPath) => {
assertValidId(id);
assertValidRelPath(relPath);
return fs.createFile(id, relPath);
},
);

handle<[string, string], FileWriteResult>(
IpcChannels.fsCreateDir,
async (_e, id, relPath) => {
assertValidId(id);
assertValidRelPath(relPath);
return fs.createDir(id, relPath);
},
);

handle<[string, string, unknown], void>(
IpcChannels.fsDelete,
async (_e, id, relPath, opts) => {
assertValidId(id);
assertValidRelPath(relPath);
await fs.deleteEntry(id, relPath, sanitizeOpts(opts));
},
);

handle<[string, string, string, unknown], FileWriteResult>(
IpcChannels.fsRename,
async (_e, id, fromRel, toRel, opts) => {
assertValidId(id);
assertValidRelPath(fromRel);
assertValidRelPath(toRel);
return fs.rename(id, fromRel, toRel, sanitizeOpts(opts));
},
);

handle<[string, string, string, unknown], FileWriteResult>(
IpcChannels.fsCopy,
async (_e, id, fromRel, toRel, opts) => {
assertValidId(id);
assertValidRelPath(fromRel);
assertValidRelPath(toRel);
return fs.copy(id, fromRel, toRel, sanitizeOpts(opts));
},
);
}
Loading
Loading