#pragma once // Phase 3 sub-step 3: the jj/git bridge-flow controller. // // Absorbs Shell.qml's async flow state (pickMode/pickItems/pendingAddMode/ // renameTarget/forgetTarget) and the bridge `Connections` handlers (Shell.qml 1764-1862) as C++ // signal connections. TerminalPage.qml instantiates one of these; it owns // the JjBridge + GitBridge instances (QML never instantiates them) and // routes every workspace add/rename/forget/set-directory through AppModel // mutators. It never opens dialogs: it emits decision-point signals // (setDirectoryRequested, pickDialogClose, pickDialogError) and the // normalized pick list (jjWorkspacesReady / gitWorktreesReady) so the page // owns the FolderDialog / PickWorkspaceDialog wiring. // // Workspace-kind detection is per-workspace and asynchronous: detect* // probe both bridges for a workspace path and the identify() results // reseat the workspace kind / store its VCS capabilities. The per- // workspace kind IS the gate; there is no global repo-root state. #include #include #include #include #include #include class AppModel; class GitBridge; class JjBridge; class Workspace; class WorkspaceController : public QObject { Q_OBJECT QML_ELEMENT Q_DISABLE_COPY_MOVE(WorkspaceController) public: explicit WorkspaceController(QObject* parent = nullptr); ~WorkspaceController() override; // --- page-facing API (Q_INVOKABLE; called via the page from // SidebarView / AddWorkspaceMenu / PickWorkspaceDialog) -------------- // Probe the workspace at `index` (async, both bridges): reseat its kind // (jj wins over git; git decides worktree-vs-repository) and store the // detected VCS capabilities. Q_INVOKABLE void detectWorkspaceKind(int index); // Probe every workspace (each runs async; several may be in flight). Q_INVOKABLE void detectAllWorkspaces(); // Add a workspace directly. `kind` is the WorkspaceKind enum value (0 // Directory, 1 Repository, 2 JjWorkspace, 3 GitWorktree). The model // selects the new workspace, then its kind is re-detected (async) and // reseated idempotently from the probe results. Q_INVOKABLE void addWorkspaceDirect(int kind, const QString& path, const QString& name, const QString& repoRoot); // Start the jj/git pick flows: list the workspaces/worktrees under // `repoRoot`, emitting jjWorkspacesReady/gitWorktreesReady when done. Q_INVOKABLE void addJjWorkspace(const QString& repoRoot); Q_INVOKABLE void addGitWorktree(const QString& repoRoot); // Menu-command conveniences: start the pick flow for workspace `index` // (a sidebar-row right-click), or for the active workspace when `index` // is < 0 (the AddWorkspaceMenu). No-op when the workspace has no stored // repo root. Q_INVOKABLE void pickJjWorkspace(int index = -1); Q_INVOKABLE void pickGitWorktree(int index = -1); // Reset the pick session and ask the bridge to list items for `mode` // ("jj" | "git") under `repoRoot`. Q_INVOKABLE void requestPickList(const QString& mode, const QString& repoRoot); // Add a workspace from a pick-dialog item ({name, path}); the repo root // comes from the pick session so forget/rename work on jj workspaces. Q_INVOKABLE void addPickedWorkspace(const QString& mode, const QVariantMap& item); // "Add another": create a memorable-named workspace in the workspaces // dir, disambiguating against the current pick items and existing dirs. Q_INVOKABLE void addMemorableWorkspace(const QString& mode); // Optimistically rename through the model; mirror to jj when the // workspace is a jj workspace (reverted on jj failure). Q_INVOKABLE void renameWorkspace(int index, const QString& newName); // Forget a Repository/JjWorkspace in jj terms (JjWorkspace -> // RepositoryWorkspace reseat on success). Q_INVOKABLE void forgetJjWorkspace(int index); // Ask the page to open the set-directory FolderDialog for `index`. Q_INVOKABLE void setWorkspaceDirectory(int index); Q_INVOKABLE static QString defaultNameFromPath(const QString& path); // Shell.qml localPath port: strip a file:// prefix + percent-decode. Q_INVOKABLE static QString localPath(const QUrl& url); // The spawn directory for a new workspace: the active workspace's path // when it has one (the startup default workspace's path == the startup // cwd), else the user's home. Q_INVOKABLE static QString currentDir(); signals: // Normalized ({name, path}) pick list ready — the page feeds it to // PickWorkspaceDialog.items. void jjWorkspacesReady(const QVariantList& items); void gitWorktreesReady(const QVariantList& items); // Decision points the controller deliberately hands to the page. void pickDialogClose(); // memorable-add succeeded void pickDialogError(const QString& error); // memorable-add failed void setDirectoryRequested(int index); // open the FolderDialog private slots: // JjBridge async results (Shell.qml 1769-1830). void onJjIdentified(const QString& path, const QString& repoRoot, const QString& workspaceName); void onJjWorkspacesListed(const QVariantList& items); void onWorkspaceAdded(bool ok, const QString& error, const QString& root, const QString& name); void onWorkspaceRenamed(bool ok, const QString& error); void onWorkspaceForgotten(bool ok, const QString& error); // GitBridge async results (Shell.qml 1837-1862). void onGitIdentified(const QString& path, const QString& repoRoot, bool isWorktree); void onGitWorktreesListed(const QVariantList& items); void onWorktreeAdded(bool ok, const QString& error, const QString& root, const QString& name); private: // NOLINT(readability-redundant-access-specifiers) - members must // live outside the `private slots:` region for moc to parse them. // Pending workspace-kind detection bookkeeping: one entry per identify() // echo path. detect* launches probes whose results come back keyed by // the requesting path, so concurrent detections (detectAllWorkspaces) // stay distinguishable. Several workspaces can share one path (jj // workspaces of the same repo), so the entry tracks EVERY pending // index for that path; identify results apply to all of them. // jjFound records the jj probe outcome once known so the (possibly // later) git result defers to jj when jj already claimed the // workspaces. The pending flags track which bridge results are still // outstanding (shared across the entry: all indices for a path are // probed together). struct PendingDetect { QList indexes; bool jjPending = false; bool gitPending = false; bool jjFound = false; }; QHash m_pendingDetects; // Pick-dialog session state. QString m_pickMode; QString m_pickRepoRoot; QVariantList m_pickItems; // "Add another" flow: a memorable-name add in flight ("jj" | "git"). QString m_pendingAddMode; // Optimistic-rename rollback bookkeeping. int m_renameTarget = -1; QString m_renameOldName; // forget-in-jj bookkeeping. int m_forgetTarget = -1; // Detection internals: look up the active workspace (or nullptr) and // release the path-keyed pending entry once no probe is outstanding. [[nodiscard]] static const Workspace* activeWorkspace(); [[nodiscard]] static const Workspace* workspaceAt(int index); void maybeFinishDetect(const QString& path, const PendingDetect& pending); // The greatest common directory of the workspace's panes' live cwds // (the model's pane records; falls back to spawn cwds). "" when the // workspace has no panes or no common root. Mirrors the sidebar // preview's QML inference — this C++ copy exists so detection can // promote the root onto the workspace path. [[nodiscard]] static QString inferWorkspaceRoot(const Workspace& ws); JjBridge* m_jjBridge; GitBridge* m_gitBridge; };