@oh-my-pi/pi-coding-agent 13.7.1 → 13.7.3
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 +32 -0
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +3 -12
- package/src/debug/log-formatting.ts +2 -4
- package/src/debug/log-viewer.ts +3 -3
- package/src/exa/factory.ts +1 -1
- package/src/exa/mcp-client.ts +1 -1
- package/src/exa/websets.ts +1 -1
- package/src/ipy/gateway-coordinator.ts +1 -1
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/tool-execution.ts +5 -5
- package/src/modes/controllers/command-controller.ts +4 -4
- package/src/modes/controllers/input-controller.ts +8 -9
- package/src/modes/controllers/selector-controller.ts +98 -94
- package/src/modes/interactive-mode.ts +4 -4
- package/src/modes/types.ts +3 -3
- package/src/patch/hashline.ts +15 -12
- package/src/patch/index.ts +5 -4
- package/src/prompts/tools/hashline.md +148 -66
- package/src/slash-commands/builtin-registry.ts +17 -1
- package/src/task/executor.ts +1 -1
- package/src/task/render.ts +3 -1
- package/src/tools/fetch.ts +17 -6
- package/src/tools/submit-result.ts +4 -51
- package/src/tui/output-block.ts +7 -8
- package/src/utils/tools-manager.ts +1 -1
- package/src/web/kagi.ts +161 -0
- package/src/web/scrapers/youtube.ts +22 -4
- package/src/web/search/index.ts +16 -39
- package/src/web/search/providers/kagi.ts +26 -104
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.7.3] - 2026-03-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added Kagi Universal Summarizer integration for URL summarization, now prioritized before Jina and other methods
|
|
10
|
+
- Added Kagi Universal Summarizer support for YouTube video summaries when credentials are available
|
|
11
|
+
- Exported `searchWithKagi` and `summarizeUrlWithKagi` functions from new `web/kagi` module for direct API access
|
|
12
|
+
- Added `KagiApiError` exception class for Kagi API-specific error handling
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Updated hashline prompt documentation with clearer operation syntax and improved examples showing full edit structure with path and edits array
|
|
17
|
+
- Refactored `hlineref` Handlebars helper to return JSON-quoted strings for safer embedding in JSON blocks within prompts
|
|
18
|
+
- Improved `hashlineParseText` to correctly preserve blank lines and trailing empty strings in array input while stripping trailing newlines from string input
|
|
19
|
+
- Optimized duplicate line detection in range replacements to use trimmed comparison, reducing false positives from whitespace differences
|
|
20
|
+
- Refactored Kagi search provider to use shared Kagi API utilities from `web/kagi` module
|
|
21
|
+
- Changed HTML-to-text rendering priority order to try Kagi first, then Jina, Trafilatura, and Lynx
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Fixed `isEscapedTabAutocorrectEnabled` environment variable parsing to use switch statement for clearer logic and consistent default behavior
|
|
26
|
+
|
|
27
|
+
## [13.7.2] - 2026-03-04
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Added support for direct OAuth provider login via `/login <provider>` command (e.g., `/login kagi`)
|
|
31
|
+
- Added optional `providerId` parameter to `showOAuthSelector()` to enable direct provider selection without UI selector
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Simplified web search result formatting to omit empty sections and metadata when not present
|
|
36
|
+
|
|
5
37
|
## [13.7.0] - 2026-03-03
|
|
6
38
|
|
|
7
39
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.7.
|
|
4
|
+
"version": "13.7.3",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.7.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.7.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.7.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.7.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.7.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.7.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.7.3",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.7.3",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.7.3",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.7.3",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.7.3",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.7.3",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -249,10 +249,6 @@ export function sectionSeparator(name: string): string {
|
|
|
249
249
|
|
|
250
250
|
handlebars.registerHelper("SECTION_SEPERATOR", (name: unknown): string => sectionSeparator(String(name)));
|
|
251
251
|
|
|
252
|
-
/**
|
|
253
|
-
* {{hlineref lineNum "content"}} — compute a real hashline ref for prompt examples.
|
|
254
|
-
* Returns `"lineNum#hash"` using the actual hash algorithm.
|
|
255
|
-
*/
|
|
256
252
|
function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; text: string; ref: string } {
|
|
257
253
|
const num = typeof lineNum === "number" ? lineNum : Number.parseInt(String(lineNum), 10);
|
|
258
254
|
const raw = typeof content === "string" ? content : String(content ?? "");
|
|
@@ -261,16 +257,11 @@ function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; t
|
|
|
261
257
|
return { num, text, ref };
|
|
262
258
|
}
|
|
263
259
|
|
|
264
|
-
handlebars.registerHelper("hlineref", (lineNum: unknown, content: unknown): string => {
|
|
265
|
-
const { ref } = formatHashlineRef(lineNum, content);
|
|
266
|
-
return ref;
|
|
267
|
-
});
|
|
268
|
-
|
|
269
260
|
/**
|
|
270
|
-
* {{
|
|
271
|
-
*
|
|
261
|
+
* {{hlineref lineNum "content"}} — compute a real hashline ref for prompt examples.
|
|
262
|
+
* Returns `"lineNum#hash"` using the actual hash algorithm.
|
|
272
263
|
*/
|
|
273
|
-
handlebars.registerHelper("
|
|
264
|
+
handlebars.registerHelper("hlineref", (lineNum: unknown, content: unknown): string => {
|
|
274
265
|
const { ref } = formatHashlineRef(lineNum, content);
|
|
275
266
|
return JSON.stringify(ref);
|
|
276
267
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
1
|
+
import { sanitizeText, wrapTextWithAnsi } from "@oh-my-pi/pi-natives";
|
|
2
2
|
import { replaceTabs, truncateToWidth } from "../tools/render-utils";
|
|
3
3
|
|
|
4
4
|
export function formatDebugLogLine(line: string, maxWidth: number): string {
|
|
@@ -17,9 +17,7 @@ export function formatDebugLogExpandedLines(line: string, maxWidth: number): str
|
|
|
17
17
|
return [""];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
return normalized
|
|
21
|
-
.split("\n")
|
|
22
|
-
.flatMap(segment => Bun.wrapAnsi(segment, width, { hard: true, trim: false, wordWrap: true }).split("\n"));
|
|
20
|
+
return normalized.split("\n").flatMap(segment => wrapTextWithAnsi(segment, width));
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
export function parseDebugLogTimestampMs(line: string): number | undefined {
|
package/src/debug/log-viewer.ts
CHANGED
|
@@ -499,7 +499,7 @@ export class DebugLogViewerComponent implements Component {
|
|
|
499
499
|
}
|
|
500
500
|
|
|
501
501
|
if (matchesKey(keyData, "ctrl+c")) {
|
|
502
|
-
|
|
502
|
+
this.#copySelected();
|
|
503
503
|
return;
|
|
504
504
|
}
|
|
505
505
|
|
|
@@ -878,7 +878,7 @@ export class DebugLogViewerComponent implements Component {
|
|
|
878
878
|
return `${theme.boxSharp.vertical}${truncated}${padding(remaining)}${theme.boxSharp.vertical}`;
|
|
879
879
|
}
|
|
880
880
|
|
|
881
|
-
|
|
881
|
+
#copySelected() {
|
|
882
882
|
const selectedPayload = buildLogCopyPayload(this.#model.getSelectedRawLines());
|
|
883
883
|
const selected = selectedPayload.length === 0 ? [] : selectedPayload.split("\n");
|
|
884
884
|
|
|
@@ -890,7 +890,7 @@ export class DebugLogViewerComponent implements Component {
|
|
|
890
890
|
}
|
|
891
891
|
|
|
892
892
|
try {
|
|
893
|
-
|
|
893
|
+
copyToClipboard(selectedPayload);
|
|
894
894
|
const message = `Copied ${selected.length} log ${selected.length === 1 ? "entry" : "entries"}`;
|
|
895
895
|
this.#statusMessage = message;
|
|
896
896
|
this.#onStatus?.(message);
|
package/src/exa/factory.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function createExaTool(
|
|
|
30
30
|
parameters,
|
|
31
31
|
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
|
|
32
32
|
try {
|
|
33
|
-
const apiKey =
|
|
33
|
+
const apiKey = findApiKey();
|
|
34
34
|
// Exa MCP endpoint is publicly accessible; API key is optional
|
|
35
35
|
const args = transformParams ? transformParams(params as Record<string, unknown>) : params;
|
|
36
36
|
const response = await callExaTool(mcpToolName, args, apiKey);
|
package/src/exa/mcp-client.ts
CHANGED
|
@@ -238,7 +238,7 @@ export class MCPWrappedTool implements CustomTool<TSchema, ExaRenderDetails> {
|
|
|
238
238
|
_signal?: AbortSignal,
|
|
239
239
|
): Promise<CustomToolResult<ExaRenderDetails>> {
|
|
240
240
|
try {
|
|
241
|
-
const apiKey =
|
|
241
|
+
const apiKey = findApiKey();
|
|
242
242
|
// Websets tools require an API key; basic Exa MCP tools work without one
|
|
243
243
|
if (!apiKey && this.config.isWebsetsTool) {
|
|
244
244
|
return {
|
package/src/exa/websets.ts
CHANGED
|
@@ -23,7 +23,7 @@ function createWebsetTool(
|
|
|
23
23
|
parameters,
|
|
24
24
|
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
|
|
25
25
|
try {
|
|
26
|
-
const apiKey =
|
|
26
|
+
const apiKey = findApiKey();
|
|
27
27
|
if (!apiKey) {
|
|
28
28
|
return {
|
|
29
29
|
content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
|
|
@@ -230,7 +230,7 @@ async function startGatewayProcess(
|
|
|
230
230
|
const settings = await Settings.init();
|
|
231
231
|
const { shell, env } = settings.getShellConfig();
|
|
232
232
|
const filteredEnv = filterEnv(env);
|
|
233
|
-
const runtime =
|
|
233
|
+
const runtime = resolvePythonRuntime(cwd, filteredEnv);
|
|
234
234
|
const snapshotPath = await getOrCreateSnapshot(shell, env).catch((err: unknown) => {
|
|
235
235
|
logger.warn("Failed to resolve shell snapshot for shared Python gateway", {
|
|
236
236
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -89,7 +89,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
89
89
|
try {
|
|
90
90
|
for (const content of message.content) {
|
|
91
91
|
if (content.type === "text" && content.text.trim() && hasPendingMermaid(content.text)) {
|
|
92
|
-
|
|
92
|
+
prerenderMermaid(content.text);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
} catch (error) {
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
stripInternalArgs,
|
|
31
31
|
} from "../../tools/json-tree";
|
|
32
32
|
import { PYTHON_DEFAULT_PREVIEW_LINES } from "../../tools/python";
|
|
33
|
-
import { formatExpandHint, truncateToWidth } from "../../tools/render-utils";
|
|
33
|
+
import { formatExpandHint, replaceTabs, truncateToWidth } from "../../tools/render-utils";
|
|
34
34
|
import { toolRenderers } from "../../tools/renderers";
|
|
35
35
|
import { renderStatusLine } from "../../tui";
|
|
36
36
|
import { convertToPng } from "../../utils/image-convert";
|
|
@@ -431,14 +431,14 @@ export class ToolExecutionComponent extends Container {
|
|
|
431
431
|
// Fall back to showing raw output on error
|
|
432
432
|
const output = this.#getTextOutput();
|
|
433
433
|
if (output) {
|
|
434
|
-
this.#contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
|
|
434
|
+
this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
|
|
435
435
|
}
|
|
436
436
|
}
|
|
437
437
|
} else if (this.#result) {
|
|
438
438
|
// Has result but no custom renderResult
|
|
439
439
|
const output = this.#getTextOutput();
|
|
440
440
|
if (output) {
|
|
441
|
-
this.#contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
|
|
441
|
+
this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
444
|
} else if (this.#toolName in toolRenderers) {
|
|
@@ -488,7 +488,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
488
488
|
// Fall back to showing raw output on error
|
|
489
489
|
const output = this.#getTextOutput();
|
|
490
490
|
if (output) {
|
|
491
|
-
this.#contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
|
|
491
|
+
this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
|
|
492
492
|
}
|
|
493
493
|
}
|
|
494
494
|
}
|
|
@@ -679,7 +679,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
679
679
|
const displayLines = outputLines.slice(0, maxOutputLines);
|
|
680
680
|
|
|
681
681
|
for (const line of displayLines) {
|
|
682
|
-
lines.push(theme.fg("toolOutput", truncateToWidth(line, 80)));
|
|
682
|
+
lines.push(theme.fg("toolOutput", truncateToWidth(replaceTabs(line), 80)));
|
|
683
683
|
}
|
|
684
684
|
|
|
685
685
|
if (outputLines.length > maxOutputLines) {
|
|
@@ -57,14 +57,14 @@ export class CommandController {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
handleDumpCommand() {
|
|
61
61
|
try {
|
|
62
62
|
const formatted = this.ctx.session.formatSessionAsText();
|
|
63
63
|
if (!formatted) {
|
|
64
64
|
this.ctx.showError("No messages to dump yet.");
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
copyToClipboard(formatted);
|
|
68
68
|
this.ctx.showStatus("Session copied to clipboard");
|
|
69
69
|
} catch (error: unknown) {
|
|
70
70
|
this.ctx.showError(`Failed to copy session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -214,7 +214,7 @@ export class CommandController {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
handleCopyCommand() {
|
|
218
218
|
const text = this.ctx.session.getLastAssistantText();
|
|
219
219
|
if (!text) {
|
|
220
220
|
this.ctx.showError("No agent messages to copy yet.");
|
|
@@ -222,7 +222,7 @@ export class CommandController {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
try {
|
|
225
|
-
|
|
225
|
+
copyToClipboard(text);
|
|
226
226
|
this.ctx.showStatus("Copied last agent message to clipboard");
|
|
227
227
|
} catch (error) {
|
|
228
228
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
@@ -510,15 +510,14 @@ export class InputController {
|
|
|
510
510
|
this.ctx.showStatus("Nothing to copy");
|
|
511
511
|
return;
|
|
512
512
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
.
|
|
520
|
-
|
|
521
|
-
});
|
|
513
|
+
try {
|
|
514
|
+
copyToClipboard(text);
|
|
515
|
+
const sanitized = sanitizeText(text);
|
|
516
|
+
const preview = sanitized.length > 30 ? `${sanitized.slice(0, 30)}...` : sanitized;
|
|
517
|
+
this.ctx.showStatus(`Copied: ${preview}`);
|
|
518
|
+
} catch {
|
|
519
|
+
this.ctx.showWarning("Failed to copy to clipboard");
|
|
520
|
+
}
|
|
522
521
|
}
|
|
523
522
|
|
|
524
523
|
cycleThinkingLevel(): void {
|
|
@@ -617,7 +617,99 @@ export class SelectorController {
|
|
|
617
617
|
this.ctx.showStatus("Resumed session");
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
-
async
|
|
620
|
+
async #handleOAuthLogin(providerId: string): Promise<void> {
|
|
621
|
+
this.ctx.showStatus(`Logging in to ${providerId}…`);
|
|
622
|
+
const manualInput = this.ctx.oauthManualInput;
|
|
623
|
+
const useManualInput = CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider);
|
|
624
|
+
try {
|
|
625
|
+
await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
|
|
626
|
+
onAuth: (info: { url: string; instructions?: string }) => {
|
|
627
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
628
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
|
|
629
|
+
const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
|
|
630
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
|
|
631
|
+
if (info.instructions) {
|
|
632
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
633
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
|
|
634
|
+
}
|
|
635
|
+
if (useManualInput) {
|
|
636
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
637
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", MANUAL_LOGIN_TIP), 1, 0));
|
|
638
|
+
}
|
|
639
|
+
this.ctx.ui.requestRender();
|
|
640
|
+
this.ctx.openInBrowser(info.url);
|
|
641
|
+
},
|
|
642
|
+
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
|
|
643
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
644
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("warning", prompt.message), 1, 0));
|
|
645
|
+
if (prompt.placeholder) {
|
|
646
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0));
|
|
647
|
+
}
|
|
648
|
+
this.ctx.ui.requestRender();
|
|
649
|
+
const { promise, resolve } = Promise.withResolvers<string>();
|
|
650
|
+
const codeInput = new Input();
|
|
651
|
+
codeInput.onSubmit = () => {
|
|
652
|
+
const code = codeInput.getValue();
|
|
653
|
+
this.ctx.editorContainer.clear();
|
|
654
|
+
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
655
|
+
this.ctx.ui.setFocus(this.ctx.editor);
|
|
656
|
+
resolve(code);
|
|
657
|
+
};
|
|
658
|
+
this.ctx.editorContainer.clear();
|
|
659
|
+
this.ctx.editorContainer.addChild(codeInput);
|
|
660
|
+
this.ctx.ui.setFocus(codeInput);
|
|
661
|
+
this.ctx.ui.requestRender();
|
|
662
|
+
return promise;
|
|
663
|
+
},
|
|
664
|
+
onProgress: (message: string) => {
|
|
665
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
|
|
666
|
+
this.ctx.ui.requestRender();
|
|
667
|
+
},
|
|
668
|
+
onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
|
|
669
|
+
});
|
|
670
|
+
await this.ctx.session.modelRegistry.refresh();
|
|
671
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
672
|
+
this.ctx.chatContainer.addChild(
|
|
673
|
+
new Text(theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`), 1, 0),
|
|
674
|
+
);
|
|
675
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0));
|
|
676
|
+
this.ctx.ui.requestRender();
|
|
677
|
+
} catch (error: unknown) {
|
|
678
|
+
this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
679
|
+
} finally {
|
|
680
|
+
if (useManualInput) {
|
|
681
|
+
manualInput.clear(`Manual OAuth input cleared for ${providerId}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async #handleOAuthLogout(providerId: string): Promise<void> {
|
|
687
|
+
try {
|
|
688
|
+
await this.ctx.session.modelRegistry.authStorage.logout(providerId);
|
|
689
|
+
await this.ctx.session.modelRegistry.refresh();
|
|
690
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
691
|
+
this.ctx.chatContainer.addChild(
|
|
692
|
+
new Text(theme.fg("success", `${theme.status.success} Successfully logged out of ${providerId}`), 1, 0),
|
|
693
|
+
);
|
|
694
|
+
this.ctx.chatContainer.addChild(
|
|
695
|
+
new Text(theme.fg("dim", `Credentials removed from ${getAgentDbPath()}`), 1, 0),
|
|
696
|
+
);
|
|
697
|
+
this.ctx.ui.requestRender();
|
|
698
|
+
} catch (error: unknown) {
|
|
699
|
+
this.ctx.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
|
|
704
|
+
if (providerId) {
|
|
705
|
+
if (mode === "login") {
|
|
706
|
+
await this.#handleOAuthLogin(providerId);
|
|
707
|
+
} else {
|
|
708
|
+
await this.#handleOAuthLogout(providerId);
|
|
709
|
+
}
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
621
713
|
if (mode === "logout") {
|
|
622
714
|
await this.#refreshOAuthProviderAuthState();
|
|
623
715
|
const oauthProviders = getOAuthProviders();
|
|
@@ -635,101 +727,13 @@ export class SelectorController {
|
|
|
635
727
|
selector = new OAuthSelectorComponent(
|
|
636
728
|
mode,
|
|
637
729
|
this.ctx.session.modelRegistry.authStorage,
|
|
638
|
-
async (
|
|
730
|
+
async (selectedProviderId: string) => {
|
|
639
731
|
selector.stopValidation();
|
|
640
732
|
done();
|
|
641
733
|
if (mode === "login") {
|
|
642
|
-
this
|
|
643
|
-
const manualInput = this.ctx.oauthManualInput;
|
|
644
|
-
const useManualInput = CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider);
|
|
645
|
-
try {
|
|
646
|
-
await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
|
|
647
|
-
onAuth: (info: { url: string; instructions?: string }) => {
|
|
648
|
-
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
649
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
|
|
650
|
-
// Use OSC 8 hyperlink escape sequence for clickable link
|
|
651
|
-
const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
|
|
652
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
|
|
653
|
-
if (info.instructions) {
|
|
654
|
-
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
655
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
|
|
656
|
-
}
|
|
657
|
-
if (useManualInput) {
|
|
658
|
-
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
659
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", MANUAL_LOGIN_TIP), 1, 0));
|
|
660
|
-
}
|
|
661
|
-
this.ctx.ui.requestRender();
|
|
662
|
-
this.ctx.openInBrowser(info.url);
|
|
663
|
-
},
|
|
664
|
-
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
|
|
665
|
-
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
666
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("warning", prompt.message), 1, 0));
|
|
667
|
-
if (prompt.placeholder) {
|
|
668
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0));
|
|
669
|
-
}
|
|
670
|
-
this.ctx.ui.requestRender();
|
|
671
|
-
return new Promise<string>(resolve => {
|
|
672
|
-
const codeInput = new Input();
|
|
673
|
-
codeInput.onSubmit = () => {
|
|
674
|
-
const code = codeInput.getValue();
|
|
675
|
-
this.ctx.editorContainer.clear();
|
|
676
|
-
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
677
|
-
this.ctx.ui.setFocus(this.ctx.editor);
|
|
678
|
-
resolve(code);
|
|
679
|
-
};
|
|
680
|
-
this.ctx.editorContainer.clear();
|
|
681
|
-
this.ctx.editorContainer.addChild(codeInput);
|
|
682
|
-
this.ctx.ui.setFocus(codeInput);
|
|
683
|
-
this.ctx.ui.requestRender();
|
|
684
|
-
});
|
|
685
|
-
},
|
|
686
|
-
onProgress: (message: string) => {
|
|
687
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
|
|
688
|
-
this.ctx.ui.requestRender();
|
|
689
|
-
},
|
|
690
|
-
onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
|
|
691
|
-
});
|
|
692
|
-
// Refresh models to pick up new baseUrl (e.g., github-copilot)
|
|
693
|
-
await this.ctx.session.modelRegistry.refresh();
|
|
694
|
-
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
695
|
-
this.ctx.chatContainer.addChild(
|
|
696
|
-
new Text(
|
|
697
|
-
theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`),
|
|
698
|
-
1,
|
|
699
|
-
0,
|
|
700
|
-
),
|
|
701
|
-
);
|
|
702
|
-
this.ctx.chatContainer.addChild(
|
|
703
|
-
new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0),
|
|
704
|
-
);
|
|
705
|
-
this.ctx.ui.requestRender();
|
|
706
|
-
} catch (error: unknown) {
|
|
707
|
-
this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
708
|
-
} finally {
|
|
709
|
-
if (useManualInput) {
|
|
710
|
-
manualInput.clear(`Manual OAuth input cleared for ${providerId}`);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
734
|
+
await this.#handleOAuthLogin(selectedProviderId);
|
|
713
735
|
} else {
|
|
714
|
-
|
|
715
|
-
await this.ctx.session.modelRegistry.authStorage.logout(providerId);
|
|
716
|
-
// Refresh models to reset baseUrl
|
|
717
|
-
await this.ctx.session.modelRegistry.refresh();
|
|
718
|
-
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
719
|
-
this.ctx.chatContainer.addChild(
|
|
720
|
-
new Text(
|
|
721
|
-
theme.fg("success", `${theme.status.success} Successfully logged out of ${providerId}`),
|
|
722
|
-
1,
|
|
723
|
-
0,
|
|
724
|
-
),
|
|
725
|
-
);
|
|
726
|
-
this.ctx.chatContainer.addChild(
|
|
727
|
-
new Text(theme.fg("dim", `Credentials removed from ${getAgentDbPath()}`), 1, 0),
|
|
728
|
-
);
|
|
729
|
-
this.ctx.ui.requestRender();
|
|
730
|
-
} catch (error: unknown) {
|
|
731
|
-
this.ctx.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
732
|
-
}
|
|
736
|
+
await this.#handleOAuthLogout(selectedProviderId);
|
|
733
737
|
}
|
|
734
738
|
},
|
|
735
739
|
() => {
|
|
@@ -738,9 +742,9 @@ export class SelectorController {
|
|
|
738
742
|
this.ctx.ui.requestRender();
|
|
739
743
|
},
|
|
740
744
|
{
|
|
741
|
-
validateAuth: async (
|
|
745
|
+
validateAuth: async (selectedProviderId: string) => {
|
|
742
746
|
const apiKey = await this.ctx.session.modelRegistry.getApiKeyForProvider(
|
|
743
|
-
|
|
747
|
+
selectedProviderId,
|
|
744
748
|
this.ctx.session.sessionId,
|
|
745
749
|
);
|
|
746
750
|
return !!apiKey;
|
|
@@ -949,7 +949,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
949
949
|
return this.#commandController.handleExportCommand(text);
|
|
950
950
|
}
|
|
951
951
|
|
|
952
|
-
handleDumpCommand()
|
|
952
|
+
handleDumpCommand() {
|
|
953
953
|
return this.#commandController.handleDumpCommand();
|
|
954
954
|
}
|
|
955
955
|
|
|
@@ -961,7 +961,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
961
961
|
return this.#commandController.handleShareCommand();
|
|
962
962
|
}
|
|
963
963
|
|
|
964
|
-
handleCopyCommand()
|
|
964
|
+
handleCopyCommand() {
|
|
965
965
|
return this.#commandController.handleCopyCommand();
|
|
966
966
|
}
|
|
967
967
|
|
|
@@ -1149,8 +1149,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1149
1149
|
return this.#selectorController.handleResumeSession(sessionPath);
|
|
1150
1150
|
}
|
|
1151
1151
|
|
|
1152
|
-
showOAuthSelector(mode: "login" | "logout"): Promise<void> {
|
|
1153
|
-
return this.#selectorController.showOAuthSelector(mode);
|
|
1152
|
+
showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
|
|
1153
|
+
return this.#selectorController.showOAuthSelector(mode, providerId);
|
|
1154
1154
|
}
|
|
1155
1155
|
|
|
1156
1156
|
showHookConfirm(title: string, message: string): Promise<boolean> {
|
package/src/modes/types.ts
CHANGED
|
@@ -147,13 +147,13 @@ export interface InteractiveModeContext {
|
|
|
147
147
|
// Command handling
|
|
148
148
|
handleExportCommand(text: string): Promise<void>;
|
|
149
149
|
handleShareCommand(): Promise<void>;
|
|
150
|
-
handleCopyCommand():
|
|
150
|
+
handleCopyCommand(): void;
|
|
151
151
|
handleSessionCommand(): Promise<void>;
|
|
152
152
|
handleJobsCommand(): Promise<void>;
|
|
153
153
|
handleUsageCommand(reports?: UsageReport[] | null): Promise<void>;
|
|
154
154
|
handleChangelogCommand(showFull?: boolean): Promise<void>;
|
|
155
155
|
handleHotkeysCommand(): void;
|
|
156
|
-
handleDumpCommand():
|
|
156
|
+
handleDumpCommand(): void;
|
|
157
157
|
handleDebugTranscriptCommand(): Promise<void>;
|
|
158
158
|
handleClearCommand(): Promise<void>;
|
|
159
159
|
handleForkCommand(): Promise<void>;
|
|
@@ -180,7 +180,7 @@ export interface InteractiveModeContext {
|
|
|
180
180
|
showTreeSelector(): void;
|
|
181
181
|
showSessionSelector(): void;
|
|
182
182
|
handleResumeSession(sessionPath: string): Promise<void>;
|
|
183
|
-
showOAuthSelector(mode: "login" | "logout"): Promise<void>;
|
|
183
|
+
showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void>;
|
|
184
184
|
showHookConfirm(title: string, message: string): Promise<boolean>;
|
|
185
185
|
showDebugSelector(): void;
|
|
186
186
|
|
package/src/patch/hashline.ts
CHANGED
|
@@ -412,10 +412,14 @@ export function validateLineRef(ref: { line: number; hash: string }, fileLines:
|
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
function isEscapedTabAutocorrectEnabled(): boolean {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
415
|
+
switch (Bun.env.PI_HASHLINE_AUTOCORRECT_ESCAPED_TABS) {
|
|
416
|
+
case "0":
|
|
417
|
+
return false;
|
|
418
|
+
case "1":
|
|
419
|
+
return true;
|
|
420
|
+
default:
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
419
423
|
}
|
|
420
424
|
|
|
421
425
|
function maybeAutocorrectEscapedTabIndentation(edits: HashlineEdit[], warnings: string[]): void {
|
|
@@ -623,20 +627,19 @@ export function applyHashlineEdits(
|
|
|
623
627
|
} else {
|
|
624
628
|
const count = edit.end.line - edit.pos.line + 1;
|
|
625
629
|
const newLines = [...edit.lines];
|
|
626
|
-
const trailingReplacementLine = newLines[newLines.length - 1];
|
|
627
|
-
const nextSurvivingLine = fileLines[edit.end.line];
|
|
630
|
+
const trailingReplacementLine = newLines[newLines.length - 1]?.trimEnd();
|
|
631
|
+
const nextSurvivingLine = fileLines[edit.end.line]?.trimEnd();
|
|
628
632
|
if (
|
|
629
|
-
trailingReplacementLine
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
trailingReplacementLine.trim() === nextSurvivingLine.trim() &&
|
|
633
|
+
trailingReplacementLine &&
|
|
634
|
+
nextSurvivingLine &&
|
|
635
|
+
trailingReplacementLine === nextSurvivingLine &&
|
|
633
636
|
// Safety: only correct when end-line content differs from the duplicate.
|
|
634
637
|
// If end already points to the boundary, matching next line is coincidence.
|
|
635
|
-
fileLines[edit.end.line - 1]
|
|
638
|
+
fileLines[edit.end.line - 1]?.trimEnd() !== trailingReplacementLine
|
|
636
639
|
) {
|
|
637
640
|
newLines.pop();
|
|
638
641
|
warnings.push(
|
|
639
|
-
`Auto-corrected range replace ${edit.pos.line}#${edit.pos.hash}-${edit.end.line}#${edit.end.hash}: removed trailing replacement line "${trailingReplacementLine
|
|
642
|
+
`Auto-corrected range replace ${edit.pos.line}#${edit.pos.hash}-${edit.end.line}#${edit.end.hash}: removed trailing replacement line "${trailingReplacementLine}" that duplicated next surviving line`,
|
|
640
643
|
);
|
|
641
644
|
}
|
|
642
645
|
fileLines.splice(edit.pos.line - 1, count, ...newLines);
|
package/src/patch/index.ts
CHANGED
|
@@ -129,10 +129,11 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
|
129
129
|
|
|
130
130
|
export function hashlineParseText(edit: string[] | string | null): string[] {
|
|
131
131
|
if (edit === null) return [];
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
if (typeof edit === "string") {
|
|
133
|
+
const normalizedEdit = edit.endsWith("\n") ? edit.slice(0, -1) : edit;
|
|
134
|
+
edit = normalizedEdit.replaceAll("\r", "").split("\n");
|
|
135
|
+
}
|
|
136
|
+
return stripNewLinePrefixes(edit);
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
const hashlineEditSchema = Type.Object(
|