Skip to content

feat: panel side setting, hidden-file browsing, media previews, and video attachments - #8047

Open
HEREISCB wants to merge 2 commits into
pingdotgg:mainfrom
HEREISCB:feat/panel-side-hidden-files-video-attachments
Open

feat: panel side setting, hidden-file browsing, media previews, and video attachments#8047
HEREISCB wants to merge 2 commits into
pingdotgg:mainfrom
HEREISCB:feat/panel-side-hidden-files-video-attachments

Conversation

@HEREISCB

@HEREISCB HEREISCB commented Aug 24, 2026

Copy link
Copy Markdown

What changed

1. Panel side preference (Settings → Appearance → "Panel side")

  • New rightPanelSide client setting in packages/contracts (synced on web and desktop, searchable in settings search).
  • Setting it to Left docks the tool panel (files, terminal, diff, browser preview, agents) on the left and moves the chat column to the right.
  • The resize handle, panel border, maximize control, and titlebar button reservations all follow the chosen side.
  • Inside the Files surface, the tree mirrors too: docked left, the explorer hugs the outer edge and files open toward the chat.

2. Hidden files in the workspace file browser

  • The file tree is fed by the native search index, which bakes in .gitignore — so ignored files and some dotfiles could never be shown, and the indexer exposes no option to include them.
  • Added an eye toggle in the Files tab header ("Show hidden and ignored files") backed by a new includeHidden flag on projectsListEntries: the server walks the filesystem directly (breadth-first, .git and node_modules still skipped, same 25k entry cap). Ships with a server test.

3. Video/audio preview in the Files panel

  • Selecting an .mp4/.mov/.webm/.m4v used to fail the binary-file check; media files now render inline players via signed asset URLs (audio too: mp3, wav, ogg, flac, m4a). Extension allowlists extended in packages/shared/filePreview and the asset access layer.

4. Video attachments in chat

  • New ChatVideoAttachment in the wire contract: MP4/MOV/WebM up to 25 MB per file (kept as a one-shot data-URL upload, same transport as images).
  • Composer accepts drop/paste/pick, shows a first-frame chip; the timeline renders an inline <video> player that persists with the thread.
  • No provider ingests raw video, so each adapter passes the stored file's absolute path as a text part (OpenCode gets a real file part) — agents can then work on the file with their tools. Videos are skipped in localStorage draft persistence (too large) and show the existing "may not persist" badge.
  • Mobile renders video attachments as a plain tile for now (decision: not supported there yet).

Bugs found and fixed along the way

  • Revert prune deleted non-image attachments from disk: the keep-set builder in ProjectionPipeline skipped any attachment whose type wasn't "image", so any future non-image attachment was deleted on the next thread revert. The skip is removed; attachmentRelativePath now covers every attachment type.
  • A file dropped outside the chat column navigated the whole app to the file (Chromium default): a dropped video would play full-window and then "disappear". A window-level dragover/drop preventDefault guard in the root route suppresses the default without interfering with real drop targets.
  • Left-docked panel clipped under floating titlebar controls: the collapsed-sidebar toggle and the maximize/panel cluster overlap the top corners; the tab bar and chat header now reserve space based on which side the panel is docked.

