Skip to content

feat(windows): agent identity in card tmux + channels store cleanups#115

Open
Aryansharma28 wants to merge 11 commits into
mainfrom
feat/windows-channels-followup-part1
Open

feat(windows): agent identity in card tmux + channels store cleanups#115
Aryansharma28 wants to merge 11 commits into
mainfrom
feat/windows-channels-followup-part1

Conversation

@Aryansharma28

Copy link
Copy Markdown
Contributor

Bundle of three Phase-7 follow-ups so a Claude session launched inside a Kanban card can talk to the human (and other cards) via the kanban CLI out of the box.

Summary

  • #106 — Inject KANBAN_CARD_ID + KANBAN_HANDLE into every card-launch shell. This is the blocking one — without it the kanban CLI inside a card falls all the way to default handle="user", and kanban dm read refuses with the "ambient identity" guard. Now both env vars are prepended to the env-prefix in buildInnerBashCmd and buildInnerCmdShellCmd, so they apply to tmux, bash-without-tmux, and native cmd paths (and the launch-dialog preview reflects them). User-supplied env entries still override since identity goes first. Handle is a slug of card.displayTitle (lowercased, [^a-z0-9_-]-, collapsed, trimmed, ≤32 chars, falls back to card if empty).
  • #107 — Collapse tail_messages into read_messages. tail_messages(ch, n) was a one-line wrapper around read_messages(ch, Some(n)). Removed the method and updated all three call sites (bin/kanban.rs:268, bin/kanban.rs:350, and the tail_returns_last_n_only test).
  • #108 — Enable serde_json preserve_order. One-line Cargo.toml flip so the channels / read-state / drafts JSON files keep insertion order on round-trip and produce byte-stable diffs.

Verification

#107 and #108 are cargo-only — both confirmed green:

cargo build --bins        # finished, only pre-existing dead-code warnings
cargo test channels_store # 11 passed; 0 failed (incl. tail_returns_last_n_only on the renamed call)
npm run build             # tsc + vite both green

#106 — CLI-side end-to-end check ran against the freshly built kanban.exe with only the env vars set (no --as flags), proving the identity resolution path the launch shell will exercise:

$ KANBAN_CARD_ID=card_TEST123 KANBAN_HANDLE=agent-fix-foo kanban handle
{ "cardId": "card_TEST123", "handle": "agent-fix-foo" }

$ KANBAN_CARD_ID=... KANBAN_HANDLE=... kanban channel send "#acceptance" "ping from env-identity"
@agent-fix-foo → #acceptance: ping from env-identity

$ KANBAN_CARD_ID=... KANBAN_HANDLE=... kanban dm send @user "hello user from card"
@agent-fix-foo → @user: hello user from card

$ KANBAN_CARD_ID=... KANBAN_HANDLE=... kanban dm read @user
[2026-06-13T22:12:08.950+00:00] @agent-fix-foo: hello user from card

The TS-side injection was traced by hand through both builders — for tmux the inner cmd is cd /path && KANBAN_CARD_ID=<id> KANBAN_HANDLE=<slug> claude ..., base64-encoded as before, so the env vars survive the wsl bash -lc transport and apply to claude (and its kanban-CLI children) inside the pane. The launch-dialog preview path uses the same builder, so the user sees what runs.

Runtime GUI verification (interactive Tauri window — launching a card and running env | grep ^KANBAN_ / kanban handle from inside the pane) was not run in this session and should be done before merge — best with a freshly launched card on each shell variant (tmux+bash, bash-no-tmux, native cmd).

Out of scope

  • $TMUX auto-identity (deferred per windows-parity-port).
  • Persisting the env vars across ; exec bash after the Claude binary exits (inline VAR=val cmd form intentional, matches existing envPrefix style; subprocess-of-Claude inheritance is what the agent path needs).
  • Per-card handle customization UI (override via launch-dialog envPrefix works today).

Closes #106, closes #107, closes #108

🤖 Generated with Claude Code

Aryansharma28 and others added 11 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant