@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.
Files changed (142) hide show
  1. package/README.md +1 -0
  2. package/app/api/agent-roles/route.test.ts +67 -0
  3. package/app/api/agent-roles/route.ts +61 -0
  4. package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
  5. package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
  6. package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
  7. package/app/api/analyst/[id]/review/route.ts +22 -0
  8. package/app/api/analyst/[id]/route.ts +44 -0
  9. package/app/api/analyst/route.ts +10 -0
  10. package/app/api/bus/spawn/route.ts +22 -1
  11. package/app/api/models/availability/route.ts +9 -0
  12. package/app/api/models/choices/route.ts +9 -0
  13. package/app/api/sessions/[id]/analyst/route.ts +11 -0
  14. package/app/api/sessions/[id]/crew/route.ts +38 -0
  15. package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
  16. package/app/api/sessions/[id]/export/route.test.ts +125 -0
  17. package/app/api/sessions/[id]/export/route.ts +34 -0
  18. package/app/api/sessions/[id]/model/route.ts +49 -0
  19. package/app/api/sessions/route.ts +16 -0
  20. package/app/page.tsx +5 -0
  21. package/components/Chat/Messages.tsx +104 -10
  22. package/components/Draft/DraftChips.tsx +6 -1
  23. package/components/Draft/DraftPanel.tsx +51 -2
  24. package/components/Draft/ModelUnavailableDialog.tsx +78 -0
  25. package/components/Draft/RoleChips.tsx +110 -0
  26. package/components/Export/ExportActions.tsx +136 -0
  27. package/components/Export/ExportSheet.tsx +217 -0
  28. package/components/Export/RangePicker.tsx +144 -0
  29. package/components/Export/Seg.tsx +38 -0
  30. package/components/Export/TurnSelectBar.tsx +45 -0
  31. package/components/Models/AgentRoleCard.tsx +114 -0
  32. package/components/Models/AgentRolesSection.tsx +132 -0
  33. package/components/Models/ModelBadges.tsx +48 -0
  34. package/components/Models/ModelsSection.tsx +4 -2
  35. package/components/Models/RoleModelSelect.tsx +87 -0
  36. package/components/Pane/DesktopTabBar.tsx +2 -0
  37. package/components/Pane/MobileTabBar.tsx +12 -0
  38. package/components/Pane/PaneMenu.tsx +10 -0
  39. package/components/SessionList/SessionRow.tsx +18 -7
  40. package/components/SessionList/SessionRowMenu.tsx +6 -0
  41. package/components/Sessions/RolePill.tsx +196 -0
  42. package/components/Settings/SettingsDialog.tsx +9 -0
  43. package/components/Share/ShareDialog.tsx +13 -0
  44. package/data/analyst/index.ts +3 -0
  45. package/data/analyst/keys.ts +6 -0
  46. package/data/analyst/mutations.ts +116 -0
  47. package/data/analyst/queries.ts +35 -0
  48. package/data/exports/index.ts +2 -0
  49. package/data/exports/keys.ts +3 -0
  50. package/data/exports/queries.ts +51 -0
  51. package/data/models/index.ts +98 -0
  52. package/data/push/topics.ts +5 -0
  53. package/data/sessions/queries.ts +26 -0
  54. package/lib/agents/brief.ts +1 -0
  55. package/lib/analyst/__fixtures__/items.ts +57 -0
  56. package/lib/analyst/__fixtures__/mocks.ts +49 -0
  57. package/lib/analyst/brief.ts +17 -0
  58. package/lib/analyst/buildDigest.ts +204 -0
  59. package/lib/analyst/createLink.ts +98 -0
  60. package/lib/analyst/endLink.ts +31 -0
  61. package/lib/analyst/forward.ts +109 -0
  62. package/lib/analyst/index.ts +16 -0
  63. package/lib/analyst/linkView.ts +38 -0
  64. package/lib/analyst/onTurnEnd.ts +119 -0
  65. package/lib/analyst/parseReview.ts +41 -0
  66. package/lib/analyst/pushDigest.ts +143 -0
  67. package/lib/analyst/resume.ts +35 -0
  68. package/lib/analyst/route.ts +50 -0
  69. package/lib/analyst/signals.ts +121 -0
  70. package/lib/analyst/store.ts +303 -0
  71. package/lib/analyst/types.ts +140 -0
  72. package/lib/bus/condense.ts +5 -1
  73. package/lib/bus/index.ts +5 -0
  74. package/lib/bus/read-scope.ts +12 -0
  75. package/lib/bus/role-alias.ts +70 -0
  76. package/lib/bus/targets.ts +3 -0
  77. package/lib/chat/drivers/opencode-mapper.ts +55 -1
  78. package/lib/chat/drivers/pi-approvals.ts +7 -4
  79. package/lib/chat/drivers/pi-mapper.ts +38 -0
  80. package/lib/chat/drivers/pi-text-tools.ts +104 -0
  81. package/lib/chat/drivers/pi.ts +8 -3
  82. package/lib/chat/drivers/turn-items.ts +36 -11
  83. package/lib/chat/drivers/turn-outcome.ts +108 -0
  84. package/lib/chat/events.ts +8 -0
  85. package/lib/chat/registry.ts +13 -0
  86. package/lib/chat/store.ts +19 -0
  87. package/lib/chat/turns.ts +1 -0
  88. package/lib/chat/worker/extras.ts +4 -2
  89. package/lib/claims/collectLocal.ts +3 -1
  90. package/lib/db/changes.ts +6 -0
  91. package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
  92. package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
  93. package/lib/db/session-parts/columns.ts +2 -0
  94. package/lib/db/types.ts +3 -0
  95. package/lib/done/archive.ts +2 -0
  96. package/lib/drafts.ts +4 -0
  97. package/lib/export/build.ts +470 -0
  98. package/lib/export/compact.ts +156 -0
  99. package/lib/export/estimate.ts +83 -0
  100. package/lib/export/fixtures.ts +45 -0
  101. package/lib/export/html.tsx +195 -0
  102. package/lib/export/json.ts +39 -0
  103. package/lib/export/load.ts +64 -0
  104. package/lib/export/markdown.ts +127 -0
  105. package/lib/export/request.ts +36 -0
  106. package/lib/export/service.ts +142 -0
  107. package/lib/export/summary.ts +86 -0
  108. package/lib/export/turns.ts +63 -0
  109. package/lib/export/view.ts +173 -0
  110. package/lib/models/agent-binding.ts +102 -0
  111. package/lib/models/agent-roles-view.ts +157 -0
  112. package/lib/models/agent-roles.ts +87 -0
  113. package/lib/models/availability-refs.ts +17 -0
  114. package/lib/models/availability.ts +91 -0
  115. package/lib/models/catalog.ts +156 -0
  116. package/lib/models/config.ts +27 -0
  117. package/lib/models/locality.ts +33 -0
  118. package/lib/models/providers.ts +11 -0
  119. package/lib/models/resolve.ts +50 -12
  120. package/lib/models/roles.ts +12 -0
  121. package/lib/needs-you.ts +1 -1
  122. package/lib/orchestrator/facts.ts +7 -2
  123. package/lib/orchestrator/read.ts +5 -1
  124. package/lib/orchestrator/untrusted.ts +5 -0
  125. package/lib/projects.ts +1 -0
  126. package/lib/session-artifacts/agentos-files.ts +94 -0
  127. package/lib/session-artifacts/collect.ts +17 -0
  128. package/lib/sessions/crew.ts +72 -0
  129. package/lib/sessions/launch.ts +49 -9
  130. package/lib/settings-url.ts +1 -0
  131. package/lib/share/markdown.ts +2 -2
  132. package/lib/share/redact.ts +16 -10
  133. package/lib/share/render.tsx +1 -1
  134. package/lib/share/snapshot.ts +2 -2
  135. package/lib/sidebar/row-menu.ts +6 -1
  136. package/lib/tasks/index.ts +6 -0
  137. package/lib/worktree-deps/caches.ts +3 -2
  138. package/lib/worktree-deps/clone.ts +3 -1
  139. package/lib/worktree-deps/paths.ts +26 -0
  140. package/package.json +1 -1
  141. package/server.ts +9 -1
  142. 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. A workspace can set its own
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
- className={cn(
131
- "truncate text-sm",
132
- muted && !active ? "text-muted-foreground" : "text-foreground",
133
- active && "font-medium"
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
- {session.name}
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&apos;s server isn&apos;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
 
@@ -0,0 +1,3 @@
1
+ export { analystKeys } from "./keys";
2
+ export { useAnalystLink, useSessionAnalyst } from "./queries";
3
+ export * from "./mutations";
@@ -0,0 +1,6 @@
1
+ export const analystKeys = {
2
+ all: ["analyst"] as const,
3
+ link: (id: string) => [...analystKeys.all, "link", id] as const,
4
+ ofSession: (sessionId: string) =>
5
+ [...analystKeys.all, "session", sessionId] as const,
6
+ };