@saadnvd1/agent-os 0.3.736 → 0.3.738
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/app/api/hosts/[id]/agents/route.ts +22 -0
- package/app/api/hosts/[id]/route.ts +24 -1
- package/app/api/hosts/route.ts +3 -0
- package/app/api/machine/browser/route.ts +22 -0
- package/app/api/machine/route.ts +10 -0
- package/components/Chat/ChatPanel.tsx +12 -3
- package/components/Chat/Messages.test.ts +13 -1
- package/components/Chat/Messages.tsx +19 -2
- package/components/Chat/StepOutcomes.tsx +66 -0
- package/components/Chat/Tools.test.ts +52 -0
- package/components/Chat/Tools.tsx +21 -4
- package/components/Claims/ClaimsSheet.tsx +19 -1
- package/components/Claims/claims-ui.test.ts +1 -1
- package/components/Claims/format.ts +1 -1
- package/components/Draft/AgentModelChip.test.ts +43 -0
- package/components/Draft/AgentModelChip.tsx +162 -0
- package/components/Draft/Chip.tsx +9 -2
- package/components/Draft/DraftChips.tsx +7 -45
- package/components/Draft/DraftPanel.tsx +10 -1
- package/components/Hosts/AccountMachines.tsx +3 -1
- package/components/Hosts/HostRow.tsx +57 -7
- package/components/Hosts/RenameHost.tsx +59 -0
- package/components/SessionList/MachineSwitcher.tsx +67 -0
- package/components/SessionList/SidebarHeader.tsx +4 -0
- package/components/SessionList/useSidebarData.ts +21 -3
- package/data/hosts/queries.ts +17 -0
- package/data/machines/index.ts +54 -0
- package/data/machines/keys.ts +5 -0
- package/hooks/useDraftRequests.ts +6 -1
- package/lib/chat/drivers/pi-mapper.ts +17 -1
- package/lib/chat/drivers/pi.ts +13 -1
- package/lib/chat/drivers/turn-outcome.ts +25 -0
- package/lib/chat/events.ts +2 -0
- package/lib/chat/group.ts +23 -3
- package/lib/chat/page.ts +3 -0
- package/lib/chat/tool-outcome.ts +45 -0
- package/lib/db/migrations/20261023T1200_host_info.ts +20 -0
- package/lib/db/types.ts +4 -0
- package/lib/drafts.ts +8 -2
- package/lib/hosts/index.ts +43 -5
- package/lib/machines/browser.ts +37 -0
- package/lib/machines/catalog.ts +97 -0
- package/lib/machines/identity.ts +52 -0
- package/lib/machines/label.ts +36 -0
- package/lib/machines/ollama.ts +32 -0
- package/lib/machines/remote.ts +179 -0
- package/lib/machines/report.ts +8 -0
- package/lib/machines/types.ts +55 -0
- package/lib/peer/allowlist.ts +2 -0
- package/lib/peer/machines.ts +30 -2
- package/lib/sidebar/machines.ts +10 -0
- package/lib/start-options.ts +20 -4
- package/package.json +1 -1
- package/scripts/screenshots/chat-verify.ts +68 -0
- package/scripts/screenshots/seed.ts +9 -0
- package/stores/machines.ts +51 -0
|
@@ -90,10 +90,17 @@ export function ChipItem({
|
|
|
90
90
|
);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export function ChipHeading({
|
|
93
|
+
export function ChipHeading({
|
|
94
|
+
children,
|
|
95
|
+
first,
|
|
96
|
+
}: {
|
|
97
|
+
children: React.ReactNode;
|
|
98
|
+
// The top of the menu: nothing above it to separate from.
|
|
99
|
+
first?: boolean;
|
|
100
|
+
}) {
|
|
94
101
|
return (
|
|
95
102
|
<>
|
|
96
|
-
<DropdownMenuSeparator />
|
|
103
|
+
{!first && <DropdownMenuSeparator />}
|
|
97
104
|
<DropdownMenuLabel className="text-muted-foreground text-[11px] font-medium">
|
|
98
105
|
{children}
|
|
99
106
|
</DropdownMenuLabel>
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect } from "react";
|
|
4
4
|
import {
|
|
5
|
-
Bot,
|
|
6
5
|
Cpu,
|
|
7
6
|
Eye,
|
|
8
7
|
FolderGit2,
|
|
@@ -15,14 +14,13 @@ import {
|
|
|
15
14
|
import type { Host } from "@/lib/db";
|
|
16
15
|
import type { Project } from "@/lib/db";
|
|
17
16
|
import { draftHostGone, newDraft, type Draft } from "@/lib/drafts";
|
|
18
|
-
import { AGENT_OPTIONS, agentLabel } from "@/lib/agent-options";
|
|
19
|
-
import { resolveModelForAgent } from "@/lib/model-catalog";
|
|
20
17
|
import type { GitCheck } from "@/data/git/queries";
|
|
21
|
-
import { useAgentStatusQuery } from "@/data/agents";
|
|
22
18
|
import { useProjectMachinesQuery } from "@/data/projects";
|
|
23
19
|
import { modelChoices, useModelsQuery } from "@/data/models";
|
|
24
|
-
import
|
|
20
|
+
import { useBrowserMachine } from "@/data/machines";
|
|
21
|
+
import { machineLabel } from "@/lib/machines/label";
|
|
25
22
|
import { RoleChips } from "./RoleChips";
|
|
23
|
+
import { AgentModelChip } from "./AgentModelChip";
|
|
26
24
|
import { ChipHeading, ChipItem, ChipMenu, ChipToggle } from "./Chip";
|
|
27
25
|
import {
|
|
28
26
|
DEFAULT_PRIORITY,
|
|
@@ -31,14 +29,6 @@ import {
|
|
|
31
29
|
PRIORITY_LABEL,
|
|
32
30
|
} from "@/lib/tasks/schedule-types";
|
|
33
31
|
|
|
34
|
-
// What stands between an agent and its first prompt, if anything.
|
|
35
|
-
export function agentBlocker(probe?: AgentProbe): string | null {
|
|
36
|
-
if (!probe) return null;
|
|
37
|
-
if (!probe.installed) return "Not installed";
|
|
38
|
-
if (probe.auth === "needs-login") return "Needs sign-in";
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
32
|
const BRANCHES_SHOWN = 20;
|
|
43
33
|
|
|
44
34
|
// A draft's choices, above its composer: where it runs and how.
|
|
@@ -58,6 +48,7 @@ export function DraftChips({
|
|
|
58
48
|
git: GitCheck | undefined;
|
|
59
49
|
onChange: (patch: Partial<Draft>) => void;
|
|
60
50
|
}) {
|
|
51
|
+
const browser = useBrowserMachine();
|
|
61
52
|
const real = projects.filter((p) => !p.is_uncategorized);
|
|
62
53
|
const project = real.find((p) => p.id === draft.projectId) ?? null;
|
|
63
54
|
const projectHere = !!project && (project.host_id || "local") === "local";
|
|
@@ -70,10 +61,6 @@ export function DraftChips({
|
|
|
70
61
|
: hosts;
|
|
71
62
|
const host = hosts.find((h) => h.id === draft.hostId);
|
|
72
63
|
const local = draft.hostId === "local";
|
|
73
|
-
// Which agents are installed and signed in, on this machine: a draft for
|
|
74
|
-
// another machine shows them all.
|
|
75
|
-
const { data: localProbes } = useAgentStatusQuery();
|
|
76
|
-
const probes = local ? localProbes : undefined;
|
|
77
64
|
const base = draft.baseBranch ?? git?.defaultBranch ?? "main";
|
|
78
65
|
const { data: models } = useModelsQuery(draft.openPr);
|
|
79
66
|
// A project elsewhere runs where it lives; a scratch chat, a task or a
|
|
@@ -147,7 +134,7 @@ export function DraftChips({
|
|
|
147
134
|
<ChipMenu
|
|
148
135
|
icon={Monitor}
|
|
149
136
|
name="Machine"
|
|
150
|
-
label={host
|
|
137
|
+
label={host ? machineLabel(host) : draft.hostId}
|
|
151
138
|
disabled={!canMove}
|
|
152
139
|
title={canMove ? undefined : "Runs where the project lives"}
|
|
153
140
|
>
|
|
@@ -164,31 +151,14 @@ export function DraftChips({
|
|
|
164
151
|
})
|
|
165
152
|
}
|
|
166
153
|
>
|
|
167
|
-
{h.
|
|
154
|
+
{machineLabel(h, browser.hostId)}
|
|
168
155
|
</ChipItem>
|
|
169
156
|
))}
|
|
170
157
|
</ChipMenu>
|
|
171
158
|
)}
|
|
172
159
|
|
|
173
160
|
{!draft.openPr && !draft.agentRole && (
|
|
174
|
-
<
|
|
175
|
-
{AGENT_OPTIONS.map((o) => (
|
|
176
|
-
<ChipItem
|
|
177
|
-
key={o.value}
|
|
178
|
-
checked={o.value === draft.agentType}
|
|
179
|
-
disabled={probes?.[o.value] ? !probes[o.value].installed : false}
|
|
180
|
-
hint={agentBlocker(probes?.[o.value]) ?? undefined}
|
|
181
|
-
onSelect={() =>
|
|
182
|
-
onChange({
|
|
183
|
-
agentType: o.value,
|
|
184
|
-
model: resolveModelForAgent(o.value, draft.model),
|
|
185
|
-
})
|
|
186
|
-
}
|
|
187
|
-
>
|
|
188
|
-
{o.label}
|
|
189
|
-
</ChipItem>
|
|
190
|
-
))}
|
|
191
|
-
</ChipMenu>
|
|
161
|
+
<AgentModelChip draft={draft} onChange={onChange} />
|
|
192
162
|
)}
|
|
193
163
|
|
|
194
164
|
{!draft.openPr && local && (
|
|
@@ -314,14 +284,6 @@ export function DraftChips({
|
|
|
314
284
|
onChange={(terminal) => onChange({ terminal })}
|
|
315
285
|
/>
|
|
316
286
|
)}
|
|
317
|
-
{!draft.openPr && agentBlocker(probes?.[draft.agentType]) && (
|
|
318
|
-
<p className="w-full text-xs text-amber-600 dark:text-amber-400">
|
|
319
|
-
{agentBlocker(probes?.[draft.agentType])}
|
|
320
|
-
{probes?.[draft.agentType]?.hint
|
|
321
|
-
? `: ${probes[draft.agentType].hint}`
|
|
322
|
-
: ""}
|
|
323
|
-
</p>
|
|
324
|
-
)}
|
|
325
287
|
</div>
|
|
326
288
|
);
|
|
327
289
|
}
|
|
@@ -27,6 +27,7 @@ import { useStartAnalystBeside } from "@/components/Analyst/pair";
|
|
|
27
27
|
import { DraftChips } from "./DraftChips";
|
|
28
28
|
import { DraftHeading } from "./DraftHeading";
|
|
29
29
|
import { ModelUnavailableDialog } from "./ModelUnavailableDialog";
|
|
30
|
+
import { useMachineCatalog } from "@/data/machines";
|
|
30
31
|
import { draftModels } from "./draftModels";
|
|
31
32
|
|
|
32
33
|
// A new session before its first send: choices on top, a composer below.
|
|
@@ -57,6 +58,8 @@ export function DraftPanel({
|
|
|
57
58
|
draft?.projectId ?? null,
|
|
58
59
|
discovers
|
|
59
60
|
);
|
|
61
|
+
// On another machine the models are the ones it reports, not this one's.
|
|
62
|
+
const there = useMachineCatalog(draft?.hostId ?? "local");
|
|
60
63
|
const launch = useLaunchSession();
|
|
61
64
|
const startAnalyst = useStartAnalystBeside();
|
|
62
65
|
const { workspace } = useSelectedWorkspace();
|
|
@@ -253,7 +256,13 @@ export function DraftPanel({
|
|
|
253
256
|
}
|
|
254
257
|
onSend={(text, images, files) => void send(text, images, files)}
|
|
255
258
|
onStop={() => {}}
|
|
256
|
-
models={
|
|
259
|
+
models={
|
|
260
|
+
(draft.hostId !== "local" &&
|
|
261
|
+
there.data?.catalog?.agents.find(
|
|
262
|
+
(a) => a.agent === draft.agentType
|
|
263
|
+
)?.models) ||
|
|
264
|
+
draftModels(draft.agentType, found)
|
|
265
|
+
}
|
|
257
266
|
model={draft.model}
|
|
258
267
|
onSetModel={(model) => draftsActions.update(draft.id, { model })}
|
|
259
268
|
access={draft.openPr ? undefined : draft.access}
|
|
@@ -55,7 +55,9 @@ function MachineRow({
|
|
|
55
55
|
</div>
|
|
56
56
|
{linked ? (
|
|
57
57
|
<span className="text-xs font-medium text-emerald-600 dark:text-emerald-400">
|
|
58
|
-
|
|
58
|
+
{machine.linkedVia === "tailscale"
|
|
59
|
+
? "Linked over Tailscale"
|
|
60
|
+
: "Linked"}
|
|
59
61
|
</span>
|
|
60
62
|
) : machine.linkState === "suspended" ? (
|
|
61
63
|
<span
|
|
@@ -11,15 +11,20 @@ import {
|
|
|
11
11
|
PlugZap,
|
|
12
12
|
Loader2,
|
|
13
13
|
Link2,
|
|
14
|
+
MapPin,
|
|
15
|
+
Pencil,
|
|
14
16
|
Unlink,
|
|
15
17
|
} from "lucide-react";
|
|
16
18
|
import { Notice } from "@/components/ui/notice";
|
|
17
19
|
import type { Host } from "@/lib/db";
|
|
18
20
|
import { useDeleteHost, useLinkHost, useTestHost } from "@/data/hosts";
|
|
21
|
+
import { useBrowserMachine } from "@/data/machines";
|
|
22
|
+
import { machineDetail, machineLabel } from "@/lib/machines/label";
|
|
19
23
|
import { useUnlinkPeer } from "@/data/peers";
|
|
20
24
|
import { NEEDS_PRO, UPGRADE_URL } from "@/lib/peer/reasons";
|
|
21
25
|
import { cn } from "@/lib/utils";
|
|
22
26
|
import { PeerActivitySheet } from "./PeerActivitySheet";
|
|
27
|
+
import { RenameHost } from "./RenameHost";
|
|
23
28
|
|
|
24
29
|
function connectLine(host: Host): string {
|
|
25
30
|
if (host.peer_state === "active")
|
|
@@ -67,6 +72,9 @@ export function HostRow({ host }: { host: Host }) {
|
|
|
67
72
|
const unlink = useUnlinkPeer();
|
|
68
73
|
const [activity, setActivity] = useState(false);
|
|
69
74
|
const [confirmUnlink, setConfirmUnlink] = useState(false);
|
|
75
|
+
const [renaming, setRenaming] = useState(false);
|
|
76
|
+
const browser = useBrowserMachine();
|
|
77
|
+
const here = browser.hostId === host.id;
|
|
70
78
|
const isLocal = host.id === "local";
|
|
71
79
|
const viaConnect = host.kind === "connect";
|
|
72
80
|
const pinned =
|
|
@@ -80,17 +88,58 @@ export function HostRow({ host }: { host: Host }) {
|
|
|
80
88
|
<div className="flex items-center gap-3">
|
|
81
89
|
<Icon className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
82
90
|
<div className="min-w-0 flex-1">
|
|
83
|
-
<p className="truncate text-sm font-medium">
|
|
91
|
+
<p className="truncate text-sm font-medium">
|
|
92
|
+
{machineLabel(host, browser.hostId)}
|
|
93
|
+
</p>
|
|
84
94
|
<p className="text-muted-foreground truncate text-xs">
|
|
85
95
|
{isLocal
|
|
86
|
-
? "
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
? [machineDetail(host), "serves this dashboard"]
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
.join(" · ")
|
|
99
|
+
: [
|
|
100
|
+
machineDetail(host),
|
|
101
|
+
viaConnect
|
|
102
|
+
? connectLine(host)
|
|
103
|
+
: host.linked
|
|
104
|
+
? `${host.ssh_target} · tasks run on its AgentOS`
|
|
105
|
+
: host.ssh_target,
|
|
106
|
+
]
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.join(" · ")}
|
|
92
109
|
</p>
|
|
93
110
|
</div>
|
|
111
|
+
<Button
|
|
112
|
+
variant="ghost"
|
|
113
|
+
size="icon-sm"
|
|
114
|
+
className="h-11 w-11 sm:h-8 sm:w-8"
|
|
115
|
+
onClick={() => setRenaming(true)}
|
|
116
|
+
disabled={renaming}
|
|
117
|
+
aria-label={`Rename ${host.name}`}
|
|
118
|
+
title="Give it a name of your own"
|
|
119
|
+
>
|
|
120
|
+
<Pencil className="h-4 w-4" />
|
|
121
|
+
</Button>
|
|
122
|
+
<Button
|
|
123
|
+
variant="ghost"
|
|
124
|
+
size="icon-sm"
|
|
125
|
+
className={cn("h-11 w-11 sm:h-8 sm:w-8", here && "text-primary")}
|
|
126
|
+
aria-pressed={here}
|
|
127
|
+
onClick={() => browser.set(here ? null : host.id)}
|
|
128
|
+
aria-label={
|
|
129
|
+
here
|
|
130
|
+
? `This browser is not on ${host.name}`
|
|
131
|
+
: `This browser is on ${host.name}`
|
|
132
|
+
}
|
|
133
|
+
title={
|
|
134
|
+
here
|
|
135
|
+
? browser.detected
|
|
136
|
+
? "Detected from the connection. Tap to say otherwise"
|
|
137
|
+
: "You said you're here. Tap to undo"
|
|
138
|
+
: "Say this is the machine you're using now"
|
|
139
|
+
}
|
|
140
|
+
>
|
|
141
|
+
<MapPin className="h-4 w-4" />
|
|
142
|
+
</Button>
|
|
94
143
|
{!isLocal && (
|
|
95
144
|
<>
|
|
96
145
|
{!viaConnect && (
|
|
@@ -171,6 +220,7 @@ export function HostRow({ host }: { host: Host }) {
|
|
|
171
220
|
</>
|
|
172
221
|
)}
|
|
173
222
|
</div>
|
|
223
|
+
{renaming && <RenameHost host={host} onDone={() => setRenaming(false)} />}
|
|
174
224
|
{result && (
|
|
175
225
|
<p
|
|
176
226
|
className={cn(
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import type { Host } from "@/lib/db";
|
|
7
|
+
import { useRenameHost } from "@/data/hosts";
|
|
8
|
+
|
|
9
|
+
/** A name of the person's own for a machine, shown on every screen. */
|
|
10
|
+
export function RenameHost({
|
|
11
|
+
host,
|
|
12
|
+
onDone,
|
|
13
|
+
}: {
|
|
14
|
+
host: Host;
|
|
15
|
+
onDone: () => void;
|
|
16
|
+
}) {
|
|
17
|
+
const rename = useRenameHost();
|
|
18
|
+
const [name, setName] = useState(host.name);
|
|
19
|
+
const save = () =>
|
|
20
|
+
rename.mutate({ id: host.id, name }, { onSuccess: onDone });
|
|
21
|
+
return (
|
|
22
|
+
<form
|
|
23
|
+
className="mt-2 flex flex-wrap items-center gap-2"
|
|
24
|
+
onSubmit={(e) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
save();
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<Input
|
|
30
|
+
value={name}
|
|
31
|
+
onChange={(e) => setName(e.target.value)}
|
|
32
|
+
aria-label={`Name for ${host.name}`}
|
|
33
|
+
maxLength={60}
|
|
34
|
+
autoFocus
|
|
35
|
+
className="h-11 min-w-0 flex-1 sm:h-9"
|
|
36
|
+
/>
|
|
37
|
+
<Button
|
|
38
|
+
type="submit"
|
|
39
|
+
className="min-h-11 sm:min-h-9"
|
|
40
|
+
disabled={rename.isPending || !name.trim()}
|
|
41
|
+
>
|
|
42
|
+
Save
|
|
43
|
+
</Button>
|
|
44
|
+
<Button
|
|
45
|
+
type="button"
|
|
46
|
+
variant="ghost"
|
|
47
|
+
className="min-h-11 sm:min-h-9"
|
|
48
|
+
onClick={onDone}
|
|
49
|
+
>
|
|
50
|
+
Cancel
|
|
51
|
+
</Button>
|
|
52
|
+
{rename.error && (
|
|
53
|
+
<p className="text-destructive w-full text-xs">
|
|
54
|
+
{rename.error.message}
|
|
55
|
+
</p>
|
|
56
|
+
)}
|
|
57
|
+
</form>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { useSnapshot } from "valtio";
|
|
5
|
+
import { Check, ChevronDown, Monitor } from "lucide-react";
|
|
6
|
+
import * as DM from "@/components/ui/dropdown-menu";
|
|
7
|
+
import { useHostsQuery } from "@/data/hosts";
|
|
8
|
+
import { useBrowserMachine } from "@/data/machines";
|
|
9
|
+
import { machineLabel } from "@/lib/machines/label";
|
|
10
|
+
import { machinesUi, machinesUiActions } from "@/stores/machines";
|
|
11
|
+
import { cn } from "@/lib/utils";
|
|
12
|
+
|
|
13
|
+
// A quick way between the machines linked here: narrows the sidebar to one
|
|
14
|
+
// machine's sessions and starts new chats there. Hidden until one is linked.
|
|
15
|
+
export function MachineSwitcher() {
|
|
16
|
+
const { data: hosts = [], isSuccess } = useHostsQuery();
|
|
17
|
+
const { scopeHostId } = useSnapshot(machinesUi);
|
|
18
|
+
const browser = useBrowserMachine();
|
|
19
|
+
const machines = hosts.filter((h) => h.id === "local" || h.linked);
|
|
20
|
+
const current = machines.find((h) => h.id === scopeHostId) ?? null;
|
|
21
|
+
|
|
22
|
+
// A machine that was unlinked since goes back to all of them.
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (isSuccess && scopeHostId && !current) machinesUiActions.setScope(null);
|
|
25
|
+
}, [isSuccess, scopeHostId, current]);
|
|
26
|
+
|
|
27
|
+
if (machines.length < 2) return null;
|
|
28
|
+
return (
|
|
29
|
+
<DM.DropdownMenu>
|
|
30
|
+
<DM.DropdownMenuTrigger asChild>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
aria-label={`Machine: ${current ? machineLabel(current) : "All machines"}`}
|
|
34
|
+
className={cn(
|
|
35
|
+
"border-border/70 text-muted-foreground hover:text-foreground hover:bg-foreground/[0.04] flex h-11 max-w-full min-w-0 items-center gap-1.5 rounded-lg border px-2.5 text-xs md:h-8",
|
|
36
|
+
current && "border-primary/40 bg-primary/10 text-primary"
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<Monitor className="h-3.5 w-3.5 shrink-0" />
|
|
40
|
+
<span className="truncate">
|
|
41
|
+
{current ? machineLabel(current) : "All machines"}
|
|
42
|
+
</span>
|
|
43
|
+
<ChevronDown className="h-3 w-3 shrink-0" />
|
|
44
|
+
</button>
|
|
45
|
+
</DM.DropdownMenuTrigger>
|
|
46
|
+
<DM.DropdownMenuContent align="start" className="max-w-80">
|
|
47
|
+
<DM.DropdownMenuLabel className="text-muted-foreground text-xs font-normal">
|
|
48
|
+
Machine
|
|
49
|
+
</DM.DropdownMenuLabel>
|
|
50
|
+
{[null, ...machines].map((h) => (
|
|
51
|
+
<DM.DropdownMenuItem
|
|
52
|
+
key={h?.id ?? "all"}
|
|
53
|
+
className="min-h-11 md:min-h-8"
|
|
54
|
+
onClick={() => machinesUiActions.setScope(h?.id ?? null)}
|
|
55
|
+
>
|
|
56
|
+
<span className="flex-1 truncate">
|
|
57
|
+
{h ? machineLabel(h, browser.hostId) : "All machines"}
|
|
58
|
+
</span>
|
|
59
|
+
{(current?.id ?? null) === (h?.id ?? null) && (
|
|
60
|
+
<Check className="text-primary h-3.5 w-3.5" />
|
|
61
|
+
)}
|
|
62
|
+
</DM.DropdownMenuItem>
|
|
63
|
+
))}
|
|
64
|
+
</DM.DropdownMenuContent>
|
|
65
|
+
</DM.DropdownMenu>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { MachineSwitcher } from "./MachineSwitcher";
|
|
3
4
|
import {
|
|
4
5
|
Archive,
|
|
5
6
|
Star,
|
|
@@ -189,6 +190,9 @@ export function SidebarHeader(props: SidebarHeaderProps) {
|
|
|
189
190
|
]}
|
|
190
191
|
/>
|
|
191
192
|
</div>
|
|
193
|
+
<div className="w-full empty:hidden">
|
|
194
|
+
<MachineSwitcher />
|
|
195
|
+
</div>
|
|
192
196
|
</div>
|
|
193
197
|
);
|
|
194
198
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback, useMemo } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo } from "react";
|
|
4
4
|
import { useSnapshot } from "valtio";
|
|
5
5
|
import type { Session } from "@/lib/db";
|
|
6
6
|
import type { SessionGroup } from "@/lib/sidebar/shelves";
|
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
projectsInWorkspace,
|
|
12
12
|
} from "@/lib/sidebar/shelves";
|
|
13
13
|
import type { TaskState } from "@/lib/tasks/state";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
inMachineScope,
|
|
16
|
+
machineGroups,
|
|
17
|
+
onMachineOnly,
|
|
18
|
+
} from "@/lib/sidebar/machines";
|
|
15
19
|
import { orchestratorsToStart } from "@/lib/sidebar/orchestrator-row";
|
|
16
20
|
import { useSessionsQuery } from "@/data/sessions";
|
|
17
21
|
import { useLinkedHostNames } from "@/data/hosts";
|
|
@@ -23,6 +27,7 @@ import { useSelectedWorkspace } from "@/hooks/useSelectedWorkspace";
|
|
|
23
27
|
import { useOrchestratorsQuery } from "@/data/orchestrators";
|
|
24
28
|
import { useClock } from "@/hooks/useMinuteTick";
|
|
25
29
|
import { sidebarUi } from "@/stores/sidebarUi";
|
|
30
|
+
import { machinesUi, machinesUiActions } from "@/stores/machines";
|
|
26
31
|
import type { SessionStatus } from "./SessionList.types";
|
|
27
32
|
|
|
28
33
|
// One empty list for queries still loading, so memos below hold.
|
|
@@ -35,6 +40,10 @@ export function useSidebarData(
|
|
|
35
40
|
sessionStatuses: Record<string, SessionStatus> | undefined
|
|
36
41
|
) {
|
|
37
42
|
const ui = useSnapshot(sidebarUi);
|
|
43
|
+
const scope = useSnapshot(machinesUi).scopeHostId;
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
machinesUiActions.hydrate();
|
|
46
|
+
}, []);
|
|
38
47
|
// Wakes and 3-day settles are read from the clock.
|
|
39
48
|
const now = useClock();
|
|
40
49
|
|
|
@@ -98,6 +107,7 @@ export function useSidebarData(
|
|
|
98
107
|
sessions: sessions.filter(
|
|
99
108
|
(s) =>
|
|
100
109
|
!onMachineOnly(s, linked) &&
|
|
110
|
+
inMachineScope(s, scope) &&
|
|
101
111
|
inWorkspace(
|
|
102
112
|
s,
|
|
103
113
|
(id) => byId.get(id)?.workspace_id,
|
|
@@ -122,7 +132,14 @@ export function useSidebarData(
|
|
|
122
132
|
machines:
|
|
123
133
|
project || ui.orchestratorId
|
|
124
134
|
? []
|
|
125
|
-
: machineGroups(
|
|
135
|
+
: machineGroups(
|
|
136
|
+
{ ...common, sessions },
|
|
137
|
+
scope
|
|
138
|
+
? Object.fromEntries(
|
|
139
|
+
Object.entries(linked).filter(([id]) => id === scope)
|
|
140
|
+
)
|
|
141
|
+
: linked
|
|
142
|
+
),
|
|
126
143
|
};
|
|
127
144
|
}, [
|
|
128
145
|
now,
|
|
@@ -138,6 +155,7 @@ export function useSidebarData(
|
|
|
138
155
|
project,
|
|
139
156
|
ui.query,
|
|
140
157
|
ui.orchestratorId,
|
|
158
|
+
scope,
|
|
141
159
|
]);
|
|
142
160
|
|
|
143
161
|
// Per workspace, how many of its project sessions are working: the
|
package/data/hosts/queries.ts
CHANGED
|
@@ -61,6 +61,23 @@ export function useDeleteHost() {
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Rename a machine; "local" renames this one. */
|
|
65
|
+
export function useRenameHost() {
|
|
66
|
+
const queryClient = useQueryClient();
|
|
67
|
+
return useMutation({
|
|
68
|
+
mutationFn: async ({ id, name }: { id: string; name: string }) =>
|
|
69
|
+
json<{ name: string }>(
|
|
70
|
+
await fetch(`/api/hosts/${encodeURIComponent(id)}`, {
|
|
71
|
+
method: "PATCH",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify({ name }),
|
|
74
|
+
})
|
|
75
|
+
),
|
|
76
|
+
// The name is on the claims, the sidebar and the chips too.
|
|
77
|
+
onSuccess: () => queryClient.invalidateQueries(),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
export interface HostTestResult {
|
|
65
82
|
ok: boolean;
|
|
66
83
|
hostname?: string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useSnapshot } from "valtio";
|
|
4
|
+
import { machinesUi, machinesUiActions } from "@/stores/machines";
|
|
5
|
+
import type { HostCatalogResponse } from "@/lib/machines/types";
|
|
6
|
+
import type { BrowserMachine } from "@/lib/machines/browser";
|
|
7
|
+
import { machineKeys } from "./keys";
|
|
8
|
+
|
|
9
|
+
/** The agents and models a machine reports, for the new-session form. */
|
|
10
|
+
export function useMachineCatalog(hostId: string) {
|
|
11
|
+
return useQuery({
|
|
12
|
+
queryKey: machineKeys.agents(hostId),
|
|
13
|
+
queryFn: async () => {
|
|
14
|
+
const res = await fetch(
|
|
15
|
+
`/api/hosts/${encodeURIComponent(hostId)}/agents`
|
|
16
|
+
);
|
|
17
|
+
const data = await res.json().catch(() => ({}));
|
|
18
|
+
if (!res.ok) throw new Error(data.error || "Couldn't list its agents");
|
|
19
|
+
return data as HostCatalogResponse;
|
|
20
|
+
},
|
|
21
|
+
staleTime: 60_000,
|
|
22
|
+
retry: false,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The machine this browser is on: what the person set, else what the server
|
|
28
|
+
* could tell from the connection, else unknown.
|
|
29
|
+
*/
|
|
30
|
+
export function useBrowserMachine(): {
|
|
31
|
+
hostId: string | null;
|
|
32
|
+
detected: BrowserMachine["how"];
|
|
33
|
+
set: (hostId: string | null) => void;
|
|
34
|
+
} {
|
|
35
|
+
const snap = useSnapshot(machinesUi);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
machinesUiActions.hydrate();
|
|
38
|
+
}, []);
|
|
39
|
+
const { data } = useQuery({
|
|
40
|
+
queryKey: machineKeys.browser(),
|
|
41
|
+
queryFn: async () => {
|
|
42
|
+
const res = await fetch("/api/machine/browser");
|
|
43
|
+
if (!res.ok) throw new Error("Couldn't tell");
|
|
44
|
+
return (await res.json()) as BrowserMachine;
|
|
45
|
+
},
|
|
46
|
+
staleTime: Infinity,
|
|
47
|
+
retry: false,
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
hostId: snap.browserHostId ?? data?.hostId ?? null,
|
|
51
|
+
detected: data?.how ?? null,
|
|
52
|
+
set: machinesUiActions.setBrowserHost,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { machinesUi } from "@/stores/machines";
|
|
3
4
|
import { useEffect, useRef } from "react";
|
|
4
5
|
import { subscribe } from "valtio";
|
|
5
6
|
import { FolderGit2 } from "lucide-react";
|
|
@@ -64,7 +65,11 @@ export function useDraftRequests(options: Options) {
|
|
|
64
65
|
? { agentType: s.agent_type, model: s.model, access: s.chat_access }
|
|
65
66
|
: {};
|
|
66
67
|
const project = projects.find((p) => p.id === projectId) ?? null;
|
|
67
|
-
const draft = makeDraft(uuid(), project, carry, {
|
|
68
|
+
const draft = makeDraft(uuid(), project, carry, {
|
|
69
|
+
openPr,
|
|
70
|
+
workspaceId,
|
|
71
|
+
hostId: openPr ? null : machinesUi.scopeHostId,
|
|
72
|
+
});
|
|
68
73
|
draftsActions.put(draft);
|
|
69
74
|
show(draft.id);
|
|
70
75
|
};
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "crypto";
|
|
|
2
2
|
import type { DriverEvent, UsageTotals } from "../events";
|
|
3
3
|
import { toolDiff, toolTitle } from "../tools";
|
|
4
4
|
import { TurnItems, type ToolStart } from "./turn-items";
|
|
5
|
-
import { logTurnEnd, turnOutcome } from "./turn-outcome";
|
|
5
|
+
import { logTurnEnd, needsSummary, turnOutcome } from "./turn-outcome";
|
|
6
6
|
|
|
7
7
|
type P = Record<string, unknown>;
|
|
8
8
|
const str = (v: unknown) => (typeof v === "string" ? v : "");
|
|
@@ -53,6 +53,7 @@ export class PiMapper {
|
|
|
53
53
|
private error = "";
|
|
54
54
|
// The last answer's text, its tool calls and who gave it, for the notice
|
|
55
55
|
// when a turn ends without doing anything.
|
|
56
|
+
private toolsRun = 0;
|
|
56
57
|
private last = { text: "", calls: 0, provider: "", model: "", api: "" };
|
|
57
58
|
totals: UsageTotals = {
|
|
58
59
|
costUsd: 0,
|
|
@@ -74,6 +75,8 @@ export class PiMapper {
|
|
|
74
75
|
map(e: P): void {
|
|
75
76
|
switch (e.type) {
|
|
76
77
|
case "agent_start":
|
|
78
|
+
// A nudged run continues the same turn, so its tools still count.
|
|
79
|
+
if (!this.items.inTurn) this.toolsRun = 0;
|
|
77
80
|
this.stopReason = this.error = "";
|
|
78
81
|
this.last = { text: "", calls: 0, provider: "", model: "", api: "" };
|
|
79
82
|
return this.items.startTurn();
|
|
@@ -85,6 +88,7 @@ export class PiMapper {
|
|
|
85
88
|
case "message_end":
|
|
86
89
|
return this.ended(e.message as P);
|
|
87
90
|
case "tool_execution_start":
|
|
91
|
+
this.toolsRun++;
|
|
88
92
|
return this.items.tool(
|
|
89
93
|
str(e.toolCallId),
|
|
90
94
|
piTool(str(e.toolName), e.args)
|
|
@@ -168,6 +172,18 @@ export class PiMapper {
|
|
|
168
172
|
};
|
|
169
173
|
}
|
|
170
174
|
|
|
175
|
+
// The run ran tools and ended without saying what they found.
|
|
176
|
+
needsSummary(): boolean {
|
|
177
|
+
return needsSummary({
|
|
178
|
+
toolsRun: this.toolsRun,
|
|
179
|
+
text: this.last.text,
|
|
180
|
+
toolCalls: this.last.calls,
|
|
181
|
+
finishReason: this.stopReason,
|
|
182
|
+
error: this.error || undefined,
|
|
183
|
+
aborted: this.aborted,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
171
187
|
// The run is over: Pi has nothing left to do for it.
|
|
172
188
|
endTurn(): void {
|
|
173
189
|
if (!this.items.inTurn) return;
|