Merged
Conversation
TableBuilder requires unique IDs when multiple tables exist in the same Ui. Without id_salt, all tables shared the same auto-generated ID, corrupting each other's column width state and breaking layout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Initial implementation of JSONL to nostr event conversion: - session_jsonl.rs: ProfileState-style Value wrapper parser - path_normalize.rs: cwd-based path normalization - session_events.rs: kind-1988 event builder with NIP-10 threading - session_converter.rs: JSONL to ndb orchestrator - session_loader.rs: ndb to Message loader (incomplete) - Design doc with full spec Shelved because the 1:1 JSONL-line-to-event model does not cleanly handle mixed content blocks, tool_use/tool_result pairing, or subagent conversation trees. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add monotonic seq tag for unambiguous event ordering, split tag for multi-event assistant messages, tool-id tag pairing tool_call/result events, and cwd tag. Source-data now stores raw JSONL verbatim (no path normalization) and is only included on the first event of split groups. Add session_reconstructor module that uses ndb.fold to reconstruct original JSONL from stored events. Deduplicate get_tag_value into session_events for reuse. Include async round-trip integration test verifying JSONL → events → ndb → JSONL equality. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ume flow Kind 1988 events are now lightweight (no source-data tag). A companion kind 1989 event carries the raw JSONL per line, linked via e-tag and sharing the same seq/d tags. Reconstructor queries 1989 instead of 1988. On session resume, the JSONL file path is threaded through the UI and converted to nostr events in update() where AppContext is available. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… loading - Wire set_sub_callback in ndb Config to trigger UI repaint on subscription matches - Replace synchronous message loading with subscribe-before-ingest + poll pattern (process_event_with queues async indexing, so immediate loads found nothing) - Fix file-history-snapshot lines lacking top-level timestamp/sessionId by adding context inheritance to ThreadingState (carries forward last-seen values) - Add timestamp() fallback to snapshot.timestamp in session_jsonl.rs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nversations Refactor build_single_event to accept optional JsonlLine, allowing reuse for both archive (JSONL) and live paths. Add build_live_event() for generating 1988 events directly from role + content strings. Wire into process_events() for assistant (on finalize), permission requests, tool results, and errors. Wire into handle_user_send() for user messages. No 1989 source-data events for live — archive only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Live events were starting a new NIP-10 root instead of threading onto the existing archive conversation. Seed ThreadingState with root/last note IDs from loaded archive events so resumed sessions thread correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… selection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements deterministic key derivation and NIP-44 v2 encryption for kind-1080 PNS events. Used for encrypting AI conversation events before publishing to relays for remote session control. - derive_pns_keys(): HKDF-based key derivation from device secret key - encrypt()/decrypt(): NIP-44 v2 with pre-derived conversation key - 5 tests covering determinism, isolation, and round-trip Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e polling - Refactor BuiltEvent to store note_json (bare event JSON), add to_event_json() for ndb ingestion. All build sites updated. - ingest_live_event() returns Option<BuiltEvent> for relay publishing - process_events() returns (sessions, events) tuple, update() publishes to pool - pending_relay_events queue for events from non-pool contexts (handle_user_send) - build_permission_request_event(): kind-1988 with perm-id, tool-name, t:ai-permission - build_permission_response_event(): kind-1988 response with e-tag linking to request - Subscribe for remote permission responses when claude_session_id is learned - poll_remote_permission_responses(): routes relay events through existing oneshot channels, first-response-wins with local UI - AgenticSessionData: add perm_request_note_ids, perm_response_sub fields Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Relay publishing of AI conversation events is commented out to prevent accidentally broadcasting plaintext conversations. Will be re-enabled once NIP-PNS wrapping is wired in. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Done sessions were being silently added to the queue but never auto-focused, making it easy to miss completed work. Now auto-steal focuses Done sessions after NeedsInput, returning home only when neither remain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevent jarring session switches by checking if the active session has non-empty input. Auto-steal resumes after the message is sent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use egui_extras::syntax_highlighting for colored code blocks in both complete and streaming renders. Supports Rust, C/C++, Python, and TOML with no new dependencies. Results are frame-cached. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Publish a parameterized replaceable event (NIP-33) on every agent status transition and title change. On startup, query ndb for these events to restore sessions and skip the directory picker. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Spawn process_pns thread on first update to unwrap kind-1080 events. Bump nostrdb dep to 9aeecd3 which adds ndb_process_pns support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All kind-1988/1989/31988 events are now encrypted with NIP-PNS (kind-1080 envelope) before being sent to relays. Adds wrap_pns() helper that encrypts inner event JSON and signs with derived PNS keypair. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Subscribe for kind-1080 (PNS) events authored by our derived PNS pubkey so that session state events from other devices arrive via relay pool. Add local ndb subscription for kind-31988 session state events and poll each frame to detect newly-unwrapped sessions, creating them in the UI with conversation history and threading state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remote sessions discovered via relay have no local Claude process. Permission responses publish as nostr events instead of oneshot channels. Session status derived from kind-31988 state events. - Add SessionSource enum to ChatSession (Local vs Remote) - Remote-aware derive_status(), has_pending_permissions(), first_pending_permission(), handle_permission_response() - PermissionResponseResult propagated through UI via new variants on KeyActionResult, SendActionResult, UiActionResult - Load permission_request/response events in session_loader - Guard local-only ops (state publish, git status, remote perm poll) - Remote user messages publish to relay, skip local backend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Subscribe to kind-1988 events for remote sessions and poll for new messages in the update loop. Dedup via seen_note_ids HashSet seeded from initial session restore. Also update remote_status when new kind-31988 events arrive for existing sessions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add PNS_RELAY_URL constant and use send_to for both subscription and event publishing. Ensures the relay is in the pool before use. Avoids blasting PNS-wrapped events to all relays in the pool. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of ingesting raw 1988/31988 events directly, PNS-wrap them first and ingest the 1080 wrapper. ndb process_pns handles unwrapping internally. This ensures 1080 events are in the local db for relay sync. Also fixes session state events (31988) not being published to the relay at all. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add query_replaceable() helper that folds over replaceable events, keeping only the latest revision per d-tag. Fixes duplicate sessions appearing in the side menu when multiple kind-31988 revisions exist in ndb. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pns_ingest was passing bare {...} JSON to ndb.process_event which
expects ["EVENT","subid", {...}] format, silently failing. Use
process_client_event with ["EVENT", {...}] wrapping instead. This
was why no 1988/31988 events were appearing in ndb during sessions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The old code called Hkdf::new() then hk.expand(), which runs both HKDF-Extract and HKDF-Expand. The nostrdb C code only does raw HMAC-SHA256 (equivalent to HKDF-Extract). The extra Expand step produced different keys, so ndb couldn't match the PNS pubkey and 1080 events were never auto-unwrapped. Add a test vector from nostrdb's test_pns_unwrap (device key 0x02) to ensure the derived PNS pubkey matches the C implementation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Local sessions were only sending permission responses through the oneshot channel to the Claude process, without creating a nostr event. On session reload, the permission_request events existed in ndb but no matching permission_response events, so all requests appeared unresolved. Now both local and remote sessions publish permission response events, so resolved state persists across reloads. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a session was deleted, the kind 31988 replaceable event persisted in ndb and on relays, causing deleted sessions to reappear on restart. Now we publish a replacement 31988 event with status "deleted" which overwrites the old state. Session loading and live polling both filter out deleted sessions. Also fixes: PNS ingest uses process_event with relay format so ndb triggers PNS unwrapping, and Claude stream errors from unknown message types (e.g. rate_limit_event) are now non-fatal warnings instead of killing the session. Resumed sessions always send just the latest message since Claude Code already has context via --resume. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Session metadata (title, cwd, status) is now stored as tags rather than serialized JSON in the content field. This avoids unnecessary JSON parsing when reading session state, especially in the query_replaceable_filtered predicate that checks for deleted sessions. Also adds query_replaceable_filtered which takes a predicate closure to reject notes during the fold pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- switch_and_focus_session(): consolidates switch_to/select/focus_on/focus_requested pattern (8 call sites) - cycle_agent(): generic direction-based agent cycling, used by next/prev - secret_key_bytes(): replaces 5 copies of keypair→secret_key→as_secret_bytes→try_into - init_note_builder(): replaces duplicated NoteBuilder::new().kind().content().options() chains (6 call sites) - finalize_built_event(): replaces duplicated sign/build/id/json pattern (6 call sites) - now_secs(): replaces 4 copies of SystemTime::now timestamp boilerplate Net: 128 insertions, 316 deletions across 3 files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap diff view in a ScrollArea with max 400px height so large diffs no longer extend off screen. Move allow/deny buttons to bottom left for easier access on mobile. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remote sessions have no local git repo or interruptible process, so hide the git status bar and the "press esc to interrupt" hint. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Changed permission_buttons from right-to-left to left-to-right layout so Allow/Deny buttons appear at the bottom left instead of top right, making them easier to reach on mobile. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add note IDs to seen_note_ids in ingest_live_event so events echoed back from the relay are skipped. Subscribe local sessions to conversation events and handle incoming user messages from remote clients, dispatching them to the backend. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep full diff height visible, only scroll horizontally for long lines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Non-diff permission UIs had buttons inline with the tool name, causing them to appear top-right. Move them to a separate line. Also fix plan approval buttons to use left-to-right layout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
StripBuilder with clip=true sets TextWrapMode::Truncate on the cell style, causing all text to truncate instead of wrapping when the window is narrowed. Override to Wrap at dave UI entry. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This reverts commit 801452d.
Sessions now include a hostname tag in kind-31988 events, parsed on restore, and displayed as hostname:cwd in the session list sidebar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ff90477 to
9dbf828
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When multiple user messages arrived from a remote client (phone) while the backend was already streaming a response, each new message would call send_user_message_for and overwrite the active stream. This caused responses to lag behind by one message. Now we guard dispatch: skip if the session is already streaming (the message is already in chat), and re-dispatch when the stream ends if there are unanswered user messages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handle_question_response() only sent through the local oneshot channel, never publishing to relays for remote sessions. Mirror the permission response flow: return PermissionResponseResult, insert into responded_perm_ids for remote, and propagate PublishPermissionResponse from the UI action handler. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract a shared PermissionPublish struct to replace the duplicated
{perm_id, allowed, message} fields across PermissionResponseResult,
UiActionResult, KeyActionResult, SendActionResult, and
PendingPermResponse. Handlers now return Option<PermissionPublish>,
making it impossible to forget relay publishing for new prompt types.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tool result messages from nostr notes were showing "tool" as the tool name instead of the actual name (e.g. "Bash", "Read") because the tool-name tag was never written when building events. The session_loader was already reading this tag but it always fell back to "tool". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three publish functions (session states, deletions, permission responses) all repeated the same build→log→pns_ingest→queue pattern. Unified into a single queue_built_event() helper. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Unifies three scattered permission fields (pending_permissions, perm_request_note_ids, responded_perm_ids) into a single PermissionTracker struct with a merge_loaded() helper for the common session-restore pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When multiple revisions of a kind-31988 session state event arrive out of order (e.g. after relay reconnect from sleep), the last one processed would win regardless of timestamp. Track remote_status_ts so we only apply updates from newer events. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When multiple revisions of the same kind-31988 session state arrive in one poll batch (e.g. after relay reconnect), an older non-deleted revision could be processed after the newer deleted one, creating a phantom session. Deduplicate the batch by d-tag before processing so only the latest revision per session is used. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ndb.fold iterates notes in storage order, not chronological. If a newer deleted revision was visited before an older non-deleted one, the predicate rejection was a no-op (nothing to remove yet), and the older revision would then be inserted unchallenged — resurrecting deleted sessions as zombies. Fix by always tracking the highest created_at per d-tag regardless of predicate result, storing Option<NoteKey> so rejected revisions still block older ones from winning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nt batches" This reverts commit 56987d9.
handle_permission_response() and handle_question_response() each independently updated req.response, permissions.responded, and the oneshot channel — three redundant state locations that had to stay in sync. When 950b430 fixed permission responses to always publish nostr events, the Q&A path was missed, requiring a catch-up fix (4c05d07). Add PermissionTracker::resolve() as the single method that updates all resolution state. Both handlers now funnel through it, so future changes to resolution logic only need to touch one place. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After restarting Dave, restored local sessions had no live_conversation_sub because the subscription was only created for remote sessions. This meant remote user messages (e.g. from phone) were ignored until a local message triggered the backend, which created the subscription via SessionInfo. Subscribe to conversation events for all restored sessions regardless of local/remote status. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the static "(⇧ for message)" hint with a clickable "+ msg" link that enters tentative mode without needing a shift key. Also make the "Will Allow"/"Will Deny" labels clickable to toggle between accept and deny. This enables permission feedback from mobile clients where no shift key is available. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The PLAN and AUTO toggle badges were not clickable (using hover sense) and were placed in the input box area. This moves them to the git status bar (right of the refresh button) and makes them clickable to toggle plan mode and auto-steal focus mode respectively. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The status bar with badges was only rendered when git_status was available, which excluded remote sessions (e.g. Android). Now the status bar renders with just the toggle badges when there's no git status but we're in agentic mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When in tentative state (composing a message to attach to a permission response), replace Allow/Deny buttons with a Send button so tapping the button actually submits the response with the typed message. This fixes mobile where clicking Allow/Deny would discard the message. Extract tentative_send_ui() and add_msg_link() helpers to share the tentative-state UI between permission_buttons and exit_plan_mode_ui. Co-Authored-By: Claude Opus 4.6 <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
Test plan
🤖 Generated with Claude Code