@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/cli.js +2480 -2481
  3. package/dist/types/export/html/index.d.ts +31 -2
  4. package/dist/types/export/html/web-palette.d.ts +117 -0
  5. package/dist/types/export/share.d.ts +10 -5
  6. package/dist/types/hindsight/content.d.ts +7 -0
  7. package/dist/types/hindsight/transcript.d.ts +1 -1
  8. package/dist/types/modes/components/hook-editor.d.ts +2 -1
  9. package/dist/types/modes/interactive-mode.d.ts +5 -0
  10. package/dist/types/modes/types.d.ts +17 -0
  11. package/dist/types/modes/utils/keybinding-matchers.d.ts +13 -0
  12. package/dist/types/secrets/index.d.ts +1 -1
  13. package/dist/types/secrets/obfuscator.d.ts +43 -9
  14. package/dist/types/session/agent-session.d.ts +4 -0
  15. package/dist/types/session/session-context.d.ts +2 -0
  16. package/dist/types/session/session-entries.d.ts +6 -0
  17. package/dist/types/session/session-manager.d.ts +2 -1
  18. package/dist/types/tools/acp-bridge.d.ts +29 -0
  19. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +7 -0
  20. package/dist/types/tools/browser/tab-worker.d.ts +20 -0
  21. package/dist/types/utils/jj.d.ts +25 -0
  22. package/dist/types/utils/title-generator.d.ts +0 -2
  23. package/package.json +12 -12
  24. package/scripts/generate-share-viewer.ts +4 -2
  25. package/src/autoresearch/git.ts +12 -0
  26. package/src/discovery/opencode.ts +47 -4
  27. package/src/edit/hashline/filesystem.ts +8 -0
  28. package/src/edit/modes/patch.ts +18 -2
  29. package/src/edit/modes/replace.ts +13 -10
  30. package/src/export/html/index.ts +50 -8
  31. package/src/export/html/web-palette.ts +142 -0
  32. package/src/export/share.ts +198 -8
  33. package/src/hindsight/backend.ts +4 -4
  34. package/src/hindsight/content.ts +17 -1
  35. package/src/hindsight/transcript.ts +2 -2
  36. package/src/internal-urls/docs-index.generated.txt +1 -1
  37. package/src/main.ts +8 -0
  38. package/src/modes/components/agent-dashboard.ts +8 -8
  39. package/src/modes/components/hook-editor.ts +13 -10
  40. package/src/modes/components/session-selector.ts +3 -0
  41. package/src/modes/controllers/event-controller.ts +9 -5
  42. package/src/modes/controllers/extension-ui-controller.ts +6 -2
  43. package/src/modes/interactive-mode.ts +69 -29
  44. package/src/modes/theme/dark.json +1 -1
  45. package/src/modes/types.ts +18 -0
  46. package/src/modes/utils/keybinding-matchers.ts +36 -1
  47. package/src/modes/utils/ui-helpers.ts +1 -0
  48. package/src/prompts/tools/browser.md +3 -2
  49. package/src/sdk.ts +14 -2
  50. package/src/secrets/index.ts +1 -1
  51. package/src/secrets/obfuscator.ts +220 -71
  52. package/src/session/agent-session.ts +57 -43
  53. package/src/session/session-context.ts +5 -0
  54. package/src/session/session-entries.ts +6 -0
  55. package/src/session/session-manager.ts +3 -1
  56. package/src/task/worktree.ts +12 -4
  57. package/src/thinking.ts +1 -1
  58. package/src/tools/acp-bridge.ts +66 -0
  59. package/src/tools/browser/cmux/cmux-tab.ts +37 -0
  60. package/src/tools/browser/tab-worker.ts +160 -37
  61. package/src/tools/write.ts +2 -25
  62. package/src/utils/jj.ts +47 -0
  63. 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 { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
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 && this.#shouldSubmitCreateDescription(data)) {
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, Ctrl+Enter submits, bordered popup
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 { matchesAppExternalEditor, matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
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=submit (original behavior) */
122
+ /** Hook-style: Enter=newline, app.message.followUp chord (Ctrl+Q/Ctrl+Enter) submits. */
122
123
  #handleHookStyleInput(keyData: string): void {
123
- // Ctrl+Enter to submit. Use key matching so lock-key and keypad Enter variants work.
124
- if (isCtrlEnterSubmit(keyData)) {
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 wasLocallySubmitted = this.ctx.locallySubmittedUserSignatures.delete(signature) || wasOptimistic;
300
- if (!wasOptimistic) {
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
- if (!wasStreaming && shouldDisplay) {
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.addMessageToChat(
1276
- {
1277
- role: "user",
1278
- content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
1279
- attribution: "user",
1280
- timestamp: Date.now(),
1281
- },
1282
- { imageLinks: input.imageLinks },
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.optimisticUserMessageSignature = undefined;
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.optimisticUserMessageSignature = undefined;
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.addMessageToChat(
1438
- {
1439
- role: "user",
1440
- content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
1441
- attribution: "user",
1442
- timestamp: Date.now(),
1443
- },
1444
- { imageLinks: submission.imageLinks },
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
- const hasResolveTool = this.session.getToolByName("resolve") !== undefined;
1930
- const planTools = hasResolveTool ? [...previousTools, "resolve"] : previousTools;
1931
- const uniquePlanTools = [...new Set(planTools)];
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.optimisticUserMessageSignature = undefined;
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);
@@ -90,6 +90,6 @@
90
90
  "export": {
91
91
  "pageBg": "#18181e",
92
92
  "cardBg": "#1e1e24",
93
- "infoBg": "#3c3728"
93
+ "infoBg": "#26262e"
94
94
  }
95
95
  }
@@ -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 = parseThinkingLevel(existingSession.thinkingLevel);
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.deobfuscateObject(result);
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,
@@ -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";