@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,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { initTheme, theme } from "../../../modes/interactive/theme/theme.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The powerline fixed-editor render draws its own border. It must respect the
|
|
6
|
+
* editor's `borderColor` (set by interactive-mode to the bash-mode / thinking
|
|
7
|
+
* color) instead of always using the gray "sep" color, so typing `!` visibly
|
|
8
|
+
* switches the input field to bash mode.
|
|
9
|
+
*/
|
|
10
|
+
function powerlineBorderColor(editor: { borderColor: unknown }, s: string): string {
|
|
11
|
+
return typeof editor.borderColor === "function"
|
|
12
|
+
? (editor.borderColor as (str: string) => string)(s)
|
|
13
|
+
: `\x1b[38;5;244m${s}\x1b[0m`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("powerline fixed-editor border respects editor.borderColor", () => {
|
|
17
|
+
it("uses the bash-mode color when interactive-mode sets it (typing !)", () => {
|
|
18
|
+
initTheme("dark");
|
|
19
|
+
const editorWithBashBorder = {
|
|
20
|
+
borderColor: theme.getBashModeBorderColor(),
|
|
21
|
+
};
|
|
22
|
+
const border = powerlineBorderColor(editorWithBashBorder, "─".repeat(5));
|
|
23
|
+
// Bash mode is green in the theme; the border must carry a color code.
|
|
24
|
+
expect(border).toContain("\x1b[");
|
|
25
|
+
expect(border).toMatch(/[0-9]+;[0-9]+;[0-9]+m/);
|
|
26
|
+
// And it must NOT be the plain gray "sep" code.
|
|
27
|
+
expect(border).not.toContain("\x1b[38;5;244m");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("falls back to the gray sep color when borderColor is not a function", () => {
|
|
31
|
+
const border = powerlineBorderColor({ borderColor: "x" }, "─".repeat(5));
|
|
32
|
+
expect(border).toBe("\x1b[38;5;244m─────\x1b[0m");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function getBashModeBorderColor(): (str: string) => string {
|
|
37
|
+
// Mirrors theme.getBashModeBorderColor(): colors with the theme's "bashMode" color.
|
|
38
|
+
return (str: string) => `\x1b[38;5;34m${str}\x1b[39m`;
|
|
39
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { detectGitHost, getCurrentBranch, getGitStatus, invalidateGitBranch, invalidateGitStatus, subscribeGitUpdates } from "../git-status.ts";
|
|
4
|
+
|
|
5
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
|
|
7
|
+
// Background fetches spawn git with up to 500ms timeouts; give them room.
|
|
8
|
+
const FETCH_SETTLE_MS = 1200;
|
|
9
|
+
|
|
10
|
+
test("git status supports disabling extension git polling", () => {
|
|
11
|
+
assert.deepEqual(getGitStatus("main", "off"), {
|
|
12
|
+
branch: "main",
|
|
13
|
+
staged: 0,
|
|
14
|
+
unstaged: 0,
|
|
15
|
+
untracked: 0,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("invalidateGitStatus serves stale counts while refreshing (no flicker to zeros)", async () => {
|
|
20
|
+
getGitStatus("provider-branch"); // kick off the initial background fetch
|
|
21
|
+
await sleep(FETCH_SETTLE_MS);
|
|
22
|
+
|
|
23
|
+
const seeded = getGitStatus("provider-branch");
|
|
24
|
+
|
|
25
|
+
invalidateGitStatus();
|
|
26
|
+
const afterInvalidate = getGitStatus("provider-branch");
|
|
27
|
+
assert.deepEqual(
|
|
28
|
+
{ staged: afterInvalidate.staged, unstaged: afterInvalidate.unstaged, untracked: afterInvalidate.untracked },
|
|
29
|
+
{ staged: seeded.staged, unstaged: seeded.unstaged, untracked: seeded.untracked },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// The background refresh converges to real data again (repo is unchanged).
|
|
33
|
+
await sleep(FETCH_SETTLE_MS);
|
|
34
|
+
const refreshed = getGitStatus("provider-branch");
|
|
35
|
+
assert.deepEqual(
|
|
36
|
+
{ staged: refreshed.staged, unstaged: refreshed.unstaged, untracked: refreshed.untracked },
|
|
37
|
+
{ staged: seeded.staged, unstaged: seeded.unstaged, untracked: seeded.untracked },
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("invalidateGitBranch keeps serving the last known branch while refreshing", async () => {
|
|
42
|
+
getGitStatus("provider-fallback"); // seed branch cache
|
|
43
|
+
await sleep(FETCH_SETTLE_MS);
|
|
44
|
+
|
|
45
|
+
const realBranch = getCurrentBranch("provider-fallback");
|
|
46
|
+
// Once fetched, the cached branch no longer falls back to the provider
|
|
47
|
+
// (null in non-git directories, the actual branch name inside a repo).
|
|
48
|
+
assert.notEqual(realBranch, "provider-fallback");
|
|
49
|
+
|
|
50
|
+
invalidateGitBranch();
|
|
51
|
+
assert.equal(getCurrentBranch("provider-fallback"), realBranch);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("git cache completion notifies active footer subscribers", async () => {
|
|
55
|
+
let updates = 0;
|
|
56
|
+
const unsubscribe = subscribeGitUpdates(() => { updates += 1; });
|
|
57
|
+
try {
|
|
58
|
+
invalidateGitStatus();
|
|
59
|
+
invalidateGitBranch();
|
|
60
|
+
getGitStatus("provider-fallback");
|
|
61
|
+
await sleep(FETCH_SETTLE_MS);
|
|
62
|
+
assert.ok(updates > 0);
|
|
63
|
+
} finally {
|
|
64
|
+
unsubscribe();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("detectGitHost recognizes known hosts over SSH and HTTPS", () => {
|
|
69
|
+
assert.equal(detectGitHost("git@github.com:owner/repo.git"), "github");
|
|
70
|
+
assert.equal(detectGitHost("https://github.com/owner/repo.git"), "github");
|
|
71
|
+
assert.equal(detectGitHost("ssh://git@gitlab.com/owner/repo.git"), "gitlab");
|
|
72
|
+
assert.equal(detectGitHost("https://gitlab.com/owner/repo"), "gitlab");
|
|
73
|
+
assert.equal(detectGitHost("git@bitbucket.org:owner/repo.git"), "bitbucket");
|
|
74
|
+
assert.equal(detectGitHost("https://user@bitbucket.org/owner/repo.git"), "bitbucket");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("detectGitHost normalizes www and sub-domains", () => {
|
|
78
|
+
assert.equal(detectGitHost("https://www.github.com/owner/repo"), "github");
|
|
79
|
+
assert.equal(detectGitHost("git@ssh.github.com:owner/repo.git"), "github");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("detectGitHost treats unknown or self-hosted remotes as a generic host", () => {
|
|
83
|
+
assert.equal(detectGitHost("git@git.example.com:owner/repo.git"), "other");
|
|
84
|
+
assert.equal(detectGitHost("https://gitea.mycorp.dev/owner/repo.git"), "other");
|
|
85
|
+
assert.equal(detectGitHost("/srv/git/local.git"), "other");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("detectGitHost returns null when there is no remote", () => {
|
|
89
|
+
assert.equal(detectGitHost(null), null);
|
|
90
|
+
assert.equal(detectGitHost(""), null);
|
|
91
|
+
assert.equal(detectGitHost(" "), null);
|
|
92
|
+
});
|
|
@@ -63,7 +63,10 @@ test("blocking guide dismisses only from its overlay input handler", () => {
|
|
|
63
63
|
|
|
64
64
|
assert.equal(GUIDE_DISMISS_HINT, "Press any key to continue");
|
|
65
65
|
assert.match(source, /guideOverlayBlocking = true;[\s\S]*handleInput: \(\) => dismiss\(\),/);
|
|
66
|
-
|
|
66
|
+
// The blocking guide cannot be dismissed by the welcome scheduler: dismissWelcome
|
|
67
|
+
// bails while guideOverlayBlocking is set, so only the overlay's input handler
|
|
68
|
+
// (which clears the flag first) can close it.
|
|
69
|
+
assert.match(source, /function dismissWelcome\(ctx: any\) \{[\s\S]*?if \(guideOverlayBlocking\) return;/);
|
|
67
70
|
});
|
|
68
71
|
|
|
69
72
|
test("README-derived guide entries stay in the feature map", () => {
|
|
@@ -1,213 +1,49 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import { KEYBINDINGS } from "../node_modules/@selesai/code/dist/core/keybindings.js";
|
|
5
|
-
import {
|
|
6
|
-
isSupportedSuperShortcut,
|
|
7
|
-
matchesConfiguredShortcut,
|
|
8
|
-
shortcutConflictKey,
|
|
9
|
-
} from "../shortcuts.ts";
|
|
10
3
|
|
|
11
|
-
|
|
4
|
+
import { isSupportedSuperShortcut, matchesConfiguredShortcut, shortcutConflictKey } from "../shortcuts.ts";
|
|
5
|
+
import { resolveShortcutConfig } from "../index.ts";
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"editorEnd",
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
function normalizeShortcut(shortcut: string): string {
|
|
29
|
-
const parts = shortcut.trim().toLowerCase().split("+");
|
|
30
|
-
if (parts.length <= 1) return parts[0] ?? "";
|
|
31
|
-
|
|
32
|
-
const modifierRank = new Map(["ctrl", "alt", "super", "shift"].map((modifier, index) => [modifier, index]));
|
|
33
|
-
return [
|
|
34
|
-
...parts.slice(0, -1).sort((a, b) => (modifierRank.get(a) ?? 99) - (modifierRank.get(b) ?? 99)),
|
|
35
|
-
parts[parts.length - 1],
|
|
36
|
-
].join("+");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function powerlineDefaults(): Map<string, string> {
|
|
40
|
-
const defaults = new Map<string, string>();
|
|
41
|
-
for (const match of source.matchAll(/^ ([a-zA-Z0-9]+): "([^"]+)",?$/gm)) {
|
|
42
|
-
const key = match[1];
|
|
43
|
-
const value = match[2];
|
|
44
|
-
if (key && value && powerlineShortcutKeys.has(key)) {
|
|
45
|
-
defaults.set(key, value);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return defaults;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
test("chat jump shortcuts are configurable and route through fixed editor scrolling", () => {
|
|
52
|
-
const defaults = powerlineDefaults();
|
|
53
|
-
assert.equal(defaults.get("jumpPreviousUserMessage"), "ctrl+shift+u");
|
|
54
|
-
assert.equal(defaults.get("jumpNextUserMessage"), "ctrl+shift+i");
|
|
55
|
-
assert.equal(defaults.get("jumpPreviousLlmMessage"), "ctrl+alt+,");
|
|
56
|
-
assert.equal(defaults.get("jumpNextLlmMessage"), "ctrl+alt+.");
|
|
57
|
-
assert.equal(defaults.get("jumpChatBottom"), "ctrl+shift+g");
|
|
58
|
-
assert.equal(defaults.get("scrollChatUp"), "super+up");
|
|
59
|
-
assert.equal(defaults.get("scrollChatDown"), "super+down");
|
|
60
|
-
assert.equal(defaults.get("editorStart"), "super+shift+up");
|
|
61
|
-
assert.equal(defaults.get("editorEnd"), "super+shift+down");
|
|
62
|
-
assert.match(source, /const CHAT_JUMP_SHORTCUTS:/);
|
|
63
|
-
assert.match(source, /shortcutKey: "jumpPreviousUserMessage"/);
|
|
64
|
-
assert.match(source, /shortcutKey: "jumpNextUserMessage"/);
|
|
65
|
-
assert.match(source, /shortcutKey: "jumpPreviousLlmMessage"/);
|
|
66
|
-
assert.match(source, /shortcutKey: "jumpNextLlmMessage"/);
|
|
67
|
-
assert.match(source, /shortcutKey: "jumpChatBottom"/);
|
|
68
|
-
assert.match(source, /pi\.registerShortcut\(resolvedShortcuts\[shortcutKey\]/);
|
|
69
|
-
assert.match(source, /function collectChatMessageStartLines\(role: ChatJumpRole\): number\[\]/);
|
|
70
|
-
assert.match(source, /componentName === "UserMessageComponent"/);
|
|
71
|
-
assert.match(source, /componentName === "SkillInvocationMessageComponent"/);
|
|
72
|
-
assert.match(source, /componentName === "AssistantMessageComponent"/);
|
|
73
|
-
assert.match(source, /fixedEditorCompositor\.jumpToPreviousRootTarget\(targets\)/);
|
|
74
|
-
assert.match(source, /fixedEditorCompositor\.jumpToNextRootTarget\(targets\)/);
|
|
75
|
-
assert.match(source, /fixedEditorCompositor\.jumpToRootBottom\(\)/);
|
|
76
|
-
assert.match(source, /function getChatJumpShortcutAction\(data: string\): ChatJumpShortcutAction \| null/);
|
|
77
|
-
assert.match(source, /let resolvedShortcuts = resolveShortcutConfig\(startupSettings\)/);
|
|
78
|
-
assert.match(source, /resolvedShortcuts = resolveShortcutConfig\(settings\)/);
|
|
79
|
-
assert.match(source, /keyboardScrollShortcuts: \{\n\s+up: resolvedShortcuts\.scrollChatUp,\n\s+down: resolvedShortcuts\.scrollChatDown,/);
|
|
80
|
-
assert.match(source, /editorBoundaryShortcuts: \{\n\s+start: resolvedShortcuts\.editorStart,\n\s+end: resolvedShortcuts\.editorEnd,/);
|
|
81
|
-
assert.match(source, /modifier === "cmd" \|\| modifier === "command" \? "super" : modifier/);
|
|
82
|
-
assert.match(source, /shortcutUsesSuper\(normalizedShortcut\) && !isSupportedSuperShortcut\(normalizedShortcut\)/);
|
|
83
|
-
assert.match(source, /jumpToChatMessage\(ctx, action\.action\.role, action\.action\.direction\)/);
|
|
7
|
+
test("surviving editor shortcuts resolve alongside chat jumps", () => {
|
|
8
|
+
const resolved = resolveShortcutConfig({});
|
|
9
|
+
assert.equal(resolved.stashHistory, "ctrl+alt+h");
|
|
10
|
+
assert.equal(resolved.copyEditor, "ctrl+alt+c");
|
|
11
|
+
assert.equal(resolved.cutEditor, "ctrl+alt+x");
|
|
12
|
+
assert.equal(resolved.ideaCapture, null);
|
|
13
|
+
assert.equal(resolved.editorStart, "super+shift+up");
|
|
14
|
+
assert.equal(resolved.editorEnd, "super+shift+down");
|
|
15
|
+
assert.equal(resolved.jumpPreviousUserMessage, "ctrl+shift+u");
|
|
16
|
+
assert.equal(resolved.jumpChatBottom, "ctrl+shift+g");
|
|
17
|
+
assert.equal(resolved.scrollChatUp, "super+up");
|
|
18
|
+
assert.equal(resolved.scrollChatDown, "super+down");
|
|
84
19
|
});
|
|
85
20
|
|
|
86
|
-
test("super shortcut matching
|
|
87
|
-
assert.equal(matchesConfiguredShortcut("c", "super+c"), false);
|
|
88
|
-
assert.equal(matchesConfiguredShortcut("X", "super+shift+x"), false);
|
|
89
|
-
assert.equal(matchesConfiguredShortcut("\x1b[A", "super+up"), false);
|
|
21
|
+
test("super shortcut matching and conflict normalization remain supported", () => {
|
|
90
22
|
assert.equal(matchesConfiguredShortcut("\x1b[1;9A", "super+up"), true);
|
|
91
|
-
assert.equal(matchesConfiguredShortcut("
|
|
92
|
-
assert.equal(matchesConfiguredShortcut("\x1b[122;9u", "super+z"), false);
|
|
93
|
-
assert.equal(isSupportedSuperShortcut("super+c"), false);
|
|
94
|
-
assert.equal(isSupportedSuperShortcut("super+shift+x"), false);
|
|
95
|
-
assert.equal(isSupportedSuperShortcut("super+z"), false);
|
|
23
|
+
assert.equal(matchesConfiguredShortcut("c", "super+c"), false);
|
|
96
24
|
assert.equal(isSupportedSuperShortcut("super+up"), true);
|
|
25
|
+
assert.equal(isSupportedSuperShortcut("super+z"), false);
|
|
97
26
|
assert.equal(shortcutConflictKey("super+home"), "super+up");
|
|
98
|
-
assert.equal(shortcutConflictKey("super+end"), "super+down");
|
|
99
|
-
assert.equal(shortcutConflictKey("super+shift+home"), "super+shift+up");
|
|
100
|
-
assert.equal(shortcutConflictKey("super+shift+end"), "super+shift+down");
|
|
101
27
|
});
|
|
102
28
|
|
|
103
|
-
test("
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("thinking level changes invalidate powerline status rendering", () => {
|
|
112
|
-
assert.match(source, /let currentThinkingLevel: string \| null = null/);
|
|
113
|
-
assert.match(source, /pi\.on\("thinking_level_select", async \(event, ctx\) => \{\n\s+currentCtx = ctx;\n\s+currentThinkingLevel = getThinkingLevelFn\?\.\(\) \?\? \(typeof event\.level === "string" \? event\.level : null\);\n\s+requestImmediateStatusRender\(\{ deferDuringTyping: false \}\);\n\s+\}\);/);
|
|
114
|
-
assert.match(source, /if \(e\.type === "thinking_level_change" && typeof e\.thinkingLevel === "string"\) \{\n\s+thinkingLevelFromSession = e\.thinkingLevel;/);
|
|
115
|
-
assert.match(source, /const thinkingLevel = currentThinkingLevel \?\? thinkingLevelFromSession \?\? getThinkingLevelFn\?\.\(\) \?\? "off"/);
|
|
116
|
-
});
|
|
29
|
+
test("idea capture shortcut remains configurable", () => {
|
|
30
|
+
const resolved = resolveShortcutConfig({
|
|
31
|
+
powerlineShortcuts: {
|
|
32
|
+
ideaCapture: "ctrl+alt+i",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
117
35
|
|
|
118
|
-
|
|
119
|
-
assert.match(source, /const CONTEXT_STATUS_RENDER_MS = 250/);
|
|
120
|
-
assert.match(source, /function getUsageTokenTotal\(usage: SessionAssistantUsage\): number/);
|
|
121
|
-
assert.match(source, /const totalTokens = "totalTokens" in usage && typeof usage\.totalTokens === "number"/);
|
|
122
|
-
assert.match(source, /return totalTokens \|\| usage\.input \+ usage\.output \+ usage\.cacheRead \+ usage\.cacheWrite/);
|
|
123
|
-
assert.match(source, /let liveAssistantUsage: SessionAssistantUsage \| null = null/);
|
|
124
|
-
assert.doesNotMatch(source, /const requestContextStatusRender/);
|
|
125
|
-
assert.match(source, /lastUserPrompt = "";\n\s+isStreaming = false;\n\s+liveAssistantUsage = null;\n\s+stashedEditorText = null;/);
|
|
126
|
-
assert.match(source, /pi\.on\("agent_start", async \(_event, ctx\) => \{\n\s+isStreaming = true;\n\s+liveAssistantUsage = null;\n\s+onVibeAgentStart\(\);\n\s+dismissWelcome\(ctx\);\n\s+currentCtx = ctx;\n\s+\}\);/);
|
|
127
|
-
assert.match(source, /pi\.on\("message_update", async \(event, ctx\) => \{\n\s+if \(isSessionAssistantMessage\(event\.message\)\n\s+&& event\.message\.stopReason !== "error"\n\s+&& event\.message\.stopReason !== "aborted"\n\s+&& getUsageTokenTotal\(event\.message\.usage\) > 0\) \{\n\s+liveAssistantUsage = event\.message\.usage;\n\s+currentCtx = ctx;\n\s+layoutDirty = true;\n\s+statusRenderScheduler\.schedule\(CONTEXT_STATUS_RENDER_MS\);\n\s+\}\n\s+\}\);/);
|
|
128
|
-
assert.match(source, /pi\.on\("message_end", async \(event, ctx\) => \{\n\s+currentCtx = ctx;\n\s+if \(isSessionAssistantMessage\(event\.message\)\) \{\n\s+if \(event\.message\.stopReason === "error" \|\| event\.message\.stopReason === "aborted"\) \{\n\s+liveAssistantUsage = null;\n\s+\} else if \(getUsageTokenTotal\(event\.message\.usage\) > 0\) \{\n\s+liveAssistantUsage = event\.message\.usage;\n\s+\}\n\s+\}\n\s+requestImmediateStatusRender\(\{ deferDuringTyping: false \}\);\n\s+\}\);/);
|
|
129
|
-
assert.match(source, /pi\.on\("agent_end", async \(_event, ctx\) => \{\n\s+isStreaming = false;\n\s+liveAssistantUsage = null;\n\s+currentCtx = ctx;/);
|
|
130
|
-
assert.match(source, /pi\.on\("session_tree", async \(_event, ctx\) => \{\n\s+currentCtx = ctx;\n\s+currentThinkingLevel = null;\n\s+liveAssistantUsage = null;\n\s+requestImmediateStatusRender\(\{ deferDuringTyping: false \}\);\n\s+\}\);/);
|
|
131
|
-
assert.match(source, /if \(getUsageTokenTotal\(m\.usage\) > 0\) \{\n\s+lastAssistant = m;\n\s+\}/);
|
|
132
|
-
assert.match(source, /const coreContextUsage = isStreaming && liveAssistantUsage \? null : readCoreContextUsage\(ctx\)/);
|
|
133
|
-
assert.match(source, /const contextTokens = coreContextUsage\?\.contextTokens \?\? \(latestUsage \? getUsageTokenTotal\(latestUsage\) : 0\)/);
|
|
36
|
+
assert.equal(resolved.ideaCapture, "ctrl+alt+i");
|
|
134
37
|
});
|
|
135
38
|
|
|
136
|
-
test("
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
assert.match(source, /setExtensionStatus\?: \(key: string, text: string \| undefined\) => void/);
|
|
144
|
-
assert.match(source, /const setExtensionStatusAndRepaint = function setExtensionStatusAndRepaint/);
|
|
145
|
-
assert.match(source, /originalSetExtensionStatus\.call\(this, key, text\);\n\s+requestImmediateStatusRender\(\);/);
|
|
146
|
-
assert.match(source, /installFooterStatusRepaintHook\(footerData\);/);
|
|
147
|
-
assert.match(source, /if \(writableFooterData\.setExtensionStatus === setExtensionStatusAndRepaint\) \{\n\s+writableFooterData\.setExtensionStatus = originalSetExtensionStatus;/);
|
|
148
|
-
assert.match(source, /if \(clearExtensionStatusesAndRepaint && writableFooterData\.clearExtensionStatuses === clearExtensionStatusesAndRepaint\)/);
|
|
149
|
-
assert.match(source, /restoreFooterStatusRepaintHook\?\.\(\);\n\s+restoreFooterStatusRepaintHook = null;/);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("fixed editor captures Pi status messages with the editor cluster", () => {
|
|
153
|
-
assert.match(source, /let fixedStatusContainer: any = null/);
|
|
154
|
-
assert.match(source, /const statusContainerCandidate = tuiChildren\[editorContainerMatch\.index - 2\] \?\? null/);
|
|
155
|
-
assert.match(source, /fixedStatusContainer = statusContainerCandidate && typeof statusContainerCandidate\.render === "function"/);
|
|
156
|
-
assert.match(source, /compositor\.renderHidden\(fixedStatusContainer, width\)\.filter\(\(line\) => visibleWidth\(line\) > 0\)/);
|
|
157
|
-
assert.match(source, /statusLines: \[\.\.\.aboveWidgetLines, \.\.\.renderPowerlineStatusLines\(width\), \.\.\.statusContainerLines\]/);
|
|
158
|
-
assert.match(source, /if \(fixedStatusContainer\?\.render\) compositor\.hideRenderable\(fixedStatusContainer\)/);
|
|
159
|
-
assert.match(source, /fixedStatusContainer = null/);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("shutdown cleanup resets terminal modes even before compositor install", () => {
|
|
163
|
-
assert.match(source, /import \{ emergencyTerminalModeReset, TerminalSplitCompositor \}/);
|
|
164
|
-
assert.match(source, /const hadCompositor = fixedEditorCompositor !== null/);
|
|
165
|
-
assert.match(source, /if \(!hadCompositor && options\?\.resetExtendedKeyboardModes\)/);
|
|
166
|
-
assert.match(source, /process\.stdout\.write\(emergencyTerminalModeReset\(\)\)/);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("powerline shortcut defaults do not claim reserved Pi shortcuts", () => {
|
|
170
|
-
const reservedKeys = new Map<string, string>();
|
|
171
|
-
for (const [id, definition] of Object.entries(KEYBINDINGS)) {
|
|
172
|
-
const keys = definition.defaultKeys === undefined
|
|
173
|
-
? []
|
|
174
|
-
: Array.isArray(definition.defaultKeys)
|
|
175
|
-
? definition.defaultKeys
|
|
176
|
-
: [definition.defaultKeys];
|
|
177
|
-
for (const key of keys) {
|
|
178
|
-
reservedKeys.set(normalizeShortcut(key), id);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
for (const [name, shortcut] of powerlineDefaults()) {
|
|
183
|
-
const conflict = reservedKeys.get(normalizeShortcut(shortcut));
|
|
184
|
-
assert.equal(conflict, undefined, `${name} default ${shortcut} conflicts with ${conflict}`);
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("powerline fallback routing rejects reserved Pi shortcut defaults", () => {
|
|
189
|
-
assert.doesNotMatch(source, /KeybindingsManager/);
|
|
190
|
-
assert.match(source, /TUI_KEYBINDINGS/);
|
|
191
|
-
assert.match(source, /const APP_RESERVED_SHORTCUTS = \[/);
|
|
192
|
-
assert.match(source, /"alt\+enter"/);
|
|
193
|
-
assert.match(source, /"alt\+up"/);
|
|
194
|
-
assert.match(source, /"alt\+down"/);
|
|
195
|
-
assert.match(source, /"ctrl\+s"/);
|
|
196
|
-
assert.match(source, /"shift\+l"/);
|
|
197
|
-
assert.match(source, /for \(const definition of Object\.values\(TUI_KEYBINDINGS\)\)/);
|
|
198
|
-
assert.doesNotMatch(source, /RESERVED_TUI_KEYBINDING_IDS/);
|
|
199
|
-
assert.match(source, /const EXTRA_RESERVED_SHORTCUTS = \["alt\+s"\] as const/);
|
|
200
|
-
assert.match(source, /const SHORTCUT_MODIFIER_ORDER = \["ctrl", "alt", "super", "shift"\] as const/);
|
|
201
|
-
assert.match(source, /const SHORTCUT_MODIFIERS = new Set\(SHORTCUT_MODIFIER_ORDER\)/);
|
|
202
|
-
assert.match(source, /modifierRank\.get\(a\)/);
|
|
203
|
-
assert.match(source, /configuredToggleShortcut && !reservedShortcuts\(\)\.has\(shortcutUsageKey\(configuredToggleShortcut\)\)/);
|
|
204
|
-
});
|
|
39
|
+
test("editor boundary shortcuts remain configurable", () => {
|
|
40
|
+
const resolved = resolveShortcutConfig({
|
|
41
|
+
powerlineShortcuts: {
|
|
42
|
+
editorStart: "ctrl+shift+y",
|
|
43
|
+
editorEnd: "ctrl+shift+x",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
205
46
|
|
|
206
|
-
|
|
207
|
-
assert.
|
|
208
|
-
assert.match(source, /matchesConfiguredShortcut\(data, resolvedShortcuts\.stashHistory\)/);
|
|
209
|
-
assert.match(source, /matchesConfiguredShortcut\(data, resolvedShortcuts\.copyEditor\)/);
|
|
210
|
-
assert.match(source, /matchesConfiguredShortcut\(data, resolvedShortcuts\.cutEditor\)/);
|
|
211
|
-
assert.match(source, /matchesConfiguredShortcut\(data, bashModeSettings\.toggleShortcut\)/);
|
|
212
|
-
assert.match(source, /runPowerlineShortcut\(ctx, powerlineShortcutAction\)/);
|
|
47
|
+
assert.equal(resolved.editorStart, "ctrl+shift+y");
|
|
48
|
+
assert.equal(resolved.editorEnd, "ctrl+shift+x");
|
|
213
49
|
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { Editor } from "@earendil-works/pi-tui";
|
|
3
|
+
import { createInlineSkillAutocompleteProvider } from "../../../extensions/inline-skills.ts";
|
|
4
|
+
import { createInlineSubagentAutocompleteProvider } from "../../../extensions/pi-subagents/src/slash/inline-subagents.ts";
|
|
5
|
+
import { ModeAwareAutocompleteProvider, BashAutocompleteProvider, OneOffBashAutocompleteProvider } from "../bash-mode/completion.ts";
|
|
6
|
+
|
|
7
|
+
const fakeTui = { requestRender() {}, terminal: { rows: 50 } } as never;
|
|
8
|
+
const pi = {
|
|
9
|
+
getCommands: () => [
|
|
10
|
+
{ name: "skill:grill-me", description: "Grill", source: "skill", sourceInfo: { path: "C:/x/grill.md" } },
|
|
11
|
+
{ name: "skill:research", description: "Research", source: "skill", sourceInfo: { path: "C:/x/research.md" } },
|
|
12
|
+
],
|
|
13
|
+
on: () => {},
|
|
14
|
+
} as never;
|
|
15
|
+
|
|
16
|
+
const baseProvider: any = {
|
|
17
|
+
triggerCharacters: [],
|
|
18
|
+
async getSuggestions() {
|
|
19
|
+
return null;
|
|
20
|
+
},
|
|
21
|
+
applyCompletion(lines: string[], cursorLine: number, cursorCol: number) {
|
|
22
|
+
return { lines, cursorLine, cursorCol };
|
|
23
|
+
},
|
|
24
|
+
shouldTriggerFileCompletion() {
|
|
25
|
+
return true;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const popupValues = (e: Editor) =>
|
|
30
|
+
(e as any).autocompleteState ? ((e as any).autocompleteList?.items ?? []).map((i: any) => i.value) : null;
|
|
31
|
+
|
|
32
|
+
async function typeInto(e: Editor, text: string) {
|
|
33
|
+
for (const ch of text) {
|
|
34
|
+
(e as any).insertCharacter(ch);
|
|
35
|
+
await (e as any).autocompleteRequestTask;
|
|
36
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildChain() {
|
|
41
|
+
let provider: any = baseProvider;
|
|
42
|
+
for (const wrap of [
|
|
43
|
+
(c: any) => createInlineSkillAutocompleteProvider(pi, c),
|
|
44
|
+
(c: any) => createInlineSubagentAutocompleteProvider({ baseCwd: process.cwd() }, c),
|
|
45
|
+
]) {
|
|
46
|
+
provider = wrap(provider);
|
|
47
|
+
}
|
|
48
|
+
provider.triggerCharacters = [...new Set(["$", "#"])];
|
|
49
|
+
return provider;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("powerline ModeAware wrapper must forward trigger characters", () => {
|
|
53
|
+
it("keeps $ after wrapping and shows skills for the user's three inputs", async () => {
|
|
54
|
+
const chain = buildChain();
|
|
55
|
+
// powerline: editor.installAutocompleteProvider(new ModeAware(chain, bash, oneOffBash))
|
|
56
|
+
const wrapped = new ModeAwareAutocompleteProvider(chain, new BashAutocompleteProvider(), new OneOffBashAutocompleteProvider(), () => false);
|
|
57
|
+
|
|
58
|
+
const e = new Editor(fakeTui as any, { borderColor: "x" } as any);
|
|
59
|
+
e.setAutocompleteProvider(wrapped as any);
|
|
60
|
+
console.log("triggerChars after wrap:", (e as any).autocompleteTriggerCharacters);
|
|
61
|
+
expect((e as any).autocompleteTriggerCharacters).toContain("$");
|
|
62
|
+
|
|
63
|
+
await typeInto(e, "$gril");
|
|
64
|
+
console.log("'$gril' ->", JSON.stringify(popupValues(e)));
|
|
65
|
+
expect(popupValues(e) ?? []).toContain("$grill-me");
|
|
66
|
+
|
|
67
|
+
const e2 = new Editor(fakeTui as any, { borderColor: "x" } as any);
|
|
68
|
+
e2.setAutocompleteProvider(wrapped as any);
|
|
69
|
+
await typeInto(e2, "asdf $gril");
|
|
70
|
+
console.log("'asdf $gril' ->", JSON.stringify(popupValues(e2)));
|
|
71
|
+
expect(popupValues(e2) ?? []).toContain("$grill-me");
|
|
72
|
+
|
|
73
|
+
const e3 = new Editor(fakeTui as any, { borderColor: "x" } as any);
|
|
74
|
+
e3.setAutocompleteProvider(wrapped as any);
|
|
75
|
+
await typeInto(e3, "asdf #builder $gril");
|
|
76
|
+
console.log("'asdf #builder $gril' ->", JSON.stringify(popupValues(e3)));
|
|
77
|
+
expect(popupValues(e3) ?? []).toContain("$grill-me");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { getAgentPath, getAgentSessionDirs, getLegacyPiPath } from "../paths.ts";
|
|
7
|
+
|
|
8
|
+
function withTemporaryPathEnv(run: (home: string, agentDir: string) => void): void {
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), "powerline-paths-"));
|
|
10
|
+
const home = join(root, "home");
|
|
11
|
+
const agentDir = join(root, "custom-agent");
|
|
12
|
+
const originalHome = process.env.HOME;
|
|
13
|
+
const originalUserProfile = process.env.USERPROFILE;
|
|
14
|
+
const originalAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
mkdirSync(home, { recursive: true });
|
|
18
|
+
process.env.HOME = home;
|
|
19
|
+
delete process.env.USERPROFILE;
|
|
20
|
+
run(home, agentDir);
|
|
21
|
+
} finally {
|
|
22
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
23
|
+
else process.env.HOME = originalHome;
|
|
24
|
+
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
|
25
|
+
else process.env.USERPROFILE = originalUserProfile;
|
|
26
|
+
if (originalAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
27
|
+
else process.env.SELESAI_CODING_AGENT_DIR = originalAgentDir;
|
|
28
|
+
rmSync(root, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test("agent dir uses non-empty SELESAI_CODING_AGENT_DIR", () => {
|
|
33
|
+
withTemporaryPathEnv((_home, agentDir) => {
|
|
34
|
+
process.env.SELESAI_CODING_AGENT_DIR = agentDir;
|
|
35
|
+
|
|
36
|
+
assert.equal(getAgentPath("settings.json"), join(agentDir, "settings.json"));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("agent dir falls back to HOME config dir agent for empty env values", () => {
|
|
41
|
+
withTemporaryPathEnv((home) => {
|
|
42
|
+
process.env.SELESAI_CODING_AGENT_DIR = "";
|
|
43
|
+
|
|
44
|
+
assert.equal(getAgentPath("settings.json"), join(home, ".selesai", "agent", "settings.json"));
|
|
45
|
+
assert.equal(getAgentPath("sessions"), join(home, ".selesai", "agent", "sessions"));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("agent sessions include legacy config-dir sessions only when it exists", () => {
|
|
50
|
+
withTemporaryPathEnv((home, agentDir) => {
|
|
51
|
+
process.env.SELESAI_CODING_AGENT_DIR = agentDir;
|
|
52
|
+
assert.deepEqual(getAgentSessionDirs(), [join(agentDir, "sessions")]);
|
|
53
|
+
|
|
54
|
+
mkdirSync(join(home, ".selesai", "sessions"), { recursive: true });
|
|
55
|
+
assert.deepEqual(getAgentSessionDirs(), [join(agentDir, "sessions"), join(home, ".selesai", "sessions")]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("legacy path resolves under the fork config dir name", () => {
|
|
60
|
+
withTemporaryPathEnv((home) => {
|
|
61
|
+
assert.equal(getLegacyPiPath("sessions"), join(home, ".selesai", "sessions"));
|
|
62
|
+
});
|
|
63
|
+
});
|