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.
- package/dist/main.mjs +929 -743
- 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
|
-
|
|
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
|
-
|
|
122136
|
-
|
|
122137
|
-
|
|
122138
|
-
|
|
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
|
-
|
|
122222
|
-
|
|
122223
|
-
|
|
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
|
-
|
|
122238
|
-
|
|
122239
|
-
|
|
122240
|
-
|
|
122241
|
-
|
|
122242
|
-
|
|
122243
|
-
|
|
122244
|
-
|
|
122245
|
-
|
|
122246
|
-
|
|
122247
|
-
|
|
122248
|
-
|
|
122249
|
-
|
|
122250
|
-
|
|
122251
|
-
|
|
122252
|
-
|
|
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
|
|
122256
|
-
|
|
122257
|
-
|
|
122258
|
-
|
|
122259
|
-
|
|
122260
|
-
|
|
122261
|
-
|
|
122262
|
-
|
|
122263
|
-
|
|
122264
|
-
|
|
122265
|
-
|
|
122266
|
-
|
|
122267
|
-
|
|
122268
|
-
|
|
122269
|
-
|
|
122270
|
-
|
|
122271
|
-
|
|
122272
|
-
|
|
122273
|
-
|
|
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
|
|
122307
|
-
|
|
122308
|
-
|
|
122309
|
-
|
|
122310
|
-
|
|
122311
|
-
|
|
122312
|
-
|
|
122313
|
-
|
|
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
|
|
122344
|
-
|
|
122345
|
-
|
|
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
|
-
|
|
122383
|
-
|
|
122384
|
-
|
|
122385
|
-
|
|
122386
|
-
|
|
122387
|
-
|
|
122388
|
-
|
|
122389
|
-
|
|
122390
|
-
|
|
122391
|
-
|
|
122392
|
-
|
|
122393
|
-
|
|
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
|
-
|
|
122400
|
-
|
|
122401
|
-
|
|
122402
|
-
|
|
122403
|
-
|
|
122404
|
-
|
|
122405
|
-
|
|
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
|
-
|
|
122409
|
-
|
|
122410
|
-
|
|
122411
|
-
|
|
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
|
-
|
|
122414
|
-
|
|
122415
|
-
|
|
122416
|
-
|
|
122417
|
-
|
|
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
|
|
122859
|
-
const
|
|
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
|
|
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
|
-
...
|
|
122488
|
+
...logo,
|
|
122875
122489
|
"",
|
|
122876
|
-
|
|
122490
|
+
labelStyle("版本:") + " " + versionValue,
|
|
122491
|
+
labelStyle("模型:") + " " + modelValue,
|
|
122492
|
+
labelStyle("目录:") + " " + this.state.workDir,
|
|
122877
122493
|
"",
|
|
122878
|
-
|
|
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
|
-
|
|
122884
|
-
|
|
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
|
|
122890
|
-
|
|
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(
|
|
122893
|
-
lines.push(
|
|
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 =
|
|
136787
|
+
const LOADING_DURATION_MS = 1500;
|
|
136602
136788
|
const THEME_ACCENT = {
|
|
136603
136789
|
dark: [
|
|
136604
136790
|
78,
|