@oh-my-pi/pi-coding-agent 8.12.1 → 8.12.4
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 +5 -0
- package/package.json +7 -10
- package/src/config/settings-manager.ts +36 -0
- package/src/config.ts +0 -5
- package/src/exec/bash-executor.ts +4 -4
- package/src/exec/exec.ts +9 -12
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/plugins/doctor.ts +0 -2
- package/src/ipy/kernel.ts +11 -13
- package/src/migrations.ts +1 -46
- package/src/modes/components/hook-selector.ts +55 -7
- package/src/modes/components/settings-defs.ts +23 -0
- package/src/modes/controllers/extension-ui-controller.ts +32 -16
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/session/compaction/compaction.ts +37 -3
- package/src/ssh/ssh-executor.ts +3 -5
- package/src/system-prompt.ts +25 -98
- package/src/tools/ask.ts +55 -8
- package/src/tools/fetch.ts +9 -61
- package/src/tools/find.ts +28 -49
- package/src/tools/grep.ts +110 -550
- package/src/tools/read.ts +41 -102
- package/src/utils/image-convert.ts +7 -11
- package/src/utils/image-resize.ts +15 -25
- package/src/utils/tools-manager.ts +3 -43
- package/src/web/scrapers/utils.ts +11 -6
- package/src/web/scrapers/youtube.ts +21 -49
- package/src/utils/utils.ts +0 -1
- package/src/vendor/photon/LICENSE.md +0 -201
- package/src/vendor/photon/README.md +0 -158
- package/src/vendor/photon/index.d.ts +0 -3013
- package/src/vendor/photon/index.js +0 -4521
- package/src/vendor/photon/photon_rs_bg.wasm +0 -0
- package/src/vendor/photon/photon_rs_bg.wasm.d.ts +0 -193
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "8.12.
|
|
3
|
+
"version": "8.12.4",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -58,10 +58,6 @@
|
|
|
58
58
|
"types": "./src/internal-urls/*.ts",
|
|
59
59
|
"import": "./src/internal-urls/*.ts"
|
|
60
60
|
},
|
|
61
|
-
"./vendor/photon/*": {
|
|
62
|
-
"types": "./src/vendor/photon/*",
|
|
63
|
-
"import": "./src/vendor/photon/*"
|
|
64
|
-
},
|
|
65
61
|
"./*": {
|
|
66
62
|
"types": "./src/*.ts",
|
|
67
63
|
"import": "./src/*.ts"
|
|
@@ -83,11 +79,12 @@
|
|
|
83
79
|
"test": "bun test"
|
|
84
80
|
},
|
|
85
81
|
"dependencies": {
|
|
86
|
-
"@oh-my-pi/omp-stats": "8.12.
|
|
87
|
-
"@oh-my-pi/pi-agent-core": "8.12.
|
|
88
|
-
"@oh-my-pi/pi-ai": "8.12.
|
|
89
|
-
"@oh-my-pi/pi-
|
|
90
|
-
"@oh-my-pi/pi-
|
|
82
|
+
"@oh-my-pi/omp-stats": "8.12.4",
|
|
83
|
+
"@oh-my-pi/pi-agent-core": "8.12.4",
|
|
84
|
+
"@oh-my-pi/pi-ai": "8.12.4",
|
|
85
|
+
"@oh-my-pi/pi-natives": "8.12.4",
|
|
86
|
+
"@oh-my-pi/pi-tui": "8.12.4",
|
|
87
|
+
"@oh-my-pi/pi-utils": "8.12.4",
|
|
91
88
|
"@openai/agents": "^0.4.4",
|
|
92
89
|
"@sinclair/typebox": "^0.34.48",
|
|
93
90
|
"ajv": "^8.17.1",
|
|
@@ -72,6 +72,13 @@ export interface NotificationSettings {
|
|
|
72
72
|
onComplete?: NotificationMethod; // default: "auto"
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
export interface AskSettings {
|
|
76
|
+
/** Timeout in seconds for ask tool selections (0 or null to disable, default: 30) */
|
|
77
|
+
timeout?: number | null;
|
|
78
|
+
/** Notification method when ask tool is waiting for input (default: "auto") */
|
|
79
|
+
notification?: NotificationMethod;
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
export interface ExaSettings {
|
|
76
83
|
enabled?: boolean; // default: true (master toggle for all Exa tools)
|
|
77
84
|
enableSearch?: boolean; // default: true (search, deep, code, crawl)
|
|
@@ -242,6 +249,7 @@ export interface Settings {
|
|
|
242
249
|
showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME
|
|
243
250
|
normativeRewrite?: boolean; // default: false (rewrite tool call arguments to normalized format in session history)
|
|
244
251
|
readLineNumbers?: boolean; // default: false (prepend line numbers to read tool output by default)
|
|
252
|
+
ask?: AskSettings;
|
|
245
253
|
}
|
|
246
254
|
|
|
247
255
|
export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
|
|
@@ -307,6 +315,7 @@ const DEFAULT_SETTINGS: Settings = {
|
|
|
307
315
|
terminal: { showImages: true },
|
|
308
316
|
images: { autoResize: true },
|
|
309
317
|
notifications: { onComplete: "auto" },
|
|
318
|
+
ask: { timeout: 30, notification: "auto" },
|
|
310
319
|
exa: {
|
|
311
320
|
enabled: true,
|
|
312
321
|
enableSearch: true,
|
|
@@ -1117,6 +1126,33 @@ export class SettingsManager {
|
|
|
1117
1126
|
await this.save();
|
|
1118
1127
|
}
|
|
1119
1128
|
|
|
1129
|
+
/** Get ask tool timeout in milliseconds (0 or null = disabled) */
|
|
1130
|
+
getAskTimeout(): number | null {
|
|
1131
|
+
const timeout = this.settings.ask?.timeout;
|
|
1132
|
+
if (timeout === null || timeout === 0) return null;
|
|
1133
|
+
return (timeout ?? 30) * 1000;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
async setAskTimeout(seconds: number | null): Promise<void> {
|
|
1137
|
+
if (!this.globalSettings.ask) {
|
|
1138
|
+
this.globalSettings.ask = {};
|
|
1139
|
+
}
|
|
1140
|
+
this.globalSettings.ask.timeout = seconds;
|
|
1141
|
+
await this.save();
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
getAskNotification(): NotificationMethod {
|
|
1145
|
+
return this.settings.ask?.notification ?? "auto";
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async setAskNotification(method: NotificationMethod): Promise<void> {
|
|
1149
|
+
if (!this.globalSettings.ask) {
|
|
1150
|
+
this.globalSettings.ask = {};
|
|
1151
|
+
}
|
|
1152
|
+
this.globalSettings.ask.notification = method;
|
|
1153
|
+
await this.save();
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1120
1156
|
getImageAutoResize(): boolean {
|
|
1121
1157
|
return this.settings.images?.autoResize ?? true;
|
|
1122
1158
|
}
|
package/src/config.ts
CHANGED
|
@@ -93,11 +93,6 @@ export function getToolsDir(): string {
|
|
|
93
93
|
return path.join(getAgentDir(), "tools");
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/** Get path to managed binaries directory (fd, rg) */
|
|
97
|
-
export function getBinDir(): string {
|
|
98
|
-
return path.join(getAgentDir(), "bin");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
96
|
/** Get path to slash commands directory */
|
|
102
97
|
export function getCommandsDir(): string {
|
|
103
98
|
return path.join(getAgentDir(), "commands");
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides unified bash execution for AgentSession.executeBash() and direct calls.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { Exception, ptree } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { OutputSink } from "../session/streaming-output";
|
|
8
8
|
import { getShellConfig } from "../utils/shell";
|
|
9
9
|
import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
|
|
@@ -50,11 +50,12 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
50
50
|
artifactId: options?.artifactId,
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
using child = ptree.spawn([shell, ...args, finalCommand], {
|
|
54
54
|
cwd: options?.cwd,
|
|
55
55
|
env: finalEnv,
|
|
56
56
|
signal: options?.signal,
|
|
57
57
|
timeout: options?.timeout,
|
|
58
|
+
detached: true,
|
|
58
59
|
});
|
|
59
60
|
|
|
60
61
|
// Pump streams - errors during abort/timeout are expected
|
|
@@ -65,9 +66,8 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
65
66
|
|
|
66
67
|
// Wait for process exit
|
|
67
68
|
try {
|
|
68
|
-
await child.exited;
|
|
69
69
|
return {
|
|
70
|
-
exitCode: child.
|
|
70
|
+
exitCode: await child.exited,
|
|
71
71
|
cancelled: false,
|
|
72
72
|
...(await sink.dump()),
|
|
73
73
|
};
|
package/src/exec/exec.ts
CHANGED
|
@@ -35,22 +35,19 @@ export async function execCommand(
|
|
|
35
35
|
cwd: string,
|
|
36
36
|
options?: ExecOptions,
|
|
37
37
|
): Promise<ExecResult> {
|
|
38
|
-
const
|
|
38
|
+
const result = await ptree.exec([command, ...args], {
|
|
39
39
|
cwd,
|
|
40
40
|
signal: options?.signal,
|
|
41
41
|
timeout: options?.timeout,
|
|
42
|
+
allowNonZero: true,
|
|
43
|
+
allowAbort: true,
|
|
44
|
+
stderr: "full",
|
|
42
45
|
});
|
|
43
|
-
|
|
44
|
-
const [stdoutText, stderrText] = await Promise.all([proc.stdout.text(), proc.stderr.text()]);
|
|
45
|
-
try {
|
|
46
|
-
await proc.exited;
|
|
47
|
-
} catch {
|
|
48
|
-
// ChildProcess rejects on non-zero exit; we handle it below
|
|
49
|
-
}
|
|
46
|
+
|
|
50
47
|
return {
|
|
51
|
-
stdout:
|
|
52
|
-
stderr:
|
|
53
|
-
code:
|
|
54
|
-
killed:
|
|
48
|
+
stdout: result.stdout,
|
|
49
|
+
stderr: result.stderr,
|
|
50
|
+
code: result.exitCode ?? 0,
|
|
51
|
+
killed: Boolean(result.exitError?.aborted),
|
|
55
52
|
};
|
|
56
53
|
}
|
|
@@ -6,8 +6,6 @@ export async function runDoctorChecks(): Promise<DoctorCheck[]> {
|
|
|
6
6
|
|
|
7
7
|
// Check external tools
|
|
8
8
|
const tools = [
|
|
9
|
-
{ name: "fd", description: "File finder" },
|
|
10
|
-
{ name: "rg", description: "Ripgrep" },
|
|
11
9
|
{ name: "sd", description: "Find-replace" },
|
|
12
10
|
{ name: "sg", description: "AST-grep" },
|
|
13
11
|
{ name: "git", description: "Version control" },
|
package/src/ipy/kernel.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createServer } from "node:net";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
|
-
import {
|
|
3
|
+
import { logger, ptree } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { $ } from "bun";
|
|
5
5
|
import { nanoid } from "nanoid";
|
|
6
|
-
import { getShellConfig
|
|
6
|
+
import { getShellConfig } from "../utils/shell";
|
|
7
7
|
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
8
8
|
import { time } from "../utils/timings";
|
|
9
9
|
import { htmlToBasicMarkdown } from "../web/scrapers/types";
|
|
@@ -452,7 +452,7 @@ export function serializeWebSocketMessage(msg: JupyterMessage): ArrayBuffer {
|
|
|
452
452
|
export class PythonKernel {
|
|
453
453
|
readonly id: string;
|
|
454
454
|
readonly kernelId: string;
|
|
455
|
-
readonly gatewayProcess:
|
|
455
|
+
readonly gatewayProcess: ptree.ChildProcess | null;
|
|
456
456
|
readonly gatewayUrl: string;
|
|
457
457
|
readonly sessionId: string;
|
|
458
458
|
readonly username: string;
|
|
@@ -469,7 +469,7 @@ export class PythonKernel {
|
|
|
469
469
|
private constructor(
|
|
470
470
|
id: string,
|
|
471
471
|
kernelId: string,
|
|
472
|
-
gatewayProcess:
|
|
472
|
+
gatewayProcess: ptree.ChildProcess | null,
|
|
473
473
|
gatewayUrl: string,
|
|
474
474
|
sessionId: string,
|
|
475
475
|
username: string,
|
|
@@ -633,14 +633,14 @@ export class PythonKernel {
|
|
|
633
633
|
kernelEnv.PYTHONPATH = pythonPathParts;
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
let gatewayProcess:
|
|
636
|
+
let gatewayProcess: ptree.ChildProcess | null = null;
|
|
637
637
|
let gatewayUrl: string | null = null;
|
|
638
638
|
let lastError: string | null = null;
|
|
639
639
|
|
|
640
640
|
for (let attempt = 0; attempt < GATEWAY_STARTUP_ATTEMPTS; attempt += 1) {
|
|
641
641
|
const gatewayPort = await allocatePort();
|
|
642
642
|
const candidateUrl = `http://127.0.0.1:${gatewayPort}`;
|
|
643
|
-
const candidateProcess =
|
|
643
|
+
const candidateProcess = ptree.spawn(
|
|
644
644
|
[
|
|
645
645
|
runtime.pythonPath,
|
|
646
646
|
"-m",
|
|
@@ -653,10 +653,8 @@ export class PythonKernel {
|
|
|
653
653
|
],
|
|
654
654
|
{
|
|
655
655
|
cwd: options.cwd,
|
|
656
|
-
stdin: "ignore",
|
|
657
|
-
stdout: "pipe",
|
|
658
|
-
stderr: "pipe",
|
|
659
656
|
env: kernelEnv,
|
|
657
|
+
detached: true,
|
|
660
658
|
},
|
|
661
659
|
);
|
|
662
660
|
|
|
@@ -687,7 +685,7 @@ export class PythonKernel {
|
|
|
687
685
|
|
|
688
686
|
if (gatewayProcess && gatewayUrl) break;
|
|
689
687
|
|
|
690
|
-
|
|
688
|
+
candidateProcess.kill();
|
|
691
689
|
lastError = exited ? "Kernel gateway process exited during startup" : "Kernel gateway failed to start";
|
|
692
690
|
}
|
|
693
691
|
|
|
@@ -702,7 +700,7 @@ export class PythonKernel {
|
|
|
702
700
|
});
|
|
703
701
|
|
|
704
702
|
if (!createResponse.ok) {
|
|
705
|
-
|
|
703
|
+
gatewayProcess.kill();
|
|
706
704
|
throw new Error(`Failed to create kernel: ${await createResponse.text()}`);
|
|
707
705
|
}
|
|
708
706
|
|
|
@@ -1118,7 +1116,7 @@ export class PythonKernel {
|
|
|
1118
1116
|
await releaseSharedGateway();
|
|
1119
1117
|
} else if (this.gatewayProcess) {
|
|
1120
1118
|
try {
|
|
1121
|
-
|
|
1119
|
+
this.gatewayProcess.kill();
|
|
1122
1120
|
} catch (err: unknown) {
|
|
1123
1121
|
logger.warn("Failed to terminate gateway process", {
|
|
1124
1122
|
error: err instanceof Error ? err.message : String(err),
|
package/src/migrations.ts
CHANGED
|
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import chalk from "chalk";
|
|
8
|
-
import { getAgentDbPath, getAgentDir
|
|
8
|
+
import { getAgentDbPath, getAgentDir } from "./config";
|
|
9
9
|
import { AgentStorage } from "./session/agent-storage";
|
|
10
10
|
import type { AuthCredential } from "./session/auth-storage";
|
|
11
11
|
|
|
@@ -144,50 +144,6 @@ export async function migrateSessionsFromAgentRoot(): Promise<void> {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
/**
|
|
148
|
-
* Move fd/rg binaries from tools/ to bin/ if they exist.
|
|
149
|
-
*/
|
|
150
|
-
async function migrateToolsToBin(): Promise<void> {
|
|
151
|
-
const agentDir = getAgentDir();
|
|
152
|
-
const toolsDir = path.join(agentDir, "tools");
|
|
153
|
-
const binDir = getBinDir();
|
|
154
|
-
|
|
155
|
-
if (!fs.existsSync(toolsDir)) return;
|
|
156
|
-
|
|
157
|
-
const binaries = ["fd", "rg", "fd.exe", "rg.exe"];
|
|
158
|
-
let movedAny = false;
|
|
159
|
-
|
|
160
|
-
for (const bin of binaries) {
|
|
161
|
-
const oldPath = path.join(toolsDir, bin);
|
|
162
|
-
const newPath = path.join(binDir, bin);
|
|
163
|
-
if (!fs.existsSync(oldPath)) continue;
|
|
164
|
-
|
|
165
|
-
if (!fs.existsSync(binDir)) {
|
|
166
|
-
await fs.promises.mkdir(binDir, { recursive: true });
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (!fs.existsSync(newPath)) {
|
|
170
|
-
try {
|
|
171
|
-
await fs.promises.rename(oldPath, newPath);
|
|
172
|
-
movedAny = true;
|
|
173
|
-
} catch (error) {
|
|
174
|
-
logger.warn("Failed to migrate binary", { from: oldPath, to: newPath, error: String(error) });
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
// Target exists, just delete the old one
|
|
178
|
-
try {
|
|
179
|
-
await fs.promises.rm(oldPath, { force: true });
|
|
180
|
-
} catch {
|
|
181
|
-
// Ignore
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (movedAny) {
|
|
187
|
-
console.log(chalk.green(`Migrated managed binaries tools/ → bin/`));
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
147
|
/**
|
|
192
148
|
* Run all migrations. Called once on startup.
|
|
193
149
|
*
|
|
@@ -201,7 +157,6 @@ export async function runMigrations(_cwd: string): Promise<{
|
|
|
201
157
|
// Then: run data migrations
|
|
202
158
|
const migratedAuthProviders = await migrateAuthToAgentDb();
|
|
203
159
|
await migrateSessionsFromAgentRoot();
|
|
204
|
-
await migrateToolsToBin();
|
|
205
160
|
|
|
206
161
|
return { migratedAuthProviders, deprecationWarnings: [] };
|
|
207
162
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Generic selector component for hooks.
|
|
3
3
|
* Displays a list of string options with keyboard navigation.
|
|
4
4
|
*/
|
|
5
|
-
import { Container, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { Container, matchesKey, Spacer, Text, type TUI, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { theme } from "../../modes/theme/theme";
|
|
7
7
|
import { CountdownTimer } from "./countdown-timer";
|
|
8
8
|
import { DynamicBorder } from "./dynamic-border";
|
|
@@ -11,12 +11,36 @@ export interface HookSelectorOptions {
|
|
|
11
11
|
tui?: TUI;
|
|
12
12
|
timeout?: number;
|
|
13
13
|
initialIndex?: number;
|
|
14
|
+
outline?: boolean;
|
|
15
|
+
maxVisible?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class OutlinedList extends Container {
|
|
19
|
+
private lines: string[] = [];
|
|
20
|
+
|
|
21
|
+
setLines(lines: string[]): void {
|
|
22
|
+
this.lines = lines;
|
|
23
|
+
this.invalidate();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
render(width: number): string[] {
|
|
27
|
+
const borderColor = (text: string) => theme.fg("border", text);
|
|
28
|
+
const horizontal = borderColor(theme.boxSharp.horizontal.repeat(Math.max(1, width)));
|
|
29
|
+
const innerWidth = Math.max(1, width - 2);
|
|
30
|
+
const content = this.lines.map(line => {
|
|
31
|
+
const pad = Math.max(0, innerWidth - visibleWidth(line));
|
|
32
|
+
return `${borderColor(theme.boxSharp.vertical)}${line}${" ".repeat(pad)}${borderColor(theme.boxSharp.vertical)}`;
|
|
33
|
+
});
|
|
34
|
+
return [horizontal, ...content, horizontal];
|
|
35
|
+
}
|
|
14
36
|
}
|
|
15
37
|
|
|
16
38
|
export class HookSelectorComponent extends Container {
|
|
17
39
|
private options: string[];
|
|
18
40
|
private selectedIndex: number;
|
|
19
|
-
private
|
|
41
|
+
private maxVisible: number;
|
|
42
|
+
private listContainer: Container | undefined;
|
|
43
|
+
private outlinedList: OutlinedList | undefined;
|
|
20
44
|
private onSelectCallback: (option: string) => void;
|
|
21
45
|
private onCancelCallback: () => void;
|
|
22
46
|
private titleText: Text;
|
|
@@ -34,6 +58,7 @@ export class HookSelectorComponent extends Container {
|
|
|
34
58
|
|
|
35
59
|
this.options = options;
|
|
36
60
|
this.selectedIndex = Math.min(opts?.initialIndex ?? 0, options.length - 1);
|
|
61
|
+
this.maxVisible = Math.max(3, opts?.maxVisible ?? 12);
|
|
37
62
|
this.onSelectCallback = onSelect;
|
|
38
63
|
this.onCancelCallback = onCancel;
|
|
39
64
|
this.baseTitle = title;
|
|
@@ -62,8 +87,13 @@ export class HookSelectorComponent extends Container {
|
|
|
62
87
|
);
|
|
63
88
|
}
|
|
64
89
|
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
if (opts?.outline) {
|
|
91
|
+
this.outlinedList = new OutlinedList();
|
|
92
|
+
this.addChild(this.outlinedList);
|
|
93
|
+
} else {
|
|
94
|
+
this.listContainer = new Container();
|
|
95
|
+
this.addChild(this.listContainer);
|
|
96
|
+
}
|
|
67
97
|
this.addChild(new Spacer(1));
|
|
68
98
|
this.addChild(new Text(theme.fg("dim", "up/down navigate enter select esc cancel"), 1, 0));
|
|
69
99
|
this.addChild(new Spacer(1));
|
|
@@ -73,13 +103,31 @@ export class HookSelectorComponent extends Container {
|
|
|
73
103
|
}
|
|
74
104
|
|
|
75
105
|
private updateList(): void {
|
|
76
|
-
|
|
77
|
-
|
|
106
|
+
const lines: string[] = [];
|
|
107
|
+
const startIndex = Math.max(
|
|
108
|
+
0,
|
|
109
|
+
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.options.length - this.maxVisible),
|
|
110
|
+
);
|
|
111
|
+
const endIndex = Math.min(startIndex + this.maxVisible, this.options.length);
|
|
112
|
+
|
|
113
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
78
114
|
const isSelected = i === this.selectedIndex;
|
|
79
115
|
const text = isSelected
|
|
80
116
|
? theme.fg("accent", `${theme.nav.cursor} `) + theme.fg("accent", this.options[i])
|
|
81
117
|
: ` ${theme.fg("text", this.options[i])}`;
|
|
82
|
-
|
|
118
|
+
lines.push(text);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (startIndex > 0 || endIndex < this.options.length) {
|
|
122
|
+
lines.push(theme.fg("dim", ` (${this.selectedIndex + 1}/${this.options.length})`));
|
|
123
|
+
}
|
|
124
|
+
if (this.outlinedList) {
|
|
125
|
+
this.outlinedList.setLines(lines);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.listContainer?.clear();
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
this.listContainer?.addChild(new Text(line, 1, 0));
|
|
83
131
|
}
|
|
84
132
|
}
|
|
85
133
|
|
|
@@ -189,6 +189,29 @@ export const SETTINGS_DEFS: SettingDef[] = [
|
|
|
189
189
|
get: sm => sm.getNotificationOnComplete(),
|
|
190
190
|
set: (sm, v) => sm.setNotificationOnComplete(v as NotificationMethod),
|
|
191
191
|
},
|
|
192
|
+
{
|
|
193
|
+
id: "askTimeout",
|
|
194
|
+
tab: "behavior",
|
|
195
|
+
type: "enum",
|
|
196
|
+
label: "Ask tool timeout",
|
|
197
|
+
description: "Auto-select recommended option after timeout (disabled in plan mode)",
|
|
198
|
+
values: ["off", "15", "30", "60", "120"],
|
|
199
|
+
get: sm => {
|
|
200
|
+
const timeout = sm.getAskTimeout();
|
|
201
|
+
return timeout === null ? "off" : String(timeout / 1000);
|
|
202
|
+
},
|
|
203
|
+
set: (sm, v) => sm.setAskTimeout(v === "off" ? null : Number.parseInt(v, 10)),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "askNotification",
|
|
207
|
+
tab: "behavior",
|
|
208
|
+
type: "enum",
|
|
209
|
+
label: "Ask notification",
|
|
210
|
+
description: "Notify when ask tool is waiting for input",
|
|
211
|
+
values: ["auto", "bell", "osc99", "osc9", "off"],
|
|
212
|
+
get: sm => sm.getAskNotification(),
|
|
213
|
+
set: (sm, v) => sm.setAskNotification(v as NotificationMethod),
|
|
214
|
+
},
|
|
192
215
|
{
|
|
193
216
|
id: "startupQuiet",
|
|
194
217
|
tab: "behavior",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Component, TUI } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import type { Component, OverlayHandle, TUI } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { KeybindingsManager } from "../../config/keybindings";
|
|
@@ -18,6 +18,17 @@ import type { InteractiveModeContext } from "../../modes/types";
|
|
|
18
18
|
import { setTerminalTitle } from "../../utils/title-generator";
|
|
19
19
|
|
|
20
20
|
export class ExtensionUiController {
|
|
21
|
+
private hookSelectorOverlay: OverlayHandle | undefined;
|
|
22
|
+
private hookInputOverlay: OverlayHandle | undefined;
|
|
23
|
+
|
|
24
|
+
private readonly dialogOverlayOptions = {
|
|
25
|
+
anchor: "bottom-center",
|
|
26
|
+
width: "80%",
|
|
27
|
+
minWidth: 40,
|
|
28
|
+
maxHeight: "70%",
|
|
29
|
+
margin: 1,
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
21
32
|
constructor(private ctx: InteractiveModeContext) {}
|
|
22
33
|
|
|
23
34
|
/**
|
|
@@ -491,6 +502,9 @@ export class ExtensionUiController {
|
|
|
491
502
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
492
503
|
): Promise<string | undefined> {
|
|
493
504
|
const { promise, resolve } = Promise.withResolvers<string | undefined>();
|
|
505
|
+
this.hookSelectorOverlay?.hide();
|
|
506
|
+
this.hookSelectorOverlay = undefined;
|
|
507
|
+
const maxVisible = Math.max(4, Math.min(15, this.ctx.ui.terminal.rows - 12));
|
|
494
508
|
this.ctx.hookSelector = new HookSelectorComponent(
|
|
495
509
|
title,
|
|
496
510
|
options,
|
|
@@ -502,13 +516,15 @@ export class ExtensionUiController {
|
|
|
502
516
|
this.hideHookSelector();
|
|
503
517
|
resolve(undefined);
|
|
504
518
|
},
|
|
505
|
-
{
|
|
519
|
+
{
|
|
520
|
+
initialIndex: dialogOptions?.initialIndex,
|
|
521
|
+
timeout: dialogOptions?.timeout,
|
|
522
|
+
tui: this.ctx.ui,
|
|
523
|
+
outline: dialogOptions?.outline,
|
|
524
|
+
maxVisible,
|
|
525
|
+
},
|
|
506
526
|
);
|
|
507
|
-
|
|
508
|
-
this.ctx.editorContainer.clear();
|
|
509
|
-
this.ctx.editorContainer.addChild(this.ctx.hookSelector);
|
|
510
|
-
this.ctx.ui.setFocus(this.ctx.hookSelector);
|
|
511
|
-
this.ctx.ui.requestRender();
|
|
527
|
+
this.hookSelectorOverlay = this.ctx.ui.showOverlay(this.ctx.hookSelector, this.dialogOverlayOptions);
|
|
512
528
|
return promise;
|
|
513
529
|
}
|
|
514
530
|
|
|
@@ -516,8 +532,9 @@ export class ExtensionUiController {
|
|
|
516
532
|
* Hide the hook selector.
|
|
517
533
|
*/
|
|
518
534
|
hideHookSelector(): void {
|
|
519
|
-
this.ctx.
|
|
520
|
-
this.
|
|
535
|
+
this.ctx.hookSelector?.dispose();
|
|
536
|
+
this.hookSelectorOverlay?.hide();
|
|
537
|
+
this.hookSelectorOverlay = undefined;
|
|
521
538
|
this.ctx.hookSelector = undefined;
|
|
522
539
|
this.ctx.ui.setFocus(this.ctx.editor);
|
|
523
540
|
this.ctx.ui.requestRender();
|
|
@@ -536,6 +553,8 @@ export class ExtensionUiController {
|
|
|
536
553
|
*/
|
|
537
554
|
showHookInput(title: string, placeholder?: string): Promise<string | undefined> {
|
|
538
555
|
const { promise, resolve } = Promise.withResolvers<string | undefined>();
|
|
556
|
+
this.hookInputOverlay?.hide();
|
|
557
|
+
this.hookInputOverlay = undefined;
|
|
539
558
|
this.ctx.hookInput = new HookInputComponent(
|
|
540
559
|
title,
|
|
541
560
|
placeholder,
|
|
@@ -548,11 +567,7 @@ export class ExtensionUiController {
|
|
|
548
567
|
resolve(undefined);
|
|
549
568
|
},
|
|
550
569
|
);
|
|
551
|
-
|
|
552
|
-
this.ctx.editorContainer.clear();
|
|
553
|
-
this.ctx.editorContainer.addChild(this.ctx.hookInput);
|
|
554
|
-
this.ctx.ui.setFocus(this.ctx.hookInput);
|
|
555
|
-
this.ctx.ui.requestRender();
|
|
570
|
+
this.hookInputOverlay = this.ctx.ui.showOverlay(this.ctx.hookInput, this.dialogOverlayOptions);
|
|
556
571
|
return promise;
|
|
557
572
|
}
|
|
558
573
|
|
|
@@ -560,8 +575,9 @@ export class ExtensionUiController {
|
|
|
560
575
|
* Hide the hook input.
|
|
561
576
|
*/
|
|
562
577
|
hideHookInput(): void {
|
|
563
|
-
this.ctx.
|
|
564
|
-
this.
|
|
578
|
+
this.ctx.hookInput?.dispose();
|
|
579
|
+
this.hookInputOverlay?.hide();
|
|
580
|
+
this.hookInputOverlay = undefined;
|
|
565
581
|
this.ctx.hookInput = undefined;
|
|
566
582
|
this.ctx.ui.setFocus(this.ctx.editor);
|
|
567
583
|
this.ctx.ui.requestRender();
|
|
@@ -112,7 +112,7 @@ export class RpcClient {
|
|
|
112
112
|
args.push(...this.options.args);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
this.process = ptree.
|
|
115
|
+
this.process = ptree.spawn(["bun", cliPath, ...args], {
|
|
116
116
|
cwd: this.options.cwd,
|
|
117
117
|
env: { ...process.env, ...this.options.env },
|
|
118
118
|
stdin: "pipe",
|
|
@@ -140,7 +140,7 @@ export class RpcClient {
|
|
|
140
140
|
await Bun.sleep(100);
|
|
141
141
|
|
|
142
142
|
try {
|
|
143
|
-
const exitCode = await Promise.race([this.process.exited, Bun.sleep(
|
|
143
|
+
const exitCode = await Promise.race([this.process.exited, Bun.sleep(500).then(() => null)]);
|
|
144
144
|
if (exitCode !== null) {
|
|
145
145
|
throw new Error(
|
|
146
146
|
`Agent process exited immediately with code ${exitCode}. Stderr: ${this.process.peekStderr()}`,
|
|
@@ -154,11 +154,11 @@ export class RpcClient {
|
|
|
154
154
|
/**
|
|
155
155
|
* Stop the RPC agent process.
|
|
156
156
|
*/
|
|
157
|
-
|
|
157
|
+
stop() {
|
|
158
158
|
if (!this.process) return;
|
|
159
159
|
|
|
160
160
|
this.lineReader?.cancel();
|
|
161
|
-
|
|
161
|
+
this.process.kill();
|
|
162
162
|
|
|
163
163
|
this.process = null;
|
|
164
164
|
this.lineReader = null;
|