arkaos 3.70.7 → 3.70.8

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.70.7
1
+ 3.70.8
@@ -89,7 +89,7 @@ function formatCost(cost: number | null): string {
89
89
  th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
90
90
  td: 'border-b border-default',
91
91
  }"
92
- @select="(row: { original: DeptRow }) => navigateTo(`/departments/${row.original.department}`)"
92
+ @select="(_e: Event, row: { original: DeptRow }) => row?.original && navigateTo(`/departments/${row.original.department}`)"
93
93
  >
94
94
  <template #department-cell="{ row }">
95
95
  <span class="font-semibold capitalize">{{ row.original.department }}</span>
@@ -308,10 +308,48 @@ function markedHtml(src: string): string {
308
308
  }
309
309
 
310
310
  // PR90a v3.31.0 — download persona as Markdown.
311
- const downloadingMd = ref(false)
311
+ // v3.70.8 instead of triggering a download, open a fullscreen
312
+ // dialog with raw markdown + live preview + Edit Bio + Download.
313
+ import { marked } from 'marked'
314
+
315
+ const mdOpen = ref(false)
316
+ const mdLoading = ref(false)
317
+ const mdRaw = ref('')
318
+ const mdMode = ref<'view' | 'edit'>('view')
319
+ const mdBioDraft = ref('')
320
+ const mdSaving = ref(false)
321
+
322
+ async function openMdViewer() {
323
+ if (!detail.value) return
324
+ mdOpen.value = true
325
+ mdMode.value = 'view'
326
+ mdBioDraft.value = detail.value.bio_md || ''
327
+ if (mdRaw.value) return
328
+ mdLoading.value = true
329
+ try {
330
+ const text = await $fetch<string>(
331
+ `${apiBase}/api/personas/${personaId}/markdown`,
332
+ { responseType: 'text' },
333
+ )
334
+ mdRaw.value = typeof text === 'string' ? text : String(text ?? '')
335
+ } catch (err) {
336
+ toast.add({
337
+ title: 'Markdown fetch failed',
338
+ description: err instanceof Error ? err.message : 'unknown error',
339
+ color: 'error',
340
+ })
341
+ mdOpen.value = false
342
+ } finally {
343
+ mdLoading.value = false
344
+ }
345
+ }
346
+
347
+ function closeMdViewer() {
348
+ mdOpen.value = false
349
+ }
350
+
312
351
  async function downloadMarkdown() {
313
352
  if (!detail.value) return
314
- downloadingMd.value = true
315
353
  try {
316
354
  const blob = await $fetch<Blob>(
317
355
  `${apiBase}/api/personas/${personaId}/markdown`,
@@ -337,11 +375,60 @@ async function downloadMarkdown() {
337
375
  description: err instanceof Error ? err.message : 'unknown error',
338
376
  color: 'error',
339
377
  })
378
+ }
379
+ }
380
+
381
+ async function copyMd() {
382
+ if (!mdRaw.value) return
383
+ try {
384
+ await navigator.clipboard.writeText(mdRaw.value)
385
+ toast.add({ title: 'Copied to clipboard', color: 'success', icon: 'i-lucide-check' })
386
+ } catch {
387
+ toast.add({ title: 'Copy failed', color: 'error' })
388
+ }
389
+ }
390
+
391
+ const mdPreviewHtml = computed<string>(() => {
392
+ const source = mdMode.value === 'edit' ? mdBioDraft.value : mdRaw.value
393
+ if (!source) return ''
394
+ try {
395
+ return marked.parse(source, { breaks: true, gfm: true }) as string
396
+ } catch {
397
+ return ''
398
+ }
399
+ })
400
+
401
+ async function saveBio() {
402
+ if (!detail.value) return
403
+ mdSaving.value = true
404
+ try {
405
+ await $fetch(`${apiBase}/api/personas/${personaId}`, {
406
+ method: 'PUT',
407
+ body: { bio_md: mdBioDraft.value },
408
+ })
409
+ toast.add({ title: 'Bio updated', color: 'success', icon: 'i-lucide-check' })
410
+ mdRaw.value = '' // force refetch on next open so the export reflects the new bio
411
+ mdMode.value = 'view'
412
+ await refresh()
413
+ // Re-pull the exported MD now that the bio changed.
414
+ const text = await $fetch<string>(
415
+ `${apiBase}/api/personas/${personaId}/markdown`,
416
+ { responseType: 'text' },
417
+ )
418
+ mdRaw.value = typeof text === 'string' ? text : String(text ?? '')
419
+ } catch (err) {
420
+ toast.add({
421
+ title: 'Save failed',
422
+ description: err instanceof Error ? err.message : 'unknown error',
423
+ color: 'error',
424
+ })
340
425
  } finally {
341
- downloadingMd.value = false
426
+ mdSaving.value = false
342
427
  }
343
428
  }
