ProcessesPage

tcmts.studio.ProcessesPage · #jvejlqfi · term · runs on the server

uses node:fs

What tcm runs on the codebase, whoever started it (the studio, a terminal, an editor, an agent): each tcm dev, start, serve and studio, with where it listens, its branch and how long it's run, to open or stop, and the codebase's apps that aren't running, to start (tcm dev); each agent's session and the background jobs it's running (the machine's slots have a page of their own). Made again every few seconds while it's shown.

import { jobIds, jobsDirFor, liveMcpServers, readJob } from "#codebase/tcmts/cli/mcp";
import { liveRunning, shownUptime } from "#codebase/tcmts/cli/running";
import { at, servedCodebase, shareNow } from "#codebase/tcmts/share";
import {
  appLog, BranchLink, Live, StartAppButton, StopButton, studioApps, StudioFrame, studioShare,
} from "#codebase/tcmts/studio";
import fs from "node:fs";

export async function ProcessesPage() {
  studioShare();
  const s = shareNow();
  if (!s.signedIn) return <StudioFrame tab="processes" title="Sign in">{null}</StudioFrame>;
  const root = s.cb.lay.root;
  const now = Date.now();
  const running = liveRunning(root);
  // (the codebase's apps, those not running to start: only on its own branch, where the studio changes things)
  const own = s.cb === servedCodebase().cb;
  const idle = own
    ? s.cache
        .atHead(s.cb, "studio apps", () => studioApps(s.cb, s.cache.placement(s.cb)))
        .filter((a) => !running.some((r) => r.app === a.path))
    : [];
  const agents = liveMcpServers(root).map((a) => {
    const dir = jobsDirFor(root, a.branch);
    const jobs = jobIds(dir)
      .flatMap((id) => readJob(dir, id) ?? [])
      .filter((j) => !j.done && !j.lost);
    return { ...a, jobs };
  });
  return (
    <StudioFrame tab="processes" title="Processes">
      <Live />
      <div className="pagehead">
        <h2 className="title">Processes</h2>
        <span className="muted">what tcm runs on this codebase, whoever started it</span>
      </div>
      <div className="box">
        <div className="boxhead">
          <b>Apps and servers</b>
          <span className="count">{running.length}</span>
        </div>
        {running.length ? (
          running.map((r) => (
            <div className="lrow" key={r.pid}>
              <div>
                <span className="live" />{" "}
                <code>{`tcm ${r.command}`}</code>{" "}
                {r.app && (
                  <>
                    <a className="mono" href={at(`/apps/${encodeURIComponent(r.app)}`)}>
                      {r.app}
                    </a>{" "}
                  </>
                )}
                <span className="muted">
                  {[r.url, r.branch, `${shownUptime(now - r.started)}`, `process ${r.pid}`].filter(Boolean).join(" · ")}
                </span>
              </div>
              <div className="actions">
                {r.url && r.command !== "studio" && (
                  <a className="btn small" href={r.url} target="_blank" rel="noreferrer">Open</a>
                )}
                {r.command === "dev" && r.app && fs.existsSync(appLog(s.cb, r.app) ?? "") && (
                  <a className="btn small" href={at(`/processes/output?app=${encodeURIComponent(r.app)}`)}>
                    Output
                  </a>
                )}
                {r.command !== "studio" && <StopButton pid={r.pid} />}
              </div>
            </div>
          ))
        ) : (
          <div className="empty">
            {idle.length ? "Nothing's running." : <>Nothing's running: <code>tcm dev &lt;app&gt;</code> runs one.</>}
          </div>
        )}
        {idle.length > 0 && (
          <>
            <div className="relhead">The codebase's apps, not running</div>
            {idle.map((a) => (
              <div className="lrow" key={a.path}>
                <div>
                  <span className="live off" />{" "}
                  <a className="mono" href={at(`/apps/${encodeURIComponent(a.path)}`)}>
                    {a.path}
                  </a>
                </div>
                <StartAppButton name={a.path} />
              </div>
            ))}
          </>
        )}
      </div>
      <div className="box">
        <div className="boxhead">
          <b>Agents</b>
          <span className="count">{agents.length}</span>
          <span className="muted">MCP servers, each an agent's session on a branch of its own</span>
        </div>
        {agents.length ? (
          agents.map((a) => (
            <div className="lrow" key={a.pid}>
              <div>
                <span className={`live${a.jobs.length ? " busy" : ""}`} />{" "}
                {a.branch ? (
                  <BranchLink branch={a.branch} />
                ) : (
                  <span>on the codebase's own branch</span>
                )}{" "}
                {a.client && (
                  <span className="muted" title={a.client}>
                    for …/{a.client.split("/").filter(Boolean).pop()}
                  </span>
                )}{" "}
                <span className="muted">· process {a.pid}</span>
                {a.jobs.map((j) => (
                  <div className="muted" key={j.id} style={{ fontSize: 13, marginTop: 2 }}>
                    job {j.id}: <code>{j.tool ?? `tcm ${j.argv.join(" ")}`}</code>, running for {shownUptime(now - j.started)}
                  </div>
                ))}
              </div>
            </div>
          ))
        ) : (
          <div className="empty">No agent is working on this codebase.</div>
        )}
      </div>
    </StudioFrame>
  );
}

uses

used by