@saadnvd1/agent-os 0.3.715 → 0.3.723
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 +1 -0
- package/app/api/agent-roles/route.test.ts +67 -0
- package/app/api/agent-roles/route.ts +61 -0
- package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
- package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
- package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
- package/app/api/analyst/[id]/review/route.ts +22 -0
- package/app/api/analyst/[id]/route.ts +44 -0
- package/app/api/analyst/route.ts +10 -0
- package/app/api/bus/spawn/route.ts +22 -1
- package/app/api/models/availability/route.ts +9 -0
- package/app/api/models/choices/route.ts +9 -0
- package/app/api/sessions/[id]/analyst/route.ts +11 -0
- package/app/api/sessions/[id]/crew/route.ts +38 -0
- package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
- package/app/api/sessions/[id]/export/route.test.ts +125 -0
- package/app/api/sessions/[id]/export/route.ts +34 -0
- package/app/api/sessions/[id]/model/route.ts +49 -0
- package/app/api/sessions/route.ts +16 -0
- package/app/page.tsx +5 -0
- package/components/Chat/Messages.tsx +104 -10
- package/components/Draft/DraftChips.tsx +6 -1
- package/components/Draft/DraftPanel.tsx +51 -2
- package/components/Draft/ModelUnavailableDialog.tsx +78 -0
- package/components/Draft/RoleChips.tsx +110 -0
- package/components/Export/ExportActions.tsx +136 -0
- package/components/Export/ExportSheet.tsx +217 -0
- package/components/Export/RangePicker.tsx +144 -0
- package/components/Export/Seg.tsx +38 -0
- package/components/Export/TurnSelectBar.tsx +45 -0
- package/components/Models/AgentRoleCard.tsx +114 -0
- package/components/Models/AgentRolesSection.tsx +132 -0
- package/components/Models/ModelBadges.tsx +48 -0
- package/components/Models/ModelsSection.tsx +4 -2
- package/components/Models/RoleModelSelect.tsx +87 -0
- package/components/Pane/DesktopTabBar.tsx +2 -0
- package/components/Pane/MobileTabBar.tsx +12 -0
- package/components/Pane/PaneMenu.tsx +10 -0
- package/components/SessionList/SessionRow.tsx +18 -7
- package/components/SessionList/SessionRowMenu.tsx +6 -0
- package/components/Sessions/RolePill.tsx +196 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Share/ShareDialog.tsx +13 -0
- package/data/analyst/index.ts +3 -0
- package/data/analyst/keys.ts +6 -0
- package/data/analyst/mutations.ts +116 -0
- package/data/analyst/queries.ts +35 -0
- package/data/exports/index.ts +2 -0
- package/data/exports/keys.ts +3 -0
- package/data/exports/queries.ts +51 -0
- package/data/models/index.ts +98 -0
- package/data/push/topics.ts +5 -0
- package/data/sessions/queries.ts +26 -0
- package/lib/agents/brief.ts +1 -0
- package/lib/analyst/__fixtures__/items.ts +57 -0
- package/lib/analyst/__fixtures__/mocks.ts +49 -0
- package/lib/analyst/brief.ts +17 -0
- package/lib/analyst/buildDigest.ts +204 -0
- package/lib/analyst/createLink.ts +98 -0
- package/lib/analyst/endLink.ts +31 -0
- package/lib/analyst/forward.ts +109 -0
- package/lib/analyst/index.ts +16 -0
- package/lib/analyst/linkView.ts +38 -0
- package/lib/analyst/onTurnEnd.ts +119 -0
- package/lib/analyst/parseReview.ts +41 -0
- package/lib/analyst/pushDigest.ts +143 -0
- package/lib/analyst/resume.ts +35 -0
- package/lib/analyst/route.ts +50 -0
- package/lib/analyst/signals.ts +121 -0
- package/lib/analyst/store.ts +303 -0
- package/lib/analyst/types.ts +140 -0
- package/lib/bus/condense.ts +5 -1
- package/lib/bus/index.ts +5 -0
- package/lib/bus/read-scope.ts +12 -0
- package/lib/bus/role-alias.ts +70 -0
- package/lib/bus/targets.ts +3 -0
- package/lib/chat/drivers/opencode-mapper.ts +55 -1
- package/lib/chat/drivers/pi-approvals.ts +7 -4
- package/lib/chat/drivers/pi-mapper.ts +38 -0
- package/lib/chat/drivers/pi-text-tools.ts +104 -0
- package/lib/chat/drivers/pi.ts +8 -3
- package/lib/chat/drivers/turn-items.ts +36 -11
- package/lib/chat/drivers/turn-outcome.ts +108 -0
- package/lib/chat/events.ts +8 -0
- package/lib/chat/registry.ts +13 -0
- package/lib/chat/store.ts +19 -0
- package/lib/chat/turns.ts +1 -0
- package/lib/chat/worker/extras.ts +4 -2
- package/lib/claims/collectLocal.ts +3 -1
- package/lib/db/changes.ts +6 -0
- package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
- package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
- package/lib/db/session-parts/columns.ts +2 -0
- package/lib/db/types.ts +3 -0
- package/lib/done/archive.ts +2 -0
- package/lib/drafts.ts +4 -0
- package/lib/export/build.ts +470 -0
- package/lib/export/compact.ts +156 -0
- package/lib/export/estimate.ts +83 -0
- package/lib/export/fixtures.ts +45 -0
- package/lib/export/html.tsx +195 -0
- package/lib/export/json.ts +39 -0
- package/lib/export/load.ts +64 -0
- package/lib/export/markdown.ts +127 -0
- package/lib/export/request.ts +36 -0
- package/lib/export/service.ts +142 -0
- package/lib/export/summary.ts +86 -0
- package/lib/export/turns.ts +63 -0
- package/lib/export/view.ts +173 -0
- package/lib/models/agent-binding.ts +102 -0
- package/lib/models/agent-roles-view.ts +157 -0
- package/lib/models/agent-roles.ts +87 -0
- package/lib/models/availability-refs.ts +17 -0
- package/lib/models/availability.ts +91 -0
- package/lib/models/catalog.ts +156 -0
- package/lib/models/config.ts +27 -0
- package/lib/models/locality.ts +33 -0
- package/lib/models/providers.ts +11 -0
- package/lib/models/resolve.ts +50 -12
- package/lib/models/roles.ts +12 -0
- package/lib/needs-you.ts +1 -1
- package/lib/orchestrator/facts.ts +7 -2
- package/lib/orchestrator/read.ts +5 -1
- package/lib/orchestrator/untrusted.ts +5 -0
- package/lib/projects.ts +1 -0
- package/lib/session-artifacts/agentos-files.ts +94 -0
- package/lib/session-artifacts/collect.ts +17 -0
- package/lib/sessions/crew.ts +72 -0
- package/lib/sessions/launch.ts +49 -9
- package/lib/settings-url.ts +1 -0
- package/lib/share/markdown.ts +2 -2
- package/lib/share/redact.ts +16 -10
- package/lib/share/render.tsx +1 -1
- package/lib/share/snapshot.ts +2 -2
- package/lib/sidebar/row-menu.ts +6 -1
- package/lib/tasks/index.ts +6 -0
- package/lib/worktree-deps/caches.ts +3 -2
- package/lib/worktree-deps/clone.ts +3 -1
- package/lib/worktree-deps/paths.ts +26 -0
- package/package.json +1 -1
- package/server.ts +9 -1
- package/stores/exportUi.ts +87 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Availability } from "@/lib/models/availability";
|
|
2
|
+
import type { Locality } from "@/lib/models/locality";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
// Whether a model runs on this machine's own networks or in the cloud.
|
|
6
|
+
export function LocalityBadge({ locality }: { locality: Locality }) {
|
|
7
|
+
return (
|
|
8
|
+
<span
|
|
9
|
+
className={cn(
|
|
10
|
+
"inline-flex h-5 shrink-0 items-center rounded-full px-2 text-[11px] font-medium",
|
|
11
|
+
locality === "local"
|
|
12
|
+
? "bg-emerald-500/15 text-emerald-700 dark:text-emerald-400"
|
|
13
|
+
: "bg-muted text-muted-foreground ring-border ring-1 ring-inset"
|
|
14
|
+
)}
|
|
15
|
+
>
|
|
16
|
+
{locality === "local" ? "Local" : "Cloud"}
|
|
17
|
+
</span>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DOT = {
|
|
22
|
+
up: "bg-emerald-500",
|
|
23
|
+
down: "bg-destructive",
|
|
24
|
+
unknown: "bg-muted-foreground/50",
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
const TITLE = {
|
|
28
|
+
up: "Answering",
|
|
29
|
+
down: "Not answering",
|
|
30
|
+
unknown: "Not checked",
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
// The last availability check: green answering, red not, grey not checked.
|
|
34
|
+
export function AvailabilityDot({
|
|
35
|
+
availability,
|
|
36
|
+
}: {
|
|
37
|
+
availability: Pick<Availability, "state"> | undefined;
|
|
38
|
+
}) {
|
|
39
|
+
const state = availability?.state ?? "unknown";
|
|
40
|
+
return (
|
|
41
|
+
<span
|
|
42
|
+
role="img"
|
|
43
|
+
aria-label={TITLE[state]}
|
|
44
|
+
title={TITLE[state]}
|
|
45
|
+
className={cn("inline-block h-2 w-2 shrink-0 rounded-full", DOT[state])}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type ModelProvider,
|
|
12
12
|
} from "@/lib/models/providers";
|
|
13
13
|
import { SubagentModels } from "./SubagentModels";
|
|
14
|
+
import { INTERNAL_MODEL_ROLES } from "@/lib/models/agent-roles";
|
|
14
15
|
import { MODEL_ROLES, ROLE_LABELS } from "@/lib/models/roles";
|
|
15
16
|
import { ModelSelect } from "./ModelSelect";
|
|
16
17
|
import { ProviderCard } from "./ProviderCard";
|
|
@@ -58,11 +59,12 @@ export function ModelsSection() {
|
|
|
58
59
|
<div className="space-y-6">
|
|
59
60
|
<section className="space-y-2">
|
|
60
61
|
<p className="text-muted-foreground text-sm">
|
|
61
|
-
Which model each kind of work runs on.
|
|
62
|
+
Which model each kind of work runs on. Developer, Analyst, Reviewer
|
|
63
|
+
and Supervisor are in Agent roles. A workspace can set its own
|
|
62
64
|
(Settings › Workspaces), and a task its own when it starts or resumes.
|
|
63
65
|
</p>
|
|
64
66
|
<ul className="space-y-2">
|
|
65
|
-
{MODEL_ROLES.map((role) => (
|
|
67
|
+
{INTERNAL_MODEL_ROLES(MODEL_ROLES).map((role) => (
|
|
66
68
|
<li
|
|
67
69
|
key={role}
|
|
68
70
|
className="bg-muted/40 space-y-1.5 rounded-lg px-3 py-2.5"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectGroup,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectLabel,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from "@/components/ui/select";
|
|
12
|
+
import { useModelChoices } from "@/data/models";
|
|
13
|
+
import type { ModelChoice } from "@/lib/models/catalog";
|
|
14
|
+
import { AvailabilityDot, LocalityBadge } from "./ModelBadges";
|
|
15
|
+
|
|
16
|
+
const INHERIT = "__inherit";
|
|
17
|
+
|
|
18
|
+
function Row({ choice }: { choice: ModelChoice }) {
|
|
19
|
+
return (
|
|
20
|
+
<span className="flex min-w-0 flex-1 items-center gap-2">
|
|
21
|
+
<AvailabilityDot availability={choice.availability} />
|
|
22
|
+
<span className="truncate">{choice.label}</span>
|
|
23
|
+
<LocalityBadge locality={choice.locality} />
|
|
24
|
+
</span>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A model picker over the server's list: grouped by provider, each model
|
|
29
|
+
// labelled Local or Cloud with whether its server answers. `claudeOnly`
|
|
30
|
+
// leaves out the models that run through Pi.
|
|
31
|
+
export function RoleModelSelect({
|
|
32
|
+
id,
|
|
33
|
+
value,
|
|
34
|
+
inheritLabel,
|
|
35
|
+
claudeOnly,
|
|
36
|
+
gateOnly,
|
|
37
|
+
disabled,
|
|
38
|
+
onChange,
|
|
39
|
+
}: {
|
|
40
|
+
id?: string;
|
|
41
|
+
value: string | null;
|
|
42
|
+
inheritLabel: string;
|
|
43
|
+
claudeOnly?: boolean;
|
|
44
|
+
// Only models that can serve a merge gate are selectable.
|
|
45
|
+
gateOnly?: boolean;
|
|
46
|
+
disabled?: boolean;
|
|
47
|
+
onChange: (ref: string | null) => void;
|
|
48
|
+
}) {
|
|
49
|
+
const { data } = useModelChoices();
|
|
50
|
+
const choices = (data ?? []).filter((c) => !claudeOnly || c.agent !== "pi");
|
|
51
|
+
const groups = [...new Set(choices.map((c) => c.group))];
|
|
52
|
+
const current = value ? choices.find((c) => c.ref === value) : undefined;
|
|
53
|
+
return (
|
|
54
|
+
<Select
|
|
55
|
+
value={value ?? INHERIT}
|
|
56
|
+
onValueChange={(v) => onChange(v === INHERIT ? null : v)}
|
|
57
|
+
disabled={disabled}
|
|
58
|
+
>
|
|
59
|
+
<SelectTrigger id={id} className="h-11 md:h-9">
|
|
60
|
+
<SelectValue>
|
|
61
|
+
{current ? <Row choice={current} /> : (value ?? inheritLabel)}
|
|
62
|
+
</SelectValue>
|
|
63
|
+
</SelectTrigger>
|
|
64
|
+
<SelectContent>
|
|
65
|
+
<SelectItem value={INHERIT}>{inheritLabel}</SelectItem>
|
|
66
|
+
{value && !current && <SelectItem value={value}>{value}</SelectItem>}
|
|
67
|
+
{groups.map((g) => (
|
|
68
|
+
<SelectGroup key={g}>
|
|
69
|
+
<SelectLabel>{g}</SelectLabel>
|
|
70
|
+
{choices
|
|
71
|
+
.filter((c) => c.group === g)
|
|
72
|
+
.map((c) => (
|
|
73
|
+
<SelectItem
|
|
74
|
+
key={c.ref}
|
|
75
|
+
value={c.ref}
|
|
76
|
+
disabled={gateOnly && !c.gateCapable}
|
|
77
|
+
className="min-h-11 md:min-h-0"
|
|
78
|
+
>
|
|
79
|
+
<Row choice={c} />
|
|
80
|
+
</SelectItem>
|
|
81
|
+
))}
|
|
82
|
+
</SelectGroup>
|
|
83
|
+
))}
|
|
84
|
+
</SelectContent>
|
|
85
|
+
</Select>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -15,6 +15,7 @@ import type { Session } from "@/lib/db";
|
|
|
15
15
|
import { PaneViewToggle } from "./PaneViewToggle";
|
|
16
16
|
import { PaneMenu } from "./PaneMenu";
|
|
17
17
|
import { HostBadge } from "@/components/Hosts/HostBadge";
|
|
18
|
+
import { RolePill } from "@/components/Sessions/RolePill";
|
|
18
19
|
import { usePaneBarSlots } from "./PaneBarSlots";
|
|
19
20
|
import { ChatBookmarksButton } from "@/components/Bookmarks/ChatBookmarksButton";
|
|
20
21
|
import type { ViewMode } from "@/contexts/PaneContext";
|
|
@@ -161,6 +162,7 @@ export function DesktopTabBar({
|
|
|
161
162
|
</div>
|
|
162
163
|
|
|
163
164
|
<div className="flex shrink-0 items-center gap-1.5">
|
|
165
|
+
<RolePill session={session} paneId={paneId} />
|
|
164
166
|
<HostBadge hostId={session?.host_id} />
|
|
165
167
|
<ContextMeter sessionId={session?.id} />
|
|
166
168
|
<SessionPrStage sessionId={session?.id} />
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
Search,
|
|
25
25
|
Star,
|
|
26
26
|
Share2,
|
|
27
|
+
Download,
|
|
27
28
|
TextSearch,
|
|
28
29
|
} from "lucide-react";
|
|
29
30
|
import { cn } from "@/lib/utils";
|
|
@@ -31,8 +32,10 @@ import { useAwayQuery } from "@/data/away";
|
|
|
31
32
|
import { awayUntil } from "@/components/Away/time";
|
|
32
33
|
import type { Session, Project } from "@/lib/db";
|
|
33
34
|
import { HostBadge } from "@/components/Hosts/HostBadge";
|
|
35
|
+
import { RolePill } from "@/components/Sessions/RolePill";
|
|
34
36
|
import { bookmarksUiActions } from "@/stores/bookmarksUi";
|
|
35
37
|
import { shareUiActions } from "@/stores/shareUi";
|
|
38
|
+
import { exportUiActions } from "@/stores/exportUi";
|
|
36
39
|
import { MoveMenuItems } from "@/components/Tasks/MoveMenuItems";
|
|
37
40
|
import { ChatBookmarksButton } from "@/components/Bookmarks/ChatBookmarksButton";
|
|
38
41
|
import type { ViewMode } from "@/contexts/PaneContext";
|
|
@@ -219,6 +222,14 @@ export function MobileTabBar({
|
|
|
219
222
|
<Share2 className="h-4 w-4" />
|
|
220
223
|
Share…
|
|
221
224
|
</DropdownMenuItem>
|
|
225
|
+
<DropdownMenuItem
|
|
226
|
+
onSelect={() =>
|
|
227
|
+
exportUiActions.open(session.id, session.name)
|
|
228
|
+
}
|
|
229
|
+
>
|
|
230
|
+
<Download className="h-4 w-4" />
|
|
231
|
+
Export for AI…
|
|
232
|
+
</DropdownMenuItem>
|
|
222
233
|
<DropdownMenuSeparator />
|
|
223
234
|
</>
|
|
224
235
|
)}
|
|
@@ -270,6 +281,7 @@ export function MobileTabBar({
|
|
|
270
281
|
</button>
|
|
271
282
|
</div>
|
|
272
283
|
|
|
284
|
+
<RolePill session={session} compact />
|
|
273
285
|
{bookmarksSessionId && (
|
|
274
286
|
<ChatBookmarksButton
|
|
275
287
|
sessionId={bookmarksSessionId}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Link,
|
|
9
9
|
MoreHorizontal,
|
|
10
10
|
Share2,
|
|
11
|
+
Download,
|
|
11
12
|
SplitSquareHorizontal,
|
|
12
13
|
SplitSquareVertical,
|
|
13
14
|
Star,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
import { copySessionLink, writeClipboard } from "@/hooks/useCopyToClipboard";
|
|
28
29
|
import type { Session } from "@/lib/db";
|
|
29
30
|
import { shareUiActions } from "@/stores/shareUi";
|
|
31
|
+
import { exportUiActions } from "@/stores/exportUi";
|
|
30
32
|
import { useSessionBookmarks } from "@/data/bookmarks";
|
|
31
33
|
import { useDocsWorkspace } from "@/data/lumifyhub/docs";
|
|
32
34
|
import { bookmarksUiActions } from "@/stores/bookmarksUi";
|
|
@@ -117,6 +119,14 @@ export function PaneMenu({
|
|
|
117
119
|
Share…
|
|
118
120
|
</DropdownMenuItem>
|
|
119
121
|
)}
|
|
122
|
+
{session.view === "chat" && (
|
|
123
|
+
<DropdownMenuItem
|
|
124
|
+
onSelect={() => exportUiActions.open(session.id, session.name)}
|
|
125
|
+
>
|
|
126
|
+
<Download className="h-4 w-4" />
|
|
127
|
+
Export for AI…
|
|
128
|
+
</DropdownMenuItem>
|
|
129
|
+
)}
|
|
120
130
|
<DropdownMenuSeparator />
|
|
121
131
|
<MoveMenuItems
|
|
122
132
|
session={session}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { AGENT_ROLE_DEFS, isAgentRole } from "@/lib/models/agent-roles";
|
|
3
4
|
import { isFork } from "@/lib/sessions/fork";
|
|
4
5
|
import { memo, useState } from "react";
|
|
5
6
|
import { useSnapshot } from "valtio";
|
|
@@ -126,14 +127,24 @@ export const SessionRow = memo(function SessionRow({
|
|
|
126
127
|
}}
|
|
127
128
|
/>
|
|
128
129
|
) : (
|
|
129
|
-
<span
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
<span className="flex min-w-0 items-center gap-1.5">
|
|
131
|
+
{isAgentRole(session.agent_role) && (
|
|
132
|
+
<span
|
|
133
|
+
title={AGENT_ROLE_DEFS[session.agent_role].label}
|
|
134
|
+
className="bg-accent text-primary grid h-4 w-4 shrink-0 place-items-center rounded text-[10px] font-semibold"
|
|
135
|
+
>
|
|
136
|
+
{AGENT_ROLE_DEFS[session.agent_role].label[0]}
|
|
137
|
+
</span>
|
|
134
138
|
)}
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
<span
|
|
140
|
+
className={cn(
|
|
141
|
+
"truncate text-sm",
|
|
142
|
+
muted && !active ? "text-muted-foreground" : "text-foreground",
|
|
143
|
+
active && "font-medium"
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
{session.name}
|
|
147
|
+
</span>
|
|
137
148
|
</span>
|
|
138
149
|
)}
|
|
139
150
|
<RowSubtitle
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
KanbanSquare,
|
|
16
16
|
Link,
|
|
17
17
|
Share2,
|
|
18
|
+
Download,
|
|
18
19
|
Loader2,
|
|
19
20
|
Pencil,
|
|
20
21
|
Pin,
|
|
@@ -49,6 +50,7 @@ import type { SidebarRow } from "@/lib/sidebar/shelves";
|
|
|
49
50
|
import { useRowContext } from "./RowContext";
|
|
50
51
|
import { MoveMenuItems } from "@/components/Tasks/MoveMenuItems";
|
|
51
52
|
import { shareUiActions } from "@/stores/shareUi";
|
|
53
|
+
import { exportUiActions } from "@/stores/exportUi";
|
|
52
54
|
import { copySessionLink } from "@/hooks/useCopyToClipboard";
|
|
53
55
|
import { useLinkedHostNames } from "@/data/hosts";
|
|
54
56
|
import { remoteBlocks, remoteNote } from "@/lib/hosts/remote-menu";
|
|
@@ -196,6 +198,10 @@ export function SessionRowMenu({
|
|
|
196
198
|
title: session.name,
|
|
197
199
|
})
|
|
198
200
|
);
|
|
201
|
+
case "export":
|
|
202
|
+
return item("Export for AI…", Download, () =>
|
|
203
|
+
exportUiActions.open(session.id, session.name)
|
|
204
|
+
);
|
|
199
205
|
case "fork":
|
|
200
206
|
return item("Fork session", Copy, () => ctx.onFork(session.id));
|
|
201
207
|
case "fresh-start":
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from "@/components/ui/dialog";
|
|
13
|
+
import {
|
|
14
|
+
AvailabilityDot,
|
|
15
|
+
LocalityBadge,
|
|
16
|
+
} from "@/components/Models/ModelBadges";
|
|
17
|
+
import { RoleModelSelect } from "@/components/Models/RoleModelSelect";
|
|
18
|
+
import { useCrewQuery, useSwitchSessionModel } from "@/data/models";
|
|
19
|
+
import { useLaunchSession } from "@/data/sessions";
|
|
20
|
+
import { usePanes } from "@/contexts/PaneContext";
|
|
21
|
+
import type { Session } from "@/lib/db";
|
|
22
|
+
import {
|
|
23
|
+
AGENT_ROLES,
|
|
24
|
+
AGENT_ROLE_DEFS,
|
|
25
|
+
isAgentRole,
|
|
26
|
+
} from "@/lib/models/agent-roles";
|
|
27
|
+
import { cn } from "@/lib/utils";
|
|
28
|
+
import { uuid } from "@/lib/uuid";
|
|
29
|
+
|
|
30
|
+
const modelName = (ref: string | null) =>
|
|
31
|
+
ref ? ref.split("/").slice(1).join("/") : "model";
|
|
32
|
+
|
|
33
|
+
// The role a running session was started in and the model it runs on, with
|
|
34
|
+
// its crew: the sessions it works with, by @name. Nothing for a session
|
|
35
|
+
// started without a role. `compact` is a phone's small button.
|
|
36
|
+
export function RolePill({
|
|
37
|
+
session,
|
|
38
|
+
paneId,
|
|
39
|
+
compact,
|
|
40
|
+
}: {
|
|
41
|
+
session: Session | null | undefined;
|
|
42
|
+
paneId?: string;
|
|
43
|
+
compact?: boolean;
|
|
44
|
+
}) {
|
|
45
|
+
const [open, setOpen] = useState(false);
|
|
46
|
+
const role = isAgentRole(session?.agent_role) ? session.agent_role : null;
|
|
47
|
+
const crew = useCrewQuery(session?.id, !!role);
|
|
48
|
+
const switchModel = useSwitchSessionModel(session?.id ?? "");
|
|
49
|
+
const launch = useLaunchSession();
|
|
50
|
+
const { attachSession } = usePanes();
|
|
51
|
+
if (!session || !role) return null;
|
|
52
|
+
const me = crew.data?.members.find((m) => m.id === session.id);
|
|
53
|
+
const down = me?.availability === "down";
|
|
54
|
+
const label = AGENT_ROLE_DEFS[role].label;
|
|
55
|
+
const others = crew.data?.members.filter((m) => m.id !== session.id) ?? [];
|
|
56
|
+
|
|
57
|
+
const add = async (next: (typeof AGENT_ROLES)[number]) => {
|
|
58
|
+
try {
|
|
59
|
+
const { session: made } = await launch.mutateAsync({
|
|
60
|
+
id: uuid(),
|
|
61
|
+
projectId: session.project_id,
|
|
62
|
+
agentType: "claude",
|
|
63
|
+
model: "",
|
|
64
|
+
access: "full",
|
|
65
|
+
useWorktree: false,
|
|
66
|
+
baseBranch: null,
|
|
67
|
+
prompt: "",
|
|
68
|
+
agentRole: next,
|
|
69
|
+
crewId: session.crew_id ?? session.id,
|
|
70
|
+
});
|
|
71
|
+
if (paneId) attachSession(paneId, made.id, made.tmux_name ?? "");
|
|
72
|
+
setOpen(false);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
toast.error(error instanceof Error ? error.message : String(error));
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={(e) => {
|
|
83
|
+
e.stopPropagation();
|
|
84
|
+
setOpen(true);
|
|
85
|
+
}}
|
|
86
|
+
aria-label={`${label}, ${modelName(me?.ref ?? null)}${down ? ", model not answering" : ""}`}
|
|
87
|
+
className={cn(
|
|
88
|
+
"ring-border/70 flex shrink-0 items-center gap-1.5 rounded-full text-xs ring-1 ring-inset",
|
|
89
|
+
down
|
|
90
|
+
? "bg-amber-500/10 text-amber-600 dark:text-amber-400"
|
|
91
|
+
: "bg-muted/60",
|
|
92
|
+
compact
|
|
93
|
+
? "-my-1.5 h-11 w-11 justify-center rounded-lg ring-0"
|
|
94
|
+
: "h-7 max-w-64 px-2.5"
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
<span className="bg-accent text-primary grid h-4 w-4 shrink-0 place-items-center rounded text-[10px] font-semibold">
|
|
98
|
+
{label[0]}
|
|
99
|
+
</span>
|
|
100
|
+
{!compact && (
|
|
101
|
+
<>
|
|
102
|
+
<span className="truncate">
|
|
103
|
+
{label}
|
|
104
|
+
{me?.ref ? ` · ${modelName(me.ref)}` : ""}
|
|
105
|
+
</span>
|
|
106
|
+
{me && !down && <LocalityBadge locality={me.locality} />}
|
|
107
|
+
{down && <span className="shrink-0">unreachable</span>}
|
|
108
|
+
</>
|
|
109
|
+
)}
|
|
110
|
+
{compact && down && (
|
|
111
|
+
<span className="bg-destructive absolute h-2 w-2 translate-x-3 -translate-y-3 rounded-full" />
|
|
112
|
+
)}
|
|
113
|
+
</button>
|
|
114
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
115
|
+
<DialogContent>
|
|
116
|
+
<DialogHeader>
|
|
117
|
+
<DialogTitle>{label} · role and crew</DialogTitle>
|
|
118
|
+
<DialogDescription>
|
|
119
|
+
{AGENT_ROLE_DEFS[role].purpose}
|
|
120
|
+
</DialogDescription>
|
|
121
|
+
</DialogHeader>
|
|
122
|
+
<div className="bg-muted/40 space-y-2 rounded-xl p-3">
|
|
123
|
+
<div className="flex items-center justify-between gap-2">
|
|
124
|
+
<span className="flex min-w-0 items-center gap-2 text-sm font-medium">
|
|
125
|
+
<AvailabilityDot
|
|
126
|
+
availability={{ state: me?.availability ?? "unknown" }}
|
|
127
|
+
/>
|
|
128
|
+
<span className="truncate">{modelName(me?.ref ?? null)}</span>
|
|
129
|
+
</span>
|
|
130
|
+
{me && <LocalityBadge locality={me.locality} />}
|
|
131
|
+
</div>
|
|
132
|
+
{down && (
|
|
133
|
+
<p role="alert" className="text-destructive text-xs">
|
|
134
|
+
This model's server isn't answering. Your conversation
|
|
135
|
+
is kept: switch to another model to carry on.
|
|
136
|
+
</p>
|
|
137
|
+
)}
|
|
138
|
+
<RoleModelSelect
|
|
139
|
+
value={me?.ref ?? null}
|
|
140
|
+
inheritLabel="Switch model…"
|
|
141
|
+
disabled={switchModel.isPending}
|
|
142
|
+
onChange={(ref) =>
|
|
143
|
+
ref &&
|
|
144
|
+
switchModel.mutate(ref, {
|
|
145
|
+
onError: (e) => toast.error(e.message),
|
|
146
|
+
onSuccess: () => toast.success("Switched model"),
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="space-y-1.5">
|
|
152
|
+
<h3 className="text-muted-foreground text-xs font-medium uppercase">
|
|
153
|
+
Crew
|
|
154
|
+
</h3>
|
|
155
|
+
{others.length === 0 && (
|
|
156
|
+
<p className="text-muted-foreground text-sm">
|
|
157
|
+
Nobody else yet. Add a role to work with this session.
|
|
158
|
+
</p>
|
|
159
|
+
)}
|
|
160
|
+
{others.map((m) => (
|
|
161
|
+
<div key={m.id} className="flex items-center gap-2 text-sm">
|
|
162
|
+
<span className="min-w-0 flex-1">
|
|
163
|
+
<span className="block truncate">{m.name}</span>
|
|
164
|
+
<span className="text-muted-foreground block truncate font-mono text-xs">
|
|
165
|
+
{m.agentRole ? `@${m.agentRole}` : "no role"} ·{" "}
|
|
166
|
+
{modelName(m.ref)}
|
|
167
|
+
</span>
|
|
168
|
+
</span>
|
|
169
|
+
<AvailabilityDot availability={{ state: m.availability }} />
|
|
170
|
+
<LocalityBadge locality={m.locality} />
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
<div className="flex flex-wrap gap-2">
|
|
175
|
+
{AGENT_ROLES.filter(
|
|
176
|
+
(r) =>
|
|
177
|
+
r !== role &&
|
|
178
|
+
r !== "supervisor" &&
|
|
179
|
+
!others.some((m) => m.agentRole === r)
|
|
180
|
+
).map((r) => (
|
|
181
|
+
<Button
|
|
182
|
+
key={r}
|
|
183
|
+
variant="outline"
|
|
184
|
+
className="h-11 md:h-9"
|
|
185
|
+
disabled={launch.isPending}
|
|
186
|
+
onClick={() => void add(r)}
|
|
187
|
+
>
|
|
188
|
+
Add {AGENT_ROLE_DEFS[r].label}
|
|
189
|
+
</Button>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
</DialogContent>
|
|
193
|
+
</Dialog>
|
|
194
|
+
</>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
FlaskConical,
|
|
20
20
|
RefreshCw,
|
|
21
21
|
Smartphone,
|
|
22
|
+
Users,
|
|
22
23
|
X,
|
|
23
24
|
type LucideIcon,
|
|
24
25
|
} from "lucide-react";
|
|
@@ -34,6 +35,7 @@ import { AuditsPanel } from "@/components/Audits/AuditsPanel";
|
|
|
34
35
|
import { HealthPanel } from "@/components/Health/HealthPanel";
|
|
35
36
|
import { DevicesPanel } from "@/components/Devices";
|
|
36
37
|
import { GlobalMergeSection } from "@/components/Merge/GlobalMergeSection";
|
|
38
|
+
import { AgentRolesSection } from "@/components/Models/AgentRolesSection";
|
|
37
39
|
import { ModelsSection } from "@/components/Models/ModelsSection";
|
|
38
40
|
import { SharedLinksPanel } from "@/components/Shares/SharedLinksPanel";
|
|
39
41
|
import { SchedulesPanel } from "@/components/Schedules";
|
|
@@ -87,6 +89,11 @@ export const SECTION_INFO: Record<
|
|
|
87
89
|
hint: "Which model each kind of work runs on, and providers",
|
|
88
90
|
icon: Cpu,
|
|
89
91
|
},
|
|
92
|
+
"agent-roles": {
|
|
93
|
+
label: "Agent roles",
|
|
94
|
+
hint: "Developer, Analyst, Reviewer and Supervisor: a model each, local or cloud",
|
|
95
|
+
icon: Users,
|
|
96
|
+
},
|
|
90
97
|
notifications: {
|
|
91
98
|
label: "Notifications",
|
|
92
99
|
hint: "Sound, browser alerts, your phone",
|
|
@@ -145,6 +152,8 @@ function SectionBody({
|
|
|
145
152
|
return <WorkspaceSettings />;
|
|
146
153
|
case "models":
|
|
147
154
|
return <ModelsSection />;
|
|
155
|
+
case "agent-roles":
|
|
156
|
+
return <AgentRolesSection />;
|
|
148
157
|
case "devices":
|
|
149
158
|
return <DevicesPanel />;
|
|
150
159
|
case "shares":
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type SharePreview,
|
|
29
29
|
} from "@/data/shares";
|
|
30
30
|
import { shareUi, shareUiActions } from "@/stores/shareUi";
|
|
31
|
+
import { exportUiActions } from "@/stores/exportUi";
|
|
31
32
|
import {
|
|
32
33
|
DEFAULT_EXPIRY,
|
|
33
34
|
EXPIRIES,
|
|
@@ -271,6 +272,18 @@ function ShareBody({
|
|
|
271
272
|
{error && preview && (
|
|
272
273
|
<p className="text-destructive text-sm">{error}</p>
|
|
273
274
|
)}
|
|
275
|
+
{kind === "chat" && sessionId && (
|
|
276
|
+
<button
|
|
277
|
+
type="button"
|
|
278
|
+
onClick={() => {
|
|
279
|
+
shareUiActions.close();
|
|
280
|
+
exportUiActions.open(sessionId, target.title);
|
|
281
|
+
}}
|
|
282
|
+
className="text-muted-foreground hover:text-foreground min-h-11 self-start text-xs underline underline-offset-2"
|
|
283
|
+
>
|
|
284
|
+
Export for AI instead…
|
|
285
|
+
</button>
|
|
286
|
+
)}
|
|
274
287
|
</div>
|
|
275
288
|
</div>
|
|
276
289
|
|