@oh-my-pi/pi-coding-agent 1.337.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 (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. package/src/utils/tools-manager.ts +274 -0
@@ -0,0 +1,204 @@
1
+ import {
2
+ type Component,
3
+ Container,
4
+ Input,
5
+ isArrowDown,
6
+ isArrowUp,
7
+ isCtrlC,
8
+ isEnter,
9
+ isEscape,
10
+ Spacer,
11
+ Text,
12
+ truncateToWidth,
13
+ } from "@oh-my-pi/pi-tui";
14
+ import type { SessionInfo } from "../../../core/session-manager.js";
15
+ import { fuzzyFilter } from "../../../utils/fuzzy.js";
16
+ import { theme } from "../theme/theme.js";
17
+ import { DynamicBorder } from "./dynamic-border.js";
18
+
19
+ /**
20
+ * Custom session list component with multi-line items and search
21
+ */
22
+ class SessionList implements Component {
23
+ private allSessions: SessionInfo[] = [];
24
+ private filteredSessions: SessionInfo[] = [];
25
+ private selectedIndex: number = 0;
26
+ private searchInput: Input;
27
+ public onSelect?: (sessionPath: string) => void;
28
+ public onCancel?: () => void;
29
+ public onExit: () => void = () => {};
30
+ private maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)
31
+
32
+ constructor(sessions: SessionInfo[]) {
33
+ this.allSessions = sessions;
34
+ this.filteredSessions = sessions;
35
+ this.searchInput = new Input();
36
+
37
+ // Handle Enter in search input - select current item
38
+ this.searchInput.onSubmit = () => {
39
+ if (this.filteredSessions[this.selectedIndex]) {
40
+ const selected = this.filteredSessions[this.selectedIndex];
41
+ if (this.onSelect) {
42
+ this.onSelect(selected.path);
43
+ }
44
+ }
45
+ };
46
+ }
47
+
48
+ private filterSessions(query: string): void {
49
+ this.filteredSessions = fuzzyFilter(this.allSessions, query, (session) => session.allMessagesText);
50
+ this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));
51
+ }
52
+
53
+ invalidate(): void {
54
+ // No cached state to invalidate currently
55
+ }
56
+
57
+ render(width: number): string[] {
58
+ const lines: string[] = [];
59
+
60
+ // Render search input
61
+ lines.push(...this.searchInput.render(width));
62
+ lines.push(""); // Blank line after search
63
+
64
+ if (this.filteredSessions.length === 0) {
65
+ lines.push(theme.fg("muted", " No sessions found"));
66
+ return lines;
67
+ }
68
+
69
+ // Format dates
70
+ const formatDate = (date: Date): string => {
71
+ const now = new Date();
72
+ const diffMs = now.getTime() - date.getTime();
73
+ const diffMins = Math.floor(diffMs / 60000);
74
+ const diffHours = Math.floor(diffMs / 3600000);
75
+ const diffDays = Math.floor(diffMs / 86400000);
76
+
77
+ if (diffMins < 1) return "just now";
78
+ if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? "s" : ""} ago`;
79
+ if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`;
80
+ if (diffDays === 1) return "1 day ago";
81
+ if (diffDays < 7) return `${diffDays} days ago`;
82
+
83
+ return date.toLocaleDateString();
84
+ };
85
+
86
+ // Calculate visible range with scrolling
87
+ const startIndex = Math.max(
88
+ 0,
89
+ Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredSessions.length - this.maxVisible),
90
+ );
91
+ const endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);
92
+
93
+ // Render visible sessions (2 lines per session + blank line)
94
+ for (let i = startIndex; i < endIndex; i++) {
95
+ const session = this.filteredSessions[i];
96
+ const isSelected = i === this.selectedIndex;
97
+
98
+ // Normalize first message to single line
99
+ const normalizedMessage = session.firstMessage.replace(/\n/g, " ").trim();
100
+
101
+ // First line: cursor + message (truncate to visible width)
102
+ const cursor = isSelected ? theme.fg("accent", "› ") : " ";
103
+ const maxMsgWidth = width - 2; // Account for cursor (2 visible chars)
104
+ const truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth, "...");
105
+ const messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);
106
+
107
+ // Second line: metadata (dimmed) - also truncate for safety
108
+ const modified = formatDate(session.modified);
109
+ const msgCount = `${session.messageCount} message${session.messageCount !== 1 ? "s" : ""}`;
110
+ const metadata = ` ${modified} · ${msgCount}`;
111
+ const metadataLine = theme.fg("dim", truncateToWidth(metadata, width, ""));
112
+
113
+ lines.push(messageLine);
114
+ lines.push(metadataLine);
115
+ lines.push(""); // Blank line between sessions
116
+ }
117
+
118
+ // Add scroll indicator if needed
119
+ if (startIndex > 0 || endIndex < this.filteredSessions.length) {
120
+ const scrollText = ` (${this.selectedIndex + 1}/${this.filteredSessions.length})`;
121
+ const scrollInfo = theme.fg("muted", truncateToWidth(scrollText, width, ""));
122
+ lines.push(scrollInfo);
123
+ }
124
+
125
+ return lines;
126
+ }
127
+
128
+ handleInput(keyData: string): void {
129
+ // Up arrow
130
+ if (isArrowUp(keyData)) {
131
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
132
+ }
133
+ // Down arrow
134
+ else if (isArrowDown(keyData)) {
135
+ this.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);
136
+ }
137
+ // Enter
138
+ else if (isEnter(keyData)) {
139
+ const selected = this.filteredSessions[this.selectedIndex];
140
+ if (selected && this.onSelect) {
141
+ this.onSelect(selected.path);
142
+ }
143
+ }
144
+ // Escape - cancel
145
+ else if (isEscape(keyData)) {
146
+ if (this.onCancel) {
147
+ this.onCancel();
148
+ }
149
+ }
150
+ // Ctrl+C - exit
151
+ else if (isCtrlC(keyData)) {
152
+ this.onExit();
153
+ }
154
+ // Pass everything else to search input
155
+ else {
156
+ this.searchInput.handleInput(keyData);
157
+ this.filterSessions(this.searchInput.getValue());
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Component that renders a session selector
164
+ */
165
+ export class SessionSelectorComponent extends Container {
166
+ private sessionList: SessionList;
167
+
168
+ constructor(
169
+ sessions: SessionInfo[],
170
+ onSelect: (sessionPath: string) => void,
171
+ onCancel: () => void,
172
+ onExit: () => void,
173
+ ) {
174
+ super();
175
+
176
+ // Add header
177
+ this.addChild(new Spacer(1));
178
+ this.addChild(new Text(theme.bold("Resume Session"), 1, 0));
179
+ this.addChild(new Spacer(1));
180
+ this.addChild(new DynamicBorder());
181
+ this.addChild(new Spacer(1));
182
+
183
+ // Create session list
184
+ this.sessionList = new SessionList(sessions);
185
+ this.sessionList.onSelect = onSelect;
186
+ this.sessionList.onCancel = onCancel;
187
+ this.sessionList.onExit = onExit;
188
+
189
+ this.addChild(this.sessionList);
190
+
191
+ // Add bottom border
192
+ this.addChild(new Spacer(1));
193
+ this.addChild(new DynamicBorder());
194
+
195
+ // Auto-cancel if no sessions
196
+ if (sessions.length === 0) {
197
+ setTimeout(() => onCancel(), 100);
198
+ }
199
+ }
200
+
201
+ getSessionList(): SessionList {
202
+ return this.sessionList;
203
+ }
204
+ }
@@ -0,0 +1,453 @@
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
+ import {
3
+ Container,
4
+ getCapabilities,
5
+ isArrowLeft,
6
+ isArrowRight,
7
+ isEscape,
8
+ isShiftTab,
9
+ isTab,
10
+ type SelectItem,
11
+ SelectList,
12
+ type SettingItem,
13
+ SettingsList,
14
+ Spacer,
15
+ type Tab,
16
+ TabBar,
17
+ type TabBarTheme,
18
+ Text,
19
+ } from "@oh-my-pi/pi-tui";
20
+ import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.js";
21
+ import { DynamicBorder } from "./dynamic-border.js";
22
+ import { PluginSettingsComponent } from "./plugin-settings.js";
23
+
24
+ const THINKING_DESCRIPTIONS: Record<ThinkingLevel, string> = {
25
+ off: "No reasoning",
26
+ minimal: "Very brief reasoning (~1k tokens)",
27
+ low: "Light reasoning (~2k tokens)",
28
+ medium: "Moderate reasoning (~8k tokens)",
29
+ high: "Deep reasoning (~16k tokens)",
30
+ xhigh: "Maximum reasoning (~32k tokens)",
31
+ };
32
+
33
+ export interface ExaToolsConfig {
34
+ enabled: boolean;
35
+ enableSearch: boolean;
36
+ enableLinkedin: boolean;
37
+ enableCompany: boolean;
38
+ enableResearcher: boolean;
39
+ enableWebsets: boolean;
40
+ }
41
+
42
+ export interface SettingsConfig {
43
+ autoCompact: boolean;
44
+ showImages: boolean;
45
+ queueMode: "all" | "one-at-a-time";
46
+ thinkingLevel: ThinkingLevel;
47
+ availableThinkingLevels: ThinkingLevel[];
48
+ currentTheme: string;
49
+ availableThemes: string[];
50
+ hideThinkingBlock: boolean;
51
+ collapseChangelog: boolean;
52
+ cwd: string;
53
+ exa: ExaToolsConfig;
54
+ }
55
+
56
+ export interface SettingsCallbacks {
57
+ onAutoCompactChange: (enabled: boolean) => void;
58
+ onShowImagesChange: (enabled: boolean) => void;
59
+ onQueueModeChange: (mode: "all" | "one-at-a-time") => void;
60
+ onThinkingLevelChange: (level: ThinkingLevel) => void;
61
+ onThemeChange: (theme: string) => void;
62
+ onThemePreview?: (theme: string) => void;
63
+ onHideThinkingBlockChange: (hidden: boolean) => void;
64
+ onCollapseChangelogChange: (collapsed: boolean) => void;
65
+ onPluginsChanged?: () => void;
66
+ onExaSettingChange: (setting: keyof ExaToolsConfig, enabled: boolean) => void;
67
+ onCancel: () => void;
68
+ }
69
+
70
+ function getTabBarTheme(): TabBarTheme {
71
+ return {
72
+ label: (text) => theme.bold(theme.fg("accent", text)),
73
+ activeTab: (text) => theme.bold(theme.bg("selectedBg", theme.fg("text", text))),
74
+ inactiveTab: (text) => theme.fg("muted", text),
75
+ hint: (text) => theme.fg("dim", text),
76
+ };
77
+ }
78
+
79
+ /**
80
+ * A submenu component for selecting from a list of options.
81
+ */
82
+ class SelectSubmenu extends Container {
83
+ private selectList: SelectList;
84
+
85
+ constructor(
86
+ title: string,
87
+ description: string,
88
+ options: SelectItem[],
89
+ currentValue: string,
90
+ onSelect: (value: string) => void,
91
+ onCancel: () => void,
92
+ onSelectionChange?: (value: string) => void,
93
+ ) {
94
+ super();
95
+
96
+ // Title
97
+ this.addChild(new Text(theme.bold(theme.fg("accent", title)), 0, 0));
98
+
99
+ // Description
100
+ if (description) {
101
+ this.addChild(new Spacer(1));
102
+ this.addChild(new Text(theme.fg("muted", description), 0, 0));
103
+ }
104
+
105
+ // Spacer
106
+ this.addChild(new Spacer(1));
107
+
108
+ // Select list
109
+ this.selectList = new SelectList(options, Math.min(options.length, 10), getSelectListTheme());
110
+
111
+ // Pre-select current value
112
+ const currentIndex = options.findIndex((o) => o.value === currentValue);
113
+ if (currentIndex !== -1) {
114
+ this.selectList.setSelectedIndex(currentIndex);
115
+ }
116
+
117
+ this.selectList.onSelect = (item) => {
118
+ onSelect(item.value);
119
+ };
120
+
121
+ this.selectList.onCancel = onCancel;
122
+
123
+ if (onSelectionChange) {
124
+ this.selectList.onSelectionChange = (item) => {
125
+ onSelectionChange(item.value);
126
+ };
127
+ }
128
+
129
+ this.addChild(this.selectList);
130
+
131
+ // Hint
132
+ this.addChild(new Spacer(1));
133
+ this.addChild(new Text(theme.fg("dim", " Enter to select · Esc to go back"), 0, 0));
134
+ }
135
+
136
+ handleInput(data: string): void {
137
+ this.selectList.handleInput(data);
138
+ }
139
+ }
140
+
141
+ type TabId = "config" | "exa" | "plugins";
142
+
143
+ const SETTINGS_TABS: Tab[] = [
144
+ { id: "config", label: "Config" },
145
+ { id: "exa", label: "Exa" },
146
+ { id: "plugins", label: "Plugins" },
147
+ ];
148
+
149
+ /**
150
+ * Main tabbed settings selector component.
151
+ */
152
+ export class SettingsSelectorComponent extends Container {
153
+ private tabBar: TabBar;
154
+ private currentList: SettingsList | null = null;
155
+ private currentSubmenu: Container | null = null;
156
+ private pluginComponent: PluginSettingsComponent | null = null;
157
+
158
+ private config: SettingsConfig;
159
+ private callbacks: SettingsCallbacks;
160
+
161
+ constructor(config: SettingsConfig, callbacks: SettingsCallbacks) {
162
+ super();
163
+
164
+ this.config = config;
165
+ this.callbacks = callbacks;
166
+
167
+ // Add top border
168
+ this.addChild(new DynamicBorder());
169
+
170
+ // Tab bar
171
+ this.tabBar = new TabBar("Settings", SETTINGS_TABS, getTabBarTheme());
172
+ this.tabBar.onTabChange = () => {
173
+ this.switchToTab(this.tabBar.getActiveTab().id as TabId);
174
+ };
175
+ this.addChild(this.tabBar);
176
+
177
+ // Spacer after tab bar
178
+ this.addChild(new Spacer(1));
179
+
180
+ // Initialize with first tab
181
+ this.switchToTab("config");
182
+
183
+ // Add bottom border
184
+ this.addChild(new DynamicBorder());
185
+ }
186
+
187
+ private switchToTab(tabId: TabId): void {
188
+ // Remove current content
189
+ if (this.currentList) {
190
+ this.removeChild(this.currentList);
191
+ this.currentList = null;
192
+ }
193
+ if (this.pluginComponent) {
194
+ this.removeChild(this.pluginComponent);
195
+ this.pluginComponent = null;
196
+ }
197
+
198
+ // Remove bottom border temporarily
199
+ const bottomBorder = this.children[this.children.length - 1];
200
+ this.removeChild(bottomBorder);
201
+
202
+ switch (tabId) {
203
+ case "config":
204
+ this.showConfigTab();
205
+ break;
206
+ case "exa":
207
+ this.showExaTab();
208
+ break;
209
+ case "plugins":
210
+ this.showPluginsTab();
211
+ break;
212
+ }
213
+
214
+ // Re-add bottom border
215
+ this.addChild(bottomBorder);
216
+ }
217
+
218
+ private showConfigTab(): void {
219
+ const supportsImages = getCapabilities().images;
220
+
221
+ const items: SettingItem[] = [
222
+ {
223
+ id: "autocompact",
224
+ label: "Auto-compact",
225
+ description: "Automatically compact context when it gets too large",
226
+ currentValue: this.config.autoCompact ? "true" : "false",
227
+ values: ["true", "false"],
228
+ },
229
+ {
230
+ id: "queue-mode",
231
+ label: "Queue mode",
232
+ description: "How to process queued messages while agent is working",
233
+ currentValue: this.config.queueMode,
234
+ values: ["one-at-a-time", "all"],
235
+ },
236
+ {
237
+ id: "hide-thinking",
238
+ label: "Hide thinking",
239
+ description: "Hide thinking blocks in assistant responses",
240
+ currentValue: this.config.hideThinkingBlock ? "true" : "false",
241
+ values: ["true", "false"],
242
+ },
243
+ {
244
+ id: "collapse-changelog",
245
+ label: "Collapse changelog",
246
+ description: "Show condensed changelog after updates",
247
+ currentValue: this.config.collapseChangelog ? "true" : "false",
248
+ values: ["true", "false"],
249
+ },
250
+ {
251
+ id: "thinking",
252
+ label: "Thinking level",
253
+ description: "Reasoning depth for thinking-capable models",
254
+ currentValue: this.config.thinkingLevel,
255
+ submenu: (currentValue, done) =>
256
+ new SelectSubmenu(
257
+ "Thinking Level",
258
+ "Select reasoning depth for thinking-capable models",
259
+ this.config.availableThinkingLevels.map((level) => ({
260
+ value: level,
261
+ label: level,
262
+ description: THINKING_DESCRIPTIONS[level],
263
+ })),
264
+ currentValue,
265
+ (value) => {
266
+ this.callbacks.onThinkingLevelChange(value as ThinkingLevel);
267
+ done(value);
268
+ },
269
+ () => done(),
270
+ ),
271
+ },
272
+ {
273
+ id: "theme",
274
+ label: "Theme",
275
+ description: "Color theme for the interface",
276
+ currentValue: this.config.currentTheme,
277
+ submenu: (currentValue, done) =>
278
+ new SelectSubmenu(
279
+ "Theme",
280
+ "Select color theme",
281
+ this.config.availableThemes.map((t) => ({
282
+ value: t,
283
+ label: t,
284
+ })),
285
+ currentValue,
286
+ (value) => {
287
+ this.callbacks.onThemeChange(value);
288
+ done(value);
289
+ },
290
+ () => {
291
+ this.callbacks.onThemePreview?.(currentValue);
292
+ done();
293
+ },
294
+ (value) => {
295
+ this.callbacks.onThemePreview?.(value);
296
+ },
297
+ ),
298
+ },
299
+ ];
300
+
301
+ // Add image toggle if supported
302
+ if (supportsImages) {
303
+ items.splice(1, 0, {
304
+ id: "show-images",
305
+ label: "Show images",
306
+ description: "Render images inline in terminal",
307
+ currentValue: this.config.showImages ? "true" : "false",
308
+ values: ["true", "false"],
309
+ });
310
+ }
311
+
312
+ this.currentList = new SettingsList(
313
+ items,
314
+ 10,
315
+ getSettingsListTheme(),
316
+ (id, newValue) => {
317
+ switch (id) {
318
+ case "autocompact":
319
+ this.callbacks.onAutoCompactChange(newValue === "true");
320
+ break;
321
+ case "show-images":
322
+ this.callbacks.onShowImagesChange(newValue === "true");
323
+ break;
324
+ case "queue-mode":
325
+ this.callbacks.onQueueModeChange(newValue as "all" | "one-at-a-time");
326
+ break;
327
+ case "hide-thinking":
328
+ this.callbacks.onHideThinkingBlockChange(newValue === "true");
329
+ break;
330
+ case "collapse-changelog":
331
+ this.callbacks.onCollapseChangelogChange(newValue === "true");
332
+ break;
333
+ }
334
+ },
335
+ () => this.callbacks.onCancel(),
336
+ );
337
+
338
+ this.addChild(this.currentList);
339
+ }
340
+
341
+ private showExaTab(): void {
342
+ const items: SettingItem[] = [
343
+ {
344
+ id: "exa-enabled",
345
+ label: "Exa enabled",
346
+ description: "Master toggle for all Exa search tools",
347
+ currentValue: this.config.exa.enabled ? "true" : "false",
348
+ values: ["true", "false"],
349
+ },
350
+ {
351
+ id: "exa-search",
352
+ label: "Exa search",
353
+ description: "Basic search, deep search, code search, crawl",
354
+ currentValue: this.config.exa.enableSearch ? "true" : "false",
355
+ values: ["true", "false"],
356
+ },
357
+ {
358
+ id: "exa-linkedin",
359
+ label: "Exa LinkedIn",
360
+ description: "Search LinkedIn for people and companies",
361
+ currentValue: this.config.exa.enableLinkedin ? "true" : "false",
362
+ values: ["true", "false"],
363
+ },
364
+ {
365
+ id: "exa-company",
366
+ label: "Exa company",
367
+ description: "Comprehensive company research tool",
368
+ currentValue: this.config.exa.enableCompany ? "true" : "false",
369
+ values: ["true", "false"],
370
+ },
371
+ {
372
+ id: "exa-researcher",
373
+ label: "Exa researcher",
374
+ description: "AI-powered deep research tasks",
375
+ currentValue: this.config.exa.enableResearcher ? "true" : "false",
376
+ values: ["true", "false"],
377
+ },
378
+ {
379
+ id: "exa-websets",
380
+ label: "Exa websets",
381
+ description: "Webset management and enrichment tools",
382
+ currentValue: this.config.exa.enableWebsets ? "true" : "false",
383
+ values: ["true", "false"],
384
+ },
385
+ ];
386
+
387
+ this.currentList = new SettingsList(
388
+ items,
389
+ 10,
390
+ getSettingsListTheme(),
391
+ (id, newValue) => {
392
+ const enabled = newValue === "true";
393
+ switch (id) {
394
+ case "exa-enabled":
395
+ this.callbacks.onExaSettingChange("enabled", enabled);
396
+ break;
397
+ case "exa-search":
398
+ this.callbacks.onExaSettingChange("enableSearch", enabled);
399
+ break;
400
+ case "exa-linkedin":
401
+ this.callbacks.onExaSettingChange("enableLinkedin", enabled);
402
+ break;
403
+ case "exa-company":
404
+ this.callbacks.onExaSettingChange("enableCompany", enabled);
405
+ break;
406
+ case "exa-researcher":
407
+ this.callbacks.onExaSettingChange("enableResearcher", enabled);
408
+ break;
409
+ case "exa-websets":
410
+ this.callbacks.onExaSettingChange("enableWebsets", enabled);
411
+ break;
412
+ }
413
+ },
414
+ () => this.callbacks.onCancel(),
415
+ );
416
+
417
+ this.addChild(this.currentList);
418
+ }
419
+
420
+ private showPluginsTab(): void {
421
+ this.pluginComponent = new PluginSettingsComponent(this.config.cwd, {
422
+ onClose: () => this.callbacks.onCancel(),
423
+ onPluginChanged: () => this.callbacks.onPluginsChanged?.(),
424
+ });
425
+ this.addChild(this.pluginComponent);
426
+ }
427
+
428
+ getFocusComponent(): SettingsList | PluginSettingsComponent {
429
+ // Return the current focusable component - one of these will always be set
430
+ return (this.currentList || this.pluginComponent)!;
431
+ }
432
+
433
+ handleInput(data: string): void {
434
+ // Handle tab switching first (tab, shift+tab, or left/right arrows)
435
+ if (isTab(data) || isShiftTab(data) || isArrowLeft(data) || isArrowRight(data)) {
436
+ this.tabBar.handleInput(data);
437
+ return;
438
+ }
439
+
440
+ // Escape at top level cancels
441
+ if (isEscape(data) && !this.currentSubmenu) {
442
+ this.callbacks.onCancel();
443
+ return;
444
+ }
445
+
446
+ // Pass to current content
447
+ if (this.currentList) {
448
+ this.currentList.handleInput(data);
449
+ } else if (this.pluginComponent) {
450
+ this.pluginComponent.handleInput(data);
451
+ }
452
+ }
453
+ }
@@ -0,0 +1,45 @@
1
+ import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
2
+ import { getSelectListTheme } from "../theme/theme.js";
3
+ import { DynamicBorder } from "./dynamic-border.js";
4
+
5
+ /**
6
+ * Component that renders a show images selector with borders
7
+ */
8
+ export class ShowImagesSelectorComponent extends Container {
9
+ private selectList: SelectList;
10
+
11
+ constructor(currentValue: boolean, onSelect: (show: boolean) => void, onCancel: () => void) {
12
+ super();
13
+
14
+ const items: SelectItem[] = [
15
+ { value: "yes", label: "Yes", description: "Show images inline in terminal" },
16
+ { value: "no", label: "No", description: "Show text placeholder instead" },
17
+ ];
18
+
19
+ // Add top border
20
+ this.addChild(new DynamicBorder());
21
+
22
+ // Create selector
23
+ this.selectList = new SelectList(items, 5, getSelectListTheme());
24
+
25
+ // Preselect current value
26
+ this.selectList.setSelectedIndex(currentValue ? 0 : 1);
27
+
28
+ this.selectList.onSelect = (item) => {
29
+ onSelect(item.value === "yes");
30
+ };
31
+
32
+ this.selectList.onCancel = () => {
33
+ onCancel();
34
+ };
35
+
36
+ this.addChild(this.selectList);
37
+
38
+ // Add bottom border
39
+ this.addChild(new DynamicBorder());
40
+ }
41
+
42
+ getSelectList(): SelectList {
43
+ return this.selectList;
44
+ }
45
+ }