Skip to content

Commit ea60290

Browse files
authored
Merge pull request #22 from OpenKnots/okcode/new-worktree-onboarding
Add onboarding tour and default new threads to worktrees
2 parents 8dfa886 + 8027d10 commit ea60290

8 files changed

Lines changed: 308 additions & 3 deletions

File tree

apps/web/src/appSettings.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ describe("AppSettingsSchema", () => {
255255
claudeBinaryPath: "",
256256
codexBinaryPath: "/usr/local/bin/codex",
257257
codexHomePath: "",
258-
defaultThreadEnvMode: "local",
258+
defaultThreadEnvMode: "worktree",
259259
confirmThreadDelete: false,
260260
enableAssistantStreaming: false,
261261
sidebarProjectSortOrder: DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,

apps/web/src/appSettings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export const AppSettingsSchema = Schema.Struct({
6060
claudeBinaryPath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")),
6161
codexBinaryPath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")),
6262
codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")),
63-
defaultThreadEnvMode: EnvMode.pipe(withDefaults(() => "local" as const satisfies EnvMode)),
63+
defaultThreadEnvMode: EnvMode.pipe(withDefaults(() => "worktree" as const satisfies EnvMode)),
6464
confirmThreadDelete: Schema.Boolean.pipe(withDefaults(() => true)),
6565
diffWordWrap: Schema.Boolean.pipe(withDefaults(() => false)),
6666
enableAssistantStreaming: Schema.Boolean.pipe(withDefaults(() => false)),
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { useState } from "react";
2+
import {
3+
FileDiffIcon,
4+
GitBranchIcon,
5+
ListChecksIcon,
6+
MessageSquareIcon,
7+
RocketIcon,
8+
ShieldCheckIcon,
9+
SparklesIcon,
10+
TerminalSquareIcon,
11+
} from "lucide-react";
12+
import { Dialog, DialogFooter, DialogHeader, DialogPopup } from "~/components/ui/dialog";
13+
import { Button } from "~/components/ui/button";
14+
import { OnboardingStep } from "./OnboardingStep";
15+
import { ONBOARDING_STEPS } from "./onboardingSteps";
16+
import { useOnboardingState } from "./useOnboardingState";
17+
18+
const STEP_ICONS = [
19+
<SparklesIcon key="sparkles" className="size-7" />,
20+
<MessageSquareIcon key="message" className="size-7" />,
21+
<GitBranchIcon key="git" className="size-7" />,
22+
<FileDiffIcon key="diff" className="size-7" />,
23+
<TerminalSquareIcon key="terminal" className="size-7" />,
24+
<ListChecksIcon key="plan" className="size-7" />,
25+
<ShieldCheckIcon key="shield" className="size-7" />,
26+
<RocketIcon key="rocket" className="size-7" />,
27+
];
28+
29+
export function OnboardingDialog() {
30+
const { open, complete, skip } = useOnboardingState();
31+
const [step, setStep] = useState(0);
32+
33+
if (!open) return null;
34+
35+
const totalSteps = ONBOARDING_STEPS.length;
36+
const isFirst = step === 0;
37+
const isLast = step === totalSteps - 1;
38+
const currentStep = ONBOARDING_STEPS[step]!;
39+
40+
const handleNext = () => {
41+
if (isLast) {
42+
complete();
43+
} else {
44+
setStep((s) => s + 1);
45+
}
46+
};
47+
48+
const handleBack = () => {
49+
setStep((s) => Math.max(0, s - 1));
50+
};
51+
52+
return (
53+
<Dialog
54+
open={open}
55+
onOpenChange={(nextOpen) => {
56+
if (!nextOpen) skip();
57+
}}
58+
>
59+
<DialogPopup showCloseButton={false} className="max-w-xl">
60+
<DialogHeader className="px-8 pt-8 pb-2">
61+
<div key={step} className="animate-in fade-in duration-300">
62+
<OnboardingStep step={currentStep} icon={STEP_ICONS[step]} />
63+
</div>
64+
</DialogHeader>
65+
66+
<DialogFooter
67+
variant="bare"
68+
className="flex-row items-center justify-between px-8 pt-4 pb-8"
69+
>
70+
{/* Step indicator dots */}
71+
<div className="flex items-center gap-1.5" role="group" aria-label="Onboarding progress">
72+
{ONBOARDING_STEPS.map((s, i) => (
73+
<button
74+
key={s.id}
75+
type="button"
76+
onClick={() => setStep(i)}
77+
aria-label={`Go to step ${i + 1} of ${totalSteps}: ${s.title}`}
78+
aria-current={i === step ? "step" : undefined}
79+
className={`size-2 rounded-full transition-all duration-200 ${
80+
i === step
81+
? "scale-125 bg-primary"
82+
: i < step
83+
? "bg-primary/40 hover:bg-primary/60"
84+
: "bg-muted-foreground/20 hover:bg-muted-foreground/40"
85+
}`}
86+
/>
87+
))}
88+
</div>
89+
90+
{/* Navigation buttons */}
91+
<div className="flex items-center gap-2">
92+
{!isLast && (
93+
<Button variant="ghost" size="sm" onClick={skip}>
94+
Skip
95+
</Button>
96+
)}
97+
{!isFirst && (
98+
<Button variant="outline" size="sm" onClick={handleBack}>
99+
Back
100+
</Button>
101+
)}
102+
<Button size="sm" onClick={handleNext}>
103+
{isLast ? "Get Started" : "Next"}
104+
</Button>
105+
</div>
106+
</DialogFooter>
107+
</DialogPopup>
108+
</Dialog>
109+
);
110+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { ReactNode } from "react";
2+
import { DialogDescription, DialogTitle } from "~/components/ui/dialog";
3+
import type { OnboardingStepConfig } from "./onboardingSteps";
4+
5+
const ACCENT_STYLES: Record<string, { icon: string; dot: string }> = {
6+
primary: {
7+
icon: "bg-primary/10 text-primary",
8+
dot: "bg-primary/60",
9+
},
10+
sky: {
11+
icon: "bg-sky-500/10 text-sky-500",
12+
dot: "bg-sky-500/60",
13+
},
14+
emerald: {
15+
icon: "bg-emerald-500/10 text-emerald-500",
16+
dot: "bg-emerald-500/60",
17+
},
18+
amber: {
19+
icon: "bg-amber-500/10 text-amber-500",
20+
dot: "bg-amber-500/60",
21+
},
22+
violet: {
23+
icon: "bg-violet-500/10 text-violet-500",
24+
dot: "bg-violet-500/60",
25+
},
26+
rose: {
27+
icon: "bg-rose-500/10 text-rose-500",
28+
dot: "bg-rose-500/60",
29+
},
30+
orange: {
31+
icon: "bg-orange-500/10 text-orange-500",
32+
dot: "bg-orange-500/60",
33+
},
34+
};
35+
36+
export function OnboardingStep({ step, icon }: { step: OnboardingStepConfig; icon: ReactNode }) {
37+
const accent = ACCENT_STYLES[step.accentColor] ?? ACCENT_STYLES.primary;
38+
39+
return (
40+
<div className="flex flex-col items-center text-center">
41+
<div className={`mb-4 flex size-14 items-center justify-center rounded-2xl ${accent.icon}`}>
42+
{icon}
43+
</div>
44+
45+
<DialogTitle className="text-xl font-semibold tracking-tight">{step.title}</DialogTitle>
46+
47+
<DialogDescription className="mt-2 max-w-sm text-sm leading-relaxed text-muted-foreground">
48+
{step.description}
49+
</DialogDescription>
50+
51+
<ul className="mt-5 w-full max-w-sm space-y-2.5 text-left">
52+
{step.details.map((detail) => (
53+
<li
54+
key={detail}
55+
className="flex items-start gap-2.5 text-[13px] leading-snug text-foreground/80"
56+
>
57+
<span className={`mt-1.5 size-1.5 shrink-0 rounded-full ${accent.dot}`} />
58+
{detail}
59+
</li>
60+
))}
61+
</ul>
62+
</div>
63+
);
64+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
export interface OnboardingStepConfig {
2+
id: string;
3+
title: string;
4+
description: string;
5+
details: string[];
6+
accentColor: string;
7+
}
8+
9+
export const ONBOARDING_STEPS: OnboardingStepConfig[] = [
10+
{
11+
id: "welcome",
12+
title: "Welcome to OK Code",
13+
description:
14+
"Your AI-powered coding companion. Let's take a quick tour of the features that will supercharge your workflow.",
15+
details: [
16+
"Work alongside AI agents that read, write, and reason about your code",
17+
"Every conversation runs in an isolated git worktree by default",
18+
"This tour takes about a minute — you can skip at any time",
19+
],
20+
accentColor: "primary",
21+
},
22+
{
23+
id: "chat",
24+
title: "AI-Powered Conversations",
25+
description:
26+
"Chat with AI coding agents in real time. Ask questions, request changes, or let the agent drive entire features.",
27+
details: [
28+
"Choose between multiple providers — Codex and Claude",
29+
"Stream responses in real time as the agent works",
30+
"Attach images and terminal context directly in your prompts",
31+
],
32+
accentColor: "sky",
33+
},
34+
{
35+
id: "git",
36+
title: "Built-in Git Workflows",
37+
description:
38+
"Every thread can run in its own git worktree, keeping your main branch safe while the agent experiments freely.",
39+
details: [
40+
"New threads automatically create isolated worktrees",
41+
"Switch branches, create PRs, and manage worktrees from the toolbar",
42+
"Link threads to existing pull requests for focused code review",
43+
],
44+
accentColor: "emerald",
45+
},
46+
{
47+
id: "diff",
48+
title: "Review Changes Side-by-Side",
49+
description:
50+
"Inspect every code change the agent makes with a built-in diff viewer before accepting anything.",
51+
details: [
52+
"Inline and side-by-side diff views with syntax highlighting",
53+
"Accept or reject changes per-file with a single click",
54+
"Word-level highlighting shows exactly what changed",
55+
],
56+
accentColor: "amber",
57+
},
58+
{
59+
id: "terminal",
60+
title: "Integrated Terminal",
61+
description:
62+
"A full terminal lives inside every thread — run commands, see output, and feed context back to the agent.",
63+
details: [
64+
"Up to four terminal tabs per thread for parallel workflows",
65+
"Select terminal output and add it directly to your prompt",
66+
"Track running subprocesses with live activity indicators",
67+
],
68+
accentColor: "violet",
69+
},
70+
{
71+
id: "plan",
72+
title: "AI-Generated Plans",
73+
description:
74+
"Switch to Plan mode and let the agent outline a structured implementation strategy before writing a single line of code.",
75+
details: [
76+
"Step-by-step plans with status tracking as work progresses",
77+
"Review, copy, or export plans as Markdown",
78+
'Click "Implement Plan" to kick off execution in a new thread',
79+
],
80+
accentColor: "rose",
81+
},
82+
{
83+
id: "approvals",
84+
title: "Stay in Control",
85+
description:
86+
"You decide what gets executed. The agent asks for your approval before making changes, so nothing happens without your say-so.",
87+
details: [
88+
"Approve, request changes, or cancel any proposed action",
89+
"Switch between full-access and approval-required modes per thread",
90+
"Review pending file changes before they're applied",
91+
],
92+
accentColor: "orange",
93+
},
94+
{
95+
id: "getStarted",
96+
title: "You're All Set!",
97+
description: "You're ready to start building. Here are a few shortcuts to help you move fast.",
98+
details: [
99+
"Press Cmd+N (or Ctrl+N) to create a new thread instantly",
100+
"Use the sidebar to switch between projects and threads",
101+
"Open Settings to customize models, themes, and keybindings",
102+
],
103+
accentColor: "primary",
104+
},
105+
];
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useCallback, useState } from "react";
2+
3+
const STORAGE_KEY = "okcode:onboarding-completed:v1";
4+
5+
export function useOnboardingState() {
6+
const [open, setOpen] = useState(() => {
7+
try {
8+
return localStorage.getItem(STORAGE_KEY) !== "true";
9+
} catch {
10+
return false;
11+
}
12+
});
13+
14+
const complete = useCallback(() => {
15+
try {
16+
localStorage.setItem(STORAGE_KEY, "true");
17+
} catch {
18+
// Ignore storage errors
19+
}
20+
setOpen(false);
21+
}, []);
22+
23+
return { open, complete, skip: complete };
24+
}

apps/web/src/hooks/useHandleNewThread.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export function useHandleNewThread() {
9999
createdAt,
100100
branch: options?.branch ?? null,
101101
worktreePath: options?.worktreePath ?? null,
102-
envMode: options?.envMode ?? "local",
102+
envMode: options?.envMode ?? "worktree",
103103
runtimeMode: DEFAULT_RUNTIME_MODE,
104104
});
105105
if (stickyModel) {

apps/web/src/routes/__root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { onServerConfigUpdated, onServerWelcome } from "../wsNativeApi";
2424
import { providerQueryKeys } from "../lib/providerReactQuery";
2525
import { projectQueryKeys } from "../lib/projectReactQuery";
2626
import { collectActiveTerminalThreadIds } from "../lib/terminalStateCleanup";
27+
import { OnboardingDialog } from "../components/onboarding/OnboardingDialog";
2728

2829
export const Route = createRootRouteWithContext<{
2930
queryClient: QueryClient;
@@ -54,6 +55,7 @@ function RootRouteView() {
5455
<EventRouter />
5556
<DesktopProjectBootstrap />
5657
<Outlet />
58+
<OnboardingDialog />
5759
</AnchoredToastProvider>
5860
</ToastProvider>
5961
);

0 commit comments

Comments
 (0)