@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.
- package/README.md +11 -0
- package/app/api/welcome/route.ts +22 -0
- package/app/page.tsx +6 -0
- package/app/welcome/page.tsx +6 -0
- package/components/Chat/BackgroundTasks.tsx +76 -10
- package/components/Chat/ChatPanel.tsx +90 -34
- package/components/Chat/Subagent.tsx +25 -20
- package/components/Chat/Tools.tsx +70 -39
- package/components/Chat/background.tsx +114 -0
- package/components/Projects/AddProject/AddProjectDialog.tsx +2 -2
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Settings/SetupSection.tsx +37 -0
- package/components/Welcome/AgentList.tsx +73 -0
- package/components/Welcome/AgentRow.tsx +102 -0
- package/components/Welcome/ProjectStep.tsx +100 -0
- package/components/Welcome/SetupTerminal.tsx +64 -0
- package/components/Welcome/WelcomeDialog.tsx +153 -0
- package/data/agents/index.ts +16 -1
- package/data/chat/useChat.ts +40 -0
- package/data/welcome/index.ts +36 -0
- package/hooks/useWelcome.ts +70 -0
- package/lib/agents/probe.ts +27 -0
- package/lib/agents/setup-commands.ts +54 -0
- package/lib/chat/activity.ts +4 -2
- package/lib/chat/background.ts +77 -0
- package/lib/chat/driver.ts +6 -0
- package/lib/chat/drivers/claude-mapper.ts +44 -6
- package/lib/chat/drivers/claude.ts +4 -0
- package/lib/chat/events.ts +14 -0
- package/lib/chat/registry.ts +2 -0
- package/lib/chat/runner.ts +20 -0
- package/lib/chat/settings.ts +1 -0
- package/lib/chat/socket.ts +9 -0
- package/lib/chat/worker/client.ts +33 -0
- package/lib/chat/worker/host.ts +13 -0
- package/lib/chat/worker/main.ts +1 -1
- package/lib/chat/worker/protocol.ts +2 -0
- package/lib/db/migrations.ts +14 -0
- package/lib/security/auth.ts +11 -1
- package/lib/settings-url.ts +1 -0
- package/lib/welcome-url.ts +33 -0
- package/lib/welcome.ts +88 -0
- package/package.json +1 -1
- package/stores/addProject.ts +19 -1
- 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.
|
|
33
|
-
|
|
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
|
+
});
|