cognova 0.1.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 (205) hide show
  1. package/.env.example +58 -0
  2. package/Claude/CLAUDE.md +92 -0
  3. package/Claude/hooks/lib/__init__.py +1 -0
  4. package/Claude/hooks/lib/hook_client.py +207 -0
  5. package/Claude/hooks/log-event.py +97 -0
  6. package/Claude/hooks/pre-compact.py +46 -0
  7. package/Claude/hooks/session-end.py +26 -0
  8. package/Claude/hooks/session-start.py +35 -0
  9. package/Claude/hooks/stop-extract.py +40 -0
  10. package/Claude/rules/frontmatter.md +54 -0
  11. package/Claude/rules/markdown.md +43 -0
  12. package/Claude/rules/note-organization.md +33 -0
  13. package/Claude/settings.json +54 -0
  14. package/Claude/skills/README.md +136 -0
  15. package/Claude/skills/_lib/__init__.py +1 -0
  16. package/Claude/skills/_lib/api.py +164 -0
  17. package/Claude/skills/_lib/output.py +95 -0
  18. package/Claude/skills/environment/SKILL.md +73 -0
  19. package/Claude/skills/environment/environment.py +239 -0
  20. package/Claude/skills/memory/SKILL.md +153 -0
  21. package/Claude/skills/memory/memory.py +270 -0
  22. package/Claude/skills/project/SKILL.md +105 -0
  23. package/Claude/skills/project/project.py +203 -0
  24. package/Claude/skills/skill-creator/SKILL.md +261 -0
  25. package/Claude/skills/task/SKILL.md +135 -0
  26. package/Claude/skills/task/task.py +310 -0
  27. package/LICENSE +21 -0
  28. package/README.md +176 -0
  29. package/app/app.config.ts +8 -0
  30. package/app/app.vue +39 -0
  31. package/app/assets/css/main.css +10 -0
  32. package/app/components/AppLogo.vue +40 -0
  33. package/app/components/AssistantPanel.client.vue +518 -0
  34. package/app/components/ConfirmModal.vue +84 -0
  35. package/app/components/TemplateMenu.vue +49 -0
  36. package/app/components/agents/AgentActivityChart.client.vue +105 -0
  37. package/app/components/agents/AgentActivityChart.server.vue +25 -0
  38. package/app/components/agents/AgentForm.vue +304 -0
  39. package/app/components/agents/AgentRunModal.vue +154 -0
  40. package/app/components/agents/AgentStatsCards.vue +98 -0
  41. package/app/components/chat/ChatInput.vue +85 -0
  42. package/app/components/chat/ConversationList.vue +78 -0
  43. package/app/components/chat/MessageBubble.vue +81 -0
  44. package/app/components/chat/StreamingMessage.vue +36 -0
  45. package/app/components/chat/ToolCallBlock.vue +77 -0
  46. package/app/components/editor/CodeEditor.client.vue +212 -0
  47. package/app/components/editor/CodeEditorFallback.vue +12 -0
  48. package/app/components/editor/DocumentEditor.vue +326 -0
  49. package/app/components/editor/DocumentMetadata.vue +140 -0
  50. package/app/components/editor/MarkdownEditor.vue +146 -0
  51. package/app/components/files/FileTree.vue +436 -0
  52. package/app/components/hooks/HookActivityChart.client.vue +117 -0
  53. package/app/components/hooks/HookActivityChart.server.vue +25 -0
  54. package/app/components/hooks/HookStatsCards.vue +63 -0
  55. package/app/components/hooks/RecentEventsTable.vue +123 -0
  56. package/app/components/hooks/ToolBreakdownTable.vue +72 -0
  57. package/app/components/search/DashboardSearch.vue +122 -0
  58. package/app/components/tasks/ProjectSelect.vue +35 -0
  59. package/app/components/tasks/TaskCard.vue +182 -0
  60. package/app/components/tasks/TaskDetail.vue +160 -0
  61. package/app/components/tasks/TaskForm.vue +280 -0
  62. package/app/components/tasks/TaskList.vue +69 -0
  63. package/app/components/view/ViewToc.vue +85 -0
  64. package/app/composables/useAgents.ts +153 -0
  65. package/app/composables/useAuth.ts +73 -0
  66. package/app/composables/useChat.ts +298 -0
  67. package/app/composables/useDocument.ts +141 -0
  68. package/app/composables/useEditor.ts +100 -0
  69. package/app/composables/useFileTree.ts +220 -0
  70. package/app/composables/useHookEvents.ts +68 -0
  71. package/app/composables/useMemories.ts +83 -0
  72. package/app/composables/useNotificationBus.ts +154 -0
  73. package/app/composables/usePreferences.ts +131 -0
  74. package/app/composables/useProjects.ts +97 -0
  75. package/app/composables/useSearch.ts +52 -0
  76. package/app/composables/useTasks.ts +201 -0
  77. package/app/composables/useTerminal.ts +135 -0
  78. package/app/layouts/auth.vue +20 -0
  79. package/app/layouts/dashboard.vue +186 -0
  80. package/app/layouts/view.vue +60 -0
  81. package/app/middleware/auth.ts +9 -0
  82. package/app/pages/agents/[id].vue +602 -0
  83. package/app/pages/agents/index.vue +412 -0
  84. package/app/pages/chat.vue +146 -0
  85. package/app/pages/dashboard.vue +80 -0
  86. package/app/pages/docs.vue +131 -0
  87. package/app/pages/hooks.vue +163 -0
  88. package/app/pages/index.vue +249 -0
  89. package/app/pages/login.vue +60 -0
  90. package/app/pages/memories.vue +282 -0
  91. package/app/pages/settings.vue +625 -0
  92. package/app/pages/tasks.vue +312 -0
  93. package/app/pages/view/[uuid].vue +376 -0
  94. package/dist/cli/index.js +2711 -0
  95. package/drizzle.config.ts +10 -0
  96. package/nuxt.config.ts +98 -0
  97. package/package.json +107 -0
  98. package/server/api/agents/[id]/cancel.post.ts +27 -0
  99. package/server/api/agents/[id]/run.post.ts +34 -0
  100. package/server/api/agents/[id]/runs.get.ts +45 -0
  101. package/server/api/agents/[id]/stats.get.ts +94 -0
  102. package/server/api/agents/[id].delete.ts +29 -0
  103. package/server/api/agents/[id].get.ts +25 -0
  104. package/server/api/agents/[id].patch.ts +55 -0
  105. package/server/api/agents/index.get.ts +15 -0
  106. package/server/api/agents/index.post.ts +48 -0
  107. package/server/api/agents/stats.get.ts +86 -0
  108. package/server/api/auth/[...all].ts +5 -0
  109. package/server/api/conversations/[id].delete.ts +16 -0
  110. package/server/api/conversations/[id].get.ts +34 -0
  111. package/server/api/conversations/index.get.ts +17 -0
  112. package/server/api/documents/[id]/index.delete.ts +47 -0
  113. package/server/api/documents/[id]/index.put.ts +102 -0
  114. package/server/api/documents/[id]/public.get.ts +60 -0
  115. package/server/api/documents/[id]/restore.post.ts +65 -0
  116. package/server/api/documents/by-path.post.ts +168 -0
  117. package/server/api/documents/index.get.ts +48 -0
  118. package/server/api/fs/delete.post.ts +41 -0
  119. package/server/api/fs/list.get.ts +99 -0
  120. package/server/api/fs/mkdir.post.ts +44 -0
  121. package/server/api/fs/move.post.ts +68 -0
  122. package/server/api/fs/read.post.ts +48 -0
  123. package/server/api/fs/rename.post.ts +55 -0
  124. package/server/api/fs/write.post.ts +51 -0
  125. package/server/api/health.get.ts +40 -0
  126. package/server/api/home.get.ts +26 -0
  127. package/server/api/hooks/events/index.get.ts +56 -0
  128. package/server/api/hooks/events/index.post.ts +36 -0
  129. package/server/api/hooks/stats.get.ts +99 -0
  130. package/server/api/memory/[id].delete.ts +26 -0
  131. package/server/api/memory/context.get.ts +83 -0
  132. package/server/api/memory/extract.post.ts +42 -0
  133. package/server/api/memory/search.get.ts +70 -0
  134. package/server/api/memory/store.post.ts +31 -0
  135. package/server/api/projects/[id]/index.delete.ts +40 -0
  136. package/server/api/projects/[id]/index.get.ts +25 -0
  137. package/server/api/projects/[id]/index.put.ts +50 -0
  138. package/server/api/projects/index.get.ts +20 -0
  139. package/server/api/projects/index.post.ts +34 -0
  140. package/server/api/secrets/[key].delete.ts +31 -0
  141. package/server/api/secrets/[key].get.ts +30 -0
  142. package/server/api/secrets/[key].put.ts +52 -0
  143. package/server/api/secrets/index.get.ts +20 -0
  144. package/server/api/secrets/index.post.ts +58 -0
  145. package/server/api/tasks/[id]/index.delete.ts +46 -0
  146. package/server/api/tasks/[id]/index.get.ts +24 -0
  147. package/server/api/tasks/[id]/index.put.ts +70 -0
  148. package/server/api/tasks/[id]/restore.post.ts +49 -0
  149. package/server/api/tasks/index.get.ts +53 -0
  150. package/server/api/tasks/index.post.ts +47 -0
  151. package/server/api/tasks/tags.get.ts +21 -0
  152. package/server/api/user/email.patch.ts +56 -0
  153. package/server/db/index.ts +76 -0
  154. package/server/db/migrate.ts +41 -0
  155. package/server/db/schema.ts +345 -0
  156. package/server/db/seed.ts +46 -0
  157. package/server/db/types.ts +28 -0
  158. package/server/drizzle/migrations/0000_brown_george_stacy.sql +34 -0
  159. package/server/drizzle/migrations/0001_stormy_pyro.sql +16 -0
  160. package/server/drizzle/migrations/0002_clean_colossus.sql +50 -0
  161. package/server/drizzle/migrations/0003_fine_joystick.sql +12 -0
  162. package/server/drizzle/migrations/0004_tan_groot.sql +26 -0
  163. package/server/drizzle/migrations/0005_cloudy_lilith.sql +33 -0
  164. package/server/drizzle/migrations/0006_ordinary_retro_girl.sql +13 -0
  165. package/server/drizzle/migrations/0007_flowery_venus.sql +15 -0
  166. package/server/drizzle/migrations/0008_talented_zombie.sql +13 -0
  167. package/server/drizzle/migrations/0009_gray_shen.sql +15 -0
  168. package/server/drizzle/migrations/meta/0000_snapshot.json +230 -0
  169. package/server/drizzle/migrations/meta/0001_snapshot.json +306 -0
  170. package/server/drizzle/migrations/meta/0002_snapshot.json +615 -0
  171. package/server/drizzle/migrations/meta/0003_snapshot.json +730 -0
  172. package/server/drizzle/migrations/meta/0004_snapshot.json +916 -0
  173. package/server/drizzle/migrations/meta/0005_snapshot.json +1127 -0
  174. package/server/drizzle/migrations/meta/0006_snapshot.json +1213 -0
  175. package/server/drizzle/migrations/meta/0007_snapshot.json +1307 -0
  176. package/server/drizzle/migrations/meta/0008_snapshot.json +1390 -0
  177. package/server/drizzle/migrations/meta/0009_snapshot.json +1487 -0
  178. package/server/drizzle/migrations/meta/_journal.json +76 -0
  179. package/server/middleware/auth.ts +79 -0
  180. package/server/plugins/00.env-validate.ts +38 -0
  181. package/server/plugins/01.api-token.ts +31 -0
  182. package/server/plugins/02.database.ts +54 -0
  183. package/server/plugins/03.file-watcher.ts +65 -0
  184. package/server/plugins/04.cron-agents.ts +26 -0
  185. package/server/routes/_ws/chat.ts +252 -0
  186. package/server/routes/notifications.ts +47 -0
  187. package/server/routes/terminal.ts +98 -0
  188. package/server/services/agent-executor.ts +218 -0
  189. package/server/services/cron-scheduler.ts +78 -0
  190. package/server/services/memory-extractor.ts +120 -0
  191. package/server/utils/agent-cleanup.ts +91 -0
  192. package/server/utils/agent-registry.ts +95 -0
  193. package/server/utils/auth.ts +33 -0
  194. package/server/utils/chat-session-manager.ts +59 -0
  195. package/server/utils/crypto.ts +40 -0
  196. package/server/utils/db-guard.ts +12 -0
  197. package/server/utils/db-state.ts +63 -0
  198. package/server/utils/document-sync.ts +207 -0
  199. package/server/utils/frontmatter.ts +84 -0
  200. package/server/utils/notification-bus.ts +60 -0
  201. package/server/utils/path-validator.ts +55 -0
  202. package/server/utils/pty-manager.ts +130 -0
  203. package/shared/types/index.ts +604 -0
  204. package/shared/utils/language-detection.ts +87 -0
  205. package/tsconfig.json +10 -0
