@geminilight/mindos 0.5.43 → 0.5.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,30 +1,58 @@
1
- export type UseCaseCategory = 'getting-started' | 'cross-agent' | 'knowledge-evolution' | 'advanced';
1
+ /** Capability axis maps to product pillars */
2
+ export type UseCaseCategory = 'knowledge-management' | 'memory-sync' | 'auto-execute' | 'experience-evolution' | 'human-insights' | 'audit-control';
3
+
4
+ /** Scenario axis — maps to user journey phase */
5
+ export type UseCaseScenario = 'first-day' | 'daily' | 'project' | 'advanced';
2
6
 
3
7
  export interface UseCase {
4
8
  id: string;
5
9
  icon: string;
6
10
  category: UseCaseCategory;
11
+ scenario: UseCaseScenario;
7
12
  }
8
13
 
9
14
  /**
10
15
  * C1-C9 use case definitions.
11
16
  * All display text (title, description, prompt) comes from i18n — this file is structure only.
17
+ *
18
+ * Category (capability axis):
19
+ * knowledge-management — Inject, organize, and maintain knowledge
20
+ * memory-sync — Record once, all Agents know
21
+ * auto-execute — One sentence, auto-execute
22
+ * experience-evolution — Gets smarter with use
23
+ * human-insights — Understand and manage relationships
24
+ * audit-control — You have final say
25
+ *
26
+ * Scenario (journey axis):
27
+ * first-day — Onboarding / first-time tasks
28
+ * daily — Everyday workflows
29
+ * project — Project-scoped work
30
+ * advanced — Power-user patterns
12
31
  */
13
32
  export const useCases: UseCase[] = [
14
- { id: 'c1', icon: '👤', category: 'getting-started' },
15
- { id: 'c2', icon: '📥', category: 'getting-started' },
16
- { id: 'c3', icon: '🔄', category: 'cross-agent' },
17
- { id: 'c4', icon: '🔁', category: 'knowledge-evolution' },
18
- { id: 'c5', icon: '💡', category: 'cross-agent' },
19
- { id: 'c6', icon: '🚀', category: 'cross-agent' },
20
- { id: 'c7', icon: '🔍', category: 'knowledge-evolution' },
21
- { id: 'c8', icon: '🤝', category: 'knowledge-evolution' },
22
- { id: 'c9', icon: '🛡️', category: 'advanced' },
33
+ { id: 'c1', icon: '👤', category: 'memory-sync', scenario: 'first-day' },
34
+ { id: 'c2', icon: '📥', category: 'knowledge-management', scenario: 'daily' },
35
+ { id: 'c3', icon: '🔄', category: 'memory-sync', scenario: 'project' },
36
+ { id: 'c4', icon: '🔁', category: 'experience-evolution', scenario: 'daily' },
37
+ { id: 'c5', icon: '💡', category: 'auto-execute', scenario: 'daily' },
38
+ { id: 'c6', icon: '🚀', category: 'auto-execute', scenario: 'project' },
39
+ { id: 'c7', icon: '🔍', category: 'knowledge-management', scenario: 'project' },
40
+ { id: 'c8', icon: '🤝', category: 'human-insights', scenario: 'daily' },
41
+ { id: 'c9', icon: '🛡️', category: 'audit-control', scenario: 'advanced' },
23
42
  ];
24
43
 
25
44
  export const categories: UseCaseCategory[] = [
26
- 'getting-started',
27
- 'cross-agent',
28
- 'knowledge-evolution',
45
+ 'knowledge-management',
46
+ 'memory-sync',
47
+ 'auto-execute',
48
+ 'experience-evolution',
49
+ 'human-insights',
50
+ 'audit-control',
51
+ ];
52
+
53
+ export const scenarios: UseCaseScenario[] = [
54
+ 'first-day',
55
+ 'daily',
56
+ 'project',
29
57
  'advanced',
30
58
  ];
@@ -1,8 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { useState } from 'react';
4
3
  import Link from 'next/link';
5
- import { ChevronDown, ChevronRight, ExternalLink, Blocks, Zap } from 'lucide-react';
4
+ import { Lightbulb, Blocks, Zap, LayoutTemplate, ChevronRight, User, Download, RefreshCw, Repeat, Rocket, Search, Handshake, ShieldCheck } from 'lucide-react';
6
5
  import PanelHeader from './PanelHeader';
7
6
  import { useLocale } from '@/lib/LocaleContext';
8
7
  import { useCases } from '@/components/explore/use-cases';
