@saadnvd1/agent-os 0.3.723 → 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 +1 -0
- package/app/api/loops/route.ts +17 -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 +6 -1
- 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/SessionRowMenu.tsx +6 -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/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/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/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/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/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/package.json +2 -2
- package/stores/analystUi.ts +19 -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
|
|
@@ -27,6 +27,7 @@ import { openSessionLink } from "./session-link";
|
|
|
27
27
|
import { UndoButton } from "./Undo";
|
|
28
28
|
import { useRowState } from "./rowState";
|
|
29
29
|
import { useBookmarkChat } from "@/components/Bookmarks/context";
|
|
30
|
+
import { ChatItemContext } from "./itemContext";
|
|
30
31
|
import { exportUi, exportUiActions, pickedRange } from "@/stores/exportUi";
|
|
31
32
|
import {
|
|
32
33
|
BookmarkButton,
|
|
@@ -299,7 +300,11 @@ export function AssistantMessage({
|
|
|
299
300
|
<BookmarkPress itemId={item.id}>
|
|
300
301
|
<div className="group">
|
|
301
302
|
<div data-quotable className={cn(item.streaming && "streaming-cursor")}>
|
|
302
|
-
<
|
|
303
|
+
<ChatItemContext.Provider
|
|
304
|
+
value={{ itemId: item.id, streaming: !!item.streaming }}
|
|
305
|
+
>
|
|
306
|
+
<Markdown text={item.text} streaming={item.streaming} />
|
|
307
|
+
</ChatItemContext.Provider>
|
|
303
308
|
</div>
|
|
304
309
|
{!item.streaming && (
|
|
305
310
|
<div className="-ml-1.5 flex items-center">
|
|
@@ -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,
|
|
@@ -252,6 +253,15 @@ export function DraftChips({
|
|
|
252
253
|
onChange={(openPr) => onChange({ openPr })}
|
|
253
254
|
/>
|
|
254
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
|
+
)}
|
|
255
265
|
{project && draft.openPr && local && (
|
|
256
266
|
<ChipMenu
|
|
257
267
|
icon={Cpu}
|
|
@@ -23,6 +23,7 @@ import type { StartWarning } from "@/lib/claims/startWarning";
|
|
|
23
23
|
import { usePanes } from "@/contexts/PaneContext";
|
|
24
24
|
import { setPendingPrompt } from "@/stores/initialPrompt";
|
|
25
25
|
import { draftsActions, draftsStore, newDraft } from "@/stores/drafts";
|
|
26
|
+
import { useStartAnalystBeside } from "@/components/Analyst/pair";
|
|
26
27
|
import { DraftChips } from "./DraftChips";
|
|
27
28
|
import { DraftHeading } from "./DraftHeading";
|
|
28
29
|
import { ModelUnavailableDialog } from "./ModelUnavailableDialog";
|
|
@@ -57,6 +58,7 @@ export function DraftPanel({
|
|
|
57
58
|
discovers
|
|
58
59
|
);
|
|
59
60
|
const launch = useLaunchSession();
|
|
61
|
+
const startAnalyst = useStartAnalystBeside();
|
|
60
62
|
const { workspace } = useSelectedWorkspace();
|
|
61
63
|
const createTask = useCreateTask();
|
|
62
64
|
const checkStart = useCheckStart();
|
|
@@ -172,6 +174,11 @@ export function DraftPanel({
|
|
|
172
174
|
session.tmux_name ?? "",
|
|
173
175
|
session.host_id
|
|
174
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
|
+
}
|
|
175
182
|
} catch (error) {
|
|
176
183
|
if (error instanceof ModelUnavailableError) {
|
|
177
184
|
setDown({ error, text, images, files });
|
|
@@ -16,6 +16,7 @@ import { PaneViewToggle } from "./PaneViewToggle";
|
|
|
16
16
|
import { PaneMenu } from "./PaneMenu";
|
|
17
17
|
import { HostBadge } from "@/components/Hosts/HostBadge";
|
|
18
18
|
import { RolePill } from "@/components/Sessions/RolePill";
|
|
19
|
+
import { PairChip } from "@/components/Analyst/PairChip";
|
|
19
20
|
import { usePaneBarSlots } from "./PaneBarSlots";
|
|
20
21
|
import { ChatBookmarksButton } from "@/components/Bookmarks/ChatBookmarksButton";
|
|
21
22
|
import type { ViewMode } from "@/contexts/PaneContext";
|
|
@@ -37,6 +38,7 @@ interface DesktopTabBarProps {
|
|
|
37
38
|
isFocused: boolean;
|
|
38
39
|
isConductor: boolean;
|
|
39
40
|
hasLog?: boolean;
|
|
41
|
+
hasReport?: boolean;
|
|
40
42
|
workerCount: number;
|
|
41
43
|
canSplit: boolean;
|
|
42
44
|
canClose: boolean;
|
|
@@ -68,6 +70,7 @@ export function DesktopTabBar({
|
|
|
68
70
|
isFocused,
|
|
69
71
|
isConductor,
|
|
70
72
|
hasLog,
|
|
73
|
+
hasReport,
|
|
71
74
|
workerCount,
|
|
72
75
|
canSplit,
|
|
73
76
|
canClose,
|
|
@@ -163,6 +166,7 @@ export function DesktopTabBar({
|
|
|
163
166
|
|
|
164
167
|
<div className="flex shrink-0 items-center gap-1.5">
|
|
165
168
|
<RolePill session={session} paneId={paneId} />
|
|
169
|
+
<PairChip session={session} paneId={paneId} />
|
|
166
170
|
<HostBadge hostId={session?.host_id} />
|
|
167
171
|
<ContextMeter sessionId={session?.id} />
|
|
168
172
|
<SessionPrStage sessionId={session?.id} />
|
|
@@ -182,6 +186,7 @@ export function DesktopTabBar({
|
|
|
182
186
|
isLocalSession={isLocalSession}
|
|
183
187
|
isConductor={isConductor}
|
|
184
188
|
hasLog={hasLog}
|
|
189
|
+
hasReport={hasReport}
|
|
185
190
|
workerCount={workerCount}
|
|
186
191
|
showGit={showGit}
|
|
187
192
|
gitDrawerOpen={gitDrawerOpen}
|
|
@@ -194,6 +199,7 @@ export function DesktopTabBar({
|
|
|
194
199
|
<div className="flex items-center">
|
|
195
200
|
<PaneMenu
|
|
196
201
|
session={session}
|
|
202
|
+
paneId={paneId}
|
|
197
203
|
bookmarksSessionId={bookmarksSessionId}
|
|
198
204
|
canSplit={canSplit}
|
|
199
205
|
canClose={canClose}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
Share2,
|
|
27
27
|
Download,
|
|
28
28
|
TextSearch,
|
|
29
|
+
Eye,
|
|
29
30
|
} from "lucide-react";
|
|
30
31
|
import { cn } from "@/lib/utils";
|
|
31
32
|
import { useAwayQuery } from "@/data/away";
|
|
@@ -36,6 +37,8 @@ import { RolePill } from "@/components/Sessions/RolePill";
|
|
|
36
37
|
import { bookmarksUiActions } from "@/stores/bookmarksUi";
|
|
37
38
|
import { shareUiActions } from "@/stores/shareUi";
|
|
38
39
|
import { exportUiActions } from "@/stores/exportUi";
|
|
40
|
+
import { analystUiActions } from "@/stores/analystUi";
|
|
41
|
+
import { useCanAddAnalyst } from "@/components/Analyst/pair";
|
|
39
42
|
import { MoveMenuItems } from "@/components/Tasks/MoveMenuItems";
|
|
40
43
|
import { ChatBookmarksButton } from "@/components/Bookmarks/ChatBookmarksButton";
|
|
41
44
|
import type { ViewMode } from "@/contexts/PaneContext";
|
|
@@ -50,6 +53,7 @@ interface MobileTabBarProps {
|
|
|
50
53
|
viewMode: ViewMode;
|
|
51
54
|
isConductor: boolean;
|
|
52
55
|
hasLog?: boolean;
|
|
56
|
+
hasReport?: boolean;
|
|
53
57
|
workerCount: number;
|
|
54
58
|
// Local, and the folder is not known to be outside a git repository.
|
|
55
59
|
showGit: boolean;
|
|
@@ -70,6 +74,7 @@ export function MobileTabBar({
|
|
|
70
74
|
viewMode,
|
|
71
75
|
isConductor,
|
|
72
76
|
hasLog,
|
|
77
|
+
hasReport,
|
|
73
78
|
workerCount,
|
|
74
79
|
showGit,
|
|
75
80
|
onMenuClick,
|
|
@@ -128,6 +133,7 @@ export function MobileTabBar({
|
|
|
128
133
|
};
|
|
129
134
|
|
|
130
135
|
const isLocalSession = !session?.host_id || session.host_id === "local";
|
|
136
|
+
const canAddAnalyst = useCanAddAnalyst(session);
|
|
131
137
|
|
|
132
138
|
return (
|
|
133
139
|
<div
|
|
@@ -230,6 +236,16 @@ export function MobileTabBar({
|
|
|
230
236
|
<Download className="h-4 w-4" />
|
|
231
237
|
Export for AI…
|
|
232
238
|
</DropdownMenuItem>
|
|
239
|
+
{canAddAnalyst && (
|
|
240
|
+
<DropdownMenuItem
|
|
241
|
+
onSelect={() =>
|
|
242
|
+
analystUiActions.open(session.id, session.name)
|
|
243
|
+
}
|
|
244
|
+
>
|
|
245
|
+
<Eye className="h-4 w-4" />
|
|
246
|
+
Add analyst…
|
|
247
|
+
</DropdownMenuItem>
|
|
248
|
+
)}
|
|
233
249
|
<DropdownMenuSeparator />
|
|
234
250
|
</>
|
|
235
251
|
)}
|
|
@@ -375,6 +391,7 @@ export function MobileTabBar({
|
|
|
375
391
|
isLocalSession={isLocalSession}
|
|
376
392
|
showGit={showGit}
|
|
377
393
|
hasLog={hasLog}
|
|
394
|
+
hasReport={hasReport}
|
|
378
395
|
isConductor={isConductor}
|
|
379
396
|
workerCount={workerCount}
|
|
380
397
|
/>
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
GitBranch,
|
|
8
8
|
Package,
|
|
9
9
|
ScrollText,
|
|
10
|
+
ClipboardList,
|
|
10
11
|
Terminal as TerminalIcon,
|
|
11
12
|
Users,
|
|
12
13
|
type LucideIcon,
|
|
@@ -31,6 +32,7 @@ export interface MobileViewAvailability {
|
|
|
31
32
|
// Local, and the folder is not known to be outside a git repository.
|
|
32
33
|
showGit: boolean;
|
|
33
34
|
hasLog?: boolean;
|
|
35
|
+
hasReport?: boolean;
|
|
34
36
|
isConductor: boolean;
|
|
35
37
|
workerCount: number;
|
|
36
38
|
}
|
|
@@ -40,6 +42,7 @@ export function mobileViews({
|
|
|
40
42
|
isLocalSession,
|
|
41
43
|
showGit,
|
|
42
44
|
hasLog,
|
|
45
|
+
hasReport,
|
|
43
46
|
isConductor,
|
|
44
47
|
workerCount,
|
|
45
48
|
}: MobileViewAvailability): MobileView[] {
|
|
@@ -48,6 +51,7 @@ export function mobileViews({
|
|
|
48
51
|
isLocalSession && { mode: "files", label: "Files", icon: FolderOpen },
|
|
49
52
|
showGit && { mode: "git", label: "Git", icon: GitBranch },
|
|
50
53
|
{ mode: "artifacts", label: "Artifacts", icon: Package },
|
|
54
|
+
!!hasReport && { mode: "report", label: "Report", icon: ClipboardList },
|
|
51
55
|
!!hasLog && { mode: "log", label: "Log", icon: ScrollText },
|
|
52
56
|
isConductor && {
|
|
53
57
|
mode: "workers",
|
|
@@ -6,10 +6,12 @@ import type { Session } from "@/lib/db/types";
|
|
|
6
6
|
import { bookmarksUi } from "@/stores/bookmarksUi";
|
|
7
7
|
import { docsUi } from "@/stores/docsUi";
|
|
8
8
|
import { paletteUi } from "@/stores/palette";
|
|
9
|
+
import { analystUi } from "@/stores/analystUi";
|
|
9
10
|
|
|
10
11
|
const data = vi.hoisted(() => ({
|
|
11
12
|
bookmarks: [] as unknown[],
|
|
12
13
|
docs: null as { id: string } | null,
|
|
14
|
+
canAddAnalyst: false,
|
|
13
15
|
}));
|
|
14
16
|
vi.mock("@/data/bookmarks", () => ({
|
|
15
17
|
useSessionBookmarks: () => ({ data: data.bookmarks }),
|
|
@@ -17,6 +19,9 @@ vi.mock("@/data/bookmarks", () => ({
|
|
|
17
19
|
vi.mock("@/data/lumifyhub/docs", () => ({
|
|
18
20
|
useDocsWorkspace: () => data.docs,
|
|
19
21
|
}));
|
|
22
|
+
vi.mock("@/components/Analyst/pair", () => ({
|
|
23
|
+
useCanAddAnalyst: () => data.canAddAnalyst,
|
|
24
|
+
}));
|
|
20
25
|
vi.mock("@/components/Tasks/MoveMenuItems", () => ({
|
|
21
26
|
MoveMenuItems: () => null,
|
|
22
27
|
}));
|
|
@@ -60,6 +65,8 @@ describe("PaneMenu", () => {
|
|
|
60
65
|
root = createRoot(host);
|
|
61
66
|
data.bookmarks = [];
|
|
62
67
|
data.docs = null;
|
|
68
|
+
data.canAddAnalyst = false;
|
|
69
|
+
analystUi.open = false;
|
|
63
70
|
paletteUi.open = false;
|
|
64
71
|
bookmarksUi.chatSessionId = null;
|
|
65
72
|
docsUi.workspaceId = null;
|
|
@@ -73,6 +80,7 @@ describe("PaneMenu", () => {
|
|
|
73
80
|
root.render(
|
|
74
81
|
createElement(PaneMenu, {
|
|
75
82
|
session,
|
|
83
|
+
paneId: "pane-1",
|
|
76
84
|
bookmarksSessionId,
|
|
77
85
|
canSplit: true,
|
|
78
86
|
canClose: true,
|
|
@@ -115,4 +123,17 @@ describe("PaneMenu", () => {
|
|
|
115
123
|
act(() => item("Docs")?.click());
|
|
116
124
|
expect(docsUi.workspaceId).toBe("w1");
|
|
117
125
|
});
|
|
126
|
+
|
|
127
|
+
it("offers Add analyst only when the chat can have one, beside this pane", () => {
|
|
128
|
+
render();
|
|
129
|
+
expect(item("Add analyst")).toBeUndefined();
|
|
130
|
+
data.canAddAnalyst = true;
|
|
131
|
+
render();
|
|
132
|
+
act(() => item("Add analyst")?.click());
|
|
133
|
+
expect(analystUi).toMatchObject({
|
|
134
|
+
open: true,
|
|
135
|
+
sessionId: "s1",
|
|
136
|
+
paneId: "pane-1",
|
|
137
|
+
});
|
|
138
|
+
});
|
|
118
139
|
});
|
|
@@ -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
|
+
}
|