@oh-my-pi/pi-coding-agent 12.3.0 → 12.5.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 +66 -0
- package/docs/custom-tools.md +21 -6
- package/docs/extensions.md +20 -0
- package/package.json +12 -12
- package/src/cli/setup-cli.ts +62 -2
- package/src/commands/setup.ts +1 -1
- package/src/config/keybindings.ts +6 -2
- package/src/config/settings-schema.ts +58 -4
- package/src/config/settings.ts +23 -9
- package/src/debug/index.ts +26 -19
- package/src/debug/log-formatting.ts +60 -0
- package/src/debug/log-viewer.ts +903 -0
- package/src/debug/report-bundle.ts +87 -8
- package/src/discovery/helpers.ts +131 -137
- package/src/extensibility/custom-tools/types.ts +44 -6
- package/src/extensibility/extensions/types.ts +60 -0
- package/src/extensibility/hooks/types.ts +60 -0
- package/src/extensibility/skills.ts +4 -2
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +7 -1
- package/src/memories/index.ts +11 -7
- package/src/modes/components/bash-execution.ts +16 -9
- package/src/modes/components/custom-editor.ts +8 -0
- package/src/modes/components/python-execution.ts +16 -7
- package/src/modes/components/settings-selector.ts +29 -14
- package/src/modes/components/tool-execution.ts +2 -1
- package/src/modes/controllers/command-controller.ts +3 -1
- package/src/modes/controllers/event-controller.ts +7 -0
- package/src/modes/controllers/input-controller.ts +23 -2
- package/src/modes/controllers/selector-controller.ts +9 -7
- package/src/modes/interactive-mode.ts +84 -1
- package/src/modes/rpc/rpc-client.ts +7 -0
- package/src/modes/rpc/rpc-mode.ts +8 -0
- package/src/modes/rpc/rpc-types.ts +2 -0
- package/src/modes/theme/theme.ts +163 -7
- package/src/modes/types.ts +1 -0
- package/src/patch/hashline.ts +2 -1
- package/src/patch/shared.ts +44 -13
- package/src/prompts/system/plan-mode-approved.md +5 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +10 -0
- package/src/prompts/tools/todo-write.md +3 -1
- package/src/sdk.ts +82 -9
- package/src/session/agent-session.ts +137 -29
- package/src/session/streaming-output.ts +1 -1
- package/src/stt/downloader.ts +71 -0
- package/src/stt/index.ts +3 -0
- package/src/stt/recorder.ts +351 -0
- package/src/stt/setup.ts +52 -0
- package/src/stt/stt-controller.ts +160 -0
- package/src/stt/transcribe.py +70 -0
- package/src/stt/transcriber.ts +91 -0
- package/src/task/executor.ts +10 -2
- package/src/tools/bash-interactive.ts +10 -6
- package/src/tools/fetch.ts +1 -1
- package/src/tools/output-meta.ts +6 -2
- package/src/web/scrapers/types.ts +1 -0
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
8
8
|
import type { ImageContent, Message, Model, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
|
|
9
9
|
import type { Component, TUI } from "@oh-my-pi/pi-tui";
|
|
10
|
+
import type { Rule } from "../../capability/rule";
|
|
10
11
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
11
12
|
import type { ExecOptions, ExecResult } from "../../exec/exec";
|
|
12
13
|
import type { Theme } from "../../modes/theme/theme";
|
|
@@ -21,6 +22,7 @@ import type {
|
|
|
21
22
|
SessionManager,
|
|
22
23
|
} from "../../session/session-manager";
|
|
23
24
|
import type { BashToolDetails, FindToolDetails, GrepToolDetails, ReadToolDetails } from "../../tools";
|
|
25
|
+
import type { TodoItem } from "../../tools/todo-write";
|
|
24
26
|
|
|
25
27
|
// Re-export for backward compatibility
|
|
26
28
|
export type { ExecOptions, ExecResult } from "../../exec/exec";
|
|
@@ -389,6 +391,52 @@ export interface TurnEndEvent {
|
|
|
389
391
|
toolResults: ToolResultMessage[];
|
|
390
392
|
}
|
|
391
393
|
|
|
394
|
+
/** Event data for auto_compaction_start event. */
|
|
395
|
+
export interface AutoCompactionStartEvent {
|
|
396
|
+
type: "auto_compaction_start";
|
|
397
|
+
reason: "threshold" | "overflow";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Event data for auto_compaction_end event. */
|
|
401
|
+
export interface AutoCompactionEndEvent {
|
|
402
|
+
type: "auto_compaction_end";
|
|
403
|
+
result: CompactionResult | undefined;
|
|
404
|
+
aborted: boolean;
|
|
405
|
+
willRetry: boolean;
|
|
406
|
+
errorMessage?: string;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Event data for auto_retry_start event. */
|
|
410
|
+
export interface AutoRetryStartEvent {
|
|
411
|
+
type: "auto_retry_start";
|
|
412
|
+
attempt: number;
|
|
413
|
+
maxAttempts: number;
|
|
414
|
+
delayMs: number;
|
|
415
|
+
errorMessage: string;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Event data for auto_retry_end event. */
|
|
419
|
+
export interface AutoRetryEndEvent {
|
|
420
|
+
type: "auto_retry_end";
|
|
421
|
+
success: boolean;
|
|
422
|
+
attempt: number;
|
|
423
|
+
finalError?: string;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Event data for ttsr_triggered event. */
|
|
427
|
+
export interface TtsrTriggeredEvent {
|
|
428
|
+
type: "ttsr_triggered";
|
|
429
|
+
rules: Rule[];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Event data for todo_reminder event. */
|
|
433
|
+
export interface TodoReminderEvent {
|
|
434
|
+
type: "todo_reminder";
|
|
435
|
+
todos: TodoItem[];
|
|
436
|
+
attempt: number;
|
|
437
|
+
maxAttempts: number;
|
|
438
|
+
}
|
|
439
|
+
|
|
392
440
|
/**
|
|
393
441
|
* Event data for tool_call event.
|
|
394
442
|
* Fired before a tool is executed. Hooks can block execution.
|
|
@@ -485,6 +533,12 @@ export type HookEvent =
|
|
|
485
533
|
| AgentEndEvent
|
|
486
534
|
| TurnStartEvent
|
|
487
535
|
| TurnEndEvent
|
|
536
|
+
| AutoCompactionStartEvent
|
|
537
|
+
| AutoCompactionEndEvent
|
|
538
|
+
| AutoRetryStartEvent
|
|
539
|
+
| AutoRetryEndEvent
|
|
540
|
+
| TtsrTriggeredEvent
|
|
541
|
+
| TodoReminderEvent
|
|
488
542
|
| ToolCallEvent
|
|
489
543
|
| ToolResultEvent;
|
|
490
544
|
|
|
@@ -658,6 +712,12 @@ export interface HookAPI {
|
|
|
658
712
|
on(event: "agent_end", handler: HookHandler<AgentEndEvent>): void;
|
|
659
713
|
on(event: "turn_start", handler: HookHandler<TurnStartEvent>): void;
|
|
660
714
|
on(event: "turn_end", handler: HookHandler<TurnEndEvent>): void;
|
|
715
|
+
on(event: "auto_compaction_start", handler: HookHandler<AutoCompactionStartEvent>): void;
|
|
716
|
+
on(event: "auto_compaction_end", handler: HookHandler<AutoCompactionEndEvent>): void;
|
|
717
|
+
on(event: "auto_retry_start", handler: HookHandler<AutoRetryStartEvent>): void;
|
|
718
|
+
on(event: "auto_retry_end", handler: HookHandler<AutoRetryEndEvent>): void;
|
|
719
|
+
on(event: "ttsr_triggered", handler: HookHandler<TtsrTriggeredEvent>): void;
|
|
720
|
+
on(event: "todo_reminder", handler: HookHandler<TodoReminderEvent>): void;
|
|
661
721
|
on(event: "tool_call", handler: HookHandler<ToolCallEvent, ToolCallEventResult>): void;
|
|
662
722
|
on(event: "tool_result", handler: HookHandler<ToolResultEvent, ToolResultEventResult>): void;
|
|
663
723
|
|
|
@@ -235,6 +235,8 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
235
235
|
return { skills: [], warnings: [] };
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
const anyBuiltInSkillSourceEnabled =
|
|
239
|
+
enableCodexUser || enableClaudeUser || enableClaudeProject || enablePiUser || enablePiProject;
|
|
238
240
|
// Helper to check if a source is enabled
|
|
239
241
|
function isSourceEnabled(source: SourceMeta): boolean {
|
|
240
242
|
const { provider, level } = source;
|
|
@@ -243,8 +245,8 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
243
245
|
if (provider === "claude" && level === "project") return enableClaudeProject;
|
|
244
246
|
if (provider === "native" && level === "user") return enablePiUser;
|
|
245
247
|
if (provider === "native" && level === "project") return enablePiProject;
|
|
246
|
-
// For other providers (
|
|
247
|
-
return
|
|
248
|
+
// For other providers (agents, claude-plugins, etc.), treat them as built-in skill sources.
|
|
249
|
+
return anyBuiltInSkillSourceEnabled;
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
// Use capability API to load all skills
|
package/src/lsp/render.ts
CHANGED
|
@@ -282,7 +282,7 @@ function renderHover(
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
/**
|
|
285
|
-
* Syntax highlight code using native
|
|
285
|
+
* Syntax highlight code using native highlighter.
|
|
286
286
|
*/
|
|
287
287
|
function highlightCode(codeText: string, language: string, theme: Theme): string[] {
|
|
288
288
|
const validLang = language && supportsLanguage(language) ? language : undefined;
|
package/src/main.ts
CHANGED
|
@@ -560,7 +560,13 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
560
560
|
});
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
-
await initTheme(
|
|
563
|
+
await initTheme(
|
|
564
|
+
isInteractive,
|
|
565
|
+
settings.get("symbolPreset"),
|
|
566
|
+
settings.get("colorBlindMode"),
|
|
567
|
+
settings.get("theme.dark"),
|
|
568
|
+
settings.get("theme.light"),
|
|
569
|
+
);
|
|
564
570
|
debugStartup("main:initTheme2");
|
|
565
571
|
time("initTheme");
|
|
566
572
|
|
package/src/memories/index.ts
CHANGED
|
@@ -152,7 +152,7 @@ export async function buildMemoryToolDeveloperInstructions(
|
|
|
152
152
|
): Promise<string | undefined> {
|
|
153
153
|
const cfg = loadMemoryConfig(settings);
|
|
154
154
|
if (!cfg.enabled) return undefined;
|
|
155
|
-
const memoryRoot = getMemoryRoot(agentDir);
|
|
155
|
+
const memoryRoot = getMemoryRoot(agentDir, settings.getCwd());
|
|
156
156
|
const summaryPath = path.join(memoryRoot, "memory_summary.md");
|
|
157
157
|
|
|
158
158
|
let text: string;
|
|
@@ -176,14 +176,14 @@ export async function buildMemoryToolDeveloperInstructions(
|
|
|
176
176
|
/**
|
|
177
177
|
* Clear all persisted memory state and generated artifacts.
|
|
178
178
|
*/
|
|
179
|
-
export async function clearMemoryData(agentDir: string): Promise<void> {
|
|
179
|
+
export async function clearMemoryData(agentDir: string, cwd: string): Promise<void> {
|
|
180
180
|
const db = openMemoryDb(getAgentDbPath(agentDir));
|
|
181
181
|
try {
|
|
182
182
|
clearMemoryDataInDb(db);
|
|
183
183
|
} finally {
|
|
184
184
|
closeMemoryDb(db);
|
|
185
185
|
}
|
|
186
|
-
await fs.rm(getMemoryRoot(agentDir), { recursive: true, force: true });
|
|
186
|
+
await fs.rm(getMemoryRoot(agentDir, cwd), { recursive: true, force: true });
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
/**
|
|
@@ -221,7 +221,7 @@ async function runPhase1(options: {
|
|
|
221
221
|
const db = openMemoryDb(getAgentDbPath(agentDir));
|
|
222
222
|
const nowSec = unixNow();
|
|
223
223
|
const workerId = `memory-${process.pid}`;
|
|
224
|
-
const memoryRoot = getMemoryRoot(agentDir);
|
|
224
|
+
const memoryRoot = getMemoryRoot(agentDir, session.sessionManager.getCwd());
|
|
225
225
|
const currentThreadId = session.sessionManager.getSessionId();
|
|
226
226
|
|
|
227
227
|
try {
|
|
@@ -345,7 +345,7 @@ async function runPhase2(options: {
|
|
|
345
345
|
const db = openMemoryDb(getAgentDbPath(agentDir));
|
|
346
346
|
const nowSec = unixNow();
|
|
347
347
|
const workerId = `memory-${process.pid}`;
|
|
348
|
-
const memoryRoot = getMemoryRoot(agentDir);
|
|
348
|
+
const memoryRoot = getMemoryRoot(agentDir, session.sessionManager.getCwd());
|
|
349
349
|
|
|
350
350
|
try {
|
|
351
351
|
const claimResult = tryClaimGlobalPhase2Job(db, {
|
|
@@ -1077,8 +1077,12 @@ function loadMemoryConfig(settings: Settings): MemoryRuntimeConfig {
|
|
|
1077
1077
|
};
|
|
1078
1078
|
}
|
|
1079
1079
|
|
|
1080
|
-
function getMemoryRoot(agentDir: string): string {
|
|
1081
|
-
return path.join(agentDir, "memories");
|
|
1080
|
+
function getMemoryRoot(agentDir: string, cwd: string): string {
|
|
1081
|
+
return path.join(agentDir, "memories", encodeProjectPath(cwd));
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function encodeProjectPath(cwd: string): string {
|
|
1085
|
+
return `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
1082
1086
|
}
|
|
1083
1087
|
|
|
1084
1088
|
function unixNow(): number {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Component for displaying bash command execution with streaming output.
|
|
3
3
|
*/
|
|
4
|
+
|
|
5
|
+
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
4
6
|
import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
5
7
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
6
8
|
import type { TruncationMeta } from "../../tools/output-meta";
|
|
@@ -10,6 +12,7 @@ import { truncateToVisualLines } from "./visual-truncate";
|
|
|
10
12
|
|
|
11
13
|
// Preview line limit when not expanded (matches tool execution behavior)
|
|
12
14
|
const PREVIEW_LINES = 20;
|
|
15
|
+
const MAX_DISPLAY_LINE_CHARS = 4000;
|
|
13
16
|
|
|
14
17
|
export class BashExecutionComponent extends Container {
|
|
15
18
|
#outputLines: string[] = [];
|
|
@@ -73,13 +76,15 @@ export class BashExecutionComponent extends Container {
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
appendOutput(chunk: string): void {
|
|
76
|
-
const clean =
|
|
79
|
+
const clean = sanitizeText(chunk);
|
|
77
80
|
|
|
78
81
|
// Append to output lines
|
|
79
|
-
const newLines = clean.split("\n");
|
|
82
|
+
const newLines = clean.split("\n").map(line => this.#clampDisplayLine(line));
|
|
80
83
|
if (this.#outputLines.length > 0 && newLines.length > 0) {
|
|
81
84
|
// Append first chunk to last line (incomplete line continuation)
|
|
82
|
-
this.#outputLines[this.#outputLines.length - 1]
|
|
85
|
+
this.#outputLines[this.#outputLines.length - 1] = this.#clampDisplayLine(
|
|
86
|
+
`${this.#outputLines[this.#outputLines.length - 1]}${newLines[0]}`,
|
|
87
|
+
);
|
|
83
88
|
this.#outputLines.push(...newLines.slice(1));
|
|
84
89
|
} else {
|
|
85
90
|
this.#outputLines.push(...newLines);
|
|
@@ -184,15 +189,17 @@ export class BashExecutionComponent extends Container {
|
|
|
184
189
|
}
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
#
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
#clampDisplayLine(line: string): string {
|
|
193
|
+
if (line.length <= MAX_DISPLAY_LINE_CHARS) {
|
|
194
|
+
return line;
|
|
195
|
+
}
|
|
196
|
+
const omitted = line.length - MAX_DISPLAY_LINE_CHARS;
|
|
197
|
+
return `${line.slice(0, MAX_DISPLAY_LINE_CHARS)}… [${omitted} chars omitted]`;
|
|
191
198
|
}
|
|
192
199
|
|
|
193
200
|
#setOutput(output: string): void {
|
|
194
|
-
const clean =
|
|
195
|
-
this.#outputLines = clean ? clean.split("\n") : [];
|
|
201
|
+
const clean = sanitizeText(output);
|
|
202
|
+
this.#outputLines = clean ? clean.split("\n").map(line => this.#clampDisplayLine(line)) : [];
|
|
196
203
|
}
|
|
197
204
|
|
|
198
205
|
/**
|
|
@@ -19,6 +19,8 @@ export class CustomEditor extends Editor {
|
|
|
19
19
|
onQuestionMark?: () => void;
|
|
20
20
|
onCapsLock?: () => void;
|
|
21
21
|
onAltP?: () => void;
|
|
22
|
+
/** Called when Alt+Shift+C is pressed to copy prompt to clipboard. */
|
|
23
|
+
onCopyPrompt?: () => void;
|
|
22
24
|
/** Called when Ctrl+V is pressed. Returns true if handled (image found), false to fall through to text paste. */
|
|
23
25
|
onCtrlV?: () => Promise<boolean>;
|
|
24
26
|
/** Called when Alt+Up is pressed (dequeue keybinding). */
|
|
@@ -150,6 +152,12 @@ export class CustomEditor extends Editor {
|
|
|
150
152
|
return;
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
// Intercept Alt+Shift+C to copy prompt to clipboard
|
|
156
|
+
if (matchesKey(data, "alt+shift+c") && this.onCopyPrompt) {
|
|
157
|
+
this.onCopyPrompt();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
153
161
|
// Intercept ? when editor is empty to show hotkeys
|
|
154
162
|
if (data === "?" && this.getText().length === 0 && this.onQuestionMark) {
|
|
155
163
|
this.onQuestionMark();
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Component for displaying user-initiated Python execution with streaming output.
|
|
3
3
|
* Shares the same kernel session as the agent's Python tool.
|
|
4
4
|
*/
|
|
5
|
+
|
|
6
|
+
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
5
7
|
import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
6
8
|
import { getSymbolTheme, highlightCode, theme } from "../../modes/theme/theme";
|
|
7
9
|
import type { TruncationMeta } from "../../tools/output-meta";
|
|
@@ -10,6 +12,7 @@ import { DynamicBorder } from "./dynamic-border";
|
|
|
10
12
|
import { truncateToVisualLines } from "./visual-truncate";
|
|
11
13
|
|
|
12
14
|
const PREVIEW_LINES = 20;
|
|
15
|
+
const MAX_DISPLAY_LINE_CHARS = 4000;
|
|
13
16
|
|
|
14
17
|
export class PythonExecutionComponent extends Container {
|
|
15
18
|
#outputLines: string[] = [];
|
|
@@ -70,11 +73,13 @@ export class PythonExecutionComponent extends Container {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
appendOutput(chunk: string): void {
|
|
73
|
-
const clean =
|
|
76
|
+
const clean = sanitizeText(chunk);
|
|
74
77
|
|
|
75
|
-
const newLines = clean.split("\n");
|
|
78
|
+
const newLines = clean.split("\n").map(line => this.#clampDisplayLine(line));
|
|
76
79
|
if (this.#outputLines.length > 0 && newLines.length > 0) {
|
|
77
|
-
this.#outputLines[this.#outputLines.length - 1]
|
|
80
|
+
this.#outputLines[this.#outputLines.length - 1] = this.#clampDisplayLine(
|
|
81
|
+
`${this.#outputLines[this.#outputLines.length - 1]}${newLines[0]}`,
|
|
82
|
+
);
|
|
78
83
|
this.#outputLines.push(...newLines.slice(1));
|
|
79
84
|
} else {
|
|
80
85
|
this.#outputLines.push(...newLines);
|
|
@@ -168,13 +173,17 @@ export class PythonExecutionComponent extends Container {
|
|
|
168
173
|
}
|
|
169
174
|
}
|
|
170
175
|
|
|
171
|
-
#
|
|
172
|
-
|
|
176
|
+
#clampDisplayLine(line: string): string {
|
|
177
|
+
if (line.length <= MAX_DISPLAY_LINE_CHARS) {
|
|
178
|
+
return line;
|
|
179
|
+
}
|
|
180
|
+
const omitted = line.length - MAX_DISPLAY_LINE_CHARS;
|
|
181
|
+
return `${line.slice(0, MAX_DISPLAY_LINE_CHARS)}… [${omitted} chars omitted]`;
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
#setOutput(output: string): void {
|
|
176
|
-
const clean =
|
|
177
|
-
this.#outputLines = clean ? clean.split("\n") : [];
|
|
185
|
+
const clean = sanitizeText(output);
|
|
186
|
+
this.#outputLines = clean ? clean.split("\n").map(line => this.#clampDisplayLine(line)) : [];
|
|
178
187
|
}
|
|
179
188
|
|
|
180
189
|
getOutput(): string {
|
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
StatusLineSeparatorStyle,
|
|
21
21
|
} from "../../config/settings-schema";
|
|
22
22
|
import { SETTING_TABS, TAB_METADATA } from "../../config/settings-schema";
|
|
23
|
-
import { getSelectListTheme, getSettingsListTheme, theme } from "../../modes/theme/theme";
|
|
23
|
+
import { getCurrentThemeName, getSelectListTheme, getSettingsListTheme, theme } from "../../modes/theme/theme";
|
|
24
24
|
import { DynamicBorder } from "./dynamic-border";
|
|
25
25
|
import { PluginSettingsComponent } from "./plugin-settings";
|
|
26
26
|
import { getSettingsForTab, type SettingDef } from "./settings-defs";
|
|
@@ -41,6 +41,7 @@ function getTabBarTheme(): TabBarTheme {
|
|
|
41
41
|
class SelectSubmenu extends Container {
|
|
42
42
|
#selectList: SelectList;
|
|
43
43
|
#previewText: Text | null = null;
|
|
44
|
+
#previewUpdateRequestId: number = 0;
|
|
44
45
|
|
|
45
46
|
constructor(
|
|
46
47
|
title: string,
|
|
@@ -49,7 +50,7 @@ class SelectSubmenu extends Container {
|
|
|
49
50
|
currentValue: string,
|
|
50
51
|
onSelect: (value: string) => void,
|
|
51
52
|
onCancel: () => void,
|
|
52
|
-
onSelectionChange?: (value: string) => void
|
|
53
|
+
onSelectionChange?: (value: string) => void | Promise<void>,
|
|
53
54
|
private readonly getPreview?: () => string,
|
|
54
55
|
) {
|
|
55
56
|
super();
|
|
@@ -91,9 +92,19 @@ class SelectSubmenu extends Container {
|
|
|
91
92
|
|
|
92
93
|
if (onSelectionChange) {
|
|
93
94
|
this.#selectList.onSelectionChange = item => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
const requestId = ++this.#previewUpdateRequestId;
|
|
96
|
+
const result = onSelectionChange(item.value);
|
|
97
|
+
if (result && typeof (result as Promise<void>).then === "function") {
|
|
98
|
+
void (result as Promise<void>).finally(() => {
|
|
99
|
+
if (requestId === this.#previewUpdateRequestId) {
|
|
100
|
+
this.#updatePreview();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (requestId === this.#previewUpdateRequestId) {
|
|
106
|
+
this.#updatePreview();
|
|
107
|
+
}
|
|
97
108
|
};
|
|
98
109
|
}
|
|
99
110
|
|
|
@@ -153,7 +164,7 @@ export interface SettingsCallbacks {
|
|
|
153
164
|
/** Called when any setting value changes */
|
|
154
165
|
onChange: (path: SettingPath, newValue: unknown) => void;
|
|
155
166
|
/** Called for theme preview while browsing */
|
|
156
|
-
onThemePreview?: (theme: string) => void
|
|
167
|
+
onThemePreview?: (theme: string) => void | Promise<void>;
|
|
157
168
|
/** Called for status line preview while configuring */
|
|
158
169
|
onStatusLinePreview?: (settings: StatusLinePreviewSettings) => void;
|
|
159
170
|
/** Get current rendered status line for inline preview */
|
|
@@ -299,17 +310,22 @@ export class SettingsSelectorComponent extends Container {
|
|
|
299
310
|
const baseOpt = options.find(o => o.value === level);
|
|
300
311
|
return baseOpt || { value: level, label: level };
|
|
301
312
|
});
|
|
302
|
-
} else if (def.path === "theme") {
|
|
313
|
+
} else if (def.path === "theme.dark" || def.path === "theme.light") {
|
|
303
314
|
options = this.context.availableThemes.map(t => ({ value: t, label: t }));
|
|
304
315
|
}
|
|
305
316
|
|
|
306
317
|
// Preview handlers
|
|
307
|
-
let onPreview: ((value: string) => void) | undefined;
|
|
318
|
+
let onPreview: ((value: string) => void | Promise<void>) | undefined;
|
|
308
319
|
let onPreviewCancel: (() => void) | undefined;
|
|
309
320
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
321
|
+
const activeThemeBeforePreview = getCurrentThemeName() ?? currentValue;
|
|
322
|
+
if (def.path === "theme.dark" || def.path === "theme.light") {
|
|
323
|
+
onPreview = value => {
|
|
324
|
+
return this.callbacks.onThemePreview?.(value);
|
|
325
|
+
};
|
|
326
|
+
onPreviewCancel = () => {
|
|
327
|
+
this.callbacks.onThemePreview?.(activeThemeBeforePreview);
|
|
328
|
+
};
|
|
313
329
|
} else if (def.path === "statusLine.preset") {
|
|
314
330
|
onPreview = value => {
|
|
315
331
|
const presetDef = getPreset(
|
|
@@ -347,7 +363,8 @@ export class SettingsSelectorComponent extends Container {
|
|
|
347
363
|
}
|
|
348
364
|
|
|
349
365
|
// Provide status line preview for theme selection
|
|
350
|
-
const
|
|
366
|
+
const isThemeSetting = def.path === "theme.dark" || def.path === "theme.light";
|
|
367
|
+
const getPreview = isThemeSetting ? this.callbacks.getStatusLinePreview : undefined;
|
|
351
368
|
|
|
352
369
|
return new SelectSubmenu(
|
|
353
370
|
def.label,
|
|
@@ -355,9 +372,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
355
372
|
options,
|
|
356
373
|
currentValue,
|
|
357
374
|
value => {
|
|
358
|
-
// Persist
|
|
359
375
|
this.#setSettingValue(def.path, value);
|
|
360
|
-
// Notify
|
|
361
376
|
this.callbacks.onChange(def.path, value);
|
|
362
377
|
done(value);
|
|
363
378
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
2
3
|
import {
|
|
3
4
|
Box,
|
|
4
5
|
type Component,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
Text,
|
|
13
14
|
type TUI,
|
|
14
15
|
} from "@oh-my-pi/pi-tui";
|
|
15
|
-
import { logger
|
|
16
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
16
17
|
import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
17
18
|
import type { Theme } from "../../modes/theme/theme";
|
|
18
19
|
import { theme } from "../../modes/theme/theme";
|
|
@@ -358,6 +358,7 @@ export class CommandController {
|
|
|
358
358
|
handleHotkeysCommand(): void {
|
|
359
359
|
const expandToolsKey = this.ctx.keybindings.getDisplayString("expandTools") || "Ctrl+O";
|
|
360
360
|
const planModeKey = this.ctx.keybindings.getDisplayString("togglePlanMode") || "Alt+Shift+P";
|
|
361
|
+
const sttKey = this.ctx.keybindings.getDisplayString("toggleSTT") || "Alt+H";
|
|
361
362
|
const hotkeys = `
|
|
362
363
|
**Navigation**
|
|
363
364
|
| Key | Action |
|
|
@@ -394,6 +395,7 @@ export class CommandController {
|
|
|
394
395
|
| \`${expandToolsKey}\` | Toggle tool output expansion |
|
|
395
396
|
| \`Ctrl+T\` | Toggle todo list expansion |
|
|
396
397
|
| \`Ctrl+G\` | Edit message in external editor |
|
|
398
|
+
| \`${sttKey}\` | Toggle speech-to-text recording |
|
|
397
399
|
| \`/\` | Slash commands |
|
|
398
400
|
| \`!\` | Run bash command |
|
|
399
401
|
| \`!!\` | Run bash command (excluded from context) |
|
|
@@ -432,7 +434,7 @@ export class CommandController {
|
|
|
432
434
|
|
|
433
435
|
if (action === "reset" || action === "clear") {
|
|
434
436
|
try {
|
|
435
|
-
await clearMemoryData(agentDir);
|
|
437
|
+
await clearMemoryData(agentDir, this.ctx.sessionManager.getCwd());
|
|
436
438
|
await this.ctx.session.refreshBaseSystemPrompt();
|
|
437
439
|
this.ctx.showStatus("Memory data cleared and system prompt refreshed.");
|
|
438
440
|
} catch (error) {
|
|
@@ -249,6 +249,13 @@ export class EventController {
|
|
|
249
249
|
if (details?.todos) {
|
|
250
250
|
this.ctx.setTodos(details.todos);
|
|
251
251
|
}
|
|
252
|
+
} else if (event.toolName === "todo_write" && event.isError) {
|
|
253
|
+
const textContent = event.result.content.find(
|
|
254
|
+
(content: { type: string; text?: string }) => content.type === "text",
|
|
255
|
+
)?.text;
|
|
256
|
+
this.ctx.showWarning(
|
|
257
|
+
`Todo update failed${textContent ? `: ${textContent}` : ". Progress may be stale until todo_write succeeds."}`,
|
|
258
|
+
);
|
|
252
259
|
}
|
|
253
260
|
if (event.toolName === "exit_plan_mode" && !event.isError) {
|
|
254
261
|
const details = event.result.details as ExitPlanModeDetails | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
import { readImageFromClipboard } from "@oh-my-pi/pi-natives";
|
|
3
|
+
import { copyToClipboard, readImageFromClipboard, sanitizeText } from "@oh-my-pi/pi-natives";
|
|
4
4
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import type { SettingPath, SettingValue } from "../../config/settings";
|
|
6
6
|
import { settings } from "../../config/settings";
|
|
@@ -74,6 +74,7 @@ export class InputController {
|
|
|
74
74
|
this.ctx.editor.onCtrlG = () => void this.openExternalEditor();
|
|
75
75
|
this.ctx.editor.onQuestionMark = () => this.ctx.handleHotkeysCommand();
|
|
76
76
|
this.ctx.editor.onCtrlV = () => this.handleImagePaste();
|
|
77
|
+
this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt();
|
|
77
78
|
|
|
78
79
|
// Wire up extension shortcuts
|
|
79
80
|
this.registerExtensionShortcuts();
|
|
@@ -112,6 +113,9 @@ export class InputController {
|
|
|
112
113
|
for (const key of this.ctx.keybindings.getKeys("followUp")) {
|
|
113
114
|
this.ctx.editor.setCustomKeyHandler(key, () => void this.handleFollowUp());
|
|
114
115
|
}
|
|
116
|
+
for (const key of this.ctx.keybindings.getKeys("toggleSTT")) {
|
|
117
|
+
this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handleSTTToggle());
|
|
118
|
+
}
|
|
115
119
|
|
|
116
120
|
this.ctx.editor.onChange = (text: string) => {
|
|
117
121
|
const wasBashMode = this.ctx.isBashMode;
|
|
@@ -348,7 +352,6 @@ export class InputController {
|
|
|
348
352
|
void this.ctx.shutdown();
|
|
349
353
|
return;
|
|
350
354
|
}
|
|
351
|
-
|
|
352
355
|
// Handle MCP server management commands
|
|
353
356
|
if (text === "/mcp" || text.startsWith("/mcp ")) {
|
|
354
357
|
this.ctx.editor.addToHistory(text);
|
|
@@ -664,6 +667,24 @@ export class InputController {
|
|
|
664
667
|
}
|
|
665
668
|
}
|
|
666
669
|
|
|
670
|
+
/** Copy current prompt text to system clipboard. */
|
|
671
|
+
handleCopyPrompt(): void {
|
|
672
|
+
const text = this.ctx.editor.getText();
|
|
673
|
+
if (!text) {
|
|
674
|
+
this.ctx.showStatus("Nothing to copy");
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
copyToClipboard(text)
|
|
678
|
+
.then(() => {
|
|
679
|
+
const sanitized = sanitizeText(text);
|
|
680
|
+
const preview = sanitized.length > 30 ? `${sanitized.slice(0, 30)}...` : sanitized;
|
|
681
|
+
this.ctx.showStatus(`Copied: ${preview}`);
|
|
682
|
+
})
|
|
683
|
+
.catch(() => {
|
|
684
|
+
this.ctx.showWarning("Failed to copy to clipboard");
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
667
688
|
cycleThinkingLevel(): void {
|
|
668
689
|
const newLevel = this.ctx.session.cycleThinkingLevel();
|
|
669
690
|
if (newLevel === undefined) {
|
|
@@ -20,6 +20,7 @@ import { UserMessageSelectorComponent } from "../../modes/components/user-messag
|
|
|
20
20
|
import {
|
|
21
21
|
getAvailableThemes,
|
|
22
22
|
getSymbolTheme,
|
|
23
|
+
previewTheme,
|
|
23
24
|
setColorBlindMode,
|
|
24
25
|
setSymbolPreset,
|
|
25
26
|
setTheme,
|
|
@@ -71,13 +72,14 @@ export class SelectorController {
|
|
|
71
72
|
},
|
|
72
73
|
{
|
|
73
74
|
onChange: (id, value) => this.handleSettingChange(id, value),
|
|
74
|
-
onThemePreview: themeName => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
onThemePreview: async themeName => {
|
|
76
|
+
const result = await previewTheme(themeName);
|
|
77
|
+
if (result.success) {
|
|
78
|
+
this.ctx.statusLine.invalidate();
|
|
79
|
+
this.ctx.updateEditorTopBorder();
|
|
80
|
+
this.ctx.ui.invalidate();
|
|
81
|
+
this.ctx.ui.requestRender();
|
|
82
|
+
}
|
|
81
83
|
},
|
|
82
84
|
onStatusLinePreview: previewSettings => {
|
|
83
85
|
// Update status line with preview settings
|