@saadnvd1/agent-os 0.3.781 → 0.3.786
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 +6 -4
- package/app/api/machine/route.ts +5 -3
- package/app/api/whats-new/route.ts +47 -0
- package/components/Chat/ChatPanel.tsx +2 -0
- package/components/Chat/ModelPicker.tsx +82 -42
- package/components/Chat/composer/ComposerBody.tsx +3 -0
- package/components/Chat/composer/Toolbar.tsx +3 -0
- package/components/Draft/AgentModelChip.test.ts +1 -0
- package/components/Draft/AgentModelChip.tsx +36 -93
- package/components/Draft/Chip.tsx +27 -0
- package/components/Draft/DraftChips.tsx +5 -1
- package/components/Draft/DraftPanel.tsx +0 -21
- package/components/Draft/catalogBlocker.ts +19 -0
- package/components/Draft/picker/AgentModelPicker.tsx +152 -0
- package/components/Draft/picker/AgentRail.tsx +145 -0
- package/components/Draft/picker/ModelList.tsx +136 -0
- package/components/Draft/picker/ModelNote.tsx +15 -0
- package/components/Draft/picker/PickerSheet.tsx +61 -0
- package/components/Draft/picker/SameAsLast.tsx +52 -0
- package/components/Draft/picker/SearchField.tsx +48 -0
- package/components/Draft/picker/picker.test.ts +152 -0
- package/components/Draft/picker/sections.test.ts +115 -0
- package/components/Draft/picker/sections.ts +121 -0
- package/components/Draft/picker/useListKeys.ts +54 -0
- package/components/Draft/picker/useModelSearch.ts +66 -0
- package/components/SidebarFooter.tsx +3 -1
- package/components/Updates/UpdateBanner.test.ts +1 -1
- package/components/Updates/UpdateBanner.tsx +27 -13
- package/components/WhatsNew/WhatsNewButton.tsx +36 -0
- package/components/WhatsNew/WhatsNewDialog.tsx +169 -0
- package/components/WhatsNew/WhatsNewHost.tsx +17 -0
- package/components/WhatsNew/WhatsNewRows.tsx +62 -0
- package/components/WhatsNew/index.ts +2 -0
- package/components/ui/dialog.tsx +12 -1
- package/content/whats-new.json +1773 -0
- package/data/chat/useChat.ts +3 -0
- package/data/machines/index.ts +13 -4
- package/data/machines/keys.ts +2 -1
- package/data/whats-new/index.ts +44 -0
- package/hooks/useWhatsNewSeen.ts +50 -0
- package/lib/chat/drivers/copilot.ts +89 -11
- package/lib/chat/drivers/opencode.ts +1 -1
- package/lib/chat/drivers/pi.ts +1 -1
- package/lib/chat/events.ts +6 -0
- package/lib/chat/settings.ts +2 -0
- package/lib/machines/catalog.ts +5 -1
- package/lib/machines/recent.ts +31 -0
- package/lib/machines/remote.ts +28 -9
- package/lib/machines/report.ts +7 -2
- package/lib/machines/types.ts +12 -0
- package/lib/orchestrator/review-prompt.ts +2 -0
- package/lib/security/demo.ts +2 -0
- package/lib/tasks/brief.ts +1 -0
- package/lib/whats-new/ahead.ts +28 -0
- package/lib/whats-new/areas.ts +76 -0
- package/lib/whats-new/claude.ts +35 -0
- package/lib/whats-new/collect.ts +68 -0
- package/lib/whats-new/generate.ts +107 -0
- package/lib/whats-new/lint.ts +69 -0
- package/lib/whats-new/load.ts +59 -0
- package/lib/whats-new/rows.ts +79 -0
- package/lib/whats-new/schema.ts +41 -0
- package/lib/whats-new/summarize.ts +91 -0
- package/lib/whats-new/unread.ts +14 -0
- package/package.json +2 -1
- package/scripts/autodeploy +28 -0
- package/scripts/autodeploy.test.ts +53 -0
- package/scripts/whats-new.ts +118 -0
- package/stores/whatsNew.ts +16 -0
- package/components/Draft/draftModels.test.ts +0 -31
- package/components/Draft/draftModels.ts +0 -17
|
@@ -7,16 +7,18 @@ import type { HostCatalogResponse } from "@/lib/machines/types";
|
|
|
7
7
|
export const dynamic = "force-dynamic";
|
|
8
8
|
|
|
9
9
|
// The agents and models a machine reports, for the new-session form: this
|
|
10
|
-
// machine's own, or a linked machine's through its AgentOS.
|
|
10
|
+
// machine's own, or a linked machine's through its AgentOS. ?project= adds
|
|
11
|
+
// what that project last ran with.
|
|
11
12
|
export async function GET(
|
|
12
|
-
|
|
13
|
+
request: Request,
|
|
13
14
|
{ params }: { params: Promise<{ id: string }> }
|
|
14
15
|
) {
|
|
15
16
|
const { id } = await params;
|
|
16
17
|
if (!getHost(id))
|
|
17
18
|
return NextResponse.json({ error: "Unknown machine" }, { status: 404 });
|
|
19
|
+
const project = new URL(request.url).searchParams.get("project") || null;
|
|
18
20
|
const body: HostCatalogResponse = isRemoteHost(id)
|
|
19
|
-
? await hostCatalog(id)
|
|
20
|
-
: { catalog: await machineCatalog(), error: null };
|
|
21
|
+
? await hostCatalog(id, project)
|
|
22
|
+
: { catalog: await machineCatalog(project), error: null };
|
|
21
23
|
return NextResponse.json(body);
|
|
22
24
|
}
|
package/app/api/machine/route.ts
CHANGED
|
@@ -4,7 +4,9 @@ import { thisMachineReport } from "@/lib/machines/report";
|
|
|
4
4
|
export const dynamic = "force-dynamic";
|
|
5
5
|
|
|
6
6
|
// This machine's name, OS, addresses, agents and models. A linked machine
|
|
7
|
-
// asks for it (it is on the peer allowlist) to fill the new-session form
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
// asks for it (it is on the peer allowlist) to fill the new-session form;
|
|
8
|
+
// ?project= adds what that project last ran with.
|
|
9
|
+
export async function GET(request: Request) {
|
|
10
|
+
const project = new URL(request.url).searchParams.get("project");
|
|
11
|
+
return NextResponse.json(await thisMachineReport(project || null));
|
|
10
12
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { cachedCheck } from "@/lib/updates/check";
|
|
5
|
+
import { aheadEntries } from "@/lib/whats-new/ahead";
|
|
6
|
+
import { headOf, loadEntries, pageOf } from "@/lib/whats-new/load";
|
|
7
|
+
|
|
8
|
+
const query = z.object({
|
|
9
|
+
head: z.literal("1").optional(),
|
|
10
|
+
ahead: z.literal("1").optional(),
|
|
11
|
+
before: z
|
|
12
|
+
.string()
|
|
13
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
14
|
+
.optional(),
|
|
15
|
+
limit: z.coerce.number().int().min(1).max(200).default(60),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// GET /api/whats-new?head=1 - the newest id and the count, for the footer's
|
|
19
|
+
// dot. With ahead=1: what a waiting update would add. Otherwise a page of entries, newest first, older than `before`.
|
|
20
|
+
// Reads a static file; nothing here calls a model.
|
|
21
|
+
export async function GET(request: NextRequest) {
|
|
22
|
+
const parsed = query.safeParse(
|
|
23
|
+
Object.fromEntries(request.nextUrl.searchParams)
|
|
24
|
+
);
|
|
25
|
+
if (!parsed.success)
|
|
26
|
+
return NextResponse.json({ error: "Bad query" }, { status: 400 });
|
|
27
|
+
const { head, ahead, before, limit } = parsed.data;
|
|
28
|
+
const entries = loadEntries();
|
|
29
|
+
const check = ahead ? cachedCheck() : null;
|
|
30
|
+
const body = ahead
|
|
31
|
+
? {
|
|
32
|
+
entries: check?.available
|
|
33
|
+
? await aheadEntries(check.latest?.sha, entries)
|
|
34
|
+
: [],
|
|
35
|
+
}
|
|
36
|
+
: head
|
|
37
|
+
? headOf(entries)
|
|
38
|
+
: pageOf(entries, { before, limit });
|
|
39
|
+
const text = JSON.stringify(body);
|
|
40
|
+
const etag = `"${createHash("sha1").update(text).digest("hex").slice(0, 16)}"`;
|
|
41
|
+
const headers = { ETag: etag, "Cache-Control": "private, no-cache" };
|
|
42
|
+
if (request.headers.get("if-none-match") === etag)
|
|
43
|
+
return new NextResponse(null, { status: 304, headers });
|
|
44
|
+
return new NextResponse(text, {
|
|
45
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -291,6 +291,7 @@ export function ChatPanel({
|
|
|
291
291
|
models,
|
|
292
292
|
model,
|
|
293
293
|
modelLabel,
|
|
294
|
+
agentName,
|
|
294
295
|
send,
|
|
295
296
|
interrupt,
|
|
296
297
|
setModel,
|
|
@@ -881,6 +882,7 @@ export function ChatPanel({
|
|
|
881
882
|
models={models}
|
|
882
883
|
model={model}
|
|
883
884
|
modelLabel={modelLabel}
|
|
885
|
+
agentName={agentName}
|
|
884
886
|
onSetModel={setModel}
|
|
885
887
|
access={access}
|
|
886
888
|
onSetAccess={accessLocked ? undefined : setAccess}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
DropdownMenu,
|
|
6
|
-
DropdownMenuContent,
|
|
7
|
-
DropdownMenuItem,
|
|
8
|
-
DropdownMenuTrigger,
|
|
9
|
-
} from "@/components/ui/dropdown-menu";
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
import { ChevronDown, Lock } from "lucide-react";
|
|
10
5
|
import type { ChatModel } from "@/lib/chat/events";
|
|
6
|
+
import { ModelList } from "@/components/Draft/picker/ModelList";
|
|
7
|
+
import { PickerSheet } from "@/components/Draft/picker/PickerSheet";
|
|
8
|
+
import { SearchField } from "@/components/Draft/picker/SearchField";
|
|
9
|
+
import {
|
|
10
|
+
focusSelectedRow,
|
|
11
|
+
useListKeys,
|
|
12
|
+
} from "@/components/Draft/picker/useListKeys";
|
|
13
|
+
import { useModelSearch } from "@/components/Draft/picker/useModelSearch";
|
|
11
14
|
|
|
12
15
|
export function modelLabel(
|
|
13
16
|
models: ChatModel[],
|
|
@@ -17,11 +20,25 @@ export function modelLabel(
|
|
|
17
20
|
return models.find((m) => m.value === value)?.label ?? (serverLabel || value);
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The list value a chat's model is: itself, or the family alias when the chat
|
|
25
|
+
* holds a full id ("claude-opus-5-5" is the "opus" row).
|
|
26
|
+
*/
|
|
27
|
+
export function currentModelValue(
|
|
28
|
+
models: ChatModel[],
|
|
29
|
+
model: string
|
|
30
|
+
): string | undefined {
|
|
31
|
+
if (models.some((m) => m.value === model)) return model;
|
|
32
|
+
const family = /^claude-([a-z]+)-\d/.exec(model)?.[1];
|
|
33
|
+
return family ? models.find((m) => m.value === family)?.value : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
// The session's model, switchable mid-conversation. Opened by its button or /model.
|
|
21
37
|
export function ModelPicker({
|
|
22
38
|
models,
|
|
23
39
|
model,
|
|
24
40
|
label,
|
|
41
|
+
agentName,
|
|
25
42
|
open,
|
|
26
43
|
onOpenChange,
|
|
27
44
|
onPick,
|
|
@@ -29,47 +46,70 @@ export function ModelPicker({
|
|
|
29
46
|
models: ChatModel[];
|
|
30
47
|
model: string;
|
|
31
48
|
label?: string;
|
|
49
|
+
agentName?: string;
|
|
32
50
|
open: boolean;
|
|
33
51
|
onOpenChange: (open: boolean) => void;
|
|
34
52
|
onPick: (value: string) => void;
|
|
35
53
|
}) {
|
|
54
|
+
const current = currentModelValue(models, model);
|
|
55
|
+
const root = useRef<HTMLDivElement>(null);
|
|
56
|
+
const search = useRef<HTMLInputElement>(null);
|
|
57
|
+
const { query, setQuery, sections } = useModelSearch({
|
|
58
|
+
agent: { agent: "chat", label: "", models },
|
|
59
|
+
currentValue: current,
|
|
60
|
+
open,
|
|
61
|
+
});
|
|
62
|
+
const onKeyDown = useListKeys(root, search);
|
|
36
63
|
if (!model && models.length === 0) return null;
|
|
37
64
|
return (
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
</
|
|
51
|
-
<
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
<>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
aria-label="Model"
|
|
69
|
+
aria-haspopup="dialog"
|
|
70
|
+
onClick={() => onOpenChange(true)}
|
|
71
|
+
className="text-muted-foreground hover:text-foreground hover:bg-foreground/[0.05] flex h-10 max-w-36 min-w-0 items-center gap-1 rounded-lg px-2 text-xs"
|
|
72
|
+
>
|
|
73
|
+
<span className="truncate">
|
|
74
|
+
{modelLabel(models, model, label) || "Model"}
|
|
75
|
+
</span>
|
|
76
|
+
<ChevronDown className="h-3 w-3 shrink-0" />
|
|
77
|
+
</button>
|
|
78
|
+
<PickerSheet
|
|
79
|
+
open={open}
|
|
80
|
+
onOpenChange={(o) => {
|
|
81
|
+
onOpenChange(o);
|
|
82
|
+
}}
|
|
83
|
+
title="Model"
|
|
84
|
+
onKeyDown={onKeyDown}
|
|
85
|
+
onOpened={() => focusSelectedRow(root.current)}
|
|
86
|
+
>
|
|
87
|
+
<div ref={root} className="flex min-h-0 flex-1 flex-col">
|
|
88
|
+
<p className="text-muted-foreground bg-foreground/[0.03] mx-1.5 mt-1.5 flex shrink-0 items-center gap-2 rounded-lg px-3 py-2 text-xs">
|
|
89
|
+
<Lock className="h-3.5 w-3.5 shrink-0" />
|
|
90
|
+
{agentName && `${agentName} · `}
|
|
91
|
+
{agentName ? "switching" : "Switching"} agent starts a new session
|
|
92
|
+
</p>
|
|
93
|
+
<SearchField
|
|
94
|
+
value={query}
|
|
95
|
+
onChange={setQuery}
|
|
96
|
+
placeholder="Search models"
|
|
97
|
+
inputRef={search}
|
|
98
|
+
/>
|
|
99
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
|
100
|
+
<ModelList
|
|
101
|
+
sections={sections}
|
|
102
|
+
selected={current ?? null}
|
|
103
|
+
query={query}
|
|
104
|
+
agentLabel=""
|
|
105
|
+
onPick={(value) => {
|
|
106
|
+
onPick(value);
|
|
107
|
+
onOpenChange(false);
|
|
108
|
+
}}
|
|
61
109
|
/>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{m.disabled ?? m.description}
|
|
67
|
-
</span>
|
|
68
|
-
)}
|
|
69
|
-
</span>
|
|
70
|
-
</DropdownMenuItem>
|
|
71
|
-
))}
|
|
72
|
-
</DropdownMenuContent>
|
|
73
|
-
</DropdownMenu>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</PickerSheet>
|
|
113
|
+
</>
|
|
74
114
|
);
|
|
75
115
|
}
|
|
@@ -63,6 +63,7 @@ export interface ComposerProps {
|
|
|
63
63
|
models?: ChatModel[];
|
|
64
64
|
model?: string;
|
|
65
65
|
modelLabel?: string;
|
|
66
|
+
agentName?: string;
|
|
66
67
|
onSetModel?: (model: string) => void;
|
|
67
68
|
access?: ChatAccess;
|
|
68
69
|
onSetAccess?: (access: ChatAccess) => void;
|
|
@@ -101,6 +102,7 @@ export function ComposerBody({
|
|
|
101
102
|
models = [],
|
|
102
103
|
model = "",
|
|
103
104
|
modelLabel,
|
|
105
|
+
agentName,
|
|
104
106
|
onSetModel,
|
|
105
107
|
access,
|
|
106
108
|
onSetAccess,
|
|
@@ -383,6 +385,7 @@ export function ComposerBody({
|
|
|
383
385
|
models={models}
|
|
384
386
|
model={model}
|
|
385
387
|
modelLabel={modelLabel}
|
|
388
|
+
agentName={agentName}
|
|
386
389
|
onSetModel={onSetModel}
|
|
387
390
|
modelOpen={modelOpen}
|
|
388
391
|
onModelOpenChange={setModelOpen}
|
|
@@ -33,6 +33,7 @@ export function Toolbar({
|
|
|
33
33
|
models,
|
|
34
34
|
model,
|
|
35
35
|
modelLabel,
|
|
36
|
+
agentName,
|
|
36
37
|
onSetModel,
|
|
37
38
|
modelOpen,
|
|
38
39
|
onModelOpenChange,
|
|
@@ -56,6 +57,7 @@ export function Toolbar({
|
|
|
56
57
|
models: ChatModel[];
|
|
57
58
|
model: string;
|
|
58
59
|
modelLabel?: string;
|
|
60
|
+
agentName?: string;
|
|
59
61
|
onSetModel?: (model: string) => void;
|
|
60
62
|
modelOpen: boolean;
|
|
61
63
|
onModelOpenChange: (open: boolean) => void;
|
|
@@ -141,6 +143,7 @@ export function Toolbar({
|
|
|
141
143
|
models={models}
|
|
142
144
|
model={model}
|
|
143
145
|
label={modelLabel}
|
|
146
|
+
agentName={agentName}
|
|
144
147
|
open={modelOpen}
|
|
145
148
|
onOpenChange={onModelOpenChange}
|
|
146
149
|
onPick={onSetModel}
|
|
@@ -1,45 +1,33 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect } from "react";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
4
|
import { Bot } from "lucide-react";
|
|
5
5
|
import type { Draft } from "@/lib/drafts";
|
|
6
6
|
import { AGENT_OPTIONS, agentLabel } from "@/lib/agent-options";
|
|
7
7
|
import { resolveModelForAgent } from "@/lib/model-catalog";
|
|
8
8
|
import { useMachineCatalog } from "@/data/machines";
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
9
|
+
import { catalogBlocker, usableAgent } from "./catalogBlocker";
|
|
10
|
+
import { ChipItem, ChipMenu, ChipTrigger } from "./Chip";
|
|
11
|
+
import { AgentModelPicker } from "./picker/AgentModelPicker";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
export function catalogBlocker(a: CatalogAgent): string | null {
|
|
14
|
-
if (!a.installed) return "Not installed";
|
|
15
|
-
if (a.auth === "needs-login") return a.hint ?? "Needs sign-in";
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** The agent to use when the one chosen can't run on this machine. */
|
|
20
|
-
export function usableAgent(
|
|
21
|
-
catalog: MachineCatalog,
|
|
22
|
-
agentType: Draft["agentType"]
|
|
23
|
-
): CatalogAgent | null {
|
|
24
|
-
const have = catalog.agents.find((a) => a.agent === agentType);
|
|
25
|
-
if (have && !catalogBlocker(have)) return null;
|
|
26
|
-
return catalog.agents.find((a) => !catalogBlocker(a)) ?? null;
|
|
27
|
-
}
|
|
13
|
+
export { catalogBlocker, usableAgent };
|
|
28
14
|
|
|
29
15
|
/**
|
|
30
|
-
* The agent and its model in one
|
|
31
|
-
*
|
|
32
|
-
* agent that can run it.
|
|
16
|
+
* The agent and its model in one picker, from what the machine the session
|
|
17
|
+
* will run on reports: pick the agent, then only its models.
|
|
33
18
|
*/
|
|
34
19
|
export function AgentModelChip({
|
|
35
20
|
draft,
|
|
21
|
+
projectName,
|
|
36
22
|
onChange,
|
|
37
23
|
}: {
|
|
38
24
|
draft: Draft;
|
|
25
|
+
projectName: string | null;
|
|
39
26
|
onChange: (patch: Partial<Draft>) => void;
|
|
40
27
|
}) {
|
|
41
|
-
const { data, isPending } = useMachineCatalog(draft.hostId);
|
|
28
|
+
const { data, isPending } = useMachineCatalog(draft.hostId, draft.projectId);
|
|
42
29
|
const catalog = data?.catalog ?? null;
|
|
30
|
+
const [open, setOpen] = useState(false);
|
|
43
31
|
|
|
44
32
|
// The machine changed under the draft: move to an agent it has.
|
|
45
33
|
useEffect(() => {
|
|
@@ -59,18 +47,33 @@ export function AgentModelChip({
|
|
|
59
47
|
|
|
60
48
|
const current = catalog?.agents.find((a) => a.agent === draft.agentType);
|
|
61
49
|
const model = current?.models.find((m) => m.value === draft.model);
|
|
62
|
-
const label =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
: agentLabel(draft.agentType);
|
|
66
|
-
|
|
50
|
+
const label = model
|
|
51
|
+
? `${agentLabel(draft.agentType)} · ${model.label}`
|
|
52
|
+
: agentLabel(draft.agentType);
|
|
67
53
|
const blocker = current ? catalogBlocker(current) : null;
|
|
68
54
|
|
|
69
55
|
return (
|
|
70
56
|
<>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
57
|
+
{catalog ? (
|
|
58
|
+
<>
|
|
59
|
+
<ChipTrigger
|
|
60
|
+
icon={Bot}
|
|
61
|
+
name="Agent and model"
|
|
62
|
+
label={label}
|
|
63
|
+
onClick={() => setOpen(true)}
|
|
64
|
+
/>
|
|
65
|
+
<AgentModelPicker
|
|
66
|
+
open={open}
|
|
67
|
+
onOpenChange={setOpen}
|
|
68
|
+
catalog={catalog}
|
|
69
|
+
value={{ agent: draft.agentType, model: draft.model }}
|
|
70
|
+
projectName={projectName}
|
|
71
|
+
onPick={(agentType, m) => onChange({ agentType, model: m })}
|
|
72
|
+
/>
|
|
73
|
+
</>
|
|
74
|
+
) : (
|
|
75
|
+
<ChipMenu icon={Bot} name="Agent" label={label}>
|
|
76
|
+
{AGENT_OPTIONS.map((o) => (
|
|
74
77
|
<ChipItem
|
|
75
78
|
key={o.value}
|
|
76
79
|
checked={o.value === draft.agentType}
|
|
@@ -89,68 +92,8 @@ export function AgentModelChip({
|
|
|
89
92
|
{o.label}
|
|
90
93
|
</ChipItem>
|
|
91
94
|
))}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<div key={a.agent}>
|
|
96
|
-
<ChipHeading first={i === 0}>
|
|
97
|
-
{a.label}
|
|
98
|
-
{blocked ? ` · ${blocked}` : ""}
|
|
99
|
-
</ChipHeading>
|
|
100
|
-
{a.models.map((m) => (
|
|
101
|
-
<ChipItem
|
|
102
|
-
key={m.value}
|
|
103
|
-
checked={
|
|
104
|
-
a.agent === draft.agentType && m.value === draft.model
|
|
105
|
-
}
|
|
106
|
-
disabled={!!blocked}
|
|
107
|
-
hint={m.description}
|
|
108
|
-
onSelect={() =>
|
|
109
|
-
onChange({ agentType: a.agent, model: m.value })
|
|
110
|
-
}
|
|
111
|
-
>
|
|
112
|
-
{m.label}
|
|
113
|
-
</ChipItem>
|
|
114
|
-
))}
|
|
115
|
-
{a.modelsError && (
|
|
116
|
-
<p className="text-muted-foreground px-2 py-1 text-xs">
|
|
117
|
-
Couldn't list its models: {a.modelsError}
|
|
118
|
-
</p>
|
|
119
|
-
)}
|
|
120
|
-
</div>
|
|
121
|
-
);
|
|
122
|
-
})}
|
|
123
|
-
{catalog && catalog.ollama.length > 0 && (
|
|
124
|
-
<div>
|
|
125
|
-
<ChipHeading>Ollama (local models)</ChipHeading>
|
|
126
|
-
{catalog.ollama.map((o) => (
|
|
127
|
-
<ChipItem
|
|
128
|
-
key={o.name}
|
|
129
|
-
checked={
|
|
130
|
-
!!o.runsWith &&
|
|
131
|
-
o.runsWith.agent === draft.agentType &&
|
|
132
|
-
o.runsWith.model === draft.model
|
|
133
|
-
}
|
|
134
|
-
disabled={!o.runsWith}
|
|
135
|
-
hint={
|
|
136
|
-
o.runsWith
|
|
137
|
-
? `Runs in ${agentLabel(o.runsWith.agent)}`
|
|
138
|
-
: "No agent here is set up to run it"
|
|
139
|
-
}
|
|
140
|
-
onSelect={() =>
|
|
141
|
-
o.runsWith &&
|
|
142
|
-
onChange({
|
|
143
|
-
agentType: o.runsWith.agent,
|
|
144
|
-
model: o.runsWith.model,
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
>
|
|
148
|
-
{o.name}
|
|
149
|
-
</ChipItem>
|
|
150
|
-
))}
|
|
151
|
-
</div>
|
|
152
|
-
)}
|
|
153
|
-
</ChipMenu>
|
|
95
|
+
</ChipMenu>
|
|
96
|
+
)}
|
|
154
97
|
{blocker && (
|
|
155
98
|
<p className="w-full text-xs text-amber-600 dark:text-amber-400">
|
|
156
99
|
{blocker}
|
|
@@ -14,6 +14,33 @@ import { cn } from "@/lib/utils";
|
|
|
14
14
|
const chipClass =
|
|
15
15
|
"border-border/70 bg-card 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 disabled:pointer-events-none disabled:opacity-60";
|
|
16
16
|
|
|
17
|
+
// The button of a choice whose picker is its own dialog.
|
|
18
|
+
export function ChipTrigger({
|
|
19
|
+
icon: Icon,
|
|
20
|
+
label,
|
|
21
|
+
name,
|
|
22
|
+
onClick,
|
|
23
|
+
}: {
|
|
24
|
+
icon: LucideIcon;
|
|
25
|
+
label: string;
|
|
26
|
+
name: string;
|
|
27
|
+
onClick: () => void;
|
|
28
|
+
}) {
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
aria-label={`${name}: ${label}`}
|
|
33
|
+
aria-haspopup="dialog"
|
|
34
|
+
onClick={onClick}
|
|
35
|
+
className={chipClass}
|
|
36
|
+
>
|
|
37
|
+
<Icon className="h-3.5 w-3.5 shrink-0" />
|
|
38
|
+
<span className="truncate">{label}</span>
|
|
39
|
+
<ChevronDown className="h-3 w-3 shrink-0" />
|
|
40
|
+
</button>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
17
44
|
// One of a draft's choices: what it is now, and a menu to change it.
|
|
18
45
|
export function ChipMenu({
|
|
19
46
|
icon: Icon,
|
|
@@ -159,7 +159,11 @@ export function DraftChips({
|
|
|
159
159
|
)}
|
|
160
160
|
|
|
161
161
|
{!draft.openPr && !draft.agentRole && (
|
|
162
|
-
<AgentModelChip
|
|
162
|
+
<AgentModelChip
|
|
163
|
+
draft={draft}
|
|
164
|
+
projectName={project?.name ?? null}
|
|
165
|
+
onChange={onChange}
|
|
166
|
+
/>
|
|
163
167
|
)}
|
|
164
168
|
|
|
165
169
|
{!draft.openPr && local && (
|
|
@@ -8,8 +8,6 @@ import { Button } from "@/components/ui/button";
|
|
|
8
8
|
import type { ChatFile, ChatImage } from "@/lib/chat/events";
|
|
9
9
|
import type { Project } from "@/lib/db";
|
|
10
10
|
import { draftComposerKey, type Draft } from "@/lib/drafts";
|
|
11
|
-
import { discoversModels } from "@/lib/model-catalog";
|
|
12
|
-
import { useDiscoveredModels } from "@/data/agents";
|
|
13
11
|
import { useHostsQuery } from "@/data/hosts";
|
|
14
12
|
import { useGitCheck } from "@/data/git/queries";
|
|
15
13
|
import { useSelectedWorkspace } from "@/hooks/useSelectedWorkspace";
|
|
@@ -27,8 +25,6 @@ import { useStartAnalystBeside } from "@/components/Analyst/pair";
|
|
|
27
25
|
import { DraftChips } from "./DraftChips";
|
|
28
26
|
import { DraftHeading } from "./DraftHeading";
|
|
29
27
|
import { ModelUnavailableDialog } from "./ModelUnavailableDialog";
|
|
30
|
-
import { useMachineCatalog } from "@/data/machines";
|
|
31
|
-
import { draftModels } from "./draftModels";
|
|
32
28
|
|
|
33
29
|
// A new session before its first send: choices on top, a composer below.
|
|
34
30
|
// Sending makes the session (and its worktree) and turns this tab into it.
|
|
@@ -52,14 +48,6 @@ export function DraftPanel({
|
|
|
52
48
|
const { data: git } = useGitCheck(
|
|
53
49
|
project && draft?.hostId === "local" ? project.working_directory : ""
|
|
54
50
|
);
|
|
55
|
-
const discovers = !!draft && discoversModels(draft.agentType);
|
|
56
|
-
const found = useDiscoveredModels(
|
|
57
|
-
draft?.agentType ?? "",
|
|
58
|
-
draft?.projectId ?? null,
|
|
59
|
-
discovers
|
|
60
|
-
);
|
|
61
|
-
// On another machine the models are the ones it reports, not this one's.
|
|
62
|
-
const there = useMachineCatalog(draft?.hostId ?? "local");
|
|
63
51
|
const launch = useLaunchSession();
|
|
64
52
|
const startAnalyst = useStartAnalystBeside();
|
|
65
53
|
const { workspace } = useSelectedWorkspace();
|
|
@@ -262,15 +250,6 @@ export function DraftPanel({
|
|
|
262
250
|
}
|
|
263
251
|
onSend={(text, images, files) => void send(text, images, files)}
|
|
264
252
|
onStop={() => {}}
|
|
265
|
-
models={
|
|
266
|
-
(draft.hostId !== "local" &&
|
|
267
|
-
there.data?.catalog?.agents.find(
|
|
268
|
-
(a) => a.agent === draft.agentType
|
|
269
|
-
)?.models) ||
|
|
270
|
-
draftModels(draft.agentType, found)
|
|
271
|
-
}
|
|
272
|
-
model={draft.model}
|
|
273
|
-
onSetModel={(model) => draftsActions.update(draft.id, { model })}
|
|
274
253
|
access={draft.openPr ? undefined : draft.access}
|
|
275
254
|
onSetAccess={(access) => draftsActions.update(draft.id, { access })}
|
|
276
255
|
prefill={prefill}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Draft } from "@/lib/drafts";
|
|
2
|
+
import type { CatalogAgent, MachineCatalog } from "@/lib/machines/types";
|
|
3
|
+
|
|
4
|
+
// What stands between an agent and its first prompt, if anything.
|
|
5
|
+
export function catalogBlocker(a: CatalogAgent): string | null {
|
|
6
|
+
if (!a.installed) return "Not installed";
|
|
7
|
+
if (a.auth === "needs-login") return a.hint ?? "Needs sign-in";
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The agent to use when the one chosen can't run on this machine. */
|
|
12
|
+
export function usableAgent(
|
|
13
|
+
catalog: MachineCatalog,
|
|
14
|
+
agentType: Draft["agentType"]
|
|
15
|
+
): CatalogAgent | null {
|
|
16
|
+
const have = catalog.agents.find((a) => a.agent === agentType);
|
|
17
|
+
if (have && !catalogBlocker(have)) return null;
|
|
18
|
+
return catalog.agents.find((a) => !catalogBlocker(a)) ?? null;
|
|
19
|
+
}
|