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

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

> 源码：`2.1.139-unbundled/extracted/src/entrypoints/cli.js`（构建版本 `208bf4b4`，2026-05-11）。
> 官方博客参考：[Agent view in Claude Code](https://claude.com/blog/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 客户端。

下面使用的命名约定：把被混淆过的符号重命名为有语义的名字，并在首次出现时附 `/* <原始名> */`。没有重命名的（`oqH`、`pT`、`Q7`、`B9`、`Q`、`yH`、`xH` 等）是宿主进程共享的辅助函数（状态文件 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<{ id: string; kind: "frame" | "pr" | ...; href: string }>;
  source?: "shell" | "slash" | "fleet" | "spare" | "respawn" | "left_arrow";

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

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

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

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

`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) => JobState` | 542886 处使用 | 用默认值构造一份新的 `JobState`。 |
| `pT` | `(jobDir, partial) => Promise<void>` | 542884 处使用 | 原子地 merge 写 `state.json`。 |
| `Q7` | `(jobDir) => Promise<JobState\|null>` | 542964 | 读 `state.json`。 |
| `W4` | `(short) => string` | 542879 | `~/.claude/bg-jobs/<short>/`。 |
| `rW` | `() => string` | 543233 | jobs 根目录；前缀查找用 `readdir` 扫这里。 |
| `k_` | `() => string` | 542814 | 当前进程的 session id（在 fork 中被设置）。 |
| `L7` | `() => boolean` | 543959 | `CLAUDE_JOB_DIR` 是否存在 → 即"我是否已经是后台 fork"。 |
| `getCurrentJobDir` / `Xv3` | `() => string \| undefined` | 544011 | 返回 `process.env.CLAUDE_JOB_DIR`。 |
| `V$` | `(req, opts?) => Promise<DaemonResp>` | 543031 | 与 daemon 的 Unix socket RPC（`op: "list" \| "dispatch"`）。 |
| `mW_` | `() => Promise<{ok,reason?}>` | 543277 | daemon 健康检查（`claude attach` 用）。 |
| `MQ` | `(short) => Promise<{outcome,msg?}>` | 543286 | "把我接到这个 job 上去" RPC。 |
| `PGK` | `(short, {alreadyInAlt}) => Promise<AttachResult>` | 606723 | attach 期间负责本地 PTY ↔ daemon 中 job 的双向桥接。 |
| `fW6` | `(jobId, {force?,knownState?,knownAlive?}) => Promise<RespawnResult>` | 606700 | 若 worker 已死则唤醒（Re·spawn）。 |
| `B9` | `(exitCode, reason, opts?) => Promise<never>` | 543885 | 前台进程的优雅退出。 |
| `aH7.setupGracefulShutdown` | — | 651511 | 把 SIGINT/SIGTERM 接入 `B9`。 |
| `cH.createRoot` / `Mu` | `(opts) => Promise<InkRoot>` | 651518 | 创建一个共用 stdout 的 Ink 根节点。 |
| `v3_` | `({exitOnCtrlC}) => Promise<InkRoot>` | 606748 | PTY 移交之后重建 Ink 根。 |
| `Q5.get(process.stdout)?.handoffAltScreen` | — | 606681 | 把 alt-screen 控制权交给 PTY 用于 attach。 |
| `Q5.get(process.stdout)?.unmount` | — | 606926 | 在 in-process 重新挂载 fleet view 之前先卸载 Ink。 |
| `Q("tengu_*")` / `yH` / `xH` / `j6` | 遥测 sink | 各处 | `tengu_bg_dispatch`、`tengu_background_fork`、`fleet_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` / `qxK` | bg-view UI 状态 | 604647 | 按 cwd 记忆"上次的搜索与折叠状态"。 |
| `s6()` | `() => {columns, rows}` | 604583 | 终端尺寸。 |
| `pA()` | `() => boolean` | 604578 | 终端当前是否获焦。 |
| `un_()` | alt-screen flag | 606680 | 是否拥有 alt-screen。 |
| `lT()` | `() => WorktreeInfo \| null` | 606956 | 当前 worktree 信息。 |
| `g$()` / `qp()` | name 查找 | 543840 | 用户设定 / 自动派生的 job 名。 |
| `WE(msg)` / `oI(msg)` | transcript 辅助 | 543828 | 抽取 assistant/user 消息正文。 |
| `viH(text)` | 元信息文本判定 | 543834 | 用来在挑选 intent 时跳过斜杠命令回显。 |
| `LD(state)` | `(JobState) => boolean` | 544019 | job 处于终止态。 |
| `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. 无参数的 `claude` 在 `defaultToAgentsView: 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
      ? { mode: "resume", sessionId: resumeId,
          fork: !resumingSelf && (isFork || forkFlag.length > 0),
          flagArgs: [...respawnFlags, ...(dashDash >= 0 ? argv.slice(dashDash) : [])] }
      : { 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 {
    intent: (firstUserText || "(backgrounded)").slice(0, 200),
    name:   userName ?? autoName,
    nameSource: userName ? "user" : autoName ? "auto" : undefined,
    detail: lastAssistant,
  };
}
```

### 7b. `BackgroundConfirmDialog` —— `Jv3`（543849 行）

* 通过 `J_` 读两个 AppState 字段：
  * `effortValue`（`Dv3`）
  * `taskCount`（`jv3` —— `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),
  });
  ```
* `forkCurrentSessionToBackground`（`pf6`，同模块）—— 以 `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`)、`pickIcon`、`jobLabel`、`childStatusColor` 等若干辅助。

### 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.needs` 与 `state.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 注册的 `agentsHandler`（`Cs3`）只在 fleet view **关闭**时生效；它输出的是按 source 分组的 subagent 定义列表。博客所说的 "agent view" 实际是 651485 行的 fast-path FleetView。
2. **`/bg` 是别名。** 真名是 `/background`（543999 行），`bg` 在 `aliases`。`immediate: (raw) => !raw.trim()` 表示纯输 `/bg<Enter>` 跳过 JSX 确认；带 prompt 文本则会卡在 React 树。
3. **`/bg` 与 `--bg` 走同一条 dispatch。** 唯一差异是：(a) 写入 `state.json` 的 `source` 字段不同；(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。** 它读的 `taskCount`（`jv3 = state => state.tasks`）来自 AppState 的 `tasks` map —— 也就是 bash/长跑工具 —— **不是** in-flight 请求。是两个不同概念，但 UI 文案只提到 tasks。
7. **左方向键退出只在输入为空时生效。** 实际触发的是多行输入 hook `v0` 的 `onCancel`；输入有内容时左键只是移动光标。
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-jsx`，`aliases: ["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)` 循环：渲染 → 等 `{type:"open"|"done"}` → 若 `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_fork`、`tengu_bg_dispatch`、`tengu_bg_dispatch_fallback`、`tengu_bg_dispatch_rescued`、`tengu_open_agents_via_left`、`tengu_fleetview`、`tengu_bg_agent_action`、`tengu_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` 配合 `bypassPermissions` 或 `auto` 权限模式需要先在交互式中确认一次 —— 不要绕过 543643-543646 行的检查，避免 headless dispatch 偷过 UX 关卡。

