@oh-my-pi/pi-coding-agent 16.1.10 → 16.1.12
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 +45 -0
- package/dist/cli.js +2480 -2481
- package/dist/types/export/html/index.d.ts +31 -2
- package/dist/types/export/html/web-palette.d.ts +117 -0
- package/dist/types/export/share.d.ts +10 -5
- 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/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +43 -9
- 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/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/export/share.ts +198 -8
- 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/main.ts +8 -0
- 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 +14 -2
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +220 -71
- package/src/session/agent-session.ts +57 -43
- 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/jj.ts +47 -0
- package/src/utils/title-generator.ts +31 -99
package/src/main.ts
CHANGED
|
@@ -149,8 +149,16 @@ const RPC_BACKGROUND_DEFAULTED_SETTING_PATHS: SettingPath[] = [
|
|
|
149
149
|
"bash.autoBackground.thresholdMs",
|
|
150
150
|
];
|
|
151
151
|
|
|
152
|
+
// Protocol-mode hosts opt into a small set of paths whose host-default we
|
|
153
|
+
// re-apply at startup so embedders inherit OMP's neutral defaults instead of
|
|
154
|
+
// the local user's globally-persisted preferences for interactive use. The
|
|
155
|
+
// guard preserves any explicit configuration — caller `Settings.isolated`
|
|
156
|
+
// overrides, project `.claude/settings.yml`, `--config` overlays, or global
|
|
157
|
+
// `config.yml` — so the host default only kicks in when nothing is set. Without
|
|
158
|
+
// it the override clobbers every caller/host choice (#2598, #3207).
|
|
152
159
|
function applyDefaultSettingOverrides(settingPaths: SettingPath[], targetSettings: Settings): void {
|
|
153
160
|
for (const settingPath of settingPaths) {
|
|
161
|
+
if (targetSettings.isConfigured(settingPath)) continue;
|
|
154
162
|
targetSettings.override(settingPath, getDefault(settingPath));
|
|
155
163
|
}
|
|
156
164
|
}
|
|
@@ -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
|
@@ -97,6 +97,7 @@ import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
|
|
|
97
97
|
import {
|
|
98
98
|
collectEnvSecrets,
|
|
99
99
|
deobfuscateSessionContext,
|
|
100
|
+
deobfuscateToolArguments,
|
|
100
101
|
loadSecrets,
|
|
101
102
|
obfuscateMessages,
|
|
102
103
|
obfuscateProviderContext,
|
|
@@ -1288,7 +1289,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1288
1289
|
const pickInitialThinkingLevel = (selectedModel: Model | undefined): ConfiguredThinkingLevel | undefined => {
|
|
1289
1290
|
let level = options.thinkingLevel;
|
|
1290
1291
|
if (level === undefined && hasExistingSession && hasThinkingEntry) {
|
|
1291
|
-
level =
|
|
1292
|
+
level =
|
|
1293
|
+
parseConfiguredThinkingLevel(existingSession.configuredThinkingLevel) ??
|
|
1294
|
+
parseThinkingLevel(existingSession.thinkingLevel);
|
|
1292
1295
|
}
|
|
1293
1296
|
if (level === undefined && !hasThinkingEntry && restoredSessionThinkingLevel !== undefined) {
|
|
1294
1297
|
level = restoredSessionThinkingLevel;
|
|
@@ -2019,18 +2022,22 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2019
2022
|
);
|
|
2020
2023
|
|
|
2021
2024
|
// All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
|
|
2025
|
+
const builtInRegistryToolNames = new Set<string>();
|
|
2022
2026
|
const toolRegistry = new Map<string, Tool>();
|
|
2023
2027
|
for (const tool of builtinTools) {
|
|
2024
2028
|
toolRegistry.set(tool.name, tool);
|
|
2029
|
+
builtInRegistryToolNames.add(tool.name);
|
|
2025
2030
|
}
|
|
2026
2031
|
if (!toolRegistry.has("goal") && settings.get("goal.enabled")) {
|
|
2027
2032
|
const goalTool = await logger.time("createTools:goal:session", HIDDEN_TOOLS.goal, toolSession);
|
|
2028
2033
|
if (goalTool) {
|
|
2029
2034
|
toolRegistry.set(goalTool.name, wrapToolWithMetaNotice(goalTool));
|
|
2035
|
+
builtInRegistryToolNames.add(goalTool.name);
|
|
2030
2036
|
}
|
|
2031
2037
|
}
|
|
2032
2038
|
for (const tool of wrappedExtensionTools) {
|
|
2033
2039
|
toolRegistry.set(tool.name, tool);
|
|
2040
|
+
builtInRegistryToolNames.delete(tool.name);
|
|
2034
2041
|
}
|
|
2035
2042
|
if (deferMCPDiscoveryForUI && mcpManager) {
|
|
2036
2043
|
for (const name of collectPendingMCPToolNames(options.toolNames, existingSession.selectedMCPToolNames)) {
|
|
@@ -2048,6 +2055,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2048
2055
|
}
|
|
2049
2056
|
if (model?.provider === "cursor") {
|
|
2050
2057
|
toolRegistry.delete("edit");
|
|
2058
|
+
builtInRegistryToolNames.delete("edit");
|
|
2051
2059
|
}
|
|
2052
2060
|
|
|
2053
2061
|
// `resolve` is hidden but must stay in the registry whenever any code path can invoke it:
|
|
@@ -2060,10 +2068,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2060
2068
|
const needsResolveTool = hasDeferrableTools || planModeAvailable;
|
|
2061
2069
|
if (!needsResolveTool) {
|
|
2062
2070
|
toolRegistry.delete("resolve");
|
|
2071
|
+
builtInRegistryToolNames.delete("resolve");
|
|
2063
2072
|
} else if (!toolRegistry.has("resolve")) {
|
|
2064
2073
|
const resolveTool = await logger.time("createTools:resolve:session", HIDDEN_TOOLS.resolve, toolSession);
|
|
2065
2074
|
if (resolveTool) {
|
|
2066
2075
|
toolRegistry.set(resolveTool.name, wrapToolWithMetaNotice(resolveTool));
|
|
2076
|
+
builtInRegistryToolNames.add(resolveTool.name);
|
|
2067
2077
|
}
|
|
2068
2078
|
}
|
|
2069
2079
|
|
|
@@ -2080,6 +2090,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2080
2090
|
searchTool.name,
|
|
2081
2091
|
new ExtensionToolWrapper(wrapToolWithMetaNotice(searchTool), extensionRunner) as Tool,
|
|
2082
2092
|
);
|
|
2093
|
+
builtInRegistryToolNames.add(searchTool.name);
|
|
2083
2094
|
}
|
|
2084
2095
|
let mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off"; // back-compat: true when any discovery active
|
|
2085
2096
|
|
|
@@ -2525,7 +2536,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2525
2536
|
result = { ...result, timeout: Math.min(result.timeout, maxTimeout) };
|
|
2526
2537
|
}
|
|
2527
2538
|
if (obfuscator?.hasSecrets()) {
|
|
2528
|
-
result = obfuscator
|
|
2539
|
+
result = deobfuscateToolArguments(obfuscator, result);
|
|
2529
2540
|
}
|
|
2530
2541
|
return result;
|
|
2531
2542
|
},
|
|
@@ -2623,6 +2634,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2623
2634
|
skillsSettings: settings.getGroup("skills"),
|
|
2624
2635
|
modelRegistry,
|
|
2625
2636
|
toolRegistry,
|
|
2637
|
+
builtInToolNames: builtInRegistryToolNames,
|
|
2626
2638
|
transformContext,
|
|
2627
2639
|
onPayload,
|
|
2628
2640
|
onResponse,
|
package/src/secrets/index.ts
CHANGED
|
@@ -6,9 +6,9 @@ import { compileSecretRegex } from "./regex";
|
|
|
6
6
|
|
|
7
7
|
export {
|
|
8
8
|
deobfuscateSessionContext,
|
|
9
|
+
deobfuscateToolArguments,
|
|
9
10
|
obfuscateMessages,
|
|
10
11
|
obfuscateProviderContext,
|
|
11
|
-
obfuscateProviderTools,
|
|
12
12
|
type SecretEntry,
|
|
13
13
|
SecretObfuscator,
|
|
14
14
|
} from "./obfuscator";
|