@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,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
|
+
}
|