Skip to content

Commit 6c41ffc

Browse files
committed
feat(046): wizard v2 — 3 tabs, sidebar Setup, sectioned import w/ rename, activity panel
Related #46 Reframes the first-run wizard as a persistent, idempotent setup surface driven by the activation funnel telemetry (SynapBus #27345, 78.2% → 11.7% cliff at IDE connect). Replaces the linear v1 flow with three idempotent tabs (Clients · Servers · Verify), a top-pinned sidebar entry with an animated badge, and a passive Verify tab that flips green automatically on the first successful MCP `initialize` round-trip — closing the loop on the cliff in-product. ## Changes Spec - specs/046-local-first-onboarding/spec.md: appended a "v2 — Wizard Surface Redesign" section. v1 text preserved verbatim. Backend (Go) - internal/httpapi/onboarding.go: extended /api/v1/onboarding/state with first_mcp_client_ever, mcp_clients_seen_ever, incomplete_tab_count. ShouldShowWizard semantics widened to cover Verify. - internal/httpapi/server.go + internal/server/server.go + internal/runtime/runtime.go: new GetActivationFirstMCPClient() reads Spec 044's existing FirstMCPClientEver / MCPClientsSeenEver from the ActivationStore. No new BBolt buckets, no new MCP hook. - internal/httpapi/import.go: ImportFromPathRequest gains skip_quarantine query param ("Import as active") and a rename map applied after parsing — so the wizard can disambiguate cross-source name collisions like "mcpproxy" appearing in both Claude Code and Claude Desktop. Frontend (Vue 3 + Pinia) - frontend/src/components/OnboardingWizard.vue: full rewrite as a 3-tab modal at min(960px, 90vw) × min(640px, 85vh). Tabs are bidirectionally clickable; per-tab idempotent state. - Clients tab: detected sort -> always-pinned trio (Claude Code / Codex / Gemini) -> "Show N more" collapse. Inline security expander. - Servers tab: sectioned checkbox list with indented server rows under each section header; per-section "select all" with proper indeterminate state; cross-source name collisions render an inline rename pill ("→ <renamed>"); sticky non-scrollable footer with selection count, conflict count, and two equal-width action buttons (Import & quarantine, Import as active). Compact toggles for Docker isolation and quarantine_enabled with inline Docker- availability warning. Educational note about post-hoc scanning. - Verify tab: passive, flips green on FirstMCPClientEver. Three sample prompts annotated with the built-in tool each one exercises. Recent Activity panel below shows the last 5 records with status badges and "View all in Activity Log" link. - frontend/src/components/SidebarNav.vue: top-pinned "Setup" entry above Dashboard for personal edition. - frontend/src/views/Dashboard.vue: removed the v1 "Run setup wizard" button. - frontend/src/stores/onboarding.ts + types/api.ts + services/api.ts: store gains firstMCPClientEver, mcpClientsSeenEver, incompleteTabCount computeds. importServersFromPath supports skip_quarantine + rename. OpenAPI - oas/swagger.yaml + oas/docs.go: ImportFromPathRequest gains rename. Verification - specs/046-local-first-onboarding/verification/: 7 Playwright-captured PNGs and a self-contained report.html (1.4 MB, base64-embedded screenshots). ## Testing - internal/httpapi unit + contract tests pass; mocks updated. - frontend vue-tsc clean. - Playwright sweep on a fresh data dir: all scenarios pass; out-of-band curl confirms rename produces mcpproxy_claude_code / mcpproxy_claude_desktop with quarantined=true.
1 parent 4729cc1 commit 6c41ffc

83 files changed

Lines changed: 1959 additions & 540 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/src/components/OnboardingWizard.vue

Lines changed: 932 additions & 195 deletions
Large diffs are not rendered by default.

frontend/src/components/SidebarNav.vue

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,52 @@
131131

