@oh-my-pi/pi-coding-agent 14.5.8 → 14.5.10

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 (58) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/package.json +7 -15
  3. package/scripts/build-binary.ts +1 -1
  4. package/src/cli/update-cli.ts +25 -1
  5. package/src/config/model-registry.ts +21 -19
  6. package/src/config/settings-schema.ts +14 -19
  7. package/src/discovery/claude-plugins.ts +28 -3
  8. package/src/edit/modes/atom.lark +7 -5
  9. package/src/edit/modes/atom.ts +510 -73
  10. package/src/edit/modes/hashline.ts +172 -91
  11. package/src/extensibility/extensions/runner.ts +34 -1
  12. package/src/extensibility/extensions/types.ts +8 -0
  13. package/src/lsp/client.ts +27 -35
  14. package/src/lsp/index.ts +2 -4
  15. package/src/lsp/render.ts +0 -3
  16. package/src/lsp/types.ts +1 -4
  17. package/src/lsp/utils.ts +18 -14
  18. package/src/memories/index.ts +5 -0
  19. package/src/modes/components/settings-defs.ts +1 -1
  20. package/src/modes/controllers/command-controller.ts +17 -0
  21. package/src/modes/controllers/input-controller.ts +7 -1
  22. package/src/modes/controllers/selector-controller.ts +2 -2
  23. package/src/modes/interactive-mode.ts +57 -26
  24. package/src/modes/theme/theme.ts +10 -1
  25. package/src/modes/types.ts +5 -3
  26. package/src/modes/utils/context-usage.ts +294 -0
  27. package/src/modes/utils/ui-helpers.ts +19 -6
  28. package/src/prompts/system/auto-continue.md +1 -0
  29. package/src/prompts/tools/atom.md +99 -44
  30. package/src/prompts/tools/exit-plan-mode.md +5 -39
  31. package/src/prompts/tools/github.md +3 -3
  32. package/src/prompts/tools/lsp.md +2 -3
  33. package/src/prompts/tools/{run-command.md → recipe.md} +1 -1
  34. package/src/prompts/tools/task.md +34 -147
  35. package/src/prompts/tools/todo-write.md +22 -64
  36. package/src/sdk.ts +13 -2
  37. package/src/session/agent-session.ts +175 -79
  38. package/src/session/compaction/compaction.ts +35 -22
  39. package/src/session/session-dump-format.ts +1 -0
  40. package/src/session/session-manager.ts +19 -2
  41. package/src/slash-commands/builtin-registry.ts +12 -5
  42. package/src/tools/bash.ts +9 -4
  43. package/src/tools/debug.ts +57 -70
  44. package/src/tools/gh.ts +267 -119
  45. package/src/tools/index.ts +7 -7
  46. package/src/tools/{run-command → recipe}/index.ts +19 -19
  47. package/src/tools/recipe/render.ts +19 -0
  48. package/src/tools/{run-command → recipe}/runner.ts +28 -7
  49. package/src/tools/{run-command → recipe}/runners/pkg.ts +23 -53
  50. package/src/tools/renderers.ts +2 -2
  51. package/src/utils/git.ts +61 -2
  52. package/src/web/search/providers/searxng.ts +71 -13
  53. package/src/tools/run-command/render.ts +0 -18
  54. /package/src/tools/{run-command → recipe}/runners/cargo.ts +0 -0
  55. /package/src/tools/{run-command → recipe}/runners/index.ts +0 -0
  56. /package/src/tools/{run-command → recipe}/runners/just.ts +0 -0
  57. /package/src/tools/{run-command → recipe}/runners/make.ts +0 -0
  58. /package/src/tools/{run-command → recipe}/runners/task.ts +0 -0
@@ -26,6 +26,7 @@ import {
26
26
  getOpenAIResponsesHistoryPayload,
27
27
  normalizeResponsesToolCallId,
28
28
  } from "@oh-my-pi/pi-ai/utils";
29
+ import { countTokens } from "@oh-my-pi/pi-natives";
29
30
  import { logger, prompt } from "@oh-my-pi/pi-utils";
30
31
  import compactionShortSummaryPrompt from "../../prompts/compaction/compaction-short-summary.md" with { type: "text" };
