@oh-my-pi/pi-coding-agent 8.0.16 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/package.json +14 -11
  3. package/scripts/generate-wasm-b64.ts +24 -0
  4. package/src/capability/context-file.ts +1 -1
  5. package/src/capability/extension-module.ts +1 -1
  6. package/src/capability/extension.ts +1 -1
  7. package/src/capability/hook.ts +1 -1
  8. package/src/capability/instruction.ts +1 -1
  9. package/src/capability/mcp.ts +1 -1
  10. package/src/capability/prompt.ts +1 -1
  11. package/src/capability/rule.ts +1 -1
  12. package/src/capability/settings.ts +1 -1
  13. package/src/capability/skill.ts +1 -1
  14. package/src/capability/slash-command.ts +1 -1
  15. package/src/capability/ssh.ts +1 -1
  16. package/src/capability/system-prompt.ts +1 -1
  17. package/src/capability/tool.ts +1 -1
  18. package/src/cli/args.ts +1 -1
  19. package/src/cli/plugin-cli.ts +1 -5
  20. package/src/commit/agentic/agent.ts +309 -0
  21. package/src/commit/agentic/fallback.ts +96 -0
  22. package/src/commit/agentic/index.ts +359 -0
  23. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  24. package/src/commit/agentic/prompts/session-user.md +26 -0
  25. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  26. package/src/commit/agentic/prompts/system.md +40 -0
  27. package/src/commit/agentic/state.ts +74 -0
  28. package/src/commit/agentic/tools/analyze-file.ts +131 -0
  29. package/src/commit/agentic/tools/git-file-diff.ts +194 -0
  30. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  31. package/src/commit/agentic/tools/git-overview.ts +84 -0
  32. package/src/commit/agentic/tools/index.ts +56 -0
  33. package/src/commit/agentic/tools/propose-changelog.ts +128 -0
  34. package/src/commit/agentic/tools/propose-commit.ts +154 -0
  35. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  36. package/src/commit/agentic/tools/split-commit.ts +284 -0
  37. package/src/commit/agentic/topo-sort.ts +44 -0
  38. package/src/commit/agentic/trivial.ts +51 -0
  39. package/src/commit/agentic/validation.ts +200 -0
  40. package/src/commit/analysis/conventional.ts +169 -0
  41. package/src/commit/analysis/index.ts +4 -0
  42. package/src/commit/analysis/scope.ts +242 -0
  43. package/src/commit/analysis/summary.ts +114 -0
  44. package/src/commit/analysis/validation.ts +66 -0
  45. package/src/commit/changelog/detect.ts +36 -0
  46. package/src/commit/changelog/generate.ts +112 -0
  47. package/src/commit/changelog/index.ts +233 -0
  48. package/src/commit/changelog/parse.ts +44 -0
  49. package/src/commit/cli.ts +93 -0
  50. package/src/commit/git/diff.ts +148 -0
  51. package/src/commit/git/errors.ts +11 -0
  52. package/src/commit/git/index.ts +217 -0
  53. package/src/commit/git/operations.ts +53 -0
  54. package/src/commit/index.ts +5 -0
  55. package/src/commit/map-reduce/.map-phase.ts.kate-swp +0 -0
  56. package/src/commit/map-reduce/index.ts +63 -0
  57. package/src/commit/map-reduce/map-phase.ts +193 -0
  58. package/src/commit/map-reduce/reduce-phase.ts +147 -0
  59. package/src/commit/map-reduce/utils.ts +9 -0
  60. package/src/commit/message.ts +11 -0
  61. package/src/commit/model-selection.ts +84 -0
  62. package/src/commit/pipeline.ts +242 -0
  63. package/src/commit/prompts/analysis-system.md +155 -0
  64. package/src/commit/prompts/analysis-user.md +41 -0
  65. package/src/commit/prompts/changelog-system.md +56 -0
  66. package/src/commit/prompts/changelog-user.md +19 -0
  67. package/src/commit/prompts/file-observer-system.md +26 -0
  68. package/src/commit/prompts/file-observer-user.md +9 -0
  69. package/src/commit/prompts/reduce-system.md +60 -0
  70. package/src/commit/prompts/reduce-user.md +17 -0
  71. package/src/commit/prompts/summary-retry.md +4 -0
  72. package/src/commit/prompts/summary-system.md +52 -0
  73. package/src/commit/prompts/summary-user.md +13 -0
  74. package/src/commit/prompts/types-description.md +2 -0
  75. package/src/commit/types.ts +109 -0
  76. package/src/commit/utils/exclusions.ts +42 -0
  77. package/src/config/file-lock.ts +111 -0
  78. package/src/config/model-registry.ts +16 -7
  79. package/src/config/settings-manager.ts +115 -40
  80. package/src/config.ts +5 -5
  81. package/src/discovery/agents-md.ts +1 -1
  82. package/src/discovery/builtin.ts +1 -1
  83. package/src/discovery/claude.ts +1 -1
  84. package/src/discovery/cline.ts +1 -1
  85. package/src/discovery/codex.ts +1 -1
  86. package/src/discovery/cursor.ts +1 -1
  87. package/src/discovery/gemini.ts +1 -1
  88. package/src/discovery/github.ts +1 -1
  89. package/src/discovery/index.ts +11 -11
  90. package/src/discovery/mcp-json.ts +1 -1
  91. package/src/discovery/ssh.ts +1 -1
  92. package/src/discovery/vscode.ts +1 -1
  93. package/src/discovery/windsurf.ts +1 -1
  94. package/src/extensibility/custom-commands/loader.ts +1 -1
  95. package/src/extensibility/custom-commands/types.ts +1 -1
  96. package/src/extensibility/custom-tools/loader.ts +1 -1
  97. package/src/extensibility/custom-tools/types.ts +1 -1
  98. package/src/extensibility/extensions/loader.ts +1 -1
  99. package/src/extensibility/extensions/types.ts +1 -1
  100. package/src/extensibility/hooks/loader.ts +1 -1
  101. package/src/extensibility/hooks/types.ts +3 -3
  102. package/src/index.ts +10 -10
  103. package/src/ipy/executor.ts +97 -1
  104. package/src/lsp/index.ts +1 -1
  105. package/src/lsp/render.ts +90 -46
  106. package/src/main.ts +16 -3
  107. package/src/mcp/loader.ts +3 -3
  108. package/src/migrations.ts +3 -3
  109. package/src/modes/components/assistant-message.ts +29 -1
  110. package/src/modes/components/tool-execution.ts +5 -3
  111. package/src/modes/components/tree-selector.ts +1 -1
  112. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  113. package/src/modes/controllers/selector-controller.ts +1 -1
  114. package/src/modes/interactive-mode.ts +5 -3
  115. package/src/modes/rpc/rpc-client.ts +1 -1
  116. package/src/modes/rpc/rpc-mode.ts +1 -4
  117. package/src/modes/rpc/rpc-types.ts +1 -1
  118. package/src/modes/theme/mermaid-cache.ts +89 -0
  119. package/src/modes/theme/theme.ts +2 -0
  120. package/src/modes/types.ts +2 -2
  121. package/src/patch/index.ts +3 -9
  122. package/src/patch/shared.ts +33 -5
  123. package/src/prompts/tools/task.md +2 -0
  124. package/src/sdk.ts +60 -22
  125. package/src/session/agent-session.ts +3 -3
  126. package/src/session/agent-storage.ts +32 -28
  127. package/src/session/artifacts.ts +24 -1
  128. package/src/session/auth-storage.ts +25 -10
  129. package/src/session/storage-migration.ts +12 -53
  130. package/src/system-prompt.ts +2 -2
  131. package/src/task/.executor.ts.kate-swp +0 -0
  132. package/src/task/executor.ts +1 -1
  133. package/src/task/index.ts +10 -1
  134. package/src/task/output-manager.ts +94 -0
  135. package/src/task/render.ts +7 -12
  136. package/src/task/worker.ts +1 -1
  137. package/src/tools/ask.ts +35 -13
  138. package/src/tools/bash.ts +80 -87
  139. package/src/tools/calculator.ts +42 -40
  140. package/src/tools/complete.ts +1 -1
  141. package/src/tools/fetch.ts +67 -104
  142. package/src/tools/find.ts +83 -86
  143. package/src/tools/grep.ts +80 -96
  144. package/src/tools/index.ts +10 -7
  145. package/src/tools/ls.ts +39 -65
  146. package/src/tools/notebook.ts +48 -64
  147. package/src/tools/output-utils.ts +1 -1
  148. package/src/tools/python.ts +71 -183
  149. package/src/tools/read.ts +74 -15
  150. package/src/tools/render-utils.ts +1 -15
  151. package/src/tools/ssh.ts +43 -24
  152. package/src/tools/todo-write.ts +27 -15
  153. package/src/tools/write.ts +93 -64
  154. package/src/tui/code-cell.ts +115 -0
  155. package/src/tui/file-list.ts +48 -0
  156. package/src/tui/index.ts +11 -0
  157. package/src/tui/output-block.ts +73 -0
  158. package/src/tui/status-line.ts +40 -0
  159. package/src/tui/tree-list.ts +56 -0
  160. package/src/tui/types.ts +17 -0
  161. package/src/tui/utils.ts +49 -0
  162. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
  163. package/src/web/search/auth.ts +1 -1
  164. package/src/web/search/index.ts +1 -1
  165. package/src/web/search/render.ts +119 -163
  166. package/tsconfig.json +0 -42
