Promote develop → main: Sessions board by-bot, CodeMirror/Monaco editors, workspace git hardening#176
Conversation
… drag - Group by bot: each bot is a header with its session cards under it (primary first). The bot name moves off every card into the header. - Show workdir on the card face (left-truncated, full path on hover), with a small primary badge. - Polish drag-to-promote: source dims, primary card shows a dashed droppable hint, hovering surfaces a centered '↑ Make primary' pill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…session-workdir root picker Connector: run_git now passes `-c safe.directory=*` so a repo whose files are owned by a different UID than the connector process (bind-mount / container / mixed-UID) stays inspectable — git's dubious-ownership guard otherwise fails even `rev-parse` and it was misreported as E_NOT_A_REPO (which also hid the History/Changes views). Only a genuine "not a git repository" now maps to E_NOT_A_REPO; other rev-parse failures surface their real stderr as E_GIT. Frontend: live-refetch circuit breaker. WS ticks/signals drive up-to-5-request refetch batches with no error gating, so an offline/erroring connector got hammered on every frame. After 3 consecutive failed pulls the 400ms debounce drops to a ~12s probe (without restarting the timer per frame, which would starve it), recovering the moment a pull succeeds. Manual Refresh and foreground navigation bypass the throttle; a "live updates paused" hint shows. Workspace root picker: new DB-only endpoint GET .../workspace/session-workdirs enumerates a channel's session workdirs. The picker now offers session workdirs first (each scoping the browse to its owning session so the connector accepts the subdir as a root) and the connector's allowed_roots as the fallback set — no connector/security change needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… silently) The grouped Sessions board kept native HTML5 drag-and-drop for drag-to-promote, but real drops silently no-op'd for the user (the drop never fired). Rewrite it on pointer events, matching the reliable lane-window drag: - The bot group owns the drag: pointerdown on a non-primary card starts it (with setPointerCapture + a small move threshold so a plain click is inert), window listeners track the cursor, and the group hit-tests the primary card's rect. Release over the primary promotes it. - Visuals unchanged in spirit: dragged source dims, the primary card shows a dashed droppable hint, and a "↑ Make primary" pill while the cursor is over it. `busy` (promote in flight) is lifted to the group. Verified end-to-end via dispatched pointer events (hot ring + pill + Applied toast + primary moved); works with touch too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The composer's session-target chip listed each session by its tag (primary / time / dir basename) but not its working directory. Add a second line per session row — a folder icon + the cwd (left-truncated, full on hover; "default" when the connector chose it) — matching the Sessions board so both surfaces read the same. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
react-markdown v10 (bumped in c4efbb4) removed the `className` prop; passing it throws inside <Markdown>, which — with no error boundary — crashed the whole ChannelView to a blank screen for any channel that had messages. Wrap ReactMarkdown in a styled <div> so the "prose" + caller classes still apply without feeding a rejected prop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The workbench File panel's Raw mode was a plain <textarea>. Swap it for a small, embeddable CodeMirror 6 editor: line numbers, undo history, bracket matching, and markdown/json syntax highlighting. - New CodeEditor component; dark theme matches the panel tokens (zinc-950 field, indigo focus/caret, tinted "data-coding" syntax hues per DESIGN.md). Still renders content as INERT TEXT (no HTML execution), so the textarea's XSS-safety property is preserved. - Uncontrolled-with-sync: CodeMirror owns the doc; external changes (path switch, live-push reload) are pushed in only when they differ, so typing never loses the cursor/selection. - Only markdown + json language packs are loaded; everything else is a small core module, and the editor is click-gated via React.lazy so it downloads only when a user opens Raw mode — off the chat critical path (ChatLayout chunk drops ~520 kB; CodeMirror lands in its own chunk), consistent with how pdf/hljs are handled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The updateListener forwarded every docChanged to onChange, including the programmatic dispatches in the path-switch and value-sync effects. That marked clean server reloads dirty, wrongly enabled Save, and made FilePanel skip subsequent live-push reloads (it saw phantom unsaved edits). Tag those internal dispatches with a `sync` annotation and skip onChange for annotated transactions, so only real user edits set dirty. Addresses Codex review on #175. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…xclude closed sessions
P1 (security): `safe.directory=*` re-enables trusting a foreign-owned repo's
local config, and `git diff`/`git show` without `--no-ext-diff` let such a repo
run arbitrary commands (diff.external / textconv) as the connector user. Now:
- git_diff/git_show/git_commit_files pass `--no-ext-diff --no-textconv`.
- run_git disables `core.fsmonitor` (a query hook run during index refresh) for
every invocation. (A `-c diff.external=` override is NOT used — git then tries
to exec the empty string and aborts the diff; `--no-ext-diff` is the lever.)
- New unix test proves a hostile repo-level diff.external does not execute.
P2: channel_session_workdirs now excludes closed sessions
(`status NOT IN ('terminated','revoked','expired')`), matching the session
switcher, so the root picker no longer offers stale closed-session workdirs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…runcation Self-review of the sessions PR found two issues: - BotGroup.startDrag registered pointermove/pointerup on window but never handled pointercancel. On touch, an interrupted gesture (system gesture, extra touch point, captured element unmounting) fires pointercancel instead of pointerup — so the listeners leaked AND the drag state was never cleared, leaving the source card dimmed forever and letting a stale onMove corrupt the next drag. Add a pointercancel handler (shared cleanup) that tears the drag down. Verified: after pointercancel the card is no longer dimmed, and a normal drag still promotes. - SessionChip: the two-line refactor put the tag in a nested flex row as `truncate` but dropped the `min-w-0` the old single-line row had, so a long tag wouldn't shrink/truncate and pushed the status out. Restore `min-w-0`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(sessions): group Sessions board by bot, show workdir, polish drag
fix(workspace): git dubious-ownership recovery · refetch-storm brake · session-workdir root picker
…49c9c feat(workbench): CodeMirror 6 editor for Raw mode (+ fix react-markdown v10 crash)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: be51994acf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ) -> Result<std::process::Output, (String, String)> { | ||
| tokio::process::Command::new("git") | ||
| .arg("-c") | ||
| .arg("safe.directory=*") |
There was a problem hiding this comment.
Block Git hooks when bypassing dubious ownership
When this safe.directory=* bypass is used for git_status, a foreign-owned repo can ship an executable .git/hooks/post-index-change and have it run as the connector user: the git-status docs say status refreshes and writes the index by default, and the githooks docs say post-index-change is invoked when the index is written. I also reproduced this locally with Git 2.43: git -c safe.directory=* -c core.fsmonitor=false status --porcelain=v2 --branch executed that hook unless GIT_OPTIONAL_LOCKS=0 was set, so the ownership bypass still enables arbitrary code execution during read-only status inspection.
Useful? React with 👍 / 👎.
Promotion: develop → main
Rolls up 13 commits merged into
develop(PRs #173, #174, #175 plus fixes).Sessions board
pointercancel; restore tag truncationEditors
Workspace / connector
Chat
🤖 Generated with Claude Code