@oh-my-pi/pi-coding-agent 14.0.5 → 14.1.1

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 (101) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/package.json +8 -8
  3. package/src/async/index.ts +1 -0
  4. package/src/async/job-manager.ts +43 -10
  5. package/src/async/support.ts +5 -0
  6. package/src/cli/list-models.ts +96 -57
  7. package/src/commit/agentic/tools/analyze-file.ts +1 -2
  8. package/src/commit/model-selection.ts +16 -13
  9. package/src/config/mcp-schema.json +1 -1
  10. package/src/config/model-equivalence.ts +675 -0
  11. package/src/config/model-registry.ts +242 -45
  12. package/src/config/model-resolver.ts +282 -65
  13. package/src/config/settings-schema.ts +27 -3
  14. package/src/config/settings.ts +1 -1
  15. package/src/cursor.ts +64 -23
  16. package/src/edit/index.ts +254 -89
  17. package/src/edit/modes/chunk.ts +336 -57
  18. package/src/edit/modes/hashline.ts +51 -26
  19. package/src/edit/modes/patch.ts +16 -10
  20. package/src/edit/modes/replace.ts +15 -7
  21. package/src/edit/renderer.ts +248 -94
  22. package/src/export/html/template.css +82 -0
  23. package/src/export/html/template.generated.ts +1 -1
  24. package/src/export/html/template.js +614 -97
  25. package/src/extensibility/custom-tools/types.ts +0 -3
  26. package/src/extensibility/extensions/loader.ts +16 -0
  27. package/src/extensibility/extensions/runner.ts +2 -7
  28. package/src/extensibility/extensions/types.ts +8 -4
  29. package/src/internal-urls/docs-index.generated.ts +4 -4
  30. package/src/internal-urls/jobs-protocol.ts +2 -1
  31. package/src/ipy/executor.ts +447 -52
  32. package/src/ipy/kernel.ts +39 -13
  33. package/src/lsp/client.ts +55 -1
  34. package/src/lsp/index.ts +8 -0
  35. package/src/lsp/types.ts +6 -0
  36. package/src/main.ts +6 -2
  37. package/src/memories/index.ts +7 -6
  38. package/src/modes/acp/acp-agent.ts +4 -1
  39. package/src/modes/components/bash-execution.ts +16 -4
  40. package/src/modes/components/model-selector.ts +221 -64
  41. package/src/modes/components/status-line/presets.ts +17 -6
  42. package/src/modes/components/status-line/segments.ts +15 -0
  43. package/src/modes/components/status-line-segment-editor.ts +1 -0
  44. package/src/modes/components/status-line.ts +7 -1
  45. package/src/modes/components/tool-execution.ts +145 -75
  46. package/src/modes/controllers/command-controller.ts +42 -1
  47. package/src/modes/controllers/event-controller.ts +4 -1
  48. package/src/modes/controllers/extension-ui-controller.ts +28 -5
  49. package/src/modes/controllers/input-controller.ts +9 -3
  50. package/src/modes/controllers/selector-controller.ts +17 -6
  51. package/src/modes/interactive-mode.ts +19 -3
  52. package/src/modes/print-mode.ts +13 -4
  53. package/src/modes/prompt-action-autocomplete.ts +3 -5
  54. package/src/modes/rpc/rpc-mode.ts +8 -2
  55. package/src/modes/shared.ts +2 -2
  56. package/src/modes/types.ts +1 -0
  57. package/src/modes/utils/ui-helpers.ts +1 -0
  58. package/src/prompts/system/system-prompt.md +5 -1
  59. package/src/prompts/tools/bash.md +16 -1
  60. package/src/prompts/tools/cancel-job.md +1 -1
  61. package/src/prompts/tools/chunk-edit.md +191 -163
  62. package/src/prompts/tools/hashline.md +11 -11
  63. package/src/prompts/tools/patch.md +10 -5
  64. package/src/prompts/tools/{await.md → poll.md} +1 -1
  65. package/src/prompts/tools/read-chunk.md +12 -3
  66. package/src/prompts/tools/read.md +9 -0
  67. package/src/prompts/tools/task.md +2 -2
  68. package/src/prompts/tools/vim.md +98 -0
  69. package/src/prompts/tools/write.md +1 -0
  70. package/src/sdk.ts +758 -725
  71. package/src/session/agent-session.ts +187 -40
  72. package/src/session/session-manager.ts +50 -4
  73. package/src/slash-commands/builtin-registry.ts +17 -0
  74. package/src/task/executor.ts +9 -5
  75. package/src/task/index.ts +3 -5
  76. package/src/task/types.ts +2 -2
  77. package/src/tools/bash.ts +240 -57
  78. package/src/tools/cancel-job.ts +2 -1
  79. package/src/tools/find.ts +5 -2
  80. package/src/tools/grep.ts +77 -8
  81. package/src/tools/index.ts +48 -19
  82. package/src/tools/inspect-image.ts +1 -1
  83. package/src/tools/{await-tool.ts → poll-tool.ts} +38 -31
  84. package/src/tools/python.ts +293 -278
  85. package/src/tools/read.ts +218 -1
  86. package/src/tools/sqlite-reader.ts +623 -0
  87. package/src/tools/submit-result.ts +5 -2
  88. package/src/tools/todo-write.ts +8 -2
  89. package/src/tools/vim.ts +966 -0
  90. package/src/tools/write.ts +187 -1
  91. package/src/utils/commit-message-generator.ts +1 -0
  92. package/src/utils/edit-mode.ts +2 -1
  93. package/src/utils/git.ts +24 -1
  94. package/src/utils/session-color.ts +55 -0
  95. package/src/utils/title-generator.ts +16 -7
  96. package/src/vim/buffer.ts +309 -0
  97. package/src/vim/commands.ts +382 -0
  98. package/src/vim/engine.ts +2426 -0
  99. package/src/vim/parser.ts +151 -0
  100. package/src/vim/render.ts +252 -0
  101. package/src/vim/types.ts +197 -0
