@selesai/code 0.7.0 → 0.8.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 +11 -0
- package/README.md +4 -2
- package/dist/extensions/agent-browser.test.ts +195 -0
- package/dist/extensions/agent-browser.ts +158 -0
- package/dist/extensions/inline-skills.test.ts +14 -0
- package/dist/extensions/inline-skills.ts +14 -7
- package/dist/extensions/package.json +2 -1
- package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +27 -0
- package/dist/extensions/pi-powerline-footer/CHANGELOG.md +153 -0
- package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +16 -0
- package/dist/extensions/pi-powerline-footer/README.md +192 -44
- package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +19 -4
- package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +80 -25
- package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +81 -29
- package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +1 -1
- package/dist/extensions/pi-powerline-footer/cd-command.ts +190 -0
- package/dist/extensions/pi-powerline-footer/colors.ts +2 -1
- package/dist/extensions/pi-powerline-footer/context-usage.ts +121 -6
- package/dist/extensions/pi-powerline-footer/currency-rates.ts +142 -0
- package/dist/extensions/pi-powerline-footer/editor-composition.ts +32 -0
- package/dist/extensions/pi-powerline-footer/git-status.ts +108 -6
- package/dist/extensions/pi-powerline-footer/icons.ts +11 -2
- package/dist/extensions/pi-powerline-footer/index.ts +1056 -324
- package/dist/extensions/pi-powerline-footer/lifecycle.ts +10 -0
- package/dist/extensions/pi-powerline-footer/package.json +10 -6
- package/dist/extensions/pi-powerline-footer/paths.ts +22 -0
- package/dist/extensions/pi-powerline-footer/powerline-config.ts +297 -15
- package/dist/extensions/pi-powerline-footer/presets.ts +6 -25
- package/dist/extensions/pi-powerline-footer/queue/store.ts +361 -0
- package/dist/extensions/pi-powerline-footer/queue/types.ts +54 -0
- package/dist/extensions/pi-powerline-footer/render-scheduler.ts +12 -7
- package/dist/extensions/pi-powerline-footer/segments.ts +114 -107
- package/dist/extensions/pi-powerline-footer/session-usage.ts +1 -1
- package/dist/extensions/pi-powerline-footer/shortcuts.ts +17 -3
- package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +189 -17
- package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +144 -0
- package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +120 -2
- package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +245 -22
- package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +48 -0
- package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +34 -0
- package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +39 -0
- package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +92 -0
- package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +4 -1
- package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +33 -197
- package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +79 -0
- package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +63 -0
- package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +195 -0
- package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +14 -0
- package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +167 -0
- package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +23 -18
- package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +16 -1
- package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +249 -0
- package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +237 -0
- package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +199 -0
- package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +191 -100
- package/dist/extensions/pi-powerline-footer/theme.ts +32 -18
- package/dist/extensions/pi-powerline-footer/token-stats.ts +304 -0
- package/dist/extensions/pi-powerline-footer/tsconfig.json +13 -0
- package/dist/extensions/pi-powerline-footer/types.ts +74 -29
- package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +34 -0
- package/dist/extensions/pi-powerline-footer/welcome.ts +65 -10
- package/dist/extensions/pi-powerline-footer/working-vibes.ts +44 -11
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/src/slash/inline-subagents.ts +57 -24
- package/dist/extensions/pi-subagents/test/unit/inline-subagents.test.ts +82 -7
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +1 -1
- package/dist/extensions/ponytail/index.js +1 -31
- package/dist/extensions/ponytail/test/extension.test.js +5 -22
- package/dist/extensions/rtk.test.ts +27 -12
- package/dist/extensions/rtk.ts +41 -35
- package/package.json +1 -1
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
type SessionAssistantUsage = AssistantMessage["usage"];
|
|
4
|
+
|
|
5
|
+
export interface SessionTokenStats {
|
|
6
|
+
input: number;
|
|
7
|
+
output: number;
|
|
8
|
+
cacheRead: number;
|
|
9
|
+
cacheWrite: number;
|
|
10
|
+
cost: number;
|
|
11
|
+
subagentCost: number;
|
|
12
|
+
lastAssistant: AssistantMessage | undefined;
|
|
13
|
+
thinkingLevelFromSession: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hasSessionAssistantUsage(value: unknown): value is SessionAssistantUsage {
|
|
21
|
+
if (!isRecord(value)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
typeof value.input !== "number" ||
|
|
27
|
+
typeof value.output !== "number" ||
|
|
28
|
+
typeof value.cacheRead !== "number" ||
|
|
29
|
+
typeof value.cacheWrite !== "number"
|
|
30
|
+
) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return isRecord(value.cost) && typeof value.cost.total === "number";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isSessionAssistantMessage(value: unknown): value is AssistantMessage {
|
|
38
|
+
return isRecord(value)
|
|
39
|
+
&& value.role === "assistant"
|
|
40
|
+
&& hasSessionAssistantUsage(value.usage)
|
|
41
|
+
&& (value.stopReason === undefined || typeof value.stopReason === "string");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getUsageTokenTotal(usage: SessionAssistantUsage): number {
|
|
45
|
+
const totalTokens = "totalTokens" in usage && typeof usage.totalTokens === "number" ? usage.totalTokens : 0;
|
|
46
|
+
return totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Matches pi-subagents' internal SLASH_RESULT_TYPE custom_message marker.
|
|
50
|
+
const SUBAGENT_SLASH_RESULT_TYPE = "subagent-slash-result";
|
|
51
|
+
|
|
52
|
+
function subagentDetailsFromSessionEntry(e: Record<string, unknown>): { results: unknown[] } | undefined {
|
|
53
|
+
if (e.type === "custom_message" && e.customType === SUBAGENT_SLASH_RESULT_TYPE) {
|
|
54
|
+
const details = isRecord(e.details) ? e.details : undefined;
|
|
55
|
+
const result = isRecord(details?.result) ? details.result : undefined;
|
|
56
|
+
const inner = isRecord(result?.details) ? result.details : undefined;
|
|
57
|
+
return Array.isArray(inner?.results) ? { results: inner.results } : undefined;
|
|
58
|
+
}
|
|
59
|
+
if (e.type === "message" && isRecord(e.message)) {
|
|
60
|
+
const message = e.message as { role?: unknown; toolName?: unknown; details?: unknown };
|
|
61
|
+
if (message.role === "toolResult" && message.toolName === "subagent" && isRecord(message.details) && Array.isArray(message.details.results)) {
|
|
62
|
+
return { results: message.details.results };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Sums subagent child usage cost recorded on a single session entry (parallel/chain/single runs
|
|
69
|
+
// launched via the subagent tool or /parallel, /worker etc. slash commands), so the footer can
|
|
70
|
+
// show total spend rather than only the interactive parent session's cost.
|
|
71
|
+
function extractSubagentResultCost(e: Record<string, unknown>): number {
|
|
72
|
+
const details = subagentDetailsFromSessionEntry(e);
|
|
73
|
+
if (!details) return 0;
|
|
74
|
+
let total = 0;
|
|
75
|
+
for (const result of details.results) {
|
|
76
|
+
if (!isRecord(result)) continue;
|
|
77
|
+
const usage = isRecord(result.usage) ? result.usage : undefined;
|
|
78
|
+
if (typeof usage?.cost === "number") total += usage.cost;
|
|
79
|
+
}
|
|
80
|
+
return total;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Fingerprint of the fields on a session event that influence the aggregated
|
|
85
|
+
* stats. Comparing only the event count is not enough: while streaming, pi
|
|
86
|
+
* may update the trailing assistant message in place (usage grows, stopReason
|
|
87
|
+
* flips), so the same event reference can carry fresh numbers.
|
|
88
|
+
*/
|
|
89
|
+
function eventStatsSignature(event: unknown): string {
|
|
90
|
+
if (!isRecord(event)) {
|
|
91
|
+
return "?";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (event.type === "thinking_level_change") {
|
|
95
|
+
return `t:${typeof event.thinkingLevel === "string" ? event.thinkingLevel : ""}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const subagentDetails = subagentDetailsFromSessionEntry(event);
|
|
99
|
+
if (subagentDetails) {
|
|
100
|
+
return `s:${subagentDetails.results.length}:${extractSubagentResultCost(event)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (event.type === "message" && isRecord(event.message)) {
|
|
104
|
+
const message = event.message;
|
|
105
|
+
const role = typeof message.role === "string" ? message.role : "";
|
|
106
|
+
const stopReason = typeof message.stopReason === "string" ? message.stopReason : "";
|
|
107
|
+
if (role === "assistant" && hasSessionAssistantUsage(message.usage)) {
|
|
108
|
+
const usage = message.usage;
|
|
109
|
+
return `a:${stopReason}:${usage.input}/${usage.output}/${usage.cacheRead}/${usage.cacheWrite}/${usage.cost.total}`;
|
|
110
|
+
}
|
|
111
|
+
return `m:${role}:${stopReason}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return `e:${typeof event.type === "string" ? event.type : "?"}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function emptySessionTokenStats(): SessionTokenStats {
|
|
118
|
+
return {
|
|
119
|
+
input: 0,
|
|
120
|
+
output: 0,
|
|
121
|
+
cacheRead: 0,
|
|
122
|
+
cacheWrite: 0,
|
|
123
|
+
cost: 0,
|
|
124
|
+
subagentCost: 0,
|
|
125
|
+
lastAssistant: undefined,
|
|
126
|
+
thinkingLevelFromSession: null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function copySessionTokenStats(stats: SessionTokenStats): SessionTokenStats {
|
|
131
|
+
return { ...stats };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function accumulateSessionEvent(stats: SessionTokenStats, event: unknown): void {
|
|
135
|
+
if (!isRecord(event)) return;
|
|
136
|
+
|
|
137
|
+
if (event.type === "thinking_level_change" && typeof event.thinkingLevel === "string") {
|
|
138
|
+
stats.thinkingLevelFromSession = event.thinkingLevel;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
stats.subagentCost += extractSubagentResultCost(event);
|
|
142
|
+
|
|
143
|
+
if (event.type !== "message" || !isSessionAssistantMessage(event.message)) return;
|
|
144
|
+
|
|
145
|
+
const message = event.message;
|
|
146
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") return;
|
|
147
|
+
|
|
148
|
+
stats.input += message.usage.input;
|
|
149
|
+
stats.output += message.usage.output;
|
|
150
|
+
stats.cacheRead += message.usage.cacheRead;
|
|
151
|
+
stats.cacheWrite += message.usage.cacheWrite;
|
|
152
|
+
stats.cost += message.usage.cost.total;
|
|
153
|
+
if (getUsageTokenTotal(message.usage) > 0) {
|
|
154
|
+
stats.lastAssistant = message;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface SessionBranchProvider {
|
|
159
|
+
getLeafId(): string | null;
|
|
160
|
+
getBranch(): readonly unknown[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isSessionBranchProvider(value: unknown): value is SessionBranchProvider {
|
|
164
|
+
return isRecord(value) && typeof value.getLeafId === "function" && typeof value.getBranch === "function";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Session branches are immutable for a given leaf. Keep the already-built path
|
|
169
|
+
* while streaming updates mutate only its trailing message in place.
|
|
170
|
+
*/
|
|
171
|
+
export class SessionBranchCache {
|
|
172
|
+
private provider: SessionBranchProvider | null = null;
|
|
173
|
+
private leafId: string | null = null;
|
|
174
|
+
private branch: readonly unknown[] = [];
|
|
175
|
+
|
|
176
|
+
get(source: unknown): readonly unknown[] {
|
|
177
|
+
if (!isSessionBranchProvider(source)) return [];
|
|
178
|
+
|
|
179
|
+
const provider = source;
|
|
180
|
+
const leafId = provider.getLeafId();
|
|
181
|
+
// A reset cache holds provider === null, which never matches a live provider.
|
|
182
|
+
if (this.provider !== provider || this.leafId !== leafId) {
|
|
183
|
+
this.provider = provider;
|
|
184
|
+
this.leafId = leafId;
|
|
185
|
+
this.branch = provider.getBranch();
|
|
186
|
+
}
|
|
187
|
+
return this.branch;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
reset(): void {
|
|
191
|
+
this.provider = null;
|
|
192
|
+
this.leafId = null;
|
|
193
|
+
this.branch = [];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function computeSessionTokenStats(sessionEvents: readonly unknown[]): SessionTokenStats {
|
|
198
|
+
let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0, subagentCost = 0;
|
|
199
|
+
let lastAssistant: AssistantMessage | undefined;
|
|
200
|
+
let thinkingLevelFromSession: string | null = null;
|
|
201
|
+
|
|
202
|
+
for (const e of sessionEvents) {
|
|
203
|
+
if (!isRecord(e)) continue;
|
|
204
|
+
|
|
205
|
+
if (e.type === "thinking_level_change" && typeof e.thinkingLevel === "string") {
|
|
206
|
+
thinkingLevelFromSession = e.thinkingLevel;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
subagentCost += extractSubagentResultCost(e);
|
|
210
|
+
|
|
211
|
+
if (e.type !== "message" || !isSessionAssistantMessage(e.message)) continue;
|
|
212
|
+
|
|
213
|
+
const m = e.message;
|
|
214
|
+
if (m.stopReason === "error" || m.stopReason === "aborted") continue;
|
|
215
|
+
|
|
216
|
+
input += m.usage.input;
|
|
217
|
+
output += m.usage.output;
|
|
218
|
+
cacheRead += m.usage.cacheRead;
|
|
219
|
+
cacheWrite += m.usage.cacheWrite;
|
|
220
|
+
cost += m.usage.cost.total;
|
|
221
|
+
if (getUsageTokenTotal(m.usage) > 0) {
|
|
222
|
+
lastAssistant = m;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { input, output, cacheRead, cacheWrite, cost, subagentCost, lastAssistant, thinkingLevelFromSession };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Cache for token counting: avoid re-scanning the full session event list on
|
|
231
|
+
* every render (250ms-1s cadence while streaming). Historical session entries
|
|
232
|
+
* are append-only, so appended entries are accumulated and an in-place update
|
|
233
|
+
* to the trailing streaming entry only recomputes that entry.
|
|
234
|
+
*/
|
|
235
|
+
export class SessionTokenStatsCache {
|
|
236
|
+
private eventCount = -1;
|
|
237
|
+
private lastEvent: unknown;
|
|
238
|
+
private lastSignature = "";
|
|
239
|
+
private prefixStats: SessionTokenStats | null = null;
|
|
240
|
+
private stats: SessionTokenStats | null = null;
|
|
241
|
+
|
|
242
|
+
get(sessionEvents: readonly unknown[]): SessionTokenStats {
|
|
243
|
+
const eventCount = sessionEvents.length;
|
|
244
|
+
const lastEvent = eventCount > 0 ? sessionEvents[eventCount - 1] : undefined;
|
|
245
|
+
const lastSignature = eventStatsSignature(lastEvent);
|
|
246
|
+
|
|
247
|
+
if (
|
|
248
|
+
this.stats !== null
|
|
249
|
+
&& this.eventCount === eventCount
|
|
250
|
+
&& this.lastEvent === lastEvent
|
|
251
|
+
&& this.lastSignature === lastSignature
|
|
252
|
+
) {
|
|
253
|
+
return this.stats;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// getBranch() walks unique parent links, so an identical trailing event at
|
|
257
|
+
// the previous count proves the whole previous array is an unchanged prefix.
|
|
258
|
+
const previousStats = this.stats;
|
|
259
|
+
const canExtendPreviousStats = previousStats !== null
|
|
260
|
+
&& eventCount > this.eventCount
|
|
261
|
+
&& (this.eventCount === 0 || (
|
|
262
|
+
sessionEvents[this.eventCount - 1] === this.lastEvent
|
|
263
|
+
&& eventStatsSignature(sessionEvents[this.eventCount - 1]) === this.lastSignature
|
|
264
|
+
));
|
|
265
|
+
|
|
266
|
+
let prefixStats: SessionTokenStats;
|
|
267
|
+
if (canExtendPreviousStats && previousStats !== null) {
|
|
268
|
+
prefixStats = copySessionTokenStats(previousStats);
|
|
269
|
+
for (let index = this.eventCount; index < eventCount - 1; index += 1) {
|
|
270
|
+
accumulateSessionEvent(prefixStats, sessionEvents[index]);
|
|
271
|
+
}
|
|
272
|
+
} else if (
|
|
273
|
+
this.prefixStats !== null
|
|
274
|
+
&& this.eventCount === eventCount
|
|
275
|
+
&& this.lastEvent === lastEvent
|
|
276
|
+
) {
|
|
277
|
+
// Same array with an in-place tail mutation: reuse the cached prefix.
|
|
278
|
+
prefixStats = this.prefixStats;
|
|
279
|
+
} else {
|
|
280
|
+
prefixStats = emptySessionTokenStats();
|
|
281
|
+
for (let index = 0; index < eventCount - 1; index += 1) {
|
|
282
|
+
accumulateSessionEvent(prefixStats, sessionEvents[index]);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const stats = copySessionTokenStats(prefixStats);
|
|
287
|
+
if (eventCount > 0) accumulateSessionEvent(stats, lastEvent);
|
|
288
|
+
|
|
289
|
+
this.eventCount = eventCount;
|
|
290
|
+
this.lastEvent = lastEvent;
|
|
291
|
+
this.lastSignature = lastSignature;
|
|
292
|
+
this.prefixStats = prefixStats;
|
|
293
|
+
this.stats = stats;
|
|
294
|
+
return stats;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
reset(): void {
|
|
298
|
+
this.eventCount = -1;
|
|
299
|
+
this.lastEvent = undefined;
|
|
300
|
+
this.lastSignature = "";
|
|
301
|
+
this.prefixStats = null;
|
|
302
|
+
this.stats = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"types": ["node"],
|
|
10
|
+
"allowImportingTsExtensions": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["*.ts", "bash-mode/**/*.ts", "fixed-editor/**/*.ts", "queue/**/*.ts"]
|
|
13
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Theme, ThemeColor } from "@selesai/code";
|
|
2
|
+
import type { CostCurrencyCode } from "./currency-rates.ts";
|
|
2
3
|
|
|
3
4
|
// Theme color - either a pi theme color name or a custom hex color
|
|
4
5
|
export type ColorValue = ThemeColor | `#${string}`;
|
|
@@ -20,6 +21,7 @@ export type SemanticColor =
|
|
|
20
21
|
| "contextError"
|
|
21
22
|
| "cost"
|
|
22
23
|
| "tokens"
|
|
24
|
+
| "queue"
|
|
23
25
|
| "separator"
|
|
24
26
|
| "border";
|
|
25
27
|
|
|
@@ -27,29 +29,30 @@ export type SemanticColor =
|
|
|
27
29
|
export type ColorScheme = Partial<Record<SemanticColor, ColorValue>>;
|
|
28
30
|
|
|
29
31
|
// Built-in segment identifiers
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
32
|
+
export const BUILTIN_STATUS_LINE_SEGMENT_IDS = [
|
|
33
|
+
"model",
|
|
34
|
+
"shell_mode",
|
|
35
|
+
"path",
|
|
36
|
+
"git",
|
|
37
|
+
"subagents",
|
|
38
|
+
"queue",
|
|
39
|
+
"token_in",
|
|
40
|
+
"token_out",
|
|
41
|
+
"token_total",
|
|
42
|
+
"cost",
|
|
43
|
+
"context_pct",
|
|
44
|
+
"context_total",
|
|
45
|
+
"time_spent",
|
|
46
|
+
"time",
|
|
47
|
+
"session",
|
|
48
|
+
"hostname",
|
|
49
|
+
"cache_read",
|
|
50
|
+
"cache_write",
|
|
51
|
+
"thinking",
|
|
52
|
+
"extension_statuses",
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
export type BuiltinStatusLineSegmentId = typeof BUILTIN_STATUS_LINE_SEGMENT_IDS[number];
|
|
53
56
|
|
|
54
57
|
// Segment identifiers (built-in + dynamically registered custom items)
|
|
55
58
|
export type StatusLineSegmentId = BuiltinStatusLineSegmentId | `custom:${string}`;
|
|
@@ -68,28 +71,47 @@ export type StatusLineSeparatorStyle =
|
|
|
68
71
|
| "star";
|
|
69
72
|
|
|
70
73
|
// Preset names
|
|
74
|
+
export type PowerlinePlacement = "above" | "below";
|
|
75
|
+
|
|
71
76
|
export type StatusLinePreset =
|
|
72
77
|
| "default"
|
|
73
78
|
| "minimal"
|
|
74
79
|
| "compact"
|
|
75
80
|
| "full"
|
|
76
81
|
| "nerd"
|
|
77
|
-
| "ascii"
|
|
78
|
-
| "custom";
|
|
82
|
+
| "ascii";
|
|
79
83
|
|
|
80
84
|
// Per-segment options
|
|
81
85
|
export interface StatusLineSegmentOptions {
|
|
82
|
-
model?: { showThinkingLevel?: boolean };
|
|
86
|
+
model?: { showThinkingLevel?: boolean; display?: "name" | "qualified" };
|
|
83
87
|
path?: {
|
|
84
88
|
mode?: "basename" | "abbreviated" | "full";
|
|
85
89
|
maxLength?: number;
|
|
86
90
|
};
|
|
87
|
-
git?: {
|
|
91
|
+
git?: {
|
|
92
|
+
showBranch?: boolean;
|
|
93
|
+
showStaged?: boolean;
|
|
94
|
+
showUnstaged?: boolean;
|
|
95
|
+
showUntracked?: boolean;
|
|
96
|
+
polling?: "full" | "branch" | "off";
|
|
97
|
+
/** Replace the branch icon with the origin remote's host logo
|
|
98
|
+
* (GitHub/GitLab/Bitbucket, or a generic git logo). Default false. */
|
|
99
|
+
hostIcon?: boolean;
|
|
100
|
+
};
|
|
88
101
|
time?: { format?: "12h" | "24h"; showSeconds?: boolean };
|
|
102
|
+
cost?: { subscriptionDisplay?: "subscription" | "reported-cost" | "both"; currency?: CostCurrencyCode };
|
|
103
|
+
context?: { format?: "full" | "percent" };
|
|
104
|
+
cache_read?: { format?: "tokens" | "percent" | "both" };
|
|
89
105
|
}
|
|
90
106
|
|
|
91
107
|
export type CustomItemPosition = "left" | "right" | "secondary";
|
|
92
108
|
|
|
109
|
+
export interface StatusLineLayout {
|
|
110
|
+
left?: StatusLineSegmentId[];
|
|
111
|
+
right?: StatusLineSegmentId[];
|
|
112
|
+
secondary?: StatusLineSegmentId[];
|
|
113
|
+
}
|
|
114
|
+
|
|
93
115
|
export interface CustomStatusItem {
|
|
94
116
|
id: string;
|
|
95
117
|
statusKey: string;
|
|
@@ -132,18 +154,38 @@ export interface GitStatus {
|
|
|
132
154
|
}
|
|
133
155
|
|
|
134
156
|
// Usage statistics
|
|
157
|
+
export interface QueueSummary {
|
|
158
|
+
queueCount: number;
|
|
159
|
+
ideaCount: number;
|
|
160
|
+
blockedCount: number;
|
|
161
|
+
compacting: boolean;
|
|
162
|
+
leadingText: string | null;
|
|
163
|
+
leadingIntent: "steer" | "follow-up" | "post-compact" | "idea" | null;
|
|
164
|
+
leadingStatus: "queued" | "blocked" | "delivering" | "sent" | "failed" | null;
|
|
165
|
+
}
|
|
166
|
+
|
|
135
167
|
export interface UsageStats {
|
|
136
168
|
input: number;
|
|
137
169
|
output: number;
|
|
138
170
|
cacheRead: number;
|
|
139
171
|
cacheWrite: number;
|
|
140
172
|
cost: number;
|
|
173
|
+
// Cumulative cost of subagent child runs (e.g. /parallel, /worker) launched from this session.
|
|
174
|
+
subagentCost: number;
|
|
141
175
|
}
|
|
142
176
|
|
|
143
177
|
// Context passed to segment render functions
|
|
144
178
|
export interface SegmentContext {
|
|
145
179
|
// From pi-mono
|
|
146
|
-
model: {
|
|
180
|
+
model: {
|
|
181
|
+
id: string;
|
|
182
|
+
name?: string;
|
|
183
|
+
provider?: string;
|
|
184
|
+
providerId?: string;
|
|
185
|
+
providerName?: string;
|
|
186
|
+
reasoning?: boolean;
|
|
187
|
+
contextWindow?: number;
|
|
188
|
+
} | undefined;
|
|
147
189
|
thinkingLevel: string;
|
|
148
190
|
sessionId: string | undefined;
|
|
149
191
|
sessionName: string | undefined;
|
|
@@ -151,11 +193,14 @@ export interface SegmentContext {
|
|
|
151
193
|
|
|
152
194
|
// Computed
|
|
153
195
|
usageStats: UsageStats;
|
|
154
|
-
|
|
196
|
+
contextTokens: number | null;
|
|
197
|
+
contextPercent: number | null;
|
|
155
198
|
contextWindow: number;
|
|
199
|
+
contextApproximate: boolean;
|
|
156
200
|
autoCompactEnabled: boolean;
|
|
157
201
|
customCompactionEnabled: boolean;
|
|
158
202
|
usingSubscription: boolean;
|
|
203
|
+
queueSummary: QueueSummary;
|
|
159
204
|
sessionStartTime: number;
|
|
160
205
|
shellModeActive: boolean;
|
|
161
206
|
shellRunning: boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface WelcomeDismissScheduler<Context> {
|
|
2
|
+
schedule(ctx: Context): void;
|
|
3
|
+
cancel(): void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface WelcomeDismissSchedulerOptions<Context> {
|
|
7
|
+
dismiss(ctx: Context): void;
|
|
8
|
+
getGeneration(): number;
|
|
9
|
+
isEnabled(): boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createWelcomeDismissScheduler<Context>(
|
|
13
|
+
options: WelcomeDismissSchedulerOptions<Context>,
|
|
14
|
+
): WelcomeDismissScheduler<Context> {
|
|
15
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
schedule(ctx) {
|
|
19
|
+
if (timer) return;
|
|
20
|
+
|
|
21
|
+
const generation = options.getGeneration();
|
|
22
|
+
timer = setTimeout(() => {
|
|
23
|
+
timer = null;
|
|
24
|
+
if (!options.isEnabled() || generation !== options.getGeneration()) return;
|
|
25
|
+
options.dismiss(ctx);
|
|
26
|
+
}, 0);
|
|
27
|
+
},
|
|
28
|
+
cancel() {
|
|
29
|
+
if (!timer) return;
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
timer = null;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { join, basename } from "node:path";
|
|
3
3
|
import { homedir as osHomedir } from "node:os";
|
|
4
4
|
import { CONFIG_DIR_NAME, getAgentDir, getSettingsPath } from "@selesai/code";
|
|
@@ -19,6 +19,13 @@ export interface LoadedCounts {
|
|
|
19
19
|
promptTemplates: number;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function formatTokens(tokens: number): string {
|
|
23
|
+
if (tokens < 1000) return tokens.toString();
|
|
24
|
+
if (tokens < 10000) return `${(tokens / 1000).toFixed(1)}k`;
|
|
25
|
+
if (tokens < 1000000) return `${Math.round(tokens / 1000)}k`;
|
|
26
|
+
return `${(tokens / 1000000).toFixed(tokens < 10000000 ? 1 : 0)}M`;
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
23
30
|
// Shared rendering utilities
|
|
24
31
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -41,6 +48,7 @@ const GRADIENT_COLORS = [
|
|
|
41
48
|
"\x1b[38;5;75m",
|
|
42
49
|
"\x1b[38;5;51m",
|
|
43
50
|
];
|
|
51
|
+
const SESSION_HEADER_READ_BYTES = 8192;
|
|
44
52
|
|
|
45
53
|
function bold(text: string): string {
|
|
46
54
|
return `\x1b[1m${text}\x1b[22m`;
|
|
@@ -97,6 +105,7 @@ interface WelcomeData {
|
|
|
97
105
|
loadedCounts: LoadedCounts;
|
|
98
106
|
guideMode: GuideMode;
|
|
99
107
|
newFeatures: GuideFeature[];
|
|
108
|
+
initialContextTokens: number | null;
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
function buildLeftColumn(data: WelcomeData, colWidth: number): string[] {
|
|
@@ -172,6 +181,14 @@ function buildRightColumn(data: WelcomeData, colWidth: number): string[] {
|
|
|
172
181
|
countLines.push(` ${dim("No extensions loaded")}`);
|
|
173
182
|
}
|
|
174
183
|
|
|
184
|
+
if (
|
|
185
|
+
data.initialContextTokens !== null
|
|
186
|
+
&& Number.isFinite(data.initialContextTokens)
|
|
187
|
+
&& data.initialContextTokens > 0
|
|
188
|
+
) {
|
|
189
|
+
countLines.push(` ${itemPrefix}${fgOnly("gitClean", `≈ ${formatTokens(data.initialContextTokens)}`)} initial prompt tokens`);
|
|
190
|
+
}
|
|
191
|
+
|
|
175
192
|
const dashboardLines = data.guideMode === "full"
|
|
176
193
|
? []
|
|
177
194
|
: [
|
|
@@ -182,7 +199,6 @@ function buildRightColumn(data: WelcomeData, colWidth: number): string[] {
|
|
|
182
199
|
` ${bold(fgOnly("accent", "Recent sessions"))}`,
|
|
183
200
|
...sessionLines,
|
|
184
201
|
];
|
|
185
|
-
|
|
186
202
|
return [
|
|
187
203
|
` ${bold(fgOnly("accent", "Tips"))}: ${dim("/")} commands · ${dim("/settings")} configure`,
|
|
188
204
|
` ${dim("Shift+Tab")} cycle thinking`,
|
|
@@ -266,8 +282,9 @@ export class WelcomeComponent implements Component {
|
|
|
266
282
|
loadedCounts: LoadedCounts = { contextFiles: 0, extensions: 0, skills: 0, promptTemplates: 0 },
|
|
267
283
|
guideMode: GuideMode = "full",
|
|
268
284
|
newFeatures: GuideFeature[] = [],
|
|
285
|
+
initialContextTokens: number | null = null,
|
|
269
286
|
) {
|
|
270
|
-
this.data = { modelName, providerName, recentSessions, loadedCounts, guideMode, newFeatures };
|
|
287
|
+
this.data = { modelName, providerName, recentSessions, loadedCounts, guideMode, newFeatures, initialContextTokens };
|
|
271
288
|
}
|
|
272
289
|
|
|
273
290
|
invalidate(): void {}
|
|
@@ -313,8 +330,9 @@ export class WelcomeHeader implements Component {
|
|
|
313
330
|
loadedCounts: LoadedCounts = { contextFiles: 0, extensions: 0, skills: 0, promptTemplates: 0 },
|
|
314
331
|
guideMode: GuideMode = "compact",
|
|
315
332
|
newFeatures: GuideFeature[] = [],
|
|
333
|
+
initialContextTokens: number | null = null,
|
|
316
334
|
) {
|
|
317
|
-
this.data = { modelName, providerName, recentSessions, loadedCounts, guideMode, newFeatures };
|
|
335
|
+
this.data = { modelName, providerName, recentSessions, loadedCounts, guideMode, newFeatures, initialContextTokens };
|
|
318
336
|
}
|
|
319
337
|
|
|
320
338
|
invalidate(): void {}
|
|
@@ -352,6 +370,15 @@ export class WelcomeHeader implements Component {
|
|
|
352
370
|
const loggedDiscoveryErrors = new Set<string>();
|
|
353
371
|
|
|
354
372
|
function logDiscoveryError(scope: string, error: unknown): void {
|
|
373
|
+
if (
|
|
374
|
+
typeof error === "object" &&
|
|
375
|
+
error !== null &&
|
|
376
|
+
"code" in error &&
|
|
377
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
378
|
+
) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
355
382
|
const message = error instanceof Error ? error.message : String(error);
|
|
356
383
|
const key = `${scope}:${message}`;
|
|
357
384
|
if (loggedDiscoveryErrors.has(key)) {
|
|
@@ -578,6 +605,39 @@ export function discoverLoadedCounts(): LoadedCounts {
|
|
|
578
605
|
/**
|
|
579
606
|
* Get recent sessions from the sessions directory.
|
|
580
607
|
*/
|
|
608
|
+
function readSessionHeaderProjectName(filePath: string): string | null {
|
|
609
|
+
let fd: number | null = null;
|
|
610
|
+
try {
|
|
611
|
+
fd = openSync(filePath, "r");
|
|
612
|
+
const buffer = Buffer.alloc(SESSION_HEADER_READ_BYTES);
|
|
613
|
+
const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
614
|
+
const firstLine = buffer.toString("utf8", 0, bytesRead).split(/\r?\n/, 1)[0]?.trim();
|
|
615
|
+
if (!firstLine) return null;
|
|
616
|
+
|
|
617
|
+
const header: unknown = JSON.parse(firstLine);
|
|
618
|
+
if (typeof header !== "object" || header === null || Array.isArray(header)) return null;
|
|
619
|
+
|
|
620
|
+
const cwd = Reflect.get(header, "cwd");
|
|
621
|
+
if (typeof cwd !== "string" || cwd.trim().length === 0) return null;
|
|
622
|
+
|
|
623
|
+
return basename(cwd) || cwd;
|
|
624
|
+
} catch {
|
|
625
|
+
return null;
|
|
626
|
+
} finally {
|
|
627
|
+
if (fd !== null) closeSync(fd);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function sessionProjectNameFromDirectory(dir: string): string {
|
|
632
|
+
const parentName = basename(dir);
|
|
633
|
+
if (!parentName.startsWith("--")) {
|
|
634
|
+
return parentName;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const parts = parentName.split("-").filter(p => p);
|
|
638
|
+
return parts[parts.length - 1] || parentName;
|
|
639
|
+
}
|
|
640
|
+
|
|
581
641
|
export function getRecentSessions(maxCount: number = 3): RecentSession[] {
|
|
582
642
|
const homeDir = getAgentDir();
|
|
583
643
|
const home = homedirSafe();
|
|
@@ -600,12 +660,7 @@ export function getRecentSessions(maxCount: number = 3): RecentSession[] {
|
|
|
600
660
|
if (stats.isDirectory()) {
|
|
601
661
|
scanDir(entryPath);
|
|
602
662
|
} else if (entry.endsWith(".jsonl")) {
|
|
603
|
-
const
|
|
604
|
-
let projectName = parentName;
|
|
605
|
-
if (parentName.startsWith("--")) {
|
|
606
|
-
const parts = parentName.split("-").filter(p => p);
|
|
607
|
-
projectName = parts[parts.length - 1] || parentName;
|
|
608
|
-
}
|
|
663
|
+
const projectName = readSessionHeaderProjectName(entryPath) ?? sessionProjectNameFromDirectory(dir);
|
|
609
664
|
sessions.push({ name: projectName, mtime: stats.mtimeMs });
|
|
610
665
|
}
|
|
611
666
|
} catch (error) {
|