fix: credential picker in host modal, fix xterm dimensions crash

- Added credential dropdown to New Host modal (loads from vault API)
- Fixed xterm.js "Cannot read dimensions" crash by guarding fitAddon.fit()
  with requestAnimationFrame and container dimension checks
- Added WebGL context loss handler
- credentialId now passed when creating hosts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-03-13 10:31:39 -04:00
parent 7e347ce378
commit 1f32ce4620
2 changed files with 40 additions and 7 deletions

View File

@ -33,13 +33,23 @@ export function useTerminal() {
term.open(container) term.open(container)
try { try {
term.loadAddon(new WebglAddon()) const webgl = new WebglAddon()
webgl.onContextLoss(() => { webgl.dispose() })
term.loadAddon(webgl)
} catch { } catch {
// WebGL not available, fall back to canvas // WebGL not available, fall back to canvas
} }
// Delay fit until container has layout dimensions
const safeFit = () => {
try {
if (container.offsetWidth > 0 && container.offsetHeight > 0) {
fitAddon.fit() fitAddon.fit()
const resizeObserver = new ResizeObserver(() => fitAddon.fit()) }
} catch { /* ignore fit errors on unmounted containers */ }
}
requestAnimationFrame(safeFit)
const resizeObserver = new ResizeObserver(() => safeFit())
resizeObserver.observe(container) resizeObserver.observe(container)
return { term, fitAddon, searchAddon, resizeObserver } return { term, fitAddon, searchAddon, resizeObserver }

View File

@ -17,6 +17,10 @@ const selectedHost = ref<any>(null)
const hostCredential = ref<any>(null) const hostCredential = ref<any>(null)
const loadingCredential = ref(false) const loadingCredential = ref(false)
// Credentials list for host modal dropdown
const allCredentials = ref<any[]>([])
const allSshKeys = ref<any[]>([])
// Terminal composable for connect-on-click // Terminal composable for connect-on-click
const { createTerminal, connectToHost } = useTerminal() const { createTerminal, connectToHost } = useTerminal()
@ -24,9 +28,18 @@ onMounted(async () => {
await Promise.all([connections.fetchHosts(), connections.fetchTree()]) await Promise.all([connections.fetchHosts(), connections.fetchTree()])
}) })
function openNewHost(groupId?: number) { async function openNewHost(groupId?: number) {
editingHost.value = groupId ? { groupId } : null editingHost.value = groupId ? { groupId } : null
inlineHost.value = { name: '', hostname: '', port: 22, protocol: 'ssh', groupId: groupId || null, tags: '' } inlineHost.value = { name: '', hostname: '', port: 22, protocol: 'ssh', groupId: groupId || null, tags: '', credentialId: null }
// Load credentials for dropdown
try {
const [creds, keys] = await Promise.all([
vault.listCredentials() as Promise<any[]>,
vault.listKeys() as Promise<any[]>,
])
allCredentials.value = creds
allSshKeys.value = keys
} catch { /* ignore */ }
showHostDialog.value = true showHostDialog.value = true
} }
@ -113,7 +126,7 @@ function dismissSavePrompt() {
} }
// Inline modal state // Inline modal state
const inlineHost = ref({ name: '', hostname: '', port: 22, protocol: 'ssh' as 'ssh' | 'rdp', groupId: null as number | null, tags: '' }) const inlineHost = ref({ name: '', hostname: '', port: 22, protocol: 'ssh' as 'ssh' | 'rdp', groupId: null as number | null, tags: '', credentialId: null as number | null })
async function createGroupInline() { async function createGroupInline() {
const nameEl = document.getElementById('grp-name') as HTMLInputElement const nameEl = document.getElementById('grp-name') as HTMLInputElement
@ -134,10 +147,11 @@ async function createHostInline() {
port: inlineHost.value.port, port: inlineHost.value.port,
protocol: inlineHost.value.protocol, protocol: inlineHost.value.protocol,
groupId: inlineHost.value.groupId, groupId: inlineHost.value.groupId,
credentialId: inlineHost.value.credentialId,
tags, tags,
}) })
showHostDialog.value = false showHostDialog.value = false
inlineHost.value = { name: '', hostname: '', port: 22, protocol: 'ssh', groupId: null, tags: '' } inlineHost.value = { name: '', hostname: '', port: 22, protocol: 'ssh', groupId: null, tags: '', credentialId: null }
await connections.fetchTree() await connections.fetchTree()
} }
@ -419,6 +433,15 @@ async function deleteSelectedHost() {
<option value="rdp">RDP</option> <option value="rdp">RDP</option>
</select> </select>
</div> </div>
<div>
<label class="block text-sm text-gray-400 mb-1">Credential</label>
<select v-model="inlineHost.credentialId" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white">
<option :value="null"> None </option>
<option v-for="cred in allCredentials" :key="cred.id" :value="cred.id">
{{ cred.name }} ({{ cred.username || cred.type }})
</option>
</select>
</div>
<div> <div>
<label class="block text-sm text-gray-400 mb-1">Tags <span class="text-gray-600">(comma separated)</span></label> <label class="block text-sm text-gray-400 mb-1">Tags <span class="text-gray-600">(comma separated)</span></label>
<input v-model="inlineHost.tags" type="text" placeholder="ssh, prod, web" <input v-model="inlineHost.tags" type="text" placeholder="ssh, prod, web"