v0.8.9: sessions in the Qt front end, transcript replay, UI test helpers

A conversation is now something the window can pick. The sidebar lists the
workspace's stored sessions and switching replays one into the view.

Sessions (app/src/core/AgentCore.*, app/qml/SessionRow.qml, Sidebar.qml):
- AgentCore::open() resolves the workspace's current session through
  cst_agent_store_resolve_default() before the engine starts. Without it the
  engine opened session.jsonl in the process's working directory, next to a
  session list that stayed empty.
- sessionId property; sessions() (id, title, firstUser, messages, mtime in ms,
  current); newSession(), switchSession(), deleteSession() (moves the engine
  off a session it has open first), renameSession(); sessionChanged and
  sessionListChanged signals. No mutation runs during a turn.
- history() turns the open session back into the rows the live signals build
  (kind/role/text/name/state/detail/output/exitCode/complete): system prompt
  skipped, notes and attachments shown, assistant reasoning as a think row,
  tool_calls_json as one card per call, and each result paired with its call
  the way findTool() pairs the live stream. A call with no result keeps state
  "unfinished" ("no result"), so a replay never shows a spinner that cannot
  stop. Main.qml replays on sessionChanged.
- SessionRow.qml is a file of its own: as an inline component it would not see
  the Repeater's modelData. Session controls are disabled while a turn runs.

Tests (app/src/ui/UiTest.cpp, app/src/main.cpp):
- the fixture session is written through the store's own API (a tool call with
  its result), then the checks click its row, assert the five replayed rows and
  the paired result, start two new sessions (the view empties), delete one by
  its button (the open one stays), and switch back. 28 checks in total.
- item() falls back to the visual tree: delegates a model owns are not always
  in the window's QObject tree, which is why the rows were invisible to
  findChild even though they were on screen.
- --uitest drives a scratch workspace unless --workspace is given; it creates
  and deletes sessions. CST_APP_SHOT=file saves the window as a PNG when the
  run ends and CST_APP_DUMP=1 prints the session rows seen; both are how the
  layout gets looked at on a machine without a screen.

QML: ThinkBlock's click target is a TapHandler on its row (a MouseArea anchored
inside a layout is undefined behavior and Qt said so).

Verified: 28/28 UI checks, --selftest, ctest cst-app-selftest, make check rc=0,
and a screenshot of a replayed session (cards, tool card with its result, the
think block, the sidebar list).

Version 0.8.9.
This commit is contained in:
WHC
2026-10-11 12:14:59 +08:00
parent da812cc7e8
commit f5d340bd2a
13 changed files with 691 additions and 13 deletions
+35 -4
View File
@@ -80,9 +80,9 @@ One shell, three sizes (everything is styled from `qml/Theme.qml`):
`--uitest` checks this without a human: it loads the window, clicks with
synthesized **mouse** events, taps with a synthesized **touch** device, types
into the composer, then narrows the window to 460 px to assert the dock →
drawer switch and opens/closes the drawer. One line per check, non-zero exit
on failure:
into the composer, drives the session list, then narrows the window to 460 px
to assert the dock → drawer switch and opens/closes the drawer. One line per
check, non-zero exit on failure:
```sh
QT_QPA_PLATFORM=offscreen ./build-app/cst-agent-app --uitest
@@ -92,6 +92,36 @@ QT_QPA_PLATFORM=offscreen ./build-app/cst-agent-app --uitest
# ...
```
## Sessions
A GUI session is a file in the workspace's store (`.agent/sessions/<id>.jsonl`),
the same one the CLI, the TUI and the WebUI use — `AgentCore::open()` resolves
the workspace's current session before the engine starts, so nothing is written
next to the process's working directory.
- The sidebar lists the store (newest first): title or first prompt, message
count, "3h ago"; the open one is highlighted. `+ new` starts a session, `×`
deletes one (the engine first moves off a session it has open, so it never
sits on a file that is gone), and a click switches.
- Switching **replays** the transcript: `AgentCore::history()` walks the stored
messages and returns the same rows the live signals build — messages,
thoughts, notes, and one card per tool call, with its result paired by name
(a call with no result keeps state `unfinished`, so no spinner runs
forever). A reopened session therefore renders like the turn that wrote it.
- Session controls are disabled while a turn runs: switching would move the
engine out from under it.
`--uitest` covers the flow end to end: `writeFixtureSession()` writes a small
session through the store API (a tool call with its result), the checks click
its row, assert the five replayed rows and the paired result, start two new
sessions (the view empties), delete one by its `×` (the open one stays), and
switch back.
`--uitest` uses a scratch workspace unless `--workspace` is given, since it
creates and deletes sessions. On a machine without a screen,
`CST_APP_SHOT=shot.png` makes the run save the window as a PNG when it is
done; `CST_APP_DUMP=1` prints how many session rows the visual tree holds.
## Tools the app provides
A tool implemented in the front end is registered with `cst_tool_register()`
@@ -147,7 +177,8 @@ The build flag that keeps Unix helpers out of the picture is
- [x] QML shell: transcript cards, tool cards with diff coloring, thinking
blocks, consent/ask dialogs, sidebar, responsive dock/drawer layout,
`--uitest` for mouse, touch and small-screen checks
- [x] session list in the sidebar: new / switch / delete, transcript replay
- [ ] a real turn driven through the window (needs a provider key)
- [ ] session list / switch, attachments, settings page
- [ ] attachments (drag in, paste), settings page (provider, model, key)
- [ ] Kirigami (Breeze look), tray, global shortcuts
- [ ] Android APK (engine in-process, JNI platform tools)