@oh-my-pi/pi-coding-agent 15.7.2 → 15.7.5
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 +114 -6
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/extension-flags.d.ts +36 -0
- package/dist/types/config/config-file.d.ts +4 -0
- package/dist/types/config/file-lock.d.ts +23 -0
- package/dist/types/config/keybindings.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +69 -65
- package/dist/types/edit/hashline/diff.d.ts +3 -3
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
- package/dist/types/eval/agent-bridge.d.ts +25 -0
- package/dist/types/eval/backend.d.ts +17 -2
- package/dist/types/eval/budget-bridge.d.ts +29 -0
- package/dist/types/eval/idle-timeout.d.ts +28 -0
- package/dist/types/eval/js/executor.d.ts +8 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/eval/py/executor.d.ts +13 -0
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
- package/dist/types/extensibility/plugins/manager.d.ts +12 -1
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/local-protocol.d.ts +19 -9
- package/dist/types/internal-urls/types.d.ts +14 -0
- package/dist/types/lsp/client.d.ts +3 -0
- package/dist/types/mcp/manager.d.ts +14 -5
- package/dist/types/memory-backend/index.d.ts +1 -1
- package/dist/types/memory-backend/resolve.d.ts +1 -1
- package/dist/types/memory-backend/types.d.ts +3 -3
- package/dist/types/mnemopi/backend.d.ts +4 -0
- package/dist/types/mnemopi/config.d.ts +29 -0
- package/dist/types/mnemopi/state.d.ts +72 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -2
- package/dist/types/modes/components/omfg-panel.d.ts +19 -0
- package/dist/types/modes/controllers/command-controller.d.ts +6 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
- package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
- package/dist/types/modes/gradient-highlight.d.ts +5 -1
- package/dist/types/modes/interactive-mode.d.ts +7 -3
- package/dist/types/modes/magic-keywords.d.ts +14 -0
- package/dist/types/modes/markdown-prose.d.ts +27 -0
- package/dist/types/modes/orchestrate.d.ts +7 -2
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/turn-budget.d.ts +18 -0
- package/dist/types/modes/types.d.ts +7 -3
- package/dist/types/modes/ultrathink.d.ts +7 -2
- package/dist/types/modes/workflow.d.ts +15 -0
- package/dist/types/sdk.d.ts +13 -3
- package/dist/types/session/agent-session.d.ts +36 -17
- package/dist/types/session/session-manager.d.ts +18 -0
- package/dist/types/session/session-storage.d.ts +6 -0
- package/dist/types/session/shake-types.d.ts +24 -0
- package/dist/types/task/executor.d.ts +2 -2
- package/dist/types/task/repair-args.d.ts +52 -0
- package/dist/types/tiny/models.d.ts +1 -1
- package/dist/types/tiny/title-client.d.ts +28 -2
- package/dist/types/tiny/title-protocol.d.ts +8 -5
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/index.d.ts +19 -3
- package/dist/types/tools/memory-edit.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tui/output-block.d.ts +7 -7
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/package.json +10 -10
- package/scripts/build-binary.ts +0 -1
- package/src/autoresearch/tools/run-experiment.ts +45 -113
- package/src/cli/args.ts +39 -16
- package/src/cli/extension-flags.ts +48 -0
- package/src/cli/plugin-cli.ts +11 -2
- package/src/cli.ts +59 -0
- package/src/config/config-file.ts +98 -13
- package/src/config/file-lock.ts +60 -17
- package/src/config/keybindings.ts +78 -27
- package/src/config/model-registry.ts +7 -1
- package/src/config/settings-schema.ts +73 -67
- package/src/config/settings.ts +22 -0
- package/src/edit/hashline/diff.ts +81 -24
- package/src/edit/renderer.ts +16 -12
- package/src/eval/__tests__/agent-bridge.test.ts +433 -0
- package/src/eval/__tests__/budget-bridge.test.ts +69 -0
- package/src/eval/__tests__/idle-timeout.test.ts +66 -0
- package/src/eval/__tests__/shared-executors.test.ts +21 -0
- package/src/eval/agent-bridge.ts +295 -0
- package/src/eval/backend.ts +17 -2
- package/src/eval/budget-bridge.ts +48 -0
- package/src/eval/idle-timeout.ts +80 -0
- package/src/eval/js/executor.ts +35 -7
- package/src/eval/js/index.ts +2 -1
- package/src/eval/js/shared/prelude.txt +85 -1
- package/src/eval/js/tool-bridge.ts +9 -0
- package/src/eval/py/executor.ts +41 -14
- package/src/eval/py/index.ts +2 -1
- package/src/eval/py/prelude.py +132 -1
- package/src/exec/bash-executor.ts +2 -3
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/runner.ts +12 -2
- package/src/extensibility/plugins/git-url.ts +90 -4
- package/src/extensibility/plugins/manager.ts +103 -7
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +88 -88
- package/src/internal-urls/local-protocol.ts +23 -11
- package/src/internal-urls/types.ts +15 -0
- package/src/lsp/client.ts +28 -5
- package/src/main.ts +44 -55
- package/src/mcp/manager.ts +87 -4
- package/src/memory-backend/index.ts +1 -1
- package/src/memory-backend/resolve.ts +3 -3
- package/src/memory-backend/types.ts +3 -3
- package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
- package/src/{mnemosyne → mnemopi}/config.ts +36 -36
- package/src/{mnemosyne → mnemopi}/state.ts +67 -67
- package/src/modes/components/agent-dashboard.ts +6 -6
- package/src/modes/components/custom-editor.ts +4 -11
- package/src/modes/components/extensions/state-manager.ts +3 -4
- package/src/modes/components/footer.ts +8 -9
- package/src/modes/components/hook-selector.ts +86 -20
- package/src/modes/components/oauth-selector.ts +93 -21
- package/src/modes/components/omfg-panel.ts +141 -0
- package/src/modes/components/settings-defs.ts +2 -2
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +38 -19
- package/src/modes/components/tree-selector.ts +4 -3
- package/src/modes/components/user-message-selector.ts +94 -19
- package/src/modes/components/user-message.ts +8 -1
- package/src/modes/controllers/command-controller.ts +25 -0
- package/src/modes/controllers/event-controller.ts +68 -3
- package/src/modes/controllers/input-controller.ts +14 -11
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/omfg-controller.ts +283 -0
- package/src/modes/controllers/omfg-rule.ts +647 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/gradient-highlight.ts +23 -6
- package/src/modes/interactive-mode.ts +41 -7
- package/src/modes/magic-keywords.ts +20 -0
- package/src/modes/markdown-prose.ts +247 -0
- package/src/modes/orchestrate.ts +17 -11
- package/src/modes/shared.ts +3 -11
- package/src/modes/turn-budget.ts +31 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/ultrathink.ts +16 -10
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/workflow.ts +42 -0
- package/src/prompts/system/omfg-user.md +51 -0
- package/src/prompts/system/project-prompt.md +3 -2
- package/src/prompts/system/subagent-system-prompt.md +12 -8
- package/src/prompts/system/system-prompt.md +9 -6
- package/src/prompts/system/workflow-notice.md +70 -0
- package/src/prompts/tools/eval.md +13 -1
- package/src/prompts/tools/memory-edit.md +1 -1
- package/src/sdk.ts +63 -33
- package/src/session/agent-session.ts +290 -55
- package/src/session/session-manager.ts +32 -0
- package/src/session/session-storage.ts +68 -8
- package/src/session/shake-types.ts +43 -0
- package/src/slash-commands/builtin-registry.ts +39 -16
- package/src/task/executor.ts +17 -7
- package/src/task/index.ts +9 -8
- package/src/task/repair-args.ts +117 -0
- package/src/tiny/models.ts +2 -2
- package/src/tiny/title-client.ts +133 -43
- package/src/tiny/title-protocol.ts +10 -5
- package/src/tiny/worker.ts +3 -46
- package/src/tools/ast-edit.ts +3 -0
- package/src/tools/ast-grep.ts +3 -0
- package/src/tools/eval.ts +202 -26
- package/src/tools/find.ts +20 -6
- package/src/tools/gh.ts +1 -0
- package/src/tools/grouped-file-output.ts +9 -2
- package/src/tools/index.ts +17 -5
- package/src/tools/memory-edit.ts +4 -4
- package/src/tools/memory-recall.ts +5 -5
- package/src/tools/memory-reflect.ts +5 -5
- package/src/tools/memory-retain.ts +4 -4
- package/src/tools/path-utils.ts +13 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/render-utils.ts +2 -1
- package/src/tools/search.ts +491 -76
- package/src/tui/output-block.ts +37 -75
- package/src/utils/git.ts +9 -3
- package/dist/types/mnemosyne/backend.d.ts +0 -4
- package/dist/types/mnemosyne/config.d.ts +0 -29
- package/dist/types/mnemosyne/state.d.ts +0 -72
- /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
- /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Container, Markdown, Spacer } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
3
|
+
import { highlightMagicKeywords } from "../magic-keywords";
|
|
3
4
|
|
|
4
5
|
// OSC 133 shell integration: marks prompt zones for terminal multiplexers
|
|
5
6
|
const OSC133_ZONE_START = "\x1b]133;A\x07";
|
|
@@ -13,9 +14,15 @@ export class UserMessageComponent extends Container {
|
|
|
13
14
|
constructor(text: string, synthetic = false) {
|
|
14
15
|
super();
|
|
15
16
|
const bgColor = (value: string) => theme.bg("userMessageBg", value);
|
|
17
|
+
// Paint the magic keywords ("ultrathink"/"orchestrate"/"workflow") inside the rendered
|
|
18
|
+
// bubble too — matching the live editor glow. The Markdown component routes code spans and
|
|
19
|
+
// fenced blocks through its own code styling (never `color`), so those are already excluded;
|
|
20
|
+
// `highlightMagicKeywords` additionally restores the bubble's own foreground after each
|
|
21
|
+
// painted keyword so the gradient never bleeds into the rest of the line.
|
|
22
|
+
const keywordReset = theme.getFgAnsi("userMessageText") || "\x1b[39m";
|
|
16
23
|
const color = synthetic
|
|
17
24
|
? (value: string) => theme.fg("dim", value)
|
|
18
|
-
: (value: string) => theme.fg("userMessageText", value);
|
|
25
|
+
: (value: string) => theme.fg("userMessageText", highlightMagicKeywords(value, keywordReset));
|
|
19
26
|
this.addChild(new Spacer(1));
|
|
20
27
|
this.addChild(
|
|
21
28
|
new Markdown(text, 1, 1, getMarkdownTheme(), {
|
|
@@ -39,6 +39,7 @@ import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
|
|
|
39
39
|
import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
|
40
40
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
41
41
|
import type { NewSessionOptions } from "../../session/session-manager";
|
|
42
|
+
import { formatShakeSummary, type ShakeMode, type ShakeResult } from "../../session/shake-types";
|
|
42
43
|
import { outputMeta } from "../../tools/output-meta";
|
|
43
44
|
import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
|
|
44
45
|
import { replaceTabs } from "../../tools/render-utils";
|
|
@@ -1122,6 +1123,30 @@ export class CommandController {
|
|
|
1122
1123
|
return this.executeCompaction(customInstructions, false);
|
|
1123
1124
|
}
|
|
1124
1125
|
|
|
1126
|
+
/**
|
|
1127
|
+
* TUI handler for `/shake`. `elide` drops heavy structural content and
|
|
1128
|
+
* `images` strips image blocks. Rebuilds the chat and reports counts.
|
|
1129
|
+
*/
|
|
1130
|
+
async handleShakeCommand(mode: ShakeMode): Promise<void> {
|
|
1131
|
+
let result: ShakeResult;
|
|
1132
|
+
try {
|
|
1133
|
+
result = await this.ctx.session.shake(mode);
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
this.ctx.showError(`Shake failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const dropped = result.toolResultsDropped + result.blocksDropped + (result.imagesDropped ?? 0);
|
|
1140
|
+
if (dropped === 0) {
|
|
1141
|
+
this.ctx.showStatus("Nothing to shake.");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
this.ctx.rebuildChatFromMessages();
|
|
1145
|
+
this.ctx.statusLine.invalidate();
|
|
1146
|
+
this.ctx.updateEditorTopBorder();
|
|
1147
|
+
this.ctx.showStatus(formatShakeSummary(result));
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1125
1150
|
async handleSkillCommand(skillPath: string, args: string): Promise<void> {
|
|
1126
1151
|
try {
|
|
1127
1152
|
const content = await Bun.file(skillPath).text();
|
|
@@ -25,6 +25,19 @@ type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
|
25
25
|
|
|
26
26
|
const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
|
|
27
27
|
|
|
28
|
+
// Events that change foreground streaming state, or that reset a turn. The TUI
|
|
29
|
+
// eager native-scrollback rebuild mode is recomputed only on these so unrelated
|
|
30
|
+
// IRC/notices/status refreshes do not toggle scrollback replay policy.
|
|
31
|
+
const STREAM_RENDER_MODE_EVENTS: Record<string, true> = {
|
|
32
|
+
agent_start: true,
|
|
33
|
+
agent_end: true,
|
|
34
|
+
message_start: true,
|
|
35
|
+
message_end: true,
|
|
36
|
+
tool_execution_start: true,
|
|
37
|
+
tool_execution_update: true,
|
|
38
|
+
tool_execution_end: true,
|
|
39
|
+
};
|
|
40
|
+
|
|
28
41
|
type AgentSessionEventHandlers = {
|
|
29
42
|
[E in AgentSessionEventKind]: (event: Extract<AgentSessionEvent, { type: E }>) => Promise<void>;
|
|
30
43
|
};
|
|
@@ -35,6 +48,7 @@ export class EventController {
|
|
|
35
48
|
#renderedCustomMessages = new Set<string>();
|
|
36
49
|
#lastIntent: string | undefined = undefined;
|
|
37
50
|
#backgroundToolCallIds = new Set<string>();
|
|
51
|
+
#assistantMessageStreaming = false;
|
|
38
52
|
#readToolCallArgs = new Map<string, Record<string, unknown>>();
|
|
39
53
|
#readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
40
54
|
#lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
|
|
@@ -158,12 +172,37 @@ export class EventController {
|
|
|
158
172
|
|
|
159
173
|
const run = this.#handlers[event.type] as (e: AgentSessionEvent) => Promise<void>;
|
|
160
174
|
await run(event);
|
|
175
|
+
// While assistant text or a foreground tool is streaming, rows above the
|
|
176
|
+
// viewport can re-layout after they have already entered native scrollback
|
|
177
|
+
// (Markdown fences, wrapping, previews). Let the TUI rebuild history on
|
|
178
|
+
// those offscreen edits instead of deferring, which otherwise leaves stale
|
|
179
|
+
// tail rows duplicated above the live viewport.
|
|
180
|
+
// Background-running tools are excluded so late async updates outside the
|
|
181
|
+
// active foreground stream keep the no-yank deferral; agent_start resets
|
|
182
|
+
// the mode at every turn boundary.
|
|
183
|
+
if (STREAM_RENDER_MODE_EVENTS[event.type]) {
|
|
184
|
+
this.#refreshToolRenderMode();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#refreshToolRenderMode(): void {
|
|
189
|
+
let foregroundToolActive = this.#assistantMessageStreaming;
|
|
190
|
+
if (!foregroundToolActive) {
|
|
191
|
+
for (const toolCallId of this.ctx.pendingTools.keys()) {
|
|
192
|
+
if (!this.#backgroundToolCallIds.has(toolCallId)) {
|
|
193
|
+
foregroundToolActive = true;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
this.ctx.ui.setEagerNativeScrollbackRebuild(foregroundToolActive);
|
|
161
199
|
}
|
|
162
200
|
|
|
163
201
|
async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
|
|
164
202
|
this.#lastIntent = undefined;
|
|
165
203
|
this.#readToolCallArgs.clear();
|
|
166
204
|
this.#readToolCallAssistantComponents.clear();
|
|
205
|
+
this.#assistantMessageStreaming = false;
|
|
167
206
|
this.#lastAssistantComponent = undefined;
|
|
168
207
|
if (this.ctx.retryEscapeHandler) {
|
|
169
208
|
this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
|
|
@@ -236,6 +275,7 @@ export class EventController {
|
|
|
236
275
|
this.ctx.ui.requestRender();
|
|
237
276
|
} else if (event.message.role === "assistant") {
|
|
238
277
|
this.#lastThinkingCount = 0;
|
|
278
|
+
this.#assistantMessageStreaming = true;
|
|
239
279
|
this.#resetReadGroup();
|
|
240
280
|
this.ctx.streamingComponent = new AssistantMessageComponent(undefined, this.ctx.hideThinkingBlock, () =>
|
|
241
281
|
this.ctx.ui.requestRender(),
|
|
@@ -382,6 +422,9 @@ export class EventController {
|
|
|
382
422
|
|
|
383
423
|
async #handleMessageEnd(event: Extract<AgentSessionEvent, { type: "message_end" }>): Promise<void> {
|
|
384
424
|
if (event.message.role === "user") return;
|
|
425
|
+
if (event.message.role === "assistant") {
|
|
426
|
+
this.#assistantMessageStreaming = false;
|
|
427
|
+
}
|
|
385
428
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
386
429
|
this.ctx.streamingMessage = event.message;
|
|
387
430
|
let errorMessage: string | undefined;
|
|
@@ -564,8 +607,8 @@ export class EventController {
|
|
|
564
607
|
}
|
|
565
608
|
}
|
|
566
609
|
}
|
|
567
|
-
|
|
568
610
|
async #handleAgentEnd(_event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
|
|
611
|
+
this.#assistantMessageStreaming = false;
|
|
569
612
|
if (this.ctx.loadingAnimation) {
|
|
570
613
|
this.ctx.loadingAnimation.stop();
|
|
571
614
|
this.ctx.loadingAnimation = undefined;
|
|
@@ -610,7 +653,12 @@ export class EventController {
|
|
|
610
653
|
: event.reason === "idle"
|
|
611
654
|
? "Idle "
|
|
612
655
|
: "";
|
|
613
|
-
const actionLabel =
|
|
656
|
+
const actionLabel =
|
|
657
|
+
event.action === "handoff"
|
|
658
|
+
? "Auto-handoff"
|
|
659
|
+
: event.action === "shake"
|
|
660
|
+
? "Auto-shake"
|
|
661
|
+
: "Auto context-full maintenance";
|
|
614
662
|
this.ctx.autoCompactionLoader = new Loader(
|
|
615
663
|
this.ctx.ui,
|
|
616
664
|
spinner => theme.fg("accent", spinner),
|
|
@@ -634,8 +682,25 @@ export class EventController {
|
|
|
634
682
|
this.ctx.statusContainer.clear();
|
|
635
683
|
}
|
|
636
684
|
const isHandoffAction = event.action === "handoff";
|
|
685
|
+
const isShakeAction = event.action === "shake";
|
|
637
686
|
if (event.aborted) {
|
|
638
|
-
this.ctx.showStatus(
|
|
687
|
+
this.ctx.showStatus(
|
|
688
|
+
isHandoffAction
|
|
689
|
+
? "Auto-handoff cancelled"
|
|
690
|
+
: isShakeAction
|
|
691
|
+
? "Auto-shake cancelled"
|
|
692
|
+
: "Auto context-full maintenance cancelled",
|
|
693
|
+
);
|
|
694
|
+
} else if (isShakeAction) {
|
|
695
|
+
// Shake produces no CompactionResult; rebuild on success, suppress benign skips.
|
|
696
|
+
if (event.errorMessage) {
|
|
697
|
+
this.ctx.showWarning(event.errorMessage);
|
|
698
|
+
} else if (!event.skipped) {
|
|
699
|
+
this.ctx.rebuildChatFromMessages();
|
|
700
|
+
this.ctx.statusLine.invalidate();
|
|
701
|
+
this.ctx.updateEditorTopBorder();
|
|
702
|
+
this.ctx.showStatus("Auto-shake completed");
|
|
703
|
+
}
|
|
639
704
|
} else if (event.result) {
|
|
640
705
|
this.ctx.rebuildChatFromMessages();
|
|
641
706
|
this.ctx.statusLine.invalidate();
|
|
@@ -8,7 +8,6 @@ import { renderSegmentTrack } from "../../modes/components/segment-track";
|
|
|
8
8
|
import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
|
|
9
9
|
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
10
10
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
11
|
-
import { theme } from "../../modes/theme/theme";
|
|
12
11
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
13
12
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
14
13
|
import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
|
|
@@ -91,6 +90,7 @@ export class InputController {
|
|
|
91
90
|
Boolean(
|
|
92
91
|
this.ctx.loadingAnimation ||
|
|
93
92
|
this.ctx.hasActiveBtw() ||
|
|
93
|
+
this.ctx.hasActiveOmfg() ||
|
|
94
94
|
this.ctx.session.isStreaming ||
|
|
95
95
|
this.ctx.session.isCompacting ||
|
|
96
96
|
this.ctx.session.isGeneratingHandoff ||
|
|
@@ -114,6 +114,9 @@ export class InputController {
|
|
|
114
114
|
if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
|
+
if (this.ctx.hasActiveOmfg() && this.ctx.handleOmfgEscape()) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
117
120
|
if (this.ctx.loadingAnimation) {
|
|
118
121
|
if (this.ctx.cancelPendingSubmission()) {
|
|
119
122
|
return;
|
|
@@ -161,9 +164,9 @@ export class InputController {
|
|
|
161
164
|
this.ctx.editor.setActionKeys("app.thinking.cycle", this.ctx.keybindings.getKeys("app.thinking.cycle"));
|
|
162
165
|
this.ctx.editor.onCycleThinkingLevel = () => this.cycleThinkingLevel();
|
|
163
166
|
this.ctx.editor.setActionKeys("app.model.cycleForward", this.ctx.keybindings.getKeys("app.model.cycleForward"));
|
|
164
|
-
this.ctx.editor.onCycleModelForward = () => this.cycleRoleModel();
|
|
167
|
+
this.ctx.editor.onCycleModelForward = () => this.cycleRoleModel("forward");
|
|
165
168
|
this.ctx.editor.setActionKeys("app.model.cycleBackward", this.ctx.keybindings.getKeys("app.model.cycleBackward"));
|
|
166
|
-
this.ctx.editor.onCycleModelBackward = () => this.cycleRoleModel(
|
|
169
|
+
this.ctx.editor.onCycleModelBackward = () => this.cycleRoleModel("backward");
|
|
167
170
|
this.ctx.editor.setActionKeys(
|
|
168
171
|
"app.model.selectTemporary",
|
|
169
172
|
this.ctx.keybindings.getKeys("app.model.selectTemporary"),
|
|
@@ -180,7 +183,6 @@ export class InputController {
|
|
|
180
183
|
this.ctx.editor.onToggleThinking = () => this.ctx.toggleThinkingBlockVisibility();
|
|
181
184
|
this.ctx.editor.setActionKeys("app.editor.external", this.ctx.keybindings.getKeys("app.editor.external"));
|
|
182
185
|
this.ctx.editor.onExternalEditor = () => void this.openExternalEditor();
|
|
183
|
-
this.ctx.editor.onShowHotkeys = () => this.ctx.handleHotkeysCommand();
|
|
184
186
|
this.ctx.editor.setActionKeys(
|
|
185
187
|
"app.clipboard.pasteImage",
|
|
186
188
|
this.ctx.keybindings.getKeys("app.clipboard.pasteImage"),
|
|
@@ -606,6 +608,9 @@ export class InputController {
|
|
|
606
608
|
if (this.ctx.hasActiveBtw()) {
|
|
607
609
|
this.ctx.handleBtwEscape();
|
|
608
610
|
}
|
|
611
|
+
if (this.ctx.hasActiveOmfg()) {
|
|
612
|
+
this.ctx.handleOmfgEscape();
|
|
613
|
+
}
|
|
609
614
|
|
|
610
615
|
this.ctx.isBackgrounded = true;
|
|
611
616
|
const backgroundUiContext = this.ctx.createBackgroundUiContext();
|
|
@@ -760,10 +765,10 @@ export class InputController {
|
|
|
760
765
|
}
|
|
761
766
|
}
|
|
762
767
|
|
|
763
|
-
async cycleRoleModel(
|
|
768
|
+
async cycleRoleModel(direction: "forward" | "backward" = "forward"): Promise<void> {
|
|
764
769
|
try {
|
|
765
770
|
const cycleOrder = settings.get("cycleOrder");
|
|
766
|
-
const result = await this.ctx.session.cycleRoleModels(cycleOrder,
|
|
771
|
+
const result = await this.ctx.session.cycleRoleModels(cycleOrder, direction);
|
|
767
772
|
if (!result) {
|
|
768
773
|
this.ctx.showStatus("Only one role model available");
|
|
769
774
|
return;
|
|
@@ -773,14 +778,12 @@ export class InputController {
|
|
|
773
778
|
this.ctx.updateEditorBorderColor();
|
|
774
779
|
// The status line already reports the resolved model + thinking level, so
|
|
775
780
|
// the cycle status is just a status-line-style chip track (active role
|
|
776
|
-
// filled), matching the plan-approval model slider.
|
|
777
|
-
// temporary switch since that isn't shown elsewhere.
|
|
781
|
+
// filled), matching the plan-approval model slider.
|
|
778
782
|
const track = renderSegmentTrack(
|
|
779
783
|
cycleOrder.map(role => ({ label: role, color: getRoleInfo(role, settings).color })),
|
|
780
784
|
cycleOrder.indexOf(result.role),
|
|
781
785
|
);
|
|
782
|
-
|
|
783
|
-
this.ctx.showStatus(`${track}${tempLabel}`, { dim: false });
|
|
786
|
+
this.ctx.showStatus(track, { dim: false });
|
|
784
787
|
} catch (error) {
|
|
785
788
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
786
789
|
}
|
|
@@ -797,7 +800,7 @@ export class InputController {
|
|
|
797
800
|
child.setExpanded(expanded);
|
|
798
801
|
}
|
|
799
802
|
}
|
|
800
|
-
this.ctx.ui.requestRender();
|
|
803
|
+
this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
|
|
801
804
|
}
|
|
802
805
|
|
|
803
806
|
toggleThinkingBlockVisibility(): void {
|
|
@@ -1425,7 +1425,7 @@ export class MCPCommandController {
|
|
|
1425
1425
|
this.#showMessage(["", theme.fg("muted", `Reconnecting to "${name}"...`), ""].join("\n"));
|
|
1426
1426
|
|
|
1427
1427
|
try {
|
|
1428
|
-
const connection = await this.ctx.mcpManager.reconnectServer(name);
|
|
1428
|
+
const connection = await this.ctx.mcpManager.reconnectServer(name, { manual: true });
|
|
1429
1429
|
if (connection) {
|
|
1430
1430
|
// refreshMCPTools re-registers tools and preserves the user's prior
|
|
1431
1431
|
// MCP tool selection. No need to call activateDiscoveredMCPTools —
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { CONFIG_DIR_NAME, prompt } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type { Rule } from "../../capability/rule";
|
|
4
|
+
import omfgUserPrompt from "../../prompts/system/omfg-user.md" with { type: "text" };
|
|
5
|
+
import { shortenPath } from "../../tools/render-utils";
|
|
6
|
+
import { OmfgPanelComponent } from "../components/omfg-panel";
|
|
7
|
+
import type { InteractiveModeContext } from "../types";
|
|
8
|
+
import {
|
|
9
|
+
buildOmfgRuleForPath,
|
|
10
|
+
extractGeneratedRuleJson,
|
|
11
|
+
type OmfgRuleSourceLevel,
|
|
12
|
+
type ParsedGeneratedRule,
|
|
13
|
+
parseGeneratedRule,
|
|
14
|
+
validateParsedRuleAgainstAssistantHistory,
|
|
15
|
+
} from "./omfg-rule";
|
|
16
|
+
|
|
17
|
+
interface OmfgRequest {
|
|
18
|
+
component: OmfgPanelComponent;
|
|
19
|
+
abortController: AbortController;
|
|
20
|
+
complaint: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface OmfgCandidate extends ParsedGeneratedRule {
|
|
24
|
+
validated: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface GenerateCandidateOptions {
|
|
28
|
+
initialFeedback?: string;
|
|
29
|
+
previousRule?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type SaveCandidateResult = { kind: "saved" | "aborted" | "rejected" } | { kind: "amend"; feedback: string };
|
|
33
|
+
|
|
34
|
+
const MAX_ATTEMPTS = 3;
|
|
35
|
+
const PROJECT_OPTION = "This project (.omp/rules)";
|
|
36
|
+
const GLOBAL_OPTION = "Global — all projects (~/.omp/agent/rules)";
|
|
37
|
+
const AMEND_OPTION = "Amend with feedback…";
|
|
38
|
+
|
|
39
|
+
export class OmfgController {
|
|
40
|
+
#activeRequest: OmfgRequest | undefined;
|
|
41
|
+
|
|
42
|
+
constructor(private readonly ctx: InteractiveModeContext) {}
|
|
43
|
+
|
|
44
|
+
hasActiveRequest(): boolean {
|
|
45
|
+
return this.#activeRequest !== undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
handleEscape(): boolean {
|
|
49
|
+
if (!this.#activeRequest) return false;
|
|
50
|
+
this.#closeActiveRequest({ abort: this.#activeRequest.abortController.signal.aborted === false });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
dispose(): void {
|
|
55
|
+
this.#closeActiveRequest({ abort: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async start(complaint: string): Promise<void> {
|
|
59
|
+
const trimmedComplaint = complaint.trim();
|
|
60
|
+
if (!trimmedComplaint) {
|
|
61
|
+
this.ctx.showStatus("Usage: /omfg <complaint>");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const model = this.ctx.session.model;
|
|
66
|
+
if (!model) {
|
|
67
|
+
this.ctx.showError("No active model available for /omfg.");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.#closeActiveRequest({ abort: true });
|
|
72
|
+
|
|
73
|
+
const request: OmfgRequest = {
|
|
74
|
+
component: new OmfgPanelComponent({ complaint: trimmedComplaint, tui: this.ctx.ui }),
|
|
75
|
+
abortController: new AbortController(),
|
|
76
|
+
complaint: trimmedComplaint,
|
|
77
|
+
};
|
|
78
|
+
this.ctx.omfgContainer.clear();
|
|
79
|
+
this.ctx.omfgContainer.addChild(request.component);
|
|
80
|
+
this.ctx.ui.requestRender();
|
|
81
|
+
this.#activeRequest = request;
|
|
82
|
+
void this.#runRequest(request);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async #runRequest(request: OmfgRequest): Promise<void> {
|
|
86
|
+
try {
|
|
87
|
+
let candidate = await this.#generateCandidate(request);
|
|
88
|
+
for (;;) {
|
|
89
|
+
if (!this.#isActiveRequest(request)) return;
|
|
90
|
+
if (!candidate) {
|
|
91
|
+
request.component.markError("The model did not return a valid TTSR rule.");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!candidate.validated) {
|
|
96
|
+
request.component.setStatus("confirming", "Couldn't confirm a conversation match.");
|
|
97
|
+
const shouldSave = await this.ctx.showHookConfirm(
|
|
98
|
+
"Validation",
|
|
99
|
+
"Couldn't confirm this rule matches the conversation. Save anyway?",
|
|
100
|
+
);
|
|
101
|
+
if (!this.#isActiveRequest(request)) return;
|
|
102
|
+
if (!shouldSave) {
|
|
103
|
+
request.component.markRejected();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const saveResult = await this.#saveCandidate(request, candidate);
|
|
109
|
+
if (!this.#isActiveRequest(request)) return;
|
|
110
|
+
if (saveResult.kind !== "amend") {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
candidate = await this.#generateCandidate(request, {
|
|
115
|
+
initialFeedback: `User requested this amendment before saving:\n${saveResult.feedback}`,
|
|
116
|
+
previousRule: candidate.fileContent,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (!this.#isActiveRequest(request)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (request.abortController.signal.aborted) {
|
|
124
|
+
request.component.markAborted();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
request.component.markError(error instanceof Error ? error.message : String(error));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async #generateCandidate(
|
|
132
|
+
request: OmfgRequest,
|
|
133
|
+
options: GenerateCandidateOptions = {},
|
|
134
|
+
): Promise<OmfgCandidate | undefined> {
|
|
135
|
+
const failedAttempts = options.initialFeedback ? [options.initialFeedback] : [];
|
|
136
|
+
let previousRule = options.previousRule;
|
|
137
|
+
let lastCandidate: ParsedGeneratedRule | undefined;
|
|
138
|
+
|
|
139
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
140
|
+
if (this.#shouldStop(request)) return undefined;
|
|
141
|
+
request.component.setRule("");
|
|
142
|
+
request.component.setStatus("generating", `Attempt ${attempt}/${MAX_ATTEMPTS} · generating…`);
|
|
143
|
+
const promptText = prompt.render(omfgUserPrompt, {
|
|
144
|
+
complaint: request.complaint,
|
|
145
|
+
feedback: failedAttempts.length > 0 ? failedAttempts.join("\n\n") : undefined,
|
|
146
|
+
previousRule,
|
|
147
|
+
});
|
|
148
|
+
const { replyText } = await this.ctx.session.runEphemeralTurn({
|
|
149
|
+
promptText,
|
|
150
|
+
dedupeReply: false,
|
|
151
|
+
onTextDelta: delta => {
|
|
152
|
+
if (this.#isActiveRequest(request)) {
|
|
153
|
+
request.component.appendDraft(delta);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
signal: request.abortController.signal,
|
|
157
|
+
});
|
|
158
|
+
if (this.#shouldStop(request)) return undefined;
|
|
159
|
+
|
|
160
|
+
const parsed = parseGeneratedRule(replyText);
|
|
161
|
+
if ("error" in parsed) {
|
|
162
|
+
const failedRule = extractGeneratedRuleJson(replyText) ?? replyText.trim();
|
|
163
|
+
failedAttempts.push(
|
|
164
|
+
`Attempt ${attempt} failed: invalid rule (${parsed.error}).\nFailed candidate:\n${failedRule}`,
|
|
165
|
+
);
|
|
166
|
+
previousRule = failedRule;
|
|
167
|
+
request.component.setStatus("validating", `Attempt ${attempt}/${MAX_ATTEMPTS} · ${parsed.error}`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
request.component.setRule(parsed.fileContent);
|
|
172
|
+
request.component.setStatus("validating", `Attempt ${attempt}/${MAX_ATTEMPTS} · validating…`);
|
|
173
|
+
const validated = validateParsedRuleAgainstAssistantHistory(parsed, this.ctx.session.messages);
|
|
174
|
+
if (validated.repairedCondition) {
|
|
175
|
+
request.component.setRule(validated.candidate.fileContent);
|
|
176
|
+
}
|
|
177
|
+
if (validated.validation.matched) {
|
|
178
|
+
return { ...validated.candidate, validated: true };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
lastCandidate = validated.candidate;
|
|
182
|
+
const failure =
|
|
183
|
+
validated.validation.feedback ?? "The rule condition did not match any earlier assistant output.";
|
|
184
|
+
failedAttempts.push(
|
|
185
|
+
`Attempt ${attempt} failed validation:\n${failure}\nFailed candidate:\n${validated.candidate.fileContent}`,
|
|
186
|
+
);
|
|
187
|
+
previousRule = validated.candidate.fileContent;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return lastCandidate ? { ...lastCandidate, validated: false } : undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async #saveCandidate(request: OmfgRequest, candidate: OmfgCandidate): Promise<SaveCandidateResult> {
|
|
194
|
+
if (this.#shouldStop(request)) return { kind: "aborted" };
|
|
195
|
+
request.component.setStatus("saving", "Choose where to save or amend the TTSR rule…");
|
|
196
|
+
const location = await this.ctx.showHookSelector("Save TTSR rule where?", [
|
|
197
|
+
PROJECT_OPTION,
|
|
198
|
+
GLOBAL_OPTION,
|
|
199
|
+
AMEND_OPTION,
|
|
200
|
+
]);
|
|
201
|
+
if (!this.#isActiveRequest(request)) return { kind: "aborted" };
|
|
202
|
+
if (!location) {
|
|
203
|
+
request.component.markAborted();
|
|
204
|
+
this.#closeActiveRequest({ abort: false });
|
|
205
|
+
return { kind: "aborted" };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (location === AMEND_OPTION) {
|
|
209
|
+
request.component.setStatus("confirming", "Describe how to amend the rule…");
|
|
210
|
+
const amendment = await this.ctx.showHookInput(
|
|
211
|
+
"Amend TTSR rule",
|
|
212
|
+
"e.g. Make it specific to Ruby string eval in tool:write(*.rb)",
|
|
213
|
+
);
|
|
214
|
+
if (!this.#isActiveRequest(request)) return { kind: "aborted" };
|
|
215
|
+
const feedback = amendment?.trim();
|
|
216
|
+
if (!feedback) {
|
|
217
|
+
request.component.markAborted();
|
|
218
|
+
this.#closeActiveRequest({ abort: false });
|
|
219
|
+
return { kind: "aborted" };
|
|
220
|
+
}
|
|
221
|
+
return { kind: "amend", feedback };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const target = this.#resolveTarget(location, candidate.rule.name);
|
|
225
|
+
if (await Bun.file(target.filePath).exists()) {
|
|
226
|
+
const shouldOverwrite = await this.ctx.showHookConfirm(
|
|
227
|
+
"Overwrite TTSR rule?",
|
|
228
|
+
`${shortenPath(target.filePath)} already exists. Overwrite it?`,
|
|
229
|
+
);
|
|
230
|
+
if (!this.#isActiveRequest(request)) return { kind: "aborted" };
|
|
231
|
+
if (!shouldOverwrite) {
|
|
232
|
+
request.component.markRejected();
|
|
233
|
+
return { kind: "rejected" };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
request.component.setStatus("saving", `Saving ${candidate.rule.name}…`);
|
|
238
|
+
await Bun.write(target.filePath, candidate.fileContent);
|
|
239
|
+
if (!this.#isActiveRequest(request)) return { kind: "aborted" };
|
|
240
|
+
|
|
241
|
+
const savedRule = buildOmfgRuleForPath(candidate.rule.name, candidate.fileContent, target.filePath, target.level);
|
|
242
|
+
this.#registerLive(savedRule);
|
|
243
|
+
request.component.markSaved(shortenPath(target.filePath));
|
|
244
|
+
return { kind: "saved" };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#resolveTarget(location: string, ruleName: string): { filePath: string; level: OmfgRuleSourceLevel } {
|
|
248
|
+
if (location === GLOBAL_OPTION) {
|
|
249
|
+
return {
|
|
250
|
+
filePath: path.join(this.ctx.settings.getAgentDir(), "rules", `${ruleName}.md`),
|
|
251
|
+
level: "user",
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
filePath: path.join(this.ctx.sessionManager.getCwd(), CONFIG_DIR_NAME, "rules", `${ruleName}.md`),
|
|
256
|
+
level: "project",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#registerLive(rule: Rule): void {
|
|
261
|
+
this.ctx.session.ttsrManager?.addRule(rule);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#closeActiveRequest(options: { abort: boolean }): void {
|
|
265
|
+
const request = this.#activeRequest;
|
|
266
|
+
if (!request) return;
|
|
267
|
+
this.#activeRequest = undefined;
|
|
268
|
+
if (options.abort) {
|
|
269
|
+
request.abortController.abort();
|
|
270
|
+
}
|
|
271
|
+
request.component.close();
|
|
272
|
+
this.ctx.omfgContainer.clear();
|
|
273
|
+
this.ctx.ui.requestRender();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#isActiveRequest(request: OmfgRequest): boolean {
|
|
277
|
+
return this.#activeRequest === request;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#shouldStop(request: OmfgRequest): boolean {
|
|
281
|
+
return !this.#isActiveRequest(request) || request.abortController.signal.aborted;
|
|
282
|
+
}
|
|
283
|
+
}
|