scream-code 0.5.3 → 0.5.5
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 +1844 -1763
- package/package.json +1 -1
- package/scripts/postinstall.mjs +0 -4
- package/scripts/postinstall/patch-pi-tui.mjs +0 -68
package/dist/main.mjs
CHANGED
|
@@ -118880,6 +118880,7 @@ function escapeTomlBasicString(value) {
|
|
|
118880
118880
|
}
|
|
118881
118881
|
//#endregion
|
|
118882
118882
|
//#region src/tui/constant/rendering.ts
|
|
118883
|
+
const MAX_SHELL_OUTPUT_BYTES = 128 * 1024;
|
|
118883
118884
|
const BRAILLE_SPINNER_FRAMES = [
|
|
118884
118885
|
"⠋",
|
|
118885
118886
|
"⠙",
|
|
@@ -121157,13 +121158,19 @@ var UsagePanelComponent = class {
|
|
|
121157
121158
|
lines;
|
|
121158
121159
|
borderHex;
|
|
121159
121160
|
title;
|
|
121161
|
+
cachedWidth;
|
|
121162
|
+
cachedLines;
|
|
121160
121163
|
constructor(lines, borderHex, title = " 用量 ") {
|
|
121161
121164
|
this.lines = lines;
|
|
121162
121165
|
this.borderHex = borderHex;
|
|
121163
121166
|
this.title = title;
|
|
121164
121167
|
}
|
|
121165
|
-
invalidate() {
|
|
121168
|
+
invalidate() {
|
|
121169
|
+
this.cachedWidth = void 0;
|
|
121170
|
+
this.cachedLines = void 0;
|
|
121171
|
+
}
|
|
121166
121172
|
render(width) {
|
|
121173
|
+
if (this.cachedLines !== void 0 && this.cachedWidth === width) return this.cachedLines;
|
|
121167
121174
|
const paint = (s) => chalk.hex(this.borderHex)(s);
|
|
121168
121175
|
const indent = " ".repeat(LEFT_MARGIN$1);
|
|
121169
121176
|
const availableInterior = Math.max(MIN_INTERIOR_WIDTH, width - LEFT_MARGIN$1 - 2 - 2 * SIDE_PADDING$1);
|
|
@@ -121180,6 +121187,8 @@ var UsagePanelComponent = class {
|
|
|
121180
121187
|
out.push(indent + paint("│") + " " + clipped + " ".repeat(pad) + " " + paint("│"));
|
|
121181
121188
|
}
|
|
121182
121189
|
out.push(bottom);
|
|
121190
|
+
this.cachedWidth = width;
|
|
121191
|
+
this.cachedLines = out;
|
|
121183
121192
|
return out;
|
|
121184
121193
|
}
|
|
121185
121194
|
};
|
|
@@ -122161,16 +122170,29 @@ function buildEmptyGoalLines(colors) {
|
|
|
122161
122170
|
var GoalStatusMessageComponent = class {
|
|
122162
122171
|
goal;
|
|
122163
122172
|
colors;
|
|
122173
|
+
panel;
|
|
122174
|
+
cachedWidth;
|
|
122175
|
+
cachedLines;
|
|
122164
122176
|
constructor(goal, colors) {
|
|
122165
122177
|
this.goal = goal;
|
|
122166
122178
|
this.colors = colors;
|
|
122179
|
+
if (goal === null) this.panel = new UsagePanelComponent(buildEmptyGoalLines(this.colors), this.colors.success, " Scream Goal ");
|
|
122180
|
+
else {
|
|
122181
|
+
const title = ` Scream Goal · ${statusLabel(goal.status)} `;
|
|
122182
|
+
this.panel = new UsagePanelComponent(buildGoalReportLines(goal, this.colors), this.colors.success, title);
|
|
122183
|
+
}
|
|
122184
|
+
}
|
|
122185
|
+
invalidate() {
|
|
122186
|
+
this.cachedWidth = void 0;
|
|
122187
|
+
this.cachedLines = void 0;
|
|
122188
|
+
this.panel.invalidate();
|
|
122167
122189
|
}
|
|
122168
|
-
invalidate() {}
|
|
122169
122190
|
render(width) {
|
|
122170
|
-
|
|
122171
|
-
|
|
122172
|
-
|
|
122173
|
-
|
|
122191
|
+
if (this.cachedLines !== void 0 && this.cachedWidth === width) return this.cachedLines;
|
|
122192
|
+
const lines = ["", ...this.panel.render(width)];
|
|
122193
|
+
this.cachedWidth = width;
|
|
122194
|
+
this.cachedLines = lines;
|
|
122195
|
+
return lines;
|
|
122174
122196
|
}
|
|
122175
122197
|
};
|
|
122176
122198
|
//#endregion
|
|
@@ -122350,703 +122372,117 @@ function dismissGoalPanel(host) {
|
|
|
122350
122372
|
}
|
|
122351
122373
|
}
|
|
122352
122374
|
//#endregion
|
|
122353
|
-
//#region src/
|
|
122375
|
+
//#region src/tui/components/chrome/welcome.ts
|
|
122376
|
+
const HUE_STOPS = 24;
|
|
122377
|
+
const SUB_STEPS = 5;
|
|
122378
|
+
const BREATHE_STEPS = HUE_STOPS * SUB_STEPS;
|
|
122379
|
+
const BREATHE_INTERVAL_MS = 40;
|
|
122380
|
+
const LOGO_FRAMES = [
|
|
122381
|
+
["██▄▄▄██", "▐█▄▀▄█▌"],
|
|
122382
|
+
["██▄▄▄██", "▐▄▄▀▄▄▌"],
|
|
122383
|
+
["██▄▄▄██", "▐▄▀▄▄▄▌"],
|
|
122384
|
+
["██▄▄▄██", "▐▄▄▄▀▄▌"],
|
|
122385
|
+
["██▄▄▄██", "▐█▄▀▄█▌"]
|
|
122386
|
+
];
|
|
122387
|
+
function hexToRgb$1(hex) {
|
|
122388
|
+
return [
|
|
122389
|
+
parseInt(hex.slice(1, 3), 16),
|
|
122390
|
+
parseInt(hex.slice(3, 5), 16),
|
|
122391
|
+
parseInt(hex.slice(5, 7), 16)
|
|
122392
|
+
];
|
|
122393
|
+
}
|
|
122394
|
+
function rgbToHsl(r, g, b) {
|
|
122395
|
+
const rf = r / 255, gf = g / 255, bf = b / 255;
|
|
122396
|
+
const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf);
|
|
122397
|
+
const l = (max + min) / 2;
|
|
122398
|
+
if (max === min) return [
|
|
122399
|
+
0,
|
|
122400
|
+
0,
|
|
122401
|
+
l * 100
|
|
122402
|
+
];
|
|
122403
|
+
const d = max - min;
|
|
122404
|
+
const s = l > .5 ? d / (2 - max - min) : d / (max + min);
|
|
122405
|
+
let h = 0;
|
|
122406
|
+
if (max === rf) h = ((gf - bf) / d + (gf < bf ? 6 : 0)) / 6;
|
|
122407
|
+
else if (max === gf) h = ((bf - rf) / d + 2) / 6;
|
|
122408
|
+
else h = ((rf - gf) / d + 4) / 6;
|
|
122409
|
+
return [
|
|
122410
|
+
h * 360,
|
|
122411
|
+
s * 100,
|
|
122412
|
+
l * 100
|
|
122413
|
+
];
|
|
122414
|
+
}
|
|
122415
|
+
function hslToRgb(h, s, l) {
|
|
122416
|
+
const hf = (h % 360 + 360) % 360 / 360;
|
|
122417
|
+
const sf = s / 100, lf = l / 100;
|
|
122418
|
+
if (sf === 0) {
|
|
122419
|
+
const v = Math.round(lf * 255);
|
|
122420
|
+
return [
|
|
122421
|
+
v,
|
|
122422
|
+
v,
|
|
122423
|
+
v
|
|
122424
|
+
];
|
|
122425
|
+
}
|
|
122426
|
+
const q = lf < .5 ? lf * (1 + sf) : lf + sf - lf * sf;
|
|
122427
|
+
const p = 2 * lf - q;
|
|
122428
|
+
const hue = (t) => {
|
|
122429
|
+
if (t < 0) t += 1;
|
|
122430
|
+
if (t > 1) t -= 1;
|
|
122431
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
122432
|
+
if (t < 1 / 2) return q;
|
|
122433
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
122434
|
+
return p;
|
|
122435
|
+
};
|
|
122436
|
+
return [
|
|
122437
|
+
Math.round(hue(hf + 1 / 3) * 255),
|
|
122438
|
+
Math.round(hue(hf) * 255),
|
|
122439
|
+
Math.round(hue(hf - 1 / 3) * 255)
|
|
122440
|
+
];
|
|
122441
|
+
}
|
|
122442
|
+
function rgbToHex(r, g, b) {
|
|
122443
|
+
const c = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
|
|
122444
|
+
return `#${c(r)}${c(g)}${c(b)}`;
|
|
122445
|
+
}
|
|
122354
122446
|
/**
|
|
122355
|
-
*
|
|
122356
|
-
*
|
|
122357
|
-
*
|
|
122358
|
-
* and status reads stay synchronous with short timeouts. Pull request
|
|
122359
|
-
* lookup uses an async cache so a slow `gh pr view` never blocks
|
|
122360
|
-
* footer rendering.
|
|
122447
|
+
* Build a full-hue-wheel palette anchored at the primary-green hue.
|
|
122448
|
+
* At frame 0 (and BREATHE_STEPS) the colour is pure primary; in between
|
|
122449
|
+
* it sweeps through all 24 hue stops with smooth sub-step interpolation.
|
|
122361
122450
|
*/
|
|
122362
|
-
|
|
122363
|
-
const
|
|
122364
|
-
const
|
|
122365
|
-
const
|
|
122366
|
-
const
|
|
122367
|
-
|
|
122368
|
-
|
|
122369
|
-
|
|
122370
|
-
let branch = {
|
|
122371
|
-
value: null,
|
|
122372
|
-
fetchedAt: 0
|
|
122373
|
-
};
|
|
122374
|
-
let status = {
|
|
122375
|
-
dirty: false,
|
|
122376
|
-
ahead: 0,
|
|
122377
|
-
behind: 0,
|
|
122378
|
-
diffAdded: 0,
|
|
122379
|
-
diffDeleted: 0,
|
|
122380
|
-
fetchedAt: 0
|
|
122381
|
-
};
|
|
122382
|
-
let pullRequest = {
|
|
122383
|
-
value: null,
|
|
122384
|
-
branch: null,
|
|
122385
|
-
fetchedAt: 0,
|
|
122386
|
-
pendingBranch: null,
|
|
122387
|
-
requestId: 0
|
|
122388
|
-
};
|
|
122389
|
-
return { getStatus: () => {
|
|
122390
|
-
if (!isRepo) return null;
|
|
122391
|
-
const now = Date.now();
|
|
122392
|
-
if (now - branch.fetchedAt >= BRANCH_TTL_MS) branch = {
|
|
122393
|
-
value: readBranch(workDir),
|
|
122394
|
-
fetchedAt: now
|
|
122395
|
-
};
|
|
122396
|
-
if (branch.value === null) return null;
|
|
122397
|
-
if (now - status.fetchedAt >= STATUS_TTL_MS) status = {
|
|
122398
|
-
...readStatus(workDir),
|
|
122399
|
-
fetchedAt: now
|
|
122400
|
-
};
|
|
122401
|
-
refreshPullRequestIfNeeded(branch.value, now);
|
|
122402
|
-
return {
|
|
122403
|
-
branch: branch.value,
|
|
122404
|
-
dirty: status.dirty,
|
|
122405
|
-
ahead: status.ahead,
|
|
122406
|
-
behind: status.behind,
|
|
122407
|
-
diffAdded: status.diffAdded,
|
|
122408
|
-
diffDeleted: status.diffDeleted,
|
|
122409
|
-
pullRequest: pullRequest.branch === branch.value ? pullRequest.value : null
|
|
122410
|
-
};
|
|
122411
|
-
} };
|
|
122412
|
-
function refreshPullRequestIfNeeded(branchName, now) {
|
|
122413
|
-
if (pullRequest.pendingBranch === branchName) return;
|
|
122414
|
-
const fetchedAt = pullRequest.branch === branchName ? pullRequest.fetchedAt : 0;
|
|
122415
|
-
if (now - fetchedAt < PULL_REQUEST_TTL_MS) return;
|
|
122416
|
-
const requestId = pullRequest.requestId + 1;
|
|
122417
|
-
pullRequest = {
|
|
122418
|
-
value: pullRequest.branch === branchName ? pullRequest.value : null,
|
|
122419
|
-
branch: branchName,
|
|
122420
|
-
fetchedAt,
|
|
122421
|
-
pendingBranch: branchName,
|
|
122422
|
-
requestId
|
|
122423
|
-
};
|
|
122424
|
-
readPullRequest(workDir).then((value) => {
|
|
122425
|
-
if (pullRequest.requestId !== requestId) return;
|
|
122426
|
-
const changed = !samePullRequest(pullRequest.branch === branchName ? pullRequest.value : null, value);
|
|
122427
|
-
pullRequest = {
|
|
122428
|
-
value,
|
|
122429
|
-
branch: branchName,
|
|
122430
|
-
fetchedAt: Date.now(),
|
|
122431
|
-
pendingBranch: null,
|
|
122432
|
-
requestId
|
|
122433
|
-
};
|
|
122434
|
-
if (changed) options.onChange?.();
|
|
122435
|
-
});
|
|
122451
|
+
function buildBreathingPalette(primaryHex, hueStops, subSteps) {
|
|
122452
|
+
const [r, g, b] = hexToRgb$1(primaryHex);
|
|
122453
|
+
const [baseHue] = rgbToHsl(r, g, b);
|
|
122454
|
+
const steps = hueStops * subSteps;
|
|
122455
|
+
const palette = [];
|
|
122456
|
+
for (let i = 0; i < steps; i++) {
|
|
122457
|
+
const [rr, gg, bb] = hslToRgb((baseHue + i / steps * 360) % 360, 90, 70);
|
|
122458
|
+
palette.push(rgbToHex(rr, gg, bb));
|
|
122436
122459
|
}
|
|
122460
|
+
return palette;
|
|
122437
122461
|
}
|
|
122438
|
-
|
|
122439
|
-
|
|
122440
|
-
|
|
122441
|
-
|
|
122442
|
-
|
|
122443
|
-
|
|
122444
|
-
|
|
122445
|
-
|
|
122446
|
-
|
|
122447
|
-
|
|
122448
|
-
|
|
122449
|
-
|
|
122450
|
-
|
|
122451
|
-
|
|
122462
|
+
var WelcomeComponent = class {
|
|
122463
|
+
state;
|
|
122464
|
+
colors;
|
|
122465
|
+
ui;
|
|
122466
|
+
breatheFrame = 0;
|
|
122467
|
+
breatheTimer = null;
|
|
122468
|
+
breathePalette;
|
|
122469
|
+
borderTitle = null;
|
|
122470
|
+
constructor(state, colors, ui) {
|
|
122471
|
+
this.state = state;
|
|
122472
|
+
this.colors = colors;
|
|
122473
|
+
this.ui = ui;
|
|
122474
|
+
this.breathePalette = buildBreathingPalette(colors.primary, HUE_STOPS, SUB_STEPS);
|
|
122475
|
+
this.startBreathing();
|
|
122452
122476
|
}
|
|
122453
|
-
|
|
122454
|
-
|
|
122455
|
-
|
|
122456
|
-
|
|
122457
|
-
|
|
122458
|
-
|
|
122459
|
-
|
|
122460
|
-
|
|
122461
|
-
|
|
122462
|
-
encoding: "utf8",
|
|
122463
|
-
timeout: SPAWN_TIMEOUT_MS
|
|
122464
|
-
});
|
|
122465
|
-
if (result.status !== 0) return null;
|
|
122466
|
-
const name = result.stdout.trim();
|
|
122467
|
-
return name.length > 0 ? name : null;
|
|
122468
|
-
} catch {
|
|
122469
|
-
return null;
|
|
122470
|
-
}
|
|
122471
|
-
}
|
|
122472
|
-
function readStatus(workDir) {
|
|
122473
|
-
try {
|
|
122474
|
-
const result = spawnSync("git", [
|
|
122475
|
-
"-C",
|
|
122476
|
-
workDir,
|
|
122477
|
-
"status",
|
|
122478
|
-
"--porcelain",
|
|
122479
|
-
"-b"
|
|
122480
|
-
], {
|
|
122481
|
-
encoding: "utf8",
|
|
122482
|
-
timeout: SPAWN_TIMEOUT_MS,
|
|
122483
|
-
maxBuffer: 4 * 1024 * 1024
|
|
122484
|
-
});
|
|
122485
|
-
if (result.status !== 0) return {
|
|
122486
|
-
dirty: false,
|
|
122487
|
-
ahead: 0,
|
|
122488
|
-
behind: 0,
|
|
122489
|
-
diffAdded: 0,
|
|
122490
|
-
diffDeleted: 0
|
|
122491
|
-
};
|
|
122492
|
-
let dirty = false;
|
|
122493
|
-
let ahead = 0;
|
|
122494
|
-
let behind = 0;
|
|
122495
|
-
for (const line of result.stdout.split("\n")) if (line.startsWith("## ")) {
|
|
122496
|
-
const m = AHEAD_BEHIND_RE.exec(line);
|
|
122497
|
-
if (m) {
|
|
122498
|
-
ahead = Number.parseInt(m[1] ?? "0", 10) || 0;
|
|
122499
|
-
behind = Number.parseInt(m[2] ?? "0", 10) || 0;
|
|
122500
|
-
}
|
|
122501
|
-
} else if (line.trim().length > 0) dirty = true;
|
|
122502
|
-
const diff = dirty ? readDiffStats(workDir) : {
|
|
122503
|
-
added: 0,
|
|
122504
|
-
deleted: 0
|
|
122505
|
-
};
|
|
122506
|
-
return {
|
|
122507
|
-
dirty,
|
|
122508
|
-
ahead,
|
|
122509
|
-
behind,
|
|
122510
|
-
diffAdded: diff.added,
|
|
122511
|
-
diffDeleted: diff.deleted
|
|
122512
|
-
};
|
|
122513
|
-
} catch {
|
|
122514
|
-
return {
|
|
122515
|
-
dirty: false,
|
|
122516
|
-
ahead: 0,
|
|
122517
|
-
behind: 0,
|
|
122518
|
-
diffAdded: 0,
|
|
122519
|
-
diffDeleted: 0
|
|
122520
|
-
};
|
|
122521
|
-
}
|
|
122522
|
-
}
|
|
122523
|
-
function readDiffStats(workDir) {
|
|
122524
|
-
try {
|
|
122525
|
-
const result = spawnSync("git", [
|
|
122526
|
-
"-C",
|
|
122527
|
-
workDir,
|
|
122528
|
-
"diff",
|
|
122529
|
-
"--numstat",
|
|
122530
|
-
"HEAD",
|
|
122531
|
-
"--"
|
|
122532
|
-
], {
|
|
122533
|
-
encoding: "utf8",
|
|
122534
|
-
timeout: SPAWN_TIMEOUT_MS,
|
|
122535
|
-
maxBuffer: 4 * 1024 * 1024
|
|
122536
|
-
});
|
|
122537
|
-
if (result.status !== 0) return {
|
|
122538
|
-
added: 0,
|
|
122539
|
-
deleted: 0
|
|
122540
|
-
};
|
|
122541
|
-
let added = 0;
|
|
122542
|
-
let deleted = 0;
|
|
122543
|
-
for (const line of result.stdout.split("\n")) {
|
|
122544
|
-
if (!line) continue;
|
|
122545
|
-
const [addedText, deletedText] = line.split(" ");
|
|
122546
|
-
added += parseDiffNumstatCount(addedText);
|
|
122547
|
-
deleted += parseDiffNumstatCount(deletedText);
|
|
122548
|
-
}
|
|
122549
|
-
return {
|
|
122550
|
-
added,
|
|
122551
|
-
deleted
|
|
122552
|
-
};
|
|
122553
|
-
} catch {
|
|
122554
|
-
return {
|
|
122555
|
-
added: 0,
|
|
122556
|
-
deleted: 0
|
|
122557
|
-
};
|
|
122558
|
-
}
|
|
122559
|
-
}
|
|
122560
|
-
function parseDiffNumstatCount(value) {
|
|
122561
|
-
if (value === void 0 || value === "-") return 0;
|
|
122562
|
-
const n = Number.parseInt(value, 10);
|
|
122563
|
-
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
122564
|
-
}
|
|
122565
|
-
function readPullRequest(workDir) {
|
|
122566
|
-
return new Promise((resolve) => {
|
|
122567
|
-
try {
|
|
122568
|
-
execFile("gh", [
|
|
122569
|
-
"pr",
|
|
122570
|
-
"view",
|
|
122571
|
-
"--json",
|
|
122572
|
-
"number,url"
|
|
122573
|
-
], {
|
|
122574
|
-
cwd: workDir,
|
|
122575
|
-
encoding: "utf8",
|
|
122576
|
-
env: {
|
|
122577
|
-
...process.env,
|
|
122578
|
-
GH_NO_UPDATE_NOTIFIER: "1",
|
|
122579
|
-
GH_PROMPT_DISABLED: "1"
|
|
122580
|
-
},
|
|
122581
|
-
timeout: PR_SPAWN_TIMEOUT_MS,
|
|
122582
|
-
maxBuffer: 256 * 1024
|
|
122583
|
-
}, (error, stdout) => {
|
|
122584
|
-
if (error !== null) {
|
|
122585
|
-
resolve(null);
|
|
122586
|
-
return;
|
|
122587
|
-
}
|
|
122588
|
-
resolve(parsePullRequest(stdout));
|
|
122589
|
-
});
|
|
122590
|
-
} catch {
|
|
122591
|
-
resolve(null);
|
|
122592
|
-
}
|
|
122593
|
-
});
|
|
122594
|
-
}
|
|
122595
|
-
function samePullRequest(a, b) {
|
|
122596
|
-
if (a === null || b === null) return a === b;
|
|
122597
|
-
return a.number === b.number && a.url === b.url;
|
|
122598
|
-
}
|
|
122599
|
-
function parsePullRequest(stdout) {
|
|
122600
|
-
try {
|
|
122601
|
-
const raw = JSON.parse(stdout);
|
|
122602
|
-
if (typeof raw !== "object" || raw === null) return null;
|
|
122603
|
-
const record = raw;
|
|
122604
|
-
const number = record["number"];
|
|
122605
|
-
const url = record["url"];
|
|
122606
|
-
if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) return null;
|
|
122607
|
-
if (typeof url !== "string" || !isSafeHttpUrl(url)) return null;
|
|
122608
|
-
return {
|
|
122609
|
-
number,
|
|
122610
|
-
url
|
|
122611
|
-
};
|
|
122612
|
-
} catch {
|
|
122613
|
-
return null;
|
|
122614
|
-
}
|
|
122615
|
-
}
|
|
122616
|
-
function isSafeHttpUrl(value) {
|
|
122617
|
-
if (hasControlChars(value)) return false;
|
|
122618
|
-
try {
|
|
122619
|
-
const url = new URL(value);
|
|
122620
|
-
return url.protocol === "https:" || url.protocol === "http:";
|
|
122621
|
-
} catch {
|
|
122622
|
-
return false;
|
|
122623
|
-
}
|
|
122624
|
-
}
|
|
122625
|
-
function hasControlChars(value) {
|
|
122626
|
-
for (const char of value) {
|
|
122627
|
-
const code = char.codePointAt(0) ?? 0;
|
|
122628
|
-
if (code <= 31 || code === 127) return true;
|
|
122629
|
-
}
|
|
122630
|
-
return false;
|
|
122631
|
-
}
|
|
122632
|
-
function formatGitBadgeBase(status) {
|
|
122633
|
-
const parts = [];
|
|
122634
|
-
const diff = formatDiffStats(status);
|
|
122635
|
-
if (diff) parts.push(diff);
|
|
122636
|
-
let sync = "";
|
|
122637
|
-
if (status.ahead > 0) sync += `↑${status.ahead}`;
|
|
122638
|
-
if (status.behind > 0) sync += `↓${status.behind}`;
|
|
122639
|
-
if (sync) parts.push(sync);
|
|
122640
|
-
return parts.length === 0 ? status.branch : `${status.branch} [${parts.join(" ")}]`;
|
|
122641
|
-
}
|
|
122642
|
-
function formatPullRequestBadge(pullRequest, options = {}) {
|
|
122643
|
-
const prText = `[PR#${String(pullRequest.number)}]`;
|
|
122644
|
-
return options.linkPullRequest ? toTerminalHyperlink(prText, pullRequest.url) : prText;
|
|
122645
|
-
}
|
|
122646
|
-
function formatDiffStats(status) {
|
|
122647
|
-
const parts = [];
|
|
122648
|
-
if (status.diffAdded > 0) parts.push(`+${String(status.diffAdded)}`);
|
|
122649
|
-
if (status.diffDeleted > 0) parts.push(`-${String(status.diffDeleted)}`);
|
|
122650
|
-
if (parts.length > 0) return parts.join(" ");
|
|
122651
|
-
return status.dirty ? "±" : null;
|
|
122652
|
-
}
|
|
122653
|
-
function toTerminalHyperlink(text, url) {
|
|
122654
|
-
if (!isSafeHttpUrl(url)) return text;
|
|
122655
|
-
return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`;
|
|
122656
|
-
}
|
|
122657
|
-
//#endregion
|
|
122658
|
-
//#region src/tui/components/chrome/footer.ts
|
|
122659
|
-
const MAX_CWD_SEGMENTS = 3;
|
|
122660
|
-
const TIP_ROTATE_INTERVAL_MS = 1e4;
|
|
122661
|
-
const TOOLBAR_TIPS = [
|
|
122662
|
-
{ text: "shift+tab: 计划模式" },
|
|
122663
|
-
{ text: "/model: 切换模型" },
|
|
122664
|
-
{
|
|
122665
|
-
text: "ctrl+s: 中途干预",
|
|
122666
|
-
priority: 2
|
|
122667
|
-
},
|
|
122668
|
-
{
|
|
122669
|
-
text: "/compact: 压缩上下文",
|
|
122670
|
-
priority: 2
|
|
122671
|
-
},
|
|
122672
|
-
{ text: "ctrl+o: 展开工具输出" },
|
|
122673
|
-
{ text: "/tasks: 后台任务" },
|
|
122674
|
-
{ text: "shift+enter: 换行" },
|
|
122675
|
-
{
|
|
122676
|
-
text: "/init: 生成 AGENTS.md",
|
|
122677
|
-
priority: 2
|
|
122678
|
-
},
|
|
122679
|
-
{ text: "@: 提及文件" },
|
|
122680
|
-
{ text: "ctrl+c: 取消" },
|
|
122681
|
-
{ text: "/theme: 切换主题" },
|
|
122682
|
-
{ text: "/auto: 自动权限模式" },
|
|
122683
|
-
{ text: "/yes: 自动批准" },
|
|
122684
|
-
{ text: "/help: 显示命令" },
|
|
122685
|
-
{
|
|
122686
|
-
text: "/config: 选择并配置你常用的模型商",
|
|
122687
|
-
solo: true,
|
|
122688
|
-
priority: 3
|
|
122689
|
-
},
|
|
122690
|
-
{
|
|
122691
|
-
text: "让 Scream 安排任务,例如 \"2个小时后提醒我去拿快递\"",
|
|
122692
|
-
solo: true,
|
|
122693
|
-
priority: 3
|
|
122694
|
-
}
|
|
122695
|
-
];
|
|
122696
|
-
/**
|
|
122697
|
-
* Expand tips into a rotation sequence using smooth weighted round-robin
|
|
122698
|
-
* (the nginx SWRR algorithm). Higher-`priority` tips appear more often while
|
|
122699
|
-
* staying evenly spread, so a tip generally does not land next to its own
|
|
122700
|
-
* duplicate. Deterministic and computed once at module load. Exported for
|
|
122701
|
-
* unit testing.
|
|
122702
|
-
*/
|
|
122703
|
-
function buildWeightedTips(tips) {
|
|
122704
|
-
const items = tips.map((t) => ({
|
|
122705
|
-
tip: t,
|
|
122706
|
-
weight: Math.max(1, Math.trunc(t.priority ?? 1)),
|
|
122707
|
-
current: 0
|
|
122708
|
-
}));
|
|
122709
|
-
const total = items.reduce((sum, it) => sum + it.weight, 0);
|
|
122710
|
-
const seq = [];
|
|
122711
|
-
for (let n = 0; n < total; n++) {
|
|
122712
|
-
let best = items[0];
|
|
122713
|
-
for (const it of items) {
|
|
122714
|
-
it.current += it.weight;
|
|
122715
|
-
if (it.current > best.current) best = it;
|
|
122716
|
-
}
|
|
122717
|
-
best.current -= total;
|
|
122718
|
-
seq.push(best.tip);
|
|
122719
|
-
}
|
|
122720
|
-
return seq;
|
|
122721
|
-
}
|
|
122722
|
-
const ROTATION = buildWeightedTips(TOOLBAR_TIPS);
|
|
122723
|
-
function currentTipIndex() {
|
|
122724
|
-
return Math.floor(Date.now() / TIP_ROTATE_INTERVAL_MS);
|
|
122725
|
-
}
|
|
122726
|
-
/**
|
|
122727
|
-
* Pick the tip(s) for a rotation index over the weighted ROTATION sequence.
|
|
122728
|
-
* `primary` is always shown when it fits; `pair` (primary + next tip joined
|
|
122729
|
-
* by the separator) is offered for wide terminals. Pairing is skipped when
|
|
122730
|
-
* the current/next tip is `solo` or when the neighbour is a duplicate of the
|
|
122731
|
-
* current tip (which can happen at the wrap boundary), keeping long/important
|
|
122732
|
-
* tips on their own and avoiding "X | X".
|
|
122733
|
-
*/
|
|
122734
|
-
function tipsForIndex(index) {
|
|
122735
|
-
const n = ROTATION.length;
|
|
122736
|
-
if (n === 0) return {
|
|
122737
|
-
primary: "",
|
|
122738
|
-
pair: null
|
|
122739
|
-
};
|
|
122740
|
-
const offset = (index % n + n) % n;
|
|
122741
|
-
const current = ROTATION[offset];
|
|
122742
|
-
if (n === 1 || current.solo) return {
|
|
122743
|
-
primary: current.text,
|
|
122744
|
-
pair: null
|
|
122745
|
-
};
|
|
122746
|
-
const next = ROTATION[(offset + 1) % n];
|
|
122747
|
-
if (next.solo || next.text === current.text) return {
|
|
122748
|
-
primary: current.text,
|
|
122749
|
-
pair: null
|
|
122750
|
-
};
|
|
122751
|
-
return {
|
|
122752
|
-
primary: current.text,
|
|
122753
|
-
pair: current.text + " | " + next.text
|
|
122754
|
-
};
|
|
122755
|
-
}
|
|
122756
|
-
function shortenModel(model) {
|
|
122757
|
-
if (!model) return model;
|
|
122758
|
-
const slash = model.lastIndexOf("/");
|
|
122759
|
-
return slash >= 0 ? model.slice(slash + 1) : model;
|
|
122760
|
-
}
|
|
122761
|
-
function modelDisplayName(state) {
|
|
122762
|
-
const model = state.availableModels[state.model];
|
|
122763
|
-
return model?.displayName ?? model?.model ?? state.model;
|
|
122764
|
-
}
|
|
122765
|
-
function shortenCwd(path) {
|
|
122766
|
-
if (!path) return path;
|
|
122767
|
-
const home = process.env["HOME"] ?? "";
|
|
122768
|
-
let work = path;
|
|
122769
|
-
if (home && path === home) return "~";
|
|
122770
|
-
if (home && path.startsWith(home + "/")) work = "~" + path.slice(home.length);
|
|
122771
|
-
const segments = work.split("/").filter((s) => s.length > 0);
|
|
122772
|
-
if (segments.length <= MAX_CWD_SEGMENTS) return work;
|
|
122773
|
-
return `…/${segments.slice(-3).join("/")}`;
|
|
122774
|
-
}
|
|
122775
|
-
function formatTokenCount(n) {
|
|
122776
|
-
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
122777
|
-
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
122778
|
-
return String(n);
|
|
122779
|
-
}
|
|
122780
|
-
function safeUsage(usage) {
|
|
122781
|
-
return safeUsageRatio(usage);
|
|
122782
|
-
}
|
|
122783
|
-
function formatContextStatus(usage, tokens, maxTokens) {
|
|
122784
|
-
const pct = `${(safeUsage(usage) * 100).toFixed(1)}%`;
|
|
122785
|
-
if (maxTokens && maxTokens > 0 && tokens !== void 0) return `上下文:${pct} (${formatTokenCount(tokens)}/${formatTokenCount(maxTokens)})`;
|
|
122786
|
-
return `上下文:${pct}`;
|
|
122787
|
-
}
|
|
122788
|
-
const BRAND_COLORS = [
|
|
122789
|
-
"#72A4E9",
|
|
122790
|
-
"#A78BFA",
|
|
122791
|
-
"#34D399"
|
|
122792
|
-
];
|
|
122793
|
-
const GRADIENT_CYCLE_MS = 4e3;
|
|
122794
|
-
const SPINNER_FRAMES$1 = [
|
|
122795
|
-
"●",
|
|
122796
|
-
"◉",
|
|
122797
|
-
"◎",
|
|
122798
|
-
"◌",
|
|
122799
|
-
"○",
|
|
122800
|
-
"◌",
|
|
122801
|
-
"◎",
|
|
122802
|
-
"◉"
|
|
122803
|
-
];
|
|
122804
|
-
const SPINNER_TICK_MS = 120;
|
|
122805
|
-
function hexToRgb$1(hex) {
|
|
122806
|
-
const v = parseInt(hex.slice(1), 16);
|
|
122807
|
-
return [
|
|
122808
|
-
v >> 16 & 255,
|
|
122809
|
-
v >> 8 & 255,
|
|
122810
|
-
v & 255
|
|
122811
|
-
];
|
|
122812
|
-
}
|
|
122813
|
-
function lerpGradient(t) {
|
|
122814
|
-
const count = BRAND_COLORS.length;
|
|
122815
|
-
const segment = Math.min(t * count, count - 1);
|
|
122816
|
-
const idx = Math.floor(segment);
|
|
122817
|
-
const localT = segment - idx;
|
|
122818
|
-
const nextIdx = (idx + 1) % count;
|
|
122819
|
-
const [r0, g0, b0] = hexToRgb$1(BRAND_COLORS[idx]);
|
|
122820
|
-
const [r1, g1, b1] = hexToRgb$1(BRAND_COLORS[nextIdx]);
|
|
122821
|
-
const r = Math.round(r0 + (r1 - r0) * localT);
|
|
122822
|
-
const g = Math.round(g0 + (g1 - g0) * localT);
|
|
122823
|
-
const b = Math.round(b0 + (b1 - b0) * localT);
|
|
122824
|
-
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
122825
|
-
}
|
|
122826
|
-
function buildStatusLine(streamingPhase, livePaneMode, streamingStartTime) {
|
|
122827
|
-
if (streamingPhase === "idle" && livePaneMode !== "tool") return "○ 空闲";
|
|
122828
|
-
let label;
|
|
122829
|
-
if (livePaneMode === "tool") label = "执行中";
|
|
122830
|
-
else if (streamingPhase === "waiting") label = "等待响应";
|
|
122831
|
-
else if (streamingPhase === "thinking") label = "思考中";
|
|
122832
|
-
else if (streamingPhase === "composing") label = "输出中";
|
|
122833
|
-
else label = "";
|
|
122834
|
-
const elapsed = Date.now() - streamingStartTime;
|
|
122835
|
-
const totalSeconds = Math.floor(elapsed / 1e3);
|
|
122836
|
-
const elapsedStr = totalSeconds < 60 ? `${totalSeconds}s` : `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
|
|
122837
|
-
const now = Date.now();
|
|
122838
|
-
const frame = SPINNER_FRAMES$1[Math.floor(now / SPINNER_TICK_MS) % SPINNER_FRAMES$1.length];
|
|
122839
|
-
const gradientColor = lerpGradient(now % GRADIENT_CYCLE_MS / GRADIENT_CYCLE_MS);
|
|
122840
|
-
return chalk.hex(gradientColor).bold(frame) + " " + label + " " + elapsedStr;
|
|
122841
|
-
}
|
|
122842
|
-
function formatFooterGitBadge(status, colors) {
|
|
122843
|
-
const base = chalk.hex(colors.status)(formatGitBadgeBase(status));
|
|
122844
|
-
if (status.pullRequest === null) return base;
|
|
122845
|
-
return `${base} ${chalk.hex(colors.primary)(formatPullRequestBadge(status.pullRequest, { linkPullRequest: true }))}`;
|
|
122846
|
-
}
|
|
122847
|
-
var FooterComponent = class {
|
|
122848
|
-
state;
|
|
122849
|
-
colors;
|
|
122850
|
-
onGitStatusChange;
|
|
122851
|
-
gitCache;
|
|
122852
|
-
gitCacheWorkDir;
|
|
122853
|
-
transientHint = null;
|
|
122854
|
-
/**
|
|
122855
|
-
* Non-terminal background-task counts split by kind so the footer can
|
|
122856
|
-
* render two distinct badges. `bashTasks` covers `bash-*` BPM tasks
|
|
122857
|
-
* spawned via `Shell run_in_background=true`; `agentTasks` covers
|
|
122858
|
-
* `agent-*` BPM tasks (background subagents). Either zero hides its
|
|
122859
|
-
* respective badge.
|
|
122860
|
-
*/
|
|
122861
|
-
backgroundBashTaskCount = 0;
|
|
122862
|
-
backgroundAgentCount = 0;
|
|
122863
|
-
constructor(state, colors, onGitStatusChange = () => {}) {
|
|
122864
|
-
this.state = state;
|
|
122865
|
-
this.colors = colors;
|
|
122866
|
-
this.onGitStatusChange = onGitStatusChange;
|
|
122867
|
-
this.gitCacheWorkDir = state.workDir;
|
|
122868
|
-
this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
|
|
122869
|
-
}
|
|
122870
|
-
setState(state) {
|
|
122871
|
-
if (state.workDir !== this.gitCacheWorkDir) {
|
|
122872
|
-
this.gitCacheWorkDir = state.workDir;
|
|
122873
|
-
this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
|
|
122874
|
-
}
|
|
122875
|
-
this.state = state;
|
|
122876
|
-
}
|
|
122877
|
-
setColors(colors) {
|
|
122878
|
-
this.colors = colors;
|
|
122879
|
-
}
|
|
122880
|
-
/**
|
|
122881
|
-
* Short-lived hint that replaces the rotating toolbar tips on line 1.
|
|
122882
|
-
* Used by the exit-confirmation double-tap flow to show "Press Ctrl+C
|
|
122883
|
-
* again to exit" without requiring a toast/overlay subsystem.
|
|
122884
|
-
* Pass `null` to clear.
|
|
122885
|
-
*/
|
|
122886
|
-
setTransientHint(hint) {
|
|
122887
|
-
this.transientHint = hint;
|
|
122888
|
-
}
|
|
122889
|
-
/**
|
|
122890
|
-
* Sync both background-task badges with live counts. Each non-zero
|
|
122891
|
-
* count produces its own bracketed badge on line 1; zeros hide them
|
|
122892
|
-
* independently.
|
|
122893
|
-
*/
|
|
122894
|
-
setBackgroundCounts(counts) {
|
|
122895
|
-
this.backgroundBashTaskCount = Math.max(0, counts.bashTasks);
|
|
122896
|
-
this.backgroundAgentCount = Math.max(0, counts.agentTasks);
|
|
122897
|
-
}
|
|
122898
|
-
invalidate() {}
|
|
122899
|
-
render(width) {
|
|
122900
|
-
const colors = this.colors;
|
|
122901
|
-
const state = this.state;
|
|
122902
|
-
const left = [];
|
|
122903
|
-
if (state.permissionMode === "auto") left.push(chalk.hex(colors.warning).bold("auto"));
|
|
122904
|
-
if (state.permissionMode === "yolo") left.push(chalk.hex(colors.warning).bold("YES"));
|
|
122905
|
-
if (state.planMode) left.push(chalk.hex(colors.primary).bold("plan"));
|
|
122906
|
-
if (state.wolfpackMode) left.push(chalk.hex(colors.primary).bold("wolfpack"));
|
|
122907
|
-
if (state.goalActive) left.push(chalk.hex(colors.primary).bold("goal"));
|
|
122908
|
-
const model = shortenModel(modelDisplayName(state));
|
|
122909
|
-
if (model) {
|
|
122910
|
-
const thinkingLabel = state.thinking ? " 思考中" : "";
|
|
122911
|
-
left.push(chalk.hex(colors.text)(`${model}${thinkingLabel}`));
|
|
122912
|
-
}
|
|
122913
|
-
if (this.backgroundBashTaskCount > 0) {
|
|
122914
|
-
const noun = this.backgroundBashTaskCount === 1 ? "个任务" : "个任务";
|
|
122915
|
-
left.push(chalk.hex(colors.primary)(`[${String(this.backgroundBashTaskCount)}${noun} 运行中]`));
|
|
122916
|
-
}
|
|
122917
|
-
if (this.backgroundAgentCount > 0) {
|
|
122918
|
-
const noun = this.backgroundAgentCount === 1 ? "个代理" : "个代理";
|
|
122919
|
-
left.push(chalk.hex(colors.primary)(`[${String(this.backgroundAgentCount)}${noun} 运行中]`));
|
|
122920
|
-
}
|
|
122921
|
-
const cwd = shortenCwd(state.workDir);
|
|
122922
|
-
if (cwd) left.push(chalk.hex(colors.status)(cwd));
|
|
122923
|
-
const git = this.gitCache.getStatus();
|
|
122924
|
-
if (git !== null) left.push(formatFooterGitBadge(git, colors));
|
|
122925
|
-
const leftLine = left.join(" ");
|
|
122926
|
-
const leftWidth = visibleWidth(leftLine);
|
|
122927
|
-
let rightText;
|
|
122928
|
-
if (this.transientHint) rightText = chalk.hex(colors.warning).bold(this.transientHint);
|
|
122929
|
-
else {
|
|
122930
|
-
const statusLine = buildStatusLine(state.streamingPhase, state.livePaneMode, state.streamingStartTime);
|
|
122931
|
-
const ccDot = state.ccConnectActive ? chalk.hex(colors.success)("●") : chalk.hex(colors.textDim)("●");
|
|
122932
|
-
rightText = chalk.hex(colors.textDim)(ccDot + " " + formatContextStatus(state.contextUsage, state.contextTokens, state.maxContextTokens) + " " + statusLine);
|
|
122933
|
-
}
|
|
122934
|
-
const rightWidth = visibleWidth(rightText);
|
|
122935
|
-
const gap = 3;
|
|
122936
|
-
let line1;
|
|
122937
|
-
if (leftWidth + gap + rightWidth <= width) {
|
|
122938
|
-
const pad = width - leftWidth - rightWidth;
|
|
122939
|
-
line1 = leftLine + " ".repeat(pad) + rightText;
|
|
122940
|
-
} else if (leftWidth <= width) line1 = leftLine;
|
|
122941
|
-
else line1 = truncateToWidth(leftLine, width, "…");
|
|
122942
|
-
return [truncateToWidth(line1, width)];
|
|
122943
|
-
}
|
|
122944
|
-
};
|
|
122945
|
-
//#endregion
|
|
122946
|
-
//#region src/tui/components/chrome/welcome.ts
|
|
122947
|
-
const HUE_STOPS = 24;
|
|
122948
|
-
const SUB_STEPS = 5;
|
|
122949
|
-
const BREATHE_STEPS = HUE_STOPS * SUB_STEPS;
|
|
122950
|
-
const BREATHE_INTERVAL_MS = 40;
|
|
122951
|
-
function hexToRgb(hex) {
|
|
122952
|
-
return [
|
|
122953
|
-
parseInt(hex.slice(1, 3), 16),
|
|
122954
|
-
parseInt(hex.slice(3, 5), 16),
|
|
122955
|
-
parseInt(hex.slice(5, 7), 16)
|
|
122956
|
-
];
|
|
122957
|
-
}
|
|
122958
|
-
function rgbToHsl(r, g, b) {
|
|
122959
|
-
const rf = r / 255, gf = g / 255, bf = b / 255;
|
|
122960
|
-
const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf);
|
|
122961
|
-
const l = (max + min) / 2;
|
|
122962
|
-
if (max === min) return [
|
|
122963
|
-
0,
|
|
122964
|
-
0,
|
|
122965
|
-
l * 100
|
|
122966
|
-
];
|
|
122967
|
-
const d = max - min;
|
|
122968
|
-
const s = l > .5 ? d / (2 - max - min) : d / (max + min);
|
|
122969
|
-
let h = 0;
|
|
122970
|
-
if (max === rf) h = ((gf - bf) / d + (gf < bf ? 6 : 0)) / 6;
|
|
122971
|
-
else if (max === gf) h = ((bf - rf) / d + 2) / 6;
|
|
122972
|
-
else h = ((rf - gf) / d + 4) / 6;
|
|
122973
|
-
return [
|
|
122974
|
-
h * 360,
|
|
122975
|
-
s * 100,
|
|
122976
|
-
l * 100
|
|
122977
|
-
];
|
|
122978
|
-
}
|
|
122979
|
-
function hslToRgb(h, s, l) {
|
|
122980
|
-
const hf = (h % 360 + 360) % 360 / 360;
|
|
122981
|
-
const sf = s / 100, lf = l / 100;
|
|
122982
|
-
if (sf === 0) {
|
|
122983
|
-
const v = Math.round(lf * 255);
|
|
122984
|
-
return [
|
|
122985
|
-
v,
|
|
122986
|
-
v,
|
|
122987
|
-
v
|
|
122988
|
-
];
|
|
122989
|
-
}
|
|
122990
|
-
const q = lf < .5 ? lf * (1 + sf) : lf + sf - lf * sf;
|
|
122991
|
-
const p = 2 * lf - q;
|
|
122992
|
-
const hue = (t) => {
|
|
122993
|
-
if (t < 0) t += 1;
|
|
122994
|
-
if (t > 1) t -= 1;
|
|
122995
|
-
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
122996
|
-
if (t < 1 / 2) return q;
|
|
122997
|
-
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
122998
|
-
return p;
|
|
122999
|
-
};
|
|
123000
|
-
return [
|
|
123001
|
-
Math.round(hue(hf + 1 / 3) * 255),
|
|
123002
|
-
Math.round(hue(hf) * 255),
|
|
123003
|
-
Math.round(hue(hf - 1 / 3) * 255)
|
|
123004
|
-
];
|
|
123005
|
-
}
|
|
123006
|
-
function rgbToHex(r, g, b) {
|
|
123007
|
-
const c = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
|
|
123008
|
-
return `#${c(r)}${c(g)}${c(b)}`;
|
|
123009
|
-
}
|
|
123010
|
-
/**
|
|
123011
|
-
* Build a full-hue-wheel palette anchored at the primary-green hue.
|
|
123012
|
-
* At frame 0 (and BREATHE_STEPS) the colour is pure primary; in between
|
|
123013
|
-
* it sweeps through all 24 hue stops with smooth sub-step interpolation.
|
|
123014
|
-
*/
|
|
123015
|
-
function buildBreathingPalette(primaryHex, hueStops, subSteps) {
|
|
123016
|
-
const [r, g, b] = hexToRgb(primaryHex);
|
|
123017
|
-
const [baseHue, sat, lit] = rgbToHsl(r, g, b);
|
|
123018
|
-
const steps = hueStops * subSteps;
|
|
123019
|
-
const palette = [];
|
|
123020
|
-
for (let i = 0; i < steps; i++) {
|
|
123021
|
-
const [rr, gg, bb] = hslToRgb((baseHue + i / steps * 360) % 360, sat, lit);
|
|
123022
|
-
palette.push(rgbToHex(rr, gg, bb));
|
|
123023
|
-
}
|
|
123024
|
-
return palette;
|
|
123025
|
-
}
|
|
123026
|
-
var WelcomeComponent = class {
|
|
123027
|
-
state;
|
|
123028
|
-
colors;
|
|
123029
|
-
ui;
|
|
123030
|
-
breatheFrame = 0;
|
|
123031
|
-
breatheTimer = null;
|
|
123032
|
-
breathePalette;
|
|
123033
|
-
borderTitle = null;
|
|
123034
|
-
constructor(state, colors, ui) {
|
|
123035
|
-
this.state = state;
|
|
123036
|
-
this.colors = colors;
|
|
123037
|
-
this.ui = ui;
|
|
123038
|
-
this.breathePalette = buildBreathingPalette(colors.primary, HUE_STOPS, SUB_STEPS);
|
|
123039
|
-
this.startBreathing();
|
|
123040
|
-
}
|
|
123041
|
-
stopBreathing() {
|
|
123042
|
-
if (this.breatheTimer !== null) {
|
|
123043
|
-
clearInterval(this.breatheTimer);
|
|
123044
|
-
this.breatheTimer = null;
|
|
123045
|
-
}
|
|
123046
|
-
if (this.breatheFrame !== 0) {
|
|
123047
|
-
this.breatheFrame = 0;
|
|
123048
|
-
this.ui.requestRender();
|
|
123049
|
-
}
|
|
122477
|
+
stopBreathing() {
|
|
122478
|
+
if (this.breatheTimer !== null) {
|
|
122479
|
+
clearInterval(this.breatheTimer);
|
|
122480
|
+
this.breatheTimer = null;
|
|
122481
|
+
}
|
|
122482
|
+
if (this.breatheFrame !== 0) {
|
|
122483
|
+
this.breatheFrame = 0;
|
|
122484
|
+
this.ui.requestRender();
|
|
122485
|
+
}
|
|
123050
122486
|
}
|
|
123051
122487
|
startBreathing() {
|
|
123052
122488
|
this.breatheTimer = setInterval(() => {
|
|
@@ -123058,53 +122494,52 @@ var WelcomeComponent = class {
|
|
|
123058
122494
|
render(width) {
|
|
123059
122495
|
const breatheColor = this.breathePalette[this.breatheFrame] ?? this.colors.primary;
|
|
123060
122496
|
const logoColor = (s) => chalk.hex(breatheColor)(s);
|
|
123061
|
-
const primary = (s) => chalk.hex(this.colors.primary)(s);
|
|
123062
|
-
const innerWidth = Math.max(10, width - 4);
|
|
123063
|
-
const pad = " ";
|
|
123064
|
-
const logo = ["░▒▓██▄▄▄██", "░▒▓▐█▄▀▄█▌"];
|
|
123065
|
-
const logoWidth = Math.max(...logo.map((row) => visibleWidth(row)));
|
|
123066
|
-
const gap = " ";
|
|
123067
|
-
const textWidth = Math.max(4, innerWidth - logoWidth - 2);
|
|
123068
|
-
const rightRow0 = truncateToWidth(chalk.bold.hex(this.colors.primary)("欢迎使用Scream 您的中文Ai助手"), textWidth, "…");
|
|
123069
|
-
const isLoggedOut = !this.state.model;
|
|
123070
122497
|
const dim = chalk.hex(this.colors.textDim);
|
|
123071
122498
|
const labelStyle = chalk.bold.hex(this.colors.textDim);
|
|
123072
|
-
const
|
|
123073
|
-
const
|
|
122499
|
+
const innerWidth = Math.max(10, width - 4);
|
|
122500
|
+
const isLoggedOut = !this.state.model;
|
|
122501
|
+
const frame = LOGO_FRAMES[this.breatheTimer !== null ? Math.floor(this.breatheFrame / 24) % LOGO_FRAMES.length : 0];
|
|
122502
|
+
const logo = [logoColor(frame[0]), logoColor(frame[1])];
|
|
123074
122503
|
const activeModel = this.state.availableModels[this.state.model];
|
|
123075
122504
|
const modelValue = isLoggedOut ? chalk.hex(this.colors.warning)("未设置,运行 /config") : activeModel?.displayName ?? activeModel?.model ?? this.state.model;
|
|
123076
122505
|
let versionValue;
|
|
123077
122506
|
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 + ")");
|
|
123078
122507
|
else versionValue = this.state.version;
|
|
123079
|
-
const
|
|
123080
|
-
labelStyle("目录: ") + this.state.workDir,
|
|
123081
|
-
labelStyle("模型: ") + modelValue,
|
|
123082
|
-
labelStyle("版本: ") + versionValue
|
|
123083
|
-
];
|
|
123084
|
-
const { primary: tipPrimary, pair: tipPair } = tipsForIndex(currentTipIndex());
|
|
123085
|
-
const tip = tipPair && visibleWidth(tipPair) <= innerWidth ? tipPair : tipPrimary;
|
|
123086
|
-
const tipLine = chalk.hex(this.colors.textMuted)("Tips: " + tip);
|
|
122508
|
+
const hintText = isLoggedOut ? "运行 /config 开始配置" : "发送 / 进入快捷菜单,/exit 保存并退出";
|
|
123087
122509
|
const contentLines = [
|
|
123088
|
-
...
|
|
122510
|
+
...logo,
|
|
123089
122511
|
"",
|
|
123090
|
-
|
|
122512
|
+
labelStyle("版本:") + " " + versionValue,
|
|
122513
|
+
labelStyle("模型:") + " " + modelValue,
|
|
122514
|
+
labelStyle("目录:") + " " + this.state.workDir,
|
|
123091
122515
|
"",
|
|
123092
|
-
|
|
122516
|
+
dim(hintText)
|
|
123093
122517
|
];
|
|
123094
|
-
const borderTitle = this.borderTitle;
|
|
122518
|
+
const borderTitle = this.borderTitle ?? "";
|
|
122519
|
+
const contentWidth = width - 2;
|
|
122520
|
+
let topBorder;
|
|
122521
|
+
if (borderTitle) {
|
|
122522
|
+
const centerPos = Math.floor(contentWidth / 2);
|
|
122523
|
+
const titleText = `─ ${borderTitle} ─`;
|
|
122524
|
+
const titleStart = centerPos - Math.floor(visibleWidth(titleText) / 2);
|
|
122525
|
+
const leftDash = Math.max(0, titleStart);
|
|
122526
|
+
const rightDash = Math.max(0, contentWidth - leftDash - visibleWidth(titleText));
|
|
122527
|
+
topBorder = logoColor("╭" + "─".repeat(leftDash) + titleText + "─".repeat(rightDash) + "╮");
|
|
122528
|
+
} else topBorder = logoColor("╭" + "─".repeat(contentWidth) + "╮");
|
|
123095
122529
|
const lines = [
|
|
123096
122530
|
"",
|
|
123097
|
-
|
|
123098
|
-
|
|
122531
|
+
topBorder,
|
|
122532
|
+
logoColor("│") + " ".repeat(width - 2) + logoColor("│")
|
|
123099
122533
|
];
|
|
123100
122534
|
for (const content of contentLines) {
|
|
123101
122535
|
const truncated = truncateToWidth(content, innerWidth, "…");
|
|
123102
122536
|
const vis = visibleWidth(truncated);
|
|
123103
|
-
const
|
|
123104
|
-
|
|
122537
|
+
const centerPad = Math.floor((width - 1 - vis) / 2);
|
|
122538
|
+
const rightPad = width - 2 - vis - centerPad;
|
|
122539
|
+
lines.push(logoColor("│") + " ".repeat(centerPad) + truncated + " ".repeat(rightPad) + logoColor("│"));
|
|
123105
122540
|
}
|
|
123106
|
-
lines.push(
|
|
123107
|
-
lines.push(
|
|
122541
|
+
lines.push(logoColor("│") + " ".repeat(width - 2) + logoColor("│"));
|
|
122542
|
+
lines.push(logoColor("╰" + "─".repeat(width - 2) + "╯"));
|
|
123108
122543
|
lines.push("");
|
|
123109
122544
|
return lines;
|
|
123110
122545
|
}
|
|
@@ -123293,6 +122728,9 @@ var AssistantMessageComponent = class {
|
|
|
123293
122728
|
bulletColor;
|
|
123294
122729
|
lastText = "";
|
|
123295
122730
|
showBullet;
|
|
122731
|
+
cachedWidth;
|
|
122732
|
+
cachedLines;
|
|
122733
|
+
markdownChild;
|
|
123296
122734
|
constructor(markdownTheme, colors, showBullet = true) {
|
|
123297
122735
|
this.markdownTheme = markdownTheme;
|
|
123298
122736
|
this.bulletColor = colors.roleAssistant;
|
|
@@ -123300,19 +122738,40 @@ var AssistantMessageComponent = class {
|
|
|
123300
122738
|
this.contentContainer = new Container();
|
|
123301
122739
|
}
|
|
123302
122740
|
setShowBullet(show) {
|
|
122741
|
+
if (this.showBullet === show) return;
|
|
123303
122742
|
this.showBullet = show;
|
|
122743
|
+
this.cachedWidth = void 0;
|
|
122744
|
+
this.cachedLines = void 0;
|
|
123304
122745
|
}
|
|
123305
122746
|
updateContent(text) {
|
|
123306
|
-
const
|
|
123307
|
-
|
|
123308
|
-
|
|
122747
|
+
const trimmedText = text.trim();
|
|
122748
|
+
const previousTrimmed = this.lastText.trim();
|
|
122749
|
+
if (trimmedText === previousTrimmed) {
|
|
122750
|
+
this.lastText = text;
|
|
122751
|
+
return;
|
|
122752
|
+
}
|
|
122753
|
+
this.lastText = text;
|
|
122754
|
+
this.cachedWidth = void 0;
|
|
122755
|
+
this.cachedLines = void 0;
|
|
122756
|
+
const markdownChild = this.markdownChild;
|
|
122757
|
+
if (markdownChild !== void 0 && trimmedText.startsWith(previousTrimmed) && trimmedText.length > previousTrimmed.length) {
|
|
122758
|
+
markdownChild.setText(trimmedText);
|
|
122759
|
+
return;
|
|
122760
|
+
}
|
|
123309
122761
|
this.contentContainer.clear();
|
|
123310
|
-
|
|
122762
|
+
this.markdownChild = void 0;
|
|
122763
|
+
if (trimmedText.length > 0) {
|
|
122764
|
+
this.markdownChild = new Markdown(trimmedText, 0, 0, this.markdownTheme);
|
|
122765
|
+
this.contentContainer.addChild(this.markdownChild);
|
|
122766
|
+
}
|
|
123311
122767
|
}
|
|
123312
122768
|
invalidate() {
|
|
122769
|
+
this.cachedWidth = void 0;
|
|
122770
|
+
this.cachedLines = void 0;
|
|
123313
122771
|
this.contentContainer.invalidate?.();
|
|
123314
122772
|
}
|
|
123315
122773
|
render(width) {
|
|
122774
|
+
if (this.cachedLines !== void 0 && this.cachedWidth === width) return this.cachedLines;
|
|
123316
122775
|
if (this.lastText.trim().length === 0) return [];
|
|
123317
122776
|
const prefix = this.showBullet ? STATUS_BULLET : " ";
|
|
123318
122777
|
const contentWidth = Math.max(1, width - visibleWidth(prefix));
|
|
@@ -123322,6 +122781,8 @@ var AssistantMessageComponent = class {
|
|
|
123322
122781
|
const p = i === 0 && this.showBullet ? chalk.hex(this.bulletColor)(STATUS_BULLET) : " ";
|
|
123323
122782
|
lines.push(p + contentLines[i]);
|
|
123324
122783
|
}
|
|
122784
|
+
this.cachedWidth = width;
|
|
122785
|
+
this.cachedLines = lines;
|
|
123325
122786
|
return lines;
|
|
123326
122787
|
}
|
|
123327
122788
|
};
|
|
@@ -123330,17 +122791,29 @@ var AssistantMessageComponent = class {
|
|
|
123330
122791
|
var BackgroundAgentStatusComponent = class {
|
|
123331
122792
|
data;
|
|
123332
122793
|
colors;
|
|
122794
|
+
bullet;
|
|
122795
|
+
textComponent;
|
|
122796
|
+
cachedWidth;
|
|
122797
|
+
cachedLines;
|
|
123333
122798
|
constructor(data, colors) {
|
|
123334
122799
|
this.data = data;
|
|
123335
122800
|
this.colors = colors;
|
|
122801
|
+
const tone = data.phase === "started" ? colors.primary : data.phase === "completed" ? colors.success : colors.error;
|
|
122802
|
+
this.bullet = data.phase === "failed" ? chalk.hex(tone)(FAILURE_MARK) : chalk.hex(tone)(STATUS_BULLET);
|
|
122803
|
+
const text = chalk.hex(tone)(data.headline) + (data.detail !== void 0 && data.detail.length > 0 ? chalk.hex(colors.textDim)(` (${data.detail})`) : "");
|
|
122804
|
+
this.textComponent = new Text(text, 0, 0);
|
|
122805
|
+
}
|
|
122806
|
+
invalidate() {
|
|
122807
|
+
this.cachedWidth = void 0;
|
|
122808
|
+
this.cachedLines = void 0;
|
|
123336
122809
|
}
|
|
123337
|
-
invalidate() {}
|
|
123338
122810
|
render(width) {
|
|
123339
|
-
|
|
123340
|
-
const bullet = this.data.phase === "failed" ? chalk.hex(tone)(FAILURE_MARK) : chalk.hex(tone)(STATUS_BULLET);
|
|
123341
|
-
const textComponent = new Text(chalk.hex(tone)(this.data.headline) + (this.data.detail !== void 0 && this.data.detail.length > 0 ? chalk.hex(this.colors.textDim)(` (${this.data.detail})`) : ""), 0, 0);
|
|
122811
|
+
if (this.cachedLines !== void 0 && this.cachedWidth === width) return this.cachedLines;
|
|
123342
122812
|
const contentWidth = Math.max(1, width - 2);
|
|
123343
|
-
|
|
122813
|
+
const lines = ["", ...this.textComponent.render(contentWidth).map((line, index) => (index === 0 ? this.bullet : " ") + line)];
|
|
122814
|
+
this.cachedWidth = width;
|
|
122815
|
+
this.cachedLines = lines;
|
|
122816
|
+
return lines;
|
|
123344
122817
|
}
|
|
123345
122818
|
};
|
|
123346
122819
|
//#endregion
|
|
@@ -123507,6 +122980,8 @@ var ThinkingComponent = class {
|
|
|
123507
122980
|
spinnerFrame = 0;
|
|
123508
122981
|
spinnerInterval;
|
|
123509
122982
|
textComponent;
|
|
122983
|
+
cachedWidth;
|
|
122984
|
+
cachedLines;
|
|
123510
122985
|
constructor(text, colors, showMarker = true, mode = "finalized", ui) {
|
|
123511
122986
|
this.text = text;
|
|
123512
122987
|
this.color = colors.roleThinking;
|
|
@@ -123516,17 +122991,25 @@ var ThinkingComponent = class {
|
|
|
123516
122991
|
this.textComponent = new Text(this.styled(text), 0, 0);
|
|
123517
122992
|
if (mode === "live") this.startSpinner();
|
|
123518
122993
|
}
|
|
123519
|
-
invalidate() {
|
|
122994
|
+
invalidate() {
|
|
122995
|
+
this.cachedWidth = void 0;
|
|
122996
|
+
this.cachedLines = void 0;
|
|
122997
|
+
}
|
|
123520
122998
|
setText(text) {
|
|
123521
122999
|
if (this.text === text) return;
|
|
123522
123000
|
this.text = text;
|
|
123001
|
+
this.cachedWidth = void 0;
|
|
123002
|
+
this.cachedLines = void 0;
|
|
123523
123003
|
this.textComponent.setText(this.styled(text));
|
|
123524
123004
|
}
|
|
123525
123005
|
styled(text) {
|
|
123526
123006
|
return chalk.hex(this.color).italic(text);
|
|
123527
123007
|
}
|
|
123528
123008
|
finalize() {
|
|
123009
|
+
if (this.mode === "finalized") return;
|
|
123529
123010
|
this.mode = "finalized";
|
|
123011
|
+
this.cachedWidth = void 0;
|
|
123012
|
+
this.cachedLines = void 0;
|
|
123530
123013
|
this.stopSpinner();
|
|
123531
123014
|
}
|
|
123532
123015
|
dispose() {
|
|
@@ -123535,8 +123018,11 @@ var ThinkingComponent = class {
|
|
|
123535
123018
|
setExpanded(expanded) {
|
|
123536
123019
|
if (this.expanded === expanded) return;
|
|
123537
123020
|
this.expanded = expanded;
|
|
123021
|
+
this.cachedWidth = void 0;
|
|
123022
|
+
this.cachedLines = void 0;
|
|
123538
123023
|
}
|
|
123539
123024
|
render(width) {
|
|
123025
|
+
if (this.mode === "finalized" && this.cachedLines !== void 0 && this.cachedWidth === width) return this.cachedLines;
|
|
123540
123026
|
const contentWidth = Math.max(1, width - 2);
|
|
123541
123027
|
const contentLines = this.text.length > 0 ? this.textComponent.render(contentWidth) : [""];
|
|
123542
123028
|
if (this.mode === "live") {
|
|
@@ -123552,10 +123038,16 @@ var ThinkingComponent = class {
|
|
|
123552
123038
|
const p = i === 0 && this.showMarker ? chalk.hex(this.color)(STATUS_BULLET) : " ";
|
|
123553
123039
|
rendered.push(p + contentLines[i]);
|
|
123554
123040
|
}
|
|
123555
|
-
if (this.expanded || contentLines.length <= 2)
|
|
123041
|
+
if (this.expanded || contentLines.length <= 2) {
|
|
123042
|
+
this.cachedWidth = width;
|
|
123043
|
+
this.cachedLines = rendered;
|
|
123044
|
+
return rendered;
|
|
123045
|
+
}
|
|
123556
123046
|
const truncated = rendered.slice(0, 3);
|
|
123557
123047
|
const remaining = contentLines.length - 2;
|
|
123558
123048
|
truncated.push(" " + chalk.dim(`... (${String(remaining)} more lines, ctrl+o to expand)`));
|
|
123049
|
+
this.cachedWidth = width;
|
|
123050
|
+
this.cachedLines = truncated;
|
|
123559
123051
|
return truncated;
|
|
123560
123052
|
}
|
|
123561
123053
|
startSpinner() {
|
|
@@ -123944,6 +123436,26 @@ function trimTrailingEmptyLines(lines) {
|
|
|
123944
123436
|
return lines.slice(0, end);
|
|
123945
123437
|
}
|
|
123946
123438
|
/**
|
|
123439
|
+
* Returns the tail of `text` whose UTF-8 byte length is at most `maxBytes`.
|
|
123440
|
+
* Iterates by Unicode code points so multi-byte characters and surrogate
|
|
123441
|
+
* pairs are never split.
|
|
123442
|
+
*/
|
|
123443
|
+
function truncateTailBytes(text, maxBytes) {
|
|
123444
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
123445
|
+
const chars = Array.from(text);
|
|
123446
|
+
let start = chars.length;
|
|
123447
|
+
let bytes = 0;
|
|
123448
|
+
for (let i = chars.length - 1; i >= 0; i--) {
|
|
123449
|
+
const char = chars[i];
|
|
123450
|
+
if (char === void 0) continue;
|
|
123451
|
+
const charBytes = Buffer.byteLength(char, "utf8");
|
|
123452
|
+
if (bytes + charBytes > maxBytes) break;
|
|
123453
|
+
bytes += charBytes;
|
|
123454
|
+
start = i;
|
|
123455
|
+
}
|
|
123456
|
+
return chars.slice(start).join("");
|
|
123457
|
+
}
|
|
123458
|
+
/**
|
|
123947
123459
|
* Component that renders tool output with wrap-aware line truncation.
|
|
123948
123460
|
* Uses pi-tui's Text component to compute actual visual wrapped lines,
|
|
123949
123461
|
* then caps at PREVIEW_LINES. This handles long single-line output (e.g.
|
|
@@ -123960,7 +123472,8 @@ var TruncatedOutputComponent = class {
|
|
|
123960
123472
|
this.hintFormatter = options.hintFormatter;
|
|
123961
123473
|
const tint = options.isError ? chalk.hex(options.colors.error) : chalk.dim;
|
|
123962
123474
|
const cleaned = trimTrailingEmptyLines(output.split("\n")).join("\n");
|
|
123963
|
-
|
|
123475
|
+
const truncated = options.maxBytes === void 0 ? cleaned : truncateTailBytes(cleaned, options.maxBytes);
|
|
123476
|
+
this.textComponent = new Text(tint(truncated), 2, 0);
|
|
123964
123477
|
}
|
|
123965
123478
|
invalidate() {
|
|
123966
123479
|
this.textComponent.invalidate();
|
|
@@ -124006,6 +123519,7 @@ var ShellExecutionComponent = class extends Container {
|
|
|
124006
123519
|
isError: result.is_error ?? false,
|
|
124007
123520
|
colors,
|
|
124008
123521
|
maxLines: previewLines,
|
|
123522
|
+
maxBytes: MAX_SHELL_OUTPUT_BYTES,
|
|
124009
123523
|
hintFormatter: (remaining) => `...(还有 ${String(remaining)} 行,按 ctrl+o 展开)`
|
|
124010
123524
|
}));
|
|
124011
123525
|
}
|
|
@@ -124600,6 +124114,7 @@ var ToolCallComponent = class ToolCallComponent extends Container {
|
|
|
124600
124114
|
subagentError;
|
|
124601
124115
|
streamingProgressTimer;
|
|
124602
124116
|
subagentElapsedTimer;
|
|
124117
|
+
disposed = false;
|
|
124603
124118
|
subagentStartedAtMs;
|
|
124604
124119
|
subagentEndedAtMs;
|
|
124605
124120
|
progressLines = [];
|
|
@@ -124679,6 +124194,8 @@ var ToolCallComponent = class ToolCallComponent extends Container {
|
|
|
124679
124194
|
this.ui?.requestRender();
|
|
124680
124195
|
}
|
|
124681
124196
|
dispose() {
|
|
124197
|
+
if (this.disposed) return;
|
|
124198
|
+
this.disposed = true;
|
|
124682
124199
|
this.stopStreamingProgressTimer();
|
|
124683
124200
|
this.stopSubagentElapsedTimer();
|
|
124684
124201
|
}
|
|
@@ -124834,6 +124351,10 @@ var ToolCallComponent = class ToolCallComponent extends Container {
|
|
|
124834
124351
|
}
|
|
124835
124352
|
if (this.ui === void 0 || this.streamingProgressTimer !== void 0) return;
|
|
124836
124353
|
this.streamingProgressTimer = setInterval(() => {
|
|
124354
|
+
if (this.disposed) {
|
|
124355
|
+
this.stopStreamingProgressTimer();
|
|
124356
|
+
return;
|
|
124357
|
+
}
|
|
124837
124358
|
if (!this.isStreamingEditPreview()) {
|
|
124838
124359
|
this.stopStreamingProgressTimer();
|
|
124839
124360
|
return;
|
|
@@ -124855,6 +124376,10 @@ var ToolCallComponent = class ToolCallComponent extends Container {
|
|
|
124855
124376
|
}
|
|
124856
124377
|
if (this.ui === void 0 || this.subagentElapsedTimer !== void 0) return;
|
|
124857
124378
|
this.subagentElapsedTimer = setInterval(() => {
|
|
124379
|
+
if (this.disposed) {
|
|
124380
|
+
this.stopSubagentElapsedTimer();
|
|
124381
|
+
return;
|
|
124382
|
+
}
|
|
124858
124383
|
const latestPhase = this.getDerivedSubagentPhase();
|
|
124859
124384
|
if (latestPhase !== "spawning" && latestPhase !== "running") {
|
|
124860
124385
|
this.stopSubagentElapsedTimer();
|
|
@@ -125544,6 +125069,8 @@ var UserMessageComponent = class {
|
|
|
125544
125069
|
textComponent;
|
|
125545
125070
|
spacerComponent;
|
|
125546
125071
|
imageThumbnails;
|
|
125072
|
+
cachedWidth;
|
|
125073
|
+
cachedLines;
|
|
125547
125074
|
constructor(text, colors, images) {
|
|
125548
125075
|
this.color = colors.roleUser;
|
|
125549
125076
|
this.textComponent = new Text(chalk.hex(colors.roleUser).bold(text), 0, 0);
|
|
@@ -125551,10 +125078,13 @@ var UserMessageComponent = class {
|
|
|
125551
125078
|
this.imageThumbnails = images?.map((img) => new ImageThumbnail(img, colors)) ?? [];
|
|
125552
125079
|
}
|
|
125553
125080
|
invalidate() {
|
|
125081
|
+
this.cachedWidth = void 0;
|
|
125082
|
+
this.cachedLines = void 0;
|
|
125554
125083
|
this.textComponent.invalidate();
|
|
125555
125084
|
for (const img of this.imageThumbnails) img.invalidate?.();
|
|
125556
125085
|
}
|
|
125557
125086
|
render(width) {
|
|
125087
|
+
if (this.cachedLines !== void 0 && this.cachedWidth === width) return this.cachedLines;
|
|
125558
125088
|
const border = chalk.hex(this.color).bold(USER_MESSAGE_BULLET);
|
|
125559
125089
|
const borderWidth = visibleWidth(border);
|
|
125560
125090
|
const contentWidth = Math.max(1, width - borderWidth);
|
|
@@ -125569,6 +125099,8 @@ var UserMessageComponent = class {
|
|
|
125569
125099
|
const imageLines = thumbnail.render(contentWidth);
|
|
125570
125100
|
for (const line of imageLines) lines.push(" ".repeat(borderWidth) + line);
|
|
125571
125101
|
}
|
|
125102
|
+
this.cachedWidth = width;
|
|
125103
|
+
this.cachedLines = lines;
|
|
125572
125104
|
return lines;
|
|
125573
125105
|
}
|
|
125574
125106
|
};
|
|
@@ -127152,7 +126684,7 @@ async function confirmUninstall(host, label) {
|
|
|
127152
126684
|
* restores the editor. The question and answer are never recorded in the
|
|
127153
126685
|
* main conversation history.
|
|
127154
126686
|
*/
|
|
127155
|
-
const SPINNER_FRAMES = [
|
|
126687
|
+
const SPINNER_FRAMES$1 = [
|
|
127156
126688
|
"⠋",
|
|
127157
126689
|
"⠙",
|
|
127158
126690
|
"⠹",
|
|
@@ -127212,7 +126744,7 @@ var BtwOverlayComponent = class extends Container {
|
|
|
127212
126744
|
startSpinner() {
|
|
127213
126745
|
this.spinnerFrame = 0;
|
|
127214
126746
|
this.spinnerInterval = setInterval(() => {
|
|
127215
|
-
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
126747
|
+
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES$1.length;
|
|
127216
126748
|
this.requestRender();
|
|
127217
126749
|
}, 80);
|
|
127218
126750
|
}
|
|
@@ -127232,7 +126764,7 @@ var BtwOverlayComponent = class extends Container {
|
|
|
127232
126764
|
lines.push(chalk.hex(c.primary)("/btw") + chalk.hex(c.textMuted)(" — ") + chalk.hex(c.text)(truncated));
|
|
127233
126765
|
lines.push("");
|
|
127234
126766
|
if (this.status === "loading") {
|
|
127235
|
-
const spinner = SPINNER_FRAMES[this.spinnerFrame];
|
|
126767
|
+
const spinner = SPINNER_FRAMES$1[this.spinnerFrame];
|
|
127236
126768
|
lines.push(chalk.hex(c.textMuted)(`${spinner} Answering…`));
|
|
127237
126769
|
} else if (this.status === "done" && this.markdown !== void 0) {
|
|
127238
126770
|
const contentWidth = Math.max(20, width - 2);
|
|
@@ -130768,6 +130300,7 @@ var StreamingUIController = class {
|
|
|
130768
130300
|
const tc = this._pendingToolComponents.get(toolCallId);
|
|
130769
130301
|
if (tc) {
|
|
130770
130302
|
tc.setResult(result);
|
|
130303
|
+
tc.dispose();
|
|
130771
130304
|
this._pendingToolComponents.delete(toolCallId);
|
|
130772
130305
|
state.ui.requestRender();
|
|
130773
130306
|
return;
|
|
@@ -131267,1119 +130800,1687 @@ var TasksBrowserApp = class extends Container {
|
|
|
131267
130800
|
}
|
|
131268
130801
|
this.invalidate();
|
|
131269
130802
|
}
|
|
131270
|
-
syncSelectionFromProps() {
|
|
131271
|
-
if (this.sortedVisible.length === 0) {
|
|
131272
|
-
this.selectedIndex = 0;
|
|
131273
|
-
this.listScroll = 0;
|
|
131274
|
-
return;
|
|
131275
|
-
}
|
|
131276
|
-
if (this.props.selectedTaskId !== void 0) {
|
|
131277
|
-
const idx = this.sortedVisible.findIndex((t) => t.taskId === this.props.selectedTaskId);
|
|
131278
|
-
if (idx !== -1) {
|
|
131279
|
-
this.selectedIndex = idx;
|
|
131280
|
-
return;
|
|
131281
|
-
}
|
|
130803
|
+
syncSelectionFromProps() {
|
|
130804
|
+
if (this.sortedVisible.length === 0) {
|
|
130805
|
+
this.selectedIndex = 0;
|
|
130806
|
+
this.listScroll = 0;
|
|
130807
|
+
return;
|
|
130808
|
+
}
|
|
130809
|
+
if (this.props.selectedTaskId !== void 0) {
|
|
130810
|
+
const idx = this.sortedVisible.findIndex((t) => t.taskId === this.props.selectedTaskId);
|
|
130811
|
+
if (idx !== -1) {
|
|
130812
|
+
this.selectedIndex = idx;
|
|
130813
|
+
return;
|
|
130814
|
+
}
|
|
130815
|
+
}
|
|
130816
|
+
if (this.selectedIndex >= this.sortedVisible.length) this.selectedIndex = this.sortedVisible.length - 1;
|
|
130817
|
+
}
|
|
130818
|
+
clearPendingStop() {
|
|
130819
|
+
this.pendingStopTaskId = void 0;
|
|
130820
|
+
if (this.pendingStopTimer !== void 0) {
|
|
130821
|
+
clearTimeout(this.pendingStopTimer);
|
|
130822
|
+
this.pendingStopTimer = void 0;
|
|
130823
|
+
}
|
|
130824
|
+
}
|
|
130825
|
+
emitSelect() {
|
|
130826
|
+
const task = this.sortedVisible[this.selectedIndex];
|
|
130827
|
+
if (task) this.props.onSelect(task.taskId);
|
|
130828
|
+
}
|
|
130829
|
+
handleInput(data) {
|
|
130830
|
+
const k = printableChar(data);
|
|
130831
|
+
if (this.pendingStopTaskId !== void 0) {
|
|
130832
|
+
if (k === "y" || k === "Y") {
|
|
130833
|
+
const taskId = this.pendingStopTaskId;
|
|
130834
|
+
this.clearPendingStop();
|
|
130835
|
+
this.props.onStopConfirmed(taskId);
|
|
130836
|
+
this.invalidate();
|
|
130837
|
+
return;
|
|
130838
|
+
}
|
|
130839
|
+
this.clearPendingStop();
|
|
130840
|
+
this.invalidate();
|
|
130841
|
+
return;
|
|
130842
|
+
}
|
|
130843
|
+
if (matchesKey(data, Key.escape) || k === "q" || k === "Q") {
|
|
130844
|
+
this.props.onCancel();
|
|
130845
|
+
return;
|
|
130846
|
+
}
|
|
130847
|
+
if (matchesKey(data, Key.up) || k === "k") {
|
|
130848
|
+
if (this.sortedVisible.length === 0) return;
|
|
130849
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
130850
|
+
this.emitSelect();
|
|
130851
|
+
this.invalidate();
|
|
130852
|
+
return;
|
|
130853
|
+
}
|
|
130854
|
+
if (matchesKey(data, Key.down) || k === "j") {
|
|
130855
|
+
if (this.sortedVisible.length === 0) return;
|
|
130856
|
+
this.selectedIndex = Math.min(this.sortedVisible.length - 1, this.selectedIndex + 1);
|
|
130857
|
+
this.emitSelect();
|
|
130858
|
+
this.invalidate();
|
|
130859
|
+
return;
|
|
130860
|
+
}
|
|
130861
|
+
if (matchesKey(data, Key.tab) || k === " ") {
|
|
130862
|
+
this.props.onToggleFilter();
|
|
130863
|
+
return;
|
|
130864
|
+
}
|
|
130865
|
+
if (k === "r" || k === "R") {
|
|
130866
|
+
this.props.onRefresh();
|
|
130867
|
+
return;
|
|
130868
|
+
}
|
|
130869
|
+
if (k === "s" || k === "S") {
|
|
130870
|
+
const task = this.sortedVisible[this.selectedIndex];
|
|
130871
|
+
if (task === void 0) return;
|
|
130872
|
+
if (isTerminal(task.status)) {
|
|
130873
|
+
this.props.onStopIgnored?.(task.taskId, "terminal");
|
|
130874
|
+
return;
|
|
130875
|
+
}
|
|
130876
|
+
this.pendingStopTaskId = task.taskId;
|
|
130877
|
+
this.pendingStopTimer = setTimeout(() => {
|
|
130878
|
+
this.clearPendingStop();
|
|
130879
|
+
this.invalidate();
|
|
130880
|
+
}, STOP_CONFIRM_TIMEOUT_MS);
|
|
130881
|
+
this.invalidate();
|
|
130882
|
+
return;
|
|
130883
|
+
}
|
|
130884
|
+
if (k === "o" || k === "O" || matchesKey(data, Key.enter)) {
|
|
130885
|
+
const task = this.sortedVisible[this.selectedIndex];
|
|
130886
|
+
if (task) this.props.onOpenOutput(task.taskId);
|
|
130887
|
+
return;
|
|
130888
|
+
}
|
|
130889
|
+
}
|
|
130890
|
+
/**
|
|
130891
|
+
* Render the entire screen as `terminal.rows` lines of `width` cols.
|
|
130892
|
+
* Layout: header(1) + body(rows-2) + footer(1).
|
|
130893
|
+
*/
|
|
130894
|
+
render(width) {
|
|
130895
|
+
const rows = Math.max(1, this.terminal.rows);
|
|
130896
|
+
if (width < MIN_WIDTH || rows < MIN_HEIGHT) return this.renderTooSmall(width, rows);
|
|
130897
|
+
const header = this.renderHeader(width);
|
|
130898
|
+
const footer = this.renderFooter(width);
|
|
130899
|
+
const bodyHeight = rows - 2;
|
|
130900
|
+
const listWidth = Math.max(LIST_COL_MIN, Math.min(LIST_COL_MAX, Math.floor(width * LIST_COL_RATIO)));
|
|
130901
|
+
const rightWidth = width - listWidth;
|
|
130902
|
+
const listFrame = this.renderListFrame(listWidth, bodyHeight);
|
|
130903
|
+
const rightFrames = this.renderRightStack(rightWidth, bodyHeight);
|
|
130904
|
+
const lines = [header];
|
|
130905
|
+
for (let i = 0; i < bodyHeight; i++) lines.push((listFrame[i] ?? " ".repeat(listWidth)) + (rightFrames[i] ?? " ".repeat(rightWidth)));
|
|
130906
|
+
lines.push(footer);
|
|
130907
|
+
return lines;
|
|
130908
|
+
}
|
|
130909
|
+
renderHeader(width) {
|
|
130910
|
+
const colors = this.props.colors;
|
|
130911
|
+
const title = chalk.hex(colors.primary).bold(" TASK BROWSER ");
|
|
130912
|
+
const filterText = chalk.hex(colors.textMuted)(` filter=${this.props.filter === "all" ? "ALL" : "ACTIVE"} `);
|
|
130913
|
+
const counts = countByStatus(this.props.tasks);
|
|
130914
|
+
const countSegments = [];
|
|
130915
|
+
if (counts.running > 0) countSegments.push(chalk.hex(colors.success)(` ${String(counts.running)} 运行中 `));
|
|
130916
|
+
if (counts.awaiting > 0) countSegments.push(chalk.hex(colors.warning)(` ${String(counts.awaiting)} 等待中 `));
|
|
130917
|
+
if (counts.completed > 0) countSegments.push(chalk.hex(colors.textDim)(` ${String(counts.completed)} 已完成 `));
|
|
130918
|
+
if (counts.terminalFailed > 0) countSegments.push(chalk.hex(colors.error)(` ${String(counts.terminalFailed)} 已中断 `));
|
|
130919
|
+
const totals = chalk.hex(colors.textMuted)(` ${String(this.props.tasks.length)} 总计 `);
|
|
130920
|
+
return fitExactly$1(title + filterText + countSegments.join("") + totals, width);
|
|
130921
|
+
}
|
|
130922
|
+
renderFooter(width) {
|
|
130923
|
+
const colors = this.props.colors;
|
|
130924
|
+
const key = (text) => chalk.hex(colors.primary).bold(text);
|
|
130925
|
+
const dim = (text) => chalk.hex(colors.textMuted)(text);
|
|
130926
|
+
if (this.pendingStopTaskId !== void 0) {
|
|
130927
|
+
const warn = (text) => chalk.hex(colors.warning).bold(text);
|
|
130928
|
+
return fitExactly$1(` ${warn("停止")} ${chalk.hex(colors.text)(this.pendingStopTaskId)}? ${key("Y")} ${dim("确认")} ${key("N")} ${dim("取消")} `, width);
|
|
130929
|
+
}
|
|
130930
|
+
const left = [
|
|
130931
|
+
` ${key("↑↓")} ${dim("选择")}`,
|
|
130932
|
+
`${key("Enter/O")} ${dim("输出")}`,
|
|
130933
|
+
`${key("S")} ${dim("停止")}`,
|
|
130934
|
+
`${key("R")} ${dim("刷新")}`,
|
|
130935
|
+
`${key("Tab")} ${dim("筛选")}`,
|
|
130936
|
+
`${key("Q/Esc")} ${dim("退出")} `
|
|
130937
|
+
].join(" ");
|
|
130938
|
+
const flash = this.props.flashMessage;
|
|
130939
|
+
if (flash !== void 0 && flash.length > 0) {
|
|
130940
|
+
const flashStyled = chalk.hex(colors.warning)(` ${flash} `);
|
|
130941
|
+
const total = visibleWidth(left) + visibleWidth(flashStyled);
|
|
130942
|
+
if (total <= width) return left + " ".repeat(width - total) + flashStyled;
|
|
130943
|
+
}
|
|
130944
|
+
return fitExactly$1(left, width);
|
|
130945
|
+
}
|
|
130946
|
+
/**
|
|
130947
|
+
* Render a framed box: `┌─ Title ─┐` top, `│ <content> │` sides, `└─┘`
|
|
130948
|
+
* bottom. Result is exactly `width × height` cells. `content` is a
|
|
130949
|
+
* pre-rendered array of inner-width-sized lines; extra rows are padded.
|
|
130950
|
+
*/
|
|
130951
|
+
renderFrame(title, content, width, height) {
|
|
130952
|
+
if (height < 2 || width < 4) {
|
|
130953
|
+
const out = [];
|
|
130954
|
+
for (let i = 0; i < height; i++) out.push(" ".repeat(width));
|
|
130955
|
+
return out;
|
|
130956
|
+
}
|
|
130957
|
+
const stroke = this.props.colors.primary;
|
|
130958
|
+
const innerWidth = width - 2;
|
|
130959
|
+
const innerHeight = height - 2;
|
|
130960
|
+
const titleStyled = chalk.hex(this.props.colors.textStrong).bold(title);
|
|
130961
|
+
const titleWidth = visibleWidth(titleStyled);
|
|
130962
|
+
const titleSegmentWidth = visibleWidth(`─ ${titleStyled} `);
|
|
130963
|
+
const remainingDashes = Math.max(0, innerWidth - titleSegmentWidth);
|
|
130964
|
+
const topMid = titleWidth > 0 && titleSegmentWidth <= innerWidth ? chalk.hex(stroke)("─ ") + titleStyled + " " + chalk.hex(stroke)("─".repeat(remainingDashes)) : chalk.hex(stroke)("─".repeat(innerWidth));
|
|
130965
|
+
const top = chalk.hex(stroke)("┌") + topMid + chalk.hex(stroke)("┐");
|
|
130966
|
+
const bottom = chalk.hex(stroke)("└" + "─".repeat(innerWidth) + "┘");
|
|
130967
|
+
const lines = [top];
|
|
130968
|
+
for (let i = 0; i < innerHeight; i++) {
|
|
130969
|
+
const inner = content[i] ?? "";
|
|
130970
|
+
lines.push(chalk.hex(stroke)("│") + fitExactly$1(inner, innerWidth) + chalk.hex(stroke)("│"));
|
|
130971
|
+
}
|
|
130972
|
+
lines.push(bottom);
|
|
130973
|
+
return lines;
|
|
130974
|
+
}
|
|
130975
|
+
renderListFrame(width, height) {
|
|
130976
|
+
const title = `Tasks [${this.props.filter}]`;
|
|
130977
|
+
const innerHeight = Math.max(0, height - 2);
|
|
130978
|
+
if (this.sortedVisible.length === 0) {
|
|
130979
|
+
const empty = this.props.filter === "active" ? "无活跃任务。Tab = 显示全部。" : "本会话无后台任务。";
|
|
130980
|
+
const lines = [chalk.hex(this.props.colors.textMuted)(empty)];
|
|
130981
|
+
while (lines.length < innerHeight) lines.push("");
|
|
130982
|
+
return this.renderFrame(title, lines, width, height);
|
|
130983
|
+
}
|
|
130984
|
+
this.adjustScroll(innerHeight);
|
|
130985
|
+
const start = this.listScroll;
|
|
130986
|
+
const window = this.sortedVisible.slice(start, start + innerHeight);
|
|
130987
|
+
const innerWidth = width - 2;
|
|
130988
|
+
const lines = [];
|
|
130989
|
+
for (const [vi, task] of window.entries()) {
|
|
130990
|
+
const index = start + vi;
|
|
130991
|
+
lines.push(this.renderListRow(task, index === this.selectedIndex, innerWidth));
|
|
130992
|
+
}
|
|
130993
|
+
while (lines.length < innerHeight) lines.push("");
|
|
130994
|
+
return this.renderFrame(title, lines, width, height);
|
|
130995
|
+
}
|
|
130996
|
+
renderListRow(task, selected, innerWidth) {
|
|
130997
|
+
const colors = this.props.colors;
|
|
130998
|
+
const pointer = selected ? "> " : " ";
|
|
130999
|
+
const pointerStyled = chalk.hex(selected ? colors.primary : colors.textDim)(pointer);
|
|
131000
|
+
const idColor = selected ? colors.primary : task.taskId.startsWith("agent-") ? colors.success : colors.accent;
|
|
131001
|
+
const idText = selected ? chalk.hex(idColor).bold(task.taskId) : chalk.hex(idColor)(task.taskId);
|
|
131002
|
+
const idPad = " ".repeat(Math.max(0, 17 - task.taskId.length));
|
|
131003
|
+
const status = STATUS_LABEL[task.status];
|
|
131004
|
+
const prefix = `${pointerStyled}${idText}${idPad} ${chalk.hex(statusColor(colors, task.status))(status)}`;
|
|
131005
|
+
const prefixWidth = visibleWidth(prefix);
|
|
131006
|
+
const descBudget = Math.max(0, innerWidth - prefixWidth - 1);
|
|
131007
|
+
if (descBudget < 4) return fitExactly$1(prefix, innerWidth);
|
|
131008
|
+
const desc = truncateToWidth(singleLine$2(task.description) || singleLine$2(task.command) || "(no description)", descBudget, ELLIPSIS$3);
|
|
131009
|
+
return fitExactly$1(`${prefix} ${chalk.hex(colors.text)(desc)}`, innerWidth);
|
|
131010
|
+
}
|
|
131011
|
+
adjustScroll(visibleRows) {
|
|
131012
|
+
if (visibleRows <= 0) {
|
|
131013
|
+
this.listScroll = 0;
|
|
131014
|
+
return;
|
|
131015
|
+
}
|
|
131016
|
+
if (this.selectedIndex < this.listScroll) this.listScroll = this.selectedIndex;
|
|
131017
|
+
else if (this.selectedIndex >= this.listScroll + visibleRows) this.listScroll = this.selectedIndex - visibleRows + 1;
|
|
131018
|
+
const maxScroll = Math.max(0, this.sortedVisible.length - visibleRows);
|
|
131019
|
+
if (this.listScroll < 0) this.listScroll = 0;
|
|
131020
|
+
if (this.listScroll > maxScroll) this.listScroll = maxScroll;
|
|
131021
|
+
}
|
|
131022
|
+
renderRightStack(width, height) {
|
|
131023
|
+
const detailHeight = Math.max(8, Math.min(Math.floor(height * .4), height - 5));
|
|
131024
|
+
const previewHeight = height - detailHeight;
|
|
131025
|
+
return [...this.renderDetailFrame(width, detailHeight), ...this.renderPreviewFrame(width, previewHeight)];
|
|
131026
|
+
}
|
|
131027
|
+
renderDetailFrame(width, height) {
|
|
131028
|
+
const colors = this.props.colors;
|
|
131029
|
+
const innerHeight = Math.max(0, height - 2);
|
|
131030
|
+
const task = this.sortedVisible[this.selectedIndex];
|
|
131031
|
+
if (task === void 0) {
|
|
131032
|
+
const lines = [chalk.hex(colors.textMuted)("从列表中选择一个任务。")];
|
|
131033
|
+
while (lines.length < innerHeight) lines.push("");
|
|
131034
|
+
return this.renderFrame("详情", lines, width, height);
|
|
131035
|
+
}
|
|
131036
|
+
const label = (text) => chalk.hex(colors.textMuted)(text.padEnd(14));
|
|
131037
|
+
const value = (text) => chalk.hex(colors.text)(text);
|
|
131038
|
+
const lines = [
|
|
131039
|
+
`${label("任务 ID:")}${value(task.taskId)}`,
|
|
131040
|
+
`${label("状态:")}${chalk.hex(statusColor(colors, task.status))(STATUS_LABEL[task.status])}`,
|
|
131041
|
+
`${label("描述:")}${value(singleLine$2(task.description) || "—")}`
|
|
131042
|
+
];
|
|
131043
|
+
if (task.command && task.command !== task.description) lines.push(`${label("命令:")}${value(singleLine$2(task.command))}`);
|
|
131044
|
+
const timing = task.status === "running" || task.status === "awaiting_approval" ? `运行中 ${formatRelativeTime$2(task.startedAt)}` : task.endedAt !== null && task.endedAt !== void 0 ? `已完成 ${formatRelativeTime$2(task.endedAt)}` : "";
|
|
131045
|
+
if (timing.length > 0) lines.push(`${label("时间:")}${chalk.hex(colors.textMuted)(timing)}`);
|
|
131046
|
+
if (task.pid > 0) lines.push(`${label("进程 ID:")}${chalk.hex(colors.textMuted)(String(task.pid))}`);
|
|
131047
|
+
if (task.exitCode !== null && task.exitCode !== void 0) lines.push(`${label("退出码:")}${chalk.hex(colors.textMuted)(String(task.exitCode))}`);
|
|
131048
|
+
if (task.stopReason !== void 0 && task.stopReason.length > 0) lines.push(`${label("停止原因:")}${chalk.hex(colors.textMuted)(task.stopReason)}`);
|
|
131049
|
+
if (task.timedOut === true) lines.push(`${label("已超时:")}${chalk.hex(colors.warning)("是")}`);
|
|
131050
|
+
if (task.approvalReason !== void 0 && task.approvalReason.length > 0) lines.push(`${label("等待中:")}${chalk.hex(colors.warning)(singleLine$2(task.approvalReason))}`);
|
|
131051
|
+
while (lines.length < innerHeight) lines.push("");
|
|
131052
|
+
return this.renderFrame("详情", lines, width, height);
|
|
131053
|
+
}
|
|
131054
|
+
renderPreviewFrame(width, height) {
|
|
131055
|
+
const colors = this.props.colors;
|
|
131056
|
+
const innerHeight = Math.max(0, height - 2);
|
|
131057
|
+
if (this.sortedVisible[this.selectedIndex] === void 0) {
|
|
131058
|
+
const lines = [chalk.hex(colors.textMuted)("No task selected.")];
|
|
131059
|
+
while (lines.length < innerHeight) lines.push("");
|
|
131060
|
+
return this.renderFrame("Preview Output", lines, width, height);
|
|
131282
131061
|
}
|
|
131283
|
-
|
|
131062
|
+
let body;
|
|
131063
|
+
if (this.props.tailLoading) body = "[loading…]";
|
|
131064
|
+
else if (this.props.tailOutput === void 0 || this.props.tailOutput.length === 0) body = "[no output captured]";
|
|
131065
|
+
else body = this.props.tailOutput;
|
|
131066
|
+
const styled = body.split("\n").slice(-innerHeight).map((line) => chalk.hex(colors.textDim)(line));
|
|
131067
|
+
while (styled.length < innerHeight) styled.push("");
|
|
131068
|
+
return this.renderFrame("Preview Output", styled, width, height);
|
|
131284
131069
|
}
|
|
131285
|
-
|
|
131286
|
-
|
|
131287
|
-
|
|
131288
|
-
|
|
131289
|
-
|
|
131290
|
-
|
|
131070
|
+
renderTooSmall(width, rows) {
|
|
131071
|
+
const lines = [];
|
|
131072
|
+
const msg = chalk.hex(this.props.colors.error)(`Terminal too small (need ≥ ${String(MIN_WIDTH)} × ${String(MIN_HEIGHT)})`);
|
|
131073
|
+
lines.push(fitExactly$1(msg, width));
|
|
131074
|
+
for (let i = 1; i < rows; i++) lines.push(" ".repeat(width));
|
|
131075
|
+
return lines;
|
|
131291
131076
|
}
|
|
131292
|
-
|
|
131293
|
-
|
|
131294
|
-
|
|
131077
|
+
};
|
|
131078
|
+
//#endregion
|
|
131079
|
+
//#region src/tui/controllers/tasks-browser.ts
|
|
131080
|
+
var TasksBrowserController = class {
|
|
131081
|
+
host;
|
|
131082
|
+
constructor(host) {
|
|
131083
|
+
this.host = host;
|
|
131295
131084
|
}
|
|
131296
|
-
|
|
131297
|
-
const
|
|
131298
|
-
if (
|
|
131299
|
-
|
|
131300
|
-
|
|
131301
|
-
|
|
131302
|
-
this.props.onStopConfirmed(taskId);
|
|
131303
|
-
this.invalidate();
|
|
131304
|
-
return;
|
|
131305
|
-
}
|
|
131306
|
-
this.clearPendingStop();
|
|
131307
|
-
this.invalidate();
|
|
131085
|
+
async show() {
|
|
131086
|
+
const { state } = this.host;
|
|
131087
|
+
if (state.tasksBrowser !== void 0) return;
|
|
131088
|
+
const session = this.host.session;
|
|
131089
|
+
if (session === void 0) {
|
|
131090
|
+
this.host.showError("没有活动会话。");
|
|
131308
131091
|
return;
|
|
131309
131092
|
}
|
|
131310
|
-
|
|
131311
|
-
|
|
131093
|
+
let tasks = [];
|
|
131094
|
+
try {
|
|
131095
|
+
tasks = await session.listBackgroundTasks({ activeOnly: false });
|
|
131096
|
+
} catch (error) {
|
|
131097
|
+
this.host.showError(`加载任务失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
131312
131098
|
return;
|
|
131313
131099
|
}
|
|
131314
|
-
if (
|
|
131315
|
-
|
|
131316
|
-
|
|
131317
|
-
|
|
131318
|
-
|
|
131100
|
+
if (state.tasksBrowser !== void 0) return;
|
|
131101
|
+
const filter = "all";
|
|
131102
|
+
const selectedTaskId = this.pickInitialSelection(tasks, filter);
|
|
131103
|
+
const component = new TasksBrowserApp({
|
|
131104
|
+
tasks,
|
|
131105
|
+
filter,
|
|
131106
|
+
selectedTaskId,
|
|
131107
|
+
tailOutput: void 0,
|
|
131108
|
+
tailLoading: false,
|
|
131109
|
+
flashMessage: void 0,
|
|
131110
|
+
colors: state.theme.colors,
|
|
131111
|
+
...this.buildCallbacks()
|
|
131112
|
+
}, state.terminal);
|
|
131113
|
+
const savedChildren = [...state.ui.children];
|
|
131114
|
+
state.ui.clear();
|
|
131115
|
+
state.ui.addChild(component);
|
|
131116
|
+
state.ui.setFocus(component);
|
|
131117
|
+
state.ui.requestRender(true);
|
|
131118
|
+
const pollTimer = setInterval(() => {
|
|
131119
|
+
this.refresh({ silent: true });
|
|
131120
|
+
}, 1e3);
|
|
131121
|
+
this.host.setTasksBrowser({
|
|
131122
|
+
component,
|
|
131123
|
+
savedChildren,
|
|
131124
|
+
filter,
|
|
131125
|
+
selectedTaskId,
|
|
131126
|
+
tailOutput: void 0,
|
|
131127
|
+
tailLoading: false,
|
|
131128
|
+
tailRequestId: 0,
|
|
131129
|
+
flashMessage: void 0,
|
|
131130
|
+
flashTimer: void 0,
|
|
131131
|
+
pollTimer,
|
|
131132
|
+
viewer: void 0
|
|
131133
|
+
});
|
|
131134
|
+
if (selectedTaskId !== void 0) this.loadTail(selectedTaskId);
|
|
131135
|
+
}
|
|
131136
|
+
close() {
|
|
131137
|
+
const { state } = this.host;
|
|
131138
|
+
const browser = state.tasksBrowser;
|
|
131139
|
+
if (browser === void 0) return;
|
|
131140
|
+
if (browser.viewer !== void 0) this.closeOutputViewer();
|
|
131141
|
+
if (browser.pollTimer !== void 0) clearInterval(browser.pollTimer);
|
|
131142
|
+
if (browser.flashTimer !== void 0) clearTimeout(browser.flashTimer);
|
|
131143
|
+
state.ui.clear();
|
|
131144
|
+
for (const child of browser.savedChildren) state.ui.addChild(child);
|
|
131145
|
+
this.host.setTasksBrowser(void 0);
|
|
131146
|
+
state.ui.setFocus(state.editor);
|
|
131147
|
+
state.ui.requestRender(true);
|
|
131148
|
+
}
|
|
131149
|
+
repaint() {
|
|
131150
|
+
if (this.host.state.tasksBrowser === void 0) return;
|
|
131151
|
+
const tasks = [...this.host.backgroundTasks.values()];
|
|
131152
|
+
this.pushProps(tasks);
|
|
131153
|
+
}
|
|
131154
|
+
async refreshOutputViewer(opts = {}) {
|
|
131155
|
+
const { state } = this.host;
|
|
131156
|
+
const browser = state.tasksBrowser;
|
|
131157
|
+
const viewer = browser?.viewer;
|
|
131158
|
+
if (browser === void 0 || viewer === void 0) return;
|
|
131159
|
+
const session = this.host.session;
|
|
131160
|
+
if (session === void 0) return;
|
|
131161
|
+
const myRefreshId = ++viewer.refreshId;
|
|
131162
|
+
let output;
|
|
131163
|
+
try {
|
|
131164
|
+
output = await session.getBackgroundTaskOutput(viewer.taskId);
|
|
131165
|
+
} catch (error) {
|
|
131166
|
+
if (!opts.silent) {
|
|
131167
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131168
|
+
this.flash(`输出刷新失败: ${message}`);
|
|
131169
|
+
}
|
|
131319
131170
|
return;
|
|
131320
131171
|
}
|
|
131321
|
-
|
|
131322
|
-
|
|
131323
|
-
|
|
131324
|
-
|
|
131325
|
-
|
|
131172
|
+
const current = state.tasksBrowser?.viewer;
|
|
131173
|
+
if (current === void 0 || current !== viewer || current.refreshId !== myRefreshId) return;
|
|
131174
|
+
if (output === viewer.output) return;
|
|
131175
|
+
viewer.output = output;
|
|
131176
|
+
const info = this.host.backgroundTasks.get(viewer.taskId);
|
|
131177
|
+
viewer.component.setProps({
|
|
131178
|
+
taskId: viewer.taskId,
|
|
131179
|
+
info,
|
|
131180
|
+
output,
|
|
131181
|
+
colors: state.theme.colors,
|
|
131182
|
+
onClose: () => {
|
|
131183
|
+
this.closeOutputViewer();
|
|
131184
|
+
}
|
|
131185
|
+
});
|
|
131186
|
+
state.ui.requestRender();
|
|
131187
|
+
}
|
|
131188
|
+
pickInitialSelection(tasks, filter) {
|
|
131189
|
+
const candidates = filter === "all" ? tasks : tasks.filter((t) => t.status !== "completed" && t.status !== "failed" && t.status !== "killed" && t.status !== "lost");
|
|
131190
|
+
if (candidates.length === 0) return void 0;
|
|
131191
|
+
return candidates.find((t) => t.status === "running" || t.status === "awaiting_approval")?.taskId ?? candidates[0].taskId;
|
|
131192
|
+
}
|
|
131193
|
+
async refresh(opts = {}) {
|
|
131194
|
+
const { state } = this.host;
|
|
131195
|
+
const browser = state.tasksBrowser;
|
|
131196
|
+
if (browser === void 0) return;
|
|
131197
|
+
const session = this.host.session;
|
|
131198
|
+
if (session === void 0) return;
|
|
131199
|
+
let tasks;
|
|
131200
|
+
try {
|
|
131201
|
+
tasks = await session.listBackgroundTasks({ activeOnly: false });
|
|
131202
|
+
} catch (error) {
|
|
131203
|
+
if (!opts.silent) this.flash(`刷新失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
131326
131204
|
return;
|
|
131327
131205
|
}
|
|
131328
|
-
if (
|
|
131329
|
-
|
|
131206
|
+
if (state.tasksBrowser !== browser) return;
|
|
131207
|
+
this.pushProps(tasks);
|
|
131208
|
+
}
|
|
131209
|
+
pushProps(tasks) {
|
|
131210
|
+
const browser = this.host.state.tasksBrowser;
|
|
131211
|
+
if (browser === void 0) return;
|
|
131212
|
+
browser.component.setProps({
|
|
131213
|
+
tasks,
|
|
131214
|
+
filter: browser.filter,
|
|
131215
|
+
selectedTaskId: browser.selectedTaskId,
|
|
131216
|
+
tailOutput: browser.tailOutput,
|
|
131217
|
+
tailLoading: browser.tailLoading,
|
|
131218
|
+
flashMessage: browser.flashMessage,
|
|
131219
|
+
colors: this.host.state.theme.colors,
|
|
131220
|
+
...this.buildCallbacks()
|
|
131221
|
+
});
|
|
131222
|
+
this.host.state.ui.requestRender();
|
|
131223
|
+
}
|
|
131224
|
+
buildCallbacks() {
|
|
131225
|
+
return {
|
|
131226
|
+
onSelect: (taskId) => {
|
|
131227
|
+
this.handleSelect(taskId);
|
|
131228
|
+
},
|
|
131229
|
+
onToggleFilter: () => {
|
|
131230
|
+
this.handleToggleFilter();
|
|
131231
|
+
},
|
|
131232
|
+
onRefresh: () => {
|
|
131233
|
+
this.handleRefresh();
|
|
131234
|
+
},
|
|
131235
|
+
onCancel: () => {
|
|
131236
|
+
this.close();
|
|
131237
|
+
},
|
|
131238
|
+
onStopConfirmed: (taskId) => {
|
|
131239
|
+
this.handleStop(taskId);
|
|
131240
|
+
},
|
|
131241
|
+
onOpenOutput: (taskId) => {
|
|
131242
|
+
this.handleOpenOutput(taskId);
|
|
131243
|
+
},
|
|
131244
|
+
onStopIgnored: (taskId, reason) => {
|
|
131245
|
+
if (reason === "terminal") this.flash(`${taskId} 已是终止状态 — 无需停止。`);
|
|
131246
|
+
}
|
|
131247
|
+
};
|
|
131248
|
+
}
|
|
131249
|
+
handleSelect(taskId) {
|
|
131250
|
+
const browser = this.host.state.tasksBrowser;
|
|
131251
|
+
if (browser === void 0) return;
|
|
131252
|
+
if (browser.selectedTaskId === taskId) return;
|
|
131253
|
+
browser.selectedTaskId = taskId;
|
|
131254
|
+
browser.tailOutput = void 0;
|
|
131255
|
+
browser.tailLoading = true;
|
|
131256
|
+
this.repaint();
|
|
131257
|
+
this.loadTail(taskId);
|
|
131258
|
+
}
|
|
131259
|
+
handleToggleFilter() {
|
|
131260
|
+
const browser = this.host.state.tasksBrowser;
|
|
131261
|
+
if (browser === void 0) return;
|
|
131262
|
+
browser.filter = browser.filter === "all" ? "active" : "all";
|
|
131263
|
+
this.repaint();
|
|
131264
|
+
}
|
|
131265
|
+
handleRefresh() {
|
|
131266
|
+
this.flash("正在刷新…", 600);
|
|
131267
|
+
this.refresh();
|
|
131268
|
+
}
|
|
131269
|
+
async handleStop(taskId) {
|
|
131270
|
+
if (this.host.state.tasksBrowser === void 0) return;
|
|
131271
|
+
const session = this.host.session;
|
|
131272
|
+
if (session === void 0) {
|
|
131273
|
+
this.flash("没有活动会话。");
|
|
131330
131274
|
return;
|
|
131331
131275
|
}
|
|
131332
|
-
|
|
131333
|
-
|
|
131334
|
-
|
|
131276
|
+
this.flash(`正在停止 ${taskId}…`, 1500);
|
|
131277
|
+
try {
|
|
131278
|
+
await session.stopBackgroundTask(taskId, { reason: "用户发起停止" });
|
|
131279
|
+
await this.refresh({ silent: true });
|
|
131280
|
+
} catch (error) {
|
|
131281
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131282
|
+
this.flash(`停止失败: ${message}`);
|
|
131335
131283
|
}
|
|
131336
|
-
|
|
131337
|
-
|
|
131338
|
-
|
|
131339
|
-
|
|
131340
|
-
|
|
131341
|
-
|
|
131342
|
-
|
|
131343
|
-
|
|
131344
|
-
this.
|
|
131345
|
-
this.clearPendingStop();
|
|
131346
|
-
this.invalidate();
|
|
131347
|
-
}, STOP_CONFIRM_TIMEOUT_MS);
|
|
131348
|
-
this.invalidate();
|
|
131284
|
+
}
|
|
131285
|
+
async handleOpenOutput(taskId) {
|
|
131286
|
+
const { state } = this.host;
|
|
131287
|
+
const browser = state.tasksBrowser;
|
|
131288
|
+
if (browser === void 0) return;
|
|
131289
|
+
if (browser.viewer !== void 0) return;
|
|
131290
|
+
const session = this.host.session;
|
|
131291
|
+
if (session === void 0) {
|
|
131292
|
+
this.flash("没有活动会话。");
|
|
131349
131293
|
return;
|
|
131350
131294
|
}
|
|
131351
|
-
|
|
131352
|
-
|
|
131353
|
-
|
|
131295
|
+
let output;
|
|
131296
|
+
try {
|
|
131297
|
+
output = await session.getBackgroundTaskOutput(taskId);
|
|
131298
|
+
} catch (error) {
|
|
131299
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131300
|
+
this.flash(`无法打开输出: ${message}`);
|
|
131354
131301
|
return;
|
|
131355
131302
|
}
|
|
131303
|
+
const current = state.tasksBrowser;
|
|
131304
|
+
if (current === void 0 || current !== browser) return;
|
|
131305
|
+
const viewer = new TaskOutputViewer({
|
|
131306
|
+
taskId,
|
|
131307
|
+
info: this.host.backgroundTasks.get(taskId),
|
|
131308
|
+
output,
|
|
131309
|
+
colors: state.theme.colors,
|
|
131310
|
+
onClose: () => {
|
|
131311
|
+
this.closeOutputViewer();
|
|
131312
|
+
}
|
|
131313
|
+
}, state.terminal);
|
|
131314
|
+
const savedBrowserChildren = [...state.ui.children];
|
|
131315
|
+
state.ui.clear();
|
|
131316
|
+
state.ui.addChild(viewer);
|
|
131317
|
+
state.ui.setFocus(viewer);
|
|
131318
|
+
state.ui.requestRender(true);
|
|
131319
|
+
const pollTimer = setInterval(() => {
|
|
131320
|
+
this.refreshOutputViewer({ silent: true });
|
|
131321
|
+
}, 1e3);
|
|
131322
|
+
browser.viewer = {
|
|
131323
|
+
component: viewer,
|
|
131324
|
+
savedChildren: savedBrowserChildren,
|
|
131325
|
+
taskId,
|
|
131326
|
+
output,
|
|
131327
|
+
refreshId: 0,
|
|
131328
|
+
pollTimer
|
|
131329
|
+
};
|
|
131356
131330
|
}
|
|
131357
|
-
|
|
131358
|
-
|
|
131359
|
-
|
|
131360
|
-
|
|
131361
|
-
|
|
131362
|
-
|
|
131363
|
-
|
|
131364
|
-
|
|
131365
|
-
|
|
131366
|
-
|
|
131367
|
-
const
|
|
131368
|
-
|
|
131369
|
-
|
|
131370
|
-
|
|
131371
|
-
|
|
131372
|
-
|
|
131373
|
-
|
|
131374
|
-
|
|
131331
|
+
loadTail(taskId) {
|
|
131332
|
+
const { state } = this.host;
|
|
131333
|
+
const browser = state.tasksBrowser;
|
|
131334
|
+
if (browser === void 0) return;
|
|
131335
|
+
const session = this.host.session;
|
|
131336
|
+
if (session === void 0) {
|
|
131337
|
+
browser.tailLoading = false;
|
|
131338
|
+
this.repaint();
|
|
131339
|
+
return;
|
|
131340
|
+
}
|
|
131341
|
+
const requestId = ++browser.tailRequestId;
|
|
131342
|
+
session.getBackgroundTaskOutput(taskId, { tail: 4e3 }).then((output) => {
|
|
131343
|
+
const current = state.tasksBrowser;
|
|
131344
|
+
if (current === void 0) return;
|
|
131345
|
+
if (current !== browser || current.tailRequestId !== requestId) return;
|
|
131346
|
+
if (current.selectedTaskId !== taskId) return;
|
|
131347
|
+
current.tailOutput = output;
|
|
131348
|
+
current.tailLoading = false;
|
|
131349
|
+
this.repaint();
|
|
131350
|
+
}).catch(() => {
|
|
131351
|
+
const current = state.tasksBrowser;
|
|
131352
|
+
if (current === void 0) return;
|
|
131353
|
+
if (current !== browser || current.tailRequestId !== requestId) return;
|
|
131354
|
+
if (current.selectedTaskId !== taskId) return;
|
|
131355
|
+
current.tailOutput = "";
|
|
131356
|
+
current.tailLoading = false;
|
|
131357
|
+
this.repaint();
|
|
131358
|
+
});
|
|
131375
131359
|
}
|
|
131376
|
-
|
|
131377
|
-
const
|
|
131378
|
-
|
|
131379
|
-
|
|
131380
|
-
|
|
131381
|
-
|
|
131382
|
-
|
|
131383
|
-
|
|
131384
|
-
|
|
131385
|
-
|
|
131386
|
-
|
|
131387
|
-
|
|
131360
|
+
flash(message, durationMs = 2500) {
|
|
131361
|
+
const browser = this.host.state.tasksBrowser;
|
|
131362
|
+
if (browser === void 0) return;
|
|
131363
|
+
if (browser.flashTimer !== void 0) clearTimeout(browser.flashTimer);
|
|
131364
|
+
browser.flashMessage = message;
|
|
131365
|
+
browser.flashTimer = setTimeout(() => {
|
|
131366
|
+
const current = this.host.state.tasksBrowser;
|
|
131367
|
+
if (current !== browser) return;
|
|
131368
|
+
current.flashMessage = void 0;
|
|
131369
|
+
current.flashTimer = void 0;
|
|
131370
|
+
this.repaint();
|
|
131371
|
+
}, durationMs);
|
|
131372
|
+
this.repaint();
|
|
131388
131373
|
}
|
|
131389
|
-
|
|
131390
|
-
const
|
|
131391
|
-
|
|
131392
|
-
const
|
|
131393
|
-
|
|
131394
|
-
|
|
131395
|
-
|
|
131396
|
-
|
|
131397
|
-
|
|
131398
|
-
|
|
131399
|
-
`${key("Enter/O")} ${dim("输出")}`,
|
|
131400
|
-
`${key("S")} ${dim("停止")}`,
|
|
131401
|
-
`${key("R")} ${dim("刷新")}`,
|
|
131402
|
-
`${key("Tab")} ${dim("筛选")}`,
|
|
131403
|
-
`${key("Q/Esc")} ${dim("退出")} `
|
|
131404
|
-
].join(" ");
|
|
131405
|
-
const flash = this.props.flashMessage;
|
|
131406
|
-
if (flash !== void 0 && flash.length > 0) {
|
|
131407
|
-
const flashStyled = chalk.hex(colors.warning)(` ${flash} `);
|
|
131408
|
-
const total = visibleWidth(left) + visibleWidth(flashStyled);
|
|
131409
|
-
if (total <= width) return left + " ".repeat(width - total) + flashStyled;
|
|
131410
|
-
}
|
|
131411
|
-
return fitExactly$1(left, width);
|
|
131374
|
+
closeOutputViewer() {
|
|
131375
|
+
const browser = this.host.state.tasksBrowser;
|
|
131376
|
+
if (browser === void 0 || browser.viewer === void 0) return;
|
|
131377
|
+
const viewer = browser.viewer;
|
|
131378
|
+
clearInterval(viewer.pollTimer);
|
|
131379
|
+
browser.viewer = void 0;
|
|
131380
|
+
this.host.state.ui.clear();
|
|
131381
|
+
for (const child of viewer.savedChildren) this.host.state.ui.addChild(child);
|
|
131382
|
+
this.host.state.ui.setFocus(browser.component);
|
|
131383
|
+
this.host.state.ui.requestRender(true);
|
|
131412
131384
|
}
|
|
131413
|
-
|
|
131414
|
-
|
|
131415
|
-
|
|
131416
|
-
|
|
131417
|
-
|
|
131418
|
-
|
|
131419
|
-
|
|
131420
|
-
|
|
131421
|
-
|
|
131422
|
-
|
|
131423
|
-
|
|
131424
|
-
|
|
131425
|
-
|
|
131426
|
-
|
|
131427
|
-
|
|
131428
|
-
|
|
131429
|
-
|
|
131430
|
-
|
|
131431
|
-
|
|
131432
|
-
|
|
131433
|
-
|
|
131434
|
-
|
|
131435
|
-
|
|
131436
|
-
|
|
131437
|
-
|
|
131438
|
-
|
|
131439
|
-
|
|
131440
|
-
|
|
131385
|
+
};
|
|
131386
|
+
//#endregion
|
|
131387
|
+
//#region src/tui/components/editor/file-mention-provider.ts
|
|
131388
|
+
/**
|
|
131389
|
+
* `@file` autocomplete provider for the input box.
|
|
131390
|
+
*
|
|
131391
|
+
* pi-tui's `CombinedAutocompleteProvider` handles the mechanical parts
|
|
131392
|
+
* (extract `@…` prefix, insert completion with the right quoting). This
|
|
131393
|
+
* wrapper adds scream-specific ranking + filtering so the default "empty
|
|
131394
|
+
* `@`" list surfaces files the user actually wants, not alphabetical
|
|
131395
|
+
* noise from `.agents/skills/*` et al.
|
|
131396
|
+
*
|
|
131397
|
+
* Sort order — empty query:
|
|
131398
|
+
* 1. recently edited (from `git log --name-only`)
|
|
131399
|
+
* 2. recent fs mtime
|
|
131400
|
+
* 3. basename alphabetical
|
|
131401
|
+
* (first 15, not 50 — pi-tui's menu height is ~6-10 lines anyway)
|
|
131402
|
+
*
|
|
131403
|
+
* Sort order — non-empty query (strict to fuzzy):
|
|
131404
|
+
* cat 0: basename starts-with query
|
|
131405
|
+
* cat 1: basename contains query
|
|
131406
|
+
* cat 2: fuzzyMatch succeeds on full path
|
|
131407
|
+
* tie-break within each cat: recency rank → mtime → basename length
|
|
131408
|
+
* (first 50)
|
|
131409
|
+
*
|
|
131410
|
+
* Filter — dot directories are hidden by default. User can opt in by starting the query
|
|
131411
|
+
* with `.` (e.g. `@.github/`), since those paths rarely need
|
|
131412
|
+
* completion.
|
|
131413
|
+
*
|
|
131414
|
+
* When `fd` is available the inner pi-tui provider owns the `@` branch
|
|
131415
|
+
* verbatim — its fd invocation respects `.gitignore` and is strictly
|
|
131416
|
+
* better than anything we can cheaply reproduce in TS. We only kick in
|
|
131417
|
+
* when `fd` is missing AND we're in a git repo.
|
|
131418
|
+
*/
|
|
131419
|
+
const MAX_SUGGESTIONS_WHEN_QUERY = 50;
|
|
131420
|
+
const MAX_SUGGESTIONS_WHEN_EMPTY = 15;
|
|
131421
|
+
const PATH_DELIMITERS = new Set([
|
|
131422
|
+
" ",
|
|
131423
|
+
" ",
|
|
131424
|
+
"\"",
|
|
131425
|
+
"'",
|
|
131426
|
+
"="
|
|
131427
|
+
]);
|
|
131428
|
+
var FileMentionProvider = class {
|
|
131429
|
+
fdPath;
|
|
131430
|
+
gitCache;
|
|
131431
|
+
inner;
|
|
131432
|
+
slashCommandItems;
|
|
131433
|
+
constructor(slashCommands, workDir, fdPath, gitCache) {
|
|
131434
|
+
this.fdPath = fdPath;
|
|
131435
|
+
this.gitCache = gitCache;
|
|
131436
|
+
this.slashCommandItems = slashCommands.map((cmd) => {
|
|
131437
|
+
const ac = cmd;
|
|
131438
|
+
if (ac.label !== void 0 && ac.label.length > 0) return {
|
|
131439
|
+
value: ac.value ?? ac.name ?? "",
|
|
131440
|
+
label: ac.label
|
|
131441
|
+
};
|
|
131442
|
+
const name = ac.value ?? ac.name ?? "";
|
|
131443
|
+
const desc = ac.description ?? "";
|
|
131444
|
+
return {
|
|
131445
|
+
value: name,
|
|
131446
|
+
label: `/${name}${desc ? ` — ${desc}` : ""}`
|
|
131447
|
+
};
|
|
131448
|
+
});
|
|
131449
|
+
this.inner = new CombinedAutocompleteProvider(slashCommands, workDir, fdPath);
|
|
131441
131450
|
}
|
|
131442
|
-
|
|
131443
|
-
const
|
|
131444
|
-
const
|
|
131445
|
-
if (
|
|
131446
|
-
const
|
|
131447
|
-
const
|
|
131448
|
-
|
|
131449
|
-
return
|
|
131451
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
131452
|
+
const textBeforeCursor = (lines[cursorLine] ?? "").slice(0, cursorCol);
|
|
131453
|
+
const atPrefix = extractAtPrefix(textBeforeCursor);
|
|
131454
|
+
if (!options.force && atPrefix === null && textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")) {
|
|
131455
|
+
const query = textBeforeCursor.slice(1);
|
|
131456
|
+
const filtered = fuzzyFilter(this.slashCommandItems, query, (item) => item.value).slice(0, MAX_SUGGESTIONS_WHEN_QUERY);
|
|
131457
|
+
if (filtered.length === 0) return null;
|
|
131458
|
+
return {
|
|
131459
|
+
items: filtered,
|
|
131460
|
+
prefix: textBeforeCursor
|
|
131461
|
+
};
|
|
131450
131462
|
}
|
|
131451
|
-
this.
|
|
131452
|
-
|
|
131453
|
-
const
|
|
131454
|
-
|
|
131455
|
-
const
|
|
131456
|
-
|
|
131457
|
-
|
|
131458
|
-
|
|
131463
|
+
if (atPrefix === null) return this.inner.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
131464
|
+
if (this.fdPath !== null) return this.inner.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
131465
|
+
const snapshot = this.gitCache.getSnapshot();
|
|
131466
|
+
if (snapshot === null || snapshot.files.length === 0) return this.inner.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
131467
|
+
const query = atPrefix.slice(1);
|
|
131468
|
+
const candidates = query.startsWith(".") ? snapshot.files : snapshot.files.filter((p) => !containsDotSegment(p));
|
|
131469
|
+
const items = query.length === 0 ? rankForEmptyQuery(candidates, snapshot) : rankForQuery(candidates, query, snapshot);
|
|
131470
|
+
if (items.length === 0) return this.inner.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
131471
|
+
return {
|
|
131472
|
+
items,
|
|
131473
|
+
prefix: atPrefix
|
|
131474
|
+
};
|
|
131475
|
+
}
|
|
131476
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
131477
|
+
if (prefix.startsWith("/")) {
|
|
131478
|
+
const line = lines[cursorLine] ?? "";
|
|
131479
|
+
const before = line.slice(0, cursorCol - prefix.length);
|
|
131480
|
+
const after = line.slice(cursorCol);
|
|
131481
|
+
const replacement = `/${item.value} `;
|
|
131482
|
+
const newLine = `${before}${replacement}${after}`;
|
|
131483
|
+
return {
|
|
131484
|
+
lines: [
|
|
131485
|
+
...lines.slice(0, cursorLine),
|
|
131486
|
+
newLine,
|
|
131487
|
+
...lines.slice(cursorLine + 1)
|
|
131488
|
+
],
|
|
131489
|
+
cursorLine,
|
|
131490
|
+
cursorCol: before.length + replacement.length
|
|
131491
|
+
};
|
|
131459
131492
|
}
|
|
131460
|
-
|
|
131461
|
-
return this.renderFrame(title, lines, width, height);
|
|
131493
|
+
return this.inner.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
131462
131494
|
}
|
|
131463
|
-
|
|
131464
|
-
|
|
131465
|
-
|
|
131466
|
-
|
|
131467
|
-
|
|
131468
|
-
|
|
131469
|
-
|
|
131470
|
-
|
|
131471
|
-
|
|
131472
|
-
|
|
131473
|
-
|
|
131474
|
-
|
|
131475
|
-
const desc = truncateToWidth(singleLine$2(task.description) || singleLine$2(task.command) || "(no description)", descBudget, ELLIPSIS$3);
|
|
131476
|
-
return fitExactly$1(`${prefix} ${chalk.hex(colors.text)(desc)}`, innerWidth);
|
|
131495
|
+
};
|
|
131496
|
+
/**
|
|
131497
|
+
* Return the `@…` token ending at the cursor, or `null` if we're not in
|
|
131498
|
+
* an `@` mention. Mirrors pi-tui's `extractAtPrefix` — the token
|
|
131499
|
+
* boundary is the last PATH_DELIMITER before the cursor, and the token
|
|
131500
|
+
* must start with `@`.
|
|
131501
|
+
*/
|
|
131502
|
+
function extractAtPrefix(text) {
|
|
131503
|
+
let tokenStart = 0;
|
|
131504
|
+
for (let i = text.length - 1; i >= 0; i -= 1) if (PATH_DELIMITERS.has(text[i] ?? "")) {
|
|
131505
|
+
tokenStart = i + 1;
|
|
131506
|
+
break;
|
|
131477
131507
|
}
|
|
131478
|
-
|
|
131479
|
-
|
|
131480
|
-
|
|
131481
|
-
|
|
131508
|
+
if (text[tokenStart] !== "@") return null;
|
|
131509
|
+
return text.slice(tokenStart);
|
|
131510
|
+
}
|
|
131511
|
+
/** True when any path segment starts with a dot (e.g. `.github/x.yml`). */
|
|
131512
|
+
function containsDotSegment(path) {
|
|
131513
|
+
for (const segment of path.split("/")) if (segment.startsWith(".")) return true;
|
|
131514
|
+
return false;
|
|
131515
|
+
}
|
|
131516
|
+
/**
|
|
131517
|
+
* Empty-query ranking: stratified by signal strength.
|
|
131518
|
+
*
|
|
131519
|
+
* Layer 1: files touched in the last RECENT_COMMIT_DEPTH commits,
|
|
131520
|
+
* ordered by how recently. Strongest signal — if the user
|
|
131521
|
+
* just worked on it, they probably want to mention it.
|
|
131522
|
+
* Layer 2: files with the newest fs mtime (covers uncommitted edits
|
|
131523
|
+
* and files edited but not yet added to git).
|
|
131524
|
+
* Layer 3: everything else, alphabetical by basename so
|
|
131525
|
+
* README/package.json-style top-level files bubble up
|
|
131526
|
+
* relative to deeply-nested alphabetical paths.
|
|
131527
|
+
*
|
|
131528
|
+
* Cap at MAX_SUGGESTIONS_WHEN_EMPTY. Layers fill in order; dedup by
|
|
131529
|
+
* path so a recently-edited file isn't also listed in layer 2.
|
|
131530
|
+
*/
|
|
131531
|
+
function rankForEmptyQuery(files, snapshot) {
|
|
131532
|
+
const picked = /* @__PURE__ */ new Set();
|
|
131533
|
+
const result = [];
|
|
131534
|
+
const cap = MAX_SUGGESTIONS_WHEN_EMPTY;
|
|
131535
|
+
const inFiles = new Set(files);
|
|
131536
|
+
const byRecency = [...snapshot.recencyOrder.entries()].filter(([path]) => inFiles.has(path)).toSorted((a, b) => a[1] - b[1]);
|
|
131537
|
+
for (const [path] of byRecency) {
|
|
131538
|
+
if (result.length >= cap) break;
|
|
131539
|
+
if (picked.has(path)) continue;
|
|
131540
|
+
picked.add(path);
|
|
131541
|
+
result.push(path);
|
|
131542
|
+
}
|
|
131543
|
+
if (result.length < cap) {
|
|
131544
|
+
const byMtime = files.filter((p) => !picked.has(p) && snapshot.mtimeByPath.has(p)).toSorted((a, b) => (snapshot.mtimeByPath.get(b) ?? 0) - (snapshot.mtimeByPath.get(a) ?? 0));
|
|
131545
|
+
for (const path of byMtime) {
|
|
131546
|
+
if (result.length >= cap) break;
|
|
131547
|
+
picked.add(path);
|
|
131548
|
+
result.push(path);
|
|
131482
131549
|
}
|
|
131483
|
-
if (this.selectedIndex < this.listScroll) this.listScroll = this.selectedIndex;
|
|
131484
|
-
else if (this.selectedIndex >= this.listScroll + visibleRows) this.listScroll = this.selectedIndex - visibleRows + 1;
|
|
131485
|
-
const maxScroll = Math.max(0, this.sortedVisible.length - visibleRows);
|
|
131486
|
-
if (this.listScroll < 0) this.listScroll = 0;
|
|
131487
|
-
if (this.listScroll > maxScroll) this.listScroll = maxScroll;
|
|
131488
131550
|
}
|
|
131489
|
-
|
|
131490
|
-
const
|
|
131491
|
-
const
|
|
131492
|
-
|
|
131551
|
+
if (result.length < cap) {
|
|
131552
|
+
const rest = files.filter((p) => !picked.has(p)).toSorted((a, b) => basename(a).localeCompare(basename(b)) || a.localeCompare(b));
|
|
131553
|
+
for (const path of rest) {
|
|
131554
|
+
if (result.length >= cap) break;
|
|
131555
|
+
result.push(path);
|
|
131556
|
+
}
|
|
131493
131557
|
}
|
|
131494
|
-
|
|
131495
|
-
|
|
131496
|
-
|
|
131497
|
-
|
|
131498
|
-
|
|
131499
|
-
|
|
131500
|
-
|
|
131501
|
-
|
|
131558
|
+
return result.map(toItem);
|
|
131559
|
+
}
|
|
131560
|
+
/**
|
|
131561
|
+
* Non-empty-query ranking: three strictness tiers, with recency /
|
|
131562
|
+
* mtime as tie-breakers inside each tier so "the readme you just
|
|
131563
|
+
* edited" beats "a readme deep in a vendor dir".
|
|
131564
|
+
*/
|
|
131565
|
+
function rankForQuery(files, query, snapshot) {
|
|
131566
|
+
const lowerQuery = query.toLowerCase();
|
|
131567
|
+
const scored = [];
|
|
131568
|
+
for (const path of files) {
|
|
131569
|
+
const base = basename(path).toLowerCase();
|
|
131570
|
+
if (base.startsWith(lowerQuery)) {
|
|
131571
|
+
scored.push({
|
|
131572
|
+
path,
|
|
131573
|
+
cat: 0,
|
|
131574
|
+
fuzzyScore: 0
|
|
131575
|
+
});
|
|
131576
|
+
continue;
|
|
131502
131577
|
}
|
|
131503
|
-
|
|
131504
|
-
|
|
131505
|
-
|
|
131506
|
-
|
|
131507
|
-
|
|
131508
|
-
|
|
131509
|
-
|
|
131510
|
-
|
|
131511
|
-
const
|
|
131512
|
-
if (
|
|
131513
|
-
|
|
131514
|
-
|
|
131515
|
-
|
|
131516
|
-
|
|
131517
|
-
|
|
131518
|
-
|
|
131519
|
-
|
|
131578
|
+
if (base.includes(lowerQuery)) {
|
|
131579
|
+
scored.push({
|
|
131580
|
+
path,
|
|
131581
|
+
cat: 1,
|
|
131582
|
+
fuzzyScore: 0
|
|
131583
|
+
});
|
|
131584
|
+
continue;
|
|
131585
|
+
}
|
|
131586
|
+
const fuzzy = fuzzyMatch(query, path);
|
|
131587
|
+
if (fuzzy.matches) scored.push({
|
|
131588
|
+
path,
|
|
131589
|
+
cat: 2,
|
|
131590
|
+
fuzzyScore: fuzzy.score
|
|
131591
|
+
});
|
|
131592
|
+
}
|
|
131593
|
+
if (scored.length === 0) return fuzzyFilter([...files], query, (p) => p).slice(0, MAX_SUGGESTIONS_WHEN_QUERY).map(toItem);
|
|
131594
|
+
scored.sort((a, b) => {
|
|
131595
|
+
if (a.cat !== b.cat) return a.cat - b.cat;
|
|
131596
|
+
if (a.cat === 2 && a.fuzzyScore !== b.fuzzyScore) return a.fuzzyScore - b.fuzzyScore;
|
|
131597
|
+
const ra = snapshot.recencyOrder.get(a.path);
|
|
131598
|
+
const rb = snapshot.recencyOrder.get(b.path);
|
|
131599
|
+
if (ra !== void 0 && rb !== void 0 && ra !== rb) return ra - rb;
|
|
131600
|
+
if (ra !== void 0 && rb === void 0) return -1;
|
|
131601
|
+
if (ra === void 0 && rb !== void 0) return 1;
|
|
131602
|
+
const ma = snapshot.mtimeByPath.get(a.path) ?? 0;
|
|
131603
|
+
const mb = snapshot.mtimeByPath.get(b.path) ?? 0;
|
|
131604
|
+
if (ma !== mb) return mb - ma;
|
|
131605
|
+
const baseLenDiff = basename(a.path).length - basename(b.path).length;
|
|
131606
|
+
if (baseLenDiff !== 0) return baseLenDiff;
|
|
131607
|
+
return a.path.localeCompare(b.path);
|
|
131608
|
+
});
|
|
131609
|
+
return scored.slice(0, MAX_SUGGESTIONS_WHEN_QUERY).map((entry) => toItem(entry.path));
|
|
131610
|
+
}
|
|
131611
|
+
function toItem(path) {
|
|
131612
|
+
return {
|
|
131613
|
+
value: `@${path}`,
|
|
131614
|
+
label: basename(path),
|
|
131615
|
+
description: path
|
|
131616
|
+
};
|
|
131617
|
+
}
|
|
131618
|
+
//#endregion
|
|
131619
|
+
//#region src/tui/components/messages/cron-message.ts
|
|
131620
|
+
var CronMessageComponent = class {
|
|
131621
|
+
colors;
|
|
131622
|
+
spacer = new Spacer(1);
|
|
131623
|
+
title;
|
|
131624
|
+
detail;
|
|
131625
|
+
titleColor;
|
|
131626
|
+
promptText;
|
|
131627
|
+
bullet;
|
|
131628
|
+
bulletWidth;
|
|
131629
|
+
cachedWidth;
|
|
131630
|
+
cachedLines;
|
|
131631
|
+
constructor(prompt, data, colors) {
|
|
131632
|
+
this.colors = colors;
|
|
131633
|
+
const missed = data.missedCount !== void 0;
|
|
131634
|
+
this.title = missed ? "错过的定时提醒" : "定时提醒触发";
|
|
131635
|
+
this.detail = cronDetail(data);
|
|
131636
|
+
this.titleColor = data.stale === true || missed ? colors.warning : colors.accent;
|
|
131637
|
+
this.promptText = new Text(chalk.hex(colors.text)(prompt), 0, 0);
|
|
131638
|
+
this.bullet = chalk.hex(this.titleColor).bold(STATUS_BULLET);
|
|
131639
|
+
this.bulletWidth = visibleWidth(this.bullet);
|
|
131520
131640
|
}
|
|
131521
|
-
|
|
131522
|
-
|
|
131523
|
-
|
|
131524
|
-
|
|
131525
|
-
const lines = [chalk.hex(colors.textMuted)("No task selected.")];
|
|
131526
|
-
while (lines.length < innerHeight) lines.push("");
|
|
131527
|
-
return this.renderFrame("Preview Output", lines, width, height);
|
|
131528
|
-
}
|
|
131529
|
-
let body;
|
|
131530
|
-
if (this.props.tailLoading) body = "[loading…]";
|
|
131531
|
-
else if (this.props.tailOutput === void 0 || this.props.tailOutput.length === 0) body = "[no output captured]";
|
|
131532
|
-
else body = this.props.tailOutput;
|
|
131533
|
-
const styled = body.split("\n").slice(-innerHeight).map((line) => chalk.hex(colors.textDim)(line));
|
|
131534
|
-
while (styled.length < innerHeight) styled.push("");
|
|
131535
|
-
return this.renderFrame("Preview Output", styled, width, height);
|
|
131641
|
+
invalidate() {
|
|
131642
|
+
this.cachedWidth = void 0;
|
|
131643
|
+
this.cachedLines = void 0;
|
|
131644
|
+
this.promptText.invalidate();
|
|
131536
131645
|
}
|
|
131537
|
-
|
|
131646
|
+
render(width) {
|
|
131647
|
+
if (this.cachedLines !== void 0 && this.cachedWidth === width) return this.cachedLines;
|
|
131648
|
+
const contentWidth = Math.max(1, width - this.bulletWidth);
|
|
131538
131649
|
const lines = [];
|
|
131539
|
-
const
|
|
131540
|
-
|
|
131541
|
-
|
|
131650
|
+
for (const line of this.spacer.render(width)) lines.push(line);
|
|
131651
|
+
const title = chalk.hex(this.titleColor).bold(this.title);
|
|
131652
|
+
lines.push(`${this.bullet}${title}`);
|
|
131653
|
+
if (this.detail !== void 0) lines.push(`${" ".repeat(this.bulletWidth)}${chalk.hex(this.colors.textDim)(this.detail)}`);
|
|
131654
|
+
const promptLines = this.promptText.render(contentWidth);
|
|
131655
|
+
for (const line of promptLines) lines.push(`${" ".repeat(this.bulletWidth)}${line}`);
|
|
131656
|
+
this.cachedWidth = width;
|
|
131657
|
+
this.cachedLines = lines;
|
|
131542
131658
|
return lines;
|
|
131543
131659
|
}
|
|
131544
131660
|
};
|
|
131661
|
+
function cronDetail(data) {
|
|
131662
|
+
const parts = [];
|
|
131663
|
+
if (data.cron !== void 0 && data.cron.length > 0) parts.push(data.cron);
|
|
131664
|
+
if (data.jobId !== void 0 && data.jobId.length > 0) parts.push(`job ${data.jobId}`);
|
|
131665
|
+
if (data.recurring === false) parts.push("一次性");
|
|
131666
|
+
if (data.coalescedCount !== void 0 && data.coalescedCount > 1) parts.push(`${String(data.coalescedCount)} 次合并触发`);
|
|
131667
|
+
if (data.missedCount !== void 0) parts.push(`${String(data.missedCount)} 次错过`);
|
|
131668
|
+
if (data.stale === true) parts.push("最终投递");
|
|
131669
|
+
return parts.length > 0 ? parts.join(" | ") : void 0;
|
|
131670
|
+
}
|
|
131545
131671
|
//#endregion
|
|
131546
|
-
//#region src/tui/
|
|
131547
|
-
var
|
|
131548
|
-
|
|
131549
|
-
|
|
131550
|
-
|
|
131551
|
-
|
|
131552
|
-
|
|
131553
|
-
|
|
131554
|
-
|
|
131555
|
-
|
|
131556
|
-
|
|
131557
|
-
|
|
131672
|
+
//#region src/tui/components/panes/activity-pane.ts
|
|
131673
|
+
var ActivityPaneComponent = class extends Container {
|
|
131674
|
+
constructor(options) {
|
|
131675
|
+
super();
|
|
131676
|
+
if (options.mode === "waiting" || options.mode === "tool") {
|
|
131677
|
+
if (options.spinner !== void 0) {
|
|
131678
|
+
this.addChild(new Spacer(1));
|
|
131679
|
+
this.addChild(options.spinner);
|
|
131680
|
+
}
|
|
131681
|
+
if (options.pulseWave !== void 0) {
|
|
131682
|
+
this.addChild(new Spacer(1));
|
|
131683
|
+
this.addChild(options.pulseWave);
|
|
131684
|
+
}
|
|
131558
131685
|
return;
|
|
131559
131686
|
}
|
|
131560
|
-
|
|
131561
|
-
|
|
131562
|
-
|
|
131563
|
-
|
|
131564
|
-
|
|
131565
|
-
|
|
131687
|
+
if (options.mode === "composing" && options.spinner !== void 0) {
|
|
131688
|
+
this.addChild(new Spacer(1));
|
|
131689
|
+
this.addChild(options.spinner);
|
|
131690
|
+
if (options.pulseWave !== void 0) {
|
|
131691
|
+
this.addChild(new Spacer(1));
|
|
131692
|
+
this.addChild(options.pulseWave);
|
|
131693
|
+
}
|
|
131566
131694
|
}
|
|
131567
|
-
if (state.tasksBrowser !== void 0) return;
|
|
131568
|
-
const filter = "all";
|
|
131569
|
-
const selectedTaskId = this.pickInitialSelection(tasks, filter);
|
|
131570
|
-
const component = new TasksBrowserApp({
|
|
131571
|
-
tasks,
|
|
131572
|
-
filter,
|
|
131573
|
-
selectedTaskId,
|
|
131574
|
-
tailOutput: void 0,
|
|
131575
|
-
tailLoading: false,
|
|
131576
|
-
flashMessage: void 0,
|
|
131577
|
-
colors: state.theme.colors,
|
|
131578
|
-
...this.buildCallbacks()
|
|
131579
|
-
}, state.terminal);
|
|
131580
|
-
const savedChildren = [...state.ui.children];
|
|
131581
|
-
state.ui.clear();
|
|
131582
|
-
state.ui.addChild(component);
|
|
131583
|
-
state.ui.setFocus(component);
|
|
131584
|
-
state.ui.requestRender(true);
|
|
131585
|
-
const pollTimer = setInterval(() => {
|
|
131586
|
-
this.refresh({ silent: true });
|
|
131587
|
-
}, 1e3);
|
|
131588
|
-
this.host.setTasksBrowser({
|
|
131589
|
-
component,
|
|
131590
|
-
savedChildren,
|
|
131591
|
-
filter,
|
|
131592
|
-
selectedTaskId,
|
|
131593
|
-
tailOutput: void 0,
|
|
131594
|
-
tailLoading: false,
|
|
131595
|
-
tailRequestId: 0,
|
|
131596
|
-
flashMessage: void 0,
|
|
131597
|
-
flashTimer: void 0,
|
|
131598
|
-
pollTimer,
|
|
131599
|
-
viewer: void 0
|
|
131600
|
-
});
|
|
131601
|
-
if (selectedTaskId !== void 0) this.loadTail(selectedTaskId);
|
|
131602
131695
|
}
|
|
131603
|
-
|
|
131604
|
-
|
|
131605
|
-
|
|
131606
|
-
|
|
131607
|
-
|
|
131608
|
-
|
|
131609
|
-
|
|
131610
|
-
|
|
131611
|
-
for (const
|
|
131612
|
-
|
|
131613
|
-
|
|
131614
|
-
|
|
131696
|
+
};
|
|
131697
|
+
//#endregion
|
|
131698
|
+
//#region src/tui/components/panes/queue-pane.ts
|
|
131699
|
+
var QueuePaneComponent = class extends Container {
|
|
131700
|
+
constructor(options) {
|
|
131701
|
+
super();
|
|
131702
|
+
const accent = chalk.hex(options.colors.accent);
|
|
131703
|
+
const dim = chalk.hex(options.colors.textDim);
|
|
131704
|
+
for (const item of options.messages) this.addChild(new Text(accent(` ❯ ${item.text}`), 0, 0));
|
|
131705
|
+
if (options.messages.length > 0) {
|
|
131706
|
+
const hint = options.isCompacting && !options.isStreaming ? " ↑ to edit · will send after compaction" : !options.canSteerImmediately ? " ↑ to edit · will send after current task" : " ↑ to edit · ctrl-s to steer immediately";
|
|
131707
|
+
this.addChild(new Text(dim(hint), 0, 0));
|
|
131708
|
+
}
|
|
131615
131709
|
}
|
|
131616
|
-
|
|
131617
|
-
|
|
131618
|
-
|
|
131619
|
-
|
|
131710
|
+
};
|
|
131711
|
+
//#endregion
|
|
131712
|
+
//#region src/tui/reverse-rpc/base-controller.ts
|
|
131713
|
+
var ReverseRpcController = class {
|
|
131714
|
+
uiHooks = null;
|
|
131715
|
+
current = null;
|
|
131716
|
+
queue = [];
|
|
131717
|
+
setUIHooks(hooks) {
|
|
131718
|
+
this.uiHooks = hooks;
|
|
131620
131719
|
}
|
|
131621
|
-
|
|
131622
|
-
|
|
131623
|
-
|
|
131624
|
-
|
|
131625
|
-
|
|
131626
|
-
|
|
131627
|
-
|
|
131628
|
-
|
|
131629
|
-
|
|
131630
|
-
|
|
131631
|
-
|
|
131632
|
-
|
|
131633
|
-
|
|
131634
|
-
|
|
131635
|
-
this.flash(`输出刷新失败: ${message}`);
|
|
131636
|
-
}
|
|
131637
|
-
return;
|
|
131638
|
-
}
|
|
131639
|
-
const current = state.tasksBrowser?.viewer;
|
|
131640
|
-
if (current === void 0 || current !== viewer || current.refreshId !== myRefreshId) return;
|
|
131641
|
-
if (output === viewer.output) return;
|
|
131642
|
-
viewer.output = output;
|
|
131643
|
-
const info = this.host.backgroundTasks.get(viewer.taskId);
|
|
131644
|
-
viewer.component.setProps({
|
|
131645
|
-
taskId: viewer.taskId,
|
|
131646
|
-
info,
|
|
131647
|
-
output,
|
|
131648
|
-
colors: state.theme.colors,
|
|
131649
|
-
onClose: () => {
|
|
131650
|
-
this.closeOutputViewer();
|
|
131651
|
-
}
|
|
131720
|
+
/**
|
|
131721
|
+
* Called when a reverse RPC request arrives from core. The returned promise
|
|
131722
|
+
* resolves after the user responds or `cancelAll` forces cancellation.
|
|
131723
|
+
*/
|
|
131724
|
+
show(payload) {
|
|
131725
|
+
return new Promise((resolve) => {
|
|
131726
|
+
const entry = {
|
|
131727
|
+
payload,
|
|
131728
|
+
resolve
|
|
131729
|
+
};
|
|
131730
|
+
if (this.current === null) {
|
|
131731
|
+
this.current = entry;
|
|
131732
|
+
this.uiHooks?.showPanel(payload);
|
|
131733
|
+
} else this.queue.push(entry);
|
|
131652
131734
|
});
|
|
131653
|
-
state.ui.requestRender();
|
|
131654
131735
|
}
|
|
131655
|
-
|
|
131656
|
-
|
|
131657
|
-
|
|
131658
|
-
|
|
131736
|
+
/** Called by the UI after the user makes a panel choice. */
|
|
131737
|
+
respond(data) {
|
|
131738
|
+
const pending = this.current;
|
|
131739
|
+
this.current = null;
|
|
131740
|
+
pending?.resolve(data);
|
|
131741
|
+
if (pending !== null) this.drainAutoResolved(pending.payload, data);
|
|
131742
|
+
this.advanceOrHide();
|
|
131659
131743
|
}
|
|
131660
|
-
|
|
131661
|
-
|
|
131662
|
-
const
|
|
131663
|
-
|
|
131664
|
-
|
|
131665
|
-
|
|
131666
|
-
|
|
131667
|
-
|
|
131668
|
-
|
|
131669
|
-
|
|
131670
|
-
|
|
131744
|
+
/** Cancels all pending requests during shutdown or session switches. */
|
|
131745
|
+
cancelAll(reason) {
|
|
131746
|
+
const all = [...this.current === null ? [] : [this.current], ...this.queue];
|
|
131747
|
+
this.current = null;
|
|
131748
|
+
this.queue = [];
|
|
131749
|
+
this.uiHooks?.hidePanel();
|
|
131750
|
+
for (const entry of all) entry.resolve(this.createCancelResponse(reason));
|
|
131751
|
+
}
|
|
131752
|
+
hasPending() {
|
|
131753
|
+
return this.current !== null || this.queue.length > 0;
|
|
131754
|
+
}
|
|
131755
|
+
advanceOrHide() {
|
|
131756
|
+
const next = this.queue.shift();
|
|
131757
|
+
if (next === void 0) {
|
|
131758
|
+
this.uiHooks?.hidePanel();
|
|
131671
131759
|
return;
|
|
131672
131760
|
}
|
|
131673
|
-
|
|
131674
|
-
this.
|
|
131761
|
+
this.current = next;
|
|
131762
|
+
this.uiHooks?.showPanel(next.payload);
|
|
131675
131763
|
}
|
|
131676
|
-
|
|
131677
|
-
const
|
|
131678
|
-
|
|
131679
|
-
|
|
131680
|
-
|
|
131681
|
-
|
|
131682
|
-
|
|
131683
|
-
|
|
131684
|
-
tailLoading: browser.tailLoading,
|
|
131685
|
-
flashMessage: browser.flashMessage,
|
|
131686
|
-
colors: this.host.state.theme.colors,
|
|
131687
|
-
...this.buildCallbacks()
|
|
131688
|
-
});
|
|
131689
|
-
this.host.state.ui.requestRender();
|
|
131764
|
+
drainAutoResolved(resolvedPayload, response) {
|
|
131765
|
+
const remaining = [];
|
|
131766
|
+
for (const entry of this.queue) {
|
|
131767
|
+
const auto = this.autoResolveFor(resolvedPayload, response, entry.payload);
|
|
131768
|
+
if (auto === void 0) remaining.push(entry);
|
|
131769
|
+
else entry.resolve(auto);
|
|
131770
|
+
}
|
|
131771
|
+
this.queue = remaining;
|
|
131690
131772
|
}
|
|
131691
|
-
|
|
131773
|
+
/**
|
|
131774
|
+
* Subclasses override to short-circuit queued requests when an answer to the
|
|
131775
|
+
* just-resolved one (e.g. an approve-for-session) implies the same answer
|
|
131776
|
+
* for matching queued requests. Return `undefined` to leave the queued
|
|
131777
|
+
* request waiting for its own panel turn.
|
|
131778
|
+
*/
|
|
131779
|
+
autoResolveFor(_resolvedPayload, _response, _queuedPayload) {}
|
|
131780
|
+
};
|
|
131781
|
+
//#endregion
|
|
131782
|
+
//#region src/tui/reverse-rpc/approval/controller.ts
|
|
131783
|
+
var ApprovalController = class extends ReverseRpcController {
|
|
131784
|
+
createCancelResponse(reason) {
|
|
131692
131785
|
return {
|
|
131693
|
-
|
|
131694
|
-
|
|
131695
|
-
},
|
|
131696
|
-
onToggleFilter: () => {
|
|
131697
|
-
this.handleToggleFilter();
|
|
131698
|
-
},
|
|
131699
|
-
onRefresh: () => {
|
|
131700
|
-
this.handleRefresh();
|
|
131701
|
-
},
|
|
131702
|
-
onCancel: () => {
|
|
131703
|
-
this.close();
|
|
131704
|
-
},
|
|
131705
|
-
onStopConfirmed: (taskId) => {
|
|
131706
|
-
this.handleStop(taskId);
|
|
131707
|
-
},
|
|
131708
|
-
onOpenOutput: (taskId) => {
|
|
131709
|
-
this.handleOpenOutput(taskId);
|
|
131710
|
-
},
|
|
131711
|
-
onStopIgnored: (taskId, reason) => {
|
|
131712
|
-
if (reason === "terminal") this.flash(`${taskId} 已是终止状态 — 无需停止。`);
|
|
131713
|
-
}
|
|
131786
|
+
decision: "cancelled",
|
|
131787
|
+
feedback: reason
|
|
131714
131788
|
};
|
|
131715
131789
|
}
|
|
131716
|
-
|
|
131717
|
-
|
|
131718
|
-
if (
|
|
131719
|
-
if (
|
|
131720
|
-
|
|
131721
|
-
|
|
131722
|
-
|
|
131723
|
-
|
|
131724
|
-
this.loadTail(taskId);
|
|
131790
|
+
autoResolveFor(resolvedPayload, response, queuedPayload) {
|
|
131791
|
+
if (response.decision !== "approved") return void 0;
|
|
131792
|
+
if (response.scope !== "session") return void 0;
|
|
131793
|
+
if (resolvedPayload.action !== queuedPayload.action) return void 0;
|
|
131794
|
+
return {
|
|
131795
|
+
decision: "approved",
|
|
131796
|
+
scope: "session"
|
|
131797
|
+
};
|
|
131725
131798
|
}
|
|
131726
|
-
|
|
131727
|
-
|
|
131728
|
-
|
|
131729
|
-
|
|
131730
|
-
|
|
131799
|
+
};
|
|
131800
|
+
//#endregion
|
|
131801
|
+
//#region src/tui/reverse-rpc/modal-coordinator.ts
|
|
131802
|
+
var ReverseRpcModalCoordinator = class {
|
|
131803
|
+
hooks;
|
|
131804
|
+
active = null;
|
|
131805
|
+
queued = [];
|
|
131806
|
+
constructor(hooks) {
|
|
131807
|
+
this.hooks = hooks;
|
|
131731
131808
|
}
|
|
131732
|
-
|
|
131733
|
-
this.
|
|
131734
|
-
|
|
131809
|
+
showApproval(payload) {
|
|
131810
|
+
this.show({
|
|
131811
|
+
owner: "approval",
|
|
131812
|
+
show: () => {
|
|
131813
|
+
this.hooks.showApprovalPanel(payload);
|
|
131814
|
+
},
|
|
131815
|
+
hide: () => {
|
|
131816
|
+
this.hooks.hideApprovalPanel();
|
|
131817
|
+
}
|
|
131818
|
+
});
|
|
131735
131819
|
}
|
|
131736
|
-
|
|
131737
|
-
|
|
131738
|
-
|
|
131739
|
-
|
|
131740
|
-
|
|
131820
|
+
showQuestion(payload) {
|
|
131821
|
+
this.show({
|
|
131822
|
+
owner: "question",
|
|
131823
|
+
show: () => {
|
|
131824
|
+
this.hooks.showQuestionDialog(payload);
|
|
131825
|
+
},
|
|
131826
|
+
hide: () => {
|
|
131827
|
+
this.hooks.hideQuestionDialog();
|
|
131828
|
+
}
|
|
131829
|
+
});
|
|
131830
|
+
}
|
|
131831
|
+
hide(owner) {
|
|
131832
|
+
if (this.active?.owner === owner) {
|
|
131833
|
+
const active = this.active;
|
|
131834
|
+
this.active = null;
|
|
131835
|
+
active.hide();
|
|
131836
|
+
this.showNext();
|
|
131741
131837
|
return;
|
|
131742
131838
|
}
|
|
131743
|
-
this.
|
|
131744
|
-
|
|
131745
|
-
await session.stopBackgroundTask(taskId, { reason: "用户发起停止" });
|
|
131746
|
-
await this.refresh({ silent: true });
|
|
131747
|
-
} catch (error) {
|
|
131748
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
131749
|
-
this.flash(`停止失败: ${message}`);
|
|
131750
|
-
}
|
|
131839
|
+
const queuedIndex = this.queued.findIndex((entry) => entry.owner === owner);
|
|
131840
|
+
if (queuedIndex >= 0) this.queued.splice(queuedIndex, 1);
|
|
131751
131841
|
}
|
|
131752
|
-
|
|
131753
|
-
const
|
|
131754
|
-
|
|
131755
|
-
|
|
131756
|
-
|
|
131757
|
-
|
|
131758
|
-
|
|
131759
|
-
|
|
131842
|
+
clear() {
|
|
131843
|
+
const active = this.active;
|
|
131844
|
+
this.active = null;
|
|
131845
|
+
this.queued.length = 0;
|
|
131846
|
+
active?.hide();
|
|
131847
|
+
}
|
|
131848
|
+
show(entry) {
|
|
131849
|
+
const active = this.active;
|
|
131850
|
+
if (active === null) {
|
|
131851
|
+
this.active = entry;
|
|
131852
|
+
entry.show();
|
|
131760
131853
|
return;
|
|
131761
131854
|
}
|
|
131762
|
-
|
|
131763
|
-
|
|
131764
|
-
|
|
131765
|
-
} catch (error) {
|
|
131766
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
131767
|
-
this.flash(`无法打开输出: ${message}`);
|
|
131855
|
+
if (active.owner === entry.owner) {
|
|
131856
|
+
this.active = entry;
|
|
131857
|
+
entry.show();
|
|
131768
131858
|
return;
|
|
131769
131859
|
}
|
|
131770
|
-
const
|
|
131771
|
-
if (
|
|
131772
|
-
|
|
131773
|
-
taskId,
|
|
131774
|
-
info: this.host.backgroundTasks.get(taskId),
|
|
131775
|
-
output,
|
|
131776
|
-
colors: state.theme.colors,
|
|
131777
|
-
onClose: () => {
|
|
131778
|
-
this.closeOutputViewer();
|
|
131779
|
-
}
|
|
131780
|
-
}, state.terminal);
|
|
131781
|
-
const savedBrowserChildren = [...state.ui.children];
|
|
131782
|
-
state.ui.clear();
|
|
131783
|
-
state.ui.addChild(viewer);
|
|
131784
|
-
state.ui.setFocus(viewer);
|
|
131785
|
-
state.ui.requestRender(true);
|
|
131786
|
-
const pollTimer = setInterval(() => {
|
|
131787
|
-
this.refreshOutputViewer({ silent: true });
|
|
131788
|
-
}, 1e3);
|
|
131789
|
-
browser.viewer = {
|
|
131790
|
-
component: viewer,
|
|
131791
|
-
savedChildren: savedBrowserChildren,
|
|
131792
|
-
taskId,
|
|
131793
|
-
output,
|
|
131794
|
-
refreshId: 0,
|
|
131795
|
-
pollTimer
|
|
131796
|
-
};
|
|
131797
|
-
}
|
|
131798
|
-
loadTail(taskId) {
|
|
131799
|
-
const { state } = this.host;
|
|
131800
|
-
const browser = state.tasksBrowser;
|
|
131801
|
-
if (browser === void 0) return;
|
|
131802
|
-
const session = this.host.session;
|
|
131803
|
-
if (session === void 0) {
|
|
131804
|
-
browser.tailLoading = false;
|
|
131805
|
-
this.repaint();
|
|
131860
|
+
const queuedIndex = this.queued.findIndex((queued) => queued.owner === entry.owner);
|
|
131861
|
+
if (queuedIndex >= 0) {
|
|
131862
|
+
this.queued[queuedIndex] = entry;
|
|
131806
131863
|
return;
|
|
131807
131864
|
}
|
|
131808
|
-
|
|
131809
|
-
session.getBackgroundTaskOutput(taskId, { tail: 4e3 }).then((output) => {
|
|
131810
|
-
const current = state.tasksBrowser;
|
|
131811
|
-
if (current === void 0) return;
|
|
131812
|
-
if (current !== browser || current.tailRequestId !== requestId) return;
|
|
131813
|
-
if (current.selectedTaskId !== taskId) return;
|
|
131814
|
-
current.tailOutput = output;
|
|
131815
|
-
current.tailLoading = false;
|
|
131816
|
-
this.repaint();
|
|
131817
|
-
}).catch(() => {
|
|
131818
|
-
const current = state.tasksBrowser;
|
|
131819
|
-
if (current === void 0) return;
|
|
131820
|
-
if (current !== browser || current.tailRequestId !== requestId) return;
|
|
131821
|
-
if (current.selectedTaskId !== taskId) return;
|
|
131822
|
-
current.tailOutput = "";
|
|
131823
|
-
current.tailLoading = false;
|
|
131824
|
-
this.repaint();
|
|
131825
|
-
});
|
|
131865
|
+
this.queued.push(entry);
|
|
131826
131866
|
}
|
|
131827
|
-
|
|
131828
|
-
const
|
|
131829
|
-
if (
|
|
131830
|
-
|
|
131831
|
-
|
|
131832
|
-
browser.flashTimer = setTimeout(() => {
|
|
131833
|
-
const current = this.host.state.tasksBrowser;
|
|
131834
|
-
if (current !== browser) return;
|
|
131835
|
-
current.flashMessage = void 0;
|
|
131836
|
-
current.flashTimer = void 0;
|
|
131837
|
-
this.repaint();
|
|
131838
|
-
}, durationMs);
|
|
131839
|
-
this.repaint();
|
|
131867
|
+
showNext() {
|
|
131868
|
+
const next = this.queued.shift();
|
|
131869
|
+
if (next === void 0) return;
|
|
131870
|
+
this.active = next;
|
|
131871
|
+
next.show();
|
|
131840
131872
|
}
|
|
131841
|
-
|
|
131842
|
-
|
|
131843
|
-
|
|
131844
|
-
|
|
131845
|
-
|
|
131846
|
-
|
|
131847
|
-
|
|
131848
|
-
|
|
131849
|
-
|
|
131850
|
-
|
|
131873
|
+
};
|
|
131874
|
+
//#endregion
|
|
131875
|
+
//#region src/tui/reverse-rpc/index.ts
|
|
131876
|
+
function registerReverseRPCHandlers(approvalController, questionController, uiHooks) {
|
|
131877
|
+
const modalCoordinator = new ReverseRpcModalCoordinator(uiHooks);
|
|
131878
|
+
approvalController.setUIHooks({
|
|
131879
|
+
showPanel: (payload) => {
|
|
131880
|
+
modalCoordinator.showApproval(payload);
|
|
131881
|
+
},
|
|
131882
|
+
hidePanel: () => {
|
|
131883
|
+
modalCoordinator.hide("approval");
|
|
131884
|
+
}
|
|
131885
|
+
});
|
|
131886
|
+
questionController.setUIHooks({
|
|
131887
|
+
showPanel: (payload) => {
|
|
131888
|
+
modalCoordinator.showQuestion(payload);
|
|
131889
|
+
},
|
|
131890
|
+
hidePanel: () => {
|
|
131891
|
+
modalCoordinator.hide("question");
|
|
131892
|
+
}
|
|
131893
|
+
});
|
|
131894
|
+
return [() => {
|
|
131895
|
+
modalCoordinator.clear();
|
|
131896
|
+
}];
|
|
131897
|
+
}
|
|
131898
|
+
//#endregion
|
|
131899
|
+
//#region src/tui/reverse-rpc/question/controller.ts
|
|
131900
|
+
var QuestionController = class extends ReverseRpcController {
|
|
131901
|
+
createCancelResponse(_reason) {
|
|
131902
|
+
return { answers: [] };
|
|
131851
131903
|
}
|
|
131852
131904
|
};
|
|
131853
131905
|
//#endregion
|
|
131854
|
-
//#region src/tui/
|
|
131906
|
+
//#region src/tui/theme/bundle.ts
|
|
131907
|
+
function createScreamTUIThemeBundle(theme, resolvedTheme) {
|
|
131908
|
+
const actualTheme = resolvedTheme ?? resolveThemeSync(theme);
|
|
131909
|
+
const colors = { ...getColorPalette(actualTheme) };
|
|
131910
|
+
return {
|
|
131911
|
+
resolvedTheme: actualTheme,
|
|
131912
|
+
colors,
|
|
131913
|
+
styles: createThemeStyles(colors),
|
|
131914
|
+
markdownTheme: createMarkdownTheme(colors)
|
|
131915
|
+
};
|
|
131916
|
+
}
|
|
131917
|
+
//#endregion
|
|
131918
|
+
//#region src/tui/types.ts
|
|
131919
|
+
const INITIAL_LIVE_PANE = {
|
|
131920
|
+
mode: "idle",
|
|
131921
|
+
pendingApproval: null,
|
|
131922
|
+
pendingQuestion: null
|
|
131923
|
+
};
|
|
131924
|
+
//#endregion
|
|
131925
|
+
//#region src/utils/git/git-status.ts
|
|
131855
131926
|
/**
|
|
131856
|
-
*
|
|
131857
|
-
*
|
|
131858
|
-
* pi-tui's `CombinedAutocompleteProvider` handles the mechanical parts
|
|
131859
|
-
* (extract `@…` prefix, insert completion with the right quoting). This
|
|
131860
|
-
* wrapper adds scream-specific ranking + filtering so the default "empty
|
|
131861
|
-
* `@`" list surfaces files the user actually wants, not alphabetical
|
|
131862
|
-
* noise from `.agents/skills/*` et al.
|
|
131863
|
-
*
|
|
131864
|
-
* Sort order — empty query:
|
|
131865
|
-
* 1. recently edited (from `git log --name-only`)
|
|
131866
|
-
* 2. recent fs mtime
|
|
131867
|
-
* 3. basename alphabetical
|
|
131868
|
-
* (first 15, not 50 — pi-tui's menu height is ~6-10 lines anyway)
|
|
131869
|
-
*
|
|
131870
|
-
* Sort order — non-empty query (strict to fuzzy):
|
|
131871
|
-
* cat 0: basename starts-with query
|
|
131872
|
-
* cat 1: basename contains query
|
|
131873
|
-
* cat 2: fuzzyMatch succeeds on full path
|
|
131874
|
-
* tie-break within each cat: recency rank → mtime → basename length
|
|
131875
|
-
* (first 50)
|
|
131876
|
-
*
|
|
131877
|
-
* Filter — dot directories are hidden by default. User can opt in by starting the query
|
|
131878
|
-
* with `.` (e.g. `@.github/`), since those paths rarely need
|
|
131879
|
-
* completion.
|
|
131927
|
+
* Cached git branch + working-tree status for the footer/statusline.
|
|
131880
131928
|
*
|
|
131881
|
-
*
|
|
131882
|
-
*
|
|
131883
|
-
*
|
|
131884
|
-
*
|
|
131929
|
+
* Branch name refreshes every 5s, porcelain status every 15s. Branch
|
|
131930
|
+
* and status reads stay synchronous with short timeouts. Pull request
|
|
131931
|
+
* lookup uses an async cache so a slow `gh pr view` never blocks
|
|
131932
|
+
* footer rendering.
|
|
131885
131933
|
*/
|
|
131886
|
-
const
|
|
131887
|
-
const
|
|
131888
|
-
const
|
|
131889
|
-
|
|
131890
|
-
|
|
131891
|
-
|
|
131892
|
-
|
|
131893
|
-
|
|
131894
|
-
|
|
131895
|
-
|
|
131896
|
-
|
|
131897
|
-
|
|
131898
|
-
|
|
131899
|
-
|
|
131900
|
-
|
|
131901
|
-
|
|
131902
|
-
|
|
131903
|
-
|
|
131904
|
-
|
|
131905
|
-
|
|
131906
|
-
|
|
131907
|
-
|
|
131908
|
-
|
|
131909
|
-
|
|
131910
|
-
|
|
131911
|
-
|
|
131912
|
-
|
|
131913
|
-
|
|
131934
|
+
const BRANCH_TTL_MS = 5e3;
|
|
131935
|
+
const STATUS_TTL_MS = 15e3;
|
|
131936
|
+
const PULL_REQUEST_TTL_MS = 6e4;
|
|
131937
|
+
const SPAWN_TIMEOUT_MS = 500;
|
|
131938
|
+
const PR_SPAWN_TIMEOUT_MS = 5e3;
|
|
131939
|
+
const AHEAD_BEHIND_RE = /\[(?:ahead (\d+))?(?:, )?(?:behind (\d+))?\]/;
|
|
131940
|
+
function createGitStatusCache(workDir, options = {}) {
|
|
131941
|
+
const isRepo = detectGitRepo(workDir);
|
|
131942
|
+
let branch = {
|
|
131943
|
+
value: null,
|
|
131944
|
+
fetchedAt: 0
|
|
131945
|
+
};
|
|
131946
|
+
let status = {
|
|
131947
|
+
dirty: false,
|
|
131948
|
+
ahead: 0,
|
|
131949
|
+
behind: 0,
|
|
131950
|
+
diffAdded: 0,
|
|
131951
|
+
diffDeleted: 0,
|
|
131952
|
+
fetchedAt: 0
|
|
131953
|
+
};
|
|
131954
|
+
let pullRequest = {
|
|
131955
|
+
value: null,
|
|
131956
|
+
branch: null,
|
|
131957
|
+
fetchedAt: 0,
|
|
131958
|
+
pendingBranch: null,
|
|
131959
|
+
requestId: 0
|
|
131960
|
+
};
|
|
131961
|
+
return { getStatus: () => {
|
|
131962
|
+
if (!isRepo) return null;
|
|
131963
|
+
const now = Date.now();
|
|
131964
|
+
if (now - branch.fetchedAt >= BRANCH_TTL_MS) branch = {
|
|
131965
|
+
value: readBranch(workDir),
|
|
131966
|
+
fetchedAt: now
|
|
131967
|
+
};
|
|
131968
|
+
if (branch.value === null) return null;
|
|
131969
|
+
if (now - status.fetchedAt >= STATUS_TTL_MS) status = {
|
|
131970
|
+
...readStatus(workDir),
|
|
131971
|
+
fetchedAt: now
|
|
131972
|
+
};
|
|
131973
|
+
refreshPullRequestIfNeeded(branch.value, now);
|
|
131974
|
+
return {
|
|
131975
|
+
branch: branch.value,
|
|
131976
|
+
dirty: status.dirty,
|
|
131977
|
+
ahead: status.ahead,
|
|
131978
|
+
behind: status.behind,
|
|
131979
|
+
diffAdded: status.diffAdded,
|
|
131980
|
+
diffDeleted: status.diffDeleted,
|
|
131981
|
+
pullRequest: pullRequest.branch === branch.value ? pullRequest.value : null
|
|
131982
|
+
};
|
|
131983
|
+
} };
|
|
131984
|
+
function refreshPullRequestIfNeeded(branchName, now) {
|
|
131985
|
+
if (pullRequest.pendingBranch === branchName) return;
|
|
131986
|
+
const fetchedAt = pullRequest.branch === branchName ? pullRequest.fetchedAt : 0;
|
|
131987
|
+
if (now - fetchedAt < PULL_REQUEST_TTL_MS) return;
|
|
131988
|
+
const requestId = pullRequest.requestId + 1;
|
|
131989
|
+
pullRequest = {
|
|
131990
|
+
value: pullRequest.branch === branchName ? pullRequest.value : null,
|
|
131991
|
+
branch: branchName,
|
|
131992
|
+
fetchedAt,
|
|
131993
|
+
pendingBranch: branchName,
|
|
131994
|
+
requestId
|
|
131995
|
+
};
|
|
131996
|
+
readPullRequest(workDir).then((value) => {
|
|
131997
|
+
if (pullRequest.requestId !== requestId) return;
|
|
131998
|
+
const changed = !samePullRequest(pullRequest.branch === branchName ? pullRequest.value : null, value);
|
|
131999
|
+
pullRequest = {
|
|
132000
|
+
value,
|
|
132001
|
+
branch: branchName,
|
|
132002
|
+
fetchedAt: Date.now(),
|
|
132003
|
+
pendingBranch: null,
|
|
132004
|
+
requestId
|
|
131914
132005
|
};
|
|
132006
|
+
if (changed) options.onChange?.();
|
|
131915
132007
|
});
|
|
131916
|
-
this.inner = new CombinedAutocompleteProvider(slashCommands, workDir, fdPath);
|
|
131917
132008
|
}
|
|
131918
|
-
|
|
131919
|
-
|
|
131920
|
-
|
|
131921
|
-
|
|
131922
|
-
|
|
131923
|
-
|
|
131924
|
-
|
|
131925
|
-
|
|
131926
|
-
|
|
131927
|
-
|
|
131928
|
-
|
|
131929
|
-
}
|
|
131930
|
-
|
|
131931
|
-
|
|
131932
|
-
|
|
131933
|
-
|
|
131934
|
-
|
|
131935
|
-
|
|
131936
|
-
|
|
131937
|
-
|
|
132009
|
+
}
|
|
132010
|
+
function detectGitRepo(workDir) {
|
|
132011
|
+
try {
|
|
132012
|
+
const result = spawnSync("git", [
|
|
132013
|
+
"-C",
|
|
132014
|
+
workDir,
|
|
132015
|
+
"rev-parse",
|
|
132016
|
+
"--is-inside-work-tree"
|
|
132017
|
+
], {
|
|
132018
|
+
encoding: "utf8",
|
|
132019
|
+
timeout: SPAWN_TIMEOUT_MS
|
|
132020
|
+
});
|
|
132021
|
+
return result.status === 0 && result.stdout.trim() === "true";
|
|
132022
|
+
} catch {
|
|
132023
|
+
return false;
|
|
132024
|
+
}
|
|
132025
|
+
}
|
|
132026
|
+
function readBranch(workDir) {
|
|
132027
|
+
try {
|
|
132028
|
+
const result = spawnSync("git", [
|
|
132029
|
+
"-C",
|
|
132030
|
+
workDir,
|
|
132031
|
+
"branch",
|
|
132032
|
+
"--show-current"
|
|
132033
|
+
], {
|
|
132034
|
+
encoding: "utf8",
|
|
132035
|
+
timeout: SPAWN_TIMEOUT_MS
|
|
132036
|
+
});
|
|
132037
|
+
if (result.status !== 0) return null;
|
|
132038
|
+
const name = result.stdout.trim();
|
|
132039
|
+
return name.length > 0 ? name : null;
|
|
132040
|
+
} catch {
|
|
132041
|
+
return null;
|
|
132042
|
+
}
|
|
132043
|
+
}
|
|
132044
|
+
function readStatus(workDir) {
|
|
132045
|
+
try {
|
|
132046
|
+
const result = spawnSync("git", [
|
|
132047
|
+
"-C",
|
|
132048
|
+
workDir,
|
|
132049
|
+
"status",
|
|
132050
|
+
"--porcelain",
|
|
132051
|
+
"-b"
|
|
132052
|
+
], {
|
|
132053
|
+
encoding: "utf8",
|
|
132054
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
132055
|
+
maxBuffer: 4 * 1024 * 1024
|
|
132056
|
+
});
|
|
132057
|
+
if (result.status !== 0) return {
|
|
132058
|
+
dirty: false,
|
|
132059
|
+
ahead: 0,
|
|
132060
|
+
behind: 0,
|
|
132061
|
+
diffAdded: 0,
|
|
132062
|
+
diffDeleted: 0
|
|
132063
|
+
};
|
|
132064
|
+
let dirty = false;
|
|
132065
|
+
let ahead = 0;
|
|
132066
|
+
let behind = 0;
|
|
132067
|
+
for (const line of result.stdout.split("\n")) if (line.startsWith("## ")) {
|
|
132068
|
+
const m = AHEAD_BEHIND_RE.exec(line);
|
|
132069
|
+
if (m) {
|
|
132070
|
+
ahead = Number.parseInt(m[1] ?? "0", 10) || 0;
|
|
132071
|
+
behind = Number.parseInt(m[2] ?? "0", 10) || 0;
|
|
132072
|
+
}
|
|
132073
|
+
} else if (line.trim().length > 0) dirty = true;
|
|
132074
|
+
const diff = dirty ? readDiffStats(workDir) : {
|
|
132075
|
+
added: 0,
|
|
132076
|
+
deleted: 0
|
|
132077
|
+
};
|
|
132078
|
+
return {
|
|
132079
|
+
dirty,
|
|
132080
|
+
ahead,
|
|
132081
|
+
behind,
|
|
132082
|
+
diffAdded: diff.added,
|
|
132083
|
+
diffDeleted: diff.deleted
|
|
132084
|
+
};
|
|
132085
|
+
} catch {
|
|
131938
132086
|
return {
|
|
131939
|
-
|
|
131940
|
-
|
|
132087
|
+
dirty: false,
|
|
132088
|
+
ahead: 0,
|
|
132089
|
+
behind: 0,
|
|
132090
|
+
diffAdded: 0,
|
|
132091
|
+
diffDeleted: 0
|
|
131941
132092
|
};
|
|
131942
132093
|
}
|
|
131943
|
-
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
131944
|
-
if (prefix.startsWith("/")) {
|
|
131945
|
-
const line = lines[cursorLine] ?? "";
|
|
131946
|
-
const before = line.slice(0, cursorCol - prefix.length);
|
|
131947
|
-
const after = line.slice(cursorCol);
|
|
131948
|
-
const replacement = `/${item.value} `;
|
|
131949
|
-
const newLine = `${before}${replacement}${after}`;
|
|
131950
|
-
return {
|
|
131951
|
-
lines: [
|
|
131952
|
-
...lines.slice(0, cursorLine),
|
|
131953
|
-
newLine,
|
|
131954
|
-
...lines.slice(cursorLine + 1)
|
|
131955
|
-
],
|
|
131956
|
-
cursorLine,
|
|
131957
|
-
cursorCol: before.length + replacement.length
|
|
131958
|
-
};
|
|
131959
|
-
}
|
|
131960
|
-
return this.inner.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
131961
|
-
}
|
|
131962
|
-
};
|
|
131963
|
-
/**
|
|
131964
|
-
* Return the `@…` token ending at the cursor, or `null` if we're not in
|
|
131965
|
-
* an `@` mention. Mirrors pi-tui's `extractAtPrefix` — the token
|
|
131966
|
-
* boundary is the last PATH_DELIMITER before the cursor, and the token
|
|
131967
|
-
* must start with `@`.
|
|
131968
|
-
*/
|
|
131969
|
-
function extractAtPrefix(text) {
|
|
131970
|
-
let tokenStart = 0;
|
|
131971
|
-
for (let i = text.length - 1; i >= 0; i -= 1) if (PATH_DELIMITERS.has(text[i] ?? "")) {
|
|
131972
|
-
tokenStart = i + 1;
|
|
131973
|
-
break;
|
|
131974
|
-
}
|
|
131975
|
-
if (text[tokenStart] !== "@") return null;
|
|
131976
|
-
return text.slice(tokenStart);
|
|
131977
|
-
}
|
|
131978
|
-
/** True when any path segment starts with a dot (e.g. `.github/x.yml`). */
|
|
131979
|
-
function containsDotSegment(path) {
|
|
131980
|
-
for (const segment of path.split("/")) if (segment.startsWith(".")) return true;
|
|
131981
|
-
return false;
|
|
131982
132094
|
}
|
|
131983
|
-
|
|
131984
|
-
|
|
131985
|
-
|
|
131986
|
-
|
|
131987
|
-
|
|
131988
|
-
|
|
131989
|
-
|
|
131990
|
-
|
|
131991
|
-
|
|
131992
|
-
|
|
131993
|
-
|
|
131994
|
-
|
|
131995
|
-
|
|
131996
|
-
|
|
131997
|
-
|
|
131998
|
-
|
|
131999
|
-
|
|
132000
|
-
|
|
132001
|
-
|
|
132002
|
-
|
|
132003
|
-
|
|
132004
|
-
|
|
132005
|
-
|
|
132006
|
-
|
|
132007
|
-
|
|
132008
|
-
result.push(path);
|
|
132009
|
-
}
|
|
132010
|
-
if (result.length < cap) {
|
|
132011
|
-
const byMtime = files.filter((p) => !picked.has(p) && snapshot.mtimeByPath.has(p)).toSorted((a, b) => (snapshot.mtimeByPath.get(b) ?? 0) - (snapshot.mtimeByPath.get(a) ?? 0));
|
|
132012
|
-
for (const path of byMtime) {
|
|
132013
|
-
if (result.length >= cap) break;
|
|
132014
|
-
picked.add(path);
|
|
132015
|
-
result.push(path);
|
|
132016
|
-
}
|
|
132017
|
-
}
|
|
132018
|
-
if (result.length < cap) {
|
|
132019
|
-
const rest = files.filter((p) => !picked.has(p)).toSorted((a, b) => basename(a).localeCompare(basename(b)) || a.localeCompare(b));
|
|
132020
|
-
for (const path of rest) {
|
|
132021
|
-
if (result.length >= cap) break;
|
|
132022
|
-
result.push(path);
|
|
132095
|
+
function readDiffStats(workDir) {
|
|
132096
|
+
try {
|
|
132097
|
+
const result = spawnSync("git", [
|
|
132098
|
+
"-C",
|
|
132099
|
+
workDir,
|
|
132100
|
+
"diff",
|
|
132101
|
+
"--numstat",
|
|
132102
|
+
"HEAD",
|
|
132103
|
+
"--"
|
|
132104
|
+
], {
|
|
132105
|
+
encoding: "utf8",
|
|
132106
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
132107
|
+
maxBuffer: 4 * 1024 * 1024
|
|
132108
|
+
});
|
|
132109
|
+
if (result.status !== 0) return {
|
|
132110
|
+
added: 0,
|
|
132111
|
+
deleted: 0
|
|
132112
|
+
};
|
|
132113
|
+
let added = 0;
|
|
132114
|
+
let deleted = 0;
|
|
132115
|
+
for (const line of result.stdout.split("\n")) {
|
|
132116
|
+
if (!line) continue;
|
|
132117
|
+
const [addedText, deletedText] = line.split(" ");
|
|
132118
|
+
added += parseDiffNumstatCount(addedText);
|
|
132119
|
+
deleted += parseDiffNumstatCount(deletedText);
|
|
132023
132120
|
}
|
|
132121
|
+
return {
|
|
132122
|
+
added,
|
|
132123
|
+
deleted
|
|
132124
|
+
};
|
|
132125
|
+
} catch {
|
|
132126
|
+
return {
|
|
132127
|
+
added: 0,
|
|
132128
|
+
deleted: 0
|
|
132129
|
+
};
|
|
132024
132130
|
}
|
|
132025
|
-
return result.map(toItem);
|
|
132026
132131
|
}
|
|
132027
|
-
|
|
132028
|
-
|
|
132029
|
-
|
|
132030
|
-
|
|
132031
|
-
|
|
132032
|
-
function
|
|
132033
|
-
|
|
132034
|
-
|
|
132035
|
-
|
|
132036
|
-
|
|
132037
|
-
|
|
132038
|
-
|
|
132039
|
-
|
|
132040
|
-
|
|
132041
|
-
|
|
132042
|
-
|
|
132043
|
-
|
|
132044
|
-
|
|
132045
|
-
|
|
132046
|
-
|
|
132047
|
-
|
|
132048
|
-
|
|
132049
|
-
|
|
132132
|
+
function parseDiffNumstatCount(value) {
|
|
132133
|
+
if (value === void 0 || value === "-") return 0;
|
|
132134
|
+
const n = Number.parseInt(value, 10);
|
|
132135
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
132136
|
+
}
|
|
132137
|
+
function readPullRequest(workDir) {
|
|
132138
|
+
return new Promise((resolve) => {
|
|
132139
|
+
try {
|
|
132140
|
+
execFile("gh", [
|
|
132141
|
+
"pr",
|
|
132142
|
+
"view",
|
|
132143
|
+
"--json",
|
|
132144
|
+
"number,url"
|
|
132145
|
+
], {
|
|
132146
|
+
cwd: workDir,
|
|
132147
|
+
encoding: "utf8",
|
|
132148
|
+
env: {
|
|
132149
|
+
...process.env,
|
|
132150
|
+
GH_NO_UPDATE_NOTIFIER: "1",
|
|
132151
|
+
GH_PROMPT_DISABLED: "1"
|
|
132152
|
+
},
|
|
132153
|
+
timeout: PR_SPAWN_TIMEOUT_MS,
|
|
132154
|
+
maxBuffer: 256 * 1024
|
|
132155
|
+
}, (error, stdout) => {
|
|
132156
|
+
if (error !== null) {
|
|
132157
|
+
resolve(null);
|
|
132158
|
+
return;
|
|
132159
|
+
}
|
|
132160
|
+
resolve(parsePullRequest(stdout));
|
|
132050
132161
|
});
|
|
132051
|
-
|
|
132162
|
+
} catch {
|
|
132163
|
+
resolve(null);
|
|
132052
132164
|
}
|
|
132053
|
-
const fuzzy = fuzzyMatch(query, path);
|
|
132054
|
-
if (fuzzy.matches) scored.push({
|
|
132055
|
-
path,
|
|
132056
|
-
cat: 2,
|
|
132057
|
-
fuzzyScore: fuzzy.score
|
|
132058
|
-
});
|
|
132059
|
-
}
|
|
132060
|
-
if (scored.length === 0) return fuzzyFilter([...files], query, (p) => p).slice(0, MAX_SUGGESTIONS_WHEN_QUERY).map(toItem);
|
|
132061
|
-
scored.sort((a, b) => {
|
|
132062
|
-
if (a.cat !== b.cat) return a.cat - b.cat;
|
|
132063
|
-
if (a.cat === 2 && a.fuzzyScore !== b.fuzzyScore) return a.fuzzyScore - b.fuzzyScore;
|
|
132064
|
-
const ra = snapshot.recencyOrder.get(a.path);
|
|
132065
|
-
const rb = snapshot.recencyOrder.get(b.path);
|
|
132066
|
-
if (ra !== void 0 && rb !== void 0 && ra !== rb) return ra - rb;
|
|
132067
|
-
if (ra !== void 0 && rb === void 0) return -1;
|
|
132068
|
-
if (ra === void 0 && rb !== void 0) return 1;
|
|
132069
|
-
const ma = snapshot.mtimeByPath.get(a.path) ?? 0;
|
|
132070
|
-
const mb = snapshot.mtimeByPath.get(b.path) ?? 0;
|
|
132071
|
-
if (ma !== mb) return mb - ma;
|
|
132072
|
-
const baseLenDiff = basename(a.path).length - basename(b.path).length;
|
|
132073
|
-
if (baseLenDiff !== 0) return baseLenDiff;
|
|
132074
|
-
return a.path.localeCompare(b.path);
|
|
132075
132165
|
});
|
|
132076
|
-
return scored.slice(0, MAX_SUGGESTIONS_WHEN_QUERY).map((entry) => toItem(entry.path));
|
|
132077
132166
|
}
|
|
132078
|
-
function
|
|
132079
|
-
return
|
|
132080
|
-
|
|
132081
|
-
label: basename(path),
|
|
132082
|
-
description: path
|
|
132083
|
-
};
|
|
132167
|
+
function samePullRequest(a, b) {
|
|
132168
|
+
if (a === null || b === null) return a === b;
|
|
132169
|
+
return a.number === b.number && a.url === b.url;
|
|
132084
132170
|
}
|
|
132085
|
-
|
|
132086
|
-
|
|
132087
|
-
|
|
132088
|
-
|
|
132089
|
-
|
|
132090
|
-
|
|
132091
|
-
|
|
132092
|
-
|
|
132093
|
-
|
|
132094
|
-
|
|
132095
|
-
|
|
132096
|
-
|
|
132097
|
-
|
|
132098
|
-
|
|
132099
|
-
|
|
132100
|
-
this.promptText = new Text(chalk.hex(colors.text)(prompt), 0, 0);
|
|
132171
|
+
function parsePullRequest(stdout) {
|
|
132172
|
+
try {
|
|
132173
|
+
const raw = JSON.parse(stdout);
|
|
132174
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
132175
|
+
const record = raw;
|
|
132176
|
+
const number = record["number"];
|
|
132177
|
+
const url = record["url"];
|
|
132178
|
+
if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) return null;
|
|
132179
|
+
if (typeof url !== "string" || !isSafeHttpUrl(url)) return null;
|
|
132180
|
+
return {
|
|
132181
|
+
number,
|
|
132182
|
+
url
|
|
132183
|
+
};
|
|
132184
|
+
} catch {
|
|
132185
|
+
return null;
|
|
132101
132186
|
}
|
|
132102
|
-
|
|
132103
|
-
|
|
132187
|
+
}
|
|
132188
|
+
function isSafeHttpUrl(value) {
|
|
132189
|
+
if (hasControlChars(value)) return false;
|
|
132190
|
+
try {
|
|
132191
|
+
const url = new URL(value);
|
|
132192
|
+
return url.protocol === "https:" || url.protocol === "http:";
|
|
132193
|
+
} catch {
|
|
132194
|
+
return false;
|
|
132104
132195
|
}
|
|
132105
|
-
|
|
132106
|
-
|
|
132107
|
-
|
|
132108
|
-
const
|
|
132109
|
-
|
|
132110
|
-
for (const line of this.spacer.render(width)) lines.push(line);
|
|
132111
|
-
const title = chalk.hex(this.titleColor).bold(this.title);
|
|
132112
|
-
lines.push(`${bullet}${title}`);
|
|
132113
|
-
if (this.detail !== void 0) lines.push(`${" ".repeat(bulletWidth)}${chalk.hex(this.colors.textDim)(this.detail)}`);
|
|
132114
|
-
const promptLines = this.promptText.render(contentWidth);
|
|
132115
|
-
for (const line of promptLines) lines.push(`${" ".repeat(bulletWidth)}${line}`);
|
|
132116
|
-
return lines;
|
|
132196
|
+
}
|
|
132197
|
+
function hasControlChars(value) {
|
|
132198
|
+
for (const char of value) {
|
|
132199
|
+
const code = char.codePointAt(0) ?? 0;
|
|
132200
|
+
if (code <= 31 || code === 127) return true;
|
|
132117
132201
|
}
|
|
132118
|
-
|
|
132119
|
-
|
|
132202
|
+
return false;
|
|
132203
|
+
}
|
|
132204
|
+
function formatGitBadgeBase(status) {
|
|
132120
132205
|
const parts = [];
|
|
132121
|
-
|
|
132122
|
-
if (
|
|
132123
|
-
|
|
132124
|
-
if (
|
|
132125
|
-
if (
|
|
132126
|
-
if (
|
|
132127
|
-
return parts.length
|
|
132206
|
+
const diff = formatDiffStats(status);
|
|
132207
|
+
if (diff) parts.push(diff);
|
|
132208
|
+
let sync = "";
|
|
132209
|
+
if (status.ahead > 0) sync += `↑${status.ahead}`;
|
|
132210
|
+
if (status.behind > 0) sync += `↓${status.behind}`;
|
|
132211
|
+
if (sync) parts.push(sync);
|
|
132212
|
+
return parts.length === 0 ? status.branch : `${status.branch} [${parts.join(" ")}]`;
|
|
132213
|
+
}
|
|
132214
|
+
function formatPullRequestBadge(pullRequest, options = {}) {
|
|
132215
|
+
const prText = `[PR#${String(pullRequest.number)}]`;
|
|
132216
|
+
return options.linkPullRequest ? toTerminalHyperlink(prText, pullRequest.url) : prText;
|
|
132217
|
+
}
|
|
132218
|
+
function formatDiffStats(status) {
|
|
132219
|
+
const parts = [];
|
|
132220
|
+
if (status.diffAdded > 0) parts.push(`+${String(status.diffAdded)}`);
|
|
132221
|
+
if (status.diffDeleted > 0) parts.push(`-${String(status.diffDeleted)}`);
|
|
132222
|
+
if (parts.length > 0) return parts.join(" ");
|
|
132223
|
+
return status.dirty ? "±" : null;
|
|
132224
|
+
}
|
|
132225
|
+
function toTerminalHyperlink(text, url) {
|
|
132226
|
+
if (!isSafeHttpUrl(url)) return text;
|
|
132227
|
+
return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`;
|
|
132128
132228
|
}
|
|
132129
132229
|
//#endregion
|
|
132130
|
-
//#region src/tui/components/
|
|
132131
|
-
|
|
132132
|
-
|
|
132133
|
-
|
|
132134
|
-
|
|
132135
|
-
|
|
132136
|
-
|
|
132137
|
-
|
|
132138
|
-
|
|
132139
|
-
|
|
132140
|
-
|
|
132141
|
-
|
|
132142
|
-
|
|
132143
|
-
|
|
132144
|
-
|
|
132145
|
-
|
|
132146
|
-
|
|
132147
|
-
|
|
132148
|
-
|
|
132149
|
-
|
|
132150
|
-
|
|
132151
|
-
|
|
132152
|
-
|
|
132230
|
+
//#region src/tui/components/chrome/footer.ts
|
|
132231
|
+
const MAX_CWD_SEGMENTS = 3;
|
|
132232
|
+
const TOOLBAR_TIPS = [
|
|
132233
|
+
{ text: "shift+tab: 计划模式" },
|
|
132234
|
+
{ text: "/model: 切换模型" },
|
|
132235
|
+
{
|
|
132236
|
+
text: "ctrl+s: 中途干预",
|
|
132237
|
+
priority: 2
|
|
132238
|
+
},
|
|
132239
|
+
{
|
|
132240
|
+
text: "/compact: 压缩上下文",
|
|
132241
|
+
priority: 2
|
|
132242
|
+
},
|
|
132243
|
+
{ text: "ctrl+o: 展开工具输出" },
|
|
132244
|
+
{ text: "/tasks: 后台任务" },
|
|
132245
|
+
{ text: "shift+enter: 换行" },
|
|
132246
|
+
{
|
|
132247
|
+
text: "/init: 生成 AGENTS.md",
|
|
132248
|
+
priority: 2
|
|
132249
|
+
},
|
|
132250
|
+
{ text: "@: 提及文件" },
|
|
132251
|
+
{ text: "ctrl+c: 取消" },
|
|
132252
|
+
{ text: "/theme: 切换主题" },
|
|
132253
|
+
{ text: "/auto: 自动权限模式" },
|
|
132254
|
+
{ text: "/yes: 自动批准" },
|
|
132255
|
+
{ text: "/help: 显示命令" },
|
|
132256
|
+
{
|
|
132257
|
+
text: "/config: 选择并配置你常用的模型商",
|
|
132258
|
+
solo: true,
|
|
132259
|
+
priority: 3
|
|
132260
|
+
},
|
|
132261
|
+
{
|
|
132262
|
+
text: "让 Scream 安排任务,例如 \"2个小时后提醒我去拿快递\"",
|
|
132263
|
+
solo: true,
|
|
132264
|
+
priority: 3
|
|
132153
132265
|
}
|
|
132154
|
-
|
|
132155
|
-
|
|
132156
|
-
|
|
132157
|
-
|
|
132158
|
-
|
|
132159
|
-
|
|
132160
|
-
|
|
132161
|
-
|
|
132162
|
-
|
|
132163
|
-
|
|
132164
|
-
|
|
132165
|
-
|
|
132266
|
+
];
|
|
132267
|
+
/**
|
|
132268
|
+
* Expand tips into a rotation sequence using smooth weighted round-robin
|
|
132269
|
+
* (the nginx SWRR algorithm). Higher-`priority` tips appear more often while
|
|
132270
|
+
* staying evenly spread, so a tip generally does not land next to its own
|
|
132271
|
+
* duplicate. Deterministic and computed once at module load. Exported for
|
|
132272
|
+
* unit testing.
|
|
132273
|
+
*/
|
|
132274
|
+
function buildWeightedTips(tips) {
|
|
132275
|
+
const items = tips.map((t) => ({
|
|
132276
|
+
tip: t,
|
|
132277
|
+
weight: Math.max(1, Math.trunc(t.priority ?? 1)),
|
|
132278
|
+
current: 0
|
|
132279
|
+
}));
|
|
132280
|
+
const total = items.reduce((sum, it) => sum + it.weight, 0);
|
|
132281
|
+
const seq = [];
|
|
132282
|
+
for (let n = 0; n < total; n++) {
|
|
132283
|
+
let best = items[0];
|
|
132284
|
+
for (const it of items) {
|
|
132285
|
+
it.current += it.weight;
|
|
132286
|
+
if (it.current > best.current) best = it;
|
|
132166
132287
|
}
|
|
132288
|
+
best.current -= total;
|
|
132289
|
+
seq.push(best.tip);
|
|
132167
132290
|
}
|
|
132168
|
-
|
|
132169
|
-
|
|
132170
|
-
|
|
132171
|
-
|
|
132172
|
-
|
|
132173
|
-
|
|
132174
|
-
|
|
132175
|
-
|
|
132176
|
-
|
|
132177
|
-
|
|
132178
|
-
|
|
132179
|
-
|
|
132180
|
-
|
|
132181
|
-
|
|
132182
|
-
|
|
132183
|
-
|
|
132184
|
-
|
|
132185
|
-
|
|
132186
|
-
|
|
132187
|
-
|
|
132188
|
-
|
|
132189
|
-
|
|
132190
|
-
|
|
132191
|
-
|
|
132192
|
-
|
|
132193
|
-
|
|
132194
|
-
|
|
132195
|
-
|
|
132196
|
-
|
|
132197
|
-
|
|
132198
|
-
|
|
132199
|
-
|
|
132200
|
-
|
|
132201
|
-
}
|
|
132202
|
-
|
|
132203
|
-
|
|
132204
|
-
|
|
132205
|
-
|
|
132206
|
-
|
|
132207
|
-
|
|
132208
|
-
|
|
132209
|
-
|
|
132210
|
-
|
|
132211
|
-
|
|
132291
|
+
return seq;
|
|
132292
|
+
}
|
|
132293
|
+
buildWeightedTips(TOOLBAR_TIPS);
|
|
132294
|
+
function shortenModel(model) {
|
|
132295
|
+
if (!model) return model;
|
|
132296
|
+
const slash = model.lastIndexOf("/");
|
|
132297
|
+
return slash >= 0 ? model.slice(slash + 1) : model;
|
|
132298
|
+
}
|
|
132299
|
+
function modelDisplayName(state) {
|
|
132300
|
+
const model = state.availableModels[state.model];
|
|
132301
|
+
return model?.displayName ?? model?.model ?? state.model;
|
|
132302
|
+
}
|
|
132303
|
+
function shortenCwd(path) {
|
|
132304
|
+
if (!path) return path;
|
|
132305
|
+
const home = process.env["HOME"] ?? "";
|
|
132306
|
+
let work = path;
|
|
132307
|
+
if (home && path === home) return "~";
|
|
132308
|
+
if (home && path.startsWith(home + "/")) work = "~" + path.slice(home.length);
|
|
132309
|
+
const segments = work.split("/").filter((s) => s.length > 0);
|
|
132310
|
+
if (segments.length <= MAX_CWD_SEGMENTS) return work;
|
|
132311
|
+
return `…/${segments.slice(-3).join("/")}`;
|
|
132312
|
+
}
|
|
132313
|
+
function formatTokenCount(n) {
|
|
132314
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
132315
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
132316
|
+
return String(n);
|
|
132317
|
+
}
|
|
132318
|
+
function safeUsage(usage) {
|
|
132319
|
+
return safeUsageRatio(usage);
|
|
132320
|
+
}
|
|
132321
|
+
function formatContextStatus(usage, tokens, maxTokens) {
|
|
132322
|
+
const pct = `${(safeUsage(usage) * 100).toFixed(1)}%`;
|
|
132323
|
+
if (maxTokens && maxTokens > 0 && tokens !== void 0) return `上下文:${pct} (${formatTokenCount(tokens)}/${formatTokenCount(maxTokens)})`;
|
|
132324
|
+
return `上下文:${pct}`;
|
|
132325
|
+
}
|
|
132326
|
+
const BRAND_COLORS = [
|
|
132327
|
+
"#72A4E9",
|
|
132328
|
+
"#A78BFA",
|
|
132329
|
+
"#34D399"
|
|
132330
|
+
];
|
|
132331
|
+
const GRADIENT_CYCLE_MS = 4e3;
|
|
132332
|
+
const SPINNER_FRAMES = [
|
|
132333
|
+
"●",
|
|
132334
|
+
"◉",
|
|
132335
|
+
"◎",
|
|
132336
|
+
"◌",
|
|
132337
|
+
"○",
|
|
132338
|
+
"◌",
|
|
132339
|
+
"◎",
|
|
132340
|
+
"◉"
|
|
132341
|
+
];
|
|
132342
|
+
const SPINNER_TICK_MS = 120;
|
|
132343
|
+
function hexToRgb(hex) {
|
|
132344
|
+
const v = parseInt(hex.slice(1), 16);
|
|
132345
|
+
return [
|
|
132346
|
+
v >> 16 & 255,
|
|
132347
|
+
v >> 8 & 255,
|
|
132348
|
+
v & 255
|
|
132349
|
+
];
|
|
132350
|
+
}
|
|
132351
|
+
function lerpGradient(t) {
|
|
132352
|
+
const count = BRAND_COLORS.length;
|
|
132353
|
+
const segment = Math.min(t * count, count - 1);
|
|
132354
|
+
const idx = Math.floor(segment);
|
|
132355
|
+
const localT = segment - idx;
|
|
132356
|
+
const nextIdx = (idx + 1) % count;
|
|
132357
|
+
const [r0, g0, b0] = hexToRgb(BRAND_COLORS[idx]);
|
|
132358
|
+
const [r1, g1, b1] = hexToRgb(BRAND_COLORS[nextIdx]);
|
|
132359
|
+
const r = Math.round(r0 + (r1 - r0) * localT);
|
|
132360
|
+
const g = Math.round(g0 + (g1 - g0) * localT);
|
|
132361
|
+
const b = Math.round(b0 + (b1 - b0) * localT);
|
|
132362
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
132363
|
+
}
|
|
132364
|
+
function buildStatusLine(streamingPhase, livePaneMode, streamingStartTime) {
|
|
132365
|
+
if (streamingPhase === "idle" && livePaneMode !== "tool") return "○ 空闲";
|
|
132366
|
+
let label;
|
|
132367
|
+
if (livePaneMode === "tool") label = "执行中";
|
|
132368
|
+
else if (streamingPhase === "waiting") label = "等待响应";
|
|
132369
|
+
else if (streamingPhase === "thinking") label = "思考中";
|
|
132370
|
+
else if (streamingPhase === "composing") label = "输出中";
|
|
132371
|
+
else label = "";
|
|
132372
|
+
const elapsed = Date.now() - streamingStartTime;
|
|
132373
|
+
const totalSeconds = Math.floor(elapsed / 1e3);
|
|
132374
|
+
const elapsedStr = totalSeconds < 60 ? `${totalSeconds}s` : `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
|
|
132375
|
+
const now = Date.now();
|
|
132376
|
+
const frame = SPINNER_FRAMES[Math.floor(now / SPINNER_TICK_MS) % SPINNER_FRAMES.length];
|
|
132377
|
+
const gradientColor = lerpGradient(now % GRADIENT_CYCLE_MS / GRADIENT_CYCLE_MS);
|
|
132378
|
+
return chalk.hex(gradientColor).bold(frame) + " " + label + " " + elapsedStr;
|
|
132379
|
+
}
|
|
132380
|
+
function formatFooterGitBadge(status, colors) {
|
|
132381
|
+
const base = chalk.hex(colors.status)(formatGitBadgeBase(status));
|
|
132382
|
+
if (status.pullRequest === null) return base;
|
|
132383
|
+
return `${base} ${chalk.hex(colors.primary)(formatPullRequestBadge(status.pullRequest, { linkPullRequest: true }))}`;
|
|
132384
|
+
}
|
|
132385
|
+
var FooterComponent = class {
|
|
132386
|
+
state;
|
|
132387
|
+
colors;
|
|
132388
|
+
onGitStatusChange;
|
|
132389
|
+
gitCache;
|
|
132390
|
+
gitCacheWorkDir;
|
|
132391
|
+
transientHint = null;
|
|
132392
|
+
/**
|
|
132393
|
+
* Non-terminal background-task counts split by kind so the footer can
|
|
132394
|
+
* render two distinct badges. `bashTasks` covers `bash-*` BPM tasks
|
|
132395
|
+
* spawned via `Shell run_in_background=true`; `agentTasks` covers
|
|
132396
|
+
* `agent-*` BPM tasks (background subagents). Either zero hides its
|
|
132397
|
+
* respective badge.
|
|
132398
|
+
*/
|
|
132399
|
+
backgroundBashTaskCount = 0;
|
|
132400
|
+
backgroundAgentCount = 0;
|
|
132401
|
+
constructor(state, colors, onGitStatusChange = () => {}) {
|
|
132402
|
+
this.state = state;
|
|
132403
|
+
this.colors = colors;
|
|
132404
|
+
this.onGitStatusChange = onGitStatusChange;
|
|
132405
|
+
this.gitCacheWorkDir = state.workDir;
|
|
132406
|
+
this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
|
|
132212
132407
|
}
|
|
132213
|
-
|
|
132214
|
-
|
|
132215
|
-
|
|
132216
|
-
this.
|
|
132217
|
-
return;
|
|
132408
|
+
setState(state) {
|
|
132409
|
+
if (state.workDir !== this.gitCacheWorkDir) {
|
|
132410
|
+
this.gitCacheWorkDir = state.workDir;
|
|
132411
|
+
this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
|
|
132218
132412
|
}
|
|
132219
|
-
this.
|
|
132220
|
-
this.uiHooks?.showPanel(next.payload);
|
|
132413
|
+
this.state = state;
|
|
132221
132414
|
}
|
|
132222
|
-
|
|
132223
|
-
|
|
132224
|
-
for (const entry of this.queue) {
|
|
132225
|
-
const auto = this.autoResolveFor(resolvedPayload, response, entry.payload);
|
|
132226
|
-
if (auto === void 0) remaining.push(entry);
|
|
132227
|
-
else entry.resolve(auto);
|
|
132228
|
-
}
|
|
132229
|
-
this.queue = remaining;
|
|
132415
|
+
setColors(colors) {
|
|
132416
|
+
this.colors = colors;
|
|
132230
132417
|
}
|
|
132231
132418
|
/**
|
|
132232
|
-
*
|
|
132233
|
-
*
|
|
132234
|
-
*
|
|
132235
|
-
*
|
|
132419
|
+
* Short-lived hint that replaces the rotating toolbar tips on line 1.
|
|
132420
|
+
* Used by the exit-confirmation double-tap flow to show "Press Ctrl+C
|
|
132421
|
+
* again to exit" without requiring a toast/overlay subsystem.
|
|
132422
|
+
* Pass `null` to clear.
|
|
132236
132423
|
*/
|
|
132237
|
-
|
|
132238
|
-
|
|
132239
|
-
//#endregion
|
|
132240
|
-
//#region src/tui/reverse-rpc/approval/controller.ts
|
|
132241
|
-
var ApprovalController = class extends ReverseRpcController {
|
|
132242
|
-
createCancelResponse(reason) {
|
|
132243
|
-
return {
|
|
132244
|
-
decision: "cancelled",
|
|
132245
|
-
feedback: reason
|
|
132246
|
-
};
|
|
132247
|
-
}
|
|
132248
|
-
autoResolveFor(resolvedPayload, response, queuedPayload) {
|
|
132249
|
-
if (response.decision !== "approved") return void 0;
|
|
132250
|
-
if (response.scope !== "session") return void 0;
|
|
132251
|
-
if (resolvedPayload.action !== queuedPayload.action) return void 0;
|
|
132252
|
-
return {
|
|
132253
|
-
decision: "approved",
|
|
132254
|
-
scope: "session"
|
|
132255
|
-
};
|
|
132256
|
-
}
|
|
132257
|
-
};
|
|
132258
|
-
//#endregion
|
|
132259
|
-
//#region src/tui/reverse-rpc/modal-coordinator.ts
|
|
132260
|
-
var ReverseRpcModalCoordinator = class {
|
|
132261
|
-
hooks;
|
|
132262
|
-
active = null;
|
|
132263
|
-
queued = [];
|
|
132264
|
-
constructor(hooks) {
|
|
132265
|
-
this.hooks = hooks;
|
|
132266
|
-
}
|
|
132267
|
-
showApproval(payload) {
|
|
132268
|
-
this.show({
|
|
132269
|
-
owner: "approval",
|
|
132270
|
-
show: () => {
|
|
132271
|
-
this.hooks.showApprovalPanel(payload);
|
|
132272
|
-
},
|
|
132273
|
-
hide: () => {
|
|
132274
|
-
this.hooks.hideApprovalPanel();
|
|
132275
|
-
}
|
|
132276
|
-
});
|
|
132277
|
-
}
|
|
132278
|
-
showQuestion(payload) {
|
|
132279
|
-
this.show({
|
|
132280
|
-
owner: "question",
|
|
132281
|
-
show: () => {
|
|
132282
|
-
this.hooks.showQuestionDialog(payload);
|
|
132283
|
-
},
|
|
132284
|
-
hide: () => {
|
|
132285
|
-
this.hooks.hideQuestionDialog();
|
|
132286
|
-
}
|
|
132287
|
-
});
|
|
132288
|
-
}
|
|
132289
|
-
hide(owner) {
|
|
132290
|
-
if (this.active?.owner === owner) {
|
|
132291
|
-
const active = this.active;
|
|
132292
|
-
this.active = null;
|
|
132293
|
-
active.hide();
|
|
132294
|
-
this.showNext();
|
|
132295
|
-
return;
|
|
132296
|
-
}
|
|
132297
|
-
const queuedIndex = this.queued.findIndex((entry) => entry.owner === owner);
|
|
132298
|
-
if (queuedIndex >= 0) this.queued.splice(queuedIndex, 1);
|
|
132424
|
+
setTransientHint(hint) {
|
|
132425
|
+
this.transientHint = hint;
|
|
132299
132426
|
}
|
|
132300
|
-
|
|
132301
|
-
|
|
132302
|
-
|
|
132303
|
-
|
|
132304
|
-
|
|
132427
|
+
/**
|
|
132428
|
+
* Sync both background-task badges with live counts. Each non-zero
|
|
132429
|
+
* count produces its own bracketed badge on line 1; zeros hide them
|
|
132430
|
+
* independently.
|
|
132431
|
+
*/
|
|
132432
|
+
setBackgroundCounts(counts) {
|
|
132433
|
+
this.backgroundBashTaskCount = Math.max(0, counts.bashTasks);
|
|
132434
|
+
this.backgroundAgentCount = Math.max(0, counts.agentTasks);
|
|
132305
132435
|
}
|
|
132306
|
-
|
|
132307
|
-
|
|
132308
|
-
|
|
132309
|
-
|
|
132310
|
-
|
|
132311
|
-
|
|
132312
|
-
|
|
132313
|
-
if (
|
|
132314
|
-
|
|
132315
|
-
|
|
132316
|
-
|
|
132436
|
+
invalidate() {}
|
|
132437
|
+
render(width) {
|
|
132438
|
+
const colors = this.colors;
|
|
132439
|
+
const state = this.state;
|
|
132440
|
+
const left = [];
|
|
132441
|
+
if (state.permissionMode === "auto") left.push(chalk.hex(colors.warning).bold("auto"));
|
|
132442
|
+
if (state.permissionMode === "yolo") left.push(chalk.hex(colors.warning).bold("YES"));
|
|
132443
|
+
if (state.planMode) left.push(chalk.hex(colors.primary).bold("plan"));
|
|
132444
|
+
if (state.wolfpackMode) left.push(chalk.hex(colors.primary).bold("wolfpack"));
|
|
132445
|
+
if (state.goalActive) left.push(chalk.hex(colors.primary).bold("goal"));
|
|
132446
|
+
const model = shortenModel(modelDisplayName(state));
|
|
132447
|
+
if (model) {
|
|
132448
|
+
const thinkingLabel = state.thinking ? " 思考中" : "";
|
|
132449
|
+
left.push(chalk.hex(colors.text)(`${model}${thinkingLabel}`));
|
|
132317
132450
|
}
|
|
132318
|
-
|
|
132319
|
-
|
|
132320
|
-
this.
|
|
132321
|
-
return;
|
|
132451
|
+
if (this.backgroundBashTaskCount > 0) {
|
|
132452
|
+
const noun = this.backgroundBashTaskCount === 1 ? "个任务" : "个任务";
|
|
132453
|
+
left.push(chalk.hex(colors.primary)(`[${String(this.backgroundBashTaskCount)}${noun} 运行中]`));
|
|
132322
132454
|
}
|
|
132323
|
-
this.
|
|
132324
|
-
|
|
132325
|
-
|
|
132326
|
-
const next = this.queued.shift();
|
|
132327
|
-
if (next === void 0) return;
|
|
132328
|
-
this.active = next;
|
|
132329
|
-
next.show();
|
|
132330
|
-
}
|
|
132331
|
-
};
|
|
132332
|
-
//#endregion
|
|
132333
|
-
//#region src/tui/reverse-rpc/index.ts
|
|
132334
|
-
function registerReverseRPCHandlers(approvalController, questionController, uiHooks) {
|
|
132335
|
-
const modalCoordinator = new ReverseRpcModalCoordinator(uiHooks);
|
|
132336
|
-
approvalController.setUIHooks({
|
|
132337
|
-
showPanel: (payload) => {
|
|
132338
|
-
modalCoordinator.showApproval(payload);
|
|
132339
|
-
},
|
|
132340
|
-
hidePanel: () => {
|
|
132341
|
-
modalCoordinator.hide("approval");
|
|
132455
|
+
if (this.backgroundAgentCount > 0) {
|
|
132456
|
+
const noun = this.backgroundAgentCount === 1 ? "个代理" : "个代理";
|
|
132457
|
+
left.push(chalk.hex(colors.primary)(`[${String(this.backgroundAgentCount)}${noun} 运行中]`));
|
|
132342
132458
|
}
|
|
132343
|
-
|
|
132344
|
-
|
|
132345
|
-
|
|
132346
|
-
|
|
132347
|
-
|
|
132348
|
-
|
|
132349
|
-
|
|
132459
|
+
const cwd = shortenCwd(state.workDir);
|
|
132460
|
+
if (cwd) left.push(chalk.hex(colors.status)(cwd));
|
|
132461
|
+
const git = this.gitCache.getStatus();
|
|
132462
|
+
if (git !== null) left.push(formatFooterGitBadge(git, colors));
|
|
132463
|
+
const leftLine = left.join(" ");
|
|
132464
|
+
const leftWidth = visibleWidth(leftLine);
|
|
132465
|
+
let rightText;
|
|
132466
|
+
if (this.transientHint) rightText = chalk.hex(colors.warning).bold(this.transientHint);
|
|
132467
|
+
else {
|
|
132468
|
+
const statusLine = buildStatusLine(state.streamingPhase, state.livePaneMode, state.streamingStartTime);
|
|
132469
|
+
const ccDot = state.ccConnectActive ? chalk.hex(colors.success)("●") : chalk.hex(colors.textDim)("●");
|
|
132470
|
+
rightText = chalk.hex(colors.textDim)(ccDot + " " + formatContextStatus(state.contextUsage, state.contextTokens, state.maxContextTokens) + " " + statusLine);
|
|
132350
132471
|
}
|
|
132351
|
-
|
|
132352
|
-
|
|
132353
|
-
|
|
132354
|
-
|
|
132355
|
-
|
|
132356
|
-
|
|
132357
|
-
|
|
132358
|
-
|
|
132359
|
-
|
|
132360
|
-
return { answers: [] };
|
|
132472
|
+
const rightWidth = visibleWidth(rightText);
|
|
132473
|
+
const gap = 3;
|
|
132474
|
+
let line1;
|
|
132475
|
+
if (leftWidth + gap + rightWidth <= width) {
|
|
132476
|
+
const pad = width - leftWidth - rightWidth;
|
|
132477
|
+
line1 = leftLine + " ".repeat(pad) + rightText;
|
|
132478
|
+
} else if (leftWidth <= width) line1 = leftLine;
|
|
132479
|
+
else line1 = truncateToWidth(leftLine, width, "…");
|
|
132480
|
+
return [truncateToWidth(line1, width)];
|
|
132361
132481
|
}
|
|
132362
132482
|
};
|
|
132363
132483
|
//#endregion
|
|
132364
|
-
//#region src/tui/theme/bundle.ts
|
|
132365
|
-
function createScreamTUIThemeBundle(theme, resolvedTheme) {
|
|
132366
|
-
const actualTheme = resolvedTheme ?? resolveThemeSync(theme);
|
|
132367
|
-
const colors = { ...getColorPalette(actualTheme) };
|
|
132368
|
-
return {
|
|
132369
|
-
resolvedTheme: actualTheme,
|
|
132370
|
-
colors,
|
|
132371
|
-
styles: createThemeStyles(colors),
|
|
132372
|
-
markdownTheme: createMarkdownTheme(colors)
|
|
132373
|
-
};
|
|
132374
|
-
}
|
|
132375
|
-
//#endregion
|
|
132376
|
-
//#region src/tui/types.ts
|
|
132377
|
-
const INITIAL_LIVE_PANE = {
|
|
132378
|
-
mode: "idle",
|
|
132379
|
-
pendingApproval: null,
|
|
132380
|
-
pendingQuestion: null
|
|
132381
|
-
};
|
|
132382
|
-
//#endregion
|
|
132383
132484
|
//#region src/tui/components/chrome/todo-panel.ts
|
|
132384
132485
|
const MAX_VISIBLE = 5;
|
|
132385
132486
|
/**
|
|
@@ -136147,27 +136248,6 @@ var ScreamTUI = class ScreamTUI {
|
|
|
136147
136248
|
const footerWrap = new GutterContainer(1, 1);
|
|
136148
136249
|
footerWrap.addChild(this.state.footer);
|
|
136149
136250
|
this.state.ui.addChild(footerWrap);
|
|
136150
|
-
this.installFixedBottomRegion();
|
|
136151
|
-
}
|
|
136152
|
-
/**
|
|
136153
|
-
* Pin the editor + footer to the terminal bottom. The pi-tui patch adds a
|
|
136154
|
-
* `fixedBottomLineCount` property: the last N rendered lines stay pinned
|
|
136155
|
-
* while the transcript above scrolls independently.
|
|
136156
|
-
*
|
|
136157
|
-
* We override `doRender` to measure the editor + footer height each frame
|
|
136158
|
-
* (they change with multi-line input or terminal resize) and set the count
|
|
136159
|
-
* before the real render runs.
|
|
136160
|
-
*/
|
|
136161
|
-
installFixedBottomRegion() {
|
|
136162
|
-
const ui = this.state.ui;
|
|
136163
|
-
const originalDoRender = ui.doRender.bind(ui);
|
|
136164
|
-
const editorContainer = this.state.editorContainer;
|
|
136165
|
-
const footerWrap = ui.children.at(-1);
|
|
136166
|
-
ui.doRender = () => {
|
|
136167
|
-
const w = ui.terminal?.columns ?? 0;
|
|
136168
|
-
if (w > 0) ui.fixedBottomLineCount = editorContainer.render(w).length + (footerWrap ? footerWrap.render(w).length : 0);
|
|
136169
|
-
originalDoRender();
|
|
136170
|
-
};
|
|
136171
136251
|
}
|
|
136172
136252
|
handlePlanToggle(next) {
|
|
136173
136253
|
handlePlanCommand(this, next ? "on" : "off");
|
|
@@ -136598,7 +136678,7 @@ var ScreamTUI = class ScreamTUI {
|
|
|
136598
136678
|
updateActivityPane() {
|
|
136599
136679
|
const effectiveMode = this.resolveActivityPaneMode();
|
|
136600
136680
|
this.syncTerminalProgress(this.shouldShowTerminalProgress(effectiveMode));
|
|
136601
|
-
if (effectiveMode === this.lastActivityMode
|
|
136681
|
+
if (effectiveMode === this.lastActivityMode) return;
|
|
136602
136682
|
this.lastActivityMode = effectiveMode;
|
|
136603
136683
|
this.state.activityContainer.clear();
|
|
136604
136684
|
switch (effectiveMode) {
|
|
@@ -136702,6 +136782,7 @@ var ScreamTUI = class ScreamTUI {
|
|
|
136702
136782
|
this.state.theme.markdownTheme = nextTheme.markdownTheme;
|
|
136703
136783
|
this.setAppState({ theme });
|
|
136704
136784
|
this.updateEditorBorderHighlight();
|
|
136785
|
+
for (const child of this.state.transcriptContainer.children) child.invalidate?.();
|
|
136705
136786
|
this.state.ui.requestRender(true);
|
|
136706
136787
|
}
|
|
136707
136788
|
refreshTerminalThemeTracking() {
|