mirror of
https://github.com/openagentmarket/openagent-obsidian-plugin.git
synced 2026-07-22 07:02:58 +00:00
12338 lines
690 KiB
JavaScript
12338 lines
690 KiB
JavaScript
"use strict";
|
|
|
|
const crypto = require("crypto");
|
|
const fs = require("fs");
|
|
const http = require("http");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const { spawn } = require("child_process");
|
|
const { fileURLToPath, pathToFileURL } = require("url");
|
|
const {
|
|
ButtonComponent,
|
|
editorInfoField,
|
|
ItemView,
|
|
Modal,
|
|
Notice,
|
|
Plugin,
|
|
PluginSettingTab,
|
|
Setting,
|
|
setIcon,
|
|
setTooltip,
|
|
TextComponent,
|
|
TFile,
|
|
} = require("obsidian");
|
|
let EditorView = null;
|
|
try {
|
|
({ EditorView } = require("@codemirror/view"));
|
|
} catch {
|
|
EditorView = null;
|
|
}
|
|
|
|
const VIEW_TYPE = "openagent-view";
|
|
const VOICE_GRAPH_VIEW_TYPE = "openagent-voice-graph-view";
|
|
const DAEMON_CONFIG_PATH = path.join(os.homedir(), ".openagent", "daemon-config.json");
|
|
const DAEMON_LOG_PATH = path.join(os.homedir(), ".openagent", "daemon.log");
|
|
const PLUGIN_LOGO_FILE_NAME = "logo.png";
|
|
const PLUGIN_LOGO_PATH = path.join(__dirname, PLUGIN_LOGO_FILE_NAME);
|
|
const VAULT_PET_MASCOT_FILE_NAME = "vault-pet-mascot.png";
|
|
const VAULT_PET_MASCOT_PATH = path.join(__dirname, VAULT_PET_MASCOT_FILE_NAME);
|
|
const DEFAULT_PLUGIN_LOGO_DATA_URL = "data:image/png;base64,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";
|
|
const DEFAULT_VAULT_PET_MASCOT_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAEAAElEQVR4nOz9B7xt910fiH5XX7vvfco9t+jqXl0VW10usiVjW9iWC8U2tkcyZQJJSGIySd6bPEJIQhwhSiBA5r2XNyQwmc+kkDBgOc4L1SGJHdkYy8hgXHCTLOn20/fZffU1n+/v91/7HAPJEGLAktZPPr7nnrL3Wmvvu37tW4A66qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo4466qijjjrqqKOOOuqoo446vqrD+tM+gDrqeJ7+myv/ED/bANAG4ANwze+uArgdwJ2WZW0EjSDwfR+NRgjfC+B4DlzXhe/48Dwfrm/DcW3LtfyO7djHLdc66zhO2ypsPRAbKMoSZZbvZEVxPsvz3TxLF1lSlGVZIE1jJFmKLM2QxAniJEYUR1YSJXmapnMAFwB8AsDnACwA5OaDn88AJF+ha1FHHXV8haMuAOqo40/239gflOyY4PsAmgAC8/dzlmW9uN1u39jutLutVstttpthq9FeaXfb1/R7/UG/30ev30Ov20Gn3ULYbCIMQ/0IAgR+ANdx4EpR4KO0LDiOA9u2YVs8MD20vKyOKkee58iyHGmSo8hTRPECcZIgimLMZgvMZ3OMx2NMJhPs7u5jb7iD6WRyZTqZXokW8/FkMksmk0kynoz38iz/XVMcbAJgsTACMAGQ/RGvUx11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CKPhEMNev+50Ooz6dcIwQOiHohj0vEAmf+kHWkMeIQkq4Y91Xm7tqhdEwCU8Mwj4ESotUCKgRO4q2bDl44sCwAQdqRpAJQmc6ilBlKwD8ebRGGQ5BNApsEGTV5UU/myxwHyRYXpx4R6fnODoeB/HR0dnJydnt05Ojt4s8+xLAL48GAzujsfjM/N6WAMfC4vvMWwDYGHx3jcC+A4ZIZuBa67r/lgYdf5iFEd/PummPzToj/pLwyUMh8vo9bpNN02bMIkQhjHCMHAY0sMTgeO5DhUE9ATwGCpEYx8RAegJgRJAdfpj0A/9COj6b+KMDZNfij/leS4Z+hpQpG/QBIhxwCQckjognrwoGQ9clAzkaYo8Q5bluJhO3bPTU4xPT3FycoKz05OTyeTi7YvpxRv5fP4VAF8yjn1t0ScME9Hq+C0svtewDYCFxfsra6D5Ls3Adbj4S54f/4U4jH8kSdKPJ51uv9freb3h0On3Buh1UqRp3IRxiDiKmjCKnJDZAF4gun8nUEtiXxIHTf6A0f4Zkr9xEiA5T537eRpQowBO/Ercq7nXrxm/WyEv66bKC6cocsyzBaaLhTO5uMDR8RHOz85wfnZ6cTGdvJNli7em55P7TVORyPcNs94/+y72yiYUwMLC4vsB2wBYWLw//06636Ug6pkA+GG47qUgiJb9INwJo+TPJVG4EyVpnHY7Xr8/8Dppik6SSnSwHzEkiIFAAQLfF9Mfkgs9GvC6YsEjX9R3vPZ+z2uAxPFyl89pPy8r5EWOPFuoD3+eYzqdYja5qGaTaTWZTJr5YpLPF/O7i2zxWj7PbldVcRvAa2bKPweQf8fzfPa+bwu/hcX3GbYBsLB4/+KZNKD/z0rcNed8Egmv6ZbAX/M8bz3w/Bcc3/+RIAhXkzjygyhs2ACEbTpgECCMfNfj0cBzQyfwHFr5iud+Wed1U5VlWVdlwYm/BCd8Fv8sW4grX1EUjAA+rMv8taqs3iiq4qguy6mZ6t81zP3xdxT8Zx83YYu+hcV7DNsAWFh88P6u/lGFUwZ5ACOjLqDcsPPUCID/C+GF4iQYeDVWajg/CAfPo4bvwLnreHijQr2H2llUUr4Z5JOTCfjs154D2DeF/ghcFvzRN/tnCY9/3GO3sLD4PsM2ABYWH96G4E/yOQYA1gz57hDAyZ8BAc9K9ywsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCzwp8T/C6pj/dcwsvlUAAAAAElFTkSuQmCC";
|
|
const DEFAULT_SETTINGS = Object.freeze({
|
|
automationFolder: "",
|
|
daemonLaunchCommand: "",
|
|
daemonLaunchCwd: "",
|
|
daemonSandboxMode: "workspace-write",
|
|
enableDebugLogging: false,
|
|
enableDevSmokeRequests: false,
|
|
enableVaultPet: true,
|
|
stopActiveTaskHotkey: "",
|
|
workspaceRoot: "Workspaces",
|
|
});
|
|
const REPO_WORKSPACE_DIR_NAME = ".openagent";
|
|
const WORKSPACE_CONFIG_FILE_NAME = "workspace.json";
|
|
const DEFAULT_WORKSPACE_CANVAS_FILE = "Main.canvas";
|
|
const WORKSPACE_AUTOMATION_DIR_NAME = "Automations";
|
|
const DEFAULT_DAILY_VAULT_REVIEW_AUTOMATION_FILE = "Daily Vault Review.md";
|
|
const DEFAULT_DAILY_VAULT_REVIEW_CWD = "/Users/applefather/Documents/Applefather";
|
|
const WORKSPACE_INDEX_WAIT_MS = 4_000;
|
|
const WORKSPACE_INDEX_POLL_MS = 100;
|
|
const CANVAS_EDITOR_TEXT_CACHE_TTL_MS = 10_000;
|
|
const DAEMON_SANDBOX_MODE_OPTIONS = Object.freeze({
|
|
WORKSPACE_WRITE: "workspace-write",
|
|
DANGER_FULL_ACCESS: "danger-full-access",
|
|
});
|
|
const RECENT_SELECTION_TTL_MS = 15_000;
|
|
const DEV_SMOKE_REQUEST_RELATIVE_PATH = path.join(".openagent", "smoke-request.json");
|
|
const DEV_SMOKE_RESULT_RELATIVE_PATH = path.join(".openagent", "smoke-result.json");
|
|
const DEBUG_LOG_RELATIVE_PATH = path.join(".openagent", "new-thread-debug.jsonl");
|
|
const DEV_SMOKE_POLL_MS = 1_000;
|
|
const MAX_DEBUG_EVENTS = 30;
|
|
const DAEMON_STATUS_POLL_MS = 10_000;
|
|
const ACTIVE_TASK_STREAM_RECONNECT_DELAY_MS = 1_500;
|
|
const RESULT_NODE_Y_GAP = 40;
|
|
const RESULT_NODE_MIN_HEIGHT = 160;
|
|
const RESULT_NODE_MAX_HEIGHT = 520;
|
|
const RESULT_NODE_DEFAULT_WIDTH = 420;
|
|
const RESULT_NODE_MIN_WIDTH = 320;
|
|
const RESULT_NODE_MAX_WIDTH = 720;
|
|
const RESULT_NODE_CHARS_PER_LINE = 52;
|
|
const FOLLOW_UP_NODE_DEFAULT_HEIGHT = 120;
|
|
const CANVAS_LAYOUT_BASE_X = 80;
|
|
const CANVAS_LAYOUT_BASE_Y = 80;
|
|
const CANVAS_LAYOUT_X_GAP = 180;
|
|
const CANVAS_LAYOUT_Y_GAP = 80;
|
|
const CANVAS_LAYOUT_COMPONENT_GAP = 220;
|
|
const CANVAS_LAYOUT_MIN_WIDTH = 260;
|
|
const CANVAS_LAYOUT_MIN_HEIGHT = 120;
|
|
const RUNNING_CANVAS_NODE_COLOR = "3";
|
|
const COMPLETED_CANVAS_NODE_COLOR = "#086ddd";
|
|
const CANVAS_NEW_THREAD_BUTTON_ID = "oa-canvas-new-thread-button";
|
|
const CANVAS_FOLLOW_UP_NODE_BUTTON_ID = "oa-canvas-follow-up-node-button";
|
|
const CANVAS_FORK_NODE_BUTTON_ID = "oa-canvas-fork-node-button";
|
|
const AUTO_RUN_TRIGGER_CANVAS_NODE_COLORS = Object.freeze([
|
|
"4",
|
|
"#22c55e",
|
|
"#16a34a",
|
|
"#008000",
|
|
]);
|
|
const OPENAGENT_CANVAS_SCHEMA_VERSION = 1;
|
|
const CHAT_MESSAGE_PAGE_SIZE = 20;
|
|
const SETTINGS_RECENT_TASKS_PAGE_SIZE = 10;
|
|
const PANEL_TAB_OPTIONS = Object.freeze({
|
|
ACTIVE_TASK: "active-task",
|
|
WORKSPACE_SETTINGS: "workspace-settings",
|
|
HEARTBEAT: "heartbeat",
|
|
WORKSPACE: "workspace",
|
|
TASK_LIST: "task-list",
|
|
ARCHIVED_TASK_LIST: "archived-task-list",
|
|
DEBUG: "debug",
|
|
});
|
|
const DAEMON_CONNECTION_STATES = Object.freeze({
|
|
UNKNOWN: "unknown",
|
|
CHECKING: "checking",
|
|
ONLINE: "online",
|
|
OFFLINE: "offline",
|
|
});
|
|
const HOTKEY_CAPTURE_INPUT_CLASS = "oa-hotkey-capture-input";
|
|
const CANVAS_SELECTION_TRACKING_PATCH_SYMBOL = Symbol("openagentCanvasSelectionTrackingPatch");
|
|
const VAULT_PET_WIDTH = 118;
|
|
const VAULT_PET_HEIGHT = 142;
|
|
const VAULT_PET_MARGIN = 14;
|
|
const VAULT_PET_ACTIVITY_COOLDOWN_MS = 1_200;
|
|
const VOICE_GRAPH_MAX_NODES = 120;
|
|
const VOICE_GRAPH_MAX_EDGES = 260;
|
|
const VOICE_GRAPH_MIN_NODE_RADIUS = 4;
|
|
const VOICE_GRAPH_MAX_NODE_RADIUS = 24;
|
|
const VAULT_PET_IDLE_PHRASES = Object.freeze([
|
|
"Bloop. I live here now.",
|
|
"Your vault has snacks?",
|
|
"One badge, many thoughts.",
|
|
"I am guarding this thought.",
|
|
"Tiny splash break.",
|
|
"Today we make the graph wiggle.",
|
|
]);
|
|
const VAULT_PET_ACTIVITY_PHRASES = Object.freeze({
|
|
create: [
|
|
"New note! Fresh plankton.",
|
|
"I saw that file hatch.",
|
|
"A baby idea just appeared.",
|
|
],
|
|
modify: [
|
|
"Nice. The vault is breathing.",
|
|
"Ink deployed.",
|
|
"That note got juicier.",
|
|
],
|
|
delete: [
|
|
"Poof. I will not ask questions.",
|
|
"The deep takes one back.",
|
|
"Clean water, clean vault.",
|
|
],
|
|
rename: [
|
|
"New name, same soul.",
|
|
"Identity upgrade detected.",
|
|
"That file got a little hat.",
|
|
],
|
|
workspace: [
|
|
"New room smell.",
|
|
"I am recalibrating my halo.",
|
|
"Different pane, same tiny ocean.",
|
|
],
|
|
pet: [
|
|
"Hehe. Again.",
|
|
"Critical morale upgrade.",
|
|
"Tentacle high five.",
|
|
"I will remember this.",
|
|
],
|
|
});
|
|
|
|
function shortHash(value) {
|
|
return crypto.createHash("sha256").update(String(value || ""), "utf8").digest("hex").slice(0, 12);
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
}
|
|
|
|
function pickRandomItem(items) {
|
|
if (!Array.isArray(items) || items.length === 0) {
|
|
return "";
|
|
}
|
|
return items[Math.floor(Math.random() * items.length)];
|
|
}
|
|
|
|
function isUsableClientRect(rect) {
|
|
return Boolean(
|
|
rect
|
|
&& Number.isFinite(rect.left)
|
|
&& Number.isFinite(rect.top)
|
|
&& rect.width > 0
|
|
&& rect.height > 0
|
|
&& rect.right >= 0
|
|
&& rect.bottom >= 0
|
|
&& rect.left <= window.innerWidth
|
|
&& rect.top <= window.innerHeight
|
|
);
|
|
}
|
|
|
|
function combineClientRects(rects) {
|
|
const usableRects = Array.from(rects || []).filter(isUsableClientRect);
|
|
if (usableRects.length === 0) {
|
|
return null;
|
|
}
|
|
const left = Math.min(...usableRects.map((rect) => rect.left));
|
|
const top = Math.min(...usableRects.map((rect) => rect.top));
|
|
const right = Math.max(...usableRects.map((rect) => rect.right));
|
|
const bottom = Math.max(...usableRects.map((rect) => rect.bottom));
|
|
return {
|
|
left,
|
|
top,
|
|
right,
|
|
bottom,
|
|
width: right - left,
|
|
height: bottom - top,
|
|
};
|
|
}
|
|
|
|
function patchObjectMethod(target, methodName, wrap) {
|
|
if (!target || typeof target !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const original = target[methodName];
|
|
if (typeof original !== "function" || typeof wrap !== "function") {
|
|
return null;
|
|
}
|
|
|
|
const wrapped = wrap(original);
|
|
if (typeof wrapped !== "function" || wrapped === original) {
|
|
return null;
|
|
}
|
|
|
|
target[methodName] = wrapped;
|
|
return () => {
|
|
if (target[methodName] === wrapped) {
|
|
target[methodName] = original;
|
|
}
|
|
};
|
|
}
|
|
|
|
function shellEscape(value) {
|
|
return `'${String(value || "").replace(/'/g, `'\\''`)}'`;
|
|
}
|
|
|
|
function slugifyWorkspaceName(value) {
|
|
return String(value || "")
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
|| "workspace";
|
|
}
|
|
|
|
function joinVaultPath(...parts) {
|
|
return parts
|
|
.map((part) => String(part || "").replace(/^\/+|\/+$/g, ""))
|
|
.filter(Boolean)
|
|
.join("/");
|
|
}
|
|
|
|
function parseAutomationScalar(value) {
|
|
const raw = String(value || "").trim();
|
|
if (!raw) {
|
|
return "";
|
|
}
|
|
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
return raw.slice(1, -1);
|
|
}
|
|
if (raw === "true") {
|
|
return true;
|
|
}
|
|
if (raw === "false") {
|
|
return false;
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
function parseAutomationFrontmatter(raw) {
|
|
const text = String(raw || "");
|
|
if (!text.startsWith("---\n")) {
|
|
return { data: {}, body: text };
|
|
}
|
|
|
|
const endIndex = text.indexOf("\n---", 4);
|
|
if (endIndex < 0) {
|
|
return { data: {}, body: text };
|
|
}
|
|
|
|
const frontmatter = text.slice(4, endIndex).trim();
|
|
const body = text.slice(endIndex + 4).replace(/^\n+/, "");
|
|
const data = {};
|
|
frontmatter.split(/\r?\n/).forEach((line) => {
|
|
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
if (!match) {
|
|
return;
|
|
}
|
|
data[match[1]] = parseAutomationScalar(match[2]);
|
|
});
|
|
return { data, body };
|
|
}
|
|
|
|
function dirnameVaultPath(value) {
|
|
const normalized = String(value || "").replace(/^\/+|\/+$/g, "");
|
|
if (!normalized || !normalized.includes("/")) {
|
|
return "";
|
|
}
|
|
return normalized.slice(0, normalized.lastIndexOf("/"));
|
|
}
|
|
|
|
function isVaultPathWithinRoot(vaultPath, rootPath) {
|
|
const normalizedVaultPath = String(vaultPath || "").replace(/^\/+|\/+$/g, "");
|
|
const normalizedRootPath = String(rootPath || "").replace(/^\/+|\/+$/g, "");
|
|
if (!normalizedVaultPath || !normalizedRootPath) {
|
|
return false;
|
|
}
|
|
return normalizedVaultPath === normalizedRootPath || normalizedVaultPath.startsWith(`${normalizedRootPath}/`);
|
|
}
|
|
|
|
function safeRealpathSync(value) {
|
|
try {
|
|
return fs.realpathSync.native ? fs.realpathSync.native(value) : fs.realpathSync(value);
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function pathExistsOrIsSymlink(value) {
|
|
try {
|
|
fs.lstatSync(value);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getDirectorySymlinkType() {
|
|
return process.platform === "win32" ? "junction" : "dir";
|
|
}
|
|
|
|
function normalizePanelTab(value) {
|
|
if (value === PANEL_TAB_OPTIONS.WORKSPACE_SETTINGS) {
|
|
return PANEL_TAB_OPTIONS.TASK_LIST;
|
|
}
|
|
if (value === PANEL_TAB_OPTIONS.TASK_LIST) {
|
|
return PANEL_TAB_OPTIONS.TASK_LIST;
|
|
}
|
|
if (value === PANEL_TAB_OPTIONS.HEARTBEAT) {
|
|
return PANEL_TAB_OPTIONS.HEARTBEAT;
|
|
}
|
|
if (value === PANEL_TAB_OPTIONS.WORKSPACE) {
|
|
return PANEL_TAB_OPTIONS.WORKSPACE;
|
|
}
|
|
return PANEL_TAB_OPTIONS.TASK_LIST;
|
|
}
|
|
|
|
function normalizePromptPath(value) {
|
|
return String(value || "").trim().replace(/\\/g, "/");
|
|
}
|
|
|
|
function normalizeFilesystemPathForCompare(value) {
|
|
return normalizePromptPath(path.resolve(String(value || "").trim()));
|
|
}
|
|
|
|
function encodeMarkdownLinkPath(value) {
|
|
return String(value || "")
|
|
.split("/")
|
|
.map((segment) => encodeURIComponent(segment).replace(/[()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`))
|
|
.join("/");
|
|
}
|
|
|
|
function decodeMarkdownLinkTarget(value) {
|
|
const normalized = String(value || "").trim();
|
|
try {
|
|
return decodeURI(normalized);
|
|
} catch {
|
|
return normalized;
|
|
}
|
|
}
|
|
|
|
function parseLocalMarkdownLinkTarget(rawTarget) {
|
|
const target = String(rawTarget || "").trim();
|
|
if (!target) {
|
|
return null;
|
|
}
|
|
|
|
const hashIndex = target.indexOf("#");
|
|
const targetWithoutFragment = hashIndex >= 0 ? target.slice(0, hashIndex) : target;
|
|
const fragment = hashIndex >= 0 ? target.slice(hashIndex) : "";
|
|
const decodedTarget = decodeMarkdownLinkTarget(targetWithoutFragment);
|
|
let localPath = decodedTarget;
|
|
|
|
if (/^file:\/\//i.test(decodedTarget)) {
|
|
try {
|
|
localPath = fileURLToPath(decodedTarget);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (!path.isAbsolute(localPath)) {
|
|
return null;
|
|
}
|
|
|
|
let normalizedPath = localPath;
|
|
if (!fs.existsSync(normalizedPath)) {
|
|
const lineSuffixMatch = normalizedPath.match(/^(.*):\d+(?::\d+)?$/);
|
|
if (lineSuffixMatch && path.isAbsolute(lineSuffixMatch[1])) {
|
|
normalizedPath = lineSuffixMatch[1];
|
|
}
|
|
}
|
|
|
|
return {
|
|
localPath: normalizedPath,
|
|
fragment,
|
|
};
|
|
}
|
|
|
|
function rewriteMarkdownLinks(text, rewriteTarget) {
|
|
const source = String(text || "");
|
|
if (!source || typeof rewriteTarget !== "function") {
|
|
return source;
|
|
}
|
|
|
|
return source.replace(/(\[[^\]\n]*\]\()(<[^>\n]*>|[^)\s\n]*)(\))/g, (match, prefix, rawTarget, suffix) => {
|
|
const target = String(rawTarget || "");
|
|
const unwrappedTarget = target.startsWith("<") && target.endsWith(">")
|
|
? target.slice(1, -1)
|
|
: target;
|
|
const rewrittenTarget = rewriteTarget(unwrappedTarget);
|
|
return rewrittenTarget ? `${prefix}${rewrittenTarget}${suffix}` : match;
|
|
});
|
|
}
|
|
|
|
function createDaemonStatusSnapshot(overrides = {}) {
|
|
return {
|
|
state: DAEMON_CONNECTION_STATES.UNKNOWN,
|
|
checkedAt: 0,
|
|
host: "",
|
|
port: "",
|
|
error: "",
|
|
runtimeAvailable: null,
|
|
runtimeMessage: "",
|
|
lastRuntimeError: "",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function formatDaemonStatusError(message) {
|
|
const normalizedMessage = String(message || "").trim();
|
|
if (!normalizedMessage) {
|
|
return "Can't reach the local Codex server.";
|
|
}
|
|
|
|
const lowercaseMessage = normalizedMessage.toLowerCase();
|
|
if (lowercaseMessage.includes("config was not found")) {
|
|
return "Codex server is not configured yet.";
|
|
}
|
|
if (lowercaseMessage.includes("not responding") || lowercaseMessage.includes("unable to reach")) {
|
|
return "Can't reach the local Codex server.";
|
|
}
|
|
return normalizedMessage;
|
|
}
|
|
|
|
function getDaemonStatusTagClassName(state) {
|
|
switch (state) {
|
|
case DAEMON_CONNECTION_STATES.ONLINE:
|
|
return "is-online";
|
|
case DAEMON_CONNECTION_STATES.OFFLINE:
|
|
return "is-offline";
|
|
case DAEMON_CONNECTION_STATES.CHECKING:
|
|
return "is-checking";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function normalizeHotkeyModifierToken(value) {
|
|
const normalized = String(value || "").trim().toLowerCase();
|
|
switch (normalized) {
|
|
case "mod":
|
|
return "Mod";
|
|
case "cmd":
|
|
case "command":
|
|
case "meta":
|
|
return "Meta";
|
|
case "ctrl":
|
|
case "control":
|
|
return "Ctrl";
|
|
case "alt":
|
|
case "option":
|
|
return "Alt";
|
|
case "shift":
|
|
return "Shift";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function normalizeHotkeyPrimaryKey(value) {
|
|
const normalized = String(value || "").trim();
|
|
if (!normalized) {
|
|
return "";
|
|
}
|
|
|
|
const lowercase = normalized.toLowerCase();
|
|
if (lowercase === "space" || lowercase === "spacebar" || normalized === " ") {
|
|
return "Space";
|
|
}
|
|
if (lowercase === "esc" || lowercase === "escape") {
|
|
return "Escape";
|
|
}
|
|
if (lowercase === "enter" || lowercase === "return") {
|
|
return "Enter";
|
|
}
|
|
if (lowercase === "tab") {
|
|
return "Tab";
|
|
}
|
|
if (lowercase === "backspace") {
|
|
return "Backspace";
|
|
}
|
|
if (lowercase === "delete" || lowercase === "del") {
|
|
return "Delete";
|
|
}
|
|
if (lowercase === "up" || lowercase === "arrowup") {
|
|
return "ArrowUp";
|
|
}
|
|
if (lowercase === "down" || lowercase === "arrowdown") {
|
|
return "ArrowDown";
|
|
}
|
|
if (lowercase === "left" || lowercase === "arrowleft") {
|
|
return "ArrowLeft";
|
|
}
|
|
if (lowercase === "right" || lowercase === "arrowright") {
|
|
return "ArrowRight";
|
|
}
|
|
if (/^f\d{1,2}$/i.test(normalized)) {
|
|
return normalized.toUpperCase();
|
|
}
|
|
if (normalized.length === 1) {
|
|
return /[a-z]/i.test(normalized) ? normalized.toUpperCase() : normalized;
|
|
}
|
|
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
|
}
|
|
|
|
function composeHotkeyString({
|
|
useMod = false,
|
|
useCtrl = false,
|
|
useMeta = false,
|
|
useAlt = false,
|
|
useShift = false,
|
|
key = "",
|
|
} = {}) {
|
|
const normalizedKey = normalizeHotkeyPrimaryKey(key);
|
|
if (!normalizedKey) {
|
|
return "";
|
|
}
|
|
|
|
const parts = [];
|
|
if (useMod) {
|
|
parts.push("Mod");
|
|
} else {
|
|
if (useCtrl) {
|
|
parts.push("Ctrl");
|
|
}
|
|
if (useMeta) {
|
|
parts.push("Meta");
|
|
}
|
|
}
|
|
if (useAlt) {
|
|
parts.push("Alt");
|
|
}
|
|
if (useShift) {
|
|
parts.push("Shift");
|
|
}
|
|
parts.push(normalizedKey);
|
|
return parts.join("+");
|
|
}
|
|
|
|
function normalizeStopHotkeySetting(value) {
|
|
const rawValue = String(value || "").trim();
|
|
if (!rawValue) {
|
|
return "";
|
|
}
|
|
|
|
const tokens = rawValue
|
|
.split("+")
|
|
.map((token) => token.trim())
|
|
.filter(Boolean);
|
|
if (tokens.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
const key = normalizeHotkeyPrimaryKey(tokens[tokens.length - 1]);
|
|
if (!key || normalizeHotkeyModifierToken(key)) {
|
|
return "";
|
|
}
|
|
|
|
let useMod = false;
|
|
let useCtrl = false;
|
|
let useMeta = false;
|
|
let useAlt = false;
|
|
let useShift = false;
|
|
|
|
for (const token of tokens.slice(0, -1)) {
|
|
const modifier = normalizeHotkeyModifierToken(token);
|
|
if (!modifier) {
|
|
return "";
|
|
}
|
|
if (modifier === "Mod") {
|
|
useMod = true;
|
|
useCtrl = false;
|
|
useMeta = false;
|
|
continue;
|
|
}
|
|
if (useMod && (modifier === "Ctrl" || modifier === "Meta")) {
|
|
continue;
|
|
}
|
|
if (modifier === "Ctrl") {
|
|
useCtrl = true;
|
|
} else if (modifier === "Meta") {
|
|
useMeta = true;
|
|
} else if (modifier === "Alt") {
|
|
useAlt = true;
|
|
} else if (modifier === "Shift") {
|
|
useShift = true;
|
|
}
|
|
}
|
|
|
|
return composeHotkeyString({
|
|
useMod,
|
|
useCtrl,
|
|
useMeta,
|
|
useAlt,
|
|
useShift,
|
|
key,
|
|
});
|
|
}
|
|
|
|
function formatHotkeyForDisplay(value) {
|
|
const normalized = normalizeStopHotkeySetting(value);
|
|
if (!normalized) {
|
|
return "";
|
|
}
|
|
|
|
const isMac = process.platform === "darwin";
|
|
return normalized
|
|
.split("+")
|
|
.map((part) => {
|
|
if (part === "Mod") {
|
|
return isMac ? "Cmd" : "Ctrl";
|
|
}
|
|
if (part === "Meta") {
|
|
return "Cmd";
|
|
}
|
|
if (part === "Alt") {
|
|
return isMac ? "Option" : "Alt";
|
|
}
|
|
return part;
|
|
})
|
|
.join("+");
|
|
}
|
|
|
|
function normalizeHotkeyFromEvent(event) {
|
|
const key = normalizeHotkeyPrimaryKey(event?.key);
|
|
if (!key || normalizeHotkeyModifierToken(key)) {
|
|
return "";
|
|
}
|
|
|
|
const isMac = process.platform === "darwin";
|
|
return composeHotkeyString({
|
|
useMod: isMac ? event.metaKey : event.ctrlKey,
|
|
useCtrl: isMac ? event.ctrlKey : false,
|
|
useMeta: !isMac && event.metaKey,
|
|
useAlt: event.altKey,
|
|
useShift: event.shiftKey,
|
|
key,
|
|
});
|
|
}
|
|
|
|
function hotkeyMatchesKeyboardEvent(configuredHotkey, event) {
|
|
const normalizedConfiguredHotkey = normalizeStopHotkeySetting(configuredHotkey);
|
|
if (!normalizedConfiguredHotkey) {
|
|
return false;
|
|
}
|
|
return normalizedConfiguredHotkey === normalizeHotkeyFromEvent(event);
|
|
}
|
|
|
|
function isHotkeyCaptureTarget(target) {
|
|
return Boolean(
|
|
target
|
|
&& target.classList
|
|
&& target.classList.contains(HOTKEY_CAPTURE_INPUT_CLASS)
|
|
);
|
|
}
|
|
|
|
function isPathInsideDirectory(candidatePath, directoryPath) {
|
|
const normalizedCandidatePath = String(candidatePath || "").trim();
|
|
const normalizedDirectoryPath = String(directoryPath || "").trim();
|
|
if (!normalizedCandidatePath || !normalizedDirectoryPath) {
|
|
return false;
|
|
}
|
|
|
|
const relativePath = path.relative(normalizedDirectoryPath, normalizedCandidatePath);
|
|
return relativePath === ""
|
|
|| (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
}
|
|
|
|
function resolveMarkdownFilePromptPath(file, cwd = "") {
|
|
const absolutePath = String(file?.absolutePath || "").trim();
|
|
const normalizedCwd = String(cwd || "").trim();
|
|
const relativeFilePath = normalizePromptPath(file?.path || "");
|
|
if (absolutePath) {
|
|
return {
|
|
promptPath: normalizePromptPath(absolutePath),
|
|
isDirectPath: true,
|
|
};
|
|
}
|
|
|
|
if (normalizedCwd && relativeFilePath) {
|
|
const candidateAbsolutePath = path.resolve(normalizedCwd, relativeFilePath);
|
|
if (fs.existsSync(candidateAbsolutePath)) {
|
|
return {
|
|
promptPath: normalizePromptPath(candidateAbsolutePath),
|
|
isDirectPath: true,
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
promptPath: relativeFilePath,
|
|
isDirectPath: false,
|
|
};
|
|
}
|
|
|
|
function getMarkdownFilePromptPath(file, options = {}) {
|
|
const { promptPath, isDirectPath } = resolveMarkdownFilePromptPath(file, options.cwd);
|
|
if (promptPath && isDirectPath) {
|
|
return promptPath;
|
|
}
|
|
|
|
return promptPath || String(file?.path || file?.absolutePath || "").trim();
|
|
}
|
|
|
|
function isSkillMarkdownFile(file, options = {}) {
|
|
const promptPath = getMarkdownFilePromptPath(file, options);
|
|
const normalizedPath = promptPath.replace(/\\/g, "/");
|
|
const name = String(file?.name || "").trim();
|
|
return /(^|\/)SKILL\.md$/i.test(normalizedPath)
|
|
|| /(^|\/)Codex Skills\//i.test(normalizedPath)
|
|
|| /(^|\/)\.codex\/skills\//i.test(normalizedPath)
|
|
|| /(^|\/)\.agents\/skills\//i.test(normalizedPath)
|
|
|| /^SKILL\.md$/i.test(name);
|
|
}
|
|
|
|
function buildMarkdownFilePromptBlock(file, index, options = {}) {
|
|
const { promptPath, isDirectPath } = resolveMarkdownFilePromptPath(file, options.cwd);
|
|
if (promptPath && isDirectPath) {
|
|
return `- ${promptPath}`;
|
|
}
|
|
|
|
const filePath = promptPath || String(file?.path || file?.absolutePath || "").trim();
|
|
const fileContent = String(file?.content || "");
|
|
if (filePath && fileContent) {
|
|
return `- ${filePath}\n\n\`\`\`md\n${fileContent}\n\`\`\``;
|
|
}
|
|
|
|
return filePath ? `- ${filePath}` : `- Markdown file ${index + 1}`;
|
|
}
|
|
|
|
function shouldAppendUserRequest(textBlocks, markdownFiles, imageFiles, trimmedMessage, includeContext = true) {
|
|
if (!trimmedMessage) {
|
|
return false;
|
|
}
|
|
|
|
if (!includeContext) {
|
|
return true;
|
|
}
|
|
|
|
if (textBlocks.length !== 1) {
|
|
return true;
|
|
}
|
|
|
|
return String(textBlocks[0]?.text || "").trim() !== trimmedMessage;
|
|
}
|
|
|
|
function buildMarkdownFilePromptSections(markdownFiles, options = {}) {
|
|
const contextFiles = [];
|
|
const skillFiles = [];
|
|
|
|
markdownFiles.forEach((file) => {
|
|
if (isSkillMarkdownFile(file, options)) {
|
|
skillFiles.push(file);
|
|
} else {
|
|
contextFiles.push(file);
|
|
}
|
|
});
|
|
|
|
const sections = [];
|
|
if (contextFiles.length > 0) {
|
|
sections.push(`Context:\n${contextFiles.map((file, index) => buildMarkdownFilePromptBlock(file, index, options)).join("\n")}`);
|
|
}
|
|
if (skillFiles.length > 0) {
|
|
sections.push(`Skills:\n${skillFiles.map((file, index) => buildMarkdownFilePromptBlock(file, index, options)).join("\n")}`);
|
|
}
|
|
|
|
return sections;
|
|
}
|
|
|
|
function buildCanvasSelectionPrompt(selection, userMessage, options = {}) {
|
|
const textBlocks = Array.isArray(selection?.textBlocks)
|
|
? selection.textBlocks.filter((block) => String(block?.text || "").trim())
|
|
: [];
|
|
const markdownFiles = Array.isArray(selection?.markdownFiles)
|
|
? selection.markdownFiles.filter((file) => String(file?.path || "").trim())
|
|
: [];
|
|
const imageFiles = Array.isArray(selection?.imageFiles)
|
|
? selection.imageFiles.filter((file) => String(file?.path || "").trim())
|
|
: [];
|
|
const warnings = Array.isArray(selection?.warnings)
|
|
? selection.warnings.map((warning) => String(warning || "").trim()).filter(Boolean)
|
|
: [];
|
|
const trimmedMessage = String(userMessage || "").trim();
|
|
const parts = [];
|
|
|
|
if (textBlocks.length > 0) {
|
|
parts.push(
|
|
textBlocks
|
|
.map((block) => String(block.text || ""))
|
|
.join("\n\n")
|
|
);
|
|
}
|
|
|
|
if (markdownFiles.length > 0) {
|
|
parts.push(...buildMarkdownFilePromptSections(markdownFiles, options));
|
|
}
|
|
|
|
if (imageFiles.length > 0) {
|
|
parts.push("Selected image files are attached to this turn as image inputs. Use them as part of the Canvas context.");
|
|
parts.push(
|
|
imageFiles
|
|
.map((file, index) => `Image file ${index + 1}: ${String(file.path || file.name || "Untitled")}`)
|
|
.join("\n\n")
|
|
);
|
|
}
|
|
|
|
if (warnings.length > 0) {
|
|
parts.push(`Resolver warnings:\n- ${warnings.join("\n- ")}`);
|
|
}
|
|
|
|
if (shouldAppendUserRequest(textBlocks, markdownFiles, imageFiles, trimmedMessage, true)) {
|
|
parts.push(trimmedMessage);
|
|
}
|
|
|
|
return parts.join("\n\n").trim();
|
|
}
|
|
|
|
function basenameVaultPath(value) {
|
|
const normalized = String(value || "").replace(/^\/+|\/+$/g, "");
|
|
if (!normalized) {
|
|
return "";
|
|
}
|
|
const index = normalized.lastIndexOf("/");
|
|
return index >= 0 ? normalized.slice(index + 1) : normalized;
|
|
}
|
|
|
|
function normalizeDaemonSandboxMode(value) {
|
|
return value === DAEMON_SANDBOX_MODE_OPTIONS.DANGER_FULL_ACCESS
|
|
? DAEMON_SANDBOX_MODE_OPTIONS.DANGER_FULL_ACCESS
|
|
: DAEMON_SANDBOX_MODE_OPTIONS.WORKSPACE_WRITE;
|
|
}
|
|
|
|
function getDaemonSandboxModeLabel(value) {
|
|
return normalizeDaemonSandboxMode(value) === DAEMON_SANDBOX_MODE_OPTIONS.DANGER_FULL_ACCESS
|
|
? "Full access"
|
|
: "Workspace only";
|
|
}
|
|
|
|
function getDaemonSandboxModeHelpText(value) {
|
|
return normalizeDaemonSandboxMode(value) === DAEMON_SANDBOX_MODE_OPTIONS.DANGER_FULL_ACCESS
|
|
? "Full access removes the filesystem sandbox and can reach files outside the selected project folder. Use it only for repos and prompts you trust."
|
|
: "Workspace-only keeps Codex limited to the selected project folder and is the safer default for normal work.";
|
|
}
|
|
|
|
function normalizeRepoPath(value) {
|
|
const trimmed = String(value || "").trim();
|
|
if (!trimmed || !path.isAbsolute(trimmed)) {
|
|
return "";
|
|
}
|
|
|
|
return path.normalize(trimmed);
|
|
}
|
|
|
|
function compactPathLabel(value) {
|
|
const normalized = String(value || "").trim();
|
|
if (!normalized) {
|
|
return "";
|
|
}
|
|
|
|
return path.basename(normalized) || normalized;
|
|
}
|
|
|
|
function inferWorkspaceNameFromRepoPath(repoPath) {
|
|
const normalized = normalizeRepoPath(repoPath);
|
|
return normalized ? path.basename(normalized) : "";
|
|
}
|
|
|
|
function stableShortHash(value) {
|
|
return crypto.createHash("sha256").update(String(value || ""), "utf8").digest("hex").slice(0, 16);
|
|
}
|
|
|
|
function createCanvasObjectId(prefix = "oa-canvas") {
|
|
const normalizedPrefix = String(prefix || "oa-canvas").trim() || "oa-canvas";
|
|
if (typeof crypto.randomUUID === "function") {
|
|
return `${normalizedPrefix}-${crypto.randomUUID()}`;
|
|
}
|
|
|
|
return `${normalizedPrefix}-${stableShortHash(`${Date.now()}\0${Math.random().toString(16)}`)}`;
|
|
}
|
|
|
|
function createVaultId() {
|
|
return createCanvasObjectId("oa-vault");
|
|
}
|
|
|
|
function buildTaskResultNodeId(taskId, sourceNodeId) {
|
|
return `oa-result-${stableShortHash(`${String(taskId || "").trim()}\0${String(sourceNodeId || "").trim()}`)}`;
|
|
}
|
|
|
|
function getOpenAgentCanvasMetadata(value) {
|
|
const metadata = value?.openagent;
|
|
return metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : null;
|
|
}
|
|
|
|
function isOpenAgentAssistantResultNode(node) {
|
|
const metadata = getOpenAgentCanvasMetadata(node);
|
|
return metadata?.kind === "assistant-result" || String(node?.id || "").startsWith("oa-result-");
|
|
}
|
|
|
|
function getOpenAgentResultSourceNodeId(node) {
|
|
const metadata = getOpenAgentCanvasMetadata(node);
|
|
return metadata?.kind === "assistant-result" ? String(metadata.sourceNodeId || "").trim() : "";
|
|
}
|
|
|
|
function getOpenAgentResultTaskId(node) {
|
|
const metadata = getOpenAgentCanvasMetadata(node);
|
|
return metadata?.kind === "assistant-result" ? String(metadata.taskId || "").trim() : "";
|
|
}
|
|
|
|
function normalizeResultNodeSyncState(value) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return {};
|
|
}
|
|
|
|
return Object.fromEntries(
|
|
Object.entries(value)
|
|
.map(([taskId, signature]) => [String(taskId || "").trim(), String(signature || "").trim()])
|
|
.filter(([taskId, signature]) => taskId && signature)
|
|
);
|
|
}
|
|
|
|
function normalizeCanvasRunSourceRefState(value) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return {};
|
|
}
|
|
|
|
return Object.fromEntries(
|
|
Object.entries(value)
|
|
.map(([taskId, ref]) => {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
const canvasPath = String(ref?.canvasPath || "").trim();
|
|
const nodeId = String(ref?.nodeId || "").trim();
|
|
return [normalizedTaskId, { canvasPath, nodeId }];
|
|
})
|
|
.filter(([taskId, ref]) => taskId && ref.canvasPath && ref.nodeId)
|
|
);
|
|
}
|
|
|
|
function normalizeUnreadTaskState(value) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return {};
|
|
}
|
|
|
|
return Object.fromEntries(
|
|
Object.entries(value)
|
|
.map(([taskId, unread]) => [String(taskId || "").trim(), unread === true])
|
|
.filter(([taskId, unread]) => taskId && unread)
|
|
);
|
|
}
|
|
|
|
function clampNumber(value, min, max) {
|
|
return Math.max(min, Math.min(max, value));
|
|
}
|
|
|
|
function toFiniteNumber(value, fallback = 0) {
|
|
const numeric = Number(value);
|
|
return Number.isFinite(numeric) ? numeric : fallback;
|
|
}
|
|
|
|
function estimateCanvasTextNodeHeight(text, width = RESULT_NODE_DEFAULT_WIDTH) {
|
|
const normalizedText = String(text || "").replace(/\r\n/g, "\n");
|
|
if (!normalizedText.trim()) {
|
|
return RESULT_NODE_MIN_HEIGHT;
|
|
}
|
|
|
|
const charsPerLine = clampNumber(
|
|
Math.round(toFiniteNumber(width, RESULT_NODE_DEFAULT_WIDTH) / 8),
|
|
24,
|
|
RESULT_NODE_CHARS_PER_LINE
|
|
);
|
|
const visualLines = normalizedText
|
|
.split("\n")
|
|
.reduce((count, line) => count + Math.max(1, Math.ceil(Math.max(1, line.length) / charsPerLine)), 0);
|
|
|
|
return clampNumber(88 + (visualLines * 22), RESULT_NODE_MIN_HEIGHT, RESULT_NODE_MAX_HEIGHT);
|
|
}
|
|
|
|
function getCanvasTextNodeAutoFitHeight(node) {
|
|
if (String(node?.type || "").trim() !== "text") {
|
|
return null;
|
|
}
|
|
|
|
const width = clampNumber(
|
|
toFiniteNumber(node?.width, RESULT_NODE_DEFAULT_WIDTH),
|
|
RESULT_NODE_MIN_WIDTH,
|
|
RESULT_NODE_MAX_WIDTH
|
|
);
|
|
const estimatedHeight = estimateCanvasTextNodeHeight(node?.text, width);
|
|
const existingHeight = toFiniteNumber(node?.height, 0);
|
|
return Math.max(estimatedHeight, existingHeight);
|
|
}
|
|
|
|
function compareCanvasNodeOrder(a, b) {
|
|
const yDifference = toFiniteNumber(a?.y, 0) - toFiniteNumber(b?.y, 0);
|
|
if (yDifference !== 0) {
|
|
return yDifference;
|
|
}
|
|
|
|
const xDifference = toFiniteNumber(a?.x, 0) - toFiniteNumber(b?.x, 0);
|
|
if (xDifference !== 0) {
|
|
return xDifference;
|
|
}
|
|
|
|
return String(a?.id || "").localeCompare(String(b?.id || ""));
|
|
}
|
|
|
|
function averageNumbers(values) {
|
|
return values.length > 0
|
|
? values.reduce((sum, value) => sum + value, 0) / values.length
|
|
: null;
|
|
}
|
|
|
|
function getCanvasLayoutNodeWidth(node) {
|
|
const fallbackWidth = String(node?.type || "").trim() === "text" ? RESULT_NODE_DEFAULT_WIDTH : 360;
|
|
return Math.max(CANVAS_LAYOUT_MIN_WIDTH, toFiniteNumber(node?.width, fallbackWidth));
|
|
}
|
|
|
|
function getCanvasLayoutNodeHeight(node, width) {
|
|
const existingHeight = toFiniteNumber(node?.height, 0);
|
|
if (existingHeight > 0) {
|
|
return Math.max(CANVAS_LAYOUT_MIN_HEIGHT, existingHeight);
|
|
}
|
|
|
|
if (String(node?.type || "").trim() === "text") {
|
|
return Math.max(CANVAS_LAYOUT_MIN_HEIGHT, estimateCanvasTextNodeHeight(node?.text, width));
|
|
}
|
|
|
|
return 220;
|
|
}
|
|
|
|
function getCanvasNodeCenter(node) {
|
|
if (!node || typeof node !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const width = Math.max(0, toFiniteNumber(node.width, 0));
|
|
const height = Math.max(0, toFiniteNumber(node.height, 0));
|
|
return {
|
|
x: toFiniteNumber(node.x, 0) + (width / 2),
|
|
y: toFiniteNumber(node.y, 0) + (height / 2),
|
|
};
|
|
}
|
|
|
|
function doesCanvasGroupContainNode(groupNode, candidateNode) {
|
|
if (String(groupNode?.type || "").trim() !== "group") {
|
|
return false;
|
|
}
|
|
|
|
const center = getCanvasNodeCenter(candidateNode);
|
|
if (!center) {
|
|
return false;
|
|
}
|
|
|
|
const groupX = toFiniteNumber(groupNode.x, 0);
|
|
const groupY = toFiniteNumber(groupNode.y, 0);
|
|
const groupWidth = Math.max(0, toFiniteNumber(groupNode.width, 0));
|
|
const groupHeight = Math.max(0, toFiniteNumber(groupNode.height, 0));
|
|
return (
|
|
center.x >= groupX
|
|
&& center.x <= groupX + groupWidth
|
|
&& center.y >= groupY
|
|
&& center.y <= groupY + groupHeight
|
|
);
|
|
}
|
|
|
|
function escapeAttributeSelectorValue(value) {
|
|
return String(value || "")
|
|
.replace(/\\/g, "\\\\")
|
|
.replace(/"/g, '\\"');
|
|
}
|
|
|
|
function parseIsoTimestamp(value) {
|
|
const normalizedValue = String(value || "").trim();
|
|
if (!normalizedValue) {
|
|
return null;
|
|
}
|
|
|
|
const timestamp = Date.parse(normalizedValue);
|
|
return Number.isFinite(timestamp) ? timestamp : null;
|
|
}
|
|
|
|
function getStartOfLocalDayTimestamp(value) {
|
|
const timestamp = Number(value);
|
|
if (!Number.isFinite(timestamp)) {
|
|
return null;
|
|
}
|
|
|
|
const date = new Date(timestamp);
|
|
date.setHours(0, 0, 0, 0);
|
|
return date.getTime();
|
|
}
|
|
|
|
function formatTaskListDayLabel(dayTimestamp) {
|
|
const normalizedDayTimestamp = Number(dayTimestamp);
|
|
if (!Number.isFinite(normalizedDayTimestamp)) {
|
|
return "Unknown";
|
|
}
|
|
|
|
const dayDate = new Date(normalizedDayTimestamp);
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
const dayDifference = Math.round((today.getTime() - normalizedDayTimestamp) / 86_400_000);
|
|
if (dayDifference === 0) {
|
|
return "Today";
|
|
}
|
|
if (dayDifference === 1) {
|
|
return "Yesterday";
|
|
}
|
|
|
|
return dayDate.toLocaleDateString([], {
|
|
weekday: "short",
|
|
month: "short",
|
|
day: "numeric",
|
|
year: dayDate.getFullYear() === today.getFullYear() ? undefined : "numeric",
|
|
});
|
|
}
|
|
|
|
function formatTaskListTimeLabel(timestamp) {
|
|
const normalizedTimestamp = Number(timestamp);
|
|
if (!Number.isFinite(normalizedTimestamp) || normalizedTimestamp <= 0) {
|
|
return "";
|
|
}
|
|
|
|
try {
|
|
return new Date(normalizedTimestamp).toLocaleTimeString([], {
|
|
hour: "numeric",
|
|
minute: "2-digit",
|
|
});
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function autoArrangeCanvasNodes(nodes, edges) {
|
|
const originalNodes = Array.isArray(nodes)
|
|
? nodes
|
|
: [];
|
|
const normalizedNodes = originalNodes.filter((node) => String(node?.id || "").trim());
|
|
const groupNodes = normalizedNodes.filter((node) => String(node?.type || "").trim() === "group");
|
|
const layoutNodes = normalizedNodes.filter((node) => {
|
|
if (String(node?.type || "").trim() === "group") {
|
|
return false;
|
|
}
|
|
|
|
// Leave group contents untouched so auto-arrange only reflows top-level nodes.
|
|
return !groupNodes.some((groupNode) => doesCanvasGroupContainNode(groupNode, node));
|
|
});
|
|
if (layoutNodes.length <= 1) {
|
|
return originalNodes;
|
|
}
|
|
|
|
const normalizedEdges = Array.isArray(edges) ? edges : [];
|
|
const nodeById = new Map(
|
|
layoutNodes.map((node) => [String(node.id || "").trim(), node])
|
|
);
|
|
const outgoingNodeIdsByNodeId = new Map();
|
|
const incomingNodeIdsByNodeId = new Map();
|
|
const neighborNodeIdsByNodeId = new Map();
|
|
|
|
nodeById.forEach((_node, nodeId) => {
|
|
outgoingNodeIdsByNodeId.set(nodeId, []);
|
|
incomingNodeIdsByNodeId.set(nodeId, []);
|
|
neighborNodeIdsByNodeId.set(nodeId, []);
|
|
});
|
|
|
|
normalizedEdges.forEach((edge) => {
|
|
const fromNodeId = String(edge?.fromNode || "").trim();
|
|
const toNodeId = String(edge?.toNode || "").trim();
|
|
if (!fromNodeId || !toNodeId || fromNodeId === toNodeId) {
|
|
return;
|
|
}
|
|
if (!nodeById.has(fromNodeId) || !nodeById.has(toNodeId)) {
|
|
return;
|
|
}
|
|
|
|
outgoingNodeIdsByNodeId.get(fromNodeId).push(toNodeId);
|
|
incomingNodeIdsByNodeId.get(toNodeId).push(fromNodeId);
|
|
neighborNodeIdsByNodeId.get(fromNodeId).push(toNodeId);
|
|
neighborNodeIdsByNodeId.get(toNodeId).push(fromNodeId);
|
|
});
|
|
|
|
const sortNodeIds = (nodeIds) => {
|
|
return [...new Set(nodeIds)].sort((leftNodeId, rightNodeId) => {
|
|
return compareCanvasNodeOrder(nodeById.get(leftNodeId), nodeById.get(rightNodeId));
|
|
});
|
|
};
|
|
|
|
const orderedNodeIds = sortNodeIds([...nodeById.keys()]);
|
|
const unvisitedNodeIds = new Set(orderedNodeIds);
|
|
const connectedComponents = [];
|
|
|
|
while (unvisitedNodeIds.size > 0) {
|
|
const startNodeId = orderedNodeIds.find((nodeId) => unvisitedNodeIds.has(nodeId));
|
|
if (!startNodeId) {
|
|
break;
|
|
}
|
|
|
|
const componentNodeIds = [];
|
|
const queue = [startNodeId];
|
|
unvisitedNodeIds.delete(startNodeId);
|
|
|
|
while (queue.length > 0) {
|
|
const currentNodeId = queue.shift();
|
|
componentNodeIds.push(currentNodeId);
|
|
|
|
sortNodeIds(neighborNodeIdsByNodeId.get(currentNodeId) || []).forEach((neighborNodeId) => {
|
|
if (!unvisitedNodeIds.has(neighborNodeId)) {
|
|
return;
|
|
}
|
|
|
|
unvisitedNodeIds.delete(neighborNodeId);
|
|
queue.push(neighborNodeId);
|
|
});
|
|
}
|
|
|
|
connectedComponents.push(sortNodeIds(componentNodeIds));
|
|
}
|
|
|
|
const isolatedNodeIds = [];
|
|
const layoutComponents = connectedComponents.filter((componentNodeIds) => {
|
|
if (componentNodeIds.length !== 1) {
|
|
return true;
|
|
}
|
|
|
|
const nodeId = componentNodeIds[0];
|
|
if ((neighborNodeIdsByNodeId.get(nodeId) || []).length > 0) {
|
|
return true;
|
|
}
|
|
|
|
isolatedNodeIds.push(nodeId);
|
|
return false;
|
|
});
|
|
if (isolatedNodeIds.length > 0) {
|
|
layoutComponents.push(sortNodeIds(isolatedNodeIds));
|
|
}
|
|
|
|
const nextPositionByNodeId = new Map();
|
|
let componentOffsetX = CANVAS_LAYOUT_BASE_X;
|
|
|
|
[...layoutComponents].reverse().forEach((componentNodeIds) => {
|
|
const componentNodeIdSet = new Set(componentNodeIds);
|
|
const localIncomingCountByNodeId = new Map();
|
|
const depthByNodeId = new Map();
|
|
const pendingNodeIds = [];
|
|
const consumedNodeIds = new Set();
|
|
|
|
componentNodeIds.forEach((nodeId) => {
|
|
const incomingNodeIds = (incomingNodeIdsByNodeId.get(nodeId) || []).filter((incomingNodeId) => {
|
|
return componentNodeIdSet.has(incomingNodeId);
|
|
});
|
|
localIncomingCountByNodeId.set(nodeId, incomingNodeIds.length);
|
|
if (incomingNodeIds.length === 0) {
|
|
depthByNodeId.set(nodeId, 0);
|
|
pendingNodeIds.push(nodeId);
|
|
}
|
|
});
|
|
|
|
while (pendingNodeIds.length > 0) {
|
|
pendingNodeIds.sort((leftNodeId, rightNodeId) => {
|
|
return compareCanvasNodeOrder(nodeById.get(leftNodeId), nodeById.get(rightNodeId));
|
|
});
|
|
const currentNodeId = pendingNodeIds.shift();
|
|
if (consumedNodeIds.has(currentNodeId)) {
|
|
continue;
|
|
}
|
|
|
|
consumedNodeIds.add(currentNodeId);
|
|
const nextDepth = (depthByNodeId.get(currentNodeId) || 0) + 1;
|
|
|
|
sortNodeIds((outgoingNodeIdsByNodeId.get(currentNodeId) || []).filter((nextNodeId) => {
|
|
return componentNodeIdSet.has(nextNodeId);
|
|
})).forEach((nextNodeId) => {
|
|
depthByNodeId.set(nextNodeId, Math.max(depthByNodeId.get(nextNodeId) || 0, nextDepth));
|
|
localIncomingCountByNodeId.set(nextNodeId, Math.max(0, (localIncomingCountByNodeId.get(nextNodeId) || 0) - 1));
|
|
if ((localIncomingCountByNodeId.get(nextNodeId) || 0) === 0) {
|
|
pendingNodeIds.push(nextNodeId);
|
|
}
|
|
});
|
|
}
|
|
|
|
let maxDepth = depthByNodeId.size > 0
|
|
? Math.max(...depthByNodeId.values())
|
|
: 0;
|
|
|
|
sortNodeIds(componentNodeIds.filter((nodeId) => !depthByNodeId.has(nodeId))).forEach((nodeId) => {
|
|
const incomingDepths = (incomingNodeIdsByNodeId.get(nodeId) || [])
|
|
.filter((incomingNodeId) => componentNodeIdSet.has(incomingNodeId) && depthByNodeId.has(incomingNodeId))
|
|
.map((incomingNodeId) => depthByNodeId.get(incomingNodeId));
|
|
const suggestedDepth = incomingDepths.length > 0 ? Math.max(...incomingDepths) + 1 : maxDepth + 1;
|
|
depthByNodeId.set(nodeId, suggestedDepth);
|
|
maxDepth = Math.max(maxDepth, suggestedDepth);
|
|
});
|
|
|
|
const rowDepths = [...new Set(componentNodeIds.map((nodeId) => depthByNodeId.get(nodeId) || 0))].sort((leftDepth, rightDepth) => {
|
|
return leftDepth - rightDepth;
|
|
});
|
|
const rowHeightByDepth = new Map();
|
|
rowDepths.forEach((depth) => {
|
|
const rowHeight = componentNodeIds
|
|
.filter((nodeId) => (depthByNodeId.get(nodeId) || 0) === depth)
|
|
.reduce((maxHeight, nodeId) => {
|
|
const width = getCanvasLayoutNodeWidth(nodeById.get(nodeId));
|
|
return Math.max(maxHeight, getCanvasLayoutNodeHeight(nodeById.get(nodeId), width));
|
|
}, CANVAS_LAYOUT_MIN_HEIGHT);
|
|
rowHeightByDepth.set(depth, rowHeight);
|
|
});
|
|
|
|
const placedCenterXByNodeId = new Map();
|
|
let componentWidth = 0;
|
|
let rowOffsetY = CANVAS_LAYOUT_BASE_Y;
|
|
|
|
rowDepths.forEach((depth, rowIndex) => {
|
|
if (rowIndex > 0) {
|
|
const previousDepth = rowDepths[rowIndex - 1];
|
|
rowOffsetY += (rowHeightByDepth.get(previousDepth) || CANVAS_LAYOUT_MIN_HEIGHT) + CANVAS_LAYOUT_Y_GAP;
|
|
}
|
|
|
|
const rowNodeIds = componentNodeIds
|
|
.filter((nodeId) => (depthByNodeId.get(nodeId) || 0) === depth)
|
|
.sort((leftNodeId, rightNodeId) => {
|
|
const leftAverageIncomingCenter = averageNumbers(
|
|
(incomingNodeIdsByNodeId.get(leftNodeId) || [])
|
|
.map((incomingNodeId) => placedCenterXByNodeId.get(incomingNodeId))
|
|
.filter((value) => Number.isFinite(value))
|
|
);
|
|
const rightAverageIncomingCenter = averageNumbers(
|
|
(incomingNodeIdsByNodeId.get(rightNodeId) || [])
|
|
.map((incomingNodeId) => placedCenterXByNodeId.get(incomingNodeId))
|
|
.filter((value) => Number.isFinite(value))
|
|
);
|
|
if (leftAverageIncomingCenter !== null && rightAverageIncomingCenter !== null && leftAverageIncomingCenter !== rightAverageIncomingCenter) {
|
|
return leftAverageIncomingCenter - rightAverageIncomingCenter;
|
|
}
|
|
if (leftAverageIncomingCenter !== null || rightAverageIncomingCenter !== null) {
|
|
return leftAverageIncomingCenter === null ? 1 : -1;
|
|
}
|
|
|
|
return compareCanvasNodeOrder(nodeById.get(leftNodeId), nodeById.get(rightNodeId));
|
|
});
|
|
|
|
let rowOffsetX = 0;
|
|
rowNodeIds.forEach((nodeId) => {
|
|
const node = nodeById.get(nodeId);
|
|
const width = getCanvasLayoutNodeWidth(node);
|
|
const height = getCanvasLayoutNodeHeight(node, width);
|
|
nextPositionByNodeId.set(nodeId, {
|
|
x: componentOffsetX + rowOffsetX,
|
|
y: rowOffsetY,
|
|
});
|
|
placedCenterXByNodeId.set(nodeId, rowOffsetX + (width / 2));
|
|
rowOffsetX += width + CANVAS_LAYOUT_X_GAP;
|
|
});
|
|
|
|
componentWidth = Math.max(componentWidth, Math.max(0, rowOffsetX - CANVAS_LAYOUT_X_GAP));
|
|
});
|
|
|
|
componentOffsetX += componentWidth + CANVAS_LAYOUT_COMPONENT_GAP;
|
|
});
|
|
|
|
return originalNodes.map((node) => {
|
|
const nodeId = String(node?.id || "").trim();
|
|
const nextPosition = nextPositionByNodeId.get(nodeId);
|
|
if (!nextPosition) {
|
|
return node;
|
|
}
|
|
|
|
return {
|
|
...node,
|
|
x: nextPosition.x,
|
|
y: nextPosition.y,
|
|
};
|
|
});
|
|
}
|
|
|
|
class WorkspacePickerModal extends Modal {
|
|
constructor(app, plugin, options = {}) {
|
|
super(app);
|
|
this.plugin = plugin;
|
|
this.options = options;
|
|
this.repoPathInput = null;
|
|
this.workspaceNameInput = null;
|
|
}
|
|
|
|
onOpen() {
|
|
const loadPromise = this.plugin.ensureWorkspaceSummariesLoaded();
|
|
this.render();
|
|
void loadPromise.finally(() => {
|
|
if (this.contentEl?.isConnected) {
|
|
this.render();
|
|
}
|
|
});
|
|
}
|
|
|
|
render() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
contentEl.addClass("oa-workspace-modal");
|
|
contentEl.createEl("h2", { text: "Choose workspace" });
|
|
contentEl.createDiv({
|
|
cls: "oa-workspace-modal-subtitle",
|
|
text: "Open an existing repo workspace or create a new Canvas home for a project.",
|
|
});
|
|
|
|
const existing = this.plugin.listWorkspaceSummaries();
|
|
const isLoadingWorkspaces = this.plugin.isWorkspaceSummariesLoading();
|
|
const existingSection = contentEl.createDiv({ cls: "oa-workspace-section" });
|
|
existingSection.createEl("h3", { text: "Existing workspaces" });
|
|
|
|
if (existing.length === 0 && isLoadingWorkspaces) {
|
|
existingSection.createDiv({
|
|
cls: "oa-muted",
|
|
text: "Loading workspaces...",
|
|
});
|
|
} else if (existing.length === 0) {
|
|
existingSection.createDiv({
|
|
cls: "oa-muted",
|
|
text: "No workspace yet. Create one from a repo path below.",
|
|
});
|
|
} else {
|
|
const list = existingSection.createDiv({ cls: "oa-workspace-list" });
|
|
existing.forEach((workspace) => {
|
|
const item = list.createDiv({ cls: "oa-workspace-item" });
|
|
const itemMain = item.createDiv({ cls: "oa-workspace-item-main" });
|
|
itemMain.createDiv({ cls: "oa-task-title", text: workspace.name });
|
|
itemMain.createDiv({ cls: "oa-task-meta", text: workspace.repoPath });
|
|
const buttonRow = item.createDiv({ cls: "oa-action-row" });
|
|
const openButton = new ButtonComponent(buttonRow);
|
|
openButton.setButtonText("Open");
|
|
openButton.buttonEl?.addClass("oa-workspace-open-button");
|
|
openButton.onClick(async () => {
|
|
await this.plugin.openWorkspace(workspace);
|
|
this.close();
|
|
});
|
|
});
|
|
}
|
|
|
|
const createSection = contentEl.createDiv({ cls: "oa-workspace-section oa-workspace-create-panel" });
|
|
createSection.createEl("h3", { text: "Create workspace" });
|
|
|
|
new Setting(createSection)
|
|
.setName("Repo path")
|
|
.setDesc("Absolute filesystem path to the repo folder that Codex should use for this workspace.")
|
|
.addText((text) => {
|
|
this.repoPathInput = text;
|
|
text
|
|
.setPlaceholder("/absolute/path/to/repo")
|
|
.setValue("");
|
|
})
|
|
.addButton((button) => {
|
|
button.setButtonText("Browse...");
|
|
button.onClick(async () => {
|
|
try {
|
|
const defaultPath = this.repoPathInput?.getValue?.() || "";
|
|
const selectedPath = await this.plugin.pickDirectoryPath({
|
|
defaultPath,
|
|
title: "Choose repo folder",
|
|
});
|
|
if (!selectedPath) {
|
|
return;
|
|
}
|
|
|
|
this.repoPathInput?.setValue(selectedPath);
|
|
if (this.workspaceNameInput && !String(this.workspaceNameInput.getValue?.() || "").trim()) {
|
|
this.workspaceNameInput.setValue(inferWorkspaceNameFromRepoPath(selectedPath));
|
|
}
|
|
} catch (error) {
|
|
new Notice(String(error?.message || error));
|
|
}
|
|
});
|
|
});
|
|
|
|
new Setting(createSection)
|
|
.setName("Workspace name")
|
|
.setDesc("Optional. Leave blank to use the repo folder name.")
|
|
.addText((text) => {
|
|
this.workspaceNameInput = text;
|
|
text
|
|
.setPlaceholder("openagent")
|
|
.setValue("");
|
|
});
|
|
|
|
const actions = createSection.createDiv({ cls: "oa-action-row" });
|
|
const createButton = new ButtonComponent(actions);
|
|
createButton.setButtonText("Create");
|
|
createButton.setCta();
|
|
createButton.onClick(async () => {
|
|
try {
|
|
const repoPath = this.repoPathInput?.getValue?.() || "";
|
|
const workspaceName = this.workspaceNameInput?.getValue?.() || "";
|
|
await this.plugin.createWorkspaceFromRepoPath(repoPath, workspaceName);
|
|
this.close();
|
|
} catch (error) {
|
|
new Notice(String(error?.message || error));
|
|
}
|
|
});
|
|
}
|
|
|
|
onClose() {
|
|
this.contentEl.empty();
|
|
}
|
|
}
|
|
|
|
class CanvasForkConfirmModal extends Modal {
|
|
constructor(app, options = {}) {
|
|
super(app);
|
|
this.options = options;
|
|
this.resolve = typeof options.resolve === "function" ? options.resolve : () => {};
|
|
this.didChoose = false;
|
|
}
|
|
|
|
onOpen() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
contentEl.addClass("oa-fork-modal");
|
|
contentEl.createEl("h2", { text: "Fork from here?" });
|
|
contentEl.createDiv({
|
|
cls: "oa-fork-modal-subtitle",
|
|
text: "This branch starts a separate OpenAgent thread from the selected Canvas point.",
|
|
});
|
|
|
|
const details = contentEl.createDiv({ cls: "oa-fork-modal-details" });
|
|
const originLabel = String(this.options.originLabel || "").trim();
|
|
const rollbackTurns = Math.max(0, Math.floor(Number(this.options.rollbackTurns || 0)));
|
|
if (originLabel) {
|
|
details.createDiv({ text: `Origin: ${originLabel}` });
|
|
}
|
|
if (rollbackTurns > 0) {
|
|
details.createDiv({ text: `Codex will fork the thread and drop ${rollbackTurns} later turn${rollbackTurns === 1 ? "" : "s"} before running this node.` });
|
|
}
|
|
|
|
const actions = contentEl.createDiv({ cls: "oa-action-row oa-fork-modal-actions" });
|
|
const forkButton = new ButtonComponent(actions);
|
|
forkButton.setButtonText("Fork and run");
|
|
forkButton.setCta();
|
|
forkButton.onClick(() => this.choose("fork"));
|
|
|
|
const continueButton = new ButtonComponent(actions);
|
|
continueButton.setButtonText("Continue thread");
|
|
continueButton.onClick(() => this.choose("continue"));
|
|
|
|
const cancelButton = new ButtonComponent(actions);
|
|
cancelButton.setButtonText("Cancel");
|
|
cancelButton.onClick(() => this.choose("cancel"));
|
|
}
|
|
|
|
choose(value) {
|
|
this.didChoose = true;
|
|
this.resolve(value);
|
|
this.close();
|
|
}
|
|
|
|
onClose() {
|
|
this.contentEl.empty();
|
|
if (!this.didChoose) {
|
|
this.resolve("cancel");
|
|
}
|
|
}
|
|
}
|
|
|
|
class OpenAgentDaemonLauncher {
|
|
constructor(plugin) {
|
|
this.plugin = plugin;
|
|
this.startPromise = null;
|
|
}
|
|
|
|
async ensureStarted() {
|
|
if (this.startPromise) {
|
|
return this.startPromise;
|
|
}
|
|
|
|
this.startPromise = this.startInternal();
|
|
try {
|
|
return await this.startPromise;
|
|
} finally {
|
|
this.startPromise = null;
|
|
}
|
|
}
|
|
|
|
async startInternal() {
|
|
const launchSpec = this.resolveLaunchSpec();
|
|
if (!launchSpec?.command) {
|
|
throw new Error(
|
|
"Codex server is not running, and auto-launch could not find this repo. Set a server launch command in OpenAgent settings."
|
|
);
|
|
}
|
|
|
|
fs.mkdirSync(path.dirname(DAEMON_LOG_PATH), { recursive: true });
|
|
|
|
const shellCommand = `${launchSpec.command} >> ${shellEscape(DAEMON_LOG_PATH)} 2>&1`;
|
|
const child = spawn("/bin/zsh", ["-lc", shellCommand], {
|
|
cwd: launchSpec.cwd || os.homedir(),
|
|
detached: true,
|
|
stdio: "ignore",
|
|
});
|
|
child.unref();
|
|
|
|
await this.waitForReady();
|
|
}
|
|
|
|
resolveLaunchSpec() {
|
|
const configuredCommand = String(this.plugin.settings?.daemonLaunchCommand || "").trim();
|
|
const configuredCwd = this.normalizeDirectory(this.plugin.settings?.daemonLaunchCwd);
|
|
if (configuredCommand) {
|
|
return {
|
|
cwd: configuredCwd || os.homedir(),
|
|
command: configuredCommand,
|
|
};
|
|
}
|
|
|
|
const repoRoot = this.findRepoRoot();
|
|
if (!repoRoot) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
cwd: repoRoot,
|
|
command: `cd ${shellEscape(repoRoot)} && exec pnpm dev:daemon`,
|
|
};
|
|
}
|
|
|
|
findRepoRoot() {
|
|
const candidates = [
|
|
this.findRepoNearPlugin(),
|
|
path.join(os.homedir(), "Documents", "GitHub", "openagent"),
|
|
path.join(os.homedir(), "GitHub", "openagent"),
|
|
path.join(os.homedir(), "Documents", "openagent"),
|
|
path.join(os.homedir(), "openagent"),
|
|
].filter(Boolean);
|
|
|
|
for (const candidate of candidates) {
|
|
const daemonPath = path.join(candidate, "apps", "openagent-daemon", "src", "server.js");
|
|
const packagePath = path.join(candidate, "package.json");
|
|
if (fs.existsSync(daemonPath) && fs.existsSync(packagePath)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
findRepoNearPlugin() {
|
|
let currentPath = __dirname;
|
|
while (currentPath) {
|
|
const daemonPath = path.join(currentPath, "apps", "openagent-daemon", "src", "server.js");
|
|
if (fs.existsSync(daemonPath)) {
|
|
return currentPath;
|
|
}
|
|
|
|
const parentPath = path.dirname(currentPath);
|
|
if (parentPath === currentPath) {
|
|
return "";
|
|
}
|
|
currentPath = parentPath;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
normalizeDirectory(value) {
|
|
const trimmed = String(value || "").trim();
|
|
if (!trimmed || !path.isAbsolute(trimmed)) {
|
|
return "";
|
|
}
|
|
|
|
const normalized = path.normalize(trimmed);
|
|
if (!fs.existsSync(normalized) || !fs.statSync(normalized).isDirectory()) {
|
|
return "";
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
async waitForReady() {
|
|
let lastError = null;
|
|
|
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
await sleep(attempt === 0 ? 250 : 500);
|
|
|
|
try {
|
|
await this.plugin.api.requestOnce("GET", "/health");
|
|
return;
|
|
} catch (error) {
|
|
lastError = error;
|
|
}
|
|
}
|
|
|
|
throw lastError || new Error("Codex server did not become ready.");
|
|
}
|
|
}
|
|
|
|
class OpenAgentApiClient {
|
|
constructor(options = {}) {
|
|
this.config = null;
|
|
this.daemonLauncher = options.daemonLauncher || null;
|
|
}
|
|
|
|
loadConfig() {
|
|
if (this.config) {
|
|
return this.config;
|
|
}
|
|
|
|
if (!fs.existsSync(DAEMON_CONFIG_PATH)) {
|
|
throw new Error("Codex server config was not found. Start the server first.");
|
|
}
|
|
|
|
const parsed = JSON.parse(fs.readFileSync(DAEMON_CONFIG_PATH, "utf8"));
|
|
if (!parsed?.host || !parsed?.port || !parsed?.token) {
|
|
throw new Error("Codex server config is invalid.");
|
|
}
|
|
|
|
this.config = parsed;
|
|
return this.config;
|
|
}
|
|
|
|
async request(method, route, body = null) {
|
|
try {
|
|
return await this.requestOnce(method, route, body);
|
|
} catch (error) {
|
|
if (!this.shouldTryAutoLaunch(error)) {
|
|
throw error;
|
|
}
|
|
|
|
if (!this.daemonLauncher) {
|
|
throw error;
|
|
}
|
|
|
|
this.config = null;
|
|
await this.daemonLauncher.ensureStarted();
|
|
this.config = null;
|
|
return this.requestOnce(method, route, body);
|
|
}
|
|
}
|
|
|
|
async requestOnce(method, route, body = null) {
|
|
const config = this.loadConfig();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const payload = body == null ? null : JSON.stringify(body);
|
|
const request = http.request({
|
|
host: config.host,
|
|
port: config.port,
|
|
path: route,
|
|
method,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-openagent-token": config.token,
|
|
...(payload ? { "content-length": Buffer.byteLength(payload) } : {}),
|
|
},
|
|
}, (response) => {
|
|
let buffer = "";
|
|
response.setEncoding("utf8");
|
|
response.on("data", (chunk) => {
|
|
buffer += chunk;
|
|
});
|
|
response.on("end", () => {
|
|
let parsed = {};
|
|
try {
|
|
parsed = buffer.trim() ? JSON.parse(buffer) : {};
|
|
} catch (error) {
|
|
reject(new Error(`Unable to parse Codex server response: ${error.message}`));
|
|
return;
|
|
}
|
|
|
|
if (response.statusCode >= 400) {
|
|
reject(new Error(parsed?.error?.message || `Codex server request failed (${response.statusCode}).`));
|
|
return;
|
|
}
|
|
|
|
resolve(parsed);
|
|
});
|
|
});
|
|
|
|
request.on("error", (error) => {
|
|
reject(new Error(`Unable to reach the Codex server: ${error.message}`));
|
|
});
|
|
|
|
if (payload) {
|
|
request.write(payload);
|
|
}
|
|
|
|
request.end();
|
|
});
|
|
}
|
|
|
|
shouldTryAutoLaunch(error) {
|
|
const message = String(error?.message || error || "").toLowerCase();
|
|
return (
|
|
message.includes("codex server config was not found")
|
|
|| message.includes("unable to reach the codex server")
|
|
|| message.includes("econnrefused")
|
|
|| message.includes("connect enoent")
|
|
);
|
|
}
|
|
|
|
getHealth() {
|
|
return this.request("GET", "/health");
|
|
}
|
|
|
|
getHealthOnce(options = {}) {
|
|
if (options.reloadConfig === true) {
|
|
this.config = null;
|
|
}
|
|
return this.requestOnce("GET", "/health");
|
|
}
|
|
|
|
getTasks(options = {}) {
|
|
const vaultId = String(options.vaultId || "").trim();
|
|
const query = vaultId ? `?vaultId=${encodeURIComponent(vaultId)}` : "";
|
|
return this.request("GET", `/tasks${query}`);
|
|
}
|
|
|
|
getAutomations() {
|
|
return this.request("GET", "/automations");
|
|
}
|
|
|
|
syncAutomations(body = {}) {
|
|
return this.request("PUT", "/automations/sync", body);
|
|
}
|
|
|
|
runAutomation(automationId, body = {}) {
|
|
return this.request("POST", `/automations/${encodeURIComponent(automationId)}/run`, body);
|
|
}
|
|
|
|
getTask(taskId) {
|
|
return this.request("GET", `/tasks/${encodeURIComponent(taskId)}`);
|
|
}
|
|
|
|
createTaskFromCanvasSelection(selection) {
|
|
return this.request("POST", "/tasks/from-canvas-selection", selection);
|
|
}
|
|
|
|
runTask(taskId, body = {}) {
|
|
return this.request("POST", `/tasks/${encodeURIComponent(taskId)}/run`, body);
|
|
}
|
|
|
|
forkTask(taskId, body = {}) {
|
|
return this.request("POST", `/tasks/${encodeURIComponent(taskId)}/fork`, body);
|
|
}
|
|
|
|
sendMessage(taskId, text, runtimeConfig = null) {
|
|
return this.request("POST", `/tasks/${encodeURIComponent(taskId)}/messages`, {
|
|
text,
|
|
...(runtimeConfig ? { runtimeConfig } : {}),
|
|
});
|
|
}
|
|
|
|
updateTaskCanvasBinding(taskId, body = {}) {
|
|
return this.request("PATCH", `/tasks/${encodeURIComponent(taskId)}/canvas-binding`, body);
|
|
}
|
|
|
|
interruptTask(taskId) {
|
|
return this.request("POST", `/tasks/${encodeURIComponent(taskId)}/interrupt`, {});
|
|
}
|
|
|
|
startVoiceRealtime(body = {}) {
|
|
return this.request("POST", "/voice/realtime/start", body);
|
|
}
|
|
|
|
stopVoiceRealtime(sessionId) {
|
|
return this.request("POST", `/voice/realtime/${encodeURIComponent(sessionId)}/stop`, {});
|
|
}
|
|
|
|
openVoiceRealtimeStream(sessionId, handlers = {}) {
|
|
return this.openEventStream(`/voice/realtime/${encodeURIComponent(sessionId)}/stream`, handlers);
|
|
}
|
|
|
|
openEventStream(route, handlers = {}) {
|
|
return this.openTaskLikeStream(route, handlers);
|
|
}
|
|
|
|
openTaskStream(taskId, handlers = {}) {
|
|
return this.openTaskLikeStream(`/tasks/${encodeURIComponent(taskId)}/stream`, handlers);
|
|
}
|
|
|
|
openTaskLikeStream(route, handlers = {}) {
|
|
const config = this.loadConfig();
|
|
const normalizedHandlers = typeof handlers === "function"
|
|
? { onEvent: handlers }
|
|
: (handlers && typeof handlers === "object" ? handlers : {});
|
|
const onEvent = typeof normalizedHandlers.onEvent === "function"
|
|
? normalizedHandlers.onEvent
|
|
: () => {};
|
|
const onOpen = typeof normalizedHandlers.onOpen === "function"
|
|
? normalizedHandlers.onOpen
|
|
: () => {};
|
|
const onClose = typeof normalizedHandlers.onClose === "function"
|
|
? normalizedHandlers.onClose
|
|
: () => {};
|
|
const onError = typeof normalizedHandlers.onError === "function"
|
|
? normalizedHandlers.onError
|
|
: () => {};
|
|
const request = http.request({
|
|
host: config.host,
|
|
port: config.port,
|
|
path: route,
|
|
method: "GET",
|
|
headers: {
|
|
Accept: "text/event-stream",
|
|
"x-openagent-token": config.token,
|
|
},
|
|
});
|
|
|
|
let buffer = "";
|
|
let responseRef = null;
|
|
let isClosed = false;
|
|
const notifyClose = (reason, detail = null) => {
|
|
if (isClosed) {
|
|
return;
|
|
}
|
|
|
|
isClosed = true;
|
|
try {
|
|
onClose(reason, detail);
|
|
} catch {
|
|
// Ignore stream close callback failures.
|
|
}
|
|
};
|
|
|
|
request.on("response", (response) => {
|
|
responseRef = response;
|
|
if ((response.statusCode || 0) >= 400) {
|
|
let errorBuffer = "";
|
|
response.setEncoding("utf8");
|
|
response.on("data", (chunk) => {
|
|
errorBuffer += chunk;
|
|
});
|
|
response.on("end", () => {
|
|
let message = `Task stream failed (${response.statusCode}).`;
|
|
try {
|
|
const parsed = errorBuffer.trim() ? JSON.parse(errorBuffer) : {};
|
|
message = parsed?.error?.message || message;
|
|
} catch {
|
|
if (errorBuffer.trim()) {
|
|
message = errorBuffer.trim();
|
|
}
|
|
}
|
|
|
|
try {
|
|
onError(new Error(message));
|
|
} catch {
|
|
// Ignore stream error callback failures.
|
|
}
|
|
notifyClose("http-error", {
|
|
statusCode: response.statusCode,
|
|
message,
|
|
});
|
|
});
|
|
response.on("close", () => {
|
|
notifyClose("close");
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
onOpen();
|
|
} catch {
|
|
// Ignore stream open callback failures.
|
|
}
|
|
|
|
response.setEncoding("utf8");
|
|
response.on("data", (chunk) => {
|
|
buffer += chunk;
|
|
|
|
while (buffer.includes("\n\n")) {
|
|
const boundary = buffer.indexOf("\n\n");
|
|
const block = buffer.slice(0, boundary);
|
|
buffer = buffer.slice(boundary + 2);
|
|
|
|
const lines = block.split("\n");
|
|
let event = "message";
|
|
let data = "";
|
|
for (const line of lines) {
|
|
if (line.startsWith("event:")) {
|
|
event = line.slice(6).trim();
|
|
} else if (line.startsWith("data:")) {
|
|
data += line.slice(5).trim();
|
|
}
|
|
}
|
|
|
|
if (!data) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
onEvent(event, JSON.parse(data));
|
|
} catch {
|
|
// Ignore malformed stream frames.
|
|
}
|
|
}
|
|
});
|
|
response.on("end", () => {
|
|
notifyClose("end");
|
|
});
|
|
response.on("close", () => {
|
|
notifyClose("close");
|
|
});
|
|
response.on("error", (error) => {
|
|
try {
|
|
onError(error);
|
|
} catch {
|
|
// Ignore stream error callback failures.
|
|
}
|
|
notifyClose("response-error", {
|
|
error: String(error?.message || error),
|
|
});
|
|
});
|
|
});
|
|
|
|
request.on("error", (error) => {
|
|
try {
|
|
onError(error);
|
|
} catch {
|
|
// Ignore stream error callback failures.
|
|
}
|
|
notifyClose("request-error", {
|
|
error: String(error?.message || error),
|
|
});
|
|
});
|
|
request.end();
|
|
|
|
return () => {
|
|
isClosed = true;
|
|
if (responseRef && !responseRef.destroyed) {
|
|
responseRef.destroy();
|
|
}
|
|
request.destroy();
|
|
};
|
|
}
|
|
}
|
|
|
|
class CanvasSelectionResolver {
|
|
constructor(app, plugin = null) {
|
|
this.app = app;
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
async resolveActiveSelection() {
|
|
const view = this.getActiveCanvasView();
|
|
if (!view) {
|
|
throw new Error("Open a Canvas view first.");
|
|
}
|
|
|
|
const file = view.file || this.app.workspace.getActiveFile();
|
|
if (!(file instanceof TFile) || file.extension !== "canvas") {
|
|
throw new Error("The active view is not backed by a .canvas file.");
|
|
}
|
|
|
|
const selectedNodeIds = this.extractSelectedNodeIds(view);
|
|
if (selectedNodeIds.length === 0) {
|
|
throw new Error("Select one or more Canvas nodes first.");
|
|
}
|
|
|
|
const liveNodeDataById = this.extractLiveSelectedNodeDataById(view, selectedNodeIds);
|
|
await this.flushPendingCanvasEdits(view);
|
|
|
|
return this.resolveCanvasSelection(file, selectedNodeIds, { view, liveNodeDataById });
|
|
}
|
|
|
|
getActiveCanvasView() {
|
|
const activeLeaf = this.app.workspace.activeLeaf;
|
|
if (activeLeaf?.view?.getViewType?.() === "canvas") {
|
|
return activeLeaf.view;
|
|
}
|
|
|
|
const canvasLeaves = this.app.workspace.getLeavesOfType("canvas");
|
|
return canvasLeaves[0]?.view || null;
|
|
}
|
|
|
|
isEditableCanvasElement(element) {
|
|
if (!element || typeof element !== "object") {
|
|
return false;
|
|
}
|
|
|
|
if (typeof element.matches === "function" && element.matches("textarea, input, [contenteditable='true'], .cm-content")) {
|
|
return true;
|
|
}
|
|
|
|
return Boolean(element.closest?.("[contenteditable='true'], .cm-content"));
|
|
}
|
|
|
|
async requestCanvasSave(view) {
|
|
const calls = [
|
|
[view, "requestSave"],
|
|
[view, "save"],
|
|
[view?.canvas, "requestSave"],
|
|
[view?.canvas, "save"],
|
|
];
|
|
|
|
for (const [owner, method] of calls) {
|
|
if (typeof owner?.[method] !== "function") {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const result = owner[method]();
|
|
if (result && typeof result.then === "function") {
|
|
await result;
|
|
}
|
|
} catch {
|
|
// Canvas internals differ between Obsidian versions; best effort is enough here.
|
|
}
|
|
}
|
|
}
|
|
|
|
async flushPendingCanvasEdits(view) {
|
|
const containerEl = view?.containerEl;
|
|
const activeElement = containerEl?.ownerDocument?.activeElement;
|
|
const isEditingCanvas = Boolean(
|
|
activeElement
|
|
&& typeof containerEl?.contains === "function"
|
|
&& containerEl.contains(activeElement)
|
|
&& this.isEditableCanvasElement(activeElement)
|
|
);
|
|
|
|
if (isEditingCanvas && typeof activeElement.blur === "function") {
|
|
activeElement.blur();
|
|
await sleep(0);
|
|
}
|
|
|
|
await this.requestCanvasSave(view);
|
|
|
|
if (isEditingCanvas) {
|
|
await sleep(50);
|
|
await this.requestCanvasSave(view);
|
|
}
|
|
}
|
|
|
|
getCanvasViewForFile(fileOrPath) {
|
|
const normalizedPath = typeof fileOrPath === "string"
|
|
? String(fileOrPath || "").trim()
|
|
: String(fileOrPath?.path || "").trim();
|
|
if (!normalizedPath) {
|
|
return null;
|
|
}
|
|
|
|
const activeView = this.getActiveCanvasView();
|
|
if (String(activeView?.file?.path || "").trim() === normalizedPath) {
|
|
return activeView;
|
|
}
|
|
|
|
const canvasLeaves = this.app.workspace.getLeavesOfType("canvas");
|
|
for (const leaf of canvasLeaves) {
|
|
const view = leaf?.view;
|
|
if (String(view?.file?.path || "").trim() === normalizedPath) {
|
|
return view;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
getCanvasRuntimeData(view) {
|
|
const canvas = view?.canvas;
|
|
if (!canvas) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const runtimeData = typeof canvas.getData === "function" ? canvas.getData() : canvas.data;
|
|
return runtimeData && typeof runtimeData === "object" ? runtimeData : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async readCanvasFileData(file) {
|
|
const raw = await this.app.vault.cachedRead(file);
|
|
try {
|
|
return JSON.parse(raw);
|
|
} catch {
|
|
throw new Error(`Unable to parse canvas JSON for ${file.path}`);
|
|
}
|
|
}
|
|
|
|
getCachedCanvasNodeText(canvasPath, nodeId) {
|
|
if (!this.plugin || typeof this.plugin.getCanvasNodeEditorText !== "function") {
|
|
return undefined;
|
|
}
|
|
return this.plugin.getCanvasNodeEditorText(canvasPath, nodeId);
|
|
}
|
|
|
|
async resolveCanvasSelection(file, selectedNodeIds, options = {}) {
|
|
const resolvedView = String(options.view?.file?.path || "").trim() === String(file?.path || "").trim()
|
|
? options.view
|
|
: this.getCanvasViewForFile(file);
|
|
const liveSelectedNodeDataById = options.liveNodeDataById instanceof Map
|
|
? new Map(options.liveNodeDataById)
|
|
: new Map();
|
|
this.extractLiveSelectedNodeDataById(resolvedView, selectedNodeIds).forEach((nodeData, nodeId) => {
|
|
const previous = liveSelectedNodeDataById.get(nodeId) || {};
|
|
liveSelectedNodeDataById.set(nodeId, { ...previous, ...nodeData });
|
|
});
|
|
|
|
const selectedIdSet = new Set(selectedNodeIds.map((nodeId) => String(nodeId)));
|
|
const selectedNodesById = new Map();
|
|
const rememberSelectedNodes = (nodes) => {
|
|
(Array.isArray(nodes) ? nodes : []).forEach((node) => {
|
|
const nodeId = String(node?.id || "").trim();
|
|
if (!nodeId || !selectedIdSet.has(nodeId) || selectedNodesById.has(nodeId)) {
|
|
return;
|
|
}
|
|
selectedNodesById.set(nodeId, node);
|
|
});
|
|
};
|
|
|
|
const runtimeData = this.getCanvasRuntimeData(resolvedView);
|
|
rememberSelectedNodes(runtimeData?.nodes);
|
|
|
|
if (selectedNodesById.size < selectedIdSet.size) {
|
|
const parsed = await this.readCanvasFileData(file);
|
|
rememberSelectedNodes(parsed?.nodes);
|
|
}
|
|
|
|
const selectedNodes = Array.from(selectedNodesById.values())
|
|
.map((node) => {
|
|
const liveNodeData = liveSelectedNodeDataById.get(String(node?.id || ""));
|
|
const mergedNode = liveNodeData ? { ...node, ...liveNodeData } : { ...node };
|
|
const cachedText = this.getCachedCanvasNodeText(file?.path, mergedNode?.id);
|
|
if (cachedText !== undefined && String(mergedNode?.type || "") === "text") {
|
|
mergedNode.text = cachedText;
|
|
}
|
|
return mergedNode;
|
|
})
|
|
.sort(compareCanvasNodeOrder);
|
|
|
|
if (selectedNodes.length === 0) {
|
|
throw new Error("The Canvas does not contain the current selection.");
|
|
}
|
|
|
|
const textBlocks = [];
|
|
const markdownFiles = [];
|
|
const imageFiles = [];
|
|
const warnings = [];
|
|
|
|
for (const node of selectedNodes) {
|
|
const nodeId = String(node.id || "");
|
|
if (!nodeId) {
|
|
continue;
|
|
}
|
|
|
|
if (node.type === "text") {
|
|
const text = typeof node.text === "string" ? node.text.trim() : "";
|
|
if (text) {
|
|
textBlocks.push({ id: nodeId, text });
|
|
} else {
|
|
warnings.push(`Text node ${nodeId} is empty.`);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (node.type === "file") {
|
|
const selectionEntry = await this.buildCanvasFileSelectionEntry(node, warnings);
|
|
if (selectionEntry?.kind === "markdown") {
|
|
markdownFiles.push(selectionEntry.value);
|
|
} else if (selectionEntry?.kind === "image") {
|
|
imageFiles.push(selectionEntry.value);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
warnings.push(`Unsupported canvas node type skipped: ${node.type || "unknown"}`);
|
|
}
|
|
|
|
const normalizedNodeIds = selectedNodes
|
|
.map((node) => String(node.id))
|
|
.filter(Boolean)
|
|
.sort();
|
|
|
|
return {
|
|
canvasPath: file.path,
|
|
canvasName: file.basename,
|
|
nodeIds: normalizedNodeIds,
|
|
textBlocks,
|
|
markdownFiles,
|
|
imageFiles,
|
|
warnings,
|
|
title: this.deriveTitle(file.basename, textBlocks, markdownFiles, imageFiles),
|
|
};
|
|
}
|
|
|
|
getCanvasNodeCenter(node) {
|
|
return getCanvasNodeCenter(node);
|
|
}
|
|
|
|
doesCanvasGroupContainNode(groupNode, candidateNode) {
|
|
return doesCanvasGroupContainNode(groupNode, candidateNode);
|
|
}
|
|
|
|
findSmallestCanvasGroupForNode(nodes, candidateNode) {
|
|
if (!Array.isArray(nodes) || !candidateNode) {
|
|
return null;
|
|
}
|
|
|
|
const containingGroups = nodes
|
|
.filter((node) => this.doesCanvasGroupContainNode(node, candidateNode))
|
|
.sort((a, b) => (
|
|
(Math.max(0, toFiniteNumber(a?.width, 0)) * Math.max(0, toFiniteNumber(a?.height, 0)))
|
|
- (Math.max(0, toFiniteNumber(b?.width, 0)) * Math.max(0, toFiniteNumber(b?.height, 0)))
|
|
));
|
|
|
|
return containingGroups[0] || null;
|
|
}
|
|
|
|
async buildCanvasFileSelectionEntry(node, warnings) {
|
|
const filePath = typeof node?.file === "string" ? node.file.trim() : "";
|
|
const abstractFile = await this.waitForCanvasFile(filePath);
|
|
if (!(abstractFile instanceof TFile)) {
|
|
warnings.push(`Missing file node target: ${filePath}`);
|
|
return null;
|
|
}
|
|
|
|
const vaultBasePath = this.getVaultBasePath();
|
|
const baseEntry = {
|
|
id: String(node?.id || ""),
|
|
path: abstractFile.path,
|
|
absolutePath: vaultBasePath
|
|
? this.resolveVaultPath(vaultBasePath, abstractFile.path)
|
|
: "",
|
|
name: abstractFile.basename,
|
|
};
|
|
|
|
if (abstractFile.extension === "md") {
|
|
return {
|
|
kind: "markdown",
|
|
value: {
|
|
...baseEntry,
|
|
content: await this.app.vault.cachedRead(abstractFile),
|
|
},
|
|
};
|
|
}
|
|
|
|
if (this.isSupportedCanvasImageFile(abstractFile)) {
|
|
return {
|
|
kind: "image",
|
|
value: {
|
|
...baseEntry,
|
|
mimeType: this.getCanvasImageMimeType(abstractFile),
|
|
},
|
|
};
|
|
}
|
|
|
|
warnings.push(`Unsupported file node type skipped: ${filePath}`);
|
|
return null;
|
|
}
|
|
|
|
isSupportedCanvasImageFile(file) {
|
|
const extension = String(file?.extension || "").trim().toLowerCase();
|
|
return extension === "png"
|
|
|| extension === "jpg"
|
|
|| extension === "jpeg"
|
|
|| extension === "gif"
|
|
|| extension === "webp";
|
|
}
|
|
|
|
getCanvasImageMimeType(file) {
|
|
const extension = String(file?.extension || "").trim().toLowerCase();
|
|
if (extension === "jpg" || extension === "jpeg") {
|
|
return "image/jpeg";
|
|
}
|
|
if (extension === "gif") {
|
|
return "image/gif";
|
|
}
|
|
if (extension === "webp") {
|
|
return "image/webp";
|
|
}
|
|
return "image/png";
|
|
}
|
|
|
|
async waitForCanvasFile(filePath, options = {}) {
|
|
const normalizedPath = String(filePath || "").trim();
|
|
const timeoutMs = Math.max(0, toFiniteNumber(options.timeoutMs, 4_000));
|
|
const pollMs = Math.max(25, toFiniteNumber(options.pollMs, 100));
|
|
const deadline = Date.now() + timeoutMs;
|
|
|
|
while (true) {
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(normalizedPath);
|
|
if (!normalizedPath || abstractFile instanceof TFile) {
|
|
return abstractFile || null;
|
|
}
|
|
|
|
if (Date.now() >= deadline) {
|
|
return abstractFile || null;
|
|
}
|
|
|
|
await sleep(pollMs);
|
|
}
|
|
}
|
|
|
|
collectConnectedCanvasNodeIds(edges, sourceNodeId) {
|
|
const normalizedSourceNodeId = String(sourceNodeId || "").trim();
|
|
const connectedNodeIds = new Set();
|
|
if (!normalizedSourceNodeId || !Array.isArray(edges)) {
|
|
return connectedNodeIds;
|
|
}
|
|
|
|
edges.forEach((edge) => {
|
|
const fromNodeId = String(edge?.fromNode || "").trim();
|
|
const toNodeId = String(edge?.toNode || "").trim();
|
|
if (fromNodeId === normalizedSourceNodeId && toNodeId) {
|
|
connectedNodeIds.add(toNodeId);
|
|
}
|
|
if (toNodeId === normalizedSourceNodeId && fromNodeId) {
|
|
connectedNodeIds.add(fromNodeId);
|
|
}
|
|
});
|
|
|
|
return connectedNodeIds;
|
|
}
|
|
|
|
async appendImplicitCanvasFiles(nodes, warnings, collectedMarkdownFiles, collectedImageFiles, seenNodeIds, predicate) {
|
|
const candidateNodes = (Array.isArray(nodes) ? nodes : [])
|
|
.filter((node) => {
|
|
const nodeId = String(node?.id || "").trim();
|
|
return nodeId
|
|
&& !seenNodeIds.has(nodeId)
|
|
&& String(node?.type || "").trim() === "file"
|
|
&& predicate(node);
|
|
})
|
|
.sort(compareCanvasNodeOrder);
|
|
|
|
for (const node of candidateNodes) {
|
|
const selectionEntry = await this.buildCanvasFileSelectionEntry(node, warnings);
|
|
const fileEntry = selectionEntry?.value || null;
|
|
if (!fileEntry) {
|
|
continue;
|
|
}
|
|
|
|
const nodeId = String(fileEntry.id || "").trim();
|
|
if (!nodeId || seenNodeIds.has(nodeId)) {
|
|
continue;
|
|
}
|
|
|
|
seenNodeIds.add(nodeId);
|
|
if (selectionEntry.kind === "markdown") {
|
|
collectedMarkdownFiles.push(fileEntry);
|
|
} else if (selectionEntry.kind === "image") {
|
|
collectedImageFiles.push(fileEntry);
|
|
}
|
|
}
|
|
}
|
|
|
|
async addImplicitCanvasFileContext(selection) {
|
|
const canvasPath = String(selection?.canvasPath || "").trim();
|
|
const nodeIds = Array.isArray(selection?.nodeIds) ? selection.nodeIds.map((nodeId) => String(nodeId)).filter(Boolean) : [];
|
|
const textBlocks = Array.isArray(selection?.textBlocks) ? selection.textBlocks : [];
|
|
const markdownFiles = Array.isArray(selection?.markdownFiles) ? selection.markdownFiles : [];
|
|
const imageFiles = Array.isArray(selection?.imageFiles) ? selection.imageFiles : [];
|
|
if (!canvasPath || nodeIds.length !== 1 || textBlocks.length !== 1 || (markdownFiles.length > 0 || imageFiles.length > 0)) {
|
|
return selection;
|
|
}
|
|
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(canvasPath);
|
|
if (!(abstractFile instanceof TFile) || abstractFile.extension !== "canvas") {
|
|
return selection;
|
|
}
|
|
|
|
const resolvedView = this.getCanvasViewForFile(canvasPath);
|
|
let parsed = this.getCanvasRuntimeData(resolvedView);
|
|
if (!parsed || !Array.isArray(parsed?.nodes) || !Array.isArray(parsed?.edges)) {
|
|
try {
|
|
parsed = await this.readCanvasFileData(abstractFile);
|
|
} catch {
|
|
return selection;
|
|
}
|
|
}
|
|
|
|
const nodes = Array.isArray(parsed?.nodes) ? parsed.nodes : [];
|
|
const sourceNodeId = nodeIds[0];
|
|
const sourceNode = nodes.find((node) => String(node?.id || "") === sourceNodeId) || null;
|
|
if ((!sourceNode || String(sourceNode?.type || "") !== "text") && parsed !== null) {
|
|
try {
|
|
const diskParsed = await this.readCanvasFileData(abstractFile);
|
|
parsed = diskParsed;
|
|
} catch {
|
|
return selection;
|
|
}
|
|
}
|
|
|
|
const effectiveNodes = Array.isArray(parsed?.nodes) ? parsed.nodes : [];
|
|
const effectiveSourceNode = effectiveNodes.find((node) => String(node?.id || "") === sourceNodeId) || null;
|
|
if (!effectiveSourceNode || String(effectiveSourceNode?.type || "") !== "text") {
|
|
return selection;
|
|
}
|
|
|
|
const edges = Array.isArray(parsed?.edges) ? parsed.edges : [];
|
|
const implicitMarkdownFiles = [];
|
|
const implicitImageFiles = [];
|
|
const nextWarnings = Array.isArray(selection?.warnings) ? [...selection.warnings] : [];
|
|
const seenNodeIds = new Set([
|
|
...markdownFiles.map((file) => String(file?.id || "").trim()),
|
|
...imageFiles.map((file) => String(file?.id || "").trim()),
|
|
].filter(Boolean));
|
|
const connectedNodeIds = this.collectConnectedCanvasNodeIds(edges, sourceNodeId);
|
|
await this.appendImplicitCanvasFiles(
|
|
effectiveNodes,
|
|
nextWarnings,
|
|
implicitMarkdownFiles,
|
|
implicitImageFiles,
|
|
seenNodeIds,
|
|
(node) => connectedNodeIds.has(String(node?.id || "").trim())
|
|
);
|
|
|
|
const containingGroup = this.findSmallestCanvasGroupForNode(effectiveNodes, effectiveSourceNode);
|
|
if (containingGroup) {
|
|
await this.appendImplicitCanvasFiles(
|
|
effectiveNodes,
|
|
nextWarnings,
|
|
implicitMarkdownFiles,
|
|
implicitImageFiles,
|
|
seenNodeIds,
|
|
(node) => String(node?.id || "").trim() !== sourceNodeId
|
|
&& this.doesCanvasGroupContainNode(containingGroup, node)
|
|
);
|
|
}
|
|
|
|
if (implicitMarkdownFiles.length === 0 && implicitImageFiles.length === 0) {
|
|
return selection;
|
|
}
|
|
|
|
return {
|
|
...selection,
|
|
markdownFiles: implicitMarkdownFiles,
|
|
imageFiles: implicitImageFiles,
|
|
warnings: nextWarnings,
|
|
};
|
|
}
|
|
|
|
async addImplicitCanvasMarkdownContext(selection) {
|
|
return this.addImplicitCanvasFileContext(selection);
|
|
}
|
|
|
|
async addImplicitGroupMarkdownContext(selection) {
|
|
return this.addImplicitCanvasFileContext(selection);
|
|
}
|
|
|
|
deriveTitle(canvasName, textBlocks, markdownFiles, imageFiles = []) {
|
|
if (textBlocks.length > 0) {
|
|
const firstLine = textBlocks[0].text.split("\n")[0].trim();
|
|
if (firstLine) {
|
|
return firstLine.slice(0, 80);
|
|
}
|
|
}
|
|
|
|
if (markdownFiles.length > 0) {
|
|
return markdownFiles[0].name || markdownFiles[0].path;
|
|
}
|
|
|
|
if (imageFiles.length > 0) {
|
|
return imageFiles[0].name || imageFiles[0].path;
|
|
}
|
|
|
|
return `${canvasName} selection`;
|
|
}
|
|
|
|
normalizeCanvasNodeData(value) {
|
|
if (!value || typeof value !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const nodeId = String(value.id || "").trim();
|
|
if (!nodeId) {
|
|
return null;
|
|
}
|
|
|
|
const normalized = { id: nodeId };
|
|
const nodeType = String(value.type || "").trim();
|
|
if (nodeType) {
|
|
normalized.type = nodeType;
|
|
}
|
|
|
|
if (typeof value.text === "string") {
|
|
normalized.text = value.text;
|
|
}
|
|
|
|
if (typeof value.file === "string") {
|
|
normalized.file = value.file;
|
|
}
|
|
|
|
if (typeof value.subpath === "string") {
|
|
normalized.subpath = value.subpath;
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
extractEditableText(element) {
|
|
if (!element) {
|
|
return "";
|
|
}
|
|
|
|
if (typeof element.value === "string") {
|
|
return element.value;
|
|
}
|
|
|
|
const cmContent = element.matches?.(".cm-content")
|
|
? element
|
|
: element.querySelector?.(".cm-content");
|
|
if (cmContent) {
|
|
const lines = Array.from(cmContent.querySelectorAll?.(".cm-line") || []);
|
|
if (lines.length > 0) {
|
|
return lines.map((line) => line.textContent || "").join("\n");
|
|
}
|
|
return typeof cmContent.textContent === "string" ? cmContent.textContent : "";
|
|
}
|
|
|
|
return typeof element.textContent === "string" ? element.textContent : "";
|
|
}
|
|
|
|
rememberDomTextForNodeElement(element, selectedIdSet, textById) {
|
|
const nodeId = String(element?.getAttribute?.("data-node-id") || "").trim();
|
|
if (!nodeId || (selectedIdSet.size > 0 && !selectedIdSet.has(nodeId))) {
|
|
return;
|
|
}
|
|
|
|
const editor = element.querySelector?.("textarea, input, [contenteditable='true'], .cm-content");
|
|
if (!editor) {
|
|
return;
|
|
}
|
|
|
|
const liveText = this.extractEditableText(editor);
|
|
if (typeof liveText === "string") {
|
|
textById.set(nodeId, liveText);
|
|
}
|
|
}
|
|
|
|
extractSelectedDomTextById(view, selectedIdSet) {
|
|
const selectedElements = view?.containerEl?.querySelectorAll?.(
|
|
"[data-node-id].is-selected, .canvas-node.is-selected[data-node-id], .canvas-node[data-node-id][aria-selected='true']"
|
|
);
|
|
const textById = new Map();
|
|
selectedElements?.forEach((element) => this.rememberDomTextForNodeElement(element, selectedIdSet, textById));
|
|
|
|
const activeElement = view?.containerEl?.ownerDocument?.activeElement;
|
|
const activeNodeElement = activeElement?.closest?.("[data-node-id]");
|
|
if (activeNodeElement && view?.containerEl?.contains?.(activeNodeElement)) {
|
|
this.rememberDomTextForNodeElement(activeNodeElement, selectedIdSet, textById);
|
|
}
|
|
|
|
return textById;
|
|
}
|
|
|
|
extractLiveSelectedNodeDataById(view, selectedNodeIds) {
|
|
const selectedIdSet = new Set(
|
|
(Array.isArray(selectedNodeIds) ? selectedNodeIds : [])
|
|
.map((nodeId) => String(nodeId || "").trim())
|
|
.filter(Boolean)
|
|
);
|
|
const collected = new Map();
|
|
const visited = new WeakSet();
|
|
const canvas = view?.canvas;
|
|
|
|
const rememberNodeData = (candidate) => {
|
|
const normalized = this.normalizeCanvasNodeData(candidate);
|
|
if (!normalized || !selectedIdSet.has(normalized.id)) {
|
|
return;
|
|
}
|
|
|
|
const previous = collected.get(normalized.id) || {};
|
|
collected.set(normalized.id, { ...previous, ...normalized });
|
|
};
|
|
|
|
const ingest = (value) => {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
|
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
return;
|
|
}
|
|
|
|
if (typeof value.getData === "function") {
|
|
try {
|
|
rememberNodeData(value.getData());
|
|
} catch {
|
|
// Ignore runtime node wrappers that throw while being inspected.
|
|
}
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
value.forEach(ingest);
|
|
return;
|
|
}
|
|
|
|
if (value instanceof Set) {
|
|
value.forEach(ingest);
|
|
return;
|
|
}
|
|
|
|
if (value instanceof Map) {
|
|
value.forEach((mapValue) => {
|
|
rememberNodeData(mapValue);
|
|
ingest(mapValue);
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (typeof value !== "object") {
|
|
return;
|
|
}
|
|
|
|
if (visited.has(value)) {
|
|
return;
|
|
}
|
|
visited.add(value);
|
|
|
|
rememberNodeData(value);
|
|
rememberNodeData(value.data);
|
|
rememberNodeData(value.node);
|
|
rememberNodeData(value.node?.data);
|
|
rememberNodeData(value.item);
|
|
rememberNodeData(value.item?.data);
|
|
|
|
[
|
|
value.node,
|
|
value.data,
|
|
value.item,
|
|
value.items,
|
|
value.nodes,
|
|
value.values,
|
|
value.selection,
|
|
value.selectedNodes,
|
|
].forEach(ingest);
|
|
};
|
|
|
|
ingest(canvas?.selection);
|
|
ingest(canvas?.selectionManager?.selection);
|
|
ingest(canvas?.selectionManager?.selectedNodes);
|
|
ingest(canvas?.selectedNodes);
|
|
|
|
if (collected.size < selectedIdSet.size) {
|
|
ingest(canvas?.nodes);
|
|
}
|
|
|
|
const domTextById = this.extractSelectedDomTextById(view, selectedIdSet);
|
|
domTextById.forEach((text, nodeId) => {
|
|
const previous = collected.get(nodeId) || { id: nodeId, type: "text" };
|
|
collected.set(nodeId, {
|
|
...previous,
|
|
id: nodeId,
|
|
type: previous.type || "text",
|
|
text,
|
|
});
|
|
});
|
|
|
|
return collected;
|
|
}
|
|
|
|
extractSelectedNodeIds(view) {
|
|
const canvas = view?.canvas;
|
|
const selectedElements = view?.containerEl?.querySelectorAll?.(
|
|
"[data-node-id].is-selected, .canvas-node.is-selected[data-node-id], .canvas-node[data-node-id][aria-selected='true']"
|
|
);
|
|
const domSelectedNodeIds = new Set();
|
|
if (selectedElements) {
|
|
selectedElements.forEach((element) => {
|
|
const nodeId = element.getAttribute("data-node-id");
|
|
if (nodeId) {
|
|
domSelectedNodeIds.add(String(nodeId));
|
|
}
|
|
});
|
|
}
|
|
|
|
if (domSelectedNodeIds.size > 0) {
|
|
return Array.from(domSelectedNodeIds);
|
|
}
|
|
|
|
const collected = new Set();
|
|
const visited = new WeakSet();
|
|
const addNodeId = (value) => {
|
|
if (typeof value === "string" || typeof value === "number") {
|
|
collected.add(String(value));
|
|
}
|
|
};
|
|
|
|
const ingest = (value) => {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
|
|
if (typeof value === "string" || typeof value === "number") {
|
|
addNodeId(value);
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
value.forEach(ingest);
|
|
return;
|
|
}
|
|
|
|
if (value instanceof Set) {
|
|
value.forEach(ingest);
|
|
return;
|
|
}
|
|
|
|
if (value instanceof Map) {
|
|
value.forEach((mapValue, key) => {
|
|
ingest(mapValue);
|
|
addNodeId(key);
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (typeof value !== "object") {
|
|
return;
|
|
}
|
|
|
|
if (visited.has(value)) {
|
|
return;
|
|
}
|
|
visited.add(value);
|
|
|
|
addNodeId(value.id);
|
|
addNodeId(value.node?.id);
|
|
addNodeId(value.data?.id);
|
|
addNodeId(value.item?.id);
|
|
|
|
[
|
|
value.node,
|
|
value.data,
|
|
value.item,
|
|
value.items,
|
|
value.nodes,
|
|
value.values,
|
|
value.selection,
|
|
value.selectedNodes,
|
|
].forEach(ingest);
|
|
};
|
|
|
|
ingest(canvas?.selection);
|
|
ingest(canvas?.selectionManager?.selection);
|
|
ingest(canvas?.selectionManager?.selectedNodes);
|
|
ingest(canvas?.selectedNodes);
|
|
|
|
return Array.from(collected);
|
|
}
|
|
|
|
inferWorkingDirectory(selection) {
|
|
const vaultBasePath = this.getVaultBasePath();
|
|
if (!vaultBasePath) {
|
|
return "";
|
|
}
|
|
|
|
const candidatePaths = [
|
|
this.resolveVaultPath(vaultBasePath, selection.canvasPath),
|
|
...selection.markdownFiles.map((file) => this.resolveVaultPath(vaultBasePath, file.path)),
|
|
].filter(Boolean);
|
|
|
|
for (const candidatePath of candidatePaths) {
|
|
const repoRoot = this.findNearestGitRoot(candidatePath);
|
|
if (repoRoot) {
|
|
return repoRoot;
|
|
}
|
|
}
|
|
|
|
for (const candidatePath of candidatePaths) {
|
|
const existingDirectory = this.findNearestExistingDirectory(candidatePath);
|
|
if (existingDirectory) {
|
|
return existingDirectory;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
getVaultBasePath() {
|
|
const adapter = this.app.vault?.adapter;
|
|
if (typeof adapter?.getBasePath === "function") {
|
|
return adapter.getBasePath();
|
|
}
|
|
if (typeof adapter?.basePath === "string" && adapter.basePath.trim()) {
|
|
return adapter.basePath.trim();
|
|
}
|
|
if (typeof adapter?.path === "string" && adapter.path.trim()) {
|
|
return adapter.path.trim();
|
|
}
|
|
return "";
|
|
}
|
|
|
|
resolveVaultPath(vaultBasePath, relativePath) {
|
|
const trimmedPath = String(relativePath || "").trim();
|
|
if (!trimmedPath) {
|
|
return "";
|
|
}
|
|
|
|
return path.resolve(vaultBasePath, trimmedPath);
|
|
}
|
|
|
|
findNearestGitRoot(startPath) {
|
|
let currentPath = this.findNearestExistingDirectory(startPath);
|
|
while (currentPath) {
|
|
const gitPath = path.join(currentPath, ".git");
|
|
if (fs.existsSync(gitPath)) {
|
|
return currentPath;
|
|
}
|
|
|
|
const parentPath = path.dirname(currentPath);
|
|
if (parentPath === currentPath) {
|
|
return "";
|
|
}
|
|
currentPath = parentPath;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
findNearestExistingDirectory(startPath) {
|
|
let currentPath = String(startPath || "").trim();
|
|
if (!currentPath) {
|
|
return "";
|
|
}
|
|
|
|
if (fs.existsSync(currentPath)) {
|
|
const stats = fs.statSync(currentPath);
|
|
return stats.isDirectory() ? currentPath : path.dirname(currentPath);
|
|
}
|
|
|
|
currentPath = path.dirname(currentPath);
|
|
while (currentPath && currentPath !== path.dirname(currentPath)) {
|
|
if (fs.existsSync(currentPath) && fs.statSync(currentPath).isDirectory()) {
|
|
return currentPath;
|
|
}
|
|
currentPath = path.dirname(currentPath);
|
|
}
|
|
|
|
if (currentPath && fs.existsSync(currentPath) && fs.statSync(currentPath).isDirectory()) {
|
|
return currentPath;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
}
|
|
|
|
class OpenAgentVoiceGraphView extends ItemView {
|
|
constructor(leaf, plugin) {
|
|
super(leaf);
|
|
this.plugin = plugin;
|
|
this.canvas = null;
|
|
this.ctx = null;
|
|
this.statusEl = null;
|
|
this.volumeEl = null;
|
|
this.nodeCountEl = null;
|
|
this.dictationInputEl = null;
|
|
this.dictationSendButtonEl = null;
|
|
this.sensitivityInput = null;
|
|
this.nodes = [];
|
|
this.edges = [];
|
|
this.nodeByPath = new Map();
|
|
this.animationFrameId = 0;
|
|
this.resizeObserver = null;
|
|
this.audioContext = null;
|
|
this.audioAnalyser = null;
|
|
this.audioData = null;
|
|
this.mediaStream = null;
|
|
this.audioLevel = 0;
|
|
this.frequencyTilt = 0;
|
|
this.isMicActive = false;
|
|
this.realtimePeerConnection = null;
|
|
this.realtimeDataChannel = null;
|
|
this.realtimeRemoteAudioEl = null;
|
|
this.realtimeSessionId = "";
|
|
this.realtimeStreamDisposer = null;
|
|
this.isDictating = false;
|
|
this.dictationTranscript = "";
|
|
this.dictationIsSending = false;
|
|
this.sensitivity = 1.35;
|
|
this.hoveredNode = null;
|
|
this.draggingNode = null;
|
|
this.isPanning = false;
|
|
this.panX = 0;
|
|
this.panY = 0;
|
|
this.zoom = 1;
|
|
this.pointerStart = null;
|
|
this.lastFrameAt = 0;
|
|
}
|
|
|
|
getViewType() {
|
|
return VOICE_GRAPH_VIEW_TYPE;
|
|
}
|
|
|
|
getDisplayText() {
|
|
return "Voice Graph";
|
|
}
|
|
|
|
getIcon() {
|
|
return "audio-lines";
|
|
}
|
|
|
|
async onOpen() {
|
|
this.render();
|
|
this.rebuildGraph();
|
|
this.startAnimation();
|
|
this.registerEvent(this.app.metadataCache.on("changed", () => this.scheduleGraphRefresh()));
|
|
this.registerEvent(this.app.metadataCache.on("resolved", () => this.scheduleGraphRefresh()));
|
|
}
|
|
|
|
async onClose() {
|
|
window.clearTimeout(this.graphRefreshTimer);
|
|
this.graphRefreshTimer = null;
|
|
this.stopAnimation();
|
|
this.stopDictation();
|
|
this.stopMic();
|
|
this.resizeObserver?.disconnect();
|
|
this.resizeObserver = null;
|
|
this.contentEl.empty();
|
|
}
|
|
|
|
render() {
|
|
this.contentEl.empty();
|
|
this.contentEl.addClass("oa-voice-graph-view");
|
|
|
|
const toolbar = this.contentEl.createDiv({ cls: "oa-voice-graph-toolbar" });
|
|
const title = toolbar.createDiv({ cls: "oa-voice-graph-title" });
|
|
title.createEl("span", { text: "Voice Graph" });
|
|
this.statusEl = title.createEl("small", { text: "Ambient mode" });
|
|
|
|
const controls = toolbar.createDiv({ cls: "oa-voice-graph-controls" });
|
|
const micButton = controls.createEl("button", {
|
|
cls: "clickable-icon oa-voice-graph-icon-button",
|
|
attr: { "aria-label": "Start microphone" },
|
|
});
|
|
setIcon(micButton, "mic");
|
|
setTooltip(micButton, "Animate graph from microphone");
|
|
micButton.addEventListener("click", () => {
|
|
void this.startMic();
|
|
});
|
|
|
|
const stopButton = controls.createEl("button", {
|
|
cls: "clickable-icon oa-voice-graph-icon-button",
|
|
attr: { "aria-label": "Stop microphone" },
|
|
});
|
|
setIcon(stopButton, "mic-off");
|
|
setTooltip(stopButton, "Stop microphone animation");
|
|
stopButton.addEventListener("click", () => this.stopMic());
|
|
|
|
const refreshButton = controls.createEl("button", {
|
|
cls: "clickable-icon oa-voice-graph-icon-button",
|
|
attr: { "aria-label": "Refresh graph" },
|
|
});
|
|
setIcon(refreshButton, "refresh-cw");
|
|
setTooltip(refreshButton, "Refresh graph");
|
|
refreshButton.addEventListener("click", () => this.rebuildGraph());
|
|
|
|
const centerButton = controls.createEl("button", {
|
|
cls: "clickable-icon oa-voice-graph-icon-button",
|
|
attr: { "aria-label": "Center graph" },
|
|
});
|
|
setIcon(centerButton, "scan");
|
|
setTooltip(centerButton, "Center graph");
|
|
centerButton.addEventListener("click", () => this.centerGraph());
|
|
|
|
const dictateButton = controls.createEl("button", {
|
|
cls: "clickable-icon oa-voice-graph-icon-button",
|
|
attr: { "aria-label": "Start dictation" },
|
|
});
|
|
setIcon(dictateButton, "message-circle");
|
|
setTooltip(dictateButton, "Dictate a message");
|
|
dictateButton.addEventListener("click", () => this.toggleDictation());
|
|
|
|
const sensitivityWrap = controls.createDiv({ cls: "oa-voice-graph-slider" });
|
|
sensitivityWrap.createEl("span", { text: "Sensitivity" });
|
|
this.sensitivityInput = sensitivityWrap.createEl("input", {
|
|
attr: {
|
|
type: "range",
|
|
min: "0.5",
|
|
max: "3",
|
|
step: "0.05",
|
|
value: String(this.sensitivity),
|
|
},
|
|
});
|
|
this.sensitivityInput.addEventListener("input", () => {
|
|
this.sensitivity = Number(this.sensitivityInput?.value || 1.35);
|
|
});
|
|
|
|
const stage = this.contentEl.createDiv({ cls: "oa-voice-graph-stage" });
|
|
this.canvas = stage.createEl("canvas", { cls: "oa-voice-graph-canvas" });
|
|
this.ctx = this.canvas.getContext("2d");
|
|
const hud = stage.createDiv({ cls: "oa-voice-graph-hud" });
|
|
this.nodeCountEl = hud.createEl("span", { text: "0 notes" });
|
|
this.volumeEl = hud.createEl("span", { text: "signal 0%" });
|
|
const dictationDock = stage.createDiv({ cls: "oa-voice-graph-dictation" });
|
|
this.dictationInputEl = dictationDock.createEl("textarea", {
|
|
attr: {
|
|
rows: "2",
|
|
placeholder: "Click the dictation button, or type here...",
|
|
},
|
|
});
|
|
this.dictationInputEl.addEventListener("input", () => {
|
|
this.dictationTranscript = String(this.dictationInputEl?.value || "");
|
|
this.updateDictationSendState();
|
|
});
|
|
this.dictationSendButtonEl = dictationDock.createEl("button", { text: "Send" });
|
|
this.dictationSendButtonEl.addEventListener("click", () => {
|
|
void this.sendDictation();
|
|
});
|
|
this.updateDictationSendState();
|
|
|
|
this.registerCanvasEvents();
|
|
this.resizeObserver = new ResizeObserver(() => this.resizeCanvas());
|
|
this.resizeObserver.observe(stage);
|
|
this.resizeCanvas();
|
|
}
|
|
|
|
registerCanvasEvents() {
|
|
this.canvas.addEventListener("pointerdown", (event) => {
|
|
const point = this.getGraphPoint(event);
|
|
const node = this.findNodeAt(point.x, point.y);
|
|
this.pointerStart = { clientX: event.clientX, clientY: event.clientY, panX: this.panX, panY: this.panY };
|
|
if (node) {
|
|
this.draggingNode = node;
|
|
node.fixed = true;
|
|
} else {
|
|
this.isPanning = true;
|
|
}
|
|
this.canvas.setPointerCapture(event.pointerId);
|
|
});
|
|
this.canvas.addEventListener("pointermove", (event) => {
|
|
const point = this.getGraphPoint(event);
|
|
this.hoveredNode = this.findNodeAt(point.x, point.y);
|
|
this.canvas.style.cursor = this.draggingNode ? "grabbing" : (this.hoveredNode ? "pointer" : (this.isPanning ? "grabbing" : "grab"));
|
|
if (this.draggingNode) {
|
|
this.draggingNode.x = point.x;
|
|
this.draggingNode.y = point.y;
|
|
this.draggingNode.vx = 0;
|
|
this.draggingNode.vy = 0;
|
|
} else if (this.isPanning && this.pointerStart) {
|
|
this.panX = this.pointerStart.panX + event.clientX - this.pointerStart.clientX;
|
|
this.panY = this.pointerStart.panY + event.clientY - this.pointerStart.clientY;
|
|
}
|
|
});
|
|
this.canvas.addEventListener("pointerup", (event) => this.finishPointer(event));
|
|
this.canvas.addEventListener("pointercancel", (event) => this.finishPointer(event));
|
|
this.canvas.addEventListener("dblclick", (event) => {
|
|
const point = this.getGraphPoint(event);
|
|
const node = this.findNodeAt(point.x, point.y);
|
|
if (node) {
|
|
void this.app.workspace.openLinkText(node.path, "", false);
|
|
}
|
|
});
|
|
this.canvas.addEventListener("wheel", (event) => {
|
|
event.preventDefault();
|
|
const before = this.getGraphPoint(event);
|
|
const factor = event.deltaY > 0 ? 0.92 : 1.08;
|
|
this.zoom = Math.max(0.35, Math.min(2.8, this.zoom * factor));
|
|
const after = this.getGraphPoint(event);
|
|
this.panX += (after.x - before.x) * this.zoom;
|
|
this.panY += (after.y - before.y) * this.zoom;
|
|
}, { passive: false });
|
|
}
|
|
|
|
finishPointer(event) {
|
|
if (this.draggingNode) {
|
|
this.draggingNode.fixed = false;
|
|
}
|
|
this.draggingNode = null;
|
|
this.isPanning = false;
|
|
this.pointerStart = null;
|
|
try {
|
|
this.canvas.releasePointerCapture(event.pointerId);
|
|
} catch {
|
|
// Pointer capture can already be gone if Obsidian moved focus.
|
|
}
|
|
}
|
|
|
|
resizeCanvas() {
|
|
if (!this.canvas) {
|
|
return;
|
|
}
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
const ratio = window.devicePixelRatio || 1;
|
|
const width = Math.max(1, Math.floor(rect.width * ratio));
|
|
const height = Math.max(1, Math.floor(rect.height * ratio));
|
|
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
this.canvas.width = width;
|
|
this.canvas.height = height;
|
|
this.centerGraph({ keepZoom: true });
|
|
}
|
|
}
|
|
|
|
scheduleGraphRefresh() {
|
|
window.clearTimeout(this.graphRefreshTimer);
|
|
this.graphRefreshTimer = window.setTimeout(() => this.rebuildGraph(), 300);
|
|
}
|
|
|
|
rebuildGraph() {
|
|
const files = this.app.vault.getMarkdownFiles();
|
|
const fileByPath = new Map(files.map((file) => [file.path, file]));
|
|
const degreeByPath = new Map(files.map((file) => [file.path, 0]));
|
|
const rawEdges = [];
|
|
const resolvedLinks = this.app.metadataCache.resolvedLinks || {};
|
|
Object.entries(resolvedLinks).forEach(([sourcePath, links]) => {
|
|
if (!fileByPath.has(sourcePath)) {
|
|
return;
|
|
}
|
|
Object.entries(links || {}).forEach(([targetPath, count]) => {
|
|
if (!fileByPath.has(targetPath) || sourcePath === targetPath) {
|
|
return;
|
|
}
|
|
const weight = Math.max(1, Number(count || 1));
|
|
rawEdges.push({ sourcePath, targetPath, weight });
|
|
degreeByPath.set(sourcePath, (degreeByPath.get(sourcePath) || 0) + weight);
|
|
degreeByPath.set(targetPath, (degreeByPath.get(targetPath) || 0) + weight);
|
|
});
|
|
});
|
|
|
|
const activePath = this.app.workspace.getActiveFile()?.path || "";
|
|
const selectedPaths = files
|
|
.map((file) => ({
|
|
file,
|
|
score: (degreeByPath.get(file.path) || 0) + (file.path === activePath ? 1_000 : 0),
|
|
}))
|
|
.sort((a, b) => b.score - a.score || a.file.path.localeCompare(b.file.path))
|
|
.slice(0, VOICE_GRAPH_MAX_NODES)
|
|
.map((entry) => entry.file.path);
|
|
const selectedSet = new Set(selectedPaths);
|
|
const previousByPath = this.nodeByPath;
|
|
this.nodeByPath = new Map();
|
|
this.nodes = selectedPaths.map((filePath, index) => {
|
|
const file = fileByPath.get(filePath);
|
|
const previous = previousByPath.get(filePath);
|
|
const angle = (Math.PI * 2 * index) / Math.max(1, selectedPaths.length);
|
|
const radius = 170 + 7 * Math.sqrt(index);
|
|
const degree = degreeByPath.get(filePath) || 0;
|
|
const node = {
|
|
path: filePath,
|
|
label: file?.basename || path.basename(filePath, ".md"),
|
|
x: previous?.x ?? Math.cos(angle) * radius,
|
|
y: previous?.y ?? Math.sin(angle) * radius,
|
|
vx: previous?.vx ?? 0,
|
|
vy: previous?.vy ?? 0,
|
|
radius: Math.min(VOICE_GRAPH_MAX_NODE_RADIUS, VOICE_GRAPH_MIN_NODE_RADIUS + Math.sqrt(degree) * 1.7),
|
|
pulse: Math.random() * Math.PI * 2,
|
|
degree,
|
|
fixed: false,
|
|
};
|
|
this.nodeByPath.set(filePath, node);
|
|
return node;
|
|
});
|
|
this.edges = rawEdges
|
|
.filter((edge) => selectedSet.has(edge.sourcePath) && selectedSet.has(edge.targetPath))
|
|
.slice(0, VOICE_GRAPH_MAX_EDGES)
|
|
.map((edge) => ({
|
|
source: this.nodeByPath.get(edge.sourcePath),
|
|
target: this.nodeByPath.get(edge.targetPath),
|
|
weight: edge.weight,
|
|
}))
|
|
.filter((edge) => edge.source && edge.target);
|
|
this.nodeCountEl?.setText(`${this.nodes.length} notes, ${this.edges.length} links`);
|
|
this.centerGraph({ keepZoom: true });
|
|
}
|
|
|
|
centerGraph(options = {}) {
|
|
const rect = this.canvas?.getBoundingClientRect();
|
|
if (!rect) {
|
|
return;
|
|
}
|
|
this.panX = rect.width / 2;
|
|
this.panY = rect.height / 2;
|
|
if (!options.keepZoom) {
|
|
this.zoom = 1;
|
|
}
|
|
}
|
|
|
|
async startMic() {
|
|
if (this.isMicActive) {
|
|
return this.mediaStream;
|
|
}
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
|
const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
|
|
this.audioContext = new AudioContextCtor();
|
|
const source = this.audioContext.createMediaStreamSource(stream);
|
|
this.audioAnalyser = this.audioContext.createAnalyser();
|
|
this.audioAnalyser.fftSize = 512;
|
|
this.audioAnalyser.smoothingTimeConstant = 0.72;
|
|
this.audioData = new Uint8Array(this.audioAnalyser.frequencyBinCount);
|
|
source.connect(this.audioAnalyser);
|
|
this.mediaStream = stream;
|
|
this.isMicActive = true;
|
|
this.statusEl?.setText("Listening");
|
|
return stream;
|
|
} catch (error) {
|
|
this.statusEl?.setText("Mic unavailable");
|
|
new Notice(`Voice Graph could not start the microphone: ${error.message || error}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
stopMic() {
|
|
if (this.isDictating) {
|
|
this.stopDictation();
|
|
}
|
|
this.mediaStream?.getTracks?.().forEach((track) => track.stop());
|
|
this.mediaStream = null;
|
|
this.audioContext?.close?.().catch(() => {});
|
|
this.audioContext = null;
|
|
this.audioAnalyser = null;
|
|
this.audioData = null;
|
|
this.isMicActive = false;
|
|
this.audioLevel = 0;
|
|
this.frequencyTilt = 0;
|
|
this.statusEl?.setText("Ambient mode");
|
|
}
|
|
|
|
toggleDictation() {
|
|
if (this.isDictating) {
|
|
this.stopDictation();
|
|
return;
|
|
}
|
|
this.startDictation();
|
|
}
|
|
|
|
async startDictation() {
|
|
if (this.isDictating) {
|
|
return;
|
|
}
|
|
this.stopDictation({ stopServer: false });
|
|
this.isDictating = true;
|
|
this.statusEl?.setText("Starting voice mode");
|
|
this.updateDictationSendState();
|
|
|
|
try {
|
|
await this.startMic();
|
|
const offerSdp = await this.createRealtimeWebRtcOffer();
|
|
const context = this.plugin.getVaultPetQuickChatContext();
|
|
const response = await this.plugin.api.startVoiceRealtime({
|
|
cwd: context.cwd || this.plugin.getVaultBasePath(),
|
|
outputModality: "text",
|
|
offerSdp,
|
|
prompt: [
|
|
"You are OpenAgent Voice Graph inside Obsidian.",
|
|
"Transcribe the user's speech accurately, including Vietnamese.",
|
|
"Keep assistant responses concise. The UI primarily uses the user transcript as a prompt.",
|
|
].join(" "),
|
|
runtimeConfig: this.plugin.buildRuntimeConfigPayload(),
|
|
});
|
|
this.realtimeSessionId = String(response.sessionId || "");
|
|
if (!this.realtimeSessionId) {
|
|
throw new Error("OpenAgent daemon did not return a realtime session id.");
|
|
}
|
|
const answerSdp = String(response.answerSdp || "").trim();
|
|
if (!answerSdp) {
|
|
throw new Error("OpenAgent daemon did not return a realtime SDP answer.");
|
|
}
|
|
await this.applyRealtimeWebRtcAnswer(answerSdp);
|
|
this.openRealtimeTranscriptStream(this.realtimeSessionId);
|
|
this.statusEl?.setText("Voice mode");
|
|
} catch (error) {
|
|
this.stopDictation({ stopServer: true });
|
|
new Notice(`Voice Graph could not start Codex voice mode: ${error.message || error}`);
|
|
}
|
|
}
|
|
|
|
async createRealtimeWebRtcOffer() {
|
|
const PeerConnectionCtor = window.RTCPeerConnection || window.webkitRTCPeerConnection;
|
|
if (!PeerConnectionCtor) {
|
|
throw new Error("WebRTC is not available in this Obsidian runtime.");
|
|
}
|
|
if (!this.mediaStream) {
|
|
throw new Error("Microphone stream is not active.");
|
|
}
|
|
const audioTracks = this.mediaStream.getAudioTracks?.() || [];
|
|
if (!audioTracks.length) {
|
|
throw new Error("Microphone stream does not contain an audio track.");
|
|
}
|
|
|
|
this.stopRealtimePeerConnection();
|
|
const peerConnection = new PeerConnectionCtor({ iceServers: [] });
|
|
this.realtimePeerConnection = peerConnection;
|
|
this.realtimeDataChannel = peerConnection.createDataChannel("oai-events", { ordered: true });
|
|
|
|
peerConnection.onconnectionstatechange = () => {
|
|
const state = String(peerConnection.connectionState || "");
|
|
if (state === "connected") {
|
|
this.statusEl?.setText("Voice mode");
|
|
} else if (state === "failed" || state === "disconnected") {
|
|
this.statusEl?.setText("Voice connection interrupted");
|
|
}
|
|
};
|
|
peerConnection.ontrack = (event) => {
|
|
const stream = event.streams?.[0];
|
|
if (!stream) {
|
|
return;
|
|
}
|
|
if (!this.realtimeRemoteAudioEl) {
|
|
this.realtimeRemoteAudioEl = document.createElement("audio");
|
|
this.realtimeRemoteAudioEl.autoplay = true;
|
|
this.realtimeRemoteAudioEl.hidden = true;
|
|
this.contentEl.appendChild(this.realtimeRemoteAudioEl);
|
|
}
|
|
this.realtimeRemoteAudioEl.srcObject = stream;
|
|
void this.realtimeRemoteAudioEl.play?.().catch(() => {});
|
|
};
|
|
|
|
for (const track of audioTracks) {
|
|
peerConnection.addTransceiver(track, { direction: "sendrecv" });
|
|
}
|
|
|
|
const offer = await peerConnection.createOffer({
|
|
offerToReceiveAudio: true,
|
|
offerToReceiveVideo: false,
|
|
});
|
|
await peerConnection.setLocalDescription(offer);
|
|
await this.waitForRealtimeIceGathering(peerConnection, 5_000);
|
|
const sdp = String(peerConnection.localDescription?.sdp || "").trim();
|
|
if (!sdp) {
|
|
throw new Error("WebRTC did not produce a local SDP offer.");
|
|
}
|
|
return sdp;
|
|
}
|
|
|
|
waitForRealtimeIceGathering(peerConnection, timeoutMs) {
|
|
if (peerConnection.iceGatheringState === "complete") {
|
|
return Promise.resolve();
|
|
}
|
|
return new Promise((resolve) => {
|
|
let settled = false;
|
|
const finish = () => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
window.clearTimeout(timer);
|
|
peerConnection.removeEventListener("icegatheringstatechange", onIceGatheringStateChange);
|
|
resolve();
|
|
};
|
|
const onIceGatheringStateChange = () => {
|
|
if (peerConnection.iceGatheringState === "complete") {
|
|
finish();
|
|
}
|
|
};
|
|
const timer = window.setTimeout(finish, timeoutMs);
|
|
peerConnection.addEventListener("icegatheringstatechange", onIceGatheringStateChange);
|
|
});
|
|
}
|
|
|
|
async applyRealtimeWebRtcAnswer(answerSdp) {
|
|
if (!this.realtimePeerConnection) {
|
|
throw new Error("WebRTC peer connection is not active.");
|
|
}
|
|
await this.realtimePeerConnection.setRemoteDescription({
|
|
type: "answer",
|
|
sdp: answerSdp,
|
|
});
|
|
}
|
|
|
|
openRealtimeTranscriptStream(sessionId) {
|
|
this.realtimeStreamDisposer?.();
|
|
this.realtimeStreamDisposer = this.plugin.api.openVoiceRealtimeStream(sessionId, {
|
|
onEvent: (event, payload) => {
|
|
if (event === "transcript.delta" || event === "transcript.done" || event === "ready") {
|
|
const transcript = String(payload?.transcript || payload?.text || "").trim();
|
|
if (transcript) {
|
|
this.dictationTranscript = transcript;
|
|
if (this.dictationInputEl) {
|
|
this.dictationInputEl.value = transcript;
|
|
}
|
|
this.updateDictationSendState();
|
|
}
|
|
}
|
|
if (event === "error") {
|
|
new Notice(String(payload?.message || "Codex voice mode failed."));
|
|
}
|
|
if (event === "closed") {
|
|
this.isDictating = false;
|
|
if (!this.isMicActive) {
|
|
this.statusEl?.setText("Ambient mode");
|
|
}
|
|
}
|
|
},
|
|
onError: (error) => {
|
|
new Notice(`Voice transcript stream failed: ${error.message || error}`);
|
|
},
|
|
});
|
|
}
|
|
|
|
stopDictation(options = {}) {
|
|
const stopServer = options.stopServer !== false;
|
|
const sessionId = this.realtimeSessionId;
|
|
this.realtimeStreamDisposer?.();
|
|
this.realtimeStreamDisposer = null;
|
|
this.realtimeSessionId = "";
|
|
this.stopRealtimePeerConnection();
|
|
if (stopServer && sessionId) {
|
|
void this.plugin.api.stopVoiceRealtime(sessionId).catch(() => {});
|
|
}
|
|
this.isDictating = false;
|
|
if (!this.isMicActive) {
|
|
this.statusEl?.setText("Ambient mode");
|
|
}
|
|
}
|
|
|
|
stopRealtimePeerConnection() {
|
|
if (this.realtimeDataChannel) {
|
|
try {
|
|
this.realtimeDataChannel.close();
|
|
} catch {
|
|
// Ignore peer cleanup failures.
|
|
}
|
|
}
|
|
if (this.realtimePeerConnection) {
|
|
try {
|
|
this.realtimePeerConnection.getSenders?.().forEach((sender) => {
|
|
try {
|
|
this.realtimePeerConnection.removeTrack(sender);
|
|
} catch {
|
|
// Ignore senders already removed by the runtime.
|
|
}
|
|
});
|
|
this.realtimePeerConnection.close();
|
|
} catch {
|
|
// Ignore peer cleanup failures.
|
|
}
|
|
}
|
|
if (this.realtimeRemoteAudioEl) {
|
|
try {
|
|
this.realtimeRemoteAudioEl.pause();
|
|
this.realtimeRemoteAudioEl.srcObject = null;
|
|
this.realtimeRemoteAudioEl.remove();
|
|
} catch {
|
|
// Ignore audio element cleanup failures.
|
|
}
|
|
}
|
|
this.realtimeDataChannel = null;
|
|
this.realtimePeerConnection = null;
|
|
this.realtimeRemoteAudioEl = null;
|
|
}
|
|
|
|
updateDictationSendState() {
|
|
if (!this.dictationSendButtonEl) {
|
|
return;
|
|
}
|
|
this.dictationSendButtonEl.disabled = this.dictationIsSending || !String(this.dictationTranscript || "").trim();
|
|
this.dictationSendButtonEl.textContent = this.dictationIsSending ? "Sending..." : "Send";
|
|
}
|
|
|
|
async sendDictation() {
|
|
const message = String(this.dictationTranscript || this.dictationInputEl?.value || "").trim();
|
|
if (!message) {
|
|
new Notice("Dictate or type a message first.");
|
|
return;
|
|
}
|
|
this.stopDictation();
|
|
this.dictationIsSending = true;
|
|
this.updateDictationSendState();
|
|
try {
|
|
const context = this.plugin.getVaultPetQuickChatContext({ preferVaultCanvas: true });
|
|
await this.plugin.startVaultPetChat(context, message);
|
|
this.dictationTranscript = "";
|
|
if (this.dictationInputEl) {
|
|
this.dictationInputEl.value = "";
|
|
}
|
|
this.updateDictationSendState();
|
|
} catch (error) {
|
|
new Notice(String(error?.message || error));
|
|
} finally {
|
|
this.dictationIsSending = false;
|
|
this.updateDictationSendState();
|
|
}
|
|
}
|
|
|
|
startAnimation() {
|
|
if (this.animationFrameId) {
|
|
return;
|
|
}
|
|
const frame = (timestamp) => {
|
|
const dt = Math.min(0.05, Math.max(0.001, (timestamp - (this.lastFrameAt || timestamp)) / 1000));
|
|
this.lastFrameAt = timestamp;
|
|
this.updateAudio();
|
|
this.stepPhysics(dt, timestamp / 1000);
|
|
this.draw(timestamp / 1000);
|
|
this.animationFrameId = window.requestAnimationFrame(frame);
|
|
};
|
|
this.animationFrameId = window.requestAnimationFrame(frame);
|
|
}
|
|
|
|
stopAnimation() {
|
|
if (this.animationFrameId) {
|
|
window.cancelAnimationFrame(this.animationFrameId);
|
|
this.animationFrameId = 0;
|
|
}
|
|
}
|
|
|
|
updateAudio() {
|
|
if (!this.audioAnalyser || !this.audioData) {
|
|
this.audioLevel *= 0.92;
|
|
this.frequencyTilt *= 0.9;
|
|
return;
|
|
}
|
|
this.audioAnalyser.getByteFrequencyData(this.audioData);
|
|
let sum = 0;
|
|
let high = 0;
|
|
for (let index = 0; index < this.audioData.length; index += 1) {
|
|
const value = this.audioData[index] / 255;
|
|
sum += value * value;
|
|
if (index > this.audioData.length * 0.45) {
|
|
high += value;
|
|
}
|
|
}
|
|
const rms = Math.sqrt(sum / Math.max(1, this.audioData.length));
|
|
const highAverage = high / Math.max(1, this.audioData.length * 0.55);
|
|
this.audioLevel = this.audioLevel * 0.72 + rms * 0.28;
|
|
this.frequencyTilt = this.frequencyTilt * 0.78 + highAverage * 0.22;
|
|
this.volumeEl?.setText(`signal ${Math.round(Math.min(1, this.audioLevel * 3) * 100)}%`);
|
|
}
|
|
|
|
stepPhysics(dt, time) {
|
|
const signal = Math.min(2.2, (this.audioLevel * 5.5 + 0.08) * this.sensitivity);
|
|
const repel = 4800 + signal * 8800;
|
|
const linkDistance = 70 + signal * 44;
|
|
for (let i = 0; i < this.nodes.length; i += 1) {
|
|
const a = this.nodes[i];
|
|
for (let j = i + 1; j < this.nodes.length; j += 1) {
|
|
const b = this.nodes[j];
|
|
const dx = b.x - a.x;
|
|
const dy = b.y - a.y;
|
|
const distanceSq = Math.max(80, dx * dx + dy * dy);
|
|
const force = repel / distanceSq;
|
|
const distance = Math.sqrt(distanceSq);
|
|
const fx = (dx / distance) * force;
|
|
const fy = (dy / distance) * force;
|
|
if (!a.fixed) {
|
|
a.vx -= fx * dt;
|
|
a.vy -= fy * dt;
|
|
}
|
|
if (!b.fixed) {
|
|
b.vx += fx * dt;
|
|
b.vy += fy * dt;
|
|
}
|
|
}
|
|
}
|
|
this.edges.forEach((edge) => {
|
|
const dx = edge.target.x - edge.source.x;
|
|
const dy = edge.target.y - edge.source.y;
|
|
const distance = Math.max(1, Math.sqrt(dx * dx + dy * dy));
|
|
const desired = linkDistance + Math.min(45, edge.weight * 5);
|
|
const force = (distance - desired) * 0.018;
|
|
const fx = (dx / distance) * force;
|
|
const fy = (dy / distance) * force;
|
|
if (!edge.source.fixed) {
|
|
edge.source.vx += fx;
|
|
edge.source.vy += fy;
|
|
}
|
|
if (!edge.target.fixed) {
|
|
edge.target.vx -= fx;
|
|
edge.target.vy -= fy;
|
|
}
|
|
});
|
|
this.nodes.forEach((node, index) => {
|
|
const centerPull = 0.018;
|
|
if (!node.fixed) {
|
|
node.vx += -node.x * centerPull * dt;
|
|
node.vy += -node.y * centerPull * dt;
|
|
const wave = Math.sin(time * (2.2 + this.frequencyTilt * 4) + node.pulse + index * 0.11);
|
|
node.vx += Math.cos(node.pulse) * wave * signal * 4.5 * dt;
|
|
node.vy += Math.sin(node.pulse) * wave * signal * 4.5 * dt;
|
|
node.x += node.vx;
|
|
node.y += node.vy;
|
|
}
|
|
node.vx *= 0.86;
|
|
node.vy *= 0.86;
|
|
});
|
|
}
|
|
|
|
draw(time) {
|
|
if (!this.ctx || !this.canvas) {
|
|
return;
|
|
}
|
|
const ratio = window.devicePixelRatio || 1;
|
|
const width = this.canvas.width;
|
|
const height = this.canvas.height;
|
|
const cssWidth = width / ratio;
|
|
const cssHeight = height / ratio;
|
|
const signal = Math.min(1.2, (this.audioLevel * 3.2 + 0.035) * this.sensitivity);
|
|
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
this.ctx.clearRect(0, 0, width, height);
|
|
this.ctx.fillStyle = getComputedStyle(this.contentEl).getPropertyValue("--background-primary") || "#111";
|
|
this.ctx.fillRect(0, 0, width, height);
|
|
this.ctx.setTransform(ratio * this.zoom, 0, 0, ratio * this.zoom, ratio * this.panX, ratio * this.panY);
|
|
const styles = getComputedStyle(this.contentEl);
|
|
const normalColor = styles.getPropertyValue("--text-normal").trim() || "#d7d7d7";
|
|
const mutedColor = styles.getPropertyValue("--text-muted").trim() || "#9a9a9a";
|
|
const faintColor = styles.getPropertyValue("--background-modifier-border").trim() || "#555";
|
|
|
|
this.edges.forEach((edge) => {
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(edge.source.x, edge.source.y);
|
|
this.ctx.lineTo(edge.target.x, edge.target.y);
|
|
this.ctx.globalAlpha = 0.2 + Math.min(0.22, signal * 0.12);
|
|
this.ctx.strokeStyle = mutedColor;
|
|
this.ctx.lineWidth = Math.min(2, 0.65 + edge.weight * 0.12 + signal * 0.16) / this.zoom;
|
|
this.ctx.stroke();
|
|
});
|
|
this.ctx.globalAlpha = 1;
|
|
|
|
this.nodes.forEach((node, index) => {
|
|
const isHot = node === this.hoveredNode;
|
|
const breath = Math.sin(time * 4.5 + node.pulse + index * 0.05) * 0.5 + 0.5;
|
|
const reactiveRadius = node.radius + signal * (1.2 + breath * 2.6) + (isHot ? 2.5 : 0);
|
|
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(node.x, node.y, reactiveRadius, 0, Math.PI * 2);
|
|
this.ctx.fillStyle = isHot ? normalColor : mutedColor;
|
|
this.ctx.globalAlpha = isHot ? 0.92 : 0.54 + Math.min(0.12, signal * 0.06);
|
|
this.ctx.fill();
|
|
this.ctx.lineWidth = (isHot ? 1.7 : 0.9) / this.zoom;
|
|
this.ctx.globalAlpha = isHot ? 0.82 : 0.36;
|
|
this.ctx.strokeStyle = normalColor;
|
|
this.ctx.stroke();
|
|
this.ctx.globalAlpha = 1;
|
|
|
|
if ((isHot || this.zoom > 1.02) && reactiveRadius > 7) {
|
|
this.ctx.font = `${Math.max(10, 11 / this.zoom)}px -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
this.ctx.globalAlpha = isHot ? 0.88 : 0.48;
|
|
this.ctx.fillStyle = isHot ? normalColor : faintColor;
|
|
this.ctx.textAlign = "center";
|
|
this.ctx.textBaseline = "top";
|
|
this.ctx.fillText(node.label.slice(0, 34), node.x, node.y + reactiveRadius + 7 / this.zoom);
|
|
this.ctx.globalAlpha = 1;
|
|
}
|
|
});
|
|
|
|
if (this.nodes.length === 0) {
|
|
this.ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
this.ctx.fillStyle = "rgba(180, 188, 202, 0.85)";
|
|
this.ctx.font = "14px -apple-system, BlinkMacSystemFont, sans-serif";
|
|
this.ctx.textAlign = "center";
|
|
this.ctx.fillText("No markdown links found yet", cssWidth / 2, cssHeight / 2);
|
|
}
|
|
}
|
|
|
|
getGraphPoint(event) {
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
return {
|
|
x: (event.clientX - rect.left - this.panX) / this.zoom,
|
|
y: (event.clientY - rect.top - this.panY) / this.zoom,
|
|
};
|
|
}
|
|
|
|
findNodeAt(x, y) {
|
|
for (let index = this.nodes.length - 1; index >= 0; index -= 1) {
|
|
const node = this.nodes[index];
|
|
const radius = node.radius + Math.max(8, this.audioLevel * 24);
|
|
const dx = x - node.x;
|
|
const dy = y - node.y;
|
|
if (dx * dx + dy * dy <= radius * radius) {
|
|
return node;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class OpenAgentView extends ItemView {
|
|
constructor(leaf, plugin) {
|
|
super(leaf);
|
|
this.plugin = plugin;
|
|
this.cwdInput = null;
|
|
this.composerInput = null;
|
|
this.panelScrollEl = null;
|
|
this.panelScrollKey = "";
|
|
this.expandedToolMessages = new Set();
|
|
this.visibleMessageCountByTask = new Map();
|
|
this.visibleSettingsTaskCountByKey = new Map();
|
|
this.settingsTaskTab = "all";
|
|
this.lastRenderedActiveTaskId = null;
|
|
this.pendingInitialPanelScrollKey = "";
|
|
this.lastRenderedTaskMessageStateByTask = new Map();
|
|
this.activeSelectableMessageEl = null;
|
|
this.isClampingMessageSelection = false;
|
|
this.taskListHovered = false;
|
|
this.renderedActiveTaskId = "";
|
|
this.activeTaskStatusEl = null;
|
|
this.messagesSectionEl = null;
|
|
}
|
|
|
|
getViewType() {
|
|
return VIEW_TYPE;
|
|
}
|
|
|
|
getDisplayText() {
|
|
return "OpenAgent";
|
|
}
|
|
|
|
getIcon() {
|
|
return "bot";
|
|
}
|
|
|
|
async onOpen() {
|
|
const document = this.containerEl?.ownerDocument;
|
|
if (document) {
|
|
this.registerDomEvent(document, "selectionchange", () => this.handleMessageSelectionChange());
|
|
this.registerDomEvent(document, "mouseup", () => this.clearActiveMessageSelection());
|
|
this.registerDomEvent(document, "pointerup", () => this.clearActiveMessageSelection());
|
|
}
|
|
await this.plugin.refreshTasks();
|
|
this.render();
|
|
}
|
|
|
|
clearActiveMessageSelection() {
|
|
this.activeSelectableMessageEl = null;
|
|
}
|
|
|
|
bindMessageSelectionGuard(messageTextEl) {
|
|
if (!messageTextEl) {
|
|
return;
|
|
}
|
|
|
|
const activateMessageSelection = (event) => {
|
|
if (event && typeof event.button === "number" && event.button !== 0) {
|
|
return;
|
|
}
|
|
this.activeSelectableMessageEl = messageTextEl;
|
|
};
|
|
|
|
messageTextEl.addEventListener("mousedown", activateMessageSelection);
|
|
messageTextEl.addEventListener("pointerdown", activateMessageSelection);
|
|
}
|
|
|
|
bindTaskListHoverGuard(taskListEl) {
|
|
if (!taskListEl) {
|
|
return;
|
|
}
|
|
|
|
taskListEl.addEventListener("mouseenter", () => {
|
|
this.taskListHovered = true;
|
|
this.plugin.setTaskListHovered(true);
|
|
});
|
|
taskListEl.addEventListener("mouseleave", () => {
|
|
this.taskListHovered = false;
|
|
this.plugin.setTaskListHovered(false);
|
|
});
|
|
}
|
|
|
|
hasHoveredTaskList() {
|
|
return this.taskListHovered === true;
|
|
}
|
|
|
|
handleMessageSelectionChange() {
|
|
if (this.isClampingMessageSelection) {
|
|
return;
|
|
}
|
|
|
|
const messageTextEl = this.activeSelectableMessageEl;
|
|
if (!messageTextEl || !messageTextEl.isConnected) {
|
|
this.activeSelectableMessageEl = null;
|
|
return;
|
|
}
|
|
|
|
const document = messageTextEl.ownerDocument;
|
|
const selection = document?.getSelection?.();
|
|
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
|
|
return;
|
|
}
|
|
|
|
const range = selection.getRangeAt(0);
|
|
const startInside = messageTextEl.contains(range.startContainer);
|
|
const endInside = messageTextEl.contains(range.endContainer);
|
|
if (startInside && endInside) {
|
|
return;
|
|
}
|
|
|
|
if (!range.intersectsNode(messageTextEl)) {
|
|
return;
|
|
}
|
|
|
|
const clampedRange = range.cloneRange();
|
|
if (!startInside) {
|
|
clampedRange.setStart(messageTextEl, 0);
|
|
}
|
|
if (!endInside) {
|
|
clampedRange.setEnd(messageTextEl, messageTextEl.childNodes.length);
|
|
}
|
|
|
|
this.isClampingMessageSelection = true;
|
|
try {
|
|
selection.removeAllRanges();
|
|
selection.addRange(clampedRange);
|
|
} finally {
|
|
this.isClampingMessageSelection = false;
|
|
}
|
|
}
|
|
|
|
captureComposerFocusState() {
|
|
const input = this.composerInput;
|
|
if (!input) {
|
|
return null;
|
|
}
|
|
|
|
const activeElement = input.ownerDocument?.activeElement;
|
|
if (activeElement !== input) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
taskId: String(input.dataset.taskId || ""),
|
|
selectionStart: typeof input.selectionStart === "number" ? input.selectionStart : null,
|
|
selectionEnd: typeof input.selectionEnd === "number" ? input.selectionEnd : null,
|
|
selectionDirection: input.selectionDirection || "none",
|
|
scrollTop: input.scrollTop || 0,
|
|
};
|
|
}
|
|
|
|
restoreComposerFocusState(focusState) {
|
|
if (!focusState || !this.composerInput) {
|
|
return;
|
|
}
|
|
|
|
if (String(this.composerInput.dataset.taskId || "") !== String(focusState.taskId || "")) {
|
|
return;
|
|
}
|
|
|
|
window.requestAnimationFrame(() => {
|
|
if (!this.composerInput) {
|
|
return;
|
|
}
|
|
|
|
this.composerInput.focus();
|
|
if (typeof focusState.selectionStart === "number" && typeof focusState.selectionEnd === "number") {
|
|
try {
|
|
this.composerInput.setSelectionRange(
|
|
focusState.selectionStart,
|
|
focusState.selectionEnd,
|
|
focusState.selectionDirection
|
|
);
|
|
} catch {
|
|
// Ignore selection restore failures on detached or unsupported inputs.
|
|
}
|
|
}
|
|
this.composerInput.scrollTop = Number(focusState.scrollTop || 0);
|
|
});
|
|
}
|
|
|
|
capturePanelScrollState() {
|
|
const scrollEl = this.panelScrollEl;
|
|
if (!scrollEl) {
|
|
return null;
|
|
}
|
|
|
|
const maxScrollTop = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight);
|
|
const scrollTop = Number(scrollEl.scrollTop || 0);
|
|
const distanceFromBottom = Math.max(0, maxScrollTop - scrollTop);
|
|
return {
|
|
key: this.panelScrollKey,
|
|
scrollTop,
|
|
atBottom: distanceFromBottom <= 32,
|
|
};
|
|
}
|
|
|
|
restorePanelScrollState(scrollState, nextKey) {
|
|
if (!scrollState || scrollState.key !== nextKey) {
|
|
return;
|
|
}
|
|
|
|
window.requestAnimationFrame(() => {
|
|
const scrollEl = this.panelScrollEl;
|
|
if (!scrollEl || this.panelScrollKey !== nextKey) {
|
|
return;
|
|
}
|
|
|
|
const maxScrollTop = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight);
|
|
scrollEl.scrollTop = scrollState.atBottom
|
|
? maxScrollTop
|
|
: Math.min(Number(scrollState.scrollTop || 0), maxScrollTop);
|
|
});
|
|
}
|
|
|
|
getLatestMessageState(task) {
|
|
const taskId = String(task?.taskId || "");
|
|
const messages = Array.isArray(task?.messages) ? task.messages : [];
|
|
if (!taskId || messages.length === 0) {
|
|
return {
|
|
taskId,
|
|
count: messages.length,
|
|
lastIdentity: "",
|
|
lastRole: "",
|
|
};
|
|
}
|
|
|
|
const lastIndex = messages.length - 1;
|
|
const lastMessage = messages[lastIndex];
|
|
return {
|
|
taskId,
|
|
count: messages.length,
|
|
lastIdentity: this.getMessageIdentity(lastMessage, lastIndex),
|
|
lastRole: String(lastMessage?.role || ""),
|
|
};
|
|
}
|
|
|
|
shouldAutoScrollToLatestUserMessage(task, options = {}) {
|
|
if (options.didSwitchActiveTask === true) {
|
|
return false;
|
|
}
|
|
|
|
const nextState = this.getLatestMessageState(task);
|
|
if (!nextState.taskId || nextState.count === 0 || nextState.lastRole !== "user") {
|
|
return false;
|
|
}
|
|
|
|
const previousState = this.lastRenderedTaskMessageStateByTask.get(nextState.taskId);
|
|
if (!previousState) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
nextState.count > previousState.count
|
|
|| nextState.lastIdentity !== previousState.lastIdentity
|
|
);
|
|
}
|
|
|
|
rememberRenderedTaskMessageState(task) {
|
|
const state = this.getLatestMessageState(task);
|
|
if (!state.taskId) {
|
|
return;
|
|
}
|
|
|
|
this.lastRenderedTaskMessageStateByTask.set(state.taskId, state);
|
|
}
|
|
|
|
scrollMessageIntoView(messageEl, nextKey) {
|
|
if (!messageEl) {
|
|
return;
|
|
}
|
|
|
|
window.requestAnimationFrame(() => {
|
|
const scrollEl = this.panelScrollEl;
|
|
if (!scrollEl || this.panelScrollKey !== nextKey || !messageEl.isConnected) {
|
|
return;
|
|
}
|
|
|
|
messageEl.scrollIntoView({
|
|
block: "end",
|
|
inline: "nearest",
|
|
});
|
|
});
|
|
}
|
|
|
|
queueInitialPanelScroll(nextKey) {
|
|
this.pendingInitialPanelScrollKey = String(nextKey || "").trim();
|
|
}
|
|
|
|
shouldInitialScrollToConversationEnd(nextKey) {
|
|
return String(nextKey || "").trim() !== ""
|
|
&& this.pendingInitialPanelScrollKey === String(nextKey || "").trim();
|
|
}
|
|
|
|
consumeInitialPanelScroll(nextKey) {
|
|
if (this.pendingInitialPanelScrollKey === String(nextKey || "").trim()) {
|
|
this.pendingInitialPanelScrollKey = "";
|
|
}
|
|
}
|
|
|
|
hasFocusedComposer() {
|
|
const input = this.composerInput;
|
|
return Boolean(input && input.ownerDocument?.activeElement === input);
|
|
}
|
|
|
|
getMessageIdentity(message, index) {
|
|
return String(
|
|
message?.id
|
|
|| message?.streamKey
|
|
|| `${message?.role || "system"}:${message?.kind || ""}:${message?.turnId || ""}:${message?.itemId || ""}:${index}`
|
|
);
|
|
}
|
|
|
|
isToolMessage(message) {
|
|
return String(message?.kind || "").trim() === "tool";
|
|
}
|
|
|
|
getToolMessagePreview(text) {
|
|
const compact = String(text || "").replace(/\s+/g, " ").trim();
|
|
return compact || "Tool output";
|
|
}
|
|
|
|
resetActiveTaskDomRefs() {
|
|
this.renderedActiveTaskId = "";
|
|
this.activeTaskStatusEl = null;
|
|
this.messagesSectionEl = null;
|
|
}
|
|
|
|
updateActiveTaskStatus(task) {
|
|
if (!this.activeTaskStatusEl?.isConnected) {
|
|
return;
|
|
}
|
|
|
|
const status = String(task?.status || "idle");
|
|
this.activeTaskStatusEl.className = `oa-status-tag${["running", "starting"].includes(status) ? " is-running" : ""}`;
|
|
this.activeTaskStatusEl.setText(status);
|
|
}
|
|
|
|
renderTaskMessages(messagesSection, activeTask, options = {}) {
|
|
if (!messagesSection) {
|
|
return;
|
|
}
|
|
|
|
const panelScrollState = options.panelScrollState || null;
|
|
const panelScrollKey = String(options.panelScrollKey || this.panelScrollKey || "");
|
|
const didSwitchActiveTask = options.didSwitchActiveTask === true;
|
|
const shouldAutoScrollToLatestUserMessage = this.shouldAutoScrollToLatestUserMessage(activeTask, {
|
|
didSwitchActiveTask,
|
|
});
|
|
|
|
messagesSection.empty();
|
|
const messages = Array.isArray(activeTask?.messages) ? activeTask.messages : [];
|
|
const messagesLoaded = this.plugin.taskHasLoadedMessages(activeTask);
|
|
const messageCount = Math.max(Number(activeTask?.messageCount) || 0, messages.length);
|
|
const renderSelectionImageBubble = () => {
|
|
const messageList = messagesSection.createDiv({ cls: "oa-message-list" });
|
|
this.plugin.renderSelectionImageMessage(messageList, activeTask?.selectionContext);
|
|
return messageList;
|
|
};
|
|
|
|
if (!messagesLoaded && messageCount > 0) {
|
|
renderSelectionImageBubble();
|
|
messagesSection.createDiv({
|
|
cls: "oa-muted",
|
|
text: `Loading conversation (${messageCount} messages)...`,
|
|
});
|
|
this.restorePanelScrollState(panelScrollState, panelScrollKey);
|
|
this.rememberRenderedTaskMessageState(activeTask);
|
|
return;
|
|
}
|
|
|
|
if (messages.length === 0) {
|
|
renderSelectionImageBubble();
|
|
messagesSection.createDiv({ cls: "oa-muted", text: "No conversation yet." });
|
|
this.restorePanelScrollState(panelScrollState, panelScrollKey);
|
|
this.rememberRenderedTaskMessageState(activeTask);
|
|
return;
|
|
}
|
|
|
|
const visibleMessageCount = this.syncVisibleMessageCount(activeTask.taskId, messages.length, {
|
|
reset: didSwitchActiveTask,
|
|
});
|
|
const firstVisibleIndex = Math.max(0, messages.length - visibleMessageCount);
|
|
const hiddenMessageCount = firstVisibleIndex;
|
|
const shouldInitialScrollToConversationEnd = this.shouldInitialScrollToConversationEnd(panelScrollKey);
|
|
let latestMessageEl = null;
|
|
let latestUserMessageEl = null;
|
|
|
|
if (hiddenMessageCount > 0) {
|
|
const pagination = messagesSection.createDiv({ cls: "oa-message-pagination" });
|
|
pagination.createDiv({
|
|
cls: "oa-message-pagination-summary",
|
|
text: `Showing ${visibleMessageCount} latest of ${messages.length} messages.`,
|
|
});
|
|
const paginationActions = pagination.createDiv({ cls: "oa-action-row oa-message-pagination-actions" });
|
|
const nextVisibleCount = Math.min(messages.length, visibleMessageCount + CHAT_MESSAGE_PAGE_SIZE);
|
|
const loadCount = nextVisibleCount - visibleMessageCount;
|
|
this.makeButton(
|
|
paginationActions,
|
|
`Show ${loadCount} earlier`,
|
|
() => this.showOlderMessages(activeTask.taskId, messages.length),
|
|
false,
|
|
{ runOnMouseDown: true }
|
|
);
|
|
}
|
|
|
|
const messageList = renderSelectionImageBubble();
|
|
messages.slice(firstVisibleIndex).forEach((message, index) => {
|
|
const messageIndex = firstVisibleIndex + index;
|
|
const isToolMessage = this.isToolMessage(message);
|
|
const item = messageList.createDiv({
|
|
cls: `oa-message oa-chat-message oa-role-${message.role || "system"}${isToolMessage ? " oa-message-tool" : ""}`,
|
|
});
|
|
latestMessageEl = item;
|
|
if (isToolMessage) {
|
|
const messageKey = this.getMessageIdentity(message, messageIndex);
|
|
const expanded = this.expandedToolMessages.has(messageKey);
|
|
item.toggleClass("is-expanded", expanded);
|
|
item.toggleClass("is-collapsed", !expanded);
|
|
|
|
const toggle = item.createEl("button", {
|
|
cls: "oa-message-toggle",
|
|
attr: {
|
|
type: "button",
|
|
"aria-expanded": expanded ? "true" : "false",
|
|
},
|
|
});
|
|
const toggleText = toggle.createDiv({ cls: "oa-message-toggle-text" });
|
|
toggleText.createDiv({
|
|
cls: "oa-message-preview",
|
|
text: this.getToolMessagePreview(message.text),
|
|
});
|
|
toggle.createDiv({
|
|
cls: "oa-message-toggle-icon",
|
|
text: expanded ? "-" : "+",
|
|
});
|
|
|
|
const body = item.createDiv({
|
|
cls: "oa-message-text",
|
|
text: message.text || "",
|
|
});
|
|
this.bindMessageSelectionGuard(body);
|
|
body.toggleClass("is-hidden", !expanded);
|
|
|
|
toggle.addEventListener("click", () => {
|
|
if (this.expandedToolMessages.has(messageKey)) {
|
|
this.expandedToolMessages.delete(messageKey);
|
|
item.removeClass("is-expanded");
|
|
item.addClass("is-collapsed");
|
|
toggle.setAttribute("aria-expanded", "false");
|
|
body.addClass("is-hidden");
|
|
const icon = toggle.querySelector(".oa-message-toggle-icon");
|
|
if (icon) {
|
|
icon.textContent = "+";
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.expandedToolMessages.add(messageKey);
|
|
item.removeClass("is-collapsed");
|
|
item.addClass("is-expanded");
|
|
toggle.setAttribute("aria-expanded", "true");
|
|
body.removeClass("is-hidden");
|
|
const icon = toggle.querySelector(".oa-message-toggle-icon");
|
|
if (icon) {
|
|
icon.textContent = "-";
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
const body = item.createDiv({
|
|
cls: "oa-message-text",
|
|
text: message.text || "",
|
|
});
|
|
this.bindMessageSelectionGuard(body);
|
|
if (messageIndex === messages.length - 1 && String(message.role || "") === "user") {
|
|
latestUserMessageEl = item;
|
|
}
|
|
});
|
|
|
|
if (shouldInitialScrollToConversationEnd && latestMessageEl) {
|
|
this.consumeInitialPanelScroll(panelScrollKey);
|
|
this.scrollMessageIntoView(latestMessageEl, panelScrollKey);
|
|
} else if (shouldAutoScrollToLatestUserMessage && latestUserMessageEl) {
|
|
this.scrollMessageIntoView(latestUserMessageEl, panelScrollKey);
|
|
} else {
|
|
this.restorePanelScrollState(panelScrollState, panelScrollKey);
|
|
}
|
|
|
|
this.rememberRenderedTaskMessageState(activeTask);
|
|
}
|
|
|
|
tryApplyStreamingTaskUpdate(task) {
|
|
const normalizedTaskId = String(task?.taskId || "").trim();
|
|
if (!normalizedTaskId) {
|
|
return false;
|
|
}
|
|
|
|
if (this.plugin.getPanelTab() !== PANEL_TAB_OPTIONS.TASK_LIST) {
|
|
return false;
|
|
}
|
|
|
|
const activeTask = this.plugin.getActiveTask();
|
|
if (normalizedTaskId !== String(activeTask?.taskId || "").trim()) {
|
|
return false;
|
|
}
|
|
|
|
if (normalizedTaskId !== String(this.renderedActiveTaskId || "").trim()) {
|
|
return false;
|
|
}
|
|
|
|
if (!this.messagesSectionEl?.isConnected || !this.activeTaskStatusEl?.isConnected) {
|
|
return false;
|
|
}
|
|
|
|
if (!this.plugin.isTaskRunning(task) || String(task?.lastError || "").trim()) {
|
|
return false;
|
|
}
|
|
|
|
const panelScrollState = this.capturePanelScrollState();
|
|
this.updateActiveTaskStatus(task);
|
|
this.renderTaskMessages(this.messagesSectionEl, task, {
|
|
panelScrollState,
|
|
panelScrollKey: this.panelScrollKey,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
getDefaultVisibleMessageCount(totalMessages) {
|
|
return Math.min(Math.max(Number(totalMessages) || 0, 0), CHAT_MESSAGE_PAGE_SIZE);
|
|
}
|
|
|
|
syncVisibleMessageCount(taskId, totalMessages, options = {}) {
|
|
const normalizedTaskId = String(taskId || "");
|
|
const normalizedTotal = Math.max(Number(totalMessages) || 0, 0);
|
|
if (!normalizedTaskId) {
|
|
return this.getDefaultVisibleMessageCount(normalizedTotal);
|
|
}
|
|
|
|
const defaultCount = this.getDefaultVisibleMessageCount(normalizedTotal);
|
|
const currentCount = this.visibleMessageCountByTask.get(normalizedTaskId);
|
|
if (options.reset === true || typeof currentCount !== "number" || currentCount <= 0) {
|
|
this.visibleMessageCountByTask.set(normalizedTaskId, defaultCount);
|
|
return defaultCount;
|
|
}
|
|
|
|
const nextCount = Math.max(defaultCount, Math.min(currentCount, normalizedTotal));
|
|
this.visibleMessageCountByTask.set(normalizedTaskId, nextCount);
|
|
return nextCount;
|
|
}
|
|
|
|
showOlderMessages(taskId, totalMessages) {
|
|
const normalizedTaskId = String(taskId || "");
|
|
if (!normalizedTaskId) {
|
|
return;
|
|
}
|
|
|
|
const currentCount = this.syncVisibleMessageCount(normalizedTaskId, totalMessages);
|
|
const nextCount = Math.min(Math.max(Number(totalMessages) || 0, 0), currentCount + CHAT_MESSAGE_PAGE_SIZE);
|
|
if (nextCount === currentCount) {
|
|
return;
|
|
}
|
|
|
|
this.visibleMessageCountByTask.set(normalizedTaskId, nextCount);
|
|
this.render();
|
|
}
|
|
|
|
getDefaultVisibleSettingsTaskCount(totalTasks) {
|
|
return Math.min(Math.max(Number(totalTasks) || 0, 0), SETTINGS_RECENT_TASKS_PAGE_SIZE);
|
|
}
|
|
|
|
syncVisibleSettingsTaskCount(listKey, totalTasks, options = {}) {
|
|
const normalizedKey = String(listKey || "");
|
|
const normalizedTotal = Math.max(Number(totalTasks) || 0, 0);
|
|
if (!normalizedKey) {
|
|
return this.getDefaultVisibleSettingsTaskCount(normalizedTotal);
|
|
}
|
|
|
|
const defaultCount = this.getDefaultVisibleSettingsTaskCount(normalizedTotal);
|
|
const currentCount = this.visibleSettingsTaskCountByKey.get(normalizedKey);
|
|
if (options.reset === true || typeof currentCount !== "number" || currentCount <= 0) {
|
|
this.visibleSettingsTaskCountByKey.set(normalizedKey, defaultCount);
|
|
return defaultCount;
|
|
}
|
|
|
|
const nextCount = Math.max(defaultCount, Math.min(currentCount, normalizedTotal));
|
|
this.visibleSettingsTaskCountByKey.set(normalizedKey, nextCount);
|
|
return nextCount;
|
|
}
|
|
|
|
showMoreSettingsTasks(listKey, totalTasks) {
|
|
const normalizedKey = String(listKey || "");
|
|
if (!normalizedKey) {
|
|
return;
|
|
}
|
|
|
|
const currentCount = this.syncVisibleSettingsTaskCount(normalizedKey, totalTasks);
|
|
const nextCount = Math.min(Math.max(Number(totalTasks) || 0, 0), currentCount + SETTINGS_RECENT_TASKS_PAGE_SIZE);
|
|
if (nextCount === currentCount) {
|
|
return;
|
|
}
|
|
|
|
this.visibleSettingsTaskCountByKey.set(normalizedKey, nextCount);
|
|
this.render();
|
|
}
|
|
|
|
getSettingsTaskTab() {
|
|
if (this.settingsTaskTab === "archived") {
|
|
return "archived";
|
|
}
|
|
|
|
if (this.settingsTaskTab === "running") {
|
|
return "running";
|
|
}
|
|
|
|
return this.settingsTaskTab === "recent" ? "recent" : "all";
|
|
}
|
|
|
|
setSettingsTaskTab(tab) {
|
|
const nextTab = tab === "archived"
|
|
? "archived"
|
|
: (tab === "running"
|
|
? "running"
|
|
: (tab === "recent" ? "recent" : "all"));
|
|
if (this.settingsTaskTab === nextTab) {
|
|
return;
|
|
}
|
|
|
|
this.settingsTaskTab = nextTab;
|
|
this.render();
|
|
}
|
|
|
|
getSettingsTaskTabLabel(tab = this.getSettingsTaskTab()) {
|
|
if (tab === "archived") {
|
|
return "Archived threads";
|
|
}
|
|
if (tab === "running") {
|
|
return "Running threads";
|
|
}
|
|
if (tab === "recent") {
|
|
return "Recent threads";
|
|
}
|
|
return "All threads";
|
|
}
|
|
|
|
bindImmediateMouseAction(element, callback) {
|
|
if (!element || typeof callback !== "function") {
|
|
return;
|
|
}
|
|
|
|
let ignoreNextClick = false;
|
|
element.addEventListener("mousedown", (event) => {
|
|
if (event.button !== 0 || element.disabled) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
ignoreNextClick = true;
|
|
callback(event);
|
|
});
|
|
element.addEventListener("click", (event) => {
|
|
if (ignoreNextClick) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
ignoreNextClick = false;
|
|
return;
|
|
}
|
|
|
|
callback(event);
|
|
});
|
|
}
|
|
|
|
renderPanelTabs(parentEl, activePanelTab) {
|
|
const tabs = parentEl.createDiv({ cls: "oa-panel-tabs" });
|
|
[
|
|
{ id: PANEL_TAB_OPTIONS.TASK_LIST, label: "Threads" },
|
|
{ id: PANEL_TAB_OPTIONS.HEARTBEAT, label: "Heart beat" },
|
|
{ id: PANEL_TAB_OPTIONS.WORKSPACE, label: "Workspace" },
|
|
].forEach((tab) => {
|
|
const isActive = activePanelTab === tab.id;
|
|
const button = tabs.createEl("button", {
|
|
cls: `oa-panel-tab${isActive ? " is-active" : ""}`,
|
|
text: tab.label,
|
|
attr: {
|
|
type: "button",
|
|
"aria-pressed": isActive ? "true" : "false",
|
|
},
|
|
});
|
|
this.bindImmediateMouseAction(button, () => {
|
|
if (tab.id === PANEL_TAB_OPTIONS.TASK_LIST) {
|
|
void this.plugin.setTaskDetailOpen(false);
|
|
return;
|
|
}
|
|
void this.plugin.setPanelTab(tab.id);
|
|
});
|
|
});
|
|
}
|
|
|
|
renderThreadsPanel(parentEl, context = {}) {
|
|
const {
|
|
activeCanvasPath = "",
|
|
activeWorkspace = null,
|
|
activeTask = null,
|
|
archivedTasks = [],
|
|
runningTasks = [],
|
|
tasks = [],
|
|
} = context;
|
|
const threadsBlock = parentEl.createDiv({ cls: "oa-settings-section-block" });
|
|
const threadsHeader = threadsBlock.createDiv({ cls: "oa-settings-section-header oa-thread-filter-header" });
|
|
const settingsTaskTab = this.getSettingsTaskTab();
|
|
threadsHeader.createEl("h3", {
|
|
cls: "oa-settings-section-title",
|
|
text: activeCanvasPath ? this.getSettingsTaskTabLabel(settingsTaskTab) : "Tasks",
|
|
});
|
|
const filterShell = threadsHeader.createDiv({ cls: "oa-thread-filter-shell" });
|
|
const filterSelect = filterShell.createEl("select", {
|
|
cls: "oa-thread-filter-select",
|
|
attr: {
|
|
"aria-label": "Thread filter",
|
|
},
|
|
});
|
|
[
|
|
{ id: "all", label: "All threads" },
|
|
{ id: "recent", label: "Recent threads" },
|
|
{ id: "running", label: "Running threads" },
|
|
{ id: "archived", label: "Archived threads" },
|
|
].forEach((tab) => {
|
|
filterSelect.createEl("option", {
|
|
text: tab.label,
|
|
value: tab.id,
|
|
});
|
|
});
|
|
filterSelect.value = settingsTaskTab;
|
|
filterSelect.addEventListener("mousedown", (event) => {
|
|
event.stopPropagation();
|
|
filterSelect.focus();
|
|
});
|
|
filterSelect.addEventListener("change", () => this.setSettingsTaskTab(filterSelect.value));
|
|
|
|
const threadSection = threadsBlock.createDiv({ cls: "oa-task-section oa-settings-task-section" });
|
|
const allTasks = this.plugin.getTasks();
|
|
const displayedTasks = settingsTaskTab === "all"
|
|
? allTasks
|
|
: (settingsTaskTab === "archived"
|
|
? archivedTasks
|
|
: (settingsTaskTab === "running" ? runningTasks : tasks));
|
|
const taskListKey = [
|
|
"panel-threads",
|
|
settingsTaskTab,
|
|
settingsTaskTab === "all" ? "all" : (activeCanvasPath || ""),
|
|
settingsTaskTab === "all" ? "" : (activeWorkspace?.repoPath || ""),
|
|
].join(":");
|
|
this.renderTaskList(threadSection, displayedTasks, {
|
|
emptyText: settingsTaskTab === "archived"
|
|
? (activeCanvasPath
|
|
? "No archived thread yet for this canvas."
|
|
: "No archived task yet.")
|
|
: (settingsTaskTab === "all"
|
|
? "No OpenAgent threads yet."
|
|
: (settingsTaskTab === "running"
|
|
? (activeCanvasPath
|
|
? "No running thread for this canvas."
|
|
: "No running task right now.")
|
|
: (activeCanvasPath
|
|
? "No thread yet for this canvas. Select a node and start one."
|
|
: "Open a Canvas and select a text node to work with its thread."))),
|
|
listKey: taskListKey,
|
|
activeTask,
|
|
});
|
|
}
|
|
|
|
renderHeartbeatPanel(parentEl) {
|
|
const block = parentEl.createDiv({ cls: "oa-settings-section-block" });
|
|
block.createEl("h3", {
|
|
cls: "oa-settings-section-title",
|
|
text: "Automations",
|
|
});
|
|
const section = block.createDiv({ cls: "oa-settings-section" });
|
|
const automationFiles = this.plugin.getAutomationFiles();
|
|
const list = section.createDiv({ cls: "oa-settings-list" });
|
|
if (automationFiles.length === 0) {
|
|
list.createDiv({
|
|
cls: "oa-muted",
|
|
text: "No automation files found.",
|
|
});
|
|
} else {
|
|
automationFiles.forEach((file) => {
|
|
const row = list.createDiv({ cls: "oa-settings-row" });
|
|
row.createDiv({ cls: "oa-settings-label", text: basenameVaultPath(file.path).replace(/\.md$/i, "") });
|
|
const value = row.createDiv({ cls: "oa-settings-value oa-settings-value-stack" });
|
|
value.createDiv({ cls: "oa-settings-value-path", text: file.path, attr: { title: file.path } });
|
|
});
|
|
}
|
|
|
|
const actions = section.createDiv({ cls: "oa-action-row oa-settings-actions" });
|
|
this.makeButton(actions, "Sync automations", () => {
|
|
void this.plugin.syncAutomationsWithFeedback();
|
|
}, false, { runOnMouseDown: true });
|
|
this.makeButton(actions, "Run daily review", () => {
|
|
void this.plugin.runAutomationWithFeedback("daily-vault-review");
|
|
}, false, { runOnMouseDown: true });
|
|
}
|
|
|
|
renderWorkspacePanel(parentEl, context = {}) {
|
|
const { activeWorkspace = null, workspaceSummariesLoading = false } = context;
|
|
const workspaceBlock = parentEl.createDiv({ cls: "oa-settings-section-block" });
|
|
workspaceBlock.createEl("h3", {
|
|
cls: "oa-settings-section-title",
|
|
text: "Workspace",
|
|
});
|
|
const workspaceSection = workspaceBlock.createDiv({
|
|
cls: "oa-settings-section oa-workspace-settings-section",
|
|
});
|
|
const workspaceSummary = workspaceSection.createDiv({ cls: "oa-settings-list" });
|
|
const repoPath = activeWorkspace?.repoPath || "";
|
|
const projectConfigPath = repoPath ? path.join(repoPath, REPO_WORKSPACE_DIR_NAME, WORKSPACE_CONFIG_FILE_NAME) : "";
|
|
const agentsPath = repoPath ? path.join(repoPath, "AGENTS.md") : "";
|
|
[
|
|
{
|
|
label: "Current workspace",
|
|
value: repoPath || (workspaceSummariesLoading ? "Loading workspace..." : "No workspace selected"),
|
|
title: repoPath,
|
|
},
|
|
{
|
|
label: "Vault config",
|
|
value: activeWorkspace?.configPath || "Missing",
|
|
title: activeWorkspace?.configPath || "",
|
|
},
|
|
{
|
|
label: "Project config",
|
|
value: projectConfigPath
|
|
? (fs.existsSync(projectConfigPath) ? `Found: ${projectConfigPath}` : `Missing: ${projectConfigPath}`)
|
|
: "No workspace selected",
|
|
title: projectConfigPath,
|
|
},
|
|
{
|
|
label: "AGENTS.md",
|
|
value: agentsPath
|
|
? (fs.existsSync(agentsPath) ? agentsPath : `Missing: ${agentsPath}`)
|
|
: "No workspace selected",
|
|
title: agentsPath,
|
|
},
|
|
].forEach((item) => {
|
|
const row = workspaceSummary.createDiv({ cls: "oa-settings-row" });
|
|
row.createDiv({ cls: "oa-settings-label", text: item.label });
|
|
row.createDiv({
|
|
cls: "oa-settings-value oa-settings-value-path",
|
|
text: item.value,
|
|
attr: item.title ? { title: item.title } : {},
|
|
});
|
|
});
|
|
|
|
const workspaceActions = workspaceSection.createDiv({ cls: "oa-action-row oa-settings-actions" });
|
|
const chooseWorkspaceButton = this.makeButton(workspaceActions, "New workspace", () => this.plugin.showWorkspacePicker());
|
|
chooseWorkspaceButton.setCta();
|
|
if (activeWorkspace?.configPath) {
|
|
this.makeButton(workspaceActions, "Open workspace.json", () => {
|
|
void this.plugin.app.workspace.openLinkText(activeWorkspace.configPath, "", false);
|
|
});
|
|
}
|
|
if (agentsPath && fs.existsSync(agentsPath)) {
|
|
this.makeButton(workspaceActions, "Open AGENTS.md", () => {
|
|
window.open(pathToFileURL(agentsPath).href);
|
|
});
|
|
}
|
|
|
|
const daemonBlock = parentEl.createDiv({ cls: "oa-settings-section-block" });
|
|
daemonBlock.createEl("h3", {
|
|
cls: "oa-settings-section-title",
|
|
text: "Codex server",
|
|
});
|
|
const daemonSection = daemonBlock.createDiv({ cls: "oa-settings-section oa-settings-summary" });
|
|
const daemonSummary = daemonSection.createDiv({ cls: "oa-settings-list" });
|
|
const daemonRow = daemonSummary.createDiv({ cls: "oa-settings-row" });
|
|
daemonRow.createDiv({ cls: "oa-settings-label", text: "Connection" });
|
|
const daemonValue = daemonRow.createDiv({ cls: "oa-settings-value oa-settings-value-stack" });
|
|
daemonValue.createDiv({
|
|
cls: `oa-status-tag ${this.plugin.getDaemonStatusTagClassName()}`.trim(),
|
|
text: this.plugin.getDaemonStatusLabel(),
|
|
});
|
|
daemonValue.createDiv({
|
|
cls: "oa-task-meta",
|
|
text: this.plugin.getDaemonStatusDetail() || "Checks whether the local Codex server is reachable.",
|
|
});
|
|
|
|
const daemonActions = daemonSection.createDiv({ cls: "oa-action-row oa-settings-actions" });
|
|
this.makeButton(daemonActions, "Refresh", () => {
|
|
void this.plugin.refreshDaemonStatus({
|
|
silent: false,
|
|
forceRefresh: true,
|
|
});
|
|
}, false, {
|
|
runOnMouseDown: true,
|
|
});
|
|
if (this.plugin.getDaemonStatus().state !== DAEMON_CONNECTION_STATES.ONLINE) {
|
|
this.makeButton(daemonActions, "Start server", () => {
|
|
void this.plugin.startDaemonWithFeedback();
|
|
}, false, {
|
|
runOnMouseDown: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
renderTaskList(parentEl, tasks, options = {}) {
|
|
if (!parentEl) {
|
|
return;
|
|
}
|
|
|
|
const title = String(options.title || "").trim();
|
|
if (title) {
|
|
parentEl.createEl("h3", { text: title });
|
|
}
|
|
|
|
const displayedTasks = Array.isArray(tasks) ? tasks : [];
|
|
const listEl = parentEl.createDiv({ cls: "oa-task-list" });
|
|
this.bindTaskListHoverGuard(listEl);
|
|
if (displayedTasks.length === 0) {
|
|
listEl.createDiv({
|
|
cls: "oa-muted",
|
|
text: String(options.emptyText || "No threads yet."),
|
|
});
|
|
return;
|
|
}
|
|
|
|
const listKey = String(options.listKey || "").trim();
|
|
const visibleTaskCount = options.enablePagination === false
|
|
? displayedTasks.length
|
|
: this.syncVisibleSettingsTaskCount(listKey, displayedTasks.length, {
|
|
reset: options.resetPagination === true,
|
|
});
|
|
const onTaskClick = typeof options.onTaskClick === "function"
|
|
? options.onTaskClick
|
|
: ((task) => this.plugin.activateTaskFromList(task.taskId));
|
|
const visibleTasks = displayedTasks.slice(0, visibleTaskCount);
|
|
let previousDayKey = "";
|
|
|
|
visibleTasks.forEach((task) => {
|
|
const dayTimestamp = this.plugin.getTaskConversationLastMessageDayTimestamp(task);
|
|
const dayKey = Number.isFinite(dayTimestamp) ? String(dayTimestamp) : "unknown";
|
|
if (dayKey !== previousDayKey) {
|
|
previousDayKey = dayKey;
|
|
listEl.createEl("h4", {
|
|
cls: "oa-task-list-date-heading",
|
|
text: formatTaskListDayLabel(dayTimestamp),
|
|
});
|
|
}
|
|
|
|
const isRunningTask = this.plugin.isTaskRunning(task);
|
|
const item = listEl.createDiv({
|
|
cls: `oa-task-item oa-thread-list-item${options.activeTask?.taskId === task.taskId ? " is-active" : ""}${isRunningTask ? " is-running" : ""}`,
|
|
});
|
|
const itemHeader = item.createDiv({ cls: "oa-task-item-header" });
|
|
const itemMain = itemHeader.createDiv({ cls: "oa-thread-list-main" });
|
|
itemMain.createDiv({ cls: "oa-task-title oa-thread-list-title", text: task.title || "Untitled task" });
|
|
const metaParts = [];
|
|
const projectLabel = this.plugin.getTaskProjectLabel(task);
|
|
if (projectLabel) {
|
|
metaParts.push(projectLabel);
|
|
}
|
|
const conversationTimestamp = this.plugin.getTaskConversationLastMessageTimestamp(task);
|
|
const conversationTimeLabel = formatTaskListTimeLabel(conversationTimestamp);
|
|
if (conversationTimeLabel) {
|
|
metaParts.push(conversationTimeLabel);
|
|
}
|
|
if (metaParts.length > 0) {
|
|
itemMain.createDiv({
|
|
cls: "oa-thread-list-meta",
|
|
text: metaParts.join(" • "),
|
|
});
|
|
}
|
|
if (this.plugin.hasUnreadTask(task.taskId)) {
|
|
itemHeader.createDiv({
|
|
cls: "oa-thread-list-unread-dot",
|
|
attr: {
|
|
"aria-label": "Unread result",
|
|
title: "Unread result",
|
|
},
|
|
});
|
|
} else {
|
|
const detailButton = itemHeader.createEl("button", {
|
|
cls: "oa-thread-list-arrow",
|
|
text: "→",
|
|
attr: {
|
|
type: "button",
|
|
"aria-label": "Show detail",
|
|
title: "Show detail",
|
|
},
|
|
});
|
|
this.bindImmediateMouseAction(detailButton, () => {
|
|
void this.plugin.activateTaskFromList(task.taskId, {
|
|
showDetail: true,
|
|
});
|
|
});
|
|
}
|
|
|
|
this.bindImmediateMouseAction(item, () => {
|
|
void onTaskClick(task);
|
|
});
|
|
});
|
|
|
|
if (options.enablePagination === false || displayedTasks.length <= visibleTaskCount) {
|
|
return;
|
|
}
|
|
|
|
const moreActions = parentEl.createDiv({ cls: "oa-action-row" });
|
|
this.makeButton(
|
|
moreActions,
|
|
`Show more (${displayedTasks.length - visibleTaskCount} left)`,
|
|
() => this.showMoreSettingsTasks(listKey, displayedTasks.length),
|
|
false,
|
|
{ runOnMouseDown: true }
|
|
);
|
|
}
|
|
|
|
renderGroupContextPreview(parentEl, preview) {
|
|
if (!parentEl || !preview) {
|
|
return;
|
|
}
|
|
|
|
const isLoading = preview.status === "loading";
|
|
const textCount = Array.isArray(preview.textBlocks) ? preview.textBlocks.length : 0;
|
|
const fileCount = Array.isArray(preview.markdownFiles) ? preview.markdownFiles.length : 0;
|
|
const summaryText = isLoading
|
|
? `Loading group context from ${preview.canvasPath}...`
|
|
: fileCount > 0
|
|
? `${fileCount} markdown file${fileCount === 1 ? "" : "s"} in ${preview.canvasPath}`
|
|
: `No markdown file context found in ${preview.canvasPath}`;
|
|
|
|
const card = parentEl.createDiv({ cls: "oa-task-item oa-group-context-card" });
|
|
const cardHeader = card.createDiv({ cls: "oa-task-item-header" });
|
|
cardHeader.createDiv({
|
|
cls: "oa-task-title",
|
|
text: `Group context: ${preview.groupLabel || "Untitled group"}`,
|
|
});
|
|
cardHeader.createDiv({
|
|
cls: "oa-status-tag",
|
|
text: isLoading ? "Loading" : "Preview",
|
|
});
|
|
|
|
card.createDiv({
|
|
cls: "oa-task-meta",
|
|
text: summaryText,
|
|
attr: { title: preview.canvasPath },
|
|
});
|
|
|
|
const list = card.createDiv({ cls: "oa-group-context-list" });
|
|
(preview.markdownFiles || []).slice(0, 4).forEach((file, index) => {
|
|
const item = list.createDiv({ cls: "oa-group-context-item" });
|
|
item.createDiv({ cls: "oa-group-context-label", text: `Markdown ${index + 1}` });
|
|
item.createDiv({
|
|
cls: "oa-group-context-value",
|
|
text: String(file?.path || file?.name || "Untitled"),
|
|
attr: file?.path ? { title: file.path } : {},
|
|
});
|
|
});
|
|
if (isLoading && fileCount === 0) {
|
|
list.createDiv({
|
|
cls: "oa-muted",
|
|
text: "Resolving markdown files in this group...",
|
|
});
|
|
}
|
|
if ((preview.markdownFiles || []).length > 4) {
|
|
list.createDiv({
|
|
cls: "oa-muted",
|
|
text: `And ${(preview.markdownFiles || []).length - 4} more markdown file${(preview.markdownFiles || []).length - 4 === 1 ? "" : "s"}.`,
|
|
});
|
|
}
|
|
|
|
if (fileCount === 0 && textCount > 0) {
|
|
list.createDiv({
|
|
cls: "oa-muted",
|
|
text: "This group has text nodes, but only markdown file nodes become default context.",
|
|
});
|
|
}
|
|
|
|
if (Array.isArray(preview.warnings) && preview.warnings.length > 0) {
|
|
preview.warnings.forEach((warning) => {
|
|
card.createDiv({ cls: "oa-banner oa-banner-warning", text: warning });
|
|
});
|
|
}
|
|
|
|
card.createDiv({
|
|
cls: "oa-muted",
|
|
text: "Select a text node inside this group to start a new thread with this group context.",
|
|
});
|
|
}
|
|
|
|
render() {
|
|
const composerFocusState = this.captureComposerFocusState();
|
|
const panelScrollState = this.capturePanelScrollState();
|
|
const { contentEl: rootEl } = this;
|
|
rootEl.empty();
|
|
this.resetActiveTaskDomRefs();
|
|
rootEl.addClass("openagent-view");
|
|
const activeCanvasPath = this.plugin.getActiveCanvasPath();
|
|
const activeWorkspace = this.plugin.getActiveWorkspace();
|
|
const tasks = this.plugin.getVisibleTasks();
|
|
const archivedTasks = this.plugin.getArchivedTasks();
|
|
const runningTasks = this.plugin.getRunningTasks();
|
|
const activeTask = this.plugin.getActiveTask();
|
|
const selectedGroupContextPreview = this.plugin.getSelectedGroupContextPreview();
|
|
const workspaceSummariesLoading = this.plugin.isWorkspaceSummariesLoading();
|
|
const activeTaskId = activeTask?.taskId ? String(activeTask.taskId) : "";
|
|
const didSwitchActiveTask = Boolean(activeTaskId && activeTaskId !== this.lastRenderedActiveTaskId);
|
|
const shouldAutoScrollToLatestUserMessage = this.shouldAutoScrollToLatestUserMessage(activeTask, {
|
|
didSwitchActiveTask,
|
|
});
|
|
this.lastRenderedActiveTaskId = activeTaskId || null;
|
|
const panelTab = this.plugin.getPanelTab();
|
|
const taskDetailOpen = panelTab === PANEL_TAB_OPTIONS.TASK_LIST && this.plugin.isTaskDetailOpen();
|
|
const isUtilityTab = panelTab !== PANEL_TAB_OPTIONS.TASK_LIST;
|
|
const contentEl = rootEl.createDiv({ cls: `oa-panel-scroll${isUtilityTab ? " is-settings-screen" : ""}` });
|
|
this.panelScrollEl = contentEl;
|
|
this.cwdInput = null;
|
|
this.composerInput = null;
|
|
const activeTaskArchived = activeTask ? this.plugin.isTaskArchived(activeTask) : false;
|
|
const panelScrollKey = [
|
|
panelTab,
|
|
activeCanvasPath || "",
|
|
panelTab === PANEL_TAB_OPTIONS.TASK_LIST ? (activeTask?.taskId || "") : "",
|
|
].join(":");
|
|
this.panelScrollKey = panelScrollKey;
|
|
if (panelTab === PANEL_TAB_OPTIONS.TASK_LIST && activeTaskId && didSwitchActiveTask) {
|
|
this.queueInitialPanelScroll(panelScrollKey);
|
|
}
|
|
|
|
const header = contentEl.createDiv({ cls: "oa-header" });
|
|
const headerTop = header.createDiv({ cls: "oa-header-top" });
|
|
const brand = headerTop.createDiv({ cls: "oa-brand" });
|
|
const logo = brand.createDiv({ cls: "oa-logo", attr: { "aria-hidden": "true" } });
|
|
const logoDataUrl = this.plugin.getPluginLogoDataUrl();
|
|
if (logoDataUrl) {
|
|
logo.createEl("img", {
|
|
cls: "oa-logo-image",
|
|
attr: {
|
|
src: logoDataUrl,
|
|
alt: "",
|
|
},
|
|
});
|
|
} else {
|
|
logo.createSpan({ cls: "oa-logo-mark", text: "OA" });
|
|
}
|
|
const brandText = brand.createDiv({ cls: "oa-brand-text" });
|
|
const brandTitleRow = brandText.createDiv({ cls: "oa-brand-title-row" });
|
|
brandTitleRow.createEl("h2", { text: "OpenAgent" });
|
|
if (this.plugin.getDaemonStatus().state === DAEMON_CONNECTION_STATES.ONLINE) {
|
|
brandTitleRow.createSpan({
|
|
cls: "oa-daemon-online-dot",
|
|
attr: {
|
|
"aria-label": "Codex server online",
|
|
title: this.plugin.getDaemonStatusDetail() || "Codex server is online.",
|
|
},
|
|
});
|
|
}
|
|
brandText.createDiv({
|
|
cls: "oa-brand-subtitle",
|
|
text: activeTask?.cwd
|
|
? compactPathLabel(activeTask.cwd)
|
|
: (activeWorkspace?.name || (workspaceSummariesLoading ? "Loading workspace..." : "Canvas-linked conversations")),
|
|
});
|
|
const actions = headerTop.createDiv({ cls: "oa-action-row oa-header-actions" });
|
|
this.makeButton(actions, "Refresh", () => {
|
|
void this.plugin.refreshTasks();
|
|
}, false, { icon: "refresh-cw", iconOnly: true, tooltip: "Refresh threads" });
|
|
if (activeCanvasPath) {
|
|
this.makeButton(actions, "Arrange", () => this.plugin.autoArrangeActiveCanvas(), false, {
|
|
icon: "layout-dashboard",
|
|
iconOnly: true,
|
|
tooltip: "Arrange canvas",
|
|
});
|
|
}
|
|
if (taskDetailOpen && activeTask) {
|
|
const headerConversation = header.createDiv({ cls: "oa-conversation-heading" });
|
|
headerConversation.createDiv({ cls: "oa-detail-title", text: activeTask.title || "Untitled task" });
|
|
this.activeTaskStatusEl = headerConversation.createDiv({
|
|
cls: `oa-status-tag${["running", "starting"].includes(String(activeTask.status || "")) ? " is-running" : ""}`,
|
|
text: activeTask.status || "idle",
|
|
});
|
|
}
|
|
if (this.plugin.runtimeIssue) {
|
|
contentEl.createDiv({
|
|
cls: "oa-banner oa-banner-error",
|
|
text: this.plugin.runtimeIssue,
|
|
});
|
|
}
|
|
|
|
const panelBody = contentEl.createDiv({ cls: "oa-settings-body" });
|
|
const panelContent = panelBody.createDiv({ cls: "oa-settings-page-content" });
|
|
this.renderPanelTabs(panelContent, panelTab);
|
|
if (panelTab === PANEL_TAB_OPTIONS.HEARTBEAT) {
|
|
this.renderHeartbeatPanel(panelContent);
|
|
this.restoreComposerFocusState(composerFocusState);
|
|
this.restorePanelScrollState(panelScrollState, panelScrollKey);
|
|
return;
|
|
}
|
|
if (panelTab === PANEL_TAB_OPTIONS.WORKSPACE) {
|
|
this.renderWorkspacePanel(panelContent, {
|
|
activeWorkspace,
|
|
workspaceSummariesLoading,
|
|
});
|
|
this.restoreComposerFocusState(composerFocusState);
|
|
this.restorePanelScrollState(panelScrollState, panelScrollKey);
|
|
return;
|
|
}
|
|
if (!taskDetailOpen || !activeTask) {
|
|
this.renderThreadsPanel(panelContent, {
|
|
activeCanvasPath,
|
|
activeWorkspace,
|
|
activeTask,
|
|
archivedTasks,
|
|
runningTasks,
|
|
tasks,
|
|
});
|
|
this.restoreComposerFocusState(composerFocusState);
|
|
this.restorePanelScrollState(panelScrollState, panelScrollKey);
|
|
return;
|
|
}
|
|
|
|
const detail = panelContent.createDiv({ cls: "oa-detail-section" });
|
|
|
|
if (selectedGroupContextPreview) {
|
|
this.renderGroupContextPreview(detail, selectedGroupContextPreview);
|
|
}
|
|
|
|
if (activeTaskArchived) {
|
|
detail.createDiv({
|
|
cls: "oa-banner oa-banner-warning",
|
|
text: "Archived because its root Canvas node was deleted.",
|
|
});
|
|
}
|
|
if (Array.isArray(activeTask.selectionContext?.warnings) && activeTask.selectionContext.warnings.length > 0) {
|
|
activeTask.selectionContext.warnings.forEach((warning) => {
|
|
detail.createDiv({ cls: "oa-banner oa-banner-warning", text: warning });
|
|
});
|
|
}
|
|
|
|
if (activeTask.lastError) {
|
|
detail.createDiv({ cls: "oa-banner oa-banner-error", text: activeTask.lastError });
|
|
}
|
|
|
|
const messagesSection = detail.createDiv({ cls: "oa-messages-section" });
|
|
this.messagesSectionEl = messagesSection;
|
|
this.renderedActiveTaskId = String(activeTask.taskId || "");
|
|
this.renderTaskMessages(messagesSection, activeTask, {
|
|
didSwitchActiveTask,
|
|
panelScrollState,
|
|
panelScrollKey,
|
|
});
|
|
|
|
const composer = rootEl.createDiv({ cls: "oa-composer-section" });
|
|
const composerInputShell = composer.createDiv({ cls: "oa-composer-input-shell" });
|
|
const taskIsRunning = this.plugin.isTaskRunning(activeTask);
|
|
const canStopActiveTurn = Boolean(activeTask?.currentTurnId);
|
|
this.composerInput = composerInputShell.createEl("textarea", { cls: "oa-composer-input" });
|
|
this.composerInput.dataset.taskId = activeTask.taskId;
|
|
this.composerInput.placeholder = "Add a follow-up message...";
|
|
this.composerInput.rows = 2;
|
|
this.composerInput.value = this.plugin.getDraft(activeTask.taskId);
|
|
this.composerInput.addEventListener("focus", () => {
|
|
this.plugin.setComposerFocused(true);
|
|
});
|
|
this.composerInput.addEventListener("input", (event) => {
|
|
this.plugin.setDraft(activeTask.taskId, event.target.value);
|
|
});
|
|
this.composerInput.addEventListener("blur", () => {
|
|
this.plugin.setComposerFocused(false);
|
|
void this.plugin.persistPluginState();
|
|
});
|
|
this.composerInput.addEventListener("keydown", (event) => {
|
|
if (event.isComposing || event.keyCode === 229) {
|
|
return;
|
|
}
|
|
|
|
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
event.preventDefault();
|
|
this.plugin.sendDraftForTask(activeTask.taskId, { draft: this.composerInput.value });
|
|
}
|
|
});
|
|
|
|
const stopActionTitle = this.plugin.getStopActiveTaskButtonTitle();
|
|
const composerActionButton = composerInputShell.createEl("button", {
|
|
cls: `oa-composer-action-button${canStopActiveTurn ? " is-stop" : " is-send"}`,
|
|
text: canStopActiveTurn ? "Stop" : "↑",
|
|
attr: {
|
|
type: "button",
|
|
"aria-label": canStopActiveTurn ? "Stop active task" : "Send message",
|
|
"aria-keyshortcuts": canStopActiveTurn ? this.plugin.getStopActiveTaskHotkey() : "",
|
|
title: canStopActiveTurn ? stopActionTitle : "Send",
|
|
},
|
|
});
|
|
composerActionButton.disabled = !canStopActiveTurn && taskIsRunning;
|
|
composerActionButton.addEventListener("click", () => {
|
|
if (canStopActiveTurn) {
|
|
void this.plugin.stopActiveTask();
|
|
return;
|
|
}
|
|
this.plugin.sendDraftForTask(activeTask.taskId, { draft: this.composerInput?.value || "" });
|
|
});
|
|
|
|
this.restoreComposerFocusState(composerFocusState);
|
|
}
|
|
|
|
makeButton(container, label, onClick, disabled = false, options = {}) {
|
|
const button = new ButtonComponent(container);
|
|
button.setButtonText(options.iconOnly ? "" : label);
|
|
button.setDisabled(Boolean(disabled));
|
|
if (options.icon && button.buttonEl) {
|
|
setIcon(button.buttonEl, options.icon);
|
|
button.buttonEl.addClass("oa-icon-button");
|
|
if (options.iconOnly) {
|
|
button.buttonEl.addClass("is-icon-only");
|
|
}
|
|
button.buttonEl.setAttribute("aria-label", String(options.tooltip || label || "Action"));
|
|
setTooltip(button.buttonEl, String(options.tooltip || label || "Action"));
|
|
}
|
|
if (options.runOnMouseDown && button.buttonEl) {
|
|
let ignoreNextClick = false;
|
|
button.buttonEl.addEventListener("mousedown", (event) => {
|
|
if (event.button !== 0 || button.buttonEl.disabled) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
ignoreNextClick = true;
|
|
onClick();
|
|
});
|
|
button.buttonEl.addEventListener("click", (event) => {
|
|
if (ignoreNextClick) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
ignoreNextClick = false;
|
|
return;
|
|
}
|
|
|
|
onClick();
|
|
});
|
|
return button;
|
|
}
|
|
|
|
button.onClick(onClick);
|
|
return button;
|
|
}
|
|
}
|
|
|
|
class OpenAgentVaultPet {
|
|
constructor(plugin) {
|
|
this.plugin = plugin;
|
|
this.rootEl = null;
|
|
this.bubbleEl = null;
|
|
this.chatEl = null;
|
|
this.chatPathEl = null;
|
|
this.chatCwdEl = null;
|
|
this.chatTextareaEl = null;
|
|
this.chatSendButtonEl = null;
|
|
this.dragState = null;
|
|
this.chatContext = null;
|
|
this.chatIsSending = false;
|
|
this.cleanupCallbacks = [];
|
|
this.idleTimer = null;
|
|
this.messageTimer = null;
|
|
this.saveTimer = null;
|
|
this.lastActivityAt = 0;
|
|
this.suppressNextClick = false;
|
|
}
|
|
|
|
mount() {
|
|
if (this.rootEl) {
|
|
return;
|
|
}
|
|
|
|
const root = document.createElement("div");
|
|
root.className = "oa-vault-pet";
|
|
root.dataset.mood = "idle";
|
|
root.setAttribute("role", "button");
|
|
root.setAttribute("tabindex", "0");
|
|
root.setAttribute("aria-label", "OpenAgent vault pet");
|
|
root.setAttribute("title", "OpenAgent vault pet");
|
|
|
|
const bubble = document.createElement("div");
|
|
bubble.className = "oa-vault-pet-bubble";
|
|
root.appendChild(bubble);
|
|
|
|
const chat = document.createElement("div");
|
|
chat.className = "oa-vault-pet-chat";
|
|
chat.setAttribute("aria-label", "Vault pet selection chat");
|
|
root.appendChild(chat);
|
|
|
|
const chatHeader = document.createElement("div");
|
|
chatHeader.className = "oa-vault-pet-chat-header";
|
|
chat.appendChild(chatHeader);
|
|
|
|
const chatTitle = document.createElement("div");
|
|
chatTitle.className = "oa-vault-pet-chat-title";
|
|
chatTitle.textContent = "Vault pet chat";
|
|
chatHeader.appendChild(chatTitle);
|
|
|
|
const chatCloseButton = document.createElement("button");
|
|
chatCloseButton.className = "oa-vault-pet-chat-close";
|
|
chatCloseButton.type = "button";
|
|
chatCloseButton.setAttribute("aria-label", "Close vault pet chat");
|
|
chatCloseButton.textContent = "x";
|
|
chatHeader.appendChild(chatCloseButton);
|
|
|
|
const chatPath = document.createElement("div");
|
|
chatPath.className = "oa-vault-pet-chat-path";
|
|
chat.appendChild(chatPath);
|
|
|
|
const chatCwd = document.createElement("div");
|
|
chatCwd.className = "oa-vault-pet-chat-cwd";
|
|
chat.appendChild(chatCwd);
|
|
|
|
const chatTextarea = document.createElement("textarea");
|
|
chatTextarea.className = "oa-vault-pet-chat-input";
|
|
chatTextarea.rows = 6;
|
|
chatTextarea.placeholder = "Chat or search from here...";
|
|
chat.appendChild(chatTextarea);
|
|
|
|
const chatActions = document.createElement("div");
|
|
chatActions.className = "oa-vault-pet-chat-actions";
|
|
chat.appendChild(chatActions);
|
|
|
|
const chatHint = document.createElement("div");
|
|
chatHint.className = "oa-vault-pet-chat-hint";
|
|
chatHint.textContent = "Ctrl/Cmd Enter sends a new thread";
|
|
chatActions.appendChild(chatHint);
|
|
|
|
const chatSendButton = document.createElement("button");
|
|
chatSendButton.className = "oa-vault-pet-chat-send";
|
|
chatSendButton.type = "button";
|
|
chatSendButton.textContent = "Send";
|
|
chatActions.appendChild(chatSendButton);
|
|
|
|
const creature = document.createElement("div");
|
|
creature.className = "oa-vault-pet-creature";
|
|
root.appendChild(creature);
|
|
|
|
const mascot = document.createElement("img");
|
|
mascot.className = "oa-vault-pet-mascot";
|
|
mascot.alt = "";
|
|
mascot.draggable = false;
|
|
mascot.src = this.plugin.getVaultPetMascotDataUrl();
|
|
creature.appendChild(mascot);
|
|
|
|
const shadow = document.createElement("div");
|
|
shadow.className = "oa-vault-pet-shadow";
|
|
root.appendChild(shadow);
|
|
|
|
document.body.appendChild(root);
|
|
this.rootEl = root;
|
|
this.bubbleEl = bubble;
|
|
this.chatEl = chat;
|
|
this.chatPathEl = chatPath;
|
|
this.chatCwdEl = chatCwd;
|
|
this.chatTextareaEl = chatTextarea;
|
|
this.chatSendButtonEl = chatSendButton;
|
|
chatCloseButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
this.hideChat();
|
|
});
|
|
chatSendButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
void this.submitChat();
|
|
});
|
|
chatTextarea.addEventListener("keydown", (event) => {
|
|
event.stopPropagation();
|
|
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
event.preventDefault();
|
|
void this.submitChat();
|
|
} else if (event.key === "Escape") {
|
|
event.preventDefault();
|
|
this.hideChat();
|
|
}
|
|
});
|
|
["pointerdown", "pointermove", "pointerup", "click"].forEach((eventName) => {
|
|
chat.addEventListener(eventName, (event) => event.stopPropagation());
|
|
});
|
|
this.applyPosition(this.getStoredPosition());
|
|
this.bindEvents();
|
|
this.say(pickRandomItem(VAULT_PET_IDLE_PHRASES), {
|
|
mood: "idle",
|
|
timeoutMs: 4_800,
|
|
});
|
|
this.idleTimer = window.setInterval(() => this.tick(), 13_000);
|
|
}
|
|
|
|
unmount() {
|
|
window.clearInterval(this.idleTimer);
|
|
window.clearTimeout(this.messageTimer);
|
|
window.clearTimeout(this.saveTimer);
|
|
this.idleTimer = null;
|
|
this.messageTimer = null;
|
|
this.saveTimer = null;
|
|
this.cleanupCallbacks.forEach((cleanup) => cleanup());
|
|
this.cleanupCallbacks = [];
|
|
this.rootEl?.remove();
|
|
this.rootEl = null;
|
|
this.bubbleEl = null;
|
|
this.chatEl = null;
|
|
this.chatPathEl = null;
|
|
this.chatCwdEl = null;
|
|
this.chatTextareaEl = null;
|
|
this.chatSendButtonEl = null;
|
|
this.chatContext = null;
|
|
this.chatIsSending = false;
|
|
this.dragState = null;
|
|
}
|
|
|
|
bindEvents() {
|
|
this.addListener(this.rootEl, "pointerdown", (event) => this.handlePointerDown(event));
|
|
this.addListener(this.rootEl, "click", (event) => this.handleClick(event));
|
|
this.addListener(this.rootEl, "keydown", (event) => this.handleKeydown(event));
|
|
this.addListener(window, "resize", () => this.applyPosition(this.getStoredPosition()));
|
|
this.addListener(document, "pointermove", (event) => this.handlePointerMove(event));
|
|
this.addListener(document, "pointerup", (event) => this.handlePointerUp(event));
|
|
}
|
|
|
|
addListener(target, eventName, handler) {
|
|
if (!target) {
|
|
return;
|
|
}
|
|
target.addEventListener(eventName, handler);
|
|
this.cleanupCallbacks.push(() => target.removeEventListener(eventName, handler));
|
|
}
|
|
|
|
handlePointerDown(event) {
|
|
if (event.button !== 0 || !this.rootEl) {
|
|
return;
|
|
}
|
|
const rect = this.rootEl.getBoundingClientRect();
|
|
this.dragState = {
|
|
offsetX: event.clientX - rect.left,
|
|
offsetY: event.clientY - rect.top,
|
|
startX: event.clientX,
|
|
startY: event.clientY,
|
|
moved: false,
|
|
};
|
|
this.rootEl.classList.add("is-dragging");
|
|
this.rootEl.setPointerCapture?.(event.pointerId);
|
|
}
|
|
|
|
handlePointerMove(event) {
|
|
if (!this.dragState || !this.rootEl) {
|
|
return;
|
|
}
|
|
const nextPosition = this.clampPosition({
|
|
x: event.clientX - this.dragState.offsetX,
|
|
y: event.clientY - this.dragState.offsetY,
|
|
});
|
|
if (
|
|
Math.abs(event.clientX - this.dragState.startX) > 3
|
|
|| Math.abs(event.clientY - this.dragState.startY) > 3
|
|
) {
|
|
this.dragState.moved = true;
|
|
}
|
|
this.applyPosition(nextPosition, { save: false });
|
|
}
|
|
|
|
handlePointerUp(event) {
|
|
if (!this.dragState || !this.rootEl) {
|
|
return;
|
|
}
|
|
const wasMoved = this.dragState.moved;
|
|
this.dragState = null;
|
|
this.rootEl.classList.remove("is-dragging");
|
|
this.rootEl.releasePointerCapture?.(event.pointerId);
|
|
this.schedulePositionSave();
|
|
if (wasMoved) {
|
|
this.suppressNextClick = true;
|
|
this.say("I like this spot.", {
|
|
mood: "curious",
|
|
timeoutMs: 3_200,
|
|
});
|
|
}
|
|
}
|
|
|
|
handleClick(event) {
|
|
if (this.suppressNextClick) {
|
|
this.suppressNextClick = false;
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
this.showQuickChat();
|
|
}
|
|
|
|
handleKeydown(event) {
|
|
if (event.key !== "Enter" && event.key !== " ") {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
this.showQuickChat();
|
|
}
|
|
|
|
tick() {
|
|
if (!this.rootEl || this.dragState) {
|
|
return;
|
|
}
|
|
if (Math.random() > 0.42) {
|
|
this.wander();
|
|
}
|
|
if (Math.random() > 0.35) {
|
|
this.say(pickRandomItem(VAULT_PET_IDLE_PHRASES), {
|
|
mood: "idle",
|
|
timeoutMs: 4_600,
|
|
});
|
|
}
|
|
}
|
|
|
|
wander() {
|
|
const current = this.getCurrentPosition();
|
|
const next = this.clampPosition({
|
|
x: current.x + Math.round((Math.random() - 0.5) * 130),
|
|
y: current.y + Math.round((Math.random() - 0.5) * 92),
|
|
});
|
|
this.applyPosition(next, { save: false });
|
|
this.rootEl?.classList.add("is-wandering");
|
|
window.setTimeout(() => this.rootEl?.classList.remove("is-wandering"), 850);
|
|
this.schedulePositionSave();
|
|
}
|
|
|
|
flyToRect(rect, options = {}) {
|
|
if (!this.rootEl || !isUsableClientRect(rect)) {
|
|
return false;
|
|
}
|
|
const side = rect.right + VAULT_PET_WIDTH + 24 < window.innerWidth ? "right" : "left";
|
|
const targetX = side === "right"
|
|
? rect.right + 12
|
|
: rect.left - VAULT_PET_WIDTH - 12;
|
|
const targetY = rect.top + (rect.height / 2) - (VAULT_PET_HEIGHT * 0.58);
|
|
this.applyPosition({
|
|
x: targetX,
|
|
y: targetY,
|
|
}, { save: false });
|
|
this.rootEl.classList.add("is-summoned");
|
|
window.setTimeout(() => this.rootEl?.classList.remove("is-summoned"), 920);
|
|
this.schedulePositionSave();
|
|
this.say(options.message || "Selection spotted.", {
|
|
mood: "curious",
|
|
timeoutMs: 3_800,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
showSelectionChat(context = {}) {
|
|
if (!this.rootEl || !this.chatEl || !this.chatTextareaEl) {
|
|
return;
|
|
}
|
|
this.chatContext = {
|
|
...context,
|
|
selectedText: String(context.selectedText || ""),
|
|
filePath: String(context.filePath || ""),
|
|
absolutePath: String(context.absolutePath || ""),
|
|
cwd: String(context.cwd || ""),
|
|
};
|
|
this.chatPathEl.textContent = this.chatContext.filePath
|
|
? `File: ${this.chatContext.filePath}`
|
|
: "File: current selection";
|
|
this.chatPathEl.title = this.chatContext.absolutePath || this.chatContext.filePath || "";
|
|
this.chatCwdEl.textContent = this.chatContext.cwd
|
|
? `CWD: ${this.chatContext.cwd}`
|
|
: "CWD: default workspace";
|
|
this.chatCwdEl.title = this.chatContext.cwd || "";
|
|
this.chatTextareaEl.value = this.chatContext.selectedText;
|
|
this.chatTextareaEl.placeholder = "Ask about the selected text...";
|
|
this.chatTextareaEl.disabled = false;
|
|
if (this.chatSendButtonEl) {
|
|
this.chatSendButtonEl.disabled = false;
|
|
this.chatSendButtonEl.textContent = "Send";
|
|
}
|
|
const currentPosition = this.getCurrentPosition();
|
|
this.rootEl.classList.toggle("is-chat-right", currentPosition.x < window.innerWidth / 2);
|
|
this.rootEl.classList.add("has-chat");
|
|
window.setTimeout(() => {
|
|
this.chatTextareaEl?.focus();
|
|
this.chatTextareaEl?.select();
|
|
}, 80);
|
|
}
|
|
|
|
showQuickChat() {
|
|
if (!this.rootEl || !this.chatEl || !this.chatTextareaEl) {
|
|
return;
|
|
}
|
|
const context = this.plugin.getVaultPetQuickChatContext();
|
|
this.chatContext = context;
|
|
this.chatPathEl.textContent = context.filePath
|
|
? `File: ${context.filePath}`
|
|
: "File: current vault";
|
|
this.chatPathEl.title = context.absolutePath || context.filePath || "";
|
|
this.chatCwdEl.textContent = context.cwd
|
|
? `CWD: ${context.cwd}`
|
|
: "CWD: default workspace";
|
|
this.chatCwdEl.title = context.cwd || "";
|
|
this.chatTextareaEl.value = "";
|
|
this.chatTextareaEl.placeholder = "Chat or search from here...";
|
|
this.chatTextareaEl.disabled = false;
|
|
if (this.chatSendButtonEl) {
|
|
this.chatSendButtonEl.disabled = false;
|
|
this.chatSendButtonEl.textContent = "Send";
|
|
}
|
|
const currentPosition = this.getCurrentPosition();
|
|
this.rootEl.classList.toggle("is-chat-right", currentPosition.x < window.innerWidth / 2);
|
|
this.rootEl.classList.add("has-chat");
|
|
this.say("Ready.", {
|
|
mood: "curious",
|
|
timeoutMs: 1_400,
|
|
});
|
|
window.setTimeout(() => {
|
|
this.chatTextareaEl?.focus();
|
|
}, 80);
|
|
}
|
|
|
|
hideChat() {
|
|
this.rootEl?.classList.remove("has-chat");
|
|
}
|
|
|
|
async submitChat() {
|
|
if (this.chatIsSending || !this.chatContext || !this.chatTextareaEl) {
|
|
return;
|
|
}
|
|
const message = String(this.chatTextareaEl.value || "").trim();
|
|
if (!message) {
|
|
this.say("Give me words to swim with.", {
|
|
mood: "surprised",
|
|
timeoutMs: 3_000,
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.chatIsSending = true;
|
|
this.chatTextareaEl.disabled = true;
|
|
if (this.chatSendButtonEl) {
|
|
this.chatSendButtonEl.disabled = true;
|
|
this.chatSendButtonEl.textContent = "Sending...";
|
|
}
|
|
try {
|
|
await this.plugin.startVaultPetChat(this.chatContext, message);
|
|
this.say("Thread launched.", {
|
|
mood: "happy",
|
|
timeoutMs: 3_600,
|
|
});
|
|
this.hideChat();
|
|
} catch (error) {
|
|
const messageText = String(error?.message || error || "Unable to start chat.");
|
|
new Notice(messageText);
|
|
this.say("The current got weird.", {
|
|
mood: "surprised",
|
|
timeoutMs: 3_600,
|
|
});
|
|
this.chatTextareaEl.disabled = false;
|
|
if (this.chatSendButtonEl) {
|
|
this.chatSendButtonEl.disabled = false;
|
|
this.chatSendButtonEl.textContent = "Send";
|
|
}
|
|
} finally {
|
|
this.chatIsSending = false;
|
|
}
|
|
}
|
|
|
|
pet() {
|
|
this.say(pickRandomItem(VAULT_PET_ACTIVITY_PHRASES.pet), {
|
|
mood: "happy",
|
|
timeoutMs: 4_200,
|
|
});
|
|
this.rootEl?.classList.add("is-petted");
|
|
window.setTimeout(() => this.rootEl?.classList.remove("is-petted"), 720);
|
|
}
|
|
|
|
handleVaultActivity(kind, file) {
|
|
if (!this.rootEl) {
|
|
return;
|
|
}
|
|
const filePath = String(file?.path || "");
|
|
if (!filePath || filePath.startsWith(".obsidian/plugins/openagent/")) {
|
|
return;
|
|
}
|
|
const now = Date.now();
|
|
if (now - this.lastActivityAt < VAULT_PET_ACTIVITY_COOLDOWN_MS) {
|
|
return;
|
|
}
|
|
this.lastActivityAt = now;
|
|
this.say(pickRandomItem(VAULT_PET_ACTIVITY_PHRASES[kind]) || "The vault moved.", {
|
|
mood: kind === "delete" ? "surprised" : "happy",
|
|
timeoutMs: 4_400,
|
|
});
|
|
this.rootEl.classList.add("is-excited");
|
|
window.setTimeout(() => this.rootEl?.classList.remove("is-excited"), 950);
|
|
}
|
|
|
|
handleWorkspaceChange() {
|
|
if (!this.rootEl || Date.now() - this.lastActivityAt < VAULT_PET_ACTIVITY_COOLDOWN_MS) {
|
|
return;
|
|
}
|
|
this.lastActivityAt = Date.now();
|
|
this.say(pickRandomItem(VAULT_PET_ACTIVITY_PHRASES.workspace), {
|
|
mood: "curious",
|
|
timeoutMs: 3_800,
|
|
});
|
|
}
|
|
|
|
say(message, options = {}) {
|
|
if (!this.rootEl || !this.bubbleEl) {
|
|
return;
|
|
}
|
|
const mood = String(options.mood || "idle");
|
|
this.rootEl.dataset.mood = mood;
|
|
this.bubbleEl.textContent = String(message || "Bloop.");
|
|
this.rootEl.classList.add("has-message");
|
|
window.clearTimeout(this.messageTimer);
|
|
this.messageTimer = window.setTimeout(() => {
|
|
this.rootEl?.classList.remove("has-message");
|
|
if (this.rootEl) {
|
|
this.rootEl.dataset.mood = "idle";
|
|
}
|
|
}, Number(options.timeoutMs || 4_000));
|
|
}
|
|
|
|
resetPosition() {
|
|
if (this.plugin.uiState) {
|
|
this.plugin.uiState.vaultPetState = {};
|
|
}
|
|
this.applyPosition(this.getDefaultPosition());
|
|
this.say("Back to my splash zone.", {
|
|
mood: "happy",
|
|
timeoutMs: 3_600,
|
|
});
|
|
this.schedulePositionSave();
|
|
}
|
|
|
|
getStoredPosition() {
|
|
const state = this.plugin.uiState?.vaultPetState || {};
|
|
const x = Number(state.x);
|
|
const y = Number(state.y);
|
|
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
return this.clampPosition({ x, y });
|
|
}
|
|
return this.getDefaultPosition();
|
|
}
|
|
|
|
getDefaultPosition() {
|
|
return this.clampPosition({
|
|
x: window.innerWidth - VAULT_PET_WIDTH - 34,
|
|
y: window.innerHeight - VAULT_PET_HEIGHT - 30,
|
|
});
|
|
}
|
|
|
|
getCurrentPosition() {
|
|
if (!this.rootEl) {
|
|
return this.getDefaultPosition();
|
|
}
|
|
return this.clampPosition({
|
|
x: Number.parseFloat(this.rootEl.style.left) || 0,
|
|
y: Number.parseFloat(this.rootEl.style.top) || 0,
|
|
});
|
|
}
|
|
|
|
clampPosition(position) {
|
|
const maxX = Math.max(VAULT_PET_MARGIN, window.innerWidth - VAULT_PET_WIDTH - VAULT_PET_MARGIN);
|
|
const maxY = Math.max(VAULT_PET_MARGIN, window.innerHeight - VAULT_PET_HEIGHT - VAULT_PET_MARGIN);
|
|
return {
|
|
x: Math.min(Math.max(Number(position.x) || VAULT_PET_MARGIN, VAULT_PET_MARGIN), maxX),
|
|
y: Math.min(Math.max(Number(position.y) || VAULT_PET_MARGIN, VAULT_PET_MARGIN), maxY),
|
|
};
|
|
}
|
|
|
|
applyPosition(position, options = {}) {
|
|
if (!this.rootEl) {
|
|
return;
|
|
}
|
|
const next = this.clampPosition(position);
|
|
this.rootEl.style.left = `${next.x}px`;
|
|
this.rootEl.style.top = `${next.y}px`;
|
|
if (options.save !== false) {
|
|
this.schedulePositionSave();
|
|
}
|
|
}
|
|
|
|
schedulePositionSave() {
|
|
window.clearTimeout(this.saveTimer);
|
|
this.saveTimer = window.setTimeout(() => {
|
|
if (!this.plugin.uiState || !this.rootEl) {
|
|
return;
|
|
}
|
|
const position = this.getCurrentPosition();
|
|
this.plugin.uiState.vaultPetState = {
|
|
x: Math.round(position.x),
|
|
y: Math.round(position.y),
|
|
};
|
|
void this.plugin.persistPluginState();
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
class OpenAgentSettingTab extends PluginSettingTab {
|
|
constructor(app, plugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
display() {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
|
|
containerEl.createEl("h2", { text: "OpenAgent" });
|
|
|
|
new Setting(containerEl)
|
|
.setName("Workspace root")
|
|
.setDesc("Vault folder where OpenAgent creates one symlinked project entry per repo.")
|
|
.addText((text) => {
|
|
text
|
|
.setPlaceholder("Workspaces")
|
|
.setValue(this.plugin.settings.workspaceRoot)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.workspaceRoot = String(value || "").trim() || DEFAULT_SETTINGS.workspaceRoot;
|
|
await this.plugin.persistPluginState();
|
|
});
|
|
});
|
|
|
|
new Setting(containerEl)
|
|
.setName("Automation folder")
|
|
.setDesc("Optional extra vault folder for automations. By default OpenAgent uses each workspace's Automations folder next to Main.canvas.")
|
|
.addText((text) => {
|
|
text
|
|
.setPlaceholder("Leave blank for workspace Automations folders")
|
|
.setValue(this.plugin.settings.automationFolder)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.automationFolder = String(value || "").trim();
|
|
await this.plugin.persistPluginState();
|
|
await this.plugin.ensureDefaultAutomationFile();
|
|
this.plugin.scheduleAutomationSync();
|
|
});
|
|
});
|
|
|
|
new Setting(containerEl)
|
|
.setName("Codex server launch command")
|
|
.setDesc("Optional. When the server is offline, OpenAgent will run this shell command and retry automatically.")
|
|
.addText((text) => {
|
|
text
|
|
.setPlaceholder("cd /path/to/openagent && pnpm dev:daemon")
|
|
.setValue(this.plugin.settings.daemonLaunchCommand)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.daemonLaunchCommand = String(value || "");
|
|
await this.plugin.persistPluginState();
|
|
});
|
|
});
|
|
|
|
new Setting(containerEl)
|
|
.setName("Codex server launch directory")
|
|
.setDesc("Optional working directory for the launch command above. Leave blank to use auto-discovery.")
|
|
.addText((text) => {
|
|
text
|
|
.setPlaceholder("/absolute/path/to/openagent")
|
|
.setValue(this.plugin.settings.daemonLaunchCwd)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.daemonLaunchCwd = String(value || "");
|
|
await this.plugin.persistPluginState();
|
|
});
|
|
});
|
|
|
|
const daemonStatusSetting = new Setting(containerEl)
|
|
.setName(`Codex server status: ${this.plugin.getDaemonStatusLabel()}`)
|
|
.setDesc(this.plugin.getDaemonStatusDetail() || "Checks whether the local Codex server is reachable.")
|
|
.addButton((button) => {
|
|
button.setButtonText("Refresh");
|
|
button.onClick(async () => {
|
|
await this.plugin.refreshDaemonStatus({
|
|
silent: false,
|
|
forceRefresh: true,
|
|
});
|
|
});
|
|
});
|
|
if (this.plugin.getDaemonStatus().state !== DAEMON_CONNECTION_STATES.ONLINE) {
|
|
daemonStatusSetting.addButton((button) => {
|
|
button.setButtonText("Start");
|
|
button.onClick(async () => {
|
|
await this.plugin.startDaemonWithFeedback();
|
|
});
|
|
});
|
|
}
|
|
|
|
new Setting(containerEl)
|
|
.setName("Codex sandbox mode")
|
|
.setDesc("Choose how much access turns launched from this plugin get. Workspace-only stays inside the selected project folder. Full access removes the filesystem sandbox and should only be used for trusted work.")
|
|
.addDropdown((dropdown) => {
|
|
dropdown
|
|
.addOption(DAEMON_SANDBOX_MODE_OPTIONS.WORKSPACE_WRITE, "Workspace only (safer)")
|
|
.addOption(DAEMON_SANDBOX_MODE_OPTIONS.DANGER_FULL_ACCESS, "Full access (advanced)")
|
|
.setValue(normalizeDaemonSandboxMode(this.plugin.settings.daemonSandboxMode))
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.daemonSandboxMode = normalizeDaemonSandboxMode(value);
|
|
await this.plugin.persistPluginState();
|
|
new Notice(`OpenAgent runtime mode: ${this.plugin.describeRuntimeConfig({ sandboxMode: value })}.`);
|
|
});
|
|
});
|
|
|
|
containerEl.createDiv({
|
|
cls: "oa-task-meta",
|
|
text: getDaemonSandboxModeHelpText(this.plugin.settings.daemonSandboxMode),
|
|
});
|
|
|
|
new Setting(containerEl)
|
|
.setName("Workspace")
|
|
.setDesc("Create or open a workspace folder that points to a real repo on disk.")
|
|
.addButton((button) => {
|
|
button.setButtonText("New workspace");
|
|
button.onClick(() => {
|
|
this.plugin.showWorkspacePicker();
|
|
});
|
|
});
|
|
|
|
new Setting(containerEl)
|
|
.setName("Stop active task hotkey")
|
|
.setDesc("Optional. Click the box, then press a shortcut like Mod+. or Mod+Shift+S to interrupt the current OpenAgent run. Backspace clears it.")
|
|
.addText((text) => {
|
|
text
|
|
.setPlaceholder("Click and press a shortcut")
|
|
.setValue(this.plugin.getStopActiveTaskHotkeyLabel());
|
|
text.inputEl.readOnly = true;
|
|
text.inputEl.classList.add(HOTKEY_CAPTURE_INPUT_CLASS);
|
|
text.inputEl.addEventListener("focus", () => {
|
|
text.inputEl.select();
|
|
});
|
|
text.inputEl.addEventListener("keydown", (event) => {
|
|
if (event.key === "Tab" && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
if (event.isComposing || event.keyCode === 229) {
|
|
return;
|
|
}
|
|
|
|
if (event.key === "Backspace" || event.key === "Delete") {
|
|
this.plugin.settings.stopActiveTaskHotkey = "";
|
|
text.setValue("");
|
|
this.plugin.requestViewRefresh();
|
|
void this.plugin.persistPluginState();
|
|
return;
|
|
}
|
|
|
|
const nextHotkey = normalizeHotkeyFromEvent(event);
|
|
if (!nextHotkey) {
|
|
return;
|
|
}
|
|
|
|
this.plugin.settings.stopActiveTaskHotkey = nextHotkey;
|
|
text.setValue(this.plugin.getStopActiveTaskHotkeyLabel());
|
|
this.plugin.requestViewRefresh();
|
|
void this.plugin.persistPluginState();
|
|
});
|
|
})
|
|
.addButton((button) => {
|
|
button.setButtonText("Clear");
|
|
button.onClick(async () => {
|
|
this.plugin.settings.stopActiveTaskHotkey = "";
|
|
await this.plugin.persistPluginState();
|
|
this.plugin.requestViewRefresh();
|
|
this.display();
|
|
});
|
|
});
|
|
|
|
if (this.plugin.getDaemonStatus().state !== DAEMON_CONNECTION_STATES.ONLINE) {
|
|
new Setting(containerEl)
|
|
.setName("Start server now")
|
|
.setDesc("Useful after changing launch settings or if you want to warm it up before using Canvas.")
|
|
.addButton((button) => {
|
|
button.setButtonText("Start");
|
|
button.onClick(async () => {
|
|
await this.plugin.startDaemonWithFeedback();
|
|
});
|
|
});
|
|
}
|
|
|
|
containerEl.createEl("h3", { text: "Vault pet" });
|
|
|
|
new Setting(containerEl)
|
|
.setName("Vault pet mascot")
|
|
.setDesc("Keep a small animated vault pet floating inside Obsidian.")
|
|
.addToggle((toggle) => {
|
|
toggle
|
|
.setValue(this.plugin.isVaultPetEnabled())
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.enableVaultPet = value === true;
|
|
this.plugin.configureVaultPet();
|
|
await this.plugin.persistPluginState();
|
|
});
|
|
})
|
|
.addButton((button) => {
|
|
button.setButtonText("Pet");
|
|
button.onClick(() => {
|
|
this.plugin.petVaultOctopus();
|
|
});
|
|
})
|
|
.addButton((button) => {
|
|
button.setButtonText("Reset spot");
|
|
button.onClick(async () => {
|
|
this.plugin.resetVaultPetPosition();
|
|
await this.plugin.persistPluginState();
|
|
});
|
|
});
|
|
|
|
containerEl.createEl("h3", { text: "Developer options" });
|
|
|
|
new Setting(containerEl)
|
|
.setName("Enable smoke request polling")
|
|
.setDesc("Developer only. Poll `.openagent/smoke-request.json` in the vault and execute smoke runs automatically.")
|
|
.addToggle((toggle) => {
|
|
toggle
|
|
.setValue(this.plugin.isDevSmokePollingEnabled())
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.enableDevSmokeRequests = value === true;
|
|
this.plugin.configureDevSmokePolling();
|
|
await this.plugin.persistPluginState();
|
|
});
|
|
});
|
|
|
|
new Setting(containerEl)
|
|
.setName("Enable debug logging")
|
|
.setDesc("Developer only. Record OpenAgent debug events and append them to `.openagent/new-thread-debug.jsonl` in the vault.")
|
|
.addToggle((toggle) => {
|
|
toggle
|
|
.setValue(this.plugin.isDebugLoggingEnabled())
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.enableDebugLogging = value === true;
|
|
if (!this.plugin.isDebugLoggingEnabled()) {
|
|
this.plugin.debugEvents = [];
|
|
}
|
|
await this.plugin.persistPluginState();
|
|
this.plugin.requestViewRefresh();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = class OpenAgentPlugin extends Plugin {
|
|
buildCanvasNodeTextCacheKey(canvasPath, nodeId) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedNodeId = String(nodeId || "").trim();
|
|
if (!normalizedCanvasPath || !normalizedNodeId) {
|
|
return "";
|
|
}
|
|
return `${normalizedCanvasPath}::${normalizedNodeId}`;
|
|
}
|
|
|
|
setCanvasNodeEditorText(canvasPath, nodeId, text) {
|
|
const cacheKey = this.buildCanvasNodeTextCacheKey(canvasPath, nodeId);
|
|
if (!cacheKey) {
|
|
return;
|
|
}
|
|
this.liveCanvasNodeTextByKey.set(cacheKey, {
|
|
text: String(text || ""),
|
|
updatedAt: Date.now(),
|
|
});
|
|
}
|
|
|
|
getCanvasNodeEditorText(canvasPath, nodeId) {
|
|
const cacheKey = this.buildCanvasNodeTextCacheKey(canvasPath, nodeId);
|
|
if (!cacheKey) {
|
|
return undefined;
|
|
}
|
|
const entry = this.liveCanvasNodeTextByKey.get(cacheKey);
|
|
if (!entry) {
|
|
return undefined;
|
|
}
|
|
if (Date.now() - Number(entry.updatedAt || 0) > CANVAS_EDITOR_TEXT_CACHE_TTL_MS) {
|
|
this.liveCanvasNodeTextByKey.delete(cacheKey);
|
|
return undefined;
|
|
}
|
|
return String(entry.text || "");
|
|
}
|
|
|
|
registerCanvasEditorTrackingExtension() {
|
|
if (!EditorView || !editorInfoField || typeof this.registerEditorExtension !== "function") {
|
|
return;
|
|
}
|
|
|
|
this.registerEditorExtension([
|
|
EditorView.updateListener.of((update) => {
|
|
if (!update.docChanged) {
|
|
return;
|
|
}
|
|
|
|
let editorInfo;
|
|
try {
|
|
editorInfo = update.state.field(editorInfoField);
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
const node = editorInfo?.node;
|
|
const canvasPath = String(node?.canvas?.view?.file?.path || "").trim();
|
|
const nodeId = String(node?.getData?.()?.id || node?.id || "").trim();
|
|
if (!canvasPath || !nodeId) {
|
|
return;
|
|
}
|
|
|
|
this.setCanvasNodeEditorText(canvasPath, nodeId, update.state.doc.toString());
|
|
}),
|
|
]);
|
|
}
|
|
|
|
async onload() {
|
|
const savedState = (await this.loadData()) || {};
|
|
const savedVaultId = String(savedState.vaultId || savedState.uiState?.vaultId || "").trim();
|
|
const didCreateVaultId = !savedVaultId;
|
|
this.vaultId = savedVaultId || createVaultId();
|
|
this.settings = {
|
|
...DEFAULT_SETTINGS,
|
|
...(savedState.settings || {}),
|
|
};
|
|
if (this.settings.automationFolder === "OpenAgent/Automations") {
|
|
this.settings.automationFolder = "";
|
|
}
|
|
this.settings.daemonSandboxMode = normalizeDaemonSandboxMode(this.settings.daemonSandboxMode);
|
|
this.settings.stopActiveTaskHotkey = normalizeStopHotkeySetting(this.settings.stopActiveTaskHotkey);
|
|
this.uiState = {
|
|
activeTaskId: savedState.uiState?.activeTaskId ?? savedState.activeTaskId ?? null,
|
|
panelTab: normalizePanelTab(savedState.uiState?.panelTab ?? savedState.panelTab),
|
|
taskDetailOpen: savedState.uiState?.taskDetailOpen === true,
|
|
draftsByTaskId: savedState.uiState?.draftsByTaskId ?? savedState.draftsByTaskId ?? {},
|
|
canvasNodeHighlightStateByKey: savedState.uiState?.canvasNodeHighlightStateByKey ?? {},
|
|
unreadTaskStateById: normalizeUnreadTaskState(
|
|
savedState.uiState?.unreadTaskStateById
|
|
?? savedState.unreadTaskStateById
|
|
?? {}
|
|
),
|
|
vaultPetState: savedState.uiState?.vaultPetState ?? {},
|
|
};
|
|
const savedSyncState = savedState.syncState || {};
|
|
this.daemonLauncher = new OpenAgentDaemonLauncher(this);
|
|
this.api = new OpenAgentApiClient({
|
|
daemonLauncher: this.daemonLauncher,
|
|
});
|
|
this.resolver = new CanvasSelectionResolver(this.app, this);
|
|
this.runtimeIssue = "";
|
|
this.tasksById = {};
|
|
this.viewRefreshTimer = null;
|
|
this.canvasSelectionCaptureTimer = null;
|
|
this.activeStreamDisposer = null;
|
|
this.activeStreamReconnectTimer = null;
|
|
this.activeStreamTaskId = "";
|
|
this.activeStreamSessionId = 0;
|
|
this.runningTaskStreamDisposers = new Map();
|
|
this.runningTaskStreamReconnectTimers = new Map();
|
|
this.runningTaskStreamSessionIds = {};
|
|
this.lastCanvasSelectionSnapshot = null;
|
|
this.devSmokeRunPromise = null;
|
|
this.lastProcessedSmokeRequestId = "";
|
|
this.logoDataUrl = "";
|
|
this.logoLoadPromise = null;
|
|
this.vaultPetMascotDataUrl = "";
|
|
this.debugEvents = [];
|
|
this.devSmokePollIntervalId = null;
|
|
this.daemonStatus = createDaemonStatusSnapshot({
|
|
state: DAEMON_CONNECTION_STATES.CHECKING,
|
|
});
|
|
this.daemonStatusRefreshPromise = null;
|
|
this.settingTab = null;
|
|
this.resultNodeSyncStateByTaskId = normalizeResultNodeSyncState(
|
|
savedSyncState.resultNodeSyncStateByTaskId
|
|
|| savedState.uiState?.resultNodeSyncStateByTaskId
|
|
|| {}
|
|
);
|
|
this.resultNodeSyncInFlight = new Set();
|
|
this.canvasRunSourceRefByTaskId = normalizeCanvasRunSourceRefState(
|
|
savedSyncState.canvasRunSourceRefByTaskId
|
|
|| savedState.uiState?.canvasRunSourceRefByTaskId
|
|
|| {}
|
|
);
|
|
this.canvasSnapshotCacheByPath = new Map();
|
|
this.canvasMutationQueueByPath = new Map();
|
|
this.canvasPopupMenuPatchStateByMenu = new Map();
|
|
this.canvasNodeHighlightSyncInFlight = new Set();
|
|
this.canvasNodeHighlightSyncPending = new Set();
|
|
this.canvasAutoRunInFlightByKey = new Set();
|
|
this.taskDetailRefreshInFlight = new Set();
|
|
this.composerFocused = false;
|
|
this.taskListHovered = false;
|
|
this.pendingViewRefresh = false;
|
|
this.workspaceSummaryCacheByConfigPath = new Map();
|
|
this.workspaceSummariesLoadPromise = null;
|
|
this.workspaceSummariesLoaded = false;
|
|
this.automationSyncTimer = null;
|
|
this.automationSyncPromise = null;
|
|
this.selectedGroupContextPreviewState = {
|
|
key: "",
|
|
preview: null,
|
|
loading: false,
|
|
};
|
|
this.selectedGroupContextPreviewLoadPromise = null;
|
|
this.liveCanvasNodeTextByKey = new Map();
|
|
this.vaultPet = null;
|
|
|
|
this.registerView(VIEW_TYPE, (leaf) => new OpenAgentView(leaf, this));
|
|
this.registerView(VOICE_GRAPH_VIEW_TYPE, (leaf) => new OpenAgentVoiceGraphView(leaf, this));
|
|
this.registerCanvasEditorTrackingExtension();
|
|
this.settingTab = new OpenAgentSettingTab(this.app, this);
|
|
this.addSettingTab(this.settingTab);
|
|
this.configureVaultPet();
|
|
this.configureDevSmokePolling();
|
|
this.registerInterval(window.setInterval(() => {
|
|
void this.refreshDaemonStatus({
|
|
silent: true,
|
|
});
|
|
}, DAEMON_STATUS_POLL_MS));
|
|
this.registerEvent(this.app.vault.on("create", (file) => {
|
|
this.handleWorkspaceConfigMutation(file);
|
|
this.handleAutomationFileMutation(file);
|
|
this.handleSelectedGroupContextDependencyMutation(file);
|
|
this.handleCanvasFileMutation(file);
|
|
this.handleVaultPetActivity("create", file);
|
|
}));
|
|
this.registerEvent(this.app.vault.on("modify", (file) => {
|
|
this.handleWorkspaceConfigMutation(file);
|
|
this.handleAutomationFileMutation(file);
|
|
this.handleSelectedGroupContextDependencyMutation(file);
|
|
this.handleCanvasFileMutation(file);
|
|
this.handleVaultPetActivity("modify", file);
|
|
}));
|
|
this.registerEvent(this.app.vault.on("delete", (file) => {
|
|
this.handleWorkspaceConfigMutation(file);
|
|
this.handleAutomationFileMutation(file);
|
|
this.handleSelectedGroupContextDependencyMutation(file);
|
|
this.handleCanvasFileMutation(file);
|
|
this.handleVaultPetActivity("delete", file);
|
|
}));
|
|
this.registerEvent(this.app.vault.on("rename", (file, oldPath) => {
|
|
this.handleWorkspaceConfigMutation(file, oldPath);
|
|
this.handleAutomationFileMutation(file, oldPath);
|
|
this.handleSelectedGroupContextDependencyMutation(file, oldPath);
|
|
this.handleCanvasFileMutation(file);
|
|
this.handleVaultPetActivity("rename", file);
|
|
}));
|
|
this.registerEvent(this.app.workspace.on("active-leaf-change", () => {
|
|
this.ensureCanvasPopupMenuPatches();
|
|
this.ensureCanvasSelectionTrackingPatches();
|
|
this.captureCanvasSelectionSnapshot();
|
|
const activeCanvasPath = this.getActiveCanvasPath();
|
|
if (activeCanvasPath) {
|
|
this.getCanvasSnapshotSync(activeCanvasPath);
|
|
}
|
|
this.syncActiveTaskToCanvasContext({ markReadOnSelection: true });
|
|
void this.refreshSelectedGroupContextPreview();
|
|
this.requestViewRefresh();
|
|
this.handleVaultPetWorkspaceChange();
|
|
}));
|
|
this.registerEvent(this.app.workspace.on("layout-change", () => {
|
|
this.ensureCanvasPopupMenuPatches();
|
|
this.ensureCanvasSelectionTrackingPatches();
|
|
}));
|
|
this.registerDomEvent(document, "keydown", (event) => {
|
|
void this.handleStopActiveTaskHotkey(event);
|
|
});
|
|
const initialCanvasPath = this.getActiveCanvasPath();
|
|
if (initialCanvasPath) {
|
|
this.getCanvasSnapshotSync(initialCanvasPath);
|
|
}
|
|
this.ensureCanvasPopupMenuPatches();
|
|
this.ensureCanvasSelectionTrackingPatches();
|
|
this.captureCanvasSelectionSnapshot();
|
|
|
|
this.addCommand({
|
|
id: "openagent-new-thread-from-selection",
|
|
name: "New thread from selection",
|
|
callback: () => this.handleNewThreadFromSelectionCommand(),
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-choose-repo-workspace",
|
|
name: "Choose workspace",
|
|
callback: () => this.showWorkspacePicker(),
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-open-panel",
|
|
name: "Open tasks",
|
|
callback: () => this.activateView(),
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-open-voice-graph",
|
|
name: "Open voice graph",
|
|
callback: () => {
|
|
void this.activateVoiceGraphView();
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-open-thread-list",
|
|
name: "Open thread list",
|
|
callback: () => {
|
|
void this.openThreadList();
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-resume-last-task",
|
|
name: "Resume last task",
|
|
callback: () => this.resumeLastTask(),
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-stop-active-task",
|
|
name: "Stop active task",
|
|
callback: () => this.stopActiveTask(),
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-auto-arrange-active-canvas",
|
|
name: "Auto arrange active canvas",
|
|
callback: () => this.autoArrangeActiveCanvas(),
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-start-daemon",
|
|
name: "Start Codex server",
|
|
callback: () => {
|
|
void this.startDaemonWithFeedback();
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-sync-automations",
|
|
name: "Sync automations",
|
|
callback: () => {
|
|
void this.syncAutomationsWithFeedback();
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-run-daily-vault-review",
|
|
name: "Run daily vault review",
|
|
callback: () => {
|
|
void this.runAutomationWithFeedback("daily-vault-review");
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-create-follow-up-node",
|
|
name: "Create follow-up node",
|
|
callback: () => {
|
|
void this.handleCreateFollowUpNodeCommand();
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-create-fork-node",
|
|
name: "Create fork node",
|
|
callback: () => {
|
|
void this.handleCreateForkNodeCommand();
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-toggle-vault-pet",
|
|
name: "Toggle vault mascot",
|
|
callback: () => {
|
|
void this.toggleVaultPet();
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-pet-vault-octopus",
|
|
name: "Pet vault mascot",
|
|
callback: () => {
|
|
this.petVaultOctopus();
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "openagent-summon-vault-octopus-to-selection",
|
|
name: "Summon vault mascot to selection",
|
|
hotkeys: [
|
|
{
|
|
modifiers: ["Ctrl"],
|
|
key: "r",
|
|
},
|
|
],
|
|
callback: () => {
|
|
this.summonVaultPetToSelection();
|
|
},
|
|
});
|
|
|
|
this.app.workspace.onLayoutReady(() => {
|
|
this.ensureCanvasPopupMenuPatches();
|
|
this.ensureCanvasSelectionTrackingPatches();
|
|
this.captureCanvasSelectionSnapshot();
|
|
void this.ensurePanelVisibleOnStartup();
|
|
});
|
|
void this.ensureWorkspaceSummariesLoaded();
|
|
void this.ensureDefaultAutomationFile().then(() => this.syncAutomationsToDaemon()).catch(() => {});
|
|
void this.ensurePluginLogoDataUrlLoaded();
|
|
void this.refreshDaemonStatus();
|
|
void this.refreshTasks().catch(() => {});
|
|
if (didCreateVaultId) {
|
|
void this.persistPluginState();
|
|
}
|
|
}
|
|
|
|
async onunload() {
|
|
window.clearTimeout(this.canvasSelectionCaptureTimer);
|
|
this.canvasSelectionCaptureTimer = null;
|
|
window.clearTimeout(this.automationSyncTimer);
|
|
this.automationSyncTimer = null;
|
|
this.disposeActiveStream();
|
|
this.disposeAllRunningTaskStreams();
|
|
this.restoreCanvasPopupMenuPatches();
|
|
this.vaultPet?.unmount();
|
|
this.vaultPet = null;
|
|
try {
|
|
await this.persistPluginState();
|
|
} catch {
|
|
// Ignore persistence failures during plugin teardown.
|
|
}
|
|
this.app.workspace.getLeavesOfType(VIEW_TYPE).forEach((leaf) => leaf.detach());
|
|
this.app.workspace.getLeavesOfType(VOICE_GRAPH_VIEW_TYPE).forEach((leaf) => leaf.detach());
|
|
}
|
|
|
|
async persistPluginState() {
|
|
await this.saveData({
|
|
vaultId: this.vaultId,
|
|
settings: this.settings,
|
|
uiState: this.uiState,
|
|
syncState: {
|
|
resultNodeSyncStateByTaskId: this.resultNodeSyncStateByTaskId,
|
|
canvasRunSourceRefByTaskId: this.canvasRunSourceRefByTaskId,
|
|
},
|
|
});
|
|
}
|
|
|
|
isVaultPetEnabled() {
|
|
return this.settings?.enableVaultPet !== false;
|
|
}
|
|
|
|
configureVaultPet() {
|
|
if (!this.isVaultPetEnabled()) {
|
|
this.vaultPet?.unmount();
|
|
this.vaultPet = null;
|
|
return;
|
|
}
|
|
if (!this.vaultPet) {
|
|
this.vaultPet = new OpenAgentVaultPet(this);
|
|
}
|
|
this.vaultPet.mount();
|
|
}
|
|
|
|
async toggleVaultPet() {
|
|
this.settings.enableVaultPet = !this.isVaultPetEnabled();
|
|
this.configureVaultPet();
|
|
await this.persistPluginState();
|
|
new Notice(this.isVaultPetEnabled() ? "Vault pet is awake." : "Vault pet is napping.");
|
|
this.settingTab?.display();
|
|
}
|
|
|
|
petVaultOctopus() {
|
|
if (!this.isVaultPetEnabled()) {
|
|
this.settings.enableVaultPet = true;
|
|
this.configureVaultPet();
|
|
void this.persistPluginState();
|
|
}
|
|
this.vaultPet?.pet();
|
|
}
|
|
|
|
summonVaultPetToSelection() {
|
|
if (!this.isVaultPetEnabled()) {
|
|
this.settings.enableVaultPet = true;
|
|
this.configureVaultPet();
|
|
void this.persistPluginState();
|
|
} else {
|
|
this.configureVaultPet();
|
|
}
|
|
|
|
const chatContext = this.getActiveMarkdownSelectionChatContext();
|
|
const selectionRect = this.getActiveSelectionClientRect();
|
|
if (!selectionRect || !chatContext?.selectedText) {
|
|
new Notice("Select some text first, then summon the vault pet.");
|
|
this.vaultPet?.say("Point me at some text.", {
|
|
mood: "surprised",
|
|
timeoutMs: 3_400,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const didFly = this.vaultPet?.flyToRect(selectionRect, {
|
|
message: "I found the thought.",
|
|
});
|
|
this.vaultPet?.showSelectionChat(chatContext);
|
|
if (!didFly) {
|
|
new Notice("Vault pet could not find a visible selection.");
|
|
}
|
|
}
|
|
|
|
getActiveMarkdownSelectionChatContext() {
|
|
const file = this.app.workspace.activeLeaf?.view?.file || this.app.workspace.getActiveFile();
|
|
if (!(file instanceof TFile)) {
|
|
return null;
|
|
}
|
|
const selectedText = this.getActiveSelectedText();
|
|
if (!selectedText.trim()) {
|
|
return null;
|
|
}
|
|
|
|
const vaultBasePath = this.getVaultBasePath();
|
|
const absolutePath = vaultBasePath ? path.resolve(vaultBasePath, file.path) : "";
|
|
const selectionContext = this.buildVaultPetSelectionContext({
|
|
file,
|
|
selectedText,
|
|
absolutePath,
|
|
});
|
|
const cwd = this.getDefaultCwdForVaultPetSelection(file.path, selectionContext);
|
|
return {
|
|
filePath: file.path,
|
|
fileName: file.basename || file.name || basenameVaultPath(file.path),
|
|
absolutePath,
|
|
selectedText,
|
|
cwd,
|
|
selectionContext,
|
|
};
|
|
}
|
|
|
|
getActiveSelectedText() {
|
|
const activeViewContainer = this.app.workspace.activeLeaf?.view?.containerEl;
|
|
const nativeSelection = document.getSelection?.();
|
|
if (nativeSelection && !nativeSelection.isCollapsed) {
|
|
const selectedText = String(nativeSelection.toString() || "");
|
|
if (selectedText.trim()) {
|
|
const anchorNode = nativeSelection.anchorNode;
|
|
const focusNode = nativeSelection.focusNode;
|
|
const anchorElement = anchorNode?.nodeType === Node.ELEMENT_NODE ? anchorNode : anchorNode?.parentElement;
|
|
const focusElement = focusNode?.nodeType === Node.ELEMENT_NODE ? focusNode : focusNode?.parentElement;
|
|
if (
|
|
!activeViewContainer
|
|
|| (anchorElement && activeViewContainer.contains(anchorElement))
|
|
|| (focusElement && activeViewContainer.contains(focusElement))
|
|
) {
|
|
return selectedText;
|
|
}
|
|
}
|
|
}
|
|
|
|
const editorSelection = this.app.workspace.activeLeaf?.view?.editor?.getSelection?.();
|
|
return String(editorSelection || "");
|
|
}
|
|
|
|
buildVaultPetSelectionContext({ file, selectedText, absolutePath }) {
|
|
const filePath = String(file?.path || "").trim();
|
|
const selectionText = String(selectedText || "").trim();
|
|
const selectionId = `vault-pet-${shortHash(`${filePath}\n${selectionText}\n${Date.now()}`)}`;
|
|
return {
|
|
canvasPath: filePath,
|
|
canvasName: file?.basename || file?.name || basenameVaultPath(filePath),
|
|
nodeIds: [selectionId],
|
|
textBlocks: [
|
|
{
|
|
id: selectionId,
|
|
text: selectionText,
|
|
},
|
|
],
|
|
markdownFiles: [
|
|
{
|
|
id: `file-${shortHash(filePath)}`,
|
|
path: filePath,
|
|
absolutePath: String(absolutePath || ""),
|
|
name: file?.name || basenameVaultPath(filePath),
|
|
content: "",
|
|
},
|
|
],
|
|
imageFiles: [],
|
|
warnings: [],
|
|
title: `${file?.basename || basenameVaultPath(filePath) || "Markdown"} selection`,
|
|
};
|
|
}
|
|
|
|
getDefaultCwdForVaultPetSelection(filePath, selectionContext) {
|
|
const workspace = this.getWorkspaceForVaultPath(filePath);
|
|
return workspace?.repoPath
|
|
|| this.resolveWorkspaceRepoPathForSelection(selectionContext)
|
|
|| this.resolver.inferWorkingDirectory(selectionContext)
|
|
|| this.getVaultBasePath();
|
|
}
|
|
|
|
getVaultPetQuickChatContext(options = {}) {
|
|
const preferCanvas = options.preferCanvas === true;
|
|
const preferVaultCanvas = options.preferVaultCanvas === true;
|
|
const vaultCanvasFile = preferVaultCanvas ? this.getVaultRootCanvasFile() : null;
|
|
const explicitFile = options.file instanceof TFile ? options.file : null;
|
|
const activeLeafFile = this.app.workspace.activeLeaf?.view?.file;
|
|
const activeFile = activeLeafFile instanceof TFile ? activeLeafFile : null;
|
|
const canvasFile = this.resolver.getActiveCanvasView()?.file;
|
|
const activeCanvasFile = canvasFile instanceof TFile ? canvasFile : null;
|
|
const workspaceActiveFile = this.app.workspace.getActiveFile();
|
|
const file = explicitFile
|
|
|| vaultCanvasFile
|
|
|| (preferCanvas ? activeCanvasFile : activeFile)
|
|
|| workspaceActiveFile
|
|
|| activeCanvasFile
|
|
|| activeFile;
|
|
const filePath = file instanceof TFile ? String(file.path || "").trim() : "";
|
|
const vaultBasePath = this.getVaultBasePath();
|
|
const absolutePath = filePath && vaultBasePath ? path.resolve(vaultBasePath, filePath) : "";
|
|
const selectionContext = this.buildVaultPetQuickChatSelectionContext({
|
|
file,
|
|
filePath,
|
|
absolutePath,
|
|
});
|
|
const cwd = this.getDefaultCwdForVaultPetSelection(filePath, selectionContext);
|
|
return {
|
|
mode: "quick",
|
|
filePath,
|
|
fileName: file instanceof TFile ? (file.basename || file.name || basenameVaultPath(filePath)) : "",
|
|
absolutePath,
|
|
selectedText: "",
|
|
cwd,
|
|
targetCanvasPath: vaultCanvasFile?.path || "",
|
|
selectionContext,
|
|
};
|
|
}
|
|
|
|
getVaultRootCanvasFile() {
|
|
const rootCanvasFiles = this.app.vault.getFiles()
|
|
.filter((file) => file instanceof TFile && file.extension === "canvas")
|
|
.filter((file) => {
|
|
const filePath = String(file.path || "").trim();
|
|
return filePath && !filePath.includes("/") && !filePath.startsWith(".");
|
|
})
|
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
if (rootCanvasFiles.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const vaultName = String(this.app.vault.getName?.() || "").trim();
|
|
const vaultBaseName = path.basename(this.getVaultBasePath() || "");
|
|
const preferredNames = [
|
|
vaultName ? `${vaultName} Vault.canvas` : "",
|
|
vaultBaseName ? `${vaultBaseName} Vault.canvas` : "",
|
|
vaultName ? `${vaultName}.canvas` : "",
|
|
vaultBaseName ? `${vaultBaseName}.canvas` : "",
|
|
"Vault.canvas",
|
|
DEFAULT_WORKSPACE_CANVAS_FILE,
|
|
]
|
|
.map((name) => name.toLowerCase())
|
|
.filter(Boolean);
|
|
|
|
return rootCanvasFiles.find((file) => preferredNames.includes(basenameVaultPath(file.path).toLowerCase()))
|
|
|| rootCanvasFiles[0];
|
|
}
|
|
|
|
buildVaultPetQuickChatSelectionContext({ file, filePath, absolutePath }) {
|
|
const normalizedFilePath = String(filePath || "").trim();
|
|
const baseTitle = file instanceof TFile
|
|
? (file.basename || basenameVaultPath(normalizedFilePath) || "Vault")
|
|
: "Vault";
|
|
const nodeId = `vault-pet-quick-${shortHash(`${normalizedFilePath}\n${Date.now()}`)}`;
|
|
const text = normalizedFilePath
|
|
? `Quick chat from active file: ${normalizedFilePath}`
|
|
: "Quick chat from Obsidian vault.";
|
|
return {
|
|
canvasPath: normalizedFilePath || "vault-pet/quick-chat",
|
|
canvasName: baseTitle,
|
|
nodeIds: [nodeId],
|
|
textBlocks: [
|
|
{
|
|
id: nodeId,
|
|
text,
|
|
},
|
|
],
|
|
markdownFiles: normalizedFilePath && !/\.canvas$/i.test(normalizedFilePath)
|
|
? [
|
|
{
|
|
id: `file-${shortHash(normalizedFilePath)}`,
|
|
path: normalizedFilePath,
|
|
absolutePath: String(absolutePath || ""),
|
|
name: file?.name || basenameVaultPath(normalizedFilePath),
|
|
content: "",
|
|
},
|
|
]
|
|
: [],
|
|
imageFiles: [],
|
|
warnings: [],
|
|
title: normalizedFilePath ? `${baseTitle} quick chat` : "Vault pet quick chat",
|
|
};
|
|
}
|
|
|
|
async startVaultPetChat(context = {}, message = "") {
|
|
return this.startVaultPetSelectionChat(context, message);
|
|
}
|
|
|
|
async startVaultPetSelectionChat(context = {}, message = "") {
|
|
const rawPrompt = String(message || "").trim();
|
|
if (!rawPrompt) {
|
|
throw new Error("Write a chat message first.");
|
|
}
|
|
const materialized = await this.materializeVaultPetChatOnCanvas(context, {
|
|
message,
|
|
rawPrompt,
|
|
});
|
|
const selectionContext = materialized.selectionContext;
|
|
const cwd = materialized.cwd;
|
|
|
|
await this.activateView();
|
|
const response = await this.api.createTaskFromCanvasSelection(this.withVaultScope({
|
|
...selectionContext,
|
|
cwd,
|
|
forceNewTask: true,
|
|
runtimeConfig: this.buildRuntimeConfigPayload(),
|
|
}));
|
|
const task = this.mergeTask(response.task, { adoptCurrentVault: true });
|
|
this.runtimeIssue = "";
|
|
await this.setActiveTask(task.taskId, { revealInActiveTab: true });
|
|
|
|
if (!task.cwd) {
|
|
new Notice("Set a working directory in the OpenAgent panel to continue.");
|
|
return task;
|
|
}
|
|
|
|
new Notice("Starting a new OpenAgent thread from this selection.");
|
|
await this.api.runTask(task.taskId, {
|
|
rawPrompt,
|
|
transcriptMessage: String(message || "").trim() || rawPrompt,
|
|
forceContext: false,
|
|
runtimeConfig: this.buildRuntimeConfigPayload(),
|
|
});
|
|
await this.refreshTask(task.taskId);
|
|
void this.revealCanvasNode(selectionContext.canvasPath, selectionContext.nodeIds?.[0] || "");
|
|
return this.tasksById[task.taskId] || task;
|
|
}
|
|
|
|
async materializeVaultPetChatOnCanvas(context = {}, options = {}) {
|
|
const rawPrompt = String(options.rawPrompt || "").trim();
|
|
const message = String(options.message || "").trim();
|
|
const sourceSelection = context.selectionContext || this.buildVaultPetSelectionContext({
|
|
file: this.app.workspace.getActiveFile(),
|
|
selectedText: context.selectedText,
|
|
absolutePath: context.absolutePath,
|
|
});
|
|
const requestedCwd = String(context.cwd || "").trim()
|
|
|| this.getDefaultCwdForVaultPetSelection(context.filePath, sourceSelection);
|
|
const cwd = normalizeRepoPath(requestedCwd || this.getVaultBasePath());
|
|
const targetCanvasPath = String(context.targetCanvasPath || "").trim();
|
|
let workspace = null;
|
|
let canvasPath = targetCanvasPath;
|
|
if (canvasPath) {
|
|
const targetCanvasFile = this.app.vault.getAbstractFileByPath(canvasPath);
|
|
if (!(targetCanvasFile instanceof TFile) || targetCanvasFile.extension !== "canvas") {
|
|
throw new Error(`Canvas file not found: ${canvasPath}`);
|
|
}
|
|
} else {
|
|
workspace = await this.ensureWorkspaceForRepoPath(cwd, path.basename(cwd || "Vault"));
|
|
canvasPath = joinVaultPath(workspace.folderPath, workspace.defaultCanvas || DEFAULT_WORKSPACE_CANVAS_FILE);
|
|
}
|
|
const sourceFilePath = String(context.filePath || "").trim();
|
|
const absolutePath = String(context.absolutePath || "").trim();
|
|
const title = context.mode === "quick"
|
|
? "Vault pet quick chat"
|
|
: "Vault pet selection chat";
|
|
const displayText = String(message || "").trim();
|
|
const node = await this.appendVaultPetPromptNodeToCanvas(canvasPath, {
|
|
title,
|
|
text: displayText,
|
|
});
|
|
const markdownFiles = sourceFilePath && !/\.canvas$/i.test(sourceFilePath)
|
|
? [
|
|
{
|
|
id: `file-${shortHash(sourceFilePath)}`,
|
|
path: sourceFilePath,
|
|
absolutePath,
|
|
name: context.fileName || basenameVaultPath(sourceFilePath),
|
|
content: "",
|
|
},
|
|
]
|
|
: [];
|
|
|
|
return {
|
|
cwd: workspace?.repoPath || cwd,
|
|
selectionContext: this.withVaultScope({
|
|
canvasPath,
|
|
canvasName: basenameVaultPath(canvasPath).replace(/\.canvas$/i, ""),
|
|
nodeIds: [node.id],
|
|
textBlocks: [
|
|
{
|
|
id: node.id,
|
|
text: rawPrompt || displayText,
|
|
},
|
|
],
|
|
markdownFiles,
|
|
imageFiles: [],
|
|
warnings: [],
|
|
title: displayText.slice(0, 80) || title,
|
|
}),
|
|
};
|
|
}
|
|
|
|
async appendVaultPetPromptNodeToCanvas(canvasPath, options = {}) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
if (!normalizedCanvasPath) {
|
|
throw new Error("Vault pet chat is missing a target Canvas.");
|
|
}
|
|
|
|
return this.withSerializedCanvasMutation(normalizedCanvasPath, async () => {
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(normalizedCanvasPath);
|
|
if (!(abstractFile instanceof TFile) || abstractFile.extension !== "canvas") {
|
|
throw new Error(`Canvas file not found: ${normalizedCanvasPath}`);
|
|
}
|
|
|
|
const raw = await this.app.vault.cachedRead(abstractFile);
|
|
let parsed;
|
|
try {
|
|
parsed = raw.trim() ? JSON.parse(raw) : { nodes: [], edges: [] };
|
|
} catch {
|
|
throw new Error(`Unable to parse canvas JSON for ${normalizedCanvasPath}`);
|
|
}
|
|
|
|
const nodes = Array.isArray(parsed?.nodes) ? [...parsed.nodes] : [];
|
|
const edges = Array.isArray(parsed?.edges) ? [...parsed.edges] : [];
|
|
const text = String(options.text || "").trim() || "Vault pet chat";
|
|
const width = RESULT_NODE_DEFAULT_WIDTH;
|
|
const height = FOLLOW_UP_NODE_DEFAULT_HEIGHT;
|
|
const maxBottom = nodes.reduce((maxValue, node) => {
|
|
const nodeY = toFiniteNumber(node?.y, 80);
|
|
const nodeHeight = toFiniteNumber(node?.height, FOLLOW_UP_NODE_DEFAULT_HEIGHT);
|
|
return Math.max(maxValue, nodeY + nodeHeight);
|
|
}, CANVAS_LAYOUT_BASE_Y - RESULT_NODE_Y_GAP);
|
|
const node = {
|
|
id: createCanvasObjectId("oa-pet-chat"),
|
|
type: "text",
|
|
x: 80,
|
|
y: maxBottom + RESULT_NODE_Y_GAP,
|
|
width,
|
|
height,
|
|
text,
|
|
};
|
|
nodes.push(node);
|
|
|
|
const nextParsed = {
|
|
...parsed,
|
|
nodes,
|
|
edges,
|
|
};
|
|
await this.app.vault.modify(abstractFile, JSON.stringify(nextParsed, null, 2) + "\n");
|
|
this.primeCanvasSnapshot(normalizedCanvasPath, nextParsed);
|
|
return node;
|
|
});
|
|
}
|
|
|
|
getActiveSelectionClientRect() {
|
|
return this.getNativeSelectionClientRect()
|
|
|| this.getCodeMirrorSelectionClientRect()
|
|
|| this.getFocusedElementClientRect();
|
|
}
|
|
|
|
getNativeSelectionClientRect() {
|
|
const selection = document.getSelection?.();
|
|
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
|
|
return null;
|
|
}
|
|
const activeViewContainer = this.app.workspace.activeLeaf?.view?.containerEl;
|
|
const rects = [];
|
|
for (let index = 0; index < selection.rangeCount; index += 1) {
|
|
const range = selection.getRangeAt(index);
|
|
if (
|
|
activeViewContainer
|
|
&& range.commonAncestorContainer
|
|
&& !activeViewContainer.contains(
|
|
range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
|
|
? range.commonAncestorContainer
|
|
: range.commonAncestorContainer.parentElement
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
rects.push(...Array.from(range.getClientRects()));
|
|
const boundingRect = range.getBoundingClientRect();
|
|
if (isUsableClientRect(boundingRect)) {
|
|
rects.push(boundingRect);
|
|
}
|
|
}
|
|
return combineClientRects(rects);
|
|
}
|
|
|
|
getCodeMirrorSelectionClientRect() {
|
|
const activeViewContainer = this.app.workspace.activeLeaf?.view?.containerEl;
|
|
if (!activeViewContainer) {
|
|
return null;
|
|
}
|
|
return combineClientRects(
|
|
Array.from(activeViewContainer.querySelectorAll(".cm-selectionBackground"))
|
|
.map((element) => element.getBoundingClientRect())
|
|
);
|
|
}
|
|
|
|
getFocusedElementClientRect() {
|
|
const activeViewContainer = this.app.workspace.activeLeaf?.view?.containerEl;
|
|
const activeElement = document.activeElement;
|
|
if (!activeViewContainer || !activeElement || !activeViewContainer.contains(activeElement)) {
|
|
return null;
|
|
}
|
|
const rect = activeElement.getBoundingClientRect();
|
|
return isUsableClientRect(rect) ? rect : null;
|
|
}
|
|
|
|
resetVaultPetPosition() {
|
|
if (!this.uiState) {
|
|
return;
|
|
}
|
|
this.uiState.vaultPetState = {};
|
|
if (!this.vaultPet && this.isVaultPetEnabled()) {
|
|
this.configureVaultPet();
|
|
}
|
|
this.vaultPet?.resetPosition();
|
|
}
|
|
|
|
handleVaultPetActivity(kind, file) {
|
|
if (!this.isVaultPetEnabled()) {
|
|
return;
|
|
}
|
|
this.vaultPet?.handleVaultActivity(kind, file);
|
|
}
|
|
|
|
handleVaultPetWorkspaceChange() {
|
|
if (!this.isVaultPetEnabled()) {
|
|
return;
|
|
}
|
|
this.vaultPet?.handleWorkspaceChange();
|
|
}
|
|
|
|
ensureCanvasPopupMenuPatches() {
|
|
this.app.workspace.getLeavesOfType("canvas").forEach((leaf) => {
|
|
this.ensureCanvasPopupMenuPatched(leaf?.view);
|
|
});
|
|
}
|
|
|
|
ensureCanvasSelectionTrackingPatches() {
|
|
this.app.workspace.getLeavesOfType("canvas").forEach((leaf) => {
|
|
this.ensureCanvasSelectionTrackingPatched(leaf?.view);
|
|
});
|
|
}
|
|
|
|
ensureCanvasSelectionTrackingPatched(view) {
|
|
const canvas = view?.canvas;
|
|
if (!canvas || typeof canvas !== "object") {
|
|
return false;
|
|
}
|
|
|
|
if (canvas[CANVAS_SELECTION_TRACKING_PATCH_SYMBOL]) {
|
|
return true;
|
|
}
|
|
|
|
const unpatches = [];
|
|
const scheduleCapture = () => {
|
|
this.scheduleCanvasSelectionSnapshotCapture(view);
|
|
};
|
|
const patchTargetMethod = (target, methodName) => {
|
|
const unpatch = patchObjectMethod(target, methodName, (original) => {
|
|
return function patchedCanvasSelectionMethod(...args) {
|
|
const result = original.apply(this, args);
|
|
scheduleCapture();
|
|
return result;
|
|
};
|
|
});
|
|
if (!unpatch) {
|
|
return false;
|
|
}
|
|
|
|
unpatches.push(unpatch);
|
|
return true;
|
|
};
|
|
|
|
let patched = false;
|
|
[
|
|
[canvas, "updateSelection"],
|
|
[canvas, "selectOnly"],
|
|
[canvas, "deselectAll"],
|
|
[canvas?.selectionManager, "updateSelection"],
|
|
[canvas?.selectionManager, "select"],
|
|
[canvas?.selectionManager, "selectOnly"],
|
|
[canvas?.selectionManager, "deselectAll"],
|
|
[canvas?.selectionManager, "clear"],
|
|
[canvas?.selectionManager, "setSelection"],
|
|
].forEach(([target, methodName]) => {
|
|
patched = patchTargetMethod(target, methodName) || patched;
|
|
});
|
|
|
|
if (!patched) {
|
|
return false;
|
|
}
|
|
|
|
const cleanup = () => {
|
|
unpatches.splice(0).reverse().forEach((unpatch) => {
|
|
try {
|
|
unpatch();
|
|
} catch {
|
|
// Ignore cleanup failures during plugin unload or hot reload.
|
|
}
|
|
});
|
|
delete canvas[CANVAS_SELECTION_TRACKING_PATCH_SYMBOL];
|
|
};
|
|
|
|
canvas[CANVAS_SELECTION_TRACKING_PATCH_SYMBOL] = true;
|
|
this.register(cleanup);
|
|
return true;
|
|
}
|
|
|
|
ensureCanvasPopupMenuPatched(view) {
|
|
const menu = view?.canvas?.menu;
|
|
if (!menu || typeof menu.render !== "function") {
|
|
return false;
|
|
}
|
|
|
|
if (this.canvasPopupMenuPatchStateByMenu.has(menu)) {
|
|
this.syncCanvasPopupActionButtons(view);
|
|
return true;
|
|
}
|
|
|
|
const originalRender = menu.render.bind(menu);
|
|
menu.render = (...args) => {
|
|
const result = originalRender(...args);
|
|
queueMicrotask(() => {
|
|
this.syncCanvasPopupActionButtons(view);
|
|
});
|
|
return result;
|
|
};
|
|
|
|
this.canvasPopupMenuPatchStateByMenu.set(menu, originalRender);
|
|
this.syncCanvasPopupActionButtons(view);
|
|
return true;
|
|
}
|
|
|
|
restoreCanvasPopupMenuPatches() {
|
|
this.canvasPopupMenuPatchStateByMenu.forEach((originalRender, menu) => {
|
|
if (menu && typeof originalRender === "function") {
|
|
menu.render = originalRender;
|
|
}
|
|
});
|
|
this.canvasPopupMenuPatchStateByMenu.clear();
|
|
}
|
|
|
|
syncCanvasPopupActionButtons(view) {
|
|
const menuEl = view?.canvas?.menu?.menuEl;
|
|
if (!menuEl) {
|
|
return false;
|
|
}
|
|
|
|
const selectedNodeIds = this.resolver.extractSelectedNodeIds(view);
|
|
const shouldShow = selectedNodeIds.length === 1;
|
|
const selectedCanvasPath = String(view?.file?.path || this.app.workspace.getActiveFile()?.path || "").trim();
|
|
const selectedNode = shouldShow && selectedCanvasPath
|
|
? this.getCanvasSnapshotSync(selectedCanvasPath)?.nodeById?.get(String(selectedNodeIds[0] || "").trim()) || null
|
|
: null;
|
|
const shouldShowFork = shouldShow && isOpenAgentAssistantResultNode(selectedNode);
|
|
const existingNewThreadButton = menuEl.querySelector(`#${CANVAS_NEW_THREAD_BUTTON_ID}`);
|
|
const existingFollowUpButton = menuEl.querySelector(`#${CANVAS_FOLLOW_UP_NODE_BUTTON_ID}`);
|
|
const existingForkButton = menuEl.querySelector(`#${CANVAS_FORK_NODE_BUTTON_ID}`);
|
|
|
|
if (!shouldShow) {
|
|
existingNewThreadButton?.remove();
|
|
existingFollowUpButton?.remove();
|
|
existingForkButton?.remove();
|
|
return false;
|
|
}
|
|
|
|
if (!shouldShowFork) {
|
|
existingForkButton?.remove();
|
|
}
|
|
|
|
if (!existingNewThreadButton) {
|
|
const newThreadButton = document.createElement("button");
|
|
newThreadButton.id = CANVAS_NEW_THREAD_BUTTON_ID;
|
|
newThreadButton.classList.add("clickable-icon");
|
|
newThreadButton.setAttribute("type", "button");
|
|
newThreadButton.setAttribute("aria-label", "New thread");
|
|
setIcon(newThreadButton, "play");
|
|
setTooltip(newThreadButton, "New thread", { placement: "top" });
|
|
newThreadButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
void this.handleNewThreadFromSelectionCommand();
|
|
});
|
|
|
|
menuEl.appendChild(newThreadButton);
|
|
}
|
|
|
|
if (!existingFollowUpButton) {
|
|
const followUpButton = document.createElement("button");
|
|
followUpButton.id = CANVAS_FOLLOW_UP_NODE_BUTTON_ID;
|
|
followUpButton.classList.add("clickable-icon");
|
|
followUpButton.setAttribute("type", "button");
|
|
followUpButton.setAttribute("aria-label", "Create follow-up node");
|
|
setIcon(followUpButton, "message-square");
|
|
setTooltip(followUpButton, "Create follow-up node", { placement: "top" });
|
|
followUpButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
void this.handleCreateFollowUpNodeCommand();
|
|
});
|
|
|
|
menuEl.appendChild(followUpButton);
|
|
}
|
|
|
|
if (shouldShowFork && !existingForkButton) {
|
|
const forkButton = document.createElement("button");
|
|
forkButton.id = CANVAS_FORK_NODE_BUTTON_ID;
|
|
forkButton.classList.add("clickable-icon");
|
|
forkButton.setAttribute("type", "button");
|
|
forkButton.setAttribute("aria-label", "Create fork node");
|
|
setIcon(forkButton, "git-fork");
|
|
setTooltip(forkButton, "Create fork node", { placement: "top" });
|
|
forkButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
void this.handleCreateForkNodeCommand();
|
|
});
|
|
|
|
if (existingFollowUpButton?.nextSibling) {
|
|
menuEl.insertBefore(forkButton, existingFollowUpButton.nextSibling);
|
|
} else {
|
|
menuEl.appendChild(forkButton);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
getSelectedSandboxMode() {
|
|
return normalizeDaemonSandboxMode(this.settings?.daemonSandboxMode);
|
|
}
|
|
|
|
buildRuntimeConfigPayload(overrides = {}) {
|
|
return {
|
|
approvalPolicy: "never",
|
|
sandboxMode: normalizeDaemonSandboxMode(overrides?.sandboxMode || this.getSelectedSandboxMode()),
|
|
};
|
|
}
|
|
|
|
describeRuntimeConfig(runtimeConfig) {
|
|
const sandboxMode = normalizeDaemonSandboxMode(runtimeConfig?.sandboxMode || this.getSelectedSandboxMode());
|
|
const label = getDaemonSandboxModeLabel(sandboxMode);
|
|
return sandboxMode === DAEMON_SANDBOX_MODE_OPTIONS.DANGER_FULL_ACCESS
|
|
? `${label} - can access files outside the selected folder`
|
|
: `${label} - limited to the selected folder`;
|
|
}
|
|
|
|
getPluginLogoDataUrl() {
|
|
return this.logoDataUrl;
|
|
}
|
|
|
|
getVaultPetMascotDataUrl() {
|
|
if (this.vaultPetMascotDataUrl) {
|
|
return this.vaultPetMascotDataUrl;
|
|
}
|
|
|
|
const mascotPath = this.getVaultPetMascotPath();
|
|
if (!mascotPath) {
|
|
return DEFAULT_VAULT_PET_MASCOT_DATA_URL || this.getPluginLogoDataUrl();
|
|
}
|
|
|
|
try {
|
|
this.vaultPetMascotDataUrl = `data:image/png;base64,${fs.readFileSync(mascotPath).toString("base64")}`;
|
|
return this.vaultPetMascotDataUrl;
|
|
} catch {
|
|
return DEFAULT_VAULT_PET_MASCOT_DATA_URL || this.getPluginLogoDataUrl();
|
|
}
|
|
}
|
|
|
|
getVaultPetMascotPath() {
|
|
const candidatePaths = [
|
|
this.getManifestPluginPath(VAULT_PET_MASCOT_FILE_NAME),
|
|
VAULT_PET_MASCOT_PATH,
|
|
].filter(Boolean);
|
|
|
|
return candidatePaths.find((candidatePath) => fs.existsSync(candidatePath)) || "";
|
|
}
|
|
|
|
async ensurePluginLogoDataUrlLoaded() {
|
|
if (this.logoDataUrl) {
|
|
return this.logoDataUrl;
|
|
}
|
|
|
|
if (this.logoLoadPromise) {
|
|
return this.logoLoadPromise;
|
|
}
|
|
|
|
const logoPath = this.getPluginLogoPath();
|
|
if (!logoPath) {
|
|
this.logoDataUrl = DEFAULT_PLUGIN_LOGO_DATA_URL;
|
|
return this.logoDataUrl;
|
|
}
|
|
|
|
this.logoLoadPromise = fs.promises.readFile(logoPath)
|
|
.then((buffer) => {
|
|
this.logoDataUrl = `data:image/png;base64,${buffer.toString("base64")}`;
|
|
this.requestViewRefresh();
|
|
return this.logoDataUrl;
|
|
})
|
|
.catch(() => DEFAULT_PLUGIN_LOGO_DATA_URL)
|
|
.finally(() => {
|
|
this.logoLoadPromise = null;
|
|
});
|
|
|
|
return this.logoLoadPromise;
|
|
}
|
|
|
|
getPluginLogoPath() {
|
|
const candidatePaths = [
|
|
this.getManifestPluginPath(PLUGIN_LOGO_FILE_NAME),
|
|
PLUGIN_LOGO_PATH,
|
|
].filter(Boolean);
|
|
|
|
return candidatePaths.find((candidatePath) => fs.existsSync(candidatePath)) || "";
|
|
}
|
|
|
|
getManifestPluginPath(fileName) {
|
|
const manifestDir = String(this.manifest?.dir || "").trim();
|
|
if (!manifestDir) {
|
|
return "";
|
|
}
|
|
|
|
const adapter = this.app.vault?.adapter;
|
|
if (typeof adapter?.getBasePath !== "function") {
|
|
return "";
|
|
}
|
|
|
|
return path.join(adapter.getBasePath(), manifestDir, fileName);
|
|
}
|
|
|
|
refreshSettingTab() {
|
|
if (!this.settingTab?.containerEl?.isConnected || typeof this.settingTab.display !== "function") {
|
|
return;
|
|
}
|
|
|
|
this.settingTab.display();
|
|
}
|
|
|
|
getStoredDaemonConfigSummary() {
|
|
try {
|
|
if (!fs.existsSync(DAEMON_CONFIG_PATH)) {
|
|
return {
|
|
host: "",
|
|
port: "",
|
|
};
|
|
}
|
|
|
|
const parsed = JSON.parse(fs.readFileSync(DAEMON_CONFIG_PATH, "utf8"));
|
|
return {
|
|
host: String(parsed?.host || "").trim(),
|
|
port: String(parsed?.port || "").trim(),
|
|
};
|
|
} catch {
|
|
return {
|
|
host: "",
|
|
port: "",
|
|
};
|
|
}
|
|
}
|
|
|
|
updateDaemonStatus(nextStatus, options = {}) {
|
|
const previousStatus = this.daemonStatus || createDaemonStatusSnapshot();
|
|
const normalizedStatus = createDaemonStatusSnapshot(nextStatus);
|
|
const didChange = [
|
|
"state",
|
|
"host",
|
|
"port",
|
|
"error",
|
|
"runtimeAvailable",
|
|
"runtimeMessage",
|
|
"lastRuntimeError",
|
|
].some((key) => previousStatus[key] !== normalizedStatus[key]);
|
|
this.daemonStatus = normalizedStatus;
|
|
|
|
if (didChange || options.forceRefresh === true) {
|
|
this.requestViewRefresh();
|
|
this.refreshSettingTab();
|
|
}
|
|
}
|
|
|
|
getDaemonStatus() {
|
|
return this.daemonStatus || createDaemonStatusSnapshot();
|
|
}
|
|
|
|
getDaemonStatusLabel() {
|
|
const state = this.getDaemonStatus().state;
|
|
if (state === DAEMON_CONNECTION_STATES.ONLINE) {
|
|
return "Online";
|
|
}
|
|
if (state === DAEMON_CONNECTION_STATES.OFFLINE) {
|
|
return "Offline";
|
|
}
|
|
if (state === DAEMON_CONNECTION_STATES.CHECKING) {
|
|
return "Checking...";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
getDaemonStatusTagClassName() {
|
|
return getDaemonStatusTagClassName(this.getDaemonStatus().state);
|
|
}
|
|
|
|
getDaemonStatusDetail() {
|
|
const status = this.getDaemonStatus();
|
|
if (status.state === DAEMON_CONNECTION_STATES.ONLINE) {
|
|
if (status.runtimeAvailable === false) {
|
|
return status.runtimeMessage || "Connected, but Codex runtime needs attention.";
|
|
}
|
|
return "Connected to local Codex server.";
|
|
}
|
|
if (status.state === DAEMON_CONNECTION_STATES.OFFLINE) {
|
|
return formatDaemonStatusError(status.error);
|
|
}
|
|
if (status.state === DAEMON_CONNECTION_STATES.CHECKING) {
|
|
return "Checking local Codex server...";
|
|
}
|
|
return "Status has not been checked yet.";
|
|
}
|
|
|
|
async refreshDaemonStatus(options = {}) {
|
|
if (this.daemonStatusRefreshPromise) {
|
|
if (options.forceRefresh === true) {
|
|
this.requestViewRefresh();
|
|
this.refreshSettingTab();
|
|
}
|
|
return this.daemonStatusRefreshPromise;
|
|
}
|
|
|
|
const storedConfig = this.getStoredDaemonConfigSummary();
|
|
if (options.silent !== true) {
|
|
this.updateDaemonStatus({
|
|
...this.getDaemonStatus(),
|
|
...storedConfig,
|
|
state: DAEMON_CONNECTION_STATES.CHECKING,
|
|
error: "",
|
|
}, {
|
|
forceRefresh: true,
|
|
});
|
|
}
|
|
|
|
this.daemonStatusRefreshPromise = this.api.getHealthOnce({
|
|
reloadConfig: true,
|
|
})
|
|
.then((health) => {
|
|
this.updateDaemonStatus({
|
|
state: DAEMON_CONNECTION_STATES.ONLINE,
|
|
checkedAt: Date.now(),
|
|
host: String(health?.daemon?.host || storedConfig.host || "").trim(),
|
|
port: String(health?.daemon?.port || storedConfig.port || "").trim(),
|
|
error: "",
|
|
runtimeAvailable: typeof health?.runtime?.ok === "boolean" ? health.runtime.ok : null,
|
|
runtimeMessage: String(health?.runtime?.message || "").trim(),
|
|
lastRuntimeError: String(health?.lastRuntimeError || "").trim(),
|
|
}, {
|
|
forceRefresh: options.forceRefresh === true,
|
|
});
|
|
return this.getDaemonStatus();
|
|
})
|
|
.catch((error) => {
|
|
this.updateDaemonStatus({
|
|
state: DAEMON_CONNECTION_STATES.OFFLINE,
|
|
checkedAt: Date.now(),
|
|
host: storedConfig.host,
|
|
port: storedConfig.port,
|
|
error: String(error?.message || error),
|
|
runtimeAvailable: null,
|
|
runtimeMessage: "",
|
|
lastRuntimeError: "",
|
|
}, {
|
|
forceRefresh: options.forceRefresh === true || options.silent !== true,
|
|
});
|
|
return this.getDaemonStatus();
|
|
})
|
|
.finally(() => {
|
|
this.daemonStatusRefreshPromise = null;
|
|
});
|
|
|
|
return this.daemonStatusRefreshPromise;
|
|
}
|
|
|
|
async startDaemonWithFeedback() {
|
|
try {
|
|
await this.daemonLauncher.ensureStarted();
|
|
this.runtimeIssue = "";
|
|
await this.refreshDaemonStatus({
|
|
silent: false,
|
|
forceRefresh: true,
|
|
});
|
|
new Notice("Codex server is running.");
|
|
return true;
|
|
} catch (error) {
|
|
this.runtimeIssue = String(error?.message || error);
|
|
await this.refreshDaemonStatus({
|
|
silent: false,
|
|
forceRefresh: true,
|
|
}).catch(() => {});
|
|
new Notice(this.runtimeIssue);
|
|
this.requestViewRefresh();
|
|
this.refreshSettingTab();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async pickDirectoryPath(options = {}) {
|
|
const defaultPath = normalizeRepoPath(options.defaultPath) || os.homedir();
|
|
const title = String(options.title || "Choose folder");
|
|
const candidates = [];
|
|
|
|
try {
|
|
const electron = require("electron");
|
|
if (electron?.remote?.dialog) {
|
|
candidates.push(electron.remote.dialog);
|
|
}
|
|
} catch {
|
|
// Ignore unavailable electron remote module.
|
|
}
|
|
|
|
try {
|
|
const remote = require("@electron/remote");
|
|
if (remote?.dialog) {
|
|
candidates.push(remote.dialog);
|
|
}
|
|
} catch {
|
|
// Ignore unavailable @electron/remote module.
|
|
}
|
|
|
|
for (const dialog of candidates) {
|
|
if (typeof dialog?.showOpenDialogSync === "function") {
|
|
const filePaths = dialog.showOpenDialogSync({
|
|
title,
|
|
defaultPath,
|
|
properties: ["openDirectory", "createDirectory"],
|
|
});
|
|
if (Array.isArray(filePaths) && filePaths[0]) {
|
|
return normalizeRepoPath(filePaths[0]);
|
|
}
|
|
}
|
|
|
|
if (typeof dialog?.showOpenDialog === "function") {
|
|
const result = await dialog.showOpenDialog({
|
|
title,
|
|
defaultPath,
|
|
properties: ["openDirectory", "createDirectory"],
|
|
});
|
|
if (result && !result.canceled && Array.isArray(result.filePaths) && result.filePaths[0]) {
|
|
return normalizeRepoPath(result.filePaths[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error("Folder picker is not available in this Obsidian runtime yet. Paste the repo path for now.");
|
|
}
|
|
|
|
showWorkspacePicker() {
|
|
new WorkspacePickerModal(this.app, this).open();
|
|
}
|
|
|
|
getVaultBasePath() {
|
|
return this.resolver?.getVaultBasePath?.() || "";
|
|
}
|
|
|
|
getVaultScope() {
|
|
const vaultId = String(this.vaultId || "").trim();
|
|
const vaultBasePath = this.getVaultBasePath();
|
|
return {
|
|
scopeType: "obsidian-vault",
|
|
vaultId,
|
|
vaultName: String(this.app.vault?.getName?.() || "").trim(),
|
|
vaultPathHash: vaultBasePath ? stableShortHash(path.resolve(vaultBasePath)) : "",
|
|
};
|
|
}
|
|
|
|
withVaultScope(payload = {}) {
|
|
const vaultScope = this.getVaultScope();
|
|
return {
|
|
...payload,
|
|
vaultScope,
|
|
vaultId: vaultScope.vaultId,
|
|
vaultName: vaultScope.vaultName,
|
|
vaultPathHash: vaultScope.vaultPathHash,
|
|
};
|
|
}
|
|
|
|
isTaskForCurrentVault(task) {
|
|
const currentVaultId = String(this.vaultId || "").trim();
|
|
if (!currentVaultId) {
|
|
return false;
|
|
}
|
|
|
|
return this.getTaskVaultId(task) === currentVaultId;
|
|
}
|
|
|
|
getTaskVaultId(task) {
|
|
return String(task?.vaultScope?.vaultId || task?.selectionContext?.vaultId || "").trim();
|
|
}
|
|
|
|
scopeTaskForCurrentVault(task) {
|
|
if (!task) {
|
|
return null;
|
|
}
|
|
|
|
const scopedSelectionContext = task.selectionContext
|
|
? this.withVaultScope(task.selectionContext)
|
|
: task.selectionContext;
|
|
const scopedTask = this.withVaultScope({
|
|
...task,
|
|
selectionContext: scopedSelectionContext,
|
|
});
|
|
return scopedTask;
|
|
}
|
|
|
|
getVaultPathForAbsolutePath(absolutePath) {
|
|
const normalizedAbsolutePath = String(absolutePath || "").trim();
|
|
const vaultBasePath = this.getVaultBasePath();
|
|
if (!normalizedAbsolutePath || !path.isAbsolute(normalizedAbsolutePath) || !vaultBasePath) {
|
|
return "";
|
|
}
|
|
|
|
const targetPath = normalizeFilesystemPathForCompare(normalizedAbsolutePath);
|
|
const normalizedVaultBasePath = normalizeFilesystemPathForCompare(vaultBasePath);
|
|
if (targetPath === normalizedVaultBasePath || targetPath.startsWith(`${normalizedVaultBasePath}/`)) {
|
|
const directVaultPath = normalizePromptPath(path.relative(vaultBasePath, normalizedAbsolutePath));
|
|
const directFile = this.app.vault.getAbstractFileByPath(directVaultPath);
|
|
if (directFile instanceof TFile) {
|
|
return directFile.path;
|
|
}
|
|
}
|
|
|
|
const targetRealpath = safeRealpathSync(normalizedAbsolutePath);
|
|
const targetComparablePath = targetRealpath
|
|
? normalizeFilesystemPathForCompare(targetRealpath)
|
|
: targetPath;
|
|
|
|
for (const file of this.app.vault.getFiles()) {
|
|
const fileAbsolutePath = this.getVaultAbsolutePath(file.path);
|
|
if (!fileAbsolutePath) {
|
|
continue;
|
|
}
|
|
|
|
const fileRealpath = safeRealpathSync(fileAbsolutePath);
|
|
const fileComparablePath = fileRealpath
|
|
? normalizeFilesystemPathForCompare(fileRealpath)
|
|
: normalizeFilesystemPathForCompare(fileAbsolutePath);
|
|
if (fileComparablePath === targetComparablePath) {
|
|
return file.path;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
normalizeAssistantLinksForObsidian(text) {
|
|
return rewriteMarkdownLinks(text, (rawTarget) => {
|
|
const parsed = parseLocalMarkdownLinkTarget(rawTarget);
|
|
if (!parsed) {
|
|
return "";
|
|
}
|
|
|
|
const vaultPath = this.getVaultPathForAbsolutePath(parsed.localPath);
|
|
if (vaultPath) {
|
|
return `${encodeMarkdownLinkPath(vaultPath)}${parsed.fragment || ""}`;
|
|
}
|
|
|
|
return pathToFileURL(parsed.localPath).href;
|
|
});
|
|
}
|
|
|
|
getVaultAbsolutePath(vaultPath) {
|
|
const vaultBasePath = this.getVaultBasePath();
|
|
if (!vaultBasePath) {
|
|
return "";
|
|
}
|
|
return path.join(vaultBasePath, String(vaultPath || "").replace(/^\/+|\/+$/g, ""));
|
|
}
|
|
|
|
getWorkspaceRootVaultPath() {
|
|
return String(this.settings.workspaceRoot || DEFAULT_SETTINGS.workspaceRoot).trim() || DEFAULT_SETTINGS.workspaceRoot;
|
|
}
|
|
|
|
getRepoWorkspaceDirectory(repoPath) {
|
|
return path.join(repoPath, REPO_WORKSPACE_DIR_NAME);
|
|
}
|
|
|
|
getWorkspaceConfigFiles() {
|
|
const workspaceRoot = this.getWorkspaceRootVaultPath();
|
|
return this.app.vault.getFiles().filter((file) => {
|
|
return basenameVaultPath(file.path) === WORKSPACE_CONFIG_FILE_NAME
|
|
&& isVaultPathWithinRoot(file.path, workspaceRoot);
|
|
});
|
|
}
|
|
|
|
isWorkspaceSummariesLoading() {
|
|
return Boolean(this.workspaceSummariesLoadPromise);
|
|
}
|
|
|
|
getWorkspaceSummarySignature(workspace) {
|
|
if (!workspace) {
|
|
return "";
|
|
}
|
|
|
|
return [
|
|
workspace.configPath,
|
|
workspace.folderPath,
|
|
workspace.name,
|
|
workspace.repoPath,
|
|
workspace.defaultCanvas,
|
|
].join("|");
|
|
}
|
|
|
|
haveWorkspaceSummariesChanged(nextMap) {
|
|
const previousMap = this.workspaceSummaryCacheByConfigPath;
|
|
if (previousMap.size !== nextMap.size) {
|
|
return true;
|
|
}
|
|
|
|
for (const [configPath, nextSummary] of nextMap.entries()) {
|
|
if (this.getWorkspaceSummarySignature(previousMap.get(configPath)) !== this.getWorkspaceSummarySignature(nextSummary)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async readWorkspaceConfigSummary(file) {
|
|
if (!(file instanceof TFile) || basenameVaultPath(file.path) !== WORKSPACE_CONFIG_FILE_NAME) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(await this.app.vault.cachedRead(file));
|
|
const repoPath = normalizeRepoPath(parsed?.repoPath);
|
|
if (!repoPath) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
configPath: file.path,
|
|
folderPath: dirnameVaultPath(file.path),
|
|
name: String(parsed?.name || basenameVaultPath(dirnameVaultPath(file.path)) || "Workspace").trim(),
|
|
repoPath,
|
|
defaultCanvas: String(parsed?.defaultCanvas || DEFAULT_WORKSPACE_CANVAS_FILE).trim() || DEFAULT_WORKSPACE_CANVAS_FILE,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async ensureWorkspaceSummariesLoaded(options = {}) {
|
|
const force = options.force === true;
|
|
if (!force && this.workspaceSummariesLoaded) {
|
|
return this.listWorkspaceSummaries();
|
|
}
|
|
|
|
if (!force && this.workspaceSummariesLoadPromise) {
|
|
return this.workspaceSummariesLoadPromise;
|
|
}
|
|
|
|
this.workspaceSummariesLoadPromise = (async () => {
|
|
const nextMap = new Map();
|
|
const summaries = await Promise.all(this.getWorkspaceConfigFiles().map((file) => this.readWorkspaceConfigSummary(file)));
|
|
summaries.filter(Boolean).forEach((summary) => {
|
|
nextMap.set(summary.configPath, summary);
|
|
});
|
|
|
|
const changed = this.haveWorkspaceSummariesChanged(nextMap);
|
|
this.workspaceSummaryCacheByConfigPath = nextMap;
|
|
this.workspaceSummariesLoaded = true;
|
|
if (changed || force) {
|
|
this.requestViewRefresh();
|
|
}
|
|
|
|
return this.listWorkspaceSummaries();
|
|
})().finally(() => {
|
|
this.workspaceSummariesLoadPromise = null;
|
|
});
|
|
|
|
return this.workspaceSummariesLoadPromise;
|
|
}
|
|
|
|
handleWorkspaceConfigMutation(file, oldPath = "") {
|
|
const nextPath = file instanceof TFile ? String(file.path || "").trim() : "";
|
|
const normalizedOldPath = String(oldPath || "").trim();
|
|
const workspaceRoot = this.getWorkspaceRootVaultPath();
|
|
if (basenameVaultPath(nextPath) !== WORKSPACE_CONFIG_FILE_NAME && basenameVaultPath(normalizedOldPath) !== WORKSPACE_CONFIG_FILE_NAME) {
|
|
return;
|
|
}
|
|
if (!isVaultPathWithinRoot(nextPath, workspaceRoot) && !isVaultPathWithinRoot(normalizedOldPath, workspaceRoot)) {
|
|
return;
|
|
}
|
|
|
|
this.workspaceSummariesLoaded = false;
|
|
void this.ensureWorkspaceSummariesLoaded({ force: true });
|
|
}
|
|
|
|
getAutomationFolderVaultPaths() {
|
|
const roots = [];
|
|
this.listWorkspaceSummaries().forEach((workspace) => {
|
|
if (workspace?.folderPath) {
|
|
roots.push(joinVaultPath(workspace.folderPath, WORKSPACE_AUTOMATION_DIR_NAME));
|
|
}
|
|
});
|
|
const customFolder = String(this.settings.automationFolder || "").trim();
|
|
if (customFolder) {
|
|
roots.push(customFolder);
|
|
}
|
|
return Array.from(new Set(roots.filter(Boolean)));
|
|
}
|
|
|
|
isAutomationFilePath(vaultPath) {
|
|
const normalizedPath = String(vaultPath || "").trim();
|
|
return normalizedPath.endsWith(".md")
|
|
&& this.getAutomationFolderVaultPaths().some((automationFolder) => isVaultPathWithinRoot(normalizedPath, automationFolder));
|
|
}
|
|
|
|
getAutomationFiles() {
|
|
return this.app.vault.getFiles().filter((file) => this.isAutomationFilePath(file.path));
|
|
}
|
|
|
|
async ensureDefaultAutomationFile() {
|
|
await this.ensureWorkspaceSummariesLoaded();
|
|
const activeWorkspace = this.getActiveWorkspace();
|
|
const workspace = activeWorkspace
|
|
|| this.listWorkspaceSummaries().find((summary) => summary.name === "openagent")
|
|
|| this.listWorkspaceSummaries()[0]
|
|
|| null;
|
|
const automationFolder = workspace?.folderPath
|
|
? joinVaultPath(workspace.folderPath, WORKSPACE_AUTOMATION_DIR_NAME)
|
|
: (String(this.settings.automationFolder || "").trim() || joinVaultPath(this.getWorkspaceRootVaultPath(), "openagent", WORKSPACE_AUTOMATION_DIR_NAME));
|
|
await this.ensureVaultFolderExists(automationFolder);
|
|
const automationPath = joinVaultPath(automationFolder, DEFAULT_DAILY_VAULT_REVIEW_AUTOMATION_FILE);
|
|
if (this.app.vault.getAbstractFileByPath(automationPath)) {
|
|
return;
|
|
}
|
|
|
|
await this.writeVaultFile(automationPath, [
|
|
"---",
|
|
"openagent: automation",
|
|
"id: daily-vault-review",
|
|
"enabled: true",
|
|
"schedule: \"0 18 * * *\"",
|
|
`cwd: "${DEFAULT_DAILY_VAULT_REVIEW_CWD}"`,
|
|
"threadStrategy: resume",
|
|
"approvalPolicy: never",
|
|
"sandboxMode: workspace-write",
|
|
"---",
|
|
"",
|
|
"Use the Codex skill [[Codex Skills/daily-vault-review/SKILL|daily-vault-review]] to create or update today's daily review note.",
|
|
"",
|
|
"Run the normal daily vault review workflow for /Users/applefather/Documents/Applefather.",
|
|
"",
|
|
].join("\n"));
|
|
}
|
|
|
|
async readAutomationDefinition(file) {
|
|
if (!(file instanceof TFile) || !this.isAutomationFilePath(file.path)) {
|
|
return null;
|
|
}
|
|
|
|
const raw = await this.app.vault.cachedRead(file);
|
|
const parsed = parseAutomationFrontmatter(raw);
|
|
if (String(parsed.data.openagent || "").trim() !== "automation") {
|
|
return null;
|
|
}
|
|
|
|
const cwd = String(parsed.data.cwd || "").trim();
|
|
const prompt = String(parsed.body || "").trim();
|
|
const id = String(parsed.data.id || basenameVaultPath(file.path).replace(/\.md$/i, "")).trim();
|
|
return {
|
|
id,
|
|
sourcePath: file.path,
|
|
title: String(parsed.data.title || basenameVaultPath(file.path).replace(/\.md$/i, "")).trim(),
|
|
enabled: parsed.data.enabled !== false,
|
|
schedule: String(parsed.data.schedule || "manual").trim() || "manual",
|
|
cwd,
|
|
prompt,
|
|
threadStrategy: String(parsed.data.threadStrategy || "resume").trim() === "new" ? "new" : "resume",
|
|
runtimeConfig: {
|
|
approvalPolicy: String(parsed.data.approvalPolicy || "never").trim(),
|
|
sandboxMode: normalizeDaemonSandboxMode(parsed.data.sandboxMode || this.getSelectedSandboxMode()),
|
|
},
|
|
};
|
|
}
|
|
|
|
async readAutomationDefinitions() {
|
|
const definitions = await Promise.all(this.getAutomationFiles().map((file) => this.readAutomationDefinition(file)));
|
|
return definitions.filter(Boolean);
|
|
}
|
|
|
|
scheduleAutomationSync() {
|
|
window.clearTimeout(this.automationSyncTimer);
|
|
this.automationSyncTimer = window.setTimeout(() => {
|
|
this.automationSyncTimer = null;
|
|
void this.syncAutomationsToDaemon().catch(() => {});
|
|
}, 750);
|
|
}
|
|
|
|
handleAutomationFileMutation(file, oldPath = "") {
|
|
const nextPath = file instanceof TFile ? String(file.path || "").trim() : "";
|
|
const previousPath = String(oldPath || "").trim();
|
|
if (!this.isAutomationFilePath(nextPath) && !this.isAutomationFilePath(previousPath)) {
|
|
return;
|
|
}
|
|
|
|
this.scheduleAutomationSync();
|
|
}
|
|
|
|
async syncAutomationsToDaemon() {
|
|
if (this.automationSyncPromise) {
|
|
return this.automationSyncPromise;
|
|
}
|
|
|
|
this.automationSyncPromise = (async () => {
|
|
const automations = await this.readAutomationDefinitions();
|
|
await this.api.syncAutomations({
|
|
source: this.getAutomationSyncSource(),
|
|
pruneMissing: true,
|
|
automations,
|
|
});
|
|
return automations;
|
|
})().finally(() => {
|
|
this.automationSyncPromise = null;
|
|
});
|
|
|
|
return this.automationSyncPromise;
|
|
}
|
|
|
|
getAutomationSyncSource() {
|
|
const basePath = String(this.app.vault.adapter?.basePath || "").trim();
|
|
return basePath ? `vault:${basePath}` : "vault";
|
|
}
|
|
|
|
async syncAutomationsWithFeedback() {
|
|
try {
|
|
await this.ensureDefaultAutomationFile();
|
|
const automations = await this.syncAutomationsToDaemon();
|
|
new Notice(`Synced ${automations.length} OpenAgent automation${automations.length === 1 ? "" : "s"}.`);
|
|
} catch (error) {
|
|
new Notice(`Automation sync failed: ${String(error?.message || error)}`);
|
|
}
|
|
}
|
|
|
|
async runAutomationWithFeedback(automationId) {
|
|
try {
|
|
await this.ensureDefaultAutomationFile();
|
|
await this.syncAutomationsToDaemon();
|
|
await this.api.runAutomation(automationId, { force: true });
|
|
new Notice("Daily vault review automation started.");
|
|
} catch (error) {
|
|
new Notice(`Automation run failed: ${String(error?.message || error)}`);
|
|
}
|
|
}
|
|
|
|
listWorkspaceSummaries() {
|
|
return Array.from(this.workspaceSummaryCacheByConfigPath.values())
|
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
}
|
|
|
|
getWorkspaceForVaultPath(vaultPath) {
|
|
const normalizedPath = String(vaultPath || "").trim();
|
|
if (!normalizedPath) {
|
|
return null;
|
|
}
|
|
|
|
return this.listWorkspaceSummaries().find((workspace) => {
|
|
return normalizedPath === workspace.folderPath || normalizedPath.startsWith(`${workspace.folderPath}/`);
|
|
}) || null;
|
|
}
|
|
|
|
getActiveWorkspace() {
|
|
const activeCanvasPath = this.getActiveCanvasPath();
|
|
if (!activeCanvasPath) {
|
|
return null;
|
|
}
|
|
|
|
return this.getWorkspaceForVaultPath(activeCanvasPath);
|
|
}
|
|
|
|
resolveWorkspaceRepoPathForSelection(selection) {
|
|
const workspace = this.getWorkspaceForVaultPath(selection?.canvasPath);
|
|
return workspace?.repoPath || "";
|
|
}
|
|
|
|
async ensureVaultFolderExists(vaultFolderPath) {
|
|
const parts = String(vaultFolderPath || "").split("/").filter(Boolean);
|
|
let currentPath = "";
|
|
|
|
for (const part of parts) {
|
|
currentPath = joinVaultPath(currentPath, part);
|
|
if (!this.app.vault.getAbstractFileByPath(currentPath)) {
|
|
await this.app.vault.createFolder(currentPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
ensureFilesystemFolderExists(folderPath) {
|
|
fs.mkdirSync(folderPath, { recursive: true });
|
|
}
|
|
|
|
writeFilesystemFile(filePath, content) {
|
|
this.ensureFilesystemFolderExists(path.dirname(filePath));
|
|
fs.writeFileSync(filePath, content, "utf8");
|
|
}
|
|
|
|
async writeVaultFile(vaultPath, content) {
|
|
const existing = this.app.vault.getAbstractFileByPath(vaultPath);
|
|
if (existing instanceof TFile) {
|
|
await this.app.vault.modify(existing, content);
|
|
return existing;
|
|
}
|
|
|
|
return this.app.vault.create(vaultPath, content);
|
|
}
|
|
|
|
buildDefaultWorkspaceCanvas(name, repoPath) {
|
|
return JSON.stringify({
|
|
nodes: [
|
|
{
|
|
id: "workspace-intro",
|
|
type: "text",
|
|
x: 80,
|
|
y: 80,
|
|
width: 440,
|
|
height: 280,
|
|
text: [
|
|
`Workspace: ${name}`,
|
|
`Repo: ${repoPath}`,
|
|
"",
|
|
"- One text node = one prompt/thread",
|
|
"- Run: OpenAgent: New thread from selection",
|
|
"- Shortcut helper: OpenAgent: Create follow-up node",
|
|
"- Optional context: markdown file nodes, or markdown files in the same group",
|
|
"- Follow-up: create a follow-up node from a previous OpenAgent node, then run again",
|
|
"- OpenAgent writes the answer back to the canvas",
|
|
].join("\n"),
|
|
},
|
|
],
|
|
edges: [],
|
|
}, null, 2) + "\n";
|
|
}
|
|
|
|
readRepoWorkspaceConfig(repoWorkspaceDir) {
|
|
const configPath = path.join(repoWorkspaceDir, WORKSPACE_CONFIG_FILE_NAME);
|
|
if (!fs.existsSync(configPath)) {
|
|
return null;
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
const repoPath = normalizeRepoPath(parsed?.repoPath);
|
|
if (!repoPath) {
|
|
return null;
|
|
}
|
|
return {
|
|
configPath,
|
|
folderPath: repoWorkspaceDir,
|
|
name: String(parsed?.name || path.basename(repoPath) || "Workspace").trim() || "Workspace",
|
|
repoPath,
|
|
defaultCanvas: String(parsed?.defaultCanvas || DEFAULT_WORKSPACE_CANVAS_FILE).trim() || DEFAULT_WORKSPACE_CANVAS_FILE,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
ensureRepoWorkspaceFiles(repoPath, workspaceNameInput = "") {
|
|
const repoWorkspaceDir = this.getRepoWorkspaceDirectory(repoPath);
|
|
this.ensureFilesystemFolderExists(repoWorkspaceDir);
|
|
|
|
const existing = this.readRepoWorkspaceConfig(repoWorkspaceDir);
|
|
const workspaceName = String(existing?.name || workspaceNameInput || path.basename(repoPath)).trim() || path.basename(repoPath);
|
|
const defaultCanvas = String(existing?.defaultCanvas || DEFAULT_WORKSPACE_CANVAS_FILE).trim() || DEFAULT_WORKSPACE_CANVAS_FILE;
|
|
const workspace = {
|
|
configPath: path.join(repoWorkspaceDir, WORKSPACE_CONFIG_FILE_NAME),
|
|
folderPath: repoWorkspaceDir,
|
|
name: workspaceName,
|
|
repoPath,
|
|
defaultCanvas,
|
|
};
|
|
|
|
if (!existing) {
|
|
this.writeFilesystemFile(workspace.configPath, JSON.stringify({
|
|
name: workspace.name,
|
|
repoPath: workspace.repoPath,
|
|
defaultCanvas: workspace.defaultCanvas,
|
|
createdAt: new Date().toISOString(),
|
|
}, null, 2) + "\n");
|
|
}
|
|
|
|
const canvasPath = path.join(repoWorkspaceDir, workspace.defaultCanvas);
|
|
if (!fs.existsSync(canvasPath)) {
|
|
this.writeFilesystemFile(canvasPath, this.buildDefaultWorkspaceCanvas(workspace.name, workspace.repoPath));
|
|
}
|
|
|
|
return workspace;
|
|
}
|
|
|
|
resolveVaultWorkspaceLink(folderName, repoWorkspaceDir) {
|
|
const workspaceRoot = this.getWorkspaceRootVaultPath();
|
|
const repoWorkspaceRealPath = safeRealpathSync(repoWorkspaceDir) || path.normalize(repoWorkspaceDir);
|
|
let candidateFolderName = slugifyWorkspaceName(folderName);
|
|
let suffix = 2;
|
|
|
|
while (candidateFolderName) {
|
|
const folderPath = joinVaultPath(workspaceRoot, candidateFolderName);
|
|
const linkPath = this.getVaultAbsolutePath(folderPath);
|
|
if (!linkPath) {
|
|
throw new Error("OpenAgent could not resolve the current vault path.");
|
|
}
|
|
|
|
if (!pathExistsOrIsSymlink(linkPath)) {
|
|
return { folderPath, linkPath };
|
|
}
|
|
|
|
const existingTarget = safeRealpathSync(linkPath);
|
|
if (existingTarget && existingTarget === repoWorkspaceRealPath) {
|
|
return { folderPath, linkPath };
|
|
}
|
|
|
|
candidateFolderName = `${slugifyWorkspaceName(folderName)}-${suffix}`;
|
|
suffix += 1;
|
|
}
|
|
|
|
throw new Error("OpenAgent could not allocate a workspace entry for this repo.");
|
|
}
|
|
|
|
async ensureWorkspaceSymlink(repoWorkspaceDir, workspaceName) {
|
|
const workspaceRoot = this.getWorkspaceRootVaultPath();
|
|
await this.ensureVaultFolderExists(workspaceRoot);
|
|
|
|
const { folderPath, linkPath } = this.resolveVaultWorkspaceLink(workspaceName, repoWorkspaceDir);
|
|
if (!pathExistsOrIsSymlink(linkPath)) {
|
|
fs.symlinkSync(repoWorkspaceDir, linkPath, getDirectorySymlinkType());
|
|
}
|
|
|
|
return { folderPath, linkPath };
|
|
}
|
|
|
|
async waitForWorkspaceCanvasFile(vaultFolderPath, canvasFileName) {
|
|
const canvasPath = joinVaultPath(vaultFolderPath, canvasFileName);
|
|
const startedAt = Date.now();
|
|
while (Date.now() - startedAt <= WORKSPACE_INDEX_WAIT_MS) {
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(canvasPath);
|
|
if (abstractFile instanceof TFile) {
|
|
return abstractFile;
|
|
}
|
|
await sleep(WORKSPACE_INDEX_POLL_MS);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async createWorkspaceFromRepoPath(repoPathInput, workspaceNameInput = "") {
|
|
const repoPath = normalizeRepoPath(repoPathInput);
|
|
if (!repoPath) {
|
|
throw new Error("Enter an absolute repo path.");
|
|
}
|
|
if (!fs.existsSync(repoPath) || !fs.statSync(repoPath).isDirectory()) {
|
|
throw new Error(`Repo path does not exist: ${repoPath}`);
|
|
}
|
|
|
|
await this.ensureWorkspaceSummariesLoaded();
|
|
const existing = this.listWorkspaceSummaries().find((workspace) => workspace.repoPath === repoPath);
|
|
if (existing) {
|
|
await this.openWorkspace(existing);
|
|
new Notice(`Opened existing workspace: ${existing.name}`);
|
|
return existing;
|
|
}
|
|
|
|
const repoWorkspace = this.ensureRepoWorkspaceFiles(repoPath, workspaceNameInput);
|
|
const { folderPath } = await this.ensureWorkspaceSymlink(repoWorkspace.folderPath, repoWorkspace.name);
|
|
const workspace = {
|
|
configPath: joinVaultPath(folderPath, WORKSPACE_CONFIG_FILE_NAME),
|
|
folderPath,
|
|
name: repoWorkspace.name,
|
|
repoPath: repoWorkspace.repoPath,
|
|
defaultCanvas: repoWorkspace.defaultCanvas,
|
|
};
|
|
|
|
const canvasFile = await this.waitForWorkspaceCanvasFile(workspace.folderPath, workspace.defaultCanvas);
|
|
if (!canvasFile) {
|
|
throw new Error(`OpenAgent created ${REPO_WORKSPACE_DIR_NAME}, but Obsidian has not indexed the new workspace link yet. Try reopening the workspace picker.`);
|
|
}
|
|
|
|
await this.ensureWorkspaceSummariesLoaded({ force: true });
|
|
await this.openWorkspace(workspace);
|
|
new Notice(`Created workspace: ${workspace.name}`);
|
|
return workspace;
|
|
}
|
|
|
|
async ensureWorkspaceForRepoPath(repoPathInput, workspaceNameInput = "") {
|
|
const repoPath = normalizeRepoPath(repoPathInput);
|
|
if (!repoPath) {
|
|
throw new Error("OpenAgent could not infer a working directory for this chat.");
|
|
}
|
|
if (!fs.existsSync(repoPath) || !fs.statSync(repoPath).isDirectory()) {
|
|
throw new Error(`Working directory does not exist: ${repoPath}`);
|
|
}
|
|
|
|
await this.ensureWorkspaceSummariesLoaded();
|
|
const existing = this.listWorkspaceSummaries().find((workspace) => workspace.repoPath === repoPath);
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
|
|
const repoWorkspace = this.ensureRepoWorkspaceFiles(repoPath, workspaceNameInput);
|
|
const { folderPath } = await this.ensureWorkspaceSymlink(repoWorkspace.folderPath, repoWorkspace.name);
|
|
const workspace = {
|
|
configPath: joinVaultPath(folderPath, WORKSPACE_CONFIG_FILE_NAME),
|
|
folderPath,
|
|
name: repoWorkspace.name,
|
|
repoPath: repoWorkspace.repoPath,
|
|
defaultCanvas: repoWorkspace.defaultCanvas,
|
|
};
|
|
|
|
const canvasFile = await this.waitForWorkspaceCanvasFile(workspace.folderPath, workspace.defaultCanvas);
|
|
if (!canvasFile) {
|
|
throw new Error(`OpenAgent created a workspace canvas, but Obsidian has not indexed it yet: ${workspace.defaultCanvas}`);
|
|
}
|
|
|
|
await this.ensureWorkspaceSummariesLoaded({ force: true });
|
|
return workspace;
|
|
}
|
|
|
|
async openWorkspace(workspace) {
|
|
const canvasPath = joinVaultPath(workspace.folderPath, workspace.defaultCanvas || DEFAULT_WORKSPACE_CANVAS_FILE);
|
|
const canvasFile = this.app.vault.getAbstractFileByPath(canvasPath);
|
|
if (!(canvasFile instanceof TFile)) {
|
|
throw new Error(`Default canvas not found: ${canvasPath}`);
|
|
}
|
|
|
|
const leaf = this.app.workspace.getMostRecentLeaf() || this.app.workspace.getLeaf(true);
|
|
await leaf.openFile(canvasFile);
|
|
await this.activateView();
|
|
await this.refreshTasks();
|
|
}
|
|
|
|
getDevSmokeAbsolutePath(relativePath) {
|
|
const vaultBasePath = this.getVaultBasePath();
|
|
if (!vaultBasePath) {
|
|
return "";
|
|
}
|
|
|
|
return path.join(vaultBasePath, relativePath);
|
|
}
|
|
|
|
isDevSmokePollingEnabled() {
|
|
return this.settings?.enableDevSmokeRequests === true;
|
|
}
|
|
|
|
isDebugLoggingEnabled() {
|
|
return this.settings?.enableDebugLogging === true;
|
|
}
|
|
|
|
configureDevSmokePolling() {
|
|
if (this.devSmokePollIntervalId != null) {
|
|
window.clearInterval(this.devSmokePollIntervalId);
|
|
this.devSmokePollIntervalId = null;
|
|
}
|
|
|
|
if (!this.isDevSmokePollingEnabled()) {
|
|
void this.appendDebugEvent("dev_smoke_polling_configured", {
|
|
enabled: false,
|
|
requestPath: this.getDevSmokeAbsolutePath(DEV_SMOKE_REQUEST_RELATIVE_PATH),
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.devSmokePollIntervalId = window.setInterval(() => {
|
|
this.maybeRunDevSmokeRequest();
|
|
}, DEV_SMOKE_POLL_MS);
|
|
this.registerInterval(this.devSmokePollIntervalId);
|
|
void this.appendDebugEvent("dev_smoke_polling_configured", {
|
|
enabled: true,
|
|
pollMs: DEV_SMOKE_POLL_MS,
|
|
requestPath: this.getDevSmokeAbsolutePath(DEV_SMOKE_REQUEST_RELATIVE_PATH),
|
|
});
|
|
this.maybeRunDevSmokeRequest();
|
|
}
|
|
|
|
maybeRunDevSmokeRequest() {
|
|
if (!this.isDevSmokePollingEnabled()) {
|
|
return;
|
|
}
|
|
|
|
if (this.devSmokeRunPromise) {
|
|
return;
|
|
}
|
|
|
|
this.devSmokeRunPromise = this.runDevSmokeRequest()
|
|
.catch(() => {})
|
|
.finally(() => {
|
|
this.devSmokeRunPromise = null;
|
|
});
|
|
}
|
|
|
|
async runDevSmokeRequest() {
|
|
const requestPath = this.getDevSmokeAbsolutePath(DEV_SMOKE_REQUEST_RELATIVE_PATH);
|
|
if (!requestPath || !fs.existsSync(requestPath)) {
|
|
return;
|
|
}
|
|
|
|
let request;
|
|
try {
|
|
request = JSON.parse(fs.readFileSync(requestPath, "utf8"));
|
|
} catch (error) {
|
|
fs.unlinkSync(requestPath);
|
|
await this.writeDevSmokeResult({
|
|
status: "error",
|
|
requestId: "",
|
|
message: `Invalid smoke request: ${error.message}`,
|
|
finishedAt: new Date().toISOString(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
const requestId = String(request?.id || "").trim() || `anonymous-${Date.now()}`;
|
|
if (requestId === this.lastProcessedSmokeRequestId) {
|
|
return;
|
|
}
|
|
this.lastProcessedSmokeRequestId = requestId;
|
|
void this.appendDebugEvent("dev_smoke_request_detected", {
|
|
requestId,
|
|
canvasPath: String(request?.canvasPath || ""),
|
|
mode: String(request?.mode || ""),
|
|
runTask: request?.runTask !== false,
|
|
forceNewTask: request?.forceNewTask === true,
|
|
});
|
|
|
|
try {
|
|
let selection = await this.resolveDevSmokeSelection(request);
|
|
const requestedCwd = this.normalizeWorkingDirectoryInput(request?.cwd);
|
|
const smokeMode = String(request?.mode || "").trim();
|
|
let task = null;
|
|
let rawPrompt = "";
|
|
let runMode = smokeMode || "default";
|
|
|
|
if (request?.runTask !== false && smokeMode === "new-thread" && request?.forceNewTask === true) {
|
|
selection = await this.resolver.addImplicitCanvasFileContext(selection);
|
|
const followUpResult = await this.runSelectionAsFollowUp(selection, {
|
|
confirmFork: async () => request?.forkBehavior === "fork" ? "fork" : "continue",
|
|
});
|
|
if (followUpResult) {
|
|
if (followUpResult.cancelled) {
|
|
runMode = "cancelled";
|
|
} else {
|
|
task = followUpResult.task;
|
|
rawPrompt = followUpResult.message;
|
|
runMode = followUpResult.mode || "follow-up";
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!task) {
|
|
if (request?.forceNewTask === true) {
|
|
selection = await this.resolver.addImplicitCanvasFileContext(selection);
|
|
}
|
|
|
|
const response = await this.api.createTaskFromCanvasSelection(this.withVaultScope({
|
|
...selection,
|
|
cwd: requestedCwd || this.resolver.inferWorkingDirectory(selection),
|
|
forceNewTask: request?.forceNewTask === true,
|
|
runtimeConfig: this.buildRuntimeConfigPayload(),
|
|
}));
|
|
task = this.mergeTask(response.task, { adoptCurrentVault: true });
|
|
|
|
if (request?.runTask !== false) {
|
|
if (smokeMode === "new-thread") {
|
|
rawPrompt = this.getNewThreadPromptFromSelection(selection, {
|
|
cwd: requestedCwd || this.resolver.inferWorkingDirectory(selection),
|
|
});
|
|
const runResponse = await this.api.runTask(task.taskId, {
|
|
rawPrompt,
|
|
transcriptMessage: rawPrompt,
|
|
forceContext: false,
|
|
runtimeConfig: this.buildRuntimeConfigPayload(),
|
|
});
|
|
this.mergeTask(runResponse.task, { adoptCurrentVault: true });
|
|
} else {
|
|
const runResponse = await this.api.runTask(task.taskId, {
|
|
forceContext: true,
|
|
runtimeConfig: this.buildRuntimeConfigPayload(),
|
|
});
|
|
this.mergeTask(runResponse.task, { adoptCurrentVault: true });
|
|
}
|
|
}
|
|
}
|
|
|
|
await this.refreshTask(task.taskId);
|
|
|
|
await this.writeDevSmokeResult({
|
|
status: "ok",
|
|
requestId,
|
|
taskId: task.taskId,
|
|
cwd: task.cwd || requestedCwd || "",
|
|
canvasPath: selection.canvasPath,
|
|
nodeIds: selection.nodeIds,
|
|
selectionSummary: this.summarizeSelection(selection),
|
|
selectionDebug: this.describeSelectionDebug(selection),
|
|
mode: runMode,
|
|
rawPrompt,
|
|
runTriggered: request?.runTask !== false,
|
|
finishedAt: new Date().toISOString(),
|
|
});
|
|
} catch (error) {
|
|
await this.writeDevSmokeResult({
|
|
status: "error",
|
|
requestId,
|
|
message: String(error?.message || error),
|
|
finishedAt: new Date().toISOString(),
|
|
});
|
|
} finally {
|
|
if (fs.existsSync(requestPath)) {
|
|
fs.unlinkSync(requestPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
async resolveDevSmokeSelection(request) {
|
|
const canvasPath = String(request?.canvasPath || "").trim();
|
|
if (!canvasPath) {
|
|
throw new Error("Smoke request is missing canvasPath.");
|
|
}
|
|
|
|
const abstractFile = await this.waitForSmokeCanvasFile(canvasPath);
|
|
if (!(abstractFile instanceof TFile) || abstractFile.extension !== "canvas") {
|
|
throw new Error(`Smoke canvas not found: ${canvasPath}`);
|
|
}
|
|
|
|
const explicitNodeIds = Array.isArray(request?.nodeIds)
|
|
? request.nodeIds.map((nodeId) => String(nodeId)).filter(Boolean)
|
|
: [];
|
|
const nodeIds = explicitNodeIds.length > 0
|
|
? explicitNodeIds
|
|
: await this.resolveAllSupportedCanvasNodeIds(abstractFile);
|
|
|
|
if (nodeIds.length === 0) {
|
|
throw new Error(`Smoke canvas has no supported nodes: ${canvasPath}`);
|
|
}
|
|
|
|
return this.resolver.resolveCanvasSelection(abstractFile, nodeIds);
|
|
}
|
|
|
|
async waitForSmokeCanvasFile(canvasPath, options = {}) {
|
|
const normalizedPath = String(canvasPath || "").trim();
|
|
const timeoutMs = Math.max(0, toFiniteNumber(options.timeoutMs, 8_000));
|
|
const pollMs = Math.max(25, toFiniteNumber(options.pollMs, 100));
|
|
const deadline = Date.now() + timeoutMs;
|
|
|
|
while (true) {
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(normalizedPath);
|
|
if (!normalizedPath || abstractFile instanceof TFile) {
|
|
return abstractFile || null;
|
|
}
|
|
|
|
if (Date.now() >= deadline) {
|
|
return abstractFile || null;
|
|
}
|
|
|
|
await sleep(pollMs);
|
|
}
|
|
}
|
|
|
|
async resolveAllSupportedCanvasNodeIds(file) {
|
|
const raw = await this.app.vault.cachedRead(file);
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(raw);
|
|
} catch {
|
|
throw new Error(`Unable to parse canvas JSON for ${file.path}`);
|
|
}
|
|
|
|
return Array.isArray(parsed?.nodes)
|
|
? parsed.nodes
|
|
.filter((node) => node && (node.type === "text" || node.type === "file"))
|
|
.map((node) => String(node.id || ""))
|
|
.filter(Boolean)
|
|
: [];
|
|
}
|
|
|
|
async writeDevSmokeResult(result) {
|
|
const resultPath = this.getDevSmokeAbsolutePath(DEV_SMOKE_RESULT_RELATIVE_PATH);
|
|
if (!resultPath) {
|
|
return;
|
|
}
|
|
|
|
fs.mkdirSync(path.dirname(resultPath), { recursive: true });
|
|
fs.writeFileSync(resultPath, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
|
}
|
|
|
|
getDebugLogAbsolutePath() {
|
|
return this.getDevSmokeAbsolutePath(DEBUG_LOG_RELATIVE_PATH);
|
|
}
|
|
|
|
getRecentDebugEvents() {
|
|
return this.debugEvents.slice(-8).reverse();
|
|
}
|
|
|
|
async appendDebugEvent(type, payload = {}) {
|
|
if (!this.isDebugLoggingEnabled()) {
|
|
return null;
|
|
}
|
|
|
|
const event = {
|
|
type: String(type || "event"),
|
|
createdAt: new Date().toISOString(),
|
|
payload,
|
|
summary: this.formatDebugSummary(payload),
|
|
};
|
|
|
|
this.debugEvents.push(event);
|
|
if (this.debugEvents.length > MAX_DEBUG_EVENTS) {
|
|
this.debugEvents = this.debugEvents.slice(-MAX_DEBUG_EVENTS);
|
|
}
|
|
|
|
const debugPath = this.getDebugLogAbsolutePath();
|
|
if (debugPath) {
|
|
fs.mkdirSync(path.dirname(debugPath), { recursive: true });
|
|
fs.appendFileSync(debugPath, `${JSON.stringify(event)}\n`, "utf8");
|
|
}
|
|
|
|
this.requestViewRefresh();
|
|
return event;
|
|
}
|
|
|
|
formatDebugSummary(payload = {}) {
|
|
return Object.entries(payload)
|
|
.filter(([, value]) => value != null && value !== "")
|
|
.map(([key, value]) => `${key}=${this.truncateInline(typeof value === "string" ? value : JSON.stringify(value), 180)}`)
|
|
.join(" | ");
|
|
}
|
|
|
|
getTasks() {
|
|
return Object.values(this.tasksById).filter((task) => this.isTaskForCurrentVault(task)).sort((left, right) => {
|
|
return (right.updatedAt || "").localeCompare(left.updatedAt || "");
|
|
});
|
|
}
|
|
|
|
getTaskConversationLastMessageTimestamp(task) {
|
|
const messages = Array.isArray(task?.messages) ? task.messages : [];
|
|
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
const lastMessageTimestamp = parseIsoTimestamp(lastMessage?.updatedAt || lastMessage?.createdAt || "");
|
|
if (lastMessageTimestamp != null) {
|
|
return lastMessageTimestamp;
|
|
}
|
|
|
|
if (String(task?.lastMessageRole || "") === "assistant") {
|
|
const latestAssistantTimestamp = parseIsoTimestamp(
|
|
task?.latestAssistantMessage?.updatedAt || task?.latestAssistantMessage?.createdAt || ""
|
|
);
|
|
if (latestAssistantTimestamp != null) {
|
|
return latestAssistantTimestamp;
|
|
}
|
|
}
|
|
|
|
return parseIsoTimestamp(task?.updatedAt || task?.createdAt || "") || 0;
|
|
}
|
|
|
|
getTaskConversationLastMessageDayTimestamp(task) {
|
|
return getStartOfLocalDayTimestamp(this.getTaskConversationLastMessageTimestamp(task));
|
|
}
|
|
|
|
getTaskProjectLabel(task) {
|
|
const canvasWorkspace = this.getWorkspaceForVaultPath(task?.selectionContext?.canvasPath);
|
|
if (canvasWorkspace?.name) {
|
|
return canvasWorkspace.name;
|
|
}
|
|
|
|
const cwd = normalizeRepoPath(task?.cwd);
|
|
if (!cwd) {
|
|
return "";
|
|
}
|
|
|
|
const cwdWorkspace = this.listWorkspaceSummaries().find((workspace) => {
|
|
return normalizeRepoPath(workspace?.repoPath) === cwd;
|
|
});
|
|
return cwdWorkspace?.name || compactPathLabel(cwd);
|
|
}
|
|
|
|
getTaskRootNodeRef(task) {
|
|
const canvasPath = this.getTaskCanvasPath(task);
|
|
const bindingRootNodeIds = Array.isArray(task?.canvasBinding?.rootNodeIds)
|
|
? task.canvasBinding.rootNodeIds.map((nodeId) => String(nodeId)).filter(Boolean)
|
|
: [];
|
|
const selectionNodeIds = Array.isArray(task?.selectionContext?.nodeIds)
|
|
? task.selectionContext.nodeIds.map((nodeId) => String(nodeId)).filter(Boolean)
|
|
: [];
|
|
const nodeIds = bindingRootNodeIds.length > 0 ? bindingRootNodeIds : selectionNodeIds;
|
|
if (!canvasPath || nodeIds.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const rootNodeId = this.getConversationSelectionNodeIds(canvasPath, [nodeIds[0]])[0] || nodeIds[0];
|
|
if (!rootNodeId) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
canvasPath,
|
|
nodeId: rootNodeId,
|
|
};
|
|
}
|
|
|
|
getTaskCanvasRunSourceRef(task) {
|
|
const bindingCanvasPath = String(task?.canvasBinding?.canvasPath || "").trim();
|
|
const bindingNodeId = String(task?.canvasBinding?.activeSourceNodeId || "").trim();
|
|
if (bindingCanvasPath && bindingNodeId) {
|
|
return {
|
|
canvasPath: bindingCanvasPath,
|
|
nodeId: bindingNodeId,
|
|
};
|
|
}
|
|
|
|
const taskId = String(task?.taskId || "").trim();
|
|
const override = taskId ? this.canvasRunSourceRefByTaskId[taskId] : null;
|
|
const overrideCanvasPath = String(override?.canvasPath || "").trim();
|
|
const overrideNodeId = String(override?.nodeId || "").trim();
|
|
if (overrideCanvasPath && overrideNodeId) {
|
|
return {
|
|
canvasPath: overrideCanvasPath,
|
|
nodeId: overrideNodeId,
|
|
};
|
|
}
|
|
|
|
const canvasPath = String(task?.selectionContext?.canvasPath || "").trim();
|
|
const nodeIds = Array.isArray(task?.selectionContext?.nodeIds)
|
|
? task.selectionContext.nodeIds.map((nodeId) => String(nodeId)).filter(Boolean)
|
|
: [];
|
|
if (!canvasPath || nodeIds.length !== 1) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
canvasPath,
|
|
nodeId: nodeIds[0],
|
|
};
|
|
}
|
|
|
|
cacheTaskCanvasRunSourceRef(taskId, ref) {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
const normalizedCanvasPath = String(ref?.canvasPath || "").trim();
|
|
const normalizedNodeId = String(ref?.nodeId || "").trim();
|
|
if (!normalizedTaskId || !normalizedCanvasPath || !normalizedNodeId) {
|
|
return null;
|
|
}
|
|
|
|
const previousRef = this.canvasRunSourceRefByTaskId[normalizedTaskId] || null;
|
|
if (
|
|
previousRef
|
|
&& String(previousRef.canvasPath || "") === normalizedCanvasPath
|
|
&& String(previousRef.nodeId || "") === normalizedNodeId
|
|
) {
|
|
return {
|
|
taskId: normalizedTaskId,
|
|
canvasPath: normalizedCanvasPath,
|
|
nodeId: normalizedNodeId,
|
|
};
|
|
}
|
|
|
|
this.canvasRunSourceRefByTaskId[normalizedTaskId] = {
|
|
canvasPath: normalizedCanvasPath,
|
|
nodeId: normalizedNodeId,
|
|
};
|
|
|
|
const affectedCanvasPaths = new Set([
|
|
String(previousRef?.canvasPath || "").trim(),
|
|
normalizedCanvasPath,
|
|
].filter(Boolean));
|
|
affectedCanvasPaths.forEach((canvasPath) => {
|
|
this.scheduleCanvasNodeHighlightSync(canvasPath);
|
|
});
|
|
void this.persistPluginState();
|
|
return {
|
|
taskId: normalizedTaskId,
|
|
canvasPath: normalizedCanvasPath,
|
|
nodeId: normalizedNodeId,
|
|
};
|
|
}
|
|
|
|
async setTaskCanvasRunSourceRef(taskId, ref, options = {}) {
|
|
const cachedRef = this.cacheTaskCanvasRunSourceRef(taskId, ref);
|
|
if (!cachedRef) {
|
|
return this.tasksById[String(taskId || "").trim()] || null;
|
|
}
|
|
|
|
if (options.persistDaemon === false) {
|
|
return this.tasksById[cachedRef.taskId] || null;
|
|
}
|
|
|
|
try {
|
|
const response = await this.api.updateTaskCanvasBinding(cachedRef.taskId, {
|
|
canvasPath: cachedRef.canvasPath,
|
|
activeSourceNodeId: cachedRef.nodeId,
|
|
});
|
|
return this.mergeTask(response.task);
|
|
} catch (error) {
|
|
if (options.throwOnError) {
|
|
throw error;
|
|
}
|
|
return this.tasksById[cachedRef.taskId] || null;
|
|
}
|
|
}
|
|
|
|
findTaskByRootCanvasNode(canvasPath, nodeId) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedNodeId = String(nodeId || "").trim();
|
|
if (!normalizedCanvasPath || !normalizedNodeId) {
|
|
return null;
|
|
}
|
|
|
|
return this.getTasks().find((task) => {
|
|
const rootRef = this.getTaskRootNodeRef(task);
|
|
return rootRef?.canvasPath === normalizedCanvasPath && rootRef?.nodeId === normalizedNodeId;
|
|
}) || null;
|
|
}
|
|
|
|
findTaskByCanvasRunSourceNode(canvasPath, nodeId) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedNodeId = String(nodeId || "").trim();
|
|
if (!normalizedCanvasPath || !normalizedNodeId) {
|
|
return null;
|
|
}
|
|
|
|
return this.getTasks().find((task) => {
|
|
const sourceRef = this.getTaskCanvasRunSourceRef(task);
|
|
return sourceRef?.canvasPath === normalizedCanvasPath && sourceRef?.nodeId === normalizedNodeId;
|
|
}) || null;
|
|
}
|
|
|
|
findTaskByResultNode(canvasPath, resultNodeId, sourceNodeId) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedResultNodeId = String(resultNodeId || "").trim();
|
|
const normalizedSourceNodeId = String(sourceNodeId || "").trim();
|
|
if (!normalizedCanvasPath || !normalizedResultNodeId || !normalizedSourceNodeId) {
|
|
return null;
|
|
}
|
|
|
|
return this.getTasks().find((task) => {
|
|
if (this.getTaskCanvasPath(task) !== normalizedCanvasPath) {
|
|
return false;
|
|
}
|
|
|
|
const boundResultNodeId = String(
|
|
task?.canvasBinding?.resultNodesBySourceNodeId?.[normalizedSourceNodeId]?.resultNodeId || ""
|
|
).trim();
|
|
if (boundResultNodeId) {
|
|
return boundResultNodeId === normalizedResultNodeId;
|
|
}
|
|
|
|
return buildTaskResultNodeId(task.taskId, normalizedSourceNodeId) === normalizedResultNodeId;
|
|
}) || null;
|
|
}
|
|
|
|
findTaskByOpenAgentResultNode(canvasPath, resultNodeId) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedResultNodeId = String(resultNodeId || "").trim();
|
|
if (!normalizedCanvasPath || !normalizedResultNodeId) {
|
|
return null;
|
|
}
|
|
|
|
const resultNode = this.readCanvasNodeByIdSync(normalizedCanvasPath, normalizedResultNodeId);
|
|
if (!resultNode || !isOpenAgentAssistantResultNode(resultNode)) {
|
|
return null;
|
|
}
|
|
|
|
const metadataTaskId = getOpenAgentResultTaskId(resultNode);
|
|
const metadataTask = metadataTaskId ? this.tasksById[metadataTaskId] : null;
|
|
if (metadataTask && this.getTaskCanvasPath(metadataTask) === normalizedCanvasPath) {
|
|
return metadataTask;
|
|
}
|
|
|
|
const sourceNodeId = getOpenAgentResultSourceNodeId(resultNode)
|
|
|| this.getConversationSelectionNodeIds(normalizedCanvasPath, [normalizedResultNodeId])[0]
|
|
|| "";
|
|
if (!sourceNodeId) {
|
|
return null;
|
|
}
|
|
|
|
return this.findTaskByResultNode(normalizedCanvasPath, normalizedResultNodeId, sourceNodeId)
|
|
|| this.findTaskByCanvasRunSourceNode(normalizedCanvasPath, sourceNodeId)
|
|
|| this.findTaskByRootCanvasNode(normalizedCanvasPath, sourceNodeId)
|
|
|| null;
|
|
}
|
|
|
|
findConversationTaskForUpstreamNode(canvasPath, upstreamNodeId) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedUpstreamNodeId = String(upstreamNodeId || "").trim();
|
|
if (!normalizedCanvasPath || !normalizedUpstreamNodeId) {
|
|
return null;
|
|
}
|
|
|
|
const resultTask = this.findTaskByOpenAgentResultNode(normalizedCanvasPath, normalizedUpstreamNodeId);
|
|
if (resultTask) {
|
|
return resultTask;
|
|
}
|
|
|
|
if (normalizedUpstreamNodeId.startsWith("oa-result-")) {
|
|
const sourceNodeId = this.getConversationSelectionNodeIds(normalizedCanvasPath, [normalizedUpstreamNodeId])[0] || "";
|
|
if (!sourceNodeId) {
|
|
return null;
|
|
}
|
|
|
|
return this.findTaskByResultNode(normalizedCanvasPath, normalizedUpstreamNodeId, sourceNodeId)
|
|
|| this.findTaskByCanvasRunSourceNode(normalizedCanvasPath, sourceNodeId)
|
|
|| this.findTaskByRootCanvasNode(normalizedCanvasPath, sourceNodeId)
|
|
|| null;
|
|
}
|
|
|
|
return this.findTaskByCanvasRunSourceNode(normalizedCanvasPath, normalizedUpstreamNodeId)
|
|
|| this.findTaskByRootCanvasNode(normalizedCanvasPath, normalizedUpstreamNodeId)
|
|
|| null;
|
|
}
|
|
|
|
async resolveCanvasFollowUpTarget(selection) {
|
|
const canvasPath = String(selection?.canvasPath || "").trim();
|
|
const nodeIds = Array.isArray(selection?.nodeIds)
|
|
? selection.nodeIds.map((nodeId) => String(nodeId)).filter(Boolean)
|
|
: [];
|
|
const textBlocks = Array.isArray(selection?.textBlocks) ? selection.textBlocks : [];
|
|
if (!canvasPath || nodeIds.length !== 1 || textBlocks.length !== 1) {
|
|
return null;
|
|
}
|
|
|
|
const followUpNodeId = nodeIds[0];
|
|
const followUpMessage = String(textBlocks[0]?.text || "").trim();
|
|
if (!followUpNodeId || !followUpMessage) {
|
|
return null;
|
|
}
|
|
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(canvasPath);
|
|
if (!(abstractFile instanceof TFile) || abstractFile.extension !== "canvas") {
|
|
return null;
|
|
}
|
|
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(await this.app.vault.cachedRead(abstractFile));
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
const edges = Array.isArray(parsed?.edges) ? parsed.edges : [];
|
|
const nodes = Array.isArray(parsed?.nodes) ? parsed.nodes : [];
|
|
const followUpNode = nodes.find((node) => String(node?.id || "").trim() === followUpNodeId) || null;
|
|
const followUpMetadata = getOpenAgentCanvasMetadata(followUpNode);
|
|
for (const edge of edges) {
|
|
const toNodeId = String(edge?.toNode || "").trim();
|
|
if (toNodeId !== followUpNodeId) {
|
|
continue;
|
|
}
|
|
|
|
const upstreamNodeId = String(edge?.fromNode || "").trim();
|
|
const upstreamNode = nodes.find((node) => String(node?.id || "").trim() === upstreamNodeId) || null;
|
|
let task = this.findConversationTaskForUpstreamNode(canvasPath, upstreamNodeId);
|
|
if (!task) {
|
|
const metadataTaskId = getOpenAgentResultTaskId(upstreamNode);
|
|
if (metadataTaskId) {
|
|
try {
|
|
await this.refreshTask(metadataTaskId, { force: true, adoptCurrentVault: true });
|
|
task = this.findConversationTaskForUpstreamNode(canvasPath, upstreamNodeId);
|
|
} catch {
|
|
task = null;
|
|
}
|
|
}
|
|
}
|
|
if (!task) {
|
|
continue;
|
|
}
|
|
|
|
const upstreamResultSourceNodeId = getOpenAgentResultSourceNodeId(upstreamNode);
|
|
const canForkFromUpstream = isOpenAgentAssistantResultNode(upstreamNode);
|
|
const branchSourceNodeId = String(
|
|
followUpMetadata?.branchSourceNodeId
|
|
|| followUpMetadata?.sourceNodeId
|
|
|| upstreamResultSourceNodeId
|
|
|| upstreamNodeId
|
|
).trim();
|
|
return {
|
|
task,
|
|
message: followUpMessage,
|
|
sourceNodeId: followUpNodeId,
|
|
upstreamNodeId,
|
|
branchSourceNodeId,
|
|
branchMessageId: String(
|
|
followUpMetadata?.branchMessageId
|
|
|| getOpenAgentCanvasMetadata(upstreamNode)?.messageId
|
|
|| task?.canvasBinding?.resultNodesBySourceNodeId?.[branchSourceNodeId]?.messageId
|
|
|| ""
|
|
).trim(),
|
|
canForkFromUpstream,
|
|
forceFork: followUpMetadata?.kind === "fork-request",
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
countTaskUserTurnsAfterMessage(task, messageId) {
|
|
const normalizedMessageId = String(messageId || "").trim();
|
|
if (!normalizedMessageId) {
|
|
return 0;
|
|
}
|
|
|
|
const messages = Array.isArray(task?.messages) ? task.messages : [];
|
|
const index = messages.findIndex((message) => {
|
|
return String(message?.id || message?.streamKey || "").trim() === normalizedMessageId;
|
|
});
|
|
if (index < 0) {
|
|
return 0;
|
|
}
|
|
|
|
return messages.slice(index + 1).filter((message) => {
|
|
return String(message?.role || "") === "user" && String(message?.text || "").trim();
|
|
}).length;
|
|
}
|
|
|
|
buildCanvasForkTarget(selection, followUpTarget) {
|
|
const task = followUpTarget?.task;
|
|
const canvasPath = String(selection?.canvasPath || "").trim();
|
|
const sourceNodeId = String(followUpTarget?.sourceNodeId || "").trim();
|
|
const branchSourceNodeId = String(followUpTarget?.branchSourceNodeId || "").trim();
|
|
const activeSourceNodeId = String(task?.canvasBinding?.activeSourceNodeId || "").trim();
|
|
const forceFork = followUpTarget?.forceFork === true;
|
|
const canForkFromUpstream = followUpTarget?.canForkFromUpstream === true;
|
|
if (!task?.threadId || !canvasPath || !sourceNodeId || !branchSourceNodeId) {
|
|
return null;
|
|
}
|
|
|
|
if (!forceFork && !canForkFromUpstream) {
|
|
return null;
|
|
}
|
|
|
|
if (!forceFork && (!activeSourceNodeId || activeSourceNodeId === branchSourceNodeId)) {
|
|
return null;
|
|
}
|
|
|
|
const branchMessageId = String(followUpTarget?.branchMessageId || "").trim();
|
|
return {
|
|
task,
|
|
sourceNodeId,
|
|
branchSourceNodeId,
|
|
branchMessageId,
|
|
forceFork,
|
|
canForkFromUpstream,
|
|
upstreamNodeId: String(followUpTarget?.upstreamNodeId || "").trim(),
|
|
rollbackTurns: this.countTaskUserTurnsAfterMessage(task, branchMessageId),
|
|
};
|
|
}
|
|
|
|
async confirmCanvasForkFromHere(forkTarget) {
|
|
return new Promise((resolve) => {
|
|
new CanvasForkConfirmModal(this.app, {
|
|
originLabel: forkTarget?.branchSourceNodeId || "",
|
|
rollbackTurns: forkTarget?.rollbackTurns || 0,
|
|
resolve,
|
|
}).open();
|
|
});
|
|
}
|
|
|
|
async runSelectionAsFollowUp(selection, options = {}) {
|
|
const followUpTarget = await this.resolveCanvasFollowUpTarget(selection);
|
|
if (!followUpTarget) {
|
|
return null;
|
|
}
|
|
|
|
const forkTarget = this.buildCanvasForkTarget(selection, followUpTarget);
|
|
if (forkTarget) {
|
|
const decideFork = typeof options.confirmFork === "function"
|
|
? await options.confirmFork(forkTarget)
|
|
: await this.confirmCanvasForkFromHere(forkTarget);
|
|
if (decideFork === "cancel" || decideFork === false || decideFork == null) {
|
|
return {
|
|
cancelled: true,
|
|
task: forkTarget.task,
|
|
message: followUpTarget.message,
|
|
};
|
|
}
|
|
if (decideFork === "fork" || decideFork === true) {
|
|
return this.runSelectionAsFork(selection, followUpTarget, forkTarget);
|
|
}
|
|
}
|
|
|
|
const task = followUpTarget.task;
|
|
await this.setTaskCanvasRunSourceRef(task.taskId, {
|
|
canvasPath: selection.canvasPath,
|
|
nodeId: followUpTarget.sourceNodeId,
|
|
}, {
|
|
throwOnError: true,
|
|
});
|
|
|
|
const markdownFiles = Array.isArray(selection?.markdownFiles) ? selection.markdownFiles : [];
|
|
const imageFiles = Array.isArray(selection?.imageFiles) ? selection.imageFiles : [];
|
|
const runtimeConfig = this.buildRuntimeConfigPayload();
|
|
const rawPrompt = markdownFiles.length > 0
|
|
? buildCanvasSelectionPrompt(selection, followUpTarget.message, { cwd: task.cwd })
|
|
: followUpTarget.message;
|
|
const response = (markdownFiles.length > 0 || imageFiles.length > 0)
|
|
? await this.api.runTask(task.taskId, {
|
|
rawPrompt,
|
|
transcriptMessage: followUpTarget.message,
|
|
forceContext: false,
|
|
selectionContext: this.withVaultScope(selection),
|
|
runtimeConfig,
|
|
})
|
|
: await this.api.sendMessage(
|
|
task.taskId,
|
|
followUpTarget.message,
|
|
runtimeConfig
|
|
);
|
|
|
|
return {
|
|
task: this.mergeTask(response.task, { adoptCurrentVault: true }),
|
|
message: followUpTarget.message,
|
|
};
|
|
}
|
|
|
|
async runSelectionAsFork(selection, followUpTarget, forkTarget) {
|
|
const task = followUpTarget.task;
|
|
const markdownFiles = Array.isArray(selection?.markdownFiles) ? selection.markdownFiles : [];
|
|
const imageFiles = Array.isArray(selection?.imageFiles) ? selection.imageFiles : [];
|
|
const runtimeConfig = this.buildRuntimeConfigPayload();
|
|
const rawPrompt = (markdownFiles.length > 0 || imageFiles.length > 0)
|
|
? buildCanvasSelectionPrompt(selection, followUpTarget.message, { cwd: task.cwd })
|
|
: followUpTarget.message;
|
|
const response = await this.api.forkTask(task.taskId, this.withVaultScope({
|
|
selectionContext: this.withVaultScope(selection),
|
|
cwd: task.cwd || this.resolver.inferWorkingDirectory(selection),
|
|
rawPrompt,
|
|
transcriptMessage: followUpTarget.message,
|
|
forceContext: false,
|
|
branchSourceNodeId: forkTarget.branchSourceNodeId,
|
|
branchMessageId: forkTarget.branchMessageId,
|
|
rollbackTurns: forkTarget.rollbackTurns,
|
|
runtimeConfig,
|
|
}));
|
|
|
|
return {
|
|
task: this.mergeTask(response.task, { adoptCurrentVault: true }),
|
|
message: followUpTarget.message,
|
|
mode: "fork",
|
|
};
|
|
}
|
|
|
|
async handleCreateFollowUpNodeCommand() {
|
|
try {
|
|
const result = await this.createFollowUpNodeFromSelection();
|
|
await this.revealCanvasNodeForEditing(result.canvasPath, result.followUpNodeId);
|
|
this.runtimeIssue = "";
|
|
this.rememberSelectionSnapshot({
|
|
canvasPath: result.canvasPath,
|
|
nodeIds: [result.followUpNodeId],
|
|
});
|
|
await this.appendDebugEvent("canvas_follow_up_node_created", {
|
|
canvasPath: result.canvasPath,
|
|
selectedNodeId: result.selectedNodeId,
|
|
anchorNodeId: result.anchorNodeId,
|
|
followUpNodeId: result.followUpNodeId,
|
|
});
|
|
} catch (error) {
|
|
this.runtimeIssue = String(error?.message || error);
|
|
await this.appendDebugEvent("canvas_follow_up_node_error", {
|
|
message: this.runtimeIssue,
|
|
});
|
|
new Notice(this.runtimeIssue);
|
|
this.requestViewRefresh({ force: true });
|
|
}
|
|
}
|
|
|
|
async handleCreateForkNodeCommand() {
|
|
try {
|
|
const result = await this.createFollowUpNodeFromSelection({ fork: true });
|
|
await this.revealCanvasNodeForEditing(result.canvasPath, result.followUpNodeId);
|
|
this.runtimeIssue = "";
|
|
this.rememberSelectionSnapshot({
|
|
canvasPath: result.canvasPath,
|
|
nodeIds: [result.followUpNodeId],
|
|
});
|
|
await this.appendDebugEvent("canvas_fork_node_created", {
|
|
canvasPath: result.canvasPath,
|
|
selectedNodeId: result.selectedNodeId,
|
|
anchorNodeId: result.anchorNodeId,
|
|
followUpNodeId: result.followUpNodeId,
|
|
});
|
|
} catch (error) {
|
|
this.runtimeIssue = String(error?.message || error);
|
|
await this.appendDebugEvent("canvas_fork_node_error", {
|
|
message: this.runtimeIssue,
|
|
});
|
|
new Notice(this.runtimeIssue);
|
|
this.requestViewRefresh({ force: true });
|
|
}
|
|
}
|
|
|
|
async createFollowUpNodeFromSelection(options = {}) {
|
|
const view = this.resolver.getActiveCanvasView();
|
|
if (!view) {
|
|
throw new Error("Open a Canvas view first.");
|
|
}
|
|
|
|
const file = view.file || this.app.workspace.getActiveFile();
|
|
if (!(file instanceof TFile) || file.extension !== "canvas") {
|
|
throw new Error("The active view is not backed by a .canvas file.");
|
|
}
|
|
|
|
const selectedNodeIds = this.resolver.extractSelectedNodeIds(view).map((nodeId) => String(nodeId)).filter(Boolean);
|
|
if (selectedNodeIds.length !== 1) {
|
|
throw new Error("Select exactly one OpenAgent source or result node first.");
|
|
}
|
|
|
|
await this.resolver.flushPendingCanvasEdits(view);
|
|
const selectedNodeId = selectedNodeIds[0];
|
|
const result = await this.withSerializedCanvasMutation(file.path, async () => {
|
|
const raw = await this.app.vault.cachedRead(file);
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(raw);
|
|
} catch {
|
|
throw new Error(`Unable to parse canvas JSON for ${file.path}`);
|
|
}
|
|
|
|
const nodes = Array.isArray(parsed?.nodes) ? [...parsed.nodes] : [];
|
|
const edges = Array.isArray(parsed?.edges) ? [...parsed.edges] : [];
|
|
const nodeById = new Map(
|
|
nodes
|
|
.map((node) => [String(node?.id || "").trim(), node])
|
|
.filter(([nodeId]) => nodeId)
|
|
);
|
|
const selectedNode = nodeById.get(selectedNodeId) || null;
|
|
if (!selectedNode) {
|
|
throw new Error("The selected Canvas node could not be found.");
|
|
}
|
|
if (options.fork === true && !isOpenAgentAssistantResultNode(selectedNode)) {
|
|
throw new Error("Select an OpenAgent result node before creating a fork.");
|
|
}
|
|
if (isOpenAgentAssistantResultNode(selectedNode)) {
|
|
await this.ensureTaskForOpenAgentResultNode(file.path, selectedNode);
|
|
}
|
|
|
|
const anchor = this.resolveFollowUpAnchorForSelectedNode(file.path, selectedNodeId, nodeById);
|
|
const isForkNode = options.fork === true;
|
|
const followUpNodeId = createCanvasObjectId(isForkNode ? "oa-fork" : "oa-follow-up");
|
|
const followUpNode = this.buildCanvasFollowUpNode(anchor.anchorNode, followUpNodeId, edges, nodeById, {
|
|
fork: isForkNode,
|
|
task: anchor.task,
|
|
branchSourceNodeId: this.getCanvasBranchSourceNodeId(anchor.anchorNode, anchor.anchorNodeId),
|
|
});
|
|
const followUpEdge = {
|
|
id: createCanvasObjectId(isForkNode ? "oa-fork-edge" : "oa-follow-up-edge"),
|
|
fromNode: anchor.anchorNodeId,
|
|
toNode: followUpNodeId,
|
|
fromSide: "bottom",
|
|
toSide: "top",
|
|
...(isForkNode
|
|
? {
|
|
openagent: {
|
|
schemaVersion: OPENAGENT_CANVAS_SCHEMA_VERSION,
|
|
kind: "fork-edge",
|
|
taskId: String(anchor.task?.taskId || ""),
|
|
threadId: String(anchor.task?.threadId || ""),
|
|
sourceNodeId: this.getCanvasBranchSourceNodeId(anchor.anchorNode, anchor.anchorNodeId),
|
|
},
|
|
}
|
|
: {}),
|
|
};
|
|
|
|
const nextParsed = {
|
|
...parsed,
|
|
nodes: [...nodes, followUpNode],
|
|
edges: [...edges, followUpEdge],
|
|
};
|
|
await this.app.vault.modify(file, JSON.stringify(nextParsed, null, 2) + "\n");
|
|
this.primeCanvasSnapshot(file.path, nextParsed);
|
|
return {
|
|
canvasPath: file.path,
|
|
selectedNodeId,
|
|
anchorNodeId: anchor.anchorNodeId,
|
|
followUpNodeId,
|
|
};
|
|
});
|
|
|
|
this.scheduleCanvasNodeHighlightSync(file.path);
|
|
this.requestViewRefresh({ force: true });
|
|
return result;
|
|
}
|
|
|
|
async ensureTaskForOpenAgentResultNode(canvasPath, resultNode) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const taskId = getOpenAgentResultTaskId(resultNode);
|
|
if (!normalizedCanvasPath || !taskId) {
|
|
return null;
|
|
}
|
|
|
|
const cachedTask = this.tasksById[taskId] || null;
|
|
if (cachedTask && this.getTaskCanvasPath(cachedTask) === normalizedCanvasPath) {
|
|
return cachedTask;
|
|
}
|
|
|
|
await this.refreshTask(taskId, { force: true, adoptCurrentVault: true });
|
|
const refreshedTask = this.tasksById[taskId] || null;
|
|
return refreshedTask && this.getTaskCanvasPath(refreshedTask) === normalizedCanvasPath
|
|
? refreshedTask
|
|
: null;
|
|
}
|
|
|
|
resolveFollowUpAnchorForSelectedNode(canvasPath, selectedNodeId, nodeById) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedNodeId = String(selectedNodeId || "").trim();
|
|
const selectedNode = nodeById?.get(normalizedNodeId) || null;
|
|
if (!normalizedCanvasPath || !normalizedNodeId || !selectedNode) {
|
|
throw new Error("The selected Canvas node could not be found.");
|
|
}
|
|
|
|
if (isOpenAgentAssistantResultNode(selectedNode)) {
|
|
const task = this.findTaskByOpenAgentResultNode(normalizedCanvasPath, normalizedNodeId);
|
|
if (!task) {
|
|
throw new Error("Select an existing OpenAgent source or result node first.");
|
|
}
|
|
|
|
return {
|
|
task,
|
|
anchorNodeId: normalizedNodeId,
|
|
anchorNode: selectedNode,
|
|
};
|
|
}
|
|
|
|
const task = this.findTaskByCanvasRunSourceNode(normalizedCanvasPath, normalizedNodeId)
|
|
|| this.findTaskByRootCanvasNode(normalizedCanvasPath, normalizedNodeId);
|
|
if (!task) {
|
|
throw new Error("Select an existing OpenAgent source or result node first.");
|
|
}
|
|
|
|
const resultNodeId = String(
|
|
task?.canvasBinding?.resultNodesBySourceNodeId?.[normalizedNodeId]?.resultNodeId
|
|
|| buildTaskResultNodeId(task.taskId, normalizedNodeId)
|
|
).trim();
|
|
const resultNode = resultNodeId ? nodeById?.get(resultNodeId) || null : null;
|
|
if (resultNode && isOpenAgentAssistantResultNode(resultNode)) {
|
|
return {
|
|
task,
|
|
anchorNodeId: resultNodeId,
|
|
anchorNode: resultNode,
|
|
};
|
|
}
|
|
|
|
return {
|
|
task,
|
|
anchorNodeId: normalizedNodeId,
|
|
anchorNode: selectedNode,
|
|
};
|
|
}
|
|
|
|
getCanvasBranchSourceNodeId(anchorNode, fallbackNodeId = "") {
|
|
return getOpenAgentResultSourceNodeId(anchorNode) || String(fallbackNodeId || anchorNode?.id || "").trim();
|
|
}
|
|
|
|
getCanvasBranchMessageId(task, branchSourceNodeId, anchorNode = null) {
|
|
const nodeMessageId = String(getOpenAgentCanvasMetadata(anchorNode)?.messageId || "").trim();
|
|
if (nodeMessageId) {
|
|
return nodeMessageId;
|
|
}
|
|
|
|
return String(task?.canvasBinding?.resultNodesBySourceNodeId?.[branchSourceNodeId]?.messageId || "").trim();
|
|
}
|
|
|
|
buildCanvasFollowUpNode(anchorNode, followUpNodeId, edges, nodeById, options = {}) {
|
|
const width = clampNumber(
|
|
toFiniteNumber(anchorNode?.width, RESULT_NODE_DEFAULT_WIDTH),
|
|
RESULT_NODE_MIN_WIDTH,
|
|
RESULT_NODE_MAX_WIDTH
|
|
);
|
|
const x = toFiniteNumber(anchorNode?.x, 0);
|
|
const childNodes = (Array.isArray(edges) ? edges : [])
|
|
.filter((edge) => String(edge?.fromNode || "").trim() === String(anchorNode?.id || "").trim())
|
|
.map((edge) => nodeById?.get(String(edge?.toNode || "").trim()) || null)
|
|
.filter(Boolean)
|
|
.filter((node) => !isOpenAgentAssistantResultNode(node));
|
|
const y = childNodes.length > 0
|
|
? childNodes.reduce((maxY, node) => {
|
|
const nextBottom = toFiniteNumber(node?.y, 0) + toFiniteNumber(node?.height, FOLLOW_UP_NODE_DEFAULT_HEIGHT);
|
|
return Math.max(maxY, nextBottom + RESULT_NODE_Y_GAP);
|
|
}, toFiniteNumber(anchorNode?.y, 0) + toFiniteNumber(anchorNode?.height, FOLLOW_UP_NODE_DEFAULT_HEIGHT) + RESULT_NODE_Y_GAP)
|
|
: toFiniteNumber(anchorNode?.y, 0) + toFiniteNumber(anchorNode?.height, FOLLOW_UP_NODE_DEFAULT_HEIGHT) + RESULT_NODE_Y_GAP;
|
|
|
|
const isForkNode = options.fork === true;
|
|
const branchSourceNodeId = String(options.branchSourceNodeId || this.getCanvasBranchSourceNodeId(anchorNode)).trim();
|
|
const branchMessageId = this.getCanvasBranchMessageId(options.task, branchSourceNodeId, anchorNode);
|
|
return {
|
|
id: followUpNodeId,
|
|
type: "text",
|
|
x,
|
|
y,
|
|
width,
|
|
height: FOLLOW_UP_NODE_DEFAULT_HEIGHT,
|
|
text: "",
|
|
...(isForkNode
|
|
? {
|
|
openagent: {
|
|
schemaVersion: OPENAGENT_CANVAS_SCHEMA_VERSION,
|
|
kind: "fork-request",
|
|
taskId: String(options.task?.taskId || ""),
|
|
threadId: String(options.task?.threadId || ""),
|
|
sourceNodeId: branchSourceNodeId,
|
|
branchSourceNodeId,
|
|
branchMessageId,
|
|
},
|
|
}
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
readCanvasNodeIdSetSync(canvasPath, cache = new Map()) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
if (!normalizedCanvasPath) {
|
|
return null;
|
|
}
|
|
|
|
if (cache.has(normalizedCanvasPath)) {
|
|
return cache.get(normalizedCanvasPath);
|
|
}
|
|
|
|
const snapshot = this.getCanvasSnapshotSync(normalizedCanvasPath);
|
|
if (!snapshot) {
|
|
cache.set(normalizedCanvasPath, null);
|
|
return null;
|
|
}
|
|
|
|
cache.set(normalizedCanvasPath, snapshot.nodeIds);
|
|
return snapshot.nodeIds;
|
|
}
|
|
|
|
readCanvasNodeByIdSync(canvasPath, nodeId) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedNodeId = String(nodeId || "").trim();
|
|
if (!normalizedCanvasPath || !normalizedNodeId) {
|
|
return null;
|
|
}
|
|
|
|
const snapshot = this.getCanvasSnapshotSync(normalizedCanvasPath);
|
|
return snapshot?.nodeById.get(normalizedNodeId) || null;
|
|
}
|
|
|
|
isTaskArchived(task, cache = new Map()) {
|
|
const rootRef = this.getTaskRootNodeRef(task);
|
|
if (!rootRef) {
|
|
return false;
|
|
}
|
|
|
|
const nodeIds = this.readCanvasNodeIdSetSync(rootRef.canvasPath, cache);
|
|
if (!nodeIds) {
|
|
return false;
|
|
}
|
|
|
|
return !nodeIds.has(rootRef.nodeId);
|
|
}
|
|
|
|
getFilteredTasksByArchiveState(options = {}) {
|
|
const activeCanvasPath = this.getActiveCanvasPath();
|
|
const scopedCanvasPath = activeCanvasPath || "";
|
|
const archiveState = options.archived === true;
|
|
const archiveProbeCache = options.archiveProbeCache instanceof Map
|
|
? options.archiveProbeCache
|
|
: new Map();
|
|
const filteredTasks = this.getTasks().filter((task) => {
|
|
if (scopedCanvasPath && this.getTaskCanvasPath(task) !== scopedCanvasPath) {
|
|
return false;
|
|
}
|
|
|
|
return this.isTaskArchived(task, archiveProbeCache) === archiveState;
|
|
});
|
|
|
|
if (!scopedCanvasPath) {
|
|
return filteredTasks;
|
|
}
|
|
|
|
const latestBySelection = new Map();
|
|
filteredTasks.forEach((task) => {
|
|
const key = this.getTaskSelectionIdentity(task);
|
|
if (!key || latestBySelection.has(key)) {
|
|
return;
|
|
}
|
|
latestBySelection.set(key, task);
|
|
});
|
|
return Array.from(latestBySelection.values());
|
|
}
|
|
|
|
getVisibleTasks(options = {}) {
|
|
const archiveProbeCache = options.archiveProbeCache;
|
|
return this.getFilteredTasksByArchiveState({ archived: false, archiveProbeCache });
|
|
}
|
|
|
|
getArchivedTasks(options = {}) {
|
|
const archiveProbeCache = options.archiveProbeCache;
|
|
return this.getFilteredTasksByArchiveState({ archived: true, archiveProbeCache });
|
|
}
|
|
|
|
hasUnreadTask(taskId) {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
if (!normalizedTaskId) {
|
|
return false;
|
|
}
|
|
|
|
return Boolean(this.uiState?.unreadTaskStateById?.[normalizedTaskId]);
|
|
}
|
|
|
|
updateTaskUnreadState(taskId, unread) {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
if (!normalizedTaskId) {
|
|
return false;
|
|
}
|
|
|
|
const currentState = normalizeUnreadTaskState(this.uiState?.unreadTaskStateById);
|
|
const hasUnread = Boolean(currentState[normalizedTaskId]);
|
|
if (unread) {
|
|
if (hasUnread) {
|
|
return false;
|
|
}
|
|
currentState[normalizedTaskId] = true;
|
|
} else {
|
|
if (!hasUnread) {
|
|
return false;
|
|
}
|
|
delete currentState[normalizedTaskId];
|
|
}
|
|
|
|
this.uiState.unreadTaskStateById = currentState;
|
|
return true;
|
|
}
|
|
|
|
setTaskUnreadState(taskId, unread, options = {}) {
|
|
const changed = this.updateTaskUnreadState(taskId, unread);
|
|
if (!changed) {
|
|
return false;
|
|
}
|
|
|
|
if (options.persist !== false) {
|
|
void this.persistPluginState();
|
|
}
|
|
if (options.refresh !== false) {
|
|
this.requestViewRefresh();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
pruneUnreadTaskState() {
|
|
const currentState = normalizeUnreadTaskState(this.uiState?.unreadTaskStateById);
|
|
const taskIds = new Set(Object.keys(this.tasksById));
|
|
let changed = false;
|
|
|
|
Object.keys(currentState).forEach((taskId) => {
|
|
if (!taskIds.has(taskId)) {
|
|
delete currentState[taskId];
|
|
changed = true;
|
|
}
|
|
});
|
|
|
|
this.uiState.unreadTaskStateById = currentState;
|
|
return changed;
|
|
}
|
|
|
|
getRunningTasks() {
|
|
const activeCanvasPath = this.getActiveCanvasPath();
|
|
return this.getTasks().filter((task) => {
|
|
if (activeCanvasPath && this.getTaskCanvasPath(task) !== activeCanvasPath) {
|
|
return false;
|
|
}
|
|
|
|
return this.isTaskRunning(task);
|
|
});
|
|
}
|
|
|
|
getPanelTab() {
|
|
return normalizePanelTab(this.uiState?.panelTab);
|
|
}
|
|
|
|
isTaskDetailOpen() {
|
|
return this.uiState?.taskDetailOpen === true;
|
|
}
|
|
|
|
async setTaskDetailOpen(isOpen) {
|
|
const nextValue = isOpen === true;
|
|
this.uiState.panelTab = PANEL_TAB_OPTIONS.TASK_LIST;
|
|
if (this.uiState.taskDetailOpen === nextValue) {
|
|
this.requestViewRefresh();
|
|
return;
|
|
}
|
|
|
|
this.uiState.taskDetailOpen = nextValue;
|
|
await this.persistPluginState();
|
|
this.requestViewRefresh();
|
|
}
|
|
|
|
async setPanelTab(panelTab) {
|
|
const nextPanelTab = normalizePanelTab(panelTab);
|
|
const previousTaskDetailOpen = this.uiState.taskDetailOpen;
|
|
if (nextPanelTab !== PANEL_TAB_OPTIONS.TASK_LIST) {
|
|
this.uiState.taskDetailOpen = false;
|
|
}
|
|
if (this.uiState.panelTab === nextPanelTab) {
|
|
if (previousTaskDetailOpen !== this.uiState.taskDetailOpen) {
|
|
await this.persistPluginState();
|
|
}
|
|
this.requestViewRefresh();
|
|
return;
|
|
}
|
|
|
|
this.uiState.panelTab = nextPanelTab;
|
|
await this.persistPluginState();
|
|
this.requestViewRefresh();
|
|
}
|
|
|
|
getActiveTask() {
|
|
const task = this.tasksById[this.uiState.activeTaskId] || null;
|
|
if (!this.isTaskForCurrentVault(task)) {
|
|
return null;
|
|
}
|
|
const activeCanvasPath = this.getActiveCanvasPath();
|
|
if (!activeCanvasPath) {
|
|
return task;
|
|
}
|
|
|
|
return this.getTaskCanvasPath(task) === activeCanvasPath ? task : null;
|
|
}
|
|
|
|
getDraft(taskId) {
|
|
return this.uiState.draftsByTaskId?.[taskId] || "";
|
|
}
|
|
|
|
setDraft(taskId, value) {
|
|
this.uiState.draftsByTaskId = this.uiState.draftsByTaskId || {};
|
|
this.uiState.draftsByTaskId[taskId] = value;
|
|
}
|
|
|
|
clearDraft(taskId) {
|
|
if (this.uiState.draftsByTaskId) {
|
|
delete this.uiState.draftsByTaskId[taskId];
|
|
this.persistPluginState();
|
|
}
|
|
}
|
|
|
|
getCanvasNodeHighlightRef(task, archiveProbeCache = new Map()) {
|
|
if (this.isTaskArchived(task, archiveProbeCache)) {
|
|
return null;
|
|
}
|
|
|
|
return this.getTaskCanvasRunSourceRef(task);
|
|
}
|
|
|
|
getCanvasNodeHighlightKey(canvasPath, nodeId) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedNodeId = String(nodeId || "").trim();
|
|
if (!normalizedCanvasPath || !normalizedNodeId) {
|
|
return "";
|
|
}
|
|
|
|
return `${normalizedCanvasPath}\0${normalizedNodeId}`;
|
|
}
|
|
|
|
isTaskRunning(task) {
|
|
return Boolean(task?.currentTurnId) || ["starting", "running"].includes(String(task?.status || ""));
|
|
}
|
|
|
|
shouldMarkTaskUnread(previousTask, nextTask) {
|
|
if (!previousTask || !nextTask?.taskId) {
|
|
return false;
|
|
}
|
|
|
|
return this.isTaskRunning(previousTask) && !this.isTaskRunning(nextTask);
|
|
}
|
|
|
|
maybeSyncCanvasNodeHighlights(task, previousTask) {
|
|
const canvasPaths = new Set();
|
|
const archiveProbeCache = new Map();
|
|
[task, previousTask].forEach((entry) => {
|
|
const ref = this.getCanvasNodeHighlightRef(entry, archiveProbeCache);
|
|
if (ref?.canvasPath) {
|
|
canvasPaths.add(ref.canvasPath);
|
|
}
|
|
});
|
|
|
|
canvasPaths.forEach((canvasPath) => {
|
|
this.scheduleCanvasNodeHighlightSync(canvasPath);
|
|
});
|
|
}
|
|
|
|
reconcileAllCanvasNodeHighlights() {
|
|
const canvasPaths = new Set();
|
|
const archiveProbeCache = new Map();
|
|
|
|
Object.values(this.tasksById).forEach((task) => {
|
|
const ref = this.getCanvasNodeHighlightRef(task, archiveProbeCache);
|
|
if (ref?.canvasPath) {
|
|
canvasPaths.add(ref.canvasPath);
|
|
}
|
|
});
|
|
|
|
Object.values(this.uiState.canvasNodeHighlightStateByKey || {}).forEach((entry) => {
|
|
const canvasPath = String(entry?.canvasPath || "").trim();
|
|
if (canvasPath) {
|
|
canvasPaths.add(canvasPath);
|
|
}
|
|
});
|
|
|
|
canvasPaths.forEach((canvasPath) => {
|
|
this.scheduleCanvasNodeHighlightSync(canvasPath);
|
|
});
|
|
}
|
|
|
|
getRunningCanvasNodeHighlightKeys(canvasPath) {
|
|
const runningKeys = new Set();
|
|
const archiveProbeCache = new Map();
|
|
|
|
Object.values(this.tasksById).forEach((task) => {
|
|
const ref = this.getCanvasNodeHighlightRef(task, archiveProbeCache);
|
|
if (!ref || ref.canvasPath !== canvasPath || !this.isTaskRunning(task)) {
|
|
return;
|
|
}
|
|
|
|
const key = this.getCanvasNodeHighlightKey(ref.canvasPath, ref.nodeId);
|
|
if (key) {
|
|
runningKeys.add(key);
|
|
}
|
|
});
|
|
|
|
return runningKeys;
|
|
}
|
|
|
|
getCompletedCanvasNodeHighlightKeys(canvasPath) {
|
|
const completedKeys = new Set();
|
|
const archiveProbeCache = new Map();
|
|
|
|
Object.values(this.tasksById).forEach((task) => {
|
|
const ref = this.getCanvasNodeHighlightRef(task, archiveProbeCache);
|
|
if (!ref || ref.canvasPath !== canvasPath || this.isTaskRunning(task)) {
|
|
return;
|
|
}
|
|
|
|
const assistantMessage = this.getLatestAssistantMessage(task);
|
|
if (
|
|
String(task?.status || "") !== "idle"
|
|
|| !assistantMessage?.text?.trim()
|
|
|| !this.isLatestAssistantMessageCurrentForActiveSource(task, assistantMessage)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const key = this.getCanvasNodeHighlightKey(ref.canvasPath, ref.nodeId);
|
|
if (key) {
|
|
completedKeys.add(key);
|
|
}
|
|
});
|
|
|
|
return completedKeys;
|
|
}
|
|
|
|
getTrackedCanvasNodeHighlightKeys(canvasPath) {
|
|
const trackedKeys = new Set();
|
|
|
|
Object.keys(this.uiState.canvasNodeHighlightStateByKey || {}).forEach((key) => {
|
|
const [trackedCanvasPath] = key.split("\0");
|
|
if (trackedCanvasPath === canvasPath) {
|
|
trackedKeys.add(key);
|
|
}
|
|
});
|
|
|
|
return trackedKeys;
|
|
}
|
|
|
|
scheduleCanvasNodeHighlightSync(canvasPath) {
|
|
if (!canvasPath) {
|
|
return;
|
|
}
|
|
|
|
if (this.canvasNodeHighlightSyncInFlight.has(canvasPath)) {
|
|
this.canvasNodeHighlightSyncPending.add(canvasPath);
|
|
return;
|
|
}
|
|
|
|
this.canvasNodeHighlightSyncInFlight.add(canvasPath);
|
|
void this.syncCanvasNodeHighlightsForCanvas(canvasPath)
|
|
.catch(async (error) => {
|
|
await this.appendDebugEvent("canvas_highlight_error", {
|
|
canvasPath,
|
|
message: String(error?.message || error),
|
|
});
|
|
})
|
|
.finally(() => {
|
|
this.canvasNodeHighlightSyncInFlight.delete(canvasPath);
|
|
if (this.canvasNodeHighlightSyncPending.has(canvasPath)) {
|
|
this.canvasNodeHighlightSyncPending.delete(canvasPath);
|
|
this.scheduleCanvasNodeHighlightSync(canvasPath);
|
|
}
|
|
});
|
|
}
|
|
|
|
async syncCanvasNodeHighlightsForCanvas(canvasPath) {
|
|
await this.withSerializedCanvasMutation(canvasPath, async () => {
|
|
const runningKeys = this.getRunningCanvasNodeHighlightKeys(canvasPath);
|
|
const completedKeys = this.getCompletedCanvasNodeHighlightKeys(canvasPath);
|
|
const trackedKeys = this.getTrackedCanvasNodeHighlightKeys(canvasPath);
|
|
if (runningKeys.size === 0 && completedKeys.size === 0 && trackedKeys.size === 0) {
|
|
return;
|
|
}
|
|
|
|
const highlightStateByKey = this.uiState.canvasNodeHighlightStateByKey || {};
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(canvasPath);
|
|
if (!(abstractFile instanceof TFile) || abstractFile.extension !== "canvas") {
|
|
let stateChanged = false;
|
|
trackedKeys.forEach((key) => {
|
|
if (highlightStateByKey[key]) {
|
|
delete highlightStateByKey[key];
|
|
stateChanged = true;
|
|
}
|
|
});
|
|
if (stateChanged) {
|
|
await this.persistPluginState();
|
|
}
|
|
return;
|
|
}
|
|
|
|
const raw = await this.app.vault.cachedRead(abstractFile);
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(raw);
|
|
} catch {
|
|
throw new Error(`Unable to parse canvas JSON for ${canvasPath}`);
|
|
}
|
|
|
|
const nodes = Array.isArray(parsed?.nodes) ? [...parsed.nodes] : [];
|
|
const relevantKeys = new Set([...runningKeys, ...completedKeys, ...trackedKeys]);
|
|
const seenKeys = new Set();
|
|
let nodesChanged = false;
|
|
let stateChanged = false;
|
|
|
|
for (let index = 0; index < nodes.length; index += 1) {
|
|
const node = nodes[index];
|
|
const nodeId = String(node?.id || "").trim();
|
|
if (!nodeId) {
|
|
continue;
|
|
}
|
|
|
|
const key = this.getCanvasNodeHighlightKey(canvasPath, nodeId);
|
|
if (!relevantKeys.has(key)) {
|
|
continue;
|
|
}
|
|
|
|
seenKeys.add(key);
|
|
const shouldHighlight = runningKeys.has(key);
|
|
const shouldMarkCompleted = !shouldHighlight && completedKeys.has(key);
|
|
const hasColor = Object.prototype.hasOwnProperty.call(node, "color");
|
|
const currentColor = hasColor ? String(node.color) : "";
|
|
const storedState = highlightStateByKey[key] || null;
|
|
|
|
if (shouldHighlight) {
|
|
if (!storedState) {
|
|
highlightStateByKey[key] = {
|
|
canvasPath,
|
|
nodeId,
|
|
originalColor: hasColor ? node.color : null,
|
|
};
|
|
stateChanged = true;
|
|
}
|
|
|
|
const autoFitHeight = getCanvasTextNodeAutoFitHeight(node);
|
|
const shouldResize = Number.isFinite(autoFitHeight) && autoFitHeight > toFiniteNumber(node?.height, 0);
|
|
if (!hasColor || currentColor !== RUNNING_CANVAS_NODE_COLOR || shouldResize) {
|
|
nodes[index] = {
|
|
...node,
|
|
color: RUNNING_CANVAS_NODE_COLOR,
|
|
...(shouldResize ? { height: autoFitHeight } : {}),
|
|
};
|
|
nodesChanged = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (shouldMarkCompleted) {
|
|
if (!hasColor || String(node.color) !== COMPLETED_CANVAS_NODE_COLOR) {
|
|
nodes[index] = {
|
|
...node,
|
|
color: COMPLETED_CANVAS_NODE_COLOR,
|
|
};
|
|
nodesChanged = true;
|
|
}
|
|
|
|
if (storedState) {
|
|
delete highlightStateByKey[key];
|
|
stateChanged = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!storedState) {
|
|
continue;
|
|
}
|
|
|
|
if (!hasColor || String(node.color) !== COMPLETED_CANVAS_NODE_COLOR) {
|
|
nodes[index] = {
|
|
...node,
|
|
color: COMPLETED_CANVAS_NODE_COLOR,
|
|
};
|
|
nodesChanged = true;
|
|
}
|
|
|
|
delete highlightStateByKey[key];
|
|
stateChanged = true;
|
|
}
|
|
|
|
trackedKeys.forEach((key) => {
|
|
if (seenKeys.has(key) || !highlightStateByKey[key]) {
|
|
return;
|
|
}
|
|
|
|
delete highlightStateByKey[key];
|
|
stateChanged = true;
|
|
});
|
|
|
|
if (nodesChanged) {
|
|
parsed = {
|
|
...parsed,
|
|
nodes,
|
|
};
|
|
await this.app.vault.modify(abstractFile, JSON.stringify(parsed, null, 2) + "\n");
|
|
this.primeCanvasSnapshot(canvasPath, parsed);
|
|
}
|
|
|
|
if (stateChanged) {
|
|
await this.persistPluginState();
|
|
}
|
|
});
|
|
}
|
|
|
|
mergeTask(task, options = {}) {
|
|
if (!task) {
|
|
return null;
|
|
}
|
|
|
|
const previousTask = this.tasksById[task.taskId] || null;
|
|
const inputTask = options.adoptCurrentVault === true && !this.getTaskVaultId(task)
|
|
? this.scopeTaskForCurrentVault(task)
|
|
: task;
|
|
const nextTask = this.normalizeTaskForStore(inputTask, previousTask);
|
|
if (!nextTask) {
|
|
return null;
|
|
}
|
|
this.tasksById[nextTask.taskId] = nextTask;
|
|
const didMarkUnread = this.shouldMarkTaskUnread(previousTask, nextTask)
|
|
&& this.updateTaskUnreadState(nextTask.taskId, true);
|
|
if (didMarkUnread) {
|
|
void this.persistPluginState();
|
|
}
|
|
this.syncActiveTaskToCanvasContext();
|
|
this.maybeSyncCanvasNodeHighlights(nextTask, previousTask);
|
|
this.maybeSyncTaskResultNode(nextTask, previousTask);
|
|
this.reconcileRunningTaskStreams();
|
|
return nextTask;
|
|
}
|
|
|
|
taskHasLoadedMessages(task) {
|
|
return Boolean(task) && task.messagesIncluded !== false;
|
|
}
|
|
|
|
normalizeTaskForStore(task, previousTask = null) {
|
|
if (!task?.taskId) {
|
|
return null;
|
|
}
|
|
const scopedTask = !this.getTaskVaultId(task) && this.isTaskForCurrentVault(previousTask)
|
|
? this.scopeTaskForCurrentVault(task)
|
|
: task;
|
|
if (!this.isTaskForCurrentVault(scopedTask)) {
|
|
return null;
|
|
}
|
|
|
|
const nextTask = {
|
|
...scopedTask,
|
|
};
|
|
const shouldReuseDetailedMessages = (
|
|
nextTask.messagesIncluded === false
|
|
&& previousTask
|
|
&& previousTask.messagesIncluded !== false
|
|
&& String(previousTask.taskId || "") === String(this.uiState?.activeTaskId || "")
|
|
&& Array.isArray(previousTask.messages)
|
|
);
|
|
|
|
if (shouldReuseDetailedMessages) {
|
|
nextTask.messages = previousTask.messages;
|
|
nextTask.messagesIncluded = true;
|
|
} else {
|
|
nextTask.messages = Array.isArray(task.messages) ? task.messages : [];
|
|
nextTask.messagesIncluded = task.messagesIncluded !== false;
|
|
}
|
|
|
|
if (typeof nextTask.messageCount !== "number") {
|
|
nextTask.messageCount = nextTask.messages.length;
|
|
}
|
|
|
|
if (!nextTask.latestAssistantMessage && previousTask?.latestAssistantMessage) {
|
|
nextTask.latestAssistantMessage = previousTask.latestAssistantMessage;
|
|
}
|
|
|
|
return nextTask;
|
|
}
|
|
|
|
getLatestAssistantMessage(task) {
|
|
const messages = Array.isArray(task?.messages) ? task.messages : [];
|
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
const message = messages[index];
|
|
const text = String(message?.text || "");
|
|
if (message?.role === "assistant" && text.trim()) {
|
|
return {
|
|
...message,
|
|
text,
|
|
};
|
|
}
|
|
}
|
|
|
|
const latestAssistantMessage = task?.latestAssistantMessage;
|
|
const text = String(latestAssistantMessage?.text || "");
|
|
if (text.trim()) {
|
|
return {
|
|
...latestAssistantMessage,
|
|
text,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
isLatestAssistantMessageCurrentForActiveSource(task, assistantMessage = null) {
|
|
const candidate = assistantMessage || this.getLatestAssistantMessage(task);
|
|
if (!candidate?.text?.trim()) {
|
|
return false;
|
|
}
|
|
|
|
const activeSourceUpdatedAt = String(task?.canvasBinding?.activeSourceUpdatedAt || "").trim();
|
|
if (!activeSourceUpdatedAt) {
|
|
return true;
|
|
}
|
|
|
|
const assistantTimestamp = String(candidate.updatedAt || candidate.createdAt || "").trim();
|
|
return !assistantTimestamp || assistantTimestamp >= activeSourceUpdatedAt;
|
|
}
|
|
|
|
getTaskResultSyncSignature(task) {
|
|
const assistantMessage = this.getLatestAssistantMessage(task);
|
|
if (!assistantMessage?.text?.trim() || !this.isLatestAssistantMessageCurrentForActiveSource(task, assistantMessage)) {
|
|
return "";
|
|
}
|
|
|
|
const sourceRef = this.getTaskCanvasRunSourceRef(task);
|
|
return stableShortHash(`${task?.taskId || ""}\0${sourceRef?.nodeId || ""}\0${assistantMessage.text}`);
|
|
}
|
|
|
|
getTaskResultNodeRef(task) {
|
|
const sourceRef = this.getTaskCanvasRunSourceRef(task);
|
|
const canvasPath = String(sourceRef?.canvasPath || "").trim();
|
|
const sourceNodeId = String(sourceRef?.nodeId || "").trim();
|
|
const boundResultNodeId = String(
|
|
task?.canvasBinding?.resultNodesBySourceNodeId?.[sourceNodeId]?.resultNodeId || ""
|
|
).trim();
|
|
const taskId = String(task?.taskId || "").trim();
|
|
if (!canvasPath || !sourceNodeId || !taskId) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
canvasPath,
|
|
sourceNodeId,
|
|
resultNodeId: boundResultNodeId || buildTaskResultNodeId(taskId, sourceNodeId),
|
|
};
|
|
}
|
|
|
|
canSyncTaskResultNode(task, cache = new Map()) {
|
|
const ref = this.getTaskResultNodeRef(task);
|
|
if (!ref) {
|
|
return false;
|
|
}
|
|
|
|
const nodeIds = this.readCanvasNodeIdSetSync(ref.canvasPath, cache);
|
|
return Boolean(nodeIds?.has(ref.sourceNodeId));
|
|
}
|
|
|
|
doesTaskResultNodeExist(task, cache = new Map()) {
|
|
const ref = this.getTaskResultNodeRef(task);
|
|
if (!ref) {
|
|
return false;
|
|
}
|
|
|
|
const nodeIds = this.readCanvasNodeIdSetSync(ref.canvasPath, cache);
|
|
return Boolean(nodeIds?.has(ref.resultNodeId));
|
|
}
|
|
|
|
doesTaskResultNodeHaveCurrentMetadata(task, signature) {
|
|
const ref = this.getTaskResultNodeRef(task);
|
|
if (!ref) {
|
|
return false;
|
|
}
|
|
|
|
const resultNode = this.readCanvasNodeByIdSync(ref.canvasPath, ref.resultNodeId);
|
|
const metadata = getOpenAgentCanvasMetadata(resultNode);
|
|
return Boolean(
|
|
metadata?.kind === "assistant-result"
|
|
&& String(metadata.taskId || "") === String(task?.taskId || "")
|
|
&& String(metadata.sourceNodeId || "") === ref.sourceNodeId
|
|
&& String(metadata.syncSignature || "") === String(signature || "")
|
|
);
|
|
}
|
|
|
|
shouldAutoSyncTaskResultNode(task, previousTask) {
|
|
const selection = task?.selectionContext;
|
|
const nodeIds = Array.isArray(selection?.nodeIds)
|
|
? selection.nodeIds.map((nodeId) => String(nodeId)).filter(Boolean)
|
|
: [];
|
|
const textBlocks = Array.isArray(selection?.textBlocks) ? selection.textBlocks : [];
|
|
const markdownFiles = Array.isArray(selection?.markdownFiles) ? selection.markdownFiles : [];
|
|
const imageFiles = Array.isArray(selection?.imageFiles) ? selection.imageFiles : [];
|
|
const hasSingleSupportedSource = (
|
|
(textBlocks.length === 1)
|
|
|| (textBlocks.length === 0 && markdownFiles.length === 1)
|
|
|| (textBlocks.length === 0 && markdownFiles.length === 0 && imageFiles.length === 1)
|
|
);
|
|
if (nodeIds.length !== 1 || !hasSingleSupportedSource) {
|
|
return false;
|
|
}
|
|
|
|
if (String(task?.status || "") !== "idle" || task?.currentTurnId) {
|
|
return false;
|
|
}
|
|
|
|
const canvasNodeCache = new Map();
|
|
if (!this.canSyncTaskResultNode(task, canvasNodeCache)) {
|
|
return false;
|
|
}
|
|
|
|
const signature = this.getTaskResultSyncSignature(task);
|
|
if (!signature) {
|
|
return false;
|
|
}
|
|
|
|
const resultNodeExists = this.doesTaskResultNodeExist(task, canvasNodeCache);
|
|
const resultNodeMetadataCurrent = resultNodeExists
|
|
? this.doesTaskResultNodeHaveCurrentMetadata(task, signature)
|
|
: false;
|
|
if (
|
|
this.resultNodeSyncStateByTaskId[task.taskId] === signature
|
|
&& resultNodeExists
|
|
&& resultNodeMetadataCurrent
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (resultNodeExists && !resultNodeMetadataCurrent) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
previousTask
|
|
&& (
|
|
previousTask.currentTurnId
|
|
|| ["starting", "running"].includes(String(previousTask.status || ""))
|
|
)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return !resultNodeExists;
|
|
}
|
|
|
|
maybeSyncTaskResultNode(task, previousTask) {
|
|
if (!this.shouldAutoSyncTaskResultNode(task, previousTask)) {
|
|
return;
|
|
}
|
|
|
|
if (this.resultNodeSyncInFlight.has(task.taskId)) {
|
|
return;
|
|
}
|
|
|
|
const signature = this.getTaskResultSyncSignature(task);
|
|
if (!signature) {
|
|
return;
|
|
}
|
|
|
|
this.resultNodeSyncInFlight.add(task.taskId);
|
|
void this.syncTaskResultNode(task, signature)
|
|
.catch((error) => {
|
|
const message = String(error?.message || error);
|
|
void this.appendDebugEvent("canvas_result_error", {
|
|
taskId: task.taskId,
|
|
message,
|
|
});
|
|
new Notice(`Unable to write the Canvas result node: ${message}`);
|
|
})
|
|
.finally(() => {
|
|
this.resultNodeSyncInFlight.delete(task.taskId);
|
|
});
|
|
}
|
|
|
|
async syncTaskResultNode(task, signature) {
|
|
const assistantMessage = this.getLatestAssistantMessage(task);
|
|
const result = await this.upsertCanvasResultNode(task, assistantMessage, signature);
|
|
const bindingResponse = await this.api.updateTaskCanvasBinding(task.taskId, {
|
|
canvasPath: result.canvasPath,
|
|
activeSourceNodeId: result.sourceNodeId,
|
|
resultNode: {
|
|
sourceNodeId: result.sourceNodeId,
|
|
resultNodeId: result.resultNodeId,
|
|
edgeId: result.edgeId,
|
|
messageId: String(assistantMessage?.id || ""),
|
|
syncSignature: signature,
|
|
},
|
|
});
|
|
this.mergeTask(bindingResponse.task);
|
|
this.resultNodeSyncStateByTaskId[task.taskId] = signature;
|
|
await this.persistPluginState();
|
|
await this.appendDebugEvent("canvas_result_synced", {
|
|
taskId: task.taskId,
|
|
canvasPath: result.canvasPath,
|
|
sourceNodeId: result.sourceNodeId,
|
|
resultNodeId: result.resultNodeId,
|
|
});
|
|
new Notice("Created a linked Canvas result node.");
|
|
}
|
|
|
|
async upsertCanvasResultNode(task, assistantMessage, signature = "") {
|
|
const sourceRef = this.getTaskCanvasRunSourceRef(task);
|
|
const canvasPath = String(sourceRef?.canvasPath || task?.selectionContext?.canvasPath || "").trim();
|
|
const sourceNodeId = String(sourceRef?.nodeId || task?.selectionContext?.nodeIds?.[0] || "").trim();
|
|
if (!canvasPath || !sourceNodeId) {
|
|
throw new Error("Task is missing the Canvas source node.");
|
|
}
|
|
|
|
const normalizedText = String(assistantMessage?.text || assistantMessage || "").trim();
|
|
if (!normalizedText) {
|
|
throw new Error("There is no assistant result to write back into the Canvas.");
|
|
}
|
|
const canvasText = this.normalizeAssistantLinksForObsidian(normalizedText);
|
|
|
|
return this.withSerializedCanvasMutation(canvasPath, async () => {
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(canvasPath);
|
|
if (!(abstractFile instanceof TFile) || abstractFile.extension !== "canvas") {
|
|
throw new Error(`Canvas file not found: ${canvasPath}`);
|
|
}
|
|
|
|
const raw = await this.app.vault.cachedRead(abstractFile);
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(raw);
|
|
} catch {
|
|
throw new Error(`Unable to parse canvas JSON for ${canvasPath}`);
|
|
}
|
|
|
|
const nodes = Array.isArray(parsed?.nodes) ? [...parsed.nodes] : [];
|
|
const edges = Array.isArray(parsed?.edges) ? [...parsed.edges] : [];
|
|
const sourceNodeIndex = nodes.findIndex((node) => String(node?.id || "") === sourceNodeId);
|
|
if (sourceNodeIndex < 0) {
|
|
throw new Error(`Source Canvas node is missing: ${sourceNodeId}`);
|
|
}
|
|
|
|
const sourceNode = nodes[sourceNodeIndex] || {};
|
|
const nextSourceNode = {
|
|
...sourceNode,
|
|
color: COMPLETED_CANVAS_NODE_COLOR,
|
|
};
|
|
nodes[sourceNodeIndex] = nextSourceNode;
|
|
const resultNodeId = buildTaskResultNodeId(task.taskId, sourceNodeId);
|
|
const edgeId = `oa-edge-${stableShortHash(`${sourceNodeId}\0${task.taskId}`)}`;
|
|
const existingResultNodeIndex = nodes.findIndex((node) => String(node?.id || "") === resultNodeId);
|
|
const existingResultNode = existingResultNodeIndex >= 0 ? nodes[existingResultNodeIndex] : null;
|
|
const resultMetadata = {
|
|
...(getOpenAgentCanvasMetadata(existingResultNode) || {}),
|
|
schemaVersion: OPENAGENT_CANVAS_SCHEMA_VERSION,
|
|
kind: "assistant-result",
|
|
taskId: String(task.taskId || ""),
|
|
threadId: String(task.threadId || ""),
|
|
sourceNodeId,
|
|
messageId: String(assistantMessage?.id || ""),
|
|
syncSignature: String(signature || this.getTaskResultSyncSignature(task) || ""),
|
|
};
|
|
|
|
const defaultWidth = clampNumber(
|
|
toFiniteNumber(existingResultNode?.width, toFiniteNumber(sourceNode.width, RESULT_NODE_DEFAULT_WIDTH)),
|
|
RESULT_NODE_MIN_WIDTH,
|
|
RESULT_NODE_MAX_WIDTH
|
|
);
|
|
const defaultX = toFiniteNumber(sourceNode.x, 0);
|
|
const siblingResultNodes = edges
|
|
.filter((edge) => String(edge?.fromNode || "") === sourceNodeId)
|
|
.map((edge) => nodes.find((node) => String(node?.id || "") === String(edge?.toNode || "")))
|
|
.filter((node) => node && String(node.id || "") !== resultNodeId && isOpenAgentAssistantResultNode(node));
|
|
const defaultY = siblingResultNodes.length > 0
|
|
? siblingResultNodes.reduce((maxY, node) => {
|
|
const nextBottom = toFiniteNumber(node.y, toFiniteNumber(sourceNode.y, 0)) + toFiniteNumber(node.height, RESULT_NODE_MIN_HEIGHT);
|
|
return Math.max(maxY, nextBottom + RESULT_NODE_Y_GAP);
|
|
}, toFiniteNumber(sourceNode.y, 0) + toFiniteNumber(sourceNode.height, RESULT_NODE_MIN_HEIGHT) + RESULT_NODE_Y_GAP)
|
|
: toFiniteNumber(sourceNode.y, 0) + toFiniteNumber(sourceNode.height, RESULT_NODE_MIN_HEIGHT) + RESULT_NODE_Y_GAP;
|
|
const resultNode = {
|
|
...(existingResultNode || {}),
|
|
id: resultNodeId,
|
|
type: "text",
|
|
x: existingResultNode ? toFiniteNumber(existingResultNode.x, defaultX) : defaultX,
|
|
y: existingResultNode ? toFiniteNumber(existingResultNode.y, defaultY) : defaultY,
|
|
width: existingResultNode ? clampNumber(toFiniteNumber(existingResultNode.width, defaultWidth), RESULT_NODE_MIN_WIDTH, RESULT_NODE_MAX_WIDTH) : defaultWidth,
|
|
height: estimateCanvasTextNodeHeight(
|
|
canvasText,
|
|
existingResultNode ? toFiniteNumber(existingResultNode.width, defaultWidth) : defaultWidth
|
|
),
|
|
text: canvasText,
|
|
openagent: resultMetadata,
|
|
};
|
|
|
|
if (existingResultNodeIndex >= 0) {
|
|
nodes[existingResultNodeIndex] = resultNode;
|
|
} else {
|
|
nodes.push(resultNode);
|
|
}
|
|
|
|
const existingEdgeIndex = edges.findIndex((edge) => (
|
|
String(edge?.id || "") === edgeId
|
|
|| (
|
|
String(edge?.fromNode || "") === sourceNodeId
|
|
&& String(edge?.toNode || "") === resultNodeId
|
|
)
|
|
));
|
|
const edge = {
|
|
...(existingEdgeIndex >= 0 ? edges[existingEdgeIndex] : {}),
|
|
id: edgeId,
|
|
fromNode: sourceNodeId,
|
|
toNode: resultNodeId,
|
|
fromSide: existingEdgeIndex >= 0 ? String(edges[existingEdgeIndex]?.fromSide || "bottom") : "bottom",
|
|
toSide: existingEdgeIndex >= 0 ? String(edges[existingEdgeIndex]?.toSide || "top") : "top",
|
|
openagent: {
|
|
...(existingEdgeIndex >= 0 ? getOpenAgentCanvasMetadata(edges[existingEdgeIndex]) || {} : {}),
|
|
schemaVersion: OPENAGENT_CANVAS_SCHEMA_VERSION,
|
|
kind: "result-edge",
|
|
taskId: String(task.taskId || ""),
|
|
threadId: String(task.threadId || ""),
|
|
sourceNodeId,
|
|
resultNodeId,
|
|
},
|
|
};
|
|
|
|
if (existingEdgeIndex >= 0) {
|
|
edges[existingEdgeIndex] = edge;
|
|
} else {
|
|
edges.push(edge);
|
|
}
|
|
|
|
parsed = {
|
|
...parsed,
|
|
nodes,
|
|
edges,
|
|
};
|
|
await this.app.vault.modify(abstractFile, JSON.stringify(parsed, null, 2) + "\n");
|
|
this.primeCanvasSnapshot(canvasPath, parsed);
|
|
|
|
return {
|
|
canvasPath,
|
|
sourceNodeId,
|
|
resultNodeId,
|
|
edgeId,
|
|
};
|
|
});
|
|
}
|
|
|
|
async activateView() {
|
|
let leaf = this.app.workspace.getLeavesOfType(VIEW_TYPE)[0];
|
|
if (!leaf) {
|
|
leaf = this.app.workspace.getRightLeaf(false);
|
|
await leaf.setViewState({ type: VIEW_TYPE, active: true });
|
|
}
|
|
|
|
this.app.workspace.revealLeaf(leaf);
|
|
this.requestViewRefresh();
|
|
}
|
|
|
|
async activateVoiceGraphView() {
|
|
let leaf = this.app.workspace.getLeavesOfType(VOICE_GRAPH_VIEW_TYPE)[0];
|
|
if (!leaf) {
|
|
leaf = this.app.workspace.getLeaf("tab");
|
|
await leaf.setViewState({ type: VOICE_GRAPH_VIEW_TYPE, active: true });
|
|
}
|
|
|
|
this.app.workspace.revealLeaf(leaf);
|
|
}
|
|
|
|
async openThreadList() {
|
|
await this.activateView();
|
|
await this.setPanelTab(PANEL_TAB_OPTIONS.WORKSPACE_SETTINGS);
|
|
}
|
|
|
|
getTaskJumpNodeRef(task) {
|
|
if (!task) {
|
|
return null;
|
|
}
|
|
|
|
const assistantMessage = this.getLatestAssistantMessage(task);
|
|
const resultRef = this.getTaskResultNodeRef(task);
|
|
if (
|
|
resultRef
|
|
&& assistantMessage?.text?.trim()
|
|
&& this.isLatestAssistantMessageCurrentForActiveSource(task, assistantMessage)
|
|
&& this.readCanvasNodeByIdSync(resultRef.canvasPath, resultRef.resultNodeId)
|
|
) {
|
|
return {
|
|
canvasPath: resultRef.canvasPath,
|
|
nodeId: resultRef.resultNodeId,
|
|
};
|
|
}
|
|
|
|
const sourceRef = this.getTaskCanvasRunSourceRef(task);
|
|
if (sourceRef && this.readCanvasNodeByIdSync(sourceRef.canvasPath, sourceRef.nodeId)) {
|
|
return sourceRef;
|
|
}
|
|
|
|
const rootRef = this.getTaskRootNodeRef(task);
|
|
if (rootRef && this.readCanvasNodeByIdSync(rootRef.canvasPath, rootRef.nodeId)) {
|
|
return rootRef;
|
|
}
|
|
|
|
return sourceRef || rootRef || null;
|
|
}
|
|
|
|
findCanvasLeafByPath(canvasPath) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
if (!normalizedCanvasPath) {
|
|
return null;
|
|
}
|
|
|
|
return this.app.workspace.getLeavesOfType("canvas").find((leaf) => {
|
|
return String(leaf?.view?.file?.path || "").trim() === normalizedCanvasPath;
|
|
}) || null;
|
|
}
|
|
|
|
findCanvasRuntimeNodeById(canvas, nodeId) {
|
|
const normalizedNodeId = String(nodeId || "").trim();
|
|
if (!canvas || !normalizedNodeId) {
|
|
return null;
|
|
}
|
|
|
|
const getCandidateNodeId = (candidate) => {
|
|
if (!candidate || typeof candidate !== "object") {
|
|
return "";
|
|
}
|
|
|
|
const directIds = [
|
|
candidate.id,
|
|
candidate.node?.id,
|
|
candidate.data?.id,
|
|
candidate.node?.data?.id,
|
|
candidate.item?.id,
|
|
candidate.item?.data?.id,
|
|
];
|
|
for (const candidateId of directIds) {
|
|
const normalizedCandidateId = String(candidateId || "").trim();
|
|
if (normalizedCandidateId) {
|
|
return normalizedCandidateId;
|
|
}
|
|
}
|
|
|
|
if (typeof candidate.getData === "function") {
|
|
try {
|
|
return String(candidate.getData()?.id || "").trim();
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
return "";
|
|
};
|
|
const matchesNodeId = (candidate) => getCandidateNodeId(candidate) === normalizedNodeId;
|
|
const candidateCollections = [
|
|
canvas?.nodes,
|
|
canvas?.nodeMap,
|
|
canvas?.selectionManager?.nodes,
|
|
].filter(Boolean);
|
|
|
|
for (const collection of candidateCollections) {
|
|
if (collection instanceof Map) {
|
|
const directMatch = collection.get(normalizedNodeId);
|
|
if (directMatch) {
|
|
return directMatch;
|
|
}
|
|
|
|
for (const value of collection.values()) {
|
|
if (matchesNodeId(value)) {
|
|
return value;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (Array.isArray(collection)) {
|
|
const directMatch = collection.find((value) => matchesNodeId(value));
|
|
if (directMatch) {
|
|
return directMatch;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (typeof collection === "object") {
|
|
const directMatch = collection[normalizedNodeId];
|
|
if (matchesNodeId(directMatch)) {
|
|
return directMatch;
|
|
}
|
|
|
|
for (const value of Object.values(collection)) {
|
|
if (matchesNodeId(value)) {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
selectCanvasNodeInView(view, nodeId, runtimeNode = null) {
|
|
const normalizedNodeId = String(nodeId || "").trim();
|
|
const canvas = view?.canvas;
|
|
if (!normalizedNodeId || !canvas) {
|
|
return false;
|
|
}
|
|
|
|
const deselectCalls = [
|
|
[canvas, "deselectAll"],
|
|
[canvas?.selectionManager, "deselectAll"],
|
|
[canvas?.selectionManager, "clear"],
|
|
];
|
|
deselectCalls.forEach(([owner, method]) => {
|
|
if (typeof owner?.[method] !== "function") {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
owner[method]();
|
|
} catch {
|
|
// Ignore Canvas API differences across Obsidian versions.
|
|
}
|
|
});
|
|
|
|
if (runtimeNode) {
|
|
const selectCalls = [
|
|
[canvas, "select"],
|
|
[canvas?.selectionManager, "select"],
|
|
];
|
|
for (const [owner, method] of selectCalls) {
|
|
if (typeof owner?.[method] !== "function") {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
owner[method](runtimeNode);
|
|
return true;
|
|
} catch {
|
|
// Fall through to DOM selection fallback below.
|
|
}
|
|
}
|
|
}
|
|
|
|
const selector = `[data-node-id="${escapeAttributeSelectorValue(normalizedNodeId)}"]`;
|
|
const nodeEl = view?.containerEl?.querySelector?.(selector);
|
|
if (!nodeEl) {
|
|
return false;
|
|
}
|
|
|
|
["mousedown", "mouseup", "click"].forEach((eventName) => {
|
|
try {
|
|
nodeEl.dispatchEvent(new MouseEvent(eventName, {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
button: 0,
|
|
}));
|
|
} catch {
|
|
// Ignore DOM event dispatch failures on older runtimes.
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
getCanvasNodeElement(view, nodeId) {
|
|
const normalizedNodeId = String(nodeId || "").trim();
|
|
if (!normalizedNodeId) {
|
|
return null;
|
|
}
|
|
|
|
const selector = `[data-node-id="${escapeAttributeSelectorValue(normalizedNodeId)}"]`;
|
|
return view?.containerEl?.querySelector?.(selector) || null;
|
|
}
|
|
|
|
getEditableCanvasNodeElement(view, nodeId) {
|
|
const nodeEl = this.getCanvasNodeElement(view, nodeId);
|
|
if (!nodeEl) {
|
|
return null;
|
|
}
|
|
|
|
return nodeEl.querySelector?.("textarea, input, [contenteditable='true'], .cm-content")
|
|
|| nodeEl;
|
|
}
|
|
|
|
focusEditableCanvasNodeElement(element) {
|
|
if (!element || typeof element.focus !== "function") {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
element.focus();
|
|
} catch {
|
|
return false;
|
|
}
|
|
|
|
if (typeof element.setSelectionRange === "function" && typeof element.value === "string") {
|
|
try {
|
|
const offset = element.value.length;
|
|
element.setSelectionRange(offset, offset);
|
|
} catch {
|
|
// Ignore selection placement failures on unsupported inputs.
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
getRuntimeEditableCanvasNodeElement(runtimeNode) {
|
|
if (!runtimeNode || typeof runtimeNode !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const directEditor = runtimeNode?.child?.editMode?.cm?.dom;
|
|
if (directEditor instanceof HTMLElement) {
|
|
return directEditor.querySelector?.(".cm-content") || directEditor;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async enterCanvasNodeEditModeInView(view, nodeId) {
|
|
const normalizedNodeId = String(nodeId || "").trim();
|
|
if (!view?.canvas || !normalizedNodeId) {
|
|
return false;
|
|
}
|
|
|
|
let runtimeNode = this.findCanvasRuntimeNodeById(view.canvas, normalizedNodeId);
|
|
this.selectCanvasNodeInView(view, normalizedNodeId, runtimeNode);
|
|
const tryStartEditing = () => {
|
|
runtimeNode = this.findCanvasRuntimeNodeById(view.canvas, normalizedNodeId) || runtimeNode;
|
|
if (typeof runtimeNode?.startEditing === "function") {
|
|
try {
|
|
runtimeNode.startEditing();
|
|
return true;
|
|
} catch {
|
|
// Fall through to older edit-mode shims below.
|
|
}
|
|
}
|
|
|
|
if (typeof runtimeNode?.setIsEditing === "function") {
|
|
try {
|
|
runtimeNode.setIsEditing(true);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
if (attempt > 0) {
|
|
await sleep(attempt === 1 ? 0 : 50);
|
|
}
|
|
|
|
tryStartEditing();
|
|
const runtimeEditableElement = this.getRuntimeEditableCanvasNodeElement(runtimeNode);
|
|
if (this.resolver.isEditableCanvasElement(runtimeEditableElement) && this.focusEditableCanvasNodeElement(runtimeEditableElement)) {
|
|
return true;
|
|
}
|
|
|
|
if (typeof runtimeNode?.focus === "function") {
|
|
try {
|
|
runtimeNode.focus();
|
|
} catch {
|
|
// Ignore runtime focus failures and continue to DOM fallbacks.
|
|
}
|
|
}
|
|
|
|
const editableElement = this.getEditableCanvasNodeElement(view, normalizedNodeId);
|
|
if (this.resolver.isEditableCanvasElement(editableElement) && this.focusEditableCanvasNodeElement(editableElement)) {
|
|
return true;
|
|
}
|
|
|
|
const nodeEl = this.getCanvasNodeElement(view, normalizedNodeId);
|
|
if (!nodeEl) {
|
|
continue;
|
|
}
|
|
|
|
const interactionTarget = nodeEl.querySelector?.(".canvas-node-content, .canvas-node-container")
|
|
|| nodeEl;
|
|
try {
|
|
interactionTarget.dispatchEvent(new MouseEvent("dblclick", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
button: 0,
|
|
detail: 2,
|
|
}));
|
|
} catch {
|
|
// Ignore DOM interaction failures on older runtimes.
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
focusCanvasNodeInView(view, canvasPath, nodeId) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedNodeId = String(nodeId || "").trim();
|
|
if (!view?.canvas || !normalizedCanvasPath || !normalizedNodeId) {
|
|
return false;
|
|
}
|
|
|
|
const node = this.readCanvasNodeByIdSync(normalizedCanvasPath, normalizedNodeId);
|
|
if (!node) {
|
|
return false;
|
|
}
|
|
|
|
if (typeof view.canvas.zoomToBbox === "function") {
|
|
try {
|
|
view.canvas.zoomToBbox({
|
|
minX: toFiniteNumber(node?.x, 0) - Math.max(1, toFiniteNumber(node?.width, RESULT_NODE_DEFAULT_WIDTH)),
|
|
minY: toFiniteNumber(node?.y, 0) - Math.max(1, toFiniteNumber(node?.height, RESULT_NODE_MIN_HEIGHT)),
|
|
maxX: toFiniteNumber(node?.x, 0) + Math.max(1, toFiniteNumber(node?.width, RESULT_NODE_DEFAULT_WIDTH)),
|
|
maxY: toFiniteNumber(node?.y, 0) + Math.max(1, toFiniteNumber(node?.height, RESULT_NODE_MIN_HEIGHT)),
|
|
});
|
|
} catch {
|
|
// Ignore zoom failures and still attempt selection below.
|
|
}
|
|
}
|
|
|
|
const runtimeNode = this.findCanvasRuntimeNodeById(view.canvas, normalizedNodeId);
|
|
return this.selectCanvasNodeInView(view, normalizedNodeId, runtimeNode);
|
|
}
|
|
|
|
async revealCanvasNode(canvasPath, nodeId) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedNodeId = String(nodeId || "").trim();
|
|
if (!normalizedCanvasPath || !normalizedNodeId) {
|
|
return false;
|
|
}
|
|
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(normalizedCanvasPath);
|
|
if (!(abstractFile instanceof TFile) || abstractFile.extension !== "canvas") {
|
|
return false;
|
|
}
|
|
|
|
const activeLeaf = this.app.workspace.activeLeaf;
|
|
const reusableLeaf = this.app.workspace.getMostRecentLeaf();
|
|
const leaf = this.findCanvasLeafByPath(normalizedCanvasPath)
|
|
|| (activeLeaf?.view?.getViewType?.() === "canvas" ? activeLeaf : null)
|
|
|| this.app.workspace.getLeavesOfType("canvas")[0]
|
|
|| (reusableLeaf?.view?.getViewType?.() === VIEW_TYPE ? null : reusableLeaf)
|
|
|| this.app.workspace.getLeaf(true);
|
|
await leaf.openFile(abstractFile);
|
|
this.app.workspace.revealLeaf(leaf);
|
|
|
|
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
const view = leaf?.view?.getViewType?.() === "canvas"
|
|
? leaf.view
|
|
: this.resolver.getActiveCanvasView();
|
|
const viewCanvasPath = String(view?.file?.path || "").trim();
|
|
if (viewCanvasPath === normalizedCanvasPath && this.focusCanvasNodeInView(view, normalizedCanvasPath, normalizedNodeId)) {
|
|
return true;
|
|
}
|
|
|
|
await sleep(attempt === 0 ? 0 : 50);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async revealCanvasNodeForEditing(canvasPath, nodeId) {
|
|
const didReveal = await this.revealCanvasNode(canvasPath, nodeId);
|
|
if (!didReveal) {
|
|
return false;
|
|
}
|
|
|
|
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
const view = this.findCanvasLeafByPath(canvasPath)?.view || this.resolver.getActiveCanvasView();
|
|
const viewCanvasPath = String(view?.file?.path || "").trim();
|
|
if (viewCanvasPath === String(canvasPath || "").trim() && await this.enterCanvasNodeEditModeInView(view, nodeId)) {
|
|
return true;
|
|
}
|
|
|
|
await sleep(attempt === 0 ? 0 : 50);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async revealTaskInCanvas(task) {
|
|
const ref = this.getTaskJumpNodeRef(task);
|
|
if (!ref) {
|
|
return false;
|
|
}
|
|
|
|
return this.revealCanvasNode(ref.canvasPath, ref.nodeId);
|
|
}
|
|
|
|
async activateTaskFromList(taskId, options = {}) {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
const task = this.tasksById[normalizedTaskId] || null;
|
|
if (!task) {
|
|
return false;
|
|
}
|
|
|
|
await this.setActiveTask(normalizedTaskId, {
|
|
revealInActiveTab: options.revealInActiveTab !== false,
|
|
showDetail: options.showDetail === true,
|
|
});
|
|
if (options.revealInCanvas === false) {
|
|
return true;
|
|
}
|
|
|
|
return this.revealTaskInCanvas(this.tasksById[normalizedTaskId] || task);
|
|
}
|
|
|
|
async ensurePanelVisibleOnStartup() {
|
|
if (this.app.workspace.getLeavesOfType(VIEW_TYPE).length > 0) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.activateView();
|
|
} catch {
|
|
// Ignore startup panel errors and let the manual command remain the fallback.
|
|
}
|
|
}
|
|
|
|
handleCanvasFileMutation(file) {
|
|
if (!(file instanceof TFile) || file.extension !== "canvas") {
|
|
return;
|
|
}
|
|
|
|
const canvasPath = String(file.path || "").trim();
|
|
if (!canvasPath) {
|
|
return;
|
|
}
|
|
|
|
const previousSnapshot = this.canvasSnapshotCacheByPath.get(canvasPath) || null;
|
|
this.invalidateCanvasSnapshot(canvasPath);
|
|
|
|
const activeCanvasPath = this.getActiveCanvasPath();
|
|
const affectsKnownTask = Object.values(this.tasksById).some((task) => this.getTaskCanvasPath(task) === canvasPath);
|
|
if (!affectsKnownTask && activeCanvasPath !== canvasPath) {
|
|
return;
|
|
}
|
|
|
|
const nextSnapshot = this.getCanvasSnapshotSync(canvasPath);
|
|
const autoRunNodeIds = this.getAutoRunCandidateNodeIds(previousSnapshot, nextSnapshot);
|
|
if (autoRunNodeIds.length > 0) {
|
|
void this.triggerAutoRunForCanvasNodes(canvasPath, autoRunNodeIds);
|
|
}
|
|
|
|
this.syncActiveTaskToCanvasContext();
|
|
void this.refreshSelectedGroupContextPreview();
|
|
this.scheduleCanvasNodeHighlightSync(canvasPath);
|
|
this.requestViewRefresh();
|
|
}
|
|
|
|
hasFocusedComposer() {
|
|
return this.app.workspace.getLeavesOfType(VIEW_TYPE).some((leaf) => {
|
|
return typeof leaf.view?.hasFocusedComposer === "function" && leaf.view.hasFocusedComposer();
|
|
});
|
|
}
|
|
|
|
hasHoveredTaskList() {
|
|
return this.app.workspace.getLeavesOfType(VIEW_TYPE).some((leaf) => {
|
|
return typeof leaf.view?.hasHoveredTaskList === "function" && leaf.view.hasHoveredTaskList();
|
|
});
|
|
}
|
|
|
|
setComposerFocused(isFocused) {
|
|
this.composerFocused = Boolean(isFocused);
|
|
if (this.composerFocused) {
|
|
window.clearTimeout(this.viewRefreshTimer);
|
|
return;
|
|
}
|
|
|
|
if (!this.pendingViewRefresh) {
|
|
return;
|
|
}
|
|
|
|
window.clearTimeout(this.viewRefreshTimer);
|
|
this.viewRefreshTimer = window.setTimeout(() => {
|
|
if (this.pendingViewRefresh && !this.hasFocusedComposer() && !this.hasHoveredTaskList()) {
|
|
this.requestViewRefresh({ force: true });
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
setTaskListHovered(isHovered) {
|
|
this.taskListHovered = Boolean(isHovered);
|
|
if (this.taskListHovered) {
|
|
window.clearTimeout(this.viewRefreshTimer);
|
|
return;
|
|
}
|
|
|
|
if (!this.pendingViewRefresh) {
|
|
return;
|
|
}
|
|
|
|
window.clearTimeout(this.viewRefreshTimer);
|
|
this.viewRefreshTimer = window.setTimeout(() => {
|
|
if (this.pendingViewRefresh && !this.hasFocusedComposer() && !this.hasHoveredTaskList()) {
|
|
this.requestViewRefresh({ force: true });
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
requestViewRefresh(options = {}) {
|
|
const force = options.force === true;
|
|
const streamTask = options.streamTask || null;
|
|
if (!force && streamTask) {
|
|
const patchedAnyView = this.app.workspace.getLeavesOfType(VIEW_TYPE).some((leaf) => {
|
|
return typeof leaf.view?.tryApplyStreamingTaskUpdate === "function"
|
|
&& leaf.view.tryApplyStreamingTaskUpdate(streamTask);
|
|
});
|
|
if (patchedAnyView) {
|
|
this.pendingViewRefresh = false;
|
|
window.clearTimeout(this.viewRefreshTimer);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!force && (this.composerFocused || this.hasFocusedComposer() || this.taskListHovered || this.hasHoveredTaskList())) {
|
|
this.pendingViewRefresh = true;
|
|
window.clearTimeout(this.viewRefreshTimer);
|
|
return;
|
|
}
|
|
|
|
this.pendingViewRefresh = false;
|
|
window.clearTimeout(this.viewRefreshTimer);
|
|
this.viewRefreshTimer = window.setTimeout(() => {
|
|
this.app.workspace.getLeavesOfType(VIEW_TYPE).forEach((leaf) => {
|
|
if (leaf.view?.render) {
|
|
leaf.view.render();
|
|
}
|
|
});
|
|
}, 50);
|
|
}
|
|
|
|
async refreshTasks() {
|
|
try {
|
|
const response = await this.api.getTasks({ vaultId: this.vaultId });
|
|
this.runtimeIssue = "";
|
|
const previousTasksById = this.tasksById;
|
|
const nextTasksById = {};
|
|
const liveTasksById = this.tasksById;
|
|
|
|
for (const rawTask of response.tasks || []) {
|
|
const taskId = String(rawTask?.taskId || "").trim();
|
|
if (!taskId) {
|
|
continue;
|
|
}
|
|
|
|
const nextTask = this.normalizeTaskForStore(
|
|
rawTask,
|
|
previousTasksById[taskId] || liveTasksById[taskId] || null
|
|
);
|
|
if (!nextTask) {
|
|
continue;
|
|
}
|
|
|
|
nextTasksById[taskId] = nextTask;
|
|
}
|
|
|
|
Object.values(liveTasksById).forEach((task) => {
|
|
const taskId = String(task?.taskId || "").trim();
|
|
if (!taskId || nextTasksById[taskId] || !this.isTaskForCurrentVault(task)) {
|
|
return;
|
|
}
|
|
nextTasksById[taskId] = task;
|
|
});
|
|
|
|
this.tasksById = nextTasksById;
|
|
this.pruneUnreadTaskState();
|
|
this.syncActiveTaskToCanvasContext();
|
|
Object.values(this.tasksById).forEach((task) => {
|
|
const previousTask = previousTasksById[task.taskId] || null;
|
|
this.maybeSyncCanvasNodeHighlights(task, previousTask);
|
|
this.maybeSyncTaskResultNode(task, previousTask);
|
|
});
|
|
this.reconcileRunningTaskStreams();
|
|
this.reconcileAllCanvasNodeHighlights();
|
|
|
|
await this.persistPluginState();
|
|
this.requestViewRefresh();
|
|
const activeTask = this.getActiveTask();
|
|
if (activeTask && !this.taskHasLoadedMessages(activeTask)) {
|
|
void this.hydrateTaskDetails(activeTask.taskId, { force: true });
|
|
}
|
|
} catch (error) {
|
|
this.runtimeIssue = String(error?.message || error);
|
|
this.requestViewRefresh();
|
|
}
|
|
}
|
|
|
|
async refreshTask(taskId, options = {}) {
|
|
if (!taskId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await this.api.getTask(taskId);
|
|
this.mergeTask(response.task, {
|
|
adoptCurrentVault: options.adoptCurrentVault === true,
|
|
});
|
|
this.requestViewRefresh(options);
|
|
} catch (error) {
|
|
this.runtimeIssue = String(error?.message || error);
|
|
this.requestViewRefresh(options);
|
|
}
|
|
}
|
|
|
|
async hydrateTaskDetails(taskId, options = {}) {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
if (!normalizedTaskId) {
|
|
return;
|
|
}
|
|
|
|
const task = this.tasksById[normalizedTaskId] || null;
|
|
if (!task || this.taskHasLoadedMessages(task) || this.taskDetailRefreshInFlight.has(normalizedTaskId)) {
|
|
return;
|
|
}
|
|
|
|
this.taskDetailRefreshInFlight.add(normalizedTaskId);
|
|
try {
|
|
await this.refreshTask(normalizedTaskId, options);
|
|
} finally {
|
|
this.taskDetailRefreshInFlight.delete(normalizedTaskId);
|
|
}
|
|
}
|
|
|
|
clearActiveStreamReconnectTimer() {
|
|
if (this.activeStreamReconnectTimer != null) {
|
|
window.clearTimeout(this.activeStreamReconnectTimer);
|
|
this.activeStreamReconnectTimer = null;
|
|
}
|
|
}
|
|
|
|
disposeActiveStream() {
|
|
this.clearActiveStreamReconnectTimer();
|
|
const disposer = this.activeStreamDisposer;
|
|
this.activeStreamDisposer = null;
|
|
this.activeStreamTaskId = "";
|
|
this.activeStreamSessionId += 1;
|
|
if (typeof disposer === "function") {
|
|
disposer();
|
|
}
|
|
}
|
|
|
|
clearRunningTaskStreamReconnectTimer(taskId) {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
const existingTimer = normalizedTaskId ? this.runningTaskStreamReconnectTimers.get(normalizedTaskId) : null;
|
|
if (existingTimer != null) {
|
|
window.clearTimeout(existingTimer);
|
|
this.runningTaskStreamReconnectTimers.delete(normalizedTaskId);
|
|
}
|
|
}
|
|
|
|
disposeRunningTaskStream(taskId, options = {}) {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
if (!normalizedTaskId) {
|
|
return;
|
|
}
|
|
|
|
this.clearRunningTaskStreamReconnectTimer(normalizedTaskId);
|
|
const disposer = this.runningTaskStreamDisposers.get(normalizedTaskId) || null;
|
|
this.runningTaskStreamDisposers.delete(normalizedTaskId);
|
|
if (options.clearSession !== false) {
|
|
delete this.runningTaskStreamSessionIds[normalizedTaskId];
|
|
}
|
|
if (typeof disposer === "function") {
|
|
disposer();
|
|
}
|
|
}
|
|
|
|
disposeAllRunningTaskStreams() {
|
|
Array.from(new Set([
|
|
...this.runningTaskStreamDisposers.keys(),
|
|
...this.runningTaskStreamReconnectTimers.keys(),
|
|
...Object.keys(this.runningTaskStreamSessionIds || {}),
|
|
])).forEach((taskId) => {
|
|
this.disposeRunningTaskStream(taskId);
|
|
});
|
|
}
|
|
|
|
shouldWatchRunningTaskStream(taskId) {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
if (!normalizedTaskId || normalizedTaskId === String(this.activeStreamTaskId || "").trim()) {
|
|
return false;
|
|
}
|
|
|
|
const task = this.tasksById[normalizedTaskId] || null;
|
|
return Boolean(task) && this.isTaskRunning(task);
|
|
}
|
|
|
|
isRunningTaskStreamSessionCurrent(taskId, sessionId) {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
return Boolean(normalizedTaskId)
|
|
&& this.shouldWatchRunningTaskStream(normalizedTaskId)
|
|
&& Number(this.runningTaskStreamSessionIds[normalizedTaskId] || 0) === Number(sessionId || 0);
|
|
}
|
|
|
|
scheduleRunningTaskStreamReconnect(taskId, sessionId) {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
if (!this.isRunningTaskStreamSessionCurrent(normalizedTaskId, sessionId)) {
|
|
return;
|
|
}
|
|
|
|
this.clearRunningTaskStreamReconnectTimer(normalizedTaskId);
|
|
const timer = window.setTimeout(() => {
|
|
this.runningTaskStreamReconnectTimers.delete(normalizedTaskId);
|
|
if (!this.isRunningTaskStreamSessionCurrent(normalizedTaskId, sessionId)) {
|
|
return;
|
|
}
|
|
|
|
this.connectRunningTaskStream(normalizedTaskId, sessionId);
|
|
}, ACTIVE_TASK_STREAM_RECONNECT_DELAY_MS);
|
|
this.runningTaskStreamReconnectTimers.set(normalizedTaskId, timer);
|
|
}
|
|
|
|
connectRunningTaskStream(taskId, sessionId) {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
if (!this.isRunningTaskStreamSessionCurrent(normalizedTaskId, sessionId)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.runningTaskStreamDisposers.set(normalizedTaskId, this.api.openTaskStream(normalizedTaskId, {
|
|
onEvent: (_event, payload) => {
|
|
if (!this.isRunningTaskStreamSessionCurrent(normalizedTaskId, sessionId)) {
|
|
return;
|
|
}
|
|
|
|
if (payload?.task) {
|
|
this.runtimeIssue = "";
|
|
this.mergeTask(payload.task);
|
|
this.requestViewRefresh();
|
|
}
|
|
},
|
|
onClose: () => {
|
|
if (!this.isRunningTaskStreamSessionCurrent(normalizedTaskId, sessionId)) {
|
|
return;
|
|
}
|
|
|
|
this.runningTaskStreamDisposers.delete(normalizedTaskId);
|
|
void this.refreshTask(normalizedTaskId, { force: true })
|
|
.catch(() => {})
|
|
.finally(() => {
|
|
this.scheduleRunningTaskStreamReconnect(normalizedTaskId, sessionId);
|
|
});
|
|
},
|
|
}));
|
|
} catch (error) {
|
|
if (!this.isRunningTaskStreamSessionCurrent(normalizedTaskId, sessionId)) {
|
|
return;
|
|
}
|
|
|
|
this.runtimeIssue = String(error?.message || error);
|
|
this.requestViewRefresh();
|
|
this.scheduleRunningTaskStreamReconnect(normalizedTaskId, sessionId);
|
|
}
|
|
}
|
|
|
|
ensureRunningTaskStream(taskId) {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
if (!this.shouldWatchRunningTaskStream(normalizedTaskId)) {
|
|
this.disposeRunningTaskStream(normalizedTaskId);
|
|
return;
|
|
}
|
|
|
|
if (
|
|
this.runningTaskStreamDisposers.has(normalizedTaskId)
|
|
|| this.runningTaskStreamReconnectTimers.has(normalizedTaskId)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const nextSessionId = Number(this.runningTaskStreamSessionIds[normalizedTaskId] || 0) + 1;
|
|
this.runningTaskStreamSessionIds[normalizedTaskId] = nextSessionId;
|
|
this.connectRunningTaskStream(normalizedTaskId, nextSessionId);
|
|
}
|
|
|
|
reconcileRunningTaskStreams() {
|
|
const watchedTaskIds = new Set(
|
|
this.getTasks()
|
|
.filter((task) => this.shouldWatchRunningTaskStream(task?.taskId))
|
|
.map((task) => String(task.taskId || "").trim())
|
|
.filter(Boolean)
|
|
);
|
|
|
|
Array.from(new Set([
|
|
...this.runningTaskStreamDisposers.keys(),
|
|
...this.runningTaskStreamReconnectTimers.keys(),
|
|
...Object.keys(this.runningTaskStreamSessionIds || {}),
|
|
])).forEach((taskId) => {
|
|
if (!watchedTaskIds.has(taskId)) {
|
|
this.disposeRunningTaskStream(taskId);
|
|
}
|
|
});
|
|
|
|
watchedTaskIds.forEach((taskId) => {
|
|
this.ensureRunningTaskStream(taskId);
|
|
});
|
|
}
|
|
|
|
isActiveStreamSessionCurrent(taskId, sessionId) {
|
|
const normalizedTaskId = String(taskId || "").trim();
|
|
return Boolean(normalizedTaskId)
|
|
&& normalizedTaskId === String(this.activeStreamTaskId || "").trim()
|
|
&& normalizedTaskId === String(this.uiState?.activeTaskId || "").trim()
|
|
&& sessionId === this.activeStreamSessionId;
|
|
}
|
|
|
|
scheduleActiveStreamReconnect(taskId, sessionId) {
|
|
if (!this.isActiveStreamSessionCurrent(taskId, sessionId)) {
|
|
return;
|
|
}
|
|
|
|
this.clearActiveStreamReconnectTimer();
|
|
this.activeStreamReconnectTimer = window.setTimeout(() => {
|
|
this.activeStreamReconnectTimer = null;
|
|
if (!this.isActiveStreamSessionCurrent(taskId, sessionId)) {
|
|
return;
|
|
}
|
|
|
|
this.connectActiveTaskStream(taskId, sessionId);
|
|
}, ACTIVE_TASK_STREAM_RECONNECT_DELAY_MS);
|
|
}
|
|
|
|
connectActiveTaskStream(taskId, sessionId) {
|
|
if (!this.isActiveStreamSessionCurrent(taskId, sessionId)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.activeStreamDisposer = this.api.openTaskStream(taskId, {
|
|
onEvent: (_event, payload) => {
|
|
if (!this.isActiveStreamSessionCurrent(taskId, sessionId)) {
|
|
return;
|
|
}
|
|
|
|
if (payload?.task) {
|
|
this.runtimeIssue = "";
|
|
const task = this.mergeTask(payload.task);
|
|
this.requestViewRefresh({ streamTask: task });
|
|
}
|
|
},
|
|
onClose: () => {
|
|
if (!this.isActiveStreamSessionCurrent(taskId, sessionId)) {
|
|
return;
|
|
}
|
|
|
|
this.activeStreamDisposer = null;
|
|
void this.refreshTask(taskId, { force: true })
|
|
.catch(() => {})
|
|
.finally(() => {
|
|
this.scheduleActiveStreamReconnect(taskId, sessionId);
|
|
});
|
|
},
|
|
});
|
|
} catch (error) {
|
|
if (!this.isActiveStreamSessionCurrent(taskId, sessionId)) {
|
|
return;
|
|
}
|
|
|
|
this.runtimeIssue = String(error?.message || error);
|
|
this.requestViewRefresh();
|
|
this.scheduleActiveStreamReconnect(taskId, sessionId);
|
|
}
|
|
}
|
|
|
|
subscribeToTask(taskId) {
|
|
this.disposeActiveStream();
|
|
if (!taskId) {
|
|
this.reconcileRunningTaskStreams();
|
|
return;
|
|
}
|
|
|
|
this.activeStreamTaskId = String(taskId || "").trim();
|
|
this.activeStreamSessionId += 1;
|
|
this.connectActiveTaskStream(this.activeStreamTaskId, this.activeStreamSessionId);
|
|
this.reconcileRunningTaskStreams();
|
|
}
|
|
|
|
async setActiveTask(taskId, options = {}) {
|
|
const normalizedTaskId = String(taskId || "").trim() || null;
|
|
this.uiState.activeTaskId = normalizedTaskId;
|
|
if (normalizedTaskId && options.markRead !== false) {
|
|
this.updateTaskUnreadState(normalizedTaskId, false);
|
|
}
|
|
if (options.revealInActiveTab) {
|
|
this.uiState.panelTab = PANEL_TAB_OPTIONS.TASK_LIST;
|
|
}
|
|
this.uiState.taskDetailOpen = options.showDetail === true;
|
|
await this.persistPluginState();
|
|
this.subscribeToTask(normalizedTaskId);
|
|
this.requestViewRefresh();
|
|
void this.hydrateTaskDetails(normalizedTaskId, { force: true });
|
|
}
|
|
|
|
async handleNewThreadFromSelectionCommand() {
|
|
await this.runActiveSelection({ forceNewTask: true });
|
|
}
|
|
|
|
async runActiveSelection(options = {}) {
|
|
const selection = await this.resolveSelectionForAction();
|
|
return this.runResolvedSelection(selection, options);
|
|
}
|
|
|
|
async runResolvedSelection(selection, options = {}) {
|
|
try {
|
|
let effectiveSelection = selection;
|
|
if (options.forceNewTask === true) {
|
|
effectiveSelection = await this.resolver.addImplicitCanvasFileContext(selection);
|
|
const followUpResult = await this.runSelectionAsFollowUp(effectiveSelection);
|
|
if (followUpResult) {
|
|
if (followUpResult.cancelled) {
|
|
await this.appendDebugEvent("follow_up_cancelled", {
|
|
taskId: followUpResult.task?.taskId || "",
|
|
sourceNodeId: effectiveSelection.nodeIds?.[0] || "",
|
|
});
|
|
return;
|
|
}
|
|
await this.appendDebugEvent("follow_up_requested", {
|
|
taskId: followUpResult.task.taskId,
|
|
message: followUpResult.message,
|
|
sourceNodeId: effectiveSelection.nodeIds?.[0] || "",
|
|
mode: followUpResult.mode || "follow-up",
|
|
});
|
|
this.runtimeIssue = "";
|
|
await this.setActiveTask(followUpResult.task.taskId, { revealInActiveTab: true });
|
|
new Notice(
|
|
followUpResult.mode === "fork"
|
|
? "Forked this Canvas branch into a new OpenAgent thread."
|
|
: "Sent this selection to the existing OpenAgent thread."
|
|
);
|
|
await this.refreshTask(followUpResult.task.taskId, { force: true });
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.rememberSelectionSnapshot(effectiveSelection);
|
|
await this.appendDebugEvent("selection_resolved", {
|
|
mode: options.forceNewTask === true ? "new-thread" : "selection",
|
|
canvasPath: effectiveSelection.canvasPath,
|
|
nodeIds: effectiveSelection.nodeIds,
|
|
textCount: effectiveSelection.textBlocks?.length || 0,
|
|
fileCount: effectiveSelection.markdownFiles?.length || 0,
|
|
imageCount: effectiveSelection.imageFiles?.length || 0,
|
|
});
|
|
const selectionSummary = this.summarizeSelection(effectiveSelection);
|
|
const inferredCwd = this.resolveWorkspaceRepoPathForSelection(effectiveSelection) || this.resolver.inferWorkingDirectory(effectiveSelection);
|
|
await this.appendDebugEvent("raw_prompt_prepared", {
|
|
promptPreview: options.forceNewTask === true
|
|
? this.truncateInline(this.getNewThreadPromptFromSelection(effectiveSelection, { cwd: inferredCwd }), 180)
|
|
: "",
|
|
cwd: inferredCwd,
|
|
});
|
|
await this.activateView();
|
|
|
|
const rawPrompt = this.getNewThreadPromptFromSelection(effectiveSelection, { cwd: inferredCwd });
|
|
const response = await this.api.createTaskFromCanvasSelection(this.withVaultScope({
|
|
...effectiveSelection,
|
|
cwd: inferredCwd,
|
|
forceNewTask: options.forceNewTask === true,
|
|
runtimeConfig: this.buildRuntimeConfigPayload(),
|
|
}));
|
|
const task = this.mergeTask(response.task, { adoptCurrentVault: true });
|
|
await this.appendDebugEvent("task_created", {
|
|
taskId: task.taskId,
|
|
threadId: task.threadId || "",
|
|
cwd: task.cwd,
|
|
selectionKey: task.selectionKey,
|
|
});
|
|
this.runtimeIssue = "";
|
|
await this.setActiveTask(task.taskId, { revealInActiveTab: true });
|
|
|
|
if (!task.cwd) {
|
|
new Notice(
|
|
selectionSummary
|
|
? `${selectionSummary}. Set a working directory in the OpenAgent panel to continue.`
|
|
: "Set a working directory in the OpenAgent panel to continue."
|
|
);
|
|
return;
|
|
}
|
|
|
|
new Notice(
|
|
options.forceNewTask === true
|
|
? "Starting a new OpenAgent thread."
|
|
: "OpenAgent is ready for this selection."
|
|
);
|
|
|
|
const runResponse = await this.api.runTask(task.taskId, {
|
|
rawPrompt,
|
|
transcriptMessage: rawPrompt,
|
|
forceContext: false,
|
|
runtimeConfig: this.buildRuntimeConfigPayload(),
|
|
});
|
|
this.mergeTask(runResponse.task, { adoptCurrentVault: true });
|
|
await this.appendDebugEvent("run_requested", {
|
|
taskId: task.taskId,
|
|
rawPrompt,
|
|
});
|
|
await this.refreshTask(task.taskId);
|
|
const refreshedTask = this.tasksById[task.taskId] || task;
|
|
await this.appendDebugEvent("task_refreshed", {
|
|
taskId: refreshedTask.taskId,
|
|
threadId: refreshedTask.threadId || "",
|
|
status: refreshedTask.status || "",
|
|
lastMessage: Array.isArray(refreshedTask.messages) && refreshedTask.messages.length > 0
|
|
? String(refreshedTask.messages[refreshedTask.messages.length - 1]?.text || "")
|
|
: "",
|
|
});
|
|
} catch (error) {
|
|
this.runtimeIssue = String(error?.message || error);
|
|
await this.appendDebugEvent("new_thread_error", {
|
|
message: this.runtimeIssue,
|
|
});
|
|
new Notice(this.runtimeIssue);
|
|
this.requestViewRefresh();
|
|
}
|
|
}
|
|
|
|
async resolveSelectionForAction() {
|
|
try {
|
|
const selection = await this.resolver.resolveActiveSelection();
|
|
this.rememberSelectionSnapshot(selection);
|
|
return selection;
|
|
} catch (error) {
|
|
if (!this.isMissingSelectionError(error)) {
|
|
throw error;
|
|
}
|
|
|
|
const fallbackSelection = await this.resolveRecentSelectionSnapshot();
|
|
this.rememberSelectionSnapshot(fallbackSelection);
|
|
new Notice("Using the most recent Canvas selection.");
|
|
return fallbackSelection;
|
|
}
|
|
}
|
|
|
|
isMissingSelectionError(error) {
|
|
const message = String(error?.message || error || "");
|
|
return message.includes("Select one or more Canvas nodes first.");
|
|
}
|
|
|
|
scheduleCanvasSelectionSnapshotCapture(view = null, options = {}) {
|
|
const delayMs = Math.max(0, toFiniteNumber(options.delayMs, 0));
|
|
window.clearTimeout(this.canvasSelectionCaptureTimer);
|
|
this.canvasSelectionCaptureTimer = window.setTimeout(() => {
|
|
this.canvasSelectionCaptureTimer = null;
|
|
this.captureCanvasSelectionSnapshot(view);
|
|
}, delayMs);
|
|
}
|
|
|
|
captureCanvasSelectionSnapshot(viewOverride = null) {
|
|
try {
|
|
const view = viewOverride?.getViewType?.() === "canvas"
|
|
? viewOverride
|
|
: this.resolver.getActiveCanvasView();
|
|
if (!view) {
|
|
return;
|
|
}
|
|
|
|
this.ensureCanvasSelectionTrackingPatched(view);
|
|
this.ensureCanvasPopupMenuPatched(view);
|
|
this.syncCanvasPopupActionButtons(view);
|
|
|
|
const file = view.file || this.app.workspace.getActiveFile();
|
|
if (!(file instanceof TFile) || file.extension !== "canvas") {
|
|
return;
|
|
}
|
|
|
|
const nodeIds = this.resolver.extractSelectedNodeIds(view);
|
|
const previousSnapshot = this.lastCanvasSelectionSnapshot;
|
|
if (nodeIds.length === 0) {
|
|
const didClearSelection = (
|
|
previousSnapshot?.canvasPath === file.path
|
|
&& Array.isArray(previousSnapshot?.nodeIds)
|
|
&& previousSnapshot.nodeIds.length > 0
|
|
);
|
|
this.lastCanvasSelectionSnapshot = {
|
|
canvasPath: file.path,
|
|
nodeIds: [],
|
|
selectionIdentity: "",
|
|
capturedAt: Date.now(),
|
|
selectedNodeType: "",
|
|
selectedNodeLabel: "",
|
|
};
|
|
if (didClearSelection) {
|
|
this.syncActiveTaskToCanvasContext();
|
|
void this.refreshSelectedGroupContextPreview({ force: true });
|
|
this.requestViewRefresh();
|
|
}
|
|
return;
|
|
}
|
|
|
|
const liveNodeDataById = this.resolver.extractLiveSelectedNodeDataById(view, nodeIds);
|
|
const selectedNode = nodeIds.length === 1 ? liveNodeDataById.get(String(nodeIds[0] || "")) || null : null;
|
|
const nextSnapshot = {
|
|
canvasPath: file.path,
|
|
nodeIds: Array.from(new Set(nodeIds.map((nodeId) => String(nodeId)).filter(Boolean))).sort(),
|
|
selectionIdentity: this.buildTaskSelectionIdentityFromNodeIds(file.path, nodeIds),
|
|
capturedAt: Date.now(),
|
|
selectedNodeType: String(selectedNode?.type || "").trim(),
|
|
selectedNodeLabel: String(selectedNode?.label || "").trim(),
|
|
};
|
|
const selectionUnchanged = (
|
|
previousSnapshot?.canvasPath === nextSnapshot.canvasPath
|
|
&& previousSnapshot?.selectionIdentity === nextSnapshot.selectionIdentity
|
|
&& Array.isArray(previousSnapshot?.nodeIds)
|
|
&& previousSnapshot.nodeIds.length === nextSnapshot.nodeIds.length
|
|
&& previousSnapshot.nodeIds.every((nodeId, index) => nodeId === nextSnapshot.nodeIds[index])
|
|
&& String(previousSnapshot?.selectedNodeType || "") === nextSnapshot.selectedNodeType
|
|
&& String(previousSnapshot?.selectedNodeLabel || "") === nextSnapshot.selectedNodeLabel
|
|
);
|
|
this.lastCanvasSelectionSnapshot = nextSnapshot;
|
|
if (!selectionUnchanged) {
|
|
this.syncActiveTaskToCanvasContext({ markReadOnSelection: true });
|
|
void this.refreshSelectedGroupContextPreview({ snapshot: nextSnapshot, force: true });
|
|
this.requestViewRefresh();
|
|
}
|
|
} catch {
|
|
// Ignore transient Canvas introspection failures during selection sync.
|
|
}
|
|
}
|
|
|
|
async resolveRecentSelectionSnapshot() {
|
|
const snapshot = this.lastCanvasSelectionSnapshot;
|
|
if (!snapshot?.canvasPath || !Array.isArray(snapshot.nodeIds) || snapshot.nodeIds.length === 0) {
|
|
throw new Error("Select one or more Canvas nodes first.");
|
|
}
|
|
|
|
if (Date.now() - Number(snapshot.capturedAt || 0) > RECENT_SELECTION_TTL_MS) {
|
|
throw new Error("Select one or more Canvas nodes first.");
|
|
}
|
|
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(snapshot.canvasPath);
|
|
if (!(abstractFile instanceof TFile) || abstractFile.extension !== "canvas") {
|
|
throw new Error("The recent Canvas selection is no longer available.");
|
|
}
|
|
|
|
return this.resolver.resolveCanvasSelection(abstractFile, snapshot.nodeIds);
|
|
}
|
|
|
|
rememberSelectionSnapshot(selection) {
|
|
const canvasPath = String(selection?.canvasPath || "").trim();
|
|
const nodeIds = Array.isArray(selection?.nodeIds) ? selection.nodeIds.map((nodeId) => String(nodeId)).filter(Boolean) : [];
|
|
if (!canvasPath || nodeIds.length === 0) {
|
|
return;
|
|
}
|
|
|
|
this.lastCanvasSelectionSnapshot = {
|
|
canvasPath,
|
|
nodeIds: Array.from(new Set(nodeIds)).sort(),
|
|
selectionIdentity: this.buildTaskSelectionIdentityFromNodeIds(canvasPath, nodeIds),
|
|
capturedAt: Date.now(),
|
|
selectedNodeType: "",
|
|
selectedNodeLabel: "",
|
|
};
|
|
this.syncActiveTaskToCanvasContext();
|
|
void this.refreshSelectedGroupContextPreview({ force: true });
|
|
}
|
|
|
|
getRecentSelectionSnapshotForActiveCanvas() {
|
|
const snapshot = this.lastCanvasSelectionSnapshot;
|
|
const activeCanvasPath = this.getActiveCanvasPath();
|
|
if (!snapshot?.canvasPath || !activeCanvasPath || snapshot.canvasPath !== activeCanvasPath) {
|
|
return null;
|
|
}
|
|
|
|
const nodeIds = Array.isArray(snapshot.nodeIds)
|
|
? snapshot.nodeIds.map((nodeId) => String(nodeId)).filter(Boolean)
|
|
: [];
|
|
if (nodeIds.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
if (Date.now() - Number(snapshot.capturedAt || 0) > RECENT_SELECTION_TTL_MS) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
...snapshot,
|
|
nodeIds,
|
|
};
|
|
}
|
|
|
|
buildSelectedGroupContextPreviewKey(snapshot = null) {
|
|
const targetSnapshot = snapshot || this.getRecentSelectionSnapshotForActiveCanvas();
|
|
if (!targetSnapshot?.canvasPath || !Array.isArray(targetSnapshot.nodeIds) || targetSnapshot.nodeIds.length !== 1) {
|
|
return "";
|
|
}
|
|
|
|
if (String(targetSnapshot.selectedNodeType || "").trim() !== "group") {
|
|
return "";
|
|
}
|
|
|
|
return `${targetSnapshot.canvasPath}::${targetSnapshot.nodeIds[0]}`;
|
|
}
|
|
|
|
buildLoadingSelectedGroupContextPreview(snapshot) {
|
|
if (!snapshot?.canvasPath) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
status: "loading",
|
|
canvasPath: snapshot.canvasPath,
|
|
groupNodeId: Array.isArray(snapshot.nodeIds) ? String(snapshot.nodeIds[0] || "").trim() : "",
|
|
groupLabel: String(snapshot.selectedNodeLabel || "").trim() || "Untitled group",
|
|
textBlocks: [],
|
|
markdownFiles: [],
|
|
warnings: [],
|
|
};
|
|
}
|
|
|
|
async readCanvasMarkdownFilePreviewEntry(node, warnings = []) {
|
|
const filePath = typeof node?.file === "string" ? node.file.trim() : "";
|
|
const abstractFile = await this.resolver.waitForCanvasFile(filePath, { timeoutMs: 0 });
|
|
if (!(abstractFile instanceof TFile)) {
|
|
warnings.push(`Missing file node target: ${filePath}`);
|
|
return null;
|
|
}
|
|
|
|
if (abstractFile.extension !== "md") {
|
|
warnings.push(`Unsupported file node type skipped: ${filePath}`);
|
|
return null;
|
|
}
|
|
|
|
const vaultBasePath = this.getVaultBasePath();
|
|
return {
|
|
id: String(node?.id || ""),
|
|
path: abstractFile.path,
|
|
absolutePath: vaultBasePath
|
|
? this.resolver.resolveVaultPath(vaultBasePath, abstractFile.path)
|
|
: "",
|
|
name: abstractFile.basename,
|
|
};
|
|
}
|
|
|
|
async buildSelectedGroupContextPreview(snapshot) {
|
|
const canvasPath = String(snapshot?.canvasPath || "").trim();
|
|
const groupNodeId = Array.isArray(snapshot?.nodeIds) ? String(snapshot.nodeIds[0] || "").trim() : "";
|
|
if (!canvasPath || !groupNodeId) {
|
|
return null;
|
|
}
|
|
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(canvasPath);
|
|
if (!(abstractFile instanceof TFile) || abstractFile.extension !== "canvas") {
|
|
return null;
|
|
}
|
|
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(await this.app.vault.cachedRead(abstractFile));
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
const nodeById = new Map(
|
|
(Array.isArray(parsed?.nodes) ? parsed.nodes : []).map((node) => [String(node?.id || "").trim(), node])
|
|
);
|
|
const selectedNode = nodeById.get(groupNodeId) || null;
|
|
if (String(selectedNode?.type || "") !== "group") {
|
|
return null;
|
|
}
|
|
|
|
const containedNodes = Array.from(nodeById.values())
|
|
.filter((node) => {
|
|
const nodeId = String(node?.id || "").trim();
|
|
return nodeId && nodeId !== groupNodeId && this.resolver.doesCanvasGroupContainNode(selectedNode, node);
|
|
})
|
|
.sort(compareCanvasNodeOrder);
|
|
|
|
const textBlocks = [];
|
|
const markdownFiles = [];
|
|
const warnings = [];
|
|
const previewEntries = await Promise.all(containedNodes.map(async (node) => {
|
|
const nodeId = String(node?.id || "").trim();
|
|
if (!nodeId) {
|
|
return null;
|
|
}
|
|
|
|
if (String(node?.type || "") === "text") {
|
|
const text = typeof node?.text === "string" ? node.text.trim() : "";
|
|
return text ? { kind: "text", value: { id: nodeId, text } } : null;
|
|
}
|
|
|
|
if (String(node?.type || "") === "file") {
|
|
const nodeWarnings = [];
|
|
const markdownFile = await this.readCanvasMarkdownFilePreviewEntry(node, nodeWarnings);
|
|
return {
|
|
kind: "file",
|
|
value: markdownFile,
|
|
warnings: nodeWarnings,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}));
|
|
|
|
previewEntries.forEach((entry) => {
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
|
|
if (entry.kind === "text" && entry.value) {
|
|
textBlocks.push(entry.value);
|
|
}
|
|
|
|
if (entry.kind === "file" && entry.value) {
|
|
markdownFiles.push(entry.value);
|
|
}
|
|
|
|
if (Array.isArray(entry.warnings) && entry.warnings.length > 0) {
|
|
warnings.push(...entry.warnings);
|
|
}
|
|
});
|
|
|
|
return {
|
|
status: "ready",
|
|
canvasPath,
|
|
groupNodeId,
|
|
groupLabel: String(selectedNode?.label || "").trim() || String(snapshot?.selectedNodeLabel || "").trim() || "Untitled group",
|
|
textBlocks,
|
|
markdownFiles,
|
|
warnings,
|
|
};
|
|
}
|
|
|
|
async refreshSelectedGroupContextPreview(options = {}) {
|
|
const snapshot = options.snapshot || this.getRecentSelectionSnapshotForActiveCanvas();
|
|
const nextKey = this.buildSelectedGroupContextPreviewKey(snapshot);
|
|
if (!nextKey) {
|
|
const didChange = this.selectedGroupContextPreviewState.key || this.selectedGroupContextPreviewState.preview || this.selectedGroupContextPreviewState.loading;
|
|
this.selectedGroupContextPreviewState = {
|
|
key: "",
|
|
preview: null,
|
|
loading: false,
|
|
};
|
|
if (didChange) {
|
|
this.requestViewRefresh();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if (
|
|
options.force !== true
|
|
&& this.selectedGroupContextPreviewState.key === nextKey
|
|
&& (this.selectedGroupContextPreviewState.loading || this.selectedGroupContextPreviewState.preview)
|
|
) {
|
|
return this.selectedGroupContextPreviewState.preview;
|
|
}
|
|
|
|
this.selectedGroupContextPreviewState = {
|
|
key: nextKey,
|
|
preview: this.buildLoadingSelectedGroupContextPreview(snapshot),
|
|
loading: true,
|
|
};
|
|
this.requestViewRefresh();
|
|
|
|
const loadPromise = this.buildSelectedGroupContextPreview(snapshot)
|
|
.then((preview) => {
|
|
if (this.selectedGroupContextPreviewState.key !== nextKey) {
|
|
return preview;
|
|
}
|
|
|
|
this.selectedGroupContextPreviewState = {
|
|
key: nextKey,
|
|
preview,
|
|
loading: false,
|
|
};
|
|
this.requestViewRefresh();
|
|
return preview;
|
|
})
|
|
.catch(() => {
|
|
if (this.selectedGroupContextPreviewState.key === nextKey) {
|
|
this.selectedGroupContextPreviewState = {
|
|
key: nextKey,
|
|
preview: null,
|
|
loading: false,
|
|
};
|
|
this.requestViewRefresh();
|
|
}
|
|
return null;
|
|
})
|
|
.finally(() => {
|
|
if (this.selectedGroupContextPreviewLoadPromise === loadPromise) {
|
|
this.selectedGroupContextPreviewLoadPromise = null;
|
|
}
|
|
});
|
|
|
|
this.selectedGroupContextPreviewLoadPromise = loadPromise;
|
|
return loadPromise;
|
|
}
|
|
|
|
handleSelectedGroupContextDependencyMutation(file, oldPath = "") {
|
|
const snapshot = this.getRecentSelectionSnapshotForActiveCanvas();
|
|
const previewKey = this.buildSelectedGroupContextPreviewKey(snapshot);
|
|
if (!previewKey) {
|
|
return;
|
|
}
|
|
|
|
const nextPath = file instanceof TFile ? String(file.path || "").trim() : "";
|
|
const normalizedOldPath = String(oldPath || "").trim();
|
|
const preview = this.selectedGroupContextPreviewState.preview;
|
|
const dependsOnCurrentFile = [nextPath, normalizedOldPath].some((candidatePath) => {
|
|
if (!candidatePath) {
|
|
return false;
|
|
}
|
|
|
|
if (candidatePath === snapshot.canvasPath) {
|
|
return true;
|
|
}
|
|
|
|
return Array.isArray(preview?.markdownFiles) && preview.markdownFiles.some((markdownFile) => markdownFile?.path === candidatePath);
|
|
});
|
|
if (!dependsOnCurrentFile) {
|
|
return;
|
|
}
|
|
|
|
void this.refreshSelectedGroupContextPreview({ snapshot, force: true });
|
|
}
|
|
|
|
getSelectedGroupContextPreview() {
|
|
const snapshot = this.getRecentSelectionSnapshotForActiveCanvas();
|
|
const previewKey = this.buildSelectedGroupContextPreviewKey(snapshot);
|
|
if (!previewKey) {
|
|
return null;
|
|
}
|
|
|
|
if (this.selectedGroupContextPreviewState.key !== previewKey) {
|
|
void this.refreshSelectedGroupContextPreview({ snapshot });
|
|
return this.buildLoadingSelectedGroupContextPreview(snapshot);
|
|
}
|
|
|
|
return this.selectedGroupContextPreviewState.preview || this.buildLoadingSelectedGroupContextPreview(snapshot);
|
|
}
|
|
|
|
getActiveCanvasPath() {
|
|
const view = this.resolver.getActiveCanvasView();
|
|
const file = view?.file || this.app.workspace.getActiveFile();
|
|
return file instanceof TFile && file.extension === "canvas" ? file.path : "";
|
|
}
|
|
|
|
getCanvasAbsolutePath(canvasPath) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const vaultBasePath = this.getVaultBasePath();
|
|
if (!normalizedCanvasPath || !vaultBasePath) {
|
|
return "";
|
|
}
|
|
|
|
return path.join(vaultBasePath, ...normalizedCanvasPath.split("/"));
|
|
}
|
|
|
|
async autoArrangeActiveCanvas() {
|
|
try {
|
|
const view = this.resolver.getActiveCanvasView();
|
|
if (!view) {
|
|
throw new Error("Open a Canvas view first.");
|
|
}
|
|
|
|
const file = view.file || this.app.workspace.getActiveFile();
|
|
if (!(file instanceof TFile) || file.extension !== "canvas") {
|
|
throw new Error("The active view is not backed by a .canvas file.");
|
|
}
|
|
|
|
await this.resolver.flushPendingCanvasEdits(view);
|
|
const result = await this.withSerializedCanvasMutation(file.path, async () => {
|
|
const raw = await this.app.vault.cachedRead(file);
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(raw);
|
|
} catch {
|
|
throw new Error(`Unable to parse canvas JSON for ${file.path}`);
|
|
}
|
|
|
|
const nodes = Array.isArray(parsed?.nodes) ? parsed.nodes : [];
|
|
const edges = Array.isArray(parsed?.edges) ? parsed.edges : [];
|
|
if (nodes.length === 0) {
|
|
throw new Error("This Canvas has no nodes to arrange.");
|
|
}
|
|
|
|
const arrangedNodes = autoArrangeCanvasNodes(nodes, edges);
|
|
const changed = arrangedNodes.some((node, index) => {
|
|
const previousNode = nodes[index] || {};
|
|
return (
|
|
toFiniteNumber(node?.x, 0) !== toFiniteNumber(previousNode?.x, 0)
|
|
|| toFiniteNumber(node?.y, 0) !== toFiniteNumber(previousNode?.y, 0)
|
|
);
|
|
});
|
|
if (!changed) {
|
|
return { changed: false, parsed };
|
|
}
|
|
|
|
const nextParsed = {
|
|
...parsed,
|
|
nodes: arrangedNodes,
|
|
edges,
|
|
};
|
|
await this.app.vault.modify(file, JSON.stringify(nextParsed, null, 2) + "\n");
|
|
this.primeCanvasSnapshot(file.path, nextParsed);
|
|
return {
|
|
changed: true,
|
|
parsed: nextParsed,
|
|
nodeCount: arrangedNodes.length,
|
|
};
|
|
});
|
|
|
|
this.runtimeIssue = "";
|
|
this.scheduleCanvasNodeHighlightSync(file.path);
|
|
this.requestViewRefresh({ force: true });
|
|
await this.appendDebugEvent("canvas_auto_arranged", {
|
|
canvasPath: file.path,
|
|
changed: result.changed,
|
|
nodeCount: result.nodeCount || 0,
|
|
});
|
|
new Notice(result.changed
|
|
? "Arranged Canvas nodes without changing edges."
|
|
: "Canvas nodes are already arranged.");
|
|
} catch (error) {
|
|
this.runtimeIssue = String(error?.message || error);
|
|
await this.appendDebugEvent("canvas_auto_arrange_error", {
|
|
message: this.runtimeIssue,
|
|
});
|
|
new Notice(this.runtimeIssue);
|
|
this.requestViewRefresh({ force: true });
|
|
}
|
|
}
|
|
|
|
buildCanvasSnapshot(parsed) {
|
|
const safeParsed = parsed && typeof parsed === "object" ? parsed : {};
|
|
const nodes = Array.isArray(safeParsed.nodes) ? safeParsed.nodes : [];
|
|
const edges = Array.isArray(safeParsed.edges) ? safeParsed.edges : [];
|
|
const nodeIds = new Set();
|
|
const nodeById = new Map();
|
|
const incomingEdgesByNodeId = new Map();
|
|
const resultSourceNodeIdByResultNodeId = new Map();
|
|
|
|
nodes.forEach((node) => {
|
|
const nodeId = String(node?.id || "").trim();
|
|
if (!nodeId) {
|
|
return;
|
|
}
|
|
|
|
nodeIds.add(nodeId);
|
|
nodeById.set(nodeId, node);
|
|
|
|
const sourceNodeId = getOpenAgentResultSourceNodeId(node);
|
|
if (sourceNodeId) {
|
|
resultSourceNodeIdByResultNodeId.set(nodeId, sourceNodeId);
|
|
}
|
|
});
|
|
|
|
edges.forEach((edge) => {
|
|
const fromNodeId = String(edge?.fromNode || "").trim();
|
|
const toNodeId = String(edge?.toNode || "").trim();
|
|
if (!fromNodeId || !toNodeId) {
|
|
return;
|
|
}
|
|
|
|
const incomingEdges = incomingEdgesByNodeId.get(toNodeId) || [];
|
|
incomingEdges.push(edge);
|
|
incomingEdgesByNodeId.set(toNodeId, incomingEdges);
|
|
|
|
if (toNodeId.startsWith("oa-result-") && !resultSourceNodeIdByResultNodeId.has(toNodeId)) {
|
|
resultSourceNodeIdByResultNodeId.set(toNodeId, fromNodeId);
|
|
}
|
|
});
|
|
|
|
return {
|
|
parsed: safeParsed,
|
|
nodeIds,
|
|
nodeById,
|
|
incomingEdgesByNodeId,
|
|
resultSourceNodeIdByResultNodeId,
|
|
};
|
|
}
|
|
|
|
primeCanvasSnapshot(canvasPath, parsed) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
if (!normalizedCanvasPath) {
|
|
return null;
|
|
}
|
|
|
|
const snapshot = this.buildCanvasSnapshot(parsed);
|
|
this.canvasSnapshotCacheByPath.set(normalizedCanvasPath, snapshot);
|
|
return snapshot;
|
|
}
|
|
|
|
invalidateCanvasSnapshot(canvasPath) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
if (!normalizedCanvasPath) {
|
|
return;
|
|
}
|
|
|
|
this.canvasSnapshotCacheByPath.delete(normalizedCanvasPath);
|
|
}
|
|
|
|
getCanvasSnapshotSync(canvasPath) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
if (!normalizedCanvasPath) {
|
|
return null;
|
|
}
|
|
|
|
if (this.canvasSnapshotCacheByPath.has(normalizedCanvasPath)) {
|
|
return this.canvasSnapshotCacheByPath.get(normalizedCanvasPath);
|
|
}
|
|
|
|
const absoluteCanvasPath = this.getCanvasAbsolutePath(normalizedCanvasPath);
|
|
if (!absoluteCanvasPath || !fs.existsSync(absoluteCanvasPath)) {
|
|
this.canvasSnapshotCacheByPath.set(normalizedCanvasPath, null);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(fs.readFileSync(absoluteCanvasPath, "utf8"));
|
|
return this.primeCanvasSnapshot(normalizedCanvasPath, parsed);
|
|
} catch {
|
|
this.canvasSnapshotCacheByPath.set(normalizedCanvasPath, null);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
normalizeCanvasNodeColorValue(value) {
|
|
return String(value || "").trim().toLowerCase();
|
|
}
|
|
|
|
isAutoRunTriggerCanvasNodeColor(value) {
|
|
const normalizedValue = this.normalizeCanvasNodeColorValue(value);
|
|
return normalizedValue !== "" && AUTO_RUN_TRIGGER_CANVAS_NODE_COLORS.includes(normalizedValue);
|
|
}
|
|
|
|
isAutoRunnableCanvasNode(node) {
|
|
if (!node || typeof node !== "object" || isOpenAgentAssistantResultNode(node)) {
|
|
return false;
|
|
}
|
|
|
|
const nodeType = String(node?.type || "").trim();
|
|
return nodeType === "text" || nodeType === "file";
|
|
}
|
|
|
|
getAutoRunCandidateNodeIds(previousSnapshot, nextSnapshot) {
|
|
if (!nextSnapshot?.nodeById) {
|
|
return [];
|
|
}
|
|
|
|
const nodeIds = Array.from(nextSnapshot.nodeById.keys()).filter((nodeId) => {
|
|
const nextNode = nextSnapshot.nodeById.get(nodeId);
|
|
if (!this.isAutoRunnableCanvasNode(nextNode)) {
|
|
return false;
|
|
}
|
|
|
|
const nextColor = this.normalizeCanvasNodeColorValue(nextNode?.color);
|
|
if (!this.isAutoRunTriggerCanvasNodeColor(nextColor)) {
|
|
return false;
|
|
}
|
|
|
|
const previousColor = this.normalizeCanvasNodeColorValue(previousSnapshot?.nodeById?.get(nodeId)?.color);
|
|
return !this.isAutoRunTriggerCanvasNodeColor(previousColor);
|
|
});
|
|
|
|
return nodeIds.sort((leftNodeId, rightNodeId) => {
|
|
return compareCanvasNodeOrder(
|
|
nextSnapshot.nodeById.get(leftNodeId),
|
|
nextSnapshot.nodeById.get(rightNodeId)
|
|
);
|
|
});
|
|
}
|
|
|
|
async triggerAutoRunForCanvasNodes(canvasPath, nodeIds) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedNodeIds = Array.from(new Set(
|
|
(Array.isArray(nodeIds) ? nodeIds : [])
|
|
.map((nodeId) => String(nodeId || "").trim())
|
|
.filter(Boolean)
|
|
));
|
|
if (!normalizedCanvasPath || normalizedNodeIds.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(normalizedCanvasPath);
|
|
if (!(abstractFile instanceof TFile) || abstractFile.extension !== "canvas") {
|
|
return;
|
|
}
|
|
|
|
for (const nodeId of normalizedNodeIds) {
|
|
const autoRunKey = this.getCanvasNodeHighlightKey(normalizedCanvasPath, nodeId);
|
|
if (!autoRunKey || this.canvasAutoRunInFlightByKey.has(autoRunKey)) {
|
|
continue;
|
|
}
|
|
|
|
const existingTask = this.findTaskByCanvasRunSourceNode(normalizedCanvasPath, nodeId)
|
|
|| this.findTaskByRootCanvasNode(normalizedCanvasPath, nodeId);
|
|
if (existingTask && this.isTaskRunning(existingTask)) {
|
|
continue;
|
|
}
|
|
|
|
this.canvasAutoRunInFlightByKey.add(autoRunKey);
|
|
try {
|
|
const selection = await this.resolver.resolveCanvasSelection(abstractFile, [nodeId]);
|
|
await this.appendDebugEvent("canvas_auto_run_triggered", {
|
|
canvasPath: normalizedCanvasPath,
|
|
nodeId,
|
|
title: selection.title,
|
|
});
|
|
this.rememberSelectionSnapshot(selection);
|
|
await this.runResolvedSelection(selection, { forceNewTask: true });
|
|
} catch (error) {
|
|
const message = String(error?.message || error);
|
|
await this.appendDebugEvent("canvas_auto_run_error", {
|
|
canvasPath: normalizedCanvasPath,
|
|
nodeId,
|
|
message,
|
|
});
|
|
new Notice(`Auto-run failed for Canvas node ${nodeId}: ${message}`);
|
|
} finally {
|
|
this.canvasAutoRunInFlightByKey.delete(autoRunKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
async withSerializedCanvasMutation(canvasPath, operation) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
if (!normalizedCanvasPath) {
|
|
return operation();
|
|
}
|
|
|
|
const previous = this.canvasMutationQueueByPath.get(normalizedCanvasPath) || Promise.resolve();
|
|
const next = previous
|
|
.catch(() => {})
|
|
.then(() => operation());
|
|
this.canvasMutationQueueByPath.set(normalizedCanvasPath, next);
|
|
|
|
try {
|
|
return await next;
|
|
} finally {
|
|
if (this.canvasMutationQueueByPath.get(normalizedCanvasPath) === next) {
|
|
this.canvasMutationQueueByPath.delete(normalizedCanvasPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
getConversationSelectionNodeIds(canvasPath, nodeIds) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedNodeIds = Array.from(new Set(
|
|
(Array.isArray(nodeIds) ? nodeIds : [])
|
|
.map((nodeId) => String(nodeId || "").trim())
|
|
.filter(Boolean)
|
|
)).sort();
|
|
if (!normalizedCanvasPath || normalizedNodeIds.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
if (!normalizedNodeIds.some((nodeId) => nodeId.startsWith("oa-result-"))) {
|
|
return normalizedNodeIds;
|
|
}
|
|
|
|
const snapshot = this.getCanvasSnapshotSync(normalizedCanvasPath);
|
|
if (!snapshot) {
|
|
return normalizedNodeIds;
|
|
}
|
|
|
|
return Array.from(new Set(
|
|
normalizedNodeIds.map((nodeId) => snapshot.resultSourceNodeIdByResultNodeId.get(nodeId) || nodeId)
|
|
)).sort();
|
|
}
|
|
|
|
buildTaskSelectionIdentityFromNodeIds(canvasPath, nodeIds) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const canonicalNodeIds = this.getConversationSelectionNodeIds(normalizedCanvasPath, nodeIds);
|
|
if (!normalizedCanvasPath || canonicalNodeIds.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
return `${normalizedCanvasPath}\n${canonicalNodeIds.join("\n")}`;
|
|
}
|
|
|
|
getTaskCanvasPath(task) {
|
|
return String(task?.canvasBinding?.canvasPath || task?.selectionContext?.canvasPath || task?.sourceRef || "").trim();
|
|
}
|
|
|
|
getTaskSelectionIdentity(task) {
|
|
const canvasPath = this.getTaskCanvasPath(task);
|
|
const nodeIds = Array.isArray(task?.selectionContext?.nodeIds)
|
|
? task.selectionContext.nodeIds.map((nodeId) => String(nodeId)).filter(Boolean).sort()
|
|
: [];
|
|
return this.buildTaskSelectionIdentityFromNodeIds(canvasPath, nodeIds);
|
|
}
|
|
|
|
getRecentSelectionIdentityForCanvas(canvasPath) {
|
|
const snapshot = this.lastCanvasSelectionSnapshot;
|
|
if (!canvasPath || !snapshot || snapshot.canvasPath !== canvasPath) {
|
|
return "";
|
|
}
|
|
|
|
if (Date.now() - Number(snapshot.capturedAt || 0) > RECENT_SELECTION_TTL_MS) {
|
|
return "";
|
|
}
|
|
|
|
const nodeIds = Array.isArray(snapshot.nodeIds)
|
|
? snapshot.nodeIds.map((nodeId) => String(nodeId)).filter(Boolean).sort()
|
|
: [];
|
|
if (nodeIds.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
return snapshot.selectionIdentity || this.buildTaskSelectionIdentityFromNodeIds(canvasPath, nodeIds);
|
|
}
|
|
|
|
resolveFollowUpTaskForCanvasNodeIds(canvasPath, nodeIds) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const normalizedNodeIds = Array.from(new Set(
|
|
(Array.isArray(nodeIds) ? nodeIds : [])
|
|
.map((nodeId) => String(nodeId || "").trim())
|
|
.filter(Boolean)
|
|
)).sort();
|
|
if (!normalizedCanvasPath || normalizedNodeIds.length !== 1) {
|
|
return null;
|
|
}
|
|
|
|
const selectedNodeId = normalizedNodeIds[0];
|
|
if (!selectedNodeId) {
|
|
return null;
|
|
}
|
|
|
|
if (selectedNodeId.startsWith("oa-result-")) {
|
|
return this.findTaskByOpenAgentResultNode(normalizedCanvasPath, selectedNodeId);
|
|
}
|
|
|
|
const snapshot = this.getCanvasSnapshotSync(normalizedCanvasPath);
|
|
if (!snapshot) {
|
|
return null;
|
|
}
|
|
|
|
const selectedNode = snapshot.nodeById.get(selectedNodeId);
|
|
if (!selectedNode || String(selectedNode?.type || "").trim() !== "text") {
|
|
return null;
|
|
}
|
|
|
|
for (const edge of snapshot.incomingEdgesByNodeId.get(selectedNodeId) || []) {
|
|
const upstreamNodeId = String(edge?.fromNode || "").trim();
|
|
const task = this.findConversationTaskForUpstreamNode(normalizedCanvasPath, upstreamNodeId);
|
|
if (task) {
|
|
return task;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
getRecentSelectionTaskForCanvas(canvasPath) {
|
|
const snapshot = this.lastCanvasSelectionSnapshot;
|
|
if (!canvasPath || !snapshot || snapshot.canvasPath !== canvasPath) {
|
|
return null;
|
|
}
|
|
|
|
if (Date.now() - Number(snapshot.capturedAt || 0) > RECENT_SELECTION_TTL_MS) {
|
|
return null;
|
|
}
|
|
|
|
return this.resolveFollowUpTaskForCanvasNodeIds(canvasPath, snapshot.nodeIds);
|
|
}
|
|
|
|
hasExplicitCanvasDeselection(canvasPath) {
|
|
const normalizedCanvasPath = String(canvasPath || "").trim();
|
|
const snapshot = this.lastCanvasSelectionSnapshot;
|
|
if (!normalizedCanvasPath || !snapshot || snapshot.canvasPath !== normalizedCanvasPath) {
|
|
return false;
|
|
}
|
|
|
|
if (Date.now() - Number(snapshot.capturedAt || 0) > RECENT_SELECTION_TTL_MS) {
|
|
return false;
|
|
}
|
|
|
|
const nodeIds = Array.isArray(snapshot.nodeIds)
|
|
? snapshot.nodeIds.map((nodeId) => String(nodeId)).filter(Boolean)
|
|
: [];
|
|
return nodeIds.length === 0;
|
|
}
|
|
|
|
syncActiveTaskToCanvasContext(options = {}) {
|
|
const activeCanvasPath = this.getActiveCanvasPath();
|
|
if (!activeCanvasPath) {
|
|
return;
|
|
}
|
|
|
|
const archiveProbeCache = new Map();
|
|
const visibleTasks = this.getVisibleTasks({ archiveProbeCache });
|
|
const archivedTasks = this.getArchivedTasks({ archiveProbeCache });
|
|
const selectedIdentity = this.getRecentSelectionIdentityForCanvas(activeCanvasPath);
|
|
const currentTask = this.tasksById[this.uiState.activeTaskId] || null;
|
|
let nextTask = null;
|
|
|
|
if (selectedIdentity) {
|
|
nextTask = visibleTasks.find((task) => this.getTaskSelectionIdentity(task) === selectedIdentity)
|
|
|| archivedTasks.find((task) => this.getTaskSelectionIdentity(task) === selectedIdentity)
|
|
|| null;
|
|
|
|
if (!nextTask) {
|
|
nextTask = this.getRecentSelectionTaskForCanvas(activeCanvasPath);
|
|
}
|
|
} else if (this.hasExplicitCanvasDeselection(activeCanvasPath)) {
|
|
nextTask = null;
|
|
} else {
|
|
if (currentTask && this.getTaskCanvasPath(currentTask) === activeCanvasPath) {
|
|
nextTask = currentTask;
|
|
}
|
|
|
|
if (!nextTask && visibleTasks.length > 0) {
|
|
nextTask = visibleTasks[0];
|
|
}
|
|
|
|
if (!nextTask && archivedTasks.length > 0) {
|
|
nextTask = archivedTasks[0];
|
|
}
|
|
}
|
|
|
|
const nextTaskId = nextTask?.taskId || null;
|
|
const shouldMarkRead = options.markReadOnSelection === true
|
|
&& nextTaskId
|
|
&& this.hasUnreadTask(nextTaskId);
|
|
if (nextTaskId === this.uiState.activeTaskId) {
|
|
if (shouldMarkRead) {
|
|
this.setTaskUnreadState(nextTaskId, false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.uiState.activeTaskId = nextTaskId;
|
|
this.uiState.taskDetailOpen = false;
|
|
if (shouldMarkRead) {
|
|
this.updateTaskUnreadState(nextTaskId, false);
|
|
}
|
|
this.persistPluginState();
|
|
if (nextTaskId) {
|
|
this.subscribeToTask(nextTaskId);
|
|
void this.hydrateTaskDetails(nextTaskId, { force: true });
|
|
} else {
|
|
this.disposeActiveStream();
|
|
}
|
|
this.reconcileRunningTaskStreams();
|
|
this.requestViewRefresh();
|
|
}
|
|
|
|
async saveCwdForTask(taskId, cwd) {
|
|
const task = this.tasksById[taskId];
|
|
if (!task?.selectionContext) {
|
|
new Notice("This task does not have enough selection context to change its project folder.");
|
|
return;
|
|
}
|
|
|
|
const normalizedCwd = this.normalizeWorkingDirectoryInput(cwd);
|
|
if (!normalizedCwd) {
|
|
new Notice("Enter an absolute project folder path.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await this.api.createTaskFromCanvasSelection(this.withVaultScope({
|
|
...task.selectionContext,
|
|
title: task.title,
|
|
cwd: normalizedCwd,
|
|
runtimeConfig: this.buildRuntimeConfigPayload(),
|
|
}));
|
|
const nextTask = this.mergeTask(response.task, { adoptCurrentVault: true });
|
|
await this.setActiveTask(nextTask.taskId, { revealInActiveTab: true });
|
|
this.runtimeIssue = "";
|
|
new Notice(`Project folder set to ${nextTask.cwd || "(not set)"}`);
|
|
await this.refreshTask(nextTask.taskId);
|
|
} catch (error) {
|
|
this.runtimeIssue = String(error?.message || error);
|
|
new Notice(this.runtimeIssue);
|
|
this.requestViewRefresh();
|
|
}
|
|
}
|
|
|
|
async sendDraftForTask(taskId, options = {}) {
|
|
const task = this.tasksById[taskId];
|
|
if (task?.status === "running") {
|
|
new Notice("Wait for the current turn to finish before sending another message.");
|
|
return;
|
|
}
|
|
|
|
const draftSource = Object.prototype.hasOwnProperty.call(options, "draft")
|
|
? String(options.draft || "")
|
|
: this.getDraft(taskId);
|
|
if (Object.prototype.hasOwnProperty.call(options, "draft")) {
|
|
this.setDraft(taskId, draftSource);
|
|
}
|
|
|
|
const draft = draftSource.trim();
|
|
if (!draft) {
|
|
new Notice("Write a follow-up message first.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.api.sendMessage(taskId, draft, this.buildRuntimeConfigPayload());
|
|
this.clearDraft(taskId);
|
|
await this.refreshTask(taskId, { force: true });
|
|
} catch (error) {
|
|
this.runtimeIssue = String(error?.message || error);
|
|
new Notice(this.runtimeIssue);
|
|
this.requestViewRefresh();
|
|
}
|
|
}
|
|
|
|
async resumeLastTask() {
|
|
await this.activateView();
|
|
await this.refreshTasks();
|
|
const task = this.getActiveTask() || this.getTasks()[0];
|
|
if (!task) {
|
|
new Notice("No OpenAgent task yet.");
|
|
return;
|
|
}
|
|
|
|
await this.activateTaskFromList(task.taskId);
|
|
}
|
|
|
|
async stopActiveTask() {
|
|
const task = this.getActiveTask();
|
|
if (!task?.currentTurnId) {
|
|
new Notice("No active turn to stop.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.api.interruptTask(task.taskId);
|
|
await this.refreshTask(task.taskId);
|
|
} catch (error) {
|
|
this.runtimeIssue = String(error?.message || error);
|
|
new Notice(this.runtimeIssue);
|
|
this.requestViewRefresh();
|
|
}
|
|
}
|
|
|
|
getStopActiveTaskHotkey() {
|
|
return normalizeStopHotkeySetting(this.settings?.stopActiveTaskHotkey);
|
|
}
|
|
|
|
getStopActiveTaskHotkeyLabel() {
|
|
return formatHotkeyForDisplay(this.getStopActiveTaskHotkey());
|
|
}
|
|
|
|
getStopActiveTaskButtonTitle() {
|
|
const hotkey = this.getStopActiveTaskHotkeyLabel();
|
|
return hotkey ? `Stop (${hotkey})` : "Stop";
|
|
}
|
|
|
|
async handleStopActiveTaskHotkey(event) {
|
|
if (!event || event.defaultPrevented || event.repeat) {
|
|
return;
|
|
}
|
|
if (event.isComposing || event.keyCode === 229) {
|
|
return;
|
|
}
|
|
if (isHotkeyCaptureTarget(event.target)) {
|
|
return;
|
|
}
|
|
if (!hotkeyMatchesKeyboardEvent(this.getStopActiveTaskHotkey(), event)) {
|
|
return;
|
|
}
|
|
|
|
const task = this.getActiveTask();
|
|
if (!task?.currentTurnId) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
await this.stopActiveTask();
|
|
}
|
|
|
|
normalizeWorkingDirectoryInput(cwd) {
|
|
const trimmedCwd = String(cwd || "").trim();
|
|
if (!trimmedCwd || !path.isAbsolute(trimmedCwd)) {
|
|
return "";
|
|
}
|
|
|
|
return path.normalize(trimmedCwd);
|
|
}
|
|
|
|
summarizeSelection(selection) {
|
|
const textCount = Array.isArray(selection?.textBlocks) ? selection.textBlocks.length : 0;
|
|
const fileCount = Array.isArray(selection?.markdownFiles) ? selection.markdownFiles.length : 0;
|
|
const imageCount = Array.isArray(selection?.imageFiles) ? selection.imageFiles.length : 0;
|
|
const parts = [];
|
|
|
|
if (textCount > 0) {
|
|
parts.push(`${textCount} text node${textCount === 1 ? "" : "s"}`);
|
|
}
|
|
|
|
if (fileCount > 0) {
|
|
parts.push(`${fileCount} markdown file${fileCount === 1 ? "" : "s"}`);
|
|
}
|
|
|
|
if (imageCount > 0) {
|
|
parts.push(`${imageCount} image file${imageCount === 1 ? "" : "s"}`);
|
|
}
|
|
|
|
const firstText = selection?.textBlocks?.[0]?.text;
|
|
if (parts.length === 1 && textCount === 1 && typeof firstText === "string" && firstText.trim()) {
|
|
return `Captured ${parts[0]}: ${this.truncateInline(firstText.trim(), 80)}`;
|
|
}
|
|
|
|
if (parts.length > 0) {
|
|
return `Captured ${parts.join(" and ")}`;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
getSelectionImageResourceUrl(file) {
|
|
const normalizedPath = String(file?.path || "").trim();
|
|
if (!normalizedPath) {
|
|
return String(file?.absolutePath || "").trim();
|
|
}
|
|
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(normalizedPath);
|
|
if (abstractFile instanceof TFile) {
|
|
if (typeof this.app.vault.getResourcePath === "function") {
|
|
try {
|
|
return this.app.vault.getResourcePath(abstractFile);
|
|
} catch {
|
|
// Fall through to adapter-based lookup.
|
|
}
|
|
}
|
|
|
|
if (typeof this.app.vault.adapter?.getResourcePath === "function") {
|
|
try {
|
|
return this.app.vault.adapter.getResourcePath(normalizedPath);
|
|
} catch {
|
|
// Fall through to absolute path.
|
|
}
|
|
}
|
|
}
|
|
|
|
return String(file?.absolutePath || "").trim();
|
|
}
|
|
|
|
renderSelectionImageMessage(container, selection) {
|
|
const imageFiles = Array.isArray(selection?.imageFiles)
|
|
? selection.imageFiles.filter((file) => String(file?.path || file?.absolutePath || "").trim())
|
|
: [];
|
|
if (imageFiles.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const item = container.createDiv({
|
|
cls: "oa-message oa-chat-message oa-role-user oa-selection-message",
|
|
});
|
|
item.createDiv({
|
|
cls: "oa-message-label",
|
|
text: imageFiles.length === 1 ? "Selected image" : "Selected images",
|
|
});
|
|
|
|
const gallery = item.createDiv({ cls: "oa-selection-image-gallery" });
|
|
imageFiles.forEach((file, index) => {
|
|
const src = this.getSelectionImageResourceUrl(file);
|
|
const card = gallery.createDiv({ cls: "oa-selection-image-card" });
|
|
if (src) {
|
|
card.createEl("img", {
|
|
cls: "oa-selection-image",
|
|
attr: {
|
|
src,
|
|
alt: String(file?.name || file?.path || `Selected image ${index + 1}`),
|
|
},
|
|
});
|
|
} else {
|
|
card.createDiv({
|
|
cls: "oa-selection-image-fallback",
|
|
text: "Image preview unavailable",
|
|
});
|
|
}
|
|
card.createDiv({
|
|
cls: "oa-selection-image-caption",
|
|
text: String(file?.path || file?.name || `Image file ${index + 1}`),
|
|
});
|
|
});
|
|
|
|
return item;
|
|
}
|
|
|
|
describeSelectionDebug(selection) {
|
|
const nodeIds = Array.isArray(selection?.nodeIds) ? selection.nodeIds.filter(Boolean) : [];
|
|
const textCount = Array.isArray(selection?.textBlocks) ? selection.textBlocks.length : 0;
|
|
const fileCount = Array.isArray(selection?.markdownFiles) ? selection.markdownFiles.length : 0;
|
|
const imageCount = Array.isArray(selection?.imageFiles) ? selection.imageFiles.length : 0;
|
|
const warningCount = Array.isArray(selection?.warnings) ? selection.warnings.length : 0;
|
|
const idsPreview = nodeIds.length > 0 ? nodeIds.join(", ") : "(none)";
|
|
return `Selection debug: ids=${idsPreview} | text=${textCount} | files=${fileCount} | images=${imageCount} | warnings=${warningCount}`;
|
|
}
|
|
|
|
getNewThreadPromptFromSelection(selection, options = {}) {
|
|
const textBlocks = Array.isArray(selection?.textBlocks) ? selection.textBlocks : [];
|
|
const markdownFiles = Array.isArray(selection?.markdownFiles) ? selection.markdownFiles : [];
|
|
const imageFiles = Array.isArray(selection?.imageFiles) ? selection.imageFiles : [];
|
|
if (textBlocks.length === 0 && markdownFiles.length > 0 && imageFiles.length === 0) {
|
|
return buildCanvasSelectionPrompt(
|
|
selection,
|
|
markdownFiles.length === 1
|
|
? "Use the selected markdown file as the primary context and continue with the most helpful next step."
|
|
: "Use the selected markdown files as the primary context and continue with the most helpful next step.",
|
|
options,
|
|
);
|
|
}
|
|
|
|
if (textBlocks.length === 0 && markdownFiles.length === 0 && imageFiles.length > 0) {
|
|
return "";
|
|
}
|
|
|
|
if (textBlocks.length === 0 && (markdownFiles.length > 0 || imageFiles.length > 0)) {
|
|
return markdownFiles.length > 0 ? buildCanvasSelectionPrompt(selection, "", options) : "";
|
|
}
|
|
|
|
if (textBlocks.length !== 1) {
|
|
throw new Error("New thread currently requires selecting exactly one text node, optionally with file context, or selecting only supported file nodes.");
|
|
}
|
|
|
|
const text = String(textBlocks[0]?.text || "");
|
|
if (!text.trim()) {
|
|
throw new Error("The selected text node is empty.");
|
|
}
|
|
|
|
if (markdownFiles.length > 0) {
|
|
return buildCanvasSelectionPrompt(selection, text, options);
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
truncateInline(value, maxLength) {
|
|
const singleLine = String(value || "").replace(/\s+/g, " ").trim();
|
|
if (!singleLine) {
|
|
return "";
|
|
}
|
|
|
|
if (singleLine.length <= maxLength) {
|
|
return singleLine;
|
|
}
|
|
|
|
return `${singleLine.slice(0, Math.max(0, maxLength - 1)).trimEnd()}...`;
|
|
}
|
|
};
|