Skip to content

Commit 3954283

Browse files
paddymulclaude
andcommitted
feat(#133): spinner + precise error + retry for the catalog data grid
The data tab loads the Buckaroo grid lazily: a spinner while the session resolves, the grid on success, and on timeout/error a panel with the reason, a retry button, and a pointer to buckaroo.log / the debug-tallyman skill. Metadata and code render immediately instead of blocking on Buckaroo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7242689 commit 3954283

4 files changed

Lines changed: 106 additions & 16 deletions

File tree

packages/app/src/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
AppError,
44
EntryDetail,
55
EntryCache,
6+
SessionResult,
67
NotebookFull,
78
DiffData,
89
ResultCache,
@@ -124,7 +125,7 @@ export const api = {
124125
return r.json() as Promise<{ project: string; cleared: number }>;
125126
}),
126127

127-
session: (project: string, hash: string): Promise<{ ws_url: string | null }> =>
128+
session: (project: string, hash: string): Promise<SessionResult> =>
128129
get(`/${project}/api/session/${hash}`),
129130

130131
resultCache: (project: string): Promise<ResultCache> =>

packages/app/src/pages/CatalogPage.tsx

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { recalcTarget } from "../recalcRefresh";
66
import { CatalogSidebar } from "../components/CatalogSidebar";
77
import { BuckarooEmbed } from "../components/BuckarooEmbed";
88
import { VegaChart } from "../components/VegaChart";
9-
import type { Entry, AppError, EntryDetail, EntryCache } from "../types";
9+
import type { Entry, AppError, EntryDetail, EntryCache, SessionResult } from "../types";
1010

1111
// ── Error banner ──────────────────────────────────────────────────────────────
1212

@@ -91,11 +91,16 @@ function EntryDetailPane({ project, hash }: { project: string; hash: string }) {
9191
.catch(() => setLoading(false));
9292
}, [project, hash]);
9393

94-
if (loading) return <div className="meta">loading…</div>;
94+
if (loading)
95+
return (
96+
<div className="spinner-row">
97+
<span className="spinner" /> loading…
98+
</div>
99+
);
95100
if (!detail) return <div className="meta">entry not found</div>;
96101

97102
const { manifest, alias, version, forensic_history, prompt_history, chart_spec,
98-
display_config, build_artifacts, total_rows, buckaroo_session, buckaroo_ws_base, code } = detail;
103+
display_config, build_artifacts, total_rows, code } = detail;
99104
const diffProvenance = display_config?.diff_provenance;
100105

101106
const handleSaveCode = async () => {
@@ -109,9 +114,6 @@ function EntryDetailPane({ project, hash }: { project: string; hash: string }) {
109114
}
110115
};
111116

112-
const wsUrl =
113-
buckaroo_session && buckaroo_ws_base ? `${buckaroo_ws_base}/ws/${buckaroo_session}` : null;
114-
115117
return (
116118
<div className="detail entry-detail">
117119
<div className="entry-header">
@@ -234,15 +236,7 @@ function EntryDetailPane({ project, hash }: { project: string; hash: string }) {
234236
{" "}matched
235237
</div>
236238
)}
237-
{wsUrl ? (
238-
<BuckarooEmbed wsUrl={wsUrl} className="buckaroo-embed" />
239-
) : (
240-
<div className="meta">
241-
{total_rows > 0
242-
? `${total_rows} rows (Buckaroo not available — run tallyman with --buckaroo)`
243-
: "no rows"}
244-
</div>
245-
)}
239+
<BuckarooDataPane project={project} hash={manifest.content_hash} totalRows={total_rows} />
246240
</div>
247241

