@oh-my-pi/pi-coding-agent 3.13.1337 → 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 +88 -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/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 +552 -41
- 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 +169 -233
- 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,61 +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: "\uf4bc", // 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
|
-
} as const;
|
|
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
|
-
}
|
|
13
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
+
// Rendering Helpers
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
16
|
|
|
39
|
-
/**
|
|
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
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Sanitize text for display in a single-line status.
|
|
48
|
-
* Removes newlines, tabs, carriage returns, and other control characters.
|
|
49
|
-
*/
|
|
17
|
+
/** Sanitize text for display in a single-line status */
|
|
50
18
|
function sanitizeStatusText(text: string): string {
|
|
51
|
-
// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces
|
|
52
19
|
return text
|
|
53
20
|
.replace(/[\r\n\t]/g, " ")
|
|
54
21
|
.replace(/ +/g, " ")
|
|
55
22
|
.trim();
|
|
56
23
|
}
|
|
57
24
|
|
|
58
|
-
/**
|
|
59
|
-
* Find the git root directory by walking up from cwd.
|
|
60
|
-
* Returns the path to .git/HEAD if found, null otherwise.
|
|
61
|
-
*/
|
|
25
|
+
/** Find the git root directory by walking up from cwd */
|
|
62
26
|
function findGitHeadPath(): string | null {
|
|
63
27
|
let dir = process.cwd();
|
|
64
28
|
while (true) {
|
|
@@ -68,43 +32,53 @@ function findGitHeadPath(): string | null {
|
|
|
68
32
|
}
|
|
69
33
|
const parent = dirname(dir);
|
|
70
34
|
if (parent === dir) {
|
|
71
|
-
// Reached filesystem root
|
|
72
35
|
return null;
|
|
73
36
|
}
|
|
74
37
|
dir = parent;
|
|
75
38
|
}
|
|
76
39
|
}
|
|
77
40
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
42
|
+
// StatusLineComponent
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
|
|
81
45
|
export class StatusLineComponent implements Component {
|
|
82
46
|
private session: AgentSession;
|
|
83
|
-
private
|
|
47
|
+
private settings: StatusLineSettings = {};
|
|
48
|
+
private cachedBranch: string | null | undefined = undefined;
|
|
84
49
|
private gitWatcher: FSWatcher | null = null;
|
|
85
50
|
private onBranchChange: (() => void) | null = null;
|
|
86
51
|
private autoCompactEnabled: boolean = true;
|
|
87
52
|
private hookStatuses: Map<string, string> = new Map();
|
|
53
|
+
private subagentCount: number = 0;
|
|
54
|
+
private sessionStartTime: number = Date.now();
|
|
88
55
|
|
|
89
|
-
// Git status caching (1s TTL
|
|
56
|
+
// Git status caching (1s TTL)
|
|
90
57
|
private cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
|
|
91
58
|
private gitStatusLastFetch = 0;
|
|
92
59
|
|
|
93
60
|
constructor(session: AgentSession) {
|
|
94
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;
|
|
95
68
|
}
|
|
96
69
|
|
|
97
70
|
setAutoCompactEnabled(enabled: boolean): void {
|
|
98
71
|
this.autoCompactEnabled = enabled;
|
|
99
72
|
}
|
|
100
73
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
74
|
+
setSubagentCount(count: number): void {
|
|
75
|
+
this.subagentCount = count;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setSessionStartTime(time: number): void {
|
|
79
|
+
this.sessionStartTime = time;
|
|
80
|
+
}
|
|
81
|
+
|
|
108
82
|
setHookStatus(key: string, text: string | undefined): void {
|
|
109
83
|
if (text === undefined) {
|
|
110
84
|
this.hookStatuses.delete(key);
|
|
@@ -113,42 +87,32 @@ export class StatusLineComponent implements Component {
|
|
|
113
87
|
}
|
|
114
88
|
}
|
|
115
89
|
|
|
116
|
-
/**
|
|
117
|
-
* Set up a file watcher on .git/HEAD to detect branch changes.
|
|
118
|
-
* Call the provided callback when branch changes.
|
|
119
|
-
*/
|
|
120
90
|
watchBranch(onBranchChange: () => void): void {
|
|
121
91
|
this.onBranchChange = onBranchChange;
|
|
122
92
|
this.setupGitWatcher();
|
|
123
93
|
}
|
|
124
94
|
|
|
125
95
|
private setupGitWatcher(): void {
|
|
126
|
-
// Clean up existing watcher
|
|
127
96
|
if (this.gitWatcher) {
|
|
128
97
|
this.gitWatcher.close();
|
|
129
98
|
this.gitWatcher = null;
|
|
130
99
|
}
|
|
131
100
|
|
|
132
101
|
const gitHeadPath = findGitHeadPath();
|
|
133
|
-
if (!gitHeadPath)
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
102
|
+
if (!gitHeadPath) return;
|
|
136
103
|
|
|
137
104
|
try {
|
|
138
105
|
this.gitWatcher = watch(gitHeadPath, () => {
|
|
139
|
-
this.cachedBranch = undefined;
|
|
106
|
+
this.cachedBranch = undefined;
|
|
140
107
|
if (this.onBranchChange) {
|
|
141
108
|
this.onBranchChange();
|
|
142
109
|
}
|
|
143
110
|
});
|
|
144
111
|
} catch {
|
|
145
|
-
// Silently fail
|
|
112
|
+
// Silently fail
|
|
146
113
|
}
|
|
147
114
|
}
|
|
148
115
|
|
|
149
|
-
/**
|
|
150
|
-
* Clean up the file watcher
|
|
151
|
-
*/
|
|
152
116
|
dispose(): void {
|
|
153
117
|
if (this.gitWatcher) {
|
|
154
118
|
this.gitWatcher.close();
|
|
@@ -157,16 +121,10 @@ export class StatusLineComponent implements Component {
|
|
|
157
121
|
}
|
|
158
122
|
|
|
159
123
|
invalidate(): void {
|
|
160
|
-
// Invalidate cached branch so it gets re-read on next render
|
|
161
124
|
this.cachedBranch = undefined;
|
|
162
125
|
}
|
|
163
126
|
|
|
164
|
-
/**
|
|
165
|
-
* Get current git branch by reading .git/HEAD directly.
|
|
166
|
-
* Returns null if not in a git repo, branch name otherwise.
|
|
167
|
-
*/
|
|
168
127
|
private getCurrentBranch(): string | null {
|
|
169
|
-
// Return cached value if available
|
|
170
128
|
if (this.cachedBranch !== undefined) {
|
|
171
129
|
return this.cachedBranch;
|
|
172
130
|
}
|
|
@@ -180,25 +138,17 @@ export class StatusLineComponent implements Component {
|
|
|
180
138
|
const content = readFileSync(gitHeadPath, "utf8").trim();
|
|
181
139
|
|
|
182
140
|
if (content.startsWith("ref: refs/heads/")) {
|
|
183
|
-
// Normal branch: extract branch name
|
|
184
141
|
this.cachedBranch = content.slice(16);
|
|
185
142
|
} else {
|
|
186
|
-
// Detached HEAD state
|
|
187
143
|
this.cachedBranch = "detached";
|
|
188
144
|
}
|
|
189
145
|
} catch {
|
|
190
|
-
// Not in a git repo or error reading file
|
|
191
146
|
this.cachedBranch = null;
|
|
192
147
|
}
|
|
193
148
|
|
|
194
149
|
return this.cachedBranch;
|
|
195
150
|
}
|
|
196
151
|
|
|
197
|
-
/**
|
|
198
|
-
* Get git status indicators (staged, unstaged, untracked counts).
|
|
199
|
-
* Returns null if not in a git repo.
|
|
200
|
-
* Cached for 1s to avoid excessive subprocess spawns.
|
|
201
|
-
*/
|
|
202
152
|
private getGitStatus(): { staged: number; unstaged: number; untracked: number } | null {
|
|
203
153
|
const now = Date.now();
|
|
204
154
|
if (now - this.gitStatusLastFetch < 1000) {
|
|
@@ -218,21 +168,18 @@ export class StatusLineComponent implements Component {
|
|
|
218
168
|
|
|
219
169
|
for (const line of output.split("\n")) {
|
|
220
170
|
if (!line) continue;
|
|
221
|
-
const x = line[0];
|
|
222
|
-
const y = line[1];
|
|
171
|
+
const x = line[0];
|
|
172
|
+
const y = line[1];
|
|
223
173
|
|
|
224
|
-
// Untracked files
|
|
225
174
|
if (x === "?" && y === "?") {
|
|
226
175
|
untracked++;
|
|
227
176
|
continue;
|
|
228
177
|
}
|
|
229
178
|
|
|
230
|
-
// Staged changes (first column is not space or ?)
|
|
231
179
|
if (x && x !== " " && x !== "?") {
|
|
232
180
|
staged++;
|
|
233
181
|
}
|
|
234
182
|
|
|
235
|
-
// Unstaged changes (second column is not space)
|
|
236
183
|
if (y && y !== " ") {
|
|
237
184
|
unstaged++;
|
|
238
185
|
}
|
|
@@ -248,27 +195,19 @@ export class StatusLineComponent implements Component {
|
|
|
248
195
|
}
|
|
249
196
|
}
|
|
250
197
|
|
|
251
|
-
private
|
|
198
|
+
private buildSegmentContext(width: number): SegmentContext {
|
|
252
199
|
const state = this.session.state;
|
|
253
200
|
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
263
|
-
totalInput += entry.message.usage.input;
|
|
264
|
-
totalOutput += entry.message.usage.output;
|
|
265
|
-
totalCacheRead += entry.message.usage.cacheRead;
|
|
266
|
-
totalCacheWrite += entry.message.usage.cacheWrite;
|
|
267
|
-
totalCost += entry.message.usage.cost.total;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
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
|
+
};
|
|
270
209
|
|
|
271
|
-
// Get context percentage
|
|
210
|
+
// Get context percentage
|
|
272
211
|
const lastAssistantMessage = state.messages
|
|
273
212
|
.slice()
|
|
274
213
|
.reverse()
|
|
@@ -281,161 +220,158 @@ export class StatusLineComponent implements Component {
|
|
|
281
220
|
lastAssistantMessage.usage.cacheWrite
|
|
282
221
|
: 0;
|
|
283
222
|
const contextWindow = state.model?.contextWindow || 0;
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
223
|
+
const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
224
|
+
|
|
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
|
+
},
|
|
293
239
|
};
|
|
240
|
+
}
|
|
294
241
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
let modelContent = `${ICONS.model} ${modelName}`;
|
|
304
|
-
if (state.model?.reasoning) {
|
|
305
|
-
const level = state.thinkingLevel || "off";
|
|
306
|
-
if (level !== "off") {
|
|
307
|
-
modelContent += ` · ${THINKING_ICONS[level] ?? level}`;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
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"] = {};
|
|
310
249
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
314
|
-
let pwd = process.cwd();
|
|
315
|
-
const home = process.env.HOME || process.env.USERPROFILE;
|
|
316
|
-
if (home && pwd.startsWith(home)) {
|
|
317
|
-
pwd = `~${pwd.slice(home.length)}`;
|
|
318
|
-
}
|
|
319
|
-
if (pwd.startsWith("/work/")) {
|
|
320
|
-
pwd = pwd.slice(6);
|
|
321
|
-
}
|
|
322
|
-
const pathContent = `${ICONS.folder} ${pwd}`;
|
|
323
|
-
|
|
324
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
325
|
-
// SEGMENT 3: Git Branch + Status
|
|
326
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
327
|
-
const branch = this.getCurrentBranch();
|
|
328
|
-
let gitContent = "";
|
|
329
|
-
let gitColorName: "statusLineGitClean" | "statusLineGitDirty" = "statusLineGitClean";
|
|
330
|
-
if (branch) {
|
|
331
|
-
const gitStatus = this.getGitStatus();
|
|
332
|
-
const isDirty = gitStatus && (gitStatus.staged > 0 || gitStatus.unstaged > 0 || gitStatus.untracked > 0);
|
|
333
|
-
gitColorName = isDirty ? "statusLineGitDirty" : "statusLineGitClean";
|
|
334
|
-
|
|
335
|
-
gitContent = `${ICONS.branch} ${branch}`;
|
|
336
|
-
|
|
337
|
-
if (gitStatus) {
|
|
338
|
-
const indicators: string[] = [];
|
|
339
|
-
if (gitStatus.unstaged > 0) {
|
|
340
|
-
indicators.push(theme.fg("statusLineDirty", `*${gitStatus.unstaged}`));
|
|
341
|
-
}
|
|
342
|
-
if (gitStatus.staged > 0) {
|
|
343
|
-
indicators.push(theme.fg("statusLineStaged", `+${gitStatus.staged}`));
|
|
344
|
-
}
|
|
345
|
-
if (gitStatus.untracked > 0) {
|
|
346
|
-
indicators.push(theme.fg("statusLineUntracked", `?${gitStatus.untracked}`));
|
|
347
|
-
}
|
|
348
|
-
if (indicators.length > 0) {
|
|
349
|
-
gitContent += ` ${indicators.join(" ")}`;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
250
|
+
for (const [segment, options] of Object.entries(presetDef.segmentOptions ?? {})) {
|
|
251
|
+
mergedSegmentOptions[segment as keyof StatusLineSegmentOptions] = { ...(options as Record<string, unknown>) };
|
|
352
252
|
}
|
|
353
253
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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}`;
|
|
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
|
+
};
|
|
366
260
|
}
|
|
367
261
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
+
}
|
|
372
270
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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);
|
|
275
|
+
|
|
276
|
+
const bgAnsi = theme.getBgAnsi("statusLineBg");
|
|
277
|
+
const fgAnsi = theme.getFgAnsi("text");
|
|
278
|
+
const sepAnsi = theme.getFgAnsi("statusLineSep");
|
|
377
279
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
+
}
|
|
382
287
|
}
|
|
383
288
|
|
|
384
|
-
const
|
|
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
|
+
}
|
|
385
296
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const bgAnsi = theme.getBgAnsi("statusLineBg");
|
|
390
|
-
const sepAnsi = theme.getFgAnsi("statusLineSep");
|
|
297
|
+
const topFillWidth = width > 0 ? Math.max(0, width - 4) : 0;
|
|
298
|
+
const left = [...leftParts];
|
|
299
|
+
const right = [...rightParts];
|
|
391
300
|
|
|
392
|
-
|
|
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;
|
|
393
305
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
+
};
|
|
397
312
|
|
|
398
|
-
|
|
399
|
-
|
|
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);
|
|
400
316
|
|
|
401
|
-
if (
|
|
402
|
-
|
|
403
|
-
|
|
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
|
+
}
|
|
404
326
|
}
|
|
405
327
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
+
};
|
|
409
348
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
349
|
+
const leftGroup = renderGroup(left, "left");
|
|
350
|
+
const rightGroup = renderGroup(right, "right");
|
|
351
|
+
if (!leftGroup && !rightGroup) return "";
|
|
413
352
|
|
|
414
|
-
|
|
415
|
-
|
|
353
|
+
if (topFillWidth === 0 || left.length === 0 || right.length === 0) {
|
|
354
|
+
return leftGroup + (leftGroup && rightGroup ? " " : "") + rightGroup;
|
|
355
|
+
}
|
|
416
356
|
|
|
417
|
-
|
|
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;
|
|
418
361
|
}
|
|
419
362
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
* Returns the content string and its visible width.
|
|
423
|
-
*/
|
|
424
|
-
getTopBorder(_width: number): { content: string; width: number } {
|
|
425
|
-
const content = this.buildStatusLine();
|
|
363
|
+
getTopBorder(width: number): { content: string; width: number } {
|
|
364
|
+
const content = this.buildStatusLine(width);
|
|
426
365
|
return {
|
|
427
366
|
content,
|
|
428
367
|
width: visibleWidth(content),
|
|
429
368
|
};
|
|
430
369
|
}
|
|
431
370
|
|
|
432
|
-
/**
|
|
433
|
-
* Render only hook statuses (if any).
|
|
434
|
-
* Used when footer is integrated into editor border.
|
|
435
|
-
*/
|
|
436
371
|
render(width: number): string[] {
|
|
437
372
|
// Only render hook statuses - main status is in editor's top border
|
|
438
|
-
|
|
373
|
+
const showHooks = this.settings.showHookStatus ?? true;
|
|
374
|
+
if (!showHooks || this.hookStatuses.size === 0) {
|
|
439
375
|
return [];
|
|
440
376
|
}
|
|
441
377
|
|
|
@@ -443,6 +379,6 @@ export class StatusLineComponent implements Component {
|
|
|
443
379
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
444
380
|
.map(([, text]) => sanitizeStatusText(text));
|
|
445
381
|
const hookLine = sortedStatuses.join(" ");
|
|
446
|
-
return [truncateToWidth(hookLine, width, theme.fg("statusLineSep",
|
|
382
|
+
return [truncateToWidth(hookLine, width, theme.fg("statusLineSep", theme.format.ellipsis))];
|
|
447
383
|
}
|
|
448
384
|
}
|