@wrongstack/tools 0.286.0 → 0.287.0

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.
Files changed (54) hide show
  1. package/README.md +2 -2
  2. package/dist/_edit-match.d.ts.map +1 -1
  3. package/dist/batch-tool-use.d.ts +1 -1
  4. package/dist/batch-tool-use.d.ts.map +1 -1
  5. package/dist/batch-tool-use.js +26 -8
  6. package/dist/batch-tool-use.js.map +2 -2
  7. package/dist/browser/index.js +1 -1
  8. package/dist/browser/index.js.map +1 -1
  9. package/dist/browser/types.d.ts +1 -1
  10. package/dist/browser/types.d.ts.map +1 -1
  11. package/dist/builtin.js +544 -118
  12. package/dist/builtin.js.map +4 -4
  13. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  14. package/dist/codebase-index/index.js +190 -96
  15. package/dist/codebase-index/index.js.map +4 -4
  16. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  17. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  18. package/dist/codebase-index/worker.js +188 -94
  19. package/dist/codebase-index/worker.js.map +4 -4
  20. package/dist/edit.d.ts.map +1 -1
  21. package/dist/edit.js +36 -12
  22. package/dist/edit.js.map +3 -3
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +818 -125
  26. package/dist/index.js.map +4 -4
  27. package/dist/kanban.d.ts +7 -2
  28. package/dist/kanban.d.ts.map +1 -1
  29. package/dist/kanban.js +158 -19
  30. package/dist/kanban.js.map +4 -4
  31. package/dist/next-steps.d.ts +6 -2
  32. package/dist/next-steps.d.ts.map +1 -1
  33. package/dist/next-steps.js +3 -3
  34. package/dist/next-steps.js.map +2 -2
  35. package/dist/pack.js +544 -118
  36. package/dist/pack.js.map +4 -4
  37. package/dist/plan.d.ts.map +1 -1
  38. package/dist/plan.js +154 -4
  39. package/dist/plan.js.map +4 -4
  40. package/dist/session-kanban.d.ts +38 -0
  41. package/dist/session-kanban.d.ts.map +1 -0
  42. package/dist/session-kanban.js +489 -0
  43. package/dist/session-kanban.js.map +7 -0
  44. package/dist/task.d.ts.map +1 -1
  45. package/dist/task.js +163 -4
  46. package/dist/task.js.map +4 -4
  47. package/dist/todo.d.ts.map +1 -1
  48. package/dist/todo.js +155 -7
  49. package/dist/todo.js.map +4 -4
  50. package/dist/tool-icon-map.d.ts.map +1 -1
  51. package/dist/tool-icons.d.ts.map +1 -1
  52. package/dist/tool-icons.js +7 -1
  53. package/dist/tool-icons.js.map +2 -2
  54. package/package.json +7 -3
