@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,144 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync, mkdtempSync, readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { SessionManager } from "@selesai/code";
|
|
7
|
+
import { getCdArgumentCompletions, resolveCdTarget, runCdCommand } from "../cd-command.ts";
|
|
8
|
+
|
|
9
|
+
const cdCommandSource = readFileSync(new URL("../cd-command.ts", import.meta.url), "utf-8");
|
|
10
|
+
const indexSource = readFileSync(new URL("../index.ts", import.meta.url), "utf-8");
|
|
11
|
+
|
|
12
|
+
test("resolveCdTarget supports current, relative, home, and invalid directories", () => {
|
|
13
|
+
const root = mkdtempSync(join(tmpdir(), "powerline-cd-root-"));
|
|
14
|
+
const home = mkdtempSync(join(tmpdir(), "powerline-cd-home-"));
|
|
15
|
+
const child = join(root, "child");
|
|
16
|
+
const homeProject = join(home, "project");
|
|
17
|
+
mkdirSync(child);
|
|
18
|
+
mkdirSync(homeProject);
|
|
19
|
+
writeFileSync(join(root, "file.txt"), "not a directory");
|
|
20
|
+
|
|
21
|
+
assert.equal(resolveCdTarget("", root, home), null);
|
|
22
|
+
assert.deepEqual(resolveCdTarget("child", root, home), { path: child });
|
|
23
|
+
assert.deepEqual(resolveCdTarget("~/project", root, home), { path: homeProject });
|
|
24
|
+
assert.deepEqual(resolveCdTarget("My\\ Folder", root, home), { error: `Directory does not exist: ${join(root, "My Folder")}` });
|
|
25
|
+
assert.match(resolveCdTarget("missing", root, home)?.error ?? "", /Directory does not exist:/);
|
|
26
|
+
assert.match(resolveCdTarget("file.txt", root, home)?.error ?? "", /Not a directory:/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("cd argument completions suggest directories and quick picks", () => {
|
|
30
|
+
const root = mkdtempSync(join(tmpdir(), "powerline-cd-complete-root-"));
|
|
31
|
+
const home = mkdtempSync(join(tmpdir(), "powerline-cd-complete-home-"));
|
|
32
|
+
mkdirSync(join(root, "child"));
|
|
33
|
+
mkdirSync(join(root, "My Folder"));
|
|
34
|
+
writeFileSync(join(root, "file.txt"), "not a directory");
|
|
35
|
+
mkdirSync(join(home, "project"));
|
|
36
|
+
|
|
37
|
+
const rootCompletions = getCdArgumentCompletions("", root, home);
|
|
38
|
+
assert.ok(rootCompletions.some((item) => item.value === "../" && item.label === "../"));
|
|
39
|
+
assert.ok(rootCompletions.some((item) => item.value === "~/" && item.label === "~/"));
|
|
40
|
+
assert.ok(rootCompletions.some((item) => item.value === "child/" && item.label === "child/"));
|
|
41
|
+
assert.ok(rootCompletions.some((item) => item.value === "My\\ Folder/" && item.label === "My Folder/"));
|
|
42
|
+
assert.ok(!rootCompletions.some((item) => item.label === "file.txt"));
|
|
43
|
+
|
|
44
|
+
if (process.platform !== "win32") {
|
|
45
|
+
assert.deepEqual(
|
|
46
|
+
getCdArgumentCompletions("~/p", root, home).map((item) => item.value),
|
|
47
|
+
["~/project/"],
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("cd command cleans up the forked session when switching is cancelled", async () => {
|
|
53
|
+
const originalCwd = process.cwd();
|
|
54
|
+
const root = mkdtempSync(join(tmpdir(), "powerline-cd-command-root-"));
|
|
55
|
+
const target = join(root, "target");
|
|
56
|
+
const sessionDir = join(root, "sessions");
|
|
57
|
+
mkdirSync(target);
|
|
58
|
+
|
|
59
|
+
const session = SessionManager.create(root, sessionDir);
|
|
60
|
+
session.appendMessage({ role: "user", content: "hello", timestamp: Date.now() });
|
|
61
|
+
session.appendMessage({ role: "assistant", content: "hi", timestamp: Date.now(), usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } });
|
|
62
|
+
|
|
63
|
+
let switchedPath: string | undefined;
|
|
64
|
+
const notifications: Array<{ message: string; type?: string }> = [];
|
|
65
|
+
const ctx = {
|
|
66
|
+
cwd: root,
|
|
67
|
+
sessionManager: {
|
|
68
|
+
getSessionFile: () => session.getSessionFile(),
|
|
69
|
+
},
|
|
70
|
+
ui: {
|
|
71
|
+
notify(message: string, type?: string) {
|
|
72
|
+
notifications.push({ message, type });
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
waitForIdle: async () => {},
|
|
76
|
+
switchSession: async (path: string) => {
|
|
77
|
+
switchedPath = path;
|
|
78
|
+
return { cancelled: true };
|
|
79
|
+
},
|
|
80
|
+
} as any;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await runCdCommand("target", ctx);
|
|
84
|
+
} finally {
|
|
85
|
+
process.chdir(originalCwd);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
assert.equal(process.cwd(), originalCwd);
|
|
89
|
+
assert.ok(switchedPath, "switchSession should receive the forked session path");
|
|
90
|
+
assert.equal(existsSync(switchedPath!), false);
|
|
91
|
+
assert.deepEqual(notifications.at(-1), { message: "Directory change cancelled", type: "warning" });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("cd command preserves the target session after a post-apply switch error", async () => {
|
|
95
|
+
const originalCwd = process.cwd();
|
|
96
|
+
const root = mkdtempSync(join(tmpdir(), "powerline-cd-post-apply-root-"));
|
|
97
|
+
const target = join(root, "target");
|
|
98
|
+
const sessionDir = join(root, "sessions");
|
|
99
|
+
mkdirSync(target);
|
|
100
|
+
|
|
101
|
+
const session = SessionManager.create(root, sessionDir);
|
|
102
|
+
session.appendMessage({ role: "user", content: "hello", timestamp: Date.now() });
|
|
103
|
+
session.appendMessage({ role: "assistant", content: "hi", timestamp: Date.now(), usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } });
|
|
104
|
+
|
|
105
|
+
let switchedPath: string | undefined;
|
|
106
|
+
const ctx = {
|
|
107
|
+
cwd: root,
|
|
108
|
+
sessionManager: {
|
|
109
|
+
getSessionFile: () => session.getSessionFile(),
|
|
110
|
+
},
|
|
111
|
+
ui: {
|
|
112
|
+
notify() {},
|
|
113
|
+
},
|
|
114
|
+
waitForIdle: async () => {},
|
|
115
|
+
switchSession: async (path: string, options: { withSession?: (ctx: any) => Promise<void> }) => {
|
|
116
|
+
switchedPath = path;
|
|
117
|
+
await options.withSession?.({
|
|
118
|
+
cwd: target,
|
|
119
|
+
ui: {
|
|
120
|
+
notify() {},
|
|
121
|
+
setTitle() {},
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
throw new Error("post-apply failure");
|
|
125
|
+
},
|
|
126
|
+
} as any;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await runCdCommand("target", ctx);
|
|
130
|
+
assert.equal(realpathSync(process.cwd()), realpathSync(target));
|
|
131
|
+
assert.ok(switchedPath, "switchSession should receive the forked session path");
|
|
132
|
+
assert.equal(existsSync(switchedPath!), true);
|
|
133
|
+
} finally {
|
|
134
|
+
process.chdir(originalCwd);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("cd command uses session replacement instead of private cwd mutation", () => {
|
|
139
|
+
assert.match(indexSource, /registerCdCommand\(pi, \(\) => currentCtx\?\.cwd \?\? process\.cwd\(\)\)/);
|
|
140
|
+
assert.match(cdCommandSource, /SessionManager\.forkFrom\(sessionFile, resolved\.path\)/);
|
|
141
|
+
assert.match(cdCommandSource, /ctx\.switchSession\(nextSessionFile!/);
|
|
142
|
+
assert.match(cdCommandSource, /process\.chdir\(nextCtx\.cwd\)/);
|
|
143
|
+
assert.doesNotMatch(cdCommandSource, /ctx\.cwd\s*=/);
|
|
144
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { readCoreContextUsage } from "../context-usage.ts";
|
|
3
|
+
import { CoreContextUsageCache, estimateInitialContextTokens, estimateUnknownContextUsage, readCoreContextUsage, resolveDisplayContextUsage } from "../context-usage.ts";
|
|
4
4
|
|
|
5
5
|
test("readCoreContextUsage returns Pi context estimates for branch summaries", () => {
|
|
6
6
|
const usage = readCoreContextUsage({
|
|
@@ -30,9 +30,127 @@ test("readCoreContextUsage computes percent when Pi returns only token totals",
|
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
test("readCoreContextUsage preserves Pi's post-compaction unknown state", () => {
|
|
34
|
+
const usage = readCoreContextUsage({
|
|
35
|
+
getContextUsage() {
|
|
36
|
+
return { tokens: null, contextWindow: 5000, percent: null };
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.deepEqual(usage, {
|
|
41
|
+
contextTokens: null,
|
|
42
|
+
contextWindow: 5000,
|
|
43
|
+
contextPercent: null,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
33
47
|
test("readCoreContextUsage ignores unknown or unusable estimates", () => {
|
|
34
48
|
assert.equal(readCoreContextUsage({}), null);
|
|
35
49
|
assert.equal(readCoreContextUsage({ getContextUsage: () => undefined }), null);
|
|
36
|
-
assert.equal(readCoreContextUsage({ getContextUsage: () => ({ tokens:
|
|
50
|
+
assert.equal(readCoreContextUsage({ getContextUsage: () => ({ tokens: undefined, contextWindow: 5000, percent: null }) }), null);
|
|
37
51
|
assert.equal(readCoreContextUsage({ getContextUsage: () => ({ tokens: 100, contextWindow: 0, percent: 0 }) }), null);
|
|
38
52
|
});
|
|
53
|
+
|
|
54
|
+
test("core context usage cache reuses a leaf and supports explicit invalidation", () => {
|
|
55
|
+
const cache = new CoreContextUsageCache();
|
|
56
|
+
let leafId = "leaf-1";
|
|
57
|
+
let tokens = 100;
|
|
58
|
+
let reads = 0;
|
|
59
|
+
const ctx = {
|
|
60
|
+
sessionManager: { getLeafId: () => leafId },
|
|
61
|
+
getContextUsage() {
|
|
62
|
+
reads += 1;
|
|
63
|
+
return { tokens, contextWindow: 1000, percent: tokens / 10 };
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
assert.equal(cache.get(ctx)?.contextTokens, 100);
|
|
68
|
+
tokens = 200;
|
|
69
|
+
assert.equal(cache.get(ctx)?.contextTokens, 100);
|
|
70
|
+
assert.equal(reads, 1);
|
|
71
|
+
|
|
72
|
+
cache.reset();
|
|
73
|
+
assert.equal(cache.get(ctx)?.contextTokens, 200);
|
|
74
|
+
leafId = "leaf-2";
|
|
75
|
+
tokens = 300;
|
|
76
|
+
assert.equal(cache.get(ctx)?.contextTokens, 300);
|
|
77
|
+
assert.equal(reads, 3);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("resolveDisplayContextUsage preserves unknown core usage over assistant fallback usage", () => {
|
|
81
|
+
assert.deepEqual(resolveDisplayContextUsage({
|
|
82
|
+
coreContextUsage: { contextTokens: null, contextWindow: 5000, contextPercent: null },
|
|
83
|
+
unknownCoreFallback: null,
|
|
84
|
+
fallbackContextTokens: 4000,
|
|
85
|
+
fallbackContextWindow: 5000,
|
|
86
|
+
}), {
|
|
87
|
+
contextTokens: null,
|
|
88
|
+
contextWindow: 5000,
|
|
89
|
+
contextPercent: null,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("resolveDisplayContextUsage uses the approximate estimate for unknown core usage", () => {
|
|
94
|
+
const reloadEstimate = { contextTokens: 1000, contextWindow: 5000, contextPercent: 20 };
|
|
95
|
+
assert.equal(resolveDisplayContextUsage({
|
|
96
|
+
coreContextUsage: { contextTokens: null, contextWindow: 5000, contextPercent: null },
|
|
97
|
+
unknownCoreFallback: reloadEstimate,
|
|
98
|
+
fallbackContextTokens: 4000,
|
|
99
|
+
fallbackContextWindow: 5000,
|
|
100
|
+
}), reloadEstimate);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("resolveDisplayContextUsage computes assistant fallback usage when Pi has no current estimate", () => {
|
|
104
|
+
assert.deepEqual(resolveDisplayContextUsage({
|
|
105
|
+
coreContextUsage: null,
|
|
106
|
+
unknownCoreFallback: null,
|
|
107
|
+
fallbackContextTokens: 1000,
|
|
108
|
+
fallbackContextWindow: 4000,
|
|
109
|
+
}), {
|
|
110
|
+
contextTokens: 1000,
|
|
111
|
+
contextWindow: 4000,
|
|
112
|
+
contextPercent: 25,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("estimateUnknownContextUsage estimates the active compacted context", () => {
|
|
117
|
+
const usage = estimateUnknownContextUsage({
|
|
118
|
+
getContextUsage: () => ({ tokens: null, contextWindow: 100, percent: null }),
|
|
119
|
+
getSystemPrompt: () => "12345678",
|
|
120
|
+
sessionManager: {
|
|
121
|
+
buildContextEntries: () => [
|
|
122
|
+
{ type: "compaction", summary: "12345678", tokensBefore: 90, timestamp: "2026-08-08T00:00:00.000Z" },
|
|
123
|
+
{ type: "message", message: { role: "user", content: "1234", timestamp: 0 } },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
assert.deepEqual(usage, {
|
|
129
|
+
contextTokens: 5,
|
|
130
|
+
contextWindow: 100,
|
|
131
|
+
contextPercent: 5,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("estimateUnknownContextUsage skips sessions with known core usage", () => {
|
|
136
|
+
let entryReads = 0;
|
|
137
|
+
assert.equal(estimateUnknownContextUsage({
|
|
138
|
+
getContextUsage: () => ({ tokens: 25, contextWindow: 100, percent: 25 }),
|
|
139
|
+
getSystemPrompt: () => "12345678",
|
|
140
|
+
sessionManager: {
|
|
141
|
+
buildContextEntries() {
|
|
142
|
+
entryReads += 1;
|
|
143
|
+
return [];
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}), null);
|
|
147
|
+
assert.equal(entryReads, 0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("estimateInitialContextTokens uses Pi's conservative character estimate", () => {
|
|
151
|
+
assert.equal(estimateInitialContextTokens({}), null);
|
|
152
|
+
assert.equal(estimateInitialContextTokens({ getSystemPrompt: () => "" }), null);
|
|
153
|
+
assert.equal(estimateInitialContextTokens({ getSystemPrompt: () => " " }), null);
|
|
154
|
+
assert.equal(estimateInitialContextTokens({ getSystemPrompt: () => "1234" }), 1);
|
|
155
|
+
assert.equal(estimateInitialContextTokens({ getSystemPrompt: () => "12345" }), 2);
|
|
156
|
+
});
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { collectHiddenExtensionStatusKeys, getNotificationExtensionStatuses, normalizeExtensionStatusValue, parsePowerlineConfig, mergeSegmentsWithCustomItems, nextPowerlineSettingWithOptions, nextPowerlineSettingWithPreset, normalizeCompactExtensionStatus } from "../powerline-config.ts";
|
|
3
|
+
import { collectHiddenExtensionStatusKeys, getNotificationExtensionStatuses, normalizeExtensionStatusValue, parsePowerlineConfig, mergeSegmentOptions, mergeSegmentsWithCustomItems, nextPowerlineSettingWithOptions, nextPowerlineSettingWithPreset, normalizeCompactExtensionStatus } from "../powerline-config.ts";
|
|
4
|
+
import { getSeparator } from "../separators.ts";
|
|
5
|
+
import { PRESETS } from "../presets.ts";
|
|
6
|
+
|
|
7
|
+
test("fixed custom preset is removed in favor of powerline.layout", () => {
|
|
8
|
+
assert.equal("custom" in PRESETS, false);
|
|
9
|
+
});
|
|
4
10
|
|
|
5
11
|
test("parsePowerlineConfig supports object config with custom items", () => {
|
|
6
12
|
const config = parsePowerlineConfig(
|
|
@@ -20,28 +26,204 @@ test("parsePowerlineConfig supports object config with custom items", () => {
|
|
|
20
26
|
assert.equal(config.customItems[0].statusKey, "ci-status");
|
|
21
27
|
assert.equal(config.customItems[1].statusKey, "review");
|
|
22
28
|
assert.equal(config.customItems[1].hideWhenMissing, false);
|
|
23
|
-
assert.
|
|
24
|
-
assert.
|
|
29
|
+
assert.deepEqual(config.disabledSegments, []);
|
|
30
|
+
assert.deepEqual(config.invalidDisabledSegments, []);
|
|
31
|
+
assert.equal(config.layout, null);
|
|
32
|
+
assert.deepEqual(config.invalidLayoutSegments, []);
|
|
33
|
+
assert.equal(config.separator, null);
|
|
34
|
+
assert.equal(config.placement, "above");
|
|
35
|
+
assert.equal(config.invalidPlacement, null);
|
|
36
|
+
assert.equal(config.welcome, true);
|
|
37
|
+
assert.equal(config.stashSharpSShortcut, false);
|
|
38
|
+
assert.deepEqual(config.queue, { captureSigil: "#" });
|
|
25
39
|
});
|
|
26
40
|
|
|
27
|
-
test("parsePowerlineConfig supports
|
|
41
|
+
test("parsePowerlineConfig supports disabled segments", () => {
|
|
28
42
|
const config = parsePowerlineConfig(
|
|
29
|
-
{
|
|
43
|
+
{
|
|
44
|
+
preset: "default",
|
|
45
|
+
customItems: [{ id: "ci" }],
|
|
46
|
+
disabledSegments: [
|
|
47
|
+
"queue",
|
|
48
|
+
"cost",
|
|
49
|
+
" extension_statuses ",
|
|
50
|
+
"custom:ci",
|
|
51
|
+
"cost",
|
|
52
|
+
"unknown",
|
|
53
|
+
"custom:missing",
|
|
54
|
+
123,
|
|
55
|
+
],
|
|
56
|
+
},
|
|
30
57
|
["default", "compact"],
|
|
31
58
|
);
|
|
32
59
|
|
|
33
|
-
assert.
|
|
34
|
-
assert.
|
|
60
|
+
assert.deepEqual(config.disabledSegments, ["queue", "cost", "extension_statuses", "custom:ci"]);
|
|
61
|
+
assert.deepEqual(config.invalidDisabledSegments, ["unknown", "custom:missing", "123"]);
|
|
35
62
|
});
|
|
36
63
|
|
|
37
|
-
test("parsePowerlineConfig supports
|
|
64
|
+
test("parsePowerlineConfig supports partial explicit layout rows", () => {
|
|
38
65
|
const config = parsePowerlineConfig(
|
|
39
|
-
{
|
|
66
|
+
{
|
|
67
|
+
preset: "default",
|
|
68
|
+
customItems: [{ id: "ci" }],
|
|
69
|
+
layout: {
|
|
70
|
+
left: ["model", "custom:ci", "model", "unknown", 123],
|
|
71
|
+
right: ["model", "cost"],
|
|
72
|
+
secondary: [],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
40
75
|
["default", "compact"],
|
|
41
76
|
);
|
|
42
77
|
|
|
43
|
-
assert.
|
|
44
|
-
|
|
78
|
+
assert.deepEqual(config.layout, {
|
|
79
|
+
left: ["model", "custom:ci"],
|
|
80
|
+
right: ["cost"],
|
|
81
|
+
secondary: [],
|
|
82
|
+
});
|
|
83
|
+
assert.deepEqual(config.invalidLayoutSegments, ["left:unknown", "left:123", "right:model"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("parsePowerlineConfig preserves reporter layout groups", () => {
|
|
87
|
+
const config = parsePowerlineConfig(
|
|
88
|
+
{
|
|
89
|
+
preset: "default",
|
|
90
|
+
layout: {
|
|
91
|
+
left: ["model"],
|
|
92
|
+
right: ["path"],
|
|
93
|
+
secondary: ["thinking"],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
["default", "compact"],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
assert.deepEqual(config.layout, {
|
|
100
|
+
left: ["model"],
|
|
101
|
+
right: ["path"],
|
|
102
|
+
secondary: ["thinking"],
|
|
103
|
+
});
|
|
104
|
+
assert.deepEqual(config.invalidLayoutSegments, []);
|
|
105
|
+
assert.deepEqual(
|
|
106
|
+
mergeSegmentsWithCustomItems(PRESETS.default, config.customItems, {
|
|
107
|
+
layout: config.layout,
|
|
108
|
+
disabledSegments: config.disabledSegments,
|
|
109
|
+
}),
|
|
110
|
+
{
|
|
111
|
+
leftSegments: ["model"],
|
|
112
|
+
rightSegments: ["path"],
|
|
113
|
+
secondarySegments: ["thinking"],
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("parsePowerlineConfig supports separator overrides", () => {
|
|
119
|
+
const config = parsePowerlineConfig(
|
|
120
|
+
{ preset: "default", separator: " chevron " },
|
|
121
|
+
["default", "compact"],
|
|
122
|
+
);
|
|
123
|
+
const invalid = parsePowerlineConfig(
|
|
124
|
+
{ preset: "default", separator: "sparkle" },
|
|
125
|
+
["default", "compact"],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
assert.equal(config.separator, "chevron");
|
|
129
|
+
assert.equal(invalid.separator, null);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("configured separators resolve independently of presets", () => {
|
|
133
|
+
const presetSeparator = getSeparator(PRESETS.default.separator).left;
|
|
134
|
+
const configuredSeparator = getSeparator("chevron").left;
|
|
135
|
+
|
|
136
|
+
assert.notEqual(configuredSeparator, presetSeparator);
|
|
137
|
+
assert.equal(configuredSeparator, "›");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("parsePowerlineConfig validates primary powerline placement", () => {
|
|
141
|
+
const below = parsePowerlineConfig(
|
|
142
|
+
{ preset: "compact", placement: "below" },
|
|
143
|
+
["default", "compact"],
|
|
144
|
+
);
|
|
145
|
+
const invalid = parsePowerlineConfig(
|
|
146
|
+
{ preset: "compact", placement: "sideways" },
|
|
147
|
+
["default", "compact"],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
assert.equal(below.placement, "below");
|
|
151
|
+
assert.equal(below.invalidPlacement, null);
|
|
152
|
+
assert.equal(invalid.placement, "above");
|
|
153
|
+
assert.equal(invalid.invalidPlacement, "sideways");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
test("parsePowerlineConfig supports welcome, legacy sharp-S, and queue capture settings", () => {
|
|
160
|
+
const disabled = parsePowerlineConfig(
|
|
161
|
+
{ preset: "compact", welcome: false, stashSharpSShortcut: true, queue: { captureSigil: false } },
|
|
162
|
+
["default", "compact"],
|
|
163
|
+
);
|
|
164
|
+
const customSigil = parsePowerlineConfig(
|
|
165
|
+
{ preset: "compact", queue: { captureSigil: "//" } },
|
|
166
|
+
["default", "compact"],
|
|
167
|
+
);
|
|
168
|
+
const invalidSigil = parsePowerlineConfig(
|
|
169
|
+
{ preset: "compact", queue: { captureSigil: "# note" } },
|
|
170
|
+
["default", "compact"],
|
|
171
|
+
);
|
|
172
|
+
const shorthand = parsePowerlineConfig("compact", ["default", "compact"]);
|
|
173
|
+
|
|
174
|
+
assert.equal(disabled.welcome, false);
|
|
175
|
+
assert.equal(disabled.stashSharpSShortcut, true);
|
|
176
|
+
assert.deepEqual(disabled.queue, { captureSigil: false });
|
|
177
|
+
assert.deepEqual(customSigil.queue, { captureSigil: "//" });
|
|
178
|
+
assert.deepEqual(invalidSigil.queue, { captureSigil: "#" });
|
|
179
|
+
assert.equal(shorthand.welcome, true);
|
|
180
|
+
assert.equal(shorthand.stashSharpSShortcut, false);
|
|
181
|
+
assert.deepEqual(shorthand.queue, { captureSigil: "#" });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("parsePowerlineConfig extracts supported segment options", () => {
|
|
185
|
+
const config = parsePowerlineConfig(
|
|
186
|
+
{
|
|
187
|
+
preset: "default",
|
|
188
|
+
model: { showThinkingLevel: true, display: "qualified" },
|
|
189
|
+
path: { mode: "full", maxLength: 120 },
|
|
190
|
+
git: { showBranch: false, showStaged: false, showUnstaged: true, showUntracked: false, polling: "branch", hostIcon: true },
|
|
191
|
+
time: { format: "12h", showSeconds: true },
|
|
192
|
+
cost: { subscriptionDisplay: "both", currency: "cny" },
|
|
193
|
+
},
|
|
194
|
+
["default", "compact"],
|
|
195
|
+
);
|
|
196
|
+
const invalidCurrency = parsePowerlineConfig(
|
|
197
|
+
{ cost: { currency: "BTC" } },
|
|
198
|
+
["default", "compact"],
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
assert.deepEqual(config.segmentOptions, {
|
|
202
|
+
model: { showThinkingLevel: true, display: "qualified" },
|
|
203
|
+
path: { mode: "full", maxLength: 120 },
|
|
204
|
+
git: { showBranch: false, showStaged: false, showUnstaged: true, showUntracked: false, polling: "branch", hostIcon: true },
|
|
205
|
+
time: { format: "12h", showSeconds: true },
|
|
206
|
+
cost: { subscriptionDisplay: "both", currency: "CNY" },
|
|
207
|
+
});
|
|
208
|
+
assert.deepEqual(invalidCurrency.segmentOptions, { cost: {} });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("mergeSegmentOptions lets user config override preset segment defaults", () => {
|
|
212
|
+
assert.deepEqual(
|
|
213
|
+
mergeSegmentOptions(
|
|
214
|
+
{ path: { mode: "basename", maxLength: 20 }, git: { showBranch: true, showUntracked: true } },
|
|
215
|
+
{ path: { mode: "full" }, git: { showUntracked: false }, cost: { subscriptionDisplay: "reported-cost" } },
|
|
216
|
+
),
|
|
217
|
+
{
|
|
218
|
+
model: {},
|
|
219
|
+
path: { mode: "full", maxLength: 20 },
|
|
220
|
+
git: { showBranch: true, showUntracked: false },
|
|
221
|
+
time: {},
|
|
222
|
+
cost: { subscriptionDisplay: "reported-cost" },
|
|
223
|
+
context: {},
|
|
224
|
+
cache_read: {},
|
|
225
|
+
},
|
|
226
|
+
);
|
|
45
227
|
});
|
|
46
228
|
|
|
47
229
|
test("mergeSegmentsWithCustomItems appends custom segment ids by position", () => {
|
|
@@ -64,6 +246,53 @@ test("mergeSegmentsWithCustomItems appends custom segment ids by position", () =
|
|
|
64
246
|
assert.deepEqual(merged.secondarySegments, ["extension_statuses", "custom:review"]);
|
|
65
247
|
});
|
|
66
248
|
|
|
249
|
+
test("mergeSegmentsWithCustomItems filters disabled segment ids", () => {
|
|
250
|
+
const merged = mergeSegmentsWithCustomItems(
|
|
251
|
+
{
|
|
252
|
+
leftSegments: ["path", "model"],
|
|
253
|
+
rightSegments: ["git", "cost"],
|
|
254
|
+
secondarySegments: ["extension_statuses"],
|
|
255
|
+
separator: "powerline",
|
|
256
|
+
},
|
|
257
|
+
[
|
|
258
|
+
{ id: "ci", statusKey: "ci", position: "left", hideWhenMissing: true, excludeFromExtensionStatuses: true },
|
|
259
|
+
{ id: "timer", statusKey: "timer", position: "right", hideWhenMissing: true, excludeFromExtensionStatuses: true },
|
|
260
|
+
{ id: "review", statusKey: "review", position: "secondary", hideWhenMissing: true, excludeFromExtensionStatuses: true },
|
|
261
|
+
],
|
|
262
|
+
{ disabledSegments: ["model", "cost", "custom:ci", "custom:review"] },
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
assert.deepEqual(merged.leftSegments, ["path"]);
|
|
266
|
+
assert.deepEqual(merged.rightSegments, ["git", "custom:timer"]);
|
|
267
|
+
assert.deepEqual(merged.secondarySegments, ["extension_statuses"]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("mergeSegmentsWithCustomItems applies partial layout rows before disabled filtering", () => {
|
|
271
|
+
const merged = mergeSegmentsWithCustomItems(
|
|
272
|
+
{
|
|
273
|
+
leftSegments: ["path", "model"],
|
|
274
|
+
rightSegments: ["git", "cost", "extension_statuses"],
|
|
275
|
+
secondarySegments: ["extension_statuses"],
|
|
276
|
+
separator: "powerline",
|
|
277
|
+
},
|
|
278
|
+
[
|
|
279
|
+
{ id: "ci", statusKey: "ci", position: "right", hideWhenMissing: true, excludeFromExtensionStatuses: true },
|
|
280
|
+
{ id: "review", statusKey: "review", position: "secondary", hideWhenMissing: true, excludeFromExtensionStatuses: true },
|
|
281
|
+
],
|
|
282
|
+
{
|
|
283
|
+
layout: {
|
|
284
|
+
left: ["model", "custom:ci", "extension_statuses"],
|
|
285
|
+
secondary: [],
|
|
286
|
+
},
|
|
287
|
+
disabledSegments: ["model"],
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
assert.deepEqual(merged.leftSegments, ["custom:ci", "extension_statuses"]);
|
|
292
|
+
assert.deepEqual(merged.rightSegments, ["git", "cost"]);
|
|
293
|
+
assert.deepEqual(merged.secondarySegments, []);
|
|
294
|
+
});
|
|
295
|
+
|
|
67
296
|
test("nextPowerlineSettingWithPreset preserves object settings", () => {
|
|
68
297
|
const updated = nextPowerlineSettingWithPreset({ preset: "default", customItems: [{ id: "ci" }] }, "compact");
|
|
69
298
|
if (typeof updated !== "object" || updated === null || Array.isArray(updated)) {
|
|
@@ -82,24 +311,18 @@ test("nextPowerlineSettingWithPreset preserves object settings", () => {
|
|
|
82
311
|
|
|
83
312
|
test("nextPowerlineSettingWithOptions preserves object settings", () => {
|
|
84
313
|
const updated = nextPowerlineSettingWithOptions(
|
|
85
|
-
{ preset: "default", customItems: [{ id: "ci" }]
|
|
86
|
-
{
|
|
314
|
+
{ preset: "default", customItems: [{ id: "ci" }] },
|
|
315
|
+
{ placement: "below" },
|
|
87
316
|
"compact",
|
|
88
317
|
);
|
|
89
|
-
if (typeof updated !== "object" || updated === null || Array.isArray(updated)) {
|
|
90
|
-
assert.fail("expected an object powerline setting");
|
|
91
|
-
}
|
|
92
318
|
|
|
93
|
-
assert.
|
|
94
|
-
assert.equal(updated.fixedEditor, false);
|
|
95
|
-
assert.equal(updated.mouseScroll, false);
|
|
96
|
-
assert.deepEqual(updated.customItems, [{ id: "ci" }]);
|
|
319
|
+
assert.deepEqual(updated, { preset: "default", customItems: [{ id: "ci" }], placement: "below" });
|
|
97
320
|
});
|
|
98
321
|
|
|
99
322
|
test("nextPowerlineSettingWithOptions converts string presets to object settings", () => {
|
|
100
|
-
assert.deepEqual(nextPowerlineSettingWithOptions("compact", {
|
|
323
|
+
assert.deepEqual(nextPowerlineSettingWithOptions("compact", { placement: "below" }, "compact"), {
|
|
101
324
|
preset: "compact",
|
|
102
|
-
|
|
325
|
+
placement: "below",
|
|
103
326
|
});
|
|
104
327
|
});
|
|
105
328
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import type { AutocompleteProvider } from "@earendil-works/pi-tui";
|
|
4
|
+
import { getEditorAutocompleteProvider, passAutocompleteProviderThroughPreviousEditor } from "../editor-composition.ts";
|
|
5
|
+
|
|
6
|
+
function baseProvider(): AutocompleteProvider {
|
|
7
|
+
return {
|
|
8
|
+
async getSuggestions() {
|
|
9
|
+
return {
|
|
10
|
+
prefix: "@",
|
|
11
|
+
items: [{ label: "base", value: "base" }],
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
applyCompletion(lines, cursorLine, cursorCol) {
|
|
15
|
+
return { lines, cursorLine, cursorCol };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("previous editor autocomplete provider wrappers are preserved", async () => {
|
|
21
|
+
const previousEditor = {
|
|
22
|
+
autocompleteProvider: undefined as AutocompleteProvider | undefined,
|
|
23
|
+
setAutocompleteProvider(provider: AutocompleteProvider) {
|
|
24
|
+
this.autocompleteProvider = {
|
|
25
|
+
async getSuggestions(lines, cursorLine, cursorCol, context) {
|
|
26
|
+
const suggestions = await provider.getSuggestions(lines, cursorLine, cursorCol, context);
|
|
27
|
+
return {
|
|
28
|
+
...suggestions,
|
|
29
|
+
items: [...suggestions.items, { label: "previous-wrapper", value: "previous-wrapper" }],
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
applyCompletion: provider.applyCompletion.bind(provider),
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const composed = passAutocompleteProviderThroughPreviousEditor(baseProvider(), previousEditor);
|
|
38
|
+
const suggestions = await composed.getSuggestions(["@"], 0, 1, { signal: new AbortController().signal });
|
|
39
|
+
|
|
40
|
+
assert.equal(composed, previousEditor.autocompleteProvider);
|
|
41
|
+
assert.deepEqual(suggestions.items.map((item) => item.label), ["base", "previous-wrapper"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("autocomplete provider detection rejects incomplete editor providers", () => {
|
|
45
|
+
assert.equal(getEditorAutocompleteProvider({ autocompleteProvider: {} }), undefined);
|
|
46
|
+
assert.equal(getEditorAutocompleteProvider({ autocompleteProvider: { getSuggestions() {} } }), undefined);
|
|
47
|
+
assert.ok(getEditorAutocompleteProvider({ autocompleteProvider: baseProvider() }));
|
|
48
|
+
});
|
|
@@ -34,6 +34,40 @@ test("render scheduler coalesces pending status renders", () => {
|
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
test("render scheduler pulls pending work forward for an earlier deadline", () => {
|
|
38
|
+
const originalSetTimeout = globalThis.setTimeout;
|
|
39
|
+
const originalClearTimeout = globalThis.clearTimeout;
|
|
40
|
+
const callbacks: Array<() => void> = [];
|
|
41
|
+
const delays: number[] = [];
|
|
42
|
+
const cleared = new Set<object>();
|
|
43
|
+
let renderCount = 0;
|
|
44
|
+
|
|
45
|
+
globalThis.setTimeout = ((callback: () => void, delay?: number) => {
|
|
46
|
+
const handle = { id: callbacks.length + 1 };
|
|
47
|
+
callbacks.push(callback);
|
|
48
|
+
delays.push(delay ?? 0);
|
|
49
|
+
return handle as ReturnType<typeof setTimeout>;
|
|
50
|
+
}) as typeof setTimeout;
|
|
51
|
+
globalThis.clearTimeout = ((handle?: ReturnType<typeof setTimeout>) => {
|
|
52
|
+
if (handle && typeof handle === "object") cleared.add(handle);
|
|
53
|
+
}) as typeof clearTimeout;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const scheduler = createRenderScheduler(() => { renderCount += 1; }, 33);
|
|
57
|
+
|
|
58
|
+
scheduler.schedule(250);
|
|
59
|
+
scheduler.schedule(33);
|
|
60
|
+
|
|
61
|
+
assert.deepEqual(delays, [250, 33]);
|
|
62
|
+
assert.equal(cleared.size, 1);
|
|
63
|
+
callbacks[1]?.();
|
|
64
|
+
assert.equal(renderCount, 1);
|
|
65
|
+
} finally {
|
|
66
|
+
globalThis.setTimeout = originalSetTimeout;
|
|
67
|
+
globalThis.clearTimeout = originalClearTimeout;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
37
71
|
test("render scheduler allows callbacks to schedule follow-up renders", () => {
|
|
38
72
|
const originalSetTimeout = globalThis.setTimeout;
|
|
39
73
|
const callbacks: Array<() => void> = [];
|