@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.
- package/CHANGELOG.md +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- 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
|
+
}
|