---

## 13. 符号映射速查表

### bg-cli 与斜杠命令模块（`QGK` / `gW_`）

| 重命名后 | 混淆名 | 行号 |
|---|---|---|
| `preSeedReplBgJob` | `wB8` | 542877 |
| `spawnBgSession` | `pHH` | 542905 |
| `spawnBgSessionAndDispatch` | `aN3` | 542926 |
| `markBgDispatchSucceeded` | `IGK` | 542802 |
| `markBgDispatchFellThrough` | `OB8` | 542820 |
| `handleBgFlag` | `sN3` | 543089 |
| `readBgStdin` | `pGK` | 543108 |
| `withStdinPositional` | `BGK` | 543129 |
| `humanizeDispatchFailureReason` | `tN3` | 543163 |
| `formatPostBackgroundHints` | `mf6` | 543177 |
| `bgVerbExtraArgsNote` | `UGK` | 543188 |
| `resolveJobByShortPrefix` | `jB8` | 543217 |
| `logsHandler` | `eN3` | 543246 |
| `attachHandler` | `Hv3` | 543271 |
| `stopHandler` | `qv3` | (export 见 542863) |
| `rmHandler` | `Kv3` | (export 见 542865) |
| `respawnHandler` | `_v3` | (export 见 542866) |
| `parseResumeTarget` | `FGK` | (export 见 542869) |
| `flagsWithoutPositional` | `gGK` | (export 见 542873) |
| `extractSeedFromTranscript` | `Bf6` | 543822 |
| `BackgroundConfirmDialog` | `Jv3` | 543849 |
| `handleBgSlashCommand` | `Yv3` | 543958 |
| `BG_CLI_FLAG_ALIASES` (`["--bg","--background"]`) | `oN3` | 543733 |
| `forkCurrentSessionToBackground` | `pf6` | (调用见 543875、606981) |
| `BG_SLASH_COMMAND_DEF` | `Mv3` / `fv3` | 543999 |
| `getCurrentJobDir` | `Xv3` | 544011 |
| `markSelfStoppedAndExit` | `Uf6` | 544013 |
| `stopSlashJsxCall` | `Pv3` | 544047 |
| `stopBridgeNonInteractiveCall` | `Wv3` | 544055 |

