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.
148 lines
5.7 KiB
C++
148 lines
5.7 KiB
C++
/* AgentCore: the Qt wrapper around one cst_agent_t.
|
|
*
|
|
* The engine runs turns on a worker thread; every cst_agent_hooks_t callback
|
|
* is turned into a Qt signal, and the two hooks that need an answer
|
|
* (on_permission, on_ask_user) block the worker until the GUI replies. No
|
|
* HTTP, no socket: the GUI owns the engine in its own process. */
|
|
|
|
#ifndef CST_APP_AGENTCORE_H
|
|
#define CST_APP_AGENTCORE_H
|
|
|
|
#include <QByteArray>
|
|
#include <QMutex>
|
|
#include <QObject>
|
|
#include <QString>
|
|
#include <QWaitCondition>
|
|
|
|
#include "cst_agent.h"
|
|
#include "cst_tools.h"
|
|
|
|
class QThread;
|
|
|
|
class AgentCore : public QObject {
|
|
Q_OBJECT
|
|
Q_PROPERTY(bool running READ running NOTIFY runningChanged)
|
|
Q_PROPERTY(QString workspace READ workspace NOTIFY workspaceChanged)
|
|
Q_PROPERTY(QString mode READ mode NOTIFY modeChanged)
|
|
Q_PROPERTY(QString provider READ provider NOTIFY configChanged)
|
|
Q_PROPERTY(QString model READ model NOTIFY configChanged)
|
|
Q_PROPERTY(int approval READ approval WRITE setApproval NOTIFY approvalChanged)
|
|
Q_PROPERTY(bool planMode READ planMode WRITE setPlanMode NOTIFY planModeChanged)
|
|
Q_PROPERTY(QString todo READ todo NOTIFY todoChanged)
|
|
|
|
public:
|
|
explicit AgentCore(QObject *parent = nullptr);
|
|
~AgentCore() override;
|
|
|
|
/* Start the engine on a workspace. provider/model/apiKey may be empty to
|
|
* fall back to the saved credentials and the provider's default model. */
|
|
bool open(const QString &workspace, const QString &provider,
|
|
const QString &model, const QString &apiKey, QString *err);
|
|
void close();
|
|
|
|
bool running() const { return m_running; }
|
|
QString workspace() const { return m_workspace; }
|
|
QString mode() const { return QString::fromUtf8(m_cfg.mode); }
|
|
QString provider() const { return QString::fromUtf8(m_cfg.provider); }
|
|
QString model() const { return QString::fromUtf8(m_cfg.model); }
|
|
int approval() const { return m_cfg.approval; }
|
|
bool planMode() const { return m_cfg.plan_mode != 0; }
|
|
QString todo() const { return m_todo; }
|
|
|
|
/* Markdown -> HTML with the engine's own renderer (cst_web_md_html), so a
|
|
* message looks the same here as it does in the WebUI. */
|
|
Q_INVOKABLE QString renderMarkdown(const QString &text) const;
|
|
|
|
/* The engine, for the parts that need it (tool registration). */
|
|
cst_agent_t *agent() const { return m_agent; }
|
|
const cst_agent_config_t *config() const { return &m_cfg; }
|
|
|
|
/* Ask a question; emits the streaming signals, then turnFinished(). */
|
|
Q_INVOKABLE void send(const QString &prompt);
|
|
Q_INVOKABLE void cancel();
|
|
|
|
/* Consent: answers map to CST_PERM_*. */
|
|
Q_INVOKABLE void answerPermission(int choice);
|
|
Q_INVOKABLE void answerQuestion(const QString &answer);
|
|
|
|
Q_INVOKABLE void setMode(const QString &mode); /* coding | general */
|
|
Q_INVOKABLE void setApproval(int approval); /* cst_approval_t */
|
|
Q_INVOKABLE void setPlanMode(bool on);
|
|
|
|
/* Tools the front end implements itself. The engine copies the definition
|
|
* (strings included), so a stack value is fine; ud must outlive it.
|
|
* owner is what unregisterOwner() later matches. */
|
|
bool registerTool(const cst_tool_def_t *def);
|
|
void unregisterOwner(const QString &owner);
|
|
/* Re-probe platform tools after a service appeared or went away. */
|
|
void reloadToolMeta() { cst_tool_meta_reload(); }
|
|
|
|
/* Answer every consent question with "once" (the C smoke test, headless
|
|
* runs, a --yes flag). */
|
|
void setAutoApprove(bool on) { m_autoApprove = on; }
|
|
|
|
signals:
|
|
void configChanged();
|
|
void approvalChanged();
|
|
void planModeChanged();
|
|
void statusChanged(const QString &text);
|
|
void assistantDelta(const QString &text);
|
|
void thinkDelta(const QString &text);
|
|
void toolEvent(const QString &phase, const QString &name,
|
|
const QString &detail);
|
|
void toolResult(const QString &name, int exitCode, const QString &output);
|
|
void todoChanged(const QString &text);
|
|
void permissionRequested(const QString &tool, const QString &detail,
|
|
const QString &preview);
|
|
void questionAsked(const QString &question, const QString &options);
|
|
void turnFinished(int rc, const QString &err);
|
|
void runningChanged();
|
|
void workspaceChanged();
|
|
void modeChanged();
|
|
|
|
private:
|
|
friend class Turn;
|
|
void finishTurn(int rc, const QString &err);
|
|
void setRunning(bool on);
|
|
|
|
/* Hook trampolines (C, run on the worker thread). */
|
|
static void hookStatus(const char *msg, void *ud);
|
|
static void hookAssistant(const char *text, size_t len, void *ud);
|
|
static void hookThink(const char *text, size_t len, void *ud);
|
|
static void hookToolEvent(const char *phase, const ctl_tool_call_t *call,
|
|
const char *detail, void *ud);
|
|
static void hookToolResult(const ctl_tool_call_t *call, const char *out,
|
|
size_t len, int exit_code, void *ud);
|
|
static void hookTodo(const char *text, void *ud);
|
|
static int hookPermission(const char *tool, const char *detail,
|
|
const char *preview, void *ud);
|
|
static int hookAskUser(const char *question, const char *options, char *ans,
|
|
size_t ans_sz, void *ud);
|
|
/* Post a lambda onto the GUI thread. */
|
|
template <typename F>
|
|
void post(F f) {
|
|
QMetaObject::invokeMethod(this, f, Qt::QueuedConnection);
|
|
}
|
|
|
|
cst_agent_t *m_agent = nullptr;
|
|
cst_agent_config_t m_cfg;
|
|
cst_agent_hooks_t m_hooks;
|
|
QString m_workspace;
|
|
QThread *m_turn = nullptr;
|
|
bool m_running = false;
|
|
bool m_closing = false;
|
|
|
|
/* consent handshake between the worker and the GUI thread */
|
|
QMutex m_askMu;
|
|
QWaitCondition m_askCv;
|
|
int m_permAnswer = CST_PERM_DENY;
|
|
bool m_permPending = false;
|
|
QString m_question;
|
|
QString m_todo;
|
|
QByteArray m_answer;
|
|
bool m_questionPending = false;
|
|
bool m_autoApprove = false;
|
|
};
|
|
|
|
#endif /* CST_APP_AGENTCORE_H */
|