31
32
  import compactionSummaryPrompt from "../../prompts/compaction/compaction-summary.md" with { type: "text" };
@@ -218,7 +219,7 @@ export function shouldCompact(contextTokens: number, contextWindow: number, sett
218
219
  return contextTokens > thresholdTokens;
219
220
  }
220
221
 
221
- function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings): number {
222
+ export function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings): number {
222
223
  // Fixed token limit takes priority over percentage
223
224
  const thresholdTokens = settings.thresholdTokens;
224
225
  if (typeof thresholdTokens === "number" && Number.isFinite(thresholdTokens) && thresholdTokens > 0) {
@@ -240,67 +241,79 @@ function resolveThresholdTokens(contextWindow: number, settings: CompactionSetti
240
241
  // ============================================================================
241
242
 
242
243
  /**
243
- * Estimate token count for a message using chars/4 heuristic.
244
- * This is conservative (overestimates tokens).
244
+ * Image content has no tokenizer representation; charge a fixed estimate
245
+ * matching what providers typically bill for inline images.
246
+ */
247
+ const IMAGE_TOKEN_ESTIMATE = 1200;
248
+
249
+ /**
250
+ * Estimate token count for a message using cl100k_base via the native
251
+ * tokenizer. This is not Claude's first-party tokenizer (Anthropic doesn't
252
+ * publish one) but is within ~5–10% across English/code text.
245
253
  */
246
254
  export function estimateTokens(message: AgentMessage): number {
247
- let chars = 0;
255
+ const fragments: string[] = [];
256
+ let extra = 0;
248
257
 
249
258
  switch (message.role) {
250
259
  case "user": {
251
260
  const content = (message as { content: string | Array<{ type: string; text?: string }> }).content;
252
261
  if (typeof content === "string") {
253
- chars = content.length;
262
+ fragments.push(content);
254
263
  } else if (Array.isArray(content)) {
255
264
  for (const block of content) {
256
265
  if (block.type === "text" && block.text) {
257
- chars += block.text.length;
266
+ fragments.push(block.text);
258
267
  }
259
268
  }
260
269
  }
261
- return Math.ceil(chars / 4);
270
+ break;
262
271
  }
263
272
  case "assistant": {
264
273
  const assistant = message as AssistantMessage;
265
274
  for (const block of assistant.content) {
266
275
  if (block.type === "text") {
267
- chars += block.text.length;
276
+ fragments.push(block.text);
268
277
  } else if (block.type === "thinking") {
269
- chars += block.thinking.length;
278
+ fragments.push(block.thinking);
270
279
  } else if (block.type === "toolCall") {
271
- chars += block.name.length + JSON.stringify(block.arguments).length;
280
+ fragments.push(block.name);
281
+ fragments.push(JSON.stringify(block.arguments));
272
282
  }
273
283
  }
274
- return Math.ceil(chars / 4);
284
+ break;
275
285
  }
276
286
  case "hookMessage":
277
287
  case "toolResult": {
278
288
  if (typeof message.content === "string") {
279
- chars = message.content.length;
289
+ fragments.push(message.content);
280
290
  } else {
281
291
  for (const block of message.content) {
282
292
  if (block.type === "text" && block.text) {
283
- chars += block.text.length;
284
- }
285
- if (block.type === "image") {
286
- chars += 4800; // Estimate images as 4000 chars, or 1200 tokens
293
+ fragments.push(block.text);
294
+ } else if (block.type === "image") {
295
+ extra += IMAGE_TOKEN_ESTIMATE;
287
296
  }
288
297
  }
289
298
  }
290
- return Math.ceil(chars / 4);
299
+ break;
291
300
  }
292
301
  case "bashExecution": {
293
- chars = message.command.length + message.output.length;
294
- return Math.ceil(chars / 4);
302
+ fragments.push(message.command);
303
+ fragments.push(message.output);
304
+ break;
295
305
  }
296
306
  case "branchSummary":
297
307
  case "compactionSummary": {
298
- chars = message.summary.length;
299
- return Math.ceil(chars / 4);
308
+ fragments.push(message.summary);
309
+ break;
300
310
  }
311
+ default:
312
+ return 0;
301
313
  }
302
314
 
303
- return 0;
315
+ if (fragments.length === 0) return extra;
316
+ return extra + countTokens(fragments);
304
317
  }
305
318
 
306
319
  function estimateEntriesTokens(entries: SessionEntry[], startIndex: number, endIndex: number): number {
@@ -114,6 +114,7 @@ export function formatSessionDumpText(options: FormatSessionDumpTextOptions): st
114
114
  if (c.type === "text") {
115
115
  lines.push(c.text);
116
116
  } else if (c.type === "thinking") {
117
+ if (c.thinking.trim().length === 0) continue;
117
118
  lines.push("<thinking>");
118
119
  lines.push(c.thinking);
119
120
  lines.push("</thinking>\n");
@@ -566,6 +566,14 @@ export function buildSessionContext(
566
566
  let hasPersistedMCPToolSelection = false;
567
567
  let mode = "none";
568
568
  let modeData: Record<string, unknown> | undefined;
569
+ // Track whether an explicit `model_change` with role="default" has been
570
+ // seen on this path. Once a user (or the agent itself) records an
571
+ // explicit default, later assistant-message inference must NOT overwrite
572
+ // it: temporary fallbacks (retry fallback, context promotion) and
573
+ // server-side model downgrades both produce assistant messages tagged
574
+ // with the wrong model id, which previously clobbered the user's pick on
575
+ // resume (issue #849).
576
+ let hasExplicitDefaultModel = false;
569
577
 
570
578
  for (const entry of path) {
571
579
  if (entry.type === "thinking_level_change") {
@@ -575,12 +583,21 @@ export function buildSessionContext(
575
583
  if (entry.model) {
576
584
  const role = entry.role ?? "default";
577
585
  models[role] = entry.model;
586
+ if (role === "default") {
587
+ hasExplicitDefaultModel = true;
588
+ }
578
589
  }
579
590
  } else if (entry.type === "service_tier_change") {
580
591
  serviceTier = entry.serviceTier ?? undefined;
581
592
  } else if (entry.type === "message" && entry.message.role === "assistant") {
582
- // Infer default model from assistant messages
583
- models.default = `${entry.message.provider}/${entry.message.model}`;
593
+ // Legacy fallback: infer default model from assistant messages only
594
+ // when no explicit `model_change` (role=default) entry has been
595
+ // recorded yet. Newer sessions always record an explicit default
596
+ // model_change at the start of the conversation, so this branch is
597
+ // only used to keep pre-model_change sessions working.
598
+ if (!hasExplicitDefaultModel) {
599
+ models.default = `${entry.message.provider}/${entry.message.model}`;
600
+ }
584
601
  } else if (entry.type === "compaction") {
585
602
  compaction = entry;
586
603
  } else if (entry.type === "ttsr_injection") {
@@ -123,11 +123,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
123
123
  },
124
124
  {
125
125
  name: "loop",
126
- description: "Loop the agent: re-submit the same prompt every time it yields (Esc to stop)",
127
- inlineHint: "<prompt>",
128
- allowArgs: true,
129
- handle: async (command, runtime) => {
130
- await runtime.ctx.handleLoopCommand(command.args || undefined);
126
+ description:
127
+ "Toggle loop mode. While enabled, the next prompt you send re-submits after every yield. Esc cancels the current iteration; /loop again to disable.",
128
+ handle: async (_command, runtime) => {
129
+ await runtime.ctx.handleLoopCommand();
131
130
  runtime.ctx.editor.setText("");
132
131
  },
133
132
  },
@@ -356,6 +355,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
356
355
  runtime.ctx.editor.setText("");
357
356
  },
358
357
  },
358
+ {
359
+ name: "context",
360
+ description: "Show estimated context usage breakdown",
361
+ handle: (_command, runtime) => {
362
+ runtime.ctx.handleContextCommand();
363
+ runtime.ctx.editor.setText("");
364
+ },
365
+ },
359
366
  {
360
367
  name: "extensions",
361
368
  aliases: ["status"],
package/src/tools/bash.ts CHANGED
@@ -508,12 +508,17 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
508
508
  const headLines = head;
509
509
  const tailLines = tail;
510
510
 
511
- // Check interception if enabled and available tools are known
511
+ // Check both the original command and the cwd-normalized command so
512
+ // leading `cd ... &&` wrappers do not hide either shell-navigation rules
513
+ // or the dedicated-tool command that follows the directory change.
512
514
  if (this.session.settings.get("bashInterceptor.enabled")) {
513
515
  const rules = this.session.settings.getBashInterceptorRules();
514
- const interception = checkBashInterception(command, ctx?.toolNames ?? [], rules);
515
- if (interception.block) {
516
- throw new ToolError(interception.message ?? "Command blocked");
516
+ const commandsToCheck = rawCommand === command ? [command] : [rawCommand, command];
517
+ for (const commandToCheck of commandsToCheck) {
518
+ const interception = checkBashInterception(commandToCheck, ctx?.toolNames ?? [], rules);
519
+ if (interception.block) {
520
+ throw new ToolError(interception.message ?? "Command blocked");
521
+ }
517
522
  }
518
523
  }
519
524
 
@@ -52,85 +52,72 @@ import { toolResult } from "./tool-result";
52
52
  import { clampTimeout } from "./tool-timeouts";
53
53
 
54
54
  const debugSchema = Type.Object({
55
- action: StringEnum(
56
- [
57
- "launch",
58
- "attach",
59
- "set_breakpoint",
60
- "remove_breakpoint",
61
- "set_instruction_breakpoint",
62
- "remove_instruction_breakpoint",
63
- "data_breakpoint_info",
64
- "set_data_breakpoint",
65
- "remove_data_breakpoint",
66
- "continue",
67
- "step_over",
68
- "step_in",
69
- "step_out",
70
- "pause",
71
- "evaluate",
72
- "stack_trace",
73
- "threads",
74
- "scopes",
75
- "variables",
76
- "disassemble",
77
- "read_memory",
78
- "write_memory",
79
- "modules",
80
- "loaded_sources",
81
- "custom_request",
82
- "output",
83
- "terminate",
84
- "sessions",
85
- ],
86
- { description: "dap debugger action" },
87
- ),
88
- program: Type.Optional(Type.String({ description: "program path", examples: ["./my_app", "src/main.py"] })),
89
- args: Type.Optional(Type.Array(Type.String(), { description: "program arguments", examples: [["--verbose"]] })),
90
- adapter: Type.Optional(
91
- Type.String({ description: "debugger adapter", examples: ["gdb", "lldb-dap", "debugpy", "dlv"] }),
92
- ),
93
- cwd: Type.Optional(Type.String({ description: "working directory", examples: ["src/"] })),
94
- file: Type.Optional(Type.String({ description: "source file", examples: ["src/main.c"] })),
95
- line: Type.Optional(Type.Number({ description: "source line", examples: [42] })),
96
- function: Type.Optional(Type.String({ description: "function name", examples: ["main", "handle_request"] })),
97
- name: Type.Optional(Type.String({ description: "variable or data name", examples: ["counter", "buffer"] })),
98
- condition: Type.Optional(Type.String({ description: "breakpoint condition", examples: ["i == 10", "x > 0"] })),
99
- hit_condition: Type.Optional(Type.String({ description: "hit condition" })),
100
- expression: Type.Optional(Type.String({ description: "expression to evaluate", examples: ["x + 1", "obj.field"] })),
55
+ action: StringEnum([
56
+ "launch",
57
+ "attach",
58
+ "set_breakpoint",
59
+ "remove_breakpoint",
60
+ "set_instruction_breakpoint",
61
+ "remove_instruction_breakpoint",
62
+ "data_breakpoint_info",
63
+ "set_data_breakpoint",
64
+ "remove_data_breakpoint",
65
+ "continue",
66
+ "step_over",
67
+ "step_in",
68
+ "step_out",
69
+ "pause",
70
+ "evaluate",
71
+ "stack_trace",
72
+ "threads",
73
+ "scopes",
74
+ "variables",
75
+ "disassemble",
76
+ "read_memory",
77
+ "write_memory",
78
+ "modules",
79
+ "loaded_sources",
80
+ "custom_request",
81
+ "output",
82
+ "terminate",
83
+ "sessions",
84
+ ]),
85
+ program: Type.Optional(Type.String({ description: "program path" })),
86
+ args: Type.Optional(Type.Array(Type.String(), { description: "program arguments" })),
87
+ adapter: Type.Optional(Type.String({ description: "debugger adapter (gdb, lldb-dap, debugpy, dlv)" })),
88
+ cwd: Type.Optional(Type.String()),
89
+ file: Type.Optional(Type.String({ description: "source file" })),
90
+ line: Type.Optional(Type.Number({ description: "source line" })),
91
+ function: Type.Optional(Type.String({ description: "function name" })),
92
+ name: Type.Optional(Type.String({ description: "variable or data name" })),
93
+ condition: Type.Optional(Type.String({ description: "breakpoint condition" })),
94
+ hit_condition: Type.Optional(Type.String()),
95
+ expression: Type.Optional(Type.String({ description: "expression to evaluate" })),
101
96
  context: Type.Optional(
102
- Type.String({ description: "evaluate context", examples: ["watch", "repl", "hover", "variables", "clipboard"] }),
97
+ Type.String({ description: "evaluate context: watch | repl | hover | variables | clipboard" }),
103
98
  ),
104
- frame_id: Type.Optional(Type.Number({ description: "stack frame id" })),
99
+ frame_id: Type.Optional(Type.Number()),
105
100
  scope_id: Type.Optional(Type.Number({ description: "scope variables reference" })),
106
101
  variable_ref: Type.Optional(Type.Number({ description: "variable reference" })),
107
- pid: Type.Optional(Type.Number({ description: "process id for attach", examples: [12345] })),
108
- port: Type.Optional(Type.Number({ description: "remote attach port", examples: [4711] })),
109
- host: Type.Optional(Type.String({ description: "remote attach host", examples: ["127.0.0.1"] })),
102
+ pid: Type.Optional(Type.Number({ description: "process id for attach" })),
103
+ port: Type.Optional(Type.Number({ description: "remote attach port" })),
104
+ host: Type.Optional(Type.String({ description: "remote attach host" })),
110
105
  levels: Type.Optional(Type.Number({ description: "max stack frames" })),
111
- memory_reference: Type.Optional(
112
- Type.String({ description: "memory reference or address", examples: ["0x7ffd1234"] }),
113
- ),
114
- instruction_reference: Type.Optional(Type.String({ description: "instruction address or reference" })),
115
- instruction_count: Type.Optional(Type.Number({ description: "instructions to disassemble" })),
116
- instruction_offset: Type.Optional(Type.Number({ description: "instruction offset" })),
106
+ memory_reference: Type.Optional(Type.String({ description: "memory reference or address" })),
107
+ instruction_reference: Type.Optional(Type.String()),
108
+ instruction_count: Type.Optional(Type.Number()),
109
+ instruction_offset: Type.Optional(Type.Number()),
117
110
  count: Type.Optional(Type.Number({ description: "bytes to read" })),
118
111
  data: Type.Optional(Type.String({ description: "base64 memory payload" })),
119
112
  data_id: Type.Optional(Type.String({ description: "data breakpoint id" })),
120
- access_type: Type.Optional(
121
- StringEnum(["read", "write", "readWrite"], { description: "data breakpoint access type" }),
122
- ),
113
+ access_type: Type.Optional(StringEnum(["read", "write", "readWrite"])),
123
114
  command: Type.Optional(Type.String({ description: "custom dap request command" })),
124
- arguments: Type.Optional(
125
- Type.Record(Type.String(), Type.Any(), {
126
- description: "custom request arguments",
127
- }),
128
- ),
129
- offset: Type.Optional(Type.Number({ description: "memory or instruction offset" })),
130
- resolve_symbols: Type.Optional(Type.Boolean({ description: "resolve symbols during disassembly" })),
131
- allow_partial: Type.Optional(Type.Boolean({ description: "allow partial writes" })),
132
- start_module: Type.Optional(Type.Number({ description: "modules start index" })),
133
- module_count: Type.Optional(Type.Number({ description: "max modules to fetch" })),
115
+ arguments: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "custom request arguments" })),
116
+ offset: Type.Optional(Type.Number()),
117
+ resolve_symbols: Type.Optional(Type.Boolean()),
118
+ allow_partial: Type.Optional(Type.Boolean()),
119
+ start_module: Type.Optional(Type.Number()),
120
+ module_count: Type.Optional(Type.Number()),
134
121
  timeout: Type.Optional(Type.Number({ description: "per-request timeout seconds" })),
135
122
  });
136
123