@saadnvd1/agent-os 0.3.502 → 0.3.505
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 +47 -0
- package/app/api/projects/[id]/session-logs/route.ts +39 -0
- package/app/api/session-logs/[id]/route.ts +18 -0
- package/components/Pane/DesktopTabBar.tsx +6 -1
- package/components/Pane/MobileTabBar.tsx +14 -1
- package/components/Pane/PaneViewToggle.tsx +14 -2
- package/components/Pane/index.tsx +28 -1
- package/components/PrStageBadge.tsx +78 -0
- package/components/Projects/ProjectSettingsDialog.tsx +11 -0
- package/components/SessionList/LiveLine.tsx +75 -0
- package/components/SessionList/RowParts.tsx +1 -7
- package/components/SessionList/RowShell.tsx +1 -1
- package/components/SessionList/RowSubtitle.tsx +4 -0
- package/components/SessionList/SessionList.types.ts +2 -0
- package/components/SessionList/SessionRow.tsx +9 -1
- package/components/SessionLogs/LogDocument.tsx +29 -0
- package/components/SessionLogs/ProjectLogsSection.tsx +140 -0
- package/components/views/types.ts +2 -0
- package/contexts/PaneContext.tsx +1 -1
- package/data/session-logs/index.ts +73 -0
- package/lib/db/migrations.ts +24 -0
- package/lib/done/index.ts +2 -0
- package/lib/orchestrator/armed.ts +2 -0
- package/lib/orchestrator/brief.ts +1 -0
- package/lib/orchestrator/checks.ts +2 -0
- package/lib/orchestrator/facts.ts +1 -0
- package/lib/orchestrator/logs.ts +81 -0
- package/lib/orchestrator/serve.ts +5 -0
- package/lib/orchestrator/task-state.ts +30 -7
- package/lib/orchestrator/tool-names.ts +1 -0
- package/lib/orchestrator/tool-schemas.ts +13 -0
- package/lib/orchestrator/tools.ts +1 -0
- package/lib/session-logs/build.ts +253 -0
- package/lib/session-logs/folder.ts +145 -0
- package/lib/session-logs/gather.ts +301 -0
- package/lib/session-logs/index.ts +178 -0
- package/lib/session-logs/store.ts +105 -0
- package/lib/sidebar/live-status.ts +52 -0
- package/lib/sidebar/shelves.ts +5 -0
- package/lib/status/collect.ts +6 -0
- package/lib/status/pr-stages.ts +55 -0
- package/lib/tasks/finish.ts +3 -0
- package/lib/tasks/pr-stage.ts +106 -0
- package/lib/tasks/session.ts +17 -0
- package/package.json +1 -1
- package/scripts/backfill-session-logs.ts +70 -0
package/README.md
CHANGED
|
@@ -1084,6 +1084,53 @@ to each idle or stopped session, and `aos done --all-idle` does the same
|
|
|
1084
1084
|
sweep. A clean-up never merges: open PRs are listed for their own done.
|
|
1085
1085
|
`aos done` reaches only sessions in the caller's workspace (or project).
|
|
1086
1086
|
|
|
1087
|
+
### Session logs
|
|
1088
|
+
|
|
1089
|
+
When a task merges, AgentOS writes a log of it. It costs no model tokens: the
|
|
1090
|
+
log is a fixed template filled from records AgentOS already holds, and nothing
|
|
1091
|
+
that makes it can call a model (a test holds it to that). Dropped tasks get
|
|
1092
|
+
none.
|
|
1093
|
+
|
|
1094
|
+
A log holds:
|
|
1095
|
+
|
|
1096
|
+
- **Header:** task name and id, workspace, project, branch, PR number and
|
|
1097
|
+
link, merge commit, start, merge time and duration.
|
|
1098
|
+
- **Brief:** the task's prompt and every scope change recorded on it, in
|
|
1099
|
+
order.
|
|
1100
|
+
- **Plan:** `.agentos/plan.md`, read at the merge before cleanup removes the
|
|
1101
|
+
worktree. A task with no plan says so.
|
|
1102
|
+
- **Timeline:** PR opened, gate failures per commit (CI with the failing
|
|
1103
|
+
check's name, review, scope), each review and scope verdict per commit with
|
|
1104
|
+
its text trimmed, escalations to you and your answers, and the merge.
|
|
1105
|
+
- **Usage:** tokens, estimated cost and model per role, from the usage ledger.
|
|
1106
|
+
- **Outcome:** the task agent's last message, verbatim.
|
|
1107
|
+
- **Links:** the PR and the session.
|
|
1108
|
+
|
|
1109
|
+
Text from tasks, CI and reviewers is untrusted. It is written inside fences
|
|
1110
|
+
or code spans, so it can't add headings or links to the document, and
|
|
1111
|
+
credential-shaped strings are redacted.
|
|
1112
|
+
|
|
1113
|
+
The database is the record: `session_logs`, one row per task, never deleted.
|
|
1114
|
+
Each log is also written to
|
|
1115
|
+
`~/.agent-os/session-logs/<workspace>/<project>/YYYY-MM-DD-<task>.md`. A
|
|
1116
|
+
project's settings have a **Session log folder** (an absolute or `~` path) to
|
|
1117
|
+
put its files elsewhere; a folder inside the project's repository is refused,
|
|
1118
|
+
because repositories can be public, and the files are never written into one.
|
|
1119
|
+
The same dialog lists the project's logs, newest first, and a merged task's
|
|
1120
|
+
view has a **Log** tab. The orchestrator reads them with its `logs` tool: the
|
|
1121
|
+
latest headers of a workspace or project, or one log in full. It only reads.
|
|
1122
|
+
|
|
1123
|
+
The log is made once per task, in the background, from wherever the merge
|
|
1124
|
+
happens: a sign-off, Done, a stack landing, or a merge seen on GitHub. A
|
|
1125
|
+
failure is logged and never reaches the merge. For tasks that merged before
|
|
1126
|
+
this existed:
|
|
1127
|
+
|
|
1128
|
+
```bash
|
|
1129
|
+
npx tsx scripts/backfill-session-logs.ts --db <path to agent-os.db> [--no-gh] [--dry]
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
Their plan files are gone, so those logs say "plan not kept".
|
|
1133
|
+
|
|
1087
1134
|
### Orchestrator
|
|
1088
1135
|
|
|
1089
1136
|
Each workspace has one standing orchestrator, pinned at the top of its
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getProject } from "@/lib/projects";
|
|
3
|
+
import { DEFAULT_ROOT } from "@/lib/session-logs/folder";
|
|
4
|
+
import { listLogs, logFolder, setLogFolder } from "@/lib/session-logs";
|
|
5
|
+
|
|
6
|
+
interface RouteParams {
|
|
7
|
+
params: Promise<{ id: string }>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function state(id: string) {
|
|
11
|
+
return {
|
|
12
|
+
folder: logFolder(id),
|
|
13
|
+
defaultFolder: DEFAULT_ROOT(),
|
|
14
|
+
logs: listLogs({ projectId: id }, 100),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The project's merged-task logs, newest first, and its log folder setting.
|
|
19
|
+
export async function GET(_request: NextRequest, { params }: RouteParams) {
|
|
20
|
+
const { id } = await params;
|
|
21
|
+
if (!getProject(id))
|
|
22
|
+
return NextResponse.json({ error: "Project not found" }, { status: 404 });
|
|
23
|
+
return NextResponse.json(state(id));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A folder inside the project's repository is refused with the reason.
|
|
27
|
+
export async function PUT(request: NextRequest, { params }: RouteParams) {
|
|
28
|
+
const { id } = await params;
|
|
29
|
+
if (!getProject(id))
|
|
30
|
+
return NextResponse.json({ error: "Project not found" }, { status: 404 });
|
|
31
|
+
try {
|
|
32
|
+
const { folder } = await request.json();
|
|
33
|
+
setLogFolder(id, typeof folder === "string" ? folder : null);
|
|
34
|
+
return NextResponse.json(state(id));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
return NextResponse.json({ error: message }, { status: 400 });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getLog, headerOf } from "@/lib/session-logs";
|
|
3
|
+
|
|
4
|
+
interface RouteParams {
|
|
5
|
+
params: Promise<{ id: string }>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// One task's log: its header fields and the markdown.
|
|
9
|
+
export async function GET(_request: NextRequest, { params }: RouteParams) {
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
const row = getLog(id);
|
|
12
|
+
if (!row)
|
|
13
|
+
return NextResponse.json(
|
|
14
|
+
{ error: "No log for this task" },
|
|
15
|
+
{ status: 404 }
|
|
16
|
+
);
|
|
17
|
+
return NextResponse.json({ header: headerOf(row), markdown: row.markdown });
|
|
18
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { SessionPrStage } from "@/components/PrStageBadge";
|
|
3
4
|
import { ViewSwitch } from "@/components/Chat/ViewSwitch";
|
|
4
5
|
import { ContextMeter } from "@/components/Chat/ContextMeter";
|
|
5
6
|
import { Button } from "@/components/ui/button";
|
|
@@ -16,7 +17,7 @@ import { PaneMenu } from "./PaneMenu";
|
|
|
16
17
|
import { HostBadge } from "@/components/Hosts/HostBadge";
|
|
17
18
|
import { usePaneBarSlots } from "./PaneBarSlots";
|
|
18
19
|
|
|
19
|
-
type ViewMode = "terminal" | "files" | "git" | "workers";
|
|
20
|
+
type ViewMode = "terminal" | "files" | "git" | "workers" | "log";
|
|
20
21
|
|
|
21
22
|
interface Tab {
|
|
22
23
|
id: string;
|
|
@@ -34,6 +35,7 @@ interface DesktopTabBarProps {
|
|
|
34
35
|
viewMode: ViewMode;
|
|
35
36
|
isFocused: boolean;
|
|
36
37
|
isConductor: boolean;
|
|
38
|
+
hasLog?: boolean;
|
|
37
39
|
workerCount: number;
|
|
38
40
|
canSplit: boolean;
|
|
39
41
|
canClose: boolean;
|
|
@@ -61,6 +63,7 @@ export function DesktopTabBar({
|
|
|
61
63
|
viewMode,
|
|
62
64
|
isFocused,
|
|
63
65
|
isConductor,
|
|
66
|
+
hasLog,
|
|
64
67
|
workerCount,
|
|
65
68
|
canSplit,
|
|
66
69
|
canClose,
|
|
@@ -155,12 +158,14 @@ export function DesktopTabBar({
|
|
|
155
158
|
<div className="flex shrink-0 items-center gap-1.5">
|
|
156
159
|
<HostBadge hostId={session?.host_id} />
|
|
157
160
|
<ContextMeter sessionId={session?.id} />
|
|
161
|
+
<SessionPrStage sessionId={session?.id} />
|
|
158
162
|
<ViewSwitch session={session} />
|
|
159
163
|
{session?.working_directory && (
|
|
160
164
|
<PaneViewToggle
|
|
161
165
|
viewMode={viewMode}
|
|
162
166
|
isLocalSession={isLocalSession}
|
|
163
167
|
isConductor={isConductor}
|
|
168
|
+
hasLog={hasLog}
|
|
164
169
|
workerCount={workerCount}
|
|
165
170
|
gitDrawerOpen={gitDrawerOpen}
|
|
166
171
|
shellDrawerOpen={shellDrawerOpen}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { SessionPrStage } from "@/components/PrStageBadge";
|
|
3
4
|
import { ViewSwitch } from "@/components/Chat/ViewSwitch";
|
|
4
5
|
import { paletteActions } from "@/stores/palette";
|
|
5
6
|
import { newDraft } from "@/stores/drafts";
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
Terminal as TerminalIcon,
|
|
26
27
|
TextSearch,
|
|
27
28
|
Users,
|
|
29
|
+
ScrollText,
|
|
28
30
|
} from "lucide-react";
|
|
29
31
|
import { cn } from "@/lib/utils";
|
|
30
32
|
import { useAwayQuery } from "@/data/away";
|
|
@@ -34,7 +36,7 @@ import type { LucideIcon } from "lucide-react";
|
|
|
34
36
|
import { HostBadge } from "@/components/Hosts/HostBadge";
|
|
35
37
|
import { MoveMenuItems } from "@/components/Tasks/MoveMenuItems";
|
|
36
38
|
|
|
37
|
-
type ViewMode = "terminal" | "files" | "git" | "workers";
|
|
39
|
+
type ViewMode = "terminal" | "files" | "git" | "workers" | "log";
|
|
38
40
|
|
|
39
41
|
interface ViewModeButtonProps {
|
|
40
42
|
mode: ViewMode;
|
|
@@ -79,6 +81,7 @@ interface MobileTabBarProps {
|
|
|
79
81
|
projects: Project[];
|
|
80
82
|
viewMode: ViewMode;
|
|
81
83
|
isConductor: boolean;
|
|
84
|
+
hasLog?: boolean;
|
|
82
85
|
workerCount: number;
|
|
83
86
|
onMenuClick?: () => void;
|
|
84
87
|
onViewModeChange: (mode: ViewMode) => void;
|
|
@@ -94,6 +97,7 @@ export function MobileTabBar({
|
|
|
94
97
|
projects,
|
|
95
98
|
viewMode,
|
|
96
99
|
isConductor,
|
|
100
|
+
hasLog,
|
|
97
101
|
workerCount,
|
|
98
102
|
onMenuClick,
|
|
99
103
|
onViewModeChange,
|
|
@@ -317,6 +321,7 @@ export function MobileTabBar({
|
|
|
317
321
|
>
|
|
318
322
|
<Plus className="h-4 w-4" />
|
|
319
323
|
</Button>
|
|
324
|
+
<SessionPrStage sessionId={session?.id} />
|
|
320
325
|
<ViewSwitch session={session} />
|
|
321
326
|
|
|
322
327
|
{/* View mode toggle */}
|
|
@@ -344,6 +349,14 @@ export function MobileTabBar({
|
|
|
344
349
|
onClick={onViewModeChange}
|
|
345
350
|
/>
|
|
346
351
|
)}
|
|
352
|
+
{hasLog && (
|
|
353
|
+
<ViewModeButton
|
|
354
|
+
mode="log"
|
|
355
|
+
currentMode={viewMode}
|
|
356
|
+
icon={ScrollText}
|
|
357
|
+
onClick={onViewModeChange}
|
|
358
|
+
/>
|
|
359
|
+
)}
|
|
347
360
|
{isConductor && (
|
|
348
361
|
<ViewModeButton
|
|
349
362
|
mode="workers"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { FolderOpen, GitBranch, Home, Users } from "lucide-react";
|
|
3
|
+
import { FolderOpen, GitBranch, Home, ScrollText, Users } from "lucide-react";
|
|
4
4
|
import {
|
|
5
5
|
Tooltip,
|
|
6
6
|
TooltipContent,
|
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
} from "@/components/ui/tooltip";
|
|
9
9
|
import { cn } from "@/lib/utils";
|
|
10
10
|
|
|
11
|
-
type ViewMode = "terminal" | "files" | "git" | "workers";
|
|
11
|
+
type ViewMode = "terminal" | "files" | "git" | "workers" | "log";
|
|
12
12
|
|
|
13
13
|
interface PaneViewToggleProps {
|
|
14
14
|
viewMode: ViewMode;
|
|
15
15
|
isLocalSession: boolean;
|
|
16
16
|
isConductor: boolean;
|
|
17
|
+
// The task merged and has a log.
|
|
18
|
+
hasLog?: boolean;
|
|
17
19
|
workerCount: number;
|
|
18
20
|
gitDrawerOpen: boolean;
|
|
19
21
|
shellDrawerOpen: boolean;
|
|
@@ -68,6 +70,7 @@ export function PaneViewToggle({
|
|
|
68
70
|
viewMode,
|
|
69
71
|
isLocalSession,
|
|
70
72
|
isConductor,
|
|
73
|
+
hasLog,
|
|
71
74
|
workerCount,
|
|
72
75
|
gitDrawerOpen,
|
|
73
76
|
shellDrawerOpen,
|
|
@@ -110,6 +113,15 @@ export function PaneViewToggle({
|
|
|
110
113
|
</ToggleButton>
|
|
111
114
|
</>
|
|
112
115
|
)}
|
|
116
|
+
{hasLog && (
|
|
117
|
+
<ToggleButton
|
|
118
|
+
label="Log"
|
|
119
|
+
active={viewMode === "log"}
|
|
120
|
+
onClick={() => onViewModeChange("log")}
|
|
121
|
+
>
|
|
122
|
+
<ScrollText className="h-3.5 w-3.5" />
|
|
123
|
+
</ToggleButton>
|
|
124
|
+
)}
|
|
113
125
|
{isConductor && (
|
|
114
126
|
<ToggleButton
|
|
115
127
|
label="Workers"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTaskLogQuery } from "@/data/session-logs";
|
|
3
4
|
import { SetupCard } from "@/components/Chat/SetupCard";
|
|
4
5
|
import type { TabData } from "@/lib/panes";
|
|
5
6
|
import { useRef, useCallback, useEffect, memo, useMemo } from "react";
|
|
@@ -34,6 +35,14 @@ import { useDemoMode } from "@/data/demo";
|
|
|
34
35
|
import { DemoNotice } from "@/components/DemoNotice";
|
|
35
36
|
|
|
36
37
|
// Dynamic imports for client-only components with loading states
|
|
38
|
+
const LogDocument = dynamic(
|
|
39
|
+
() =>
|
|
40
|
+
import("@/components/SessionLogs/LogDocument").then(
|
|
41
|
+
(mod) => mod.LogDocument
|
|
42
|
+
),
|
|
43
|
+
{ ssr: false }
|
|
44
|
+
);
|
|
45
|
+
|
|
37
46
|
const Terminal = dynamic(
|
|
38
47
|
() => import("@/components/Terminal").then((mod) => mod.Terminal),
|
|
39
48
|
{ ssr: false, loading: () => <TerminalSkeleton /> }
|
|
@@ -87,7 +96,7 @@ interface PaneProps {
|
|
|
87
96
|
onAttachSession?: (terminal: TerminalHandle, session: Session) => void;
|
|
88
97
|
}
|
|
89
98
|
|
|
90
|
-
type ViewMode = "terminal" | "files" | "git" | "workers";
|
|
99
|
+
type ViewMode = "terminal" | "files" | "git" | "workers" | "log";
|
|
91
100
|
|
|
92
101
|
export const Pane = memo(function Pane({
|
|
93
102
|
paneId,
|
|
@@ -173,6 +182,12 @@ export const Pane = memo(function Pane({
|
|
|
173
182
|
|
|
174
183
|
const isConductor = workerCount > 0;
|
|
175
184
|
|
|
185
|
+
// A merged task keeps the log AgentOS wrote of it.
|
|
186
|
+
const merged =
|
|
187
|
+
!!session?.task_prompt &&
|
|
188
|
+
(session.task_status === "merged" || session.pr_status === "merged");
|
|
189
|
+
const hasLog = !!useTaskLogQuery(session?.id, merged).data;
|
|
190
|
+
|
|
176
191
|
// Get current project and its repositories
|
|
177
192
|
const currentProject = useMemo(() => {
|
|
178
193
|
if (!session?.project_id) return null;
|
|
@@ -403,6 +418,7 @@ export const Pane = memo(function Pane({
|
|
|
403
418
|
projects={projects}
|
|
404
419
|
viewMode={viewMode}
|
|
405
420
|
isConductor={isConductor}
|
|
421
|
+
hasLog={hasLog}
|
|
406
422
|
workerCount={workerCount}
|
|
407
423
|
onMenuClick={onMenuClick}
|
|
408
424
|
onViewModeChange={setViewMode}
|
|
@@ -423,6 +439,7 @@ export const Pane = memo(function Pane({
|
|
|
423
439
|
viewMode={viewMode}
|
|
424
440
|
isFocused={isFocused}
|
|
425
441
|
isConductor={isConductor}
|
|
442
|
+
hasLog={hasLog}
|
|
426
443
|
workerCount={workerCount}
|
|
427
444
|
canSplit={canSplit}
|
|
428
445
|
canClose={canClose}
|
|
@@ -485,6 +502,11 @@ export const Pane = memo(function Pane({
|
|
|
485
502
|
</div>
|
|
486
503
|
)}
|
|
487
504
|
|
|
505
|
+
{/* Log */}
|
|
506
|
+
{viewMode === "log" && session && (
|
|
507
|
+
<LogDocument sessionId={session.id} />
|
|
508
|
+
)}
|
|
509
|
+
|
|
488
510
|
{/* Workers */}
|
|
489
511
|
{viewMode === "workers" && session && (
|
|
490
512
|
<ConductorPanel
|
|
@@ -542,6 +564,11 @@ export const Pane = memo(function Pane({
|
|
|
542
564
|
</div>
|
|
543
565
|
)}
|
|
544
566
|
|
|
567
|
+
{/* Log */}
|
|
568
|
+
{viewMode === "log" && session && (
|
|
569
|
+
<LogDocument sessionId={session.id} />
|
|
570
|
+
)}
|
|
571
|
+
|
|
545
572
|
{/* Workers */}
|
|
546
573
|
{viewMode === "workers" && session && (
|
|
547
574
|
<ConductorPanel
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
5
|
+
import { statusKeys } from "@/data/sessions/keys";
|
|
6
|
+
import {
|
|
7
|
+
PR_STAGE_BUSY,
|
|
8
|
+
prStageText,
|
|
9
|
+
PR_STAGE_SHORT,
|
|
10
|
+
type PrStage,
|
|
11
|
+
type PrStageName,
|
|
12
|
+
} from "@/lib/tasks/pr-stage";
|
|
13
|
+
import { cn } from "@/lib/utils";
|
|
14
|
+
|
|
15
|
+
const TONE: Record<PrStageName, string> = {
|
|
16
|
+
merged: "bg-purple-500/15 text-purple-700 dark:text-purple-400",
|
|
17
|
+
conflicts: "bg-amber-500/15 text-amber-700 dark:text-amber-400",
|
|
18
|
+
"ci-failed": "bg-red-500/15 text-red-700 dark:text-red-400",
|
|
19
|
+
"ci-running": "bg-blue-500/15 text-blue-700 dark:text-blue-400",
|
|
20
|
+
"ci-settling": "bg-blue-500/15 text-blue-700 dark:text-blue-400",
|
|
21
|
+
"review-running": "bg-blue-500/15 text-blue-700 dark:text-blue-400",
|
|
22
|
+
"review-blocked": "bg-red-500/15 text-red-700 dark:text-red-400",
|
|
23
|
+
waiting: "bg-amber-500/15 text-amber-700 dark:text-amber-400",
|
|
24
|
+
armed: "bg-green-500/15 text-green-700 dark:text-green-400",
|
|
25
|
+
open: "bg-muted text-muted-foreground",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Where a task's PR is in the gates, linking to the PR. The short stage word
|
|
29
|
+
// is all the text; the PR number, full wording and sha live in the tooltip
|
|
30
|
+
// and label. `showNumber` (the session header) prefixes "PR #n".
|
|
31
|
+
export function PrStageBadge({
|
|
32
|
+
stage,
|
|
33
|
+
showNumber = false,
|
|
34
|
+
}: {
|
|
35
|
+
stage?: PrStage;
|
|
36
|
+
showNumber?: boolean;
|
|
37
|
+
}) {
|
|
38
|
+
if (!stage) return null;
|
|
39
|
+
const text = prStageText(stage);
|
|
40
|
+
const short = PR_STAGE_SHORT[stage.stage];
|
|
41
|
+
const visible = !showNumber
|
|
42
|
+
? short
|
|
43
|
+
: stage.stage === "open"
|
|
44
|
+
? `PR #${stage.number}`
|
|
45
|
+
: `PR #${stage.number} · ${short}`;
|
|
46
|
+
return (
|
|
47
|
+
<a
|
|
48
|
+
href={stage.url}
|
|
49
|
+
target="_blank"
|
|
50
|
+
rel="noreferrer"
|
|
51
|
+
title={`${text}${stage.sha ? ` · ${stage.sha}` : ""}`}
|
|
52
|
+
aria-label={`${text}${stage.sha ? ` · ${stage.sha}` : ""}`}
|
|
53
|
+
onClick={(e) => e.stopPropagation()}
|
|
54
|
+
className={cn(
|
|
55
|
+
"inline-flex shrink-0 items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-semibold whitespace-nowrap hover:underline",
|
|
56
|
+
TONE[stage.stage]
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{PR_STAGE_BUSY.has(stage.stage) && (
|
|
60
|
+
<Loader2 className="h-3 w-3 animate-spin" aria-hidden />
|
|
61
|
+
)}
|
|
62
|
+
{visible}
|
|
63
|
+
</a>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The badge for one session, read from the status the sidebar already holds.
|
|
68
|
+
export function SessionPrStage({ sessionId }: { sessionId?: string | null }) {
|
|
69
|
+
const { data } = useQuery<{
|
|
70
|
+
statuses: Record<string, { prStage?: PrStage }>;
|
|
71
|
+
}>({ queryKey: statusKeys.all, enabled: false });
|
|
72
|
+
return (
|
|
73
|
+
<PrStageBadge
|
|
74
|
+
stage={sessionId ? data?.statuses[sessionId]?.prStage : undefined}
|
|
75
|
+
showNumber
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import dynamic from "next/dynamic";
|
|
3
4
|
import { useState, useEffect } from "react";
|
|
4
5
|
import {
|
|
5
6
|
Dialog,
|
|
@@ -73,6 +74,14 @@ interface ProjectSettingsDialogProps {
|
|
|
73
74
|
onSave: () => void;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
const ProjectLogsSection = dynamic(
|
|
78
|
+
() =>
|
|
79
|
+
import("@/components/SessionLogs/ProjectLogsSection").then(
|
|
80
|
+
(mod) => mod.ProjectLogsSection
|
|
81
|
+
),
|
|
82
|
+
{ ssr: false }
|
|
83
|
+
);
|
|
84
|
+
|
|
76
85
|
export function ProjectSettingsDialog({
|
|
77
86
|
project,
|
|
78
87
|
open,
|
|
@@ -766,6 +775,8 @@ export function ProjectSettingsDialog({
|
|
|
766
775
|
|
|
767
776
|
<ProjectMergeSection projectId={project.id} open={open} />
|
|
768
777
|
|
|
778
|
+
<ProjectLogsSection projectId={project.id} open={open} />
|
|
779
|
+
|
|
769
780
|
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
770
781
|
|
|
771
782
|
<DialogFooter>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Loader2 } from "lucide-react";
|
|
4
|
+
import { liveParts, liveText, type LiveTone } from "@/lib/sidebar/live-status";
|
|
5
|
+
import type { PrStage } from "@/lib/tasks/pr-stage";
|
|
6
|
+
import { prStageText } from "@/lib/tasks/pr-stage";
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
export interface LiveInput {
|
|
10
|
+
compacting?: boolean;
|
|
11
|
+
prStage?: PrStage;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const TONE: Record<LiveTone, string> = {
|
|
15
|
+
blue: "text-blue-700 dark:text-blue-400",
|
|
16
|
+
amber: "text-amber-700 dark:text-amber-400",
|
|
17
|
+
red: "text-red-700 dark:text-red-400",
|
|
18
|
+
green: "text-green-700 dark:text-green-400",
|
|
19
|
+
purple: "text-purple-700 dark:text-purple-400",
|
|
20
|
+
muted: "text-muted-foreground",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// The row's live status in small coloured words under the name: "Compacting
|
|
24
|
+
// · PR CI". The PR word links to the PR; its number, full wording and sha are
|
|
25
|
+
// in the tooltip. Renders nothing when there is nothing to say.
|
|
26
|
+
export function LiveLine({ compacting, prStage }: LiveInput) {
|
|
27
|
+
const parts = liveParts({ compacting, prStage });
|
|
28
|
+
if (parts.length === 0) return null;
|
|
29
|
+
const pr = prStage
|
|
30
|
+
? `${prStageText(prStage)}${prStage.sha ? ` · ${prStage.sha}` : ""}`
|
|
31
|
+
: "";
|
|
32
|
+
return (
|
|
33
|
+
<span
|
|
34
|
+
aria-label={liveText(parts)}
|
|
35
|
+
className="flex shrink-0 items-center gap-1 text-xs font-medium whitespace-nowrap"
|
|
36
|
+
>
|
|
37
|
+
{parts.map((p, i) => {
|
|
38
|
+
const isPr = p.text.startsWith("PR");
|
|
39
|
+
const body = (
|
|
40
|
+
<>
|
|
41
|
+
{p.busy && (
|
|
42
|
+
<Loader2 className="h-2.5 w-2.5 animate-spin" aria-hidden />
|
|
43
|
+
)}
|
|
44
|
+
{p.text}
|
|
45
|
+
</>
|
|
46
|
+
);
|
|
47
|
+
return (
|
|
48
|
+
<span key={p.text} className="flex items-center gap-1">
|
|
49
|
+
{i > 0 && <span className="text-muted-foreground/50">·</span>}
|
|
50
|
+
{isPr && prStage ? (
|
|
51
|
+
<a
|
|
52
|
+
href={prStage.url}
|
|
53
|
+
target="_blank"
|
|
54
|
+
rel="noreferrer"
|
|
55
|
+
title={pr}
|
|
56
|
+
aria-label={pr}
|
|
57
|
+
onClick={(e) => e.stopPropagation()}
|
|
58
|
+
className={cn(
|
|
59
|
+
"flex items-center gap-1 hover:underline",
|
|
60
|
+
TONE[p.tone]
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{body}
|
|
64
|
+
</a>
|
|
65
|
+
) : (
|
|
66
|
+
<span className={cn("flex items-center gap-1", TONE[p.tone])}>
|
|
67
|
+
{body}
|
|
68
|
+
</span>
|
|
69
|
+
)}
|
|
70
|
+
</span>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</span>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -26,7 +26,7 @@ export function RowDot({ row }: { row: SidebarRow }) {
|
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// What it needs from you, or a purple dot when it changed since you looked.
|
|
29
|
+
// What it needs from you, stalled, or a purple dot when it changed since you looked.
|
|
30
30
|
export function RowBadge({ row }: { row: SidebarRow }) {
|
|
31
31
|
if (row.need) {
|
|
32
32
|
const asks = row.status?.asks ?? 0;
|
|
@@ -44,12 +44,6 @@ export function RowBadge({ row }: { row: SidebarRow }) {
|
|
|
44
44
|
</span>
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
|
-
if (row.compacting)
|
|
48
|
-
return (
|
|
49
|
-
<span className="rounded-md bg-blue-500/15 px-1.5 py-0.5 text-[11px] font-semibold text-blue-700 dark:text-blue-400">
|
|
50
|
-
Compacting
|
|
51
|
-
</span>
|
|
52
|
-
);
|
|
53
47
|
if (row.status?.stalled)
|
|
54
48
|
return (
|
|
55
49
|
<span className="rounded-md bg-amber-500/15 px-1.5 py-0.5 text-[11px] font-semibold text-amber-700 dark:text-amber-400">
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { HostBadge } from "@/components/Hosts";
|
|
4
4
|
import type { Session } from "@/lib/db";
|
|
5
|
+
import { LiveLine, type LiveInput } from "./LiveLine";
|
|
5
6
|
import { headerParts } from "@/lib/orchestrator/header-line";
|
|
6
7
|
import { cn } from "@/lib/utils";
|
|
7
8
|
import { usageLabel, useSessionUsage } from "@/data/load";
|
|
@@ -16,9 +17,11 @@ const ATTENTION = (part: string) => part === "paused" || /\basks?$/.test(part);
|
|
|
16
17
|
export function RowSubtitle({
|
|
17
18
|
session,
|
|
18
19
|
detail,
|
|
20
|
+
live,
|
|
19
21
|
}: {
|
|
20
22
|
session: Session;
|
|
21
23
|
detail?: string | null;
|
|
24
|
+
live?: LiveInput;
|
|
22
25
|
}) {
|
|
23
26
|
const ctx = useRowContext();
|
|
24
27
|
// What its processes use, on a busy session: "3.1 cores · 4.2 GB".
|
|
@@ -52,6 +55,7 @@ export function RowSubtitle({
|
|
|
52
55
|
const project = ctx.projectNames.get(session.project_id ?? "");
|
|
53
56
|
return (
|
|
54
57
|
<span className="flex min-w-0 items-center gap-1.5">
|
|
58
|
+
<LiveLine {...live} />
|
|
55
59
|
{(project || detail || usage) && (
|
|
56
60
|
<span className="text-muted-foreground/70 truncate text-xs">
|
|
57
61
|
{[usage && usageLabel(usage), detail?.slice(0, 200), project]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PrStage } from "@/lib/tasks/pr-stage";
|
|
1
2
|
import type { SessionNeed } from "@/lib/sidebar/shelves";
|
|
2
3
|
|
|
3
4
|
export interface SessionStatus {
|
|
@@ -11,6 +12,7 @@ export interface SessionStatus {
|
|
|
11
12
|
unread?: boolean;
|
|
12
13
|
stalled?: boolean;
|
|
13
14
|
compacting?: { since: number; before?: number };
|
|
15
|
+
prStage?: PrStage;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface SessionListProps {
|
|
@@ -122,7 +122,15 @@ export const SessionRow = memo(function SessionRow({
|
|
|
122
122
|
{session.name}
|
|
123
123
|
</span>
|
|
124
124
|
)}
|
|
125
|
-
<RowSubtitle
|
|
125
|
+
<RowSubtitle
|
|
126
|
+
session={session}
|
|
127
|
+
detail={row.status?.detail}
|
|
128
|
+
live={
|
|
129
|
+
row.need
|
|
130
|
+
? undefined
|
|
131
|
+
: { compacting: row.compacting, prStage: row.status?.prStage }
|
|
132
|
+
}
|
|
133
|
+
/>
|
|
126
134
|
</RowShell>
|
|
127
135
|
{session.role === "orchestrator" && row.need && (
|
|
128
136
|
<OrchestratorAsks workspaceId={session.workspace_id} />
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Markdown } from "@/components/Chat/Markdown";
|
|
4
|
+
import { useTaskLogQuery } from "@/data/session-logs";
|
|
5
|
+
|
|
6
|
+
// A merged task's stored log, as the document AgentOS wrote.
|
|
7
|
+
export function LogDocument({ sessionId }: { sessionId: string }) {
|
|
8
|
+
const { data, isPending, isError, error } = useTaskLogQuery(sessionId);
|
|
9
|
+
if (isPending)
|
|
10
|
+
return <div className="bg-muted/40 m-4 h-40 animate-pulse rounded-lg" />;
|
|
11
|
+
if (isError)
|
|
12
|
+
return <p className="text-destructive p-4 text-sm">{error.message}</p>;
|
|
13
|
+
if (!data)
|
|
14
|
+
return (
|
|
15
|
+
<p className="text-muted-foreground p-4 text-sm">This task has no log.</p>
|
|
16
|
+
);
|
|
17
|
+
return (
|
|
18
|
+
<div className="h-full overflow-y-auto">
|
|
19
|
+
<div className="mx-auto max-w-3xl px-4 py-4">
|
|
20
|
+
<Markdown text={data.markdown} />
|
|
21
|
+
{data.header.filePath && (
|
|
22
|
+
<p className="text-muted-foreground mt-4 border-t pt-3 text-xs break-all">
|
|
23
|
+
Also saved at {data.header.filePath}
|
|
24
|
+
</p>
|
|
25
|
+
)}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|