@oh-my-pi/pi-coding-agent 15.12.2 → 15.12.4

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 (231) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/dist/cli.js +1121 -871
  3. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  4. package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
  5. package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
  6. package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
  7. package/dist/types/cli/args.d.ts +0 -1
  8. package/dist/types/cli/models-cli.d.ts +49 -0
  9. package/dist/types/commands/launch.d.ts +0 -3
  10. package/dist/types/commands/models.d.ts +33 -0
  11. package/dist/types/commands/token.d.ts +25 -0
  12. package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
  13. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
  14. package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
  15. package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
  16. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
  17. package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
  18. package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
  19. package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
  20. package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
  21. package/dist/types/commit/changelog/generate.d.ts +1 -1
  22. package/dist/types/commit/shared-llm.d.ts +1 -1
  23. package/dist/types/config/model-registry.d.ts +7 -0
  24. package/dist/types/config/models-config-schema.d.ts +1 -1
  25. package/dist/types/config/settings-schema.d.ts +21 -1
  26. package/dist/types/edit/hashline/params.d.ts +1 -1
  27. package/dist/types/edit/modes/apply-patch.d.ts +1 -1
  28. package/dist/types/edit/modes/patch.d.ts +1 -1
  29. package/dist/types/edit/modes/replace.d.ts +1 -1
  30. package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
  31. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  32. package/dist/types/extensibility/extensions/types.d.ts +2 -2
  33. package/dist/types/extensibility/hooks/types.d.ts +2 -2
  34. package/dist/types/goals/tools/goal-tool.d.ts +1 -1
  35. package/dist/types/lsp/types.d.ts +1 -1
  36. package/dist/types/mcp/manager.d.ts +8 -0
  37. package/dist/types/mnemopi/config.d.ts +28 -0
  38. package/dist/types/modes/acp/acp-agent.d.ts +1 -2
  39. package/dist/types/modes/components/index.d.ts +1 -0
  40. package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
  41. package/dist/types/modes/components/status-line/component.d.ts +9 -5
  42. package/dist/types/modes/components/status-line/types.d.ts +2 -1
  43. package/dist/types/modes/controllers/event-controller.d.ts +0 -17
  44. package/dist/types/modes/interactive-mode.d.ts +0 -3
  45. package/dist/types/modes/types.d.ts +0 -5
  46. package/dist/types/session/agent-session.d.ts +14 -33
  47. package/dist/types/session/agent-storage.d.ts +2 -1
  48. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  49. package/dist/types/session/messages.d.ts +8 -10
  50. package/dist/types/session/session-manager.d.ts +15 -0
  51. package/dist/types/session/session-storage.d.ts +5 -0
  52. package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
  53. package/dist/types/task/types.d.ts +1 -1
  54. package/dist/types/tools/ask.d.ts +1 -1
  55. package/dist/types/tools/ast-edit.d.ts +1 -1
  56. package/dist/types/tools/ast-grep.d.ts +1 -1
  57. package/dist/types/tools/bash.d.ts +1 -1
  58. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
  59. package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
  60. package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
  61. package/dist/types/tools/browser/registry.d.ts +16 -3
  62. package/dist/types/tools/browser/render.d.ts +2 -0
  63. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  64. package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
  65. package/dist/types/tools/browser.d.ts +3 -1
  66. package/dist/types/tools/checkpoint.d.ts +1 -1
  67. package/dist/types/tools/debug.d.ts +1 -1
  68. package/dist/types/tools/eval.d.ts +1 -1
  69. package/dist/types/tools/find.d.ts +1 -1
  70. package/dist/types/tools/gh.d.ts +1 -1
  71. package/dist/types/tools/image-gen.d.ts +1 -1
  72. package/dist/types/tools/index.d.ts +3 -1
  73. package/dist/types/tools/inspect-image.d.ts +1 -1
  74. package/dist/types/tools/irc.d.ts +1 -1
  75. package/dist/types/tools/job.d.ts +1 -1
  76. package/dist/types/tools/memory-edit.d.ts +1 -1
  77. package/dist/types/tools/memory-recall.d.ts +1 -1
  78. package/dist/types/tools/memory-reflect.d.ts +1 -1
  79. package/dist/types/tools/memory-retain.d.ts +1 -1
  80. package/dist/types/tools/read.d.ts +1 -1
  81. package/dist/types/tools/render-mermaid.d.ts +1 -1
  82. package/dist/types/tools/resolve.d.ts +1 -1
  83. package/dist/types/tools/review.d.ts +1 -1
  84. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  85. package/dist/types/tools/search.d.ts +1 -1
  86. package/dist/types/tools/ssh.d.ts +1 -1
  87. package/dist/types/tools/todo.d.ts +1 -1
  88. package/dist/types/tools/tts.d.ts +1 -1
  89. package/dist/types/tools/write.d.ts +1 -1
  90. package/dist/types/utils/clipboard.d.ts +4 -3
  91. package/dist/types/utils/image-loading.d.ts +18 -1
  92. package/dist/types/utils/thinking-display.d.ts +17 -0
  93. package/dist/types/web/search/index.d.ts +1 -1
  94. package/package.json +14 -14
  95. package/src/autoresearch/storage.ts +2 -1
  96. package/src/autoresearch/tools/init-experiment.ts +1 -1
  97. package/src/autoresearch/tools/log-experiment.ts +1 -1
  98. package/src/autoresearch/tools/run-experiment.ts +1 -1
  99. package/src/autoresearch/tools/update-notes.ts +1 -1
  100. package/src/cli/args.ts +0 -8
  101. package/src/cli/auth-gateway-cli.ts +1 -1
  102. package/src/cli/bench-cli.ts +1 -1
  103. package/src/cli/dry-balance-cli.ts +1 -1
  104. package/src/cli/models-cli.ts +427 -0
  105. package/src/cli-commands.ts +2 -0
  106. package/src/collab/host.ts +9 -12
  107. package/src/commands/launch.ts +0 -3
  108. package/src/commands/models.ts +61 -0
  109. package/src/commands/token.ts +89 -0
  110. package/src/commit/agentic/tools/analyze-file.ts +1 -1
  111. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  112. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  113. package/src/commit/agentic/tools/git-overview.ts +1 -1
  114. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  115. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  116. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  117. package/src/commit/agentic/tools/schemas.ts +1 -1
  118. package/src/commit/agentic/tools/split-commit.ts +1 -1
  119. package/src/commit/analysis/summary.ts +1 -1
  120. package/src/commit/changelog/generate.ts +1 -1
  121. package/src/commit/shared-llm.ts +1 -1
  122. package/src/config/model-registry.ts +15 -12
  123. package/src/config/model-resolver.ts +2 -2
  124. package/src/config/models-config-schema.ts +1 -1
  125. package/src/config/settings-schema.ts +19 -1
  126. package/src/edit/hashline/params.ts +1 -1
  127. package/src/edit/modes/apply-patch.ts +1 -1
  128. package/src/edit/modes/patch.ts +1 -1
  129. package/src/edit/modes/replace.ts +1 -1
  130. package/src/eval/agent-bridge.ts +1 -1
  131. package/src/eval/completion-bridge.ts +1 -1
  132. package/src/export/html/template.js +24 -2
  133. package/src/export/html/tool-views.generated.js +2 -2
  134. package/src/extensibility/custom-commands/loader.ts +1 -1
  135. package/src/extensibility/custom-commands/types.ts +2 -2
  136. package/src/extensibility/custom-tools/loader.ts +1 -1
  137. package/src/extensibility/custom-tools/types.ts +2 -2
  138. package/src/extensibility/extensions/loader.ts +2 -2
  139. package/src/extensibility/extensions/types.ts +2 -2
  140. package/src/extensibility/hooks/loader.ts +1 -1
  141. package/src/extensibility/hooks/types.ts +2 -2
  142. package/src/extensibility/skills.ts +18 -3
  143. package/src/goals/tools/goal-tool.ts +1 -1
  144. package/src/internal-urls/docs-index.generated.ts +6 -3
  145. package/src/lsp/types.ts +1 -1
  146. package/src/main.ts +0 -25
  147. package/src/mcp/config-writer.ts +7 -3
  148. package/src/mcp/manager.ts +11 -0
  149. package/src/memories/index.ts +3 -1
  150. package/src/memories/storage.ts +2 -1
  151. package/src/mnemopi/config.ts +95 -11
  152. package/src/modes/acp/acp-agent.ts +5 -48
  153. package/src/modes/acp/acp-event-mapper.ts +5 -1
  154. package/src/modes/components/agent-hub.ts +2 -1
  155. package/src/modes/components/assistant-message.ts +8 -7
  156. package/src/modes/components/index.ts +1 -0
  157. package/src/modes/components/logout-account-selector.ts +130 -0
  158. package/src/modes/components/mcp-add-wizard.ts +1 -1
  159. package/src/modes/components/model-selector.ts +2 -2
  160. package/src/modes/components/status-line/component.ts +54 -157
  161. package/src/modes/components/status-line/segments.ts +1 -1
  162. package/src/modes/components/status-line/types.ts +2 -1
  163. package/src/modes/controllers/command-controller.ts +0 -12
  164. package/src/modes/controllers/event-controller.ts +23 -62
  165. package/src/modes/controllers/input-controller.ts +60 -31
  166. package/src/modes/controllers/mcp-command-controller.ts +44 -3
  167. package/src/modes/controllers/selector-controller.ts +56 -10
  168. package/src/modes/controllers/streaming-reveal.ts +4 -3
  169. package/src/modes/interactive-mode.ts +2 -8
  170. package/src/modes/theme/theme.ts +1 -1
  171. package/src/modes/types.ts +0 -5
  172. package/src/modes/utils/ui-helpers.ts +2 -1
  173. package/src/prompts/system/empty-stop-retry.md +4 -6
  174. package/src/sdk.ts +15 -19
  175. package/src/session/agent-session.ts +125 -234
  176. package/src/session/agent-storage.ts +18 -9
  177. package/src/session/history-storage.ts +2 -1
  178. package/src/session/indexed-session-storage.ts +7 -0
  179. package/src/session/messages.ts +9 -11
  180. package/src/session/session-dump-format.ts +4 -2
  181. package/src/session/session-manager.ts +116 -0
  182. package/src/session/session-storage.ts +20 -0
  183. package/src/slash-commands/builtin-registry.ts +15 -1
  184. package/src/slash-commands/helpers/logout.ts +88 -0
  185. package/src/task/types.ts +1 -1
  186. package/src/tools/ask.ts +1 -1
  187. package/src/tools/ast-edit.ts +13 -4
  188. package/src/tools/ast-grep.ts +1 -1
  189. package/src/tools/bash.ts +1 -1
  190. package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
  191. package/src/tools/browser/cmux/rpc.ts +156 -0
  192. package/src/tools/browser/cmux/socket-client.ts +309 -0
  193. package/src/tools/browser/registry.ts +37 -3
  194. package/src/tools/browser/render.ts +6 -1
  195. package/src/tools/browser/tab-protocol.ts +2 -0
  196. package/src/tools/browser/tab-supervisor.ts +189 -18
  197. package/src/tools/browser/tab-worker.ts +1 -1
  198. package/src/tools/browser.ts +16 -1
  199. package/src/tools/checkpoint.ts +1 -1
  200. package/src/tools/debug.ts +1 -1
  201. package/src/tools/eval.ts +11 -6
  202. package/src/tools/fetch.ts +13 -2
  203. package/src/tools/find.ts +1 -1
  204. package/src/tools/gh.ts +1 -1
  205. package/src/tools/github-cache.ts +2 -1
  206. package/src/tools/image-gen.ts +1 -1
  207. package/src/tools/index.ts +3 -1
  208. package/src/tools/inspect-image.ts +3 -1
  209. package/src/tools/irc.ts +1 -1
  210. package/src/tools/job.ts +1 -1
  211. package/src/tools/memory-edit.ts +1 -1
  212. package/src/tools/memory-recall.ts +1 -1
  213. package/src/tools/memory-reflect.ts +1 -1
  214. package/src/tools/memory-retain.ts +1 -1
  215. package/src/tools/read.ts +8 -2
  216. package/src/tools/render-mermaid.ts +1 -1
  217. package/src/tools/report-tool-issue.ts +3 -2
  218. package/src/tools/resolve.ts +1 -1
  219. package/src/tools/review.ts +1 -1
  220. package/src/tools/search-tool-bm25.ts +1 -1
  221. package/src/tools/search.ts +1 -1
  222. package/src/tools/ssh.ts +1 -1
  223. package/src/tools/todo.ts +1 -1
  224. package/src/tools/tts.ts +1 -1
  225. package/src/tools/write.ts +1 -1
  226. package/src/utils/clipboard.ts +35 -18
  227. package/src/utils/image-loading.ts +35 -4
  228. package/src/utils/thinking-display.ts +37 -0
  229. package/src/web/search/index.ts +1 -1
  230. package/dist/types/cli/list-models.d.ts +0 -30
  231. package/src/cli/list-models.ts +0 -194
