@oh-my-pi/pi-coding-agent 14.0.4 → 14.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 (61) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/package.json +11 -8
  3. package/src/async/index.ts +1 -0
  4. package/src/async/support.ts +5 -0
  5. package/src/cli/list-models.ts +96 -57
  6. package/src/commit/model-selection.ts +16 -13
  7. package/src/config/model-equivalence.ts +674 -0
  8. package/src/config/model-registry.ts +182 -13
  9. package/src/config/model-resolver.ts +203 -74
  10. package/src/config/settings-schema.ts +23 -0
  11. package/src/config/settings.ts +9 -2
  12. package/src/dap/session.ts +31 -39
  13. package/src/debug/log-formatting.ts +2 -2
  14. package/src/edit/modes/chunk.ts +8 -3
  15. package/src/export/html/template.css +82 -0
  16. package/src/export/html/template.generated.ts +1 -1
  17. package/src/export/html/template.js +612 -97
  18. package/src/internal-urls/docs-index.generated.ts +1 -1
  19. package/src/internal-urls/jobs-protocol.ts +2 -1
  20. package/src/lsp/client.ts +5 -3
  21. package/src/lsp/index.ts +4 -9
  22. package/src/lsp/utils.ts +26 -0
  23. package/src/main.ts +6 -1
  24. package/src/memories/index.ts +7 -6
  25. package/src/modes/components/diff.ts +1 -1
  26. package/src/modes/components/model-selector.ts +221 -64
  27. package/src/modes/controllers/command-controller.ts +18 -0
  28. package/src/modes/controllers/event-controller.ts +438 -426
  29. package/src/modes/controllers/selector-controller.ts +13 -5
  30. package/src/modes/theme/mermaid-cache.ts +5 -7
  31. package/src/priority.json +8 -0
  32. package/src/prompts/agents/designer.md +1 -2
  33. package/src/prompts/system/system-prompt.md +5 -1
  34. package/src/prompts/tools/bash.md +15 -0
  35. package/src/prompts/tools/cancel-job.md +1 -1
  36. package/src/prompts/tools/chunk-edit.md +39 -40
  37. package/src/prompts/tools/read-chunk.md +13 -1
  38. package/src/prompts/tools/read.md +9 -0
  39. package/src/prompts/tools/write.md +1 -0
  40. package/src/sdk.ts +7 -4
  41. package/src/session/agent-session.ts +33 -6
  42. package/src/session/compaction/compaction.ts +1 -1
  43. package/src/task/executor.ts +5 -1
  44. package/src/tools/await-tool.ts +2 -1
  45. package/src/tools/bash.ts +221 -56
  46. package/src/tools/browser.ts +84 -21
  47. package/src/tools/cancel-job.ts +2 -1
  48. package/src/tools/fetch.ts +1 -1
  49. package/src/tools/find.ts +40 -94
  50. package/src/tools/gemini-image.ts +1 -0
  51. package/src/tools/inspect-image.ts +1 -1
  52. package/src/tools/read.ts +218 -1
  53. package/src/tools/render-utils.ts +1 -1
  54. package/src/tools/sqlite-reader.ts +623 -0
  55. package/src/tools/write.ts +187 -1
  56. package/src/utils/commit-message-generator.ts +1 -0
  57. package/src/utils/git.ts +24 -1
  58. package/src/utils/image-resize.ts +73 -37
  59. package/src/utils/title-generator.ts +1 -1
  60. package/src/web/scrapers/types.ts +50 -32
  61. package/src/web/search/providers/codex.ts +21 -2
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
- import { logger, ptree } from "@oh-my-pi/pi-utils";
2
+ import * as timers from "node:timers/promises";
3
+ import { logger, ptree, untilAborted } from "@oh-my-pi/pi-utils";
3
4
  import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
4
5
  import { DapClient } from "./client";
5
6
  import type {
@@ -154,27 +155,10 @@ function buildSummary(session: DapSession): DapSessionSummary {
154
155
  };
155
156
  }
156
157
 
