@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an `externalEditor` settings.json override for Ctrl+G external editor commands, with default fallbacks to Notepad on Windows and `nano` elsewhere ([#6122](https://github.com/earendil-works/pi/issues/6122)).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed `--session` and `SessionManager.open()` to reject non-empty invalid session files without overwriting them ([#6002](https://github.com/earendil-works/pi/issues/6002)).
|
- Fixed `--session` and `SessionManager.open()` to reject non-empty invalid session files without overwriting them ([#6002](https://github.com/earendil-works/pi/issues/6002)).
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ The editor can be temporarily replaced by other UI, like built-in `/settings` or
|
|||||||
| File reference | Type `@` to fuzzy-search project files |
|
| File reference | Type `@` to fuzzy-search project files |
|
||||||
| Path completion | Tab to complete paths |
|
| Path completion | Tab to complete paths |
|
||||||
| Multi-line | Shift+Enter (or Ctrl+Enter on Windows Terminal) |
|
| Multi-line | Shift+Enter (or Ctrl+Enter on Windows Terminal) |
|
||||||
|
| External editor | Ctrl+G opens `externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere |
|
||||||
| Images | Ctrl+V to paste (Alt+V on Windows), or drag onto terminal |
|
| Images | Ctrl+V to paste (Alt+V on Windows), or drag onto terminal |
|
||||||
| Bash commands | `!command` runs and sends output to LLM, `!!command` runs without sending |
|
| Bash commands | `!command` runs and sends output to LLM, `!!command` runs without sending |
|
||||||
|
|
||||||
@@ -663,7 +664,7 @@ pi --thinking high "Solve this complex problem"
|
|||||||
| `PI_SKIP_VERSION_CHECK` | Skip the Pi version update check at startup. This prevents the `pi.dev` latest-version request |
|
| `PI_SKIP_VERSION_CHECK` | Skip the Pi version update check at startup. This prevents the `pi.dev` latest-version request |
|
||||||
| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers. Use `1`/`true`/`yes` to enable or `0`/`false`/`no` to disable. This does not disable update checks |
|
| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers. Use `1`/`true`/`yes` to enable or `0`/`false`/`no` to disable. This does not disable update checks |
|
||||||
| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache (Anthropic: 1h, OpenAI: 24h) |
|
| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache (Anthropic: 1h, OpenAI: 24h) |
|
||||||
| `VISUAL`, `EDITOR` | External editor for Ctrl+G |
|
| `VISUAL`, `EDITOR` | Fallback external editor for Ctrl+G when `externalEditor` is unset; defaults to Notepad on Windows and `nano` elsewhere |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1
|
|||||||
| `app.clear` | `ctrl+c` | Clear editor |
|
| `app.clear` | `ctrl+c` | Clear editor |
|
||||||
| `app.exit` | `ctrl+d` | Exit (when editor empty) |
|
| `app.exit` | `ctrl+d` | Exit (when editor empty) |
|
||||||
| `app.suspend` | `ctrl+z` (none on Windows) | Suspend to background |
|
| `app.suspend` | `ctrl+z` (none on Windows) | Suspend to background |
|
||||||
| `app.editor.external` | `ctrl+g` | Open in external editor (`$VISUAL` or `$EDITOR`) |
|
| `app.editor.external` | `ctrl+g` | Open in external editor (`externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere) |
|
||||||
| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows) | Paste image from clipboard |
|
| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows) | Paste image from clipboard |
|
||||||
|
|
||||||
### Sessions
|
### Sessions
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ Use `/trust` in interactive mode to save a project trust decision for future ses
|
|||||||
| Setting | Type | Default | Description |
|
| Setting | Type | Default | Description |
|
||||||
|---------|------|---------|-------------|
|
|---------|------|---------|-------------|
|
||||||
| `theme` | string | `"dark"` | Theme name (`"dark"`, `"light"`, or custom) |
|
| `theme` | string | `"dark"` | Theme name (`"dark"`, `"light"`, or custom) |
|
||||||
|
| `externalEditor` | string | `$VISUAL`, then `$EDITOR`, then Notepad on Windows or `nano` elsewhere | Command for Ctrl+G external editor; takes precedence over environment variables |
|
||||||
| `quietStartup` | boolean | `false` | Hide startup header |
|
| `quietStartup` | boolean | `false` | Hide startup header |
|
||||||
| `defaultProjectTrust` | string | `"ask"` | Fallback project trust behavior: `"ask"`, `"always"`, or `"never"`. Global setting only |
|
| `defaultProjectTrust` | string | `"ask"` | Fallback project trust behavior: `"ask"`, `"always"`, or `"never"`. Global setting only |
|
||||||
| `collapseChangelog` | boolean | `false` | Show condensed changelog after updates |
|
| `collapseChangelog` | boolean | `false` | Show condensed changelog after updates |
|
||||||
@@ -63,6 +64,14 @@ Use `/trust` in interactive mode to save a project trust decision for future ses
|
|||||||
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
|
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
|
||||||
| `showHardwareCursor` | boolean | `false` | Show the terminal cursor while TUI positions it for IME support |
|
| `showHardwareCursor` | boolean | `false` | Show the terminal cursor while TUI positions it for IME support |
|
||||||
|
|
||||||
|
For VS Code, include `--wait` so pi resumes after the editor exits:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"externalEditor": "code --wait"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Telemetry and update checks
|
### Telemetry and update checks
|
||||||
|
|
||||||
`enableInstallTelemetry` only controls the anonymous install/update ping to `https://pi.dev/api/report-install`. Opting out of telemetry does not disable update checks; Pi can still fetch `https://pi.dev/api/latest-version` to look for the latest version.
|
`enableInstallTelemetry` only controls the anonymous install/update ping to `https://pi.dev/api/report-install`. Opting out of telemetry does not disable update checks; Pi can still fetch `https://pi.dev/api/latest-version` to look for the latest version.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ The editor can be replaced temporarily by built-in UI such as `/settings` or by
|
|||||||
| Images | Paste with Ctrl+V, Alt+V on Windows, or drag into the terminal |
|
| Images | Paste with Ctrl+V, Alt+V on Windows, or drag into the terminal |
|
||||||
| Shell command | `!command` runs and sends output to the model |
|
| Shell command | `!command` runs and sends output to the model |
|
||||||
| Hidden shell command | `!!command` runs without sending output to the model |
|
| Hidden shell command | `!!command` runs without sending output to the model |
|
||||||
| External editor | Ctrl+G opens `$VISUAL` or `$EDITOR` |
|
| External editor | Ctrl+G opens `externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere |
|
||||||
|
|
||||||
See [Keybindings](keybindings.md) for all shortcuts and customization.
|
See [Keybindings](keybindings.md) for all shortcuts and customization.
|
||||||
|
|
||||||
@@ -297,7 +297,7 @@ pi --exclude-tools ask_question
|
|||||||
| `PI_SKIP_VERSION_CHECK` | Skip the Pi version update check at startup. This prevents the `pi.dev` latest-version request |
|
| `PI_SKIP_VERSION_CHECK` | Skip the Pi version update check at startup. This prevents the `pi.dev` latest-version request |
|
||||||
| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers: `1`/`true`/`yes` or `0`/`false`/`no`. This does not disable update checks |
|
| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers: `1`/`true`/`yes` or `0`/`false`/`no`. This does not disable update checks |
|
||||||
| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache where supported |
|
| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache where supported |
|
||||||
| `VISUAL`, `EDITOR` | External editor for Ctrl+G |
|
| `VISUAL`, `EDITOR` | Fallback external editor for Ctrl+G when `externalEditor` is unset; defaults to Notepad on Windows and `nano` elsewhere |
|
||||||
|
|
||||||
## Design Principles
|
## Design Principles
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export interface Settings {
|
|||||||
branchSummary?: BranchSummarySettings;
|
branchSummary?: BranchSummarySettings;
|
||||||
retry?: RetrySettings;
|
retry?: RetrySettings;
|
||||||
hideThinkingBlock?: boolean;
|
hideThinkingBlock?: boolean;
|
||||||
|
externalEditor?: string; // Command for Ctrl+G external editor; takes precedence over VISUAL/EDITOR
|
||||||
shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)
|
shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)
|
||||||
quietStartup?: boolean;
|
quietStartup?: boolean;
|
||||||
defaultProjectTrust?: DefaultProjectTrust; // default: "ask"; global setting only
|
defaultProjectTrust?: DefaultProjectTrust; // default: "ask"; global setting only
|
||||||
@@ -841,6 +842,18 @@ export class SettingsManager {
|
|||||||
return this.settings.hideThinkingBlock ?? false;
|
return this.settings.hideThinkingBlock ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getExternalEditorCommand(): string | undefined {
|
||||||
|
const configuredEditor = this.settings.externalEditor;
|
||||||
|
if (typeof configuredEditor === "string" && configuredEditor.trim() !== "") {
|
||||||
|
return configuredEditor;
|
||||||
|
}
|
||||||
|
const environmentEditor = process.env.VISUAL || process.env.EDITOR;
|
||||||
|
if (environmentEditor) {
|
||||||
|
return environmentEditor;
|
||||||
|
}
|
||||||
|
return process.platform === "win32" ? "notepad" : "nano";
|
||||||
|
}
|
||||||
|
|
||||||
setHideThinkingBlock(hide: boolean): void {
|
setHideThinkingBlock(hide: boolean): void {
|
||||||
this.globalSettings.hideThinkingBlock = hide;
|
this.globalSettings.hideThinkingBlock = hide;
|
||||||
this.markModified("hideThinkingBlock");
|
this.markModified("hideThinkingBlock");
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export class ExtensionEditorComponent extends Container implements Focusable {
|
|||||||
private onCancelCallback: () => void;
|
private onCancelCallback: () => void;
|
||||||
private tui: TUI;
|
private tui: TUI;
|
||||||
private keybindings: KeybindingsManager;
|
private keybindings: KeybindingsManager;
|
||||||
|
private externalEditorCommand: string | undefined;
|
||||||
|
|
||||||
private _focused = false;
|
private _focused = false;
|
||||||
get focused(): boolean {
|
get focused(): boolean {
|
||||||
@@ -46,11 +47,13 @@ export class ExtensionEditorComponent extends Container implements Focusable {
|
|||||||
onSubmit: (value: string) => void,
|
onSubmit: (value: string) => void,
|
||||||
onCancel: () => void,
|
onCancel: () => void,
|
||||||
options?: EditorOptions,
|
options?: EditorOptions,
|
||||||
|
externalEditorCommand?: string,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.tui = tui;
|
this.tui = tui;
|
||||||
this.keybindings = keybindings;
|
this.keybindings = keybindings;
|
||||||
|
this.externalEditorCommand = externalEditorCommand;
|
||||||
this.onSubmitCallback = onSubmit;
|
this.onSubmitCallback = onSubmit;
|
||||||
this.onCancelCallback = onCancel;
|
this.onCancelCallback = onCancel;
|
||||||
|
|
||||||
@@ -76,7 +79,7 @@ export class ExtensionEditorComponent extends Container implements Focusable {
|
|||||||
this.addChild(new Spacer(1));
|
this.addChild(new Spacer(1));
|
||||||
|
|
||||||
// Add hint
|
// Add hint
|
||||||
const hasExternalEditor = !!(process.env.VISUAL || process.env.EDITOR);
|
const hasExternalEditor = !!this.getExternalEditorCommand();
|
||||||
const hint =
|
const hint =
|
||||||
keyHint("tui.select.confirm", "submit") +
|
keyHint("tui.select.confirm", "submit") +
|
||||||
" " +
|
" " +
|
||||||
@@ -110,8 +113,16 @@ export class ExtensionEditorComponent extends Container implements Focusable {
|
|||||||
this.editor.handleInput(keyData);
|
this.editor.handleInput(keyData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getExternalEditorCommand(): string | undefined {
|
||||||
|
const editorCmd = this.externalEditorCommand || process.env.VISUAL || process.env.EDITOR;
|
||||||
|
if (editorCmd) {
|
||||||
|
return editorCmd;
|
||||||
|
}
|
||||||
|
return process.platform === "win32" ? "notepad" : "nano";
|
||||||
|
}
|
||||||
|
|
||||||
private async openExternalEditor(): Promise<void> {
|
private async openExternalEditor(): Promise<void> {
|
||||||
const editorCmd = process.env.VISUAL || process.env.EDITOR;
|
const editorCmd = this.getExternalEditorCommand();
|
||||||
if (!editorCmd) {
|
if (!editorCmd) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2239,6 +2239,8 @@ export class InteractiveMode {
|
|||||||
this.hideExtensionEditor();
|
this.hideExtensionEditor();
|
||||||
resolve(undefined);
|
resolve(undefined);
|
||||||
},
|
},
|
||||||
|
undefined,
|
||||||
|
this.settingsManager.getExternalEditorCommand(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.editorContainer.clear();
|
this.editorContainer.clear();
|
||||||
@@ -3647,10 +3649,9 @@ export class InteractiveMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async openExternalEditor(): Promise<void> {
|
private async openExternalEditor(): Promise<void> {
|
||||||
// Determine editor (respect $VISUAL, then $EDITOR)
|
const editorCmd = this.settingsManager.getExternalEditorCommand();
|
||||||
const editorCmd = process.env.VISUAL || process.env.EDITOR;
|
|
||||||
if (!editorCmd) {
|
if (!editorCmd) {
|
||||||
this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
|
this.showWarning("No editor configured. Set externalEditor in settings.json or $VISUAL/$EDITOR.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -354,6 +354,49 @@ describe("SettingsManager", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("externalEditor", () => {
|
||||||
|
const originalVisual = process.env.VISUAL;
|
||||||
|
const originalEditor = process.env.EDITOR;
|
||||||
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
|
||||||
|
|
||||||
|
function setEditorEnv(visual?: string, editor?: string): void {
|
||||||
|
if (visual === undefined) delete process.env.VISUAL;
|
||||||
|
else process.env.VISUAL = visual;
|
||||||
|
if (editor === undefined) delete process.env.EDITOR;
|
||||||
|
else process.env.EDITOR = editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setEditorEnv(originalVisual, originalEditor);
|
||||||
|
if (originalPlatform) {
|
||||||
|
Object.defineProperty(process, "platform", originalPlatform);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve editor commands by precedence", () => {
|
||||||
|
setEditorEnv("vim", "nano");
|
||||||
|
expect(SettingsManager.inMemory({ externalEditor: "code --wait" }).getExternalEditorCommand()).toBe(
|
||||||
|
"code --wait",
|
||||||
|
);
|
||||||
|
expect(SettingsManager.inMemory().getExternalEditorCommand()).toBe("vim");
|
||||||
|
|
||||||
|
setEditorEnv(undefined, "emacs");
|
||||||
|
expect(SettingsManager.inMemory().getExternalEditorCommand()).toBe("emacs");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to platform defaults", () => {
|
||||||
|
setEditorEnv();
|
||||||
|
Object.defineProperty(process, "platform", { value: "win32" });
|
||||||
|
expect(SettingsManager.inMemory().getExternalEditorCommand()).toBe("notepad");
|
||||||
|
|
||||||
|
Object.defineProperty(process, "platform", { value: "darwin" });
|
||||||
|
expect(SettingsManager.inMemory().getExternalEditorCommand()).toBe("nano");
|
||||||
|
|
||||||
|
Object.defineProperty(process, "platform", { value: "linux" });
|
||||||
|
expect(SettingsManager.inMemory().getExternalEditorCommand()).toBe("nano");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("shellCommandPrefix", () => {
|
describe("shellCommandPrefix", () => {
|
||||||
it("should load shellCommandPrefix from settings", () => {
|
it("should load shellCommandPrefix from settings", () => {
|
||||||
const settingsPath = join(agentDir, "settings.json");
|
const settingsPath = join(agentDir, "settings.json");
|
||||||
|
|||||||
Reference in New Issue
Block a user