~/field-notes — leeguoo@misonote — zsh EN ● 中文 日本語
❯ field-notes v3.4.1
leeguoo@misonote:/zh/posts/claude-agents-bg-implementation/ $ 文章

# claude agents 与 /bg:Agent View 实现剖析

基于 Claude Code 2.1.139 源码,拆解 claude agents、--bg 与 /bg 如何共用后台 job 模型、daemon 与 FleetView TUI。

2026年5月12日 · 文章 · 公开 · 文章

本页目录
1.1. 数据模型2.2. 常量与文案3.3. Host-Framework 契约4.4. 注册5.4a. /bg 斜杠命令(543995 行)6.4b. /stop 斜杠命令(只在 bg job 中生效,544066 行)7.4c. claude agents Commander 子命令(647846 行,仅作 fallback)8.4d. 总开关配置项(53308 行)9.5. 入口 / 分发 —— mainCliEntry()(cli 的 main)10.6. --bg 标志 → handleBgFlag → daemon dispatch11.6a. spawnBgSession 核心 —— spawnBgSessionAndDispatch / aN3(542926 行)12.6b. stdin 作为 positional 合并 —— withStdinPositional / BGK(543129 行)13.6c. 配套 CLI 动词(543217 行起)14.7. /bg 斜杠命令 —— 确认对话与 seed 抽取15.7a. extractSeedFromTranscript —— Bf6(543822 行)16.7b. BackgroundConfirmDialog —— Jv3(543849 行)17.7c. markSelfStoppedAndExit —— Uf6(544013 行)18.8. FleetView —— TUI 部分19.8a. mountFleetView —— uQ3(606636 行)20.8b. FleetView 组件 —— tmK(604467 行)—— 状态机21.8c. 键位绑定 —— handleFleetViewKeyDown / wS(605589 行)22.8d. peek 面板 —— PeekPane / WQ3(603580 行)23.8e. 视觉指示规则24.9. 持久化25.10. 端到端时序26.11. 坑 / 校正记录27.12. 移植到其他项目的最小落地清单28.13. 符号映射速查表29.bg-cli 与斜杠命令模块(QGK / gW_)30.FleetView 模块(bd8 / xd8)31.CLI 入口与 Commander32.配置 / 常量

claude agents/bg —— Agent View 实现剖析

源码:2.1.139-unbundled/extracted/src/entrypoints/cli.js(构建版本 208bf4b4,2026-05-11)。 官方博客参考:Agent view in Claude Code

"Agent view" 是一个特性、三个入口、一份磁盘上的 job 模型

入口行为
claude agents(CLI 子命令)当 daemon 已启用时,走 fast path,在 Commander 解析 argv 之前就拦截并挂载 FleetView TUI。当 daemon 被禁用时,回退到 Commander 注册的 agentsHandler,那只是一个 已配置子代理(subagent)定义 的纯文本列表。
claude --bg [task] / --background全新会话直接由 on-demand daemon 后台拉起,父进程打印一段提示后退出。
/bg(斜杠命令,/background 的别名)当前的 REPL 会话 fork 到后台,前台进程退出。在交互式 REPL 中,空输入下按左方向键 也会触发同一条 fork 路径,并额外在进程内重新挂载 FleetView。

三条路径最终都收敛到 ~/.claude/bg-jobs/<short>/state.json 文件 + Unix 域套接字 daemon(即 "on-demand daemon")。FleetView 本质上是对那个目录的轮询读取器,外加一个连接 daemon 的 RPC 客户端。

下面使用的命名约定:把被混淆过的符号重命名为有语义的名字,并在首次出现时附 /* <原始名> */。没有重命名的(oqHpTQ7B9QyHxH 等)是宿主进程共享的辅助函数(状态文件 IO、遥测、优雅退出)——你在移植到别的工程时要逐个替换。


1. 数据模型

$ ts
// state.json —— 每个后台 job 一份,存放在 ~/.claude/bg-jobs/<short>/
interface JobState {
  proto: number;                       // I1,schema 版本号
  short: string;                       // 8 位 16 进制前缀,CLI / FleetView 中用于寻址
  sessionId: string;                   // 完整 uuid,对应 transcript 文件名
  createdAt: string;                   // ISO
  updatedAt: string;                   // ISO,每次写入都 bump
  firstTerminalAt?: string;            // 首次进入终止态的时间

  // 身份 / 显示
  template: { name: string; description: string; initialPrompt?: string };
  agent?: string;                      // 如果用了 --agent,这里是 subagent 类型
  intent: string;                      // 首条用户消息,≤200 字符
  name?: string;                       // 显式 -n/--name;否则自动派生
  nameSource?: "user" | "auto";
  detail?: string;                     // 最近一次 assistant 输出节选,≤120 字符
  color?: string;

  // 活动状态
  state: "running" | "stopped" | "errored" | "done" | ...;
  tempo?: "active" | "idle" | "blocked";
  needs?: string;                      // agent 正在等待的东西
  block?: { questions?: Question[] };  // 当 tempo === "blocked" 时
  suggestedReply?: string;             // PeekPane 中按 Tab 可补全的预填回复
  output?: Record<string, string>;     // 行内显示的若干 headline 行
  inFlight?: unknown;
  resumeSessionId?: string;

  // 排序 / 置顶
  sortOrder?: number;
  stateSortOrder?: number;
  pinned?: boolean;

  // 位置
  cwd: string;
  originCwd?: string;
  worktreePath?: string;
  worktreeBranch?: string;
  worktreeHookBased?: boolean;

  // 链接
  children?: Array<<ruby>id: string; kind: &quot;frame&quot;<rt>&quot;pr&quot; | ...; href: string</rt></ruby>>;
  source?: "shell" | "slash" | "fleet" | "spare" | "respawn" | "left_arrow";

  // 其他簿记
  respawnFlags?: string[];
  env?: Record<string, string>;
}

interface Question { text: string; options: string[] }

// FleetView 内存中从一份 JobState 构造出来的行结构
interface JobRow <ruby>id: string;          // === short
  state: JobState;
  activity: &quot;flowing&quot;<rt>&quot;slowing&quot; | &quot;stuck&quot; | &quot;success&quot; | &quot;failure&quot; | &quot;stopped&quot;;
  tempo?: &quot;active&quot; | &quot;idle&quot; | &quot;blocked&quot;;
  template: string;    // state.template.name
  children?: JobState[&quot;children&quot;];
  origin?: &quot;C8&quot; | ...; // 当前 cwd 还是其他 cwd
  group?: string;</rt></ruby>

