Skip to content

Add CCWeb: React web gateway to Claude Code sessions#75

Closed
JanusMarko wants to merge 65 commits intosix-ddc:mainfrom
JanusMarko:claude/react-cc-bot-gateway-HMWR1
Closed

Add CCWeb: React web gateway to Claude Code sessions#75
JanusMarko wants to merge 65 commits intosix-ddc:mainfrom
JanusMarko:claude/react-cc-bot-gateway-HMWR1

Conversation

@JanusMarko
Copy link
Copy Markdown

Summary

Introduces CCWeb, a new React-based web interface that replaces Telegram as the primary UI for interacting with Claude Code sessions. The backend is a FastAPI + WebSocket server that reuses transport-agnostic core modules from ccbot (tmux management, session monitoring, terminal parsing). The frontend is a React SPA that renders Claude Code output as styled messages and interactive UI components (permission prompts, decision grids, etc.) instead of terminal navigation.

Both ccbot and ccweb can coexist, connecting to the same tmux sessions simultaneously.

Key Changes

New CCWeb Backend (Python/FastAPI)

  • ccweb/backend/server.py: FastAPI application with WebSocket endpoint for bidirectional client communication
  • ccweb/backend/config.py: Environment-based configuration (mirrors ccbot's config structure)
  • ccweb/backend/session.py: Session state management (simplified from ccbot, removes Telegram-specific routing)
  • ccweb/backend/ui_parser.py: Converts raw terminal text to structured interactive UI data
  • ccweb/backend/ws_protocol.py: WebSocket message type definitions (JSON schema for client/server communication)
  • ccweb/backend/main.py: CLI entry point with serve, install, and hook subcommands

Core Modules (Forked from ccbot)

  • ccweb/backend/core/tmux_manager.py: Async tmux window/pane management via libtmux
  • ccweb/backend/core/terminal_parser.py: Detects interactive UIs in pane output via regex patterns
  • ccweb/backend/core/session_monitor.py: Async polling loop that watches JSONL files for new messages
  • ccweb/backend/core/transcript_parser.py: JSONL parser for Claude Code session files (removed Telegram expandable quote sentinels)
  • ccweb/backend/core/monitor_state.py: Persists byte offsets for incremental JSONL reading
  • ccweb/backend/core/hook.py: SessionStart hook handler for window-to-session mapping
  • ccweb/backend/core/utils.py: Shared utilities (atomic JSON writes, directory resolution)

New CCWeb Frontend (React/TypeScript)

  • ccweb/frontend/src/App.tsx: Main application component with session sidebar, message stream, and input
  • ccweb/frontend/src/protocol.ts: TypeScript types mirroring ws_protocol.py
  • ccweb/frontend/src/hooks/useWebSocket.ts: WebSocket connection management with auto-reconnect
  • ccweb/frontend/src/hooks/useSession.ts: Session state and message filtering logic
  • ccweb/frontend/src/components/: UI components for messages, interactive UIs, decision grids, file upload, directory picker, command palette, etc.
  • ccweb/frontend/src/index.css: Catppuccin-inspired dark theme with CSS variables
  • Build config: vite.config.ts, tsconfig.json, package.json

Documentation

  • ccweb/docs/architecture/design-plan.md: Comprehensive architecture and design rationale (843 lines)
  • ccweb/docs/architecture/v2-roadmap.md: Deferred features and future enhancements
  • ccbot-workshop-setup.md: Complete setup guide for fresh Windows machines

CCBot Enhancements

  • src/ccbot/process_info.py: New module for process tree inspection, memory usage, and OOM-kill detection
  • src/ccbot/handlers/status_polling.py: Added system-wide memory pressure monitoring with escalating actions (warn → interrupt → kill)
  • src/ccbot/handlers/interactive_ui.py: Improved error handling for interactive UI responses
  • src/ccbot/session.py: Refactored iter_thread_bindings()all_thread_bindings() for snapshot semantics
  • src/ccbot/tmux_manager.py: Exported SHELL_COMMANDS constant for reuse

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw

claude and others added 30 commits February 28, 2026 18:16
New env var sets a fixed starting directory for the directory browser,
falling back to Path.cwd() if not set (preserving current behavior).

https://claude.ai/code/session_01Vn1pxPc8KahAYpofYGhLjY
…6kvZ

Add CCBOT_BROWSE_ROOT config for directory browser start path

New env var sets a fixed starting directory for the directory browser,
falling back to Path.cwd() if not set (preserving current behavior).

https://claude.ai/code/session_01Vn1pxPc8KahAYpofYGhLjY
Four callback handlers (CB_DIR_SELECT, CB_DIR_UP, CB_DIR_PAGE,
CB_DIR_CONFIRM) and build_directory_browser's invalid-path fallback
used raw Path.cwd() instead of config.browse_root. This meant users
could escape the configured browse root if user_data was lost or the
path became invalid during navigation.

https://claude.ai/code/session_01Vn1pxPc8KahAYpofYGhLjY
…6kvZ

Fix inconsistent Path.cwd() fallbacks in directory browser callbacks

Four callback handlers (CB_DIR_SELECT, CB_DIR_UP, CB_DIR_PAGE,
CB_DIR_CONFIRM) and build_directory_browser's invalid-path fallback
used raw Path.cwd() instead of config.browse_root. This meant users
could escape the configured browse root if user_data was lost or the
path became invalid during navigation.

https://claude.ai/code/session_01Vn1pxPc8KahAYpofYGhLjY
- session.py: Replace deprecated asyncio.get_event_loop() with
  asyncio.get_running_loop() (Python 3.12+ compat)
- session.py: Remove redundant pass statements
- session_monitor.py: Consolidate double stat() call into one
- screenshot.py: Add explicit parens in _font_tier() for clarity
- bot.py: Add /kill command handler — kills tmux window, unbinds
  thread, cleans up state, and best-effort deletes the topic.
  Previously the /kill bot command was registered in the menu but
  had no handler, falling through to forward_command_handler.

https://claude.ai/code/session_01Vn1pxPc8KahAYpofYGhLjY
…6kvZ

Fix misc bugs: asyncio deprecation, double stat, missing /kill handler

- session.py: Replace deprecated asyncio.get_event_loop() with
  asyncio.get_running_loop() (Python 3.12+ compat)
- session.py: Remove redundant pass statements
- session_monitor.py: Consolidate double stat() call into one
- screenshot.py: Add explicit parens in _font_tier() for clarity
- bot.py: Add /kill command handler — kills tmux window, unbinds
  thread, cleans up state, and best-effort deletes the topic.
  Previously the /kill bot command was registered in the menu but
  had no handler, falling through to forward_command_handler.

https://claude.ai/code/session_01Vn1pxPc8KahAYpofYGhLjY
Add timestamp-based deduplication in handle_interactive_ui() to prevent
both JSONL monitor and status poller from sending new interactive messages
in the same short window. The check-and-set has no await between them,
making it atomic in the asyncio event loop.

Also add a defensive check in status_polling.py to skip calling
handle_interactive_ui() when an interactive message is already tracked
for the user/thread (e.g. sent by the JSONL monitor path).

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
…returning list snapshot

iter_thread_bindings() was a generator yielding from live dicts. Callers
with await between iterations (find_users_for_session, status_poll_loop)
could allow concurrent unbind_thread() calls to mutate the dict mid-iteration,
causing RuntimeError: dictionary changed size during iteration.

Fix: rename to all_thread_bindings() returning a materialized list snapshot.
The list comprehension captures all (user_id, thread_id, window_id) tuples
eagerly, so no live dict reference escapes across await points.

Changes:
- session.py: iter_thread_bindings -> all_thread_bindings, returns list
- bot.py, status_polling.py: update all 4 call sites
- Remove unused Iterator import from collections.abc
- Add tests: snapshot independence, returns list type, empty bindings

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
queue.join() in handle_new_message blocked the entire monitor loop while
waiting for one user's queue to drain. If Telegram was rate-limiting, this
could stall all sessions for 30+ seconds.

Fix: use enqueue_callable() to push interactive UI handling as a callable
task into the queue. The worker executes it in FIFO order after all pending
content messages, guaranteeing correct ordering without blocking.

Also fixes:
- Callable tasks silently dropped during flood control (the guard checked
  task_type != "content" which matched "callable" too; changed to explicit
  check for "status_update"/"status_clear" only)
- Updated stale docstring in _merge_content_tasks referencing queue.join()

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
unpin_all_forum_topic_messages was used every 60s to detect deleted topics,
but it destructively removed all user-pinned messages as a side effect.

Replace with send_chat_action(ChatAction.TYPING) which is ephemeral
(5s typing indicator) and raises the same BadRequest("Topic_id_invalid")
for deleted topics. All existing error handling works unchanged.

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
- Add MAX_TASK_RETRIES=3 retry loop for short RetryAfter (sleep and retry)
- Re-queue tasks on long RetryAfter (>10s) with MAX_REQUEUE_COUNT=5 cap
- Convert callable_fn from Coroutine to Callable factory (coroutines are
  single-use; retry requires a fresh coroutine each attempt)
- Catch RetryAfter from _check_and_send_status to prevent cosmetic status
  updates from triggering content message re-sends
- Fix test isolation: clear _last_interactive_send in test fixtures

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
The _file_mtimes dict used mtime+size to skip unchanged JSONL files, but
this introduced edge cases (sub-second writes, clock skew, file replacement).
For append-only JSONL files, comparing file size against last_byte_offset is
sufficient and eliminates all mtime-related issues.

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
Previously byte offsets were persisted to disk BEFORE delivering messages
to Telegram. If the bot crashed after save but before delivery, messages
were silently lost. Now offsets are saved AFTER the delivery loop,
guaranteeing at-least-once delivery: a crash before save means messages
are re-read and re-delivered on restart (safe duplicate) rather than
permanently lost.

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
Dead sessions were cleaned from persistent state but never from the
in-memory _pending_tools dict, causing a slow memory leak over time.
Add pop() calls in both cleanup paths (startup + runtime).

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
Previously _pending_thread_text was cleared from user_data BEFORE
attempting to send it to the tmux window. If send_to_window() failed,
the message was lost and the user had to retype it. Now the pending
text is only cleared after a successful send.

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
Typing indicators in forum topics were silently failing because
message_thread_id was not passed to send_chat_action calls. Users
in forum topics wouldn't see typing indicators while Claude worked.

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
The except Exception handler was catching RetryAfter (Telegram 429
rate limiting) and BadRequest("message is not modified"), preventing
proper rate limit propagation and causing unnecessary duplicate
message sends.

Changes:
- Re-raise RetryAfter in both edit and send paths so the queue
  worker retry loop can handle rate limiting correctly
- Treat BadRequest "is not modified" as success (content identical)
- For other BadRequest errors (message deleted, too old), delete
  orphan message before falling through to send new
- Log exception details in catch-all handler for debugging

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
When JSONL monitoring enqueues _send_interactive_ui, the callable may
execute after the interactive UI has been dismissed. This caused stale
callables to potentially send duplicate interactive messages.

Fix: introduce a monotonically incrementing generation counter per
(user_id, thread_id) key. Every state transition (set_interactive_mode,
clear_interactive_mode, clear_interactive_msg) increments the counter.
The JSONL monitor captures the generation at enqueue time and passes it
to handle_interactive_ui via expected_generation parameter. If the
generation has changed by execution time, the function bails out.

The status poller is unaffected (passes None, skipping the guard).

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
The second all_thread_bindings() call gets a fresh snapshot that
naturally excludes entries unbound by the topic probe loop above.
This is correct behavior, not a bug — add a comment to clarify
the intent for future readers.

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
The return value was already handled correctly (proceed regardless),
but the ignored bool looked like a bug. Add a comment explaining that
on timeout the monitor's 2s poll cycle picks up the entry, and thread
binding, pending text, and topic rename work without session_map.

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
…e-messages-FpKAU

Document intentionally ignored wait_for_session_map_entry return value

The return value was already handled correctly (proceed regardless),
but the ignored bool looked like a bug. Add a comment explaining that
on timeout the monitor's 2s poll cycle picks up the entry, and thread
binding, pending text, and topic rename work without session_map.

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
…edia

Telegram clients fail to re-render document thumbnails when editing
document-type media in place via editMessageMedia, causing a "white
circle with X" on screenshot refresh. Switch from reply_document +
InputMediaDocument to reply_photo + InputMediaPhoto, which Telegram
clients handle reliably for inline image edits.

Also adds debug logging for the key-press screenshot edit path.

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
…e-messages-FpKAU

Fix screenshot refresh showing broken preview by switching to photo m…
Check pane_current_command before sending keys to tmux windows.
If the pane is running a shell (bash, zsh, etc.), Claude Code has
exited and user text must not be forwarded — it would execute as
shell commands.

Guards added to: send_to_window (safety net), text_handler (with
auto-unbind), esc_command, usage_command, and screenshot key-press
callback.

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
When send_to_window detects the pane is running a shell, it now
captures the pane content and looks for:
  - "Stopped ... claude" → sends "fg" (suspended process)
  - "claude --resume <id>" → sends the resume command

Waits up to 3s (fg) or 15s (--resume) for Claude Code to take
over the terminal, then sends the user's original text.

If no resume command is found, the text_handler unbinds the topic
and tells the user to start a new session.

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
…_pane

Reduces tmux subprocess calls from ~120/s to ~21/s with 20 windows by:
- Adding 1-second TTL cache to list_windows() (all callers in the same
  poll cycle share one tmux enumeration instead of N)
- Unifying capture_pane() to always use direct `tmux capture-pane`
  subprocess (plain text mode previously used libtmux which generated
  3-4 tmux round-trips per call)
- Invalidating cache on mutations (create/kill/rename)

https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
…e-messages-FpKAU

Claude/fix duplicate interactive messages fp kau
When an assistant message contains both text blocks and an interactive
tool_use (ExitPlanMode/AskUserQuestion), the text entries were processed
first in handle_new_message, clearing the interactive UI state set by the
status poller. This caused the JSONL callable to send a second interactive
message instead of editing the existing one.

Fix: pre-scan assistant messages for interactive tools and suppress text
block emission when present — the terminal capture already includes that
preamble text.

https://claude.ai/code/session_01WHUN1GLBFr2ZkuEmeVtuPW
…icates-eUNgW

Fix duplicate interactive UI messages for numbered answers

When an assistant message contains both text blocks and an interactive
tool_use (ExitPlanMode/AskUserQuestion), the text entries were processed
first in handle_new_message, clearing the interactive UI state set by the
status poller. This caused the JSONL callable to send a second interactive
message instead of editing the existing one.

Fix: pre-scan assistant messages for interactive tools and suppress text
block emission when present — the terminal capture already includes that
preamble text.

https://claude.ai/code/session_01WHUN1GLBFr2ZkuEmeVtuPW
claude added 29 commits March 29, 2026 19:26
The 1.x release removed _update_block and renamed escape_latex,
breaking the markdown_v2.py import. Pin to 0.5.x which has the
API our code depends on.

https://claude.ai/code/session_01Db4zZKSaAkJrgHGzeLjRsz
New self-contained project in ccweb/ that replaces the Telegram interface
with a browser-based web frontend for Claude Code sessions via tmux.

Backend (Python/FastAPI):
- Forked 7 core modules from ccbot (tmux_manager, terminal_parser,
  session_monitor, transcript_parser, monitor_state, hook, utils)
  with imports adapted for ccweb (ccweb_dir, separate ~/.ccweb/ state)
- New config.py: web server settings, no Telegram dependencies
- New session.py: simplified client_bindings model (no thread_bindings)
- New server.py: FastAPI + WebSocket for real-time bidirectional comms
- New ws_protocol.py: typed message definitions (message, interactive_ui,
  decision_grid, status, sessions, health, error, replay)
- New ui_parser.py: parses raw terminal text into structured interactive
  UI data with fallback to raw text display
- New main.py: CLI with ccweb (serve), ccweb install (hook + commands),
  ccweb hook (SessionStart handler)
- Startup health checks: tmux running, hook installed, config valid
- Stale UI guard on interactive prompts (re-captures pane before acting)

Decision grid protocol: file-based detection (.ccweb/pending/*.json)
with AskUserQuestion blocking to prevent timing issues.

Docs:
- Full design plan (docs/architecture/design-plan.md)
- V2 roadmap (docs/architecture/v2-roadmap.md)

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
- Fix stale UI guard blocking Escape-to-interrupt: Escape key is now
  always allowed through (it's the interrupt key), guard only applies
  to interactive UI navigation keys
- Add lookup_window_state() that doesn't auto-create phantom entries,
  use it in _handle_new_message to prevent state pollution and disk
  I/O churn from create-delete cycles
- Remove dead code find_clients_for_session (misleading name, never called)
- Implement decision grid file polling in status_poll_loop: watches
  .ccweb/pending/*.json, validates JSON, sends to frontend, moves to
  completed/ or failed/
- Add submit_decisions WebSocket handler: formats user selections as
  text and sends to Claude via tmux, moves grid files to completed/

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Found by adversarial import chain review:

- Move backend/ under ccweb/ccweb/ so hatch builds a proper wheel
  with the ccweb.backend module hierarchy matching the entry point
- Add ccweb/__init__.py as top-level package marker
- Fix pyproject.toml: packages = ["ccweb"], inline readme (no missing file)
- Fix static files path in server.py (extra parent level after restructure)
- Fix CLI dispatch: `ccweb garbage` now shows error instead of silently
  starting the server

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Found by fresh adversarial data flow review:

- Add debug logging when message routing skips a client due to missing
  window_state (previously silently continued, making dropped messages
  invisible)
- Clear _sent_grid_files on new WebSocket connection so reconnecting
  clients can see pending decision grid files
- Add send_ack response on successful text send so the frontend knows
  the message was delivered to Claude

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Found by fresh adversarial protocol review:

- Add WsPong, WsHistory, WsSendAck dataclasses to ws_protocol.py,
  replacing hardcoded raw dicts in server.py
- Remove dead code: WsReplay and CLIENT_REPLAY_REQUEST (replay not
  implemented, deferred to v2)
- Replace all 5 silent `except Exception: pass` blocks with debug
  logging so dead WebSocket clients don't silently swallow errors
- Fix shutdown: await _status_task cancellation properly, clear
  module-level state (_clients, _client_bindings, _sent_grid_files)
  to prevent test pollution

Also confirmed by install/run review: all imports resolve, pip install
succeeds, entry point works, no remaining ccbot references in code paths.

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
React + TypeScript + Vite + Tailwind frontend with:

Components:
- App.tsx: main layout with sidebar + content area
- SessionSidebar.tsx: session list, create/kill, health warnings
- MessageStream.tsx: styled message display with markdown rendering,
  auto-scroll, copy buttons on code blocks, role-based styling
- MessageInput.tsx: multi-line textarea, visible Submit button,
  Ctrl+Enter shortcut, Escape toolbar button
- InteractiveUI.tsx: renders AskUserQuestion as clickable option cards,
  PermissionPrompt as Allow/Deny buttons, ExitPlanMode as Proceed/Edit,
  with raw-text fallback + navigation keyboard for unknown UI types
- FilterBar.tsx: toggleable chips (All/Chat/No Thinking/Tools)
- ExpandableBlock.tsx: collapsible sections for thinking/tool results
- StatusBar.tsx: connection status indicator + Claude status text

Hooks:
- useWebSocket.ts: WebSocket connection with auto-reconnect (exponential
  backoff), 30s keepalive pings, typed message dispatch
- useSession.ts: session state, message history, filter logic

Protocol:
- protocol.ts: mirrors ws_protocol.py — all server/client message types

Config:
- Vite proxy: ws: true for WebSocket, /api proxy to FastAPI
- Dark terminal theme (Catppuccin-inspired)
- TypeScript strict mode, builds clean

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Critical fixes:
- Add DecisionGrid component: full modal overlay with option cards,
  notes column per row, submit/dismiss. Wired into App.tsx + useSession
- Add decision_grid handler in useSession.ts (was silently dropped)
- Fix stale closure: use activeWindowIdRef instead of closing over state,
  removing [activeWindowId] dependency from handleServerMessage
- Gate history response by window_id to prevent wrong-session display
  on rapid session switching

WebSocket fixes:
- Guard reconnect timer against unmount (mountedRef prevents orphaned
  timers and leaked connections during HMR)
- send() now returns boolean so caller can detect dropped messages

Cleanup:
- Remove duplicate ConnectionStatus type (components/types.ts), import
  from useWebSocket.ts canonical export instead
- StatusBar imports from hook instead of deleted types.ts

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Backend fixes:
- Catch UnicodeDecodeError in session_monitor (corrupted byte offset
  mid-UTF8 char was blocking ALL sessions from being monitored)
- Per-client grid tracking replaces global _sent_grid_files (prevents
  duplicate grids to existing clients on reconnect, allows multi-client)
- Interactive UI dedup: don't re-send same UI content every second
- resolve_session_for_window uses lookup_window_state (no phantom entries)
- Clean up per-client state on disconnect

Frontend fixes:
- DecisionGrid resets row state when grid prop changes (prevents crash
  if new grid has fewer items than previous)
- Clear messages/status/UI on session switch (prevents stale messages
  from previous session showing during history load)
- Add clearSessionState to useSession return type

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Use key={grid.id} on DecisionGrid to force remount when a new grid
arrives. This eliminates the useEffect-based state reset which had a
one-render-cycle window where rows[i] could be undefined if the new
grid had more items than the old one, causing a TypeError crash.

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Backend:
- GET /api/sessions/{window_id}/skills: discovers slash commands from
  the session's .claude/commands/ directory (name + first-line description)
- GET /api/browse?path=: directory browser for session creation

Frontend:
- CommandPalette.tsx: dropdown with grouped commands (Project Commands
  + Built-in Commands), keyboard navigation (arrow keys + Enter),
  fuzzy filtering as you type
- MessageInput now triggers palette on "/" at start of line, or via
  "/ Commands" toolbar button
- 15 built-in Claude Code commands defined with descriptions
- Project commands fetched from backend on session switch
- Palette positioned above input with smooth keyboard interaction

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
…rtcuts

Gaps identified by plan-vs-implementation audit. Fixes for phases 3-7:

Phase 3 gap — FileUpload:
- New FileUpload.tsx: paperclip button with drag-and-drop support
- New POST /api/sessions/{window_id}/upload: saves to docs/inbox/,
  sends file path to Claude via tmux

Phase 4 gap — DirectoryPicker:
- New DirectoryPicker.tsx: full modal with file-tree browser using
  GET /api/browse, parent navigation, path text input, session name field
- SessionSidebar simplified: "+ New" now opens DirectoryPicker overlay

Phase 3 gap — Command history:
- Ctrl+Up/Down in MessageInput cycles through previously sent messages

Phase 7 gap — Screenshot button:
- New GET /api/sessions/{window_id}/screenshot endpoint (captures pane text)
- Screenshot button in MessageInput toolbar

Phase 3/12 gap — Image rendering:
- WsMessage now carries images field (base64-encoded from tool_result)
- server.py passes image_data through to WebSocket clients
- MessageStream renders inline images below message content

Phase 12 gap — Keyboard shortcuts:
- Ctrl+N: open new session (DirectoryPicker)

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
- Clamp selectedIndex on Enter to prevent out-of-bounds crash when
  filter shrinks the command list faster than state updates
- Only register global keydown handler when palette is visible,
  preventing Enter interception after palette closes
- Memoize allCommands array in App.tsx to prevent unnecessary
  CommandPalette re-renders and handler re-registrations

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Backend:
- Add python-multipart to pyproject.toml dependencies (required by
  FastAPI's UploadFile — app crashed on startup without it)

Frontend:
- Fix skill fetch race condition: add AbortController to cancel stale
  fetches when rapidly switching sessions
- Fix CommandPalette scrollIntoView: use data-cmd-index attribute
  instead of child index (group headers shifted indices, causing wrong
  element to scroll into view)
- Memoize allCommands with useMemo to prevent unnecessary re-renders

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
- Wrap grid file rename in try/except FileNotFoundError to handle
  race where submit handler already moved the file
- Validate window_id is non-empty in CLIENT_SWITCH_SESSION handler
  (prevents empty binding causing wasted poll work)
- Eliminate double is_interactive_ui + extract_interactive_content
  call — use single extract_interactive_content, fix indentation

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Wrap grid file rename in CLIENT_SUBMIT_DECISIONS handler with
try/except FileNotFoundError, matching the same fix already applied
to _check_decision_grids. Prevents WebSocket disconnect when a file
is moved between glob enumeration and rename.

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Infrastructure:
- CLAUDE.md at ccweb root with wiki update reminder (persists across
  commits — "when you change any feature, update the corresponding doc")
- Backend: GET /api/docs (tree with frontmatter), GET /api/docs/{path}
  (raw markdown, frontmatter stripped), GET /api/docs-search?q= (full
  text search with snippets)
- _parse_frontmatter() helper (simple YAML key:value parser, no deps)
- Path traversal protection on doc content endpoint

Frontend:
- WikiSidebar.tsx: section-grouped navigation tree, live search with
  debounced API calls, active page highlighting
- WikiPage.tsx: markdown rendering with react-markdown + remark-gfm,
  internal link resolution (relative paths navigate within wiki),
  breadcrumb navigation, styled tables and code blocks
- App.tsx routing: wikiPath state toggles between session view and wiki
  view. WikiSidebar replaces SessionSidebar when in wiki mode.
- "Wiki / Help" button at bottom of SessionSidebar

Example docs:
- index.md (home page with section links)
- getting-started/installation.md, quickstart.md
- features/sessions.md, interactive-ui.md
- troubleshooting/common-issues.md
- Existing: architecture/design-plan.md, v2-roadmap.md

All docs use YAML frontmatter (title, description, order) for sidebar
sorting. Internal links use relative paths that work both on GitHub
and in the wiki UI.

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
- Replace str.startswith() with Path.is_relative_to() for path
  traversal check (prevents prefix attack on sibling directories)
- Replace exists() + read_text() with try/except to handle race
  condition where file is deleted between check and read
- Catches FileNotFoundError, IsADirectoryError, and generic OSError

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Layout:
- Desktop (>1024px): sidebar visible alongside content (unchanged)
- Tablet (<=1024px): sidebar becomes a slide-out drawer with hamburger
  button, backdrop overlay to close

New hook:
- useResponsive.ts: detects tablet breakpoint via matchMedia listener

App.tsx changes:
- Sidebar wrapped in positioned container with CSS transition
- Hamburger button (fixed top-left, 44x44px touch target)
- Backdrop closes drawer on tap
- Session selection auto-closes drawer on tablet
- Main content area gets top padding for hamburger on tablet

CSS additions:
- @media (max-width: 1024px) enforces 44px min button/touch sizes
- font-size: 16px on textarea to prevent iOS auto-zoom
- -webkit-fill-available for virtual keyboard push-up

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
On tablet, the hamburger button (z-index 200) was visible and
clickable above DecisionGrid and DirectoryPicker modals (z-index 100),
allowing the sidebar drawer to open behind a modal. Now the hamburger
is hidden when showDirectoryPicker or decisionGrid is active.

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Backend gaps filled:
- REST POST /api/sessions and DELETE /api/sessions/{window_id} endpoints
  (plan called for both REST and WebSocket — WebSocket-only before)
- POST /api/sessions/{window_id}/setup-ccweb: creates .ccweb/instructions.md
- Stale grid file cleanup: files >1hr in pending/ moved to failed/
- Auto-create .ccweb/pending/ directory on session creation (both REST
  and WebSocket create_session paths)

Frontend gaps filled:
- localStorage persistence: recent directories (DirectoryPicker shows
  last 5 used paths), message filter state, last active session
- Swipe gestures: swipe right from left edge opens sidebar drawer,
  swipe left closes it (tablet only)
- Ctrl+K keyboard shortcut toggles command palette via
  externalPaletteToggle prop

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
- Wrap addRecentDir localStorage.setItem in try/catch (was the only
  unguarded localStorage call — would crash handleConfirm if storage
  unavailable, preventing session creation)
- Change Ctrl+K palette toggle from boolean to counter so React
  StrictMode double-invocation doesn't cancel the toggle. Effect now
  sets palette to true (idempotent) instead of toggling.

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
- Auto-send switch_session when WebSocket connects/reconnects and there
  is an active session (restored from localStorage or currently selected).
  Fixes: empty message stream on page reload, and silent message loss
  after WebSocket reconnection.
- Clear all session state (including DecisionGrid) when killing the
  active session. Fixes: stale DecisionGrid overlay lingering after
  its session is killed.

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Strip directory components from uploaded filename using Path.name
to prevent ../../../ sequences from writing files outside docs/inbox/.

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Backend:
- /api/browse now enforces containment within browse_root (defaults to
  home dir). Prevents arbitrary filesystem directory enumeration.
  Parent nav stops at the root boundary.
- load_session_map skips stale cleanup when valid_wids is empty,
  preventing total window_states wipe during transient session_map
  reads (mid-write race or mismatched tmux session name)

Frontend:
- switch_session useEffect only fires on status transition TO
  "connected" (using prevStatusRef), not on every activeWindowId
  change. Prevents double switch_session (and duplicate history
  load / potential message loss) on manual session selection.

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
When the server broadcasts an updated sessions list that no longer
includes the currently active window_id, clear the active session
state (messages, status, interactive UI, decision grid) and remove
from localStorage. Prevents the user from interacting with a phantom
session that was killed by another client or externally.

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
Browser notifications (useNotifications.ts):
- Request permission on first visit
- Notify when interactive UI appears and tab is not focused
- Click notification to focus the tab

Session rename:
- Double-click session name in sidebar to edit inline
- PUT /api/sessions/{window_id}/rename backend endpoint
- Updates tmux window name + display name + broadcasts to all clients

Export conversation:
- GET /api/sessions/{window_id}/export?fmt=markdown|json|plain endpoint
- Formats messages as Markdown (with thinking in details tags, tools
  in code blocks), JSON, or plain text
- Export button in StatusBar triggers download

Context/cost indicator:
- Parse "Context: N%" from Claude Code's status bar chrome via regex
- Added context_pct field to WsStatus protocol message
- StatusBar shows colored context percentage (green <50%, yellow
  50-80%, red >80%)
- Clears on session switch

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
- README.md: project overview, quick start, architecture diagram,
  links to all documentation, configuration reference, standalone
  repo instructions
- docs/architecture/deferred-items.md: prioritized grid of all 27
  deferred features with effort, usefulness, success probability,
  and reason for deferral. Includes top-5 impact/effort ranking.
- docs/architecture/session-history.md: complete history of the
  build session — origin, all 12 phases, key architectural decisions,
  adversarial review process (40+ bugs found/fixed), what a new
  Claude Code session needs to know to pick up where this left off

https://claude.ai/code/session_01TxXyTqEAaG3ExRtaooMBKw
@JanusMarko JanusMarko closed this Apr 11, 2026
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.

2 participants