@oh-my-pi/pi-coding-agent 15.5.13 → 15.6.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 (192) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/types/cli/classify-install-target.d.ts +0 -10
  3. package/dist/types/cli/initial-message.d.ts +1 -1
  4. package/dist/types/cli/tiny-models-cli.d.ts +9 -0
  5. package/dist/types/commands/tiny-models.d.ts +22 -0
  6. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  7. package/dist/types/commit/analysis/summary.d.ts +1 -1
  8. package/dist/types/commit/changelog/generate.d.ts +1 -1
  9. package/dist/types/commit/changelog/index.d.ts +2 -2
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  12. package/dist/types/config/model-id-affixes.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +1 -1
  14. package/dist/types/config/models-config-schema.d.ts +2 -0
  15. package/dist/types/config/settings-schema.d.ts +233 -17
  16. package/dist/types/discovery/helpers.d.ts +1 -1
  17. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  18. package/dist/types/eval/__tests__/llm-bridge.test.d.ts +1 -0
  19. package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
  20. package/dist/types/eval/llm-bridge.d.ts +25 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +15 -0
  23. package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
  24. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
  25. package/dist/types/internal-urls/local-protocol.d.ts +2 -1
  26. package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
  27. package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
  28. package/dist/types/internal-urls/router.d.ts +8 -1
  29. package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
  30. package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
  31. package/dist/types/internal-urls/types.d.ts +26 -0
  32. package/dist/types/memory-backend/index.d.ts +1 -0
  33. package/dist/types/memory-backend/resolve.d.ts +2 -1
  34. package/dist/types/memory-backend/types.d.ts +7 -1
  35. package/dist/types/mnemosyne/backend.d.ts +4 -0
  36. package/dist/types/mnemosyne/config.d.ts +29 -0
  37. package/dist/types/mnemosyne/index.d.ts +3 -0
  38. package/dist/types/mnemosyne/state.d.ts +72 -0
  39. package/dist/types/modes/components/custom-editor.d.ts +2 -3
  40. package/dist/types/modes/components/hook-selector.d.ts +27 -0
  41. package/dist/types/modes/components/index.d.ts +1 -0
  42. package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
  43. package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
  44. package/dist/types/modes/components/welcome.d.ts +1 -0
  45. package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
  46. package/dist/types/modes/gradient-highlight.d.ts +23 -0
  47. package/dist/types/modes/interactive-mode.d.ts +4 -2
  48. package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
  49. package/dist/types/modes/orchestrate.d.ts +10 -0
  50. package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
  51. package/dist/types/modes/theme/theme.d.ts +2 -1
  52. package/dist/types/modes/ultrathink.d.ts +3 -3
  53. package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
  54. package/dist/types/sdk.d.ts +3 -0
  55. package/dist/types/session/agent-session.d.ts +35 -0
  56. package/dist/types/system-prompt.d.ts +2 -0
  57. package/dist/types/task/executor.d.ts +2 -0
  58. package/dist/types/task/render.d.ts +5 -1
  59. package/dist/types/tiny/models.d.ts +185 -0
  60. package/dist/types/tiny/text.d.ts +4 -0
  61. package/dist/types/tiny/title-client.d.ts +24 -0
  62. package/dist/types/tiny/title-protocol.d.ts +74 -0
  63. package/dist/types/tiny/worker.d.ts +2 -0
  64. package/dist/types/tools/bash.d.ts +3 -1
  65. package/dist/types/tools/index.d.ts +7 -4
  66. package/dist/types/tools/memory-edit.d.ts +40 -0
  67. package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
  68. package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
  69. package/dist/types/tools/memory-render.d.ts +60 -0
  70. package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
  71. package/dist/types/tools/todo-write.d.ts +8 -0
  72. package/dist/types/tools/tool-result.d.ts +2 -0
  73. package/dist/types/utils/title-generator.d.ts +3 -0
  74. package/package.json +18 -14
  75. package/scripts/build-binary.ts +1 -0
  76. package/src/cli/tiny-models-cli.ts +127 -0
  77. package/src/cli-commands.ts +1 -0
  78. package/src/cli.ts +8 -8
  79. package/src/commands/tiny-models.ts +36 -0
  80. package/src/config/model-equivalence.ts +43 -2
  81. package/src/config/model-id-affixes.ts +64 -0
  82. package/src/config/model-registry.ts +166 -8
  83. package/src/config/models-config-schema.ts +1 -1
  84. package/src/config/settings-schema.ts +206 -14
  85. package/src/edit/hashline/diff.ts +5 -7
  86. package/src/eval/__tests__/llm-bridge.test.ts +297 -0
  87. package/src/eval/__tests__/shared-executors.test.ts +36 -0
  88. package/src/eval/js/shared/local-module-loader.ts +13 -1
  89. package/src/eval/js/shared/prelude.txt +8 -0
  90. package/src/eval/js/shared/rewrite-imports.ts +31 -26
  91. package/src/eval/js/tool-bridge.ts +4 -0
  92. package/src/eval/llm-bridge.ts +181 -0
  93. package/src/eval/py/prelude.py +52 -31
  94. package/src/export/html/template.generated.ts +1 -1
  95. package/src/export/html/template.js +0 -13
  96. package/src/extensibility/plugins/legacy-pi-compat.ts +60 -23
  97. package/src/internal-urls/agent-protocol.ts +18 -1
  98. package/src/internal-urls/artifact-protocol.ts +19 -1
  99. package/src/internal-urls/docs-index.generated.ts +5 -4
  100. package/src/internal-urls/local-protocol.ts +14 -1
  101. package/src/internal-urls/memory-protocol.ts +6 -1
  102. package/src/internal-urls/omp-protocol.ts +5 -1
  103. package/src/internal-urls/router.ts +20 -1
  104. package/src/internal-urls/rule-protocol.ts +8 -1
  105. package/src/internal-urls/skill-protocol.ts +8 -1
  106. package/src/internal-urls/types.ts +27 -0
  107. package/src/lsp/render.ts +1 -1
  108. package/src/main.ts +4 -0
  109. package/src/mcp/oauth-flow.ts +2 -2
  110. package/src/memory-backend/index.ts +1 -0
  111. package/src/memory-backend/resolve.ts +4 -1
  112. package/src/memory-backend/types.ts +8 -1
  113. package/src/mnemosyne/backend.ts +374 -0
  114. package/src/mnemosyne/config.ts +160 -0
  115. package/src/mnemosyne/index.ts +3 -0
  116. package/src/mnemosyne/state.ts +548 -0
  117. package/src/modes/acp/acp-agent.ts +11 -6
  118. package/src/modes/components/agent-dashboard.ts +4 -4
  119. package/src/modes/components/custom-editor.ts +3 -2
  120. package/src/modes/components/diff.ts +2 -2
  121. package/src/modes/components/extensions/extension-list.ts +3 -2
  122. package/src/modes/components/footer.ts +5 -6
  123. package/src/modes/components/history-search.ts +3 -3
  124. package/src/modes/components/hook-selector.ts +94 -8
  125. package/src/modes/components/index.ts +1 -0
  126. package/src/modes/components/mcp-add-wizard.ts +3 -3
  127. package/src/modes/components/model-selector.ts +124 -26
  128. package/src/modes/components/oauth-selector.ts +3 -3
  129. package/src/modes/components/session-observer-overlay.ts +19 -13
  130. package/src/modes/components/session-selector.ts +3 -3
  131. package/src/modes/components/settings-defs.ts +7 -0
  132. package/src/modes/components/status-line/context-thresholds.ts +11 -0
  133. package/src/modes/components/status-line/presets.ts +1 -0
  134. package/src/modes/components/status-line/segments.ts +25 -2
  135. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  136. package/src/modes/components/tips.txt +12 -0
  137. package/src/modes/components/tool-execution.ts +67 -3
  138. package/src/modes/components/tree-selector.ts +3 -3
  139. package/src/modes/components/user-message-selector.ts +3 -3
  140. package/src/modes/components/welcome.ts +55 -1
  141. package/src/modes/controllers/command-controller.ts +16 -1
  142. package/src/modes/controllers/extension-ui-controller.ts +3 -1
  143. package/src/modes/controllers/input-controller.ts +57 -0
  144. package/src/modes/gradient-highlight.ts +70 -0
  145. package/src/modes/interactive-mode.ts +80 -196
  146. package/src/modes/internal-url-autocomplete.ts +143 -0
  147. package/src/modes/orchestrate.ts +36 -0
  148. package/src/modes/prompt-action-autocomplete.ts +12 -0
  149. package/src/modes/theme/theme.ts +7 -0
  150. package/src/modes/ultrathink.ts +9 -53
  151. package/src/modes/utils/keybinding-matchers.ts +11 -0
  152. package/src/prompts/system/memory-consolidation-system.md +8 -0
  153. package/src/prompts/system/memory-extraction-system.md +26 -0
  154. package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +5 -16
  155. package/src/prompts/system/system-prompt.md +2 -0
  156. package/src/prompts/system/tiny-title-system.md +8 -0
  157. package/src/prompts/tools/eval.md +2 -0
  158. package/src/prompts/tools/memory-edit.md +8 -0
  159. package/src/prompts/tools/task.md +4 -7
  160. package/src/sdk.ts +8 -6
  161. package/src/session/agent-session.ts +147 -44
  162. package/src/session/session-manager.ts +47 -0
  163. package/src/slash-commands/builtin-registry.ts +10 -1
  164. package/src/system-prompt.ts +4 -0
  165. package/src/task/commands.ts +1 -5
  166. package/src/task/executor.ts +8 -0
  167. package/src/task/index.ts +2 -0
  168. package/src/task/render.ts +69 -26
  169. package/src/tiny/models.ts +217 -0
  170. package/src/tiny/text.ts +19 -0
  171. package/src/tiny/title-client.ts +340 -0
  172. package/src/tiny/title-protocol.ts +51 -0
  173. package/src/tiny/worker.ts +523 -0
  174. package/src/tools/bash.ts +58 -16
  175. package/src/tools/browser/tab-worker.ts +1 -1
  176. package/src/tools/eval.ts +24 -48
  177. package/src/tools/index.ts +17 -15
  178. package/src/tools/memory-edit.ts +59 -0
  179. package/src/tools/memory-recall.ts +100 -0
  180. package/src/tools/memory-reflect.ts +88 -0
  181. package/src/tools/memory-render.ts +185 -0
  182. package/src/tools/memory-retain.ts +91 -0
  183. package/src/tools/renderers.ts +4 -2
  184. package/src/tools/todo-write.ts +128 -29
  185. package/src/tools/tool-result.ts +8 -0
  186. package/src/utils/title-generator.ts +115 -13
  187. package/dist/types/tools/calculator.d.ts +0 -77
  188. package/src/prompts/tools/calculator.md +0 -10
  189. package/src/tools/calculator.ts +0 -541
  190. package/src/tools/hindsight-recall.ts +0 -69
  191. package/src/tools/hindsight-reflect.ts +0 -58
  192. package/src/tools/hindsight-retain.ts +0 -57
