#pragma once #include "BoardSnapshot.hpp" #include "McpClient.hpp" #include "TaskBoard.hpp" #include "TaskStore.hpp" #include #include #include #include #include namespace tp::taskboard { std::string colorToHex(const RGBA& color); std::optional colorFromHex(const std::string& text); const char* serverFieldName(TaskField field); picojson::value fieldValue(const Task& task, TaskField field); void setFieldValue(Task& task, TaskField field, const picojson::value& value); std::string fieldLabel(TaskField field); struct FieldConflict { ui::NodeId task = 0; TaskField field = TaskField::Title; picojson::value mine; picojson::value theirs; }; struct HistorySpan { std::optional first; std::optional last; }; // Keeps the board in step with the task server. // `mSaved` is the board as the server last had it. The user's unsaved work is the difference between the // live board and `mSaved`; reloads apply server changes only where the user has no unsaved change. class TaskServerSync : public TaskStore, public SecretsStore { public: TaskServerSync(TaskBoard& board, McpClient& client); bool load(); bool load(const McpToolResult& result); void setOnConnectionChanged(std::function callback) { mOnConnectionChanged = std::move(callback); } void processResponses(); void pollForChanges(); bool viewHistoryAt(std::optional moment); [[nodiscard]] std::optional viewedMoment() const { return mViewedMoment; } [[nodiscard]] bool isViewingHistory() const { return mViewedMoment.has_value(); } [[nodiscard]] const HistorySpan& historySpan() const { return mHistorySpan; } [[nodiscard]] const std::vector& historyChanges() const { return mHistoryChanges; } void save(); [[nodiscard]] BoardChanges unsavedChanges() const; [[nodiscard]] bool hasUnsavedChanges() const { return !unsavedChanges().empty(); } [[nodiscard]] bool isSaving() const { return mSaving; } void startSession(ui::NodeId id) override; void restartSession(ui::NodeId id, bool fresh) override; void stopSession(ui::NodeId id) override; void beginEditing(ui::NodeId id, TaskField field) override; void endEditing() override; bool canMessageTask(ui::NodeId id) const override; void listTaskMessages(ui::NodeId id, bool incoming, uint64_t after, TaskMessagesCallback done) override; void taskMessageDelivery(ui::NodeId id, TaskMessageDeliveryCallback done) override; void sendTaskMessage( ui::NodeId id, const std::string& text, const std::string& retryId, TaskMessageCallback done ) override; void listSecrets(std::function)> onResult) override; void setSecret( const std::string& key, const std::string& value, const std::string& description, std::function onDone ) override; void deleteSecret(const std::string& key, std::function onDone) override; void loadOptions(std::function done); void loadTaskDetails(const std::vector& ids, std::function done) override; void searchTasks(const std::string& query, std::function)> done) override; void setRootContext(std::optional root) override; void setTimelineVisible(bool visible) override; void setActiveTask(std::optional id) override; [[nodiscard]] const AgentOptions& options() const { return mOptions; } void resolveConflict(FieldConflict conflict, bool keepMine); [[nodiscard]] const std::vector& conflicts() const { return mConflicts; } [[nodiscard]] const std::string& lastError() const { return mLastError; } void loadSubrouterInfo(); void setOnBoardReloaded(std::function callback) { mOnBoardReloaded = std::move(callback); } void setOnHistoryChanged(std::function callback) { mOnHistoryChanged = std::move(callback); } void setOnSaved(std::function callback) { mOnSaved = std::move(callback); } void setOnError(std::function callback) { mOnError = std::move(callback); } void setOnConflictsChanged(std::function callback) { mOnConflictsChanged = std::move(callback); } void setOnSubrouterInfo(std::function callback) { mOnSubrouterInfo = std::move(callback); } private: void applyIndex(const picojson::object& index); void applyRootContext(const picojson::object& board); void pollRootContext(); void applyBoard(const picojson::object& board); void applyPalette(const picojson::object& board, const BoardSnapshot& local); std::vector applyTasks(const picojson::object& board, const BoardSnapshot& local); void applyLinks(const picojson::object& board, const std::vector& parentLinks, const BoardSnapshot& local); void mergeTask(ui::NodeId id, const Task& incoming, const NodeLayout& layout, const BoardSnapshot& local); void connect(const TaskLink& link); void disconnect(const TaskLink& link); [[nodiscard]] picojson::object saveRequest(const BoardChanges& changes) const; void finishSave(const McpToolResult& result, const BoardChanges& changes); void adoptCreatedIds(const picojson::object& ids); void addConflict(FieldConflict conflict); bool dropConflict(ui::NodeId id, TaskField field); void requestReload(); void requestBoard(std::optional moment); void boardArrived(const McpToolResult& result, unsigned generation); void applyHistorySpan(const picojson::object& board); void loadHistoryChanges(); void sessionCall(const std::string& tool, ui::NodeId id, picojson::object arguments); void reportError(const std::string& error); [[nodiscard]] bool isEditing(ui::NodeId id, TaskField field) const; TaskBoard& mBoard; McpClient& mClient; BoardSnapshot mSaved; std::string mRevision; std::map mIndexRows; picojson::object mIndexBoard; std::optional mActiveTask; bool mTimelineVisible = false; bool mTimelineInFlight = false; std::string mLastError; std::vector mConflicts; AgentOptions mOptions = offlineAgentOptions(); std::optional> mEditing; std::optional mViewedMoment; std::optional> mQueuedMoment; HistorySpan mHistorySpan; std::vector mHistoryChanges; unsigned mBoardGeneration = 0; bool mHistoryInFlight = false; bool mPollInFlight = false; bool mSaving = false; bool mSubrouterInFlight = false; int mSubrouterPollTick = 0; std::function mOnConnectionChanged; std::function mOnBoardReloaded; std::function mOnSaved; std::function mOnHistoryChanged; std::function mOnError; std::function mOnConflictsChanged; std::function mOnSubrouterInfo; }; }