Skip to content

feat(desktop): first-run onboarding wizard with safeStorage#1

Merged
hqhq1025 merged 1 commit intomainfrom
wt/onboarding
Apr 18, 2026
Merged

feat(desktop): first-run onboarding wizard with safeStorage#1
hqhq1025 merged 1 commit intomainfrom
wt/onboarding

Conversation

@hqhq1025
Copy link
Copy Markdown
Collaborator

Goal

Ship the 3-step first-run onboarding flow described in
docs/research/06-api-onboarding-ux.md so a new user can paste an API key,
see it validated live against the provider's /v1/models, pick default
models, and land in chat — all in under 90 seconds — without ever leaving
plaintext credentials on disk.

What's in

  • Welcome step — path picker (Try free / Use my key / Ollama placeholder)
  • Paste-key step — auto-detect provider by prefix, 500ms-debounce live
    validation against /v1/models for Anthropic, OpenAI, OpenRouter (one
    fetch per provider, Anthropic uses anthropic-version: 2023-06-01).
    Specific 401 / 402 / 429 / network error messages with a "How to get a
    key" link.
  • Choose-models step — hardcoded shortlist per provider with sensible
    defaults.
  • safeStorage keychain wrapper — encrypts the pasted key, stores
    base64 ciphertext only.
  • @iarna/toml config I/O~/.config/open-codesign/config.toml
    (XDG-aware, mode 0600). Parsed through a Zod ConfigSchema — malformed
    files throw CONFIG_* errors, never silent fallback.
  • IPC: onboarding:get-state, onboarding:validate-key,
    onboarding:save-key, onboarding:skip. The renderer no longer ships an
    apiKey in codesign:generate; main pulls the key from the keychain
    per-call so the renderer never holds long-lived plaintext.
  • Vitest coverage for pingProvider (200 / 401 / 402 / 429 / network /
    custom baseUrl / unsupported provider / empty key).
  • docs/CONFIG.md documenting every key, file location, security model
    and reset procedure.

Not in (out of scope, future PRs)

  • Ollama auto-detection (Phase 0.2)
  • Browser OAuth for Anthropic (Phase 0.4)
  • "More providers" expander beyond tier-1 three (Phase 0.3)
  • UX shell components, Settings page, theme toggle (owned by wt/preview-ux)
  • Streaming generation pipeline changes (packages/runtime, core,
    exporters, artifacts, templates not touched)

Hard rules respected

  • No silent fallbacks — every failure throws a CodesignError with a
    structured code (e.g. PROVIDER_NOT_SUPPORTED, KEYCHAIN_UNAVAILABLE,
    CONFIG_PARSE_FAILED, CONFIG_SCHEMA_INVALID) and the renderer surfaces
    the message verbatim.
  • All UI uses var(--color-*) tokens — no hardcoded hex / px / fonts.
    Button from @open-codesign/ui handles all primary actions.
  • TypeScript strict + verbatimModuleSyntaximport type for types,
    bracket notation for index access, no any.
  • DCO sign-off on the commit.

New deps (license + size)

Package Version License Approx install size
@iarna/toml ^2.2.5 ISC (Apache-2.0 compatible) ~70 KB unpacked, zero deps

Within the ≤80 MB installer budget and ≤30-prod-deps budget (now 8/30 in apps/desktop).

Acceptance test outcomes

Repo-level checks (run from worktree root):

pnpm install            ok
pnpm -r typecheck       ok (10 packages green)
pnpm lint               ok (2 pre-existing/cosmetic warnings, exit 0)
pnpm -r test            ok (9 new validate tests pass; 17 total)

Manual flow (described — Electron not run inside CI):

  1. rm -rf ~/.config/open-codesign && pnpm --filter @open-codesign/desktop dev
  2. Welcome screen renders with three path buttons (Ollama disabled).
  3. Paste sk-ant-… → within ~500 ms shows
    "Recognized: Anthropic Claude — Connected (N models)".
  4. Bad key → red AlertCircle line with the specific 401 message and the
    "How to get a key" link to console.anthropic.com/settings/keys.
  5. Finish wizard → app re-renders into chat. Kill app, restart → lands
    directly in chat (config + ciphertext on disk).
  6. cat ~/.config/open-codesign/config.toml shows base64 ciphertext under
    [secrets.anthropic].ciphertext, never the original key.

