Skip to content

Commit 125cbf9

Browse files
fix: debug & polish — dark mode, mobile responsive, accessibility, UX improvements
- Fix dark mode colors for all flagship UI elements (FlowGuide, OpenWeightHub, thinking cards, badges, buttons) with proper dark palette overrides - Add mobile responsive styles for FlowGuide and OpenWeightHub modals (slide-up animation, stacked layouts, scrollable filters) - Fix keyboard shortcut useEffect missing dependency array (perf bug) - Add Ctrl+Shift+B for drawer toggle, fix Ctrl+B docs (was sidebar all along) - Fix welcome screen flash during session switching - Increase error toast duration to 8s, add aria-live=assertive for errors - Add boot timeout with retry button after 10s - Replace fake gpt-oss:20b model with real codestral:22b from Mistral - Add lm_studio/lmstudio to local provider detection filter - Add progress indicator and 'Mark as Learned' tracking to FlowGuide - Add missing .success-icon CSS class - Add focus-visible states for all flagship buttons - Add micro-animations: submit button scale, thinking card pulse, suggestion hover shadow, composer focus glow enhancement - Add toast type-based left border colors (err/ok/info) - Fix thinking card emoji accessibility (aria-hidden) - Add op-card live state visual glow
1 parent 20bf527 commit 125cbf9

5 files changed

Lines changed: 617 additions & 17 deletions

File tree