package/src/ipy/kernel.ts CHANGED
@@ -49,6 +49,10 @@ interface KernelShutdownOptions {
49
49
  timeoutMs?: number;
50
50
  }
51
51
 
52
+ export interface KernelShutdownResult {
53
+ confirmed: boolean;
54
+ }
55
+
52
56
  function getRemainingTimeMs(deadlineMs?: number): number | undefined {
53
57
  if (deadlineMs === undefined) return undefined;
54
58
  return Math.max(0, deadlineMs - Date.now());
@@ -399,10 +403,11 @@ export class PythonKernel {
399
403
  #ws: WebSocket | null = null;
400
404
  #disposed = false;
401
405
  #alive = true;
406
+ #shutdownStarted = false;
407
+ #shutdownConfirmed = false;
402
408
  #messageHandlers = new Map<string, (msg: JupyterMessage) => void>();
403
409
  #channelHandlers = new Map<string, Set<(msg: JupyterMessage) => void>>();
404
410
  #pendingExecutions = new Map<string, (reason: string) => void>();
405
-
406
411
  private constructor(
407
412
  readonly id: string,
408
413
  readonly kernelId: string,
@@ -1004,11 +1009,18 @@ export class PythonKernel {
1004
1009
  }
1005
1010
  }
1006
1011
 
1007
- async shutdown(options?: KernelShutdownOptions): Promise<void> {
1008
- if (this.#disposed) return;
1009
- this.#disposed = true;
1010
- this.#alive = false;
1011
- this.#abortPendingExecutions("Kernel shutdown");
1012
+ async shutdown(options?: KernelShutdownOptions): Promise<KernelShutdownResult> {
1013
+ if (this.#shutdownConfirmed) return { confirmed: true };
1014
+ if (!this.#shutdownStarted) {
1015
+ this.#shutdownStarted = true;
1016
+ this.#alive = false;
1017
+ this.#abortPendingExecutions("Kernel shutdown");
1018
+
1019
+ if (this.#ws) {
1020
+ this.#ws.close();
1021
+ this.#ws = null;
1022
+ }
1023
+ }
1012
1024
 
1013
1025
  const shutdownSignal = combineAbortSignal(
1014
1026
  { signal: options?.signal },
@@ -1016,24 +1028,38 @@ export class PythonKernel {
1016
1028
  "Python kernel shutdown timed out",
1017
1029
  );
1018
1030
 
1031
+ let confirmed = false;
1019
1032
  try {
1020
- await fetch(`${this.gatewayUrl}/api/kernels/${this.kernelId}`, {
1033
+ const response = await fetch(`${this.gatewayUrl}/api/kernels/${this.kernelId}`, {
1021
1034
  method: "DELETE",
1022
1035
  headers: this.#authHeaders(),
1023
1036
  signal: shutdownSignal,
1024
1037
  });
1038
+ const deleteConfirmed = response.status === 404 || response.status === 410;
1039
+ confirmed = response.ok || deleteConfirmed;
1040
+ if (!confirmed) {
1041
+ logger.warn("Kernel delete request was not confirmed", {
1042
+ status: response.status,
1043
+ statusText: response.statusText,
1044
+ });
1045
+ }
1025
1046
  } catch (err: unknown) {
1026
1047
  logger.warn("Failed to delete kernel via API", { error: err instanceof Error ? err.message : String(err) });
1027
1048
  }
1028
-
1029
- if (this.#ws) {
1030
- this.#ws.close();
1031
- this.#ws = null;
1032
- }
1049
+ this.#shutdownConfirmed = confirmed;
1050
+ this.#disposed = confirmed;
1033
1051
 
1034
1052
  if (this.isSharedGateway) {
1035
- await releaseSharedGateway();
1053
+ try {
1054
+ await releaseSharedGateway();
1055
+ } catch (err: unknown) {
1056
+ logger.warn("Failed to release shared gateway after kernel shutdown", {
1057
+ error: err instanceof Error ? err.message : String(err),
1058
+ });
1059
+ }
1036
1060
  }
1061
+
1062
+ return { confirmed };
1037
1063
  }
1038
1064
 
1039
1065
  #sendMessage(msg: JupyterMessage): void {
package/src/lsp/client.ts CHANGED
@@ -23,7 +23,7 @@ const fileOperationLocks = new Map<string, Promise<void>>();
23
23
 
24
24
  // Idle timeout configuration (disabled by default)
25
25
  let idleTimeoutMs: number | null = null;
26
- let idleCheckInterval: Timer | null = null;
26
+ let idleCheckInterval: NodeJS.Timeout | null = null;
27
27
  const IDLE_CHECK_INTERVAL_MS = 60 * 1000;
28
28
 
29
29
  /**
@@ -136,6 +136,9 @@ const CLIENT_CAPABILITIES = {
136
136
  dataSupport: true,
137
137
  },
138
138
  },
139
+ window: {
140
+ workDoneProgress: true,
141
+ },
139
142
  workspace: {
140
143
  applyEdit: true,
141
144
  workspaceEdit: {
@@ -267,6 +270,16 @@ async function startMessageReader(client: LspClient): Promise<void> {
267
270
  version: params.version ?? null,
268
271
  });
269
272
  client.diagnosticsVersion += 1;
273
+ } else if (message.method === "$/progress" && message.params) {
274
+ const params = message.params as { token: string | number; value?: { kind?: string } };
275
+ if (params.value?.kind === "begin") {
276
+ client.activeProgressTokens.add(params.token);
277
+ } else if (params.value?.kind === "end") {
278
+ client.activeProgressTokens.delete(params.token);
279
+ if (client.activeProgressTokens.size === 0) {
280
+ client.resolveProjectLoaded();
281
+ }
282
+ }
270
283
  }
271
284
  }
272
285
 
@@ -338,6 +351,13 @@ async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest
338
351
  await handleApplyEditRequest(client, message);
339
352
  return;
340
353
  }
354
+ if (message.method === "window/workDoneProgress/create") {
355
+ // Accept progress token registration from the server
356
+ if (typeof message.id === "number") {
357
+ await sendResponse(client, message.id, null, message.method);
358
+ }
359
+ return;
360
+ }
341
361
  if (typeof message.id !== "number") return;
342
362
  await sendResponse(client, message.id, null, message.method, {
343
363
  code: -32601,
@@ -375,6 +395,9 @@ async function sendResponse(
375
395
  /** Timeout for warmup initialize requests (5 seconds) */
376
396
  export const WARMUP_TIMEOUT_MS = 5000;
377
397
 
398
+ /** Max time to wait for the server to report project loading completion via $/progress */
399
+ const PROJECT_LOAD_TIMEOUT_MS = 15_000;
400
+
378
401
  /**
379
402
  * Get or create an LSP client for the given server configuration and working directory.
380
403
  * @param config - Server configuration
@@ -413,6 +436,18 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
413
436
  env: env ? { ...Bun.env, ...env } : undefined,
414
437
  });
415
438
 
439
+ let resolveProjectLoaded!: () => void;
440
+ const projectLoaded = new Promise<void>(resolve => {
441
+ resolveProjectLoaded = resolve;
442
+ });
443
+ // Auto-resolve after timeout in case server doesn't use progress tokens
444
+ const projectLoadTimeout = setTimeout(resolveProjectLoaded, PROJECT_LOAD_TIMEOUT_MS);
445
+ const originalResolve = resolveProjectLoaded;
446
+ resolveProjectLoaded = () => {
447
+ clearTimeout(projectLoadTimeout);
448
+ originalResolve();
449
+ };
450
+
416
451
  const client: LspClient = {
417
452
  name: key,
418
453
  cwd,
@@ -426,6 +461,9 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
426
461
  messageBuffer: new Uint8Array(0),
427
462
  isReading: false,
428
463
  lastActivity: Date.now(),
464
+ activeProgressTokens: new Set(),
465
+ projectLoaded,
466
+ resolveProjectLoaded,
429
467
  };
430
468
  clients.set(key, client);
431
469
 
@@ -433,6 +471,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
433
471
  proc.exited.then(() => {
434
472
  clients.delete(key);
435
473
  clientLocks.delete(key);
474
+ client.resolveProjectLoaded();
436
475
 
437
476
  // Reject any pending requests — the server is gone, they will never complete.
438
477
  if (client.pendingRequests.size > 0) {
@@ -554,6 +593,21 @@ export async function ensureFileOpen(client: LspClient, filePath: string, signal
554
593
  }
555
594
  }
556
595
 
596
+ /**
597
+ * Wait for the server's initial project loading to complete.
598
+ * Races the server's $/progress tracking against the abort signal.
599
+ * Returns immediately if loading already completed or timed out.
600
+ */
601
+ export async function waitForProjectLoaded(client: LspClient, signal?: AbortSignal): Promise<void> {
602
+ if (signal?.aborted) return;
603
+ await Promise.race([
604
+ client.projectLoaded,
605
+ ...(signal
606
+ ? [new Promise<void>(resolve => signal.addEventListener("abort", () => resolve(), { once: true }))]
607
+ : []),
608
+ ]);
609
+ }
610
+
557
611
  /**
558
612
  * Sync in-memory content to the LSP client without reading from disk.
559
613
  * Use this to provide instant feedback during edits before the file is saved.
package/src/lsp/index.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  setIdleTimeout,
21
21
  syncContent,
22
22
  WARMUP_TIMEOUT_MS,
23
+ waitForProjectLoaded,
23
24
  } from "./client";
24
25
  import { getLinterClient } from "./clients";
25
26
  import { getServersForFile, type LspConfig, loadConfig } from "./config";
@@ -1427,6 +1428,13 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1427
1428
 
1428
1429
  let output: string;
1429
1430
 
1431
+ // Wait for project loading to complete before cross-file operations
1432
+ // to ensure the server has indexed all project files.
1433
+ const crossFileActions = new Set(["definition", "type_definition", "implementation", "references", "rename"]);
1434
+ if (crossFileActions.has(action)) {
1435
+ await waitForProjectLoaded(client, signal);
1436
+ }
1437
+
1430
1438
  switch (action) {
1431
1439
  // =====================================================================
1432
1440
  // Standard LSP Operations
package/src/lsp/types.ts CHANGED
@@ -411,6 +411,12 @@ export interface LspClient {
411
411
  isReading: boolean;
412
412
  serverCapabilities?: LspServerCapabilities;
413
413
  lastActivity: number;
414
+ /** Tracks active work-done progress tokens from the server */
415
+ activeProgressTokens: Set<string | number>;
416
+ /** Resolves when the server's initial project loading completes (or after timeout) */
417
+ projectLoaded: Promise<void>;
418
+ /** Call to signal that project loading has completed */
419
+ resolveProjectLoaded: () => void;
414
420
  }
415
421
 
416
422
  // =============================================================================
package/src/main.ts CHANGED
@@ -79,6 +79,8 @@ const RPC_DEFAULTED_SETTING_PATHS: SettingPath[] = [
79
79
  "todo.eager",
80
80
  "async.enabled",
81
81
  "async.maxJobs",
82
+ "bash.autoBackground.enabled",
83
+ "bash.autoBackground.thresholdMs",
82
84
  "task.isolation.mode",
83
85
  "task.isolation.merge",
84
86
  "task.isolation.commits",
@@ -453,7 +455,9 @@ async function buildSessionOptions(
453
455
  }
454
456
  } else if (resolved.model) {
455
457
  options.model = resolved.model;
456
- settings.overrideModelRoles({ default: `${resolved.model.provider}/${resolved.model.id}` });
458
+ settings.overrideModelRoles({
459
+ default: resolved.selector ?? `${resolved.model.provider}/${resolved.model.id}`,
460
+ });
457
461
  if (!parsed.thinking && resolved.thinkingLevel) {
458
462
  options.thinkingLevel = resolved.thinkingLevel;
459
463
  }
@@ -467,6 +471,7 @@ async function buildSessionOptions(
467
471
  {
468
472
  settings,
469
473
  matchPreferences: modelMatchPreferences,
474
+ modelRegistry,
470
475
  },
471
476
  );
472
477
  const rememberedResolvedModel = rememberedSpec.model;
@@ -834,7 +839,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
834
839
  settings: nextSettings,
835
840
  authStorage,
836
841
  modelRegistry,
837
- searchDb: session.searchDb,
838
842
  hasUI: false,
839
843
  });
840
844
  if (nextSession.extensionRunner) {
@@ -6,7 +6,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
6
6
  import { completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
7
7
  import { getAgentDbPath, getMemoriesDir, logger, parseJsonlLenient, prompt } from "@oh-my-pi/pi-utils";
8
8
  import type { ModelRegistry } from "../config/model-registry";
9
- import { parseModelString } from "../config/model-resolver";
9
+ import { resolveModelRoleValue } from "../config/model-resolver";
10
10
  import type { Settings } from "../config/settings";
11
11
  import consolidationTemplate from "../prompts/memories/consolidation.md" with { type: "text" };
12
12
  import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
@@ -1055,11 +1055,12 @@ async function resolveMemoryModel(options: {
1055
1055
  const { modelRegistry, session, fallbackRole } = options;
1056
1056
  const requestedModel = session.settings.getModelRole(fallbackRole) || session.settings.getModelRole("default");
1057
1057
  if (requestedModel) {
1058
- const parsed = parseModelString(requestedModel);
1059
- if (parsed) {
1060
- const found = modelRegistry.find(parsed.provider, parsed.id);
1061
- if (found) return found;
1062
- }
1058
+ const resolved = resolveModelRoleValue(requestedModel, modelRegistry.getAll(), {
1059
+ settings: session.settings,
1060
+ matchPreferences: { usageOrder: session.settings.getStorage()?.getModelUsageOrder() },
1061
+ modelRegistry,
1062
+ });
1063
+ if (resolved.model) return resolved.model;
1063
1064
  }
1064
1065
  return session.model ?? modelRegistry.getAll()[0];
1065
1066
  }
@@ -1148,10 +1148,13 @@ export class AcpAgent implements Agent {
1148
1148
  },
1149
1149
  getThinkingLevel: () => record.session.thinkingLevel,
1150
1150
  setThinkingLevel: level => record.session.setThinkingLevel(level),
1151
+ getSessionName: () => record.session.sessionManager.getSessionName(),
1152
+ setSessionName: async name => {
1153
+ await record.session.sessionManager.setSessionName(name, "user");
1154
+ },
1151
1155
  },
1152
1156
  {
1153
1157
  getModel: () => record.session.model,
1154
- getSearchDb: () => record.session.searchDb,
1155
1158
  isIdle: () => !record.session.isStreaming,
1156
1159
  abort: () => {
1157
1160
  void record.session.abort();
@@ -3,7 +3,18 @@
3
3
  */
4
4
 
5
5
  import { sanitizeText } from "@oh-my-pi/pi-natives";
6
- import { Container, ImageProtocol, Loader, Spacer, TERMINAL, Text, type TUI } from "@oh-my-pi/pi-tui";
6
+ import {
7
+ Container,
8
+ Ellipsis,
9
+ ImageProtocol,
10
+ Loader,
11
+ Spacer,
12
+ TERMINAL,
13
+ Text,
14
+ type TUI,
15
+ truncateToWidth,
16
+ visibleWidth,
17
+ } from "@oh-my-pi/pi-tui";
7
18
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
8
19
  import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
9
20
  import { getSixelLineMask, isSixelPassthroughEnabled, sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
@@ -210,11 +221,12 @@ export class BashExecutionComponent extends Container {
210
221
  }
211
222
 
212
223
  #clampDisplayLine(line: string): string {
213
- if (line.length <= MAX_DISPLAY_LINE_CHARS) {
224
+ const visible = visibleWidth(line);
225
+ if (visible <= MAX_DISPLAY_LINE_CHARS) {
214
226
  return line;
215
227
  }
216
- const omitted = line.length - MAX_DISPLAY_LINE_CHARS;
217
- return `${line.slice(0, MAX_DISPLAY_LINE_CHARS)}… [${omitted} chars omitted]`;
228
+ const omitted = visible - MAX_DISPLAY_LINE_CHARS;
229
+ return `${truncateToWidth(line, MAX_DISPLAY_LINE_CHARS, Ellipsis.Omit)}… [${omitted} visible columns omitted]`;
218
230
  }
219
231
 
220
232
  #clampLinesPreservingSixel(lines: string[]): string[] {