StudioFrame
uses process
Every studio page's frame, and the forge's pages' in the studio: the project and the branch it shows, the finder,
and the tabs as a column on the left: the overview, the code, the scratch files, what needs fixing (counted), the
tests, the libraries and the packages; what's running, how it runs and its data; history, branches and releases. Reading another
branch, a notice says so, with what to do with it (merge it, work on it). Without the token (the link tcm studio prints signs the browser in), it's the sign-in page instead.
import { liveMcpServers } from "#codebase/tcmts/cli/mcp"; import { liveRunning } from "#codebase/tcmts/cli/running"; import { ago, at, projectNow, SearchBox, servedCodebase, shareNow, SignInForm } from "#codebase/tcmts/share"; import { BranchActions, BranchMenu, fixCount, mergeWaiting } from "#codebase/tcmts/studio"; import type { ReactNode } from "react"; export function StudioFrame({ tab, title, children, }: { tab: string; title: string; children: ReactNode; }) { const s = shareNow(); const project = projectNow(s); // (the forge's pages say which of its tabs they're under: a contribution is a branch's) const here = tab === "contributions" ? "branches" : tab; const running = s.signedIn ? liveRunning(s.cb.lay.root).length + liveMcpServers(s.cb.lay.root).length : 0; const own = servedCodebase().cb.store.branch().name; const fixes = s.signedIn ? fixCount(s.cb) : 0; const current = s.cb.store.branch().name; const working = new Set( s.signedIn ? liveMcpServers(s.cb.lay.root).flatMap((m) => (m.branch ? [m.branch] : [])) : [], ); const changed = (b: string) => s.cb.store.causal(s.cb.store.head(b))?.ts ?? 0; const item = (b: string) => ({ name: b, detail: ago(changed(b)), ...(working.has(b) ? { live: true } : {}) }); const others = s.signedIn ? s.cb.store.branches().filter((b) => b !== own) : []; const agents = (b: string) => /^(session|agent)\//.test(b); const recent = (b: string) => working.has(b) || b === current || Date.now() - changed(b) < 86_400_000; const byChange = (a: string, b: string) => Number(working.has(b)) - Number(working.has(a)) || changed(b) - changed(a); const older = others.filter((b) => agents(b) && !recent(b)).sort(byChange).map(item); const sections = [ { label: "The codebase's own", items: [item(own)] }, { label: "Branches", items: others.filter((b) => !agents(b)).sort(byChange).map(item) }, { label: "Agents' sessions", // (those an agent is on, or changed in the last day; the rest are older) items: others.filter((b) => agents(b) && recent(b)).sort(byChange).map(item), }, ].filter((x) => x.items.length); // (links, as NavTabs draws them: React Aria's Tabs rendered only some of a group on the server, on some pages, and // the browser's then didn't match) const group = (label: string, tabs: { id: string; label: string; href: string; count?: number }[]) => ( <div className="navtabs" data-orientation="vertical" aria-label={label} role="group"> <div className="tabs" data-orientation="vertical"> {tabs.map((t) => ( <a key={t.id} className="tab" href={t.href} {...(t.id === here ? { "data-selected": "", "aria-current": "page" as const } : {})} > {t.label} {t.count !== undefined ? <span className="count">{t.count}</span> : null} </a> ))} </div> </div> ); return ( <> <title>{`${title} · ${project.name} · tcm studio`}</title> <header className="app"> <div className="wrap studiobar"> <div className="bar1"> <a className="logo" href={at("/")}> <span className="mark" aria-hidden="true" /> {project.name} </a> {s.signedIn && <BranchMenu current={current} own={own} sections={sections} older={older} />} {s.signedIn ? <SearchBox /> : <span style={{ flex: 1 }} />} </div> </div> </header> {s.signedIn ? ( <div className="studio"> <nav className="studionav" aria-label="Studio"> {group("Workspace", [ { id: "overview", label: "Overview", href: at("/") }, { id: "code", label: "Code", href: at("/code") }, { id: "scratch", label: "Scratch files", href: at("/scratch") }, { id: "fix", label: "Needs fixing", href: at("/fix"), ...(fixes ? { count: fixes } : {}) }, { id: "tests", label: "Tests", href: at("/tests") }, { id: "libraries", label: "Libraries", href: at("/libraries") }, { id: "packages", label: "Packages", href: at("/packages") }, ])} <div className="group">Running</div> {group("Running", [ { id: "processes", label: "Processes", href: at("/processes"), ...(running ? { count: running } : {}) }, { id: "slots", label: "Slots", href: at("/slots") }, { id: "traces", label: "Traces", href: at("/traces") }, { id: "data", label: "Data", href: at("/data") }, { id: "jobs", label: "Jobs", href: at("/jobs") }, ])} <div className="group">Changes</div> {group("Changes", [ { id: "history", label: "History", href: at("/history") }, { id: "branches", label: "Branches", href: at("/branches") }, { id: "releases", label: "Releases", href: at("/releases") }, ])} </nav> <main> {current !== own && ( <div className="notice readnotice" style={{ marginBottom: 16 }}> <span> Reading <code>{current}</code>, read only: its code, history and traces, as it has them; nothing here changes it. The codebase's own branch is <code>{own}</code> (the branch menu goes back to it). </span> <BranchActions name={current} own={own} canSwitch={!process.env.TCM_BRANCH} merging={!!mergeWaiting(servedCodebase().cb)} keep /> </div> )} {children} </main> </div> ) : ( <div className="signin"> <h2 className="title">Sign in</h2> <p className="muted"> The studio runs this codebase's code, so it takes the token <code>tcm studio</code> keeps: open the link it prints, which signs this browser in. </p> <SignInForm then={at("/")} /> </div> )} </> ); }
uses
- tcmts.cli.mcp.liveMcpServers
- tcmts.cli.running.liveRunning
- tcmts.share.SearchBox
- tcmts.share.SignInForm
- tcmts.share.ago
- tcmts.share.at
- tcmts.share.projectNow
- tcmts.share.servedCodebase
- tcmts.share.shareNow
- tcmts.studio.BranchActions
- tcmts.studio.BranchMenu
- tcmts.studio.fixCount
- tcmts.studio.mergeWaiting
used by
- tcmts.studio.AppOutputPage
- tcmts.studio.AppPage
- tcmts.studio.DataIndexPage
- tcmts.studio.JobsPage
- tcmts.studio.LibrariesPage
- tcmts.studio.NeedsFixingPage
- tcmts.studio.OverviewPage
- tcmts.studio.PackagePage
- tcmts.studio.PackagesPage
- tcmts.studio.ProcessesPage
- tcmts.studio.ReleasesIndexPage
- tcmts.studio.ScratchIndexPage
- tcmts.studio.ScratchPage
- tcmts.studio.SlotsPage
- tcmts.studio.StudioBranchesPage
- tcmts.studio.StudioReleasesPage
- tcmts.studio.TablePage
- tcmts.studio.TestsPage
- tcmts.studio.TraceOfPage
- tcmts.studio.TracePage
- tcmts.studio.TracesPage
- tcmts.studio.studioShare