@oh-my-pi/pi-coding-agent 3.14.0 → 3.15.0
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 +79 -0
- package/docs/theme.md +38 -5
- package/examples/sdk/11-sessions.ts +2 -2
- package/package.json +7 -4
- package/src/cli/file-processor.ts +51 -2
- package/src/cli/plugin-cli.ts +25 -19
- package/src/cli/update-cli.ts +4 -3
- package/src/core/agent-session.ts +31 -4
- package/src/core/compaction/branch-summarization.ts +4 -32
- package/src/core/compaction/compaction.ts +6 -84
- package/src/core/compaction/utils.ts +2 -3
- package/src/core/custom-tools/types.ts +2 -0
- package/src/core/export-html/index.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +0 -1
- package/src/core/hooks/types.ts +2 -2
- package/src/core/plugins/doctor.ts +9 -1
- package/src/core/sdk.ts +2 -1
- package/src/core/session-manager.ts +518 -40
- package/src/core/settings-manager.ts +174 -0
- package/src/core/system-prompt.ts +9 -14
- package/src/core/title-generator.ts +2 -8
- package/src/core/tools/ask.ts +19 -37
- package/src/core/tools/bash.ts +2 -37
- package/src/core/tools/edit.ts +2 -9
- package/src/core/tools/exa/render.ts +52 -48
- package/src/core/tools/find.ts +10 -8
- package/src/core/tools/grep.ts +45 -17
- package/src/core/tools/ls.ts +22 -2
- package/src/core/tools/lsp/clients/biome-client.ts +207 -0
- package/src/core/tools/lsp/clients/index.ts +49 -0
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
- package/src/core/tools/lsp/config.ts +3 -0
- package/src/core/tools/lsp/index.ts +107 -55
- package/src/core/tools/lsp/render.ts +192 -79
- package/src/core/tools/lsp/types.ts +27 -0
- package/src/core/tools/lsp/utils.ts +62 -22
- package/src/core/tools/notebook.ts +9 -1
- package/src/core/tools/output.ts +37 -14
- package/src/core/tools/read.ts +349 -34
- package/src/core/tools/renderers.ts +290 -89
- package/src/core/tools/review.ts +12 -5
- package/src/core/tools/task/agents.ts +5 -5
- package/src/core/tools/task/commands.ts +3 -3
- package/src/core/tools/task/executor.ts +33 -1
- package/src/core/tools/task/index.ts +93 -6
- package/src/core/tools/task/render.ts +147 -66
- package/src/core/tools/task/types.ts +14 -9
- package/src/core/tools/web-fetch.ts +242 -103
- package/src/core/tools/web-search/index.ts +64 -20
- package/src/core/tools/web-search/providers/exa.ts +68 -172
- package/src/core/tools/web-search/render.ts +264 -74
- package/src/core/tools/write.ts +2 -8
- package/src/main.ts +10 -6
- package/src/modes/cleanup.ts +23 -0
- package/src/modes/index.ts +9 -4
- package/src/modes/interactive/components/bash-execution.ts +6 -3
- package/src/modes/interactive/components/branch-summary-message.ts +1 -1
- package/src/modes/interactive/components/compaction-summary-message.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +4 -5
- package/src/modes/interactive/components/extensions/extension-list.ts +18 -16
- package/src/modes/interactive/components/extensions/inspector-panel.ts +8 -8
- package/src/modes/interactive/components/hook-message.ts +2 -2
- package/src/modes/interactive/components/hook-selector.ts +1 -1
- package/src/modes/interactive/components/model-selector.ts +22 -9
- package/src/modes/interactive/components/oauth-selector.ts +20 -4
- package/src/modes/interactive/components/plugin-settings.ts +4 -2
- package/src/modes/interactive/components/session-selector.ts +9 -6
- package/src/modes/interactive/components/settings-defs.ts +285 -1
- package/src/modes/interactive/components/settings-selector.ts +176 -3
- package/src/modes/interactive/components/status-line/index.ts +4 -0
- package/src/modes/interactive/components/status-line/presets.ts +94 -0
- package/src/modes/interactive/components/status-line/segments.ts +350 -0
- package/src/modes/interactive/components/status-line/separators.ts +55 -0
- package/src/modes/interactive/components/status-line/types.ts +81 -0
- package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
- package/src/modes/interactive/components/status-line.ts +170 -223
- package/src/modes/interactive/components/tool-execution.ts +446 -211
- package/src/modes/interactive/components/tree-selector.ts +17 -6
- package/src/modes/interactive/components/ttsr-notification.ts +4 -4
- package/src/modes/interactive/components/welcome.ts +27 -19
- package/src/modes/interactive/interactive-mode.ts +98 -13
- package/src/modes/interactive/theme/dark.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
- package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
- package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
- package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
- package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
- package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
- package/src/modes/interactive/theme/defaults/index.ts +67 -0
- package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
- package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
- package/src/modes/interactive/theme/defaults/light-github.json +114 -0
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
- package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
- package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
- package/src/modes/interactive/theme/defaults/light-one.json +105 -0
- package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
- package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
- package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
- package/src/modes/interactive/theme/light.json +3 -2
- package/src/modes/interactive/theme/theme-schema.json +120 -4
- package/src/modes/interactive/theme/theme.ts +1228 -14
- package/src/prompts/branch-summary-preamble.md +3 -0
- package/src/prompts/branch-summary.md +28 -0
- package/src/prompts/compaction-summary.md +34 -0
- package/src/prompts/compaction-turn-prefix.md +16 -0
- package/src/prompts/compaction-update-summary.md +41 -0
- package/src/prompts/init.md +30 -0
- package/src/{core/tools/task/bundled-agents → prompts}/reviewer.md +6 -0
- package/src/prompts/summarization-system.md +3 -0
- package/src/prompts/system-prompt.md +27 -0
- package/src/{core/tools/task/bundled-agents → prompts}/task.md +2 -0
- package/src/prompts/title-system.md +8 -0
- package/src/prompts/tools/ask.md +24 -0
- package/src/prompts/tools/bash.md +23 -0
- package/src/prompts/tools/edit.md +9 -0
- package/src/prompts/tools/find.md +6 -0
- package/src/prompts/tools/grep.md +12 -0
- package/src/prompts/tools/lsp.md +14 -0
- package/src/prompts/tools/output.md +23 -0
- package/src/prompts/tools/read.md +25 -0
- package/src/prompts/tools/web-fetch.md +8 -0
- package/src/prompts/tools/web-search.md +10 -0
- package/src/prompts/tools/write.md +10 -0
- package/src/commands/init.md +0 -20
- /package/src/{core/tools/task/bundled-commands → prompts}/architect-plan.md +0 -0
- /package/src/{core/tools/task/bundled-agents → prompts}/browser.md +0 -0
- /package/src/{core/tools/task/bundled-agents → prompts}/explore.md +0 -0
- /package/src/{core/tools/task/bundled-commands → prompts}/implement-with-critic.md +0 -0
- /package/src/{core/tools/task/bundled-commands → prompts}/implement.md +0 -0
- /package/src/{core/tools/task/bundled-agents → prompts}/plan.md +0 -0
|
@@ -4,62 +4,25 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
5
5
|
import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import type { AgentSession } from "../../../core/agent-session";
|
|
7
|
+
import type { StatusLineSegmentOptions, StatusLineSettings } from "../../../core/settings-manager";
|
|
7
8
|
import { theme } from "../theme/theme";
|
|
9
|
+
import { getPreset } from "./status-line/presets";
|
|
10
|
+
import { renderSegment, type SegmentContext } from "./status-line/segments";
|
|
11
|
+
import { getSeparator } from "./status-line/separators";
|
|
8
12
|
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
low: "🤔 low",
|
|
13
|
-
medium: "🤓 mid",
|
|
14
|
-
high: "🤯 high",
|
|
15
|
-
xhigh: "🧠 xhi",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
// Nerd Font icons
|
|
19
|
-
const ICONS = {
|
|
20
|
-
model: "\uec19", // robot/model
|
|
21
|
-
folder: "\uf115 ", // folder
|
|
22
|
-
branch: "\ue725", // git branch
|
|
23
|
-
sep: "\ue0b1", // powerline thin chevron
|
|
24
|
-
tokens: "\ue26b", // coins
|
|
25
|
-
context: "\ue70f", // window
|
|
26
|
-
auto: "\udb80\udc68", // auto
|
|
27
|
-
pi: "\ue22c", // pi
|
|
28
|
-
} as const;
|
|
29
|
-
|
|
30
|
-
/** Create a colored text segment with background */
|
|
31
|
-
function plSegment(content: string, fgAnsi: string, bgAnsi: string): string {
|
|
32
|
-
return `${bgAnsi}${fgAnsi} ${content} \x1b[0m`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Create separator with background */
|
|
36
|
-
function plSep(sepAnsi: string, bgAnsi: string): string {
|
|
37
|
-
return `${bgAnsi}${sepAnsi}${ICONS.sep}\x1b[0m`;
|
|
38
|
-
}
|
|
13
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
+
// Rendering Helpers
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
39
16
|
|
|
40
|
-
/**
|
|
41
|
-
function plEnd(bgAnsi: string): string {
|
|
42
|
-
// Use the bg color as fg for the arrow (creates the triangle effect)
|
|
43
|
-
const fgFromBg = bgAnsi.replace("\x1b[48;", "\x1b[38;");
|
|
44
|
-
return `${fgFromBg}\ue0b0\x1b[0m`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Sanitize text for display in a single-line status.
|
|
49
|
-
* Removes newlines, tabs, carriage returns, and other control characters.
|
|
50
|
-
*/
|
|
17
|
+
/** Sanitize text for display in a single-line status */
|
|
51
18
|
function sanitizeStatusText(text: string): string {
|
|
52
|
-
// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces
|
|
53
19
|
return text
|
|
54
20
|
.replace(/[\r\n\t]/g, " ")
|
|
55
21
|
.replace(/ +/g, " ")
|
|
56
22
|
.trim();
|
|
57
23
|
}
|
|
58
24
|
|
|
59
|
-
/**
|
|
60
|
-
* Find the git root directory by walking up from cwd.
|
|
61
|
-
* Returns the path to .git/HEAD if found, null otherwise.
|
|
62
|
-
*/
|
|
25
|
+
/** Find the git root directory by walking up from cwd */
|
|
63
26
|
function findGitHeadPath(): string | null {
|
|
64
27
|
let dir = process.cwd();
|
|
65
28
|
while (true) {
|
|
@@ -69,43 +32,53 @@ function findGitHeadPath(): string | null {
|
|
|
69
32
|
}
|
|
70
33
|
const parent = dirname(dir);
|
|
71
34
|
if (parent === dir) {
|
|
72
|
-
// Reached filesystem root
|
|
73
35
|
return null;
|
|
74
36
|
}
|
|
75
37
|
dir = parent;
|
|
76
38
|
}
|
|
77
39
|
}
|
|
78
40
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
42
|
+
// StatusLineComponent
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
|
|
82
45
|
export class StatusLineComponent implements Component {
|
|
83
46
|
private session: AgentSession;
|
|
84
|
-
private
|
|
47
|
+
private settings: StatusLineSettings = {};
|
|
48
|
+
private cachedBranch: string | null | undefined = undefined;
|
|
85
49
|
private gitWatcher: FSWatcher | null = null;
|
|
86
50
|
private onBranchChange: (() => void) | null = null;
|
|
87
51
|
private autoCompactEnabled: boolean = true;
|
|
88
52
|
private hookStatuses: Map<string, string> = new Map();
|
|
53
|
+
private subagentCount: number = 0;
|
|
54
|
+
private sessionStartTime: number = Date.now();
|
|
89
55
|
|
|
90
|
-
// Git status caching (1s TTL
|
|
56
|
+
// Git status caching (1s TTL)
|
|
91
57
|
private cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
|
|
92
58
|
private gitStatusLastFetch = 0;
|
|
93
59
|
|
|
94
60
|
constructor(session: AgentSession) {
|
|
95
61
|
this.session = session;
|
|
62
|
+
// Load initial settings
|
|
63
|
+
this.settings = session.settingsManager?.getStatusLineSettings() ?? {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
updateSettings(settings: StatusLineSettings): void {
|
|
67
|
+
this.settings = settings;
|
|
96
68
|
}
|
|
97
69
|
|
|
98
70
|
setAutoCompactEnabled(enabled: boolean): void {
|
|
99
71
|
this.autoCompactEnabled = enabled;
|
|
100
72
|
}
|
|
101
73
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
74
|
+
setSubagentCount(count: number): void {
|
|
75
|
+
this.subagentCount = count;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setSessionStartTime(time: number): void {
|
|
79
|
+
this.sessionStartTime = time;
|
|
80
|
+
}
|
|
81
|
+
|
|
109
82
|
setHookStatus(key: string, text: string | undefined): void {
|
|
110
83
|
if (text === undefined) {
|
|
111
84
|
this.hookStatuses.delete(key);
|
|
@@ -114,42 +87,32 @@ export class StatusLineComponent implements Component {
|
|
|
114
87
|
}
|
|
115
88
|
}
|
|
116
89
|
|
|
117
|
-
/**
|
|
118
|
-
* Set up a file watcher on .git/HEAD to detect branch changes.
|
|
119
|
-
* Call the provided callback when branch changes.
|
|
120
|
-
*/
|
|
121
90
|
watchBranch(onBranchChange: () => void): void {
|
|
122
91
|
this.onBranchChange = onBranchChange;
|
|
123
92
|
this.setupGitWatcher();
|
|
124
93
|
}
|
|
125
94
|
|
|
126
95
|
private setupGitWatcher(): void {
|
|
127
|
-
// Clean up existing watcher
|
|
128
96
|
if (this.gitWatcher) {
|
|
129
97
|
this.gitWatcher.close();
|
|
130
98
|
this.gitWatcher = null;
|
|
131
99
|
}
|
|
132
100
|
|
|
133
101
|
const gitHeadPath = findGitHeadPath();
|
|
134
|
-
if (!gitHeadPath)
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
102
|
+
if (!gitHeadPath) return;
|
|
137
103
|
|
|
138
104
|
try {
|
|
139
105
|
this.gitWatcher = watch(gitHeadPath, () => {
|
|
140
|
-
this.cachedBranch = undefined;
|
|
106
|
+
this.cachedBranch = undefined;
|
|
141
107
|
if (this.onBranchChange) {
|
|
142
108
|
this.onBranchChange();
|
|
143
109
|
}
|
|
144
110
|
});
|
|
145
111
|
} catch {
|
|
146
|
-
// Silently fail
|
|
112
|
+
// Silently fail
|
|
147
113
|
}
|
|
148
114
|
}
|
|
149
115
|
|
|
150
|
-
/**
|
|
151
|
-
* Clean up the file watcher
|
|
152
|
-
*/
|
|
153
116
|
dispose(): void {
|
|
154
117
|
if (this.gitWatcher) {
|
|
155
118
|
this.gitWatcher.close();
|
|
@@ -158,16 +121,10 @@ export class StatusLineComponent implements Component {
|
|
|
158
121
|
}
|
|
159
122
|
|
|
160
123
|
invalidate(): void {
|
|
161
|
-
// Invalidate cached branch so it gets re-read on next render
|
|
162
124
|
this.cachedBranch = undefined;
|
|
163
125
|
}
|
|
164
126
|
|
|
165
|
-
/**
|
|
166
|
-
* Get current git branch by reading .git/HEAD directly.
|
|
167
|
-
* Returns null if not in a git repo, branch name otherwise.
|
|
168
|
-
*/
|
|
169
127
|
private getCurrentBranch(): string | null {
|
|
170
|
-
// Return cached value if available
|
|
171
128
|
if (this.cachedBranch !== undefined) {
|
|
172
129
|
return this.cachedBranch;
|
|
173
130
|
}
|
|
@@ -181,25 +138,17 @@ export class StatusLineComponent implements Component {
|
|
|
181
138
|
const content = readFileSync(gitHeadPath, "utf8").trim();
|
|
182
139
|
|
|
183
140
|
if (content.startsWith("ref: refs/heads/")) {
|
|
184
|
-
// Normal branch: extract branch name
|
|
185
141
|
this.cachedBranch = content.slice(16);
|
|
186
142
|
} else {
|
|
187
|
-
// Detached HEAD state
|
|
188
143
|
this.cachedBranch = "detached";
|
|
189
144
|
}
|
|
190
145
|
} catch {
|
|
191
|
-
// Not in a git repo or error reading file
|
|
192
146
|
this.cachedBranch = null;
|
|
193
147
|
}
|
|
194
148
|
|
|
195
149
|
return this.cachedBranch;
|
|
196
150
|
}
|
|
197
151
|
|
|
198
|
-
/**
|
|
199
|
-
* Get git status indicators (staged, unstaged, untracked counts).
|
|
200
|
-
* Returns null if not in a git repo.
|
|
201
|
-
* Cached for 1s to avoid excessive subprocess spawns.
|
|
202
|
-
*/
|
|
203
152
|
private getGitStatus(): { staged: number; unstaged: number; untracked: number } | null {
|
|
204
153
|
const now = Date.now();
|
|
205
154
|
if (now - this.gitStatusLastFetch < 1000) {
|
|
@@ -219,21 +168,18 @@ export class StatusLineComponent implements Component {
|
|
|
219
168
|
|
|
220
169
|
for (const line of output.split("\n")) {
|
|
221
170
|
if (!line) continue;
|
|
222
|
-
const x = line[0];
|
|
223
|
-
const y = line[1];
|
|
171
|
+
const x = line[0];
|
|
172
|
+
const y = line[1];
|
|
224
173
|
|
|
225
|
-
// Untracked files
|
|
226
174
|
if (x === "?" && y === "?") {
|
|
227
175
|
untracked++;
|
|
228
176
|
continue;
|
|
229
177
|
}
|
|
230
178
|
|
|
231
|
-
// Staged changes (first column is not space or ?)
|
|
232
179
|
if (x && x !== " " && x !== "?") {
|
|
233
180
|
staged++;
|
|
234
181
|
}
|
|
235
182
|
|
|
236
|
-
// Unstaged changes (second column is not space)
|
|
237
183
|
if (y && y !== " ") {
|
|
238
184
|
unstaged++;
|
|
239
185
|
}
|
|
@@ -249,10 +195,19 @@ export class StatusLineComponent implements Component {
|
|
|
249
195
|
}
|
|
250
196
|
}
|
|
251
197
|
|
|
252
|
-
private
|
|
198
|
+
private buildSegmentContext(width: number): SegmentContext {
|
|
253
199
|
const state = this.session.state;
|
|
254
200
|
|
|
255
|
-
// Get
|
|
201
|
+
// Get usage statistics
|
|
202
|
+
const usageStats = this.session.sessionManager?.getUsageStatistics() ?? {
|
|
203
|
+
input: 0,
|
|
204
|
+
output: 0,
|
|
205
|
+
cacheRead: 0,
|
|
206
|
+
cacheWrite: 0,
|
|
207
|
+
cost: 0,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Get context percentage
|
|
256
211
|
const lastAssistantMessage = state.messages
|
|
257
212
|
.slice()
|
|
258
213
|
.reverse()
|
|
@@ -265,166 +220,158 @@ export class StatusLineComponent implements Component {
|
|
|
265
220
|
lastAssistantMessage.usage.cacheWrite
|
|
266
221
|
: 0;
|
|
267
222
|
const contextWindow = state.model?.contextWindow || 0;
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
// Format helpers
|
|
271
|
-
const formatTokens = (n: number): string => {
|
|
272
|
-
if (n < 1000) return n.toString();
|
|
273
|
-
if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
|
|
274
|
-
if (n < 1000000) return `${Math.round(n / 1000)}k`;
|
|
275
|
-
if (n < 10000000) return `${(n / 1000000).toFixed(1)}M`;
|
|
276
|
-
return `${Math.round(n / 1000000)}M`;
|
|
277
|
-
};
|
|
223
|
+
const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
278
224
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
}
|
|
225
|
+
return {
|
|
226
|
+
session: this.session,
|
|
227
|
+
width,
|
|
228
|
+
options: this.resolveSettings().segmentOptions ?? {},
|
|
229
|
+
usageStats,
|
|
230
|
+
contextPercent,
|
|
231
|
+
contextWindow,
|
|
232
|
+
autoCompactEnabled: this.autoCompactEnabled,
|
|
233
|
+
subagentCount: this.subagentCount,
|
|
234
|
+
sessionStartTime: this.sessionStartTime,
|
|
235
|
+
git: {
|
|
236
|
+
branch: this.getCurrentBranch(),
|
|
237
|
+
status: this.getGitStatus(),
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
294
241
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
if (pwd.startsWith("/work/")) {
|
|
304
|
-
pwd = pwd.slice(6);
|
|
305
|
-
}
|
|
306
|
-
const pathContent = `${ICONS.folder} ${pwd}`;
|
|
307
|
-
|
|
308
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
309
|
-
// SEGMENT 3: Git Branch + Status
|
|
310
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
311
|
-
const branch = this.getCurrentBranch();
|
|
312
|
-
let gitContent = "";
|
|
313
|
-
let gitColorName: "statusLineGitClean" | "statusLineGitDirty" = "statusLineGitClean";
|
|
314
|
-
if (branch) {
|
|
315
|
-
const gitStatus = this.getGitStatus();
|
|
316
|
-
const isDirty = gitStatus && (gitStatus.staged > 0 || gitStatus.unstaged > 0 || gitStatus.untracked > 0);
|
|
317
|
-
gitColorName = isDirty ? "statusLineGitDirty" : "statusLineGitClean";
|
|
318
|
-
|
|
319
|
-
gitContent = `${ICONS.branch} ${branch}`;
|
|
320
|
-
|
|
321
|
-
if (gitStatus) {
|
|
322
|
-
const indicators: string[] = [];
|
|
323
|
-
if (gitStatus.unstaged > 0) {
|
|
324
|
-
indicators.push(theme.fg("statusLineDirty", `*${gitStatus.unstaged}`));
|
|
325
|
-
}
|
|
326
|
-
if (gitStatus.staged > 0) {
|
|
327
|
-
indicators.push(theme.fg("statusLineStaged", `+${gitStatus.staged}`));
|
|
328
|
-
}
|
|
329
|
-
if (gitStatus.untracked > 0) {
|
|
330
|
-
indicators.push(theme.fg("statusLineUntracked", `?${gitStatus.untracked}`));
|
|
331
|
-
}
|
|
332
|
-
if (indicators.length > 0) {
|
|
333
|
-
gitContent += ` ${indicators.join(" ")}`;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
242
|
+
private resolveSettings(): Required<
|
|
243
|
+
Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
|
|
244
|
+
> &
|
|
245
|
+
StatusLineSettings {
|
|
246
|
+
const preset = this.settings.preset ?? "default";
|
|
247
|
+
const presetDef = getPreset(preset);
|
|
248
|
+
const mergedSegmentOptions: StatusLineSettings["segmentOptions"] = {};
|
|
337
249
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
341
|
-
const autoIndicator = this.autoCompactEnabled ? ` ${ICONS.auto}` : "";
|
|
342
|
-
const contextText = `${contextPercentValue.toFixed(1)}%/${formatTokens(contextWindow)}${autoIndicator}`;
|
|
343
|
-
let contextContent: string;
|
|
344
|
-
if (contextPercentValue > 90) {
|
|
345
|
-
contextContent = `${ICONS.context} ${theme.fg("error", contextText)}`;
|
|
346
|
-
} else if (contextPercentValue > 70) {
|
|
347
|
-
contextContent = `${ICONS.context} ${theme.fg("warning", contextText)}`;
|
|
348
|
-
} else {
|
|
349
|
-
contextContent = `${ICONS.context} ${contextText}`;
|
|
250
|
+
for (const [segment, options] of Object.entries(presetDef.segmentOptions ?? {})) {
|
|
251
|
+
mergedSegmentOptions[segment as keyof StatusLineSegmentOptions] = { ...(options as Record<string, unknown>) };
|
|
350
252
|
}
|
|
351
253
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const totalTokens = input + output + cacheRead + cacheWrite;
|
|
359
|
-
if (totalTokens) {
|
|
360
|
-
spendParts.push(`${ICONS.tokens} ${formatTokens(totalTokens)}`);
|
|
254
|
+
for (const [segment, options] of Object.entries(this.settings.segmentOptions ?? {})) {
|
|
255
|
+
const current = mergedSegmentOptions[segment as keyof StatusLineSegmentOptions] ?? {};
|
|
256
|
+
mergedSegmentOptions[segment as keyof StatusLineSegmentOptions] = {
|
|
257
|
+
...(current as Record<string, unknown>),
|
|
258
|
+
...(options as Record<string, unknown>),
|
|
259
|
+
};
|
|
361
260
|
}
|
|
362
261
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
262
|
+
return {
|
|
263
|
+
...this.settings,
|
|
264
|
+
leftSegments: this.settings.leftSegments ?? presetDef.leftSegments,
|
|
265
|
+
rightSegments: this.settings.rightSegments ?? presetDef.rightSegments,
|
|
266
|
+
separator: this.settings.separator ?? presetDef.separator,
|
|
267
|
+
segmentOptions: mergedSegmentOptions,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
368
270
|
|
|
369
|
-
|
|
271
|
+
private buildStatusLine(width: number): string {
|
|
272
|
+
const ctx = this.buildSegmentContext(width);
|
|
273
|
+
const effectiveSettings = this.resolveSettings();
|
|
274
|
+
const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
|
|
370
275
|
|
|
371
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
372
|
-
// Assemble: [Model] > [Path] > [Git?] > [Context] > [Spend] >
|
|
373
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
374
276
|
const bgAnsi = theme.getBgAnsi("statusLineBg");
|
|
277
|
+
const fgAnsi = theme.getFgAnsi("text");
|
|
375
278
|
const sepAnsi = theme.getFgAnsi("statusLineSep");
|
|
376
279
|
|
|
377
|
-
|
|
280
|
+
// Collect visible segment contents
|
|
281
|
+
const leftParts: string[] = [];
|
|
282
|
+
for (const segId of effectiveSettings.leftSegments) {
|
|
283
|
+
const rendered = renderSegment(segId, ctx);
|
|
284
|
+
if (rendered.visible && rendered.content) {
|
|
285
|
+
leftParts.push(rendered.content);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const rightParts: string[] = [];
|
|
290
|
+
for (const segId of effectiveSettings.rightSegments) {
|
|
291
|
+
const rendered = renderSegment(segId, ctx);
|
|
292
|
+
if (rendered.visible && rendered.content) {
|
|
293
|
+
rightParts.push(rendered.content);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const topFillWidth = width > 0 ? Math.max(0, width - 4) : 0;
|
|
298
|
+
const left = [...leftParts];
|
|
299
|
+
const right = [...rightParts];
|
|
378
300
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
301
|
+
const leftSepWidth = visibleWidth(separatorDef.left);
|
|
302
|
+
const rightSepWidth = visibleWidth(separatorDef.right);
|
|
303
|
+
const leftCapWidth = separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.right) : 0;
|
|
304
|
+
const rightCapWidth = separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.left) : 0;
|
|
382
305
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
306
|
+
const groupWidth = (parts: string[], capWidth: number, sepWidth: number): number => {
|
|
307
|
+
if (parts.length === 0) return 0;
|
|
308
|
+
const partsWidth = parts.reduce((sum, part) => sum + visibleWidth(part), 0);
|
|
309
|
+
const sepTotal = Math.max(0, parts.length - 1) * (sepWidth + 2);
|
|
310
|
+
return partsWidth + sepTotal + 2 + capWidth;
|
|
311
|
+
};
|
|
386
312
|
|
|
387
|
-
|
|
388
|
-
|
|
313
|
+
let leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
314
|
+
let rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
|
|
315
|
+
const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
|
|
389
316
|
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
317
|
+
if (topFillWidth > 0) {
|
|
318
|
+
while (totalWidth() > topFillWidth && right.length > 0) {
|
|
319
|
+
right.pop();
|
|
320
|
+
rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
|
|
321
|
+
}
|
|
322
|
+
while (totalWidth() > topFillWidth && left.length > 0) {
|
|
323
|
+
left.pop();
|
|
324
|
+
leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
325
|
+
}
|
|
393
326
|
}
|
|
394
327
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
328
|
+
const renderGroup = (parts: string[], direction: "left" | "right"): string => {
|
|
329
|
+
if (parts.length === 0) return "";
|
|
330
|
+
const sep = direction === "left" ? separatorDef.left : separatorDef.right;
|
|
331
|
+
const cap = separatorDef.endCaps
|
|
332
|
+
? direction === "left"
|
|
333
|
+
? separatorDef.endCaps.right
|
|
334
|
+
: separatorDef.endCaps.left
|
|
335
|
+
: "";
|
|
336
|
+
const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : sepAnsi;
|
|
337
|
+
const capText = cap ? `${capPrefix}${cap}\x1b[0m` : "";
|
|
338
|
+
|
|
339
|
+
let content = bgAnsi + fgAnsi;
|
|
340
|
+
content += ` ${parts.join(` ${sepAnsi}${sep}${fgAnsi} `)} `;
|
|
341
|
+
content += "\x1b[0m";
|
|
342
|
+
|
|
343
|
+
if (capText) {
|
|
344
|
+
return direction === "right" ? capText + content : content + capText;
|
|
345
|
+
}
|
|
346
|
+
return content;
|
|
347
|
+
};
|
|
398
348
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
349
|
+
const leftGroup = renderGroup(left, "left");
|
|
350
|
+
const rightGroup = renderGroup(right, "right");
|
|
351
|
+
if (!leftGroup && !rightGroup) return "";
|
|
402
352
|
|
|
403
|
-
|
|
404
|
-
|
|
353
|
+
if (topFillWidth === 0 || left.length === 0 || right.length === 0) {
|
|
354
|
+
return leftGroup + (leftGroup && rightGroup ? " " : "") + rightGroup;
|
|
355
|
+
}
|
|
405
356
|
|
|
406
|
-
|
|
357
|
+
leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
358
|
+
rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
|
|
359
|
+
const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
|
|
360
|
+
return leftGroup + " ".repeat(gapWidth) + rightGroup;
|
|
407
361
|
}
|
|
408
362
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
* Returns the content string and its visible width.
|
|
412
|
-
*/
|
|
413
|
-
getTopBorder(_width: number): { content: string; width: number } {
|
|
414
|
-
const content = this.buildStatusLine();
|
|
363
|
+
getTopBorder(width: number): { content: string; width: number } {
|
|
364
|
+
const content = this.buildStatusLine(width);
|
|
415
365
|
return {
|
|
416
366
|
content,
|
|
417
367
|
width: visibleWidth(content),
|
|
418
368
|
};
|
|
419
369
|
}
|
|
420
370
|
|
|
421
|
-
/**
|
|
422
|
-
* Render only hook statuses (if any).
|
|
423
|
-
* Used when footer is integrated into editor border.
|
|
424
|
-
*/
|
|
425
371
|
render(width: number): string[] {
|
|
426
372
|
// Only render hook statuses - main status is in editor's top border
|
|
427
|
-
|
|
373
|
+
const showHooks = this.settings.showHookStatus ?? true;
|
|
374
|
+
if (!showHooks || this.hookStatuses.size === 0) {
|
|
428
375
|
return [];
|
|
429
376
|
}
|
|
430
377
|
|
|
@@ -432,6 +379,6 @@ export class StatusLineComponent implements Component {
|
|
|
432
379
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
433
380
|
.map(([, text]) => sanitizeStatusText(text));
|
|
434
381
|
const hookLine = sortedStatuses.join(" ");
|
|
435
|
-
return [truncateToWidth(hookLine, width, theme.fg("statusLineSep",
|
|
382
|
+
return [truncateToWidth(hookLine, width, theme.fg("statusLineSep", theme.format.ellipsis))];
|
|
436
383
|
}
|
|
437
384
|
}
|