@@ -35,6 +35,7 @@ import {
35
35
  import { APP_NAME, adjustHsv, getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@oh-my-pi/pi-utils";
36
36
  import chalk from "chalk";
37
37
  import { KeybindingsManager } from "../config/keybindings";
38
+ import { MODEL_ROLES, type ModelRole } from "../config/model-registry";
38
39
  import { isSettingsInitialized, Settings, settings } from "../config/settings";
39
40
  import type {
40
41
  ExtensionUIContext,
@@ -57,7 +58,7 @@ import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" wit
57
58
  import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
58
59
  type: "text",
59
60
  };
60
- import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
61
+ import type { AgentSession, AgentSessionEvent, ResolvedRoleModel } from "../session/agent-session";
61
62
  import { HistoryStorage } from "../session/history-storage";
62
63
  import type { SessionContext, SessionManager } from "../session/session-manager";
63
64
  import { getRecentSessions } from "../session/session-manager";
@@ -80,7 +81,7 @@ import { DynamicBorder } from "./components/dynamic-border";
80
81
  import type { EvalExecutionComponent } from "./components/eval-execution";
81
82
  import type { HookEditorComponent } from "./components/hook-editor";
82
83
  import type { HookInputComponent } from "./components/hook-input";
83
- import type { HookSelectorComponent } from "./components/hook-selector";
84
+ import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
84
85
  import { StatusLineComponent } from "./components/status-line";
85
86
  import type { ToolExecutionHandle } from "./components/tool-execution";
86
87
  import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
@@ -324,10 +325,6 @@ export class InteractiveMode implements InteractiveModeContext {
324
325
  #eventBus?: EventBus;
325
326
  #eventBusUnsubscribers: Array<() => void> = [];
326
327
  #welcomeComponent?: WelcomeComponent;
327
- #todoSpinnerInterval?: NodeJS.Timeout;
328
- #todoSpinnerFrame = 0;
329
- #todoClosingTimeout?: NodeJS.Timeout;
330
- #todoClosingState: "idle" | "playing" | "done" = "idle";
331
328
 
332
329
  constructor(
333
330
  session: AgentSession,
@@ -534,9 +531,9 @@ export class InteractiveMode implements InteractiveModeContext {
534
531
  this.#observerRegistry.onChange(() => {
535
532
  this.statusLine.setSubagentCount(this.#observerRegistry.getActiveSubagentCount());
536
533
  // Auto-checkmark todos whose matching subagent just succeeded, then
537
- // re-render so the running override (animated row when a subagent
538
- // is doing the work for a still-pending todo) updates as subagents
539
- // start, finish, or fail. Also handles spinner start/stop.
534
+ // re-render so the running override (the static "live" glyph when a
535
+ // subagent is doing the work for a still-pending todo) updates as
536
+ // subagents start, finish, or fail.
540
537
  this.#reconcileTodosWithSubagents();
541
538
  this.#renderTodoList();
542
539
  this.ui.requestRender();
@@ -849,7 +846,7 @@ export class InteractiveMode implements InteractiveModeContext {
849
846
  this.#pendingSubmissionDispose = undefined;
850
847
  }
851
848
  this.editor.setText("");
852
- this.ui.refreshNativeScrollbackIfDirty();
849
+ this.ui.refreshNativeScrollbackIfDirty({ allowUnknownViewport: true });
853
850
  this.ensureLoadingAnimation();
854
851
  this.ui.requestRender();
855
852
  return submission;
@@ -960,29 +957,19 @@ export class InteractiveMode implements InteractiveModeContext {
960
957
  this.renderSessionContext(context);
961
958
  }
962
959
 
963
- #formatTodoLine(todo: TodoItem, prefix: string, matched: boolean, spinnerOn: boolean): string {
960
+ #formatTodoLine(todo: TodoItem, prefix: string, matched: boolean): string {
964
961
  const checkbox = theme.checkbox;
965
962
  const marker = formatHudNoteMarker(todo.notes?.length ?? 0);
966
- const frames = theme.spinnerFrames;
967
- // When the spinner is ticking, use the current animated frame; otherwise
968
- // fall back to the static "running" glyph so in_progress rows still look
969
- // distinct from pending rows.
970
- const runningGlyph =
971
- spinnerOn && frames.length > 0
972
- ? (frames[this.#todoSpinnerFrame % frames.length] ?? theme.status.running)
973
- : theme.status.running;
974
963
  switch (todo.status) {
975
964
  case "completed":
976
- return (
977
- theme.fg("success", `${prefix}${theme.status.success} ${chalk.strikethrough(todo.content)}`) + marker
978
- );
965
+ return theme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(todo.content)}`) + marker;
979
966
  case "in_progress":
980
- return theme.fg("accent", `${prefix}${runningGlyph} ${todo.content}`) + marker;
967
+ return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
981
968
  case "abandoned":
982
969
  return theme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(todo.content)}`) + marker;
983
970
  default:
984
971
  if (matched) {
985
- return theme.fg("accent", `${prefix}${runningGlyph} ${todo.content}`) + marker;
972
+ return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
986
973
  }
987
974
  return theme.fg("dim", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
988
975
  }
@@ -1036,25 +1023,6 @@ export class InteractiveMode implements InteractiveModeContext {
1036
1023
  this.session.setTodoPhases(next);
1037
1024
  }
1038
1025
 
1039
- #updateTodoSpinnerAnimation(needSpinner: boolean): void {
1040
- if (needSpinner) {
1041
- if (this.#todoSpinnerInterval) return;
1042
- this.#todoSpinnerInterval = setInterval(() => {
1043
- const frames = theme.spinnerFrames;
1044
- if (frames.length === 0) return;
1045
- this.#todoSpinnerFrame = (this.#todoSpinnerFrame + 1) % frames.length;
1046
- // Rebuild the todo container so the new frame appears, then schedule
1047
- // a paint. The renderer self-stops the interval once no row needs it.
1048
- this.#renderTodoList();
1049
- this.ui.requestRender();
1050
- }, 80);
1051
- } else if (this.#todoSpinnerInterval) {
1052
- clearInterval(this.#todoSpinnerInterval);
1053
- this.#todoSpinnerInterval = undefined;
1054
- this.#todoSpinnerFrame = 0;
1055
- }
1056
- }
1057
-
1058
1026
  #getActivePhase(phases: TodoPhase[]): TodoPhase | undefined {
1059
1027
  const nonEmpty = phases.filter(phase => phase.tasks.length > 0);
1060
1028
  const active = nonEmpty.find(phase =>
@@ -1066,81 +1034,29 @@ export class InteractiveMode implements InteractiveModeContext {
1066
1034
  #renderTodoList(): void {
1067
1035
  this.todoContainer.clear();
1068
1036
  const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
1069
- if (phases.length === 0) {
1070
- this.#updateTodoSpinnerAnimation(false);
1071
- this.#stopTodoClosingAnimation();
1072
- this.#todoClosingState = "idle";
1073
- return;
1074
- }
1075
-
1076
- // When every visible task is completed or abandoned, fold the panel
1077
- // away with a brief celebratory animation (see
1078
- // #startTodoClosingAnimation). State machine guards against replaying
1079
- // on every re-render once the animation has finished.
1080
- const allClosed = phases.every(phase =>
1081
- phase.tasks.every(t => t.status === "completed" || t.status === "abandoned"),
1082
- );
1083
- if (allClosed) {
1084
- this.#updateTodoSpinnerAnimation(false);
1085
- if (this.#todoClosingState === "done") return;
1086
- if (this.#todoClosingState === "idle") this.#startTodoClosingAnimation(phases);
1087
- return;
1088
- }
1089
- // Any open task here means the close animation is no longer applicable.
1090
- this.#stopTodoClosingAnimation();
1091
- this.#todoClosingState = "idle";
1092
-
1037
+ if (phases.length === 0) return;
1093
1038
  const indent = " ";
1094
1039
  const hook = theme.tree.hook;
1095
1040
  const lines = ["", indent + theme.bold(theme.fg("accent", "Todos"))];
1096
1041
 
1097
1042
  const activeDescs = this.#getActiveSubagentDescriptions();
1098
- // Cache matcher results so we don't re-scan the description list per row
1099
- // twice (once for the spinner decision, once for the render).
1100
- const matchedSet = new Set<TodoItem>();
1101
- const isMatched = (todo: TodoItem): boolean => {
1102
- if (activeDescs.length === 0) return false;
1103
- if (matchedSet.has(todo)) return true;
1104
- if (todoMatchesAnyDescription(todo.content, activeDescs)) {
1105
- matchedSet.add(todo);
1106
- return true;
1107
- }
1108
- return false;
1109
- };
1110
-
1111
- // The cube animates whenever any visible open todo is "live":
1112
- // (a) status is in_progress (the agent itself is working it), or
1113
- // (b) a still-pending todo has a matching in-flight subagent doing
1114
- // the work for it. The renderer self-stops the interval once no row
1115
- // qualifies, so an orphan in_progress row at end-of-session keeps
1116
- // ticking — that's the intentional "this todo is still open" signal.
1117
- let needsSpinner = false;
1118
- const considerForSpinner = (todo: TodoItem): void => {
1119
- if (todo.status === "in_progress") {
1120
- needsSpinner = true;
1121
- return;
1122
- }
1123
- if (todo.status !== "pending") return;
1124
- if (isMatched(todo)) needsSpinner = true;
1125
- };
1043
+ // A pending todo "lights up" (accent + running glyph) when an in-flight
1044
+ // subagent is doing its work, matched by normalized content overlap.
1045
+ const isMatched = (todo: TodoItem): boolean =>
1046
+ activeDescs.length > 0 && todoMatchesAnyDescription(todo.content, activeDescs);
1126
1047
 
1127
1048
  if (!this.todoExpanded) {
1128
1049
  const activeIdx = phases.indexOf(this.#getActivePhase(phases) ?? phases[0]);
1129
1050
  const activePhase = phases[activeIdx];
1130
- if (!activePhase) {
1131
- this.#updateTodoSpinnerAnimation(false);
1132
- return;
1133
- }
1051
+ if (!activePhase) return;
1134
1052
  const { visible, hiddenOpenCount } = selectStickyTodoWindow(activePhase.tasks, 5);
1135
- for (const todo of visible) considerForSpinner(todo);
1136
- this.#updateTodoSpinnerAnimation(needsSpinner);
1137
1053
 
1138
1054
  lines.push(
1139
1055
  `${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(activePhase.name, activeIdx + 1)}`)}`,
1140
1056
  );
1141
1057
  visible.forEach((todo, index) => {
1142
1058
  const prefix = `${indent}${index === 0 ? hook : " "} `;
1143
- lines.push(this.#formatTodoLine(todo, prefix, matchedSet.has(todo), needsSpinner));
1059
+ lines.push(this.#formatTodoLine(todo, prefix, isMatched(todo)));
1144
1060
  });
1145
1061
  if (hiddenOpenCount > 0) {
1146
1062
  lines.push(theme.fg("muted", `${indent} ${hook} +${hiddenOpenCount} more`));
@@ -1149,101 +1065,17 @@ export class InteractiveMode implements InteractiveModeContext {
1149
1065
  return;
1150
1066
  }
1151
1067
 
1152
- for (const phase of phases) for (const todo of phase.tasks) considerForSpinner(todo);
1153
- this.#updateTodoSpinnerAnimation(needsSpinner);
1154
-
1155
1068
  phases.forEach((phase, phaseIndex) => {
1156
1069
  lines.push(`${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(phase.name, phaseIndex + 1)}`)}`);
1157
1070
  phase.tasks.forEach((todo, index) => {
1158
1071
  const prefix = `${indent}${index === 0 ? hook : " "} `;
1159
- lines.push(this.#formatTodoLine(todo, prefix, matchedSet.has(todo), needsSpinner));
1072
+ lines.push(this.#formatTodoLine(todo, prefix, isMatched(todo)));
1160
1073
  });
1161
1074
  });
1162
1075
 
1163
1076
  this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
1164
1077
  }
1165
1078
 
1166
- /**
1167
- * Play a short "all done" close animation: a celebratory bright frame,
1168
- * a brief dim transition, then a row-by-row vertical collapse until the
1169
- * panel is empty. Triggered from #renderTodoList exactly once per
1170
- * open-to-all-closed transition; #todoClosingState gates re-entry.
1171
- *
1172
- * While playing, the animator owns the panel container; #renderTodoList
1173
- * returns early. Subsequent renders with state === "done" keep the
1174
- * panel hidden until a fresh open task flips state back to "idle".
1175
- */
1176
- #startTodoClosingAnimation(phases: TodoPhase[]): void {
1177
- this.#stopTodoClosingAnimation();
1178
- this.#todoClosingState = "playing";
1179
-
1180
- const indent = " ";
1181
- const hook = theme.tree.hook;
1182
- const snapshot: string[] = ["", `${indent}Todos ${theme.status.success}`];
1183
- for (let i = 0; i < phases.length; i++) {
1184
- const phase = phases[i];
1185
- snapshot.push(`${indent}${hook} ${formatPhaseDisplayName(phase.name, i + 1)}`);
1186
- for (let j = 0; j < phase.tasks.length; j++) {
1187
- const task = phase.tasks[j];
1188
- const mark = task.status === "abandoned" ? theme.status.aborted : theme.status.success;
1189
- const prefix = `${indent}${j === 0 ? hook : " "} `;
1190
- snapshot.push(`${prefix}${mark} ${task.content}`);
1191
- }
1192
- }
1193
-
1194
- // Frame schedule (tint, drop-from-bottom, hold-ms). Frame 0 holds long
1195
- // enough for the user to actually read the final checkmarks before the
1196
- // fade starts; later frames fade and progressively drop rows from the
1197
- // bottom for the collapse effect. Total runtime ≈ 1.4s.
1198
- const frames = [
1199
- { tint: "success" as const, drop: 0, holdMs: 900 },
1200
- { tint: "success" as const, drop: 0, holdMs: 150 },
1201
- { tint: "muted" as const, drop: 1, holdMs: 90 },
1202
- { tint: "muted" as const, drop: 2, holdMs: 90 },
1203
- { tint: "dim" as const, drop: 3, holdMs: 80 },
1204
- { tint: "dim" as const, drop: 4, holdMs: 80 },
1205
- ];
1206
-
1207
- let frameIdx = 0;
1208
- const tick = (): void => {
1209
- if (this.#todoClosingState !== "playing") return;
1210
- if (frameIdx >= frames.length) {
1211
- this.todoContainer.clear();
1212
- this.#stopTodoClosingAnimation();
1213
- this.#todoClosingState = "done";
1214
- this.ui.requestRender();
1215
- return;
1216
- }
1217
- const { tint, drop, holdMs } = frames[frameIdx];
1218
- const visibleCount = Math.max(0, snapshot.length - drop);
1219
- this.todoContainer.clear();
1220
- if (visibleCount > 0) {
1221
- const visible = snapshot.slice(0, visibleCount);
1222
- const painted = visible.map((line, idx) => {
1223
- if (idx === 1) {
1224
- // Header row gets a bold flourish on the opening tick.
1225
- const colored = theme.fg(tint, line);
1226
- return frameIdx === 0 ? theme.bold(colored) : colored;
1227
- }
1228
- return theme.fg(tint, line);
1229
- });
1230
- this.todoContainer.addChild(new Text(painted.join("\n"), 1, 0));
1231
- }
1232
- this.ui.requestRender();
1233
- frameIdx++;
1234
- this.#todoClosingTimeout = setTimeout(tick, holdMs);
1235
- };
1236
-
1237
- tick();
1238
- }
1239
-
1240
- #stopTodoClosingAnimation(): void {
1241
- if (this.#todoClosingTimeout) {
1242
- clearTimeout(this.#todoClosingTimeout);
1243
- this.#todoClosingTimeout = undefined;
1244
- }
1245
- }
1246
-
1247
1079
  async #loadTodoList(): Promise<void> {
1248
1080
  this.todoPhases = this.session.getTodoPhases();
1249
1081
  this.#renderTodoList();
@@ -1767,6 +1599,20 @@ export class InteractiveMode implements InteractiveModeContext {
1767
1599
  }
1768
1600
  }
1769
1601
 
1602
+ async #applyPlanExecutionModel(entry: ResolvedRoleModel | undefined): Promise<void> {
1603
+ if (!entry) return;
1604
+ try {
1605
+ await this.session.applyRoleModel(entry);
1606
+ this.statusLine.invalidate();
1607
+ this.updateEditorBorderColor();
1608
+ this.showStatus(`Continuing with ${entry.role}: ${entry.model.name || entry.model.id}`);
1609
+ } catch (error) {
1610
+ this.showWarning(
1611
+ `Could not switch to the ${entry.role} model: ${error instanceof Error ? error.message : String(error)}`,
1612
+ );
1613
+ }
1614
+ }
1615
+
1770
1616
  async #approvePlan(
1771
1617
  planContent: string,
1772
1618
  options: {
@@ -1775,6 +1621,7 @@ export class InteractiveMode implements InteractiveModeContext {
1775
1621
  title: string;
1776
1622
  preserveContext?: boolean;
1777
1623
  compactBeforeExecute?: boolean;
1624
+ executionModel?: ResolvedRoleModel;
1778
1625
  },
1779
1626
  ): Promise<void> {
1780
1627
  await renameApprovedPlanFile({
@@ -1856,6 +1703,8 @@ export class InteractiveMode implements InteractiveModeContext {
1856
1703
  return;
1857
1704
  }
1858
1705
 
1706
+ await this.#applyPlanExecutionModel(options.executionModel);
1707
+
1859
1708
  // Approved plans land in a fresh (or compacted) session whose first user-visible
1860
1709
  // turn is the synthetic plan-approved prompt — that path bypasses the
1861
1710
  // input-controller's title generation. Seed an auto-name from the plan title
@@ -2166,20 +2015,46 @@ export class InteractiveMode implements InteractiveModeContext {
2166
2015
  }
2167
2016
 
2168
2017
  this.#renderPlanPreview(planContent, { append: true });
2018
+ const contextUsage = this.session.getContextUsage();
2019
+ const keepContextLabel =
2020
+ contextUsage?.percent != null
2021
+ ? `Approve and keep context (${contextUsage.percent.toFixed(1)}%)`
2022
+ : "Approve and keep context";
2023
+
2024
+ // Model-tier slider: let the operator pick which configured role model
2025
+ // (smol/default/slow/…) executes the approved plan. Left/right move it from
2026
+ // any list position. Hidden when fewer than two role models resolve — a lone
2027
+ // tier is no choice. `selectedTierIndex` tracks the live slider position.
2028
+ const cycle = this.session.getRoleModelCycle(this.session.settings.get("cycleOrder"));
2029
+ let selectedTierIndex = cycle?.currentIndex ?? 0;
2030
+ const slider: HookSelectorSlider | undefined =
2031
+ cycle && cycle.models.length > 1
2032
+ ? {
2033
+ caption: "continue with",
2034
+ index: cycle.currentIndex,
2035
+ segments: cycle.models.map(entry => ({
2036
+ label: entry.role,
2037
+ color: MODEL_ROLES[entry.role as ModelRole]?.color,
2038
+ detail: entry.model.name || entry.model.id,
2039
+ })),
2040
+ onChange: index => {
2041
+ selectedTierIndex = index;
2042
+ },
2043
+ }
2044
+ : undefined;
2045
+ const helpText = slider ? `${this.#getPlanReviewHelpText()} ◂/▸ model` : this.#getPlanReviewHelpText();
2046
+
2169
2047
  const choice = await this.showHookSelector(
2170
2048
  "Plan mode - next step",
2171
- ["Approve and execute", "Approve and compact context", "Approve and keep context", "Refine plan"],
2049
+ ["Approve and execute", "Approve and compact context", keepContextLabel, "Refine plan"],
2172
2050
  {
2173
- helpText: this.#getPlanReviewHelpText(),
2051
+ helpText,
2174
2052
  onExternalEditor: () => void this.#openPlanInExternalEditor(planFilePath),
2175
2053
  },
2054
+ { slider },
2176
2055
  );
2177
2056
 
2178
- if (
2179
- choice === "Approve and execute" ||
2180
- choice === "Approve and compact context" ||
2181
- choice === "Approve and keep context"
2182
- ) {
2057
+ if (choice === "Approve and execute" || choice === "Approve and compact context" || choice === keepContextLabel) {
2183
2058
  const finalPlanFilePath = details.finalPlanFilePath || planFilePath;
2184
2059
  try {
2185
2060
  const latestPlanContent = await this.#readPlanFile(planFilePath);
@@ -2187,12 +2062,21 @@ export class InteractiveMode implements InteractiveModeContext {
2187
2062
  this.showError(`Plan file not found at ${planFilePath}`);
2188
2063
  return;
2189
2064
  }
2065
+ // Capture the operator's tier choice and hand it to #approvePlan, which
2066
+ // applies it AFTER #exitPlanMode. #exitPlanMode restores
2067
+ // #planModePreviousModelState (the model from before plan mode), so
2068
+ // applying the slider choice any earlier would be silently reverted —
2069
+ // the bug that made "continue with slow" keep executing on the default
2070
+ // model. Deferred application also survives newSession()/compaction.
2071
+ const executionModel =
2072
+ cycle && selectedTierIndex !== cycle.currentIndex ? cycle.models[selectedTierIndex] : undefined;
2190
2073
  await this.#approvePlan(latestPlanContent, {
2191
2074
  planFilePath,
2192
2075
  finalPlanFilePath,
2193
2076
  title: details.title,
2194
2077
  preserveContext: choice !== "Approve and execute",
2195
2078
  compactBeforeExecute: choice === "Approve and compact context",
2079
+ executionModel,
2196
2080
  });
2197
2081
  } catch (error) {
2198
2082
  this.showError(
@@ -2265,7 +2149,6 @@ export class InteractiveMode implements InteractiveModeContext {
2265
2149
  this.loadingAnimation = undefined;
2266
2150
  }
2267
2151
  this.#cleanupMicAnimation();
2268
- this.#updateTodoSpinnerAnimation(false);
2269
2152
  this.#cancelGoalContinuation();
2270
2153
  if (this.#sttController) {
2271
2154
  this.#sttController.dispose();
@@ -2969,8 +2852,9 @@ export class InteractiveMode implements InteractiveModeContext {
2969
2852
  title: string,
2970
2853
  options: string[],
2971
2854
  dialogOptions?: ExtensionUIDialogOptions,
2855
+ extra?: { slider?: HookSelectorSlider },
2972
2856
  ): Promise<string | undefined> {
2973
- return this.#extensionUiController.showHookSelector(title, options, dialogOptions);
2857
+ return this.#extensionUiController.showHookSelector(title, options, dialogOptions, extra);
2974
2858
  }
2975
2859
 
2976
2860
  hideHookSelector(): void {
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Autocomplete for internal-url schemes (skill://, rule://, omp://, local://,
3
+ * memory://, agent://, artifact://) while composing a prompt.
4
+ *
5
+ * Detection here MUST stay in sync with the generic URL-scheme trigger in the
6
+ * TUI editor (`packages/tui/src/components/editor.ts`); the editor fires the
7
+ * popup, this module decides whether there are candidates to show.
8
+ */
9
+ import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
10
+ import { InternalUrlRouter } from "../internal-urls/router";
11
+
12
+ /** Upper bound on candidates surfaced in the dropdown. */
13
+ const MAX_URL_SUGGESTIONS = 25;
14
+
15
+ /**
16
+ * A URL token ending at the cursor: a known internal scheme followed by one or
17
+ * two slashes and the partially typed host/path. The boundary/rest character
18
+ * classes mirror the editor trigger so both agree on what counts as a token.
19
+ */
20
+ const URL_TOKEN_RE = /(?:^|[\s"'`(<=])([a-z][a-z0-9+.-]*:\/{1,2}[^\s"'`()<>]*)$/i;
21
+ const SCHEME_SPLIT_RE = /^([a-z][a-z0-9+.-]*):\/{1,2}(.*)$/i;
22
+
23
+ export interface InternalUrlContext {
24
+ /** Lowercased scheme (e.g. `local`). */
25
+ scheme: string;
26
+ /** Text typed after the slashes so far (host + path); may be empty. */
27
+ query: string;
28
+ /** Exact buffer token from its boundary to the cursor (the completion prefix). */
29
+ token: string;
30
+ }
31
+
32
+ // Subsequence fuzzy match: `hum` matches `humanizer`, `lp` matches `local-plan`.
33
+ function fuzzyMatch(query: string, target: string): boolean {
34
+ if (query.length === 0) return true;
35
+ if (query.length > target.length) return false;
36
+ let q = 0;
37
+ for (let t = 0; t < target.length && q < query.length; t += 1) {
38
+ if (query[q] === target[t]) q += 1;
39
+ }
40
+ return q === query.length;
41
+ }
42
+
43
+ // Higher is better: exact > prefix > substring > scattered subsequence.
44
+ function fuzzyScore(query: string, target: string): number {
45
+ if (query.length === 0) return 1;
46
+ if (target === query) return 100;
47
+ if (target.startsWith(query)) return 80;
48
+ if (target.includes(query)) return 60;
49
+ let q = 0;
50
+ let gaps = 0;
51
+ let last = -1;
52
+ for (let t = 0; t < target.length && q < query.length; t += 1) {
53
+ if (query[q] === target[t]) {
54
+ if (last >= 0 && t - last > 1) gaps += 1;
55
+ last = t;
56
+ q += 1;
57
+ }
58
+ }
59
+ if (q !== query.length) return 0;
60
+ return Math.max(1, 40 - gaps * 5);
61
+ }
62
+
63
+ /**
64
+ * Detect a completable internal-url token immediately before the cursor.
65
+ * Returns `null` when the text is not a `scheme://` token whose scheme is
66
+ * registered with a completion-capable handler.
67
+ */
68
+ export function extractInternalUrlContext(textBeforeCursor: string): InternalUrlContext | null {
69
+ const tokenMatch = URL_TOKEN_RE.exec(textBeforeCursor);
70
+ if (!tokenMatch) return null;
71
+ const token = tokenMatch[1]!;
72
+ const parts = SCHEME_SPLIT_RE.exec(token);
73
+ if (!parts) return null;
74
+ const scheme = parts[1]!.toLowerCase();
75
+ if (!InternalUrlRouter.instance().completionSchemes().includes(scheme)) return null;
76
+ return { scheme, query: parts[2] ?? "", token };
77
+ }
78
+
79
+ /**
80
+ * Suggestions for the internal-url token ending at the cursor, or `null` when
81
+ * the text is not such a token or no candidate matches the typed query.
82
+ */
83
+ export async function getInternalUrlSuggestions(
84
+ textBeforeCursor: string,
85
+ ): Promise<{ items: AutocompleteItem[]; prefix: string } | null> {
86
+ const ctx = extractInternalUrlContext(textBeforeCursor);
87
+ if (!ctx) return null;
88
+
89
+ const candidates = await InternalUrlRouter.instance().complete(ctx.scheme, ctx.query);
90
+ if (!candidates || candidates.length === 0) return null;
91
+
92
+ const query = ctx.query.toLowerCase();
93
+ const scored: Array<{ item: AutocompleteItem; score: number }> = [];
94
+ for (const candidate of candidates) {
95
+ const target = candidate.value.toLowerCase();
96
+ if (!fuzzyMatch(query, target)) continue;
97
+ scored.push({
98
+ item: {
99
+ value: `${ctx.scheme}://${candidate.value}`,
100
+ label: candidate.label ?? candidate.value,
101
+ ...(candidate.description ? { description: candidate.description } : {}),
102
+ },
103
+ score: fuzzyScore(query, target),
104
+ });
105
+ }
106
+ if (scored.length === 0) return null;
107
+
108
+ scored.sort((a, b) => b.score - a.score);
109
+ return {
110
+ items: scored.slice(0, MAX_URL_SUGGESTIONS).map(entry => entry.item),
111
+ prefix: ctx.token,
112
+ };
113
+ }
114
+
115
+ /** Whether `prefix` (the token a completion was offered for) is an internal-url token. */
116
+ export function isInternalUrlPrefix(prefix: string): boolean {
117
+ return extractInternalUrlContext(prefix) !== null;
118
+ }
119
+
120
+ /**
121
+ * Replace the internal-url token with the selected candidate, appending a
122
+ * trailing space (matching `@` file-reference behavior) so the user can keep
123
+ * typing.
124
+ */
125
+ export function applyInternalUrlCompletion(
126
+ lines: string[],
127
+ cursorLine: number,
128
+ cursorCol: number,
129
+ item: AutocompleteItem,
130
+ prefix: string,
131
+ ): { lines: string[]; cursorLine: number; cursorCol: number } {
132
+ const currentLine = lines[cursorLine] || "";
133
+ const beforePrefix = currentLine.slice(0, cursorCol - prefix.length);
134
+ const afterCursor = currentLine.slice(cursorCol);
135
+ const insert = `${item.value} `;
136
+ const newLines = [...lines];
137
+ newLines[cursorLine] = beforePrefix + insert + afterCursor;
138
+ return {
139
+ lines: newLines,
140
+ cursorLine,
141
+ cursorCol: beforePrefix.length + insert.length,
142
+ };
143
+ }
@@ -0,0 +1,36 @@
1
+ import orchestrateNotice from "../prompts/system/orchestrate-notice.md" with { type: "text" };
2
+ import { createGradientHighlighter } from "./gradient-highlight";
3
+
4
+ /**
5
+ * "orchestrate" keyword support.
6
+ *
7
+ * Typing the standalone word in the input editor paints it with a cool
8
+ * teal→violet gradient ({@link highlightOrchestrate}); submitting a message that
9
+ * mentions it appends a hidden {@link ORCHESTRATE_NOTICE} that switches the model
10
+ * into multi-agent orchestration mode. Matching is word-bounded and
11
+ * case-insensitive, so "orchestrated"/"orchestrating" never trigger either
12
+ * behavior. Replaces the former `/orchestrate` slash command.
13
+ */
14
+
15
+ // Detection: standalone keyword, any case. Non-global so `.test` stays stateless.
16
+ const ORCHESTRATE_WORD = /\borchestrate\b/i;
17
+
18
+ /** Hidden system notice appended after a user message that mentions "orchestrate". */
19
+ export const ORCHESTRATE_NOTICE: string = orchestrateNotice.trim();
20
+
21
+ /** Whether `text` contains the standalone keyword "orchestrate" (any case). */
22
+ export function containsOrchestrate(text: string): boolean {
23
+ return ORCHESTRATE_WORD.test(text);
24
+ }
25
+
26
+ /**
27
+ * Highlight every standalone "orchestrate" in `text` for editor display with a
28
+ * cool teal→violet gradient (hue 150..280), visually distinct from ultrathink's
29
+ * full-spectrum rainbow.
30
+ */
31
+ export const highlightOrchestrate: (text: string) => string = createGradientHighlighter({
32
+ probe: /orchestrate/i,
33
+ highlight: /\borchestrate\b/gi,
34
+ stops: 14,
35
+ hue: t => 150 + t * 130,
36
+ });
@@ -8,6 +8,11 @@ import {
8
8
  import { formatKeyHints, type KeybindingsManager } from "../config/keybindings";
9
9
  import { isSettingsInitialized, settings } from "../config/settings";
10
10
  import { applyEmojiCompletion, getEmojiSuggestions, isEmojiPrefix, tryEmojiInlineReplace } from "./emoji-autocomplete";
11
+ import {
12
+ applyInternalUrlCompletion,
13
+ getInternalUrlSuggestions,
14
+ isInternalUrlPrefix,
15
+ } from "./internal-url-autocomplete";
11
16
 
12
17
  interface PromptActionDefinition {
13
18
  id: string;
@@ -128,6 +133,9 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
128
133
  }
129
134
  }
130
135
 
136
+ const urlSuggestions = await getInternalUrlSuggestions(textBeforeCursor);
137
+ if (urlSuggestions) return urlSuggestions;
138
+
131
139
  if (!isSettingsInitialized() || settings.get("emojiAutocomplete")) {
132
140
  const emojiSuggestions = getEmojiSuggestions(textBeforeCursor);
133
141
  if (emojiSuggestions) return emojiSuggestions;
@@ -170,6 +178,10 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
170
178
  };
171
179
  }
172
180
 
181
+ if (isInternalUrlPrefix(prefix)) {
182
+ return applyInternalUrlCompletion(lines, cursorLine, cursorCol, item, prefix);
183
+ }
184
+
173
185
  if (isEmojiPrefix(prefix)) {
174
186
  return applyEmojiCompletion(lines, cursorLine, cursorCol, item, prefix);
175
187
  }