@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.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 (217) hide show
  1. package/CHANGELOG.md +103 -2
  2. package/dist/cli.js +5790 -5731
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/cli-commands.d.ts +12 -0
  7. package/dist/types/commands/launch.d.ts +4 -0
  8. package/dist/types/config/api-key-resolver.d.ts +3 -0
  9. package/dist/types/config/keybindings.d.ts +6 -1
  10. package/dist/types/config/model-registry.d.ts +1 -0
  11. package/dist/types/config/model-resolver.d.ts +18 -0
  12. package/dist/types/config/settings-schema.d.ts +85 -34
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  15. package/dist/types/eval/py/executor.d.ts +5 -0
  16. package/dist/types/eval/py/kernel.d.ts +6 -1
  17. package/dist/types/eval/py/runtime.d.ts +9 -0
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/export/html/template.generated.d.ts +1 -1
  20. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  22. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  23. package/dist/types/extensibility/shared-events.d.ts +2 -2
  24. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  25. package/dist/types/internal-urls/index.d.ts +1 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/irc/bus.d.ts +66 -0
  28. package/dist/types/memory-backend/index.d.ts +1 -0
  29. package/dist/types/memory-backend/runtime.d.ts +4 -0
  30. package/dist/types/memory-backend/types.d.ts +66 -1
  31. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  32. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  34. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  35. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  36. package/dist/types/modes/components/welcome.d.ts +3 -9
  37. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  38. package/dist/types/modes/index.d.ts +3 -3
  39. package/dist/types/modes/interactive-mode.d.ts +10 -4
  40. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  41. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  42. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  43. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  44. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  45. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  46. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  47. package/dist/types/modes/theme/theme.d.ts +2 -1
  48. package/dist/types/modes/types.d.ts +5 -2
  49. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  50. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  51. package/dist/types/registry/agent-registry.d.ts +16 -5
  52. package/dist/types/secrets/index.d.ts +1 -1
  53. package/dist/types/secrets/obfuscator.d.ts +8 -2
  54. package/dist/types/session/agent-session.d.ts +49 -32
  55. package/dist/types/session/messages.d.ts +2 -4
  56. package/dist/types/session/session-history-format.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +21 -3
  58. package/dist/types/session/streaming-output.d.ts +46 -0
  59. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  60. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  61. package/dist/types/slash-commands/types.d.ts +1 -1
  62. package/dist/types/system-prompt.d.ts +2 -0
  63. package/dist/types/task/executor.d.ts +12 -2
  64. package/dist/types/task/index.d.ts +13 -6
  65. package/dist/types/task/output-manager.d.ts +0 -7
  66. package/dist/types/task/repair-args.d.ts +8 -7
  67. package/dist/types/task/types.d.ts +63 -51
  68. package/dist/types/thinking.d.ts +4 -0
  69. package/dist/types/tiny/title-client.d.ts +11 -0
  70. package/dist/types/tiny/title-protocol.d.ts +1 -0
  71. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  72. package/dist/types/tools/find.d.ts +0 -11
  73. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  74. package/dist/types/tools/index.d.ts +7 -3
  75. package/dist/types/tools/irc.d.ts +76 -38
  76. package/dist/types/tools/job.d.ts +7 -1
  77. package/dist/types/utils/git.d.ts +15 -2
  78. package/dist/types/utils/title-generator.d.ts +3 -2
  79. package/examples/extensions/with-deps/package.json +1 -0
  80. package/package.json +11 -10
  81. package/scripts/bundle-dist.ts +28 -19
  82. package/src/async/index.ts +0 -1
  83. package/src/auto-thinking/classifier.ts +1 -0
  84. package/src/cli/args.ts +3 -0
  85. package/src/cli/gallery-cli.ts +1 -1
  86. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  87. package/src/cli/gallery-fixtures/types.ts +5 -0
  88. package/src/cli-commands.ts +29 -0
  89. package/src/cli.ts +28 -15
  90. package/src/commands/launch.ts +4 -0
  91. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  92. package/src/commit/model-selection.ts +3 -2
  93. package/src/config/api-key-resolver.ts +8 -6
  94. package/src/config/keybindings.ts +6 -1
  95. package/src/config/model-registry.ts +97 -30
  96. package/src/config/model-resolver.ts +60 -0
  97. package/src/config/settings-schema.ts +99 -55
  98. package/src/config/settings.ts +68 -3
  99. package/src/edit/hashline/execute.ts +39 -2
  100. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  101. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  102. package/src/eval/agent-bridge.ts +3 -16
  103. package/src/eval/completion-bridge.ts +1 -0
  104. package/src/eval/js/shared/prelude.txt +1 -1
  105. package/src/eval/py/executor.ts +29 -7
  106. package/src/eval/py/index.ts +6 -1
  107. package/src/eval/py/kernel.ts +31 -11
  108. package/src/eval/py/prelude.py +5 -6
  109. package/src/eval/py/runtime.ts +37 -0
  110. package/src/exec/bash-executor.ts +82 -3
  111. package/src/export/html/template.generated.ts +1 -1
  112. package/src/export/html/template.js +38 -13
  113. package/src/extensibility/custom-tools/types.ts +2 -2
  114. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  115. package/src/extensibility/extensions/runner.ts +6 -1
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/shared-events.ts +2 -2
  118. package/src/hindsight/bank.ts +17 -2
  119. package/src/internal-urls/docs-index.generated.ts +11 -11
  120. package/src/internal-urls/history-protocol.ts +113 -0
  121. package/src/internal-urls/index.ts +1 -0
  122. package/src/internal-urls/router.ts +3 -1
  123. package/src/internal-urls/types.ts +1 -1
  124. package/src/irc/bus.ts +292 -0
  125. package/src/main.ts +26 -66
  126. package/src/memories/index.ts +2 -0
  127. package/src/memory-backend/index.ts +1 -0
  128. package/src/memory-backend/local-backend.ts +9 -0
  129. package/src/memory-backend/off-backend.ts +9 -0
  130. package/src/memory-backend/runtime.ts +66 -0
  131. package/src/memory-backend/types.ts +81 -1
  132. package/src/mnemopi/backend.ts +151 -4
  133. package/src/modes/acp/acp-agent.ts +119 -11
  134. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  135. package/src/modes/components/assistant-message.ts +19 -21
  136. package/src/modes/components/compaction-summary-message.ts +68 -32
  137. package/src/modes/components/custom-editor.ts +10 -0
  138. package/src/modes/components/footer.ts +3 -1
  139. package/src/modes/components/status-line/component.ts +118 -34
  140. package/src/modes/components/tool-execution.ts +31 -1
  141. package/src/modes/components/ttsr-notification.ts +72 -30
  142. package/src/modes/components/welcome.ts +9 -33
  143. package/src/modes/controllers/command-controller.ts +1 -1
  144. package/src/modes/controllers/event-controller.ts +65 -0
  145. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  146. package/src/modes/controllers/input-controller.ts +19 -2
  147. package/src/modes/controllers/mcp-command-controller.ts +38 -3
  148. package/src/modes/controllers/selector-controller.ts +21 -17
  149. package/src/modes/index.ts +3 -21
  150. package/src/modes/interactive-mode.ts +47 -22
  151. package/src/modes/oauth-manual-input.ts +30 -3
  152. package/src/modes/rpc/rpc-client.ts +154 -3
  153. package/src/modes/rpc/rpc-mode.ts +97 -12
  154. package/src/modes/rpc/rpc-subagents.ts +265 -0
  155. package/src/modes/rpc/rpc-types.ts +81 -1
  156. package/src/modes/setup-wizard/index.ts +12 -2
  157. package/src/modes/setup-wizard/lazy.ts +16 -0
  158. package/src/modes/theme/theme.ts +18 -5
  159. package/src/modes/types.ts +5 -5
  160. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  161. package/src/modes/utils/ui-helpers.ts +51 -49
  162. package/src/prompts/system/irc-incoming.md +3 -4
  163. package/src/prompts/system/orchestrate-notice.md +2 -2
  164. package/src/prompts/system/subagent-system-prompt.md +0 -5
  165. package/src/prompts/system/system-prompt.md +1 -0
  166. package/src/prompts/system/workflow-notice.md +2 -2
  167. package/src/prompts/tools/eval.md +3 -3
  168. package/src/prompts/tools/irc.md +29 -19
  169. package/src/prompts/tools/read.md +2 -2
  170. package/src/prompts/tools/task-summary.md +5 -16
  171. package/src/prompts/tools/task.md +38 -29
  172. package/src/registry/agent-lifecycle.ts +218 -0
  173. package/src/registry/agent-registry.ts +16 -5
  174. package/src/sdk.ts +37 -10
  175. package/src/secrets/index.ts +8 -1
  176. package/src/secrets/obfuscator.ts +39 -18
  177. package/src/session/agent-session.ts +422 -291
  178. package/src/session/messages.ts +11 -78
  179. package/src/session/session-history-format.ts +246 -0
  180. package/src/session/session-manager.ts +59 -5
  181. package/src/session/streaming-output.ts +226 -10
  182. package/src/slash-commands/acp-builtins.ts +24 -0
  183. package/src/slash-commands/builtin-registry.ts +20 -0
  184. package/src/slash-commands/types.ts +1 -1
  185. package/src/system-prompt.ts +14 -0
  186. package/src/task/executor.ts +851 -461
  187. package/src/task/index.ts +721 -796
  188. package/src/task/output-manager.ts +0 -11
  189. package/src/task/render.ts +148 -63
  190. package/src/task/repair-args.ts +21 -9
  191. package/src/task/types.ts +82 -66
  192. package/src/thinking.ts +7 -0
  193. package/src/tiny/title-client.ts +34 -5
  194. package/src/tiny/title-protocol.ts +1 -1
  195. package/src/tiny/worker.ts +6 -4
  196. package/src/tools/ask.ts +4 -2
  197. package/src/tools/bash.ts +61 -10
  198. package/src/tools/browser/tab-worker.ts +26 -7
  199. package/src/tools/browser.ts +28 -1
  200. package/src/tools/find.ts +2 -27
  201. package/src/tools/grouped-file-output.ts +1 -118
  202. package/src/tools/image-gen.ts +11 -4
  203. package/src/tools/index.ts +17 -13
  204. package/src/tools/inspect-image.ts +1 -0
  205. package/src/tools/irc.ts +596 -171
  206. package/src/tools/job.ts +41 -7
  207. package/src/tools/read.ts +57 -1
  208. package/src/tools/renderers.ts +2 -0
  209. package/src/tools/resolve.ts +4 -1
  210. package/src/utils/commit-message-generator.ts +1 -0
  211. package/src/utils/git.ts +267 -13
  212. package/src/utils/title-generator.ts +24 -5
  213. package/dist/types/async/support.d.ts +0 -2
  214. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  215. package/dist/types/task/simple-mode.d.ts +0 -8
  216. package/src/async/support.ts +0 -5
  217. package/src/task/simple-mode.ts +0 -27
