Skip to content

Receive capture attachments on the desktop Inbox - #698

Merged
JSdotNet merged 5 commits into
mainfrom
claude/desktop-attachment-intake-214d32
Sep 26, 2026
Merged

JSdotNet merged 5 commits into
mainfrom
claude/desktop-attachment-intake-214d32

Conversation

@JSdotNet

@JSdotNet JSdotNet commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Summary

A phone capture that names files (local ADR 0014) now arrives on the desktop Inbox as one item that carries them. The item records each file's details the moment the capture lands. It then downloads the bytes through GET /api/sync/attachments/{id} into <workspace>/_inbox/attachments/<itemId>/ and checks each one's sha256. Pictures show as thumbnails above the body, and every other file gets a row with Open. A file that failed shows its reason and a Retry button.

A failed download is recorded on that file, and the capture still counts as received. A file can never stall a sync page. A replayed page downloads nothing twice: it fetches only files never tried, and a failed file waits for Retry.

Changes

  • Inbox module
    • New InboxAttachment value object, owned by InboxItem.
      • RecordAttachments adds by id only.
      • MarkAttachmentDownloaded / MarkAttachmentFailed keep a file either on disk or failed, never both.
      • AttachmentFileName gives each file a stable, file-system-safe name.
    • Two new ports in Abstractions, IInboxAttachmentSource and IInboxAttachmentFiles. Both are optional: a head without them records the files and leaves them waiting.
    • ReceiveCaptureCommand records the files, saves the item, then downloads through InboxAttachmentDownloads. That step never throws, and it skips a file already on disk with the right sha256.
    • New RetryAttachment, ReadAttachment and OpenAttachment slices, exposed on IInboxItems.
    • ContentKindDetector: a capture that is only pictures is image; any other file makes it document.
    • RouteToBacklog hands the item's folder to the task as its AttachmentPath.
  • Infrastructure
    • New inbox_item_attachments table, additive per ADR 0006. It is written in the same transaction as the item row.
    • InboxAttachmentSyncSource: a typed client on the token pipeline, registered by AddTaskSyncClient. SyncHttp gains an overload for non-JSON bodies.
    • WorkspaceInboxAttachmentFiles, registered by AddInboxCrossContextAdapters.
      • Writes via a .partial file and a move, and cleans the .partial up on failure.
      • Reads and opens nothing outside its own folder.
      • Opens files through the shell with CreateNoWindow.
    • TaskReplicaMerge.ToCapture passes the attachment details through to the intake.
    • InboxBacklogTarget writes the attachment with EntryTextParser.WithAttachment.
  • UI: InboxItemDetail.razor shows the thumbnail strip and the file rows (Open / Retry and reason / "Waiting to download"). The styles in app.css use tokens only.
  • Devbook (.devbook/domain/inbox/)
    • An Attachment chapter and the term attachment folder in domain.md.
    • New domain.invariants.md with 7 invariants.
    • New requirements.md with 8 requirements.
    • A Capture attachments sub-feature in features.md.

Testing

  • dotnet build Backlog.sln is green, mobile head included.
  • All 23 test assemblies are green. They were run per assembly exe, because dotnet test flaps in this repo.
    • The architecture tests caught two things on the first pass, both fixed: a hand-rolled status span, now the shared Alert, and a missing CreateNoWindow.
    • One unrelated SQLite test failed once while all assemblies ran in parallel. It passed on two solo re-runs.
  • New tests:
    • Inbox module: 18 intake tests, 1 routing test and 1 save-failure test in AttachmentIntakeTests, 9 invariant tests in InboxAttachmentTests, and 4 kind-detection tests.
    • UI: 3 bUnit tests in InboxPaneTests.
    • Adapters: 1 sync-merge, 1 SQLite round-trip, 6 file-store and 1 routed-task-text test.
  • Playwright QA against the Aspire desktop harness: 5 of 5 passed. A background log monitor found no errors.
    1. A JPEG and a PDF pushed through the sync service arrive with a thumbnail and a file row. Both files are on disk with matching sha256.
    2. A capture that is only a picture reads as an image.
    3. A failed save shows its reason and Retry, and Retry recovers the file. The service refuses a capture naming a file that was never uploaded (400).
    4. A full replay re-downloads nothing: no second fetch, same file timestamps.
    5. Move to backlog hands the folder to the task.
  • node .devbook/_tools/devbook-meta/build.mjs --check and node tools/devbook/build-database.mjs --check both pass.