// FleetView 通过此 action 回报给 mountFleetView
type FleetAction =
  | { type: "done" }
  | <ruby>type: &quot;open&quot;; job: JobRow; query?: string; collapsed: string[];
      groupMode: &quot;state&quot;<rt>&quot;directory&quot;; jobs: JobRow[];
      loopKicks: Map&lt;string,number&gt;; statuses: Map&lt;string,number&gt;;
      statusesTs: number; prStatuses: Map&lt;string,unknown&gt;;
      respawnResult?: RespawnResult; freshDispatch?: boolean</rt></ruby>;

agentsHandler 回退处理器看到的"agents"是另一种东西——subagent 定义 列表(由 uk() 返回的 activeAgents),按来源(user / project / local / plugin / built-in)分组。这跟 FleetView 中的 "session 行" 没有关系。


2. 常量与文案

$ ts
// 总开关
const ENV_DISABLE = "CLAUDE_CODE_DISABLE_AGENT_VIEW";
const SETTING_DISABLE = "disableAgentView";            // 托管配置
const SETTING_DEFAULT_TO_FLEET = "defaultToAgentsView";// 用户配置(462620 行)

// 从 fork 中启动 `claude agents` 时预选 job
const ENV_PRESELECT = "CLAUDE_AGENTS_SELECT";

// 自动重启失焦的 fleet view(心跳)
const ENV_AUTORELAUNCH = "CLAUDE_AGENTS_AUTO_RELAUNCHED_AT";
const AUTO_RELAUNCH_UNFOCUSED_MS = 3_600_000;          // 1 小时
const AUTO_RELAUNCH_MIN_INTERVAL_MS = 21_600_000;      // 6 小时

// daemon 协议版本
declare const I1: number;

// `claude --bg < piped` 时 stdin 上限
const zB8 = 16 * 1024;                                 // 字节

// 每次 bg dispatch 之后打印的提示块
function formatPostBackgroundHints(short: string, idle?: string) {
  return [
    `backgrounded · ${cyan(short)}${idle ? dim(" " + idle) : ""}`,
    dim("  claude agents".padEnd(28) + "list sessions"),
    dim(`  claude attach ${short}`.padEnd(28) + "open in this terminal"),
    dim(`  claude logs   ${short}`.padEnd(28) + "show recent output"),
    dim(`  claude stop   ${short}`.padEnd(28) + "stop this session"),
  ].join("\n");
}

后台化相关的几条关键文案:

  • "Backgrounding…"—— 后台拉起过程中的 spinner。
  • "<N> tasks running — the forked session won't carry live processes."—— 确认框副标题。
  • "Background anyway (tasks will be abandoned)" / "Stay"—— 确认 / 取消按钮。
  • "Cannot background — session persistence is disabled, so the forked job would have nothing to resume."

3. Host-Framework 契约

下表是 agent view 模块依赖的、cli.js 中由其他模块提供的辅助函数。移植到其他工程时,需要把每一项替换成你那边对应的实现。

符号签名行号用途
oqH(seed) => JobState542886 处使用用默认值构造一份新的 JobState
pT(jobDir, partial) => Promise<void>542884 处使用原子地 merge 写 state.json
Q7(jobDir) => Promise<JobState|null>542964state.json
W4(short) => string542879~/.claude/bg-jobs/<short>/
rW() => string543233jobs 根目录;前缀查找用 readdir 扫这里。
k_() => string542814当前进程的 session id(在 fork 中被设置)。
L7() => boolean543959CLAUDE_JOB_DIR 是否存在 → 即"我是否已经是后台 fork"。
getCurrentJobDir / Xv3() => string | undefined544011返回 process.env.CLAUDE_JOB_DIR
V$(req, opts?) => Promise<DaemonResp>543031与 daemon 的 Unix socket RPC(op: "list" | "dispatch")。
mW_() => Promise<{ok,reason?}>543277daemon 健康检查(claude attach 用)。
MQ(short) => Promise<{outcome,msg?}>543286"把我接到这个 job 上去" RPC。
PGK(short, {alreadyInAlt}) => Promise<AttachResult>606723attach 期间负责本地 PTY ↔ daemon 中 job 的双向桥接。
fW6(jobId, {force?,knownState?,knownAlive?}) => Promise<RespawnResult>606700若 worker 已死则唤醒(Re·spawn)。
B9(exitCode, reason, opts?) => Promise<never>543885前台进程的优雅退出。
aH7.setupGracefulShutdown651511把 SIGINT/SIGTERM 接入 B9
cH.createRoot / Mu(opts) => Promise<InkRoot>651518创建一个共用 stdout 的 Ink 根节点。
v3_({exitOnCtrlC}) => Promise<InkRoot>606748PTY 移交之后重建 Ink 根。
Q5.get(process.stdout)?.handoffAltScreen606681把 alt-screen 控制权交给 PTY 用于 attach。
Q5.get(process.stdout)?.unmount606926在 in-process 重新挂载 fleet view 之前先卸载 Ink。
Q("tengu_*") / yH / xH / j6遥测 sink各处tengu_bg_dispatchtengu_background_forkfleet_view_*
IK("allow_remote_sessions")策略检查644468组织级开关。
Xc = isAgentsFleetEnabled()149497总闸门:合并托管配置、env、fast-path policy。
fleetGateRejected(verb)(string) => Promise<never>651424打印拒绝理由并退出。
EAH("claude agents")604574设置终端窗口标题。
vh() / zAH()字符串606686/606747进入 / 退出 alt-screen 的 ANSI 序列。
b_() / H6(updater)配置仓库604558/605650持久化 fleetViewGroupMode 等。
B26(cwd, {q,collapsed}) / ebK / qxKbg-view UI 状态604647按 cwd 记忆"上次的搜索与折叠状态"。
s6()() => {columns, rows}604583终端尺寸。
pA()() => boolean604578终端当前是否获焦。
un_()alt-screen flag606680是否拥有 alt-screen。
lT()() => WorktreeInfo | null606956当前 worktree 信息。
g$() / qp()name 查找543840用户设定 / 自动派生的 job 名。
WE(msg) / oI(msg)transcript 辅助543828抽取 assistant/user 消息正文。
viH(text)元信息文本判定543834用来在挑选 intent 时跳过斜杠命令回显。
LD(state)(JobState) => boolean544019job 处于终止态。
npH(state)activity 分类器602954"success"/"failure"/"stopped" 或 undefined。

daemon 协议(V$)至少接受这些请求:

$ ts
type DaemonReq =
  | { proto: number; op: "list" }
  | { proto: number; op: "dispatch"; d: JobDispatch; timeoutMs?: number };
type JobDispatch = {  // 543001-543027
  proto: number; short: string; sessionId: string; createdAt: number;
  source: "shell"|"slash"|"fleet"|"spare"|"respawn"|"left_arrow";
  cwd: string;
  launch:
    | { mode: "prompt"; args: string[] }
    | { mode: "resume"; sessionId: string; fork: boolean; flagArgs: string[] };
  respawnFlags: string[]; env: Record<string,string>; reattachEnv?: unknown;
  worktree?: { path: string; ownershipToken: string };
  isolation: "worktree" | "none";
  agent?: string; routine?: string;
  seed: { intent: string; name?: string };
  cols?: number; rows?: number;
};

