From c7fee39d515c4467c98d75c88e5430c4c1ce0e75 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 22:34:19 +0000 Subject: [PATCH 1/2] feat(frontend): guided first-run onboarding on an empty workspace home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A freshly-provisioned single-user forge has no repositories, so the workspace home rendered an empty summary strip over an empty activity stream — reading as broken on first run. Swap in a guided onboarding panel (create / import action cards + palette and comtrya.cue tips) gated on a resolved, zero-repository workspace so a slow load never flashes it. --- .../src/routes/WorkspaceHome.onboard.test.ts | 39 +++++ frontend/src/routes/WorkspaceHome.vue | 67 ++++++++- frontend/src/styles.css | 140 ++++++++++++++++++ 3 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 frontend/src/routes/WorkspaceHome.onboard.test.ts diff --git a/frontend/src/routes/WorkspaceHome.onboard.test.ts b/frontend/src/routes/WorkspaceHome.onboard.test.ts new file mode 100644 index 00000000..ca84680c --- /dev/null +++ b/frontend/src/routes/WorkspaceHome.onboard.test.ts @@ -0,0 +1,39 @@ +/** + * Tests for the WorkspaceHome first-run onboarding gate. + * + * WorkspaceHome swaps its normal activity/projects grid for a guided + * onboarding panel when the workspace has resolved with zero + * repositories. The predicate is mirrored here (matching the + * RepoTabs / AccountNav test convention) so it stays fast and + * dependency-free; any change to `isFreshWorkspace` in the SFC must + * be reflected here. + */ + +import { describe, expect, test } from "bun:test"; + +type LoadState = "loading" | "ready" | "error"; + +// Mirrors `isFreshWorkspace` in WorkspaceHome.vue. +function isFreshWorkspace(loadState: LoadState, repoCount: number): boolean { + return loadState === "ready" && repoCount === 0; +} + +describe("WorkspaceHome onboarding gate", () => { + test("shows onboarding once a resolved workspace has no repos", () => { + expect(isFreshWorkspace("ready", 0)).toBe(true); + }); + + test("hides onboarding while the workspace is still loading", () => { + // Avoid flashing the first-run panel during a slow load. + expect(isFreshWorkspace("loading", 0)).toBe(false); + }); + + test("hides onboarding on a load error", () => { + expect(isFreshWorkspace("error", 0)).toBe(false); + }); + + test("hides onboarding as soon as any repository exists", () => { + expect(isFreshWorkspace("ready", 1)).toBe(false); + expect(isFreshWorkspace("ready", 12)).toBe(false); + }); +}); diff --git a/frontend/src/routes/WorkspaceHome.vue b/frontend/src/routes/WorkspaceHome.vue index 317d012f..72a35aad 100644 --- a/frontend/src/routes/WorkspaceHome.vue +++ b/frontend/src/routes/WorkspaceHome.vue @@ -63,6 +63,26 @@ const workspace = computed(() => payload.value?.workspace ?? { }); const repositories = computed(() => workspace.value.repositories); const extensionCount = computed(() => payload.value?.extensionInstallations?.length ?? 0); + +/** + * A freshly-provisioned single-user forge has no repositories yet. + * Rather than render an empty header strip over an empty activity + * stream — which reads as "broken" on first run — the home swaps to + * a guided first-run panel: the two ways to get a repo into the forge + * (create empty / import by URL, both served by `/new`) plus the two + * navigation affordances a newcomer won't discover on their own + * (⌘K palette, the `comtrya.cue` config model). Only shown once the + * workspace summary has resolved so a slow load doesn't flash it. + */ +const isFreshWorkspace = computed( + () => loadState.value === "ready" && repositories.value.length === 0, +); + +const isMac = + typeof navigator !== "undefined" + ? /mac|iphone|ipad/i.test(navigator.platform || navigator.userAgent || "") + : false; +const cmdLabel = computed(() => (isMac ? "⌘" : "Ctrl")); const extensionRuntime = computed( () => payload.value?.instance?.capabilities?.extensionRuntime ? "enabled" : "disabled", ); @@ -388,7 +408,52 @@ async function fetchWorkspaceHome(signal: AbortSignal): Promise{{ loadError }}

-
+
+
+ First run +

Bring your first repository in

+

+ This workspace is empty. Create a fresh repository or import one you + already have — both land here with their comtrya.cue + config evaluated and ready to browse. +

+
+ +
+ + + + Create a repository + Start empty and push your first commit over git. + + + + + + Import from a URL + Clone an existing repo into the forge by its remote URL. + + +
+ +
    +
  • + {{ cmdLabel }}K + opens the command palette — jump to any repo, issue, or pull. +
  • +
  • + A comtrya.cue at a repo root declares its visibility, + default branch, enabled extensions, and projects. +
  • +
+
+ +
Date: Sun, 31 May 2026 22:37:59 +0000 Subject: [PATCH 2/2] fix(frontend): add focus-visible indicator to onboarding action cards The first-run onboarding cards are large clickable RouterLinks with only a hover treatment; keyboard users tabbing to them got no visible focus ring. Add a :focus-visible style (accent border + soft ring) matching the composer/textarea focus pattern already in the sheet. --- frontend/src/styles.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/styles.css b/frontend/src/styles.css index be515ff4..3bfe690f 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -927,6 +927,12 @@ kbd { transform: translateY(-1px); } +.shell-app .home-onboard-card:focus-visible { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); +} + .shell-app .home-onboard-card.primary { border-color: var(--accent-line); background: var(--accent-soft);