@oh-my-pi/pi-coding-agent 6.7.670 → 6.8.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 +28 -0
- package/package.json +6 -7
- package/src/cli/session-picker.ts +27 -28
- package/src/cli/setup-cli.ts +7 -16
- package/src/cli/update-cli.ts +1 -1
- package/src/config.ts +1 -1
- package/src/core/agent-session.ts +202 -37
- package/src/core/agent-storage.ts +1 -1
- package/src/core/auth-storage.ts +15 -25
- package/src/core/bash-executor.ts +63 -105
- package/src/core/custom-commands/loader.ts +1 -1
- package/src/core/custom-tools/loader.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -2
- package/src/core/exec.ts +16 -100
- package/src/core/extensions/index.ts +1 -7
- package/src/core/extensions/loader.ts +1 -1
- package/src/core/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +2 -2
- package/src/core/extensions/wrapper.ts +15 -20
- package/src/core/frontmatter.ts +1 -1
- package/src/core/history-storage.ts +3 -6
- package/src/core/hooks/index.ts +2 -2
- package/src/core/hooks/loader.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +14 -26
- package/src/core/hooks/types.ts +1 -2
- package/src/core/keybindings.ts +1 -1
- package/src/core/mcp/client.ts +13 -13
- package/src/core/mcp/json-rpc.ts +1 -1
- package/src/core/mcp/loader.ts +1 -1
- package/src/core/mcp/manager.ts +2 -2
- package/src/core/mcp/tool-cache.ts +1 -1
- package/src/core/mcp/transports/http.ts +32 -70
- package/src/core/model-registry.ts +1 -1
- package/src/core/plugins/installer.ts +13 -11
- package/src/core/prompt-templates.ts +4 -9
- package/src/core/python-executor.ts +23 -18
- package/src/core/python-gateway-coordinator.ts +29 -28
- package/src/core/python-kernel.ts +230 -211
- package/src/core/sdk.ts +10 -13
- package/src/core/session-manager.ts +1 -1
- package/src/core/settings-manager.ts +22 -9
- package/src/core/skills.ts +1 -1
- package/src/core/ssh/connection-manager.ts +19 -33
- package/src/core/ssh/ssh-executor.ts +39 -35
- package/src/core/ssh/sshfs-mount.ts +14 -33
- package/src/core/storage-migration.ts +1 -1
- package/src/core/streaming-output.ts +183 -127
- package/src/core/system-prompt.ts +119 -79
- package/src/core/title-generator.ts +1 -1
- package/src/core/tools/ask.ts +2 -2
- package/src/core/tools/bash.ts +3 -3
- package/src/core/tools/calculator.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +1 -1
- package/src/core/tools/exa/render.ts +1 -1
- package/src/core/tools/find.ts +39 -71
- package/src/core/tools/gemini-image.ts +1 -1
- package/src/core/tools/grep.ts +88 -100
- package/src/core/tools/index.ts +1 -1
- package/src/core/tools/ls.ts +1 -1
- package/src/core/tools/lsp/client.ts +50 -50
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +1 -1
- package/src/core/tools/lsp/config.ts +1 -1
- package/src/core/tools/lsp/index.ts +2 -4
- package/src/core/tools/lsp/lspmux.ts +1 -1
- package/src/core/tools/lsp/rust-analyzer.ts +2 -2
- package/src/core/tools/lsp/utils.ts +0 -14
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/patch/shared.ts +3 -4
- package/src/core/tools/python.ts +3 -3
- package/src/core/tools/read.ts +29 -68
- package/src/core/tools/render-utils.ts +0 -5
- package/src/core/tools/ssh.ts +3 -3
- package/src/core/tools/task/model-resolver.ts +7 -9
- package/src/core/tools/task/worker.ts +144 -139
- package/src/core/tools/todo-write.ts +1 -1
- package/src/core/tools/truncate.ts +2 -2
- package/src/core/tools/web-fetch.ts +13 -15
- package/src/core/tools/web-scrapers/types.ts +1 -3
- package/src/core/tools/web-scrapers/utils.ts +14 -13
- package/src/core/tools/web-scrapers/youtube.ts +39 -12
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/write.ts +1 -1
- package/src/core/ttsr.ts +1 -1
- package/src/core/utils.ts +1 -187
- package/src/core/voice-controller.ts +1 -1
- package/src/core/voice-supervisor.ts +11 -38
- package/src/core/voice.ts +1 -8
- package/src/discovery/codex.ts +1 -1
- package/src/index.ts +4 -4
- package/src/main.ts +5 -10
- package/src/migrations.ts +1 -1
- package/src/modes/index.ts +7 -40
- package/src/modes/interactive/components/extensions/state-manager.ts +1 -1
- package/src/modes/interactive/components/hook-editor.ts +12 -9
- package/src/modes/interactive/components/login-dialog.ts +24 -11
- package/src/modes/interactive/components/settings-defs.ts +9 -0
- package/src/modes/interactive/components/status-line.ts +36 -35
- package/src/modes/interactive/components/todo-display.ts +1 -1
- package/src/modes/interactive/components/tool-execution.ts +1 -1
- package/src/modes/interactive/controllers/command-controller.ts +50 -84
- package/src/modes/interactive/controllers/extension-ui-controller.ts +76 -76
- package/src/modes/interactive/controllers/input-controller.ts +12 -11
- package/src/modes/interactive/interactive-mode.ts +10 -11
- package/src/modes/interactive/theme/theme.ts +1 -1
- package/src/modes/interactive/types.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +91 -121
- package/src/modes/rpc/rpc-mode.ts +71 -79
- package/src/prompts/system/ttsr-interrupt.md +7 -0
- package/src/utils/clipboard.ts +57 -141
- package/src/utils/shell-snapshot.ts +12 -60
- package/src/utils/shell.ts +35 -56
- package/src/utils/tools-manager.ts +42 -71
- package/src/core/logger.ts +0 -111
- package/src/modes/cleanup.ts +0 -23
package/src/main.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
9
9
|
import { join, resolve } from "node:path";
|
|
10
10
|
import { createInterface } from "node:readline/promises";
|
|
11
11
|
import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
|
|
12
|
+
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
12
13
|
import chalk from "chalk";
|
|
13
14
|
import { type Args, parseArgs, printHelp } from "./cli/args";
|
|
14
15
|
import { parseConfigArgs, printConfigHelp, runConfigCommand } from "./cli/config-cli";
|
|
@@ -31,8 +32,7 @@ import { resolvePromptInput } from "./core/system-prompt";
|
|
|
31
32
|
import { printTimings, time } from "./core/timings";
|
|
32
33
|
import { initializeWithSettings } from "./discovery";
|
|
33
34
|
import { runMigrations, showDeprecationWarnings } from "./migrations";
|
|
34
|
-
import {
|
|
35
|
-
import { InteractiveMode, installTerminalCrashHandlers, runPrintMode, runRpcMode } from "./modes/index";
|
|
35
|
+
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index";
|
|
36
36
|
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme";
|
|
37
37
|
import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog";
|
|
38
38
|
|
|
@@ -352,9 +352,9 @@ async function buildSessionOptions(
|
|
|
352
352
|
|
|
353
353
|
// Auto-discover SYSTEM.md if no CLI system prompt provided
|
|
354
354
|
const systemPromptSource = parsed.systemPrompt ?? discoverSystemPromptFile();
|
|
355
|
-
const resolvedSystemPrompt = resolvePromptInput(systemPromptSource, "system prompt");
|
|
355
|
+
const resolvedSystemPrompt = await resolvePromptInput(systemPromptSource, "system prompt");
|
|
356
356
|
const appendPromptSource = parsed.appendSystemPrompt ?? discoverAppendSystemPromptFile();
|
|
357
|
-
const resolvedAppendPrompt = resolvePromptInput(appendPromptSource, "append system prompt");
|
|
357
|
+
const resolvedAppendPrompt = await resolvePromptInput(appendPromptSource, "append system prompt");
|
|
358
358
|
|
|
359
359
|
if (sessionManager) {
|
|
360
360
|
options.sessionManager = sessionManager;
|
|
@@ -708,7 +708,6 @@ export async function main(args: string[]) {
|
|
|
708
708
|
writeStdout(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`));
|
|
709
709
|
}
|
|
710
710
|
|
|
711
|
-
installTerminalCrashHandlers();
|
|
712
711
|
printTimings();
|
|
713
712
|
await runInteractiveMode(
|
|
714
713
|
session,
|
|
@@ -734,10 +733,6 @@ export async function main(args: string[]) {
|
|
|
734
733
|
});
|
|
735
734
|
await session.dispose();
|
|
736
735
|
stopThemeWatcher();
|
|
737
|
-
await
|
|
738
|
-
if (process.stdout.writableLength > 0) {
|
|
739
|
-
await new Promise<void>((resolve) => process.stdout.once("drain", resolve));
|
|
740
|
-
}
|
|
741
|
-
process.exit(0);
|
|
736
|
+
await postmortem.quit(0);
|
|
742
737
|
}
|
|
743
738
|
}
|
package/src/migrations.ts
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
7
8
|
import chalk from "chalk";
|
|
8
9
|
import { getAgentDbPath, getAgentDir, getBinDir } from "./config";
|
|
9
10
|
import { AgentStorage } from "./core/agent-storage";
|
|
10
11
|
import type { AuthCredential } from "./core/auth-storage";
|
|
11
|
-
import { logger } from "./core/logger";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Migrate legacy oauth.json and settings.json apiKeys to agent.db.
|
package/src/modes/index.ts
CHANGED
|
@@ -1,48 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Run modes for the coding agent.
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
import { emergencyTerminalRestore } from "@oh-my-pi/pi-tui";
|
|
6
|
-
import { runAsyncCleanup } from "./cleanup";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Install handlers that restore terminal state on crash/signal.
|
|
10
|
-
* Must be called before entering interactive mode.
|
|
11
|
-
*/
|
|
12
|
-
export function installTerminalCrashHandlers(): void {
|
|
13
|
-
const cleanup = () => {
|
|
14
|
-
emergencyTerminalRestore();
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// Signals - run async cleanup before exit
|
|
18
|
-
process.on("SIGINT", () => {
|
|
19
|
-
cleanup();
|
|
20
|
-
void runAsyncCleanup().finally(() => process.exit(128 + 2));
|
|
21
|
-
});
|
|
22
|
-
process.on("SIGTERM", () => {
|
|
23
|
-
cleanup();
|
|
24
|
-
void runAsyncCleanup().finally(() => process.exit(128 + 15));
|
|
25
|
-
});
|
|
26
|
-
process.on("SIGHUP", () => {
|
|
27
|
-
cleanup();
|
|
28
|
-
void runAsyncCleanup().finally(() => process.exit(128 + 1));
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// Crashes - exit immediately (async cleanup may not be safe in corrupted state)
|
|
32
|
-
process.on("uncaughtException", (err) => {
|
|
33
|
-
cleanup();
|
|
34
|
-
console.error("Uncaught exception:", err);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
});
|
|
37
|
-
process.on("unhandledRejection", (reason) => {
|
|
38
|
-
cleanup();
|
|
39
|
-
console.error("Unhandled rejection:", reason);
|
|
40
|
-
process.exit(1);
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
4
|
export { InteractiveMode, type InteractiveModeOptions } from "./interactive/interactive-mode";
|
|
45
5
|
export { type PrintModeOptions, runPrintMode } from "./print-mode";
|
|
46
6
|
export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client";
|
|
47
7
|
export { runRpcMode } from "./rpc/rpc-mode";
|
|
48
8
|
export type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc/rpc-types";
|
|
9
|
+
|
|
10
|
+
import { emergencyTerminalRestore } from "@oh-my-pi/pi-tui";
|
|
11
|
+
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
12
|
+
|
|
13
|
+
postmortem.register("terminal-restore", () => {
|
|
14
|
+
emergencyTerminalRestore();
|
|
15
|
+
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Handles data loading, tree building, filtering, and toggle persistence.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
6
7
|
import type { ContextFile } from "../../../../capability/context-file";
|
|
7
8
|
import type { ExtensionModule } from "../../../../capability/extension-module";
|
|
8
9
|
import type { Hook } from "../../../../capability/hook";
|
|
@@ -13,7 +14,6 @@ import type { Skill } from "../../../../capability/skill";
|
|
|
13
14
|
import type { SlashCommand } from "../../../../capability/slash-command";
|
|
14
15
|
import type { CustomTool } from "../../../../capability/tool";
|
|
15
16
|
import type { SourceMeta } from "../../../../capability/types";
|
|
16
|
-
import { logger } from "../../../../core/logger";
|
|
17
17
|
import {
|
|
18
18
|
disableProvider,
|
|
19
19
|
enableProvider,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Supports Ctrl+G for external editor.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import { rm } from "node:fs/promises";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { Container, Editor, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
@@ -75,7 +75,7 @@ export class HookEditorComponent extends Container {
|
|
|
75
75
|
|
|
76
76
|
// Ctrl+G for external editor
|
|
77
77
|
if (matchesKey(keyData, "ctrl+g")) {
|
|
78
|
-
this.openExternalEditor();
|
|
78
|
+
void this.openExternalEditor();
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -83,7 +83,7 @@ export class HookEditorComponent extends Container {
|
|
|
83
83
|
this.editor.handleInput(keyData);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
private openExternalEditor(): void {
|
|
86
|
+
private async openExternalEditor(): Promise<void> {
|
|
87
87
|
const editorCmd = process.env.VISUAL || process.env.EDITOR;
|
|
88
88
|
if (!editorCmd) {
|
|
89
89
|
return;
|
|
@@ -93,21 +93,24 @@ export class HookEditorComponent extends Container {
|
|
|
93
93
|
const tmpFile = path.join(os.tmpdir(), `omp-hook-editor-${nanoid()}.md`);
|
|
94
94
|
|
|
95
95
|
try {
|
|
96
|
-
|
|
96
|
+
await Bun.write(tmpFile, currentText);
|
|
97
97
|
this.tui.stop();
|
|
98
98
|
|
|
99
99
|
const [editor, ...editorArgs] = editorCmd.split(" ");
|
|
100
|
-
const
|
|
101
|
-
|
|
100
|
+
const child = Bun.spawn([editor, ...editorArgs, tmpFile], {
|
|
101
|
+
stdin: "inherit",
|
|
102
|
+
stdout: "inherit",
|
|
103
|
+
stderr: "inherit",
|
|
102
104
|
});
|
|
105
|
+
const exitCode = await child.exited;
|
|
103
106
|
|
|
104
|
-
if (
|
|
105
|
-
const newContent =
|
|
107
|
+
if (exitCode === 0) {
|
|
108
|
+
const newContent = (await Bun.file(tmpFile).text()).replace(/\n$/, "");
|
|
106
109
|
this.editor.setText(newContent);
|
|
107
110
|
}
|
|
108
111
|
} finally {
|
|
109
112
|
try {
|
|
110
|
-
|
|
113
|
+
await rm(tmpFile, { force: true });
|
|
111
114
|
} catch {
|
|
112
115
|
// Ignore cleanup errors
|
|
113
116
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { Container, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { $ } from "bun";
|
|
3
4
|
import { theme } from "../theme/theme";
|
|
4
5
|
import { DynamicBorder } from "./dynamic-border";
|
|
5
6
|
|
|
@@ -83,9 +84,21 @@ export class LoginDialogComponent extends Container {
|
|
|
83
84
|
this.contentContainer.addChild(new Text(theme.fg("warning", instructions), 1, 0));
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
// Try to open browser using
|
|
87
|
-
const
|
|
88
|
-
|
|
87
|
+
// Try to open browser using $
|
|
88
|
+
const openArgs =
|
|
89
|
+
process.platform === "darwin"
|
|
90
|
+
? ["open", url]
|
|
91
|
+
: process.platform === "win32"
|
|
92
|
+
? ["cmd", "/c", "start", "", url]
|
|
93
|
+
: ["xdg-open", url];
|
|
94
|
+
const [openCmd, ...openRest] = openArgs;
|
|
95
|
+
void (async () => {
|
|
96
|
+
try {
|
|
97
|
+
await $`${openCmd} ${openRest}`.quiet().nothrow();
|
|
98
|
+
} catch {
|
|
99
|
+
// Best-effort: browser opening is non-critical
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
89
102
|
|
|
90
103
|
this.tui.requestRender();
|
|
91
104
|
}
|
|
@@ -100,10 +113,10 @@ export class LoginDialogComponent extends Container {
|
|
|
100
113
|
this.contentContainer.addChild(new Text(theme.fg("dim", "(Escape to cancel)"), 1, 0));
|
|
101
114
|
this.tui.requestRender();
|
|
102
115
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
|
117
|
+
this.inputResolver = resolve;
|
|
118
|
+
this.inputRejecter = reject;
|
|
119
|
+
return promise;
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
/**
|
|
@@ -122,10 +135,10 @@ export class LoginDialogComponent extends Container {
|
|
|
122
135
|
this.input.setValue("");
|
|
123
136
|
this.tui.requestRender();
|
|
124
137
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
|
139
|
+
this.inputResolver = resolve;
|
|
140
|
+
this.inputRejecter = reject;
|
|
141
|
+
return promise;
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
/**
|
|
@@ -321,6 +321,15 @@ export const SETTINGS_DEFS: SettingDef[] = [
|
|
|
321
321
|
get: (sm) => sm.getEditPatchMode(),
|
|
322
322
|
set: (sm, v) => sm.setEditPatchMode(v),
|
|
323
323
|
},
|
|
324
|
+
{
|
|
325
|
+
id: "editStreamingAbort",
|
|
326
|
+
tab: "tools",
|
|
327
|
+
type: "boolean",
|
|
328
|
+
label: "Edit streaming abort",
|
|
329
|
+
description: "Abort streaming edit tool calls when patch preview fails",
|
|
330
|
+
get: (sm) => sm.getEditStreamingAbort(),
|
|
331
|
+
set: (sm, v) => sm.setEditStreamingAbort(v),
|
|
332
|
+
},
|
|
324
333
|
{
|
|
325
334
|
id: "readLineNumbers",
|
|
326
335
|
tab: "tools",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { $ } from "bun";
|
|
3
4
|
import { type FSWatcher, watch } from "fs";
|
|
4
5
|
import { dirname, join } from "path";
|
|
5
6
|
import type { AgentSession } from "../../../core/agent-session";
|
|
@@ -158,51 +159,51 @@ export class StatusLineComponent implements Component {
|
|
|
158
159
|
return this.cachedGitStatus;
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
});
|
|
162
|
+
// Fire async fetch, return cached value
|
|
163
|
+
(async () => {
|
|
164
|
+
try {
|
|
165
|
+
const result = await $`git status --porcelain`.quiet().nothrow();
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
if (result.exitCode !== 0) {
|
|
168
|
+
this.cachedGitStatus = null;
|
|
169
|
+
this.gitStatusLastFetch = now;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
const output = result.stdout.toString();
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
let staged = 0;
|
|
176
|
+
let unstaged = 0;
|
|
177
|
+
let untracked = 0;
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
for (const line of output.split("\n")) {
|
|
180
|
+
if (!line) continue;
|
|
181
|
+
const x = line[0];
|
|
182
|
+
const y = line[1];
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
if (x === "?" && y === "?") {
|
|
185
|
+
untracked++;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
if (x && x !== " " && x !== "?") {
|
|
190
|
+
staged++;
|
|
191
|
+
}
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
if (y && y !== " ") {
|
|
194
|
+
unstaged++;
|
|
195
|
+
}
|
|
195
196
|
}
|
|
197
|
+
|
|
198
|
+
this.cachedGitStatus = { staged, unstaged, untracked };
|
|
199
|
+
this.gitStatusLastFetch = now;
|
|
200
|
+
} catch {
|
|
201
|
+
this.cachedGitStatus = null;
|
|
202
|
+
this.gitStatusLastFetch = now;
|
|
196
203
|
}
|
|
204
|
+
})();
|
|
197
205
|
|
|
198
|
-
|
|
199
|
-
this.gitStatusLastFetch = now;
|
|
200
|
-
return this.cachedGitStatus;
|
|
201
|
-
} catch {
|
|
202
|
-
this.cachedGitStatus = null;
|
|
203
|
-
this.gitStatusLastFetch = now;
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
+
return this.cachedGitStatus;
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
private buildSegmentContext(width: number): SegmentContext {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import { logger } from "
|
|
3
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { getArtifactsDir } from "../../../core/tools/task/artifacts";
|
|
5
5
|
import { theme } from "../theme/theme";
|
|
6
6
|
import type { TodoItem } from "../types";
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
Text,
|
|
11
11
|
type TUI,
|
|
12
12
|
} from "@oh-my-pi/pi-tui";
|
|
13
|
-
import { sanitizeText } from "
|
|
13
|
+
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
14
14
|
import { BASH_DEFAULT_PREVIEW_LINES } from "../../../core/tools/bash";
|
|
15
15
|
import { computeEditDiff, computePatchDiff, type EditDiffError, type EditDiffResult } from "../../../core/tools/patch";
|
|
16
16
|
import { PYTHON_DEFAULT_PREVIEW_LINES } from "../../../core/tools/python";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { Loader, Markdown, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { $ } from "bun";
|
|
5
6
|
import { nanoid } from "nanoid";
|
|
6
7
|
import { getDebugLogPath } from "../../../config";
|
|
7
8
|
import { loadCustomShare } from "../../../core/custom-share";
|
|
@@ -22,17 +23,20 @@ export class CommandController {
|
|
|
22
23
|
constructor(private readonly ctx: InteractiveModeContext) {}
|
|
23
24
|
|
|
24
25
|
openInBrowser(urlOrPath: string): void {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
const args =
|
|
27
|
+
process.platform === "darwin"
|
|
28
|
+
? ["open", urlOrPath]
|
|
29
|
+
: process.platform === "win32"
|
|
30
|
+
? ["cmd", "/c", "start", "", urlOrPath]
|
|
31
|
+
: ["xdg-open", urlOrPath];
|
|
32
|
+
const [cmd, ...cmdArgs] = args;
|
|
33
|
+
void (async () => {
|
|
34
|
+
try {
|
|
35
|
+
await $`${cmd} ${cmdArgs}`.quiet().nothrow();
|
|
36
|
+
} catch {
|
|
37
|
+
// Best-effort: browser opening is non-critical
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
async handleExportCommand(text: string): Promise<void> {
|
|
@@ -69,6 +73,13 @@ export class CommandController {
|
|
|
69
73
|
|
|
70
74
|
async handleShareCommand(): Promise<void> {
|
|
71
75
|
const tmpFile = path.join(os.tmpdir(), `${nanoid()}.html`);
|
|
76
|
+
const cleanupTempFile = async () => {
|
|
77
|
+
try {
|
|
78
|
+
await rm(tmpFile, { force: true });
|
|
79
|
+
} catch {
|
|
80
|
+
// Ignore cleanup errors
|
|
81
|
+
}
|
|
82
|
+
};
|
|
72
83
|
try {
|
|
73
84
|
await this.ctx.session.exportToHtml(tmpFile);
|
|
74
85
|
} catch (error: unknown) {
|
|
@@ -85,21 +96,17 @@ export class CommandController {
|
|
|
85
96
|
this.ctx.ui.setFocus(loader);
|
|
86
97
|
this.ctx.ui.requestRender();
|
|
87
98
|
|
|
88
|
-
const restoreEditor = () => {
|
|
99
|
+
const restoreEditor = async () => {
|
|
89
100
|
loader.dispose();
|
|
90
101
|
this.ctx.editorContainer.clear();
|
|
91
102
|
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
92
103
|
this.ctx.ui.setFocus(this.ctx.editor);
|
|
93
|
-
|
|
94
|
-
fs.unlinkSync(tmpFile);
|
|
95
|
-
} catch {
|
|
96
|
-
// Ignore cleanup errors
|
|
97
|
-
}
|
|
104
|
+
await cleanupTempFile();
|
|
98
105
|
};
|
|
99
106
|
|
|
100
107
|
try {
|
|
101
108
|
const result = await customShare.fn(tmpFile);
|
|
102
|
-
restoreEditor();
|
|
109
|
+
await restoreEditor();
|
|
103
110
|
|
|
104
111
|
if (typeof result === "string") {
|
|
105
112
|
this.ctx.showStatus(`Share URL: ${result}`);
|
|
@@ -115,34 +122,26 @@ export class CommandController {
|
|
|
115
122
|
}
|
|
116
123
|
return;
|
|
117
124
|
} catch (err) {
|
|
118
|
-
restoreEditor();
|
|
125
|
+
await restoreEditor();
|
|
119
126
|
this.ctx.showError(`Custom share failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
129
|
}
|
|
123
130
|
} catch (err) {
|
|
124
|
-
|
|
125
|
-
fs.unlinkSync(tmpFile);
|
|
126
|
-
} catch {
|
|
127
|
-
// Ignore cleanup errors
|
|
128
|
-
}
|
|
131
|
+
await cleanupTempFile();
|
|
129
132
|
this.ctx.showError(err instanceof Error ? err.message : String(err));
|
|
130
133
|
return;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
try {
|
|
134
|
-
const authResult =
|
|
137
|
+
const authResult = await $`gh auth status`.quiet().nothrow();
|
|
135
138
|
if (authResult.exitCode !== 0) {
|
|
136
|
-
|
|
137
|
-
fs.unlinkSync(tmpFile);
|
|
138
|
-
} catch {}
|
|
139
|
+
await cleanupTempFile();
|
|
139
140
|
this.ctx.showError("GitHub CLI is not logged in. Run 'gh auth login' first.");
|
|
140
141
|
return;
|
|
141
142
|
}
|
|
142
143
|
} catch {
|
|
143
|
-
|
|
144
|
-
fs.unlinkSync(tmpFile);
|
|
145
|
-
} catch {}
|
|
144
|
+
await cleanupTempFile();
|
|
146
145
|
this.ctx.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/");
|
|
147
146
|
return;
|
|
148
147
|
}
|
|
@@ -153,71 +152,33 @@ export class CommandController {
|
|
|
153
152
|
this.ctx.ui.setFocus(loader);
|
|
154
153
|
this.ctx.ui.requestRender();
|
|
155
154
|
|
|
156
|
-
const restoreEditor = () => {
|
|
155
|
+
const restoreEditor = async () => {
|
|
157
156
|
loader.dispose();
|
|
158
157
|
this.ctx.editorContainer.clear();
|
|
159
158
|
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
160
159
|
this.ctx.ui.setFocus(this.ctx.editor);
|
|
161
|
-
|
|
162
|
-
fs.unlinkSync(tmpFile);
|
|
163
|
-
} catch {
|
|
164
|
-
// Ignore cleanup errors
|
|
165
|
-
}
|
|
160
|
+
await cleanupTempFile();
|
|
166
161
|
};
|
|
167
162
|
|
|
168
|
-
let proc: ReturnType<typeof Bun.spawn> | null = null;
|
|
169
|
-
|
|
170
163
|
loader.onAbort = () => {
|
|
171
|
-
|
|
172
|
-
restoreEditor();
|
|
164
|
+
void restoreEditor();
|
|
173
165
|
this.ctx.showStatus("Share cancelled");
|
|
174
166
|
};
|
|
175
167
|
|
|
176
168
|
try {
|
|
177
|
-
|
|
178
|
-
stdout: "pipe",
|
|
179
|
-
stderr: "pipe",
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
const readStream = async (stream: ReadableStream<Uint8Array> | null): Promise<string> => {
|
|
183
|
-
if (!stream) return "";
|
|
184
|
-
const reader = stream.getReader();
|
|
185
|
-
const decoder = new TextDecoder();
|
|
186
|
-
let output = "";
|
|
187
|
-
try {
|
|
188
|
-
while (true) {
|
|
189
|
-
const { done, value } = await reader.read();
|
|
190
|
-
if (done) break;
|
|
191
|
-
output += decoder.decode(value, { stream: true });
|
|
192
|
-
}
|
|
193
|
-
} catch {
|
|
194
|
-
// Ignore read errors
|
|
195
|
-
} finally {
|
|
196
|
-
output += decoder.decode();
|
|
197
|
-
reader.releaseLock();
|
|
198
|
-
}
|
|
199
|
-
return output;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const [stdout, stderr, code] = await Promise.all([
|
|
203
|
-
readStream(proc.stdout as ReadableStream<Uint8Array> | null),
|
|
204
|
-
readStream(proc.stderr as ReadableStream<Uint8Array> | null),
|
|
205
|
-
proc.exited.catch(() => 1),
|
|
206
|
-
]);
|
|
207
|
-
const result = { stdout, stderr, code };
|
|
208
|
-
|
|
169
|
+
const result = await $`gh gist create --public=false ${tmpFile}`.quiet().nothrow();
|
|
209
170
|
if (loader.signal.aborted) return;
|
|
210
171
|
|
|
211
|
-
restoreEditor();
|
|
172
|
+
await restoreEditor();
|
|
212
173
|
|
|
213
|
-
if (result.
|
|
214
|
-
const errorMsg = result.stderr
|
|
174
|
+
if (result.exitCode !== 0) {
|
|
175
|
+
const errorMsg = result.stderr.toString("utf-8").trim() || "Unknown error";
|
|
215
176
|
this.ctx.showError(`Failed to create gist: ${errorMsg}`);
|
|
216
177
|
return;
|
|
217
178
|
}
|
|
218
179
|
|
|
219
|
-
const gistUrl = result.stdout
|
|
220
|
-
const gistId = gistUrl
|
|
180
|
+
const gistUrl = result.stdout.toString("utf-8").trim();
|
|
181
|
+
const gistId = gistUrl.split("/").pop();
|
|
221
182
|
if (!gistId) {
|
|
222
183
|
this.ctx.showError("Failed to parse gist ID from gh output");
|
|
223
184
|
return;
|
|
@@ -228,7 +189,7 @@ export class CommandController {
|
|
|
228
189
|
this.openInBrowser(previewUrl);
|
|
229
190
|
} catch (error: unknown) {
|
|
230
191
|
if (!loader.signal.aborted) {
|
|
231
|
-
restoreEditor();
|
|
192
|
+
await restoreEditor();
|
|
232
193
|
this.ctx.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
233
194
|
}
|
|
234
195
|
}
|
|
@@ -420,7 +381,7 @@ export class CommandController {
|
|
|
420
381
|
this.ctx.ui.requestRender();
|
|
421
382
|
}
|
|
422
383
|
|
|
423
|
-
handleDebugCommand(): void {
|
|
384
|
+
async handleDebugCommand(): Promise<void> {
|
|
424
385
|
const width = this.ctx.ui.terminal.columns;
|
|
425
386
|
const allLines = this.ctx.ui.render(width);
|
|
426
387
|
|
|
@@ -442,8 +403,13 @@ export class CommandController {
|
|
|
442
403
|
"",
|
|
443
404
|
].join("\n");
|
|
444
405
|
|
|
445
|
-
|
|
446
|
-
|
|
406
|
+
try {
|
|
407
|
+
await mkdir(path.dirname(debugLogPath), { recursive: true });
|
|
408
|
+
await Bun.write(debugLogPath, debugData);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
this.ctx.showError(`Failed to write debug log: ${error instanceof Error ? error.message : String(error)}`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
447
413
|
|
|
448
414
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
449
415
|
this.ctx.chatContainer.addChild(
|
|
@@ -519,7 +485,7 @@ export class CommandController {
|
|
|
519
485
|
|
|
520
486
|
async handleSkillCommand(skillPath: string, args: string): Promise<void> {
|
|
521
487
|
try {
|
|
522
|
-
const content =
|
|
488
|
+
const content = await Bun.file(skillPath).text();
|
|
523
489
|
const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
|
|
524
490
|
const metaLines = [`Skill: ${skillPath}`];
|
|
525
491
|
if (args) {
|