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,220 @@
1
+ import type { FileEntry } from '~/../../shared/types'
2
+
3
+ export interface TreeItem {
4
+ id: string
5
+ label: string
6
+ icon?: string
7
+ path: string
8
+ type: 'file' | 'directory'
9
+ children?: TreeItem[]
10
+ defaultExpanded?: boolean
11
+ }
12
+
13
+ export function useFileTree() {
14
+ const items = ref<TreeItem[]>([])
15
+ const selectedFile = ref<string | null>(null)
16
+ const loading = ref(false)
17
+ const error = ref<string | null>(null)
18
+ const searchQuery = ref('')
19
+
20
+ function fileEntryToTreeItem(entry: FileEntry, expandRoot = false): TreeItem {
21
+ const icon = entry.type === 'directory'
22
+ ? undefined
23
+ : getFileIcon(entry.name)
24
+
25
+ return {
26
+ id: entry.path,
27
+ label: entry.name,
28
+ icon,
29
+ path: entry.path,
30
+ type: entry.type,
31
+ defaultExpanded: expandRoot,
32
+ children: entry.children?.map(child => fileEntryToTreeItem(child))
33
+ }
34
+ }
35
+
36
+ function getFileIcon(filename: string): string {
37
+ const ext = filename.split('.').pop()?.toLowerCase()
38
+ switch (ext) {
39
+ case 'md':
40
+ return 'i-lucide-file-text'
41
+ case 'ts':
42
+ case 'tsx':
43
+ return 'i-lucide-file-code'
44
+ case 'js':
45
+ case 'jsx':
46
+ return 'i-lucide-file-code'
47
+ case 'vue':
48
+ return 'i-lucide-file-code'
49
+ case 'json':
50
+ return 'i-lucide-file-json'
51
+ case 'css':
52
+ case 'scss':
53
+ return 'i-lucide-file-code'
54
+ case 'png':
55
+ case 'jpg':
56
+ case 'jpeg':
57
+ case 'gif':
58
+ case 'svg':
59
+ return 'i-lucide-file-image'
60
+ default:
61
+ return 'i-lucide-file'
62
+ }
63
+ }
64
+
65
+ async function loadTree(path = '/') {
66
+ loading.value = true
67
+ error.value = null
68
+
69
+ try {
70
+ const response = await $fetch<{ data: FileEntry[] }>('/api/fs/list', {
71
+ query: { path, recursive: 'true' }
72
+ })
73
+
74
+ items.value = response.data.map(entry => fileEntryToTreeItem(entry, true))
75
+ } catch (e) {
76
+ error.value = 'Failed to load file tree'
77
+ console.error('Failed to load file tree:', e)
78
+ } finally {
79
+ loading.value = false
80
+ }
81
+ }
82
+
83
+ async function readFile(path: string) {
84
+ try {
85
+ const response = await $fetch<{ data: { content: string, path: string, modifiedAt: string } }>('/api/fs/read', {
86
+ method: 'POST',
87
+ body: { path }
88
+ })
89
+ return response.data
90
+ } catch (e) {
91
+ console.error('Failed to read file:', e)
92
+ throw e
93
+ }
94
+ }
95
+
96
+ async function writeFile(path: string, content: string) {
97
+ try {
98
+ const response = await $fetch<{ data: { path: string, created: boolean } }>('/api/fs/write', {
99
+ method: 'POST',
100
+ body: { path, content }
101
+ })
102
+ return response.data
103
+ } catch (e) {
104
+ console.error('Failed to write file:', e)
105
+ throw e
106
+ }
107
+ }
108
+
109
+ async function createFile(parentPath: string, filename: string) {
110
+ const path = parentPath === '/' ? `/${filename}` : `${parentPath}/${filename}`
111
+ await writeFile(path, '')
112
+ await loadTree()
113
+ return path
114
+ }
115
+
116
+ async function createFolder(parentPath: string, foldername: string) {
117
+ const path = parentPath === '/' ? `/${foldername}` : `${parentPath}/${foldername}`
118
+ try {
119
+ await $fetch('/api/fs/mkdir', {
120
+ method: 'POST',
121
+ body: { path }
122
+ })
123
+ await loadTree()
124
+ return path
125
+ } catch (e) {
126
+ console.error('Failed to create folder:', e)
127
+ throw e
128
+ }
129
+ }
130
+
131
+ async function renameItem(oldPath: string, newName: string) {
132
+ const parts = oldPath.split('/')
133
+ parts.pop()
134
+ const newPath = [...parts, newName].join('/')
135
+
136
+ try {
137
+ await $fetch('/api/fs/rename', {
138
+ method: 'POST',
139
+ body: { oldPath, newPath }
140
+ })
141
+ await loadTree()
142
+ return newPath
143
+ } catch (e) {
144
+ console.error('Failed to rename:', e)
145
+ throw e
146
+ }
147
+ }
148
+
149
+ async function deleteItem(path: string) {
150
+ try {
151
+ await $fetch('/api/fs/delete', {
152
+ method: 'POST',
153
+ body: { path }
154
+ })
155
+ if (selectedFile.value === path) {
156
+ selectedFile.value = null
157
+ }
158
+ await loadTree()
159
+ } catch (e) {
160
+ console.error('Failed to delete:', e)
161
+ throw e
162
+ }
163
+ }
164
+
165
+ async function moveItem(sourcePath: string, destinationPath: string) {
166
+ try {
167
+ const response = await $fetch<{ data: { oldPath: string, newPath: string, moved: boolean } }>('/api/fs/move', {
168
+ method: 'POST',
169
+ body: { sourcePath, destinationPath }
170
+ })
171
+ await loadTree()
172
+ return response.data.newPath
173
+ } catch (e) {
174
+ console.error('Failed to move:', e)
175
+ throw e
176
+ }
177
+ }
178
+
179
+ const filteredItems = computed(() => {
180
+ if (!searchQuery.value) return items.value
181
+
182
+ const query = searchQuery.value.toLowerCase()
183
+
184
+ function filterTree(items: TreeItem[]): TreeItem[] {
185
+ return items.reduce<TreeItem[]>((acc, item) => {
186
+ const matches = item.label.toLowerCase().includes(query)
187
+ const filteredChildren = item.children ? filterTree(item.children) : []
188
+
189
+ if (matches || filteredChildren.length > 0) {
190
+ acc.push({
191
+ ...item,
192
+ children: filteredChildren.length > 0 ? filteredChildren : item.children,
193
+ defaultExpanded: filteredChildren.length > 0 || matches
194
+ })
195
+ }
196
+
197
+ return acc
198
+ }, [])
199
+ }
200
+
201
+ return filterTree(items.value)
202
+ })
203
+
204
+ return {
205
+ items,
206
+ filteredItems,
207
+ selectedFile,
208
+ loading,
209
+ error,
210
+ searchQuery,
211
+ loadTree,
212
+ readFile,
213
+ writeFile,
214
+ createFile,
215
+ createFolder,
216
+ renameItem,
217
+ deleteItem,
218
+ moveItem
219
+ }
220
+ }
@@ -0,0 +1,68 @@
1
+ import type { HookEvent, HookEventStats, HookEventFilters, StatsPeriod } from '~~/shared/types'
2
+
3
+ export function useHookEvents() {
4
+ const events = ref<HookEvent[]>([])
5
+ const stats = ref<HookEventStats | null>(null)
6
+ const loading = ref(false)
7
+ const error = ref<string | null>(null)
8
+
9
+ async function fetchEvents(filters?: HookEventFilters) {
10
+ loading.value = true
11
+ error.value = null
12
+
13
+ try {
14
+ const response = await $fetch<{ data: HookEvent[] }>('/api/hooks/events', {
15
+ query: filters
16
+ })
17
+ events.value = response.data
18
+ return response.data
19
+ } catch (e) {
20
+ error.value = 'Failed to load hook events'
21
+ console.error('Failed to load hook events:', e)
22
+ throw e
23
+ } finally {
24
+ loading.value = false
25
+ }
26
+ }
27
+
28
+ async function fetchStats(period: StatsPeriod = '7d') {
29
+ loading.value = true
30
+ error.value = null
31
+
32
+ try {
33
+ const response = await $fetch<{ data: HookEventStats }>('/api/hooks/stats', {
34
+ query: { period }
35
+ })
36
+ stats.value = response.data
37
+ return response.data
38
+ } catch (e) {
39
+ error.value = 'Failed to load hook stats'
40
+ console.error('Failed to load hook stats:', e)
41
+ throw e
42
+ } finally {
43
+ loading.value = false
44
+ }
45
+ }
46
+
47
+ async function fetchEventsBySession(sessionId: string, limit = 100) {
48
+ try {
49
+ const response = await $fetch<{ data: HookEvent[] }>('/api/hooks/events', {
50
+ query: { sessionId, limit }
51
+ })
52
+ return response.data
53
+ } catch (e) {
54
+ console.error('Failed to fetch session events:', e)
55
+ throw e
56
+ }
57
+ }
58
+
59
+ return {
60
+ events,
61
+ stats,
62
+ loading,
63
+ error,
64
+ fetchEvents,
65
+ fetchStats,
66
+ fetchEventsBySession
67
+ }
68
+ }
@@ -0,0 +1,83 @@
1
+ import type { MemoryChunk, MemoryChunkType } from '~~/shared/types'
2
+
3
+ interface MemoryFilters {
4
+ query?: string
5
+ chunkType?: MemoryChunkType | 'all'
6
+ projectPath?: string
7
+ limit?: number
8
+ }
9
+
10
+ export function useMemories() {
11
+ const memories = ref<MemoryChunk[]>([])
12
+ const loading = ref(true) // Start true to avoid SSR rendering issues
13
+ const error = ref<string | null>(null)
14
+
15
+ const filters = reactive<MemoryFilters>({
16
+ query: '',
17
+ chunkType: 'all',
18
+ limit: 50
19
+ })
20
+
21
+ async function fetchMemories(customFilters?: Partial<MemoryFilters>) {
22
+ loading.value = true
23
+ error.value = null
24
+
25
+ const queryFilters = { ...filters, ...customFilters }
26
+
27
+ try {
28
+ const query: Record<string, string> = {}
29
+
30
+ if (queryFilters.query)
31
+ query.query = queryFilters.query
32
+ if (queryFilters.chunkType && queryFilters.chunkType !== 'all')
33
+ query.chunkType = queryFilters.chunkType
34
+ if (queryFilters.projectPath)
35
+ query.projectPath = queryFilters.projectPath
36
+ if (queryFilters.limit)
37
+ query.limit = String(queryFilters.limit)
38
+
39
+ const response = await $fetch<{ data: MemoryChunk[] }>('/api/memory/search', { query })
40
+ memories.value = response.data
41
+ return response.data
42
+ } catch (e) {
43
+ error.value = 'Failed to load memories'
44
+ console.error('Failed to load memories:', e)
45
+ throw e
46
+ } finally {
47
+ loading.value = false
48
+ }
49
+ }
50
+
51
+ async function deleteMemory(id: string) {
52
+ try {
53
+ await $fetch(`/api/memory/${id}`, { method: 'DELETE' })
54
+ memories.value = memories.value.filter(m => m.id !== id)
55
+ } catch (e) {
56
+ console.error('Failed to delete memory:', e)
57
+ throw e
58
+ }
59
+ }
60
+
61
+ // Computed stats
62
+ const stats = computed(() => {
63
+ const byType = memories.value.reduce((acc, m) => {
64
+ acc[m.chunkType] = (acc[m.chunkType] || 0) + 1
65
+ return acc
66
+ }, {} as Record<string, number>)
67
+
68
+ return {
69
+ total: memories.value.length,
70
+ byType
71
+ }
72
+ })
73
+
74
+ return {
75
+ memories,
76
+ loading,
77
+ error,
78
+ filters,
79
+ stats,
80
+ fetchMemories,
81
+ deleteMemory
82
+ }
83
+ }
@@ -0,0 +1,154 @@
1
+ import type { NotificationPayload } from '~~/shared/types'
2
+
3
+ export type NotificationBusStatus = 'disconnected' | 'connecting' | 'connected' | 'error'
4
+
5
+ // Shared state across all component instances
6
+ const status = ref<NotificationBusStatus>('disconnected')
7
+ const runningAgentIds = ref<Set<string>>(new Set())
8
+ const ws = ref<WebSocket | null>(null)
9
+ const reconnectAttempts = ref(0)
10
+ const maxReconnectAttempts = 5
11
+ const reconnectDelay = 2000
12
+ let pingInterval: ReturnType<typeof setInterval> | null = null
13
+ let isInitialized = false
14
+
15
+ export function useNotificationBus() {
16
+ const toast = useToast()
17
+
18
+ function getWebSocketUrl(): string {
19
+ if (import.meta.server) return ''
20
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
21
+ return `${protocol}//${window.location.host}/notifications`
22
+ }
23
+
24
+ function handleNotification(payload: NotificationPayload) {
25
+ // Update running agents state
26
+ if (payload.type === 'agent:started' && payload.agentId) {
27
+ runningAgentIds.value.add(payload.agentId)
28
+ } else if ((payload.type === 'agent:completed' || payload.type === 'agent:failed') && payload.agentId) {
29
+ runningAgentIds.value.delete(payload.agentId)
30
+ }
31
+
32
+ // Show toast for agent events
33
+ if (payload.type === 'agent:started') {
34
+ toast.add({
35
+ title: 'Agent Started',
36
+ description: payload.agentName || 'An agent has started running',
37
+ icon: 'i-lucide-play',
38
+ color: 'info'
39
+ })
40
+ } else if (payload.type === 'agent:completed') {
41
+ toast.add({
42
+ title: 'Agent Completed',
43
+ description: payload.agentName || 'Agent run completed successfully',
44
+ icon: 'i-lucide-check-circle',
45
+ color: 'success'
46
+ })
47
+ } else if (payload.type === 'agent:failed') {
48
+ toast.add({
49
+ title: 'Agent Failed',
50
+ description: payload.message || payload.agentName || 'Agent run failed',
51
+ icon: 'i-lucide-alert-circle',
52
+ color: 'error'
53
+ })
54
+ } else if (payload.type === 'toast' && payload.title) {
55
+ toast.add({
56
+ title: payload.title,
57
+ description: payload.message,
58
+ color: payload.color || 'info'
59
+ })
60
+ }
61
+ }
62
+
63
+ function connect() {
64
+ if (import.meta.server) return
65
+ if (ws.value?.readyState === WebSocket.OPEN) return
66
+ if (isInitialized && status.value === 'connecting') return
67
+
68
+ isInitialized = true
69
+ status.value = 'connecting'
70
+
71
+ const socket = new WebSocket(getWebSocketUrl())
72
+ ws.value = socket
73
+
74
+ socket.onopen = () => {
75
+ status.value = 'connected'
76
+ reconnectAttempts.value = 0
77
+ console.log('[notification-bus] Connected')
78
+ startPingInterval()
79
+ }
80
+
81
+ socket.onmessage = (event) => {
82
+ try {
83
+ const data = JSON.parse(event.data) as Record<string, unknown>
84
+ const msgType = data.type as string
85
+
86
+ // Skip pong and connected messages
87
+ if (msgType === 'pong' || msgType === 'connected') return
88
+
89
+ // Only handle known notification types
90
+ if (msgType === 'agent:started' || msgType === 'agent:completed' || msgType === 'agent:failed' || msgType === 'toast') {
91
+ handleNotification(data as unknown as NotificationPayload)
92
+ }
93
+ } catch (e) {
94
+ console.error('[notification-bus] Failed to parse message:', e)
95
+ }
96
+ }
97
+
98
+ socket.onclose = () => {
99
+ status.value = 'disconnected'
100
+ stopPingInterval()
101
+
102
+ // Attempt reconnection
103
+ if (reconnectAttempts.value < maxReconnectAttempts) {
104
+ reconnectAttempts.value++
105
+ console.log(`[notification-bus] Reconnecting (attempt ${reconnectAttempts.value})...`)
106
+ setTimeout(connect, reconnectDelay * reconnectAttempts.value)
107
+ }
108
+ }
109
+
110
+ socket.onerror = () => {
111
+ status.value = 'error'
112
+ }
113
+ }
114
+
115
+ function disconnect() {
116
+ stopPingInterval()
117
+ if (ws.value) {
118
+ ws.value.close()
119
+ ws.value = null
120
+ }
121
+ status.value = 'disconnected'
122
+ isInitialized = false
123
+ }
124
+
125
+ function sendPing() {
126
+ if (ws.value?.readyState === WebSocket.OPEN) {
127
+ ws.value.send(JSON.stringify({ type: 'ping' }))
128
+ }
129
+ }
130
+
131
+ function startPingInterval() {
132
+ if (pingInterval) clearInterval(pingInterval)
133
+ pingInterval = setInterval(sendPing, 30000)
134
+ }
135
+
136
+ function stopPingInterval() {
137
+ if (pingInterval) {
138
+ clearInterval(pingInterval)
139
+ pingInterval = null
140
+ }
141
+ }
142
+
143
+ function isAgentRunning(agentId: string): boolean {
144
+ return runningAgentIds.value.has(agentId)
145
+ }
146
+
147
+ return {
148
+ status: readonly(status),
149
+ runningAgentIds: readonly(runningAgentIds),
150
+ isAgentRunning,
151
+ connect,
152
+ disconnect
153
+ }
154
+ }
@@ -0,0 +1,131 @@
1
+ import type { TaskStatus } from '~~/shared/types'
2
+
3
+ interface Preferences {
4
+ // Editor preferences
5
+ editorMode: 'editor' | 'code'
6
+ viewSourceMode: boolean
7
+
8
+ // Navigation
9
+ lastDocumentPath: string | null
10
+
11
+ // Layout
12
+ sidebarOpen: boolean
13
+ docsTreeWidth: number
14
+ assistantPanelOpen: boolean
15
+ assistantPanelTab: 'chat' | 'terminal'
16
+ assistantLastConversationId: string | null
17
+
18
+ // Task filters
19
+ taskStatusFilter: TaskStatus | 'all'
20
+ taskProjectFilter: string | null
21
+
22
+ // Agent stats
23
+ agentStatsPeriod: '24h' | '7d' | '30d'
24
+ }
25
+
26
+ const COOKIE_NAME = 'sb-preferences'
27
+ const COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
28
+
29
+ const defaults: Preferences = {
30
+ editorMode: 'editor',
31
+ viewSourceMode: false,
32
+ lastDocumentPath: null,
33
+ sidebarOpen: true,
34
+ docsTreeWidth: 16,
35
+ assistantPanelOpen: false,
36
+ assistantPanelTab: 'chat',
37
+ assistantLastConversationId: null,
38
+ taskStatusFilter: 'all',
39
+ taskProjectFilter: null,
40
+ agentStatsPeriod: '7d'
41
+ }
42
+
43
+ export function usePreferences() {
44
+ const cookie = useCookie<Partial<Preferences>>(COOKIE_NAME, {
45
+ maxAge: COOKIE_MAX_AGE,
46
+ default: () => ({})
47
+ })
48
+
49
+ function get<K extends keyof Preferences>(key: K): Preferences[K] {
50
+ return cookie.value[key] ?? defaults[key]
51
+ }
52
+
53
+ function set<K extends keyof Preferences>(key: K, value: Preferences[K]) {
54
+ cookie.value = { ...cookie.value, [key]: value }
55
+ }
56
+
57
+ // Convenience computed refs for common preferences
58
+ const editorMode = computed({
59
+ get: () => get('editorMode'),
60
+ set: v => set('editorMode', v)
61
+ })
62
+
63
+ const viewSourceMode = computed({
64
+ get: () => get('viewSourceMode'),
65
+ set: v => set('viewSourceMode', v)
66
+ })
67
+
68
+ const lastDocumentPath = computed({
69
+ get: () => get('lastDocumentPath'),
70
+ set: v => set('lastDocumentPath', v)
71
+ })
72
+
73
+ const sidebarOpen = computed({
74
+ get: () => get('sidebarOpen'),
75
+ set: v => set('sidebarOpen', v)
76
+ })
77
+
78
+ const docsTreeWidth = computed({
79
+ get: () => get('docsTreeWidth'),
80
+ set: v => set('docsTreeWidth', v)
81
+ })
82
+
83
+ const taskStatusFilter = computed({
84
+ get: () => get('taskStatusFilter'),
85
+ set: v => set('taskStatusFilter', v)
86
+ })
87
+
88
+ const taskProjectFilter = computed({
89
+ get: () => get('taskProjectFilter'),
90
+ set: v => set('taskProjectFilter', v)
91
+ })
92
+
93
+ const agentStatsPeriod = computed({
94
+ get: () => get('agentStatsPeriod'),
95
+ set: v => set('agentStatsPeriod', v)
96
+ })
97
+
98
+ const assistantPanelOpen = computed({
99
+ get: () => get('assistantPanelOpen'),
100
+ set: v => set('assistantPanelOpen', v)
101
+ })
102
+
103
+ const assistantPanelTab = computed({
104
+ get: () => get('assistantPanelTab'),
105
+ set: v => set('assistantPanelTab', v)
106
+ })
107
+
108
+ const assistantLastConversationId = computed({
109
+ get: () => get('assistantLastConversationId'),
110
+ set: v => set('assistantLastConversationId', v)
111
+ })
112
+
113
+ return {
114
+ // Raw access
115
+ get,
116
+ set,
117
+
118
+ // Computed refs
119
+ editorMode,
120
+ viewSourceMode,
121
+ lastDocumentPath,
122
+ sidebarOpen,
123
+ docsTreeWidth,
124
+ assistantPanelOpen,
125
+ assistantPanelTab,
126
+ assistantLastConversationId,
127
+ taskStatusFilter,
128
+ taskProjectFilter,
129
+ agentStatsPeriod
130
+ }
131
+ }