4. 注册

4a. /bg 斜杠命令(543995 行)

$ ts
const backgroundCommand = {
  type: "local-jsx",
  name: "background",
  aliases: ["bg"],
  description: "Continue this session in the background and free the terminal",
  immediate: (raw) => !raw.trim(),               // 无额外 prompt 时跳过确认
  isEnabled: () => true,
  load: () => Promise.resolve().then(() => (_loadBgImpl(), _bgImplExports)),
};

load() 解析后得到一个导出 { default: handleBgSlashCommand } 的模块,其中:

$ ts
// 543958 行 —— Yv3
const handleBgSlashCommand = async (
  onDone: (err?: string) => void,
  ctx: { messages: Message[] },
  args: string
) => {
  if (L7() /* 自己就是 bg session */) {
    Q("tengu_background_already_bg", {});
    onDone(); q4H();
    return null;
  }
  if (YDH() /* 会话持久化被禁用 */)
    return (onDone(
      "Cannot background — session persistence is disabled, so the forked job would have nothing to resume."
    ), null);

  const extraPrompt = (args ?? "").trim();
  const seed = extractSeedFromTranscript /*Bf6*/ (ctx.messages, extraPrompt);
  if (seed === null)
    return (onDone("Nothing to background yet — send a message first."), null);

  return React.createElement(BackgroundConfirmDialog /*Jv3*/, {
    onDone, prompt: extraPrompt, seed, messages: ctx.messages,
  });
};

4b. /stop 斜杠命令(只在 bg job 中生效,544066 行)

$ ts
const stopJsx = {
  type: "local-jsx", name: "stop", immediate: true, isEnabled: L7,
  description: "Stop this background session; transcript and worktree are kept",
  requires: { ink: true },
  load: () => ({ call: (done) => (done(), markSelfStoppedAndExit("stop_command"), null) }),
};
const stopNonInteractive = {
  type: "local", name: "stop", supportsNonInteractive: true,
  isEnabled: L7,
  load: () => ({ call: () => (markSelfStoppedAndExit("bridge"), { type: "skip" }) }),
};