@@ -116,8 +116,6 @@ export class AgentStorage {
116
116
  private static instances = new Map<string, AgentStorage>();
117
117
 
118
118
  private listSettingsStmt: Statement;
119
- private insertSettingStmt: Statement;
120
- private deleteSettingsStmt: Statement;
121
119
  private getCacheStmt: Statement;
122
120
  private upsertCacheStmt: Statement;
123
121
  private deleteExpiredCacheStmt: Statement;
@@ -137,10 +135,6 @@ export class AgentStorage {
137
135
  this.hardenPermissions(dbPath);
138
136
 
139
137
  this.listSettingsStmt = this.db.prepare("SELECT key, value FROM settings");
140
- this.insertSettingStmt = this.db.prepare(
141
- "INSERT INTO settings (key, value, updated_at) VALUES (?, ?, unixepoch())",
142
- );
143
- this.deleteSettingsStmt = this.db.prepare("DELETE FROM settings");
144
138
 
145
139
  this.getCacheStmt = this.db.prepare("SELECT value FROM cache WHERE key = ? AND expires_at > unixepoch()");
146
140
  this.upsertCacheStmt = this.db.prepare(
@@ -264,20 +258,43 @@ CREATE TABLE settings (
264
258
 
265
259
  /**
266
260
  * Returns singleton instance for the given database path, creating if needed.
261
+ * Retries on SQLITE_BUSY with exponential backoff.
267
262
  * @param dbPath - Path to the SQLite database file (defaults to config path)
268
263
  * @returns AgentStorage instance for the given path
269
264
  */
270
- static open(dbPath: string = getAgentDbPath()): AgentStorage {
265
+ static async open(dbPath: string = getAgentDbPath()): Promise<AgentStorage> {
271
266
  const existing = AgentStorage.instances.get(dbPath);
272
267
  if (existing) return existing;
273
- const storage = new AgentStorage(dbPath);
274
- AgentStorage.instances.set(dbPath, storage);
275
- return storage;
268
+
269
+ const maxRetries = 3;
270
+ const baseDelayMs = 100;
271
+ let lastError: Error | undefined;
272
+
273
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
274
+ try {
275
+ const storage = new AgentStorage(dbPath);
276
+ AgentStorage.instances.set(dbPath, storage);
277
+ return storage;
278
+ } catch (err) {
279
+ const isSqliteBusy = err && typeof err === "object" && (err as { code?: string }).code === "SQLITE_BUSY";
280
+ if (!isSqliteBusy) {
281
+ throw err;
282
+ }
283
+ lastError = err as Error;
284
+ const delayMs = baseDelayMs * 2 ** attempt;
285
+ await Bun.sleep(delayMs);
286
+ }
287
+ }
288
+
289
+ throw lastError ?? new Error("Failed to open database after retries");
276
290
  }
277
291
 
278
292
  /**
279
- * Retrieves all settings from storage.
293
+ * Retrieves all settings from storage (legacy, for migration only).
294
+ * Settings are now stored in config.yml. This method is only used
295
+ * during migration from agent.db to config.yml.
280
296
  * @returns Settings object, or null if no settings are stored
297
+ * @deprecated Use config.yml instead. This is only for migration.
281
298
  */
282
299
  getSettings(): Settings | null {
283
300
  const rows = (this.listSettingsStmt.all() as SettingsRow[]) ?? [];
@@ -297,26 +314,13 @@ CREATE TABLE settings (
297
314
  }
298
315
 
299
316
  /**
300
- * Atomically replaces all settings in storage.
301
- * Uses delete-then-insert within a transaction for consistency.
302
- * @param settings - Settings object to persist
317
+ * @deprecated Settings are now stored in config.yml, not agent.db.
318
+ * This method is kept for backward compatibility but does nothing.
303
319
  */
304
320
  saveSettings(settings: Settings): void {
305
- const entries = Object.entries(settings).filter(([, value]) => value !== undefined);
306
- const replace = this.db.transaction((rows: Array<[string, unknown]>) => {
307
- this.deleteSettingsStmt.run();
308
- for (const [key, value] of rows) {
309
- const serialized = JSON.stringify(value);
310
- if (serialized === undefined) continue;
311
- this.insertSettingStmt.run(key, serialized);
312
- }
321
+ logger.warn("AgentStorage.saveSettings is deprecated - settings are now stored in config.yml", {
322
+ keys: Object.keys(settings),
313
323
  });
314
-
315
- try {
316
- replace(entries);
317
- } catch (error) {
318
- logger.error("AgentStorage failed to save settings", { error: String(error) });
319
- }
320
324
  }
321
325
 
322
326
  /**
@@ -18,6 +18,7 @@ export class ArtifactManager {
18
18
  #nextId = 0;
19
19
  readonly #dir: string;
20
20
  #dirCreated = false;
21
+ #initialized = false;
21
22
 
22
23
  /**
23
24
  * @param sessionFile Path to the session .jsonl file
@@ -40,6 +41,28 @@ export class ArtifactManager {
40
41
  await mkdir(this.#dir, { recursive: true });
41
42
  this.#dirCreated = true;
42
43
  }
44
+ if (!this.#initialized) {
45
+ await this.#scanExistingIds();
46
+ this.#initialized = true;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Scan existing artifact files to find the next available ID.
52
+ * This ensures we don't overwrite artifacts when resuming a session.
53
+ */
54
+ async #scanExistingIds(): Promise<void> {
55
+ const files = await this.listFiles();
56
+ let maxId = -1;
57
+ for (const file of files) {
58
+ // Files are named: {id}.{toolType}.log
59
+ const match = file.match(/^(\d+)\..*\.log$/);
60
+ if (match) {
61
+ const id = parseInt(match[1], 10);
62
+ if (id > maxId) maxId = id;
63
+ }
64
+ }
65
+ this.#nextId = maxId + 1;
43
66
  }
44
67
 
45
68
  /**
@@ -58,7 +81,7 @@ export class ArtifactManager {
58
81
  async allocatePath(toolType: string): Promise<{ id: string; path: string }> {
59
82
  await this.#ensureDir();
60
83
  const id = String(this.allocateId());
61
- const filename = `${id}.${toolType}.txt`;
84
+ const filename = `${id}.${toolType}.log`;
62
85
  return { id, path: join(this.#dir, filename) };
63
86
  }
64
87
 
@@ -162,17 +162,14 @@ export class AuthStorage {
162
162
  private usageLogger?: UsageLogger;
163
163
  private fallbackResolver?: (provider: string) => string | undefined;
164
164
 
165
- /**
166
- * @param authPath - Legacy auth.json path used for migration and locating agent.db
167
- * @param fallbackPaths - Additional auth.json paths to migrate (legacy support)
168
- */
169
- constructor(
165
+ private constructor(
170
166
  private authPath: string,
171
167
  private fallbackPaths: string[] = [],
168
+ storage: AgentStorage,
172
169
  options: AuthStorageOptions = {},
173
170
  ) {
174
171
  this.dbPath = AuthStorage.resolveDbPath(authPath);
175
- this.storage = AgentStorage.open(this.dbPath);
172
+ this.storage = storage;
176
173
  this.usageProviderResolver = options.usageProviderResolver ?? resolveDefaultUsageProvider;
177
174
  this.usageCache = options.usageCache ?? new AuthStorageUsageCache(this.storage);
178
175
  this.usageFetch = options.usageFetch ?? fetch;
@@ -185,17 +182,35 @@ export class AuthStorage {
185
182
  } satisfies UsageLogger);
186
183
  }
187
184
 
185
+ /**
186
+ * Create an AuthStorage instance.
187
+ * @param authPath - Legacy auth.json path used for migration and locating agent.db
188
+ * @param fallbackPaths - Additional auth.json paths to migrate (legacy support)
189
+ */
190
+ static async create(
191
+ authPath: string,
192
+ fallbackPaths: string[] = [],
193
+ options: AuthStorageOptions = {},
194
+ ): Promise<AuthStorage> {
195
+ const dbPath = AuthStorage.resolveDbPath(authPath);
196
+ const storage = await AgentStorage.open(dbPath);
197
+ return new AuthStorage(authPath, fallbackPaths, storage, options);
198
+ }
199
+
188
200
  /**
189
201
  * Create an in-memory AuthStorage instance from serialized data.
190
202
  * Used by subagent workers to bypass discovery and use parent's credentials.
191
203
  */
192
- static fromSerialized(data: SerializedAuthStorage, options: AuthStorageOptions = {}): AuthStorage {
193
- const instance = Object.create(AuthStorage.prototype) as AuthStorage;
204
+ static async fromSerialized(data: SerializedAuthStorage, options: AuthStorageOptions = {}): Promise<AuthStorage> {
194
205
  const authPath = data.authPath ?? data.dbPath ?? getAuthPath();
206
+ const dbPath = data.dbPath ?? AuthStorage.resolveDbPath(authPath);
207
+ const storage = await AgentStorage.open(dbPath);
208
+
209
+ const instance = Object.create(AuthStorage.prototype) as AuthStorage;
195
210
  instance.authPath = authPath;
196
211
  instance.fallbackPaths = [];
197
- instance.dbPath = data.dbPath ?? AuthStorage.resolveDbPath(authPath);
198
- instance.storage = AgentStorage.open(instance.dbPath);
212
+ instance.dbPath = dbPath;
213
+ instance.storage = storage;
199
214
  instance.data = new Map();
200
215
  instance.runtimeOverrides = new Map();
201
216
  instance.providerRoundRobinIndex = new Map();
@@ -1,11 +1,13 @@
1
1
  /**
2
- * Migrates legacy JSON storage (settings.json, auth.json) to SQLite-based agent.db.
3
- * Settings migrate only when the DB has no settings; auth merges per-provider when missing.
2
+ * Migrates legacy auth.json to SQLite-based agent.db.
3
+ * Auth credentials merge per-provider when missing.
4
4
  * Original JSON files are backed up to .bak and removed after successful migration.
5
+ *
6
+ * NOTE: Settings migration is now handled by SettingsManager.migrateToYaml(),
7
+ * which migrates from both settings.json and agent.db to config.yaml.
5
8
  */
6
9
 
7
10
  import { getAgentDbPath } from "@oh-my-pi/pi-coding-agent/config";
8
- import type { Settings } from "@oh-my-pi/pi-coding-agent/config/settings-manager";
9
11
  import { logger } from "@oh-my-pi/pi-utils";
10
12
  import { AgentStorage } from "./agent-storage";
11
13
  import type { AuthCredential, AuthCredentialEntry, AuthStorageData } from "./auth-storage";
@@ -14,7 +16,7 @@ import type { AuthCredential, AuthCredentialEntry, AuthStorageData } from "./aut
14
16
  type MigrationPaths = {
15
17
  /** Directory containing agent.db */
16
18
  agentDir: string;
17
- /** Path to legacy settings.json file */
19
+ /** Path to legacy settings.json file (kept for API compatibility, no longer used) */
18
20
  settingsPath: string;
19
21
  /** Candidate paths to search for auth.json (checked in order) */
20
22
  authPaths: string[];
@@ -22,7 +24,7 @@ type MigrationPaths = {
22
24
 
23
25
  /** Result of the JSON-to-SQLite storage migration. */
24
26
  export interface StorageMigrationResult {
25
- /** Whether settings.json was migrated to agent.db */
27
+ /** Whether settings.json was migrated (always false - settings migration is handled elsewhere) */
26
28
  migratedSettings: boolean;
27
29
  /** Whether auth.json was migrated to agent.db */
28
30
  migratedAuth: boolean;
@@ -39,20 +41,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
39
41
  return !!value && typeof value === "object" && !Array.isArray(value);
40
42
  }
41
43
 
42
- /**
43
- * Transforms legacy settings to current schema (e.g., queueMode -> steeringMode).
44
- * @param settings - Settings object potentially containing deprecated keys
45
- * @returns Settings with deprecated keys renamed to current equivalents
46
- */
47
- function migrateLegacySettings(settings: Settings): Settings {
48
- const migrated = { ...settings } as Record<string, unknown>;
49
- if ("queueMode" in migrated && !("steeringMode" in migrated)) {
50
- migrated.steeringMode = migrated.queueMode;
51
- delete migrated.queueMode;
52
- }
53
- return migrated as Settings;
54
- }
55
-
56
44
  /**
57
45
  * Normalizes credential entries to array format (legacy stored single credentials).
58
46
  * @param entry - Single credential or array of credentials
@@ -99,32 +87,6 @@ async function backupJson(path: string): Promise<void> {
99
87
  }
100
88
  }
101
89
 
102
- /**
103
- * Migrates settings.json to SQLite storage if DB is empty.
104
- * @param storage - AgentStorage instance to migrate into
105
- * @param settingsPath - Path to legacy settings.json
106
- * @param warnings - Array to collect non-fatal warnings
107
- * @returns True if migration was performed
108
- */
109
- async function migrateSettings(storage: AgentStorage, settingsPath: string, warnings: string[]): Promise<boolean> {
110
- const settingsFile = Bun.file(settingsPath);
111
- const settingsExists = await settingsFile.exists();
112
- const hasDbSettings = storage.getSettings() !== null;
113
-
114
- if (!settingsExists) return false;
115
- if (hasDbSettings) {
116
- warnings.push(`settings.json exists but agent.db is authoritative: ${settingsPath}`);
117
- return false;
118
- }
119
-
120
- const settingsJson = await readJsonFile<Settings>(settingsPath);
121
- if (!settingsJson) return false;
122
-
123
- storage.saveSettings(migrateLegacySettings(settingsJson));
124
- await backupJson(settingsPath);
125
- return true;
126
- }
127
-
128
90
  /**
129
91
  * Finds the first valid auth.json from candidate paths (checked in priority order).
130
92
  * @param authPaths - Candidate paths to search (e.g., project-local before global)
@@ -191,19 +153,16 @@ async function migrateAuth(storage: AgentStorage, authPaths: string[], warnings:
191
153
  }
192
154
 
193
155
  /**
194
- * Migrates legacy JSON files (settings.json, auth.json) to SQLite-based agent.db.
195
- * Settings migrate only when the DB has no settings; auth merges per-provider when missing.
156
+ * Migrates legacy auth.json to SQLite-based agent.db.
157
+ * Settings migration is handled separately by SettingsManager.migrateToYaml().
196
158
  * @param paths - Configuration specifying locations of legacy files and target DB
197
159
  * @returns Result indicating what was migrated and any warnings encountered
198
160
  */
199
161
  export async function migrateJsonStorage(paths: MigrationPaths): Promise<StorageMigrationResult> {
200
- const storage = AgentStorage.open(getAgentDbPath(paths.agentDir));
162
+ const storage = await AgentStorage.open(getAgentDbPath(paths.agentDir));
201
163
  const warnings: string[] = [];
202
164
 
203
- const [migratedSettings, migratedAuth] = await Promise.all([
204
- migrateSettings(storage, paths.settingsPath, warnings),
205
- migrateAuth(storage, paths.authPaths, warnings),
206
- ]);
165
+ const migratedAuth = await migrateAuth(storage, paths.authPaths, warnings);
207
166
 
208
167
  if (warnings.length > 0) {
209
168
  for (const warning of warnings) {
@@ -211,5 +170,5 @@ export async function migrateJsonStorage(paths: MigrationPaths): Promise<Storage
211
170
  }
212
171
  }
213
172
 
214
- return { migratedSettings, migratedAuth, warnings };
173
+ return { migratedSettings: false, migratedAuth, warnings };
215
174
  }
@@ -13,7 +13,7 @@ import {
13
13
  type ContextFile,
14
14
  loadCapability,
15
15
  type SystemPrompt as SystemPromptFile,
16
- } from "@oh-my-pi/pi-coding-agent/discovery/index";
16
+ } from "@oh-my-pi/pi-coding-agent/discovery";
17
17
  import { loadSkills, type Skill } from "@oh-my-pi/pi-coding-agent/extensibility/skills";
18
18
  import customSystemPromptTemplate from "@oh-my-pi/pi-coding-agent/prompts/system/custom-system-prompt.md" with {
19
19
  type: "text",
@@ -21,7 +21,7 @@ import customSystemPromptTemplate from "@oh-my-pi/pi-coding-agent/prompts/system
21
21
  import systemPromptTemplate from "@oh-my-pi/pi-coding-agent/prompts/system/system-prompt.md" with { type: "text" };
22
22
  import { $ } from "bun";
23
23
  import chalk from "chalk";
24
- import type { ToolName } from "./tools/index";
24
+ import type { ToolName } from "./tools";
25
25
 
26
26
  interface GitContext {
27
27
  isRepo: boolean;
File without changes
@@ -9,7 +9,7 @@ import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
9
9
  import type { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
10
10
  import { formatModelString, parseModelPattern } from "@oh-my-pi/pi-coding-agent/config/model-resolver";
11
11
  import { checkPythonKernelAvailability } from "@oh-my-pi/pi-coding-agent/ipy/kernel";
12
- import { LspTool } from "@oh-my-pi/pi-coding-agent/lsp/index";
12
+ import { LspTool } from "@oh-my-pi/pi-coding-agent/lsp";
13
13
  import type { LspParams } from "@oh-my-pi/pi-coding-agent/lsp/types";
14
14
  import { callTool } from "@oh-my-pi/pi-coding-agent/mcp/client";
15
15
  import type { MCPManager } from "@oh-my-pi/pi-coding-agent/mcp/manager";
package/src/task/index.ts CHANGED
@@ -21,6 +21,7 @@ import type { Usage } from "@oh-my-pi/pi-ai";
21
21
  import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
22
22
  import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
23
23
  import taskDescriptionTemplate from "@oh-my-pi/pi-coding-agent/prompts/tools/task.md" with { type: "text" };
24
+ import { AgentOutputManager } from "@oh-my-pi/pi-coding-agent/task/output-manager";
24
25
  import { formatDuration } from "@oh-my-pi/pi-coding-agent/tools/render-utils";
25
26
  import { $ } from "bun";
26
27
  import { nanoid } from "nanoid";
@@ -101,6 +102,7 @@ function addUsageTotals(target: Usage, usage: Partial<Usage>): void {
101
102
  export { loadBundledAgents as BUNDLED_AGENTS } from "./agents";
102
103
  export { discoverCommands, expandCommand, getCommand } from "./commands";
103
104
  export { discoverAgents, getAgent } from "./discovery";
105
+ export { AgentOutputManager } from "./output-manager";
104
106
  export type { AgentDefinition, AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types";
105
107
  export { taskSchema } from "./types";
106
108
 
@@ -367,7 +369,14 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
367
369
  }
368
370
 
369
371
  // Build full prompts with context prepended
370
- const tasksWithContext = tasks.map((t) => renderTemplate(context, t));
372
+ // Allocate unique IDs across the session to prevent artifact collisions
373
+ const outputManager =
374
+ this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
375
+ const uniqueIds = await outputManager.allocateBatch(tasks.map((t) => t.id));
376
+ const tasksWithUniqueIds = tasks.map((t, i) => ({ ...t, id: uniqueIds[i] }));
377
+
378
+ // Build full prompts with context prepended
379
+ const tasksWithContext = tasksWithUniqueIds.map((t) => renderTemplate(context, t));
371
380
 
372
381
  // Initialize progress for all tasks
373
382
  for (let i = 0; i < tasksWithContext.length; i++) {
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Session-scoped manager for agent output IDs.
3
+ *
4
+ * Ensures unique output IDs across task tool invocations within a session.
5
+ * Prefixes each ID with a sequential number (e.g., "0-AuthProvider", "1-AuthApi").
6
+ *
7
+ * This enables reliable agent:// URL resolution and prevents artifact collisions.
8
+ */
9
+
10
+ import { readdir } from "node:fs/promises";
11
+
12
+ /**
13
+ * Manages agent output ID allocation to ensure uniqueness.
14
+ *
15
+ * Each allocated ID gets a numeric prefix based on allocation order.
16
+ * On resume, scans existing files to find the next available index.
17
+ */
18
+ export class AgentOutputManager {
19
+ #nextId = 0;
20
+ #initialized = false;
21
+ readonly #getArtifactsDir: () => string | null;
22
+
23
+ constructor(getArtifactsDir: () => string | null) {
24
+ this.#getArtifactsDir = getArtifactsDir;
25
+ }
26
+
27
+ /**
28
+ * Scan existing agent output files to find the next available ID.
29
+ * This ensures we don't overwrite outputs when resuming a session.
30
+ */
31
+ async #ensureInitialized(): Promise<void> {
32
+ if (this.#initialized) return;
33
+ this.#initialized = true;
34
+
35
+ const dir = this.#getArtifactsDir();
36
+ if (!dir) return;
37
+
38
+ let files: string[];
39
+ try {
40
+ files = await readdir(dir);
41
+ } catch {
42
+ return; // Directory doesn't exist yet
43
+ }
44
+
45
+ let maxId = -1;
46
+ for (const file of files) {
47
+ // Agent outputs are named: {index}-{id}.md (e.g., "0-AuthProvider.md")
48
+ const match = file.match(/^(\d+)-.*\.md$/);
49
+ if (match) {
50
+ const id = parseInt(match[1], 10);
51
+ if (id > maxId) maxId = id;
52
+ }
53
+ }
54
+ this.#nextId = maxId + 1;
55
+ }
56
+
57
+ /**
58
+ * Allocate a unique ID with numeric prefix.
59
+ *
60
+ * @param id Requested ID (e.g., "AuthProvider")
61
+ * @returns Unique ID with prefix (e.g., "0-AuthProvider")
62
+ */
63
+ async allocate(id: string): Promise<string> {
64
+ await this.#ensureInitialized();
65
+ return `${this.#nextId++}-${id}`;
66
+ }
67
+
68
+ /**
69
+ * Allocate unique IDs for a batch of tasks.
70
+ *
71
+ * @param ids Array of requested IDs
72
+ * @returns Array of unique IDs in same order
73
+ */
74
+ async allocateBatch(ids: string[]): Promise<string[]> {
75
+ await this.#ensureInitialized();
76
+ return ids.map((id) => `${this.#nextId++}-${id}`);
77
+ }
78
+
79
+ /**
80
+ * Get the next ID that would be allocated (without allocating).
81
+ */
82
+ async peekNextIndex(): Promise<number> {
83
+ await this.#ensureInitialized();
84
+ return this.#nextId;
85
+ }
86
+
87
+ /**
88
+ * Reset state (primarily for testing).
89
+ */
90
+ reset(): void {
91
+ this.#nextId = 0;
92
+ this.#initialized = false;
93
+ }
94
+ }
@@ -23,6 +23,7 @@ import {
23
23
  type ReportFindingDetails,
24
24
  type SubmitReviewDetails,
25
25
  } from "@oh-my-pi/pi-coding-agent/tools/review";
26
+ import { renderStatusLine } from "@oh-my-pi/pi-coding-agent/tui";
26
27
  import type { Component } from "@oh-my-pi/pi-tui";
27
28
  import { Container, Text } from "@oh-my-pi/pi-tui";
28
29
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
@@ -229,7 +230,7 @@ function renderOutputSection(
229
230
  maxExpanded = 10,
230
231
  ): string[] {
231
232
  const lines: string[] = [];
232
- const trimmedOutput = output.trim();
233
+ const trimmedOutput = output.trimEnd();
233
234
  if (!trimmedOutput) return lines;
234
235
 
235
236
  if (trimmedOutput.startsWith("{") || trimmedOutput.startsWith("[")) {
@@ -261,7 +262,7 @@ function renderOutputSection(
261
262
 
262
263
  lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
263
264
 
264
- const outputLines = output.split("\n").filter((line) => line.trim());
265
+ const outputLines = output.trimEnd().split("\n");
265
266
  const previewCount = expanded ? maxExpanded : maxCollapsed;
266
267
  for (const line of outputLines.slice(0, previewCount)) {
267
268
  lines.push(`${continuePrefix} ${theme.fg("dim", truncate(line, 70, theme.format.ellipsis))}`);
@@ -395,13 +396,8 @@ function renderArgsSection(
395
396
  * Render the tool call arguments.
396
397
  */
397
398
  export function renderCall(args: TaskParams, theme: Theme): Component {
398
- const label = theme.fg("toolTitle", theme.bold("Task"));
399
- const agentTag = theme.italic(
400
- theme.fg("dim", `${theme.format.bracketLeft}${args.agent}${theme.format.bracketRight}`),
401
- );
402
-
403
399
  const lines: string[] = [];
404
- lines.push(`${label} ${agentTag}`);
400
+ lines.push(renderStatusLine({ icon: "pending", title: "Task", description: args.agent }, theme));
405
401
 
406
402
  const contextTemplate = args.context ?? "";
407
403
  const context = contextTemplate.trim();
@@ -734,13 +730,12 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
734
730
  if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
735
731
  hasCustomRendering = true;
736
732
  const component = handler.renderFinal(dataArray as unknown[], theme, expanded);
733
+ lines.push(`${continuePrefix}${theme.fg("dim", `Tool: ${toolName}`)}`);
737
734
  if (component instanceof Text) {
738
735
  // Prefix each line with continuePrefix
739
736
  const text = component.getText();
740
737
  for (const line of text.split("\n")) {
741
- if (line.trim()) {
742
- lines.push(`${continuePrefix}${line}`);
743
- }
738
+ lines.push(`${continuePrefix}${line}`);
744
739
  }
745
740
  } else if (component instanceof Container) {
746
741
  // For containers, render each child
@@ -845,7 +840,7 @@ export function renderResult(
845
840
  }
846
841
  }
847
842
 
848
- const indented = lines.map((line) => (line.trim() ? ` ${line}` : ""));
843
+ const indented = lines.map((line) => (line.length > 0 ? ` ${line}` : ""));
849
844
  return new Text(indented.join("\n"), 0, 0);
850
845
  }
851
846
 
@@ -552,7 +552,7 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
552
552
  let modelRegistry: ModelRegistry;
553
553
 
554
554
  if (payload.serializedAuth && payload.serializedModels) {
555
- authStorage = AuthStorage.fromSerialized(payload.serializedAuth);
555
+ authStorage = await AuthStorage.fromSerialized(payload.serializedAuth);
556
556
  modelRegistry = ModelRegistry.fromSerialized(payload.serializedModels, authStorage);
557
557
  } else {
558
558
  authStorage = await discoverAuthStorage();
package/src/tools/ask.ts CHANGED
@@ -20,10 +20,11 @@ import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-te
20
20
  import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
21
21
  import { type Theme, theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
22
22
  import askDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/ask.md" with { type: "text" };
23
+ import { renderStatusLine } from "@oh-my-pi/pi-coding-agent/tui";
23
24
  import type { Component } from "@oh-my-pi/pi-tui";
24
25
  import { Text } from "@oh-my-pi/pi-tui";
25
26
  import { Type } from "@sinclair/typebox";
26
- import type { ToolSession } from "./index";
27
+ import type { ToolSession } from ".";
27
28
  import { ToolUIKit } from "./render-utils";
28
29
 
29
30
  // =============================================================================
@@ -381,36 +382,56 @@ export const askToolRenderer = {
381
382
  const { details } = result;
382
383
  if (!details) {
383
384
  const txt = result.content[0];
384
- return new Text(txt?.type === "text" && txt.text ? txt.text : "", 0, 0);
385
+ const fallback = txt?.type === "text" && txt.text ? txt.text : "";
386
+ const header = renderStatusLine({ icon: "warning", title: "Ask" }, uiTheme);
387
+ return new Text([header, uiTheme.fg("dim", fallback)].join("\n"), 0, 0);
385
388
  }
386
389
 
387
390
  // Multi-part results
388
391
  if (details.results && details.results.length > 0) {
389
392
  const lines: string[] = [];
390
-
391
- for (const r of details.results) {
393
+ const hasAnySelection = details.results.some(
394
+ (r) => r.customInput || (r.selectedOptions && r.selectedOptions.length > 0),
395
+ );
396
+ const header = renderStatusLine(
397
+ {
398
+ icon: hasAnySelection ? "success" : "warning",
399
+ title: "Ask",
400
+ meta: [`${details.results.length} questions`],
401
+ },
402
+ uiTheme,
403
+ );
404
+ lines.push(header);
405
+
406
+ for (let i = 0; i < details.results.length; i++) {
407
+ const r = details.results[i];
408
+ const isLastQuestion = i === details.results.length - 1;
409
+ const branch = isLastQuestion ? uiTheme.tree.last : uiTheme.tree.branch;
410
+ const continuation = isLastQuestion ? " " : `${uiTheme.fg("dim", uiTheme.tree.vertical)} `;
392
411
  const hasSelection = r.customInput || r.selectedOptions.length > 0;
393
412
  const statusIcon = hasSelection
394
413
  ? uiTheme.styledSymbol("status.success", "success")
395
414
  : uiTheme.styledSymbol("status.warning", "warning");
396
415
 
397
- lines.push(`${statusIcon} ${uiTheme.fg("dim", `[${r.id}]`)} ${uiTheme.fg("accent", r.question)}`);
416
+ lines.push(
417
+ ` ${uiTheme.fg("dim", branch)} ${statusIcon} ${uiTheme.fg("dim", `[${r.id}]`)} ${uiTheme.fg("accent", r.question)}`,
418
+ );
398
419
 
399
420
  if (r.customInput) {
400
421
  lines.push(
401
- ` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", r.customInput)}`,
422
+ `${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", r.customInput)}`,
402
423
  );
403
424
  } else if (r.selectedOptions.length > 0) {
404
425
  for (let j = 0; j < r.selectedOptions.length; j++) {
405
426
  const isLast = j === r.selectedOptions.length - 1;
406
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
427
+ const optBranch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
407
428
  lines.push(
408
- ` ${uiTheme.fg("dim", branch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${uiTheme.fg("toolOutput", r.selectedOptions[j])}`,
429
+ `${continuation}${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${uiTheme.fg("toolOutput", r.selectedOptions[j])}`,
409
430
  );
410
431
  }
411
432
  } else {
412
433
  lines.push(
413
- ` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`,
434
+ `${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`,
414
435
  );
415
436
  }
416
437
  }
@@ -425,11 +446,12 @@ export const askToolRenderer = {
425
446
  }
426
447
 
427
448
  const hasSelection = details.customInput || (details.selectedOptions && details.selectedOptions.length > 0);
428
- const statusIcon = hasSelection
429
- ? uiTheme.styledSymbol("status.success", "success")
430
- : uiTheme.styledSymbol("status.warning", "warning");
449
+ const header = renderStatusLine(
450
+ { icon: hasSelection ? "success" : "warning", title: "Ask", description: details.question },
451
+ uiTheme,
452
+ );
431
453
 
432
- let text = `${statusIcon} ${uiTheme.fg("accent", details.question)}`;
454
+ let text = header;
433
455
 
434
456
  if (details.customInput) {
435
457
  text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", details.customInput)}`;