@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
package/app/page.tsx
CHANGED
|
@@ -138,6 +138,10 @@ const ShareDialog = dynamic(
|
|
|
138
138
|
() => import("@/components/Share/ShareDialog").then((m) => m.ShareDialog),
|
|
139
139
|
{ ssr: false }
|
|
140
140
|
);
|
|
141
|
+
const ExportSheet = dynamic(
|
|
142
|
+
() => import("@/components/Export/ExportSheet").then((m) => m.ExportSheet),
|
|
143
|
+
{ ssr: false }
|
|
144
|
+
);
|
|
141
145
|
const CommandPalette = dynamic(
|
|
142
146
|
() =>
|
|
143
147
|
import("@/components/CommandPalette/CommandPalette").then(
|
|
@@ -166,6 +170,7 @@ const AppDialogs = memo(function AppDialogs({
|
|
|
166
170
|
<ClaimsSheet />
|
|
167
171
|
<MoveDialog />
|
|
168
172
|
<ShareDialog />
|
|
173
|
+
<ExportSheet />
|
|
169
174
|
<AwayReport />
|
|
170
175
|
<IncomingLinks />
|
|
171
176
|
<CommandPalette />
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { memo } from "react";
|
|
4
|
+
import { useSnapshot } from "valtio";
|
|
4
5
|
import {
|
|
5
6
|
Check,
|
|
6
7
|
ChevronRight,
|
|
8
|
+
Download,
|
|
7
9
|
Circle,
|
|
8
10
|
CircleDot,
|
|
9
11
|
Quote,
|
|
@@ -24,6 +26,8 @@ import { Markdown } from "./Markdown";
|
|
|
24
26
|
import { openSessionLink } from "./session-link";
|
|
25
27
|
import { UndoButton } from "./Undo";
|
|
26
28
|
import { useRowState } from "./rowState";
|
|
29
|
+
import { useBookmarkChat } from "@/components/Bookmarks/context";
|
|
30
|
+
import { exportUi, exportUiActions, pickedRange } from "@/stores/exportUi";
|
|
27
31
|
import {
|
|
28
32
|
BookmarkButton,
|
|
29
33
|
TurnBookmark,
|
|
@@ -153,6 +157,71 @@ function SendStatus({ pending }: { pending: PendingStatus }) {
|
|
|
153
157
|
);
|
|
154
158
|
}
|
|
155
159
|
|
|
160
|
+
// The turn this message begins, while turns are being picked for an export.
|
|
161
|
+
function useExportTurn(itemId: string): number | null {
|
|
162
|
+
const ui = useSnapshot(exportUi);
|
|
163
|
+
if (!ui.selecting) return null;
|
|
164
|
+
return ui.turns.find((t) => t.id === itemId)?.n ?? null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// A tick in the gutter and a tinted row: tap anywhere on it to pick the turn.
|
|
168
|
+
function TurnTick({
|
|
169
|
+
turn,
|
|
170
|
+
children,
|
|
171
|
+
}: {
|
|
172
|
+
turn: number;
|
|
173
|
+
children: React.ReactNode;
|
|
174
|
+
}) {
|
|
175
|
+
const { pick } = useSnapshot(exportUi);
|
|
176
|
+
const range = pickedRange(pick);
|
|
177
|
+
const on = !!range && turn >= range.from && turn <= range.to;
|
|
178
|
+
return (
|
|
179
|
+
<div
|
|
180
|
+
className={cn(
|
|
181
|
+
"-mx-2 flex items-start gap-2 rounded-xl px-2",
|
|
182
|
+
on && "bg-primary/10"
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
role="checkbox"
|
|
188
|
+
aria-checked={on}
|
|
189
|
+
aria-label={`Turn ${turn}`}
|
|
190
|
+
onClick={() => exportUiActions.tap(turn)}
|
|
191
|
+
className={cn(
|
|
192
|
+
"mt-1 flex h-11 min-w-11 shrink-0 flex-col items-center justify-center rounded-md border text-[11px]",
|
|
193
|
+
on
|
|
194
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
195
|
+
: "text-muted-foreground"
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
{on ? <Check className="h-4 w-4" /> : turn}
|
|
199
|
+
</button>
|
|
200
|
+
<div className="min-w-0 flex-1">{children}</div>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Export this message's turn and everything after it.
|
|
206
|
+
function ExportFromHere({ itemId }: { itemId: string }) {
|
|
207
|
+
const chat = useBookmarkChat();
|
|
208
|
+
if (!chat) return null;
|
|
209
|
+
return (
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
aria-label="Export from here"
|
|
213
|
+
title="Export from here"
|
|
214
|
+
onClick={(e) => {
|
|
215
|
+
e.stopPropagation();
|
|
216
|
+
exportUiActions.open(chat.sessionId, "", itemId);
|
|
217
|
+
}}
|
|
218
|
+
className="text-muted-foreground hover:text-foreground hover:bg-foreground/[0.06] flex h-11 w-11 shrink-0 items-center justify-center rounded-md opacity-40 group-hover:opacity-100 focus-visible:opacity-100 md:h-7 md:w-7 md:opacity-0"
|
|
219
|
+
>
|
|
220
|
+
<Download className="h-3.5 w-3.5" />
|
|
221
|
+
</button>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
156
225
|
export function UserMessage({
|
|
157
226
|
item,
|
|
158
227
|
onUndo,
|
|
@@ -162,6 +231,7 @@ export function UserMessage({
|
|
|
162
231
|
onUndo?: () => void;
|
|
163
232
|
pending?: PendingStatus;
|
|
164
233
|
}) {
|
|
234
|
+
const turn = useExportTurn(item.id);
|
|
165
235
|
const message = (
|
|
166
236
|
<div className="group flex flex-col items-end gap-1">
|
|
167
237
|
{item.from && (
|
|
@@ -204,6 +274,7 @@ export function UserMessage({
|
|
|
204
274
|
) : (
|
|
205
275
|
<div className="-mr-1.5 flex items-center">
|
|
206
276
|
{onUndo && <UndoButton onClick={onUndo} />}
|
|
277
|
+
<ExportFromHere itemId={item.id} />
|
|
207
278
|
<BookmarkButton itemId={item.id} />
|
|
208
279
|
</div>
|
|
209
280
|
)}
|
|
@@ -211,7 +282,8 @@ export function UserMessage({
|
|
|
211
282
|
);
|
|
212
283
|
// Not in the chat yet: nothing to star.
|
|
213
284
|
if (pending) return message;
|
|
214
|
-
|
|
285
|
+
const row = <BookmarkPress itemId={item.id}>{message}</BookmarkPress>;
|
|
286
|
+
return turn ? <TurnTick turn={turn}>{row}</TurnTick> : row;
|
|
215
287
|
}
|
|
216
288
|
|
|
217
289
|
export function AssistantMessage({
|
|
@@ -221,6 +293,8 @@ export function AssistantMessage({
|
|
|
221
293
|
item: Of<"assistant">;
|
|
222
294
|
onQuote?: (text: string) => void;
|
|
223
295
|
}) {
|
|
296
|
+
// A reply whose only content was a tool call AgentOS ran for it.
|
|
297
|
+
if (!item.streaming && !item.text.trim()) return null;
|
|
224
298
|
return (
|
|
225
299
|
<BookmarkPress itemId={item.id}>
|
|
226
300
|
<div className="group">
|
|
@@ -300,18 +374,38 @@ export function Todos({ item }: { item: Of<"todos"> }) {
|
|
|
300
374
|
|
|
301
375
|
export function TurnEnd({ item }: { item: Of<"turn_end"> }) {
|
|
302
376
|
const secs = item.durationMs ? Math.round(item.durationMs / 1000) : null;
|
|
377
|
+
const early = !item.interrupted && item.ended;
|
|
303
378
|
const label = item.interrupted
|
|
304
379
|
? "Stopped"
|
|
305
|
-
:
|
|
306
|
-
?
|
|
307
|
-
|
|
380
|
+
: early
|
|
381
|
+
? secs !== null
|
|
382
|
+
? `Ended early in ${secs}s`
|
|
383
|
+
: "Ended early"
|
|
384
|
+
: secs !== null
|
|
385
|
+
? `Done in ${secs}s`
|
|
386
|
+
: "Done";
|
|
308
387
|
return (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
388
|
+
<>
|
|
389
|
+
{early && (
|
|
390
|
+
<div
|
|
391
|
+
role="status"
|
|
392
|
+
className="rounded-xl bg-amber-500/10 px-3.5 py-2.5 text-sm text-amber-700 dark:text-amber-300"
|
|
393
|
+
>
|
|
394
|
+
{early.notice}
|
|
395
|
+
{item.finishReason && (
|
|
396
|
+
<span className="mt-1 block font-mono text-[11px] opacity-80">
|
|
397
|
+
finish reason: {item.finishReason}
|
|
398
|
+
</span>
|
|
399
|
+
)}
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
<div className="group text-muted-foreground/60 flex items-center gap-3 font-mono text-[11px]">
|
|
403
|
+
<span className="bg-foreground/[0.06] h-px flex-1" />
|
|
404
|
+
{label}
|
|
405
|
+
<TurnBookmark turnEndId={item.id} />
|
|
406
|
+
<span className="bg-foreground/[0.06] h-px flex-1" />
|
|
407
|
+
</div>
|
|
408
|
+
</>
|
|
315
409
|
);
|
|
316
410
|
}
|
|
317
411
|
|
|
@@ -21,6 +21,7 @@ import { useAgentStatusQuery } from "@/data/agents";
|
|
|
21
21
|
import { useProjectMachinesQuery } from "@/data/projects";
|
|
22
22
|
import { modelChoices, useModelsQuery } from "@/data/models";
|
|
23
23
|
import type { AgentProbe } from "@/lib/agents/probe";
|
|
24
|
+
import { RoleChips } from "./RoleChips";
|
|
24
25
|
import { ChipHeading, ChipItem, ChipMenu, ChipToggle } from "./Chip";
|
|
25
26
|
import {
|
|
26
27
|
DEFAULT_PRIORITY,
|
|
@@ -168,7 +169,7 @@ export function DraftChips({
|
|
|
168
169
|
</ChipMenu>
|
|
169
170
|
)}
|
|
170
171
|
|
|
171
|
-
{!draft.openPr && (
|
|
172
|
+
{!draft.openPr && !draft.agentRole && (
|
|
172
173
|
<ChipMenu icon={Bot} name="Agent" label={agentLabel(draft.agentType)}>
|
|
173
174
|
{AGENT_OPTIONS.map((o) => (
|
|
174
175
|
<ChipItem
|
|
@@ -189,6 +190,10 @@ export function DraftChips({
|
|
|
189
190
|
</ChipMenu>
|
|
190
191
|
)}
|
|
191
192
|
|
|
193
|
+
{!draft.openPr && local && (
|
|
194
|
+
<RoleChips draft={draft} onChange={onChange} />
|
|
195
|
+
)}
|
|
196
|
+
|
|
192
197
|
{project && local && git?.isGitRepo && (
|
|
193
198
|
<ChipMenu
|
|
194
199
|
icon={GitBranch}
|
|
@@ -14,6 +14,8 @@ import { useHostsQuery } from "@/data/hosts";
|
|
|
14
14
|
import { useGitCheck } from "@/data/git/queries";
|
|
15
15
|
import { useSelectedWorkspace } from "@/hooks/useSelectedWorkspace";
|
|
16
16
|
import { useLaunchSession } from "@/data/sessions";
|
|
17
|
+
import { ModelUnavailableError } from "@/data/sessions/queries";
|
|
18
|
+
import { isAgentRole } from "@/lib/models/agent-roles";
|
|
17
19
|
import { useCreateTask } from "@/data/tasks";
|
|
18
20
|
import { useCheckStart } from "@/data/claims";
|
|
19
21
|
import { StartClaimsNotice } from "@/components/Claims";
|
|
@@ -23,6 +25,7 @@ import { setPendingPrompt } from "@/stores/initialPrompt";
|
|
|
23
25
|
import { draftsActions, draftsStore, newDraft } from "@/stores/drafts";
|
|
24
26
|
import { DraftChips } from "./DraftChips";
|
|
25
27
|
import { DraftHeading } from "./DraftHeading";
|
|
28
|
+
import { ModelUnavailableDialog } from "./ModelUnavailableDialog";
|
|
26
29
|
import { draftModels } from "./draftModels";
|
|
27
30
|
|
|
28
31
|
// A new session before its first send: choices on top, a composer below.
|
|
@@ -63,6 +66,13 @@ export function DraftPanel({
|
|
|
63
66
|
text: string;
|
|
64
67
|
warnings: StartWarning[];
|
|
65
68
|
} | null>(null);
|
|
69
|
+
// A role's model server that didn't answer, with what was being sent.
|
|
70
|
+
const [down, setDown] = useState<{
|
|
71
|
+
error: ModelUnavailableError;
|
|
72
|
+
text: string;
|
|
73
|
+
images: ChatImage[];
|
|
74
|
+
files: ChatFile[];
|
|
75
|
+
} | null>(null);
|
|
66
76
|
const pending =
|
|
67
77
|
launch.isPending || createTask.isPending || checkStart.isPending;
|
|
68
78
|
|
|
@@ -81,7 +91,7 @@ export function DraftPanel({
|
|
|
81
91
|
text: string,
|
|
82
92
|
images: ChatImage[],
|
|
83
93
|
files: ChatFile[],
|
|
84
|
-
claim?: { after?: string }
|
|
94
|
+
claim?: { after?: string; onUnavailable?: "fallback" }
|
|
85
95
|
) => {
|
|
86
96
|
try {
|
|
87
97
|
if (draft.openPr) {
|
|
@@ -129,7 +139,17 @@ export function DraftPanel({
|
|
|
129
139
|
? draft.hostId
|
|
130
140
|
: undefined,
|
|
131
141
|
agentType: draft.agentType,
|
|
132
|
-
model: draft.
|
|
142
|
+
model: isAgentRole(draft.agentRole)
|
|
143
|
+
? (draft.roleModel ?? "")
|
|
144
|
+
: draft.model,
|
|
145
|
+
...(isAgentRole(draft.agentRole)
|
|
146
|
+
? {
|
|
147
|
+
agentRole: draft.agentRole,
|
|
148
|
+
...(claim?.onUnavailable && {
|
|
149
|
+
onUnavailable: claim.onUnavailable,
|
|
150
|
+
}),
|
|
151
|
+
}
|
|
152
|
+
: {}),
|
|
133
153
|
access: draft.access,
|
|
134
154
|
useWorktree: draft.useWorktree && !!git?.isGitRepo,
|
|
135
155
|
baseBranch: draft.baseBranch,
|
|
@@ -153,6 +173,10 @@ export function DraftPanel({
|
|
|
153
173
|
session.host_id
|
|
154
174
|
);
|
|
155
175
|
} catch (error) {
|
|
176
|
+
if (error instanceof ModelUnavailableError) {
|
|
177
|
+
setDown({ error, text, images, files });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
156
180
|
setPrefill({ text, at: Date.now() });
|
|
157
181
|
toast.error(error instanceof Error ? error.message : String(error));
|
|
158
182
|
}
|
|
@@ -172,6 +196,31 @@ export function DraftPanel({
|
|
|
172
196
|
git={git}
|
|
173
197
|
onChange={(patch) => draftsActions.update(draft.id, patch)}
|
|
174
198
|
/>
|
|
199
|
+
<ModelUnavailableDialog
|
|
200
|
+
error={down?.error ?? null}
|
|
201
|
+
busy={pending}
|
|
202
|
+
onClose={() => {
|
|
203
|
+
if (down) setPrefill({ text: down.text, at: Date.now() });
|
|
204
|
+
setDown(null);
|
|
205
|
+
}}
|
|
206
|
+
onRetry={() => {
|
|
207
|
+
const d = down;
|
|
208
|
+
setDown(null);
|
|
209
|
+
if (d) void send(d.text, d.images, d.files);
|
|
210
|
+
}}
|
|
211
|
+
onFallback={() => {
|
|
212
|
+
const d = down;
|
|
213
|
+
setDown(null);
|
|
214
|
+
if (d)
|
|
215
|
+
void send(d.text, d.images, d.files, {
|
|
216
|
+
onUnavailable: "fallback",
|
|
217
|
+
});
|
|
218
|
+
}}
|
|
219
|
+
onPick={() => {
|
|
220
|
+
if (down) setPrefill({ text: down.text, at: Date.now() });
|
|
221
|
+
setDown(null);
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
175
224
|
{held && (
|
|
176
225
|
<StartClaimsNotice
|
|
177
226
|
warnings={held.warnings}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from "@/components/ui/dialog";
|
|
11
|
+
import { AGENT_ROLE_DEFS, isAgentRole } from "@/lib/models/agent-roles";
|
|
12
|
+
import type { ModelUnavailableError } from "@/data/sessions/queries";
|
|
13
|
+
|
|
14
|
+
// A session in a role couldn't start: its model's server isn't answering.
|
|
15
|
+
// A local model never turns into a cloud one on its own: starting on the
|
|
16
|
+
// role's fallback is a choice made here.
|
|
17
|
+
export function ModelUnavailableDialog({
|
|
18
|
+
error,
|
|
19
|
+
busy,
|
|
20
|
+
onRetry,
|
|
21
|
+
onFallback,
|
|
22
|
+
onPick,
|
|
23
|
+
onClose,
|
|
24
|
+
}: {
|
|
25
|
+
error: ModelUnavailableError | null;
|
|
26
|
+
busy: boolean;
|
|
27
|
+
onRetry: () => void;
|
|
28
|
+
onFallback: () => void;
|
|
29
|
+
onPick: () => void;
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
}) {
|
|
32
|
+
const role =
|
|
33
|
+
error && isAgentRole(error.role)
|
|
34
|
+
? AGENT_ROLE_DEFS[error.role].label
|
|
35
|
+
: "role";
|
|
36
|
+
const name = (ref: string) => ref.split("/").slice(1).join("/");
|
|
37
|
+
return (
|
|
38
|
+
<Dialog open={!!error} onOpenChange={(o) => !o && onClose()}>
|
|
39
|
+
<DialogContent>
|
|
40
|
+
<DialogHeader>
|
|
41
|
+
<DialogTitle>Can't start the {role}</DialogTitle>
|
|
42
|
+
<DialogDescription>
|
|
43
|
+
{error ? `${name(error.ref)}'s server isn't answering.` : ""}
|
|
44
|
+
</DialogDescription>
|
|
45
|
+
</DialogHeader>
|
|
46
|
+
{error && (
|
|
47
|
+
<p className="bg-muted/60 text-muted-foreground rounded-lg px-3 py-2 font-mono text-xs break-words">
|
|
48
|
+
{error.message}
|
|
49
|
+
</p>
|
|
50
|
+
)}
|
|
51
|
+
<div className="flex flex-col gap-2">
|
|
52
|
+
<Button className="h-11" disabled={busy} onClick={onRetry}>
|
|
53
|
+
Retry
|
|
54
|
+
</Button>
|
|
55
|
+
{error?.fallback && (
|
|
56
|
+
<Button
|
|
57
|
+
variant="outline"
|
|
58
|
+
className="h-11"
|
|
59
|
+
disabled={busy}
|
|
60
|
+
onClick={onFallback}
|
|
61
|
+
>
|
|
62
|
+
Start on {name(error.fallback)}
|
|
63
|
+
</Button>
|
|
64
|
+
)}
|
|
65
|
+
<Button variant="outline" className="h-11" onClick={onPick}>
|
|
66
|
+
Pick another model
|
|
67
|
+
</Button>
|
|
68
|
+
</div>
|
|
69
|
+
{error?.fallback && (
|
|
70
|
+
<p className="text-muted-foreground text-xs">
|
|
71
|
+
Starting on the fallback sends your code to wherever that model
|
|
72
|
+
runs.
|
|
73
|
+
</p>
|
|
74
|
+
)}
|
|
75
|
+
</DialogContent>
|
|
76
|
+
</Dialog>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Cpu, Users } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
AvailabilityDot,
|
|
6
|
+
LocalityBadge,
|
|
7
|
+
} from "@/components/Models/ModelBadges";
|
|
8
|
+
import { useAgentRolesQuery, useModelChoices } from "@/data/models";
|
|
9
|
+
import type { Draft } from "@/lib/drafts";
|
|
10
|
+
import {
|
|
11
|
+
AGENT_ROLES,
|
|
12
|
+
AGENT_ROLE_DEFS,
|
|
13
|
+
isAgentRole,
|
|
14
|
+
} from "@/lib/models/agent-roles";
|
|
15
|
+
import { ChipItem, ChipMenu } from "./Chip";
|
|
16
|
+
|
|
17
|
+
// A draft's role and the model it runs on. The role's binding gives the
|
|
18
|
+
// model (Settings > Agent roles); the model chip overrides it for this
|
|
19
|
+
// session. Both lists come from the server.
|
|
20
|
+
export function RoleChips({
|
|
21
|
+
draft,
|
|
22
|
+
onChange,
|
|
23
|
+
}: {
|
|
24
|
+
draft: Draft;
|
|
25
|
+
onChange: (patch: Partial<Draft>) => void;
|
|
26
|
+
}) {
|
|
27
|
+
const roles = useAgentRolesQuery("settings");
|
|
28
|
+
const { data: choices = [] } = useModelChoices(!!draft.agentRole);
|
|
29
|
+
const role = isAgentRole(draft.agentRole) ? draft.agentRole : null;
|
|
30
|
+
const bound = roles.data?.roles.find((r) => r.id === role);
|
|
31
|
+
const override = draft.roleModel
|
|
32
|
+
? choices.find((c) => c.ref === draft.roleModel)
|
|
33
|
+
: undefined;
|
|
34
|
+
const current = override ?? bound?.choice ?? null;
|
|
35
|
+
const modelName = (ref: string | undefined | null) =>
|
|
36
|
+
ref ? ref.split("/").slice(1).join("/") : "…";
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<ChipMenu
|
|
40
|
+
icon={Users}
|
|
41
|
+
name="Role"
|
|
42
|
+
label={role ? AGENT_ROLE_DEFS[role].label : "No role"}
|
|
43
|
+
>
|
|
44
|
+
<ChipItem
|
|
45
|
+
checked={!role}
|
|
46
|
+
onSelect={() => onChange({ agentRole: null, roleModel: null })}
|
|
47
|
+
hint="A plain session on the interactive model"
|
|
48
|
+
>
|
|
49
|
+
No role
|
|
50
|
+
</ChipItem>
|
|
51
|
+
{AGENT_ROLES.map((r) => {
|
|
52
|
+
const view = roles.data?.roles.find((x) => x.id === r);
|
|
53
|
+
return (
|
|
54
|
+
<ChipItem
|
|
55
|
+
key={r}
|
|
56
|
+
checked={r === role}
|
|
57
|
+
onSelect={() =>
|
|
58
|
+
onChange({ agentRole: r, roleModel: null, openPr: false })
|
|
59
|
+
}
|
|
60
|
+
hint={`${AGENT_ROLE_DEFS[r].purpose}${view?.binding ? ` · ${modelName(view.binding.ref)}` : ""}`}
|
|
61
|
+
>
|
|
62
|
+
{AGENT_ROLE_DEFS[r].label}
|
|
63
|
+
</ChipItem>
|
|
64
|
+
);
|
|
65
|
+
})}
|
|
66
|
+
</ChipMenu>
|
|
67
|
+
{role && (
|
|
68
|
+
<ChipMenu
|
|
69
|
+
icon={Cpu}
|
|
70
|
+
name="Model"
|
|
71
|
+
label={
|
|
72
|
+
draft.roleModel
|
|
73
|
+
? modelName(draft.roleModel)
|
|
74
|
+
: `Role model: ${modelName(bound?.binding?.ref)}`
|
|
75
|
+
}
|
|
76
|
+
>
|
|
77
|
+
<ChipItem
|
|
78
|
+
checked={!draft.roleModel}
|
|
79
|
+
onSelect={() => onChange({ roleModel: null })}
|
|
80
|
+
hint={bound?.binding ? modelName(bound.binding.ref) : undefined}
|
|
81
|
+
>
|
|
82
|
+
Role model
|
|
83
|
+
</ChipItem>
|
|
84
|
+
{choices.map((c) => (
|
|
85
|
+
<ChipItem
|
|
86
|
+
key={c.ref}
|
|
87
|
+
checked={c.ref === draft.roleModel}
|
|
88
|
+
onSelect={() => onChange({ roleModel: c.ref })}
|
|
89
|
+
hint={c.group}
|
|
90
|
+
>
|
|
91
|
+
<span className="flex items-center gap-1.5">
|
|
92
|
+
<AvailabilityDot availability={c.availability} />
|
|
93
|
+
{c.label}
|
|
94
|
+
<LocalityBadge locality={c.locality} />
|
|
95
|
+
</span>
|
|
96
|
+
</ChipItem>
|
|
97
|
+
))}
|
|
98
|
+
</ChipMenu>
|
|
99
|
+
)}
|
|
100
|
+
{role && current?.availability.state === "down" && (
|
|
101
|
+
<span
|
|
102
|
+
role="status"
|
|
103
|
+
className="text-destructive flex h-11 items-center text-xs md:h-8"
|
|
104
|
+
>
|
|
105
|
+
{modelName(current.ref)} isn't answering
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { Check, Copy, Download, Loader2, Share2 } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { writeClipboard } from "@/hooks/useCopyToClipboard";
|
|
8
|
+
import { fetchExport, type ExportEstimate } from "@/data/exports";
|
|
9
|
+
import type { ExportRequest } from "@/lib/export/view";
|
|
10
|
+
|
|
11
|
+
type Busy = "copy" | "download" | "share" | null;
|
|
12
|
+
|
|
13
|
+
function save(blob: Blob, name: string) {
|
|
14
|
+
const url = URL.createObjectURL(blob);
|
|
15
|
+
const a = document.createElement("a");
|
|
16
|
+
a.href = url;
|
|
17
|
+
a.download = name;
|
|
18
|
+
a.click();
|
|
19
|
+
URL.revokeObjectURL(url);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Copy for agent, Download and (where the browser can) Share. Never blocked
|
|
23
|
+
// by a sensitive-data warning, only by the size limit.
|
|
24
|
+
export function ExportActions({
|
|
25
|
+
sessionId,
|
|
26
|
+
title,
|
|
27
|
+
req,
|
|
28
|
+
est,
|
|
29
|
+
}: {
|
|
30
|
+
sessionId: string;
|
|
31
|
+
title: string;
|
|
32
|
+
req: ExportRequest;
|
|
33
|
+
est: ExportEstimate | null;
|
|
34
|
+
}) {
|
|
35
|
+
const [busy, setBusy] = useState<Busy>(null);
|
|
36
|
+
const [copied, setCopied] = useState(false);
|
|
37
|
+
// Only rendered on the client (the sheet loads with ssr off).
|
|
38
|
+
const [canShare] = useState(() => {
|
|
39
|
+
try {
|
|
40
|
+
const probe = new File([""], "x.md", { type: "text/markdown" });
|
|
41
|
+
return !!navigator.canShare?.({ files: [probe] });
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const wire = JSON.parse(JSON.stringify(req)) as ExportRequest;
|
|
48
|
+
const file = () => fetchExport(sessionId, wire, title, est?.selected ?? null);
|
|
49
|
+
const disabled = !est || est.tooBig || busy !== null;
|
|
50
|
+
|
|
51
|
+
const run =
|
|
52
|
+
(kind: Exclude<Busy, null>, work: () => Promise<void>) => async () => {
|
|
53
|
+
setBusy(kind);
|
|
54
|
+
try {
|
|
55
|
+
await work();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if ((err as Error).name !== "AbortError")
|
|
58
|
+
toast.error((err as Error).message || "Export failed");
|
|
59
|
+
} finally {
|
|
60
|
+
setBusy(null);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const copy = run("copy", async () => {
|
|
65
|
+
const pending = file();
|
|
66
|
+
// Safari only allows a copy started inside the tap: hand it the promise.
|
|
67
|
+
if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
|
|
68
|
+
await navigator.clipboard.write([
|
|
69
|
+
new ClipboardItem({
|
|
70
|
+
"text/plain": pending.then(
|
|
71
|
+
(f) => new Blob([f.blob], { type: "text/plain" })
|
|
72
|
+
),
|
|
73
|
+
}),
|
|
74
|
+
]);
|
|
75
|
+
} else {
|
|
76
|
+
await writeClipboard(await (await pending).blob.text());
|
|
77
|
+
}
|
|
78
|
+
setCopied(true);
|
|
79
|
+
setTimeout(() => setCopied(false), 1500);
|
|
80
|
+
toast.success("Copied. Paste it into your AI chat.");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const download = run("download", async () => {
|
|
84
|
+
const f = await file();
|
|
85
|
+
save(f.blob, f.name);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const share = run("share", async () => {
|
|
89
|
+
const f = await file();
|
|
90
|
+
const shared = new File([f.blob], f.name, { type: f.blob.type });
|
|
91
|
+
if (!navigator.canShare?.({ files: [shared] })) return save(f.blob, f.name);
|
|
92
|
+
await navigator.share({ files: [shared], title });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const Spin = <Loader2 className="h-4 w-4 animate-spin" />;
|
|
96
|
+
return (
|
|
97
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:justify-end">
|
|
98
|
+
<Button
|
|
99
|
+
className="min-h-11 sm:order-last"
|
|
100
|
+
disabled={disabled}
|
|
101
|
+
onClick={copy}
|
|
102
|
+
>
|
|
103
|
+
{busy === "copy" ? (
|
|
104
|
+
Spin
|
|
105
|
+
) : copied ? (
|
|
106
|
+
<Check className="h-4 w-4" />
|
|
107
|
+
) : (
|
|
108
|
+
<Copy className="h-4 w-4" />
|
|
109
|
+
)}
|
|
110
|
+
{req.format === "md" ? "Copy for agent" : "Copy"}
|
|
111
|
+
</Button>
|
|
112
|
+
<div className="flex gap-2">
|
|
113
|
+
<Button
|
|
114
|
+
variant="outline"
|
|
115
|
+
className="min-h-11 flex-1 sm:flex-none"
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
onClick={download}
|
|
118
|
+
>
|
|
119
|
+
{busy === "download" ? Spin : <Download className="h-4 w-4" />}
|
|
120
|
+
Download
|
|
121
|
+
</Button>
|
|
122
|
+
{canShare && (
|
|
123
|
+
<Button
|
|
124
|
+
variant="outline"
|
|
125
|
+
className="min-h-11 flex-1 sm:flex-none"
|
|
126
|
+
disabled={disabled}
|
|
127
|
+
onClick={share}
|
|
128
|
+
>
|
|
129
|
+
{busy === "share" ? Spin : <Share2 className="h-4 w-4" />}
|
|
130
|
+
Share…
|
|
131
|
+
</Button>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|