4c. claude agents Commander 子命令(647846 行,仅作 fallback

$ ts
H.command("agents")
  .description("Manage background and configured agents")
  .option("--setting-sources <sources>",
          "Comma-separated list of setting sources to load (user, project, local).")
  .action(async () => {
    const [{ agentsHandler }, { createSubcommandRoot }] = await Promise.all([
      import("./agents-handler.js"),
      import("./subcommand-root.js"),
    ]);
    await agentsHandler(await createSubcommandRoot());
    process.exit(0);
  });

4d. 总开关配置项(53308 行)

$ ts
disableAgentView: {
  type: "boolean",
  description:
    "Disable agent view (`claude agents`, `--bg`, /background, the on-demand daemon). " +
    "Typically set in managed settings. Equivalent to CLAUDE_CODE_DISABLE_AGENT_VIEW=1.",
};

5. 入口 / 分发 —— mainCliEntry()(cli 的 main

mainCliEntry(混淆名 je3)跑在 Commander 之前,让 claude agents 不必走 main() 的重型导入,直接进入 FleetView。

$ ts
// 651265
async function mainCliEntry /*je3*/ () {
  const argv = process.argv.slice(2);

  // …  --version / --daemon-worker / --bg-pty-host / --bg-spare / bridge / daemon 等早返回路径

  // ───────── bg 动词 fast path(651387 行) ─────────
  if (
    argv[0] === "logs" || argv[0] === "attach"  || argv[0] === "stop" ||
    argv[0] === "kill" || argv[0] === "respawn" || argv[0] === "rm"   ||
    argv.includes("--bg") || argv.includes("--background")
  ) {
    if (!isAgentsFleetEnabled()) return fleetGateRejected(verb);
    const bg = await import("./bg-cli.js"); // QGK
    switch (argv[0]) {
      case "logs":    return bg.logsHandler(argv[1]);
      case "attach":  return bg.attachHandler(argv[1]);
      case "stop":    case "kill":  return bg.stopHandler(argv[1]);
      case "rm":      return bg.rmHandler(argv[1]);
      case "respawn": return bg.respawnHandler(argv[1]);
      default: {
        // --bg / --background 仍在 argv 中,第一个 positional 才是 prompt
        Q("tengu_background", { via_flag: true, via: "flag" });
        await bg.handleBgFlag(argv);
        return process.exit(process.exitCode ?? 0);
      }
    }
  }

  // ───────── fleet-view fast path(651485 行) ─────────
  const wantsAgents = argv[0] === "agents" && argsAreOnlyDebugFlags(argv.slice(1));
  const wantsDefault = argsAreOnlyDebugFlags(argv) && process.stdin.isTTY && process.stdout.isTTY;
  if ((wantsAgents || wantsDefault)) {
    enableConfigs();
    const defaultsToAgents = getGlobalConfig().defaultToAgentsView === true;
    if (wantsAgents || defaultsToAgents) {
      const policyErr = await loadFastPathPolicy();
      if (policyErr) exitWithError(policyErr);
      await ensureFleetGateHydrated();
      if (isAgentsFleetEnabled()) {
        initSinks(); setupGracefulShutdown(); setIsInteractive(true);
        Q("tengu_fleetview", { defaultToAgentsView: defaultsToAgents });
        const root = await createRoot({ exitOnCtrlC: false });
        consumeEarlyInput();
        await mountFleetView(root);
        await gracefulShutdown(0, "other", { suppressResumeHint: true });
        return;
      }
    }
  }

  // …其余情况 → main()(即 Commander,包含 agents 子命令的 fallback)
}

function argsAreOnlyDebugFlags /*caK*/ (argv) {
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (["--debug","-d","--debug-to-stderr","-d2e"].includes(a)
        || a.startsWith("--debug=") || a.startsWith("--debug-file=")) continue;
    if (a === "--debug-file" && i + 1 < argv.length) { i++; continue; }
    return false;
  }
  return true;
}

两个关键结论:

  1. claude agents 是 Commander 子命令里唯一在 parse 之前就被劫持的,所以它真正的实现是 mountFleetView,而不是 .action() 里那个 body —— 后者只在 isAgentsFleetEnabled() 返回 false 时才执行。
  2. 无参数的 claudedefaultToAgentsView: true 时也会直接进入 FleetView;否则正常落入 main() 走 REPL。

6. --bg 标志 → handleBgFlag → daemon dispatch

$ ts
// 543089 行 —— sN3
async function handleBgFlag(argvWithoutFlag: string[]) {
  // argvWithoutFlag 是去掉 --bg / --background 之后的 argv
  const rest = argvWithoutFlag.filter((a) => !["--bg","--background"].includes(a));
  const stdinTail = await readBgStdin();      // pGK;超过 16 KiB 会截断
  const merged = stdinTail ? withStdinPositional(rest, stdinTail) : rest;

  const res = await spawnBgSession(merged);   // pHH → aN3
  if (!res.ok) {
    process.stderr.write(res.error + "\n");
    process.exitCode = 1;
    return;
  }
  process.stdout.write(formatPostBackgroundHints(res.short, res.idle ? "(idle — waiting for…)" : undefined) + "\n");
}

6a. spawnBgSession 核心 —— spawnBgSessionAndDispatch / aN3(542926 行)

下面是把它拆开后的伪代码:

$ ts
async function spawnBgSessionAndDispatch /*aN3*/ (
  argv, source, cwd, seed /* Bf6 的结果 */, reattachEnv, t /*ids*/
) {
  const { sessionId, short, jobDir, freshDir } = t;
  const dashDash = argv.indexOf("--");
  const flagArgs = dashDash >= 0 ? argv.slice(0, dashDash) : argv;

  const agentName  = readFlag("--agent", flagArgs);
  const sessionName = readFlag("--name", flagArgs) ?? readFlag("-n", flagArgs);
  const resumeId    = parseResumeTarget(flagArgs);       // FGK
  const promptArg   = dashDash >= 0
    ? argv.slice(dashDash + 1).join(" ")
    : positionalArgFor(argv, resumeId);

  const isResume = flagArgs.some(f => f === "-c" || f === "--continue" ||
                                       f === "-r" || f === "--resume" ||
                                       f.startsWith("--resume=") || f.startsWith("-r="));
  const isFork = flagArgs.includes("--fork-session");
  const respawnFlags = respawnableFlags(flagArgs);       // Ov3
  const dispatchFlagArgs = dashDash >= 0 ? respawnFlags : flagsWithoutPositional(respawnFlags);
  const resumingSelf = resumeId !== undefined && resumeId === sessionId;
  const forkFlag = isResume && !isFork ? ["--fork-session"] : [];
  const sessionArgs = resumingSelf ? [] : ["--session-id", sessionId, ...forkFlag];

  if (source === "shell" && readFlag("--session-id", flagArgs))
    warn("--bg manages the session id; ignoring --session-id (use --resume <id> instead)");

  const subagent = agentName
    ? (await uk(cwd ?? process.cwd())).activeAgents.find(a => a.agentType === agentName)
    : undefined;
  if (agentName && !subagent && source === "shell")
    warn(`no agent named '${agentName}' — spawning with default template`);

  const intent = seed?.intent ?? promptArg ?? "";
  const isIdle = !agentName && intent === "" && !seed?.detail;

  // 预写 state.json(fleet / spare 来源跳过,它们通过 dispatch 携带 seed)
  if (source !== "fleet" && source !== "spare") {
    const prior = freshDir ? null : await readJobState(jobDir);
    if (prior === null)
      writeJobState(jobDir, oqH({
        template: {
          name: agentName ?? "bg",
          description: subagent?.whenToUse ?? "",
          initialPrompt: subagent?.initialPrompt,
        },
        respawnFlags: dispatchFlagArgs,
        intent,
        name: sessionName ?? seed?.name,
        nameSource: sessionName ? "user" : seed?.nameSource,
        detail: seed?.detail ?? (isIdle ? "(blocked)" : undefined),
        tempo: isIdle ? "blocked" : undefined,
        sessionId, cwd: cwd ?? process.cwd(),
        worktreePath: seed?.worktree?.path,
        worktreeBranch: seed?.worktree?.branch,
        worktreeHookBased: seed?.worktree?.hookBased,
        originCwd: seed?.worktree?.originCwd,
      }));
    else if (dispatchFlagArgs.length && !prior.respawnFlags.length)
      writeJobState(jobDir, { ...prior, respawnFlags: dispatchFlagArgs });
  }

  // 投递给 daemon
  const dispatch = {
    proto: I1, short, sessionId, createdAt: Date.now(),
    source: source === "repl" ? "slash" : source,
    cwd: cwd ?? process.cwd(),
    launch: isResume && resumeId
      ? <ruby>mode: &quot;resume&quot;, sessionId: resumeId,
          fork: !resumingSelf &amp;&amp; (isFork<rt>| forkFlag.length &gt; 0),
          flagArgs: [...respawnFlags, ...(dashDash &gt;= 0 ? argv.slice(dashDash) : [])]</rt></ruby>
      : { mode: "prompt", args: [...sessionArgs, ...positionalsAfterFlags(argv)] },
    respawnFlags, env: bgEnvWhitelist(), reattachEnv,
    worktree: seed?.worktree && { path: seed.worktree.path, ownershipToken: sessionId },
    isolation: subagent?.isolation === "worktree" && subagent.source !== "built-in" ? "worktree" : "none",
    agent: agentName, routine: undefined,
    seed: { intent, name: sessionName ?? seed?.name },
    cols: process.stdout.columns, rows: process.stdout.rows,
  };
  const r = await daemonRpcDispatch(dispatch);          // AB8 → V$
  if (r.ok) return { ok: true, short, sessionId, idle: isIdle };

  // 兜底分支:
  //  - ack-timeout / enoconn 但 op:"list" 显示 worker 还在 → 判定成功
  //  - ack-timeout 且 list 中没出现 → 重投一次
  //  - short-alive → "session is already running" 的友好错误
  //  - stale-short → "前一会话还在关闭,稍后重试"
  //  - daemon-unreachable → 退出并提示运行 status
  //(参见 543030-543087)
}

6b. stdin 作为 positional 合并 —— withStdinPositional / BGK(543129 行)

如果用户把 prompt 文本管道送进 --bg 且 argv 中已经有 positional,则 stdin 的尾部会被追加到最后那个 positional(中间用换行隔开),从而 daemon 只看到一个合并后的 prompt。若 argv 中没有 positional,则在 -- 哨兵后面新增一条。

6c. 配套 CLI 动词(543217 行起)

  • claude logs <prefix>logsHandler —— 向 daemon 开一个 500 ms 的 RPC 订阅,打印 streamTail 快照后退出。
  • claude attach <prefix>attachHandler —— 先做健康检查(mW_),然后 MQ(short) 接管 PTY;遇到 ERESPAWNING 最多重试 20 次;若是 ENOJOB,从 state.json 读一份内容用于更有用的错误信息。
  • claude stop / kill / rm / respawn <prefix> → 对应处理器(在 542861 行通过 M_(QGK,…) 一并注册)。

所有前缀参数都经过 resolveJobByShortPrefix /* jB8 */(543217 行)解析:readdir~/.claude/bg-jobs/,确保前缀唯一。


7. /bg 斜杠命令 —— 确认对话与 seed 抽取

7a. extractSeedFromTranscript —— Bf6(543822 行)

从下往上遍历 transcript,产出 { intent, name, nameSource, detail }

$ ts
function extractSeedFromTranscript(messages, override) {
  let firstUserText = override, hasNonMetaUser = false, lastAssistant: string|undefined;
  for (let i = messages.length - 1; i >= 0; i--) {
    const m = messages[i];
    if (m.type === "assistant" && lastAssistant === undefined) {
      const text = WE(m);                   // 最底部那条 assistant 消息正文
      if (text) lastAssistant = text.replace(/\s+/g," ").trim().slice(0,120);
    }
    if (m.type === "user" && !m.isMeta && !QY6(m)) {
      const text = oI(m)?.trim();
      if (text && viH(text)) continue;      // 跳过斜杠命令的回显
      hasNonMetaUser = true;
      if (!firstUserText && text) firstUserText = text;
    }
    if (hasNonMetaUser && firstUserText && lastAssistant !== undefined) break;
  }
  if (!hasNonMetaUser) return null;         // "Nothing to background yet"
  const userName = g$(jobSessionId());
  const autoName = qp(jobSessionId());
  return <ruby>intent: (firstUserText<rt>| &quot;(backgrounded)&quot;).slice(0, 200),
    name:   userName ?? autoName,
    nameSource: userName ? &quot;user&quot; : autoName ? &quot;auto&quot; : undefined,
    detail: lastAssistant,</rt></ruby>;
}

7b. BackgroundConfirmDialog —— Jv3(543849 行)

  • 通过 J_ 读两个 AppState 字段:
    • effortValueDv3
    • taskCountjv3 —— state.tasks
  • 若 in-flight task 数为 0,组件初次渲染就自动确认(React.useState(taskCount === 0))。
  • 否则渲染一个 <ConfirmDialog>,标题 "Background this session?"、副标题 "<N> tasks running — the forked session won't carry live processes.",按钮分别是 "Background anyway (tasks will be abandoned)""Stay"
  • 确认后:
    $ ts
    const r = await forkCurrentSessionToBackground /*pf6*/ (
      seed, extraPrompt, effortValue, "command", messages
    );
    Q("tengu_background_fork", {
      confirmed: taskCount > 0, inflight_count: taskCount,
      had_prompt: extraPrompt.length > 0,
      had_worktree: r.hadWorktree, worktree_handed_off: r.handedOff,
    });
    onDone();
    await B9(0, "prompt_input_exit", {
      suppressResumeHint: true,
      finalMessage: formatPostBackgroundHints(r.short, r.handedOff ? "(worktree handed off)" : undefined),
    });
    
  • forkCurrentSessionToBackgroundpf6,同模块)—— 以 source: "slash" 调用 spawnBgSession,移交活动的 worktree,并拆掉 bridge / daemon 监听器(DV())。