package/dist/todo.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/todo.ts"],
4
- "sourcesContent": ["import type { TodoItem, Tool } from '@wrongstack/core';\nimport { loadPlan, savePlan, setPlanItemStatus } from '@wrongstack/core';\nimport { loadTasks, saveTasks } from '@wrongstack/core';\n\ninterface TodoInput {\n todos: TodoItem[];\n}\n\ninterface TodoOutput {\n count: number;\n in_progress: number;\n}\n\nexport const todoTool: Tool<TodoInput, TodoOutput> = {\n name: 'todo',\n category: 'Session',\n description:\n 'Manage the session-level todo list. This is the primary mechanism for tracking multi-step work. ' +\n 'The list is fully replaced on every call (not appended).',\n usageHint:\n 'BEST PRACTICE for complex tasks:\\n' +\n '- At the beginning of a non-trivial task, create a clear todo list with specific, actionable items.\\n' +\n '- Only **one** item should be `in_progress` at any time.\\n' +\n '- Update the list frequently as work progresses (mark items done, add new ones, change status).\\n' +\n '- **Re-order items** to reflect current priorities \u2014 the full list is replaced each call, so item order is entirely under your control.\\n' +\n '- When all items are completed the board auto-clears \u2014 you do NOT need to send an empty list.\\n' +\n '- The system and user can see this list, so keep it honest and up-to-date.\\n' +\n 'This tool is extremely valuable for maintaining focus and giving the user visibility into your plan.',\n permission: 'auto',\n mutating: false, // mutates only conversation state (ctx.todos), not external state \u2014 no confirmation needed\n timeoutMs: 1_000,\n capabilities: ['session.todo'],\n icon: 'todo',\n inputSchema: {\n type: 'object',\n properties: {\n todos: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Unique identifier for the todo item (e.g. \"1\", \"auth-flow\").',\n },\n content: {\n type: 'string',\n description: 'Clear, actionable description of the task.',\n },\n status: {\n type: 'string',\n enum: ['pending', 'in_progress', 'completed'],\n description: 'Current status. Only one item should be \"in_progress\" at a time.',\n },\n activeForm: {\n type: 'string',\n description: 'Optional present-tense form shown while the task is active (e.g. \"Fixing auth bug\").',\n },\n },\n required: ['id', 'content', 'status'],\n },\n description: 'The complete new list of todos. This replaces the previous list entirely.',\n },\n },\n required: ['todos'],\n },\n async execute(input, ctx) {\n if (!Array.isArray(input?.todos)) {\n throw new Error('todo: todos must be an array');\n }\n const items = input.todos.filter((t): t is TodoItem => Boolean(t?.id && t.content));\n const inProgress = items.filter((t) => t.status === 'in_progress');\n if (inProgress.length > 1) {\n // Keep only the first as in_progress, mark rest pending\n let seenInProgress = false;\n for (const item of items) {\n if (item.status === 'in_progress') {\n if (seenInProgress) item.status = 'pending';\n seenInProgress = true;\n }\n }\n }\n ctx.state.replaceTodos(items);\n\n // Auto-complete parent plan items / tasks when all their promoted\n // todos are done. Runs after state mutation so the UI sees the new\n // todo list before we touch the plan/task files.\n const completedPlanIds = new Set<string>();\n const completedTaskIds = new Set<string>();\n const pendingPlanIds = new Set<string>();\n const pendingTaskIds = new Set<string>();\n\n for (const item of items) {\n if (item.promotedFromPlan) {\n (item.status === 'completed' ? completedPlanIds : pendingPlanIds).add(item.promotedFromPlan);\n }\n if (item.promotedFromTask) {\n (item.status === 'completed' ? completedTaskIds : pendingTaskIds).add(item.promotedFromTask);\n }\n }\n\n // Mark fully-completed plan items as done\n for (const planId of completedPlanIds) {\n if (pendingPlanIds.has(planId)) continue; // not all done yet\n const planPath = (ctx.meta as Record<string, unknown>)['plan.path'];\n if (typeof planPath !== 'string' || !planPath) continue;\n try {\n const plan = await loadPlan(planPath);\n if (plan) {\n const updated = setPlanItemStatus(plan, planId, 'done');\n await savePlan(planPath, updated);\n }\n } catch { /* best-effort */ }\n }\n\n // Mark fully-completed tasks as completed\n for (const taskId of completedTaskIds) {\n if (pendingTaskIds.has(taskId)) continue; // not all done yet\n const taskPath = (ctx.meta as Record<string, unknown>)['task.path'];\n if (typeof taskPath !== 'string' || !taskPath) continue;\n try {\n const file = await loadTasks(taskPath);\n if (file) {\n const task = file.tasks.find((t) => t.id === taskId);\n if (task && task.status !== 'completed') {\n task.status = 'completed';\n task.updatedAt = new Date().toISOString();\n await saveTasks(taskPath, file);\n }\n }\n } catch { /* best-effort */ }\n }\n\n return {\n count: items.length,\n in_progress: items.filter((t) => t.status === 'in_progress').length,\n };\n },\n};\n"],
5
- "mappings": "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",
6
- "names": []
3
+ "sources": ["../src/todo.ts", "../src/session-kanban.ts"],
4
+ "sourcesContent": ["import type { TodoItem, Tool } from '@wrongstack/core';\nimport { loadPlan, loadTasks, savePlan, saveTasks, setPlanItemStatus } from '@wrongstack/core';\nimport { projectSessionTodosToKanban } from './session-kanban.js';\n\ninterface TodoInput {\n todos: TodoItem[];\n}\n\ninterface TodoOutput {\n count: number;\n in_progress: number;\n}\n\nexport const todoTool: Tool<TodoInput, TodoOutput> = {\n name: 'todo',\n category: 'Session',\n description:\n 'Manage the session-level todo list. This is the primary mechanism for tracking multi-step work. ' +\n 'The list is fully replaced on every call (not appended).',\n usageHint:\n 'BEST PRACTICE for complex tasks:\\n' +\n '- At the beginning of a non-trivial task, create a clear todo list with specific, actionable items.\\n' +\n '- Only **one** item should be `in_progress` at any time.\\n' +\n '- Update the list frequently as work progresses (mark items done, add new ones, change status).\\n' +\n '- **Re-order items** to reflect current priorities \u2014 the full list is replaced each call, so item order is entirely under your control.\\n' +\n '- When all items are completed the board auto-clears \u2014 you do NOT need to send an empty list.\\n' +\n '- The system and user can see this list, so keep it honest and up-to-date.\\n' +\n 'This tool is extremely valuable for maintaining focus and giving the user visibility into your plan.',\n permission: 'auto',\n mutating: false, // mutates only conversation state (ctx.todos), not external state \u2014 no confirmation needed\n timeoutMs: 5_000,\n capabilities: ['session.todo'],\n icon: 'todo',\n inputSchema: {\n type: 'object',\n properties: {\n todos: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Unique identifier for the todo item (e.g. \"1\", \"auth-flow\").',\n },\n content: {\n type: 'string',\n description: 'Clear, actionable description of the task.',\n },\n status: {\n type: 'string',\n enum: ['pending', 'in_progress', 'completed'],\n description: 'Current status. Only one item should be \"in_progress\" at a time.',\n },\n activeForm: {\n type: 'string',\n description:\n 'Optional present-tense form shown while the task is active (e.g. \"Fixing auth bug\").',\n },\n },\n required: ['id', 'content', 'status'],\n },\n description: 'The complete new list of todos. This replaces the previous list entirely.',\n },\n },\n required: ['todos'],\n },\n async execute(input, ctx) {\n if (!Array.isArray(input?.todos)) {\n throw new Error('todo: todos must be an array');\n }\n const items = input.todos.filter((t): t is TodoItem => Boolean(t?.id && t.content));\n const inProgress = items.filter((t) => t.status === 'in_progress');\n if (inProgress.length > 1) {\n // Keep only the first as in_progress, mark rest pending\n let seenInProgress = false;\n for (const item of items) {\n if (item.status === 'in_progress') {\n if (seenInProgress) item.status = 'pending';\n seenInProgress = true;\n }\n }\n }\n ctx.state.replaceTodos(items);\n\n // Kanban is the session work surface. Mirror the requested list (rather\n // than ctx.todos) so the final all-completed snapshot reaches Done even\n // though ConversationState auto-clears an all-done tactical todo list.\n await projectSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? 'session');\n\n // Auto-complete parent plan items / tasks when all their promoted\n // todos are done. Runs after state mutation so the UI sees the new\n // todo list before we touch the plan/task files.\n const completedPlanIds = new Set<string>();\n const completedTaskIds = new Set<string>();\n const pendingPlanIds = new Set<string>();\n const pendingTaskIds = new Set<string>();\n\n for (const item of items) {\n if (item.promotedFromPlan) {\n (item.status === 'completed' ? completedPlanIds : pendingPlanIds).add(\n item.promotedFromPlan,\n );\n }\n if (item.promotedFromTask) {\n (item.status === 'completed' ? completedTaskIds : pendingTaskIds).add(\n item.promotedFromTask,\n );\n }\n }\n\n // Mark fully-completed plan items as done\n for (const planId of completedPlanIds) {\n if (pendingPlanIds.has(planId)) continue; // not all done yet\n const planPath = (ctx.meta as Record<string, unknown>)['plan.path'];\n if (typeof planPath !== 'string' || !planPath) continue;\n try {\n const plan = await loadPlan(planPath);\n if (plan) {\n const updated = setPlanItemStatus(plan, planId, 'done');\n await savePlan(planPath, updated);\n }\n } catch {\n /* best-effort */\n }\n }\n\n // Mark fully-completed tasks as completed\n for (const taskId of completedTaskIds) {\n if (pendingTaskIds.has(taskId)) continue; // not all done yet\n const taskPath = (ctx.meta as Record<string, unknown>)['task.path'];\n if (typeof taskPath !== 'string' || !taskPath) continue;\n try {\n const file = await loadTasks(taskPath);\n if (file) {\n const task = file.tasks.find((t) => t.id === taskId);\n if (task && task.status !== 'completed') {\n task.status = 'completed';\n task.updatedAt = new Date().toISOString();\n await saveTasks(taskPath, file);\n }\n }\n } catch {\n /* best-effort */\n }\n }\n\n return {\n count: items.length,\n in_progress: items.filter((t) => t.status === 'in_progress').length,\n };\n },\n};\n", "import { type FSWatcher, watch } from 'node:fs';\nimport { basename, dirname } from 'node:path';\nimport {\n type Context,\n deserializeTaskGraph,\n loadPlan,\n loadTasks,\n mutatePlan,\n mutateTasks,\n type PlanFile,\n type PlanItem,\n type SerializedTaskGraph,\n type TaskFile,\n type TaskItem,\n type TaskStatus,\n type TodoItem,\n} from '@wrongstack/core';\nimport {\n createBoard,\n getBoard,\n type KanbanBoard,\n type KanbanColumn,\n type KanbanTask,\n listBoards,\n removeBoard,\n syncBoardFromTaskGraph,\n updateBoard,\n} from '@wrongstack/kanban';\n\nconst SESSION_BOARD_TAG = 'session-work';\nconst MIRROR_DISABLED_ENV = 'WRONGSTACK_KANBAN_TASK_MIRROR';\n\n/** The canonical workflow shared by WebUI and TUI session boards. */\nexport const SESSION_KANBAN_COLUMNS: KanbanColumn[] = [\n { id: 'todo', title: 'Todo', order: 0, wipLimit: 0, color: '#2563eb' },\n { id: 'in-progress', title: 'Running', order: 1, wipLimit: 1, color: '#d97706' },\n { id: 'review', title: 'Preview', order: 2, wipLimit: 0, color: '#7c3aed' },\n { id: 'done', title: 'Done', order: 3, wipLimit: 0, color: '#16a34a' },\n];\n\nconst boardQueue = new Map<string, Promise<void>>();\nconst boardEnsures = new Map<string, Promise<KanbanBoard>>();\nconst bindings = new WeakMap<Context, () => void>();\nconst suppressedTodoMirrors = new WeakSet<Context>();\nconst activeSessionBoards = new Map<string, number>();\n\nfunction boardKey(projectRoot: string, sessionId: string): string {\n return `${projectRoot}\\0${sessionId}`;\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction sessionBoardTitle(sessionId: string): string {\n const leaf = sessionId.split(/[\\\\/]/).filter(Boolean).pop() ?? sessionId;\n return `Session ${leaf.slice(0, 12)}`;\n}\n\nfunction sessionBoardTags(sessionId: string): string[] {\n return ['session', SESSION_BOARD_TAG, sessionTag(sessionId)];\n}\n\nfunction sessionIdFromTags(tags: readonly string[] | undefined): string | null {\n const tag = tags?.find((candidate) => candidate.startsWith('session:'));\n return tag?.slice('session:'.length) || null;\n}\n\nfunction isOwnedSessionBoard(tags: readonly string[] | undefined): boolean {\n return Boolean(tags?.includes(SESSION_BOARD_TAG) && sessionIdFromTags(tags));\n}\n\nfunction retainActiveSessionBoard(projectRoot: string, sessionId: string): void {\n const key = boardKey(projectRoot, sessionId);\n activeSessionBoards.set(key, (activeSessionBoards.get(key) ?? 0) + 1);\n}\n\nfunction releaseActiveSessionBoard(projectRoot: string, sessionId: string): void {\n const key = boardKey(projectRoot, sessionId);\n const remaining = (activeSessionBoards.get(key) ?? 0) - 1;\n if (remaining > 0) activeSessionBoards.set(key, remaining);\n else activeSessionBoards.delete(key);\n}\n\nfunction isSessionBoardActive(projectRoot: string, sessionId: string): boolean {\n return (activeSessionBoards.get(boardKey(projectRoot, sessionId)) ?? 0) > 0;\n}\n\nfunction sameColumns(columns: readonly KanbanColumn[]): boolean {\n return (\n columns.length === SESSION_KANBAN_COLUMNS.length &&\n columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id)\n );\n}\n\n/** Create (or migrate) the single Kanban board owned by a session. */\nexport async function ensureSessionKanbanBoard(\n projectRoot: string | undefined,\n sessionId: string,\n): Promise<KanbanBoard | null> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return null;\n const key = boardKey(projectRoot, sessionId);\n const inFlight = boardEnsures.get(key);\n if (inFlight) return inFlight;\n\n const promise = (async () => {\n const summary = (await listBoards(projectRoot)).find((board) =>\n board.tags?.includes(sessionTag(sessionId)),\n );\n let board = summary ? await getBoard(projectRoot, summary.id) : null;\n if (!board) {\n return createBoard(projectRoot, {\n title: sessionBoardTitle(sessionId),\n description: 'Live session work: todos, tasks, and plan items.',\n tags: sessionBoardTags(sessionId),\n columns: SESSION_KANBAN_COLUMNS,\n generatedBy: `session-kanban:${sessionId}`,\n });\n }\n\n // Boards produced by the older task/plan mirrors used the generic five\n // columns. Migrate only session-owned boards; updateBoard reconciles cards\n // from Backlog into Todo while preserving Running/Preview/Done cards.\n if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {\n board =\n (await updateBoard(projectRoot, board.id, {\n title: sessionBoardTitle(sessionId),\n description: 'Live session work: todos, tasks, and plan items.',\n tags: [...new Set([...(board.tags ?? []), ...sessionBoardTags(sessionId)])],\n columns: SESSION_KANBAN_COLUMNS,\n })) ?? board;\n }\n return board;\n })();\n\n boardEnsures.set(key, promise);\n try {\n return await promise;\n } finally {\n boardEnsures.delete(key);\n }\n}\n\nfunction enqueueBoardWork<T>(\n projectRoot: string,\n sessionId: string,\n work: () => Promise<T>,\n): Promise<T> {\n const key = boardKey(projectRoot, sessionId);\n const previous = boardQueue.get(key) ?? Promise.resolve();\n const result = previous.catch(() => undefined).then(work);\n const tail = result.then(\n () => undefined,\n () => undefined,\n );\n boardQueue.set(key, tail);\n void tail.then(() => {\n if (boardQueue.get(key) === tail) boardQueue.delete(key);\n });\n return result;\n}\n\nasync function removeEmptySessionBoard(\n projectRoot: string,\n boardId: string,\n sessionId: string,\n): Promise<string | null> {\n return enqueueBoardWork(projectRoot, sessionId, async () => {\n if (isSessionBoardActive(projectRoot, sessionId)) return null;\n const board = await getBoard(projectRoot, boardId);\n if (!board || board.tasks.length > 0 || !isOwnedSessionBoard(board.tags)) return null;\n if (sessionIdFromTags(board.tags) !== sessionId) return null;\n return (await removeBoard(projectRoot, board.id)) ? board.id : null;\n });\n}\n\n/** Remove a particular inactive session's system-owned board when it has no cards. */\nexport async function cleanupSessionKanbanBoardIfEmpty(\n projectRoot: string | undefined,\n sessionId: string,\n): Promise<string[]> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return [];\n if (isSessionBoardActive(projectRoot, sessionId)) return [];\n const candidates = (await listBoards(projectRoot)).filter(\n (board) =>\n board.taskCount === 0 &&\n isOwnedSessionBoard(board.tags) &&\n sessionIdFromTags(board.tags) === sessionId,\n );\n const removed = await Promise.all(\n candidates.map((board) => removeEmptySessionBoard(projectRoot, board.id, sessionId)),\n );\n return removed.filter((boardId): boardId is string => Boolean(boardId));\n}\n\n/** Prune stale empty session boards while preserving manual and live boards. */\nexport async function cleanupEmptySessionKanbanBoards(\n projectRoot: string | undefined,\n activeSessionId = '',\n): Promise<string[]> {\n if (!projectRoot || process.env[MIRROR_DISABLED_ENV] === '0') return [];\n const candidates = (await listBoards(projectRoot)).flatMap((board) => {\n const ownerSessionId = sessionIdFromTags(board.tags);\n return board.taskCount === 0 &&\n isOwnedSessionBoard(board.tags) &&\n ownerSessionId &&\n ownerSessionId !== activeSessionId &&\n !isSessionBoardActive(projectRoot, ownerSessionId)\n ? [{ boardId: board.id, sessionId: ownerSessionId }]\n : [];\n });\n const removed = await Promise.all(\n candidates.map(({ boardId, sessionId }) =>\n removeEmptySessionBoard(projectRoot, boardId, sessionId),\n ),\n );\n return removed.filter((boardId): boardId is string => Boolean(boardId));\n}\n\nasync function projectGraph(\n projectRoot: string | undefined,\n sessionId: string,\n graph: SerializedTaskGraph,\n sourceSystem: 'session-todo' | 'session-task' | 'session-plan',\n): Promise<KanbanBoard | null> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return null;\n return enqueueBoardWork(projectRoot, sessionId, async () => {\n const board = await ensureSessionKanbanBoard(projectRoot, sessionId);\n if (!board) return null;\n const result = await syncBoardFromTaskGraph(\n projectRoot,\n board.id,\n deserializeTaskGraph(graph),\n {\n sourceSystem,\n tags: [...new Set([...(board.tags ?? []), ...sessionBoardTags(sessionId)])],\n archiveMissingTasks: true,\n includeCompletedTasks: true,\n },\n );\n return result?.board ?? null;\n });\n}\n\nexport function todoListToSerializedGraph(\n todos: readonly TodoItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const nodes = todos.map((todo, index) => ({\n id: todo.id,\n title: todo.content,\n description: todo.activeForm ?? '',\n type: 'chore' as const,\n priority: 'medium' as const,\n status: todo.status,\n createdAt: index,\n updatedAt: index,\n }));\n return {\n id: `todo:${sessionId}`,\n specId: `todo:${sessionId}`,\n title: 'Session todos',\n nodes,\n edges: [],\n rootNodes: nodes.map((node) => node.id),\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nexport function taskFileToSerializedGraph(\n tasks: readonly TaskItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const ids = new Set(tasks.map((task) => task.id));\n const nodes = tasks.map((task, index) => ({\n id: task.id,\n title: task.title,\n description: task.description ?? '',\n type: task.type,\n priority: task.priority,\n status: task.status,\n ...(task.assignee ? { assignee: task.assignee } : {}),\n ...(task.estimateHours !== undefined ? { estimateHours: task.estimateHours } : {}),\n createdAt: index,\n updatedAt: index,\n }));\n const edges = tasks.flatMap((task) =>\n (task.dependsOn ?? [])\n .filter((dependency) => ids.has(dependency))\n .map((dependency) => ({\n id: `${dependency}->${task.id}`,\n from: dependency,\n to: task.id,\n type: 'depends_on' as const,\n })),\n );\n const hasIncoming = new Set(edges.map((edge) => edge.to));\n const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);\n return {\n // Keep the historical graph id so existing mirrored task cards are reused.\n id: `session:${sessionId}`,\n specId: `session:${sessionId}`,\n title: 'Session tasks',\n nodes,\n edges,\n rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nconst PLAN_STATUS_TO_TASK: Record<PlanItem['status'], TaskStatus> = {\n open: 'pending',\n in_progress: 'in_progress',\n done: 'completed',\n};\n\nexport function planFileToSerializedGraph(\n items: readonly PlanItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const nodes = items.map((item, index) => ({\n id: item.id,\n title: item.title,\n description: item.details ?? '',\n type: 'chore' as const,\n priority: 'medium' as const,\n status: PLAN_STATUS_TO_TASK[item.status],\n createdAt: index,\n updatedAt: index,\n }));\n return {\n id: `plan:${sessionId}`,\n specId: `plan:${sessionId}`,\n title: 'Session plan',\n nodes,\n edges: [],\n rootNodes: nodes.map((node) => node.id),\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nexport function projectSessionTodosToKanban(\n projectRoot: string | undefined,\n todos: readonly TodoItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n todoListToSerializedGraph(todos, sessionId),\n 'session-todo',\n );\n}\n\nexport function projectSessionTasksToKanban(\n projectRoot: string | undefined,\n tasks: readonly TaskItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n taskFileToSerializedGraph(tasks, sessionId),\n 'session-task',\n );\n}\n\nexport function projectSessionPlanToKanban(\n projectRoot: string | undefined,\n items: readonly PlanItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n planFileToSerializedGraph(items, sessionId),\n 'session-plan',\n );\n}\n\nfunction fireAndForget(work: Promise<unknown>): void {\n void work.catch(() => {\n // The session files/state remain recoverable if the observational board\n // cannot be written; the next mutation or file watcher retries the mirror.\n });\n}\n\nexport function mirrorSessionTodosToKanban(\n projectRoot: string | undefined,\n todos: readonly TodoItem[],\n sessionId: string,\n): void {\n fireAndForget(projectSessionTodosToKanban(projectRoot, todos, sessionId));\n}\n\nexport function mirrorSessionTasksToKanban(\n projectRoot: string | undefined,\n tasks: readonly TaskItem[],\n sessionId: string,\n): void {\n fireAndForget(projectSessionTasksToKanban(projectRoot, tasks, sessionId));\n}\n\nexport function mirrorSessionPlanToKanban(\n projectRoot: string | undefined,\n items: readonly PlanItem[],\n sessionId: string,\n): void {\n fireAndForget(projectSessionPlanToKanban(projectRoot, items, sessionId));\n}\n\n/**\n * Bind all live session work paths to Kanban. Todo changes are observed from\n * ConversationState; plan/task sidecars are watched so slash commands, WebUI,\n * plugins, and tools all pass through the same board.\n */\nexport function attachSessionKanbanMirror(context: Context): () => void {\n const existing = bindings.get(context);\n if (existing) return existing;\n\n const attachedProjectRoot = context.projectRoot ?? '';\n let registeredSessionId = '';\n const syncActiveSessionRegistration = () => {\n if (!attachedProjectRoot) return;\n const currentSessionId = context.session?.id ?? '';\n if (currentSessionId === registeredSessionId) return;\n if (registeredSessionId) {\n releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));\n }\n registeredSessionId = currentSessionId;\n if (registeredSessionId) {\n retainActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n }\n };\n syncActiveSessionRegistration();\n\n let watcher: FSWatcher | null = null;\n let watchedDir = '';\n let timer: NodeJS.Timeout | null = null;\n\n const sessionId = () => context.session?.id ?? '';\n const refreshFiles = async () => {\n const id = sessionId();\n if (!id) return;\n const planPath = context.meta['plan.path'];\n if (typeof planPath === 'string' && planPath) {\n const plan = await loadPlan(planPath);\n if (plan) await projectSessionPlanToKanban(context.projectRoot, plan.items, id);\n }\n const taskPath = context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n const tasks = await loadTasks(taskPath);\n if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);\n }\n };\n\n const configureWatcher = () => {\n const planPath = context.meta['plan.path'];\n const taskPath = context.meta['task.path'];\n const candidate =\n typeof planPath === 'string' && planPath\n ? dirname(planPath)\n : typeof taskPath === 'string' && taskPath\n ? dirname(taskPath)\n : '';\n if (!candidate || candidate === watchedDir) return;\n watcher?.close();\n watcher = null;\n watchedDir = candidate;\n try {\n watcher = watch(candidate, { persistent: false }, (_event, filename) => {\n const name = filename?.toString();\n const currentPlanPath = context.meta['plan.path'];\n const currentTaskPath = context.meta['task.path'];\n const planName = typeof currentPlanPath === 'string' ? basename(currentPlanPath) : '';\n const taskName = typeof currentTaskPath === 'string' ? basename(currentTaskPath) : '';\n if (name && name !== planName && name !== taskName) return;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => fireAndForget(refreshFiles()), 60);\n });\n watcher.on('error', () => watcher?.close());\n } catch {\n // The session directory can briefly disappear during project/session\n // switches; the next meta update re-attempts the binding.\n watcher = null;\n watchedDir = '';\n }\n };\n\n const unsubscribe = context.state.onChange((change) => {\n if (change.kind === 'todos_replaced' && !suppressedTodoMirrors.has(context)) {\n // ConversationState auto-clears an all-done tactical list. Project the\n // pre-clear completion snapshot so every card reaches Done atomically.\n mirrorSessionTodosToKanban(\n context.projectRoot,\n change.completedSnapshot ?? change.todos,\n sessionId(),\n );\n return;\n }\n if (change.kind === 'meta_set' && (change.key === 'plan.path' || change.key === 'task.path')) {\n syncActiveSessionRegistration();\n configureWatcher();\n fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));\n fireAndForget(refreshFiles());\n }\n });\n\n configureWatcher();\n\n const detach = () => {\n unsubscribe();\n if (timer) clearTimeout(timer);\n watcher?.close();\n bindings.delete(context);\n if (attachedProjectRoot && registeredSessionId) {\n releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));\n registeredSessionId = '';\n }\n };\n bindings.set(context, detach);\n return detach;\n}\n\n/** Fully hydrate a session board before a host announces the session as ready. */\nexport async function hydrateSessionKanban(context: Context): Promise<KanbanBoard | null> {\n const id = context.session?.id ?? '';\n if (!id) return null;\n await cleanupEmptySessionKanbanBoards(context.projectRoot, id);\n let board = await ensureSessionKanbanBoard(context.projectRoot, id);\n if (context.todos.length) {\n board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);\n }\n const planPath = context.meta['plan.path'];\n if (typeof planPath === 'string' && planPath) {\n const plan = await loadPlan(planPath);\n if (plan) board = await projectSessionPlanToKanban(context.projectRoot, plan.items, id);\n }\n const taskPath = context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n const tasks = await loadTasks(taskPath);\n if (tasks) board = await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);\n }\n return board;\n}\n\nexport interface SessionKanbanSourceUpdate {\n source: 'todo' | 'task' | 'plan' | null;\n todos?: TodoItem[] | undefined;\n tasks?: TaskFile | undefined;\n plan?: PlanFile | undefined;\n}\n\nfunction sourceStatus(task: KanbanTask): TaskStatus {\n if (task.status === 'completed') return 'completed';\n if (task.status === 'in_progress') return 'in_progress';\n if (task.status === 'review') return 'review';\n if (task.status === 'blocked') return 'blocked';\n if (task.status === 'failed') return 'failed';\n return 'pending';\n}\n\n/** Reflect a TUI/WebUI Kanban card edit back to its originating work list. */\nexport async function applySessionKanbanTaskToSource(\n context: Context,\n task: KanbanTask,\n options: { remove?: boolean | undefined } = {},\n): Promise<SessionKanbanSourceUpdate> {\n const originId = task.origin?.taskId;\n const graphId = task.origin?.graphId ?? '';\n if (!originId) return { source: null };\n\n if (task.origin?.system === 'session-todo' || graphId.startsWith('todo:')) {\n const next = options.remove\n ? context.todos.filter((todo) => todo.id !== originId)\n : context.todos.map((todo) =>\n todo.id === originId\n ? {\n ...todo,\n content: task.title,\n status:\n sourceStatus(task) === 'completed'\n ? ('completed' as const)\n : sourceStatus(task) === 'in_progress' || sourceStatus(task) === 'review'\n ? ('in_progress' as const)\n : ('pending' as const),\n }\n : todo,\n );\n suppressedTodoMirrors.add(context);\n try {\n context.state.replaceTodos(next);\n } finally {\n suppressedTodoMirrors.delete(context);\n }\n return { source: 'todo', todos: [...context.todos] };\n }\n\n const id = context.session?.id ?? '';\n if (task.origin?.system === 'session-plan' || graphId.startsWith('plan:')) {\n const planPath = context.meta['plan.path'];\n if (typeof planPath !== 'string' || !planPath) return { source: 'plan' };\n const plan = await mutatePlan(planPath, id, (file) => ({\n ...file,\n updatedAt: new Date().toISOString(),\n items: options.remove\n ? file.items.filter((item) => item.id !== originId)\n : file.items.map((item) =>\n item.id === originId\n ? {\n ...item,\n title: task.title,\n details: task.description,\n status:\n task.status === 'completed'\n ? ('done' as const)\n : task.status === 'in_progress' || task.status === 'review'\n ? ('in_progress' as const)\n : ('open' as const),\n updatedAt: new Date().toISOString(),\n }\n : item,\n ),\n }));\n return { source: 'plan', plan };\n }\n\n if (\n task.origin?.system === 'session-task' ||\n task.origin?.system === 'session' ||\n graphId.startsWith('session:')\n ) {\n const taskPath = context.meta['task.path'];\n if (typeof taskPath !== 'string' || !taskPath) return { source: 'task' };\n const tasks = await mutateTasks(taskPath, id, (file) => ({\n ...file,\n tasks: options.remove\n ? file.tasks.filter((item) => item.id !== originId)\n : file.tasks.map((item) =>\n item.id === originId\n ? {\n ...item,\n title: task.title,\n description: task.description,\n status: sourceStatus(task),\n updatedAt: new Date().toISOString(),\n }\n : item,\n ),\n }));\n return { source: 'task', tasks };\n }\n\n return { source: null };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["loadPlan", "loadTasks", "board", "loadPlan", "loadTasks"]
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tool-icon-map.d.ts","sourceRoot":"","sources":["../src/tool-icon-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;GAGG;AACH,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,KAAK,GACL,KAAK,GACL,MAAM,GACN,MAAM,GACN,MAAM,GACN,SAAS,GACT,UAAU,GACV,UAAU,GACV,MAAM,GACN,MAAM,GACN,MAAM,GACN,MAAM,GACN,OAAO,GACP,MAAM,GACN,MAAM,GACN,MAAM,GACN,UAAU,GACV,OAAO,GACP,UAAU,CAAC;AAEf;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CA+HpD,CAAC;AAEF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAExD;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,UAAU,CAAC;IACjB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE,cAAc,CAyB/D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UAAuB,CAAC"}
1
+ {"version":3,"file":"tool-icon-map.d.ts","sourceRoot":"","sources":["../src/tool-icon-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;GAGG;AACH,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,KAAK,GACL,KAAK,GACL,MAAM,GACN,MAAM,GACN,MAAM,GACN,SAAS,GACT,UAAU,GACV,UAAU,GACV,MAAM,GACN,MAAM,GACN,MAAM,GACN,MAAM,GACN,OAAO,GACP,MAAM,GACN,MAAM,GACN,MAAM,GACN,UAAU,GACV,OAAO,GACP,UAAU,CAAC;AAEf;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAsIpD,CAAC;AAEF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAExD;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,UAAU,CAAC;IACjB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE,cAAc,CAyB/D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UAAuB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"tool-icons.d.ts","sourceRoot":"","sources":["../src/tool-icons.ts"],"names":[],"mappings":"AAYA,6DAA6D;AAC7D,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,KAAK,GACL,KAAK,GACL,MAAM,GACN,MAAM,GACN,MAAM,GACN,SAAS,GACT,UAAU,GACV,UAAU,GACV,MAAM,GACN,MAAM,GACN,MAAM,GACN,MAAM,GACN,OAAO,GACP,MAAM,GACN,MAAM,GACN,MAAM,GACN,UAAU,GACV,OAAO,GACP,UAAU,CAAC;AAEf,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAyBlE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAgFpD,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAGpD"}
1
+ {"version":3,"file":"tool-icons.d.ts","sourceRoot":"","sources":["../src/tool-icons.ts"],"names":[],"mappings":"AAYA,6DAA6D;AAC7D,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,KAAK,GACL,KAAK,GACL,MAAM,GACN,MAAM,GACN,MAAM,GACN,SAAS,GACT,UAAU,GACV,UAAU,GACV,MAAM,GACN,MAAM,GACN,MAAM,GACN,MAAM,GACN,OAAO,GACP,MAAM,GACN,MAAM,GACN,MAAM,GACN,UAAU,GACV,OAAO,GACP,UAAU,CAAC;AAEf,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAyBlE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAsFpD,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAGpD"}
@@ -128,7 +128,13 @@ var TOOL_ICON_MAP = {
128
128
  memory_graph: "brain",
129
129
  memory_verify: "brain",
130
130
  memory_hygiene: "brain",
131
- memory_candidates: "brain"
131
+ memory_candidates: "brain",
132
+ // ── language intelligence ──
133
+ lsp_diagnostics: "code",
134
+ lsp_definition: "search",
135
+ lsp_completion: "code",
136
+ lsp_rename: "edit",
137
+ "codebase-lsp-search": "index"
132
138
  };
133
139
  function getToolIcon(name) {
134
140
  if (!name) return "fallback";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/tool-icons.ts"],
4
- "sourcesContent": ["// Canonical, surface-agnostic per-tool icon identity + color.\n//\n// This is PURE DATA (no node imports) so it is safe to import from the browser\n// (WebUI) as well as the TUI. Each surface maps a `ToolIconId` to its own\n// rendering primitive \u2014 the WebUI to a lucide-react component (`tool-icon.ts`),\n// the TUI to a unicode glyph (`tool-glyph.ts`) \u2014 and pulls the shared color\n// from `TOOL_ICON_CONFIG` so both stay in lockstep.\n//\n// Add a new tool by giving it an entry in `TOOL_ICON_MAP` (name \u2192 id). The id\n// must be one of the `ToolIconId` union members below; every id already has a\n// color, a lucide component, and a glyph.\n\n/** The closed set of visual identities a tool can map to. */\nexport type ToolIconId =\n | 'file'\n | 'edit'\n | 'search'\n | 'folder'\n | 'terminal'\n | 'web'\n | 'git'\n | 'tree'\n | 'code'\n | 'test'\n | 'package'\n | 'document'\n | 'scaffold'\n | 'todo'\n | 'plan'\n | 'task'\n | 'meta'\n | 'index'\n | 'json'\n | 'diff'\n | 'logs'\n | 'settings'\n | 'brain'\n | 'fallback';\n\n/** Canonical hex color per icon id \u2014 the single source both surfaces read. */\nexport const TOOL_ICON_CONFIG: Record<ToolIconId, { color: string }> = {\n file: { color: '#60a5fa' }, // blue\n edit: { color: '#fbbf24' }, // amber\n search: { color: '#a78bfa' }, // violet\n folder: { color: '#38bdf8' }, // sky\n terminal: { color: '#67e8f9' }, // cyan-light\n web: { color: '#34d399' }, // emerald\n git: { color: '#a3e635' }, // lime\n tree: { color: '#22d3ee' }, // cyan\n code: { color: '#818cf8' }, // indigo\n test: { color: '#4ade80' }, // green\n package: { color: '#f472b6' }, // pink\n document: { color: '#94a3b8' }, // slate\n scaffold: { color: '#c084fc' }, // purple\n todo: { color: '#facc15' }, // yellow\n plan: { color: '#2dd4bf' }, // teal\n task: { color: '#5eead4' }, // teal-light\n meta: { color: '#cbd5e1' }, // slate-light\n index: { color: '#06b6d4' }, // cyan-dark\n json: { color: '#eab308' }, // yellow-dark\n diff: { color: '#93c5fd' }, // blue-light\n logs: { color: '#a3a3a3' }, // neutral\n settings: { color: '#9ca3af' }, // gray\n brain: { color: '#e879f9' }, // fuchsia\n fallback: { color: '#9ca3af' }, // gray\n};\n\n/**\n * Tool name (and common alias) \u2192 icon id. Keys are lowercase; `getToolIcon`\n * lowercases the query, so lookups are case-insensitive. Covers every builtin\n * tool plus the aliases models commonly emit.\n */\nexport const TOOL_ICON_MAP: Record<string, ToolIconId> = {\n // \u2500\u2500 file IO \u2500\u2500\n read: 'file',\n cat: 'file',\n view: 'file',\n write: 'file',\n create: 'file',\n edit: 'edit',\n replace: 'edit',\n str_replace: 'edit',\n multi_edit: 'edit',\n patch: 'diff',\n // \u2500\u2500 search \u2500\u2500\n grep: 'search',\n search: 'search',\n rg: 'search',\n ripgrep: 'search',\n glob: 'search',\n find: 'search',\n // \u2500\u2500 navigation \u2500\u2500\n folder: 'folder',\n ls: 'folder',\n list: 'folder',\n set_working_dir: 'folder',\n tree: 'tree',\n // \u2500\u2500 shell \u2500\u2500\n bash: 'terminal',\n shell: 'terminal',\n sh: 'terminal',\n exec: 'terminal',\n run: 'terminal',\n command: 'terminal',\n // \u2500\u2500 web \u2500\u2500\n fetch: 'web',\n web_fetch: 'web',\n web_search: 'web',\n // \u2500\u2500 vcs \u2500\u2500\n git: 'git',\n diff: 'diff',\n // \u2500\u2500 code quality \u2500\u2500\n lint: 'code',\n format: 'settings',\n typecheck: 'code',\n language_info: 'code',\n language: 'code',\n language_package: 'package',\n test: 'test',\n // \u2500\u2500 packages \u2500\u2500\n install: 'package',\n outdated: 'package',\n audit: 'package',\n // \u2500\u2500 docs / scaffold \u2500\u2500\n document: 'document',\n scaffold: 'scaffold',\n // \u2500\u2500 planning / work tracking \u2500\u2500\n todo: 'todo',\n plan: 'plan',\n task: 'task',\n // \u2500\u2500 data \u2500\u2500\n json: 'json',\n index: 'index',\n logs: 'logs',\n // \u2500\u2500 meta / tooling \u2500\u2500\n mode: 'meta',\n tool_search: 'meta',\n tool_use: 'meta',\n batch_tool_use: 'meta',\n tool_help: 'meta',\n // \u2500\u2500 memory \u2500\u2500\n remember: 'brain',\n forget: 'brain',\n search_memory: 'brain',\n find_related_memories: 'brain',\n memory_for_file: 'brain',\n memory_for_path: 'brain',\n memory_search: 'brain',\n memory_graph: 'brain',\n memory_verify: 'brain',\n memory_hygiene: 'brain',\n memory_candidates: 'brain',\n};\n\n/**\n * Resolve a tool name to its canonical `ToolIconId`. Case-insensitive.\n * Unknown names \u2014 including `mcp__*` server tools and plugin tools \u2014 fall back\n * to `'fallback'`.\n */\nexport function getToolIcon(name: string): ToolIconId {\n if (!name) return 'fallback';\n return TOOL_ICON_MAP[name.toLowerCase()] ?? 'fallback';\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// Canonical, surface-agnostic per-tool icon identity + color.\n//\n// This is PURE DATA (no node imports) so it is safe to import from the browser\n// (WebUI) as well as the TUI. Each surface maps a `ToolIconId` to its own\n// rendering primitive \u2014 the WebUI to a lucide-react component (`tool-icon.ts`),\n// the TUI to a unicode glyph (`tool-glyph.ts`) \u2014 and pulls the shared color\n// from `TOOL_ICON_CONFIG` so both stay in lockstep.\n//\n// Add a new tool by giving it an entry in `TOOL_ICON_MAP` (name \u2192 id). The id\n// must be one of the `ToolIconId` union members below; every id already has a\n// color, a lucide component, and a glyph.\n\n/** The closed set of visual identities a tool can map to. */\nexport type ToolIconId =\n | 'file'\n | 'edit'\n | 'search'\n | 'folder'\n | 'terminal'\n | 'web'\n | 'git'\n | 'tree'\n | 'code'\n | 'test'\n | 'package'\n | 'document'\n | 'scaffold'\n | 'todo'\n | 'plan'\n | 'task'\n | 'meta'\n | 'index'\n | 'json'\n | 'diff'\n | 'logs'\n | 'settings'\n | 'brain'\n | 'fallback';\n\n/** Canonical hex color per icon id \u2014 the single source both surfaces read. */\nexport const TOOL_ICON_CONFIG: Record<ToolIconId, { color: string }> = {\n file: { color: '#60a5fa' }, // blue\n edit: { color: '#fbbf24' }, // amber\n search: { color: '#a78bfa' }, // violet\n folder: { color: '#38bdf8' }, // sky\n terminal: { color: '#67e8f9' }, // cyan-light\n web: { color: '#34d399' }, // emerald\n git: { color: '#a3e635' }, // lime\n tree: { color: '#22d3ee' }, // cyan\n code: { color: '#818cf8' }, // indigo\n test: { color: '#4ade80' }, // green\n package: { color: '#f472b6' }, // pink\n document: { color: '#94a3b8' }, // slate\n scaffold: { color: '#c084fc' }, // purple\n todo: { color: '#facc15' }, // yellow\n plan: { color: '#2dd4bf' }, // teal\n task: { color: '#5eead4' }, // teal-light\n meta: { color: '#cbd5e1' }, // slate-light\n index: { color: '#06b6d4' }, // cyan-dark\n json: { color: '#eab308' }, // yellow-dark\n diff: { color: '#93c5fd' }, // blue-light\n logs: { color: '#a3a3a3' }, // neutral\n settings: { color: '#9ca3af' }, // gray\n brain: { color: '#e879f9' }, // fuchsia\n fallback: { color: '#9ca3af' }, // gray\n};\n\n/**\n * Tool name (and common alias) \u2192 icon id. Keys are lowercase; `getToolIcon`\n * lowercases the query, so lookups are case-insensitive. Covers every builtin\n * tool plus the aliases models commonly emit.\n */\nexport const TOOL_ICON_MAP: Record<string, ToolIconId> = {\n // \u2500\u2500 file IO \u2500\u2500\n read: 'file',\n cat: 'file',\n view: 'file',\n write: 'file',\n create: 'file',\n edit: 'edit',\n replace: 'edit',\n str_replace: 'edit',\n multi_edit: 'edit',\n patch: 'diff',\n // \u2500\u2500 search \u2500\u2500\n grep: 'search',\n search: 'search',\n rg: 'search',\n ripgrep: 'search',\n glob: 'search',\n find: 'search',\n // \u2500\u2500 navigation \u2500\u2500\n folder: 'folder',\n ls: 'folder',\n list: 'folder',\n set_working_dir: 'folder',\n tree: 'tree',\n // \u2500\u2500 shell \u2500\u2500\n bash: 'terminal',\n shell: 'terminal',\n sh: 'terminal',\n exec: 'terminal',\n run: 'terminal',\n command: 'terminal',\n // \u2500\u2500 web \u2500\u2500\n fetch: 'web',\n web_fetch: 'web',\n web_search: 'web',\n // \u2500\u2500 vcs \u2500\u2500\n git: 'git',\n diff: 'diff',\n // \u2500\u2500 code quality \u2500\u2500\n lint: 'code',\n format: 'settings',\n typecheck: 'code',\n language_info: 'code',\n language: 'code',\n language_package: 'package',\n test: 'test',\n // \u2500\u2500 packages \u2500\u2500\n install: 'package',\n outdated: 'package',\n audit: 'package',\n // \u2500\u2500 docs / scaffold \u2500\u2500\n document: 'document',\n scaffold: 'scaffold',\n // \u2500\u2500 planning / work tracking \u2500\u2500\n todo: 'todo',\n plan: 'plan',\n task: 'task',\n // \u2500\u2500 data \u2500\u2500\n json: 'json',\n index: 'index',\n logs: 'logs',\n // \u2500\u2500 meta / tooling \u2500\u2500\n mode: 'meta',\n tool_search: 'meta',\n tool_use: 'meta',\n batch_tool_use: 'meta',\n tool_help: 'meta',\n // \u2500\u2500 memory \u2500\u2500\n remember: 'brain',\n forget: 'brain',\n search_memory: 'brain',\n find_related_memories: 'brain',\n memory_for_file: 'brain',\n memory_for_path: 'brain',\n memory_search: 'brain',\n memory_graph: 'brain',\n memory_verify: 'brain',\n memory_hygiene: 'brain',\n memory_candidates: 'brain',\n // \u2500\u2500 language intelligence \u2500\u2500\n lsp_diagnostics: 'code',\n lsp_definition: 'search',\n lsp_completion: 'code',\n lsp_rename: 'edit',\n 'codebase-lsp-search': 'index',\n};\n\n/**\n * Resolve a tool name to its canonical `ToolIconId`. Case-insensitive.\n * Unknown names \u2014 including `mcp__*` server tools and plugin tools \u2014 fall back\n * to `'fallback'`.\n */\nexport function getToolIcon(name: string): ToolIconId {\n if (!name) return 'fallback';\n return TOOL_ICON_MAP[name.toLowerCase()] ?? 'fallback';\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/tools",
3
- "version": "0.286.0",
3
+ "version": "0.287.0",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack built-in tools: read/write/edit, bash/exec, grep/glob, git, fetch, test, lint, and more.",
6
6
  "repository": {
@@ -204,6 +204,10 @@
204
204
  "types": "./dist/task.d.ts",
205
205
  "import": "./dist/task.js"
206
206
  },
207
+ "./session-kanban": {
208
+ "types": "./dist/session-kanban.d.ts",
209
+ "import": "./dist/session-kanban.js"
210
+ },
207
211
  "./process-registry": {
208
212
  "types": "./dist/process-registry.d.ts",
209
213
  "import": "./dist/process-registry.js"
@@ -229,8 +233,8 @@
229
233
  "@typescript/typescript6": "^6.0.2",
230
234
  "turndown": "^7.2.4",
231
235
  "undici": "^8.5.0",
232
- "@wrongstack/core": "0.286.0",
233
- "@wrongstack/kanban": "0.286.0"
236
+ "@wrongstack/core": "0.287.0",
237
+ "@wrongstack/kanban": "0.287.0"
234
238
  },
235
239
  "devDependencies": {
236
240
  "@types/node": "^26.0.1",