@oh-my-pi/pi-coding-agent 15.1.2 → 15.1.4

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 (155) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/types/async/job-manager.d.ts +3 -2
  3. package/dist/types/cli/auth-broker-cli.d.ts +25 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +18 -0
  5. package/dist/types/cli/grievances-cli.d.ts +12 -0
  6. package/dist/types/commands/auth-broker.d.ts +54 -0
  7. package/dist/types/commands/auth-gateway.d.ts +32 -0
  8. package/dist/types/commands/grievances.d.ts +1 -1
  9. package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -1
  10. package/dist/types/commit/agentic/tools/schemas.d.ts +9 -1
  11. package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -1
  12. package/dist/types/config/model-registry.d.ts +3 -0
  13. package/dist/types/config/models-config-schema.d.ts +1 -0
  14. package/dist/types/config/settings-schema.d.ts +46 -0
  15. package/dist/types/discovery/agents.d.ts +12 -1
  16. package/dist/types/edit/renderer.d.ts +3 -0
  17. package/dist/types/eval/index.d.ts +0 -2
  18. package/dist/types/goals/tools/goal-tool.d.ts +10 -2
  19. package/dist/types/index.d.ts +0 -1
  20. package/dist/types/internal-urls/index.d.ts +1 -1
  21. package/dist/types/internal-urls/{pi-protocol.d.ts → omp-protocol.d.ts} +3 -3
  22. package/dist/types/internal-urls/types.d.ts +1 -1
  23. package/dist/types/main.d.ts +11 -2
  24. package/dist/types/modes/acp/acp-agent.d.ts +2 -1
  25. package/dist/types/modes/acp/acp-event-mapper.d.ts +13 -1
  26. package/dist/types/modes/acp/acp-mode.d.ts +3 -1
  27. package/dist/types/modes/emoji-autocomplete.d.ts +16 -0
  28. package/dist/types/modes/interactive-mode.d.ts +1 -1
  29. package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
  30. package/dist/types/plan-mode/approved-plan.d.ts +10 -4
  31. package/dist/types/sdk.d.ts +10 -3
  32. package/dist/types/session/agent-session.d.ts +7 -3
  33. package/dist/types/session/auth-broker-config.d.ts +13 -0
  34. package/dist/types/session/auth-storage.d.ts +1 -1
  35. package/dist/types/session/client-bridge.d.ts +3 -0
  36. package/dist/types/tools/eval.d.ts +41 -7
  37. package/dist/types/tools/irc.d.ts +8 -2
  38. package/dist/types/tools/report-tool-issue.d.ts +118 -1
  39. package/dist/types/tools/resolve.d.ts +8 -2
  40. package/examples/custom-tools/README.md +3 -12
  41. package/examples/extensions/README.md +2 -15
  42. package/examples/extensions/api-demo.ts +1 -7
  43. package/package.json +7 -7
  44. package/src/async/job-manager.ts +111 -13
  45. package/src/autoresearch/tools/init-experiment.ts +11 -33
  46. package/src/autoresearch/tools/log-experiment.ts +10 -24
  47. package/src/autoresearch/tools/run-experiment.ts +1 -1
  48. package/src/autoresearch/tools/update-notes.ts +2 -9
  49. package/src/cli/auth-broker-cli.ts +746 -0
  50. package/src/cli/auth-gateway-cli.ts +342 -0
  51. package/src/cli/grievances-cli.ts +109 -16
  52. package/src/cli/update-cli.ts +1 -5
  53. package/src/cli.ts +4 -2
  54. package/src/commands/auth-broker.ts +96 -0
  55. package/src/commands/auth-gateway.ts +61 -0
  56. package/src/commands/grievances.ts +13 -8
  57. package/src/commands/launch.ts +1 -1
  58. package/src/commit/agentic/agent.ts +2 -0
  59. package/src/commit/agentic/tools/analyze-file.ts +2 -2
  60. package/src/commit/agentic/tools/git-file-diff.ts +2 -2
  61. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  62. package/src/commit/agentic/tools/git-overview.ts +2 -2
  63. package/src/commit/agentic/tools/propose-changelog.ts +1 -3
  64. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  65. package/src/commit/agentic/tools/schemas.ts +1 -9
  66. package/src/config/model-equivalence.ts +279 -174
  67. package/src/config/model-registry.ts +37 -6
  68. package/src/config/model-resolver.ts +13 -8
  69. package/src/config/models-config-schema.ts +8 -0
  70. package/src/config/settings-schema.ts +52 -0
  71. package/src/cursor.ts +1 -1
  72. package/src/debug/log-formatting.ts +1 -1
  73. package/src/debug/log-viewer.ts +1 -1
  74. package/src/debug/profiler.ts +4 -0
  75. package/src/debug/raw-sse-buffer.ts +100 -59
  76. package/src/debug/raw-sse.ts +1 -1
  77. package/src/discovery/agents.ts +15 -4
  78. package/src/edit/modes/apply-patch.ts +1 -5
  79. package/src/edit/modes/patch.ts +5 -5
  80. package/src/edit/modes/replace.ts +5 -5
  81. package/src/edit/renderer.ts +2 -1
  82. package/src/edit/streaming.ts +1 -1
  83. package/src/eval/index.ts +0 -2
  84. package/src/eval/js/shared/runtime.ts +107 -2
  85. package/src/eval/py/kernel.ts +1 -1
  86. package/src/exa/researcher.ts +4 -4
  87. package/src/exa/search.ts +10 -22
  88. package/src/exa/websets.ts +33 -33
  89. package/src/extensibility/typebox.ts +44 -17
  90. package/src/goals/tools/goal-tool.ts +3 -3
  91. package/src/index.ts +0 -3
  92. package/src/internal-urls/docs-index.generated.ts +21 -18
  93. package/src/internal-urls/index.ts +1 -1
  94. package/src/internal-urls/{pi-protocol.ts → omp-protocol.ts} +10 -10
  95. package/src/internal-urls/router.ts +3 -3
  96. package/src/internal-urls/types.ts +1 -1
  97. package/src/lsp/types.ts +8 -11
  98. package/src/main.ts +216 -146
  99. package/src/mcp/tool-bridge.ts +3 -3
  100. package/src/modes/acp/acp-agent.ts +203 -57
  101. package/src/modes/acp/acp-client-bridge.ts +2 -1
  102. package/src/modes/acp/acp-event-mapper.ts +208 -32
  103. package/src/modes/acp/acp-mode.ts +11 -3
  104. package/src/modes/components/bash-execution.ts +1 -1
  105. package/src/modes/components/diff.ts +1 -2
  106. package/src/modes/components/eval-execution.ts +1 -1
  107. package/src/modes/components/oauth-selector.ts +38 -2
  108. package/src/modes/components/tool-execution.ts +1 -2
  109. package/src/modes/components/tree-selector.ts +26 -7
  110. package/src/modes/controllers/command-controller.ts +95 -34
  111. package/src/modes/controllers/input-controller.ts +4 -3
  112. package/src/modes/data/emojis.json +1 -0
  113. package/src/modes/emoji-autocomplete.ts +285 -0
  114. package/src/modes/interactive-mode.ts +92 -19
  115. package/src/modes/print-mode.ts +3 -3
  116. package/src/modes/prompt-action-autocomplete.ts +14 -0
  117. package/src/plan-mode/approved-plan.ts +30 -9
  118. package/src/prompts/system/system-prompt.md +1 -1
  119. package/src/prompts/system/ttsr-tool-reminder.md +5 -0
  120. package/src/prompts/tools/ask.md +4 -3
  121. package/src/prompts/tools/eval.md +25 -26
  122. package/src/prompts/tools/read.md +1 -1
  123. package/src/prompts/tools/resolve.md +1 -1
  124. package/src/prompts/tools/search.md +1 -1
  125. package/src/prompts/tools/web-search.md +1 -1
  126. package/src/sdk.ts +81 -8
  127. package/src/session/agent-session.ts +362 -131
  128. package/src/session/agent-storage.ts +7 -2
  129. package/src/session/auth-broker-config.ts +102 -0
  130. package/src/session/auth-storage.ts +7 -1
  131. package/src/session/client-bridge.ts +3 -0
  132. package/src/session/streaming-output.ts +1 -1
  133. package/src/task/types.ts +10 -35
  134. package/src/tools/bash-interactive.ts +4 -1
  135. package/src/tools/bash-pty-selection.ts +2 -2
  136. package/src/tools/browser.ts +12 -20
  137. package/src/tools/eval.ts +77 -100
  138. package/src/tools/gh.ts +21 -45
  139. package/src/tools/hindsight-recall.ts +1 -1
  140. package/src/tools/hindsight-reflect.ts +2 -2
  141. package/src/tools/hindsight-retain.ts +3 -7
  142. package/src/tools/index.ts +8 -1
  143. package/src/tools/inspect-image.ts +4 -1
  144. package/src/tools/irc.ts +4 -12
  145. package/src/tools/job.ts +3 -11
  146. package/src/tools/report-tool-issue.ts +462 -17
  147. package/src/tools/resolve.ts +2 -7
  148. package/src/tools/todo-write.ts +8 -15
  149. package/src/utils/title-generator.ts +3 -0
  150. package/src/web/search/index.ts +6 -6
  151. package/dist/types/eval/parse.d.ts +0 -28
  152. package/dist/types/eval/sniff.d.ts +0 -11
  153. package/src/eval/eval.lark +0 -36
  154. package/src/eval/parse.ts +0 -407
  155. package/src/eval/sniff.ts +0 -28
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  SessionNotification,
3
3
  SessionUpdate,
