@selesai/code 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +4 -2
  3. package/dist/extensions/agent-browser.test.ts +195 -0
  4. package/dist/extensions/agent-browser.ts +158 -0
  5. package/dist/extensions/inline-skills.test.ts +14 -0
  6. package/dist/extensions/inline-skills.ts +14 -7
  7. package/dist/extensions/package.json +2 -1
  8. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +27 -0
  9. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +156 -0
  10. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +16 -0
  11. package/dist/extensions/pi-powerline-footer/README.md +192 -44
  12. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +19 -4
  13. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +80 -25
  14. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +81 -29
  15. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +1 -1
  16. package/dist/extensions/pi-powerline-footer/cd-command.ts +190 -0
  17. package/dist/extensions/pi-powerline-footer/colors.ts +2 -1
  18. package/dist/extensions/pi-powerline-footer/context-usage.ts +121 -6
  19. package/dist/extensions/pi-powerline-footer/currency-rates.ts +142 -0
  20. package/dist/extensions/pi-powerline-footer/editor-composition.ts +32 -0
  21. package/dist/extensions/pi-powerline-footer/git-status.ts +108 -6
  22. package/dist/extensions/pi-powerline-footer/guide.ts +1 -1
  23. package/dist/extensions/pi-powerline-footer/icons.ts +11 -2
  24. package/dist/extensions/pi-powerline-footer/index.ts +1056 -324
  25. package/dist/extensions/pi-powerline-footer/lifecycle.ts +10 -0
  26. package/dist/extensions/pi-powerline-footer/package.json +10 -6
  27. package/dist/extensions/pi-powerline-footer/paths.ts +22 -0
  28. package/dist/extensions/pi-powerline-footer/powerline-config.ts +297 -15
  29. package/dist/extensions/pi-powerline-footer/presets.ts +6 -25
  30. package/dist/extensions/pi-powerline-footer/queue/store.ts +361 -0
  31. package/dist/extensions/pi-powerline-footer/queue/types.ts +54 -0
  32. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +12 -7
  33. package/dist/extensions/pi-powerline-footer/segments.ts +114 -107
  34. package/dist/extensions/pi-powerline-footer/session-usage.ts +1 -1
  35. package/dist/extensions/pi-powerline-footer/shortcuts.ts +17 -3
  36. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +189 -17
  37. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +144 -0
  38. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +120 -2
  39. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +245 -22
  40. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +48 -0
  41. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +34 -0
  42. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +39 -0
  43. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +92 -0
  44. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +11 -6
  45. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +33 -197
  46. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +79 -0
  47. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +63 -0
  48. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +195 -0
  49. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +14 -0
  50. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +167 -0
  51. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +23 -18
  52. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +16 -1
  53. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +249 -0
  54. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +237 -0
  55. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +199 -0
  56. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +191 -100
  57. package/dist/extensions/pi-powerline-footer/theme.ts +32 -18
  58. package/dist/extensions/pi-powerline-footer/token-stats.ts +304 -0
  59. package/dist/extensions/pi-powerline-footer/tsconfig.json +13 -0
  60. package/dist/extensions/pi-powerline-footer/types.ts +74 -29
  61. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +34 -0
  62. package/dist/extensions/pi-powerline-footer/welcome.ts +65 -10
  63. package/dist/extensions/pi-powerline-footer/working-vibes.ts +44 -11
  64. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  65. package/dist/extensions/pi-subagents/package.json +1 -1
  66. package/dist/extensions/pi-subagents/src/slash/inline-subagents.ts +57 -24
  67. package/dist/extensions/pi-subagents/test/unit/inline-subagents.test.ts +82 -7
  68. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +1 -1
  69. package/dist/extensions/ponytail/index.js +1 -31
  70. package/dist/extensions/ponytail/test/extension.test.js +5 -22
  71. package/dist/extensions/rtk.test.ts +27 -12
  72. package/dist/extensions/rtk.ts +41 -35
  73. 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
+ });
@@ -22,7 +22,7 @@ const features: GuideFeature[] = [
22
22
  ];
23
23
 
24
24
 
