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)
try {
term.loadAddon(new WebglAddon())
const webgl = new WebglAddon()
webgl.onContextLoss(() => { webgl.dispose() })
term.loadAddon(webgl)
} catch {
// 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()
const resizeObserver = new ResizeObserver(() => fitAddon.fit())
}
} catch { /* ignore fit errors on unmounted containers */ }
}
requestAnimationFrame(safeFit)
const resizeObserver = new ResizeObserver(() => safeFit())
resizeObserver.observe(container)
return { term, fitAddon, searchAddon, resizeObserver }

View File

@ -17,6 +17,10 @@ const selectedHost = ref<any>(null)
const hostCredential = ref<any>(null)
const loadingCredential = ref(false)
// Credentials list for host modal dropdown
const allCredentials = ref<any[]>([])
const allSshKeys = ref<any[]>([])
// Terminal composable for connect-on-click
const { createTerminal, connectToHost } = useTerminal()
@ -24,9 +28,18 @@ onMounted(async () => {
await Promise.all([connections.fetchHosts(), connections.fetchTree()])
})
function openNewHost(groupId?: number) {
async function openNewHost(groupId?: number) {
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
}
@ -113,7 +126,7 @@ function dismissSavePrompt() {
}
// 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() {
const nameEl = document.getElementById('grp-name') as HTMLInputElement
@ -134,10 +147,11 @@ async function createHostInline() {
port: inlineHost.value.port,
protocol: inlineHost.value.protocol,
groupId: inlineHost.value.groupId,
credentialId: inlineHost.value.credentialId,
tags,
})
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()
}
@ -419,6 +433,15 @@ async function deleteSelectedHost() {
<option value="rdp">RDP</option>
</select>
</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>
<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"