Notes / known ceilings

  • Videos over 25 MB are refused with a clear error; chunked upload is the path if larger recordings are needed.
  • Video seeking before full buffer is limited (asset endpoint doesn't serve range requests yet).
  • Verified: typecheck across contracts/shared/server/web/desktop/client-runtime; focused tests for WorkspaceEntries, attachmentStore, imageMime, Normalizer, ProjectionPipeline, AssetAccess, the five provider adapters, composer draft store, and desktop client settings all pass. Tested live in the web app on macOS.

Note

Medium Risk
Touches attachment persistence, provider adapters, asset signing, CSP, and a new filesystem walker for hidden files. Video uploads are larger (25 MB data URLs) and skip client compression/draft persistence.

Overview
Adds video chat attachments (MP4/MOV/WebM, 25 MB), a panel-side setting, hidden-file browsing, and inline video/audio previews in Files.

Composer drop/paste now accepts videos; they persist on the server and play in the timeline. Providers do not ingest raw video — adapters send a text note with the stored file path so the agent can use tools. Videos skip localStorage drafts. Revert prune now keeps non-image attachments on disk.

Settings → Appearance → Panel side docks the tool panel left or right (resize handle, borders, titlebar insets, and the Files explorer follow). Files gets an eye toggle that walks the filesystem (includeHidden) so gitignored and dotfiles show (still skipping .git / node_modules). Selecting video/audio in Files plays via signed asset URLs; desktop CSP adds media-src. Window-level drop preventDefault stops Chromium from navigating away on stray file drops.

Reviewed by Cursor Bugbot for commit c176294. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add video attachments, panel side setting, hidden-file browsing, and media previews

  • Adds end-to-end video attachment support: composer accepts and validates videos, contracts define ChatVideoAttachment with size/MIME limits, server normalizer persists them, and provider adapters (ClaudeAdapter, CodexAdapter, CursorAdapter, GrokAdapter) send videos as text content blocks with the stored file path.
  • Adds a rightPanelSide client setting ('left' | 'right', default 'right') with an Appearance settings control; PreviewPanelShell, RightPanelTabs, WorkspacePageHeader, and resize handles adapt borders, insets, and drag edge to the chosen side.
  • Adds hidden-file browsing: FileBrowserPanel gains a persisted showHidden toggle, WorkspaceEntries.list branches on includeHidden to use a bounded BFS walker (HIDDEN_WALK_MAX_ENTRIES 25000, skips .git and node_modules), and ProjectListEntriesInput accepts includeHidden.
  • Adds inline media previews in FilePreviewPanel for images, videos, and audio via <img>, <video>, and <audio> elements; CSP media-src updated to allow blob and signed-asset URLs.
  • Fixes blob URL revocation to cover all attachment types, excludes videos from the expanded image gallery, and prevents browser navigation on out-of-target file drops.
  • Risk: WorkspaceEntries.list with includeHidden: true performs direct filesystem BFS and reports truncated beyond 25,000 entries; node_modules and .git are always skipped. Video attachments are not persisted to local drafts and rely on <video> element buffering without client-side preload.

Macroscope summarized c176294.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f8187b80-4976-4a26-a6e2-b26d93d3bd84

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 24, 2026
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/server/src/orchestration/Normalizer.ts Outdated
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

UI consistency review found one regression in the attachment lightbox (two call sites) and one toggle-affordance inconsistency in the new Files header control. The panel-side layout work (PreviewPanelShell, RightPanelResizeHandle, RightPanelTabs, FilePreviewPanel explorer side, titlebar-inset conditions) looks coherent: borders, resize edge, titlebar insets and the collapsed-sidebar inset are all switched together, and the new settings row matches the neighbouring Select row exactly.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/files/FileBrowserPanel.tsx
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
Comment thread apps/web/src/components/chat/ChatComposer.tsx
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
@macroscopeapp

macroscopeapp Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR introduces multiple cross-cutting user-facing capabilities, including video uploads, media playback, filesystem walking, and configurable panel layout, with substantial server, client, and desktop changes. Unresolved substantive findings also affect attachment handling, filesystem errors, and settings restoration, so the changes need human review.

Not approved because:

  • 3 blocking correctness issues found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

UI consistency review of the changed web surfaces. Two findings, both in the new video-attachment / left-dock paths. The rest (panel side plumbing in PreviewPanelShell / RightPanelResizeHandle / FilePreviewPanel, the Button-based hidden-files toggle, the settings Select row) follows the existing primitive and Tailwind ownership conventions.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/ChatComposer.tsx
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
Comment thread apps/web/src/components/ChatView.tsx
@HEREISCB
HEREISCB force-pushed the feat/panel-side-hidden-files-video-attachments branch from 1e1fbd1 to 66bc1ae Compare August 24, 2026 02:36

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

UI consistency review of the panel-side / hidden-files / video-attachment changes. Two findings: a desktop CSP gap that blocks the new <video>/<audio> playback in the Electron renderer, and an icon-geometry mismatch in the new Files-tab toggle. The earlier lightbox and header-inset findings look resolved.

Posted via Macroscope — UI Consistency

onError={() => setFailedUrl(assetUrl.url)}
/>
) : props.kind === "video" ? (
<video

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These are the app's first <video>/<audio> elements, and the desktop window's CSP declares no media-src: makeDesktopContentSecurityPolicy emits default-src 'self' plus an explicit img-src 'self' <scheme>: blob: data: http: https: (apps/desktop/src/electron/ElectronProtocol.ts), so media falls back to default-src 'self'. The signed asset URL here lives on the environment's http(s) origin, so in the desktop app the fetch is refused before a byte moves and this surface renders "Unable to load workspace video" — while the sibling <img> branch works because img-src enumerates those schemes. The same applies to the blob-backed previews added in ChatComposer and MessagesTimeline (blob: does not match 'self'); PullRequestMarkdown already documents this exact constraint as the reason it links out instead of playing.

Consider adding a matching media-src directive next to img-src in makeDesktopContentSecurityPolicy (and the corresponding assertion in ElectronProtocol.test.ts) so video/audio resolve from the same sources images already do.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/files/FileBrowserPanel.tsx Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

UI consistency review of the panel-side / hidden-files / video-attachment changes. Two findings: a desktop CSP gap that blocks the new <video>/<audio> playback in the Electron renderer, and an icon-geometry mismatch in the new Files-tab toggle. The earlier lightbox and header-inset findings look resolved.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/files/FileBrowserPanel.tsx Outdated
onError={() => setFailedUrl(assetUrl.url)}
/>
) : props.kind === "video" ? (
<video

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These are the app's first <video>/<audio> elements, and the desktop window's CSP declares no media-src: makeDesktopContentSecurityPolicy emits default-src 'self' plus an explicit img-src 'self' <scheme>: blob: data: http: https: (apps/desktop/src/electron/ElectronProtocol.ts), so media falls back to default-src 'self'. The signed asset URL here lives on the environment's http(s) origin, so in the desktop app the fetch is refused before a byte moves and this surface renders "Unable to load workspace video" — while the sibling <img> branch works because img-src enumerates those schemes. The same applies to the blob-backed previews added in ChatComposer and MessagesTimeline (blob: does not match 'self'); PullRequestMarkdown already documents this exact constraint as the reason it links out instead of playing.

Consider adding a matching media-src directive next to img-src in makeDesktopContentSecurityPolicy (and the corresponding assertion in ElectronProtocol.test.ts) so video/audio resolve from the same sources images already do.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/web/src/components/ChatView.tsx
…ideo attachments

Adds a Settings > Appearance 'Panel side' preference to dock the tool
panel (files, terminal, diff, preview) on the left with chat on the
right; a Files-panel toggle that lists gitignored and dot-prefixed
entries via a direct filesystem walk; inline video/audio preview for
workspace files; and end-to-end video attachments in chat (stored with
the thread, played inline, handed to providers as a file path). Also
fixes the revert prune deleting non-image attachments from disk, stops
a file dropped outside the composer from navigating the app, and
includes review-bot fixes for mixed image/video blob lifecycle, the
image lightbox, video MIME validation, and left-dock titlebar insets.
… media CSP, eye icon size

- Advance the handoff preview index for every attachment so mixed
  video+image messages pair blob URLs correctly.
- Title video-only first sends 'Video: <name>' instead of 'Image:'.
- Allow media-src in the desktop CSP so video/audio previews play in
  the Electron app.
- Match the hidden-files eye icon size to the neighbouring refresh
  glyph.
@HEREISCB
HEREISCB force-pushed the feat/panel-side-hidden-files-video-attachments branch from 66bc1ae to c176294 Compare August 24, 2026 03:47
let dirents: ReadonlyArray<{ readonly name: string; isDirectory(): boolean }>;
try {
dirents = await NodeFSP.readdir(join(root, relativeDir), { withFileTypes: true });
} catch {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium workspace/WorkspaceEntries.ts:160

When the normalized workspace root is unreadable or disappears, includeHidden listing returns a successful empty entries array instead of reporting WorkspaceEntriesReadDirectoryError (or the corresponding index failure). The catch at line 160 also handles the root directory, so distinguish the root read from nested-directory reads and propagate the root failure.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/workspace/WorkspaceEntries.ts around line 160:

When the normalized workspace root is unreadable or disappears, `includeHidden` listing returns a successful empty `entries` array instead of reporting `WorkspaceEntriesReadDirectoryError` (or the corresponding index failure). The catch at line 160 also handles the root directory, so distinguish the root read from nested-directory reads and propagate the root failure.

Comment thread docs/user/composer.md
Comment on lines +26 to +27
message. Up to 8 files per message; images over 10 MB are compressed to fit, videos can be up to
25 MB. Attached videos play inline in the conversation. Coding agents cannot watch video directly,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low user/composer.md:26

Images over 10 MB can still be rejected when compressImageToByteLimit returns too_large, so the documentation's claim that they are compressed to fit is incorrect. Describe compression as an attempt and note that very large or poorly compressible images may still be refused.

Suggested change
message. Up to 8 files per message; images over 10 MB are compressed to fit, videos can be up to
25 MB. Attached videos play inline in the conversation. Coding agents cannot watch video directly,
message. Up to 8 files per message; images over 10 MB are compressed when possible, but very large or poorly compressible images can still be refused; videos can be up to
25 MB. Attached videos play inline in the conversation. Coding agents cannot watch video directly,
🤖 Copy this AI Prompt to have your agent fix this:
In file @docs/user/composer.md around lines 26-27:

Images over 10 MB can still be rejected when `compressImageToByteLimit` returns `too_large`, so the documentation's claim that they are compressed to fit is incorrect. Describe compression as an attempt and note that very large or poorly compressible images may still be refused.

for (const file of acceptedFiles) {
if (file.type.startsWith("video/")) {
// Videos ship byte-for-byte; there is no client-side recompression.
nextImages.push({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High chat/ChatComposer.tsx:2535

Videos are treated as images when restored from the prompt stash, so a stashed video is dispatched with type: "image" and its video MIME type; larger videos are also cleared despite failing image compression. The stash flow must exclude videos or persist and hydrate their type and data end-to-end.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/ChatComposer.tsx around line 2535:

Videos are treated as images when restored from the prompt stash, so a stashed video is dispatched with `type: "image"` and its video MIME type; larger videos are also cleared despite failing image compression. The stash flow must exclude videos or persist and hydrate their type and data end-to-end.


for (const attachment of input.attachments ?? []) {
if (attachment.type !== "image") {
// Claude ingests no raw video; hand the agent the stored file's path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High Layers/ClaudeAdapter.ts:1268

A non-image attachment is silently omitted when resolveAttachmentPath returns null, so sendTurn succeeds without sending the attachment or notifying either the user or agent. Unlike the image branch, this path does not validate or read the file; return ProviderAdapterRequestError for an invalid path and propagate filesystem read failures before adding the prompt.

Also found in 1 other location(s)

apps/server/src/provider/Layers/GrokAdapter.ts:975

The new video branch returns a text prompt immediately without checking that attachmentPath exists or is readable. resolveAttachmentPath only performs path normalization/containment and does not touch the filesystem, whereas the image branch's fileSystem.readFile surfaces missing-file errors. If attachment metadata survives while its stored video is missing or inaccessible, sendTurn succeeds and tells Grok the file is available at a nonexistent path, leaving the agent unable to process the attachment instead of returning a clear request error.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/ClaudeAdapter.ts around line 1268:

A non-image attachment is silently omitted when `resolveAttachmentPath` returns `null`, so `sendTurn` succeeds without sending the attachment or notifying either the user or agent. Unlike the image branch, this path does not validate or read the file; return `ProviderAdapterRequestError` for an invalid path and propagate filesystem read failures before adding the prompt.

Also found in 1 other location(s):
- apps/server/src/provider/Layers/GrokAdapter.ts:975 -- The new video branch returns a text prompt immediately without checking that `attachmentPath` exists or is readable. `resolveAttachmentPath` only performs path normalization/containment and does not touch the filesystem, whereas the image branch's `fileSystem.readFile` surfaces missing-file errors. If attachment metadata survives while its stored video is missing or inaccessible, `sendTurn` succeeds and tells Grok the file is available at a nonexistent path, leaving the agent unable to process the attachment instead of returning a clear request error.

The Files tab lists your workspace. The listing follows your `.gitignore`, so ignored files and
some dotfiles are hidden by default. Use the eye toggle in the Files tab header to show hidden and
ignored files (`.git` and `node_modules` stay hidden). Selecting an image, video, or audio file
previews it inline; other files open as text.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low user/workspace-panel.md:12

Selecting .htm, .html, or .pdf files opens the browser preview rather than the documented text view. The final sentence says every non-media file opens as text, so it should explicitly mention these browser-previewed formats.

Suggested change
previews it inline; other files open as text.
previews it inline; `.htm`, `.html`, and `.pdf` files open in the browser preview; other files open as text.
🤖 Copy this AI Prompt to have your agent fix this:
In file @docs/user/workspace-panel.md around line 12:

Selecting `.htm`, `.html`, or `.pdf` files opens the browser preview rather than the documented text view. The final sentence says every non-media file opens as text, so it should explicitly mention these browser-previewed formats.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c176294. Configure here.

}
/>
) : null}
<SettingsRow

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restore omits panel side

Medium Severity

The new rightPanelSide setting has a per-row reset on Appearance, but global Restore defaults never lists or resets it. Other Appearance prefs such as wordWrap and glassOpacity are included, so a left-docked panel survives a full restore while the confirmation dialog implies a complete reset.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by learned rule: Restore defaults on General settings page is intentionally global — not scoped per-page

Reviewed by Cursor Bugbot for commit c176294. Configure here.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One finding: the new window-level drag/drop preventDefault in apps/web/src/routes/__root.tsx cancels the browser default for every drag, not just file drags, which suppresses native text drops into the app's <textarea>/<input> surfaces. Details inline.

The panel-side, hidden-files toggle, and media-preview changes reviewed in earlier runs look addressed: the desktop CSP now declares media-src, the eye icon is sized at the call site to match the neighbouring icon-xs refresh button, buildExpandedImagePreview excludes videos from the <img> gallery, and the chat header's collapsed-sidebar inset is now opt-out via reserveCollapsedSidebarInset when the panel is docked left. Side-aware geometry in PreviewPanelShell / RightPanelResizeHandle / FilePreviewPanel (border edge, resize edge, flex-row-reverse) is consistent with the useResizableWidth edge contract.

Posted via Macroscope — UI Consistency

Comment on lines +101 to +103
const preventWindowFileDrop = (event: DragEvent) => {
event.preventDefault();
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This cancels the default action for all drags, not just file drags. preventDefault() from a window listener still applies after the target's own handler runs, so drags the target intentionally leaves to the browser lose their default too — dropping selected text into a native <textarea>/<input> (PR description editor, diff/review comment fields) no longer inserts anything, and every non-target area now reports a valid drop target on dragover.

Consider gating on the file payload, which is the only case that triggers the navigation this guards against:

Suggested change
const preventWindowFileDrop = (event: DragEvent) => {
event.preventDefault();
};
const preventWindowFileDrop = (event: DragEvent) => {
// Only file drags navigate; leave text drags to their drop target's default.
if (!event.dataTransfer?.types.includes("Files")) return;
event.preventDefault();
};

Posted via Macroscope — UI Consistency

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

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant