@oh-my-pi/pi-coding-agent 16.1.10 → 16.1.11
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 +31 -0
- package/dist/cli.js +2811 -2812
- package/dist/types/export/html/index.d.ts +31 -2
- package/dist/types/export/html/web-palette.d.ts +117 -0
- package/dist/types/hindsight/content.d.ts +7 -0
- package/dist/types/hindsight/transcript.d.ts +1 -1
- package/dist/types/modes/components/hook-editor.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +5 -0
- package/dist/types/modes/types.d.ts +17 -0
- package/dist/types/modes/utils/keybinding-matchers.d.ts +13 -0
- package/dist/types/session/agent-session.d.ts +4 -0
- package/dist/types/session/session-context.d.ts +2 -0
- package/dist/types/session/session-entries.d.ts +6 -0
- package/dist/types/session/session-manager.d.ts +2 -1
- package/dist/types/tools/acp-bridge.d.ts +29 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +7 -0
- package/dist/types/tools/browser/tab-worker.d.ts +20 -0
- package/dist/types/utils/image-loading.d.ts +4 -3
- package/dist/types/utils/jj.d.ts +25 -0
- package/dist/types/utils/title-generator.d.ts +0 -2
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -2
- package/src/autoresearch/git.ts +12 -0
- package/src/discovery/opencode.ts +47 -4
- package/src/edit/hashline/filesystem.ts +8 -0
- package/src/edit/modes/patch.ts +18 -2
- package/src/edit/modes/replace.ts +13 -10
- package/src/export/html/index.ts +50 -8
- package/src/export/html/web-palette.ts +142 -0
- package/src/hindsight/backend.ts +4 -4
- package/src/hindsight/content.ts +17 -1
- package/src/hindsight/transcript.ts +2 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/modes/components/agent-dashboard.ts +8 -8
- package/src/modes/components/hook-editor.ts +13 -10
- package/src/modes/components/session-selector.ts +3 -0
- package/src/modes/controllers/event-controller.ts +9 -5
- package/src/modes/controllers/extension-ui-controller.ts +6 -2
- package/src/modes/interactive-mode.ts +69 -29
- package/src/modes/theme/dark.json +1 -1
- package/src/modes/types.ts +18 -0
- package/src/modes/utils/keybinding-matchers.ts +36 -1
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/browser.md +3 -2
- package/src/sdk.ts +12 -1
- package/src/session/agent-session.ts +38 -14
- package/src/session/session-context.ts +5 -0
- package/src/session/session-entries.ts +6 -0
- package/src/session/session-manager.ts +3 -1
- package/src/task/worktree.ts +12 -4
- package/src/thinking.ts +1 -1
- package/src/tools/acp-bridge.ts +66 -0
- package/src/tools/browser/cmux/cmux-tab.ts +37 -0
- package/src/tools/browser/tab-worker.ts +160 -37
- package/src/tools/write.ts +2 -25
- package/src/utils/image-loading.ts +6 -3
- package/src/utils/jj.ts +47 -0
- package/src/utils/title-generator.ts +31 -99
|
@@ -52,7 +52,12 @@ import { discoverAgents } from "../../task/discovery";
|
|
|
52
52
|
import type { AgentDefinition, AgentSource } from "../../task/types";
|
|
53
53
|
import { shortenPath } from "../../tools/render-utils";
|
|
54
54
|
import { getEditorTheme, theme } from "../theme/theme";
|
|
55
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
matchesAppFollowUp,
|
|
57
|
+
matchesAppInterrupt,
|
|
58
|
+
matchesSelectDown,
|
|
59
|
+
matchesSelectUp,
|
|
60
|
+
} from "../utils/keybinding-matchers";
|
|
56
61
|
import { DynamicBorder } from "./dynamic-border";
|
|
57
62
|
|
|
58
63
|
type SourceTabId = "all" | AgentSource;
|
|
@@ -652,11 +657,6 @@ export class AgentDashboard extends Container {
|
|
|
652
657
|
this.#buildLayout();
|
|
653
658
|
}
|
|
654
659
|
|
|
655
|
-
#shouldSubmitCreateDescription(data: string): boolean {
|
|
656
|
-
if (matchesKey(data, "ctrl+enter")) return true;
|
|
657
|
-
return process.platform === "win32" && data === "\n" && this.#createDescription.trim().length > 0;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
660
|
async #generateAgentFromDescription(rawDescription: string): Promise<void> {
|
|
661
661
|
const description = rawDescription.trim();
|
|
662
662
|
this.#createDescription = description;
|
|
@@ -911,7 +911,7 @@ export class AgentDashboard extends Container {
|
|
|
911
911
|
this.addChild(new Spacer(1));
|
|
912
912
|
const hints = this.#createGenerating
|
|
913
913
|
? " Generating..."
|
|
914
|
-
: " Ctrl+Enter: generate Enter: newline Tab: toggle scope Esc: cancel";
|
|
914
|
+
: " Ctrl+Q/Ctrl+Enter: generate Enter: newline Tab: toggle scope Esc: cancel";
|
|
915
915
|
this.addChild(new Text(theme.fg("dim", hints), 0, 0));
|
|
916
916
|
}
|
|
917
917
|
|
|
@@ -1102,7 +1102,7 @@ export class AgentDashboard extends Container {
|
|
|
1102
1102
|
}
|
|
1103
1103
|
return;
|
|
1104
1104
|
}
|
|
1105
|
-
if (!this.#createGenerating &&
|
|
1105
|
+
if (!this.#createGenerating && matchesAppFollowUp(data)) {
|
|
1106
1106
|
this.#submitCreateDescription();
|
|
1107
1107
|
return;
|
|
1108
1108
|
}
|
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
* Supports Ctrl+G for external editor.
|
|
4
4
|
*
|
|
5
5
|
* Two modes:
|
|
6
|
-
* - Default (hook): Enter inserts newline,
|
|
6
|
+
* - Default (hook): Enter inserts newline, the `app.message.followUp` chord
|
|
7
|
+
* (Ctrl+Q / Ctrl+Enter) submits, bordered popup
|
|
7
8
|
* - Prompt-style (ask): Enter submits, Shift+Enter inserts newline, legacy ask chrome
|
|
8
9
|
*/
|
|
9
10
|
import { Container, Editor, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
10
11
|
import { getEditorTheme, theme } from "../../modes/theme/theme";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
matchesAppExternalEditor,
|
|
14
|
+
matchesAppFollowUp,
|
|
15
|
+
matchesAppInterrupt,
|
|
16
|
+
} from "../../modes/utils/keybinding-matchers";
|
|
12
17
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
13
18
|
import { DynamicBorder } from "./dynamic-border";
|
|
14
19
|
|
|
@@ -17,10 +22,6 @@ export interface HookEditorOptions {
|
|
|
17
22
|
promptStyle?: boolean;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
function isCtrlEnterSubmit(keyData: string): boolean {
|
|
21
|
-
return matchesKey(keyData, "ctrl+enter") || (keyData.charCodeAt(0) === 10 && keyData.length > 1);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
25
|
export class HookEditorComponent extends Container {
|
|
25
26
|
#editor: Editor;
|
|
26
27
|
#onSubmitCallback: (value: string) => void;
|
|
@@ -67,7 +68,7 @@ export class HookEditorComponent extends Container {
|
|
|
67
68
|
// Hint
|
|
68
69
|
const hint = this.#promptStyle
|
|
69
70
|
? "enter submit esc cancel ctrl+g external editor"
|
|
70
|
-
: "ctrl+enter submit esc cancel ctrl+g external editor";
|
|
71
|
+
: "ctrl+q/ctrl+enter submit esc cancel ctrl+g external editor";
|
|
71
72
|
this.addChild(new Text(theme.fg("dim", hint), 1, 0));
|
|
72
73
|
|
|
73
74
|
this.addChild(new Spacer(1));
|
|
@@ -118,10 +119,12 @@ export class HookEditorComponent extends Container {
|
|
|
118
119
|
this.#editor.handleInput(keyData);
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
/** Hook-style: Enter=newline, Ctrl+Enter
|
|
122
|
+
/** Hook-style: Enter=newline, app.message.followUp chord (Ctrl+Q/Ctrl+Enter) submits. */
|
|
122
123
|
#handleHookStyleInput(keyData: string): void {
|
|
123
|
-
//
|
|
124
|
-
|
|
124
|
+
// Submit on the follow-up chord. Uses the shared keybinding so Ctrl+Q works
|
|
125
|
+
// on Windows Terminal (#1903) and any user remap of `app.message.followUp`
|
|
126
|
+
// applies here too.
|
|
127
|
+
if (matchesAppFollowUp(keyData)) {
|
|
125
128
|
this.#submitCurrentText();
|
|
126
129
|
return;
|
|
127
130
|
}
|
|
@@ -353,6 +353,9 @@ class SessionList implements Component {
|
|
|
353
353
|
if (status) {
|
|
354
354
|
metadata += ` ${dot} ${status}`;
|
|
355
355
|
}
|
|
356
|
+
if (session.parentSessionPath) {
|
|
357
|
+
metadata += ` ${dot} ${dim(`${theme.icon.branch} fork`)}`;
|
|
358
|
+
}
|
|
356
359
|
if (this.#showCwd && session.cwd) {
|
|
357
360
|
metadata += ` ${dot} ${dim(shortenPath(session.cwd))}`;
|
|
358
361
|
}
|
|
@@ -296,8 +296,15 @@ export class EventController {
|
|
|
296
296
|
this.#resetReadGroup();
|
|
297
297
|
this.#resolveDisplaceablePoll();
|
|
298
298
|
const wasOptimistic = this.ctx.optimisticUserMessageSignature === signature;
|
|
299
|
-
const
|
|
300
|
-
|
|
299
|
+
const matchedLocalSubmission = this.ctx.locallySubmittedUserSignatures.delete(signature);
|
|
300
|
+
const replacesOptimistic =
|
|
301
|
+
this.ctx.optimisticUserMessageSignature !== undefined && !wasOptimistic && !matchedLocalSubmission;
|
|
302
|
+
const wasLocallySubmitted = matchedLocalSubmission || wasOptimistic || replacesOptimistic;
|
|
303
|
+
if (wasOptimistic) {
|
|
304
|
+
this.ctx.clearOptimisticUserMessage();
|
|
305
|
+
} else if (replacesOptimistic) {
|
|
306
|
+
this.ctx.replaceOptimisticUserMessage(event.message);
|
|
307
|
+
} else {
|
|
301
308
|
// Append synchronously: #emit dispatches to this listener fire-and-forget
|
|
302
309
|
// (see AgentSession.#emit), so any await between the user message_start and
|
|
303
310
|
// addMessageToChat lets later events (assistant message_start, tool execution
|
|
@@ -306,9 +313,6 @@ export class EventController {
|
|
|
306
313
|
// links via the synchronous putBlobSync fallback, so no await is needed here.
|
|
307
314
|
this.ctx.addMessageToChat(event.message);
|
|
308
315
|
}
|
|
309
|
-
if (wasOptimistic) {
|
|
310
|
-
this.ctx.optimisticUserMessageSignature = undefined;
|
|
311
|
-
}
|
|
312
316
|
|
|
313
317
|
// Clear the editor only when the submission did not originate from a
|
|
314
318
|
// local submission (optimistic or queued-while-streaming). Both local
|
|
@@ -810,8 +810,12 @@ export class ExtensionUiController {
|
|
|
810
810
|
|
|
811
811
|
#applyCustomMessageDisplay(wasStreaming: boolean, shouldDisplay: boolean | undefined): void {
|
|
812
812
|
// For non-streaming cases with display=true, update UI
|
|
813
|
-
// (streaming cases update via message_end event)
|
|
814
|
-
|
|
813
|
+
// (streaming cases update via message_end event).
|
|
814
|
+
// Gate on initialChatRendered (#1955): an extension's session_start
|
|
815
|
+
// sendMessage({display:true}) runs before renderInitialMessages, which would
|
|
816
|
+
// re-render from session entries AND re-append via preserveExistingChat,
|
|
817
|
+
// duplicating the message. After the initial render the rebuild must run.
|
|
818
|
+
if (!wasStreaming && shouldDisplay && this.ctx.initialChatRendered) {
|
|
815
819
|
this.ctx.rebuildChatFromMessages();
|
|
816
820
|
}
|
|
817
821
|
}
|
|
@@ -390,6 +390,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
390
390
|
statusLine: StatusLineComponent;
|
|
391
391
|
|
|
392
392
|
isInitialized = false;
|
|
393
|
+
initialChatRendered = false;
|
|
393
394
|
isBashMode = false;
|
|
394
395
|
toolOutputExpanded = false;
|
|
395
396
|
todoExpanded = false;
|
|
@@ -435,6 +436,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
435
436
|
locallySubmittedUserSignatures: Set<string> = new Set();
|
|
436
437
|
#pendingSubmittedInput: SubmittedUserInput | undefined;
|
|
437
438
|
#pendingSubmissionDispose: (() => void) | undefined;
|
|
439
|
+
#optimisticUserMessageComponents: Component[] = [];
|
|
438
440
|
lastSigintTime = 0;
|
|
439
441
|
lastEscapeTime = 0;
|
|
440
442
|
lastLeftTapTime = 0;
|
|
@@ -1247,6 +1249,32 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1247
1249
|
throw err;
|
|
1248
1250
|
}
|
|
1249
1251
|
}
|
|
1252
|
+
#captureAddedChatComponents(render: () => void): Component[] {
|
|
1253
|
+
const start = this.chatContainer.children.length;
|
|
1254
|
+
render();
|
|
1255
|
+
return this.chatContainer.children.slice(start);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
clearOptimisticUserMessage(): void {
|
|
1259
|
+
this.optimisticUserMessageSignature = undefined;
|
|
1260
|
+
this.#pendingSubmissionDispose?.();
|
|
1261
|
+
this.#pendingSubmissionDispose = undefined;
|
|
1262
|
+
this.#optimisticUserMessageComponents = [];
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
replaceOptimisticUserMessage(
|
|
1266
|
+
message: AgentMessage,
|
|
1267
|
+
options?: { imageLinks?: readonly (string | undefined)[] },
|
|
1268
|
+
): void {
|
|
1269
|
+
this.optimisticUserMessageSignature = undefined;
|
|
1270
|
+
this.#pendingSubmissionDispose?.();
|
|
1271
|
+
this.#pendingSubmissionDispose = undefined;
|
|
1272
|
+
for (const component of this.#optimisticUserMessageComponents) {
|
|
1273
|
+
this.chatContainer.removeChild(component);
|
|
1274
|
+
}
|
|
1275
|
+
this.#optimisticUserMessageComponents = [];
|
|
1276
|
+
this.addMessageToChat(message, options);
|
|
1277
|
+
}
|
|
1250
1278
|
|
|
1251
1279
|
startPendingSubmission(input: {
|
|
1252
1280
|
text: string;
|
|
@@ -1272,18 +1300,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1272
1300
|
const imageCount = submission.images?.length ?? 0;
|
|
1273
1301
|
this.optimisticUserMessageSignature = `${submission.text}\u0000${imageCount}`;
|
|
1274
1302
|
this.#pendingSubmissionDispose = this.recordLocalSubmission(submission.text, imageCount);
|
|
1275
|
-
this
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1303
|
+
this.#optimisticUserMessageComponents = this.#captureAddedChatComponents(() => {
|
|
1304
|
+
this.addMessageToChat(
|
|
1305
|
+
{
|
|
1306
|
+
role: "user",
|
|
1307
|
+
content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
|
|
1308
|
+
attribution: "user",
|
|
1309
|
+
timestamp: Date.now(),
|
|
1310
|
+
},
|
|
1311
|
+
{ imageLinks: input.imageLinks },
|
|
1312
|
+
);
|
|
1313
|
+
});
|
|
1284
1314
|
} else {
|
|
1285
|
-
this.
|
|
1286
|
-
this.#pendingSubmissionDispose = undefined;
|
|
1315
|
+
this.clearOptimisticUserMessage();
|
|
1287
1316
|
}
|
|
1288
1317
|
this.editor.setText("");
|
|
1289
1318
|
this.editor.imageLinks = undefined;
|
|
@@ -1300,9 +1329,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1300
1329
|
|
|
1301
1330
|
submission.cancelled = true;
|
|
1302
1331
|
this.#pendingSubmittedInput = undefined;
|
|
1303
|
-
this.
|
|
1304
|
-
this.#pendingSubmissionDispose?.();
|
|
1305
|
-
this.#pendingSubmissionDispose = undefined;
|
|
1332
|
+
this.clearOptimisticUserMessage();
|
|
1306
1333
|
this.#pendingWorkingMessage = undefined;
|
|
1307
1334
|
if (submission.customType === "goal-continuation") {
|
|
1308
1335
|
this.#goalContinuationTurnInFlight = false;
|
|
@@ -1344,6 +1371,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1344
1371
|
if (wasPendingSubmission && !this.session.isStreaming && !this.streamingComponent) {
|
|
1345
1372
|
this.optimisticUserMessageSignature = undefined;
|
|
1346
1373
|
pendingSubmissionDispose?.();
|
|
1374
|
+
this.#optimisticUserMessageComponents = [];
|
|
1347
1375
|
this.#pendingWorkingMessage = undefined;
|
|
1348
1376
|
if (this.loadingAnimation) {
|
|
1349
1377
|
this.#stopLoadingAnimation(true);
|
|
@@ -1434,15 +1462,17 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1434
1462
|
if (!this.optimisticUserMessageSignature) return;
|
|
1435
1463
|
const submission = this.#pendingSubmittedInput;
|
|
1436
1464
|
if (!submission || submission.cancelled || submission.customType) return;
|
|
1437
|
-
this
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1465
|
+
this.#optimisticUserMessageComponents = this.#captureAddedChatComponents(() => {
|
|
1466
|
+
this.addMessageToChat(
|
|
1467
|
+
{
|
|
1468
|
+
role: "user",
|
|
1469
|
+
content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
|
|
1470
|
+
attribution: "user",
|
|
1471
|
+
timestamp: Date.now(),
|
|
1472
|
+
},
|
|
1473
|
+
{ imageLinks: submission.imageLinks },
|
|
1474
|
+
);
|
|
1475
|
+
});
|
|
1446
1476
|
}
|
|
1447
1477
|
|
|
1448
1478
|
#formatTodoLine(todo: TodoItem, prefix: string, matched: boolean): string {
|
|
@@ -1926,9 +1956,21 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1926
1956
|
|
|
1927
1957
|
const planFilePath = options?.planFilePath ?? (await this.#getPlanFilePath());
|
|
1928
1958
|
const previousTools = this.session.getActiveToolNames();
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1959
|
+
// `plan-mode-active.md` instructs the agent to draft the plan file with
|
|
1960
|
+
// `write` and refine it with `edit`. Both must be in the active set or the
|
|
1961
|
+
// agent falls back to `edit` on a non-existent file and stalls. `edit` is an
|
|
1962
|
+
// essential built-in so it survives `tools.discoveryMode === "all"`, but
|
|
1963
|
+
// `write` has `loadMode: "discoverable"` and is hidden behind
|
|
1964
|
+
// `search_tool_bm25` — re-activate it here only when the current registry
|
|
1965
|
+
// entry is the built-in write tool (issue #3165). A shadowing extension
|
|
1966
|
+
// tool named `write` must stay inactive because plan mode's read-only
|
|
1967
|
+
// guarantee relies on the built-in write/edit guard. `resolve` is hidden
|
|
1968
|
+
// too; the standing handler below consumes plan-approval calls through it.
|
|
1969
|
+
const planAugmentations = ["resolve"];
|
|
1970
|
+
if (this.session.hasBuiltInTool("write")) {
|
|
1971
|
+
planAugmentations.push("write");
|
|
1972
|
+
}
|
|
1973
|
+
const uniquePlanTools = [...new Set([...previousTools, ...planAugmentations])];
|
|
1932
1974
|
|
|
1933
1975
|
this.#planModePreviousTools = previousTools;
|
|
1934
1976
|
this.planModePlanFilePath = planFilePath;
|
|
@@ -3244,9 +3286,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3244
3286
|
|
|
3245
3287
|
showError(message: string): void {
|
|
3246
3288
|
this.#pendingSubmittedInput = undefined;
|
|
3247
|
-
this.
|
|
3248
|
-
this.#pendingSubmissionDispose?.();
|
|
3249
|
-
this.#pendingSubmissionDispose = undefined;
|
|
3289
|
+
this.clearOptimisticUserMessage();
|
|
3250
3290
|
this.#pendingWorkingMessage = undefined;
|
|
3251
3291
|
if (this.loadingAnimation) {
|
|
3252
3292
|
this.#stopLoadingAnimation(true);
|
package/src/modes/types.ts
CHANGED
|
@@ -137,6 +137,17 @@ export interface InteractiveModeContext {
|
|
|
137
137
|
|
|
138
138
|
// State
|
|
139
139
|
isInitialized: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* `true` once `renderInitialMessages` has rendered the session transcript
|
|
142
|
+
* into `chatContainer` at least once.
|
|
143
|
+
*
|
|
144
|
+
* Extension chat-rebuilds (`ExtensionUiController.#applyCustomMessageDisplay`)
|
|
145
|
+
* are gated on this: rebuilding before the initial render would plant a
|
|
146
|
+
* session-derived component into the chat that `renderInitialMessages` then
|
|
147
|
+
* both re-renders from session entries AND re-appends via
|
|
148
|
+
* `preserveExistingChat`, duplicating the message (issue #1955).
|
|
149
|
+
*/
|
|
150
|
+
initialChatRendered: boolean;
|
|
140
151
|
isBashMode: boolean;
|
|
141
152
|
toolOutputExpanded: boolean;
|
|
142
153
|
todoExpanded: boolean;
|
|
@@ -258,6 +269,13 @@ export interface InteractiveModeContext {
|
|
|
258
269
|
* delivery error should leave the signature set untouched.
|
|
259
270
|
*/
|
|
260
271
|
withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: { imageCount?: number }): Promise<T>;
|
|
272
|
+
/** Clears bookkeeping for an optimistic local user message once the matching session event arrives. */
|
|
273
|
+
clearOptimisticUserMessage(): void;
|
|
274
|
+
/** Replaces the raw optimistic user render with the canonical message emitted by the session. */
|
|
275
|
+
replaceOptimisticUserMessage(
|
|
276
|
+
message: AgentMessage,
|
|
277
|
+
options?: { imageLinks?: readonly (string | undefined)[] },
|
|
278
|
+
): void;
|
|
261
279
|
isKnownSlashCommand(text: string): boolean;
|
|
262
280
|
addMessageToChat(
|
|
263
281
|
message: AgentMessage,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getKeybindings, matchesKey } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import { getKeybindings, type KeyId, matchesKey } from "@oh-my-pi/pi-tui";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Match the coding-agent interrupt key.
|
|
@@ -49,3 +49,38 @@ export function matchesAppExternalEditor(data: string): boolean {
|
|
|
49
49
|
}
|
|
50
50
|
return matchesKey(data, "ctrl+g");
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
function matchesEffectiveKey(data: string, key: KeyId): boolean {
|
|
54
|
+
if ((key === "ctrl+enter" || key === "ctrl+return") && data.charCodeAt(0) === 10 && data.length > 1) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return matchesKey(data, key);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function matchesEffectiveKeys(data: string, keys: readonly KeyId[]): boolean {
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
if (matchesEffectiveKey(data, key)) return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Match the "submit multi-line text input" keybinding (`app.message.followUp`).
|
|
69
|
+
*
|
|
70
|
+
* Used by forms where plain Enter inserts a newline and a modified-Enter chord
|
|
71
|
+
* submits — the main editor's follow-up handler, the agent dashboard's new-agent
|
|
72
|
+
* description, and the hook editor's hook-style mode. The keybinding defaults to
|
|
73
|
+
* `["ctrl+q", "ctrl+enter"]` so Windows Terminal (which can't deliver a distinct
|
|
74
|
+
* Ctrl+Enter event; #1903) still has a working chord without user remapping.
|
|
75
|
+
*
|
|
76
|
+
* Also recognizes modifier-tagged LF as Ctrl+Enter only when Ctrl+Enter is an
|
|
77
|
+
* effective follow-up binding.
|
|
78
|
+
*/
|
|
79
|
+
export function matchesAppFollowUp(data: string): boolean {
|
|
80
|
+
const keybindings = getKeybindings();
|
|
81
|
+
const keys = keybindings.getKeys("app.message.followUp");
|
|
82
|
+
if (keys.length > 0) {
|
|
83
|
+
return matchesEffectiveKeys(data, keys);
|
|
84
|
+
}
|
|
85
|
+
return matchesEffectiveKeys(data, ["ctrl+enter", "ctrl+q"]);
|
|
86
|
+
}
|
|
@@ -605,6 +605,7 @@ export class UiHelpers {
|
|
|
605
605
|
// dispose them (stopping any live timers/subscriptions) before clearing. When
|
|
606
606
|
// preserving, the same instances are re-added below, so detach without dispose.
|
|
607
607
|
const preservedChatChildren = options.preserveExistingChat ? this.ctx.chatContainer.children : undefined;
|
|
608
|
+
this.ctx.initialChatRendered = true;
|
|
608
609
|
if (preservedChatChildren) {
|
|
609
610
|
this.ctx.chatContainer.clear();
|
|
610
611
|
} else {
|
|
@@ -19,17 +19,18 @@ Drives real Chromium tab; full puppeteer access via JS.
|
|
|
19
19
|
- `tab.ref("e5")` — `[ref=eN]` from the last ariaSnapshot → element handle with the common action methods (`.click()`, `.type()`, `.fill()`, `.hover()`, `.evaluate()`, …); the primary way to act on a ref. For convenience `aria-ref=e5` also works inline in `tab.click`/`type`/`fill`/`waitFor`/`scrollIntoView` (e.g. `tab.click("aria-ref=e5")`).
|
|
20
20
|
- `tab.id(n)` — id from last observe → `ElementHandle` (`.click()`, `.type()`, …).
|
|
21
21
|
- `tab.click(selector)` / `tab.type(selector, text)` / `tab.fill(selector, value)` / `tab.press(key, { selector? })` / `tab.scroll(dx, dy)`.
|
|
22
|
-
- `tab.waitFor(selector)` — wait until attached; returns `ElementHandle`.
|
|
22
|
+
- `tab.waitFor(selector, { timeout? })` / `tab.waitForSelector(selector, { timeout?, visible?, hidden? })` — wait until attached (optionally visible/hidden); returns the `ElementHandle`.
|
|
23
23
|
- `tab.drag(from, to)` — endpoints: selector (center-to-center) or `{ x, y }` viewport point (canvases, sliders).
|
|
24
24
|
- `tab.scrollIntoView(selector)` — center in viewport; before clicking off-screen elements.
|
|
25
25
|
- `tab.select(selector, …values)` — set `<select>` option(s); returns selection. `tab.fill` NEVER works for selects.
|
|
26
26
|
- `tab.uploadFile(selector, …filePaths)` — attach files to `<input type="file">`; paths relative to cwd.
|
|
27
27
|
- `tab.waitForUrl(pattern, { timeout? })` — substring or `RegExp` (matches SPA pushState nav); returns matched URL.
|
|
28
28
|
- `tab.waitForResponse(pattern, { timeout? })` — substring, `RegExp`, or `(response) => boolean`; returns puppeteer `HTTPResponse` (`.text()`/`.json()`/`.status()`/`.headers()`).
|
|
29
|
+
- `tab.waitForNavigation({ waitUntil?, timeout? })` — resolves on the next navigation. Start it BEFORE the click/submit that triggers it; after `tab.goto` (which already waits) use `tab.waitForUrl`/`tab.waitForSelector` instead.
|
|
29
30
|
- `tab.evaluate(fn, …args)` — `page.evaluate` for ad-hoc DOM reads.
|
|
30
31
|
- `tab.screenshot({ selector?, fullPage?, save?, silent? })` — capture + attach for viewing (`silent: true` skips). Pass `save` only when a later step needs the file.
|
|
31
32
|
- `tab.extract(format = "markdown")` — readable page content (`"markdown"` | `"text"`); throws when nothing readable.
|
|
32
|
-
- Selectors: CSS + puppeteer handlers `aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`; also Playwright-style `p-aria/…`, `p-text/…`.
|
|
33
|
+
- Selectors: CSS + puppeteer handlers `aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`; also Playwright-style `p-aria/…`, `p-text/…`. Playwright-only engines/pseudos (`:has-text()`, `:visible`, …) are rejected — use `text/…` or `aria/…`. A stalled action/wait fails fast with a named `tab.<op> timed out` error, never the whole-cell timeout.
|
|
33
34
|
</instruction>
|
|
34
35
|
|
|
35
36
|
<critical>
|
package/src/sdk.ts
CHANGED
|
@@ -1288,7 +1288,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1288
1288
|
const pickInitialThinkingLevel = (selectedModel: Model | undefined): ConfiguredThinkingLevel | undefined => {
|
|
1289
1289
|
let level = options.thinkingLevel;
|
|
1290
1290
|
if (level === undefined && hasExistingSession && hasThinkingEntry) {
|
|
1291
|
-
level =
|
|
1291
|
+
level =
|
|
1292
|
+
parseConfiguredThinkingLevel(existingSession.configuredThinkingLevel) ??
|
|
1293
|
+
parseThinkingLevel(existingSession.thinkingLevel);
|
|
1292
1294
|
}
|
|
1293
1295
|
if (level === undefined && !hasThinkingEntry && restoredSessionThinkingLevel !== undefined) {
|
|
1294
1296
|
level = restoredSessionThinkingLevel;
|
|
@@ -2019,18 +2021,22 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2019
2021
|
);
|
|
2020
2022
|
|
|
2021
2023
|
// All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
|
|
2024
|
+
const builtInRegistryToolNames = new Set<string>();
|
|
2022
2025
|
const toolRegistry = new Map<string, Tool>();
|
|
2023
2026
|
for (const tool of builtinTools) {
|
|
2024
2027
|
toolRegistry.set(tool.name, tool);
|
|
2028
|
+
builtInRegistryToolNames.add(tool.name);
|
|
2025
2029
|
}
|
|
2026
2030
|
if (!toolRegistry.has("goal") && settings.get("goal.enabled")) {
|
|
2027
2031
|
const goalTool = await logger.time("createTools:goal:session", HIDDEN_TOOLS.goal, toolSession);
|
|
2028
2032
|
if (goalTool) {
|
|
2029
2033
|
toolRegistry.set(goalTool.name, wrapToolWithMetaNotice(goalTool));
|
|
2034
|
+
builtInRegistryToolNames.add(goalTool.name);
|
|
2030
2035
|
}
|
|
2031
2036
|
}
|
|
2032
2037
|
for (const tool of wrappedExtensionTools) {
|
|
2033
2038
|
toolRegistry.set(tool.name, tool);
|
|
2039
|
+
builtInRegistryToolNames.delete(tool.name);
|
|
2034
2040
|
}
|
|
2035
2041
|
if (deferMCPDiscoveryForUI && mcpManager) {
|
|
2036
2042
|
for (const name of collectPendingMCPToolNames(options.toolNames, existingSession.selectedMCPToolNames)) {
|
|
@@ -2048,6 +2054,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2048
2054
|
}
|
|
2049
2055
|
if (model?.provider === "cursor") {
|
|
2050
2056
|
toolRegistry.delete("edit");
|
|
2057
|
+
builtInRegistryToolNames.delete("edit");
|
|
2051
2058
|
}
|
|
2052
2059
|
|
|
2053
2060
|
// `resolve` is hidden but must stay in the registry whenever any code path can invoke it:
|
|
@@ -2060,10 +2067,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2060
2067
|
const needsResolveTool = hasDeferrableTools || planModeAvailable;
|
|
2061
2068
|
if (!needsResolveTool) {
|
|
2062
2069
|
toolRegistry.delete("resolve");
|
|
2070
|
+
builtInRegistryToolNames.delete("resolve");
|
|
2063
2071
|
} else if (!toolRegistry.has("resolve")) {
|
|
2064
2072
|
const resolveTool = await logger.time("createTools:resolve:session", HIDDEN_TOOLS.resolve, toolSession);
|
|
2065
2073
|
if (resolveTool) {
|
|
2066
2074
|
toolRegistry.set(resolveTool.name, wrapToolWithMetaNotice(resolveTool));
|
|
2075
|
+
builtInRegistryToolNames.add(resolveTool.name);
|
|
2067
2076
|
}
|
|
2068
2077
|
}
|
|
2069
2078
|
|
|
@@ -2080,6 +2089,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2080
2089
|
searchTool.name,
|
|
2081
2090
|
new ExtensionToolWrapper(wrapToolWithMetaNotice(searchTool), extensionRunner) as Tool,
|
|
2082
2091
|
);
|
|
2092
|
+
builtInRegistryToolNames.add(searchTool.name);
|
|
2083
2093
|
}
|
|
2084
2094
|
let mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off"; // back-compat: true when any discovery active
|
|
2085
2095
|
|
|
@@ -2623,6 +2633,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2623
2633
|
skillsSettings: settings.getGroup("skills"),
|
|
2624
2634
|
modelRegistry,
|
|
2625
2635
|
toolRegistry,
|
|
2636
|
+
builtInToolNames: builtInRegistryToolNames,
|
|
2626
2637
|
transformContext,
|
|
2627
2638
|
onPayload,
|
|
2628
2639
|
onResponse,
|
|
@@ -210,7 +210,7 @@ import { resolveMemoryBackend } from "../memory-backend";
|
|
|
210
210
|
import { shutdownMnemopiEmbedClient } from "../mnemopi/embed-client";
|
|
211
211
|
import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state";
|
|
212
212
|
import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
|
|
213
|
-
import {
|
|
213
|
+
import { theme } from "../modes/theme/theme";
|
|
214
214
|
import { parseTurnBudget } from "../modes/turn-budget";
|
|
215
215
|
import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
|
|
216
216
|
import { computeNonMessageBreakdown, computeNonMessageTokens } from "../modes/utils/context-usage";
|
|
@@ -477,6 +477,8 @@ export interface AgentSessionConfig {
|
|
|
477
477
|
modelRegistry: ModelRegistry;
|
|
478
478
|
/** Tool registry for LSP and settings */
|
|
479
479
|
toolRegistry?: Map<string, AgentTool>;
|
|
480
|
+
/** Tool names whose current registry entry is still the built-in implementation. */
|
|
481
|
+
builtInToolNames?: Iterable<string>;
|
|
480
482
|
/** Current session pre-LLM message transform pipeline */
|
|
481
483
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
482
484
|
/** Provider payload hook used by the active session request path */
|
|
@@ -1290,6 +1292,7 @@ export class AgentSession {
|
|
|
1290
1292
|
// Generic tool discovery (covers built-in + MCP + extension when tools.discoveryMode === "all")
|
|
1291
1293
|
#discoverableToolSearchIndex: DiscoverableToolSearchIndex | null = null;
|
|
1292
1294
|
#selectedDiscoveredToolNames = new Set<string>();
|
|
1295
|
+
#builtInToolNames = new Set<string>();
|
|
1293
1296
|
#rpcHostToolNames = new Set<string>();
|
|
1294
1297
|
#defaultSelectedMCPServerNames = new Set<string>();
|
|
1295
1298
|
#defaultSelectedMCPToolNames = new Set<string>();
|
|
@@ -1566,6 +1569,7 @@ export class AgentSession {
|
|
|
1566
1569
|
this.#pruneToolDescriptions = config.pruneToolDescriptions === true;
|
|
1567
1570
|
this.#validateRetryFallbackChains();
|
|
1568
1571
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
1572
|
+
this.#builtInToolNames = new Set(config.builtInToolNames ?? []);
|
|
1569
1573
|
this.#requestedToolNames = config.requestedToolNames;
|
|
1570
1574
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
1571
1575
|
this.#onPayload = config.onPayload;
|
|
@@ -4496,6 +4500,11 @@ export class AgentSession {
|
|
|
4496
4500
|
return this.#toolRegistry.get(name);
|
|
4497
4501
|
}
|
|
4498
4502
|
|
|
4503
|
+
/** True when the current registry entry for `name` came from a built-in factory. */
|
|
4504
|
+
hasBuiltInTool(name: string): boolean {
|
|
4505
|
+
return this.#builtInToolNames.has(name);
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4499
4508
|
/**
|
|
4500
4509
|
* Get all configured tool names (built-in via --tools or default, plus custom tools).
|
|
4501
4510
|
*/
|
|
@@ -6835,7 +6844,7 @@ export class AgentSession {
|
|
|
6835
6844
|
this.#pendingNextTurnMessages = [];
|
|
6836
6845
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
6837
6846
|
|
|
6838
|
-
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
6847
|
+
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel, this.configuredThinkingLevel());
|
|
6839
6848
|
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
6840
6849
|
if (nextDiscoverySessionToolNames) {
|
|
6841
6850
|
await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, { persistMCPSelection: false });
|
|
@@ -7234,16 +7243,20 @@ export class AgentSession {
|
|
|
7234
7243
|
return;
|
|
7235
7244
|
}
|
|
7236
7245
|
|
|
7246
|
+
const wasAuto = this.#autoThinking;
|
|
7237
7247
|
this.#autoThinking = false;
|
|
7238
7248
|
this.#autoResolvedLevel = undefined;
|
|
7239
7249
|
const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
|
|
7240
|
-
|
|
7250
|
+
// Leaving auto must persist even when the resolved effort is unchanged (e.g.
|
|
7251
|
+
// auto resolved to medium, then the user pins medium): otherwise the latest
|
|
7252
|
+
// session entry keeps `configured: "auto"` and resume re-enables auto.
|
|
7253
|
+
const isChanging = wasAuto || effectiveLevel !== this.#thinkingLevel;
|
|
7241
7254
|
|
|
7242
7255
|
this.#thinkingLevel = effectiveLevel;
|
|
7243
7256
|
this.#applyThinkingLevelToAgent(effectiveLevel);
|
|
7244
7257
|
|
|
7245
7258
|
if (isChanging) {
|
|
7246
|
-
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
7259
|
+
this.sessionManager.appendThinkingLevelChange(effectiveLevel, effectiveLevel);
|
|
7247
7260
|
if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
|
|
7248
7261
|
this.settings.set("defaultThinkingLevel", effectiveLevel);
|
|
7249
7262
|
}
|
|
@@ -7332,7 +7345,7 @@ export class AgentSession {
|
|
|
7332
7345
|
this.#thinkingLevel = effort;
|
|
7333
7346
|
this.#applyThinkingLevelToAgent(effort);
|
|
7334
7347
|
if (shouldPersistResolution) {
|
|
7335
|
-
this.sessionManager.appendThinkingLevelChange(effort);
|
|
7348
|
+
this.sessionManager.appendThinkingLevelChange(effort, AUTO_THINKING);
|
|
7336
7349
|
}
|
|
7337
7350
|
this.#emit({
|
|
7338
7351
|
type: "thinking_level_changed",
|
|
@@ -11608,18 +11621,26 @@ export class AgentSession {
|
|
|
11608
11621
|
.some(entry => entry.type === "service_tier_change");
|
|
11609
11622
|
const defaultThinkingLevel = parseConfiguredThinkingLevel(this.settings.get("defaultThinkingLevel"));
|
|
11610
11623
|
const configuredServiceTier = this.settings.get("serviceTier");
|
|
11611
|
-
//
|
|
11612
|
-
//
|
|
11613
|
-
//
|
|
11614
|
-
//
|
|
11615
|
-
//
|
|
11616
|
-
//
|
|
11624
|
+
// Restore the thinking selector. Each change persists the configured
|
|
11625
|
+
// selector (`auto` or a concrete level), so prefer it: an `auto` session
|
|
11626
|
+
// resumes in auto mode (reclassifying the next turn) instead of freezing at
|
|
11627
|
+
// the last resolved level. Entries written before the `configured` field
|
|
11628
|
+
// existed fall back to the concrete level (legacy pin-on-resume behavior).
|
|
11629
|
+
// With no thinking entry, fall back to the global default so fresh sessions
|
|
11630
|
+
// still classify their first turn.
|
|
11631
|
+
const restoredConfigured = sessionContext.configuredThinkingLevel;
|
|
11617
11632
|
const restoredThinkingLevel: ConfiguredThinkingLevel | undefined =
|
|
11618
11633
|
hasThinkingEntry || (defaultThinkingLevel === AUTO_THINKING && sessionContext.thinkingLevel !== "off")
|
|
11619
|
-
?
|
|
11634
|
+
? restoredConfigured === AUTO_THINKING
|
|
11635
|
+
? AUTO_THINKING
|
|
11636
|
+
: (sessionContext.thinkingLevel as ThinkingLevel | undefined)
|
|
11620
11637
|
: defaultThinkingLevel;
|
|
11621
11638
|
if (restoredThinkingLevel === AUTO_THINKING) {
|
|
11622
11639
|
this.#autoThinking = true;
|
|
11640
|
+
// Resume in auto (pending) like a fresh auto session: the next user
|
|
11641
|
+
// turn reclassifies. We intentionally do not seed the last resolved
|
|
11642
|
+
// effort, so the cold (--continue) and in-app switch paths display
|
|
11643
|
+
// identically as `auto` until then.
|
|
11623
11644
|
this.#autoResolvedLevel = undefined;
|
|
11624
11645
|
this.#thinkingLevel = resolveProvisionalAutoLevel(this.model);
|
|
11625
11646
|
} else {
|
|
@@ -12513,9 +12534,12 @@ export class AgentSession {
|
|
|
12513
12534
|
* @returns Path to exported file
|
|
12514
12535
|
*/
|
|
12515
12536
|
async exportToHtml(outputPath?: string): Promise<string> {
|
|
12516
|
-
|
|
12537
|
+
// Public HTML export ships in the omp brand palette (collab-web
|
|
12538
|
+
// pink/purple), matching my.omp.sh — not the host's terminal theme.
|
|
12539
|
+
// Callers who want a themed export can pass `palette: "theme"` with
|
|
12540
|
+
// `themeName` directly to `exportSessionToHtml`.
|
|
12517
12541
|
const { exportSessionToHtml } = await import("../export/html");
|
|
12518
|
-
return exportSessionToHtml(this.sessionManager, this.state, { outputPath,
|
|
12542
|
+
return exportSessionToHtml(this.sessionManager, this.state, { outputPath, palette: "web" });
|
|
12519
12543
|
}
|
|
12520
12544
|
|
|
12521
12545
|
// =========================================================================
|