Skip to content
585 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions1import {
2 ArrowLeft,
3 Ban,
4 BookmarkPlus,
5 Check,
6 CircleAlert,
7 CircleDot,
8 CornerDownRight,
9 EyeOff,
10 Flag,
11 GitBranch,
12 Lock,
13 Megaphone,
14 MessageSquare,
15 Send,
16 Square,
17 Wrench,
18} from "lucide-react";
19import { type ReactNode, useEffect, useRef } from "react";
20import { Link, data, redirect, useFetcher } from "react-router";
21
22import type { AgentSession, AgentSessionDetail, SessionEvent } from "@g1t/contracts";
23
24import type { Route } from "./+types/session";
25import { AgentAvatar } from "../../../components/agent-avatar";
26import { agentsAction, answer } from "../../../components/agents/actions.server";
27import { type ActionResult, ApproveDialog, BUTTONS, Confirm } from "../../../components/agents/dialogs";
28import { isLive, kindLabel, sessionRows, whereLabel } from "../../../components/agents/format";
29import { KindBadge, Meter, PrivateTitle, SpendOfCap, StatusChip, sessionHref, stepsLine } from "../../../components/agents/parts";
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.30import { skillsPath } from "../../../components/agents/skills";
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions31import { Markdown } from "../../../components/markdown";
32import { TimeAgo } from "../../../components/ui";
33import { Hint } from "../../../components/ui/hint";
34import { Textarea } from "../../../components/ui/textarea";
35import { microsFromDollars } from "../../../lib/agent-form";
36import { channelPath } from "../../../lib/chat";
37import { cn } from "../../../lib/cn";
38import { page } from "../../../lib/meta";
39import { useRefreshWhile } from "../../../lib/refresh";
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.40import { skillLibrary, workspaceAgents } from "../../../lib/services.server";
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions41import { requireUser, roleIn } from "../../../lib/session.server";
42
43export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
44 const session = loaderData?.detail?.session;
45 const title = session?.visible ? session.title : "A session";
46 return page(args, { title: `${title} · @${params.handle} · ${params.owner} · g1t` });
47}
48
49/** One session: its transcript, its tree, what it produced, and what the viewer may do to it. */
50export async function loader({ params, context, request }: Route.LoaderArgs): Promise<{ detail: AgentSessionDetail | null }> {
51 const viewer = requireUser(context, request);
52 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
53 const found = await workspaceAgents.session(params.owner.toLowerCase(), params.id, viewer).catch(() => null);
54 if (found && !found.ok && found.error.code === "not_found") throw data(null, { status: 404 });
55 const detail = found?.ok ? found.value : null;
56 // A session is under the agent that runs it.
57 if (detail && detail.session.agent_handle !== params.handle.toLowerCase()) {
58 throw redirect(`/${params.owner}/-/agents/${detail.session.agent_handle}/sessions/${detail.session.id}`);
59 }
60 return { detail };
61}
62
63/** Stop it (and everything under it), approve more spend, or say something to it. */
64export async function action({ params, context, request }: Route.ActionArgs): Promise<ActionResult> {
65 const { viewer, slug, form } = await agentsAction(request, context, params.owner);
66 const intent = String(form.get("intent") ?? "");
67 if (intent === "stop") return answer(intent, workspaceAgents.stopSession(slug, params.id, viewer));
68 if (intent === "approve") {
69 const cap = microsFromDollars(form.get("cap"));
70 if (cap == null || Number.isNaN(cap)) return { ok: false, intent, error: "Write the new cap in dollars, such as 5 or 2.50." };
71 return answer(intent, workspaceAgents.approveSession(slug, params.id, viewer, cap));
72 }
73 if (intent === "steer") {
74 const body = String(form.get("body") ?? "").trim();
75 if (!body) return { ok: false, intent, error: "Write something to send." };
76 return answer(intent, workspaceAgents.steerSession(slug, params.id, viewer, body));
77 }
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.78 // Save as skill: the agent drafts one from this session, for a person to review.
79 if (intent === "save_skill") {
80 const drafted = await skillLibrary.draftFromSession(slug, viewer, params.id).catch(() => null);
81 if (!drafted) return { ok: false, intent, error: "The agents service didn't answer. Try again in a moment." };
82 if (!drafted.ok) return { ok: false, intent, error: drafted.error.message };
83 throw redirect(skillsPath(slug, drafted.value.skill.name, drafted.value.skill.can_edit ? "/edit" : ""));
84 }
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions85 return { ok: false, intent, error: "Unknown request." };
86}
87
88export default function SessionPage({ loaderData, params }: Route.ComponentProps) {
89 const { detail } = loaderData;
90 const slug = params.owner;
91 const live = Boolean(detail && isLive(detail.session.status));
92 useRefreshWhile(live);
93 const back = (
94 <Link to={`/${slug}/-/agents/${params.handle}`} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
95 <ArrowLeft size={14} />
96 Sessions
97 </Link>
98 );
99 if (!detail) {
100 return (
101 <div className="space-y-4">
102 {back}
103 <div className="rounded-xl border border-dashed border-line px-6 py-14 text-center">
104 <p className="font-medium">This session can&apos;t be shown right now</p>
105 <p className="mt-1.5 text-sm text-muted">The agents service didn&apos;t answer. Reload in a moment.</p>
106 </div>
107 </div>
108 );
109 }
110 const { session, events, tree } = detail;
111 return (
112 <div className="space-y-8">
113 {back}
114 <Header slug={slug} detail={detail} />
115 <div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_17rem]">
116 <div className="min-w-0 space-y-6">
117 {!session.visible ? (
118 <div className="flex items-start gap-3 rounded-xl border border-line bg-surface px-4 py-4 text-sm text-muted">
119 <Lock size={16} className="mt-0.5 shrink-0 text-faint" />
120 <p>
121 This session came from a conversation you&apos;re not in. You can see that it ran, how far it got and what it cost, but not what it was about.
122 </p>
123 </div>
124 ) : (
125 <>
126 {session.summary && !events.some((e) => e.kind === "result") && <Report body={session.summary} />}
127 <Transcript events={events} agentHandle={session.agent_handle} agentSeed={session.agent_avatar_seed} live={live} />
128 {detail.can_steer && <Steer session={session} live={live} />}
129 </>
130 )}
131 </div>
132 <aside className="space-y-6">
133 <TreePanel slug={slug} tree={tree} current={session.id} />
134 {session.visible && <Outputs slug={slug} session={session} />}
135 </aside>
136 </div>
137 </div>
138 );
139}
140
141function Header({ slug, detail }: { slug: string; detail: AgentSessionDetail }) {
142 const { session } = detail;
143 const chat = session.visible
144 ? `${channelPath(slug, { id: session.channel_id, kind: session.channel_kind, name: session.channel_name })}${session.card_message_id ? `?thread=${encodeURIComponent(session.card_message_id)}` : ""}`
145 : null;
146 return (
147 <header className="rounded-2xl border border-line bg-surface p-5 sm:p-6">
148 <div className="flex flex-wrap items-center gap-2">
149 <KindBadge kind={session.kind} subagent={session.subagent} />
150 <StatusChip status={session.status} />
151 </div>
152 <h1 className="mt-3 text-xl font-semibold tracking-tight text-balance sm:text-2xl">
153 {session.visible ? session.title || "Untitled session" : <PrivateTitle className="not-italic" />}
154 </h1>
155 {session.visible && session.status_note && (
156 <p className={cn("mt-2 text-sm", session.status === "failed" ? "text-danger" : session.status === "needs_approval" ? "text-warn" : "text-muted")}>{session.status_note}</p>
157 )}
158 <dl className="mt-5 grid gap-x-6 gap-y-3 text-sm sm:grid-cols-2 lg:grid-cols-4">
159 <Meta label="Agent">
160 <Link to={`/${slug}/-/agents/${session.agent_handle}`} className="inline-flex items-center gap-1.5 hover:underline">
161 <AgentAvatar agent={{ handle: session.agent_handle, avatar_seed: session.agent_avatar_seed }} size={18} />
162 {session.agent_name}
163 </Link>
164 </Meta>
165 <Meta label="Asked by">{session.visible && session.asked_by_username ? `@${session.asked_by_username}` : session.kind === "routine" ? "A routine" : "—"}</Meta>
166 <Meta label="Where">{session.visible ? whereLabel(session) : "A conversation you're not in"}</Meta>
167 <Meta label="Started">
168 <TimeAgo at={session.created_at} />
169 {session.finished_at && (
170 <span className="text-faint">
171 {" "}
172 · ended <TimeAgo at={session.finished_at} />
173 </span>
174 )}
175 </Meta>
176 <Meta label="Model">{session.model ? <span className="font-mono text-[0.8125rem]">{session.model}</span> : "—"}</Meta>
177 <Meta label="Work">{stepsLine(session)}</Meta>
178 <Meta label="Tokens">
179 <span className="tabular-nums">
180 {session.input_tokens.toLocaleString("en-US")} in · {session.output_tokens.toLocaleString("en-US")} out
181 </span>
182 </Meta>
183 <Meta label={session.parent_id ? "Spent (paid by its root)" : "Spent"}>
184 <SpendOfCap spent={session.charged_micros} cap={session.cap_micros} />
185 {session.cap_micros != null && <Meter spent={session.charged_micros} cap={session.cap_micros} label="Spent of its cap" size="sm" className="mt-1.5" />}
186 </Meta>
187 </dl>
188 {(detail.can_stop || detail.can_approve || chat) && (
189 <div className="mt-5 flex flex-wrap gap-2 border-t border-line pt-4">
190 {detail.can_approve && (
191 <ApproveDialog
192 slug={slug}
193 session={session}
194 trigger={
195 <button type="button" className={`${BUTTONS.PRIMARY} h-9 py-0`}>
196 <Check size={15} />
197 Approve more…
198 </button>
199 }
200 />
201 )}
202 {chat && (
203 <Link to={chat} className={`${BUTTONS.QUIET} h-9 py-0`}>
204 <MessageSquare size={15} />
205 Open in chat
206 </Link>
207 )}
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.208 {session.visible && session.status === "done" && <SaveAsSkill agentName={session.agent_name} />}
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions209 {detail.can_stop && (
210 <Confirm
211 title="Stop this session?"
212 confirm="Stop session"
213 fields={{ intent: "stop" }}
214 fetcherKey={`stop-${session.id}`}
215 trigger={
216 <button type="button" className={`${BUTTONS.QUIET} h-9 py-0 hover:border-danger/50 hover:text-danger`}>
217 <Square size={13} />
218 Stop
219 </button>
220 }
221 >
222 {session.agent_name} stops where it is, and so does every session it started. What it spent so far stays spent; its card in the conversation says it was
223 stopped.
224 </Confirm>
225 )}
226 </div>
227 )}
228 </header>
229 );
230}
231
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.232/** Save as skill: the agent drafts a skill from this session; a person reviews it before any agent uses it. */
233function SaveAsSkill({ agentName }: { agentName: string }) {
234 const fetcher = useFetcher<ActionResult>({ key: "save-skill" });
235 const busy = fetcher.state !== "idle";
236 const error = fetcher.state === "idle" && fetcher.data && !fetcher.data.ok ? fetcher.data.error : null;
237 return (
238 <fetcher.Form method="post" className="contents">
239 <input type="hidden" name="intent" value="save_skill" />
240 <Hint label={`${agentName} drafts a skill from this session, so the work can be done the same way again. It is billed like a short step, and no agent uses it until it is reviewed and published.`}>
241 <button type="submit" className={`${BUTTONS.QUIET} h-9 py-0`} disabled={busy}>
242 <BookmarkPlus size={15} />
243 {busy ? "Drafting…" : "Save as skill"}
244 </button>
245 </Hint>
246 {error && (
247 <p role="alert" className="w-full text-sm text-danger">
248 {error}
249 </p>
250 )}
251 </fetcher.Form>
252 );
253}
254
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions255function Meta({ label, children }: { label: string; children: ReactNode }) {
256 return (
257 <div className="min-w-0">
258 <dt className="text-xs text-faint">{label}</dt>
259 <dd className="mt-0.5 truncate">{children}</dd>
260 </div>
261 );
262}
263
264const OUTCOMES: Record<string, { icon: ReactNode; label: string }> = {
265 allowed: { icon: <Check size={13} className="text-success" />, label: "Read" },
266 withheld: { icon: <EyeOff size={13} className="text-warn" />, label: "Withheld: the audience can't see it" },
267 refused: { icon: <Ban size={13} className="text-danger" />, label: "Refused" },
268 error: { icon: <CircleAlert size={13} className="text-danger" />, label: "Failed" },
269};
270
271/** The session's transcript, oldest first: its goal, what it said and did, who steered it, and its report. */
272function Transcript({ events, agentHandle, agentSeed, live }: { events: SessionEvent[]; agentHandle: string; agentSeed: string; live: boolean }) {
273 if (events.length === 0) {
274 return <p className="rounded-xl border border-dashed border-line px-4 py-8 text-center text-sm text-faint">{live ? "Getting started…" : "Nothing was recorded for this session."}</p>;
275 }
276 return (
277 <section aria-label="Transcript">
278 <ol className="relative space-y-1 before:absolute before:top-3 before:bottom-3 before:left-[0.6875rem] before:w-px before:bg-line">
279 {events.map((event) => (
280 <EventItem key={event.seq} event={event} agentHandle={agentHandle} agentSeed={agentSeed} />
281 ))}
282 {live && (
283 <li className="relative flex items-center gap-3 py-2 pl-0 text-sm text-faint">
284 <span className="relative z-10 flex size-6 items-center justify-center rounded-full bg-bg">
285 <CircleDot size={13} className="animate-pulse text-accent motion-reduce:animate-none" />
286 </span>
287 Working…
288 </li>
289 )}
290 </ol>
291 </section>
292 );
293}
294
295function Rail({ children }: { children: ReactNode }) {
296 return <span className="relative z-10 flex size-6 shrink-0 items-center justify-center rounded-full bg-bg ring-1 ring-line">{children}</span>;
297}
298
299function EventItem({ event, agentHandle, agentSeed }: { event: SessionEvent; agentHandle: string; agentSeed: string }) {
300 const when = (
301 <span className="ml-auto shrink-0 text-[0.6875rem] text-faint">
302 <TimeAgo at={event.created_at} />
303 </span>
304 );
305 switch (event.kind) {
306 case "goal":
307 return (
308 <li className="relative flex gap-3 py-2">
309 <Rail>
310 <Flag size={12} className="text-accent" />
311 </Rail>
312 <div className="min-w-0 grow rounded-lg border border-accent/30 bg-accent/[0.06] px-3.5 py-2.5">
313 <p className="flex items-center gap-2 text-xs font-medium text-accent">Goal{when}</p>
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar314 <p className="mt-1 text-sm whitespace-pre-wrap wrap-break-word text-fg">{event.body}</p>
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions315 </div>
316 </li>
317 );
318 case "text":
319 return (
320 <li className="relative flex gap-3 py-2">
321 <span className="relative z-10 shrink-0">
322 <AgentAvatar agent={{ handle: event.by ?? agentHandle, avatar_seed: event.by && event.by !== agentHandle ? event.by : agentSeed }} size={24} />
323 </span>
324 <div className="min-w-0 grow">
325 <p className="flex items-center gap-2 text-xs text-muted">
326 <span className="font-medium text-fg">{event.by ? `@${event.by}` : "g1t"}</span>
327 {when}
328 </p>
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar329 <p className="mt-0.5 text-sm whitespace-pre-wrap wrap-break-word text-fg-soft">{event.body}</p>
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions330 </div>
331 </li>
332 );
333 case "tool": {
334 const outcome = OUTCOMES[event.outcome ?? ""] ?? { icon: <Wrench size={12} className="text-faint" />, label: event.outcome ?? "Called" };
335 const long = event.body.length > 140;
336 return (
337 <li className="relative flex gap-3 py-1">
338 <Rail>
339 <Hint label={outcome.label}>
340 <span className="flex" aria-label={outcome.label}>
341 {outcome.icon}
342 </span>
343 </Hint>
344 </Rail>
345 <div className="min-w-0 grow pt-0.5">
346 {long ? (
347 <details className="group">
348 <summary className="flex cursor-pointer list-none items-center gap-2 text-[0.8125rem] [&::-webkit-details-marker]:hidden">
349 <span className="font-mono text-fg">{event.tool ?? "tool"}</span>
350 <span className="min-w-0 truncate font-mono text-xs text-faint group-open:hidden">{event.body}</span>
351 {when}
352 </summary>
353 <pre className="mt-1.5 overflow-x-auto rounded-md border border-line bg-bg px-3 py-2 font-mono text-xs whitespace-pre-wrap text-muted">{event.body}</pre>
354 </details>
355 ) : (
356 <p className="flex items-center gap-2 text-[0.8125rem]">
357 <span className="font-mono text-fg">{event.tool ?? "tool"}</span>
358 <span className="min-w-0 truncate font-mono text-xs text-faint">{event.body}</span>
359 {when}
360 </p>
361 )}
362 </div>
363 </li>
364 );
365 }
366 case "steer":
367 return (
368 <li className="relative flex gap-3 py-2">
369 <Rail>
370 <MessageSquare size={12} className="text-info" />
371 </Rail>
372 <div className="min-w-0 grow rounded-lg border border-info/30 bg-info/[0.06] px-3.5 py-2.5">
373 <p className="flex items-center gap-2 text-xs text-info">
374 <span className="font-medium">{event.by ? `@${event.by}` : "Someone"}</span> steered it{when}
375 </p>
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar376 <p className="mt-1 text-sm whitespace-pre-wrap wrap-break-word text-fg">{event.body}</p>
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions377 </div>
378 </li>
379 );
380 case "update":
381 return (
382 <li className="relative flex gap-3 py-2">
383 <Rail>
384 <Megaphone size={12} className="text-muted" />
385 </Rail>
386 <div className="min-w-0 grow">
387 <p className="flex items-center gap-2 text-xs text-muted">Posted an update in the conversation{when}</p>
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar388 <p className="mt-0.5 text-sm whitespace-pre-wrap wrap-break-word text-fg-soft">{event.body}</p>
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions389 </div>
390 </li>
391 );
392 case "child":
393 return (
394 <li className="relative flex gap-3 py-2">
395 <Rail>
396 <GitBranch size={12} className="text-merged" />
397 </Rail>
398 <div className="min-w-0 grow">
399 <p className="flex items-center gap-2 text-xs text-muted">
400 {event.by ? <span className="font-medium text-fg">@{event.by}</span> : null} reported back{when}
401 </p>
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar402 <p className="mt-0.5 text-sm whitespace-pre-wrap wrap-break-word text-fg-soft">{event.body}</p>
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions403 </div>
404 </li>
405 );
406 case "result":
407 return (
408 <li className="relative flex gap-3 py-2">
409 <Rail>
410 <Check size={12} className="text-success" />
411 </Rail>
412 <div className="min-w-0 grow">
413 <Report body={event.body} when={event.created_at} />
414 </div>
415 </li>
416 );
417 default:
418 return (
419 <li className="relative flex gap-3 py-1.5">
420 <Rail>
421 <CircleDot size={11} className="text-faint" />
422 </Rail>
423 <p className="flex min-w-0 grow items-center gap-2 pt-0.5 text-xs text-faint italic">
424 <span className="min-w-0">{event.body}</span>
425 {when}
426 </p>
427 </li>
428 );
429 }
430}
431
432/** What it found or did: its report, rendered. */
433function Report({ body, when }: { body: string; when?: string }) {
434 return (
435 <section className="rounded-xl border border-success/30 bg-surface">
436 <header className="flex items-center gap-2 border-b border-line px-4 py-2 text-xs font-medium text-success">
437 Report
438 {when && (
439 <span className="ml-auto font-normal text-faint">
440 <TimeAgo at={when} />
441 </span>
442 )}
443 </header>
444 <div className="px-4 py-3 text-sm">
445 <Markdown source={body} />
446 </div>
447 </section>
448 );
449}
450
451/** Say something to the session: it reads it before its next step, or picks back up if it had finished. */
452function Steer({ session, live }: { session: AgentSession; live: boolean }) {
453 const fetcher = useFetcher<ActionResult>({ key: `steer-${session.id}` });
454 const form = useRef<HTMLFormElement>(null);
455 const busy = fetcher.state !== "idle";
456 useEffect(() => {
457 if (fetcher.state === "idle" && fetcher.data?.ok) form.current?.reset();
458 }, [fetcher.state, fetcher.data]);
459 return (
460 <fetcher.Form ref={form} method="post" className="rounded-xl border border-line bg-surface p-3 focus-within:border-accent-dim">
461 <input type="hidden" name="intent" value="steer" />
462 <label htmlFor="steer" className="sr-only">
463 Message this session
464 </label>
465 <Textarea
466 id="steer"
467 name="body"
468 rows={2}
469 maxLength={4000}
470 required
471 placeholder={live ? `Message this session: ${session.agent_name} reads it before its next step` : `Message this session: ${session.agent_name} picks it back up`}
472 className="min-h-14 border-0 bg-transparent px-1 hover:border-0 focus-visible:ring-0"
473 onKeyDown={(event) => {
474 if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
475 event.preventDefault();
476 event.currentTarget.form?.requestSubmit();
477 }
478 }}
479 />
480 <div className="mt-2 flex items-center justify-between gap-3">
481 <p className="text-xs text-faint">
482 {fetcher.data && !fetcher.data.ok ? <span className="text-danger">{fetcher.data.error}</span> : live ? "It reads this before its next step." : "It picks the session back up with everything it knew."}
483 </p>
484 <button type="submit" disabled={busy} className={`${BUTTONS.PRIMARY} h-8 py-0`}>
485 <Send size={14} />
486 {busy ? "Sending…" : "Send"}
487 </button>
488 </div>
489 </fetcher.Form>
490 );
491}
492
493/** Every session in this one's tree, root first, each under the one that started it. */
494function TreePanel({ slug, tree, current }: { slug: string; tree: AgentSession[]; current: string }) {
495 const rows = sessionRows(tree);
496 return (
497 <section>
498 <h2 className="text-xs font-medium tracking-wide text-faint uppercase">Session tree</h2>
499 {rows.length <= 1 ? (
500 <p className="mt-2 text-sm text-muted">It hasn&apos;t brought in a colleague or a subagent. If it does, they show here, paid from this session.</p>
501 ) : (
502 <ul className="mt-2 space-y-0.5">
503 {rows.map(({ session, depth }) => {
504 const here = session.id === current;
505 const body = (
506 <>
507 {depth > 0 && <CornerDownRight size={12} className="shrink-0 text-faint" />}
508 <AgentAvatar agent={{ handle: session.agent_handle, avatar_seed: session.agent_avatar_seed }} size={18} />
509 <span className="min-w-0 grow">
510 <span className="block truncate text-[0.8125rem]">{session.visible ? session.title : "A private session"}</span>
511 <span className="block truncate text-[0.6875rem] text-faint">
512 {session.subagent ? `${session.agent_name} · ${session.subagent}` : session.agent_name} · {kindLabel(session.kind)}
513 </span>
514 </span>
515 <StatusChip status={session.status} className="px-1.5" />
516 </>
517 );
518 return (
519 <li key={session.id} style={{ paddingLeft: `${depth * 0.875}rem` }}>
520 {here ? (
521 <div className="flex items-center gap-2 rounded-md bg-raised px-2 py-1.5" aria-current="page">
522 {body}
523 </div>
524 ) : session.visible ? (
525 <Link to={sessionHref(slug, session)} className="flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-raised/60">
526 {body}
527 </Link>
528 ) : (
529 <div className="flex items-center gap-2 px-2 py-1.5 text-muted">{body}</div>
530 )}
531 </li>
532 );
533 })}
534 </ul>
535 )}
536 <p className="mt-3 text-xs text-faint">Everything in the tree is charged to the agent at its root.</p>
537 </section>
538 );
539}
540
541/** What it produced: issues filed, sessions started, memories kept. */
542function Outputs({ slug, session }: { slug: string; session: AgentSession }) {
543 return (
544 <section>
545 <h2 className="text-xs font-medium tracking-wide text-faint uppercase">What it produced</h2>
546 {session.outputs.length === 0 ? (
547 <p className="mt-2 text-sm text-muted">Nothing yet. Issues it files, sessions it starts and facts it keeps show here.</p>
548 ) : (
549 <ul className="mt-2 space-y-1">
550 {session.outputs.map((output, i) => (
551 <li key={i} className="text-sm">
552 {output.kind === "issue" ? (
553 <Link to={`/${output.repo}/issues/${output.number}`} className="flex items-start gap-2 rounded-md px-2 py-1.5 hover:bg-raised/60">
554 <CircleDot size={14} className="mt-0.5 shrink-0 text-success" />
555 <span className="min-w-0">
556 <span className="block truncate">{output.title}</span>
557 <span className="block font-mono text-[0.6875rem] text-faint">
558 {output.repo}#{output.number}
559 </span>
560 </span>
561 </Link>
562 ) : output.kind === "session" ? (
563 <Link to={`/${slug}/-/agents/${output.agent_handle}/sessions/${output.id}`} className="flex items-start gap-2 rounded-md px-2 py-1.5 hover:bg-raised/60">
564 <GitBranch size={14} className="mt-0.5 shrink-0 text-merged" />
565 <span className="min-w-0">
566 <span className="block truncate">{output.title}</span>
567 <span className="block text-[0.6875rem] text-faint">Session for @{output.agent_handle}</span>
568 </span>
569 </Link>
570 ) : (
571 <Link to={`/${slug}/-/agents/${session.agent_handle}/memory`} className="flex items-start gap-2 rounded-md px-2 py-1.5 hover:bg-raised/60">
572 <BookmarkPlus size={14} className="mt-0.5 shrink-0 text-accent" />
573 <span className="min-w-0">
574 <span className="line-clamp-2">{output.body}</span>
575 <span className="block text-[0.6875rem] text-faint">Kept in memory</span>
576 </span>
577 </Link>
578 )}
579 </li>
580 ))}
581 </ul>
582 )}
583 </section>
584 );
585}