@saadnvd1/agent-os 0.3.723 → 0.3.731

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 (150) hide show
  1. package/README.md +1 -0
  2. package/app/api/bus/ports/route.test.ts +43 -0
  3. package/app/api/bus/ports/route.ts +35 -0
  4. package/app/api/loops/route.ts +17 -0
  5. package/app/api/sessions/[id]/export/turn/route.test.ts +65 -0
  6. package/app/api/sessions/[id]/export/turn/route.ts +33 -0
  7. package/app/api/tasks/[id]/loop/cycles/[n]/log/route.ts +29 -0
  8. package/app/api/tasks/[id]/loop/draft-criteria/route.ts +17 -0
  9. package/app/api/tasks/[id]/loop/next/route.ts +25 -0
  10. package/app/api/tasks/[id]/loop/route.ts +72 -0
  11. package/app/api/tasks/[id]/loop/run/route.ts +12 -0
  12. package/app/api/tasks/[id]/loop/stop/route.ts +12 -0
  13. package/app/api/tasks/[id]/mode/route.ts +29 -0
  14. package/app/api/tasks/[id]/report/draft/route.ts +54 -0
  15. package/app/api/tasks/[id]/report/evidence/[evId]/route.ts +18 -0
  16. package/app/api/tasks/[id]/report/route.ts +39 -0
  17. package/app/api/workspaces/[id]/mode/route.ts +23 -0
  18. package/app/api/workspaces/[id]/reports/route.ts +17 -0
  19. package/app/page.tsx +8 -0
  20. package/components/Analyst/PairChip.tsx +131 -0
  21. package/components/Analyst/PairSwitch.tsx +76 -0
  22. package/components/Analyst/ReviewCard.tsx +272 -0
  23. package/components/Analyst/SignalChips.tsx +39 -0
  24. package/components/Analyst/StartAnalystSheet.tsx +109 -0
  25. package/components/Analyst/cardState.test.ts +109 -0
  26. package/components/Analyst/cardState.ts +95 -0
  27. package/components/Analyst/pair.ts +81 -0
  28. package/components/Chat/Code.tsx +3 -0
  29. package/components/Chat/Messages.tsx +8 -1
  30. package/components/Chat/TurnCopy.tsx +84 -0
  31. package/components/Chat/itemContext.ts +14 -0
  32. package/components/Draft/DraftChips.tsx +10 -0
  33. package/components/Draft/DraftPanel.tsx +7 -0
  34. package/components/Pane/DesktopTabBar.tsx +6 -0
  35. package/components/Pane/MobileTabBar.tsx +17 -0
  36. package/components/Pane/MobileViewPicker.tsx +4 -0
  37. package/components/Pane/PaneMenu.test.ts +21 -0
  38. package/components/Pane/PaneMenu.tsx +16 -0
  39. package/components/Pane/PaneViewToggle.tsx +12 -0
  40. package/components/Pane/index.tsx +19 -0
  41. package/components/Report/DraftCard.tsx +89 -0
  42. package/components/Report/EstimateBar.tsx +51 -0
  43. package/components/Report/EvidenceSheet.tsx +98 -0
  44. package/components/Report/ModeChooser.tsx +43 -0
  45. package/components/Report/ReportView.tsx +289 -0
  46. package/components/SessionList/GroupShelves.tsx +57 -22
  47. package/components/SessionList/MoveToGroupMenu.test.ts +77 -0
  48. package/components/SessionList/SessionRow.tsx +12 -1
  49. package/components/SessionList/SessionRowMenu.tsx +6 -0
  50. package/components/SessionList/SidebarContextMenu.test.ts +21 -1
  51. package/components/SessionList/SidebarContextMenu.tsx +33 -9
  52. package/components/SessionList/index.tsx +11 -2
  53. package/components/SessionList/useGroupDrag.ts +83 -0
  54. package/components/Sessions/RolePill.tsx +7 -0
  55. package/components/Tasks/Loop/LoopButton.tsx +74 -0
  56. package/components/Tasks/Loop/LoopSetup.tsx +146 -0
  57. package/components/Tasks/Loop/LoopView.tsx +350 -0
  58. package/components/Tasks/Loop/labels.ts +17 -0
  59. package/components/Tasks/TaskRow.tsx +2 -0
  60. package/components/Workspaces/WorkspaceModeForm.tsx +22 -0
  61. package/components/Workspaces/WorkspaceSettings.tsx +2 -0
  62. package/components/ui/segmented.tsx +55 -0
  63. package/components/ui/status-pill.tsx +3 -0
  64. package/contexts/PaneContext.tsx +21 -1
  65. package/data/exports/queries.ts +11 -0
  66. package/data/loops/index.ts +3 -0
  67. package/data/loops/keys.ts +4 -0
  68. package/data/loops/mutations.ts +68 -0
  69. package/data/loops/queries.ts +40 -0
  70. package/data/push/topics.ts +6 -0
  71. package/data/reports/index.ts +115 -0
  72. package/lib/agents/brief.ts +1 -1
  73. package/lib/analyst/forward.ts +1 -1
  74. package/lib/db/changes.ts +7 -0
  75. package/lib/db/migrations/20261021T1600_task_loops.ts +81 -0
  76. package/lib/db/migrations/20261021T1800_health_reports.ts +38 -0
  77. package/lib/done/index.ts +2 -0
  78. package/lib/drafts.ts +2 -0
  79. package/lib/export/markdown.ts +7 -1
  80. package/lib/export/turn.ts +48 -0
  81. package/lib/health/config.ts +2 -0
  82. package/lib/health/env/classes.ts +14 -1
  83. package/lib/health/port-wait-tick.ts +222 -0
  84. package/lib/health/port-wait.ts +160 -0
  85. package/lib/health/tick.ts +9 -2
  86. package/lib/health/types.ts +2 -0
  87. package/lib/loops/analyse.ts +46 -0
  88. package/lib/loops/analyst-prompt.ts +203 -0
  89. package/lib/loops/checkCriteria.ts +114 -0
  90. package/lib/loops/compactEvidence.ts +140 -0
  91. package/lib/loops/controls.ts +317 -0
  92. package/lib/loops/decideStop.ts +113 -0
  93. package/lib/loops/deps.ts +128 -0
  94. package/lib/loops/diffMap.ts +39 -0
  95. package/lib/loops/draftCriteria.ts +71 -0
  96. package/lib/loops/gatherEvidence.ts +265 -0
  97. package/lib/loops/hooks.ts +40 -0
  98. package/lib/loops/index.ts +7 -0
  99. package/lib/loops/route.ts +83 -0
  100. package/lib/loops/runCycle.ts +323 -0
  101. package/lib/loops/screenInstruction.ts +59 -0
  102. package/lib/loops/sendInstruction.ts +83 -0
  103. package/lib/loops/snapshot.ts +38 -0
  104. package/lib/loops/stopLoop.ts +86 -0
  105. package/lib/loops/store.ts +481 -0
  106. package/lib/loops/summary.ts +27 -0
  107. package/lib/loops/tickLoops.ts +141 -0
  108. package/lib/loops/types.ts +223 -0
  109. package/lib/loops/world.test-helpers.ts +223 -0
  110. package/lib/models/plan-step.ts +4 -1
  111. package/lib/orchestrator/digest.ts +2 -1
  112. package/lib/orchestrator/loop-guard.ts +5 -2
  113. package/lib/orchestrator/pr-wake.ts +4 -0
  114. package/lib/orchestrator/watcher.ts +8 -1
  115. package/lib/panes.ts +52 -0
  116. package/lib/plans/acceptance.ts +23 -0
  117. package/lib/{ports.ts → ports/allocatePorts.ts} +30 -7
  118. package/lib/ports/ensurePorts.ts +138 -0
  119. package/lib/ports/findHardcodedPorts.ts +116 -0
  120. package/lib/ports/index.ts +15 -0
  121. package/lib/ports/listListeners.ts +134 -0
  122. package/lib/ports/reassignPort.ts +45 -0
  123. package/lib/ports/types.ts +62 -0
  124. package/lib/ports/whoHolds.ts +155 -0
  125. package/lib/project-config/brief.ts +4 -2
  126. package/lib/project-config/session.ts +19 -3
  127. package/lib/reports/drafts.ts +87 -0
  128. package/lib/reports/end-hook.ts +17 -0
  129. package/lib/reports/estimate.ts +107 -0
  130. package/lib/reports/evidence/build.ts +193 -0
  131. package/lib/reports/evidence/gather.ts +229 -0
  132. package/lib/reports/fixtures.ts +17 -0
  133. package/lib/reports/generate.ts +159 -0
  134. package/lib/reports/hooks.ts +30 -0
  135. package/lib/reports/mode.ts +65 -0
  136. package/lib/reports/model.ts +115 -0
  137. package/lib/reports/read.ts +26 -0
  138. package/lib/reports/store.ts +180 -0
  139. package/lib/reports/template.ts +117 -0
  140. package/lib/reports/types.ts +135 -0
  141. package/lib/reports/validate.ts +142 -0
  142. package/lib/sidebar/groupDrop.ts +36 -0
  143. package/lib/sidebar/row-menu.ts +9 -0
  144. package/lib/tasks/contract/routes.ts +5 -0
  145. package/lib/tasks/contract/types.ts +3 -0
  146. package/lib/tasks/finish.ts +2 -0
  147. package/lib/tasks/move-carry.ts +1 -0
  148. package/lib/windows-ports.ts +36 -18
  149. package/package.json +2 -2
  150. package/stores/analystUi.ts +19 -0
