scream-code 0.5.1 → 0.5.3-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.mjs +929 -743
  2. package/package.json +2 -2
package/dist/main.mjs CHANGED
@@ -56244,6 +56244,15 @@ function buildGoalCompletionSummaryPrompt(goal) {
56244
56244
  "Write a concise final message for the user. State that the goal is complete, summarize the main work completed, and mention any validation you ran. Do not call more goal tools."
56245
56245
  ].join("\n");
56246
56246
  }
56247
+ function buildGradingFeedbackPrompt(reason) {
56248
+ return [
56249
+ "Goal verification failed. An independent reviewer found that the completion criteria were not genuinely met.",
56250
+ "",
56251
+ `Reviewer feedback:\n${reason}`,
56252
+ "",
56253
+ "Address every issue listed above before calling UpdateGoal with complete again. Do not re-submit until all issues are resolved."
56254
+ ].join("\n");
56255
+ }
56247
56256
  function buildGoalBlockedReasonPrompt(goal) {
56248
56257
  return [
56249
56258
  buildGoalBlockedMessage(goal),
@@ -56279,13 +56288,28 @@ const UpdateGoalToolInputSchema = z.object({ status: z.enum([
56279
56288
  "paused",
56280
56289
  "blocked"
56281
56290
  ]).describe("The lifecycle status to set for the current goal.") }).strict();
56291
+ const MAX_GRADER_OUTPUT_CHARS = 4e3;
56292
+ function extractRecentOutput(history) {
56293
+ const parts = [];
56294
+ for (let i = history.length - 1; i >= 0; i--) {
56295
+ const msg = history[i];
56296
+ if (msg === void 0 || msg.role !== "assistant") continue;
56297
+ const text = msg.content.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
56298
+ if (text) parts.unshift(text);
56299
+ if (parts.join("\n\n").length >= MAX_GRADER_OUTPUT_CHARS) break;
56300
+ }
56301
+ const joined = parts.join("\n\n");
56302
+ return joined.length > MAX_GRADER_OUTPUT_CHARS ? `${joined.slice(0, MAX_GRADER_OUTPUT_CHARS)}…` : joined;
56303
+ }
56282
56304
  var UpdateGoalTool = class {
56283
56305
  agent;
56306
+ grader;
56284
56307
  name = "UpdateGoal";
56285
56308
  description = "Update the current goal's lifecycle status. Use `complete` when the goal is achieved, `blocked` when you cannot proceed, `paused` to park it, or `active` to resume.";
56286
56309
  parameters = toInputJsonSchema(UpdateGoalToolInputSchema);
56287
- constructor(agent) {
56310
+ constructor(agent, grader) {
56288
56311
  this.agent = agent;
56312
+ this.grader = grader;
56289
56313
  }
56290
56314
  resolveExecution(args) {
56291
56315
  const goal = this.agent.goal;
@@ -56297,17 +56321,7 @@ var UpdateGoalTool = class {
56297
56321
  await goal.resumeGoal({}, "model");
56298
56322
  return { output: "Goal resumed." };
56299
56323
  }
56300
- if (args.status === "complete") {
56301
- const completed = await goal.markComplete({}, "model");
56302
- if (completed !== null) this.agent.context.appendSystemReminder(buildGoalCompletionSummaryPrompt(completed), {
56303
- kind: "system_trigger",
56304
- name: GOAL_COMPLETION_REMINDER_NAME
56305
- });
56306
- return {
56307
- output: "Goal marked complete.",
56308
- stopTurn: true
56309
- };
56310
- }
56324
+ if (args.status === "complete") return this.handleComplete(goal);
56311
56325
  if (args.status === "blocked") {
56312
56326
  const blocked = await goal.markBlocked({}, "model");
56313
56327
  if (blocked !== null) this.agent.context.appendSystemReminder(buildGoalBlockedReasonPrompt(blocked), {
@@ -56327,6 +56341,39 @@ var UpdateGoalTool = class {
56327
56341
  }
56328
56342
  };
56329
56343
  }
56344
+ async handleComplete(goal) {
56345
+ const goalState = goal.getGoal().goal;
56346
+ if (!goalState) return { output: "No active goal." };
56347
+ const output = extractRecentOutput(this.agent.context.history);
56348
+ await goal.pauseGoal({ reason: "verifying" }, "system");
56349
+ let pass;
56350
+ let reason;
56351
+ try {
56352
+ const result = await this.grader(goalState.objective, goalState.completionCriterion, output);
56353
+ pass = result.pass;
56354
+ reason = result.reason;
56355
+ } catch {
56356
+ pass = true;
56357
+ reason = "Grader unavailable";
56358
+ }
56359
+ await goal.resumeGoal({}, "system");
56360
+ if (pass) {
56361
+ const completed = await goal.markComplete({}, "model");
56362
+ if (completed !== null) this.agent.context.appendSystemReminder(buildGoalCompletionSummaryPrompt(completed), {
56363
+ kind: "system_trigger",
56364
+ name: GOAL_COMPLETION_REMINDER_NAME
56365
+ });
56366
+ return {
56367
+ output: `Goal verified and marked complete.\n${reason}`,
56368
+ stopTurn: true
56369
+ };
56370
+ }
56371
+ this.agent.context.appendSystemReminder(buildGradingFeedbackPrompt(reason), {
56372
+ kind: "system_trigger",
56373
+ name: "goal_grading_feedback"
56374
+ });
56375
+ return { output: `Verification failed: ${reason}. Continue working.` };
56376
+ }
56330
56377
  };
56331
56378
  //#endregion
56332
56379
  //#region ../../packages/agent-core/src/tools/builtin/goal/write-goal-note.ts
@@ -75991,6 +76038,8 @@ function buildGoalReminder(goal) {
75991
76038
  lines.push("When you discover important facts, verify a hypothesis, or hit a dead end, call WriteGoalNote to record it. Future turns will read these notes automatically. Keep notes concise and actionable.");
75992
76039
  lines.push("");
75993
76040
  lines.push("Goal mode is iterative. Keep the self-audit brief each turn. Do not explore unrelated interpretations once the goal can be decided. If the objective is simple, already answered, impossible, unsafe, or contradictory, do not run another goal turn. Explain briefly if useful, then call UpdateGoal with `complete` or `blocked` in the same turn. Otherwise, self-audit against the objective and any completion criteria above, then do one coherent slice of work toward the objective. Use multiple turns when the task naturally has multiple phases. Call UpdateGoal with `complete` only when all required work is done, any stated validation has passed, and there is no useful next action. Do not mark complete after only producing a plan, summary, first pass, or partial result. If an external condition or required user input prevents progress, or the objective cannot be completed as stated, call UpdateGoal with `blocked`. Otherwise keep working — after your turn ends you will be prompted to continue. Call UpdateGoal as soon as the goal is genuinely done or cannot proceed; don't keep going once there is nothing left to do.");
76041
+ lines.push("");
76042
+ lines.push("When you call UpdateGoal with `complete`, an independent reviewer will verify that the completion criteria are met. Provide a clear summary of what you accomplished in your final response so the reviewer can evaluate it.");
75994
76043
  return lines.join("\n");
75995
76044
  }
75996
76045
  function maxBudgetFraction(goal) {
@@ -76488,7 +76537,12 @@ const DEFAULT_APPROVE_TOOLS = new Set([
76488
76537
  "Agent",
76489
76538
  "AskUserQuestion",
76490
76539
  "Skill",
76491
- "WolfPack"
76540
+ "WolfPack",
76541
+ "CreateGoal",
76542
+ "UpdateGoal",
76543
+ "GetGoal",
76544
+ "SetGoalBudget",
76545
+ "WriteGoalNote"
76492
76546
  ]);
76493
76547
  var DefaultToolApprovePermissionPolicy = class {
76494
76548
  name = "default-tool-approve";
@@ -90872,6 +90926,151 @@ const DEFAULT_AGENT_PROFILES = loadAgentProfilesFromSources([
90872
90926
  ].map((file) => `profile/default/${file}`), PROFILE_SOURCES);
90873
90927
  //#endregion
90874
90928
  //#region ../../packages/agent-core/src/agent/tool/index.ts
90929
+ const CRITERIA_SYSTEM_PROMPT = [
90930
+ "You generate concrete, verifiable acceptance criteria for a given objective.",
90931
+ "Criteria must be specific and testable — state what should work end-to-end, not just what should exist.",
90932
+ "Avoid vague criteria like \"feature works\" or \"code is correct\".",
90933
+ "Respond with JSON only: {\"criteria\": [\"criterion 1\", \"criterion 2\", ...]}"
90934
+ ].join(" ");
90935
+ const GRADER_SYSTEM_PROMPT = [
90936
+ "You are a strict goal completion evaluator. Your default judgment is FAIL. Only PASS when there is clear, specific evidence that every acceptance criterion is genuinely met end-to-end.",
90937
+ "Evaluate across three dimensions:",
90938
+ "- Completeness: every acceptance criterion is individually met with concrete evidence in the output. Partial completion is FAIL.",
90939
+ "- Conformance: the work matches what was asked — no scope drift, no over-engineering, no cutting corners.",
90940
+ "- Substance: the output is real, finished, working work — not just a plan, outline, scaffold, stub, mock, or partial implementation, unless the objective specifically asks for those. Surface-level appearance without end-to-end correctness is FAIL.",
90941
+ "When FAIL, you MUST list specific issues with actionable fix directions. Do not accept plausible-sounding but unverified claims of completion.",
90942
+ "Respond with JSON only."
90943
+ ].join(" ");
90944
+ function buildCriteriaPrompt(objective) {
90945
+ return [
90946
+ "## Objective",
90947
+ objective,
90948
+ "",
90949
+ "Generate 3-8 concrete, verifiable acceptance criteria for this objective.",
90950
+ "Each criterion should describe a specific, testable behavior or outcome — focus on end-to-end correctness, not surface existence.",
90951
+ "Respond with JSON: {\"criteria\": [\"criterion 1\", \"criterion 2\", ...]}"
90952
+ ].join("\n");
90953
+ }
90954
+ function buildGraderPrompt(objective, criteria, output) {
90955
+ return [
90956
+ "## Objective",
90957
+ objective,
90958
+ "",
90959
+ "## Acceptance Criteria",
90960
+ criteria,
90961
+ "",
90962
+ "## Agent Output",
90963
+ output || "(no output captured)",
90964
+ "",
90965
+ "Evaluate each dimension independently against the acceptance criteria, then decide overall PASS/FAIL.",
90966
+ "When FAIL, list every specific issue with an actionable fix direction so the agent knows exactly what to address next.",
90967
+ "Respond with JSON:",
90968
+ "{\"completeness\":{\"pass\":true/false,\"detail\":\"...\"},\"conformance\":{\"pass\":true/false,\"detail\":\"...\"},\"substance\":{\"pass\":true/false,\"detail\":\"...\"},\"issues\":[\"issue 1: what to fix\",\"issue 2: what to fix\"],\"pass\":true/false,\"reason\":\"overall summary\"}"
90969
+ ].join("\n");
90970
+ }
90971
+ function parseGraderResponse(text) {
90972
+ try {
90973
+ const match = text.match(/\{[\s\S]*\}/);
90974
+ if (!match) return {
90975
+ pass: true,
90976
+ reason: "No JSON found in grader response",
90977
+ summary: ""
90978
+ };
90979
+ const parsed = JSON.parse(match[0]);
90980
+ const overallPass = parsed.pass === true;
90981
+ const overallReason = typeof parsed.reason === "string" ? parsed.reason : "No reason provided";
90982
+ if (![
90983
+ parsed.completeness,
90984
+ parsed.conformance,
90985
+ parsed.substance
90986
+ ].some((d) => d !== void 0)) return {
90987
+ pass: overallPass,
90988
+ reason: overallReason,
90989
+ summary: ""
90990
+ };
90991
+ const lines = [];
90992
+ const failedDims = [];
90993
+ for (const [name, dim] of Object.entries({
90994
+ Completeness: parsed.completeness,
90995
+ Conformance: parsed.conformance,
90996
+ Substance: parsed.substance
90997
+ })) {
90998
+ if (dim === void 0) continue;
90999
+ const ok = dim.pass === true;
91000
+ const detail = typeof dim.detail === "string" ? dim.detail : "";
91001
+ lines.push(` ${ok ? "✓" : "✗"} ${name}: ${detail}`);
91002
+ if (!ok) failedDims.push(`${name}: ${detail}`);
91003
+ }
91004
+ const issues = Array.isArray(parsed.issues) ? parsed.issues.filter((i) => typeof i === "string") : [];
91005
+ if (issues.length > 0) {
91006
+ lines.push("");
91007
+ lines.push(" Issues to fix:");
91008
+ for (const issue of issues) lines.push(` - ${issue}`);
91009
+ }
91010
+ const summary = lines.join("\n");
91011
+ const reasonParts = [overallReason];
91012
+ if (failedDims.length > 0) reasonParts.push(failedDims.join("\n"));
91013
+ if (issues.length > 0) reasonParts.push(`Issues to fix:\n${issues.map((i) => `- ${i}`).join("\n")}`);
91014
+ return {
91015
+ pass: overallPass,
91016
+ reason: reasonParts.join("\n"),
91017
+ summary
91018
+ };
91019
+ } catch {
91020
+ return {
91021
+ pass: true,
91022
+ reason: "Failed to parse grader response",
91023
+ summary: ""
91024
+ };
91025
+ }
91026
+ }
91027
+ function extractResponseText(response) {
91028
+ return response.message.content.filter((p) => p.type === "text").map((p) => p.text).join("");
91029
+ }
91030
+ async function generateAcceptanceCriteria(agent, objective) {
91031
+ const prompt = buildCriteriaPrompt(objective);
91032
+ const text = extractResponseText(await agent.rawGenerate(agent.config.provider, CRITERIA_SYSTEM_PROMPT, [], [{
91033
+ role: "user",
91034
+ content: [{
91035
+ type: "text",
91036
+ text: prompt
91037
+ }],
91038
+ toolCalls: []
91039
+ }]));
91040
+ try {
91041
+ const match = text.match(/\{[\s\S]*\}/);
91042
+ if (!match) return "";
91043
+ const parsed = JSON.parse(match[0]);
91044
+ if (!Array.isArray(parsed.criteria)) return "";
91045
+ return parsed.criteria.filter((c) => typeof c === "string").map((c, i) => `${i + 1}. ${c}`).join("\n");
91046
+ } catch {
91047
+ return "";
91048
+ }
91049
+ }
91050
+ function createGoalGrader(agent) {
91051
+ return async (objective, criterion, output) => {
91052
+ let criteria;
91053
+ if (criterion !== void 0) criteria = criterion;
91054
+ else {
91055
+ criteria = await generateAcceptanceCriteria(agent, objective);
91056
+ if (!criteria) criteria = "No specific criteria defined. Evaluate based on whether the objective is clearly achieved.";
91057
+ }
91058
+ const user = buildGraderPrompt(objective, criteria, output);
91059
+ const result = parseGraderResponse(extractResponseText(await agent.rawGenerate(agent.config.provider, GRADER_SYSTEM_PROMPT, [], [{
91060
+ role: "user",
91061
+ content: [{
91062
+ type: "text",
91063
+ text: user
91064
+ }],
91065
+ toolCalls: []
91066
+ }])));
91067
+ const reason = result.summary ? `${result.reason}\n${result.summary}` : result.reason;
91068
+ return {
91069
+ pass: result.pass,
91070
+ reason
91071
+ };
91072
+ };
91073
+ }
90875
91074
  var ToolManager = class {
90876
91075
  agent;
90877
91076
  builtinTools = /* @__PURE__ */ new Map();
@@ -91150,7 +91349,7 @@ var ToolManager = class {
91150
91349
  this.agent.cron && new CronListTool(this.agent.cron),
91151
91350
  this.agent.cron && new CronDeleteTool(this.agent.cron),
91152
91351
  this.agent.type === "main" && new CreateGoalTool(this.agent),
91153
- this.agent.type === "main" && new UpdateGoalTool(this.agent),
91352
+ this.agent.type === "main" && new UpdateGoalTool(this.agent, createGoalGrader(this.agent)),
91154
91353
  this.agent.type === "main" && new GetGoalTool(this.agent),
91155
91354
  this.agent.type === "main" && new SetGoalBudgetTool(this.agent),
91156
91355
  this.agent.type === "main" && new WriteGoalNoteTool(this.agent),
@@ -121976,6 +122175,9 @@ var GoalStatusMessageComponent = class {
121976
122175
  };
121977
122176
  //#endregion
121978
122177
  //#region src/tui/commands/goal.ts
122178
+ const GOAL_STATUS_DISMISS_MS = 1e4;
122179
+ let activeGoalPanel;
122180
+ let activeGoalTimer;
121979
122181
  const CONTROL_SUBCOMMANDS = new Set([
121980
122182
  "pause",
121981
122183
  "resume",
@@ -122125,772 +122327,197 @@ async function showGoalStatus(host) {
122125
122327
  }
122126
122328
  try {
122127
122329
  const result = await session.getGoal();
122128
- host.state.transcriptContainer.addChild(new GoalStatusMessageComponent(result.goal, host.state.theme.colors));
122330
+ dismissGoalPanel(host);
122331
+ const panel = new GoalStatusMessageComponent(result.goal, host.state.theme.colors);
122332
+ host.state.transcriptContainer.addChild(panel);
122333
+ activeGoalPanel = panel;
122334
+ activeGoalTimer = setTimeout(() => dismissGoalPanel(host), GOAL_STATUS_DISMISS_MS);
122129
122335
  host.state.ui.requestRender();
122130
122336
  } catch (error) {
122131
122337
  const message = error instanceof Error ? error.message : String(error);
122132
122338
  host.showError(`获取目标状态失败:${message}`);
122133
122339
  }
122134
122340
  }
122135
- //#endregion
122136
- //#region src/utils/git/git-status.ts
122137
- /**
122138
- * Cached git branch + working-tree status for the footer/statusline.
122139
- *
122140
- * Branch name refreshes every 5s, porcelain status every 15s. Branch
122141
- * and status reads stay synchronous with short timeouts. Pull request
122142
- * lookup uses an async cache so a slow `gh pr view` never blocks
122143
- * footer rendering.
122144
- */
122145
- const BRANCH_TTL_MS = 5e3;
122146
- const STATUS_TTL_MS = 15e3;
122147
- const PULL_REQUEST_TTL_MS = 6e4;
122148
- const SPAWN_TIMEOUT_MS = 500;
122149
- const PR_SPAWN_TIMEOUT_MS = 5e3;
122150
- const AHEAD_BEHIND_RE = /\[(?:ahead (\d+))?(?:, )?(?:behind (\d+))?\]/;
122151
- function createGitStatusCache(workDir, options = {}) {
122152
- const isRepo = detectGitRepo(workDir);
122153
- let branch = {
122154
- value: null,
122155
- fetchedAt: 0
122156
- };
122157
- let status = {
122158
- dirty: false,
122159
- ahead: 0,
122160
- behind: 0,
122161
- diffAdded: 0,
122162
- diffDeleted: 0,
122163
- fetchedAt: 0
122164
- };
122165
- let pullRequest = {
122166
- value: null,
122167
- branch: null,
122168
- fetchedAt: 0,
122169
- pendingBranch: null,
122170
- requestId: 0
122171
- };
122172
- return { getStatus: () => {
122173
- if (!isRepo) return null;
122174
- const now = Date.now();
122175
- if (now - branch.fetchedAt >= BRANCH_TTL_MS) branch = {
122176
- value: readBranch(workDir),
122177
- fetchedAt: now
122178
- };
122179
- if (branch.value === null) return null;
122180
- if (now - status.fetchedAt >= STATUS_TTL_MS) status = {
122181
- ...readStatus(workDir),
122182
- fetchedAt: now
122183
- };
122184
- refreshPullRequestIfNeeded(branch.value, now);
122185
- return {
122186
- branch: branch.value,
122187
- dirty: status.dirty,
122188
- ahead: status.ahead,
122189
- behind: status.behind,
122190
- diffAdded: status.diffAdded,
122191
- diffDeleted: status.diffDeleted,
122192
- pullRequest: pullRequest.branch === branch.value ? pullRequest.value : null
122193
- };
122194
- } };
122195
- function refreshPullRequestIfNeeded(branchName, now) {
122196
- if (pullRequest.pendingBranch === branchName) return;
122197
- const fetchedAt = pullRequest.branch === branchName ? pullRequest.fetchedAt : 0;
122198
- if (now - fetchedAt < PULL_REQUEST_TTL_MS) return;
122199
- const requestId = pullRequest.requestId + 1;
122200
- pullRequest = {
122201
- value: pullRequest.branch === branchName ? pullRequest.value : null,
122202
- branch: branchName,
122203
- fetchedAt,
122204
- pendingBranch: branchName,
122205
- requestId
122206
- };
122207
- readPullRequest(workDir).then((value) => {
122208
- if (pullRequest.requestId !== requestId) return;
122209
- const changed = !samePullRequest(pullRequest.branch === branchName ? pullRequest.value : null, value);
122210
- pullRequest = {
122211
- value,
122212
- branch: branchName,
122213
- fetchedAt: Date.now(),
122214
- pendingBranch: null,
122215
- requestId
122216
- };
122217
- if (changed) options.onChange?.();
122218
- });
122341
+ function dismissGoalPanel(host) {
122342
+ if (activeGoalTimer !== void 0) {
122343
+ clearTimeout(activeGoalTimer);
122344
+ activeGoalTimer = void 0;
122219
122345
  }
122220
- }
122221
- function detectGitRepo(workDir) {
122222
- try {
122223
- const result = spawnSync("git", [
122224
- "-C",
122225
- workDir,
122226
- "rev-parse",
122227
- "--is-inside-work-tree"
122228
- ], {
122229
- encoding: "utf8",
122230
- timeout: SPAWN_TIMEOUT_MS
122231
- });
122232
- return result.status === 0 && result.stdout.trim() === "true";
122233
- } catch {
122234
- return false;
122346
+ if (activeGoalPanel !== void 0) {
122347
+ host.state.transcriptContainer.removeChild(activeGoalPanel);
122348
+ activeGoalPanel = void 0;
122349
+ host.state.ui.requestRender();
122235
122350
  }
122236
122351
  }
122237
- function readBranch(workDir) {
122238
- try {
122239
- const result = spawnSync("git", [
122240
- "-C",
122241
- workDir,
122242
- "branch",
122243
- "--show-current"
122244
- ], {
122245
- encoding: "utf8",
122246
- timeout: SPAWN_TIMEOUT_MS
122247
- });
122248
- if (result.status !== 0) return null;
122249
- const name = result.stdout.trim();
122250
- return name.length > 0 ? name : null;
122251
- } catch {
122252
- return null;
122253
- }
122352
+ //#endregion
122353
+ //#region src/tui/components/chrome/welcome.ts
122354
+ const HUE_STOPS = 24;
122355
+ const SUB_STEPS = 5;
122356
+ const BREATHE_STEPS = HUE_STOPS * SUB_STEPS;
122357
+ const BREATHE_INTERVAL_MS = 40;
122358
+ const LOGO_FRAMES = [
122359
+ ["██▄▄▄██", "▐█▄▀▄█▌"],
122360
+ ["██▄▄▄██", "▐▄▄▀▄▄▌"],
122361
+ ["██▄▄▄██", "▐▄▀▄▄▄▌"],
122362
+ ["██▄▄▄██", "▐▄▄▄▀▄▌"],
122363
+ ["██▄▄▄██", "▐█▄▀▄█▌"]
122364
+ ];
122365
+ function hexToRgb$1(hex) {
122366
+ return [
122367
+ parseInt(hex.slice(1, 3), 16),
122368
+ parseInt(hex.slice(3, 5), 16),
122369
+ parseInt(hex.slice(5, 7), 16)
122370
+ ];
122254
122371
  }
122255
- function readStatus(workDir) {
122256
- try {
122257
- const result = spawnSync("git", [
122258
- "-C",
122259
- workDir,
122260
- "status",
122261
- "--porcelain",
122262
- "-b"
122263
- ], {
122264
- encoding: "utf8",
122265
- timeout: SPAWN_TIMEOUT_MS,
122266
- maxBuffer: 4 * 1024 * 1024
122267
- });
122268
- if (result.status !== 0) return {
122269
- dirty: false,
122270
- ahead: 0,
122271
- behind: 0,
122272
- diffAdded: 0,
122273
- diffDeleted: 0
122274
- };
122275
- let dirty = false;
122276
- let ahead = 0;
122277
- let behind = 0;
122278
- for (const line of result.stdout.split("\n")) if (line.startsWith("## ")) {
122279
- const m = AHEAD_BEHIND_RE.exec(line);
122280
- if (m) {
122281
- ahead = Number.parseInt(m[1] ?? "0", 10) || 0;
122282
- behind = Number.parseInt(m[2] ?? "0", 10) || 0;
122283
- }
122284
- } else if (line.trim().length > 0) dirty = true;
122285
- const diff = dirty ? readDiffStats(workDir) : {
122286
- added: 0,
122287
- deleted: 0
122288
- };
122289
- return {
122290
- dirty,
122291
- ahead,
122292
- behind,
122293
- diffAdded: diff.added,
122294
- diffDeleted: diff.deleted
122295
- };
122296
- } catch {
122297
- return {
122298
- dirty: false,
122299
- ahead: 0,
122300
- behind: 0,
122301
- diffAdded: 0,
122302
- diffDeleted: 0
122303
- };
122304
- }
122372
+ function rgbToHsl(r, g, b) {
122373
+ const rf = r / 255, gf = g / 255, bf = b / 255;
122374
+ const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf);
122375
+ const l = (max + min) / 2;
122376
+ if (max === min) return [
122377
+ 0,
122378
+ 0,
122379
+ l * 100
122380
+ ];
122381
+ const d = max - min;
122382
+ const s = l > .5 ? d / (2 - max - min) : d / (max + min);
122383
+ let h = 0;
122384
+ if (max === rf) h = ((gf - bf) / d + (gf < bf ? 6 : 0)) / 6;
122385
+ else if (max === gf) h = ((bf - rf) / d + 2) / 6;
122386
+ else h = ((rf - gf) / d + 4) / 6;
122387
+ return [
122388
+ h * 360,
122389
+ s * 100,
122390
+ l * 100
122391
+ ];
122305
122392
  }
122306
- function readDiffStats(workDir) {
122307
- try {
122308
- const result = spawnSync("git", [
122309
- "-C",
122310
- workDir,
122311
- "diff",
122312
- "--numstat",
122313
- "HEAD",
122314
- "--"
122315
- ], {
122316
- encoding: "utf8",
122317
- timeout: SPAWN_TIMEOUT_MS,
122318
- maxBuffer: 4 * 1024 * 1024
122319
- });
122320
- if (result.status !== 0) return {
122321
- added: 0,
122322
- deleted: 0
122323
- };
122324
- let added = 0;
122325
- let deleted = 0;
122326
- for (const line of result.stdout.split("\n")) {
122327
- if (!line) continue;
122328
- const [addedText, deletedText] = line.split(" ");
122329
- added += parseDiffNumstatCount(addedText);
122330
- deleted += parseDiffNumstatCount(deletedText);
122331
- }
122332
- return {
122333
- added,
122334
- deleted
122335
- };
122336
- } catch {
122337
- return {
122338
- added: 0,
122339
- deleted: 0
122340
- };
122393
+ function hslToRgb(h, s, l) {
122394
+ const hf = (h % 360 + 360) % 360 / 360;
122395
+ const sf = s / 100, lf = l / 100;
122396
+ if (sf === 0) {
122397
+ const v = Math.round(lf * 255);
122398
+ return [
122399
+ v,
122400
+ v,
122401
+ v
122402
+ ];
122341
122403
  }
122404
+ const q = lf < .5 ? lf * (1 + sf) : lf + sf - lf * sf;
122405
+ const p = 2 * lf - q;
122406
+ const hue = (t) => {
122407
+ if (t < 0) t += 1;
122408
+ if (t > 1) t -= 1;
122409
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
122410
+ if (t < 1 / 2) return q;
122411
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
122412
+ return p;
122413
+ };
122414
+ return [
122415
+ Math.round(hue(hf + 1 / 3) * 255),
122416
+ Math.round(hue(hf) * 255),
122417
+ Math.round(hue(hf - 1 / 3) * 255)
122418
+ ];
122342
122419
  }
122343
- function parseDiffNumstatCount(value) {
122344
- if (value === void 0 || value === "-") return 0;
122345
- const n = Number.parseInt(value, 10);
122346
- return Number.isFinite(n) && n > 0 ? n : 0;
122347
- }
122348
- function readPullRequest(workDir) {
122349
- return new Promise((resolve) => {
122350
- try {
122351
- execFile("gh", [
122352
- "pr",
122353
- "view",
122354
- "--json",
122355
- "number,url"
122356
- ], {
122357
- cwd: workDir,
122358
- encoding: "utf8",
122359
- env: {
122360
- ...process.env,
122361
- GH_NO_UPDATE_NOTIFIER: "1",
122362
- GH_PROMPT_DISABLED: "1"
122363
- },
122364
- timeout: PR_SPAWN_TIMEOUT_MS,
122365
- maxBuffer: 256 * 1024
122366
- }, (error, stdout) => {
122367
- if (error !== null) {
122368
- resolve(null);
122369
- return;
122370
- }
122371
- resolve(parsePullRequest(stdout));
122372
- });
122373
- } catch {
122374
- resolve(null);
122375
- }
122376
- });
122377
- }
122378
- function samePullRequest(a, b) {
122379
- if (a === null || b === null) return a === b;
122380
- return a.number === b.number && a.url === b.url;
122420
+ function rgbToHex(r, g, b) {
122421
+ const c = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
122422
+ return `#${c(r)}${c(g)}${c(b)}`;
122381
122423
  }
122382
- function parsePullRequest(stdout) {
122383
- try {
122384
- const raw = JSON.parse(stdout);
122385
- if (typeof raw !== "object" || raw === null) return null;
122386
- const record = raw;
122387
- const number = record["number"];
122388
- const url = record["url"];
122389
- if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) return null;
122390
- if (typeof url !== "string" || !isSafeHttpUrl(url)) return null;
122391
- return {
122392
- number,
122393
- url
122394
- };
122395
- } catch {
122396
- return null;
122424
+ /**
122425
+ * Build a full-hue-wheel palette anchored at the primary-green hue.
122426
+ * At frame 0 (and BREATHE_STEPS) the colour is pure primary; in between
122427
+ * it sweeps through all 24 hue stops with smooth sub-step interpolation.
122428
+ */
122429
+ function buildBreathingPalette(primaryHex, hueStops, subSteps) {
122430
+ const [r, g, b] = hexToRgb$1(primaryHex);
122431
+ const [baseHue] = rgbToHsl(r, g, b);
122432
+ const steps = hueStops * subSteps;
122433
+ const palette = [];
122434
+ for (let i = 0; i < steps; i++) {
122435
+ const [rr, gg, bb] = hslToRgb((baseHue + i / steps * 360) % 360, 90, 70);
122436
+ palette.push(rgbToHex(rr, gg, bb));
122397
122437
  }
122438
+ return palette;
122398
122439
  }
122399
- function isSafeHttpUrl(value) {
122400
- if (hasControlChars(value)) return false;
122401
- try {
122402
- const url = new URL(value);
122403
- return url.protocol === "https:" || url.protocol === "http:";
122404
- } catch {
122405
- return false;
122440
+ var WelcomeComponent = class {
122441
+ state;
122442
+ colors;
122443
+ ui;
122444
+ breatheFrame = 0;
122445
+ breatheTimer = null;
122446
+ breathePalette;
122447
+ borderTitle = null;
122448
+ constructor(state, colors, ui) {
122449
+ this.state = state;
122450
+ this.colors = colors;
122451
+ this.ui = ui;
122452
+ this.breathePalette = buildBreathingPalette(colors.primary, HUE_STOPS, SUB_STEPS);
122453
+ this.startBreathing();
122406
122454
  }
122407
- }
122408
- function hasControlChars(value) {
122409
- for (const char of value) {
122410
- const code = char.codePointAt(0) ?? 0;
122411
- if (code <= 31 || code === 127) return true;
122455
+ stopBreathing() {
122456
+ if (this.breatheTimer !== null) {
122457
+ clearInterval(this.breatheTimer);
122458
+ this.breatheTimer = null;
122459
+ }
122460
+ if (this.breatheFrame !== 0) {
122461
+ this.breatheFrame = 0;
122462
+ this.ui.requestRender();
122463
+ }
122412
122464
  }
122413
- return false;
122414
- }
122415
- function formatGitBadgeBase(status) {
122416
- const parts = [];
122417
- const diff = formatDiffStats(status);
122418
- if (diff) parts.push(diff);
122419
- let sync = "";
122420
- if (status.ahead > 0) sync += `↑${status.ahead}`;
122421
- if (status.behind > 0) sync += `↓${status.behind}`;
122422
- if (sync) parts.push(sync);
122423
- return parts.length === 0 ? status.branch : `${status.branch} [${parts.join(" ")}]`;
122424
- }
122425
- function formatPullRequestBadge(pullRequest, options = {}) {
122426
- const prText = `[PR#${String(pullRequest.number)}]`;
122427
- return options.linkPullRequest ? toTerminalHyperlink(prText, pullRequest.url) : prText;
122428
- }
122429
- function formatDiffStats(status) {
122430
- const parts = [];
122431
- if (status.diffAdded > 0) parts.push(`+${String(status.diffAdded)}`);
122432
- if (status.diffDeleted > 0) parts.push(`-${String(status.diffDeleted)}`);
122433
- if (parts.length > 0) return parts.join(" ");
122434
- return status.dirty ? "±" : null;
122435
- }
122436
- function toTerminalHyperlink(text, url) {
122437
- if (!isSafeHttpUrl(url)) return text;
122438
- return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`;
122439
- }
122440
- //#endregion
122441
- //#region src/tui/components/chrome/footer.ts
122442
- const MAX_CWD_SEGMENTS = 3;
122443
- const TIP_ROTATE_INTERVAL_MS = 1e4;
122444
- const TOOLBAR_TIPS = [
122445
- { text: "shift+tab: 计划模式" },
122446
- { text: "/model: 切换模型" },
122447
- {
122448
- text: "ctrl+s: 中途干预",
122449
- priority: 2
122450
- },
122451
- {
122452
- text: "/compact: 压缩上下文",
122453
- priority: 2
122454
- },
122455
- { text: "ctrl+o: 展开工具输出" },
122456
- { text: "/tasks: 后台任务" },
122457
- { text: "shift+enter: 换行" },
122458
- {
122459
- text: "/init: 生成 AGENTS.md",
122460
- priority: 2
122461
- },
122462
- { text: "@: 提及文件" },
122463
- { text: "ctrl+c: 取消" },
122464
- { text: "/theme: 切换主题" },
122465
- { text: "/auto: 自动权限模式" },
122466
- { text: "/yes: 自动批准" },
122467
- { text: "/help: 显示命令" },
122468
- {
122469
- text: "/config: 选择并配置你常用的模型商",
122470
- solo: true,
122471
- priority: 3
122472
- },
122473
- {
122474
- text: "让 Scream 安排任务,例如 \"2个小时后提醒我去拿快递\"",
122475
- solo: true,
122476
- priority: 3
122477
- }
122478
- ];
122479
- /**
122480
- * Expand tips into a rotation sequence using smooth weighted round-robin
122481
- * (the nginx SWRR algorithm). Higher-`priority` tips appear more often while
122482
- * staying evenly spread, so a tip generally does not land next to its own
122483
- * duplicate. Deterministic and computed once at module load. Exported for
122484
- * unit testing.
122485
- */
122486
- function buildWeightedTips(tips) {
122487
- const items = tips.map((t) => ({
122488
- tip: t,
122489
- weight: Math.max(1, Math.trunc(t.priority ?? 1)),
122490
- current: 0
122491
- }));
122492
- const total = items.reduce((sum, it) => sum + it.weight, 0);
122493
- const seq = [];
122494
- for (let n = 0; n < total; n++) {
122495
- let best = items[0];
122496
- for (const it of items) {
122497
- it.current += it.weight;
122498
- if (it.current > best.current) best = it;
122499
- }
122500
- best.current -= total;
122501
- seq.push(best.tip);
122502
- }
122503
- return seq;
122504
- }
122505
- const ROTATION = buildWeightedTips(TOOLBAR_TIPS);
122506
- function currentTipIndex() {
122507
- return Math.floor(Date.now() / TIP_ROTATE_INTERVAL_MS);
122508
- }
122509
- /**
122510
- * Pick the tip(s) for a rotation index over the weighted ROTATION sequence.
122511
- * `primary` is always shown when it fits; `pair` (primary + next tip joined
122512
- * by the separator) is offered for wide terminals. Pairing is skipped when
122513
- * the current/next tip is `solo` or when the neighbour is a duplicate of the
122514
- * current tip (which can happen at the wrap boundary), keeping long/important
122515
- * tips on their own and avoiding "X | X".
122516
- */
122517
- function tipsForIndex(index) {
122518
- const n = ROTATION.length;
122519
- if (n === 0) return {
122520
- primary: "",
122521
- pair: null
122522
- };
122523
- const offset = (index % n + n) % n;
122524
- const current = ROTATION[offset];
122525
- if (n === 1 || current.solo) return {
122526
- primary: current.text,
122527
- pair: null
122528
- };
122529
- const next = ROTATION[(offset + 1) % n];
122530
- if (next.solo || next.text === current.text) return {
122531
- primary: current.text,
122532
- pair: null
122533
- };
122534
- return {
122535
- primary: current.text,
122536
- pair: current.text + " | " + next.text
122537
- };
122538
- }
122539
- function shortenModel(model) {
122540
- if (!model) return model;
122541
- const slash = model.lastIndexOf("/");
122542
- return slash >= 0 ? model.slice(slash + 1) : model;
122543
- }
122544
- function modelDisplayName(state) {
122545
- const model = state.availableModels[state.model];
122546
- return model?.displayName ?? model?.model ?? state.model;
122547
- }
122548
- function shortenCwd(path) {
122549
- if (!path) return path;
122550
- const home = process.env["HOME"] ?? "";
122551
- let work = path;
122552
- if (home && path === home) return "~";
122553
- if (home && path.startsWith(home + "/")) work = "~" + path.slice(home.length);
122554
- const segments = work.split("/").filter((s) => s.length > 0);
122555
- if (segments.length <= MAX_CWD_SEGMENTS) return work;
122556
- return `…/${segments.slice(-3).join("/")}`;
122557
- }
122558
- function formatTokenCount(n) {
122559
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
122560
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
122561
- return String(n);
122562
- }
122563
- function safeUsage(usage) {
122564
- return safeUsageRatio(usage);
122565
- }
122566
- function formatContextStatus(usage, tokens, maxTokens) {
122567
- const pct = `${(safeUsage(usage) * 100).toFixed(1)}%`;
122568
- if (maxTokens && maxTokens > 0 && tokens !== void 0) return `上下文:${pct} (${formatTokenCount(tokens)}/${formatTokenCount(maxTokens)})`;
122569
- return `上下文:${pct}`;
122570
- }
122571
- const BRAND_COLORS = [
122572
- "#72A4E9",
122573
- "#A78BFA",
122574
- "#34D399"
122575
- ];
122576
- const GRADIENT_CYCLE_MS = 4e3;
122577
- const SPINNER_FRAMES$1 = [
122578
- "●",
122579
- "◉",
122580
- "◎",
122581
- "◌",
122582
- "○",
122583
- "◌",
122584
- "◎",
122585
- "◉"
122586
- ];
122587
- const SPINNER_TICK_MS = 120;
122588
- function hexToRgb$1(hex) {
122589
- const v = parseInt(hex.slice(1), 16);
122590
- return [
122591
- v >> 16 & 255,
122592
- v >> 8 & 255,
122593
- v & 255
122594
- ];
122595
- }
122596
- function lerpGradient(t) {
122597
- const count = BRAND_COLORS.length;
122598
- const segment = Math.min(t * count, count - 1);
122599
- const idx = Math.floor(segment);
122600
- const localT = segment - idx;
122601
- const nextIdx = (idx + 1) % count;
122602
- const [r0, g0, b0] = hexToRgb$1(BRAND_COLORS[idx]);
122603
- const [r1, g1, b1] = hexToRgb$1(BRAND_COLORS[nextIdx]);
122604
- const r = Math.round(r0 + (r1 - r0) * localT);
122605
- const g = Math.round(g0 + (g1 - g0) * localT);
122606
- const b = Math.round(b0 + (b1 - b0) * localT);
122607
- return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
122608
- }
122609
- function buildStatusLine(streamingPhase, livePaneMode, streamingStartTime) {
122610
- if (streamingPhase === "idle" && livePaneMode !== "tool") return "○ 空闲";
122611
- let label;
122612
- if (livePaneMode === "tool") label = "执行中";
122613
- else if (streamingPhase === "waiting") label = "等待响应";
122614
- else if (streamingPhase === "thinking") label = "思考中";
122615
- else if (streamingPhase === "composing") label = "输出中";
122616
- else label = "";
122617
- const elapsed = Date.now() - streamingStartTime;
122618
- const totalSeconds = Math.floor(elapsed / 1e3);
122619
- const elapsedStr = totalSeconds < 60 ? `${totalSeconds}s` : `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
122620
- const now = Date.now();
122621
- const frame = SPINNER_FRAMES$1[Math.floor(now / SPINNER_TICK_MS) % SPINNER_FRAMES$1.length];
122622
- const gradientColor = lerpGradient(now % GRADIENT_CYCLE_MS / GRADIENT_CYCLE_MS);
122623
- return chalk.hex(gradientColor).bold(frame) + " " + label + " " + elapsedStr;
122624
- }
122625
- function formatFooterGitBadge(status, colors) {
122626
- const base = chalk.hex(colors.status)(formatGitBadgeBase(status));
122627
- if (status.pullRequest === null) return base;
122628
- return `${base} ${chalk.hex(colors.primary)(formatPullRequestBadge(status.pullRequest, { linkPullRequest: true }))}`;
122629
- }
122630
- var FooterComponent = class {
122631
- state;
122632
- colors;
122633
- onGitStatusChange;
122634
- gitCache;
122635
- gitCacheWorkDir;
122636
- transientHint = null;
122637
- /**
122638
- * Non-terminal background-task counts split by kind so the footer can
122639
- * render two distinct badges. `bashTasks` covers `bash-*` BPM tasks
122640
- * spawned via `Shell run_in_background=true`; `agentTasks` covers
122641
- * `agent-*` BPM tasks (background subagents). Either zero hides its
122642
- * respective badge.
122643
- */
122644
- backgroundBashTaskCount = 0;
122645
- backgroundAgentCount = 0;
122646
- constructor(state, colors, onGitStatusChange = () => {}) {
122647
- this.state = state;
122648
- this.colors = colors;
122649
- this.onGitStatusChange = onGitStatusChange;
122650
- this.gitCacheWorkDir = state.workDir;
122651
- this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
122652
- }
122653
- setState(state) {
122654
- if (state.workDir !== this.gitCacheWorkDir) {
122655
- this.gitCacheWorkDir = state.workDir;
122656
- this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
122657
- }
122658
- this.state = state;
122659
- }
122660
- setColors(colors) {
122661
- this.colors = colors;
122662
- }
122663
- /**
122664
- * Short-lived hint that replaces the rotating toolbar tips on line 1.
122665
- * Used by the exit-confirmation double-tap flow to show "Press Ctrl+C
122666
- * again to exit" without requiring a toast/overlay subsystem.
122667
- * Pass `null` to clear.
122668
- */
122669
- setTransientHint(hint) {
122670
- this.transientHint = hint;
122671
- }
122672
- /**
122673
- * Sync both background-task badges with live counts. Each non-zero
122674
- * count produces its own bracketed badge on line 1; zeros hide them
122675
- * independently.
122676
- */
122677
- setBackgroundCounts(counts) {
122678
- this.backgroundBashTaskCount = Math.max(0, counts.bashTasks);
122679
- this.backgroundAgentCount = Math.max(0, counts.agentTasks);
122680
- }
122681
- invalidate() {}
122682
- render(width) {
122683
- const colors = this.colors;
122684
- const state = this.state;
122685
- const left = [];
122686
- if (state.permissionMode === "auto") left.push(chalk.hex(colors.warning).bold("auto"));
122687
- if (state.permissionMode === "yolo") left.push(chalk.hex(colors.warning).bold("YES"));
122688
- if (state.planMode) left.push(chalk.hex(colors.primary).bold("plan"));
122689
- if (state.wolfpackMode) left.push(chalk.hex(colors.primary).bold("wolfpack"));
122690
- if (state.goalActive && state.goal) {
122691
- const goalText = state.goal.length > 20 ? state.goal.slice(0, 20) + "…" : state.goal;
122692
- left.push(chalk.hex(colors.success).bold(`🎯 ${goalText}`));
122693
- }
122694
- const model = shortenModel(modelDisplayName(state));
122695
- if (model) {
122696
- const thinkingLabel = state.thinking ? " 思考中" : "";
122697
- left.push(chalk.hex(colors.text)(`${model}${thinkingLabel}`));
122698
- }
122699
- if (this.backgroundBashTaskCount > 0) {
122700
- const noun = this.backgroundBashTaskCount === 1 ? "个任务" : "个任务";
122701
- left.push(chalk.hex(colors.primary)(`[${String(this.backgroundBashTaskCount)}${noun} 运行中]`));
122702
- }
122703
- if (this.backgroundAgentCount > 0) {
122704
- const noun = this.backgroundAgentCount === 1 ? "个代理" : "个代理";
122705
- left.push(chalk.hex(colors.primary)(`[${String(this.backgroundAgentCount)}${noun} 运行中]`));
122706
- }
122707
- const cwd = shortenCwd(state.workDir);
122708
- if (cwd) left.push(chalk.hex(colors.status)(cwd));
122709
- const git = this.gitCache.getStatus();
122710
- if (git !== null) left.push(formatFooterGitBadge(git, colors));
122711
- const leftLine = left.join(" ");
122712
- const leftWidth = visibleWidth(leftLine);
122713
- let rightText;
122714
- if (this.transientHint) rightText = chalk.hex(colors.warning).bold(this.transientHint);
122715
- else {
122716
- const statusLine = buildStatusLine(state.streamingPhase, state.livePaneMode, state.streamingStartTime);
122717
- const ccDot = state.ccConnectActive ? chalk.hex(colors.success)("●") : chalk.hex(colors.textDim)("●");
122718
- rightText = chalk.hex(colors.textDim)(ccDot + " " + formatContextStatus(state.contextUsage, state.contextTokens, state.maxContextTokens) + " " + statusLine);
122719
- }
122720
- const rightWidth = visibleWidth(rightText);
122721
- const gap = 3;
122722
- let line1;
122723
- if (leftWidth + gap + rightWidth <= width) {
122724
- const pad = width - leftWidth - rightWidth;
122725
- line1 = leftLine + " ".repeat(pad) + rightText;
122726
- } else if (leftWidth <= width) line1 = leftLine;
122727
- else line1 = truncateToWidth(leftLine, width, "…");
122728
- return [truncateToWidth(line1, width)];
122729
- }
122730
- };
122731
- //#endregion
122732
- //#region src/tui/components/chrome/welcome.ts
122733
- const HUE_STOPS = 24;
122734
- const SUB_STEPS = 5;
122735
- const BREATHE_STEPS = HUE_STOPS * SUB_STEPS;
122736
- const BREATHE_INTERVAL_MS = 40;
122737
- function hexToRgb(hex) {
122738
- return [
122739
- parseInt(hex.slice(1, 3), 16),
122740
- parseInt(hex.slice(3, 5), 16),
122741
- parseInt(hex.slice(5, 7), 16)
122742
- ];
122743
- }
122744
- function rgbToHsl(r, g, b) {
122745
- const rf = r / 255, gf = g / 255, bf = b / 255;
122746
- const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf);
122747
- const l = (max + min) / 2;
122748
- if (max === min) return [
122749
- 0,
122750
- 0,
122751
- l * 100
122752
- ];
122753
- const d = max - min;
122754
- const s = l > .5 ? d / (2 - max - min) : d / (max + min);
122755
- let h = 0;
122756
- if (max === rf) h = ((gf - bf) / d + (gf < bf ? 6 : 0)) / 6;
122757
- else if (max === gf) h = ((bf - rf) / d + 2) / 6;
122758
- else h = ((rf - gf) / d + 4) / 6;
122759
- return [
122760
- h * 360,
122761
- s * 100,
122762
- l * 100
122763
- ];
122764
- }
122765
- function hslToRgb(h, s, l) {
122766
- const hf = (h % 360 + 360) % 360 / 360;
122767
- const sf = s / 100, lf = l / 100;
122768
- if (sf === 0) {
122769
- const v = Math.round(lf * 255);
122770
- return [
122771
- v,
122772
- v,
122773
- v
122774
- ];
122775
- }
122776
- const q = lf < .5 ? lf * (1 + sf) : lf + sf - lf * sf;
122777
- const p = 2 * lf - q;
122778
- const hue = (t) => {
122779
- if (t < 0) t += 1;
122780
- if (t > 1) t -= 1;
122781
- if (t < 1 / 6) return p + (q - p) * 6 * t;
122782
- if (t < 1 / 2) return q;
122783
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
122784
- return p;
122785
- };
122786
- return [
122787
- Math.round(hue(hf + 1 / 3) * 255),
122788
- Math.round(hue(hf) * 255),
122789
- Math.round(hue(hf - 1 / 3) * 255)
122790
- ];
122791
- }
122792
- function rgbToHex(r, g, b) {
122793
- const c = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
122794
- return `#${c(r)}${c(g)}${c(b)}`;
122795
- }
122796
- /**
122797
- * Build a full-hue-wheel palette anchored at the primary-green hue.
122798
- * At frame 0 (and BREATHE_STEPS) the colour is pure primary; in between
122799
- * it sweeps through all 24 hue stops with smooth sub-step interpolation.
122800
- */
122801
- function buildBreathingPalette(primaryHex, hueStops, subSteps) {
122802
- const [r, g, b] = hexToRgb(primaryHex);
122803
- const [baseHue, sat, lit] = rgbToHsl(r, g, b);
122804
- const steps = hueStops * subSteps;
122805
- const palette = [];
122806
- for (let i = 0; i < steps; i++) {
122807
- const [rr, gg, bb] = hslToRgb((baseHue + i / steps * 360) % 360, sat, lit);
122808
- palette.push(rgbToHex(rr, gg, bb));
122809
- }
122810
- return palette;
122811
- }
122812
- var WelcomeComponent = class {
122813
- state;
122814
- colors;
122815
- ui;
122816
- breatheFrame = 0;
122817
- breatheTimer = null;
122818
- breathePalette;
122819
- borderTitle = null;
122820
- constructor(state, colors, ui) {
122821
- this.state = state;
122822
- this.colors = colors;
122823
- this.ui = ui;
122824
- this.breathePalette = buildBreathingPalette(colors.primary, HUE_STOPS, SUB_STEPS);
122825
- this.startBreathing();
122826
- }
122827
- stopBreathing() {
122828
- if (this.breatheTimer !== null) {
122829
- clearInterval(this.breatheTimer);
122830
- this.breatheTimer = null;
122831
- }
122832
- if (this.breatheFrame !== 0) {
122833
- this.breatheFrame = 0;
122834
- this.ui.requestRender();
122835
- }
122836
- }
122837
- startBreathing() {
122838
- this.breatheTimer = setInterval(() => {
122839
- this.breatheFrame = (this.breatheFrame + 1) % BREATHE_STEPS;
122840
- this.ui.requestRender();
122841
- }, BREATHE_INTERVAL_MS);
122465
+ startBreathing() {
122466
+ this.breatheTimer = setInterval(() => {
122467
+ this.breatheFrame = (this.breatheFrame + 1) % BREATHE_STEPS;
122468
+ this.ui.requestRender();
122469
+ }, BREATHE_INTERVAL_MS);
122842
122470
  }
122843
122471
  invalidate() {}
122844
122472
  render(width) {
122845
122473
  const breatheColor = this.breathePalette[this.breatheFrame] ?? this.colors.primary;
122846
122474
  const logoColor = (s) => chalk.hex(breatheColor)(s);
122847
- const primary = (s) => chalk.hex(this.colors.primary)(s);
122848
- const innerWidth = Math.max(10, width - 4);
122849
- const pad = " ";
122850
- const logo = ["░▒▓██▄▄▄██", "░▒▓▐█▄▀▄█▌"];
122851
- const logoWidth = Math.max(...logo.map((row) => visibleWidth(row)));
122852
- const gap = " ";
122853
- const textWidth = Math.max(4, innerWidth - logoWidth - 2);
122854
- const rightRow0 = truncateToWidth(chalk.bold.hex(this.colors.primary)("欢迎使用Scream 您的中文Ai助手"), textWidth, "…");
122855
- const isLoggedOut = !this.state.model;
122856
122475
  const dim = chalk.hex(this.colors.textDim);
122857
122476
  const labelStyle = chalk.bold.hex(this.colors.textDim);
122858
- const rightRow1 = truncateToWidth(dim(isLoggedOut ? "运行 /config 开始配置。" : "发送 / 进入快捷菜单,/exit 保存并退出"), textWidth, "…");
122859
- const headerLines = [logoColor(logo[0].padEnd(logoWidth)) + gap + rightRow0, logoColor(logo[1].padEnd(logoWidth)) + gap + rightRow1];
122477
+ const innerWidth = Math.max(10, width - 4);
122478
+ const isLoggedOut = !this.state.model;
122479
+ const frame = LOGO_FRAMES[this.breatheTimer !== null ? Math.floor(this.breatheFrame / 24) % LOGO_FRAMES.length : 0];
122480
+ const logo = [logoColor(frame[0]), logoColor(frame[1])];
122860
122481
  const activeModel = this.state.availableModels[this.state.model];
122861
122482
  const modelValue = isLoggedOut ? chalk.hex(this.colors.warning)("未设置,运行 /config") : activeModel?.displayName ?? activeModel?.model ?? this.state.model;
122862
122483
  let versionValue;
122863
122484
  if (this.state.hasNewVersion && this.state.latestVersion !== null) versionValue = chalk.hex(this.colors.warning)(this.state.version) + " " + chalk.hex(this.colors.textDim)("有新版本(" + this.state.latestVersion + ")");
122864
122485
  else versionValue = this.state.version;
122865
- const infoLines = [
122866
- labelStyle("目录: ") + this.state.workDir,
122867
- labelStyle("模型: ") + modelValue,
122868
- labelStyle("版本: ") + versionValue
122869
- ];
122870
- const { primary: tipPrimary, pair: tipPair } = tipsForIndex(currentTipIndex());
122871
- const tip = tipPair && visibleWidth(tipPair) <= innerWidth ? tipPair : tipPrimary;
122872
- const tipLine = chalk.hex(this.colors.textMuted)("Tips: " + tip);
122486
+ const hintText = isLoggedOut ? "运行 /config 开始配置" : "发送 / 进入快捷菜单,/exit 保存并退出";
122873
122487
  const contentLines = [
122874
- ...headerLines,
122488
+ ...logo,
122875
122489
  "",
122876
- ...infoLines,
122490
+ labelStyle("版本:") + " " + versionValue,
122491
+ labelStyle("模型:") + " " + modelValue,
122492
+ labelStyle("目录:") + " " + this.state.workDir,
122877
122493
  "",
122878
- tipLine
122494
+ dim(hintText)
122879
122495
  ];
122880
- const borderTitle = this.borderTitle;
122496
+ const borderTitle = this.borderTitle ?? "";
122497
+ const contentWidth = width - 2;
122498
+ let topBorder;
122499
+ if (borderTitle) {
122500
+ const centerPos = Math.floor(contentWidth / 2);
122501
+ const titleText = `─ ${borderTitle} ─`;
122502
+ const titleStart = centerPos - Math.floor(visibleWidth(titleText) / 2);
122503
+ const leftDash = Math.max(0, titleStart);
122504
+ const rightDash = Math.max(0, contentWidth - leftDash - visibleWidth(titleText));
122505
+ topBorder = logoColor("╭" + "─".repeat(leftDash) + titleText + "─".repeat(rightDash) + "╮");
122506
+ } else topBorder = logoColor("╭" + "─".repeat(contentWidth) + "╮");
122881
122507
  const lines = [
122882
122508
  "",
122883
- borderTitle ? primary("╭─ " + borderTitle + " " + "─".repeat(Math.max(0, width - 5 - visibleWidth(borderTitle))) + "╮") : primary("╭" + "─".repeat(width - 2) + "╮"),
122884
- primary("│") + " ".repeat(width - 2) + primary("│")
122509
+ topBorder,
122510
+ logoColor("│") + " ".repeat(width - 2) + logoColor("│")
122885
122511
  ];
122886
122512
  for (const content of contentLines) {
122887
122513
  const truncated = truncateToWidth(content, innerWidth, "…");
122888
122514
  const vis = visibleWidth(truncated);
122889
- const rightPad = Math.max(0, innerWidth - vis);
122890
- lines.push(primary("│") + pad + truncated + " ".repeat(rightPad) + primary("│"));
122515
+ const centerPad = Math.floor((width - 1 - vis) / 2);
122516
+ const rightPad = width - 2 - vis - centerPad;
122517
+ lines.push(logoColor("│") + " ".repeat(centerPad) + truncated + " ".repeat(rightPad) + logoColor("│"));
122891
122518
  }
122892
- lines.push(primary("│") + " ".repeat(width - 2) + primary("│"));
122893
- lines.push(primary("╰" + "─".repeat(width - 2) + "╯"));
122519
+ lines.push(logoColor("│") + " ".repeat(width - 2) + logoColor("│"));
122520
+ lines.push(logoColor("╰" + "─".repeat(width - 2) + "╯"));
122894
122521
  lines.push("");
122895
122522
  return lines;
122896
122523
  }
@@ -126938,7 +126565,7 @@ async function confirmUninstall(host, label) {
126938
126565
  * restores the editor. The question and answer are never recorded in the
126939
126566
  * main conversation history.
126940
126567
  */
126941
- const SPINNER_FRAMES = [
126568
+ const SPINNER_FRAMES$1 = [
126942
126569
  "⠋",
126943
126570
  "⠙",
126944
126571
  "⠹",
@@ -126998,7 +126625,7 @@ var BtwOverlayComponent = class extends Container {
126998
126625
  startSpinner() {
126999
126626
  this.spinnerFrame = 0;
127000
126627
  this.spinnerInterval = setInterval(() => {
127001
- this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
126628
+ this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES$1.length;
127002
126629
  this.requestRender();
127003
126630
  }, 80);
127004
126631
  }
@@ -127018,7 +126645,7 @@ var BtwOverlayComponent = class extends Container {
127018
126645
  lines.push(chalk.hex(c.primary)("/btw") + chalk.hex(c.textMuted)(" — ") + chalk.hex(c.text)(truncated));
127019
126646
  lines.push("");
127020
126647
  if (this.status === "loading") {
127021
- const spinner = SPINNER_FRAMES[this.spinnerFrame];
126648
+ const spinner = SPINNER_FRAMES$1[this.spinnerFrame];
127022
126649
  lines.push(chalk.hex(c.textMuted)(`${spinner} Answering…`));
127023
126650
  } else if (this.status === "done" && this.markdown !== void 0) {
127024
126651
  const contentWidth = Math.max(20, width - 2);
@@ -132166,6 +131793,565 @@ const INITIAL_LIVE_PANE = {
132166
131793
  pendingQuestion: null
132167
131794
  };
132168
131795
  //#endregion
131796
+ //#region src/utils/git/git-status.ts
131797
+ /**
131798
+ * Cached git branch + working-tree status for the footer/statusline.
131799
+ *
131800
+ * Branch name refreshes every 5s, porcelain status every 15s. Branch
131801
+ * and status reads stay synchronous with short timeouts. Pull request
131802
+ * lookup uses an async cache so a slow `gh pr view` never blocks
131803
+ * footer rendering.
131804
+ */
131805
+ const BRANCH_TTL_MS = 5e3;
131806
+ const STATUS_TTL_MS = 15e3;
131807
+ const PULL_REQUEST_TTL_MS = 6e4;
131808
+ const SPAWN_TIMEOUT_MS = 500;
131809
+ const PR_SPAWN_TIMEOUT_MS = 5e3;
131810
+ const AHEAD_BEHIND_RE = /\[(?:ahead (\d+))?(?:, )?(?:behind (\d+))?\]/;
131811
+ function createGitStatusCache(workDir, options = {}) {
131812
+ const isRepo = detectGitRepo(workDir);
131813
+ let branch = {
131814
+ value: null,
131815
+ fetchedAt: 0
131816
+ };
131817
+ let status = {
131818
+ dirty: false,
131819
+ ahead: 0,
131820
+ behind: 0,
131821
+ diffAdded: 0,
131822
+ diffDeleted: 0,
131823
+ fetchedAt: 0
131824
+ };
131825
+ let pullRequest = {
131826
+ value: null,
131827
+ branch: null,
131828
+ fetchedAt: 0,
131829
+ pendingBranch: null,
131830
+ requestId: 0
131831
+ };
131832
+ return { getStatus: () => {
131833
+ if (!isRepo) return null;
131834
+ const now = Date.now();
131835
+ if (now - branch.fetchedAt >= BRANCH_TTL_MS) branch = {
131836
+ value: readBranch(workDir),
131837
+ fetchedAt: now
131838
+ };
131839
+ if (branch.value === null) return null;
131840
+ if (now - status.fetchedAt >= STATUS_TTL_MS) status = {
131841
+ ...readStatus(workDir),
131842
+ fetchedAt: now
131843
+ };
131844
+ refreshPullRequestIfNeeded(branch.value, now);
131845
+ return {
131846
+ branch: branch.value,
131847
+ dirty: status.dirty,
131848
+ ahead: status.ahead,
131849
+ behind: status.behind,
131850
+ diffAdded: status.diffAdded,
131851
+ diffDeleted: status.diffDeleted,
131852
+ pullRequest: pullRequest.branch === branch.value ? pullRequest.value : null
131853
+ };
131854
+ } };
131855
+ function refreshPullRequestIfNeeded(branchName, now) {
131856
+ if (pullRequest.pendingBranch === branchName) return;
131857
+ const fetchedAt = pullRequest.branch === branchName ? pullRequest.fetchedAt : 0;
131858
+ if (now - fetchedAt < PULL_REQUEST_TTL_MS) return;
131859
+ const requestId = pullRequest.requestId + 1;
131860
+ pullRequest = {
131861
+ value: pullRequest.branch === branchName ? pullRequest.value : null,
131862
+ branch: branchName,
131863
+ fetchedAt,
131864
+ pendingBranch: branchName,
131865
+ requestId
131866
+ };
131867
+ readPullRequest(workDir).then((value) => {
131868
+ if (pullRequest.requestId !== requestId) return;
131869
+ const changed = !samePullRequest(pullRequest.branch === branchName ? pullRequest.value : null, value);
131870
+ pullRequest = {
131871
+ value,
131872
+ branch: branchName,
131873
+ fetchedAt: Date.now(),
131874
+ pendingBranch: null,
131875
+ requestId
131876
+ };
131877
+ if (changed) options.onChange?.();
131878
+ });
131879
+ }
131880
+ }
131881
+ function detectGitRepo(workDir) {
131882
+ try {
131883
+ const result = spawnSync("git", [
131884
+ "-C",
131885
+ workDir,
131886
+ "rev-parse",
131887
+ "--is-inside-work-tree"
131888
+ ], {
131889
+ encoding: "utf8",
131890
+ timeout: SPAWN_TIMEOUT_MS
131891
+ });
131892
+ return result.status === 0 && result.stdout.trim() === "true";
131893
+ } catch {
131894
+ return false;
131895
+ }
131896
+ }
131897
+ function readBranch(workDir) {
131898
+ try {
131899
+ const result = spawnSync("git", [
131900
+ "-C",
131901
+ workDir,
131902
+ "branch",
131903
+ "--show-current"
131904
+ ], {
131905
+ encoding: "utf8",
131906
+ timeout: SPAWN_TIMEOUT_MS
131907
+ });
131908
+ if (result.status !== 0) return null;
131909
+ const name = result.stdout.trim();
131910
+ return name.length > 0 ? name : null;
131911
+ } catch {
131912
+ return null;
131913
+ }
131914
+ }
131915
+ function readStatus(workDir) {
131916
+ try {
131917
+ const result = spawnSync("git", [
131918
+ "-C",
131919
+ workDir,
131920
+ "status",
131921
+ "--porcelain",
131922
+ "-b"
131923
+ ], {
131924
+ encoding: "utf8",
131925
+ timeout: SPAWN_TIMEOUT_MS,
131926
+ maxBuffer: 4 * 1024 * 1024
131927
+ });
131928
+ if (result.status !== 0) return {
131929
+ dirty: false,
131930
+ ahead: 0,
131931
+ behind: 0,
131932
+ diffAdded: 0,
131933
+ diffDeleted: 0
131934
+ };
131935
+ let dirty = false;
131936
+ let ahead = 0;
131937
+ let behind = 0;
131938
+ for (const line of result.stdout.split("\n")) if (line.startsWith("## ")) {
131939
+ const m = AHEAD_BEHIND_RE.exec(line);
131940
+ if (m) {
131941
+ ahead = Number.parseInt(m[1] ?? "0", 10) || 0;
131942
+ behind = Number.parseInt(m[2] ?? "0", 10) || 0;
131943
+ }
131944
+ } else if (line.trim().length > 0) dirty = true;
131945
+ const diff = dirty ? readDiffStats(workDir) : {
131946
+ added: 0,
131947
+ deleted: 0
131948
+ };
131949
+ return {
131950
+ dirty,
131951
+ ahead,
131952
+ behind,
131953
+ diffAdded: diff.added,
131954
+ diffDeleted: diff.deleted
131955
+ };
131956
+ } catch {
131957
+ return {
131958
+ dirty: false,
131959
+ ahead: 0,
131960
+ behind: 0,
131961
+ diffAdded: 0,
131962
+ diffDeleted: 0
131963
+ };
131964
+ }
131965
+ }
131966
+ function readDiffStats(workDir) {
131967
+ try {
131968
+ const result = spawnSync("git", [
131969
+ "-C",
131970
+ workDir,
131971
+ "diff",
131972
+ "--numstat",
131973
+ "HEAD",
131974
+ "--"
131975
+ ], {
131976
+ encoding: "utf8",
131977
+ timeout: SPAWN_TIMEOUT_MS,
131978
+ maxBuffer: 4 * 1024 * 1024
131979
+ });
131980
+ if (result.status !== 0) return {
131981
+ added: 0,
131982
+ deleted: 0
131983
+ };
131984
+ let added = 0;
131985
+ let deleted = 0;
131986
+ for (const line of result.stdout.split("\n")) {
131987
+ if (!line) continue;
131988
+ const [addedText, deletedText] = line.split(" ");
131989
+ added += parseDiffNumstatCount(addedText);
131990
+ deleted += parseDiffNumstatCount(deletedText);
131991
+ }
131992
+ return {
131993
+ added,
131994
+ deleted
131995
+ };
131996
+ } catch {
131997
+ return {
131998
+ added: 0,
131999
+ deleted: 0
132000
+ };
132001
+ }
132002
+ }
132003
+ function parseDiffNumstatCount(value) {
132004
+ if (value === void 0 || value === "-") return 0;
132005
+ const n = Number.parseInt(value, 10);
132006
+ return Number.isFinite(n) && n > 0 ? n : 0;
132007
+ }
132008
+ function readPullRequest(workDir) {
132009
+ return new Promise((resolve) => {
132010
+ try {
132011
+ execFile("gh", [
132012
+ "pr",
132013
+ "view",
132014
+ "--json",
132015
+ "number,url"
132016
+ ], {
132017
+ cwd: workDir,
132018
+ encoding: "utf8",
132019
+ env: {
132020
+ ...process.env,
132021
+ GH_NO_UPDATE_NOTIFIER: "1",
132022
+ GH_PROMPT_DISABLED: "1"
132023
+ },
132024
+ timeout: PR_SPAWN_TIMEOUT_MS,
132025
+ maxBuffer: 256 * 1024
132026
+ }, (error, stdout) => {
132027
+ if (error !== null) {
132028
+ resolve(null);
132029
+ return;
132030
+ }
132031
+ resolve(parsePullRequest(stdout));
132032
+ });
132033
+ } catch {
132034
+ resolve(null);
132035
+ }
132036
+ });
132037
+ }
132038
+ function samePullRequest(a, b) {
132039
+ if (a === null || b === null) return a === b;
132040
+ return a.number === b.number && a.url === b.url;
132041
+ }
132042
+ function parsePullRequest(stdout) {
132043
+ try {
132044
+ const raw = JSON.parse(stdout);
132045
+ if (typeof raw !== "object" || raw === null) return null;
132046
+ const record = raw;
132047
+ const number = record["number"];
132048
+ const url = record["url"];
132049
+ if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) return null;
132050
+ if (typeof url !== "string" || !isSafeHttpUrl(url)) return null;
132051
+ return {
132052
+ number,
132053
+ url
132054
+ };
132055
+ } catch {
132056
+ return null;
132057
+ }
132058
+ }
132059
+ function isSafeHttpUrl(value) {
132060
+ if (hasControlChars(value)) return false;
132061
+ try {
132062
+ const url = new URL(value);
132063
+ return url.protocol === "https:" || url.protocol === "http:";
132064
+ } catch {
132065
+ return false;
132066
+ }
132067
+ }
132068
+ function hasControlChars(value) {
132069
+ for (const char of value) {
132070
+ const code = char.codePointAt(0) ?? 0;
132071
+ if (code <= 31 || code === 127) return true;
132072
+ }
132073
+ return false;
132074
+ }
132075
+ function formatGitBadgeBase(status) {
132076
+ const parts = [];
132077
+ const diff = formatDiffStats(status);
132078
+ if (diff) parts.push(diff);
132079
+ let sync = "";
132080
+ if (status.ahead > 0) sync += `↑${status.ahead}`;
132081
+ if (status.behind > 0) sync += `↓${status.behind}`;
132082
+ if (sync) parts.push(sync);
132083
+ return parts.length === 0 ? status.branch : `${status.branch} [${parts.join(" ")}]`;
132084
+ }
132085
+ function formatPullRequestBadge(pullRequest, options = {}) {
132086
+ const prText = `[PR#${String(pullRequest.number)}]`;
132087
+ return options.linkPullRequest ? toTerminalHyperlink(prText, pullRequest.url) : prText;
132088
+ }
132089
+ function formatDiffStats(status) {
132090
+ const parts = [];
132091
+ if (status.diffAdded > 0) parts.push(`+${String(status.diffAdded)}`);
132092
+ if (status.diffDeleted > 0) parts.push(`-${String(status.diffDeleted)}`);
132093
+ if (parts.length > 0) return parts.join(" ");
132094
+ return status.dirty ? "±" : null;
132095
+ }
132096
+ function toTerminalHyperlink(text, url) {
132097
+ if (!isSafeHttpUrl(url)) return text;
132098
+ return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`;
132099
+ }
132100
+ //#endregion
132101
+ //#region src/tui/components/chrome/footer.ts
132102
+ const MAX_CWD_SEGMENTS = 3;
132103
+ const TOOLBAR_TIPS = [
132104
+ { text: "shift+tab: 计划模式" },
132105
+ { text: "/model: 切换模型" },
132106
+ {
132107
+ text: "ctrl+s: 中途干预",
132108
+ priority: 2
132109
+ },
132110
+ {
132111
+ text: "/compact: 压缩上下文",
132112
+ priority: 2
132113
+ },
132114
+ { text: "ctrl+o: 展开工具输出" },
132115
+ { text: "/tasks: 后台任务" },
132116
+ { text: "shift+enter: 换行" },
132117
+ {
132118
+ text: "/init: 生成 AGENTS.md",
132119
+ priority: 2
132120
+ },
132121
+ { text: "@: 提及文件" },
132122
+ { text: "ctrl+c: 取消" },
132123
+ { text: "/theme: 切换主题" },
132124
+ { text: "/auto: 自动权限模式" },
132125
+ { text: "/yes: 自动批准" },
132126
+ { text: "/help: 显示命令" },
132127
+ {
132128
+ text: "/config: 选择并配置你常用的模型商",
132129
+ solo: true,
132130
+ priority: 3
132131
+ },
132132
+ {
132133
+ text: "让 Scream 安排任务,例如 \"2个小时后提醒我去拿快递\"",
132134
+ solo: true,
132135
+ priority: 3
132136
+ }
132137
+ ];
132138
+ /**
132139
+ * Expand tips into a rotation sequence using smooth weighted round-robin
132140
+ * (the nginx SWRR algorithm). Higher-`priority` tips appear more often while
132141
+ * staying evenly spread, so a tip generally does not land next to its own
132142
+ * duplicate. Deterministic and computed once at module load. Exported for
132143
+ * unit testing.
132144
+ */
132145
+ function buildWeightedTips(tips) {
132146
+ const items = tips.map((t) => ({
132147
+ tip: t,
132148
+ weight: Math.max(1, Math.trunc(t.priority ?? 1)),
132149
+ current: 0
132150
+ }));
132151
+ const total = items.reduce((sum, it) => sum + it.weight, 0);
132152
+ const seq = [];
132153
+ for (let n = 0; n < total; n++) {
132154
+ let best = items[0];
132155
+ for (const it of items) {
132156
+ it.current += it.weight;
132157
+ if (it.current > best.current) best = it;
132158
+ }
132159
+ best.current -= total;
132160
+ seq.push(best.tip);
132161
+ }
132162
+ return seq;
132163
+ }
132164
+ buildWeightedTips(TOOLBAR_TIPS);
132165
+ function shortenModel(model) {
132166
+ if (!model) return model;
132167
+ const slash = model.lastIndexOf("/");
132168
+ return slash >= 0 ? model.slice(slash + 1) : model;
132169
+ }
132170
+ function modelDisplayName(state) {
132171
+ const model = state.availableModels[state.model];
132172
+ return model?.displayName ?? model?.model ?? state.model;
132173
+ }
132174
+ function shortenCwd(path) {
132175
+ if (!path) return path;
132176
+ const home = process.env["HOME"] ?? "";
132177
+ let work = path;
132178
+ if (home && path === home) return "~";
132179
+ if (home && path.startsWith(home + "/")) work = "~" + path.slice(home.length);
132180
+ const segments = work.split("/").filter((s) => s.length > 0);
132181
+ if (segments.length <= MAX_CWD_SEGMENTS) return work;
132182
+ return `…/${segments.slice(-3).join("/")}`;
132183
+ }
132184
+ function formatTokenCount(n) {
132185
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
132186
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
132187
+ return String(n);
132188
+ }
132189
+ function safeUsage(usage) {
132190
+ return safeUsageRatio(usage);
132191
+ }
132192
+ function formatContextStatus(usage, tokens, maxTokens) {
132193
+ const pct = `${(safeUsage(usage) * 100).toFixed(1)}%`;
132194
+ if (maxTokens && maxTokens > 0 && tokens !== void 0) return `上下文:${pct} (${formatTokenCount(tokens)}/${formatTokenCount(maxTokens)})`;
132195
+ return `上下文:${pct}`;
132196
+ }
132197
+ const BRAND_COLORS = [
132198
+ "#72A4E9",
132199
+ "#A78BFA",
132200
+ "#34D399"
132201
+ ];
132202
+ const GRADIENT_CYCLE_MS = 4e3;
132203
+ const SPINNER_FRAMES = [
132204
+ "●",
132205
+ "◉",
132206
+ "◎",
132207
+ "◌",
132208
+ "○",
132209
+ "◌",
132210
+ "◎",
132211
+ "◉"
132212
+ ];
132213
+ const SPINNER_TICK_MS = 120;
132214
+ function hexToRgb(hex) {
132215
+ const v = parseInt(hex.slice(1), 16);
132216
+ return [
132217
+ v >> 16 & 255,
132218
+ v >> 8 & 255,
132219
+ v & 255
132220
+ ];
132221
+ }
132222
+ function lerpGradient(t) {
132223
+ const count = BRAND_COLORS.length;
132224
+ const segment = Math.min(t * count, count - 1);
132225
+ const idx = Math.floor(segment);
132226
+ const localT = segment - idx;
132227
+ const nextIdx = (idx + 1) % count;
132228
+ const [r0, g0, b0] = hexToRgb(BRAND_COLORS[idx]);
132229
+ const [r1, g1, b1] = hexToRgb(BRAND_COLORS[nextIdx]);
132230
+ const r = Math.round(r0 + (r1 - r0) * localT);
132231
+ const g = Math.round(g0 + (g1 - g0) * localT);
132232
+ const b = Math.round(b0 + (b1 - b0) * localT);
132233
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
132234
+ }
132235
+ function buildStatusLine(streamingPhase, livePaneMode, streamingStartTime) {
132236
+ if (streamingPhase === "idle" && livePaneMode !== "tool") return "○ 空闲";
132237
+ let label;
132238
+ if (livePaneMode === "tool") label = "执行中";
132239
+ else if (streamingPhase === "waiting") label = "等待响应";
132240
+ else if (streamingPhase === "thinking") label = "思考中";
132241
+ else if (streamingPhase === "composing") label = "输出中";
132242
+ else label = "";
132243
+ const elapsed = Date.now() - streamingStartTime;
132244
+ const totalSeconds = Math.floor(elapsed / 1e3);
132245
+ const elapsedStr = totalSeconds < 60 ? `${totalSeconds}s` : `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
132246
+ const now = Date.now();
132247
+ const frame = SPINNER_FRAMES[Math.floor(now / SPINNER_TICK_MS) % SPINNER_FRAMES.length];
132248
+ const gradientColor = lerpGradient(now % GRADIENT_CYCLE_MS / GRADIENT_CYCLE_MS);
132249
+ return chalk.hex(gradientColor).bold(frame) + " " + label + " " + elapsedStr;
132250
+ }
132251
+ function formatFooterGitBadge(status, colors) {
132252
+ const base = chalk.hex(colors.status)(formatGitBadgeBase(status));
132253
+ if (status.pullRequest === null) return base;
132254
+ return `${base} ${chalk.hex(colors.primary)(formatPullRequestBadge(status.pullRequest, { linkPullRequest: true }))}`;
132255
+ }
132256
+ var FooterComponent = class {
132257
+ state;
132258
+ colors;
132259
+ onGitStatusChange;
132260
+ gitCache;
132261
+ gitCacheWorkDir;
132262
+ transientHint = null;
132263
+ /**
132264
+ * Non-terminal background-task counts split by kind so the footer can
132265
+ * render two distinct badges. `bashTasks` covers `bash-*` BPM tasks
132266
+ * spawned via `Shell run_in_background=true`; `agentTasks` covers
132267
+ * `agent-*` BPM tasks (background subagents). Either zero hides its
132268
+ * respective badge.
132269
+ */
132270
+ backgroundBashTaskCount = 0;
132271
+ backgroundAgentCount = 0;
132272
+ constructor(state, colors, onGitStatusChange = () => {}) {
132273
+ this.state = state;
132274
+ this.colors = colors;
132275
+ this.onGitStatusChange = onGitStatusChange;
132276
+ this.gitCacheWorkDir = state.workDir;
132277
+ this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
132278
+ }
132279
+ setState(state) {
132280
+ if (state.workDir !== this.gitCacheWorkDir) {
132281
+ this.gitCacheWorkDir = state.workDir;
132282
+ this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
132283
+ }
132284
+ this.state = state;
132285
+ }
132286
+ setColors(colors) {
132287
+ this.colors = colors;
132288
+ }
132289
+ /**
132290
+ * Short-lived hint that replaces the rotating toolbar tips on line 1.
132291
+ * Used by the exit-confirmation double-tap flow to show "Press Ctrl+C
132292
+ * again to exit" without requiring a toast/overlay subsystem.
132293
+ * Pass `null` to clear.
132294
+ */
132295
+ setTransientHint(hint) {
132296
+ this.transientHint = hint;
132297
+ }
132298
+ /**
132299
+ * Sync both background-task badges with live counts. Each non-zero
132300
+ * count produces its own bracketed badge on line 1; zeros hide them
132301
+ * independently.
132302
+ */
132303
+ setBackgroundCounts(counts) {
132304
+ this.backgroundBashTaskCount = Math.max(0, counts.bashTasks);
132305
+ this.backgroundAgentCount = Math.max(0, counts.agentTasks);
132306
+ }
132307
+ invalidate() {}
132308
+ render(width) {
132309
+ const colors = this.colors;
132310
+ const state = this.state;
132311
+ const left = [];
132312
+ if (state.permissionMode === "auto") left.push(chalk.hex(colors.warning).bold("auto"));
132313
+ if (state.permissionMode === "yolo") left.push(chalk.hex(colors.warning).bold("YES"));
132314
+ if (state.planMode) left.push(chalk.hex(colors.primary).bold("plan"));
132315
+ if (state.wolfpackMode) left.push(chalk.hex(colors.primary).bold("wolfpack"));
132316
+ if (state.goalActive) left.push(chalk.hex(colors.primary).bold("goal"));
132317
+ const model = shortenModel(modelDisplayName(state));
132318
+ if (model) {
132319
+ const thinkingLabel = state.thinking ? " 思考中" : "";
132320
+ left.push(chalk.hex(colors.text)(`${model}${thinkingLabel}`));
132321
+ }
132322
+ if (this.backgroundBashTaskCount > 0) {
132323
+ const noun = this.backgroundBashTaskCount === 1 ? "个任务" : "个任务";
132324
+ left.push(chalk.hex(colors.primary)(`[${String(this.backgroundBashTaskCount)}${noun} 运行中]`));
132325
+ }
132326
+ if (this.backgroundAgentCount > 0) {
132327
+ const noun = this.backgroundAgentCount === 1 ? "个代理" : "个代理";
132328
+ left.push(chalk.hex(colors.primary)(`[${String(this.backgroundAgentCount)}${noun} 运行中]`));
132329
+ }
132330
+ const cwd = shortenCwd(state.workDir);
132331
+ if (cwd) left.push(chalk.hex(colors.status)(cwd));
132332
+ const git = this.gitCache.getStatus();
132333
+ if (git !== null) left.push(formatFooterGitBadge(git, colors));
132334
+ const leftLine = left.join(" ");
132335
+ const leftWidth = visibleWidth(leftLine);
132336
+ let rightText;
132337
+ if (this.transientHint) rightText = chalk.hex(colors.warning).bold(this.transientHint);
132338
+ else {
132339
+ const statusLine = buildStatusLine(state.streamingPhase, state.livePaneMode, state.streamingStartTime);
132340
+ const ccDot = state.ccConnectActive ? chalk.hex(colors.success)("●") : chalk.hex(colors.textDim)("●");
132341
+ rightText = chalk.hex(colors.textDim)(ccDot + " " + formatContextStatus(state.contextUsage, state.contextTokens, state.maxContextTokens) + " " + statusLine);
132342
+ }
132343
+ const rightWidth = visibleWidth(rightText);
132344
+ const gap = 3;
132345
+ let line1;
132346
+ if (leftWidth + gap + rightWidth <= width) {
132347
+ const pad = width - leftWidth - rightWidth;
132348
+ line1 = leftLine + " ".repeat(pad) + rightText;
132349
+ } else if (leftWidth <= width) line1 = leftLine;
132350
+ else line1 = truncateToWidth(leftLine, width, "…");
132351
+ return [truncateToWidth(line1, width)];
132352
+ }
132353
+ };
132354
+ //#endregion
132169
132355
  //#region src/tui/components/chrome/todo-panel.ts
132170
132356
  const MAX_VISIBLE = 5;
132171
132357
  /**
@@ -136598,7 +136784,7 @@ const SHADOW_CHARS = new Set([
136598
136784
  ]);
136599
136785
  const SHEEN_STEP = 2;
136600
136786
  const SHEEN_INTERVAL_MS = 150;
136601
- const LOADING_DURATION_MS = 2200;
136787
+ const LOADING_DURATION_MS = 1500;
136602
136788
  const THEME_ACCENT = {
136603
136789
  dark: [
136604
136790
  78,