arkaos 3.44.0 → 3.46.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.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.44.0
1
+ 3.46.0
@@ -0,0 +1,116 @@
1
+ <script setup lang="ts">
2
+ // PR93d v3.46.0 — bell-icon notifications popover.
3
+
4
+ const feed = useActivityFeed()
5
+ onMounted(() => feed.load())
6
+
7
+ function formatRelative(iso: string): string {
8
+ const ts = Date.parse(iso)
9
+ if (Number.isNaN(ts)) return iso
10
+ const diff = Date.now() - ts
11
+ const m = Math.floor(diff / 60_000)
12
+ if (m < 1) return 'just now'
13
+ if (m < 60) return `${m}m ago`
14
+ const h = Math.floor(m / 60)
15
+ if (h < 24) return `${h}h ago`
16
+ return `${Math.floor(h / 24)}d ago`
17
+ }
18
+
19
+ function kindIcon(kind: string): string {
20
+ return {
21
+ success: 'i-lucide-check-circle',
22
+ warning: 'i-lucide-alert-triangle',
23
+ error: 'i-lucide-x-circle',
24
+ info: 'i-lucide-info',
25
+ }[kind] ?? 'i-lucide-circle'
26
+ }
27
+
28
+ function kindColor(kind: string): string {
29
+ return {
30
+ success: 'text-emerald-500',
31
+ warning: 'text-amber-500',
32
+ error: 'text-rose-500',
33
+ info: 'text-blue-500',
34
+ }[kind] ?? 'text-muted'
35
+ }
36
+ </script>
37
+
38
+ <template>
39
+ <UPopover :ui="{ content: 'w-80' }">
40
+ <UButton
41
+ icon="i-lucide-bell"
42
+ variant="ghost"
43
+ size="sm"
44
+ aria-label="Notifications"
45
+ :ui="{ base: 'relative' }"
46
+ >
47
+ <UBadge
48
+ v-if="feed.unreadCount.value > 0"
49
+ :label="String(Math.min(feed.unreadCount.value, 99))"
50
+ color="primary"
51
+ size="xs"
52
+ class="absolute -top-1 -right-1 min-w-4"
53
+ />
54
+ </UButton>
55
+
56
+ <template #content>
57
+ <div class="p-2 border-b border-default flex items-center justify-between">
58
+ <div>
59
+ <p class="text-sm font-semibold">Recent activity</p>
60
+ <p class="text-xs text-muted">
61
+ Last {{ Math.min(feed.events.value.length, 50) }} event{{ feed.events.value.length === 1 ? '' : 's' }}
62
+ </p>
63
+ </div>
64
+ <UButton
65
+ v-if="feed.events.value.length > 0"
66
+ label="Clear"
67
+ variant="ghost"
68
+ size="xs"
69
+ @click="feed.clear()"
70
+ />
71
+ </div>
72
+ <div class="max-h-80 overflow-y-auto">
73
+ <p
74
+ v-if="feed.events.value.length === 0"
75
+ class="p-6 text-center text-sm text-muted"
76
+ >
77
+ <UIcon name="i-lucide-bell-off" class="size-6 inline mb-1" /><br>
78
+ Nothing here yet.
79
+ </p>
80
+ <ul v-else class="divide-y divide-default">
81
+ <li
82
+ v-for="ev in feed.events.value"
83
+ :key="ev.id"
84
+ class="px-3 py-2 hover:bg-elevated/40 transition-colors group"
85
+ >
86
+ <component
87
+ :is="ev.to ? 'NuxtLink' : 'div'"
88
+ :to="ev.to"
89
+ class="flex items-start gap-2"
90
+ >
91
+ <UIcon
92
+ :name="kindIcon(ev.kind)"
93
+ :class="['size-4 shrink-0 mt-0.5', kindColor(ev.kind)]"
94
+ />
95
+ <div class="flex-1 min-w-0">
96
+ <p class="text-sm font-medium truncate">{{ ev.title }}</p>
97
+ <p v-if="ev.description" class="text-xs text-muted truncate">
98
+ {{ ev.description }}
99
+ </p>
100
+ <p class="text-xs text-muted/70 mt-0.5">{{ formatRelative(ev.ts) }}</p>
101
+ </div>
102
+ <UButton
103
+ icon="i-lucide-x"
104
+ variant="ghost"
105
+ size="xs"
106
+ aria-label="Dismiss"
107
+ class="opacity-0 group-hover:opacity-100 transition-opacity"
108
+ @click.stop.prevent="feed.remove(ev.id)"
109
+ />
110
+ </component>
111
+ </li>
112
+ </ul>
113
+ </div>
114
+ </template>
115
+ </UPopover>
116
+ </template>
@@ -0,0 +1,72 @@
1
+ // PR93d v3.46.0 — client-side activity feed.
2
+ //
3
+ // Persistence: localStorage `arkaos_activity_feed` (last 50 events).
4
+ // Events are pushed by `push()` from anywhere in the app. The
5
+ // NotificationsBell component reads + clears.
6
+
7
+ import { createSharedComposable } from '@vueuse/core'
8
+
9
+ export interface ActivityEvent {
10
+ id: string
11
+ ts: string // ISO
12
+ kind: 'success' | 'warning' | 'error' | 'info'
13
+ title: string
14
+ description?: string
15
+ to?: string
16
+ }
17
+
18
+ const STORAGE_KEY = 'arkaos_activity_feed'
19
+ const MAX_EVENTS = 50
20
+
21
+ const _useActivityFeed = () => {
22
+ const events = useState<ActivityEvent[]>('activityFeed', () => [])
23
+ const loaded = useState<boolean>('activityFeedLoaded', () => false)
24
+ const unreadCount = computed(() => events.value.length)
25
+
26
+ function _persist() {
27
+ if (typeof window === 'undefined') return
28
+ try {
29
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(events.value))
30
+ } catch {
31
+ // Storage full or disabled — silently drop persistence.
32
+ }
33
+ }
34
+
35
+ function load() {
36
+ if (loaded.value || typeof window === 'undefined') return
37
+ try {
38
+ const raw = window.localStorage.getItem(STORAGE_KEY)
39
+ if (raw) {
40
+ const parsed = JSON.parse(raw)
41
+ if (Array.isArray(parsed)) {
42
+ events.value = parsed.slice(0, MAX_EVENTS)
43
+ }
44
+ }
45
+ } catch {
46
+ events.value = []
47
+ }
48
+ loaded.value = true
49
+ }
50
+
51
+ function push(entry: Omit<ActivityEvent, 'id' | 'ts'>) {
52
+ load()
53
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
54
+ const ts = new Date().toISOString()
55
+ events.value = [{ id, ts, ...entry }, ...events.value].slice(0, MAX_EVENTS)
56
+ _persist()
57
+ }
58
+
59
+ function clear() {
60
+ events.value = []
61
+ _persist()
62
+ }
63
+
64
+ function remove(id: string) {
65
+ events.value = events.value.filter((e) => e.id !== id)
66
+ _persist()
67
+ }
68
+
69
+ return { events, unreadCount, load, push, clear, remove }
70
+ }
71
+
72
+ export const useActivityFeed = createSharedComposable(_useActivityFeed)
@@ -133,7 +133,10 @@ const links = [[{
133
133
  <span class="text-xl font-bold text-primary">A</span>
134
134
  <span v-if="!collapsed" class="font-semibold">ArkaOS</span>
135
135
  </div>
136
- <UColorModeButton v-if="!collapsed" size="xs" />
136
+ <div v-if="!collapsed" class="flex items-center gap-1">
137
+ <NotificationsBell />
138
+ <UColorModeButton size="xs" />
139
+ </div>
137
140
  </div>
138
141
  </template>
139
142
 
@@ -360,6 +360,13 @@ async function bulkDelete() {
360
360
  ? [{ label: 'Undo', icon: 'i-lucide-rotate-ccw', onClick: () => undoTrashIds(trashIds) }]
361
361
  : undefined,
362
362
  })
363
+ // PR93d v3.46.0 — also surface in the notifications bell.
364
+ useActivityFeed().push({
365
+ kind: failures > 0 ? 'warning' : 'success',
366
+ title: `Deleted ${successes.length} agent${successes.length === 1 ? '' : 's'}`,
367
+ description: failures > 0 ? `${failures} skipped` : 'Undo via /trash',
368
+ to: '/trash',
369
+ })
363
370
  } else {
364
371
  toast.add({
365
372
  title: 'Nothing deleted',
@@ -142,6 +142,41 @@ const favs = useFavorites()
142
142
  await favs.load()
143
143
  const favoritesOnly = ref(false)
144
144
 
145
+ // PR93c v3.45.0 — bulk export only selected rows.
146
+ const exportingSelected = ref(false)
147
+ async function exportSelectedZip() {
148
+ if (selected.value.size === 0) return
149
+ const ids = Array.from(selected.value).join(',')
150
+ exportingSelected.value = true
151
+ try {
152
+ const blob = await $fetch<Blob>(
153
+ `${apiBase}/api/personas/export-all.zip`,
154
+ { query: { ids }, responseType: 'blob' },
155
+ )
156
+ const url = URL.createObjectURL(blob)
157
+ const a = document.createElement('a')
158
+ a.href = url
159
+ a.download = `arkaos-personas-selected-${selected.value.size}.zip`
160
+ document.body.appendChild(a)
161
+ a.click()
162
+ a.remove()
163
+ URL.revokeObjectURL(url)
164
+ toast.add({
165
+ title: `Exported ${selected.value.size} persona${selected.value.size === 1 ? '' : 's'}`,
166
+ color: 'success',
167
+ icon: 'i-lucide-archive',
168
+ })
169
+ } catch (err) {
170
+ toast.add({
171
+ title: 'Export failed',
172
+ description: err instanceof Error ? err.message : 'unknown error',
173
+ color: 'error',
174
+ })
175
+ } finally {
176
+ exportingSelected.value = false
177
+ }
178
+ }
179
+
145
180
  // PR92a v3.39.0 — bulk export every persona as a zip.
146
181
  const exportingZip = ref(false)
147
182
  async function exportAllAsZip() {
@@ -636,6 +671,14 @@ async function undoTrashIds(ids: string[]) {
636
671
  @click="clearSelection"
637
672
  />
638
673
  <div class="h-5 w-px bg-default" />
674
+ <UButton
675
+ label="Export ZIP"
676
+ icon="i-lucide-archive"
677
+ size="sm"
678
+ variant="soft"
679
+ :loading="exportingSelected"
680
+ @click="exportSelectedZip"
681
+ />
639
682
  <UButton
640
683
  label="Delete"
641
684
  icon="i-lucide-trash-2"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.44.0",
3
+ "version": "3.46.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "3.44.0"
3
+ version = "3.46.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -334,13 +334,11 @@ def personas_archetypes():
334
334
 
335
335
 
336
336
  @app.get("/api/personas/export-all.zip")
337
- def personas_export_all():
337
+ def personas_export_all(ids: Optional[str] = None):
338
338
  """PR92a v3.39.0 — stream every persona as Markdown inside a ZIP.
339
339
 
340
- Iterates `PersonaManager.list_all()` plus any Obsidian-vault entries
341
- surfaced via `persona_detail`, renders each through
342
- `ObsidianPersonaStore._render`, and zips them. Filename uses the
343
- persona name (sanitised), falling back to id.
340
+ PR93c v3.45.0 optional ``?ids=a,b,c`` filter narrows the export
341
+ to a specific subset.
344
342
  """
345
343
  mgr = _get_persona_manager()
346
344
  if not mgr:
@@ -350,6 +348,13 @@ def personas_export_all():
350
348
  except Exception as exc: # noqa: BLE001
351
349
  return {"error": f"list failed: {exc}"}
352
350
 
351
+ # PR93c — optional id allow-list.
352
+ id_filter: Optional[set[str]] = None
353
+ if ids:
354
+ id_filter = {s.strip() for s in ids.split(",") if s.strip()}
355
+ if id_filter is not None:
356
+ items = [p for p in items if hasattr(p, "id") and p.id in id_filter]
357
+
353
358
  from core.personas.obsidian_store import ObsidianPersonaStore
354
359
 
355
360
  import io