@@ -14,56 +13,61 @@ interface DiscoverPanelProps {
14
13
  onMaximize?: () => void;
15
14
  }
16
15
 
17
- /** Collapsible section with count badge */
18
- function Section({
16
+ /** Navigation entry clickable row linking to a page or showing coming soon */
17
+ function NavEntry({
19
18
  icon,
20
19
  title,
21
- count,
22
- defaultOpen = true,
23
- children,
20
+ badge,
21
+ href,
22
+ onClick,
24
23
  }: {
25
24
  icon: React.ReactNode;
26
25
  title: string;
27
- count?: number;
28
- defaultOpen?: boolean;
29
- children: React.ReactNode;
26
+ badge?: React.ReactNode;
27
+ href?: string;
28
+ onClick?: () => void;
30
29
  }) {
31
- const [open, setOpen] = useState(defaultOpen);
30
+ const content = (
31
+ <>
32
+ <span className="flex items-center justify-center w-7 h-7 rounded-md bg-muted shrink-0">{icon}</span>
33
+ <span className="text-sm font-medium text-foreground flex-1">{title}</span>
34
+ {badge}
35
+ <ChevronRight size={14} className="text-muted-foreground shrink-0" />
36
+ </>
37
+ );
38
+
39
+ const className = "flex items-center gap-3 px-4 py-2.5 hover:bg-muted/50 transition-colors cursor-pointer";
40
+
41
+ if (href) {
42
+ return <Link href={href} className={className}>{content}</Link>;
43
+ }
44
+ return <button onClick={onClick} className={`${className} w-full`}>{content}</button>;
45
+ }
46
+
47
+ /** Coming soon badge */
48
+ function ComingSoonBadge({ label }: { label: string }) {
32
49
  return (
33
- <div>
34
- <button
35
- onClick={() => setOpen(v => !v)}
36
- className="flex items-center gap-2 w-full px-4 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider hover:text-foreground transition-colors"
37
- >
38
- {open ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
39
- <span className="flex items-center gap-1.5">
40
- {icon}
41
- {title}
42
- </span>
43
- {count !== undefined && (
44
- <span className="ml-auto text-2xs tabular-nums opacity-60">{count}</span>
45
- )}
46
- </button>
47
- {open && <div className="pb-2">{children}</div>}
48
- </div>
50
+ <span className="text-2xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground shrink-0">
51
+ {label}
52
+ </span>
49
53
  );
50
54
  }
51
55
 
52
- /** Compact use case row for panel display */
56
+ /** Compact use case row */
53
57
  function UseCaseRow({
54
58
  icon,
55
59
  title,
56
60
  prompt,
57
61
  tryLabel,
58
62
  }: {
59
- icon: string;
63
+ icon: React.ReactNode;
60
64
  title: string;
61
65
  prompt: string;
62
66
  tryLabel: string;
63
67
  }) {
64
68
  return (
65
- <div className="group flex items-center gap-2 px-4 py-1.5 hover:bg-muted/50 transition-colors rounded-sm mx-1">
66
- <span className="text-sm leading-none shrink-0" suppressHydrationWarning>{icon}</span>
69
+ <div className="group flex items-center gap-2.5 px-4 py-1.5 hover:bg-muted/50 transition-colors rounded-sm mx-1">
70
+ <span className="text-muted-foreground shrink-0">{icon}</span>
67
71
  <span className="text-xs text-foreground truncate flex-1">{title}</span>
68
72
  <button
69
73
  onClick={() => openAskModal(prompt, 'user')}
@@ -75,29 +79,18 @@ function UseCaseRow({
75
79
  );
76
80
  }
77
81
 
78
- /** Coming soon placeholder */
79
- function ComingSoonSection({
80
- icon,
81
- title,
82
- description,
83
- comingSoonLabel,
84
- }: {
85
- icon: React.ReactNode;
86
- title: string;
87
- description: string;
88
- comingSoonLabel: string;
89
- }) {
90
- return (
91
- <Section icon={icon} title={title} defaultOpen={false}>
92
- <div className="px-4 py-3 mx-1">
93
- <p className="text-xs text-muted-foreground leading-relaxed">{description}</p>
94
- <span className="inline-block mt-2 text-2xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
95
- {comingSoonLabel}
96
- </span>
97
- </div>
98
- </Section>
99
- );
100
- }
82
+ /** Map use case id → lucide icon */
83
+ const useCaseIcons: Record<string, React.ReactNode> = {
84
+ c1: <User size={12} />, // Inject Identity
85
+ c2: <Download size={12} />, // Save Information
86
+ c3: <RefreshCw size={12} />, // Cross-Agent Handoff
87
+ c4: <Repeat size={12} />, // Experience → SOP
88
+ c5: <Lightbulb size={12} />, // Capture Ideas
89
+ c6: <Rocket size={12} />, // Project Cold Start
90
+ c7: <Search size={12} />, // Research & Archive
91
+ c8: <Handshake size={12} />, // Network Management
92
+ c9: <ShieldCheck size={12} />, // Audit & Correct
93
+ };
101
94
 
102
95
  export default function DiscoverPanel({ active, maximized, onMaximize }: DiscoverPanelProps) {
103
96
  const { t } = useLocale();
@@ -117,54 +110,51 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
117
110
  <div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
118
111
  <PanelHeader title={d.title} maximized={maximized} onMaximize={onMaximize} />
119
112
  <div className="flex-1 overflow-y-auto min-h-0">
120
- {/* Use Cases */}
121
- <Section icon={<span className="text-xs" suppressHydrationWarning>🎯</span>} title={d.useCases} count={useCases.length}>
122
- <div className="flex flex-col">
123
- {useCases.map(uc => {
124
- const data = getUseCaseText(uc.id);
125
- if (!data) return null;
126
- return (
127
- <UseCaseRow
128
- key={uc.id}
129
- icon={uc.icon}
130
- title={data.title}
131
- prompt={data.prompt}
132
- tryLabel={d.tryIt}
133
- />
134
- );
135
- })}
136
- </div>
137
- </Section>
138
-
139
- <div className="mx-4 border-t border-border" />
140
-
141
- {/* Plugin Market — Coming Soon */}
142
- <ComingSoonSection
143
- icon={<Blocks size={11} />}
144
- title={d.pluginMarket}
145
- description={d.pluginMarketDesc}
146
- comingSoonLabel={d.comingSoon}
147
- />
113
+ {/* Navigation entries */}
114
+ <div className="py-2">
115
+ <NavEntry
116
+ icon={<Lightbulb size={14} className="text-[var(--amber)]" />}
117
+ title={d.useCases}
118
+ badge={<span className="text-2xs tabular-nums text-muted-foreground">{useCases.length}</span>}
119
+ href="/explore"
120
+ />
121
+ <NavEntry
122
+ icon={<Blocks size={14} className="text-muted-foreground" />}
123
+ title={d.pluginMarket}
124
+ badge={<ComingSoonBadge label={d.comingSoon} />}
125
+ />
126
+ <NavEntry
127
+ icon={<Zap size={14} className="text-muted-foreground" />}
128
+ title={d.skillMarket}
129
+ badge={<ComingSoonBadge label={d.comingSoon} />}
130
+ />
131
+ <NavEntry
132
+ icon={<LayoutTemplate size={14} className="text-muted-foreground" />}
133
+ title={d.spaceTemplates}
134
+ badge={<ComingSoonBadge label={d.comingSoon} />}
135
+ />
136
+ </div>
148
137
 
149
138
  <div className="mx-4 border-t border-border" />
150
139
 
151
- {/* Skill MarketComing Soon */}
152
- <ComingSoonSection
153
- icon={<Zap size={11} />}
154
- title={d.skillMarket}
155
- description={d.skillMarketDesc}
156
- comingSoonLabel={d.comingSoon}
157
- />
158
-
159
- {/* View all link */}
160
- <div className="px-4 py-3 mt-2">
161
- <Link
162
- href="/explore"
163
- className="inline-flex items-center gap-1.5 text-xs text-[var(--amber)] hover:opacity-80 transition-opacity"
164
- >
165
- {d.viewAll}
166
- <ExternalLink size={11} />
167
- </Link>
140
+ {/* Quick tryuse case list */}
141
+ <div className="py-2">
142
+ <div className="px-4 py-1.5">
143
+ <span className="text-2xs font-medium text-muted-foreground uppercase tracking-wider">{d.useCases}</span>
144
+ </div>
145
+ {useCases.map(uc => {
146
+ const data = getUseCaseText(uc.id);
147
+ if (!data) return null;
148
+ return (
149
+ <UseCaseRow
150
+ key={uc.id}
151
+ icon={useCaseIcons[uc.id] || <Lightbulb size={12} />}
152
+ title={data.title}
153
+ prompt={data.prompt}
154
+ tryLabel={d.tryIt}
155
+ />
156
+ );
157
+ })}
168
158
  </div>
169
159
  </div>
170
160
  </div>
@@ -153,8 +153,8 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
153
153
  { id: 'ai', label: t.settings.tabs.ai, icon: <Sparkles size={iconSize} /> },
154
154
  { id: 'mcp', label: t.settings.tabs.mcp ?? 'MCP & Skills', icon: <Plug size={iconSize} /> },
155
155
  { id: 'knowledge', label: t.settings.tabs.knowledge, icon: <Settings size={iconSize} /> },
156
- { id: 'sync', label: t.settings.tabs.sync ?? 'Sync', icon: <RefreshCw size={iconSize} /> },
157
156
  { id: 'appearance', label: t.settings.tabs.appearance, icon: <Palette size={iconSize} /> },
157
+ { id: 'sync', label: t.settings.tabs.sync ?? 'Sync', icon: <RefreshCw size={iconSize} /> },
158
158
  { id: 'update', label: t.settings.tabs.update ?? 'Update', icon: <Download size={iconSize} />, badge: hasUpdate },
159
159
  ];
160
160
 
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect, useCallback, useRef } from 'react';
4
- import { Download, RefreshCw, CheckCircle2, AlertCircle, Loader2, ExternalLink } from 'lucide-react';
4
+ import { Download, RefreshCw, CheckCircle2, AlertCircle, Loader2, ExternalLink, Circle } from 'lucide-react';
5
5
  import { apiFetch } from '@/lib/api';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
7
 
@@ -11,18 +11,53 @@ interface UpdateInfo {
11
11
  hasUpdate: boolean;
12
12
  }
13
13
 
14
+ interface StageInfo {
15
+ id: string;
16
+ status: 'pending' | 'running' | 'done' | 'failed';
17
+ }
18
+
19
+ interface UpdateStatus {
20
+ stage: string;
21
+ stages: StageInfo[];
22
+ error: string | null;
23
+ version: { from: string | null; to: string | null } | null;
24
+ startedAt: string | null;
25
+ }
26
+
14
27
  type UpdateState = 'idle' | 'checking' | 'updating' | 'updated' | 'error' | 'timeout';
15
28
 
16
29
  const CHANGELOG_URL = 'https://github.com/GeminiLight/MindOS/releases';
17
- const POLL_INTERVAL = 5_000;
18
- const POLL_TIMEOUT = 4 * 60 * 1000; // 4 minutes
30
+ const POLL_INTERVAL = 3_000;
31
+ const POLL_TIMEOUT = 5 * 60 * 1000; // 5 minutes
32
+
33
+ const STAGE_LABELS: Record<string, { en: string; zh: string }> = {
34
+ downloading: { en: 'Downloading update', zh: '下载更新' },
35
+ skills: { en: 'Updating skills', zh: '更新 Skills' },
36
+ rebuilding: { en: 'Rebuilding app', zh: '重新构建应用' },
37
+ restarting: { en: 'Restarting server', zh: '重启服务' },
38
+ };
39
+
40
+ function StageIcon({ status }: { status: string }) {
41
+ switch (status) {
42
+ case 'done':
43
+ return <CheckCircle2 size={14} className="text-success shrink-0" />;
44
+ case 'running':
45
+ return <Loader2 size={14} className="animate-spin shrink-0" style={{ color: 'var(--amber)' }} />;
46
+ case 'failed':
47
+ return <AlertCircle size={14} className="text-destructive shrink-0" />;
48
+ default:
49
+ return <Circle size={14} className="text-muted-foreground/40 shrink-0" />;
50
+ }
51
+ }
19
52
 
20
53
  export function UpdateTab() {
21
- const { t } = useLocale();
54
+ const { t, locale } = useLocale();
22
55
  const u = t.settings.update;
23
56
  const [info, setInfo] = useState<UpdateInfo | null>(null);
24
57
  const [state, setState] = useState<UpdateState>('idle');
25
58
  const [errorMsg, setErrorMsg] = useState('');
59
+ const [stages, setStages] = useState<StageInfo[]>([]);
60
+ const [updateError, setUpdateError] = useState<string | null>(null);
26
61
  const pollRef = useRef<ReturnType<typeof setInterval>>(undefined);
27
62
  const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
28
63
  const originalVersion = useRef<string>('');
@@ -43,16 +78,23 @@ export function UpdateTab() {
43
78
 
44
79
  useEffect(() => { checkUpdate(); }, [checkUpdate]);
45
80
 
46
- useEffect(() => {
47
- return () => {
48
- clearInterval(pollRef.current);
49
- clearTimeout(timeoutRef.current);
50
- };
81
+ const cleanup = useCallback(() => {
82
+ clearInterval(pollRef.current);
83
+ clearTimeout(timeoutRef.current);
51
84
  }, []);
52
85
 
86
+ useEffect(() => cleanup, [cleanup]);
87
+
53
88
  const handleUpdate = useCallback(async () => {
54
89
  setState('updating');
55
90
  setErrorMsg('');
91
+ setUpdateError(null);
92
+ setStages([
93
+ { id: 'downloading', status: 'pending' },
94
+ { id: 'skills', status: 'pending' },
95
+ { id: 'rebuilding', status: 'pending' },
96
+ { id: 'restarting', status: 'pending' },
97
+ ]);
56
98
 
57
99
  try {
58
100
  await apiFetch('/api/update', { method: 'POST' });
@@ -60,26 +102,67 @@ export function UpdateTab() {
60
102
  // Expected — server may die during update
61
103
  }
62
104
 
105
+ // Poll update-status for stage progress
63
106
  pollRef.current = setInterval(async () => {
107
+ // Try status endpoint first (may fail when server is restarting)
64
108
  try {
65
- const data = await apiFetch<UpdateInfo>('/api/update-check');
66
- if (data.current !== originalVersion.current) {
67
- clearInterval(pollRef.current);
68
- clearTimeout(timeoutRef.current);
69
- setInfo(data);
70
- setState('updated');
71
- setTimeout(() => window.location.reload(), 2000);
109
+ const status = await apiFetch<UpdateStatus>('/api/update-status', { timeout: 5000 });
110
+
111
+ if (status.stages?.length > 0) {
112
+ setStages(status.stages);
113
+ }
114
+
115
+ if (status.stage === 'failed') {
116
+ cleanup();
117
+ setUpdateError(status.error || 'Update failed');
118
+ setState('error');
119
+ return;
120
+ }
121
+
122
+ if (status.stage === 'done') {
123
+ // Verify version actually changed
124
+ try {
125
+ const data = await apiFetch<UpdateInfo>('/api/update-check');
126
+ if (data.current !== originalVersion.current) {
127
+ cleanup();
128
+ setInfo(data);
129
+ setState('updated');
130
+ setTimeout(() => window.location.reload(), 2000);
131
+ return;
132
+ }
133
+ } catch { /* new server may not be fully ready */ }
72
134
  }
73
135
  } catch {
74
- // Server still restarting
136
+ // Server restarting — also try update-check as fallback
137
+ try {
138
+ const data = await apiFetch<UpdateInfo>('/api/update-check', { timeout: 5000 });
139
+ if (data.current !== originalVersion.current) {
140
+ cleanup();
141
+ setStages(prev => prev.map(s => ({ ...s, status: 'done' as const })));
142
+ setInfo(data);
143
+ setState('updated');
144
+ setTimeout(() => window.location.reload(), 2000);
145
+ }
146
+ } catch {
147
+ // Both endpoints down — server still restarting
148
+ }
75
149
  }
76
150
  }, POLL_INTERVAL);
77
151
 
78
152
  timeoutRef.current = setTimeout(() => {
79
- clearInterval(pollRef.current);
153
+ cleanup();
80
154
  setState('timeout');
81
155
  }, POLL_TIMEOUT);
82
- }, []);
156
+ }, [cleanup]);
157
+
158
+ const handleRetry = useCallback(() => {
159
+ setUpdateError(null);
160
+ handleUpdate();
161
+ }, [handleUpdate]);
162
+
163
+ const lang = locale === 'zh' ? 'zh' : 'en';
164
+ const doneCount = stages.filter(s => s.status === 'done').length;
165
+ const progress = stages.length > 0 ? Math.round((doneCount / stages.length) * 100) : 0;
83
166
 
84
167
  return (
85
168
  <div className="space-y-5">
@@ -114,11 +197,27 @@ export function UpdateTab() {
114
197
  )}
115
198
 
116
199
  {state === 'updating' && (
117
- <div className="space-y-2">
118
- <div className="flex items-center gap-2 text-xs" style={{ color: 'var(--amber)' }}>
119
- <Loader2 size={13} className="animate-spin" />
120
- {u?.updating ?? 'Updating MindOS... The server will restart shortly.'}
200
+ <div className="space-y-3">
201
+ {/* Stage list */}
202
+ <div className="space-y-1.5">
203
+ {stages.map((s) => (
204
+ <div key={s.id} className="flex items-center gap-2 text-xs">
205
+ <StageIcon status={s.status} />
206
+ <span className={s.status === 'pending' ? 'text-muted-foreground/50' : s.status === 'running' ? 'text-foreground' : 'text-muted-foreground'}>
207
+ {STAGE_LABELS[s.id]?.[lang] ?? s.id}
208
+ </span>
209
+ </div>
210
+ ))}
211
+ </div>
212
+
213
+ {/* Progress bar */}
214
+ <div className="h-1 rounded-full bg-muted overflow-hidden">
215
+ <div
216
+ className="h-full rounded-full transition-all duration-500 ease-out"
217
+ style={{ width: `${Math.max(progress, 5)}%`, background: 'var(--amber)' }}
218
+ />
121
219
  </div>
220
+
122
221
  <p className="text-2xs text-muted-foreground">
123
222
  {u?.updatingHint ?? 'This may take 1–3 minutes. Do not close this page.'}
124
223
  </p>
@@ -133,21 +232,39 @@ export function UpdateTab() {
133
232
  )}
134
233
 
135
234
  {state === 'timeout' && (
136
- <div className="space-y-1">
235
+ <div className="space-y-2">
137
236
  <div className="flex items-center gap-2 text-xs text-amber-600 dark:text-amber-400">
138
237
  <AlertCircle size={13} />
139
238
  {u?.timeout ?? 'Update may still be in progress.'}
140
239
  </div>
141
240
  <p className="text-2xs text-muted-foreground">
142
- {u?.timeoutHint ?? 'Check your terminal:'} <code className="font-mono bg-muted px-1 py-0.5 rounded">mindos logs</code>
241
+ {u?.timeoutHint ?? 'The server may need more time to rebuild. Try refreshing.'}
143
242
  </p>
243
+ <button
244
+ onClick={() => window.location.reload()}
245
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
246
+ >
247
+ <RefreshCw size={12} />
248
+ {u?.refreshButton ?? 'Refresh Page'}
249
+ </button>
144
250
  </div>
145
251
  )}
146
252
 
147
253
  {state === 'error' && (
148
- <div className="flex items-center gap-2 text-xs text-destructive">
149
- <AlertCircle size={13} />
150
- {errorMsg}
254
+ <div className="space-y-2">
255
+ <div className="flex items-center gap-2 text-xs text-destructive">
256
+ <AlertCircle size={13} />
257
+ {updateError || errorMsg}
258
+ </div>
259
+ {updateError && (
260
+ <button
261
+ onClick={handleRetry}
262
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
263
+ >
264
+ <RefreshCw size={12} />
265
+ {u?.retryButton ?? 'Retry Update'}
266
+ </button>
267
+ )}
151
268
  </div>
152
269
  )}
153
270
  </div>
@@ -60,6 +60,18 @@ async function removeSession(id: string): Promise<void> {
60
60
  }
61
61
  }
62
62
 
63
+ async function removeSessions(ids: string[]): Promise<void> {
64
+ try {
65
+ await fetch('/api/ask-sessions', {
66
+ method: 'DELETE',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body: JSON.stringify({ ids }),
69
+ });
70
+ } catch {
71
+ // ignore persistence errors
72
+ }
73
+ }
74
+
63
75
  export function useAskSession(currentFile?: string) {
64
76
  const [messages, setMessages] = useState<Message[]>([]);
65
77
  const [sessions, setSessions] = useState<ChatSession[]>([]);
@@ -166,6 +178,17 @@ export function useAskSession(currentFile?: string) {
166
178
  [activeSessionId, currentFile, sessions],
167
179
  );
168
180
 
181
+ const clearAllSessions = useCallback(() => {
182
+ const allIds = sessions.map(s => s.id);
183
+ void removeSessions(allIds);
184
+
185
+ const fresh = createSession(currentFile);
186
+ setActiveSessionId(fresh.id);
187
+ setMessages([]);
188
+ setSessions([fresh]);
189
+ void upsertSession(fresh);
190
+ }, [currentFile, sessions]);
191
+
169
192
  return {
170
193
  messages,
171
194
  setMessages,
@@ -177,5 +200,6 @@ export function useAskSession(currentFile?: string) {
177
200
  resetSession,
178
201
  loadSession,
179
202
  deleteSession,
203
+ clearAllSessions,
180
204
  };
181
205
  }