Skip to content

Commit f0ae44c

Browse files
Use URL search param for cmd+i navigation to Task Terminals
Pass the tab via ?tab=all-tasks search param instead of relying on React Query optimistic updates. This makes the intent synchronous - available on the first render before any effects run, eliminating the jank where the view briefly switched away before switching back.
1 parent f80b605 commit f0ae44c

4 files changed

Lines changed: 32 additions & 14 deletions

File tree

cli/src/commands/dev.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ export async function handleDevCommand(
2020
)
2121
}
2222

23-
output({ status: 'restarting', message: 'Triggering restart (build + restart via systemd)...' })
23+
output({ status: 'restarting', message: 'Triggering restart (build + migrate + restart)...' })
2424
const result = await client.restartVibora()
2525

2626
if (result.error) {
2727
throw new CliError('RESTART_FAILED', result.error, ExitCodes.OPERATION_FAILED)
2828
}
2929

30-
output({ status: 'initiated', message: 'Restart initiated. If build fails, old instance keeps running.' })
30+
output({ status: 'initiated', message: 'Restart initiated. If build or migration fails, old instance keeps running.' })
3131
break
3232
}
3333

server/routes/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,13 @@ app.post('/restart', (c) => {
158158
// Build first in the background, then restart only if successful
159159
// This prevents stopping the old instance if build fails
160160
setTimeout(() => {
161-
spawn('bash', ['-c', 'cd ~/projects/vibora && mise run build && systemctl --user restart vibora-dev'], {
161+
spawn('bash', ['-c', 'cd ~/projects/vibora && mise run build && bun run drizzle-kit push && systemctl --user restart vibora-dev'], {
162162
detached: true,
163163
stdio: 'ignore',
164164
}).unref()
165165
}, 100)
166166

167-
return c.json({ success: true, message: 'Restart initiated (building first)' })
167+
return c.json({ success: true, message: 'Restart initiated (build + migrate + restart)' })
168168
})
169169

170170
// GET /api/config/:key - Get config value

src/components/command-palette/command-palette.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
searchCommands,
1212
groupCommandsByCategory,
1313
} from './command-registry'
14-
import { useTerminalViewState } from '@/hooks/use-terminal-view-state'
1514
import { HugeiconsIcon } from '@hugeicons/react'
1615
import {
1716
GridViewIcon,
@@ -40,7 +39,6 @@ export function CommandPalette({ open: controlledOpen, onOpenChange, onNewTask,
4039
const inputRef = useRef<HTMLInputElement>(null)
4140
const listRef = useRef<HTMLDivElement>(null)
4241
const navigate = useNavigate()
43-
const { setActiveTab } = useTerminalViewState()
4442

4543
// Build command list
4644
const commands = useMemo<Command[]>(() => {
@@ -77,8 +75,7 @@ export function CommandPalette({ open: controlledOpen, onOpenChange, onNewTask,
7775
category: 'navigation',
7876
icon: <HugeiconsIcon icon={GridViewIcon} size={16} strokeWidth={2} />,
7977
action: () => {
80-
setActiveTab('all-tasks')
81-
navigate({ to: '/terminals' })
78+
navigate({ to: '/terminals', search: { tab: 'all-tasks' } })
8279
setOpen(false)
8380
},
8481
},
@@ -144,7 +141,7 @@ export function CommandPalette({ open: controlledOpen, onOpenChange, onNewTask,
144141
},
145142
]
146143
return cmds
147-
}, [navigate, onNewTask, onShowShortcuts, setActiveTab, setOpen, t])
144+
}, [navigate, onNewTask, onShowShortcuts, setOpen, t])
148145

149146
// Filter commands based on query
150147
const filteredCommands = useMemo(
@@ -191,8 +188,7 @@ export function CommandPalette({ open: controlledOpen, onOpenChange, onNewTask,
191188
useHotkeys('meta+1', () => navigate({ to: '/tasks' }), { allowInInput: true, allowInTerminal: true })
192189
useHotkeys('meta+2', () => navigate({ to: '/terminals' }), { allowInInput: true, allowInTerminal: true })
193190
useHotkeys('meta+i', () => {
194-
setActiveTab('all-tasks')
195-
navigate({ to: '/terminals' })
191+
navigate({ to: '/terminals', search: { tab: 'all-tasks' } })
196192
}, { allowInInput: true, allowInTerminal: true })
197193
useHotkeys('meta+3', () => navigate({ to: '/worktrees' }), { allowInInput: true, allowInTerminal: true })
198194
useHotkeys('meta+4', () => navigate({ to: '/repositories' }), { allowInInput: true, allowInTerminal: true })

src/routes/terminals/index.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createFileRoute } from '@tanstack/react-router'
1+
import { createFileRoute, useSearch, useNavigate } from '@tanstack/react-router'
22
import { useCallback, useRef, useEffect, useMemo, useState } from 'react'
33
import { useTranslation } from 'react-i18next'
44
import { TerminalGrid } from '@/components/terminal/terminal-grid'
@@ -26,12 +26,21 @@ import { log } from '@/lib/logger'
2626
const ALL_TASKS_TAB_ID = 'all-tasks'
2727
const ACTIVE_STATUSES: TaskStatus[] = ['IN_PROGRESS', 'IN_REVIEW']
2828

29+
interface TerminalsSearch {
30+
tab?: string
31+
}
32+
2933
export const Route = createFileRoute('/terminals/')({
3034
component: TerminalsView,
35+
validateSearch: (search: Record<string, unknown>): TerminalsSearch => ({
36+
tab: typeof search.tab === 'string' ? search.tab : undefined,
37+
}),
3138
})
3239

3340
function TerminalsView() {
3441
const { t } = useTranslation('terminals')
42+
const navigate = useNavigate()
43+
const { tab: tabFromUrl } = useSearch({ from: '/terminals/' })
3544
const {
3645
terminals,
3746
tabs,
@@ -53,15 +62,28 @@ function TerminalsView() {
5362
// Track active tab via server-persisted state
5463
const { activeTabId, setActiveTab, isLoading: isViewStateLoading } = useTerminalViewState()
5564

65+
// Handle tab from URL search param (e.g., from cmd+i navigation)
66+
// This runs before the fallback effect and clears the URL param
67+
const hasAppliedUrlTab = useRef(false)
68+
useEffect(() => {
69+
if (tabFromUrl && !hasAppliedUrlTab.current) {
70+
hasAppliedUrlTab.current = true
71+
setActiveTab(tabFromUrl)
72+
// Clear the search param from URL without adding to history
73+
navigate({ to: '/terminals', search: {}, replace: true })
74+
}
75+
}, [tabFromUrl, setActiveTab, navigate])
76+
5677
// Ensure activeTabId is valid - set to first tab if invalid
78+
// Skip if we just applied a tab from URL to avoid race condition
5779
useEffect(() => {
58-
if (tabs.length > 0 && !isViewStateLoading) {
80+
if (tabs.length > 0 && !isViewStateLoading && !tabFromUrl) {
5981
const tabIds = tabs.map((t) => t.id)
6082
if (!activeTabId || (!tabIds.includes(activeTabId) && activeTabId !== ALL_TASKS_TAB_ID)) {
6183
setActiveTab(tabs[0].id)
6284
}
6385
}
64-
}, [tabs, activeTabId, isViewStateLoading, setActiveTab])
86+
}, [tabs, activeTabId, isViewStateLoading, setActiveTab, tabFromUrl])
6587

6688
const { data: tasks = [], isLoading: isTasksLoading } = useTasks()
6789
const { data: repositories = [] } = useRepositories()

0 commit comments

Comments
 (0)