arkaos 3.70.1 → 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 +1 -1
- package/dashboard/app/components/Terminal.vue +39 -5
- package/dashboard/app/pages/terminal.vue +166 -30
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.70.
|
|
1
|
+
3.70.3
|
|
@@ -9,7 +9,8 @@ import { FitAddon } from '@xterm/addon-fit'
|
|
|
9
9
|
import { WebLinksAddon } from '@xterm/addon-web-links'
|
|
10
10
|
import { SearchAddon } from '@xterm/addon-search'
|
|
11
11
|
import '@xterm/xterm/css/xterm.css'
|
|
12
|
-
import type
|
|
12
|
+
import { useTerminalThemes, type XtermTheme } from '~/composables/useTerminalThemes'
|
|
13
|
+
import { useTerminalSession } from '~/composables/useTerminalSession'
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
15
16
|
session?: ReturnType<typeof useTerminalSession>
|
|
@@ -71,20 +72,53 @@ onMounted(async () => {
|
|
|
71
72
|
})
|
|
72
73
|
|
|
73
74
|
// PR99c v3.69.0 — line-buffer for command history without server-
|
|
74
|
-
// side audit. Captures only printable chars up to Enter
|
|
75
|
-
// 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).
|
|
76
79
|
let lineBuf = ''
|
|
80
|
+
let escState: 'none' | 'esc' | 'csi' | 'ss3' = 'none'
|
|
77
81
|
t.onData((data) => {
|
|
78
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
|
+
}
|
|
79
103
|
if (ch === '\r' || ch === '\n') {
|
|
80
104
|
const cmd = lineBuf.trim()
|
|
81
105
|
if (cmd) props.onInputLine?.(cmd)
|
|
82
106
|
lineBuf = ''
|
|
83
|
-
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
if (ch === '\x7f' || ch === '\b') {
|
|
84
110
|
lineBuf = lineBuf.slice(0, -1)
|
|
85
|
-
|
|
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 <= '~') {
|
|
86
119
|
lineBuf += ch
|
|
87
120
|
}
|
|
121
|
+
// any other control byte is silently dropped
|
|
88
122
|
}
|
|
89
123
|
session.sendInput(data)
|
|
90
124
|
})
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// PR99b v3.68.0 — Real-shell terminal (single session).
|
|
3
3
|
// PR99c v3.69.0 — Multi-session tabs + browser-local command history.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
// v3.70.2 — explicit composable imports (auto-import was missing the
|
|
5
|
+
// newly added useTerminalThemes on dev servers that didn't restart).
|
|
6
|
+
|
|
7
|
+
import { useTerminalTabs } from '~/composables/useTerminalTabs'
|
|
8
|
+
import { useTerminalThemes } from '~/composables/useTerminalThemes'
|
|
8
9
|
|
|
9
10
|
definePageMeta({ layout: 'default' })
|
|
10
11
|
|
|
@@ -28,11 +29,28 @@ interface HistoryEntry {
|
|
|
28
29
|
cmd: string
|
|
29
30
|
}
|
|
30
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
|
+
|
|
31
47
|
function loadHistory(): HistoryEntry[] {
|
|
32
48
|
if (typeof localStorage === 'undefined') return []
|
|
33
49
|
try {
|
|
34
50
|
const raw = localStorage.getItem(HISTORY_KEY)
|
|
35
|
-
|
|
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))
|
|
36
54
|
} catch {
|
|
37
55
|
return []
|
|
38
56
|
}
|
|
@@ -40,9 +58,18 @@ function loadHistory(): HistoryEntry[] {
|
|
|
40
58
|
|
|
41
59
|
const history = ref<HistoryEntry[]>(loadHistory())
|
|
42
60
|
|
|
61
|
+
function clearHistory() {
|
|
62
|
+
history.value = []
|
|
63
|
+
try {
|
|
64
|
+
localStorage.removeItem(HISTORY_KEY)
|
|
65
|
+
} catch {
|
|
66
|
+
// ignore
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
function recordCommand(cmd: string) {
|
|
44
71
|
const trimmed = cmd.trim()
|
|
45
|
-
if (!trimmed
|
|
72
|
+
if (!isPlausibleCommand(trimmed)) return
|
|
46
73
|
history.value.unshift({ ts: Date.now(), cmd: trimmed })
|
|
47
74
|
if (history.value.length > HISTORY_MAX) {
|
|
48
75
|
history.value = history.value.slice(0, HISTORY_MAX)
|
|
@@ -56,9 +83,11 @@ function recordCommand(cmd: string) {
|
|
|
56
83
|
}
|
|
57
84
|
|
|
58
85
|
// PR99d v3.70.0 — theme picker + Ctrl+R history search.
|
|
86
|
+
// v3.70.3 — proper command palette UX (keyboard nav, selected row).
|
|
59
87
|
const { themeName, setTheme, options: themeOptions } = useTerminalThemes()
|
|
60
88
|
const searchOpen = ref(false)
|
|
61
89
|
const searchQuery = ref('')
|
|
90
|
+
const searchSelectedIdx = ref(0)
|
|
62
91
|
|
|
63
92
|
const searchResults = computed(() => {
|
|
64
93
|
const q = searchQuery.value.trim().toLowerCase()
|
|
@@ -68,9 +97,14 @@ const searchResults = computed(() => {
|
|
|
68
97
|
.slice(0, 30)
|
|
69
98
|
})
|
|
70
99
|
|
|
100
|
+
watch(searchResults, () => {
|
|
101
|
+
searchSelectedIdx.value = 0
|
|
102
|
+
})
|
|
103
|
+
|
|
71
104
|
function openSearch() {
|
|
72
105
|
searchOpen.value = true
|
|
73
106
|
searchQuery.value = ''
|
|
107
|
+
searchSelectedIdx.value = 0
|
|
74
108
|
}
|
|
75
109
|
|
|
76
110
|
function pickFromSearch(cmd: string) {
|
|
@@ -78,6 +112,30 @@ function pickFromSearch(cmd: string) {
|
|
|
78
112
|
searchOpen.value = false
|
|
79
113
|
}
|
|
80
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
|
+
|
|
81
139
|
const editingTabId = ref<string | null>(null)
|
|
82
140
|
const renameDraft = ref('')
|
|
83
141
|
|
|
@@ -288,33 +346,111 @@ const showHistory = ref(false)
|
|
|
288
346
|
History stays in this browser only. Ctrl+R to search history.
|
|
289
347
|
</footer>
|
|
290
348
|
|
|
291
|
-
<UModal
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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"
|
|
307
379
|
>
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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>
|
|
312
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>
|
|
313
423
|
</div>
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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>
|
|
318
454
|
</template>
|
|
319
455
|
</UModal>
|
|
320
456
|
</div>
|
package/package.json
CHANGED