Screenshots

Not attached (no Electron screenshot tooling in this branch). The wizard
renders inside a 28rem max-width card centred on the existing
var(--color-background) surface, using the same Sparkles brand mark and
warm-beige token palette as the chat shell. Each step shows a "Step N of 3"
counter top-right.

hqhq1025 added a commit that referenced this pull request Apr 18, 2026
Both PRs (#1 onboarding, #2 i18n) failed because:

1. Dependency Review requires GitHub Advanced Security on private repos.
   Add `visibility == 'public'` gate, mirroring codeql.yml and scorecard.yml.

2. Codex PR Review and Issue Auto Response require OPENAI_API_KEY +
   OPENAI_BASE_URL secrets that don't exist in this repo yet (they're
   org-level on open-cowork only). Add `vars.CODEX_BOT_ENABLED == 'true'`
   guard so the workflows are no-ops until the variable is set in repo
   settings (Settings → Secrets and variables → Actions → Variables).

To enable bot review later: set `CODEX_BOT_ENABLED=true` and add
`OPENAI_API_KEY` + `OPENAI_BASE_URL` repo (or org) secrets.

Signed-off-by: Haoqing Wang <1506751656@qq.com>
Implements the 3-step onboarding flow described in
docs/research/06-api-onboarding-ux.md:

1. Welcome — path picker (free tier / paste key / Ollama placeholder)
2. Paste key — auto-detect provider by prefix, debounced 500ms live
   validation against /v1/models for Anthropic, OpenAI, OpenRouter.
3. Choose models — hardcoded shortlist per provider with sensible
   defaults.

Keys are encrypted via Electron safeStorage (OS keychain) and the
ciphertext is persisted to ~/.config/open-codesign/config.toml via
@iarna/toml. The renderer never holds a long-lived plaintext key —
codesign:generate now pulls the key from the keychain inside the
main process.

Other tier-1 providers explicitly throw PROVIDER_NOT_SUPPORTED so
failures are loud, never silent (PRINCIPLES §10).

New deps: @iarna/toml ^2.2.5 (ISC, ~70 KB).

Signed-off-by: Haoqing Wang <1506751656@qq.com>
@hqhq1025 hqhq1025 merged commit 87463af into main Apr 18, 2026
7 checks passed
@hqhq1025 hqhq1025 deleted the wt/onboarding branch April 18, 2026 04:29
hqhq1025 added a commit that referenced this pull request Apr 18, 2026
Major #1: cancelGeneration now waits for the IPC round-trip before
clearing isGenerating/activeGenerationId. If main rejects the cancel
(bad id, already aborted) the error becomes visible via toast instead
of being silently swallowed. Updated store test to reflect async cancel
and drain microtasks between cancel and the follow-up sendPrompt.

Major #2: add GeneratePayloadV1 (schemaVersion: 1, generationId required)
to @open-codesign/shared and rename the IPC channel to codesign:v1:generate,
matching the pattern established by codesign:v1:cancel-generation. Preload
builds the v1 envelope with satisfies check; main validates with
GeneratePayloadV1.parse(). Add 5 schema tests for missing/wrong schemaVersion
and empty generationId. Add notifications.cancellationFailed i18n key.

Signed-off-by: hqhq1025 <1506751656@qq.com>
hqhq1025 added a commit that referenced this pull request Apr 18, 2026
Major #1: cancelGeneration now waits for the IPC round-trip before
clearing isGenerating/activeGenerationId. If main rejects the cancel
(bad id, already aborted) the error becomes visible via toast instead
of being silently swallowed. Updated store test to reflect async cancel
and drain microtasks between cancel and the follow-up sendPrompt.

