StudioFrame

tcmts.studio.StudioFrame · #gkqgnoz6 · term · runs on the server

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

used by