arkaos 3.68.0 → 3.69.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 +1 -1
- package/core/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/audit.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/session.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/token.cpython-313.pyc +0 -0
- package/dashboard/app/components/Terminal.vue +30 -6
- package/dashboard/app/composables/useTerminalSession.ts +7 -2
- package/dashboard/app/composables/useTerminalTabs.ts +130 -0
- package/dashboard/app/pages/terminal.vue +212 -18
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.69.0
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// PR99b v3.68.0 — xterm.js terminal mount.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// will spawn N of these.
|
|
3
|
+
// PR99c v3.69.0 — accepts an external session via prop so the tab
|
|
4
|
+
// manager in /terminal can mount N instances, each bound to its own
|
|
5
|
+
// PTY session.
|
|
7
6
|
|
|
8
7
|
import { Terminal as XTerm } from '@xterm/xterm'
|
|
9
8
|
import { FitAddon } from '@xterm/addon-fit'
|
|
@@ -11,8 +10,14 @@ import { WebLinksAddon } from '@xterm/addon-web-links'
|
|
|
11
10
|
import { SearchAddon } from '@xterm/addon-search'
|
|
12
11
|
import '@xterm/xterm/css/xterm.css'
|
|
13
12
|
|
|
13
|
+
interface Props {
|
|
14
|
+
session?: ReturnType<typeof useTerminalSession>
|
|
15
|
+
onInputLine?: (line: string) => void
|
|
16
|
+
}
|
|
17
|
+
const props = defineProps<Props>()
|
|
18
|
+
|
|
14
19
|
const container = ref<HTMLDivElement | null>(null)
|
|
15
|
-
const session = useTerminalSession()
|
|
20
|
+
const session = props.session ?? useTerminalSession()
|
|
16
21
|
const term = shallowRef<XTerm | null>(null)
|
|
17
22
|
const fit = shallowRef<FitAddon | null>(null)
|
|
18
23
|
const search = shallowRef<SearchAddon | null>(null)
|
|
@@ -77,7 +82,22 @@ onMounted(async () => {
|
|
|
77
82
|
t.write(text)
|
|
78
83
|
})
|
|
79
84
|
|
|
85
|
+
// PR99c v3.69.0 — line-buffer for command history without server-
|
|
86
|
+
// side audit. Captures only printable chars up to Enter; ignores
|
|
87
|
+
// arrow keys, ctrl combos, escape sequences.
|
|
88
|
+
let lineBuf = ''
|
|
80
89
|
t.onData((data) => {
|
|
90
|
+
for (const ch of data) {
|
|
91
|
+
if (ch === '\r' || ch === '\n') {
|
|
92
|
+
const cmd = lineBuf.trim()
|
|
93
|
+
if (cmd) props.onInputLine?.(cmd)
|
|
94
|
+
lineBuf = ''
|
|
95
|
+
} else if (ch === '\x7f' || ch === '\b') {
|
|
96
|
+
lineBuf = lineBuf.slice(0, -1)
|
|
97
|
+
} else if (ch >= ' ' && ch < '\x7f') {
|
|
98
|
+
lineBuf += ch
|
|
99
|
+
}
|
|
100
|
+
}
|
|
81
101
|
session.sendInput(data)
|
|
82
102
|
})
|
|
83
103
|
|
|
@@ -103,7 +123,11 @@ onMounted(async () => {
|
|
|
103
123
|
onBeforeUnmount(async () => {
|
|
104
124
|
unsubscribeOutput?.()
|
|
105
125
|
resizeObserver?.disconnect()
|
|
106
|
-
|
|
126
|
+
// PR99c: only close the session if we created it. When the parent
|
|
127
|
+
// owns the session (props.session), parent is responsible for close.
|
|
128
|
+
if (!props.session) {
|
|
129
|
+
await session.close()
|
|
130
|
+
}
|
|
107
131
|
term.value?.dispose()
|
|
108
132
|
})
|
|
109
133
|
|
|
@@ -26,8 +26,13 @@ export interface TerminalSessionHandle {
|
|
|
26
26
|
onOutput: (cb: (chunk: Uint8Array) => void) => () => void
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export function useTerminalSession(
|
|
30
|
-
|
|
29
|
+
export function useTerminalSession(
|
|
30
|
+
apiBaseOverride?: string,
|
|
31
|
+
): TerminalSessionHandle {
|
|
32
|
+
// PR99c v3.69.0 — apiBaseOverride lets useTerminalTabs construct
|
|
33
|
+
// sessions from user-event handlers without re-entering Nuxt's
|
|
34
|
+
// composable context.
|
|
35
|
+
const apiBase = apiBaseOverride ?? useApi().apiBase
|
|
31
36
|
const meta = ref<TerminalSessionMeta | null>(null)
|
|
32
37
|
const status = ref<'idle' | 'connecting' | 'open' | 'closed' | 'error'>('idle')
|
|
33
38
|
const error = ref<string | null>(null)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// PR99c v3.69.0 — multi-session tab manager for /terminal.
|
|
2
|
+
//
|
|
3
|
+
// Owns an array of `useTerminalSession()` handles plus the active tab
|
|
4
|
+
// id. Persists tab titles (not PTYs — those are re-created fresh on
|
|
5
|
+
// reload) to localStorage. Enforces an 8-tab cap (matching the backend
|
|
6
|
+
// default).
|
|
7
|
+
|
|
8
|
+
export interface TerminalTab {
|
|
9
|
+
id: string
|
|
10
|
+
title: string
|
|
11
|
+
session: ReturnType<typeof useTerminalSession>
|
|
12
|
+
createdAt: number
|
|
13
|
+
hasActivity: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const STORAGE_KEY = 'arka-terminal-tab-titles'
|
|
17
|
+
const MAX_TABS = 8
|
|
18
|
+
|
|
19
|
+
interface PersistedTitle {
|
|
20
|
+
id: string
|
|
21
|
+
title: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadPersistedTitles(): Record<string, string> {
|
|
25
|
+
if (typeof localStorage === 'undefined') return {}
|
|
26
|
+
try {
|
|
27
|
+
const raw = localStorage.getItem(STORAGE_KEY)
|
|
28
|
+
if (!raw) return {}
|
|
29
|
+
const parsed = JSON.parse(raw) as PersistedTitle[]
|
|
30
|
+
return Object.fromEntries(parsed.map(p => [p.id, p.title]))
|
|
31
|
+
} catch {
|
|
32
|
+
return {}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function persistTitles(tabs: TerminalTab[]) {
|
|
37
|
+
if (typeof localStorage === 'undefined') return
|
|
38
|
+
try {
|
|
39
|
+
const payload: PersistedTitle[] = tabs.map(t => ({ id: t.id, title: t.title }))
|
|
40
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload))
|
|
41
|
+
} catch {
|
|
42
|
+
// quota / private mode — swallow
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useTerminalTabs() {
|
|
47
|
+
const { apiBase } = useApi()
|
|
48
|
+
const tabs = ref<TerminalTab[]>([])
|
|
49
|
+
const activeId = ref<string | null>(null)
|
|
50
|
+
const capReached = computed(() => tabs.value.length >= MAX_TABS)
|
|
51
|
+
|
|
52
|
+
function makeId(): string {
|
|
53
|
+
return Math.random().toString(36).slice(2, 10)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function newTab(): TerminalTab | null {
|
|
57
|
+
if (tabs.value.length >= MAX_TABS) return null
|
|
58
|
+
const id = makeId()
|
|
59
|
+
const persistedTitles = loadPersistedTitles()
|
|
60
|
+
const titleFromStorage = persistedTitles[id]
|
|
61
|
+
const tab: TerminalTab = {
|
|
62
|
+
id,
|
|
63
|
+
title: titleFromStorage || `Session ${tabs.value.length + 1}`,
|
|
64
|
+
session: useTerminalSession(apiBase),
|
|
65
|
+
createdAt: Date.now(),
|
|
66
|
+
hasActivity: false,
|
|
67
|
+
}
|
|
68
|
+
tabs.value.push(tab)
|
|
69
|
+
activeId.value = id
|
|
70
|
+
persistTitles(tabs.value)
|
|
71
|
+
return tab
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function closeTab(id: string) {
|
|
75
|
+
const idx = tabs.value.findIndex(t => t.id === id)
|
|
76
|
+
if (idx < 0) return
|
|
77
|
+
const tab = tabs.value[idx]
|
|
78
|
+
if (!tab) return
|
|
79
|
+
await tab.session.close()
|
|
80
|
+
tabs.value.splice(idx, 1)
|
|
81
|
+
if (activeId.value === id) {
|
|
82
|
+
const next = tabs.value[Math.min(idx, tabs.value.length - 1)]
|
|
83
|
+
activeId.value = next ? next.id : null
|
|
84
|
+
}
|
|
85
|
+
persistTitles(tabs.value)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function switchTab(id: string) {
|
|
89
|
+
const t = tabs.value.find(t => t.id === id)
|
|
90
|
+
if (!t) return
|
|
91
|
+
activeId.value = id
|
|
92
|
+
t.hasActivity = false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renameTab(id: string, title: string) {
|
|
96
|
+
const t = tabs.value.find(t => t.id === id)
|
|
97
|
+
if (!t) return
|
|
98
|
+
t.title = title.trim() || t.title
|
|
99
|
+
persistTitles(tabs.value)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function markActivity(id: string) {
|
|
103
|
+
if (activeId.value === id) return
|
|
104
|
+
const t = tabs.value.find(t => t.id === id)
|
|
105
|
+
if (t) t.hasActivity = true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function closeAll() {
|
|
109
|
+
await Promise.all(tabs.value.map(t => t.session.close()))
|
|
110
|
+
tabs.value = []
|
|
111
|
+
activeId.value = null
|
|
112
|
+
persistTitles([])
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const activeTab = computed(() => tabs.value.find(t => t.id === activeId.value) ?? null)
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
tabs,
|
|
119
|
+
activeId,
|
|
120
|
+
activeTab,
|
|
121
|
+
capReached,
|
|
122
|
+
maxTabs: MAX_TABS,
|
|
123
|
+
newTab,
|
|
124
|
+
closeTab,
|
|
125
|
+
switchTab,
|
|
126
|
+
renameTab,
|
|
127
|
+
markActivity,
|
|
128
|
+
closeAll,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -1,25 +1,135 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// PR99b v3.68.0 — Real-shell terminal (single session).
|
|
3
|
+
// PR99c v3.69.0 — Multi-session tabs + browser-local command history.
|
|
3
4
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// until PR99d, but the UI is gone as of this release.
|
|
5
|
+
// Each tab owns its own PTY session. PTYs are NOT persisted across
|
|
6
|
+
// reloads (they're per-process); only tab titles are. The 8-tab cap
|
|
7
|
+
// matches the backend default.
|
|
8
8
|
|
|
9
9
|
definePageMeta({ layout: 'default' })
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
11
|
+
const {
|
|
12
|
+
tabs,
|
|
13
|
+
activeId,
|
|
14
|
+
activeTab,
|
|
15
|
+
capReached,
|
|
16
|
+
maxTabs,
|
|
17
|
+
newTab,
|
|
18
|
+
closeTab,
|
|
19
|
+
switchTab,
|
|
20
|
+
renameTab,
|
|
21
|
+
} = useTerminalTabs()
|
|
22
|
+
|
|
23
|
+
const HISTORY_KEY = 'arka-terminal-command-history'
|
|
24
|
+
const HISTORY_MAX = 500
|
|
25
|
+
|
|
26
|
+
interface HistoryEntry {
|
|
27
|
+
ts: number
|
|
28
|
+
cmd: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function loadHistory(): HistoryEntry[] {
|
|
32
|
+
if (typeof localStorage === 'undefined') return []
|
|
33
|
+
try {
|
|
34
|
+
const raw = localStorage.getItem(HISTORY_KEY)
|
|
35
|
+
return raw ? (JSON.parse(raw) as HistoryEntry[]) : []
|
|
36
|
+
} catch {
|
|
37
|
+
return []
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const history = ref<HistoryEntry[]>(loadHistory())
|
|
42
|
+
|
|
43
|
+
function recordCommand(cmd: string) {
|
|
44
|
+
const trimmed = cmd.trim()
|
|
45
|
+
if (!trimmed || trimmed.length < 2) return
|
|
46
|
+
history.value.unshift({ ts: Date.now(), cmd: trimmed })
|
|
47
|
+
if (history.value.length > HISTORY_MAX) {
|
|
48
|
+
history.value = history.value.slice(0, HISTORY_MAX)
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
localStorage.setItem(HISTORY_KEY, JSON.stringify(history.value))
|
|
52
|
+
} catch {
|
|
53
|
+
// quota — silently truncate further
|
|
54
|
+
history.value = history.value.slice(0, 200)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const editingTabId = ref<string | null>(null)
|
|
59
|
+
const renameDraft = ref('')
|
|
60
|
+
|
|
61
|
+
function startRename(tabId: string, currentTitle: string) {
|
|
62
|
+
editingTabId.value = tabId
|
|
63
|
+
renameDraft.value = currentTitle
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function commitRename() {
|
|
67
|
+
if (editingTabId.value) {
|
|
68
|
+
renameTab(editingTabId.value, renameDraft.value)
|
|
69
|
+
}
|
|
70
|
+
editingTabId.value = null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const toast = useToast()
|
|
74
|
+
|
|
75
|
+
function tryNewTab() {
|
|
76
|
+
if (capReached.value) {
|
|
77
|
+
toast.add({
|
|
78
|
+
title: 'Maximum sessions reached',
|
|
79
|
+
description: `You can have up to ${maxTabs} sessions open at once. Close one to open a new one.`,
|
|
80
|
+
color: 'warning',
|
|
81
|
+
icon: 'i-lucide-alert-triangle',
|
|
82
|
+
})
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
newTab()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Keyboard shortcuts.
|
|
89
|
+
defineShortcuts({
|
|
90
|
+
meta_t: { handler: tryNewTab, usingInput: false },
|
|
91
|
+
meta_w: {
|
|
92
|
+
handler: () => {
|
|
93
|
+
if (activeId.value) closeTab(activeId.value)
|
|
94
|
+
},
|
|
95
|
+
usingInput: false,
|
|
96
|
+
},
|
|
97
|
+
meta_1: { handler: () => switchByIndex(0), usingInput: false },
|
|
98
|
+
meta_2: { handler: () => switchByIndex(1), usingInput: false },
|
|
99
|
+
meta_3: { handler: () => switchByIndex(2), usingInput: false },
|
|
100
|
+
meta_4: { handler: () => switchByIndex(3), usingInput: false },
|
|
101
|
+
meta_5: { handler: () => switchByIndex(4), usingInput: false },
|
|
102
|
+
meta_6: { handler: () => switchByIndex(5), usingInput: false },
|
|
103
|
+
meta_7: { handler: () => switchByIndex(6), usingInput: false },
|
|
104
|
+
meta_8: { handler: () => switchByIndex(7), usingInput: false },
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
function switchByIndex(idx: number) {
|
|
108
|
+
const t = tabs.value[idx]
|
|
109
|
+
if (t) switchTab(t.id)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// First-visit: open one tab automatically.
|
|
113
|
+
onMounted(() => {
|
|
114
|
+
if (tabs.value.length === 0) newTab()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
onBeforeUnmount(async () => {
|
|
118
|
+
// Don't proactively close tabs — operator may navigate back. Backend
|
|
119
|
+
// reaper will GC after the idle timeout (30 min).
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const showHistory = ref(false)
|
|
13
123
|
</script>
|
|
14
124
|
|
|
15
125
|
<template>
|
|
16
126
|
<div class="flex flex-col gap-3 h-full">
|
|
17
|
-
<header class="flex items-center justify-between">
|
|
127
|
+
<header class="flex items-center justify-between gap-3 flex-wrap">
|
|
18
128
|
<div>
|
|
19
129
|
<h1 class="text-2xl font-semibold">Terminal</h1>
|
|
20
130
|
<p class="text-sm text-muted">
|
|
21
|
-
Real shell
|
|
22
|
-
|
|
131
|
+
Real PTY shell — run claude, codex, git, anything. ⌘T new ·
|
|
132
|
+
⌘W close · ⌘1–8 switch.
|
|
23
133
|
</p>
|
|
24
134
|
</div>
|
|
25
135
|
<div class="flex items-center gap-2">
|
|
@@ -30,22 +140,106 @@ const expanded = ref(false)
|
|
|
30
140
|
<UButton
|
|
31
141
|
size="xs"
|
|
32
142
|
variant="ghost"
|
|
33
|
-
:icon="
|
|
34
|
-
@click="
|
|
143
|
+
:icon="showHistory ? 'i-lucide-x' : 'i-lucide-history'"
|
|
144
|
+
@click="showHistory = !showHistory"
|
|
35
145
|
>
|
|
36
|
-
{{
|
|
146
|
+
History ({{ history.length }})
|
|
37
147
|
</UButton>
|
|
38
148
|
</div>
|
|
39
149
|
</header>
|
|
40
150
|
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
151
|
+
<div class="flex items-center gap-1 border-b border-default pb-2 overflow-x-auto">
|
|
152
|
+
<div
|
|
153
|
+
v-for="(tab, idx) in tabs"
|
|
154
|
+
:key="tab.id"
|
|
155
|
+
:class="[
|
|
156
|
+
'group flex items-center gap-1 px-3 py-1.5 rounded-t-md cursor-pointer text-sm shrink-0 border-b-2 transition-colors',
|
|
157
|
+
activeId === tab.id
|
|
158
|
+
? 'bg-elevated/60 border-primary text-default'
|
|
159
|
+
: 'border-transparent text-muted hover:text-default hover:bg-elevated/30',
|
|
160
|
+
]"
|
|
161
|
+
@click="switchTab(tab.id)"
|
|
162
|
+
@dblclick="startRename(tab.id, tab.title)"
|
|
163
|
+
>
|
|
164
|
+
<span class="text-xs text-muted">{{ idx + 1 }}</span>
|
|
165
|
+
<UInput
|
|
166
|
+
v-if="editingTabId === tab.id"
|
|
167
|
+
v-model="renameDraft"
|
|
168
|
+
size="xs"
|
|
169
|
+
autofocus
|
|
170
|
+
@keydown.enter="commitRename"
|
|
171
|
+
@keydown.esc="editingTabId = null"
|
|
172
|
+
@blur="commitRename"
|
|
173
|
+
/>
|
|
174
|
+
<span v-else>{{ tab.title }}</span>
|
|
175
|
+
<UIcon
|
|
176
|
+
v-if="tab.hasActivity && activeId !== tab.id"
|
|
177
|
+
name="i-lucide-circle"
|
|
178
|
+
class="size-2 text-amber-400 fill-current"
|
|
179
|
+
/>
|
|
180
|
+
<button
|
|
181
|
+
class="ml-1 size-4 grid place-items-center rounded text-muted hover:bg-default/50 hover:text-default opacity-0 group-hover:opacity-100"
|
|
182
|
+
@click.stop="closeTab(tab.id)"
|
|
183
|
+
>
|
|
184
|
+
<UIcon name="i-lucide-x" class="size-3" />
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
<UButton
|
|
188
|
+
size="xs"
|
|
189
|
+
variant="ghost"
|
|
190
|
+
icon="i-lucide-plus"
|
|
191
|
+
:disabled="capReached"
|
|
192
|
+
:title="capReached ? `Max ${maxTabs} sessions` : 'New session'"
|
|
193
|
+
@click="tryNewTab"
|
|
194
|
+
>
|
|
195
|
+
New
|
|
196
|
+
</UButton>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div class="flex-1 min-h-[480px] flex gap-3">
|
|
200
|
+
<div class="flex-1 relative">
|
|
201
|
+
<template v-for="tab in tabs" :key="tab.id">
|
|
202
|
+
<Terminal
|
|
203
|
+
v-show="activeId === tab.id"
|
|
204
|
+
:session="tab.session"
|
|
205
|
+
:on-input-line="recordCommand"
|
|
206
|
+
class="absolute inset-0"
|
|
207
|
+
/>
|
|
208
|
+
</template>
|
|
209
|
+
<div
|
|
210
|
+
v-if="tabs.length === 0"
|
|
211
|
+
class="absolute inset-0 grid place-items-center text-muted text-sm"
|
|
212
|
+
>
|
|
213
|
+
No active sessions. Press ⌘T or click "+ New" to open one.
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
<aside
|
|
217
|
+
v-if="showHistory"
|
|
218
|
+
class="w-72 shrink-0 rounded-lg border border-default bg-elevated/10 overflow-hidden flex flex-col"
|
|
219
|
+
>
|
|
220
|
+
<div class="px-3 py-2 border-b border-default text-xs uppercase tracking-wide text-muted">
|
|
221
|
+
Command history
|
|
222
|
+
</div>
|
|
223
|
+
<div class="flex-1 overflow-auto text-xs font-mono">
|
|
224
|
+
<button
|
|
225
|
+
v-for="(entry, i) in history"
|
|
226
|
+
:key="i"
|
|
227
|
+
class="w-full text-left px-3 py-1.5 hover:bg-default/40 truncate"
|
|
228
|
+
:title="entry.cmd"
|
|
229
|
+
@click="activeTab?.session.sendInput(entry.cmd)"
|
|
230
|
+
>
|
|
231
|
+
{{ entry.cmd }}
|
|
232
|
+
</button>
|
|
233
|
+
<div v-if="history.length === 0" class="px-3 py-4 text-muted text-center">
|
|
234
|
+
No commands yet
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</aside>
|
|
238
|
+
</div>
|
|
45
239
|
|
|
46
240
|
<footer class="text-xs text-muted">
|
|
47
|
-
|
|
48
|
-
|
|
241
|
+
Sessions live on the backend until you close them or 30 min idle.
|
|
242
|
+
History stays in this browser only.
|
|
49
243
|
</footer>
|
|
50
244
|
</div>
|
|
51
245
|
</template>
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|