25
- test("guide opens fully for each real session start", () => {
25
+ test("explicit full guide opens for each real session start", () => {
26
26
  for (const reason of ["startup", "new", "resume", "fork"]) {
27
27
  assert.equal(resolveGuideDisplayMode({ reason, hasUI: true, guideMode: "full" }), "full");
28
28
  }
@@ -35,8 +35,10 @@ test("guide dismissal and compact preferences change automatic display", () => {
35
35
  assert.equal(resolveGuideDisplayMode({ reason: "startup", hasUI: false, guideMode: "full" }), "none");
36
36
  });
37
37
 
38
- test("quiet startup does not suppress the default blocking guide", () => {
39
- assert.equal(resolveGuideDisplayMode({ reason: "startup", hasUI: true, guideMode: "full" }), "full");
38
+ test("guide defaults to a compact header so fixed-editor mode stays active", () => {
39
+ const preferences = getGuidePreferences({});
40
+ assert.equal(preferences.mode, "compact");
41
+ assert.equal(resolveGuideDisplayMode({ reason: "startup", hasUI: true, guideMode: preferences.mode }), "compact");
40
42
  });
41
43
 
42
44
  test("full and compact guide content expose README commands and examples", () => {
@@ -63,7 +65,10 @@ test("blocking guide dismisses only from its overlay input handler", () => {
63
65
 
64
66
  assert.equal(GUIDE_DISMISS_HINT, "Press any key to continue");
65
67
  assert.match(source, /guideOverlayBlocking = true;[\s\S]*handleInput: \(\) => dismiss\(\),/);
66
- assert.doesNotMatch(source, /scheduleDismissWelcome|welcomeDismissScheduler|welcome-dismiss/);
68
+ // The blocking guide cannot be dismissed by the welcome scheduler: dismissWelcome
69
+ // bails while guideOverlayBlocking is set, so only the overlay's input handler
70
+ // (which clears the flag first) can close it.
71
+ assert.match(source, /function dismissWelcome\(ctx: any\) \{[\s\S]*?if \(guideOverlayBlocking\) return;/);
67
72
  });
68
73
 
69
74
  test("README-derived guide entries stay in the feature map", () => {
@@ -94,9 +99,9 @@ test("guide dismissal persists only the mode and version marker", () => {
94
99
  }
95
100
  });
96
101
 
97
- test("invalid persisted guide settings fall back to the full guide", () => {
102
+ test("invalid persisted guide settings fall back to the compact guide", () => {
98
103
  assert.deepEqual(getGuidePreferences({ selesaiGuide: { mode: "unexpected", lastSeenVersion: 42 } }), {
99
- mode: "full",
104
+ mode: "compact",
100
105
  lastSeenVersion: undefined,
101
106
  });
102
107
  });
@@ -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
- const source = readFileSync(new URL("../index.ts", import.meta.url), "utf-8");
4
+ import { isSupportedSuperShortcut, matchesConfiguredShortcut, shortcutConflictKey } from "../shortcuts.ts";
5
+ import { resolveShortcutConfig } from "../index.ts";
12
6
 
13
- const powerlineShortcutKeys = new Set([
14
- "stashHistory",
15
- "copyEditor",
16
- "cutEditor",
17
- "jumpPreviousUserMessage",
18
- "jumpNextUserMessage",
19
- "jumpPreviousLlmMessage",
20
- "jumpNextLlmMessage",
21
- "jumpChatBottom",
22
- "scrollChatUp",
23
- "scrollChatDown",
24
- "editorStart",
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 rejects plain keys and unsupported command aliases", () => {
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("\x1b[1;10A", "super+shift+up"), true);
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("editor submits follow the fixed chat viewport to bottom", () => {
104
- assert.match(source, /function followSubmittedEditorToBottom\(\): void/);
105
- assert.match(source, /onEditorSubmit: \(\) => followSubmittedEditorToBottom\(\)/);
106
- assert.match(source, /Object\.defineProperty\(editor, "onSubmit"/);
107
- assert.match(source, /followSubmittedEditorToBottom\(\);\n\s+handler\(text\);/);
108
- assert.match(source, /keybindings\.matches\(data, "app\.message\.followUp"\)/);
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
- test("context usage changes repaint from live streaming message usage", () => {
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("extension status changes invalidate powerline status rendering", () => {
137
- assert.match(source, /let forceNextLayoutRecompute = false/);
138
- assert.match(source, /let restoreFooterStatusRepaintHook: \(\(\) => void\) \| null = null/);
139
- assert.match(source, /const requestImmediateStatusRender = \(options: \{ deferDuringTyping\?: boolean \} = \{\}\) => \{/);
140
- assert.match(source, /if \(options\.deferDuringTyping !== false && Date\.now\(\) - lastEditorInputAt < EDITOR_STATUS_DEFER_MS\) \{\n\s+statusRenderScheduler\.schedule\(\);\n\s+return;\n\s+\}/);
141
- assert.match(source, /forceNextLayoutRecompute = true;\n\s+statusRenderScheduler\.cancel\(\);\n\s+statusRenderScheduler\.schedule\(0\);/);
142
- assert.match(source, /const installFooterStatusRepaintHook = \(footerData: ReadonlyFooterDataProvider\) => \{/);
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
- test("powerline shortcuts have terminal-input fallback routing", () => {
207
- assert.match(source, /function getPowerlineShortcutAction\(data: string\): PowerlineShortcutAction \| null/);
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
+ });