scream-code 0.5.2 → 0.5.3-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.mjs +1600 -1649
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -122350,605 +122350,19 @@ function dismissGoalPanel(host) {
|
|
|
122350
122350
|
}
|
|
122351
122351
|
}
|
|
122352
122352
|
//#endregion
|
|
122353
|
-
//#region src/utils/git/git-status.ts
|
|
122354
|
-
/**
|
|
122355
|
-
* Cached git branch + working-tree status for the footer/statusline.
|
|
122356
|
-
*
|
|
122357
|
-
* Branch name refreshes every 5s, porcelain status every 15s. Branch
|
|
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.
|
|
122361
|
-
*/
|
|
122362
|
-
const BRANCH_TTL_MS = 5e3;
|
|
122363
|
-
const STATUS_TTL_MS = 15e3;
|
|
122364
|
-
const PULL_REQUEST_TTL_MS = 6e4;
|
|
122365
|
-
const SPAWN_TIMEOUT_MS = 500;
|
|
122366
|
-
const PR_SPAWN_TIMEOUT_MS = 5e3;
|
|
122367
|
-
const AHEAD_BEHIND_RE = /\[(?:ahead (\d+))?(?:, )?(?:behind (\d+))?\]/;
|
|
122368
|
-
function createGitStatusCache(workDir, options = {}) {
|
|
122369
|
-
const isRepo = detectGitRepo(workDir);
|
|
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
|
-
});
|
|
122436
|
-
}
|
|
122437
|
-
}
|
|
122438
|
-
function detectGitRepo(workDir) {
|
|
122439
|
-
try {
|
|
122440
|
-
const result = spawnSync("git", [
|
|
122441
|
-
"-C",
|
|
122442
|
-
workDir,
|
|
122443
|
-
"rev-parse",
|
|
122444
|
-
"--is-inside-work-tree"
|
|
122445
|
-
], {
|
|
122446
|
-
encoding: "utf8",
|
|
122447
|
-
timeout: SPAWN_TIMEOUT_MS
|
|
122448
|
-
});
|
|
122449
|
-
return result.status === 0 && result.stdout.trim() === "true";
|
|
122450
|
-
} catch {
|
|
122451
|
-
return false;
|
|
122452
|
-
}
|
|
122453
|
-
}
|
|
122454
|
-
function readBranch(workDir) {
|
|
122455
|
-
try {
|
|
122456
|
-
const result = spawnSync("git", [
|
|
122457
|
-
"-C",
|
|
122458
|
-
workDir,
|
|
122459
|
-
"branch",
|
|
122460
|
-
"--show-current"
|
|
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
122353
|
//#region src/tui/components/chrome/welcome.ts
|
|
122947
122354
|
const HUE_STOPS = 24;
|
|
122948
122355
|
const SUB_STEPS = 5;
|
|
122949
122356
|
const BREATHE_STEPS = HUE_STOPS * SUB_STEPS;
|
|
122950
122357
|
const BREATHE_INTERVAL_MS = 40;
|
|
122951
|
-
|
|
122358
|
+
const LOGO_FRAMES = [
|
|
122359
|
+
["██▄▄▄██", "▐█▄▀▄█▌"],
|
|
122360
|
+
["██▄▄▄██", "▐▄▄▀▄▄▌"],
|
|
122361
|
+
["██▄▄▄██", "▐▄▀▄▄▄▌"],
|
|
122362
|
+
["██▄▄▄██", "▐▄▄▄▀▄▌"],
|
|
122363
|
+
["██▄▄▄██", "▐█▄▀▄█▌"]
|
|
122364
|
+
];
|
|
122365
|
+
function hexToRgb$1(hex) {
|
|
122952
122366
|
return [
|
|
122953
122367
|
parseInt(hex.slice(1, 3), 16),
|
|
122954
122368
|
parseInt(hex.slice(3, 5), 16),
|
|
@@ -123013,12 +122427,12 @@ function rgbToHex(r, g, b) {
|
|
|
123013
122427
|
* it sweeps through all 24 hue stops with smooth sub-step interpolation.
|
|
123014
122428
|
*/
|
|
123015
122429
|
function buildBreathingPalette(primaryHex, hueStops, subSteps) {
|
|
123016
|
-
const [r, g, b] = hexToRgb(primaryHex);
|
|
123017
|
-
const [baseHue
|
|
122430
|
+
const [r, g, b] = hexToRgb$1(primaryHex);
|
|
122431
|
+
const [baseHue] = rgbToHsl(r, g, b);
|
|
123018
122432
|
const steps = hueStops * subSteps;
|
|
123019
122433
|
const palette = [];
|
|
123020
122434
|
for (let i = 0; i < steps; i++) {
|
|
123021
|
-
const [rr, gg, bb] = hslToRgb((baseHue + i / steps * 360) % 360,
|
|
122435
|
+
const [rr, gg, bb] = hslToRgb((baseHue + i / steps * 360) % 360, 90, 70);
|
|
123022
122436
|
palette.push(rgbToHex(rr, gg, bb));
|
|
123023
122437
|
}
|
|
123024
122438
|
return palette;
|
|
@@ -123058,53 +122472,52 @@ var WelcomeComponent = class {
|
|
|
123058
122472
|
render(width) {
|
|
123059
122473
|
const breatheColor = this.breathePalette[this.breatheFrame] ?? this.colors.primary;
|
|
123060
122474
|
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
122475
|
const dim = chalk.hex(this.colors.textDim);
|
|
123071
122476
|
const labelStyle = chalk.bold.hex(this.colors.textDim);
|
|
123072
|
-
const
|
|
123073
|
-
const
|
|
122477
|
+
const innerWidth = Math.max(10, width - 4);
|
|
122478
|
+
const isLoggedOut = !this.state.model;
|
|
122479
|
+
const frame = LOGO_FRAMES[this.breatheTimer !== null ? Math.floor(this.breatheFrame / 24) % LOGO_FRAMES.length : 0];
|
|
122480
|
+
const logo = [logoColor(frame[0]), logoColor(frame[1])];
|
|
123074
122481
|
const activeModel = this.state.availableModels[this.state.model];
|
|
123075
122482
|
const modelValue = isLoggedOut ? chalk.hex(this.colors.warning)("未设置,运行 /config") : activeModel?.displayName ?? activeModel?.model ?? this.state.model;
|
|
123076
122483
|
let versionValue;
|
|
123077
122484
|
if (this.state.hasNewVersion && this.state.latestVersion !== null) versionValue = chalk.hex(this.colors.warning)(this.state.version) + " " + chalk.hex(this.colors.textDim)("有新版本(" + this.state.latestVersion + ")");
|
|
123078
122485
|
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);
|
|
122486
|
+
const hintText = isLoggedOut ? "运行 /config 开始配置" : "发送 / 进入快捷菜单,/exit 保存并退出";
|
|
123087
122487
|
const contentLines = [
|
|
123088
|
-
...
|
|
122488
|
+
...logo,
|
|
123089
122489
|
"",
|
|
123090
|
-
|
|
122490
|
+
labelStyle("版本:") + " " + versionValue,
|
|
122491
|
+
labelStyle("模型:") + " " + modelValue,
|
|
122492
|
+
labelStyle("目录:") + " " + this.state.workDir,
|
|
123091
122493
|
"",
|
|
123092
|
-
|
|
122494
|
+
dim(hintText)
|
|
123093
122495
|
];
|
|
123094
|
-
const borderTitle = this.borderTitle;
|
|
122496
|
+
const borderTitle = this.borderTitle ?? "";
|
|
122497
|
+
const contentWidth = width - 2;
|
|
122498
|
+
let topBorder;
|
|
122499
|
+
if (borderTitle) {
|
|
122500
|
+
const centerPos = Math.floor(contentWidth / 2);
|
|
122501
|
+
const titleText = `─ ${borderTitle} ─`;
|
|
122502
|
+
const titleStart = centerPos - Math.floor(visibleWidth(titleText) / 2);
|
|
122503
|
+
const leftDash = Math.max(0, titleStart);
|
|
122504
|
+
const rightDash = Math.max(0, contentWidth - leftDash - visibleWidth(titleText));
|
|
122505
|
+
topBorder = logoColor("╭" + "─".repeat(leftDash) + titleText + "─".repeat(rightDash) + "╮");
|
|
122506
|
+
} else topBorder = logoColor("╭" + "─".repeat(contentWidth) + "╮");
|
|
123095
122507
|
const lines = [
|
|
123096
122508
|
"",
|
|
123097
|
-
|
|
123098
|
-
|
|
122509
|
+
topBorder,
|
|
122510
|
+
logoColor("│") + " ".repeat(width - 2) + logoColor("│")
|
|
123099
122511
|
];
|
|
123100
122512
|
for (const content of contentLines) {
|
|
123101
122513
|
const truncated = truncateToWidth(content, innerWidth, "…");
|
|
123102
122514
|
const vis = visibleWidth(truncated);
|
|
123103
|
-
const
|
|
123104
|
-
|
|
122515
|
+
const centerPad = Math.floor((width - 1 - vis) / 2);
|
|
122516
|
+
const rightPad = width - 2 - vis - centerPad;
|
|
122517
|
+
lines.push(logoColor("│") + " ".repeat(centerPad) + truncated + " ".repeat(rightPad) + logoColor("│"));
|
|
123105
122518
|
}
|
|
123106
|
-
lines.push(
|
|
123107
|
-
lines.push(
|
|
122519
|
+
lines.push(logoColor("│") + " ".repeat(width - 2) + logoColor("│"));
|
|
122520
|
+
lines.push(logoColor("╰" + "─".repeat(width - 2) + "╯"));
|
|
123108
122521
|
lines.push("");
|
|
123109
122522
|
return lines;
|
|
123110
122523
|
}
|
|
@@ -127152,7 +126565,7 @@ async function confirmUninstall(host, label) {
|
|
|
127152
126565
|
* restores the editor. The question and answer are never recorded in the
|
|
127153
126566
|
* main conversation history.
|
|
127154
126567
|
*/
|
|
127155
|
-
const SPINNER_FRAMES = [
|
|
126568
|
+
const SPINNER_FRAMES$1 = [
|
|
127156
126569
|
"⠋",
|
|
127157
126570
|
"⠙",
|
|
127158
126571
|
"⠹",
|
|
@@ -127212,7 +126625,7 @@ var BtwOverlayComponent = class extends Container {
|
|
|
127212
126625
|
startSpinner() {
|
|
127213
126626
|
this.spinnerFrame = 0;
|
|
127214
126627
|
this.spinnerInterval = setInterval(() => {
|
|
127215
|
-
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
126628
|
+
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES$1.length;
|
|
127216
126629
|
this.requestRender();
|
|
127217
126630
|
}, 80);
|
|
127218
126631
|
}
|
|
@@ -127232,7 +126645,7 @@ var BtwOverlayComponent = class extends Container {
|
|
|
127232
126645
|
lines.push(chalk.hex(c.primary)("/btw") + chalk.hex(c.textMuted)(" — ") + chalk.hex(c.text)(truncated));
|
|
127233
126646
|
lines.push("");
|
|
127234
126647
|
if (this.status === "loading") {
|
|
127235
|
-
const spinner = SPINNER_FRAMES[this.spinnerFrame];
|
|
126648
|
+
const spinner = SPINNER_FRAMES$1[this.spinnerFrame];
|
|
127236
126649
|
lines.push(chalk.hex(c.textMuted)(`${spinner} Answering…`));
|
|
127237
126650
|
} else if (this.status === "done" && this.markdown !== void 0) {
|
|
127238
126651
|
const contentWidth = Math.max(20, width - 2);
|
|
@@ -131265,1121 +130678,1680 @@ var TasksBrowserApp = class extends Container {
|
|
|
131265
130678
|
const task = next.tasks.find((t) => t.taskId === this.pendingStopTaskId);
|
|
131266
130679
|
if (task === void 0 || isTerminal(task.status)) this.clearPendingStop();
|
|
131267
130680
|
}
|
|
131268
|
-
this.invalidate();
|
|
130681
|
+
this.invalidate();
|
|
130682
|
+
}
|
|
130683
|
+
syncSelectionFromProps() {
|
|
130684
|
+
if (this.sortedVisible.length === 0) {
|
|
130685
|
+
this.selectedIndex = 0;
|
|
130686
|
+
this.listScroll = 0;
|
|
130687
|
+
return;
|
|
130688
|
+
}
|
|
130689
|
+
if (this.props.selectedTaskId !== void 0) {
|
|
130690
|
+
const idx = this.sortedVisible.findIndex((t) => t.taskId === this.props.selectedTaskId);
|
|
130691
|
+
if (idx !== -1) {
|
|
130692
|
+
this.selectedIndex = idx;
|
|
130693
|
+
return;
|
|
130694
|
+
}
|
|
130695
|
+
}
|
|
130696
|
+
if (this.selectedIndex >= this.sortedVisible.length) this.selectedIndex = this.sortedVisible.length - 1;
|
|
130697
|
+
}
|
|
130698
|
+
clearPendingStop() {
|
|
130699
|
+
this.pendingStopTaskId = void 0;
|
|
130700
|
+
if (this.pendingStopTimer !== void 0) {
|
|
130701
|
+
clearTimeout(this.pendingStopTimer);
|
|
130702
|
+
this.pendingStopTimer = void 0;
|
|
130703
|
+
}
|
|
130704
|
+
}
|
|
130705
|
+
emitSelect() {
|
|
130706
|
+
const task = this.sortedVisible[this.selectedIndex];
|
|
130707
|
+
if (task) this.props.onSelect(task.taskId);
|
|
130708
|
+
}
|
|
130709
|
+
handleInput(data) {
|
|
130710
|
+
const k = printableChar(data);
|
|
130711
|
+
if (this.pendingStopTaskId !== void 0) {
|
|
130712
|
+
if (k === "y" || k === "Y") {
|
|
130713
|
+
const taskId = this.pendingStopTaskId;
|
|
130714
|
+
this.clearPendingStop();
|
|
130715
|
+
this.props.onStopConfirmed(taskId);
|
|
130716
|
+
this.invalidate();
|
|
130717
|
+
return;
|
|
130718
|
+
}
|
|
130719
|
+
this.clearPendingStop();
|
|
130720
|
+
this.invalidate();
|
|
130721
|
+
return;
|
|
130722
|
+
}
|
|
130723
|
+
if (matchesKey(data, Key.escape) || k === "q" || k === "Q") {
|
|
130724
|
+
this.props.onCancel();
|
|
130725
|
+
return;
|
|
130726
|
+
}
|
|
130727
|
+
if (matchesKey(data, Key.up) || k === "k") {
|
|
130728
|
+
if (this.sortedVisible.length === 0) return;
|
|
130729
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
130730
|
+
this.emitSelect();
|
|
130731
|
+
this.invalidate();
|
|
130732
|
+
return;
|
|
130733
|
+
}
|
|
130734
|
+
if (matchesKey(data, Key.down) || k === "j") {
|
|
130735
|
+
if (this.sortedVisible.length === 0) return;
|
|
130736
|
+
this.selectedIndex = Math.min(this.sortedVisible.length - 1, this.selectedIndex + 1);
|
|
130737
|
+
this.emitSelect();
|
|
130738
|
+
this.invalidate();
|
|
130739
|
+
return;
|
|
130740
|
+
}
|
|
130741
|
+
if (matchesKey(data, Key.tab) || k === " ") {
|
|
130742
|
+
this.props.onToggleFilter();
|
|
130743
|
+
return;
|
|
130744
|
+
}
|
|
130745
|
+
if (k === "r" || k === "R") {
|
|
130746
|
+
this.props.onRefresh();
|
|
130747
|
+
return;
|
|
130748
|
+
}
|
|
130749
|
+
if (k === "s" || k === "S") {
|
|
130750
|
+
const task = this.sortedVisible[this.selectedIndex];
|
|
130751
|
+
if (task === void 0) return;
|
|
130752
|
+
if (isTerminal(task.status)) {
|
|
130753
|
+
this.props.onStopIgnored?.(task.taskId, "terminal");
|
|
130754
|
+
return;
|
|
130755
|
+
}
|
|
130756
|
+
this.pendingStopTaskId = task.taskId;
|
|
130757
|
+
this.pendingStopTimer = setTimeout(() => {
|
|
130758
|
+
this.clearPendingStop();
|
|
130759
|
+
this.invalidate();
|
|
130760
|
+
}, STOP_CONFIRM_TIMEOUT_MS);
|
|
130761
|
+
this.invalidate();
|
|
130762
|
+
return;
|
|
130763
|
+
}
|
|
130764
|
+
if (k === "o" || k === "O" || matchesKey(data, Key.enter)) {
|
|
130765
|
+
const task = this.sortedVisible[this.selectedIndex];
|
|
130766
|
+
if (task) this.props.onOpenOutput(task.taskId);
|
|
130767
|
+
return;
|
|
130768
|
+
}
|
|
130769
|
+
}
|
|
130770
|
+
/**
|
|
130771
|
+
* Render the entire screen as `terminal.rows` lines of `width` cols.
|
|
130772
|
+
* Layout: header(1) + body(rows-2) + footer(1).
|
|
130773
|
+
*/
|
|
130774
|
+
render(width) {
|
|
130775
|
+
const rows = Math.max(1, this.terminal.rows);
|
|
130776
|
+
if (width < MIN_WIDTH || rows < MIN_HEIGHT) return this.renderTooSmall(width, rows);
|
|
130777
|
+
const header = this.renderHeader(width);
|
|
130778
|
+
const footer = this.renderFooter(width);
|
|
130779
|
+
const bodyHeight = rows - 2;
|
|
130780
|
+
const listWidth = Math.max(LIST_COL_MIN, Math.min(LIST_COL_MAX, Math.floor(width * LIST_COL_RATIO)));
|
|
130781
|
+
const rightWidth = width - listWidth;
|
|
130782
|
+
const listFrame = this.renderListFrame(listWidth, bodyHeight);
|
|
130783
|
+
const rightFrames = this.renderRightStack(rightWidth, bodyHeight);
|
|
130784
|
+
const lines = [header];
|
|
130785
|
+
for (let i = 0; i < bodyHeight; i++) lines.push((listFrame[i] ?? " ".repeat(listWidth)) + (rightFrames[i] ?? " ".repeat(rightWidth)));
|
|
130786
|
+
lines.push(footer);
|
|
130787
|
+
return lines;
|
|
130788
|
+
}
|
|
130789
|
+
renderHeader(width) {
|
|
130790
|
+
const colors = this.props.colors;
|
|
130791
|
+
const title = chalk.hex(colors.primary).bold(" TASK BROWSER ");
|
|
130792
|
+
const filterText = chalk.hex(colors.textMuted)(` filter=${this.props.filter === "all" ? "ALL" : "ACTIVE"} `);
|
|
130793
|
+
const counts = countByStatus(this.props.tasks);
|
|
130794
|
+
const countSegments = [];
|
|
130795
|
+
if (counts.running > 0) countSegments.push(chalk.hex(colors.success)(` ${String(counts.running)} 运行中 `));
|
|
130796
|
+
if (counts.awaiting > 0) countSegments.push(chalk.hex(colors.warning)(` ${String(counts.awaiting)} 等待中 `));
|
|
130797
|
+
if (counts.completed > 0) countSegments.push(chalk.hex(colors.textDim)(` ${String(counts.completed)} 已完成 `));
|
|
130798
|
+
if (counts.terminalFailed > 0) countSegments.push(chalk.hex(colors.error)(` ${String(counts.terminalFailed)} 已中断 `));
|
|
130799
|
+
const totals = chalk.hex(colors.textMuted)(` ${String(this.props.tasks.length)} 总计 `);
|
|
130800
|
+
return fitExactly$1(title + filterText + countSegments.join("") + totals, width);
|
|
130801
|
+
}
|
|
130802
|
+
renderFooter(width) {
|
|
130803
|
+
const colors = this.props.colors;
|
|
130804
|
+
const key = (text) => chalk.hex(colors.primary).bold(text);
|
|
130805
|
+
const dim = (text) => chalk.hex(colors.textMuted)(text);
|
|
130806
|
+
if (this.pendingStopTaskId !== void 0) {
|
|
130807
|
+
const warn = (text) => chalk.hex(colors.warning).bold(text);
|
|
130808
|
+
return fitExactly$1(` ${warn("停止")} ${chalk.hex(colors.text)(this.pendingStopTaskId)}? ${key("Y")} ${dim("确认")} ${key("N")} ${dim("取消")} `, width);
|
|
130809
|
+
}
|
|
130810
|
+
const left = [
|
|
130811
|
+
` ${key("↑↓")} ${dim("选择")}`,
|
|
130812
|
+
`${key("Enter/O")} ${dim("输出")}`,
|
|
130813
|
+
`${key("S")} ${dim("停止")}`,
|
|
130814
|
+
`${key("R")} ${dim("刷新")}`,
|
|
130815
|
+
`${key("Tab")} ${dim("筛选")}`,
|
|
130816
|
+
`${key("Q/Esc")} ${dim("退出")} `
|
|
130817
|
+
].join(" ");
|
|
130818
|
+
const flash = this.props.flashMessage;
|
|
130819
|
+
if (flash !== void 0 && flash.length > 0) {
|
|
130820
|
+
const flashStyled = chalk.hex(colors.warning)(` ${flash} `);
|
|
130821
|
+
const total = visibleWidth(left) + visibleWidth(flashStyled);
|
|
130822
|
+
if (total <= width) return left + " ".repeat(width - total) + flashStyled;
|
|
130823
|
+
}
|
|
130824
|
+
return fitExactly$1(left, width);
|
|
130825
|
+
}
|
|
130826
|
+
/**
|
|
130827
|
+
* Render a framed box: `┌─ Title ─┐` top, `│ <content> │` sides, `└─┘`
|
|
130828
|
+
* bottom. Result is exactly `width × height` cells. `content` is a
|
|
130829
|
+
* pre-rendered array of inner-width-sized lines; extra rows are padded.
|
|
130830
|
+
*/
|
|
130831
|
+
renderFrame(title, content, width, height) {
|
|
130832
|
+
if (height < 2 || width < 4) {
|
|
130833
|
+
const out = [];
|
|
130834
|
+
for (let i = 0; i < height; i++) out.push(" ".repeat(width));
|
|
130835
|
+
return out;
|
|
130836
|
+
}
|
|
130837
|
+
const stroke = this.props.colors.primary;
|
|
130838
|
+
const innerWidth = width - 2;
|
|
130839
|
+
const innerHeight = height - 2;
|
|
130840
|
+
const titleStyled = chalk.hex(this.props.colors.textStrong).bold(title);
|
|
130841
|
+
const titleWidth = visibleWidth(titleStyled);
|
|
130842
|
+
const titleSegmentWidth = visibleWidth(`─ ${titleStyled} `);
|
|
130843
|
+
const remainingDashes = Math.max(0, innerWidth - titleSegmentWidth);
|
|
130844
|
+
const topMid = titleWidth > 0 && titleSegmentWidth <= innerWidth ? chalk.hex(stroke)("─ ") + titleStyled + " " + chalk.hex(stroke)("─".repeat(remainingDashes)) : chalk.hex(stroke)("─".repeat(innerWidth));
|
|
130845
|
+
const top = chalk.hex(stroke)("┌") + topMid + chalk.hex(stroke)("┐");
|
|
130846
|
+
const bottom = chalk.hex(stroke)("└" + "─".repeat(innerWidth) + "┘");
|
|
130847
|
+
const lines = [top];
|
|
130848
|
+
for (let i = 0; i < innerHeight; i++) {
|
|
130849
|
+
const inner = content[i] ?? "";
|
|
130850
|
+
lines.push(chalk.hex(stroke)("│") + fitExactly$1(inner, innerWidth) + chalk.hex(stroke)("│"));
|
|
130851
|
+
}
|
|
130852
|
+
lines.push(bottom);
|
|
130853
|
+
return lines;
|
|
130854
|
+
}
|
|
130855
|
+
renderListFrame(width, height) {
|
|
130856
|
+
const title = `Tasks [${this.props.filter}]`;
|
|
130857
|
+
const innerHeight = Math.max(0, height - 2);
|
|
130858
|
+
if (this.sortedVisible.length === 0) {
|
|
130859
|
+
const empty = this.props.filter === "active" ? "无活跃任务。Tab = 显示全部。" : "本会话无后台任务。";
|
|
130860
|
+
const lines = [chalk.hex(this.props.colors.textMuted)(empty)];
|
|
130861
|
+
while (lines.length < innerHeight) lines.push("");
|
|
130862
|
+
return this.renderFrame(title, lines, width, height);
|
|
130863
|
+
}
|
|
130864
|
+
this.adjustScroll(innerHeight);
|
|
130865
|
+
const start = this.listScroll;
|
|
130866
|
+
const window = this.sortedVisible.slice(start, start + innerHeight);
|
|
130867
|
+
const innerWidth = width - 2;
|
|
130868
|
+
const lines = [];
|
|
130869
|
+
for (const [vi, task] of window.entries()) {
|
|
130870
|
+
const index = start + vi;
|
|
130871
|
+
lines.push(this.renderListRow(task, index === this.selectedIndex, innerWidth));
|
|
130872
|
+
}
|
|
130873
|
+
while (lines.length < innerHeight) lines.push("");
|
|
130874
|
+
return this.renderFrame(title, lines, width, height);
|
|
130875
|
+
}
|
|
130876
|
+
renderListRow(task, selected, innerWidth) {
|
|
130877
|
+
const colors = this.props.colors;
|
|
130878
|
+
const pointer = selected ? "> " : " ";
|
|
130879
|
+
const pointerStyled = chalk.hex(selected ? colors.primary : colors.textDim)(pointer);
|
|
130880
|
+
const idColor = selected ? colors.primary : task.taskId.startsWith("agent-") ? colors.success : colors.accent;
|
|
130881
|
+
const idText = selected ? chalk.hex(idColor).bold(task.taskId) : chalk.hex(idColor)(task.taskId);
|
|
130882
|
+
const idPad = " ".repeat(Math.max(0, 17 - task.taskId.length));
|
|
130883
|
+
const status = STATUS_LABEL[task.status];
|
|
130884
|
+
const prefix = `${pointerStyled}${idText}${idPad} ${chalk.hex(statusColor(colors, task.status))(status)}`;
|
|
130885
|
+
const prefixWidth = visibleWidth(prefix);
|
|
130886
|
+
const descBudget = Math.max(0, innerWidth - prefixWidth - 1);
|
|
130887
|
+
if (descBudget < 4) return fitExactly$1(prefix, innerWidth);
|
|
130888
|
+
const desc = truncateToWidth(singleLine$2(task.description) || singleLine$2(task.command) || "(no description)", descBudget, ELLIPSIS$3);
|
|
130889
|
+
return fitExactly$1(`${prefix} ${chalk.hex(colors.text)(desc)}`, innerWidth);
|
|
130890
|
+
}
|
|
130891
|
+
adjustScroll(visibleRows) {
|
|
130892
|
+
if (visibleRows <= 0) {
|
|
130893
|
+
this.listScroll = 0;
|
|
130894
|
+
return;
|
|
130895
|
+
}
|
|
130896
|
+
if (this.selectedIndex < this.listScroll) this.listScroll = this.selectedIndex;
|
|
130897
|
+
else if (this.selectedIndex >= this.listScroll + visibleRows) this.listScroll = this.selectedIndex - visibleRows + 1;
|
|
130898
|
+
const maxScroll = Math.max(0, this.sortedVisible.length - visibleRows);
|
|
130899
|
+
if (this.listScroll < 0) this.listScroll = 0;
|
|
130900
|
+
if (this.listScroll > maxScroll) this.listScroll = maxScroll;
|
|
130901
|
+
}
|
|
130902
|
+
renderRightStack(width, height) {
|
|
130903
|
+
const detailHeight = Math.max(8, Math.min(Math.floor(height * .4), height - 5));
|
|
130904
|
+
const previewHeight = height - detailHeight;
|
|
130905
|
+
return [...this.renderDetailFrame(width, detailHeight), ...this.renderPreviewFrame(width, previewHeight)];
|
|
130906
|
+
}
|
|
130907
|
+
renderDetailFrame(width, height) {
|
|
130908
|
+
const colors = this.props.colors;
|
|
130909
|
+
const innerHeight = Math.max(0, height - 2);
|
|
130910
|
+
const task = this.sortedVisible[this.selectedIndex];
|
|
130911
|
+
if (task === void 0) {
|
|
130912
|
+
const lines = [chalk.hex(colors.textMuted)("从列表中选择一个任务。")];
|
|
130913
|
+
while (lines.length < innerHeight) lines.push("");
|
|
130914
|
+
return this.renderFrame("详情", lines, width, height);
|
|
130915
|
+
}
|
|
130916
|
+
const label = (text) => chalk.hex(colors.textMuted)(text.padEnd(14));
|
|
130917
|
+
const value = (text) => chalk.hex(colors.text)(text);
|
|
130918
|
+
const lines = [
|
|
130919
|
+
`${label("任务 ID:")}${value(task.taskId)}`,
|
|
130920
|
+
`${label("状态:")}${chalk.hex(statusColor(colors, task.status))(STATUS_LABEL[task.status])}`,
|
|
130921
|
+
`${label("描述:")}${value(singleLine$2(task.description) || "—")}`
|
|
130922
|
+
];
|
|
130923
|
+
if (task.command && task.command !== task.description) lines.push(`${label("命令:")}${value(singleLine$2(task.command))}`);
|
|
130924
|
+
const timing = task.status === "running" || task.status === "awaiting_approval" ? `运行中 ${formatRelativeTime$2(task.startedAt)}` : task.endedAt !== null && task.endedAt !== void 0 ? `已完成 ${formatRelativeTime$2(task.endedAt)}` : "";
|
|
130925
|
+
if (timing.length > 0) lines.push(`${label("时间:")}${chalk.hex(colors.textMuted)(timing)}`);
|
|
130926
|
+
if (task.pid > 0) lines.push(`${label("进程 ID:")}${chalk.hex(colors.textMuted)(String(task.pid))}`);
|
|
130927
|
+
if (task.exitCode !== null && task.exitCode !== void 0) lines.push(`${label("退出码:")}${chalk.hex(colors.textMuted)(String(task.exitCode))}`);
|
|
130928
|
+
if (task.stopReason !== void 0 && task.stopReason.length > 0) lines.push(`${label("停止原因:")}${chalk.hex(colors.textMuted)(task.stopReason)}`);
|
|
130929
|
+
if (task.timedOut === true) lines.push(`${label("已超时:")}${chalk.hex(colors.warning)("是")}`);
|
|
130930
|
+
if (task.approvalReason !== void 0 && task.approvalReason.length > 0) lines.push(`${label("等待中:")}${chalk.hex(colors.warning)(singleLine$2(task.approvalReason))}`);
|
|
130931
|
+
while (lines.length < innerHeight) lines.push("");
|
|
130932
|
+
return this.renderFrame("详情", lines, width, height);
|
|
130933
|
+
}
|
|
130934
|
+
renderPreviewFrame(width, height) {
|
|
130935
|
+
const colors = this.props.colors;
|
|
130936
|
+
const innerHeight = Math.max(0, height - 2);
|
|
130937
|
+
if (this.sortedVisible[this.selectedIndex] === void 0) {
|
|
130938
|
+
const lines = [chalk.hex(colors.textMuted)("No task selected.")];
|
|
130939
|
+
while (lines.length < innerHeight) lines.push("");
|
|
130940
|
+
return this.renderFrame("Preview Output", lines, width, height);
|
|
130941
|
+
}
|
|
130942
|
+
let body;
|
|
130943
|
+
if (this.props.tailLoading) body = "[loading…]";
|
|
130944
|
+
else if (this.props.tailOutput === void 0 || this.props.tailOutput.length === 0) body = "[no output captured]";
|
|
130945
|
+
else body = this.props.tailOutput;
|
|
130946
|
+
const styled = body.split("\n").slice(-innerHeight).map((line) => chalk.hex(colors.textDim)(line));
|
|
130947
|
+
while (styled.length < innerHeight) styled.push("");
|
|
130948
|
+
return this.renderFrame("Preview Output", styled, width, height);
|
|
131269
130949
|
}
|
|
131270
|
-
|
|
131271
|
-
|
|
131272
|
-
|
|
131273
|
-
|
|
130950
|
+
renderTooSmall(width, rows) {
|
|
130951
|
+
const lines = [];
|
|
130952
|
+
const msg = chalk.hex(this.props.colors.error)(`Terminal too small (need ≥ ${String(MIN_WIDTH)} × ${String(MIN_HEIGHT)})`);
|
|
130953
|
+
lines.push(fitExactly$1(msg, width));
|
|
130954
|
+
for (let i = 1; i < rows; i++) lines.push(" ".repeat(width));
|
|
130955
|
+
return lines;
|
|
130956
|
+
}
|
|
130957
|
+
};
|
|
130958
|
+
//#endregion
|
|
130959
|
+
//#region src/tui/controllers/tasks-browser.ts
|
|
130960
|
+
var TasksBrowserController = class {
|
|
130961
|
+
host;
|
|
130962
|
+
constructor(host) {
|
|
130963
|
+
this.host = host;
|
|
130964
|
+
}
|
|
130965
|
+
async show() {
|
|
130966
|
+
const { state } = this.host;
|
|
130967
|
+
if (state.tasksBrowser !== void 0) return;
|
|
130968
|
+
const session = this.host.session;
|
|
130969
|
+
if (session === void 0) {
|
|
130970
|
+
this.host.showError("没有活动会话。");
|
|
131274
130971
|
return;
|
|
131275
130972
|
}
|
|
131276
|
-
|
|
131277
|
-
|
|
131278
|
-
|
|
131279
|
-
|
|
131280
|
-
|
|
131281
|
-
|
|
130973
|
+
let tasks = [];
|
|
130974
|
+
try {
|
|
130975
|
+
tasks = await session.listBackgroundTasks({ activeOnly: false });
|
|
130976
|
+
} catch (error) {
|
|
130977
|
+
this.host.showError(`加载任务失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
130978
|
+
return;
|
|
131282
130979
|
}
|
|
131283
|
-
if (
|
|
130980
|
+
if (state.tasksBrowser !== void 0) return;
|
|
130981
|
+
const filter = "all";
|
|
130982
|
+
const selectedTaskId = this.pickInitialSelection(tasks, filter);
|
|
130983
|
+
const component = new TasksBrowserApp({
|
|
130984
|
+
tasks,
|
|
130985
|
+
filter,
|
|
130986
|
+
selectedTaskId,
|
|
130987
|
+
tailOutput: void 0,
|
|
130988
|
+
tailLoading: false,
|
|
130989
|
+
flashMessage: void 0,
|
|
130990
|
+
colors: state.theme.colors,
|
|
130991
|
+
...this.buildCallbacks()
|
|
130992
|
+
}, state.terminal);
|
|
130993
|
+
const savedChildren = [...state.ui.children];
|
|
130994
|
+
state.ui.clear();
|
|
130995
|
+
state.ui.addChild(component);
|
|
130996
|
+
state.ui.setFocus(component);
|
|
130997
|
+
state.ui.requestRender(true);
|
|
130998
|
+
const pollTimer = setInterval(() => {
|
|
130999
|
+
this.refresh({ silent: true });
|
|
131000
|
+
}, 1e3);
|
|
131001
|
+
this.host.setTasksBrowser({
|
|
131002
|
+
component,
|
|
131003
|
+
savedChildren,
|
|
131004
|
+
filter,
|
|
131005
|
+
selectedTaskId,
|
|
131006
|
+
tailOutput: void 0,
|
|
131007
|
+
tailLoading: false,
|
|
131008
|
+
tailRequestId: 0,
|
|
131009
|
+
flashMessage: void 0,
|
|
131010
|
+
flashTimer: void 0,
|
|
131011
|
+
pollTimer,
|
|
131012
|
+
viewer: void 0
|
|
131013
|
+
});
|
|
131014
|
+
if (selectedTaskId !== void 0) this.loadTail(selectedTaskId);
|
|
131284
131015
|
}
|
|
131285
|
-
|
|
131286
|
-
|
|
131287
|
-
|
|
131288
|
-
|
|
131289
|
-
|
|
131290
|
-
|
|
131016
|
+
close() {
|
|
131017
|
+
const { state } = this.host;
|
|
131018
|
+
const browser = state.tasksBrowser;
|
|
131019
|
+
if (browser === void 0) return;
|
|
131020
|
+
if (browser.viewer !== void 0) this.closeOutputViewer();
|
|
131021
|
+
if (browser.pollTimer !== void 0) clearInterval(browser.pollTimer);
|
|
131022
|
+
if (browser.flashTimer !== void 0) clearTimeout(browser.flashTimer);
|
|
131023
|
+
state.ui.clear();
|
|
131024
|
+
for (const child of browser.savedChildren) state.ui.addChild(child);
|
|
131025
|
+
this.host.setTasksBrowser(void 0);
|
|
131026
|
+
state.ui.setFocus(state.editor);
|
|
131027
|
+
state.ui.requestRender(true);
|
|
131291
131028
|
}
|
|
131292
|
-
|
|
131293
|
-
|
|
131294
|
-
|
|
131029
|
+
repaint() {
|
|
131030
|
+
if (this.host.state.tasksBrowser === void 0) return;
|
|
131031
|
+
const tasks = [...this.host.backgroundTasks.values()];
|
|
131032
|
+
this.pushProps(tasks);
|
|
131295
131033
|
}
|
|
131296
|
-
|
|
131297
|
-
const
|
|
131298
|
-
|
|
131299
|
-
|
|
131300
|
-
|
|
131301
|
-
|
|
131302
|
-
|
|
131303
|
-
|
|
131304
|
-
|
|
131034
|
+
async refreshOutputViewer(opts = {}) {
|
|
131035
|
+
const { state } = this.host;
|
|
131036
|
+
const browser = state.tasksBrowser;
|
|
131037
|
+
const viewer = browser?.viewer;
|
|
131038
|
+
if (browser === void 0 || viewer === void 0) return;
|
|
131039
|
+
const session = this.host.session;
|
|
131040
|
+
if (session === void 0) return;
|
|
131041
|
+
const myRefreshId = ++viewer.refreshId;
|
|
131042
|
+
let output;
|
|
131043
|
+
try {
|
|
131044
|
+
output = await session.getBackgroundTaskOutput(viewer.taskId);
|
|
131045
|
+
} catch (error) {
|
|
131046
|
+
if (!opts.silent) {
|
|
131047
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131048
|
+
this.flash(`输出刷新失败: ${message}`);
|
|
131305
131049
|
}
|
|
131306
|
-
this.clearPendingStop();
|
|
131307
|
-
this.invalidate();
|
|
131308
131050
|
return;
|
|
131309
131051
|
}
|
|
131310
|
-
|
|
131311
|
-
|
|
131052
|
+
const current = state.tasksBrowser?.viewer;
|
|
131053
|
+
if (current === void 0 || current !== viewer || current.refreshId !== myRefreshId) return;
|
|
131054
|
+
if (output === viewer.output) return;
|
|
131055
|
+
viewer.output = output;
|
|
131056
|
+
const info = this.host.backgroundTasks.get(viewer.taskId);
|
|
131057
|
+
viewer.component.setProps({
|
|
131058
|
+
taskId: viewer.taskId,
|
|
131059
|
+
info,
|
|
131060
|
+
output,
|
|
131061
|
+
colors: state.theme.colors,
|
|
131062
|
+
onClose: () => {
|
|
131063
|
+
this.closeOutputViewer();
|
|
131064
|
+
}
|
|
131065
|
+
});
|
|
131066
|
+
state.ui.requestRender();
|
|
131067
|
+
}
|
|
131068
|
+
pickInitialSelection(tasks, filter) {
|
|
131069
|
+
const candidates = filter === "all" ? tasks : tasks.filter((t) => t.status !== "completed" && t.status !== "failed" && t.status !== "killed" && t.status !== "lost");
|
|
131070
|
+
if (candidates.length === 0) return void 0;
|
|
131071
|
+
return candidates.find((t) => t.status === "running" || t.status === "awaiting_approval")?.taskId ?? candidates[0].taskId;
|
|
131072
|
+
}
|
|
131073
|
+
async refresh(opts = {}) {
|
|
131074
|
+
const { state } = this.host;
|
|
131075
|
+
const browser = state.tasksBrowser;
|
|
131076
|
+
if (browser === void 0) return;
|
|
131077
|
+
const session = this.host.session;
|
|
131078
|
+
if (session === void 0) return;
|
|
131079
|
+
let tasks;
|
|
131080
|
+
try {
|
|
131081
|
+
tasks = await session.listBackgroundTasks({ activeOnly: false });
|
|
131082
|
+
} catch (error) {
|
|
131083
|
+
if (!opts.silent) this.flash(`刷新失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
131312
131084
|
return;
|
|
131313
131085
|
}
|
|
131314
|
-
if (
|
|
131315
|
-
|
|
131316
|
-
|
|
131317
|
-
|
|
131318
|
-
|
|
131086
|
+
if (state.tasksBrowser !== browser) return;
|
|
131087
|
+
this.pushProps(tasks);
|
|
131088
|
+
}
|
|
131089
|
+
pushProps(tasks) {
|
|
131090
|
+
const browser = this.host.state.tasksBrowser;
|
|
131091
|
+
if (browser === void 0) return;
|
|
131092
|
+
browser.component.setProps({
|
|
131093
|
+
tasks,
|
|
131094
|
+
filter: browser.filter,
|
|
131095
|
+
selectedTaskId: browser.selectedTaskId,
|
|
131096
|
+
tailOutput: browser.tailOutput,
|
|
131097
|
+
tailLoading: browser.tailLoading,
|
|
131098
|
+
flashMessage: browser.flashMessage,
|
|
131099
|
+
colors: this.host.state.theme.colors,
|
|
131100
|
+
...this.buildCallbacks()
|
|
131101
|
+
});
|
|
131102
|
+
this.host.state.ui.requestRender();
|
|
131103
|
+
}
|
|
131104
|
+
buildCallbacks() {
|
|
131105
|
+
return {
|
|
131106
|
+
onSelect: (taskId) => {
|
|
131107
|
+
this.handleSelect(taskId);
|
|
131108
|
+
},
|
|
131109
|
+
onToggleFilter: () => {
|
|
131110
|
+
this.handleToggleFilter();
|
|
131111
|
+
},
|
|
131112
|
+
onRefresh: () => {
|
|
131113
|
+
this.handleRefresh();
|
|
131114
|
+
},
|
|
131115
|
+
onCancel: () => {
|
|
131116
|
+
this.close();
|
|
131117
|
+
},
|
|
131118
|
+
onStopConfirmed: (taskId) => {
|
|
131119
|
+
this.handleStop(taskId);
|
|
131120
|
+
},
|
|
131121
|
+
onOpenOutput: (taskId) => {
|
|
131122
|
+
this.handleOpenOutput(taskId);
|
|
131123
|
+
},
|
|
131124
|
+
onStopIgnored: (taskId, reason) => {
|
|
131125
|
+
if (reason === "terminal") this.flash(`${taskId} 已是终止状态 — 无需停止。`);
|
|
131126
|
+
}
|
|
131127
|
+
};
|
|
131128
|
+
}
|
|
131129
|
+
handleSelect(taskId) {
|
|
131130
|
+
const browser = this.host.state.tasksBrowser;
|
|
131131
|
+
if (browser === void 0) return;
|
|
131132
|
+
if (browser.selectedTaskId === taskId) return;
|
|
131133
|
+
browser.selectedTaskId = taskId;
|
|
131134
|
+
browser.tailOutput = void 0;
|
|
131135
|
+
browser.tailLoading = true;
|
|
131136
|
+
this.repaint();
|
|
131137
|
+
this.loadTail(taskId);
|
|
131138
|
+
}
|
|
131139
|
+
handleToggleFilter() {
|
|
131140
|
+
const browser = this.host.state.tasksBrowser;
|
|
131141
|
+
if (browser === void 0) return;
|
|
131142
|
+
browser.filter = browser.filter === "all" ? "active" : "all";
|
|
131143
|
+
this.repaint();
|
|
131144
|
+
}
|
|
131145
|
+
handleRefresh() {
|
|
131146
|
+
this.flash("正在刷新…", 600);
|
|
131147
|
+
this.refresh();
|
|
131148
|
+
}
|
|
131149
|
+
async handleStop(taskId) {
|
|
131150
|
+
if (this.host.state.tasksBrowser === void 0) return;
|
|
131151
|
+
const session = this.host.session;
|
|
131152
|
+
if (session === void 0) {
|
|
131153
|
+
this.flash("没有活动会话。");
|
|
131319
131154
|
return;
|
|
131320
131155
|
}
|
|
131321
|
-
|
|
131322
|
-
|
|
131323
|
-
|
|
131324
|
-
this.
|
|
131325
|
-
|
|
131326
|
-
|
|
131156
|
+
this.flash(`正在停止 ${taskId}…`, 1500);
|
|
131157
|
+
try {
|
|
131158
|
+
await session.stopBackgroundTask(taskId, { reason: "用户发起停止" });
|
|
131159
|
+
await this.refresh({ silent: true });
|
|
131160
|
+
} catch (error) {
|
|
131161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131162
|
+
this.flash(`停止失败: ${message}`);
|
|
131327
131163
|
}
|
|
131328
|
-
|
|
131329
|
-
|
|
131164
|
+
}
|
|
131165
|
+
async handleOpenOutput(taskId) {
|
|
131166
|
+
const { state } = this.host;
|
|
131167
|
+
const browser = state.tasksBrowser;
|
|
131168
|
+
if (browser === void 0) return;
|
|
131169
|
+
if (browser.viewer !== void 0) return;
|
|
131170
|
+
const session = this.host.session;
|
|
131171
|
+
if (session === void 0) {
|
|
131172
|
+
this.flash("没有活动会话。");
|
|
131330
131173
|
return;
|
|
131331
131174
|
}
|
|
131332
|
-
|
|
131333
|
-
|
|
131175
|
+
let output;
|
|
131176
|
+
try {
|
|
131177
|
+
output = await session.getBackgroundTaskOutput(taskId);
|
|
131178
|
+
} catch (error) {
|
|
131179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131180
|
+
this.flash(`无法打开输出: ${message}`);
|
|
131334
131181
|
return;
|
|
131335
131182
|
}
|
|
131336
|
-
|
|
131337
|
-
|
|
131338
|
-
|
|
131339
|
-
|
|
131340
|
-
|
|
131341
|
-
|
|
131183
|
+
const current = state.tasksBrowser;
|
|
131184
|
+
if (current === void 0 || current !== browser) return;
|
|
131185
|
+
const viewer = new TaskOutputViewer({
|
|
131186
|
+
taskId,
|
|
131187
|
+
info: this.host.backgroundTasks.get(taskId),
|
|
131188
|
+
output,
|
|
131189
|
+
colors: state.theme.colors,
|
|
131190
|
+
onClose: () => {
|
|
131191
|
+
this.closeOutputViewer();
|
|
131342
131192
|
}
|
|
131343
|
-
|
|
131344
|
-
|
|
131345
|
-
|
|
131346
|
-
|
|
131347
|
-
|
|
131348
|
-
|
|
131349
|
-
|
|
131350
|
-
|
|
131351
|
-
|
|
131352
|
-
|
|
131353
|
-
|
|
131193
|
+
}, state.terminal);
|
|
131194
|
+
const savedBrowserChildren = [...state.ui.children];
|
|
131195
|
+
state.ui.clear();
|
|
131196
|
+
state.ui.addChild(viewer);
|
|
131197
|
+
state.ui.setFocus(viewer);
|
|
131198
|
+
state.ui.requestRender(true);
|
|
131199
|
+
const pollTimer = setInterval(() => {
|
|
131200
|
+
this.refreshOutputViewer({ silent: true });
|
|
131201
|
+
}, 1e3);
|
|
131202
|
+
browser.viewer = {
|
|
131203
|
+
component: viewer,
|
|
131204
|
+
savedChildren: savedBrowserChildren,
|
|
131205
|
+
taskId,
|
|
131206
|
+
output,
|
|
131207
|
+
refreshId: 0,
|
|
131208
|
+
pollTimer
|
|
131209
|
+
};
|
|
131210
|
+
}
|
|
131211
|
+
loadTail(taskId) {
|
|
131212
|
+
const { state } = this.host;
|
|
131213
|
+
const browser = state.tasksBrowser;
|
|
131214
|
+
if (browser === void 0) return;
|
|
131215
|
+
const session = this.host.session;
|
|
131216
|
+
if (session === void 0) {
|
|
131217
|
+
browser.tailLoading = false;
|
|
131218
|
+
this.repaint();
|
|
131354
131219
|
return;
|
|
131355
131220
|
}
|
|
131221
|
+
const requestId = ++browser.tailRequestId;
|
|
131222
|
+
session.getBackgroundTaskOutput(taskId, { tail: 4e3 }).then((output) => {
|
|
131223
|
+
const current = state.tasksBrowser;
|
|
131224
|
+
if (current === void 0) return;
|
|
131225
|
+
if (current !== browser || current.tailRequestId !== requestId) return;
|
|
131226
|
+
if (current.selectedTaskId !== taskId) return;
|
|
131227
|
+
current.tailOutput = output;
|
|
131228
|
+
current.tailLoading = false;
|
|
131229
|
+
this.repaint();
|
|
131230
|
+
}).catch(() => {
|
|
131231
|
+
const current = state.tasksBrowser;
|
|
131232
|
+
if (current === void 0) return;
|
|
131233
|
+
if (current !== browser || current.tailRequestId !== requestId) return;
|
|
131234
|
+
if (current.selectedTaskId !== taskId) return;
|
|
131235
|
+
current.tailOutput = "";
|
|
131236
|
+
current.tailLoading = false;
|
|
131237
|
+
this.repaint();
|
|
131238
|
+
});
|
|
131356
131239
|
}
|
|
131357
|
-
|
|
131358
|
-
|
|
131359
|
-
|
|
131360
|
-
|
|
131361
|
-
|
|
131362
|
-
|
|
131363
|
-
|
|
131364
|
-
|
|
131365
|
-
|
|
131366
|
-
|
|
131367
|
-
|
|
131368
|
-
|
|
131369
|
-
|
|
131370
|
-
const rightFrames = this.renderRightStack(rightWidth, bodyHeight);
|
|
131371
|
-
const lines = [header];
|
|
131372
|
-
for (let i = 0; i < bodyHeight; i++) lines.push((listFrame[i] ?? " ".repeat(listWidth)) + (rightFrames[i] ?? " ".repeat(rightWidth)));
|
|
131373
|
-
lines.push(footer);
|
|
131374
|
-
return lines;
|
|
131375
|
-
}
|
|
131376
|
-
renderHeader(width) {
|
|
131377
|
-
const colors = this.props.colors;
|
|
131378
|
-
const title = chalk.hex(colors.primary).bold(" TASK BROWSER ");
|
|
131379
|
-
const filterText = chalk.hex(colors.textMuted)(` filter=${this.props.filter === "all" ? "ALL" : "ACTIVE"} `);
|
|
131380
|
-
const counts = countByStatus(this.props.tasks);
|
|
131381
|
-
const countSegments = [];
|
|
131382
|
-
if (counts.running > 0) countSegments.push(chalk.hex(colors.success)(` ${String(counts.running)} 运行中 `));
|
|
131383
|
-
if (counts.awaiting > 0) countSegments.push(chalk.hex(colors.warning)(` ${String(counts.awaiting)} 等待中 `));
|
|
131384
|
-
if (counts.completed > 0) countSegments.push(chalk.hex(colors.textDim)(` ${String(counts.completed)} 已完成 `));
|
|
131385
|
-
if (counts.terminalFailed > 0) countSegments.push(chalk.hex(colors.error)(` ${String(counts.terminalFailed)} 已中断 `));
|
|
131386
|
-
const totals = chalk.hex(colors.textMuted)(` ${String(this.props.tasks.length)} 总计 `);
|
|
131387
|
-
return fitExactly$1(title + filterText + countSegments.join("") + totals, width);
|
|
131240
|
+
flash(message, durationMs = 2500) {
|
|
131241
|
+
const browser = this.host.state.tasksBrowser;
|
|
131242
|
+
if (browser === void 0) return;
|
|
131243
|
+
if (browser.flashTimer !== void 0) clearTimeout(browser.flashTimer);
|
|
131244
|
+
browser.flashMessage = message;
|
|
131245
|
+
browser.flashTimer = setTimeout(() => {
|
|
131246
|
+
const current = this.host.state.tasksBrowser;
|
|
131247
|
+
if (current !== browser) return;
|
|
131248
|
+
current.flashMessage = void 0;
|
|
131249
|
+
current.flashTimer = void 0;
|
|
131250
|
+
this.repaint();
|
|
131251
|
+
}, durationMs);
|
|
131252
|
+
this.repaint();
|
|
131388
131253
|
}
|
|
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);
|
|
131254
|
+
closeOutputViewer() {
|
|
131255
|
+
const browser = this.host.state.tasksBrowser;
|
|
131256
|
+
if (browser === void 0 || browser.viewer === void 0) return;
|
|
131257
|
+
const viewer = browser.viewer;
|
|
131258
|
+
clearInterval(viewer.pollTimer);
|
|
131259
|
+
browser.viewer = void 0;
|
|
131260
|
+
this.host.state.ui.clear();
|
|
131261
|
+
for (const child of viewer.savedChildren) this.host.state.ui.addChild(child);
|
|
131262
|
+
this.host.state.ui.setFocus(browser.component);
|
|
131263
|
+
this.host.state.ui.requestRender(true);
|
|
131412
131264
|
}
|
|
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
|
-
|
|
131265
|
+
};
|
|
131266
|
+
//#endregion
|
|
131267
|
+
//#region src/tui/components/editor/file-mention-provider.ts
|
|
131268
|
+
/**
|
|
131269
|
+
* `@file` autocomplete provider for the input box.
|
|
131270
|
+
*
|
|
131271
|
+
* pi-tui's `CombinedAutocompleteProvider` handles the mechanical parts
|
|
131272
|
+
* (extract `@…` prefix, insert completion with the right quoting). This
|
|
131273
|
+
* wrapper adds scream-specific ranking + filtering so the default "empty
|
|
131274
|
+
* `@`" list surfaces files the user actually wants, not alphabetical
|
|
131275
|
+
* noise from `.agents/skills/*` et al.
|
|
131276
|
+
*
|
|
131277
|
+
* Sort order — empty query:
|
|
131278
|
+
* 1. recently edited (from `git log --name-only`)
|
|
131279
|
+
* 2. recent fs mtime
|
|
131280
|
+
* 3. basename alphabetical
|
|
131281
|
+
* (first 15, not 50 — pi-tui's menu height is ~6-10 lines anyway)
|
|
131282
|
+
*
|
|
131283
|
+
* Sort order — non-empty query (strict to fuzzy):
|
|
131284
|
+
* cat 0: basename starts-with query
|
|
131285
|
+
* cat 1: basename contains query
|
|
131286
|
+
* cat 2: fuzzyMatch succeeds on full path
|
|
131287
|
+
* tie-break within each cat: recency rank → mtime → basename length
|
|
131288
|
+
* (first 50)
|
|
131289
|
+
*
|
|
131290
|
+
* Filter — dot directories are hidden by default. User can opt in by starting the query
|
|
131291
|
+
* with `.` (e.g. `@.github/`), since those paths rarely need
|
|
131292
|
+
* completion.
|
|
131293
|
+
*
|
|
131294
|
+
* When `fd` is available the inner pi-tui provider owns the `@` branch
|
|
131295
|
+
* verbatim — its fd invocation respects `.gitignore` and is strictly
|
|
131296
|
+
* better than anything we can cheaply reproduce in TS. We only kick in
|
|
131297
|
+
* when `fd` is missing AND we're in a git repo.
|
|
131298
|
+
*/
|
|
131299
|
+
const MAX_SUGGESTIONS_WHEN_QUERY = 50;
|
|
131300
|
+
const MAX_SUGGESTIONS_WHEN_EMPTY = 15;
|
|
131301
|
+
const PATH_DELIMITERS = new Set([
|
|
131302
|
+
" ",
|
|
131303
|
+
" ",
|
|
131304
|
+
"\"",
|
|
131305
|
+
"'",
|
|
131306
|
+
"="
|
|
131307
|
+
]);
|
|
131308
|
+
var FileMentionProvider = class {
|
|
131309
|
+
fdPath;
|
|
131310
|
+
gitCache;
|
|
131311
|
+
inner;
|
|
131312
|
+
slashCommandItems;
|
|
131313
|
+
constructor(slashCommands, workDir, fdPath, gitCache) {
|
|
131314
|
+
this.fdPath = fdPath;
|
|
131315
|
+
this.gitCache = gitCache;
|
|
131316
|
+
this.slashCommandItems = slashCommands.map((cmd) => {
|
|
131317
|
+
const ac = cmd;
|
|
131318
|
+
if (ac.label !== void 0 && ac.label.length > 0) return {
|
|
131319
|
+
value: ac.value ?? ac.name ?? "",
|
|
131320
|
+
label: ac.label
|
|
131321
|
+
};
|
|
131322
|
+
const name = ac.value ?? ac.name ?? "";
|
|
131323
|
+
const desc = ac.description ?? "";
|
|
131324
|
+
return {
|
|
131325
|
+
value: name,
|
|
131326
|
+
label: `/${name}${desc ? ` — ${desc}` : ""}`
|
|
131327
|
+
};
|
|
131328
|
+
});
|
|
131329
|
+
this.inner = new CombinedAutocompleteProvider(slashCommands, workDir, fdPath);
|
|
131441
131330
|
}
|
|
131442
|
-
|
|
131443
|
-
const
|
|
131444
|
-
const
|
|
131445
|
-
if (
|
|
131446
|
-
const
|
|
131447
|
-
const
|
|
131448
|
-
|
|
131449
|
-
return
|
|
131331
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
131332
|
+
const textBeforeCursor = (lines[cursorLine] ?? "").slice(0, cursorCol);
|
|
131333
|
+
const atPrefix = extractAtPrefix(textBeforeCursor);
|
|
131334
|
+
if (!options.force && atPrefix === null && textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")) {
|
|
131335
|
+
const query = textBeforeCursor.slice(1);
|
|
131336
|
+
const filtered = fuzzyFilter(this.slashCommandItems, query, (item) => item.value).slice(0, MAX_SUGGESTIONS_WHEN_QUERY);
|
|
131337
|
+
if (filtered.length === 0) return null;
|
|
131338
|
+
return {
|
|
131339
|
+
items: filtered,
|
|
131340
|
+
prefix: textBeforeCursor
|
|
131341
|
+
};
|
|
131450
131342
|
}
|
|
131451
|
-
this.
|
|
131452
|
-
|
|
131453
|
-
const
|
|
131454
|
-
|
|
131455
|
-
const
|
|
131456
|
-
|
|
131457
|
-
|
|
131458
|
-
|
|
131343
|
+
if (atPrefix === null) return this.inner.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
131344
|
+
if (this.fdPath !== null) return this.inner.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
131345
|
+
const snapshot = this.gitCache.getSnapshot();
|
|
131346
|
+
if (snapshot === null || snapshot.files.length === 0) return this.inner.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
131347
|
+
const query = atPrefix.slice(1);
|
|
131348
|
+
const candidates = query.startsWith(".") ? snapshot.files : snapshot.files.filter((p) => !containsDotSegment(p));
|
|
131349
|
+
const items = query.length === 0 ? rankForEmptyQuery(candidates, snapshot) : rankForQuery(candidates, query, snapshot);
|
|
131350
|
+
if (items.length === 0) return this.inner.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
131351
|
+
return {
|
|
131352
|
+
items,
|
|
131353
|
+
prefix: atPrefix
|
|
131354
|
+
};
|
|
131355
|
+
}
|
|
131356
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
131357
|
+
if (prefix.startsWith("/")) {
|
|
131358
|
+
const line = lines[cursorLine] ?? "";
|
|
131359
|
+
const before = line.slice(0, cursorCol - prefix.length);
|
|
131360
|
+
const after = line.slice(cursorCol);
|
|
131361
|
+
const replacement = `/${item.value} `;
|
|
131362
|
+
const newLine = `${before}${replacement}${after}`;
|
|
131363
|
+
return {
|
|
131364
|
+
lines: [
|
|
131365
|
+
...lines.slice(0, cursorLine),
|
|
131366
|
+
newLine,
|
|
131367
|
+
...lines.slice(cursorLine + 1)
|
|
131368
|
+
],
|
|
131369
|
+
cursorLine,
|
|
131370
|
+
cursorCol: before.length + replacement.length
|
|
131371
|
+
};
|
|
131459
131372
|
}
|
|
131460
|
-
|
|
131461
|
-
return this.renderFrame(title, lines, width, height);
|
|
131373
|
+
return this.inner.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
131462
131374
|
}
|
|
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);
|
|
131375
|
+
};
|
|
131376
|
+
/**
|
|
131377
|
+
* Return the `@…` token ending at the cursor, or `null` if we're not in
|
|
131378
|
+
* an `@` mention. Mirrors pi-tui's `extractAtPrefix` — the token
|
|
131379
|
+
* boundary is the last PATH_DELIMITER before the cursor, and the token
|
|
131380
|
+
* must start with `@`.
|
|
131381
|
+
*/
|
|
131382
|
+
function extractAtPrefix(text) {
|
|
131383
|
+
let tokenStart = 0;
|
|
131384
|
+
for (let i = text.length - 1; i >= 0; i -= 1) if (PATH_DELIMITERS.has(text[i] ?? "")) {
|
|
131385
|
+
tokenStart = i + 1;
|
|
131386
|
+
break;
|
|
131477
131387
|
}
|
|
131478
|
-
|
|
131479
|
-
|
|
131480
|
-
|
|
131481
|
-
|
|
131482
|
-
|
|
131483
|
-
|
|
131484
|
-
|
|
131485
|
-
|
|
131486
|
-
|
|
131487
|
-
|
|
131388
|
+
if (text[tokenStart] !== "@") return null;
|
|
131389
|
+
return text.slice(tokenStart);
|
|
131390
|
+
}
|
|
131391
|
+
/** True when any path segment starts with a dot (e.g. `.github/x.yml`). */
|
|
131392
|
+
function containsDotSegment(path) {
|
|
131393
|
+
for (const segment of path.split("/")) if (segment.startsWith(".")) return true;
|
|
131394
|
+
return false;
|
|
131395
|
+
}
|
|
131396
|
+
/**
|
|
131397
|
+
* Empty-query ranking: stratified by signal strength.
|
|
131398
|
+
*
|
|
131399
|
+
* Layer 1: files touched in the last RECENT_COMMIT_DEPTH commits,
|
|
131400
|
+
* ordered by how recently. Strongest signal — if the user
|
|
131401
|
+
* just worked on it, they probably want to mention it.
|
|
131402
|
+
* Layer 2: files with the newest fs mtime (covers uncommitted edits
|
|
131403
|
+
* and files edited but not yet added to git).
|
|
131404
|
+
* Layer 3: everything else, alphabetical by basename so
|
|
131405
|
+
* README/package.json-style top-level files bubble up
|
|
131406
|
+
* relative to deeply-nested alphabetical paths.
|
|
131407
|
+
*
|
|
131408
|
+
* Cap at MAX_SUGGESTIONS_WHEN_EMPTY. Layers fill in order; dedup by
|
|
131409
|
+
* path so a recently-edited file isn't also listed in layer 2.
|
|
131410
|
+
*/
|
|
131411
|
+
function rankForEmptyQuery(files, snapshot) {
|
|
131412
|
+
const picked = /* @__PURE__ */ new Set();
|
|
131413
|
+
const result = [];
|
|
131414
|
+
const cap = MAX_SUGGESTIONS_WHEN_EMPTY;
|
|
131415
|
+
const inFiles = new Set(files);
|
|
131416
|
+
const byRecency = [...snapshot.recencyOrder.entries()].filter(([path]) => inFiles.has(path)).toSorted((a, b) => a[1] - b[1]);
|
|
131417
|
+
for (const [path] of byRecency) {
|
|
131418
|
+
if (result.length >= cap) break;
|
|
131419
|
+
if (picked.has(path)) continue;
|
|
131420
|
+
picked.add(path);
|
|
131421
|
+
result.push(path);
|
|
131488
131422
|
}
|
|
131489
|
-
|
|
131490
|
-
const
|
|
131491
|
-
const
|
|
131492
|
-
|
|
131423
|
+
if (result.length < cap) {
|
|
131424
|
+
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));
|
|
131425
|
+
for (const path of byMtime) {
|
|
131426
|
+
if (result.length >= cap) break;
|
|
131427
|
+
picked.add(path);
|
|
131428
|
+
result.push(path);
|
|
131429
|
+
}
|
|
131493
131430
|
}
|
|
131494
|
-
|
|
131495
|
-
const
|
|
131496
|
-
|
|
131497
|
-
|
|
131498
|
-
|
|
131499
|
-
const lines = [chalk.hex(colors.textMuted)("从列表中选择一个任务。")];
|
|
131500
|
-
while (lines.length < innerHeight) lines.push("");
|
|
131501
|
-
return this.renderFrame("详情", lines, width, height);
|
|
131431
|
+
if (result.length < cap) {
|
|
131432
|
+
const rest = files.filter((p) => !picked.has(p)).toSorted((a, b) => basename(a).localeCompare(basename(b)) || a.localeCompare(b));
|
|
131433
|
+
for (const path of rest) {
|
|
131434
|
+
if (result.length >= cap) break;
|
|
131435
|
+
result.push(path);
|
|
131502
131436
|
}
|
|
131503
|
-
const label = (text) => chalk.hex(colors.textMuted)(text.padEnd(14));
|
|
131504
|
-
const value = (text) => chalk.hex(colors.text)(text);
|
|
131505
|
-
const lines = [
|
|
131506
|
-
`${label("任务 ID:")}${value(task.taskId)}`,
|
|
131507
|
-
`${label("状态:")}${chalk.hex(statusColor(colors, task.status))(STATUS_LABEL[task.status])}`,
|
|
131508
|
-
`${label("描述:")}${value(singleLine$2(task.description) || "—")}`
|
|
131509
|
-
];
|
|
131510
|
-
if (task.command && task.command !== task.description) lines.push(`${label("命令:")}${value(singleLine$2(task.command))}`);
|
|
131511
|
-
const timing = task.status === "running" || task.status === "awaiting_approval" ? `运行中 ${formatRelativeTime$2(task.startedAt)}` : task.endedAt !== null && task.endedAt !== void 0 ? `已完成 ${formatRelativeTime$2(task.endedAt)}` : "";
|
|
131512
|
-
if (timing.length > 0) lines.push(`${label("时间:")}${chalk.hex(colors.textMuted)(timing)}`);
|
|
131513
|
-
if (task.pid > 0) lines.push(`${label("进程 ID:")}${chalk.hex(colors.textMuted)(String(task.pid))}`);
|
|
131514
|
-
if (task.exitCode !== null && task.exitCode !== void 0) lines.push(`${label("退出码:")}${chalk.hex(colors.textMuted)(String(task.exitCode))}`);
|
|
131515
|
-
if (task.stopReason !== void 0 && task.stopReason.length > 0) lines.push(`${label("停止原因:")}${chalk.hex(colors.textMuted)(task.stopReason)}`);
|
|
131516
|
-
if (task.timedOut === true) lines.push(`${label("已超时:")}${chalk.hex(colors.warning)("是")}`);
|
|
131517
|
-
if (task.approvalReason !== void 0 && task.approvalReason.length > 0) lines.push(`${label("等待中:")}${chalk.hex(colors.warning)(singleLine$2(task.approvalReason))}`);
|
|
131518
|
-
while (lines.length < innerHeight) lines.push("");
|
|
131519
|
-
return this.renderFrame("详情", lines, width, height);
|
|
131520
131437
|
}
|
|
131521
|
-
|
|
131522
|
-
|
|
131523
|
-
|
|
131524
|
-
|
|
131525
|
-
|
|
131526
|
-
|
|
131527
|
-
|
|
131438
|
+
return result.map(toItem);
|
|
131439
|
+
}
|
|
131440
|
+
/**
|
|
131441
|
+
* Non-empty-query ranking: three strictness tiers, with recency /
|
|
131442
|
+
* mtime as tie-breakers inside each tier so "the readme you just
|
|
131443
|
+
* edited" beats "a readme deep in a vendor dir".
|
|
131444
|
+
*/
|
|
131445
|
+
function rankForQuery(files, query, snapshot) {
|
|
131446
|
+
const lowerQuery = query.toLowerCase();
|
|
131447
|
+
const scored = [];
|
|
131448
|
+
for (const path of files) {
|
|
131449
|
+
const base = basename(path).toLowerCase();
|
|
131450
|
+
if (base.startsWith(lowerQuery)) {
|
|
131451
|
+
scored.push({
|
|
131452
|
+
path,
|
|
131453
|
+
cat: 0,
|
|
131454
|
+
fuzzyScore: 0
|
|
131455
|
+
});
|
|
131456
|
+
continue;
|
|
131528
131457
|
}
|
|
131529
|
-
|
|
131530
|
-
|
|
131531
|
-
|
|
131532
|
-
|
|
131533
|
-
|
|
131534
|
-
|
|
131535
|
-
|
|
131458
|
+
if (base.includes(lowerQuery)) {
|
|
131459
|
+
scored.push({
|
|
131460
|
+
path,
|
|
131461
|
+
cat: 1,
|
|
131462
|
+
fuzzyScore: 0
|
|
131463
|
+
});
|
|
131464
|
+
continue;
|
|
131465
|
+
}
|
|
131466
|
+
const fuzzy = fuzzyMatch(query, path);
|
|
131467
|
+
if (fuzzy.matches) scored.push({
|
|
131468
|
+
path,
|
|
131469
|
+
cat: 2,
|
|
131470
|
+
fuzzyScore: fuzzy.score
|
|
131471
|
+
});
|
|
131472
|
+
}
|
|
131473
|
+
if (scored.length === 0) return fuzzyFilter([...files], query, (p) => p).slice(0, MAX_SUGGESTIONS_WHEN_QUERY).map(toItem);
|
|
131474
|
+
scored.sort((a, b) => {
|
|
131475
|
+
if (a.cat !== b.cat) return a.cat - b.cat;
|
|
131476
|
+
if (a.cat === 2 && a.fuzzyScore !== b.fuzzyScore) return a.fuzzyScore - b.fuzzyScore;
|
|
131477
|
+
const ra = snapshot.recencyOrder.get(a.path);
|
|
131478
|
+
const rb = snapshot.recencyOrder.get(b.path);
|
|
131479
|
+
if (ra !== void 0 && rb !== void 0 && ra !== rb) return ra - rb;
|
|
131480
|
+
if (ra !== void 0 && rb === void 0) return -1;
|
|
131481
|
+
if (ra === void 0 && rb !== void 0) return 1;
|
|
131482
|
+
const ma = snapshot.mtimeByPath.get(a.path) ?? 0;
|
|
131483
|
+
const mb = snapshot.mtimeByPath.get(b.path) ?? 0;
|
|
131484
|
+
if (ma !== mb) return mb - ma;
|
|
131485
|
+
const baseLenDiff = basename(a.path).length - basename(b.path).length;
|
|
131486
|
+
if (baseLenDiff !== 0) return baseLenDiff;
|
|
131487
|
+
return a.path.localeCompare(b.path);
|
|
131488
|
+
});
|
|
131489
|
+
return scored.slice(0, MAX_SUGGESTIONS_WHEN_QUERY).map((entry) => toItem(entry.path));
|
|
131490
|
+
}
|
|
131491
|
+
function toItem(path) {
|
|
131492
|
+
return {
|
|
131493
|
+
value: `@${path}`,
|
|
131494
|
+
label: basename(path),
|
|
131495
|
+
description: path
|
|
131496
|
+
};
|
|
131497
|
+
}
|
|
131498
|
+
//#endregion
|
|
131499
|
+
//#region src/tui/components/messages/cron-message.ts
|
|
131500
|
+
var CronMessageComponent = class {
|
|
131501
|
+
colors;
|
|
131502
|
+
spacer = new Spacer(1);
|
|
131503
|
+
title;
|
|
131504
|
+
detail;
|
|
131505
|
+
titleColor;
|
|
131506
|
+
promptText;
|
|
131507
|
+
constructor(prompt, data, colors) {
|
|
131508
|
+
this.colors = colors;
|
|
131509
|
+
const missed = data.missedCount !== void 0;
|
|
131510
|
+
this.title = missed ? "错过的定时提醒" : "定时提醒触发";
|
|
131511
|
+
this.detail = cronDetail(data);
|
|
131512
|
+
this.titleColor = data.stale === true || missed ? colors.warning : colors.accent;
|
|
131513
|
+
this.promptText = new Text(chalk.hex(colors.text)(prompt), 0, 0);
|
|
131536
131514
|
}
|
|
131537
|
-
|
|
131515
|
+
invalidate() {
|
|
131516
|
+
this.promptText.invalidate();
|
|
131517
|
+
}
|
|
131518
|
+
render(width) {
|
|
131519
|
+
const bullet = chalk.hex(this.titleColor).bold(STATUS_BULLET);
|
|
131520
|
+
const bulletWidth = visibleWidth(bullet);
|
|
131521
|
+
const contentWidth = Math.max(1, width - bulletWidth);
|
|
131538
131522
|
const lines = [];
|
|
131539
|
-
const
|
|
131540
|
-
|
|
131541
|
-
|
|
131523
|
+
for (const line of this.spacer.render(width)) lines.push(line);
|
|
131524
|
+
const title = chalk.hex(this.titleColor).bold(this.title);
|
|
131525
|
+
lines.push(`${bullet}${title}`);
|
|
131526
|
+
if (this.detail !== void 0) lines.push(`${" ".repeat(bulletWidth)}${chalk.hex(this.colors.textDim)(this.detail)}`);
|
|
131527
|
+
const promptLines = this.promptText.render(contentWidth);
|
|
131528
|
+
for (const line of promptLines) lines.push(`${" ".repeat(bulletWidth)}${line}`);
|
|
131542
131529
|
return lines;
|
|
131543
131530
|
}
|
|
131544
131531
|
};
|
|
131532
|
+
function cronDetail(data) {
|
|
131533
|
+
const parts = [];
|
|
131534
|
+
if (data.cron !== void 0 && data.cron.length > 0) parts.push(data.cron);
|
|
131535
|
+
if (data.jobId !== void 0 && data.jobId.length > 0) parts.push(`job ${data.jobId}`);
|
|
131536
|
+
if (data.recurring === false) parts.push("一次性");
|
|
131537
|
+
if (data.coalescedCount !== void 0 && data.coalescedCount > 1) parts.push(`${String(data.coalescedCount)} 次合并触发`);
|
|
131538
|
+
if (data.missedCount !== void 0) parts.push(`${String(data.missedCount)} 次错过`);
|
|
131539
|
+
if (data.stale === true) parts.push("最终投递");
|
|
131540
|
+
return parts.length > 0 ? parts.join(" | ") : void 0;
|
|
131541
|
+
}
|
|
131545
131542
|
//#endregion
|
|
131546
|
-
//#region src/tui/
|
|
131547
|
-
var
|
|
131548
|
-
|
|
131549
|
-
|
|
131550
|
-
|
|
131551
|
-
|
|
131552
|
-
|
|
131553
|
-
|
|
131554
|
-
|
|
131555
|
-
|
|
131556
|
-
|
|
131557
|
-
|
|
131543
|
+
//#region src/tui/components/panes/activity-pane.ts
|
|
131544
|
+
var ActivityPaneComponent = class extends Container {
|
|
131545
|
+
constructor(options) {
|
|
131546
|
+
super();
|
|
131547
|
+
if (options.mode === "waiting" || options.mode === "tool") {
|
|
131548
|
+
if (options.spinner !== void 0) {
|
|
131549
|
+
this.addChild(new Spacer(1));
|
|
131550
|
+
this.addChild(options.spinner);
|
|
131551
|
+
}
|
|
131552
|
+
if (options.pulseWave !== void 0) {
|
|
131553
|
+
this.addChild(new Spacer(1));
|
|
131554
|
+
this.addChild(options.pulseWave);
|
|
131555
|
+
}
|
|
131558
131556
|
return;
|
|
131559
131557
|
}
|
|
131560
|
-
|
|
131561
|
-
|
|
131562
|
-
|
|
131563
|
-
|
|
131564
|
-
|
|
131565
|
-
|
|
131558
|
+
if (options.mode === "composing" && options.spinner !== void 0) {
|
|
131559
|
+
this.addChild(new Spacer(1));
|
|
131560
|
+
this.addChild(options.spinner);
|
|
131561
|
+
if (options.pulseWave !== void 0) {
|
|
131562
|
+
this.addChild(new Spacer(1));
|
|
131563
|
+
this.addChild(options.pulseWave);
|
|
131564
|
+
}
|
|
131566
131565
|
}
|
|
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
131566
|
}
|
|
131603
|
-
|
|
131604
|
-
|
|
131605
|
-
|
|
131606
|
-
|
|
131607
|
-
|
|
131608
|
-
|
|
131609
|
-
|
|
131610
|
-
|
|
131611
|
-
for (const
|
|
131612
|
-
|
|
131613
|
-
|
|
131614
|
-
|
|
131567
|
+
};
|
|
131568
|
+
//#endregion
|
|
131569
|
+
//#region src/tui/components/panes/queue-pane.ts
|
|
131570
|
+
var QueuePaneComponent = class extends Container {
|
|
131571
|
+
constructor(options) {
|
|
131572
|
+
super();
|
|
131573
|
+
const accent = chalk.hex(options.colors.accent);
|
|
131574
|
+
const dim = chalk.hex(options.colors.textDim);
|
|
131575
|
+
for (const item of options.messages) this.addChild(new Text(accent(` ❯ ${item.text}`), 0, 0));
|
|
131576
|
+
if (options.messages.length > 0) {
|
|
131577
|
+
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";
|
|
131578
|
+
this.addChild(new Text(dim(hint), 0, 0));
|
|
131579
|
+
}
|
|
131615
131580
|
}
|
|
131616
|
-
|
|
131617
|
-
|
|
131618
|
-
|
|
131619
|
-
|
|
131581
|
+
};
|
|
131582
|
+
//#endregion
|
|
131583
|
+
//#region src/tui/reverse-rpc/base-controller.ts
|
|
131584
|
+
var ReverseRpcController = class {
|
|
131585
|
+
uiHooks = null;
|
|
131586
|
+
current = null;
|
|
131587
|
+
queue = [];
|
|
131588
|
+
setUIHooks(hooks) {
|
|
131589
|
+
this.uiHooks = hooks;
|
|
131620
131590
|
}
|
|
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
|
-
}
|
|
131591
|
+
/**
|
|
131592
|
+
* Called when a reverse RPC request arrives from core. The returned promise
|
|
131593
|
+
* resolves after the user responds or `cancelAll` forces cancellation.
|
|
131594
|
+
*/
|
|
131595
|
+
show(payload) {
|
|
131596
|
+
return new Promise((resolve) => {
|
|
131597
|
+
const entry = {
|
|
131598
|
+
payload,
|
|
131599
|
+
resolve
|
|
131600
|
+
};
|
|
131601
|
+
if (this.current === null) {
|
|
131602
|
+
this.current = entry;
|
|
131603
|
+
this.uiHooks?.showPanel(payload);
|
|
131604
|
+
} else this.queue.push(entry);
|
|
131652
131605
|
});
|
|
131653
|
-
state.ui.requestRender();
|
|
131654
131606
|
}
|
|
131655
|
-
|
|
131656
|
-
|
|
131657
|
-
|
|
131658
|
-
|
|
131607
|
+
/** Called by the UI after the user makes a panel choice. */
|
|
131608
|
+
respond(data) {
|
|
131609
|
+
const pending = this.current;
|
|
131610
|
+
this.current = null;
|
|
131611
|
+
pending?.resolve(data);
|
|
131612
|
+
if (pending !== null) this.drainAutoResolved(pending.payload, data);
|
|
131613
|
+
this.advanceOrHide();
|
|
131659
131614
|
}
|
|
131660
|
-
|
|
131661
|
-
|
|
131662
|
-
const
|
|
131663
|
-
|
|
131664
|
-
|
|
131665
|
-
|
|
131666
|
-
|
|
131667
|
-
|
|
131668
|
-
|
|
131669
|
-
|
|
131670
|
-
|
|
131615
|
+
/** Cancels all pending requests during shutdown or session switches. */
|
|
131616
|
+
cancelAll(reason) {
|
|
131617
|
+
const all = [...this.current === null ? [] : [this.current], ...this.queue];
|
|
131618
|
+
this.current = null;
|
|
131619
|
+
this.queue = [];
|
|
131620
|
+
this.uiHooks?.hidePanel();
|
|
131621
|
+
for (const entry of all) entry.resolve(this.createCancelResponse(reason));
|
|
131622
|
+
}
|
|
131623
|
+
hasPending() {
|
|
131624
|
+
return this.current !== null || this.queue.length > 0;
|
|
131625
|
+
}
|
|
131626
|
+
advanceOrHide() {
|
|
131627
|
+
const next = this.queue.shift();
|
|
131628
|
+
if (next === void 0) {
|
|
131629
|
+
this.uiHooks?.hidePanel();
|
|
131671
131630
|
return;
|
|
131672
131631
|
}
|
|
131673
|
-
|
|
131674
|
-
this.
|
|
131632
|
+
this.current = next;
|
|
131633
|
+
this.uiHooks?.showPanel(next.payload);
|
|
131675
131634
|
}
|
|
131676
|
-
|
|
131677
|
-
const
|
|
131678
|
-
|
|
131679
|
-
|
|
131680
|
-
|
|
131681
|
-
|
|
131682
|
-
|
|
131683
|
-
|
|
131684
|
-
|
|
131685
|
-
|
|
131686
|
-
|
|
131687
|
-
|
|
131688
|
-
|
|
131689
|
-
|
|
131635
|
+
drainAutoResolved(resolvedPayload, response) {
|
|
131636
|
+
const remaining = [];
|
|
131637
|
+
for (const entry of this.queue) {
|
|
131638
|
+
const auto = this.autoResolveFor(resolvedPayload, response, entry.payload);
|
|
131639
|
+
if (auto === void 0) remaining.push(entry);
|
|
131640
|
+
else entry.resolve(auto);
|
|
131641
|
+
}
|
|
131642
|
+
this.queue = remaining;
|
|
131643
|
+
}
|
|
131644
|
+
/**
|
|
131645
|
+
* Subclasses override to short-circuit queued requests when an answer to the
|
|
131646
|
+
* just-resolved one (e.g. an approve-for-session) implies the same answer
|
|
131647
|
+
* for matching queued requests. Return `undefined` to leave the queued
|
|
131648
|
+
* request waiting for its own panel turn.
|
|
131649
|
+
*/
|
|
131650
|
+
autoResolveFor(_resolvedPayload, _response, _queuedPayload) {}
|
|
131651
|
+
};
|
|
131652
|
+
//#endregion
|
|
131653
|
+
//#region src/tui/reverse-rpc/approval/controller.ts
|
|
131654
|
+
var ApprovalController = class extends ReverseRpcController {
|
|
131655
|
+
createCancelResponse(reason) {
|
|
131656
|
+
return {
|
|
131657
|
+
decision: "cancelled",
|
|
131658
|
+
feedback: reason
|
|
131659
|
+
};
|
|
131690
131660
|
}
|
|
131691
|
-
|
|
131661
|
+
autoResolveFor(resolvedPayload, response, queuedPayload) {
|
|
131662
|
+
if (response.decision !== "approved") return void 0;
|
|
131663
|
+
if (response.scope !== "session") return void 0;
|
|
131664
|
+
if (resolvedPayload.action !== queuedPayload.action) return void 0;
|
|
131692
131665
|
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
|
-
}
|
|
131666
|
+
decision: "approved",
|
|
131667
|
+
scope: "session"
|
|
131714
131668
|
};
|
|
131715
131669
|
}
|
|
131716
|
-
|
|
131717
|
-
|
|
131718
|
-
|
|
131719
|
-
|
|
131720
|
-
|
|
131721
|
-
|
|
131722
|
-
|
|
131723
|
-
|
|
131724
|
-
this.
|
|
131670
|
+
};
|
|
131671
|
+
//#endregion
|
|
131672
|
+
//#region src/tui/reverse-rpc/modal-coordinator.ts
|
|
131673
|
+
var ReverseRpcModalCoordinator = class {
|
|
131674
|
+
hooks;
|
|
131675
|
+
active = null;
|
|
131676
|
+
queued = [];
|
|
131677
|
+
constructor(hooks) {
|
|
131678
|
+
this.hooks = hooks;
|
|
131725
131679
|
}
|
|
131726
|
-
|
|
131727
|
-
|
|
131728
|
-
|
|
131729
|
-
|
|
131730
|
-
|
|
131680
|
+
showApproval(payload) {
|
|
131681
|
+
this.show({
|
|
131682
|
+
owner: "approval",
|
|
131683
|
+
show: () => {
|
|
131684
|
+
this.hooks.showApprovalPanel(payload);
|
|
131685
|
+
},
|
|
131686
|
+
hide: () => {
|
|
131687
|
+
this.hooks.hideApprovalPanel();
|
|
131688
|
+
}
|
|
131689
|
+
});
|
|
131731
131690
|
}
|
|
131732
|
-
|
|
131733
|
-
this.
|
|
131734
|
-
|
|
131691
|
+
showQuestion(payload) {
|
|
131692
|
+
this.show({
|
|
131693
|
+
owner: "question",
|
|
131694
|
+
show: () => {
|
|
131695
|
+
this.hooks.showQuestionDialog(payload);
|
|
131696
|
+
},
|
|
131697
|
+
hide: () => {
|
|
131698
|
+
this.hooks.hideQuestionDialog();
|
|
131699
|
+
}
|
|
131700
|
+
});
|
|
131735
131701
|
}
|
|
131736
|
-
|
|
131737
|
-
if (this.
|
|
131738
|
-
|
|
131739
|
-
|
|
131740
|
-
|
|
131702
|
+
hide(owner) {
|
|
131703
|
+
if (this.active?.owner === owner) {
|
|
131704
|
+
const active = this.active;
|
|
131705
|
+
this.active = null;
|
|
131706
|
+
active.hide();
|
|
131707
|
+
this.showNext();
|
|
131741
131708
|
return;
|
|
131742
131709
|
}
|
|
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
|
-
}
|
|
131710
|
+
const queuedIndex = this.queued.findIndex((entry) => entry.owner === owner);
|
|
131711
|
+
if (queuedIndex >= 0) this.queued.splice(queuedIndex, 1);
|
|
131751
131712
|
}
|
|
131752
|
-
|
|
131753
|
-
const
|
|
131754
|
-
|
|
131755
|
-
|
|
131756
|
-
|
|
131757
|
-
|
|
131758
|
-
|
|
131759
|
-
|
|
131713
|
+
clear() {
|
|
131714
|
+
const active = this.active;
|
|
131715
|
+
this.active = null;
|
|
131716
|
+
this.queued.length = 0;
|
|
131717
|
+
active?.hide();
|
|
131718
|
+
}
|
|
131719
|
+
show(entry) {
|
|
131720
|
+
const active = this.active;
|
|
131721
|
+
if (active === null) {
|
|
131722
|
+
this.active = entry;
|
|
131723
|
+
entry.show();
|
|
131760
131724
|
return;
|
|
131761
131725
|
}
|
|
131762
|
-
|
|
131763
|
-
|
|
131764
|
-
|
|
131765
|
-
} catch (error) {
|
|
131766
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
131767
|
-
this.flash(`无法打开输出: ${message}`);
|
|
131726
|
+
if (active.owner === entry.owner) {
|
|
131727
|
+
this.active = entry;
|
|
131728
|
+
entry.show();
|
|
131768
131729
|
return;
|
|
131769
131730
|
}
|
|
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();
|
|
131731
|
+
const queuedIndex = this.queued.findIndex((queued) => queued.owner === entry.owner);
|
|
131732
|
+
if (queuedIndex >= 0) {
|
|
131733
|
+
this.queued[queuedIndex] = entry;
|
|
131806
131734
|
return;
|
|
131807
131735
|
}
|
|
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
|
-
});
|
|
131736
|
+
this.queued.push(entry);
|
|
131826
131737
|
}
|
|
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();
|
|
131738
|
+
showNext() {
|
|
131739
|
+
const next = this.queued.shift();
|
|
131740
|
+
if (next === void 0) return;
|
|
131741
|
+
this.active = next;
|
|
131742
|
+
next.show();
|
|
131840
131743
|
}
|
|
131841
|
-
|
|
131842
|
-
|
|
131843
|
-
|
|
131844
|
-
|
|
131845
|
-
|
|
131846
|
-
|
|
131847
|
-
|
|
131848
|
-
|
|
131849
|
-
|
|
131850
|
-
|
|
131744
|
+
};
|
|
131745
|
+
//#endregion
|
|
131746
|
+
//#region src/tui/reverse-rpc/index.ts
|
|
131747
|
+
function registerReverseRPCHandlers(approvalController, questionController, uiHooks) {
|
|
131748
|
+
const modalCoordinator = new ReverseRpcModalCoordinator(uiHooks);
|
|
131749
|
+
approvalController.setUIHooks({
|
|
131750
|
+
showPanel: (payload) => {
|
|
131751
|
+
modalCoordinator.showApproval(payload);
|
|
131752
|
+
},
|
|
131753
|
+
hidePanel: () => {
|
|
131754
|
+
modalCoordinator.hide("approval");
|
|
131755
|
+
}
|
|
131756
|
+
});
|
|
131757
|
+
questionController.setUIHooks({
|
|
131758
|
+
showPanel: (payload) => {
|
|
131759
|
+
modalCoordinator.showQuestion(payload);
|
|
131760
|
+
},
|
|
131761
|
+
hidePanel: () => {
|
|
131762
|
+
modalCoordinator.hide("question");
|
|
131763
|
+
}
|
|
131764
|
+
});
|
|
131765
|
+
return [() => {
|
|
131766
|
+
modalCoordinator.clear();
|
|
131767
|
+
}];
|
|
131768
|
+
}
|
|
131769
|
+
//#endregion
|
|
131770
|
+
//#region src/tui/reverse-rpc/question/controller.ts
|
|
131771
|
+
var QuestionController = class extends ReverseRpcController {
|
|
131772
|
+
createCancelResponse(_reason) {
|
|
131773
|
+
return { answers: [] };
|
|
131851
131774
|
}
|
|
131852
131775
|
};
|
|
131853
131776
|
//#endregion
|
|
131854
|
-
//#region src/tui/
|
|
131777
|
+
//#region src/tui/theme/bundle.ts
|
|
131778
|
+
function createScreamTUIThemeBundle(theme, resolvedTheme) {
|
|
131779
|
+
const actualTheme = resolvedTheme ?? resolveThemeSync(theme);
|
|
131780
|
+
const colors = { ...getColorPalette(actualTheme) };
|
|
131781
|
+
return {
|
|
131782
|
+
resolvedTheme: actualTheme,
|
|
131783
|
+
colors,
|
|
131784
|
+
styles: createThemeStyles(colors),
|
|
131785
|
+
markdownTheme: createMarkdownTheme(colors)
|
|
131786
|
+
};
|
|
131787
|
+
}
|
|
131788
|
+
//#endregion
|
|
131789
|
+
//#region src/tui/types.ts
|
|
131790
|
+
const INITIAL_LIVE_PANE = {
|
|
131791
|
+
mode: "idle",
|
|
131792
|
+
pendingApproval: null,
|
|
131793
|
+
pendingQuestion: null
|
|
131794
|
+
};
|
|
131795
|
+
//#endregion
|
|
131796
|
+
//#region src/utils/git/git-status.ts
|
|
131855
131797
|
/**
|
|
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.
|
|
131798
|
+
* Cached git branch + working-tree status for the footer/statusline.
|
|
131880
131799
|
*
|
|
131881
|
-
*
|
|
131882
|
-
*
|
|
131883
|
-
*
|
|
131884
|
-
*
|
|
131800
|
+
* Branch name refreshes every 5s, porcelain status every 15s. Branch
|
|
131801
|
+
* and status reads stay synchronous with short timeouts. Pull request
|
|
131802
|
+
* lookup uses an async cache so a slow `gh pr view` never blocks
|
|
131803
|
+
* footer rendering.
|
|
131885
131804
|
*/
|
|
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
|
-
|
|
131805
|
+
const BRANCH_TTL_MS = 5e3;
|
|
131806
|
+
const STATUS_TTL_MS = 15e3;
|
|
131807
|
+
const PULL_REQUEST_TTL_MS = 6e4;
|
|
131808
|
+
const SPAWN_TIMEOUT_MS = 500;
|
|
131809
|
+
const PR_SPAWN_TIMEOUT_MS = 5e3;
|
|
131810
|
+
const AHEAD_BEHIND_RE = /\[(?:ahead (\d+))?(?:, )?(?:behind (\d+))?\]/;
|
|
131811
|
+
function createGitStatusCache(workDir, options = {}) {
|
|
131812
|
+
const isRepo = detectGitRepo(workDir);
|
|
131813
|
+
let branch = {
|
|
131814
|
+
value: null,
|
|
131815
|
+
fetchedAt: 0
|
|
131816
|
+
};
|
|
131817
|
+
let status = {
|
|
131818
|
+
dirty: false,
|
|
131819
|
+
ahead: 0,
|
|
131820
|
+
behind: 0,
|
|
131821
|
+
diffAdded: 0,
|
|
131822
|
+
diffDeleted: 0,
|
|
131823
|
+
fetchedAt: 0
|
|
131824
|
+
};
|
|
131825
|
+
let pullRequest = {
|
|
131826
|
+
value: null,
|
|
131827
|
+
branch: null,
|
|
131828
|
+
fetchedAt: 0,
|
|
131829
|
+
pendingBranch: null,
|
|
131830
|
+
requestId: 0
|
|
131831
|
+
};
|
|
131832
|
+
return { getStatus: () => {
|
|
131833
|
+
if (!isRepo) return null;
|
|
131834
|
+
const now = Date.now();
|
|
131835
|
+
if (now - branch.fetchedAt >= BRANCH_TTL_MS) branch = {
|
|
131836
|
+
value: readBranch(workDir),
|
|
131837
|
+
fetchedAt: now
|
|
131838
|
+
};
|
|
131839
|
+
if (branch.value === null) return null;
|
|
131840
|
+
if (now - status.fetchedAt >= STATUS_TTL_MS) status = {
|
|
131841
|
+
...readStatus(workDir),
|
|
131842
|
+
fetchedAt: now
|
|
131843
|
+
};
|
|
131844
|
+
refreshPullRequestIfNeeded(branch.value, now);
|
|
131845
|
+
return {
|
|
131846
|
+
branch: branch.value,
|
|
131847
|
+
dirty: status.dirty,
|
|
131848
|
+
ahead: status.ahead,
|
|
131849
|
+
behind: status.behind,
|
|
131850
|
+
diffAdded: status.diffAdded,
|
|
131851
|
+
diffDeleted: status.diffDeleted,
|
|
131852
|
+
pullRequest: pullRequest.branch === branch.value ? pullRequest.value : null
|
|
131853
|
+
};
|
|
131854
|
+
} };
|
|
131855
|
+
function refreshPullRequestIfNeeded(branchName, now) {
|
|
131856
|
+
if (pullRequest.pendingBranch === branchName) return;
|
|
131857
|
+
const fetchedAt = pullRequest.branch === branchName ? pullRequest.fetchedAt : 0;
|
|
131858
|
+
if (now - fetchedAt < PULL_REQUEST_TTL_MS) return;
|
|
131859
|
+
const requestId = pullRequest.requestId + 1;
|
|
131860
|
+
pullRequest = {
|
|
131861
|
+
value: pullRequest.branch === branchName ? pullRequest.value : null,
|
|
131862
|
+
branch: branchName,
|
|
131863
|
+
fetchedAt,
|
|
131864
|
+
pendingBranch: branchName,
|
|
131865
|
+
requestId
|
|
131866
|
+
};
|
|
131867
|
+
readPullRequest(workDir).then((value) => {
|
|
131868
|
+
if (pullRequest.requestId !== requestId) return;
|
|
131869
|
+
const changed = !samePullRequest(pullRequest.branch === branchName ? pullRequest.value : null, value);
|
|
131870
|
+
pullRequest = {
|
|
131871
|
+
value,
|
|
131872
|
+
branch: branchName,
|
|
131873
|
+
fetchedAt: Date.now(),
|
|
131874
|
+
pendingBranch: null,
|
|
131875
|
+
requestId
|
|
131914
131876
|
};
|
|
131877
|
+
if (changed) options.onChange?.();
|
|
131915
131878
|
});
|
|
131916
|
-
this.inner = new CombinedAutocompleteProvider(slashCommands, workDir, fdPath);
|
|
131917
131879
|
}
|
|
131918
|
-
|
|
131919
|
-
|
|
131920
|
-
|
|
131921
|
-
|
|
131922
|
-
|
|
131923
|
-
|
|
131924
|
-
|
|
131925
|
-
|
|
131926
|
-
|
|
131927
|
-
|
|
131928
|
-
|
|
131929
|
-
}
|
|
131930
|
-
|
|
131931
|
-
|
|
131932
|
-
|
|
131933
|
-
|
|
131934
|
-
|
|
131935
|
-
|
|
131936
|
-
|
|
131937
|
-
|
|
131880
|
+
}
|
|
131881
|
+
function detectGitRepo(workDir) {
|
|
131882
|
+
try {
|
|
131883
|
+
const result = spawnSync("git", [
|
|
131884
|
+
"-C",
|
|
131885
|
+
workDir,
|
|
131886
|
+
"rev-parse",
|
|
131887
|
+
"--is-inside-work-tree"
|
|
131888
|
+
], {
|
|
131889
|
+
encoding: "utf8",
|
|
131890
|
+
timeout: SPAWN_TIMEOUT_MS
|
|
131891
|
+
});
|
|
131892
|
+
return result.status === 0 && result.stdout.trim() === "true";
|
|
131893
|
+
} catch {
|
|
131894
|
+
return false;
|
|
131895
|
+
}
|
|
131896
|
+
}
|
|
131897
|
+
function readBranch(workDir) {
|
|
131898
|
+
try {
|
|
131899
|
+
const result = spawnSync("git", [
|
|
131900
|
+
"-C",
|
|
131901
|
+
workDir,
|
|
131902
|
+
"branch",
|
|
131903
|
+
"--show-current"
|
|
131904
|
+
], {
|
|
131905
|
+
encoding: "utf8",
|
|
131906
|
+
timeout: SPAWN_TIMEOUT_MS
|
|
131907
|
+
});
|
|
131908
|
+
if (result.status !== 0) return null;
|
|
131909
|
+
const name = result.stdout.trim();
|
|
131910
|
+
return name.length > 0 ? name : null;
|
|
131911
|
+
} catch {
|
|
131912
|
+
return null;
|
|
131913
|
+
}
|
|
131914
|
+
}
|
|
131915
|
+
function readStatus(workDir) {
|
|
131916
|
+
try {
|
|
131917
|
+
const result = spawnSync("git", [
|
|
131918
|
+
"-C",
|
|
131919
|
+
workDir,
|
|
131920
|
+
"status",
|
|
131921
|
+
"--porcelain",
|
|
131922
|
+
"-b"
|
|
131923
|
+
], {
|
|
131924
|
+
encoding: "utf8",
|
|
131925
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
131926
|
+
maxBuffer: 4 * 1024 * 1024
|
|
131927
|
+
});
|
|
131928
|
+
if (result.status !== 0) return {
|
|
131929
|
+
dirty: false,
|
|
131930
|
+
ahead: 0,
|
|
131931
|
+
behind: 0,
|
|
131932
|
+
diffAdded: 0,
|
|
131933
|
+
diffDeleted: 0
|
|
131934
|
+
};
|
|
131935
|
+
let dirty = false;
|
|
131936
|
+
let ahead = 0;
|
|
131937
|
+
let behind = 0;
|
|
131938
|
+
for (const line of result.stdout.split("\n")) if (line.startsWith("## ")) {
|
|
131939
|
+
const m = AHEAD_BEHIND_RE.exec(line);
|
|
131940
|
+
if (m) {
|
|
131941
|
+
ahead = Number.parseInt(m[1] ?? "0", 10) || 0;
|
|
131942
|
+
behind = Number.parseInt(m[2] ?? "0", 10) || 0;
|
|
131943
|
+
}
|
|
131944
|
+
} else if (line.trim().length > 0) dirty = true;
|
|
131945
|
+
const diff = dirty ? readDiffStats(workDir) : {
|
|
131946
|
+
added: 0,
|
|
131947
|
+
deleted: 0
|
|
131948
|
+
};
|
|
131938
131949
|
return {
|
|
131939
|
-
|
|
131940
|
-
|
|
131950
|
+
dirty,
|
|
131951
|
+
ahead,
|
|
131952
|
+
behind,
|
|
131953
|
+
diffAdded: diff.added,
|
|
131954
|
+
diffDeleted: diff.deleted
|
|
131955
|
+
};
|
|
131956
|
+
} catch {
|
|
131957
|
+
return {
|
|
131958
|
+
dirty: false,
|
|
131959
|
+
ahead: 0,
|
|
131960
|
+
behind: 0,
|
|
131961
|
+
diffAdded: 0,
|
|
131962
|
+
diffDeleted: 0
|
|
131941
131963
|
};
|
|
131942
131964
|
}
|
|
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
131965
|
}
|
|
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);
|
|
131966
|
+
function readDiffStats(workDir) {
|
|
131967
|
+
try {
|
|
131968
|
+
const result = spawnSync("git", [
|
|
131969
|
+
"-C",
|
|
131970
|
+
workDir,
|
|
131971
|
+
"diff",
|
|
131972
|
+
"--numstat",
|
|
131973
|
+
"HEAD",
|
|
131974
|
+
"--"
|
|
131975
|
+
], {
|
|
131976
|
+
encoding: "utf8",
|
|
131977
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
131978
|
+
maxBuffer: 4 * 1024 * 1024
|
|
131979
|
+
});
|
|
131980
|
+
if (result.status !== 0) return {
|
|
131981
|
+
added: 0,
|
|
131982
|
+
deleted: 0
|
|
131983
|
+
};
|
|
131984
|
+
let added = 0;
|
|
131985
|
+
let deleted = 0;
|
|
131986
|
+
for (const line of result.stdout.split("\n")) {
|
|
131987
|
+
if (!line) continue;
|
|
131988
|
+
const [addedText, deletedText] = line.split(" ");
|
|
131989
|
+
added += parseDiffNumstatCount(addedText);
|
|
131990
|
+
deleted += parseDiffNumstatCount(deletedText);
|
|
132023
131991
|
}
|
|
131992
|
+
return {
|
|
131993
|
+
added,
|
|
131994
|
+
deleted
|
|
131995
|
+
};
|
|
131996
|
+
} catch {
|
|
131997
|
+
return {
|
|
131998
|
+
added: 0,
|
|
131999
|
+
deleted: 0
|
|
132000
|
+
};
|
|
132024
132001
|
}
|
|
132025
|
-
return result.map(toItem);
|
|
132026
132002
|
}
|
|
132027
|
-
|
|
132028
|
-
|
|
132029
|
-
|
|
132030
|
-
|
|
132031
|
-
|
|
132032
|
-
function
|
|
132033
|
-
|
|
132034
|
-
|
|
132035
|
-
|
|
132036
|
-
|
|
132037
|
-
|
|
132038
|
-
|
|
132039
|
-
|
|
132040
|
-
|
|
132041
|
-
|
|
132042
|
-
|
|
132043
|
-
|
|
132044
|
-
|
|
132045
|
-
|
|
132046
|
-
|
|
132047
|
-
|
|
132048
|
-
|
|
132049
|
-
|
|
132003
|
+
function parseDiffNumstatCount(value) {
|
|
132004
|
+
if (value === void 0 || value === "-") return 0;
|
|
132005
|
+
const n = Number.parseInt(value, 10);
|
|
132006
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
132007
|
+
}
|
|
132008
|
+
function readPullRequest(workDir) {
|
|
132009
|
+
return new Promise((resolve) => {
|
|
132010
|
+
try {
|
|
132011
|
+
execFile("gh", [
|
|
132012
|
+
"pr",
|
|
132013
|
+
"view",
|
|
132014
|
+
"--json",
|
|
132015
|
+
"number,url"
|
|
132016
|
+
], {
|
|
132017
|
+
cwd: workDir,
|
|
132018
|
+
encoding: "utf8",
|
|
132019
|
+
env: {
|
|
132020
|
+
...process.env,
|
|
132021
|
+
GH_NO_UPDATE_NOTIFIER: "1",
|
|
132022
|
+
GH_PROMPT_DISABLED: "1"
|
|
132023
|
+
},
|
|
132024
|
+
timeout: PR_SPAWN_TIMEOUT_MS,
|
|
132025
|
+
maxBuffer: 256 * 1024
|
|
132026
|
+
}, (error, stdout) => {
|
|
132027
|
+
if (error !== null) {
|
|
132028
|
+
resolve(null);
|
|
132029
|
+
return;
|
|
132030
|
+
}
|
|
132031
|
+
resolve(parsePullRequest(stdout));
|
|
132050
132032
|
});
|
|
132051
|
-
|
|
132033
|
+
} catch {
|
|
132034
|
+
resolve(null);
|
|
132052
132035
|
}
|
|
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
132036
|
});
|
|
132076
|
-
return scored.slice(0, MAX_SUGGESTIONS_WHEN_QUERY).map((entry) => toItem(entry.path));
|
|
132077
132037
|
}
|
|
132078
|
-
function
|
|
132079
|
-
return
|
|
132080
|
-
|
|
132081
|
-
label: basename(path),
|
|
132082
|
-
description: path
|
|
132083
|
-
};
|
|
132038
|
+
function samePullRequest(a, b) {
|
|
132039
|
+
if (a === null || b === null) return a === b;
|
|
132040
|
+
return a.number === b.number && a.url === b.url;
|
|
132084
132041
|
}
|
|
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);
|
|
132042
|
+
function parsePullRequest(stdout) {
|
|
132043
|
+
try {
|
|
132044
|
+
const raw = JSON.parse(stdout);
|
|
132045
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
132046
|
+
const record = raw;
|
|
132047
|
+
const number = record["number"];
|
|
132048
|
+
const url = record["url"];
|
|
132049
|
+
if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) return null;
|
|
132050
|
+
if (typeof url !== "string" || !isSafeHttpUrl(url)) return null;
|
|
132051
|
+
return {
|
|
132052
|
+
number,
|
|
132053
|
+
url
|
|
132054
|
+
};
|
|
132055
|
+
} catch {
|
|
132056
|
+
return null;
|
|
132101
132057
|
}
|
|
132102
|
-
|
|
132103
|
-
|
|
132058
|
+
}
|
|
132059
|
+
function isSafeHttpUrl(value) {
|
|
132060
|
+
if (hasControlChars(value)) return false;
|
|
132061
|
+
try {
|
|
132062
|
+
const url = new URL(value);
|
|
132063
|
+
return url.protocol === "https:" || url.protocol === "http:";
|
|
132064
|
+
} catch {
|
|
132065
|
+
return false;
|
|
132104
132066
|
}
|
|
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;
|
|
132067
|
+
}
|
|
132068
|
+
function hasControlChars(value) {
|
|
132069
|
+
for (const char of value) {
|
|
132070
|
+
const code = char.codePointAt(0) ?? 0;
|
|
132071
|
+
if (code <= 31 || code === 127) return true;
|
|
132117
132072
|
}
|
|
132118
|
-
|
|
132119
|
-
|
|
132073
|
+
return false;
|
|
132074
|
+
}
|
|
132075
|
+
function formatGitBadgeBase(status) {
|
|
132120
132076
|
const parts = [];
|
|
132121
|
-
|
|
132122
|
-
if (
|
|
132123
|
-
|
|
132124
|
-
if (
|
|
132125
|
-
if (
|
|
132126
|
-
if (
|
|
132127
|
-
return parts.length
|
|
132077
|
+
const diff = formatDiffStats(status);
|
|
132078
|
+
if (diff) parts.push(diff);
|
|
132079
|
+
let sync = "";
|
|
132080
|
+
if (status.ahead > 0) sync += `↑${status.ahead}`;
|
|
132081
|
+
if (status.behind > 0) sync += `↓${status.behind}`;
|
|
132082
|
+
if (sync) parts.push(sync);
|
|
132083
|
+
return parts.length === 0 ? status.branch : `${status.branch} [${parts.join(" ")}]`;
|
|
132084
|
+
}
|
|
132085
|
+
function formatPullRequestBadge(pullRequest, options = {}) {
|
|
132086
|
+
const prText = `[PR#${String(pullRequest.number)}]`;
|
|
132087
|
+
return options.linkPullRequest ? toTerminalHyperlink(prText, pullRequest.url) : prText;
|
|
132088
|
+
}
|
|
132089
|
+
function formatDiffStats(status) {
|
|
132090
|
+
const parts = [];
|
|
132091
|
+
if (status.diffAdded > 0) parts.push(`+${String(status.diffAdded)}`);
|
|
132092
|
+
if (status.diffDeleted > 0) parts.push(`-${String(status.diffDeleted)}`);
|
|
132093
|
+
if (parts.length > 0) return parts.join(" ");
|
|
132094
|
+
return status.dirty ? "±" : null;
|
|
132095
|
+
}
|
|
132096
|
+
function toTerminalHyperlink(text, url) {
|
|
132097
|
+
if (!isSafeHttpUrl(url)) return text;
|
|
132098
|
+
return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`;
|
|
132128
132099
|
}
|
|
132129
132100
|
//#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
|
-
|
|
132101
|
+
//#region src/tui/components/chrome/footer.ts
|
|
132102
|
+
const MAX_CWD_SEGMENTS = 3;
|
|
132103
|
+
const TOOLBAR_TIPS = [
|
|
132104
|
+
{ text: "shift+tab: 计划模式" },
|
|
132105
|
+
{ text: "/model: 切换模型" },
|
|
132106
|
+
{
|
|
132107
|
+
text: "ctrl+s: 中途干预",
|
|
132108
|
+
priority: 2
|
|
132109
|
+
},
|
|
132110
|
+
{
|
|
132111
|
+
text: "/compact: 压缩上下文",
|
|
132112
|
+
priority: 2
|
|
132113
|
+
},
|
|
132114
|
+
{ text: "ctrl+o: 展开工具输出" },
|
|
132115
|
+
{ text: "/tasks: 后台任务" },
|
|
132116
|
+
{ text: "shift+enter: 换行" },
|
|
132117
|
+
{
|
|
132118
|
+
text: "/init: 生成 AGENTS.md",
|
|
132119
|
+
priority: 2
|
|
132120
|
+
},
|
|
132121
|
+
{ text: "@: 提及文件" },
|
|
132122
|
+
{ text: "ctrl+c: 取消" },
|
|
132123
|
+
{ text: "/theme: 切换主题" },
|
|
132124
|
+
{ text: "/auto: 自动权限模式" },
|
|
132125
|
+
{ text: "/yes: 自动批准" },
|
|
132126
|
+
{ text: "/help: 显示命令" },
|
|
132127
|
+
{
|
|
132128
|
+
text: "/config: 选择并配置你常用的模型商",
|
|
132129
|
+
solo: true,
|
|
132130
|
+
priority: 3
|
|
132131
|
+
},
|
|
132132
|
+
{
|
|
132133
|
+
text: "让 Scream 安排任务,例如 \"2个小时后提醒我去拿快递\"",
|
|
132134
|
+
solo: true,
|
|
132135
|
+
priority: 3
|
|
132153
132136
|
}
|
|
132154
|
-
|
|
132155
|
-
|
|
132156
|
-
|
|
132157
|
-
|
|
132158
|
-
|
|
132159
|
-
|
|
132160
|
-
|
|
132161
|
-
|
|
132162
|
-
|
|
132163
|
-
|
|
132164
|
-
|
|
132165
|
-
|
|
132137
|
+
];
|
|
132138
|
+
/**
|
|
132139
|
+
* Expand tips into a rotation sequence using smooth weighted round-robin
|
|
132140
|
+
* (the nginx SWRR algorithm). Higher-`priority` tips appear more often while
|
|
132141
|
+
* staying evenly spread, so a tip generally does not land next to its own
|
|
132142
|
+
* duplicate. Deterministic and computed once at module load. Exported for
|
|
132143
|
+
* unit testing.
|
|
132144
|
+
*/
|
|
132145
|
+
function buildWeightedTips(tips) {
|
|
132146
|
+
const items = tips.map((t) => ({
|
|
132147
|
+
tip: t,
|
|
132148
|
+
weight: Math.max(1, Math.trunc(t.priority ?? 1)),
|
|
132149
|
+
current: 0
|
|
132150
|
+
}));
|
|
132151
|
+
const total = items.reduce((sum, it) => sum + it.weight, 0);
|
|
132152
|
+
const seq = [];
|
|
132153
|
+
for (let n = 0; n < total; n++) {
|
|
132154
|
+
let best = items[0];
|
|
132155
|
+
for (const it of items) {
|
|
132156
|
+
it.current += it.weight;
|
|
132157
|
+
if (it.current > best.current) best = it;
|
|
132166
132158
|
}
|
|
132159
|
+
best.current -= total;
|
|
132160
|
+
seq.push(best.tip);
|
|
132167
132161
|
}
|
|
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
|
-
|
|
132162
|
+
return seq;
|
|
132163
|
+
}
|
|
132164
|
+
buildWeightedTips(TOOLBAR_TIPS);
|
|
132165
|
+
function shortenModel(model) {
|
|
132166
|
+
if (!model) return model;
|
|
132167
|
+
const slash = model.lastIndexOf("/");
|
|
132168
|
+
return slash >= 0 ? model.slice(slash + 1) : model;
|
|
132169
|
+
}
|
|
132170
|
+
function modelDisplayName(state) {
|
|
132171
|
+
const model = state.availableModels[state.model];
|
|
132172
|
+
return model?.displayName ?? model?.model ?? state.model;
|
|
132173
|
+
}
|
|
132174
|
+
function shortenCwd(path) {
|
|
132175
|
+
if (!path) return path;
|
|
132176
|
+
const home = process.env["HOME"] ?? "";
|
|
132177
|
+
let work = path;
|
|
132178
|
+
if (home && path === home) return "~";
|
|
132179
|
+
if (home && path.startsWith(home + "/")) work = "~" + path.slice(home.length);
|
|
132180
|
+
const segments = work.split("/").filter((s) => s.length > 0);
|
|
132181
|
+
if (segments.length <= MAX_CWD_SEGMENTS) return work;
|
|
132182
|
+
return `…/${segments.slice(-3).join("/")}`;
|
|
132183
|
+
}
|
|
132184
|
+
function formatTokenCount(n) {
|
|
132185
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
132186
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
132187
|
+
return String(n);
|
|
132188
|
+
}
|
|
132189
|
+
function safeUsage(usage) {
|
|
132190
|
+
return safeUsageRatio(usage);
|
|
132191
|
+
}
|
|
132192
|
+
function formatContextStatus(usage, tokens, maxTokens) {
|
|
132193
|
+
const pct = `${(safeUsage(usage) * 100).toFixed(1)}%`;
|
|
132194
|
+
if (maxTokens && maxTokens > 0 && tokens !== void 0) return `上下文:${pct} (${formatTokenCount(tokens)}/${formatTokenCount(maxTokens)})`;
|
|
132195
|
+
return `上下文:${pct}`;
|
|
132196
|
+
}
|
|
132197
|
+
const BRAND_COLORS = [
|
|
132198
|
+
"#72A4E9",
|
|
132199
|
+
"#A78BFA",
|
|
132200
|
+
"#34D399"
|
|
132201
|
+
];
|
|
132202
|
+
const GRADIENT_CYCLE_MS = 4e3;
|
|
132203
|
+
const SPINNER_FRAMES = [
|
|
132204
|
+
"●",
|
|
132205
|
+
"◉",
|
|
132206
|
+
"◎",
|
|
132207
|
+
"◌",
|
|
132208
|
+
"○",
|
|
132209
|
+
"◌",
|
|
132210
|
+
"◎",
|
|
132211
|
+
"◉"
|
|
132212
|
+
];
|
|
132213
|
+
const SPINNER_TICK_MS = 120;
|
|
132214
|
+
function hexToRgb(hex) {
|
|
132215
|
+
const v = parseInt(hex.slice(1), 16);
|
|
132216
|
+
return [
|
|
132217
|
+
v >> 16 & 255,
|
|
132218
|
+
v >> 8 & 255,
|
|
132219
|
+
v & 255
|
|
132220
|
+
];
|
|
132221
|
+
}
|
|
132222
|
+
function lerpGradient(t) {
|
|
132223
|
+
const count = BRAND_COLORS.length;
|
|
132224
|
+
const segment = Math.min(t * count, count - 1);
|
|
132225
|
+
const idx = Math.floor(segment);
|
|
132226
|
+
const localT = segment - idx;
|
|
132227
|
+
const nextIdx = (idx + 1) % count;
|
|
132228
|
+
const [r0, g0, b0] = hexToRgb(BRAND_COLORS[idx]);
|
|
132229
|
+
const [r1, g1, b1] = hexToRgb(BRAND_COLORS[nextIdx]);
|
|
132230
|
+
const r = Math.round(r0 + (r1 - r0) * localT);
|
|
132231
|
+
const g = Math.round(g0 + (g1 - g0) * localT);
|
|
132232
|
+
const b = Math.round(b0 + (b1 - b0) * localT);
|
|
132233
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
132234
|
+
}
|
|
132235
|
+
function buildStatusLine(streamingPhase, livePaneMode, streamingStartTime) {
|
|
132236
|
+
if (streamingPhase === "idle" && livePaneMode !== "tool") return "○ 空闲";
|
|
132237
|
+
let label;
|
|
132238
|
+
if (livePaneMode === "tool") label = "执行中";
|
|
132239
|
+
else if (streamingPhase === "waiting") label = "等待响应";
|
|
132240
|
+
else if (streamingPhase === "thinking") label = "思考中";
|
|
132241
|
+
else if (streamingPhase === "composing") label = "输出中";
|
|
132242
|
+
else label = "";
|
|
132243
|
+
const elapsed = Date.now() - streamingStartTime;
|
|
132244
|
+
const totalSeconds = Math.floor(elapsed / 1e3);
|
|
132245
|
+
const elapsedStr = totalSeconds < 60 ? `${totalSeconds}s` : `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
|
|
132246
|
+
const now = Date.now();
|
|
132247
|
+
const frame = SPINNER_FRAMES[Math.floor(now / SPINNER_TICK_MS) % SPINNER_FRAMES.length];
|
|
132248
|
+
const gradientColor = lerpGradient(now % GRADIENT_CYCLE_MS / GRADIENT_CYCLE_MS);
|
|
132249
|
+
return chalk.hex(gradientColor).bold(frame) + " " + label + " " + elapsedStr;
|
|
132250
|
+
}
|
|
132251
|
+
function formatFooterGitBadge(status, colors) {
|
|
132252
|
+
const base = chalk.hex(colors.status)(formatGitBadgeBase(status));
|
|
132253
|
+
if (status.pullRequest === null) return base;
|
|
132254
|
+
return `${base} ${chalk.hex(colors.primary)(formatPullRequestBadge(status.pullRequest, { linkPullRequest: true }))}`;
|
|
132255
|
+
}
|
|
132256
|
+
var FooterComponent = class {
|
|
132257
|
+
state;
|
|
132258
|
+
colors;
|
|
132259
|
+
onGitStatusChange;
|
|
132260
|
+
gitCache;
|
|
132261
|
+
gitCacheWorkDir;
|
|
132262
|
+
transientHint = null;
|
|
132263
|
+
/**
|
|
132264
|
+
* Non-terminal background-task counts split by kind so the footer can
|
|
132265
|
+
* render two distinct badges. `bashTasks` covers `bash-*` BPM tasks
|
|
132266
|
+
* spawned via `Shell run_in_background=true`; `agentTasks` covers
|
|
132267
|
+
* `agent-*` BPM tasks (background subagents). Either zero hides its
|
|
132268
|
+
* respective badge.
|
|
132269
|
+
*/
|
|
132270
|
+
backgroundBashTaskCount = 0;
|
|
132271
|
+
backgroundAgentCount = 0;
|
|
132272
|
+
constructor(state, colors, onGitStatusChange = () => {}) {
|
|
132273
|
+
this.state = state;
|
|
132274
|
+
this.colors = colors;
|
|
132275
|
+
this.onGitStatusChange = onGitStatusChange;
|
|
132276
|
+
this.gitCacheWorkDir = state.workDir;
|
|
132277
|
+
this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
|
|
132212
132278
|
}
|
|
132213
|
-
|
|
132214
|
-
|
|
132215
|
-
|
|
132216
|
-
this.
|
|
132217
|
-
return;
|
|
132279
|
+
setState(state) {
|
|
132280
|
+
if (state.workDir !== this.gitCacheWorkDir) {
|
|
132281
|
+
this.gitCacheWorkDir = state.workDir;
|
|
132282
|
+
this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
|
|
132218
132283
|
}
|
|
132219
|
-
this.
|
|
132220
|
-
this.uiHooks?.showPanel(next.payload);
|
|
132284
|
+
this.state = state;
|
|
132221
132285
|
}
|
|
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;
|
|
132286
|
+
setColors(colors) {
|
|
132287
|
+
this.colors = colors;
|
|
132230
132288
|
}
|
|
132231
132289
|
/**
|
|
132232
|
-
*
|
|
132233
|
-
*
|
|
132234
|
-
*
|
|
132235
|
-
*
|
|
132290
|
+
* Short-lived hint that replaces the rotating toolbar tips on line 1.
|
|
132291
|
+
* Used by the exit-confirmation double-tap flow to show "Press Ctrl+C
|
|
132292
|
+
* again to exit" without requiring a toast/overlay subsystem.
|
|
132293
|
+
* Pass `null` to clear.
|
|
132236
132294
|
*/
|
|
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);
|
|
132295
|
+
setTransientHint(hint) {
|
|
132296
|
+
this.transientHint = hint;
|
|
132299
132297
|
}
|
|
132300
|
-
|
|
132301
|
-
|
|
132302
|
-
|
|
132303
|
-
|
|
132304
|
-
|
|
132298
|
+
/**
|
|
132299
|
+
* Sync both background-task badges with live counts. Each non-zero
|
|
132300
|
+
* count produces its own bracketed badge on line 1; zeros hide them
|
|
132301
|
+
* independently.
|
|
132302
|
+
*/
|
|
132303
|
+
setBackgroundCounts(counts) {
|
|
132304
|
+
this.backgroundBashTaskCount = Math.max(0, counts.bashTasks);
|
|
132305
|
+
this.backgroundAgentCount = Math.max(0, counts.agentTasks);
|
|
132305
132306
|
}
|
|
132306
|
-
|
|
132307
|
-
|
|
132308
|
-
|
|
132309
|
-
|
|
132310
|
-
|
|
132311
|
-
|
|
132312
|
-
|
|
132313
|
-
if (
|
|
132314
|
-
|
|
132315
|
-
|
|
132316
|
-
|
|
132307
|
+
invalidate() {}
|
|
132308
|
+
render(width) {
|
|
132309
|
+
const colors = this.colors;
|
|
132310
|
+
const state = this.state;
|
|
132311
|
+
const left = [];
|
|
132312
|
+
if (state.permissionMode === "auto") left.push(chalk.hex(colors.warning).bold("auto"));
|
|
132313
|
+
if (state.permissionMode === "yolo") left.push(chalk.hex(colors.warning).bold("YES"));
|
|
132314
|
+
if (state.planMode) left.push(chalk.hex(colors.primary).bold("plan"));
|
|
132315
|
+
if (state.wolfpackMode) left.push(chalk.hex(colors.primary).bold("wolfpack"));
|
|
132316
|
+
if (state.goalActive) left.push(chalk.hex(colors.primary).bold("goal"));
|
|
132317
|
+
const model = shortenModel(modelDisplayName(state));
|
|
132318
|
+
if (model) {
|
|
132319
|
+
const thinkingLabel = state.thinking ? " 思考中" : "";
|
|
132320
|
+
left.push(chalk.hex(colors.text)(`${model}${thinkingLabel}`));
|
|
132317
132321
|
}
|
|
132318
|
-
|
|
132319
|
-
|
|
132320
|
-
this.
|
|
132321
|
-
return;
|
|
132322
|
+
if (this.backgroundBashTaskCount > 0) {
|
|
132323
|
+
const noun = this.backgroundBashTaskCount === 1 ? "个任务" : "个任务";
|
|
132324
|
+
left.push(chalk.hex(colors.primary)(`[${String(this.backgroundBashTaskCount)}${noun} 运行中]`));
|
|
132322
132325
|
}
|
|
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");
|
|
132326
|
+
if (this.backgroundAgentCount > 0) {
|
|
132327
|
+
const noun = this.backgroundAgentCount === 1 ? "个代理" : "个代理";
|
|
132328
|
+
left.push(chalk.hex(colors.primary)(`[${String(this.backgroundAgentCount)}${noun} 运行中]`));
|
|
132342
132329
|
}
|
|
132343
|
-
|
|
132344
|
-
|
|
132345
|
-
|
|
132346
|
-
|
|
132347
|
-
|
|
132348
|
-
|
|
132349
|
-
|
|
132330
|
+
const cwd = shortenCwd(state.workDir);
|
|
132331
|
+
if (cwd) left.push(chalk.hex(colors.status)(cwd));
|
|
132332
|
+
const git = this.gitCache.getStatus();
|
|
132333
|
+
if (git !== null) left.push(formatFooterGitBadge(git, colors));
|
|
132334
|
+
const leftLine = left.join(" ");
|
|
132335
|
+
const leftWidth = visibleWidth(leftLine);
|
|
132336
|
+
let rightText;
|
|
132337
|
+
if (this.transientHint) rightText = chalk.hex(colors.warning).bold(this.transientHint);
|
|
132338
|
+
else {
|
|
132339
|
+
const statusLine = buildStatusLine(state.streamingPhase, state.livePaneMode, state.streamingStartTime);
|
|
132340
|
+
const ccDot = state.ccConnectActive ? chalk.hex(colors.success)("●") : chalk.hex(colors.textDim)("●");
|
|
132341
|
+
rightText = chalk.hex(colors.textDim)(ccDot + " " + formatContextStatus(state.contextUsage, state.contextTokens, state.maxContextTokens) + " " + statusLine);
|
|
132350
132342
|
}
|
|
132351
|
-
|
|
132352
|
-
|
|
132353
|
-
|
|
132354
|
-
|
|
132355
|
-
|
|
132356
|
-
|
|
132357
|
-
|
|
132358
|
-
|
|
132359
|
-
|
|
132360
|
-
return { answers: [] };
|
|
132343
|
+
const rightWidth = visibleWidth(rightText);
|
|
132344
|
+
const gap = 3;
|
|
132345
|
+
let line1;
|
|
132346
|
+
if (leftWidth + gap + rightWidth <= width) {
|
|
132347
|
+
const pad = width - leftWidth - rightWidth;
|
|
132348
|
+
line1 = leftLine + " ".repeat(pad) + rightText;
|
|
132349
|
+
} else if (leftWidth <= width) line1 = leftLine;
|
|
132350
|
+
else line1 = truncateToWidth(leftLine, width, "…");
|
|
132351
|
+
return [truncateToWidth(line1, width)];
|
|
132361
132352
|
}
|
|
132362
132353
|
};
|
|
132363
132354
|
//#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
132355
|
//#region src/tui/components/chrome/todo-panel.ts
|
|
132384
132356
|
const MAX_VISIBLE = 5;
|
|
132385
132357
|
/**
|
|
@@ -136147,27 +136119,6 @@ var ScreamTUI = class ScreamTUI {
|
|
|
136147
136119
|
const footerWrap = new GutterContainer(1, 1);
|
|
136148
136120
|
footerWrap.addChild(this.state.footer);
|
|
136149
136121
|
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
136122
|
}
|
|
136172
136123
|
handlePlanToggle(next) {
|
|
136173
136124
|
handlePlanCommand(this, next ? "on" : "off");
|