@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,159 @@
1
+ import {
2
+ type Component,
3
+ Container,
4
+ isArrowDown,
5
+ isArrowUp,
6
+ isCtrlC,
7
+ isEnter,
8
+ isEscape,
9
+ Spacer,
10
+ Text,
11
+ truncateToWidth,
12
+ } from "@oh-my-pi/pi-tui";
13
+ import { theme } from "../theme/theme.js";
14
+ import { DynamicBorder } from "./dynamic-border.js";
15
+
16
+ interface UserMessageItem {
17
+ id: string; // Entry ID in the session
18
+ text: string; // The message text
19
+ timestamp?: string; // Optional timestamp if available
20
+ }
21
+
22
+ /**
23
+ * Custom user message list component with selection
24
+ */
25
+ class UserMessageList implements Component {
26
+ private messages: UserMessageItem[] = [];
27
+ private selectedIndex: number = 0;
28
+ public onSelect?: (entryId: string) => void;
29
+ public onCancel?: () => void;
30
+ private maxVisible: number = 10; // Max messages visible
31
+
32
+ constructor(messages: UserMessageItem[]) {
33
+ // Store messages in chronological order (oldest to newest)
34
+ this.messages = messages;
35
+ // Start with the last (most recent) message selected
36
+ this.selectedIndex = Math.max(0, messages.length - 1);
37
+ }
38
+
39
+ invalidate(): void {
40
+ // No cached state to invalidate currently
41
+ }
42
+
43
+ render(width: number): string[] {
44
+ const lines: string[] = [];
45
+
46
+ if (this.messages.length === 0) {
47
+ lines.push(theme.fg("muted", " No user messages found"));
48
+ return lines;
49
+ }
50
+
51
+ // Calculate visible range with scrolling
52
+ const startIndex = Math.max(
53
+ 0,
54
+ Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.messages.length - this.maxVisible),
55
+ );
56
+ const endIndex = Math.min(startIndex + this.maxVisible, this.messages.length);
57
+
58
+ // Render visible messages (2 lines per message + blank line)
59
+ for (let i = startIndex; i < endIndex; i++) {
60
+ const message = this.messages[i];
61
+ const isSelected = i === this.selectedIndex;
62
+
63
+ // Normalize message to single line
64
+ const normalizedMessage = message.text.replace(/\n/g, " ").trim();
65
+
66
+ // First line: cursor + message
67
+ const cursor = isSelected ? theme.fg("accent", "› ") : " ";
68
+ const maxMsgWidth = width - 2; // Account for cursor (2 chars)
69
+ const truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth);
70
+ const messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);
71
+
72
+ lines.push(messageLine);
73
+
74
+ // Second line: metadata (position in history)
75
+ const position = i + 1;
76
+ const metadata = ` Message ${position} of ${this.messages.length}`;
77
+ const metadataLine = theme.fg("muted", metadata);
78
+ lines.push(metadataLine);
79
+ lines.push(""); // Blank line between messages
80
+ }
81
+
82
+ // Add scroll indicator if needed
83
+ if (startIndex > 0 || endIndex < this.messages.length) {
84
+ const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.messages.length})`);
85
+ lines.push(scrollInfo);
86
+ }
87
+
88
+ return lines;
89
+ }
90
+
91
+ handleInput(keyData: string): void {
92
+ // Up arrow - go to previous (older) message, wrap to bottom when at top
93
+ if (isArrowUp(keyData)) {
94
+ this.selectedIndex = this.selectedIndex === 0 ? this.messages.length - 1 : this.selectedIndex - 1;
95
+ }
96
+ // Down arrow - go to next (newer) message, wrap to top when at bottom
97
+ else if (isArrowDown(keyData)) {
98
+ this.selectedIndex = this.selectedIndex === this.messages.length - 1 ? 0 : this.selectedIndex + 1;
99
+ }
100
+ // Enter - select message and branch
101
+ else if (isEnter(keyData)) {
102
+ const selected = this.messages[this.selectedIndex];
103
+ if (selected && this.onSelect) {
104
+ this.onSelect(selected.id);
105
+ }
106
+ }
107
+ // Escape - cancel
108
+ else if (isEscape(keyData)) {
109
+ if (this.onCancel) {
110
+ this.onCancel();
111
+ }
112
+ }
113
+ // Ctrl+C - cancel
114
+ else if (isCtrlC(keyData)) {
115
+ if (this.onCancel) {
116
+ this.onCancel();
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Component that renders a user message selector for branching
124
+ */
125
+ export class UserMessageSelectorComponent extends Container {
126
+ private messageList: UserMessageList;
127
+
128
+ constructor(messages: UserMessageItem[], onSelect: (entryId: string) => void, onCancel: () => void) {
129
+ super();
130
+
131
+ // Add header
132
+ this.addChild(new Spacer(1));
133
+ this.addChild(new Text(theme.bold("Branch from Message"), 1, 0));
134
+ this.addChild(new Text(theme.fg("muted", "Select a message to create a new branch from that point"), 1, 0));
135
+ this.addChild(new Spacer(1));
136
+ this.addChild(new DynamicBorder());
137
+ this.addChild(new Spacer(1));
138
+
139
+ // Create message list
140
+ this.messageList = new UserMessageList(messages);
141
+ this.messageList.onSelect = onSelect;
142
+ this.messageList.onCancel = onCancel;
143
+
144
+ this.addChild(this.messageList);
145
+
146
+ // Add bottom border
147
+ this.addChild(new Spacer(1));
148
+ this.addChild(new DynamicBorder());
149
+
150
+ // Auto-cancel if no messages
151
+ if (messages.length === 0) {
152
+ setTimeout(() => onCancel(), 100);
153
+ }
154
+ }
155
+
156
+ getMessageList(): UserMessageList {
157
+ return this.messageList;
158
+ }
159
+ }
@@ -0,0 +1,18 @@
1
+ import { Container, Markdown, Spacer } from "@oh-my-pi/pi-tui";
2
+ import { getMarkdownTheme, theme } from "../theme/theme.js";
3
+
4
+ /**
5
+ * Component that renders a user message
6
+ */
7
+ export class UserMessageComponent extends Container {
8
+ constructor(text: string) {
9
+ super();
10
+ this.addChild(new Spacer(1));
11
+ this.addChild(
12
+ new Markdown(text, 1, 1, getMarkdownTheme(), {
13
+ bgColor: (text: string) => theme.bg("userMessageBg", text),
14
+ color: (text: string) => theme.fg("userMessageText", text),
15
+ }),
16
+ );
17
+ }
18
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Shared utility for truncating text to visual lines (accounting for line wrapping).
3
+ * Used by both tool-execution.ts and bash-execution.ts for consistent behavior.
4
+ */
5
+
6
+ import { Text } from "@oh-my-pi/pi-tui";
7
+
8
+ export interface VisualTruncateResult {
9
+ /** The visual lines to display */
10
+ visualLines: string[];
11
+ /** Number of visual lines that were skipped (hidden) */
12
+ skippedCount: number;
13
+ }
14
+
15
+ /**
16
+ * Truncate text to a maximum number of visual lines (from the end).
17
+ * This accounts for line wrapping based on terminal width.
18
+ *
19
+ * @param text - The text content (may contain newlines)
20
+ * @param maxVisualLines - Maximum number of visual lines to show
21
+ * @param width - Terminal/render width
22
+ * @param paddingX - Horizontal padding for Text component (default 0).
23
+ * Use 0 when result will be placed in a Box (Box adds its own padding).
24
+ * Use 1 when result will be placed in a plain Container.
25
+ * @returns The truncated visual lines and count of skipped lines
26
+ */
27
+ export function truncateToVisualLines(
28
+ text: string,
29
+ maxVisualLines: number,
30
+ width: number,
31
+ paddingX: number = 0,
32
+ ): VisualTruncateResult {
33
+ if (!text) {
34
+ return { visualLines: [], skippedCount: 0 };
35
+ }
36
+
37
+ // Create a temporary Text component to render and get visual lines
38
+ const tempText = new Text(text, paddingX, 0);
39
+ const allVisualLines = tempText.render(width);
40
+
41
+ if (allVisualLines.length <= maxVisualLines) {
42
+ return { visualLines: allVisualLines, skippedCount: 0 };
43
+ }
44
+
45
+ // Take the last N visual lines
46
+ const truncatedLines = allVisualLines.slice(-maxVisualLines);
47
+ const skippedCount = allVisualLines.length - maxVisualLines;
48
+
49
+ return { visualLines: truncatedLines, skippedCount };
50
+ }
@@ -0,0 +1,183 @@
1
+ import { type Component, visibleWidth } from "@oh-my-pi/pi-tui";
2
+ import { APP_NAME } from "../../../config.js";
3
+ import { theme } from "../theme/theme.js";
4
+
5
+ export interface RecentSession {
6
+ name: string;
7
+ timeAgo: string;
8
+ }
9
+
10
+ /**
11
+ * Premium welcome screen with block-based Pi logo and two-column layout.
12
+ */
13
+ export class WelcomeComponent implements Component {
14
+ private version: string;
15
+ private modelName: string;
16
+ private providerName: string;
17
+ private recentSessions: RecentSession[];
18
+
19
+ constructor(version: string, modelName: string, providerName: string, recentSessions: RecentSession[] = []) {
20
+ this.version = version;
21
+ this.modelName = modelName;
22
+ this.providerName = providerName;
23
+ this.recentSessions = recentSessions;
24
+ }
25
+
26
+ invalidate(): void {}
27
+
28
+ setModel(modelName: string, providerName: string): void {
29
+ this.modelName = modelName;
30
+ this.providerName = providerName;
31
+ }
32
+
33
+ setRecentSessions(sessions: RecentSession[]): void {
34
+ this.recentSessions = sessions;
35
+ }
36
+
37
+ render(termWidth: number): string[] {
38
+ // Box dimensions - responsive with min/max
39
+ const minWidth = 80;
40
+ const maxWidth = 100;
41
+ const boxWidth = Math.max(minWidth, Math.min(termWidth - 2, maxWidth));
42
+ const leftCol = 26;
43
+ const rightCol = boxWidth - leftCol - 3; // 3 = │ + │ + │
44
+
45
+ // Block-based Pi logo (gradient: magenta → cyan)
46
+ // biome-ignore format: preserve ASCII art layout
47
+ const piLogo = ["▀████████████▀", " ╘███ ███ ", " ███ ███ ", " ███ ███ ", " ▄███▄ ▄███▄ "];
48
+
49
+ // Apply gradient to logo
50
+ const logoColored = piLogo.map((line) => this.gradientLine(line));
51
+
52
+ // Left column - centered content
53
+ const leftLines = [
54
+ "",
55
+ this.centerText(theme.bold("Welcome back!"), leftCol),
56
+ "",
57
+ ...logoColored.map((l) => this.centerText(l, leftCol)),
58
+ "",
59
+ this.centerText(theme.fg("muted", this.modelName), leftCol),
60
+ this.centerText(theme.fg("borderMuted", this.providerName), leftCol),
61
+ ];
62
+
63
+ // Right column separator
64
+ const separatorWidth = rightCol - 2; // padding on each side
65
+ const separator = ` ${theme.fg("dim", "─".repeat(separatorWidth))}`;
66
+
67
+ // Recent sessions content
68
+ const sessionLines: string[] = [];
69
+ if (this.recentSessions.length === 0) {
70
+ sessionLines.push(` ${theme.fg("dim", "No recent sessions")}`);
71
+ } else {
72
+ for (const session of this.recentSessions.slice(0, 3)) {
73
+ sessionLines.push(
74
+ ` ${theme.fg("dim", "▪ ")}${theme.fg("muted", session.name)}${theme.fg("dim", ` (${session.timeAgo})`)}`,
75
+ );
76
+ }
77
+ }
78
+
79
+ // Right column
80
+ const rightLines = [
81
+ ` ${theme.bold(theme.fg("accent", "Tips"))}`,
82
+ ` ${theme.fg("dim", "?")}${theme.fg("muted", " for keyboard shortcuts")}`,
83
+ ` ${theme.fg("dim", "/")}${theme.fg("muted", " for commands")}`,
84
+ ` ${theme.fg("dim", "!")}${theme.fg("muted", " to run bash")}`,
85
+ ` ${theme.fg("dim", "/status")}${theme.fg("muted", " for loaded extensions")}`,
86
+ separator,
87
+ ` ${theme.bold(theme.fg("accent", "Recent sessions"))}`,
88
+ ...sessionLines,
89
+ "",
90
+ ];
91
+
92
+ // Border characters (dim)
93
+ const h = theme.fg("dim", "─");
94
+ const v = theme.fg("dim", "│");
95
+ const tl = theme.fg("dim", "╭");
96
+ const tr = theme.fg("dim", "╮");
97
+ const bl = theme.fg("dim", "╰");
98
+ const br = theme.fg("dim", "╯");
99
+
100
+ const lines: string[] = [];
101
+
102
+ // Top border with embedded title
103
+ const title = ` ${APP_NAME} v${this.version} `;
104
+ const titleStyled = theme.fg("dim", "───") + theme.fg("muted", title);
105
+ const titleVisLen = 3 + title.length;
106
+ const afterTitle = boxWidth - 2 - titleVisLen;
107
+ lines.push(tl + titleStyled + h.repeat(Math.max(0, afterTitle)) + tr);
108
+
109
+ // Content rows
110
+ const maxRows = Math.max(leftLines.length, rightLines.length);
111
+ for (let i = 0; i < maxRows; i++) {
112
+ const left = this.fitToWidth(leftLines[i] ?? "", leftCol);
113
+ const right = this.fitToWidth(rightLines[i] ?? "", rightCol);
114
+ lines.push(v + left + v + right + v);
115
+ }
116
+
117
+ // Bottom border
118
+ lines.push(bl + h.repeat(leftCol) + theme.fg("dim", "┴") + h.repeat(rightCol) + br);
119
+
120
+ return lines;
121
+ }
122
+
123
+ /** Center text within a given width */
124
+ private centerText(text: string, width: number): string {
125
+ const visLen = visibleWidth(text);
126
+ if (visLen >= width) return text;
127
+ const leftPad = Math.floor((width - visLen) / 2);
128
+ const rightPad = width - visLen - leftPad;
129
+ return " ".repeat(leftPad) + text + " ".repeat(rightPad);
130
+ }
131
+
132
+ /** Apply magenta→cyan gradient to a string */
133
+ private gradientLine(line: string): string {
134
+ const colors = [
135
+ "\x1b[38;5;199m", // bright magenta
136
+ "\x1b[38;5;171m", // magenta-purple
137
+ "\x1b[38;5;135m", // purple
138
+ "\x1b[38;5;99m", // purple-blue
139
+ "\x1b[38;5;75m", // cyan-blue
140
+ "\x1b[38;5;51m", // bright cyan
141
+ ];
142
+ const reset = "\x1b[0m";
143
+
144
+ let result = "";
145
+ let colorIdx = 0;
146
+ const step = Math.max(1, Math.floor(line.length / colors.length));
147
+
148
+ for (let i = 0; i < line.length; i++) {
149
+ if (i > 0 && i % step === 0 && colorIdx < colors.length - 1) {
150
+ colorIdx++;
151
+ }
152
+ const char = line[i];
153
+ if (char !== " ") {
154
+ result += colors[colorIdx] + char + reset;
155
+ } else {
156
+ result += char;
157
+ }
158
+ }
159
+ return result;
160
+ }
161
+
162
+ /** Fit string to exact width with ANSI-aware truncation/padding */
163
+ private fitToWidth(str: string, width: number): string {
164
+ const visLen = visibleWidth(str);
165
+ if (visLen > width) {
166
+ let truncated = "";
167
+ let currentWidth = 0;
168
+ let inEscape = false;
169
+ for (const char of str) {
170
+ if (char === "\x1b") inEscape = true;
171
+ if (inEscape) {
172
+ truncated += char;
173
+ if (char === "m") inEscape = false;
174
+ } else if (currentWidth < width - 1) {
175
+ truncated += char;
176
+ currentWidth++;
177
+ }
178
+ }
179
+ return `${truncated}…`;
180
+ }
181
+ return str + " ".repeat(width - visLen);
182
+ }
183
+ }