Something went wrong. Try again.
The agentic engineering control plane for the posthuman future
Something went wrong. Try again.
15 kB · 365 lines
C
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366#pragma once
// Split-tree model nodes: the in-memory QObject tree of one tab's pane// layout. The nodes ARE the model — AppModel mutates them in place and QML// binds them directly (no proxy mirror). Pure QtCore/QtQml; no Quick.//// Tagged sum: an abstract SplitNode base with a virtual kind() returning a// NodeKind enum, two concrete finals (SplitContainerNode, PaneNode), and an// exhaustive `visit()` dispatch. AppModel enforces the structural invariants// (every container has >=2 children, ratios renormalized when all measured,// every pane in exactly one position).//// JSON shapes mirror SplitContainer.serialize() / Shell.buildFromSession()// (modulo additive id fields):// node: { "type": "node", "axis": "v"|"h", "children": [...],// "splits": [float|-1, ...] }// leaf: { "type": "leaf", "paneId", ...spawn fields (cwd, cwdRel,// command, title, restart, restartArgv) written by the session// emitter from AppModel's pane table }// Axis strings follow the QML convention: "v" = children side by side// (SplitView orientation Qt.Horizontal), "h" = stacked vertically. In// memory the same concepts are SplitAxis::Horizontal / SplitAxis::Vertical.
#include <QtQml/qqmlregistration.h>
#include <QJsonArray>#include <QJsonObject>#include <QJsonValue>#include <QList>#include <QObject>#include <QString>#include <QStringList>#include <QUuid>#include <QVariantList>
#include <cstdint>#include <iterator>#include <memory>#include <utility>
enum class SplitAxis : std::uint8_t { Horizontal, Vertical };enum class SplitDirection : std::uint8_t { Left, Right, Up, Down };
// Session-JSON axis names. "v" = children side by side (SplitView// orientation Qt.Horizontal), "h" = children stacked (Qt.Vertical) — the// strings are the QML SplitContainer "axis" property values.[[nodiscard]] inline QString splitAxisToJson(SplitAxis axis){ return axis == SplitAxis::Horizontal ? QStringLiteral("v") : QStringLiteral("h");}
[[nodiscard]] inline SplitAxis splitAxisFromJson(const QString& name){ // Tolerance mirrors Shell.populateTabRoot: anything that is not "h" is "v". return name == QLatin1String("h") ? SplitAxis::Vertical : SplitAxis::Horizontal;}
enum class NodeKind : std::uint8_t { Container, Pane };
// What a leaf pane shows. A terminal streams a shell, an editor shows one// file, and a chooser replaces itself with what the user picks.enum class PaneKind : std::uint8_t { Terminal, Editor, Picker };
// Session-JSON pane kind names: "terminal", "editor", "picker". An unknown// name reads as Terminal.[[nodiscard]] inline QString paneKindToJson(PaneKind kind){ switch (kind) { case PaneKind::Editor: return QStringLiteral("editor"); case PaneKind::Picker: return QStringLiteral("picker"); case PaneKind::Terminal: break; } return QStringLiteral("terminal");}
[[nodiscard]] inline PaneKind paneKindFromJson(const QString& name){ if (name == QLatin1String("editor")) { return PaneKind::Editor; } if (name == QLatin1String("picker")) { return PaneKind::Picker; } return PaneKind::Terminal;}
class SplitContainerNode;class PaneNode;
// Abstract tagged-sum base: the node kind is the tag; each concrete final// carries its own product fields. QML discriminates via the `isContainer`// bool (the kind enum stays available as an int-typed property).class SplitNode : public QObject { Q_OBJECT QML_ELEMENT QML_UNCREATABLE("nodes are constructed by AppModel") Q_DISABLE_COPY_MOVE(SplitNode) Q_PROPERTY(NodeKind kind READ kind CONSTANT) Q_PROPERTY(bool isContainer READ isContainer CONSTANT) Q_ENUM(NodeKind)
public: ~SplitNode() override; [[nodiscard]] virtual NodeKind kind() const = 0; [[nodiscard]] bool isContainer() const { return kind() == NodeKind::Container; }
// Exhaustive visitor (dispatch on the virtual kind, no dynamic_cast). // A new NodeKind enumerator fails to compile here until the new case is // added (-Wswitch on the closed enum). Defined below, after the concrete // classes are complete (the dispatch casts need complete types). template <typename Visitor> decltype(auto) visit(Visitor&& visitor) const;
// Typed accessors: nullptr when the node is the other kind. [[nodiscard]] SplitContainerNode* asContainer(); [[nodiscard]] const SplitContainerNode* asContainer() const; [[nodiscard]] PaneNode* asPane(); [[nodiscard]] const PaneNode* asPane() const;
protected: explicit SplitNode(QObject* parent);};
class PaneNode final : public SplitNode { Q_OBJECT QML_ELEMENT QML_UNCREATABLE("nodes are constructed by AppModel") Q_DISABLE_COPY_MOVE(PaneNode) // Identity only. Spawn state (cwd, command, title, restart flags) lives // in AppModel's pane table keyed by paneId — the pool is its live source, // the session loader its restore source. The pane kind IS node state: the // delegate swaps the leaf component from its notify. Q_PROPERTY(QString paneId READ paneId CONSTANT) Q_PROPERTY(PaneKind paneKind READ paneKind NOTIFY paneKindChanged) Q_PROPERTY(QString kindWord READ kindWord NOTIFY paneKindChanged) Q_PROPERTY(bool isTerminal READ isTerminal NOTIFY paneKindChanged) Q_PROPERTY(bool isEditor READ isEditor NOTIFY paneKindChanged) Q_PROPERTY(bool isPicker READ isPicker NOTIFY paneKindChanged) Q_ENUM(PaneKind)
public: explicit PaneNode(QString paneId, QObject* parent, PaneKind kind = PaneKind::Terminal); ~PaneNode() override;
[[nodiscard]] NodeKind kind() const override { return NodeKind::Pane; } [[nodiscard]] const QString& paneId() const { return m_paneId; } [[nodiscard]] PaneKind paneKind() const { return m_kind; } [[nodiscard]] QString kindWord() const { return paneKindToJson(m_kind); } [[nodiscard]] bool isTerminal() const { return m_kind == PaneKind::Terminal; } [[nodiscard]] bool isEditor() const { return m_kind == PaneKind::Editor; } [[nodiscard]] bool isPicker() const { return m_kind == PaneKind::Picker; } // AppModel-only mutation (the view never writes a kind). No-op when the // kind is unchanged; emits paneKindChanged. void setPaneKind(PaneKind kind);
signals: void paneKindChanged();
private: QString m_paneId; PaneKind m_kind = PaneKind::Terminal;};
class SplitContainerNode final : public SplitNode { Q_OBJECT QML_ELEMENT QML_UNCREATABLE("nodes are constructed by AppModel") Q_DISABLE_COPY_MOVE(SplitContainerNode) // SplitAxis as a QML int: 0 = Horizontal (children side by side, // SplitView.orientation Qt.Horizontal), 1 = Vertical (stacked). Q_PROPERTY(int axis READ axis NOTIFY axisChanged) Q_PROPERTY(QString containerId READ containerId CONSTANT) Q_PROPERTY(QList<SplitNode*> children READ children NOTIFY childrenChanged) // Shares of the parent's extent, 0..1, index-parallel with children. // -1.0 = "unmeasured" (legacy sessions; QML coalesces it to an even 1/n // share via coalescedShare). Unmeasured entries only exist right after a // legacy-session load whose splits array held -1. Q_PROPERTY(QList<qreal> ratios READ ratios NOTIFY ratiosChanged)
public: explicit SplitContainerNode(SplitAxis axis, QString containerId, QObject* parent); ~SplitContainerNode() override;
[[nodiscard]] NodeKind kind() const override { return NodeKind::Container; } [[nodiscard]] int axis() const; void setAxis(SplitAxis axis); // emits axisChanged [[nodiscard]] const QString& containerId() const { return m_containerId; } [[nodiscard]] const QList<SplitNode*>& children() const { return m_children; } [[nodiscard]] int childCount() const { return static_cast<int>(m_children.size()); } [[nodiscard]] SplitNode* childAt(int index) const; [[nodiscard]] const QList<qreal>& ratios() const { return m_ratios; } [[nodiscard]] qreal ratioAt(int index) const; void setRatioAt(int index, qreal ratio); // emits ratiosChanged void setRatios(QList<qreal> ratios); // emits ratiosChanged
// Insert `child` at `index` (clamped) with its ratio entry; QObject-parents // the child (the container owns the subtree) and keeps children/ratios // index-parallel. Emits childrenChanged + ratiosChanged. void insertChild(int index, SplitNode* child, qreal ratio); void appendChild(SplitNode* child, qreal ratio); // Remove and return the child at `index` (no ratio fixup; callers // renormalize). Clears the QObject parent: the caller owns the returned // node. Emits childrenChanged + ratiosChanged. [[nodiscard]] SplitNode* takeChild(int index);
// --- range interface -------------------------------------------------- // Iterates the child nodes in layout order, so containers work with the // STL ranges library: for (const SplitNode* child : container), views, // and algorithms. const-only: children are owned, never mutated through // the range. using value_type = SplitNode*; using const_iterator = const SplitNode* const*; using iterator = const_iterator; [[nodiscard]] const_iterator begin() const { return m_children.constData(); } [[nodiscard]] const_iterator end() const { return m_children.constData() + m_children.size(); } [[nodiscard]] qsizetype size() const { return m_children.size(); } [[nodiscard]] bool empty() const { return m_children.isEmpty(); }
signals: void axisChanged(); void childrenChanged(); void ratiosChanged();
private: SplitAxis m_axis; QString m_containerId; QList<SplitNode*> m_children; QList<qreal> m_ratios;};
[[nodiscard]] inline QString newUuid(){ return QUuid::createUuid().toString(QUuid::WithoutBraces);}
// --- traversal ------------------------------------------------------------
// Visit containers and panes depth-first in layout order. The visitor takes// either a `const SplitContainerNode&` or a `const PaneNode&`.template <typename Visitor>void walk(const SplitNode& node, Visitor&& visitor) // NOLINT(misc-no-recursion,cppcoreguidelines-missing-std-forward){ node.visit([&](const auto& typed) { using T = std::decay_t<decltype(typed)>; if constexpr (std::is_same_v<T, SplitContainerNode>) { visitor(typed); for (const SplitNode* child : typed.children()) { walk(*child, visitor); } } else { visitor(typed); } });}
[[nodiscard]] QStringList paneIdsInLayoutOrder(const SplitNode& root);
// --- normalized pane-rect geometry -------------------------------------------// The layout math one level up from the nodes: every pane's cell as a rect// in the tab's normalized 0..1 space. Mirrors SplitTreeNodeDelegate's// coalescedShare display math (measured ratio when > 0, else an even 1/n// share, shares normalized to sum 1).
struct PaneRect { QString paneId; qreal x = 0; qreal y = 0; qreal w = 0; qreal h = 0;};
// Coalesced share of child `index` in `container` (see the ratios property:// measured ratio when > 0, else an even 1/n share).[[nodiscard]] qreal coalescedShare(const SplitContainerNode& container, int index);
// Depth-first pane rects of `root` in layout order.[[nodiscard]] QList<PaneRect> paneRectsNormalized(const SplitNode& root);
// The pane nearest `fromPaneId` in `direction` ("left"|"right"|"up"|"down"),// scored from the tree's normalized pane rects. Kind agnostic: every pane is// a rect, so an editor or picker pane is a target like any other. "" when no// pane lies in that direction, or when either id is unknown.[[nodiscard]] QString spatialNeighborPaneId(const SplitNode& root, const QString& fromPaneId, const QString& direction);
// The container with `containerId`, or nullptr.[[nodiscard]] SplitContainerNode* findContainer(SplitNode& root, const QString& containerId);[[nodiscard]] const SplitContainerNode* findContainer( const SplitNode& root, const QString& containerId);
// --- mutation helper (AppModel enforces invariants around it) --------------
// Renormalize ratios so the container's MEASURED shares sum to 1 inside// 1e-6. Mirrors the QML coalescedShare semantics: measured + unmeasured// siblings can only coexist because QML coalesces each unmeasured child to// an even 1/n share WITHOUT any sum-to-1 fix-up. So this rescales ONLY// when every child is measured; if any child is unmeasured it leaves the// measured ones alone and lets QML handle the display. Cardinality: this is// the single invariant-enforcement entry point — every AppModel mutator// that touches ratios routes through it. Emits ratiosChanged when it// rescales.void renormalize(SplitContainerNode& container);
// Set every container in the tree rooted at `root` (root included) to equal// shares: each child gets 1/n, so ratios stay measured and sum to 1 per// container at every nesting level. Emits ratiosChanged per touched// container. Pure ratio rewrite — no structural change, no path churn.void equalizeSplits(SplitNode& root);
// --- JSON (the session "tree" value) ---------------------------------------// Node: { type:"node", axis:"v"|"h", children:[...], splits:[ratio|-1...] }// Leaf: { type:"leaf", paneId, ... } — spawn fields (cwd, cwdRel, command,// title, restart, restartArgv) are written by the session emitter from// AppModel's pane table, not here.[[nodiscard]] QJsonObject toJson(const SplitNode& node);// Builds an ownerless node tree (children QObject-parented to their// container during parse; the root has no parent — the caller parents it).// nullopt for unknown tree types (callers substitute an empty leaf).[[nodiscard]] std::unique_ptr<SplitNode> fromJson(const QJsonObject& object);
// Exhaustive visitor definitions: the dispatch casts need complete types, so// these live after the concrete classes. A new NodeKind enumerator fails to// compile here until the new case is added (-Wswitch on the closed enum).// Read-only by design: mutation goes through the typed asContainer/asPane// pointers. Each downcast is guarded by kind() — safe by construction.template <typename Visitor>decltype(auto) SplitNode::visit(Visitor&& visitor) const{ switch (kind()) { case NodeKind::Container: { const auto& container = *static_cast<const SplitContainerNode*>(this); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) return std::forward<Visitor>(visitor)(container); } case NodeKind::Pane: { const auto& pane = *static_cast<const PaneNode*>(this); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) return std::forward<Visitor>(visitor)(pane); } } Q_UNREACHABLE();}