The window is no longer one file pretending to be a chat box. The transcript, the run panel, the composer and the two consent dialogs are their own QML components over one Theme singleton, and the shell around them adapts to the screen: docked and scrollable on a desktop, a slide-out drawer on a phone. QML (app/qml/, new except Main.qml): - Theme.qml: single QML singleton, dark Breeze-flavoured tokens (surface, border, accent, text/textDim/textFaint, radius, pad, gap, sizes, monospace); diffLineColor() for unified-diff rows, compactBreakpoint 880, touchTarget 34, segmentHeight 30, headerHeight 42. - ChatView.qml: a row-type dispatcher over the transcript model (message, thought, tool, note), follow-the-tail scrolling with a "jump to latest" button once the user scrolls up, and an empty state that says where the agent runs and what it can do. - MessageCard.qml, ThinkBlock.qml, NoteLine.qml, ToolCard.qml (status pill, arguments, collapsible output, 500-line cut, diff colouring, spinner while the call runs), TodoPanel.qml. - Sidebar.qml: workspace/provider/status, mode and approval segmented rows, plan switch, the engine's todo list, version footer; scrolls, so a short window still reaches every control. - Composer.qml: auto-growing field, Enter sends / Shift+Enter newlines, red Stop while a turn runs, autocorrect hints off, finger-sized targets. - ConsentDialog.qml (Deny / Once / Always -> CST_PERM_*) and AskDialog.qml (the model's options become buttons, free text stays possible). - Main.qml: shell with header, draggable sidebar splitter (hover feedback, 200-560 px, double-click resets), transcript, composer, the two dialogs, Esc to cancel and Ctrl+L to clear. Under 880 px the sidebar becomes a drawer with a scrim instead of squeezing the chat away. AgentCore / main.cpp: - provider, model, approval and planMode are properties now; approval and plan mode write back into the engine's config, and renderMarkdown() calls cst_web_md_html(), so a table or a code fence looks like the WebUI's. - the todo hook writes m_todo on the GUI thread (it was written from the worker thread before: a data race on a QString member). - --size WxH for the window (the layout tests drive it), and the Basic style is chosen by default: the Breeze Quick style registers its types with relative URLs, which floods the log and breaks the composer. UI tests (app/src/ui/): - --uitest drives the real window with synthesized input and exits with the number of failures: 15 checks over docking/drawer transitions, mouse clicks and touch taps reaching the engine's mode, the plan switch, the composer arming the send button, and a 460x900 -> 1200x820 resize round trip. - every control the tests touch carries an objectName; the sidebar's implicitHeight binding loop is gone. Tests: cst-app-selftest and --uitest pass in the standalone app build, ctest cst-app-selftest passes in the repo build (38/38 on the host). Version 0.8.8.
154 lines
7.3 KiB
Markdown
154 lines
7.3 KiB
Markdown
# app/ — the Qt front end
|
|
|
|
CST-agent's GUI: **Qt 6 + QML + the C engine, in one process**. No HTTP
|
|
server, no port, no token, no second process — the app creates a `cst_agent_t`
|
|
directly and drives it through `cst_agent.h`.
|
|
|
|
```
|
|
app/ the GUI
|
|
src/main.cpp entry point, --workspace / --selftest
|
|
src/core/AgentCore.* QObject around cst_agent_t: turns, hooks, consent
|
|
src/core/ToolProvider.h how the front end hands tools to the engine
|
|
src/platform/<os>/ platform tools the app implements itself
|
|
qml/Main.qml the window (chat, tool runs, consent dialog)
|
|
tools/selftest.c C smoke test of the embedding path (no Qt)
|
|
```
|
|
|
|
## Build
|
|
|
|
```sh
|
|
# engine + GUI + C smoke test
|
|
cmake -S app -B build-app -DCMAKE_BUILD_TYPE=Debug
|
|
cmake --build build-app -j
|
|
QT_QPA_PLATFORM=offscreen ctest --test-dir build-app # cst-app-selftest
|
|
|
|
# the app alone
|
|
./build-app/cst-agent-app --workspace ~/notes
|
|
./build-app/cst-agent-app --size 480x900 # the narrow (drawer) layout
|
|
./build-app/cst-agent-app --uitest # drive the window, exit code = failures
|
|
./build-app/cst-agent-app --selftest # same checks as the C test
|
|
```
|
|
|
|
The standalone build is the supported one: `app/CMakeLists.txt` uses the
|
|
`cstcore` target when the parent project defines one, and otherwise builds
|
|
the engine itself from `../src` (TUI, `main.c` and the HTTP front end
|
|
excluded). Wiring it into the repository's `CMakeLists.txt` as
|
|
`-DCST_APP=ON` is on the list below.
|
|
|
|
Needs Qt 6.4+ with **Quick / Qml / QuickControls2** (`qt6.qtdeclarative`,
|
|
`qt6.qtquickcontrols2` on Nix; the online installer's "Qt 6.x Desktop" on
|
|
Windows/macOS). Without them the configure step warns and builds only the C
|
|
smoke test — that is deliberate: the embedding path stays verifiable
|
|
everywhere. Kirigami is not required yet; `qml/Main.qml` uses Qt Quick
|
|
Controls and keeps the layout in Kirigami's shape (single column, cards,
|
|
navigation drawer) so swapping in `org.kde.kirigami` later is a QML-only
|
|
change. Point the app at a Kirigami outside the Qt prefix with
|
|
`-DCST_APP_QML_IMPORT=/path/to/qml`.
|
|
|
|
## Why in one process
|
|
|
|
The engine is a library, not a daemon: `libcstcore.a` (Makefile) / the
|
|
`cstcore` target (CMake). The app owns one `cst_agent_t` per window, so
|
|
there is no protocol to version, no socket to authenticate and no second
|
|
process to babysit — and the CLI, TUI and WebUI keep working off the same
|
|
source.
|
|
|
|
- `AgentCore::send()` runs `cst_agent_run_turn_ex()` on a worker thread;
|
|
the `cst_agent_hooks_t` callbacks turn into Qt signals (assistant deltas,
|
|
tool events, tool results, todo, turn end).
|
|
- Consent is a blocking hook: the worker waits on a `QWaitCondition`, the
|
|
GUI answers, the turn goes on. `CST_PERM_DENY / ONCE / ALWAYS_TOOL /
|
|
ALWAYS_PREFIX` map 1:1 onto the buttons, and `ALWAYS_*` still lands in
|
|
`.agent/permissions.json`, so a desktop decision is visible to the TUI.
|
|
- `cancel()` is `cst_agent_request_cancel()`; the turn unwinds as a normal
|
|
result, not an exception.
|
|
|
|
## Layout and input
|
|
|
|
One shell, three sizes (everything is styled from `qml/Theme.qml`):
|
|
|
|
- **Desktop**: the sidebar is docked and its divider can be dragged (double
|
|
click restores the auto width). Hover states, tooltips, selectable text and
|
|
a wheel-scrolled transcript.
|
|
- **Narrow window** (below `Theme.compactBreakpoint`, 880 px): the sidebar
|
|
folds away, the header grows a `☰` button, and the settings slide in over
|
|
the transcript with a scrim; the sidebar itself scrolls, because a phone
|
|
window cannot show it all.
|
|
- **Touch**: controls keep a `Theme.touchTarget` (34 px) hit area, buttons do
|
|
not depend on hover, the composer asks the platform keyboard not to
|
|
autocorrect (`ImhNoPredictiveText`), and Enter/Shift+Enter still behave.
|
|
|
|
`--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:
|
|
|
|
```sh
|
|
QT_QPA_PLATFORM=offscreen ./build-app/cst-agent-app --uitest
|
|
# ok wide window: sidebar docked
|
|
# ok mouse click on "general" reaches the engine
|
|
# ok touch tap on "coding" reaches the engine
|
|
# ...
|
|
```
|
|
|
|
## Tools the app provides
|
|
|
|
A tool implemented in the front end is registered with `cst_tool_register()`
|
|
and the owner `app:<name>`, exactly like a plugin or MCP server would:
|
|
|
|
```c
|
|
cst_tool_def_t def = {0};
|
|
def.name = "notification_send";
|
|
def.desc = "Send a system notification.";
|
|
def.perm = CST_TP_ASK; /* class: drives the consent dialog */
|
|
def.perm_name = "notify"; /* policy: what the user is really granting */
|
|
def.platforms = ANDROID_ONLY; /* NULL = everywhere */
|
|
def.available = probe; /* 0 / -1 with a reason; cached */
|
|
def.run_ex = my_run;
|
|
def.ud = my_state;
|
|
def.owner = "app:android";
|
|
cst_tool_register(&def);
|
|
```
|
|
|
|
- `platforms` + `available` decide *whether* the tool exists here: an
|
|
Android-only tool never reaches the prompt or `tools[]` on Linux, and
|
|
`cst_tool_run()` refuses it with the reason if a model asks anyway.
|
|
- A front-end tool may take over the **name** of a built-in that cannot run
|
|
on this host (the Android clipboard replacing the desktop `xclip` one);
|
|
it may not shadow one that works.
|
|
- `cst_tool_unregister_owner("app:android")` drops them again — do it when a
|
|
platform service goes away, then `cst_tool_meta_reload()`.
|
|
- `setenv("CST_PLATFORM", ...)` overrides the detected host name (tests, a
|
|
launcher that knows better).
|
|
|
|
Note `run_ex` runs on the worker thread while the turn is going: touch the
|
|
GUI only through queued signals.
|
|
|
|
## Platform notes
|
|
|
|
| Platform | How the engine gets built | Platform tools |
|
|
|---|---|---|
|
|
| Linux | `cmake -S app -B build-app` (libcurl via pkg-config) | clipboard (shell helpers), notification (`notify-send` / D-Bus) |
|
|
| Windows | Qt's MinGW kit, `cmake -G Ninja -DCMAKE_PREFIX_PATH=<Qt>/lib/cmake`; the engine needs libcurl for Windows (see `docs/README-windows.md`) | notification via the WinRT/tray API, clipboard through the core's Win32 path |
|
|
| Android | engine compiled with the NDK inside the APK (`-DCST_TUI=0`, `CST_NO_WEBUI=1`); the app talks to Android APIs through Qt (JNI) | notification (Android), share sheet, clipboard, file picker |
|
|
| macOS | Qt from the installer, libcurl from the system | notification (`osascript`), clipboard (`pbpaste`/`pbcopy`) |
|
|
|
|
Android's long-term shape: the core lives in the APK (no Termux needed), the
|
|
platform tools are Kotlin/JNI behind the same `cst_tool_register()` contract.
|
|
The build flag that keeps Unix helpers out of the picture is
|
|
`CST_PLATFORM=android` (or the `__ANDROID__` detection inside the engine).
|
|
|
|
## Status
|
|
|
|
- [x] engine as a library (`libcstcore.a` / `cstcore`), in-process embed API
|
|
- [x] tool metadata: `perm_name`, `platforms`, `available`, shadowing rule
|
|
- [x] `AgentCore` (turns, cancel, consent, todo) and the C smoke test
|
|
- [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
|
|
- [ ] a real turn driven through the window (needs a provider key)
|
|
- [ ] session list / switch, attachments, settings page
|
|
- [ ] Kirigami (Breeze look), tray, global shortcuts
|
|
- [ ] Android APK (engine in-process, JNI platform tools)
|