@mariozechner/pi-coding-agent 0.36.0 → 0.37.1

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +27 -2
  3. package/dist/config.d.ts +2 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +4 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +3 -1
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +25 -8
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/auth-storage.d.ts +9 -1
  12. package/dist/core/auth-storage.d.ts.map +1 -1
  13. package/dist/core/auth-storage.js +115 -18
  14. package/dist/core/auth-storage.js.map +1 -1
  15. package/dist/core/export-html/template.css +55 -0
  16. package/dist/core/export-html/template.js +124 -7
  17. package/dist/core/model-registry.d.ts.map +1 -1
  18. package/dist/core/model-registry.js +1 -1
  19. package/dist/core/model-registry.js.map +1 -1
  20. package/dist/core/sdk.d.ts +1 -1
  21. package/dist/core/sdk.d.ts.map +1 -1
  22. package/dist/core/sdk.js +9 -0
  23. package/dist/core/sdk.js.map +1 -1
  24. package/dist/main.d.ts.map +1 -1
  25. package/dist/main.js +4 -3
  26. package/dist/main.js.map +1 -1
  27. package/dist/migrations.d.ts.map +1 -1
  28. package/dist/migrations.js +50 -3
  29. package/dist/migrations.js.map +1 -1
  30. package/dist/modes/interactive/components/login-dialog.d.ts +39 -0
  31. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -0
  32. package/dist/modes/interactive/components/login-dialog.js +135 -0
  33. package/dist/modes/interactive/components/login-dialog.js.map +1 -0
  34. package/dist/modes/interactive/interactive-mode.d.ts +5 -0
  35. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  36. package/dist/modes/interactive/interactive-mode.js +211 -84
  37. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  38. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  39. package/dist/modes/interactive/theme/theme.js +4 -2
  40. package/dist/modes/interactive/theme/theme.js.map +1 -1
  41. package/dist/utils/tools-manager.d.ts.map +1 -1
  42. package/dist/utils/tools-manager.js +2 -2
  43. package/dist/utils/tools-manager.js.map +1 -1
  44. package/docs/sdk.md +12 -4
  45. package/examples/extensions/claude-rules.ts +83 -0
  46. package/examples/extensions/with-deps/package-lock.json +2 -2
  47. package/examples/extensions/with-deps/package.json +7 -1
  48. package/examples/sdk/06-extensions.ts +5 -7
  49. package/examples/sdk/12-full-control.ts +2 -6
  50. package/package.json +12 -8
