@saadnvd1/agent-os 0.3.520 → 0.3.523

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +11 -0
  2. package/app/api/welcome/route.ts +22 -0
  3. package/app/page.tsx +6 -0
  4. package/app/welcome/page.tsx +6 -0
  5. package/components/Chat/BackgroundTasks.tsx +76 -10
  6. package/components/Chat/ChatPanel.tsx +90 -34
  7. package/components/Chat/Subagent.tsx +25 -20
  8. package/components/Chat/Tools.tsx +70 -39
  9. package/components/Chat/background.tsx +114 -0
  10. package/components/Projects/AddProject/AddProjectDialog.tsx +2 -2
  11. package/components/Settings/SettingsDialog.tsx +9 -0
  12. package/components/Settings/SetupSection.tsx +37 -0
  13. package/components/Welcome/AgentList.tsx +73 -0
  14. package/components/Welcome/AgentRow.tsx +102 -0
  15. package/components/Welcome/ProjectStep.tsx +100 -0
  16. package/components/Welcome/SetupTerminal.tsx +64 -0
  17. package/components/Welcome/WelcomeDialog.tsx +153 -0
  18. package/data/agents/index.ts +16 -1
  19. package/data/chat/useChat.ts +40 -0
  20. package/data/welcome/index.ts +36 -0
  21. package/hooks/useWelcome.ts +70 -0
  22. package/lib/agents/probe.ts +27 -0
  23. package/lib/agents/setup-commands.ts +54 -0
  24. package/lib/chat/activity.ts +4 -2
  25. package/lib/chat/background.ts +77 -0
  26. package/lib/chat/driver.ts +6 -0
  27. package/lib/chat/drivers/claude-mapper.ts +44 -6
  28. package/lib/chat/drivers/claude.ts +4 -0
  29. package/lib/chat/events.ts +14 -0
  30. package/lib/chat/registry.ts +2 -0
  31. package/lib/chat/runner.ts +20 -0
  32. package/lib/chat/settings.ts +1 -0
  33. package/lib/chat/socket.ts +9 -0
  34. package/lib/chat/worker/client.ts +33 -0
  35. package/lib/chat/worker/host.ts +13 -0
  36. package/lib/chat/worker/main.ts +1 -1
  37. package/lib/chat/worker/protocol.ts +2 -0
  38. package/lib/db/migrations.ts +14 -0
  39. package/lib/security/auth.ts +11 -1
  40. package/lib/settings-url.ts +1 -0
  41. package/lib/welcome-url.ts +33 -0
  42. package/lib/welcome.ts +88 -0
  43. package/package.json +1 -1
  44. package/stores/addProject.ts +19 -1
  45. package/stores/welcomeUi.ts +26 -0
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+ import { Check, ChevronRight, Layers, Loader2, X } from "lucide-react";
5
+ import type { ChatItem } from "@/lib/chat/events";
6
+ import type { ToolItem } from "@/lib/chat/group";
7
+ import { backgroundable } from "@/lib/chat/background";
8
+ import { formatElapsed } from "@/lib/chat/elapsed";
9
+ import { cn } from "@/lib/utils";
10
+ import { useNow } from "./useNow";
11
+
12
+ type Task = Extract<ChatItem, { kind: "task" }>;
13
+
14
+ export interface BackgroundValue {
15
+ // The agent can move a call to the background.
16
+ supported: boolean;
17
+ // The task a tool call started, by the call's id.
18
+ taskOfTool: (toolUseId: string) => Task | undefined;
19
+ taskById: (taskId: string) => Task | undefined;
20
+ // Calls asked to move and not yet answered ("all" for the shortcut).
21
+ moving: ReadonlySet<string>;
22
+ send: (toolUseId?: string) => void;
23
+ // Opens the tray on that task.
24
+ reveal: (taskId: string) => void;
25
+ }
26
+
27
+ const none: BackgroundValue = {
28
+ supported: false,
29
+ taskOfTool: () => undefined,
30
+ taskById: () => undefined,
31
+ moving: new Set(),
32
+ send: () => {},
33
+ reveal: () => {},
34
+ };
35
+
36
+ export const Background = createContext<BackgroundValue>(none);
37
+ export const useBackground = () => useContext(Background);
38
+
39
+ // Moves the call it sits on to the background. Only there while that can
40
+ // work: after a few seconds of running, so quick commands never show it.
41
+ export function BackgroundButton({ tool }: { tool: ToolItem }) {
42
+ const bg = useBackground();
43
+ const running = tool.status === "running";
44
+ const now = useNow(running && bg.supported);
45
+ if (!backgroundable(bg.supported, tool, bg.taskOfTool(tool.id), now))
46
+ return null;
47
+ const moving = bg.moving.has(tool.id) || bg.moving.has("all");
48
+ return (
49
+ <button
50
+ type="button"
51
+ disabled={moving}
52
+ onClick={(e) => {
53
+ e.stopPropagation();
54
+ bg.send(tool.id);
55
+ }}
56
+ title="Run in background (Ctrl+B)"
57
+ aria-label="Run in background"
58
+ className="text-muted-foreground hover:text-foreground hover:bg-foreground/[0.06] animate-in fade-in flex h-11 shrink-0 items-center gap-1.5 rounded-md px-2.5 text-xs duration-150 disabled:opacity-70 md:h-7 md:px-2"
59
+ >
60
+ {moving ? (
61
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
62
+ ) : (
63
+ <Layers className="h-3.5 w-3.5" />
64
+ )}
65
+ <span className="hidden sm:inline">
66
+ {moving ? "Moving…" : "Background"}
67
+ </span>
68
+ </button>
69
+ );
70
+ }
71
+
72
+ // Where a call that went to the background stands: running there (opens
73
+ // the tray on it), or how it ended.
74
+ export function BackgroundState({ tool }: { tool: ToolItem }) {
75
+ const bg = useBackground();
76
+ const task = tool.backgroundTaskId
77
+ ? bg.taskById(tool.backgroundTaskId)
78
+ : undefined;
79
+ const running = task?.status === "running";
80
+ const now = useNow(running);
81
+ if (!tool.backgroundTaskId) return null;
82
+ if (!task || running)
83
+ return (
84
+ <button
85
+ type="button"
86
+ onClick={(e) => {
87
+ e.stopPropagation();
88
+ bg.reveal(tool.backgroundTaskId!);
89
+ }}
90
+ className="text-muted-foreground hover:text-foreground flex shrink-0 items-center gap-1 text-[11px]"
91
+ >
92
+ in background
93
+ <ChevronRight className="h-3 w-3" />
94
+ </button>
95
+ );
96
+ const failed = task.status === "failed";
97
+ const took = formatElapsed((task.endedAt ?? now) - task.createdAt);
98
+ return (
99
+ <span
100
+ className={cn(
101
+ "text-muted-foreground flex shrink-0 items-center gap-1 text-[11px]",
102
+ failed && "text-destructive"
103
+ )}
104
+ >
105
+ {failed ? <X className="h-3 w-3" /> : <Check className="h-3 w-3" />}
106
+ {task.status === "stopped"
107
+ ? "stopped in background"
108
+ : failed
109
+ ? "failed in background"
110
+ : "ran in background"}
111
+ <span className="font-mono tabular-nums">· {took}</span>
112
+ </span>
113
+ );
114
+ }
@@ -29,8 +29,8 @@ export function AddProjectDialog() {
29
29
  const { open } = useSnapshot(addProjectUi);
30
30
  const hostNames = useHostNames();
31
31
  const done = (projectId: string) => {
32
- addProjectActions.close();
33
- newDraft({ kind: "project", projectId });
32
+ if (!addProjectActions.finish(projectId))
33
+ newDraft({ kind: "project", projectId });
34
34
  };
35
35
 
36
36
  return (
@@ -11,6 +11,7 @@ import {
11
11
  Cpu,
12
12
  GitMerge,
13
13
  LayoutGrid,
14
+ ListChecks,
14
15
  Smartphone,
15
16
  X,
16
17
  type LucideIcon,
@@ -34,6 +35,7 @@ import { SETTINGS_SECTIONS, type SettingsSection } from "@/lib/settings-url";
34
35
  import { cn } from "@/lib/utils";
35
36
  import { schedulesUiActions } from "@/stores/schedulesUi";
36
37
  import { settingsUi, settingsUiActions } from "@/stores/settingsUi";
38
+ import { SetupSection } from "./SetupSection";
37
39
  import {
38
40
  NotificationsSection,
39
41
  type BrowserNotifications,
@@ -78,6 +80,11 @@ export const SECTION_INFO: Record<
78
80
  hint: "Where sessions spent tokens and repeated work",
79
81
  icon: ClipboardList,
80
82
  },
83
+ setup: {
84
+ label: "Setup",
85
+ hint: "Agents on this machine, first-run setup",
86
+ icon: ListChecks,
87
+ },
81
88
  };
82
89
 
83
90
  function SectionBody({
@@ -102,6 +109,8 @@ function SectionBody({
102
109
  return <SchedulesPanel />;
103
110
  case "audits":
104
111
  return <AuditsPanel />;
112
+ case "setup":
113
+ return <SetupSection />;
105
114
  }
106
115
  }
107
116
 
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { AgentList } from "@/components/Welcome/AgentList";
5
+ import { settingsUiActions } from "@/stores/settingsUi";
6
+ import { welcomeUiActions } from "@/stores/welcomeUi";
7
+
8
+ // What this machine has for running agents, and the first-run setup again.
9
+ export function SetupSection() {
10
+ return (
11
+ <div className="flex flex-col gap-4">
12
+ <div className="bg-muted/40 flex items-center gap-3 rounded-lg px-3 py-2.5">
13
+ <div className="min-w-0 flex-1">
14
+ <p className="text-sm font-medium">First-run setup</p>
15
+ <p className="text-muted-foreground text-xs">
16
+ Agents, your first project, and a first session.
17
+ </p>
18
+ </div>
19
+ <Button
20
+ size="sm"
21
+ variant="outline"
22
+ className="min-h-11 sm:min-h-8"
23
+ onClick={() => {
24
+ settingsUiActions.close();
25
+ welcomeUiActions.open("agents");
26
+ }}
27
+ >
28
+ Run setup again
29
+ </Button>
30
+ </div>
31
+ <div className="flex flex-col gap-2">
32
+ <h3 className="text-sm font-medium">Agents on this machine</h3>
33
+ <AgentList />
34
+ </div>
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { RefreshCw } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import { useAgentStatusQuery, useRecheckAgents } from "@/data/agents";
7
+ import { FEATURED_AGENTS } from "@/lib/agents/setup-commands";
8
+ import { getAllProviderDefinitions } from "@/lib/providers/registry";
9
+ import { AgentRow, type AgentRun } from "./AgentRow";
10
+ import { SetupTerminal } from "./SetupTerminal";
11
+
12
+ /**
13
+ * Every agent CLI on this machine with what it needs, and an embedded
14
+ * terminal to install or sign one in. Checks again when it opens and when a
15
+ * terminal is closed, never on a timer.
16
+ */
17
+ export function AgentList() {
18
+ const { data: agents } = useAgentStatusQuery();
19
+ const recheck = useRecheckAgents();
20
+ const [run, setRun] = useState<(AgentRun & { key: number }) | null>(null);
21
+ const [all, setAll] = useState(false);
22
+ const { mutate } = recheck;
23
+
24
+ useEffect(() => mutate(), [mutate]);
25
+
26
+ const providers = getAllProviderDefinitions().filter((p) => p.cli);
27
+ const shown = providers.filter((p) => all || FEATURED_AGENTS.includes(p.id));
28
+ const hidden = providers.length - shown.length;
29
+
30
+ return (
31
+ <div className="flex flex-col gap-3">
32
+ <ul className="flex flex-col gap-2">
33
+ {shown.map((p) => (
34
+ <AgentRow
35
+ key={p.id}
36
+ provider={p}
37
+ probe={agents?.[p.id]}
38
+ onRun={(r) => setRun({ ...r, key: Date.now() })}
39
+ />
40
+ ))}
41
+ </ul>
42
+ {run && (
43
+ <SetupTerminal
44
+ key={run.key}
45
+ title={run.title}
46
+ command={run.command}
47
+ onDone={() => {
48
+ setRun(null);
49
+ mutate();
50
+ }}
51
+ />
52
+ )}
53
+ <div className="flex items-center justify-between gap-2">
54
+ {hidden > 0 ? (
55
+ <Button size="sm" variant="ghost" onClick={() => setAll(true)}>
56
+ Show {hidden} more
57
+ </Button>
58
+ ) : (
59
+ <span />
60
+ )}
61
+ <Button
62
+ size="sm"
63
+ variant="ghost"
64
+ disabled={recheck.isPending}
65
+ onClick={() => mutate()}
66
+ >
67
+ <RefreshCw className={recheck.isPending ? "animate-spin" : ""} />
68
+ Check again
69
+ </Button>
70
+ </div>
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ import { Check, Download, LogIn } from "lucide-react";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import type { AgentProbe } from "@/lib/agents/probe";
7
+ import { agentState, SETUP_COMMANDS } from "@/lib/agents/setup-commands";
8
+ import type { ProviderDefinition } from "@/lib/providers/registry";
9
+
10
+ const LABEL = {
11
+ missing: "Not installed",
12
+ "needs-login": "Needs sign-in",
13
+ ready: "Ready",
14
+ installed: "Installed",
15
+ } as const;
16
+
17
+ export type AgentRun = { title: string; command: string };
18
+
19
+ /** One agent CLI: its state, and the install or sign-in it needs. */
20
+ export function AgentRow({
21
+ provider,
22
+ probe,
23
+ onRun,
24
+ }: {
25
+ provider: ProviderDefinition;
26
+ probe: AgentProbe | undefined;
27
+ onRun: (run: AgentRun) => void;
28
+ }) {
29
+ const state = probe ? agentState(probe) : null;
30
+ const commands = SETUP_COMMANDS[provider.id];
31
+ return (
32
+ <li className="bg-muted/40 flex min-h-11 items-center gap-3 rounded-lg px-3 py-2">
33
+ <div className="min-w-0 flex-1">
34
+ <p className="truncate text-sm font-medium">{provider.name}</p>
35
+ <p className="text-muted-foreground truncate text-xs">
36
+ {probe?.version
37
+ ? `v${probe.version}`
38
+ : (probe?.hint ?? provider.description)}
39
+ </p>
40
+ </div>
41
+ {!state && <Badge variant="outline">Checking</Badge>}
42
+ {state === "ready" && (
43
+ <Badge variant="success">
44
+ <Check className="mr-1 size-3" />
45
+ {LABEL.ready}
46
+ </Badge>
47
+ )}
48
+ {state === "installed" && <Badge variant="outline">{LABEL[state]}</Badge>}
49
+ {state === "needs-login" && (
50
+ <>
51
+ <Badge variant="secondary">{LABEL[state]}</Badge>
52
+ {commands?.signIn && (
53
+ <Button
54
+ size="sm"
55
+ variant="outline"
56
+ className="min-h-11 sm:min-h-8"
57
+ onClick={() =>
58
+ onRun({
59
+ title: `Sign in to ${provider.name}`,
60
+ command: commands.signIn!,
61
+ })
62
+ }
63
+ >
64
+ <LogIn />
65
+ Sign in
66
+ </Button>
67
+ )}
68
+ </>
69
+ )}
70
+ {state === "missing" &&
71
+ (commands?.install ? (
72
+ <Button
73
+ size="sm"
74
+ variant="outline"
75
+ className="min-h-11 sm:min-h-8"
76
+ onClick={() =>
77
+ onRun({
78
+ title: `Install ${provider.name}`,
79
+ command: commands.install!,
80
+ })
81
+ }
82
+ >
83
+ <Download />
84
+ Install
85
+ </Button>
86
+ ) : commands?.docs ? (
87
+ <Button
88
+ size="sm"
89
+ variant="outline"
90
+ className="min-h-11 sm:min-h-8"
91
+ asChild
92
+ >
93
+ <a href={commands.docs} target="_blank" rel="noreferrer">
94
+ How to install
95
+ </a>
96
+ </Button>
97
+ ) : (
98
+ <Badge variant="outline">{LABEL.missing}</Badge>
99
+ ))}
100
+ </li>
101
+ );
102
+ }
@@ -0,0 +1,100 @@
1
+ "use client";
2
+
3
+ import { FolderGit2, FolderOpen, FolderPlus, Check } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { useProjectsQuery } from "@/data/projects/queries";
6
+ import { cn } from "@/lib/utils";
7
+ import { addProjectActions, type AddProjectKind } from "@/stores/addProject";
8
+ import { welcomeUi, welcomeUiActions } from "@/stores/welcomeUi";
9
+ import { useSnapshot } from "valtio";
10
+
11
+ const WAYS: {
12
+ kind: AddProjectKind;
13
+ title: string;
14
+ hint: string;
15
+ icon: typeof FolderOpen;
16
+ }[] = [
17
+ {
18
+ kind: "folder",
19
+ title: "Open a folder",
20
+ hint: "A project already on this machine",
21
+ icon: FolderOpen,
22
+ },
23
+ {
24
+ kind: "clone",
25
+ title: "Clone a repo",
26
+ hint: "From a Git URL",
27
+ icon: FolderGit2,
28
+ },
29
+ {
30
+ kind: "name",
31
+ title: "Start an empty project",
32
+ hint: "A new folder with a name",
33
+ icon: FolderPlus,
34
+ },
35
+ ];
36
+
37
+ /** Add the project the first session will open in, or pick one already there. */
38
+ export function ProjectStep() {
39
+ const { projectId } = useSnapshot(welcomeUi);
40
+ const { data: projects = [] } = useProjectsQuery();
41
+ const own = projects.filter((p) => !p.is_uncategorized);
42
+
43
+ const add = (kind: AddProjectKind) =>
44
+ addProjectActions.show(kind, "local", (id) => {
45
+ welcomeUiActions.setProject(id);
46
+ welcomeUiActions.go("finish");
47
+ });
48
+
49
+ return (
50
+ <div className="flex flex-col gap-3">
51
+ {own.length > 0 && (
52
+ <ul className="flex flex-col gap-2">
53
+ {own.map((p) => (
54
+ <li key={p.id}>
55
+ <button
56
+ type="button"
57
+ onClick={() => {
58
+ welcomeUiActions.setProject(p.id);
59
+ welcomeUiActions.go("finish");
60
+ }}
61
+ className={cn(
62
+ "bg-muted/40 hover:bg-accent flex min-h-11 w-full items-center gap-3 rounded-lg px-3 py-2 text-left",
63
+ projectId === p.id && "ring-primary/40 ring-2"
64
+ )}
65
+ >
66
+ <span className="min-w-0 flex-1">
67
+ <span className="block truncate text-sm font-medium">
68
+ {p.name}
69
+ </span>
70
+ <span className="text-muted-foreground block truncate text-xs">
71
+ {p.working_directory}
72
+ </span>
73
+ </span>
74
+ {projectId === p.id && <Check className="size-4" />}
75
+ </button>
76
+ </li>
77
+ ))}
78
+ </ul>
79
+ )}
80
+ <div className="flex flex-col gap-2">
81
+ {WAYS.map((w) => (
82
+ <Button
83
+ key={w.kind}
84
+ variant="outline"
85
+ className="h-auto min-h-11 justify-start gap-3 px-3 py-2.5 text-left"
86
+ onClick={() => add(w.kind)}
87
+ >
88
+ <w.icon />
89
+ <span className="min-w-0">
90
+ <span className="block text-sm font-medium">{w.title}</span>
91
+ <span className="text-muted-foreground block text-xs font-normal">
92
+ {w.hint}
93
+ </span>
94
+ </span>
95
+ </Button>
96
+ ))}
97
+ </div>
98
+ </div>
99
+ );
100
+ }
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef } from "react";
4
+ import dynamic from "next/dynamic";
5
+ import { Button } from "@/components/ui/button";
6
+ import type { TerminalHandle } from "@/components/Terminal";
7
+
8
+ const Terminal = dynamic(
9
+ () => import("@/components/Terminal").then((m) => m.Terminal),
10
+ { ssr: false }
11
+ );
12
+
13
+ /**
14
+ * A terminal on this machine with one setup command typed at the prompt, for
15
+ * the user to read and run with Enter. It goes into their own shell over the
16
+ * existing terminal socket; the server runs nothing.
17
+ */
18
+ export function SetupTerminal({
19
+ title,
20
+ command,
21
+ onDone,
22
+ }: {
23
+ title: string;
24
+ command: string;
25
+ onDone: () => void;
26
+ }) {
27
+ const ref = useRef<TerminalHandle | null>(null);
28
+ const typed = useRef(false);
29
+ const box = useRef<HTMLDivElement>(null);
30
+
31
+ // On a phone it opens below the list, out of sight.
32
+ useEffect(() => {
33
+ box.current?.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
34
+ }, []);
35
+
36
+ const connected = useCallback(() => {
37
+ if (typed.current) return;
38
+ typed.current = true;
39
+ setTimeout(() => {
40
+ ref.current?.sendInput("\x15");
41
+ // Typed, not run: the user reads it and presses Enter.
42
+ setTimeout(() => ref.current?.sendInput(command), 50);
43
+ }, 100);
44
+ }, [command]);
45
+
46
+ return (
47
+ <div
48
+ ref={box}
49
+ className="bg-muted/30 hairline flex flex-col overflow-hidden rounded-lg"
50
+ >
51
+ <div className="flex items-center justify-between gap-2 px-3 py-1.5">
52
+ <span className="text-muted-foreground truncate text-xs">
53
+ {title}. Press Enter to run it.
54
+ </span>
55
+ <Button size="sm" onClick={onDone}>
56
+ Done, check again
57
+ </Button>
58
+ </div>
59
+ <div className="h-64 min-h-0">
60
+ <Terminal ref={ref} onConnected={connected} />
61
+ </div>
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,153 @@
1
+ "use client";
2
+
3
+ import { memo } from "react";
4
+ import { useSnapshot } from "valtio";
5
+ import { Check } from "lucide-react";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogTitle,
11
+ } from "@/components/ui/dialog";
12
+ import { Button } from "@/components/ui/button";
13
+ import { useProjectsQuery } from "@/data/projects/queries";
14
+ import { useEndWelcome, useWelcomeUrl } from "@/hooks/useWelcome";
15
+ import { cn } from "@/lib/utils";
16
+ import { WELCOME_STEPS, type WelcomeStep } from "@/lib/welcome-url";
17
+ import { welcomeUi, welcomeUiActions } from "@/stores/welcomeUi";
18
+ import { AgentList } from "./AgentList";
19
+ import { ProjectStep } from "./ProjectStep";
20
+
21
+ const STEPS: Record<
22
+ WelcomeStep,
23
+ { label: string; title: string; hint: string }
24
+ > = {
25
+ agents: {
26
+ label: "Agents",
27
+ title: "Connect your agents",
28
+ hint: "Install and sign in the coding agents you want to use. You need one.",
29
+ },
30
+ project: {
31
+ label: "Project",
32
+ title: "Add your first project",
33
+ hint: "A folder or repo for the first session to work in.",
34
+ },
35
+ finish: {
36
+ label: "Start",
37
+ title: "You're set",
38
+ hint: "Open a session in your project and give it a task.",
39
+ },
40
+ };
41
+
42
+ function FinishStep() {
43
+ const { projectId } = useSnapshot(welcomeUi);
44
+ const { data: projects = [] } = useProjectsQuery();
45
+ const project = projects.find((p) => p.id === projectId);
46
+ return (
47
+ <p className="text-muted-foreground bg-muted/40 rounded-lg px-3 py-3 text-sm">
48
+ {project ? (
49
+ <>
50
+ First session opens in{" "}
51
+ <span className="text-foreground font-medium">{project.name}</span>.
52
+ </>
53
+ ) : (
54
+ "No project yet. Add one from the sidebar whenever you are ready."
55
+ )}
56
+ </p>
57
+ );
58
+ }
59
+
60
+ /**
61
+ * First-run setup: agents, then a first project, then a session. It stays
62
+ * open until finished or skipped, and every step after the first can be skipped.
63
+ */
64
+ export const WelcomeDialog = memo(function WelcomeDialog() {
65
+ useWelcomeUrl();
66
+ const { open, step, projectId } = useSnapshot(welcomeUi);
67
+ const end = useEndWelcome();
68
+ const at = WELCOME_STEPS.indexOf(step);
69
+ const info = STEPS[step];
70
+ const last = at === WELCOME_STEPS.length - 1;
71
+
72
+ return (
73
+ <Dialog open={open}>
74
+ <DialogContent
75
+ showCloseButton={false}
76
+ onInteractOutside={(e) => e.preventDefault()}
77
+ onEscapeKeyDown={(e) => e.preventDefault()}
78
+ className="max-h-[100dvh] overflow-y-auto max-sm:h-[100dvh] max-sm:max-w-full max-sm:rounded-none sm:max-h-[90dvh] sm:max-w-xl"
79
+ >
80
+ <nav aria-label="Setup steps" className="flex items-center gap-1">
81
+ {WELCOME_STEPS.map((s, i) => (
82
+ <button
83
+ key={s}
84
+ type="button"
85
+ disabled={i === at}
86
+ onClick={() => welcomeUiActions.go(s)}
87
+ aria-current={i === at ? "step" : undefined}
88
+ className={cn(
89
+ "flex min-h-11 flex-1 items-center justify-center gap-1.5 rounded-lg px-2 text-xs font-medium sm:min-h-8",
90
+ i === at
91
+ ? "bg-foreground/[0.08] text-foreground"
92
+ : "text-muted-foreground hover:bg-foreground/[0.06]"
93
+ )}
94
+ >
95
+ {i < at && <Check className="size-3" />}
96
+ {STEPS[s].label}
97
+ </button>
98
+ ))}
99
+ <Button
100
+ variant="ghost"
101
+ size="sm"
102
+ className="min-h-11 sm:min-h-8"
103
+ onClick={() => end()}
104
+ >
105
+ Skip setup
106
+ </Button>
107
+ </nav>
108
+ <div className="flex flex-col gap-1">
109
+ <DialogTitle>{info.title}</DialogTitle>
110
+ <DialogDescription>{info.hint}</DialogDescription>
111
+ </div>
112
+ {step === "agents" && <AgentList />}
113
+ {step === "project" && <ProjectStep />}
114
+ {step === "finish" && <FinishStep />}
115
+ <div className="flex items-center justify-between gap-2">
116
+ <Button
117
+ variant="ghost"
118
+ className="min-h-11 sm:min-h-9"
119
+ disabled={at === 0}
120
+ onClick={() => welcomeUiActions.go(WELCOME_STEPS[at - 1])}
121
+ >
122
+ Back
123
+ </Button>
124
+ <div className="flex items-center gap-2">
125
+ {step === "project" && (
126
+ <Button
127
+ variant="ghost"
128
+ className="min-h-11 sm:min-h-9"
129
+ onClick={() => welcomeUiActions.go("finish")}
130
+ >
131
+ Skip
132
+ </Button>
133
+ )}
134
+ <Button
135
+ className="min-h-11 sm:min-h-9"
136
+ onClick={() =>
137
+ last ? end(true) : welcomeUiActions.go(WELCOME_STEPS[at + 1])
138
+ }
139
+ >
140
+ {last
141
+ ? projectId
142
+ ? "Start your first session"
143
+ : "Go to AgentOS"
144
+ : step === "agents"
145
+ ? "Continue"
146
+ : "Next"}
147
+ </Button>
148
+ </div>
149
+ </div>
150
+ </DialogContent>
151
+ </Dialog>
152
+ );
153
+ });