@yagni-app/code-staging 1.0.0-staging.1183.1 → 1.0.0-staging.1184.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.
|
@@ -109,7 +109,28 @@ export declare class ChipEditor extends CustomEditor {
|
|
|
109
109
|
/** Drop all stashed images. Called after a successful submit so a sent image
|
|
110
110
|
* is not re-attached to the next message. */
|
|
111
111
|
clearStash(): void;
|
|
112
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Re-style chip tokens AND lay down the Kimi-style prompt box: a rounded
|
|
114
|
+
* border on all four sides, with a bold `›` caret on the first content
|
|
115
|
+
* line and continuation lines indented so wrapped text aligns under it.
|
|
116
|
+
*
|
|
117
|
+
* The base editor renders full-width lines carrying its own 1-column left
|
|
118
|
+
* padding, so we render it narrower, strip that padding, and re-wrap each
|
|
119
|
+
* line in the frame. Exact column layout (0-indexed):
|
|
120
|
+
*
|
|
121
|
+
* 0 ╭ │ ╰ border
|
|
122
|
+
* 1 space
|
|
123
|
+
* 2 › (first line) / space (continuation)
|
|
124
|
+
* 3 space
|
|
125
|
+
* 4… text — same column on every line
|
|
126
|
+
* width-2 space
|
|
127
|
+
* width-1 │ border
|
|
128
|
+
*
|
|
129
|
+
* Box-drawing glyphs are drawn centered in their cell while text glyphs
|
|
130
|
+
* start at the cell's left bearing, so no whole-column position lands the
|
|
131
|
+
* border ink exactly on the footer's text column: column 0 reads a hair
|
|
132
|
+
* outside it, column 1 a hair inside. Column 0 is the accepted tradeoff.
|
|
133
|
+
*/
|
|
113
134
|
render(width: number): string[];
|
|
114
135
|
}
|
|
115
136
|
/**
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* the extension's pi imports to the same module instance pi uses.
|
|
23
23
|
*/
|
|
24
24
|
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
25
|
-
import { matchesKey } from "@earendil-works/pi-tui";
|
|
25
|
+
import { matchesKey, stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
26
26
|
import { spawnSync } from "node:child_process";
|
|
27
27
|
import { readFileSync, unlinkSync, existsSync } from "node:fs";
|
|
28
28
|
import { tmpdir } from "node:os";
|
|
@@ -381,11 +381,64 @@ export class ChipEditor extends CustomEditor {
|
|
|
381
381
|
clearStash() {
|
|
382
382
|
this.stashed = [];
|
|
383
383
|
}
|
|
384
|
-
/**
|
|
384
|
+
/**
|
|
385
|
+
* Re-style chip tokens AND lay down the Kimi-style prompt box: a rounded
|
|
386
|
+
* border on all four sides, with a bold `›` caret on the first content
|
|
387
|
+
* line and continuation lines indented so wrapped text aligns under it.
|
|
388
|
+
*
|
|
389
|
+
* The base editor renders full-width lines carrying its own 1-column left
|
|
390
|
+
* padding, so we render it narrower, strip that padding, and re-wrap each
|
|
391
|
+
* line in the frame. Exact column layout (0-indexed):
|
|
392
|
+
*
|
|
393
|
+
* 0 ╭ │ ╰ border
|
|
394
|
+
* 1 space
|
|
395
|
+
* 2 › (first line) / space (continuation)
|
|
396
|
+
* 3 space
|
|
397
|
+
* 4… text — same column on every line
|
|
398
|
+
* width-2 space
|
|
399
|
+
* width-1 │ border
|
|
400
|
+
*
|
|
401
|
+
* Box-drawing glyphs are drawn centered in their cell while text glyphs
|
|
402
|
+
* start at the cell's left bearing, so no whole-column position lands the
|
|
403
|
+
* border ink exactly on the footer's text column: column 0 reads a hair
|
|
404
|
+
* outside it, column 1 a hair inside. Column 0 is the accepted tradeoff.
|
|
405
|
+
*/
|
|
385
406
|
render(width) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
407
|
+
const styled = super.render(Math.max(1, width - 4)).map((line) => line.replace(CHIP_RE, (m) => `${CHIP_ON}${m}${CHIP_OFF}`));
|
|
408
|
+
const border = (s) => this.borderColor(s);
|
|
409
|
+
const CARET = "\u001b[1m›\u001b[22m";
|
|
410
|
+
const FIRST_PREFIX = `${border("│")} ${CARET} `;
|
|
411
|
+
const NEXT_PREFIX = `${border("│")} `;
|
|
412
|
+
const SUFFIX = ` ${border("│")}`;
|
|
413
|
+
const contentWidth = Math.max(1, width - 6);
|
|
414
|
+
const out = [];
|
|
415
|
+
let borderCount = 0;
|
|
416
|
+
let firstContentLine = true;
|
|
417
|
+
for (const line of styled) {
|
|
418
|
+
const stripped = stripTerminalSequences(line);
|
|
419
|
+
if (/^─+$/.test(stripped) || /^─*\s*[↑↓]/.test(stripped)) {
|
|
420
|
+
borderCount += 1;
|
|
421
|
+
const [left, right] = borderCount === 1 ? ["╭", "╮"] : ["╰", "╯"];
|
|
422
|
+
// One corner + one dash on each side lands the row at `width`.
|
|
423
|
+
out.push(`${border(`${left}─`)}${line}${border(`─${right}`)}`);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
// Strip the base editor's own left padding (the frame replaces it),
|
|
427
|
+
// then fit the body to the content width exactly so no line can
|
|
428
|
+
// exceed the terminal width.
|
|
429
|
+
let body = line.startsWith(" ") ? line.slice(1) : line;
|
|
430
|
+
if (visibleWidth(body) > contentWidth)
|
|
431
|
+
body = truncateToWidth(body, contentWidth, "");
|
|
432
|
+
const pad = " ".repeat(Math.max(0, contentWidth - visibleWidth(body)));
|
|
433
|
+
if (borderCount !== 1) {
|
|
434
|
+
// Autocomplete rows (after the bottom border): align under the text.
|
|
435
|
+
out.push(`${" ".repeat(4)}${body}${pad}${" ".repeat(2)}`);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
out.push(`${firstContentLine ? FIRST_PREFIX : NEXT_PREFIX}${body}${pad}${SUFFIX}`);
|
|
439
|
+
firstContentLine = false;
|
|
440
|
+
}
|
|
441
|
+
return out;
|
|
389
442
|
}
|
|
390
443
|
}
|
|
391
444
|
/**
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Committed branch diff stat for the footer's git badge.
|
|
3
|
+
*
|
|
4
|
+
* Counting follows Codex (`tui/src/branch_summary.rs`), not Kimi: it sums the
|
|
5
|
+
* line delta between `HEAD` and the merge base with the repository's *default*
|
|
6
|
+
* branch, so the badge answers "what does this branch contain" — the same
|
|
7
|
+
* number a GitHub PR UI would show. Uncommitted working-tree edits are
|
|
8
|
+
* intentionally ignored (Codex: "the status-line item summarizes the checked-
|
|
9
|
+
* out branch, not the current dirty worktree").
|
|
10
|
+
*
|
|
11
|
+
* Display follows Kimi: `[+N -M]` appended to the branch, dim grey.
|
|
12
|
+
*
|
|
13
|
+
* Refresh reuses Kimi's synchronous TTL model: re-read at most every 15s,
|
|
14
|
+
* demanded on each footer render. The only divergence from Codex is that their
|
|
15
|
+
* probe is `async`; ours is `spawnSync` because the footer render path is
|
|
16
|
+
* synchronous.
|
|
17
|
+
*/
|
|
18
|
+
/** Committed branch delta relative to the default branch. */
|
|
19
|
+
export interface BranchDiffStat {
|
|
20
|
+
readonly added: number;
|
|
21
|
+
readonly deleted: number;
|
|
22
|
+
}
|
|
23
|
+
/** Diff-stat source with a workDir, callable on demand. */
|
|
24
|
+
export interface DiffStatCache {
|
|
25
|
+
/** Current stat, or `null` when workDir isn't a git repo. */
|
|
26
|
+
get(): BranchDiffStat | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Injectable git runner seam (tests stub it; production shells out to `git`).
|
|
30
|
+
* Returns stdout, or `null` on any failure.
|
|
31
|
+
*/
|
|
32
|
+
export type GitRunner = (args: string[], cwd: string) => string | null;
|
|
33
|
+
/** True when `cwd` is inside a git work tree. Never throws. */
|
|
34
|
+
export declare function detectGitRepo(run: GitRunner, cwd: string): boolean;
|
|
35
|
+
/** Codex's `parseDiffNumstatCount`-equivalent: `-` (binary) → 0; non-numeric → 0. */
|
|
36
|
+
export declare function parseNumstatCount(value: string | undefined): number;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the default branch name, preferring remote-tracking refs over local
|
|
39
|
+
* branches so a stale local `main` doesn't inflate the diff (Codex's ordering).
|
|
40
|
+
* Falls back to common local names when no remote advertises a default.
|
|
41
|
+
* Returns null when nothing resolves.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveDefaultBranch(run: GitRunner, cwd: string): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Sum the committed line delta between `HEAD` and the merge base with the
|
|
46
|
+
* default branch: `git diff --numstat <merge-base>..HEAD`. Returns null when
|
|
47
|
+
* the default branch or merge base can't be resolved (hide the badge).
|
|
48
|
+
*/
|
|
49
|
+
export declare function readDiffStat(run: GitRunner, cwd: string): BranchDiffStat | null;
|
|
50
|
+
/**
|
|
51
|
+
* Kimi-style display: `+N -M`, or null when the branch is even with its base
|
|
52
|
+
* (a fresh/unchanged branch shows no badge). Both-zero still renders nothing,
|
|
53
|
+
* matching "our style" (Kimi hides a clean tree; Codex would say "No changes").
|
|
54
|
+
*/
|
|
55
|
+
export declare function formatDiffStat(diff: BranchDiffStat | null): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* Time-bucketed cache: `get()` re-reads git only when the last read was more
|
|
58
|
+
* than 15s ago, null off-repo. All git failures → null (badge hidden), never
|
|
59
|
+
* throws.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createDiffStatCache(workDir: string, run?: GitRunner): DiffStatCache;
|
|
62
|
+
//# sourceMappingURL=diffStat.d.ts.map
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Committed branch diff stat for the footer's git badge.
|
|
3
|
+
*
|
|
4
|
+
* Counting follows Codex (`tui/src/branch_summary.rs`), not Kimi: it sums the
|
|
5
|
+
* line delta between `HEAD` and the merge base with the repository's *default*
|
|
6
|
+
* branch, so the badge answers "what does this branch contain" — the same
|
|
7
|
+
* number a GitHub PR UI would show. Uncommitted working-tree edits are
|
|
8
|
+
* intentionally ignored (Codex: "the status-line item summarizes the checked-
|
|
9
|
+
* out branch, not the current dirty worktree").
|
|
10
|
+
*
|
|
11
|
+
* Display follows Kimi: `[+N -M]` appended to the branch, dim grey.
|
|
12
|
+
*
|
|
13
|
+
* Refresh reuses Kimi's synchronous TTL model: re-read at most every 15s,
|
|
14
|
+
* demanded on each footer render. The only divergence from Codex is that their
|
|
15
|
+
* probe is `async`; ours is `spawnSync` because the footer render path is
|
|
16
|
+
* synchronous.
|
|
17
|
+
*/
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
/** Status refresh interval — same value Kimi uses (STATUS_TTL_MS). */
|
|
20
|
+
const STATUS_TTL_MS = 15_000;
|
|
21
|
+
/** git spawn timeout — same value Kimi uses (SPAWN_TIMEOUT_MS). */
|
|
22
|
+
const SPAWN_TIMEOUT_MS = 500;
|
|
23
|
+
const SPAWN_MAX_BUFFER = 4 * 1024 * 1024;
|
|
24
|
+
/** Production runner: `spawnSync` with a short timeout, all errors → null. */
|
|
25
|
+
function runGit(args, cwd) {
|
|
26
|
+
try {
|
|
27
|
+
const r = spawnSync("git", ["--no-optional-locks", ...args], {
|
|
28
|
+
cwd,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
31
|
+
maxBuffer: SPAWN_MAX_BUFFER,
|
|
32
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
33
|
+
});
|
|
34
|
+
if (r.error || r.status !== 0)
|
|
35
|
+
return null;
|
|
36
|
+
return r.stdout;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** True when `cwd` is inside a git work tree. Never throws. */
|
|
43
|
+
export function detectGitRepo(run, cwd) {
|
|
44
|
+
const out = run(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
45
|
+
return out !== null && out.trim() === "true";
|
|
46
|
+
}
|
|
47
|
+
/** Codex's `parseDiffNumstatCount`-equivalent: `-` (binary) → 0; non-numeric → 0. */
|
|
48
|
+
export function parseNumstatCount(value) {
|
|
49
|
+
if (value === undefined || value === "-")
|
|
50
|
+
return 0;
|
|
51
|
+
const n = Number.parseInt(value, 10);
|
|
52
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
53
|
+
}
|
|
54
|
+
/** List remotes, `origin` first (mirrors Codex's `get_git_remotes`). */
|
|
55
|
+
function gitRemotes(run, cwd) {
|
|
56
|
+
const out = run(["remote"], cwd);
|
|
57
|
+
if (out === null)
|
|
58
|
+
return [];
|
|
59
|
+
const remotes = out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
60
|
+
const originIdx = remotes.indexOf("origin");
|
|
61
|
+
if (originIdx > 0) {
|
|
62
|
+
remotes.splice(originIdx, 1);
|
|
63
|
+
remotes.unshift("origin");
|
|
64
|
+
}
|
|
65
|
+
return remotes;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the default branch name, preferring remote-tracking refs over local
|
|
69
|
+
* branches so a stale local `main` doesn't inflate the diff (Codex's ordering).
|
|
70
|
+
* Falls back to common local names when no remote advertises a default.
|
|
71
|
+
* Returns null when nothing resolves.
|
|
72
|
+
*/
|
|
73
|
+
export function resolveDefaultBranch(run, cwd) {
|
|
74
|
+
// Tier 1: remote default via `refs/remotes/<remote>/HEAD` symbolic ref.
|
|
75
|
+
for (const remote of gitRemotes(run, cwd)) {
|
|
76
|
+
const out = run(["symbolic-ref", "--quiet", `refs/remotes/${remote}/HEAD`], cwd);
|
|
77
|
+
if (out === null)
|
|
78
|
+
continue;
|
|
79
|
+
const ref = out.trim();
|
|
80
|
+
if (!ref)
|
|
81
|
+
continue;
|
|
82
|
+
// `refs/remotes/origin/main` → `main`.
|
|
83
|
+
const name = ref.replace(/^refs\/remotes\/[^/]+\//, "");
|
|
84
|
+
if (name && name !== "HEAD")
|
|
85
|
+
return name;
|
|
86
|
+
}
|
|
87
|
+
// Tier 2: local main/master/trunk/develop.
|
|
88
|
+
for (const candidate of ["main", "master", "trunk", "develop"]) {
|
|
89
|
+
const out = run(["rev-parse", "--verify", "--quiet", candidate], cwd);
|
|
90
|
+
if (out !== null && out.trim())
|
|
91
|
+
return candidate;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Sum the committed line delta between `HEAD` and the merge base with the
|
|
97
|
+
* default branch: `git diff --numstat <merge-base>..HEAD`. Returns null when
|
|
98
|
+
* the default branch or merge base can't be resolved (hide the badge).
|
|
99
|
+
*/
|
|
100
|
+
export function readDiffStat(run, cwd) {
|
|
101
|
+
const defaultBranch = resolveDefaultBranch(run, cwd);
|
|
102
|
+
if (defaultBranch === null)
|
|
103
|
+
return null;
|
|
104
|
+
const mergeBase = run(["merge-base", "HEAD", defaultBranch], cwd);
|
|
105
|
+
if (mergeBase === null || !mergeBase.trim())
|
|
106
|
+
return null;
|
|
107
|
+
const out = run(["diff", "--numstat", `${mergeBase.trim()}..HEAD`, "--"], cwd);
|
|
108
|
+
if (out === null)
|
|
109
|
+
return null;
|
|
110
|
+
let added = 0;
|
|
111
|
+
let deleted = 0;
|
|
112
|
+
for (const line of out.split("\n")) {
|
|
113
|
+
if (!line)
|
|
114
|
+
continue;
|
|
115
|
+
const [addedText, deletedText] = line.split("\t");
|
|
116
|
+
added += parseNumstatCount(addedText);
|
|
117
|
+
deleted += parseNumstatCount(deletedText);
|
|
118
|
+
}
|
|
119
|
+
return { added, deleted };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Kimi-style display: `+N -M`, or null when the branch is even with its base
|
|
123
|
+
* (a fresh/unchanged branch shows no badge). Both-zero still renders nothing,
|
|
124
|
+
* matching "our style" (Kimi hides a clean tree; Codex would say "No changes").
|
|
125
|
+
*/
|
|
126
|
+
export function formatDiffStat(diff) {
|
|
127
|
+
if (diff === null)
|
|
128
|
+
return null;
|
|
129
|
+
const parts = [];
|
|
130
|
+
if (diff.added > 0)
|
|
131
|
+
parts.push(`+${String(diff.added)}`);
|
|
132
|
+
if (diff.deleted > 0)
|
|
133
|
+
parts.push(`-${String(diff.deleted)}`);
|
|
134
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Time-bucketed cache: `get()` re-reads git only when the last read was more
|
|
138
|
+
* than 15s ago, null off-repo. All git failures → null (badge hidden), never
|
|
139
|
+
* throws.
|
|
140
|
+
*/
|
|
141
|
+
export function createDiffStatCache(workDir, run = runGit) {
|
|
142
|
+
const isRepo = detectGitRepo(run, workDir);
|
|
143
|
+
let cached = null;
|
|
144
|
+
let fetchedAt = 0;
|
|
145
|
+
return {
|
|
146
|
+
get() {
|
|
147
|
+
if (!isRepo)
|
|
148
|
+
return null;
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
if (cached === null || now - fetchedAt >= STATUS_TTL_MS) {
|
|
151
|
+
cached = readDiffStat(run, workDir);
|
|
152
|
+
fetchedAt = now;
|
|
153
|
+
}
|
|
154
|
+
return cached;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=diffStat.js.map
|
|
@@ -96,6 +96,8 @@ export declare function renderFooterLines(input: {
|
|
|
96
96
|
model: string;
|
|
97
97
|
/** Current permission mode; shown on line 2 to the left of the model as "<mode> mode". */
|
|
98
98
|
mode?: PermissionMode | null;
|
|
99
|
+
/** Formatted `+N -M` / `±`, or null when clean — appended to the branch as `[ … ]`. */
|
|
100
|
+
diff?: string | null;
|
|
99
101
|
usage: UsageTotals;
|
|
100
102
|
contextPercent: number | null;
|
|
101
103
|
statuses: string[];
|
package/dist/extension/footer.js
CHANGED
|
@@ -42,6 +42,7 @@ import { spawnSync } from "node:child_process";
|
|
|
42
42
|
import { statSync } from "node:fs";
|
|
43
43
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
44
44
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
45
|
+
import { createDiffStatCache, formatDiffStat } from "./diffStat.js";
|
|
45
46
|
export const BRANCH_MAX_WIDTH = 60;
|
|
46
47
|
const WORKTREE_MAX_WIDTH = 30;
|
|
47
48
|
/** Section separator: single space + middle dot + single space. */
|
|
@@ -229,13 +230,18 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
229
230
|
// terminal edge. Truncation runs against the reduced content width.
|
|
230
231
|
const pad = " ".repeat(Math.max(0, Math.min(3, Math.floor(padX))));
|
|
231
232
|
const contentWidth = Math.max(1, width - pad.length);
|
|
232
|
-
// Line 1: folder · [worktree] · branch
|
|
233
|
+
// Line 1: folder · [worktree] · branch [+N -M]
|
|
233
234
|
const line1Parts = [theme.fg("accent", input.git.folder)];
|
|
234
235
|
if (input.git.inRepo) {
|
|
235
236
|
if (input.git.worktree)
|
|
236
237
|
line1Parts.push(theme.fg("warning", `[${input.git.worktree}]`));
|
|
237
|
-
if (input.git.branch)
|
|
238
|
-
|
|
238
|
+
if (input.git.branch) {
|
|
239
|
+
const branchSpan = theme.fg("border", truncateEnd(input.git.branch, BRANCH_MAX_WIDTH, dim("…")));
|
|
240
|
+
// Diff badge rides the branch segment (Kimi's `branch [ +N -M ]` shape),
|
|
241
|
+
// colored dim — the same grey as the cost stats on line 2.
|
|
242
|
+
const diffSpan = input.diff ? ` ${dim(`[${input.diff}]`)}` : "";
|
|
243
|
+
line1Parts.push(branchSpan + diffSpan);
|
|
244
|
+
}
|
|
239
245
|
}
|
|
240
246
|
const line1 = pad + truncateToWidth(line1Parts.join(sep), contentWidth, dim("…"));
|
|
241
247
|
// Line 2: [mode ·] model · ↑in ↓out $cost · ctx%
|
|
@@ -269,17 +275,23 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
269
275
|
export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle) {
|
|
270
276
|
return (_tui, theme, footerData) => {
|
|
271
277
|
let gitCache;
|
|
272
|
-
|
|
278
|
+
let diffStatCache;
|
|
279
|
+
const clearCaches = () => {
|
|
273
280
|
gitCache = undefined;
|
|
274
|
-
|
|
281
|
+
diffStatCache = undefined;
|
|
282
|
+
};
|
|
283
|
+
const unsubscribeBranch = footerData.onBranchChange?.(clearCaches);
|
|
275
284
|
const gitInfo = () => {
|
|
276
285
|
if (!gitCache) {
|
|
277
|
-
|
|
286
|
+
// Read cwd once; both the git info and the diff cache key off it.
|
|
287
|
+
const dir = ctx.sessionManager.getCwd();
|
|
288
|
+
gitCache = detectGitInfo(dir, process.env.HOME || process.env.USERPROFILE);
|
|
289
|
+
diffStatCache = createDiffStatCache(dir);
|
|
278
290
|
}
|
|
279
291
|
return gitCache;
|
|
280
292
|
};
|
|
281
293
|
if (invalidateHandle) {
|
|
282
|
-
invalidateHandle.invalidateGit =
|
|
294
|
+
invalidateHandle.invalidateGit = clearCaches;
|
|
283
295
|
invalidateHandle.requestRender = () => { _tui?.requestRender?.(); };
|
|
284
296
|
}
|
|
285
297
|
return {
|
|
@@ -291,13 +303,14 @@ export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle) {
|
|
|
291
303
|
git: gitInfo(),
|
|
292
304
|
model: ctx.model?.id ?? "no-model",
|
|
293
305
|
mode: modeHolder?.get() ?? null,
|
|
306
|
+
diff: formatDiffStat(diffStatCache?.get() ?? null),
|
|
294
307
|
usage: collectUsage(ctx.sessionManager),
|
|
295
308
|
contextPercent: ctx.getContextUsage()?.percent ?? null,
|
|
296
309
|
statuses,
|
|
297
310
|
}, theme, width, resolveFooterPadX());
|
|
298
311
|
},
|
|
299
312
|
invalidate() {
|
|
300
|
-
|
|
313
|
+
clearCaches();
|
|
301
314
|
},
|
|
302
315
|
dispose() {
|
|
303
316
|
unsubscribeBranch?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.0.0-staging.
|
|
3
|
+
"version": "1.0.0-staging.1184.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -41,5 +41,5 @@
|
|
|
41
41
|
"turndown": "^7.2.4",
|
|
42
42
|
"typebox": "^1.3.15"
|
|
43
43
|
},
|
|
44
|
-
"yagniSourceSha": "
|
|
44
|
+
"yagniSourceSha": "4e0c38ced9c2bc27fd28c01af4e22a1539fd1637"
|
|
45
45
|
}
|