arkaos 3.70.2 → 3.70.4

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.2
1
+ 3.70.4
@@ -72,20 +72,53 @@ onMounted(async () => {
72
72
  })
73
73
 
74
74
  // PR99c v3.69.0 — line-buffer for command history without server-
75
- // side audit. Captures only printable chars up to Enter; ignores
76
- // arrow keys, ctrl combos, escape sequences.
75
+ // side audit. Captures only printable chars up to Enter.
76
+ // v3.70.3 — proper ANSI ESC-sequence skipper so arrow keys / cursor
77
+ // queries / function keys never leak into the history. The state
78
+ // machine handles `\x1b[...<final>` (CSI) and `\x1bO<char>` (SS3).
77
79
  let lineBuf = ''
80
+ let escState: 'none' | 'esc' | 'csi' | 'ss3' = 'none'
78
81
  t.onData((data) => {
79
82
  for (const ch of data) {
83
+ if (escState === 'esc') {
84
+ if (ch === '[') escState = 'csi'
85
+ else if (ch === 'O') escState = 'ss3'
86
+ else escState = 'none' // unknown ESC sequence, drop just this byte
87
+ continue
88
+ }
89
+ if (escState === 'csi') {
90
+ // Final byte of a CSI is in 0x40-0x7E (@A..Z[\]^_`a..z{|}~)
91
+ if (ch >= '@' && ch <= '~') escState = 'none'
92
+ continue
93
+ }
94
+ if (escState === 'ss3') {
95
+ // SS3 is ESC O <one-char>
96
+ escState = 'none'
97
+ continue
98
+ }
99
+ if (ch === '\x1b') {
100
+ escState = 'esc'
101
+ continue
102
+ }
80
103
  if (ch === '\r' || ch === '\n') {
81
104
  const cmd = lineBuf.trim()
82
105
  if (cmd) props.onInputLine?.(cmd)
83
106
  lineBuf = ''
84
- } else if (ch === '\x7f' || ch === '\b') {
107
+ continue
108
+ }
109
+ if (ch === '\x7f' || ch === '\b') {
85
110
  lineBuf = lineBuf.slice(0, -1)
86
- } else if (ch >= ' ' && ch < '\x7f') {
111
+ continue
112
+ }
113
+ if (ch === '\x03' || ch === '\x15') {
114
+ // Ctrl-C or Ctrl-U — operator abandoned the line
115
+ lineBuf = ''
116
+ continue
117
+ }
118
+ if (ch >= ' ' && ch <= '~') {
87
119
  lineBuf += ch
88
120
  }
121
+ // any other control byte is silently dropped
89
122
  }
90
123
  session.sendInput(data)
91
124
  })
@@ -29,11 +29,28 @@ interface HistoryEntry {
29
29
  cmd: string
30
30
  }
31
31
 
