@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,195 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { PowerlineQueueStore, currentQueueContext, formatIdeaIssuePrompt, formatQueueDeliveryText, parseCompactQueuedPrompt, parseSigilIdeaCapture, parseTargetPrefix, targetForIdea } from "../queue/store.ts";
|
|
7
|
+
|
|
8
|
+
function withStore(fn: (store: PowerlineQueueStore, dir: string) => void): void {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), "powerline-queue-"));
|
|
10
|
+
try {
|
|
11
|
+
fn(new PowerlineQueueStore(join(dir, "inbox.jsonl"), join(dir, "projects.json")), dir);
|
|
12
|
+
} finally {
|
|
13
|
+
rmSync(dir, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("queue store adds active project ideas and summarizes them", () => withStore((store) => {
|
|
18
|
+
const cwd = "/tmp/project-a";
|
|
19
|
+
store.add({
|
|
20
|
+
text: "remember deploy note",
|
|
21
|
+
source: { cwd, sessionId: "s1" },
|
|
22
|
+
target: { kind: "project", cwd },
|
|
23
|
+
intent: "idea",
|
|
24
|
+
now: 100,
|
|
25
|
+
});
|
|
26
|
+
store.add({
|
|
27
|
+
text: "run after compact",
|
|
28
|
+
source: { cwd, sessionId: "s1" },
|
|
29
|
+
target: { kind: "current-session" },
|
|
30
|
+
intent: "post-compact",
|
|
31
|
+
now: 101,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.equal(store.list().length, 2);
|
|
35
|
+
assert.deepEqual(store.summarize(currentQueueContext(cwd, "s1"), true), {
|
|
36
|
+
queueCount: 1,
|
|
37
|
+
ideaCount: 1,
|
|
38
|
+
blockedCount: 0,
|
|
39
|
+
compacting: true,
|
|
40
|
+
leadingText: "run after compact",
|
|
41
|
+
leadingIntent: "post-compact",
|
|
42
|
+
leadingStatus: "queued",
|
|
43
|
+
});
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
test("queue store filters inactive project items", () => withStore((store) => {
|
|
47
|
+
store.add({
|
|
48
|
+
text: "other project",
|
|
49
|
+
source: { cwd: "/tmp/project-a" },
|
|
50
|
+
target: { kind: "project", cwd: "/tmp/project-a" },
|
|
51
|
+
intent: "idea",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
assert.equal(store.activeItems(currentQueueContext("/tmp/project-b")).length, 0);
|
|
55
|
+
assert.equal(store.activeItems(currentQueueContext("/tmp/project-a")).length, 1);
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
test("queue store exposes the leading item intent for idea-only summaries", () => withStore((store) => {
|
|
59
|
+
store.add({
|
|
60
|
+
text: "saved follow-up idea",
|
|
61
|
+
source: { cwd: "/tmp/project" },
|
|
62
|
+
target: { kind: "project", cwd: "/tmp/project" },
|
|
63
|
+
intent: "idea",
|
|
64
|
+
now: 100,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
assert.deepEqual(store.summarize(currentQueueContext("/tmp/project"), false), {
|
|
68
|
+
queueCount: 0,
|
|
69
|
+
ideaCount: 1,
|
|
70
|
+
blockedCount: 0,
|
|
71
|
+
compacting: false,
|
|
72
|
+
leadingText: "saved follow-up idea",
|
|
73
|
+
leadingIntent: "idea",
|
|
74
|
+
leadingStatus: "queued",
|
|
75
|
+
});
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
test("current-session targets stay scoped to the source session when known", () => withStore((store) => {
|
|
79
|
+
store.add({
|
|
80
|
+
text: "session only",
|
|
81
|
+
source: { cwd: "/tmp/project", sessionId: "s1" },
|
|
82
|
+
target: { kind: "current-session" },
|
|
83
|
+
intent: "post-compact",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
assert.equal(store.activeItems(currentQueueContext("/tmp/project", "s1")).length, 1);
|
|
87
|
+
assert.equal(store.activeItems(currentQueueContext("/tmp/project", "s2")).length, 0);
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
test("queue store aliases resolve idea targets", () => withStore((store) => {
|
|
91
|
+
store.setAlias("pika", "/tmp/pika");
|
|
92
|
+
|
|
93
|
+
assert.deepEqual(targetForIdea("pika", store, "/tmp/current"), {
|
|
94
|
+
kind: "project",
|
|
95
|
+
cwd: resolve("/tmp/pika"),
|
|
96
|
+
alias: "pika",
|
|
97
|
+
});
|
|
98
|
+
assert.deepEqual(targetForIdea("global", store, "/tmp/current"), { kind: "global" });
|
|
99
|
+
assert.deepEqual(targetForIdea("current", store, "/tmp/current"), { kind: "current-session" });
|
|
100
|
+
assert.throws(() => targetForIdea("missing", store, "/tmp/current"), /Unknown project alias/);
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
test("parseTargetPrefix separates optional @target", () => {
|
|
104
|
+
assert.deepEqual(parseTargetPrefix("@pika check logs"), { target: "pika", text: "check logs" });
|
|
105
|
+
assert.deepEqual(parseTargetPrefix("plain idea"), { target: null, text: "plain idea" });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("parseSigilIdeaCapture turns leading sigil text into target-aware ideas", () => {
|
|
109
|
+
assert.deepEqual(parseSigilIdeaCapture("# check logs", "#"), { target: null, text: "check logs" });
|
|
110
|
+
assert.deepEqual(parseSigilIdeaCapture("# @global check logs", "#"), { target: "global", text: "check logs" });
|
|
111
|
+
assert.deepEqual(parseSigilIdeaCapture("# @pika check logs\nthen inspect events", "#"), {
|
|
112
|
+
target: "pika",
|
|
113
|
+
text: "check logs\nthen inspect events",
|
|
114
|
+
});
|
|
115
|
+
assert.deepEqual(parseSigilIdeaCapture("note # check logs", "#"), null);
|
|
116
|
+
assert.deepEqual(parseSigilIdeaCapture("## markdown heading", "#"), null);
|
|
117
|
+
assert.deepEqual(parseSigilIdeaCapture("# ", "#"), null);
|
|
118
|
+
assert.deepEqual(parseSigilIdeaCapture("# check logs", false), null);
|
|
119
|
+
assert.deepEqual(parseSigilIdeaCapture("// check logs", "//"), { target: null, text: "check logs" });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("formatQueueDeliveryText adds provenance only for ideas", () => {
|
|
123
|
+
const idea = {
|
|
124
|
+
id: "a1b2c3d4",
|
|
125
|
+
text: "check logs",
|
|
126
|
+
createdAt: 1000,
|
|
127
|
+
updatedAt: 1000,
|
|
128
|
+
source: { cwd: "/tmp/project" },
|
|
129
|
+
target: { kind: "current-session" as const },
|
|
130
|
+
intent: "idea" as const,
|
|
131
|
+
status: "queued" as const,
|
|
132
|
+
};
|
|
133
|
+
const prompt = { ...idea, intent: "follow-up" as const };
|
|
134
|
+
|
|
135
|
+
assert.equal(
|
|
136
|
+
formatQueueDeliveryText(idea),
|
|
137
|
+
"[powerline idea a1b2c3d4, captured 1970-01-01T00:00:01.000Z from /tmp/project]\ncheck logs",
|
|
138
|
+
);
|
|
139
|
+
assert.equal(formatQueueDeliveryText(prompt), "check logs");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("formatIdeaIssuePrompt requires dedupe and clear owned repo before filing", () => {
|
|
143
|
+
const prompt = formatIdeaIssuePrompt({
|
|
144
|
+
id: "a1b2c3d4",
|
|
145
|
+
text: "add typed issue handoff",
|
|
146
|
+
createdAt: 1000,
|
|
147
|
+
updatedAt: 1000,
|
|
148
|
+
source: { cwd: "/tmp/project" },
|
|
149
|
+
target: { kind: "project", cwd: "/tmp/project", alias: "powerline" },
|
|
150
|
+
intent: "idea",
|
|
151
|
+
status: "queued",
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
assert.match(prompt, /spawn one low-budget issue-filing lane/);
|
|
155
|
+
assert.match(prompt, /target repository is unclear or is not owned\/controlled by the user, ask before filing/);
|
|
156
|
+
assert.match(prompt, /dedupe against existing open issues first/);
|
|
157
|
+
assert.match(prompt, /If a matching open issue already exists, report it and do not create another issue/);
|
|
158
|
+
assert.match(prompt, /create one self-contained GitHub issue/);
|
|
159
|
+
assert.match(prompt, /project @powerline \/tmp\/project/);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("parseCompactQueuedPrompt treats /compact suffix as queued prompt text", () => {
|
|
163
|
+
assert.equal(parseCompactQueuedPrompt("/compact great lets proceed"), "great lets proceed");
|
|
164
|
+
assert.equal(parseCompactQueuedPrompt(" /compact great lets proceed "), "great lets proceed");
|
|
165
|
+
assert.equal(parseCompactQueuedPrompt("/compact\tgreat lets proceed"), "great lets proceed");
|
|
166
|
+
assert.equal(parseCompactQueuedPrompt("/compact"), null);
|
|
167
|
+
assert.equal(parseCompactQueuedPrompt("/compact "), null);
|
|
168
|
+
assert.equal(parseCompactQueuedPrompt("/compactness great lets proceed"), null);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("queue store clears items from active summary", () => withStore((store) => {
|
|
172
|
+
const item = store.add({
|
|
173
|
+
text: "queued prompt",
|
|
174
|
+
source: { cwd: "/tmp/project" },
|
|
175
|
+
target: { kind: "current-session" },
|
|
176
|
+
intent: "post-compact",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
assert.equal(store.summarize(currentQueueContext("/tmp/project"), false).queueCount, 1);
|
|
180
|
+
store.clear(item.id);
|
|
181
|
+
assert.equal(store.summarize(currentQueueContext("/tmp/project"), false).queueCount, 0);
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
test("queue store times out instead of stealing an existing lock", () => withStore((store, dir) => {
|
|
185
|
+
const lockPath = join(dir, "inbox.jsonl.lock");
|
|
186
|
+
mkdirSync(lockPath);
|
|
187
|
+
|
|
188
|
+
assert.throws(() => store.add({
|
|
189
|
+
text: "blocked write",
|
|
190
|
+
source: { cwd: "/tmp/project" },
|
|
191
|
+
target: { kind: "project", cwd: "/tmp/project" },
|
|
192
|
+
intent: "idea",
|
|
193
|
+
}), /Timed out waiting for Powerline queue store lock/);
|
|
194
|
+
assert.equal(existsSync(lockPath), true);
|
|
195
|
+
}));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { shouldShowStartupWelcome, isStaleExtensionContextError } from "../lifecycle.ts";
|
|
4
|
+
|
|
5
|
+
test("startup welcome only shows on startup when enabled", () => {
|
|
6
|
+
assert.equal(shouldShowStartupWelcome("startup", true), true);
|
|
7
|
+
assert.equal(shouldShowStartupWelcome("reload", true), false);
|
|
8
|
+
assert.equal(shouldShowStartupWelcome("startup", false), false);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("stale extension context errors are recognized", () => {
|
|
12
|
+
assert.equal(isStaleExtensionContextError(new Error("This extension instance is stale")), true);
|
|
13
|
+
assert.equal(isStaleExtensionContextError(new Error("other")), false);
|
|
14
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { NERD_ICONS } from "../icons.ts";
|
|
5
|
+
import { isStaleExtensionContextError, shouldShowStartupWelcome } from "../lifecycle.ts";
|
|
6
|
+
import { __resetCurrencyRatesForTest, __setCurrencyRatesForTest } from "../currency-rates.ts";
|
|
7
|
+
import { renderSegment } from "../segments.ts";
|
|
8
|
+
import type { SegmentContext } from "../types.ts";
|
|
9
|
+
|
|
10
|
+
const source = readFileSync(new URL("../index.ts", import.meta.url), "utf-8");
|
|
11
|
+
const originalNerdFonts = process.env.POWERLINE_NERD_FONTS;
|
|
12
|
+
process.env.POWERLINE_NERD_FONTS = "0";
|
|
13
|
+
|
|
14
|
+
test.after(() => {
|
|
15
|
+
if (originalNerdFonts === undefined) {
|
|
16
|
+
delete process.env.POWERLINE_NERD_FONTS;
|
|
17
|
+
} else {
|
|
18
|
+
process.env.POWERLINE_NERD_FONTS = originalNerdFonts;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function plainThemeText(_color: string, text: string): string {
|
|
23
|
+
return text;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stripAnsi(text: string): string {
|
|
27
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createSegmentContext(overrides: Partial<SegmentContext> = {}): SegmentContext {
|
|
31
|
+
return {
|
|
32
|
+
model: { id: "claude-sonnet-4", name: "Claude Sonnet 4" },
|
|
33
|
+
thinkingLevel: "off",
|
|
34
|
+
sessionId: undefined,
|
|
35
|
+
cwd: "/tmp/project",
|
|
36
|
+
usageStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, subagentCost: 0 },
|
|
37
|
+
contextTokens: 0,
|
|
38
|
+
contextPercent: 0,
|
|
39
|
+
contextWindow: 0,
|
|
40
|
+
contextApproximate: false,
|
|
41
|
+
autoCompactEnabled: true,
|
|
42
|
+
customCompactionEnabled: false,
|
|
43
|
+
usingSubscription: false,
|
|
44
|
+
sessionStartTime: Date.now(),
|
|
45
|
+
shellModeActive: false,
|
|
46
|
+
shellRunning: false,
|
|
47
|
+
shellName: null,
|
|
48
|
+
shellCwd: null,
|
|
49
|
+
git: { branch: null, staged: 0, unstaged: 0, untracked: 0 },
|
|
50
|
+
extensionStatuses: new Map(),
|
|
51
|
+
hiddenExtensionStatusKeys: new Set(),
|
|
52
|
+
customItemsById: new Map(),
|
|
53
|
+
options: {},
|
|
54
|
+
theme: { fg: plainThemeText },
|
|
55
|
+
colors: {},
|
|
56
|
+
...overrides,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("model segment can show provider-qualified ids", () => {
|
|
61
|
+
const normal = renderSegment("model", createSegmentContext());
|
|
62
|
+
const qualified = renderSegment("model", createSegmentContext({
|
|
63
|
+
model: { id: "claude-sonnet-4", name: "Claude Sonnet 4", provider: "anthropic" },
|
|
64
|
+
options: { model: { display: "qualified" } },
|
|
65
|
+
}));
|
|
66
|
+
const alreadyQualified = renderSegment("model", createSegmentContext({
|
|
67
|
+
model: { id: "openai/gpt-4.1", name: "GPT 4.1", provider: "openai" },
|
|
68
|
+
options: { model: { display: "qualified" } },
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
assert.equal(stripAnsi(normal.content), "Sonnet 4");
|
|
72
|
+
assert.equal(stripAnsi(qualified.content), "anthropic/claude-sonnet-4");
|
|
73
|
+
assert.equal(stripAnsi(alreadyQualified.content), "openai/gpt-4.1");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("cost segment supports subscription display modes and converted currencies", () => {
|
|
77
|
+
__setCurrencyRatesForTest({ CNY: 7.2 });
|
|
78
|
+
|
|
79
|
+
const subscription = renderSegment("cost", createSegmentContext({
|
|
80
|
+
usingSubscription: true,
|
|
81
|
+
usageStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0.42, subagentCost: 0 },
|
|
82
|
+
}));
|
|
83
|
+
const reportedCost = renderSegment("cost", createSegmentContext({
|
|
84
|
+
usingSubscription: true,
|
|
85
|
+
usageStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0.42, subagentCost: 0 },
|
|
86
|
+
options: { cost: { subscriptionDisplay: "reported-cost" } },
|
|
87
|
+
}));
|
|
88
|
+
const both = renderSegment("cost", createSegmentContext({
|
|
89
|
+
usingSubscription: true,
|
|
90
|
+
usageStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0.42, subagentCost: 0 },
|
|
91
|
+
options: { cost: { subscriptionDisplay: "both" } },
|
|
92
|
+
}));
|
|
93
|
+
const zeroReported = renderSegment("cost", createSegmentContext({
|
|
94
|
+
usingSubscription: true,
|
|
95
|
+
options: { cost: { subscriptionDisplay: "reported-cost" } },
|
|
96
|
+
}));
|
|
97
|
+
const zeroBoth = renderSegment("cost", createSegmentContext({
|
|
98
|
+
usingSubscription: true,
|
|
99
|
+
options: { cost: { subscriptionDisplay: "both" } },
|
|
100
|
+
}));
|
|
101
|
+
const withSubagentCost = renderSegment("cost", createSegmentContext({
|
|
102
|
+
usageStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0.42, subagentCost: 0.58 },
|
|
103
|
+
}));
|
|
104
|
+
const convertedCurrency = renderSegment("cost", createSegmentContext({
|
|
105
|
+
usageStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 1, subagentCost: 0.25 },
|
|
106
|
+
options: { cost: { currency: "CNY" } },
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
__resetCurrencyRatesForTest();
|
|
110
|
+
|
|
111
|
+
assert.deepEqual(subscription, { content: "(sub)", visible: true });
|
|
112
|
+
assert.deepEqual(reportedCost, { content: "$0.42", visible: true });
|
|
113
|
+
assert.deepEqual(both, { content: "$0.42 (sub)", visible: true });
|
|
114
|
+
assert.deepEqual(zeroReported, { content: "(sub)", visible: true });
|
|
115
|
+
assert.deepEqual(zeroBoth, { content: "(sub)", visible: true });
|
|
116
|
+
assert.deepEqual(withSubagentCost, { content: "$1.00", visible: true });
|
|
117
|
+
assert.deepEqual(convertedCurrency, { content: "¥9.00", visible: true });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("context segment shows used tokens, maximum, and percentage", () => {
|
|
121
|
+
const context = renderSegment("context_pct", createSegmentContext({
|
|
122
|
+
contextTokens: 4_500,
|
|
123
|
+
contextPercent: 1.7,
|
|
124
|
+
contextWindow: 272_000,
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
assert.equal(stripAnsi(context.content), "4.5k/272k (1.7%)");
|
|
128
|
+
assert.equal(context.visible, true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("Nerd Font context icon uses stable database glyph", () => {
|
|
132
|
+
assert.equal(NERD_ICONS.context, "\uF1C0");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("startup welcome predicate respects powerline.welcome false", () => {
|
|
136
|
+
assert.equal(shouldShowStartupWelcome("startup", true), true);
|
|
137
|
+
assert.equal(shouldShowStartupWelcome("startup", false), false);
|
|
138
|
+
assert.equal(shouldShowStartupWelcome("resume", true), false);
|
|
139
|
+
// Guide display modes take precedence; the welcome overlay only shows when the
|
|
140
|
+
// guide is off and shouldShowStartupWelcome passes.
|
|
141
|
+
assert.match(source, /setupCustomEditor\(ctx\);/);
|
|
142
|
+
assert.match(source, /if \(guideDisplayMode === "full"\)/);
|
|
143
|
+
assert.match(source, /else if \(shouldShowStartupWelcome\(event\.reason, config\.welcome\)\)/);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("stale ctx guard handles old and new Pi messages on agent_end", () => {
|
|
147
|
+
assert.equal(isStaleExtensionContextError(new Error("This extension instance is stale after session replacement or reload.")), true);
|
|
148
|
+
assert.equal(isStaleExtensionContextError(new Error("This extension ctx is stale after session replacement or reload.")), true);
|
|
149
|
+
assert.equal(isStaleExtensionContextError(new Error("ctx.hasUI failed for another reason")), false);
|
|
150
|
+
assert.match(source, /let hasUI = false;\r?\n\s+try \{\r?\n\s+hasUI = Boolean\(ctx\.hasUI\);/);
|
|
151
|
+
assert.match(source, /if \(!isStaleExtensionContextError\(error\)\) throw error;\r?\n\s+currentCtx = null;\r?\n\s+return;/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("post-compaction queue delivery does not read ctx.cwd from the delayed callback", () => {
|
|
155
|
+
assert.match(source, /const queueContext = getQueueContext\(ctx\);\r?\n\s+const scheduledGeneration = sessionGeneration;\r?\n\s+queueDeliveryTimer = setTimeout/);
|
|
156
|
+
assert.match(source, /if \(scheduledGeneration !== sessionGeneration\) return;\r?\n\s+try \{\r?\n\s+const item = queueStore\.queuedDeliveryItems\(queueContext, "post-compact"\)\[0\];/);
|
|
157
|
+
assert.match(source, /catch \(error\) \{\r?\n\s+if \(!isStaleExtensionContextError\(error\)\) throw error;\r?\n\s+currentCtx = null;/);
|
|
158
|
+
assert.match(source, /if \(isStaleExtensionContextError\(error\)\) \{\r?\n\s+if \(!sent\) queueStore\.update\(item\.id, \{ status: "queued", error: undefined \}\);/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("unknown context estimates are event-scoped and cleared before compaction", () => {
|
|
162
|
+
assert.match(source, /approximateContextUsage = event\.reason === "reload" \? estimateUnknownContextUsage\(ctx\) : null;/);
|
|
163
|
+
assert.match(source, /unknownCoreFallback: approximateContextUsage,/);
|
|
164
|
+
assert.match(source, /contextApproximate = coreContextUsage\?\.contextTokens === null && approximateContextUsage !== null;/);
|
|
165
|
+
assert.match(source, /pi\.on\("session_before_compact", async \(_event, ctx\) => \{\r?\n\s+powerlineCompacting = true;\r?\n\s+currentCtx = ctx;\r?\n\s+isStreaming = false;\r?\n\s+liveAssistantUsage = null;\r?\n\s+approximateContextUsage = null;\r?\n\s+coreContextUsageCache\.reset\(\);/);
|
|
166
|
+
assert.match(source, /pi\.on\("session_compact", async \(event, ctx\) => \{\r?\n\s+powerlineCompacting = false;\r?\n\s+currentCtx = ctx;\r?\n\s+isStreaming = false;\r?\n\s+liveAssistantUsage = null;\r?\n\s+approximateContextUsage = estimateUnknownContextUsage\(ctx\);\r?\n\s+coreContextUsageCache\.reset\(\);/);
|
|
167
|
+
});
|
|
@@ -1,32 +1,37 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
|
+
import { matchesStashShortcutInput } from "../shortcuts.ts";
|
|
4
5
|
|
|
5
6
|
const source = readFileSync(new URL("../index.ts", import.meta.url), "utf-8");
|
|
6
7
|
|
|
7
|
-
test("stash shortcut
|
|
8
|
-
assert.
|
|
9
|
-
assert.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
test("stash shortcut matches Alt+S encodings without consuming literal sharp-S by default", () => {
|
|
9
|
+
assert.equal(matchesStashShortcutInput("ß"), false);
|
|
10
|
+
assert.equal(matchesStashShortcutInput("ß", { includePrintableSharpS: true }), true);
|
|
11
|
+
|
|
12
|
+
for (const data of [
|
|
13
|
+
"\x1bs",
|
|
14
|
+
"\x1bS",
|
|
15
|
+
"\x1b[115;3u",
|
|
16
|
+
"\x1b[83;3u",
|
|
17
|
+
"\x1b[27;3;115~",
|
|
18
|
+
"\x1b[27;3;83~",
|
|
19
|
+
]) {
|
|
20
|
+
assert.equal(matchesStashShortcutInput(data), true, data);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
assert.equal(matchesStashShortcutInput("s"), false);
|
|
24
|
+
assert.equal(matchesStashShortcutInput("\x1b[115;5u"), false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("stash shortcut stays in terminal/editor fallback routing", () => {
|
|
28
|
+
assert.doesNotMatch(source, /pi\.registerShortcut\("alt\+s"/);
|
|
29
|
+
assert.match(source, /matchesStashShortcutInput\(data, \{ includePrintableSharpS: config\.stashSharpSShortcut \}\)/);
|
|
17
30
|
assert.match(source, /ctx\.ui\.onTerminalInput\(\(data: string\) =>/);
|
|
18
31
|
assert.match(source, /if \(isStashShortcutInput\(data\)\)/);
|
|
19
|
-
assert.match(source, /return \{ consume: true \};/);
|
|
20
32
|
assert.match(source, /function stashOrRestoreEditorText\(ctx: any\): void/);
|
|
21
|
-
assert.match(source, /if \(isStashShortcutInput\(data\)\)/);
|
|
22
|
-
assert.match(source, /stashOrRestoreEditorText\(ctx\);/);
|
|
23
33
|
assert.match(source, /function isPromptHistoryShortcutInput\(data: string\): boolean/);
|
|
24
|
-
assert.match(source, /if \(isKeyRelease\(data\)\) return null;/);
|
|
25
34
|
assert.match(source, /matchesConfiguredShortcut\(data, resolvedShortcuts\.stashHistory\)/);
|
|
26
35
|
assert.doesNotMatch(source, /data === "\\x1b\\b"/);
|
|
27
36
|
assert.doesNotMatch(source, /data === "\\x1b\\x7f"/);
|
|
28
|
-
assert.match(source, /104\(\?:/);
|
|
29
|
-
assert.match(source, /27;7;104/);
|
|
30
|
-
assert.match(source, /return \{ kind: "stashHistory" \};/);
|
|
31
|
-
assert.match(source, /void openStashHistory\(ctx\);/);
|
|
32
37
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { renderSegment } from "../segments.ts";
|
|
4
|
+
import { rainbow } from "../theme.ts";
|
|
4
5
|
import type { ColorScheme, SegmentContext, ThemeLike } from "../types.ts";
|
|
5
6
|
|
|
6
7
|
function hexAnsi(hex: `#${string}`): string {
|
|
@@ -16,9 +17,11 @@ function createSegmentContext(thinkingLevel: string, colors: ColorScheme): Segme
|
|
|
16
17
|
model: undefined,
|
|
17
18
|
thinkingLevel,
|
|
18
19
|
sessionId: undefined,
|
|
19
|
-
usageStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 },
|
|
20
|
+
usageStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, subagentCost: 0 },
|
|
21
|
+
contextTokens: 0,
|
|
20
22
|
contextPercent: 0,
|
|
21
23
|
contextWindow: 0,
|
|
24
|
+
contextApproximate: false,
|
|
22
25
|
autoCompactEnabled: true,
|
|
23
26
|
customCompactionEnabled: false,
|
|
24
27
|
usingSubscription: false,
|
|
@@ -59,3 +62,15 @@ test("thinking segment uses per-level colors for off through medium", () => {
|
|
|
59
62
|
assert.equal(low.content, `${hexAnsi("#333333")}think:low\x1b[0m`);
|
|
60
63
|
assert.equal(medium.content, `${hexAnsi("#444444")}think:med\x1b[0m`);
|
|
61
64
|
});
|
|
65
|
+
|
|
66
|
+
test("thinking segment uses rainbow styling for high through max", () => {
|
|
67
|
+
const colors: ColorScheme = { thinking: "#111111" };
|
|
68
|
+
|
|
69
|
+
for (const level of ["high", "xhigh", "max"]) {
|
|
70
|
+
const rendered = renderSegment("thinking", createSegmentContext(level, colors));
|
|
71
|
+
assert.deepEqual(rendered, {
|
|
72
|
+
content: rainbow(`think:${level}`),
|
|
73
|
+
visible: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|