arkaos 3.3.0 → 3.4.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.3.0
1
+ 3.4.0
@@ -138,6 +138,7 @@ const tierColor = (tier: number) => {
138
138
  }
139
139
 
140
140
  const columns: TableColumn<Agent>[] = [
141
+ { id: 'select', header: '' },
141
142
  { accessorKey: 'name', header: 'Name' },
142
143
  { accessorKey: 'role', header: 'Role' },
143
144
  { accessorKey: 'department', header: 'Department' },
@@ -155,6 +156,78 @@ const columns: TableColumn<Agent>[] = [
155
156
  function goToAgent(id: string) {
156
157
  navigateTo(`/agents/${id}`)
157
158
  }
159
+
160
+ // PR83b v3.4.0 — bulk selection + delete.
161
+ const confirmDialog = useConfirmDialog()
162
+ const selected = ref<Set<string>>(new Set())
163
+ const bulkDeleting = ref(false)
164
+
165
+ function toggleSelected(id: string) {
166
+ if (selected.value.has(id)) selected.value.delete(id)
167
+ else selected.value.add(id)
168
+ selected.value = new Set(selected.value)
169
+ }
170
+
171
+ function toggleAllVisible() {
172
+ const visibleIds = paginatedAgents.value.map((a) => a.id)
173
+ const allSelected = visibleIds.every((id) => selected.value.has(id))
174
+ const next = new Set(selected.value)
175
+ for (const id of visibleIds) {
176
+ if (allSelected) next.delete(id)
177
+ else next.add(id)
178
+ }
179
+ selected.value = next
180
+ }
181
+
182
+ const allVisibleSelected = computed(() => {
183
+ const visibleIds = paginatedAgents.value.map((a) => a.id)
184
+ return visibleIds.length > 0 && visibleIds.every((id) => selected.value.has(id))
185
+ })
186
+
187
+ function clearSelection() {
188
+ selected.value = new Set()
189
+ }
190
+
191
+ async function bulkDelete() {
192
+ if (selected.value.size === 0) return
193
+ const ids = Array.from(selected.value)
194
+ const ok = await confirmDialog({
195
+ title: `Delete ${ids.length} agent${ids.length === 1 ? '' : 's'}?`,
196
+ description: 'YAML files will be removed from disk. This cannot be undone. Tier 0 agents are protected and will be skipped.',
197
+ confirmLabel: `Delete ${ids.length}`,
198
+ cancelLabel: 'Cancel',
199
+ variant: 'danger',
200
+ })
201
+ if (!ok) return
202
+ bulkDeleting.value = true
203
+ const results = await Promise.allSettled(
204
+ ids.map((id) =>
205
+ $fetch<{ deleted?: boolean, error?: string }>(`${apiBase}/api/agents/${id}`, {
206
+ method: 'DELETE',
207
+ }),
208
+ ),
209
+ )
210
+ const successes = results.filter(
211
+ (r) => r.status === 'fulfilled' && r.value.deleted,
212
+ ).length
213
+ const failures = ids.length - successes
214
+ if (successes > 0) {
215
+ toast.add({
216
+ title: `Deleted ${successes} agent${successes === 1 ? '' : 's'}`,
217
+ description: failures > 0 ? `${failures} skipped (Tier 0 or missing)` : undefined,
218
+ color: failures > 0 ? 'warning' : 'success',
219
+ })
220
+ } else {
221
+ toast.add({
222
+ title: 'Nothing deleted',
223
+ description: 'All targets were protected or missing.',
224
+ color: 'error',
225
+ })
226
+ }
227
+ clearSelection()
228
+ bulkDeleting.value = false
229
+ await refreshAll()
230
+ }
158
231
  </script>
159
232
 
160
233
  <template>
@@ -233,6 +306,21 @@ function goToAgent(id: string) {
233
306
  td: 'border-b border-default'
234
307
  }"
235
308
  >
309
+ <template #select-header>
310
+ <UCheckbox
311
+ :model-value="allVisibleSelected"
312
+ aria-label="Select all visible"
313
+ @update:model-value="toggleAllVisible"
314
+ />
315
+ </template>
316
+ <template #select-cell="{ row }">
317
+ <UCheckbox
318
+ :model-value="selected.has(row.original.id)"
319
+ :aria-label="`Select ${row.original.name}`"
320
+ @update:model-value="() => toggleSelected(row.original.id)"
321
+ @click.stop
322
+ />
323
+ </template>
236
324
  <template #name-cell="{ row }">
237
325
  <button class="text-left font-medium text-primary hover:underline" @click="goToAgent(row.original.id)">
238
326
  {{ row.original.name }}
@@ -277,6 +365,39 @@ function goToAgent(id: string) {
277
365
  </template>
278
366
  </UTable>
279
367
 
368
+ <Transition
369
+ enter-active-class="transition ease-out duration-150"
370
+ enter-from-class="translate-y-4 opacity-0"
371
+ enter-to-class="translate-y-0 opacity-100"
372
+ leave-active-class="transition ease-in duration-100"
373
+ leave-from-class="translate-y-0 opacity-100"
374
+ leave-to-class="translate-y-4 opacity-0"
375
+ >
376
+ <div
377
+ v-if="selected.size > 0"
378
+ class="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 rounded-xl border border-default bg-elevated shadow-lg px-4 py-2"
379
+ >
380
+ <span class="text-sm font-semibold">
381
+ {{ selected.size }} selected
382
+ </span>
383
+ <UButton
384
+ label="Clear"
385
+ variant="ghost"
386
+ size="xs"
387
+ @click="clearSelection"
388
+ />
389
+ <div class="h-5 w-px bg-default" />
390
+ <UButton
391
+ label="Delete"
392
+ icon="i-lucide-trash-2"
393
+ color="error"
394
+ size="sm"
395
+ :loading="bulkDeleting"
396
+ @click="bulkDelete"
397
+ />
398
+ </div>
399
+ </Transition>
400
+
280
401
  <div v-if="totalPages > 1" class="flex items-center justify-center mt-6">
281
402
  <UPagination
282
403
  :page="page"
@@ -8,7 +8,7 @@
8
8
  import type { TableColumn } from '@nuxt/ui'
9
9
  import type { Persona } from '~/types'
10
10
 
11
- const { fetchApi } = useApi()
11
+ const { fetchApi, apiBase } = useApi()
12
12
 
13
13
  const { data, status, error, refresh } = await fetchApi<{
14
14
  personas: Persona[]
@@ -118,6 +118,7 @@ function agentCount(personaId: string): number {
118
118
  }
119
119
 
120
120
  const columns: TableColumn<Persona>[] = [
121
+ { id: 'select', header: '' },
121
122
  { accessorKey: 'name', header: 'Name' },
122
123
  { accessorKey: 'title', header: 'Title' },
123
124
  { accessorKey: 'source', header: 'Source' },
@@ -131,6 +132,78 @@ const columns: TableColumn<Persona>[] = [
131
132
  function goToPersona(id: string) {
132
133
  navigateTo(`/personas/${id}`)
133
134
  }
135
+
136
+ // PR83b v3.4.0 — bulk selection + delete.
137
+ const toast = useToast()
138
+ const confirmDialog = useConfirmDialog()
139
+ const selected = ref<Set<string>>(new Set())
140
+ const bulkDeleting = ref(false)
141
+
142
+ function toggleSelected(id: string) {
143
+ if (selected.value.has(id)) selected.value.delete(id)
144
+ else selected.value.add(id)
145
+ selected.value = new Set(selected.value)
146
+ }
147
+
148
+ function toggleAllVisible() {
149
+ const visibleIds = paginatedPersonas.value.map((p) => p.id)
150
+ const allSelected = visibleIds.every((id) => selected.value.has(id))
151
+ const next = new Set(selected.value)
152
+ for (const id of visibleIds) {
153
+ if (allSelected) next.delete(id)
154
+ else next.add(id)
155
+ }
156
+ selected.value = next
157
+ }
158
+
159
+ const allVisibleSelected = computed(() => {
160
+ const visibleIds = paginatedPersonas.value.map((p) => p.id)
161
+ return visibleIds.length > 0 && visibleIds.every((id) => selected.value.has(id))
162
+ })
163
+
164
+ function clearSelection() {
165
+ selected.value = new Set()
166
+ }
167
+
168
+ async function bulkDelete() {
169
+ if (selected.value.size === 0) return
170
+ const ids = Array.from(selected.value)
171
+ const ok = await confirmDialog({
172
+ title: `Delete ${ids.length} persona${ids.length === 1 ? '' : 's'}?`,
173
+ description: 'Personas will be removed from the JSON store and the Obsidian vault. This cannot be undone.',
174
+ confirmLabel: `Delete ${ids.length}`,
175
+ cancelLabel: 'Cancel',
176
+ variant: 'danger',
177
+ })
178
+ if (!ok) return
179
+ bulkDeleting.value = true
180
+ const results = await Promise.allSettled(
181
+ ids.map((id) =>
182
+ $fetch<{ deleted?: boolean, error?: string }>(`${apiBase}/api/personas/${id}`, {
183
+ method: 'DELETE',
184
+ }),
185
+ ),
186
+ )
187
+ const successes = results.filter(
188
+ (r) => r.status === 'fulfilled' && r.value.deleted,
189
+ ).length
190
+ const failures = ids.length - successes
191
+ if (successes > 0) {
192
+ toast.add({
193
+ title: `Deleted ${successes} persona${successes === 1 ? '' : 's'}`,
194
+ description: failures > 0 ? `${failures} failed` : undefined,
195
+ color: failures > 0 ? 'warning' : 'success',
196
+ })
197
+ } else {
198
+ toast.add({
199
+ title: 'Nothing deleted',
200
+ color: 'error',
201
+ })
202
+ }
203
+ clearSelection()
204
+ bulkDeleting.value = false
205
+ await refreshAll()
206
+ }
134
207
  </script>
135
208
 
136
209
  <template>
@@ -218,6 +291,21 @@ function goToPersona(id: string) {
218
291
  }"
219
292
  @select="(row: Persona) => goToPersona(row.id)"
220
293
  >
294
+ <template #select-header>
295
+ <UCheckbox
296
+ :model-value="allVisibleSelected"
297
+ aria-label="Select all visible"
298
+ @update:model-value="toggleAllVisible"
299
+ />
300
+ </template>
301
+ <template #select-cell="{ row }">
302
+ <UCheckbox
303
+ :model-value="selected.has(row.original.id)"
304
+ :aria-label="`Select ${row.original.name}`"
305
+ @update:model-value="() => toggleSelected(row.original.id)"
306
+ @click.stop
307
+ />
308
+ </template>
221
309
  <template #name-cell="{ row }">
222
310
  <span class="font-medium">{{ row.original.name }}</span>
223
311
  </template>
@@ -282,6 +370,39 @@ function goToPersona(id: string) {
282
370
  </template>
283
371
  </UTable>
284
372
 
373
+ <Transition
374
+ enter-active-class="transition ease-out duration-150"
375
+ enter-from-class="translate-y-4 opacity-0"
376
+ enter-to-class="translate-y-0 opacity-100"
377
+ leave-active-class="transition ease-in duration-100"
378
+ leave-from-class="translate-y-0 opacity-100"
379
+ leave-to-class="translate-y-4 opacity-0"
380
+ >
381
+ <div
382
+ v-if="selected.size > 0"
383
+ class="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 rounded-xl border border-default bg-elevated shadow-lg px-4 py-2"
384
+ >
385
+ <span class="text-sm font-semibold">
386
+ {{ selected.size }} selected
387
+ </span>
388
+ <UButton
389
+ label="Clear"
390
+ variant="ghost"
391
+ size="xs"
392
+ @click="clearSelection"
393
+ />
394
+ <div class="h-5 w-px bg-default" />
395
+ <UButton
396
+ label="Delete"
397
+ icon="i-lucide-trash-2"
398
+ color="error"
399
+ size="sm"
400
+ :loading="bulkDeleting"
401
+ @click="bulkDelete"
402
+ />
403
+ </div>
404
+ </Transition>
405
+
285
406
  <div v-if="totalPages > 1" class="flex items-center justify-center mt-6">
286
407
  <UPagination
287
408
  :page="page"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.3.0",
3
+ "version": "3.4.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.3.0"
3
+ version = "3.4.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"}
@@ -1094,6 +1094,55 @@ def persona_clone(persona_id: str, body: dict = {}):
1094
1094
  return {"agent_id": agent_id, "department": department, "file": f"departments/{department}/agents/{agent_id}.yaml"}
1095
1095
 
1096
1096
 
1097
+ @app.delete("/api/agents/{agent_id}")
1098
+ def agent_delete(agent_id: str):
1099
+ """PR83b v3.4.0 — delete an agent's YAML file.
1100
+
1101
+ Refuses to delete Tier 0 (C-Suite) agents — those are governance
1102
+ fixtures and need direct YAML removal to make the intent explicit.
1103
+
1104
+ Resolves the YAML location two ways:
1105
+ 1. From the cached registry (covers seeded agents)
1106
+ 2. By scanning departments/*/agents/<id>.yaml (covers
1107
+ freshly-created agents that aren't in the registry yet)
1108
+ """
1109
+ yaml_file = _resolve_agent_yaml(agent_id)
1110
+ if yaml_file is None:
1111
+ return {"error": "Agent not found"}
1112
+ tier = _agent_tier_from_yaml(yaml_file)
1113
+ if tier == 0:
1114
+ return {"error": "Cannot delete Tier 0 (C-Suite) agents from the dashboard"}
1115
+ try:
1116
+ yaml_file.unlink()
1117
+ except OSError as exc:
1118
+ return {"error": f"delete failed: {exc}"}
1119
+ return {"deleted": True, "id": agent_id, "yaml_path": str(yaml_file)}
1120
+
1121
+
1122
+ def _resolve_agent_yaml(agent_id: str) -> Optional[Path]:
1123
+ # 1. Check the cached registry first.
1124
+ for a in _load_agents():
1125
+ if a.get("id") == agent_id:
1126
+ candidate = ARKAOS_ROOT / a.get("file", "")
1127
+ if candidate.exists():
1128
+ return candidate
1129
+ # 2. Filesystem scan — covers freshly-created files.
1130
+ dept_root = ARKAOS_ROOT / "departments"
1131
+ if dept_root.exists():
1132
+ for path in dept_root.glob(f"*/agents/{agent_id}.yaml"):
1133
+ return path
1134
+ return None
1135
+
1136
+
1137
+ def _agent_tier_from_yaml(yaml_file: Path) -> int:
1138
+ try:
1139
+ import yaml as _yaml
1140
+ raw = _yaml.safe_load(yaml_file.read_text(encoding="utf-8")) or {}
1141
+ except Exception:
1142
+ return 99
1143
+ return int(raw.get("tier") or 99)
1144
+
1145
+
1097
1146
  @app.delete("/api/personas/{persona_id}")
1098
1147
  def persona_delete(persona_id: str):
1099
1148
  mgr = _get_persona_manager()