Skip to content

feat: /voice + qc voice (pairux CLI), qc archiving, live join/leave/archive events, periodic chat list (0.9.0, qc 0.8.0) - #308

Merged
ralyodio merged 3 commits into
masterfrom
feat/voice-command
Oct 12, 2026
Merged

ralyodio merged 3 commits into
masterfrom
feat/voice-command

Conversation

@ralyodio

@ralyodio ralyodio commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

/voice and qc voice

  • /voice in the PWA composer and qc TUI posts https://pairux.com/r/<room>#k=<key>, made on the device (128-bit room id, 32-byte key in the fragment, which PairUX never sees; nobody needs an account). It travels inside the normal ML-KEM-encrypted message; the PWA also opens it.
  • qc voice is an alias for the pairux CLI: bare qc voice (--send <chat>, --json) makes the link; anything else runs pairux <args> (login, logout, whoami, join, say, ...) with its exit code. Missing pairux prints the installer.
  • Needs pairux.com#138 (merged): the /r page and POST /api/v1/rooms/token.

Archiving in qc

  • The TUI hides archived chats (the title counts them). a in the list archives or unarchives, Ctrl+O or /archived switches views, and /archive / /unarchive act on the open chat.
  • CLI: qc archive|unarchive <chat>, qc chats --archived.

Live chat list on every device

  • New SSE events: PARTICIPANTS_JOINED (invite join, admin add) goes to the whole chat; PARTICIPANT_LEFT to the rest of the chat and every device of the person who left; CONVERSATION_ARCHIVED to every device of that user.
  • The web store and sidebar reload on them. The qc TUI reloads on them and also polls the list every 15 s (the web sidebar already polled every 10 s).
  • Fixed: archive could 500 after succeeding if the announcement lookup threw.

Tested

Full vitest (115 files, 752 tests) including new tests for links, qc voice pass-through, membership events reaching each device, archive announcements, and the TUI archived view and refresh. next build and the qc bundle build pass. The bundled qc voice prints a link, prints the installer when pairux is missing, and passes through exit codes.

🤖 Generated with Claude Code

…I; archive chats in qc (0.9.0, qc 0.8.0)

/voice (PWA composer and the qc TUI) mints https://pairux.com/r/<room>#k=<key>
on the device: a 128-bit room id and a 32-byte media key in the fragment, so
PairUX never sees the key and nobody needs an account to join. The link goes
out inside the usual ML-KEM-encrypted message; the PWA also opens it.

qc voice is an alias for the pairux CLI: bare `qc voice` (or --send <chat>,
--json) makes the link locally, anything else runs `pairux <args>` attached
to the terminal (login, logout, whoami, join, say, ...), with its exit code.
Both bins now keep a non-zero process.exitCode instead of forcing 0.

Archiving in qc: the TUI hides archived chats (title counts them), `a` in
the list archives/unarchives, Ctrl+O (or /archived) flips to the archived
view, /archive and /unarchive act on the open chat. CLI: qc archive|unarchive
<chat>, qc chats --archived.

Needs pairux.com#138 (the /r page and anonymous token endpoint) live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

13 finding(s)

MEDIUM: 9 | LOW: 4

Severity Rule Location
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.jsx:38
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.jsx:66
MEDIUM js-unescaped-html-sink src/app/faq/page.jsx:57
MEDIUM js-unescaped-html-sink src/app/layout.jsx:141
MEDIUM js-unescaped-html-sink src/app/layout.jsx:145
MEDIUM js-unescaped-html-sink src/app/page.jsx:47
MEDIUM redos-nested-quantifier src/lib/auth/dns-name.js:88
MEDIUM js-unescaped-html-sink src/lib/components/chat/MessageItem.jsx:85
MEDIUM js-unescaped-html-sink src/lib/components/chat/MessageItem.jsx:130
LOW secret-generic-credential src/app/api/auth/register-anon/route.test.js:32
LOW secret-jwt tests/debug-sms.js:10
LOW secret-generic-credential tests/private-key-import-export.test.js:252
LOW secret-generic-credential tests/private-key-import-export.test.js:264

Snippets are redacted; ThreatCrush never prints matched credential material.

Comment thread tests/voice.test.js Fixed
ralyodio and others added 2 commits October 12, 2026 21:41
…ex (CodeQL)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its chat list every 15 s

Server: PARTICIPANTS_JOINED (invite join, admin add) goes to everyone in the
chat, PARTICIPANT_LEFT (leave/delete for me) to the rest and every device of
the leaver, CONVERSATION_ARCHIVED to every device of the user who archived or
unarchived (src/lib/chat/membership-events.js; sendToUser fans out to each
connection). Announcing never fails the request: archive used to 500 if the
lookup threw.

Clients: the web store reloads on any of these and raises
qrypt:chats-changed so the sidebar (its own list, still polled every 10 s)
reloads too; the qc TUI reloads on them and closes a chat this user left
elsewhere. qc also reloads the list every 15 s and catches news the stream
missed (by updated_at).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ralyodio ralyodio changed the title feat: /voice posts an encrypted PairUX room; qc voice = the pairux CLI; archive chats in qc (0.9.0, qc 0.8.0) feat: /voice + qc voice (pairux CLI), qc archiving, live join/leave/archive events, periodic chat list (0.9.0, qc 0.8.0) Oct 12, 2026
@ralyodio
ralyodio merged commit aa0b676 into master Oct 12, 2026
11 checks passed
@ralyodio
ralyodio deleted the feat/voice-command branch October 12, 2026 21:54
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