157
- async function raceAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
158
- if (!signal) return promise;
159
- if (signal.aborted) {
160
- throw signal.reason instanceof Error ? signal.reason : new Error("Operation aborted");
161
- }
162
- const { promise: abortPromise, reject } = Promise.withResolvers<never>();
163
- const onAbort = () => {
164
- reject(signal.reason instanceof Error ? signal.reason : new Error("Operation aborted"));
165
- };
166
- signal.addEventListener("abort", onAbort, { once: true });
167
- try {
168
- return await Promise.race([promise, abortPromise]);
169
- } finally {
170
- signal.removeEventListener("abort", onAbort);
171
- }
172
- }
173
-
174
158
  export class DapSessionManager {
175
159
  #sessions = new Map<string, DapSession>();
176
160
  #activeSessionId: string | null = null;
177
- #cleanupTimer?: NodeJS.Timeout;
161
+ #cleanupLoopPromise?: Promise<void>;
178
162
  #nextId = 0;
179
163
 
180
164
  constructor() {
@@ -235,7 +219,7 @@ export class DapSessionManager {
235
219
  // Try to capture initial stopped state (e.g. stopOnEntry).
236
220
  // Timeout is acceptable — the program may simply be running.
237
221
  try {
238
- await raceAbort(initialStopPromise, signal);
222
+ await untilAborted(signal, initialStopPromise);
239
223
  if (session.status === "stopped") {
240
224
  await this.#fetchTopFrame(session, signal, Math.min(timeoutMs, STOP_CAPTURE_TIMEOUT_MS));
241
225
  }
@@ -283,7 +267,7 @@ export class DapSessionManager {
283
267
  await this.#completeConfigurationHandshake(session, signal, timeoutMs);
284
268
  await attachPromise;
285
269
  try {
286
- await raceAbort(initialStopPromise, signal);
270
+ await untilAborted(signal, initialStopPromise);
287
271
  if (session.status === "stopped") {
288
272
  await this.#fetchTopFrame(session, signal, Math.min(timeoutMs, STOP_CAPTURE_TIMEOUT_MS));
289
273
  }
@@ -696,9 +680,9 @@ export class DapSessionManager {
696
680
  // between the request and here. Wait for it, but tolerate timeout if the
697
681
  // session already transitioned.
698
682
  try {
699
- await raceAbort(
700
- session.client.waitForEvent<DapStoppedEventBody>("stopped", undefined, signal, timeoutMs),
683
+ await untilAborted(
701
684
  signal,
685
+ session.client.waitForEvent<DapStoppedEventBody>("stopped", undefined, signal, timeoutMs),
702
686
  );
703
687
  } catch {
704
688
  // Timeout or abort — report current state regardless
@@ -833,16 +817,16 @@ export class DapSessionManager {
833
817
  session.lastUsedAt = Date.now();
834
818
  if (session.status !== "terminated") {
835
819
  if (session.capabilities?.supportsTerminateRequest) {
836
- await raceAbort(
837
- session.client.sendRequest("terminate", undefined, signal, timeoutMs).catch(() => undefined),
820
+ await untilAborted(
838
821
  signal,
822
+ session.client.sendRequest("terminate", undefined, signal, timeoutMs).catch(() => undefined),
839
823
  );
840
824
  }
841
- await raceAbort(
825
+ await untilAborted(
826
+ signal,
842
827
  session.client
843
828
  .sendRequest("disconnect", { terminateDebuggee: true }, signal, timeoutMs)
844
829
  .catch(() => undefined),
845
- signal,
846
830
  );
847
831
  }
848
832
  session.status = "terminated";
@@ -852,22 +836,30 @@ export class DapSessionManager {
852
836
  }
853
837
 
854
838
  #startCleanupTimer(): void {
855
- if (this.#cleanupTimer) return;
856
- this.#cleanupTimer = setInterval(() => {
857
- void this.#cleanupIdleSessions();
858
- }, CLEANUP_INTERVAL_MS);
859
- this.#cleanupTimer.unref?.();
839
+ if (this.#cleanupLoopPromise) return;
840
+ this.#cleanupLoopPromise = this.#runCleanupLoop();
841
+ }
842
+
843
+ async #runCleanupLoop(): Promise<void> {
844
+ for await (const _ of timers.setInterval(CLEANUP_INTERVAL_MS, null, { ref: false })) {
845
+ try {
846
+ this.#cleanupIdleSessions();
847
+ } catch (error) {
848
+ logger.error("DAP idle session cleanup failed", { error: toErrorMessage(error) });
849
+ }
850
+ }
860
851
  }
861
852
 
862
- async #cleanupIdleSessions(): Promise<void> {
853
+ #cleanupIdleSessions(): void {
854
+ if (this.#sessions.size === 0) return;
863
855
  const now = Date.now();
864
- for (const session of Array.from(this.#sessions.values())) {
856
+ for (const session of this.#sessions.values()) {
865
857
  if (
866
858
  session.status === "terminated" ||
867
859
  now - session.lastUsedAt > IDLE_TIMEOUT_MS ||
868
860
  !session.client.isAlive()
869
861
  ) {
870
- await this.#disposeSession(session);
862
+ this.#disposeSession(session);
871
863
  }
872
864
  }
873
865
  }
@@ -1006,7 +998,7 @@ export class DapSessionManager {
1006
998
  // Wait for the initialized event if we haven't seen it yet.
1007
999
  if (!session.initializedSeen) {
1008
1000
  try {
1009
- await raceAbort(session.client.waitForEvent("initialized", undefined, signal, timeoutMs), signal);
1001
+ await untilAborted(signal, session.client.waitForEvent("initialized", undefined, signal, timeoutMs));
1010
1002
  } catch {
1011
1003
  // Adapter may not send initialized (e.g. it already terminated).
1012
1004
  // Proceed anyway — the launch/attach response will surface any real error.
@@ -1100,7 +1092,7 @@ export class DapSessionManager {
1100
1092
  timeoutMs: number = 30_000,
1101
1093
  ): Promise<DapContinueOutcome> {
1102
1094
  try {
1103
- await raceAbort(outcomePromise, signal);
1095
+ await untilAborted(signal, outcomePromise);
1104
1096
  if (session.status === "stopped") {
1105
1097
  await this.#fetchTopFrame(session, signal, Math.min(timeoutMs, 5_000));
1106
1098
  }
@@ -1243,12 +1235,12 @@ export class DapSessionManager {
1243
1235
  return session;
1244
1236
  }
1245
1237
 
1246
- async #disposeSession(session: DapSession): Promise<void> {
1238
+ #disposeSession(session: DapSession) {
1247
1239
  if (this.#activeSessionId === session.id) {
1248
1240
  this.#activeSessionId = null;
1249
1241
  }
1250
1242
  this.#sessions.delete(session.id);
1251
- await session.client.dispose().catch(() => {});
1243
+ void session.client.dispose().catch(() => {});
1252
1244
  }
1253
1245
  }
1254
1246
 
@@ -1,5 +1,5 @@
1
- import { sanitizeText, wrapTextWithAnsi } from "@oh-my-pi/pi-natives";
2
- import { replaceTabs, truncateToWidth } from "../tools/render-utils";
1
+ import { sanitizeText } from "@oh-my-pi/pi-natives";
2
+ import { replaceTabs, truncateToWidth, wrapTextWithAnsi } from "../tools/render-utils";
3
3
 
4
4
  export function formatDebugLogLine(line: string, maxWidth: number): string {
5
5
  const sanitized = sanitizeText(line);
@@ -332,7 +332,7 @@ export const chunkToolEditSchema = Type.Object({
332
332
  op: StringEnum(CHUNK_OP_VALUES),
333
333
  sel: Type.String({
334
334
  description:
335
- "Chunk selector. Format: 'path@region' for insertions, 'path#CRC@region' for replace. Omit @region to target the full chunk. Valid regions: head, body, tail, decl.",
335
+ "Chunk selector. Use 'path~' or 'path^' for insertions, 'path#CRC~' or 'path#CRC^' for replace, or omit the suffix to target the full chunk.",
336
336
  }),
337
337
  content: Type.String({
338
338
  description:
@@ -443,6 +443,12 @@ export async function executeChunkMode(
443
443
  }
444
444
  const normalizedOperations = normalizeChunkEditOperations(edits);
445
445
 
446
+ if (!sourceExists && normalizedOperations.some(op => op.sel)) {
447
+ throw new Error(
448
+ `File does not exist: ${path}. Cannot resolve chunk selectors on a non-existent file. Use the write tool to create a new file, or check the path for typos.`,
449
+ );
450
+ }
451
+
446
452
  const chunkResult = applyChunkEdits({
447
453
  source: rawContent,
448
454
  language: chunkLanguage,
@@ -453,9 +459,8 @@ export async function executeChunkMode(
453
459
  });
454
460
 
455
461
  if (!chunkResult.changed) {
456
- const responseText = `[No changes needed — content already matches.]\n\n${chunkResult.responseText}`;
457
462
  return {
458
- content: [{ type: "text", text: responseText }],
463
+ content: [{ type: "text", text: "[No changes needed \u2014 content already matches.]" }],
459
464
  details: {
460
465
  diff: "",
461
466
  op: sourceExists ? "update" : "create",
@@ -705,6 +705,88 @@
705
705
  color: var(--error);
706
706
  }
707
707
 
708
+ /* Tool renderer extras */
709
+ .tool-meta {
710
+ margin-top: 4px;
711
+ }
712
+
713
+ .tool-badge {
714
+ display: inline-block;
715
+ padding: 0 6px;
716
+ margin-right: 4px;
717
+ border-radius: 3px;
718
+ background: rgba(255, 255, 255, 0.06);
719
+ color: var(--dim);
720
+ font-size: 11px;
721
+ font-weight: normal;
722
+ vertical-align: baseline;
723
+ }
724
+
725
+ .tool-pattern {
726
+ color: var(--warning);
727
+ }
728
+
729
+ .tool-args {
730
+ margin-top: 4px;
731
+ color: var(--toolOutput);
732
+ }
733
+
734
+ .tool-arg {
735
+ display: block;
736
+ line-height: var(--line-height);
737
+ white-space: pre-wrap;
738
+ word-break: break-word;
739
+ }
740
+
741
+ .tool-arg-key {
742
+ color: var(--dim);
743
+ }
744
+
745
+ .tool-arg-val {
746
+ color: var(--text);
747
+ }
748
+
749
+ .tool-cell {
750
+ margin-top: var(--line-height);
751
+ }
752
+
753
+ .tool-cell-title {
754
+ color: var(--dim);
755
+ font-size: 11px;
756
+ margin-bottom: 2px;
757
+ }
758
+
759
+ /* Todo write tree */
760
+ .todo-tree {
761
+ margin-top: var(--line-height);
762
+ }
763
+
764
+ .todo-phase {
765
+ margin-top: 6px;
766
+ color: var(--accent);
767
+ font-weight: bold;
768
+ }
769
+
770
+ .todo-task {
771
+ padding-left: 12px;
772
+ line-height: var(--line-height);
773
+ }
774
+
775
+ .todo-icon {
776
+ display: inline-block;
777
+ width: 14px;
778
+ text-align: center;
779
+ color: var(--dim);
780
+ }
781
+
782
+ .todo-completed { color: var(--toolDiffAdded); }
783
+ .todo-completed .todo-icon { color: var(--toolDiffAdded); }
784
+ .todo-in_progress { color: var(--warning); }
785
+ .todo-in_progress .todo-icon { color: var(--warning); }
786
+ .todo-abandoned { color: var(--toolDiffRemoved); }
787
+ .todo-abandoned .todo-icon { color: var(--toolDiffRemoved); }
788
+ .todo-pending { color: var(--toolOutput); }
789
+
708
790
  /* Images */
709
791
  .message-images {
710
792
  margin-bottom: 12px;