package/src/lsp/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ptree } from "@oh-my-pi/pi-utils";
2
- import * as z from "zod/v4";
2
+ import { z } from "zod/v4";
3
3
 
4
4
  // =============================================================================
5
5
  // Tool Schema
package/src/main.ts CHANGED
@@ -25,7 +25,6 @@ import type { Args } from "./cli/args";
25
25
  import { applyExtensionFlags, type ExtensionFlagSink } from "./cli/extension-flags";
26
26
  import { processFileArguments } from "./cli/file-processor";
27
27
  import { buildInitialMessage } from "./cli/initial-message";
28
- import { runListModelsCommand } from "./cli/list-models";
29
28
  import { selectSession } from "./cli/session-picker";
30
29
  import { applyStartupCwd } from "./cli/startup-cwd";
31
30
  import { findConfigFile } from "./config";
@@ -920,30 +919,6 @@ export async function runRootCommand(
920
919
  process.exit(0);
921
920
  }
922
921
 
923
- if (parsedArgs.listModels !== undefined) {
924
- const settingsInstance = await logger.time("settings:init:list-models", Settings.init, {
925
- cwd: getProjectDir(),
926
- configFiles: parsedArgs.config,
927
- });
928
- await modelRegistry.refresh("online");
929
- const cliExtensionPaths = parsedArgs.noExtensions
930
- ? []
931
- : [...(parsedArgs.extensions ?? []), ...(parsedArgs.hooks ?? [])];
932
- const settingsExtensions = settingsInstance.get("extensions") ?? [];
933
- const disabledExtensionIds = settingsInstance.get("disabledExtensions") ?? [];
934
- const searchPattern = typeof parsedArgs.listModels === "string" ? parsedArgs.listModels : undefined;
935
- await runListModelsCommand({
936
- modelRegistry,
937
- cwd: getProjectDir(),
938
- additionalExtensionPaths: cliExtensionPaths,
939
- settingsExtensions,
940
- disabledExtensionIds,
941
- disableExtensionDiscovery: Boolean(parsedArgs.noExtensions),
942
- searchPattern,
943
- });
944
- process.exit(0);
945
- }
946
-
947
922
  if (parsedArgs.export) {
948
923
  let result: string;
949
924
  try {
@@ -67,9 +67,13 @@ export function validateServerName(name: string): string | undefined {
67
67
  if (name.length > 100) {
68
68
  return "Server name is too long (max 100 characters)";
69
69
  }
70
- // Check for invalid characters (only allow alphanumeric, dash, underscore, dot)
71
- if (!/^[a-zA-Z0-9_.-]+$/.test(name)) {
72
- return "Server name can only contain letters, numbers, dash, underscore, and dot";
70
+ // Check for invalid characters. Colon is allowed so namespaced plugin servers
71
+ // (e.g. "cloudflare:cloudflare-api" from a Claude Code marketplace plugin) can
72
+ // be persisted: the runtime already accepts colons in server names (tool names
73
+ // sanitize them via createMCPToolName) and `/mcp reauth` writes such names back
74
+ // as a user-config override that shadows the discovered entry.
75
+ if (!/^[a-zA-Z0-9_.:-]+$/.test(name)) {
76
+ return "Server name can only contain letters, numbers, dash, underscore, dot, and colon";
73
77
  }
74
78
  return undefined;
75
79
  }
@@ -643,6 +643,17 @@ export class MCPManager {
643
643
  return this.#sources.get(name) ?? this.#connections.get(name)?._source;
644
644
  }
645
645
 
646
+ /**
647
+ * Get the preserved (pre-auth) config for a known server — whether currently
648
+ * connected or merely discovered (a connect was attempted but may have failed,
649
+ * e.g. an OAuth server that has not been authorized yet). Mirrors the
650
+ * reconnect lookup at {@link reconnectServer} so callers like `/mcp reauth`
651
+ * can recover a discovered server's config without re-reading config files.
652
+ */
653
+ getServerConfig(name: string): MCPServerConfig | undefined {
654
+ return this.#connections.get(name)?.config ?? this.#serverConfigs.get(name);
655
+ }
656
+
646
657
  /**
647
658
  * Wait for a connection to complete (or fail).
648
659
  */
@@ -1071,7 +1071,9 @@ function truncateByApproxTokens(text: string, tokenLimit: number): string {
1071
1071
 
1072
1072
  function computeModelTokenBudget(model: Model, config: MemoryRuntimeConfig): number {
1073
1073
  const maxTokens =
1074
- Number.isFinite(model.contextWindow) && model.contextWindow > 0 ? model.contextWindow : config.fallbackTokenLimit;
1074
+ model.contextWindow !== null && Number.isFinite(model.contextWindow) && model.contextWindow > 0
1075
+ ? model.contextWindow
1076
+ : config.fallbackTokenLimit;
1075
1077
  return Math.max(2048, Math.floor(maxTokens));
1076
1078
  }
1077
1079
 
@@ -46,10 +46,11 @@ function globalJobKey(cwd: string): string {
46
46
 
47
47
  export function openMemoryDb(dbPath: string): Database {
48
48
  const db = new Database(dbPath);
49
+ // Install the busy handler BEFORE any lock-taking statement. See #2421.
50
+ db.exec("PRAGMA busy_timeout = 5000");
49
51
  db.exec(`
50
52
  PRAGMA journal_mode=WAL;
51
53
  PRAGMA synchronous=NORMAL;
52
- PRAGMA busy_timeout=5000;
53
54
 
54
55
  CREATE TABLE IF NOT EXISTS threads (
55
56
  id TEXT PRIMARY KEY,
@@ -1,8 +1,9 @@
1
+ import { Database } from "bun:sqlite";
2
+ import * as fs from "node:fs";
1
3
  import * as path from "node:path";
2
4
  import type { MnemopiOptions } from "@oh-my-pi/pi-mnemopi";
3
- import { getMemoriesDir } from "@oh-my-pi/pi-utils";
5
+ import { getMemoriesDir, logger } from "@oh-my-pi/pi-utils";
4
6
  import type { Settings } from "../config/settings";
5
- import * as git from "../utils/git";
6
7
 
7
8
  export type MnemopiLlmMode = "none" | "smol" | "remote";
8
9
 
@@ -42,15 +43,18 @@ export function loadMnemopiConfig(settings: Settings, agentDir: string): Mnemopi
42
43
  const configuredDbPath = settings.get("mnemopi.dbPath");
43
44
  const cwd = settings.getCwd();
44
45
  const scoping = settings.get("mnemopi.scoping");
45
- const scope = resolveBankScope(settings.get("mnemopi.bank"), cwd, scoping);
46
+ const dbPath = configuredDbPath ?? path.join(getMemoriesDir(agentDir), "mnemopi", "mnemopi.db");
47
+ const scope = computeMnemopiBankScope(settings.get("mnemopi.bank"), cwd, scoping);
48
+ const recallBanks =
49
+ scoping === "global" ? scope.recallBanks : extendRecallWithLegacyBanks(scope.recallBanks, dbPath, cwd);
46
50
  const llmMode = settings.get("mnemopi.llmMode");
47
51
  return {
48
- dbPath: configuredDbPath ?? path.join(getMemoriesDir(agentDir), "mnemopi", "mnemopi.db"),
52
+ dbPath,
49
53
  baseBank: scope.baseBank,
50
54
  bank: scope.bank,
51
55
  globalBank: scope.globalBank,
52
56
  retainBank: scope.retainBank,
53
- recallBanks: scope.recallBanks,
57
+ recallBanks,
54
58
  scoping,
55
59
  autoRecall: settings.get("mnemopi.autoRecall"),
56
60
  autoRetain: settings.get("mnemopi.autoRetain"),
@@ -86,7 +90,11 @@ export function loadMnemopiConfig(settings: Settings, agentDir: string): Mnemopi
86
90
 
87
91
  const DEFAULT_SHARED_BANK = "default";
88
92
 
89
- interface MnemopiBankScope {
93
+ // Cap legacy-bank scanning at session start so a pathological banks/
94
+ // directory cannot dominate startup latency.
95
+ const LEGACY_BANK_SCAN_LIMIT = 64;
96
+
97
+ export interface MnemopiBankScope {
90
98
  baseBank: string;
91
99
  bank: string;
92
100
  globalBank: string;
@@ -94,9 +102,19 @@ interface MnemopiBankScope {
94
102
  recallBanks: readonly string[];
95
103
  }
96
104
 
97
- // Mnemopi does not have built-in tag-filtered recall, so `per-project-tagged`
98
- // maps to a project-local write bank plus a shared recall-visible bank.
99
- function resolveBankScope(configured: string | undefined, cwd: string, scoping: MnemopiScoping): MnemopiBankScope {
105
+ /**
106
+ * Resolve write/recall banks for a session.
107
+ *
108
+ * Mnemopi has no tag-filtered recall, so `per-project-tagged` maps to a
109
+ * project-local write bank plus a shared recall-visible bank. The project
110
+ * bank is derived purely from {@link cwd} — see {@link projectBank} for the
111
+ * stability contract.
112
+ */
113
+ export function computeMnemopiBankScope(
114
+ configured: string | undefined,
115
+ cwd: string,
116
+ scoping: MnemopiScoping,
117
+ ): MnemopiBankScope {
100
118
  const project = projectBank(configured, cwd);
101
119
  const globalBank = sharedBank(configured);
102
120
  switch (scoping) {
@@ -131,8 +149,17 @@ function sharedBank(configured: string | undefined): string {
131
149
  return sanitizeBankName(configured) ?? DEFAULT_SHARED_BANK;
132
150
  }
133
151
 
152
+ /**
153
+ * Derive the per-project bank id from `cwd` alone.
154
+ *
155
+ * Earlier versions resolved the enclosing git root before hashing, which
156
+ * made the bank id unstable: removing or adding a `.git` anywhere above the
157
+ * cwd repointed the same conversation directory to a different bank and
158
+ * fragmented memories (#2412). The git lookup is gone here; the rescue path
159
+ * for already-fragmented installs lives in {@link extendRecallWithLegacyBanks}.
160
+ */
134
161
  function projectBank(configured: string | undefined, cwd: string): string {
135
- const projectRoot = git.repo.resolveSync(cwd)?.repoRoot ?? path.resolve(cwd);
162
+ const projectRoot = path.resolve(cwd || ".");
136
163
  const project = projectBankSegment(projectRoot);
137
164
  const base = sanitizeBankName(configured);
138
165
  return limitBankName(base ? `${base}-${project}` : project);
@@ -140,7 +167,64 @@ function projectBank(configured: string | undefined, cwd: string): string {
140
167
 
141
168
  function projectBankSegment(projectRoot: string): string {
142
169
  const project = sanitizeBankName(path.basename(projectRoot)) ?? "default";
143
- return limitBankName(`${project}-${Bun.hash(path.resolve(projectRoot)).toString(36)}`);
170
+ return limitBankName(`${project}-${Bun.hash(projectRoot).toString(36)}`);
171
+ }
172
+
173
+ /**
174
+ * Discover sibling banks under `<dbDir>/banks/` whose `working_memory` rows
175
+ * already carry the active `cwd` in `metadata_json.$.cwd`, and add them to
176
+ * the recall set. This rescues memories stranded by a previous, less-stable
177
+ * bank derivation (#2412) without changing the write target — only recall is
178
+ * widened.
179
+ *
180
+ * Robust by design: a missing banks directory, unreadable bank dir, or
181
+ * corrupt SQLite file is silently skipped. Scanning is capped at
182
+ * {@link LEGACY_BANK_SCAN_LIMIT} to bound startup cost.
183
+ */
184
+ export function extendRecallWithLegacyBanks(
185
+ resolved: readonly string[],
186
+ dbPath: string,
187
+ cwd: string,
188
+ ): readonly string[] {
189
+ const banksDir = path.join(path.dirname(dbPath), "banks");
190
+ const cwdAbs = path.resolve(cwd || ".");
191
+ let entries: fs.Dirent[];
192
+ try {
193
+ entries = fs.readdirSync(banksDir, { withFileTypes: true });
194
+ } catch {
195
+ return resolved;
196
+ }
197
+ const have = new Set(resolved);
198
+ const extras: string[] = [];
199
+ let scanned = 0;
200
+ for (const entry of entries) {
201
+ if (!entry.isDirectory() || have.has(entry.name)) continue;
202
+ if (scanned >= LEGACY_BANK_SCAN_LIMIT) break;
203
+ scanned++;
204
+ const candidate = path.join(banksDir, entry.name, "mnemopi.db");
205
+ if (bankHasCwd(candidate, cwdAbs)) extras.push(entry.name);
206
+ }
207
+ return extras.length === 0 ? resolved : [...resolved, ...extras];
208
+ }
209
+
210
+ function bankHasCwd(dbPath: string, cwd: string): boolean {
211
+ let db: Database | undefined;
212
+ try {
213
+ db = new Database(dbPath, { readonly: true });
214
+ const row = db
215
+ .query("SELECT 1 FROM working_memory WHERE json_extract(metadata_json, '$.cwd') = ? LIMIT 1")
216
+ .get(cwd);
217
+ return row !== null;
218
+ } catch (error) {
219
+ logger.debug("Mnemopi: legacy bank probe failed", { dbPath, error: String(error) });
220
+ return false;
221
+ } finally {
222
+ try {
223
+ db?.close();
224
+ } catch {
225
+ // nothing to do — read-only handle.
226
+ }
227
+ }
144
228
  }
145
229
 
146
230
  function sanitizeBankName(value: string | undefined): string | undefined {
@@ -31,14 +31,11 @@ import {
31
31
  type ResumeSessionResponse,
32
32
  type SessionConfigOption,
33
33
  type SessionInfo,
34
- type SessionModelState,
35
34
  type SessionModeState,
36
35
  type SessionNotification,
37
36
  type SessionUpdate,
38
37
  type SetSessionConfigOptionRequest,
39
38
  type SetSessionConfigOptionResponse,
40
- type SetSessionModelRequest,
41
- type SetSessionModelResponse,
42
39
  type SetSessionModeRequest,
43
40
  type SetSessionModeResponse,
44
41
  type Usage,
@@ -77,6 +74,7 @@ import { AUTO_THINKING, parseConfiguredThinkingLevel } from "../../thinking";
77
74
  import { normalizeLocalScheme } from "../../tools/path-utils";
78
75
  import { runResolveInvocation } from "../../tools/resolve";
79
76
  import { ToolError } from "../../tools/tool-errors";
77
+ import { getVisibleThinkingText } from "../../utils/thinking-display";
80
78
  import { createAcpClientBridge } from "./acp-client-bridge";
81
79
  import {
82
80
  buildToolCallStartUpdate,
@@ -121,7 +119,6 @@ type PromptQueueState = {
121
119
  type PromptLifecycleError = Error & { readonly code: "ACP_SESSION_CLOSED" };
122
120
 
123
121
  type PromptTurnState = {
124
- userMessageId: string;
125
122
  cancelRequested: boolean;
126
123
  settled: boolean;
127
124
  /**
@@ -465,7 +462,6 @@ export class AcpAgent implements Agent {
465
462
  const response: NewSessionResponse = {
466
463
  sessionId: record.session.sessionId,
467
464
  configOptions: this.#buildConfigOptions(record.session),
468
- models: this.#buildModelState(record.session),
469
465
  modes: this.#buildModeState(record.session),
470
466
  };
471
467
  this.#scheduleBootstrapUpdates(record.session.sessionId);
@@ -478,7 +474,6 @@ export class AcpAgent implements Agent {
478
474
  await this.#replaySessionHistory(record);
479
475
  const response: LoadSessionResponse = {
480
476
  configOptions: this.#buildConfigOptions(record.session),
481
- models: this.#buildModelState(record.session),
482
477
  modes: this.#buildModeState(record.session),
483
478
  };
484
479
  this.#scheduleBootstrapUpdates(record.session.sessionId);
@@ -507,7 +502,6 @@ export class AcpAgent implements Agent {
507
502
  const record = await this.#resumeManagedSession(params.sessionId, params.cwd, params.mcpServers ?? []);
508
503
  const response: ResumeSessionResponse = {
509
504
  configOptions: this.#buildConfigOptions(record.session),
510
- models: this.#buildModelState(record.session),
511
505
  modes: this.#buildModeState(record.session),
512
506
  };
513
507
  this.#scheduleBootstrapUpdates(record.session.sessionId);
@@ -520,7 +514,6 @@ export class AcpAgent implements Agent {
520
514
  const response: ForkSessionResponse = {
521
515
  sessionId: record.session.sessionId,
522
516
  configOptions: this.#buildConfigOptions(record.session),
523
- models: this.#buildModelState(record.session),
524
517
  modes: this.#buildModeState(record.session),
525
518
  };
526
519
  this.#scheduleBootstrapUpdates(record.session.sessionId);
@@ -588,13 +581,6 @@ export class AcpAgent implements Agent {
588
581
  return { configOptions: this.#buildConfigOptions(record.session) };
589
582
  }
590
583
 
591
- async unstable_setSessionModel(params: SetSessionModelRequest): Promise<SetSessionModelResponse> {
592
- const record = this.#getSessionRecord(params.sessionId);
593
- await this.#setModelById(record.session, params.modelId);
594
- await this.#pushConfigOptionUpdate(record);
595
- return {};
596
- }
597
-
598
584
  async prompt(params: PromptRequest): Promise<PromptResponse> {
599
585
  const record = this.#getSessionRecord(params.sessionId);
600
586
  const activeTurn = record.promptTurn;
@@ -633,7 +619,6 @@ export class AcpAgent implements Agent {
633
619
  const converted = this.#convertPromptBlocks(params.prompt);
634
620
  const pendingPrompt = Promise.withResolvers<PromptResponse>();
635
621
  record.promptTurn = {
636
- userMessageId: params.messageId ?? crypto.randomUUID(),
637
622
  cancelRequested: false,
638
623
  settled: false,
639
624
  cleanup: undefined,
@@ -766,7 +751,6 @@ export class AcpAgent implements Agent {
766
751
  this.#cloneUsageStatistics(record.session.sessionManager.getUsageStatistics()),
767
752
  record.session.sessionManager.getUsageStatistics(),
768
753
  ),
769
- userMessageId: promptTurn?.userMessageId,
770
754
  });
771
755
  return;
772
756
  }
@@ -844,7 +828,6 @@ export class AcpAgent implements Agent {
844
828
  this.#finishPrompt(record, {
845
829
  stopReason: "cancelled",
846
830
  usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
847
- userMessageId: promptTurn.userMessageId,
848
831
  });
849
832
  return cleanup;
850
833
  }
@@ -1162,7 +1145,6 @@ export class AcpAgent implements Agent {
1162
1145
  this.#finishPrompt(record, {
1163
1146
  stopReason: this.#resolveStopReason(event, promptTurn.cancelRequested),
1164
1147
  usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
1165
- userMessageId: promptTurn.userMessageId,
1166
1148
  });
1167
1149
  }
1168
1150
  }
@@ -1385,28 +1367,6 @@ export class AcpAgent implements Agent {
1385
1367
  return configOptions;
1386
1368
  }
1387
1369
 
1388
- #buildModelState(session: AgentSession): SessionModelState | undefined {
1389
- const models = session.getAvailableModels();
1390
- if (models.length === 0) {
1391
- return undefined;
1392
- }
1393
-
1394
- const availableModels = models.map(model => ({
1395
- modelId: this.#toModelId(model),
1396
- name: model.name,
1397
- description: `${model.provider}/${model.id}`,
1398
- }));
1399
- const currentModelId = session.model ? this.#toModelId(session.model) : availableModels[0]?.modelId;
1400
- if (!currentModelId) {
1401
- return undefined;
1402
- }
1403
-
1404
- return {
1405
- availableModels,
1406
- currentModelId,
1407
- };
1408
- }
1409
-
1410
1370
  #buildThinkingOptions(session: AgentSession): Array<{ value: string; name: string; description?: string }> {
1411
1371
  return [
1412
1372
  { value: THINKING_OFF, name: "Off" },
@@ -1948,17 +1908,14 @@ export class AcpAgent implements Agent {
1948
1908
  });
1949
1909
  continue;
1950
1910
  }
1951
- if (
1952
- item.type === "thinking" &&
1953
- "thinking" in item &&
1954
- typeof item.thinking === "string" &&
1955
- item.thinking.length > 0
1956
- ) {
1911
+ if (item.type === "thinking" && "thinking" in item && typeof item.thinking === "string") {
1912
+ const thinking = getVisibleThinkingText(item);
1913
+ if (thinking.length === 0) continue;
1957
1914
  notifications.push({
1958
1915
  sessionId,
1959
1916
  update: {
1960
1917
  sessionUpdate: "agent_thought_chunk",
1961
- content: { type: "text", text: item.thinking },
1918
+ content: { type: "text", text: thinking },
1962
1919
  messageId,
1963
1920
  },
1964
1921
  });
@@ -9,6 +9,7 @@ import type {
9
9
  import type { AgentSessionEvent } from "../../session/agent-session";
10
10
  import { resolveToCwd } from "../../tools/path-utils";
11
11
  import type { TodoStatus } from "../../tools/todo";
12
+ import { hasVisibleThinking } from "../../utils/thinking-display";
12
13
 
13
14
  interface MessageProgress {
14
15
  textEmitted: boolean;
@@ -256,13 +257,16 @@ function mapAssistantMessageUpdate(
256
257
  progress.textEmitted = true;
257
258
  }
258
259
  break;
259
- case "thinking_delta":
260
+ case "thinking_delta": {
261
+ const block = event.assistantMessageEvent.partial?.content?.[event.assistantMessageEvent.contentIndex];
262
+ if (block?.type === "thinking" && !hasVisibleThinking(block)) return [];
260
263
  sessionUpdate = "agent_thought_chunk";
261
264
  text = event.assistantMessageEvent.delta;
262
265
  if (text.length > 0 && progress) {
263
266
  progress.thoughtEmitted = true;
264
267
  }
265
268
  break;
269
+ }
266
270
  case "done":
267
271
  if (progress?.textEmitted) {
268
272
  return [];
@@ -39,6 +39,7 @@ import type { SessionMessageEntry } from "../../session/session-manager";
39
39
  import { parseSessionEntries } from "../../session/session-manager";
40
40
  import { createIrcMessageCard } from "../../tools/irc";
41
41
  import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
42
+ import { hasVisibleThinking } from "../../utils/thinking-display";
42
43
  import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
43
44
  import { getEditorTheme, theme } from "../theme/theme";
44
45
  import { matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
@@ -992,7 +993,7 @@ export class AgentHubOverlayComponent extends Container {
992
993
  const hasVisibleAssistantContent = message.content.some(
993
994
  content =>
994
995
  (content.type === "text" && content.text.trim().length > 0) ||
995
- (content.type === "thinking" && content.thinking.trim().length > 0),
996
+ (content.type === "thinking" && hasVisibleThinking(content)),
996
997
  );
997
998
  if (hasVisibleAssistantContent) {
998
999
  // New visible turn content closes the current read run (mirrors rebuild).
@@ -6,6 +6,7 @@ import type { AssistantThinkingRenderer } from "../../extensibility/extensions/t
6
6
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
7
7
  import { resolveAbortLabel, shouldRenderAbortReason } from "../../session/messages";
8
8
  import { getPreviewLines, resolveImageOptions, TRUNCATE_LENGTHS } from "../../tools/render-utils";
9
+ import { getVisibleThinkingText, hasVisibleThinking } from "../../utils/thinking-display";
9
10
 
10
11
  /**
11
12
  * Max lines of a turn-ending provider error rendered inline in the transcript.
@@ -245,7 +246,7 @@ export class AssistantMessageComponent extends Container {
245
246
  if (content.type === "text") {
246
247
  parts.push(content.text.trim() ? "T1" : "T0");
247
248
  } else if (content.type === "thinking") {
248
- if (!content.thinking.trim()) parts.push("K0");
249
+ if (!hasVisibleThinking(content)) parts.push("K0");
249
250
  else if (this.hideThinkingBlock) parts.push("KH");
250
251
  else parts.push("KV");
251
252
  } else {
@@ -284,7 +285,7 @@ export class AssistantMessageComponent extends Container {
284
285
  for (const item of this.#fastPathItems) {
285
286
  if (item.blockType === "thinking") {
286
287
  const content = message.content[item.contentIndex];
287
- if (content?.type === "thinking" && content.thinking.trim() !== item.lastText) return false;
288
+ if (content?.type === "thinking" && getVisibleThinkingText(content) !== item.lastText) return false;
288
289
  }
289
290
  }
290
291
  }
@@ -312,7 +313,7 @@ export class AssistantMessageComponent extends Container {
312
313
  if (item.blockType === "text" && content?.type === "text") {
313
314
  newText = content.text.trim();
314
315
  } else if (item.blockType === "thinking" && content?.type === "thinking") {
315
- newText = content.thinking.trim();
316
+ newText = getVisibleThinkingText(content);
316
317
  } else {
317
318
  // Block at this index is gone or changed type (index shift) — fail closed.
318
319
  this.#fastPathKey = undefined;
@@ -347,7 +348,7 @@ export class AssistantMessageComponent extends Container {
347
348
  const hasVisibleContent = message.content.some(
348
349
  c =>
349
350
  (c.type === "text" && c.text.trim()) ||
350
- (!this.hideThinkingBlock && c.type === "thinking" && c.thinking.trim()),
351
+ (!this.hideThinkingBlock && c.type === "thinking" && hasVisibleThinking(c)),
351
352
  );
352
353
 
353
354
  // Render content in order
@@ -362,7 +363,8 @@ export class AssistantMessageComponent extends Container {
362
363
  md.transientRenderCache = this.#lastUpdateTransient;
363
364
  this.#contentContainer.addChild(md);
364
365
  captureItems?.push({ md, contentIndex: i, blockType: "text", lastText: trimmed });
365
- } else if (content.type === "thinking" && content.thinking.trim()) {
366
+ } else if (content.type === "thinking" && hasVisibleThinking(content)) {
367
+ const thinkingText = getVisibleThinkingText(content);
366
368
  if (this.hideThinkingBlock) {
367
369
  thinkingIndex += 1;
368
370
  continue;
@@ -371,9 +373,8 @@ export class AssistantMessageComponent extends Container {
371
373
  // This avoids a superfluous blank line before separately-rendered tool execution blocks.
372
374
  const hasVisibleContentAfter = message.content
373
375
  .slice(i + 1)
374
- .some(c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
376
+ .some(c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && hasVisibleThinking(c)));
375
377
 
376
- const thinkingText = content.thinking.trim();
377
378
  // Thinking traces in thinkingText color, italic
378
379
  const md = new Markdown(thinkingText, 1, 0, getMarkdownTheme(), {
379
380
  color: (text: string) => theme.fg("thinkingText", text),
@@ -16,6 +16,7 @@ export * from "./hook-message";
16
16
  export * from "./hook-selector";
17
17
  export * from "./keybinding-hints";
18
18
  export * from "./login-dialog";
19
+ export * from "./logout-account-selector";
19
20
  export * from "./model-selector";
20
21
  export * from "./oauth-selector";
21
22
  export * from "./queue-mode-selector";
@@ -0,0 +1,130 @@
1
+ import { Container, matchesKey, ScrollView, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
2
+ import { theme } from "../../modes/theme/theme";
3
+ import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
4
+ import type { LogoutAccount } from "../../slash-commands/helpers/logout";
5
+ import { DynamicBorder } from "./dynamic-border";
6
+
7
+ const LOGOUT_SELECTOR_MAX_VISIBLE = 10;
8
+
9
+ /** Account picker for `/logout` after the provider has been selected. */
10
+ export class LogoutAccountSelectorComponent extends Container {
11
+ #listContainer: Container;
12
+ #accounts: LogoutAccount[];
13
+ #selectedIndex = 0;
14
+ #statusMessage: string | undefined;
15
+ #onSelectCallback: (account: LogoutAccount) => void;
16
+ #onCancelCallback: () => void;
17
+
18
+ constructor(
19
+ providerName: string,
20
+ accounts: LogoutAccount[],
21
+ onSelect: (account: LogoutAccount) => void,
22
+ onCancel: () => void,
23
+ ) {
24
+ super();
25
+ this.#accounts = accounts;
26
+ this.#onSelectCallback = onSelect;
27
+ this.#onCancelCallback = onCancel;
28
+ const activeIndex = accounts.findIndex(account => account.active);
29
+ this.#selectedIndex = activeIndex >= 0 ? activeIndex : 0;
30
+
31
+ this.addChild(new DynamicBorder());
32
+ this.addChild(new Spacer(1));
33
+ this.addChild(new TruncatedText(theme.bold(`Select ${providerName} account to log out:`)));
34
+ this.addChild(new Spacer(1));
35
+ this.#listContainer = new Container();
36
+ this.addChild(this.#listContainer);
37
+ this.addChild(new Spacer(1));
38
+ this.addChild(new DynamicBorder());
39
+ this.#updateList();
40
+ }
41
+
42
+ #updateList(): void {
43
+ this.#listContainer.clear();
44
+
45
+ const total = this.#accounts.length;
46
+ const maxVisible = LOGOUT_SELECTOR_MAX_VISIBLE;
47
+ const startIndex =
48
+ total <= maxVisible
49
+ ? 0
50
+ : Math.max(0, Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
51
+ const endIndex = Math.min(startIndex + maxVisible, total);
52
+
53
+ const rows: string[] = [];
54
+ for (let i = startIndex; i < endIndex; i++) {
55
+ const account = this.#accounts[i];
56
+ if (!account) continue;
57
+ const activeTag = account.active ? theme.fg("muted", " (active)") : "";
58
+ const detail = account.detail ? theme.fg("dim", ` ${account.detail}`) : "";
59
+ if (i === this.#selectedIndex) {
60
+ rows.push(`${theme.fg("accent", `${theme.nav.cursor} ${account.label}`)}${activeTag}${detail}`);
61
+ } else {
62
+ rows.push(` ${account.label}${activeTag}${detail}`);
63
+ }
64
+ }
65
+
66
+ if (rows.length > 0) {
67
+ const sv = new ScrollView(rows, {
68
+ height: rows.length,
69
+ scrollbar: "auto",
70
+ totalRows: total,
71
+ theme: { track: text => theme.fg("muted", text), thumb: text => theme.fg("accent", text) },
72
+ });
73
+ sv.setScrollOffset(startIndex);
74
+ this.#listContainer.addChild(sv);
75
+ }
76
+
77
+ if (total === 0) {
78
+ this.#listContainer.addChild(new TruncatedText(theme.fg("muted", " No stored accounts to log out"), 0, 0));
79
+ }
80
+
81
+ this.#listContainer.addChild(
82
+ new TruncatedText(theme.fg("muted", " ↑/↓ select · ↵ log out account · Esc cancel"), 0, 0),
83
+ );
84
+
85
+ if (this.#statusMessage) {
86
+ this.#listContainer.addChild(new Spacer(1));
87
+ this.#listContainer.addChild(new TruncatedText(theme.fg("warning", ` ${this.#statusMessage}`), 0, 0));
88
+ }
89
+ }
90
+
91
+ handleInput(keyData: string): void {
92
+ if (matchesSelectCancel(keyData)) {
93
+ this.#onCancelCallback();
94
+ return;
95
+ }
96
+
97
+ if (matchesSelectUp(keyData)) {
98
+ if (this.#accounts.length > 0) {
99
+ this.#selectedIndex = this.#selectedIndex === 0 ? this.#accounts.length - 1 : this.#selectedIndex - 1;
100
+ }
101
+ this.#statusMessage = undefined;
102
+ this.#updateList();
103
+ } else if (matchesSelectDown(keyData)) {
104
+ if (this.#accounts.length > 0) {
105
+ this.#selectedIndex = this.#selectedIndex === this.#accounts.length - 1 ? 0 : this.#selectedIndex + 1;
106
+ }
107
+ this.#statusMessage = undefined;
108
+ this.#updateList();
109
+ } else if (matchesKey(keyData, "pageUp")) {
110
+ if (this.#accounts.length > 0) {
111
+ this.#selectedIndex = Math.max(0, this.#selectedIndex - LOGOUT_SELECTOR_MAX_VISIBLE);
112
+ }
113
+ this.#statusMessage = undefined;
114
+ this.#updateList();
115
+ } else if (matchesKey(keyData, "pageDown")) {
116
+ if (this.#accounts.length > 0) {
117
+ this.#selectedIndex = Math.min(
118
+ this.#accounts.length - 1,
119
+ this.#selectedIndex + LOGOUT_SELECTOR_MAX_VISIBLE,
120
+ );
121
+ }
122
+ this.#statusMessage = undefined;
123
+ this.#updateList();
124
+ } else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
125
+ const account = this.#accounts[this.#selectedIndex];
126
+ if (!account) return;
127
+ this.#onSelectCallback(account);
128
+ }
129
+ }
130
+ }