248242
{tab === "code" && (
@@ -306,6 +300,85 @@ function EntryDetailPane({ project, hash }: { project: string; hash: string }) {
306300
);
307301
}
308302

303+
// ── Lazy Buckaroo grid: spinner → grid, or a precise error + retry (#133) ─────
304+
305+
function BuckarooDataPane({ project, hash, totalRows }: { project: string; hash: string; totalRows: number }) {
306+
const [result, setResult] = useState<SessionResult | null>(null);
307+
const [attempt, setAttempt] = useState(0);
308+
309+
useEffect(() => {
310+
let cancelled = false;
311+
setResult(null);
312+
api
313+
.session(project, hash)
314+
.then((d) => {
315+
if (!cancelled) setResult(d);
316+
})
317+
.catch((e) => {
318+
if (!cancelled)
319+
setResult({ status: "error", ws_url: null, detail: e instanceof Error ? e.message : String(e) });
320+
});
321+
return () => {
322+
cancelled = true;
323+
};
324+
}, [project, hash, attempt]);
325+
326+
if (result === null) {
327+
return (
328+
<div className="buckaroo-loading">
329+
<span className="spinner" /> loading data…
330+
</div>
331+
);
332+
}
333+
if (result.status === "ok" && result.ws_url) {
334+
return <BuckarooEmbed wsUrl={result.ws_url} className="buckaroo-embed" />;
335+
}
336+
337+
const retry = () => setAttempt((n) => n + 1);
338+
339+
if (result.status === "unavailable" || result.status === "no_build") {
340+
return (
341+
<div className="meta">
342+
{result.status === "no_build"
343+
? "no build for this entry"
344+
: totalRows > 0
345+
? `${totalRows} rows (Buckaroo not available — run tallyman with --buckaroo)`
346+
: "no rows"}
347+
{result.status === "unavailable" && (
348+
<>
349+
{" · "}
350+
<button type="button" className="link-btn" onClick={retry}>
351+
retry
352+
</button>
353+
</>
354+
)}
355+
</div>
356+
);
357+
}
358+
359+
// timeout | error — surface what went wrong and a way out.
360+
return (
361+
<div className="buckaroo-error">
362+
<strong>
363+
{result.status === "timeout"
364+
? "Buckaroo timed out loading this entry."
365+
: "Buckaroo couldn’t load this entry."}
366+
</strong>
367+
{result.detail && <div className="meta detail">{result.detail}</div>}
368+
<div className="meta">
369+
<button type="button" className="link-btn" onClick={retry}>
370+
retry
371+
</button>
372+
{" · to debug, check "}
373+
<code>~/.tallyman-notebooks/projects/{project}/buckaroo.log</code>
374+
{" and the companion log, or run the "}
375+
<code>debug-tallyman</code>
376+
{" skill."}
377+
</div>
378+
</div>
379+
);
380+
}
381+
309382
// ── Per-expression cache footprint (metadata tab) ─────────────────────────────
310383

311384
function ShareBar({ value, total }: { value: number; total: number }) {

packages/app/src/styles.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,16 @@ table.cache-table .muted { color: var(--muted); }
534534
.danger-btn:hover:not(:disabled) { background: #7a2525; }
535535
.danger-btn:disabled { opacity: 0.4; cursor: default; }
536536

537+
/* loading spinner + lazy Buckaroo grid states (#133) */
538+
@keyframes tm-spin { to { transform: rotate(360deg); } }
539+
.spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--line); border-top-color: var(--accent); border-radius: 50%; animation: tm-spin 0.7s linear infinite; vertical-align: -2px; }
540+
.spinner-row { color: var(--muted); display: flex; align-items: center; gap: 8px; padding: 8px 0; }
541+
.buckaroo-loading { color: var(--muted); display: flex; align-items: center; gap: 8px; min-height: 120px; }
542+
.buckaroo-error { background: #3a1e1e; border: 1px solid #7a3a3a; border-radius: 4px; padding: 12px; color: #ffd9d9; }
543+
.buckaroo-error .detail { color: #e8b4b8; margin: 4px 0; white-space: pre-wrap; }
544+
.buckaroo-error code { background: rgba(0, 0, 0, 0.25); padding: 1px 4px; border-radius: 3px; }
545+
.link-btn { background: none; border: none; color: var(--accent); cursor: pointer; font: inherit; padding: 0; text-decoration: underline; }
546+
537547
/* per-expression cache metadata tab */
538548
.cache-meta { padding: 2px; }
539549
.cache-meta .cache-detail { font-size: 11px; white-space: normal; max-width: 420px; }

packages/app/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ export interface EntryCache {
187187
total_formatted: string;
188188
}
189189

190+
export interface SessionResult {
191+
status: "ok" | "unavailable" | "no_build" | "timeout" | "error";
192+
ws_url: string | null;
193+
detail: string;
194+
}
195+
190196
export interface Projects {
191197
active: string | null;
192198
available: string[];

0 commit comments

Comments
 (0)