@abacus-ai/cli 1.106.25007 → 2.0.0-canary.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.
Files changed (200) hide show
  1. package/.oxlintrc.json +8 -0
  2. package/dist/index.mjs +12603 -0
  3. package/package.json +7 -39
  4. package/resources/abacus.ico +0 -0
  5. package/resources/entitlements.plist +9 -0
  6. package/src/__e2e__/README.md +196 -0
  7. package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
  8. package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
  9. package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
  10. package/src/__e2e__/conversation.e2e.test.tsx +56 -0
  11. package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
  12. package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
  13. package/src/__e2e__/helpers/test-helpers.ts +450 -0
  14. package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
  15. package/src/__e2e__/llm-models.e2e.test.ts +402 -0
  16. package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
  17. package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
  18. package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
  19. package/src/__e2e__/repl.e2e.test.tsx +78 -0
  20. package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
  21. package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
  22. package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
  23. package/src/args.ts +22 -0
  24. package/src/components/__tests__/react-compiler.test.tsx +78 -0
  25. package/src/components/__tests__/status-indicator.test.tsx +403 -0
  26. package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
  27. package/src/components/composer/agent-mode-indicator.tsx +63 -0
  28. package/src/components/composer/bash-runner.tsx +54 -0
  29. package/src/components/composer/commands/default-commands.tsx +615 -0
  30. package/src/components/composer/commands/handler.tsx +59 -0
  31. package/src/components/composer/commands/picker.tsx +273 -0
  32. package/src/components/composer/commands/registry.ts +233 -0
  33. package/src/components/composer/commands/types.ts +33 -0
  34. package/src/components/composer/context.tsx +88 -0
  35. package/src/components/composer/file-mention-picker.tsx +83 -0
  36. package/src/components/composer/help.tsx +44 -0
  37. package/src/components/composer/index.tsx +1006 -0
  38. package/src/components/composer/mentions.ts +57 -0
  39. package/src/components/composer/message-queue.tsx +70 -0
  40. package/src/components/composer/mode-panel.tsx +35 -0
  41. package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
  42. package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
  43. package/src/components/composer/modes/bash-handler.tsx +132 -0
  44. package/src/components/composer/modes/bash-renderer.tsx +175 -0
  45. package/src/components/composer/modes/default-handlers.tsx +33 -0
  46. package/src/components/composer/modes/index.ts +41 -0
  47. package/src/components/composer/modes/types.ts +21 -0
  48. package/src/components/composer/persistent-shell.ts +283 -0
  49. package/src/components/composer/process.ts +65 -0
  50. package/src/components/composer/types.ts +9 -0
  51. package/src/components/composer/use-mention-search.ts +68 -0
  52. package/src/components/error-boundry.tsx +60 -0
  53. package/src/components/exit-message.tsx +29 -0
  54. package/src/components/expanded-view.tsx +74 -0
  55. package/src/components/file-completion.tsx +127 -0
  56. package/src/components/header.tsx +47 -0
  57. package/src/components/logo.tsx +37 -0
  58. package/src/components/segments.tsx +356 -0
  59. package/src/components/status-indicator.tsx +306 -0
  60. package/src/components/tool-group-summary.tsx +263 -0
  61. package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +312 -0
  62. package/src/components/tool-permissions/diff-preview.tsx +355 -0
  63. package/src/components/tool-permissions/index.ts +5 -0
  64. package/src/components/tool-permissions/permission-options.tsx +375 -0
  65. package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
  66. package/src/components/tool-permissions/tool-permission-ui.tsx +398 -0
  67. package/src/components/tools/agent/ask-user-question.tsx +101 -0
  68. package/src/components/tools/agent/enter-plan-mode.tsx +49 -0
  69. package/src/components/tools/agent/exit-plan-mode.tsx +75 -0
  70. package/src/components/tools/agent/handoff-to-main.tsx +27 -0
  71. package/src/components/tools/agent/subagent.tsx +37 -0
  72. package/src/components/tools/agent/todo-write.tsx +104 -0
  73. package/src/components/tools/browser/close-tab.tsx +58 -0
  74. package/src/components/tools/browser/computer.tsx +70 -0
  75. package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
  76. package/src/components/tools/browser/get-tab-content.tsx +51 -0
  77. package/src/components/tools/browser/navigate-to.tsx +59 -0
  78. package/src/components/tools/browser/new-tab.tsx +60 -0
  79. package/src/components/tools/browser/perform-action.tsx +63 -0
  80. package/src/components/tools/browser/refresh-tab.tsx +43 -0
  81. package/src/components/tools/browser/switch-tab.tsx +58 -0
  82. package/src/components/tools/filesystem/delete-file.tsx +104 -0
  83. package/src/components/tools/filesystem/edit.tsx +220 -0
  84. package/src/components/tools/filesystem/list-dir.tsx +78 -0
  85. package/src/components/tools/filesystem/read-file.tsx +180 -0
  86. package/src/components/tools/filesystem/upload-image.tsx +76 -0
  87. package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
  88. package/src/components/tools/index.ts +91 -0
  89. package/src/components/tools/mcp/mcp-tool.tsx +158 -0
  90. package/src/components/tools/search/fetch-url.tsx +73 -0
  91. package/src/components/tools/search/file-search.tsx +78 -0
  92. package/src/components/tools/search/grep.tsx +90 -0
  93. package/src/components/tools/search/semantic-search.tsx +66 -0
  94. package/src/components/tools/search/web-search.tsx +71 -0
  95. package/src/components/tools/shared/index.tsx +48 -0
  96. package/src/components/tools/shared/zod-coercion.ts +35 -0
  97. package/src/components/tools/terminal/bash-tool-output.tsx +174 -0
  98. package/src/components/tools/terminal/get-terminal-output.tsx +85 -0
  99. package/src/components/tools/terminal/run-in-terminal.tsx +106 -0
  100. package/src/components/tools/types.ts +16 -0
  101. package/src/components/tools.tsx +66 -0
  102. package/src/components/ui/__tests__/divider.test.tsx +61 -0
  103. package/src/components/ui/__tests__/gradient.test.tsx +125 -0
  104. package/src/components/ui/__tests__/input.test.tsx +166 -0
  105. package/src/components/ui/__tests__/select.test.tsx +273 -0
  106. package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
  107. package/src/components/ui/blinking-indicator.tsx +25 -0
  108. package/src/components/ui/divider.tsx +162 -0
  109. package/src/components/ui/gradient.tsx +56 -0
  110. package/src/components/ui/input.tsx +228 -0
  111. package/src/components/ui/select.tsx +151 -0
  112. package/src/components/ui/shimmer.tsx +84 -0
  113. package/src/context/agent-mode.tsx +95 -0
  114. package/src/context/extension-file.tsx +136 -0
  115. package/src/context/network-activity.tsx +45 -0
  116. package/src/context/notification.tsx +62 -0
  117. package/src/context/shell-size.tsx +49 -0
  118. package/src/context/shell-title.tsx +38 -0
  119. package/src/entrypoints/print-mode.ts +312 -0
  120. package/src/entrypoints/repl.tsx +401 -0
  121. package/src/hooks/use-agent.ts +15 -0
  122. package/src/hooks/use-api-client.ts +1 -0
  123. package/src/hooks/use-available-height.ts +8 -0
  124. package/src/hooks/use-cleanup.ts +29 -0
  125. package/src/hooks/use-interrupt-manager.ts +242 -0
  126. package/src/hooks/use-models.ts +22 -0
  127. package/src/index.ts +217 -0
  128. package/src/lib/__tests__/ansi.test.ts +255 -0
  129. package/src/lib/__tests__/cli.test.ts +122 -0
  130. package/src/lib/__tests__/commands.test.ts +325 -0
  131. package/src/lib/__tests__/constants.test.ts +15 -0
  132. package/src/lib/__tests__/focusables.test.ts +25 -0
  133. package/src/lib/__tests__/fs.test.ts +231 -0
  134. package/src/lib/__tests__/markdown.test.tsx +348 -0
  135. package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
  136. package/src/lib/__tests__/mcpManagement.test.ts +38 -0
  137. package/src/lib/__tests__/path-paste.test.ts +144 -0
  138. package/src/lib/__tests__/path.test.ts +300 -0
  139. package/src/lib/__tests__/queries.test.ts +39 -0
  140. package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
  141. package/src/lib/__tests__/text-buffer.test.ts +328 -0
  142. package/src/lib/__tests__/text-utils.test.ts +32 -0
  143. package/src/lib/__tests__/timing.test.ts +78 -0
  144. package/src/lib/__tests__/utils.test.ts +238 -0
  145. package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
  146. package/src/lib/ansi.ts +150 -0
  147. package/src/lib/cli-push-server.ts +112 -0
  148. package/src/lib/cli.ts +44 -0
  149. package/src/lib/clipboard.ts +226 -0
  150. package/src/lib/command-utils.ts +93 -0
  151. package/src/lib/commands.ts +270 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/extension-connection.ts +181 -0
  154. package/src/lib/focusables.ts +7 -0
  155. package/src/lib/fs.ts +533 -0
  156. package/src/lib/markdown/code-block.tsx +63 -0
  157. package/src/lib/markdown/index.ts +4 -0
  158. package/src/lib/markdown/link.tsx +19 -0
  159. package/src/lib/markdown/markdown.tsx +372 -0
  160. package/src/lib/markdown/types.ts +15 -0
  161. package/src/lib/mcpCommandHandler.ts +121 -0
  162. package/src/lib/mcpManagement.ts +44 -0
  163. package/src/lib/path-paste.ts +185 -0
  164. package/src/lib/path.ts +179 -0
  165. package/src/lib/queries.ts +15 -0
  166. package/src/lib/standaloneMcpService.ts +688 -0
  167. package/src/lib/status-utils.ts +237 -0
  168. package/src/lib/test-utils.tsx +72 -0
  169. package/src/lib/text-buffer.ts +2415 -0
  170. package/src/lib/text-utils.ts +272 -0
  171. package/src/lib/timing.ts +63 -0
  172. package/src/lib/types.ts +295 -0
  173. package/src/lib/utils.ts +182 -0
  174. package/src/lib/vim-buffer-actions.ts +732 -0
  175. package/src/providers/agent.tsx +1075 -0
  176. package/src/providers/api-client.tsx +43 -0
  177. package/src/services/logger.ts +85 -0
  178. package/src/terminal/detection.ts +187 -0
  179. package/src/terminal/exit.ts +279 -0
  180. package/src/terminal/notification.ts +83 -0
  181. package/src/terminal/progress.ts +201 -0
  182. package/src/terminal/setup.ts +797 -0
  183. package/src/terminal/suspend.ts +58 -0
  184. package/src/terminal/types.ts +51 -0
  185. package/src/theme/context.tsx +57 -0
  186. package/src/theme/index.ts +4 -0
  187. package/src/theme/themed.tsx +35 -0
  188. package/src/theme/themes.json +546 -0
  189. package/src/theme/types.ts +110 -0
  190. package/src/tools/types.ts +59 -0
  191. package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
  192. package/src/tools/utils/tool-ui-components.tsx +631 -0
  193. package/src/tools/utils/zod-coercion.ts +35 -0
  194. package/tsconfig.json +11 -0
  195. package/tsconfig.node.json +29 -0
  196. package/tsconfig.test.json +27 -0
  197. package/tsdown.config.ts +17 -0
  198. package/vitest.config.ts +76 -0
  199. package/README.md +0 -28
  200. package/dist/index.js +0 -26
