Preview workdir files in-chat with a swipeable kind-aware pager#262
Merged
Conversation
…kdir Workdir file rows previewed only raster images before; everything else fell through to download. This dispatches by extension to one of seven in-app renderers and reuses the existing markdown + flutter_highlight pipeline (no new deps): images stay on Image.memory, SVG renders inside InteractiveViewer, markdown/text route through FlutterMarkdownPlusRenderer, code/html/csv wrap into a fenced code block so CodeBlockBuilder's syntax highlighter and copy button apply, and JSON is pretty-printed with a graceful fallback for malformed input. Per-kind leading icons replace the generic file icon on previewable rows. Files over 5 MB show a "too large" placeholder with a download button so the chat scroller never decodes a huge log or PDF. PDFs continue to fall through to download — preview would need a client-side library this app deliberately doesn't pull in, or a backend page-image endpoint (follow-up). The shared markdown renderer now uses ExtensionSet.gitHubFlavored so tables, strikethrough, task lists, and autolinks render in both chat and preview. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
showDialog's default fade and MaterialPageRoute's slide both add a visible flash before the preview content lands. Swap in showGeneralDialog / PageRouteBuilder with zero-duration transitions so the preview appears immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opening any row's eye icon now lands on that file with the whole run's files reachable via swipe, prev/next chevrons, keyboard arrows, and tappable dots. PDFs and unknown extensions occupy slots in the sequence but render the existing "Can't preview" state without fetching bytes, so swiping never dead-ends and never wastes a request. Per-index byte futures are cached for the page's lifetime, so swiping back is instant; the cache is GC'd on close. Title bar carries only filename + "N / M" + close X. Prev/next chevrons sit at the edges of the bottom navigation strip flanking the dots: `[<] • ● • [>]` (or `[<] [>]` when the file count exceeds the dot threshold). The strip hides entirely for single-file runs. This keeps the close button well clear of the next chevron and lets long filenames use the full title-bar width. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mation Both the workdir preview pager and the citation chunk visualization rendered nearly identical dot indicators inline. Extract a shared PagerDots widget — capped at 12 dots, tappable jump-to-index, optional per-dot tooltip — and route both consumers through it. As a side effect the citation dots become tap-to-jump, matching the workdir pager. Also fix two bugs in chunk_visualization_page: - The mobile Scaffold body had no SafeArea, so the system home indicator sat on top of the dots row and made them effectively untappable. Wrap body in SafeArea(top: false). - showDialog/MaterialPageRoute added a fade/slide transition before the rendered pages appeared. Switch to showGeneralDialog + PageRouteBuilder with zero-duration transitions, matching the workdir preview's open behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eader Workdir preview rows and the citation rows both wanted an eye icon that opens a preview. Workdir had an inline InkWell+Tooltip+Icon block; citations had a TextButton.icon with the label "View in PDF" tucked inside the expanded body. Extract a shared PreviewIconButton mirroring CopyButton's shape (bare icon, hover tooltip, no chrome) and route both consumers through it. For the citation row, the affordance moves up to the header alongside the copy button: visible without expanding the row, and consistent with workdir preview's row chrome. The label "View in PDF" is gone; the tooltip "View source PDF" carries the meaning on hover. Citation tests follow accordingly — they assert the tooltip instead of the button text and no longer need to expand a row to find the affordance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Type-design and consistency fixes from the multi-agent PR review. PreviewKind now owns its own row icon, render-eligibility, and highlight-language mappings as enum methods. The previous structure kept three parallel lookup tables (a code-extension Set in preview_kind, a language Map in code_extensions, and the local _leadingIconFor / _canPreview helpers in workdir_files_section) that could drift independently. The duplicate _extensionOf in workdir_files_section — which silently skipped the toLowerCase that preview_kind's version performs — is gone with it. code_extensions.dart deleted; languageForExtension folded into PreviewKind.highlightLanguageFor(filename). DownloadOutcome moves to its own file under workdir_preview/ so too_large_preview.dart no longer reaches back into the section file. The byte cache (and retry-token map) in WorkdirPreviewPage are now keyed by WorkdirFile (which has proper == / hashCode) rather than by list index, so any future list reordering or mutation can't alias bytes across files. show() asserts files.isNotEmpty and clamps initialIndex into range so an out-of-bounds caller can't crash the first build. Other tidying: - PreviewIconButton: BorderRadius.circular(4) → soliplexRadii.sm (design-system rule); Semantics reports enabled per onTap. - Diagnostic logging at every previously-silent catch site and errorBuilder in the preview path — image and SVG decode failures, download contract violations, the section's listing FutureBuilder, and the JSON pretty-print fallback (info level). - Tokenized the new title-bar EdgeInsets. - Removed the "per Jaemin's call" attribution comment and the inaccurate [codeExtensions] doc reference flagged in review. Tests for the renamed/refactored APIs updated in lockstep. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New coverage for behaviors that had real branches with no test: - SvgPreview decode failure: garbage bytes should swap in the fallback widget (the post-frame setState path was untested). - TooLargePreview state machine: success / failed / cancelled / throwing / in-flight reentry. The widget carries its own copy of the icon-swap pattern, so the section's tests don't cover it. - WorkdirPreviewPage: retry-twice on the same slide must clear the cache and re-run fetch each time (a regression that reused the failed future would leave the count at 1 across all retries). - WorkdirPreviewPage: bytes exactly at the 5 MB cap still preview (the cap is a strict `>`, easy to regress to `>=`). - fitFilenameForWidth: pure-logic unit tests for the extension- preserving truncation, including the dotfile and long-extension fallback branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UTF-8 decoding moves up into _PreviewBody so the leaf renderers (TextPreview, CodePreview, SvgPreview, JsonPreview) now accept a String content rather than Uint8List bytes. PreviewKind gains an isText getter so the dispatch can decide once whether to decode. _PreviewBody asserts kind.canRender in its constructor, expressing the invariant the runtime guard in _slideFor already enforces. _CannotPreview and TooLargePreview each take a filename so their download-throw logs carry it as a structured attribute. Contract- violation catches (the three "callback threw" sites) now log at error level instead of warning, and include error.runtimeType so a refactor-introduced TypeError is grep-distinguishable from a routine IO failure. PagerDots gains const-constructor asserts on itemCount, maxVisible, and currentIndex bounds. _measure() in workdir_files_section now disposes its TextPainter to avoid the Flutter 3.16+ leak warning. Test coverage extends to the _CannotPreview download state machine (success, failed, cancelled, throwing — driven through the empty-bytes path) and to keyboard arrow nav at the first and last slides (no wrap, no exception). The previously-over-specified exhaustive rowIcon and canRender PreviewKind tests are removed. A few dartdoc fixes: PreviewKind.rowIcon no longer misdescribes its contract, highlightLanguageFor no longer references a rot-prone [CodeBlockBuilder] symbol, and a couple of class-level dartdocs trim restated-WHAT prose. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract DownloadFeedbackButton so the file-row, can't-preview, and too-large callsites share one in-flight + 2 s revert state machine. Distinguish decode-failure fallbacks (image/SVG corrupt, binary content, empty file) so the user can tell a damaged artifact from an unsupported kind. Strict UTF-8 decode on text-shaped extensions replaces silent mojibake. JsonPreview renders a banner above raw content when parsing fails. Preview-fetch logs at error with stackTrace + errorType; chunk-viz drops raw error.toString() from the user-facing body. WorkdirPreviewPage constructor asserts non-empty files and in-range initialIndex. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- DownloadFeedbackButton: route _inFlight transitions through setState so the button rebuilds disabled while a download is in flight, and gate onTap on _inFlight in addition to state. - PreviewKind: make canRender/isText exhaustive switch expressions so adding a new kind forces a deliberate decision instead of inheriting the boolean default. - Logging: drop the routine "preview bytes empty" / file-listing failure warnings to debug (the UI already surfaces them); promote workdir download failures to error with errorType/filename attributes so unexpected throws are diagnosable. - Tests: cover the image- and svg-specific corrupt fallbacks (distinct from the generic unsupported copy), the WorkdirPreviewPage.show out-of-range initialIndex clamp, and switch the PreviewIconButton null-onTap assertion to a semantics check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- workdir_controller: keep the download-failure log at warning;
the catch swallows routine SocketException/HttpException/Timeout
where error level would spam on normal network failures. Keep
the errorType/filename attributes for triage. Restores severity
parity with sibling fetchFiles/fetchBytes catches.
- DownloadFeedbackButton: move the "must not throw on routine IO"
contract onto the onDownload field doc; the catch comment points
at it. The contract belongs on the API, not buried in a catch.
- Tests: assert the download InkWell's onTap is null mid-flight —
the existing "second tap is a no-op" test is also satisfied by
the _handleTap early-return, so it doesn't prove build() wires
the disabled state. Add an exhaustive
PreviewKind.{canRender,isText} truth table so the point of the
exhaustive switch (forcing a decision per variant) is defended;
tighten the show()-clamp test comment to drop a hypothetical
race scenario.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… flag
- WorkdirPreviewPage: drop the public useDialogLayout field and
re-derive the layout from MediaQuery in build(). The flag was a
derived fact (>= tablet breakpoint) that show() computed and the
constructor exposed, letting a direct caller break the layout
invariant silently. Also drop the explicit Colors.black54 barrier
so the dialog inherits the theme's barrier color.
- DownloadFeedbackAffordance: new extension on DownloadFeedbackState
returning (icon, label). The file-row tooltip, too-large-preview
button, and _CannotPreview button all rendered the same three
icon/label pairs via duplicated switches; one source of truth keeps
the wording from drifting. The row keeps its own color switch since
that's the only field that varies per site.
- workdir_controller: add errorType to the fetchFiles and fetchBytes
catch attributes for grep parity with download. Same on the
_PreviewBody UTF-8 decode warning.
- Doc accuracy: drop the cross-private [_handleTap] reference;
rewrite previewSizeCapBytes wording (the cap is enforced post-fetch);
rewrite highlightLanguageFor doc to say only code/html/csv route
through it; flag the plaintext-markdown trade-off in TextPreview;
remove the duplicate .bashrc rule from PreviewKind.from.
- Tests pinning real product decisions:
- sub-tablet width opens the preview as a Scaffold, not a Dialog
- arrow-key release does not advance the pager a second time
- JsonPreview malformed payload renders banner AND raw CodePreview
- fitFilenameForWidth keeps an 8-char extension, drops a 9-char one
- wrapInCodeFence rounds the fence up when content ends with the
default 3-backtick run (CommonMark §4.5)
- TooLargePreview formats 1024 B as 1 KB and 1 MiB as 1.0 MB
- chunk visualization failure does not leak the raw exception text
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the same fix on WorkdirPreviewPage. The previous showDialog inherited the theme's barrier color; the showGeneralDialog migration explicitly set Colors.black54, bypassing the theme. Drop the override so both dialogs honor whatever the app theme decides. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PreviewKind(extension-based) drives kind detection, icon mapping, and language selection for the shared code-block renderer. Exhaustiveswitchon every getter keeps the dispatch table closed.Future<Uint8List>cache + retry token re-keys theFutureBuilderso swipe-back doesn't refetch and Retry can replace a failed future.DownloadFeedbackButtoncentralizes the idle → in-flight → success/error → 2s revert state machine so every download affordance (file row, fallback, too-large) behaves identically. A sharedDownloadFeedbackAffordanceextension maps state → (icon, label) so the three sites can't drift.PagerDotsandPreviewIconButtonextracted tolib/src/shared/andlib/src/modules/room/ui/. Citation PDF eye moved to the header.WorkdirPreviewPagelayout is derived fromMediaQueryinsidebuild()(dialog at ≥ tablet width, full-screenScaffoldbelow) so callers can't pass a stale flag.Test plan
flutter analyze— zero warningsflutter test— 1410 tests pass>=vs>), NotFound vs generic error split, retry invalidates cache, swipe-back doesn't refetch, UTF-8 binary fallback, cancelled-download stays idle,1024 Bformats as1 KB, content ending in 3 backticks gets a 4-backtick fence, raw exception text never leaks to chunk-viz UI, sub-tablet width opens asScaffoldnotDialog, arrow-key release doesn't double-advance.🤖 Generated with Claude Code