Major #2: add GeneratePayloadV1 (schemaVersion: 1, generationId required)
to @open-codesign/shared and rename the IPC channel to codesign:v1:generate,
matching the pattern established by codesign:v1:cancel-generation. Preload
builds the v1 envelope with satisfies check; main validates with
GeneratePayloadV1.parse(). Add 5 schema tests for missing/wrong schemaVersion
and empty generationId. Add notifications.cancellationFailed i18n key.

Signed-off-by: hqhq1025 <1506751656@qq.com>
hqhq1025 added a commit that referenced this pull request Apr 18, 2026
* feat(desktop): redesign sidebar input + add cancellation IPC

- Replace single-line input with autosize textarea (1–6 rows, max-h 144 px)
- Move send button into bottom-right corner of textarea container, icon-only 28 px
- During generation, send button becomes a stop button (Square icon) wired to cancelGeneration
- Remove redundant keyboard-hint bar; fold hint into placeholder text
- Enter sends, Shift+Enter inserts newline, ⌘↵ global send preserved
- main: extract runGenerate helper; maintain Map<id, AbortController> for in-flight
  requests; new codesign:cancel-generation IPC handler; logger scope 'ipc'
- preload: expose cancelGeneration(id) on window.codesign
- shared: GeneratePayload accepts optional generationId
- store: add activeGenerationId state + cancelGeneration action; suppress abort
  errors silently without pushing an error toast

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix: harden generation cancellation flows

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix: version cancellation ipc and tokenise sidebar

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix: preserve current-generation failures

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): guard cancel race + version IPC payload + add tests

- applyGenerateSuccess/applyGenerateError already guarded via
  finishIfCurrent and the early-return id check; confirm with tests
- Add noop test: unknown generationId leaves all in-flight intact
- Add schema rejection test: empty generationId and missing
  schemaVersion both throw via CancelGenerationPayloadV1.parse
- Fix biome formatting in store.test.ts (pre-existing)

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): properly merge rebase onto main — restore all origin/main features

Rebase onto origin/main exposed conflicts where PRs #18, #19, #20 had
advanced store, preload, shared and main/index.ts beyond the branch
point. Resolve by taking origin/main as the base and layering our
cancellation additions on top:

- shared/index.ts: add LocalInputFile, SelectedElement, ElementSelectionRect,
  ApplyCommentPayload, StoredDesignSystem exports; add generationId to
  GeneratePayload; keep CancelGenerationPayloadV1
- preload/index.ts: add locale API, pickInputFiles, pickDesignSystemDirectory,
  clearDesignSystem, applyComment; add generationId to generate payload;
  keep cancelGeneration with schemaVersion: 1
- main/index.ts: add applyComment, scanDesignSystem, preparePromptContext,
  registerLocaleIpc; wire AbortController into generate handler; keep
  codesign:v1:cancel-generation handler
- store.ts: merge all origin/main state (selectedElement, inputFiles,
  referenceUrl, i18n tr(), applyInlineComment, etc.) with PR's
  activeGenerationId / cancelGeneration / finishIfCurrent guards
- store.test.ts: update sendPrompt calls to object form; add designSystem:
  null to OnboardingState fixture; relax toast title assertion for i18n

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): wait for cancel IPC ack + version generate payload

Major #1: cancelGeneration now waits for the IPC round-trip before
clearing isGenerating/activeGenerationId. If main rejects the cancel
(bad id, already aborted) the error becomes visible via toast instead
of being silently swallowed. Updated store test to reflect async cancel
and drain microtasks between cancel and the follow-up sendPrompt.

Major #2: add GeneratePayloadV1 (schemaVersion: 1, generationId required)
to @open-codesign/shared and rename the IPC channel to codesign:v1:generate,
matching the pattern established by codesign:v1:cancel-generation. Preload
builds the v1 envelope with satisfies check; main validates with
GeneratePayloadV1.parse(). Add 5 schema tests for missing/wrong schemaVersion
and empty generationId. Add notifications.cancellationFailed i18n key.

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): restore legacy generate IPC shim + use ui tokens in sidebar

