Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions docs/notes/claude-science-ui-behavior-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ This is a behavior index, not a request to reproduce Claude branding. It records

| Surface | Claude Science behavior | OpenScience contract |
| -------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Default presentation | A step opens into a language/environment header, source, and separate output control. | Notebook and R cards auto-open so code is never hidden behind Show steps. |
| Default presentation | A step opens into a language/environment header, source, and separate output control. | Notebook, artifact, and remote-compute cards start collapsed so completed work stays compact and expands on demand. |
| Source height | Long source remains contained inside the operation instead of dominating the transcript. | Show exactly five code lines in a vertically and horizontally scrollable source window; retain the complete source in that window. |
| Output | Text output is visually separated from source. | Text output is open by default and independently scrollable. |
| Figures | Figures appear immediately after the cell that produced them. | Inline notebook images stay visible even when text output is collapsed. |
Expand All @@ -51,15 +51,15 @@ This is a behavior index, not a request to reproduce Claude branding. It records

## 5. Compute

| Surface | Claude Science behavior | OpenScience contract |
| --------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Host strip | Memory, CPU, live-kernel count, and running count are always visible. | Host totals combine local kernels, shell commands, and remote jobs. Unknown metrics render as unavailable, never fabricated zeroes. |
| Project ledger | Work is grouped by owning session with a current-session marker. | Compute aggregates every session in the project and does not reset when the selected session changes. |
| Kernel row | Language, state, age/cell count, activity label, RSS, CPU, and stop action form one dense row. | Live rows show named kernel, state/recovery text, uptime, RSS, cores, and Stop. |
| Job row | Long-running/background work stays visible independently of chat scroll. | Shell commands and Modal/GPU jobs are first-class rows with command/target, resources, duration, status, output, artifacts, cleanup, and cancel. |
| Completed work | Claude commonly leaves idle kernels visible. | OpenScience intentionally stops finished kernels, then keeps up to five recent local completion rows so the trail remains without wasting compute. |
| Manual creation | Claude exposes environment setup as part of agent work, not a user kernel launcher in the completed session. | Do not expose manual kernel creation. Kernels start only when an agent executes work. |
| Cleanup | Claude exposes stop/kill per kernel but may leave kernels idle. | The research agent must stop every named kernel after outputs and artifacts are verified. Remote cleanup warnings remain visible. |
| Surface | Claude Science behavior | OpenScience contract |
| --------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Host strip | Memory, CPU, live-kernel count, and running count are always visible. | Host totals combine local kernels, shell commands, and remote jobs. Unknown metrics render as unavailable, never fabricated zeroes. |
| Project ledger | Work is grouped by owning session with a current-session marker. | Compute aggregates every session in the project and does not reset when the selected session changes. |
| Kernel row | Language, state, age/cell count, activity label, RSS, CPU, and stop action form one dense row. | Live rows show named kernel, state/recovery text, uptime, RSS, cores, and Stop. |
| Job row | Long-running/background work stays visible independently of chat scroll. | Shell commands and Modal/GPU jobs are first-class rows with command/target, resources, duration, status, output, artifacts, cleanup, and cancel. |
| Completed work | Claude commonly leaves idle kernels visible. | OpenScience removes completed, stopped, and killed local kernels from Compute; their source, results, and artifacts remain in chat and Files. |
| Manual creation | Claude exposes environment setup as part of agent work, not a user kernel launcher in the completed session. | Do not expose manual kernel creation. Kernels start only when an agent executes work. |
| Cleanup | Claude exposes stop/kill per kernel but may leave kernels idle. | The research agent must stop every named kernel after outputs and artifacts are verified. Remote cleanup warnings remain visible. |

## 6. Files and artifacts

Expand Down Expand Up @@ -98,7 +98,7 @@ OpenScience's research prompt now requires at least two decision-useful figures

## 9. Intentional OpenScience differences

