Q176: onboarding progress survives a restart, and an unfinished setup reopens
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
43dcb1e7d3
commit
fd79f059ca
20 files changed
+341
-71
No files matched your search
@@ -7,6 +7,14 @@ const id = z.string();
|
||||
const nullableId = z.string().nullable();
|
||||
|
||||
export const organizationContracts = {
|
||||
saveOnboardingDraft: z.strictObject({
|
||||
draft: z.strictObject({
|
||||
step: z.enum(["welcome", "provider", "company", "mode", "mission"]), companyName: z.string(), companyDescription: z.string(), employeeName: z.string(),
|
||||
workspacePath: z.string(), executionMode: z.enum(["ai_ceo", "ai_cofounder"]), providerKey: z.string(), providerBaseUrl: z.string(), providerModel: z.string(),
|
||||
goalTitle: z.string(), goalDescription: z.string(),
|
||||
}).nullable(),
|
||||
}),
|
||||
completeSetup: z.strictObject({ orgId: id }),
|
||||
bootstrapOnboarding: z.strictObject({
|
||||
companyName: z.string(), companyDescription: z.string(), employeeName: z.string(), workspacePath: z.string(),
|
||||
executionMode: z.enum(["ai_ceo", "ai_cofounder"]), autoApproveHires: z.boolean(), reviewDeliverables: z.boolean(),
|
||||
|
||||
@@ -41,6 +41,7 @@ import { cancelQueuedRun } from "../services/run-cancel";
|
||||
import { runHistory } from "../services/run-history";
|
||||
import { deleteWorkspace, setPrimaryWorkspace, updateWorkspace } from "../services/workspace-admin";
|
||||
import { setProviderModel } from "../services/provider-default-model";
|
||||
import { completeSetup, saveOnboardingDraft } from "../services/onboarding-progress";
|
||||
import { providerDisconnectImpact } from "../services/provider-disconnect-impact";
|
||||
|
||||
export interface IpcDeps {
|
||||
@@ -141,6 +142,15 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
handle("createOrganization", (input) => mutate(() => organization.createOrganization(input)));
|
||||
handle("setActiveOrganization", (input) => mutate(() => organization.setActiveOrganization(input.orgId)));
|
||||
handle("setUiPreferences", (input) => mutate(() => organization.updateUiPreferences(input)));
|
||||
handle("saveOnboardingDraft", (input) => {
|
||||
try {
|
||||
saveOnboardingDraft(deps.connection.db, input.draft);
|
||||
return ok(null);
|
||||
} catch (error) {
|
||||
return fail("onboarding_draft_failed", error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
});
|
||||
handle("completeSetup", (input) => mutate(() => completeSetup(deps.connection.db, input.orgId)));
|
||||
handle("runningSessionCount", () => ok((deps.connection.db.prepare("select count(*) as n from task_runs where state = 'running'").get() as { n: number }).n));
|
||||
// Chromium's locale is fixed at launch; the reply goes out before the process exits.
|
||||
handle("relaunchApp", () => {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Onboarding survives a restart: the unfinished wizard (never its key) and whether a created company finished setup; existing companies are done.
|
||||
export const onboardingProgressSchema = `
|
||||
alter table app_state add column onboarding_draft_json text;
|
||||
alter table organizations add column setup_completed_at text;
|
||||
update organizations set setup_completed_at = created_at;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (105, 'onboarding_progress', datetime('now'));
|
||||
`;
|
||||
@@ -103,6 +103,7 @@ import { taskBlockedSchema } from "./migrations/101-task-blocked";
|
||||
import { goalDueDateSchema } from "./migrations/102-goal-due-date";
|
||||
import { workVerdictsSchema } from "./migrations/103-work-verdicts";
|
||||
import { workLanguageSchema } from "./migrations/104-work-language";
|
||||
import { onboardingProgressSchema } from "./migrations/105-onboarding-progress";
|
||||
|
||||
interface Migration {
|
||||
version: number;
|
||||
@@ -215,6 +216,7 @@ const MIGRATIONS: Migration[] = [
|
||||
{ version: 102, name: "goal_due_date", sql: goalDueDateSchema },
|
||||
{ version: 103, name: "work_verdicts", sql: workVerdictsSchema },
|
||||
{ version: 104, name: "work_language", sql: workLanguageSchema },
|
||||
{ version: 105, name: "onboarding_progress", sql: onboardingProgressSchema },
|
||||
];
|
||||
|
||||
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { OnboardingDraft } from "../../shared/commands";
|
||||
|
||||
/** Keeps the unfinished wizard so a restart resumes it; null discards it. The draft type has no key field, so no key is stored. */
|
||||
export function saveOnboardingDraft(db: DatabaseSync, draft: OnboardingDraft | null): void {
|
||||
db.prepare(`
|
||||
insert into app_state(id, onboarding_draft_json, updated_at) values (1, ?, ?)
|
||||
on conflict(id) do update set onboarding_draft_json = excluded.onboarding_draft_json, updated_at = excluded.updated_at
|
||||
`).run(draft ? JSON.stringify(draft) : null, new Date().toISOString());
|
||||
}
|
||||
|
||||
export function loadOnboardingDraft(db: DatabaseSync): OnboardingDraft | null {
|
||||
const row = db.prepare("select onboarding_draft_json from app_state where id = 1").get() as { onboarding_draft_json: string | null } | undefined;
|
||||
return row?.onboarding_draft_json ? JSON.parse(row.onboarding_draft_json) as OnboardingDraft : null;
|
||||
}
|
||||
|
||||
/** Marks the company's setup (the steps after it was created) finished; until then the wizard reopens on it. */
|
||||
export function completeSetup(db: DatabaseSync, orgId: string): void {
|
||||
const result = db.prepare("update organizations set setup_completed_at = coalesce(setup_completed_at, ?) where id = ?").run(new Date().toISOString(), orgId);
|
||||
if (result.changes === 0) throw new Error("Organization was not found.");
|
||||
console.info(`[onboarding] ${orgId} finished setup`);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { saveOnboardingDraft } from "./onboarding-progress";
|
||||
import { COMPANY_PRESETS } from "../../shared/company-presets";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type {
|
||||
@@ -32,6 +33,8 @@ export class OnboardingService {
|
||||
|
||||
bootstrap(input: BootstrapOnboardingInput): BootstrapOnboardingSummary {
|
||||
const provider = this.resolveProvider(input.providerKey);
|
||||
// The company now exists, so the saved wizard is spent; this commits with it.
|
||||
saveOnboardingDraft(this.deps.db, null);
|
||||
const orgId = this.organization.createOrganization({
|
||||
organizationName: input.companyName,
|
||||
description: input.companyDescription,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { loadOnboardingDraft } from "./onboarding-progress";
|
||||
import type { DatabaseSync, StatementSync } from "node:sqlite";
|
||||
import type { AppSnapshot } from "../../shared/domain";
|
||||
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
|
||||
@@ -86,6 +87,7 @@ export class SnapshotService {
|
||||
return {
|
||||
activeOrgId,
|
||||
uiPreferences: this.resolveUiPreferences(),
|
||||
onboardingDraft: loadOnboardingDraft(this.db),
|
||||
autonomousPaused: this.resolveAutonomousPaused(),
|
||||
integrationKeys: new IntegrationKeyStore(this.db).statuses(),
|
||||
organizations,
|
||||
|
||||
Reference in new issue
Block a user