@oh-my-pi/pi-coding-agent 15.7.1 → 15.7.3
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 +95 -6
- package/dist/types/auto-thinking/classifier.d.ts +35 -0
- 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 +112 -69
- package/dist/types/edit/hashline/diff.d.ts +9 -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/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/model-selector.d.ts +3 -2
- package/dist/types/modes/components/omfg-panel.d.ts +19 -0
- package/dist/types/modes/controllers/command-controller.d.ts +7 -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/theme/theme.d.ts +2 -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 +15 -4
- package/dist/types/session/agent-session.d.ts +59 -23
- 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/thinking.d.ts +39 -1
- package/dist/types/tiny/device.d.ts +3 -3
- package/dist/types/tiny/models.d.ts +34 -1
- package/dist/types/tiny/title-protocol.d.ts +4 -0
- package/dist/types/tools/index.d.ts +19 -3
- package/dist/types/tools/memory-edit.d.ts +1 -1
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/package.json +10 -10
- package/src/auto-thinking/classifier.ts +180 -0
- 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/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 +118 -71
- package/src/config/settings.ts +12 -0
- package/src/edit/hashline/diff.ts +87 -22
- package/src/edit/renderer.ts +16 -12
- package/src/edit/streaming.ts +17 -6
- 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 +53 -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/local-module-loader.ts +75 -10
- 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/main.ts +50 -56
- 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/acp/acp-agent.ts +13 -3
- 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 +18 -12
- package/src/modes/components/hook-selector.ts +86 -20
- package/src/modes/components/model-selector.ts +20 -11
- 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 +9 -2
- package/src/modes/components/settings-selector.ts +4 -1
- package/src/modes/components/status-line/segments.ts +13 -5
- 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 +57 -0
- package/src/modes/controllers/event-controller.ts +65 -3
- package/src/modes/controllers/input-controller.ts +14 -11
- 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 +21 -6
- 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/theme/theme.ts +6 -0
- 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/auto-thinking-difficulty-local.md +14 -0
- package/src/prompts/system/auto-thinking-difficulty.md +12 -0
- package/src/prompts/system/omfg-user.md +51 -0
- package/src/prompts/system/system-prompt.md +1 -0
- 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 +86 -38
- package/src/session/agent-session.ts +558 -80
- package/src/session/session-manager.ts +32 -0
- package/src/session/session-storage.ts +68 -8
- package/src/session/shake-types.ts +44 -0
- package/src/slash-commands/builtin-registry.ts +41 -16
- package/src/task/executor.ts +3 -3
- package/src/task/index.ts +6 -6
- package/src/thinking.ts +73 -1
- package/src/tiny/device.ts +4 -10
- package/src/tiny/models.ts +54 -2
- package/src/tiny/title-protocol.ts +11 -1
- package/src/tiny/worker.ts +19 -7
- package/src/tools/eval.ts +202 -26
- 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/render-utils.ts +2 -1
- package/src/tools/search.ts +480 -76
- 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
|
@@ -45,15 +45,6 @@ function ensureInvalidate(component: unknown): Component {
|
|
|
45
45
|
return c as Component;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function cloneToolArgs<T>(args: T): T {
|
|
49
|
-
if (args === null || args === undefined) return args;
|
|
50
|
-
try {
|
|
51
|
-
return structuredClone(args);
|
|
52
|
-
} catch {
|
|
53
|
-
return args;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
48
|
/**
|
|
58
49
|
* Drop trailing removal/hunk-header lines that appear in a streaming diff
|
|
59
50
|
* before the matching `+added` lines have arrived. Without this, a partial
|
|
@@ -107,6 +98,27 @@ function resolveEditModeForTool(toolName: string, tool: AgentTool | undefined):
|
|
|
107
98
|
return (tool as { mode?: EditMode } | undefined)?.mode;
|
|
108
99
|
}
|
|
109
100
|
|
|
101
|
+
function rawTextInputFromPartialJson(partialJson: unknown): string | undefined {
|
|
102
|
+
if (typeof partialJson !== "string") return undefined;
|
|
103
|
+
if (partialJson.length === 0) return undefined;
|
|
104
|
+
const trimmed = partialJson.trimStart();
|
|
105
|
+
if (trimmed.length === 0) return undefined;
|
|
106
|
+
const first = trimmed[0];
|
|
107
|
+
// Function-tool arguments stream as JSON. Custom/free-form tools stream raw
|
|
108
|
+
// text in the same transport field; only the raw form is a valid fallback for
|
|
109
|
+
// the conventional `input` parameter.
|
|
110
|
+
if (first === "{" || first === "[" || first === '"') return undefined;
|
|
111
|
+
return partialJson;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getArgsWithStreamedTextInput(args: unknown): unknown {
|
|
115
|
+
if (args == null || typeof args !== "object") return args;
|
|
116
|
+
const record = args as Record<string, unknown>;
|
|
117
|
+
if (typeof record.input === "string") return args;
|
|
118
|
+
const input = rawTextInputFromPartialJson(record.__partialJson);
|
|
119
|
+
return input === undefined ? args : { ...record, input };
|
|
120
|
+
}
|
|
121
|
+
|
|
110
122
|
export interface ToolExecutionOptions {
|
|
111
123
|
snapshots?: SnapshotStore;
|
|
112
124
|
showImages?: boolean; // default: true (only used if terminal supports images)
|
|
@@ -198,7 +210,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
198
210
|
this.#tool = tool;
|
|
199
211
|
this.#ui = ui;
|
|
200
212
|
this.#cwd = cwd;
|
|
201
|
-
this.#args =
|
|
213
|
+
this.#args = args;
|
|
202
214
|
|
|
203
215
|
this.addChild(new Spacer(1));
|
|
204
216
|
|
|
@@ -222,7 +234,12 @@ export class ToolExecutionComponent extends Container {
|
|
|
222
234
|
}
|
|
223
235
|
|
|
224
236
|
updateArgs(args: any, _toolCallId?: string): void {
|
|
225
|
-
|
|
237
|
+
// Reference-equality short-circuit before any further work. Callers
|
|
238
|
+
// always allocate a new arg object on each streamed delta (see
|
|
239
|
+
// event-controller.ts and ui-helpers.ts), so a same-reference assignment
|
|
240
|
+
// signals "nothing meaningful changed" and the renderer can skip.
|
|
241
|
+
if (args === this.#args) return;
|
|
242
|
+
this.#args = args;
|
|
226
243
|
this.#updateSpinnerAnimation();
|
|
227
244
|
void this.#runPreviewDiff();
|
|
228
245
|
this.#updateDisplay();
|
|
@@ -247,12 +264,13 @@ export class ToolExecutionComponent extends Container {
|
|
|
247
264
|
const args = this.#args;
|
|
248
265
|
if (args == null || typeof args !== "object") return;
|
|
249
266
|
|
|
250
|
-
const
|
|
267
|
+
const previewArgs = getArgsWithStreamedTextInput(args);
|
|
268
|
+
const partialJson = (previewArgs as { __partialJson?: string }).__partialJson;
|
|
251
269
|
let effectiveArgs: unknown;
|
|
252
270
|
try {
|
|
253
|
-
effectiveArgs = strategy.extractCompleteEdits(
|
|
271
|
+
effectiveArgs = strategy.extractCompleteEdits(previewArgs, partialJson);
|
|
254
272
|
} catch {
|
|
255
|
-
effectiveArgs =
|
|
273
|
+
effectiveArgs = previewArgs;
|
|
256
274
|
}
|
|
257
275
|
|
|
258
276
|
// Coalesce duplicate computes for identical args. The key pairs the
|
|
@@ -717,20 +735,21 @@ export class ToolExecutionComponent extends Container {
|
|
|
717
735
|
}
|
|
718
736
|
|
|
719
737
|
#getCallArgsForRender(): any {
|
|
738
|
+
const renderArgs = getArgsWithStreamedTextInput(this.#args);
|
|
720
739
|
if (!isEditLikeToolName(this.#toolName)) {
|
|
721
|
-
return
|
|
740
|
+
return renderArgs;
|
|
722
741
|
}
|
|
723
742
|
const previews = this.#editDiffPreview;
|
|
724
743
|
if (!previews || previews.length === 0) {
|
|
725
|
-
return
|
|
744
|
+
return renderArgs;
|
|
726
745
|
}
|
|
727
746
|
// Single-file previews feed the existing `previewDiff` channel consumed
|
|
728
747
|
// by `formatStreamingDiff` in the renderer.
|
|
729
748
|
const first = previews[0];
|
|
730
749
|
if (!first?.diff) {
|
|
731
|
-
return
|
|
750
|
+
return renderArgs;
|
|
732
751
|
}
|
|
733
|
-
return { ...(
|
|
752
|
+
return { ...(renderArgs as Record<string, unknown>), previewDiff: first.diff };
|
|
734
753
|
}
|
|
735
754
|
|
|
736
755
|
/**
|
|
@@ -781,7 +800,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
781
800
|
if (!previews?.some(preview => preview.diff)) {
|
|
782
801
|
const editMode = this.#editMode;
|
|
783
802
|
const strategy = editMode ? EDIT_MODE_STRATEGIES[editMode] : undefined;
|
|
784
|
-
const fallback = strategy?.renderStreamingFallback(this.#args, theme);
|
|
803
|
+
const fallback = strategy?.renderStreamingFallback(getArgsWithStreamedTextInput(this.#args), theme);
|
|
785
804
|
if (fallback) context.editStreamingFallback = fallback;
|
|
786
805
|
}
|
|
787
806
|
context.renderDiff = renderDiff;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type Component,
|
|
4
4
|
Container,
|
|
5
5
|
extractPrintableText,
|
|
6
|
+
fuzzyMatch,
|
|
6
7
|
Input,
|
|
7
8
|
matchesKey,
|
|
8
9
|
Spacer,
|
|
@@ -325,10 +326,10 @@ class TreeList implements Component {
|
|
|
325
326
|
|
|
326
327
|
if (!passesFilter) return false;
|
|
327
328
|
|
|
328
|
-
// Apply search filter
|
|
329
|
+
// Apply fuzzy search filter
|
|
329
330
|
if (searchTokens.length > 0) {
|
|
330
|
-
const nodeText = this.#getSearchableText(flatNode.node)
|
|
331
|
-
return searchTokens.every(token =>
|
|
331
|
+
const nodeText = this.#getSearchableText(flatNode.node);
|
|
332
|
+
return searchTokens.every(token => fuzzyMatch(token, nodeText).matches);
|
|
332
333
|
}
|
|
333
334
|
|
|
334
335
|
return true;
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
Container,
|
|
4
|
+
extractPrintableText,
|
|
5
|
+
fuzzyFilter,
|
|
6
|
+
matchesKey,
|
|
7
|
+
Spacer,
|
|
8
|
+
Text,
|
|
9
|
+
truncateToWidth,
|
|
10
|
+
} from "@oh-my-pi/pi-tui";
|
|
2
11
|
import { theme } from "../../modes/theme/theme";
|
|
3
12
|
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
4
13
|
import { DynamicBorder } from "./dynamic-border";
|
|
@@ -13,6 +22,8 @@ interface UserMessageItem {
|
|
|
13
22
|
* Custom user message list component with selection
|
|
14
23
|
*/
|
|
15
24
|
class UserMessageList implements Component {
|
|
25
|
+
#filteredMessages: UserMessageItem[];
|
|
26
|
+
#searchQuery = "";
|
|
16
27
|
#selectedIndex: number = 0;
|
|
17
28
|
onSelect?: (entryId: string) => void;
|
|
18
29
|
onCancel?: () => void;
|
|
@@ -20,14 +31,60 @@ class UserMessageList implements Component {
|
|
|
20
31
|
|
|
21
32
|
constructor(private readonly messages: UserMessageItem[]) {
|
|
22
33
|
// Store messages in chronological order (oldest to newest)
|
|
34
|
+
this.#filteredMessages = messages;
|
|
23
35
|
// Start with the last (most recent) message selected
|
|
24
|
-
this.#selectedIndex = Math.max(0, this.
|
|
36
|
+
this.#selectedIndex = Math.max(0, this.#filteredMessages.length - 1);
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
invalidate(): void {
|
|
28
40
|
// No cached state to invalidate currently
|
|
29
41
|
}
|
|
30
42
|
|
|
43
|
+
#isSearchEnabled(): boolean {
|
|
44
|
+
return this.messages.length > this.#maxVisible;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#shouldRenderSearchStatus(): boolean {
|
|
48
|
+
return this.#isSearchEnabled() || this.#searchQuery.length > 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#renderStatusLine(total: number): string {
|
|
52
|
+
const selectedCount = total === 0 ? 0 : this.#selectedIndex + 1;
|
|
53
|
+
const count =
|
|
54
|
+
this.#searchQuery.trim() && total !== this.messages.length
|
|
55
|
+
? `${selectedCount}/${total} of ${this.messages.length}`
|
|
56
|
+
: `${selectedCount}/${total}`;
|
|
57
|
+
const suffix = this.#searchQuery.trim() ? ` Search: ${this.#searchQuery}` : " Type to search";
|
|
58
|
+
return theme.fg("muted", ` (${count})${suffix}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#setSearchQuery(query: string): void {
|
|
62
|
+
this.#searchQuery = query;
|
|
63
|
+
this.#filteredMessages = query.trim()
|
|
64
|
+
? fuzzyFilter(this.messages, query, message => `${message.text} ${message.timestamp ?? ""}`)
|
|
65
|
+
: this.messages;
|
|
66
|
+
this.#selectedIndex = query.trim() ? 0 : Math.max(0, this.#filteredMessages.length - 1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#handleSearchInput(keyData: string): boolean {
|
|
70
|
+
if (!this.#isSearchEnabled()) return false;
|
|
71
|
+
|
|
72
|
+
if (matchesKey(keyData, "backspace")) {
|
|
73
|
+
if (this.#searchQuery.length === 0) return false;
|
|
74
|
+
const chars = [...this.#searchQuery];
|
|
75
|
+
chars.pop();
|
|
76
|
+
this.#setSearchQuery(chars.join(""));
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const printableText = extractPrintableText(keyData);
|
|
81
|
+
if (printableText === undefined) return false;
|
|
82
|
+
if (this.#searchQuery.length === 0 && printableText.trim().length === 0) return false;
|
|
83
|
+
|
|
84
|
+
this.#setSearchQuery(this.#searchQuery + printableText);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
31
88
|
render(width: number): string[] {
|
|
32
89
|
const lines: string[] = [];
|
|
33
90
|
|
|
@@ -36,16 +93,19 @@ class UserMessageList implements Component {
|
|
|
36
93
|
return lines;
|
|
37
94
|
}
|
|
38
95
|
|
|
96
|
+
const total = this.#filteredMessages.length;
|
|
97
|
+
|
|
39
98
|
// Calculate visible range with scrolling
|
|
40
99
|
const startIndex = Math.max(
|
|
41
100
|
0,
|
|
42
|
-
Math.min(this.#selectedIndex - Math.floor(this.#maxVisible / 2),
|
|
101
|
+
Math.min(this.#selectedIndex - Math.floor(this.#maxVisible / 2), total - this.#maxVisible),
|
|
43
102
|
);
|
|
44
|
-
const endIndex = Math.min(startIndex + this.#maxVisible,
|
|
103
|
+
const endIndex = Math.min(startIndex + this.#maxVisible, total);
|
|
45
104
|
|
|
46
105
|
// Render visible messages (2 lines per message + blank line)
|
|
47
106
|
for (let i = startIndex; i < endIndex; i++) {
|
|
48
|
-
const message = this
|
|
107
|
+
const message = this.#filteredMessages[i];
|
|
108
|
+
if (!message) continue;
|
|
49
109
|
const isSelected = i === this.#selectedIndex;
|
|
50
110
|
|
|
51
111
|
// Normalize message to single line
|
|
@@ -60,44 +120,59 @@ class UserMessageList implements Component {
|
|
|
60
120
|
lines.push(messageLine);
|
|
61
121
|
|
|
62
122
|
// Second line: metadata (position in history)
|
|
63
|
-
const position =
|
|
123
|
+
const position = this.messages.indexOf(message) + 1;
|
|
64
124
|
const metadata = ` Message ${position} of ${this.messages.length}`;
|
|
65
125
|
const metadataLine = theme.fg("muted", metadata);
|
|
66
126
|
lines.push(metadataLine);
|
|
67
127
|
lines.push(""); // Blank line between messages
|
|
68
128
|
}
|
|
69
129
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
130
|
+
if (total === 0) {
|
|
131
|
+
lines.push(theme.fg("muted", " No matching messages"));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add scroll/search indicator if needed
|
|
135
|
+
if (startIndex > 0 || endIndex < total || this.#shouldRenderSearchStatus()) {
|
|
136
|
+
lines.push(this.#renderStatusLine(total));
|
|
74
137
|
}
|
|
75
138
|
|
|
76
139
|
return lines;
|
|
77
140
|
}
|
|
78
141
|
|
|
79
142
|
handleInput(keyData: string): void {
|
|
143
|
+
// Escape / cancel
|
|
144
|
+
if (matchesSelectCancel(keyData)) {
|
|
145
|
+
if (this.onCancel) {
|
|
146
|
+
this.onCancel();
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (this.#handleSearchInput(keyData)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
80
155
|
// Up arrow - go to previous (older) message, wrap to bottom when at top
|
|
81
156
|
if (matchesSelectUp(keyData)) {
|
|
82
|
-
|
|
157
|
+
if (this.#filteredMessages.length > 0) {
|
|
158
|
+
this.#selectedIndex =
|
|
159
|
+
this.#selectedIndex === 0 ? this.#filteredMessages.length - 1 : this.#selectedIndex - 1;
|
|
160
|
+
}
|
|
83
161
|
}
|
|
84
162
|
// Down arrow - go to next (newer) message, wrap to top when at bottom
|
|
85
163
|
else if (matchesSelectDown(keyData)) {
|
|
86
|
-
|
|
164
|
+
if (this.#filteredMessages.length > 0) {
|
|
165
|
+
this.#selectedIndex =
|
|
166
|
+
this.#selectedIndex === this.#filteredMessages.length - 1 ? 0 : this.#selectedIndex + 1;
|
|
167
|
+
}
|
|
87
168
|
}
|
|
88
169
|
// Enter - select message and branch
|
|
89
170
|
else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
90
|
-
const selected = this
|
|
171
|
+
const selected = this.#filteredMessages[this.#selectedIndex];
|
|
91
172
|
if (selected && this.onSelect) {
|
|
92
173
|
this.onSelect(selected.id);
|
|
93
174
|
}
|
|
94
175
|
}
|
|
95
|
-
// Escape / cancel
|
|
96
|
-
else if (matchesSelectCancel(keyData)) {
|
|
97
|
-
if (this.onCancel) {
|
|
98
|
-
this.onCancel();
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
176
|
}
|
|
102
177
|
}
|
|
103
178
|
|
|
@@ -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,62 @@ export class CommandController {
|
|
|
1122
1123
|
return this.executeCompaction(customInstructions, false);
|
|
1123
1124
|
}
|
|
1124
1125
|
|
|
1126
|
+
/**
|
|
1127
|
+
* TUI handler for `/shake`. `elide`/`images` are instant structural drops;
|
|
1128
|
+
* `summary` runs the local on-device compressor behind a cancelable loader
|
|
1129
|
+
* (Esc aborts via `abortCompaction`). Rebuilds the chat and reports counts.
|
|
1130
|
+
*/
|
|
1131
|
+
async handleShakeCommand(mode: ShakeMode): Promise<void> {
|
|
1132
|
+
let result: ShakeResult;
|
|
1133
|
+
if (mode === "summary") {
|
|
1134
|
+
if (this.ctx.loadingAnimation) {
|
|
1135
|
+
this.ctx.loadingAnimation.stop();
|
|
1136
|
+
this.ctx.loadingAnimation = undefined;
|
|
1137
|
+
}
|
|
1138
|
+
this.ctx.statusContainer.clear();
|
|
1139
|
+
const originalOnEscape = this.ctx.editor.onEscape;
|
|
1140
|
+
this.ctx.editor.onEscape = () => {
|
|
1141
|
+
this.ctx.session.abortCompaction();
|
|
1142
|
+
};
|
|
1143
|
+
const loader = new Loader(
|
|
1144
|
+
this.ctx.ui,
|
|
1145
|
+
spinner => theme.fg("accent", spinner),
|
|
1146
|
+
text => theme.fg("muted", text),
|
|
1147
|
+
"Shaking context (summary)… (esc to cancel)",
|
|
1148
|
+
getSymbolTheme().spinnerFrames,
|
|
1149
|
+
);
|
|
1150
|
+
this.ctx.statusContainer.addChild(loader);
|
|
1151
|
+
this.ctx.ui.requestRender();
|
|
1152
|
+
try {
|
|
1153
|
+
result = await this.ctx.session.shake("summary");
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
this.ctx.showError(`Shake failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1156
|
+
return;
|
|
1157
|
+
} finally {
|
|
1158
|
+
loader.stop();
|
|
1159
|
+
this.ctx.statusContainer.clear();
|
|
1160
|
+
this.ctx.editor.onEscape = originalOnEscape;
|
|
1161
|
+
}
|
|
1162
|
+
} else {
|
|
1163
|
+
try {
|
|
1164
|
+
result = await this.ctx.session.shake(mode);
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
this.ctx.showError(`Shake failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const dropped = result.toolResultsDropped + result.blocksDropped + (result.imagesDropped ?? 0);
|
|
1172
|
+
if (dropped === 0) {
|
|
1173
|
+
this.ctx.showStatus("Nothing to shake.");
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
this.ctx.rebuildChatFromMessages();
|
|
1177
|
+
this.ctx.statusLine.invalidate();
|
|
1178
|
+
this.ctx.updateEditorTopBorder();
|
|
1179
|
+
this.ctx.showStatus(formatShakeSummary(result));
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1125
1182
|
async handleSkillCommand(skillPath: string, args: string): Promise<void> {
|
|
1126
1183
|
try {
|
|
1127
1184
|
const content = await Bun.file(skillPath).text();
|
|
@@ -25,6 +25,17 @@ type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
|
25
25
|
|
|
26
26
|
const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
|
|
27
27
|
|
|
28
|
+
// Events that change which foreground tools are executing, or that reset a turn.
|
|
29
|
+
// The eager native-scrollback rebuild mode is recomputed only on these — other
|
|
30
|
+
// events (assistant text streaming, IRC, notices) leave it untouched so plain
|
|
31
|
+
// streaming keeps the no-yank deferral.
|
|
32
|
+
const TOOL_RENDER_MODE_EVENTS: Record<string, true> = {
|
|
33
|
+
agent_start: true,
|
|
34
|
+
tool_execution_start: true,
|
|
35
|
+
tool_execution_update: true,
|
|
36
|
+
tool_execution_end: true,
|
|
37
|
+
};
|
|
38
|
+
|
|
28
39
|
type AgentSessionEventHandlers = {
|
|
29
40
|
[E in AgentSessionEventKind]: (event: Extract<AgentSessionEvent, { type: E }>) => Promise<void>;
|
|
30
41
|
};
|
|
@@ -65,7 +76,11 @@ export class EventController {
|
|
|
65
76
|
todo_auto_clear: e => this.#handleTodoAutoClear(e),
|
|
66
77
|
irc_message: e => this.#handleIrcMessage(e),
|
|
67
78
|
notice: e => this.#handleNotice(e),
|
|
68
|
-
thinking_level_changed: async () => {
|
|
79
|
+
thinking_level_changed: async () => {
|
|
80
|
+
this.ctx.statusLine.invalidate();
|
|
81
|
+
this.ctx.updateEditorBorderColor();
|
|
82
|
+
this.ctx.ui.requestRender();
|
|
83
|
+
},
|
|
69
84
|
goal_updated: async () => {},
|
|
70
85
|
} satisfies AgentSessionEventHandlers;
|
|
71
86
|
}
|
|
@@ -154,6 +169,27 @@ export class EventController {
|
|
|
154
169
|
|
|
155
170
|
const run = this.#handlers[event.type] as (e: AgentSessionEvent) => Promise<void>;
|
|
156
171
|
await run(event);
|
|
172
|
+
// While a foreground tool is executing, its streaming result re-renders and can
|
|
173
|
+
// re-lay-out rows that already scrolled into native scrollback. Let the TUI
|
|
174
|
+
// rebuild history on those offscreen edits (a snap to the tail is acceptable
|
|
175
|
+
// mid-tool) instead of deferring, which would leave stale/duplicated rows.
|
|
176
|
+
// Background-running tools are excluded so their late async updates — and the
|
|
177
|
+
// assistant text that streams alongside them — keep the no-yank deferral;
|
|
178
|
+
// agent_start resets the mode at every turn boundary.
|
|
179
|
+
if (TOOL_RENDER_MODE_EVENTS[event.type]) {
|
|
180
|
+
this.#refreshToolRenderMode();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#refreshToolRenderMode(): void {
|
|
185
|
+
let foregroundToolActive = false;
|
|
186
|
+
for (const toolCallId of this.ctx.pendingTools.keys()) {
|
|
187
|
+
if (!this.#backgroundToolCallIds.has(toolCallId)) {
|
|
188
|
+
foregroundToolActive = true;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
this.ctx.ui.setEagerNativeScrollbackRebuild(foregroundToolActive);
|
|
157
193
|
}
|
|
158
194
|
|
|
159
195
|
async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
|
|
@@ -606,7 +642,14 @@ export class EventController {
|
|
|
606
642
|
: event.reason === "idle"
|
|
607
643
|
? "Idle "
|
|
608
644
|
: "";
|
|
609
|
-
const actionLabel =
|
|
645
|
+
const actionLabel =
|
|
646
|
+
event.action === "handoff"
|
|
647
|
+
? "Auto-handoff"
|
|
648
|
+
: event.action === "shake"
|
|
649
|
+
? "Auto-shake"
|
|
650
|
+
: event.action === "shake-summary"
|
|
651
|
+
? "Auto-shake (summary)"
|
|
652
|
+
: "Auto context-full maintenance";
|
|
610
653
|
this.ctx.autoCompactionLoader = new Loader(
|
|
611
654
|
this.ctx.ui,
|
|
612
655
|
spinner => theme.fg("accent", spinner),
|
|
@@ -630,8 +673,27 @@ export class EventController {
|
|
|
630
673
|
this.ctx.statusContainer.clear();
|
|
631
674
|
}
|
|
632
675
|
const isHandoffAction = event.action === "handoff";
|
|
676
|
+
const isShakeAction = event.action === "shake" || event.action === "shake-summary";
|
|
633
677
|
if (event.aborted) {
|
|
634
|
-
this.ctx.showStatus(
|
|
678
|
+
this.ctx.showStatus(
|
|
679
|
+
isHandoffAction
|
|
680
|
+
? "Auto-handoff cancelled"
|
|
681
|
+
: isShakeAction
|
|
682
|
+
? "Auto-shake cancelled"
|
|
683
|
+
: "Auto context-full maintenance cancelled",
|
|
684
|
+
);
|
|
685
|
+
} else if (isShakeAction) {
|
|
686
|
+
// Shake produces no CompactionResult; rebuild on success, suppress benign skips.
|
|
687
|
+
if (event.errorMessage) {
|
|
688
|
+
this.ctx.showWarning(event.errorMessage);
|
|
689
|
+
} else if (!event.skipped) {
|
|
690
|
+
this.ctx.rebuildChatFromMessages();
|
|
691
|
+
this.ctx.statusLine.invalidate();
|
|
692
|
+
this.ctx.updateEditorTopBorder();
|
|
693
|
+
this.ctx.showStatus(
|
|
694
|
+
event.action === "shake-summary" ? "Auto-shake (summary) completed" : "Auto-shake completed",
|
|
695
|
+
);
|
|
696
|
+
}
|
|
635
697
|
} else if (event.result) {
|
|
636
698
|
this.ctx.rebuildChatFromMessages();
|
|
637
699
|
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 {
|