7c. markSelfStoppedAndExit —— Uf6(544013 行)

$ ts
async function markSelfStoppedAndExit(source: "stop_command"|"bridge") {
  Q("tengu_bg_agent_action", { action: "stop", source, jobSessionId: jobSessionId() });
  const dir = process.env.CLAUDE_JOB_DIR;
  if (L7() && dir) {
    const now = new Date().toISOString();
    const cur = await readJobState(dir);
    if (cur && !isJobTerminal(cur))
      await writeJobState(dir, {
        ...cur, state: "stopped", detail: "stopped from session",
        tempo: "idle", needs: undefined, block: undefined, inFlight: undefined,
        updatedAt: now,
        firstTerminalAt: cur.firstTerminalAt ?? now,
      });
    if (isInteractive()) process.stdout.write(formatExitNotice("Session stopped."));
  }
  return B9(0, "prompt_input_exit", { suppressResumeHint: true });
}

8. FleetView —— TUI 部分

8a. mountFleetView —— uQ3(606636 行)

主循环就是渲染 → 等动作 → 可能 attach → 卸载重挂。它在 agent view 的整段生命周期里独占 alt-screen。

$ ts
async function mountFleetView(inkRoot) {
  Q("tengu_bg_agent_action", { action: "list_open" });
  setTerminalTitle("claude agents");

  const preselectId = process.env.CLAUDE_AGENTS_SELECT;
  delete process.env.CLAUDE_AGENTS_SELECT;               // 单次使用
  const persisted = await loadFleetUiState(currentCwd());// qxK
  let query     = persisted?.q;
  let collapsed = persisted?.collapsed;
  let jobId     = preselectId;
  let error: string | undefined;
  let groupMode: "state"|"directory"|undefined;
  let savedAppState: any;

  for (;;) {
    const action = await new Promise<FleetAction>((resolve) => {
      inkRoot.render(
        <ProcessTitleProvider>
          <AppStateBoundary
            initialState={savedAppState && {...savedAppState, notifications:{current:null,queue:[]}}}
            onChangeAppState={({newState}) => savedAppState = newState}
          >
            <ContextProviders>
              <ScrollFrame>
                <FleetView
                  onAction={resolve}
                  initialJobId={jobId}
                  initialQuery={query}
                  initialCollapsed={collapsed}
                  initialError={error}
                  initialGroupMode={groupMode}
                />
              </ScrollFrame>
            </ContextProviders>
          </AppStateBoundary>
        </ProcessTitleProvider>,
      );
    });

    const inAlt = ownsAltScreen();                       // un_
    if (inAlt && action.type === "open") handoffAltScreen();
    if (!inAlt) inkRoot.render(null);
    inkRoot.unmount();

    if (action.type === "done") break;

    // ── action.type === "open" → attach ──
    if (isWindows() && process.stdin.isTTY) { process.stdin.setRawMode(true); process.stdin.ref(); }
    const restoreCursor = inAlt
      ? batchUpdate(() => process.stdout.write(altScreenReentry()))
      : () => {};

    jobId     = action.job.id;
    query     = action.query;
    collapsed = action.collapsed;
    groupMode = action.groupMode;
    cachedJobs       = action.jobs;
    cachedLoopKicks  = action.loopKicks;
    cachedStatuses   = action.statuses;
    cachedStatusesTs = action.statusesTs;
    cachedPrStatuses = action.prStatuses;
    cachedFirstMount = true;

    const respawn = action.respawnResult ?? await respawnJob(action.job.id, {
      knownState: action.freshDispatch ? undefined : action.job.state,
      knownAlive: (Date.now() - action.statusesTs < 1500
                   && action.statuses.get(action.job.state.resumeSessionId
                                          ?? action.job.state.sessionId) !== undefined)
                  ? true : undefined,
    });

    if (respawn.ok || respawn.alive) {
      logFleetViewLifecycle("attach", action.job.state);
      process.stdout.write(SET_TITLE(renderJobLabel(action.job.state, true)));
      let attach = await attachJob(respawn.short ?? action.job.id, { alreadyInAlt: inAlt });

      if (attach.kind === "error" && attach.orphaned) {
        // 最后一次努力:强制 respawn 一次再试
        const re = await respawnJob(action.job.id, { force: true, knownState: action.job.state });
        if (re.ok || re.alive) attach = await attachJob(re.short ?? action.job.id, { alreadyInAlt: inAlt });
        else attach = { kind: "error", msg: re.error };
      }
      if (attach.kind === "error" && !attach.ended) {
        error = attach.msg;
        xH("fleet_view_open", "attach_failed");
      } else {
        if (attach.msg) error = attach.msg;
        yH("fleet_view_open");
      }
      logFleetViewLifecycle("detach", action.job.state, { attachDurationMs: Date.now() - tStart });
    } else {
      error = respawn.error;
      xH("fleet_view_open", "respawn_failed");
    }

    if (inAlt) process.stdout.write(restoreAltMargins());
    inkRoot = await createRoot({ exitOnCtrlC: false });
    restoreCursor();
  }
}