- Finished kernels are stopped automatically rather than left idle. Recent completion rows preserve visibility without retaining memory.
- Finished kernels are stopped automatically rather than left idle, and disappear from Compute once they are no longer live.
- Manual kernel creation is removed. The execution ledger describes real work; it is not a launcher.
- Compute also includes shell subprocesses and Modal/GPU jobs, which the reference surface did not expose in this exact local run.
- Project-wide Files and Compute remain stable while sessions switch, matching the requested cross-session workspace model.
Expand All @@ -108,13 +108,13 @@ OpenScience's research prompt now requires at least two decision-useful figures
- [x] Exact prompt starts exactly four named managed kernels.
- [x] Four kernel rows are visible in Compute during execution.
- [x] Python source and output remain visible in chat while working and after completion.
- [x] Source is auto-open but capped to a five-line scroll window.
- [x] Computed cards start collapsed; expanded source is capped to a five-line scroll window.
- [x] Figures display inline beside their producing cells.
- [x] Saved report, tables, and figures auto-appear in Files.
- [x] Artifact titles are meaningful and previews open beside chat.
- [x] Failed analysis is visible and can be retried without losing history.
- [x] Every named kernel stops after result verification.
- [x] Recent completed local work remains visible in Compute.
- [x] Completed, stopped, and killed local kernels disappear from Compute.
- [x] Modal/GPU jobs have live and recent-result rows with resources, logs, artifacts, cancel, and cleanup state.
- [x] The right workspace remains project-scoped across session changes.
- [x] Compute and artifact cards adapt at narrow container widths.
12 changes: 11 additions & 1 deletion frontend/ui/src/components/message-part-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ const source = () => readFileSync(fileURLToPath(new URL("./message-part.tsx", im

test("saved workspace artifacts render previewable, openable results", () => {
const part = source()
const artifact = part.slice(
part.indexOf("function SavedArtifactTool"),
part.indexOf('ToolRegistry.register({\n name: "artifact"'),
)

expect(part).toContain('name: "artifact"')
expect(part).toContain('data-component="saved-artifact-tool"')
Expand All @@ -16,12 +20,18 @@ test("saved workspace artifacts render previewable, openable results", () => {
expect(part).toContain("data.openFile?.(artifact().path)")
expect(part).toContain("Open beside chat")
expect(part).toContain("<summary>Show save receipt</summary>")
expect(artifact).not.toContain("defaultOpen")
})

test("Modal and compute job results use a dedicated visible renderer", () => {
test("Modal and compute job results use a dedicated compact renderer", () => {
const part = source()
const remote = part.slice(
part.indexOf("function RemoteComputeTool"),
part.indexOf('ToolRegistry.register({ name: "modal"'),
)

expect(part).toContain('name: "modal"')
expect(part).toContain('name: "compute_job"')
expect(part).toContain('title: props.tool === "modal" ? "Modal compute" : "Remote compute result"')
expect(remote).not.toContain("defaultOpen")
})
7 changes: 6 additions & 1 deletion frontend/ui/src/components/message-part-notebook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import { fileURLToPath } from "node:url"
const source = () => readFileSync(fileURLToPath(new URL("./message-part.tsx", import.meta.url)), "utf8")
const styles = () => readFileSync(fileURLToPath(new URL("./message-part.css", import.meta.url)), "utf8")

test("notebook tools open source, text output, and figures by default", () => {
test("notebook tools keep complete source, output, and figures behind a compact summary", () => {
const part = source()
const kernel = part.slice(
part.indexOf("function KernelTool"),
part.indexOf('ToolRegistry.register({\n name: "notebook"'),
)

expect(part).toContain('name: "notebook"')
expect(part).toContain('name: "rkernel"')
Expand All @@ -20,6 +24,7 @@ test("notebook tools open source, text output, and figures by default", () => {
expect(part).toContain('props.input.action === "stop"')
expect(part).toContain('trigger={{ title: "Kernel stopped"')
expect(part).toContain('title: props.status === "completed" ? "Computed" : "Computing"')
expect(kernel).not.toContain("defaultOpen")
expect(styles()).toContain("max-height: calc(5 * 1.55em + 20px)")
expect(styles()).toContain("overflow: auto")
})
3 changes: 0 additions & 3 deletions frontend/ui/src/components/message-part.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,6 @@ function KernelTool(props: ToolProps & { language: "python" | "r"; label: "Pytho
return (
<BasicTool
{...props}
defaultOpen
icon="code"
trigger={{
title: props.status === "completed" ? "Computed" : "Computing",
Expand Down Expand Up @@ -906,7 +905,6 @@ function SavedArtifactTool(props: ToolProps) {
return (
<BasicTool
{...props}
defaultOpen
icon="archive"
trigger={{
title: saved() ? "Saved artifact" : props.title || "Artifact",
Expand Down Expand Up @@ -985,7 +983,6 @@ function RemoteComputeTool(props: ToolProps) {
return (
<BasicTool
{...props}
defaultOpen
icon="console"
trigger={{
title: props.tool === "modal" ? "Modal compute" : "Remote compute result",
Expand Down
37 changes: 0 additions & 37 deletions frontend/workspace/src/atlas/ComputeSurface.css
Original file line number Diff line number Diff line change
Expand Up @@ -312,31 +312,6 @@
border-top: 8px solid var(--color-bg-subtle);
}

.compute-surface .kernel-history-card {
grid-template-columns: minmax(160px, 1fr) 64px minmax(112px, auto);
}

.compute-surface .kernel-history-card__result {
display: flex;
min-width: 0;
flex-direction: column;
gap: 2px;
}

.compute-surface .kernel-history-card__result strong {
color: var(--color-text);
font-size: 11px;
font-weight: 500;
}

.compute-surface .kernel-history-card__result small {
overflow: hidden;
color: var(--color-text-faint);
font-size: 10px;
text-overflow: ellipsis;
white-space: nowrap;
}

.compute-surface .remote-job-card {
grid-template-columns: minmax(160px, 1fr) 64px minmax(118px, auto) auto;
}
Expand Down Expand Up @@ -441,14 +416,6 @@
grid-template-columns: minmax(0, 1fr) auto;
}

.compute-surface .kernel-history-card {
grid-template-columns: minmax(0, 1fr) auto;
}

.compute-surface .kernel-history-card__result {
padding-left: 39px;
}

.compute-surface .remote-job-card__result {
padding-left: 39px;
}
Expand All @@ -466,10 +433,6 @@
.compute-surface .remote-job-card__result {
grid-column: 1 / -1;
}

.compute-surface .kernel-history-card__result {
grid-column: 1 / -1;
}
}

@media (prefers-reduced-motion: reduce) {
Expand Down
37 changes: 0 additions & 37 deletions frontend/workspace/src/atlas/KernelCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,43 +73,6 @@ export function KernelCard(props: {
)
}

const ago = (value: number | null) => {
if (!value) return "—"
const seconds = Math.max(0, Math.floor((Date.now() - value) / 1_000))
if (seconds < 60) return `${seconds}s ago`
const minutes = Math.floor(seconds / 60)
if (minutes < 60) return `${minutes}m ago`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}h ago`
return `${Math.floor(hours / 24)}d ago`
}

export function KernelResultCard(props: { kernel: KernelStatus }): JSX.Element {
return (
<article class="kernel-card kernel-history-card" data-kernel-id={props.kernel.id} data-state={props.kernel.state}>
<div class="kernel-card__main">
<span class="kernel-card__language" aria-hidden="true">
{props.kernel.language === "python" ? "Py" : props.kernel.language === "r" ? "R" : "›_"}
</span>
<div class="kernel-card__copy">
<strong title={kernelLabel(props.kernel)}>{kernelLabel(props.kernel)}</strong>
<span>
<i data-tone={props.kernel.state === "crashed" ? "danger" : "muted"} aria-hidden="true" />
{kernelStateLabel(props.kernel.state)} · output preserved in chat and Files
</span>
</div>
</div>
<span class="kernel-card__uptime" aria-label={`Finished ${ago(props.kernel.last_activity_at)}`}>
{ago(props.kernel.last_activity_at)}
</span>
<span class="kernel-history-card__result">
<strong>{props.kernel.state === "crashed" ? "Needs review" : "Complete"}</strong>
<small>workspace cleared</small>
</span>
</article>
)
}

function Metric(props: { label: string; value: string }): JSX.Element {
return (
<span class="kernel-card__metric">
Expand Down
6 changes: 4 additions & 2 deletions frontend/workspace/src/atlas/KernelPanel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ describe("live compute inventory", () => {
expect(panel).toContain("<CommandCard")
expect(panel).toContain("jobApi.list()")
expect(panel).toContain("<RemoteJobCard")
expect(panel).toContain("<KernelResultCard")
expect(panel).toContain('aria-label="Recent local results"')
expect(panel).not.toContain("KernelResultCard")
expect(panel).not.toContain("recentLocal")
expect(panel).not.toContain('aria-label="Recent local results"')
expect(panel).toContain('aria-label="Recent remote results"')
expect(panel).toContain('kernels.filter((kernel) => kernel.active || kernel.state === "starting")')
})

test("does not expose any manual kernel creation or restart path", () => {
Expand Down
24 changes: 2 additions & 22 deletions frontend/workspace/src/atlas/KernelPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createStore } from "solid-js/store"
import { useParams } from "@solidjs/router"
import { IconCpu } from "@/atlas/shared/Icon"
import { identify } from "@/atlas/poll-identity"
import { KernelCard, KernelResultCard, type KernelAction } from "@/atlas/KernelCard"
import { KernelCard, type KernelAction } from "@/atlas/KernelCard"
import { CommandCard } from "@/atlas/CommandCard"
import type { Job, Status } from "@/atlas/ComputeJobsAPI"
import { RemoteJobCard, jobLive } from "@/atlas/RemoteJobCard"
Expand Down Expand Up @@ -120,12 +120,6 @@ export function KernelPanel(props: KernelPanelProps = {}): JSX.Element {
.filter((job) => job.target.kind === "modal" && terminal.has(job.status) && !jobLive(job))
.slice(0, 5),
)
const recentLocal = createMemo(() =>
[...kernels]
.filter((kernel) => !kernel.active && kernel.state !== "lazy" && kernel.last_activity_at !== null)
.sort((a, b) => (b.last_activity_at ?? 0) - (a.last_activity_at ?? 0))
.slice(0, 5),
)
const groups = createMemo(() =>
[...grouped().keys()].sort((a, b) => {
const current = Number(route() === b) - Number(route() === a)
Expand Down Expand Up @@ -221,7 +215,7 @@ export function KernelPanel(props: KernelPanelProps = {}): JSX.Element {
</Show>

<Show
when={groups().length > 0 || recentLocal().length > 0 || recentRemote().length > 0}
when={groups().length > 0 || recentRemote().length > 0}
fallback={
<div class="kernel-panel__empty">
<span aria-hidden="true">
Expand Down Expand Up @@ -283,20 +277,6 @@ export function KernelPanel(props: KernelPanelProps = {}): JSX.Element {
</section>
)}
</For>
<Show when={recentLocal().length > 0}>
<section class="remote-results" aria-label="Recent local results">
<header class="kernel-session__header">
<div class="kernel-session__identity">
<span aria-hidden="true">↳</span>
<strong>Recent local results</strong>
</div>
<span>{recentLocal().length} completed</span>
</header>
<div class="kernel-panel__list">
<For each={recentLocal()}>{(kernel) => <KernelResultCard kernel={kernel} />}</For>
</div>
</section>
</Show>
<Show when={recentRemote().length > 0}>
<section class="remote-results" aria-label="Recent remote results">
<header class="kernel-session__header">
Expand Down