@oh-my-pi/pi-coding-agent 14.8.0 → 14.9.0
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 +38 -0
- package/package.json +16 -7
- package/src/config/model-resolver.ts +92 -35
- package/src/config/prompt-templates.ts +1 -1
- package/src/debug/index.ts +21 -0
- package/src/debug/raw-sse-buffer.ts +229 -0
- package/src/debug/raw-sse.ts +213 -0
- package/src/edit/index.ts +9 -10
- package/src/edit/streaming.ts +6 -5
- package/src/eval/js/context-manager.ts +91 -47
- package/src/extensibility/extensions/loader.ts +9 -3
- package/src/extensibility/extensions/types.ts +10 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +99 -20
- package/src/hashline/anchors.ts +113 -0
- package/src/hashline/apply.ts +732 -0
- package/src/hashline/bigrams.json +649 -0
- package/src/hashline/constants.ts +8 -0
- package/src/hashline/diff-preview.ts +43 -0
- package/src/hashline/diff.ts +56 -0
- package/src/hashline/execute.ts +268 -0
- package/src/{edit/modes/hashline.lark → hashline/grammar.lark} +1 -1
- package/src/{edit/line-hash.ts → hashline/hash.ts} +5 -651
- package/src/hashline/index.ts +14 -0
- package/src/hashline/input.ts +110 -0
- package/src/hashline/parser.ts +220 -0
- package/src/hashline/prefixes.ts +101 -0
- package/src/hashline/recovery.ts +72 -0
- package/src/hashline/stream.ts +123 -0
- package/src/hashline/types.ts +69 -0
- package/src/hashline/utils.ts +3 -0
- package/src/index.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/lsp/render.ts +4 -0
- package/src/memories/index.ts +13 -4
- package/src/modes/components/assistant-message.ts +55 -9
- package/src/modes/components/welcome.ts +114 -38
- package/src/modes/controllers/event-controller.ts +3 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +8 -1
- package/src/modes/interactive-mode.ts +50 -11
- package/src/modes/prompt-action-autocomplete.ts +3 -0
- package/src/modes/rpc/rpc-client.ts +53 -2
- package/src/modes/rpc/rpc-mode.ts +67 -1
- package/src/modes/rpc/rpc-types.ts +17 -2
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/prompts/agents/reviewer.md +14 -0
- package/src/prompts/tools/hashline.md +57 -10
- package/src/sdk.ts +4 -3
- package/src/session/agent-session.ts +195 -30
- package/src/session/compaction/branch-summarization.ts +4 -2
- package/src/session/compaction/compaction.ts +22 -3
- package/src/task/executor.ts +21 -2
- package/src/task/index.ts +4 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/match-line-format.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/title-generator.ts +11 -0
- package/src/edit/modes/hashline.ts +0 -2039
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - Events: AgentSessionEvent objects streamed as they occur
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
|
+
import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
13
14
|
import { $env, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
|
|
14
15
|
import type {
|
|
15
16
|
ExtensionUIContext,
|
|
@@ -149,7 +150,6 @@ export function requestRpcEditor(
|
|
|
149
150
|
} as RpcExtensionUIRequest);
|
|
150
151
|
return promise;
|
|
151
152
|
}
|
|
152
|
-
|
|
153
153
|
/**
|
|
154
154
|
* Run in RPC mode.
|
|
155
155
|
* Listens for JSON commands on stdin, outputs events and responses on stdout.
|
|
@@ -755,6 +755,72 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
755
755
|
return success(id, "get_messages", { messages: session.messages });
|
|
756
756
|
}
|
|
757
757
|
|
|
758
|
+
// =================================================================
|
|
759
|
+
// Login
|
|
760
|
+
// =================================================================
|
|
761
|
+
|
|
762
|
+
case "get_login_providers": {
|
|
763
|
+
const providers = getOAuthProviders().map(provider => ({
|
|
764
|
+
id: provider.id,
|
|
765
|
+
name: provider.name,
|
|
766
|
+
available: provider.available,
|
|
767
|
+
authenticated: session.modelRegistry.authStorage.hasAuth(provider.id),
|
|
768
|
+
}));
|
|
769
|
+
return success(id, "get_login_providers", { providers });
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
case "login": {
|
|
773
|
+
const knownProvider = getOAuthProviders().find(p => p.id === command.providerId);
|
|
774
|
+
if (!knownProvider) {
|
|
775
|
+
return error(id, "login", `Unknown OAuth provider: ${command.providerId}`);
|
|
776
|
+
}
|
|
777
|
+
const uiCtx = new RpcExtensionUIContext(pendingExtensionRequests, output);
|
|
778
|
+
// Track whether onAuth has fired. Providers that use OAuthCallbackFlow
|
|
779
|
+
// always call onAuth first (emit browser URL), then onManualCodeInput as
|
|
780
|
+
// a fallback. Providers that require interactive input (API-key paste,
|
|
781
|
+
// GitHub Enterprise URL, device-code entry) call onPrompt before onAuth.
|
|
782
|
+
// We use this ordering to self-classify at runtime — no static allowlist.
|
|
783
|
+
let authEmitted = false;
|
|
784
|
+
try {
|
|
785
|
+
await session.modelRegistry.authStorage.login(command.providerId, {
|
|
786
|
+
onAuth: info => {
|
|
787
|
+
authEmitted = true;
|
|
788
|
+
output({
|
|
789
|
+
type: "extension_ui_request",
|
|
790
|
+
id: Snowflake.next() as string,
|
|
791
|
+
method: "open_url",
|
|
792
|
+
url: info.url,
|
|
793
|
+
instructions: info.instructions,
|
|
794
|
+
} as RpcExtensionUIRequest);
|
|
795
|
+
},
|
|
796
|
+
onProgress: message => {
|
|
797
|
+
uiCtx.notify(message, "info");
|
|
798
|
+
},
|
|
799
|
+
onPrompt: () => {
|
|
800
|
+
if (!authEmitted) {
|
|
801
|
+
// onPrompt called before any auth URL — provider requires
|
|
802
|
+
// interactive input that cannot be satisfied headlessly.
|
|
803
|
+
return Promise.reject(
|
|
804
|
+
new Error(
|
|
805
|
+
`Provider '${command.providerId}' requires interactive prompts ` +
|
|
806
|
+
"which are not supported in RPC mode. Use the terminal UI to log in.",
|
|
807
|
+
),
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
// onAuth has already fired — we are inside OAuthCallbackFlow's
|
|
811
|
+
// manual-redirect fallback race. Returning a never-settling promise
|
|
812
|
+
// lets the race block until the callback server wins; a rejection
|
|
813
|
+
// would be caught as null and spin the while(true) loop.
|
|
814
|
+
return new Promise<string>(() => {});
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
await session.modelRegistry.refresh();
|
|
818
|
+
return success(id, "login", { providerId: command.providerId });
|
|
819
|
+
} catch (err: unknown) {
|
|
820
|
+
return error(id, "login", err instanceof Error ? err.message : String(err));
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
758
824
|
default: {
|
|
759
825
|
const unknownCommand = command as { type: string };
|
|
760
826
|
return error(undefined, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);
|
|
@@ -67,7 +67,11 @@ export type RpcCommand =
|
|
|
67
67
|
| { id?: string; type: "handoff"; customInstructions?: string }
|
|
68
68
|
|
|
69
69
|
// Messages
|
|
70
|
-
| { id?: string; type: "get_messages" }
|
|
70
|
+
| { id?: string; type: "get_messages" }
|
|
71
|
+
|
|
72
|
+
// Login
|
|
73
|
+
| { id?: string; type: "get_login_providers" }
|
|
74
|
+
| { id?: string; type: "login"; providerId: string };
|
|
71
75
|
|
|
72
76
|
// ============================================================================
|
|
73
77
|
// RPC State
|
|
@@ -193,6 +197,16 @@ export type RpcResponse =
|
|
|
193
197
|
// Messages
|
|
194
198
|
| { id?: string; type: "response"; command: "get_messages"; success: true; data: { messages: AgentMessage[] } }
|
|
195
199
|
|
|
200
|
+
// Login
|
|
201
|
+
| {
|
|
202
|
+
id?: string;
|
|
203
|
+
type: "response";
|
|
204
|
+
command: "get_login_providers";
|
|
205
|
+
success: true;
|
|
206
|
+
data: { providers: Array<{ id: string; name: string; available: boolean; authenticated: boolean }> };
|
|
207
|
+
}
|
|
208
|
+
| { id?: string; type: "response"; command: "login"; success: true; data: { providerId: string } }
|
|
209
|
+
|
|
196
210
|
// Error response (any command can fail)
|
|
197
211
|
| { id?: string; type: "response"; command: string; success: false; error: string };
|
|
198
212
|
|
|
@@ -244,7 +258,8 @@ export type RpcExtensionUIRequest =
|
|
|
244
258
|
widgetPlacement?: "aboveEditor" | "belowEditor";
|
|
245
259
|
}
|
|
246
260
|
| { type: "extension_ui_request"; id: string; method: "setTitle"; title: string }
|
|
247
|
-
| { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string }
|
|
261
|
+
| { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string }
|
|
262
|
+
| { type: "extension_ui_request"; id: string; method: "open_url"; url: string; instructions?: string };
|
|
248
263
|
|
|
249
264
|
// ============================================================================
|
|
250
265
|
// Host Tool Frames (bidirectional)
|
package/src/modes/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-my-pi/pi-ai";
|
|
3
|
-
import type { Component, Container, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import type { Component, Container, EditorTheme, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import type { KeybindingsManager } from "../config/keybindings";
|
|
5
5
|
import type { Settings } from "../config/settings";
|
|
6
6
|
import type {
|
|
@@ -131,6 +131,9 @@ export interface InteractiveModeContext {
|
|
|
131
131
|
setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
|
|
132
132
|
initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void;
|
|
133
133
|
createBackgroundUiContext(): ExtensionUIContext;
|
|
134
|
+
setEditorComponent(
|
|
135
|
+
factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined,
|
|
136
|
+
): void;
|
|
134
137
|
|
|
135
138
|
// Event handling
|
|
136
139
|
handleBackgroundEvent(event: AgentSessionEvent): Promise<void>;
|
|
@@ -226,7 +226,9 @@ export class UiHelpers {
|
|
|
226
226
|
break;
|
|
227
227
|
}
|
|
228
228
|
case "assistant": {
|
|
229
|
-
const assistantComponent = new AssistantMessageComponent(message, this.ctx.hideThinkingBlock)
|
|
229
|
+
const assistantComponent = new AssistantMessageComponent(message, this.ctx.hideThinkingBlock, () =>
|
|
230
|
+
this.ctx.ui.requestRender(),
|
|
231
|
+
);
|
|
230
232
|
this.ctx.chatContainer.addChild(assistantComponent);
|
|
231
233
|
break;
|
|
232
234
|
}
|
|
@@ -77,6 +77,20 @@ Report issue only when ALL conditions hold:
|
|
|
77
77
|
- **Proportionate rigor**: Fix doesn't demand rigor absent elsewhere in codebase
|
|
78
78
|
</criteria>
|
|
79
79
|
|
|
80
|
+
<cross-boundary>
|
|
81
|
+
For every new type, variant, or value introduced by the patch that crosses a function or module boundary
|
|
82
|
+
(event, message, command, frame, enum variant, queue item, IPC payload):
|
|
83
|
+
1. Locate the **dispatch point** — the switch, router, filter chain, handler registry, or loop body
|
|
84
|
+
that receives and routes values of that kind on the **consuming** side.
|
|
85
|
+
2. Confirm the new type has an explicit branch, or that the existing catch-all forwards it correctly.
|
|
86
|
+
3. If the new type falls through to a silent drop, no-op, or discard (e.g. an unmatched `if`/`switch`
|
|
87
|
+
that simply returns without processing), report it as a defect.
|
|
88
|
+
|
|
89
|
+
The dispatch point is frequently **outside the diff**. You **MUST** read it before concluding
|
|
90
|
+
the producing side is correct. Tracing only the emitting code while skipping the consuming
|
|
91
|
+
routing logic is the single most common source of missed integration bugs in reviews.
|
|
92
|
+
</cross-boundary>
|
|
93
|
+
|
|
80
94
|
<priority>
|
|
81
95
|
|Level|Criteria|Example|
|
|
82
96
|
|---|---|---|
|
|
@@ -10,17 +10,18 @@ Purely textual format. The tool has NO awareness of language, indentation, brack
|
|
|
10
10
|
@PATH header: subsequent ops apply to PATH
|
|
11
11
|
+ ANCHOR insert lines AFTER the anchored line (or EOF); payload follows as `{{hsep}}TEXT` lines
|
|
12
12
|
< ANCHOR insert lines BEFORE the anchored line (or BOF); payload follows as `{{hsep}}TEXT` lines
|
|
13
|
-
- A..B delete the line range (inclusive)
|
|
14
|
-
= A..B replace the range with payload `{{hsep}}TEXT` lines, or with one blank line if no payload follows
|
|
13
|
+
- A..B delete the line range (inclusive).
|
|
14
|
+
= A..B replace the range with payload `{{hsep}}TEXT` lines, or with one blank line if no payload follows.
|
|
15
15
|
</ops>
|
|
16
16
|
|
|
17
17
|
<rules>
|
|
18
18
|
- Every line of inserted/replacement content **MUST** be emitted as a payload line starting with `{{hsep}}`.
|
|
19
19
|
- `{{hsep}}` is syntax, not content. The inserted text begins after the first `{{hsep}}`; use a bare `{{hsep}}` to insert a blank line.
|
|
20
20
|
- `< A` inserts before line A; `+ A` inserts after line A. `< BOF` / `+ BOF` both prepend; `< EOF` / `+ EOF` both append.
|
|
21
|
-
- `= A..B` replaces the inclusive range with the following payload lines. `= A
|
|
22
|
-
- `- A..B` deletes the inclusive range;
|
|
23
|
-
-
|
|
21
|
+
- `= A..B` replaces the inclusive range with the following payload lines. `= A..B` with no payload blanks the range to a single empty line.
|
|
22
|
+
- `- A..B` deletes the inclusive range; `A..A` for one line.
|
|
23
|
+
- **Choose a self-contained syntactic unit first.** If the change touches part of a multiline call, destructuring assignment, control-flow header, wrapper, or other construct, widen the range to include the whole construct before optimizing for size.
|
|
24
|
+
- Only after the range is self-contained, pick the smallest op for the change: pure addition → `+`/`<`; pure deletion → `-`; `= A..B` ONLY when content inside `A..B` is actually being modified or removed.
|
|
24
25
|
</rules>
|
|
25
26
|
|
|
26
27
|
<brace-shapes>
|
|
@@ -35,7 +36,7 @@ When your edit involves brace boundaries (`{` / `}`), prefer these shapes:
|
|
|
35
36
|
- **Do not replay the line past your range.** For `= A..B`, never end the payload with content that already exists at B+1. Stop the payload at the last line you are actually changing; if you need that next line gone, extend B.
|
|
36
37
|
- **Do not duplicate chunks inside one payload.** When emitting a long `=` payload, never paste the same multi-line block twice. If you catch yourself re-emitting an earlier run of lines, stop and rewrite the op.
|
|
37
38
|
- **Anchor only inside the visible region.** If the read output around your `=`/`-` end anchor is truncated (you cannot see the line at B+1), issue a fresh `read` before editing — anchoring blind drops or duplicates the boundary line.
|
|
38
|
-
- **Prefer
|
|
39
|
+
- **Prefer the narrowest self-contained edit.** Once your range cleanly contains the construct you are changing, a `+`/`<` insert plus a small `-` delete is almost always clearer and safer than a single wide `= A..B` that re-emits unchanged context.
|
|
39
40
|
</common-failures>
|
|
40
41
|
|
|
41
42
|
<case file="a.ts">
|
|
@@ -47,10 +48,22 @@ When your edit involves brace boundaries (`{` / `}`), prefer these shapes:
|
|
|
47
48
|
{{hline 6 "}"}}
|
|
48
49
|
</case>
|
|
49
50
|
|
|
51
|
+
<case file="b.ts">
|
|
52
|
+
{{hline 1 "const {"}}
|
|
53
|
+
{{hline 2 "\tevents,"}}
|
|
54
|
+
{{hline 3 "\tresponse,"}}
|
|
55
|
+
{{hline 4 "\trequestId,"}}
|
|
56
|
+
{{hline 5 "} = await getStreamResponse("}}
|
|
57
|
+
{{hline 6 "\trequest,"}}
|
|
58
|
+
{{hline 7 "\tsignal,"}}
|
|
59
|
+
{{hline 8 ");"}}
|
|
60
|
+
{{hline 9 "await notify(requestId);"}}
|
|
61
|
+
</case>
|
|
62
|
+
|
|
50
63
|
<examples>
|
|
51
64
|
# Replace one line (preserve the leading tab from the original)
|
|
52
65
|
@a.ts
|
|
53
|
-
= {{hrefr 5}}
|
|
66
|
+
= {{hrefr 5}}..{{hrefr 5}}
|
|
54
67
|
{{hsep}} return clean.trim().toUpperCase();
|
|
55
68
|
|
|
56
69
|
# Replace a contiguous range with multiple lines
|
|
@@ -59,6 +72,19 @@ When your edit involves brace boundaries (`{` / `}`), prefer these shapes:
|
|
|
59
72
|
{{hsep}} const clean = (name || DEF).trim();
|
|
60
73
|
{{hsep}} return clean.length === 0 ? DEF : clean.toUpperCase();
|
|
61
74
|
|
|
75
|
+
# Replace a full multiline destructuring/call statement
|
|
76
|
+
@b.ts
|
|
77
|
+
= {{hrefr 1}}..{{hrefr 8}}
|
|
78
|
+
{{hsep}}const {
|
|
79
|
+
{{hsep}} events,
|
|
80
|
+
{{hsep}} response,
|
|
81
|
+
{{hsep}} requestId,
|
|
82
|
+
{{hsep}}} = await getStreamResponse(
|
|
83
|
+
{{hsep}} request,
|
|
84
|
+
{{hsep}} signal,
|
|
85
|
+
{{hsep}} onEvent,
|
|
86
|
+
{{hsep}});
|
|
87
|
+
|
|
62
88
|
# Insert BEFORE a line
|
|
63
89
|
@a.ts
|
|
64
90
|
< {{hrefr 5}}
|
|
@@ -80,11 +106,11 @@ When your edit involves brace boundaries (`{` / `}`), prefer these shapes:
|
|
|
80
106
|
|
|
81
107
|
# Delete a single line
|
|
82
108
|
@a.ts
|
|
83
|
-
- {{hrefr 2}}
|
|
109
|
+
- {{hrefr 2}}..{{hrefr 2}}
|
|
84
110
|
|
|
85
111
|
# Blank a line in place (no payload required)
|
|
86
112
|
@a.ts
|
|
87
|
-
= {{hrefr 2}}
|
|
113
|
+
= {{hrefr 2}}..{{hrefr 2}}
|
|
88
114
|
</examples>
|
|
89
115
|
|
|
90
116
|
<anti-pattern>
|
|
@@ -103,7 +129,28 @@ When your edit involves brace boundaries (`{` / `}`), prefer these shapes:
|
|
|
103
129
|
+ {{hrefr 1}}
|
|
104
130
|
{{hsep}}const DEBUG = false;
|
|
105
131
|
|
|
106
|
-
|
|
132
|
+
# WRONG — continuation-fragment payload from the middle of a larger statement.
|
|
133
|
+
@b.ts
|
|
134
|
+
= {{hrefr 5}}..{{hrefr 7}}
|
|
135
|
+
{{hsep}}} = await getStreamResponse(
|
|
136
|
+
{{hsep}} request,
|
|
137
|
+
{{hsep}} signal,
|
|
138
|
+
{{hsep}} onEvent,
|
|
139
|
+
|
|
140
|
+
# RIGHT — widen to the full statement so the payload starts at a self-contained boundary.
|
|
141
|
+
@b.ts
|
|
142
|
+
= {{hrefr 1}}..{{hrefr 8}}
|
|
143
|
+
{{hsep}}const {
|
|
144
|
+
{{hsep}} events,
|
|
145
|
+
{{hsep}} response,
|
|
146
|
+
{{hsep}} requestId,
|
|
147
|
+
{{hsep}}} = await getStreamResponse(
|
|
148
|
+
{{hsep}} request,
|
|
149
|
+
{{hsep}} signal,
|
|
150
|
+
{{hsep}} onEvent,
|
|
151
|
+
{{hsep}});
|
|
152
|
+
|
|
153
|
+
If your replacement payload would render with even one unchanged line in the diff, or if the first or last payload line is only a continuation fragment from a larger construct (`} =`, `);`, `,`, `.method(`), you have the wrong op or range. Stop and widen to a self-contained boundary before minimizing the edit.
|
|
107
154
|
</anti-pattern>
|
|
108
155
|
|
|
109
156
|
<critical>
|
package/src/sdk.ts
CHANGED
|
@@ -1675,9 +1675,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1675
1675
|
preferWebsockets: preferOpenAICodexWebsockets,
|
|
1676
1676
|
getToolContext: tc => toolContextStore.getContext(tc),
|
|
1677
1677
|
getApiKey: async provider => {
|
|
1678
|
-
//
|
|
1679
|
-
//
|
|
1680
|
-
const key = await modelRegistry.getApiKeyForProvider(provider,
|
|
1678
|
+
// Read agent.sessionId at call time so credential selection stays aligned
|
|
1679
|
+
// with metadataResolver after /new, fork, resume, or branch switches.
|
|
1680
|
+
const key = await modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
|
|
1681
1681
|
if (!key) {
|
|
1682
1682
|
throw new Error(`No API key found for provider "${provider}"`);
|
|
1683
1683
|
}
|
|
@@ -1757,6 +1757,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1757
1757
|
asyncJobManager,
|
|
1758
1758
|
agentId: resolvedAgentId,
|
|
1759
1759
|
agentRegistry,
|
|
1760
|
+
providerSessionId: options.providerSessionId,
|
|
1760
1761
|
});
|
|
1761
1762
|
hasSession = true;
|
|
1762
1763
|
|