@@ -12,6 +12,7 @@ import {
12
12
  SplitSquareHorizontal,
13
13
  SplitSquareVertical,
14
14
  Star,
15
+ Eye,
15
16
  Unplug,
16
17
  X,
17
18
  } from "lucide-react";
@@ -29,6 +30,8 @@ import { copySessionLink, writeClipboard } from "@/hooks/useCopyToClipboard";
29
30
  import type { Session } from "@/lib/db";
30
31
  import { shareUiActions } from "@/stores/shareUi";
31
32
  import { exportUiActions } from "@/stores/exportUi";
33
+ import { analystUiActions } from "@/stores/analystUi";
34
+ import { useCanAddAnalyst } from "@/components/Analyst/pair";
32
35
  import { useSessionBookmarks } from "@/data/bookmarks";
33
36
  import { useDocsWorkspace } from "@/data/lumifyhub/docs";
34
37
  import { bookmarksUiActions } from "@/stores/bookmarksUi";
@@ -38,6 +41,7 @@ import { MoveMenuItems } from "@/components/Tasks/MoveMenuItems";
38
41
 
39
42
  interface PaneMenuProps {
40
43
  session: Session | null | undefined;
44
+ paneId?: string;
41
45
  // The chat on show, for its bookmarks; null for a terminal or another view.
42
46
  bookmarksSessionId?: string | null;
43
47
  canSplit: boolean;
@@ -53,6 +57,7 @@ interface PaneMenuProps {
53
57
  // the bar.
54
58
  export function PaneMenu({
55
59
  session,
60
+ paneId,
56
61
  bookmarksSessionId,
57
62
  canSplit,
58
63
  canClose,
@@ -64,6 +69,7 @@ export function PaneMenu({
64
69
  }: PaneMenuProps) {
65
70
  const { data: bookmarks = [] } = useSessionBookmarks(bookmarksSessionId);
66
71
  const docs = useDocsWorkspace(session?.project_id);
72
+ const canAddAnalyst = useCanAddAnalyst(session);
67
73
  const copyId = async (id: string) => {
68
74
  try {
69
75
  await writeClipboard(id);
@@ -127,6 +133,16 @@ export function PaneMenu({
127
133
  Export for AI…
128
134
  </DropdownMenuItem>
129
135
  )}
136
+ {canAddAnalyst && session && (
137
+ <DropdownMenuItem
138
+ onSelect={() =>
139
+ analystUiActions.open(session.id, session.name, paneId)
140
+ }
141
+ >
142
+ <Eye className="h-4 w-4" />
143
+ Add analyst…
144
+ </DropdownMenuItem>
145
+ )}
130
146
  <DropdownMenuSeparator />
131
147
  <MoveMenuItems
132
148
  session={session}
@@ -6,6 +6,7 @@ import {
6
6
  Home,
7
7
  Package,
8
8
  ScrollText,
9
+ ClipboardList,
9
10
  Users,
10
11
  } from "lucide-react";
11
12
  import {
@@ -27,6 +28,7 @@ interface PaneViewToggleProps {
27
28
  isConductor: boolean;
28
29
  // The task merged and has a log.
29
30
  hasLog?: boolean;
31
+ hasReport?: boolean;
30
32
  workerCount: number;
31
33
  gitDrawerOpen: boolean;
32
34
  shellDrawerOpen: boolean;
@@ -85,6 +87,7 @@ export function PaneViewToggle({
85
87
  showGit,
86
88
  isConductor,
87
89
  hasLog,
90
+ hasReport,
88
91
  workerCount,
89
92
  gitDrawerOpen,
90
93
  shellDrawerOpen,
@@ -138,6 +141,15 @@ export function PaneViewToggle({
138
141
  <Package className="h-3.5 w-3.5" />
139
142
  </ToggleButton>
140
143
  )}
144
+ {hasReport && (
145
+ <ToggleButton
146
+ label="Report"
147
+ active={viewMode === "report"}
148
+ onClick={() => onViewModeChange("report")}
149
+ >
150
+ <ClipboardList className="h-3.5 w-3.5" />
151
+ </ToggleButton>
152
+ )}
141
153
  {hasLog && (
142
154
  <ToggleButton
143
155
  label="Log"
@@ -16,6 +16,7 @@ import { cn } from "@/lib/utils";
16
16
  import { ConductorPanel } from "@/components/ConductorPanel";
17
17
  import { useFileEditor } from "@/hooks/useFileEditor";
18
18
  import { MobileTabBar } from "./MobileTabBar";
19
+ import { PairSwitch } from "@/components/Analyst/PairSwitch";
19
20
  import { openFind } from "@/components/Chat/useFind";
20
21
  import { DesktopTabBar } from "./DesktopTabBar";
21
22
  import {
@@ -46,6 +47,10 @@ const ArtifactsPanel = dynamic(
46
47
  ),
47
48
  { ssr: false }
48
49
  );
50
+ const ReportView = dynamic(
51
+ () => import("@/components/Report/ReportView").then((mod) => mod.ReportView),
52
+ { ssr: false }
53
+ );
49
54
  const LogDocument = dynamic(
50
55
  () =>
51
56
  import("@/components/SessionLogs/LogDocument").then(
@@ -215,6 +220,7 @@ export const Pane = memo(function Pane({
215
220
  !!session?.task_prompt &&
216
221
  (session.task_status === "merged" || session.pr_status === "merged");
217
222
  const hasLog = !!useTaskLogQuery(session?.id, merged).data;
223
+ const hasReport = !!session?.task_prompt;
218
224
 
219
225
  // Get current project and its repositories
220
226
  const currentProject = useMemo(() => {
@@ -471,6 +477,7 @@ export const Pane = memo(function Pane({
471
477
  viewMode={viewMode}
472
478
  isConductor={isConductor}
473
479
  hasLog={hasLog}
480
+ hasReport={hasReport}
474
481
  workerCount={workerCount}
475
482
  showGit={showGit}
476
483
  onMenuClick={onMenuClick}
@@ -494,6 +501,7 @@ export const Pane = memo(function Pane({
494
501
  isFocused={isFocused}
495
502
  isConductor={isConductor}
496
503
  hasLog={hasLog}
504
+ hasReport={hasReport}
497
505
  workerCount={workerCount}
498
506
  canSplit={canSplit}
499
507
  canClose={canClose}
@@ -514,6 +522,7 @@ export const Pane = memo(function Pane({
514
522
  bookmarksSessionId={bookmarksSessionId}
515
523
  />
516
524
  )}
525
+ {isMobile && <PairSwitch session={session} paneId={paneId} />}
517
526
 
518
527
  {/* Content Area - Mobile: simple flex, Desktop: resizable panels */}
519
528
  {isMobile ? (
@@ -558,6 +567,11 @@ export const Pane = memo(function Pane({
558
567
  </div>
559
568
  )}
560
569
 
570
+ {/* Report */}
571
+ {viewMode === "report" && session && (
572
+ <ReportView sessionId={session.id} name={session.name} />
573
+ )}
574
+
561
575
  {/* Log */}
562
576
  {viewMode === "log" && session && (
563
577
  <LogDocument sessionId={session.id} />
@@ -629,6 +643,11 @@ export const Pane = memo(function Pane({
629
643
  </div>
630
644
  )}
631
645
 
646
+ {/* Report */}
647
+ {viewMode === "report" && session && (
648
+ <ReportView sessionId={session.id} name={session.name} />
649
+ )}
650
+
632
651
  {/* Log */}
633
652
  {viewMode === "log" && session && (
634
653
  <LogDocument sessionId={session.id} />
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Textarea } from "@/components/ui/textarea";
6
+ import { useReportDraft } from "@/data/reports";
7
+ import type { NextPromptDraft } from "@/lib/reports/types";
8
+
9
+ // The analyst's drafted next prompt, shown in full. Approve & send is the one
10
+ // click; nothing is sent without it.
11
+ export function DraftCard({
12
+ sessionId,
13
+ draft,
14
+ }: {
15
+ sessionId: string;
16
+ draft: NextPromptDraft;
17
+ }) {
18
+ const act = useReportDraft(sessionId);
19
+ const [editing, setEditing] = useState(false);
20
+ const [text, setText] = useState(draft.text);
21
+ const ref = { source: draft.source, id: draft.id, linkId: draft.linkId };
22
+ const decided = draft.status !== "pending";
23
+
24
+ if (decided)
25
+ return (
26
+ <p className="text-muted-foreground bg-muted/40 rounded-lg px-3 py-2 text-sm">
27
+ {draft.status === "sent"
28
+ ? "Sent to the coding agent."
29
+ : draft.status === "dismissed"
30
+ ? "Draft dismissed."
31
+ : "A newer report replaced this draft."}
32
+ </p>
33
+ );
34
+
35
+ return (
36
+ <section
37
+ aria-label="Drafted next prompt"
38
+ className="bg-card space-y-3 rounded-xl p-4 shadow-sm"
39
+ >
40
+ <h3 className="text-sm font-semibold">Drafted next prompt</h3>
41
+ {editing ? (
42
+ <Textarea
43
+ value={text}
44
+ onChange={(e) => setText(e.target.value)}
45
+ rows={6}
46
+ aria-label="Edit the prompt"
47
+ />
48
+ ) : (
49
+ <p className="bg-muted/50 rounded-lg p-3 text-sm whitespace-pre-wrap">
50
+ {draft.text}
51
+ </p>
52
+ )}
53
+ {act.isError && (
54
+ <p className="text-destructive text-sm">{act.error.message}</p>
55
+ )}
56
+ <div className="flex flex-wrap gap-2">
57
+ <Button
58
+ className="min-h-11 flex-1"
59
+ disabled={act.isPending || !text.trim()}
60
+ onClick={() =>
61
+ act.mutate({
62
+ ...ref,
63
+ action: "send",
64
+ text: editing ? text : undefined,
65
+ })
66
+ }
67
+ >
68
+ Approve &amp; send
69
+ </Button>
70
+ <Button
71
+ variant="secondary"
72
+ className="min-h-11"
73
+ disabled={act.isPending}
74
+ onClick={() => setEditing((e) => !e)}
75
+ >
76
+ {editing ? "Cancel edit" : "Edit"}
77
+ </Button>
78
+ <Button
79
+ variant="ghost"
80
+ className="min-h-11"
81
+ disabled={act.isPending}
82
+ onClick={() => act.mutate({ ...ref, action: "dismiss" })}
83
+ >
84
+ Dismiss
85
+ </Button>
86
+ </div>
87
+ </section>
88
+ );
89
+ }
@@ -0,0 +1,51 @@
1
+ import type { Estimate } from "@/lib/reports/types";
2
+ import { estimateLabel, estimatePercent } from "@/lib/reports/types";
3
+
4
+ // The percentage never shows without its label: it is an estimate, and the
5
+ // label says how it was counted.
6
+ export function EstimateBar({ estimate }: { estimate: Estimate }) {
7
+ const pct = estimatePercent(estimate);
8
+ const how =
9
+ estimate.basis === "acceptance"
10
+ ? "Acceptance items the plan listed, counted when a passing test, check or review backs them."
11
+ : "The plan has no acceptance items, so this counts delivery stages: plan, push, pull request, CI, review, merge.";
12
+ return (
13
+ <div className="space-y-2" title={how}>
14
+ <div className="flex items-baseline justify-between gap-3">
15
+ <span className="text-muted-foreground text-xs">
16
+ {estimateLabel(estimate)}
17
+ </span>
18
+ <span className="text-sm font-semibold tabular-nums">{pct}%</span>
19
+ </div>
20
+ <div
21
+ role="progressbar"
22
+ aria-valuemin={0}
23
+ aria-valuemax={estimate.total}
24
+ aria-valuenow={estimate.done}
25
+ aria-label={estimateLabel(estimate)}
26
+ className="bg-muted h-2 overflow-hidden rounded-full"
27
+ >
28
+ <div
29
+ className="bg-primary h-full rounded-full"
30
+ style={{ width: `${pct}%` }}
31
+ />
32
+ </div>
33
+ <details className="text-xs">
34
+ <summary className="text-muted-foreground flex min-h-11 cursor-pointer items-center">
35
+ How this is counted
36
+ </summary>
37
+ <p className="text-muted-foreground pb-2">{how}</p>
38
+ <ul className="space-y-1 pb-1">
39
+ {estimate.items.map((i) => (
40
+ <li key={i.label} className="flex gap-2">
41
+ <span aria-hidden>{i.verified ? "✓" : "○"}</span>
42
+ <span className={i.verified ? "" : "text-muted-foreground"}>
43
+ {i.label}
44
+ </span>
45
+ </li>
46
+ ))}
47
+ </ul>
48
+ </details>
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ import { ExternalLink } from "lucide-react";
4
+ import {
5
+ ASheet,
6
+ ASheetContent,
7
+ ASheetDescription,
8
+ ASheetHeader,
9
+ ASheetTitle,
10
+ } from "@/components/a/ASheet";
11
+ import { StatusPill } from "@/components/ui/status-pill";
12
+ import { useMediaQuery } from "@/hooks/useMediaQuery";
13
+ import type { EvidenceRef } from "@/lib/reports/types";
14
+
15
+ const TONE = {
16
+ pass: "green",
17
+ fail: "red",
18
+ pending: "amber",
19
+ info: "muted",
20
+ } as const;
21
+ const WORD = {
22
+ pass: "Passed",
23
+ fail: "Failed",
24
+ pending: "Waiting",
25
+ info: "Info",
26
+ } as const;
27
+
28
+ // What a report line rests on. The text is untrusted output: shown, never run.
29
+ export function EvidenceSheet({
30
+ evidence,
31
+ stale,
32
+ onClose,
33
+ }: {
34
+ evidence: EvidenceRef | null;
35
+ stale: boolean;
36
+ onClose: () => void;
37
+ }) {
38
+ const wide = useMediaQuery("(min-width: 768px)");
39
+ return (
40
+ <ASheet open={!!evidence} onOpenChange={(o) => !o && onClose()}>
41
+ <ASheetContent
42
+ side={wide ? "right" : "bottom"}
43
+ className={wide ? "w-full max-w-md" : "max-h-[80dvh] rounded-t-2xl"}
44
+ >
45
+ {evidence && (
46
+ <>
47
+ <ASheetHeader className="pr-12">
48
+ <ASheetTitle className="break-words">
49
+ {evidence.label}
50
+ </ASheetTitle>
51
+ <ASheetDescription className="flex flex-wrap items-center gap-2">
52
+ <StatusPill tone={TONE[evidence.status]}>
53
+ {WORD[evidence.status]}
54
+ </StatusPill>
55
+ <span>{evidence.kind}</span>
56
+ {evidence.sha && (
57
+ <span className="font-mono">{evidence.sha.slice(0, 7)}</span>
58
+ )}
59
+ {stale && evidence.sha && <span>older push</span>}
60
+ </ASheetDescription>
61
+ </ASheetHeader>
62
+ <div className="min-h-0 flex-1 space-y-3 overflow-y-auto px-4 pb-6">
63
+ {evidence.excerpt ? (
64
+ <pre className="bg-muted/60 max-h-[50dvh] overflow-auto rounded-lg p-3 text-xs break-words whitespace-pre-wrap">
65
+ {evidence.excerpt}
66
+ </pre>
67
+ ) : (
68
+ <p className="text-muted-foreground text-sm">
69
+ No output was kept for this one.
70
+ </p>
71
+ )}
72
+ <div className="flex flex-wrap gap-2">
73
+ {evidence.url && (
74
+ <a
75
+ href={evidence.url}
76
+ target="_blank"
77
+ rel="noopener noreferrer"
78
+ className="bg-secondary inline-flex min-h-11 items-center gap-1.5 rounded-lg px-3 text-sm"
79
+ >
80
+ Open in CI <ExternalLink className="h-3.5 w-3.5" />
81
+ </a>
82
+ )}
83
+ {evidence.href && (
84
+ <a
85
+ href={evidence.href}
86
+ className="bg-secondary inline-flex min-h-11 items-center rounded-lg px-3 text-sm"
87
+ >
88
+ Show in the task
89
+ </a>
90
+ )}
91
+ </div>
92
+ </div>
93
+ </>
94
+ )}
95
+ </ASheetContent>
96
+ </ASheet>
97
+ );
98
+ }
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import { Segmented } from "@/components/ui/segmented";
4
+ import {
5
+ MODE_COPY,
6
+ OPERATING_MODES,
7
+ type OperatingMode,
8
+ } from "@/lib/reports/types";
9
+
10
+ const OPTIONS = OPERATING_MODES.map((m) => ({
11
+ value: m,
12
+ label: MODE_COPY[m].label,
13
+ hint: MODE_COPY[m].blurb,
14
+ }));
15
+
16
+ // Manual / Assisted / Autonomous, with what the chosen one does underneath.
17
+ export function ModeChooser({
18
+ mode,
19
+ onChange,
20
+ disabled,
21
+ note,
22
+ }: {
23
+ mode: OperatingMode;
24
+ onChange: (m: OperatingMode) => void;
25
+ disabled?: boolean;
26
+ note?: string;
27
+ }) {
28
+ return (
29
+ <div className="space-y-1.5">
30
+ <Segmented
31
+ label="Operating mode"
32
+ value={mode}
33
+ options={OPTIONS}
34
+ onChange={onChange}
35
+ disabled={disabled}
36
+ />
37
+ <p className="text-muted-foreground text-xs">
38
+ {MODE_COPY[mode].blurb}
39
+ {note ? ` ${note}` : ""}
40
+ </p>
41
+ </div>
42
+ );
43
+ }