omarsol Claude Fable 5 commited on
Commit
117a59c
·
1 Parent(s): 51484cc

Show a loading skeleton in the sidebar until the tool registry arrives

Browse files

Before the /api/tools response lands, the sidebar rendered its empty
state: a "0 of 0 on" count and no rows, which flashed for a moment on
every load (longer on the deployed Space). Distinguish loading from
empty: hide the count (screen readers get a loading note) and render
placeholder rows mirroring the loaded layout. The skeleton appears only
on the true first load; model-switch refetches keep the old list and
fetch failures still show the error message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

frontend/components/chat-shell.tsx CHANGED
@@ -498,6 +498,7 @@ export function ChatShell() {
498
  {!sidebarCollapsed ? (
499
  <div className="hidden lg:block lg:h-full lg:min-h-0">
500
  <SourceSidebar
 
501
  onNewChat={handleNewChat}
502
  onToggleSource={toggleSource}
503
  onToggleTool={toggleTool}
@@ -519,6 +520,7 @@ export function ChatShell() {
519
  />
520
  <div className="absolute inset-y-0 left-0 w-[300px] max-w-[85vw] p-2">
521
  <SourceSidebar
 
522
  onNewChat={() => {
523
  handleNewChat();
524
  setSidebarOpen(false);
 
498
  {!sidebarCollapsed ? (
499
  <div className="hidden lg:block lg:h-full lg:min-h-0">
500
  <SourceSidebar
501
+ loading={tools.length === 0 && !sourceError}
502
  onNewChat={handleNewChat}
503
  onToggleSource={toggleSource}
504
  onToggleTool={toggleTool}
 
520
  />
521
  <div className="absolute inset-y-0 left-0 w-[300px] max-w-[85vw] p-2">
522
  <SourceSidebar
523
+ loading={tools.length === 0 && !sourceError}
524
  onNewChat={() => {
525
  handleNewChat();
526
  setSidebarOpen(false);
frontend/components/source-sidebar.tsx CHANGED
@@ -36,6 +36,7 @@ type SourceSidebarProps = {
36
  tools: TutorTool[];
37
  onClose?: () => void;
38
  onCollapse?: () => void;
 
39
  };
40
 
41
  type ToggleToolMeta = {
@@ -73,6 +74,7 @@ export function SourceSidebar({
73
  tools,
74
  onClose,
75
  onCollapse,
 
76
  }: SourceSidebarProps) {
77
  const retrievalTool = tools.find(
78
  (tool): tool is Extract<TutorTool, { kind: "configurable" }> =>
@@ -170,12 +172,16 @@ export function SourceSidebar({
170
  Tools
171
  </span>
172
  </div>
173
- <span
174
- className="eyebrow text-[10.5px] text-[var(--muted)]"
175
- title={`${activeCount} of ${totalCount} tools on`}
176
- >
177
- {activeCount} of {totalCount} on
178
- </span>
 
 
 
 
179
  </div>
180
  <p className="text-[11px] leading-[1.4] text-[var(--muted)]">
181
  Toggle tools, pick sources.
@@ -183,6 +189,7 @@ export function SourceSidebar({
183
  </div>
184
 
185
  <div className="scrollbar-thin min-h-0 flex-1 space-y-2 overflow-y-auto pr-0.5">
 
186
  {retrievalTool ? (
187
  <RetrievalTool
188
  tool={retrievalTool}
@@ -235,6 +242,23 @@ export function SourceSidebar({
235
  );
236
  }
237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  function RetrievalTool({
239
  isOpen,
240
  onOpenChange,
 
36
  tools: TutorTool[];
37
  onClose?: () => void;
38
  onCollapse?: () => void;
39
+ loading?: boolean;
40
  };
41
 
42
  type ToggleToolMeta = {
 
74
  tools,
75
  onClose,
76
  onCollapse,
77
+ loading = false,
78
  }: SourceSidebarProps) {
79
  const retrievalTool = tools.find(
80
  (tool): tool is Extract<TutorTool, { kind: "configurable" }> =>
 
172
  Tools
173
  </span>
174
  </div>
175
+ {loading ? (
176
+ <span className="sr-only">Loading tools and sources</span>
177
+ ) : (
178
+ <span
179
+ className="eyebrow text-[10.5px] text-[var(--muted)]"
180
+ title={`${activeCount} of ${totalCount} tools on`}
181
+ >
182
+ {activeCount} of {totalCount} on
183
+ </span>
184
+ )}
185
  </div>
186
  <p className="text-[11px] leading-[1.4] text-[var(--muted)]">
187
  Toggle tools, pick sources.
 
189
  </div>
190
 
191
  <div className="scrollbar-thin min-h-0 flex-1 space-y-2 overflow-y-auto pr-0.5">
192
+ {loading ? <SidebarSkeleton /> : null}
193
  {retrievalTool ? (
194
  <RetrievalTool
195
  tool={retrievalTool}
 
242
  );
243
  }
244
 
245
+ function SidebarSkeleton() {
246
+ return (
247
+ <div aria-hidden className="space-y-2">
248
+ <div className="h-[38px] animate-pulse rounded-lg border border-[var(--line)] bg-[var(--surface-subtle)]" />
249
+ <div className="ml-[11px] space-y-1.5 border-l border-[var(--line)] pl-3">
250
+ {Array.from({ length: 7 }).map((_, index) => (
251
+ <div
252
+ key={index}
253
+ className="h-[31px] animate-pulse rounded-lg border border-[var(--line)] bg-[var(--surface-soft)]"
254
+ style={{ animationDelay: `${index * 90}ms` }}
255
+ />
256
+ ))}
257
+ </div>
258
+ </div>
259
+ );
260
+ }
261
+
262
  function RetrievalTool({
263
  isOpen,
264
  onOpenChange,