@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.
- package/CHANGELOG.md +77 -0
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/tiny-models-cli.d.ts +9 -0
- package/dist/types/commands/tiny-models.d.ts +22 -0
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +1 -1
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/config/model-id-affixes.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +233 -17
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/eval/__tests__/llm-bridge.test.d.ts +1 -0
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
- package/dist/types/eval/llm-bridge.d.ts +25 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +15 -0
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
- package/dist/types/internal-urls/local-protocol.d.ts +2 -1
- package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
- package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
- package/dist/types/internal-urls/router.d.ts +8 -1
- package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
- package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
- package/dist/types/internal-urls/types.d.ts +26 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/resolve.d.ts +2 -1
- package/dist/types/memory-backend/types.d.ts +7 -1
- package/dist/types/mnemosyne/backend.d.ts +4 -0
- package/dist/types/mnemosyne/config.d.ts +29 -0
- package/dist/types/mnemosyne/index.d.ts +3 -0
- package/dist/types/mnemosyne/state.d.ts +72 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -3
- package/dist/types/modes/components/hook-selector.d.ts +27 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
- package/dist/types/modes/gradient-highlight.d.ts +23 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
- package/dist/types/modes/orchestrate.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/ultrathink.d.ts +3 -3
- package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +35 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/render.d.ts +5 -1
- package/dist/types/tiny/models.d.ts +185 -0
- package/dist/types/tiny/text.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +24 -0
- package/dist/types/tiny/title-protocol.d.ts +74 -0
- package/dist/types/tiny/worker.d.ts +2 -0
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/index.d.ts +7 -4
- package/dist/types/tools/memory-edit.d.ts +40 -0
- package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
- package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
- package/dist/types/tools/memory-render.d.ts +60 -0
- package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
- package/dist/types/tools/todo-write.d.ts +8 -0
- package/dist/types/tools/tool-result.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +3 -0
- package/package.json +18 -14
- package/scripts/build-binary.ts +1 -0
- package/src/cli/tiny-models-cli.ts +127 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +8 -8
- package/src/commands/tiny-models.ts +36 -0
- package/src/config/model-equivalence.ts +43 -2
- package/src/config/model-id-affixes.ts +64 -0
- package/src/config/model-registry.ts +166 -8
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +206 -14
- package/src/edit/hashline/diff.ts +5 -7
- package/src/eval/__tests__/llm-bridge.test.ts +297 -0
- package/src/eval/__tests__/shared-executors.test.ts +36 -0
- package/src/eval/js/shared/local-module-loader.ts +13 -1
- package/src/eval/js/shared/prelude.txt +8 -0
- package/src/eval/js/shared/rewrite-imports.ts +31 -26
- package/src/eval/js/tool-bridge.ts +4 -0
- package/src/eval/llm-bridge.ts +181 -0
- package/src/eval/py/prelude.py +52 -31
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -13
- package/src/extensibility/plugins/legacy-pi-compat.ts +60 -23
- package/src/internal-urls/agent-protocol.ts +18 -1
- package/src/internal-urls/artifact-protocol.ts +19 -1
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/internal-urls/local-protocol.ts +14 -1
- package/src/internal-urls/memory-protocol.ts +6 -1
- package/src/internal-urls/omp-protocol.ts +5 -1
- package/src/internal-urls/router.ts +20 -1
- package/src/internal-urls/rule-protocol.ts +8 -1
- package/src/internal-urls/skill-protocol.ts +8 -1
- package/src/internal-urls/types.ts +27 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +4 -0
- package/src/mcp/oauth-flow.ts +2 -2
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/resolve.ts +4 -1
- package/src/memory-backend/types.ts +8 -1
- package/src/mnemosyne/backend.ts +374 -0
- package/src/mnemosyne/config.ts +160 -0
- package/src/mnemosyne/index.ts +3 -0
- package/src/mnemosyne/state.ts +548 -0
- package/src/modes/acp/acp-agent.ts +11 -6
- package/src/modes/components/agent-dashboard.ts +4 -4
- package/src/modes/components/custom-editor.ts +3 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/extensions/extension-list.ts +3 -2
- package/src/modes/components/footer.ts +5 -6
- package/src/modes/components/history-search.ts +3 -3
- package/src/modes/components/hook-selector.ts +94 -8
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/mcp-add-wizard.ts +3 -3
- package/src/modes/components/model-selector.ts +124 -26
- package/src/modes/components/oauth-selector.ts +3 -3
- package/src/modes/components/session-observer-overlay.ts +19 -13
- package/src/modes/components/session-selector.ts +3 -3
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/status-line/context-thresholds.ts +11 -0
- package/src/modes/components/status-line/presets.ts +1 -0
- package/src/modes/components/status-line/segments.ts +25 -2
- package/src/modes/components/tiny-title-download-progress.ts +90 -0
- package/src/modes/components/tips.txt +12 -0
- package/src/modes/components/tool-execution.ts +67 -3
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +3 -3
- package/src/modes/components/welcome.ts +55 -1
- package/src/modes/controllers/command-controller.ts +16 -1
- package/src/modes/controllers/extension-ui-controller.ts +3 -1
- package/src/modes/controllers/input-controller.ts +57 -0
- package/src/modes/gradient-highlight.ts +70 -0
- package/src/modes/interactive-mode.ts +80 -196
- package/src/modes/internal-url-autocomplete.ts +143 -0
- package/src/modes/orchestrate.ts +36 -0
- package/src/modes/prompt-action-autocomplete.ts +12 -0
- package/src/modes/theme/theme.ts +7 -0
- package/src/modes/ultrathink.ts +9 -53
- package/src/modes/utils/keybinding-matchers.ts +11 -0
- package/src/prompts/system/memory-consolidation-system.md +8 -0
- package/src/prompts/system/memory-extraction-system.md +26 -0
- package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +5 -16
- package/src/prompts/system/system-prompt.md +2 -0
- package/src/prompts/system/tiny-title-system.md +8 -0
- package/src/prompts/tools/eval.md +2 -0
- package/src/prompts/tools/memory-edit.md +8 -0
- package/src/prompts/tools/task.md +4 -7
- package/src/sdk.ts +8 -6
- package/src/session/agent-session.ts +147 -44
- package/src/session/session-manager.ts +47 -0
- package/src/slash-commands/builtin-registry.ts +10 -1
- package/src/system-prompt.ts +4 -0
- package/src/task/commands.ts +1 -5
- package/src/task/executor.ts +8 -0
- package/src/task/index.ts +2 -0
- package/src/task/render.ts +69 -26
- package/src/tiny/models.ts +217 -0
- package/src/tiny/text.ts +19 -0
- package/src/tiny/title-client.ts +340 -0
- package/src/tiny/title-protocol.ts +51 -0
- package/src/tiny/worker.ts +523 -0
- package/src/tools/bash.ts +58 -16
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/eval.ts +24 -48
- package/src/tools/index.ts +17 -15
- package/src/tools/memory-edit.ts +59 -0
- package/src/tools/memory-recall.ts +100 -0
- package/src/tools/memory-reflect.ts +88 -0
- package/src/tools/memory-render.ts +185 -0
- package/src/tools/memory-retain.ts +91 -0
- package/src/tools/renderers.ts +4 -2
- package/src/tools/todo-write.ts +128 -29
- package/src/tools/tool-result.ts +8 -0
- package/src/utils/title-generator.ts +115 -13
- package/dist/types/tools/calculator.d.ts +0 -77
- package/src/prompts/tools/calculator.md +0 -10
- package/src/tools/calculator.ts +0 -541
- package/src/tools/hindsight-recall.ts +0 -69
- package/src/tools/hindsight-reflect.ts +0 -58
- 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 (
|
|
538
|
-
// is doing the work for a still-pending todo) updates as
|
|
539
|
-
// start, finish, or fail.
|
|
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
|
|
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}${
|
|
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}${
|
|
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
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
const
|
|
1101
|
-
|
|
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,
|
|
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,
|
|
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",
|
|
2049
|
+
["Approve and execute", "Approve and compact context", keepContextLabel, "Refine plan"],
|
|
2172
2050
|
{
|
|
2173
|
-
helpText
|
|
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
|
}
|