diff --git a/apps/web/src/components/AppShell.tsx b/apps/web/src/components/AppShell.tsx
index 8292831..28721bb 100644
--- a/apps/web/src/components/AppShell.tsx
+++ b/apps/web/src/components/AppShell.tsx
@@ -31,6 +31,7 @@ export function AppShell() {
const railCollapsed = useWorkspace((state) => state.railCollapsed);
const railWidth = useWorkspace((state) => state.railWidth);
const railOverlayOpen = useWorkspace((state) => state.railOverlayOpen);
+ const focusMode = useWorkspace((state) => state.focusMode);
const sessionListView = useWorkspace((state) => state.sessionListView);
const projectExpandedById = useWorkspace((state) => state.projectExpandedById);
const dismissedEmptyProjectIds = useWorkspace((state) => state.dismissedEmptyProjectIds);
@@ -105,7 +106,12 @@ export function AppShell() {
// Dialog-based surfaces (palette, sheets) close themselves; the
// docked pane is plain layout, so Escape peels it here.
const state = workspaceStore.getState();
- if (state.paletteOpen || state.railOverlayOpen) return;
+ if (state.paletteOpen || (state.railOverlayOpen && !state.focusMode)) return;
+ if (state.focusMode) {
+ state.setFocusMode(false);
+ event.preventDefault();
+ return;
+ }
const activeId = state.activeSessionId;
if (activeId !== null && selectSessionView(state, activeId).paneOpen) {
state.setPaneOpen(activeId, false);
@@ -123,8 +129,26 @@ export function AppShell() {
if (action.kind === "palette") {
state.setPaletteOpen(!state.paletteOpen);
} else if (action.kind === "toggle-rail") {
- if (railOverlaid) state.setRailOverlayOpen(!state.railOverlayOpen);
+ if (state.focusMode) {
+ state.setFocusMode(false);
+ state.setRailCollapsed(false);
+ } else if (railOverlaid) state.setRailOverlayOpen(!state.railOverlayOpen);
else state.setRailCollapsed(!state.railCollapsed);
+ } else if (action.kind === "toggle-terminal") {
+ const activeId = state.activeSessionId;
+ const activeSession =
+ activeId === null ? undefined : getShellData().sessions.find((session) => session.id === activeId);
+ if (activeId !== null && activeSession?.archivedAt === undefined) {
+ const view = selectSessionView(state, activeId);
+ if (state.focusMode) {
+ state.setFocusMode(false);
+ state.setTerminalDrawerOpen(activeId, true);
+ } else {
+ state.setTerminalDrawerOpen(activeId, !view.terminalDrawerOpen);
+ }
+ }
+ } else if (action.kind === "toggle-focus") {
+ state.setFocusMode(!state.focusMode);
} else if (action.kind === "settings") {
void navigate({ to: "/settings" });
} else {
@@ -153,24 +177,29 @@ export function AppShell() {
const railToggle = resolveRailTogglePresentation({
overlaid: railOverlaid,
overlayOpen: railOverlayOpen,
- collapsed: railCollapsed,
+ collapsed: railCollapsed || focusMode,
});
return (
workspaceStore.getState().setFocusMode(false)}
onToggleRail={() => {
const state = workspaceStore.getState();
- if (railOverlaid) state.setRailOverlayOpen(!state.railOverlayOpen);
+ if (state.focusMode) {
+ state.setFocusMode(false);
+ state.setRailCollapsed(false);
+ } else if (railOverlaid) state.setRailOverlayOpen(!state.railOverlayOpen);
else state.setRailCollapsed(!state.railCollapsed);
}}
railToggle={railToggle}
/>
- {!railOverlaid && (
+ {!railOverlaid && !focusMode && (
<>
{railCollapsed ? (
@@ -219,7 +248,7 @@ export function AppShell() {
{railOverlaid && (
workspaceStore.getState().setRailOverlayOpen(open)}
- open={railOverlayOpen}
+ open={railOverlayOpen && !focusMode}
>
- Sessions
+ T4 Code
Working folders and sessions
void;
}
+const GROUP_LABEL = {
+ recent: "Recent work",
+ workspace: "Workspace",
+ navigate: "Navigate",
+ app: "App",
+} as const;
+
+const DEFAULT_RECENT_LIMIT = 5;
+
function buildItems(
groups: readonly ProjectGroup[],
navigate: (sessionId: string) => void,
@@ -32,6 +44,7 @@ function buildItems(
for (const row of group.sessions) {
items.push({
id: `session:${row.session.id}`,
+ group: "recent",
label: row.session.title,
hint: `${group.project.name} · ${row.session.model}`,
status:
@@ -43,27 +56,82 @@ function buildItems(
}
}
const state = workspaceStore.getState();
+ const activeSessionId = state.activeSessionId;
+ const activeSessionVisible =
+ activeSessionId !== null &&
+ groups.some((group) => group.sessions.some((row) => row.session.id === activeSessionId));
+ if (activeSessionId !== null && activeSessionVisible) {
+ const view = selectSessionView(state, activeSessionId);
+ for (const meta of PANE_FAMILY_META) {
+ const active = !state.focusMode && view.paneOpen && view.paneFamily === meta.id;
+ const Icon = meta.icon;
+ const label = meta.id === "terminals" ? "Agent terminals" : meta.label;
+ items.push({
+ id: `action:pane:${meta.id}`,
+ group: "workspace",
+ label: active ? `Close ${label}` : `Open ${label}`,
+ hint: "Workspace · Right",
+ status: ,
+ run: () => {
+ const current = workspaceStore.getState();
+ const currentView = selectSessionView(current, activeSessionId);
+ if (current.focusMode) {
+ current.setFocusMode(false);
+ if (!(currentView.paneOpen && currentView.paneFamily === meta.id)) {
+ current.togglePaneFamily(activeSessionId, meta.id);
+ }
+ } else {
+ current.togglePaneFamily(activeSessionId, meta.id);
+ }
+ },
+ });
+ }
+ items.push({
+ id: "action:terminal",
+ group: "workspace",
+ label: !state.focusMode && view.terminalDrawerOpen ? "Close terminal" : "Open terminal",
+ hint: "Workspace · Below · ⌘J",
+ status: ,
+ run: () => {
+ const current = workspaceStore.getState();
+ const currentView = selectSessionView(current, activeSessionId);
+ if (current.focusMode) {
+ current.setFocusMode(false);
+ current.setTerminalDrawerOpen(activeSessionId, true);
+ } else {
+ current.setTerminalDrawerOpen(activeSessionId, !currentView.terminalDrawerOpen);
+ }
+ },
+ });
+ }
items.push(
{
- id: "action:theme",
- label:
- resolveTheme(state.theme) === "dark" ? "Switch to light colors" : "Switch to dark colors",
- hint: "Appearance",
+ id: "action:focus",
+ group: "workspace",
+ label: state.focusMode ? "Exit focus mode" : "Enter focus mode",
+ hint: "⌘⇧F",
status: null,
- run: () => {
- const current = resolveTheme(workspaceStore.getState().theme);
- workspaceStore.getState().setTheme(current === "dark" ? "light" : "dark");
- },
+ run: () => workspaceStore.getState().setFocusMode(!state.focusMode),
},
{
id: "action:rail",
- label: state.railCollapsed ? "Show session list" : "Hide session list",
- hint: "Layout",
+ group: "workspace",
+ label: state.focusMode || state.railCollapsed ? "Show session list" : "Hide session list",
+ hint: "Sidebar",
status: null,
- run: () => workspaceStore.getState().setRailCollapsed(!state.railCollapsed),
+ run: () => {
+ const current = workspaceStore.getState();
+ if (current.focusMode) {
+ current.setFocusMode(false);
+ current.setRailCollapsed(false);
+ } else {
+ current.setRailCollapsed(!current.railCollapsed);
+ }
+ },
},
{
id: "action:inbox",
+ group: "navigate",
label: "Open Inbox",
hint: "Attention across sessions",
status: null,
@@ -71,6 +139,7 @@ function buildItems(
},
{
id: "action:transcript-search",
+ group: "navigate",
label: "Open transcript search",
hint: "Prior decisions and code discussions",
status: ,
@@ -78,6 +147,7 @@ function buildItems(
},
{
id: "action:agents",
+ group: "navigate",
label: "Open Agent View",
hint: "Agents",
status: null,
@@ -85,11 +155,24 @@ function buildItems(
},
{
id: "action:settings",
+ group: "app",
label: "Open settings",
- hint: "App",
+ hint: "Preferences",
status: null,
run: openSettings,
},
+ {
+ id: "action:theme",
+ group: "app",
+ label:
+ resolveTheme(state.theme) === "dark" ? "Switch to light colors" : "Switch to dark colors",
+ hint: "Appearance",
+ status: null,
+ run: () => {
+ const current = resolveTheme(workspaceStore.getState().theme);
+ workspaceStore.getState().setTheme(current === "dark" ? "light" : "dark");
+ },
+ },
);
return items;
}
@@ -126,9 +209,15 @@ export function CommandPalette({ groups }: { groups: readonly ProjectGroup[] })
);
const needle = query.trim().toLowerCase();
+ const defaultRecentIds = new Set(
+ items
+ .filter((item) => item.group === "recent")
+ .slice(0, DEFAULT_RECENT_LIMIT)
+ .map((item) => item.id),
+ );
const baseFiltered =
needle === ""
- ? items
+ ? items.filter((item) => item.group !== "recent" || defaultRecentIds.has(item.id))
: items.filter((item) => `${item.label} ${item.hint}`.toLowerCase().includes(needle));
const filtered =
needle.length < 2
@@ -137,6 +226,7 @@ export function CommandPalette({ groups }: { groups: readonly ProjectGroup[] })
...baseFiltered,
{
id: "action:transcript-search-query",
+ group: "navigate" as const,
label: "View all transcript results",
hint: `Search for “${query.trim()}”`,
status: ,
@@ -177,33 +267,39 @@ export function CommandPalette({ groups }: { groups: readonly ProjectGroup[] })
className="w-full max-w-lg overflow-hidden p-0"
showCloseButton={false}
>
- 0 ? `palette-item-${highlighted}` : undefined}
- aria-controls="palette-results"
- aria-expanded="true"
- autoFocus
- className="h-11 w-full border-border border-b bg-transparent px-4 text-foreground text-sm outline-none placeholder:text-muted-foreground"
- onChange={(event) => setQuery(event.target.value)}
- onKeyDown={(event) => {
- if (event.key === "ArrowDown") {
- event.preventDefault();
- setHighlighted((index) => Math.min(index + 1, filtered.length - 1));
- } else if (event.key === "ArrowUp") {
- event.preventDefault();
- setHighlighted((index) => Math.max(index - 1, 0));
- } else if (event.key === "Enter") {
- event.preventDefault();
- runItem(filtered[highlighted]);
- }
- }}
- placeholder="Search sessions, transcripts, and commands"
- role="combobox"
- type="text"
- value={query}
- />
+
+
+ 0 ? `palette-item-${highlighted}` : undefined}
+ aria-controls="palette-results"
+ aria-expanded="true"
+ autoFocus
+ className="h-12 w-full bg-transparent pe-4 ps-10 text-foreground text-sm outline-none placeholder:text-muted-foreground"
+ onChange={(event) => setQuery(event.target.value)}
+ onKeyDown={(event) => {
+ if (event.key === "ArrowDown") {
+ event.preventDefault();
+ setHighlighted((index) => Math.min(index + 1, filtered.length - 1));
+ } else if (event.key === "ArrowUp") {
+ event.preventDefault();
+ setHighlighted((index) => Math.max(index - 1, 0));
+ } else if (event.key === "Enter") {
+ event.preventDefault();
+ runItem(filtered[highlighted]);
+ }
+ }}
+ placeholder="Search sessions, transcripts, and commands"
+ role="combobox"
+ type="text"
+ value={query}
+ />
+
)}
- {filtered.map((item, index) => (
- - runItem(item)}
- onMouseMove={() => setHighlighted(index)}
- role="option"
- >
- {item.label}
- {item.hint}
- {item.status}
- {index === highlighted && (
-
- )}
-
- ))}
+ {filtered.map((item, index) => {
+ const startsGroup = index === 0 || filtered[index - 1]?.group !== item.group;
+ return (
+
+ {startsGroup && (
+ -
+ {GROUP_LABEL[item.group]}
+
+ )}
+ - runItem(item)}
+ onMouseMove={() => setHighlighted(index)}
+ role="option"
+ >
+ {item.label}
+ {item.hint}
+ {item.status}
+ {index === highlighted && (
+
+ )}
+
+
+ );
+ })}
+
+
+ ↑↓
+ Navigate
+
+
+ ↵
+ Open
+
+
+ Esc
+ Close
+
+
);
diff --git a/apps/web/src/components/Rail.tsx b/apps/web/src/components/Rail.tsx
index 9555fbd..11c4ad6 100644
--- a/apps/web/src/components/Rail.tsx
+++ b/apps/web/src/components/Rail.tsx
@@ -966,18 +966,18 @@ export function Rail({
return (