arkaos 3.70.9 → 3.71.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/personas/__pycache__/obsidian_store.cpython-313.pyc +0 -0
- package/core/personas/obsidian_store.py +7 -0
- package/core/terminal/__pycache__/session.cpython-313.pyc +0 -0
- package/core/terminal/session.py +28 -0
- package/dashboard/app/components/TerminalDock.vue +638 -0
- package/dashboard/app/composables/useTerminalDock.ts +88 -0
- package/dashboard/app/composables/useTerminalSession.ts +68 -29
- package/dashboard/app/composables/useTerminalTabs.ts +121 -46
- package/dashboard/app/layouts/default.vue +6 -0
- package/dashboard/app/pages/terminal.vue +25 -531
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +14 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// v3.71.0 — app-wide terminal dock UI state. Module-level singleton so
|
|
2
|
+
// open/maximized/height survive route navigation, and persisted to
|
|
3
|
+
// localStorage so the dock restores "as you left it" across a reload.
|
|
4
|
+
// The dock itself (TerminalDock.vue) is mounted once in the default
|
|
5
|
+
// layout.
|
|
6
|
+
|
|
7
|
+
const STATE_KEY = 'arka-terminal-dock'
|
|
8
|
+
|
|
9
|
+
interface DockState {
|
|
10
|
+
open: boolean
|
|
11
|
+
maximized: boolean
|
|
12
|
+
height: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function loadState(): DockState {
|
|
16
|
+
const fallback: DockState = { open: false, maximized: false, height: 45 }
|
|
17
|
+
if (typeof localStorage === 'undefined') return fallback
|
|
18
|
+
try {
|
|
19
|
+
const s = JSON.parse(localStorage.getItem(STATE_KEY) || '{}')
|
|
20
|
+
return {
|
|
21
|
+
open: Boolean(s.open),
|
|
22
|
+
maximized: Boolean(s.maximized),
|
|
23
|
+
height: s.height >= 20 && s.height <= 95 ? s.height : 45
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
return fallback
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const _initial = loadState()
|
|
31
|
+
const isOpen = ref(_initial.open)
|
|
32
|
+
const isMaximized = ref(_initial.maximized)
|
|
33
|
+
const heightVh = ref(_initial.height)
|
|
34
|
+
|
|
35
|
+
function save() {
|
|
36
|
+
if (typeof localStorage === 'undefined') return
|
|
37
|
+
try {
|
|
38
|
+
localStorage.setItem(
|
|
39
|
+
STATE_KEY,
|
|
40
|
+
JSON.stringify({
|
|
41
|
+
open: isOpen.value,
|
|
42
|
+
maximized: isMaximized.value,
|
|
43
|
+
height: heightVh.value
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
} catch {
|
|
47
|
+
// quota / private mode
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useTerminalDock() {
|
|
52
|
+
function open(opts?: { maximized?: boolean }) {
|
|
53
|
+
isOpen.value = true
|
|
54
|
+
if (opts?.maximized) isMaximized.value = true
|
|
55
|
+
save()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function close() {
|
|
59
|
+
isOpen.value = false
|
|
60
|
+
save()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toggle() {
|
|
64
|
+
if (isOpen.value) close()
|
|
65
|
+
else open()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toggleMaximize() {
|
|
69
|
+
isMaximized.value = !isMaximized.value
|
|
70
|
+
save()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function setHeight(vh: number) {
|
|
74
|
+
heightVh.value = Math.min(95, Math.max(20, Math.round(vh)))
|
|
75
|
+
save()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
isOpen,
|
|
80
|
+
isMaximized,
|
|
81
|
+
heightVh,
|
|
82
|
+
open,
|
|
83
|
+
close,
|
|
84
|
+
toggle,
|
|
85
|
+
toggleMaximize,
|
|
86
|
+
setHeight
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// PR99b v3.68.0 — single PTY session lifecycle.
|
|
2
|
+
// v3.71.0 — added attach(): reconnect to an EXISTING backend session
|
|
3
|
+
// (no POST). The WS connect triggers the server-side scrollback replay,
|
|
4
|
+
// so a client reattaching after a browser reload restores its view.
|
|
2
5
|
//
|
|
3
6
|
// Encapsulates the REST + WebSocket handshake against /api/terminal/*.
|
|
4
7
|
// The composable owns no DOM and no xterm instance — it just produces
|
|
@@ -20,6 +23,7 @@ export interface TerminalSessionHandle {
|
|
|
20
23
|
status: Ref<'idle' | 'connecting' | 'open' | 'closed' | 'error'>
|
|
21
24
|
error: Ref<string | null>
|
|
22
25
|
open: () => Promise<void>
|
|
26
|
+
attach: (sessionId: string) => Promise<void>
|
|
23
27
|
sendInput: (data: string) => void
|
|
24
28
|
sendResize: (cols: number, rows: number) => void
|
|
25
29
|
close: () => Promise<void>
|
|
@@ -27,9 +31,9 @@ export interface TerminalSessionHandle {
|
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export function useTerminalSession(
|
|
30
|
-
apiBaseOverride?: string
|
|
34
|
+
apiBaseOverride?: string
|
|
31
35
|
): TerminalSessionHandle {
|
|
32
|
-
// PR99c v3.69.0 — apiBaseOverride lets
|
|
36
|
+
// PR99c v3.69.0 — apiBaseOverride lets the tab store construct
|
|
33
37
|
// sessions from user-event handlers without re-entering Nuxt's
|
|
34
38
|
// composable context.
|
|
35
39
|
const apiBase = apiBaseOverride ?? useApi().apiBase
|
|
@@ -49,7 +53,7 @@ export function useTerminalSession(
|
|
|
49
53
|
const r = await fetch(`${apiBase}/api/terminal/sessions`, {
|
|
50
54
|
method: 'POST',
|
|
51
55
|
headers: { 'content-type': 'application/json' },
|
|
52
|
-
body: JSON.stringify({ cols: 120, rows: 32 })
|
|
56
|
+
body: JSON.stringify({ cols: 120, rows: 32 })
|
|
53
57
|
})
|
|
54
58
|
if (!r.ok) {
|
|
55
59
|
const body = await r.text()
|
|
@@ -58,6 +62,40 @@ export function useTerminalSession(
|
|
|
58
62
|
return await r.json()
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
async function fetchToken(): Promise<string> {
|
|
66
|
+
const r = await fetch(`${apiBase}/api/terminal/token`)
|
|
67
|
+
if (!r.ok) throw new Error(`token fetch failed: ${r.status}`)
|
|
68
|
+
const body = await r.json()
|
|
69
|
+
return String(body.token || '')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function connect(wsPath: string, token: string) {
|
|
73
|
+
ws = new WebSocket(wsUrl(wsPath, token))
|
|
74
|
+
ws.binaryType = 'arraybuffer'
|
|
75
|
+
ws.onopen = () => {
|
|
76
|
+
status.value = 'open'
|
|
77
|
+
}
|
|
78
|
+
ws.onmessage = (ev) => {
|
|
79
|
+
if (ev.data instanceof ArrayBuffer) {
|
|
80
|
+
const chunk = new Uint8Array(ev.data)
|
|
81
|
+
for (const cb of listeners) cb(chunk)
|
|
82
|
+
} else if (typeof ev.data === 'string') {
|
|
83
|
+
const enc = new TextEncoder().encode(ev.data)
|
|
84
|
+
for (const cb of listeners) cb(enc)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
ws.onerror = () => {
|
|
88
|
+
status.value = 'error'
|
|
89
|
+
error.value = 'websocket error'
|
|
90
|
+
}
|
|
91
|
+
ws.onclose = (ev) => {
|
|
92
|
+
status.value = 'closed'
|
|
93
|
+
if (ev.code !== 1000 && ev.code !== 1005) {
|
|
94
|
+
error.value = `closed (${ev.code}) ${ev.reason || ''}`.trim()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
61
99
|
async function open() {
|
|
62
100
|
if (status.value === 'open' || status.value === 'connecting') return
|
|
63
101
|
status.value = 'connecting'
|
|
@@ -65,30 +103,30 @@ export function useTerminalSession(
|
|
|
65
103
|
try {
|
|
66
104
|
const m = await createSession()
|
|
67
105
|
meta.value = m
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
106
|
+
connect(m.ws_path, m.token)
|
|
107
|
+
} catch (e) {
|
|
108
|
+
status.value = 'error'
|
|
109
|
+
error.value = e instanceof Error ? e.message : String(e)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function attach(sessionId: string) {
|
|
114
|
+
if (status.value === 'open' || status.value === 'connecting') return
|
|
115
|
+
status.value = 'connecting'
|
|
116
|
+
error.value = null
|
|
117
|
+
try {
|
|
118
|
+
const token = await fetchToken()
|
|
119
|
+
const wsPath = `/ws/terminal/${sessionId}`
|
|
120
|
+
meta.value = {
|
|
121
|
+
session_id: sessionId,
|
|
122
|
+
shell: '',
|
|
123
|
+
cwd: '',
|
|
124
|
+
token,
|
|
125
|
+
ws_path: wsPath,
|
|
126
|
+
max_sessions: 0,
|
|
127
|
+
active_count: 0
|
|
91
128
|
}
|
|
129
|
+
connect(wsPath, token)
|
|
92
130
|
} catch (e) {
|
|
93
131
|
status.value = 'error'
|
|
94
132
|
error.value = e instanceof Error ? e.message : String(e)
|
|
@@ -108,14 +146,14 @@ export function useTerminalSession(
|
|
|
108
146
|
async function close() {
|
|
109
147
|
try {
|
|
110
148
|
ws?.close(1000, 'client close')
|
|
111
|
-
} catch
|
|
149
|
+
} catch {
|
|
112
150
|
// ignore
|
|
113
151
|
}
|
|
114
152
|
const id = meta.value?.session_id
|
|
115
153
|
if (id) {
|
|
116
154
|
try {
|
|
117
155
|
await fetch(`${apiBase}/api/terminal/sessions/${id}`, { method: 'DELETE' })
|
|
118
|
-
} catch
|
|
156
|
+
} catch {
|
|
119
157
|
// ignore — best-effort cleanup; backend reaper will catch it
|
|
120
158
|
}
|
|
121
159
|
}
|
|
@@ -135,9 +173,10 @@ export function useTerminalSession(
|
|
|
135
173
|
status,
|
|
136
174
|
error,
|
|
137
175
|
open,
|
|
176
|
+
attach,
|
|
138
177
|
sendInput,
|
|
139
178
|
sendResize,
|
|
140
179
|
close,
|
|
141
|
-
onOutput
|
|
180
|
+
onOutput
|
|
142
181
|
}
|
|
143
182
|
}
|
|
@@ -1,88 +1,134 @@
|
|
|
1
|
-
// PR99c v3.69.0 — multi-session tab manager for
|
|
1
|
+
// PR99c v3.69.0 — multi-session tab manager for the terminal.
|
|
2
|
+
// v3.71.0 — state is now a MODULE-LEVEL singleton (not page-scoped), so
|
|
3
|
+
// tabs + their live PTY sessions survive route navigation. The terminal
|
|
4
|
+
// UI lives in the app-wide dock (TerminalDock.vue), mounted once in the
|
|
5
|
+
// default layout outside <NuxtPage>, so navigating never unmounts it.
|
|
6
|
+
// On a full browser reload the singleton is gone, so reattachOnLoad()
|
|
7
|
+
// reconciles persisted session IDs against the live backend sessions and
|
|
8
|
+
// reconnects (the WS connect replays scrollback — see PR-T1 v3.71.0).
|
|
2
9
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
10
|
+
// Live session handles (which carry refs) are kept in a NON-reactive Map,
|
|
11
|
+
// separate from the reactive `tabs` metadata, so Vue never deep-unwraps
|
|
12
|
+
// their inner refs. SSR-safe: the dashboard runs with ssr:false.
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
useTerminalSession,
|
|
16
|
+
type TerminalSessionHandle
|
|
17
|
+
} from '~/composables/useTerminalSession'
|
|
7
18
|
|
|
8
19
|
export interface TerminalTab {
|
|
9
20
|
id: string
|
|
10
21
|
title: string
|
|
11
|
-
session: ReturnType<typeof useTerminalSession>
|
|
12
22
|
createdAt: number
|
|
13
23
|
hasActivity: boolean
|
|
14
24
|
}
|
|
15
25
|
|
|
16
|
-
const STORAGE_KEY = 'arka-terminal-
|
|
26
|
+
const STORAGE_KEY = 'arka-terminal-tabs'
|
|
17
27
|
const MAX_TABS = 8
|
|
18
28
|
|
|
19
|
-
interface
|
|
29
|
+
interface PersistedTab {
|
|
20
30
|
id: string
|
|
21
31
|
title: string
|
|
32
|
+
sessionId?: string
|
|
22
33
|
}
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
// ─── Module-level singleton state (survives navigation) ──────────────────
|
|
36
|
+
const tabs = ref<TerminalTab[]>([])
|
|
37
|
+
const activeId = ref<string | null>(null)
|
|
38
|
+
const sessions = new Map<string, TerminalSessionHandle>()
|
|
39
|
+
const stopWatchers = new Map<string, () => void>()
|
|
40
|
+
let _apiBase = ''
|
|
41
|
+
let _reattached = false
|
|
42
|
+
|
|
43
|
+
function loadPersisted(): PersistedTab[] {
|
|
44
|
+
if (typeof localStorage === 'undefined') return []
|
|
26
45
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const parsed = JSON.parse(raw) as PersistedTitle[]
|
|
30
|
-
return Object.fromEntries(parsed.map(p => [p.id, p.title]))
|
|
46
|
+
const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
|
|
47
|
+
return Array.isArray(parsed) ? parsed : []
|
|
31
48
|
} catch {
|
|
32
|
-
return
|
|
49
|
+
return []
|
|
33
50
|
}
|
|
34
51
|
}
|
|
35
52
|
|
|
36
|
-
function
|
|
53
|
+
function persist() {
|
|
37
54
|
if (typeof localStorage === 'undefined') return
|
|
38
55
|
try {
|
|
39
|
-
const payload:
|
|
56
|
+
const payload: PersistedTab[] = tabs.value.map(t => ({
|
|
57
|
+
id: t.id,
|
|
58
|
+
title: t.title,
|
|
59
|
+
sessionId: sessions.get(t.id)?.meta.value?.session_id
|
|
60
|
+
}))
|
|
40
61
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload))
|
|
41
62
|
} catch {
|
|
42
63
|
// quota / private mode — swallow
|
|
43
64
|
}
|
|
44
65
|
}
|
|
45
66
|
|
|
67
|
+
// Light the activity dot on an inactive tab when its session emits output.
|
|
68
|
+
function markActivity(id: string) {
|
|
69
|
+
if (activeId.value === id) return
|
|
70
|
+
const t = tabs.value.find(t => t.id === id)
|
|
71
|
+
if (t) t.hasActivity = true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Register a handle: persist again once its backend session_id lands (it
|
|
75
|
+
// arrives asynchronously after the WS connects), and flag background tabs
|
|
76
|
+
// that produce output. Both subscriptions are torn down in unregister().
|
|
77
|
+
function register(id: string, handle: TerminalSessionHandle) {
|
|
78
|
+
sessions.set(id, handle)
|
|
79
|
+
const stopWatch = watch(() => handle.meta.value?.session_id, () => persist())
|
|
80
|
+
const stopOutput = handle.onOutput(() => markActivity(id))
|
|
81
|
+
stopWatchers.set(id, () => {
|
|
82
|
+
stopWatch()
|
|
83
|
+
stopOutput()
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function unregister(id: string) {
|
|
88
|
+
stopWatchers.get(id)?.()
|
|
89
|
+
stopWatchers.delete(id)
|
|
90
|
+
sessions.delete(id)
|
|
91
|
+
}
|
|
92
|
+
|
|
46
93
|
export function useTerminalTabs() {
|
|
47
|
-
|
|
48
|
-
const tabs = ref<TerminalTab[]>([])
|
|
49
|
-
const activeId = ref<string | null>(null)
|
|
94
|
+
if (!_apiBase) _apiBase = useApi().apiBase
|
|
50
95
|
const capReached = computed(() => tabs.value.length >= MAX_TABS)
|
|
51
96
|
|
|
52
97
|
function makeId(): string {
|
|
53
98
|
return Math.random().toString(36).slice(2, 10)
|
|
54
99
|
}
|
|
55
100
|
|
|
101
|
+
function sessionFor(id: string): TerminalSessionHandle | undefined {
|
|
102
|
+
return sessions.get(id)
|
|
103
|
+
}
|
|
104
|
+
|
|
56
105
|
function newTab(): TerminalTab | null {
|
|
57
106
|
if (tabs.value.length >= MAX_TABS) return null
|
|
58
107
|
const id = makeId()
|
|
59
|
-
|
|
60
|
-
const titleFromStorage = persistedTitles[id]
|
|
108
|
+
register(id, useTerminalSession(_apiBase))
|
|
61
109
|
const tab: TerminalTab = {
|
|
62
110
|
id,
|
|
63
|
-
title:
|
|
64
|
-
session: useTerminalSession(apiBase),
|
|
111
|
+
title: `Session ${tabs.value.length + 1}`,
|
|
65
112
|
createdAt: Date.now(),
|
|
66
|
-
hasActivity: false
|
|
113
|
+
hasActivity: false
|
|
67
114
|
}
|
|
68
115
|
tabs.value.push(tab)
|
|
69
116
|
activeId.value = id
|
|
70
|
-
|
|
117
|
+
persist()
|
|
71
118
|
return tab
|
|
72
119
|
}
|
|
73
120
|
|
|
74
121
|
async function closeTab(id: string) {
|
|
75
122
|
const idx = tabs.value.findIndex(t => t.id === id)
|
|
76
123
|
if (idx < 0) return
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
await tab.session.close()
|
|
124
|
+
await sessions.get(id)?.close()
|
|
125
|
+
unregister(id)
|
|
80
126
|
tabs.value.splice(idx, 1)
|
|
81
127
|
if (activeId.value === id) {
|
|
82
128
|
const next = tabs.value[Math.min(idx, tabs.value.length - 1)]
|
|
83
129
|
activeId.value = next ? next.id : null
|
|
84
130
|
}
|
|
85
|
-
|
|
131
|
+
persist()
|
|
86
132
|
}
|
|
87
133
|
|
|
88
134
|
function switchTab(id: string) {
|
|
@@ -96,35 +142,64 @@ export function useTerminalTabs() {
|
|
|
96
142
|
const t = tabs.value.find(t => t.id === id)
|
|
97
143
|
if (!t) return
|
|
98
144
|
t.title = title.trim() || t.title
|
|
99
|
-
|
|
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
|
|
145
|
+
persist()
|
|
106
146
|
}
|
|
107
147
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
148
|
+
// After a full browser reload the singleton is empty. Reconcile the
|
|
149
|
+
// persisted session IDs against the live backend sessions and reattach
|
|
150
|
+
// the survivors; the WS connect replays their scrollback. Dead IDs are
|
|
151
|
+
// dropped. Runs at most once per page load.
|
|
152
|
+
async function reattachOnLoad() {
|
|
153
|
+
if (_reattached) return
|
|
154
|
+
_reattached = true
|
|
155
|
+
const persisted = loadPersisted()
|
|
156
|
+
if (persisted.length === 0) return
|
|
157
|
+
const alive = new Set<string>()
|
|
158
|
+
try {
|
|
159
|
+
const r = await fetch(`${_apiBase}/api/terminal/sessions`)
|
|
160
|
+
if (!r.ok) return
|
|
161
|
+
const data = await r.json()
|
|
162
|
+
for (const s of data.sessions || []) alive.add(s.session_id)
|
|
163
|
+
} catch {
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
const restore = persisted
|
|
167
|
+
.filter(p => p.sessionId && alive.has(p.sessionId))
|
|
168
|
+
.slice(0, MAX_TABS)
|
|
169
|
+
for (const p of restore) {
|
|
170
|
+
const handle = useTerminalSession(_apiBase)
|
|
171
|
+
register(p.id, handle)
|
|
172
|
+
tabs.value.push({
|
|
173
|
+
id: p.id,
|
|
174
|
+
title: p.title,
|
|
175
|
+
createdAt: Date.now(),
|
|
176
|
+
hasActivity: false
|
|
177
|
+
})
|
|
178
|
+
// Sets status to 'connecting' synchronously, so Terminal.vue's
|
|
179
|
+
// auto-open() no-ops and we reuse the live PTY instead of spawning.
|
|
180
|
+
void handle.attach(p.sessionId as string)
|
|
181
|
+
}
|
|
182
|
+
if (tabs.value.length > 0 && !activeId.value) {
|
|
183
|
+
activeId.value = tabs.value[0]!.id
|
|
184
|
+
}
|
|
185
|
+
persist()
|
|
113
186
|
}
|
|
114
187
|
|
|
115
|
-
const
|
|
188
|
+
const activeSession = computed(() =>
|
|
189
|
+
activeId.value ? sessions.get(activeId.value) ?? null : null
|
|
190
|
+
)
|
|
116
191
|
|
|
117
192
|
return {
|
|
118
193
|
tabs,
|
|
119
194
|
activeId,
|
|
120
|
-
|
|
195
|
+
activeSession,
|
|
121
196
|
capReached,
|
|
122
197
|
maxTabs: MAX_TABS,
|
|
198
|
+
sessionFor,
|
|
123
199
|
newTab,
|
|
124
200
|
closeTab,
|
|
125
201
|
switchTab,
|
|
126
202
|
renameTab,
|
|
127
|
-
|
|
128
|
-
closeAll,
|
|
203
|
+
reattachOnLoad
|
|
129
204
|
}
|
|
130
205
|
}
|
|
@@ -176,5 +176,11 @@ const links = [[{
|
|
|
176
176
|
<KeyboardShortcutsHelp />
|
|
177
177
|
<GlobalSearch />
|
|
178
178
|
<OnboardingTour />
|
|
179
|
+
<!-- v3.71.0 — app-wide terminal dock. Mounted once here (outside
|
|
180
|
+
<NuxtPage>) so PTY sessions survive route navigation. Client-only
|
|
181
|
+
because xterm.js + WebSocket have no SSR. -->
|
|
182
|
+
<ClientOnly>
|
|
183
|
+
<TerminalDock />
|
|
184
|
+
</ClientOnly>
|
|
179
185
|
</UDashboardGroup>
|
|
180
186
|
</template>
|