8b. FleetView 组件 —— tmK(604467 行)—— 状态机

组件内部状态(按 useState/useRef 顺序大致排列):

槽位用途
jobs / setJobs (A, z)当前挂载的 JobRow 列表。bootstrap 中为 null。快照镜像到 $.current 供回调使用。
incomingJobs (Y, w)本次挂载内新发起、还在等 state.json 落盘的 job。
prStatuses (F, g)PR check 状态缓存。
cursorIndex (d, a)扁平化后行列表 h9 的索引。
peekJob (_H, t)当前正在 peek 的 job。
dispatchFolded (qH, i)是否折叠 "queued/folded" 段落。
replyDrafts (e.current)Map<jobId, draftText>:peek 面板的内联回复草稿。
replyErr ($H, AH)最近一次回复失败的信息。
helpOpen (s, o) / confirmDelete (HH, KH) / selectMenu (jH, YH)各种 overlay 模态。
exiting (MH, kH)退出二次确认;LH = ph(setExiting, () => onAction({type:"done"})) 把 Ctrl+C 串起来。
renaming (WH, DH)正在重命名的 job 或 null。
attaching (XH, NH)正在 attach 前 respawn 的 job id。
error ($9, L6)toast / 错误行。
groupMode (tH, v_) —— "state" | "directory"持久化到 b_().fleetViewGroupMode
collapsedGroups (UH, BH) —— Set<string>按 cwd 持久化。
manuallyCollapsedPinned (FH, q_)pinned 段被手动折叠但不影响持久化集合时使用。
cwd (L) → 最终解析为 repo 根(_Y(cwd))。
pinnedDir map (u, b)按 cwd 的快速 pin 映射。
query / cursorOffset(来自 v0(...) 多行输入 hook)搜索框。

它还从同模块(602820–602850 行)导入 repoGroup({cwd}) (Nd8)、pickIconjobLabelchildStatusColor 等若干辅助。

8c. 键位绑定 —— handleFleetViewKeyDown / wS(605589 行)

$ text
当 overlay 显示中(renaming / attaching):
  Ctrl+C        取消 overlay
  up/down       穿透给 overlay 自身的滚动逻辑
  其他          转交给 overlay 内部的输入处理

全局:
  Ctrl+C        清空输入 → 帮助/确认删除若开则关 → 否则退出
  Escape        关帮助 → 关确认删除 → 关折叠帮助 → 关菜单 → 清输入 → 撤销 armed 动作 → onAction("done")
  Shift+up/dn   在组内移动 job(Sy/SY)
  Ctrl+R        重命名所选 job   (renaming = job.id)
  Ctrl+S        在 "state" ↔ "directory" 分组模式之间切换
  Ctrl+G        在查询中展开 / 替换
  Ctrl+T        pin / unpin 所选 job
  up / Ctrl+P   移动光标(或滚动补全建议)
  down / Ctrl+N 移动光标
  Ctrl+X (帮助打开时) → 杀掉 armed 动作目标
  Tab           接受斜杠建议 / 切换 dispatchFolded
  →             attach 当前行(也就是博客中的 right-arrow)
  ⌘/Super+1-9   attach 当前 cwd 组的第 N 行
  Return        若输入为空:识别 /exit/quit 退出 → 否则提交建议 → 解析 "<template> <intent>",
                  分发一个新 bg job →(Shift+Return ⇒ 额外通过 {type:"open", freshDispatch:true} 自动 attach)
  空格-空输入   切换当前行的 peek 模式  (也就是博客中的 "peek")

FleetView 中的"左方向键"由多行输入 hook(v0 的默认 cancel)处理 —— 输入为空时回退到 onCancel: setExiting,由 ph 接管做一次"再按一次确认退出"(LH)。

8d. peek 面板 —— PeekPane / WQ3(603580 行)

这就是博客描述的 "peek mode":选中某一行但不完整 attach。

  • q.state.updatedAt 渲染 r7(now - updated) 显示 "Last interaction X ago"。最近 1 分钟内每 1s 重渲,否则 30s。
  • q.state.output 里挑三条信息行(剔除与内联子列表标签重复的),再加 state.needsstate.detail,根据终端高度自适应。
  • 它有自己v0 输入框,绑定 replyDrafts.get(jobId)
    • onSpaceOnEmpty: onBack —— 空格退出 peek。
    • onTabOnEmpty: () => setQuery(suggestedReply); Q("tengu_prompt_suggestion", { outcome: "accepted", source: "fleetview_peek" }) —— Tab 接受 state.suggestedReply(仅当 tempo === "blocked" 且没有 questions 表单时才存在)。
    • onExit → 通过 onReply(formattedDraft) 提交草稿;成功后 daemon 那边的 worker 自动恢复 —— 这就是博客中"内联回复自动 resume"的实现。
  • state.tempo === "blocked"state.block.questions 存在,渲染一个 <QuestionForm>jH = 2 + opts.length + 1 行的额外布局空间)。

8e. 视觉指示规则

  • 行颜色 / activity 字形 = jobActivityForRow(jobRow, prStatuses)"flowing" | "slowing" | "stuck" | "success" | "failure" | "stopped"。"slowing" / "stuck" 阈值取自 updatedAt
    • tempo === "active":3 分钟 → "flowing",超过 15 分钟 → "stuck"。
    • 否则:15 分钟 → "flowing",超过 75 分钟 → "stuck"。
  • "awaits input" = tempo === "blocked"(block.questions 存在)或 state.needs 非空。
  • "actively processing" = tempo === "active" 且非终止态。
  • "completed" = LD(state) 为真。
  • "next run time"(循环 job)= vd8(jobRow, loopNextRun),显示 "in <时长>"

