window.__ModuleLoader__.load({ id: "dsh-project-tree", factory: (require) => { var module = { exports: {} }; var exports = module.exports; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const react = require("react"); const runtimeClient = require("@deepseek-ai/dsh-client-runtime/client"); const e = react.createElement; const name = "dsh-project-tree"; const inject = ["slots", "sessions", "workspaces"]; const ROUTE_PREFIX = "/dsh-project-tree"; const VIEW_PERSIST_KEY = "dsh-project-tree.view"; const STYLE_ID = "dsh-project-tree-style"; const RELOAD_MS = 4000; const INDENT = 10; const STYLE_TEXT = [ ".dpt{display:flex;flex-direction:column;height:100%;overflow:auto;font-family:var(--dsw-font-family);color:var(--dsw-alias-label-primary);font-size:13px;padding:6px 12px 24px 0}", ".dpt__hint{padding:8px 10px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}", ".dpt__notice{margin:4px 8px;padding:6px 8px;border-radius:6px;background:var(--dsw-alias-status-error,#b34);color:#fff;font-size:12px}", ".dpt__bar{display:flex;align-items:center;justify-content:space-between;padding:2px 8px 6px;color:var(--dsw-alias-label-tertiary)}", ".dpt__bar b{font-size:11px;text-transform:uppercase;letter-spacing:.05em;font-weight:600}", ".dpt__iconbtn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:5px;cursor:pointer;color:var(--dsw-alias-label-tertiary);background:transparent;border:none}", ".dpt__iconbtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}", ".dpt__row{position:relative;display:flex;align-items:center;height:28px;padding-left:4px;cursor:pointer;white-space:nowrap;user-select:none}", ".dpt__body{position:relative;flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:5px;height:24px;padding:0 6px;border-radius:6px;box-sizing:border-box}", ".dpt__row:hover .dpt__body{background:var(--dsw-alias-interactive-bg-hover)}", ".dpt__row[data-active=true] .dpt__body{box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--dsw-alias-label-primary,#dddddd) 45%, transparent)}", ".dpt__row[data-drop=into] .dpt__body{background:var(--dsw-alias-interactive-bg-active,rgba(80,140,255,.18));box-shadow:inset 0 0 0 1px var(--dsw-alias-accent,#58f)}", ".dpt__row[data-drop=before] .dpt__body::before{content:'';position:absolute;left:0;right:0;top:-2px;height:2px;background:var(--dsw-alias-accent,#58f);border-radius:2px}", ".dpt__row[data-dragging=true]{opacity:.4}", ".dpt__twist{display:inline-flex;justify-content:center;width:14px;color:var(--dsw-alias-label-tertiary);flex:0 0 auto;transition:transform .1s}", ".dpt__ico{display:inline-flex;flex:0 0 auto;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}", ".dpt__ico[data-kind=session]{color:var(--dsw-alias-label-tertiary)}", ".dpt__label{overflow:hidden;text-overflow:ellipsis;flex:1 1 auto;min-width:0}", ".dpt__sub{display:none;color:var(--dsw-alias-label-tertiary);font-size:11px;flex:0 0 auto;max-width:110px;overflow:hidden;text-overflow:ellipsis}", ".dpt__row:hover .dpt__sub{display:inline-block}", ".dpt__actions{display:none;align-items:center;gap:2px;flex:0 0 auto}", ".dpt__row:hover .dpt__actions{display:inline-flex}", ".dpt__work{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:4px;flex:0 0 auto;color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:.55}", ".dpt__work:hover{background:var(--dsw-alias-interactive-bg-active,rgba(255,255,255,.08));color:var(--dsw-alias-label-primary);opacity:1}", ".dpt__work[data-on=true]{color:var(--dsw-alias-status-success,#3b9);opacity:1}", ".dpt__act{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:4px;color:var(--dsw-alias-label-tertiary)}", ".dpt__act:hover{background:var(--dsw-alias-interactive-bg-active,rgba(255,255,255,.08));color:var(--dsw-alias-label-primary)}", ".dpt__children{position:relative;margin-left:" + (INDENT + 6) + "px;border-left:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,.12))}", ".dpt__dot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-tertiary)}", ".dpt__dot[data-state=running]{background:var(--dsw-alias-status-success,#3b9)}", ".dpt__dot[data-state=waiting]{background:var(--dsw-alias-status-warning,#e94)}", ".dpt__dot[data-state=idle]{background:transparent;box-shadow:inset 0 0 0 1px var(--dsw-alias-label-tertiary)}", ".dpt__menu{position:fixed;z-index:9999;min-width:190px;max-height:70vh;overflow:auto;background:var(--dsw-alias-surface-overlay,#232323);border:1px solid var(--dsw-alias-border-l2,#444);border-radius:8px;padding:4px;box-shadow:0 8px 30px rgba(0,0,0,.45);font-size:13px}", ".dpt__mi{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:5px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}", ".dpt__mi:hover{background:var(--dsw-alias-interactive-bg-hover)}", ".dpt__mi small{color:var(--dsw-alias-label-tertiary);margin-left:auto;padding-left:12px}", ".dpt__sep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l2,#444)}" ].join(""); function injectStyle() { if (document.getElementById(STYLE_ID) !== null) return; const style = document.createElement("style"); style.id = STYLE_ID; style.dataset.plugin = name; style.textContent = STYLE_TEXT; document.head.appendChild(style); } function svg(paths, size) { return e("svg", { width: size || 15, height: size || 15, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round" }, paths); } const ICON = { chevron: (open) => e("span", { className: "dpt__twist", style: { transform: open ? "rotate(90deg)" : "none" } }, svg([e("path", { key: "p", d: "M6 4l4 4-4 4" })], 12)), project: () => svg([e("path", { key: "a", d: "M2 4.2l6-2.2 6 2.2v7.6l-6 2.2-6-2.2z" }), e("path", { key: "b", d: "M2 4.2l6 2.3 6-2.3M8 6.5v7.5" })]), group: () => svg([e("path", { key: "a", d: "M2 5.2a1 1 0 011-1h3l1.4 1.4H13a1 1 0 011 1v5.2a1 1 0 01-1 1H3a1 1 0 01-1-1z" })]), task: () => svg([e("path", { key: "a", d: "M3 4h10M3 8h10M3 12h7" })]), plus: () => svg([e("path", { key: "a", d: "M8 3.5v9M3.5 8h9" }), ], 13), edit: () => svg([e("path", { key: "a", d: "M9.5 3.5l3 3L6 13l-3.2.7L3.5 10z" })], 13), trash: () => svg([e("path", { key: "a", d: "M3 4.5h10M6 4.5V3h4v1.5M4.5 4.5l.6 8.5h5.8l.6-8.5" })], 13), newchat: () => svg([e("path", { key: "a", d: "M13.5 8.5v3a1.5 1.5 0 01-1.5 1.5H5l-2.5 2V5A1.5 1.5 0 014 3.5h4" }), e("path", { key: "b", d: "M11 2.5v4M9 4.5h4" })], 14), dir: () => svg([e("path", { key: "a", d: "M2 4.2a1 1 0 011-1h3l1.4 1.4H14v7a1 1 0 01-1 1H3a1 1 0 01-1-1z" })], 13), work: (on) => svg([ e("circle", { key: "c", cx: 8, cy: 8, r: 5.2, fill: on ? "currentColor" : "none" }), on ? e("path", { key: "k", d: "M5.7 8.1l1.6 1.6 3-3.4", stroke: "var(--dsw-alias-surface-overlay,#232323)" }) : null ], 14) }; const TRANSPORT_ERROR = { code: "route-unavailable", message: "project-tree route unavailable" }; async function api(path, body) { let response; try { response = await fetch(ROUTE_PREFIX + "/" + path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body ?? {}) }); } catch { return { ok: false, error: TRANSPORT_ERROR }; } let payload; try { payload = await response.json(); } catch { return { ok: false, error: TRANSPORT_ERROR }; } if (response.ok) return { ok: true, value: payload }; return { ok: false, error: payload?.error ?? TRANSPORT_ERROR }; } function createViewStore() { return runtimeClient.defineStore({ init: () => ({ collapsed: {} }), persist: VIEW_PERSIST_KEY, actions: { setCollapsed: (draft, key, collapsed) => { draft.collapsed[key] = collapsed; } } }); } function sessionVisible(summary, archived) { return summary !== undefined && summary.origin !== "subagent" && !archived.has(summary.id) && !summary.blank; } function sessionState(summary) { if (summary.running) return "running"; if (summary.pendingInteraction) return "waiting"; if (summary.completed) return "completed"; return "idle"; } function byOrder(a, b) { const ai = a.sortIndex ?? Number.MAX_SAFE_INTEGER; const bi = b.sortIndex ?? Number.MAX_SAFE_INTEGER; if (ai !== bi) return ai - bi; return String(a.name).localeCompare(String(b.name)); } function baseName(path) { if (typeof path !== "string" || path.length === 0) return ""; const parts = path.replace(/[/\\]+$/, "").split(/[/\\]/); return parts[parts.length - 1] || path; } function insertBefore(ids, movedId, beforeId) { const without = ids.filter((id) => id !== movedId); if (beforeId === null || beforeId === undefined) return [...without, movedId]; const at = without.indexOf(beforeId); if (at < 0) return [...without, movedId]; return [...without.slice(0, at), movedId, ...without.slice(at)]; } function endIndex(items) { return items.reduce((m, x) => Math.max(m, x.sortIndex ?? 0), 0) + 1; } function applyOrder(items, orderedIds) { const idx = new Map(orderedIds.map((id, i) => [id, i])); for (const it of items) if (idx.has(it.id)) it.sortIndex = idx.get(it.id); } const mut = { assign: (sessionId, taskId) => (t) => { for (const task of t.tasks) task.sessionIds = task.sessionIds.filter((s) => s !== sessionId); if (taskId) { const task = t.tasks.find((x) => x.id === taskId); if (task) task.sessionIds = [sessionId, ...task.sessionIds]; } return t; }, taskMove: (taskId, groupId) => (t) => { const task = t.tasks.find((x) => x.id === taskId); if (task) { task.groupId = groupId; task.sortIndex = endIndex(t.tasks.filter((x) => x.groupId === groupId && x.id !== taskId)); } return t; }, tasksReorder: (groupId, orderedIds) => (t) => { applyOrder(t.tasks.filter((x) => x.groupId === groupId), orderedIds); return t; }, groupMove: (groupId, projectId) => (t) => { const group = t.groups.find((x) => x.id === groupId); if (group) { group.projectId = projectId; group.sortIndex = endIndex(t.groups.filter((x) => x.projectId === projectId && x.id !== groupId)); } return t; }, groupsReorder: (projectId, orderedIds) => (t) => { applyOrder(t.groups.filter((x) => x.projectId === projectId), orderedIds); return t; }, projectsReorder: (orderedIds) => (t) => { applyOrder(t.projects, orderedIds); return t; }, renameProject: (id, name) => (t) => { const p = t.projects.find((x) => x.id === id); if (p) p.name = name; return t; }, renameGroup: (id, name) => (t) => { const g = t.groups.find((x) => x.id === id); if (g) g.name = name; return t; }, renameTask: (id, name) => (t) => { const tk = t.tasks.find((x) => x.id === id); if (tk) tk.name = name; return t; }, deleteProject: (id) => (t) => { const groups = new Set(t.groups.filter((g) => g.projectId === id).map((g) => g.id)); t.projects = t.projects.filter((p) => p.id !== id); t.groups = t.groups.filter((g) => g.projectId !== id); t.tasks = t.tasks.filter((tk) => !groups.has(tk.groupId)); return t; }, deleteGroup: (id) => (t) => { t.groups = t.groups.filter((g) => g.id !== id); t.tasks = t.tasks.filter((tk) => tk.groupId !== id); return t; }, deleteTask: (id) => (t) => { t.tasks = t.tasks.filter((tk) => tk.id !== id); return t; }, inWork: (id, value) => (t) => { const has = (t.inWork || []).includes(id); t.inWork = value ? (has ? t.inWork : [...(t.inWork || []), id]) : (t.inWork || []).filter((x) => x !== id); return t; } }; let DRAG = null; function ProjectTreeBrowser(props) { injectStyle(); const { useSessions, useWorkspaces, useStore, actions } = props; const A = props; const list = useSessions((state) => state); const wsItems = useWorkspaces((state) => state.items); const archivedSessionIds = useWorkspaces((state) => state.archivedSessionIds); const collapsed = useStore((state) => state.collapsed); const [tree, setTree] = react.useState(null); const [menu, setMenu] = react.useState(null); const [notice, setNotice] = react.useState(null); const pendingAssign = react.useRef(null); const opSeq = react.useRef(0); const archived = react.useMemo(() => new Set(archivedSessionIds ?? []), [archivedSessionIds]); const flash = react.useCallback((error) => { const code = typeof error === "string" ? error : error?.code ?? error?.message ?? "error"; setNotice(String(code)); window.setTimeout(() => setNotice(null), 3500); }, []); // Guarded reload: drop the result if a newer mutation started while we // were fetching, so a background poll never clobbers an optimistic edit. const reload = react.useCallback(async () => { const seq = opSeq.current; const result = await A.listTree(); if (result.ok && opSeq.current === seq) setTree(result.value); }, []); react.useEffect(() => { let active = true; const tick = () => { if (active && document.visibilityState !== "hidden") void reload(); }; tick(); const timer = window.setInterval(tick, RELOAD_MS); return () => { active = false; window.clearInterval(timer); }; }, [reload]); // Optimistic mutation: patch local tree immediately, fire the request(s), // then reconcile from the server (only if no newer op superseded us). const commit = react.useCallback((mutator, apiCall) => { const mySeq = ++opSeq.current; if (mutator) setTree((prev) => prev ? mutator(JSON.parse(JSON.stringify(prev))) : prev); Promise.resolve(apiCall()).then(async (res) => { if (res && res.ok === false) flash(res.error); const result = await A.listTree(); if (result.ok && opSeq.current === mySeq) setTree(result.value); }); }, [flash]); // Awaiting variant for flows that need the server result (create → id). const run = react.useCallback(async (result) => { opSeq.current++; if (result && result.ok === false) { flash(result.error); return null; } await reload(); return result ? result.value : true; }, [reload, flash]); const isCollapsed = (key) => collapsed[key] !== false; const toggle = (key) => actions.setCollapsed(key, !isCollapsed(key)); const closeMenu = react.useCallback(() => setMenu(null), []); react.useEffect(() => { if (menu === null) return undefined; const onDown = () => closeMenu(); window.addEventListener("mousedown", onDown); window.addEventListener("resize", onDown); return () => { window.removeEventListener("mousedown", onDown); window.removeEventListener("resize", onDown); }; }, [menu, closeMenu]); const openMenu = (event, items) => { event.preventDefault(); event.stopPropagation(); setMenu({ x: event.clientX, y: event.clientY, items: items.filter(Boolean) }); }; const sessionWorkspace = react.useMemo(() => { const map = new Map(); for (const workspace of wsItems ?? []) for (const id of workspace.sessionIds) map.set(id, workspace); return map; }, [wsItems]); const view = react.useMemo(() => { const projects = (tree?.projects ?? []).slice().sort(byOrder); const groups = tree?.groups ?? []; const tasks = tree?.tasks ?? []; const visible = (summary) => sessionVisible(summary, archived); const groupsByProject = new Map(); for (const group of groups) { const bucket = groupsByProject.get(group.projectId) ?? []; bucket.push(group); groupsByProject.set(group.projectId, bucket); } const tasksByGroup = new Map(); for (const task of tasks) { const bucket = tasksByGroup.get(task.groupId) ?? []; bucket.push(task); tasksByGroup.set(task.groupId, bucket); } const claimed = new Set(); const renderedProjects = projects.map((project) => ({ id: project.id, name: project.name, groups: (groupsByProject.get(project.id) ?? []).slice().sort(byOrder).map((group) => ({ id: group.id, name: group.name, projectId: project.id, tasks: (tasksByGroup.get(group.id) ?? []).slice().sort(byOrder).map((task) => { const sessions = task.sessionIds.map((id) => list.byId[id]).filter(visible); for (const s of sessions) claimed.add(s.id); return { id: task.id, name: task.name, groupId: group.id, sessionIds: task.sessionIds, sessions }; }) })) })); const unsorted = Object.values(list.byId).filter(visible).filter((s) => !claimed.has(s.id)) .sort((a, b) => String(a.displayTitle).localeCompare(String(b.displayTitle))); return { projects: renderedProjects, unsorted, workspaces: tree?.workspaces ?? [] }; }, [tree, list, archived]); react.useEffect(() => { const p = pendingAssign.current; if (p === null) return; const now = new Set((wsItems ?? []).flatMap((w) => w.sessionIds)); const fresh = [...now].find((id) => !p.known.has(id) && (sessionWorkspace.get(id)?.workspaceId ?? sessionWorkspace.get(id)?.id) === p.workspaceId); if (fresh !== undefined) { pendingAssign.current = null; void run(api("session-assign", { sessionId: fresh, taskId: p.taskId })); } }, [wsItems, sessionWorkspace, run]); const groupTaskIds = (groupId) => view.projects.flatMap((p) => p.groups).filter((g) => g.id === groupId).flatMap((g) => g.tasks.map((t) => t.id)); const projectGroupIds = (projectId) => view.projects.filter((p) => p.id === projectId).flatMap((p) => p.groups.map((g) => g.id)); const projectIds = () => view.projects.map((p) => p.id); const onDrop = (target) => { const drag = DRAG; DRAG = null; if (drag === null || (drag.type === target.type && drag.id === target.id)) return; if (drag.type === "session") { if (target.type === "task") return commit(mut.assign(drag.id, target.id), () => api("session-assign", { sessionId: drag.id, taskId: target.id })); if (target.type === "unsorted") return commit(mut.assign(drag.id, null), () => api("session-assign", { sessionId: drag.id, taskId: null })); return; } if (drag.type === "task") { if (target.type === "group") return commit(mut.taskMove(drag.id, target.id), () => api("task-move", { taskId: drag.id, groupId: target.id })); if (target.type === "task") { const ordered = insertBefore(groupTaskIds(target.groupId).filter((id) => id !== drag.id), drag.id, target.id); if (target.groupId !== drag.groupId) { return commit((t) => mut.tasksReorder(target.groupId, ordered)(mut.taskMove(drag.id, target.groupId)(t)), async () => { const r = await api("task-move", { taskId: drag.id, groupId: target.groupId }); return r.ok === false ? r : api("tasks-reorder", { groupId: target.groupId, orderedIds: ordered }); }); } return commit(mut.tasksReorder(target.groupId, ordered), () => api("tasks-reorder", { groupId: target.groupId, orderedIds: ordered })); } return; } if (drag.type === "group") { if (target.type === "project") return commit(mut.groupMove(drag.id, target.id), () => api("group-move", { groupId: drag.id, projectId: target.id })); if (target.type === "group") { const ordered = insertBefore(projectGroupIds(target.projectId).filter((id) => id !== drag.id), drag.id, target.id); if (target.projectId !== drag.projectId) { return commit((t) => mut.groupsReorder(target.projectId, ordered)(mut.groupMove(drag.id, target.projectId)(t)), async () => { const r = await api("group-move", { groupId: drag.id, projectId: target.projectId }); return r.ok === false ? r : api("groups-reorder", { projectId: target.projectId, orderedIds: ordered }); }); } return commit(mut.groupsReorder(target.projectId, ordered), () => api("groups-reorder", { projectId: target.projectId, orderedIds: ordered })); } return; } if (drag.type === "project" && target.type === "project") { const ordered = insertBefore(projectIds(), drag.id, target.id); return commit(mut.projectsReorder(ordered), () => api("projects-reorder", { orderedIds: ordered })); } }; const canDrop = (drag, node) => (drag.type === "session" && (node.type === "task" || node.type === "unsorted")) || (drag.type === "task" && (node.type === "group" || node.type === "task")) || (drag.type === "group" && (node.type === "project" || node.type === "group")) || (drag.type === "project" && node.type === "project"); const dropAttrs = (node) => ({ onDragOver: (ev) => { if (DRAG === null || !canDrop(DRAG, node)) return; if (DRAG.type === node.type && DRAG.id === node.id) return; ev.preventDefault(); ev.dataTransfer.dropEffect = "move"; ev.currentTarget.dataset.drop = DRAG.type === node.type ? "before" : "into"; }, onDragLeave: (ev) => { delete ev.currentTarget.dataset.drop; }, onDrop: (ev) => { ev.preventDefault(); ev.stopPropagation(); delete ev.currentTarget.dataset.drop; void onDrop(node); } }); const dragAttrs = (item) => ({ draggable: true, onDragStart: (ev) => { DRAG = item; ev.dataTransfer.effectAllowed = "move"; try { ev.dataTransfer.setData("text/plain", item.id); } catch {} }, onDragEnd: () => { DRAG = null; } }); const promptName = (label, current) => { const v = window.prompt(label, current ?? ""); if (v === null) return null; const t = v.trim(); return t.length === 0 ? null : t; }; const newSessionInTask = (taskId) => { const workspaces = view.workspaces ?? []; if (workspaces.length === 0) return void flash("no-workspaces"); openMenuAt(lastMenuEvent.current, workspaces.map((w) => ({ icon: ICON.dir(), label: w.title, hint: baseName(w.path), run: () => { const known = new Set((wsItems ?? []).flatMap((x) => x.sessionIds)); pendingAssign.current = { taskId, workspaceId: w.id, known }; A.startSession(w.id); flash("starting-session…"); } }))); }; const lastMenuEvent = react.useRef(null); const openMenuAt = (ev, items) => { if (ev) setMenu({ x: ev.clientX, y: ev.clientY, items: items.filter(Boolean) }); }; const actBtn = (icon, title, onClick) => e("span", { className: "dpt__act", title, onClick: (ev) => { ev.stopPropagation(); onClick(ev); } }, icon); const inWorkSet = new Set(tree?.inWork ?? []); const workToggle = (id) => e("span", { key: "work", className: "dpt__work", "data-on": inWorkSet.has(id), title: inWorkSet.has(id) ? "In work — click to stop" : "Mark as in work", onClick: (ev) => { ev.stopPropagation(); const value = !inWorkSet.has(id); commit(mut.inWork(id, value), () => api("in-work", { id, value })); } }, ICON.work(inWorkSet.has(id))); const row = (opts) => { const [twist, ...body] = opts.children; return e("div", Object.assign({ className: "dpt__row", "data-type": opts.dragType, "data-id": opts.id, "data-inwork": opts.inWork === true, "data-active": opts.active === true, "data-dragging": DRAG !== null && DRAG.type === opts.dragType && DRAG.id === opts.id, onClick: opts.onClick, onContextMenu: opts.onContextMenu }, opts.drag ? dragAttrs({ type: opts.dragType, id: opts.id, groupId: opts.groupId, projectId: opts.projectId }) : {}, opts.drop || {}), [ twist, e("div", { key: "body", className: "dpt__body" }, body) ]); }; const sessionRow = (summary) => { const workspace = sessionWorkspace.get(summary.id); return row({ id: summary.id, dragType: "session", drag: true, inWork: inWorkSet.has(summary.id), active: list.current === summary.id, onClick: () => A.openSession(summary.id), onContextMenu: (ev) => { lastMenuEvent.current = ev; openMenu(ev, sessionMenuItems(summary.id)); }, children: [ e("span", { key: "t", className: "dpt__twist" }), e("span", { key: "d", className: "dpt__ico", "data-kind": "session" }, e("span", { className: "dpt__dot", "data-state": sessionState(summary) })), e("span", { key: "l", className: "dpt__label", title: summary.displayTitle }, summary.displayTitle || "Untitled"), workspace && e("span", { key: "w", className: "dpt__sub", title: workspace.path }, baseName(workspace.path)), workToggle(summary.id) ] }); }; const taskNode = (task) => { const key = "t:" + task.id; const open = !isCollapsed(key); return e(react.Fragment, { key: task.id }, [ row({ id: task.id, groupId: task.groupId, dragType: "task", drag: true, drop: dropAttrs({ type: "task", id: task.id, groupId: task.groupId }), onClick: () => toggle(key), onContextMenu: (ev) => { lastMenuEvent.current = ev; openMenu(ev, [ { icon: ICON.newchat(), label: "New session here…", run: () => newSessionInTask(task.id) }, { icon: ICON.edit(), label: "Rename task", run: () => { const n = promptName("Rename task", task.name); if (n) commit(mut.renameTask(task.id, n), () => api("task-rename", { taskId: task.id, name: n })); } }, { icon: ICON.trash(), label: "Delete task", run: () => { if (window.confirm(`Delete task "${task.name}"? Its sessions return to Unsorted.`)) commit(mut.deleteTask(task.id), () => api("task-delete", { taskId: task.id })); } } ]); }, children: [ ICON.chevron(open), e("span", { key: "i", className: "dpt__ico" }, ICON.task()), e("span", { key: "l", className: "dpt__label" }, task.name), e("span", { key: "a", className: "dpt__actions" }, [ actBtn(ICON.newchat(), "New session here", () => newSessionInTask(task.id)) ]) ] }), open && e("div", { key: "c", className: "dpt__children", ...dropAttrs({ type: "task", id: task.id, groupId: task.groupId }) }, task.sessions.map((s) => sessionRow(s))) ]); }; const groupNode = (group) => { const key = "g:" + group.id; const open = !isCollapsed(key); return e(react.Fragment, { key: group.id }, [ row({ id: group.id, projectId: group.projectId, dragType: "group", drag: true, drop: dropAttrs({ type: "group", id: group.id, projectId: group.projectId }), onClick: () => toggle(key), onContextMenu: (ev) => { lastMenuEvent.current = ev; openMenu(ev, [ { icon: ICON.plus(), label: "New task…", run: async () => { const n = promptName("New task name"); if (n) await run(api("task-create", { groupId: group.id, name: n })); } }, { icon: ICON.edit(), label: "Rename group", run: () => { const n = promptName("Rename group", group.name); if (n) commit(mut.renameGroup(group.id, n), () => api("group-rename", { groupId: group.id, name: n })); } }, { icon: ICON.trash(), label: "Delete group", run: () => { if (window.confirm(`Delete group "${group.name}"? Its tasks are removed; sessions return to Unsorted.`)) commit(mut.deleteGroup(group.id), () => api("group-delete", { groupId: group.id })); } } ]); }, children: [ ICON.chevron(open), e("span", { key: "i", className: "dpt__ico" }, ICON.group()), e("span", { key: "l", className: "dpt__label" }, group.name), e("span", { key: "a", className: "dpt__actions" }, [ actBtn(ICON.plus(), "New task", async () => { const n = promptName("New task name"); if (n) await run(api("task-create", { groupId: group.id, name: n })); }) ]) ] }), open && e("div", { key: "c", className: "dpt__children", ...dropAttrs({ type: "group", id: group.id, projectId: group.projectId }) }, group.tasks.map((t) => taskNode(t))) ]); }; const projectNode = (project) => { const key = "p:" + project.id; const open = !isCollapsed(key); return e(react.Fragment, { key: project.id }, [ row({ id: project.id, dragType: "project", drag: true, drop: dropAttrs({ type: "project", id: project.id }), onClick: () => toggle(key), onContextMenu: (ev) => { lastMenuEvent.current = ev; openMenu(ev, [ { icon: ICON.plus(), label: "New group…", run: async () => { const n = promptName("New group name"); if (n) await run(api("group-create", { projectId: project.id, name: n })); } }, { icon: ICON.edit(), label: "Rename project", run: () => { const n = promptName("Rename project", project.name); if (n) commit(mut.renameProject(project.id, n), () => api("project-rename", { projectId: project.id, name: n })); } }, { icon: ICON.trash(), label: "Delete project", run: () => { if (window.confirm(`Delete project "${project.name}"? Its groups and tasks are removed; sessions return to Unsorted.`)) commit(mut.deleteProject(project.id), () => api("project-delete", { projectId: project.id })); } } ]); }, children: [ ICON.chevron(open), e("span", { key: "i", className: "dpt__ico" }, ICON.project()), e("span", { key: "l", className: "dpt__label" }, project.name), e("span", { key: "a", className: "dpt__actions" }, [ actBtn(ICON.plus(), "New group", async () => { const n = promptName("New group name"); if (n) await run(api("group-create", { projectId: project.id, name: n })); }) ]) ] }), open && e("div", { key: "c", className: "dpt__children", ...dropAttrs({ type: "project", id: project.id }) }, project.groups.map((g) => groupNode(g))) ]); }; const renameSession = (sessionId) => { const current = list.byId[sessionId]?.displayTitle ?? ""; const n = promptName("Rename session", current); if (n && n !== current) A.renameSession(sessionId, n).then((r) => { if (r && r.ok === false) flash(r.error); }); }; const sessionMenuItems = (sessionId) => { const items = [{ icon: ICON.edit(), label: "Rename session", run: () => renameSession(sessionId) }, { separator: true }]; for (const project of view.projects) for (const group of project.groups) for (const task of group.tasks) { items.push({ icon: ICON.task(), label: task.name, hint: project.name + " / " + group.name, run: () => commit(mut.assign(sessionId, task.id), () => api("session-assign", { sessionId, taskId: task.id })) }); } if (items.length > 0) items.push({ separator: true }); items.push({ label: "Remove from task (Unsorted)", run: () => commit(mut.assign(sessionId, null), () => api("session-assign", { sessionId, taskId: null })) }); return items; }; const unsortedKey = "unsorted"; const unsortedOpen = !isCollapsed(unsortedKey); const menuNode = menu === null ? null : e("div", { className: "dpt__menu", style: { left: Math.min(menu.x, window.innerWidth - 210), top: Math.min(menu.y, window.innerHeight - 40) }, onMouseDown: (ev) => ev.stopPropagation() }, menu.items.map((item, index) => item.separator ? e("div", { key: "s" + index, className: "dpt__sep" }) : e("div", { key: index, className: "dpt__mi", onClick: () => { closeMenu(); void item.run(); } }, [ item.icon ? e("span", { key: "i", className: "dpt__ico" }, item.icon) : null, e("span", { key: "l", style: { overflow: "hidden", textOverflow: "ellipsis" } }, item.label), item.hint ? e("small", { key: "h" }, item.hint) : null ]) )); const workItems = []; for (const id of (tree?.inWork ?? [])) { const s = list.byId[id]; if (s && sessionVisible(s, archived)) { workItems.push({ id, name: s.displayTitle || "Untitled", icon: e("span", { className: "dpt__dot", "data-state": sessionState(s) }) }); } } const workSection = workItems.length === 0 ? null : e(react.Fragment, { key: "work" }, [ e("div", { key: "wb", className: "dpt__bar" }, [e("b", { key: "t" }, "In work")]), workItems.map((it) => e("div", { key: it.id, className: "dpt__row", "data-active": it.id === list.current, onClick: () => A.openSession(it.id), onContextMenu: (ev) => { lastMenuEvent.current = ev; openMenu(ev, sessionMenuItems(it.id)); } }, [ e("span", { key: "t", className: "dpt__twist" }), e("div", { key: "body", className: "dpt__body" }, [ e("span", { key: "i", className: "dpt__ico", "data-kind": "session" }, it.icon), e("span", { key: "l", className: "dpt__label", title: it.name }, it.name), workToggle(it.id) ]) ])) ]); return e("div", { className: "dpt" }, [ notice && e("div", { key: "notice", className: "dpt__notice" }, notice), workSection, e("div", { key: "bar", className: "dpt__bar" }, [ e("b", { key: "t" }, "Projects"), e("span", { key: "add", className: "dpt__iconbtn", title: "New project", onClick: async () => { const n = promptName("New project name"); if (n) await run(api("project-create", { name: n })); } }, ICON.plus()) ]), tree === null ? e("div", { key: "hint", className: "dpt__hint" }, "Loading…") : e(react.Fragment, { key: "tree" }, [ view.projects.map((p) => projectNode(p)), e(react.Fragment, { key: "uns" }, [ row({ id: unsortedKey, dragType: "_", drag: false, drop: dropAttrs({ type: "unsorted" }), onClick: () => toggle(unsortedKey), children: [ ICON.chevron(unsortedOpen), e("span", { key: "i", className: "dpt__ico" }, ICON.group()), e("span", { key: "l", className: "dpt__label", style: { color: "var(--dsw-alias-label-tertiary)" } }, "Unsorted") ] }), unsortedOpen && e("div", { key: "c", className: "dpt__children", ...dropAttrs({ type: "unsorted" }) }, view.unsorted.map((s) => sessionRow(s))) ]) ]), menuNode ]); } function apply(ctx) { const injected = { openSession: (sessionId) => { ctx.sessions.open(sessionId); }, startSession: (workspaceId) => ctx.workspaces.startSession(workspaceId), renameSession: async (sessionId, title) => { const session = ctx.sessions.binding(sessionId)?.session; if (session === undefined) return { ok: false, error: { code: "unknown-session" } }; const result = await session.rename(title); return result?.ok === true ? { ok: true } : { ok: false, error: result?.error ?? { code: "rename-failed" } }; }, listTree: () => api("list", {}) }; ctx.slots.inject("sidebar.workspaces", () => ctx.slots.register({ name: "sidebar.workspaces", priority: -1, store: createViewStore(), children: {}, inject: () => injected }, ProjectTreeBrowser)); } module.exports = { name, inject, apply }; return module.exports; } });