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 +1 -1
- package/dashboard/app/components/Terminal.vue +37 -4
- package/dashboard/app/pages/terminal.vue +257 -40
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.70.
|
|
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
|
|
76
|
-
// arrow keys
|
|
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
|
-
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
if (ch === '\x7f' || ch === '\b') {
|
|
85
110
|
lineBuf = lineBuf.slice(0, -1)
|
|
86
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
|
268
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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