9. 持久化

  • 每个 job 一份 state.json —— FleetView 与 claude logs/attach/... 的唯一真相源。通过 pT(jobDir, partial) 原子 merge 写入。
  • ~/.claude/bg-jobs/ —— <short>/ 子目录的集合。stdout/stderr 是 worker 的 PTY 历史(通过 daemon RPC 读取,经磁盘)。
  • FleetView UI 状态(按 cwd) —— B26(cwd, { q, collapsed }) 以 repo 路径为 key,加载时通过 qxK(repoRoot),写入有 300 ms debounce。
  • 分组模式偏好 —— b_().fleetViewGroupMode 通过 H6(updater) 写入用户配置仓库。
  • Daemon socket / 心跳 —— .fleetview-heartbeat 锁文件(Moq,104916 行)。

10. 端到端时序

$ text
A) `claude --bg "ship the redis fix"`

  argv 进入 mainCliEntry() → 命中 `argv.includes("--bg")`(651394 行)
        │
        ▼
  fleetGate ok → handleBgFlag(argv)                  [543089]
        │   stdin (TTY) → "";rest = argv \ ["--bg"]
        ▼
  spawnBgSession(rest)                               [pHH, 542905]
        │ 生成 sessionId, short, jobDir
        ▼
  spawnBgSessionAndDispatch                          [aN3, 542926]
        │  • 写 state.json(seed 数据)
        │  • 构造 JobDispatch
        ▼
  daemon V$ {op:"dispatch", d}                       [543031]
        │  daemon 可能回 ok / ack-timeout / enoconn / short-alive / stale-short
        ▼  ack-timeout 的重投 / 救援循环
  stdout ← formatPostBackgroundHints(short, idle?)   [543177]
  exit 0


B) 实时会话里输入 `/bg`

  REPL 接到 /bg → 加载 handleBgSlashCommand          [Yv3, 543958]
        │ 守卫:已经是 bg / 持久化关闭
        ▼
  extractSeedFromTranscript 走 transcript → {intent,name,detail}
        ▼
  <BackgroundConfirmDialog/>                         [Jv3, 543849]
        │  task=0 自动确认;否则弹 <ConfirmDialog>
        ▼
  forkCurrentSessionToBackground(seed, prompt, effort, "command", messages)  [pf6]
        │  与 --bg 同一条 dispatch 路径,但 source="slash",移交 worktree、拆 bridge
        ▼
  Q("tengu_background_fork", …)
  onDone() → B9(0, "prompt_input_exit", {finalMessage: formatPostBackgroundHints(short, …)})


C) `claude agents`(或 REPL 中按 ←)

  REPL 中 ← → forkAndOpenFleetViewFromRepl(messages, effort, autoName)  [KpK, 606947]
        │  extractSeedFromTranscript(允许空)→ wB8 预写 state.json
        │  forkCurrentSessionToBackground 拉起 bg fork
        │  Q("tengu_open_agents_via_left")
        │  若 tengu_bg_leftarrow_inprocess 开关为 true:
        │    → remountFleetViewInProcess(带这个 short 预选)
        │  否则:
        │    → FjH({ args:["agents"], env:{CLAUDE_AGENTS_SELECT: short} })  ← exec 一个新的 claude

  `claude agents`  →  mainCliEntry() → argsAreOnlyDebugFlags 通过 → isAgentsFleetEnabled()
        │
        ▼
  mountFleetView(root)                               [uQ3, 606636]
        ┌──────────────── 循环 ────────────────┐
        ▼                                      │
  <FleetView/> 渲染 → 用户导航 ─────────────────┤
        │                                      │
        ▼ onAction === "open"                  │
  respawnJob → attachJob (PTY)                 │
        │ 用户工作 → 最终 detach               │
        ▼ onAction === "done"                  │
  gracefulShutdown(0)                          │
        └──────────────────────────────────────┘

11. 坑 / 校正记录

  1. claude agents 是两条命令的合体。 647846 行 Commander 注册的 agentsHandlerCs3)只在 fleet view 关闭时生效;它输出的是按 source 分组的 subagent 定义列表。博客所说的 "agent view" 实际是 651485 行的 fast-path FleetView。
  2. /bg 是别名。 真名是 /background(543999 行),bgaliasesimmediate: (raw) => !raw.trim() 表示纯输 /bg<Enter> 跳过 JSX 确认;带 prompt 文本则会卡在 React 树。
  3. /bg--bg 走同一条 dispatch。 唯一差异是:(a) 写入 state.jsonsource 字段不同;(b) 父进程是否需要拆 worktree/bridge。daemon 完全不区分。
  4. L7()("是否在 bg 中")≠ "会话已被后台化"。 它当且仅当本进程的 CLAUDE_JOB_DIR 被设置时返回 true —— 即"我是那个 fork"。刚刚起 fork 的那个前台进程仍然不属于"已后台化";所以 /bg 在 dispatch 之后调 B9(0, "prompt_input_exit") 让前台主动退出。
  5. CLAUDE_AGENTS_SELECT 是一次性的。 mountFleetView 一拿到就 delete env(606640 行),避免长生命周期的 fleet view 锁死在某个过期 id 上。
  6. "Background anyway" 文案说的是 tasks,不是 files。 它读的 taskCountjv3 = state => state.tasks)来自 AppState 的 tasks map —— 也就是 bash/长跑工具 —— 不是 in-flight 请求。是两个不同概念,但 UI 文案只提到 tasks。
  7. 左方向键退出只在输入为空时生效。 实际触发的是多行输入 hook v0onCancel;输入有内容时左键只是移动光标。
  8. disableAgentView 是唯一一刀切。 一旦置位(托管配置或 env),claude agents 会回退到 agentsHandler--bg/--background 会被 gate 拦下、/background 斜杠命令虽然 isEnabled 永远为 true,但内部 pHH 仍会在 fleetGateRejected 处失败,daemon 也拒绝拉起。
  9. spawnBgSession 救援两个特定 daemon 故障ack-timeout(worker 还在,只是 daemon 回包丢了)与 enoconn(RPC 半途 socket 没了)。它先发 op:"list" 探一下,要么宣告成功,要么尝试重投一次。
  10. 轮询节奏。 FleetView 中 peek 时间戳每 1s(近 1 分钟)或 30s(更久)重渲。但文件 watcher / RPC poller 频率在另外的模块里(602820 一带的 seedLastJobs 加另一组 hook)。移植时这块要单独重写。
  11. claude 配合 defaultToAgentsView: true 默认进 FleetView。 用户在设置面板(462620 行)勾上后再裸跑 claude 就直接进去 —— 容易被意外触发。

12. 移植到其他项目的最小落地清单

