@oh-my-pi/pi-coding-agent 15.5.15 → 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 (167) hide show
  1. package/CHANGELOG.md +46 -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/settings-schema.d.ts +232 -7
  14. package/dist/types/discovery/helpers.d.ts +1 -1
  15. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  16. package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
  17. package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
  18. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
  19. package/dist/types/internal-urls/local-protocol.d.ts +2 -1
  20. package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
  21. package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
  22. package/dist/types/internal-urls/router.d.ts +8 -1
  23. package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
  24. package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
  25. package/dist/types/internal-urls/types.d.ts +26 -0
  26. package/dist/types/memory-backend/index.d.ts +1 -0
  27. package/dist/types/memory-backend/resolve.d.ts +2 -1
  28. package/dist/types/memory-backend/types.d.ts +7 -1
  29. package/dist/types/mnemosyne/backend.d.ts +4 -0
  30. package/dist/types/mnemosyne/config.d.ts +29 -0
  31. package/dist/types/mnemosyne/index.d.ts +3 -0
  32. package/dist/types/mnemosyne/state.d.ts +72 -0
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -3
  34. package/dist/types/modes/components/hook-selector.d.ts +27 -0
  35. package/dist/types/modes/components/index.d.ts +1 -0
  36. package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
  37. package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
  38. package/dist/types/modes/components/welcome.d.ts +1 -0
  39. package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
  40. package/dist/types/modes/gradient-highlight.d.ts +23 -0
  41. package/dist/types/modes/interactive-mode.d.ts +4 -2
  42. package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
  43. package/dist/types/modes/orchestrate.d.ts +10 -0
  44. package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
  45. package/dist/types/modes/ultrathink.d.ts +3 -3
  46. package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
  47. package/dist/types/sdk.d.ts +3 -0
  48. package/dist/types/session/agent-session.d.ts +33 -0
  49. package/dist/types/system-prompt.d.ts +2 -0
  50. package/dist/types/task/executor.d.ts +2 -0
  51. package/dist/types/task/render.d.ts +5 -1
  52. package/dist/types/tiny/models.d.ts +185 -0
  53. package/dist/types/tiny/text.d.ts +4 -0
  54. package/dist/types/tiny/title-client.d.ts +24 -0
  55. package/dist/types/tiny/title-protocol.d.ts +74 -0
  56. package/dist/types/tiny/worker.d.ts +2 -0
  57. package/dist/types/tools/bash.d.ts +3 -1
  58. package/dist/types/tools/index.d.ts +7 -3
  59. package/dist/types/tools/memory-edit.d.ts +40 -0
  60. package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
  61. package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
  62. package/dist/types/tools/memory-render.d.ts +60 -0
  63. package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
  64. package/dist/types/tools/todo-write.d.ts +8 -0
  65. package/dist/types/tools/tool-result.d.ts +2 -0
  66. package/dist/types/utils/title-generator.d.ts +3 -0
  67. package/package.json +18 -14
  68. package/scripts/build-binary.ts +1 -0
  69. package/src/cli/tiny-models-cli.ts +127 -0
  70. package/src/cli-commands.ts +1 -0
  71. package/src/cli.ts +8 -8
  72. package/src/commands/tiny-models.ts +36 -0
  73. package/src/config/model-equivalence.ts +43 -2
  74. package/src/config/model-id-affixes.ts +64 -0
  75. package/src/config/model-registry.ts +84 -10
  76. package/src/config/settings-schema.ts +205 -4
  77. package/src/edit/hashline/diff.ts +5 -7
  78. package/src/eval/__tests__/shared-executors.test.ts +36 -0
  79. package/src/eval/js/shared/local-module-loader.ts +13 -1
  80. package/src/eval/js/shared/rewrite-imports.ts +31 -26
  81. package/src/internal-urls/agent-protocol.ts +18 -1
  82. package/src/internal-urls/artifact-protocol.ts +19 -1
  83. package/src/internal-urls/docs-index.generated.ts +3 -1
  84. package/src/internal-urls/local-protocol.ts +14 -1
  85. package/src/internal-urls/memory-protocol.ts +6 -1
  86. package/src/internal-urls/omp-protocol.ts +5 -1
  87. package/src/internal-urls/router.ts +20 -1
  88. package/src/internal-urls/rule-protocol.ts +8 -1
  89. package/src/internal-urls/skill-protocol.ts +8 -1
  90. package/src/internal-urls/types.ts +27 -0
  91. package/src/lsp/render.ts +1 -1
  92. package/src/mcp/oauth-flow.ts +2 -2
  93. package/src/memory-backend/index.ts +1 -0
  94. package/src/memory-backend/resolve.ts +4 -1
  95. package/src/memory-backend/types.ts +8 -1
  96. package/src/mnemosyne/backend.ts +374 -0
  97. package/src/mnemosyne/config.ts +160 -0
  98. package/src/mnemosyne/index.ts +3 -0
  99. package/src/mnemosyne/state.ts +548 -0
  100. package/src/modes/acp/acp-agent.ts +11 -6
  101. package/src/modes/components/agent-dashboard.ts +4 -4
  102. package/src/modes/components/custom-editor.ts +3 -2
  103. package/src/modes/components/diff.ts +2 -2
  104. package/src/modes/components/extensions/extension-list.ts +3 -2
  105. package/src/modes/components/footer.ts +5 -6
  106. package/src/modes/components/history-search.ts +3 -3
  107. package/src/modes/components/hook-selector.ts +94 -8
  108. package/src/modes/components/index.ts +1 -0
  109. package/src/modes/components/mcp-add-wizard.ts +3 -3
  110. package/src/modes/components/model-selector.ts +5 -4
  111. package/src/modes/components/oauth-selector.ts +3 -3
  112. package/src/modes/components/session-observer-overlay.ts +19 -13
  113. package/src/modes/components/session-selector.ts +3 -3
  114. package/src/modes/components/settings-defs.ts +7 -0
  115. package/src/modes/components/status-line/context-thresholds.ts +11 -0
  116. package/src/modes/components/status-line/segments.ts +2 -2
  117. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  118. package/src/modes/components/tips.txt +12 -0
  119. package/src/modes/components/tool-execution.ts +67 -3
  120. package/src/modes/components/tree-selector.ts +3 -3
  121. package/src/modes/components/user-message-selector.ts +3 -3
  122. package/src/modes/components/welcome.ts +55 -1
  123. package/src/modes/controllers/command-controller.ts +16 -1
  124. package/src/modes/controllers/extension-ui-controller.ts +3 -1
  125. package/src/modes/controllers/input-controller.ts +57 -0
  126. package/src/modes/gradient-highlight.ts +70 -0
  127. package/src/modes/interactive-mode.ts +58 -109
  128. package/src/modes/internal-url-autocomplete.ts +143 -0
  129. package/src/modes/orchestrate.ts +36 -0
  130. package/src/modes/prompt-action-autocomplete.ts +12 -0
  131. package/src/modes/ultrathink.ts +9 -53
  132. package/src/modes/utils/keybinding-matchers.ts +11 -0
  133. package/src/prompts/system/memory-consolidation-system.md +8 -0
  134. package/src/prompts/system/memory-extraction-system.md +26 -0
  135. package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +5 -16
  136. package/src/prompts/system/system-prompt.md +2 -0
  137. package/src/prompts/system/tiny-title-system.md +8 -0
  138. package/src/prompts/tools/memory-edit.md +8 -0
  139. package/src/prompts/tools/task.md +4 -7
  140. package/src/sdk.ts +8 -6
  141. package/src/session/agent-session.ts +128 -44
  142. package/src/slash-commands/builtin-registry.ts +10 -1
  143. package/src/system-prompt.ts +4 -0
  144. package/src/task/commands.ts +1 -5
  145. package/src/task/executor.ts +8 -0
  146. package/src/task/index.ts +2 -0
  147. package/src/task/render.ts +69 -26
  148. package/src/tiny/models.ts +217 -0
  149. package/src/tiny/text.ts +19 -0
  150. package/src/tiny/title-client.ts +340 -0
  151. package/src/tiny/title-protocol.ts +51 -0
  152. package/src/tiny/worker.ts +523 -0
  153. package/src/tools/bash.ts +58 -16
  154. package/src/tools/browser/tab-worker.ts +1 -1
  155. package/src/tools/index.ts +17 -11
  156. package/src/tools/memory-edit.ts +59 -0
  157. package/src/tools/memory-recall.ts +100 -0
  158. package/src/tools/memory-reflect.ts +88 -0
  159. package/src/tools/memory-render.ts +185 -0
  160. package/src/tools/memory-retain.ts +91 -0
  161. package/src/tools/renderers.ts +4 -0
  162. package/src/tools/todo-write.ts +128 -29
  163. package/src/tools/tool-result.ts +8 -0
  164. package/src/utils/title-generator.ts +115 -13
  165. package/src/tools/hindsight-recall.ts +0 -69
  166. package/src/tools/hindsight-reflect.ts +0 -58
  167. 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,8 +325,6 @@ export class InteractiveMode implements InteractiveModeContext {
324
325
  #eventBus?: EventBus;
325
326
  #eventBusUnsubscribers: Array<() => void> = [];
326
327
  #welcomeComponent?: WelcomeComponent;
327
- #todoClosingTimeout?: NodeJS.Timeout;
328
- #todoClosingState: "idle" | "playing" | "done" = "idle";
329
328
 
330
329
  constructor(
331
330
  session: AgentSession,
@@ -1035,28 +1034,7 @@ export class InteractiveMode implements InteractiveModeContext {
1035
1034
  #renderTodoList(): void {
1036
1035
  this.todoContainer.clear();
1037
1036
  const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
1038
- if (phases.length === 0) {
1039
- this.#stopTodoClosingAnimation();
1040
- this.#todoClosingState = "idle";
1041
- return;
1042
- }
1043
-
1044
- // When every visible task is completed or abandoned, fold the panel
1045
- // away with a brief celebratory animation (see
1046
- // #startTodoClosingAnimation). State machine guards against replaying
1047
- // on every re-render once the animation has finished.
1048
- const allClosed = phases.every(phase =>
1049
- phase.tasks.every(t => t.status === "completed" || t.status === "abandoned"),
1050
- );
1051
- if (allClosed) {
1052
- if (this.#todoClosingState === "done") return;
1053
- if (this.#todoClosingState === "idle") this.#startTodoClosingAnimation(phases);
1054
- return;
1055
- }
1056
- // Any open task here means the close animation is no longer applicable.
1057
- this.#stopTodoClosingAnimation();
1058
- this.#todoClosingState = "idle";
1059
-
1037
+ if (phases.length === 0) return;
1060
1038
  const indent = " ";
1061
1039
  const hook = theme.tree.hook;
1062
1040
  const lines = ["", indent + theme.bold(theme.fg("accent", "Todos"))];
@@ -1098,87 +1076,6 @@ export class InteractiveMode implements InteractiveModeContext {
1098
1076
  this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
1099
1077
  }
1100
1078
 
1101
- /**
1102
- * Play a short "all done" close animation: a celebratory bright frame,
1103
- * a brief dim transition, then a row-by-row vertical collapse until the
1104
- * panel is empty. Triggered from #renderTodoList exactly once per
1105
- * open-to-all-closed transition; #todoClosingState gates re-entry.
1106
- *
1107
- * While playing, the animator owns the panel container; #renderTodoList
1108
- * returns early. Subsequent renders with state === "done" keep the
1109
- * panel hidden until a fresh open task flips state back to "idle".
1110
- */
1111
- #startTodoClosingAnimation(phases: TodoPhase[]): void {
1112
- this.#stopTodoClosingAnimation();
1113
- this.#todoClosingState = "playing";
1114
-
1115
- const indent = " ";
1116
- const hook = theme.tree.hook;
1117
- const snapshot: string[] = ["", `${indent}Todos ${theme.status.success}`];
1118
- for (let i = 0; i < phases.length; i++) {
1119
- const phase = phases[i];
1120
- snapshot.push(`${indent}${hook} ${formatPhaseDisplayName(phase.name, i + 1)}`);
1121
- for (let j = 0; j < phase.tasks.length; j++) {
1122
- const task = phase.tasks[j];
1123
- const mark = task.status === "abandoned" ? theme.status.aborted : theme.status.success;
1124
- const prefix = `${indent}${j === 0 ? hook : " "} `;
1125
- snapshot.push(`${prefix}${mark} ${task.content}`);
1126
- }
1127
- }
1128
-
1129
- // Frame schedule (tint, drop-from-bottom, hold-ms). Frame 0 holds long
1130
- // enough for the user to actually read the final checkmarks before the
1131
- // fade starts; later frames fade and progressively drop rows from the
1132
- // bottom for the collapse effect. Total runtime ≈ 1.4s.
1133
- const frames = [
1134
- { tint: "success" as const, drop: 0, holdMs: 900 },
1135
- { tint: "success" as const, drop: 0, holdMs: 150 },
1136
- { tint: "muted" as const, drop: 1, holdMs: 90 },
1137
- { tint: "muted" as const, drop: 2, holdMs: 90 },
1138
- { tint: "dim" as const, drop: 3, holdMs: 80 },
1139
- { tint: "dim" as const, drop: 4, holdMs: 80 },
1140
- ];
1141
-
1142
- let frameIdx = 0;
1143
- const tick = (): void => {
1144
- if (this.#todoClosingState !== "playing") return;
1145
- if (frameIdx >= frames.length) {
1146
- this.todoContainer.clear();
1147
- this.#stopTodoClosingAnimation();
1148
- this.#todoClosingState = "done";
1149
- this.ui.requestRender();
1150
- return;
1151
- }
1152
- const { tint, drop, holdMs } = frames[frameIdx];
1153
- const visibleCount = Math.max(0, snapshot.length - drop);
1154
- this.todoContainer.clear();
1155
- if (visibleCount > 0) {
1156
- const visible = snapshot.slice(0, visibleCount);
1157
- const painted = visible.map((line, idx) => {
1158
- if (idx === 1) {
1159
- // Header row gets a bold flourish on the opening tick.
1160
- const colored = theme.fg(tint, line);
1161
- return frameIdx === 0 ? theme.bold(colored) : colored;
1162
- }
1163
- return theme.fg(tint, line);
1164
- });
1165
- this.todoContainer.addChild(new Text(painted.join("\n"), 1, 0));
1166
- }
1167
- this.ui.requestRender();
1168
- frameIdx++;
1169
- this.#todoClosingTimeout = setTimeout(tick, holdMs);
1170
- };
1171
-
1172
- tick();
1173
- }
1174
-
1175
- #stopTodoClosingAnimation(): void {
1176
- if (this.#todoClosingTimeout) {
1177
- clearTimeout(this.#todoClosingTimeout);
1178
- this.#todoClosingTimeout = undefined;
1179
- }
1180
- }
1181
-
1182
1079
  async #loadTodoList(): Promise<void> {
1183
1080
  this.todoPhases = this.session.getTodoPhases();
1184
1081
  this.#renderTodoList();
@@ -1702,6 +1599,20 @@ export class InteractiveMode implements InteractiveModeContext {
1702
1599
  }
1703
1600
  }
1704
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
+
1705
1616
  async #approvePlan(
1706
1617
  planContent: string,
1707
1618
  options: {
@@ -1710,6 +1621,7 @@ export class InteractiveMode implements InteractiveModeContext {
1710
1621
  title: string;
1711
1622
  preserveContext?: boolean;
1712
1623
  compactBeforeExecute?: boolean;
1624
+ executionModel?: ResolvedRoleModel;
1713
1625
  },
1714
1626
  ): Promise<void> {
1715
1627
  await renameApprovedPlanFile({
@@ -1791,6 +1703,8 @@ export class InteractiveMode implements InteractiveModeContext {
1791
1703
  return;
1792
1704
  }
1793
1705
 
1706
+ await this.#applyPlanExecutionModel(options.executionModel);
1707
+
1794
1708
  // Approved plans land in a fresh (or compacted) session whose first user-visible
1795
1709
  // turn is the synthetic plan-approved prompt — that path bypasses the
1796
1710
  // input-controller's title generation. Seed an auto-name from the plan title
@@ -2106,13 +2020,38 @@ export class InteractiveMode implements InteractiveModeContext {
2106
2020
  contextUsage?.percent != null
2107
2021
  ? `Approve and keep context (${contextUsage.percent.toFixed(1)}%)`
2108
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
+
2109
2047
  const choice = await this.showHookSelector(
2110
2048
  "Plan mode - next step",
2111
2049
  ["Approve and execute", "Approve and compact context", keepContextLabel, "Refine plan"],
2112
2050
  {
2113
- helpText: this.#getPlanReviewHelpText(),
2051
+ helpText,
2114
2052
  onExternalEditor: () => void this.#openPlanInExternalEditor(planFilePath),
2115
2053
  },
2054
+ { slider },
2116
2055
  );
2117
2056
 
2118
2057
  if (choice === "Approve and execute" || choice === "Approve and compact context" || choice === keepContextLabel) {
@@ -2123,12 +2062,21 @@ export class InteractiveMode implements InteractiveModeContext {
2123
2062
  this.showError(`Plan file not found at ${planFilePath}`);
2124
2063
  return;
2125
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;
2126
2073
  await this.#approvePlan(latestPlanContent, {
2127
2074
  planFilePath,
2128
2075
  finalPlanFilePath,
2129
2076
  title: details.title,
2130
2077
  preserveContext: choice !== "Approve and execute",
2131
2078
  compactBeforeExecute: choice === "Approve and compact context",
2079
+ executionModel,
2132
2080
  });
2133
2081
  } catch (error) {
2134
2082
  this.showError(
@@ -2904,8 +2852,9 @@ export class InteractiveMode implements InteractiveModeContext {
2904
2852
  title: string,
2905
2853
  options: string[],
2906
2854
  dialogOptions?: ExtensionUIDialogOptions,
2855
+ extra?: { slider?: HookSelectorSlider },
2907
2856
  ): Promise<string | undefined> {
2908
- return this.#extensionUiController.showHookSelector(title, options, dialogOptions);
2857
+ return this.#extensionUiController.showHookSelector(title, options, dialogOptions, extra);
2909
2858
  }
2910
2859
 
2911
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
  }
@@ -1,5 +1,5 @@
1
1
  import ultrathinkNotice from "../prompts/system/ultrathink-notice.md" with { type: "text" };
2
- import { theme } from "./theme/theme";
2
+ import { createGradientHighlighter } from "./gradient-highlight";
3
3
 
4
4
  /**
5
5
  * "ultrathink" keyword support, mirroring Claude Code's affordance.
@@ -11,12 +11,8 @@ import { theme } from "./theme/theme";
11
11
  * "ultrathinking"/"ultrathinks" never trigger either behavior.
12
12
  */
13
13
 
14
- // Cheap, stateless presence probe used to skip the boundary regex on most lines.
15
- const ULTRATHINK_PROBE = /ultrathink/i;
16
14
  // Detection: standalone keyword, any case. Non-global so `.test` stays stateless.
17
15
  const ULTRATHINK_WORD = /\bultrathink\b/i;
18
- // Highlight: global so `.replace` walks every occurrence.
19
- const ULTRATHINK_HIGHLIGHT = /\bultrathink\b/gi;
20
16
 
21
17
  /** Hidden system notice appended after a user message that mentions "ultrathink". */
22
18
  export const ULTRATHINK_NOTICE: string = ultrathinkNotice.trim();
@@ -26,54 +22,14 @@ export function containsUltrathink(text: string): boolean {
26
22
  return ULTRATHINK_WORD.test(text);
27
23
  }
28
24
 
29
- const FG_RESET = "\x1b[39m";
30
- // Hue stops swept across the visible spectrum. More stops than the keyword has
31
- // letters so the gradient resolves smoothly regardless of casing/match length.
32
- const RAINBOW_STOPS = 14;
33
-
34
- let cachedMode: string | undefined;
35
- let cachedPalette: readonly string[] | undefined;
36
-
37
- /** Rainbow foreground escapes for the active color mode, compiled once per mode. */
38
- function rainbowPalette(): readonly string[] {
39
- const mode = theme.getColorMode();
40
- if (cachedPalette && cachedMode === mode) return cachedPalette;
41
- const format = mode === "truecolor" ? "ansi-16m" : "ansi-256";
42
- const palette: string[] = [];
43
- for (let i = 0; i < RAINBOW_STOPS; i++) {
44
- // Sweep red→violet (0..330°), stopping short of the wrap back to red.
45
- const hue = Math.round((i / RAINBOW_STOPS) * 330);
46
- palette.push(Bun.color(`hsl(${hue}, 90%, 62%)`, format) ?? "");
47
- }
48
- cachedMode = mode;
49
- cachedPalette = palette;
50
- return palette;
51
- }
52
-
53
- /** Paint each character of `word` with the next rainbow stop, resetting fg after. */
54
- function rainbow(word: string): string {
55
- const palette = rainbowPalette();
56
- const n = word.length;
57
- let out = "";
58
- let prev = "";
59
- for (let i = 0; i < n; i++) {
60
- const color = palette[Math.floor((i / n) * palette.length)] ?? palette[0] ?? "";
61
- // Coalesce consecutive characters that resolve to the same stop.
62
- if (color !== prev) {
63
- out += color;
64
- prev = color;
65
- }
66
- out += word[i];
67
- }
68
- return `${out}${FG_RESET}`;
69
- }
70
-
71
25
  /**
72
26
  * Rainbow-highlight every standalone "ultrathink" in `text` for editor display.
73
- * Adds only zero-width SGR escapes the visible width is unchanged and returns
74
- * the input untouched when the keyword is absent.
27
+ * Sweeps red→violet (hue 0..330), stopping short of the wrap back to red so the
28
+ * gradient resolves smoothly regardless of casing or match length.
75
29
  */
76
- export function highlightUltrathink(text: string): string {
77
- if (!ULTRATHINK_PROBE.test(text)) return text;
78
- return text.replace(ULTRATHINK_HIGHLIGHT, rainbow);
79
- }
30
+ export const highlightUltrathink: (text: string) => string = createGradientHighlighter({
31
+ probe: /ultrathink/i,
32
+ highlight: /\bultrathink\b/gi,
33
+ stops: 14,
34
+ hue: t => t * 330,
35
+ });
@@ -16,10 +16,21 @@ export function matchesAppInterrupt(data: string): boolean {
16
16
  return matchesKey(data, "escape") || matchesKey(data, "esc");
17
17
  }
18
18
 
19
+ /** Match the generic selector cancel keybinding. */
19
20
  export function matchesSelectCancel(data: string): boolean {
20
21
  return getKeybindings().matches(data, "tui.select.cancel");
21
22
  }
22
23
 
24
+ /** Match the generic selector up-navigation keybinding. */
25
+ export function matchesSelectUp(data: string): boolean {
26
+ return getKeybindings().matches(data, "tui.select.up");
27
+ }
28
+
29
+ /** Match the generic selector down-navigation keybinding. */
30
+ export function matchesSelectDown(data: string): boolean {
31
+ return getKeybindings().matches(data, "tui.select.down");
32
+ }
33
+
23
34
  export function matchesAppExternalEditor(data: string): boolean {
24
35
  const keybindings = getKeybindings();
25
36
  const externalEditorKeys = keybindings.getKeys("app.editor.external");
@@ -0,0 +1,8 @@
1
+ Summarize the memories below into 1-3 concise sentences.
2
+
3
+ Preserve every fact, name, number, version, date, and decision exactly. Merge duplicates and near-duplicates; never repeat the same point. When memories conflict, state only the most recent as current. Do not invent, infer, or add anything that is not present in the memories. Output only the summary sentences, nothing else.
4
+
5
+ Memories:
6
+ {memories}
7
+
8
+ Summary:
@@ -0,0 +1,26 @@
1
+ Extract durable, long-term memory items from the user message below.
2
+
3
+ Output ONE item per line as a short plain-text statement: no JSON, no bullets, no numbering, no field labels.
4
+ Capture only persistent, reusable information:
5
+ - facts (name, role, employer, config, ports, versions, numbers)
6
+ - explicit instructions to the assistant
7
+ - stable preferences
8
+ - dated events or deadlines
9
+
10
+ Keep names, numbers, versions, and dates exact, in the message's original language. When a value is updated, output only the latest value. Ignore greetings, acknowledgements, small talk, weather, and one-off remarks.
11
+ If nothing qualifies, output exactly: NO_FACTS
12
+
13
+ Example
14
+ Message: My name is Sam, I work at Globex, and I always use 2-space indents.
15
+ Items:
16
+ name is Sam
17
+ works at Globex
18
+ prefers 2-space indents
19
+
20
+ Example
21
+ Message: lol nice weather today, might grab a coffee later
22
+ Items:
23
+ NO_FACTS
24
+
25
+ Message: {text}
26
+ Items:
@@ -1,17 +1,5 @@
1
- ---
2
- name: orchestrate
3
- description: Drive a multi-phase task to completion via parallel subagents
4
- ---
5
-
6
- # Task
7
-
8
- $@
9
-
10
- ---
11
-
12
- # Orchestration Contract
13
-
14
- You are the **orchestrator** for the task above. Read it once, then execute under the rules below. The contract overrides any default tendency to yield early, narrate, or do work yourself.
1
+ <system-notice>
2
+ The user's message above is an **orchestration request**. Execute it as the orchestrator under the contract below. This contract overrides any default tendency to yield early, narrate, or do the work yourself.
15
3
 
16
4
  <role>
17
5
  You decompose, dispatch, verify, and iterate. You do **not** edit code. Every file mutation goes through a `task` subagent. Your tool budget is: reading for planning, `task` for dispatch, verification (`bun check`, `bun test`, `recipe`, `lsp diagnostics`), git via `bash`, and `todo_write` for tracking.
@@ -19,11 +7,11 @@ You decompose, dispatch, verify, and iterate. You do **not** edit code. Every fi
19
7
 
20
8
  <rules>
21
9
  1. **Do not yield until everything is closed.** A phase finishing is *not* a yield point — launch the next phase in the same turn. Stop only when every requested item is verifiably done, or you hit a concrete [blocked] state that genuinely requires the user.
22
- 2. **Enumerate the full surface before dispatching.** If the task references audits, plans, checklists, phase lists, or file lists, expand them into a flat set of items in `todo_write`. "Most of them" or "the important ones" is failure. Re-read the source documents — do not work from memory.
10
+ 2. **Enumerate the full surface before dispatching.** If the request references audits, plans, checklists, phase lists, or file lists, expand them into a flat set of items in `todo_write`. "Most of them" or "the important ones" is failure. Re-read the source documents — do not work from memory.
23
11
  3. **Parallelize maximally.** Every set of edits with disjoint file scope MUST ship as one `task` batch. Serialize only when one subagent produces a contract (types, schema, shared module) the next consumes — and state the dependency when you do.
24
12
  4. **Each `task` assignment is self-contained.** Subagents have no shared context. Spell out: target files (≤3–5 explicit paths, no globs), the change with APIs and patterns, edge cases, and observable acceptance criteria. Do not assume they read the same plan you did.
25
13
  5. **Verify after every phase before launching the next.** Run the appropriate gate: `bun check` for types, package-scoped `bun test` for behavior, `lsp diagnostics` for changed files. If a phase introduced breakage, dispatch fix-up subagents *before* moving on. Never declare a phase done on a red tree.
26
- 6. **Commit policy.** If the task asks for commits or the repo workflow expects them, commit after each green phase with a focused message. Never commit a red tree. Never commit work the user did not ask to commit.
14
+ 6. **Commit policy.** If the request asks for commits or the repo workflow expects them, commit after each green phase with a focused message. Never commit a red tree. Never commit work the user did not ask to commit.
27
15
  7. **Respawn, do not absorb.** If a subagent returns incomplete or wrong work, spawn a corrective subagent with the specific gap — do not silently fix it yourself.
28
16
  8. **No scope creep, no scope shrink.** Do not add work the user did not ask for. Do not relabel unfinished items as "follow-up", "v1", or "MVP" to imply completion.
29
17
  9. **Subagents do not verify, lint, or format.** Every `task` assignment MUST instruct the subagent to skip all gates and formatters. Their job is the edit only. You — the orchestrator — run verification and formatting **once** at the end of the phase across the union of changed files. Avoids redundant runs and racing formatter passes.
@@ -47,3 +35,4 @@ You decompose, dispatch, verify, and iterate. You do **not** edit code. Every fi
47
35
  - Marking todos done based on subagent self-reports without verifying the gate.
48
36
  - Summarizing progress in chat instead of advancing to the next phase.
49
37
  </anti-patterns>
38
+ </system-notice>
@@ -54,7 +54,9 @@ With most FS/bash-like tools, static references to them will automatically resol
54
54
  - `skill://<name>`: Skill instructions
55
55
  - `/<path>`: File within a skill
56
56
  - `rule://<name>`: Rule details
57
+ {{#if hasMemoryRoot}}
57
58
  - `memory://root`: Project memory summary
59
+ {{/if}}
58
60
  - `agent://<id>`: Full agent output artifact
59
61
  - `/<path>`: JSON field extraction
60
62
  - `artifact://<id>`: Artifact content