@saadnvd1/agent-os 0.3.715 → 0.3.728
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 +2 -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/loops/route.ts +17 -0
- 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/api/tasks/[id]/loop/cycles/[n]/log/route.ts +29 -0
- package/app/api/tasks/[id]/loop/draft-criteria/route.ts +17 -0
- package/app/api/tasks/[id]/loop/next/route.ts +25 -0
- package/app/api/tasks/[id]/loop/route.ts +72 -0
- package/app/api/tasks/[id]/loop/run/route.ts +12 -0
- package/app/api/tasks/[id]/loop/stop/route.ts +12 -0
- package/app/api/tasks/[id]/mode/route.ts +29 -0
- package/app/api/tasks/[id]/report/draft/route.ts +54 -0
- package/app/api/tasks/[id]/report/evidence/[evId]/route.ts +18 -0
- package/app/api/tasks/[id]/report/route.ts +39 -0
- package/app/api/workspaces/[id]/mode/route.ts +23 -0
- package/app/api/workspaces/[id]/reports/route.ts +17 -0
- package/app/page.tsx +13 -0
- package/components/Analyst/PairChip.tsx +131 -0
- package/components/Analyst/PairSwitch.tsx +76 -0
- package/components/Analyst/ReviewCard.tsx +272 -0
- package/components/Analyst/SignalChips.tsx +39 -0
- package/components/Analyst/StartAnalystSheet.tsx +109 -0
- package/components/Analyst/cardState.test.ts +109 -0
- package/components/Analyst/cardState.ts +95 -0
- package/components/Analyst/pair.ts +81 -0
- package/components/Chat/Code.tsx +3 -0
- package/components/Chat/Messages.tsx +110 -11
- package/components/Chat/itemContext.ts +14 -0
- package/components/Draft/DraftChips.tsx +16 -1
- package/components/Draft/DraftPanel.tsx +58 -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 +8 -0
- package/components/Pane/MobileTabBar.tsx +29 -0
- package/components/Pane/MobileViewPicker.tsx +4 -0
- package/components/Pane/PaneMenu.test.ts +21 -0
- package/components/Pane/PaneMenu.tsx +26 -0
- package/components/Pane/PaneViewToggle.tsx +12 -0
- package/components/Pane/index.tsx +19 -0
- package/components/Report/DraftCard.tsx +89 -0
- package/components/Report/EstimateBar.tsx +51 -0
- package/components/Report/EvidenceSheet.tsx +98 -0
- package/components/Report/ModeChooser.tsx +43 -0
- package/components/Report/ReportView.tsx +289 -0
- package/components/SessionList/SessionRow.tsx +18 -7
- package/components/SessionList/SessionRowMenu.tsx +12 -0
- package/components/Sessions/RolePill.tsx +203 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Share/ShareDialog.tsx +13 -0
- package/components/Tasks/Loop/LoopButton.tsx +74 -0
- package/components/Tasks/Loop/LoopSetup.tsx +146 -0
- package/components/Tasks/Loop/LoopView.tsx +350 -0
- package/components/Tasks/Loop/labels.ts +17 -0
- package/components/Tasks/TaskRow.tsx +2 -0
- package/components/Workspaces/WorkspaceModeForm.tsx +22 -0
- package/components/Workspaces/WorkspaceSettings.tsx +2 -0
- package/components/ui/segmented.tsx +55 -0
- package/components/ui/status-pill.tsx +3 -0
- package/contexts/PaneContext.tsx +21 -1
- 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/loops/index.ts +3 -0
- package/data/loops/keys.ts +4 -0
- package/data/loops/mutations.ts +68 -0
- package/data/loops/queries.ts +40 -0
- package/data/models/index.ts +98 -0
- package/data/push/topics.ts +11 -0
- package/data/reports/index.ts +115 -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 +13 -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/migrations/20261021T1600_task_loops.ts +81 -0
- package/lib/db/migrations/20261021T1800_health_reports.ts +38 -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/done/index.ts +2 -0
- package/lib/drafts.ts +6 -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/loops/analyse.ts +46 -0
- package/lib/loops/analyst-prompt.ts +203 -0
- package/lib/loops/checkCriteria.ts +114 -0
- package/lib/loops/compactEvidence.ts +140 -0
- package/lib/loops/controls.ts +317 -0
- package/lib/loops/decideStop.ts +113 -0
- package/lib/loops/deps.ts +128 -0
- package/lib/loops/diffMap.ts +39 -0
- package/lib/loops/draftCriteria.ts +71 -0
- package/lib/loops/gatherEvidence.ts +265 -0
- package/lib/loops/hooks.ts +40 -0
- package/lib/loops/index.ts +7 -0
- package/lib/loops/route.ts +83 -0
- package/lib/loops/runCycle.ts +323 -0
- package/lib/loops/screenInstruction.ts +59 -0
- package/lib/loops/sendInstruction.ts +83 -0
- package/lib/loops/snapshot.ts +38 -0
- package/lib/loops/stopLoop.ts +86 -0
- package/lib/loops/store.ts +481 -0
- package/lib/loops/summary.ts +27 -0
- package/lib/loops/tickLoops.ts +141 -0
- package/lib/loops/types.ts +223 -0
- package/lib/loops/world.test-helpers.ts +223 -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/plan-step.ts +4 -1
- 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/digest.ts +2 -1
- package/lib/orchestrator/facts.ts +7 -2
- package/lib/orchestrator/loop-guard.ts +5 -2
- package/lib/orchestrator/pr-wake.ts +4 -0
- package/lib/orchestrator/read.ts +5 -1
- package/lib/orchestrator/untrusted.ts +5 -0
- package/lib/orchestrator/watcher.ts +8 -1
- package/lib/panes.ts +52 -0
- package/lib/plans/acceptance.ts +23 -0
- package/lib/projects.ts +1 -0
- package/lib/reports/drafts.ts +87 -0
- package/lib/reports/end-hook.ts +17 -0
- package/lib/reports/estimate.ts +107 -0
- package/lib/reports/evidence/build.ts +193 -0
- package/lib/reports/evidence/gather.ts +229 -0
- package/lib/reports/fixtures.ts +17 -0
- package/lib/reports/generate.ts +159 -0
- package/lib/reports/hooks.ts +30 -0
- package/lib/reports/mode.ts +65 -0
- package/lib/reports/model.ts +115 -0
- package/lib/reports/read.ts +26 -0
- package/lib/reports/store.ts +180 -0
- package/lib/reports/template.ts +117 -0
- package/lib/reports/types.ts +135 -0
- package/lib/reports/validate.ts +142 -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 +15 -1
- package/lib/tasks/contract/routes.ts +5 -0
- package/lib/tasks/contract/types.ts +3 -0
- package/lib/tasks/finish.ts +2 -0
- package/lib/tasks/index.ts +6 -0
- package/lib/tasks/move-carry.ts +1 -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 +2 -2
- package/server.ts +9 -1
- package/stores/analystUi.ts +19 -0
- package/stores/exportUi.ts +87 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { usePanes } from "@/contexts/PaneContext";
|
|
6
|
+
import {
|
|
7
|
+
useSessionAnalyst,
|
|
8
|
+
useStartAnalyst,
|
|
9
|
+
type StartAnalystInput,
|
|
10
|
+
} from "@/data/analyst";
|
|
11
|
+
import type { Session } from "@/lib/db";
|
|
12
|
+
import type { AnalystLinkView, PaneSession } from "@/lib/analyst/types";
|
|
13
|
+
|
|
14
|
+
// A chat's tmux name: `<agent>-<id>`, as every chat tab attaches.
|
|
15
|
+
export const paneTmux = (s: PaneSession) => `${s.agentType}-${s.id}`;
|
|
16
|
+
|
|
17
|
+
export type PairStatus = "paused" | "reviewing" | "recommendation" | "watching";
|
|
18
|
+
|
|
19
|
+
export function pairStatus(link: AnalystLinkView): PairStatus {
|
|
20
|
+
if (link.paused) return "paused";
|
|
21
|
+
if (link.pendingDigestId) return "reviewing";
|
|
22
|
+
if (link.recommendations.some((r) => r.state === "open"))
|
|
23
|
+
return "recommendation";
|
|
24
|
+
return "watching";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const PAIR_STATUS_LABEL: Record<PairStatus, string> = {
|
|
28
|
+
paused: "Paused",
|
|
29
|
+
reviewing: "Reviewing",
|
|
30
|
+
recommendation: "Prompt ready",
|
|
31
|
+
watching: "Watching",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// The side of a pairing a session isn't.
|
|
35
|
+
export function partnerOf(link: AnalystLinkView, sessionId: string) {
|
|
36
|
+
return link.coder.id === sessionId ? link.analyst : link.coder;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Starts an analyst for a chat and shows it beside the coder's pane: the
|
|
40
|
+
// one given, else one showing the coder, else the focused one.
|
|
41
|
+
export function useStartAnalystBeside() {
|
|
42
|
+
const start = useStartAnalyst();
|
|
43
|
+
const { openBeside, state, focusedPaneId } = usePanes();
|
|
44
|
+
const run = useCallback(
|
|
45
|
+
async (input: StartAnalystInput, paneId?: string) => {
|
|
46
|
+
try {
|
|
47
|
+
const link = await start.mutateAsync(input);
|
|
48
|
+
const showing = Object.entries(state.panes).find(([, p]) =>
|
|
49
|
+
p.tabs.some(
|
|
50
|
+
(t) => t.id === p.activeTabId && t.sessionId === link.coder.id
|
|
51
|
+
)
|
|
52
|
+
)?.[0];
|
|
53
|
+
openBeside(paneId ?? showing ?? focusedPaneId, {
|
|
54
|
+
sessionId: link.analyst.id,
|
|
55
|
+
tmux: paneTmux(link.analyst),
|
|
56
|
+
});
|
|
57
|
+
return link;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
toast.error(error instanceof Error ? error.message : String(error));
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
[start, openBeside, state.panes, focusedPaneId]
|
|
64
|
+
);
|
|
65
|
+
return { run, isPending: start.isPending };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Whether a session can be given an analyst here: a local chat that isn't
|
|
69
|
+
// an orchestrator, an analyst or already paired. The server checks again.
|
|
70
|
+
export function useCanAddAnalyst(session: Session | null | undefined) {
|
|
71
|
+
const chat = session?.view === "chat" ? session : null;
|
|
72
|
+
const paired = useSessionAnalyst(chat?.id).data;
|
|
73
|
+
return (
|
|
74
|
+
!!chat &&
|
|
75
|
+
!chat.role &&
|
|
76
|
+
!chat.archived_at &&
|
|
77
|
+
(chat.host_id || "local") === "local" &&
|
|
78
|
+
chat.agent_role !== "analyst" &&
|
|
79
|
+
!paired
|
|
80
|
+
);
|
|
81
|
+
}
|
package/components/Chat/Code.tsx
CHANGED
|
@@ -12,6 +12,8 @@ import oneDark from "react-syntax-highlighter/dist/esm/styles/prism/one-dark";
|
|
|
12
12
|
import oneLight from "react-syntax-highlighter/dist/esm/styles/prism/one-light";
|
|
13
13
|
import { CopyButton } from "./CopyButton";
|
|
14
14
|
import { Mermaid } from "./Mermaid";
|
|
15
|
+
import { ReviewCard } from "@/components/Analyst/ReviewCard";
|
|
16
|
+
import { REVIEW_FENCE } from "@/lib/analyst/parseReview";
|
|
15
17
|
|
|
16
18
|
const ALIASES: Record<string, string> = {
|
|
17
19
|
sh: "bash",
|
|
@@ -117,6 +119,7 @@ export function CodeBlock({
|
|
|
117
119
|
language?: string;
|
|
118
120
|
}) {
|
|
119
121
|
if (language === "mermaid") return <Mermaid code={code} />;
|
|
122
|
+
if (language === REVIEW_FENCE) return <ReviewCard code={code} />;
|
|
120
123
|
return (
|
|
121
124
|
<div className="bg-foreground/[0.04] not-prose my-3 overflow-hidden rounded-xl">
|
|
122
125
|
<div
|
|
@@ -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,9 @@ 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 { ChatItemContext } from "./itemContext";
|
|
31
|
+
import { exportUi, exportUiActions, pickedRange } from "@/stores/exportUi";
|
|
27
32
|
import {
|
|
28
33
|
BookmarkButton,
|
|
29
34
|
TurnBookmark,
|
|
@@ -153,6 +158,71 @@ function SendStatus({ pending }: { pending: PendingStatus }) {
|
|
|
153
158
|
);
|
|
154
159
|
}
|
|
155
160
|
|
|
161
|
+
// The turn this message begins, while turns are being picked for an export.
|
|
162
|
+
function useExportTurn(itemId: string): number | null {
|
|
163
|
+
const ui = useSnapshot(exportUi);
|
|
164
|
+
if (!ui.selecting) return null;
|
|
165
|
+
return ui.turns.find((t) => t.id === itemId)?.n ?? null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// A tick in the gutter and a tinted row: tap anywhere on it to pick the turn.
|
|
169
|
+
function TurnTick({
|
|
170
|
+
turn,
|
|
171
|
+
children,
|
|
172
|
+
}: {
|
|
173
|
+
turn: number;
|
|
174
|
+
children: React.ReactNode;
|
|
175
|
+
}) {
|
|
176
|
+
const { pick } = useSnapshot(exportUi);
|
|
177
|
+
const range = pickedRange(pick);
|
|
178
|
+
const on = !!range && turn >= range.from && turn <= range.to;
|
|
179
|
+
return (
|
|
180
|
+
<div
|
|
181
|
+
className={cn(
|
|
182
|
+
"-mx-2 flex items-start gap-2 rounded-xl px-2",
|
|
183
|
+
on && "bg-primary/10"
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
role="checkbox"
|
|
189
|
+
aria-checked={on}
|
|
190
|
+
aria-label={`Turn ${turn}`}
|
|
191
|
+
onClick={() => exportUiActions.tap(turn)}
|
|
192
|
+
className={cn(
|
|
193
|
+
"mt-1 flex h-11 min-w-11 shrink-0 flex-col items-center justify-center rounded-md border text-[11px]",
|
|
194
|
+
on
|
|
195
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
196
|
+
: "text-muted-foreground"
|
|
197
|
+
)}
|
|
198
|
+
>
|
|
199
|
+
{on ? <Check className="h-4 w-4" /> : turn}
|
|
200
|
+
</button>
|
|
201
|
+
<div className="min-w-0 flex-1">{children}</div>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Export this message's turn and everything after it.
|
|
207
|
+
function ExportFromHere({ itemId }: { itemId: string }) {
|
|
208
|
+
const chat = useBookmarkChat();
|
|
209
|
+
if (!chat) return null;
|
|
210
|
+
return (
|
|
211
|
+
<button
|
|
212
|
+
type="button"
|
|
213
|
+
aria-label="Export from here"
|
|
214
|
+
title="Export from here"
|
|
215
|
+
onClick={(e) => {
|
|
216
|
+
e.stopPropagation();
|
|
217
|
+
exportUiActions.open(chat.sessionId, "", itemId);
|
|
218
|
+
}}
|
|
219
|
+
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"
|
|
220
|
+
>
|
|
221
|
+
<Download className="h-3.5 w-3.5" />
|
|
222
|
+
</button>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
156
226
|
export function UserMessage({
|
|
157
227
|
item,
|
|
158
228
|
onUndo,
|
|
@@ -162,6 +232,7 @@ export function UserMessage({
|
|
|
162
232
|
onUndo?: () => void;
|
|
163
233
|
pending?: PendingStatus;
|
|
164
234
|
}) {
|
|
235
|
+
const turn = useExportTurn(item.id);
|
|
165
236
|
const message = (
|
|
166
237
|
<div className="group flex flex-col items-end gap-1">
|
|
167
238
|
{item.from && (
|
|
@@ -204,6 +275,7 @@ export function UserMessage({
|
|
|
204
275
|
) : (
|
|
205
276
|
<div className="-mr-1.5 flex items-center">
|
|
206
277
|
{onUndo && <UndoButton onClick={onUndo} />}
|
|
278
|
+
<ExportFromHere itemId={item.id} />
|
|
207
279
|
<BookmarkButton itemId={item.id} />
|
|
208
280
|
</div>
|
|
209
281
|
)}
|
|
@@ -211,7 +283,8 @@ export function UserMessage({
|
|
|
211
283
|
);
|
|
212
284
|
// Not in the chat yet: nothing to star.
|
|
213
285
|
if (pending) return message;
|
|
214
|
-
|
|
286
|
+
const row = <BookmarkPress itemId={item.id}>{message}</BookmarkPress>;
|
|
287
|
+
return turn ? <TurnTick turn={turn}>{row}</TurnTick> : row;
|
|
215
288
|
}
|
|
216
289
|
|
|
217
290
|
export function AssistantMessage({
|
|
@@ -221,11 +294,17 @@ export function AssistantMessage({
|
|
|
221
294
|
item: Of<"assistant">;
|
|
222
295
|
onQuote?: (text: string) => void;
|
|
223
296
|
}) {
|
|
297
|
+
// A reply whose only content was a tool call AgentOS ran for it.
|
|
298
|
+
if (!item.streaming && !item.text.trim()) return null;
|
|
224
299
|
return (
|
|
225
300
|
<BookmarkPress itemId={item.id}>
|
|
226
301
|
<div className="group">
|
|
227
302
|
<div data-quotable className={cn(item.streaming && "streaming-cursor")}>
|
|
228
|
-
<
|
|
303
|
+
<ChatItemContext.Provider
|
|
304
|
+
value={{ itemId: item.id, streaming: !!item.streaming }}
|
|
305
|
+
>
|
|
306
|
+
<Markdown text={item.text} streaming={item.streaming} />
|
|
307
|
+
</ChatItemContext.Provider>
|
|
229
308
|
</div>
|
|
230
309
|
{!item.streaming && (
|
|
231
310
|
<div className="-ml-1.5 flex items-center">
|
|
@@ -300,18 +379,38 @@ export function Todos({ item }: { item: Of<"todos"> }) {
|
|
|
300
379
|
|
|
301
380
|
export function TurnEnd({ item }: { item: Of<"turn_end"> }) {
|
|
302
381
|
const secs = item.durationMs ? Math.round(item.durationMs / 1000) : null;
|
|
382
|
+
const early = !item.interrupted && item.ended;
|
|
303
383
|
const label = item.interrupted
|
|
304
384
|
? "Stopped"
|
|
305
|
-
:
|
|
306
|
-
?
|
|
307
|
-
|
|
385
|
+
: early
|
|
386
|
+
? secs !== null
|
|
387
|
+
? `Ended early in ${secs}s`
|
|
388
|
+
: "Ended early"
|
|
389
|
+
: secs !== null
|
|
390
|
+
? `Done in ${secs}s`
|
|
391
|
+
: "Done";
|
|
308
392
|
return (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
393
|
+
<>
|
|
394
|
+
{early && (
|
|
395
|
+
<div
|
|
396
|
+
role="status"
|
|
397
|
+
className="rounded-xl bg-amber-500/10 px-3.5 py-2.5 text-sm text-amber-700 dark:text-amber-300"
|
|
398
|
+
>
|
|
399
|
+
{early.notice}
|
|
400
|
+
{item.finishReason && (
|
|
401
|
+
<span className="mt-1 block font-mono text-[11px] opacity-80">
|
|
402
|
+
finish reason: {item.finishReason}
|
|
403
|
+
</span>
|
|
404
|
+
)}
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
<div className="group text-muted-foreground/60 flex items-center gap-3 font-mono text-[11px]">
|
|
408
|
+
<span className="bg-foreground/[0.06] h-px flex-1" />
|
|
409
|
+
{label}
|
|
410
|
+
<TurnBookmark turnEndId={item.id} />
|
|
411
|
+
<span className="bg-foreground/[0.06] h-px flex-1" />
|
|
412
|
+
</div>
|
|
413
|
+
</>
|
|
315
414
|
);
|
|
316
415
|
}
|
|
317
416
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
// The message a rendered block belongs to, for blocks that act on it (a
|
|
6
|
+
// review card finds its stored recommendation by the item's id).
|
|
7
|
+
export interface ChatItemRef {
|
|
8
|
+
itemId: string;
|
|
9
|
+
streaming: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ChatItemContext = createContext<ChatItemRef | null>(null);
|
|
13
|
+
|
|
14
|
+
export const useChatItem = () => useContext(ChatItemContext);
|
|
@@ -4,6 +4,7 @@ import { useEffect } from "react";
|
|
|
4
4
|
import {
|
|
5
5
|
Bot,
|
|
6
6
|
Cpu,
|
|
7
|
+
Eye,
|
|
7
8
|
FolderGit2,
|
|
8
9
|
GitBranch,
|
|
9
10
|
Flag,
|
|
@@ -21,6 +22,7 @@ import { useAgentStatusQuery } from "@/data/agents";
|
|
|
21
22
|
import { useProjectMachinesQuery } from "@/data/projects";
|
|
22
23
|
import { modelChoices, useModelsQuery } from "@/data/models";
|
|
23
24
|
import type { AgentProbe } from "@/lib/agents/probe";
|
|
25
|
+
import { RoleChips } from "./RoleChips";
|
|
24
26
|
import { ChipHeading, ChipItem, ChipMenu, ChipToggle } from "./Chip";
|
|
25
27
|
import {
|
|
26
28
|
DEFAULT_PRIORITY,
|
|
@@ -168,7 +170,7 @@ export function DraftChips({
|
|
|
168
170
|
</ChipMenu>
|
|
169
171
|
)}
|
|
170
172
|
|
|
171
|
-
{!draft.openPr && (
|
|
173
|
+
{!draft.openPr && !draft.agentRole && (
|
|
172
174
|
<ChipMenu icon={Bot} name="Agent" label={agentLabel(draft.agentType)}>
|
|
173
175
|
{AGENT_OPTIONS.map((o) => (
|
|
174
176
|
<ChipItem
|
|
@@ -189,6 +191,10 @@ export function DraftChips({
|
|
|
189
191
|
</ChipMenu>
|
|
190
192
|
)}
|
|
191
193
|
|
|
194
|
+
{!draft.openPr && local && (
|
|
195
|
+
<RoleChips draft={draft} onChange={onChange} />
|
|
196
|
+
)}
|
|
197
|
+
|
|
192
198
|
{project && local && git?.isGitRepo && (
|
|
193
199
|
<ChipMenu
|
|
194
200
|
icon={GitBranch}
|
|
@@ -247,6 +253,15 @@ export function DraftChips({
|
|
|
247
253
|
onChange={(openPr) => onChange({ openPr })}
|
|
248
254
|
/>
|
|
249
255
|
)}
|
|
256
|
+
{project && !draft.openPr && local && draft.agentRole !== "analyst" && (
|
|
257
|
+
<ChipToggle
|
|
258
|
+
icon={Eye}
|
|
259
|
+
label="With analyst"
|
|
260
|
+
on={!!draft.withAnalyst}
|
|
261
|
+
title="A second chat reviews each turn and drafts the next prompt for you to send"
|
|
262
|
+
onChange={(withAnalyst) => onChange({ withAnalyst })}
|
|
263
|
+
/>
|
|
264
|
+
)}
|
|
250
265
|
{project && draft.openPr && local && (
|
|
251
266
|
<ChipMenu
|
|
252
267
|
icon={Cpu}
|
|
@@ -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";
|
|
@@ -21,8 +23,10 @@ import type { StartWarning } from "@/lib/claims/startWarning";
|
|
|
21
23
|
import { usePanes } from "@/contexts/PaneContext";
|
|
22
24
|
import { setPendingPrompt } from "@/stores/initialPrompt";
|
|
23
25
|
import { draftsActions, draftsStore, newDraft } from "@/stores/drafts";
|
|
26
|
+
import { useStartAnalystBeside } from "@/components/Analyst/pair";
|
|
24
27
|
import { DraftChips } from "./DraftChips";
|
|
25
28
|
import { DraftHeading } from "./DraftHeading";
|
|
29
|
+
import { ModelUnavailableDialog } from "./ModelUnavailableDialog";
|
|
26
30
|
import { draftModels } from "./draftModels";
|
|
27
31
|
|
|
28
32
|
// A new session before its first send: choices on top, a composer below.
|
|
@@ -54,6 +58,7 @@ export function DraftPanel({
|
|
|
54
58
|
discovers
|
|
55
59
|
);
|
|
56
60
|
const launch = useLaunchSession();
|
|
61
|
+
const startAnalyst = useStartAnalystBeside();
|
|
57
62
|
const { workspace } = useSelectedWorkspace();
|
|
58
63
|
const createTask = useCreateTask();
|
|
59
64
|
const checkStart = useCheckStart();
|
|
@@ -63,6 +68,13 @@ export function DraftPanel({
|
|
|
63
68
|
text: string;
|
|
64
69
|
warnings: StartWarning[];
|
|
65
70
|
} | null>(null);
|
|
71
|
+
// A role's model server that didn't answer, with what was being sent.
|
|
72
|
+
const [down, setDown] = useState<{
|
|
73
|
+
error: ModelUnavailableError;
|
|
74
|
+
text: string;
|
|
75
|
+
images: ChatImage[];
|
|
76
|
+
files: ChatFile[];
|
|
77
|
+
} | null>(null);
|
|
66
78
|
const pending =
|
|
67
79
|
launch.isPending || createTask.isPending || checkStart.isPending;
|
|
68
80
|
|
|
@@ -81,7 +93,7 @@ export function DraftPanel({
|
|
|
81
93
|
text: string,
|
|
82
94
|
images: ChatImage[],
|
|
83
95
|
files: ChatFile[],
|
|
84
|
-
claim?: { after?: string }
|
|
96
|
+
claim?: { after?: string; onUnavailable?: "fallback" }
|
|
85
97
|
) => {
|
|
86
98
|
try {
|
|
87
99
|
if (draft.openPr) {
|
|
@@ -129,7 +141,17 @@ export function DraftPanel({
|
|
|
129
141
|
? draft.hostId
|
|
130
142
|
: undefined,
|
|
131
143
|
agentType: draft.agentType,
|
|
132
|
-
model: draft.
|
|
144
|
+
model: isAgentRole(draft.agentRole)
|
|
145
|
+
? (draft.roleModel ?? "")
|
|
146
|
+
: draft.model,
|
|
147
|
+
...(isAgentRole(draft.agentRole)
|
|
148
|
+
? {
|
|
149
|
+
agentRole: draft.agentRole,
|
|
150
|
+
...(claim?.onUnavailable && {
|
|
151
|
+
onUnavailable: claim.onUnavailable,
|
|
152
|
+
}),
|
|
153
|
+
}
|
|
154
|
+
: {}),
|
|
133
155
|
access: draft.access,
|
|
134
156
|
useWorktree: draft.useWorktree && !!git?.isGitRepo,
|
|
135
157
|
baseBranch: draft.baseBranch,
|
|
@@ -152,7 +174,16 @@ export function DraftPanel({
|
|
|
152
174
|
session.tmux_name ?? "",
|
|
153
175
|
session.host_id
|
|
154
176
|
);
|
|
177
|
+
if (draft.withAnalyst && project && draft.hostId === "local") {
|
|
178
|
+
if (session.view === "chat")
|
|
179
|
+
void startAnalyst.run({ coderSessionId: session.id }, paneId);
|
|
180
|
+
else toast("An analyst reviews chats: this session runs in a terminal");
|
|
181
|
+
}
|
|
155
182
|
} catch (error) {
|
|
183
|
+
if (error instanceof ModelUnavailableError) {
|
|
184
|
+
setDown({ error, text, images, files });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
156
187
|
setPrefill({ text, at: Date.now() });
|
|
157
188
|
toast.error(error instanceof Error ? error.message : String(error));
|
|
158
189
|
}
|
|
@@ -172,6 +203,31 @@ export function DraftPanel({
|
|
|
172
203
|
git={git}
|
|
173
204
|
onChange={(patch) => draftsActions.update(draft.id, patch)}
|
|
174
205
|
/>
|
|
206
|
+
<ModelUnavailableDialog
|
|
207
|
+
error={down?.error ?? null}
|
|
208
|
+
busy={pending}
|
|
209
|
+
onClose={() => {
|
|
210
|
+
if (down) setPrefill({ text: down.text, at: Date.now() });
|
|
211
|
+
setDown(null);
|
|
212
|
+
}}
|
|
213
|
+
onRetry={() => {
|
|
214
|
+
const d = down;
|
|
215
|
+
setDown(null);
|
|
216
|
+
if (d) void send(d.text, d.images, d.files);
|
|
217
|
+
}}
|
|
218
|
+
onFallback={() => {
|
|
219
|
+
const d = down;
|
|
220
|
+
setDown(null);
|
|
221
|
+
if (d)
|
|
222
|
+
void send(d.text, d.images, d.files, {
|
|
223
|
+
onUnavailable: "fallback",
|
|
224
|
+
});
|
|
225
|
+
}}
|
|
226
|
+
onPick={() => {
|
|
227
|
+
if (down) setPrefill({ text: down.text, at: Date.now() });
|
|
228
|
+
setDown(null);
|
|
229
|
+
}}
|
|
230
|
+
/>
|
|
175
231
|
{held && (
|
|
176
232
|
<StartClaimsNotice
|
|
177
233
|
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
|
+
}
|