@@ -0,0 +1,85 @@
1
+ <script setup lang="ts">
2
+ import type { ChatSessionStatus, ChatConnectionStatus } from '~~/shared/types'
3
+
4
+ const props = defineProps<{
5
+ sessionStatus: ChatSessionStatus
6
+ connectionStatus: ChatConnectionStatus
7
+ }>()
8
+
9
+ const emit = defineEmits<{
10
+ send: [message: string]
11
+ interrupt: []
12
+ }>()
13
+
14
+ const inputText = ref('')
15
+ const textareaRef = ref<HTMLTextAreaElement | null>(null)
16
+
17
+ const isStreaming = computed(() => props.sessionStatus === 'streaming')
18
+ const isConnected = computed(() => props.connectionStatus === 'connected')
19
+ const canSend = computed(() => isConnected.value && !isStreaming.value && inputText.value.trim().length > 0)
20
+
21
+ function handleKeydown(e: KeyboardEvent) {
22
+ if (e.key === 'Enter' && !e.shiftKey) {
23
+ e.preventDefault()
24
+ handleSend()
25
+ }
26
+ }
27
+
28
+ function handleSend() {
29
+ if (!canSend.value) return
30
+ emit('send', inputText.value.trim())
31
+ inputText.value = ''
32
+ nextTick(() => {
33
+ if (textareaRef.value) textareaRef.value.style.height = 'auto'
34
+ })
35
+ }
36
+
37
+ function autoResize(e: Event) {
38
+ const target = e.target as HTMLTextAreaElement
39
+ target.style.height = 'auto'
40
+ target.style.height = Math.min(target.scrollHeight, 200) + 'px'
41
+ }
42
+ </script>
43
+
44
+ <template>
45
+ <div class="border-t border-default p-4">
46
+ <!-- Connection status -->
47
+ <div
48
+ v-if="!isConnected"
49
+ class="flex items-center gap-2 mb-2 text-xs text-dimmed"
50
+ >
51
+ <span class="size-2 rounded-full bg-warning" />
52
+ <span>{{ connectionStatus === 'connecting' ? 'Connecting...' : 'Disconnected' }}</span>
53
+ </div>
54
+
55
+ <div class="flex items-start gap-2">
56
+ <textarea
57
+ ref="textareaRef"
58
+ v-model="inputText"
59
+ :disabled="!isConnected || isStreaming"
60
+ placeholder="Send a message..."
61
+ rows="1"
62
+ class="flex-1 resize-none bg-elevated/50 border border-default rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-50"
63
+ @keydown="handleKeydown"
64
+ @input="autoResize"
65
+ />
66
+
67
+ <UButton
68
+ v-if="isStreaming"
69
+ icon="i-lucide-square"
70
+ color="error"
71
+ variant="soft"
72
+ size="md"
73
+ @click="emit('interrupt')"
74
+ />
75
+ <UButton
76
+ v-else
77
+ icon="i-lucide-send"
78
+ color="primary"
79
+ :disabled="!canSend"
80
+ size="md"
81
+ @click="handleSend"
82
+ />
83
+ </div>
84
+ </div>
85
+ </template>
@@ -0,0 +1,78 @@
1
+ <script setup lang="ts">
2
+ import type { ChatConversation } from '~~/shared/types'
3
+
4
+ defineProps<{
5
+ conversations: ChatConversation[]
6
+ activeId: string | null
7
+ }>()
8
+
9
+ const emit = defineEmits<{
10
+ select: [id: string]
11
+ delete: [id: string]
12
+ new: []
13
+ }>()
14
+
15
+ function formatDate(date: Date | string) {
16
+ const d = new Date(date)
17
+ const now = new Date()
18
+ const diff = now.getTime() - d.getTime()
19
+
20
+ if (diff < 60000) return 'just now'
21
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`
22
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`
23
+ return d.toLocaleDateString()
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <div class="flex flex-col h-full">
29
+ <!-- Header -->
30
+ <div class="p-3 border-b border-default">
31
+ <UButton
32
+ label="New Chat"
33
+ icon="i-lucide-plus"
34
+ color="primary"
35
+ variant="soft"
36
+ block
37
+ @click="emit('new')"
38
+ />
39
+ </div>
40
+
41
+ <!-- Conversation list -->
42
+ <div class="flex-1 overflow-y-auto">
43
+ <div
44
+ v-if="conversations.length === 0"
45
+ class="p-4 text-sm text-dimmed text-center"
46
+ >
47
+ No conversations yet
48
+ </div>
49
+
50
+ <button
51
+ v-for="conv in conversations"
52
+ :key="conv.id"
53
+ class="w-full p-3 text-left border-b border-default hover:bg-elevated/50 transition-colors group"
54
+ :class="{ 'bg-elevated/50': activeId === conv.id }"
55
+ @click="emit('select', conv.id)"
56
+ >
57
+ <div class="flex items-start justify-between gap-2">
58
+ <div class="truncate text-sm font-medium">
59
+ {{ conv.title || 'Untitled' }}
60
+ </div>
61
+ <UButton
62
+ icon="i-lucide-trash-2"
63
+ variant="ghost"
64
+ color="error"
65
+ size="xs"
66
+ class="opacity-0 group-hover:opacity-100 shrink-0"
67
+ @click.stop="emit('delete', conv.id)"
68
+ />
69
+ </div>
70
+ <div class="flex items-center gap-2 mt-1 text-xs text-dimmed">
71
+ <span>{{ formatDate(conv.startedAt) }}</span>
72
+ <span v-if="conv.messageCount">{{ conv.messageCount }} msgs</span>
73
+ <span v-if="conv.totalCostUsd > 0">${{ conv.totalCostUsd.toFixed(4) }}</span>
74
+ </div>
75
+ </button>
76
+ </div>
77
+ </div>
78
+ </template>
@@ -0,0 +1,81 @@
1
+ <script setup lang="ts">
2
+ import type { ChatMessage, ChatContentBlock } from '~~/shared/types'
3
+
4
+ defineProps<{
5
+ message: ChatMessage
6
+ }>()
7
+
8
+ function getTextContent(blocks: ChatContentBlock[]): string {
9
+ return blocks
10
+ .filter(b => b.type === 'text')
11
+ .map(b => b.text)
12
+ .join('\n')
13
+ }
14
+
15
+ function getToolPairs(blocks: ChatContentBlock[]) {
16
+ const tools: { name: string, id: string, result?: string, isError?: boolean }[] = []
17
+ for (const block of blocks) {
18
+ if (block.type === 'tool_use')
19
+ tools.push({ name: block.name, id: block.id })
20
+ else if (block.type === 'tool_result') {
21
+ const tool = tools.find(t => t.id === block.tool_use_id)
22
+ if (tool) {
23
+ tool.result = block.content
24
+ tool.isError = block.is_error
25
+ }
26
+ }
27
+ }
28
+ return tools
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <div
34
+ class="flex"
35
+ :class="message.role === 'user' ? 'justify-end' : 'justify-start'"
36
+ >
37
+ <div
38
+ class="max-w-[85%] rounded-xl px-4 py-3"
39
+ :class="message.role === 'user'
40
+ ? 'bg-primary/5 text-highlighted'
41
+ : 'bg-muted'"
42
+ >
43
+ <!-- User message -->
44
+ <div
45
+ v-if="message.role === 'user'"
46
+ class="text-sm whitespace-pre-wrap"
47
+ >
48
+ {{ getTextContent(message.content) }}
49
+ </div>
50
+
51
+ <!-- Assistant message -->
52
+ <template v-else>
53
+ <!-- Text content rendered as markdown -->
54
+ <div
55
+ v-if="getTextContent(message.content)"
56
+ class="chat-prose prose prose-sm dark:prose-invert max-w-none"
57
+ >
58
+ <MDC :value="getTextContent(message.content)" />
59
+ </div>
60
+
61
+ <!-- Tool calls -->
62
+ <ChatToolCallBlock
63
+ v-for="tool in getToolPairs(message.content)"
64
+ :key="tool.id"
65
+ :tool-name="tool.name"
66
+ :result="tool.result"
67
+ :is-error="tool.isError"
68
+ />
69
+
70
+ <!-- Cost/duration metadata -->
71
+ <div
72
+ v-if="message.costUsd || message.durationMs"
73
+ class="flex items-center gap-3 mt-2 text-xs text-dimmed"
74
+ >
75
+ <span v-if="message.costUsd">${{ message.costUsd.toFixed(4) }}</span>
76
+ <span v-if="message.durationMs">{{ (message.durationMs / 1000).toFixed(1) }}s</span>
77
+ </div>
78
+ </template>
79
+ </div>
80
+ </div>
81
+ </template>
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ text: string
4
+ toolCalls: Record<string, { name: string, result?: string, isError?: boolean }>
5
+ }>()
6
+ </script>
7
+
8
+ <template>
9
+ <div class="flex justify-start">
10
+ <div class="max-w-[85%] rounded-xl px-4 py-3 bg-elevated/50">
11
+ <!-- Streaming text rendered as markdown -->
12
+ <div
13
+ v-if="text"
14
+ class="chat-prose prose prose-sm dark:prose-invert max-w-none"
15
+ >
16
+ <MDC :value="text" />
17
+ </div>
18
+
19
+ <!-- Active tool calls -->
20
+ <ChatToolCallBlock
21
+ v-for="(tool, id) in toolCalls"
22
+ :key="id"
23
+ :tool-name="tool.name"
24
+ :result="tool.result"
25
+ :is-error="tool.isError"
26
+ :pending="!tool.result"
27
+ />
28
+
29
+ <!-- Streaming indicator -->
30
+ <div class="flex items-center gap-1.5 mt-2">
31
+ <span class="size-2 rounded-full bg-primary animate-pulse" />
32
+ <span class="text-xs text-dimmed">Thinking...</span>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </template>
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ toolName: string
4
+ result?: string
5
+ isError?: boolean
6
+ pending?: boolean
7
+ }>()
8
+
9
+ const open = ref(false)
10
+
11
+ const iconMap: Record<string, string> = {
12
+ Read: 'i-lucide-file-text',
13
+ Edit: 'i-lucide-pencil',
14
+ Write: 'i-lucide-file-plus',
15
+ Bash: 'i-lucide-terminal',
16
+ Glob: 'i-lucide-search',
17
+ Grep: 'i-lucide-text-search',
18
+ Task: 'i-lucide-git-branch',
19
+ WebFetch: 'i-lucide-globe',
20
+ WebSearch: 'i-lucide-search'
21
+ }
22
+
23
+ const icon = computed(() => iconMap[props.toolName] || 'i-lucide-wrench')
24
+
25
+ const truncatedResult = computed(() => {
26
+ if (!props.result) return ''
27
+ return props.result.length > 500 ? props.result.slice(0, 500) + '...' : props.result
28
+ })
29
+ </script>
30
+
31
+ <template>
32
+ <div class="border border-default rounded-lg my-2 overflow-hidden">
33
+ <button
34
+ class="flex items-center gap-2 w-full px-3 py-2 text-sm text-muted hover:bg-elevated/50 transition-colors"
35
+ @click="open = !open"
36
+ >
37
+ <UIcon
38
+ v-if="pending"
39
+ name="i-lucide-loader-2"
40
+ class="size-4 animate-spin text-primary"
41
+ />
42
+ <UIcon
43
+ v-else
44
+ :name="icon"
45
+ class="size-4"
46
+ />
47
+ <span class="font-mono text-xs">{{ toolName }}</span>
48
+ <UBadge
49
+ v-if="isError"
50
+ color="error"
51
+ variant="subtle"
52
+ size="xs"
53
+ >
54
+ error
55
+ </UBadge>
56
+ <UBadge
57
+ v-else-if="pending"
58
+ color="warning"
59
+ variant="subtle"
60
+ size="xs"
61
+ >
62
+ running
63
+ </UBadge>
64
+ <UIcon
65
+ :name="open ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
66
+ class="size-4 ms-auto"
67
+ />
68
+ </button>
69
+ <div
70
+ v-if="open && result"
71
+ class="border-t border-default px-3 py-2 text-xs font-mono bg-elevated/25 max-h-64 overflow-auto whitespace-pre-wrap"
72
+ :class="{ 'text-error': isError }"
73
+ >
74
+ {{ truncatedResult }}
75
+ </div>
76
+ </div>
77
+ </template>
@@ -0,0 +1,212 @@
1
+ <script setup lang="ts">
2
+ import { EditorView, basicSetup } from 'codemirror'
3
+ import { EditorState, Compartment } from '@codemirror/state'
4
+ import { oneDark } from '@codemirror/theme-one-dark'
5
+ import { markdown } from '@codemirror/lang-markdown'
6
+ import { javascript } from '@codemirror/lang-javascript'
7
+ import { json } from '@codemirror/lang-json'
8
+ import { html } from '@codemirror/lang-html'
9
+ import { css } from '@codemirror/lang-css'
10
+ import { vue } from '@codemirror/lang-vue'
11
+ import { python } from '@codemirror/lang-python'
12
+ import { sql } from '@codemirror/lang-sql'
13
+ import { yaml } from '@codemirror/lang-yaml'
14
+ import { rust } from '@codemirror/lang-rust'
15
+ import { java } from '@codemirror/lang-java'
16
+ import { cpp } from '@codemirror/lang-cpp'
17
+ import { xml } from '@codemirror/lang-xml'
18
+ import { StreamLanguage } from '@codemirror/language'
19
+ import { go } from '@codemirror/legacy-modes/mode/go'
20
+ import { shell } from '@codemirror/legacy-modes/mode/shell'
21
+ import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile'
22
+ import type { Extension } from '@codemirror/state'
23
+ import type { CodeLanguage } from '~~/shared/types'
24
+
25
+ const props = withDefaults(defineProps<{
26
+ modelValue: string
27
+ language?: CodeLanguage
28
+ readOnly?: boolean
29
+ placeholder?: string
30
+ }>(), {
31
+ language: 'plaintext',
32
+ readOnly: false,
33
+ placeholder: ''
34
+ })
35
+
36
+ const emit = defineEmits<{
37
+ 'update:modelValue': [value: string]
38
+ }>()
39
+
40
+ const editorRef = ref<HTMLDivElement>()
41
+ let view: EditorView | null = null
42
+
43
+ const languageCompartment = new Compartment()
44
+ const readOnlyCompartment = new Compartment()
45
+ const editableCompartment = new Compartment()
46
+ const themeCompartment = new Compartment()
47
+
48
+ const colorMode = useColorMode()
49
+
50
+ function getLanguageExtension(lang: CodeLanguage): Extension {
51
+ switch (lang) {
52
+ case 'markdown':
53
+ return markdown()
54
+ case 'javascript':
55
+ return javascript()
56
+ case 'typescript':
57
+ return javascript({ typescript: true })
58
+ case 'json':
59
+ return json()
60
+ case 'html':
61
+ return html()
62
+ case 'css':
63
+ return css()
64
+ case 'vue':
65
+ return vue()
66
+ case 'python':
67
+ return python()
68
+ case 'sql':
69
+ return sql()
70
+ case 'yaml':
71
+ return yaml()
72
+ case 'bash':
73
+ return StreamLanguage.define(shell)
74
+ case 'go':
75
+ return StreamLanguage.define(go)
76
+ case 'rust':
77
+ return rust()
78
+ case 'dockerfile':
79
+ return StreamLanguage.define(dockerFile)
80
+ case 'java':
81
+ return java()
82
+ case 'cpp':
83
+ return cpp()
84
+ case 'xml':
85
+ return xml()
86
+ default:
87
+ return []
88
+ }
89
+ }
90
+
91
+ function getThemeExtension(): Extension[] {
92
+ const baseTheme = EditorView.theme({
93
+ '&': {
94
+ height: '100%',
95
+ fontSize: '14px'
96
+ },
97
+ '.cm-scroller': {
98
+ overflow: 'auto',
99
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
100
+ },
101
+ '.cm-content': {
102
+ padding: '12px 0'
103
+ },
104
+ '.cm-line': {
105
+ padding: '0 16px'
106
+ },
107
+ '&.cm-focused': {
108
+ outline: 'none'
109
+ }
110
+ })
111
+
112
+ // Use One Dark theme for dark mode
113
+ if (colorMode.value === 'dark')
114
+ return [baseTheme, oneDark]
115
+
116
+ return [baseTheme]
117
+ }
118
+
119
+ onMounted(() => {
120
+ if (!editorRef.value) return
121
+
122
+ const extensions: Extension[] = [
123
+ basicSetup,
124
+ languageCompartment.of(getLanguageExtension(props.language)),
125
+ readOnlyCompartment.of(EditorState.readOnly.of(props.readOnly)),
126
+ editableCompartment.of(EditorView.editable.of(!props.readOnly)),
127
+ themeCompartment.of(getThemeExtension()),
128
+ EditorView.updateListener.of((update) => {
129
+ if (update.docChanged) {
130
+ emit('update:modelValue', update.state.doc.toString())
131
+ }
132
+ })
133
+ ]
134
+
135
+ view = new EditorView({
136
+ state: EditorState.create({
137
+ doc: props.modelValue,
138
+ extensions
139
+ }),
140
+ parent: editorRef.value
141
+ })
142
+ })
143
+
144
+ // Watch for external content changes
145
+ watch(() => props.modelValue, (newVal) => {
146
+ if (view && newVal !== view.state.doc.toString()) {
147
+ view.dispatch({
148
+ changes: { from: 0, to: view.state.doc.length, insert: newVal }
149
+ })
150
+ }
151
+ })
152
+
153
+ // Watch for language changes
154
+ watch(() => props.language, (newLang) => {
155
+ if (view) {
156
+ view.dispatch({
157
+ effects: languageCompartment.reconfigure(getLanguageExtension(newLang))
158
+ })
159
+ }
160
+ })
161
+
162
+ // Watch for readOnly changes
163
+ watch(() => props.readOnly, (newReadOnly) => {
164
+ if (view) {
165
+ view.dispatch({
166
+ effects: [
167
+ readOnlyCompartment.reconfigure(EditorState.readOnly.of(newReadOnly)),
168
+ editableCompartment.reconfigure(EditorView.editable.of(!newReadOnly))
169
+ ]
170
+ })
171
+ }
172
+ })
173
+
174
+ // Watch for theme changes
175
+ watch(() => colorMode.value, () => {
176
+ if (view) {
177
+ view.dispatch({
178
+ effects: themeCompartment.reconfigure(getThemeExtension())
179
+ })
180
+ }
181
+ })
182
+
183
+ onUnmounted(() => {
184
+ view?.destroy()
185
+ view = null
186
+ })
187
+ </script>
188
+
189
+ <template>
190
+ <div
191
+ ref="editorRef"
192
+ class="code-editor h-full overflow-hidden"
193
+ />
194
+ </template>
195
+
196
+ <style>
197
+ .code-editor .cm-editor {
198
+ height: 100%;
199
+ }
200
+
201
+ /* Light mode */
202
+ .code-editor .cm-editor {
203
+ background-color: var(--ui-bg);
204
+ }
205
+
206
+ .code-editor .cm-gutters {
207
+ background-color: var(--ui-bg-muted);
208
+ border-right: 1px solid var(--ui-border);
209
+ }
210
+
211
+ /* Dark mode adjustments are handled by CodeMirror's dark option */
212
+ </style>
@@ -0,0 +1,12 @@
1
+ <script setup lang="ts">
2
+ // Loading fallback shown during SSR hydration
3
+ </script>
4
+
5
+ <template>
6
+ <div class="h-full flex items-center justify-center bg-muted/50">
7
+ <UIcon
8
+ name="i-lucide-loader-2"
9
+ class="size-6 animate-spin text-dimmed"
10
+ />
11
+ </div>
12
+ </template>