@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/package.json +16 -7
  3. package/src/config/model-resolver.ts +92 -35
  4. package/src/config/prompt-templates.ts +1 -1
  5. package/src/debug/index.ts +21 -0
  6. package/src/debug/raw-sse-buffer.ts +229 -0
  7. package/src/debug/raw-sse.ts +213 -0
  8. package/src/edit/index.ts +9 -10
  9. package/src/edit/streaming.ts +6 -5
  10. package/src/eval/js/context-manager.ts +91 -47
  11. package/src/extensibility/extensions/loader.ts +9 -3
  12. package/src/extensibility/extensions/types.ts +10 -3
  13. package/src/extensibility/plugins/legacy-pi-compat.ts +99 -20
  14. package/src/hashline/anchors.ts +113 -0
  15. package/src/hashline/apply.ts +732 -0
  16. package/src/hashline/bigrams.json +649 -0
  17. package/src/hashline/constants.ts +8 -0
  18. package/src/hashline/diff-preview.ts +43 -0
  19. package/src/hashline/diff.ts +56 -0
  20. package/src/hashline/execute.ts +268 -0
  21. package/src/{edit/modes/hashline.lark → hashline/grammar.lark} +1 -1
  22. package/src/{edit/line-hash.ts → hashline/hash.ts} +5 -651
  23. package/src/hashline/index.ts +14 -0
  24. package/src/hashline/input.ts +110 -0
  25. package/src/hashline/parser.ts +220 -0
  26. package/src/hashline/prefixes.ts +101 -0
  27. package/src/hashline/recovery.ts +72 -0
  28. package/src/hashline/stream.ts +123 -0
  29. package/src/hashline/types.ts +69 -0
  30. package/src/hashline/utils.ts +3 -0
  31. package/src/index.ts +1 -1
  32. package/src/lsp/index.ts +1 -1
  33. package/src/lsp/render.ts +4 -0
  34. package/src/memories/index.ts +13 -4
  35. package/src/modes/components/assistant-message.ts +55 -9
  36. package/src/modes/components/welcome.ts +114 -38
  37. package/src/modes/controllers/event-controller.ts +3 -1
  38. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  39. package/src/modes/controllers/input-controller.ts +8 -1
  40. package/src/modes/interactive-mode.ts +50 -11
  41. package/src/modes/prompt-action-autocomplete.ts +3 -0
  42. package/src/modes/rpc/rpc-client.ts +53 -2
  43. package/src/modes/rpc/rpc-mode.ts +67 -1
  44. package/src/modes/rpc/rpc-types.ts +17 -2
  45. package/src/modes/types.ts +4 -1
  46. package/src/modes/utils/ui-helpers.ts +3 -1
  47. package/src/prompts/agents/reviewer.md +14 -0
  48. package/src/prompts/tools/hashline.md +57 -10
  49. package/src/sdk.ts +4 -3
  50. package/src/session/agent-session.ts +195 -30
  51. package/src/session/compaction/branch-summarization.ts +4 -2
  52. package/src/session/compaction/compaction.ts +22 -3
  53. package/src/task/executor.ts +21 -2
  54. package/src/task/index.ts +4 -1
  55. package/src/tools/ast-edit.ts +1 -1
  56. package/src/tools/match-line-format.ts +1 -1
  57. package/src/tools/read.ts +1 -1
  58. package/src/utils/file-mentions.ts +1 -1
  59. package/src/utils/title-generator.ts +11 -0
  60. 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)
@@ -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); `- A` for one line
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` (or `= A..B`) with no payload blanks the range to a single empty line.
22
- - `- A..B` deletes the inclusive range; omit `..B` for one line.
23
- - 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.
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 narrow ops over wide `=`.** A `+`/`<` insert plus a small `-` delete is almost always clearer and safer than a single wide `= A..B` that re-emits unchanged context.
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
- If your replacement payload would render with even one unchanged line in the diff, you have the wrong op or range. Stop and rewrite as `+`/`<`/`-` plus a narrower `=`.
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
- // Use the provider-facing session id for sticky credential selection so cache keys
1679
- // and provider auth affinity stay aligned across fresh benchmark sessions.
1680
- const key = await modelRegistry.getApiKeyForProvider(provider, providerSessionId);
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