Verification (devbook:verify-change, after opening)

All 7 acceptance criteria and 21 of 24 chapter rows are aligned. Nothing is spec-ahead, in conflict or unresolved. Every chapter is status: draft.

Row Verdict Finding Calls for
features.md#capture-attachments code-ahead A picture is drawn as a thumbnail only when it is downloaded and at most 8 MB (InboxDesktopState.ThumbnailMaxBytes). A larger one becomes a file row. The chapter doesn't mention the cap capture-specs
Req: the detail view shows every file code-ahead Same cap; no scenario for a picture over it capture-specs
requirements.md#capture-attachments code-ahead A malformed file entry (empty id, bad sha256) is dropped silently and the capture still lands, but no requirement states this. The requirements also carry no tests links capture-specs
Invariant: capture details never change thin No test asserts that downloading or failing keeps the file's Name/Sha256 optional test
Req: kind follows files edge An older item that gets files on a replay is never reclassified decide

Related

Follow-ups, left out on purpose:

  • "Create plan" does not hand the folder to the tasks it creates yet; only "Move to backlog" does.
  • A failed download is shown on the file row but not logged, because the module has no logging dependency.
  • model.md and flow.md diagrams don't show attachments.
  • The older Inbox Item, List and Group rules are still tables in domain.md.

Checklist

  • Scope is focused and excludes unrelated changes
  • Documentation or instruction files were updated when governance or behavior changed
  • Running Aspire instances for this checkout were stopped before PR creation (build\stop-aspire-before-pr.ps1)
  • Follow-up work is captured when the implementation is intentionally partial
  • Issues this PR closes are linked with a closing keyword, or there are none

🤖 Generated with Claude Code

JSdotNet and others added 3 commits September 27, 2026 00:32
A phone capture that names files (local ADR 0014) now lands as an Inbox
item that carries them. The intake records each file's metadata and saves
the item before fetching any byte, then downloads each blob through
GET /api/sync/attachments/{id} into _inbox/attachments/<itemId>/,
sha256-verified. A failed fetch or save is recorded on that file and the
capture is still received, so a file never stalls a sync page; a replay
fetches only files never tried, and a failed one waits for Retry.

- InboxAttachment value object owned by InboxItem, persisted in the new
  additive inbox_item_attachments table (local ADR 0006)
- Ports IInboxAttachmentSource (sync GET) and IInboxAttachmentFiles
  (workspace folder), answered in Infrastructure.Sync / .FileSystem
- ContentKindDetector: pictures only is image, any other file is document
- Detail pane: thumbnails above the body, file rows with Open / Retry
- Routing hands the item's folder to the task as its AttachmentPath
- Devbook: Attachment in the inbox domain chapter, domain.invariants.md,
  requirements.md and the Capture attachments feature

Backlog plan item desktop-attachment-intake (+mobile-inbox-slice).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the two conflicts with the Inbox multi-select work (#701):
InboxErrors keeps both the attachment errors and ItemAlreadyDecided, and
requirements.md keeps both chapters (Act on several at once, then Capture
attachments) under one header. BatchTests constructs InboxItems by hand,
so it now names the three attachment handlers as unwired, the way it
already leaves the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#694 moved the Inbox aggregates' invariant tables into a new
domain.invariants.md, the same file this branch created for the
attachment invariants. The merged page keeps main's header and its 22
invariants, and adds the 7 attachment invariants to the Inbox Item
chapter. domain.md keeps this branch's related and tests lists, which
include main's, and no longer says the older rules are still in a table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JSdotNet
JSdotNet enabled auto-merge September 26, 2026 23:15
…ment-intake-214d32

# Conflicts:
#	src/Modules/Inbox/Backlog.Modules.Inbox.UI/InboxDesktopState.cs
#	src/Modules/Inbox/Backlog.Modules.Inbox/Extensions/InboxModuleRegistration.cs
#	src/Modules/Inbox/Backlog.Modules.Inbox/Services/InboxItems.cs
…ment-intake-214d32

# Conflicts:
#	src/Modules/Inbox/Backlog.Modules.Inbox.Abstractions/DataTransferObjects/InboxCaptureDto.cs
#	src/Modules/Inbox/Backlog.Modules.Inbox/Features/ReceiveCapture/ReceiveCaptureCommand.cs
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.

1 participant