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);