Skip to content

Commit 27077f7

Browse files
suleimanshclaude
andauthored
A web run's row says what its cloud side is doing, not "in cloud" forever (#1668) (#1669)
* A web run's row says what its cloud side is doing, not "in cloud" forever (#1668) cloudRunState(meta, now), node-free: waiting when the bridge holds the session's question; merged / pr once its work was adopted (#1601); in cloud inside the 48 h adoption window; done past it. onAgents marks a web run cloudWaiting from the bridge store on the way out; the rail row and the Overview's working list render the word, and the Overview lists in-cloud / waiting web runs among the agents at work. CLOUD_ADOPTION_WINDOW_MS moves to the new module. SPECs updated per sdd.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * In cloud only inside the bridge's 12 h session window; one Overview entry per web run across shared archives (#1668) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * A cloud run with a pull request reads done, as a local one does: the PR's state is not on the record (#1668) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 92b19d5 commit 27077f7

25 files changed

Lines changed: 327 additions & 36 deletions

packages/the-framework/dashboard/components/AgentHistory.SPEC.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ The dashboard's left column, present on every page: the brand, the New agent lau
66
- **New agent adapts to what exists** - inside a project it starts another agent there; with exactly one project registered it starts there; with several it asks which; with none it offers to add a project first, since there is nowhere to run an agent.
77
- **The recent list follows the scope** - a selected project shows its own agents; the Overview pools every project's agents newest-first, each row naming its project and jumping into it.
88
- **A started agent appears immediately** - a placeholder row says "starting…" from the moment Start is pressed until the real agent shows up, and gives up after a while if none ever does.
9-
- **Each row says what its agent is doing** - working, waiting on the user, publishing, in cloud, or its final status; plus when it started, which driver ran it, and whether it runs on another device or as a cloud session.
9+
- **Each row says what its agent is doing** - working, waiting on the user, publishing, or its final status — and for a cloud session, what the cloud side is doing; plus when it started, which driver ran it, and whether it runs on another device or as a cloud session.
1010
- **Exactly one destination is highlighted** - New, Overview, Tickets, a project, or an agent row.
1111
- **Projects carry their health** - each project shows whether it is activated, and turns red with the reason when the daemon has recorded an error for it.
1212

@@ -50,7 +50,7 @@ Scanning the list, the user needs to tell at a glance which agents need them, wh
5050

5151
#### Business logic
5252

53-
Each row shows a status word with a matching dot: a working agent pulses and reads its running status; an agent parked on the user reads "waiting" with a still dot, because it used to pulse identically whether it was mid-edit or had been idle for an hour; an agent that ended clean but has not yet reported its handoff reads "publishing…" with a pulsing dot, matching the wording on its own page; an agent that handed its task to a cloud session reads "in cloud" once its local half is done, since the cloud side keeps working and opens its own pull request. Otherwise the row shows the agent's final status.
53+
Each row shows a status word with a matching dot: a working agent pulses and reads its running status; an agent parked on the user reads "waiting" with a still dot, because it used to pulse identically whether it was mid-edit or had been idle for an hour; an agent that ended clean but has not yet reported its handoff reads "publishing…" with a pulsing dot, matching the wording on its own page; an agent that handed its task to a cloud session reads, once its local half is done, what the cloud side is doing per the cloud state rule (`cloud-run-state`): "waiting" with a still dot when the browser bridge holds a question its session is parked on, "in cloud" while the session is assumed to still be working, "merged" once the framework merged its pull request, and "done" once it has a pull request or the session can no longer be working — never "in cloud" forever. Otherwise the row shows the agent's final status.
5454

5555
Beside that: how long ago it started (preceded by its project name on the Overview), and, at the end of the line, a device glyph naming the device when the agent runs on another machine, a cloud glyph when it is a cloud session, and the logo of the driver that ran it. Underneath sits the agent's task; a task too long for the rail's fixed width is faded at its end and shows in full on hover, while one that fits is shown plainly with no hover at all.
5656

packages/the-framework/dashboard/components/AgentHistory.test.SPEC.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ What the tests cover:
22

33
- **What a row's status says** - a working agent reads "running" and animates; one parked on the user reads "waiting" with the animation stopped; an agent that ended clean with a handoff still to come reads "publishing…" and animates, and reads plain "done" once the handoff has reported or when none was armed; a finished agent is never relabelled "waiting".
44
- **The starting placeholder** - it carries the highlight while the user follows an agent whose row has not arrived yet; it retires as soon as any agent lands that was not in the list when Start was pressed, even one that failed too fast to ever be seen running; and an agent that was already in the list does not count as that handover.
5-
- **Where the agent runs** - an agent on another device shows a glyph naming that device, while a local one shows none; an agent handed to a cloud session reads "in cloud" instead of "done" (because the cloud side is still working), shows a cloud glyph, and still names its driver — but one stopped early reads plainly "stopped", since nothing is working anywhere.
5+
- **Where the agent runs** - an agent on another device shows a glyph naming that device, while a local one shows none; an agent handed to a cloud session reads "in cloud" while its session may still be working, "waiting" when the bridge holds a question for it, "merged" once the framework merged its pull request, and "done" once it has a pull request or is too old for its session to be alive instead of "done" (because the cloud side is still working), shows a cloud glyph, and still names its driver — but one stopped early reads plainly "stopped", since nothing is working anywhere.
66
- **Scope of the list** - with no project selected the rail still shows New agent and says "No agents yet."; the Overview pools every project's agents, naming each row's project, and selecting one jumps into that project's agent.
77
- **New agent** - with one project it starts there; inside a project it starts another agent there; with several projects and none selected it opens a picker instead of starting immediately.
88
- **Tickets** - offered both inside a project and on the Overview, opens the ticket view, carries the active marker while it is the current view (with Overview not also claiming it), and is not offered at all when there is nowhere to route it.

packages/the-framework/dashboard/components/AgentHistory.test.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ describe('cloud sessions on the rail (#1263/#1264)', () => {
294294
renderRail(
295295
<AgentHistory
296296
projectId="p1"
297-
agents={[agent({ status: 'done', target: 'web', driver: 'claude-web' })]}
297+
agents={[agent({ status: 'done', target: 'web', driver: 'claude-web', startedAt: new Date().toISOString() })]}
298298
selectedAgentId={null}
299299
onSelect={() => {}}
300300
/>,
@@ -316,6 +316,39 @@ describe('cloud sessions on the rail (#1263/#1264)', () => {
316316
expect(screen.queryByText('in cloud')).toBeNull()
317317
})
318318

319+
test('a web run parked on a question the bridge reported reads as waiting (#1668)', () => {
320+
renderRail(
321+
<AgentHistory
322+
projectId="p1"
323+
agents={[agent({ status: 'done', target: 'web', driver: 'claude-web', startedAt: new Date().toISOString(), cloudWaiting: true })]}
324+
selectedAgentId={null}
325+
onSelect={() => {}}
326+
/>,
327+
)
328+
expect(screen.getByText('waiting')).toBeTruthy()
329+
expect(screen.queryByText('in cloud')).toBeNull()
330+
})
331+
332+
test('adopted cloud work reads done (its PR badge says the rest) or merged; an old run with nothing adopted is done (#1668)', () => {
333+
const fresh = new Date().toISOString()
334+
const old = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString()
335+
renderRail(
336+
<AgentHistory
337+
projectId="p1"
338+
agents={[
339+
agent({ id: 'pr', status: 'done', target: 'web', driver: 'claude-web', startedAt: fresh, pr: { number: 1, url: 'u' } }),
340+
agent({ id: 'merged', status: 'done', target: 'web', driver: 'claude-web', startedAt: old, pr: { number: 2, url: 'u' }, mergeOutcome: 'merged' }),
341+
agent({ id: 'stale', status: 'done', target: 'web', driver: 'claude-web', startedAt: old }),
342+
]}
343+
selectedAgentId={null}
344+
onSelect={() => {}}
345+
/>,
346+
)
347+
expect(screen.getByText('merged')).toBeTruthy()
348+
expect(screen.getAllByText('done')).toHaveLength(2)
349+
expect(screen.queryByText('in cloud')).toBeNull()
350+
})
351+
319352
test('a web run shows the cloud glyph and still names its agent (#1263)', () => {
320353
renderRail(
321354
<AgentHistory

packages/the-framework/dashboard/components/AgentHistory.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useEffect, useRef, useState } from 'react'
22
import { Plus, ChevronDown, Cloud, MonitorSmartphone, Settings, LayoutDashboard, FolderGit2, Ticket } from 'lucide-react'
33
import type { AgentMeta, AgentStatus, RecentAgent, ProjectSummary } from '../../src/index.js'
4-
import { DRIVER_LABELS, driverFromImpl } from '../../src/client.js'
4+
import { DRIVER_LABELS, driverFromImpl, cloudRunState, type CloudRunState } from '../../src/client.js'
55
import { Button, buttonVariants } from './ui/button.js'
66
import { Badge } from './ui/badge.js'
77
import { cn } from '../lib/utils.js'
@@ -257,6 +257,7 @@ export function AgentHistory({
257257
waiting={row.agent.settledAt !== undefined}
258258
remote={row.agent.target === 'remote'}
259259
cloud={row.agent.target === 'web'}
260+
cloudState={cloudRunState(row.agent, Date.now())}
260261
{...(row.agent.remoteLabel ? { remoteLabel: row.agent.remoteLabel } : {})}
261262
onClick={row.onClick}
262263
/>
@@ -528,6 +529,7 @@ function AgentHistoryRow({
528529
publishing = false,
529530
remote = false,
530531
cloud = false,
532+
cloudState,
531533
remoteLabel,
532534
}: {
533535
status: AgentStatus
@@ -547,19 +549,25 @@ function AgentHistoryRow({
547549
remote?: boolean
548550
/** A Claude Code cloud session (#1263): the row gets a cloud glyph beside the agent logo. */
549551
cloud?: boolean
552+
/** What that session is doing (#1668), once the local half is over; undefined = the status is the word. */
553+
cloudState?: CloudRunState | undefined
550554
/** The device's label, for the glyph's tooltip. */
551555
remoteLabel?: string | undefined
552556
}) {
553-
// Only a live agent can be waiting on you; a finished one is just finished.
554-
const parked = waiting && status === 'running'
557+
// Only a live agent can be waiting on you; a finished one is just finished — except a web run
558+
// whose cloud session the bridge reports as parked (#1668).
559+
const parked = (waiting && status === 'running') || cloudState === 'waiting'
555560
const picked = driverFromImpl(driver)
556561
// A web agent's local process ends at the hand-off by design, so its `done` is about this
557562
// machine, not the session (#1264): the cloud side keeps working and opens its own PR. Saying
558-
// "done" under ten working cloud agents is the lie the demo would put on camera.
559-
const inCloud = cloud && status === 'done'
563+
// "done" under ten working cloud agents is the lie the demo would put on camera — and "in cloud"
564+
// over a run whose PR merged two days ago is the opposite lie (#1668), so the word comes from
565+
// what is known of the session: waiting, in cloud, merged, or finally done.
566+
const inCloud = cloudState === 'in-cloud'
567+
const cloudWord = cloudState === 'merged' ? 'merged' : undefined
560568
// "In cloud" outranks "publishing…": a web agent's local half is over either way, and the cloud
561569
// side owns its own push/PR, so the cloud word is the truer one for that row.
562-
const publishingNow = publishing && !inCloud
570+
const publishingNow = publishing && !cloud
563571
// The title only fades + carries a tooltip when it actually overflows the fixed-width rail; a
564572
// short one shows plainly. Measured here since CSS cannot tell. The rail width is fixed, so
565573
// intent is the only thing that changes the answer.
@@ -592,8 +600,8 @@ function AgentHistoryRow({
592600
<span className={cn('inline-block h-2 w-2 shrink-0 rounded-full', parked ? 'bg-muted-foreground' : 'animate-pulse bg-primary')} />
593601
)}
594602
{publishingNow && <span className="inline-block h-2 w-2 shrink-0 animate-pulse rounded-full bg-success" />}
595-
<Badge className={cn('shrink-0 border-transparent px-0 text-[10px] uppercase', parked || publishingNow ? 'text-muted-foreground' : inCloud ? 'text-primary' : STATUS_TONE[status])}>
596-
{parked ? 'waiting' : inCloud ? 'in cloud' : publishingNow ? 'publishing…' : status}
603+
<Badge className={cn('shrink-0 border-transparent px-0 text-[10px] uppercase', parked || publishingNow ? 'text-muted-foreground' : inCloud ? 'text-primary' : cloudWord ? 'text-success' : STATUS_TONE[status])}>
604+
{parked ? 'waiting' : inCloud ? 'in cloud' : cloudWord ? cloudWord : publishingNow ? 'publishing…' : status}
597605
</Badge>
598606
<span className="truncate text-xs font-normal text-muted-foreground">{subtitle}</span>
599607
{/* Right cluster: a device glyph when the run is relayed to a connected device (#1067),

packages/the-framework/dashboard/components/Agents.SPEC.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ The Overview's Agents card: every agent working right now, across all projects,
22

33
## Business logic — TL;DR
44

5-
- **Only what is working now** - the card lists agents currently working; finished agents are not repeated here because the sidebar already lists them.
5+
- **Only what is working now** - the card lists agents currently working, a cloud session's agent included while the session works or waits; finished agents are not repeated here because the sidebar already lists them.
66
- **A row opens the agent itself** - clicking a row goes to that agent in its project, never merely to the project's launcher.
77
- **Each row names the agent the way the sidebar does** - its intent, else its session name, else its scope, else its project.
88

@@ -16,7 +16,7 @@ The user wants one place that answers "what is being worked on right now?" witho
1616

1717
#### Business logic
1818

19-
The card pools the currently working agents from every project. While the list is still being fetched it says so; when nothing is working it says no agents are working right now. Finished agents are deliberately absent — the sidebar's agent list already holds them, so a second copy here would say nothing new.
19+
The card pools the currently working agents from every project. An agent whose cloud session is still at work is among them, its row saying "in cloud" or "waiting" since no local process backs it. While the list is still being fetched it says so; when nothing is working it says no agents are working right now. Finished agents are deliberately absent — the sidebar's agent list already holds them, so a second copy here would say nothing new.
2020

2121
### A row opens the agent itself
2222

packages/the-framework/dashboard/components/Agents.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function Agents({
4646
<AgentRow
4747
key={`${a.projectId}:${a.agentId}`}
4848
label={activeLabel(a)}
49+
cloud={a.cloud}
4950
at={a.updatedAt}
5051
projectName={a.projectName}
5152
onOpen={() => onSelectAgent(a.projectId, a.agentId)}
@@ -60,12 +61,15 @@ export function Agents({
6061

6162
function AgentRow({
6263
label,
64+
cloud,
6365
at,
6466
projectName,
6567
onOpen,
6668
}: {
6769
/** The session's one-liner, the same the sidebar shows. */
6870
label: string
71+
/** A web run's cloud side at work (#1668): say where it is, since no local process backs the row. */
72+
cloud?: 'in-cloud' | 'waiting' | undefined
6973
/** ISO: the session's last activity. */
7074
at: string | undefined
7175
projectName: string
@@ -83,6 +87,7 @@ function AgentRow({
8387
>
8488
<span aria-hidden className="shrink-0 text-muted-foreground/50"></span>
8589
<span className="min-w-0 flex-1 truncate text-sm">{label}</span>
90+
{cloud && <span className="shrink-0 text-[10px] uppercase text-muted-foreground">{cloud === 'waiting' ? 'waiting' : 'in cloud'}</span>}
8691
<span className="shrink-0 text-xs text-muted-foreground">{projectName}</span>
8792
{at && (
8893
<Tooltip>

packages/the-framework/src/client.SPEC.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
The dashboard's browser-safe entry: everything the browser app imports at runtime, re-exported from one place that reaches no Node built-in, so the client bundle never drags in the daemon's server half. What lives behind it is exactly the pure logic the dashboard and the daemon must agree on: event formatting and the agent-view projections, the system-prompt composition and the preset catalog (so the dashboard can show the built-in prompt *before* an agent runs rather than describing it), Auto PM's routine list (the list on screen is the list the daemon runs, not a copy of it), the handoff ladder's ordering and conversions, notification identity and preference defaults, the preferences-to-agent-options mapping (so an unattended agent starts with the same settings a dashboard-started one would), Discord credential validation (so the setup dialog rejects a malformed token before the round trip), and the loopback test behind the daemon's token gate (the browser once kept a looser copy that disagreed with the daemon about what "local" means). Sharing the one implementation is the point: copies drift silently.
1+
The dashboard's browser-safe entry: everything the browser app imports at runtime, re-exported from one place that reaches no Node built-in, so the client bundle never drags in the daemon's server half. What lives behind it is exactly the pure logic the dashboard and the daemon must agree on: event formatting and the agent-view projections, the system-prompt composition and the preset catalog (so the dashboard can show the built-in prompt *before* an agent runs rather than describing it), Auto PM's routine list (the list on screen is the list the daemon runs, not a copy of it), the handoff ladder's ordering and conversions, notification identity and preference defaults, the preferences-to-agent-options mapping (so an unattended agent starts with the same settings a dashboard-started one would), Discord credential validation (so the setup dialog rejects a malformed token before the round trip), the loopback test behind the daemon's token gate (the browser once kept a looser copy that disagreed with the daemon about what "local" means), the bridged question's projection onto the gate panel, and the cloud state rule for a `web`-target agent (so the rail says the same word the Overview does). Sharing the one implementation is the point: copies drift silently.
22

33
## Before modifying/creating SPEC.md files
44

packages/the-framework/src/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,5 @@ export {
8181
export { isLoopbackHost } from './loopback-host.js'
8282
// A bridged question as the gate panel renders it (#1554): pure, so the client projects it itself.
8383
export { bridgeChoiceRequest } from './dashboard/bridge-question.js'
84+
// What a web run's cloud side is doing, from its record (#1668): pure, so every surface derives the same word.
85+
export { cloudRunState, cloudRunActive, CLOUD_SESSION_WINDOW_MS, type CloudRunState } from './cloud-run-state.js'

0 commit comments

Comments
 (0)