@openeryc/pi-coding-agent 0.75.56 → 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.
Files changed (35) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/core/keybindings.d.ts +10 -0
  3. package/dist/core/keybindings.d.ts.map +1 -1
  4. package/dist/core/keybindings.js +8 -0
  5. package/dist/core/keybindings.js.map +1 -1
  6. package/dist/core/model-registry.d.ts +6 -0
  7. package/dist/core/model-registry.d.ts.map +1 -1
  8. package/dist/core/model-registry.js +38 -0
  9. package/dist/core/model-registry.js.map +1 -1
  10. package/dist/modes/interactive/components/login-dialog.d.ts +7 -1
  11. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  12. package/dist/modes/interactive/components/login-dialog.js +14 -4
  13. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  14. package/dist/modes/interactive/components/model-hub.d.ts +2 -0
  15. package/dist/modes/interactive/components/model-hub.d.ts.map +1 -1
  16. package/dist/modes/interactive/components/model-hub.js +30 -8
  17. package/dist/modes/interactive/components/model-hub.js.map +1 -1
  18. package/dist/modes/interactive/goal-runner-utils.d.ts +11 -0
  19. package/dist/modes/interactive/goal-runner-utils.d.ts.map +1 -0
  20. package/dist/modes/interactive/goal-runner-utils.js +39 -0
  21. package/dist/modes/interactive/goal-runner-utils.js.map +1 -0
  22. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  23. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  24. package/dist/modes/interactive/interactive-mode.js +77 -29
  25. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  26. package/docs/models.md +1 -1
  27. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  28. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  29. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  30. package/examples/extensions/sandbox/package-lock.json +2 -2
  31. package/examples/extensions/sandbox/package.json +1 -1
  32. package/examples/extensions/with-deps/package-lock.json +2 -2
  33. package/examples/extensions/with-deps/package.json +1 -1
  34. package/npm-shrinkwrap.json +12 -12
  35. package/package.json +4 -4
@@ -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;
@@ -4003,11 +4008,10 @@ export class InteractiveMode {
4003
4008
  this.showModelHub("manage");
4004
4009
  return;
4005
4010
  }
4006
- // models.json apiKey is opaque (env/command/literal); do not surface the raw value
4007
4011
  existing = {
4008
4012
  providerId: options.providerId,
4009
4013
  baseUrl: listed.baseUrl ?? "",
4010
- apiKey: "",
4014
+ apiKey: listed.apiKey ?? "",
4011
4015
  models: listed.modelIds.map((id) => ({ id, name: id })),
4012
4016
  };
4013
4017
  }
@@ -4032,31 +4036,33 @@ export class InteractiveMode {
4032
4036
  this.ui.requestRender();
4033
4037
  };
4034
4038
  try {
4035
- let providerId = existing?.providerId ?? "";
4036
- if (!isEdit) {
4037
- const rawName = (await dialog.showPrompt("Provider name/id (e.g. ollama, agnes):", existing?.providerId ?? "ollama")).trim();
4038
- providerId = slugifyProviderId(rawName);
4039
- if (!providerId) {
4040
- throw new Error("Provider name cannot be empty.");
4041
- }
4042
- if (BUILT_IN_MODEL_PROVIDERS.has(providerId) && options.target === "openai_compatible") {
4043
- throw new Error(`"${providerId}" is a built-in provider. Use a different name, or add it via Manage → models.json override.`);
4044
- }
4045
- }
4046
- const baseUrl = (await dialog.showPrompt("Enter base URL:", existing?.baseUrl || "http://localhost:11434/v1")).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();
4042
+ const providerId = slugifyProviderId(rawName);
4043
+ if (!providerId) {
4044
+ throw new Error("Provider name cannot be empty.");
4045
+ }
4046
+ if (BUILT_IN_MODEL_PROVIDERS.has(providerId) && options.target === "openai_compatible") {
4047
+ throw new Error(`"${providerId}" is a built-in provider. Use a different name, or add it via Manage → models.json override.`);
4048
+ }
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();
4047
4052
  if (!baseUrl) {
4048
4053
  throw new Error("Base URL cannot be empty.");
4049
4054
  }
4050
4055
  const apiKeyPrompt = options.target === "models_json"
4051
- ? "API key (literal, env var name, or !command; blank = not-needed):"
4052
- : "Enter API key (or leave blank for local endpoints):";
4053
- const apiKey = (await dialog.showPrompt(apiKeyPrompt, existing?.apiKey === "not-needed" ? "" : (existing?.apiKey ?? ""))).trim();
4054
- // On models.json edit, blank keeps the existing apiKey field (not rewritten).
4055
- const effectiveApiKey = apiKey
4056
- ? apiKey
4057
- : isEdit && options.target === "models_json"
4058
- ? undefined
4059
- : "not-needed";
4056
+ ? isEdit
4057
+ ? "API key (literal/env/!command; blank keeps current):"
4058
+ : "API key (literal, env var name, or !command; blank = not-needed):"
4059
+ : isEdit
4060
+ ? "API key (blank keeps current; type not-needed for local):"
4061
+ : "Enter API key (or leave blank for local endpoints):";
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();
4064
+ // Blank on edit keeps the existing apiKey (models.json and openai_compatible).
4065
+ const effectiveApiKey = apiKey ? apiKey : isEdit ? undefined : "not-needed";
4060
4066
  dialog.showInfo([theme.fg("muted", "Discovering models from /models ...")]);
4061
4067
  this.ui.requestRender();
4062
4068
  const discovered = await fetchOpenAICompatibleModels(baseUrl, effectiveApiKey);
@@ -4123,7 +4129,7 @@ export class InteractiveMode {
4123
4129
  ]);
