@oh-my-pi/pi-coding-agent 3.8.1337 → 3.9.1337
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 +12 -0
- package/package.json +4 -4
- package/src/core/settings-manager.ts +2 -2
- package/src/core/tools/lsp/client.ts +3 -0
- package/src/modes/interactive/components/{footer.ts → status-line.ts} +124 -71
- package/src/modes/interactive/interactive-mode.ts +34 -19
- package/src/modes/interactive/theme/dark.json +13 -13
- package/src/modes/interactive/theme/light.json +13 -13
- package/src/modes/interactive/theme/theme.ts +29 -28
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.9.1337] - 2026-01-04
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Changed default for `lsp.formatOnWrite` setting from `true` to `false`
|
|
10
|
+
- Updated status line thinking level display to use emoji icons instead of abbreviated text
|
|
11
|
+
- Changed auto-compact indicator from "(auto)" text to icon
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed stale diagnostics persisting after file content changes in LSP client
|
|
16
|
+
|
|
5
17
|
## [3.8.1337] - 2026-01-04
|
|
6
18
|
### Added
|
|
7
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.1337",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-agent-core": "3.
|
|
43
|
-
"@oh-my-pi/pi-ai": "3.
|
|
44
|
-
"@oh-my-pi/pi-tui": "3.
|
|
42
|
+
"@oh-my-pi/pi-agent-core": "3.9.1337",
|
|
43
|
+
"@oh-my-pi/pi-ai": "3.9.1337",
|
|
44
|
+
"@oh-my-pi/pi-tui": "3.9.1337",
|
|
45
45
|
"@sinclair/typebox": "^0.34.46",
|
|
46
46
|
"ajv": "^8.17.1",
|
|
47
47
|
"chalk": "^5.5.0",
|
|
@@ -59,7 +59,7 @@ export interface MCPSettings {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export interface LspSettings {
|
|
62
|
-
formatOnWrite?: boolean; // default:
|
|
62
|
+
formatOnWrite?: boolean; // default: false (format files using LSP after write tool writes code files)
|
|
63
63
|
diagnosticsOnWrite?: boolean; // default: true (return LSP diagnostics after write tool writes code files)
|
|
64
64
|
diagnosticsOnEdit?: boolean; // default: false (return LSP diagnostics after edit tool edits code files)
|
|
65
65
|
}
|
|
@@ -539,7 +539,7 @@ export class SettingsManager {
|
|
|
539
539
|
}
|
|
540
540
|
|
|
541
541
|
getLspFormatOnWrite(): boolean {
|
|
542
|
-
return this.settings.lsp?.formatOnWrite ??
|
|
542
|
+
return this.settings.lsp?.formatOnWrite ?? false;
|
|
543
543
|
}
|
|
544
544
|
|
|
545
545
|
setLspFormatOnWrite(enabled: boolean): void {
|
|
@@ -526,6 +526,9 @@ export async function syncContent(client: LspClient, filePath: string, content:
|
|
|
526
526
|
}
|
|
527
527
|
|
|
528
528
|
const syncPromise = (async () => {
|
|
529
|
+
// Clear stale diagnostics before syncing new content
|
|
530
|
+
client.diagnostics.delete(uri);
|
|
531
|
+
|
|
529
532
|
const info = client.openFiles.get(uri);
|
|
530
533
|
|
|
531
534
|
if (!info) {
|
|
@@ -6,15 +6,43 @@ import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui"
|
|
|
6
6
|
import type { AgentSession } from "../../../core/agent-session";
|
|
7
7
|
import { theme } from "../theme/theme";
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// Thinking level icons (Nerd Font)
|
|
10
|
+
const THINKING_ICONS: Record<string, string> = {
|
|
11
|
+
minimal: "🤨 min",
|
|
12
|
+
low: "🤔 low",
|
|
13
|
+
medium: "🤓 mid",
|
|
14
|
+
high: "🤯 high",
|
|
15
|
+
xhigh: "🧠 xhi",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Nerd Font icons
|
|
10
19
|
const ICONS = {
|
|
11
20
|
model: "\uf4bc", // robot/model
|
|
12
21
|
folder: "\uf115", // folder
|
|
13
|
-
branch: "\
|
|
22
|
+
branch: "\ue725", // git branch
|
|
14
23
|
sep: "\ue0b1", // powerline thin chevron
|
|
15
|
-
tokens: "\
|
|
24
|
+
tokens: "\ue26b", // coins
|
|
25
|
+
context: "\ue70f", // window
|
|
26
|
+
auto: "\udb80\udc68", // auto
|
|
16
27
|
} as const;
|
|
17
28
|
|
|
29
|
+
/** Create a colored text segment with background */
|
|
30
|
+
function plSegment(content: string, fgAnsi: string, bgAnsi: string): string {
|
|
31
|
+
return `${bgAnsi}${fgAnsi} ${content} \x1b[0m`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Create separator with background */
|
|
35
|
+
function plSep(sepAnsi: string, bgAnsi: string): string {
|
|
36
|
+
return `${bgAnsi}${sepAnsi}${ICONS.sep}\x1b[0m`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Create end cap - solid arrow transitioning bg to terminal default */
|
|
40
|
+
function plEnd(bgAnsi: string): string {
|
|
41
|
+
// Use the bg color as fg for the arrow (creates the triangle effect)
|
|
42
|
+
const fgFromBg = bgAnsi.replace("\x1b[48;", "\x1b[38;");
|
|
43
|
+
return `${fgFromBg}\ue0b0\x1b[0m`;
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
/**
|
|
19
47
|
* Sanitize text for display in a single-line status.
|
|
20
48
|
* Removes newlines, tabs, carriage returns, and other control characters.
|
|
@@ -50,7 +78,7 @@ function findGitHeadPath(): string | null {
|
|
|
50
78
|
/**
|
|
51
79
|
* Footer component that shows pwd, token stats, and context usage
|
|
52
80
|
*/
|
|
53
|
-
export class
|
|
81
|
+
export class StatusLineComponent implements Component {
|
|
54
82
|
private session: AgentSession;
|
|
55
83
|
private cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name
|
|
56
84
|
private gitWatcher: FSWatcher | null = null;
|
|
@@ -220,7 +248,7 @@ export class FooterComponent implements Component {
|
|
|
220
248
|
}
|
|
221
249
|
}
|
|
222
250
|
|
|
223
|
-
|
|
251
|
+
private buildStatusLine(): string {
|
|
224
252
|
const state = this.session.state;
|
|
225
253
|
|
|
226
254
|
// Calculate cumulative usage from ALL session entries
|
|
@@ -264,132 +292,157 @@ export class FooterComponent implements Component {
|
|
|
264
292
|
return `${Math.round(n / 1000000)}M`;
|
|
265
293
|
};
|
|
266
294
|
|
|
267
|
-
// Powerline separator (very dim)
|
|
268
|
-
const sep = theme.fg("footerSep", ` ${ICONS.sep} `);
|
|
269
|
-
|
|
270
295
|
// ═══════════════════════════════════════════════════════════════════════
|
|
271
|
-
// SEGMENT 1: Model
|
|
296
|
+
// SEGMENT 1: Model
|
|
272
297
|
// ═══════════════════════════════════════════════════════════════════════
|
|
273
|
-
|
|
274
|
-
|
|
298
|
+
let modelName = state.model?.name || state.model?.id || "no-model";
|
|
299
|
+
// Strip "Claude " prefix for brevity
|
|
300
|
+
if (modelName.startsWith("Claude ")) {
|
|
301
|
+
modelName = modelName.slice(7);
|
|
302
|
+
}
|
|
303
|
+
let modelContent = `${ICONS.model} ${modelName}`;
|
|
275
304
|
if (state.model?.reasoning) {
|
|
276
305
|
const level = state.thinkingLevel || "off";
|
|
277
306
|
if (level !== "off") {
|
|
278
|
-
|
|
307
|
+
modelContent += ` · ${THINKING_ICONS[level] ?? level}`;
|
|
279
308
|
}
|
|
280
309
|
}
|
|
281
310
|
|
|
282
311
|
// ═══════════════════════════════════════════════════════════════════════
|
|
283
|
-
// SEGMENT 2: Path
|
|
284
|
-
// Replace home with ~, strip /work/, color separators
|
|
312
|
+
// SEGMENT 2: Path
|
|
285
313
|
// ═══════════════════════════════════════════════════════════════════════
|
|
286
314
|
let pwd = process.cwd();
|
|
287
315
|
const home = process.env.HOME || process.env.USERPROFILE;
|
|
288
316
|
if (home && pwd.startsWith(home)) {
|
|
289
317
|
pwd = `~${pwd.slice(home.length)}`;
|
|
290
318
|
}
|
|
291
|
-
// Strip /work/ prefix
|
|
292
319
|
if (pwd.startsWith("/work/")) {
|
|
293
320
|
pwd = pwd.slice(6);
|
|
294
321
|
}
|
|
295
|
-
|
|
296
|
-
const pathColored = pwd
|
|
297
|
-
.split("/")
|
|
298
|
-
.map((part) => theme.fg("footerPath", part))
|
|
299
|
-
.join(theme.fg("footerSep", "/"));
|
|
300
|
-
const pathSegment = theme.fg("footerIcon", `${ICONS.folder} `) + pathColored;
|
|
322
|
+
const pathContent = `${ICONS.folder} ${pwd}`;
|
|
301
323
|
|
|
302
324
|
// ═══════════════════════════════════════════════════════════════════════
|
|
303
|
-
// SEGMENT 3: Git Branch + Status
|
|
325
|
+
// SEGMENT 3: Git Branch + Status
|
|
304
326
|
// ═══════════════════════════════════════════════════════════════════════
|
|
305
327
|
const branch = this.getCurrentBranch();
|
|
306
|
-
let
|
|
328
|
+
let gitContent = "";
|
|
329
|
+
let gitColorName: "statusLineGitClean" | "statusLineGitDirty" = "statusLineGitClean";
|
|
307
330
|
if (branch) {
|
|
308
331
|
const gitStatus = this.getGitStatus();
|
|
309
332
|
const isDirty = gitStatus && (gitStatus.staged > 0 || gitStatus.unstaged > 0 || gitStatus.untracked > 0);
|
|
333
|
+
gitColorName = isDirty ? "statusLineGitDirty" : "statusLineGitClean";
|
|
310
334
|
|
|
311
|
-
|
|
312
|
-
const branchColor = isDirty ? "footerDirty" : "footerBranch";
|
|
313
|
-
gitSegment = theme.fg("footerIcon", `${ICONS.branch} `) + theme.fg(branchColor, branch);
|
|
335
|
+
gitContent = `${ICONS.branch} ${branch}`;
|
|
314
336
|
|
|
315
|
-
// Add status indicators
|
|
316
337
|
if (gitStatus) {
|
|
317
338
|
const indicators: string[] = [];
|
|
318
339
|
if (gitStatus.unstaged > 0) {
|
|
319
|
-
indicators.push(theme.fg("
|
|
340
|
+
indicators.push(theme.fg("statusLineDirty", `*${gitStatus.unstaged}`));
|
|
320
341
|
}
|
|
321
342
|
if (gitStatus.staged > 0) {
|
|
322
|
-
indicators.push(theme.fg("
|
|
343
|
+
indicators.push(theme.fg("statusLineStaged", `+${gitStatus.staged}`));
|
|
323
344
|
}
|
|
324
345
|
if (gitStatus.untracked > 0) {
|
|
325
|
-
indicators.push(theme.fg("
|
|
346
|
+
indicators.push(theme.fg("statusLineUntracked", `?${gitStatus.untracked}`));
|
|
326
347
|
}
|
|
327
348
|
if (indicators.length > 0) {
|
|
328
|
-
|
|
349
|
+
gitContent += ` ${indicators.join(" ")}`;
|
|
329
350
|
}
|
|
330
351
|
}
|
|
331
352
|
}
|
|
332
353
|
|
|
333
354
|
// ═══════════════════════════════════════════════════════════════════════
|
|
334
|
-
// SEGMENT 4:
|
|
335
|
-
// Concise: total tokens, cost, context%
|
|
355
|
+
// SEGMENT 4: Context (window usage)
|
|
336
356
|
// ═══════════════════════════════════════════════════════════════════════
|
|
337
|
-
const
|
|
357
|
+
const autoIndicator = this.autoCompactEnabled ? ` ${ICONS.auto}` : "";
|
|
358
|
+
const contextText = `${contextPercentValue.toFixed(1)}%/${formatTokens(contextWindow)}${autoIndicator}`;
|
|
359
|
+
let contextContent: string;
|
|
360
|
+
if (contextPercentValue > 90) {
|
|
361
|
+
contextContent = `${ICONS.context} ${theme.fg("error", contextText)}`;
|
|
362
|
+
} else if (contextPercentValue > 70) {
|
|
363
|
+
contextContent = `${ICONS.context} ${theme.fg("warning", contextText)}`;
|
|
364
|
+
} else {
|
|
365
|
+
contextContent = `${ICONS.context} ${contextText}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
369
|
+
// SEGMENT 5: Spend (tokens + cost)
|
|
370
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
371
|
+
const spendParts: string[] = [];
|
|
338
372
|
|
|
339
|
-
// Total tokens (input + output + cache)
|
|
340
373
|
const totalTokens = totalInput + totalOutput + totalCacheRead + totalCacheWrite;
|
|
341
374
|
if (totalTokens) {
|
|
342
|
-
|
|
375
|
+
spendParts.push(`${ICONS.tokens} ${formatTokens(totalTokens)}`);
|
|
343
376
|
}
|
|
344
377
|
|
|
345
|
-
// Cost (pink)
|
|
346
378
|
const usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;
|
|
347
379
|
if (totalCost || usingSubscription) {
|
|
348
|
-
const costDisplay = `$${totalCost.toFixed(
|
|
349
|
-
|
|
380
|
+
const costDisplay = `$${totalCost.toFixed(2)}${usingSubscription ? " (sub)" : ""}`;
|
|
381
|
+
spendParts.push(costDisplay);
|
|
350
382
|
}
|
|
351
383
|
|
|
352
|
-
|
|
353
|
-
const autoIndicator = this.autoCompactEnabled ? " (auto)" : "";
|
|
354
|
-
const contextDisplay = `${contextPercentValue.toFixed(1)}%/${formatTokens(contextWindow)}${autoIndicator}`;
|
|
355
|
-
let contextColored: string;
|
|
356
|
-
if (contextPercentValue > 90) {
|
|
357
|
-
contextColored = theme.fg("error", contextDisplay);
|
|
358
|
-
} else if (contextPercentValue > 70) {
|
|
359
|
-
contextColored = theme.fg("warning", contextDisplay);
|
|
360
|
-
} else {
|
|
361
|
-
contextColored = theme.fg("footerSep", contextDisplay);
|
|
362
|
-
}
|
|
363
|
-
statParts.push(contextColored);
|
|
364
|
-
|
|
365
|
-
const statsSegment = statParts.join(" ");
|
|
384
|
+
const spendContent = theme.fg("statusLineCost", spendParts.join(" · "));
|
|
366
385
|
|
|
367
386
|
// ═══════════════════════════════════════════════════════════════════════
|
|
368
|
-
// Assemble
|
|
369
|
-
// [Model] > [Path] > [Git] > [Stats]
|
|
387
|
+
// Assemble: [Model] > [Path] > [Git?] > [Context] > [Spend] >
|
|
370
388
|
// ═══════════════════════════════════════════════════════════════════════
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
389
|
+
const bgAnsi = theme.getBgAnsi("statusLineBg");
|
|
390
|
+
const sepAnsi = theme.getFgAnsi("statusLineSep");
|
|
391
|
+
|
|
392
|
+
let statusLine = "";
|
|
393
|
+
|
|
394
|
+
// Model segment
|
|
395
|
+
statusLine += plSegment(modelContent, theme.getFgAnsi("statusLineModel"), bgAnsi);
|
|
396
|
+
statusLine += plSep(sepAnsi, bgAnsi);
|
|
374
397
|
|
|
375
|
-
|
|
398
|
+
// Path segment
|
|
399
|
+
statusLine += plSegment(pathContent, theme.getFgAnsi("statusLinePath"), bgAnsi);
|
|
376
400
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
statusLine
|
|
401
|
+
if (gitContent) {
|
|
402
|
+
statusLine += plSep(sepAnsi, bgAnsi);
|
|
403
|
+
statusLine += plSegment(gitContent, theme.getFgAnsi(gitColorName), bgAnsi);
|
|
380
404
|
}
|
|
381
405
|
|
|
382
|
-
|
|
406
|
+
// Context segment
|
|
407
|
+
statusLine += plSep(sepAnsi, bgAnsi);
|
|
408
|
+
statusLine += plSegment(contextContent, theme.getFgAnsi("statusLineContext"), bgAnsi);
|
|
409
|
+
|
|
410
|
+
// Spend segment
|
|
411
|
+
statusLine += plSep(sepAnsi, bgAnsi);
|
|
412
|
+
statusLine += plSegment(spendContent, theme.getFgAnsi("statusLineSpend"), bgAnsi);
|
|
413
|
+
|
|
414
|
+
// End cap (solid arrow to terminal bg)
|
|
415
|
+
statusLine += plEnd(bgAnsi);
|
|
416
|
+
|
|
417
|
+
return statusLine;
|
|
418
|
+
}
|
|
383
419
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
420
|
+
/**
|
|
421
|
+
* Get the status line content for use as editor top border.
|
|
422
|
+
* Returns the content string and its visible width.
|
|
423
|
+
*/
|
|
424
|
+
getTopBorder(_width: number): { content: string; width: number } {
|
|
425
|
+
const content = this.buildStatusLine();
|
|
426
|
+
return {
|
|
427
|
+
content,
|
|
428
|
+
width: visibleWidth(content),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Render only hook statuses (if any).
|
|
434
|
+
* Used when footer is integrated into editor border.
|
|
435
|
+
*/
|
|
436
|
+
render(width: number): string[] {
|
|
437
|
+
// Only render hook statuses - main status is in editor's top border
|
|
438
|
+
if (this.hookStatuses.size === 0) {
|
|
439
|
+
return [];
|
|
391
440
|
}
|
|
392
441
|
|
|
393
|
-
|
|
442
|
+
const sortedStatuses = Array.from(this.hookStatuses.entries())
|
|
443
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
444
|
+
.map(([, text]) => sanitizeStatusText(text));
|
|
445
|
+
const hookLine = sortedStatuses.join(" ");
|
|
446
|
+
return [truncateToWidth(hookLine, width, theme.fg("statusLineSep", "…"))];
|
|
394
447
|
}
|
|
395
448
|
}
|
|
@@ -43,7 +43,6 @@ import { CompactionSummaryMessageComponent } from "./components/compaction-summa
|
|
|
43
43
|
import { CustomEditor } from "./components/custom-editor";
|
|
44
44
|
import { DynamicBorder } from "./components/dynamic-border";
|
|
45
45
|
import { ExtensionDashboard } from "./components/extensions";
|
|
46
|
-
import { FooterComponent } from "./components/footer";
|
|
47
46
|
import { HookEditorComponent } from "./components/hook-editor";
|
|
48
47
|
import { HookInputComponent } from "./components/hook-input";
|
|
49
48
|
import { HookMessageComponent } from "./components/hook-message";
|
|
@@ -52,6 +51,7 @@ import { ModelSelectorComponent } from "./components/model-selector";
|
|
|
52
51
|
import { OAuthSelectorComponent } from "./components/oauth-selector";
|
|
53
52
|
import { SessionSelectorComponent } from "./components/session-selector";
|
|
54
53
|
import { SettingsSelectorComponent } from "./components/settings-selector";
|
|
54
|
+
import { StatusLineComponent } from "./components/status-line";
|
|
55
55
|
import { ToolExecutionComponent } from "./components/tool-execution";
|
|
56
56
|
import { TreeSelectorComponent } from "./components/tree-selector";
|
|
57
57
|
import { TtsrNotificationComponent } from "./components/ttsr-notification";
|
|
@@ -85,7 +85,7 @@ export class InteractiveMode {
|
|
|
85
85
|
private statusContainer: Container;
|
|
86
86
|
private editor: CustomEditor;
|
|
87
87
|
private editorContainer: Container;
|
|
88
|
-
private
|
|
88
|
+
private statusLine: StatusLineComponent;
|
|
89
89
|
private version: string;
|
|
90
90
|
private isInitialized = false;
|
|
91
91
|
private onInputCallback?: (input: { text: string; images?: ImageContent[] }) => void;
|
|
@@ -176,8 +176,8 @@ export class InteractiveMode {
|
|
|
176
176
|
this.editor = new CustomEditor(getEditorTheme());
|
|
177
177
|
this.editorContainer = new Container();
|
|
178
178
|
this.editorContainer.addChild(this.editor);
|
|
179
|
-
this.
|
|
180
|
-
this.
|
|
179
|
+
this.statusLine = new StatusLineComponent(session);
|
|
180
|
+
this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
|
|
181
181
|
|
|
182
182
|
// Define slash commands for autocomplete
|
|
183
183
|
const slashCommands: SlashCommand[] = [
|
|
@@ -287,7 +287,7 @@ export class InteractiveMode {
|
|
|
287
287
|
this.ui.addChild(this.statusContainer);
|
|
288
288
|
this.ui.addChild(new Spacer(1));
|
|
289
289
|
this.ui.addChild(this.editorContainer);
|
|
290
|
-
this.ui.addChild(this.
|
|
290
|
+
this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
|
|
291
291
|
this.ui.setFocus(this.editor);
|
|
292
292
|
|
|
293
293
|
this.setupKeyHandlers();
|
|
@@ -311,9 +311,13 @@ export class InteractiveMode {
|
|
|
311
311
|
});
|
|
312
312
|
|
|
313
313
|
// Set up git branch watcher
|
|
314
|
-
this.
|
|
314
|
+
this.statusLine.watchBranch(() => {
|
|
315
|
+
this.updateEditorTopBorder();
|
|
315
316
|
this.ui.requestRender();
|
|
316
317
|
});
|
|
318
|
+
|
|
319
|
+
// Initial top border update
|
|
320
|
+
this.updateEditorTopBorder();
|
|
317
321
|
}
|
|
318
322
|
|
|
319
323
|
// =========================================================================
|
|
@@ -492,7 +496,7 @@ export class InteractiveMode {
|
|
|
492
496
|
* Set hook status text in the footer.
|
|
493
497
|
*/
|
|
494
498
|
private setHookStatus(key: string, text: string | undefined): void {
|
|
495
|
-
this.
|
|
499
|
+
this.statusLine.setHookStatus(key, text);
|
|
496
500
|
this.ui.requestRender();
|
|
497
501
|
}
|
|
498
502
|
|
|
@@ -934,7 +938,8 @@ export class InteractiveMode {
|
|
|
934
938
|
await this.init();
|
|
935
939
|
}
|
|
936
940
|
|
|
937
|
-
this.
|
|
941
|
+
this.statusLine.invalidate();
|
|
942
|
+
this.updateEditorTopBorder();
|
|
938
943
|
|
|
939
944
|
switch (event.type) {
|
|
940
945
|
case "agent_start":
|
|
@@ -1039,7 +1044,8 @@ export class InteractiveMode {
|
|
|
1039
1044
|
}
|
|
1040
1045
|
this.streamingComponent = undefined;
|
|
1041
1046
|
this.streamingMessage = undefined;
|
|
1042
|
-
this.
|
|
1047
|
+
this.statusLine.invalidate();
|
|
1048
|
+
this.updateEditorTopBorder();
|
|
1043
1049
|
}
|
|
1044
1050
|
this.ui.requestRender();
|
|
1045
1051
|
break;
|
|
@@ -1147,7 +1153,8 @@ export class InteractiveMode {
|
|
|
1147
1153
|
summary: event.result.summary,
|
|
1148
1154
|
timestamp: Date.now(),
|
|
1149
1155
|
});
|
|
1150
|
-
this.
|
|
1156
|
+
this.statusLine.invalidate();
|
|
1157
|
+
this.updateEditorTopBorder();
|
|
1151
1158
|
}
|
|
1152
1159
|
this.ui.requestRender();
|
|
1153
1160
|
break;
|
|
@@ -1324,7 +1331,7 @@ export class InteractiveMode {
|
|
|
1324
1331
|
this.pendingTools.clear();
|
|
1325
1332
|
|
|
1326
1333
|
if (options.updateFooter) {
|
|
1327
|
-
this.
|
|
1334
|
+
this.statusLine.invalidate();
|
|
1328
1335
|
this.updateEditorBorderColor();
|
|
1329
1336
|
}
|
|
1330
1337
|
|
|
@@ -1492,17 +1499,24 @@ export class InteractiveMode {
|
|
|
1492
1499
|
const level = this.session.thinkingLevel || "off";
|
|
1493
1500
|
this.editor.borderColor = theme.getThinkingBorderColor(level);
|
|
1494
1501
|
}
|
|
1502
|
+
// Update footer content in editor's top border
|
|
1503
|
+
this.updateEditorTopBorder();
|
|
1495
1504
|
this.ui.requestRender();
|
|
1496
1505
|
}
|
|
1497
1506
|
|
|
1507
|
+
private updateEditorTopBorder(): void {
|
|
1508
|
+
const width = this.ui.getWidth();
|
|
1509
|
+
const topBorder = this.statusLine.getTopBorder(width);
|
|
1510
|
+
this.editor.setTopBorder(topBorder);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1498
1513
|
private cycleThinkingLevel(): void {
|
|
1499
1514
|
const newLevel = this.session.cycleThinkingLevel();
|
|
1500
1515
|
if (newLevel === undefined) {
|
|
1501
1516
|
this.showStatus("Current model does not support thinking");
|
|
1502
1517
|
} else {
|
|
1503
|
-
this.
|
|
1518
|
+
this.statusLine.invalidate();
|
|
1504
1519
|
this.updateEditorBorderColor();
|
|
1505
|
-
this.showStatus(`Thinking level: ${newLevel}`);
|
|
1506
1520
|
}
|
|
1507
1521
|
}
|
|
1508
1522
|
|
|
@@ -1513,7 +1527,7 @@ export class InteractiveMode {
|
|
|
1513
1527
|
const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
|
|
1514
1528
|
this.showStatus(msg);
|
|
1515
1529
|
} else {
|
|
1516
|
-
this.
|
|
1530
|
+
this.statusLine.invalidate();
|
|
1517
1531
|
this.updateEditorBorderColor();
|
|
1518
1532
|
const thinkingStr =
|
|
1519
1533
|
result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
|
|
@@ -1749,7 +1763,7 @@ export class InteractiveMode {
|
|
|
1749
1763
|
// Session-managed settings (not in SettingsManager)
|
|
1750
1764
|
case "autoCompact":
|
|
1751
1765
|
this.session.setAutoCompactionEnabled(value as boolean);
|
|
1752
|
-
this.
|
|
1766
|
+
this.statusLine.setAutoCompactEnabled(value as boolean);
|
|
1753
1767
|
break;
|
|
1754
1768
|
case "queueMode":
|
|
1755
1769
|
this.session.setQueueMode(value as "all" | "one-at-a-time");
|
|
@@ -1759,7 +1773,7 @@ export class InteractiveMode {
|
|
|
1759
1773
|
break;
|
|
1760
1774
|
case "thinkingLevel":
|
|
1761
1775
|
this.session.setThinkingLevel(value as ThinkingLevel);
|
|
1762
|
-
this.
|
|
1776
|
+
this.statusLine.invalidate();
|
|
1763
1777
|
this.updateEditorBorderColor();
|
|
1764
1778
|
break;
|
|
1765
1779
|
|
|
@@ -1808,7 +1822,7 @@ export class InteractiveMode {
|
|
|
1808
1822
|
// Only update agent state for default role
|
|
1809
1823
|
if (role === "default") {
|
|
1810
1824
|
await this.session.setModel(model, role);
|
|
1811
|
-
this.
|
|
1825
|
+
this.statusLine.invalidate();
|
|
1812
1826
|
this.updateEditorBorderColor();
|
|
1813
1827
|
}
|
|
1814
1828
|
// For other roles (small), just show status - settings already updated by selector
|
|
@@ -2533,7 +2547,8 @@ export class InteractiveMode {
|
|
|
2533
2547
|
const msg = createCompactionSummaryMessage(result.summary, result.tokensBefore, new Date().toISOString());
|
|
2534
2548
|
this.addMessageToChat(msg);
|
|
2535
2549
|
|
|
2536
|
-
this.
|
|
2550
|
+
this.statusLine.invalidate();
|
|
2551
|
+
this.updateEditorTopBorder();
|
|
2537
2552
|
} catch (error) {
|
|
2538
2553
|
const message = error instanceof Error ? error.message : String(error);
|
|
2539
2554
|
if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
|
|
@@ -2553,7 +2568,7 @@ export class InteractiveMode {
|
|
|
2553
2568
|
this.loadingAnimation.stop();
|
|
2554
2569
|
this.loadingAnimation = undefined;
|
|
2555
2570
|
}
|
|
2556
|
-
this.
|
|
2571
|
+
this.statusLine.dispose();
|
|
2557
2572
|
if (this.unsubscribe) {
|
|
2558
2573
|
this.unsubscribe();
|
|
2559
2574
|
}
|
|
@@ -79,19 +79,19 @@
|
|
|
79
79
|
|
|
80
80
|
"bashMode": "cyan",
|
|
81
81
|
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
82
|
+
"statusLineBg": "#121212",
|
|
83
|
+
"statusLineSep": 244,
|
|
84
|
+
"statusLineModel": "#d787af",
|
|
85
|
+
"statusLinePath": "#00afaf",
|
|
86
|
+
"statusLineGitClean": "#5faf5f",
|
|
87
|
+
"statusLineGitDirty": "#d7af5f",
|
|
88
|
+
"statusLineContext": "#8787af",
|
|
89
|
+
"statusLineSpend": "#5fafaf",
|
|
90
|
+
"statusLineStaged": 70,
|
|
91
|
+
"statusLineDirty": 178,
|
|
92
|
+
"statusLineUntracked": 39,
|
|
93
|
+
"statusLineOutput": 205,
|
|
94
|
+
"statusLineCost": 205
|
|
95
95
|
},
|
|
96
96
|
"export": {
|
|
97
97
|
"pageBg": "#18181e",
|
|
@@ -76,19 +76,19 @@
|
|
|
76
76
|
|
|
77
77
|
"bashMode": "green",
|
|
78
78
|
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
79
|
+
"statusLineBg": "#e0e0e0",
|
|
80
|
+
"statusLineSep": "#808080",
|
|
81
|
+
"statusLineModel": "#875f87",
|
|
82
|
+
"statusLinePath": "#005f87",
|
|
83
|
+
"statusLineGitClean": "#005f00",
|
|
84
|
+
"statusLineGitDirty": "#af5f00",
|
|
85
|
+
"statusLineContext": "#5f5f87",
|
|
86
|
+
"statusLineSpend": "#005f5f",
|
|
87
|
+
"statusLineStaged": 28,
|
|
88
|
+
"statusLineDirty": 136,
|
|
89
|
+
"statusLineUntracked": 31,
|
|
90
|
+
"statusLineOutput": 133,
|
|
91
|
+
"statusLineCost": 133
|
|
92
92
|
},
|
|
93
93
|
"export": {
|
|
94
94
|
"pageBg": "#f8f8f8",
|
|
@@ -85,20 +85,20 @@ const ThemeJsonSchema = Type.Object({
|
|
|
85
85
|
thinkingXhigh: ColorValueSchema,
|
|
86
86
|
// Bash Mode (1 color)
|
|
87
87
|
bashMode: ColorValueSchema,
|
|
88
|
-
// Footer Status Line
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
88
|
+
// Footer Status Line
|
|
89
|
+
statusLineBg: ColorValueSchema,
|
|
90
|
+
statusLineSep: ColorValueSchema,
|
|
91
|
+
statusLineModel: ColorValueSchema,
|
|
92
|
+
statusLinePath: ColorValueSchema,
|
|
93
|
+
statusLineGitClean: ColorValueSchema,
|
|
94
|
+
statusLineGitDirty: ColorValueSchema,
|
|
95
|
+
statusLineContext: ColorValueSchema,
|
|
96
|
+
statusLineSpend: ColorValueSchema,
|
|
97
|
+
statusLineStaged: ColorValueSchema,
|
|
98
|
+
statusLineDirty: ColorValueSchema,
|
|
99
|
+
statusLineUntracked: ColorValueSchema,
|
|
100
|
+
statusLineOutput: ColorValueSchema,
|
|
101
|
+
statusLineCost: ColorValueSchema,
|
|
102
102
|
}),
|
|
103
103
|
export: Type.Optional(
|
|
104
104
|
Type.Object({
|
|
@@ -160,19 +160,18 @@ export type ThemeColor =
|
|
|
160
160
|
| "thinkingHigh"
|
|
161
161
|
| "thinkingXhigh"
|
|
162
162
|
| "bashMode"
|
|
163
|
-
| "
|
|
164
|
-
| "
|
|
165
|
-
| "
|
|
166
|
-
| "
|
|
167
|
-
| "
|
|
168
|
-
| "
|
|
169
|
-
| "
|
|
170
|
-
| "
|
|
171
|
-
| "
|
|
172
|
-
| "
|
|
173
|
-
| "
|
|
174
|
-
| "
|
|
175
|
-
| "footerCost";
|
|
163
|
+
| "statusLineSep"
|
|
164
|
+
| "statusLineModel"
|
|
165
|
+
| "statusLinePath"
|
|
166
|
+
| "statusLineGitClean"
|
|
167
|
+
| "statusLineGitDirty"
|
|
168
|
+
| "statusLineContext"
|
|
169
|
+
| "statusLineSpend"
|
|
170
|
+
| "statusLineStaged"
|
|
171
|
+
| "statusLineDirty"
|
|
172
|
+
| "statusLineUntracked"
|
|
173
|
+
| "statusLineOutput"
|
|
174
|
+
| "statusLineCost";
|
|
176
175
|
|
|
177
176
|
export type ThemeBg =
|
|
178
177
|
| "selectedBg"
|
|
@@ -180,7 +179,8 @@ export type ThemeBg =
|
|
|
180
179
|
| "customMessageBg"
|
|
181
180
|
| "toolPendingBg"
|
|
182
181
|
| "toolSuccessBg"
|
|
183
|
-
| "toolErrorBg"
|
|
182
|
+
| "toolErrorBg"
|
|
183
|
+
| "statusLineBg";
|
|
184
184
|
|
|
185
185
|
type ColorMode = "truecolor" | "256color";
|
|
186
186
|
|
|
@@ -536,6 +536,7 @@ function createTheme(themeJson: ThemeJson, mode?: ColorMode): Theme {
|
|
|
536
536
|
"toolPendingBg",
|
|
537
537
|
"toolSuccessBg",
|
|
538
538
|
"toolErrorBg",
|
|
539
|
+
"statusLineBg",
|
|
539
540
|
]);
|
|
540
541
|
for (const [key, value] of Object.entries(resolvedColors)) {
|
|
541
542
|
if (bgColorKeys.has(key)) {
|