diff --git a/internal/server/handlers_dashboard.go b/internal/server/handlers_dashboard.go index 07e3fdf8..60bb41ab 100644 --- a/internal/server/handlers_dashboard.go +++ b/internal/server/handlers_dashboard.go @@ -31,6 +31,15 @@ type DashboardResponse struct { // workspace's agent loop is wired up and the banner stops nagging // the user on this workspace. HasAgentActivity bool `json:"has_agent_activity"` + // NeedsOnboarding is true when the workspace has zero items with + // source != 'template' โ€” i.e. nothing beyond what the template + // seeded. Mirrors the canonical AgentBootstrap.NeedsOnboarding + // flag (PLAN-1496 / TASK-1504) so the web UI can render its + // onboarding nudge without making a second bootstrap call. Flips + // false the moment any user/agent-sourced item exists; the + // dashboard's onboarding banner uses this as its sole gating + // signal post IDEA-1516 / TASK-1530. + NeedsOnboarding bool `json:"needs_onboarding"` // OnboardingSeed identifies the seeded onboarding entry point for // the workspace (e.g. IDEA-1 for `startup`, BACK-1 for `scrum`, // FEAT-1 for `product`) when present and untouched. The web UI's @@ -366,6 +375,18 @@ func (s *Server) buildDashboardResponse(workspaceID string, r *http.Request) (*D } resp.HasAgentActivity = hasAgent + // needs_onboarding mirrors AgentBootstrap.NeedsOnboarding (TASK-1504): + // true when the workspace has zero items with source != 'template'. + // Web UI's onboarding nudge banner (TASK-1530) reads this from the + // dashboard fetch the page already does, so no second round-trip + // against the heavier bootstrap endpoint is needed. Predicate is the + // same EXISTS-backed store helper bootstrap uses. + hasUserItems, err := s.store.WorkspaceHasUserCreatedItems(workspaceID) + if err != nil { + return nil, err + } + resp.NeedsOnboarding = !hasUserItems + // Summary: items grouped by collection slug and status field allItems, err := s.store.ListItems(workspaceID, models.ItemListParams{CollectionIDs: dashCollIDs, ItemIDs: dashItemIDs}) if err != nil { diff --git a/web/src/lib/components/OnboardingChecklist.svelte b/web/src/lib/components/OnboardingChecklist.svelte deleted file mode 100644 index 2d47c6dc..00000000 --- a/web/src/lib/components/OnboardingChecklist.svelte +++ /dev/null @@ -1,368 +0,0 @@ - - -
-
-
-

Set up your workspace

-

Complete these steps to get the most out of Pad.

-
- {#if ondismiss} - - {/if} -
- -
- {completedCount} of {steps.length} complete -
-
-
-
- -
    - {#each steps as step (step.title)} -
  1. -
    - {#if step.done} - - - - - {:else} - - - - {/if} -
    -
    - {step.title} - {#if !step.done} - - Try: {step.hint} - - - {/if} -
    -
  2. - {/each} -
- - -
- - diff --git a/web/src/lib/components/OnboardingIdeaBanner.svelte b/web/src/lib/components/OnboardingIdeaBanner.svelte deleted file mode 100644 index 79f2a1ce..00000000 --- a/web/src/lib/components/OnboardingIdeaBanner.svelte +++ /dev/null @@ -1,150 +0,0 @@ - - -
- -
-

Your workspace has a starting point waiting.

-

- Open a fresh agent session โ€” Claude Code, Cursor, Codex, whatever you have โ€” - and say: -

-
- {triggerPhrase} - -
-

- {primaryRef} is a note from your future self to whoever's helping you set up. - The agent will read it and walk through your project with you, capturing - what you tell it โ€” using your real work, not toy data. - Read it first - if you'd like to see what's there. -

-
-
- - diff --git a/web/src/lib/components/OnboardingNudgeBanner.svelte b/web/src/lib/components/OnboardingNudgeBanner.svelte new file mode 100644 index 00000000..6a89a606 --- /dev/null +++ b/web/src/lib/components/OnboardingNudgeBanner.svelte @@ -0,0 +1,183 @@ + + +
+ +
+ Set up your workspace +

+ Your workspace is ready. Connect your agent and type + /pad onboard to walk through setting it up. +

+
+ + Connect agent → + + +
+ + diff --git a/web/src/lib/types/index.ts b/web/src/lib/types/index.ts index 8c9b1dd9..7d6f7bda 100644 --- a/web/src/lib/types/index.ts +++ b/web/src/lib/types/index.ts @@ -751,6 +751,11 @@ export interface DashboardResponse { // the underlying store query also matches). Drives the connect-agent // banner's auto-hide. has_agent_activity: boolean; + // needs_onboarding mirrors AgentBootstrap.NeedsOnboarding (PLAN-1496 / + // TASK-1504): true when the workspace has zero items with + // source != 'template'. Drives the post-IDEA-1516 onboarding nudge + // banner. Flips false the moment any user/agent-sourced item exists. + needs_onboarding: boolean; // onboarding_seed identifies the seeded onboarding entry for the // workspace (e.g. IDEA-1 for `startup`, BACK-1 for `scrum`, // FEAT-1 for `product`). Present + active drives the diff --git a/web/src/routes/[username]/[workspace]/+page.svelte b/web/src/routes/[username]/[workspace]/+page.svelte index 5870da21..bfc7b141 100644 --- a/web/src/routes/[username]/[workspace]/+page.svelte +++ b/web/src/routes/[username]/[workspace]/+page.svelte @@ -8,8 +8,7 @@ import { uiStore } from '$lib/stores/ui.svelte'; import { syncService } from '$lib/services/sync.svelte'; import { relativeTime } from '$lib/utils/markdown'; - import OnboardingChecklist from '$lib/components/OnboardingChecklist.svelte'; - import OnboardingIdeaBanner from '$lib/components/OnboardingIdeaBanner.svelte'; + import OnboardingNudgeBanner from '$lib/components/OnboardingNudgeBanner.svelte'; import ConnectWorkspaceModal from '$lib/components/ConnectWorkspaceModal.svelte'; import CreateCollectionModal from '$lib/components/collections/CreateCollectionModal.svelte'; import { collectionStore } from '$lib/stores/collections.svelte'; @@ -67,13 +66,13 @@ if (mem !== null) isOwner = mem.role === 'owner'; }); - // The dashboard response carries an `onboarding_seed` field when the - // workspace has a seeded onboarding primary (IDEA-1 / BACK-1 / FEAT-1 - // per template). The OnboardingIdeaBanner shows only when that seed - // is still active (status equals its initial value โ€” agent has not - // yet engaged). The server computes `active` so the frontend doesn't - // need a per-collection "what's the initial status" map. - let onboardingSeed = $derived(dashboard?.onboarding_seed); + // Post IDEA-1516 / TASK-1530: the canonical onboarding signal is + // `dashboard.needs_onboarding` (mirrors AgentBootstrap.NeedsOnboarding + // from PLAN-1496 / TASK-1504). The old `onboarding_seed` field still + // rides on the dashboard response (its backend cleanup is out of + // scope) but no longer has a consumer in this page โ€” the + // OnboardingIdeaBanner that read it was retired with this task. + let needsOnboarding = $derived(dashboard?.needs_onboarding ?? false); // Sync dismissed state from localStorage when workspace changes $effect(() => { @@ -271,44 +270,31 @@ - - - {#if onboardingSeed?.active && !onboardingDismissed} + + + {#if needsOnboarding && !onboardingDismissed}
- (connectOpen = true)} + ondismiss={dismissOnboarding} />
- {/if} - {#if totalItems === 0 && !onboardingDismissed} -
- c.slug)} ondismiss={dismissOnboarding} /> - -
- {:else if totalItems === 0 && onboardingDismissed} + {:else if needsOnboarding && onboardingDismissed}
@@ -649,63 +635,6 @@ flex-direction: column; gap: var(--space-3); } - /* Connect-your-local-project card โ€” sibling under OnboardingChecklist. */ - .connect-card { - display: flex; - align-items: center; - gap: var(--space-3); - width: 100%; - padding: var(--space-3) var(--space-4); - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - text-align: left; - cursor: pointer; - color: inherit; - transition: border-color 0.15s, background 0.15s, transform 0.05s; - } - .connect-card:hover { - border-color: var(--accent-blue); - background: color-mix(in srgb, var(--accent-blue) 4%, var(--bg-secondary)); - } - .connect-card:active { - transform: translateY(1px); - } - .connect-card-icon { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - border-radius: var(--radius); - background: var(--bg-tertiary); - color: var(--accent-blue); - flex-shrink: 0; - } - .connect-card-body { - display: flex; - flex-direction: column; - gap: 2px; - flex: 1; - min-width: 0; - } - .connect-card-title { - font-size: 0.95em; - font-weight: 600; - color: var(--text-primary); - } - .connect-card-subtitle { - font-size: 0.82em; - color: var(--text-muted); - } - .connect-card-cta { - font-size: 1.1em; - color: var(--text-muted); - flex-shrink: 0; - } - .connect-card:hover .connect-card-cta { - color: var(--accent-blue); - } .onboarding-reshow { margin-bottom: var(--space-4); }