Skip to content

Commit 1008e97

Browse files
Merge pull request #10 from offendingcommit/codex/page-loading-placeholders
[codex] Show structured page placeholders while loading
2 parents 9cc7d08 + 4c54899 commit 1008e97

13 files changed

Lines changed: 443 additions & 102 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,9 @@ jobs:
1717

1818
- run: make ci-web
1919

20-
cargo-check:
21-
name: Rust compile check
22-
runs-on: ubuntu-latest
23-
steps:
24-
- uses: actions/checkout@v4
25-
26-
- name: Install Linux dependencies
27-
run: |
28-
sudo apt-get update
29-
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
30-
31-
- uses: dtolnay/rust-toolchain@stable
32-
33-
- uses: Swatinem/rust-cache@v2
34-
with:
35-
workspaces: packages/desktop/src-tauri -> target
36-
37-
- uses: ./.github/actions/setup
38-
39-
- run: make ci-desktop
40-
4120
release:
4221
name: Release
43-
needs: [check, cargo-check]
22+
needs: [check]
4423
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
4524
runs-on: ubuntu-latest
4625
permissions:

AGENTS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# openconcho Agent Notes
2+
3+
## CI policy
4+
5+
- PR CI only runs the web checks.
6+
- Rust/Tauri compile-check is local-only for now because the Linux dependency setup on GitHub Actions is too slow for routine PR validation.
7+
8+
## Required local preflight
9+
10+
- Before pushing any change under `packages/desktop/**` or `packages/desktop/src-tauri/**`, run:
11+
- `pnpm --filter @openconcho/desktop cargo-check`
12+
13+
## Useful commands
14+
15+
- `make ci-web` — matches current PR CI
16+
- `pnpm --filter @openconcho/desktop cargo-check` — local desktop compile check

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Frontend UI for self-hosted Honcho instances — browse memories, peers, session
1717
| `make test` | Vitest (unit + integration), excludes `e2e/` |
1818
| `make test-e2e` | Playwright e2e (uncached) |
1919
| `make check` | lint + typecheck + test |
20+
| `pnpm --filter @openconcho/desktop cargo-check` | Local Rust/Tauri compile check before pushing desktop changes |
2021
| `pnpm --filter @openconcho/web generate:api` | Regen `src/api/schema.d.ts` from `openapi.json` |
2122

2223
## Structure
@@ -58,3 +59,4 @@ Read `docs/architecture.md` for component overview, data flow, and design decisi
5859
- **Conventional commits enforced** — commitlint runs in husky `commit-msg`; body lines must be ≤100 chars
5960
- **Releases via semantic-release**`.releaserc.json`; commits land on `main`, no manual version bumps
6061
- **GitHub account** — push under `offendingcommit` (`gh auth switch` if needed)
62+
- **Desktop preflight is local** — Rust/Tauri compile-check no longer runs in PR CI; run `pnpm --filter @openconcho/desktop cargo-check` before pushing any `packages/desktop/**` or `packages/desktop/src-tauri/**` change

