Skip to content

Commit a4c787c

Browse files
Merge branch 'fix-new-terminal-tab-creation-d0j9'
2 parents af3b506 + c5a086e commit a4c787c

4 files changed

Lines changed: 42 additions & 7 deletions

File tree

frontend/hooks/use-terminal-view-state.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface TerminalViewState {
1919
}
2020

2121
interface PendingUpdates {
22+
activeTabId?: string | null
2223
focusedTerminals?: FocusedTerminalsMap
2324
currentView?: string | null
2425
currentTaskId?: string | null
@@ -93,7 +94,7 @@ export function useTerminalViewState() {
9394
queryClient.setQueryData(['terminal-view-state'], (current: TerminalViewState | undefined) => {
9495
const currentState = current ?? DEFAULT_VIEW_STATE
9596
return {
96-
activeTabId: currentState.activeTabId, // Deprecated: tab state is now in URL
97+
activeTabId: merged.activeTabId !== undefined ? merged.activeTabId : currentState.activeTabId,
9798
focusedTerminals: {
9899
...currentState.focusedTerminals,
99100
...merged.focusedTerminals,
@@ -173,10 +174,11 @@ export function useTerminalViewState() {
173174

174175
// Update view tracking (for route changes)
175176
const updateViewTracking = useCallback(
176-
(currentView: string, currentTaskId: string | null) => {
177+
(currentView: string, currentTaskId: string | null, activeTabId?: string | null) => {
177178
updateViewState({
178179
currentView,
179180
currentTaskId,
181+
activeTabId,
180182
viewUpdatedAt: new Date().toISOString(),
181183
})
182184
},

frontend/routes/__root.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,20 @@ function ViewTracking() {
3939
const path = location.pathname
4040
let currentView = 'other'
4141
let currentTaskId: string | null = null
42+
let activeTabId: string | null = null
4243

4344
if (path.startsWith('/tasks/')) {
4445
currentView = 'task-detail'
4546
currentTaskId = path.split('/')[2] || null
4647
} else if (path === '/terminals') {
4748
currentView = 'terminals'
49+
// Extract the active terminal tab from URL search params
50+
const params = new URLSearchParams(location.search)
51+
activeTabId = params.get('tab')
4852
}
4953

50-
updateViewTracking(currentView, currentTaskId)
51-
}, [location.pathname, updateViewTracking])
54+
updateViewTracking(currentView, currentTaskId, activeTabId)
55+
}, [location.pathname, location.search, updateViewTracking])
5256

5357
return null
5458
}

frontend/routes/terminals/index.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ const TerminalsView = observer(function TerminalsView() {
9191
setupImagePaste,
9292
writeToTerminal,
9393
sendInputToTerminal,
94+
newTerminalIds,
9495
} = useTerminalStore()
9596

9697
// State for tab edit dialog
@@ -114,8 +115,9 @@ const TerminalsView = observer(function TerminalsView() {
114115
)
115116

116117
// Redirect to last tab (from localStorage) or first tab if URL has no/invalid tab
118+
// Skip if we're in the middle of an intentional navigation (e.g., after creating a new tab)
117119
useEffect(() => {
118-
if (tabs.length > 0 && !isValidTab) {
120+
if (tabs.length > 0 && !isValidTab && !intentionalNavigationRef.current) {
119121
const lastTab = localStorage.getItem(LAST_TAB_STORAGE_KEY)
120122
const targetTab = lastTab && (tabs.some(t => t.id === lastTab) || lastTab === ALL_TASKS_TAB_ID)
121123
? lastTab
@@ -220,6 +222,8 @@ const TerminalsView = observer(function TerminalsView() {
220222
const pendingTabCreateRef = useRef(false)
221223
// Track the number of tabs when we initiated a tab creation to detect new tabs
222224
const tabCountBeforeCreateRef = useRef<number | null>(null)
225+
// Track when we're intentionally navigating to a new tab (prevents localStorage redirect race)
226+
const intentionalNavigationRef = useRef(false)
223227

224228
// Filter terminals for the active tab and convert to TerminalInfo for component compatibility
225229
const visibleTerminals = useMemo(() => {
@@ -325,6 +329,13 @@ const TerminalsView = observer(function TerminalsView() {
325329
curr.position > prev.position ? curr : prev
326330
)
327331

332+
// Don't navigate to temp IDs - they will be replaced with real IDs shortly.
333+
// Wait for the store to replace the temp tab with the real one.
334+
if (newestTab.id.startsWith('temp-')) {
335+
log.terminal.debug('New tab has temp ID, waiting for real ID', { tabId: newestTab.id })
336+
return // Keep waiting - effect will run again when real ID arrives
337+
}
338+
328339
log.terminal.debug('New tab detected, auto-opening', { tabId: newestTab.id, name: newestTab.name })
329340

330341
// Clear the ref to prevent re-triggering
@@ -333,6 +344,11 @@ const TerminalsView = observer(function TerminalsView() {
333344
// Switch to the new tab
334345
setActiveTab(newestTab.id)
335346

347+
// Clear intentional navigation flag after a short delay to allow URL to update
348+
setTimeout(() => {
349+
intentionalNavigationRef.current = false
350+
}, 100)
351+
336352
// Create a terminal inside the new tab after a short delay
337353
// to ensure the tab switch is processed first
338354
setTimeout(() => {
@@ -369,8 +385,16 @@ const TerminalsView = observer(function TerminalsView() {
369385
// Attach xterm to terminal via WebSocket
370386
const cleanup = attachXterm(terminalId, xterm)
371387
cleanupFnsRef.current.set(terminalId, cleanup)
388+
389+
// Auto-focus newly created terminals
390+
if (newTerminalIds.has(terminalId)) {
391+
// Small delay to ensure terminal is fully initialized
392+
setTimeout(() => {
393+
xterm.focus()
394+
}, 50)
395+
}
372396
},
373-
[attachXterm]
397+
[attachXterm, newTerminalIds]
374398
)
375399

376400
const handleTerminalResize = useCallback(
@@ -398,6 +422,8 @@ const TerminalsView = observer(function TerminalsView() {
398422

399423
// Record current tab count to detect when new tab arrives
400424
tabCountBeforeCreateRef.current = tabs.length
425+
// Prevent localStorage redirect from overriding our navigation to the new tab
426+
intentionalNavigationRef.current = true
401427

402428
const name = `Tab ${tabs.length + 1}`
403429
log.terminal.debug('Quick creating tab', { name })
@@ -428,6 +454,8 @@ const TerminalsView = observer(function TerminalsView() {
428454

429455
// Record current tab count to detect when new tab arrives
430456
tabCountBeforeCreateRef.current = tabs.length
457+
// Prevent localStorage redirect from overriding our navigation to the new tab
458+
intentionalNavigationRef.current = true
431459

432460
log.terminal.debug('Creating tab', { name, directory })
433461
createTab(name, undefined, directory)

server/routes/terminal-view-state.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ app.patch('/', async (c) => {
6868

6969
if (body.activeTabId !== undefined) {
7070
// Validate that the tab exists (if not null)
71-
if (body.activeTabId !== null) {
71+
// 'all-tasks' is a virtual tab, not stored in database
72+
if (body.activeTabId !== null && body.activeTabId !== 'all-tasks') {
7273
const tab = db.select().from(terminalTabs).where(eq(terminalTabs.id, body.activeTabId)).get()
7374
if (!tab) {
7475
return c.json({ error: 'Tab not found' }, 404)

0 commit comments

Comments
 (0)