@openeryc/pi-coding-agent 0.75.57 → 0.75.58

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.
@@ -62,6 +62,7 @@ import { ToolExecutionComponent } from "./components/tool-execution.js";
62
62
  import { TreeSelectorComponent } from "./components/tree-selector.js";
63
63
  import { UserMessageComponent } from "./components/user-message.js";
64
64
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
65
+ import { isNearDuplicateGoalOutput, normalizeGoalOutput } from "./goal-runner-utils.js";
65
66
  import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
66
67
  function isExpandable(obj) {
67
68
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
@@ -181,9 +182,13 @@ export class InteractiveMode {
181
182
  goalRunnerAbortController = undefined;
182
183
  goalRunnerRetryAttempt = 0;
183
184
  goalRunnerStatusDisplayed = false;
185
+ /** Normalized assistant texts from recent goal turns (for duplicate detection). */
186
+ goalRunnerRecentOutputs = [];
184
187
  GOAL_RUNNER_MAX_RETRIES = 10;
185
188
  GOAL_RUNNER_BASE_DELAY_MS = 3000;
186
189
  GOAL_RUNNER_TURN_DELAY_MS = 800;
190
+ /** Pause when this many consecutive turns look like near-duplicates. */
191
+ GOAL_RUNNER_MAX_DUPLICATE_STREAK = 3;
187
192
  // Extension UI state
188
193
  extensionSelector = undefined;
189
194
  extensionInput = undefined;
@@ -4031,7 +4036,9 @@ export class InteractiveMode {
4031
4036
  this.ui.requestRender();
4032
4037
  };
4033
4038
  try {
4034
- const rawName = (await dialog.showPrompt(isEdit ? "Provider name/id:" : "Provider name/id (e.g. ollama, agnes):", existing?.providerId ?? "ollama")).trim();
4039
+ const rawName = (await dialog.showPrompt(isEdit ? "Provider name/id:" : "Provider name/id (e.g. ollama, agnes):", isEdit
4040
+ ? { value: existing?.providerId ?? "" }
4041
+ : { placeholder: "ollama", value: existing?.providerId ?? "ollama" })).trim();
4035
4042
  const providerId = slugifyProviderId(rawName);
4036
4043
  if (!providerId) {
4037
4044
  throw new Error("Provider name cannot be empty.");
@@ -4039,7 +4046,9 @@ export class InteractiveMode {
4039
4046
  if (BUILT_IN_MODEL_PROVIDERS.has(providerId) && options.target === "openai_compatible") {
4040
4047
  throw new Error(`"${providerId}" is a built-in provider. Use a different name, or add it via Manage → models.json override.`);
4041
4048
  }
4042
- const baseUrl = (await dialog.showPrompt("Enter base URL:", existing?.baseUrl || "http://localhost:11434/v1")).trim();
4049
+ const baseUrl = (await dialog.showPrompt("Enter base URL:", isEdit || existing?.baseUrl
4050
+ ? { value: existing?.baseUrl || "http://localhost:11434/v1" }
4051
+ : { placeholder: "http://localhost:11434/v1", value: "http://localhost:11434/v1" })).trim();
4043
4052
  if (!baseUrl) {
4044
4053
  throw new Error("Base URL cannot be empty.");
4045
4054
  }
@@ -4050,7 +4059,8 @@ export class InteractiveMode {
4050
4059
  : isEdit
4051
4060
  ? "API key (blank keeps current; type not-needed for local):"
4052
4061
  : "Enter API key (or leave blank for local endpoints):";
4053
- const apiKey = (await dialog.showPrompt(apiKeyPrompt, existing?.apiKey === "not-needed" ? "" : (existing?.apiKey ?? ""))).trim();
4062
+ const apiKeyCurrent = existing?.apiKey === "not-needed" ? "" : (existing?.apiKey ?? "");
4063
+ const apiKey = (await dialog.showPrompt(apiKeyPrompt, isEdit || apiKeyCurrent ? { value: apiKeyCurrent } : { placeholder: "sk-... or leave blank" })).trim();
4054
4064
  // Blank on edit keeps the existing apiKey (models.json and openai_compatible).
4055
4065
  const effectiveApiKey = apiKey ? apiKey : isEdit ? undefined : "not-needed";
4056
4066
  dialog.showInfo([theme.fg("muted", "Discovering models from /models ...")]);
@@ -4119,7 +4129,7 @@ export class InteractiveMode {
4119
4129
  ]);
4120
4130
  this.ui.requestRender();
4121
4131
  const defaultIds = existing?.models.map((m) => m.id).join(", ") || "llama3.1";
4122
- const modelNames = (await dialog.showPrompt("Enter model name(s) (comma-separated):", defaultIds)).trim();
4132
+ const modelNames = (await dialog.showPrompt("Enter model name(s) (comma-separated):", { value: defaultIds })).trim();
4123
4133
  if (!modelNames) {
4124
4134
  throw new Error("At least one model name is required.");
4125
4135
  }
@@ -4603,6 +4613,9 @@ export class InteractiveMode {
4603
4613
  const lower = goal.toLowerCase();
4604
4614
  if (lower === "on" || lower === "off") {
4605
4615
  this.goalRunnerEnabled = lower === "on";
4616
+ if (lower === "on") {
4617
+ this.goalRunnerRecentOutputs = [];
4618
+ }
4606
4619
  const currentGoal = this.session.sessionGoal;
4607
4620
  if (currentGoal) {
4608
4621
  const label = lower === "on" ? theme.fg("success", "enabled") : theme.fg("warning", "paused");
@@ -4617,6 +4630,7 @@ export class InteractiveMode {
4617
4630
  }
4618
4631
  this.session.setSessionGoal(goal);
4619
4632
  this.goalRunnerEnabled = true;
4633
+ this.goalRunnerRecentOutputs = [];
4620
4634
  this.chatContainer.addChild(new Spacer(1));
4621
4635
  this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal set: ${goal}`), 1, 0));
4622
4636
  this.ui.requestRender();
@@ -4655,9 +4669,41 @@ export class InteractiveMode {
4655
4669
  this.showStatus(`Pursuing goal: "${goal}" (${keyText("app.interrupt")} to pause)`);
4656
4670
  }
4657
4671
  try {
4658
- await this.session.prompt(`Continue pursuing the goal: "${goal}". If you have fully completed this goal, report what was accomplished. If you cannot make further progress, explain what blocked you.`);
4672
+ await this.session.prompt(`Continue pursuing the goal: "${goal}". If you have fully completed this goal, report what was accomplished. If you cannot make further progress, explain what blocked you. Do not repeat the same status/report as previous turns — either make concrete progress or clearly state that the goal is done/blocked.`);
4659
4673
  // Successful turn completed, reset retry counter
4660
4674
  this.goalRunnerRetryAttempt = 0;
4675
+ // Auto-pause when the model keeps emitting near-duplicate content.
4676
+ const lastText = this.session.getLastAssistantText();
4677
+ if (lastText) {
4678
+ const normalized = normalizeGoalOutput(lastText);
4679
+ const prev = this.goalRunnerRecentOutputs[this.goalRunnerRecentOutputs.length - 1];
4680
+ if (prev && isNearDuplicateGoalOutput(prev, normalized)) {
4681
+ this.goalRunnerRecentOutputs.push(normalized);
4682
+ }
4683
+ else {
4684
+ this.goalRunnerRecentOutputs = [normalized];
4685
+ }
4686
+ // Keep a small window
4687
+ if (this.goalRunnerRecentOutputs.length > this.GOAL_RUNNER_MAX_DUPLICATE_STREAK) {
4688
+ this.goalRunnerRecentOutputs = this.goalRunnerRecentOutputs.slice(-this.GOAL_RUNNER_MAX_DUPLICATE_STREAK);
4689
+ }
4690
+ if (this.goalRunnerRecentOutputs.length >= this.GOAL_RUNNER_MAX_DUPLICATE_STREAK) {
4691
+ const window = this.goalRunnerRecentOutputs;
4692
+ let allDup = true;
4693
+ for (let i = 1; i < window.length; i++) {
4694
+ if (!isNearDuplicateGoalOutput(window[i - 1], window[i])) {
4695
+ allDup = false;
4696
+ break;
4697
+ }
4698
+ }
4699
+ if (allDup) {
4700
+ this.goalRunnerEnabled = false;
4701
+ this.goalRunnerRecentOutputs = [];
4702
+ this.showStatus(`Goal runner paused: model repeated similar output ${this.GOAL_RUNNER_MAX_DUPLICATE_STREAK} turns in a row. Use /goal on to resume, or refine the goal.`);
4703
+ break;
4704
+ }
4705
+ }
4706
+ }
4661
4707
  }
4662
4708
  catch (error) {
4663
4709
  this.goalRunnerRetryAttempt++;