### FleetView 模块（`bd8` / `xd8`）

| 重命名后 | 混淆名 | 行号 |
|---|---|---|
| `FleetView`（组件） | `tmK` | 604467 |
| `mountFleetView` | `uQ3` | 606636 |
| `remountFleetViewInProcess` | `emK` | 606921 |
| `forkAndOpenFleetViewFromRepl` | `KpK` | 606947 |
| `PeekPane`（组件） | `WQ3` | 603580 |
| `renderJobLabel` | `ZW6` | 602868 |
| `formatJobAgeOrCountdown` | `vd8` | 602863 |
| `classifyJobActivity` | `jW6` | 602953 |
| `deriveStateBand` | `IvH` | 602971 |
| `repoGroupKey` | `Nd8` | 604497 |
| `parseDispatchInput` | `Zd8` | 603040 |
| `parseSearchQuery` | `BmK` | 603002 |
| `pruneMap` | `Pd8` | 602855 |
| `logFleetViewLifecycle("peek"\|"attach"\|"detach")` | `Rd8` | 603598 |
| `handleFleetViewKeyDown` | `wS` | 605589 |
| `swapAdjacentJobsInGroup` | `SY` | 605532 |
| `attachJobOrSurfaceError` | `YS` | 605454 |
| `runRowKeyAction` | `lj` | 605425 |

### CLI 入口与 Commander

| 重命名后 | 混淆名 | 行号 |
|---|---|---|
| `mainCliEntry` | `je3` | 651265 |
| `argsAreOnlyDebugFlags` | `caK` | 651245 |
| `agentsHandler`（fallback —— 列出 agent 定义，不是 jobs） | `Cs3` | 644428 |
| `AgentsList`（fallback 组件） | `FoK` | 644390 |
| `formatAgentDefinitionRow` | `BoK` | 644383 |
| `renderAgentSourceGroup` | `Es3` | 644416 |
| `isAgentsFleetEnabled` | `Xc` | 149497 |
| `fleetGateRejected` | （以同名 re-export） | 651424 |

### 配置 / 常量

| 重命名后 | 混淆名 | 行号 |
|---|---|---|
| `SETTING_DISABLE`（`"disableAgentView"`） | `y` | 53308 |
| `SETTING_DEFAULT_TO_FLEET`（`"defaultToAgentsView"`） | — | 462620 |
| `ENV_PRESELECT`（`"CLAUDE_AGENTS_SELECT"`） | — | 606639 |
| `ENV_AUTORELAUNCH`（`"CLAUDE_AGENTS_AUTO_RELAUNCHED_AT"`） | `Xd8` | 606765 |
| `AUTO_RELAUNCH_UNFOCUSED_MS` | `YW6` | 606763 |
| `AUTO_RELAUNCH_MIN_INTERVAL_MS` | `umK` | 606764 |
| `FLEETVIEW_HEARTBEAT`（`.fleetview-heartbeat`） | `Moq` | 104916 |