如果想把 "agent view + /bg" 带到别的 shell(比如某个 Anthropic CLI 的 fork,或者一个无关的助手 CLI):

  1. 磁盘模型。 ~/.claude/bg-jobs/<short>/state.json:原子 merge 写、readdir 前缀查找、第 1 节定义的 JobState schema。
  2. Daemon。 一个 Unix socket 服务,至少支持 {op:"list"}{op:"dispatch", d:JobDispatch} 以及一个按 job 的 PTY attach RPC。macOS/Linux 每用户一个 unix:-协议 socket;Windows 用同样 JSON 信封的 named pipe。带 fast_path_policy 标记,便于被托管配置硬关。
  3. Worker。 由 daemon 用 JobDispatch.env 中的环境拉起,环境包含 CLAUDE_JOB_DIR=<dir>--session-id <uuid>,prompt 作为最后一个 positional。worker 自己 update state.json(intent、name、output、tempo、block)。
  4. /bg 斜杠命令。 local-jsxaliases: ["bg"]。seed 由 transcript 自下而上扫描得到(跳过 meta 和斜杠命令回显)。state.tasks > 0 时弹确认;否则自动确认。
  5. --bg CLI 标志。 去掉该 flag,读管道 stdin(截断 16 KiB),调入同一个 dispatcher。成功时打印 5 行提示块。
  6. claude logs / attach / stop / kill / rm / respawn <prefix> 把 prefix 解析为完整 short,发 RPC。attach 要小心:必须接管 PTY 并能扛住 ERESPAWNING
  7. FleetView TUI。
    • mountFleetView(inkRoot) 循环:渲染 → 等 <ruby>type:&quot;open&quot;<rt>&quot;done&quot;</rt></ruby> → 若 open 则 respawn + attachJob + detach 后再渲染。
    • 组件内部:按 cwd 持久化的 UI 状态(query + collapsed)、state/directory 分组切换、双重用途的 peek 面板(既看又回)、多行输入、重命名 overlay、删除确认 overlay、帮助 overlay、组内拖拽排序。
    • 单独维护 attaching ref,避免同一行被双击触发两次。
  8. 闸门。 托管配置 disableAgentView + env CLAUDE_CODE_DISABLE_AGENT_VIEW + 组织策略返回的 fast_path_policy。每个入口前都过一次同一个 isAgentsFleetEnabled()
  9. 遥测。 至少:tengu_background(flag/slash)、tengu_background_forktengu_bg_dispatchtengu_bg_dispatch_fallbacktengu_bg_dispatch_rescuedtengu_open_agents_via_lefttengu_fleetviewtengu_bg_agent_actiontengu_prompt_suggestion,以及所有 fleet_view_* 动作动词(open / rename / pin_toggle / reorder / stop / attach / detach)。
  10. 左方向键 → agents。 Hook 住 REPL 输入框"空 + 左方向键":(a) 从当前 transcript seed 一个 fresh bg fork,(b) 拆当前 worktree/bridge,(c) 优先 in-process 重挂 fleet view;失败回退 exec
  11. 风险声明的 gating。 --bg 配合 bypassPermissionsauto 权限模式需要先在交互式中确认一次 —— 不要绕过 543643-543646 行的检查,避免 headless dispatch 偷过 UX 关卡。

13. 符号映射速查表

bg-cli 与斜杠命令模块(QGK / gW_

重命名后混淆名行号
preSeedReplBgJobwB8542877
spawnBgSessionpHH542905
spawnBgSessionAndDispatchaN3542926
markBgDispatchSucceededIGK542802
markBgDispatchFellThroughOB8542820
handleBgFlagsN3543089
readBgStdinpGK543108
withStdinPositionalBGK543129
humanizeDispatchFailureReasontN3543163
formatPostBackgroundHintsmf6543177
bgVerbExtraArgsNoteUGK543188
resolveJobByShortPrefixjB8543217
logsHandlereN3543246
attachHandlerHv3543271
stopHandlerqv3(export 见 542863)
rmHandlerKv3(export 见 542865)
respawnHandler_v3(export 见 542866)
parseResumeTargetFGK(export 见 542869)
flagsWithoutPositionalgGK(export 见 542873)
extractSeedFromTranscriptBf6543822
BackgroundConfirmDialogJv3543849
handleBgSlashCommandYv3543958
BG_CLI_FLAG_ALIASES (["--bg","--background"])oN3543733
forkCurrentSessionToBackgroundpf6(调用见 543875、606981)
BG_SLASH_COMMAND_DEFMv3 / fv3543999
getCurrentJobDirXv3544011
markSelfStoppedAndExitUf6544013
stopSlashJsxCallPv3544047
stopBridgeNonInteractiveCallWv3544055

FleetView 模块(bd8 / xd8

重命名后混淆名行号
FleetView(组件)tmK604467
mountFleetViewuQ3606636
remountFleetViewInProcessemK606921
forkAndOpenFleetViewFromReplKpK606947
PeekPane(组件)WQ3603580
renderJobLabelZW6602868
formatJobAgeOrCountdownvd8602863
classifyJobActivityjW6602953
deriveStateBandIvH602971
repoGroupKeyNd8604497
parseDispatchInputZd8603040
parseSearchQueryBmK603002
pruneMapPd8602855
logFleetViewLifecycle("peek"|"attach"|"detach")Rd8603598
handleFleetViewKeyDownwS605589
swapAdjacentJobsInGroupSY605532
attachJobOrSurfaceErrorYS605454
runRowKeyActionlj605425

CLI 入口与 Commander

重命名后混淆名行号
mainCliEntryje3651265
argsAreOnlyDebugFlagscaK651245
agentsHandler(fallback —— 列出 agent 定义,不是 jobs)Cs3644428
AgentsList(fallback 组件)FoK644390
formatAgentDefinitionRowBoK644383
renderAgentSourceGroupEs3644416
isAgentsFleetEnabledXc149497
fleetGateRejected(以同名 re-export)651424

配置 / 常量

重命名后混淆名行号
SETTING_DISABLE"disableAgentView"y53308
SETTING_DEFAULT_TO_FLEET"defaultToAgentsView"462620
ENV_PRESELECT"CLAUDE_AGENTS_SELECT"606639
ENV_AUTORELAUNCH"CLAUDE_AGENTS_AUTO_RELAUNCHED_AT"Xd8606765
AUTO_RELAUNCH_UNFOCUSED_MSYW6606763
AUTO_RELAUNCH_MIN_INTERVAL_MSumK606764
FLEETVIEW_HEARTBEAT.fleetview-heartbeatMoq104916
← 上一篇
/goal 命令实现剖析
下一篇 →
为浏览器自动化设计攻防策略:检测模型与分层控制平面

评论

评论发布后会立即公开,如触发规则可能被审核下架。

最多 1000 字。

    ⎇ main ● 0 errors · 0 warnings UTF-8 Markdown /zh/posts/claude-agents-bg-implementation/ © 2026 leeguoo