scream-code 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.mjs +1240 -779
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -555,6 +555,7 @@ const ErrorCodes = {
555
555
  SESSION_THINKING_EMPTY: "session.thinking_empty",
556
556
  SESSION_MODEL_EMPTY: "session.model_empty",
557
557
  SESSION_PLAN_MODE_INVALID: "session.plan_mode_invalid",
558
+ SESSION_WOLFPACK_MODE_INVALID: "session.wolfpack_mode_invalid",
558
559
  SESSION_APPROVAL_HANDLER_ERROR: "session.approval_handler_error",
559
560
  SESSION_QUESTION_HANDLER_ERROR: "session.question_handler_error",
560
561
  SESSION_INIT_FAILED: "session.init_failed",
@@ -692,6 +693,12 @@ const SCREAM_ERROR_INFO = {
692
693
  public: true,
693
694
  action: "Provide a boolean plan mode."
694
695
  },
696
+ "session.wolfpack_mode_invalid": {
697
+ title: "Invalid wolfpack mode",
698
+ retryable: false,
699
+ public: true,
700
+ action: "Provide a boolean wolfpack mode."
701
+ },
695
702
  "session.approval_handler_error": {
696
703
  title: "Approval handler threw",
697
704
  retryable: false,
@@ -64622,6 +64629,141 @@ function escapeXml(input) {
64622
64629
  return input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
64623
64630
  }
64624
64631
  //#endregion
64632
+ //#region ../../packages/agent-core/src/tools/builtin/collaboration/wolfpack.md
64633
+ var wolfpack_default = "Use WolfPack to spawn multiple subagents in parallel for batch operations.\nThis is ideal when processing many independent items (files, checks, searches)\nthat all use the same subagent type and follow a similar pattern.\n\nInput:\n- description: Brief (3-5 word) task summary.\n- subagent_type: Subagent profile name. Defaults to \"coder\".\n- prompt_template: A prompt pattern where each item value is substituted in\n to produce a per-item prompt. See the parameter schema for placeholder syntax.\n- items: Array of item strings. Each item gets its own subagent (max 20).\n\nItems must be independent — no subagent depends on another's output.\nIf items depend on each other, use separate Agent calls instead.\n\nExample: review three source files for OWASP vulnerabilities by setting\nitems to the file paths and prompt_template to the review instruction.\n";
64634
+ //#endregion
64635
+ //#region ../../packages/agent-core/src/tools/builtin/collaboration/wolfpack.ts
64636
+ /**
64637
+ * WolfPackTool — batch parallel subagent execution.
64638
+ *
64639
+ * Spawns multiple subagents in parallel using a template + items pattern.
64640
+ * Each item gets its own subagent; results are batched together.
64641
+ * V1 uses Promise.allSettled — no concurrency control or rate-limit handling.
64642
+ */
64643
+ const MAX_ITEMS = 20;
64644
+ const WolfPackToolInputSchema = z.object({
64645
+ description: z.string().min(1).describe("Short task description (3-5 words, e.g., \"Security review all files\")"),
64646
+ subagent_type: z.string().default("coder").describe("Subagent type for all spawned agents (e.g., coder, explore, verify)"),
64647
+ prompt_template: z.string().min(1).describe("Prompt template with {{item}} placeholder. Each item is substituted in."),
64648
+ items: z.array(z.string().min(1)).min(1).max(MAX_ITEMS).describe("Array of items to process. Each item gets its own subagent.")
64649
+ });
64650
+ var WolfPackTool = class {
64651
+ subagentHost;
64652
+ isEnabled;
64653
+ name = "WolfPack";
64654
+ description = wolfpack_default;
64655
+ parameters = toInputJsonSchema(WolfPackToolInputSchema);
64656
+ constructor(subagentHost, isEnabled, _options) {
64657
+ this.subagentHost = subagentHost;
64658
+ this.isEnabled = isEnabled;
64659
+ }
64660
+ resolveExecution(args) {
64661
+ return {
64662
+ description: `WolfPack: ${args.description} (${args.items.length} agents)`,
64663
+ accesses: ToolAccesses.none(),
64664
+ display: {
64665
+ kind: "generic",
64666
+ summary: `WolfPack: ${args.description}`,
64667
+ detail: {
64668
+ itemCount: args.items.length,
64669
+ subagent_type: args.subagent_type
64670
+ }
64671
+ },
64672
+ approvalRule: this.name,
64673
+ execute: (ctx) => this.execution(args, ctx)
64674
+ };
64675
+ }
64676
+ async execution(args, ctx) {
64677
+ ctx.signal.throwIfAborted();
64678
+ if (!this.isEnabled()) return {
64679
+ output: "WolfPack 模式未开启。请输入 /wolfpack 打开后再试。",
64680
+ isError: true
64681
+ };
64682
+ if (args.items.length > MAX_ITEMS) return {
64683
+ output: `WolfPack max ${MAX_ITEMS} items. Got ${args.items.length}.`,
64684
+ isError: true
64685
+ };
64686
+ const profileName = args.subagent_type ?? "coder";
64687
+ const template = args.prompt_template;
64688
+ const handlePromises = args.items.map(async (item) => {
64689
+ ctx.signal.throwIfAborted();
64690
+ const prompt = template.replace(/\{\{item\}\}/g, item);
64691
+ return {
64692
+ item,
64693
+ handle: await this.subagentHost.spawn(profileName, {
64694
+ parentToolCallId: ctx.toolCallId,
64695
+ prompt,
64696
+ description: `${args.description}: ${item}`,
64697
+ runInBackground: false,
64698
+ signal: ctx.signal
64699
+ })
64700
+ };
64701
+ });
64702
+ const completionPromises = (await Promise.allSettled(handlePromises)).map(async (settled) => {
64703
+ if (settled.status === "rejected") return {
64704
+ item: "unknown",
64705
+ result: `Spawn failed: ${settled.reason instanceof Error ? settled.reason.message : String(settled.reason)}`,
64706
+ success: false
64707
+ };
64708
+ const { item, handle } = settled.value;
64709
+ try {
64710
+ return {
64711
+ item,
64712
+ result: (await handle.completion).result,
64713
+ success: true,
64714
+ agentId: handle.agentId
64715
+ };
64716
+ } catch (error) {
64717
+ let message;
64718
+ if (isAbortError$3(error)) message = "The subagent was stopped before it finished.";
64719
+ else message = error instanceof Error ? error.message : String(error);
64720
+ return {
64721
+ item,
64722
+ result: message,
64723
+ success: false,
64724
+ agentId: handle.agentId
64725
+ };
64726
+ }
64727
+ });
64728
+ const completions = await Promise.allSettled(completionPromises);
64729
+ let successCount = 0;
64730
+ let failureCount = 0;
64731
+ const lines = [];
64732
+ for (const settled of completions) {
64733
+ if (settled.status === "fulfilled") {
64734
+ const { item, result: _result, success, agentId } = settled.value;
64735
+ if (success) {
64736
+ successCount++;
64737
+ lines.push(`### ${item} (OK)`);
64738
+ } else {
64739
+ failureCount++;
64740
+ lines.push(`### ${item} (FAILED)`);
64741
+ }
64742
+ if (agentId !== void 0) lines.push(`agent_id: ${agentId}`);
64743
+ } else {
64744
+ failureCount++;
64745
+ const msg = settled.reason instanceof Error ? settled.reason.message : String(settled.reason);
64746
+ lines.push(`### error: ${msg}`);
64747
+ }
64748
+ lines.push("");
64749
+ }
64750
+ const summary = `Success: ${successCount}, Failed: ${failureCount}, Total: ${completions.length}`;
64751
+ if (failureCount > 0 && successCount === 0) return {
64752
+ output: [
64753
+ summary,
64754
+ "",
64755
+ ...lines
64756
+ ].join("\n"),
64757
+ isError: true
64758
+ };
64759
+ return { output: [
64760
+ summary,
64761
+ "",
64762
+ ...lines
64763
+ ].join("\n") };
64764
+ }
64765
+ };
64766
+ //#endregion
64625
64767
  //#region ../../packages/agent-core/src/tools/builtin/file/line-endings.ts
64626
64768
  function detectLineEndingStyle(text) {
64627
64769
  let hasCrLf = false;
@@ -72470,7 +72612,9 @@ var MemoryMemoStore = class {
72470
72612
  };
72471
72613
  //#endregion
72472
72614
  //#region ../../packages/memory/src/extractor.ts
72473
- /** Parse memory-memo blocks from LLM compaction output. */
72615
+ /**
72616
+ * Parse memory-memo blocks from LLM compaction output.
72617
+ */
72474
72618
  function parseMemoryMemos(text) {
72475
72619
  const memos = [];
72476
72620
  const regex = /```memory-memo[\s\S]*?\n([\s\S]*?)```/g;
@@ -72830,6 +72974,12 @@ var DreamTracker = class {
72830
72974
  /** Max consecutive compaction failures before auto-compaction is
72831
72975
  * disabled for the remainder of the turn. Resets each turn. */
72832
72976
  const MAX_CONSECUTIVE_FAILURES = 3;
72977
+ /** Minimal system prompt used during compaction. The full agent system
72978
+ * prompt contains tool descriptions and runtime injections that contradict
72979
+ * the compaction instruction ("DO NOT CALL ANY TOOLS"). This compact prompt
72980
+ * keeps the LLM focused and explicitly references the memory-memo extraction
72981
+ * section inside compaction-instruction.md. */
72982
+ const COMPACTION_SYSTEM_PROMPT = "You are a conversation context compaction assistant. Your job is to summarize the conversation above into a structured summary. Output text only. DO NOT CALL ANY TOOLS. Follow the compaction instruction in the last user message exactly. Pay special attention to the Memory Memo Extraction section — you MUST output memory-memo blocks for every completed task loop.";
72833
72983
  var FullCompaction = class {
72834
72984
  agent;
72835
72985
  compactionCountInTurn = 0;
@@ -72998,7 +73148,7 @@ var FullCompaction = class {
72998
73148
  }];
72999
73149
  class TruncatedError extends Error {}
73000
73150
  try {
73001
- const response = await this.agent.generate(this.agent.config.provider, this.agent.config.systemPrompt, [], messages, void 0, { signal });
73151
+ const response = await this.agent.generate(this.agent.config.provider, COMPACTION_SYSTEM_PROMPT, [], messages, void 0, { signal });
73002
73152
  if (response.finishReason === "truncated") throw new TruncatedError();
73003
73153
  usage = response.usage;
73004
73154
  summary = extractCompactionSummary(response);
@@ -73114,16 +73264,15 @@ var FullCompaction = class {
73114
73264
  if (memos.length === 0) return;
73115
73265
  const sessionId = this.agent.homedir ? basename$1(dirname$2(dirname$2(this.agent.homedir))) : "unknown";
73116
73266
  const sessionTitle = await this.agent.getSessionTitle();
73117
- for (const memo of memos) {
73267
+ const failed = (await Promise.allSettled(memos.map((memo) => {
73118
73268
  memo.sourceSessionId = sessionId;
73119
73269
  memo.sourceSessionTitle = sessionTitle ?? "";
73120
- memoStore.append(memo).catch((error) => {
73121
- this.agent.log.warn("Failed to store memory memo from compaction", {
73122
- memoId: memo.id,
73123
- error: String(error)
73124
- });
73125
- });
73126
- }
73270
+ return memoStore.append(memo);
73271
+ }))).filter((r) => r.status === "rejected").length;
73272
+ if (failed > 0) this.agent.log.warn("Some memory memos failed to store from compaction", {
73273
+ failed,
73274
+ total: memos.length
73275
+ });
73127
73276
  this.agent.log.info("Extracted memory memos from compaction", {
73128
73277
  count: memos.length,
73129
73278
  sessionId
@@ -75378,6 +75527,46 @@ function isTodoStatus(value) {
75378
75527
  return value === "pending" || value === "in_progress" || value === "done";
75379
75528
  }
75380
75529
  //#endregion
75530
+ //#region ../../packages/agent-core/src/agent/injection/wolfpack.ts
75531
+ const WOLFPACK_MODE_ENTER_REMINDER = [
75532
+ "WolfPack mode is active. Prefer using the WolfPack tool for batch parallel execution.",
75533
+ "",
75534
+ "When to use: When the user's task involves performing the same operation across",
75535
+ "multiple independent items (files, directories, data entries, searches).",
75536
+ "",
75537
+ "How to use:",
75538
+ " WolfPack(",
75539
+ " description=\"Short task description (3-5 words)\",",
75540
+ " subagent_type=\"coder\",",
75541
+ " prompt_template=\"Review {{item}} for security issues\",",
75542
+ " items=[\"src/auth.ts\", \"src/api.ts\", \"src/config.ts\"]",
75543
+ " )",
75544
+ "",
75545
+ "This spawns one subagent per item in parallel. Results are batched and returned together.",
75546
+ "Items must be independent — do not use WolfPack when one item depends on another's output.",
75547
+ "Max 20 items per call."
75548
+ ].join("\n");
75549
+ const WOLFPACK_MODE_EXIT_REMINDER = "WolfPack mode is no longer active. The WolfPack tool for batch parallel execution is no longer available.";
75550
+ var WolfPackModeInjector = class extends DynamicInjector {
75551
+ injectionVariant = "wolfpack";
75552
+ wasActive = false;
75553
+ getInjection() {
75554
+ if (!this.agent.wolfpackMode.isActive) {
75555
+ if (this.wasActive) {
75556
+ this.wasActive = false;
75557
+ this.injectedAt = null;
75558
+ return WOLFPACK_MODE_EXIT_REMINDER;
75559
+ }
75560
+ return;
75561
+ }
75562
+ if (!this.wasActive) {
75563
+ this.injectedAt = null;
75564
+ this.wasActive = true;
75565
+ return WOLFPACK_MODE_ENTER_REMINDER;
75566
+ }
75567
+ }
75568
+ };
75569
+ //#endregion
75381
75570
  //#region ../../packages/agent-core/src/agent/injection/manager.ts
75382
75571
  var InjectionManager = class {
75383
75572
  agent;
@@ -75389,6 +75578,7 @@ var InjectionManager = class {
75389
75578
  this.memoryRecall = autoRecallEnabled ? new MemoryRecallInjector(agent) : null;
75390
75579
  this.injectors = [
75391
75580
  new PluginSessionStartInjector(agent),
75581
+ new WolfPackModeInjector(agent),
75392
75582
  new PlanModeInjector(agent),
75393
75583
  new PermissionModeInjector(agent),
75394
75584
  new TodoListReminderInjector(agent),
@@ -75466,7 +75656,8 @@ const DEFAULT_APPROVE_TOOLS = new Set([
75466
75656
  "FetchURL",
75467
75657
  "Agent",
75468
75658
  "AskUserQuestion",
75469
- "Skill"
75659
+ "Skill",
75660
+ "WolfPack"
75470
75661
  ]);
75471
75662
  var DefaultToolApprovePermissionPolicy = class {
75472
75663
  name = "default-tool-approve";
@@ -76045,6 +76236,19 @@ var YoloModeApprovePermissionPolicy = class {
76045
76236
  }
76046
76237
  };
76047
76238
  //#endregion
76239
+ //#region ../../packages/agent-core/src/agent/permission/policies/wolfpack-mode-approve.ts
76240
+ var WolfPackModeApprovePermissionPolicy = class {
76241
+ agent;
76242
+ name = "wolfpack-mode-approve";
76243
+ constructor(agent) {
76244
+ this.agent = agent;
76245
+ }
76246
+ evaluate() {
76247
+ if (!this.agent.wolfpackMode?.isActive) return;
76248
+ return { kind: "approve" };
76249
+ }
76250
+ };
76251
+ //#endregion
76048
76252
  //#region ../../packages/agent-core/src/agent/permission/policies/index.ts
76049
76253
  /** Permission policies run in order; the first non-undefined result wins. */
76050
76254
  function createPermissionDecisionPolicies(agent) {
@@ -76063,6 +76267,7 @@ function createPermissionDecisionPolicies(agent) {
76063
76267
  new GitControlPathAccessAskPermissionPolicy(agent),
76064
76268
  new CwdOutsideFileWriteAskPermissionPolicy(agent),
76065
76269
  new YoloModeApprovePermissionPolicy(agent),
76270
+ new WolfPackModeApprovePermissionPolicy(agent),
76066
76271
  new DefaultToolApprovePermissionPolicy(),
76067
76272
  new GitCwdWriteApprovePermissionPolicy(agent),
76068
76273
  new FallbackAskPermissionPolicy()
@@ -76677,6 +76882,33 @@ function isMissingFileError(error) {
76677
76882
  return error.code === "ENOENT";
76678
76883
  }
76679
76884
  //#endregion
76885
+ //#region ../../packages/agent-core/src/agent/wolfpack/index.ts
76886
+ var WolfPackMode = class {
76887
+ agent;
76888
+ _isActive = false;
76889
+ constructor(agent) {
76890
+ this.agent = agent;
76891
+ }
76892
+ enter() {
76893
+ if (this._isActive) return;
76894
+ this._isActive = true;
76895
+ this.agent.records.logRecord({ type: "wolfpack.enter" });
76896
+ this.agent.emitStatusUpdated();
76897
+ }
76898
+ restoreEnter() {
76899
+ this._isActive = true;
76900
+ }
76901
+ exit() {
76902
+ if (!this._isActive) return;
76903
+ this.agent.records.logRecord({ type: "wolfpack.exit" });
76904
+ this._isActive = false;
76905
+ this.agent.emitStatusUpdated();
76906
+ }
76907
+ get isActive() {
76908
+ return this._isActive;
76909
+ }
76910
+ };
76911
+ //#endregion
76680
76912
  //#region ../../packages/agent-core/src/agent/session-memory.ts
76681
76913
  const MAX_EVENTS = 50;
76682
76914
  const MAX_SUMMARY_LENGTH = 1500;
@@ -77201,6 +77433,12 @@ function restoreAgentRecord(agent, input) {
77201
77433
  case "plan_mode.exit":
77202
77434
  agent.planMode.exit(input.id);
77203
77435
  return;
77436
+ case "wolfpack.enter":
77437
+ agent.wolfpackMode.restoreEnter();
77438
+ return;
77439
+ case "wolfpack.exit":
77440
+ agent.wolfpackMode.exit();
77441
+ return;
77204
77442
  case "context.append_message":
77205
77443
  agent.context.appendMessage(input.message);
77206
77444
  return;
@@ -89772,7 +90010,7 @@ function normalizeSourcePath(path) {
89772
90010
  }
89773
90011
  //#endregion
89774
90012
  //#region ../../packages/agent-core/src/profile/default/agent.yaml
89775
- var agent_default = "name: agent\ndescription: Default Scream Code agent\n\nsystemPromptPath: ./system.md\npromptVars:\n roleAdditional: ''\n\ntools:\n - Read\n - Write\n - Edit\n - Grep\n - Glob\n - Bash\n - TaskList\n - TaskOutput\n - TaskStop\n - CronCreate\n - CronList\n - CronDelete\n - ReadMediaFile\n - TodoList\n - Skill\n - WebSearch\n - Agent\n\n - FetchURL\n - AskUserQuestion\n - EnterPlanMode\n - ExitPlanMode\n - mcp__*\n\nsubagents:\n coder:\n description: Good at general software engineering tasks.\n explore:\n description: Fast codebase exploration with prompt-enforced read-only behavior.\n plan:\n description: Read-only implementation planning and architecture design.\n verify:\n description: Verification specialist. Runs build, test, and lint commands to validate code changes.\n writer:\n description: Content production and research specialist. Produces structured, data-driven reports, analyses, and Markdown documents.\n";
90013
+ var agent_default = "name: agent\ndescription: Default Scream Code agent\n\nsystemPromptPath: ./system.md\npromptVars:\n roleAdditional: ''\n\ntools:\n - Read\n - Write\n - Edit\n - Grep\n - Glob\n - Bash\n - TaskList\n - TaskOutput\n - TaskStop\n - CronCreate\n - CronList\n - CronDelete\n - ReadMediaFile\n - TodoList\n - Skill\n - WebSearch\n - Agent\n - WolfPack\n\n - FetchURL\n - AskUserQuestion\n - EnterPlanMode\n - ExitPlanMode\n - mcp__*\n\nsubagents:\n coder:\n description: Good at general software engineering tasks.\n explore:\n description: Fast codebase exploration with prompt-enforced read-only behavior.\n plan:\n description: Read-only implementation planning and architecture design.\n verify:\n description: Verification specialist. Runs build, test, and lint commands to validate code changes.\n writer:\n description: Content production and research specialist. Produces structured, data-driven reports, analyses, and Markdown documents.\n";
89776
90014
  //#endregion
89777
90015
  //#region ../../packages/agent-core/src/profile/default/coder.yaml
89778
90016
  var coder_default = "extends: agent\nname: coder\npromptVars:\n roleAdditional: |\n You are now running as a subagent. All the `user` messages are sent by the main agent. The main agent cannot see your context, it can only see your last message when you finish the task. You must treat the parent agent as your caller. Do not directly ask the end user questions. If something is unclear, explain the ambiguity in your final summary to the parent agent.\nwhenToUse: |\n Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent.\ntools:\n - Bash\n - Read\n - ReadMediaFile\n - Glob\n - Grep\n - Write\n - Edit\n - WebSearch\n - FetchURL\n - mcp__*\n";
@@ -90086,6 +90324,7 @@ var ToolManager = class {
90086
90324
  allowBackground,
90087
90325
  log: this.agent.log
90088
90326
  }),
90327
+ this.agent.subagentHost && new WolfPackTool(this.agent.subagentHost, () => this.agent.wolfpackMode.isActive, { log: this.agent.log }),
90089
90328
  toolServices?.webSearcher && new WebSearchTool(toolServices.webSearcher),
90090
90329
  toolServices?.urlFetcher && new FetchURLTool(toolServices.urlFetcher)
90091
90330
  ].filter((tool) => !!tool).map((tool) => [tool.name, tool]));
@@ -91272,6 +91511,7 @@ var Agent = class {
91272
91511
  injection;
91273
91512
  permission;
91274
91513
  planMode;
91514
+ wolfpackMode;
91275
91515
  usage;
91276
91516
  skills;
91277
91517
  tools;
@@ -91310,8 +91550,9 @@ var Agent = class {
91310
91550
  this.config = new ConfigState(this);
91311
91551
  this.turn = new TurnFlow(this);
91312
91552
  this.injection = new InjectionManager(this);
91313
- this.permission = new PermissionManager(this, options.permission);
91314
91553
  this.planMode = new PlanMode(this);
91554
+ this.wolfpackMode = new WolfPackMode(this);
91555
+ this.permission = new PermissionManager(this, options.permission);
91315
91556
  this.usage = new UsageRecorder(this);
91316
91557
  this.skills = options.skills ? new SkillManager(this, options.skills) : null;
91317
91558
  this.tools = new ToolManager(this);
@@ -91447,6 +91688,12 @@ var Agent = class {
91447
91688
  enterPlan: async () => {
91448
91689
  await this.planMode.enter();
91449
91690
  },
91691
+ enterWolfpack: () => {
91692
+ this.wolfpackMode.enter();
91693
+ },
91694
+ exitWolfpack: () => {
91695
+ this.wolfpackMode.exit();
91696
+ },
91450
91697
  cancelPlan: (payload) => {
91451
91698
  this.planMode.cancel(payload.id);
91452
91699
  },
@@ -91534,17 +91781,17 @@ var Agent = class {
91534
91781
  }]);
91535
91782
  const memos = parseMemoryMemos(typeof response.message.content === "string" ? response.message.content : response.message.content.map((p) => p.type === "text" ? p.text : "").join(""));
91536
91783
  if (memos.length === 0) return;
91537
- for (const memo of memos) {
91784
+ const store = this.memoStore;
91785
+ const failed = (await Promise.allSettled(memos.map((memo) => {
91538
91786
  memo.sourceSessionId = sessionId;
91539
91787
  memo.sourceSessionTitle = sessionTitle ?? "";
91540
91788
  memo.extractionSource = "exit";
91541
- this.memoStore.append(memo).catch((error) => {
91542
- this.log.warn("Failed to store memory memo from exit extraction", {
91543
- memoId: memo.id,
91544
- error: String(error)
91545
- });
91546
- });
91547
- }
91789
+ return store.append(memo);
91790
+ }))).filter((r) => r.status === "rejected").length;
91791
+ if (failed > 0) this.log.warn("Some memory memos failed to store from exit extraction", {
91792
+ failed,
91793
+ total: memos.length
91794
+ });
91548
91795
  this.log.info("Extracted memory memos on session exit", {
91549
91796
  count: memos.length,
91550
91797
  sessionId
@@ -111070,6 +111317,11 @@ const FLAG_DEFINITIONS = [{
111070
111317
  env: "SCREAM_CODE_EXPERIMENTAL_MICRO_COMPACTION",
111071
111318
  default: false,
111072
111319
  surface: "both"
111320
+ }, {
111321
+ id: "wolfpack",
111322
+ env: "SCREAM_CODE_EXPERIMENTAL_WOLFPACK",
111323
+ default: false,
111324
+ surface: "both"
111073
111325
  }];
111074
111326
  /**
111075
111327
  * Pure, synchronous flag resolver. State comes entirely from (env, registry) and nothing is
@@ -112446,6 +112698,12 @@ var SessionAPIImpl = class {
112446
112698
  clearPlan({ agentId, ...payload }) {
112447
112699
  return this.getAgent(agentId).clearPlan(payload);
112448
112700
  }
112701
+ enterWolfpack({ agentId, ...payload }) {
112702
+ return this.getAgent(agentId).enterWolfpack(payload);
112703
+ }
112704
+ exitWolfpack({ agentId, ...payload }) {
112705
+ return this.getAgent(agentId).exitWolfpack(payload);
112706
+ }
112449
112707
  beginCompaction({ agentId, ...payload }) {
112450
112708
  return this.getAgent(agentId).beginCompaction(payload);
112451
112709
  }
@@ -114072,6 +114330,12 @@ var ScreamCore = class {
114072
114330
  clearPlan({ sessionId, ...payload }) {
114073
114331
  return this.sessionApi(sessionId).clearPlan(payload);
114074
114332
  }
114333
+ enterWolfpack({ sessionId, ...payload }) {
114334
+ return this.sessionApi(sessionId).enterWolfpack(payload);
114335
+ }
114336
+ exitWolfpack({ sessionId, ...payload }) {
114337
+ return this.sessionApi(sessionId).exitWolfpack(payload);
114338
+ }
114075
114339
  beginCompaction({ sessionId, ...payload }) {
114076
114340
  return this.sessionApi(sessionId).beginCompaction(payload);
114077
114341
  }
@@ -114650,6 +114914,17 @@ var SDKRpcClient = class {
114650
114914
  agentId: this.interactiveAgentId
114651
114915
  });
114652
114916
  }
114917
+ async setWolfpackMode(input) {
114918
+ const rpc = await this.getRpc();
114919
+ if (!input.enabled) return rpc.exitWolfpack({
114920
+ sessionId: input.sessionId,
114921
+ agentId: this.interactiveAgentId
114922
+ });
114923
+ return rpc.enterWolfpack({
114924
+ sessionId: input.sessionId,
114925
+ agentId: this.interactiveAgentId
114926
+ });
114927
+ }
114653
114928
  async getPlan(input) {
114654
114929
  return (await this.getRpc()).getPlan({
114655
114930
  sessionId: input.sessionId,
@@ -115046,6 +115321,14 @@ var Session = class {
115046
115321
  enabled
115047
115322
  });
115048
115323
  }
115324
+ async setWolfpackMode(enabled) {
115325
+ this.ensureOpen();
115326
+ if (typeof enabled !== "boolean") throw new ScreamError(ErrorCodes.SESSION_WOLFPACK_MODE_INVALID, "Session wolfpack mode must be a boolean");
115327
+ await this.rpc.setWolfpackMode({
115328
+ sessionId: this.id,
115329
+ enabled
115330
+ });
115331
+ }
115049
115332
  async getPlan() {
115050
115333
  this.ensureOpen();
115051
115334
  return this.rpc.getPlan({ sessionId: this.id });
@@ -115245,10 +115528,10 @@ var Session = class {
115245
115528
  }
115246
115529
  async close() {
115247
115530
  if (this.closed) return;
115248
- this.closed = true;
115249
115531
  try {
115250
115532
  await Promise.race([this.extractMemoriesOnExit(), new Promise((resolve) => setTimeout(resolve, 3e4))]).catch(() => {});
115251
115533
  } catch {}
115534
+ this.closed = true;
115252
115535
  try {
115253
115536
  await this.rpc.closeSession({ sessionId: this.id });
115254
115537
  } finally {
@@ -117596,75 +117879,82 @@ const BUILTIN_SLASH_COMMANDS = [
117596
117879
  availability: "always"
117597
117880
  },
117598
117881
  {
117599
- name: "power",
117600
- aliases: ["parallel"],
117601
- description: "切换至Agent并发模式",
117882
+ name: "yes",
117883
+ aliases: ["yolo"],
117884
+ description: "切换至自动批准模式(yolo)",
117602
117885
  priority: 124,
117603
117886
  availability: "always"
117604
117887
  },
117605
117888
  {
117606
- name: "yes",
117607
- aliases: ["yolo"],
117608
- description: "切换至自动批准模式(yolo)",
117889
+ name: "wolfpack",
117890
+ aliases: ["wp"],
117891
+ description: "切换群狼协作模式,自动批准+批量并发",
117609
117892
  priority: 123,
117610
117893
  availability: "always"
117611
117894
  },
117612
117895
  {
117613
- name: "model",
117614
- aliases: [],
117615
- description: "切换 LLM 模型",
117896
+ name: "sessions",
117897
+ aliases: ["resume"],
117898
+ description: "浏览并恢复会话",
117616
117899
  priority: 122
117617
117900
  },
117618
- {
117619
- name: "new",
117620
- aliases: ["clear"],
117621
- description: "在当前工作区开启新会话",
117622
- priority: 121
117623
- },
117624
117901
  {
117625
117902
  name: "memory",
117626
117903
  aliases: ["memo", "mem"],
117627
117904
  description: "浏览、搜索、注入记忆备忘录",
117628
- priority: 120,
117905
+ priority: 121,
117629
117906
  availability: "always"
117630
117907
  },
117631
117908
  {
117632
- name: "help",
117633
- aliases: ["h", "?"],
117634
- description: "显示可用命令和快捷键",
117635
- priority: 119,
117636
- availability: "always"
117909
+ name: "new",
117910
+ aliases: ["clear"],
117911
+ description: "在当前工作区开启新会话",
117912
+ priority: 121
117913
+ },
117914
+ {
117915
+ name: "model",
117916
+ aliases: [],
117917
+ description: "切换 LLM 模型",
117918
+ priority: 120
117637
117919
  },
117638
117920
  {
117639
117921
  name: "compact",
117640
117922
  aliases: [],
117641
117923
  description: "压缩对话上下文",
117642
- priority: 118
117924
+ priority: 119
117643
117925
  },
117644
117926
  {
117645
117927
  name: "plan",
117646
117928
  aliases: [],
117647
117929
  description: "切换计划模式",
117648
- priority: 117,
117930
+ priority: 118,
117649
117931
  availability: (args) => args.trim().toLowerCase() === "clear" ? "idle-only" : "always"
117650
117932
  },
117651
- {
117652
- name: "sessions",
117653
- aliases: ["resume"],
117654
- description: "浏览并恢复会话",
117655
- priority: 116
117656
- },
117657
117933
  {
117658
117934
  name: "tasks",
117659
117935
  aliases: ["task"],
117660
117936
  description: "浏览后台任务",
117937
+ priority: 117,
117938
+ availability: "always"
117939
+ },
117940
+ {
117941
+ name: "help",
117942
+ aliases: ["h", "?"],
117943
+ description: "显示可用命令和快捷键",
117944
+ priority: 116,
117945
+ availability: "always"
117946
+ },
117947
+ {
117948
+ name: "status",
117949
+ aliases: [],
117950
+ description: "显示当前会话和运行时状态",
117661
117951
  priority: 115,
117662
117952
  availability: "always"
117663
117953
  },
117664
117954
  {
117665
- name: "mcp",
117955
+ name: "usage",
117666
117956
  aliases: [],
117667
- description: "管理 MCP 服务器(安装/停用/卸载)",
117957
+ description: "显示 token 用量和上下文窗口",
117668
117958
  priority: 114,
117669
117959
  availability: "always"
117670
117960
  },
@@ -117676,58 +117966,45 @@ const BUILTIN_SLASH_COMMANDS = [
117676
117966
  availability: "always"
117677
117967
  },
117678
117968
  {
117679
- name: "theme",
117969
+ name: "mcp",
117680
117970
  aliases: [],
117681
- description: "设置终端 UI 主题",
117971
+ description: "管理 MCP 服务器(安装/停用/卸载)",
117682
117972
  priority: 112,
117683
117973
  availability: "always"
117684
117974
  },
117685
117975
  {
117686
- name: "editor",
117687
- aliases: [],
117688
- description: "设置外部编辑器",
117976
+ name: "plugin",
117977
+ aliases: ["plugins"],
117978
+ description: "ScreamCode 插件中心:浏览、安装、卸载插件",
117689
117979
  priority: 111,
117690
117980
  availability: "always"
117691
117981
  },
117692
117982
  {
117693
- name: "status",
117983
+ name: "cc",
117694
117984
  aliases: [],
117695
- description: "显示当前会话和运行时状态",
117985
+ description: "操控你的cc(启动/关闭/重启)",
117696
117986
  priority: 110,
117697
117987
  availability: "always"
117698
117988
  },
117699
117989
  {
117700
- name: "usage",
117990
+ name: "cc-connect",
117701
117991
  aliases: [],
117702
- description: "显示 token 用量和上下文窗口",
117992
+ description: "cc-connect 快速通道配置(需先安装)",
117703
117993
  priority: 109,
117704
117994
  availability: "always"
117705
117995
  },
117706
- {
117707
- name: "permission",
117708
- aliases: [],
117709
- description: "选择权限模式",
117710
- priority: 108,
117711
- availability: "always"
117712
- },
117713
117996
  {
117714
117997
  name: "revoke",
117715
117998
  aliases: [],
117716
117999
  description: "撤回上一次对话(可指定轮数,如 /revoke 3)",
117717
- priority: 107,
118000
+ priority: 108,
117718
118001
  availability: "idle-only"
117719
118002
  },
117720
- {
117721
- name: "config",
117722
- aliases: [],
117723
- description: "浏览并配置模型(远程拉取最新目录)",
117724
- priority: 106
117725
- },
117726
118003
  {
117727
118004
  name: "goal",
117728
118005
  aliases: [],
117729
118006
  description: "管理自动目标(status状态/pause暂停/resume恢复/replace替换,取消用 /goaloff)",
117730
- priority: 105,
118007
+ priority: 107,
117731
118008
  availability: (args) => {
117732
118009
  const trimmed = args.trim();
117733
118010
  return trimmed === "" || trimmed === "status" || trimmed === "pause" ? "always" : "idle-only";
@@ -117737,88 +118014,94 @@ const BUILTIN_SLASH_COMMANDS = [
117737
118014
  name: "goaloff",
117738
118015
  aliases: [],
117739
118016
  description: "取消并清空当前目标",
117740
- priority: 104,
117741
- availability: "always"
117742
- },
117743
- {
117744
- name: "settings",
117745
- aliases: [],
117746
- description: "打开 TUI 设置",
117747
- priority: 103,
118017
+ priority: 106,
117748
118018
  availability: "always"
117749
118019
  },
117750
118020
  {
117751
118021
  name: "fork",
117752
118022
  aliases: [],
117753
118023
  description: "复制当前会话并新开分支",
117754
- priority: 102
118024
+ priority: 105
117755
118025
  },
117756
118026
  {
117757
118027
  name: "title",
117758
118028
  aliases: ["rename"],
117759
118029
  description: "设置或显示会话标题",
117760
- priority: 101,
118030
+ priority: 104,
117761
118031
  availability: "always"
117762
118032
  },
117763
118033
  {
117764
- name: "init",
118034
+ name: "config",
117765
118035
  aliases: [],
117766
- description: "分析代码库并生成 AGENTS.md",
117767
- priority: 100
118036
+ description: "浏览并配置模型(远程拉取最新目录)",
118037
+ priority: 103
117768
118038
  },
117769
118039
  {
117770
- name: "cc",
118040
+ name: "permission",
117771
118041
  aliases: [],
117772
- description: "操控你的cc(启动/关闭/重启)",
117773
- priority: 98,
118042
+ description: "选择权限模式",
118043
+ priority: 102,
117774
118044
  availability: "always"
117775
118045
  },
117776
118046
  {
117777
- name: "cc-connect",
118047
+ name: "theme",
117778
118048
  aliases: [],
117779
- description: "cc-connect 快速通道配置(需先安装)",
117780
- priority: 96,
118049
+ description: "设置终端 UI 主题",
118050
+ priority: 101,
117781
118051
  availability: "always"
117782
118052
  },
117783
118053
  {
117784
- name: "export-md",
117785
- aliases: ["export"],
117786
- description: "导出当前会话为 Markdown",
117787
- priority: 95
118054
+ name: "editor",
118055
+ aliases: [],
118056
+ description: "设置外部编辑器",
118057
+ priority: 100,
118058
+ availability: "always"
117788
118059
  },
117789
118060
  {
117790
- name: "update",
118061
+ name: "settings",
117791
118062
  aliases: [],
117792
- description: "手动更新 Scream Code 到最新版本",
117793
- priority: 94,
117794
- availability: "idle-only"
118063
+ description: "打开 TUI 设置",
118064
+ priority: 99,
118065
+ availability: "always"
117795
118066
  },
117796
118067
  {
117797
- name: "plugin",
117798
- aliases: ["plugins"],
117799
- description: "ScreamCode 插件中心:浏览、安装、卸载插件",
117800
- priority: 93,
117801
- availability: "always"
118068
+ name: "init",
118069
+ aliases: [],
118070
+ description: "分析代码库并生成 AGENTS.md",
118071
+ priority: 98
118072
+ },
118073
+ {
118074
+ name: "export-md",
118075
+ aliases: ["export"],
118076
+ description: "导出当前会话为 Markdown",
118077
+ priority: 97
117802
118078
  },
117803
118079
  {
117804
118080
  name: "export-debug-zip",
117805
118081
  aliases: [],
117806
118082
  description: "导出当前会话为调试 ZIP 存档",
117807
- priority: 92
118083
+ priority: 96
117808
118084
  },
117809
118085
  {
117810
- name: "logout",
117811
- aliases: ["disconnect"],
117812
- description: "删除已配置的模型",
117813
- priority: 91
118086
+ name: "update",
118087
+ aliases: [],
118088
+ description: "手动更新 Scream Code 到最新版本",
118089
+ priority: 95,
118090
+ availability: "idle-only"
117814
118091
  },
117815
118092
  {
117816
118093
  name: "version",
117817
118094
  aliases: [],
117818
118095
  description: "显示版本信息",
117819
- priority: 90,
118096
+ priority: 94,
117820
118097
  availability: "always"
117821
118098
  },
118099
+ {
118100
+ name: "logout",
118101
+ aliases: ["disconnect"],
118102
+ description: "删除已配置的模型",
118103
+ priority: 93
118104
+ },
117822
118105
  {
117823
118106
  name: "exit",
117824
118107
  aliases: ["quit", "q"],
@@ -119839,11 +120122,36 @@ async function handleAutoCommand(host, args) {
119839
120122
  host.showNotice("自动模式:开启", "工具自动批准。代理不会提问。");
119840
120123
  }
119841
120124
  }
119842
- async function handlePowerCommand(host, _args) {
119843
- const next = !host.state.appState.parallelMode;
119844
- host.setAppState({ parallelMode: next });
119845
- if (next) host.showNotice("Power 模式:开启", "成功注入power能量 你的任务交给我了");
119846
- else host.showNotice("Power 模式:关闭", "可惜了 早知道再多花点Token了");
120125
+ async function handleWolfpackCommand(host, args) {
120126
+ const session = host.session;
120127
+ if (session === void 0) {
120128
+ host.showError(NO_ACTIVE_SESSION_MESSAGE);
120129
+ return;
120130
+ }
120131
+ const subcmd = args.trim().toLowerCase();
120132
+ let enabled;
120133
+ if (subcmd.length === 0) enabled = !host.state.appState.wolfpackMode;
120134
+ else if (subcmd === "on") enabled = true;
120135
+ else if (subcmd === "off") enabled = false;
120136
+ else {
120137
+ host.showError(`Unknown wolfpack subcommand: ${subcmd}`);
120138
+ return;
120139
+ }
120140
+ await applyWolfpackMode(host, session, enabled);
120141
+ }
120142
+ async function applyWolfpackMode(host, session, enabled) {
120143
+ try {
120144
+ await session.setWolfpackMode(enabled);
120145
+ host.setAppState({ wolfpackMode: enabled });
120146
+ if (enabled) {
120147
+ host.showNotice("WolfPack 模式:开启", "批量并发代理已激活。");
120148
+ return;
120149
+ }
120150
+ host.showNotice("WolfPack 模式:关闭");
120151
+ } catch (error) {
120152
+ const msg = formatErrorMessage(error);
120153
+ host.showError(`Failed to set wolfpack mode: ${msg}`);
120154
+ }
119847
120155
  }
119848
120156
  async function handleCompactCommand(host, args) {
119849
120157
  const session = host.session;
@@ -120568,12 +120876,608 @@ function syncGoalMetadata(host) {
120568
120876
  session.writeMetadata();
120569
120877
  }
120570
120878
  //#endregion
120879
+ //#region src/utils/git/git-status.ts
120880
+ /**
120881
+ * Cached git branch + working-tree status for the footer/statusline.
120882
+ *
120883
+ * Branch name refreshes every 5s, porcelain status every 15s. Branch
120884
+ * and status reads stay synchronous with short timeouts. Pull request
120885
+ * lookup uses an async cache so a slow `gh pr view` never blocks
120886
+ * footer rendering.
120887
+ */
120888
+ const BRANCH_TTL_MS = 5e3;
120889
+ const STATUS_TTL_MS = 15e3;
120890
+ const PULL_REQUEST_TTL_MS = 6e4;
120891
+ const SPAWN_TIMEOUT_MS = 500;
120892
+ const PR_SPAWN_TIMEOUT_MS = 5e3;
120893
+ const AHEAD_BEHIND_RE = /\[(?:ahead (\d+))?(?:, )?(?:behind (\d+))?\]/;
120894
+ function createGitStatusCache(workDir, options = {}) {
120895
+ const isRepo = detectGitRepo(workDir);
120896
+ let branch = {
120897
+ value: null,
120898
+ fetchedAt: 0
120899
+ };
120900
+ let status = {
120901
+ dirty: false,
120902
+ ahead: 0,
120903
+ behind: 0,
120904
+ diffAdded: 0,
120905
+ diffDeleted: 0,
120906
+ fetchedAt: 0
120907
+ };
120908
+ let pullRequest = {
120909
+ value: null,
120910
+ branch: null,
120911
+ fetchedAt: 0,
120912
+ pendingBranch: null,
120913
+ requestId: 0
120914
+ };
120915
+ return { getStatus: () => {
120916
+ if (!isRepo) return null;
120917
+ const now = Date.now();
120918
+ if (now - branch.fetchedAt >= BRANCH_TTL_MS) branch = {
120919
+ value: readBranch(workDir),
120920
+ fetchedAt: now
120921
+ };
120922
+ if (branch.value === null) return null;
120923
+ if (now - status.fetchedAt >= STATUS_TTL_MS) status = {
120924
+ ...readStatus(workDir),
120925
+ fetchedAt: now
120926
+ };
120927
+ refreshPullRequestIfNeeded(branch.value, now);
120928
+ return {
120929
+ branch: branch.value,
120930
+ dirty: status.dirty,
120931
+ ahead: status.ahead,
120932
+ behind: status.behind,
120933
+ diffAdded: status.diffAdded,
120934
+ diffDeleted: status.diffDeleted,
120935
+ pullRequest: pullRequest.branch === branch.value ? pullRequest.value : null
120936
+ };
120937
+ } };
120938
+ function refreshPullRequestIfNeeded(branchName, now) {
120939
+ if (pullRequest.pendingBranch === branchName) return;
120940
+ const fetchedAt = pullRequest.branch === branchName ? pullRequest.fetchedAt : 0;
120941
+ if (now - fetchedAt < PULL_REQUEST_TTL_MS) return;
120942
+ const requestId = pullRequest.requestId + 1;
120943
+ pullRequest = {
120944
+ value: pullRequest.branch === branchName ? pullRequest.value : null,
120945
+ branch: branchName,
120946
+ fetchedAt,
120947
+ pendingBranch: branchName,
120948
+ requestId
120949
+ };
120950
+ readPullRequest(workDir).then((value) => {
120951
+ if (pullRequest.requestId !== requestId) return;
120952
+ const changed = !samePullRequest(pullRequest.branch === branchName ? pullRequest.value : null, value);
120953
+ pullRequest = {
120954
+ value,
120955
+ branch: branchName,
120956
+ fetchedAt: Date.now(),
120957
+ pendingBranch: null,
120958
+ requestId
120959
+ };
120960
+ if (changed) options.onChange?.();
120961
+ });
120962
+ }
120963
+ }
120964
+ function detectGitRepo(workDir) {
120965
+ try {
120966
+ const result = spawnSync("git", [
120967
+ "-C",
120968
+ workDir,
120969
+ "rev-parse",
120970
+ "--is-inside-work-tree"
120971
+ ], {
120972
+ encoding: "utf8",
120973
+ timeout: SPAWN_TIMEOUT_MS
120974
+ });
120975
+ return result.status === 0 && result.stdout.trim() === "true";
120976
+ } catch {
120977
+ return false;
120978
+ }
120979
+ }
120980
+ function readBranch(workDir) {
120981
+ try {
120982
+ const result = spawnSync("git", [
120983
+ "-C",
120984
+ workDir,
120985
+ "branch",
120986
+ "--show-current"
120987
+ ], {
120988
+ encoding: "utf8",
120989
+ timeout: SPAWN_TIMEOUT_MS
120990
+ });
120991
+ if (result.status !== 0) return null;
120992
+ const name = result.stdout.trim();
120993
+ return name.length > 0 ? name : null;
120994
+ } catch {
120995
+ return null;
120996
+ }
120997
+ }
120998
+ function readStatus(workDir) {
120999
+ try {
121000
+ const result = spawnSync("git", [
121001
+ "-C",
121002
+ workDir,
121003
+ "status",
121004
+ "--porcelain",
121005
+ "-b"
121006
+ ], {
121007
+ encoding: "utf8",
121008
+ timeout: SPAWN_TIMEOUT_MS,
121009
+ maxBuffer: 4 * 1024 * 1024
121010
+ });
121011
+ if (result.status !== 0) return {
121012
+ dirty: false,
121013
+ ahead: 0,
121014
+ behind: 0,
121015
+ diffAdded: 0,
121016
+ diffDeleted: 0
121017
+ };
121018
+ let dirty = false;
121019
+ let ahead = 0;
121020
+ let behind = 0;
121021
+ for (const line of result.stdout.split("\n")) if (line.startsWith("## ")) {
121022
+ const m = AHEAD_BEHIND_RE.exec(line);
121023
+ if (m) {
121024
+ ahead = Number.parseInt(m[1] ?? "0", 10) || 0;
121025
+ behind = Number.parseInt(m[2] ?? "0", 10) || 0;
121026
+ }
121027
+ } else if (line.trim().length > 0) dirty = true;
121028
+ const diff = dirty ? readDiffStats(workDir) : {
121029
+ added: 0,
121030
+ deleted: 0
121031
+ };
121032
+ return {
121033
+ dirty,
121034
+ ahead,
121035
+ behind,
121036
+ diffAdded: diff.added,
121037
+ diffDeleted: diff.deleted
121038
+ };
121039
+ } catch {
121040
+ return {
121041
+ dirty: false,
121042
+ ahead: 0,
121043
+ behind: 0,
121044
+ diffAdded: 0,
121045
+ diffDeleted: 0
121046
+ };
121047
+ }
121048
+ }
121049
+ function readDiffStats(workDir) {
121050
+ try {
121051
+ const result = spawnSync("git", [
121052
+ "-C",
121053
+ workDir,
121054
+ "diff",
121055
+ "--numstat",
121056
+ "HEAD",
121057
+ "--"
121058
+ ], {
121059
+ encoding: "utf8",
121060
+ timeout: SPAWN_TIMEOUT_MS,
121061
+ maxBuffer: 4 * 1024 * 1024
121062
+ });
121063
+ if (result.status !== 0) return {
121064
+ added: 0,
121065
+ deleted: 0
121066
+ };
121067
+ let added = 0;
121068
+ let deleted = 0;
121069
+ for (const line of result.stdout.split("\n")) {
121070
+ if (!line) continue;
121071
+ const [addedText, deletedText] = line.split(" ");
121072
+ added += parseDiffNumstatCount(addedText);
121073
+ deleted += parseDiffNumstatCount(deletedText);
121074
+ }
121075
+ return {
121076
+ added,
121077
+ deleted
121078
+ };
121079
+ } catch {
121080
+ return {
121081
+ added: 0,
121082
+ deleted: 0
121083
+ };
121084
+ }
121085
+ }
121086
+ function parseDiffNumstatCount(value) {
121087
+ if (value === void 0 || value === "-") return 0;
121088
+ const n = Number.parseInt(value, 10);
121089
+ return Number.isFinite(n) && n > 0 ? n : 0;
121090
+ }
121091
+ function readPullRequest(workDir) {
121092
+ return new Promise((resolve) => {
121093
+ try {
121094
+ execFile("gh", [
121095
+ "pr",
121096
+ "view",
121097
+ "--json",
121098
+ "number,url"
121099
+ ], {
121100
+ cwd: workDir,
121101
+ encoding: "utf8",
121102
+ env: {
121103
+ ...process.env,
121104
+ GH_NO_UPDATE_NOTIFIER: "1",
121105
+ GH_PROMPT_DISABLED: "1"
121106
+ },
121107
+ timeout: PR_SPAWN_TIMEOUT_MS,
121108
+ maxBuffer: 256 * 1024
121109
+ }, (error, stdout) => {
121110
+ if (error !== null) {
121111
+ resolve(null);
121112
+ return;
121113
+ }
121114
+ resolve(parsePullRequest(stdout));
121115
+ });
121116
+ } catch {
121117
+ resolve(null);
121118
+ }
121119
+ });
121120
+ }
121121
+ function samePullRequest(a, b) {
121122
+ if (a === null || b === null) return a === b;
121123
+ return a.number === b.number && a.url === b.url;
121124
+ }
121125
+ function parsePullRequest(stdout) {
121126
+ try {
121127
+ const raw = JSON.parse(stdout);
121128
+ if (typeof raw !== "object" || raw === null) return null;
121129
+ const record = raw;
121130
+ const number = record["number"];
121131
+ const url = record["url"];
121132
+ if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) return null;
121133
+ if (typeof url !== "string" || !isSafeHttpUrl(url)) return null;
121134
+ return {
121135
+ number,
121136
+ url
121137
+ };
121138
+ } catch {
121139
+ return null;
121140
+ }
121141
+ }
121142
+ function isSafeHttpUrl(value) {
121143
+ if (hasControlChars(value)) return false;
121144
+ try {
121145
+ const url = new URL(value);
121146
+ return url.protocol === "https:" || url.protocol === "http:";
121147
+ } catch {
121148
+ return false;
121149
+ }
121150
+ }
121151
+ function hasControlChars(value) {
121152
+ for (const char of value) {
121153
+ const code = char.codePointAt(0) ?? 0;
121154
+ if (code <= 31 || code === 127) return true;
121155
+ }
121156
+ return false;
121157
+ }
121158
+ function formatGitBadgeBase(status) {
121159
+ const parts = [];
121160
+ const diff = formatDiffStats(status);
121161
+ if (diff) parts.push(diff);
121162
+ let sync = "";
121163
+ if (status.ahead > 0) sync += `↑${status.ahead}`;
121164
+ if (status.behind > 0) sync += `↓${status.behind}`;
121165
+ if (sync) parts.push(sync);
121166
+ return parts.length === 0 ? status.branch : `${status.branch} [${parts.join(" ")}]`;
121167
+ }
121168
+ function formatPullRequestBadge(pullRequest, options = {}) {
121169
+ const prText = `[PR#${String(pullRequest.number)}]`;
121170
+ return options.linkPullRequest ? toTerminalHyperlink(prText, pullRequest.url) : prText;
121171
+ }
121172
+ function formatDiffStats(status) {
121173
+ const parts = [];
121174
+ if (status.diffAdded > 0) parts.push(`+${String(status.diffAdded)}`);
121175
+ if (status.diffDeleted > 0) parts.push(`-${String(status.diffDeleted)}`);
121176
+ if (parts.length > 0) return parts.join(" ");
121177
+ return status.dirty ? "±" : null;
121178
+ }
121179
+ function toTerminalHyperlink(text, url) {
121180
+ if (!isSafeHttpUrl(url)) return text;
121181
+ return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`;
121182
+ }
121183
+ //#endregion
121184
+ //#region src/tui/components/chrome/footer.ts
121185
+ const MAX_CWD_SEGMENTS = 3;
121186
+ const TIP_ROTATE_INTERVAL_MS = 1e4;
121187
+ const TOOLBAR_TIPS = [
121188
+ { text: "shift+tab: 计划模式" },
121189
+ { text: "/model: 切换模型" },
121190
+ {
121191
+ text: "ctrl+s: 中途干预",
121192
+ priority: 2
121193
+ },
121194
+ {
121195
+ text: "/compact: 压缩上下文",
121196
+ priority: 2
121197
+ },
121198
+ { text: "ctrl+o: 展开工具输出" },
121199
+ { text: "/tasks: 后台任务" },
121200
+ { text: "shift+enter: 换行" },
121201
+ {
121202
+ text: "/init: 生成 AGENTS.md",
121203
+ priority: 2
121204
+ },
121205
+ { text: "@: 提及文件" },
121206
+ { text: "ctrl+c: 取消" },
121207
+ { text: "/theme: 切换主题" },
121208
+ { text: "/auto: 自动权限模式" },
121209
+ { text: "/yes: 自动批准" },
121210
+ { text: "/help: 显示命令" },
121211
+ {
121212
+ text: "/config: 选择并配置你常用的模型商",
121213
+ solo: true,
121214
+ priority: 3
121215
+ },
121216
+ {
121217
+ text: "让 Scream 安排任务,例如 \"2个小时后提醒我去拿快递\"",
121218
+ solo: true,
121219
+ priority: 3
121220
+ }
121221
+ ];
121222
+ /**
121223
+ * Expand tips into a rotation sequence using smooth weighted round-robin
121224
+ * (the nginx SWRR algorithm). Higher-`priority` tips appear more often while
121225
+ * staying evenly spread, so a tip generally does not land next to its own
121226
+ * duplicate. Deterministic and computed once at module load. Exported for
121227
+ * unit testing.
121228
+ */
121229
+ function buildWeightedTips(tips) {
121230
+ const items = tips.map((t) => ({
121231
+ tip: t,
121232
+ weight: Math.max(1, Math.trunc(t.priority ?? 1)),
121233
+ current: 0
121234
+ }));
121235
+ const total = items.reduce((sum, it) => sum + it.weight, 0);
121236
+ const seq = [];
121237
+ for (let n = 0; n < total; n++) {
121238
+ let best = items[0];
121239
+ for (const it of items) {
121240
+ it.current += it.weight;
121241
+ if (it.current > best.current) best = it;
121242
+ }
121243
+ best.current -= total;
121244
+ seq.push(best.tip);
121245
+ }
121246
+ return seq;
121247
+ }
121248
+ const ROTATION = buildWeightedTips(TOOLBAR_TIPS);
121249
+ function currentTipIndex() {
121250
+ return Math.floor(Date.now() / TIP_ROTATE_INTERVAL_MS);
121251
+ }
121252
+ /**
121253
+ * Pick the tip(s) for a rotation index over the weighted ROTATION sequence.
121254
+ * `primary` is always shown when it fits; `pair` (primary + next tip joined
121255
+ * by the separator) is offered for wide terminals. Pairing is skipped when
121256
+ * the current/next tip is `solo` or when the neighbour is a duplicate of the
121257
+ * current tip (which can happen at the wrap boundary), keeping long/important
121258
+ * tips on their own and avoiding "X | X".
121259
+ */
121260
+ function tipsForIndex(index) {
121261
+ const n = ROTATION.length;
121262
+ if (n === 0) return {
121263
+ primary: "",
121264
+ pair: null
121265
+ };
121266
+ const offset = (index % n + n) % n;
121267
+ const current = ROTATION[offset];
121268
+ if (n === 1 || current.solo) return {
121269
+ primary: current.text,
121270
+ pair: null
121271
+ };
121272
+ const next = ROTATION[(offset + 1) % n];
121273
+ if (next.solo || next.text === current.text) return {
121274
+ primary: current.text,
121275
+ pair: null
121276
+ };
121277
+ return {
121278
+ primary: current.text,
121279
+ pair: current.text + " | " + next.text
121280
+ };
121281
+ }
121282
+ function shortenModel(model) {
121283
+ if (!model) return model;
121284
+ const slash = model.lastIndexOf("/");
121285
+ return slash >= 0 ? model.slice(slash + 1) : model;
121286
+ }
121287
+ function modelDisplayName(state) {
121288
+ const model = state.availableModels[state.model];
121289
+ return model?.displayName ?? model?.model ?? state.model;
121290
+ }
121291
+ function shortenCwd(path) {
121292
+ if (!path) return path;
121293
+ const home = process.env["HOME"] ?? "";
121294
+ let work = path;
121295
+ if (home && path === home) return "~";
121296
+ if (home && path.startsWith(home + "/")) work = "~" + path.slice(home.length);
121297
+ const segments = work.split("/").filter((s) => s.length > 0);
121298
+ if (segments.length <= MAX_CWD_SEGMENTS) return work;
121299
+ return `…/${segments.slice(-3).join("/")}`;
121300
+ }
121301
+ function formatTokenCount(n) {
121302
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
121303
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
121304
+ return String(n);
121305
+ }
121306
+ function safeUsage(usage) {
121307
+ return safeUsageRatio(usage);
121308
+ }
121309
+ function formatContextStatus(usage, tokens, maxTokens) {
121310
+ const pct = `${(safeUsage(usage) * 100).toFixed(1)}%`;
121311
+ if (maxTokens && maxTokens > 0 && tokens !== void 0) return `上下文:${pct} (${formatTokenCount(tokens)}/${formatTokenCount(maxTokens)})`;
121312
+ return `上下文:${pct}`;
121313
+ }
121314
+ const BRAND_COLORS = [
121315
+ "#72A4E9",
121316
+ "#A78BFA",
121317
+ "#34D399"
121318
+ ];
121319
+ const GRADIENT_CYCLE_MS = 4e3;
121320
+ const SPINNER_FRAMES$1 = [
121321
+ "●",
121322
+ "◉",
121323
+ "◎",
121324
+ "◌",
121325
+ "○",
121326
+ "◌",
121327
+ "◎",
121328
+ "◉"
121329
+ ];
121330
+ const SPINNER_TICK_MS = 120;
121331
+ function hexToRgb$1(hex) {
121332
+ const v = parseInt(hex.slice(1), 16);
121333
+ return [
121334
+ v >> 16 & 255,
121335
+ v >> 8 & 255,
121336
+ v & 255
121337
+ ];
121338
+ }
121339
+ function lerpGradient(t) {
121340
+ const count = BRAND_COLORS.length;
121341
+ const segment = Math.min(t * count, count - 1);
121342
+ const idx = Math.floor(segment);
121343
+ const localT = segment - idx;
121344
+ const nextIdx = (idx + 1) % count;
121345
+ const [r0, g0, b0] = hexToRgb$1(BRAND_COLORS[idx]);
121346
+ const [r1, g1, b1] = hexToRgb$1(BRAND_COLORS[nextIdx]);
121347
+ const r = Math.round(r0 + (r1 - r0) * localT);
121348
+ const g = Math.round(g0 + (g1 - g0) * localT);
121349
+ const b = Math.round(b0 + (b1 - b0) * localT);
121350
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
121351
+ }
121352
+ function buildStatusLine(streamingPhase, livePaneMode, streamingStartTime) {
121353
+ if (streamingPhase === "idle" && livePaneMode !== "tool") return "○ 空闲";
121354
+ let label;
121355
+ if (livePaneMode === "tool") label = "执行中";
121356
+ else if (streamingPhase === "waiting") label = "等待响应";
121357
+ else if (streamingPhase === "thinking") label = "思考中";
121358
+ else if (streamingPhase === "composing") label = "输出中";
121359
+ else label = "";
121360
+ const elapsed = Date.now() - streamingStartTime;
121361
+ const totalSeconds = Math.floor(elapsed / 1e3);
121362
+ const elapsedStr = totalSeconds < 60 ? `${totalSeconds}s` : `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
121363
+ const now = Date.now();
121364
+ const frame = SPINNER_FRAMES$1[Math.floor(now / SPINNER_TICK_MS) % SPINNER_FRAMES$1.length];
121365
+ const gradientColor = lerpGradient(now % GRADIENT_CYCLE_MS / GRADIENT_CYCLE_MS);
121366
+ return chalk.hex(gradientColor).bold(frame) + " " + label + " " + elapsedStr;
121367
+ }
121368
+ function formatFooterGitBadge(status, colors) {
121369
+ const base = chalk.hex(colors.status)(formatGitBadgeBase(status));
121370
+ if (status.pullRequest === null) return base;
121371
+ return `${base} ${chalk.hex(colors.primary)(formatPullRequestBadge(status.pullRequest, { linkPullRequest: true }))}`;
121372
+ }
121373
+ var FooterComponent = class {
121374
+ state;
121375
+ colors;
121376
+ onGitStatusChange;
121377
+ gitCache;
121378
+ gitCacheWorkDir;
121379
+ transientHint = null;
121380
+ /**
121381
+ * Non-terminal background-task counts split by kind so the footer can
121382
+ * render two distinct badges. `bashTasks` covers `bash-*` BPM tasks
121383
+ * spawned via `Shell run_in_background=true`; `agentTasks` covers
121384
+ * `agent-*` BPM tasks (background subagents). Either zero hides its
121385
+ * respective badge.
121386
+ */
121387
+ backgroundBashTaskCount = 0;
121388
+ backgroundAgentCount = 0;
121389
+ constructor(state, colors, onGitStatusChange = () => {}) {
121390
+ this.state = state;
121391
+ this.colors = colors;
121392
+ this.onGitStatusChange = onGitStatusChange;
121393
+ this.gitCacheWorkDir = state.workDir;
121394
+ this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
121395
+ }
121396
+ setState(state) {
121397
+ if (state.workDir !== this.gitCacheWorkDir) {
121398
+ this.gitCacheWorkDir = state.workDir;
121399
+ this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
121400
+ }
121401
+ this.state = state;
121402
+ }
121403
+ setColors(colors) {
121404
+ this.colors = colors;
121405
+ }
121406
+ /**
121407
+ * Short-lived hint that replaces the rotating toolbar tips on line 1.
121408
+ * Used by the exit-confirmation double-tap flow to show "Press Ctrl+C
121409
+ * again to exit" without requiring a toast/overlay subsystem.
121410
+ * Pass `null` to clear.
121411
+ */
121412
+ setTransientHint(hint) {
121413
+ this.transientHint = hint;
121414
+ }
121415
+ /**
121416
+ * Sync both background-task badges with live counts. Each non-zero
121417
+ * count produces its own bracketed badge on line 1; zeros hide them
121418
+ * independently.
121419
+ */
121420
+ setBackgroundCounts(counts) {
121421
+ this.backgroundBashTaskCount = Math.max(0, counts.bashTasks);
121422
+ this.backgroundAgentCount = Math.max(0, counts.agentTasks);
121423
+ }
121424
+ invalidate() {}
121425
+ render(width) {
121426
+ const colors = this.colors;
121427
+ const state = this.state;
121428
+ const left = [];
121429
+ if (state.permissionMode === "auto") left.push(chalk.hex(colors.warning).bold("auto"));
121430
+ if (state.permissionMode === "yolo") left.push(chalk.hex(colors.warning).bold("YES"));
121431
+ if (state.planMode) left.push(chalk.hex(colors.primary).bold("plan"));
121432
+ if (state.wolfpackMode) left.push(chalk.hex(colors.primary).bold("wolfpack"));
121433
+ if (state.goalActive && state.goal) {
121434
+ const goalText = state.goal.length > 20 ? state.goal.slice(0, 20) + "…" : state.goal;
121435
+ left.push(chalk.hex(colors.success).bold(`🎯 ${goalText}`));
121436
+ }
121437
+ const model = shortenModel(modelDisplayName(state));
121438
+ if (model) {
121439
+ const thinkingLabel = state.thinking ? " 思考中" : "";
121440
+ left.push(chalk.hex(colors.text)(`${model}${thinkingLabel}`));
121441
+ }
121442
+ if (this.backgroundBashTaskCount > 0) {
121443
+ const noun = this.backgroundBashTaskCount === 1 ? "个任务" : "个任务";
121444
+ left.push(chalk.hex(colors.primary)(`[${String(this.backgroundBashTaskCount)}${noun} 运行中]`));
121445
+ }
121446
+ if (this.backgroundAgentCount > 0) {
121447
+ const noun = this.backgroundAgentCount === 1 ? "个代理" : "个代理";
121448
+ left.push(chalk.hex(colors.primary)(`[${String(this.backgroundAgentCount)}${noun} 运行中]`));
121449
+ }
121450
+ const cwd = shortenCwd(state.workDir);
121451
+ if (cwd) left.push(chalk.hex(colors.status)(cwd));
121452
+ const git = this.gitCache.getStatus();
121453
+ if (git !== null) left.push(formatFooterGitBadge(git, colors));
121454
+ const leftLine = left.join(" ");
121455
+ const leftWidth = visibleWidth(leftLine);
121456
+ let rightText;
121457
+ if (this.transientHint) rightText = chalk.hex(colors.warning).bold(this.transientHint);
121458
+ else {
121459
+ const statusLine = buildStatusLine(state.streamingPhase, state.livePaneMode, state.streamingStartTime);
121460
+ const ccDot = state.ccConnectActive ? chalk.hex(colors.success)("●") : chalk.hex(colors.textDim)("●");
121461
+ rightText = chalk.hex(colors.textDim)(ccDot + " " + formatContextStatus(state.contextUsage, state.contextTokens, state.maxContextTokens) + " " + statusLine);
121462
+ }
121463
+ const rightWidth = visibleWidth(rightText);
121464
+ const gap = 3;
121465
+ let line1;
121466
+ if (leftWidth + gap + rightWidth <= width) {
121467
+ const pad = width - leftWidth - rightWidth;
121468
+ line1 = leftLine + " ".repeat(pad) + rightText;
121469
+ } else if (leftWidth <= width) line1 = leftLine;
121470
+ else line1 = truncateToWidth(leftLine, width, "…");
121471
+ return [truncateToWidth(line1, width)];
121472
+ }
121473
+ };
121474
+ //#endregion
120571
121475
  //#region src/tui/components/chrome/welcome.ts
120572
121476
  const HUE_STOPS = 24;
120573
121477
  const SUB_STEPS = 5;
120574
121478
  const BREATHE_STEPS = HUE_STOPS * SUB_STEPS;
120575
121479
  const BREATHE_INTERVAL_MS = 40;
120576
- function hexToRgb$1(hex) {
121480
+ function hexToRgb(hex) {
120577
121481
  return [
120578
121482
  parseInt(hex.slice(1, 3), 16),
120579
121483
  parseInt(hex.slice(3, 5), 16),
@@ -120638,7 +121542,7 @@ function rgbToHex(r, g, b) {
120638
121542
  * it sweeps through all 24 hue stops with smooth sub-step interpolation.
120639
121543
  */
120640
121544
  function buildBreathingPalette(primaryHex, hueStops, subSteps) {
120641
- const [r, g, b] = hexToRgb$1(primaryHex);
121545
+ const [r, g, b] = hexToRgb(primaryHex);
120642
121546
  const [baseHue, sat, lit] = rgbToHsl(r, g, b);
120643
121547
  const steps = hueStops * subSteps;
120644
121548
  const palette = [];
@@ -120655,6 +121559,7 @@ var WelcomeComponent = class {
120655
121559
  breatheFrame = 0;
120656
121560
  breatheTimer = null;
120657
121561
  breathePalette;
121562
+ borderTitle = null;
120658
121563
  constructor(state, colors, ui) {
120659
121564
  this.state = state;
120660
121565
  this.colors = colors;
@@ -120693,7 +121598,7 @@ var WelcomeComponent = class {
120693
121598
  const isLoggedOut = !this.state.model;
120694
121599
  const dim = chalk.hex(this.colors.textDim);
120695
121600
  const labelStyle = chalk.bold.hex(this.colors.textDim);
120696
- const rightRow1 = truncateToWidth(dim(isLoggedOut ? "运行 /config 开始配置。" : "发送 /help 获取帮助信息。/config 配置模型"), textWidth, "…");
121601
+ const rightRow1 = truncateToWidth(dim(isLoggedOut ? "运行 /config 开始配置。" : "发送 / 进入快捷菜单,/exit 保存并退出"), textWidth, "…");
120697
121602
  const headerLines = [logoColor(logo[0].padEnd(logoWidth)) + gap + rightRow0, logoColor(logo[1].padEnd(logoWidth)) + gap + rightRow1];
120698
121603
  const activeModel = this.state.availableModels[this.state.model];
120699
121604
  const modelValue = isLoggedOut ? chalk.hex(this.colors.warning)("未设置,运行 /config") : activeModel?.displayName ?? activeModel?.model ?? this.state.model;
@@ -120705,19 +121610,20 @@ var WelcomeComponent = class {
120705
121610
  labelStyle("模型: ") + modelValue,
120706
121611
  labelStyle("版本: ") + versionValue
120707
121612
  ];
120708
- const hintLine = chalk.hex(this.colors.textMuted)("按 / 可进入快捷指令菜单,输入 /sessions 可恢复或管理会话");
120709
- const tipLine = chalk.hex(this.colors.textMuted)("Tips:试试让它写代码、做研报、做攻略、清理电脑...");
121613
+ const { primary: tipPrimary, pair: tipPair } = tipsForIndex(currentTipIndex());
121614
+ const tip = tipPair && visibleWidth(tipPair) <= innerWidth ? tipPair : tipPrimary;
121615
+ const tipLine = chalk.hex(this.colors.textMuted)("Tips: " + tip);
120710
121616
  const contentLines = [
120711
121617
  ...headerLines,
120712
121618
  "",
120713
121619
  ...infoLines,
120714
121620
  "",
120715
- hintLine,
120716
121621
  tipLine
120717
121622
  ];
121623
+ const borderTitle = this.borderTitle;
120718
121624
  const lines = [
120719
121625
  "",
120720
- primary("╭" + "─".repeat(width - 2) + "╮"),
121626
+ borderTitle ? primary("╭─ " + borderTitle + " " + "─".repeat(Math.max(0, width - 5 - visibleWidth(borderTitle))) + "╮") : primary("╭" + "─".repeat(width - 2) + "╮"),
120721
121627
  primary("│") + " ".repeat(width - 2) + primary("│")
120722
121628
  ];
120723
121629
  for (const content of contentLines) {
@@ -124718,7 +125624,7 @@ async function confirmUninstall(host, label) {
124718
125624
  * restores the editor. The question and answer are never recorded in the
124719
125625
  * main conversation history.
124720
125626
  */
124721
- const SPINNER_FRAMES$1 = [
125627
+ const SPINNER_FRAMES = [
124722
125628
  "⠋",
124723
125629
  "⠙",
124724
125630
  "⠹",
@@ -124778,7 +125684,7 @@ var BtwOverlayComponent = class extends Container {
124778
125684
  startSpinner() {
124779
125685
  this.spinnerFrame = 0;
124780
125686
  this.spinnerInterval = setInterval(() => {
124781
- this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES$1.length;
125687
+ this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
124782
125688
  this.requestRender();
124783
125689
  }, 80);
124784
125690
  }
@@ -124798,7 +125704,7 @@ var BtwOverlayComponent = class extends Container {
124798
125704
  lines.push(chalk.hex(c.primary)("/btw") + chalk.hex(c.textMuted)(" — ") + chalk.hex(c.text)(truncated));
124799
125705
  lines.push("");
124800
125706
  if (this.status === "loading") {
124801
- const spinner = SPINNER_FRAMES$1[this.spinnerFrame];
125707
+ const spinner = SPINNER_FRAMES[this.spinnerFrame];
124802
125708
  lines.push(chalk.hex(c.textMuted)(`${spinner} Answering…`));
124803
125709
  } else if (this.status === "done" && this.markdown !== void 0) {
124804
125710
  const contentWidth = Math.max(20, width - 2);
@@ -124958,8 +125864,8 @@ async function handleBuiltInSlashCommand(host, name, args) {
124958
125864
  case "plan":
124959
125865
  await handlePlanCommand(host, args);
124960
125866
  return;
124961
- case "power":
124962
- await handlePowerCommand(host, args);
125867
+ case "wolfpack":
125868
+ await handleWolfpackCommand(host, args);
124963
125869
  return;
124964
125870
  case "revoke":
124965
125871
  await handleRevokeCommand(host, args);
@@ -129964,635 +130870,6 @@ const INITIAL_LIVE_PANE = {
129964
130870
  pendingQuestion: null
129965
130871
  };
129966
130872
  //#endregion
129967
- //#region src/utils/git/git-status.ts
129968
- /**
129969
- * Cached git branch + working-tree status for the footer/statusline.
129970
- *
129971
- * Branch name refreshes every 5s, porcelain status every 15s. Branch
129972
- * and status reads stay synchronous with short timeouts. Pull request
129973
- * lookup uses an async cache so a slow `gh pr view` never blocks
129974
- * footer rendering.
129975
- */
129976
- const BRANCH_TTL_MS = 5e3;
129977
- const STATUS_TTL_MS = 15e3;
129978
- const PULL_REQUEST_TTL_MS = 6e4;
129979
- const SPAWN_TIMEOUT_MS = 500;
129980
- const PR_SPAWN_TIMEOUT_MS = 5e3;
129981
- const AHEAD_BEHIND_RE = /\[(?:ahead (\d+))?(?:, )?(?:behind (\d+))?\]/;
129982
- function createGitStatusCache(workDir, options = {}) {
129983
- const isRepo = detectGitRepo(workDir);
129984
- let branch = {
129985
- value: null,
129986
- fetchedAt: 0
129987
- };
129988
- let status = {
129989
- dirty: false,
129990
- ahead: 0,
129991
- behind: 0,
129992
- diffAdded: 0,
129993
- diffDeleted: 0,
129994
- fetchedAt: 0
129995
- };
129996
- let pullRequest = {
129997
- value: null,
129998
- branch: null,
129999
- fetchedAt: 0,
130000
- pendingBranch: null,
130001
- requestId: 0
130002
- };
130003
- return { getStatus: () => {
130004
- if (!isRepo) return null;
130005
- const now = Date.now();
130006
- if (now - branch.fetchedAt >= BRANCH_TTL_MS) branch = {
130007
- value: readBranch(workDir),
130008
- fetchedAt: now
130009
- };
130010
- if (branch.value === null) return null;
130011
- if (now - status.fetchedAt >= STATUS_TTL_MS) status = {
130012
- ...readStatus(workDir),
130013
- fetchedAt: now
130014
- };
130015
- refreshPullRequestIfNeeded(branch.value, now);
130016
- return {
130017
- branch: branch.value,
130018
- dirty: status.dirty,
130019
- ahead: status.ahead,
130020
- behind: status.behind,
130021
- diffAdded: status.diffAdded,
130022
- diffDeleted: status.diffDeleted,
130023
- pullRequest: pullRequest.branch === branch.value ? pullRequest.value : null
130024
- };
130025
- } };
130026
- function refreshPullRequestIfNeeded(branchName, now) {
130027
- if (pullRequest.pendingBranch === branchName) return;
130028
- const fetchedAt = pullRequest.branch === branchName ? pullRequest.fetchedAt : 0;
130029
- if (now - fetchedAt < PULL_REQUEST_TTL_MS) return;
130030
- const requestId = pullRequest.requestId + 1;
130031
- pullRequest = {
130032
- value: pullRequest.branch === branchName ? pullRequest.value : null,
130033
- branch: branchName,
130034
- fetchedAt,
130035
- pendingBranch: branchName,
130036
- requestId
130037
- };
130038
- readPullRequest(workDir).then((value) => {
130039
- if (pullRequest.requestId !== requestId) return;
130040
- const changed = !samePullRequest(pullRequest.branch === branchName ? pullRequest.value : null, value);
130041
- pullRequest = {
130042
- value,
130043
- branch: branchName,
130044
- fetchedAt: Date.now(),
130045
- pendingBranch: null,
130046
- requestId
130047
- };
130048
- if (changed) options.onChange?.();
130049
- });
130050
- }
130051
- }
130052
- function detectGitRepo(workDir) {
130053
- try {
130054
- const result = spawnSync("git", [
130055
- "-C",
130056
- workDir,
130057
- "rev-parse",
130058
- "--is-inside-work-tree"
130059
- ], {
130060
- encoding: "utf8",
130061
- timeout: SPAWN_TIMEOUT_MS
130062
- });
130063
- return result.status === 0 && result.stdout.trim() === "true";
130064
- } catch {
130065
- return false;
130066
- }
130067
- }
130068
- function readBranch(workDir) {
130069
- try {
130070
- const result = spawnSync("git", [
130071
- "-C",
130072
- workDir,
130073
- "branch",
130074
- "--show-current"
130075
- ], {
130076
- encoding: "utf8",
130077
- timeout: SPAWN_TIMEOUT_MS
130078
- });
130079
- if (result.status !== 0) return null;
130080
- const name = result.stdout.trim();
130081
- return name.length > 0 ? name : null;
130082
- } catch {
130083
- return null;
130084
- }
130085
- }
130086
- function readStatus(workDir) {
130087
- try {
130088
- const result = spawnSync("git", [
130089
- "-C",
130090
- workDir,
130091
- "status",
130092
- "--porcelain",
130093
- "-b"
130094
- ], {
130095
- encoding: "utf8",
130096
- timeout: SPAWN_TIMEOUT_MS,
130097
- maxBuffer: 4 * 1024 * 1024
130098
- });
130099
- if (result.status !== 0) return {
130100
- dirty: false,
130101
- ahead: 0,
130102
- behind: 0,
130103
- diffAdded: 0,
130104
- diffDeleted: 0
130105
- };
130106
- let dirty = false;
130107
- let ahead = 0;
130108
- let behind = 0;
130109
- for (const line of result.stdout.split("\n")) if (line.startsWith("## ")) {
130110
- const m = AHEAD_BEHIND_RE.exec(line);
130111
- if (m) {
130112
- ahead = Number.parseInt(m[1] ?? "0", 10) || 0;
130113
- behind = Number.parseInt(m[2] ?? "0", 10) || 0;
130114
- }
130115
- } else if (line.trim().length > 0) dirty = true;
130116
- const diff = dirty ? readDiffStats(workDir) : {
130117
- added: 0,
130118
- deleted: 0
130119
- };
130120
- return {
130121
- dirty,
130122
- ahead,
130123
- behind,
130124
- diffAdded: diff.added,
130125
- diffDeleted: diff.deleted
130126
- };
130127
- } catch {
130128
- return {
130129
- dirty: false,
130130
- ahead: 0,
130131
- behind: 0,
130132
- diffAdded: 0,
130133
- diffDeleted: 0
130134
- };
130135
- }
130136
- }
130137
- function readDiffStats(workDir) {
130138
- try {
130139
- const result = spawnSync("git", [
130140
- "-C",
130141
- workDir,
130142
- "diff",
130143
- "--numstat",
130144
- "HEAD",
130145
- "--"
130146
- ], {
130147
- encoding: "utf8",
130148
- timeout: SPAWN_TIMEOUT_MS,
130149
- maxBuffer: 4 * 1024 * 1024
130150
- });
130151
- if (result.status !== 0) return {
130152
- added: 0,
130153
- deleted: 0
130154
- };
130155
- let added = 0;
130156
- let deleted = 0;
130157
- for (const line of result.stdout.split("\n")) {
130158
- if (!line) continue;
130159
- const [addedText, deletedText] = line.split(" ");
130160
- added += parseDiffNumstatCount(addedText);
130161
- deleted += parseDiffNumstatCount(deletedText);
130162
- }
130163
- return {
130164
- added,
130165
- deleted
130166
- };
130167
- } catch {
130168
- return {
130169
- added: 0,
130170
- deleted: 0
130171
- };
130172
- }
130173
- }
130174
- function parseDiffNumstatCount(value) {
130175
- if (value === void 0 || value === "-") return 0;
130176
- const n = Number.parseInt(value, 10);
130177
- return Number.isFinite(n) && n > 0 ? n : 0;
130178
- }
130179
- function readPullRequest(workDir) {
130180
- return new Promise((resolve) => {
130181
- try {
130182
- execFile("gh", [
130183
- "pr",
130184
- "view",
130185
- "--json",
130186
- "number,url"
130187
- ], {
130188
- cwd: workDir,
130189
- encoding: "utf8",
130190
- env: {
130191
- ...process.env,
130192
- GH_NO_UPDATE_NOTIFIER: "1",
130193
- GH_PROMPT_DISABLED: "1"
130194
- },
130195
- timeout: PR_SPAWN_TIMEOUT_MS,
130196
- maxBuffer: 256 * 1024
130197
- }, (error, stdout) => {
130198
- if (error !== null) {
130199
- resolve(null);
130200
- return;
130201
- }
130202
- resolve(parsePullRequest(stdout));
130203
- });
130204
- } catch {
130205
- resolve(null);
130206
- }
130207
- });
130208
- }
130209
- function samePullRequest(a, b) {
130210
- if (a === null || b === null) return a === b;
130211
- return a.number === b.number && a.url === b.url;
130212
- }
130213
- function parsePullRequest(stdout) {
130214
- try {
130215
- const raw = JSON.parse(stdout);
130216
- if (typeof raw !== "object" || raw === null) return null;
130217
- const record = raw;
130218
- const number = record["number"];
130219
- const url = record["url"];
130220
- if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) return null;
130221
- if (typeof url !== "string" || !isSafeHttpUrl(url)) return null;
130222
- return {
130223
- number,
130224
- url
130225
- };
130226
- } catch {
130227
- return null;
130228
- }
130229
- }
130230
- function isSafeHttpUrl(value) {
130231
- if (hasControlChars(value)) return false;
130232
- try {
130233
- const url = new URL(value);
130234
- return url.protocol === "https:" || url.protocol === "http:";
130235
- } catch {
130236
- return false;
130237
- }
130238
- }
130239
- function hasControlChars(value) {
130240
- for (const char of value) {
130241
- const code = char.codePointAt(0) ?? 0;
130242
- if (code <= 31 || code === 127) return true;
130243
- }
130244
- return false;
130245
- }
130246
- function formatGitBadgeBase(status) {
130247
- const parts = [];
130248
- const diff = formatDiffStats(status);
130249
- if (diff) parts.push(diff);
130250
- let sync = "";
130251
- if (status.ahead > 0) sync += `↑${status.ahead}`;
130252
- if (status.behind > 0) sync += `↓${status.behind}`;
130253
- if (sync) parts.push(sync);
130254
- return parts.length === 0 ? status.branch : `${status.branch} [${parts.join(" ")}]`;
130255
- }
130256
- function formatPullRequestBadge(pullRequest, options = {}) {
130257
- const prText = `[PR#${String(pullRequest.number)}]`;
130258
- return options.linkPullRequest ? toTerminalHyperlink(prText, pullRequest.url) : prText;
130259
- }
130260
- function formatDiffStats(status) {
130261
- const parts = [];
130262
- if (status.diffAdded > 0) parts.push(`+${String(status.diffAdded)}`);
130263
- if (status.diffDeleted > 0) parts.push(`-${String(status.diffDeleted)}`);
130264
- if (parts.length > 0) return parts.join(" ");
130265
- return status.dirty ? "±" : null;
130266
- }
130267
- function toTerminalHyperlink(text, url) {
130268
- if (!isSafeHttpUrl(url)) return text;
130269
- return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`;
130270
- }
130271
- //#endregion
130272
- //#region src/tui/components/chrome/footer.ts
130273
- const MAX_CWD_SEGMENTS = 3;
130274
- const TIP_ROTATE_INTERVAL_MS = 1e4;
130275
- const TIP_SEPARATOR = " | ";
130276
- const TOOLBAR_TIPS = [
130277
- { text: "shift+tab: 计划模式" },
130278
- { text: "/model: 切换模型" },
130279
- {
130280
- text: "ctrl+s: 中途干预",
130281
- priority: 2
130282
- },
130283
- {
130284
- text: "/compact: 压缩上下文",
130285
- priority: 2
130286
- },
130287
- { text: "ctrl+o: 展开工具输出" },
130288
- { text: "/tasks: 后台任务" },
130289
- { text: "shift+enter: 换行" },
130290
- {
130291
- text: "/init: 生成 AGENTS.md",
130292
- priority: 2
130293
- },
130294
- { text: "@: 提及文件" },
130295
- { text: "ctrl+c: 取消" },
130296
- { text: "/theme: 切换主题" },
130297
- { text: "/auto: 自动权限模式" },
130298
- { text: "/yes: 自动批准" },
130299
- { text: "/help: 显示命令" },
130300
- {
130301
- text: "/config: 选择并配置你常用的模型商",
130302
- solo: true,
130303
- priority: 3
130304
- },
130305
- {
130306
- text: "让 Scream 安排任务,例如 \"2个小时后提醒我去拿快递\"",
130307
- solo: true,
130308
- priority: 3
130309
- }
130310
- ];
130311
- /**
130312
- * Expand tips into a rotation sequence using smooth weighted round-robin
130313
- * (the nginx SWRR algorithm). Higher-`priority` tips appear more often while
130314
- * staying evenly spread, so a tip generally does not land next to its own
130315
- * duplicate. Deterministic and computed once at module load. Exported for
130316
- * unit testing.
130317
- */
130318
- function buildWeightedTips(tips) {
130319
- const items = tips.map((t) => ({
130320
- tip: t,
130321
- weight: Math.max(1, Math.trunc(t.priority ?? 1)),
130322
- current: 0
130323
- }));
130324
- const total = items.reduce((sum, it) => sum + it.weight, 0);
130325
- const seq = [];
130326
- for (let n = 0; n < total; n++) {
130327
- let best = items[0];
130328
- for (const it of items) {
130329
- it.current += it.weight;
130330
- if (it.current > best.current) best = it;
130331
- }
130332
- best.current -= total;
130333
- seq.push(best.tip);
130334
- }
130335
- return seq;
130336
- }
130337
- const ROTATION = buildWeightedTips(TOOLBAR_TIPS);
130338
- function currentTipIndex() {
130339
- return Math.floor(Date.now() / TIP_ROTATE_INTERVAL_MS);
130340
- }
130341
- /**
130342
- * Pick the tip(s) for a rotation index over the weighted ROTATION sequence.
130343
- * `primary` is always shown when it fits; `pair` (primary + next tip joined
130344
- * by the separator) is offered for wide terminals. Pairing is skipped when
130345
- * the current/next tip is `solo` or when the neighbour is a duplicate of the
130346
- * current tip (which can happen at the wrap boundary), keeping long/important
130347
- * tips on their own and avoiding "X | X".
130348
- */
130349
- function tipsForIndex(index) {
130350
- const n = ROTATION.length;
130351
- if (n === 0) return {
130352
- primary: "",
130353
- pair: null
130354
- };
130355
- const offset = (index % n + n) % n;
130356
- const current = ROTATION[offset];
130357
- if (n === 1 || current.solo) return {
130358
- primary: current.text,
130359
- pair: null
130360
- };
130361
- const next = ROTATION[(offset + 1) % n];
130362
- if (next.solo || next.text === current.text) return {
130363
- primary: current.text,
130364
- pair: null
130365
- };
130366
- return {
130367
- primary: current.text,
130368
- pair: current.text + TIP_SEPARATOR + next.text
130369
- };
130370
- }
130371
- function shortenModel(model) {
130372
- if (!model) return model;
130373
- const slash = model.lastIndexOf("/");
130374
- return slash >= 0 ? model.slice(slash + 1) : model;
130375
- }
130376
- function modelDisplayName(state) {
130377
- const model = state.availableModels[state.model];
130378
- return model?.displayName ?? model?.model ?? state.model;
130379
- }
130380
- function shortenCwd(path) {
130381
- if (!path) return path;
130382
- const home = process.env["HOME"] ?? "";
130383
- let work = path;
130384
- if (home && path === home) return "~";
130385
- if (home && path.startsWith(home + "/")) work = "~" + path.slice(home.length);
130386
- const segments = work.split("/").filter((s) => s.length > 0);
130387
- if (segments.length <= MAX_CWD_SEGMENTS) return work;
130388
- return `…/${segments.slice(-3).join("/")}`;
130389
- }
130390
- function formatTokenCount(n) {
130391
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
130392
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
130393
- return String(n);
130394
- }
130395
- function safeUsage(usage) {
130396
- return safeUsageRatio(usage);
130397
- }
130398
- function formatContextStatus(usage, tokens, maxTokens) {
130399
- const pct = `${(safeUsage(usage) * 100).toFixed(1)}%`;
130400
- if (maxTokens && maxTokens > 0 && tokens !== void 0) return `上下文:${pct} (${formatTokenCount(tokens)}/${formatTokenCount(maxTokens)})`;
130401
- return `上下文:${pct}`;
130402
- }
130403
- const BRAND_COLORS = [
130404
- "#72A4E9",
130405
- "#A78BFA",
130406
- "#34D399"
130407
- ];
130408
- const GRADIENT_CYCLE_MS = 4e3;
130409
- const SPINNER_FRAMES = [
130410
- "●",
130411
- "◉",
130412
- "◎",
130413
- "◌",
130414
- "○",
130415
- "◌",
130416
- "◎",
130417
- "◉"
130418
- ];
130419
- const SPINNER_TICK_MS = 120;
130420
- function hexToRgb(hex) {
130421
- const v = parseInt(hex.slice(1), 16);
130422
- return [
130423
- v >> 16 & 255,
130424
- v >> 8 & 255,
130425
- v & 255
130426
- ];
130427
- }
130428
- function lerpGradient(t) {
130429
- const count = BRAND_COLORS.length;
130430
- const segment = Math.min(t * count, count - 1);
130431
- const idx = Math.floor(segment);
130432
- const localT = segment - idx;
130433
- const nextIdx = (idx + 1) % count;
130434
- const [r0, g0, b0] = hexToRgb(BRAND_COLORS[idx]);
130435
- const [r1, g1, b1] = hexToRgb(BRAND_COLORS[nextIdx]);
130436
- const r = Math.round(r0 + (r1 - r0) * localT);
130437
- const g = Math.round(g0 + (g1 - g0) * localT);
130438
- const b = Math.round(b0 + (b1 - b0) * localT);
130439
- return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
130440
- }
130441
- const POWER_GRADIENT_CYCLE_MS = 2e3;
130442
- /** Each character of `text` gets a phase-offset colour from the brand gradient, creating a flowing rainbow effect. */
130443
- function gradientText(text, now) {
130444
- const t = now % POWER_GRADIENT_CYCLE_MS / POWER_GRADIENT_CYCLE_MS;
130445
- let out = "";
130446
- for (let i = 0; i < text.length; i++) {
130447
- const charT = (t + i / text.length) % 1;
130448
- out += chalk.hex(lerpGradient(charT))(text[i]);
130449
- }
130450
- return chalk.bold(out);
130451
- }
130452
- function buildStatusLine(streamingPhase, livePaneMode, streamingStartTime) {
130453
- if (streamingPhase === "idle" && livePaneMode !== "tool") return "○ 空闲";
130454
- let label;
130455
- if (livePaneMode === "tool") label = "执行中";
130456
- else if (streamingPhase === "waiting") label = "等待响应";
130457
- else if (streamingPhase === "thinking") label = "思考中";
130458
- else if (streamingPhase === "composing") label = "输出中";
130459
- else label = "";
130460
- const elapsed = Date.now() - streamingStartTime;
130461
- const totalSeconds = Math.floor(elapsed / 1e3);
130462
- const elapsedStr = totalSeconds < 60 ? `${totalSeconds}s` : `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
130463
- const now = Date.now();
130464
- const frame = SPINNER_FRAMES[Math.floor(now / SPINNER_TICK_MS) % SPINNER_FRAMES.length];
130465
- const gradientColor = lerpGradient(now % GRADIENT_CYCLE_MS / GRADIENT_CYCLE_MS);
130466
- return chalk.hex(gradientColor).bold(frame) + " " + label + " " + elapsedStr;
130467
- }
130468
- function formatFooterGitBadge(status, colors) {
130469
- const base = chalk.hex(colors.status)(formatGitBadgeBase(status));
130470
- if (status.pullRequest === null) return base;
130471
- return `${base} ${chalk.hex(colors.primary)(formatPullRequestBadge(status.pullRequest, { linkPullRequest: true }))}`;
130472
- }
130473
- var FooterComponent = class {
130474
- state;
130475
- colors;
130476
- onGitStatusChange;
130477
- gitCache;
130478
- gitCacheWorkDir;
130479
- transientHint = null;
130480
- /**
130481
- * Non-terminal background-task counts split by kind so the footer can
130482
- * render two distinct badges. `bashTasks` covers `bash-*` BPM tasks
130483
- * spawned via `Shell run_in_background=true`; `agentTasks` covers
130484
- * `agent-*` BPM tasks (background subagents). Either zero hides its
130485
- * respective badge.
130486
- */
130487
- backgroundBashTaskCount = 0;
130488
- backgroundAgentCount = 0;
130489
- powerGradientTimer;
130490
- constructor(state, colors, onGitStatusChange = () => {}) {
130491
- this.state = state;
130492
- this.colors = colors;
130493
- this.onGitStatusChange = onGitStatusChange;
130494
- this.gitCacheWorkDir = state.workDir;
130495
- this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
130496
- }
130497
- setState(state) {
130498
- if (state.workDir !== this.gitCacheWorkDir) {
130499
- this.gitCacheWorkDir = state.workDir;
130500
- this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
130501
- }
130502
- this.state = state;
130503
- }
130504
- startPowerGradient() {
130505
- if (this.powerGradientTimer !== void 0) return;
130506
- this.powerGradientTimer = setInterval(() => {
130507
- this.onGitStatusChange();
130508
- }, 80);
130509
- }
130510
- stopPowerGradient() {
130511
- if (this.powerGradientTimer === void 0) return;
130512
- clearInterval(this.powerGradientTimer);
130513
- this.powerGradientTimer = void 0;
130514
- }
130515
- setColors(colors) {
130516
- this.colors = colors;
130517
- }
130518
- /**
130519
- * Short-lived hint that replaces the rotating toolbar tips on line 1.
130520
- * Used by the exit-confirmation double-tap flow to show "Press Ctrl+C
130521
- * again to exit" without requiring a toast/overlay subsystem.
130522
- * Pass `null` to clear.
130523
- */
130524
- setTransientHint(hint) {
130525
- this.transientHint = hint;
130526
- }
130527
- /**
130528
- * Sync both background-task badges with live counts. Each non-zero
130529
- * count produces its own bracketed badge on line 1; zeros hide them
130530
- * independently.
130531
- */
130532
- setBackgroundCounts(counts) {
130533
- this.backgroundBashTaskCount = Math.max(0, counts.bashTasks);
130534
- this.backgroundAgentCount = Math.max(0, counts.agentTasks);
130535
- }
130536
- invalidate() {}
130537
- render(width) {
130538
- const colors = this.colors;
130539
- const state = this.state;
130540
- const left = [];
130541
- if (state.permissionMode === "auto") left.push(chalk.hex(colors.warning).bold("auto"));
130542
- if (state.permissionMode === "yolo") left.push(chalk.hex(colors.warning).bold("YES"));
130543
- if (state.planMode) left.push(chalk.hex(colors.primary).bold("plan"));
130544
- if (state.parallelMode) left.push(gradientText("power", Date.now()));
130545
- if (state.goalActive && state.goal) {
130546
- const goalText = state.goal.length > 20 ? state.goal.slice(0, 20) + "…" : state.goal;
130547
- left.push(chalk.hex(colors.success).bold(`🎯 ${goalText}`));
130548
- }
130549
- const model = shortenModel(modelDisplayName(state));
130550
- if (model) {
130551
- const thinkingLabel = state.thinking ? " 思考中" : "";
130552
- left.push(chalk.hex(colors.text)(`${model}${thinkingLabel}`));
130553
- }
130554
- if (this.backgroundBashTaskCount > 0) {
130555
- const noun = this.backgroundBashTaskCount === 1 ? "个任务" : "个任务";
130556
- left.push(chalk.hex(colors.primary)(`[${String(this.backgroundBashTaskCount)}${noun} 运行中]`));
130557
- }
130558
- if (this.backgroundAgentCount > 0) {
130559
- const noun = this.backgroundAgentCount === 1 ? "个代理" : "个代理";
130560
- left.push(chalk.hex(colors.primary)(`[${String(this.backgroundAgentCount)}${noun} 运行中]`));
130561
- }
130562
- const cwd = shortenCwd(state.workDir);
130563
- if (cwd) left.push(chalk.hex(colors.status)(cwd));
130564
- const git = this.gitCache.getStatus();
130565
- if (git !== null) left.push(formatFooterGitBadge(git, colors));
130566
- const leftLine = left.join(" ");
130567
- const leftWidth = visibleWidth(leftLine);
130568
- const { primary, pair } = tipsForIndex(currentTipIndex());
130569
- const remaining = Math.max(0, width - leftWidth - 2);
130570
- let tipText = "";
130571
- if (pair && visibleWidth(pair) <= remaining) tipText = pair;
130572
- else if (primary && visibleWidth(primary) <= remaining) tipText = primary;
130573
- let line1;
130574
- if (tipText) {
130575
- const pad = width - leftWidth - visibleWidth(tipText);
130576
- line1 = leftLine + " ".repeat(Math.max(0, pad)) + chalk.hex(colors.textMuted)(tipText);
130577
- } else if (leftWidth <= width) line1 = leftLine;
130578
- else line1 = truncateToWidth(leftLine, width, "…");
130579
- const statusLine = buildStatusLine(state.streamingPhase, state.livePaneMode, state.streamingStartTime);
130580
- const rightText = (state.ccConnectActive ? chalk.hex(colors.success)("●") : chalk.hex(colors.textDim)("●")) + " " + formatContextStatus(state.contextUsage, state.contextTokens, state.maxContextTokens) + " " + statusLine;
130581
- const rightWidth = visibleWidth(rightText);
130582
- let line2;
130583
- if (this.transientHint) {
130584
- const hintText = chalk.hex(colors.warning).bold(this.transientHint);
130585
- const hintWidth = visibleWidth(hintText);
130586
- const pad = Math.max(2, width - hintWidth - rightWidth);
130587
- line2 = hintText + " ".repeat(pad) + chalk.hex(colors.textDim)(rightText);
130588
- } else {
130589
- const lpad = Math.max(0, width - rightWidth);
130590
- line2 = " ".repeat(lpad) + chalk.hex(colors.textDim)(rightText);
130591
- }
130592
- return [truncateToWidth(line1, width), truncateToWidth(line2, width)];
130593
- }
130594
- };
130595
- //#endregion
130596
130873
  //#region src/tui/components/chrome/todo-panel.ts
130597
130874
  const MAX_VISIBLE = 5;
130598
130875
  /**
@@ -133820,23 +134097,20 @@ var DialogManager = class {
133820
134097
  }
133821
134098
  }));
133822
134099
  }
133823
- showMemoryPicker() {
134100
+ showMemoryPicker(preloadedMemos, preloadedTotal) {
133824
134101
  const store = new MemoryMemoStore(resolveProjectDir(getDataDir(), this.host.getCurrentWorkDir()));
133825
- let memos = [];
133826
- let total = 0;
133827
- try {
133828
- store.list({ limit: 50 }).then((result) => {
133829
- memos = result.memos;
133830
- total = result.total;
133831
- if (this.host.state.activeDialog === "memory-picker") this.showMemoryPicker();
133832
- });
133833
- } catch {}
134102
+ const hasData = preloadedMemos !== void 0;
134103
+ const memos = preloadedMemos ?? [];
134104
+ const total = preloadedTotal ?? 0;
134105
+ if (!hasData) store.list({ limit: 50 }).then((result) => {
134106
+ if (this.host.state.activeDialog === "memory-picker") this.showMemoryPicker(result.memos, result.total);
134107
+ }).catch(() => {});
133834
134108
  this.host.state.activeDialog = "memory-picker";
133835
134109
  this.mountEditorReplacement(new MemoryPickerComponent({
133836
134110
  store,
133837
134111
  memos,
133838
134112
  total,
133839
- loading: false,
134113
+ loading: !hasData,
133840
134114
  colors: this.host.state.theme.colors,
133841
134115
  onCancel: () => {
133842
134116
  this.host.state.activeDialog = null;
@@ -133959,7 +134233,7 @@ function createInitialAppState(input) {
133959
134233
  goalActive: false,
133960
134234
  goalContinuationCount: 0,
133961
134235
  ccConnectActive: false,
133962
- parallelMode: false
134236
+ wolfpackMode: false
133963
134237
  };
133964
134238
  }
133965
134239
  var ScreamTUI = class {
@@ -134159,6 +134433,9 @@ var ScreamTUI = class {
134159
134433
  for (const dispose of this.reverseRpcDisposers) dispose();
134160
134434
  this.reverseRpcDisposers.length = 0;
134161
134435
  this.disposeTerminalTracking();
134436
+ this.state.footer.setTransientHint("正在整理会话记忆...");
134437
+ this.state.ui.requestRender();
134438
+ await new Promise((r) => setTimeout(r, 0));
134162
134439
  await this.closeSession();
134163
134440
  await this.harness.close();
134164
134441
  this.sessionEventHandler.stopAllMcpServerStatusSpinners();
@@ -134373,9 +134650,7 @@ var ScreamTUI = class {
134373
134650
  imageAttachmentIds
134374
134651
  });
134375
134652
  this.beginSessionRequest();
134376
- let modelInput = options?.parts ?? input;
134377
- if (this.state.appState.parallelMode && options?.parts === void 0) modelInput = "[系统指令:Power 并行模式已激活。你必须将用户的任务拆解为多个独立的子任务,并在同一个回复中同时调用多个 Agent 工具并行执行。每个 Agent 处理一个子任务,系统自动并发。不要逐个串行调用 Agent——除非子任务之间有严格的顺序依赖或会写入同一文件。即使是简单任务,也至少拆成 2 个 Agent 并行。优先并行,宁可多拆不要少拆。]\n\n" + input;
134378
- session.prompt(modelInput).catch((error) => {
134653
+ session.prompt(options?.parts ?? input).catch((error) => {
134379
134654
  const message = formatErrorMessage(error);
134380
134655
  this.failSessionRequest(`发送失败:${message}`);
134381
134656
  });
@@ -134472,12 +134747,9 @@ var ScreamTUI = class {
134472
134747
  setAppState(patch) {
134473
134748
  if (!hasPatchChanges(this.state.appState, patch)) return;
134474
134749
  const busyChanged = "streamingPhase" in patch || "isCompacting" in patch;
134475
- const prevParallelMode = this.state.appState.parallelMode;
134476
134750
  Object.assign(this.state.appState, patch);
134477
134751
  if ("planMode" in patch) this.updateEditorBorderHighlight();
134478
134752
  if ("streamingPhase" in patch && patch.streamingPhase !== "idle") this.welcomeComponent?.stopBreathing();
134479
- if ("parallelMode" in patch && patch.parallelMode !== prevParallelMode) if (patch.parallelMode) this.state.footer.startPowerGradient();
134480
- else this.state.footer.stopPowerGradient();
134481
134753
  this.state.footer.setState(this.state.appState);
134482
134754
  this.updateActivityPane();
134483
134755
  if (busyChanged) this.updateQueueDisplay();
@@ -134620,6 +134892,7 @@ var ScreamTUI = class {
134620
134892
  renderWelcome() {
134621
134893
  this.welcomeComponent?.stopBreathing();
134622
134894
  const welcome = new WelcomeComponent(this.state.appState, this.state.theme.colors, this.state.ui);
134895
+ welcome.borderTitle = "Scream Code";
134623
134896
  this.welcomeComponent = welcome;
134624
134897
  if (this.state.editor.hasFirstInputFired()) welcome.stopBreathing();
134625
134898
  this.state.transcriptContainer.addChild(welcome);
@@ -134893,6 +135166,193 @@ var ScreamTUI = class {
134893
135166
  }
134894
135167
  };
134895
135168
  //#endregion
135169
+ //#region src/tui/components/chrome/loading.ts
135170
+ const { stdout } = process$1;
135171
+ const BRIGHT = "\x1B[38;2;255;255;255m";
135172
+ const THEME_GREEN = {
135173
+ dark: "\x1B[38;2;78;200;126m",
135174
+ light: "\x1B[38;2;14;122;56m"
135175
+ };
135176
+ const FRAMES = [
135177
+ {
135178
+ duration: 80,
135179
+ content: [
135180
+ "┌┐",
135181
+ "││",
135182
+ "││",
135183
+ "└┘"
135184
+ ]
135185
+ },
135186
+ {
135187
+ duration: 80,
135188
+ content: [
135189
+ "┌──────┐",
135190
+ "│ │",
135191
+ "│ │",
135192
+ "└──────┘"
135193
+ ]
135194
+ },
135195
+ {
135196
+ duration: 80,
135197
+ content: [
135198
+ "┌──────────────────┐",
135199
+ "│ │",
135200
+ "│ │",
135201
+ "└──────────────────┘"
135202
+ ]
135203
+ },
135204
+ {
135205
+ duration: 80,
135206
+ content: [
135207
+ "┌──────────────────────────────┐",
135208
+ "│ │",
135209
+ "│ welcome to scream code │",
135210
+ "│ │",
135211
+ "└──────────────────────────────┘"
135212
+ ]
135213
+ },
135214
+ {
135215
+ duration: 100,
135216
+ content: [
135217
+ "┌─────────────────────────────────────────────────────────────────┐",
135218
+ "│ │",
135219
+ "│ welcome to scream code │",
135220
+ "│ │",
135221
+ "│ ███████╗ ██████╗██████╗ ███████╗ █████╗ ███╗ ███╗ │",
135222
+ "│ ██╔════╝ ██╔════╝██╔══██╗██╔════╝██╔══██╗████╗ ████║ │",
135223
+ "│ ███████╗ ██║ ██████╔╝█████╗ ███████║██╔████╔██║ │",
135224
+ "│ ╚════██║ ██║ ██╔══██╗██╔══╝ ██╔══██║██║╚██╔╝██║ │",
135225
+ "│ ███████║ ╚██████╗██║ ██║███████╗██║ ██║██║ ╚═╝ ██║ │",
135226
+ "│ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ │",
135227
+ "│ │",
135228
+ "└─────────────────────────────────────────────────────────────────┘"
135229
+ ]
135230
+ },
135231
+ {
135232
+ duration: 120,
135233
+ content: [
135234
+ "┌─────────────────────────────────────────────────────────────────┐",
135235
+ "│ │",
135236
+ "│ welcome to scream code │",
135237
+ "│ │",
135238
+ "│ ███████╗ ██████╗██████╗ ███████╗ █████╗ ███╗ ███╗ │",
135239
+ "│ ██╔════╝ ██╔════╝██╔══██╗██╔════╝██╔══██╗████╗ ████║ │",
135240
+ "│ ███████╗ ██║ ██████╔╝█████╗ ███████║██╔████╔██║ │",
135241
+ "│ ╚════██║ ██║ ██╔══██╗██╔══╝ ██╔══██║██║╚██╔╝██║ │",
135242
+ "│ ███████║ ╚██████╗██║ ██║███████╗██║ ██║██║ ╚═╝ ██║ │",
135243
+ "│ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ │",
135244
+ "│ │",
135245
+ "│ 你的中文智能Ai助手 │",
135246
+ "└─────────────────────────────────────────────────────────────────┘"
135247
+ ]
135248
+ },
135249
+ {
135250
+ duration: 9999,
135251
+ content: [
135252
+ "┌─────────────────────────────────────────────────────────────────┐",
135253
+ "│ │",
135254
+ "│ welcome to scream code │",
135255
+ "│ │",
135256
+ "│ ███████╗ ██████╗██████╗ ███████╗ █████╗ ███╗ ███╗ │",
135257
+ "│ ██╔════╝ ██╔════╝██╔══██╗██╔════╝██╔══██╗████╗ ████║ │",
135258
+ "│ ███████╗ ██║ ██████╔╝█████╗ ███████║██╔████╔██║ │",
135259
+ "│ ╚════██║ ██║ ██╔══██╗██╔══╝ ██╔══██║██║╚██╔╝██║ │",
135260
+ "│ ███████║ ╚██████╗██║ ██║███████╗██║ ██║██║ ╚═╝ ██║ │",
135261
+ "│ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ │",
135262
+ "│ │",
135263
+ "│ 你的中文智能Ai助手 │",
135264
+ "└─────────────────────────────────────────────────────────────────┘"
135265
+ ]
135266
+ }
135267
+ ];
135268
+ function color(line, green) {
135269
+ const s = line.replace(/[│ ]/g, "");
135270
+ const tb = (line.startsWith("┌") || line.startsWith("└")) && (line.endsWith("┐") || line.endsWith("┘")) && s.replace(/[─┌┐└┘]/g, "") === "";
135271
+ const es = line.startsWith("│") && line.endsWith("│") && s === "";
135272
+ if (tb || es) return green;
135273
+ if (line.includes("welcome") || line.includes("scream") || line.includes("你的中文")) return green;
135274
+ return BRIGHT;
135275
+ }
135276
+ let ansiSupported = null;
135277
+ function supportsAnsi() {
135278
+ if (ansiSupported !== null) return ansiSupported;
135279
+ if (!stdout.isTTY) {
135280
+ ansiSupported = false;
135281
+ return false;
135282
+ }
135283
+ if (process$1.env.NO_COLOR) {
135284
+ ansiSupported = false;
135285
+ return false;
135286
+ }
135287
+ if (process$1.env.FORCE_COLOR) {
135288
+ ansiSupported = true;
135289
+ return true;
135290
+ }
135291
+ if (process$1.platform === "win32") {
135292
+ const term = (process$1.env.TERM ?? "").toLowerCase();
135293
+ const session = (process$1.env.TERM_PROGRAM ?? "").toLowerCase();
135294
+ if (term.includes("xterm") || term.includes("vt100") || term.includes("256color")) {
135295
+ ansiSupported = true;
135296
+ return true;
135297
+ }
135298
+ if (session.includes("terminal") || session.includes("vscode")) {
135299
+ ansiSupported = true;
135300
+ return true;
135301
+ }
135302
+ if (process$1.env.CI) {
135303
+ ansiSupported = true;
135304
+ return true;
135305
+ }
135306
+ ansiSupported = true;
135307
+ return true;
135308
+ }
135309
+ if (process$1.env.TERM && process$1.env.TERM !== "dumb") {
135310
+ ansiSupported = true;
135311
+ return true;
135312
+ }
135313
+ ansiSupported = false;
135314
+ return false;
135315
+ }
135316
+ function plainFrame(frameIndex, green) {
135317
+ const f = FRAMES[Math.min(frameIndex, FRAMES.length - 1)];
135318
+ let out = "";
135319
+ for (const l of f.content) out += color(l, green) + (l || " ") + "\x1B[0m\n";
135320
+ return out;
135321
+ }
135322
+ function runLoadingAnimation(theme = "dark") {
135323
+ const green = THEME_GREEN[theme];
135324
+ if (!supportsAnsi()) {
135325
+ stdout.write(plainFrame(FRAMES.length - 1, green));
135326
+ stdout.write("\n\x1B[38;2;136;136;136m正在加载scream code....\x1B[0m\n");
135327
+ return Promise.resolve();
135328
+ }
135329
+ return new Promise((resolve) => {
135330
+ const last = FRAMES.length - 1;
135331
+ let frame = 0;
135332
+ function draw(i) {
135333
+ stdout.write("\x1B[H" + plainFrame(i, green));
135334
+ }
135335
+ function tick() {
135336
+ if (frame >= last) {
135337
+ setTimeout(() => {
135338
+ stdout.write("\n\x1B[38;2;136;136;136m正在加载scream code....\x1B[0m\n");
135339
+ setTimeout(() => {
135340
+ stdout.write("\x1B[2J\x1B[H\x1B[?25h");
135341
+ resolve();
135342
+ }, 400);
135343
+ }, 600);
135344
+ return;
135345
+ }
135346
+ draw(frame);
135347
+ frame++;
135348
+ setTimeout(tick, FRAMES[frame - 1].duration);
135349
+ }
135350
+ stdout.write("\x1B[?25l\x1B[2J\x1B[H");
135351
+ draw(0);
135352
+ setTimeout(tick, FRAMES[0].duration);
135353
+ });
135354
+ }
135355
+ //#endregion
134896
135356
  //#region src/cli/run-shell.ts
134897
135357
  async function runShell(opts, version) {
134898
135358
  const startedAt = Date.now();
@@ -134929,6 +135389,7 @@ async function runShell(opts, version) {
134929
135389
  await harness.ensureConfigFile();
134930
135390
  const config = await harness.getConfig();
134931
135391
  const configMs = Date.now() - configStartedAt;
135392
+ await runLoadingAnimation(resolvedTheme);
134932
135393
  const tui = new ScreamTUI(harness, {
134933
135394
  cliOptions: opts,
134934
135395
  tuiConfig,