@@ -7,8 +7,9 @@ import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import Clipboard from "@crosscopy/clipboard";
10
- import { CombinedAutocompleteProvider, Container, getEditorKeybindings, Input, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
11
- import { exec, spawn, spawnSync } from "child_process";
10
+ import { getOAuthProviders } from "@mariozechner/pi-ai";
11
+ import { CombinedAutocompleteProvider, Container, getEditorKeybindings, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
12
+ import { spawn, spawnSync } from "child_process";
12
13
  import { APP_NAME, getAuthPath, getDebugLogPath } from "../../config.js";
13
14
  import { KeybindingsManager } from "../../core/keybindings.js";
14
15
  import { createCompactionSummaryMessage } from "../../core/messages.js";
@@ -30,6 +31,7 @@ import { ExtensionEditorComponent } from "./components/extension-editor.js";
30
31
  import { ExtensionInputComponent } from "./components/extension-input.js";
31
32
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
32
33
  import { FooterComponent } from "./components/footer.js";
34
+ import { LoginDialogComponent } from "./components/login-dialog.js";
33
35
  import { ModelSelectorComponent } from "./components/model-selector.js";
34
36
  import { OAuthSelectorComponent } from "./components/oauth-selector.js";
35
37
  import { SessionSelectorComponent } from "./components/session-selector.js";
@@ -86,6 +88,8 @@ export class InteractiveMode {
86
88
  // Auto-retry state
87
89
  retryLoader = undefined;
88
90
  retryEscapeHandler;
91
+ // Messages queued while compaction is running
92
+ compactionQueuedMessages = [];
89
93
  // Extension UI state
90
94
  extensionSelector = undefined;
91
95
  extensionInput = undefined;
@@ -361,6 +365,7 @@ export class InteractiveMode {
361
365
  // Clear UI state
362
366
  this.chatContainer.clear();
363
367
  this.pendingMessagesContainer.clear();
368
+ this.compactionQueuedMessages = [];
364
369
  this.streamingComponent = undefined;
365
370
  this.streamingMessage = undefined;
366
371
  this.pendingTools.clear();
@@ -852,13 +857,7 @@ export class InteractiveMode {
852
857
  if (text === "/compact" || text.startsWith("/compact ")) {
853
858
  const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
854
859
  this.editor.setText("");
855
- this.editor.disableSubmit = true;
856
- try {
857
- await this.handleCompactCommand(customInstructions);
858
- }
859
- finally {
860
- this.editor.disableSubmit = false;
861
- }
860
+ await this.handleCompactCommand(customInstructions);
862
861
  return;
863
862
  }
864
863
  if (text === "/debug") {
@@ -898,8 +897,16 @@ export class InteractiveMode {
898
897
  return;
899
898
  }
900
899
  }
901
- // Block input during compaction
900
+ // Queue input during compaction (extension commands execute immediately)
902
901
  if (this.session.isCompacting) {
902
+ if (this.isExtensionCommand(text)) {
903
+ this.editor.addToHistory(text);
904
+ this.editor.setText("");
905
+ await this.session.prompt(text);
906
+ }
907
+ else {
908
+ this.queueCompactionMessage(text, "steer");
909
+ }
903
910
  return;
904
911
  }
905
912
  // If streaming, use prompt() with steer behavior
@@ -1060,8 +1067,7 @@ export class InteractiveMode {
1060
1067
  this.ui.requestRender();
1061
1068
  break;
1062
1069
  case "auto_compaction_start": {
1063
- // Disable submit to preserve editor text during compaction
1064
- this.editor.disableSubmit = true;
1070
+ // Keep editor active; submissions are queued during compaction.
1065
1071
  // Set up escape to abort auto-compaction
1066
1072
  this.autoCompactionEscapeHandler = this.editor.onEscape;
1067
1073
  this.editor.onEscape = () => {
@@ -1076,8 +1082,6 @@ export class InteractiveMode {
1076
1082
  break;
1077
1083
  }
1078
1084
  case "auto_compaction_end": {
1079
- // Re-enable submit
1080
- this.editor.disableSubmit = false;
1081
1085
  // Restore escape handler
1082
1086
  if (this.autoCompactionEscapeHandler) {
1083
1087
  this.editor.onEscape = this.autoCompactionEscapeHandler;
@@ -1106,6 +1110,7 @@ export class InteractiveMode {
1106
1110
  });
1107
1111
  this.footer.invalidate();
1108
1112
  }
1113
+ void this.flushCompactionQueue({ willRetry: event.willRetry });
1109
1114
  this.ui.requestRender();
1110
1115
  break;
1111
1116
  }
@@ -1356,6 +1361,18 @@ export class InteractiveMode {
1356
1361
  const text = this.editor.getText().trim();
1357
1362
  if (!text)
1358
1363
  return;
1364
+ // Queue input during compaction (extension commands execute immediately)
1365
+ if (this.session.isCompacting) {
1366
+ if (this.isExtensionCommand(text)) {
1367
+ this.editor.addToHistory(text);
1368
+ this.editor.setText("");
1369
+ await this.session.prompt(text);
1370
+ }
1371
+ else {
1372
+ this.queueCompactionMessage(text, "followUp");
1373
+ }
1374
+ return;
1375
+ }
1359
1376
  // Alt+Enter queues a follow-up message (waits until agent finishes)
1360
1377
  // This handles extension commands (execute immediately), prompt template expansion, and queueing
1361
1378
  if (this.session.isStreaming) {
@@ -1501,8 +1518,14 @@ export class InteractiveMode {
1501
1518
  }
1502
1519
  updatePendingMessagesDisplay() {
1503
1520
  this.pendingMessagesContainer.clear();
1504
- const steeringMessages = this.session.getSteeringMessages();
1505
- const followUpMessages = this.session.getFollowUpMessages();
1521
+ const steeringMessages = [
1522
+ ...this.session.getSteeringMessages(),
1523
+ ...this.compactionQueuedMessages.filter((msg) => msg.mode === "steer").map((msg) => msg.text),
1524
+ ];
1525
+ const followUpMessages = [
1526
+ ...this.session.getFollowUpMessages(),
1527
+ ...this.compactionQueuedMessages.filter((msg) => msg.mode === "followUp").map((msg) => msg.text),
1528
+ ];
1506
1529
  if (steeringMessages.length > 0 || followUpMessages.length > 0) {
1507
1530
  this.pendingMessagesContainer.addChild(new Spacer(1));
1508
1531
  for (const message of steeringMessages) {
@@ -1515,6 +1538,92 @@ export class InteractiveMode {
1515
1538
  }
1516
1539
  }
1517
1540
  }
1541
+ queueCompactionMessage(text, mode) {
1542
+ this.compactionQueuedMessages.push({ text, mode });
1543
+ this.editor.addToHistory(text);
1544
+ this.editor.setText("");
1545
+ this.updatePendingMessagesDisplay();
1546
+ this.showStatus("Queued message for after compaction");
1547
+ }
1548
+ isExtensionCommand(text) {
1549
+ if (!text.startsWith("/"))
1550
+ return false;
1551
+ const extensionRunner = this.session.extensionRunner;
1552
+ if (!extensionRunner)
1553
+ return false;
1554
+ const spaceIndex = text.indexOf(" ");
1555
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
1556
+ return !!extensionRunner.getCommand(commandName);
1557
+ }
1558
+ async flushCompactionQueue(options) {
1559
+ if (this.compactionQueuedMessages.length === 0) {
1560
+ return;
1561
+ }
1562
+ const queuedMessages = [...this.compactionQueuedMessages];
1563
+ this.compactionQueuedMessages = [];
1564
+ this.updatePendingMessagesDisplay();
1565
+ const restoreQueue = (error) => {
1566
+ this.session.clearQueue();
1567
+ this.compactionQueuedMessages = queuedMessages;
1568
+ this.updatePendingMessagesDisplay();
1569
+ this.showError(`Failed to send queued message${queuedMessages.length > 1 ? "s" : ""}: ${error instanceof Error ? error.message : String(error)}`);
1570
+ };
1571
+ try {
1572
+ if (options?.willRetry) {
1573
+ // When retry is pending, queue messages for the retry turn
1574
+ for (const message of queuedMessages) {
1575
+ if (this.isExtensionCommand(message.text)) {
1576
+ await this.session.prompt(message.text);
1577
+ }
1578
+ else if (message.mode === "followUp") {
1579
+ await this.session.followUp(message.text);
1580
+ }
1581
+ else {
1582
+ await this.session.steer(message.text);
1583
+ }
1584
+ }
1585
+ this.updatePendingMessagesDisplay();
1586
+ return;
1587
+ }
1588
+ // Find first non-extension-command message to use as prompt
1589
+ const firstPromptIndex = queuedMessages.findIndex((message) => !this.isExtensionCommand(message.text));
1590
+ if (firstPromptIndex === -1) {
1591
+ // All extension commands - execute them all
1592
+ for (const message of queuedMessages) {
1593
+ await this.session.prompt(message.text);
1594
+ }
1595
+ return;
1596
+ }
1597
+ // Execute any extension commands before the first prompt
1598
+ const preCommands = queuedMessages.slice(0, firstPromptIndex);
1599
+ const firstPrompt = queuedMessages[firstPromptIndex];
1600
+ const rest = queuedMessages.slice(firstPromptIndex + 1);
1601
+ for (const message of preCommands) {
1602
+ await this.session.prompt(message.text);
1603
+ }
1604
+ // Send first prompt (starts streaming)
1605
+ const promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {
1606
+ restoreQueue(error);
1607
+ });
1608
+ // Queue remaining messages
1609
+ for (const message of rest) {
1610
+ if (this.isExtensionCommand(message.text)) {
1611
+ await this.session.prompt(message.text);
1612
+ }
1613
+ else if (message.mode === "followUp") {
1614
+ await this.session.followUp(message.text);
1615
+ }
1616
+ else {
1617
+ await this.session.steer(message.text);
1618
+ }
1619
+ }
1620
+ this.updatePendingMessagesDisplay();
1621
+ void promptPromise;
1622
+ }
1623
+ catch (error) {
1624
+ restoreQueue(error);
1625
+ }
1626
+ }
1518
1627
  /** Move pending bash components from pending area to chat */
1519
1628
  flushPendingBashComponents() {
1520
1629
  for (const component of this.pendingBashComponents) {
@@ -1776,6 +1885,7 @@ export class InteractiveMode {
1776
1885
  this.statusContainer.clear();
1777
1886
  // Clear UI state
1778
1887
  this.pendingMessagesContainer.clear();
1888
+ this.compactionQueuedMessages = [];
1779
1889
  this.streamingComponent = undefined;
1780
1890
  this.streamingMessage = undefined;
1781
1891
  this.pendingTools.clear();
@@ -1799,78 +1909,16 @@ export class InteractiveMode {
1799
1909
  const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, async (providerId) => {
1800
1910
  done();
1801
1911
  if (mode === "login") {
1802
- this.showStatus(`Logging in to ${providerId}...`);
1803
- try {
1804
- await this.session.modelRegistry.authStorage.login(providerId, {
1805
- onAuth: (info) => {
1806
- this.chatContainer.addChild(new Spacer(1));
1807
- // OSC 8 hyperlink for desktop terminals that support clicking
1808
- const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
1809
- this.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
1810
- // OSC 52 to copy URL to clipboard (works over SSH, e.g., Termux)
1811
- const urlBase64 = Buffer.from(info.url).toString("base64");
1812
- process.stdout.write(`\x1b]52;c;${urlBase64}\x07`);
1813
- this.chatContainer.addChild(new Text(theme.fg("dim", "(URL copied to clipboard - paste in browser)"), 1, 0));
1814
- if (info.instructions) {
1815
- this.chatContainer.addChild(new Spacer(1));
1816
- this.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
1817
- }
1818
- this.ui.requestRender();
1819
- // Try to open browser on desktop
1820
- const openCmd = process.platform === "darwin"
1821
- ? "open"
1822
- : process.platform === "win32"
1823
- ? "start"
1824
- : "xdg-open";
1825
- exec(`${openCmd} "${info.url}"`);
1826
- },
1827
- onPrompt: async (prompt) => {
1828
- this.chatContainer.addChild(new Spacer(1));
1829
- this.chatContainer.addChild(new Text(theme.fg("warning", prompt.message), 1, 0));
1830
- if (prompt.placeholder) {
1831
- this.chatContainer.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0));
1832
- }
1833
- this.ui.requestRender();
1834
- return new Promise((resolve) => {
1835
- const codeInput = new Input();
1836
- codeInput.onSubmit = () => {
1837
- const code = codeInput.getValue();
1838
- this.editorContainer.clear();
1839
- this.editorContainer.addChild(this.editor);
1840
- this.ui.setFocus(this.editor);
1841
- resolve(code);
1842
- };
1843
- this.editorContainer.clear();
1844
- this.editorContainer.addChild(codeInput);
1845
- this.ui.setFocus(codeInput);
1846
- this.ui.requestRender();
1847
- });
1848
- },
1849
- onProgress: (message) => {
1850
- this.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
1851
- this.ui.requestRender();
1852
- },
1853
- });
1854
- // Refresh models to pick up new baseUrl (e.g., github-copilot)
1855
- this.session.modelRegistry.refresh();
1856
- this.chatContainer.addChild(new Spacer(1));
1857
- this.chatContainer.addChild(new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0));
1858
- this.chatContainer.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAuthPath()}`), 1, 0));
1859
- this.ui.requestRender();
1860
- }
1861
- catch (error) {
1862
- this.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
1863
- }
1912
+ await this.showLoginDialog(providerId);
1864
1913
  }
1865
1914
  else {
1915
+ // Logout flow
1916
+ const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
1917
+ const providerName = providerInfo?.name || providerId;
1866
1918
  try {
1867
1919
  this.session.modelRegistry.authStorage.logout(providerId);
1868
- // Refresh models to reset baseUrl
1869
1920
  this.session.modelRegistry.refresh();
1870
- this.chatContainer.addChild(new Spacer(1));
1871
- this.chatContainer.addChild(new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0));
1872
- this.chatContainer.addChild(new Text(theme.fg("dim", `Credentials removed from ${getAuthPath()}`), 1, 0));
1873
- this.ui.requestRender();
1921
+ this.showStatus(`Logged out of ${providerName}`);
1874
1922
  }
1875
1923
  catch (error) {
1876
1924
  this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -1883,6 +1931,83 @@ export class InteractiveMode {
1883
1931
  return { component: selector, focus: selector };
1884
1932
  });
1885
1933
  }
1934
+ async showLoginDialog(providerId) {
1935
+ const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
1936
+ const providerName = providerInfo?.name || providerId;
1937
+ // Providers that use callback servers (can paste redirect URL)
1938
+ const usesCallbackServer = providerId === "openai-codex" || providerId === "google-gemini-cli" || providerId === "google-antigravity";
1939
+ // Create login dialog component
1940
+ const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
1941
+ // Completion handled below
1942
+ });
1943
+ // Show dialog in editor container
1944
+ this.editorContainer.clear();
1945
+ this.editorContainer.addChild(dialog);
1946
+ this.ui.setFocus(dialog);
1947
+ this.ui.requestRender();
1948
+ // Promise for manual code input (racing with callback server)
1949
+ let manualCodeResolve;
1950
+ let manualCodeReject;
1951
+ const manualCodePromise = new Promise((resolve, reject) => {
1952
+ manualCodeResolve = resolve;
1953
+ manualCodeReject = reject;
1954
+ });
1955
+ // Restore editor helper
1956
+ const restoreEditor = () => {
1957
+ this.editorContainer.clear();
1958
+ this.editorContainer.addChild(this.editor);
1959
+ this.ui.setFocus(this.editor);
1960
+ this.ui.requestRender();
1961
+ };
1962
+ try {
1963
+ await this.session.modelRegistry.authStorage.login(providerId, {
1964
+ onAuth: (info) => {
1965
+ dialog.showAuth(info.url, info.instructions);
1966
+ if (usesCallbackServer) {
1967
+ // Show input for manual paste, racing with callback
1968
+ dialog
1969
+ .showManualInput("Paste redirect URL below, or complete login in browser:")
1970
+ .then((value) => {
1971
+ if (value && manualCodeResolve) {
1972
+ manualCodeResolve(value);
1973
+ manualCodeResolve = undefined;
1974
+ }
1975
+ })
1976
+ .catch(() => {
1977
+ if (manualCodeReject) {
1978
+ manualCodeReject(new Error("Login cancelled"));
1979
+ manualCodeReject = undefined;
1980
+ }
1981
+ });
1982
+ }
1983
+ else if (providerId === "github-copilot") {
1984
+ // GitHub Copilot polls after onAuth
1985
+ dialog.showWaiting("Waiting for browser authentication...");
1986
+ }
1987
+ // For Anthropic: onPrompt is called immediately after
1988
+ },
1989
+ onPrompt: async (prompt) => {
1990
+ return dialog.showPrompt(prompt.message, prompt.placeholder);
1991
+ },
1992
+ onProgress: (message) => {
1993
+ dialog.showProgress(message);
1994
+ },
1995
+ onManualCodeInput: () => manualCodePromise,
1996
+ signal: dialog.signal,
1997
+ });
1998
+ // Success
1999
+ restoreEditor();
2000
+ this.session.modelRegistry.refresh();
2001
+ this.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`);
2002
+ }
2003
+ catch (error) {
2004
+ restoreEditor();
2005
+ const errorMsg = error instanceof Error ? error.message : String(error);
2006
+ if (errorMsg !== "Login cancelled") {
2007
+ this.showError(`Failed to login to ${providerName}: ${errorMsg}`);
2008
+ }
2009
+ }
2010
+ }
1886
2011
  // =========================================================================
1887
2012
  // Command handlers
1888
2013
  // =========================================================================
@@ -2107,7 +2232,7 @@ export class InteractiveMode {
2107
2232
  | Key | Action |
2108
2233
  |-----|--------|
2109
2234
  | \`${submit}\` | Send message |
2110
- | \`${newLine}\` | New line |
2235
+ | \`${newLine}\` | New line${process.platform === "win32" ? " (Ctrl+Enter on Windows Terminal)" : ""} |
2111
2236
  | \`${deleteWordBackward}\` | Delete word backwards |
2112
2237
  | \`${deleteToLineStart}\` | Delete to start of line |
2113
2238
  | \`${deleteToLineEnd}\` | Delete to end of line |
@@ -2166,6 +2291,7 @@ export class InteractiveMode {
2166
2291
  // Clear UI state
2167
2292
  this.chatContainer.clear();
2168
2293
  this.pendingMessagesContainer.clear();
2294
+ this.compactionQueuedMessages = [];
2169
2295
  this.streamingComponent = undefined;
2170
2296
  this.streamingMessage = undefined;
2171
2297
  this.pendingTools.clear();
@@ -2287,6 +2413,7 @@ export class InteractiveMode {
2287
2413
  this.statusContainer.clear();
2288
2414
  this.editor.onEscape = originalOnEscape;
2289
2415
  }
2416
+ void this.flushCompactionQueue({ willRetry: false });
2290
2417
  }
2291
2418
  stop() {
2292
2419
  if (this.loadingAnimation) {