@dyyz1993/pi-coding-agent 0.70.6 → 0.74.5

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 (275) hide show
  1. package/CHANGELOG.md +266 -80
  2. package/README.md +48 -20
  3. package/dist/bun/cli.d.ts.map +1 -1
  4. package/dist/bun/cli.js +4 -2
  5. package/dist/bun/cli.js.map +1 -1
  6. package/dist/bun/restore-sandbox-env.d.ts +13 -0
  7. package/dist/bun/restore-sandbox-env.d.ts.map +1 -0
  8. package/dist/bun/restore-sandbox-env.js +32 -0
  9. package/dist/bun/restore-sandbox-env.js.map +1 -0
  10. package/dist/cli/args.d.ts +2 -1
  11. package/dist/cli/args.d.ts.map +1 -1
  12. package/dist/cli/args.js +34 -22
  13. package/dist/cli/args.js.map +1 -1
  14. package/dist/cli/list-models.d.ts.map +1 -1
  15. package/dist/cli/list-models.js +2 -1
  16. package/dist/cli/list-models.js.map +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +9 -4
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config.d.ts +16 -8
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +238 -66
  23. package/dist/config.js.map +1 -1
  24. package/dist/core/agent-session-runtime.d.ts +10 -0
  25. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  26. package/dist/core/agent-session-runtime.js +14 -0
  27. package/dist/core/agent-session-runtime.js.map +1 -1
  28. package/dist/core/agent-session-services.d.ts +2 -1
  29. package/dist/core/agent-session-services.d.ts.map +1 -1
  30. package/dist/core/agent-session-services.js +1 -0
  31. package/dist/core/agent-session-services.js.map +1 -1
  32. package/dist/core/agent-session.d.ts +37 -26
  33. package/dist/core/agent-session.d.ts.map +1 -1
  34. package/dist/core/agent-session.js +1068 -1116
  35. package/dist/core/agent-session.js.map +1 -1
  36. package/dist/core/auth-guidance.d.ts +5 -0
  37. package/dist/core/auth-guidance.d.ts.map +1 -0
  38. package/dist/core/auth-guidance.js +21 -0
  39. package/dist/core/auth-guidance.js.map +1 -0
  40. package/dist/core/auth-storage.d.ts +9 -0
  41. package/dist/core/auth-storage.d.ts.map +1 -1
  42. package/dist/core/auth-storage.js +20 -1
  43. package/dist/core/auth-storage.js.map +1 -1
  44. package/dist/core/bash-executor.d.ts.map +1 -1
  45. package/dist/core/bash-executor.js +9 -6
  46. package/dist/core/bash-executor.js.map +1 -1
  47. package/dist/core/compaction/compaction.d.ts +0 -1
  48. package/dist/core/compaction/compaction.d.ts.map +1 -1
  49. package/dist/core/compaction/compaction.js.map +1 -1
  50. package/dist/core/export-html/ansi-to-html.d.ts.map +1 -1
  51. package/dist/core/export-html/ansi-to-html.js +1 -1
  52. package/dist/core/export-html/ansi-to-html.js.map +1 -1
  53. package/dist/core/export-html/template.css +53 -4
  54. package/dist/core/export-html/template.js +84 -20
  55. package/dist/core/export-html/tool-renderer.d.ts +0 -6
  56. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  57. package/dist/core/export-html/tool-renderer.js +15 -2
  58. package/dist/core/export-html/tool-renderer.js.map +1 -1
  59. package/dist/core/extensions/index.d.ts +1 -1
  60. package/dist/core/extensions/index.d.ts.map +1 -1
  61. package/dist/core/extensions/index.js.map +1 -1
  62. package/dist/core/extensions/loader.d.ts +0 -1
  63. package/dist/core/extensions/loader.d.ts.map +1 -1
  64. package/dist/core/extensions/loader.js +49 -137
  65. package/dist/core/extensions/loader.js.map +1 -1
  66. package/dist/core/extensions/runner.d.ts +24 -20
  67. package/dist/core/extensions/runner.d.ts.map +1 -1
  68. package/dist/core/extensions/runner.js +128 -253
  69. package/dist/core/extensions/runner.js.map +1 -1
  70. package/dist/core/extensions/types.d.ts +88 -60
  71. package/dist/core/extensions/types.d.ts.map +1 -1
  72. package/dist/core/extensions/types.js +10 -0
  73. package/dist/core/extensions/types.js.map +1 -1
  74. package/dist/core/file-store/file-snapshot-manager.d.ts +95 -0
  75. package/dist/core/file-store/file-snapshot-manager.d.ts.map +1 -0
  76. package/dist/core/file-store/file-snapshot-manager.js +508 -0
  77. package/dist/core/file-store/file-snapshot-manager.js.map +1 -0
  78. package/dist/core/file-store/index.d.ts +5 -0
  79. package/dist/core/file-store/index.d.ts.map +1 -0
  80. package/dist/core/file-store/index.js +3 -0
  81. package/dist/core/file-store/index.js.map +1 -0
  82. package/dist/core/messages.d.ts +10 -2
  83. package/dist/core/messages.d.ts.map +1 -1
  84. package/dist/core/messages.js +23 -6
  85. package/dist/core/messages.js.map +1 -1
  86. package/dist/core/model-registry.d.ts +19 -1
  87. package/dist/core/model-registry.d.ts.map +1 -1
  88. package/dist/core/model-registry.js +97 -16
  89. package/dist/core/model-registry.js.map +1 -1
  90. package/dist/core/model-resolver.d.ts.map +1 -1
  91. package/dist/core/model-resolver.js +24 -15
  92. package/dist/core/model-resolver.js.map +1 -1
  93. package/dist/core/package-manager.d.ts +1 -0
  94. package/dist/core/package-manager.d.ts.map +1 -1
  95. package/dist/core/package-manager.js +61 -35
  96. package/dist/core/package-manager.js.map +1 -1
  97. package/dist/core/provider-display-names.d.ts +2 -0
  98. package/dist/core/provider-display-names.d.ts.map +1 -0
  99. package/dist/core/provider-display-names.js +32 -0
  100. package/dist/core/provider-display-names.js.map +1 -0
  101. package/dist/core/resource-loader.d.ts.map +1 -1
  102. package/dist/core/resource-loader.js +9 -21
  103. package/dist/core/resource-loader.js.map +1 -1
  104. package/dist/core/sdk.d.ts +9 -1
  105. package/dist/core/sdk.d.ts.map +1 -1
  106. package/dist/core/sdk.js +39 -18
  107. package/dist/core/sdk.js.map +1 -1
  108. package/dist/core/session-manager.d.ts +27 -17
  109. package/dist/core/session-manager.d.ts.map +1 -1
  110. package/dist/core/session-manager.js +133 -47
  111. package/dist/core/session-manager.js.map +1 -1
  112. package/dist/core/settings-manager.d.ts +22 -3
  113. package/dist/core/settings-manager.d.ts.map +1 -1
  114. package/dist/core/settings-manager.js +54 -6
  115. package/dist/core/settings-manager.js.map +1 -1
  116. package/dist/core/skills.d.ts.map +1 -1
  117. package/dist/core/skills.js +3 -8
  118. package/dist/core/skills.js.map +1 -1
  119. package/dist/core/slash-commands.d.ts.map +1 -1
  120. package/dist/core/slash-commands.js +4 -3
  121. package/dist/core/slash-commands.js.map +1 -1
  122. package/dist/core/tools/bash.d.ts +0 -2
  123. package/dist/core/tools/bash.d.ts.map +1 -1
  124. package/dist/core/tools/bash.js +155 -110
  125. package/dist/core/tools/bash.js.map +1 -1
  126. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  127. package/dist/core/tools/edit-diff.js +3 -2
  128. package/dist/core/tools/edit-diff.js.map +1 -1
  129. package/dist/core/tools/edit.d.ts.map +1 -1
  130. package/dist/core/tools/edit.js +4 -3
  131. package/dist/core/tools/edit.js.map +1 -1
  132. package/dist/core/tools/find.d.ts.map +1 -1
  133. package/dist/core/tools/find.js +1 -1
  134. package/dist/core/tools/find.js.map +1 -1
  135. package/dist/core/tools/grep.d.ts.map +1 -1
  136. package/dist/core/tools/grep.js +1 -1
  137. package/dist/core/tools/grep.js.map +1 -1
  138. package/dist/core/tools/output-accumulator.d.ts +50 -0
  139. package/dist/core/tools/output-accumulator.d.ts.map +1 -0
  140. package/dist/core/tools/output-accumulator.js +178 -0
  141. package/dist/core/tools/output-accumulator.js.map +1 -0
  142. package/dist/core/tools/read.d.ts.map +1 -1
  143. package/dist/core/tools/read.js +70 -13
  144. package/dist/core/tools/read.js.map +1 -1
  145. package/dist/index.d.ts +1 -1
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js.map +1 -1
  148. package/dist/main.d.ts.map +1 -1
  149. package/dist/main.js +17 -39
  150. package/dist/main.js.map +1 -1
  151. package/dist/migrations.d.ts +1 -1
  152. package/dist/migrations.d.ts.map +1 -1
  153. package/dist/migrations.js +3 -3
  154. package/dist/migrations.js.map +1 -1
  155. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  156. package/dist/modes/interactive/components/config-selector.js +3 -1
  157. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  158. package/dist/modes/interactive/components/extension-selector.d.ts +1 -4
  159. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  160. package/dist/modes/interactive/components/extension-selector.js +14 -56
  161. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  162. package/dist/modes/interactive/components/login-dialog.d.ts +5 -1
  163. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  164. package/dist/modes/interactive/components/login-dialog.js +19 -4
  165. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  166. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  167. package/dist/modes/interactive/components/model-selector.js +1 -1
  168. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  169. package/dist/modes/interactive/components/oauth-selector.d.ts +18 -6
  170. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  171. package/dist/modes/interactive/components/oauth-selector.js +93 -25
  172. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  173. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  174. package/dist/modes/interactive/components/scoped-models-selector.js +1 -1
  175. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  176. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  177. package/dist/modes/interactive/components/session-selector.js +3 -7
  178. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  179. package/dist/modes/interactive/components/settings-selector.d.ts +5 -0
  180. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  181. package/dist/modes/interactive/components/settings-selector.js +53 -1
  182. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  183. package/dist/modes/interactive/interactive-mode.d.ts +20 -4
  184. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  185. package/dist/modes/interactive/interactive-mode.js +423 -186
  186. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  187. package/dist/modes/interactive/theme/dark.json +1 -1
  188. package/dist/modes/interactive/theme/light.json +1 -1
  189. package/dist/modes/print-mode.d.ts +3 -0
  190. package/dist/modes/print-mode.d.ts.map +1 -1
  191. package/dist/modes/print-mode.js +62 -19
  192. package/dist/modes/print-mode.js.map +1 -1
  193. package/dist/modes/rpc/rpc-client.d.ts +80 -60
  194. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  195. package/dist/modes/rpc/rpc-client.js +108 -93
  196. package/dist/modes/rpc/rpc-client.js.map +1 -1
  197. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  198. package/dist/modes/rpc/rpc-mode.js +106 -0
  199. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  200. package/dist/modes/rpc/rpc-types.d.ts +115 -0
  201. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  202. package/dist/modes/rpc/rpc-types.js.map +1 -1
  203. package/dist/package-manager-cli.d.ts.map +1 -1
  204. package/dist/package-manager-cli.js +238 -12
  205. package/dist/package-manager-cli.js.map +1 -1
  206. package/dist/utils/child-process.d.ts +1 -0
  207. package/dist/utils/child-process.d.ts.map +1 -1
  208. package/dist/utils/child-process.js +8 -0
  209. package/dist/utils/child-process.js.map +1 -1
  210. package/dist/utils/clipboard-image.d.ts.map +1 -1
  211. package/dist/utils/clipboard-image.js +2 -2
  212. package/dist/utils/clipboard-image.js.map +1 -1
  213. package/dist/utils/clipboard.d.ts.map +1 -1
  214. package/dist/utils/clipboard.js +84 -45
  215. package/dist/utils/clipboard.js.map +1 -1
  216. package/dist/utils/paths.d.ts +9 -0
  217. package/dist/utils/paths.d.ts.map +1 -1
  218. package/dist/utils/paths.js +31 -0
  219. package/dist/utils/paths.js.map +1 -1
  220. package/dist/utils/pi-user-agent.d.ts +2 -0
  221. package/dist/utils/pi-user-agent.d.ts.map +1 -0
  222. package/dist/utils/pi-user-agent.js +5 -0
  223. package/dist/utils/pi-user-agent.js.map +1 -0
  224. package/dist/utils/structured-output.d.ts +10 -0
  225. package/dist/utils/structured-output.d.ts.map +1 -0
  226. package/dist/utils/structured-output.js +57 -0
  227. package/dist/utils/structured-output.js.map +1 -0
  228. package/dist/utils/tools-manager.d.ts.map +1 -1
  229. package/dist/utils/tools-manager.js +6 -2
  230. package/dist/utils/tools-manager.js.map +1 -1
  231. package/dist/utils/version-check.d.ts +14 -0
  232. package/dist/utils/version-check.d.ts.map +1 -0
  233. package/dist/utils/version-check.js +77 -0
  234. package/dist/utils/version-check.js.map +1 -0
  235. package/docs/compaction.md +14 -14
  236. package/docs/custom-provider.md +40 -31
  237. package/docs/development.md +1 -1
  238. package/docs/docs.json +148 -0
  239. package/docs/extensions.md +116 -56
  240. package/docs/index.md +70 -0
  241. package/docs/json.md +4 -4
  242. package/docs/models.md +150 -3
  243. package/docs/packages.md +10 -5
  244. package/docs/providers.md +62 -17
  245. package/docs/quickstart.md +142 -0
  246. package/docs/rollback-architecture.md +693 -0
  247. package/docs/rollback-test-cases.md +412 -0
  248. package/docs/rpc.md +1 -1
  249. package/docs/sdk.md +26 -26
  250. package/docs/{session.md → session-format.md} +6 -6
  251. package/docs/sessions.md +137 -0
  252. package/docs/settings.md +52 -9
  253. package/docs/termux.md +1 -1
  254. package/docs/themes.md +2 -2
  255. package/docs/tui.md +20 -20
  256. package/docs/usage.md +277 -0
  257. package/examples/extensions/README.md +2 -4
  258. package/examples/extensions/border-status-editor.ts +150 -0
  259. package/examples/extensions/commands.ts +2 -2
  260. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  261. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  262. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  263. package/examples/extensions/dynamic-resources/dynamic.json +1 -1
  264. package/examples/extensions/git-checkpoint.ts +1 -1
  265. package/examples/extensions/handoff.ts +49 -11
  266. package/examples/extensions/plan-mode/index.ts +1 -1
  267. package/examples/extensions/sandbox/package-lock.json +5 -5
  268. package/examples/extensions/sandbox/package.json +1 -1
  269. package/examples/extensions/subagent/agents.ts +126 -0
  270. package/examples/extensions/with-deps/package-lock.json +2 -2
  271. package/examples/extensions/with-deps/package.json +1 -1
  272. package/examples/sdk/README.md +2 -2
  273. package/package.json +7 -16
  274. package/docs/tree.md +0 -233
  275. package/examples/extensions/antigravity-image-gen.ts +0 -418
