Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Live now lists the sessions working now: on a repository's overview and Work, a session not heard from for 15 minutes (idle) is folded into one row under the list, "N idle sessions", last seen when, which opens to show them; each moves back up when it is heard from and drops off when it times out. A session that has done nothing since it started (no title, issue or pull request, never heard from again: a Claude Code tab opened and not typed in) is not listed at all (startedOnly). The panel's counts are of the sessions working now, with "N idle" beside them, and its icon is green only for those. Sessions on the default branch are headed "On main" (the branch's name), not "No branch yet", which is kept for sessions git has named no branch for, and their rows leave the branch out. An untitled session's title, read from its first prompt, skips what VS Code puts ahead of what was typed (<ide_opened_file>, <ide_selection>): such a prompt read as a turn Claude Code started itself, so a session started from VS Code took its second prompt as its title. Checked: apps/web tsc -b; npm test (986; new: awake and idle split, startedOnly, an IDE-prefixed prompt's intent).

G1t-Session:
ses_01m4zzpc96ect8d41mffgr1a45
syntaqxcommitted Parent6a6efc8Browse files
5 files+140−180/5 viewed
+74−8
99 Activity,
1010 ArrowRightLeft,
1111 Bot,
12+ ChevronRight,
1213 CircleCheck,
1314 CircleDot,
1415 CircleX,
5758 harnessName,
5859 type LiveGroup,
5960 type LiveIssue,
61+ awakeAndIdle,
6062 liveByIssue,
6163 mergeSessions,
6264 numberedParts,
7072 import { Badge, type BadgeTone } from "./ui/badge";
7173 import { Button } from "./ui/button";
7274 import { Card } from "./ui/card";
75+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible";
7376 import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
7477 import { Field, FieldError, FieldLabel, FieldLegend, FieldSet } from "./ui/field";
7578 import { Hint } from "./ui/hint";
879882 export type LivePull = { title: string; status: Pull["status"] };
880883
881884 /** What a group of sessions on no issue is on, as its card's heading: the pull request, the branch, or neither. */
882−function LiveGroupHeading({ group, pulls, base }: { group: LiveGroup; pulls: Record<number, LivePull>; base: string }) {
885+function LiveGroupHeading({
886+ group,
887+ pulls,
888+ base,
889+ defaultBranch,
890+}: {
891+ group: LiveGroup;
892+ pulls: Record<number, LivePull>;
893+ base: string;
894+ defaultBranch?: string | null;
895+}) {
883896 if (group.kind === "pull") {
884897 const pull = pulls[group.pull];
885898 return (
905918 </div>
906919 );
907920 }
921+ // Working on the default branch itself, or somewhere git has not said yet.
922+ const onDefault = defaultBranch != null && group.sessions.some((session) => session.branch === defaultBranch);
908923 return (
909924 <div className="flex items-center gap-2.5 px-4 py-3">
910− <Activity size={16} className="shrink-0 text-faint" />
911− <Hint label="Sessions on no issue, pull request or branch of their own yet, each titled with what it is for. One opens or claims an issue when its work becomes a change.">
912− <span className="text-sm text-muted">No branch yet</span>
925+ {onDefault ? <GitBranch size={16} className="shrink-0 text-faint" /> : <Activity size={16} className="shrink-0 text-faint" />}
926+ <Hint label="Sessions on no issue, pull request or branch of their own, each titled with what it is for. One opens or claims an issue when its work becomes a change.">
927+ <span className="text-sm text-muted">
928+ {onDefault ? (
929+ <>
930+ On <span className="font-mono">{defaultBranch}</span>
931+ </>
932+ ) : (
933+ "No branch yet"
934+ )}
935+ </span>
913936 </Hint>
914937 </div>
915938 );
916939 }
917940
918941 /** Sessions on no issue yet that share a pull request or a branch, or share neither: a heading, then each session. */
919−export function LiveGroupCard({ group, pulls, base }: { group: LiveGroup; pulls: Record<number, LivePull>; base: string }) {
942+export function LiveGroupCard({
943+ group,
944+ pulls,
945+ base,
946+ defaultBranch,
947+}: {
948+ group: LiveGroup;
949+ pulls: Record<number, LivePull>;
950+ base: string;
951+ defaultBranch?: string | null;
952+}) {
920953 return (
921954 <Card asChild divided>
922955 <li>
923− <LiveGroupHeading group={group} pulls={pulls} base={base} />
956+ <LiveGroupHeading group={group} pulls={pulls} base={base} defaultBranch={defaultBranch} />
924957 {group.sessions.map((session) => (
925958 <div key={session.id} className="px-4 py-3">
926959 {/* Its pull request and branch are the heading's, unless it has neither of its own. */}
927− <SessionLine session={session} base={base} showWhere={group.kind === "none"} />
960+ <SessionLine session={session} base={base} showWhere={group.kind === "none"} showBranch={group.kind === "none" && session.branch !== defaultBranch} />
928961 </div>
929962 ))}
930963 </li>
959992 now: number;
960993 limit?: number;
961994 }) {
962− const live = liveByIssue(claims, sessions, defaultBranch);
995+ const { awake, idle } = awakeAndIdle(sessions);
996+ const live = liveByIssue(claims, awake, defaultBranch);
963997 const issues = limit == null ? live.issues : live.issues.slice(0, limit);
964998 return (
965999 <ol className="space-y-3">
9721006 group={group}
9731007 pulls={pulls}
9741008 base={base}
1009+ defaultBranch={defaultBranch}
9751010 />
9761011 ))}
1012+ {idle.length > 0 && <IdleSessions sessions={idle} base={base} />}
9771013 </ol>
9781014 );
9791015 }
9801016
1017+/**
1018+ * Sessions not heard from for 15 minutes, folded under Live now: usually
1019+ * an open tab, not work going on. Each moves back up when it is heard from,
1020+ * and drops off when it times out.
1021+ */
1022+function IdleSessions({ sessions, base }: { sessions: WorkSession[]; base: string }) {
1023+ return (
1024+ <Card asChild divided>
1025+ <li>
1026+ <Collapsible>
1027+ <CollapsibleTrigger className="w-full px-4 py-2.5">
1028+ <ChevronRight size={14} className="shrink-0" />
1029+ {sessions.length === 1 ? "1 idle session" : `${sessions.length} idle sessions`}
1030+ <span className="ml-auto text-xs text-faint">
1031+ last seen <TimeAgo at={sessions[0].lastSeenAt} />
1032+ </span>
1033+ </CollapsibleTrigger>
1034+ <CollapsibleContent className="divide-y divide-line border-t border-line">
1035+ {sessions.map((session) => (
1036+ <div key={session.id} className="px-4 py-3">
1037+ <SessionLine session={session} base={base} />
1038+ </div>
1039+ ))}
1040+ </CollapsibleContent>
1041+ </Collapsible>
1042+ </li>
1043+ </Card>
1044+ );
1045+}
1046+
9811047 /** What to do when nobody is working on a project: bring your own harness. */
9821048 export function StartHere({ title }: { title: string }) {
9831049 return (
+18−0
1010 feedLine,
1111 harnessName,
1212 intentTitle,
13+ awakeAndIdle,
1314 liveByIssue,
1415 sessionTitle,
1516 duration,
105106 assert.equal(span(2 * 3_600_000), "2h");
106107 });
107108
109+test("Live now lists sessions heard from lately and folds away the idle, the latest first", () => {
110+ const { awake, idle } = awakeAndIdle([
111+ session("ses_a", { state: "active" }),
112+ session("ses_b", { state: "idle", title: "Tidy the README", lastSeenAt: "2026-10-12T10:00:00Z" }),
113+ session("ses_c", { state: "idle", lastSeenAt: "2026-10-12T10:10:00Z" }),
114+ ]);
115+ assert.deepEqual(awake.map((one) => one.id), ["ses_a"]);
116+ assert.deepEqual(idle.map((one) => one.id), ["ses_c", "ses_b"]);
117+ // A tab opened and never typed in is in neither; once it says what it is for, it shows.
118+ const opened = session("ses_d", { state: "active", title: null, startedAt: "2026-10-12T10:00:00Z", lastSeenAt: "2026-10-12T10:00:00Z" });
119+ assert.deepEqual(awakeAndIdle([opened]), { awake: [], idle: [] });
120+ assert.equal(awakeAndIdle([{ ...opened, lastSeenAt: "2026-10-12T10:00:05Z" }]).awake.length, 1);
121+ assert.equal(awakeAndIdle([{ ...opened, title: "Fix the build" }]).awake.length, 1);
122+});
123+
108124 test("live work is grouped by issue, most recent first, with sessions on no issue apart", () => {
109125 const { issues, loose } = liveByIssue(
110126 [claim(1), claim(2, { createdAt: "2026-10-12T10:20:00Z" }), claim(2, { actor: bo })],
162178
163179 test("a prompt's intent is its first line or sentence, plain, clipped on a word", () => {
164180 assert.equal(intentTitle("Make preview builds faster. They time out on big repositories."), "Make preview builds faster");
181+ // What an editor puts ahead of the prompt is not the prompt.
182+ assert.equal(intentTitle("<ide_opened_file>The user opened a.ts in the IDE.</ide_opened_file>\nWhy is the page crashing?"), "Why is the page crashing?");
165183 assert.equal(intentTitle("Why does `src/app.ts` fail? It worked yesterday."), "Why does src/app.ts fail?");
166184 assert.equal(intentTitle("```js\nconst a = 1;\n```\n## Clean **this** up\nmore"), "Clean this up");
167185 assert.equal(intentTitle("\n\n- look at [the docs](https://docs.g1t.sh) for g1t.mjs"), "Look at the docs for g1t.mjs");
+32−1
8080 export const SESSION_STATES: Record<SessionState, { label: string; hint: string }> = {
8181 active: { label: "Active", hint: "Heard from in the last 15 minutes." },
8282 idle: { label: "Idle", hint: "Not heard from for 15 minutes. It may still come back." },
83− ended: { label: "Ended", hint: "Ended, or not heard from for 2 hours." },
83+ ended: { label: "Ended", hint: "Ended, or not heard from for 30 minutes. One that went quiet comes back when it is heard from again." },
8484 };
8585
8686 // ── Session titles ──────────────────────────────────────────────────────
9191 /** A turn the harness starts itself, as a tag: `<command-name>`, `<task-notification>`, one with attributes. */
9292 const HARNESS_TAG = /^\s*<[a-z][\w-]*(?:\s[^>]*)?>/i;
9393
94+/** What an editor adds ahead of a prompt, a whole block at a time: `<ide_opened_file>…</ide_opened_file>`. */
95+const IDE_CONTEXT = /^\s*<(ide_[a-z_]+|system-reminder)\b[^>]*>[\s\S]*?<\/\1>/i;
96+
9497 /**
9598 * What a prompt is for, as a session's title: its first line or sentence,
9699 * without code blocks or markdown, whitespace collapsed, clipped at
106109 text = text
107110 .replace(/<pasted_content\b[^>]*>[\s\S]*?(?:<\/pasted_content[^>]*>|$)/gi, "\n")
108111 .replace(/\[Image(?: #\d+)?(?::[^\]\n]*)?\]/g, " ");
112+ // Context an editor puts before what was typed (VS Code's
113+ // <ide_opened_file>, <ide_selection>) is not a turn the harness started.
114+ while (IDE_CONTEXT.test(text)) text = text.replace(IDE_CONTEXT, "");
109115 if (/^\s*\//.test(text) || HARNESS_TAG.test(text)) return null;
110116 text = text
111117 .replace(/```[\s\S]*?(```|$)/g, "\n")
158164 | { kind: "branch"; branch: string; sessions: WorkSession[] }
159165 | { kind: "none"; sessions: WorkSession[] };
160166
167+/**
168+ * Whether a session has done nothing since it started: no title, no issue
169+ * or pull request, never heard from again. Opening a Claude Code tab starts
170+ * one before anything is typed in it, and it is nothing to show yet.
171+ */
172+export function startedOnly(session: WorkSession): boolean {
173+ return !session.title && session.issue == null && session.pull == null && session.lastSeenAt === session.startedAt;
174+}
175+
176+/**
177+ * The sessions Live now lists, and those it folds away: one not heard from
178+ * for 15 minutes (idle) is someone's open tab more often than work going
179+ * on, so it waits under the list until it is heard from or times out. One
180+ * that has done nothing since it started is in neither.
181+ */
182+export function awakeAndIdle(sessions: WorkSession[]): { awake: WorkSession[]; idle: WorkSession[] } {
183+ const awake: WorkSession[] = [];
184+ const idle: WorkSession[] = [];
185+ for (const session of sessions) {
186+ if (!startedOnly(session)) (session.state === "idle" ? idle : awake).push(session);
187+ }
188+ idle.sort((a, b) => Date.parse(b.lastSeenAt) - Date.parse(a.lastSeenAt));
189+ return { awake, idle };
190+}
191+
161192 const latestSeen = (sessions: WorkSession[]) => Math.max(0, ...sessions.map((session) => Date.parse(session.lastSeenAt)));
162193
163194 /**
+9−5
3434 import { ButtonLink, CopyLine, SubmitButton, TimeAgo } from "../../components/ui";
3535 import { Card } from "../../components/ui/card";
3636 import { type LivePull, LiveNowList, StartHere } from "../../components/coordination";
37−import { liveByIssue } from "../../lib/coordination";
37+import { awakeAndIdle, liveByIssue } from "../../lib/coordination";
3838 import { type Need, ageBuckets, firstPassRate, passRate, rankNeeds, runHealth } from "../../lib/mission";
3939 import { count, duration, percent } from "../../lib/observability";
4040 import { ECOSYSTEM_LABEL, installCommands } from "../../lib/packages";
817817 : null;
818818 const addImage = canChange ? `${base}/settings#production-image` : null;
819819 const elsewhereUrl = loaderData.deployment?.url ?? project?.productionUrl ?? null;
820− const working = claims.length + sessions.length > 0;
820+ // Idle sessions fold under the list (LiveNowList); the counts are of the rest.
821+ const { awake, idle } = awakeAndIdle(sessions);
822+ const busy = claims.length + awake.length > 0;
823+ const working = busy || idle.length > 0;
821824 // Deployments, checks, releases, claims and sessions show as they happen.
822825 useRepoLive({ match: repoPages.overview(loaderData.commit?.hash) });
823826 // Claims lapse and sessions go idle with no event to say so.
824827 useRefreshWhile(working, 15_000);
825− const live = liveByIssue(claims, sessions);
828+ const live = liveByIssue(claims, awake);
826829 const moreIssues = live.issues.length - LIVE_SHOWN;
827830
828831 return (
892895
893896 <Panel
894897 title="Live now"
895− icon={<Radio size={14} className={working ? "text-success" : undefined} />}
898+ icon={<Radio size={14} className={busy ? "text-success" : undefined} />}
896899 extra={
897900 working ? (
898901 <span className="text-xs text-muted">
899− {plural(sessions.length, "session")} · {plural(claims.length, "claim")}
902+ {plural(awake.length, "session")} · {plural(claims.length, "claim")}
903+ {idle.length > 0 && ` · ${idle.length} idle`}
900904 </span>
901905 ) : null
902906 }
+7−4
1212 import { Card } from "../../components/ui/card";
1313 import { PullIcon } from "../../components/work-icons";
1414 import { requireRepo } from "../../lib/access.server";
15−import { COORDINATION_TYPES, feedLine } from "../../lib/coordination";
15+import { COORDINATION_TYPES, awakeAndIdle, feedLine } from "../../lib/coordination";
1616 import { page } from "../../lib/meta";
1717 import { useRefreshWhile } from "../../lib/refresh";
1818 import { repoPages } from "../../lib/repo-live";
121121 const { claims, sessions, queue, feed, titles, pulls, landed, defaultBranch } = loaderData;
122122 const base = `/${params.owner}/${params.repo}`;
123123 const now = Date.now();
124− const working = claims.length > 0 || sessions.length > 0;
124+ // Idle sessions fold under the list (LiveNowList); the counts are of the rest.
125+ const { awake, idle } = awakeAndIdle(sessions);
126+ const busy = claims.length + awake.length > 0;
127+ const working = busy || idle.length > 0;
125128 // Claims, sessions, overlaps, integration results and merges as they happen.
126129 useRepoLive({ match: repoPages.work() });
127130 // Claims lapse and sessions go idle with no event to say so.
139142 </p>
140143
141144 <Section
142− icon={<Radio size={15} className={working ? "text-success" : "text-faint"} />}
145+ icon={<Radio size={15} className={busy ? "text-success" : "text-faint"} />}
143146 title="Live now"
144147 aside={
145148 working
146− ? `${claims.length} ${claims.length === 1 ? "claim" : "claims"}, ${sessions.length} ${sessions.length === 1 ? "session" : "sessions"}`
149+ ? `${claims.length} ${claims.length === 1 ? "claim" : "claims"}, ${awake.length} ${awake.length === 1 ? "session" : "sessions"}${idle.length > 0 ? `, ${idle.length} idle` : ""}`
147150 : undefined
148151 }
149152 >

Comments

Sign in to comment.