Something went wrong. Try again.
The agentic engineering control plane for the posthuman future
Something went wrong. Try again.
20 kB · 705 lines
C++
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706#include "TerminalPageController.h"
#include <QPointF>
#include <cmath>#include <limits>#include <ranges>
#include "AppModel.h"#include "Commands.h"#include "EditorController.h"#include "PanePool.h"#include "TerminalQuickItem.h"#include "WorkspaceController.h"
namespace {
// Accepts the splitFocused spellings ("splitRight") and the spatial// spellings ("right"); anything unknown falls back to Down.SplitDirection directionFromString(const QString& direction){ if (direction == QLatin1String("splitLeft") || direction == QLatin1String("left")) { return SplitDirection::Left; } if (direction == QLatin1String("splitRight") || direction == QLatin1String("right")) { return SplitDirection::Right; } if (direction == QLatin1String("splitUp") || direction == QLatin1String("up")) { return SplitDirection::Up; } return SplitDirection::Down;}
// The Commands singleton is created lazily by the QML engine, so the runCommand// connection can need a retry. Poll at 20 ms and give up after 5 s: the// singleton appears while the QML tree loads, well inside that window.constexpr int kCommandsRetryIntervalMs = 20;constexpr int kCommandsRetryLimit = 250;
} // namespace
// ---------------------------------------------------------------------------// Construction / DI// ---------------------------------------------------------------------------
TerminalPageController::TerminalPageController(QObject* parent) : QObject(parent){ m_commandsRetry.setInterval(kCommandsRetryIntervalMs); connect(&m_commandsRetry, &QTimer::timeout, this, [this]() { ensureCommandsConnected(); }); ensureModelConnected(); ensureCommandsConnected();}
TerminalPageController::~TerminalPageController() = default;
// Mirror of ensureModelConnected() for the command stream. Commands is a lazy// QML singleton, and main.qml reaches it only after the page's controller is// constructed, so the constructor's attempt finds nothing. onRunCommand cannot// self-heal that: it needs the very connection it would repair. Poll the event// loop instead, and reconnect when the singleton is superseded (the QPointer// reads as null once the old instance dies).void TerminalPageController::ensureCommandsConnected(){ Commands* commands = Commands::instance(); if (commands != nullptr && m_commands == commands) { return; } if (commands == nullptr) { if (m_commandsRetryTicks++ < kCommandsRetryLimit) { if (!m_commandsRetry.isActive()) { m_commandsRetry.start(); } } else { m_commandsRetry.stop(); } return; } connect(commands, &Commands::runCommand, this, &TerminalPageController::onRunCommand); m_commands = commands; m_commandsRetry.stop();}
void TerminalPageController::ensureModelConnected(){ // Self-healing: the cached QPointer may have been invalidated if a // superseded singleton instance was collected. Re-resolve and reconnect. if (m_modelConnected && m_model) { return; } AppModel* model = AppModel::instance(); if (!model) { return; // Engine hasn't created the singleton yet; retry on next call. } if (m_modelConnected) { // The previous connection target died; the new instance needs a fresh // connect (the old one died with the sender). m_modelConnected = false; } m_model = model; connect(model, &AppModel::focusedPaneChanged, this, &TerminalPageController::onFocusedPaneChanged); m_modelConnected = true; m_focusedPaneId = model->focusedPaneId(); m_pool = PanePool::instance();}
void TerminalPageController::setSplitHost(QQuickItem* host){ if (m_splitHost == host) { return; } m_splitHost = host; emit splitHostChanged();}
void TerminalPageController::setParkHost(QQuickItem* host){ if (m_parkHost == host) { return; } m_parkHost = host; emit parkHostChanged();}
void TerminalPageController::setShellShim(QObject* shim){ if (m_shellShim == shim) { return; } m_shellShim = shim; emit shellShimChanged();}
void TerminalPageController::setWorkspaceController(QObject* controller){ if (m_workspaceController == controller) { return; } m_workspaceController = controller; emit workspaceControllerChanged();}
// ---------------------------------------------------------------------------// Facade-index helpers// ---------------------------------------------------------------------------
int TerminalPageController::activeWsIndex() const{ if (!m_model) { return -1; } return m_model->workspaceIndexOf(m_model->activeWorkspaceId());}
int TerminalPageController::activeWorkspaceIndex() const{ // Ported from CommandRegistry.qml activeWorkspaceIndex(): id-compare // walk, 0 fallback when no workspace matches. if (!m_model) { return 0; } const QString activeId = m_model->activeWorkspaceId(); const QList<Workspace*>& workspaces = m_model->workspaces(); for (const auto [index, workspace] : std::views::zip(std::views::iota(0), workspaces)) { if (workspace->id() == activeId) { return index; } } return 0;}
int TerminalPageController::activeTabIndex() const{ if (!m_model) { return -1; } const int wsIndex = activeWsIndex(); if (wsIndex < 0) { return -1; } const QList<Workspace*>& workspaces = m_model->workspaces(); if (wsIndex >= workspaces.size()) { return -1; } return workspaces.at(wsIndex)->activeTabIndex();}
// ---------------------------------------------------------------------------// Page-facing handlers// ---------------------------------------------------------------------------
void TerminalPageController::newTab(){ ensureModelConnected(); if (!m_model) { return; } const int wsIndex = activeWsIndex(); m_model->newTab(wsIndex < 0 ? 0 : wsIndex);}
void TerminalPageController::closeTab(int wsIndex, int tabIndex){ ensureModelConnected(); if (!m_model) { return; } const QList<Workspace*>& workspaces = m_model->workspaces(); if (wsIndex < 0 || wsIndex >= workspaces.size()) { return; } const Workspace* workspace = workspaces.at(wsIndex); if (workspace->tabs().size() <= 1) { // Last tab: the UI asks before the model removes it (the model's // closeTab removes unconditionally). Page decides, then calls the // workspace-delete path itself. emit closeLastTabRequested(wsIndex, workspace->name()); return; } m_model->closeTab(wsIndex, tabIndex);}
void TerminalPageController::selectTab(int wsIndex, int tabIndex){ ensureModelConnected(); if (m_model) { m_model->selectTab(wsIndex, tabIndex); }}
void TerminalPageController::selectWorkspace(int index){ ensureModelConnected(); if (m_model) { m_model->selectWorkspace(index); }}
void TerminalPageController::nextTab(){ ensureModelConnected(); if (m_model) { m_model->nextTab(); }}
void TerminalPageController::previousTab(){ ensureModelConnected(); if (m_model) { m_model->previousTab(); }}
void TerminalPageController::nextWorkspace(){ ensureModelConnected(); if (m_model) { m_model->nextWorkspace(); }}
void TerminalPageController::previousWorkspace(){ ensureModelConnected(); if (m_model) { m_model->previousWorkspace(); }}
void TerminalPageController::selectWorkspaceByNumber(int number){ ensureModelConnected(); if (m_model) { m_model->selectWorkspaceByNumber(number); }}
void TerminalPageController::splitFocused(const QString& direction){ ensureModelConnected(); if (!m_model) { return; } const QString paneId = m_model->focusedPaneId(); if (paneId.isEmpty()) { return; } m_model->splitPane(paneId, directionFromString(direction));}
void TerminalPageController::closeFocused(){ ensureModelConnected(); if (!m_model) { return; } const QString paneId = m_model->focusedPaneId(); if (!paneId.isEmpty()) { m_model->closePane(paneId); }}
QString TerminalPageController::openPathInEditor(const QString& anchorPaneId, const QString& path, int line, int column){ ensureModelConnected(); if (m_model == nullptr || path.isEmpty() || anchorPaneId.isEmpty()) { return {}; } QString existing = m_model->editorPaneShowingPath(anchorPaneId, path); if (!existing.isEmpty()) { m_model->focusPane(existing); if (line > 0) { PanePool* pool = m_pool.data() != nullptr ? m_pool.data() : PanePool::instance(); if (pool != nullptr) { EditorController* controller = pool->editorControllerFor(existing); if (controller != nullptr) { controller->requestCursor(line - 1, qMax(0, column - 1)); } } } return existing; } return m_model->splitPaneWithKind(anchorPaneId, QStringLiteral("right"), QStringLiteral("editor"), path, line, column);}
void TerminalPageController::keepOnlyFocused(){ ensureModelConnected(); if (!m_model) { return; } const QString paneId = m_model->focusedPaneId(); if (!paneId.isEmpty()) { m_model->keepOnlyPane(paneId); }}
void TerminalPageController::focusNext(){ ensureModelConnected(); if (!m_model) { return; } const int wsIndex = activeWsIndex(); const int tabIndexInWs = activeTabIndex(); if (wsIndex < 0 || tabIndexInWs < 0) { return; } SplitContainerNode* tree = m_model->tabTree(wsIndex, tabIndexInWs); if (!tree) { return; } const QStringList ids = paneIdsInLayoutOrder(*tree); const qsizetype count = ids.size(); if (count == 0) { return; } const qsizetype current = ids.indexOf(m_model->focusedPaneId()); const qsizetype next = (current + 1) % count; m_model->focusPane(ids.at(next));}
void TerminalPageController::focusPrevious(){ ensureModelConnected(); if (!m_model) { return; } const int wsIndex = activeWsIndex(); const int tabIndexInWs = activeTabIndex(); if (wsIndex < 0 || tabIndexInWs < 0) { return; } SplitContainerNode* tree = m_model->tabTree(wsIndex, tabIndexInWs); if (!tree) { return; } const QStringList ids = paneIdsInLayoutOrder(*tree); const qsizetype count = ids.size(); if (count == 0) { return; } const qsizetype current = ids.indexOf(m_model->focusedPaneId()); const qsizetype previous = (current - 1 + count) % count; m_model->focusPane(ids.at(previous));}
void TerminalPageController::focusDirection(const QString& direction){ ensureModelConnected(); if (!m_model) { return; } const QString paneId = m_model->focusedPaneId(); if (paneId.isEmpty()) { return; } m_model->focusDirection(paneId, directionFromString(direction));}
void TerminalPageController::swapDirection(const QString& direction){ ensureModelConnected(); if (!m_model) { return; } const QString srcId = m_model->focusedPaneId(); if (srcId.isEmpty()) { return; } const QString targetId = spatialNeighborPaneId(srcId, direction); if (targetId.isEmpty()) { return; } m_model->swapPanes(srcId, targetId);}
void TerminalPageController::copyFocused(){ ensureModelConnected(); if (!m_model) { return; } m_pool = PanePool::instance(); if (!m_pool) { return; } auto* term = qobject_cast<TerminalQuickItem*>( m_pool->item(m_model->focusedPaneId())); if (term) { term->copySelection(); }}
void TerminalPageController::pasteFocused(){ ensureModelConnected(); if (!m_model) { return; } m_pool = PanePool::instance(); if (!m_pool) { return; } auto* term = qobject_cast<TerminalQuickItem*>( m_pool->item(m_model->focusedPaneId())); if (term) { term->pasteFromClipboard(); }}
void TerminalPageController::resetFocused(){ ensureModelConnected(); if (!m_model) { return; } m_pool = PanePool::instance(); if (!m_pool) { return; } auto* term = qobject_cast<TerminalQuickItem*>( m_pool->item(m_model->focusedPaneId())); if (term) { term->resetTerminal(); }}
void TerminalPageController::resetLayout(){ ensureModelConnected(); if (m_model) { m_model->resetLayout(); }}
void TerminalPageController::jumpTo(const QVariantMap& candidate){ ensureModelConnected(); if (!m_model) { return; } const QString paneId = candidate.value(QStringLiteral("paneId")).toString(); if (!paneId.isEmpty()) { m_model->revealPane(paneId); return; } const QString tabId = candidate.value(QStringLiteral("tabId")).toString(); if (!tabId.isEmpty()) { const QString workspaceId = candidate.value(QStringLiteral("workspaceId")).toString(); const int wsIndex = m_model->workspaceIndexOf(workspaceId); if (wsIndex >= 0) { m_model->selectWorkspace(wsIndex); m_model->selectTab(wsIndex, m_model->tabIndexOf(workspaceId, tabId)); } return; } const QString workspaceId = candidate.value(QStringLiteral("workspaceId")).toString(); if (!workspaceId.isEmpty()) { m_model->selectWorkspace(m_model->workspaceIndexOf(workspaceId)); }}
void TerminalPageController::paneClicked(const QString& paneId){ ensureModelConnected(); if (m_model && !paneId.isEmpty()) { m_model->focusPane(paneId); }}
void TerminalPageController::refocusFocused(){ ensureModelConnected(); if (!m_model) { return; } m_pool = PanePool::instance(); if (!m_pool) { return; } const QString paneId = m_model->focusedPaneId(); if (paneId.isEmpty()) { return; } auto* term = qobject_cast<TerminalQuickItem*>(m_pool->item(paneId)); if (term) { term->forceActiveFocus(); }}
QObject* TerminalPageController::focusedPaneItem(){ // Resolve the model lazily: the controller is constructed before the // AppModel QML singleton exists, and the boot focusedPaneChanged emit // may predate the connect — so the initial id is picked up here rather // than only via signal. ensureModelConnected(); if (!m_model) { return nullptr; } const QString paneId = m_model->focusedPaneId(); if (paneId.isEmpty()) { return nullptr; } PanePool* pool = m_pool.data(); if (!pool) { pool = PanePool::instance(); } if (pool) { return pool->item(paneId); } return nullptr;}
// ---------------------------------------------------------------------------// Focus mirror// ---------------------------------------------------------------------------
void TerminalPageController::onFocusedPaneChanged(const QString& paneId){ ensureModelConnected(); if (m_focusedPaneId != paneId) { m_focusedPaneId = paneId; emit focusedPaneIdChanged(); } if (paneId.isEmpty()) { return; } m_pool = PanePool::instance(); if (!m_pool) { return; } auto* term = qobject_cast<TerminalQuickItem*>(m_pool->item(paneId)); if (term) { term->forceActiveFocus(); }}
// ---------------------------------------------------------------------------// Pane-level helpers (by paneId through the pool)// ---------------------------------------------------------------------------
QString TerminalPageController::spatialNeighborPaneId( const QString& fromPaneId, const QString& direction) const{ if (fromPaneId.isEmpty() || !m_model) { return {}; } const int wsIndex = activeWsIndex(); const int tabIndexInWs = activeTabIndex(); if (wsIndex < 0 || tabIndexInWs < 0) { return {}; } SplitContainerNode* tree = m_model->tabTree(wsIndex, tabIndexInWs); if (!tree) { return {}; } // Model-native: the same tree geometry the model owns. return ::spatialNeighborPaneId(*tree, fromPaneId, direction);}// ---------------------------------------------------------------------------// Commands pub-sub// ---------------------------------------------------------------------------
void TerminalPageController::onRunCommand(const QString& commandId, const QVariant& ctx){ // ctx arrives as a QVariant: a map {index: int, sidebar: QVariant()} for // the workspace-row commands, or invalid for global ones. Safe-extract. const QVariantMap ctxMap = ctx.toMap(); const int ctxIndex = ctxMap.value("index", -1).toInt();
// Workspace commands route through the injected WorkspaceController (the // page assigns it; guarded while unwired). auto* workspaceController = qobject_cast<WorkspaceController*>(m_workspaceController.data());
if (commandId == QLatin1String("jumpToPane")) { emit openJumpRequested(); } else if (commandId == QLatin1String("commandPalette")) { emit openPaletteRequested(); } else if (commandId == QLatin1String("preferences")) { emit openSettingsRequested(); } else if (commandId == QLatin1String("missionControl")) { emit toggleMissionControlRequested(); } else if (commandId == QLatin1String("toggleSidebar")) { emit toggleSidebarRequested(); } else if (commandId == QLatin1String("newWorkspace")) { // The sidebar owns the AddWorkspaceMenu popup. emit openAddWorkspaceRequested(); } else if (commandId == QLatin1String("addSameDirectory")) { if (workspaceController) { // Kind 0 = Directory (WorkspaceKind enum order). workspaceController->addWorkspaceDirect( 0, WorkspaceController::currentDir(), "", ""); } } else if (commandId == QLatin1String("addJjWorkspace")) { if (workspaceController) { workspaceController->pickJjWorkspace(ctxIndex); } } else if (commandId == QLatin1String("addGitWorktree")) { if (workspaceController) { workspaceController->pickGitWorktree(ctxIndex); } } else if (commandId == QLatin1String("addLocation")) { emit openPathFuzzyRequested(); } else if (commandId == QLatin1String("closeWorkspace")) { if (m_model) { m_model->deleteWorkspace(activeWorkspaceIndex()); } } else if (commandId == QLatin1String("workspaceSetDirectory")) { emit setDirectoryRequested(ctxIndex); } else if (commandId == QLatin1String("workspaceForgetJj")) { if (workspaceController && ctxIndex >= 0) { workspaceController->forgetJjWorkspace(ctxIndex); } } else if (commandId == QLatin1String("workspaceDelete")) { if (m_model && ctxIndex >= 0) { m_model->deleteWorkspace(ctxIndex); } } else if (commandId == QLatin1String("workspaceRename")) { // The sidebar owns the inline rename edit UI. emit renameWorkspaceRequested(ctxIndex); } else if (commandId == QLatin1String("renameTab")) { // The rename dialog is a UI decision point; the page/main.qml owns it. emit renameTabRequested(); } else if (commandId == QLatin1String("openSearch")) { // A pane-targeted UI decision point: the page owns the search bar. emit openSearchRequested(); } else if (commandId == QLatin1String("closeSearch")) { emit closeSearchRequested(); } else if (commandId == QLatin1String("searchNext")) { if (auto* term = qobject_cast<TerminalQuickItem*>(focusedPaneItem())) { term->searchNext(); } } else if (commandId == QLatin1String("searchPrevious")) { if (auto* term = qobject_cast<TerminalQuickItem*>(focusedPaneItem())) { term->searchPrevious(); } } else if (commandId == QLatin1String("enterCopyMode")) { if (auto* term = qobject_cast<TerminalQuickItem*>(focusedPaneItem())) { term->enterCopyMode(); } } else if (commandId == QLatin1String("exitCopyMode")) { if (auto* term = qobject_cast<TerminalQuickItem*>(focusedPaneItem())) { term->exitCopyMode(); } }}