killeros 2.0.19 → 2.0.21
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/CHANGELOG.md +29 -0
- package/Killeros.ts +3 -1
- package/README.md +6 -3
- package/killeros/auto-compaction.ts +40 -10
- package/killeros/display.ts +8 -3
- package/killeros/footer.ts +120 -27
- package/killeros/goal-state.ts +407 -0
- package/killeros/goals.ts +43 -380
- package/killeros/handoff.ts +79 -26
- package/killeros/hooks.ts +1 -1
- package/killeros/init-evidence.ts +15 -3
- package/killeros/personal-instructions.ts +57 -6
- package/killeros/question-ui.ts +590 -0
- package/killeros/question.ts +12 -559
- package/killeros/runtime.ts +4 -1
- package/killeros/secret-detector.ts +19 -0
- package/killeros/shell-ui.ts +1 -2
- package/package.json +10 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.0.21] - 2026-08-29
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Bounded goal verification and Pi compatibility, reduced Git status polling, reserved handoff context, and separated goal state and question UI logic.
|
|
12
|
+
- Blocked moderate dependency advisories in CI and updated TypeBox within the supported 1.x line.
|
|
13
|
+
- Preserved seconds in footer and goal elapsed times after one minute.
|
|
14
|
+
- Added a muted top border to the prompt editor.
|
|
15
|
+
- Colored the footer workspace path `#F0F89A`.
|
|
16
|
+
- Added a live changed-file count beside the Git branch when the worktree is dirty.
|
|
17
|
+
- Synced `dev` back to successful `main` releases after publishing.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Kept likely secrets out of `/init`, restricted personal-instruction imports to Pi's agent directory, and replaced injectable handoff framing with validated JSON.
|
|
22
|
+
- Hardened releases against stale CI runs, mismatched existing npm artifacts, and tag conflicts discovered after publication.
|
|
23
|
+
|
|
24
|
+
## [2.0.20] - 2026-08-25
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Added a typescript-eslint type-checked lint gate (`npm run lint`) alongside `tsc --noEmit`, and cleaned up the dead imports and untyped boundaries it surfaced.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Treated Pi's exact `Nothing to compact (session too small)` rejection as an expected automatic-compaction skip: no failure notification, silent retry on the next eligible turn, and active goals resume through a dedicated skip recovery instead of pausing for `/goal resume`.
|
|
33
|
+
- Made `/handoff` resilient to an unreadable `killeros.json`: explicit `KillerosOptions` budgets bypass the file, while other summaries fall back to the default budget with a visible warning.
|
|
34
|
+
- Named `/handoff` output truncation instead of reporting "did not finish", raised the default summary budget to 8192 tokens for reasoning models, and made the budget configurable through `KillerosOptions` or the `killeros.json` `handoffMaxTokens` key.
|
|
35
|
+
|
|
7
36
|
## [2.0.19] - 2026-08-24
|
|
8
37
|
|
|
9
38
|
### Changed
|
package/Killeros.ts
CHANGED
|
@@ -30,6 +30,8 @@ export { buildInitEvidence, listInitEvidence, readInitEvidence } from "./killero
|
|
|
30
30
|
export { captureInitTargetBaseline, installInitAgentsFile, validateGeneratedGuidance, writeInitAgentsFile } from "./killeros/init-target.ts";
|
|
31
31
|
export interface KillerosOptions {
|
|
32
32
|
completionNotifications?: CompletionNotificationDependencies;
|
|
33
|
+
/** Output-token budget for /handoff summaries; invalid values fall back to killeros.json, then the default. */
|
|
34
|
+
handoffMaxTokens?: number;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}): void {
|
|
@@ -41,7 +43,7 @@ export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}
|
|
|
41
43
|
registerPersonalInstructions(pi, initRuntime);
|
|
42
44
|
registerQuestionTool(pi);
|
|
43
45
|
registerAliases(pi);
|
|
44
|
-
registerHandoff(pi, goalRuntime);
|
|
46
|
+
registerHandoff(pi, goalRuntime, options.handoffMaxTokens);
|
|
45
47
|
registerSlashAutocomplete(pi, commandResolver);
|
|
46
48
|
registerFooter(pi, goalRuntime);
|
|
47
49
|
registerVariants(pi);
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
|
|
|
19
19
|
## Requirements
|
|
20
20
|
|
|
21
21
|
- Node.js 22.19.0+
|
|
22
|
-
- Pi 0.84.3
|
|
22
|
+
- Pi 0.84.3 or later within the 0.x release line
|
|
23
23
|
- An interactive TUI session for the custom header, editor, footer, `question`, and `/init`
|
|
24
24
|
|
|
25
25
|
## Install
|
|
@@ -34,7 +34,7 @@ Or from GitHub:
|
|
|
34
34
|
pi install git:github.com/KyrosHendrix/pi-KillerOS
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Pin a release by appending its tag, for example `@v2.0.
|
|
37
|
+
Pin a release by appending its tag, for example `@v2.0.21`. Add `-l` to install only for the current project. Restart Pi after installing.
|
|
38
38
|
|
|
39
39
|
## Commands
|
|
40
40
|
|
|
@@ -67,10 +67,13 @@ The packaged `killeros` theme activates on TUI start. Compaction triggers by def
|
|
|
67
67
|
"autoCompaction": {
|
|
68
68
|
"enabled": true,
|
|
69
69
|
"percentRemaining": 15
|
|
70
|
-
}
|
|
70
|
+
},
|
|
71
|
+
"handoffMaxTokens": 8192
|
|
71
72
|
}
|
|
72
73
|
```
|
|
73
74
|
|
|
75
|
+
`handoffMaxTokens` caps the `/handoff` summary output at 8192 tokens by default; raise it when long sessions truncate the summary.
|
|
76
|
+
|
|
74
77
|
Completion sounds are off by default; change with `/notification` in TUI mode. The tab-title indicator requires a Nerd Font.
|
|
75
78
|
|
|
76
79
|
## Development
|
|
@@ -12,6 +12,8 @@ import { createKillerosSettingsStore } from "./settings.ts";
|
|
|
12
12
|
export const DEFAULT_AUTO_COMPACTION_PERCENT_REMAINING = 15;
|
|
13
13
|
export const AUTO_COMPACTION_MESSAGE_TYPE = "killeros-auto-compaction";
|
|
14
14
|
export const AUTO_COMPACTION_MESSAGE = "Continue the interrupted task from the compacted context.";
|
|
15
|
+
/** Pi's exact rejection when a session has no eligible history to summarize; an expected skip, not a failure. */
|
|
16
|
+
export const SESSION_TOO_SMALL_COMPACTION_ERROR = "Nothing to compact (session too small)";
|
|
15
17
|
|
|
16
18
|
export interface AutoCompactionPreference {
|
|
17
19
|
enabled: boolean;
|
|
@@ -23,6 +25,8 @@ export interface AutoCompactionGoalHandlers {
|
|
|
23
25
|
onRequested(): void;
|
|
24
26
|
onCompleted(ctx: ExtensionContext): void;
|
|
25
27
|
onFailed(ctx: ExtensionContext, error: unknown): void;
|
|
28
|
+
/** Recovers the goal paused for a request Pi rejected as session-too-small, without claiming success. */
|
|
29
|
+
onSkipped(ctx: ExtensionContext): void;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export interface AutoCompactionDependencies {
|
|
@@ -72,6 +76,11 @@ function reserveTokens(settings: Pick<CompactionSettings, "reserveTokens">): num
|
|
|
72
76
|
: 0;
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
/** Matches only Pi's exact rejection text on a real Error; near-matches and stringified values stay failures. */
|
|
80
|
+
export function isSessionTooSmallCompactionError(error: unknown): boolean {
|
|
81
|
+
return error instanceof Error && error.message === SESSION_TOO_SMALL_COMPACTION_ERROR;
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
/** Triggers at the stricter of the user's percentage and Pi's token reserve. */
|
|
76
85
|
export function shouldTriggerAutoCompaction(
|
|
77
86
|
usage: Pick<ContextUsage, "tokens" | "contextWindow"> | undefined,
|
|
@@ -116,13 +125,7 @@ export function registerAutoCompaction(
|
|
|
116
125
|
ctx.ui.notify(`Automatic compaction failed: ${errorMessage(error)}`, "error");
|
|
117
126
|
};
|
|
118
127
|
|
|
119
|
-
const finishFailure = (
|
|
120
|
-
ctx: ExtensionContext,
|
|
121
|
-
token: symbol,
|
|
122
|
-
goal: boolean,
|
|
123
|
-
error: unknown,
|
|
124
|
-
): void => {
|
|
125
|
-
if (!request || request.token !== token) return;
|
|
128
|
+
const finishFailure = (ctx: ExtensionContext, goal: boolean, error: unknown): void => {
|
|
126
129
|
request = undefined;
|
|
127
130
|
if (goal && dependencies.goal) {
|
|
128
131
|
try {
|
|
@@ -135,6 +138,33 @@ export function registerAutoCompaction(
|
|
|
135
138
|
notifyFailure(ctx, error);
|
|
136
139
|
};
|
|
137
140
|
|
|
141
|
+
/** Ends a request with Pi's expected eligibility rejection: silent, rearmed, and never a failure. */
|
|
142
|
+
const finishSkip = (ctx: ExtensionContext, goal: boolean): void => {
|
|
143
|
+
request = undefined;
|
|
144
|
+
armed = true;
|
|
145
|
+
if (!goal) return;
|
|
146
|
+
try {
|
|
147
|
+
dependencies.goal?.onSkipped(ctx);
|
|
148
|
+
} catch (callbackError) {
|
|
149
|
+
notifyFailure(ctx, callbackError);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Single entry point for rejected requests: classifies before any sanitization, stale tokens stay inert. */
|
|
154
|
+
const finishRequestError = (
|
|
155
|
+
ctx: ExtensionContext,
|
|
156
|
+
token: symbol,
|
|
157
|
+
goal: boolean,
|
|
158
|
+
error: unknown,
|
|
159
|
+
): void => {
|
|
160
|
+
if (!request || request.token !== token) return;
|
|
161
|
+
if (isSessionTooSmallCompactionError(error)) {
|
|
162
|
+
finishSkip(ctx, goal);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
finishFailure(ctx, goal, error);
|
|
166
|
+
};
|
|
167
|
+
|
|
138
168
|
pi.on("turn_end", (_event, ctx) => {
|
|
139
169
|
if (!supportedMode(ctx) || request) return;
|
|
140
170
|
|
|
@@ -179,7 +209,7 @@ export function registerAutoCompaction(
|
|
|
179
209
|
try {
|
|
180
210
|
dependencies.goal.onRequested();
|
|
181
211
|
} catch (error) {
|
|
182
|
-
finishFailure(ctx,
|
|
212
|
+
finishFailure(ctx, true, error);
|
|
183
213
|
return;
|
|
184
214
|
}
|
|
185
215
|
}
|
|
@@ -207,10 +237,10 @@ export function registerAutoCompaction(
|
|
|
207
237
|
notifyFailure(ctx, error);
|
|
208
238
|
}
|
|
209
239
|
},
|
|
210
|
-
onError: (error) =>
|
|
240
|
+
onError: (error) => finishRequestError(ctx, token, goal, error),
|
|
211
241
|
});
|
|
212
242
|
} catch (error) {
|
|
213
|
-
|
|
243
|
+
finishRequestError(ctx, token, goal, error);
|
|
214
244
|
}
|
|
215
245
|
});
|
|
216
246
|
|
package/killeros/display.ts
CHANGED
|
@@ -28,9 +28,14 @@ export function formatTime(milliseconds: number): string {
|
|
|
28
28
|
if (!Number.isFinite(milliseconds)) return "0s";
|
|
29
29
|
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1_000));
|
|
30
30
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
|
|
32
|
+
const seconds = totalSeconds % 60;
|
|
33
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
34
|
+
if (totalMinutes < 60) return `${totalMinutes}m ${seconds.toString().padStart(2, "0")}s`;
|
|
35
|
+
|
|
36
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
37
|
+
const minutes = totalMinutes % 60;
|
|
38
|
+
return `${hours}h ${minutes.toString().padStart(2, "0")}m ${seconds.toString().padStart(2, "0")}s`;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
export function formatTokens(value: number): string {
|
package/killeros/footer.ts
CHANGED
|
@@ -1,14 +1,98 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { type ExtensionAPI, type ExtensionContext, type Theme, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { truncateToWidth, visibleWidth, type TUI } from "@earendil-works/pi-tui";
|
|
3
4
|
import { isCodexFastEnabled, subscribeCodexFast } from "./codex-fast-state.ts";
|
|
4
5
|
import { formatCwd, formatTime, formatTokens, padRight } from "./display.ts";
|
|
5
|
-
import { goalElapsedMilliseconds } from "./
|
|
6
|
+
import { goalElapsedMilliseconds } from "./goal-state.ts";
|
|
6
7
|
import type { GoalRuntime, GoalState } from "./runtime.ts";
|
|
7
8
|
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
8
9
|
import { LEVEL_COLORS, type ThinkingLevel } from "./variants.ts";
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
+
const GIT_STATUS_REFRESH_INTERVAL_MS = 30_000;
|
|
11
12
|
const CODEX_PROVIDER = "openai-codex";
|
|
13
|
+
const colorDirectory = (text: string): string => `\x1B[38;2;240;248;154m${text}\x1B[39m`;
|
|
14
|
+
|
|
15
|
+
function resolveUncommittedFileCount(cwd: string): Promise<number | undefined> {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
execFile(
|
|
18
|
+
"git",
|
|
19
|
+
["-C", cwd, "status", "--porcelain=v1", "-z", "--untracked-files=all"],
|
|
20
|
+
{
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
23
|
+
timeout: 1_000,
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
},
|
|
26
|
+
(error, stdout) => {
|
|
27
|
+
if (error) {
|
|
28
|
+
resolve(undefined);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const entries = stdout.split("\0");
|
|
33
|
+
let count = 0;
|
|
34
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
35
|
+
const entry = entries[index];
|
|
36
|
+
if (!entry) continue;
|
|
37
|
+
count += 1;
|
|
38
|
+
if (entry[0] === "R" || entry[0] === "C" || entry[1] === "R" || entry[1] === "C") index += 1;
|
|
39
|
+
}
|
|
40
|
+
resolve(count);
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Coalesces Git status requests to one active scan and one queued follow-up. */
|
|
47
|
+
export function createGitStatusRefresh(
|
|
48
|
+
cwd: string,
|
|
49
|
+
onCount: (count: number | undefined) => void,
|
|
50
|
+
resolveCount: (cwd: string) => Promise<number | undefined> = resolveUncommittedFileCount,
|
|
51
|
+
): { request: () => void; dispose: () => void } {
|
|
52
|
+
let disposed = false;
|
|
53
|
+
let pending = false;
|
|
54
|
+
let queued = false;
|
|
55
|
+
const request = (): void => {
|
|
56
|
+
if (disposed) return;
|
|
57
|
+
if (pending) {
|
|
58
|
+
queued = true;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
pending = true;
|
|
62
|
+
void resolveCount(cwd).then((count) => {
|
|
63
|
+
if (!disposed) onCount(count);
|
|
64
|
+
}).finally(() => {
|
|
65
|
+
pending = false;
|
|
66
|
+
if (!disposed && queued) {
|
|
67
|
+
queued = false;
|
|
68
|
+
request();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
request,
|
|
74
|
+
dispose() {
|
|
75
|
+
disposed = true;
|
|
76
|
+
queued = false;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type ScheduleFallback = (refresh: () => void, intervalMs: number) => () => void;
|
|
82
|
+
|
|
83
|
+
const scheduleFallback: ScheduleFallback = (refresh, intervalMs) => {
|
|
84
|
+
const timer = setInterval(refresh, intervalMs);
|
|
85
|
+
timer.unref?.();
|
|
86
|
+
return () => clearInterval(timer);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Schedules the fallback Git scan independently from footer rendering. */
|
|
90
|
+
export function scheduleGitStatusFallback(
|
|
91
|
+
refresh: () => void,
|
|
92
|
+
schedule: ScheduleFallback = scheduleFallback,
|
|
93
|
+
): () => void {
|
|
94
|
+
return schedule(refresh, GIT_STATUS_REFRESH_INTERVAL_MS);
|
|
95
|
+
}
|
|
12
96
|
|
|
13
97
|
export function formatCost(usd: number): string {
|
|
14
98
|
if (!Number.isFinite(usd)) return "$—";
|
|
@@ -142,23 +226,10 @@ function renderFooter(rows: string[], width: number, theme: Theme): string[] {
|
|
|
142
226
|
return [theme.fg("borderMuted", "─".repeat(width)), ...rows];
|
|
143
227
|
}
|
|
144
228
|
|
|
145
|
-
function formatGoalElapsed(milliseconds: number): string {
|
|
146
|
-
const totalSeconds = Number.isFinite(milliseconds) ? Math.max(0, Math.floor(milliseconds / 1_000)) : 0;
|
|
147
|
-
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
148
|
-
|
|
149
|
-
const seconds = totalSeconds % 60;
|
|
150
|
-
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
151
|
-
if (totalMinutes < 60) return `${totalMinutes}m ${seconds.toString().padStart(2, "0")}s`;
|
|
152
|
-
|
|
153
|
-
const hours = Math.floor(totalMinutes / 60);
|
|
154
|
-
const minutes = totalMinutes % 60;
|
|
155
|
-
return `${hours}h ${minutes.toString().padStart(2, "0")}m ${seconds.toString().padStart(2, "0")}s`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
229
|
function formatGoalFooter(state: GoalState | undefined, theme: Theme): string {
|
|
159
230
|
if (!state) return "";
|
|
160
231
|
if (state.status === "active") {
|
|
161
|
-
return theme.fg("warning", `/goal is active (${
|
|
232
|
+
return theme.fg("warning", `/goal is active (${formatTime(goalElapsedMilliseconds(state, Date.now()))})`);
|
|
162
233
|
}
|
|
163
234
|
if (state.status === "paused") return theme.fg("warning", "/goal is paused");
|
|
164
235
|
if (state.status === "blocked") return theme.fg("error", "/goal is blocked");
|
|
@@ -172,6 +243,7 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
172
243
|
let cachedSessionCost = 0;
|
|
173
244
|
let sessionCostDirty = true;
|
|
174
245
|
let unsubscribeCodexFast: (() => void) | undefined;
|
|
246
|
+
let requestGitStatusRefresh: (() => void) | undefined;
|
|
175
247
|
const resetSessionCost = (): void => {
|
|
176
248
|
cachedSessionCost = 0;
|
|
177
249
|
sessionCostDirty = true;
|
|
@@ -201,13 +273,25 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
201
273
|
|
|
202
274
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
203
275
|
activeTui = tui;
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
276
|
+
let uncommittedFileCount: number | undefined;
|
|
277
|
+
const gitStatus = createGitStatusRefresh(ctx.cwd, (count) => {
|
|
278
|
+
if (count === uncommittedFileCount) return;
|
|
279
|
+
uncommittedFileCount = count;
|
|
280
|
+
tui.requestRender();
|
|
281
|
+
});
|
|
282
|
+
requestGitStatusRefresh = gitStatus.request;
|
|
283
|
+
const unsubscribe = footerData.onBranchChange(() => {
|
|
284
|
+
gitStatus.request();
|
|
285
|
+
tui.requestRender();
|
|
286
|
+
});
|
|
287
|
+
gitStatus.request();
|
|
288
|
+
const stopFallback = scheduleGitStatusFallback(gitStatus.request);
|
|
207
289
|
return {
|
|
208
290
|
dispose() {
|
|
209
291
|
unsubscribe();
|
|
210
|
-
|
|
292
|
+
stopFallback();
|
|
293
|
+
gitStatus.dispose();
|
|
294
|
+
if (requestGitStatusRefresh === gitStatus.request) requestGitStatusRefresh = undefined;
|
|
211
295
|
if (activeTui === tui) activeTui = undefined;
|
|
212
296
|
},
|
|
213
297
|
invalidate() {},
|
|
@@ -227,8 +311,8 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
227
311
|
const context = formatContextProgress(usage?.tokens ?? null, contextWindow, theme);
|
|
228
312
|
const branch = footerData.getGitBranch();
|
|
229
313
|
const signature = formatModel(model, theme, true, isCodexFastEnabled());
|
|
230
|
-
const fullDirectory =
|
|
231
|
-
const focusedDirectory =
|
|
314
|
+
const fullDirectory = colorDirectory(cwd);
|
|
315
|
+
const focusedDirectory = colorDirectory(compactDirectory(cwd));
|
|
232
316
|
const goal = formatGoalFooter(goalRuntime.state, theme);
|
|
233
317
|
const primary = joinFooterParts([
|
|
234
318
|
signature,
|
|
@@ -248,7 +332,11 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
248
332
|
: footerRowFits(primaryFocused, "", width)
|
|
249
333
|
? renderFooterRow(primaryFocused, "", width)
|
|
250
334
|
: renderFooterRow(essentialModel, context, width);
|
|
251
|
-
const branchLabel = branch
|
|
335
|
+
const branchLabel = branch
|
|
336
|
+
? uncommittedFileCount
|
|
337
|
+
? `${theme.fg("dim", `${branch} · `)}${theme.fg("warning", `${uncommittedFileCount} changed`)}`
|
|
338
|
+
: theme.fg("dim", branch)
|
|
339
|
+
: "";
|
|
252
340
|
const workspaceRight = goal || fullDirectory;
|
|
253
341
|
const secondaryRow = footerRowFits(branchLabel, workspaceRight, width)
|
|
254
342
|
? renderFooterRow(branchLabel, workspaceRight, width)
|
|
@@ -271,8 +359,12 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
271
359
|
thinkingLevel = event.level;
|
|
272
360
|
activeTui?.requestRender();
|
|
273
361
|
});
|
|
274
|
-
|
|
275
|
-
|
|
362
|
+
const refreshAfterActivity = (): void => {
|
|
363
|
+
invalidateSessionCost();
|
|
364
|
+
requestGitStatusRefresh?.();
|
|
365
|
+
};
|
|
366
|
+
pi.on("turn_end", refreshAfterActivity);
|
|
367
|
+
pi.on("session_compact", refreshAfterActivity);
|
|
276
368
|
pi.on("session_tree", () => {
|
|
277
369
|
resetSessionCost();
|
|
278
370
|
activeTui?.requestRender();
|
|
@@ -282,6 +374,7 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
282
374
|
unsubscribeCodexFast = undefined;
|
|
283
375
|
resetSessionCost();
|
|
284
376
|
activeTui = undefined;
|
|
377
|
+
requestGitStatusRefresh = undefined;
|
|
285
378
|
goalRuntime.requestRender = undefined;
|
|
286
379
|
});
|
|
287
380
|
}
|