Size and margins scale with row height:
- height: 30% of row height
- width: same as height (circular)
- left margin: 15% of row height
- right margin: 25% of row height
Small colored dot to the right of agent name shows execution state:
- Green: idle
- Blue: thinking
- Orange: calling tool
- Gray: paused
Updates reactively via agentStateChanged event signal.
o sess approve [id] - approve pending, optionally verify id
o sess deny [id] - deny pending, optionally verify id
Shows request id in output: [42] approving: cmd
Errors if specified id doesn't match pending request.
Server emits session.{sid}.agent.{aid}.bypass.resolved with id and
action (approved/denied) when a bypass is resolved by any client.
GUI handles bypass.resolved events to clear the banner and pending
count when CLI or another client resolves a bypass request.
This allows CLI 'o sess approve' to clear the GUI banner automatically.
Works at session or agent context (bypass is session-level).
Reads pending request from session/{s}/bypass, extracts ID,
writes '{id} approve' or '{id} deny' to resolve.
Track multiple concurrent pending bypasses per agent in C++ backend:
- Add m_pendingBypasses QHash<QString,QSet<QString>> keyed by session:agent
- Add pendingBypassCount(sessionId, agentId) Q_INVOKABLE
- Add pendingBypassCountChanged(sessionId, agentId) signal
- Update resolveBypass to take agentId and remove from tracking set
SessionTree.qml: show ⚠ indicator left of agent name when the agent
has pending bypass requests. Uses Connections to refresh on signal.
ChatPane.qml: pass agentId to resolveBypass calls.
- Changed event topic: session.{sid}.agent.{aid}.bypass.request
- Added agentId parameter to bypassRequested signal
- Filter bypass events to show only for the active agent
Bypass approval now flows through:
1. GUI - via event stream and banner
2. CLI - via agent loop (to be implemented)
Removed:
- bypass_notify.go (D-Bus notification)
- BypassNotifyFunc type and all references
- godbus/dbus dependency
The bypass event is still published via SetBypassPending.
If fidOK was false for the event file, we fell through to the
default stream handling path which doesn't work for events.
Now we return 'bad fid' error instead of falling through.
The pubsub library had issues:
- Published to literal '*' topic (nonsensical)
- Used TrySend which drops events
- Complex hierarchical wildcard publishing
New implementation:
- Simple eventHub with map of subscribers
- PublishEvent fans out to all subscribers (blocking send)
- SubscribeEvents returns channel, cleaned up on ctx cancel
- SubscribeEventsFiltered filters client-side with MatchTopic
- Removed simonfxr/pubsub dependency
The subscription was being cancelled after the first read completed
because we used the per-request context. Now using the connection
context so the subscription lives until the fid is clunked or
connection closed.
The pubsub library's SubscribeChan uses non-blocking TrySend which
drops events when the channel is full. Switch to Subscribe with a
blocking callback to ensure no events are lost.
Also increased channel buffers from 64 to 256.
Plain reads (without writing a filter first) now get a per-fid
subscription with '*' filter, identical to 'echo * | rdwrs event'.
This fixes event loss — the old EventStream path overwrote events
when multiple arrived before the reader consumed them.
Backend adapters (~8.3K) are mostly mechanical API glue.
Core runtime is ~25K lines (22K Go core + 3K compiled tools).
KDE adds another ~15.7K. The interesting logic is concentrated.
The feed file was documented but never used by any frontend
or script. The observer agent pattern was never adopted.
- Remove feed.go, FeedWrite, ConsumeFeed
- Remove WatchFeed constant
- Remove feed file from 9P namespace
- Remove ConsumeFeed goroutine spawns from session
- Update docs (architecture-9p, architecture-ide, architecture, usage)
The event stream now covers real-time observation patterns better.
The event stream with filtering replaces statewait:
- echo filter | rdwrs event
Removed:
- statewait file from agent namespace
- All non-historical references in docs and code
The state file remains for simple polling reads.
- AGENTS.md: simplified architecture description
- architecture-9p.md: examples use event stream with filtering
- usage.md: o tui and wiring examples use rdwrs event
- state: immediate read of current agent state
- statewait: blocks until state changes
Clearer semantics than overloading statewait with both behaviors.
The server event stream already delivers state change events for all
agents. Removed the redundant per-agent statewait streamer - now agent
switching has no teardown/startup overhead for state monitoring.
- Removed m_state streamer entirely
- State updates come via event stream's session.{sid}.agent.{aid}.state events
- Only chat stream needs per-agent setup/teardown
EventValue was storing only the latest event and using hash comparison,
which caused events to be overwritten if they arrived faster than the
client could read them.
Now eventwait uses Stream mode with EventStream which delivers each
event as it arrives. Events won't be lost due to rapid arrival.
Events can be missed if GUI wasn't connected when they were published.
Now checkPendingBypass() reads the bypass file directly when switching
agents/sessions to catch any pending requests.
Server changes:
- Session tracks pending bypass request and exposes methods
- New 9P files: session/{sid}/bypass (read/write), bypasswait (blocking)
- Publish bypass.request events for GUI listeners
GUI changes:
- Handle bypass.request events from eventwait
- Show inline amber banner with command and cwd
- Approve/Deny buttons resolve via 9P
Desktop notifications still work in parallel for non-GUI usage.
When switching agents, the old state/chat streams would block until
data arrived (up to 500ms for statewait, indefinitely for chat).
Now requestStop() closes the fid from the calling thread, which
unblocks the worker's blocking read immediately. This makes agent
switching instant instead of waiting for the read timeout.
When reading statewait with the same state value, the server now waits
500ms instead of 5 seconds before returning. This makes state polling
much more responsive.
QThread::~QThread() aborts if the thread is still running. The previous
code called delete unconditionally after a 100ms wait timeout. Now we
follow the same pattern as stopWorker(): check if wait() succeeded,
and if not, abandon the thread safely via deleteLater.
Switch streaming reads (chat, state, events) from subprocess-based
ollie-9p to native lib9p via worker threads. This eliminates:
- Process spawn/teardown overhead
- Subprocess failure modes
- Signal handling complexity
Lib9pStreamer uses QThread workers with blocking reads. On stop,
workers are given 200ms to exit cleanly, then abandoned (they'll
exit when the read completes or connection closes).
Also: make KRunner/Kate/KIO plugins optional in CMakeLists.txt
to fix build when KF6Runner etc aren't installed. Added
QT_DEFAULT_MAJOR_VERSION=6 to fix Qt6 detection with CMake 4.x.
Background procs with bypass were hanging because the caller blocked on
<-startedCh waiting for the process to start, but bypass.Submit blocks
until approval. Now we signal started immediately when entering bypass
path (with Process: nil), so the agent sees the proc ID right away.
Also handle term/kill when proc.proc is nil — cancel the context to stop
the operation (e.g., abort a pending bypass request).