@@ -6,27 +6,31 @@ import * as crypto from "node:crypto";
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
+ import { getProviders, } from "@dyyz1993/pi-ai";
9
10
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@dyyz1993/pi-tui";
10
11
  import { spawn, spawnSync } from "child_process";
11
- import { APP_NAME, getAgentDir, getAuthPath, getDebugLogPath, getShareViewerUrl, getUpdateInstruction, VERSION, } from "../../config.js";
12
+ import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
12
13
  import { parseSkillBlock } from "../../core/agent-session.js";
13
14
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
14
15
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
15
16
  import { KeybindingsManager } from "../../core/keybindings.js";
16
17
  import { createCompactionSummaryMessage } from "../../core/messages.js";
17
- import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelAlias, resolveModelScope, } from "../../core/model-resolver.js";
18
+ import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
18
19
  import { DefaultPackageManager } from "../../core/package-manager.js";
20
+ import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
19
21
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
20
22
  import { SessionManager } from "../../core/session-manager.js";
21
23
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
22
- import { getCwdDataDir, getGlobalDataDir, getProjectDataDir, getSessionDataDir, resolveProjectRoot, } from "../../core/storage.js";
23
24
  import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
24
25
  import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
25
26
  import { copyToClipboard } from "../../utils/clipboard.js";
