@oh-my-pi/pi-coding-agent 1.340.0 → 2.0.1337

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 (153) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +5 -3
  5. package/src/cli/args.ts +13 -6
  6. package/src/cli/file-processor.ts +3 -3
  7. package/src/cli/list-models.ts +2 -2
  8. package/src/cli/plugin-cli.ts +1 -1
  9. package/src/cli/session-picker.ts +2 -2
  10. package/src/cli.ts +1 -1
  11. package/src/config.ts +3 -3
  12. package/src/core/agent-session.ts +189 -29
  13. package/src/core/bash-executor.ts +50 -10
  14. package/src/core/compaction/branch-summarization.ts +5 -5
  15. package/src/core/compaction/compaction.ts +3 -3
  16. package/src/core/compaction/index.ts +3 -3
  17. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  18. package/src/core/custom-commands/index.ts +15 -0
  19. package/src/core/custom-commands/loader.ts +232 -0
  20. package/src/core/custom-commands/types.ts +112 -0
  21. package/src/core/custom-tools/index.ts +3 -3
  22. package/src/core/custom-tools/loader.ts +10 -8
  23. package/src/core/custom-tools/types.ts +11 -6
  24. package/src/core/custom-tools/wrapper.ts +2 -1
  25. package/src/core/exec.ts +22 -12
  26. package/src/core/export-html/index.ts +5 -5
  27. package/src/core/file-mentions.ts +54 -0
  28. package/src/core/hooks/index.ts +5 -5
  29. package/src/core/hooks/loader.ts +21 -16
  30. package/src/core/hooks/runner.ts +6 -6
  31. package/src/core/hooks/tool-wrapper.ts +2 -2
  32. package/src/core/hooks/types.ts +12 -15
  33. package/src/core/index.ts +6 -6
  34. package/src/core/logger.ts +112 -0
  35. package/src/core/mcp/client.ts +3 -3
  36. package/src/core/mcp/config.ts +1 -1
  37. package/src/core/mcp/index.ts +12 -12
  38. package/src/core/mcp/loader.ts +2 -2
  39. package/src/core/mcp/manager.ts +6 -6
  40. package/src/core/mcp/tool-bridge.ts +3 -3
  41. package/src/core/mcp/transports/http.ts +1 -1
  42. package/src/core/mcp/transports/index.ts +2 -2
  43. package/src/core/mcp/transports/stdio.ts +1 -1
  44. package/src/core/messages.ts +22 -0
  45. package/src/core/model-registry.ts +2 -2
  46. package/src/core/model-resolver.ts +103 -2
  47. package/src/core/plugins/doctor.ts +1 -1
  48. package/src/core/plugins/index.ts +6 -6
  49. package/src/core/plugins/installer.ts +4 -4
  50. package/src/core/plugins/loader.ts +4 -9
  51. package/src/core/plugins/manager.ts +5 -5
  52. package/src/core/plugins/paths.ts +3 -3
  53. package/src/core/sdk.ts +127 -52
  54. package/src/core/session-manager.ts +123 -20
  55. package/src/core/settings-manager.ts +106 -22
  56. package/src/core/skills.ts +5 -5
  57. package/src/core/slash-commands.ts +60 -45
  58. package/src/core/system-prompt.ts +6 -6
  59. package/src/core/title-generator.ts +94 -0
  60. package/src/core/tools/bash.ts +33 -157
  61. package/src/core/tools/context.ts +2 -2
  62. package/src/core/tools/edit-diff.ts +5 -5
  63. package/src/core/tools/edit.ts +60 -9
  64. package/src/core/tools/exa/company.ts +3 -3
  65. package/src/core/tools/exa/index.ts +16 -17
  66. package/src/core/tools/exa/linkedin.ts +3 -3
  67. package/src/core/tools/exa/mcp-client.ts +9 -9
  68. package/src/core/tools/exa/render.ts +5 -5
  69. package/src/core/tools/exa/researcher.ts +3 -3
  70. package/src/core/tools/exa/search.ts +6 -5
  71. package/src/core/tools/exa/types.ts +5 -6
  72. package/src/core/tools/exa/websets.ts +3 -3
  73. package/src/core/tools/find.ts +3 -3
  74. package/src/core/tools/grep.ts +6 -5
  75. package/src/core/tools/index.ts +114 -40
  76. package/src/core/tools/ls.ts +4 -4
  77. package/src/core/tools/lsp/client.ts +204 -108
  78. package/src/core/tools/lsp/config.ts +709 -35
  79. package/src/core/tools/lsp/edits.ts +2 -2
  80. package/src/core/tools/lsp/index.ts +432 -30
  81. package/src/core/tools/lsp/render.ts +2 -2
  82. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  83. package/src/core/tools/lsp/types.ts +5 -0
  84. package/src/core/tools/lsp/utils.ts +1 -1
  85. package/src/core/tools/notebook.ts +1 -1
  86. package/src/core/tools/output.ts +175 -0
  87. package/src/core/tools/read.ts +7 -7
  88. package/src/core/tools/renderers.ts +92 -13
  89. package/src/core/tools/review.ts +268 -0
  90. package/src/core/tools/task/agents.ts +1 -1
  91. package/src/core/tools/task/bundled-agents/explore.md +1 -1
  92. package/src/core/tools/task/bundled-agents/reviewer.md +53 -38
  93. package/src/core/tools/task/discovery.ts +2 -2
  94. package/src/core/tools/task/executor.ts +145 -28
  95. package/src/core/tools/task/index.ts +78 -30
  96. package/src/core/tools/task/model-resolver.ts +72 -13
  97. package/src/core/tools/task/parallel.ts +1 -1
  98. package/src/core/tools/task/render.ts +219 -30
  99. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  100. package/src/core/tools/task/types.ts +36 -2
  101. package/src/core/tools/web-fetch.ts +5 -3
  102. package/src/core/tools/web-search/auth.ts +1 -1
  103. package/src/core/tools/web-search/index.ts +17 -15
  104. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  105. package/src/core/tools/web-search/providers/exa.ts +3 -5
  106. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  107. package/src/core/tools/web-search/render.ts +3 -3
  108. package/src/core/tools/write.ts +70 -7
  109. package/src/index.ts +33 -17
  110. package/src/main.ts +60 -34
  111. package/src/migrations.ts +3 -3
  112. package/src/modes/index.ts +5 -5
  113. package/src/modes/interactive/components/armin.ts +1 -1
  114. package/src/modes/interactive/components/assistant-message.ts +1 -1
  115. package/src/modes/interactive/components/bash-execution.ts +4 -4
  116. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  117. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  118. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  119. package/src/modes/interactive/components/diff.ts +1 -1
  120. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  121. package/src/modes/interactive/components/footer.ts +5 -5
  122. package/src/modes/interactive/components/hook-editor.ts +2 -2
  123. package/src/modes/interactive/components/hook-input.ts +2 -2
  124. package/src/modes/interactive/components/hook-message.ts +3 -3
  125. package/src/modes/interactive/components/hook-selector.ts +2 -2
  126. package/src/modes/interactive/components/model-selector.ts +341 -41
  127. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  128. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  129. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  130. package/src/modes/interactive/components/session-selector.ts +24 -11
  131. package/src/modes/interactive/components/settings-defs.ts +51 -3
  132. package/src/modes/interactive/components/settings-selector.ts +13 -16
  133. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  134. package/src/modes/interactive/components/theme-selector.ts +2 -2
  135. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  136. package/src/modes/interactive/components/tool-execution.ts +44 -8
  137. package/src/modes/interactive/components/tree-selector.ts +5 -5
  138. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  139. package/src/modes/interactive/components/user-message.ts +1 -1
  140. package/src/modes/interactive/components/welcome.ts +42 -5
  141. package/src/modes/interactive/interactive-mode.ts +169 -48
  142. package/src/modes/interactive/theme/theme.ts +8 -7
  143. package/src/modes/print-mode.ts +4 -3
  144. package/src/modes/rpc/rpc-client.ts +4 -4
  145. package/src/modes/rpc/rpc-mode.ts +21 -11
  146. package/src/modes/rpc/rpc-types.ts +3 -3
  147. package/src/utils/changelog.ts +2 -2
  148. package/src/utils/clipboard.ts +1 -1
  149. package/src/utils/shell-snapshot.ts +218 -0
  150. package/src/utils/shell.ts +93 -13
  151. package/src/utils/tools-manager.ts +1 -1
  152. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  153. package/src/core/tools/exa/logger.ts +0 -56
@@ -23,39 +23,40 @@ import {
23
23
  TUI,
24
24
  visibleWidth,
25
25
  } from "@oh-my-pi/pi-tui";
26
- import { getAuthPath, getDebugLogPath } from "../../config.js";
27
- import type { AgentSession, AgentSessionEvent } from "../../core/agent-session.js";
28
- import type { CustomToolSessionEvent, LoadedCustomTool } from "../../core/custom-tools/index.js";
29
- import type { HookUIContext } from "../../core/hooks/index.js";
30
- import { createCompactionSummaryMessage } from "../../core/messages.js";
31
- import { type SessionContext, SessionManager } from "../../core/session-manager.js";
32
- import { loadSkills } from "../../core/skills.js";
33
- import { loadProjectContextFiles } from "../../core/system-prompt.js";
34
- import type { TruncationResult } from "../../core/tools/truncate.js";
35
- import { getChangelogPath, parseChangelog } from "../../utils/changelog.js";
36
- import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard.js";
37
- import { ArminComponent } from "./components/armin.js";
38
- import { AssistantMessageComponent } from "./components/assistant-message.js";
39
- import { BashExecutionComponent } from "./components/bash-execution.js";
40
- import { BorderedLoader } from "./components/bordered-loader.js";
41
- import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
42
- import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
43
- import { CustomEditor } from "./components/custom-editor.js";
44
- import { DynamicBorder } from "./components/dynamic-border.js";
45
- import { FooterComponent } from "./components/footer.js";
46
- import { HookEditorComponent } from "./components/hook-editor.js";
47
- import { HookInputComponent } from "./components/hook-input.js";
48
- import { HookMessageComponent } from "./components/hook-message.js";
49
- import { HookSelectorComponent } from "./components/hook-selector.js";
50
- import { ModelSelectorComponent } from "./components/model-selector.js";
51
- import { OAuthSelectorComponent } from "./components/oauth-selector.js";
52
- import { SessionSelectorComponent } from "./components/session-selector.js";
53
- import { SettingsSelectorComponent } from "./components/settings-selector.js";
54
- import { ToolExecutionComponent } from "./components/tool-execution.js";
55
- import { TreeSelectorComponent } from "./components/tree-selector.js";
56
- import { UserMessageComponent } from "./components/user-message.js";
57
- import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
58
- import { WelcomeComponent } from "./components/welcome.js";
26
+ import { getAuthPath, getDebugLogPath } from "../../config";
27
+ import type { AgentSession, AgentSessionEvent } from "../../core/agent-session";
28
+ import type { CustomToolSessionEvent, LoadedCustomTool } from "../../core/custom-tools/index";
29
+ import type { HookUIContext } from "../../core/hooks/index";
30
+ import { createCompactionSummaryMessage } from "../../core/messages";
31
+ import { getRecentSessions, type SessionContext, SessionManager } from "../../core/session-manager";
32
+ import { loadSkills } from "../../core/skills";
33
+ import { loadProjectContextFiles } from "../../core/system-prompt";
34
+ import { generateSessionTitle, setTerminalTitle } from "../../core/title-generator";
35
+ import type { TruncationResult } from "../../core/tools/truncate";
36
+ import { getChangelogPath, parseChangelog } from "../../utils/changelog";
37
+ import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
38
+ import { ArminComponent } from "./components/armin";
39
+ import { AssistantMessageComponent } from "./components/assistant-message";
40
+ import { BashExecutionComponent } from "./components/bash-execution";
41
+ import { BorderedLoader } from "./components/bordered-loader";
42
+ import { BranchSummaryMessageComponent } from "./components/branch-summary-message";
43
+ import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message";
44
+ import { CustomEditor } from "./components/custom-editor";
45
+ import { DynamicBorder } from "./components/dynamic-border";
46
+ import { FooterComponent } from "./components/footer";
47
+ import { HookEditorComponent } from "./components/hook-editor";
48
+ import { HookInputComponent } from "./components/hook-input";
49
+ import { HookMessageComponent } from "./components/hook-message";
50
+ import { HookSelectorComponent } from "./components/hook-selector";
51
+ import { ModelSelectorComponent } from "./components/model-selector";
52
+ import { OAuthSelectorComponent } from "./components/oauth-selector";
53
+ import { SessionSelectorComponent } from "./components/session-selector";
54
+ import { SettingsSelectorComponent } from "./components/settings-selector";
55
+ import { ToolExecutionComponent } from "./components/tool-execution";
56
+ import { TreeSelectorComponent } from "./components/tree-selector";
57
+ import { UserMessageComponent } from "./components/user-message";
58
+ import { UserMessageSelectorComponent } from "./components/user-message-selector";
59
+ import { WelcomeComponent } from "./components/welcome";
59
60
  import {
60
61
  getAvailableThemes,
61
62
  getEditorTheme,
@@ -64,7 +65,7 @@ import {
64
65
  setTheme,
65
66
  type Theme,
66
67
  theme,
67
- } from "./theme/theme.js";
68
+ } from "./theme/theme";
68
69
 
69
70
  /** Interface for components that can be expanded/collapsed */
70
71
  interface Expandable {
@@ -141,6 +142,9 @@ export class InteractiveMode {
141
142
  // Custom tools for custom rendering
142
143
  private customTools: Map<string, LoadedCustomTool>;
143
144
 
145
+ // Title generation state
146
+ private titleGenerationAttempted = false;
147
+
144
148
  // Convenience accessors
145
149
  private get agent() {
146
150
  return this.session.agent;
@@ -158,6 +162,9 @@ export class InteractiveMode {
158
162
  changelogMarkdown: string | undefined = undefined,
159
163
  customTools: LoadedCustomTool[] = [],
160
164
  private setToolUIContext: (uiContext: HookUIContext, hasUI: boolean) => void = () => {},
165
+ private lspServers:
166
+ | Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }>
167
+ | undefined = undefined,
161
168
  fdPath: string | undefined = undefined,
162
169
  ) {
163
170
  this.session = session;
@@ -178,7 +185,7 @@ export class InteractiveMode {
178
185
  const slashCommands: SlashCommand[] = [
179
186
  { name: "settings", description: "Open settings menu" },
180
187
  { name: "model", description: "Select model (opens selector UI)" },
181
- { name: "export", description: "Export session to HTML file" },
188
+ { name: "export", description: "Export session to HTML file or clipboard (--copy)" },
182
189
  { name: "share", description: "Share session as a secret GitHub gist" },
183
190
  { name: "copy", description: "Copy last agent message to clipboard" },
184
191
  { name: "session", description: "Show session info and stats" },
@@ -209,9 +216,15 @@ export class InteractiveMode {
209
216
  description: cmd.description ?? "(hook command)",
210
217
  }));
211
218
 
219
+ // Convert custom commands (TypeScript) to SlashCommand format
220
+ const customCommands: SlashCommand[] = this.session.customCommands.map((loaded) => ({
221
+ name: loaded.command.name,
222
+ description: `${loaded.command.description} (${loaded.source})`,
223
+ }));
224
+
212
225
  // Setup autocomplete
213
226
  const autocompleteProvider = new CombinedAutocompleteProvider(
214
- [...slashCommands, ...fileSlashCommands, ...hookCommands],
227
+ [...slashCommands, ...fileSlashCommands, ...hookCommands, ...customCommands],
215
228
  process.cwd(),
216
229
  fdPath,
217
230
  );
@@ -225,8 +238,29 @@ export class InteractiveMode {
225
238
  const modelName = this.session.model?.name ?? "Unknown";
226
239
  const providerName = this.session.model?.provider ?? "Unknown";
227
240
 
241
+ // Get recent sessions
242
+ const recentSessions = getRecentSessions(this.sessionManager.getSessionDir()).map((s) => ({
243
+ name: s.name,
244
+ timeAgo: s.timeAgo,
245
+ }));
246
+
247
+ // Convert LSP servers to welcome format
248
+ const lspServerInfo =
249
+ this.lspServers?.map((s) => ({
250
+ name: s.name,
251
+ status: s.status as "ready" | "error" | "connecting",
252
+ fileTypes: s.fileTypes,
253
+ })) ?? [];
254
+
228
255
  // Add welcome header
229
- const welcome = new WelcomeComponent(this.version, modelName, providerName);
256
+ const welcome = new WelcomeComponent(this.version, modelName, providerName, recentSessions, lspServerInfo);
257
+
258
+ // Set terminal title if session already has one (resumed session)
259
+ const existingTitle = this.sessionManager.getSessionTitle();
260
+ if (existingTitle) {
261
+ setTerminalTitle(`pi: ${existingTitle}`);
262
+ this.titleGenerationAttempted = true; // Don't try to generate again
263
+ }
230
264
 
231
265
  // Setup UI layout
232
266
  this.ui.addChild(new Spacer(1));
@@ -717,7 +751,7 @@ export class InteractiveMode {
717
751
  return;
718
752
  }
719
753
  if (text.startsWith("/export")) {
720
- this.handleExportCommand(text);
754
+ await this.handleExportCommand(text);
721
755
  this.editor.setText("");
722
756
  return;
723
757
  }
@@ -839,6 +873,19 @@ export class InteractiveMode {
839
873
  }
840
874
  }
841
875
 
876
+ // Custom commands (TypeScript slash commands) - route through session.prompt()
877
+ if (text.startsWith("/") && this.session.customCommands.length > 0) {
878
+ const spaceIndex = text.indexOf(" ");
879
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
880
+ const hasCustomCommand = this.session.customCommands.some((c) => c.command.name === commandName);
881
+ if (hasCustomCommand) {
882
+ this.editor.addToHistory(text);
883
+ this.editor.setText("");
884
+ await this.session.prompt(text);
885
+ return;
886
+ }
887
+ }
888
+
842
889
  // Queue regular messages if agent is streaming
843
890
  if (this.session.isStreaming) {
844
891
  await this.session.queueMessage(text);
@@ -1025,6 +1072,12 @@ export class InteractiveMode {
1025
1072
  }
1026
1073
  this.pendingTools.clear();
1027
1074
  this.ui.requestRender();
1075
+
1076
+ // Generate session title after first turn (if not already titled)
1077
+ if (!this.titleGenerationAttempted && !this.sessionManager.getSessionTitle()) {
1078
+ this.titleGenerationAttempted = true;
1079
+ this.maybeGenerateTitle();
1080
+ }
1028
1081
  break;
1029
1082
 
1030
1083
  case "auto_compaction_start": {
@@ -1198,6 +1251,14 @@ export class InteractiveMode {
1198
1251
  this.chatContainer.addChild(component);
1199
1252
  break;
1200
1253
  }
1254
+ case "fileMention": {
1255
+ // Render compact file mention display
1256
+ for (const file of message.files) {
1257
+ const text = `${theme.fg("dim", "⎿ ")}${theme.fg("muted", "Read")} ${theme.fg("accent", file.path)} ${theme.fg("dim", `(${file.lineCount} lines)`)}`;
1258
+ this.chatContainer.addChild(new Text(text, 0, 0));
1259
+ }
1260
+ break;
1261
+ }
1201
1262
  case "user": {
1202
1263
  const textContent = this.getUserMessageText(message);
1203
1264
  if (textContent) {
@@ -1552,6 +1613,42 @@ export class InteractiveMode {
1552
1613
  this.ui.requestRender();
1553
1614
  }
1554
1615
 
1616
+ /**
1617
+ * Generate a title for the session based on the first user message.
1618
+ * Runs in background, doesn't block UI.
1619
+ */
1620
+ private maybeGenerateTitle(): void {
1621
+ // Find the first user message
1622
+ const messages = this.agent.state.messages;
1623
+ const firstUserMessage = messages.find((m) => m.role === "user");
1624
+ if (!firstUserMessage) return;
1625
+
1626
+ // Extract text content
1627
+ let messageText = "";
1628
+ for (const content of firstUserMessage.content) {
1629
+ if (typeof content === "string") {
1630
+ messageText += content;
1631
+ } else if (content.type === "text") {
1632
+ messageText += content.text;
1633
+ }
1634
+ }
1635
+ if (!messageText.trim()) return;
1636
+
1637
+ // Generate title in background
1638
+ const registry = this.session.modelRegistry;
1639
+ const smolModel = this.settingsManager.getModelRole("smol");
1640
+ generateSessionTitle(messageText, registry, smolModel)
1641
+ .then((title) => {
1642
+ if (title) {
1643
+ this.sessionManager.setSessionTitle(title);
1644
+ setTerminalTitle(`pi: ${title}`);
1645
+ }
1646
+ })
1647
+ .catch(() => {
1648
+ // Ignore title generation errors
1649
+ });
1650
+ }
1651
+
1555
1652
  private updatePendingMessagesDisplay(): void {
1556
1653
  this.pendingMessagesContainer.clear();
1557
1654
  const queuedMessages = this.session.getQueuedMessages();
@@ -1641,6 +1738,9 @@ export class InteractiveMode {
1641
1738
  case "queueMode":
1642
1739
  this.session.setQueueMode(value as "all" | "one-at-a-time");
1643
1740
  break;
1741
+ case "interruptMode":
1742
+ this.session.setInterruptMode(value as "immediate" | "wait");
1743
+ break;
1644
1744
  case "thinkingLevel":
1645
1745
  this.session.setThinkingLevel(value as ThinkingLevel);
1646
1746
  this.footer.invalidate();
@@ -1687,17 +1787,21 @@ export class InteractiveMode {
1687
1787
  this.settingsManager,
1688
1788
  this.session.modelRegistry,
1689
1789
  this.session.scopedModels,
1690
- async (model) => {
1790
+ async (model, role) => {
1691
1791
  try {
1692
- await this.session.setModel(model);
1693
- this.footer.invalidate();
1694
- this.updateEditorBorderColor();
1695
- done();
1696
- this.showStatus(`Model: ${model.id}`);
1792
+ // Only update agent state for default role
1793
+ if (role === "default") {
1794
+ await this.session.setModel(model, role);
1795
+ this.footer.invalidate();
1796
+ this.updateEditorBorderColor();
1797
+ }
1798
+ // For other roles (small), just show status - settings already updated by selector
1799
+ const roleLabel = role === "default" ? "Default" : role === "smol" ? "Smol" : role;
1800
+ this.showStatus(`${roleLabel} model: ${model.id}`);
1697
1801
  } catch (error) {
1698
- done();
1699
1802
  this.showError(error instanceof Error ? error.message : String(error));
1700
1803
  }
1804
+ // Don't call done() - selector stays open
1701
1805
  },
1702
1806
  () => {
1703
1807
  done();
@@ -2004,12 +2108,29 @@ export class InteractiveMode {
2004
2108
  // Command handlers
2005
2109
  // =========================================================================
2006
2110
 
2007
- private handleExportCommand(text: string): void {
2111
+ private async handleExportCommand(text: string): Promise<void> {
2008
2112
  const parts = text.split(/\s+/);
2009
- const outputPath = parts.length > 1 ? parts[1] : undefined;
2113
+ const arg = parts.length > 1 ? parts[1] : undefined;
2114
+
2115
+ // Check for clipboard export
2116
+ if (arg === "--copy" || arg === "clipboard" || arg === "copy") {
2117
+ try {
2118
+ const formatted = this.session.formatSessionAsText();
2119
+ if (!formatted) {
2120
+ this.showError("No messages to export yet.");
2121
+ return;
2122
+ }
2123
+ await copyToClipboard(formatted);
2124
+ this.showStatus("Session copied to clipboard");
2125
+ } catch (error: unknown) {
2126
+ this.showError(`Failed to copy session: ${error instanceof Error ? error.message : "Unknown error"}`);
2127
+ }
2128
+ return;
2129
+ }
2010
2130
 
2131
+ // HTML file export
2011
2132
  try {
2012
- const filePath = this.session.exportToHtml(outputPath);
2133
+ const filePath = this.session.exportToHtml(arg);
2013
2134
  this.showStatus(`Session exported to: ${filePath}`);
2014
2135
  } catch (error: unknown) {
2015
2136
  this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -5,7 +5,8 @@ import { type Static, Type } from "@sinclair/typebox";
5
5
  import { TypeCompiler } from "@sinclair/typebox/compiler";
6
6
  import chalk from "chalk";
7
7
  import { highlight, supportsLanguage } from "cli-highlight";
8
- import { getCustomThemesDir, getThemesDir } from "../../../config.js";
8
+ import { getCustomThemesDir, getThemesDir } from "../../../config";
9
+ import { logger } from "../../../core/logger";
9
10
 
10
11
  // ============================================================================
11
12
  // Types & Schema
@@ -590,8 +591,8 @@ export function initTheme(themeName?: string, enableWatcher: boolean = false): v
590
591
  if (enableWatcher) {
591
592
  startThemeWatcher();
592
593
  }
593
- } catch (_error) {
594
- // Theme is invalid - fall back to dark theme silently
594
+ } catch (err) {
595
+ logger.debug("Theme loading failed, falling back to dark theme", { error: String(err) });
595
596
  currentThemeName = "dark";
596
597
  theme = loadTheme("dark");
597
598
  // Don't start watcher for fallback theme
@@ -654,8 +655,8 @@ function startThemeWatcher(): void {
654
655
  if (onThemeChangeCallback) {
655
656
  onThemeChangeCallback();
656
657
  }
657
- } catch (_error) {
658
- // Ignore errors (file might be in invalid state while being edited)
658
+ } catch (err) {
659
+ logger.debug("Theme reload error during file change", { error: String(err) });
659
660
  }
660
661
  }, 100);
661
662
  } else if (eventType === "rename") {
@@ -675,8 +676,8 @@ function startThemeWatcher(): void {
675
676
  }, 100);
676
677
  }
677
678
  });
678
- } catch (_error) {
679
- // Ignore errors starting watcher
679
+ } catch (err) {
680
+ logger.debug("Failed to start theme watcher", { error: String(err) });
680
681
  }
681
682
  }
682
683
 
@@ -7,7 +7,8 @@
7
7
  */
8
8
 
9
9
  import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
10
- import type { AgentSession } from "../core/agent-session.js";
10
+ import type { AgentSession } from "../core/agent-session";
11
+ import { logger } from "../core/logger";
11
12
 
12
13
  /**
13
14
  * Run in print (single-shot) mode.
@@ -70,8 +71,8 @@ export async function runPrintMode(
70
71
  },
71
72
  },
72
73
  );
73
- } catch (_err) {
74
- // Silently ignore tool errors
74
+ } catch (err) {
75
+ logger.warn("Tool onSession error", { error: String(err) });
75
76
  }
76
77
  }
77
78
  }
@@ -7,10 +7,10 @@
7
7
  import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
8
  import type { ImageContent } from "@oh-my-pi/pi-ai";
9
9
  import type { Subprocess } from "bun";
10
- import type { SessionStats } from "../../core/agent-session.js";
11
- import type { BashResult } from "../../core/bash-executor.js";
12
- import type { CompactionResult } from "../../core/compaction/index.js";
13
- import type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc-types.js";
10
+ import type { SessionStats } from "../../core/agent-session";
11
+ import type { BashResult } from "../../core/bash-executor";
12
+ import type { CompactionResult } from "../../core/compaction/index";
13
+ import type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc-types";
14
14
 
15
15
  // ============================================================================
16
16
  // Types
@@ -11,13 +11,14 @@
11
11
  * - Hook UI: Hook UI requests are emitted, client responds with hook_ui_response
12
12
  */
13
13
 
14
- import type { AgentSession } from "../../core/agent-session.js";
15
- import type { HookUIContext } from "../../core/hooks/index.js";
16
- import { theme } from "../interactive/theme/theme.js";
17
- import type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types.js";
14
+ import type { AgentSession } from "../../core/agent-session";
15
+ import type { HookUIContext } from "../../core/hooks/index";
16
+ import { logger } from "../../core/logger";
17
+ import { theme } from "../interactive/theme/theme";
18
+ import type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types";
18
19
 
19
20
  // Re-export types for consumers
20
- export type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types.js";
21
+ export type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types";
21
22
 
22
23
  /**
23
24
  * Run in RPC mode.
@@ -220,8 +221,8 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
220
221
  },
221
222
  },
222
223
  );
223
- } catch (_err) {
224
- // Silently ignore tool errors
224
+ } catch (err) {
225
+ logger.warn("Tool onSession error", { error: String(err) });
225
226
  }
226
227
  }
227
228
  }
@@ -231,6 +232,9 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
231
232
  output(event);
232
233
  });
233
234
 
235
+ // Serialize prompt commands to prevent concurrent execution
236
+ let activePrompt: Promise<void> | null = null;
237
+
234
238
  // Handle a single command
235
239
  const handleCommand = async (command: RpcCommand): Promise<RpcResponse> => {
236
240
  const id = command.id;
@@ -241,13 +245,18 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
241
245
  // =================================================================
242
246
 
243
247
  case "prompt": {
244
- // Don't await - events will stream
245
- // Hook commands and file slash commands are handled in session.prompt()
246
- session
248
+ // Serialize prompts to prevent concurrent execution
249
+ if (activePrompt) {
250
+ await activePrompt;
251
+ }
252
+ activePrompt = session
247
253
  .prompt(command.message, {
248
254
  images: command.images,
249
255
  })
250
- .catch((e) => output(error(id, "prompt", e.message)));
256
+ .catch((e) => output(error(id, "prompt", e.message)))
257
+ .finally(() => {
258
+ activePrompt = null;
259
+ });
251
260
  return success(id, "prompt");
252
261
  }
253
262
 
@@ -462,6 +471,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
462
471
  const response = parsed as RpcHookUIResponse;
463
472
  const pending = pendingHookRequests.get(response.id);
464
473
  if (pending) {
474
+ // Atomic delete: remove before resolve to prevent double-resolution
465
475
  pendingHookRequests.delete(response.id);
466
476
  pending.resolve(response);
467
477
  }
@@ -7,9 +7,9 @@
7
7
 
8
8
  import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
9
9
  import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
10
- import type { SessionStats } from "../../core/agent-session.js";
11
- import type { BashResult } from "../../core/bash-executor.js";
12
- import type { CompactionResult } from "../../core/compaction/index.js";
10
+ import type { SessionStats } from "../../core/agent-session";
11
+ import type { BashResult } from "../../core/bash-executor";
12
+ import type { CompactionResult } from "../../core/compaction/index";
13
13
 
14
14
  // ============================================================================
15
15
  // RPC Commands (stdin)
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
 
3
3
  export interface ChangelogEntry {
4
4
  major: number;
@@ -96,4 +96,4 @@ export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): C
96
96
  }
97
97
 
98
98
  // Re-export getChangelogPath from paths.ts for convenience
99
- export { getChangelogPath } from "../config.js";
99
+ export { getChangelogPath } from "../config";
@@ -1,4 +1,4 @@
1
- import { platform } from "os";
1
+ import { platform } from "node:os";
2
2
 
3
3
  async function spawnWithTimeout(cmd: string[], input: string, timeoutMs: number): Promise<void> {
4
4
  const proc = Bun.spawn(cmd, { stdin: "pipe" });