dsh-taskboard 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +301 -282
- package/lib/client.js +937 -920
- package/lib/host/archive-sessions.js +30 -0
- package/lib/host/archive-sessions.js.map +1 -0
- package/lib/host/execution.js +3 -0
- package/lib/host/execution.js.map +1 -1
- package/lib/host/locale.js +17 -0
- package/lib/host/locale.js.map +1 -0
- package/lib/host/routes.js +45 -6
- package/lib/host/routes.js.map +1 -1
- package/lib/host/scheduler.js +2 -0
- package/lib/host/scheduler.js.map +1 -1
- package/lib/host/session-sync.js +4 -1
- package/lib/host/session-sync.js.map +1 -1
- package/lib/host/templates.js +11 -75
- package/lib/host/templates.js.map +1 -1
- package/lib/host/tools.js +19 -6
- package/lib/host/tools.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/builtin-templates.js +155 -0
- package/lib/shared/builtin-templates.js.map +1 -0
- package/lib/shared/protocol.js +39 -2
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +90 -90
- package/src/client/api.ts +5 -1
- package/src/client/board/TaskBoard.tsx +16 -11
- package/src/client/board/TaskDetail.tsx +116 -10
- package/src/client/board/TemplateManager.tsx +22 -10
- package/src/client/board-mount.tsx +4 -0
- package/src/client/controller.ts +23 -3
- package/src/client/i18n/en.ts +18 -0
- package/src/client/i18n/templates.ts +25 -0
- package/src/client/i18n/zh.ts +18 -0
- package/src/client/styles.ts +14 -3
- package/src/client/window-inset.ts +38 -0
- package/src/host/archive-sessions.ts +18 -0
- package/src/host/execution.ts +4 -1
- package/src/host/locale.ts +44 -0
- package/src/host/routes.ts +40 -7
- package/src/host/scheduler.ts +2 -0
- package/src/host/session-sync.ts +4 -1
- package/src/host/templates.ts +8 -59
- package/src/host/tools.ts +26 -9
- package/src/shared/api.ts +6 -3
- package/src/shared/builtin-templates.ts +153 -0
- package/src/shared/protocol.ts +64 -0
- package/src/shared/version.ts +9 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-sync.js","names":[],"sources":["../../src/host/session-sync.ts"],"sourcesContent":["/**\n * External workspace session synchronization service.\n *\n * When `settings.syncExternalSessions` is enabled (0.5.4):\n * - Listens to session lifecycle events from outside the taskboard.\n * - On `turn/start`: automatically captures or resumes the session on the board\n * (status: `in_progress`, claimedBy: sessionId).\n * - On `user/message` / `session/title`: enriches/updates task title & description.\n * - On `turn/end`: settles the execution (success -> `in_review` 待验收, failure -> `todo`).\n *\n * @module dsh-taskboard/host/session-sync\n */\nimport {\n defaultSyncExternalSessionsOf,\n newCommentId,\n newExecutionId,\n newTaskId,\n normalizeBody,\n normalizeTitle,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport type { EventsFace } from './execution.ts'\nimport type { TaskStore } from './store.ts'\nimport type { WorkspaceFace } from './tools.ts'\n\n/** Extract text content from a user message event payload. */\nexport function extractUserMessageText(msg: unknown): string {\n if (typeof msg !== 'object' || msg === null) return ''\n const content = (msg as { content?: unknown }).content\n if (typeof content === 'string') return content\n if (Array.isArray(content)) {\n return content\n .map(part => {\n if (typeof part === 'string') return part\n if (typeof part === 'object' && part !== null && 'text' in part && typeof (part as { text: unknown }).text === 'string') {\n return (part as { text: string }).text\n }\n return ''\n })\n .filter(Boolean)\n .join('\\n')\n }\n return ''\n}\n\n/** Extract a short one-line title from prompt text. */\nexport function titleFromText(text: string): string {\n const clean = text.trim().replace(/^#+\\s*/, '')\n const firstLine = clean.split('\\n')[0]?.trim() ?? ''\n return firstLine.slice(0, 50).trim()\n}\n\n/**\n * Detect whether a session represents a subagent child conversation.\n * Subagents are created by agent delegation (e.g. invoke_subagent / subagents service)\n * and should never be automatically converted into user tasks on the taskboard.\n */\nexport function isSubagentSession(\n sessionId: string,\n sessionMeta?: unknown,\n event?: { type: string; data?: unknown },\n): boolean {\n if (typeof sessionId === 'string') {\n if (sessionId.startsWith('subagent-') || sessionId.startsWith('child-') || sessionId.startsWith('delegate-')) {\n return true\n }\n }\n\n if (typeof sessionMeta === 'object' && sessionMeta !== null) {\n const s = sessionMeta as {\n header?: Record<string, unknown>\n meta?: Record<string, unknown>\n options?: Record<string, unknown>\n }\n\n const header = s.header\n const meta = s.meta\n const options = s.options\n\n // Check origin\n if (header?.origin === 'subagent' || meta?.origin === 'subagent') return true\n\n // Check parent session lineage\n if (\n header?.parentSession !== undefined\n || header?.parentSessionId !== undefined\n || meta?.parentSession !== undefined\n || meta?.parentSessionId !== undefined\n ) {\n return true\n }\n\n // Check delegation depth\n if (typeof header?.delegationDepth === 'number' && header.delegationDepth > 0) return true\n if (typeof meta?.delegationDepth === 'number' && meta.delegationDepth > 0) return true\n if (typeof options?.subagentDepth === 'number' && options.subagentDepth > 0) return true\n }\n\n if (event !== undefined) {\n if (event.type === 'subagent/descriptor' || event.type === 'subagent/start' || event.type === 'subagent/end') {\n return true\n }\n if (typeof event.data === 'object' && event.data !== null) {\n const d = event.data as Record<string, unknown>\n if (\n d.origin === 'subagent'\n || d.subagent === true\n || typeof d.subagentId === 'string'\n || typeof d.parentSession === 'string'\n ) {\n return true\n }\n }\n }\n\n return false\n}\n\n/**\n * Detect whether a session represents an active working conversation.\n * Inspects state, status, isWorking/isBusy methods, running flags, and active turns.\n */\nexport function isSessionActiveWorking(session: unknown): boolean {\n if (typeof session !== 'object' || session === null) return false\n const s = session as Record<string, unknown>\n\n // 1. Method checks\n if (typeof s.isWorking === 'function') {\n try { if (Boolean((s.isWorking as () => boolean)())) return true } catch { /* ignore */ }\n } else if (s.isWorking === true) {\n return true\n }\n\n if (typeof s.isBusy === 'function') {\n try { if (Boolean((s.isBusy as () => boolean)())) return true } catch { /* ignore */ }\n } else if (s.busy === true || s.isBusy === true) {\n return true\n }\n\n // 2. Boolean flags\n if (s.running === true || s.active === true || s.isGenerating === true || s.generating === true) {\n return true\n }\n\n // 3. State string\n if (typeof s.state === 'string') {\n const st = s.state.toLowerCase()\n if (st === 'running' || st === 'working' || st === 'busy' || st === 'generating' || st === 'executing') {\n return true\n }\n }\n\n // 4. Status string\n if (typeof s.status === 'string') {\n const st = s.status.toLowerCase()\n if (st === 'running' || st === 'working' || st === 'busy' || st === 'active' || st === 'generating' || st === 'executing') {\n return true\n }\n }\n\n // 5. Active turn checks\n if (s.activeTurn !== undefined && s.activeTurn !== null && s.activeTurn !== false) {\n return true\n }\n if (s.currentTurn !== undefined && s.currentTurn !== null) {\n if (typeof s.currentTurn === 'object') {\n const ct = s.currentTurn as Record<string, unknown>\n if (ct.status === 'running' || ct.state === 'running' || ct.outcome === 'running' || ct.endedAt === undefined) {\n return true\n }\n } else {\n return true\n }\n }\n\n // 6. Turns list\n if (Array.isArray(s.turns) && s.turns.length > 0) {\n const lastTurn = s.turns[s.turns.length - 1]\n if (typeof lastTurn === 'object' && lastTurn !== null) {\n const lt = lastTurn as Record<string, unknown>\n if (lt.status === 'running' || lt.state === 'running' || lt.outcome === 'running' || (lt.startedAt !== undefined && lt.endedAt === undefined)) {\n return true\n }\n }\n }\n\n return false\n}\n\n/** Dependencies required by the external session sync service. */\nexport interface SessionSyncDeps {\n store: TaskStore\n workspaces: WorkspaceFace\n events: EventsFace\n sessions?: {\n get?: (id: string) => unknown\n list?: () => unknown[]\n }\n now: () => number\n scanIntervalMs?: number\n}\n\n/** Default scan interval: 4s. */\nexport const DEFAULT_SCAN_INTERVAL_MS = 4000\n\n/**\n * Service that synchronizes external workspace sessions into the taskboard.\n */\nexport class ExternalSessionSyncService {\n private readonly unsubscribe: () => void\n private readonly ignoredSessions = new Set<string>()\n private scanTimer?: NodeJS.Timeout | number\n\n constructor(private readonly deps: SessionSyncDeps) {\n this.unsubscribe = deps.events.onSessionEvent((sessionId, event, sessionMeta) => {\n void this.handleSessionEvent(sessionId, event, sessionMeta)\n })\n\n const interval = deps.scanIntervalMs ?? DEFAULT_SCAN_INTERVAL_MS\n if (interval > 0) {\n this.scanTimer = setInterval(() => {\n void this.scanActiveSessions()\n }, interval)\n }\n }\n\n /** Detach listener and clear scanner on teardown. */\n dispose(): void {\n this.unsubscribe()\n if (this.scanTimer !== undefined) {\n clearInterval(this.scanTimer as NodeJS.Timeout)\n this.scanTimer = undefined\n }\n }\n\n /**\n * Periodic active scan: checks whether external sessions linked to board tasks\n * are actively working, ensuring tasks in `in_review` / `todo` / `backlog`\n * automatically pull back to `in_progress`.\n */\n async scanActiveSessions(): Promise<void> {\n const snapshot = this.deps.store.snapshot()\n if (!defaultSyncExternalSessionsOf(snapshot.settings)) return\n if (this.deps.sessions === undefined) return\n\n const now = this.deps.now()\n const tasks = snapshot.tasks.filter(t => t.trashedAt === undefined)\n\n for (const task of tasks) {\n const sessionId = task.claimedBy ?? task.executions[task.executions.length - 1]?.sessionId\n if (sessionId === undefined || typeof sessionId !== 'string') continue\n if (this.ignoredSessions.has(sessionId) || sessionId.startsWith('session-taskboard-')) continue\n\n let session: unknown\n try {\n session = this.deps.sessions.get?.(sessionId)\n } catch { /* ignore */ }\n\n if (session === undefined && typeof this.deps.sessions.list === 'function') {\n try {\n const list = this.deps.sessions.list()\n session = list?.find(s => (s as { id?: string })?.id === sessionId)\n } catch { /* ignore */ }\n }\n\n if (session === undefined || session === null) continue\n if (isSubagentSession(sessionId, session)) {\n this.ignoredSessions.add(sessionId)\n continue\n }\n\n const isWorking = isSessionActiveWorking(session)\n if (isWorking) {\n if (task.status !== 'in_progress') {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const current = ledger.tasks.find(t => t.id === task.id)\n if (current === undefined || current.trashedAt !== undefined) return undefined\n current.status = 'in_progress'\n current.claimedBy = sessionId\n current.claimedAt = current.claimedAt ?? now\n current.updatedAt = now\n current.updatedBy = { kind: 'agent', sessionId }\n const hasRunning = current.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n current.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n }\n return [current]\n })\n }\n }\n }\n }\n\n private async handleSessionEvent(\n sessionId: string,\n event: { type: string; data?: unknown },\n sessionMeta?: {\n header?: { cwd?: string; origin?: string; parentSession?: string; parentSessionId?: string; delegationDepth?: number }\n meta?: { origin?: string; parentSession?: string; delegationDepth?: number }\n options?: { subagentDepth?: number }\n },\n ): Promise<void> {\n // 1. Check if already ignored\n if (this.ignoredSessions.has(sessionId)) return\n\n // 2. Ignore taskboard's internal execution sessions\n if (sessionId.startsWith('session-taskboard-')) {\n this.ignoredSessions.add(sessionId)\n return\n }\n\n // 3. Ignore subagent sessions (delegated children)\n if (isSubagentSession(sessionId, sessionMeta, event)) {\n this.ignoredSessions.add(sessionId)\n // If a task was previously created for this subagent before detection, clean it up\n await this.deps.store.mutate('task-deleted', (ledger) => {\n const idx = ledger.tasks.findIndex(\n t => t.claimedBy === sessionId && t.createdBy.kind === 'agent' && t.createdBy.sessionId === sessionId,\n )\n if (idx >= 0) {\n ledger.tasks.splice(idx, 1)\n return []\n }\n return undefined\n })\n return\n }\n\n // 4. Check if external session sync is enabled in board settings\n const snapshot = this.deps.store.snapshot()\n if (!defaultSyncExternalSessionsOf(snapshot.settings)) return\n\n const now = this.deps.now()\n\n if (event.type === 'turn/start') {\n await this.handleTurnStart(sessionId, sessionMeta?.header?.cwd, now)\n return\n }\n\n if (event.type === 'user/message') {\n await this.handleUserMessage(sessionId, event.data, now)\n return\n }\n\n if (\n event.type === 'turn/step'\n || event.type === 'turn/progress'\n || event.type === 'agent/step'\n || event.type === 'agent/thought'\n || event.type === 'agent/turn/start'\n ) {\n await this.ensureSessionInProgress(sessionId, now)\n return\n }\n\n if (event.type === 'session/title') {\n await this.handleSessionTitle(sessionId, event.data, now)\n return\n }\n\n if (event.type === 'turn/end') {\n await this.handleTurnEnd(sessionId, event.data, now)\n return\n }\n }\n\n private async ensureSessionInProgress(sessionId: string, now: number): Promise<void> {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n\n let changed = false\n if (task.status !== 'in_progress') {\n task.status = 'in_progress'\n task.claimedBy = sessionId\n task.claimedAt = task.claimedAt ?? now\n changed = true\n }\n const hasRunning = task.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n task.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n changed = true\n }\n if (changed) {\n task.updatedAt = now\n task.updatedBy = { kind: 'agent', sessionId }\n return [task]\n }\n return undefined\n })\n }\n\n private async handleTurnStart(sessionId: string, cwd: string | undefined, now: number): Promise<void> {\n // Resolve workspace\n let wsId: string | undefined\n if (cwd !== undefined && cwd.length > 0) {\n const resolved = await this.deps.workspaces.resolveByPath(cwd)\n wsId = resolved?.id\n }\n if (wsId === undefined) {\n wsId = this.deps.workspaces.list()[0]?.id ?? 'default'\n }\n\n await this.deps.store.mutate('task-created', (ledger) => {\n // Find existing task linked to this session\n const existing = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n\n if (existing !== undefined) {\n if (existing.trashedAt !== undefined) return undefined\n // If already in_progress and holding claim, ensure running execution\n if (existing.status === 'in_progress' && existing.claimedBy === sessionId) {\n const hasRunning = existing.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n existing.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n existing.updatedAt = now\n existing.updatedBy = { kind: 'agent', sessionId }\n return [existing]\n }\n return undefined\n }\n\n // Resumed or continued turn (e.g. from in_review or todo)\n existing.status = 'in_progress'\n existing.claimedBy = sessionId\n existing.claimedAt = now\n existing.updatedAt = now\n existing.updatedBy = { kind: 'agent', sessionId }\n existing.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n return [existing]\n }\n\n // Create new task for this external session\n const shortId = sessionId.replace(/^session-/, '').slice(0, 8)\n const newTask: TaskRecord = {\n id: newTaskId(),\n title: `会话 ${shortId}`,\n description: '',\n prompt: '',\n workspaceId: wsId,\n urgency: 'normal',\n status: 'in_progress',\n blocked: false,\n execution: { mode: 'claim' },\n isolation: 'none',\n claimedBy: sessionId,\n claimedAt: now,\n version: 1,\n createdAt: now,\n updatedAt: now,\n createdBy: { kind: 'agent', sessionId },\n updatedBy: { kind: 'agent', sessionId },\n comments: [],\n executions: [\n {\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n },\n ],\n }\n ledger.tasks.push(newTask)\n return [newTask]\n })\n }\n\n private async handleUserMessage(sessionId: string, msgData: unknown, now: number): Promise<void> {\n const text = extractUserMessageText(msgData)\n\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n\n let changed = false\n if (text.trim().length > 0) {\n // If title is default placeholder \"会话 ...\", replace with prompt summary\n if (task.title.startsWith('会话 ') && task.title.length <= 16) {\n const derived = titleFromText(text)\n if (derived.length > 0) {\n task.title = normalizeTitle(derived)\n changed = true\n }\n }\n // If description is empty, record initial prompt\n if (task.description.length === 0) {\n task.description = text.slice(0, 2000)\n changed = true\n }\n }\n\n // If task was in in_review, todo, or backlog, user message resumes work -> move to in_progress\n if (task.status !== 'in_progress') {\n task.status = 'in_progress'\n task.claimedBy = sessionId\n task.claimedAt = now\n changed = true\n }\n\n // Ensure running execution exists\n const hasRunning = task.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n task.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n changed = true\n }\n\n if (changed) {\n task.updatedAt = now\n task.updatedBy = { kind: 'user' }\n return [task]\n }\n return undefined\n })\n }\n\n private async handleSessionTitle(sessionId: string, titleData: unknown, now: number): Promise<void> {\n const rawTitle = typeof titleData === 'object' && titleData !== null && 'title' in titleData && typeof (titleData as { title: unknown }).title === 'string'\n ? (titleData as { title: string }).title\n : typeof titleData === 'string'\n ? titleData\n : ''\n if (rawTitle.trim().length === 0) return\n\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n task.title = normalizeTitle(rawTitle)\n task.updatedAt = now\n task.updatedBy = { kind: 'user' }\n return [task]\n })\n }\n\n private async handleTurnEnd(sessionId: string, endData: unknown, now: number): Promise<void> {\n const reason = typeof endData === 'object' && endData !== null && 'reason' in endData\n ? (endData as { reason: unknown }).reason\n : endData\n\n // Check if error or failure\n let isFailure = false\n let errorMessage = ''\n if (typeof reason === 'object' && reason !== null) {\n const r = reason as Record<string, unknown>\n if (r.kind === 'error' || r.kind === 'failure') {\n isFailure = true\n errorMessage = typeof r.error === 'string' ? r.error : typeof r.message === 'string' ? r.message : 'turn error'\n } else if (r.kind === 'cancel') {\n isFailure = true\n errorMessage = 'cancelled'\n }\n } else if (typeof reason === 'string' && (reason.includes('error') || reason.includes('fail'))) {\n isFailure = true\n errorMessage = reason\n }\n\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n\n // Settle running execution\n for (const exec of task.executions) {\n if (exec.sessionId === sessionId && exec.outcome === 'running') {\n exec.endedAt = now\n if (isFailure) {\n exec.outcome = 'failed'\n exec.error = errorMessage.slice(0, 500)\n } else {\n exec.outcome = 'succeeded'\n }\n }\n }\n\n delete task.claimedBy\n delete task.claimedAt\n task.updatedAt = now\n task.updatedBy = { kind: 'agent', sessionId }\n\n if (isFailure) {\n // Failed session hands back to todo with comment\n if (task.status === 'in_progress') {\n task.status = 'todo'\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 会话执行异常:${errorMessage.slice(0, 300)};任务已退回待办。`),\n version: 1,\n createdAt: now,\n })\n }\n } else {\n // Successful settlement automatically moves to in_review (待验收)\n if (task.status === 'in_progress') {\n task.status = 'in_review'\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody('[系统] 会话执行完毕,已自动进入待验收。'),\n version: 1,\n createdAt: now,\n })\n }\n }\n return [task]\n })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session-sync.js","names":[],"sources":["../../src/host/session-sync.ts"],"sourcesContent":["/**\n * External workspace session synchronization service.\n *\n * When `settings.syncExternalSessions` is enabled (0.5.4):\n * - Listens to session lifecycle events from outside the taskboard.\n * - On `turn/start`: automatically captures or resumes the session on the board\n * (status: `in_progress`, claimedBy: sessionId).\n * - On `user/message` / `session/title`: enriches/updates task title & description.\n * - On `turn/end`: settles the execution (success -> `in_review` 待验收, failure -> `todo`).\n *\n * @module dsh-taskboard/host/session-sync\n */\nimport {\n defaultSyncExternalSessionsOf,\n newCommentId,\n newExecutionId,\n newTaskId,\n normalizeBody,\n normalizeTitle,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport type { EventsFace } from './execution.ts'\nimport type { TaskStore } from './store.ts'\nimport type { WorkspaceFace } from './tools.ts'\n\n/** Extract text content from a user message event payload. */\nexport function extractUserMessageText(msg: unknown): string {\n if (typeof msg !== 'object' || msg === null) return ''\n const content = (msg as { content?: unknown }).content\n if (typeof content === 'string') return content\n if (Array.isArray(content)) {\n return content\n .map(part => {\n if (typeof part === 'string') return part\n if (typeof part === 'object' && part !== null && 'text' in part && typeof (part as { text: unknown }).text === 'string') {\n return (part as { text: string }).text\n }\n return ''\n })\n .filter(Boolean)\n .join('\\n')\n }\n return ''\n}\n\n/** Extract a short one-line title from prompt text. */\nexport function titleFromText(text: string): string {\n const clean = text.trim().replace(/^#+\\s*/, '')\n const firstLine = clean.split('\\n')[0]?.trim() ?? ''\n return firstLine.slice(0, 50).trim()\n}\n\n/**\n * Detect whether a session represents a subagent child conversation.\n * Subagents are created by agent delegation (e.g. invoke_subagent / subagents service)\n * and should never be automatically converted into user tasks on the taskboard.\n */\nexport function isSubagentSession(\n sessionId: string,\n sessionMeta?: unknown,\n event?: { type: string; data?: unknown },\n): boolean {\n if (typeof sessionId === 'string') {\n if (sessionId.startsWith('subagent-') || sessionId.startsWith('child-') || sessionId.startsWith('delegate-')) {\n return true\n }\n }\n\n if (typeof sessionMeta === 'object' && sessionMeta !== null) {\n const s = sessionMeta as {\n header?: Record<string, unknown>\n meta?: Record<string, unknown>\n options?: Record<string, unknown>\n }\n\n const header = s.header\n const meta = s.meta\n const options = s.options\n\n // Check origin\n if (header?.origin === 'subagent' || meta?.origin === 'subagent') return true\n\n // Check parent session lineage\n if (\n header?.parentSession !== undefined\n || header?.parentSessionId !== undefined\n || meta?.parentSession !== undefined\n || meta?.parentSessionId !== undefined\n ) {\n return true\n }\n\n // Check delegation depth\n if (typeof header?.delegationDepth === 'number' && header.delegationDepth > 0) return true\n if (typeof meta?.delegationDepth === 'number' && meta.delegationDepth > 0) return true\n if (typeof options?.subagentDepth === 'number' && options.subagentDepth > 0) return true\n }\n\n if (event !== undefined) {\n if (event.type === 'subagent/descriptor' || event.type === 'subagent/start' || event.type === 'subagent/end') {\n return true\n }\n if (typeof event.data === 'object' && event.data !== null) {\n const d = event.data as Record<string, unknown>\n if (\n d.origin === 'subagent'\n || d.subagent === true\n || typeof d.subagentId === 'string'\n || typeof d.parentSession === 'string'\n ) {\n return true\n }\n }\n }\n\n return false\n}\n\n/**\n * Detect whether a session represents an active working conversation.\n * Inspects state, status, isWorking/isBusy methods, running flags, and active turns.\n */\nexport function isSessionActiveWorking(session: unknown): boolean {\n if (typeof session !== 'object' || session === null) return false\n const s = session as Record<string, unknown>\n\n // 1. Method checks\n if (typeof s.isWorking === 'function') {\n try { if (Boolean((s.isWorking as () => boolean)())) return true } catch { /* ignore */ }\n } else if (s.isWorking === true) {\n return true\n }\n\n if (typeof s.isBusy === 'function') {\n try { if (Boolean((s.isBusy as () => boolean)())) return true } catch { /* ignore */ }\n } else if (s.busy === true || s.isBusy === true) {\n return true\n }\n\n // 2. Boolean flags\n if (s.running === true || s.active === true || s.isGenerating === true || s.generating === true) {\n return true\n }\n\n // 3. State string\n if (typeof s.state === 'string') {\n const st = s.state.toLowerCase()\n if (st === 'running' || st === 'working' || st === 'busy' || st === 'generating' || st === 'executing') {\n return true\n }\n }\n\n // 4. Status string\n if (typeof s.status === 'string') {\n const st = s.status.toLowerCase()\n if (st === 'running' || st === 'working' || st === 'busy' || st === 'active' || st === 'generating' || st === 'executing') {\n return true\n }\n }\n\n // 5. Active turn checks\n if (s.activeTurn !== undefined && s.activeTurn !== null && s.activeTurn !== false) {\n return true\n }\n if (s.currentTurn !== undefined && s.currentTurn !== null) {\n if (typeof s.currentTurn === 'object') {\n const ct = s.currentTurn as Record<string, unknown>\n if (ct.status === 'running' || ct.state === 'running' || ct.outcome === 'running' || ct.endedAt === undefined) {\n return true\n }\n } else {\n return true\n }\n }\n\n // 6. Turns list\n if (Array.isArray(s.turns) && s.turns.length > 0) {\n const lastTurn = s.turns[s.turns.length - 1]\n if (typeof lastTurn === 'object' && lastTurn !== null) {\n const lt = lastTurn as Record<string, unknown>\n if (lt.status === 'running' || lt.state === 'running' || lt.outcome === 'running' || (lt.startedAt !== undefined && lt.endedAt === undefined)) {\n return true\n }\n }\n }\n\n return false\n}\n\n/** Dependencies required by the external session sync service. */\nexport interface SessionSyncDeps {\n store: TaskStore\n workspaces: WorkspaceFace\n events: EventsFace\n sessions?: {\n get?: (id: string) => unknown\n list?: () => unknown[]\n }\n now: () => number\n scanIntervalMs?: number\n}\n\n/** Default scan interval: 4s. */\nexport const DEFAULT_SCAN_INTERVAL_MS = 4000\n\n/**\n * Service that synchronizes external workspace sessions into the taskboard.\n */\nexport class ExternalSessionSyncService {\n private readonly unsubscribe: () => void\n private readonly ignoredSessions = new Set<string>()\n private scanTimer?: NodeJS.Timeout | number\n\n constructor(private readonly deps: SessionSyncDeps) {\n this.unsubscribe = deps.events.onSessionEvent((sessionId, event, sessionMeta) => {\n return this.handleSessionEvent(sessionId, event, sessionMeta)\n })\n\n const interval = deps.scanIntervalMs ?? DEFAULT_SCAN_INTERVAL_MS\n if (interval > 0) {\n this.scanTimer = setInterval(() => {\n void this.scanActiveSessions()\n }, interval)\n }\n }\n\n /** Detach listener and clear scanner on teardown. */\n dispose(): void {\n this.unsubscribe()\n if (this.scanTimer !== undefined) {\n clearInterval(this.scanTimer as NodeJS.Timeout)\n this.scanTimer = undefined\n }\n }\n\n /**\n * Periodic active scan: checks whether external sessions linked to board tasks\n * are actively working, ensuring tasks in `in_review` / `todo` / `backlog`\n * automatically pull back to `in_progress`.\n */\n async scanActiveSessions(): Promise<void> {\n const snapshot = this.deps.store.snapshot()\n if (!defaultSyncExternalSessionsOf(snapshot.settings)) return\n if (this.deps.sessions === undefined) return\n\n const now = this.deps.now()\n const tasks = snapshot.tasks.filter(t => t.trashedAt === undefined)\n\n for (const task of tasks) {\n const sessionId = task.claimedBy ?? task.executions[task.executions.length - 1]?.sessionId\n if (sessionId === undefined || typeof sessionId !== 'string') continue\n if (this.ignoredSessions.has(sessionId) || sessionId.startsWith('session-taskboard-')) continue\n\n let session: unknown\n try {\n session = this.deps.sessions.get?.(sessionId)\n } catch { /* ignore */ }\n\n if (session === undefined && typeof this.deps.sessions.list === 'function') {\n try {\n const list = this.deps.sessions.list()\n session = list?.find(s => (s as { id?: string })?.id === sessionId)\n } catch { /* ignore */ }\n }\n\n if (session === undefined || session === null) continue\n if (isSubagentSession(sessionId, session)) {\n this.ignoredSessions.add(sessionId)\n continue\n }\n\n const isWorking = isSessionActiveWorking(session)\n if (isWorking) {\n if (task.status !== 'in_progress') {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const current = ledger.tasks.find(t => t.id === task.id)\n if (current === undefined || current.trashedAt !== undefined) return undefined\n current.status = 'in_progress'\n current.claimedBy = sessionId\n current.claimedAt = current.claimedAt ?? now\n current.updatedAt = now\n current.updatedBy = { kind: 'agent', sessionId }\n const hasRunning = current.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n current.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n }\n return [current]\n })\n }\n }\n }\n }\n\n private async handleSessionEvent(\n sessionId: string,\n event: { type: string; data?: unknown },\n sessionMeta?: {\n header?: { cwd?: string; origin?: string; parentSession?: string; parentSessionId?: string; delegationDepth?: number }\n meta?: { origin?: string; parentSession?: string; delegationDepth?: number }\n options?: { subagentDepth?: number }\n },\n ): Promise<void> {\n // 1. Check if already ignored\n if (this.ignoredSessions.has(sessionId)) return\n\n // 2. Ignore taskboard's internal execution sessions\n if (sessionId.startsWith('session-taskboard-')) {\n this.ignoredSessions.add(sessionId)\n return\n }\n\n // 3. Ignore subagent sessions (delegated children)\n if (isSubagentSession(sessionId, sessionMeta, event)) {\n this.ignoredSessions.add(sessionId)\n // If a task was previously created for this subagent before detection, clean it up\n await this.deps.store.mutate('task-deleted', (ledger) => {\n const idx = ledger.tasks.findIndex(\n t => t.claimedBy === sessionId && t.createdBy.kind === 'agent' && t.createdBy.sessionId === sessionId,\n )\n if (idx >= 0) {\n ledger.tasks.splice(idx, 1)\n return []\n }\n return undefined\n })\n return\n }\n\n // 4. Check if external session sync is enabled in board settings\n const snapshot = this.deps.store.snapshot()\n if (!defaultSyncExternalSessionsOf(snapshot.settings)) return\n\n const now = this.deps.now()\n\n if (event.type === 'turn/start') {\n await this.handleTurnStart(sessionId, sessionMeta?.header?.cwd, now)\n return\n }\n\n if (event.type === 'user/message') {\n await this.handleUserMessage(sessionId, event.data, now)\n return\n }\n\n if (\n event.type === 'turn/step'\n || event.type === 'turn/progress'\n || event.type === 'agent/step'\n || event.type === 'agent/thought'\n || event.type === 'agent/turn/start'\n ) {\n await this.ensureSessionInProgress(sessionId, now)\n return\n }\n\n if (event.type === 'session/title') {\n await this.handleSessionTitle(sessionId, event.data, now)\n return\n }\n\n if (event.type === 'turn/end') {\n await this.handleTurnEnd(sessionId, event.data, now)\n return\n }\n }\n\n private async ensureSessionInProgress(sessionId: string, now: number): Promise<void> {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n\n let changed = false\n if (task.status !== 'in_progress') {\n task.status = 'in_progress'\n task.claimedBy = sessionId\n task.claimedAt = task.claimedAt ?? now\n changed = true\n }\n const hasRunning = task.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n task.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n changed = true\n }\n if (changed) {\n task.updatedAt = now\n task.updatedBy = { kind: 'agent', sessionId }\n return [task]\n }\n return undefined\n })\n }\n\n private async handleTurnStart(sessionId: string, cwd: string | undefined, now: number): Promise<void> {\n // Resolve workspace\n let wsId: string | undefined\n if (cwd !== undefined && cwd.length > 0) {\n const resolved = await this.deps.workspaces.resolveByPath(cwd)\n wsId = resolved?.id\n }\n if (wsId === undefined) {\n wsId = this.deps.workspaces.list()[0]?.id ?? 'default'\n }\n\n await this.deps.store.mutate('task-created', (ledger) => {\n // Find existing task linked to this session\n const existing = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n\n if (existing !== undefined) {\n if (existing.trashedAt !== undefined) return undefined\n // If already in_progress and holding claim, ensure running execution\n if (existing.status === 'in_progress' && existing.claimedBy === sessionId) {\n const hasRunning = existing.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n existing.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n existing.updatedAt = now\n existing.updatedBy = { kind: 'agent', sessionId }\n return [existing]\n }\n return undefined\n }\n\n // Resumed or continued turn (e.g. from in_review or todo)\n existing.status = 'in_progress'\n existing.claimedBy = sessionId\n existing.claimedAt = now\n existing.updatedAt = now\n existing.updatedBy = { kind: 'agent', sessionId }\n existing.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n return [existing]\n }\n\n // Create new task for this external session\n const shortId = sessionId.replace(/^session-/, '').slice(0, 8)\n const newTask: TaskRecord = {\n id: newTaskId(),\n title: `会话 ${shortId}`,\n description: '',\n prompt: '',\n workspaceId: wsId,\n urgency: 'normal',\n status: 'in_progress',\n blocked: false,\n execution: { mode: 'claim' },\n isolation: 'none',\n claimedBy: sessionId,\n claimedAt: now,\n version: 1,\n createdAt: now,\n updatedAt: now,\n createdBy: { kind: 'agent', sessionId },\n updatedBy: { kind: 'agent', sessionId },\n comments: [],\n executions: [\n {\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n },\n ],\n }\n ledger.tasks.push(newTask)\n return [newTask]\n })\n }\n\n private async handleUserMessage(sessionId: string, msgData: unknown, now: number): Promise<void> {\n const text = extractUserMessageText(msgData)\n\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n\n let changed = false\n if (text.trim().length > 0) {\n // If title is default placeholder \"会话 ...\", replace with prompt summary\n if (task.title.startsWith('会话 ') && task.title.length <= 16) {\n const derived = titleFromText(text)\n if (derived.length > 0) {\n task.title = normalizeTitle(derived)\n changed = true\n }\n }\n // If description is empty, record initial prompt\n if (task.description.length === 0) {\n task.description = text.slice(0, 2000)\n changed = true\n }\n }\n\n // If task was in in_review, todo, or backlog, user message resumes work -> move to in_progress\n if (task.status !== 'in_progress') {\n task.status = 'in_progress'\n task.claimedBy = sessionId\n task.claimedAt = now\n changed = true\n }\n\n // Ensure running execution exists\n const hasRunning = task.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n task.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n changed = true\n }\n\n if (changed) {\n task.updatedAt = now\n task.updatedBy = { kind: 'user' }\n return [task]\n }\n return undefined\n })\n }\n\n private async handleSessionTitle(sessionId: string, titleData: unknown, now: number): Promise<void> {\n const rawTitle = typeof titleData === 'object' && titleData !== null && 'title' in titleData && typeof (titleData as { title: unknown }).title === 'string'\n ? (titleData as { title: string }).title\n : typeof titleData === 'string'\n ? titleData\n : ''\n if (rawTitle.trim().length === 0) return\n\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n task.title = normalizeTitle(rawTitle)\n task.updatedAt = now\n task.updatedBy = { kind: 'user' }\n return [task]\n })\n }\n\n private async handleTurnEnd(sessionId: string, endData: unknown, now: number): Promise<void> {\n const reason = typeof endData === 'object' && endData !== null && 'reason' in endData\n ? (endData as { reason: unknown }).reason\n : endData\n\n // Check if error or failure\n let isFailure = false\n let errorMessage = ''\n if (typeof reason === 'object' && reason !== null) {\n const r = reason as Record<string, unknown>\n if (r.kind === 'error' || r.kind === 'failure') {\n isFailure = true\n errorMessage = typeof r.error === 'string' ? r.error : typeof r.message === 'string' ? r.message : 'turn error'\n } else if (r.kind === 'cancel') {\n isFailure = true\n errorMessage = 'cancelled'\n }\n } else if (typeof reason === 'string' && (reason.includes('error') || reason.includes('fail'))) {\n isFailure = true\n errorMessage = reason\n }\n\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n\n // Settle running execution\n for (const exec of task.executions) {\n if (exec.sessionId === sessionId && exec.outcome === 'running') {\n exec.endedAt = now\n if (isFailure) {\n exec.outcome = 'failed'\n exec.error = errorMessage.slice(0, 500)\n } else {\n exec.outcome = 'succeeded'\n }\n }\n }\n\n delete task.claimedBy\n delete task.claimedAt\n task.updatedAt = now\n task.updatedBy = { kind: 'agent', sessionId }\n\n if (isFailure) {\n // Failed session hands back to todo with comment\n if (task.status === 'in_progress') {\n task.status = 'todo'\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 会话执行异常:${errorMessage.slice(0, 300)};任务已退回待办。`),\n systemKey: 'sys.sessionError',\n systemParams: { error: errorMessage.slice(0, 300) },\n version: 1,\n createdAt: now,\n })\n }\n } else {\n // Successful settlement automatically moves to in_review (待验收)\n if (task.status === 'in_progress') {\n task.status = 'in_review'\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody('[系统] 会话执行完毕,已自动进入待验收。'),\n systemKey: 'sys.sessionDone',\n version: 1,\n createdAt: now,\n })\n }\n }\n return [task]\n })\n }\n}\n"],"mappings":"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"}
|
package/lib/host/templates.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BUILTIN_TEMPLATE_CONTENT, BUILTIN_TEMPLATE_IDS } from "../shared/builtin-templates.js";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
//#region src/host/templates.ts
|
|
3
4
|
/**
|
|
@@ -10,81 +11,16 @@ import { readFile } from "node:fs/promises";
|
|
|
10
11
|
*
|
|
11
12
|
* @module dsh-taskboard/host/templates
|
|
12
13
|
*/
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"2. 实现功能(含类型定义与错误处理)",
|
|
24
|
-
"3. 补充测试(单测/回归)",
|
|
25
|
-
"4. 运行相关测试套件确认通过"
|
|
26
|
-
].join("\n"),
|
|
27
|
-
urgency: "normal",
|
|
28
|
-
checklist: [
|
|
29
|
-
"实现要点已明确(需求边界与验收标准)",
|
|
30
|
-
"功能已实现并补充测试",
|
|
31
|
-
"相关测试套件通过"
|
|
32
|
-
]
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
id: "tpl-bugfix",
|
|
37
|
-
name: "Bug 修复",
|
|
38
|
-
task: {
|
|
39
|
-
title: "修复:",
|
|
40
|
-
prompt: [
|
|
41
|
-
"修复以上问题并按序交接:",
|
|
42
|
-
"1. 复现问题(写最小复现步骤或测试)",
|
|
43
|
-
"2. 定位根因,说明为什么会发生",
|
|
44
|
-
"3. 修复并补回归测试",
|
|
45
|
-
"4. 运行相关测试套件确认无回归"
|
|
46
|
-
].join("\n"),
|
|
47
|
-
urgency: "urgent",
|
|
48
|
-
checklist: [
|
|
49
|
-
"已复现并定位根因",
|
|
50
|
-
"修复已提交到任务分支",
|
|
51
|
-
"回归测试通过"
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: "tpl-release",
|
|
57
|
-
name: "发布检查",
|
|
58
|
-
task: {
|
|
59
|
-
title: "发布:",
|
|
60
|
-
prompt: "执行发布流程:版本号更新、构建、测试、变更记录,完成后按序交接(不要实际推送/发布,等用户确认)。",
|
|
61
|
-
urgency: "normal",
|
|
62
|
-
checklist: [
|
|
63
|
-
"版本号已更新(package.json 与版本常量同步)",
|
|
64
|
-
"构建通过",
|
|
65
|
-
"全部测试通过",
|
|
66
|
-
"变更记录已写"
|
|
67
|
-
]
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: "tpl-patrol",
|
|
72
|
-
name: "例行巡检",
|
|
73
|
-
task: {
|
|
74
|
-
title: "巡检:",
|
|
75
|
-
prompt: [
|
|
76
|
-
"例行巡检:检查依赖更新、失败测试、明显代码问题与未处理的告警。",
|
|
77
|
-
"发现的问题逐条列出(严重度/位置/建议),小问题直接修复,大问题只报告不动手。",
|
|
78
|
-
"输出巡检摘要(用 {{lastComments}} 可回看上次巡检结论)。"
|
|
79
|
-
].join("\n"),
|
|
80
|
-
urgency: "relaxed",
|
|
81
|
-
execution: {
|
|
82
|
-
mode: "scheduled",
|
|
83
|
-
cron: "0 9 * * 1"
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
];
|
|
14
|
+
/**
|
|
15
|
+
* The built-in templates seeded when the side file does not exist yet.
|
|
16
|
+
* Seeded from the shared zh content (the side file is plain data; the client
|
|
17
|
+
* resolves the active locale at render time — see shared/builtin-templates.ts).
|
|
18
|
+
*/
|
|
19
|
+
const BUILTIN_TEMPLATES = BUILTIN_TEMPLATE_IDS.map((id) => ({
|
|
20
|
+
id,
|
|
21
|
+
name: BUILTIN_TEMPLATE_CONTENT.zh[id].name,
|
|
22
|
+
task: BUILTIN_TEMPLATE_CONTENT.zh[id].task
|
|
23
|
+
}));
|
|
88
24
|
/** Mint a template id. */
|
|
89
25
|
function newTemplateId() {
|
|
90
26
|
return `tpl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.js","names":[],"sources":["../../src/host/templates.ts"],"sourcesContent":["/**\n * Host-side task-template store (0.4.0): one JSON side file next to the\n * ledger, seeded with the built-in templates on first load, mutated through\n * the same atomic persist discipline as the ledger.\n *\n * Pure data, no Cordis deps — the routes layer owns it and tests drive it\n * directly against a temp dir.\n *\n * @module dsh-taskboard/host/templates\n */\nimport { readFile } from 'node:fs/promises'\nimport type { TaskTemplate } from '../shared/api.ts'\n\n
|
|
1
|
+
{"version":3,"file":"templates.js","names":[],"sources":["../../src/host/templates.ts"],"sourcesContent":["/**\n * Host-side task-template store (0.4.0): one JSON side file next to the\n * ledger, seeded with the built-in templates on first load, mutated through\n * the same atomic persist discipline as the ledger.\n *\n * Pure data, no Cordis deps — the routes layer owns it and tests drive it\n * directly against a temp dir.\n *\n * @module dsh-taskboard/host/templates\n */\nimport { readFile } from 'node:fs/promises'\nimport type { TaskTemplate } from '../shared/api.ts'\nimport { BUILTIN_TEMPLATE_CONTENT, BUILTIN_TEMPLATE_IDS, type BuiltinTemplateId } from '../shared/builtin-templates.ts'\n\n/**\n * The built-in templates seeded when the side file does not exist yet.\n * Seeded from the shared zh content (the side file is plain data; the client\n * resolves the active locale at render time — see shared/builtin-templates.ts).\n */\nexport const BUILTIN_TEMPLATES: ReadonlyArray<{ id: BuiltinTemplateId; name: string; task: TaskTemplate['task'] }> =\n BUILTIN_TEMPLATE_IDS.map(id => ({ id, name: BUILTIN_TEMPLATE_CONTENT.zh[id].name, task: BUILTIN_TEMPLATE_CONTENT.zh[id].task }))\n\n/** Mint a template id. */\nfunction newTemplateId(): string {\n return `tpl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`\n}\n\n/**\n * The template store. NOT thread-synchronized like the ledger (template\n * writes are rare, human-paced GUI operations; last-write-wins is fine).\n */\nexport class TemplateStore {\n private templates: TaskTemplate[] | undefined\n private loaded = false\n\n /** @param file - absolute side-file path (next to the ledger). */\n constructor(private readonly file: string) {}\n\n /** Load once; a missing file seeds the built-ins; a corrupt file resets. */\n private async ensure(): Promise<void> {\n if (this.loaded) return\n let parsed: TaskTemplate[] | undefined\n try {\n const raw = await readFile(this.file, 'utf8')\n const value = JSON.parse(raw) as { templates?: unknown }\n if (Array.isArray(value.templates)) {\n parsed = value.templates.filter((t): t is TaskTemplate =>\n typeof t === 'object' && t !== null && typeof (t as TaskTemplate).id === 'string'\n && typeof (t as TaskTemplate).name === 'string' && typeof (t as TaskTemplate).task === 'object')\n }\n } catch { /* missing or corrupt → seed */ }\n if (parsed === undefined) {\n const now = Date.now()\n parsed = BUILTIN_TEMPLATES.map((t, i) => ({ ...t, task: { ...t.task }, builtin: true, createdAt: now, updatedAt: now + i }))\n try { await this.persist(parsed) } catch { /* best effort — the seed returns in-memory */ }\n }\n this.templates = parsed\n this.loaded = true\n }\n\n /** Atomic persist (temp + fsync + rename — S10, same discipline as the ledger). */\n private async persist(templates: TaskTemplate[]): Promise<void> {\n const { mkdir, open, rename } = await import('node:fs/promises')\n const { dirname, join } = await import('node:path')\n await mkdir(dirname(this.file), { recursive: true })\n const temp = join(dirname(this.file), `.${Math.random().toString(36).slice(2)}.tmp`)\n const fh = await open(temp, 'w')\n try {\n await fh.writeFile(JSON.stringify({ templates }, null, 2), 'utf8')\n await fh.sync()\n } finally {\n await fh.close()\n }\n await rename(temp, this.file)\n }\n\n /** All templates (oldest first). */\n async list(): Promise<TaskTemplate[]> {\n await this.ensure()\n return (this.templates ?? []).slice()\n }\n\n /**\n * Create or replace a template by id (a body without id creates).\n * @returns the stored template.\n */\n async upsert(input: { id?: string; name: string; task: TaskTemplate['task'] }): Promise<TaskTemplate> {\n await this.ensure()\n const templates = this.templates ?? []\n const name = input.name.trim()\n if (name.length === 0 || name.length > 60) throw new Error('模板名必须 1..60 字符')\n const now = Date.now()\n const existing = input.id !== undefined ? templates.find(t => t.id === input.id) : undefined\n // T12: built-ins are factory content — editable only by delete + recreate\n // (deleting stays allowed), never silently overwritten in place.\n if (existing?.builtin === true) throw new Error('内置模板不可覆盖;可删除后另建,或以新名称存为新模板')\n const stored: TaskTemplate = existing !== undefined\n ? { ...existing, name, task: input.task, updatedAt: now }\n : { id: input.id ?? newTemplateId(), name, task: input.task, createdAt: now, updatedAt: now }\n const index = existing !== undefined ? templates.indexOf(existing) : -1\n if (index >= 0) templates[index] = stored\n else templates.push(stored)\n await this.persist(templates)\n return stored\n }\n\n /** Delete a template by id; returns whether it existed. */\n async remove(id: string): Promise<boolean> {\n await this.ensure()\n const templates = this.templates ?? []\n const index = templates.findIndex(t => t.id === id)\n if (index < 0) return false\n templates.splice(index, 1)\n await this.persist(templates)\n return true\n }\n}\n"],"mappings":"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"}
|
package/lib/host/tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { asIsolation, asStatus, asUrgency, canTransition, checklistFromTexts, defaultIsolationOf, effectivePrompt, isClaim, isClaimedBy, newCommentId, newTaskId, normalizeBody, normalizeExecution, normalizeExecutionReport, normalizeModel, normalizePrompt, normalizeTitle, summarize, syncClaim } from "../shared/protocol.js";
|
|
2
2
|
import { defineTool } from "./sdk.js";
|
|
3
|
+
import { archiveTaskSessions } from "./archive-sessions.js";
|
|
3
4
|
//#region src/host/tools.ts
|
|
4
5
|
/** Render side: one compact task line (id/status/version are load-bearing). */
|
|
5
6
|
function taskLine(t) {
|
|
@@ -30,11 +31,11 @@ function taskDetail(t) {
|
|
|
30
31
|
if (t.checklist !== void 0 && t.checklist.length > 0) {
|
|
31
32
|
const done = t.checklist.filter((i) => i.checked).length;
|
|
32
33
|
lines.push(`验收清单 (${done}/${t.checklist.length}):`);
|
|
33
|
-
for (const item of t.checklist) {
|
|
34
|
+
for (const [index, item] of t.checklist.entries()) {
|
|
34
35
|
const mark = item.checked ? "☑" : "☐";
|
|
35
36
|
const who = item.checkedBy === void 0 ? "" : item.checkedBy === "user" ? " ·用户勾选" : ` ·agent ${String(item.checkedBy).slice(0, 24)}勾选`;
|
|
36
37
|
const note = item.note !== void 0 ? ` ·证据: ${item.note}` : "";
|
|
37
|
-
lines.push(` ${mark} ${item.text}${who}${note}`);
|
|
38
|
+
lines.push(` ${mark} [${index + 1}] ${item.text}${who}${note} id=${item.id}`);
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
if (t.comments.length > 0) {
|
|
@@ -96,7 +97,8 @@ function workspaceFace(registry) {
|
|
|
96
97
|
id: ws.id,
|
|
97
98
|
path: ws.path,
|
|
98
99
|
title: ws.title
|
|
99
|
-
}))
|
|
100
|
+
})),
|
|
101
|
+
...typeof registry.archiveSession === "function" ? { archiveSession: (sessionId) => registry.archiveSession(sessionId) } : {}
|
|
100
102
|
};
|
|
101
103
|
}
|
|
102
104
|
/** Validate a pinned model: structural check always, provider route when known. */
|
|
@@ -490,15 +492,20 @@ function registerTaskboardTools(ctx, deps) {
|
|
|
490
492
|
type: "number",
|
|
491
493
|
required: true,
|
|
492
494
|
description: "Task version you read; fails on mismatch."
|
|
495
|
+
},
|
|
496
|
+
archiveSessions: {
|
|
497
|
+
type: "boolean",
|
|
498
|
+
description: "When moving to archived: whether to archive associated execution sessions as well. Defaults to false."
|
|
493
499
|
}
|
|
494
500
|
},
|
|
495
501
|
output: {
|
|
496
502
|
schema: JSON_OUT,
|
|
497
503
|
render: (_args, value) => {
|
|
498
|
-
const
|
|
504
|
+
const v = value;
|
|
505
|
+
const t = v.task;
|
|
499
506
|
return [{
|
|
500
507
|
type: "text",
|
|
501
|
-
text: t === void 0 ? "移动失败。" : `任务 ${t.id} 已移到 ${t.status},当前 v${t.version}
|
|
508
|
+
text: t === void 0 ? "移动失败。" : `任务 ${t.id} 已移到 ${t.status},当前 v${t.version}。${v.sessionArchive === void 0 ? "" : ` 会话归档结果:${JSON.stringify(v.sessionArchive)}`}`
|
|
502
509
|
}];
|
|
503
510
|
}
|
|
504
511
|
},
|
|
@@ -508,9 +515,11 @@ function registerTaskboardTools(ctx, deps) {
|
|
|
508
515
|
const to = asStatus(args.status);
|
|
509
516
|
const callerWsId = to === "in_progress" ? await callerWorkspace(deps, exec) : void 0;
|
|
510
517
|
let next;
|
|
518
|
+
let beforeTask;
|
|
511
519
|
await store.mutate("task-moved", (ledger) => {
|
|
512
520
|
const { index, task } = liveTaskAt(ledger, args.id);
|
|
513
521
|
versionGuard(task, args.ifVersion);
|
|
522
|
+
beforeTask = task;
|
|
514
523
|
if (to === "done") throw new ToolError(ERR.forbidden, "moving a task to done requires explicit user confirmation (GUI); agents cannot do it");
|
|
515
524
|
if (!canTransition(task.status, to)) throw new ToolError(ERR.invalidTransition, `illegal transition ${task.status} → ${to}`);
|
|
516
525
|
if (task.status === "in_progress" && task.claimedBy !== void 0 && task.claimedBy !== actor.sessionId) throw new ToolError(ERR.forbidden, `task is held by session ${task.claimedBy}; never take over another session's claim`);
|
|
@@ -525,7 +534,11 @@ function registerTaskboardTools(ctx, deps) {
|
|
|
525
534
|
ledger.tasks[index] = next;
|
|
526
535
|
return [next];
|
|
527
536
|
});
|
|
528
|
-
|
|
537
|
+
const sessionArchive = to === "archived" && args.archiveSessions === true ? await archiveTaskSessions(beforeTask ?? next, deps.workspaces.archiveSession) : void 0;
|
|
538
|
+
return json({
|
|
539
|
+
task: summarize(next),
|
|
540
|
+
...sessionArchive !== void 0 ? { sessionArchive } : {}
|
|
541
|
+
});
|
|
529
542
|
} catch (error) {
|
|
530
543
|
fail(error);
|
|
531
544
|
}
|