@oh-my-pi/pi-coding-agent 13.9.11 → 13.9.13
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 +17 -0
- package/package.json +7 -7
- package/src/cli/args.ts +18 -16
- package/src/config/keybindings.ts +6 -0
- package/src/config/model-registry.ts +4 -4
- package/src/config/settings-schema.ts +10 -9
- package/src/debug/log-viewer.ts +11 -7
- package/src/exec/bash-executor.ts +15 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +11 -8
- package/src/modes/components/extensions/extension-list.ts +16 -8
- package/src/modes/components/settings-defs.ts +2 -2
- package/src/modes/components/status-line.ts +5 -9
- package/src/modes/components/tree-selector.ts +4 -6
- package/src/modes/components/welcome.ts +1 -0
- package/src/modes/controllers/command-controller.ts +47 -42
- package/src/modes/controllers/event-controller.ts +12 -9
- package/src/modes/controllers/input-controller.ts +54 -1
- package/src/modes/interactive-mode.ts +4 -10
- package/src/modes/prompt-action-autocomplete.ts +201 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +12 -0
- package/src/patch/index.ts +1 -1
- package/src/prompts/system/system-prompt.md +97 -107
- package/src/prompts/tools/ast-edit.md +5 -2
- package/src/prompts/tools/ast-grep.md +5 -2
- package/src/prompts/tools/inspect-image-system.md +20 -0
- package/src/prompts/tools/inspect-image.md +32 -0
- package/src/session/agent-session.ts +33 -36
- package/src/session/compaction/compaction.ts +26 -29
- package/src/session/session-manager.ts +15 -7
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/fetch.ts +5 -27
- package/src/tools/index.ts +4 -0
- package/src/tools/inspect-image-renderer.ts +103 -0
- package/src/tools/inspect-image.ts +168 -0
- package/src/tools/read.ts +62 -49
- package/src/tools/renderers.ts +2 -0
- package/src/utils/image-input.ts +264 -0
- package/src/web/kagi.ts +0 -42
- package/src/web/scrapers/youtube.ts +0 -17
- package/src/web/search/index.ts +3 -1
- package/src/web/search/provider.ts +4 -1
- package/src/web/search/providers/exa.ts +8 -0
- package/src/web/search/providers/tavily.ts +162 -0
- package/src/web/search/types.ts +1 -0
|
@@ -149,7 +149,6 @@ export class StatusLineComponent implements Component {
|
|
|
149
149
|
#invalidateGitCaches(): void {
|
|
150
150
|
this.#cachedBranch = undefined;
|
|
151
151
|
this.#cachedBranchRepoId = undefined;
|
|
152
|
-
this.#cachedPr = undefined;
|
|
153
152
|
this.#cachedPrContext = undefined;
|
|
154
153
|
}
|
|
155
154
|
#getCurrentBranch(): string | null {
|
|
@@ -261,20 +260,17 @@ export class StatusLineComponent implements Component {
|
|
|
261
260
|
return this.#cachedPr ?? null;
|
|
262
261
|
}
|
|
263
262
|
|
|
264
|
-
|
|
265
|
-
this.#cachedPr = undefined;
|
|
266
|
-
this.#cachedPrContext = undefined;
|
|
267
|
-
}
|
|
263
|
+
const stalePr = this.#cachedPr;
|
|
268
264
|
|
|
269
265
|
// Don't look up if no branch, detached HEAD, default branch, or already in flight
|
|
270
266
|
if (!branch || branch === "detached" || this.#isDefaultBranch(branch) || this.#prLookupInFlight) {
|
|
271
|
-
return null;
|
|
267
|
+
return stalePr ?? null;
|
|
272
268
|
}
|
|
273
269
|
|
|
274
270
|
this.#prLookupInFlight = true;
|
|
275
271
|
const lookupContext = currentContext;
|
|
276
272
|
|
|
277
|
-
// Fire async lookup,
|
|
273
|
+
// Fire async lookup, keep stale value visible until resolved
|
|
278
274
|
(async () => {
|
|
279
275
|
// Helper: only write cache if branch/repo context hasn't changed since launch
|
|
280
276
|
const setCachedPr = (value: { number: number; url: string } | null) => {
|
|
@@ -304,13 +300,13 @@ export class StatusLineComponent implements Component {
|
|
|
304
300
|
setCachedPr(null);
|
|
305
301
|
} finally {
|
|
306
302
|
this.#prLookupInFlight = false;
|
|
307
|
-
if (this.#
|
|
303
|
+
if (this.#onBranchChange) {
|
|
308
304
|
this.#onBranchChange();
|
|
309
305
|
}
|
|
310
306
|
}
|
|
311
307
|
})();
|
|
312
308
|
|
|
313
|
-
return null;
|
|
309
|
+
return stalePr ?? null;
|
|
314
310
|
}
|
|
315
311
|
|
|
316
312
|
#getTokensPerSecond(): number | null {
|
|
@@ -2,6 +2,7 @@ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import {
|
|
3
3
|
type Component,
|
|
4
4
|
Container,
|
|
5
|
+
extractPrintableText,
|
|
5
6
|
Input,
|
|
6
7
|
matchesKey,
|
|
7
8
|
Spacer,
|
|
@@ -745,12 +746,9 @@ class TreeList implements Component {
|
|
|
745
746
|
this.onLabelEdit(selected.node.entry.id, selected.node.label);
|
|
746
747
|
}
|
|
747
748
|
} else {
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
});
|
|
752
|
-
if (!hasControlChars && keyData.length > 0) {
|
|
753
|
-
this.#searchQuery += keyData;
|
|
749
|
+
const printableText = extractPrintableText(keyData);
|
|
750
|
+
if (printableText) {
|
|
751
|
+
this.#searchQuery += printableText;
|
|
754
752
|
this.#applyFilter();
|
|
755
753
|
}
|
|
756
754
|
}
|
|
@@ -122,6 +122,7 @@ export class WelcomeComponent implements Component {
|
|
|
122
122
|
const rightLines = [
|
|
123
123
|
` ${theme.bold(theme.fg("accent", "Tips"))}`,
|
|
124
124
|
` ${theme.fg("dim", "?")}${theme.fg("muted", " for keyboard shortcuts")}`,
|
|
125
|
+
` ${theme.fg("dim", "#")}${theme.fg("muted", " for prompt actions")}`,
|
|
125
126
|
` ${theme.fg("dim", "/")}${theme.fg("muted", " for commands")}`,
|
|
126
127
|
` ${theme.fg("dim", "!")}${theme.fg("muted", " to run bash")}`,
|
|
127
128
|
` ${theme.fg("dim", "$")}${theme.fg("muted", " to run python")}`,
|
|
@@ -435,49 +435,54 @@ export class CommandController {
|
|
|
435
435
|
const expandToolsKey = this.ctx.keybindings.getDisplayString("expandTools") || "Ctrl+O";
|
|
436
436
|
const planModeKey = this.ctx.keybindings.getDisplayString("togglePlanMode") || "Alt+Shift+P";
|
|
437
437
|
const sttKey = this.ctx.keybindings.getDisplayString("toggleSTT") || "Alt+H";
|
|
438
|
+
const copyLineKey = this.ctx.keybindings.getDisplayString("copyLine") || "Alt+Shift+L";
|
|
439
|
+
const copyPromptKey = this.ctx.keybindings.getDisplayString("copyPrompt") || "Alt+Shift+C";
|
|
438
440
|
const hotkeys = `
|
|
439
|
-
**Navigation**
|
|
440
|
-
| Key | Action |
|
|
441
|
-
|-----|--------|
|
|
442
|
-
| \`Arrow keys\` | Move cursor / browse history (Up when empty) |
|
|
443
|
-
| \`Option+Left/Right\` | Move by word |
|
|
444
|
-
| \`Ctrl+A\` / \`Home\` / \`Cmd+Left\` | Start of line |
|
|
445
|
-
| \`Ctrl+E\` / \`End\` / \`Cmd+Right\` | End of line |
|
|
446
|
-
|
|
447
|
-
**Editing**
|
|
448
|
-
| Key | Action |
|
|
449
|
-
|-----|--------|
|
|
450
|
-
| \`Enter\` | Send message |
|
|
451
|
-
| \`Shift+Enter\` / \`Alt+Enter\` | New line |
|
|
452
|
-
| \`Ctrl+W\` / \`Option+Backspace\` | Delete word backwards |
|
|
453
|
-
| \`Ctrl+U\` | Delete to start of line |
|
|
454
|
-
| \`Ctrl+K\` | Delete to end of line |
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
|
460
|
-
|
|
461
|
-
| \`
|
|
462
|
-
| \`
|
|
463
|
-
| \`Ctrl+
|
|
464
|
-
| \`
|
|
465
|
-
| \`Ctrl+
|
|
466
|
-
| \`Shift+
|
|
467
|
-
| \`
|
|
468
|
-
| \`Ctrl+
|
|
469
|
-
|
|
|
470
|
-
| \`Ctrl+
|
|
471
|
-
| \`${
|
|
472
|
-
| \`Ctrl+
|
|
473
|
-
| \`
|
|
474
|
-
|
|
|
475
|
-
|
|
|
476
|
-
|
|
|
477
|
-
|
|
|
478
|
-
|
|
|
479
|
-
|
|
|
480
|
-
|
|
441
|
+
**Navigation**
|
|
442
|
+
| Key | Action |
|
|
443
|
+
|-----|--------|
|
|
444
|
+
| \`Arrow keys\` | Move cursor / browse history (Up when empty) |
|
|
445
|
+
| \`Option+Left/Right\` | Move by word |
|
|
446
|
+
| \`Ctrl+A\` / \`Home\` / \`Cmd+Left\` | Start of line |
|
|
447
|
+
| \`Ctrl+E\` / \`End\` / \`Cmd+Right\` | End of line |
|
|
448
|
+
|
|
449
|
+
**Editing**
|
|
450
|
+
| Key | Action |
|
|
451
|
+
|-----|--------|
|
|
452
|
+
| \`Enter\` | Send message |
|
|
453
|
+
| \`Shift+Enter\` / \`Alt+Enter\` | New line |
|
|
454
|
+
| \`Ctrl+W\` / \`Option+Backspace\` | Delete word backwards |
|
|
455
|
+
| \`Ctrl+U\` | Delete to start of line |
|
|
456
|
+
| \`Ctrl+K\` | Delete to end of line |
|
|
457
|
+
| \`${copyLineKey}\` | Copy current line |
|
|
458
|
+
| \`${copyPromptKey}\` | Copy whole prompt |
|
|
459
|
+
|
|
460
|
+
**Other**
|
|
461
|
+
| Key | Action |
|
|
462
|
+
|-----|--------|
|
|
463
|
+
| \`Tab\` | Path completion / accept autocomplete |
|
|
464
|
+
| \`Escape\` | Cancel autocomplete / abort streaming |
|
|
465
|
+
| \`Ctrl+C\` | Clear editor (first) / exit (second) |
|
|
466
|
+
| \`Ctrl+D\` | Exit (when editor is empty) |
|
|
467
|
+
| \`Ctrl+Z\` | Suspend to background |
|
|
468
|
+
| \`Shift+Tab\` | Cycle thinking level |
|
|
469
|
+
| \`Ctrl+P\` | Cycle role models (slow/default/smol) |
|
|
470
|
+
| \`Shift+Ctrl+P\` | Cycle role models (temporary) |
|
|
471
|
+
| \`Alt+P\` | Select model (temporary) |
|
|
472
|
+
| \`Ctrl+L\` | Select model (set roles) |
|
|
473
|
+
| \`${planModeKey}\` | Toggle plan mode |
|
|
474
|
+
| \`Ctrl+R\` | Search prompt history |
|
|
475
|
+
| \`${expandToolsKey}\` | Toggle tool output expansion |
|
|
476
|
+
| \`Ctrl+T\` | Toggle todo list expansion |
|
|
477
|
+
| \`Ctrl+G\` | Edit message in external editor |
|
|
478
|
+
| \`${sttKey}\` | Toggle speech-to-text recording |
|
|
479
|
+
| \`#\` | Open prompt actions |
|
|
480
|
+
| \`/\` | Slash commands |
|
|
481
|
+
| \`!\` | Run bash command |
|
|
482
|
+
| \`!!\` | Run bash command (excluded from context) |
|
|
483
|
+
| \`$\` | Run Python in shared kernel |
|
|
484
|
+
| \`$$\` | Run Python (excluded from context) |
|
|
485
|
+
`;
|
|
481
486
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
482
487
|
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
483
488
|
this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0));
|
|
@@ -134,8 +134,19 @@ export class EventController {
|
|
|
134
134
|
this.ctx.addMessageToChat(event.message);
|
|
135
135
|
this.ctx.ui.requestRender();
|
|
136
136
|
} else if (event.message.role === "user") {
|
|
137
|
+
const textContent = this.ctx.getUserMessageText(event.message);
|
|
138
|
+
const imageCount =
|
|
139
|
+
typeof event.message.content === "string"
|
|
140
|
+
? 0
|
|
141
|
+
: event.message.content.filter(content => content.type === "image").length;
|
|
142
|
+
const signature = `${textContent}\u0000${imageCount}`;
|
|
143
|
+
|
|
137
144
|
this.#resetReadGroup();
|
|
138
|
-
this.ctx.
|
|
145
|
+
if (this.ctx.optimisticUserMessageSignature !== signature) {
|
|
146
|
+
this.ctx.addMessageToChat(event.message);
|
|
147
|
+
}
|
|
148
|
+
this.ctx.optimisticUserMessageSignature = undefined;
|
|
149
|
+
|
|
139
150
|
if (!event.message.synthetic) {
|
|
140
151
|
this.ctx.editor.setText("");
|
|
141
152
|
this.ctx.updatePendingMessagesDisplay();
|
|
@@ -473,15 +484,7 @@ export class EventController {
|
|
|
473
484
|
isHandoffAction ? "Auto-handoff cancelled" : "Auto context-full maintenance cancelled",
|
|
474
485
|
);
|
|
475
486
|
} else if (event.result) {
|
|
476
|
-
this.ctx.chatContainer.clear();
|
|
477
487
|
this.ctx.rebuildChatFromMessages();
|
|
478
|
-
this.ctx.addMessageToChat({
|
|
479
|
-
role: "compactionSummary",
|
|
480
|
-
tokensBefore: event.result.tokensBefore,
|
|
481
|
-
summary: event.result.summary,
|
|
482
|
-
shortSummary: event.result.shortSummary,
|
|
483
|
-
timestamp: Date.now(),
|
|
484
|
-
});
|
|
485
488
|
this.ctx.statusLine.invalidate();
|
|
486
489
|
this.ctx.updateEditorTopBorder();
|
|
487
490
|
} else if (event.errorMessage) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import { type AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import { copyToClipboard, readImageFromClipboard, sanitizeText } from "@oh-my-pi/pi-natives";
|
|
4
|
+
import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
4
5
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
5
6
|
import { settings } from "../../config/settings";
|
|
7
|
+
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
6
8
|
import { theme } from "../../modes/theme/theme";
|
|
7
9
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
8
10
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
@@ -87,7 +89,8 @@ export class InputController {
|
|
|
87
89
|
this.ctx.editor.onCtrlG = () => void this.openExternalEditor();
|
|
88
90
|
this.ctx.editor.onQuestionMark = () => this.ctx.handleHotkeysCommand();
|
|
89
91
|
this.ctx.editor.onCtrlV = () => this.handleImagePaste();
|
|
90
|
-
this.ctx.
|
|
92
|
+
const copyPromptKeys = this.ctx.keybindings.getKeys("copyPrompt");
|
|
93
|
+
this.ctx.editor.onCopyPrompt = copyPromptKeys.includes("alt+shift+c") ? () => this.handleCopyPrompt() : undefined;
|
|
91
94
|
|
|
92
95
|
// Wire up extension shortcuts
|
|
93
96
|
this.registerExtensionShortcuts();
|
|
@@ -129,6 +132,13 @@ export class InputController {
|
|
|
129
132
|
for (const key of this.ctx.keybindings.getKeys("toggleSTT")) {
|
|
130
133
|
this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handleSTTToggle());
|
|
131
134
|
}
|
|
135
|
+
for (const key of this.ctx.keybindings.getKeys("copyLine")) {
|
|
136
|
+
this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyCurrentLine());
|
|
137
|
+
}
|
|
138
|
+
for (const key of copyPromptKeys) {
|
|
139
|
+
if (key === "alt+shift+c") continue;
|
|
140
|
+
this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyPrompt());
|
|
141
|
+
}
|
|
132
142
|
|
|
133
143
|
this.ctx.editor.onChange = (text: string) => {
|
|
134
144
|
const wasBashMode = this.ctx.isBashMode;
|
|
@@ -318,6 +328,19 @@ export class InputController {
|
|
|
318
328
|
// Include any pending images from clipboard paste
|
|
319
329
|
const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
|
|
320
330
|
this.ctx.pendingImages = [];
|
|
331
|
+
|
|
332
|
+
// Render user message immediately, then let session events catch up
|
|
333
|
+
this.ctx.optimisticUserMessageSignature = `${text}\u0000${images?.length ?? 0}`;
|
|
334
|
+
const optimisticMessage: AgentMessage = {
|
|
335
|
+
role: "user",
|
|
336
|
+
content: [{ type: "text", text }, ...(images ?? [])],
|
|
337
|
+
attribution: "user",
|
|
338
|
+
timestamp: Date.now(),
|
|
339
|
+
};
|
|
340
|
+
this.ctx.addMessageToChat(optimisticMessage);
|
|
341
|
+
this.ctx.editor.setText("");
|
|
342
|
+
this.ctx.ui.requestRender();
|
|
343
|
+
|
|
321
344
|
this.ctx.onInputCallback({ text, images });
|
|
322
345
|
}
|
|
323
346
|
this.ctx.editor.addToHistory(text);
|
|
@@ -503,6 +526,36 @@ export class InputController {
|
|
|
503
526
|
}
|
|
504
527
|
}
|
|
505
528
|
|
|
529
|
+
createAutocompleteProvider(commands: SlashCommand[], basePath: string): AutocompleteProvider {
|
|
530
|
+
return createPromptActionAutocompleteProvider({
|
|
531
|
+
commands,
|
|
532
|
+
basePath,
|
|
533
|
+
keybindings: this.ctx.keybindings,
|
|
534
|
+
copyCurrentLine: () => this.handleCopyCurrentLine(),
|
|
535
|
+
copyPrompt: () => this.handleCopyPrompt(),
|
|
536
|
+
moveCursorToLineStart: () => this.ctx.editor.moveToLineStart(),
|
|
537
|
+
moveCursorToLineEnd: () => this.ctx.editor.moveToLineEnd(),
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Copy the current editor line to the system clipboard. */
|
|
542
|
+
handleCopyCurrentLine(): void {
|
|
543
|
+
const { line } = this.ctx.editor.getCursor();
|
|
544
|
+
const text = this.ctx.editor.getLines()[line] || "";
|
|
545
|
+
if (!text) {
|
|
546
|
+
this.ctx.showStatus("Nothing to copy");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
copyToClipboard(text);
|
|
551
|
+
const sanitized = sanitizeText(text);
|
|
552
|
+
const preview = sanitized.length > 30 ? `${sanitized.slice(0, 30)}...` : sanitized;
|
|
553
|
+
this.ctx.showStatus(`Copied line: ${preview}`);
|
|
554
|
+
} catch {
|
|
555
|
+
this.ctx.showWarning("Failed to copy to clipboard");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
506
559
|
/** Copy current prompt text to system clipboard. */
|
|
507
560
|
handleCopyPrompt(): void {
|
|
508
561
|
const text = this.ctx.editor.getText();
|
|
@@ -6,15 +6,7 @@ import * as path from "node:path";
|
|
|
6
6
|
import { type Agent, type AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
7
7
|
import type { AssistantMessage, ImageContent, Message, Model, UsageReport } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import type { Component, Loader, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
9
|
-
import {
|
|
10
|
-
CombinedAutocompleteProvider,
|
|
11
|
-
Container,
|
|
12
|
-
Markdown,
|
|
13
|
-
ProcessTerminal,
|
|
14
|
-
Spacer,
|
|
15
|
-
Text,
|
|
16
|
-
TUI,
|
|
17
|
-
} from "@oh-my-pi/pi-tui";
|
|
9
|
+
import { Container, Markdown, ProcessTerminal, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
|
|
18
10
|
import { APP_NAME, getProjectDir, hsvToRgb, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
19
11
|
import chalk from "chalk";
|
|
20
12
|
import { KeybindingsManager } from "../config/keybindings";
|
|
@@ -123,6 +115,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
123
115
|
retryEscapeHandler?: () => void;
|
|
124
116
|
unsubscribe?: () => void;
|
|
125
117
|
onInputCallback?: (input: { text: string; images?: ImageContent[] }) => void;
|
|
118
|
+
optimisticUserMessageSignature: string | undefined = undefined;
|
|
126
119
|
lastSigintTime = 0;
|
|
127
120
|
lastEscapeTime = 0;
|
|
128
121
|
shutdownRequested = false;
|
|
@@ -389,7 +382,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
389
382
|
name: cmd.name,
|
|
390
383
|
description: cmd.description,
|
|
391
384
|
}));
|
|
392
|
-
const autocompleteProvider =
|
|
385
|
+
const autocompleteProvider = this.#inputController.createAutocompleteProvider(
|
|
393
386
|
[...this.#pendingSlashCommands, ...fileSlashCommands],
|
|
394
387
|
basePath,
|
|
395
388
|
);
|
|
@@ -855,6 +848,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
855
848
|
}
|
|
856
849
|
|
|
857
850
|
showError(message: string): void {
|
|
851
|
+
this.optimisticUserMessageSignature = undefined;
|
|
858
852
|
this.#uiHelpers.showError(message);
|
|
859
853
|
}
|
|
860
854
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AutocompleteItem,
|
|
3
|
+
type AutocompleteProvider,
|
|
4
|
+
CombinedAutocompleteProvider,
|
|
5
|
+
getEditorKeybindings,
|
|
6
|
+
type SlashCommand,
|
|
7
|
+
} from "@oh-my-pi/pi-tui";
|
|
8
|
+
import { formatKeyHints, type KeybindingsManager } from "../config/keybindings";
|
|
9
|
+
|
|
10
|
+
interface PromptActionDefinition {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
description: string;
|
|
14
|
+
keywords: string[];
|
|
15
|
+
execute: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PromptActionAutocompleteItem extends AutocompleteItem {
|
|
19
|
+
actionId: string;
|
|
20
|
+
execute: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PromptActionAutocompleteOptions {
|
|
24
|
+
commands: SlashCommand[];
|
|
25
|
+
basePath: string;
|
|
26
|
+
keybindings: KeybindingsManager;
|
|
27
|
+
copyCurrentLine: () => void;
|
|
28
|
+
copyPrompt: () => void;
|
|
29
|
+
moveCursorToLineStart: () => void;
|
|
30
|
+
moveCursorToLineEnd: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fuzzyMatch(query: string, target: string): boolean {
|
|
34
|
+
if (query.length === 0) return true;
|
|
35
|
+
if (query.length > target.length) return false;
|
|
36
|
+
|
|
37
|
+
let queryIndex = 0;
|
|
38
|
+
for (let targetIndex = 0; targetIndex < target.length && queryIndex < query.length; targetIndex += 1) {
|
|
39
|
+
if (query[queryIndex] === target[targetIndex]) {
|
|
40
|
+
queryIndex += 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return queryIndex === query.length;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fuzzyScore(query: string, target: string): number {
|
|
48
|
+
if (query.length === 0) return 1;
|
|
49
|
+
if (target === query) return 100;
|
|
50
|
+
if (target.startsWith(query)) return 80;
|
|
51
|
+
if (target.includes(query)) return 60;
|
|
52
|
+
|
|
53
|
+
let queryIndex = 0;
|
|
54
|
+
let gaps = 0;
|
|
55
|
+
let lastMatchIndex = -1;
|
|
56
|
+
for (let targetIndex = 0; targetIndex < target.length && queryIndex < query.length; targetIndex += 1) {
|
|
57
|
+
if (query[queryIndex] === target[targetIndex]) {
|
|
58
|
+
if (lastMatchIndex >= 0 && targetIndex - lastMatchIndex > 1) {
|
|
59
|
+
gaps += 1;
|
|
60
|
+
}
|
|
61
|
+
lastMatchIndex = targetIndex;
|
|
62
|
+
queryIndex += 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (queryIndex !== query.length) return 0;
|
|
67
|
+
return Math.max(1, 40 - gaps * 5);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isPromptActionItem(item: AutocompleteItem): item is PromptActionAutocompleteItem {
|
|
71
|
+
return (
|
|
72
|
+
"actionId" in item && "execute" in item && typeof (item as PromptActionAutocompleteItem).execute === "function"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getPromptActionPrefix(textBeforeCursor: string): string | null {
|
|
77
|
+
const hashIndex = textBeforeCursor.lastIndexOf("#");
|
|
78
|
+
if (hashIndex === -1) return null;
|
|
79
|
+
|
|
80
|
+
const query = textBeforeCursor.slice(hashIndex + 1);
|
|
81
|
+
if (/[\s]/.test(query)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return textBeforeCursor.slice(hashIndex);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
89
|
+
#baseProvider: CombinedAutocompleteProvider;
|
|
90
|
+
#actions: PromptActionDefinition[];
|
|
91
|
+
|
|
92
|
+
constructor(commands: SlashCommand[], basePath: string, actions: PromptActionDefinition[]) {
|
|
93
|
+
this.#baseProvider = new CombinedAutocompleteProvider(commands, basePath);
|
|
94
|
+
this.#actions = actions;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async getSuggestions(
|
|
98
|
+
lines: string[],
|
|
99
|
+
cursorLine: number,
|
|
100
|
+
cursorCol: number,
|
|
101
|
+
): Promise<{ items: AutocompleteItem[]; prefix: string } | null> {
|
|
102
|
+
const currentLine = lines[cursorLine] || "";
|
|
103
|
+
const textBeforeCursor = currentLine.slice(0, cursorCol);
|
|
104
|
+
const promptActionPrefix = getPromptActionPrefix(textBeforeCursor);
|
|
105
|
+
if (promptActionPrefix) {
|
|
106
|
+
const query = promptActionPrefix.slice(1).toLowerCase();
|
|
107
|
+
const items = this.#actions
|
|
108
|
+
.map(action => {
|
|
109
|
+
const searchable = [action.label, action.description, ...action.keywords].join(" ").toLowerCase();
|
|
110
|
+
if (!fuzzyMatch(query, searchable)) return null;
|
|
111
|
+
return {
|
|
112
|
+
value: action.label,
|
|
113
|
+
label: action.label,
|
|
114
|
+
description: action.description,
|
|
115
|
+
actionId: action.id,
|
|
116
|
+
execute: action.execute,
|
|
117
|
+
score: fuzzyScore(query, searchable),
|
|
118
|
+
} satisfies PromptActionAutocompleteItem & { score: number };
|
|
119
|
+
})
|
|
120
|
+
.filter(item => item !== null)
|
|
121
|
+
.sort((a, b) => b.score - a.score)
|
|
122
|
+
.map(({ score: _score, ...item }) => item);
|
|
123
|
+
if (items.length > 0) {
|
|
124
|
+
return { items, prefix: promptActionPrefix };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return this.#baseProvider.getSuggestions(lines, cursorLine, cursorCol);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
applyCompletion(
|
|
132
|
+
lines: string[],
|
|
133
|
+
cursorLine: number,
|
|
134
|
+
cursorCol: number,
|
|
135
|
+
item: AutocompleteItem,
|
|
136
|
+
prefix: string,
|
|
137
|
+
): {
|
|
138
|
+
lines: string[];
|
|
139
|
+
cursorLine: number;
|
|
140
|
+
cursorCol: number;
|
|
141
|
+
onApplied?: () => void;
|
|
142
|
+
} {
|
|
143
|
+
if (prefix.startsWith("#") && isPromptActionItem(item)) {
|
|
144
|
+
const currentLine = lines[cursorLine] || "";
|
|
145
|
+
const beforePrefix = currentLine.slice(0, cursorCol - prefix.length);
|
|
146
|
+
const afterCursor = currentLine.slice(cursorCol);
|
|
147
|
+
const newLines = [...lines];
|
|
148
|
+
newLines[cursorLine] = beforePrefix + afterCursor;
|
|
149
|
+
return {
|
|
150
|
+
lines: newLines,
|
|
151
|
+
cursorLine,
|
|
152
|
+
cursorCol: beforePrefix.length,
|
|
153
|
+
onApplied: item.execute,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return this.#baseProvider.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getInlineHint(lines: string[], cursorLine: number, cursorCol: number): string | null {
|
|
161
|
+
return this.#baseProvider.getInlineHint?.(lines, cursorLine, cursorCol) ?? null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function createPromptActionAutocompleteProvider(
|
|
166
|
+
options: PromptActionAutocompleteOptions,
|
|
167
|
+
): PromptActionAutocompleteProvider {
|
|
168
|
+
const editorKeybindings = getEditorKeybindings();
|
|
169
|
+
const actions: PromptActionDefinition[] = [
|
|
170
|
+
{
|
|
171
|
+
id: "copy-line",
|
|
172
|
+
label: "Copy current line",
|
|
173
|
+
description: formatKeyHints(options.keybindings.getKeys("copyLine")),
|
|
174
|
+
keywords: ["copy", "line", "clipboard", "current"],
|
|
175
|
+
execute: options.copyCurrentLine,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "copy-prompt",
|
|
179
|
+
label: "Copy whole prompt",
|
|
180
|
+
description: formatKeyHints(options.keybindings.getKeys("copyPrompt")),
|
|
181
|
+
keywords: ["copy", "prompt", "clipboard", "message"],
|
|
182
|
+
execute: options.copyPrompt,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "cursor-line-start",
|
|
186
|
+
label: "Move cursor to beginning of line",
|
|
187
|
+
description: formatKeyHints(editorKeybindings.getKeys("cursorLineStart")),
|
|
188
|
+
keywords: ["move", "cursor", "line", "start", "beginning", "home"],
|
|
189
|
+
execute: options.moveCursorToLineStart,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "cursor-line-end",
|
|
193
|
+
label: "Move cursor to end of line",
|
|
194
|
+
description: formatKeyHints(editorKeybindings.getKeys("cursorLineEnd")),
|
|
195
|
+
keywords: ["move", "cursor", "line", "end"],
|
|
196
|
+
execute: options.moveCursorToLineEnd,
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
return new PromptActionAutocompleteProvider(options.commands, options.basePath, actions);
|
|
201
|
+
}
|
package/src/modes/types.ts
CHANGED
|
@@ -88,6 +88,7 @@ export interface InteractiveModeContext {
|
|
|
88
88
|
retryEscapeHandler?: () => void;
|
|
89
89
|
unsubscribe?: () => void;
|
|
90
90
|
onInputCallback?: (input: { text: string; images?: ImageContent[] }) => void;
|
|
91
|
+
optimisticUserMessageSignature: string | undefined;
|
|
91
92
|
lastSigintTime: number;
|
|
92
93
|
lastEscapeTime: number;
|
|
93
94
|
shutdownRequested: boolean;
|
|
@@ -209,6 +209,7 @@ export class UiHelpers {
|
|
|
209
209
|
sessionContext: SessionContext,
|
|
210
210
|
options: { updateFooter?: boolean; populateHistory?: boolean } = {},
|
|
211
211
|
): void {
|
|
212
|
+
this.ctx.optimisticUserMessageSignature = undefined;
|
|
212
213
|
this.ctx.pendingTools.clear();
|
|
213
214
|
|
|
214
215
|
if (options.updateFooter) {
|
|
@@ -219,7 +220,13 @@ export class UiHelpers {
|
|
|
219
220
|
let readGroup: ReadToolGroupComponent | null = null;
|
|
220
221
|
const readToolCallArgs = new Map<string, Record<string, unknown>>();
|
|
221
222
|
const readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
223
|
+
const deferredMessages: AgentMessage[] = [];
|
|
222
224
|
for (const message of sessionContext.messages) {
|
|
225
|
+
// Defer compaction summaries so they render at the bottom (visible after scroll)
|
|
226
|
+
if (message.role === "compactionSummary") {
|
|
227
|
+
deferredMessages.push(message);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
223
230
|
// Assistant messages need special handling for tool calls
|
|
224
231
|
if (message.role === "assistant") {
|
|
225
232
|
this.ctx.addMessageToChat(message);
|
|
@@ -349,6 +356,11 @@ export class UiHelpers {
|
|
|
349
356
|
}
|
|
350
357
|
}
|
|
351
358
|
|
|
359
|
+
// Render deferred messages (compaction summaries) at the bottom so they're visible
|
|
360
|
+
for (const message of deferredMessages) {
|
|
361
|
+
this.ctx.addMessageToChat(message, options);
|
|
362
|
+
}
|
|
363
|
+
|
|
352
364
|
this.ctx.pendingTools.clear();
|
|
353
365
|
this.ctx.ui.requestRender();
|
|
354
366
|
}
|
package/src/patch/index.ts
CHANGED
|
@@ -96,7 +96,7 @@ export type ReplaceParams = Static<typeof replaceEditSchema>;
|
|
|
96
96
|
export type PatchParams = Static<typeof patchEditSchema>;
|
|
97
97
|
|
|
98
98
|
/** Pattern matching hashline display format prefixes: `LINE#ID:CONTENT` and `#ID:CONTENT` */
|
|
99
|
-
const HASHLINE_PREFIX_RE = /^\s*(?:>>>|>>)?\s*(?:\d+\s*#\s
|
|
99
|
+
const HASHLINE_PREFIX_RE = /^\s*(?:>>>|>>)?\s*(?:\d+\s*#\s*|#\s*)[ZPMQVRWSNKTXJBYH]{2}:/;
|
|
100
100
|
|
|
101
101
|
/** Pattern matching a unified-diff added-line `+` prefix (but not `++`). Does NOT match `-` to avoid corrupting Markdown list items. */
|
|
102
102
|
const DIFF_PLUS_RE = /^[+](?![+])/;
|