When a fence () closes inside a block, prepend any content
that came before the fence to renderedHtml so both text and the
highlighted fence show up. When content is appended after the fence,
keep renderedHtml in sync by appending the escaped text. This prevents
the fence from 'eating' surrounding content.
- 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.
The dominant 9P spam came from buildSessionTree() re-listing
session/<sid>/agent on every activeSessionIdChanged and
activeAgentIdChanged signal. Tree rebuilds are now limited to
sessionsChanged and startup refresh; selection changes only
refresh the right pane transcript.
- 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.
- ListView and its placeholder label now have activeFocusOnTab: false
and focus: false so clicking the empty sidebar doesn't steal focus
from the prompt text area
- 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
- Call ollie.refreshSessions() from Component.onCompleted before
buildSessionTree() for reliable initial load
- Add a 2-second periodic refresh timer as fallback for missed events
- 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
- Add Interrupt block type to ChatBlock enum
- Detect <system-user-interruption> and </system-user-interruption>
tags as block openers/closers in the parser
- Render interrupt blocks with bright yellow header
(#3d2e00 dark, #fff8e1 light)
- Show ⚠ User Interrupt header label
Right-click now stores the target session ID in a root-level
property (killTargetSessionId) before popping the menu, avoiding
unreliable QML parent chain traversal that could resolve to the
wrong delegate.
Session-level kill (session/{id}/ctl) does the full cleanup:
removes from session map, cancels context, closes agent, kills
toolsrv process, removes persisted JSON. Agent-level kill just
cancels the session context without cleaning up the filesystem.
Also remove unused getAgents call.
- Add killSession(sessionId) Q_INVOKABLE to Ollie9pClient
- Accept right button in session tree MouseArea
- Context menu with Kill option on right-click
- MenuItem resolves session ID via parent chain
Replace the single-bracket INI-like format with triple-bracket blocks
with explicit [[[end]]] terminators. The parser tracks m_inBlock to
ignore content between blocks, making the format robust against
accidental header-like content in tool output or LLM responses.
- Go sender: FormatEvent + NewEventHandler + replayMessagesToLog
- C++ parser: new regex for [[[type]]], new [[[end]]] handler, m_inBlock gate
- QML: update hardcoded [info] string
- Remove 300ms click delay (timer-based single/double-click disambiguation)
- Use native QML onDoubleClicked for rename instead of timer hack
- Capture model index as delegate property to fix rename targeting all nodes
- Remove dead rename code that was orphaned by the timer removal
- Clean up Escape shortcut handler
The C++ connection mapping activeSessionIdChanged to chatModel->reset()
raced against the QML onActiveSessionIdChanged handler which does the
actual reload (reset → readLogForSession → appendData → trimFront).
When the C++ slot fired after the QML handler, it cleared the freshly
loaded chat data.
Remove the redundant C++ connection — the QML handler owns the full
reset+reload sequence and sequences the 9P log read correctly.
buildSessionTree() was clearing the tree and setting expanded=true
only for the active session, collapsing all other sessions whenever
an agent in a different session was selected.
Now snapshot expanded state before clearing and restore it, so manually
expanded sessions stay open across agent switches.