Skip to content

Commit c7e6ff6

Browse files
feat: UX overhaul — add project flow, breadcrumbs, status bar, keyboard shortcuts, state persistence, contextual guidance
- Add Project button + folder picker in sidebar, command palette, and hero screen - Better empty states: fresh install onboarding card, no-tab quick actions - Breadcrumb bar: Project > Thread > [branch] > [PR #N] with clickable segments - Tab status dots (green/blue/red), unread badges, pulsing animation - Thread hover tooltips with project, branch, PR, last message preview - Status bar: model, provider, session status, token count - Keyboard shortcuts: Cmd+1-9 tabs, Cmd+W close, Cmd+T new, Cmd+Shift+T reopen, Cmd+. stop, Cmd+? help - Keyboard help overlay with categorized shortcut grid - Command palette shortcut badges - Window state persistence: tabs, sidebar width, model, provider, auto-accept - MCP empty state with description and common servers - Theme selector header with active theme indicator - Settings section descriptions - Composer meta bar tooltips - Context-aware suggestion chips (PR vs regular threads) - Right-click "Open in Finder" for projects
1 parent b0eb4ab commit c7e6ff6

21 files changed

Lines changed: 1271 additions & 41 deletions

crates/tauri-app/frontend/src/App.tsx

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ import { SplitView } from "./components/shared/SplitView";
1717
import { BrowserPanel } from "./components/browser/BrowserPanel";
1818
import { DiffEditor } from "./components/diff/DiffEditor";
1919
import { UpdateChecker } from "./components/shared/UpdateChecker";
20+
import { KeyboardHelp } from "./components/shared/KeyboardHelp";
21+
import { Breadcrumb } from "./components/chat/Breadcrumb";
22+
import { StatusBar } from "./components/shared/StatusBar";
2023
import * as ipc from "./ipc";
2124

2225
export function App() {
23-
const { store, setStore } = appStore;
26+
const { store, setStore, closeTab, selectThread, newThread, sendUserMessage, reopenLastClosedTab } = appStore;
2427
const [showWelcome, setShowWelcome] = createSignal(true);
2528
const [showSetup, setShowSetup] = createSignal(false);
2629

@@ -134,6 +137,18 @@ export function App() {
134137
function handleKeyDown(e: KeyboardEvent) {
135138
const mod = e.metaKey || e.ctrlKey;
136139
const key = e.key.toLowerCase();
140+
141+
// Escape: close overlays in priority order
142+
if (key === "escape") {
143+
if (store.keyboardHelpOpen) { e.preventDefault(); setStore("keyboardHelpOpen", false); return; }
144+
if (store.commandPaletteOpen) { e.preventDefault(); setStore("commandPaletteOpen", false); return; }
145+
if (store.searchOpen) { e.preventDefault(); setStore("searchOpen", false); return; }
146+
if (store.usageDashboardOpen) { e.preventDefault(); setStore("usageDashboardOpen", false); return; }
147+
if (store.settingsOpen) { e.preventDefault(); setStore("settingsOpen", false); return; }
148+
if (store.themeOpen) { e.preventDefault(); setStore("themeOpen", false); return; }
149+
if (store.providerPickerOpen) { e.preventDefault(); setStore("providerPickerOpen", false); return; }
150+
}
151+
137152
if (mod && key === "k" && !e.shiftKey) {
138153
e.preventDefault();
139154
setStore("commandPaletteOpen", !store.commandPaletteOpen);
@@ -165,6 +180,73 @@ export function App() {
165180
e.preventDefault();
166181
if (store.activeTab) setStore("threadDiffOpen", store.activeTab, !store.threadDiffOpen[store.activeTab]);
167182
}
183+
184+
// Cmd+W: Close current tab
185+
if (mod && key === "w" && !e.shiftKey) {
186+
e.preventDefault();
187+
if (store.activeTab) closeTab(store.activeTab);
188+
}
189+
190+
// Cmd+T: New thread
191+
if (mod && key === "t" && !e.shiftKey) {
192+
e.preventDefault();
193+
newThread();
194+
}
195+
196+
// Cmd+N: New thread (alias)
197+
if (mod && key === "n" && !e.shiftKey) {
198+
e.preventDefault();
199+
newThread();
200+
}
201+
202+
// Cmd+Shift+T: Reopen last closed tab
203+
if (mod && e.shiftKey && key === "t") {
204+
e.preventDefault();
205+
reopenLastClosedTab();
206+
}
207+
208+
// Cmd+1 through Cmd+9: Switch to tab by position
209+
if (mod && !e.shiftKey && key >= "1" && key <= "9") {
210+
e.preventDefault();
211+
const idx = key === "9" ? store.openTabs.length - 1 : parseInt(key) - 1;
212+
const tabId = store.openTabs[idx];
213+
if (tabId) selectThread(tabId);
214+
}
215+
216+
// Cmd+Enter: Force send message (even mid-generation)
217+
if (mod && key === "enter") {
218+
e.preventDefault();
219+
sendUserMessage();
220+
}
221+
222+
// Cmd+.: Stop/interrupt generation
223+
if (mod && key === ".") {
224+
e.preventDefault();
225+
if (store.activeTab) {
226+
const status = store.sessionStatuses[store.activeTab];
227+
if (status === "generating" || status === "interrupting") {
228+
if (status === "interrupting") {
229+
ipc.stopSession(store.activeTab).catch(() => {});
230+
setStore("sessionStatuses", store.activeTab, "ready");
231+
} else {
232+
setStore("sessionStatuses", store.activeTab, "interrupting");
233+
ipc.interruptSession(store.activeTab).catch(() => {});
234+
}
235+
}
236+
}
237+
}
238+
239+
// Cmd+,: Settings
240+
if (mod && key === ",") {
241+
e.preventDefault();
242+
setStore("settingsOpen", !store.settingsOpen);
243+
}
244+
245+
// Cmd+? (Cmd+Shift+/): Keyboard help
246+
if (mod && e.shiftKey && (key === "/" || key === "?")) {
247+
e.preventDefault();
248+
setStore("keyboardHelpOpen", !store.keyboardHelpOpen);
249+
}
168250
}
169251

170252
onMount(() => window.addEventListener("keydown", handleKeyDown));
@@ -210,6 +292,7 @@ export function App() {
210292
fallback={
211293
<div class="main-panel">
212294
<TabBar />
295+
<Breadcrumb />
213296
<div
214297
ref={bodyRef}
215298
class="main-panel-body"
@@ -221,6 +304,7 @@ export function App() {
221304
<div class="main-panel-chat" style={hasSidePane() ? chatStyle() : { flex: "1" }}>
222305
<ChatArea />
223306
<Composer />
307+
<StatusBar />
224308
</div>
225309

226310
<Show when={hasSidePane()}>
@@ -275,6 +359,10 @@ export function App() {
275359
<ThemeSelector />
276360
</Show>
277361

362+
<Show when={store.keyboardHelpOpen}>
363+
<KeyboardHelp />
364+
</Show>
365+
278366
<Show when={showWelcome()}>
279367
<WelcomeScreen onDismiss={() => setShowWelcome(false)} />
280368
</Show>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { Show } from "solid-js";
2+
import { appStore } from "../../stores/app-store";
3+
4+
export function Breadcrumb() {
5+
const { store, setStore } = appStore;
6+
7+
const activeThread = () => {
8+
const tab = store.activeTab;
9+
if (!tab) return null;
10+
return store.projects.flatMap((p) => p.threads).find((t) => t.id === tab) || null;
11+
};
12+
13+
const activeProject = () => {
14+
const tab = store.activeTab;
15+
if (!tab) return null;
16+
return store.projects.find((p) => p.threads.some((t) => t.id === tab)) || null;
17+
};
18+
19+
const worktreeBranch = () => {
20+
const tab = store.activeTab;
21+
if (!tab) return null;
22+
const wt = store.worktrees[tab];
23+
return wt?.active ? wt.branch : null;
24+
};
25+
26+
const prNumber = () => {
27+
const tab = store.activeTab;
28+
const project = activeProject();
29+
if (!tab || !project) return null;
30+
const prMap = store.projectPrMap[project.id];
31+
return prMap?.[tab] ?? null;
32+
};
33+
34+
const isSpecialTab = () => {
35+
const tab = store.activeTab;
36+
return tab?.startsWith("__") ?? false;
37+
};
38+
39+
function toggleProjectCollapse() {
40+
const project = activeProject();
41+
if (!project) return;
42+
setStore("projects", (projects) =>
43+
projects.map((p) => p.id === project.id ? { ...p, collapsed: !p.collapsed } : p)
44+
);
45+
}
46+
47+
function openDiff() {
48+
const tab = store.activeTab;
49+
if (tab) setStore("threadDiffOpen", tab, true);
50+
}
51+
52+
return (
53+
<Show when={store.activeTab && !isSpecialTab()}>
54+
<div class="breadcrumb-bar">
55+
<Show when={activeProject()}>
56+
{(proj) => (
57+
<span class="bc-segment bc-clickable" onClick={toggleProjectCollapse}>
58+
{proj().name}
59+
</span>
60+
)}
61+
</Show>
62+
<Show when={activeThread()}>
63+
{(thread) => (
64+
<>
65+
<span class="bc-sep">&rsaquo;</span>
66+
<span class="bc-segment">{thread().title}</span>
67+
</>
68+
)}
69+
</Show>
70+
<Show when={worktreeBranch()}>
71+
{(branch) => (
72+
<>
73+
<span class="bc-sep">&rsaquo;</span>
74+
<span class="bc-segment bc-badge bc-clickable" onClick={openDiff}>
75+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
76+
<circle cx="18" cy="18" r="3" /><circle cx="6" cy="6" r="3" /><path d="M6 21V9a9 9 0 009 9" />
77+
</svg>
78+
{branch()}
79+
</span>
80+
</>
81+
)}
82+
</Show>
83+
<Show when={prNumber()}>
84+
{(pr) => (
85+
<>
86+
<span class="bc-sep">&rsaquo;</span>
87+
<span class="bc-segment bc-badge bc-pr">PR #{pr()}</span>
88+
</>
89+
)}
90+
</Show>
91+
</div>
92+
</Show>
93+
);
94+
}
95+
96+
if (!document.getElementById("breadcrumb-styles")) {
97+
const s = document.createElement("style");
98+
s.id = "breadcrumb-styles";
99+
s.textContent = `
100+
.breadcrumb-bar {
101+
display: flex;
102+
align-items: center;
103+
gap: 4px;
104+
height: 24px;
105+
padding: 0 12px;
106+
background: var(--bg-muted);
107+
border-bottom: 1px solid var(--border);
108+
flex-shrink: 0;
109+
user-select: none;
110+
overflow: hidden;
111+
}
112+
.bc-segment {
113+
font-size: 11px;
114+
color: var(--text-tertiary);
115+
white-space: nowrap;
116+
overflow: hidden;
117+
text-overflow: ellipsis;
118+
}
119+
.bc-clickable {
120+
cursor: pointer;
121+
border-radius: 3px;
122+
padding: 1px 4px;
123+
transition: background 0.1s, color 0.1s;
124+
}
125+
.bc-clickable:hover {
126+
background: var(--bg-accent);
127+
color: var(--text-secondary);
128+
}
129+
.bc-sep {
130+
font-size: 11px;
131+
color: var(--text-tertiary);
132+
opacity: 0.5;
133+
flex-shrink: 0;
134+
}
135+
.bc-badge {
136+
display: inline-flex;
137+
align-items: center;
138+
gap: 3px;
139+
font-family: var(--font-mono);
140+
font-size: 10px;
141+
padding: 1px 6px;
142+
border-radius: var(--radius-pill, 99px);
143+
background: rgba(107, 124, 255, 0.08);
144+
color: var(--primary);
145+
}
146+
.bc-pr {
147+
background: rgba(107, 124, 255, 0.1);
148+
color: var(--primary);
149+
font-weight: 600;
150+
}
151+
`;
152+
document.head.appendChild(s);
153+
}

0 commit comments

Comments
 (0)