Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions apps/web/src/workbench/surfaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ describe('surfaceTabId', () => {
expect(surfaceTabId(surface('finance'))).toBe('finance:finance')
})

it('is null for panels and non-tab routes', () => {
it('resolves the tab id for a panel surface with a companion route', () => {
expect(surfaceTabId(surface('tasks'))).toBe('tasks:tasks')
})

it('is null for routeless panels and non-tab routes', () => {
expect(surfaceTabId(surface('explorer'))).toBeNull() // panel
expect(surfaceTabId(surface('discover'))).toBeNull() // route, but untabbed
expect(surfaceTabId(surface('analytics'))).toBeNull()
Expand Down Expand Up @@ -91,16 +95,28 @@ describe('activateSurface (VS Code preview tabs, 0288)', () => {
expect(consumePreviewIntent()).toBe(false)
})

it('drives the bottom island for panel surfaces without navigating or arming preview', () => {
it('drives the bottom island for routeless panel surfaces without navigating or arming preview', () => {
const activated: string[] = []
activateSurface(surface('explorer'), {
navigate: () => expect.unreachable('panel surfaces do not navigate'),
navigate: () => expect.unreachable('routeless panel surfaces do not navigate'),
setActiveSurface: (id) => activated.push(id)
})
expect(activated).toEqual(['explorer'])
expect(consumePreviewIntent()).toBe(false)
})

it('drives the bottom island AND opens the board for the Tasks panel surface', () => {
const activated: string[] = []
const calls: Array<{ to: string }> = []
activateSurface(surface('tasks'), {
navigate: (opts) => calls.push(opts),
setActiveSurface: (id) => activated.push(id)
})
expect(activated).toEqual(['tasks'])
expect(calls).toEqual([{ to: '/tasks' }])
expect(consumePreviewIntent()).toBe(true) // /tasks is a singleton tab route
})

it('leaves the latch clean afterwards', () => {
setPreviewIntent()
expect(consumePreviewIntent()).toBe(true)
Expand Down
27 changes: 16 additions & 11 deletions apps/web/src/workbench/surfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ export interface SurfaceDef {
kind: 'panel' | 'route'
/** Slot view id (panel surfaces). */
viewId?: string
/** Route path (route surfaces). */
/**
* Route path. Required for route surfaces; a panel surface may also carry
* one, in which case activating it drives the bottom island *and* opens the
* route in the editor (Tasks → the task board).
*/
to?: string
/** Live count source for the trailing badge. */
badge?: 'requests'
Expand All @@ -58,7 +62,7 @@ export const SURFACES: SurfaceDef[] = [
badge: 'requests',
emphasis: true
},
{ id: 'tasks', label: 'Tasks', icon: CheckSquare2, kind: 'panel', viewId: 'tasks' },
{ id: 'tasks', label: 'Tasks', icon: CheckSquare2, kind: 'panel', viewId: 'tasks', to: '/tasks' },
{ id: 'chats', label: 'Chats', icon: MessageSquare, kind: 'panel', viewId: 'chats' },
{ id: 'today', label: 'Today', icon: Sunrise, kind: 'panel', viewId: 'today' },
{ id: 'data', label: 'Data', icon: Database, kind: 'panel', viewId: 'data' },
Expand All @@ -85,33 +89,34 @@ export function pinnedSurfaces(navPinned: string[]): SurfaceDef[] {
export const DEFAULT_SURFACE = 'explorer'

/**
* The tab a route surface opens/promotes, or null for panel surfaces and
* The tab a surface's route opens/promotes, or null for routeless panels and
* non-tab routes (Discover, Analytics). Lets a surface row promote its tab on
* double-click without knowing the node model.
*/
export function surfaceTabId(surface: SurfaceDef): string | null {
if (surface.kind !== 'route' || !surface.to) return null
if (!surface.to) return null
return tabIdForRoute(surface.to)
}

/**
* Activating a surface: a **panel** drives the contextual bottom island
* (`activeSurface`); a **route** opens in the editor as a VS Code-style preview
* tab (0288) — a single click renders it italic and the next single-click open
* replaces it; double-clicking the row (or editing) promotes it. Pure so the
* decision is testable; the hook below wires the side-effecting deps.
* (`activeSurface`); a surface with a **route** opens it in the editor as a
* VS Code-style preview tab (0288) — a single click renders it italic and the
* next single-click open replaces it; double-clicking the row (or editing)
* promotes it. A panel surface that also carries a route (Tasks) does both.
* Pure so the decision is testable; the hook below wires the side-effecting
* deps.
*/
export function activateSurface(
surface: SurfaceDef,
deps: { navigate: (opts: { to: string }) => void; setActiveSurface: (id: string) => void }
): void {
if (surface.kind === 'route' && surface.to) {
if (surface.kind === 'panel') deps.setActiveSurface(surface.id)
if (surface.to) {
// Only tab routes honour the preview latch; arming it for a non-tab route
// (Discover, Analytics, Inbox) would leave it set for the next navigation.
if (tabIdForRoute(surface.to)) setPreviewIntent()
deps.navigate({ to: surface.to })
} else {
deps.setActiveSurface(surface.id)
}
}

Expand Down
18 changes: 4 additions & 14 deletions apps/web/src/workbench/views/left.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,11 @@ import { TasksDashboard } from './TasksPanel'
import { useSavedViews } from './tray'

export function TasksPanelView() {
// No board link here: activating the Tasks surface already opens the task
// board in the editor (see surfaces.ts).
return (
<div className="flex h-full min-h-0 flex-col">
<div className="min-h-0 flex-1 overflow-y-auto p-2">
<TasksDashboard />
</div>
<div className="shrink-0 border-t border-hairline p-2">
<Link
to="/tasks"
search={{}}
className="flex items-center gap-1.5 px-1 text-xs text-ink-2 no-underline transition-colors hover:text-ink-1 hover:no-underline"
>
Open task board
<ArrowUpRight size={11} strokeWidth={1.5} />
</Link>
</div>
<div className="h-full min-h-0 overflow-y-auto p-2">
<TasksDashboard />
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "2026-07-11-clicking-tasks-opens-the-task-board",
"date": "July 11, 2026",
"title": "Clicking Tasks opens the task board",
"summary": "Selecting Tasks in the sidebar now opens the task board directly alongside the panel, so the extra \"Open task board\" link is gone.",
"highlights": [],
"tags": ["app", "tasks"]
}
Loading