4
+ ToolCall,
4
5
  ToolCallContent,
5
6
  ToolCallLocation,
6
7
  ToolKind,
@@ -17,6 +18,7 @@ interface MessageProgress {
17
18
  interface AcpEventMapperOptions {
18
19
  getMessageId?: (message: unknown) => string | undefined;
19
20
  getMessageProgress?: (message: unknown) => MessageProgress | undefined;
21
+ getToolArgs?: (toolCallId: string) => unknown;
20
22
  /**
21
23
  * Session cwd. Tool call locations sent to ACP clients must be absolute
22
24
  * (the editor host needs them to open or focus files). When provided,
@@ -30,6 +32,10 @@ interface ContentArrayContainer {
30
32
  content?: unknown;
31
33
  }
32
34
 
35
+ interface DetailsContainer {
36
+ details?: unknown;
37
+ }
38
+
33
39
  interface TypedValue {
34
40
  type?: unknown;
35
41
  }
@@ -38,6 +44,10 @@ interface TextLikeContent extends TypedValue {
38
44
  text?: unknown;
39
45
  }
40
46
 
47
+ interface TerminalIdContainer {
48
+ terminalId?: unknown;
49
+ }
50
+
41
51
  interface BinaryLikeContent extends TypedValue {
42
52
  data?: unknown;
43
53
  mimeType?: unknown;
@@ -118,6 +128,8 @@ export function mapToolKind(toolName: string): ToolKind {
118
128
  case "move":
119
129
  return "move";
120
130
  case "bash":
131
+ case "shell":
132
+ case "exec":
121
133
  case "eval":
122
134
  return "execute";
123
135
  case "search":
@@ -144,24 +156,20 @@ export function mapAgentSessionEventToAcpSessionUpdates(
144
156
  case "message_end":
145
157
  return mapAssistantMessageEnd(event, sessionId, options);
146
158
  case "tool_execution_start": {
147
- const update: SessionUpdate = {
148
- sessionUpdate: "tool_call",
159
+ const update = buildToolCallStartUpdate({
149
160
  toolCallId: event.toolCallId,
150
- title: buildToolTitle(event.toolName, event.args, event.intent),
151
- kind: mapToolKind(event.toolName),
152
- status: "pending",
153
- rawInput: event.args,
154
- };
155
- const locations = extractToolLocations(event.args, options.cwd);
156
- if (locations.length > 0) {
157
- update.locations = locations;
158
- }
161
+ toolName: event.toolName,
162
+ args: event.args,
163
+ intent: event.intent,
164
+ cwd: options.cwd,
165
+ });
159
166
  return [toSessionNotification(sessionId, update)];
160
167
  }
161
168
  case "tool_execution_update": {
162
- const terminalContent = extractTerminalToolCallContent(event.partialResult);
163
- const otherContent = terminalContent.length > 0 ? [] : extractToolCallContent(event.partialResult);
164
- const content = [...terminalContent, ...otherContent];
169
+ const content = mergeToolUpdateContent(
170
+ buildToolStartContent(event.toolName, event.args),
171
+ extractToolCallContent(event.partialResult),
172
+ );
165
173
  const update: SessionUpdate = {
166
174
  sessionUpdate: "tool_call_update",
167
175
  toolCallId: event.toolCallId,
@@ -178,10 +186,11 @@ export function mapAgentSessionEventToAcpSessionUpdates(
178
186
  return [toSessionNotification(sessionId, update)];
179
187
  }
180
188
  case "tool_execution_end": {
181
- const diffContent = extractDiffToolCallContent(event.result);
182
- const terminalContent = extractTerminalToolCallContent(event.result);
183
- const otherContent = extractToolCallContent(event.result);
184
- const content = [...diffContent, ...terminalContent, ...otherContent];
189
+ const resultContent = [...extractDiffToolCallContent(event.result), ...extractToolCallContent(event.result)];
190
+ const content = mergeToolUpdateContent(
191
+ buildToolStartContent(event.toolName, getToolExecutionEndArgs(event, options)),
192
+ resultContent,
193
+ );
185
194
  const update: SessionUpdate = {
186
195
  sessionUpdate: "tool_call_update",
187
196
  toolCallId: event.toolCallId,
@@ -195,7 +204,12 @@ export function mapAgentSessionEventToAcpSessionUpdates(
195
204
  if (locations.length > 0) {
196
205
  update.locations = locations;
197
206
  }
198
- return [toSessionNotification(sessionId, update)];
207
+ const notifications = [toSessionNotification(sessionId, update)];
208
+ const planUpdate = mapTodoWriteResultToPlanUpdate(event);
209
+ if (planUpdate) {
210
+ notifications.push(toSessionNotification(sessionId, planUpdate));
211
+ }
212
+ return notifications;
199
213
  }
200
214
  case "todo_reminder": {
201
215
  const entries = event.todos.map(todo => ({
@@ -312,6 +326,144 @@ function mapTodoStatus(status: TodoStatus): "pending" | "in_progress" | "complet
312
326
  return todoStatusMap[status];
313
327
  }
314
328
 
329
+ function mapTodoWriteResultToPlanUpdate(
330
+ event: Extract<AgentSessionEvent, { type: "tool_execution_end" }>,
331
+ ): SessionUpdate | undefined {
332
+ if (event.toolName !== "todo_write" || event.isError) {
333
+ return undefined;
334
+ }
335
+ const phases = extractTodoWritePhases(event.result);
336
+ if (!Array.isArray(phases)) {
337
+ return undefined;
338
+ }
339
+ return {
340
+ sessionUpdate: "plan",
341
+ entries: extractTodoEntries(phases).map(todo => ({
342
+ content: todo.content,
343
+ priority: "medium" as const,
344
+ status: mapTodoStatus(todo.status),
345
+ })),
346
+ };
347
+ }
348
+
349
+ function extractTodoWritePhases(result: unknown): unknown {
350
+ if (typeof result !== "object" || result === null || !("details" in result)) {
351
+ return undefined;
352
+ }
353
+ const details = (result as { details?: unknown }).details;
354
+ if (typeof details !== "object" || details === null || !("phases" in details)) {
355
+ return undefined;
356
+ }
357
+ return (details as { phases?: unknown }).phases;
358
+ }
359
+
360
+ function extractTodoEntries(phases: unknown[]): Array<{ content: string; status: TodoStatus }> {
361
+ const entries: Array<{ content: string; status: TodoStatus }> = [];
362
+ for (const phase of phases) {
363
+ if (typeof phase !== "object" || phase === null || !("tasks" in phase)) {
364
+ continue;
365
+ }
366
+ const tasks = (phase as { tasks?: unknown }).tasks;
367
+ if (!Array.isArray(tasks)) {
368
+ continue;
369
+ }
370
+ for (const task of tasks) {
371
+ if (typeof task !== "object" || task === null || !("content" in task)) {
372
+ continue;
373
+ }
374
+ const content = (task as { content?: unknown }).content;
375
+ if (typeof content !== "string" || content.length === 0) {
376
+ continue;
377
+ }
378
+ const status = (task as { status?: TodoStatus }).status;
379
+ entries.push({ content, status: isTodoStatus(status) ? status : "pending" });
380
+ }
381
+ }
382
+ return entries;
383
+ }
384
+
385
+ function isTodoStatus(status: unknown): status is TodoStatus {
386
+ return status === "pending" || status === "in_progress" || status === "completed" || status === "abandoned";
387
+ }
388
+ export function buildToolCallStartUpdate(input: {
389
+ toolCallId: string;
390
+ toolName: string;
391
+ args: unknown;
392
+ intent?: string;
393
+ cwd?: string;
394
+ status?: "pending" | "completed";
395
+ }): SessionUpdate {
396
+ const update: ToolCall & { sessionUpdate: "tool_call" } = {
397
+ sessionUpdate: "tool_call",
398
+ toolCallId: input.toolCallId,
399
+ title: buildToolTitle(input.toolName, input.args, input.intent),
400
+ kind: mapToolKind(input.toolName),
401
+ status: input.status ?? "pending",
402
+ rawInput: input.args,
403
+ };
404
+ const content = buildToolStartContent(input.toolName, input.args);
405
+ if (content.length > 0) {
406
+ update.content = content;
407
+ }
408
+ const locations = extractToolLocations(input.args, input.cwd);
409
+ if (locations.length > 0) {
410
+ update.locations = locations;
411
+ }
412
+ return update;
413
+ }
414
+
415
+ export function normalizeReplayToolArguments(value: unknown): { args: unknown } {
416
+ if (typeof value !== "string") {
417
+ return { args: value ?? {} };
418
+ }
419
+ try {
420
+ const parsed: unknown = JSON.parse(value);
421
+ return { args: parsed };
422
+ } catch {
423
+ return { args: value };
424
+ }
425
+ }
426
+
427
+ function getToolExecutionEndArgs(
428
+ event: Extract<AgentSessionEvent, { type: "tool_execution_end" }>,
429
+ options: AcpEventMapperOptions,
430
+ ): unknown {
431
+ if ("args" in event) {
432
+ return (event as { args?: unknown }).args;
433
+ }
434
+ return options.getToolArgs?.(event.toolCallId);
435
+ }
436
+
437
+ function buildToolStartContent(toolName: string, args: unknown): ToolCallContent[] {
438
+ if (!isCommandToolName(toolName)) {
439
+ return [];
440
+ }
441
+ const command = extractStringProperty<CommandContainer>(args, "command");
442
+ return command ? [textToolCallContent(`$ ${command}`)] : [];
443
+ }
444
+
445
+ function mergeToolUpdateContent(startContent: ToolCallContent[], resultContent: ToolCallContent[]): ToolCallContent[] {
446
+ if (startContent.length === 0) {
447
+ return resultContent;
448
+ }
449
+ const merged = [...startContent];
450
+ for (const item of resultContent) {
451
+ if (
452
+ item.type === "content" &&
453
+ item.content.type === "text" &&
454
+ hasEquivalentTextContent(merged, item.content.text)
455
+ ) {
456
+ continue;
457
+ }
458
+ merged.push(item);
459
+ }
460
+ return merged;
461
+ }
462
+
463
+ function isCommandToolName(toolName: string): boolean {
464
+ return toolName === "bash" || toolName === "shell" || toolName === "exec";
465
+ }
466
+
315
467
  function buildToolTitle(toolName: string, args: unknown, intent: string | undefined): string {
316
468
  const trimmedIntent = intent?.trim();
317
469
  if (trimmedIntent) {
@@ -418,26 +570,33 @@ function buildDiffContent(entry: unknown): ToolCallContent | undefined {
418
570
  };
419
571
  }
420
572
 
421
- /** Emit a `terminal` ToolCallContent when a tool result carries a `details.terminalId` (e.g. bash routed through ACP terminal/*). */
422
- function extractTerminalToolCallContent(result: unknown): ToolCallContent[] {
423
- if (typeof result !== "object" || result === null) return [];
424
- const details = (result as { details?: unknown }).details;
425
- if (typeof details !== "object" || details === null) return [];
426
- const terminalId = (details as { terminalId?: unknown }).terminalId;
427
- if (typeof terminalId !== "string" || terminalId.length === 0) return [];
428
- return [{ type: "terminal", terminalId }];
573
+ function extractTerminalId(value: unknown): string | undefined {
574
+ const direct = extractStringProperty<TerminalIdContainer>(value, "terminalId");
575
+ if (direct) return direct;
576
+ if (typeof value !== "object" || value === null) return undefined;
577
+ const details = (value as DetailsContainer).details;
578
+ return extractStringProperty<TerminalIdContainer>(details, "terminalId");
579
+ }
580
+
581
+ function terminalToolCallContent(terminalId: string): ToolCallContent {
582
+ return { type: "terminal", terminalId };
429
583
  }
430
584
 
431
585
  function extractToolCallContent(value: unknown): ToolCallContent[] {
432
586
  const richContent = extractStructuredToolCallContent(value);
587
+ const terminalId = extractTerminalId(value);
588
+ const content =
589
+ terminalId && !hasTerminalContent(richContent, terminalId)
590
+ ? [...richContent, terminalToolCallContent(terminalId)]
591
+ : richContent;
433
592
  const fallbackText = extractReadableText(value);
434
593
  if (!fallbackText) {
435
- return richContent;
594
+ return content;
436
595
  }
437
- if (hasEquivalentTextContent(richContent, fallbackText)) {
438
- return richContent;
596
+ if (hasEquivalentTextContent(content, fallbackText)) {
597
+ return content;
439
598
  }
440
- return [...richContent, textToolCallContent(fallbackText)];
599
+ return [...content, textToolCallContent(fallbackText)];
441
600
  }
442
601
 
443
602
  function extractStructuredToolCallContent(value: unknown): ToolCallContent[] {
@@ -596,6 +755,10 @@ function hasEquivalentTextContent(content: ToolCallContent[], text: string): boo
596
755
  return content.some(item => item.type === "content" && item.content.type === "text" && item.content.text === text);
597
756
  }
598
757
 
758
+ function hasTerminalContent(content: ToolCallContent[], terminalId: string): boolean {
759
+ return content.some(item => item.type === "terminal" && item.terminalId === terminalId);
760
+ }
761
+
599
762
  function extractReadableText(value: unknown): string | undefined {
600
763
  if (typeof value === "string") {
601
764
  return normalizeText(value);
@@ -625,11 +788,24 @@ function extractReadableText(value: unknown): string | undefined {
625
788
  return normalizeText(text);
626
789
  }
627
790
  }
628
-
791
+ if (isTerminalOnlyDetails(value)) {
792
+ return undefined;
793
+ }
629
794
  const serialized = safeJsonStringify(value);
630
795
  return normalizeText(serialized);
631
796
  }
632
797
 
798
+ function isTerminalOnlyDetails(value: unknown): boolean {
799
+ if (typeof value !== "object" || value === null) {
800
+ return false;
801
+ }
802
+ if (extractTerminalId(value) === undefined) {
803
+ return false;
804
+ }
805
+ const content = (value as ContentArrayContainer).content;
806
+ return content === undefined || (Array.isArray(content) && content.length === 0);
807
+ }
808
+
633
809
  function extractAssistantMessageText(value: unknown): string {
634
810
  if (typeof value !== "object" || value === null || !("content" in value)) {
635
811
  return "";
@@ -1,15 +1,23 @@
1
1
  import * as stream from "node:stream";
2
- import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
2
+ import { AgentSideConnection, ndJsonStream, type Stream } from "@agentclientprotocol/sdk";
3
3
  import type { AgentSession } from "../../session/agent-session";
4
4
  import { AcpAgent } from "./acp-agent";
5
5
 
6
6
  export type AcpSessionFactory = (cwd: string) => Promise<AgentSession>;
7
7
 
8
- export async function runAcpMode(session: AgentSession, createSession: AcpSessionFactory): Promise<never> {
8
+ export function createAcpConnection(
9
+ transport: Stream,
10
+ createSession: AcpSessionFactory,
11
+ initialSession?: AgentSession,
12
+ ): AgentSideConnection {
13
+ return new AgentSideConnection(conn => new AcpAgent(conn, createSession, initialSession), transport);
14
+ }
15
+
16
+ export async function runAcpMode(createSession: AcpSessionFactory, initialSession?: AgentSession): Promise<never> {
9
17
  const input = stream.Writable.toWeb(process.stdout);
10
18
  const output = stream.Readable.toWeb(process.stdin);
11
19
  const transport = ndJsonStream(input, output);
12
- const connection = new AgentSideConnection(conn => new AcpAgent(conn, session, createSession), transport);
20
+ const connection = createAcpConnection(transport, createSession, initialSession);
13
21
  await connection.closed;
14
22
  process.exit(0);
15
23
  }
@@ -2,7 +2,6 @@
2
2
  * Component for displaying bash command execution with streaming output.
3
3
  */
4
4
 
5
- import { sanitizeText } from "@oh-my-pi/pi-natives";
6
5
  import {
7
6
  Container,
8
7
  Ellipsis,
@@ -14,6 +13,7 @@ import {
14
13
  truncateToWidth,
15
14
  visibleWidth,
16
15
  } from "@oh-my-pi/pi-tui";
16
+ import { sanitizeText } from "@oh-my-pi/pi-utils";
17
17
  import { theme } from "../../modes/theme/theme";
18
18
  import type { TruncationMeta } from "../../tools/output-meta";
19
19
  import { getSixelLineMask, isSixelPassthroughEnabled, sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
@@ -1,5 +1,4 @@
1
- import { sanitizeText } from "@oh-my-pi/pi-natives";
2
- import { getIndentation } from "@oh-my-pi/pi-utils";
1
+ import { getIndentation, sanitizeText } from "@oh-my-pi/pi-utils";
3
2
  import * as Diff from "diff";
4
3
  import { getLanguageFromPath, highlightCode, theme } from "../../modes/theme/theme";
5
4
  import { type CodeFrameMarker, formatCodeFrameLine, replaceTabs } from "../../tools/render-utils";
@@ -3,8 +3,8 @@
3
3
  * Shares the same kernel session as the agent's eval tool.
4
4
  */
5
5
 
6
- import { sanitizeText } from "@oh-my-pi/pi-natives";
7
6
  import { Container, type Loader, Text, type TUI } from "@oh-my-pi/pi-tui";
7
+ import { sanitizeText } from "@oh-my-pi/pi-utils";
8
8
  import { highlightCode, theme } from "../../modes/theme/theme";
9
9
  import type { TruncationMeta } from "../../tools/output-meta";
10
10
  import {
@@ -5,6 +5,8 @@ import { theme } from "../../modes/theme/theme";
5
5
  import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
6
6
  import type { AuthStorage } from "../../session/auth-storage";
7
7
  import { DynamicBorder } from "./dynamic-border";
8
+
9
+ const OAUTH_SELECTOR_MAX_VISIBLE = 10;
8
10
  /**
9
11
  * Component that renders an OAuth provider selector.
10
12
  */
@@ -144,7 +146,16 @@ export class OAuthSelectorComponent extends Container {
144
146
  }
145
147
  #updateList(): void {
146
148
  this.#listContainer.clear();
147
- for (let i = 0; i < this.#allProviders.length; i++) {
149
+
150
+ const total = this.#allProviders.length;
151
+ const maxVisible = OAUTH_SELECTOR_MAX_VISIBLE;
152
+ const startIndex =
153
+ total <= maxVisible
154
+ ? 0
155
+ : Math.max(0, Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
156
+ const endIndex = Math.min(startIndex + maxVisible, total);
157
+
158
+ for (let i = startIndex; i < endIndex; i++) {
148
159
  const provider = this.#allProviders[i];
149
160
  if (!provider) continue;
150
161
  const isSelected = i === this.#selectedIndex;
@@ -163,8 +174,14 @@ export class OAuthSelectorComponent extends Container {
163
174
  this.#listContainer.addChild(new TruncatedText(line, 0, 0));
164
175
  }
165
176
 
177
+ // Scroll indicator when list is windowed
178
+ if (startIndex > 0 || endIndex < total) {
179
+ const scrollInfo = theme.fg("muted", ` (${this.#selectedIndex + 1}/${total})`);
180
+ this.#listContainer.addChild(new TruncatedText(scrollInfo, 0, 0));
181
+ }
182
+
166
183
  // Show "no providers" if empty
167
- if (this.#allProviders.length === 0) {
184
+ if (total === 0) {
168
185
  const message =
169
186
  this.#mode === "login" ? "No OAuth providers available" : "No OAuth providers logged in. Use /login first.";
170
187
  this.#listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${message}`), 0, 0));
@@ -191,6 +208,25 @@ export class OAuthSelectorComponent extends Container {
191
208
  this.#statusMessage = undefined;
192
209
  this.#updateList();
193
210
  }
211
+ // Page up - jump up by one visible page
212
+ else if (matchesKey(keyData, "pageUp")) {
213
+ if (this.#allProviders.length > 0) {
214
+ this.#selectedIndex = Math.max(0, this.#selectedIndex - OAUTH_SELECTOR_MAX_VISIBLE);
215
+ }
216
+ this.#statusMessage = undefined;
217
+ this.#updateList();
218
+ }
219
+ // Page down - jump down by one visible page
220
+ else if (matchesKey(keyData, "pageDown")) {
221
+ if (this.#allProviders.length > 0) {
222
+ this.#selectedIndex = Math.min(
223
+ this.#allProviders.length - 1,
224
+ this.#selectedIndex + OAUTH_SELECTOR_MAX_VISIBLE,
225
+ );
226
+ }
227
+ this.#statusMessage = undefined;
228
+ this.#updateList();
229
+ }
194
230
  // Enter
195
231
  else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
196
232
  const selectedProvider = this.#allProviders[this.#selectedIndex];
@@ -1,5 +1,4 @@
1
1
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
- import { sanitizeText } from "@oh-my-pi/pi-natives";
3
2
  import {
4
3
  Box,
5
4
  type Component,
@@ -13,7 +12,7 @@ import {
13
12
  Text,
14
13
  type TUI,
15
14
  } from "@oh-my-pi/pi-tui";
16
- import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
15
+ import { getProjectDir, logger, sanitizeText } from "@oh-my-pi/pi-utils";
17
16
  import { EDIT_MODE_STRATEGIES, type EditMode, type PerFileDiffPreview } from "../../edit";
18
17
  import type { Theme } from "../../modes/theme/theme";
19
18
  import { theme } from "../../modes/theme/theme";
@@ -453,6 +453,16 @@ class TreeList implements Component {
453
453
  );
454
454
  const endIndex = Math.min(startIndex + this.maxVisibleLines, this.#filteredNodes.length);
455
455
 
456
+ // Cap the per-row gutter prefix so a content budget is always preserved.
457
+ // Each indent level renders as 3 cells; deep branching would otherwise eat the
458
+ // entire viewport (issue #1144). Reserve at least MIN_CONTENT_COLS for entry
459
+ // text — or half the viewport, whichever is larger — and compress older gutter
460
+ // levels off-screen behind a leading ellipsis when the row would exceed budget.
461
+ const MIN_CONTENT_COLS = 24;
462
+ const OVERHEAD_COLS = 4; // cursor (2) + a touch of breathing room
463
+ const contentReserve = Math.max(MIN_CONTENT_COLS, Math.floor(width / 2));
464
+ const maxIndentLevels = Math.max(1, Math.floor((width - contentReserve - OVERHEAD_COLS) / 3));
465
+
456
466
  for (let i = startIndex; i < endIndex; i++) {
457
467
  const flatNode = this.#filteredNodes[i];
458
468
  const entry = flatNode.node.entry;
@@ -464,29 +474,34 @@ class TreeList implements Component {
464
474
  // If multiple roots, shift display (roots at 0, not 1)
465
475
  const displayIndent = this.#multipleRoots ? Math.max(0, flatNode.indent - 1) : flatNode.indent;
466
476
 
467
- // Build prefix with gutters at their correct positions
468
- // Each gutter has a position (displayIndent where its connector was shown)
477
+ // Build prefix with gutters at their correct positions, clamped to
478
+ // `maxIndentLevels` cells so the content always fits. When clamped, the
479
+ // leftmost cells represent the deepest visible ancestors and a `…` marker
480
+ // indicates older branch context has been compressed.
469
481
  const hasConnector = flatNode.showConnector && !flatNode.isVirtualRootChild;
470
482
  const connectorSymbol = hasConnector ? (flatNode.isLast ? theme.tree.last : theme.tree.branch) : "";
471
483
  const connectorChars = hasConnector ? Array.from(connectorSymbol) : [];
472
- const connectorPosition = hasConnector ? displayIndent - 1 : -1;
484
+ const renderedIndent = Math.min(displayIndent, maxIndentLevels);
485
+ const scrollOffset = displayIndent - renderedIndent;
486
+ const connectorPositionDisplay = hasConnector ? renderedIndent - 1 : -1;
473
487
 
474
488
  // Build prefix char by char, placing gutters and connector at their positions
475
- const totalChars = displayIndent * 3;
489
+ const totalChars = renderedIndent * 3;
476
490
  const prefixChars: string[] = [];
477
491
  for (let i = 0; i < totalChars; i++) {
478
492
  const level = Math.floor(i / 3);
493
+ const originalLevel = level + scrollOffset;
479
494
  const posInLevel = i % 3;
480
495
 
481
- // Check if there's a gutter at this level
482
- const gutter = flatNode.gutters.find(g => g.position === level);
496
+ // Check if there's a gutter at this level (translated to original tree depth)
497
+ const gutter = flatNode.gutters.find(g => g.position === originalLevel);
483
498
  if (gutter) {
484
499
  if (posInLevel === 0) {
485
500
  prefixChars.push(gutter.show ? theme.tree.vertical : " ");
486
501
  } else {
487
502
  prefixChars.push(" ");
488
503
  }
489
- } else if (hasConnector && level === connectorPosition) {
504
+ } else if (hasConnector && level === connectorPositionDisplay) {
490
505
  // Connector at this level
491
506
  if (posInLevel === 0) {
492
507
  prefixChars.push(connectorChars[0] ?? " ");
@@ -499,6 +514,10 @@ class TreeList implements Component {
499
514
  prefixChars.push(" ");
500
515
  }
501
516
  }
517
+ // Mark the leftmost cell when ancestors were compressed off-screen.
518
+ if (scrollOffset > 0 && prefixChars.length > 0) {
519
+ prefixChars[0] = "…";
520
+ }
502
521
  const prefix = prefixChars.join("");
503
522
 
504
523
  // Active path marker - shown right before the entry text