26
27
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
27
28
  import { parseGitUrl } from "../../utils/git.js";
29
+ import { getCwdRelativePath } from "../../utils/paths.js";
30
+ import { getPiUserAgent } from "../../utils/pi-user-agent.js";
28
31
  import { killTrackedDetachedChildren } from "../../utils/shell.js";
29
32
  import { ensureTool } from "../../utils/tools-manager.js";
33
+ import { checkForNewPiVersion } from "../../utils/version-check.js";
30
34
  import { ArminComponent } from "./components/armin.js";
31
35
  import { AssistantMessageComponent } from "./components/assistant-message.js";
32
36
  import { BashExecutionComponent } from "./components/bash-execution.js";
@@ -71,7 +75,15 @@ class ExpandableText extends Text {
71
75
  this.setText(expanded ? this.getExpandedText() : this.getCollapsedText());
72
76
  }
73
77
  }
74
- const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth is active. Third-party usage now draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage.";
78
+ const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
79
+ function isDeadTerminalError(error) {
80
+ if (!error || typeof error !== "object" || !("code" in error)) {
81
+ return false;
82
+ }
83
+ const code = error.code;
84
+ return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
85
+ }
86
+ const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage.";
75
87
  function isAnthropicSubscriptionAuthKey(apiKey) {
76
88
  return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
77
89
  }
