@oh-my-pi/pi-coding-agent 16.1.9 → 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 +46 -0
- package/dist/cli.js +3455 -3430
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/edit/streaming.d.ts +5 -5
- 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/controllers/selector-controller.d.ts +0 -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/sdk.d.ts +1 -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/system-prompt.d.ts +2 -0
- package/dist/types/tools/acp-bridge.d.ts +29 -0
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +39 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +10 -0
- package/dist/types/tools/browser/tab-worker.d.ts +20 -0
- package/dist/types/tools/browser.d.ts +1 -0
- package/dist/types/tools/find.d.ts +1 -2
- package/dist/types/tools/write.d.ts +1 -2
- 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-aria-snapshot.ts +134 -0
- package/scripts/generate-share-viewer.ts +4 -2
- package/src/autoresearch/git.ts +12 -0
- package/src/config/settings-schema.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/edit/streaming.ts +5 -5
- 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 +29 -5
- package/src/modes/controllers/extension-ui-controller.ts +6 -2
- package/src/modes/controllers/selector-controller.ts +3 -5
- 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/system/project-prompt.md +2 -0
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/browser.md +5 -2
- package/src/sdk.ts +20 -2
- 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/system-prompt.ts +15 -3
- package/src/task/worktree.ts +12 -4
- package/src/thinking.ts +1 -1
- package/src/tools/acp-bridge.ts +66 -0
- package/src/tools/bash.ts +3 -3
- package/src/tools/browser/aria/aria-snapshot.bundle.txt +7 -0
- package/src/tools/browser/aria/aria-snapshot.ts +103 -0
- package/src/tools/browser/cmux/cmux-tab.ts +73 -1
- package/src/tools/browser/tab-worker.ts +242 -47
- package/src/tools/browser.ts +5 -0
- package/src/tools/find.ts +1 -2
- package/src/tools/index.ts +1 -1
- package/src/tools/write.ts +3 -27
- package/src/utils/git.ts +3 -2
- 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
|
|
@@ -738,6 +742,26 @@ export class EventController {
|
|
|
738
742
|
this.ctx.chatContainer.addChild(component);
|
|
739
743
|
this.ctx.pendingTools.set(event.toolCallId, component);
|
|
740
744
|
this.ctx.ui.requestRender();
|
|
745
|
+
} else {
|
|
746
|
+
// The tool is about to run, so its arguments are final and validated.
|
|
747
|
+
// A pending component created while args streamed (message_update) may
|
|
748
|
+
// still show a mid-reveal prefix — or, when the closing full-args
|
|
749
|
+
// `message_update` never lands (smooth-streaming off leaving the
|
|
750
|
+
// throttled `arguments` stale, an owned-dialect projector, or a
|
|
751
|
+
// superseded/aborted turn that still executes the call), a stale body
|
|
752
|
+
// the result render then freezes at its `…` placeholder. Reconcile the
|
|
753
|
+
// authoritative args here and drop any live reveal so a late tick can't
|
|
754
|
+
// re-truncate them: tool_execution_start is the one event every
|
|
755
|
+
// execution path emits with the full args immediately before the result.
|
|
756
|
+
this.#toolArgsReveal.finish(event.toolCallId);
|
|
757
|
+
const component = this.ctx.pendingTools.get(event.toolCallId);
|
|
758
|
+
if (component && typeof component.updateArgs === "function") {
|
|
759
|
+
component.updateArgs(event.args, event.toolCallId);
|
|
760
|
+
if (typeof component.setArgsComplete === "function") {
|
|
761
|
+
component.setArgsComplete(event.toolCallId);
|
|
762
|
+
}
|
|
763
|
+
this.ctx.ui.requestRender();
|
|
764
|
+
}
|
|
741
765
|
}
|
|
742
766
|
}
|
|
743
767
|
|
|
@@ -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
|
}
|
|
@@ -71,10 +71,6 @@ import { buildCopyTargets } from "../utils/copy-targets";
|
|
|
71
71
|
|
|
72
72
|
const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
|
|
73
73
|
|
|
74
|
-
export function formatTemporaryModelStatus(modelLabel: string, roleSelectorHint = "Alt+M"): string {
|
|
75
|
-
return `Temporary model selection is session-only: ${modelLabel}. Use ${roleSelectorHint} or /model for role models (default/smol/plan/task/slow/custom roles).`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
74
|
export class SelectorController {
|
|
79
75
|
constructor(private ctx: InteractiveModeContext) {}
|
|
80
76
|
|
|
@@ -499,7 +495,9 @@ export class SelectorController {
|
|
|
499
495
|
this.ctx.statusLine.invalidate();
|
|
500
496
|
this.ctx.updateEditorBorderColor();
|
|
501
497
|
const roleSelectorHint = this.ctx.keybindings.getKeys("app.model.select")[0] ?? "Alt+M";
|
|
502
|
-
this.ctx.showStatus(
|
|
498
|
+
this.ctx.showStatus(
|
|
499
|
+
`Session-only model: ${selector ?? model.id}. Use ${roleSelectorHint} or /model for roles.`,
|
|
500
|
+
);
|
|
503
501
|
done();
|
|
504
502
|
this.ctx.ui.requestRender();
|
|
505
503
|
} else if (role === "default") {
|
|
@@ -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 {
|
|
@@ -29,6 +29,7 @@ Before making changes within these directories, you MUST read:
|
|
|
29
29
|
The context files above are loaded automatically. You NEVER `search`/`find` for `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, or similar agent/context files — the relevant ones are already in your context; any others are noise.
|
|
30
30
|
{{/ifAny}}
|
|
31
31
|
|
|
32
|
+
{{#if includeWorkspaceTree}}
|
|
32
33
|
{{#if workspaceTree.rendered}}
|
|
33
34
|
<workspace-tree>
|
|
34
35
|
Working directory layout (sorted by mtime, recent first; depth ≤ 3):
|
|
@@ -38,6 +39,7 @@ Working directory layout (sorted by mtime, recent first; depth ≤ 3):
|
|
|
38
39
|
{{/if}}
|
|
39
40
|
</workspace-tree>
|
|
40
41
|
{{/if}}
|
|
42
|
+
{{/if}}
|
|
41
43
|
|
|
42
44
|
Today is {{date}}, and the current working directory is '{{cwd}}'.
|
|
43
45
|
|
|
@@ -17,7 +17,6 @@ You are a helpful assistant the team trusts with load-bearing changes, operating
|
|
|
17
17
|
- You are not alone in this repo. Treat unexpected changes as the user's work and adapt.
|
|
18
18
|
- In terminal prose and final chat, you MAY use LaTeX math (`$`, `$$`, `\text`, `\times`) and color (`\textcolor`, `\colorbox`, `\fcolorbox`).
|
|
19
19
|
- To show a diagram, you MAY emit a ` ```mermaid ` block — the terminal renders it as ASCII. Use it for genuine structure or flow, not trivia.
|
|
20
|
-
- For a visual separator between sections, use `─` (U+2500).
|
|
21
20
|
|
|
22
21
|
RUNTIME
|
|
23
22
|
==============
|
|
@@ -15,19 +15,22 @@ Drives real Chromium tab; full puppeteer access via JS.
|
|
|
15
15
|
- `tab` helpers; drop to raw puppeteer `page` for anything uncovered:
|
|
16
16
|
- `tab.goto(url, { waitUntil? })` — navigate.
|
|
17
17
|
- `tab.observe({ includeAll?, viewportOnly? })` — accessibility snapshot: `{ url, title, viewport, scroll, elements: [{ id, role, name, value, states, … }] }`. Ids stable until next observe/goto.
|
|
18
|
+
- `tab.ariaSnapshot(selector?, { depth?, boxes? })` — Playwright-format ARIA-tree YAML (nested roles + accessible names + `/url`/`/placeholder`), scoped to `selector` or the whole document. Every node carries a `[ref=eN]` id; `[cursor=pointer]` flags clickables. Captures dense, hierarchical structure/text that `observe()`'s flat list flattens away. Refs renumber from e1 each call and stay valid until the next `ariaSnapshot()`.
|
|
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")`).
|
|
18
20
|
- `tab.id(n)` — id from last observe → `ElementHandle` (`.click()`, `.type()`, …).
|
|
19
21
|
- `tab.click(selector)` / `tab.type(selector, text)` / `tab.fill(selector, value)` / `tab.press(key, { selector? })` / `tab.scroll(dx, dy)`.
|
|
20
|
-
- `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`.
|
|
21
23
|
- `tab.drag(from, to)` — endpoints: selector (center-to-center) or `{ x, y }` viewport point (canvases, sliders).
|
|
22
24
|
- `tab.scrollIntoView(selector)` — center in viewport; before clicking off-screen elements.
|
|
23
25
|
- `tab.select(selector, …values)` — set `<select>` option(s); returns selection. `tab.fill` NEVER works for selects.
|
|
24
26
|
- `tab.uploadFile(selector, …filePaths)` — attach files to `<input type="file">`; paths relative to cwd.
|
|
25
27
|
- `tab.waitForUrl(pattern, { timeout? })` — substring or `RegExp` (matches SPA pushState nav); returns matched URL.
|
|
26
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.
|
|
27
30
|
- `tab.evaluate(fn, …args)` — `page.evaluate` for ad-hoc DOM reads.
|
|
28
31
|
- `tab.screenshot({ selector?, fullPage?, save?, silent? })` — capture + attach for viewing (`silent: true` skips). Pass `save` only when a later step needs the file.
|
|
29
32
|
- `tab.extract(format = "markdown")` — readable page content (`"markdown"` | `"text"`); throws when nothing readable.
|
|
30
|
-
- 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.
|
|
31
34
|
</instruction>
|
|
32
35
|
|
|
33
36
|
<critical>
|
package/src/sdk.ts
CHANGED
|
@@ -796,6 +796,7 @@ export interface BuildSystemPromptOptions {
|
|
|
796
796
|
customPrompt?: string;
|
|
797
797
|
appendPrompt?: string;
|
|
798
798
|
inlineToolDescriptors?: boolean;
|
|
799
|
+
includeWorkspaceTree?: boolean;
|
|
799
800
|
}
|
|
800
801
|
|
|
801
802
|
/**
|
|
@@ -813,6 +814,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
813
814
|
contextFiles: options.contextFiles,
|
|
814
815
|
appendSystemPrompt: options.appendPrompt,
|
|
815
816
|
inlineToolDescriptors: options.inlineToolDescriptors,
|
|
817
|
+
includeWorkspaceTree: options.includeWorkspaceTree,
|
|
816
818
|
toolNames: options.tools?.map(tool => tool.name),
|
|
817
819
|
tools: toolMap ? buildSystemPromptToolMetadata(toolMap) : undefined,
|
|
818
820
|
});
|
|
@@ -1120,9 +1122,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1120
1122
|
// startup does not perform a second recursive filesystem search. Subagents
|
|
1121
1123
|
// inherit the parent's resolved values via options.
|
|
1122
1124
|
const STARTUP_SCAN_DEADLINE_MS = 5000;
|
|
1125
|
+
const includeWorkspaceTree = settings.get("includeWorkspaceTree") ?? false;
|
|
1123
1126
|
const workspaceTreePromise: Promise<WorkspaceTree> = options.workspaceTree
|
|
1124
1127
|
? Promise.resolve(options.workspaceTree)
|
|
1125
|
-
:
|
|
1128
|
+
: includeWorkspaceTree
|
|
1129
|
+
? logger.time("buildWorkspaceTree", () => buildWorkspaceTree(cwd, { timeoutMs: STARTUP_SCAN_DEADLINE_MS }))
|
|
1130
|
+
: Promise.resolve({ rootPath: cwd, rendered: "", truncated: false, totalLines: 0, agentsMdFiles: [] });
|
|
1126
1131
|
workspaceTreePromise.catch(() => {});
|
|
1127
1132
|
|
|
1128
1133
|
// Independent discoveries that depend only on cwd/agentDir — kicked off in parallel and awaited
|
|
@@ -1283,7 +1288,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1283
1288
|
const pickInitialThinkingLevel = (selectedModel: Model | undefined): ConfiguredThinkingLevel | undefined => {
|
|
1284
1289
|
let level = options.thinkingLevel;
|
|
1285
1290
|
if (level === undefined && hasExistingSession && hasThinkingEntry) {
|
|
1286
|
-
level =
|
|
1291
|
+
level =
|
|
1292
|
+
parseConfiguredThinkingLevel(existingSession.configuredThinkingLevel) ??
|
|
1293
|
+
parseThinkingLevel(existingSession.thinkingLevel);
|
|
1287
1294
|
}
|
|
1288
1295
|
if (level === undefined && !hasThinkingEntry && restoredSessionThinkingLevel !== undefined) {
|
|
1289
1296
|
level = restoredSessionThinkingLevel;
|
|
@@ -2014,18 +2021,22 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2014
2021
|
);
|
|
2015
2022
|
|
|
2016
2023
|
// All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
|
|
2024
|
+
const builtInRegistryToolNames = new Set<string>();
|
|
2017
2025
|
const toolRegistry = new Map<string, Tool>();
|
|
2018
2026
|
for (const tool of builtinTools) {
|
|
2019
2027
|
toolRegistry.set(tool.name, tool);
|
|
2028
|
+
builtInRegistryToolNames.add(tool.name);
|
|
2020
2029
|
}
|
|
2021
2030
|
if (!toolRegistry.has("goal") && settings.get("goal.enabled")) {
|
|
2022
2031
|
const goalTool = await logger.time("createTools:goal:session", HIDDEN_TOOLS.goal, toolSession);
|
|
2023
2032
|
if (goalTool) {
|
|
2024
2033
|
toolRegistry.set(goalTool.name, wrapToolWithMetaNotice(goalTool));
|
|
2034
|
+
builtInRegistryToolNames.add(goalTool.name);
|
|
2025
2035
|
}
|
|
2026
2036
|
}
|
|
2027
2037
|
for (const tool of wrappedExtensionTools) {
|
|
2028
2038
|
toolRegistry.set(tool.name, tool);
|
|
2039
|
+
builtInRegistryToolNames.delete(tool.name);
|
|
2029
2040
|
}
|
|
2030
2041
|
if (deferMCPDiscoveryForUI && mcpManager) {
|
|
2031
2042
|
for (const name of collectPendingMCPToolNames(options.toolNames, existingSession.selectedMCPToolNames)) {
|
|
@@ -2043,6 +2054,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2043
2054
|
}
|
|
2044
2055
|
if (model?.provider === "cursor") {
|
|
2045
2056
|
toolRegistry.delete("edit");
|
|
2057
|
+
builtInRegistryToolNames.delete("edit");
|
|
2046
2058
|
}
|
|
2047
2059
|
|
|
2048
2060
|
// `resolve` is hidden but must stay in the registry whenever any code path can invoke it:
|
|
@@ -2055,10 +2067,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2055
2067
|
const needsResolveTool = hasDeferrableTools || planModeAvailable;
|
|
2056
2068
|
if (!needsResolveTool) {
|
|
2057
2069
|
toolRegistry.delete("resolve");
|
|
2070
|
+
builtInRegistryToolNames.delete("resolve");
|
|
2058
2071
|
} else if (!toolRegistry.has("resolve")) {
|
|
2059
2072
|
const resolveTool = await logger.time("createTools:resolve:session", HIDDEN_TOOLS.resolve, toolSession);
|
|
2060
2073
|
if (resolveTool) {
|
|
2061
2074
|
toolRegistry.set(resolveTool.name, wrapToolWithMetaNotice(resolveTool));
|
|
2075
|
+
builtInRegistryToolNames.add(resolveTool.name);
|
|
2062
2076
|
}
|
|
2063
2077
|
}
|
|
2064
2078
|
|
|
@@ -2075,6 +2089,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2075
2089
|
searchTool.name,
|
|
2076
2090
|
new ExtensionToolWrapper(wrapToolWithMetaNotice(searchTool), extensionRunner) as Tool,
|
|
2077
2091
|
);
|
|
2092
|
+
builtInRegistryToolNames.add(searchTool.name);
|
|
2078
2093
|
}
|
|
2079
2094
|
let mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off"; // back-compat: true when any discovery active
|
|
2080
2095
|
|
|
@@ -2101,6 +2116,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2101
2116
|
const eagerTasks = settings.get("task.eager") !== "default";
|
|
2102
2117
|
const eagerTasksAlways = settings.get("task.eager") === "always";
|
|
2103
2118
|
const intentField = $flag("PI_INTENT_TRACING", settings.get("tools.intentTracing")) ? INTENT_FIELD : undefined;
|
|
2119
|
+
const includeWorkspaceTree = settings.get("includeWorkspaceTree") ?? false;
|
|
2104
2120
|
const rebuildSystemPrompt = async (
|
|
2105
2121
|
toolNames: string[],
|
|
2106
2122
|
tools: Map<string, AgentTool>,
|
|
@@ -2194,6 +2210,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2194
2210
|
taskBatch: settings.get("task.batch"),
|
|
2195
2211
|
secretsEnabled,
|
|
2196
2212
|
workspaceTree: workspaceTreePromise,
|
|
2213
|
+
includeWorkspaceTree,
|
|
2197
2214
|
memoryRootEnabled: memoryBackend.id === "local",
|
|
2198
2215
|
model: settings.get("includeModelInPrompt") ? getActiveModelString() : undefined,
|
|
2199
2216
|
personality: agentKind === "sub" ? "none" : settings.get("personality"),
|
|
@@ -2616,6 +2633,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2616
2633
|
skillsSettings: settings.getGroup("skills"),
|
|
2617
2634
|
modelRegistry,
|
|
2618
2635
|
toolRegistry,
|
|
2636
|
+
builtInToolNames: builtInRegistryToolNames,
|
|
2619
2637
|
transformContext,
|
|
2620
2638
|
onPayload,
|
|
2621
2639
|
onResponse,
|