@saadnvd1/agent-os 0.3.594 → 0.3.596
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/sessions/[id]/artifacts/[artifactId]/route.ts +34 -0
- package/app/api/sessions/[id]/artifacts/route.ts +25 -9
- package/components/Artifacts/ArtifactPreview.tsx +239 -0
- package/components/Artifacts/ArtifactRow.tsx +147 -0
- package/components/Artifacts/ArtifactsPanel.tsx +244 -0
- package/components/Artifacts/helpers.test.ts +90 -0
- package/components/Artifacts/helpers.ts +99 -0
- package/components/Artifacts/index.ts +1 -0
- package/components/Chat/ArtifactFrame.tsx +4 -1
- package/components/Pane/DesktopTabBar.tsx +2 -2
- package/components/Pane/MobileTabBar.tsx +14 -2
- package/components/Pane/PaneViewToggle.tsx +20 -3
- package/components/Pane/index.tsx +35 -3
- package/components/Providers.tsx +5 -1
- package/components/SessionList/LiveLine.tsx +1 -1
- package/components/SessionList/OrchestratorPinRow.tsx +10 -5
- package/components/SessionList/QuickChangeRow.tsx +1 -1
- package/components/SessionList/RowParts.tsx +37 -2
- package/components/SessionList/RowShell.tsx +6 -2
- package/components/SessionList/RowSubtitle.tsx +20 -13
- package/components/SessionList/SessionRow.tsx +28 -17
- package/components/SessionList/SessionShelves.tsx +40 -8
- package/components/SessionList/SidebarExtras.tsx +7 -1
- package/components/SessionList/index.tsx +2 -0
- package/components/SessionList/useSessionTips.ts +62 -0
- package/components/ui/HoverTip.tsx +20 -0
- package/contexts/PaneContext.tsx +7 -1
- package/data/presence/index.ts +14 -7
- package/data/session-artifacts/index.ts +2 -0
- package/data/session-artifacts/keys.ts +7 -0
- package/data/session-artifacts/queries.ts +53 -0
- package/lib/chat/task-output.ts +1 -1
- package/lib/done/worktree.ts +3 -0
- package/lib/hover-tip.ts +161 -0
- package/lib/security/demo.ts +1 -0
- package/lib/session-artifacts/access.ts +81 -0
- package/lib/session-artifacts/collect.ts +318 -0
- package/lib/session-artifacts/derive.ts +437 -0
- package/lib/session-artifacts/keep.ts +119 -0
- package/lib/session-artifacts/peer.ts +187 -0
- package/lib/session-artifacts/serve.ts +265 -0
- package/lib/session-artifacts/types.ts +43 -0
- package/lib/sidebar/row-badge.ts +40 -0
- package/lib/sidebar/row-details.ts +53 -0
- package/lib/tasks/finish.ts +2 -0
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ import { useRowContext } from "./RowContext";
|
|
|
16
16
|
import { SessionRowMenu } from "./SessionRowMenu";
|
|
17
17
|
import { RowRename, UnreadDot } from "./RowParts";
|
|
18
18
|
import { QuickChangeRow } from "./QuickChangeRow";
|
|
19
|
+
import { NestRail } from "./RowParts";
|
|
19
20
|
|
|
20
21
|
// A workspace's orchestrator, pinned above its sessions and titled with the
|
|
21
22
|
// workspace's name: what it's doing and how many asks wait on you, on a line
|
|
@@ -54,7 +55,7 @@ export const OrchestratorPinRow = memo(function OrchestratorPinRow({
|
|
|
54
55
|
|
|
55
56
|
const content = (
|
|
56
57
|
<RowFrame
|
|
57
|
-
|
|
58
|
+
data-tip={`${label}\n${summary}`}
|
|
58
59
|
onActivate={() => !renaming && ctx.onSelect(session.id)}
|
|
59
60
|
className={
|
|
60
61
|
active
|
|
@@ -172,9 +173,13 @@ export const OrchestratorPinRow = memo(function OrchestratorPinRow({
|
|
|
172
173
|
{menu("context")}
|
|
173
174
|
</CM.ContextMenuContent>
|
|
174
175
|
</CM.ContextMenu>
|
|
175
|
-
{row.quick?.
|
|
176
|
-
<
|
|
177
|
-
|
|
176
|
+
{!!row.quick?.length && (
|
|
177
|
+
<NestRail>
|
|
178
|
+
{row.quick.map((q) => (
|
|
179
|
+
<QuickChangeRow key={q.session.id} row={q} />
|
|
180
|
+
))}
|
|
181
|
+
</NestRail>
|
|
182
|
+
)}
|
|
178
183
|
</>
|
|
179
184
|
);
|
|
180
185
|
}, sameProps);
|
|
@@ -190,7 +195,7 @@ export function OrchestratorStartRow({
|
|
|
190
195
|
}) {
|
|
191
196
|
return (
|
|
192
197
|
<RowFrame
|
|
193
|
-
|
|
198
|
+
data-tip={`${name}\nStart orchestrator`}
|
|
194
199
|
onActivate={() => orchestratorOpenActions.request(workspaceId)}
|
|
195
200
|
className="hover:bg-muted/50 border-dashed"
|
|
196
201
|
>
|
|
@@ -42,7 +42,7 @@ export function QuickChangeRow({ row }: { row: SidebarRow }) {
|
|
|
42
42
|
);
|
|
43
43
|
const openPr = pr ? () => window.open(pr, "_blank") : undefined;
|
|
44
44
|
return (
|
|
45
|
-
<div
|
|
45
|
+
<div>
|
|
46
46
|
<RowShell
|
|
47
47
|
title={`Quick change: ${session.name}\n${label}`}
|
|
48
48
|
onActivate={openPr}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
4
|
+
import { ClipboardList, Pencil } from "lucide-react";
|
|
4
5
|
import { NEED_LABEL, type SidebarRow } from "@/lib/sidebar/shelves";
|
|
6
|
+
import { rowKindBadge, type RowKindBadge } from "@/lib/sidebar/row-badge";
|
|
5
7
|
import { cn } from "@/lib/utils";
|
|
6
8
|
|
|
7
9
|
// Amber when it needs you, red when it failed, green while it works, and a
|
|
@@ -26,7 +28,8 @@ export function RowDot({ row }: { row: SidebarRow }) {
|
|
|
26
28
|
);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
// What it needs from you, stalled, or a purple dot when it
|
|
31
|
+
// What it needs from you, stalled, its plan step, or a purple dot when it
|
|
32
|
+
// changed since you looked.
|
|
30
33
|
export function RowBadge({ row }: { row: SidebarRow }) {
|
|
31
34
|
if (row.need) {
|
|
32
35
|
const asks = row.status?.asks ?? 0;
|
|
@@ -50,10 +53,42 @@ export function RowBadge({ row }: { row: SidebarRow }) {
|
|
|
50
53
|
Stalled
|
|
51
54
|
</span>
|
|
52
55
|
);
|
|
56
|
+
const kind = rowKindBadge(row);
|
|
57
|
+
if (kind) return <KindBadge badge={kind} />;
|
|
53
58
|
if (row.unread) return <UnreadDot />;
|
|
54
59
|
return null;
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
// Planning in blue, a plan waiting for approval in the primary colour:
|
|
63
|
+
// an icon and a word, never colour alone.
|
|
64
|
+
function KindBadge({ badge }: { badge: RowKindBadge }) {
|
|
65
|
+
const Icon = badge.kind === "planning" ? Pencil : ClipboardList;
|
|
66
|
+
return (
|
|
67
|
+
<span
|
|
68
|
+
data-tip={badge.tip}
|
|
69
|
+
className={cn(
|
|
70
|
+
"flex shrink-0 items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-semibold whitespace-nowrap",
|
|
71
|
+
badge.kind === "planning"
|
|
72
|
+
? "bg-blue-500/15 text-blue-700 dark:text-blue-400"
|
|
73
|
+
: "bg-primary/15 text-primary"
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<Icon aria-hidden className="h-3 w-3" />
|
|
77
|
+
{badge.label}
|
|
78
|
+
</span>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// What belongs to the row above it (a conductor's workers, an
|
|
83
|
+
// orchestrator's quick changes), behind a thin rule.
|
|
84
|
+
export function NestRail({ children }: { children: ReactNode }) {
|
|
85
|
+
return (
|
|
86
|
+
<div className="border-foreground/10 ml-3.5 border-l pl-1.5">
|
|
87
|
+
{children}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
57
92
|
export function UnreadDot() {
|
|
58
93
|
return (
|
|
59
94
|
<span
|
|
@@ -11,9 +11,11 @@ export type RowMenuKind = "dropdown" | "context";
|
|
|
11
11
|
|
|
12
12
|
// Every sidebar row's frame: a leading mark, title over subtitle, what's on
|
|
13
13
|
// the right (hidden under the ⋯ on hover), and the same menu on ⋯ and on
|
|
14
|
-
// right-click. Session rows and queued tasks both draw through it.
|
|
14
|
+
// right-click. Session rows and queued tasks both draw through it. The
|
|
15
|
+
// title is its hover tip (lib/hover-tip), or tipKey's when one resolves.
|
|
15
16
|
export function RowShell({
|
|
16
17
|
title,
|
|
18
|
+
tipKey,
|
|
17
19
|
highlight,
|
|
18
20
|
onClick,
|
|
19
21
|
onActivate,
|
|
@@ -26,6 +28,7 @@ export function RowShell({
|
|
|
26
28
|
menuButton = true,
|
|
27
29
|
}: {
|
|
28
30
|
title: string;
|
|
31
|
+
tipKey?: string;
|
|
29
32
|
highlight?: "selected" | "active" | null;
|
|
30
33
|
onClick?: (e: React.MouseEvent) => void;
|
|
31
34
|
// Enter or Space on the row; without it the row isn't a button.
|
|
@@ -45,7 +48,8 @@ export function RowShell({
|
|
|
45
48
|
<div
|
|
46
49
|
role={onActivate ? "button" : undefined}
|
|
47
50
|
tabIndex={onActivate ? 0 : undefined}
|
|
48
|
-
|
|
51
|
+
data-tip={title}
|
|
52
|
+
data-tip-key={tipKey}
|
|
49
53
|
onClick={onClick}
|
|
50
54
|
onKeyDown={(e) => {
|
|
51
55
|
if (!onActivate || e.target !== e.currentTarget) return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Repeat, Layers } from "lucide-react";
|
|
3
|
+
import { Repeat, Layers, Pause } from "lucide-react";
|
|
4
4
|
import { HostBadge } from "@/components/Hosts";
|
|
5
5
|
import { rowGroup } from "@/lib/recipes/chip";
|
|
6
6
|
import type { Session } from "@/lib/db";
|
|
@@ -21,10 +21,12 @@ export function RowSubtitle({
|
|
|
21
21
|
session,
|
|
22
22
|
detail,
|
|
23
23
|
live,
|
|
24
|
+
hideHost = false,
|
|
24
25
|
}: {
|
|
25
26
|
session: Session;
|
|
26
27
|
detail?: string | null;
|
|
27
28
|
live?: LiveInput;
|
|
29
|
+
hideHost?: boolean;
|
|
28
30
|
}) {
|
|
29
31
|
const ctx = useRowContext();
|
|
30
32
|
// What its processes use, on a busy session: "3.1 cores · 4.2 GB".
|
|
@@ -61,29 +63,34 @@ export function RowSubtitle({
|
|
|
61
63
|
return (
|
|
62
64
|
<span className="flex min-w-0 items-center gap-1.5">
|
|
63
65
|
{pause ? (
|
|
64
|
-
<span className="min-w-0
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
:
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
<span className="flex min-w-0 items-center gap-1 text-xs text-amber-600 dark:text-amber-400">
|
|
67
|
+
<Pause aria-hidden className="h-3 w-3 shrink-0" />
|
|
68
|
+
<span className="truncate">
|
|
69
|
+
{pauseSubtitle({
|
|
70
|
+
state: pause,
|
|
71
|
+
until: session.paused_until
|
|
72
|
+
? Date.parse(`${session.paused_until.replace(" ", "T")}Z`)
|
|
73
|
+
: null,
|
|
74
|
+
note: session.pause_note,
|
|
75
|
+
})}
|
|
76
|
+
</span>
|
|
72
77
|
</span>
|
|
73
78
|
) : (
|
|
74
79
|
<LiveLine {...live} />
|
|
75
80
|
)}
|
|
76
81
|
{group && (
|
|
77
82
|
<span
|
|
78
|
-
className="text-muted-foreground/70
|
|
79
|
-
|
|
83
|
+
className="text-muted-foreground/70 flex min-w-0 shrink-0 items-center gap-1 truncate text-xs @[14rem]:shrink"
|
|
84
|
+
data-tip={`${group.kind === "recipe" ? "Recipe run" : "Plan piece"}: ${group.text}`}
|
|
80
85
|
>
|
|
81
86
|
{group.kind === "recipe" ? (
|
|
82
87
|
<Repeat aria-hidden className="h-3 w-3 shrink-0" />
|
|
83
88
|
) : (
|
|
84
89
|
<Layers aria-hidden className="h-3 w-3 shrink-0" />
|
|
85
90
|
)}
|
|
86
|
-
<span className="truncate">
|
|
91
|
+
<span className="sr-only truncate @[14rem]:not-sr-only">
|
|
92
|
+
{group.text}
|
|
93
|
+
</span>
|
|
87
94
|
</span>
|
|
88
95
|
)}
|
|
89
96
|
{(project || detail || usage || session.placed_note) && (
|
|
@@ -98,7 +105,7 @@ export function RowSubtitle({
|
|
|
98
105
|
.join(" · ")}
|
|
99
106
|
</span>
|
|
100
107
|
)}
|
|
101
|
-
<HostBadge hostId={session.host_id} />
|
|
108
|
+
{!hideHost && <HostBadge hostId={session.host_id} />}
|
|
102
109
|
</span>
|
|
103
110
|
);
|
|
104
111
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { compactTimeAgo } from "@/lib/session-meta";
|
|
13
13
|
import { wakeLabel } from "@/lib/sidebar/snooze";
|
|
14
14
|
import { sameRow, type SidebarRow } from "@/lib/sidebar/shelves";
|
|
15
|
+
import { rowKindBadge } from "@/lib/sidebar/row-badge";
|
|
15
16
|
import { useMinuteTick } from "@/hooks/useMinuteTick";
|
|
16
17
|
import { selectionStore, selectionActions } from "@/stores/sessionSelection";
|
|
17
18
|
import { cn } from "@/lib/utils";
|
|
@@ -19,7 +20,7 @@ import { QuickChangeRow } from "./QuickChangeRow";
|
|
|
19
20
|
import { useRowContext } from "./RowContext";
|
|
20
21
|
import { SessionRowMenu } from "./SessionRowMenu";
|
|
21
22
|
import { RowShell, type RowMenuKind } from "./RowShell";
|
|
22
|
-
import { RowBadge, RowDot, RowRename } from "./RowParts";
|
|
23
|
+
import { NestRail, RowBadge, RowDot, RowRename } from "./RowParts";
|
|
23
24
|
import { RowSubtitle } from "./RowSubtitle";
|
|
24
25
|
import { OrchestratorAsks } from "./OrchestratorAsks";
|
|
25
26
|
|
|
@@ -29,11 +30,12 @@ import { OrchestratorAsks } from "./OrchestratorAsks";
|
|
|
29
30
|
export const SessionRow = memo(function SessionRow({
|
|
30
31
|
row,
|
|
31
32
|
muted = false,
|
|
32
|
-
|
|
33
|
+
inMachineShelf = false,
|
|
33
34
|
}: {
|
|
34
35
|
row: SidebarRow;
|
|
35
36
|
muted?: boolean;
|
|
36
|
-
|
|
37
|
+
// Under its machine's own shelf, which already names the machine.
|
|
38
|
+
inMachineShelf?: boolean;
|
|
37
39
|
}) {
|
|
38
40
|
const ctx = useRowContext();
|
|
39
41
|
const { selectedIds } = useSnapshot(selectionStore);
|
|
@@ -69,9 +71,10 @@ export const SessionRow = memo(function SessionRow({
|
|
|
69
71
|
);
|
|
70
72
|
|
|
71
73
|
return (
|
|
72
|
-
<div
|
|
74
|
+
<div>
|
|
73
75
|
<RowShell
|
|
74
76
|
title={session.name}
|
|
77
|
+
tipKey={`session:${session.id}`}
|
|
75
78
|
highlight={selected ? "selected" : active ? "active" : null}
|
|
76
79
|
onClick={onClick}
|
|
77
80
|
onActivate={() => ctx.onSelect(session.id)}
|
|
@@ -89,7 +92,7 @@ export const SessionRow = memo(function SessionRow({
|
|
|
89
92
|
aside={
|
|
90
93
|
<>
|
|
91
94
|
{viaName && (
|
|
92
|
-
<span
|
|
95
|
+
<span data-tip={`Started by ${viaName}`} className="shrink-0">
|
|
93
96
|
<Workflow
|
|
94
97
|
aria-label={`Via orchestrator: ${viaName}`}
|
|
95
98
|
className="text-muted-foreground/60 h-3 w-3"
|
|
@@ -104,7 +107,7 @@ export const SessionRow = memo(function SessionRow({
|
|
|
104
107
|
trailing={
|
|
105
108
|
<>
|
|
106
109
|
<RowBadge row={row} />
|
|
107
|
-
{!row.need && <RowTime row={row} />}
|
|
110
|
+
{!row.need && !rowKindBadge(row) && <RowTime row={row} />}
|
|
108
111
|
</>
|
|
109
112
|
}
|
|
110
113
|
menu={menu}
|
|
@@ -132,6 +135,7 @@ export const SessionRow = memo(function SessionRow({
|
|
|
132
135
|
)}
|
|
133
136
|
<RowSubtitle
|
|
134
137
|
session={session}
|
|
138
|
+
hideHost={inMachineShelf}
|
|
135
139
|
detail={row.status?.detail}
|
|
136
140
|
live={
|
|
137
141
|
row.need
|
|
@@ -143,21 +147,28 @@ export const SessionRow = memo(function SessionRow({
|
|
|
143
147
|
{session.role === "orchestrator" && row.need && (
|
|
144
148
|
<OrchestratorAsks workspaceId={session.workspace_id} />
|
|
145
149
|
)}
|
|
146
|
-
{row.workers.
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
{(row.workers.length > 0 || !!row.quick?.length) && (
|
|
151
|
+
<NestRail>
|
|
152
|
+
{row.workers.map((worker) => (
|
|
153
|
+
<SessionRow key={worker.session.id} row={worker} muted={muted} />
|
|
154
|
+
))}
|
|
155
|
+
{row.quick?.map((q) => (
|
|
156
|
+
<QuickChangeRow key={q.session.id} row={q} />
|
|
157
|
+
))}
|
|
158
|
+
</NestRail>
|
|
159
|
+
)}
|
|
152
160
|
</div>
|
|
153
161
|
);
|
|
154
162
|
}, sameRowProps);
|
|
155
163
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
type RowProps = { row: SidebarRow; muted?: boolean; inMachineShelf?: boolean };
|
|
165
|
+
|
|
166
|
+
function sameRowProps(a: RowProps, b: RowProps) {
|
|
167
|
+
return (
|
|
168
|
+
a.muted === b.muted &&
|
|
169
|
+
a.inMachineShelf === b.inMachineShelf &&
|
|
170
|
+
sameRow(a.row, b.row)
|
|
171
|
+
);
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
// Snoozed: when it wakes. Otherwise when it last did something.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
-
import { ChevronDown } from "lucide-react";
|
|
4
|
+
import { ChevronDown, ListTodo, Server, type LucideIcon } from "lucide-react";
|
|
5
5
|
import {
|
|
6
6
|
DONE_PAGE,
|
|
7
7
|
doneLimit,
|
|
@@ -16,21 +16,35 @@ import { OrchestratorPinRow, OrchestratorStartRow } from "./OrchestratorPinRow";
|
|
|
16
16
|
import { QueuedRow } from "./QueuedRow";
|
|
17
17
|
import type { QueuedRow as QueuedRowData } from "@/lib/sidebar/queued";
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// A shelf's header. Only groups that aren't a state (a machine, the line,
|
|
20
|
+
// terminals) carry an icon; a shelf that asks for you counts in amber.
|
|
21
|
+
export function ShelfLabel({
|
|
20
22
|
label,
|
|
21
23
|
count,
|
|
24
|
+
icon: Icon,
|
|
25
|
+
accent = false,
|
|
22
26
|
collapsed,
|
|
23
27
|
onToggle,
|
|
24
28
|
}: {
|
|
25
29
|
label: string;
|
|
26
30
|
count?: number;
|
|
31
|
+
icon?: LucideIcon;
|
|
32
|
+
accent?: boolean;
|
|
27
33
|
collapsed?: boolean;
|
|
28
34
|
onToggle?: () => void;
|
|
29
35
|
}) {
|
|
30
36
|
const text = (
|
|
31
37
|
<>
|
|
38
|
+
{Icon && <Icon aria-hidden className="h-3 w-3 shrink-0" />}
|
|
32
39
|
{label}
|
|
33
|
-
{count !== undefined &&
|
|
40
|
+
{count !== undefined && (
|
|
41
|
+
<>
|
|
42
|
+
{" · "}
|
|
43
|
+
<span className={cn(accent && "text-amber-600 dark:text-amber-400")}>
|
|
44
|
+
{count}
|
|
45
|
+
</span>
|
|
46
|
+
</>
|
|
47
|
+
)}
|
|
34
48
|
{onToggle && (
|
|
35
49
|
<ChevronDown
|
|
36
50
|
className={cn(
|
|
@@ -63,15 +77,19 @@ function ShelfLabel({
|
|
|
63
77
|
function Shelf({
|
|
64
78
|
label,
|
|
65
79
|
count,
|
|
80
|
+
icon,
|
|
81
|
+
accent,
|
|
66
82
|
children,
|
|
67
83
|
}: {
|
|
68
84
|
label: string;
|
|
69
85
|
count?: number;
|
|
86
|
+
icon?: LucideIcon;
|
|
87
|
+
accent?: boolean;
|
|
70
88
|
children: ReactNode;
|
|
71
89
|
}) {
|
|
72
90
|
return (
|
|
73
91
|
<section aria-label={label}>
|
|
74
|
-
<ShelfLabel label={label} count={count} />
|
|
92
|
+
<ShelfLabel label={label} count={count} icon={icon} accent={accent} />
|
|
75
93
|
{children}
|
|
76
94
|
</section>
|
|
77
95
|
);
|
|
@@ -80,9 +98,9 @@ function Shelf({
|
|
|
80
98
|
// A linked machine's sessions no project here holds, under its name.
|
|
81
99
|
export function MachineShelves({ groups }: { groups: MachineGroup[] }) {
|
|
82
100
|
return groups.map((g) => (
|
|
83
|
-
<Shelf key={g.hostId} label={g.name} count={g.rows.length}>
|
|
101
|
+
<Shelf key={g.hostId} label={g.name} count={g.rows.length} icon={Server}>
|
|
84
102
|
{g.rows.map((r) => (
|
|
85
|
-
<SessionRow key={r.session.id} row={r} />
|
|
103
|
+
<SessionRow key={r.session.id} row={r} inMachineShelf />
|
|
86
104
|
))}
|
|
87
105
|
</Shelf>
|
|
88
106
|
));
|
|
@@ -108,6 +126,17 @@ export function SessionShelves({
|
|
|
108
126
|
const shown = shelves.done.slice(0, doneLimit(donePages));
|
|
109
127
|
const hidden = shelves.done.length - shown.length;
|
|
110
128
|
const summary = doneSummary(shelves.done).text;
|
|
129
|
+
// Live work above, what's resting below a hairline.
|
|
130
|
+
const live =
|
|
131
|
+
shelves.orchestrators.length +
|
|
132
|
+
toStart.length +
|
|
133
|
+
shelves.pinned.length +
|
|
134
|
+
shelves.needsYou.length +
|
|
135
|
+
shelves.working.length +
|
|
136
|
+
shelves.inReview.length +
|
|
137
|
+
queued.length +
|
|
138
|
+
shelves.ongoing.length >
|
|
139
|
+
0;
|
|
111
140
|
return (
|
|
112
141
|
<>
|
|
113
142
|
{shelves.orchestrators.length + toStart.length > 0 && (
|
|
@@ -128,7 +157,7 @@ export function SessionShelves({
|
|
|
128
157
|
</Shelf>
|
|
129
158
|
)}
|
|
130
159
|
{shelves.needsYou.length > 0 && (
|
|
131
|
-
<Shelf label="Needs you" count={shelves.needsYou.length}>
|
|
160
|
+
<Shelf label="Needs you" count={shelves.needsYou.length} accent>
|
|
132
161
|
{shelves.needsYou.map((r) => (
|
|
133
162
|
<SessionRow key={r.session.id} row={r} />
|
|
134
163
|
))}
|
|
@@ -149,7 +178,7 @@ export function SessionShelves({
|
|
|
149
178
|
</Shelf>
|
|
150
179
|
)}
|
|
151
180
|
{queued.length > 0 && (
|
|
152
|
-
<Shelf label="Queued" count={queued.length}>
|
|
181
|
+
<Shelf label="Queued" count={queued.length} icon={ListTodo}>
|
|
153
182
|
{queued.map((r) => (
|
|
154
183
|
<QueuedRow key={r.item.id} row={r} />
|
|
155
184
|
))}
|
|
@@ -162,6 +191,9 @@ export function SessionShelves({
|
|
|
162
191
|
))}
|
|
163
192
|
</Shelf>
|
|
164
193
|
)}
|
|
194
|
+
{live && shelves.snoozed.length + shelves.done.length > 0 && (
|
|
195
|
+
<div aria-hidden className="border-border mx-2.5 mt-2.5 border-t" />
|
|
196
|
+
)}
|
|
165
197
|
{shelves.snoozed.length > 0 && (
|
|
166
198
|
<section aria-label="Snoozed">
|
|
167
199
|
<ShelfLabel
|
|
@@ -4,7 +4,9 @@ import { DevServerCard } from "@/components/DevServers/DevServerCard";
|
|
|
4
4
|
import { ElsewhereTmuxList, TmuxSessionRow } from "@/components/Hosts";
|
|
5
5
|
import { useDevServersQuery } from "@/data/dev-servers";
|
|
6
6
|
import { useDiscoveredTmuxQuery } from "@/data/hosts";
|
|
7
|
+
import { SquareTerminal } from "lucide-react";
|
|
7
8
|
import type { ProjectWithRepositories } from "@/lib/projects";
|
|
9
|
+
import { ShelfLabel } from "./SessionShelves";
|
|
8
10
|
|
|
9
11
|
interface DevServerHandlers {
|
|
10
12
|
onStop: (id: string) => Promise<void>;
|
|
@@ -58,7 +60,11 @@ export function SidebarExtras({
|
|
|
58
60
|
)}
|
|
59
61
|
{tmux.length > 0 && (
|
|
60
62
|
<section aria-label="Terminals">
|
|
61
|
-
<
|
|
63
|
+
<ShelfLabel
|
|
64
|
+
label="Terminals"
|
|
65
|
+
count={tmux.length}
|
|
66
|
+
icon={SquareTerminal}
|
|
67
|
+
/>
|
|
62
68
|
{tmux.map((t) => (
|
|
63
69
|
<TmuxSessionRow key={`${t.hostId}:${t.name}`} session={t} />
|
|
64
70
|
))}
|
|
@@ -12,6 +12,7 @@ import type { OrchestratorOverview } from "@/lib/orchestrator/overview";
|
|
|
12
12
|
import { useSessionListMutations } from "./hooks/useSessionListMutations";
|
|
13
13
|
import { useBulkDelete } from "./hooks/useBulkDelete";
|
|
14
14
|
import { useSidebarData } from "./useSidebarData";
|
|
15
|
+
import { useSessionTips } from "./useSessionTips";
|
|
15
16
|
import { RowProvider, type RowContextValue } from "./RowContext";
|
|
16
17
|
import { SidebarHeader } from "./SidebarHeader";
|
|
17
18
|
import { useAddProject } from "@/components/Projects/AddProject/useAddProject";
|
|
@@ -43,6 +44,7 @@ export function SessionList({
|
|
|
43
44
|
pinControls,
|
|
44
45
|
}: SessionListProps) {
|
|
45
46
|
const data = useSidebarData(sessionStatuses);
|
|
47
|
+
useSessionTips(data);
|
|
46
48
|
const { shelves, ui, project } = data;
|
|
47
49
|
const mutations = useSessionListMutations({ onSelectSession: onSelect });
|
|
48
50
|
const bulkDelete = useBulkDelete();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
4
|
+
import { useHostNames } from "@/data/hosts";
|
|
5
|
+
import type { SidebarRow } from "@/lib/sidebar/shelves";
|
|
6
|
+
import type { SidebarData } from "./useSidebarData";
|
|
7
|
+
|
|
8
|
+
function find(rows: readonly SidebarRow[], id: string): SidebarRow | null {
|
|
9
|
+
for (const row of rows) {
|
|
10
|
+
if (row.session.id === id) return row;
|
|
11
|
+
const nested = find(row.workers, id) ?? find(row.quick ?? [], id);
|
|
12
|
+
if (nested) return nested;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// A session row's hover tip, built from the latest list only when it
|
|
18
|
+
// opens: nothing runs per row, and registering never re-renders.
|
|
19
|
+
export function useSessionTips(data: SidebarData) {
|
|
20
|
+
const hostNames = useHostNames();
|
|
21
|
+
const latest = useRef({ data, hostNames });
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
latest.current = { data, hostNames };
|
|
24
|
+
});
|
|
25
|
+
// Loaded after the page, like the tip itself: no first-load cost.
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
let off: (() => void) | undefined;
|
|
28
|
+
let gone = false;
|
|
29
|
+
Promise.all([
|
|
30
|
+
import("@/lib/hover-tip"),
|
|
31
|
+
import("@/lib/sidebar/row-details"),
|
|
32
|
+
]).then(([{ registerTip }, { rowDetails }]) => {
|
|
33
|
+
if (gone) return;
|
|
34
|
+
off = registerTip("session", (id) => {
|
|
35
|
+
const { data, hostNames } = latest.current;
|
|
36
|
+
const row = find(
|
|
37
|
+
[
|
|
38
|
+
...Object.values(data.shelves).flat(),
|
|
39
|
+
...data.machines.flatMap((g) => g.rows),
|
|
40
|
+
],
|
|
41
|
+
id
|
|
42
|
+
);
|
|
43
|
+
if (!row) return null;
|
|
44
|
+
const s = row.session;
|
|
45
|
+
return rowDetails(row, {
|
|
46
|
+
project: data.projectNames.get(s.project_id ?? ""),
|
|
47
|
+
startedBy: s.started_by
|
|
48
|
+
? data.orchestratorNames.get(s.started_by)
|
|
49
|
+
: undefined,
|
|
50
|
+
host:
|
|
51
|
+
s.host_id && s.host_id !== "local"
|
|
52
|
+
? hostNames[s.host_id] || "remote"
|
|
53
|
+
: undefined,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
return () => {
|
|
58
|
+
gone = true;
|
|
59
|
+
off?.();
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
// Mounted once: the app's single hover tooltip (lib/hover-tip.ts), loaded
|
|
6
|
+
// after the page so it costs the first load nothing.
|
|
7
|
+
export function HoverTip() {
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
let off: (() => void) | undefined;
|
|
10
|
+
let gone = false;
|
|
11
|
+
import("@/lib/hover-tip").then(({ installHoverTip }) => {
|
|
12
|
+
if (!gone) off = installHoverTip();
|
|
13
|
+
});
|
|
14
|
+
return () => {
|
|
15
|
+
gone = true;
|
|
16
|
+
off?.();
|
|
17
|
+
};
|
|
18
|
+
}, []);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
package/contexts/PaneContext.tsx
CHANGED
|
@@ -26,7 +26,13 @@ import {
|
|
|
26
26
|
} from "@/lib/panes";
|
|
27
27
|
import { useViewport } from "@/hooks/useViewport";
|
|
28
28
|
|
|
29
|
-
export type ViewMode =
|
|
29
|
+
export type ViewMode =
|
|
30
|
+
| "terminal"
|
|
31
|
+
| "files"
|
|
32
|
+
| "git"
|
|
33
|
+
| "workers"
|
|
34
|
+
| "log"
|
|
35
|
+
| "artifacts";
|
|
30
36
|
|
|
31
37
|
export interface PaneViewState {
|
|
32
38
|
viewMode: ViewMode;
|
package/data/presence/index.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
startAuthentication,
|
|
3
|
-
startRegistration,
|
|
4
|
-
} from "@simplewebauthn/browser";
|
|
5
1
|
import type {
|
|
6
2
|
AuthenticationResponseJSON,
|
|
7
3
|
PublicKeyCredentialCreationOptionsJSON,
|
|
@@ -30,12 +26,20 @@ export const passkeysHere = () =>
|
|
|
30
26
|
export const NO_PASSKEYS_HERE =
|
|
31
27
|
"Approving this needs a passkey, and passkeys need https or localhost: approve it on the machine running AgentOS, or over Connect.";
|
|
32
28
|
|
|
29
|
+
// The passkey library loads only when a passkey is used, fetched alongside
|
|
30
|
+
// the server's options so the prompt waits on nothing extra.
|
|
31
|
+
const webauthn = () => import("@simplewebauthn/browser");
|
|
32
|
+
|
|
33
33
|
// Adds a passkey on this browser: free for the very first one, otherwise
|
|
34
34
|
// with a code from a device that has one.
|
|
35
35
|
export async function registerPasskey(enrollCode?: string): Promise<void> {
|
|
36
|
-
const { options } = await
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const [{ startRegistration }, { options }] = await Promise.all([
|
|
37
|
+
webauthn(),
|
|
38
|
+
post<{ options: PublicKeyCredentialCreationOptionsJSON }>(
|
|
39
|
+
"/api/presence/register/options",
|
|
40
|
+
{ enrollCode }
|
|
41
|
+
),
|
|
42
|
+
]);
|
|
39
43
|
const response = await startRegistration({ optionsJSON: options });
|
|
40
44
|
await post("/api/presence/register/verify", { response, enrollCode });
|
|
41
45
|
}
|
|
@@ -55,6 +59,8 @@ export async function provePresence(req: {
|
|
|
55
59
|
needsPasskey?: boolean;
|
|
56
60
|
canBootstrap?: boolean;
|
|
57
61
|
};
|
|
62
|
+
const lib = webauthn();
|
|
63
|
+
lib.catch(() => {}); // awaited below; an early throw mustn't leave it unhandled
|
|
58
64
|
let answer = await post<Answer>("/api/presence/options", req);
|
|
59
65
|
if (answer.needsPasskey) {
|
|
60
66
|
if (!answer.canBootstrap)
|
|
@@ -65,5 +71,6 @@ export async function provePresence(req: {
|
|
|
65
71
|
answer = await post<Answer>("/api/presence/options", req);
|
|
66
72
|
}
|
|
67
73
|
if (!answer.options) throw new Error("No passkey for this host");
|
|
74
|
+
const { startAuthentication } = await lib;
|
|
68
75
|
return startAuthentication({ optionsJSON: answer.options });
|
|
69
76
|
}
|