- Add codesign:generate legacy handler that accepts old GeneratePayload
  (no schemaVersion) and promotes it to V1 by injecting schemaVersion:1
  and falling back generationId to gen-${Date.now()} when absent.
  Emits a warn log to track removal in the next minor release.
- Add --color-on-accent token to packages/ui tokens.css (light + dark)
  so accent-button text never hard-codes a color value.
- Replace text-white in Sidebar.tsx with text-[var(--color-on-accent)]
  on both the stop and send IconButton variants.
- Add 3 tests for GeneratePayload legacy schema and the V1 promotion
  path that the shim relies on.

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): surface error when renderer bridge missing during cancel

Split the combined `!id || !window.codesign` guard in `cancelGeneration`
into two separate checks. A missing `activeGenerationId` is a legitimate
no-op, but a missing IPC bridge is a real error that must be surfaced via
`errorMessage`/`lastError` and a toast. Adds a Vitest test to cover the
new error path (with `beforeAll` i18n init so `tr()` keys resolve).

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): restore attachments/refURL/designSystem entries + i18n sidebar strings

Restores the local context panel (attachments, reference URL, design
system) that was accidentally dropped during rebase when the cancel
button was added.  All hardcoded English strings are now routed through
useT(); chat.send and chat.stop keys were added to en.json / zh-CN.json.

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): fix import order and trailing blank line after rebase

Biome import sort and formatter fixes introduced during rebase conflict
resolution.

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): remove hardcoded textarea fallback, throw on missing tokens

Removes FALLBACK_FONT_SIZE and FALLBACK_LINE_HEIGHT_MULTIPLIER constants
from getTextareaLineHeight. When computed lineHeight is not a finite
positive number and either fontSize or --leading-body token is absent or
invalid, the function now throws instead of silently using magic numbers.
The --leading-body token is already defined in packages/ui/src/tokens.css.

Updates Sidebar.test.ts: replaces the "falls back" test with two tests —
one verifying valid-token multiplication and one verifying the throw path.

Signed-off-by: hqhq1025 <1506751656@qq.com>

---------

Signed-off-by: hqhq1025 <1506751656@qq.com>
Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Co-authored-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
hqhq1025 added a commit that referenced this pull request Apr 23, 2026
- Activity row: last-commit, commits/month, contributors, total-downloads
  (auto-updated via shields.io — no maintenance cost)
- What's new rolling block (3 recent releases) above "What it is" so new
  visitors see the project moving
- Install section now leads with `winget` / `brew` one-liners (winget
  manifest merged into microsoft/winget-pkgs; was "PR under review")
- Extended SEO keyword strip with v0/bolt/lovable-alternative,
  prompt-to-design, ai-prototyping, desktop-design-tool
- Same across README.md and README.zh-CN.md

Context: Google is now the #1 referrer (49% of traffic over 14d), so
README SEO and freshness signals matter more than a single banner.

Signed-off-by: hqhq1025 <1506751656@qq.com>
hqhq1025 added a commit that referenced this pull request Apr 23, 2026
…JSON-LD

SoftwareApplication JSON-LD now includes:
- `screenshot` — product-hero.png and comment-mode.png so Google can
  surface real product imagery in rich results
- `applicationSubCategory: "AI Design Tool"` — more specific than the
  generic "DesignApplication" for long-tail searches
- `featureList` — 8 product capabilities so AI summarizers (Perplexity,
  ChatGPT, Gemini) can describe us accurately when users ask for an
  "open-source v0 / bolt / lovable alternative"

Rationale: Google's organic traffic is already the #1 referrer (49% of
referrer traffic over 14d). These additions are the last low-hanging
rich-result signals we don't already ship.

Deliberately NOT adding `aggregateRating` — GitHub stars are not user
product ratings and Google classifies that as misleading structured
data (can trigger a manual action).

Signed-off-by: hqhq1025 <1506751656@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant