Skip to content

feat(windows): Phase 7 follow-ups (Parts 1–4)#115

Merged
Aryansharma28 merged 13 commits into
mainfrom
feat/windows-channels-followup-part1
Jun 14, 2026
Merged

feat(windows): Phase 7 follow-ups (Parts 1–4)#115
Aryansharma28 merged 13 commits into
mainfrom
feat/windows-channels-followup-part1

Conversation

@Aryansharma28

@Aryansharma28 Aryansharma28 commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Lands all four follow-up parts from the 2026-06-13 Phase 7 audit into `main` as one bundle. Parts 2, 3, and 4 were merged into this branch in flight, so this PR is the integration target.

Part 1 — Agent runtime wiring

Part 2 — Chat UX completeness

Part 3 — Chat polish

Part 4 — Late-May macOS board features the audit missed

Closes

Closes #106, closes #107, closes #108, closes #109, closes #110, closes #111, closes #112, closes #113, closes #114

Verify

  • `cd windows && npm run build` — green
  • `cd windows/src-tauri && cargo build` — green (only pre-existing warnings)
  • `cargo test` — 58 passed

Known follow-ups (NOT in this PR)

  • Self-compact generation loop (needs Claude Code statusline emitting context-usage JSON on Windows)
  • Channel reorder (macOS `BoardStore.reorderChannel`)
  • Phase 8 architectural gaps from latest parity audit: embedded browser tabs, multi-assistant (Codex/Gemini) adapters, image-paste pipeline, Process Manager view, Session History view checkpoints, APIService entity

🤖 Generated with Claude Code

Aryansharma28 and others added 12 commits June 14, 2026 00:14
Closes the gap in the 4-event channels watcher contract: writes to
drafts.json now emit a DRAFTS_CHANGED event, and the frontend store
refetches via get_drafts. This lets a second app instance — or an
external edit — pick up draft changes within ~100ms instead of being
stuck on the in-memory copy until the next save_drafts call.
The chat panel renders a Direct Messages section alongside Channels.
Threads from list_dm_pairs are shown in the sidebar; selecting one
loads via read_dm_messages and shows the same thread layout as a
channel. Sends go through send_dm with from=SELF; drafts and
read-state are persisted under their respective dms.<pairKey> slots,
mirroring the channel flow.

Adds a Start DM affordance that accepts an @handle or card_<ksuid>,
plus the parsePartyKey / dmPairKey / otherPartyOfPair helpers needed
to map between the on-disk sorted-pair-key format and ChannelParticipant.
Channel and DM message events now fire an OS notification (via
tauri-plugin-notification) and a Pushover push (via the existing
pushover module) when:

  - SELF is a channel member, or SELF is one of the DM pair parties
  - The message isn't from SELF
  - The message kind is "message" (system/join/leave are skipped)
  - The chat panel isn't open on that exact thread (foreground = skip)

A per-thread 2s debounce coalesces bursts: 5 agent messages within
2s fire one notification, not five. The frontend store does the
"should we notify?" decision; the new notify_chat_message Tauri
command reuses the same dispatch path the card-finish notifications
already use, so settings toggles + Pushover creds work the same way.
Previously read_jsonl loaded the whole log file into RAM and then dropped
the head when a limit was given — memory O(file size) regardless of n.

Adds read_jsonl_tail: walks back from EOF in 64 KB chunks, splitting on
'\n' and stitching the trailing partial line across chunks via a
`pending` buffer that grows naturally when a single line is bigger than
the chunk. Memory is O(n × avg_line) for typical traffic.

The None-limit path is unchanged. Behavior for read_channel_messages /
read_dm_messages is preserved: lenient parsing, file-not-found → empty.

Verified on a synthetic 1M-line / 125 MB jsonl: tail(20) completed in
708 µs (well under the O(ms) acceptance bar). Eight new unit tests
cover empty, missing, fewer-than-n, exact, many-more, oversized single
line (200 KB), corrupt-line tolerance, chunk-boundary stitching, and
agreement with the whole-file path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The backend image-storage path (persist_images + image_paths field) shipped
in Phase 7 but nothing on either surface produced non-null imagePaths. This
wires it up.

Frontend (Channels.tsx + channelsStore.ts):
- Paperclip button opens the @tauri-apps/plugin-dialog file picker
  (multiple, scoped to common image extensions).
- Tauri webview onDragDropEvent collects dropped paths when the drop
  position lands inside the compose region.
- Paste handler iterates ClipboardEvent.clipboardData.items; image MIME
  blobs are pushed to a new persist_clipboard_image command which writes
  them to a uniquely-named file in the system temp dir and returns the
  path.
- Queued attachments render as 64×64 thumbnails above the textarea with
  per-item × buttons.
- MessageRow renders message.imagePaths as 160-wide thumbnails; clicking
  opens the file via @tauri-apps/plugin-shell.
- sendMessage accepts an optional imagePaths array; passes through to
  send_channel_message.

Backend (lib.rs):
- read_image_bytes returns raw bytes for blob-URL rendering. 25 MB cap
  to avoid OOM if a bogus path is ever requested.
- persist_clipboard_image writes pasted bytes to
  $TEMP/kanban-code-clipboard/<uuid>.<ext> and returns the path; the
  existing persist_images downstream copies it to the message's final
  location at send time.

CLI (bin/kanban.rs):
- Adds repeatable -i/--image <PATH> on `channel send` and `dm send`.
- Pre-validates paths and prints a non-fatal warning + skip for missing
  files (matches the lenient persist_images contract).

Verified: cargo build + npm run build green; `kanban channel send --help`
shows the new flag; lenient skip behavior confirmed with mixed real/missing
input.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…113)

Adds the four chat affordances messages need to feel real, all using the
append-only JSONL strategy the macOS Swift app uses — no rewrite of
existing rows, so loading a pre-#113 channel still parses cleanly.

Schema (channels.rs):
- New MessageType variants Edit, Delete, Reaction. Existing Message/Join/
  Leave/System untouched.
- New MessageRefs struct with optional editsMessageId / reactionTo / emoji
  carries the cross-row pointer.
- ChannelMessage gets optional `refs` and `mentions` fields. Both
  skip_serializing_if = "Option::is_none", so legacy rows round-trip
  without phantom fields.
- extract_mentions(body): pure helper used at send time to populate
  mentions from the literal "@handle" tokens in the body.

Store + Tauri commands:
- edit_channel_message / delete_channel_message / react_channel_message
  and DM equivalents append the appropriate row with refs populated.
- send_message / send_dm now also populate mentions automatically.

Render-time collapse helper (src/lib/messageCollapse.ts):
- Pure module so the channels view AND the future DM view share one
  pipeline. Walks raw messages once, builds the rendered view: applies
  latest Edit per id, marks Delete, aggregates Reaction rows with toggle
  semantics (odd count per (target, emoji, sender) = on).
- tokenizeBody splits body into text + mention spans for styled rendering.

UI (Channels.tsx):
- MessageRow grew hover affordances: react, edit (own), delete (own).
  Inline edit mode replaces the body with a textarea; Save / Cancel /
  Esc behave naturally.
- Reaction picker is a small static emoji palette (👍 ❤️ 😄 🎉 😢 😮 🙏);
  chips show counts and highlight when SELF has reacted; click toggles.
- Deleted messages render as a "(message deleted)" stub so the list
  doesn't reflow.
- @mentions in stored bodies are styled as blue handle pills.
- MentionTextarea: typeahead popup on `@<query>` against the channel
  member list, with arrow/Enter/Tab/Escape keybindings.

Read state filter:
- unreadCount / markRead now ignore edit/delete/reaction rows so a side-
  channel action on a read message doesn't inflate the unread badge.

CLI (bin/kanban.rs):
- `channel edit <name> <messageId> <body…>`
- `channel delete-msg <name> <messageId>`
- `channel react <name> <messageId> <emoji>`
- Equivalent `dm edit / delete-msg / react` subcommands.