@@ -81,6 +93,17 @@ function isUnknownModel(model) {
81
93
  function hasDefaultModelProvider(providerId) {
82
94
  return providerId in defaultModelPerProvider;
83
95
  }
96
+ const BEDROCK_PROVIDER_ID = "amazon-bedrock";
97
+ const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders());
98
+ export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProviderIds = BUILT_IN_MODEL_PROVIDERS) {
99
+ if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
100
+ return true;
101
+ }
102
+ if (builtInProviderIds.has(providerId)) {
103
+ return false;
104
+ }
105
+ return !oauthProviderIds.has(providerId);
106
+ }
84
107
  export class InteractiveMode {
85
108
  options;
86
109
  runtimeHost;
@@ -90,6 +113,7 @@ export class InteractiveMode {
90
113
  statusContainer;
91
114
  defaultEditor;
92
115
  editor;
116
+ editorComponentFactory;
93
117
  autocompleteProvider;
94
118
  autocompleteProviderWrappers = [];
95
119
  fdPath;
@@ -103,6 +127,7 @@ export class InteractiveMode {
103
127
  onInputCallback;
104
128
  loadingAnimation = undefined;
105
129
  workingMessage = undefined;
130
+ workingVisible = true;
106
131
  workingIndicatorOptions = undefined;
107
132
  defaultWorkingMessage = "Working...";
108
133
  defaultHiddenThinkingLabel = "Thinking...";
@@ -120,8 +145,6 @@ export class InteractiveMode {
120
145
  streamingMessage = undefined;
121
146
  // Tool execution tracking: toolCallId -> component
122
147
  pendingTools = new Map();
123
- // Track first user message to avoid leading spacer at top of chat
124
- isFirstUserMessage = true;
125
148
  // Tool output expansion state
126
149
  toolOutputExpanded = false;
127
150
  // Thinking block visibility state
@@ -182,6 +205,9 @@ export class InteractiveMode {
182
205
  constructor(runtimeHost, options = {}) {
183
206
  this.options = options;
184
207
  this.runtimeHost = runtimeHost;
208
+ this.runtimeHost.setBeforeSessionInvalidate(() => {
209
+ this.resetExtensionUI();
210
+ });
185
211
  this.runtimeHost.setRebindSession(async () => {
186
212
  await this.rebindCurrentSession();
187
213
  });
@@ -449,10 +475,10 @@ export class InteractiveMode {
449
475
  const cwdBasename = path.basename(this.sessionManager.getCwd());
450
476
  const sessionName = this.sessionManager.getSessionName();
451
477
  if (sessionName) {
452
- this.ui.terminal.setTitle( - ${sessionName} - ${cwdBasename}`);
478
+ this.ui.terminal.setTitle(`${APP_TITLE} - ${sessionName} - ${cwdBasename}`);
453
479
  }
454
480
  else {
455
- this.ui.terminal.setTitle( - ${cwdBasename}`);
481
+ this.ui.terminal.setTitle(`${APP_TITLE} - ${cwdBasename}`);
456
482
  }
457
483
  }
458
484
  /**
@@ -462,7 +488,7 @@ export class InteractiveMode {
462
488
  async run() {
463
489
  await this.init();
464
490
  // Start version check asynchronously
465
- this.checkForNewVersion().then((newVersion) => {
491
+ checkForNewPiVersion(this.version).then((newVersion) => {
466
492
  if (newVersion) {
467
493
  this.showNewVersionNotification(newVersion);
468
494
  }
@@ -525,29 +551,6 @@ export class InteractiveMode {
525
551
  }
526
552
  }
527
553
  }
528
- /**
529
- * Check npm registry for a newer version.
530
- */
531
- async checkForNewVersion() {
532
- if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE)
533
- return undefined;
534
- try {
535
- const response = await fetch("https://registry.npmjs.org/@dyyz1993/pi-coding-agent/latest", {
536
- signal: AbortSignal.timeout(10000),
537
- });
538
- if (!response.ok)
539
- return undefined;
540
- const data = (await response.json());
541
- const latestVersion = data.version;
542
- if (latestVersion && latestVersion !== this.version) {
543
- return latestVersion;
544
- }
545
- return undefined;
546
- }
547
- catch {
548
- return undefined;
549
- }
550
- }
551
554
  async checkForPackageUpdates() {
552
555
  if (process.env.PI_OFFLINE) {
553
556
  return [];
@@ -639,7 +642,10 @@ export class InteractiveMode {
639
642
  if (!isInstallTelemetryEnabled(this.settingsManager)) {
640
643
  return;
641
644
  }
642
- void fetch(`https://pi.dev/install?version=${encodeURIComponent(version)}`, {
645
+ void fetch(`https://pi.dev/api/report-install?version=${encodeURIComponent(version)}`, {
646
+ headers: {
647
+ "User-Agent": getPiUserAgent(version),
648
+ },
643
649
  signal: AbortSignal.timeout(5000),
644
650
  })
645
651
  .then(() => undefined)
@@ -663,16 +669,17 @@ export class InteractiveMode {
663
669
  }
664
670
  return result;
665
671
  }
672
+ formatExtensionDisplayPath(path) {
673
+ let result = this.formatDisplayPath(path);
674
+ result = result.replace(/\/index\.ts$/, "").replace(/\/index\.js$/, "");
675
+ return result;
676
+ }
666
677
  formatContextPath(p) {
667
678
  const cwd = path.resolve(this.sessionManager.getCwd());
668
679
  const absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p);
669
- const relativePath = path.relative(cwd, absolutePath);
670
- const isInsideCwd = relativePath === "" ||
671
- (!relativePath.startsWith("..") &&
672
- !relativePath.startsWith(`..${path.sep}`) &&
673
- !path.isAbsolute(relativePath));
674
- if (isInsideCwd) {
675
- return relativePath || ".";
680
+ const relativePath = getCwdRelativePath(absolutePath, cwd);
681
+ if (relativePath !== undefined) {
682
+ return relativePath;
676
683
  }
677
684
  return this.formatDisplayPath(absolutePath);
678
685
  }
@@ -768,11 +775,18 @@ export class InteractiveMode {
768
775
  }
769
776
  getCompactExtensionLabels(extensions) {
770
777
  const nonPackageExtensions = extensions
771
- .map((extension) => ({
772
- path: extension.path,
773
- sourceInfo: extension.sourceInfo,
774
- segments: this.getCompactDisplayPathSegments(extension.path),
775
- }))
778
+ .map((extension) => {
779
+ const segments = this.getCompactDisplayPathSegments(extension.path);
780
+ const lastSegment = segments[segments.length - 1];
781
+ if (segments.length > 1 && (lastSegment === "index.ts" || lastSegment === "index.js")) {
782
+ segments.pop();
783
+ }
784
+ return {
785
+ path: extension.path,
786
+ sourceInfo: extension.sourceInfo,
787
+ segments,
788
+ };
789
+ })
776
790
  .filter((extension) => !this.isPackageSource(extension.sourceInfo));
777
791
  return extensions.map((extension) => {
778
792
  if (this.isPackageSource(extension.sourceInfo)) {
@@ -1011,8 +1025,8 @@ export class InteractiveMode {
1011
1025
  if (extensions.length > 0) {
1012
1026
  const groups = this.buildScopeGroups(extensions);
1013
1027
  const extList = this.formatScopeGroups(groups, {
1014
- formatPath: (item) => this.formatDisplayPath(item.path),
1015
- formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
1028
+ formatPath: (item) => this.formatExtensionDisplayPath(item.path),
1029
+ formatPackagePath: (item) => this.formatExtensionDisplayPath(this.getShortPath(item.path, item.sourceInfo)),
1016
1030
  });
1017
1031
  const extensionCompactList = formatCompactList(this.getCompactExtensionLabels(extensions));
1018
1032
  addLoadedSection("Extensions", extensionCompactList, extList, "mdHeading");
@@ -1152,7 +1166,7 @@ export class InteractiveMode {
1152
1166
  this.setupAutocompleteProvider();
1153
1167
  const extensionRunner = this.session.extensionRunner;
1154
1168
  this.setupExtensionShortcuts(extensionRunner);
1155
- this.showLoadedResources({ force: false });
1169
+ this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1156
1170
  this.showStartupNoticesIfNeeded();
1157
1171
  }
1158
1172
  applyRuntimeSettings() {
@@ -1172,7 +1186,6 @@ export class InteractiveMode {
1172
1186
  }
1173
1187
  }
1174
1188
  async rebindCurrentSession() {
1175
- this.resetExtensionUI();
1176
1189
  this.unsubscribe?.();
1177
1190
  this.unsubscribe = undefined;
1178
1191
  this.applyRuntimeSettings();
@@ -1212,22 +1225,15 @@ export class InteractiveMode {
1212
1225
  if (shortcuts.size === 0)
1213
1226
  return;
1214
1227
  // Create a context for shortcut handlers
1215
- const createContext = (extName = "unknown") => ({
1228
+ const createContext = () => ({
1216
1229
  ui: this.createExtensionUIContext(),
1217
1230
  hasUI: true,
1218
1231
  cwd: this.sessionManager.getCwd(),
1219
- extensionName: extName,
1220
- projectRoot: resolveProjectRoot(this.sessionManager.getCwd()),
1221
- sessionDataDir: getSessionDataDir(this.sessionManager.getSessionDir(), this.sessionManager.getSessionId(), extName),
1222
- projectDataDir: getProjectDataDir(resolveProjectRoot(this.sessionManager.getCwd()), extName),
1223
- cwdDataDir: getCwdDataDir(this.sessionManager.getCwd(), extName),
1224
- globalDataDir: getGlobalDataDir(extName),
1225
1232
  sessionManager: this.sessionManager,
1226
1233
  modelRegistry: this.session.modelRegistry,
1227
1234
  model: this.session.model,
1228
1235
  isIdle: () => !this.session.isStreaming,
1229
1236
  signal: this.session.agent.signal,
1230
- sessionSignal: this.session.sessionSignal,
1231
1237
  abort: () => this.session.abort(),
1232
1238
  hasPendingMessages: () => this.session.pendingMessageCount > 0,
1233
1239
  shutdown: () => {
@@ -1247,16 +1253,23 @@ export class InteractiveMode {
1247
1253
  })();
1248
1254
  },
1249
1255
  getSystemPrompt: () => this.session.systemPrompt,
1250
- respondUI: (id, result) => extensionRunner.respondUI(id, result),
1256
+ extensionName: "",
1257
+ projectRoot: this.sessionManager.getCwd(),
1258
+ sessionDataDir: "",
1259
+ projectDataDir: "",
1260
+ cwdDataDir: "",
1261
+ globalDataDir: "",
1262
+ sessionSignal: AbortSignal.abort(),
1263
+ respondUI: () => { },
1264
+ fileSnapshotManager: this.session.fileSnapshotManager,
1251
1265
  });
1252
1266
  // Set up the extension shortcut handler on the default editor
1253
1267
  this.defaultEditor.onExtensionShortcut = (data) => {
1254
1268
  for (const [shortcutStr, shortcut] of shortcuts) {
1255
1269
  // Cast to KeyId - extension shortcuts use the same format
1256
1270
  if (matchesKey(data, shortcutStr)) {
1257
- const extName = extensionRunner.getExtensionNameByPath(shortcut.extensionPath) ?? "unknown";
1258
1271
  // Run handler async, don't block input
1259
- Promise.resolve(shortcut.handler(createContext(extName))).catch((err) => {
1272
+ Promise.resolve(shortcut.handler(createContext())).catch((err) => {
1260
1273
  this.showError(`Shortcut handler error: ${err instanceof Error ? err.message : String(err)}`);
1261
1274
  });
1262
1275
  return true;
@@ -1272,6 +1285,33 @@ export class InteractiveMode {
1272
1285
  this.footerDataProvider.setExtensionStatus(key, text);
1273
1286
  this.ui.requestRender();
1274
1287
  }
1288
+ getWorkingLoaderMessage() {
1289
+ return this.workingMessage ?? this.defaultWorkingMessage;
1290
+ }
1291
+ createWorkingLoader() {
1292
+ return new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.getWorkingLoaderMessage(), this.workingIndicatorOptions);
1293
+ }
1294
+ stopWorkingLoader() {
1295
+ if (this.loadingAnimation) {
1296
+ this.loadingAnimation.stop();
1297
+ this.loadingAnimation = undefined;
1298
+ }
1299
+ this.statusContainer.clear();
1300
+ }
1301
+ setWorkingVisible(visible) {
1302
+ this.workingVisible = visible;
1303
+ if (!visible) {
1304
+ this.stopWorkingLoader();
1305
+ this.ui.requestRender();
1306
+ return;
1307
+ }
1308
+ if (this.session.isStreaming && !this.loadingAnimation) {
1309
+ this.statusContainer.clear();
1310
+ this.loadingAnimation = this.createWorkingLoader();
1311
+ this.statusContainer.addChild(this.loadingAnimation);
1312
+ }
1313
+ this.ui.requestRender();
1314
+ }
1275
1315
  setWorkingIndicator(options) {
1276
1316
  this.workingIndicatorOptions = options;
1277
1317
  this.loadingAnimation?.setIndicator(options);
@@ -1360,6 +1400,7 @@ export class InteractiveMode {
1360
1400
  this.defaultEditor.onExtensionShortcut = undefined;
1361
1401
  this.updateTerminalTitle();
1362
1402
  this.workingMessage = undefined;
1403
+ this.workingVisible = true;
1363
1404
  this.setWorkingIndicator();
1364
1405
  if (this.loadingAnimation) {
1365
1406
  this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
@@ -1489,14 +1530,10 @@ export class InteractiveMode {
1489
1530
  setWorkingMessage: (message) => {
1490
1531
  this.workingMessage = message;
1491
1532
  if (this.loadingAnimation) {
1492
- if (message) {
1493
- this.loadingAnimation.setMessage(message);
1494
- }
1495
- else {
1496
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1497
- }
1533
+ this.loadingAnimation.setMessage(message ?? this.defaultWorkingMessage);
1498
1534
  }
1499
1535
  },
1536
+ setWorkingVisible: (visible) => this.setWorkingVisible(visible),
1500
1537
  setWorkingIndicator: (options) => this.setWorkingIndicator(options),
1501
1538
  setHiddenThinkingLabel: (label) => this.setHiddenThinkingLabel(label),
1502
1539
  setWidget: (key, content, options) => this.setExtensionWidget(key, content, options),
@@ -1513,6 +1550,7 @@ export class InteractiveMode {
1513
1550
  this.setupAutocompleteProvider();
1514
1551
  },
1515
1552
  setEditorComponent: (factory) => this.setCustomEditorComponent(factory),
1553
+ getEditorComponent: () => this.editorComponentFactory,
1516
1554
  get theme() {
1517
1555
  return theme;
1518
1556
  },
@@ -1559,7 +1597,7 @@ export class InteractiveMode {
1559
1597
  opts?.signal?.removeEventListener("abort", onAbort);
1560
1598
  this.hideExtensionSelector();
1561
1599
  resolve(undefined);
1562
- }, { tui: this.ui, timeout: opts?.timeout, multiple: opts?.multiple });
1600
+ }, { tui: this.ui, timeout: opts?.timeout });
1563
1601
  this.editorContainer.clear();
1564
1602
  this.editorContainer.addChild(this.extensionSelector);
1565
1603
  this.ui.setFocus(this.extensionSelector);
@@ -1661,6 +1699,7 @@ export class InteractiveMode {
1661
1699
  * Pass undefined to restore the default editor.
1662
1700
  */
1663
1701
  setCustomEditorComponent(factory) {
1702
+ this.editorComponentFactory = factory;
1664
1703
  // Save text from current editor before switching
1665
1704
  const currentText = this.editor.getText();
1666
1705
  this.editorContainer.clear();
@@ -1825,7 +1864,7 @@ export class InteractiveMode {
1825
1864
  // Set up handlers on defaultEditor - they use this.editor for text access
1826
1865
  // so they work correctly regardless of which editor is active
1827
1866
  this.defaultEditor.onEscape = () => {
1828
- if (this.loadingAnimation) {
1867
+ if (this.session.isStreaming) {
1829
1868
  this.restoreQueuedMessagesToEditor({ abort: true });
1830
1869
  }
1831
1870
  else if (this.session.isBashRunning) {
@@ -2095,7 +2134,10 @@ export class InteractiveMode {
2095
2134
  this.footer.invalidate();
2096
2135
  switch (event.type) {
2097
2136
  case "agent_start":
2098
- this.ui.terminal.setProgress(true);
2137
+ this.pendingTools.clear();
2138
+ if (this.settingsManager.getShowTerminalProgress()) {
2139
+ this.ui.terminal.setProgress(true);
2140
+ }
2099
2141
  // Restore main escape handler if retry handler is still active
2100
2142
  // (retry success event fires later, but we need main handler now)
2101
2143
  if (this.retryEscapeHandler) {
@@ -2110,18 +2152,26 @@ export class InteractiveMode {
2110
2152
  this.retryLoader.stop();
2111
2153
  this.retryLoader = undefined;
2112
2154
  }
2113
- if (this.loadingAnimation) {
2114
- this.loadingAnimation.stop();
2155
+ this.stopWorkingLoader();
2156
+ if (this.workingVisible) {
2157
+ this.loadingAnimation = this.createWorkingLoader();
2158
+ this.statusContainer.addChild(this.loadingAnimation);
2115
2159
  }
2116
- this.statusContainer.clear();
2117
- this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.workingMessage || this.defaultWorkingMessage, this.workingIndicatorOptions);
2118
- this.statusContainer.addChild(this.loadingAnimation);
2119
2160
  this.ui.requestRender();
2120
2161
  break;
2121
2162
  case "queue_update":
2122
2163
  this.updatePendingMessagesDisplay();
2123
2164
  this.ui.requestRender();
2124
2165
  break;
2166
+ case "session_info_changed":
2167
+ this.updateTerminalTitle();
2168
+ this.footer.invalidate();
2169
+ this.ui.requestRender();
2170
+ break;
2171
+ case "thinking_level_changed":
2172
+ this.footer.invalidate();
2173
+ this.updateEditorBorderColor();
2174
+ break;
2125
2175
  case "message_start":
2126
2176
  if (event.message.role === "custom") {
2127
2177
  this.addMessageToChat(event.message);
@@ -2238,7 +2288,9 @@ export class InteractiveMode {
2238
2288
  break;
2239
2289
  }
2240
2290
  case "agent_end":
2241
- this.ui.terminal.setProgress(false);
2291
+ if (this.settingsManager.getShowTerminalProgress()) {
2292
+ this.ui.terminal.setProgress(false);
2293
+ }
2242
2294
  if (this.loadingAnimation) {
2243
2295
  this.loadingAnimation.stop();
2244
2296
  this.loadingAnimation = undefined;
@@ -2254,7 +2306,9 @@ export class InteractiveMode {
2254
2306
  this.ui.requestRender();
2255
2307
  break;
2256
2308
  case "compaction_start": {
2257
- this.ui.terminal.setProgress(true);
2309
+ if (this.settingsManager.getShowTerminalProgress()) {
2310
+ this.ui.terminal.setProgress(true);
2311
+ }
2258
2312
  // Keep editor active; submissions are queued during compaction.
2259
2313
  this.autoCompactionEscapeHandler = this.defaultEditor.onEscape;
2260
2314
  this.defaultEditor.onEscape = () => {
@@ -2271,7 +2325,9 @@ export class InteractiveMode {
2271
2325
  break;
2272
2326
  }
2273
2327
  case "compaction_end": {
2274
- this.ui.terminal.setProgress(false);
2328
+ if (this.settingsManager.getShowTerminalProgress()) {
2329
+ this.ui.terminal.setProgress(false);
2330
+ }
2275
2331
  if (this.autoCompactionEscapeHandler) {
2276
2332
  this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
2277
2333
  this.autoCompactionEscapeHandler = undefined;
@@ -2419,13 +2475,10 @@ export class InteractiveMode {
2419
2475
  this.chatContainer.addChild(component);
2420
2476
  break;
2421
2477
  }
2422
- case "foldSummary": {
2423
- break;
2424
- }
2425
2478
  case "user": {
2426
2479
  const textContent = this.getUserMessageText(message);
2427
2480
  if (textContent) {
2428
- if (!this.isFirstUserMessage) {
2481
+ if (this.chatContainer.children.length > 0) {
2429
2482
  this.chatContainer.addChild(new Spacer(1));
2430
2483
  }
2431
2484
  const skillBlock = parseSkillBlock(textContent);
@@ -2444,7 +2497,6 @@ export class InteractiveMode {
2444
2497
  const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());
2445
2498
  this.chatContainer.addChild(userComponent);
2446
2499
  }
2447
- this.isFirstUserMessage = false;
2448
2500
  if (options?.populateHistory) {
2449
2501
  this.editor.addToHistory?.(textContent);
2450
2502
  }
@@ -2460,6 +2512,10 @@ export class InteractiveMode {
2460
2512
  // Tool results are rendered inline with tool calls, handled separately
2461
2513
  break;
2462
2514
  }
2515
+ case "foldSummary":
2516
+ case "segmentSummary": {
2517
+ break;
2518
+ }
2463
2519
  default: {
2464
2520
  const _exhaustive = message;
2465
2521
  }
@@ -2473,7 +2529,7 @@ export class InteractiveMode {
2473
2529
  */
2474
2530
  renderSessionContext(sessionContext, options = {}) {
2475
2531
  this.pendingTools.clear();
2476
- this.isFirstUserMessage = true;
2532
+ const renderedPendingTools = new Map();
2477
2533
  if (options.updateFooter) {
2478
2534
  this.footer.invalidate();
2479
2535
  this.updateEditorBorderColor();
@@ -2506,17 +2562,17 @@ export class InteractiveMode {
2506
2562
  component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
2507
2563
  }
2508
2564
  else {
2509
- this.pendingTools.set(content.id, component);
2565
+ renderedPendingTools.set(content.id, component);
2510
2566
  }
2511
2567
  }
2512
2568
  }
2513
2569
  }
2514
2570
  else if (message.role === "toolResult") {
2515
2571
  // Match tool results to pending tool components
2516
- const component = this.pendingTools.get(message.toolCallId);
2572
+ const component = renderedPendingTools.get(message.toolCallId);
2517
2573
  if (component) {
2518
2574
  component.updateResult(message);
2519
- this.pendingTools.delete(message.toolCallId);
2575
+ renderedPendingTools.delete(message.toolCallId);
2520
2576
  }
2521
2577
  }
2522
2578
  else {
@@ -2524,7 +2580,9 @@ export class InteractiveMode {
2524
2580
  this.addMessageToChat(message, options);
2525
2581
  }
2526
2582
  }
2527
- this.pendingTools.clear();
2583
+ for (const [toolCallId, component] of renderedPendingTools) {
2584
+ this.pendingTools.set(toolCallId, component);
2585
+ }
2528
2586
  this.ui.requestRender();
2529
2587
  }
2530
2588
  renderInitialMessages() {
@@ -2574,7 +2632,8 @@ export class InteractiveMode {
2574
2632
  }
2575
2633
  /**
2576
2634
  * Gracefully shutdown the agent.
2577
- * Emits shutdown event to extensions, then exits.
2635
+ * Stops the TUI before emitting shutdown events so extension UI cleanup cannot
2636
+ * repaint the final frame while the process is exiting.
2578
2637
  */
2579
2638
  isShuttingDown = false;
2580
2639
  async shutdown() {
@@ -2582,16 +2641,21 @@ export class InteractiveMode {
2582
2641
  return;
2583
2642
  this.isShuttingDown = true;
2584
2643
  this.unregisterSignalHandlers();
2585
- await this.runtimeHost.dispose();
2586
- // Wait for any pending renders to complete
2587
- // requestRender() uses process.nextTick(), so we wait one tick
2588
- await new Promise((resolve) => process.nextTick(resolve));
2589
2644
  // Drain any in-flight Kitty key release events before stopping.
2590
2645
  // This prevents escape sequences from leaking to the parent shell over slow SSH.
2591
2646
  await this.ui.terminal.drainInput(1000);
2592
2647
  this.stop();
2648
+ await this.runtimeHost.dispose();
2593
2649
  process.exit(0);
2594
2650
  }
2651
+ emergencyTerminalExit() {
2652
+ this.isShuttingDown = true;
2653
+ this.unregisterSignalHandlers();
2654
+ killTrackedDetachedChildren();
2655
+ // The terminal is gone. Do not run normal shutdown because TUI and
2656
+ // extension cleanup can write restore sequences and re-trigger EIO.
2657
+ process.exit(129);
2658
+ }
2595
2659
  /**
2596
2660
  * Check if shutdown was requested and perform shutdown if so.
2597
2661
  */
@@ -2608,12 +2672,25 @@ export class InteractiveMode {
2608
2672
  }
2609
2673
  for (const signal of signals) {
2610
2674
  const handler = () => {
2675
+ if (signal === "SIGHUP") {
2676
+ this.emergencyTerminalExit();
2677
+ }
2611
2678
  killTrackedDetachedChildren();
2612
2679
  void this.shutdown();
2613
2680
  };
2614
- process.on(signal, handler);
2681
+ process.prependListener(signal, handler);
2615
2682
  this.signalCleanupHandlers.push(() => process.off(signal, handler));
2616
2683
  }
2684
+ const terminalErrorHandler = (error) => {
2685
+ if (isDeadTerminalError(error)) {
2686
+ this.emergencyTerminalExit();
2687
+ }
2688
+ throw error;
2689
+ };
2690
+ process.stdout.on("error", terminalErrorHandler);
2691
+ process.stderr.on("error", terminalErrorHandler);
2692
+ this.signalCleanupHandlers.push(() => process.stdout.off("error", terminalErrorHandler));
2693
+ this.signalCleanupHandlers.push(() => process.stderr.off("error", terminalErrorHandler));
2617
2694
  }
2618
2695
  unregisterSignalHandlers() {
2619
2696
  for (const cleanup of this.signalCleanupHandlers) {
@@ -2680,6 +2757,7 @@ export class InteractiveMode {
2680
2757
  }
2681
2758
  // If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
2682
2759
  else if (this.editor.onSubmit) {
2760
+ this.editor.setText("");
2683
2761
  this.editor.onSubmit(text);
2684
2762
  }
2685
2763
  }
@@ -2822,9 +2900,9 @@ export class InteractiveMode {
2822
2900
  this.ui.requestRender();
2823
2901
  }
2824
2902
  showNewVersionNotification(newVersion) {
2825
- const action = theme.fg("accent", getUpdateInstruction("@dyyz1993/pi-coding-agent"));
2826
- const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action;
2827
- const changelogUrl = theme.fg("accent", "https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md");
2903
+ const action = theme.fg("accent", `${APP_NAME} update`);
2904
+ const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. Run `) + action;
2905
+ const changelogUrl = theme.fg("accent", "https://github.com/dyyz1993/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md");
2828
2906
  const changelogLine = theme.fg("muted", "Changelog: ") + changelogUrl;
2829
2907
  this.chatContainer.addChild(new Spacer(1));
2830
2908
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
@@ -3051,6 +3129,8 @@ export class InteractiveMode {
3051
3129
  autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
3052
3130
  quietStartup: this.settingsManager.getQuietStartup(),
3053
3131
  clearOnShrink: this.settingsManager.getClearOnShrink(),
3132
+ showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
3133
+ warnings: this.settingsManager.getWarnings(),
3054
3134
  }, {
3055
3135
  onAutoCompactChange: (enabled) => {
3056
3136
  this.session.setAutoCompactionEnabled(enabled);
@@ -3160,6 +3240,12 @@ export class InteractiveMode {
3160
3240
  this.settingsManager.setClearOnShrink(enabled);
3161
3241
  this.ui.setClearOnShrink(enabled);
3162
3242
  },
3243
+ onShowTerminalProgressChange: (enabled) => {
3244
+ this.settingsManager.setShowTerminalProgress(enabled);
3245
+ },
3246
+ onWarningsChange: (warnings) => {
3247
+ this.settingsManager.setWarnings(warnings);
3248
+ },
3163
3249
  onCancel: () => {
3164
3250
  done();
3165
3251
  this.ui.requestRender();
@@ -3191,19 +3277,6 @@ export class InteractiveMode {
3191
3277
  this.showModelSelector(searchTerm);
3192
3278
  }
3193
3279
  async findExactModelMatch(searchTerm) {
3194
- const tierModels = this.settingsManager.getTierModels();
3195
- const aliasTarget = resolveModelAlias(searchTerm, tierModels);
3196
- if (aliasTarget) {
3197
- const slashIndex = aliasTarget.indexOf("/");
3198
- if (slashIndex !== -1) {
3199
- const provider = aliasTarget.substring(0, slashIndex);
3200
- const modelId = aliasTarget.substring(slashIndex + 1);
3201
- const model = this.session.modelRegistry.find(provider, modelId);
3202
- if (model && this.session.modelRegistry.hasConfiguredAuth(model)) {
3203
- return model;
3204
- }
3205
- }
3206
- }
3207
3280
  const models = await this.getModelCandidates();
3208
3281
  return findExactModelReferenceMatch(searchTerm, models);
3209
3282
  }
@@ -3226,6 +3299,9 @@ export class InteractiveMode {
3226
3299
  this.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);
3227
3300
  }
3228
3301
  async maybeWarnAboutAnthropicSubscriptionAuth(model = this.session.model) {
3302
+ if (this.settingsManager.getWarnings().anthropicExtraUsage === false) {
3303
+ return;
3304
+ }
3229
3305
  if (this.anthropicSubscriptionWarningShown) {
3230
3306
  return;
3231
3307
  }
@@ -3555,54 +3631,259 @@ export class InteractiveMode {
3555
3631
  return this.handleFatalRuntimeError("Failed to resume session", error);
3556
3632
  }
3557
3633
  }
3558
- async showOAuthSelector(mode) {
3559
- if (mode === "logout") {
3560
- const providers = this.session.modelRegistry.authStorage.list();
3561
- const loggedInProviders = providers.filter((p) => this.session.modelRegistry.authStorage.get(p)?.type === "oauth");
3562
- if (loggedInProviders.length === 0) {
3563
- this.showStatus("No OAuth providers logged in. Use /login first.");
3564
- return;
3634
+ getLoginProviderOptions(authType) {
3635
+ const authStorage = this.session.modelRegistry.authStorage;
3636
+ const oauthProviders = authStorage.getOAuthProviders();
3637
+ const oauthProviderIds = new Set(oauthProviders.map((provider) => provider.id));
3638
+ const options = oauthProviders.map((provider) => ({
3639
+ id: provider.id,
3640
+ name: provider.name,
3641
+ authType: "oauth",
3642
+ }));
3643
+ const modelProviders = new Set(this.session.modelRegistry.getAll().map((model) => model.provider));
3644
+ for (const providerId of modelProviders) {
3645
+ if (!isApiKeyLoginProvider(providerId, oauthProviderIds)) {
3646
+ continue;
3647
+ }
3648
+ options.push({
3649
+ id: providerId,
3650
+ name: this.session.modelRegistry.getProviderDisplayName(providerId),
3651
+ authType: "api_key",
3652
+ });
3653
+ }
3654
+ const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
3655
+ return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
3656
+ }
3657
+ getLogoutProviderOptions() {
3658
+ const authStorage = this.session.modelRegistry.authStorage;
3659
+ const options = [];
3660
+ for (const providerId of authStorage.list()) {
3661
+ const credential = authStorage.get(providerId);
3662
+ if (!credential) {
3663
+ continue;
3565
3664
  }
3665
+ options.push({
3666
+ id: providerId,
3667
+ name: this.session.modelRegistry.getProviderDisplayName(providerId),
3668
+ authType: credential.type,
3669
+ });
3670
+ }
3671
+ return options.sort((a, b) => a.name.localeCompare(b.name));
3672
+ }
3673
+ showLoginAuthTypeSelector() {
3674
+ const subscriptionLabel = "Use a subscription";
3675
+ const apiKeyLabel = "Use an API key";
3676
+ this.showSelector((done) => {
3677
+ const selector = new ExtensionSelectorComponent("Select authentication method:", [subscriptionLabel, apiKeyLabel], (option) => {
3678
+ done();
3679
+ const authType = option === subscriptionLabel ? "oauth" : "api_key";
3680
+ this.showLoginProviderSelector(authType);
3681
+ }, () => {
3682
+ done();
3683
+ this.ui.requestRender();
3684
+ });
3685
+ return { component: selector, focus: selector };
3686
+ });
3687
+ }
3688
+ showLoginProviderSelector(authType) {
3689
+ const providerOptions = this.getLoginProviderOptions(authType);
3690
+ if (providerOptions.length === 0) {
3691
+ this.showStatus(authType === "oauth" ? "No subscription providers available." : "No API key providers available.");
3692
+ return;
3566
3693
  }
3567
3694
  this.showSelector((done) => {
3568
- const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, async (providerId) => {
3695
+ const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
3569
3696
  done();
3570
- if (mode === "login") {
3571
- await this.showLoginDialog(providerId);
3697
+ const providerOption = providerOptions.find((provider) => provider.id === providerId);
3698
+ if (!providerOption) {
3699
+ return;
3700
+ }
3701
+ if (providerOption.authType === "oauth") {
3702
+ await this.showLoginDialog(providerOption.id, providerOption.name);
3703
+ }
3704
+ else if (providerOption.id === BEDROCK_PROVIDER_ID) {
3705
+ this.showBedrockSetupDialog(providerOption.id, providerOption.name);
3706
+ }
3707
+ else {
3708
+ await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
3709
+ }
3710
+ }, () => {
3711
+ done();
3712
+ this.showLoginAuthTypeSelector();
3713
+ }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId));
3714
+ return { component: selector, focus: selector };
3715
+ });
3716
+ }
3717
+ async showOAuthSelector(mode) {
3718
+ if (mode === "login") {
3719
+ this.showLoginAuthTypeSelector();
3720
+ return;
3721
+ }
3722
+ const providerOptions = this.getLogoutProviderOptions();
3723
+ if (providerOptions.length === 0) {
3724
+ this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
3725
+ return;
3726
+ }
3727
+ this.showSelector((done) => {
3728
+ const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
3729
+ done();
3730
+ const providerOption = providerOptions.find((provider) => provider.id === providerId);
3731
+ if (!providerOption) {
3732
+ return;
3733
+ }
3734
+ try {
3735
+ this.session.modelRegistry.authStorage.logout(providerOption.id);
3736
+ this.session.modelRegistry.refresh();
3737
+ await this.updateAvailableProviderCount();
3738
+ const message = providerOption.authType === "oauth"
3739
+ ? `Logged out of ${providerOption.name}`
3740
+ : `Removed stored API key for ${providerOption.name}. Environment variables and models.json config are unchanged.`;
3741
+ this.showStatus(message);
3742
+ }
3743
+ catch (error) {
3744
+ this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
3745
+ }
3746
+ }, () => {
3747
+ done();
3748
+ this.ui.requestRender();
3749
+ });
3750
+ return { component: selector, focus: selector };
3751
+ });
3752
+ }
3753
+ async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
3754
+ this.session.modelRegistry.refresh();
3755
+ const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
3756
+ let selectedModel;
3757
+ let selectionError;
3758
+ if (isUnknownModel(previousModel)) {
3759
+ const availableModels = this.session.modelRegistry.getAvailable();
3760
+ const providerModels = availableModels.filter((model) => model.provider === providerId);
3761
+ if (!hasDefaultModelProvider(providerId)) {
3762
+ selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
3763
+ }
3764
+ else if (providerModels.length === 0) {
3765
+ selectionError = `${actionLabel}, but no models are available for that provider. Use /model to select a model.`;
3766
+ }
3767
+ else {
3768
+ const defaultModelId = defaultModelPerProvider[providerId];
3769
+ selectedModel = providerModels.find((model) => model.id === defaultModelId);
3770
+ if (!selectedModel) {
3771
+ selectionError = `${actionLabel}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
3572
3772
  }
3573
3773
  else {
3574
- // Logout flow
3575
- const providerInfo = this.session.modelRegistry.authStorage
3576
- .getOAuthProviders()
3577
- .find((p) => p.id === providerId);
3578
- const providerName = providerInfo?.name || providerId;
3579
3774
  try {
3580
- this.session.modelRegistry.authStorage.logout(providerId);
3581
- this.session.modelRegistry.refresh();
3582
- await this.updateAvailableProviderCount();
3583
- this.showStatus(`Logged out of ${providerName}`);
3775
+ await this.session.setModel(selectedModel);
3584
3776
  }
3585
3777
  catch (error) {
3586
- this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
3778
+ selectedModel = undefined;
3779
+ const errorMessage = error instanceof Error ? error.message : String(error);
3780
+ selectionError = `${actionLabel}, but selecting its default model failed: ${errorMessage}. Use /model to select a model.`;
3587
3781
  }
3588
3782
  }
3589
- }, () => {
3590
- done();
3783
+ }
3784
+ }
3785
+ await this.updateAvailableProviderCount();
3786
+ this.footer.invalidate();
3787
+ this.updateEditorBorderColor();
3788
+ if (selectedModel) {
3789
+ this.showStatus(`${actionLabel}. Selected ${selectedModel.id}. Credentials saved to ${getAuthPath()}`);
3790
+ void this.maybeWarnAboutAnthropicSubscriptionAuth(selectedModel);
3791
+ this.checkDaxnutsEasterEgg(selectedModel);
3792
+ }
3793
+ else {
3794
+ this.showStatus(`${actionLabel}. Credentials saved to ${getAuthPath()}`);
3795
+ if (selectionError) {
3796
+ this.showError(selectionError);
3797
+ }
3798
+ else {
3799
+ void this.maybeWarnAboutAnthropicSubscriptionAuth();
3800
+ }
3801
+ }
3802
+ }
3803
+ showBedrockSetupDialog(providerId, providerName) {
3804
+ const restoreEditor = () => {
3805
+ this.editorContainer.clear();
3806
+ this.editorContainer.addChild(this.editor);
3807
+ this.ui.setFocus(this.editor);
3808
+ this.ui.requestRender();
3809
+ };
3810
+ const dialog = new LoginDialogComponent(this.ui, providerId, () => restoreEditor(), providerName, "Amazon Bedrock setup");
3811
+ dialog.showInfo([
3812
+ theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."),
3813
+ theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."),
3814
+ theme.fg("muted", "See:"),
3815
+ theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
3816
+ ]);
3817
+ this.editorContainer.clear();
3818
+ this.editorContainer.addChild(dialog);
3819
+ this.ui.setFocus(dialog);
3820
+ this.ui.requestRender();
3821
+ }
3822
+ async showApiKeyLoginDialog(providerId, providerName) {
3823
+ const previousModel = this.session.model;
3824
+ const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
3825
+ // Completion handled below
3826
+ }, providerName);
3827
+ this.editorContainer.clear();
3828
+ this.editorContainer.addChild(dialog);
3829
+ this.ui.setFocus(dialog);
3830
+ this.ui.requestRender();
3831
+ const restoreEditor = () => {
3832
+ this.editorContainer.clear();
3833
+ this.editorContainer.addChild(this.editor);
3834
+ this.ui.setFocus(this.editor);
3835
+ this.ui.requestRender();
3836
+ };
3837
+ try {
3838
+ const apiKey = (await dialog.showPrompt("Enter API key:")).trim();
3839
+ if (!apiKey) {
3840
+ throw new Error("API key cannot be empty.");
3841
+ }
3842
+ this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
3843
+ restoreEditor();
3844
+ await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
3845
+ }
3846
+ catch (error) {
3847
+ restoreEditor();
3848
+ const errorMsg = error instanceof Error ? error.message : String(error);
3849
+ if (errorMsg !== "Login cancelled") {
3850
+ this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
3851
+ }
3852
+ }
3853
+ }
3854
+ showOAuthLoginSelect(dialog, prompt) {
3855
+ return new Promise((resolve) => {
3856
+ const restoreDialog = () => {
3857
+ this.editorContainer.clear();
3858
+ this.editorContainer.addChild(dialog);
3859
+ this.ui.setFocus(dialog);
3591
3860
  this.ui.requestRender();
3861
+ };
3862
+ const labels = prompt.options.map((option) => option.label);
3863
+ const selector = new ExtensionSelectorComponent(prompt.message, labels, (optionLabel) => {
3864
+ restoreDialog();
3865
+ resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
3866
+ }, () => {
3867
+ restoreDialog();
3868
+ resolve(undefined);
3592
3869
  });
3593
- return { component: selector, focus: selector };
3870
+ this.editorContainer.clear();
3871
+ this.editorContainer.addChild(selector);
3872
+ this.ui.setFocus(selector);
3873
+ this.ui.requestRender();
3594
3874
  });
3595
3875
  }
3596
- async showLoginDialog(providerId) {
3597
- const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
3598
- const providerName = providerInfo?.name || providerId;
3876
+ async showLoginDialog(providerId, providerName) {
3877
+ const providerInfo = this.session.modelRegistry.authStorage
3878
+ .getOAuthProviders()
3879
+ .find((provider) => provider.id === providerId);
3599
3880
  const previousModel = this.session.model;
3600
3881
  // Providers that use callback servers (can paste redirect URL)
3601
3882
  const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
3602
3883
  // Create login dialog component
3603
3884
  const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
3604
3885
  // Completion handled below
3605
- });
3886
+ }, providerName);
3606
3887
  // Show dialog in editor container
3607
3888
  this.editorContainer.clear();
3608
3889
  this.editorContainer.addChild(dialog);
@@ -3655,58 +3936,13 @@ export class InteractiveMode {
3655
3936
  onProgress: (message) => {
3656
3937
  dialog.showProgress(message);
3657
3938
  },
3939
+ onSelect: (prompt) => this.showOAuthLoginSelect(dialog, prompt),
3658
3940
  onManualCodeInput: () => manualCodePromise,
3659
3941
  signal: dialog.signal,
3660
3942
  });
3661
3943
  // Success
3662
3944
  restoreEditor();
3663
- this.session.modelRegistry.refresh();
3664
- let selectedModel;
3665
- let selectionError;
3666
- if (isUnknownModel(previousModel)) {
3667
- const availableModels = this.session.modelRegistry.getAvailable();
3668
- const providerModels = availableModels.filter((model) => model.provider === providerId);
3669
- if (!hasDefaultModelProvider(providerId)) {
3670
- selectionError = `Logged in to ${providerName}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
3671
- }
3672
- else if (providerModels.length === 0) {
3673
- selectionError = `Logged in to ${providerName}, but no models are available for that provider. Use /model to select a model.`;
3674
- }
3675
- else {
3676
- const defaultModelId = defaultModelPerProvider[providerId];
3677
- selectedModel = providerModels.find((model) => model.id === defaultModelId);
3678
- if (!selectedModel) {
3679
- selectionError = `Logged in to ${providerName}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
3680
- }
3681
- else {
3682
- try {
3683
- await this.session.setModel(selectedModel);
3684
- }
3685
- catch (error) {
3686
- selectedModel = undefined;
3687
- const errorMessage = error instanceof Error ? error.message : String(error);
3688
- selectionError = `Logged in to ${providerName}, but selecting its default model failed: ${errorMessage}. Use /model to select a model.`;
3689
- }
3690
- }
3691
- }
3692
- }
3693
- await this.updateAvailableProviderCount();
3694
- this.footer.invalidate();
3695
- this.updateEditorBorderColor();
3696
- if (selectedModel) {
3697
- this.showStatus(`Logged in to ${providerName}. Selected ${selectedModel.id}. Credentials saved to ${getAuthPath()}`);
3698
- void this.maybeWarnAboutAnthropicSubscriptionAuth(selectedModel);
3699
- this.checkDaxnutsEasterEgg(selectedModel);
3700
- }
3701
- else {
3702
- this.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`);
3703
- if (selectionError) {
3704
- this.showError(selectionError);
3705
- }
3706
- else {
3707
- void this.maybeWarnAboutAnthropicSubscriptionAuth();
3708
- }
3709
- }
3945
+ await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
3710
3946
  }
3711
3947
  catch (error) {
3712
3948
  restoreEditor();
@@ -3996,8 +4232,7 @@ export class InteractiveMode {
3996
4232
  this.ui.requestRender();
3997
4233
  return;
3998
4234
  }
3999
- this.sessionManager.appendSessionInfo(name);
4000
- this.updateTerminalTitle();
4235
+ this.session.setSessionName(name);
4001
4236
  this.chatContainer.addChild(new Spacer(1));
4002
4237
  this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
4003
4238
  this.ui.requestRender();
@@ -4338,7 +4573,9 @@ export class InteractiveMode {
4338
4573
  }
4339
4574
  stop() {
4340
4575
  this.unregisterSignalHandlers();
4341
- this.ui.terminal.setProgress(false);
4576
+ if (this.settingsManager.getShowTerminalProgress()) {
4577
+ this.ui.terminal.setProgress(false);
4578
+ }
4342
4579
  if (this.loadingAnimation) {
4343
4580
  this.loadingAnimation.stop();
4344
4581
  this.loadingAnimation = undefined;