32
+ // v3.70.3 — sanitise legacy entries polluted by ANSI ESC sequences
33
+ // that leaked through the v3.69.0 line-buffer before the proper
34
+ // state-machine filter landed.
35
+ function isPlausibleCommand(cmd: string): boolean {
36
+ if (!cmd || cmd.length < 2) return false
37
+ // Reject anything that looks like a CSI/SS3 remnant
38
+ if (/^\[?\?/.test(cmd)) return false
39
+ if (/\[[\d;?]*[A-Za-z~]/.test(cmd)) return false
40
+ // Reject anything starting with `[` followed by digits or letter — ESC remnant
41
+ if (/^\[[\dA-Za-z]/.test(cmd)) return false
42
+ // Must contain at least one alphanumeric — pure punctuation is suspect
43
+ if (!/[A-Za-z0-9]/.test(cmd)) return false
44
+ return true
45
+ }
46
+
32
47
  function loadHistory(): HistoryEntry[] {
33
48
  if (typeof localStorage === 'undefined') return []
34
49
  try {
35
50
  const raw = localStorage.getItem(HISTORY_KEY)
36
- return raw ? (JSON.parse(raw) as HistoryEntry[]) : []
51
+ if (!raw) return []
52
+ const parsed = JSON.parse(raw) as HistoryEntry[]
53
+ return parsed.filter((e) => e && typeof e.cmd === 'string' && isPlausibleCommand(e.cmd))
37
54
  } catch {
38
55
  return []
39
56
  }
@@ -41,9 +58,18 @@ function loadHistory(): HistoryEntry[] {
41
58
 
42
59
  const history = ref<HistoryEntry[]>(loadHistory())
43
60
 
61
+ function clearHistory() {
62
+ history.value = []
63
+ try {
64
+ localStorage.removeItem(HISTORY_KEY)
65
+ } catch {
66
+ // ignore
67
+ }
68
+ }
69
+
44
70
  function recordCommand(cmd: string) {
45
71
  const trimmed = cmd.trim()
46
- if (!trimmed || trimmed.length < 2) return
72
+ if (!isPlausibleCommand(trimmed)) return
47
73
  history.value.unshift({ ts: Date.now(), cmd: trimmed })
48
74
  if (history.value.length > HISTORY_MAX) {
49
75
  history.value = history.value.slice(0, HISTORY_MAX)
@@ -57,9 +83,11 @@ function recordCommand(cmd: string) {
57
83
  }
58
84
 
59
85
  // PR99d v3.70.0 — theme picker + Ctrl+R history search.
86
+ // v3.70.3 — proper command palette UX (keyboard nav, selected row).
60
87
  const { themeName, setTheme, options: themeOptions } = useTerminalThemes()
61
88
  const searchOpen = ref(false)
62
89
  const searchQuery = ref('')
90
+ const searchSelectedIdx = ref(0)
63
91
 
64
92
  const searchResults = computed(() => {
65
93
  const q = searchQuery.value.trim().toLowerCase()
@@ -69,9 +97,14 @@ const searchResults = computed(() => {
69
97
  .slice(0, 30)
70
98
  })
71
99
 
100
+ watch(searchResults, () => {
101
+ searchSelectedIdx.value = 0
102
+ })
103
+
72
104
  function openSearch() {
73
105
  searchOpen.value = true
74
106
  searchQuery.value = ''
107
+ searchSelectedIdx.value = 0
75
108
  }
76
109
 
77
110
  function pickFromSearch(cmd: string) {
@@ -79,6 +112,44 @@ function pickFromSearch(cmd: string) {
79
112
  searchOpen.value = false
80
113
  }
81
114
 
115
+ // v3.70.4 — inline filter for the side panel.
116
+ const sidebarFilter = ref('')
117
+
118
+ const visibleHistory = computed(() => {
119
+ const q = sidebarFilter.value.trim().toLowerCase()
120
+ const filtered = history.value.filter((e) => isPlausibleCommand(e.cmd))
121
+ if (!q) return filtered
122
+ return filtered.filter((e) => e.cmd.toLowerCase().includes(q))
123
+ })
124
+
125
+ function sendToActive(cmd: string) {
126
+ activeTab.value?.session.sendInput(cmd)
127
+ }
128
+
129
+ function searchKeydown(e: KeyboardEvent) {
130
+ const total = searchResults.value.length
131
+ if (total === 0) return
132
+ if (e.key === 'ArrowDown') {
133
+ e.preventDefault()
134
+ searchSelectedIdx.value = (searchSelectedIdx.value + 1) % total
135
+ } else if (e.key === 'ArrowUp') {
136
+ e.preventDefault()
137
+ searchSelectedIdx.value = (searchSelectedIdx.value - 1 + total) % total
138
+ } else if (e.key === 'Enter') {
139
+ e.preventDefault()
140
+ const chosen = searchResults.value[searchSelectedIdx.value]
141
+ if (chosen) pickFromSearch(chosen.cmd)
142
+ }
143
+ }
144
+
145
+ function relativeTime(ts: number): string {
146
+ const diff = (Date.now() - ts) / 1000
147
+ if (diff < 60) return 'just now'
148
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
149
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
150
+ return `${Math.floor(diff / 86400)}d ago`
151
+ }
152
+
82
153
  const editingTabId = ref<string | null>(null)
83
154
  const renameDraft = ref('')
84
155
 
@@ -262,24 +333,92 @@ const showHistory = ref(false)
262
333
  </div>
263
334
  <aside
264
335
  v-if="showHistory"
265
- class="w-72 shrink-0 rounded-lg border border-default bg-elevated/10 overflow-hidden flex flex-col"
336
+ class="w-80 shrink-0 rounded-lg border border-default bg-elevated/10 overflow-hidden flex flex-col"
266
337
  >
267
- <div class="px-3 py-2 border-b border-default text-xs uppercase tracking-wide text-muted">
268
- Command history
338
+ <div class="px-3 py-2.5 border-b border-default flex items-center gap-2">
339
+ <UIcon name="i-lucide-history" class="size-4 text-muted shrink-0" />
340
+ <span class="text-sm font-semibold">History</span>
341
+ <UBadge :label="String(visibleHistory.length)" size="xs" variant="subtle" />
342
+ <div class="ml-auto flex items-center gap-1">
343
+ <UButton
344
+ size="xs"
345
+ variant="ghost"
346
+ icon="i-lucide-search"
347
+ title="Open full search (⌃R)"
348
+ @click="openSearch"
349
+ />
350
+ <UButton
351
+ v-if="history.length > 0"
352
+ size="xs"
353
+ variant="ghost"
354
+ color="error"
355
+ icon="i-lucide-trash-2"
356
+ title="Clear all"
357
+ @click="clearHistory"
358
+ />
359
+ <UButton
360
+ size="xs"
361
+ variant="ghost"
362
+ icon="i-lucide-x"
363
+ title="Close panel"
364
+ @click="showHistory = false"
365
+ />
366
+ </div>
269
367
  </div>
270
- <div class="flex-1 overflow-auto text-xs font-mono">
271
- <button
272
- v-for="(entry, i) in history"
273
- :key="i"
274
- class="w-full text-left px-3 py-1.5 hover:bg-default/40 truncate"
275
- :title="entry.cmd"
276
- @click="activeTab?.session.sendInput(entry.cmd)"
368
+
369
+ <div class="px-3 py-2 border-b border-default">
370
+ <UInput
371
+ v-model="sidebarFilter"
372
+ size="xs"
373
+ placeholder="Filter…"
374
+ icon="i-lucide-search"
375
+ class="w-full"
376
+ />
377
+ </div>
378
+
379
+ <div class="flex-1 overflow-y-auto">
380
+ <div
381
+ v-if="history.length === 0"
382
+ class="p-6 text-center text-xs text-muted"
383
+ >
384
+ <UIcon name="i-lucide-terminal" class="size-6 mx-auto mb-2 opacity-50" />
385
+ <p>No commands yet.</p>
386
+ </div>
387
+ <div
388
+ v-else-if="visibleHistory.length === 0"
389
+ class="p-6 text-center text-xs text-muted"
277
390
  >
278
- {{ entry.cmd }}
279
- </button>
280
- <div v-if="history.length === 0" class="px-3 py-4 text-muted text-center">
281
- No commands yet
391
+ No matches for
392
+ <span class="font-mono text-default">{{ sidebarFilter }}</span>.
282
393
  </div>
394
+ <ul v-else class="divide-y divide-default">
395
+ <li
396
+ v-for="entry in visibleHistory"
397
+ :key="entry.ts"
398
+ class="group px-3 py-1.5 hover:bg-elevated/40 cursor-pointer flex items-center gap-2"
399
+ :title="`${entry.cmd} — ${relativeTime(entry.ts)}`"
400
+ @click="sendToActive(entry.cmd)"
401
+ >
402
+ <UIcon
403
+ name="i-lucide-chevron-right"
404
+ class="size-3 shrink-0 text-muted group-hover:text-primary"
405
+ />
406
+ <span class="flex-1 min-w-0 font-mono text-xs truncate">
407
+ {{ entry.cmd }}
408
+ </span>
409
+ <span class="text-[10px] text-muted shrink-0 tabular-nums opacity-0 group-hover:opacity-100 transition-opacity">
410
+ {{ relativeTime(entry.ts) }}
411
+ </span>
412
+ <UIcon
413
+ name="i-lucide-corner-down-left"
414
+ class="size-3 shrink-0 text-muted opacity-0 group-hover:opacity-100 transition-opacity"
415
+ />
416
+ </li>
417
+ </ul>
418
+ </div>
419
+
420
+ <div class="px-3 py-2 border-t border-default text-[10px] text-muted">
421
+ Click a command to send it to the active session.
283
422
  </div>
284
423
  </aside>
285
424
  </div>
@@ -289,33 +428,111 @@ const showHistory = ref(false)
289
428
  History stays in this browser only. Ctrl+R to search history.
290
429
  </footer>
291
430
 
292
- <UModal v-model:open="searchOpen" :title="`Search history (${history.length})`">
293
- <template #body>
294
- <div class="space-y-2">
295
- <UInput
296
- v-model="searchQuery"
297
- placeholder="type to filter…"
298
- autofocus
299
- icon="i-lucide-search"
300
- @keydown.enter="searchResults[0] && pickFromSearch(searchResults[0].cmd)"
301
- />
302
- <div class="max-h-80 overflow-y-auto rounded-md border border-default divide-y divide-default">
303
- <button
304
- v-for="(entry, i) in searchResults"
305
- :key="i"
306
- class="w-full text-left px-3 py-2 text-sm font-mono hover:bg-elevated/40 truncate"
307
- @click="pickFromSearch(entry.cmd)"
431
+ <UModal
432
+ v-model:open="searchOpen"
433
+ :ui="{ content: 'max-w-2xl' }"
434
+ >
435
+ <template #content>
436
+ <UCard :ui="{ body: 'p-0', header: 'px-4 py-3', footer: 'px-4 py-2.5' }">
437
+ <template #header>
438
+ <div class="flex items-center gap-3">
439
+ <UIcon name="i-lucide-history" class="size-5 text-muted shrink-0" />
440
+ <UInput
441
+ v-model="searchQuery"
442
+ placeholder="Filter command history…"
443
+ size="lg"
444
+ autofocus
445
+ :ui="{ root: 'flex-1', base: 'border-0 shadow-none ring-0 focus:ring-0 px-0' }"
446
+ @keydown="searchKeydown"
447
+ />
448
+ <span class="text-xs text-muted shrink-0 tabular-nums">
449
+ {{ searchResults.length }} / {{ history.length }}
450
+ </span>
451
+ <kbd class="px-1.5 py-0.5 rounded bg-elevated/50 text-xs font-mono text-muted shrink-0">
452
+ esc
453
+ </kbd>
454
+ </div>
455
+ </template>
456
+
457
+ <div class="max-h-[60vh] overflow-y-auto">
458
+ <div
459
+ v-if="history.length === 0"
460
+ class="p-10 text-center text-sm text-muted"
461
+ >
462
+ <UIcon name="i-lucide-terminal" class="size-8 mx-auto mb-3 opacity-50" />
463
+ <p>No commands yet.</p>
464
+ <p class="text-xs mt-1">
465
+ Run something in the terminal — it'll show up here.
466
+ </p>
467
+ </div>
468
+ <div
469
+ v-else-if="searchResults.length === 0"
470
+ class="p-10 text-center text-sm text-muted"
308
471
  >
309
- {{ entry.cmd }}
310
- </button>
311
- <div v-if="searchResults.length === 0" class="px-3 py-4 text-muted text-center text-sm">
312
- No matches
472
+ No match for
473
+ <span class="font-mono text-default">{{ searchQuery }}</span>.
313
474
  </div>
475
+ <ul v-else class="divide-y divide-default">
476
+ <li
477
+ v-for="(entry, i) in searchResults"
478
+ :key="entry.ts"
479
+ class="px-4 py-2 cursor-pointer transition-colors flex items-center gap-3"
480
+ :class="i === searchSelectedIdx
481
+ ? 'bg-primary/10 border-l-2 border-primary pl-[14px]'
482
+ : 'hover:bg-elevated/40 border-l-2 border-transparent'"
483
+ @click="pickFromSearch(entry.cmd)"
484
+ @mouseenter="searchSelectedIdx = i"
485
+ >
486
+ <UIcon
487
+ name="i-lucide-chevron-right"
488
+ class="size-3.5 shrink-0"
489
+ :class="i === searchSelectedIdx ? 'text-primary' : 'text-muted'"
490
+ />
491
+ <span class="flex-1 min-w-0 font-mono text-sm truncate">
492
+ {{ entry.cmd }}
493
+ </span>
494
+ <span class="text-xs text-muted shrink-0 tabular-nums">
495
+ {{ relativeTime(entry.ts) }}
496
+ </span>
497
+ <kbd
498
+ v-if="i === searchSelectedIdx"
499
+ class="px-1.5 py-0.5 rounded bg-primary/20 text-[10px] font-mono text-primary shrink-0"
500
+ >
501
+ ↵ send
502
+ </kbd>
503
+ </li>
504
+ </ul>
314
505
  </div>
315
- <p class="text-xs text-muted">
316
- Enter sends the top match to the active session.
317
- </p>
318
- </div>
506
+
507
+ <template #footer>
508
+ <div class="text-xs text-muted flex items-center gap-4">
509
+ <span class="flex items-center gap-1">
510
+ <kbd class="px-1.5 py-0.5 rounded bg-elevated/50 font-mono">↑</kbd>
511
+ <kbd class="px-1.5 py-0.5 rounded bg-elevated/50 font-mono">↓</kbd>
512
+ navigate
513
+ </span>
514
+ <span class="flex items-center gap-1">
515
+ <kbd class="px-1.5 py-0.5 rounded bg-elevated/50 font-mono">↵</kbd>
516
+ send to active session
517
+ </span>
518
+ <span class="flex items-center gap-1">
519
+ <kbd class="px-1.5 py-0.5 rounded bg-elevated/50 font-mono">esc</kbd>
520
+ close
521
+ </span>
522
+ <UButton
523
+ v-if="history.length > 0"
524
+ size="xs"
525
+ variant="ghost"
526
+ color="error"
527
+ icon="i-lucide-trash-2"
528
+ class="ml-auto"
529
+ @click="clearHistory"
530
+ >
531
+ Clear all
532
+ </UButton>
533
+ </div>
534
+ </template>
535
+ </UCard>
319
536
  </template>
320
537
  </UModal>
321
538
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.70.2",
3
+ "version": "3.70.4",
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.2"
3
+ version = "3.70.4"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}