4124
4130
  this.ui.requestRender();
4125
4131
  const defaultIds = existing?.models.map((m) => m.id).join(", ") || "llama3.1";
4126
- 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();
4127
4133
  if (!modelNames) {
4128
4134
  throw new Error("At least one model name is required.");
4129
4135
  }
@@ -4134,8 +4140,8 @@ export class InteractiveMode {
4134
4140
  .map((id) => ({ id, name: id }));
4135
4141
  }
4136
4142
  if (options.target === "openai_compatible") {
4137
- const storedKey = effectiveApiKey ?? "not-needed";
4138
- // Replace previous id if renaming isn't supported (edit keeps id)
4143
+ const storedKey = effectiveApiKey ?? (isEdit && existing?.apiKey ? existing.apiKey : "not-needed");
4144
+ // Rename: drop old id if the name changed
4139
4145
  if (isEdit && existing && existing.providerId !== providerId) {
4140
4146
  this.session.modelRegistry.authStorage.remove(existing.providerId);
4141
4147
  }
@@ -4156,7 +4162,7 @@ export class InteractiveMode {
4156
4162
  await this.completeProviderAuthentication(providerId, baseUrl, "api_key", previousModel);
4157
4163
  }
4158
4164
  else {
4159
- this.session.modelRegistry.upsertProviderInModelsJson(providerId, {
4165
+ const writeConfig = {
4160
4166
  baseUrl,
4161
4167
  ...(effectiveApiKey !== undefined ? { apiKey: effectiveApiKey } : {}),
4162
4168
  api: "openai-completions",
@@ -4166,7 +4172,13 @@ export class InteractiveMode {
4166
4172
  reasoning: false,
4167
4173
  input: ["text"],
4168
4174
  })),
4169
- });
4175
+ };
4176
+ if (isEdit && existing && existing.providerId !== providerId) {
4177
+ this.session.modelRegistry.renameProviderInModelsJson(existing.providerId, providerId, writeConfig);
4178
+ }
4179
+ else {
4180
+ this.session.modelRegistry.upsertProviderInModelsJson(providerId, writeConfig);
4181
+ }
4170
4182
  restoreEditor();
4171
4183
  await this.updateAvailableProviderCount();
4172
4184
  // Prefer selecting first model if no real model yet
@@ -4601,6 +4613,9 @@ export class InteractiveMode {
4601
4613
  const lower = goal.toLowerCase();
4602
4614
  if (lower === "on" || lower === "off") {
4603
4615
  this.goalRunnerEnabled = lower === "on";
4616
+ if (lower === "on") {
4617
+ this.goalRunnerRecentOutputs = [];
4618
+ }
4604
4619
  const currentGoal = this.session.sessionGoal;
4605
4620
  if (currentGoal) {
4606
4621
  const label = lower === "on" ? theme.fg("success", "enabled") : theme.fg("warning", "paused");
@@ -4615,6 +4630,7 @@ export class InteractiveMode {
4615
4630
  }
4616
4631
  this.session.setSessionGoal(goal);
4617
4632
  this.goalRunnerEnabled = true;
4633
+ this.goalRunnerRecentOutputs = [];
4618
4634
  this.chatContainer.addChild(new Spacer(1));
4619
4635
  this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal set: ${goal}`), 1, 0));
4620
4636
  this.ui.requestRender();
@@ -4653,9 +4669,41 @@ export class InteractiveMode {
4653
4669
  this.showStatus(`Pursuing goal: "${goal}" (${keyText("app.interrupt")} to pause)`);
4654
4670
  }
4655
4671
  try {
4656
- 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.`);
4657
4673
  // Successful turn completed, reset retry counter
4658
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
+ }
4659
4707
  }
4660
4708
  catch (error) {
4661
4709
  this.goalRunnerRetryAttempt++;