arkaos 3.70.2 → 3.70.3

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.3
@@ -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,30 @@ function pickFromSearch(cmd: string) {
79
112
  searchOpen.value = false
80
113
  }
81
114
 
115
+ function searchKeydown(e: KeyboardEvent) {
116
+ const total = searchResults.value.length
117
+ if (total === 0) return
118
+ if (e.key === 'ArrowDown') {
119
+ e.preventDefault()
120
+ searchSelectedIdx.value = (searchSelectedIdx.value + 1) % total
121
+ } else if (e.key === 'ArrowUp') {
122
+ e.preventDefault()
123
+ searchSelectedIdx.value = (searchSelectedIdx.value - 1 + total) % total
124
+ } else if (e.key === 'Enter') {
125
+ e.preventDefault()
126
+ const chosen = searchResults.value[searchSelectedIdx.value]
127
+ if (chosen) pickFromSearch(chosen.cmd)
128
+ }
129
+ }
130
+
131
+ function relativeTime(ts: number): string {
132
+ const diff = (Date.now() - ts) / 1000
133
+ if (diff < 60) return 'just now'
134
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
135
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
136
+ return `${Math.floor(diff / 86400)}d ago`
137
+ }
138
+
82
139
  const editingTabId = ref<string | null>(null)
83
140
  const renameDraft = ref('')
84
141
 
@@ -289,33 +346,111 @@ const showHistory = ref(false)
289
346
  History stays in this browser only. Ctrl+R to search history.
290
347
  </footer>
291
348
 
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)"
349
+ <UModal
350
+ v-model:open="searchOpen"
351
+ :ui="{ content: 'max-w-2xl' }"
352
+ >
353
+ <template #content>
354
+ <UCard :ui="{ body: 'p-0', header: 'px-4 py-3', footer: 'px-4 py-2.5' }">
355
+ <template #header>
356
+ <div class="flex items-center gap-3">
357
+ <UIcon name="i-lucide-history" class="size-5 text-muted shrink-0" />
358
+ <UInput
359
+ v-model="searchQuery"
360
+ placeholder="Filter command history…"
361
+ size="lg"
362
+ autofocus
363
+ :ui="{ root: 'flex-1', base: 'border-0 shadow-none ring-0 focus:ring-0 px-0' }"
364
+ @keydown="searchKeydown"
365
+ />
366
+ <span class="text-xs text-muted shrink-0 tabular-nums">
367
+ {{ searchResults.length }} / {{ history.length }}
368
+ </span>
369
+ <kbd class="px-1.5 py-0.5 rounded bg-elevated/50 text-xs font-mono text-muted shrink-0">
370
+ esc
371
+ </kbd>
372
+ </div>
373
+ </template>
374
+
375
+ <div class="max-h-[60vh] overflow-y-auto">
376
+ <div
377
+ v-if="history.length === 0"
378
+ class="p-10 text-center text-sm text-muted"
308
379
  >
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
380
+ <UIcon name="i-lucide-terminal" class="size-8 mx-auto mb-3 opacity-50" />
381
+ <p>No commands yet.</p>
382
+ <p class="text-xs mt-1">
383
+ Run something in the terminal — it'll show up here.
384
+ </p>
313
385
  </div>
386
+ <div
387
+ v-else-if="searchResults.length === 0"
388
+ class="p-10 text-center text-sm text-muted"
389
+ >
390
+ No match for
391
+ <span class="font-mono text-default">{{ searchQuery }}</span>.
392
+ </div>
393
+ <ul v-else class="divide-y divide-default">
394
+ <li
395
+ v-for="(entry, i) in searchResults"
396
+ :key="entry.ts"
397
+ class="px-4 py-2 cursor-pointer transition-colors flex items-center gap-3"
398
+ :class="i === searchSelectedIdx
399
+ ? 'bg-primary/10 border-l-2 border-primary pl-[14px]'
400
+ : 'hover:bg-elevated/40 border-l-2 border-transparent'"
401
+ @click="pickFromSearch(entry.cmd)"
402
+ @mouseenter="searchSelectedIdx = i"
403
+ >
404
+ <UIcon
405
+ name="i-lucide-chevron-right"
406
+ class="size-3.5 shrink-0"
407
+ :class="i === searchSelectedIdx ? 'text-primary' : 'text-muted'"
408
+ />
409
+ <span class="flex-1 min-w-0 font-mono text-sm truncate">
410
+ {{ entry.cmd }}
411
+ </span>
412
+ <span class="text-xs text-muted shrink-0 tabular-nums">
413
+ {{ relativeTime(entry.ts) }}
414
+ </span>
415
+ <kbd
416
+ v-if="i === searchSelectedIdx"
417
+ class="px-1.5 py-0.5 rounded bg-primary/20 text-[10px] font-mono text-primary shrink-0"
418
+ >
419
+ ↵ send
420
+ </kbd>
421
+ </li>
422
+ </ul>
314
423
  </div>
315
- <p class="text-xs text-muted">
316
- Enter sends the top match to the active session.
317
- </p>
318
- </div>
424
+
425
+ <template #footer>
426
+ <div class="text-xs text-muted flex items-center gap-4">
427
+ <span class="flex items-center gap-1">
428
+ <kbd class="px-1.5 py-0.5 rounded bg-elevated/50 font-mono">↑</kbd>
429
+ <kbd class="px-1.5 py-0.5 rounded bg-elevated/50 font-mono">↓</kbd>
430
+ navigate
431
+ </span>
432
+ <span class="flex items-center gap-1">
433
+ <kbd class="px-1.5 py-0.5 rounded bg-elevated/50 font-mono">↵</kbd>
434
+ send to active session
435
+ </span>
436
+ <span class="flex items-center gap-1">
437
+ <kbd class="px-1.5 py-0.5 rounded bg-elevated/50 font-mono">esc</kbd>
438
+ close
439
+ </span>
440
+ <UButton
441
+ v-if="history.length > 0"
442
+ size="xs"
443
+ variant="ghost"
444
+ color="error"
445
+ icon="i-lucide-trash-2"
446
+ class="ml-auto"
447
+ @click="clearHistory"
448
+ >
449
+ Clear all
450
+ </UButton>
451
+ </div>
452
+ </template>
453
+ </UCard>
319
454
  </template>
320
455
  </UModal>
321
456
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.70.2",
3
+ "version": "3.70.3",
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.3"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}