@@ -8,8 +8,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
8
8
  import {
9
9
  type BranchSummaryMessage,
10
10
  type CompactionSummaryMessage,
11
- renderBranchSummaryContext,
12
- renderCompactionSummaryContext,
11
+ convertMessageToLlm,
13
12
  } from "@oh-my-pi/pi-agent-core/compaction/messages";
14
13
  import type {
15
14
  AssistantMessage,
@@ -17,7 +16,6 @@ import type {
17
16
  Message,
18
17
  MessageAttribution,
19
18
  TextContent,
20
- ToolResultMessage,
21
19
  UserMessage,
22
20
  } from "@oh-my-pi/pi-ai";
23
21
  import { prompt } from "@oh-my-pi/pi-utils";
@@ -28,6 +26,7 @@ export {
28
26
  type CompactionSummaryMessage,
29
27
  createBranchSummaryMessage,
30
28
  createCompactionSummaryMessage,
29
+ createCustomMessage,
31
30
  } from "@oh-my-pi/pi-agent-core/compaction/messages";
32
31
 
33
32
  import type { OutputMeta } from "../tools/output-meta";
@@ -59,7 +58,7 @@ export interface SkillPromptDetails {
59
58
  *
60
59
  * Consumers: `AgentSession.#handleAgentEvent` (stamper) writes this value;
61
60
  * `EventController.#handleMessageEnd`, `AssistantMessageComponent`,
62
- * `ui-helpers.addMessageToChat` (renderers), `SessionObserverOverlay
61
+ * `ui-helpers.addMessageToChat` (renderers), `AgentHubOverlayComponent
63
62
  * #buildTranscriptLines`, `runPrintMode`, and `AcpAgent#replayAssistantMessage`
64
63
  * (fallback error emission) read it via `isSilentAbort`. */
65
64
  export const SILENT_ABORT_MARKER = "__omp.silent_abort__";
@@ -220,15 +219,6 @@ export function wrapSteeringForModel(messages: AgentMessage[]): AgentMessage[] {
220
219
  return wrappedMessages ?? messages;
221
220
  }
222
221
 
223
- function getPrunedToolResultContent(message: ToolResultMessage): (TextContent | ImageContent)[] {
224
- if (message.prunedAt === undefined) {
225
- return message.content;
226
- }
227
- const textBlocks = message.content.filter((content): content is TextContent => content.type === "text");
228
- const text = textBlocks.map(block => block.text).join("") || "[Output truncated]";
229
- return [{ type: "text", text }];
230
- }
231
-
232
222
  /** Result of filtering image blocks out of a `(TextContent | ImageContent)[]` array. */
233
223
  interface StripContentResult {
234
224
  content: (TextContent | ImageContent)[];
@@ -478,26 +468,6 @@ export function sanitizeRehydratedOpenAIResponsesAssistantMessage(message: Assis
478
468
  };
479
469
  }
480
470
 
481
- /** Convert CustomMessageEntry to AgentMessage format */
482
- export function createCustomMessage(
483
- customType: string,
484
- content: string | (TextContent | ImageContent)[],
485
- display: boolean,
486
- details: unknown | undefined,
487
- timestamp: string,
488
- attribution?: MessageAttribution,
489
- ): CustomMessage {
490
- return {
491
- role: "custom",
492
- customType,
493
- content,
494
- display,
495
- details,
496
- attribution,
497
- timestamp: new Date(timestamp).getTime(),
498
- };
499
- }
500
-
501
471
  /**
502
472
  * Transform AgentMessages (including custom types) to LLM-compatible Messages.
503
473
  *
@@ -530,43 +500,6 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
530
500
  attribution: "user",
531
501
  timestamp: m.timestamp,
532
502
  };
533
- case "custom":
534
- case "hookMessage": {
535
- const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content;
536
- const role = "developer";
537
- const attribution = m.attribution;
538
- return {
539
- role,
540
- content,
541
- attribution,
542
- timestamp: m.timestamp,
543
- };
544
- }
545
- case "branchSummary":
546
- return {
547
- role: "user",
548
- content: [
549
- {
550
- type: "text" as const,
551
- text: renderBranchSummaryContext(m.summary),
552
- },
553
- ],
554
- attribution: "agent",
555
- timestamp: m.timestamp,
556
- };
557
- case "compactionSummary":
558
- return {
559
- role: "user",
560
- content: [
561
- {
562
- type: "text" as const,
563
- text: renderCompactionSummaryContext(m.summary),
564
- },
565
- ],
566
- attribution: "agent",
567
- providerPayload: m.providerPayload,
568
- timestamp: m.timestamp,
569
- };
570
503
  case "fileMention": {
571
504
  const fileContents = m.files
572
505
  .map(file => {
@@ -587,18 +520,18 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
587
520
  timestamp: m.timestamp,
588
521
  };
589
522
  }
523
+ case "custom":
524
+ case "hookMessage":
525
+ case "branchSummary":
526
+ case "compactionSummary":
590
527
  case "user":
591
- return { ...m, attribution: m.attribution ?? "user" };
592
528
  case "developer":
593
- return { ...m, attribution: m.attribution ?? "agent" };
594
529
  case "assistant":
595
- return m;
596
530
  case "toolResult":
597
- return {
598
- ...m,
599
- content: getPrunedToolResultContent(m as ToolResultMessage),
600
- attribution: m.attribution ?? "agent",
601
- };
531
+ // Core roles share one transformer with agent-core —
532
+ // duplicating them here is how snapcompact frames once
533
+ // silently fell off the provider request.
534
+ return convertMessageToLlm(m);
602
535
  default:
603
536
  m satisfies never;
604
537
  return undefined;
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Concise markdown transcript serializer for `history://` URLs.
3
+ *
4
+ * Unlike `session-dump-format.ts` (verbose `/dump` export), this emits a
5
+ * compressed transcript: full user/assistant/developer text, tool call +
6
+ * result pairs collapsed to single lines, thinking elided, custom messages
7
+ * as one-liners. No system prompt, no tool catalog, no config sections.
8
+ */
9
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
10
+ import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
11
+ import type { AssistantMessage, ImageContent, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
12
+ import type {
13
+ BashExecutionMessage,
14
+ BranchSummaryMessage,
15
+ CompactionSummaryMessage,
16
+ CustomMessage,
17
+ FileMentionMessage,
18
+ HookMessage,
19
+ PythonExecutionMessage,
20
+ } from "./messages";
21
+
22
+ export interface HistoryFormatOptions {
23
+ /** Optional H1 prepended to the transcript. */
24
+ title?: string;
25
+ }
26
+
27
+ /** Max length of the primary-arg summary inside `→ tool(...)` lines. */
28
+ const PRIMARY_ARG_MAX = 120;
29
+
30
+ /** Per-tool preference order for the most informative scalar argument. */
31
+ const PRIMARY_ARG_KEYS = [
32
+ "path",
33
+ "file_path",
34
+ "filePath",
35
+ "command",
36
+ "cmd",
37
+ "pattern",
38
+ "url",
39
+ "query",
40
+ "prompt",
41
+ "assignment",
42
+ "message",
43
+ "op",
44
+ "name",
45
+ "id",
46
+ ] as const;
47
+
48
+ /** Collapse whitespace runs and truncate to `max` chars with an ellipsis. */
49
+ function oneLine(text: string, max = PRIMARY_ARG_MAX): string {
50
+ const flat = text.replace(/\s+/g, " ").trim();
51
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
52
+ }
53
+
54
+ /** Join the text blocks of a string-or-blocks content field. Images become `[image]`. */
55
+ function contentToText(content: string | readonly (TextContent | ImageContent)[]): string {
56
+ if (typeof content === "string") return content;
57
+ const parts: string[] = [];
58
+ for (const block of content) {
59
+ if (block.type === "text") parts.push(block.text);
60
+ else parts.push("[image]");
61
+ }
62
+ return parts.join("\n");
63
+ }
64
+
65
+ function lineCount(text: string): number {
66
+ if (!text) return 0;
67
+ return text.split("\n").length;
68
+ }
69
+
70
+ /** Pick the most informative scalar argument of a tool call. */
71
+ function primaryArg(args: Record<string, unknown> | undefined): string {
72
+ if (!args || typeof args !== "object") return "";
73
+ for (const key of PRIMARY_ARG_KEYS) {
74
+ const value = args[key];
75
+ if (typeof value === "string" && value.length > 0) return oneLine(value);
76
+ if (Array.isArray(value) && value.length > 0 && value.every(v => typeof v === "string")) {
77
+ return oneLine(value.join(", "));
78
+ }
79
+ }
80
+ // Fallback: first non-intent string arg, then a compact JSON of the args.
81
+ const rest: Record<string, unknown> = {};
82
+ let restCount = 0;
83
+ for (const key in args) {
84
+ if (key === INTENT_FIELD) continue;
85
+ const value = args[key];
86
+ if (typeof value === "string" && value.length > 0) return oneLine(value);
87
+ rest[key] = value;
88
+ restCount++;
89
+ }
90
+ if (restCount === 0) return "";
91
+ try {
92
+ return oneLine(JSON.stringify(rest));
93
+ } catch {
94
+ return "";
95
+ }
96
+ }
97
+
98
+ /** One line per tool call: `→ read(src/foo.ts:50-80) ⇒ ok · 31 lines`. */
99
+ function toolCallLine(
100
+ name: string,
101
+ args: Record<string, unknown> | undefined,
102
+ result: ToolResultMessage | undefined,
103
+ ): string {
104
+ const head = `→ ${name}(${primaryArg(args)})`;
105
+ if (!result) return `${head} ⇒ pending`;
106
+ const text = contentToText(result.content);
107
+ const lines = lineCount(text);
108
+ const count = `${lines} ${lines === 1 ? "line" : "lines"}`;
109
+ if (result.isError) {
110
+ const firstLine = oneLine(text.split("\n", 1)[0] ?? "");
111
+ return firstLine ? `${head} ⇒ error · ${count} — ${firstLine}` : `${head} ⇒ error · ${count}`;
112
+ }
113
+ return `${head} ⇒ ok · ${count}`;
114
+ }
115
+
116
+ /** One line for a user-initiated `!`/`$` execution. */
117
+ function executionLine(
118
+ kind: "bash" | "python",
119
+ source: string,
120
+ msg: BashExecutionMessage | PythonExecutionMessage,
121
+ ): string {
122
+ const status = msg.cancelled
123
+ ? "cancelled"
124
+ : msg.exitCode !== undefined && msg.exitCode !== 0
125
+ ? `error · exit ${msg.exitCode}`
126
+ : "ok";
127
+ const lines = lineCount(msg.output);
128
+ return `→ ${kind}! ${oneLine(source)} ⇒ ${status} · ${lines} ${lines === 1 ? "line" : "lines"}`;
129
+ }
130
+
131
+ /** One-liner for custom/hook messages: `[irc] A → B: body…`. */
132
+ function customOneLiner(msg: CustomMessage | HookMessage): string {
133
+ const details = (msg.details ?? {}) as Record<string, unknown>;
134
+ const str = (key: string): string => (typeof details[key] === "string" ? (details[key] as string) : "");
135
+ switch (msg.customType) {
136
+ case "irc:incoming":
137
+ return `[irc] ${str("from") || "?"} → me: ${oneLine(str("message"))}`;
138
+ case "irc:relay":
139
+ return `[irc] ${str("from") || "?"} → ${str("to") || "?"}: ${oneLine(str("body"))}`;
140
+ case "async-result": {
141
+ const jobs = Array.isArray(details.jobs) && details.jobs.length > 0 ? details.jobs : [details];
142
+ const labels = jobs
143
+ .map(job => {
144
+ const j = (job ?? {}) as Record<string, unknown>;
145
+ return typeof j.label === "string" && j.label ? j.label : typeof j.jobId === "string" ? j.jobId : "job";
146
+ })
147
+ .join(", ");
148
+ return `[async-result] ${oneLine(labels)}`;
149
+ }
150
+ default:
151
+ return `[${msg.customType}] ${oneLine(contentToText(msg.content))}`;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Format a session's message array as a concise markdown transcript.
157
+ *
158
+ * `messages` is the session's in-memory message array (or the read-only
159
+ * equivalent loaded from a session file) — the same shapes
160
+ * `session-dump-format.ts` consumes.
161
+ */
162
+ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: HistoryFormatOptions): string {
163
+ const typed = messages as AgentMessage[];
164
+ const lines: string[] = [];
165
+ if (opts?.title) {
166
+ lines.push(`# ${opts.title}`, "");
167
+ }
168
+
169
+ // Index tool results by call id so each toolCall collapses to one line.
170
+ const resultsByCallId = new Map<string, ToolResultMessage>();
171
+ for (const msg of typed) {
172
+ if (msg.role === "toolResult") {
173
+ resultsByCallId.set(msg.toolCallId, msg);
174
+ }
175
+ }
176
+ const consumed = new Set<string>();
177
+
178
+ for (const msg of typed) {
179
+ switch (msg.role) {
180
+ case "user":
181
+ case "developer": {
182
+ const text = contentToText(msg.content);
183
+ if (!text.trim()) break;
184
+ lines.push(`## ${msg.role}`, "", text, "");
185
+ break;
186
+ }
187
+ case "assistant": {
188
+ const assistantMsg = msg as AssistantMessage;
189
+ const body: string[] = [];
190
+ for (const block of assistantMsg.content) {
191
+ if (block.type === "text") {
192
+ if (block.text.trim()) body.push(block.text);
193
+ } else if (block.type === "toolCall") {
194
+ const result = resultsByCallId.get(block.id);
195
+ if (result) consumed.add(block.id);
196
+ body.push(toolCallLine(block.name, block.arguments, result));
197
+ }
198
+ // thinking / redactedThinking elided entirely
199
+ }
200
+ if (body.length === 0) break;
201
+ lines.push("## assistant", "", ...body, "");
202
+ break;
203
+ }
204
+ case "toolResult": {
205
+ // Normally consumed by its toolCall; orphans (e.g. truncated history) get their own line.
206
+ if (consumed.has(msg.toolCallId)) break;
207
+ lines.push(toolCallLine(msg.toolName, undefined, msg), "");
208
+ break;
209
+ }
210
+ case "bashExecution": {
211
+ const bashMsg = msg as BashExecutionMessage;
212
+ if (bashMsg.excludeFromContext) break;
213
+ lines.push(executionLine("bash", bashMsg.command, bashMsg), "");
214
+ break;
215
+ }
216
+ case "pythonExecution": {
217
+ const pythonMsg = msg as PythonExecutionMessage;
218
+ if (pythonMsg.excludeFromContext) break;
219
+ lines.push(executionLine("python", pythonMsg.code, pythonMsg), "");
220
+ break;
221
+ }
222
+ case "custom":
223
+ case "hookMessage": {
224
+ lines.push(customOneLiner(msg as CustomMessage | HookMessage), "");
225
+ break;
226
+ }
227
+ case "branchSummary": {
228
+ const branchMsg = msg as BranchSummaryMessage;
229
+ lines.push(`[branch] from ${branchMsg.fromId}: ${oneLine(branchMsg.summary)}`, "");
230
+ break;
231
+ }
232
+ case "compactionSummary": {
233
+ const compactMsg = msg as CompactionSummaryMessage;
234
+ lines.push(`[compaction] ${oneLine(compactMsg.summary)}`, "");
235
+ break;
236
+ }
237
+ case "fileMention": {
238
+ const fileMsg = msg as FileMentionMessage;
239
+ lines.push(`[file-mention] ${oneLine(fileMsg.files.map(f => f.path).join(", "))}`, "");
240
+ break;
241
+ }
242
+ }
243
+ }
244
+
245
+ return `${lines.join("\n").trim()}\n`;
246
+ }
@@ -27,6 +27,7 @@ import {
27
27
  Snowflake,
28
28
  toError,
29
29
  } from "@oh-my-pi/pi-utils";
30
+ import { getPreservedSnapcompactArchive, snapcompactImages } from "@oh-my-pi/snapcompact";
30
31
  import { ArtifactManager } from "./artifacts";
31
32
  import {
32
33
  type BlobPutOptions,
@@ -544,6 +545,17 @@ export function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEnt
544
545
  return null;
545
546
  }
546
547
 
548
+ export interface BuildSessionContextOptions {
549
+ /**
550
+ * Build the full-history display transcript instead of the LLM context:
551
+ * every path entry in chronological order, with each compaction emitted
552
+ * inline as a `compactionSummary` message at the position it fired rather
553
+ * than replacing the history before it. Display-only — never send the
554
+ * result to a provider.
555
+ */
556
+ transcript?: boolean;
557
+ }
558
+
547
559
  /**
548
560
  * Build the session context from entries using tree traversal.
549
561
  * If leafId is provided, walks from that entry to root.
@@ -553,6 +565,7 @@ export function buildSessionContext(
553
565
  entries: SessionEntry[],
554
566
  leafId?: string | null,
555
567
  byId?: Map<string, SessionEntry>,
568
+ options?: BuildSessionContextOptions,
556
569
  ): SessionContext {
557
570
  // Build uuid index if not available
558
571
  if (!byId) {
@@ -692,7 +705,29 @@ export function buildSessionContext(
692
705
  }
693
706
  };
694
707
 
695
- if (compaction) {
708
+ if (options?.transcript) {
709
+ // Display transcript: every entry in chronological order. Compactions do
710
+ // not erase prior history here — each renders inline (as a divider in the
711
+ // TUI) at the point it fired, with any snapcompact frames re-attached so
712
+ // the component can report them.
713
+ for (const entry of path) {
714
+ if (entry.type === "compaction") {
715
+ const snapcompactArchive = getPreservedSnapcompactArchive(entry.preserveData);
716
+ messages.push(
717
+ createCompactionSummaryMessage(
718
+ entry.summary,
719
+ entry.tokensBefore,
720
+ entry.timestamp,
721
+ entry.shortSummary,
722
+ undefined,
723
+ snapcompactArchive ? snapcompactImages(snapcompactArchive) : undefined,
724
+ ),
725
+ );
726
+ } else {
727
+ appendMessage(entry);
728
+ }
729
+ }
730
+ } else if (compaction) {
696
731
  const providerPayload: ProviderPayload | undefined = (() => {
697
732
  const candidate = compaction.preserveData?.openaiRemoteCompaction;
698
733
  if (!candidate || typeof candidate !== "object") return undefined;
@@ -707,7 +742,9 @@ export function buildSessionContext(
707
742
  })();
708
743
  const remoteReplacementHistory = providerPayload?.items;
709
744
 
710
- // Emit summary first
745
+ // Emit summary first; re-attach any archived snapcompact frames so the
746
+ // model can keep reading the archived history after every context rebuild.
747
+ const snapcompactArchive = getPreservedSnapcompactArchive(compaction.preserveData);
711
748
  messages.push(
712
749
  createCompactionSummaryMessage(
713
750
  compaction.summary,
@@ -715,6 +752,7 @@ export function buildSessionContext(
715
752
  compaction.timestamp,
716
753
  compaction.shortSummary,
717
754
  providerPayload,
755
+ snapcompactArchive ? snapcompactImages(snapcompactArchive) : undefined,
718
756
  ),
719
757
  );
720
758
 
@@ -957,6 +995,21 @@ async function resolveBlobRefsInEntries(entries: FileEntry[], blobStore: BlobSto
957
995
  await Promise.all(promises);
958
996
  }
959
997
 
998
+ /**
999
+ * Read-only message view of a session file: load entries, migrate to the
1000
+ * current version, resolve blob refs, and build the context along the
1001
+ * persisted leaf path (last entry). Does NOT create a writer or take the
1002
+ * session lock — safe to call against a file another session is writing.
1003
+ */
1004
+ export async function loadSessionMessagesReadOnly(filePath: string): Promise<AgentMessage[]> {
1005
+ const entries = await loadEntriesFromFile(filePath);
1006
+ if (entries.length === 0) return [];
1007
+ migrateToCurrentVersion(entries);
1008
+ await resolveBlobRefsInEntries(entries, new BlobStore(getBlobsDir()));
1009
+ const sessionEntries = entries.filter((e): e is SessionEntry => e.type !== "session");
1010
+ return buildSessionContext(sessionEntries).messages;
1011
+ }
1012
+
960
1013
  /**
961
1014
  * Lightweight metadata for a session file, used in session picker UI.
962
1015
  * Uses lazy getters to defer string formatting until actually displayed.
@@ -3205,11 +3258,12 @@ export class SessionManager {
3205
3258
  }
3206
3259
 
3207
3260
  /**
3208
- * Build the session context (what gets sent to the LLM).
3261
+ * Build the session context (what gets sent to the LLM), or — with
3262
+ * `{ transcript: true }` — the full-history display transcript.
3209
3263
  * Uses tree traversal from current leaf.
3210
3264
  */
3211
- buildSessionContext(): SessionContext {
3212
- return buildSessionContext(this.getEntries(), this.#leafId, this.#byId);
3265
+ buildSessionContext(options?: BuildSessionContextOptions): SessionContext {
3266
+ return buildSessionContext(this.getEntries(), this.#leafId, this.#byId, options);
3213
3267
  }
3214
3268
 
3215
3269
  /** Strip stale OpenAI Responses assistant replay metadata from loaded in-memory entries. */