344
429
 
430
+ const downloadingMd = ref(false) // kept for the old button binding
431
+
345
432
  // PR85a v3.11.0 — Clone to Agent dialog.
346
433
  const cloneOpen = ref(false)
347
434
  function onCloned(agentId: string) {
@@ -689,11 +776,10 @@ const vocabOptions = [
689
776
  />
690
777
  <UButton
691
778
  label="MD"
692
- icon="i-lucide-download"
779
+ icon="i-lucide-file-text"
693
780
  variant="ghost"
694
781
  size="sm"
695
- :loading="downloadingMd"
696
- @click="downloadMarkdown"
782
+ @click="openMdViewer"
697
783
  />
698
784
  <UButton label="Edit" icon="i-lucide-pencil" size="sm" @click="startEdit" />
699
785
  <UButton
@@ -1265,5 +1351,196 @@ const vocabOptions = [
1265
1351
  </div>
1266
1352
  </DashboardState>
1267
1353
  </template>
1354
+
1355
+ <!-- v3.70.8 — Markdown viewer/editor (fullscreen) -->
1356
+ <UModal
1357
+ v-model:open="mdOpen"
1358
+ :ui="{
1359
+ overlay: 'bg-default/80 backdrop-blur-sm',
1360
+ content: 'sm:max-w-[95vw] w-[95vw] h-[92vh] sm:rounded-xl ring-0 shadow-2xl',
1361
+ }"
1362
+ >
1363
+ <template #content>
1364
+ <div class="flex flex-col h-full bg-default rounded-xl overflow-hidden">
1365
+ <div class="flex items-center gap-3 px-4 py-3 border-b border-default/60 shrink-0">
1366
+ <UIcon name="i-lucide-file-text" class="size-4 text-muted shrink-0" />
1367
+ <span class="text-sm font-semibold">{{ detail?.name || personaId }}.md</span>
1368
+ <UBadge
1369
+ :label="mdMode === 'edit' ? 'editing bio' : 'preview'"
1370
+ :color="mdMode === 'edit' ? 'warning' : 'neutral'"
1371
+ variant="subtle"
1372
+ size="xs"
1373
+ class="ml-1"
1374
+ />
1375
+ <span class="ml-auto flex items-center gap-1">
1376
+ <UButton
1377
+ v-if="mdMode === 'view'"
1378
+ size="sm"
1379
+ variant="ghost"
1380
+ icon="i-lucide-clipboard"
1381
+ @click="copyMd"
1382
+ >
1383
+ Copy
1384
+ </UButton>
1385
+ <UButton
1386
+ v-if="mdMode === 'view'"
1387
+ size="sm"
1388
+ variant="ghost"
1389
+ icon="i-lucide-download"
1390
+ @click="downloadMarkdown"
1391
+ >
1392
+ Download
1393
+ </UButton>
1394
+ <UButton
1395
+ v-if="mdMode === 'view'"
1396
+ size="sm"
1397
+ variant="soft"
1398
+ icon="i-lucide-pencil"
1399
+ @click="mdMode = 'edit'"
1400
+ >
1401
+ Edit bio
1402
+ </UButton>
1403
+ <UButton
1404
+ v-else
1405
+ size="sm"
1406
+ variant="ghost"
1407
+ @click="() => { mdMode = 'view'; mdBioDraft = detail?.bio_md || '' }"
1408
+ >
1409
+ Cancel
1410
+ </UButton>
1411
+ <UButton
1412
+ v-if="mdMode === 'edit'"
1413
+ size="sm"
1414
+ color="primary"
1415
+ icon="i-lucide-check"
1416
+ :loading="mdSaving"
1417
+ @click="saveBio"
1418
+ >
1419
+ Save bio
1420
+ </UButton>
1421
+ <UButton
1422
+ size="sm"
1423
+ variant="ghost"
1424
+ icon="i-lucide-x"
1425
+ aria-label="Close"
1426
+ @click="closeMdViewer"
1427
+ />
1428
+ </span>
1429
+ </div>
1430
+
1431
+ <div
1432
+ v-if="mdLoading"
1433
+ class="flex-1 grid place-items-center text-sm text-muted"
1434
+ >
1435
+ <div class="flex items-center gap-2">
1436
+ <UIcon name="i-lucide-loader-2" class="size-4 animate-spin" />
1437
+ Loading markdown…
1438
+ </div>
1439
+ </div>
1440
+
1441
+ <div v-else class="flex-1 grid grid-cols-2 min-h-0">
1442
+ <!-- Source side: read-only in view mode, textarea in edit mode -->
1443
+ <div class="border-r border-default/60 flex flex-col min-h-0">
1444
+ <div class="px-3 py-1.5 border-b border-default/60 text-[11px] uppercase tracking-wide text-muted shrink-0">
1445
+ {{ mdMode === 'edit' ? 'Bio (editable, supports Markdown)' : 'Source' }}
1446
+ </div>
1447
+ <textarea
1448
+ v-if="mdMode === 'edit'"
1449
+ v-model="mdBioDraft"
1450
+ class="md-editor flex-1 w-full p-4 bg-transparent text-default font-mono text-sm leading-relaxed resize-none focus:outline-none"
1451
+ spellcheck="false"
1452
+ placeholder="Write the persona bio here…"
1453
+ />
1454
+ <pre
1455
+ v-else
1456
+ class="flex-1 min-h-0 overflow-auto p-4 font-mono text-sm leading-relaxed text-default whitespace-pre-wrap"
1457
+ >{{ mdRaw }}</pre>
1458
+ </div>
1459
+
1460
+ <!-- Preview side -->
1461
+ <div class="flex flex-col min-h-0">
1462
+ <div class="px-3 py-1.5 border-b border-default/60 text-[11px] uppercase tracking-wide text-muted shrink-0">
1463
+ Preview
1464
+ </div>
1465
+ <div
1466
+ class="md-preview flex-1 min-h-0 overflow-auto p-6 prose prose-sm prose-invert max-w-none"
1467
+ v-html="mdPreviewHtml"
1468
+ />
1469
+ </div>
1470
+ </div>
1471
+ </div>
1472
+ </template>
1473
+ </UModal>
1268
1474
  </UDashboardPanel>
1269
1475
  </template>
1476
+
1477
+ <style scoped>
1478
+ .md-editor:focus,
1479
+ .md-editor:focus-visible {
1480
+ outline: none !important;
1481
+ box-shadow: none !important;
1482
+ }
1483
+ .md-preview :deep(h1),
1484
+ .md-preview :deep(h2),
1485
+ .md-preview :deep(h3) {
1486
+ font-weight: 600;
1487
+ margin-top: 1.25em;
1488
+ margin-bottom: 0.5em;
1489
+ color: rgb(var(--ui-text));
1490
+ }
1491
+ .md-preview :deep(h1) { font-size: 1.5rem; }
1492
+ .md-preview :deep(h2) { font-size: 1.25rem; }
1493
+ .md-preview :deep(h3) { font-size: 1.05rem; }
1494
+ .md-preview :deep(p) { margin: 0.5em 0; line-height: 1.6; }
1495
+ .md-preview :deep(ul),
1496
+ .md-preview :deep(ol) { padding-left: 1.25rem; margin: 0.5em 0; }
1497
+ .md-preview :deep(li) { margin: 0.2em 0; }
1498
+ .md-preview :deep(code) {
1499
+ font-family: ui-monospace, SFMono-Regular, monospace;
1500
+ background-color: rgb(var(--ui-bg-elevated) / 0.6);
1501
+ padding: 0.1em 0.35em;
1502
+ border-radius: 4px;
1503
+ font-size: 0.875em;
1504
+ }
1505
+ .md-preview :deep(pre) {
1506
+ background-color: rgb(var(--ui-bg-elevated) / 0.4);
1507
+ padding: 0.75rem 1rem;
1508
+ border-radius: 8px;
1509
+ overflow-x: auto;
1510
+ margin: 0.75em 0;
1511
+ }
1512
+ .md-preview :deep(pre code) {
1513
+ background: transparent;
1514
+ padding: 0;
1515
+ }
1516
+ .md-preview :deep(blockquote) {
1517
+ border-left: 3px solid rgb(var(--ui-primary) / 0.5);
1518
+ padding-left: 0.75rem;
1519
+ margin: 0.75em 0;
1520
+ color: rgb(var(--ui-text-muted));
1521
+ }
1522
+ .md-preview :deep(a) {
1523
+ color: rgb(var(--ui-primary));
1524
+ text-decoration: underline;
1525
+ text-underline-offset: 2px;
1526
+ }
1527
+ .md-preview :deep(table) {
1528
+ border-collapse: collapse;
1529
+ margin: 0.75em 0;
1530
+ font-size: 0.875em;
1531
+ }
1532
+ .md-preview :deep(th),
1533
+ .md-preview :deep(td) {
1534
+ border: 1px solid rgb(var(--ui-border));
1535
+ padding: 0.4em 0.7em;
1536
+ }
1537
+ .md-preview :deep(th) {
1538
+ background-color: rgb(var(--ui-bg-elevated) / 0.4);
1539
+ font-weight: 600;
1540
+ }
1541
+ .md-preview :deep(hr) {
1542
+ border: none;
1543
+ border-top: 1px solid rgb(var(--ui-border));
1544
+ margin: 1.25em 0;
1545
+ }
1546
+ </style>
@@ -1,16 +1,51 @@
1
1
  <script setup lang="ts">
2
2
  // PR96c v3.57.0 — Compare two personas side-by-side.
3
+ // v3.70.8 — picker UI when query params are missing (was blank before).
3
4
  //
4
- // Driven by `?a=p1&b=p2`. Mirrors the agents/compare layout but
5
- // adapts to the persona schema (flat mental_models, no department).
5
+ // Driven by `?a=p1&b=p2`. The page renders pickers when either id is
6
+ // missing so the operator can land on /personas/compare directly.
6
7
 
7
8
  const route = useRoute()
9
+ const router = useRouter()
8
10
  const { fetchApi } = useApi()
9
11
 
10
- const ids = computed<string[]>(() => {
11
- const raw = [route.query.a, route.query.b]
12
- return raw.map((v) => String(v ?? '').trim()).filter(Boolean).slice(0, 2)
13
- })
12
+ interface PersonaSummary {
13
+ id: string
14
+ name: string
15
+ title?: string
16
+ }
17
+
18
+ const { data: listData, status: listStatus } = fetchApi<{ personas: PersonaSummary[] }>(
19
+ '/api/personas',
20
+ )
21
+ const personaList = computed(() => listData.value?.personas ?? [])
22
+ const loadingList = computed(() => listStatus.value === 'pending')
23
+
24
+ const personaOptions = computed(() =>
25
+ personaList.value.map((p) => ({
26
+ label: p.name + (p.title ? ` — ${p.title}` : ''),
27
+ value: p.id,
28
+ })),
29
+ )
30
+
31
+ const leftId = computed(() => String(route.query.a ?? '').trim())
32
+ const rightId = computed(() => String(route.query.b ?? '').trim())
33
+
34
+ function setLeft(id: string) {
35
+ router.replace({ query: { ...route.query, a: id || undefined } })
36
+ }
37
+ function setRight(id: string) {
38
+ router.replace({ query: { ...route.query, b: id || undefined } })
39
+ }
40
+ function swapSides() {
41
+ router.replace({
42
+ query: { ...route.query, a: rightId.value || undefined, b: leftId.value || undefined },
43
+ })
44
+ }
45
+
46
+ const ids = computed<string[]>(() =>
47
+ [leftId.value, rightId.value].filter(Boolean).slice(0, 2),
48
+ )
14
49
 
15
50
  interface PersonaDetail {
16
51
  id: string
@@ -46,11 +81,11 @@ const { data: b, status: bStatus } = fetchApi<PersonaDetail>(
46
81
 
47
82
  const loading = computed(() => aStatus.value === 'pending' || bStatus.value === 'pending')
48
83
  const errorMsg = computed(() => {
49
- if (ids.value.length < 2) return 'Pass two persona ids via ?a=p1&b=p2'
50
84
  if (a.value?.error) return `Left: ${a.value.error}`
51
85
  if (b.value?.error) return `Right: ${b.value.error}`
52
86
  return null
53
87
  })
88
+ const bothSelected = computed(() => leftId.value && rightId.value)
54
89
 
55
90
  function diffClass(left: unknown, right: unknown): string {
56
91
  return left !== right ? 'bg-yellow-500/10 border-yellow-500/30' : ''
@@ -78,13 +113,95 @@ const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeable
78
113
  </template>
79
114
 
80
115
  <template #body>
81
- <div v-if="errorMsg" class="p-6 text-center text-sm text-error">
82
- {{ errorMsg }}
83
- </div>
84
- <div v-else-if="loading" class="p-6 text-center text-sm text-muted">
85
- <UIcon name="i-lucide-loader-2" class="size-4 animate-spin inline" /> Loading…
86
- </div>
87
- <div v-else-if="a && b" class="space-y-4 max-w-6xl">
116
+ <div class="p-4 space-y-4">
117
+ <!-- Empty state: no personas at all -->
118
+ <div
119
+ v-if="!loadingList && personaList.length === 0"
120
+ class="rounded-xl border border-default p-12 text-center"
121
+ >
122
+ <UIcon name="i-lucide-users" class="size-10 mx-auto mb-3 text-muted opacity-50" />
123
+ <p class="text-default font-medium">No personas to compare yet.</p>
124
+ <p class="text-sm text-muted mt-1">
125
+ Create at least two personas before using the compare view.
126
+ </p>
127
+ <UButton to="/personas" class="mt-4" icon="i-lucide-arrow-left">
128
+ Back to personas
129
+ </UButton>
130
+ </div>
131
+
132
+ <!-- Empty state: only 1 persona -->
133
+ <div
134
+ v-else-if="!loadingList && personaList.length < 2"
135
+ class="rounded-xl border border-default p-12 text-center"
136
+ >
137
+ <UIcon name="i-lucide-users" class="size-10 mx-auto mb-3 text-muted opacity-50" />
138
+ <p class="text-default font-medium">You need at least 2 personas.</p>
139
+ <p class="text-sm text-muted mt-1">
140
+ Currently {{ personaList.length }} persona in the system.
141
+ </p>
142
+ <UButton to="/personas/new" class="mt-4" icon="i-lucide-plus">
143
+ Create another persona
144
+ </UButton>
145
+ </div>
146
+
147
+ <!-- Pickers (always visible when we have 2+ personas) -->
148
+ <div v-else class="space-y-4">
149
+ <div class="grid grid-cols-[1fr_auto_1fr] gap-2 items-end">
150
+ <div>
151
+ <label class="text-xs text-muted uppercase tracking-wide mb-1 block">Left</label>
152
+ <USelectMenu
153
+ :model-value="leftId"
154
+ :items="personaOptions"
155
+ placeholder="Pick a persona…"
156
+ value-key="value"
157
+ class="w-full"
158
+ @update:model-value="(v: any) => setLeft(typeof v === 'string' ? v : (v?.value ?? ''))"
159
+ />
160
+ </div>
161
+ <UButton
162
+ icon="i-lucide-arrow-left-right"
163
+ variant="ghost"
164
+ size="sm"
165
+ class="mb-0.5"
166
+ :disabled="!bothSelected"
167
+ title="Swap sides"
168
+ @click="swapSides"
169
+ />
170
+ <div>
171
+ <label class="text-xs text-muted uppercase tracking-wide mb-1 block">Right</label>
172
+ <USelectMenu
173
+ :model-value="rightId"
174
+ :items="personaOptions"
175
+ placeholder="Pick a persona…"
176
+ value-key="value"
177
+ class="w-full"
178
+ @update:model-value="(v: any) => setRight(typeof v === 'string' ? v : (v?.value ?? ''))"
179
+ />
180
+ </div>
181
+ </div>
182
+
183
+ <!-- Hint when not both selected -->
184
+ <div
185
+ v-if="!bothSelected"
186
+ class="rounded-xl border border-default border-dashed p-10 text-center text-sm text-muted"
187
+ >
188
+ <UIcon name="i-lucide-columns-2" class="size-7 mx-auto mb-2 opacity-50" />
189
+ Pick two personas above to see the side-by-side diff.
190
+ </div>
191
+
192
+ <!-- Error from detail fetch -->
193
+ <div v-else-if="errorMsg" class="p-4 text-sm text-error border border-error/30 rounded-lg">
194
+ {{ errorMsg }}
195
+ </div>
196
+
197
+ <!-- Loading detail -->
198
+ <div v-else-if="loading" class="p-6 text-center text-sm text-muted">
199
+ <UIcon name="i-lucide-loader-2" class="size-4 animate-spin inline mr-2" />
200
+ Loading personas…
201
+ </div>
202
+
203
+ <!-- Comparison content -->
204
+ <div v-else-if="a && b" class="space-y-4 max-w-6xl">
88
205
  <section class="grid grid-cols-2 gap-3">
89
206
  <NuxtLink :to="`/personas/${a.id}`" class="rounded-lg border border-default p-4 hover:border-primary/40">
90
207
  <p class="text-xs text-muted uppercase tracking-wide">Left</p>
@@ -207,6 +324,8 @@ const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeable
207
324
  <p class="text-xs text-muted pt-4 italic">
208
325
  Yellow tint = different. Red removed, green added.
209
326
  </p>
327
+ </div>
328
+ </div>
210
329
  </div>
211
330
  </template>
212
331
  </UDashboardPanel>
@@ -133,7 +133,8 @@ const columns: TableColumn<Persona>[] = [
133
133
  { id: 'actions', header: '' },
134
134
  ]
135
135
 
136
- function goToPersona(id: string) {
136
+ function goToPersona(id: string | undefined | null) {
137
+ if (!id) return
137
138
  navigateTo(`/personas/${id}`)
138
139
  }
139
140
 
@@ -617,7 +618,7 @@ async function undoTrashIds(ids: string[]) {
617
618
  th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
618
619
  td: 'border-b border-default',
619
620
  }"
620
- @select="(row: { original: Persona }) => goToPersona(row.original.id)"
621
+ @select="(_e: Event, row: { original: Persona }) => goToPersona(row?.original?.id)"
621
622
  >
622
623
  <template #select-header>
623
624
  <UCheckbox
@@ -241,7 +241,7 @@ const columns: TableColumn<Workflow>[] = [
241
241
  th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
242
242
  td: 'border-b border-default',
243
243
  }"
244
- @select="(row: { original: Workflow }) => { selected = row.original; sidePanelTab = 'flow'; runs = []; editingYaml = false; loadRuns(row.original.id) }"
244
+ @select="(_e: Event, row: { original: Workflow }) => { if (!row?.original) return; selected = row.original; sidePanelTab = 'flow'; runs = []; editingYaml = false; loadRuns(row.original.id) }"
245
245
  >
246
246
  <template #name-cell="{ row }">
247
247
  <div class="min-w-0">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.70.7",
3
+ "version": "3.70.8",
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.70.7"
3
+ version = "3.70.8"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}