# 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// 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, 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 # ok wide window: sidebar docked # ok mouse click on "general" reaches the engine # ok touch tap on "coding" reaches the engine # ... ``` ## Sessions A GUI session is a file in the workspace's store (`.agent/sessions/.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. ## Provider, model, key The sidebar shows where the engine points (`provider · model`); the gear in the header opens the dialog that changes it. - The provider list is the engine's own preset table (`cst_provider_count()`), with each entry's base URL and where its key would come from: "key saved" (`.agent`-independent, `/cst-agent/credentials`), "key from OPENAI_API_KEY", or "no key". A key itself is never sent to the QML side — the dialog only ever describes it. - "Use this" calls `AgentCore::useProvider()`, which runs `cst_ps_switch_provider()` / `cst_ps_set_key()` on the engine's live config: a pasted key is verified against the provider's `/models` before it is saved (offline it is kept unverified and the header says so), the model is remembered in `settings.json`, and the **session is not touched**. Model candidates come from the provider entry, the fetched cache (`cst_ps_cached`, no network) and the remembered pick; the label next to the field is `cst_model_info` ("200K · text only"). - The dialog also reports the session's token use (`cst_agent_usage`). `--uitest` checks it end to end: the presets are readable, an unknown provider is refused, the gear opens the panel, the provider rows exist, a model typed into the field reaches the engine when "Use this" is clicked, the note says so, and Close puts the dialog away. The run points `XDG_CONFIG_HOME` at its scratch directory, so settings and keys of the real user are never rewritten. ## Tools the app provides A tool implemented in the front end is registered with `cst_tool_register()` and the owner `app:`, 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=/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 - [x] session list in the sidebar: new / switch / delete, transcript replay - [x] settings dialog: provider, model, key (`cst_ps_*`), token counters - [ ] a real turn driven through the window (needs a provider key) - [ ] attachments (drag in, paste) - [ ] Kirigami (Breeze look), tray, global shortcuts - [ ] Android APK (engine in-process, JNI platform tools)