Skip to content

compose: a message can carry several files, not just the first one dropped - #82

Merged
nixfred merged 1 commit into
nixfred:mainfrom
zachwilke:cursor/multi-file-sends-fe38
Sep 15, 2026
Merged

nixfred merged 1 commit into
nixfred:mainfrom
zachwilke:cursor/multi-file-sends-fe38

Conversation

@zachwilke

Copy link
Copy Markdown
Contributor

Split out of #47 (third of five, as requested). Rebased onto current main (2.5.0). Independent of the UTC and GIF PRs.

What

A dropped, pasted, or /attached set of files all stay on the draft. They ship one part per file, caption on the first only, capped at ten. A failing part stops the batch and leaves the rest attached.

Why

Dropping five photos attached one and discarded four in silence: the handler read urls[0] and the draft was a single path. A drop that loses most of what you dropped, with no message, is worse than one that refuses.

Sends go through the existing send-file.ts path (unchanged: still one file per invocation, still stdin, still refusing groups whose guid is unknown). The service is captured when the batch starts, never re-read from root.active per part, so switching threads mid-send cannot push a later part onto a different service (war room #2).

Chips are one per row, for the implicit-width reason the received ones are.

main already uses drafts for the per-conversation text draft map, so the attachment list is attachDrafts / attachCount / addAttachment(s) / removeAttachment / clearAttachments.

How it was verified

  • bun test is green (new QML invariants for drop-all-files, the cap, caption-on-first, one-chip-per-row, service capture)
  • Any send/receive behavior was exercised against my own number only — never a contact or a group
  • UI change → screenshot attached
  • Touches an invariant in CLAUDE.md → named below, and the doc updated if it changed

invariants touched: Never send to a handle for a group (still --chat-id / --to via send-file.ts); Message text never rides argv (caption still --caption-stdin); Never let one delegate's implicit width exceed the panel (chips one per row).

Rebase notes

pumpFileSend sits next to current-main's richer copyProc (clipboard feedback). The QML tests that used to slice to Process { id: copyProc } now slice to function copyText, because copyProc is no longer a one-liner.

Supersedes the multi-file slice of #47 (20f8bc1).

…opped

Dropping five photos attached one and discarded four in silence — the drop
handler took urls[0] and draftPath was a single string. A drop that loses
most of what you dropped, with no message, is worse than one that refuses.

Drafts are a list now. Drag-and-drop queues every file it was given, /attach
and Ctrl+V append rather than replace, and each chip removes only itself.
Sends go out one part per file in queue order through the existing
send-file.ts path (unchanged: still one file per invocation, still stdin,
still refusing groups whose guid is unknown). The caption rides the first
part alone, or five files would post the same sentence five times.

Two things a batch has to survive. The service is captured when it STARTS,
never re-read from root.active per part, so switching threads mid-send cannot
push a later part onto a different service (war room #2). And a failing part
stops the batch, leaves the remaining files attached and says how many are
left, instead of making the user work out which of five actually went.

Capped at ten files: a stray drop of a whole folder should be refused, not
turned into eighty sends. Chips are one per row for the reason the received
ones are — a RowLayout of N sums implicit widths and drags the conversation
column past the panel edge, taking every right-aligned element with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xxS9WCT7ifHhbDjYyGnHS

Co-authored-by: zachwilke <zachwilke@users.noreply.github.com>
@zachwilke
zachwilke requested a review from nixfred as a code owner September 14, 2026 14:18
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-14T14:21:32.976671Z 1df07c8 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1df07c8ea1

ℹ️ 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".

Comment thread BlipView.qml
Comment on lines +1013 to +1014
function removeAttachment(path) {
root.attachDrafts = root.attachDrafts.filter(function(d) { return d.path !== String(path) })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove deleted files from the active send queue

While the first part is in flight, clicking ✕ on a later attachment only removes it from attachDrafts; the fileQueue snapshot still contains that path, so pumpFileSend() subsequently transmits a file the user explicitly removed. Either remove not-yet-started paths from fileQueue as well or disable removal for the active batch.

AGENTS.md reference: AGENTS.md:L8-L10

Useful? React with 👍 / 👎.

Comment thread BlipView.qml
Comment on lines +1481 to +1484
if (root.fileQueue.length === 0) return
var path = root.fileQueue[0]
root.fileQueue = root.fileQueue.slice(1)
root.sendDraftPath = path

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move the file-send state machine into tested TypeScript

This adds batching decisions—queue advancement, ordering, caption consumption, and retry state—directly to QML, while the new tests only search the QML source for strings and cannot execute these transitions. Extract this state machine into TypeScript and unit-test success, failure, removal, and navigation transitions, leaving QML to render and dispatch effects as required by the repository.

AGENTS.md reference: AGENTS.md:L17-L19

Useful? React with 👍 / 👎.

Comment thread BlipView.qml
Comment on lines +1370 to +1374
addAttachment(p)
composeField.text = ""
note = "attached — type a caption or press Enter to send"
note = root.attachCount > 1
? root.attachCount + " files attached — caption, or Enter to send"
: "attached — type a caption or press Enter to send"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the attachment-limit error for /attach

When ten files are already queued, addAttachment(p) reports the limit and returns without adding the requested path, but these following assignments clear the command and overwrite that error with “10 files attached.” The user is therefore told the command succeeded and may send a batch that silently omits the requested file; only clear the field and show the success note when an attachment was actually added.

Useful? React with 👍 / 👎.

@nixfred

nixfred commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Merged, thanks @zachwilke! every dropped file goes out, with the caption on the first one. Fixing the chat and service when the batch starts closes the switch-threads-mid-send hole. It's in main as of 186b536 and deployed on my machine. I merged it together with nine other PRs, and all 526 tests pass on the combined tree.

nixfred added a commit that referenced this pull request Sep 16, 2026
WHAT: CHANGE.log is a running, newest-first log of every change since the last
X post, each entry naming the person with real name, GitHub login and their
X account as a https://x.com/<handle> link. X-POSTS.md is the queue of posts
for @nixfred: Post 1 (CONTRIBUTORS-X-POST.md, 2026-09-11) marked posted,
Post 2 drafted covering the 19 merged PRs since (#54 #71-#77 #80-#82 #84 #85
#87 #88 #92-#95) plus 4bad82d and 481e555.
WHY: Fred's standing rule 2026-09-16. Handles were missed before; a thank-you
without the @ never reaches the person, and a wrong @ tags a stranger.
HANDLES: verified @jondkinney (GitHub profile), @zachwilke_1 (pinefall.dev),
@bbishdotdev (bbish.dev + name), @mwhuss (name + Omarchy posts replying to
@nixfred). @jefehoser likely (same login, Omarchy posts), flagged for Fred.
Ian Swope and cjoh: none verified, recorded as checked.
.gitignore: *.log swallowed CHANGE.log, so it is re-included explicitly.

LR-T: blip, changelog, x-posts, contributors, handles
LR-D: blip, docs, community
LR-K: change-log-verified-x-handles, x-post-queue

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ainpzHJ5F7nsV4hB6HpZy
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