132132
<!-- Personal Edition: Grouped Menu -->
133133
<template v-else>
134+
<!-- Spec 046 v2: top-pinned Setup entry (above Dashboard).
135+
Shows badge with count of incomplete tabs while > 0; collapses
136+
to a quiet checkmark when all three tabs (clients/servers/verify)
137+
are satisfied. Click reopens the wizard at the first
138+
incomplete tab. -->
139+
<ul class="menu menu-sm gap-0.5 p-0 mb-1">
140+
<li>
141+
<a
142+
href="#"
143+
class="rounded-lg font-medium relative group"
144+
:class="setupIncomplete
145+
? 'bg-gradient-to-r from-primary/10 to-secondary/10 hover:from-primary/15 hover:to-secondary/15'
146+
: 'text-base-content/60'"
147+
:title="collapsed ? setupTitleAttr : ''"
148+
data-test="sidebar-setup"
149+
@click.prevent="onClickSetup"
150+
>
151+
<span class="relative inline-flex items-center justify-center">
152+
<IconSparkles
153+
class="w-5 h-5 shrink-0"
154+
:class="setupIncomplete ? 'text-primary' : 'text-base-content/40'"
155+
/>
156+
<!-- Pulse halo when incomplete -->
157+
<span
158+
v-if="setupIncomplete"
159+
class="absolute inline-flex h-5 w-5 rounded-full bg-primary opacity-30 animate-ping"
160+
aria-hidden="true"
161+
></span>
162+
</span>
163+
<span v-show="!collapsed" class="flex-1">
164+
<span v-if="setupIncomplete">Setup</span>
165+
<span v-else class="inline-flex items-center gap-1">
166+
<span>Setup</span>
167+
<span class="text-success text-xs">✓</span>
168+
</span>
169+
</span>
170+
<span
171+
v-if="setupIncomplete && setupCount > 0"
172+
class="badge badge-primary badge-sm"
173+
:class="collapsed ? 'absolute -top-1 -right-1 badge-xs' : ''"
174+
data-test="sidebar-setup-badge"
175+
>{{ setupCount }}</span>
176+
</a>
177+
</li>
178+
</ul>
179+
134180
<!-- Dashboard (solo top row, no section label) -->
135181
<ul class="menu menu-sm gap-0.5 p-0">
136182
<li>
@@ -342,15 +388,45 @@
342388
</template>
343389

