Skip to content

Commit 885fab7

Browse files
feat(control): command API for external agents, covering every feature
Agents no longer probe and click the UI. They call commands (GET /commands, POST /commands/<name>) and the UI shows what each command changed: the tab or panel comes to the front and flashes, the status bar names the step, the activity log keeps it (docs/adr/0007). 230 commands across app, menu, workspace, ssh, tabs, files, editor, search, preview, browser, mini apps, csv, hex, embed, settings, lua, macro, terminal, tasks, pm2, git, ai, goal and schedule. A decision a user would make in a dialog is an argument; without it the command answers 409 with `needs`. Skill v4 carries recipes for each domain and a command reference generated from the registry. Bugs found by driving every domain live and fixed: - A dialog the app opened during a command (the reload prompt that a CSV preview save raises in the file's text editor) held the answer until the user closed it. ControlApi::execute now cancels a modal or popup opened while the handler is on the stack and reports it as `dismissedDialogs`. - app.quit counted only modified buffers, so an editor whose file was deleted still raised the save prompt after "discard"; the file watch also learns of a deletion late. It now refreshes file state, uses the window's own rule, and pre-answers the window's prompt for that close only. - preview.open fell back to any non-editor tab (a terminal); git.diff with nothing tracked changed failed instead of answering an empty diff; agent edits bypassed the macro recorder; settings.list was empty (moc ignores `public slots:` inside a macro); terminal.read counted blank rows; git commands answered with stale status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 29bf7dd commit 885fab7

113 files changed

Lines changed: 12672 additions & 6502 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ In a chat, a finished turn folds into a single "Worked for 2m14s" row; click it
1414

1515
There is also a Goal Agent: pick an ACP judge (Claude Code, Codex, Opencode) or a Custom API endpoint that speaks Anthropic's Messages API. You set success criteria and it drives the working agent until those criteria hold, or attach a goal to an existing conversation. Criteria presets can be saved globally, or into the current project's `.agents/.goals` folder so they travel with the repo; files already in that folder show up in the load list. A judge that needs a human action, or that hits the iteration cap with the criterion still unmet, stops and hands the criterion back instead of marking the goal done. With Prefix /goal checked, the first criterion is sent immediately when the agent supports `/goal`. If a reply is still running and the agent does not support `/goal`, that goal waits in the send queue so the current reply is not stopped. Later criteria wait until the previous is achieved. Send with Goal defaults to the agent's own `/goal` command instead: each criterion goes out on its own, with no separate judge. A `/goal` typed in the composer is sent while a turn is still running only when the agent supports that command; otherwise it waits in the queue. A goal is not judged, and auto compact does not run, while the agent still has background tasks running or a wake-up turn pending. When a goal is achieved, the session can compact automatically. Prompts sent while an agent is still working are queued and delivered when it finishes. AI chats group by project, with a session strip to switch conversations in the same dock. Open chats come back after a restart and resume their agent session on the first prompt. A local Claude Code chat can be moved to another agent from the agent button in the dock's title bar, keeping its transcript. The composer can ask the agent to work in a new git worktree. When you commit, AI can write the commit message from your staged diff. That generator (and other LLM calls) can talk to an OpenAI-compatible endpoint or Anthropic's Messages API. Leave the commit box empty (when not amending) and Commit lets you pick an ACP agent to do the commit. Protocol details and how to wire up a custom ACP agent are in [doc/AcpAgents.md](doc/AcpAgents.md).
1616

17-
An outside AI agent can also drive NotepadAI itself. AI ▸ Connect External Agent turns on a local HTTP API (off by default, token-protected) and gives you a prompt to hand to the agent; it fetches a skill from the app describing how to use it. The agent works only through the controls you see: menus, buttons, fields, tabs, editors, terminals and chats, each step highlighted on screen and listed in an activity log, with a Stop button in the status bar. Workspaces, terminals and tasks, AI sessions, goals, git, settings and pane sizes are all reachable this way.
17+
An outside AI agent can also drive NotepadAI itself. AI ▸ Connect External Agent turns on a local HTTP API (off by default, token-protected) and gives you a prompt to hand to the agent; it fetches a skill from the app describing how to use it. The agent gives NotepadAI commands (open a file, edit it, run a command in a terminal, send a prompt to an AI session and answer its questions, commit and push, change a setting) and the UI shows each one as it happens: what it changed comes to the front and flashes, the status bar names the step, and an activity log lists every step, with a Stop button in the status bar. Workspaces, files and editors, search, terminals and tasks, pm2 processes, git, AI sessions, goals, scheduled tasks, the browser and settings all have commands; the app lists them with their arguments, so the skill's reference is always current.
1818

1919
### Git
2020

‎src/AcpAgentPicker.h‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
#include "AcpAgentDefinition.h"
2323
#include "AcpAgentIcons.h"
2424
#include "AcpAgentRegistry.h"
25-
#include "control/ControlIds.h"
2625

2726
#include <QAction>
2827
#include <QFont>
@@ -61,7 +60,6 @@ inline void fillAcpAgentPickerMenu(QMenu *menu,
6160
return;
6261
for (const AcpAgentDefinition &agent : agents) {
6362
QAction *action = menu->addAction(acpAgentIcon(agent, menu->palette()), agent.name);
64-
control::ids::setKey(action, QStringLiteral("agent:") + agent.id);
6563
if (agent.id == defaultId) {
6664
QFont f = action->font();
6765
f.setBold(true);

‎src/ApplicationSettings.h‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,14 @@ class ApplicationSetting
5555
};
5656

5757

58+
// The getter is invokable and the setter a slot so the Control API can list,
59+
// read and change every setting through the meta-object (settings.list /
60+
// settings.set, docs/adr/0007). Q_SLOT, not "public slots:": moc ignores an
61+
// access section inside a macro, so the setters were missing from it.
5862
#define DEFINE_SETTING(name, lname, type)\
5963
public:\
60-
type lname() const;\
61-
public slots:\
62-
void set##name(type lname);\
64+
Q_INVOKABLE type lname() const;\
65+
Q_SLOT void set##name(type lname);\
6366
Q_SIGNAL\
6467
void lname##Changed(type lname);\
6568

‎src/CMakeLists.txt‎

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -595,42 +595,43 @@ qt_add_executable(NotepadAI
595595
pm2/Pm2ProcessesDialog.h
596596
pm2/Pm2ProcessesDialog.cpp
597597
pm2/pm2.qrc
598-
control/ControlActionIndex.h
599-
control/ControlActionIndex.cpp
600598
control/ControlActivityLog.h
601599
control/ControlActivityLog.cpp
602600
control/ControlApi.h
603601
control/ControlApi.cpp
604-
control/ControlAppAdapters.h
605-
control/ControlAppAdapters.cpp
606602
control/ControlApiDialog.h
607-
control/ControlScreenshot.h
608-
control/ControlScreenshot.cpp
609603
control/ControlApiDialog.cpp
610604
control/ControlAudit.h
611605
control/ControlAudit.cpp
612606
control/ControlAuth.h
613607
control/ControlAuth.cpp
614-
control/ControlEffects.h
615-
control/ControlEffects.cpp
608+
control/ControlCommands.h
609+
control/ControlCommands.cpp
616610
control/ControlEvents.h
617611
control/ControlEvents.cpp
618612
control/ControlFeedback.h
619613
control/ControlFeedback.cpp
620614
control/ControlHttp.h
621615
control/ControlHttp.cpp
622-
control/ControlIds.h
623-
control/ControlIds.cpp
616+
control/ControlScreenshot.h
617+
control/ControlScreenshot.cpp
624618
control/ControlServer.h
625619
control/ControlServer.cpp
626620
control/ControlStatusIndicator.h
627621
control/ControlStatusIndicator.cpp
628-
control/ControlAdapter.h
629-
control/ControlStandardAdapters.cpp
630-
control/ControlPicker.h
631-
control/ControlPicker.cpp
632-
control/ControlUiTree.h
633-
control/ControlUiTree.cpp
622+
control/commands/Registrar.h
623+
control/commands/Registrar.cpp
624+
control/commands/AppCommands.cpp
625+
control/commands/SettingsCommands.cpp
626+
control/commands/WorkspaceCommands.cpp
627+
control/commands/EditorCommands.cpp
628+
control/commands/PreviewCommands.cpp
629+
control/commands/TerminalCommands.cpp
630+
control/commands/Pm2Commands.cpp
631+
control/commands/GitCommands.cpp
632+
control/commands/AiCommands.cpp
633+
control/commands/TransferCommands.cpp
634+
control/commands/CsvCommands.cpp
634635
control/control.qrc
635636
MiniAppDefinition.h
636637
MiniAppMenuLayout.h

‎src/CrocShareFlow.cpp‎

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@ CrocShareFlow::CrocShareFlow(QWidget *dialogParent)
4343
m_loadDlg->setLabelText(text);
4444
});
4545
connect(m_croc, &CrocTool::codePhraseReady, this, [this](const QString &phrase) {
46+
emit phraseReady(phrase);
4647
showPhrase(phrase);
4748
});
4849
connect(m_croc, &CrocTool::sendFinished, this, [this]() {
50+
emit finished();
4951
if (m_phraseDlg)
5052
m_phraseDlg->close();
5153
cleanupTemp();
@@ -78,14 +80,17 @@ CrocShareFlow::CrocShareFlow(QWidget *dialogParent)
7880
// Temp dir lives until this flow is destroyed; destroy after extract starts.
7981
// Extract reads the zip on a worker; keep tmp until zip transfer completes.
8082
connect(m_zip, &FolderZipTransfer::transferCompleted, this, [this](int) {
83+
emit finished();
8184
cleanupTemp();
8285
deleteLater();
8386
});
84-
connect(m_zip, &FolderZipTransfer::transferError, this, [this](const QString &) {
87+
connect(m_zip, &FolderZipTransfer::transferError, this, [this](const QString &msg) {
88+
emit failed(msg);
8589
cleanupTemp();
8690
deleteLater();
8791
});
8892
connect(m_zip, &FolderZipTransfer::transferCancelled, this, [this]() {
93+
emit failed(tr("The extraction was cancelled."));
8994
cleanupTemp();
9095
deleteLater();
9196
});
@@ -104,6 +109,17 @@ void CrocShareFlow::closeLoading()
104109

105110
void CrocShareFlow::showError(const QString &msg)
106111
{
112+
emit failed(msg);
113+
if (m_quiet) {
114+
closeLoading();
115+
if (m_phraseDlg) {
116+
m_phraseDlg->disconnect();
117+
m_phraseDlg->hide();
118+
m_phraseDlg->deleteLater();
119+
m_phraseDlg = nullptr;
120+
}
121+
return;
122+
}
107123
QWidget *anchor = m_parent;
108124
if (m_loadDlg) {
109125
m_loadDlg->disconnect();
@@ -174,7 +190,7 @@ void CrocShareFlow::shareFolder(FolderZipTransfer *zip, const QString &workspace
174190
m_zip = zip;
175191
m_tmp = new QTemporaryDir();
176192
if (!m_tmp->isValid()) {
177-
QMessageBox::warning(m_parent, tr("croc"), tr("Could not create a temp folder."));
193+
showError(tr("Could not create a temp folder."));
178194
deleteLater();
179195
return;
180196
}
@@ -192,11 +208,12 @@ void CrocShareFlow::shareFolder(FolderZipTransfer *zip, const QString &workspace
192208
m_croc->sendFile(m_zipPath);
193209
}, Qt::SingleShotConnection);
194210
connect(zip, &FolderZipTransfer::transferError, this, [this](const QString &msg) {
195-
QMessageBox::warning(m_parent, tr("Zip"), msg);
211+
showError(msg);
196212
cleanupTemp();
197213
deleteLater();
198214
}, Qt::SingleShotConnection);
199215
connect(zip, &FolderZipTransfer::transferCancelled, this, [this]() {
216+
emit failed(tr("Zipping was cancelled."));
200217
cleanupTemp();
201218
deleteLater();
202219
}, Qt::SingleShotConnection);
@@ -249,24 +266,33 @@ void CrocShareFlow::showPhrase(const QString &phrase)
249266
void CrocShareFlow::receiveIntoFolder(FolderZipTransfer *zip, const QString &workspaceRoot,
250267
const QString &folderPath, bool ssh)
251268
{
252-
m_zip = zip;
253-
m_extractWorkspaceRoot = workspaceRoot;
254-
m_extractTarget = folderPath;
255-
m_extractSsh = ssh;
256269
bool ok = false;
257270
const QString phrase = QInputDialog::getText(m_parent, tr("croc code-phrase"),
258271
tr("Code-phrase:"), QLineEdit::Normal,
259272
QString(), &ok).trimmed();
260-
if (!ok || !CrocTool::isValidCodePhrase(phrase)) {
261-
if (ok)
262-
QMessageBox::warning(m_parent, tr("croc"), tr("That does not look like a croc code-phrase."));
273+
if (!ok) {
274+
deleteLater();
275+
return;
276+
}
277+
receiveIntoFolder(zip, workspaceRoot, folderPath, ssh, phrase);
278+
}
279+
280+
void CrocShareFlow::receiveIntoFolder(FolderZipTransfer *zip, const QString &workspaceRoot,
281+
const QString &folderPath, bool ssh, const QString &phrase)
282+
{
283+
m_zip = zip;
284+
m_extractWorkspaceRoot = workspaceRoot;
285+
m_extractTarget = folderPath;
286+
m_extractSsh = ssh;
287+
if (!CrocTool::isValidCodePhrase(phrase)) {
288+
showError(tr("That does not look like a croc code-phrase."));
263289
deleteLater();
264290
return;
265291
}
266292
ensureThen([this, phrase]() {
267293
m_tmp = new QTemporaryDir();
268294
if (!m_tmp->isValid()) {
269-
QMessageBox::warning(m_parent, tr("croc"), tr("Could not create a temp folder."));
295+
showError(tr("Could not create a temp folder."));
270296
deleteLater();
271297
return;
272298
}

‎src/CrocShareFlow.h‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ class CrocShareFlow : public QObject
3030
const QString &folderPath, const QString &folderName, bool ssh);
3131
void receiveIntoFolder(FolderZipTransfer *zip, const QString &workspaceRoot,
3232
const QString &folderPath, bool ssh);
33+
// The same with the code-phrase given (the Control API) instead of asked.
34+
void receiveIntoFolder(FolderZipTransfer *zip, const QString &workspaceRoot,
35+
const QString &folderPath, bool ssh, const QString &phrase);
36+
// Errors reach the caller through failed() only, without a message box.
37+
void setQuiet(bool quiet) { m_quiet = quiet; }
38+
39+
signals:
40+
// Sharing: the code-phrase to give the other side (also shown to the user).
41+
void phraseReady(const QString &phrase);
42+
// Sharing: the other side received it. Receiving: the files are extracted.
43+
void finished();
44+
void failed(const QString &message);
3345

3446
private:
3547
void ensureThen(const std::function<void()> &next);
@@ -49,6 +61,7 @@ class CrocShareFlow : public QObject
4961
QString m_extractTarget;
5062
QString m_extractWorkspaceRoot;
5163
bool m_extractSsh = false;
64+
bool m_quiet = false;
5265
};
5366

5467
#endif // CROCSHAREFLOW_H

0 commit comments

Comments
 (0)