- Add createSession(name, remote) with New Session dialog
- Remove agent connectivity code (redundant with session connectivity)
- Refactor NinePConnection as formal state machine with explicit events
- Fix silent restart to not cycle through Connecting state
- Check if event is for active agent FIRST using QStringView comparison
- Active agent: compare payload directly against m_activeState (no hash lookup)
- Non-active: use iterator find() to avoid double hash lookup
For the most common case (active agent state change), this reduces
the hot path to: QStringView compare → QString convert → direct compare
- Use QStringView to avoid string allocations during parsing
- Use QLatin1String for comparisons (faster than QString)
- Manual indexOf parsing instead of split('.') allocation
- Early return for non-session events
Reduces allocations in the agent.state hot path from ~7 QStrings
to 4 (only when state actually changes and needs to be stored).
Consolidate refreshSessions() to do everything in one iteration:
- Parse session/idx data
- Update m_agentConnected cache
- Track active session/agent existence
- Emit connection state change
Eliminates separate reconcileAgentConnections() call and second loop
to check active session/agent. O(n) instead of O(3n).
Agent connection state is now derived from session.connected in session/idx,
updated via refreshSessions(). No longer spawns N processes for N agents.
- Remove m_agentConnections hash and per-agent NinePConnection instances
- Remove readAgentRecords() (dead code - agents come from session/idx)
- reconcileAgentConnections() now just updates m_agentConnected from m_sessions
This eliminates one ollie-9p process per agent, leaving only:
- 1 daemon eventwait stream (for all events)
- 1 chat stream (for active agent only)
- 1 state stream (for active agent only)
Replace subprocess spawning (run9p) with native 9P connection for hot paths:
- refreshSessions() - called on every sessionsChanged event
- loadRootBackends() - called on daemon connect
- loadRootAgents() - called on daemon connect
Lib9pClient wraps the Go shared library with a Qt-friendly C++ API.
Connection persists across operations, eliminating ~2-5ms per-call
process spawn overhead.
Requires libollie9p.so from parent repo's lib9p/ package.
- Parse new session/idx format with all data in one read:
name, id, paused, connected, cwd, backend, model, agents
- Handle delta events from eventwait instead of full refresh:
- session.{sid}.agent.{aid}.state -> update cache, emit signal
- Structural changes (new/kill/pause/resume) -> full refresh
- Remove per-agent statewait streams (eliminated N processes)
- Single eventwait stream handles all events via pubsub
This eliminates O(sessions * agents) synchronous reads per refresh,
replacing them with one read + incremental delta updates.
- Add Pause/Resume toggle to session context menu
- Gray agent dots when parent session is paused
- Disable input field with 'Session paused' placeholder when paused
- Preserve focus on prompt input during tree rebuilds
- Add per-agent statewait streams to track execution state
- Agent dots show: green=idle, orange=calling, blue=thinking
- Session dots show RPC connection health (unchanged)
- Use version counter pattern to force QML binding re-evaluation
- Green: connected and not paused (healthy)
- Gray: paused (intentional)
- Red: not paused but disconnected (crashed/broken)
- Yellow: paused but still connected (zombie)
- Fetch paused status from session/{id}/paused file
- Gray dot indicator for paused sessions (green otherwise)
- Session names now show agent count: 'ollie (2)'
- Fixed rename to use baseName for sessions (without count suffix)
refreshSessions now checks if the active session/agent still exist after
updating the list. If the agent was killed, clears m_agentId and stops
streams. The statewait guard will then return false, stopping the loop.
Session selection now auto-selects first agent:
- Clicking a session row selects its first agent (not just the session)
- Auto-select at startup now picks first agent of first session
- Fixes issue where clicking session would clear agent and show empty chat
Improved UI feedback:
- Header shows 'Session / Agent' name instead of just state
- State indicator gray when no agent selected
- Placeholder message when no agent selected: 'Select an agent to view chat'
- Chat ListView hidden when no agent (shows placeholder instead)
- Input already disabled with 'No agent' placeholder (existing behavior)
Cleanup:
- Remove dead declaration reconcileAgentConnections(const QVariantList&)
Agent connections were using retryMs=0, causing a tight loop when the
/connection file read failed (e.g., toolsConn nil). The loop was:
connection fails → disconnected signal → refreshSessions →
reconcileAgentConnections → new connection → fails immediately → repeat
Fix by:
- Add maxRetries parameter to NinePConnection::start()
- Use 5s retry interval with max 3 retries for agent connections
- Reset retry count on successful connection
Three fixes:
1. Disconnect signals before stopping removed connections in
reconcileAgentConnections — prevents disconnect->refresh loop.
2. Clear m_activeSessionId/m_agentId when killing the active
session — guard function returns false, statewait stream stops.
3. Same disconnect-before-stop in stopAgentConnections.
Agent connections no longer retry on their own. When an agent is
killed, the connection goes to Disconnected and stays there.
reconcileAgentConnections (triggered by refreshSessions) is the
sole authority for creating/removing connections.
For the happy path (probe returns normally), the connection
immediately re-probes without a timer delay.
When an agent connection drops (killed or server restart), refresh
the session model from idx rather than immediately marking the
session as disconnected. This avoids a transient red indicator
when an agent is killed but the session is still alive.
The daemon disconnect handler (server down) still correctly marks
everything as disconnected.
Change createAgent to return error string (empty=success).
Replace standardButtons with custom footer so dialog stays open
on error, showing the message and letting the user adjust values.
Connection monitoring and all 9P operations now use immutable
session/agent UUIDs in paths instead of mutable display names.
The server resolves these via the new Aliases mechanism in findChild.
This fixes the disconnected-indicator bug after session rename:
connections no longer go stale when names change.
Removed dead sessionNameForId/agentNameForId helpers and the
m_agentConnectionPaths tracking (unnecessary with stable paths).
Rename operations no longer restart streams.
killSession, renameSession, readLogForSession, getAgents, and
renameAgent were using immutable IDs directly in paths like
session/{id}/ctl. The 9P namespace uses the mutable display name
as the directory key, not the ID. Use sessionNameForId() and
agentNameForId() consistently (matching agentPath()).
- Add chatblockmodel.cpp and thememanager.cpp to KF5 GUI target
- Link KF5::ConfigCore and KF5::SyntaxHighlighting to GUI
- Strip Qt6-only grouped palette syntax from QML at configure time
(ThemeManager::applyPalette handles palette via QGuiApplication)
- Fix QStringList brace-init ambiguity in Qt5
- Fix QList::remove(pos,count) not available in Qt5 (use erase)
- Introduce StreamFsm class wrapping QProcess lifecycle with
Oneshot/Looping/Guarded restart policies
- Replace raw QProcess management in Ollie9pClient with StreamFsm
- Fix chatblockmodel FSM to properly handle [[[end]]] and fence
transitions with a combined State enum (Outside/InBlock/InFence)
- Add Q_INVOKABLE to setActiveSessionId for direct QML access
- Add streamfsm.cpp to CMakeLists.txt for both KF5 and KF6 builds
- Changed path from session/eventwait to /eventwait (global, not per-session)
- Use ollie-9p instead of plan9port 9p for the read
- Changed from parsing full session index to just calling refreshSessions()
on any event (the event format is now structured deltas, but the GUI
is kept simple with a full index refresh)
startStateStream now trusts the explicit session/agent selection and
opens session/<sid>/agent/<aid>/statewait directly. It no longer calls
getAgents(), which was repeatedly listing session/<sid>/agent on every
statewait reopen.
- Session row click now calls setActiveSessionId(id)
- setActiveSessionId no longer returns early for same-session clicks;
it always clears m_agentId and stops per-agent streams
- This enforces a clean 'session selected, no agent selected' state
- setActiveSessionId remains session-only and never starts streams
- setActiveAgentId now clears any existing per-agent streams and only
updates selection/state
- switchAgent is explicitly the only place that starts chat/statewait
- onStateFinished only reopens for the current explicit selection
- setActiveSessionId now selects only the session; it clears m_agentId
and never auto-selects a lone agent.
- startStateStream no longer re-resolves or falls back to another agent;
it only runs when the explicit active agent still exists.
- QML now handles activeAgentId changes explicitly and keeps the right
pane blank/disabled when no session or no agent is selected.
Add a run9p stat check before opening blocking reads on
statewait and chat. If the agent directory doesn't exist,
skip starting (prevents tight restart loops when a session
is killed while streams are active). Only one statewait
reader runs at a time.
onStateFinished and onChatFinished no longer restart their
streams. Streams are only started when the user explicitly
selects an agent (setActiveSessionId for single-agent,
switchAgent for multi-agent). This prevents tight restart
loops when pointing to a nonexistent directory.
- Deduplicate session entries in refreshSessions and onEventReadyRead
using QSet to skip duplicate session IDs from multi-agent index.
- Fix field mapping: index format is name\tstate\tcwd\tbackend\tmodel\tagent
- Added QSet include
- setActiveSessionId: only auto-select agent when exactly ONE exists.
With multiple agents, clear m_agentId and let user click on
an agent node in the tree to activate it.
- startStateStream: verify current agent directory still exists
instead of always re-discovering to the first agent. Only
fall back to first agent if current agent is gone.
- Emit activeAgentIdChanged so QML reacts to agent changes.
The previous fix stopped trimming the whole output but still called
.trimmed() on each individual line, which stripped trailing tabs from
empty-session lines like "testsession2\t\t\t\t\t". Now we split
by tab directly without any trimming — empty fields stay as empty
strings and the parts count stays at 6.
refreshSessions() and onEventReadyRead() called .trimmed() on the full
session/idx output, which stripped trailing tab characters from the
last line. An empty session line ("testsession2\t\t\t\t\t")
became just "testsession2" — a single field that failed the size>=2
check and was silently dropped from the session list.
- onEventReadyRead() now parses the eventwait data directly instead of
discarding it and re-reading session/idx
- Removed the 2-second refresh timer that was stealing focus from the
prompt text area on every tick
- ensureRootDataLoaded() now calls refreshSessions() so the initial
session list is populated immediately, not only on events
- Added '(no sessions)' placeholder label in the session tree ListView
when no sessions exist
- getAgents(): filter out 'new' (agent creation file), return empty list
- setActiveSessionId(): skip agent init when no agents exist
- submit/interrupt/kill/streams: guard against empty m_agentId
- prompt input: disable when no agent, show 'No agent' placeholder