@selesai/code 0.7.0 → 0.8.1
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 +16 -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 +156 -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/guide.ts +1 -1
- 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 +11 -6
- 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
|
@@ -5,6 +5,10 @@ import type { BuiltinStatusLineSegmentId, RenderedSegment, SegmentContext, Seman
|
|
|
5
5
|
import { normalizeCompactExtensionStatus, normalizeExtensionStatusValue } from "./powerline-config.ts";
|
|
6
6
|
import { fg, rainbow, applyColor } from "./theme.ts";
|
|
7
7
|
import { getIcons, SEP_DOT, getThinkingText } from "./icons.ts";
|
|
8
|
+
import { formatUsdCost } from "./currency-rates.ts";
|
|
9
|
+
import { getGitRemoteHost } from "./git-status.ts";
|
|
10
|
+
import type { IconSet } from "./icons.ts";
|
|
11
|
+
import type { GitHost } from "./git-status.ts";
|
|
8
12
|
|
|
9
13
|
function color(ctx: SegmentContext, semantic: SemanticColor, text: string): string {
|
|
10
14
|
return fg(ctx.theme, semantic, text, ctx.colors);
|
|
@@ -47,8 +51,10 @@ const modelSegment: StatusLineSegment = {
|
|
|
47
51
|
const opts = ctx.options.model ?? {};
|
|
48
52
|
|
|
49
53
|
let modelName = ctx.model?.name || ctx.model?.id || "no-model";
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
if (opts.display === "qualified" && ctx.model?.id) {
|
|
55
|
+
const provider = ctx.model.provider || ctx.model.providerId || ctx.model.providerName;
|
|
56
|
+
modelName = provider && !ctx.model.id.includes("/") ? `${provider}/${ctx.model.id}` : ctx.model.id;
|
|
57
|
+
} else if (modelName.startsWith("Claude ")) {
|
|
52
58
|
modelName = modelName.slice(7);
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -68,38 +74,6 @@ const modelSegment: StatusLineSegment = {
|
|
|
68
74
|
},
|
|
69
75
|
};
|
|
70
76
|
|
|
71
|
-
const identityGroupSegment: StatusLineSegment = {
|
|
72
|
-
id: "identity_group",
|
|
73
|
-
render(ctx) {
|
|
74
|
-
const model = modelSegment.render({
|
|
75
|
-
...ctx,
|
|
76
|
-
options: { ...ctx.options, model: { ...(ctx.options.model ?? {}), showThinkingLevel: false } },
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const parts = model.visible && model.content ? [model.content] : [];
|
|
80
|
-
const thinkingText = getThinkingText(ctx.thinkingLevel || "off");
|
|
81
|
-
if (thinkingText) {
|
|
82
|
-
const level = ctx.thinkingLevel || "off";
|
|
83
|
-
let thinkingContent: string;
|
|
84
|
-
if (level === "high" || level === "xhigh") {
|
|
85
|
-
thinkingContent = rainbow(thinkingText);
|
|
86
|
-
} else if (level === "minimal") {
|
|
87
|
-
thinkingContent = color(ctx, "thinkingMinimal", thinkingText);
|
|
88
|
-
} else if (level === "low") {
|
|
89
|
-
thinkingContent = color(ctx, "thinkingLow", thinkingText);
|
|
90
|
-
} else if (level === "medium") {
|
|
91
|
-
thinkingContent = color(ctx, "thinkingMedium", thinkingText);
|
|
92
|
-
} else {
|
|
93
|
-
thinkingContent = color(ctx, "thinking", thinkingText);
|
|
94
|
-
}
|
|
95
|
-
parts.push(thinkingContent);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (parts.length === 0) return { content: "", visible: false };
|
|
99
|
-
return { content: parts.join(" "), visible: true };
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
|
|
103
77
|
const shellModeSegment: StatusLineSegment = {
|
|
104
78
|
id: "shell_mode",
|
|
105
79
|
render(ctx) {
|
|
@@ -157,14 +131,31 @@ const pathSegment: StatusLineSegment = {
|
|
|
157
131
|
},
|
|
158
132
|
};
|
|
159
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Icon for the branch label: the origin remote's host logo when hostIcon is
|
|
136
|
+
* enabled and a remote is known, otherwise the plain branch icon. An
|
|
137
|
+
* unrecognized remote falls back to the generic git logo.
|
|
138
|
+
*/
|
|
139
|
+
function resolveBranchIcon(icons: IconSet, hostIcon: boolean): string {
|
|
140
|
+
if (!hostIcon) return icons.branch;
|
|
141
|
+
const host = getGitRemoteHost();
|
|
142
|
+
const byHost: Record<GitHost, string> = {
|
|
143
|
+
github: icons.github,
|
|
144
|
+
gitlab: icons.gitlab,
|
|
145
|
+
bitbucket: icons.bitbucket,
|
|
146
|
+
other: icons.git,
|
|
147
|
+
};
|
|
148
|
+
return host ? byHost[host] : icons.branch;
|
|
149
|
+
}
|
|
150
|
+
|
|
160
151
|
const gitSegment: StatusLineSegment = {
|
|
161
152
|
id: "git",
|
|
162
153
|
render(ctx) {
|
|
163
154
|
const icons = getIcons();
|
|
164
155
|
const opts = ctx.options.git ?? {};
|
|
165
156
|
const { branch, staged, unstaged, untracked } = ctx.git;
|
|
166
|
-
const gitStatus = (staged > 0 || unstaged > 0 || untracked > 0)
|
|
167
|
-
? { staged, unstaged, untracked }
|
|
157
|
+
const gitStatus = (staged > 0 || unstaged > 0 || untracked > 0)
|
|
158
|
+
? { staged, unstaged, untracked }
|
|
168
159
|
: null;
|
|
169
160
|
|
|
170
161
|
if (!branch && !gitStatus) return { content: "", visible: false };
|
|
@@ -177,7 +168,8 @@ const gitSegment: StatusLineSegment = {
|
|
|
177
168
|
let content = "";
|
|
178
169
|
if (showBranch && branch) {
|
|
179
170
|
// Color just the branch name (icon + branch text)
|
|
180
|
-
|
|
171
|
+
const branchIcon = resolveBranchIcon(icons, opts.hostIcon === true);
|
|
172
|
+
content = color(ctx, branchColor, withIcon(branchIcon, branch));
|
|
181
173
|
}
|
|
182
174
|
|
|
183
175
|
// Add status indicators (each with their own color, not wrapped)
|
|
@@ -225,7 +217,7 @@ const thinkingSegment: StatusLineSegment = {
|
|
|
225
217
|
const label = levelText[level] || level;
|
|
226
218
|
const content = `think:${label}`;
|
|
227
219
|
|
|
228
|
-
if (level === "high" || level === "xhigh") {
|
|
220
|
+
if (level === "high" || level === "xhigh" || level === "max") {
|
|
229
221
|
return { content: rainbow(content), visible: true };
|
|
230
222
|
}
|
|
231
223
|
|
|
@@ -253,6 +245,31 @@ const subagentsSegment: StatusLineSegment = {
|
|
|
253
245
|
},
|
|
254
246
|
};
|
|
255
247
|
|
|
248
|
+
const queueSegment: StatusLineSegment = {
|
|
249
|
+
id: "queue",
|
|
250
|
+
render(ctx) {
|
|
251
|
+
const summary = ctx.queueSummary;
|
|
252
|
+
const parts: string[] = [];
|
|
253
|
+
|
|
254
|
+
if (summary.compacting && summary.queueCount > 0) {
|
|
255
|
+
parts.push(`compact q ${summary.queueCount}`);
|
|
256
|
+
} else if (summary.queueCount > 0) {
|
|
257
|
+
parts.push(`q ${summary.queueCount}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (summary.ideaCount > 0) {
|
|
261
|
+
parts.push(`ideas ${summary.ideaCount}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (summary.blockedCount > 0) {
|
|
265
|
+
parts.push(`blocked ${summary.blockedCount}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (parts.length === 0) return { content: "", visible: false };
|
|
269
|
+
return { content: color(ctx, "queue", parts.join(SEP_DOT)), visible: true };
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
256
273
|
const tokenInSegment: StatusLineSegment = {
|
|
257
274
|
id: "token_in",
|
|
258
275
|
render(ctx) {
|
|
@@ -290,52 +307,32 @@ const tokenTotalSegment: StatusLineSegment = {
|
|
|
290
307
|
},
|
|
291
308
|
};
|
|
292
309
|
|
|
293
|
-
const
|
|
294
|
-
id: "
|
|
310
|
+
const costSegment: StatusLineSegment = {
|
|
311
|
+
id: "cost",
|
|
295
312
|
render(ctx) {
|
|
296
|
-
const
|
|
297
|
-
const { input, output, cacheRead, cacheWrite, cost } = ctx.usageStats;
|
|
298
|
-
const total = input + output + cacheRead + cacheWrite;
|
|
313
|
+
const cost = ctx.usageStats.cost + (ctx.usageStats.subagentCost ?? 0);
|
|
299
314
|
const usingSubscription = ctx.usingSubscription;
|
|
300
315
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const inText = input ? withIcon(icons.input, formatTokens(input)) : withIcon(icons.input, "0");
|
|
304
|
-
const cacheRate = input + cacheRead > 0 ? Math.round((cacheRead / (input + cacheRead)) * 100) : 0;
|
|
305
|
-
const cacheText = `${formatTokens(cacheRead)}/${cacheRate}%`;
|
|
306
|
-
parts.push(color(ctx, "tokens", `${inText}(${cacheText})`));
|
|
316
|
+
if (!cost && !usingSubscription) {
|
|
317
|
+
return { content: "", visible: false };
|
|
307
318
|
}
|
|
308
319
|
|
|
309
|
-
|
|
310
|
-
|
|
320
|
+
const reportedCost = cost > 0 ? formatUsdCost(cost, ctx.options.cost?.currency) : null;
|
|
321
|
+
if (!usingSubscription) {
|
|
322
|
+
return reportedCost
|
|
323
|
+
? { content: color(ctx, "cost", reportedCost), visible: true }
|
|
324
|
+
: { content: "", visible: false };
|
|
311
325
|
}
|
|
312
326
|
|
|
313
|
-
const
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
totalCostParts.push(usingSubscription ? "sub" : `$${cost.toFixed(2)}`);
|
|
327
|
+
const subscriptionDisplay = ctx.options.cost?.subscriptionDisplay ?? "subscription";
|
|
328
|
+
if (subscriptionDisplay === "reported-cost" && reportedCost) {
|
|
329
|
+
return { content: color(ctx, "cost", reportedCost), visible: true };
|
|
317
330
|
}
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (parts.length === 0) return { content: "", visible: false };
|
|
323
|
-
return { content: parts.join(SEP_DOT), visible: true };
|
|
324
|
-
},
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
const costSegment: StatusLineSegment = {
|
|
328
|
-
id: "cost",
|
|
329
|
-
render(ctx) {
|
|
330
|
-
const { cost } = ctx.usageStats;
|
|
331
|
-
const usingSubscription = ctx.usingSubscription;
|
|
332
|
-
|
|
333
|
-
if (!cost && !usingSubscription) {
|
|
334
|
-
return { content: "", visible: false };
|
|
331
|
+
if (subscriptionDisplay === "both" && reportedCost) {
|
|
332
|
+
return { content: color(ctx, "cost", `${reportedCost} (sub)`), visible: true };
|
|
335
333
|
}
|
|
336
334
|
|
|
337
|
-
|
|
338
|
-
return { content: color(ctx, "cost", costDisplay), visible: true };
|
|
335
|
+
return { content: color(ctx, "cost", "(sub)"), visible: true };
|
|
339
336
|
},
|
|
340
337
|
};
|
|
341
338
|
|
|
@@ -345,24 +342,33 @@ const contextPctSegment: StatusLineSegment = {
|
|
|
345
342
|
if (ctx.customCompactionEnabled) return { content: "", visible: false };
|
|
346
343
|
|
|
347
344
|
const icons = getIcons();
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
345
|
+
const { contextTokens, contextPercent, contextWindow } = ctx;
|
|
346
|
+
|
|
347
|
+
const autoIcon = ctx.autoCompactEnabled && icons.auto ? ` ${icons.auto}` : "";
|
|
348
|
+
const percentOnly = ctx.options.context?.format === "percent";
|
|
349
|
+
const hasKnownUsage = contextTokens !== null && contextPercent !== null;
|
|
350
|
+
const approximate = ctx.contextApproximate ? "~" : "";
|
|
351
|
+
// "full" (default): tokens/window + one-decimal percentage + auto-compact icon.
|
|
352
|
+
// "percent": bare rounded percentage, threshold-colored, no icons.
|
|
353
|
+
const text = percentOnly
|
|
354
|
+
? (hasKnownUsage ? `${approximate}${Math.round(contextPercent)}%` : "?")
|
|
355
|
+
: hasKnownUsage
|
|
356
|
+
? `${approximate}${formatTokens(contextTokens)}/${formatTokens(contextWindow)} (${contextPercent.toFixed(1)}%)${autoIcon}`
|
|
357
|
+
: `?/${formatTokens(contextWindow)}${autoIcon}`;
|
|
358
|
+
|
|
359
|
+
// Icon outside color, text inside - use semantic colors for thresholds
|
|
360
|
+
let content: string;
|
|
361
|
+
const colored = (semantic: "context" | "contextWarn" | "contextError") =>
|
|
362
|
+
percentOnly ? color(ctx, semantic, text) : withIcon(icons.context, color(ctx, semantic, text));
|
|
363
|
+
if (hasKnownUsage && contextPercent > 90) {
|
|
364
|
+
content = colored("contextError");
|
|
365
|
+
} else if (hasKnownUsage && contextPercent > 70) {
|
|
366
|
+
content = colored("contextWarn");
|
|
361
367
|
} else {
|
|
362
|
-
|
|
368
|
+
content = colored("context");
|
|
363
369
|
}
|
|
364
370
|
|
|
365
|
-
return { content
|
|
371
|
+
return { content, visible: true };
|
|
366
372
|
},
|
|
367
373
|
};
|
|
368
374
|
|
|
@@ -429,17 +435,6 @@ const sessionSegment: StatusLineSegment = {
|
|
|
429
435
|
},
|
|
430
436
|
};
|
|
431
437
|
|
|
432
|
-
const sessionNameSegment: StatusLineSegment = {
|
|
433
|
-
id: "session_name",
|
|
434
|
-
render(ctx) {
|
|
435
|
-
const icons = getIcons();
|
|
436
|
-
const display = ctx.sessionName?.trim();
|
|
437
|
-
if (!display) return { content: "", visible: false };
|
|
438
|
-
|
|
439
|
-
return { content: color(ctx, "model", withIcon(icons.session, display)), visible: true };
|
|
440
|
-
},
|
|
441
|
-
};
|
|
442
|
-
|
|
443
438
|
const hostnameSegment: StatusLineSegment = {
|
|
444
439
|
id: "hostname",
|
|
445
440
|
render() {
|
|
@@ -453,11 +448,21 @@ const cacheReadSegment: StatusLineSegment = {
|
|
|
453
448
|
id: "cache_read",
|
|
454
449
|
render(ctx) {
|
|
455
450
|
const icons = getIcons();
|
|
456
|
-
const { cacheRead } = ctx.usageStats;
|
|
451
|
+
const { cacheRead, input } = ctx.usageStats;
|
|
457
452
|
if (!cacheRead) return { content: "", visible: false };
|
|
458
453
|
|
|
459
|
-
const
|
|
460
|
-
const
|
|
454
|
+
const format = ctx.options.cache_read?.format ?? "tokens";
|
|
455
|
+
const hitRate = input + cacheRead > 0
|
|
456
|
+
? ((cacheRead / (input + cacheRead)) * 100).toFixed(0)
|
|
457
|
+
: "0";
|
|
458
|
+
|
|
459
|
+
let content: string;
|
|
460
|
+
if (format === "percent") {
|
|
461
|
+
content = [icons.cache, `${hitRate}%`].filter(Boolean).join(" ");
|
|
462
|
+
} else {
|
|
463
|
+
const tokens = [icons.cache, icons.input, formatTokens(cacheRead)].filter(Boolean).join(" ");
|
|
464
|
+
content = format === "both" ? `${tokens} (${hitRate}%)` : tokens;
|
|
465
|
+
}
|
|
461
466
|
return { content: color(ctx, "tokens", content), visible: true };
|
|
462
467
|
},
|
|
463
468
|
};
|
|
@@ -508,23 +513,21 @@ const extensionStatusesSegment: StatusLineSegment = {
|
|
|
508
513
|
|
|
509
514
|
export const SEGMENTS: Record<BuiltinStatusLineSegmentId, StatusLineSegment> = {
|
|
510
515
|
model: modelSegment,
|
|
511
|
-
identity_group: identityGroupSegment,
|
|
512
516
|
shell_mode: shellModeSegment,
|
|
513
517
|
path: pathSegment,
|
|
514
518
|
git: gitSegment,
|
|
515
519
|
thinking: thinkingSegment,
|
|
516
520
|
subagents: subagentsSegment,
|
|
521
|
+
queue: queueSegment,
|
|
517
522
|
token_in: tokenInSegment,
|
|
518
523
|
token_out: tokenOutSegment,
|
|
519
524
|
token_total: tokenTotalSegment,
|
|
520
|
-
usage_group: usageGroupSegment,
|
|
521
525
|
cost: costSegment,
|
|
522
526
|
context_pct: contextPctSegment,
|
|
523
527
|
context_total: contextTotalSegment,
|
|
524
528
|
time_spent: timeSpentSegment,
|
|
525
529
|
time: timeSegment,
|
|
526
530
|
session: sessionSegment,
|
|
527
|
-
session_name: sessionNameSegment,
|
|
528
531
|
hostname: hostnameSegment,
|
|
529
532
|
cache_read: cacheReadSegment,
|
|
530
533
|
cache_write: cacheWriteSegment,
|
|
@@ -553,8 +556,12 @@ function renderCustomSegment(id: `custom:${string}`, ctx: SegmentContext): Rende
|
|
|
553
556
|
return { content, visible: true };
|
|
554
557
|
}
|
|
555
558
|
|
|
559
|
+
function isCustomSegmentId(id: StatusLineSegmentId): id is `custom:${string}` {
|
|
560
|
+
return id.startsWith("custom:");
|
|
561
|
+
}
|
|
562
|
+
|
|
556
563
|
export function renderSegment(id: StatusLineSegmentId, ctx: SegmentContext): RenderedSegment {
|
|
557
|
-
if (id
|
|
564
|
+
if (isCustomSegmentId(id)) {
|
|
558
565
|
return renderCustomSegment(id, ctx);
|
|
559
566
|
}
|
|
560
567
|
|
|
@@ -22,7 +22,7 @@ function readUsage(value: unknown): UsageStats | null {
|
|
|
22
22
|
const { input, output, cacheRead, cacheWrite, cost } = value;
|
|
23
23
|
if (![input, output, cacheRead, cacheWrite, cost].every(finiteNumber)) return null;
|
|
24
24
|
|
|
25
|
-
return { input, output, cacheRead, cacheWrite, cost };
|
|
25
|
+
return { input, output, cacheRead, cacheWrite, cost } as UsageStats;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function readSubagentToolResultUsage(message: unknown): UsageStats | null {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { matchesKey } from "@earendil-works/pi-tui";
|
|
1
|
+
import { isKeyRelease, matchesKey, type KeyId } from "@earendil-works/pi-tui";
|
|
2
2
|
|
|
3
3
|
const SUPER_SHORTCUT_PATTERNS = new Map<string, RegExp>([
|
|
4
4
|
["super+up", /^\x1b\[(?:1;9(?::[12])?[AH]|574(?:19|23);9(?::[12])?u|7;9(?::[12])?~|27;9;65~)$/],
|
|
@@ -37,11 +37,25 @@ export function shortcutConflictKey(shortcut: string): string {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export function matchesConfiguredShortcut(data: string, shortcut: string): boolean {
|
|
40
|
+
export function matchesConfiguredShortcut(data: string, shortcut: string | null | undefined): boolean {
|
|
41
|
+
if (!shortcut) return false;
|
|
42
|
+
|
|
41
43
|
const normalizedShortcut = shortcut.toLowerCase();
|
|
42
44
|
if (shortcutUsesSuper(normalizedShortcut)) {
|
|
43
45
|
return SUPER_SHORTCUT_PATTERNS.get(normalizedShortcut)?.test(data) ?? false;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
return matchesKey(data, shortcut);
|
|
48
|
+
return matchesKey(data, shortcut as KeyId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function matchesStashShortcutInput(data: string, options: { includePrintableSharpS?: boolean } = {}): boolean {
|
|
52
|
+
if (isKeyRelease(data)) return false;
|
|
53
|
+
|
|
54
|
+
return (options.includePrintableSharpS === true && data === "ß")
|
|
55
|
+
|| data === "\x1bs"
|
|
56
|
+
|| data === "\x1bS"
|
|
57
|
+
|| /^\x1b\[(?:83|115)(?::\d*)?(?::\d*)?;3(?::\d+)?u$/.test(data)
|
|
58
|
+
|| data === "\x1b[27;3;115~"
|
|
59
|
+
|| data === "\x1b[27;3;83~"
|
|
60
|
+
|| matchesKey(data, "alt+s");
|
|
47
61
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
OneOffBashAutocompleteProvider,
|
|
14
14
|
} from "../bash-mode/completion.ts";
|
|
15
15
|
import { getIcons } from "../icons.ts";
|
|
16
|
+
import { resolveColor } from "../theme.ts";
|
|
16
17
|
import { ManagedShellSession } from "../bash-mode/shell-session.ts";
|
|
17
18
|
|
|
18
19
|
function getMethod(target: object, name: string): Function {
|
|
@@ -23,6 +24,13 @@ function getMethod(target: object, name: string): Function {
|
|
|
23
24
|
return method;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
function resolveManagedShellPath(): string | null {
|
|
28
|
+
for (const shellPath of ["/bin/zsh", "/bin/bash"]) {
|
|
29
|
+
if (existsSync(shellPath)) return shellPath;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
function ensureEditorModuleLinks(): { cleanup: () => void } {
|
|
27
35
|
const nodeModulesDir = join(process.cwd(), "node_modules", "@earendil-works");
|
|
28
36
|
mkdirSync(nodeModulesDir, { recursive: true });
|
|
@@ -76,6 +84,38 @@ test("project history is stored newest-first and global zsh history parses histf
|
|
|
76
84
|
assert.deepEqual(global, ["plain-command", "git pull", "git fetch"]);
|
|
77
85
|
});
|
|
78
86
|
|
|
87
|
+
test("global history caches an unreadable file until its fingerprint changes", () => {
|
|
88
|
+
const cwd = mkdtempSync(join(tmpdir(), "powerline-unreadable-history-"));
|
|
89
|
+
const historyPath = join(cwd, ".zsh_history");
|
|
90
|
+
const originalHistfile = process.env.HISTFILE;
|
|
91
|
+
const originalDebug = console.debug;
|
|
92
|
+
let debugCalls = 0;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
mkdirSync(historyPath);
|
|
96
|
+
process.env.HISTFILE = historyPath;
|
|
97
|
+
console.debug = () => {
|
|
98
|
+
debugCalls += 1;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
assert.deepEqual(readGlobalShellHistory("/bin/zsh"), []);
|
|
102
|
+
assert.deepEqual(readGlobalShellHistory("/bin/zsh"), []);
|
|
103
|
+
assert.equal(debugCalls, 1);
|
|
104
|
+
|
|
105
|
+
rmSync(historyPath, { recursive: true });
|
|
106
|
+
writeFileSync(historyPath, ": 1711111111:0;git status\n");
|
|
107
|
+
assert.deepEqual(readGlobalShellHistory("/bin/zsh"), ["git status"]);
|
|
108
|
+
} finally {
|
|
109
|
+
console.debug = originalDebug;
|
|
110
|
+
if (originalHistfile === undefined) {
|
|
111
|
+
delete process.env.HISTFILE;
|
|
112
|
+
} else {
|
|
113
|
+
process.env.HISTFILE = originalHistfile;
|
|
114
|
+
}
|
|
115
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
79
119
|
test("matchHistoryEntries returns newest entries when the prefix is empty", () => {
|
|
80
120
|
const matches = matchHistoryEntries([
|
|
81
121
|
"git stash",
|
|
@@ -115,6 +155,28 @@ test("theme.json can override icons without touching colors", () => {
|
|
|
115
155
|
}
|
|
116
156
|
});
|
|
117
157
|
|
|
158
|
+
test("theme.json loads from the documented agent extension path", () => {
|
|
159
|
+
const originalAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
160
|
+
const agentDir = mkdtempSync(join(tmpdir(), "powerline-theme-"));
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const themeDir = join(agentDir, "extensions", "powerline-footer");
|
|
164
|
+
mkdirSync(themeDir, { recursive: true });
|
|
165
|
+
writeFileSync(join(themeDir, "theme.json"), JSON.stringify({ colors: { model: "#ff5500", path: "#ff5500" } }));
|
|
166
|
+
process.env.SELESAI_CODING_AGENT_DIR = agentDir;
|
|
167
|
+
|
|
168
|
+
assert.equal(resolveColor("model"), "#ff5500");
|
|
169
|
+
assert.equal(resolveColor("path"), "#ff5500");
|
|
170
|
+
} finally {
|
|
171
|
+
if (originalAgentDir === undefined) {
|
|
172
|
+
delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
173
|
+
} else {
|
|
174
|
+
process.env.SELESAI_CODING_AGENT_DIR = originalAgentDir;
|
|
175
|
+
}
|
|
176
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
118
180
|
test("one-off bash command context strips ! and !! prefixes", () => {
|
|
119
181
|
assert.deepEqual(getOneOffBashCommandContext("!git status"), {
|
|
120
182
|
prefix: "!",
|
|
@@ -450,12 +512,18 @@ test("deterministic path completion handles bash argument position", async () =>
|
|
|
450
512
|
assert.equal(suggestion?.source, "path");
|
|
451
513
|
});
|
|
452
514
|
|
|
453
|
-
test("managed shell session preserves cwd changes across commands", async () => {
|
|
515
|
+
test("managed shell session preserves cwd changes across commands", async (t) => {
|
|
516
|
+
const shellPath = resolveManagedShellPath();
|
|
517
|
+
if (!shellPath) {
|
|
518
|
+
t.skip("requires zsh or bash");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
454
522
|
const cwd = mkdtempSync(join(tmpdir(), "powerline-shell-"));
|
|
455
523
|
const childDir = join(cwd, "child");
|
|
456
524
|
mkdirSync(childDir, { recursive: true });
|
|
457
525
|
const store = new BashTranscriptStore({ transcriptMaxLines: 100, transcriptMaxBytes: 64 * 1024 });
|
|
458
|
-
const session = new ManagedShellSession(
|
|
526
|
+
const session = new ManagedShellSession(shellPath, cwd, store, () => {}, () => {});
|
|
459
527
|
|
|
460
528
|
try {
|
|
461
529
|
await session.ensureReady();
|
|
@@ -482,10 +550,16 @@ test("managed shell session preserves cwd changes across commands", async () =>
|
|
|
482
550
|
}
|
|
483
551
|
});
|
|
484
552
|
|
|
485
|
-
test("managed shell session recovers cleanly after interrupt", async () => {
|
|
553
|
+
test("managed shell session recovers cleanly after interrupt", async (t) => {
|
|
554
|
+
const shellPath = resolveManagedShellPath();
|
|
555
|
+
if (!shellPath) {
|
|
556
|
+
t.skip("requires zsh or bash");
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
486
560
|
const cwd = mkdtempSync(join(tmpdir(), "powerline-shell-interrupt-"));
|
|
487
561
|
const store = new BashTranscriptStore({ transcriptMaxLines: 100, transcriptMaxBytes: 64 * 1024 });
|
|
488
|
-
const session = new ManagedShellSession(
|
|
562
|
+
const session = new ManagedShellSession(shellPath, cwd, store, () => {}, () => {});
|
|
489
563
|
|
|
490
564
|
const waitForCommand = async () => {
|
|
491
565
|
const start = Date.now();
|
|
@@ -661,7 +735,12 @@ test("bash editor refreshes shell ghost state after a bracketed paste completes"
|
|
|
661
735
|
}
|
|
662
736
|
});
|
|
663
737
|
|
|
664
|
-
test("bash editor inserts Finder file drops as path strings", async () => {
|
|
738
|
+
test("bash editor inserts Finder file drops as path strings", async (t) => {
|
|
739
|
+
if (process.platform === "win32") {
|
|
740
|
+
t.skip("Finder file drops are macOS/POSIX paths");
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
665
744
|
const links = ensureEditorModuleLinks();
|
|
666
745
|
|
|
667
746
|
try {
|
|
@@ -733,28 +812,26 @@ test("one-off bash autocomplete provider stays inactive even inside bang command
|
|
|
733
812
|
assert.equal(suggestions, null);
|
|
734
813
|
});
|
|
735
814
|
|
|
736
|
-
test("bash autocomplete providers return null
|
|
815
|
+
test("bash autocomplete providers return null in shell contexts", async () => {
|
|
737
816
|
const signal = new AbortController().signal;
|
|
738
817
|
|
|
739
|
-
const bashSuggestions = new BashAutocompleteProvider().getSuggestions(["git st"], 0, 6, { signal });
|
|
740
|
-
const oneOffSuggestions = new OneOffBashAutocompleteProvider().getSuggestions(["!git st"], 0, 7, { signal });
|
|
818
|
+
const bashSuggestions = await new BashAutocompleteProvider().getSuggestions(["git st"], 0, 6, { signal });
|
|
819
|
+
const oneOffSuggestions = await new OneOffBashAutocompleteProvider().getSuggestions(["!git st"], 0, 7, { signal });
|
|
741
820
|
|
|
742
821
|
assert.equal(bashSuggestions, null);
|
|
743
822
|
assert.equal(oneOffSuggestions, null);
|
|
744
|
-
assert.equal(bashSuggestions instanceof Promise, false);
|
|
745
|
-
assert.equal(oneOffSuggestions instanceof Promise, false);
|
|
746
823
|
});
|
|
747
824
|
|
|
748
|
-
test("mode-aware autocomplete provider preserves
|
|
825
|
+
test("mode-aware autocomplete provider preserves default results", async () => {
|
|
749
826
|
const signal = new AbortController().signal;
|
|
750
|
-
const
|
|
827
|
+
const result = {
|
|
751
828
|
items: [{ value: "status", label: "status" }],
|
|
752
829
|
prefix: "st",
|
|
753
830
|
};
|
|
754
831
|
const provider = new ModeAwareAutocompleteProvider(
|
|
755
832
|
{
|
|
756
|
-
getSuggestions() {
|
|
757
|
-
return
|
|
833
|
+
async getSuggestions() {
|
|
834
|
+
return result;
|
|
758
835
|
},
|
|
759
836
|
applyCompletion(lines: string[], cursorLine: number, cursorCol: number) {
|
|
760
837
|
return { lines, cursorLine, cursorCol };
|
|
@@ -765,10 +842,9 @@ test("mode-aware autocomplete provider preserves synchronous default results", (
|
|
|
765
842
|
() => false,
|
|
766
843
|
);
|
|
767
844
|
|
|
768
|
-
const suggestions = provider.getSuggestions(["st"], 0, 2, { signal });
|
|
845
|
+
const suggestions = await provider.getSuggestions(["st"], 0, 2, { signal });
|
|
769
846
|
|
|
770
|
-
assert.equal(suggestions,
|
|
771
|
-
assert.equal(suggestions instanceof Promise, false);
|
|
847
|
+
assert.equal(suggestions, result);
|
|
772
848
|
});
|
|
773
849
|
|
|
774
850
|
test("one-off bash autocomplete provider stays inactive before the bang command starts", async () => {
|
|
@@ -876,6 +952,102 @@ test("bash editor shell history state does not clobber the base prompt history i
|
|
|
876
952
|
}
|
|
877
953
|
});
|
|
878
954
|
|
|
955
|
+
test("bash editor recalls prompt history from single-line end without losing the live draft", async () => {
|
|
956
|
+
const links = ensureEditorModuleLinks();
|
|
957
|
+
|
|
958
|
+
try {
|
|
959
|
+
const { BashModeEditor } = await import("../bash-mode/editor.ts");
|
|
960
|
+
const { KeybindingsManager } = await import(new URL("../node_modules/@selesai/code/dist/core/keybindings.js", import.meta.url).href);
|
|
961
|
+
const keybindings = KeybindingsManager.create();
|
|
962
|
+
const createEditor = () => new BashModeEditor(
|
|
963
|
+
{ requestRender() {}, terminal: { columns: 80, rows: 24 } },
|
|
964
|
+
{},
|
|
965
|
+
keybindings,
|
|
966
|
+
{
|
|
967
|
+
keybindings,
|
|
968
|
+
isBashModeActive: () => false,
|
|
969
|
+
isShellRunning: () => false,
|
|
970
|
+
onExitBashMode() {},
|
|
971
|
+
onSubmitCommand() {},
|
|
972
|
+
onInterrupt() {},
|
|
973
|
+
onNotify() {},
|
|
974
|
+
getHistoryEntries: () => [],
|
|
975
|
+
resolveGhostSuggestion: async () => null,
|
|
976
|
+
},
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
const editor = createEditor();
|
|
980
|
+
editor.addToHistory("older prompt");
|
|
981
|
+
editor.addToHistory("previous prompt");
|
|
982
|
+
editor.setText("draft");
|
|
983
|
+
|
|
984
|
+
editor.handleInput("\x1b[A");
|
|
985
|
+
assert.equal(editor.getText(), "previous prompt");
|
|
986
|
+
|
|
987
|
+
editor.handleInput("\x1b[A");
|
|
988
|
+
assert.equal(editor.getText(), "older prompt");
|
|
989
|
+
|
|
990
|
+
editor.handleInput("\x1b[B");
|
|
991
|
+
assert.equal(editor.getText(), "previous prompt");
|
|
992
|
+
|
|
993
|
+
editor.handleInput("\x1b[B");
|
|
994
|
+
assert.equal(editor.getText(), "draft");
|
|
995
|
+
|
|
996
|
+
const midLineEditor = createEditor();
|
|
997
|
+
midLineEditor.addToHistory("previous prompt");
|
|
998
|
+
midLineEditor.setText("draft");
|
|
999
|
+
midLineEditor.handleInput("\x1b[D");
|
|
1000
|
+
midLineEditor.handleInput("\x1b[A");
|
|
1001
|
+
|
|
1002
|
+
assert.equal(midLineEditor.getText(), "draft");
|
|
1003
|
+
|
|
1004
|
+
const multilineEditor = createEditor();
|
|
1005
|
+
multilineEditor.addToHistory("previous prompt");
|
|
1006
|
+
multilineEditor.setText("first line\nsecond line");
|
|
1007
|
+
multilineEditor.handleInput("\x1b[A");
|
|
1008
|
+
|
|
1009
|
+
assert.equal(multilineEditor.getText(), "first line\nsecond line");
|
|
1010
|
+
assert.equal(Reflect.get(multilineEditor, "historyIndex"), -1);
|
|
1011
|
+
|
|
1012
|
+
const firstLineEditor = createEditor();
|
|
1013
|
+
firstLineEditor.addToHistory("previous prompt");
|
|
1014
|
+
firstLineEditor.setText("first line\nsecond line");
|
|
1015
|
+
Reflect.set(Reflect.get(firstLineEditor, "state"), "cursorLine", 0);
|
|
1016
|
+
Reflect.set(Reflect.get(firstLineEditor, "state"), "cursorCol", 0);
|
|
1017
|
+
firstLineEditor.handleInput("\x1b[A");
|
|
1018
|
+
|
|
1019
|
+
assert.equal(firstLineEditor.getText(), "previous prompt");
|
|
1020
|
+
|
|
1021
|
+
const customKeybindings = new KeybindingsManager({
|
|
1022
|
+
"tui.editor.cursorUp": ["up", "alt+k"],
|
|
1023
|
+
});
|
|
1024
|
+
const customBindingEditor = new BashModeEditor(
|
|
1025
|
+
{ requestRender() {}, terminal: { columns: 80, rows: 24 } },
|
|
1026
|
+
{},
|
|
1027
|
+
customKeybindings,
|
|
1028
|
+
{
|
|
1029
|
+
keybindings: customKeybindings,
|
|
1030
|
+
isBashModeActive: () => false,
|
|
1031
|
+
isShellRunning: () => false,
|
|
1032
|
+
onExitBashMode() {},
|
|
1033
|
+
onSubmitCommand() {},
|
|
1034
|
+
onInterrupt() {},
|
|
1035
|
+
onNotify() {},
|
|
1036
|
+
getHistoryEntries: () => [],
|
|
1037
|
+
resolveGhostSuggestion: async () => null,
|
|
1038
|
+
},
|
|
1039
|
+
);
|
|
1040
|
+
customBindingEditor.addToHistory("previous prompt");
|
|
1041
|
+
customBindingEditor.setText("draft");
|
|
1042
|
+
customBindingEditor.handleInput("\x1bk");
|
|
1043
|
+
|
|
1044
|
+
assert.equal(customBindingEditor.getText(), "draft");
|
|
1045
|
+
assert.equal(Reflect.get(customBindingEditor, "historyIndex"), -1);
|
|
1046
|
+
} finally {
|
|
1047
|
+
links.cleanup();
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
|
|
879
1051
|
test("bash editor escape exits bash mode", async () => {
|
|
880
1052
|
const links = ensureEditorModuleLinks();
|
|
881
1053
|
|