@@ -0,0 +1,125 @@
1
+ import { Text } from "@codellm/jar";
2
+ import stripAnsi from "strip-ansi";
3
+ import { describe, expect } from "vitest";
4
+
5
+ // TODO: re-enable when jar testing utilities are available
6
+ import { render, logInk, cleanup } from "../../../lib/test-utils.js";
7
+ import { Gradient } from "../gradient.js";
8
+
9
+ describe.concurrent("Gradient", () => {
10
+ afterEach(() => {
11
+ cleanup();
12
+ });
13
+
14
+ it("should render text with a named gradient", () => {
15
+ const instance = render(
16
+ <Gradient name="rainbow">
17
+ <Text>Hello World</Text>
18
+ </Gradient>,
19
+ );
20
+
21
+ logInk(instance);
22
+
23
+ const output = instance.frames.join("");
24
+ const plainText = "Hello World";
25
+
26
+ expect(output).toBeDefined();
27
+ expect(typeof output).toBe("string");
28
+ // The gradient should apply ANSI color codes to the text
29
+ expect(stripAnsi(output)).toContain(plainText);
30
+ // Verify that ANSI codes are present (output should be longer than plain text)
31
+ expect(output.length).toBeGreaterThan(plainText.length);
32
+ // Verify ANSI escape sequences are present (should contain escape character)
33
+ expect(output).toMatch(new RegExp(String.fromCharCode(27) + "\\["));
34
+ });
35
+
36
+ it("should render text with custom colors", () => {
37
+ const instance = render(
38
+ <Gradient colors={["#ff0000", "#0000ff"]}>
39
+ <Text>Test Text</Text>
40
+ </Gradient>,
41
+ );
42
+
43
+ logInk(instance);
44
+
45
+ const output = instance.frames.join("");
46
+ const plainText = "Test Text";
47
+
48
+ expect(output).toBeDefined();
49
+ expect(stripAnsi(output)).toContain(plainText);
50
+ // Verify that ANSI codes are present (output should be longer than plain text)
51
+ expect(output.length).toBeGreaterThan(plainText.length);
52
+ // Verify ANSI escape sequences are present
53
+ expect(output).toMatch(new RegExp(String.fromCharCode(27) + "\\["));
54
+ });
55
+
56
+ const gradientNames = [
57
+ "atlas",
58
+ "cristal",
59
+ "teen",
60
+ "mind",
61
+ "morning",
62
+ "vice",
63
+ "passion",
64
+ "fruit",
65
+ "instagram",
66
+ "retro",
67
+ "summer",
68
+ "pastel",
69
+ "rainbow",
70
+ ] as const;
71
+ for (const name of gradientNames) {
72
+ it(`should handle gradient name: ${name}`, () => {
73
+ const instance = render(
74
+ <Gradient name={name}>
75
+ <Text>Test Text</Text>
76
+ </Gradient>,
77
+ );
78
+
79
+ logInk(instance);
80
+
81
+ const output = instance.frames.join("");
82
+ const plainText = "Test Text";
83
+
84
+ expect(output).toBeDefined();
85
+ expect(stripAnsi(output)).toContain(plainText);
86
+ // Verify that ANSI codes are present
87
+ expect(output.length).toBeGreaterThan(plainText.length);
88
+ expect(output).toMatch(new RegExp(String.fromCharCode(27) + "\\["));
89
+ });
90
+ }
91
+
92
+ it("should handle multiline text", () => {
93
+ const instance = render(
94
+ <Gradient name="rainbow">
95
+ <Text>Line 1{"\n"}Line 2</Text>
96
+ </Gradient>,
97
+ );
98
+
99
+ logInk(instance);
100
+
101
+ const output = instance.frames.join("");
102
+
103
+ expect(output).toBeDefined();
104
+ expect(stripAnsi(output)).toContain("Line 1");
105
+ expect(stripAnsi(output)).toContain("Line 2");
106
+ // Verify that ANSI codes are present
107
+ expect(output).toMatch(new RegExp(String.fromCharCode(27) + "\\["));
108
+ });
109
+
110
+ it("should handle empty text", () => {
111
+ const instance = render(
112
+ <Gradient name="rainbow">
113
+ <Text></Text>
114
+ </Gradient>,
115
+ );
116
+
117
+ logInk(instance);
118
+
119
+ const output = instance.frames.join("");
120
+
121
+ expect(output).toBeDefined();
122
+ // Empty text should still be valid (may or may not have ANSI codes)
123
+ expect(typeof output).toBe("string");
124
+ });
125
+ });
@@ -0,0 +1,166 @@
1
+ import { createRef } from "react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { stripAnsi } from "../../../lib/ansi.js";
5
+ import { render, logInk, cleanup } from "../../../lib/test-utils.js";
6
+ import { Input } from "../input.js";
7
+
8
+ describe.concurrent("Input", () => {
9
+ const TestWrapper = ({ children }: React.PropsWithChildren) => <>{children}</>;
10
+
11
+ afterEach(() => {
12
+ cleanup();
13
+ });
14
+
15
+ it("should render with placeholder", () => {
16
+ const instance = render(
17
+ <TestWrapper>
18
+ <Input placeholder="Enter text..." />
19
+ </TestWrapper>,
20
+ );
21
+
22
+ logInk(instance);
23
+
24
+ const output = stripAnsi(instance.frames.join(""));
25
+
26
+ expect(output).toBeDefined();
27
+ expect(typeof output).toBe("string");
28
+ expect(output).toContain("Enter text...");
29
+ });
30
+
31
+ it("should render with initial value", () => {
32
+ const instance = render(
33
+ <TestWrapper>
34
+ <Input initialValue="Hello" />
35
+ </TestWrapper>,
36
+ );
37
+
38
+ logInk(instance);
39
+
40
+ const output = stripAnsi(instance.frames.join(""));
41
+
42
+ expect(output).toBeDefined();
43
+ expect(output).toContain("Hello");
44
+ });
45
+
46
+ it("should call onChange when value changes", () => {
47
+ const onChange = vi.fn();
48
+ render(
49
+ <TestWrapper>
50
+ <Input onChange={onChange} />
51
+ </TestWrapper>,
52
+ );
53
+
54
+ // Note: Testing actual keypress events would require more complex setup
55
+ // This test verifies the component renders correctly with the onChange prop
56
+ expect(onChange).toBeDefined();
57
+ });
58
+
59
+ it("should call onSubmit when enter is pressed", () => {
60
+ const onSubmit = vi.fn();
61
+ render(
62
+ <TestWrapper>
63
+ <Input initialValue="test" onSubmit={onSubmit} />
64
+ </TestWrapper>,
65
+ );
66
+
67
+ // Note: Testing actual keypress events would require more complex setup
68
+ // This test verifies the component renders correctly with the onSubmit prop
69
+ expect(onSubmit).toBeDefined();
70
+ });
71
+
72
+ it("should handle ref", () => {
73
+ const ref = createRef<any>();
74
+ render(
75
+ <TestWrapper>
76
+ <Input ref={ref} />
77
+ </TestWrapper>,
78
+ );
79
+
80
+ // The ref should be set after render
81
+ expect(ref.current).toBeDefined();
82
+ });
83
+
84
+ it("should render with custom placeholder props", () => {
85
+ const instance = render(
86
+ <TestWrapper>
87
+ <Input placeholder="Custom" placeholderProps={{ color: "blue" }} />
88
+ </TestWrapper>,
89
+ );
90
+
91
+ logInk(instance);
92
+
93
+ const output = stripAnsi(instance.frames.join(""));
94
+
95
+ expect(output).toBeDefined();
96
+ expect(output).toContain("Custom");
97
+ });
98
+
99
+ it("should handle disabled state", () => {
100
+ const instance = render(
101
+ <TestWrapper>
102
+ <Input disabled placeholder="Disabled input" />
103
+ </TestWrapper>,
104
+ );
105
+
106
+ logInk(instance);
107
+
108
+ const output = stripAnsi(instance.frames.join(""));
109
+
110
+ expect(output).toBeDefined();
111
+ expect(output).toContain("Disabled input");
112
+ });
113
+
114
+ it("should handle grammar highlighting", () => {
115
+ const grammar = [
116
+ { regex: /\d+/g, color: "red" },
117
+ { regex: /[a-z]+/g, color: "blue" },
118
+ ];
119
+
120
+ const instance = render(
121
+ <TestWrapper>
122
+ <Input initialValue="test123" grammar={grammar} />
123
+ </TestWrapper>,
124
+ );
125
+
126
+ logInk(instance);
127
+
128
+ const output = stripAnsi(instance.frames.join(""));
129
+
130
+ expect(output).toBeDefined();
131
+ expect(output).toContain("test");
132
+ expect(output).toContain("123");
133
+ });
134
+
135
+ it("should handle onKeyDown callback", () => {
136
+ const onKeyDown = vi.fn();
137
+ render(
138
+ <TestWrapper>
139
+ <Input onKeyDown={onKeyDown} />
140
+ </TestWrapper>,
141
+ );
142
+
143
+ // Note: Testing actual keypress events would require more complex setup
144
+ // This test verifies the component renders correctly with the onKeyDown prop
145
+ expect(onKeyDown).toBeDefined();
146
+ });
147
+
148
+ it("should handle width and height constraints", () => {
149
+ const instance = render(
150
+ <TestWrapper>
151
+ <Input
152
+ initialValue="This is a long text that should be constrained"
153
+ width={20}
154
+ height={5}
155
+ />
156
+ </TestWrapper>,
157
+ );
158
+
159
+ logInk(instance);
160
+
161
+ const output = stripAnsi(instance.frames.join(""));
162
+
163
+ expect(output).toBeDefined();
164
+ expect(output).toContain("This is a long");
165
+ });
166
+ });
@@ -0,0 +1,273 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import { cleanup, render, logInk } from "../../../lib/test-utils.js";
4
+ import { Select } from "../select.js";
5
+
6
+ describe.concurrent("Select", () => {
7
+ const TestWrapper = ({ children }: React.PropsWithChildren) => <>{children}</>;
8
+
9
+ afterEach(() => {
10
+ cleanup();
11
+ });
12
+
13
+ it("should render items", () => {
14
+ const items = ["Item 1", "Item 2", "Item 3"];
15
+ const instance = render(
16
+ <TestWrapper>
17
+ <Select items={items} />
18
+ </TestWrapper>,
19
+ );
20
+
21
+ logInk(instance);
22
+
23
+ const output = instance.frames.join("");
24
+
25
+ expect(output).toBeDefined();
26
+ expect(typeof output).toBe("string");
27
+ expect(output).toContain("Item 1");
28
+ expect(output).toContain("Item 2");
29
+ expect(output).toContain("Item 3");
30
+ });
31
+
32
+ it("should highlight the first item by default", () => {
33
+ const items = ["First", "Second", "Third"];
34
+ const instance = render(
35
+ <TestWrapper>
36
+ <Select items={items} />
37
+ </TestWrapper>,
38
+ );
39
+
40
+ logInk(instance);
41
+
42
+ const output = instance.frames.join("");
43
+
44
+ expect(output).toBeDefined();
45
+ // The selected item should have a '>' prefix
46
+ expect(output).toContain("> First");
47
+ });
48
+
49
+ it("should use initialIndex to set initial selection", () => {
50
+ const items = ["First", "Second", "Third"];
51
+ const instance = render(
52
+ <TestWrapper>
53
+ <Select items={items} initialIndex={1} />
54
+ </TestWrapper>,
55
+ );
56
+
57
+ logInk(instance);
58
+
59
+ const output = instance.frames.join("");
60
+
61
+ expect(output).toBeDefined();
62
+ expect(output).toContain("> Second");
63
+ });
64
+
65
+ it('should show "No matches" when items array is empty', () => {
66
+ const instance = render(
67
+ <TestWrapper>
68
+ <Select items={[]} />
69
+ </TestWrapper>,
70
+ );
71
+
72
+ logInk(instance);
73
+
74
+ const output = instance.frames.join("");
75
+
76
+ expect(output).toBeDefined();
77
+ expect(output).toContain("No matches");
78
+ });
79
+
80
+ it("should respect limit prop", () => {
81
+ const items = Array.from({ length: 20 }, (_, i) => `Item ${i + 1}`);
82
+ const instance = render(
83
+ <TestWrapper>
84
+ <Select items={items} limit={5} />
85
+ </TestWrapper>,
86
+ );
87
+
88
+ logInk(instance);
89
+
90
+ const output = instance.frames.join("");
91
+
92
+ expect(output).toBeDefined();
93
+ // Should only show up to limit items
94
+ expect(output).toContain("Item 1");
95
+ expect(output).toContain("Item 5");
96
+ // Should not show items beyond limit
97
+ expect(output).not.toContain("Item 6");
98
+ });
99
+
100
+ it("should show index when showIndex is true", () => {
101
+ const items = ["Item 1", "Item 2", "Item 3"];
102
+ const instance = render(
103
+ <TestWrapper>
104
+ <Select items={items} showIndex={true} />
105
+ </TestWrapper>,
106
+ );
107
+
108
+ logInk(instance);
109
+
110
+ const output = instance.frames.join("");
111
+
112
+ expect(output).toBeDefined();
113
+ // Should show index like (1/3)
114
+ expect(output).toContain("(1/3)");
115
+ });
116
+
117
+ it("should not show index when showIndex is false", () => {
118
+ const items = ["Item 1", "Item 2", "Item 3"];
119
+ const instance = render(
120
+ <TestWrapper>
121
+ <Select items={items} showIndex={false} />
122
+ </TestWrapper>,
123
+ );
124
+
125
+ logInk(instance);
126
+
127
+ const output = instance.frames.join("");
128
+
129
+ expect(output).toBeDefined();
130
+ // Should not show index
131
+ expect(output).not.toContain("(1/3)");
132
+ });
133
+
134
+ it("should show indicators when showIndicators is true and there are more items", () => {
135
+ const items = Array.from({ length: 15 }, (_, i) => `Item ${i + 1}`);
136
+ const instance = render(
137
+ <TestWrapper>
138
+ <Select items={items} limit={5} showIndicators={true} />
139
+ </TestWrapper>,
140
+ );
141
+
142
+ logInk(instance);
143
+
144
+ const output = instance.frames.join("");
145
+
146
+ expect(output).toBeDefined();
147
+ // Should show scroll indicators when there are more items
148
+ expect(output).toContain("▼");
149
+ });
150
+
151
+ it("should call onHighlight when selection changes", () => {
152
+ const onHighlight = vi.fn();
153
+ const items = ["Item 1", "Item 2", "Item 3"];
154
+ render(
155
+ <TestWrapper>
156
+ <Select items={items} onHighlight={onHighlight} />
157
+ </TestWrapper>,
158
+ );
159
+
160
+ // onHighlight should be called with the initial selection
161
+ // Note: Testing actual keypress events would require more complex setup
162
+ expect(onHighlight).toBeDefined();
163
+ });
164
+
165
+ it("should call onSubmit when enter is pressed", () => {
166
+ const onSubmit = vi.fn();
167
+ const items = ["Item 1", "Item 2", "Item 3"];
168
+ render(
169
+ <TestWrapper>
170
+ <Select items={items} onSubmit={onSubmit} />
171
+ </TestWrapper>,
172
+ );
173
+
174
+ // Note: Testing actual keypress events would require more complex setup
175
+ // This test verifies the component renders correctly with the onSubmit prop
176
+ expect(onSubmit).toBeDefined();
177
+ });
178
+
179
+ it("should call onCancel when escape is pressed", () => {
180
+ const onCancel = vi.fn();
181
+ const items = ["Item 1", "Item 2", "Item 3"];
182
+ render(
183
+ <TestWrapper>
184
+ <Select items={items} onCancel={onCancel} />
185
+ </TestWrapper>,
186
+ );
187
+
188
+ // Note: Testing actual keypress events would require more complex setup
189
+ // This test verifies the component renders correctly with the onCancel prop
190
+ expect(onCancel).toBeDefined();
191
+ });
192
+
193
+ it("should call onTab when tab is pressed", () => {
194
+ const onTab = vi.fn();
195
+ const items = ["Item 1", "Item 2", "Item 3"];
196
+ render(
197
+ <TestWrapper>
198
+ <Select items={items} onTab={onTab} />
199
+ </TestWrapper>,
200
+ );
201
+
202
+ // Note: Testing actual keypress events would require more complex setup
203
+ // This test verifies the component renders correctly with the onTab prop
204
+ expect(onTab).toBeDefined();
205
+ });
206
+
207
+ it("should respect isActive prop", () => {
208
+ const items = ["Item 1", "Item 2", "Item 3"];
209
+ const instance = render(
210
+ <TestWrapper>
211
+ <Select items={items} isActive={false} />
212
+ </TestWrapper>,
213
+ );
214
+
215
+ logInk(instance);
216
+
217
+ const output = instance.frames.join("");
218
+
219
+ expect(output).toBeDefined();
220
+ expect(output).toContain("Item 1");
221
+ });
222
+
223
+ it("should handle maxHeight constraint", () => {
224
+ const items = Array.from({ length: 20 }, (_, i) => `Item ${i + 1}`);
225
+ const instance = render(
226
+ <TestWrapper>
227
+ <Select items={items} limit={10} maxHeight={5} />
228
+ </TestWrapper>,
229
+ );
230
+
231
+ logInk(instance);
232
+
233
+ const output = instance.frames.join("");
234
+
235
+ expect(output).toBeDefined();
236
+ // Should respect maxHeight constraint
237
+ expect(output).toContain("Item 1");
238
+ });
239
+
240
+ it("should clamp initialIndex to valid range", () => {
241
+ const items = ["Item 1", "Item 2", "Item 3"];
242
+ const instance = render(
243
+ <TestWrapper>
244
+ <Select items={items} initialIndex={10} />
245
+ </TestWrapper>,
246
+ );
247
+
248
+ logInk(instance);
249
+
250
+ const output = instance.frames.join("");
251
+
252
+ expect(output).toBeDefined();
253
+ // Should clamp to last item (index 2)
254
+ expect(output).toContain("> Item 3");
255
+ });
256
+
257
+ it("should handle negative initialIndex", () => {
258
+ const items = ["Item 1", "Item 2", "Item 3"];
259
+ const instance = render(
260
+ <TestWrapper>
261
+ <Select items={items} initialIndex={-1} />
262
+ </TestWrapper>,
263
+ );
264
+
265
+ logInk(instance);
266
+
267
+ const output = instance.frames.join("");
268
+
269
+ expect(output).toBeDefined();
270
+ // Should clamp to first item (index 0)
271
+ expect(output).toContain("> Item 1");
272
+ });
273
+ });
@@ -0,0 +1,99 @@
1
+ import stripAnsi from "strip-ansi";
2
+ import { describe, expect } from "vitest";
3
+
4
+ import { render, logInk, cleanup } from "../../../lib/test-utils.js";
5
+ import { Shimmer } from "../shimmer.js";
6
+
7
+ describe.concurrent("Shimmer", () => {
8
+ afterEach(() => {
9
+ cleanup();
10
+ });
11
+
12
+ it("should render text with shimmer effect", () => {
13
+ const instance = render(<Shimmer text="Hello World Shimmer" />);
14
+
15
+ logInk(instance);
16
+
17
+ const output = instance.frames.join("");
18
+ const plainText = "Hello World Shimmer";
19
+
20
+ expect(output).toBeDefined();
21
+ expect(typeof output).toBe("string");
22
+ expect(stripAnsi(output)).toContain(plainText);
23
+ // Verify that ANSI codes are present (output should be longer than plain text)
24
+ expect(output.length).toBeGreaterThan(plainText.length);
25
+ // Verify ANSI escape sequences are present
26
+ expect(output).toMatch(new RegExp(String.fromCharCode(27) + "\\["));
27
+ });
28
+
29
+ it("should render with custom color", () => {
30
+ const instance = render(<Shimmer text="Test" color="#ff0000" />);
31
+
32
+ logInk(instance);
33
+
34
+ const output = instance.frames.join("");
35
+ const plainText = "Test";
36
+
37
+ expect(output).toBeDefined();
38
+ expect(stripAnsi(output)).toContain(plainText);
39
+ // Verify that ANSI codes are present
40
+ expect(output.length).toBeGreaterThan(plainText.length);
41
+ expect(output).toMatch(new RegExp(String.fromCharCode(27) + "\\["));
42
+ });
43
+
44
+ it("should handle empty text", () => {
45
+ const instance = render(<Shimmer text="" />);
46
+
47
+ logInk(instance);
48
+
49
+ const output = instance.frames.join("");
50
+
51
+ expect(output).toBeDefined();
52
+ });
53
+
54
+ it("should handle single character", () => {
55
+ const instance = render(<Shimmer text="A" />);
56
+
57
+ logInk(instance);
58
+
59
+ const output = instance.frames.join("");
60
+ const plainText = "A";
61
+
62
+ expect(output).toBeDefined();
63
+ expect(stripAnsi(output)).toContain(plainText);
64
+ // Verify that ANSI codes are present
65
+ expect(output.length).toBeGreaterThan(plainText.length);
66
+ expect(output).toMatch(new RegExp(String.fromCharCode(27) + "\\["));
67
+ });
68
+
69
+ it("should handle long text", () => {
70
+ const longText =
71
+ "This is a very long text that should still render properly with the shimmer effect";
72
+ const instance = render(<Shimmer text={longText} />);
73
+
74
+ logInk(instance);
75
+
76
+ const output = instance.frames.join("");
77
+
78
+ expect(output).toBeDefined();
79
+ expect(stripAnsi(output)).toContain(longText);
80
+ // Verify that ANSI codes are present
81
+ expect(output.length).toBeGreaterThan(longText.length);
82
+ expect(output).toMatch(new RegExp(String.fromCharCode(27) + "\\["));
83
+ });
84
+
85
+ it("should use default color when not provided", () => {
86
+ const instance = render(<Shimmer text="Default Shimmer" />);
87
+
88
+ logInk(instance);
89
+
90
+ const output = instance.frames.join("");
91
+ const plainText = "Default Shimmer";
92
+
93
+ expect(output).toBeDefined();
94
+ expect(stripAnsi(output)).toContain(plainText);
95
+ // Verify that ANSI codes are present
96
+ expect(output.length).toBeGreaterThan(plainText.length);
97
+ expect(output).toMatch(new RegExp(String.fromCharCode(27) + "\\["));
98
+ });
99
+ });
@@ -0,0 +1,25 @@
1
+ import { Text } from "@codellm/jar";
2
+ import { memo, useEffect, useState } from "react";
3
+
4
+ interface BlinkingIndicatorProps {
5
+ char: string;
6
+ color?: string;
7
+ }
8
+
9
+ const BLINK_INTERVAL = 500; // ms
10
+
11
+ export const BlinkingIndicator = memo(({ char, color }: BlinkingIndicatorProps) => {
12
+ const [visible, setVisible] = useState(true);
13
+
14
+ useEffect(() => {
15
+ const interval = setInterval(() => {
16
+ setVisible((prev) => !prev);
17
+ }, BLINK_INTERVAL);
18
+
19
+ return () => clearInterval(interval);
20
+ }, []);
21
+
22
+ return <Text color={color}>{visible ? char : " "}</Text>;
23
+ });
24
+
25
+ BlinkingIndicator.displayName = "BlinkingIndicator";