scream-code 0.3.10 → 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.
- package/dist/main.mjs +1203 -749
- package/package.json +24 -24
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
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;
|
|
@@ -75385,6 +75527,46 @@ function isTodoStatus(value) {
|
|
|
75385
75527
|
return value === "pending" || value === "in_progress" || value === "done";
|
|
75386
75528
|
}
|
|
75387
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
|
|
75388
75570
|
//#region ../../packages/agent-core/src/agent/injection/manager.ts
|
|
75389
75571
|
var InjectionManager = class {
|
|
75390
75572
|
agent;
|
|
@@ -75396,6 +75578,7 @@ var InjectionManager = class {
|
|
|
75396
75578
|
this.memoryRecall = autoRecallEnabled ? new MemoryRecallInjector(agent) : null;
|
|
75397
75579
|
this.injectors = [
|
|
75398
75580
|
new PluginSessionStartInjector(agent),
|
|
75581
|
+
new WolfPackModeInjector(agent),
|
|
75399
75582
|
new PlanModeInjector(agent),
|
|
75400
75583
|
new PermissionModeInjector(agent),
|
|
75401
75584
|
new TodoListReminderInjector(agent),
|
|
@@ -75473,7 +75656,8 @@ const DEFAULT_APPROVE_TOOLS = new Set([
|
|
|
75473
75656
|
"FetchURL",
|
|
75474
75657
|
"Agent",
|
|
75475
75658
|
"AskUserQuestion",
|
|
75476
|
-
"Skill"
|
|
75659
|
+
"Skill",
|
|
75660
|
+
"WolfPack"
|
|
75477
75661
|
]);
|
|
75478
75662
|
var DefaultToolApprovePermissionPolicy = class {
|
|
75479
75663
|
name = "default-tool-approve";
|
|
@@ -76052,6 +76236,19 @@ var YoloModeApprovePermissionPolicy = class {
|
|
|
76052
76236
|
}
|
|
76053
76237
|
};
|
|
76054
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
|
|
76055
76252
|
//#region ../../packages/agent-core/src/agent/permission/policies/index.ts
|
|
76056
76253
|
/** Permission policies run in order; the first non-undefined result wins. */
|
|
76057
76254
|
function createPermissionDecisionPolicies(agent) {
|
|
@@ -76070,6 +76267,7 @@ function createPermissionDecisionPolicies(agent) {
|
|
|
76070
76267
|
new GitControlPathAccessAskPermissionPolicy(agent),
|
|
76071
76268
|
new CwdOutsideFileWriteAskPermissionPolicy(agent),
|
|
76072
76269
|
new YoloModeApprovePermissionPolicy(agent),
|
|
76270
|
+
new WolfPackModeApprovePermissionPolicy(agent),
|
|
76073
76271
|
new DefaultToolApprovePermissionPolicy(),
|
|
76074
76272
|
new GitCwdWriteApprovePermissionPolicy(agent),
|
|
76075
76273
|
new FallbackAskPermissionPolicy()
|
|
@@ -76684,6 +76882,33 @@ function isMissingFileError(error) {
|
|
|
76684
76882
|
return error.code === "ENOENT";
|
|
76685
76883
|
}
|
|
76686
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
|
|
76687
76912
|
//#region ../../packages/agent-core/src/agent/session-memory.ts
|
|
76688
76913
|
const MAX_EVENTS = 50;
|
|
76689
76914
|
const MAX_SUMMARY_LENGTH = 1500;
|
|
@@ -77208,6 +77433,12 @@ function restoreAgentRecord(agent, input) {
|
|
|
77208
77433
|
case "plan_mode.exit":
|
|
77209
77434
|
agent.planMode.exit(input.id);
|
|
77210
77435
|
return;
|
|
77436
|
+
case "wolfpack.enter":
|
|
77437
|
+
agent.wolfpackMode.restoreEnter();
|
|
77438
|
+
return;
|
|
77439
|
+
case "wolfpack.exit":
|
|
77440
|
+
agent.wolfpackMode.exit();
|
|
77441
|
+
return;
|
|
77211
77442
|
case "context.append_message":
|
|
77212
77443
|
agent.context.appendMessage(input.message);
|
|
77213
77444
|
return;
|
|
@@ -89779,7 +90010,7 @@ function normalizeSourcePath(path) {
|
|
|
89779
90010
|
}
|
|
89780
90011
|
//#endregion
|
|
89781
90012
|
//#region ../../packages/agent-core/src/profile/default/agent.yaml
|
|
89782
|
-
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";
|
|
89783
90014
|
//#endregion
|
|
89784
90015
|
//#region ../../packages/agent-core/src/profile/default/coder.yaml
|
|
89785
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";
|
|
@@ -90093,6 +90324,7 @@ var ToolManager = class {
|
|
|
90093
90324
|
allowBackground,
|
|
90094
90325
|
log: this.agent.log
|
|
90095
90326
|
}),
|
|
90327
|
+
this.agent.subagentHost && new WolfPackTool(this.agent.subagentHost, () => this.agent.wolfpackMode.isActive, { log: this.agent.log }),
|
|
90096
90328
|
toolServices?.webSearcher && new WebSearchTool(toolServices.webSearcher),
|
|
90097
90329
|
toolServices?.urlFetcher && new FetchURLTool(toolServices.urlFetcher)
|
|
90098
90330
|
].filter((tool) => !!tool).map((tool) => [tool.name, tool]));
|
|
@@ -91279,6 +91511,7 @@ var Agent = class {
|
|
|
91279
91511
|
injection;
|
|
91280
91512
|
permission;
|
|
91281
91513
|
planMode;
|
|
91514
|
+
wolfpackMode;
|
|
91282
91515
|
usage;
|
|
91283
91516
|
skills;
|
|
91284
91517
|
tools;
|
|
@@ -91317,8 +91550,9 @@ var Agent = class {
|
|
|
91317
91550
|
this.config = new ConfigState(this);
|
|
91318
91551
|
this.turn = new TurnFlow(this);
|
|
91319
91552
|
this.injection = new InjectionManager(this);
|
|
91320
|
-
this.permission = new PermissionManager(this, options.permission);
|
|
91321
91553
|
this.planMode = new PlanMode(this);
|
|
91554
|
+
this.wolfpackMode = new WolfPackMode(this);
|
|
91555
|
+
this.permission = new PermissionManager(this, options.permission);
|
|
91322
91556
|
this.usage = new UsageRecorder(this);
|
|
91323
91557
|
this.skills = options.skills ? new SkillManager(this, options.skills) : null;
|
|
91324
91558
|
this.tools = new ToolManager(this);
|
|
@@ -91454,6 +91688,12 @@ var Agent = class {
|
|
|
91454
91688
|
enterPlan: async () => {
|
|
91455
91689
|
await this.planMode.enter();
|
|
91456
91690
|
},
|
|
91691
|
+
enterWolfpack: () => {
|
|
91692
|
+
this.wolfpackMode.enter();
|
|
91693
|
+
},
|
|
91694
|
+
exitWolfpack: () => {
|
|
91695
|
+
this.wolfpackMode.exit();
|
|
91696
|
+
},
|
|
91457
91697
|
cancelPlan: (payload) => {
|
|
91458
91698
|
this.planMode.cancel(payload.id);
|
|
91459
91699
|
},
|
|
@@ -111077,6 +111317,11 @@ const FLAG_DEFINITIONS = [{
|
|
|
111077
111317
|
env: "SCREAM_CODE_EXPERIMENTAL_MICRO_COMPACTION",
|
|
111078
111318
|
default: false,
|
|
111079
111319
|
surface: "both"
|
|
111320
|
+
}, {
|
|
111321
|
+
id: "wolfpack",
|
|
111322
|
+
env: "SCREAM_CODE_EXPERIMENTAL_WOLFPACK",
|
|
111323
|
+
default: false,
|
|
111324
|
+
surface: "both"
|
|
111080
111325
|
}];
|
|
111081
111326
|
/**
|
|
111082
111327
|
* Pure, synchronous flag resolver. State comes entirely from (env, registry) and nothing is
|
|
@@ -112453,6 +112698,12 @@ var SessionAPIImpl = class {
|
|
|
112453
112698
|
clearPlan({ agentId, ...payload }) {
|
|
112454
112699
|
return this.getAgent(agentId).clearPlan(payload);
|
|
112455
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
|
+
}
|
|
112456
112707
|
beginCompaction({ agentId, ...payload }) {
|
|
112457
112708
|
return this.getAgent(agentId).beginCompaction(payload);
|
|
112458
112709
|
}
|
|
@@ -114079,6 +114330,12 @@ var ScreamCore = class {
|
|
|
114079
114330
|
clearPlan({ sessionId, ...payload }) {
|
|
114080
114331
|
return this.sessionApi(sessionId).clearPlan(payload);
|
|
114081
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
|
+
}
|
|
114082
114339
|
beginCompaction({ sessionId, ...payload }) {
|
|
114083
114340
|
return this.sessionApi(sessionId).beginCompaction(payload);
|
|
114084
114341
|
}
|
|
@@ -114657,6 +114914,17 @@ var SDKRpcClient = class {
|
|
|
114657
114914
|
agentId: this.interactiveAgentId
|
|
114658
114915
|
});
|
|
114659
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
|
+
}
|
|
114660
114928
|
async getPlan(input) {
|
|
114661
114929
|
return (await this.getRpc()).getPlan({
|
|
114662
114930
|
sessionId: input.sessionId,
|
|
@@ -115053,6 +115321,14 @@ var Session = class {
|
|
|
115053
115321
|
enabled
|
|
115054
115322
|
});
|
|
115055
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
|
+
}
|
|
115056
115332
|
async getPlan() {
|
|
115057
115333
|
this.ensureOpen();
|
|
115058
115334
|
return this.rpc.getPlan({ sessionId: this.id });
|
|
@@ -117603,75 +117879,82 @@ const BUILTIN_SLASH_COMMANDS = [
|
|
|
117603
117879
|
availability: "always"
|
|
117604
117880
|
},
|
|
117605
117881
|
{
|
|
117606
|
-
name: "
|
|
117607
|
-
aliases: ["
|
|
117608
|
-
description: "
|
|
117882
|
+
name: "yes",
|
|
117883
|
+
aliases: ["yolo"],
|
|
117884
|
+
description: "切换至自动批准模式(yolo)",
|
|
117609
117885
|
priority: 124,
|
|
117610
117886
|
availability: "always"
|
|
117611
117887
|
},
|
|
117612
117888
|
{
|
|
117613
|
-
name: "
|
|
117614
|
-
aliases: ["
|
|
117615
|
-
description: "
|
|
117889
|
+
name: "wolfpack",
|
|
117890
|
+
aliases: ["wp"],
|
|
117891
|
+
description: "切换群狼协作模式,自动批准+批量并发",
|
|
117616
117892
|
priority: 123,
|
|
117617
117893
|
availability: "always"
|
|
117618
117894
|
},
|
|
117619
117895
|
{
|
|
117620
|
-
name: "
|
|
117621
|
-
aliases: [],
|
|
117622
|
-
description: "
|
|
117896
|
+
name: "sessions",
|
|
117897
|
+
aliases: ["resume"],
|
|
117898
|
+
description: "浏览并恢复会话",
|
|
117623
117899
|
priority: 122
|
|
117624
117900
|
},
|
|
117625
|
-
{
|
|
117626
|
-
name: "new",
|
|
117627
|
-
aliases: ["clear"],
|
|
117628
|
-
description: "在当前工作区开启新会话",
|
|
117629
|
-
priority: 121
|
|
117630
|
-
},
|
|
117631
117901
|
{
|
|
117632
117902
|
name: "memory",
|
|
117633
117903
|
aliases: ["memo", "mem"],
|
|
117634
117904
|
description: "浏览、搜索、注入记忆备忘录",
|
|
117635
|
-
priority:
|
|
117905
|
+
priority: 121,
|
|
117636
117906
|
availability: "always"
|
|
117637
117907
|
},
|
|
117638
117908
|
{
|
|
117639
|
-
name: "
|
|
117640
|
-
aliases: ["
|
|
117641
|
-
description: "
|
|
117642
|
-
priority:
|
|
117643
|
-
|
|
117909
|
+
name: "new",
|
|
117910
|
+
aliases: ["clear"],
|
|
117911
|
+
description: "在当前工作区开启新会话",
|
|
117912
|
+
priority: 121
|
|
117913
|
+
},
|
|
117914
|
+
{
|
|
117915
|
+
name: "model",
|
|
117916
|
+
aliases: [],
|
|
117917
|
+
description: "切换 LLM 模型",
|
|
117918
|
+
priority: 120
|
|
117644
117919
|
},
|
|
117645
117920
|
{
|
|
117646
117921
|
name: "compact",
|
|
117647
117922
|
aliases: [],
|
|
117648
117923
|
description: "压缩对话上下文",
|
|
117649
|
-
priority:
|
|
117924
|
+
priority: 119
|
|
117650
117925
|
},
|
|
117651
117926
|
{
|
|
117652
117927
|
name: "plan",
|
|
117653
117928
|
aliases: [],
|
|
117654
117929
|
description: "切换计划模式",
|
|
117655
|
-
priority:
|
|
117930
|
+
priority: 118,
|
|
117656
117931
|
availability: (args) => args.trim().toLowerCase() === "clear" ? "idle-only" : "always"
|
|
117657
117932
|
},
|
|
117658
|
-
{
|
|
117659
|
-
name: "sessions",
|
|
117660
|
-
aliases: ["resume"],
|
|
117661
|
-
description: "浏览并恢复会话",
|
|
117662
|
-
priority: 116
|
|
117663
|
-
},
|
|
117664
117933
|
{
|
|
117665
117934
|
name: "tasks",
|
|
117666
117935
|
aliases: ["task"],
|
|
117667
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: "显示当前会话和运行时状态",
|
|
117668
117951
|
priority: 115,
|
|
117669
117952
|
availability: "always"
|
|
117670
117953
|
},
|
|
117671
117954
|
{
|
|
117672
|
-
name: "
|
|
117955
|
+
name: "usage",
|
|
117673
117956
|
aliases: [],
|
|
117674
|
-
description: "
|
|
117957
|
+
description: "显示 token 用量和上下文窗口",
|
|
117675
117958
|
priority: 114,
|
|
117676
117959
|
availability: "always"
|
|
117677
117960
|
},
|
|
@@ -117683,58 +117966,45 @@ const BUILTIN_SLASH_COMMANDS = [
|
|
|
117683
117966
|
availability: "always"
|
|
117684
117967
|
},
|
|
117685
117968
|
{
|
|
117686
|
-
name: "
|
|
117969
|
+
name: "mcp",
|
|
117687
117970
|
aliases: [],
|
|
117688
|
-
description: "
|
|
117971
|
+
description: "管理 MCP 服务器(安装/停用/卸载)",
|
|
117689
117972
|
priority: 112,
|
|
117690
117973
|
availability: "always"
|
|
117691
117974
|
},
|
|
117692
117975
|
{
|
|
117693
|
-
name: "
|
|
117694
|
-
aliases: [],
|
|
117695
|
-
description: "
|
|
117976
|
+
name: "plugin",
|
|
117977
|
+
aliases: ["plugins"],
|
|
117978
|
+
description: "ScreamCode 插件中心:浏览、安装、卸载插件",
|
|
117696
117979
|
priority: 111,
|
|
117697
117980
|
availability: "always"
|
|
117698
117981
|
},
|
|
117699
117982
|
{
|
|
117700
|
-
name: "
|
|
117983
|
+
name: "cc",
|
|
117701
117984
|
aliases: [],
|
|
117702
|
-
description: "
|
|
117985
|
+
description: "操控你的cc(启动/关闭/重启)",
|
|
117703
117986
|
priority: 110,
|
|
117704
117987
|
availability: "always"
|
|
117705
117988
|
},
|
|
117706
117989
|
{
|
|
117707
|
-
name: "
|
|
117990
|
+
name: "cc-connect",
|
|
117708
117991
|
aliases: [],
|
|
117709
|
-
description: "
|
|
117992
|
+
description: "cc-connect 快速通道配置(需先安装)",
|
|
117710
117993
|
priority: 109,
|
|
117711
117994
|
availability: "always"
|
|
117712
117995
|
},
|
|
117713
|
-
{
|
|
117714
|
-
name: "permission",
|
|
117715
|
-
aliases: [],
|
|
117716
|
-
description: "选择权限模式",
|
|
117717
|
-
priority: 108,
|
|
117718
|
-
availability: "always"
|
|
117719
|
-
},
|
|
117720
117996
|
{
|
|
117721
117997
|
name: "revoke",
|
|
117722
117998
|
aliases: [],
|
|
117723
117999
|
description: "撤回上一次对话(可指定轮数,如 /revoke 3)",
|
|
117724
|
-
priority:
|
|
118000
|
+
priority: 108,
|
|
117725
118001
|
availability: "idle-only"
|
|
117726
118002
|
},
|
|
117727
|
-
{
|
|
117728
|
-
name: "config",
|
|
117729
|
-
aliases: [],
|
|
117730
|
-
description: "浏览并配置模型(远程拉取最新目录)",
|
|
117731
|
-
priority: 106
|
|
117732
|
-
},
|
|
117733
118003
|
{
|
|
117734
118004
|
name: "goal",
|
|
117735
118005
|
aliases: [],
|
|
117736
118006
|
description: "管理自动目标(status状态/pause暂停/resume恢复/replace替换,取消用 /goaloff)",
|
|
117737
|
-
priority:
|
|
118007
|
+
priority: 107,
|
|
117738
118008
|
availability: (args) => {
|
|
117739
118009
|
const trimmed = args.trim();
|
|
117740
118010
|
return trimmed === "" || trimmed === "status" || trimmed === "pause" ? "always" : "idle-only";
|
|
@@ -117744,88 +118014,94 @@ const BUILTIN_SLASH_COMMANDS = [
|
|
|
117744
118014
|
name: "goaloff",
|
|
117745
118015
|
aliases: [],
|
|
117746
118016
|
description: "取消并清空当前目标",
|
|
117747
|
-
priority:
|
|
117748
|
-
availability: "always"
|
|
117749
|
-
},
|
|
117750
|
-
{
|
|
117751
|
-
name: "settings",
|
|
117752
|
-
aliases: [],
|
|
117753
|
-
description: "打开 TUI 设置",
|
|
117754
|
-
priority: 103,
|
|
118017
|
+
priority: 106,
|
|
117755
118018
|
availability: "always"
|
|
117756
118019
|
},
|
|
117757
118020
|
{
|
|
117758
118021
|
name: "fork",
|
|
117759
118022
|
aliases: [],
|
|
117760
118023
|
description: "复制当前会话并新开分支",
|
|
117761
|
-
priority:
|
|
118024
|
+
priority: 105
|
|
117762
118025
|
},
|
|
117763
118026
|
{
|
|
117764
118027
|
name: "title",
|
|
117765
118028
|
aliases: ["rename"],
|
|
117766
118029
|
description: "设置或显示会话标题",
|
|
117767
|
-
priority:
|
|
118030
|
+
priority: 104,
|
|
117768
118031
|
availability: "always"
|
|
117769
118032
|
},
|
|
117770
118033
|
{
|
|
117771
|
-
name: "
|
|
118034
|
+
name: "config",
|
|
117772
118035
|
aliases: [],
|
|
117773
|
-
description: "
|
|
117774
|
-
priority:
|
|
118036
|
+
description: "浏览并配置模型(远程拉取最新目录)",
|
|
118037
|
+
priority: 103
|
|
117775
118038
|
},
|
|
117776
118039
|
{
|
|
117777
|
-
name: "
|
|
118040
|
+
name: "permission",
|
|
117778
118041
|
aliases: [],
|
|
117779
|
-
description: "
|
|
117780
|
-
priority:
|
|
118042
|
+
description: "选择权限模式",
|
|
118043
|
+
priority: 102,
|
|
117781
118044
|
availability: "always"
|
|
117782
118045
|
},
|
|
117783
118046
|
{
|
|
117784
|
-
name: "
|
|
118047
|
+
name: "theme",
|
|
117785
118048
|
aliases: [],
|
|
117786
|
-
description: "
|
|
117787
|
-
priority:
|
|
118049
|
+
description: "设置终端 UI 主题",
|
|
118050
|
+
priority: 101,
|
|
117788
118051
|
availability: "always"
|
|
117789
118052
|
},
|
|
117790
118053
|
{
|
|
117791
|
-
name: "
|
|
117792
|
-
aliases: [
|
|
117793
|
-
description: "
|
|
117794
|
-
priority:
|
|
118054
|
+
name: "editor",
|
|
118055
|
+
aliases: [],
|
|
118056
|
+
description: "设置外部编辑器",
|
|
118057
|
+
priority: 100,
|
|
118058
|
+
availability: "always"
|
|
117795
118059
|
},
|
|
117796
118060
|
{
|
|
117797
|
-
name: "
|
|
118061
|
+
name: "settings",
|
|
117798
118062
|
aliases: [],
|
|
117799
|
-
description: "
|
|
117800
|
-
priority:
|
|
117801
|
-
availability: "
|
|
118063
|
+
description: "打开 TUI 设置",
|
|
118064
|
+
priority: 99,
|
|
118065
|
+
availability: "always"
|
|
117802
118066
|
},
|
|
117803
118067
|
{
|
|
117804
|
-
name: "
|
|
117805
|
-
aliases: [
|
|
117806
|
-
description: "
|
|
117807
|
-
priority:
|
|
117808
|
-
|
|
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
|
|
117809
118078
|
},
|
|
117810
118079
|
{
|
|
117811
118080
|
name: "export-debug-zip",
|
|
117812
118081
|
aliases: [],
|
|
117813
118082
|
description: "导出当前会话为调试 ZIP 存档",
|
|
117814
|
-
priority:
|
|
118083
|
+
priority: 96
|
|
117815
118084
|
},
|
|
117816
118085
|
{
|
|
117817
|
-
name: "
|
|
117818
|
-
aliases: [
|
|
117819
|
-
description: "
|
|
117820
|
-
priority:
|
|
118086
|
+
name: "update",
|
|
118087
|
+
aliases: [],
|
|
118088
|
+
description: "手动更新 Scream Code 到最新版本",
|
|
118089
|
+
priority: 95,
|
|
118090
|
+
availability: "idle-only"
|
|
117821
118091
|
},
|
|
117822
118092
|
{
|
|
117823
118093
|
name: "version",
|
|
117824
118094
|
aliases: [],
|
|
117825
118095
|
description: "显示版本信息",
|
|
117826
|
-
priority:
|
|
118096
|
+
priority: 94,
|
|
117827
118097
|
availability: "always"
|
|
117828
118098
|
},
|
|
118099
|
+
{
|
|
118100
|
+
name: "logout",
|
|
118101
|
+
aliases: ["disconnect"],
|
|
118102
|
+
description: "删除已配置的模型",
|
|
118103
|
+
priority: 93
|
|
118104
|
+
},
|
|
117829
118105
|
{
|
|
117830
118106
|
name: "exit",
|
|
117831
118107
|
aliases: ["quit", "q"],
|
|
@@ -119846,11 +120122,36 @@ async function handleAutoCommand(host, args) {
|
|
|
119846
120122
|
host.showNotice("自动模式:开启", "工具自动批准。代理不会提问。");
|
|
119847
120123
|
}
|
|
119848
120124
|
}
|
|
119849
|
-
async function
|
|
119850
|
-
const
|
|
119851
|
-
|
|
119852
|
-
|
|
119853
|
-
|
|
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
|
+
}
|
|
119854
120155
|
}
|
|
119855
120156
|
async function handleCompactCommand(host, args) {
|
|
119856
120157
|
const session = host.session;
|
|
@@ -120575,12 +120876,608 @@ function syncGoalMetadata(host) {
|
|
|
120575
120876
|
session.writeMetadata();
|
|
120576
120877
|
}
|
|
120577
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
|
|
120578
121475
|
//#region src/tui/components/chrome/welcome.ts
|
|
120579
121476
|
const HUE_STOPS = 24;
|
|
120580
121477
|
const SUB_STEPS = 5;
|
|
120581
121478
|
const BREATHE_STEPS = HUE_STOPS * SUB_STEPS;
|
|
120582
121479
|
const BREATHE_INTERVAL_MS = 40;
|
|
120583
|
-
function hexToRgb
|
|
121480
|
+
function hexToRgb(hex) {
|
|
120584
121481
|
return [
|
|
120585
121482
|
parseInt(hex.slice(1, 3), 16),
|
|
120586
121483
|
parseInt(hex.slice(3, 5), 16),
|
|
@@ -120645,7 +121542,7 @@ function rgbToHex(r, g, b) {
|
|
|
120645
121542
|
* it sweeps through all 24 hue stops with smooth sub-step interpolation.
|
|
120646
121543
|
*/
|
|
120647
121544
|
function buildBreathingPalette(primaryHex, hueStops, subSteps) {
|
|
120648
|
-
const [r, g, b] = hexToRgb
|
|
121545
|
+
const [r, g, b] = hexToRgb(primaryHex);
|
|
120649
121546
|
const [baseHue, sat, lit] = rgbToHsl(r, g, b);
|
|
120650
121547
|
const steps = hueStops * subSteps;
|
|
120651
121548
|
const palette = [];
|
|
@@ -120662,6 +121559,7 @@ var WelcomeComponent = class {
|
|
|
120662
121559
|
breatheFrame = 0;
|
|
120663
121560
|
breatheTimer = null;
|
|
120664
121561
|
breathePalette;
|
|
121562
|
+
borderTitle = null;
|
|
120665
121563
|
constructor(state, colors, ui) {
|
|
120666
121564
|
this.state = state;
|
|
120667
121565
|
this.colors = colors;
|
|
@@ -120700,7 +121598,7 @@ var WelcomeComponent = class {
|
|
|
120700
121598
|
const isLoggedOut = !this.state.model;
|
|
120701
121599
|
const dim = chalk.hex(this.colors.textDim);
|
|
120702
121600
|
const labelStyle = chalk.bold.hex(this.colors.textDim);
|
|
120703
|
-
const rightRow1 = truncateToWidth(dim(isLoggedOut ? "运行 /config 开始配置。" : "发送 /
|
|
121601
|
+
const rightRow1 = truncateToWidth(dim(isLoggedOut ? "运行 /config 开始配置。" : "发送 / 进入快捷菜单,/exit 保存并退出"), textWidth, "…");
|
|
120704
121602
|
const headerLines = [logoColor(logo[0].padEnd(logoWidth)) + gap + rightRow0, logoColor(logo[1].padEnd(logoWidth)) + gap + rightRow1];
|
|
120705
121603
|
const activeModel = this.state.availableModels[this.state.model];
|
|
120706
121604
|
const modelValue = isLoggedOut ? chalk.hex(this.colors.warning)("未设置,运行 /config") : activeModel?.displayName ?? activeModel?.model ?? this.state.model;
|
|
@@ -120712,19 +121610,20 @@ var WelcomeComponent = class {
|
|
|
120712
121610
|
labelStyle("模型: ") + modelValue,
|
|
120713
121611
|
labelStyle("版本: ") + versionValue
|
|
120714
121612
|
];
|
|
120715
|
-
const
|
|
120716
|
-
const
|
|
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);
|
|
120717
121616
|
const contentLines = [
|
|
120718
121617
|
...headerLines,
|
|
120719
121618
|
"",
|
|
120720
121619
|
...infoLines,
|
|
120721
121620
|
"",
|
|
120722
|
-
hintLine,
|
|
120723
121621
|
tipLine
|
|
120724
121622
|
];
|
|
121623
|
+
const borderTitle = this.borderTitle;
|
|
120725
121624
|
const lines = [
|
|
120726
121625
|
"",
|
|
120727
|
-
primary("╭" + "─".repeat(width - 2) + "╮"),
|
|
121626
|
+
borderTitle ? primary("╭─ " + borderTitle + " " + "─".repeat(Math.max(0, width - 5 - visibleWidth(borderTitle))) + "╮") : primary("╭" + "─".repeat(width - 2) + "╮"),
|
|
120728
121627
|
primary("│") + " ".repeat(width - 2) + primary("│")
|
|
120729
121628
|
];
|
|
120730
121629
|
for (const content of contentLines) {
|
|
@@ -124725,7 +125624,7 @@ async function confirmUninstall(host, label) {
|
|
|
124725
125624
|
* restores the editor. The question and answer are never recorded in the
|
|
124726
125625
|
* main conversation history.
|
|
124727
125626
|
*/
|
|
124728
|
-
const SPINNER_FRAMES
|
|
125627
|
+
const SPINNER_FRAMES = [
|
|
124729
125628
|
"⠋",
|
|
124730
125629
|
"⠙",
|
|
124731
125630
|
"⠹",
|
|
@@ -124785,7 +125684,7 @@ var BtwOverlayComponent = class extends Container {
|
|
|
124785
125684
|
startSpinner() {
|
|
124786
125685
|
this.spinnerFrame = 0;
|
|
124787
125686
|
this.spinnerInterval = setInterval(() => {
|
|
124788
|
-
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES
|
|
125687
|
+
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
124789
125688
|
this.requestRender();
|
|
124790
125689
|
}, 80);
|
|
124791
125690
|
}
|
|
@@ -124805,7 +125704,7 @@ var BtwOverlayComponent = class extends Container {
|
|
|
124805
125704
|
lines.push(chalk.hex(c.primary)("/btw") + chalk.hex(c.textMuted)(" — ") + chalk.hex(c.text)(truncated));
|
|
124806
125705
|
lines.push("");
|
|
124807
125706
|
if (this.status === "loading") {
|
|
124808
|
-
const spinner = SPINNER_FRAMES
|
|
125707
|
+
const spinner = SPINNER_FRAMES[this.spinnerFrame];
|
|
124809
125708
|
lines.push(chalk.hex(c.textMuted)(`${spinner} Answering…`));
|
|
124810
125709
|
} else if (this.status === "done" && this.markdown !== void 0) {
|
|
124811
125710
|
const contentWidth = Math.max(20, width - 2);
|
|
@@ -124965,8 +125864,8 @@ async function handleBuiltInSlashCommand(host, name, args) {
|
|
|
124965
125864
|
case "plan":
|
|
124966
125865
|
await handlePlanCommand(host, args);
|
|
124967
125866
|
return;
|
|
124968
|
-
case "
|
|
124969
|
-
await
|
|
125867
|
+
case "wolfpack":
|
|
125868
|
+
await handleWolfpackCommand(host, args);
|
|
124970
125869
|
return;
|
|
124971
125870
|
case "revoke":
|
|
124972
125871
|
await handleRevokeCommand(host, args);
|
|
@@ -129971,635 +130870,6 @@ const INITIAL_LIVE_PANE = {
|
|
|
129971
130870
|
pendingQuestion: null
|
|
129972
130871
|
};
|
|
129973
130872
|
//#endregion
|
|
129974
|
-
//#region src/utils/git/git-status.ts
|
|
129975
|
-
/**
|
|
129976
|
-
* Cached git branch + working-tree status for the footer/statusline.
|
|
129977
|
-
*
|
|
129978
|
-
* Branch name refreshes every 5s, porcelain status every 15s. Branch
|
|
129979
|
-
* and status reads stay synchronous with short timeouts. Pull request
|
|
129980
|
-
* lookup uses an async cache so a slow `gh pr view` never blocks
|
|
129981
|
-
* footer rendering.
|
|
129982
|
-
*/
|
|
129983
|
-
const BRANCH_TTL_MS = 5e3;
|
|
129984
|
-
const STATUS_TTL_MS = 15e3;
|
|
129985
|
-
const PULL_REQUEST_TTL_MS = 6e4;
|
|
129986
|
-
const SPAWN_TIMEOUT_MS = 500;
|
|
129987
|
-
const PR_SPAWN_TIMEOUT_MS = 5e3;
|
|
129988
|
-
const AHEAD_BEHIND_RE = /\[(?:ahead (\d+))?(?:, )?(?:behind (\d+))?\]/;
|
|
129989
|
-
function createGitStatusCache(workDir, options = {}) {
|
|
129990
|
-
const isRepo = detectGitRepo(workDir);
|
|
129991
|
-
let branch = {
|
|
129992
|
-
value: null,
|
|
129993
|
-
fetchedAt: 0
|
|
129994
|
-
};
|
|
129995
|
-
let status = {
|
|
129996
|
-
dirty: false,
|
|
129997
|
-
ahead: 0,
|
|
129998
|
-
behind: 0,
|
|
129999
|
-
diffAdded: 0,
|
|
130000
|
-
diffDeleted: 0,
|
|
130001
|
-
fetchedAt: 0
|
|
130002
|
-
};
|
|
130003
|
-
let pullRequest = {
|
|
130004
|
-
value: null,
|
|
130005
|
-
branch: null,
|
|
130006
|
-
fetchedAt: 0,
|
|
130007
|
-
pendingBranch: null,
|
|
130008
|
-
requestId: 0
|
|
130009
|
-
};
|
|
130010
|
-
return { getStatus: () => {
|
|
130011
|
-
if (!isRepo) return null;
|
|
130012
|
-
const now = Date.now();
|
|
130013
|
-
if (now - branch.fetchedAt >= BRANCH_TTL_MS) branch = {
|
|
130014
|
-
value: readBranch(workDir),
|
|
130015
|
-
fetchedAt: now
|
|
130016
|
-
};
|
|
130017
|
-
if (branch.value === null) return null;
|
|
130018
|
-
if (now - status.fetchedAt >= STATUS_TTL_MS) status = {
|
|
130019
|
-
...readStatus(workDir),
|
|
130020
|
-
fetchedAt: now
|
|
130021
|
-
};
|
|
130022
|
-
refreshPullRequestIfNeeded(branch.value, now);
|
|
130023
|
-
return {
|
|
130024
|
-
branch: branch.value,
|
|
130025
|
-
dirty: status.dirty,
|
|
130026
|
-
ahead: status.ahead,
|
|
130027
|
-
behind: status.behind,
|
|
130028
|
-
diffAdded: status.diffAdded,
|
|
130029
|
-
diffDeleted: status.diffDeleted,
|
|
130030
|
-
pullRequest: pullRequest.branch === branch.value ? pullRequest.value : null
|
|
130031
|
-
};
|
|
130032
|
-
} };
|
|
130033
|
-
function refreshPullRequestIfNeeded(branchName, now) {
|
|
130034
|
-
if (pullRequest.pendingBranch === branchName) return;
|
|
130035
|
-
const fetchedAt = pullRequest.branch === branchName ? pullRequest.fetchedAt : 0;
|
|
130036
|
-
if (now - fetchedAt < PULL_REQUEST_TTL_MS) return;
|
|
130037
|
-
const requestId = pullRequest.requestId + 1;
|
|
130038
|
-
pullRequest = {
|
|
130039
|
-
value: pullRequest.branch === branchName ? pullRequest.value : null,
|
|
130040
|
-
branch: branchName,
|
|
130041
|
-
fetchedAt,
|
|
130042
|
-
pendingBranch: branchName,
|
|
130043
|
-
requestId
|
|
130044
|
-
};
|
|
130045
|
-
readPullRequest(workDir).then((value) => {
|
|
130046
|
-
if (pullRequest.requestId !== requestId) return;
|
|
130047
|
-
const changed = !samePullRequest(pullRequest.branch === branchName ? pullRequest.value : null, value);
|
|
130048
|
-
pullRequest = {
|
|
130049
|
-
value,
|
|
130050
|
-
branch: branchName,
|
|
130051
|
-
fetchedAt: Date.now(),
|
|
130052
|
-
pendingBranch: null,
|
|
130053
|
-
requestId
|
|
130054
|
-
};
|
|
130055
|
-
if (changed) options.onChange?.();
|
|
130056
|
-
});
|
|
130057
|
-
}
|
|
130058
|
-
}
|
|
130059
|
-
function detectGitRepo(workDir) {
|
|
130060
|
-
try {
|
|
130061
|
-
const result = spawnSync("git", [
|
|
130062
|
-
"-C",
|
|
130063
|
-
workDir,
|
|
130064
|
-
"rev-parse",
|
|
130065
|
-
"--is-inside-work-tree"
|
|
130066
|
-
], {
|
|
130067
|
-
encoding: "utf8",
|
|
130068
|
-
timeout: SPAWN_TIMEOUT_MS
|
|
130069
|
-
});
|
|
130070
|
-
return result.status === 0 && result.stdout.trim() === "true";
|
|
130071
|
-
} catch {
|
|
130072
|
-
return false;
|
|
130073
|
-
}
|
|
130074
|
-
}
|
|
130075
|
-
function readBranch(workDir) {
|
|
130076
|
-
try {
|
|
130077
|
-
const result = spawnSync("git", [
|
|
130078
|
-
"-C",
|
|
130079
|
-
workDir,
|
|
130080
|
-
"branch",
|
|
130081
|
-
"--show-current"
|
|
130082
|
-
], {
|
|
130083
|
-
encoding: "utf8",
|
|
130084
|
-
timeout: SPAWN_TIMEOUT_MS
|
|
130085
|
-
});
|
|
130086
|
-
if (result.status !== 0) return null;
|
|
130087
|
-
const name = result.stdout.trim();
|
|
130088
|
-
return name.length > 0 ? name : null;
|
|
130089
|
-
} catch {
|
|
130090
|
-
return null;
|
|
130091
|
-
}
|
|
130092
|
-
}
|
|
130093
|
-
function readStatus(workDir) {
|
|
130094
|
-
try {
|
|
130095
|
-
const result = spawnSync("git", [
|
|
130096
|
-
"-C",
|
|
130097
|
-
workDir,
|
|
130098
|
-
"status",
|
|
130099
|
-
"--porcelain",
|
|
130100
|
-
"-b"
|
|
130101
|
-
], {
|
|
130102
|
-
encoding: "utf8",
|
|
130103
|
-
timeout: SPAWN_TIMEOUT_MS,
|
|
130104
|
-
maxBuffer: 4 * 1024 * 1024
|
|
130105
|
-
});
|
|
130106
|
-
if (result.status !== 0) return {
|
|
130107
|
-
dirty: false,
|
|
130108
|
-
ahead: 0,
|
|
130109
|
-
behind: 0,
|
|
130110
|
-
diffAdded: 0,
|
|
130111
|
-
diffDeleted: 0
|
|
130112
|
-
};
|
|
130113
|
-
let dirty = false;
|
|
130114
|
-
let ahead = 0;
|
|
130115
|
-
let behind = 0;
|
|
130116
|
-
for (const line of result.stdout.split("\n")) if (line.startsWith("## ")) {
|
|
130117
|
-
const m = AHEAD_BEHIND_RE.exec(line);
|
|
130118
|
-
if (m) {
|
|
130119
|
-
ahead = Number.parseInt(m[1] ?? "0", 10) || 0;
|
|
130120
|
-
behind = Number.parseInt(m[2] ?? "0", 10) || 0;
|
|
130121
|
-
}
|
|
130122
|
-
} else if (line.trim().length > 0) dirty = true;
|
|
130123
|
-
const diff = dirty ? readDiffStats(workDir) : {
|
|
130124
|
-
added: 0,
|
|
130125
|
-
deleted: 0
|
|
130126
|
-
};
|
|
130127
|
-
return {
|
|
130128
|
-
dirty,
|
|
130129
|
-
ahead,
|
|
130130
|
-
behind,
|
|
130131
|
-
diffAdded: diff.added,
|
|
130132
|
-
diffDeleted: diff.deleted
|
|
130133
|
-
};
|
|
130134
|
-
} catch {
|
|
130135
|
-
return {
|
|
130136
|
-
dirty: false,
|
|
130137
|
-
ahead: 0,
|
|
130138
|
-
behind: 0,
|
|
130139
|
-
diffAdded: 0,
|
|
130140
|
-
diffDeleted: 0
|
|
130141
|
-
};
|
|
130142
|
-
}
|
|
130143
|
-
}
|
|
130144
|
-
function readDiffStats(workDir) {
|
|
130145
|
-
try {
|
|
130146
|
-
const result = spawnSync("git", [
|
|
130147
|
-
"-C",
|
|
130148
|
-
workDir,
|
|
130149
|
-
"diff",
|
|
130150
|
-
"--numstat",
|
|
130151
|
-
"HEAD",
|
|
130152
|
-
"--"
|
|
130153
|
-
], {
|
|
130154
|
-
encoding: "utf8",
|
|
130155
|
-
timeout: SPAWN_TIMEOUT_MS,
|
|
130156
|
-
maxBuffer: 4 * 1024 * 1024
|
|
130157
|
-
});
|
|
130158
|
-
if (result.status !== 0) return {
|
|
130159
|
-
added: 0,
|
|
130160
|
-
deleted: 0
|
|
130161
|
-
};
|
|
130162
|
-
let added = 0;
|
|
130163
|
-
let deleted = 0;
|
|
130164
|
-
for (const line of result.stdout.split("\n")) {
|
|
130165
|
-
if (!line) continue;
|
|
130166
|
-
const [addedText, deletedText] = line.split(" ");
|
|
130167
|
-
added += parseDiffNumstatCount(addedText);
|
|
130168
|
-
deleted += parseDiffNumstatCount(deletedText);
|
|
130169
|
-
}
|
|
130170
|
-
return {
|
|
130171
|
-
added,
|
|
130172
|
-
deleted
|
|
130173
|
-
};
|
|
130174
|
-
} catch {
|
|
130175
|
-
return {
|
|
130176
|
-
added: 0,
|
|
130177
|
-
deleted: 0
|
|
130178
|
-
};
|
|
130179
|
-
}
|
|
130180
|
-
}
|
|
130181
|
-
function parseDiffNumstatCount(value) {
|
|
130182
|
-
if (value === void 0 || value === "-") return 0;
|
|
130183
|
-
const n = Number.parseInt(value, 10);
|
|
130184
|
-
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
130185
|
-
}
|
|
130186
|
-
function readPullRequest(workDir) {
|
|
130187
|
-
return new Promise((resolve) => {
|
|
130188
|
-
try {
|
|
130189
|
-
execFile("gh", [
|
|
130190
|
-
"pr",
|
|
130191
|
-
"view",
|
|
130192
|
-
"--json",
|
|
130193
|
-
"number,url"
|
|
130194
|
-
], {
|
|
130195
|
-
cwd: workDir,
|
|
130196
|
-
encoding: "utf8",
|
|
130197
|
-
env: {
|
|
130198
|
-
...process.env,
|
|
130199
|
-
GH_NO_UPDATE_NOTIFIER: "1",
|
|
130200
|
-
GH_PROMPT_DISABLED: "1"
|
|
130201
|
-
},
|
|
130202
|
-
timeout: PR_SPAWN_TIMEOUT_MS,
|
|
130203
|
-
maxBuffer: 256 * 1024
|
|
130204
|
-
}, (error, stdout) => {
|
|
130205
|
-
if (error !== null) {
|
|
130206
|
-
resolve(null);
|
|
130207
|
-
return;
|
|
130208
|
-
}
|
|
130209
|
-
resolve(parsePullRequest(stdout));
|
|
130210
|
-
});
|
|
130211
|
-
} catch {
|
|
130212
|
-
resolve(null);
|
|
130213
|
-
}
|
|
130214
|
-
});
|
|
130215
|
-
}
|
|
130216
|
-
function samePullRequest(a, b) {
|
|
130217
|
-
if (a === null || b === null) return a === b;
|
|
130218
|
-
return a.number === b.number && a.url === b.url;
|
|
130219
|
-
}
|
|
130220
|
-
function parsePullRequest(stdout) {
|
|
130221
|
-
try {
|
|
130222
|
-
const raw = JSON.parse(stdout);
|
|
130223
|
-
if (typeof raw !== "object" || raw === null) return null;
|
|
130224
|
-
const record = raw;
|
|
130225
|
-
const number = record["number"];
|
|
130226
|
-
const url = record["url"];
|
|
130227
|
-
if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) return null;
|
|
130228
|
-
if (typeof url !== "string" || !isSafeHttpUrl(url)) return null;
|
|
130229
|
-
return {
|
|
130230
|
-
number,
|
|
130231
|
-
url
|
|
130232
|
-
};
|
|
130233
|
-
} catch {
|
|
130234
|
-
return null;
|
|
130235
|
-
}
|
|
130236
|
-
}
|
|
130237
|
-
function isSafeHttpUrl(value) {
|
|
130238
|
-
if (hasControlChars(value)) return false;
|
|
130239
|
-
try {
|
|
130240
|
-
const url = new URL(value);
|
|
130241
|
-
return url.protocol === "https:" || url.protocol === "http:";
|
|
130242
|
-
} catch {
|
|
130243
|
-
return false;
|
|
130244
|
-
}
|
|
130245
|
-
}
|
|
130246
|
-
function hasControlChars(value) {
|
|
130247
|
-
for (const char of value) {
|
|
130248
|
-
const code = char.codePointAt(0) ?? 0;
|
|
130249
|
-
if (code <= 31 || code === 127) return true;
|
|
130250
|
-
}
|
|
130251
|
-
return false;
|
|
130252
|
-
}
|
|
130253
|
-
function formatGitBadgeBase(status) {
|
|
130254
|
-
const parts = [];
|
|
130255
|
-
const diff = formatDiffStats(status);
|
|
130256
|
-
if (diff) parts.push(diff);
|
|
130257
|
-
let sync = "";
|
|
130258
|
-
if (status.ahead > 0) sync += `↑${status.ahead}`;
|
|
130259
|
-
if (status.behind > 0) sync += `↓${status.behind}`;
|
|
130260
|
-
if (sync) parts.push(sync);
|
|
130261
|
-
return parts.length === 0 ? status.branch : `${status.branch} [${parts.join(" ")}]`;
|
|
130262
|
-
}
|
|
130263
|
-
function formatPullRequestBadge(pullRequest, options = {}) {
|
|
130264
|
-
const prText = `[PR#${String(pullRequest.number)}]`;
|
|
130265
|
-
return options.linkPullRequest ? toTerminalHyperlink(prText, pullRequest.url) : prText;
|
|
130266
|
-
}
|
|
130267
|
-
function formatDiffStats(status) {
|
|
130268
|
-
const parts = [];
|
|
130269
|
-
if (status.diffAdded > 0) parts.push(`+${String(status.diffAdded)}`);
|
|
130270
|
-
if (status.diffDeleted > 0) parts.push(`-${String(status.diffDeleted)}`);
|
|
130271
|
-
if (parts.length > 0) return parts.join(" ");
|
|
130272
|
-
return status.dirty ? "±" : null;
|
|
130273
|
-
}
|
|
130274
|
-
function toTerminalHyperlink(text, url) {
|
|
130275
|
-
if (!isSafeHttpUrl(url)) return text;
|
|
130276
|
-
return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`;
|
|
130277
|
-
}
|
|
130278
|
-
//#endregion
|
|
130279
|
-
//#region src/tui/components/chrome/footer.ts
|
|
130280
|
-
const MAX_CWD_SEGMENTS = 3;
|
|
130281
|
-
const TIP_ROTATE_INTERVAL_MS = 1e4;
|
|
130282
|
-
const TIP_SEPARATOR = " | ";
|
|
130283
|
-
const TOOLBAR_TIPS = [
|
|
130284
|
-
{ text: "shift+tab: 计划模式" },
|
|
130285
|
-
{ text: "/model: 切换模型" },
|
|
130286
|
-
{
|
|
130287
|
-
text: "ctrl+s: 中途干预",
|
|
130288
|
-
priority: 2
|
|
130289
|
-
},
|
|
130290
|
-
{
|
|
130291
|
-
text: "/compact: 压缩上下文",
|
|
130292
|
-
priority: 2
|
|
130293
|
-
},
|
|
130294
|
-
{ text: "ctrl+o: 展开工具输出" },
|
|
130295
|
-
{ text: "/tasks: 后台任务" },
|
|
130296
|
-
{ text: "shift+enter: 换行" },
|
|
130297
|
-
{
|
|
130298
|
-
text: "/init: 生成 AGENTS.md",
|
|
130299
|
-
priority: 2
|
|
130300
|
-
},
|
|
130301
|
-
{ text: "@: 提及文件" },
|
|
130302
|
-
{ text: "ctrl+c: 取消" },
|
|
130303
|
-
{ text: "/theme: 切换主题" },
|
|
130304
|
-
{ text: "/auto: 自动权限模式" },
|
|
130305
|
-
{ text: "/yes: 自动批准" },
|
|
130306
|
-
{ text: "/help: 显示命令" },
|
|
130307
|
-
{
|
|
130308
|
-
text: "/config: 选择并配置你常用的模型商",
|
|
130309
|
-
solo: true,
|
|
130310
|
-
priority: 3
|
|
130311
|
-
},
|
|
130312
|
-
{
|
|
130313
|
-
text: "让 Scream 安排任务,例如 \"2个小时后提醒我去拿快递\"",
|
|
130314
|
-
solo: true,
|
|
130315
|
-
priority: 3
|
|
130316
|
-
}
|
|
130317
|
-
];
|
|
130318
|
-
/**
|
|
130319
|
-
* Expand tips into a rotation sequence using smooth weighted round-robin
|
|
130320
|
-
* (the nginx SWRR algorithm). Higher-`priority` tips appear more often while
|
|
130321
|
-
* staying evenly spread, so a tip generally does not land next to its own
|
|
130322
|
-
* duplicate. Deterministic and computed once at module load. Exported for
|
|
130323
|
-
* unit testing.
|
|
130324
|
-
*/
|
|
130325
|
-
function buildWeightedTips(tips) {
|
|
130326
|
-
const items = tips.map((t) => ({
|
|
130327
|
-
tip: t,
|
|
130328
|
-
weight: Math.max(1, Math.trunc(t.priority ?? 1)),
|
|
130329
|
-
current: 0
|
|
130330
|
-
}));
|
|
130331
|
-
const total = items.reduce((sum, it) => sum + it.weight, 0);
|
|
130332
|
-
const seq = [];
|
|
130333
|
-
for (let n = 0; n < total; n++) {
|
|
130334
|
-
let best = items[0];
|
|
130335
|
-
for (const it of items) {
|
|
130336
|
-
it.current += it.weight;
|
|
130337
|
-
if (it.current > best.current) best = it;
|
|
130338
|
-
}
|
|
130339
|
-
best.current -= total;
|
|
130340
|
-
seq.push(best.tip);
|
|
130341
|
-
}
|
|
130342
|
-
return seq;
|
|
130343
|
-
}
|
|
130344
|
-
const ROTATION = buildWeightedTips(TOOLBAR_TIPS);
|
|
130345
|
-
function currentTipIndex() {
|
|
130346
|
-
return Math.floor(Date.now() / TIP_ROTATE_INTERVAL_MS);
|
|
130347
|
-
}
|
|
130348
|
-
/**
|
|
130349
|
-
* Pick the tip(s) for a rotation index over the weighted ROTATION sequence.
|
|
130350
|
-
* `primary` is always shown when it fits; `pair` (primary + next tip joined
|
|
130351
|
-
* by the separator) is offered for wide terminals. Pairing is skipped when
|
|
130352
|
-
* the current/next tip is `solo` or when the neighbour is a duplicate of the
|
|
130353
|
-
* current tip (which can happen at the wrap boundary), keeping long/important
|
|
130354
|
-
* tips on their own and avoiding "X | X".
|
|
130355
|
-
*/
|
|
130356
|
-
function tipsForIndex(index) {
|
|
130357
|
-
const n = ROTATION.length;
|
|
130358
|
-
if (n === 0) return {
|
|
130359
|
-
primary: "",
|
|
130360
|
-
pair: null
|
|
130361
|
-
};
|
|
130362
|
-
const offset = (index % n + n) % n;
|
|
130363
|
-
const current = ROTATION[offset];
|
|
130364
|
-
if (n === 1 || current.solo) return {
|
|
130365
|
-
primary: current.text,
|
|
130366
|
-
pair: null
|
|
130367
|
-
};
|
|
130368
|
-
const next = ROTATION[(offset + 1) % n];
|
|
130369
|
-
if (next.solo || next.text === current.text) return {
|
|
130370
|
-
primary: current.text,
|
|
130371
|
-
pair: null
|
|
130372
|
-
};
|
|
130373
|
-
return {
|
|
130374
|
-
primary: current.text,
|
|
130375
|
-
pair: current.text + TIP_SEPARATOR + next.text
|
|
130376
|
-
};
|
|
130377
|
-
}
|
|
130378
|
-
function shortenModel(model) {
|
|
130379
|
-
if (!model) return model;
|
|
130380
|
-
const slash = model.lastIndexOf("/");
|
|
130381
|
-
return slash >= 0 ? model.slice(slash + 1) : model;
|
|
130382
|
-
}
|
|
130383
|
-
function modelDisplayName(state) {
|
|
130384
|
-
const model = state.availableModels[state.model];
|
|
130385
|
-
return model?.displayName ?? model?.model ?? state.model;
|
|
130386
|
-
}
|
|
130387
|
-
function shortenCwd(path) {
|
|
130388
|
-
if (!path) return path;
|
|
130389
|
-
const home = process.env["HOME"] ?? "";
|
|
130390
|
-
let work = path;
|
|
130391
|
-
if (home && path === home) return "~";
|
|
130392
|
-
if (home && path.startsWith(home + "/")) work = "~" + path.slice(home.length);
|
|
130393
|
-
const segments = work.split("/").filter((s) => s.length > 0);
|
|
130394
|
-
if (segments.length <= MAX_CWD_SEGMENTS) return work;
|
|
130395
|
-
return `…/${segments.slice(-3).join("/")}`;
|
|
130396
|
-
}
|
|
130397
|
-
function formatTokenCount(n) {
|
|
130398
|
-
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
130399
|
-
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
130400
|
-
return String(n);
|
|
130401
|
-
}
|
|
130402
|
-
function safeUsage(usage) {
|
|
130403
|
-
return safeUsageRatio(usage);
|
|
130404
|
-
}
|
|
130405
|
-
function formatContextStatus(usage, tokens, maxTokens) {
|
|
130406
|
-
const pct = `${(safeUsage(usage) * 100).toFixed(1)}%`;
|
|
130407
|
-
if (maxTokens && maxTokens > 0 && tokens !== void 0) return `上下文:${pct} (${formatTokenCount(tokens)}/${formatTokenCount(maxTokens)})`;
|
|
130408
|
-
return `上下文:${pct}`;
|
|
130409
|
-
}
|
|
130410
|
-
const BRAND_COLORS = [
|
|
130411
|
-
"#72A4E9",
|
|
130412
|
-
"#A78BFA",
|
|
130413
|
-
"#34D399"
|
|
130414
|
-
];
|
|
130415
|
-
const GRADIENT_CYCLE_MS = 4e3;
|
|
130416
|
-
const SPINNER_FRAMES = [
|
|
130417
|
-
"●",
|
|
130418
|
-
"◉",
|
|
130419
|
-
"◎",
|
|
130420
|
-
"◌",
|
|
130421
|
-
"○",
|
|
130422
|
-
"◌",
|
|
130423
|
-
"◎",
|
|
130424
|
-
"◉"
|
|
130425
|
-
];
|
|
130426
|
-
const SPINNER_TICK_MS = 120;
|
|
130427
|
-
function hexToRgb(hex) {
|
|
130428
|
-
const v = parseInt(hex.slice(1), 16);
|
|
130429
|
-
return [
|
|
130430
|
-
v >> 16 & 255,
|
|
130431
|
-
v >> 8 & 255,
|
|
130432
|
-
v & 255
|
|
130433
|
-
];
|
|
130434
|
-
}
|
|
130435
|
-
function lerpGradient(t) {
|
|
130436
|
-
const count = BRAND_COLORS.length;
|
|
130437
|
-
const segment = Math.min(t * count, count - 1);
|
|
130438
|
-
const idx = Math.floor(segment);
|
|
130439
|
-
const localT = segment - idx;
|
|
130440
|
-
const nextIdx = (idx + 1) % count;
|
|
130441
|
-
const [r0, g0, b0] = hexToRgb(BRAND_COLORS[idx]);
|
|
130442
|
-
const [r1, g1, b1] = hexToRgb(BRAND_COLORS[nextIdx]);
|
|
130443
|
-
const r = Math.round(r0 + (r1 - r0) * localT);
|
|
130444
|
-
const g = Math.round(g0 + (g1 - g0) * localT);
|
|
130445
|
-
const b = Math.round(b0 + (b1 - b0) * localT);
|
|
130446
|
-
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
130447
|
-
}
|
|
130448
|
-
const POWER_GRADIENT_CYCLE_MS = 2e3;
|
|
130449
|
-
/** Each character of `text` gets a phase-offset colour from the brand gradient, creating a flowing rainbow effect. */
|
|
130450
|
-
function gradientText(text, now) {
|
|
130451
|
-
const t = now % POWER_GRADIENT_CYCLE_MS / POWER_GRADIENT_CYCLE_MS;
|
|
130452
|
-
let out = "";
|
|
130453
|
-
for (let i = 0; i < text.length; i++) {
|
|
130454
|
-
const charT = (t + i / text.length) % 1;
|
|
130455
|
-
out += chalk.hex(lerpGradient(charT))(text[i]);
|
|
130456
|
-
}
|
|
130457
|
-
return chalk.bold(out);
|
|
130458
|
-
}
|
|
130459
|
-
function buildStatusLine(streamingPhase, livePaneMode, streamingStartTime) {
|
|
130460
|
-
if (streamingPhase === "idle" && livePaneMode !== "tool") return "○ 空闲";
|
|
130461
|
-
let label;
|
|
130462
|
-
if (livePaneMode === "tool") label = "执行中";
|
|
130463
|
-
else if (streamingPhase === "waiting") label = "等待响应";
|
|
130464
|
-
else if (streamingPhase === "thinking") label = "思考中";
|
|
130465
|
-
else if (streamingPhase === "composing") label = "输出中";
|
|
130466
|
-
else label = "";
|
|
130467
|
-
const elapsed = Date.now() - streamingStartTime;
|
|
130468
|
-
const totalSeconds = Math.floor(elapsed / 1e3);
|
|
130469
|
-
const elapsedStr = totalSeconds < 60 ? `${totalSeconds}s` : `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
|
|
130470
|
-
const now = Date.now();
|
|
130471
|
-
const frame = SPINNER_FRAMES[Math.floor(now / SPINNER_TICK_MS) % SPINNER_FRAMES.length];
|
|
130472
|
-
const gradientColor = lerpGradient(now % GRADIENT_CYCLE_MS / GRADIENT_CYCLE_MS);
|
|
130473
|
-
return chalk.hex(gradientColor).bold(frame) + " " + label + " " + elapsedStr;
|
|
130474
|
-
}
|
|
130475
|
-
function formatFooterGitBadge(status, colors) {
|
|
130476
|
-
const base = chalk.hex(colors.status)(formatGitBadgeBase(status));
|
|
130477
|
-
if (status.pullRequest === null) return base;
|
|
130478
|
-
return `${base} ${chalk.hex(colors.primary)(formatPullRequestBadge(status.pullRequest, { linkPullRequest: true }))}`;
|
|
130479
|
-
}
|
|
130480
|
-
var FooterComponent = class {
|
|
130481
|
-
state;
|
|
130482
|
-
colors;
|
|
130483
|
-
onGitStatusChange;
|
|
130484
|
-
gitCache;
|
|
130485
|
-
gitCacheWorkDir;
|
|
130486
|
-
transientHint = null;
|
|
130487
|
-
/**
|
|
130488
|
-
* Non-terminal background-task counts split by kind so the footer can
|
|
130489
|
-
* render two distinct badges. `bashTasks` covers `bash-*` BPM tasks
|
|
130490
|
-
* spawned via `Shell run_in_background=true`; `agentTasks` covers
|
|
130491
|
-
* `agent-*` BPM tasks (background subagents). Either zero hides its
|
|
130492
|
-
* respective badge.
|
|
130493
|
-
*/
|
|
130494
|
-
backgroundBashTaskCount = 0;
|
|
130495
|
-
backgroundAgentCount = 0;
|
|
130496
|
-
powerGradientTimer;
|
|
130497
|
-
constructor(state, colors, onGitStatusChange = () => {}) {
|
|
130498
|
-
this.state = state;
|
|
130499
|
-
this.colors = colors;
|
|
130500
|
-
this.onGitStatusChange = onGitStatusChange;
|
|
130501
|
-
this.gitCacheWorkDir = state.workDir;
|
|
130502
|
-
this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
|
|
130503
|
-
}
|
|
130504
|
-
setState(state) {
|
|
130505
|
-
if (state.workDir !== this.gitCacheWorkDir) {
|
|
130506
|
-
this.gitCacheWorkDir = state.workDir;
|
|
130507
|
-
this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
|
|
130508
|
-
}
|
|
130509
|
-
this.state = state;
|
|
130510
|
-
}
|
|
130511
|
-
startPowerGradient() {
|
|
130512
|
-
if (this.powerGradientTimer !== void 0) return;
|
|
130513
|
-
this.powerGradientTimer = setInterval(() => {
|
|
130514
|
-
this.onGitStatusChange();
|
|
130515
|
-
}, 80);
|
|
130516
|
-
}
|
|
130517
|
-
stopPowerGradient() {
|
|
130518
|
-
if (this.powerGradientTimer === void 0) return;
|
|
130519
|
-
clearInterval(this.powerGradientTimer);
|
|
130520
|
-
this.powerGradientTimer = void 0;
|
|
130521
|
-
}
|
|
130522
|
-
setColors(colors) {
|
|
130523
|
-
this.colors = colors;
|
|
130524
|
-
}
|
|
130525
|
-
/**
|
|
130526
|
-
* Short-lived hint that replaces the rotating toolbar tips on line 1.
|
|
130527
|
-
* Used by the exit-confirmation double-tap flow to show "Press Ctrl+C
|
|
130528
|
-
* again to exit" without requiring a toast/overlay subsystem.
|
|
130529
|
-
* Pass `null` to clear.
|
|
130530
|
-
*/
|
|
130531
|
-
setTransientHint(hint) {
|
|
130532
|
-
this.transientHint = hint;
|
|
130533
|
-
}
|
|
130534
|
-
/**
|
|
130535
|
-
* Sync both background-task badges with live counts. Each non-zero
|
|
130536
|
-
* count produces its own bracketed badge on line 1; zeros hide them
|
|
130537
|
-
* independently.
|
|
130538
|
-
*/
|
|
130539
|
-
setBackgroundCounts(counts) {
|
|
130540
|
-
this.backgroundBashTaskCount = Math.max(0, counts.bashTasks);
|
|
130541
|
-
this.backgroundAgentCount = Math.max(0, counts.agentTasks);
|
|
130542
|
-
}
|
|
130543
|
-
invalidate() {}
|
|
130544
|
-
render(width) {
|
|
130545
|
-
const colors = this.colors;
|
|
130546
|
-
const state = this.state;
|
|
130547
|
-
const left = [];
|
|
130548
|
-
if (state.permissionMode === "auto") left.push(chalk.hex(colors.warning).bold("auto"));
|
|
130549
|
-
if (state.permissionMode === "yolo") left.push(chalk.hex(colors.warning).bold("YES"));
|
|
130550
|
-
if (state.planMode) left.push(chalk.hex(colors.primary).bold("plan"));
|
|
130551
|
-
if (state.parallelMode) left.push(gradientText("power", Date.now()));
|
|
130552
|
-
if (state.goalActive && state.goal) {
|
|
130553
|
-
const goalText = state.goal.length > 20 ? state.goal.slice(0, 20) + "…" : state.goal;
|
|
130554
|
-
left.push(chalk.hex(colors.success).bold(`🎯 ${goalText}`));
|
|
130555
|
-
}
|
|
130556
|
-
const model = shortenModel(modelDisplayName(state));
|
|
130557
|
-
if (model) {
|
|
130558
|
-
const thinkingLabel = state.thinking ? " 思考中" : "";
|
|
130559
|
-
left.push(chalk.hex(colors.text)(`${model}${thinkingLabel}`));
|
|
130560
|
-
}
|
|
130561
|
-
if (this.backgroundBashTaskCount > 0) {
|
|
130562
|
-
const noun = this.backgroundBashTaskCount === 1 ? "个任务" : "个任务";
|
|
130563
|
-
left.push(chalk.hex(colors.primary)(`[${String(this.backgroundBashTaskCount)}${noun} 运行中]`));
|
|
130564
|
-
}
|
|
130565
|
-
if (this.backgroundAgentCount > 0) {
|
|
130566
|
-
const noun = this.backgroundAgentCount === 1 ? "个代理" : "个代理";
|
|
130567
|
-
left.push(chalk.hex(colors.primary)(`[${String(this.backgroundAgentCount)}${noun} 运行中]`));
|
|
130568
|
-
}
|
|
130569
|
-
const cwd = shortenCwd(state.workDir);
|
|
130570
|
-
if (cwd) left.push(chalk.hex(colors.status)(cwd));
|
|
130571
|
-
const git = this.gitCache.getStatus();
|
|
130572
|
-
if (git !== null) left.push(formatFooterGitBadge(git, colors));
|
|
130573
|
-
const leftLine = left.join(" ");
|
|
130574
|
-
const leftWidth = visibleWidth(leftLine);
|
|
130575
|
-
const { primary, pair } = tipsForIndex(currentTipIndex());
|
|
130576
|
-
const remaining = Math.max(0, width - leftWidth - 2);
|
|
130577
|
-
let tipText = "";
|
|
130578
|
-
if (pair && visibleWidth(pair) <= remaining) tipText = pair;
|
|
130579
|
-
else if (primary && visibleWidth(primary) <= remaining) tipText = primary;
|
|
130580
|
-
let line1;
|
|
130581
|
-
if (tipText) {
|
|
130582
|
-
const pad = width - leftWidth - visibleWidth(tipText);
|
|
130583
|
-
line1 = leftLine + " ".repeat(Math.max(0, pad)) + chalk.hex(colors.textMuted)(tipText);
|
|
130584
|
-
} else if (leftWidth <= width) line1 = leftLine;
|
|
130585
|
-
else line1 = truncateToWidth(leftLine, width, "…");
|
|
130586
|
-
const statusLine = buildStatusLine(state.streamingPhase, state.livePaneMode, state.streamingStartTime);
|
|
130587
|
-
const rightText = (state.ccConnectActive ? chalk.hex(colors.success)("●") : chalk.hex(colors.textDim)("●")) + " " + formatContextStatus(state.contextUsage, state.contextTokens, state.maxContextTokens) + " " + statusLine;
|
|
130588
|
-
const rightWidth = visibleWidth(rightText);
|
|
130589
|
-
let line2;
|
|
130590
|
-
if (this.transientHint) {
|
|
130591
|
-
const hintText = chalk.hex(colors.warning).bold(this.transientHint);
|
|
130592
|
-
const hintWidth = visibleWidth(hintText);
|
|
130593
|
-
const pad = Math.max(2, width - hintWidth - rightWidth);
|
|
130594
|
-
line2 = hintText + " ".repeat(pad) + chalk.hex(colors.textDim)(rightText);
|
|
130595
|
-
} else {
|
|
130596
|
-
const lpad = Math.max(0, width - rightWidth);
|
|
130597
|
-
line2 = " ".repeat(lpad) + chalk.hex(colors.textDim)(rightText);
|
|
130598
|
-
}
|
|
130599
|
-
return [truncateToWidth(line1, width), truncateToWidth(line2, width)];
|
|
130600
|
-
}
|
|
130601
|
-
};
|
|
130602
|
-
//#endregion
|
|
130603
130873
|
//#region src/tui/components/chrome/todo-panel.ts
|
|
130604
130874
|
const MAX_VISIBLE = 5;
|
|
130605
130875
|
/**
|
|
@@ -133963,7 +134233,7 @@ function createInitialAppState(input) {
|
|
|
133963
134233
|
goalActive: false,
|
|
133964
134234
|
goalContinuationCount: 0,
|
|
133965
134235
|
ccConnectActive: false,
|
|
133966
|
-
|
|
134236
|
+
wolfpackMode: false
|
|
133967
134237
|
};
|
|
133968
134238
|
}
|
|
133969
134239
|
var ScreamTUI = class {
|
|
@@ -134380,9 +134650,7 @@ var ScreamTUI = class {
|
|
|
134380
134650
|
imageAttachmentIds
|
|
134381
134651
|
});
|
|
134382
134652
|
this.beginSessionRequest();
|
|
134383
|
-
|
|
134384
|
-
if (this.state.appState.parallelMode && options?.parts === void 0) modelInput = "[系统指令:Power 并行模式已激活。你必须将用户的任务拆解为多个独立的子任务,并在同一个回复中同时调用多个 Agent 工具并行执行。每个 Agent 处理一个子任务,系统自动并发。不要逐个串行调用 Agent——除非子任务之间有严格的顺序依赖或会写入同一文件。即使是简单任务,也至少拆成 2 个 Agent 并行。优先并行,宁可多拆不要少拆。]\n\n" + input;
|
|
134385
|
-
session.prompt(modelInput).catch((error) => {
|
|
134653
|
+
session.prompt(options?.parts ?? input).catch((error) => {
|
|
134386
134654
|
const message = formatErrorMessage(error);
|
|
134387
134655
|
this.failSessionRequest(`发送失败:${message}`);
|
|
134388
134656
|
});
|
|
@@ -134479,12 +134747,9 @@ var ScreamTUI = class {
|
|
|
134479
134747
|
setAppState(patch) {
|
|
134480
134748
|
if (!hasPatchChanges(this.state.appState, patch)) return;
|
|
134481
134749
|
const busyChanged = "streamingPhase" in patch || "isCompacting" in patch;
|
|
134482
|
-
const prevParallelMode = this.state.appState.parallelMode;
|
|
134483
134750
|
Object.assign(this.state.appState, patch);
|
|
134484
134751
|
if ("planMode" in patch) this.updateEditorBorderHighlight();
|
|
134485
134752
|
if ("streamingPhase" in patch && patch.streamingPhase !== "idle") this.welcomeComponent?.stopBreathing();
|
|
134486
|
-
if ("parallelMode" in patch && patch.parallelMode !== prevParallelMode) if (patch.parallelMode) this.state.footer.startPowerGradient();
|
|
134487
|
-
else this.state.footer.stopPowerGradient();
|
|
134488
134753
|
this.state.footer.setState(this.state.appState);
|
|
134489
134754
|
this.updateActivityPane();
|
|
134490
134755
|
if (busyChanged) this.updateQueueDisplay();
|
|
@@ -134627,6 +134892,7 @@ var ScreamTUI = class {
|
|
|
134627
134892
|
renderWelcome() {
|
|
134628
134893
|
this.welcomeComponent?.stopBreathing();
|
|
134629
134894
|
const welcome = new WelcomeComponent(this.state.appState, this.state.theme.colors, this.state.ui);
|
|
134895
|
+
welcome.borderTitle = "Scream Code";
|
|
134630
134896
|
this.welcomeComponent = welcome;
|
|
134631
134897
|
if (this.state.editor.hasFirstInputFired()) welcome.stopBreathing();
|
|
134632
134898
|
this.state.transcriptContainer.addChild(welcome);
|
|
@@ -134900,6 +135166,193 @@ var ScreamTUI = class {
|
|
|
134900
135166
|
}
|
|
134901
135167
|
};
|
|
134902
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
|
|
134903
135356
|
//#region src/cli/run-shell.ts
|
|
134904
135357
|
async function runShell(opts, version) {
|
|
134905
135358
|
const startedAt = Date.now();
|
|
@@ -134936,6 +135389,7 @@ async function runShell(opts, version) {
|
|
|
134936
135389
|
await harness.ensureConfigFile();
|
|
134937
135390
|
const config = await harness.getConfig();
|
|
134938
135391
|
const configMs = Date.now() - configStartedAt;
|
|
135392
|
+
await runLoadingAnimation(resolvedTheme);
|
|
134939
135393
|
const tui = new ScreamTUI(harness, {
|
|
134940
135394
|
cliOptions: opts,
|
|
134941
135395
|
tuiConfig,
|