@oh-my-pi/pi-coding-agent 15.7.2 → 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 +75 -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 +88 -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/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 +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/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 +40 -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/tiny/models.d.ts +15 -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/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 +94 -67
- package/src/config/settings.ts +12 -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/main.ts +44 -55
- 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 +57 -0
- package/src/modes/controllers/event-controller.ts +60 -2
- 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 +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/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 +63 -33
- package/src/session/agent-session.ts +373 -56
- 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/tiny/models.ts +30 -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
|
@@ -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
|
};
|
|
@@ -158,6 +169,27 @@ export class EventController {
|
|
|
158
169
|
|
|
159
170
|
const run = this.#handlers[event.type] as (e: AgentSessionEvent) => Promise<void>;
|
|
160
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);
|
|
161
193
|
}
|
|
162
194
|
|
|
163
195
|
async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
|
|
@@ -610,7 +642,14 @@ export class EventController {
|
|
|
610
642
|
: event.reason === "idle"
|
|
611
643
|
? "Idle "
|
|
612
644
|
: "";
|
|
613
|
-
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";
|
|
614
653
|
this.ctx.autoCompactionLoader = new Loader(
|
|
615
654
|
this.ctx.ui,
|
|
616
655
|
spinner => theme.fg("accent", spinner),
|
|
@@ -634,8 +673,27 @@ export class EventController {
|
|
|
634
673
|
this.ctx.statusContainer.clear();
|
|
635
674
|
}
|
|
636
675
|
const isHandoffAction = event.action === "handoff";
|
|
676
|
+
const isShakeAction = event.action === "shake" || event.action === "shake-summary";
|
|
637
677
|
if (event.aborted) {
|
|
638
|
-
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
|
+
}
|
|
639
697
|
} else if (event.result) {
|
|
640
698
|
this.ctx.rebuildChatFromMessages();
|
|
641
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 {
|