344390
<script setup lang="ts">
345-
import { computed, h, type FunctionalComponent } from 'vue'
391+
import { computed, h, onMounted, type FunctionalComponent } from 'vue'
346392
import { useRoute, useRouter } from 'vue-router'
347393
import { useSystemStore } from '@/stores/system'
348394
import { useAuthStore } from '@/stores/auth'
395+
import { useOnboardingStore } from '@/stores/onboarding'
349396
350397
const route = useRoute()
351398
const router = useRouter()
352399
const systemStore = useSystemStore()
353400
const authStore = useAuthStore()
401+
const onboardingStore = useOnboardingStore()
402+
403+
// Spec 046 v2: badge count drives the sidebar Setup entry's pulse + count.
404+
// Refetched on mount; the wizard itself drives subsequent updates while open.
405+
const setupCount = computed(() => onboardingStore.incompleteTabCount)
406+
const setupIncomplete = computed(() => setupCount.value > 0)
407+
const setupTitleAttr = computed(() =>
408+
setupIncomplete.value
409+
? `Setup (${setupCount.value} step${setupCount.value === 1 ? '' : 's'} remaining)`
410+
: 'Setup ✓'
411+
)
412+
413+
function onClickSetup() {
414+
// Open the wizard via the store. If the user is on a deep route, they stay
415+
// there — the wizard is a modal and renders above whatever view is mounted.
416+
if (route.path !== '/') {
417+
router.push('/').then(() => onboardingStore.openWizard())
418+
} else {
419+
onboardingStore.openWizard()
420+
}
421+
}
422+
423+
onMounted(() => {
424+
// Pull initial state so the badge is correct on first render. Personal-
425+
// edition only — the surrounding template gates this for personal users.
426+
if (!authStore.isTeamsEdition) {
427+
void onboardingStore.fetchState()
428+
}
429+
})
354430
355431
const collapsed = computed(() => systemStore.sidebarCollapsed)
356432
@@ -437,6 +513,10 @@ const IconRepo = makeIcon(
437513
const IconSettings = makeIcon(
438514
'M10.3 3.6a1.5 1.5 0 013.4 0l.2 1.1a7 7 0 011.9.8l1-.6a1.5 1.5 0 012.1 2.1l-.6 1a7 7 0 01.8 1.9l1.1.2a1.5 1.5 0 010 3.4l-1.1.2a7 7 0 01-.8 1.9l.6 1a1.5 1.5 0 01-2.1 2.1l-1-.6a7 7 0 01-1.9.8l-.2 1.1a1.5 1.5 0 01-3.4 0l-.2-1.1a7 7 0 01-1.9-.8l-1 .6a1.5 1.5 0 01-2.1-2.1l.6-1a7 7 0 01-.8-1.9l-1.1-.2a1.5 1.5 0 010-3.4l1.1-.2a7 7 0 01.8-1.9l-.6-1a1.5 1.5 0 012.1-2.1l1 .6a7 7 0 011.9-.8l.2-1.1zM12 9a3 3 0 100 6 3 3 0 000-6z'
439515
)
516+
// Spec 046 v2: sparkles icon for the top-pinned Setup entry.
517+
const IconSparkles = makeIcon(
518+
'M12 3l1.6 4.6L18 9l-4.4 1.4L12 15l-1.6-4.6L6 9l4.4-1.4L12 3zm6 11l.8 2.4L21 17l-2.2.6L18 20l-.8-2.4L15 17l2.2-.6L18 14zM6 14l.8 2.4L9 17l-2.2.6L6 20l-.8-2.4L3 17l2.2-.6L6 14z'
519+
)
440520
441521
// Server edition menus (unchanged behavior)
442522
const teamsUserMenu = [

frontend/src/services/api.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -702,20 +702,29 @@ class APIService {
702702
return this.request<CanonicalConfigPathsResponse>('/api/v1/servers/import/paths')
703703
}
704704

705-
// Import servers from a file path on the server's filesystem
705+
// Import servers from a file path on the server's filesystem.
706+
// Spec 046 v2: skip_quarantine=true imports as already-trusted (skips
707+
// the quarantine holding state). Default false preserves the safe-by-
708+
// default posture for any caller that doesn't pass the flag.
706709
async importServersFromPath(params: {
707710
path: string
708711
format?: string
709712
server_names?: string[]
710713
preview?: boolean
714+
skip_quarantine?: boolean
715+
rename?: Record<string, string>
711716
}): Promise<APIResponse<ImportResponse>> {
712-
const url = `/api/v1/servers/import/path${params.preview ? '?preview=true' : ''}`
717+
const qs: string[] = []
718+
if (params.preview) qs.push('preview=true')
719+
if (params.skip_quarantine) qs.push('skip_quarantine=true')
720+
const url = `/api/v1/servers/import/path${qs.length ? '?' + qs.join('&') : ''}`
713721
return this.request<ImportResponse>(url, {
714722
method: 'POST',
715723
body: JSON.stringify({
716724
path: params.path,
717725
format: params.format,
718-
server_names: params.server_names
726+
server_names: params.server_names,
727+
rename: params.rename,
719728
})
720729
})
721730
}

frontend/src/stores/onboarding.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,13 @@ export const useOnboardingStore = defineStore('onboarding', () => {
3535
const hasConfiguredServer = computed(() => state.value?.has_configured_server ?? false)
3636
const isEngaged = computed(() => state.value?.state.engaged ?? false)
3737

38-
// The list of step IDs the wizard should render, in order. Derived
39-
// from the live predicates so the wizard adapts to state on each open.
38+
// Spec 046 v2 — passive Verify tab + sidebar badge sources.
39+
const firstMCPClientEver = computed(() => state.value?.first_mcp_client_ever ?? false)
40+
const mcpClientsSeenEver = computed<string[]>(() => state.value?.mcp_clients_seen_ever ?? [])
41+
const incompleteTabCount = computed(() => state.value?.incomplete_tab_count ?? 0)
42+
43+
// v1 visibleSteps kept for back-compat with any remaining caller. The v2
44+
// wizard renders a fixed three-tab surface and ignores this value.
4045
const visibleSteps = computed<Array<'connect' | 'server'>>(() => {
4146
const steps: Array<'connect' | 'server'> = []
4247
if (!hasConnectedClient.value) steps.push('connect')
@@ -153,6 +158,9 @@ export const useOnboardingStore = defineStore('onboarding', () => {
153158
hasConnectedClient,
154159
hasConfiguredServer,
155160
isEngaged,
161+
firstMCPClientEver,
162+
mcpClientsSeenEver,
163+
incompleteTabCount,
156164
visibleSteps,
157165
fetchState,
158166
mark,

frontend/src/types/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,10 @@ export interface OnboardingStateResponse {
692692
configured_server_count: number
693693
state: OnboardingState
694694
should_show_wizard: boolean
695+
// Spec 046 v2 — passive Verify tab + sidebar badge
696+
first_mcp_client_ever: boolean
697+
mcp_clients_seen_ever: string[]
698+
incomplete_tab_count: number
695699
}
696700

697701
export interface OnboardingMarkRequest {

frontend/src/views/Dashboard.vue

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,6 @@
121121
<button @click="showConnectModal = true" class="btn btn-primary btn-sm w-full gap-1">
122122
Connect Clients
123123
</button>
124-
<button @click="onboardingStore.openWizard()" class="btn btn-ghost btn-xs w-full gap-1" data-test="run-setup-wizard">
125-
Run setup wizard
126-
</button>
127124
<button @click="showAddServer = true" class="btn btn-secondary btn-outline btn-sm w-full gap-1">
128125
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
129126
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />

internal/httpapi/contracts_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ func (m *MockServerController) GetOnboardingState() (*storage.OnboardingState, e
345345
return &storage.OnboardingState{}, nil
346346
}
347347
func (m *MockServerController) SaveOnboardingState(_ *storage.OnboardingState) error { return nil }
348+
func (m *MockServerController) GetActivationFirstMCPClient() (bool, []string) { return false, nil }
348349

349350
// Test contract compliance for API responses
350351
func TestAPIContractCompliance(t *testing.T) {

internal/httpapi/import.go

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,20 @@ import (
1616

1717
// ImportRequest represents a request to import servers from JSON/TOML content
1818
type ImportRequest struct {
19-
Content string `json:"content"` // Raw JSON or TOML content
20-
Format string `json:"format,omitempty"` // Optional format hint
19+
Content string `json:"content"` // Raw JSON or TOML content
20+
Format string `json:"format,omitempty"` // Optional format hint
2121
ServerNames []string `json:"server_names,omitempty"` // Optional: import only these servers
2222
}
2323

2424
// ImportResponse represents the response from an import operation
2525
type ImportResponse struct {
26-
Format string `json:"format"`
27-
FormatName string `json:"format_name"`
28-
Summary configimport.ImportSummary `json:"summary"`
29-
Imported []ImportedServerResponse `json:"imported"`
30-
Skipped []configimport.SkippedServer `json:"skipped"`
31-
Failed []configimport.FailedServer `json:"failed"`
32-
Warnings []string `json:"warnings"`
26+
Format string `json:"format"`
27+
FormatName string `json:"format_name"`
28+
Summary configimport.ImportSummary `json:"summary"`
29+
Imported []ImportedServerResponse `json:"imported"`
30+
Skipped []configimport.SkippedServer `json:"skipped"`
31+
Failed []configimport.FailedServer `json:"failed"`
32+
Warnings []string `json:"warnings"`
3333
}
3434

3535
// ImportedServerResponse represents an imported server in the response
@@ -47,12 +47,12 @@ type ImportedServerResponse struct {
4747

4848
// CanonicalConfigPath represents a well-known config file path
4949
type CanonicalConfigPath struct {
50-
Name string `json:"name"` // Display name (e.g., "Claude Desktop")
51-
Format string `json:"format"` // Format identifier (e.g., "claude_desktop")
52-
Path string `json:"path"` // Full path to the config file
53-
Exists bool `json:"exists"` // Whether the file exists
54-
OS string `json:"os"` // Operating system (darwin, windows, linux)
55-
Description string `json:"description"` // Brief description
50+
Name string `json:"name"` // Display name (e.g., "Claude Desktop")
51+
Format string `json:"format"` // Format identifier (e.g., "claude_desktop")
52+
Path string `json:"path"` // Full path to the config file
53+
Exists bool `json:"exists"` // Whether the file exists
54+
OS string `json:"os"` // Operating system (darwin, windows, linux)
55+
Description string `json:"description"` // Brief description
5656
}
5757

5858
// CanonicalConfigPathsResponse represents the response for canonical config paths
@@ -164,6 +164,11 @@ type ImportFromPathRequest struct {
164164
Path string `json:"path"` // File path to import from
165165
Format string `json:"format,omitempty"` // Optional format hint
166166
ServerNames []string `json:"server_names,omitempty"` // Optional: import only these servers
167+
// Rename maps OriginalName → new name. Applied after parsing so the
168+
// caller can disambiguate cross-source name collisions (Spec 046 v2 —
169+
// e.g. "mcpproxy" → "mcpproxy_claude_code"). Keys not present in the
170+
// imported set are ignored.
171+
Rename map[string]string `json:"rename,omitempty"`
167172
}
168173

169174
// handleImportFromPath godoc
@@ -220,7 +225,7 @@ func (s *Server) handleImportFromPath(w http.ResponseWriter, r *http.Request) {
220225
preview := r.URL.Query().Get("preview") == "true"
221226

222227
// Use the common runImport function
223-
result, err := s.runImport(r, content, req.Format, req.ServerNames, preview)
228+
result, err := s.runImport(r, content, req.Format, req.ServerNames, preview, req.Rename)
224229
if err != nil {
225230
logger.Error("Import from path failed", "path", path, "error", err)
226231
s.writeError(w, r, http.StatusBadRequest, err.Error())
@@ -283,8 +288,8 @@ func (s *Server) handleImportServers(w http.ResponseWriter, r *http.Request) {
283288
}
284289
}
285290

286-
// Run import
287-
result, err := s.runImport(r, content, formatHint, serverNames, preview)
291+
// Run import (rename not supported via multipart upload — leave nil)
292+
result, err := s.runImport(r, content, formatHint, serverNames, preview, nil)
288293
if err != nil {
289294
logger.Error("Import failed", "error", err)
290295
s.writeError(w, r, http.StatusBadRequest, err.Error())
@@ -326,7 +331,7 @@ func (s *Server) handleImportServersJSON(w http.ResponseWriter, r *http.Request)
326331
preview := r.URL.Query().Get("preview") == "true"
327332

328333
// Run import
329-
result, err := s.runImport(r, []byte(req.Content), req.Format, req.ServerNames, preview)
334+
result, err := s.runImport(r, []byte(req.Content), req.Format, req.ServerNames, preview, nil)
330335
if err != nil {
331336
logger.Error("Import failed", "error", err)
332337
s.writeError(w, r, http.StatusBadRequest, err.Error())
@@ -336,14 +341,23 @@ func (s *Server) handleImportServersJSON(w http.ResponseWriter, r *http.Request)
336341
s.writeSuccess(w, result)
337342
}
338343

339-
// runImport executes the import logic and optionally applies the servers
340-
func (s *Server) runImport(r *http.Request, content []byte, formatHint string, serverNames []string, preview bool) (*ImportResponse, error) {
344+
// runImport executes the import logic and optionally applies the servers.
345+
// rename is an optional map of OriginalName → new name applied after parsing,
346+
// before adding the servers — used by the wizard to disambiguate cross-source
347+
// name collisions. May be nil.
348+
func (s *Server) runImport(r *http.Request, content []byte, formatHint string, serverNames []string, preview bool, rename map[string]string) (*ImportResponse, error) {
341349
logger := s.getRequestLogger(r)
342350

351+
// Spec 046 v2: opt-in trust path. Default behaviour is unchanged
352+
// (servers enter quarantine). The wizard's "Import active" path passes
353+
// `skip_quarantine=true` to import them as already-trusted.
354+
skipQuarantine := r.URL.Query().Get("skip_quarantine") == "true"
355+
343356
// Build import options
344357
opts := &configimport.ImportOptions{
345-
Preview: preview,
346-
Now: time.Now(),
358+
Preview: preview,
359+
SkipQuarantine: skipQuarantine,
360+
Now: time.Now(),
347361
}
348362

349363
// Parse format hint
@@ -378,6 +392,18 @@ func (s *Server) runImport(r *http.Request, content []byte, formatHint string, s
378392
return nil, err
379393
}
380394

395+
// Spec 046 v2: apply caller-supplied renames before persisting. Keyed
396+
// by OriginalName so the caller doesn't need to know the parser's name-
397+
// sanitization rules. Server.Name is rewritten in place; OriginalName is
398+
// preserved on the response so the caller can reconcile the mapping.
399+
if len(rename) > 0 {
400+
for i := range result.Imported {
401+
if newName, ok := rename[result.Imported[i].OriginalName]; ok && newName != "" {
402+
result.Imported[i].Server.Name = newName
403+
}
404+
}
405+
}
406+
381407
// Build response
382408
response := &ImportResponse{
383409
Format: string(result.Format),

internal/httpapi/onboarding.go

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,30 @@ type OnboardingStateResponse struct {
4141
State storage.OnboardingState `json:"state"`
4242

4343
// ShouldShowWizard is the derived flag the frontend uses to decide
44-
// whether to auto-show. True when not engaged and at least one
45-
// predicate is false.
44+
// whether to auto-show. True when not engaged and IncompleteTabCount > 0
45+
// (Spec 046 v2 — semantics widened to also count the Verify tab).
4646
ShouldShowWizard bool `json:"should_show_wizard"`
47+
48+
// --- Spec 046 v2 additions ---
49+
50+
// FirstMCPClientEver is true once any MCP client has successfully completed
51+
// an `initialize` round-trip with this mcpproxy. Sourced from the Spec 044
52+
// activation bucket. Drives the Verify tab's "green check" state.
53+
FirstMCPClientEver bool `json:"first_mcp_client_ever"`
54+
55+
// MCPClientsSeenEver is the capped list of recognized client names that
56+
// have ever called this mcpproxy. Names come from the MCP `initialize`
57+
// payload's `clientInfo.name` field, sanitized. Surfaces on the Verify tab
58+
// so the user can see whether their real IDE — not a test client — has
59+
// connected.
60+
MCPClientsSeenEver []string `json:"mcp_clients_seen_ever"`
61+
62+
// IncompleteTabCount is the number of wizard tabs whose state is incomplete.
63+
// Drives the sidebar Setup entry's badge. Formula:
64+
// +1 if HasConnectedClient == false
65+
// +1 if HasConfiguredServer == false
66+
// +1 if FirstMCPClientEver == false
67+
IncompleteTabCount int `json:"incomplete_tab_count"`
4768
}
4869

4970
// OnboardingMarkRequest is the request body for /api/v1/onboarding/mark
@@ -182,7 +203,24 @@ func (s *Server) computeOnboardingState() (*OnboardingStateResponse, error) {
182203
}
183204
resp.State = *state
184205

185-
resp.ShouldShowWizard = !state.Engaged && (!resp.HasConnectedClient || !resp.HasConfiguredServer)
206+
// Spec 046 v2: pull activation state for the Verify tab.
207+
resp.FirstMCPClientEver, resp.MCPClientsSeenEver = s.controller.GetActivationFirstMCPClient()
208+
if resp.MCPClientsSeenEver == nil {
209+
resp.MCPClientsSeenEver = []string{}
210+
}
211+
212+
// Badge formula: +1 per incomplete tab.
213+
if !resp.HasConnectedClient {
214+
resp.IncompleteTabCount++
215+
}
216+
if !resp.HasConfiguredServer {
217+
resp.IncompleteTabCount++
218+
}
219+
if !resp.FirstMCPClientEver {
220+
resp.IncompleteTabCount++
221+
}
222+
223+
resp.ShouldShowWizard = !state.Engaged && resp.IncompleteTabCount > 0
186224

187225
return resp, nil
188226
}

internal/httpapi/security_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,4 @@ func (m *baseController) GetOnboardingState() (*storage.OnboardingState, error)
342342
return &storage.OnboardingState{}, nil
343343
}
344344
func (m *baseController) SaveOnboardingState(_ *storage.OnboardingState) error { return nil }
345+
func (m *baseController) GetActivationFirstMCPClient() (bool, []string) { return false, nil }

0 commit comments

Comments
 (0)