‎ui/src/App.tsx‎

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,10 @@ export default function App() {
164164
const toast = useCallback((text: string, kind: Toast["kind"] = "info") => {
165165
const id = ++toastSeq.current;
166166
setToasts((prev) => [...prev.slice(-3), { id, text, kind }]);
167+
const duration = kind === "err" ? 8000 : 5000;
167168
setTimeout(
168169
() => setToasts((prev) => prev.filter((t) => t.id !== id)),
169-
5000,
170+
duration,
170171
);
171172
}, []);
172173

@@ -957,6 +958,10 @@ export default function App() {
957958
e.preventDefault();
958959
setSidebar((v) => !v);
959960
}
961+
if (mod && e.shiftKey && key === "b") {
962+
e.preventDefault();
963+
setPanel((p) => (p ? null : "changes"));
964+
}
960965
if (mod && key === ",") {
961966
e.preventDefault();
962967
setOverlay("settings");
@@ -988,7 +993,7 @@ export default function App() {
988993
};
989994
window.addEventListener("keydown", onKey);
990995
return () => window.removeEventListener("keydown", onKey);
991-
});
996+
}, [overlay, trust, slashOpen, panel, workspace, sessionId, busy, modelChoice, mode]);
992997
const reloadCommands = useCallback(async () => {
993998
const result = await api.commands();
994999
setCommands(result.commands);
@@ -1036,16 +1041,33 @@ export default function App() {
10361041
? commands.filter((c) => c.name.startsWith(task.slice(1))).slice(0, 8)
10371042
: [];
10381043
const empty =
1039-
!transcript.items.length && !commandCards.length && !busy && !submitting;
1044+
!transcript.items.length && !commandCards.length && !busy && !submitting && !switching;
10401045
const completedSteps = transcript.plan.filter(
10411046
(p) => p.status === "done",
10421047
).length;
1048+
const [bootTimeout, setBootTimeout] = useState(false);
1049+
useEffect(() => {
1050+
if (!ready) {
1051+
const timer = setTimeout(() => setBootTimeout(true), 10000);
1052+
return () => clearTimeout(timer);
1053+
}
1054+
}, [ready]);
10431055
if (!ready)
10441056
return (
10451057
<div className="boot">
10461058
<img src="/icon.svg" alt="" />
10471059
<span>Opening your workspace…</span>
10481060
<LoaderCircle className="spin" size={18} />
1061+
{bootTimeout && (
1062+
<div className="boot-timeout">
1063+
<p>
1064+
Taking longer than expected. The backend may be starting up or unreachable.
1065+
</p>
1066+
<button type="button" onClick={() => { setBootTimeout(false); void boot(); }}>
1067+
Retry connection
1068+
</button>
1069+
</div>
1070+
)}
10491071
</div>
10501072
);
10511073
if (needsOnboard)
@@ -1915,7 +1937,12 @@ export default function App() {
19151937
)}
19161938
<div className="toasts" aria-live="polite">
19171939
{toasts.map((t) => (
1918-
<div key={t.id} className={`toast ${t.kind}`}>
1940+
<div
1941+
key={t.id}
1942+
className={`toast ${t.kind}`}
1943+
role={t.kind === "err" ? "alert" : "status"}
1944+
aria-live={t.kind === "err" ? "assertive" : "polite"}
1945+
>
19191946
<span>{t.text}</span>
19201947
<button
19211948
type="button"

‎ui/src/components/FlowGuide.tsx‎

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from "react";
1+
import { useState, useEffect, useCallback } from "react";
22
import {
33
BookOpen,
44
Check,
@@ -24,10 +24,34 @@ interface FlowGuideProps {
2424
onSelectPrompt?: (prompt: string, mode?: string) => void;
2525
}
2626

27+
const TAB_IDS = ["overview", "modes", "loop", "steering", "shortcuts"] as const;
28+
type TabId = (typeof TAB_IDS)[number];
29+
2730
export function FlowGuide({ onClose, onSelectPrompt }: FlowGuideProps) {
28-
const [activeTab, setActiveTab] = useState<
29-
"overview" | "modes" | "loop" | "steering" | "shortcuts"
30-
>("overview");
31+
const [activeTab, setActiveTab] = useState<TabId>("overview");
32+
const [learned, setLearned] = useState<Set<string>>(() => {
33+
try {
34+
const saved = localStorage.getItem("shadow:flow-learned");
35+
return saved ? new Set(JSON.parse(saved)) : new Set();
36+
} catch {
37+
return new Set();
38+
}
39+
});
40+
41+
useEffect(() => {
42+
localStorage.setItem("shadow:flow-learned", JSON.stringify([...learned]));
43+
}, [learned]);
44+
45+
const toggleLearned = useCallback((id: string) => {
46+
setLearned((prev) => {
47+
const next = new Set(prev);
48+
if (next.has(id)) next.delete(id);
49+
else next.add(id);
50+
return next;
51+
});
52+
}, []);
53+
54+
const activeIndex = TAB_IDS.indexOf(activeTab);
3155

3256
const startTask = (prompt: string, mode = "coder") => {
3357
if (onSelectPrompt) {
@@ -80,6 +104,24 @@ export function FlowGuide({ onClose, onSelectPrompt }: FlowGuideProps) {
80104
))}
81105
</nav>
82106

107+
<div className="flow-progress">
108+
{TAB_IDS.map((id, i) => (
109+
<>
110+
<span
111+
key={id}
112+
className={`flow-progress-dot ${activeTab === id ? "active" : ""} ${learned.has(id) ? "completed" : ""}`}
113+
title={learned.has(id) ? "Learned" : `Section ${i + 1}`}
114+
/>
115+
{i < TAB_IDS.length - 1 && (
116+
<span
117+
key={`line-${id}`}
118+
className={`flow-progress-line ${learned.has(id) ? "completed" : ""}`}
119+
/>
120+
)}
121+
</>
122+
))}
123+
</div>
124+
83125
<div className="flow-guide-content">
84126
{activeTab === "overview" && (
85127
<div className="flow-tab-pane">
@@ -146,6 +188,13 @@ export function FlowGuide({ onClose, onSelectPrompt }: FlowGuideProps) {
146188
<strong>Zero data leaves your machine:</strong> When using local providers (Ollama on <code>:11434</code> or LM Studio on <code>:1234</code>), every file, prompt, and tool execution stays strictly inside your Linux environment.
147189
</span>
148190
</div>
191+
<button
192+
type="button"
193+
className={`learned-check ${learned.has("overview") ? "done" : ""}`}
194+
onClick={() => toggleLearned("overview")}
195+
>
196+
{learned.has("overview") ? <><Check size={12} /> Learned</> : "Mark as learned"}
197+
</button>
149198
</div>
150199
)}
151200

@@ -229,6 +278,13 @@ export function FlowGuide({ onClose, onSelectPrompt }: FlowGuideProps) {
229278
</button>
230279
</div>
231280
</div>
281+
<button
282+
type="button"
283+
className={`learned-check ${learned.has("modes") ? "done" : ""}`}
284+
onClick={() => toggleLearned("modes")}
285+
>
286+
{learned.has("modes") ? <><Check size={12} /> Learned</> : "Mark as learned"}
287+
</button>
232288
</div>
233289
)}
234290

@@ -280,6 +336,13 @@ export function FlowGuide({ onClose, onSelectPrompt }: FlowGuideProps) {
280336
</div>
281337
</div>
282338
</div>
339+
<button
340+
type="button"
341+
className={`learned-check ${learned.has("loop") ? "done" : ""}`}
342+
onClick={() => toggleLearned("loop")}
343+
>
344+
{learned.has("loop") ? <><Check size={12} /> Learned</> : "Mark as learned"}
345+
</button>
283346
</div>
284347
)}
285348

@@ -328,6 +391,13 @@ export function FlowGuide({ onClose, onSelectPrompt }: FlowGuideProps) {
328391
ShadowCode records dirty-state snapshots before file edits. If an automated change fails verification or a command is interrupted, your git working tree is safeguarded and can be reverted with one click.
329392
</p>
330393
</div>
394+
<button
395+
type="button"
396+
className={`learned-check ${learned.has("steering") ? "done" : ""}`}
397+
onClick={() => toggleLearned("steering")}
398+
>
399+
{learned.has("steering") ? <><Check size={12} /> Learned</> : "Mark as learned"}
400+
</button>
331401
</div>
332402
)}
333403

@@ -357,6 +427,10 @@ export function FlowGuide({ onClose, onSelectPrompt }: FlowGuideProps) {
357427
</tr>
358428
<tr>
359429
<td><kbd>Ctrl</kbd> + <kbd>B</kbd></td>
430+
<td>Toggle Sidebar</td>
431+
</tr>
432+
<tr>
433+
<td><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>B</kbd></td>
360434
<td>Toggle Right Drawer (Files / Diff / Goals)</td>
361435
</tr>
362436
<tr>
@@ -403,6 +477,13 @@ export function FlowGuide({ onClose, onSelectPrompt }: FlowGuideProps) {
403477
</table>
404478
</div>
405479
</div>
480+
<button
481+
type="button"
482+
className={`learned-check ${learned.has("shortcuts") ? "done" : ""}`}
483+
onClick={() => toggleLearned("shortcuts")}
484+
>
485+
{learned.has("shortcuts") ? <><Check size={12} /> Learned</> : "Mark as learned"}
486+
</button>
406487
</div>
407488
)}
408489
</div>

‎ui/src/components/OpenWeightHub.tsx‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,17 @@ const CHAMPIONS: ChampionModel[] = [
7171
tags: ["Chain of Thought", "Architecture", "Deep Debugging"],
7272
},
7373
{
74-
id: "gpt-oss:20b",
75-
name: "GPT-OSS 20B",
76-
creator: "Open Source Community",
74+
id: "codestral:22b",
75+
name: "Mistral Codestral 22B",
76+
creator: "Mistral AI",
7777
size: "13.0 GB",
7878
vramReq: "14 GB VRAM",
7979
vramFit: "optimal",
80-
context: "128k",
80+
context: "32k",
8181
description:
82-
"Tailored open-weight coding agent harness model. Highly optimized for file reading, lint verification, and diff patch generation.",
83-
pullCmd: "ollama pull gpt-oss:20b",
84-
tags: ["Local Installed", "Long Context", "Autonomous"],
82+
"Mistral's dedicated coding model. Exceptional at fill-in-the-middle, refactoring, test generation, and multi-language fluency with concise, clean output.",
83+
pullCmd: "ollama pull codestral:22b",
84+
tags: ["Mistral AI", "Long Context", "Code Specialist"],
8585
},
8686
{
8787
id: "huihui_ai/gemma-4-abliterated:12b",
@@ -175,7 +175,7 @@ export function OpenWeightHub({
175175

176176
const localProviders = useMemo(() => {
177177
return providers.filter((p) =>
178-
["ollama", "local", "llamacpp", "vllm"].includes(p.id)
178+
["ollama", "local", "llamacpp", "vllm", "lm_studio", "lmstudio"].includes(p.id)
179179
);
180180
}, [providers]);
181181

‎ui/src/components/cards.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export function ThinkingCard({
163163
aria-expanded={open}
164164
onClick={() => setOpen(!open)}
165165
>
166-
<span className="thinking-icon">🧠</span>
166+
<span className="thinking-icon" aria-hidden="true">🧠</span>
167167
<span className="thinking-title">
168168
{live ? "Reasoning in progress…" : "Reasoning process"}
169169
{durationSec ? ` (${durationSec.toFixed(1)}s)` : ""}

0 commit comments

Comments
 (0)