Verified: 32 Rust tests pass (added pre_113_message_still_parses,
message_with_refs_round_trips, extracts_mentions_in_first_occurrence_order,
and an end-to-end edit_delete_react_round_trip store test). cargo build
and npm run build are green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings the #112/#113/#114 work onto the Part 1+2 trunk so the full
channels feature can be reviewed and dogfooded together.

Conflict resolution notes:
- src-tauri/src/lib.rs — Part 2's notify_chat_message and Part 3's
  read_image_bytes / persist_clipboard_image both sit immediately after
  save_drafts; kept both, and registered all of notify_chat_message,
  read_image_bytes, and persist_clipboard_image in invoke_handler.
- src/store/channelsStore.ts — extended sendDm to take imagePaths
  (matching sendMessage's new signature). Added DM-side editDm/deleteDm/
  reactDm actions to mirror the channel-side ones. Hooked the new
  isVisibleRow filter into markDmRead / unreadDmCount so reactions on
  already-read DMs don't bump the badge.
- src/components/Channels.tsx — collapsed the previously-separate
  ChannelPane / DmPane / ThreadPane plumbing so ThreadPane now also
  receives `members` (for @mention autocomplete), `rawMessages`, and
  the onEdit/onDelete/onReact callbacks. DmPane synthesizes a 2-member
  list from SELF + the other party so mention autocomplete works inside
  DMs too. Single textarea-vs-MentionTextarea + drag-drop/paste/attachments
  surface is now shared between both surfaces.

Verified:
- cargo test --lib: 69 passed.
- npm run build: clean.
- CLI end-to-end smoke: create channel → send w/ -i image + warn-skip
  missing → react → edit (mentions re-extracted) → history → delete.
  All shapes (mentions, refs.reactionTo, refs.editsMessageId, imagePaths)
  serialize correctly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two macOS-side features from late May/early June that were missed in the
Phase 7 parity audit. Both ride into the Part 1 follow-up branch.

Pinned cards (mirrors macOS Sources/.../BoardStore + BoardView):
- Adds pinned_at / pinned_sort_order to Link, byte-compatible with the
  macOS JSON shape. Archive/move-to-all-sessions clear the pin.
- Adds set_card_pinned + reorder_pinned_cards Tauri commands.
- Adds a Pinned strip above the columns in BoardView with horizontal
  drag-reorder. Pinned cards are hidden from their real lane so they
  don't double-render.
- Right-click "Pin to top" / "Unpin from top" in the card context menu;
  pinned cards show a small gold pin glyph next to the title.
- merge_ops inherits the pin from source when target wasn't pinned.

Self-compact (data shapes + drop-guard, mirrors macOS SelfCompactSettings):
- New SelfCompactSettings struct with byte-compatible defaults (500k/
  600k/700k queueing, 750k /compact).
- QueuedPrompt grows a selfCompactThresholdTokens field so the guard
  can distinguish auto-enqueued nudges from manual prompts.
- New context_usage.rs reads <data_dir>/context/<sessionId>.json (the
  statusline-emitted file, same path layout as macOS).
- New should_drop_self_compact_prompt command runs the macOS
  shouldDropStaleSelfCompactPrompt logic. Wired into the
  CardDetailView auto-send path so a compact nudge that the user
  already addressed gets dropped instead of sent.
- SettingsView gets a Self-compact toggle plus a read-only summary of
  the configured thresholds.

Out of scope here (each deserves its own PR):
- Polling/generation loop that *enqueues* compact prompts when usage
  crosses a threshold. Needs the statusline script to emit the
  context-usage JSON in the first place — no plumbing on Windows yet.
- Channel reorder (the macOS commit bundled "channels + pinned" — only
  pinned ported here).
…ompact

feat(windows): pinned cards + self-compact settings
@Aryansharma28 Aryansharma28 changed the title feat(windows): agent identity in card tmux + channels store cleanups feat(windows): Phase 7 follow-ups (Parts 1–4) Jun 14, 2026
@Aryansharma28 Aryansharma28 merged commit 43daa0a into main Jun 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment