@oh-my-pi/pi-coding-agent 1.338.0 → 1.341.0

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 (42) hide show
  1. package/CHANGELOG.md +60 -1
  2. package/package.json +3 -3
  3. package/src/cli/args.ts +8 -0
  4. package/src/core/agent-session.ts +32 -14
  5. package/src/core/export-html/index.ts +48 -15
  6. package/src/core/export-html/template.html +3 -11
  7. package/src/core/mcp/client.ts +43 -16
  8. package/src/core/mcp/config.ts +152 -6
  9. package/src/core/mcp/index.ts +6 -2
  10. package/src/core/mcp/loader.ts +30 -3
  11. package/src/core/mcp/manager.ts +69 -10
  12. package/src/core/mcp/types.ts +9 -3
  13. package/src/core/model-resolver.ts +101 -0
  14. package/src/core/sdk.ts +65 -18
  15. package/src/core/session-manager.ts +117 -14
  16. package/src/core/settings-manager.ts +107 -19
  17. package/src/core/title-generator.ts +94 -0
  18. package/src/core/tools/bash.ts +1 -2
  19. package/src/core/tools/edit-diff.ts +2 -2
  20. package/src/core/tools/edit.ts +43 -5
  21. package/src/core/tools/grep.ts +3 -2
  22. package/src/core/tools/index.ts +73 -13
  23. package/src/core/tools/lsp/client.ts +45 -20
  24. package/src/core/tools/lsp/config.ts +708 -34
  25. package/src/core/tools/lsp/index.ts +423 -23
  26. package/src/core/tools/lsp/types.ts +5 -0
  27. package/src/core/tools/task/bundled-agents/explore.md +1 -1
  28. package/src/core/tools/task/bundled-agents/reviewer.md +1 -1
  29. package/src/core/tools/task/model-resolver.ts +52 -3
  30. package/src/core/tools/write.ts +67 -4
  31. package/src/index.ts +5 -0
  32. package/src/main.ts +23 -2
  33. package/src/modes/interactive/components/model-selector.ts +96 -18
  34. package/src/modes/interactive/components/session-selector.ts +20 -7
  35. package/src/modes/interactive/components/settings-defs.ts +59 -2
  36. package/src/modes/interactive/components/settings-selector.ts +8 -11
  37. package/src/modes/interactive/components/tool-execution.ts +18 -0
  38. package/src/modes/interactive/components/tree-selector.ts +2 -2
  39. package/src/modes/interactive/components/welcome.ts +40 -3
  40. package/src/modes/interactive/interactive-mode.ts +87 -10
  41. package/src/core/export-html/vendor/highlight.min.js +0 -1213
  42. package/src/core/export-html/vendor/marked.min.js +0 -6
@@ -7,6 +7,12 @@ export interface RecentSession {
7
7
  timeAgo: string;
8
8
  }
9
9
 
10
+ export interface LspServerInfo {
11
+ name: string;
12
+ status: "ready" | "error" | "connecting";
13
+ fileTypes: string[];
14
+ }
15
+
10
16
  /**
11
17
  * Premium welcome screen with block-based Pi logo and two-column layout.
12
18
  */
@@ -15,12 +21,20 @@ export class WelcomeComponent implements Component {
15
21
  private modelName: string;
16
22
  private providerName: string;
17
23
  private recentSessions: RecentSession[];
18
-
19
- constructor(version: string, modelName: string, providerName: string, recentSessions: RecentSession[] = []) {
24
+ private lspServers: LspServerInfo[];
25
+
26
+ constructor(
27
+ version: string,
28
+ modelName: string,
29
+ providerName: string,
30
+ recentSessions: RecentSession[] = [],
31
+ lspServers: LspServerInfo[] = [],
32
+ ) {
20
33
  this.version = version;
21
34
  this.modelName = modelName;
22
35
  this.providerName = providerName;
23
36
  this.recentSessions = recentSessions;
37
+ this.lspServers = lspServers;
24
38
  }
25
39
 
26
40
  invalidate(): void {}
@@ -34,6 +48,10 @@ export class WelcomeComponent implements Component {
34
48
  this.recentSessions = sessions;
35
49
  }
36
50
 
51
+ setLspServers(servers: LspServerInfo[]): void {
52
+ this.lspServers = servers;
53
+ }
54
+
37
55
  render(termWidth: number): string[] {
38
56
  // Box dimensions - responsive with min/max
39
57
  const minWidth = 80;
@@ -76,13 +94,32 @@ export class WelcomeComponent implements Component {
76
94
  }
77
95
  }
78
96
 
97
+ // LSP servers content
98
+ const lspLines: string[] = [];
99
+ if (this.lspServers.length === 0) {
100
+ lspLines.push(` ${theme.fg("dim", "No LSP servers")}`);
101
+ } else {
102
+ for (const server of this.lspServers) {
103
+ const icon =
104
+ server.status === "ready"
105
+ ? theme.fg("success", "●")
106
+ : server.status === "connecting"
107
+ ? theme.fg("warning", "○")
108
+ : theme.fg("error", "●");
109
+ const exts = server.fileTypes.slice(0, 3).join(" ");
110
+ lspLines.push(` ${icon} ${theme.fg("muted", server.name)} ${theme.fg("dim", exts)}`);
111
+ }
112
+ }
113
+
79
114
  // Right column
80
115
  const rightLines = [
81
116
  ` ${theme.bold(theme.fg("accent", "Tips"))}`,
82
117
  ` ${theme.fg("dim", "?")}${theme.fg("muted", " for keyboard shortcuts")}`,
83
118
  ` ${theme.fg("dim", "/")}${theme.fg("muted", " for commands")}`,
84
119
  ` ${theme.fg("dim", "!")}${theme.fg("muted", " to run bash")}`,
85
- ` ${theme.fg("dim", "/status")}${theme.fg("muted", " for loaded extensions")}`,
120
+ separator,
121
+ ` ${theme.bold(theme.fg("accent", "LSP Servers"))}`,
122
+ ...lspLines,
86
123
  separator,
87
124
  ` ${theme.bold(theme.fg("accent", "Recent sessions"))}`,
88
125
  ...sessionLines,
@@ -28,9 +28,10 @@ import type { AgentSession, AgentSessionEvent } from "../../core/agent-session.j
28
28
  import type { CustomToolSessionEvent, LoadedCustomTool } from "../../core/custom-tools/index.js";
29
29
  import type { HookUIContext } from "../../core/hooks/index.js";
30
30
  import { createCompactionSummaryMessage } from "../../core/messages.js";
31
- import { type SessionContext, SessionManager } from "../../core/session-manager.js";
31
+ import { getRecentSessions, type SessionContext, SessionManager } from "../../core/session-manager.js";
32
32
  import { loadSkills } from "../../core/skills.js";
33
33
  import { loadProjectContextFiles } from "../../core/system-prompt.js";
34
+ import { generateSessionTitle, setTerminalTitle } from "../../core/title-generator.js";
34
35
  import type { TruncationResult } from "../../core/tools/truncate.js";
35
36
  import { getChangelogPath, parseChangelog } from "../../utils/changelog.js";
36
37
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard.js";
@@ -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;
@@ -225,8 +232,29 @@ export class InteractiveMode {
225
232
  const modelName = this.session.model?.name ?? "Unknown";
226
233
  const providerName = this.session.model?.provider ?? "Unknown";
227
234
 
235
+ // Get recent sessions
236
+ const recentSessions = getRecentSessions(this.sessionManager.getSessionDir()).map((s) => ({
237
+ name: s.name,
238
+ timeAgo: s.timeAgo,
239
+ }));
240
+
241
+ // Convert LSP servers to welcome format
242
+ const lspServerInfo =
243
+ this.lspServers?.map((s) => ({
244
+ name: s.name,
245
+ status: s.status as "ready" | "error" | "connecting",
246
+ fileTypes: s.fileTypes,
247
+ })) ?? [];
248
+
228
249
  // Add welcome header
229
- const welcome = new WelcomeComponent(this.version, modelName, providerName);
250
+ const welcome = new WelcomeComponent(this.version, modelName, providerName, recentSessions, lspServerInfo);
251
+
252
+ // Set terminal title if session already has one (resumed session)
253
+ const existingTitle = this.sessionManager.getSessionTitle();
254
+ if (existingTitle) {
255
+ setTerminalTitle(`pi: ${existingTitle}`);
256
+ this.titleGenerationAttempted = true; // Don't try to generate again
257
+ }
230
258
 
231
259
  // Setup UI layout
232
260
  this.ui.addChild(new Spacer(1));
@@ -1025,6 +1053,12 @@ export class InteractiveMode {
1025
1053
  }
1026
1054
  this.pendingTools.clear();
1027
1055
  this.ui.requestRender();
1056
+
1057
+ // Generate session title after first turn (if not already titled)
1058
+ if (!this.titleGenerationAttempted && !this.sessionManager.getSessionTitle()) {
1059
+ this.titleGenerationAttempted = true;
1060
+ this.maybeGenerateTitle();
1061
+ }
1028
1062
  break;
1029
1063
 
1030
1064
  case "auto_compaction_start": {
@@ -1552,6 +1586,42 @@ export class InteractiveMode {
1552
1586
  this.ui.requestRender();
1553
1587
  }
1554
1588
 
1589
+ /**
1590
+ * Generate a title for the session based on the first user message.
1591
+ * Runs in background, doesn't block UI.
1592
+ */
1593
+ private maybeGenerateTitle(): void {
1594
+ // Find the first user message
1595
+ const messages = this.agent.state.messages;
1596
+ const firstUserMessage = messages.find((m) => m.role === "user");
1597
+ if (!firstUserMessage) return;
1598
+
1599
+ // Extract text content
1600
+ let messageText = "";
1601
+ for (const content of firstUserMessage.content) {
1602
+ if (typeof content === "string") {
1603
+ messageText += content;
1604
+ } else if (content.type === "text") {
1605
+ messageText += content.text;
1606
+ }
1607
+ }
1608
+ if (!messageText.trim()) return;
1609
+
1610
+ // Generate title in background
1611
+ const registry = this.session.modelRegistry;
1612
+ const smolModel = this.settingsManager.getModelRole("smol");
1613
+ generateSessionTitle(messageText, registry, smolModel)
1614
+ .then((title) => {
1615
+ if (title) {
1616
+ this.sessionManager.setSessionTitle(title);
1617
+ setTerminalTitle(`pi: ${title}`);
1618
+ }
1619
+ })
1620
+ .catch(() => {
1621
+ // Ignore title generation errors
1622
+ });
1623
+ }
1624
+
1555
1625
  private updatePendingMessagesDisplay(): void {
1556
1626
  this.pendingMessagesContainer.clear();
1557
1627
  const queuedMessages = this.session.getQueuedMessages();
@@ -1641,6 +1711,9 @@ export class InteractiveMode {
1641
1711
  case "queueMode":
1642
1712
  this.session.setQueueMode(value as "all" | "one-at-a-time");
1643
1713
  break;
1714
+ case "interruptMode":
1715
+ this.session.setInterruptMode(value as "immediate" | "wait");
1716
+ break;
1644
1717
  case "thinkingLevel":
1645
1718
  this.session.setThinkingLevel(value as ThinkingLevel);
1646
1719
  this.footer.invalidate();
@@ -1687,17 +1760,21 @@ export class InteractiveMode {
1687
1760
  this.settingsManager,
1688
1761
  this.session.modelRegistry,
1689
1762
  this.session.scopedModels,
1690
- async (model) => {
1763
+ async (model, role) => {
1691
1764
  try {
1692
- await this.session.setModel(model);
1693
- this.footer.invalidate();
1694
- this.updateEditorBorderColor();
1695
- done();
1696
- this.showStatus(`Model: ${model.id}`);
1765
+ // Only update agent state for default role
1766
+ if (role === "default") {
1767
+ await this.session.setModel(model, role);
1768
+ this.footer.invalidate();
1769
+ this.updateEditorBorderColor();
1770
+ }
1771
+ // For other roles (small), just show status - settings already updated by selector
1772
+ const roleLabel = role === "default" ? "Default" : role === "smol" ? "Smol" : role;
1773
+ this.showStatus(`${roleLabel} model: ${model.id}`);
1697
1774
  } catch (error) {
1698
- done();
1699
1775
  this.showError(error instanceof Error ? error.message : String(error));
1700
1776
  }
1777
+ // Don't call done() - selector stays open
1701
1778
  },
1702
1779
  () => {
1703
1780
  done();
@@ -2122,7 +2199,7 @@ export class InteractiveMode {
2122
2199
  }
2123
2200
 
2124
2201
  // Create the preview URL
2125
- const previewUrl = `https://shittycodingagent.ai/session?${gistId}`;
2202
+ const previewUrl = `https://gistpreview.github.io/?${gistId}`;
2126
2203
  this.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
2127
2204
  } catch (error: unknown) {
2128
2205
  if (!loader.signal.aborted) {