packages/web/src/components/conclusions/ConclusionBrowser.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { ConfirmDialog } from "@/components/shared/ConfirmDialog";
1414
import { EmptyState } from "@/components/shared/EmptyState";
1515
import { ErrorAlert } from "@/components/shared/ErrorAlert";
1616
import { FormModal } from "@/components/shared/FormModal";
17-
import { PageLoader } from "@/components/shared/LoadingSpinner";
1817
import { Pagination } from "@/components/shared/Pagination";
18+
import { Skeleton } from "@/components/shared/Skeleton";
1919
import { SortControl, type SortDir } from "@/components/shared/SortControl";
2020
import { TimestampChip } from "@/components/shared/TimestampChip";
2121
import { Button } from "@/components/ui/button";
@@ -193,7 +193,7 @@ export function ConclusionBrowser() {
193193
</form>
194194

195195
<ErrorAlert error={error instanceof Error ? error : null} />
196-
{(isLoading || (activeSearch && searchLoading)) && <PageLoader />}
196+
{(isLoading || (activeSearch && searchLoading)) && <ConclusionsSkeleton />}
197197

198198
{!isLoading && !searchLoading && displayedConclusions.length === 0 && (
199199
<EmptyState
@@ -315,6 +315,32 @@ export function ConclusionBrowser() {
315315
);
316316
}
317317

318+
function ConclusionsSkeleton() {
319+
return (
320+
<div className="space-y-3" aria-hidden="true">
321+
{Array.from({ length: 4 }).map((_, index) => (
322+
<div
323+
key={index}
324+
className="rounded-xl p-5"
325+
style={{ background: "var(--surface)", border: "1px solid var(--border)" }}
326+
>
327+
<Skeleton className="h-3 w-full rounded" />
328+
<Skeleton className="mt-2 h-3 w-[94%] rounded" />
329+
<Skeleton className="mt-2 h-3 w-[76%] rounded" />
330+
<div
331+
className="flex items-center gap-3 mt-4 pt-3"
332+
style={{ borderTop: "1px solid var(--border)" }}
333+
>
334+
<Skeleton className="h-3 w-20 rounded" />
335+
<Skeleton className="h-3 w-16 rounded" />
336+
<Skeleton className="ml-auto h-6 w-28 rounded-full" />
337+
</div>
338+
</div>
339+
))}
340+
</div>
341+
);
342+
}
343+
318344
function CreateConclusionModal({
319345
open,
320346
onClose,

packages/web/src/components/dashboard/Dashboard.tsx

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useState } from "react";
55
import { useQueueStatus, useWorkspaces } from "@/api/queries";
66
import type { components } from "@/api/schema.d.ts";
77
import { ErrorAlert } from "@/components/shared/ErrorAlert";
8-
import { PageLoader } from "@/components/shared/LoadingSpinner";
8+
import { Skeleton } from "@/components/shared/Skeleton";
99
import { Body, Muted, PageTitle, SectionHeading } from "@/components/ui/typography";
1010
import { useDemo } from "@/hooks/useDemo";
1111
import { COLOR } from "@/lib/constants";
@@ -182,7 +182,7 @@ export function Dashboard() {
182182
</motion.div>
183183

184184
<ErrorAlert error={error instanceof Error ? error : null} />
185-
{isLoading && <PageLoader />}
185+
{isLoading && <DashboardSkeleton />}
186186

187187
{!isLoading && workspaces.length > 0 && (
188188
<div className="space-y-4">
@@ -265,3 +265,64 @@ export function Dashboard() {
265265
</div>
266266
);
267267
}
268+
269+
function DashboardSkeleton() {
270+
return (
271+
<div className="space-y-4" aria-hidden="true">
272+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
273+
{Array.from({ length: 4 }).map((_, index) => (
274+
<div key={index} className="rounded-xl p-4 theme-card">
275+
<Skeleton accent={index === 0} className="h-8 w-16 rounded-lg" />
276+
<Skeleton className="mt-3 h-3 w-20 rounded" />
277+
</div>
278+
))}
279+
</div>
280+
281+
<div className="rounded-xl theme-card overflow-hidden">
282+
<div
283+
className="flex items-center gap-2 px-4 py-3"
284+
style={{ borderBottom: "1px solid var(--border)" }}
285+
>
286+
<Skeleton accent className="h-4 w-4 rounded" />
287+
<Skeleton className="h-4 w-28 rounded" />
288+
<Skeleton className="ml-1 h-3 w-32 rounded" />
289+
</div>
290+
291+
<div className="overflow-x-auto">
292+
<table className="w-full text-xs">
293+
<thead>
294+
<tr style={{ background: "var(--bg-3)" }}>
295+
{Array.from({ length: 6 }).map((_, index) => (
296+
<th key={index} className="px-4 py-2 text-left">
297+
<Skeleton className="h-3 w-14 rounded" />
298+
</th>
299+
))}
300+
</tr>
301+
</thead>
302+
<tbody>
303+
{Array.from({ length: 5 }).map((_, rowIndex) => (
304+
<tr key={rowIndex} style={{ borderTop: "1px solid var(--border)" }}>
305+
<td className="px-4 py-3">
306+
<Skeleton accent className="h-3 w-28 rounded" />
307+
</td>
308+
<td className="px-4 py-3">
309+
<div className="flex justify-end">
310+
<Skeleton className="h-3 w-20 rounded" />
311+
</div>
312+
</td>
313+
{Array.from({ length: 4 }).map((__, cellIndex) => (
314+
<td key={cellIndex} className="px-4 py-3">
315+
<div className="flex justify-end">
316+
<Skeleton className="h-3 w-8 rounded" />
317+
</div>
318+
</td>
319+
))}
320+
</tr>
321+
))}
322+
</tbody>
323+
</table>
324+
</div>
325+
</div>
326+
</div>
327+
);
328+
}

packages/web/src/components/peers/PeerDetail.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { JsonViewer } from "@/components/shared/JsonViewer";
2626
import { PageLoader } from "@/components/shared/LoadingSpinner";
2727
import { MarkdownRenderer } from "@/components/shared/MarkdownRenderer";
2828
import { PeerCardViewer } from "@/components/shared/PeerCardViewer";
29+
import { Skeleton } from "@/components/shared/Skeleton";
2930
import { Button } from "@/components/ui/button";
3031
import { Input, Textarea } from "@/components/ui/input";
3132
import {
@@ -143,7 +144,7 @@ export function PeerDetail() {
143144

144145
<div className="mt-6 space-y-4">
145146
<ErrorAlert error={error instanceof Error ? error : null} />
146-
{isLoading && <PageLoader />}
147+
{isLoading && <PeerDetailSkeleton />}
147148

148149
{!isLoading && peer && (
149150
<>
@@ -423,3 +424,45 @@ export function PeerDetail() {
423424
</div>
424425
);
425426
}
427+
428+
function PeerDetailSkeleton() {
429+
return (
430+
<div className="space-y-4" aria-hidden="true">
431+
<div className="rounded-xl p-5 theme-card">
432+
<Skeleton className="h-4 w-36 rounded" />
433+
<div className="mt-4 flex gap-2">
434+
<Skeleton className="h-10 flex-1 rounded-lg" />
435+
<Skeleton accent className="h-10 w-24 rounded-lg" />
436+
</div>
437+
</div>
438+
439+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
440+
{Array.from({ length: 2 }).map((_, index) => (
441+
<div key={index} className="rounded-xl p-5 theme-card">
442+
<div className="flex items-center justify-between mb-4">
443+
<Skeleton className="h-4 w-28 rounded" />
444+
<Skeleton className="h-8 w-16 rounded-lg" />
445+
</div>
446+
<Skeleton className="h-3 w-full rounded" />
447+
<Skeleton className="mt-2 h-3 w-[92%] rounded" />
448+
<Skeleton className="mt-2 h-3 w-[68%] rounded" />
449+
<Skeleton className="mt-4 h-24 w-full rounded-lg" />
450+
</div>
451+
))}
452+
</div>
453+
454+
<div className="rounded-xl p-5 theme-card">
455+
<Skeleton className="h-4 w-24 rounded" />
456+
<Skeleton className="mt-4 h-3 w-full rounded" />
457+
<Skeleton className="mt-2 h-3 w-[95%] rounded" />
458+
<Skeleton className="mt-2 h-3 w-[76%] rounded" />
459+
</div>
460+
461+
<div className="rounded-xl theme-card overflow-hidden">
462+
<div className="px-5 py-4">
463+
<Skeleton className="h-4 w-20 rounded" />
464+
</div>
465+
</div>
466+
</div>
467+
);
468+
}

packages/web/src/components/peers/PeerList.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import type { components } from "@/api/schema.d.ts";
77
import { EmptyState } from "@/components/shared/EmptyState";
88
import { ErrorAlert } from "@/components/shared/ErrorAlert";
99
import { JsonViewer } from "@/components/shared/JsonViewer";
10-
import { PageLoader } from "@/components/shared/LoadingSpinner";
1110
import { Pagination } from "@/components/shared/Pagination";
11+
import { Skeleton } from "@/components/shared/Skeleton";
1212
import { SortControl, type SortDir } from "@/components/shared/SortControl";
1313
import { MonoCaption, PageTitle } from "@/components/ui/typography";
1414
import { useDemo } from "@/hooks/useDemo";
@@ -178,7 +178,7 @@ export function PeerList() {
178178
)}
179179

180180
<ErrorAlert error={error instanceof Error ? error : null} />
181-
{isLoading && <PageLoader />}
181+
{isLoading && <PeerListSkeleton />}
182182

183183
{!isLoading && peers.length === 0 && (
184184
<EmptyState
@@ -329,3 +329,40 @@ export function PeerList() {
329329
</div>
330330
);
331331
}
332+
333+
function PeerListSkeleton() {
334+
return (
335+
<div aria-hidden="true">
336+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
337+
{Array.from({ length: 6 }).map((_, index) => (
338+
<div
339+
key={index}
340+
className="rounded-xl px-5 py-4"
341+
style={{
342+
background: COLOR.cardBaseBg,
343+
border: `1px solid ${COLOR.cardBaseBorder}`,
344+
}}
345+
>
346+
<div className="flex items-center justify-between">
347+
<Skeleton accent className="h-4 w-40 rounded" />
348+
<Skeleton className="h-4 w-4 rounded" />
349+
</div>
350+
<div className="mt-3 flex items-center gap-2 flex-wrap">
351+
<Skeleton className="h-5 w-14 rounded-full" />
352+
<Skeleton className="h-5 w-12 rounded-full" />
353+
</div>
354+
<div className="mt-3 flex items-center gap-2">
355+
<Skeleton className="h-3 w-3 rounded-full" />
356+
<Skeleton className="h-3 w-28 rounded" />
357+
</div>
358+
</div>
359+
))}
360+
</div>
361+
<div className="mt-4 flex items-center justify-between">
362+
<Skeleton className="h-8 w-20 rounded-lg" />
363+
<Skeleton className="h-4 w-16 rounded" />
364+
<Skeleton className="h-8 w-20 rounded-lg" />
365+
</div>
366+
</div>
367+
);
368+
}

packages/web/src/components/sessions/SessionList.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { useSessions } from "@/api/queries";
66
import type { components } from "@/api/schema.d.ts";
77
import { EmptyState } from "@/components/shared/EmptyState";
88
import { ErrorAlert } from "@/components/shared/ErrorAlert";
9-
import { PageLoader } from "@/components/shared/LoadingSpinner";
109
import { Pagination } from "@/components/shared/Pagination";
10+
import { Skeleton } from "@/components/shared/Skeleton";
1111
import { SortControl, type SortDir } from "@/components/shared/SortControl";
1212
import { MonoCaption, PageTitle } from "@/components/ui/typography";
1313
import { useDemo } from "@/hooks/useDemo";
@@ -105,7 +105,7 @@ export function SessionList() {
105105
</motion.div>
106106

107107
<ErrorAlert error={error instanceof Error ? error : null} />
108-
{isLoading && <PageLoader />}
108+
{isLoading && <SessionListSkeleton />}
109109

110110
{!isLoading && sessions.length === 0 && (
111111
<EmptyState
@@ -204,3 +204,40 @@ export function SessionList() {
204204
</div>
205205
);
206206
}
207+
208+
function SessionListSkeleton() {
209+
return (
210+
<div aria-hidden="true">
211+
<div className="space-y-2">
212+
{Array.from({ length: 5 }).map((_, index) => (
213+
<div
214+
key={index}
215+
className="rounded-xl px-5 py-4"
216+
style={{
217+
background: COLOR.cardBaseBg,
218+
border: `1px solid ${COLOR.cardBaseBorder}`,
219+
}}
220+
>
221+
<div className="flex items-center justify-between">
222+
<Skeleton accent className="h-4 w-44 rounded" />
223+
<div className="flex items-center gap-2">
224+
{index % 2 === 0 && <Skeleton className="h-4 w-12 rounded-full" />}
225+
<Skeleton className="h-4 w-4 rounded" />
226+
</div>
227+
</div>
228+
<div className="mt-3 flex items-center gap-2">
229+
<Skeleton className="h-3 w-3 rounded-full" />
230+
<Skeleton className="h-3 w-28 rounded" />
231+
<Skeleton className="h-5 w-16 rounded-md" />
232+
</div>
233+
</div>
234+
))}
235+
</div>
236+
<div className="mt-4 flex items-center justify-between">
237+
<Skeleton className="h-8 w-20 rounded-lg" />
238+
<Skeleton className="h-4 w-16 rounded" />
239+
<Skeleton className="h-8 w-20 rounded-lg" />
240+
</div>
241+
</div>
242+
);
243+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { cn } from "@/lib/utils";
2+
3+
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
4+
accent?: boolean;
5+
}
6+
7+
export function Skeleton({ accent = false, className, ...props }: SkeletonProps) {
8+
return (
9+
<div
10+
aria-hidden="true"
11+
className={cn("theme-skeleton rounded-md", accent && "theme-skeleton--accent", className)}
12+
{...props}
13+
/>
14+
);
15+
}

0 commit comments

Comments
 (0)