@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.
- package/README.md +1 -0
- package/app/api/bus/ports/route.test.ts +43 -0
- package/app/api/bus/ports/route.ts +35 -0
- package/app/api/loops/route.ts +17 -0
- package/app/api/sessions/[id]/export/turn/route.test.ts +65 -0
- package/app/api/sessions/[id]/export/turn/route.ts +33 -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 +8 -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 +8 -1
- package/components/Chat/TurnCopy.tsx +84 -0
- package/components/Chat/itemContext.ts +14 -0
- package/components/Draft/DraftChips.tsx +10 -0
- package/components/Draft/DraftPanel.tsx +7 -0
- package/components/Pane/DesktopTabBar.tsx +6 -0
- package/components/Pane/MobileTabBar.tsx +17 -0
- package/components/Pane/MobileViewPicker.tsx +4 -0
- package/components/Pane/PaneMenu.test.ts +21 -0
- package/components/Pane/PaneMenu.tsx +16 -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/GroupShelves.tsx +57 -22
- package/components/SessionList/MoveToGroupMenu.test.ts +77 -0
- package/components/SessionList/SessionRow.tsx +12 -1
- package/components/SessionList/SessionRowMenu.tsx +6 -0
- package/components/SessionList/SidebarContextMenu.test.ts +21 -1
- package/components/SessionList/SidebarContextMenu.tsx +33 -9
- package/components/SessionList/index.tsx +11 -2
- package/components/SessionList/useGroupDrag.ts +83 -0
- package/components/Sessions/RolePill.tsx +7 -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/exports/queries.ts +11 -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/push/topics.ts +6 -0
- package/data/reports/index.ts +115 -0
- package/lib/agents/brief.ts +1 -1
- package/lib/analyst/forward.ts +1 -1
- package/lib/db/changes.ts +7 -0
- package/lib/db/migrations/20261021T1600_task_loops.ts +81 -0
- package/lib/db/migrations/20261021T1800_health_reports.ts +38 -0
- package/lib/done/index.ts +2 -0
- package/lib/drafts.ts +2 -0
- package/lib/export/markdown.ts +7 -1
- package/lib/export/turn.ts +48 -0
- package/lib/health/config.ts +2 -0
- package/lib/health/env/classes.ts +14 -1
- package/lib/health/port-wait-tick.ts +222 -0
- package/lib/health/port-wait.ts +160 -0
- package/lib/health/tick.ts +9 -2
- package/lib/health/types.ts +2 -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/plan-step.ts +4 -1
- package/lib/orchestrator/digest.ts +2 -1
- package/lib/orchestrator/loop-guard.ts +5 -2
- package/lib/orchestrator/pr-wake.ts +4 -0
- package/lib/orchestrator/watcher.ts +8 -1
- package/lib/panes.ts +52 -0
- package/lib/plans/acceptance.ts +23 -0
- package/lib/{ports.ts → ports/allocatePorts.ts} +30 -7
- package/lib/ports/ensurePorts.ts +138 -0
- package/lib/ports/findHardcodedPorts.ts +116 -0
- package/lib/ports/index.ts +15 -0
- package/lib/ports/listListeners.ts +134 -0
- package/lib/ports/reassignPort.ts +45 -0
- package/lib/ports/types.ts +62 -0
- package/lib/ports/whoHolds.ts +155 -0
- package/lib/project-config/brief.ts +4 -2
- package/lib/project-config/session.ts +19 -3
- 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/sidebar/groupDrop.ts +36 -0
- package/lib/sidebar/row-menu.ts +9 -0
- 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/move-carry.ts +1 -0
- package/lib/windows-ports.ts +36 -18
- package/package.json +2 -2
- 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 & 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
|
+
}
|