@bastani/atomic 0.8.27 → 0.8.28-alpha.1

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 (108) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +2 -2
  4. package/dist/builtin/subagents/package.json +1 -1
  5. package/dist/builtin/web-access/package.json +1 -1
  6. package/dist/builtin/workflows/CHANGELOG.md +14 -0
  7. package/dist/builtin/workflows/README.md +11 -9
  8. package/dist/builtin/workflows/package.json +1 -1
  9. package/dist/builtin/workflows/src/authoring.d.ts +5 -2
  10. package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +3 -1
  11. package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +17 -25
  12. package/dist/builtin/workflows/src/extension/index.ts +133 -18
  13. package/dist/builtin/workflows/src/extension/render-result.ts +22 -2
  14. package/dist/builtin/workflows/src/extension/workflow-schema.ts +3 -3
  15. package/dist/builtin/workflows/src/runs/foreground/executor.ts +210 -16
  16. package/dist/builtin/workflows/src/sdk-surface.ts +1 -1
  17. package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +42 -5
  18. package/dist/builtin/workflows/src/shared/store-types.ts +8 -2
  19. package/dist/builtin/workflows/src/shared/store.ts +51 -0
  20. package/dist/builtin/workflows/src/shared/types.ts +14 -4
  21. package/dist/builtin/workflows/src/tui/graph-view.ts +4 -1
  22. package/dist/builtin/workflows/src/tui/prompt-card.ts +6 -0
  23. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +11 -1
  24. package/dist/core/agent-session.d.ts +4 -4
  25. package/dist/core/agent-session.d.ts.map +1 -1
  26. package/dist/core/agent-session.js +147 -31
  27. package/dist/core/agent-session.js.map +1 -1
  28. package/dist/core/auth-guidance.d.ts +10 -1
  29. package/dist/core/auth-guidance.d.ts.map +1 -1
  30. package/dist/core/auth-guidance.js +26 -1
  31. package/dist/core/auth-guidance.js.map +1 -1
  32. package/dist/core/compaction/branch-summarization.d.ts +2 -2
  33. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  34. package/dist/core/compaction/branch-summarization.js +7 -7
  35. package/dist/core/compaction/branch-summarization.js.map +1 -1
  36. package/dist/core/compaction/compaction.d.ts +4 -84
  37. package/dist/core/compaction/compaction.d.ts.map +1 -1
  38. package/dist/core/compaction/compaction.js +3 -479
  39. package/dist/core/compaction/compaction.js.map +1 -1
  40. package/dist/core/compaction/context-compaction.d.ts.map +1 -1
  41. package/dist/core/compaction/context-compaction.js +39 -82
  42. package/dist/core/compaction/context-compaction.js.map +1 -1
  43. package/dist/core/compaction/index.d.ts +1 -1
  44. package/dist/core/compaction/index.d.ts.map +1 -1
  45. package/dist/core/compaction/index.js +1 -1
  46. package/dist/core/compaction/index.js.map +1 -1
  47. package/dist/core/extensions/types.d.ts +10 -8
  48. package/dist/core/extensions/types.d.ts.map +1 -1
  49. package/dist/core/extensions/types.js.map +1 -1
  50. package/dist/core/index.d.ts +1 -1
  51. package/dist/core/index.d.ts.map +1 -1
  52. package/dist/core/index.js.map +1 -1
  53. package/dist/core/messages.d.ts +1 -11
  54. package/dist/core/messages.d.ts.map +1 -1
  55. package/dist/core/messages.js +10 -25
  56. package/dist/core/messages.js.map +1 -1
  57. package/dist/core/session-manager.d.ts +5 -8
  58. package/dist/core/session-manager.d.ts.map +1 -1
  59. package/dist/core/session-manager.js +12 -76
  60. package/dist/core/session-manager.js.map +1 -1
  61. package/dist/core/settings-manager.d.ts +0 -3
  62. package/dist/core/settings-manager.d.ts.map +1 -1
  63. package/dist/core/settings-manager.js +0 -4
  64. package/dist/core/settings-manager.js.map +1 -1
  65. package/dist/index.d.ts +3 -3
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +3 -3
  68. package/dist/index.js.map +1 -1
  69. package/dist/modes/interactive/components/chat-message-renderer.d.ts +1 -5
  70. package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
  71. package/dist/modes/interactive/components/chat-message-renderer.js +5 -9
  72. package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
  73. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  74. package/dist/modes/interactive/components/chat-session-host.js +0 -3
  75. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  76. package/dist/modes/interactive/components/index.d.ts +0 -1
  77. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  78. package/dist/modes/interactive/components/index.js +0 -1
  79. package/dist/modes/interactive/components/index.js.map +1 -1
  80. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  81. package/dist/modes/interactive/interactive-mode.js +4 -27
  82. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  83. package/dist/modes/rpc/rpc-client.d.ts +1 -1
  84. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  85. package/dist/modes/rpc/rpc-client.js +2 -2
  86. package/dist/modes/rpc/rpc-client.js.map +1 -1
  87. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  88. package/dist/modes/rpc/rpc-mode.js +1 -1
  89. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  90. package/dist/modes/rpc/rpc-types.d.ts +0 -1
  91. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  92. package/dist/modes/rpc/rpc-types.js.map +1 -1
  93. package/docs/compaction.md +210 -181
  94. package/docs/extensions.md +31 -20
  95. package/docs/json.md +3 -4
  96. package/docs/session-format.md +12 -21
  97. package/docs/sessions.md +3 -1
  98. package/docs/settings.md +2 -5
  99. package/docs/workflows.md +11 -9
  100. package/examples/extensions/README.md +1 -1
  101. package/examples/extensions/custom-compaction.ts +43 -106
  102. package/examples/extensions/handoff.ts +6 -44
  103. package/examples/extensions/trigger-compact.ts +5 -4
  104. package/package.json +5 -5
  105. package/dist/modes/interactive/components/compaction-summary-message.d.ts +0 -16
  106. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +0 -1
  107. package/dist/modes/interactive/components/compaction-summary-message.js +0 -43
  108. package/dist/modes/interactive/components/compaction-summary-message.js.map +0 -1
package/docs/json.md CHANGED
@@ -53,10 +53,9 @@ Base messages come from `@earendil-works/pi-ai` (installed as an Atomic dependen
53
53
  - `ToolResultMessage`
54
54
 
55
55
  Extended messages from [`packages/coding-agent/src/core/messages.ts`](https://github.com/bastani-inc/atomic/blob/main/packages/coding-agent/src/core/messages.ts#L29):
56
- - `BashExecutionMessage` (line 29)
57
- - `CustomMessage` (line 46)
58
- - `BranchSummaryMessage` (line 55)
59
- - `CompactionSummaryMessage` (line 62)
56
+ - `BashExecutionMessage`
57
+ - `CustomMessage`
58
+ - `BranchSummaryMessage`
60
59
 
61
60
  ## Output Format
62
61
 
@@ -144,15 +144,10 @@ interface BranchSummaryMessage {
144
144
  fromId: string; // Entry we branched from
145
145
  timestamp: number;
146
146
  }
147
-
148
- interface CompactionSummaryMessage {
149
- role: "compactionSummary";
150
- summary: string;
151
- tokensBefore: number;
152
- timestamp: number;
153
- }
154
147
  ```
155
148
 
149
+ Historical sessions may contain retired `compactionSummary` role messages from the old summary-compaction implementation. Atomic no longer produces them, they are not part of the active `AgentMessage` union, and they are not injected when active LLM context is rebuilt.
150
+
156
151
  ### AgentMessage Union
157
152
 
158
153
  ```typescript
@@ -162,8 +157,8 @@ type AgentMessage =
162
157
  | ToolResultMessage
163
158
  | BashExecutionMessage
164
159
  | CustomMessage
165
- | BranchSummaryMessage
166
- | CompactionSummaryMessage;
160
+ | BranchSummaryMessage;
161
+ // CompactionSummaryMessage was removed; it is no longer part of the active union.
167
162
  ```
168
163
 
169
164
  ## Entry Base
@@ -223,14 +218,14 @@ Emitted when the user changes the thinking/reasoning level.
223
218
 
224
219
  ### CompactionEntry
225
220
 
226
- Legacy summary-compaction entry. Stores a generated summary of earlier messages for older APIs and extension hooks. Default `/compact` and auto-compaction now create `ContextCompactionEntry` records instead.
221
+ Retired summary-compaction entry. Atomic no longer produces this entry type, does not treat it as an active compaction boundary, and does not inject its generated summary into active LLM context. Historical JSONL files may still contain these lines for audit/export compatibility.
227
222
 
228
223
  ```json
229
224
  {"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000}
230
225
  ```
231
226
 
232
- Optional fields:
233
- - `details`: Implementation-specific data (e.g., `{ readFiles: string[], modifiedFiles: string[] }` for default, or custom data for extensions)
227
+ Optional historical fields:
228
+ - `details`: Legacy implementation-specific data
234
229
  - `fromHook`: `true` if generated by an extension, `false`/`undefined` if Atomic-generated (legacy field name)
235
230
 
236
231
  ### ContextCompactionEntry
@@ -316,14 +311,11 @@ Entries form a tree:
316
311
 
317
312
  `buildSessionContext()` walks from the current leaf to the root, producing the message list for the LLM:
318
313
 
319
- 1. Collects all entries on the path
314
+ 1. Collects all entries on the active branch path
320
315
  2. Extracts current model and thinking level settings
321
- 3. If a `CompactionEntry` is on the path:
322
- - Emits the summary first
323
- - Then messages from `firstKeptEntryId` to compaction
324
- - Then messages after compaction
325
- 4. Applies `ContextCompactionEntry` logical deletions recorded after the latest summary compaction
326
- 5. Converts `BranchSummaryEntry` and `CustomMessageEntry` to appropriate message formats
316
+ 3. Applies every `ContextCompactionEntry` logical deletion on that path, filtering targeted entries/content blocks from active context while leaving retained content unchanged
317
+ 4. Converts `BranchSummaryEntry` and `CustomMessageEntry` to appropriate message formats
318
+ 5. Ignores retired `CompactionEntry` lines for active LLM context; they remain archival JSONL data only
327
319
 
328
320
  ## Parsing Example
329
321
 
@@ -343,7 +335,7 @@ for (const line of lines) {
343
335
  console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);
344
336
  break;
345
337
  case "compaction":
346
- console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);
338
+ console.log(`[${entry.id}] Retired summary-compaction record: ${entry.tokensBefore} tokens summarized historically`);
347
339
  break;
348
340
  case "context_compaction":
349
341
  console.log(`[${entry.id}] Context compaction: ${entry.stats.objectsDeleted} objects deleted`);
@@ -394,7 +386,6 @@ Key methods for working with sessions programmatically.
394
386
  - `appendMessage(message)` - Add message
395
387
  - `appendThinkingLevelChange(level)` - Record thinking change
396
388
  - `appendModelChange(provider, modelId)` - Record model change
397
- - `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - Add summary compaction
398
389
  - `appendContextCompaction(deletedTargets, protectedEntryIds, stats, backupPath?)` - Add logical deletion compaction
399
390
  - `appendCustomEntry(customType, data?)` - Extension state (not in context)
400
391
  - `appendSessionInfo(name)` - Set session display name
package/docs/sessions.md CHANGED
@@ -128,10 +128,12 @@ When prompted, choose one of:
128
128
  2. summarize with the default prompt
129
129
  3. summarize with custom focus instructions
130
130
 
131
+ Branch summaries are separate from `/compact`: branch navigation can generate summary prose (optionally with focus instructions), while Verbatim Compaction records validated deletion targets and does not accept summary instructions.
132
+
131
133
  See [Compaction](/compaction) for Verbatim Compaction, branch summarization internals, and extension hooks.
132
134
 
133
135
  ## Session Format
134
136
 
135
- Session files are JSONL and contain message entries, model changes, thinking-level changes, labels, summary compactions, context compactions, branch summaries, and extension entries.
137
+ Session files are JSONL and contain message entries, model changes, thinking-level changes, labels, context compactions, branch summaries, extension entries, and retired legacy `type:"compaction"` records from older sessions.
136
138
 
137
139
  For parsers, extensions, SDK usage, and the full SessionManager API, see [Session Format](/session-format).
package/docs/settings.md CHANGED
@@ -92,14 +92,12 @@ Set `ATOMIC_SKIP_VERSION_CHECK=1` to disable the Atomic version update check. Us
92
92
  |---------|------|---------|-------------|
93
93
  | `compaction.enabled` | boolean | `true` | Enable automatic Verbatim Compaction |
94
94
  | `compaction.reserveTokens` | number | `16384` | Tokens reserved for LLM response |
95
- | `compaction.keepRecentTokens` | number | `20000` | Legacy summary-compaction retained-token budget; default Verbatim Compaction protects recent entries structurally |
96
95
 
97
96
  ```json
98
97
  {
99
98
  "compaction": {
100
99
  "enabled": true,
101
- "reserveTokens": 16384,
102
- "keepRecentTokens": 20000
100
+ "reserveTokens": 16384
103
101
  }
104
102
  }
105
103
  ```
@@ -285,8 +283,7 @@ See [Atomic packages](/packages) for package management details.
285
283
  "theme": "dark",
286
284
  "compaction": {
287
285
  "enabled": true,
288
- "reserveTokens": 16384,
289
- "keepRecentTokens": 20000
286
+ "reserveTokens": 16384
290
287
  },
291
288
  "retry": {
292
289
  "enabled": true,
package/docs/workflows.md CHANGED
@@ -10,7 +10,7 @@ Use a workflow when a task should be repeatable, inspectable, resumable, or spli
10
10
  - **Tracked stages** - Name each step and inspect it in workflow status and graph views
11
11
  - **Parallel branches** - Run independent research, review, or implementation branches concurrently
12
12
  - **Context handoffs** - Pass summaries, artifacts, files, and structured outputs between stages
13
- - **Human input** - Pause for `ctx.ui.input`, `confirm`, `select`, or `editor` decisions during a run
13
+ - **Human input** - Pause for `ctx.ui.input`, `confirm`, `select`, `editor`, or custom TUI widget decisions during a run
14
14
  - **Resumable control** - Interrupt, pause, resume, attach to, or kill workflow runs
15
15
  - **Artifacts** - Save large outputs to files instead of pushing everything through model context
16
16
  - **Model fallback chains** - Retry important stages on fallback models when providers fail
@@ -357,9 +357,11 @@ Named runs go to the background. Common controls:
357
357
  /workflow kill <run-id> # abort and retain for inspection
358
358
  ```
359
359
 
360
- Human-in-the-loop prompts from `ctx.ui.input`, `ctx.ui.confirm`, `ctx.ui.select`, and `ctx.ui.editor` appear as awaiting-input nodes in the workflow graph viewer, not as chat modals — use `/workflow connect <run-id>` (or F2), focus the node, and press Enter to answer them locally.
360
+ Human-in-the-loop prompts from `ctx.ui.input`, `ctx.ui.confirm`, `ctx.ui.select`, `ctx.ui.editor`, and `ctx.ui.custom<T>` appear as awaiting-input nodes in the workflow graph viewer, not as chat modals — use `/workflow connect <run-id>` (or F2), focus the node, and press Enter to answer them locally.
361
361
 
362
- Prompt answers are replayable only while the source run remains in the live in-memory store. `StageSnapshot.promptAnswerState` is snapshot-safe metadata for continuation: `available` means a matching live answer can be replayed, `unavailable` means the matching prompt node exists but its private answer was purged, and `ambiguous` means multiple matching prompt nodes exist so Atomic asks again. The raw answer lives in a private `PromptAnswerRecord` ledger, is never written to snapshots or persistence, and remains resident in memory until the answer is cleared, the run is removed, or the store is cleared. Prompt replay keys include the prompt kind, message text, select choices, input/editor initial value, and hashed author callsite, so changing any of those inputs may intentionally re-ask on continuation. An empty `ctx.ui.select(..., [])` has no answerable choices and throws before creating a prompt node.
362
+ `ctx.ui.custom<T>(factory, options?)` reuses Atomic's TUI component path: the factory receives the same real `(tui, theme, keybindings, done)` types as extension `ctx.ui.custom`, and the workflow resumes with the value passed to `done(value)`. Use `options.label` for a safe display-only graph/status label and `options.replayIdentity` when widget semantics can change without the callsite changing. Do not put secrets in labels or replay identities; only a hash of the identity is stored, and label text is not part of replay identity. Inline connected rendering is supported; `overlay: true` is rejected clearly because nested workflow graph overlays are not safely supported yet.
363
+
364
+ Prompt answers are replayable only while the source run remains in the live in-memory store. `StageSnapshot.promptAnswerState` is snapshot-safe metadata for continuation: `available` means a matching live answer can be replayed, `unavailable` means the matching prompt node exists but its private answer was purged, and `ambiguous` means multiple matching prompt nodes exist so Atomic asks again. The raw answer lives in a private `PromptAnswerRecord` ledger, is never written to snapshots or persistence, and remains resident in memory until the answer is cleared, the run is removed, or the store is cleared. Prompt replay keys include the prompt kind, message text, select choices, input/editor initial value, custom prompt identity hash, and hashed author callsite, so changing any of those inputs may intentionally re-ask on continuation. An empty `ctx.ui.select(..., [])` has no answerable choices and throws before creating a prompt node. Arbitrary custom-widget answers cannot be supplied through `workflow send`; focus the `custom` awaiting-input node in the interactive graph instead.
363
365
 
364
366
  ## When to Use Workflows
365
367
 
@@ -763,7 +765,7 @@ Input overrides are bare `key=value` tokens. Values are JSON-parsed when possibl
763
765
 
764
766
  In the TUI, `/workflow <name>` opens an input picker when the workflow declares inputs and either no arguments were supplied or required inputs are missing. Supplied values seed the picker. Pass `--no-picker` to skip that interactive flow.
765
767
 
766
- In non-interactive (`-p`, `--print`, or `--mode json`) sessions, named workflow dispatch waits for the terminal run snapshot and skips pickers. Because human input is runtime-only and workflows no longer carry a declaration-time HIL marker, headless dispatch does not reject a workflow just because its source contains `ctx.ui.*`. If you copy a HIL workflow example into a headless session, it can pass dispatch and then fail when execution reaches the prompt with an error such as `atomic-workflows: HIL ctx.ui.confirm is unavailable because Atomic runtime did not provide a UI adapter` (the primitive name varies). Run those workflows interactively, or guard/remove runtime `ctx.ui.*` calls before using headless mode.
768
+ In non-interactive (`-p`, `--print`, or `--mode json`) sessions, named workflow dispatch waits for the terminal run snapshot and skips pickers. Because human input is runtime-only and workflows no longer carry a declaration-time HIL marker, headless dispatch does not reject a workflow just because its source contains `ctx.ui.*`. If you copy a HIL workflow example into a headless session, it can pass dispatch and then fail when execution reaches the prompt with an error such as `atomic-workflows: HIL ctx.ui.confirm is unavailable because Atomic runtime did not provide a UI adapter` (the primitive name varies, including `ctx.ui.custom`). Run those workflows interactively, or guard/remove runtime `ctx.ui.*` calls before using headless mode.
767
769
 
768
770
  <p align="center"><img src="images/workflow-input-picker.png" alt="Workflow Input Picker" width="600" /></p>
769
771
 
@@ -789,7 +791,7 @@ Use `connect` for the workflow graph. Use `attach` when you want a chat pane for
789
791
 
790
792
  <p align="center"><img src="images/workflow-graph.png" alt="Workflow Graph Viewer" width="600" /></p>
791
793
 
792
- Human-in-the-loop prompts from `ctx.ui.input`, `ctx.ui.confirm`, `ctx.ui.select`, and `ctx.ui.editor` appear as awaiting-input nodes in the workflow UI/graph viewer, not as ordinary chat modals. Workflows do not declare HIL up front; prompt nodes are created when the runtime `ctx.ui.*` call executes. If the prompt lives inside an imported child workflow, it still appears in the same expanded parent graph so the user can focus and answer it without switching to a separate child status entry.
794
+ Human-in-the-loop prompts from `ctx.ui.input`, `ctx.ui.confirm`, `ctx.ui.select`, `ctx.ui.editor`, and `ctx.ui.custom<T>` appear as awaiting-input nodes in the workflow UI/graph viewer, not as ordinary chat modals. Workflows do not declare HIL up front; prompt nodes are created when the runtime `ctx.ui.*` call executes. If the prompt lives inside an imported child workflow, it still appears in the same expanded parent graph so the user can focus and answer it without switching to a separate child status entry. Custom widget prompts mount inside the attached stage chat and must be completed interactively with the widget's `done(value)` callback.
793
795
 
794
796
  ## Monitor and Control Runs
795
797
 
@@ -832,7 +834,7 @@ Control behavior:
832
834
  - `stages` lists stage summaries, including flattened stages from nested `ctx.workflow(...)` imports and `sessionFile`/`transcriptPath` when a stage has a persisted session. Use `statusFilter: "all"` to include completed, failed, skipped, and pending stages.
833
835
  - `stage` returns details for one stage by stage id, unique prefix, or stage name, including nested child stages shown in the expanded graph and the persisted `sessionFile` when available.
834
836
  - `transcript` is reference-first with a small preview by default: it returns metadata, transcript paths, and up to 5 recent entries. For targeted lookup, quote the exact `sessionFile`/`transcriptPath` value without changing platform separators (preserve Windows backslashes), search it with `rg` or `grep`, then read only small surrounding ranges. Text results include JSON-escaped `sessionFileJson`/`transcriptPathJson` lines for copy-safe path literals. Pass explicit `tail` or `limit` to override the 5-entry preview; `tail` overrides `limit`; `includeToolOutput` includes captured snapshot tool output in snapshot transcript results.
835
- - `send` delivery modes are `auto`, `answer`, `prompt`, `steer`, `followUp`, and `resume`. Prompt answers can include `promptId` and can carry answer content in `response`, `text`, or `message`; structured UI prompts usually prefer `response`.
837
+ - `send` delivery modes are `auto`, `answer`, `prompt`, `steer`, `followUp`, and `resume`. Prompt answers can include `promptId` and can carry answer content in `response`, `text`, or `message`; structured UI prompts usually prefer `response`. Arbitrary `ctx.ui.custom<T>` widget prompts require the interactive workflow graph and return a clear unsupported message when targeted through `send`.
836
838
  - `delivery: "auto"` first answers a pending prompt, then resumes paused work, then steers a streaming stage, then queues a follow-up.
837
839
  - `pause`, `interrupt`, and `kill` can target one top-level run or `all: true`; `stageId` cannot be combined with `all: true`. Stage-scoped controls can target a visible nested child stage from the expanded graph; Atomic routes the operation to the owning nested run internally.
838
840
  - `interrupt` is resumable: it pauses live work when pausable stages exist and keeps the run in live history/status.
@@ -847,7 +849,7 @@ Use slash commands for graph connect and stage attach because those are interact
847
849
 
848
850
  Atomic emits deduplicated main-chat notices when top-level workflow runs complete or fail. Nested child workflow completion/failure is reflected inside the expanded parent graph instead of producing separate top-level completion cards. These terminal notices are queued into the active main chat as steering/context messages (`triggerTurn: true`, `deliverAs: "steer"`) so the model can react without the user manually polling status. Awaiting-input workflow states are tracked for dedupe/restore, but they do not enqueue main-chat connect cards or wake the model; prompt state remains visible through workflow status/connect surfaces. Configure lifecycle behavior with `workflowNotifications.enabled` (default `true`) and `workflowNotifications.notifyOn` (default `["completed", "failed", "awaiting_input"]`).
849
851
 
850
- Human input is runtime-only: call `ctx.ui.input`, `ctx.ui.confirm`, `ctx.ui.select`, or `ctx.ui.editor` at the point where the workflow actually needs a decision. No builder-level declaration is required or supported.
852
+ Human input is runtime-only: call `ctx.ui.input`, `ctx.ui.confirm`, `ctx.ui.select`, `ctx.ui.editor`, or `ctx.ui.custom<T>` at the point where the workflow actually needs a decision. No builder-level declaration is required or supported.
851
853
 
852
854
  When a workflow needs human input, answer in the graph viewer or attached stage chat when possible:
853
855
 
@@ -856,7 +858,7 @@ When a workflow needs human input, answer in the graph viewer or attached stage
856
858
  /workflow attach <run-id> <stage-id-or-name>
857
859
  ```
858
860
 
859
- Agents can answer pending prompts programmatically with `workflow({ action: "send", delivery: "answer", ... })`; use `promptId` when it is present in the stage details, and provide answer content with `response`, `text`, or `message`.
861
+ Agents can answer primitive and structured pending prompts programmatically with `workflow({ action: "send", delivery: "answer", ... })`; use `promptId` when it is present in the stage details, and provide answer content with `response`, `text`, or `message`. Arbitrary custom TUI widget prompts intentionally refuse this path in iteration 1 because a generic `T` cannot be reconstructed safely from a non-TUI payload.
860
862
 
861
863
  If the user answers a human-in-the-loop prompt in the workflow UI or stage UI broker, the stage receives the answer directly and the active main chat receives a display-only notice (`triggerTurn: false`, `excludeFromContext: true`) containing a concise answer summary. The notice is rendered for the user and persisted for audit, but it does not wake the model, enter LLM context, or authorize answering any other workflow prompt. Prompt answers sent by the main-chat `workflow` tool are suppressed from this notice because the tool result already informs the current turn.
862
864
 
@@ -1347,7 +1349,7 @@ Prefer high-level primitives because they create tracked graph nodes, provide co
1347
1349
  | Dependent sequential tasks | `ctx.chain(steps, options?)` |
1348
1350
  | Independent concurrent branches | `ctx.parallel(steps, options?)` |
1349
1351
  | Reusable child workflow | Call `ctx.workflow(workflowDefinition, options?)` |
1350
- | Human input during a workflow run | `ctx.ui.input/confirm/select/editor` |
1352
+ | Human input during a workflow run | `ctx.ui.input/confirm/select/editor/custom` |
1351
1353
  | Pure deterministic computation, parsing, or file I/O | Plain TypeScript in `.run()` or helpers |
1352
1354
  | Fine-grained session control | `ctx.stage(name, options?)` |
1353
1355
 
@@ -89,7 +89,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
89
89
  |-----------|-------------|
90
90
  | `pirate.ts` | Demonstrates `systemPromptAppend` to dynamically modify system prompt |
91
91
  | `claude-rules.ts` | Scans `.claude/rules/` folder and lists rules in system prompt |
92
- | `custom-compaction.ts` | Custom compaction that summarizes entire conversation |
92
+ | `custom-compaction.ts` | Custom compaction policy that provides exact deletion targets |
93
93
  | `trigger-compact.ts` | Triggers compaction when context usage exceeds 100k tokens and adds `/trigger-compact` command |
94
94
 
95
95
  ### System Integration
@@ -1,127 +1,64 @@
1
1
  /**
2
- * Custom Compaction Extension
2
+ * Custom Compaction Deletion Policy Extension
3
3
  *
4
- * Replaces the default compaction behavior with a full summary of the entire context.
5
- * Instead of keeping the last 20k tokens of conversation turns, this extension:
6
- * 1. Summarizes ALL messages (messagesToSummarize + turnPrefixMessages)
7
- * 2. Discards all old turns completely, keeping only the summary
4
+ * Verbatim Compaction is deletion-only: extensions cannot replace history with a
5
+ * generated summary. This example shows how to provide an exact deletion request
6
+ * before Atomic's internal planner runs. Atomic still validates the requested
7
+ * entry IDs locally before appending a context_compaction entry.
8
8
  *
9
- * This example also demonstrates using a different model (Gemini Flash) for summarization,
10
- * which can be cheaper/faster than the main conversation model.
9
+ * This policy deletes older, unprotected, successful bash execution entries once
10
+ * they are large enough to matter. If no safe candidates are present, it returns
11
+ * nothing and Atomic uses its default deletion planner.
11
12
  *
12
13
  * Usage:
13
- * pi --extension examples/extensions/custom-compaction.ts
14
+ * atomic --extension examples/extensions/custom-compaction.ts
14
15
  */
15
16
 
16
- import { complete } from "@earendil-works/pi-ai";
17
- import type { ExtensionAPI } from "@bastani/atomic";
18
- import { convertToLlm, serializeConversation } from "@bastani/atomic";
17
+ import type { ContextDeletionRequest, ExtensionAPI } from "@bastani/atomic";
18
+
19
+ const MIN_BASH_OUTPUT_TOKENS = 250;
20
+ const MAX_DELETIONS_PER_RUN = 12;
19
21
 
20
22
  export default function (pi: ExtensionAPI) {
21
23
  pi.on("session_before_compact", async (event, ctx) => {
22
- ctx.ui.notify("Custom compaction extension triggered", "info");
23
-
24
- const { preparation, branchEntries: _, signal } = event;
25
- const { messagesToSummarize, turnPrefixMessages, tokensBefore, firstKeptEntryId, previousSummary } = preparation;
26
-
27
- // Use Gemini Flash for summarization (cheaper/faster than most conversation models)
28
- const model = ctx.modelRegistry.find("google", "gemini-2.5-flash");
29
- if (!model) {
30
- ctx.ui.notify(`Could not find Gemini Flash model, using default compaction`, "warning");
24
+ const { preparation, reason, mode } = event;
25
+ const candidates = preparation.transcript.entries
26
+ .filter((entry) => !entry.protected)
27
+ .filter((entry) => entry.message.role === "bashExecution" && entry.message.exitCode === 0)
28
+ .filter((entry) => entry.tokenEstimate >= MIN_BASH_OUTPUT_TOKENS)
29
+ .slice(0, MAX_DELETIONS_PER_RUN);
30
+
31
+ if (candidates.length === 0) {
32
+ if (ctx.hasUI) {
33
+ ctx.ui.notify("Custom compaction policy found no safe bash output to delete; using default planner", "info");
34
+ }
31
35
  return;
32
36
  }
33
37
 
34
- // Resolve request auth for the summarization model
35
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
36
- if (!auth.ok) {
37
- ctx.ui.notify(`Compaction auth failed: ${auth.error}`, "warning");
38
- return;
39
- }
40
- if (!auth.apiKey) {
41
- ctx.ui.notify(`No API key for ${model.provider}, using default compaction`, "warning");
42
- return;
38
+ const deletions: ContextDeletionRequest["deletions"] = candidates.map((entry) => ({
39
+ kind: "entry",
40
+ entryId: entry.entryId,
41
+ rationale: "Large successful bash output selected by custom compaction policy",
42
+ }));
43
+
44
+ if (ctx.hasUI) {
45
+ const tokenEstimate = candidates.reduce((sum, entry) => sum + entry.tokenEstimate, 0);
46
+ ctx.ui.notify(
47
+ `Custom compaction (${reason ?? "manual"}/${mode}): requesting ${deletions.length} deletion(s), about ${tokenEstimate.toLocaleString()} tokens`,
48
+ "info",
49
+ );
43
50
  }
44
51
 
45
- // Combine all messages for full summary
46
- const allMessages = [...messagesToSummarize, ...turnPrefixMessages];
52
+ return {
53
+ deletionRequest: { deletions },
54
+ };
55
+ });
47
56
 
57
+ pi.on("session_compact", async (event, ctx) => {
58
+ if (!ctx.hasUI || !event.fromExtension) return;
48
59
  ctx.ui.notify(
49
- `Custom compaction: summarizing ${allMessages.length} messages (${tokensBefore.toLocaleString()} tokens) with ${model.id}...`,
60
+ `Custom compaction policy deleted ${event.result.stats.objectsDeleted} object(s)`,
50
61
  "info",
51
62
  );
52
-
53
- // Convert messages to readable text format
54
- const conversationText = serializeConversation(convertToLlm(allMessages));
55
-
56
- // Include previous summary context if available
57
- const previousContext = previousSummary ? `\n\nPrevious session summary for context:\n${previousSummary}` : "";
58
-
59
- // Build messages that ask for a comprehensive summary
60
- const summaryMessages = [
61
- {
62
- role: "user" as const,
63
- content: [
64
- {
65
- type: "text" as const,
66
- text: `You are a conversation summarizer. Create a comprehensive summary of this conversation that captures:${previousContext}
67
-
68
- 1. The main goals and objectives discussed
69
- 2. Key decisions made and their rationale
70
- 3. Important code changes, file modifications, or technical details
71
- 4. Current state of any ongoing work
72
- 5. Any blockers, issues, or open questions
73
- 6. Next steps that were planned or suggested
74
-
75
- Be thorough but concise. The summary will replace the ENTIRE conversation history, so include all information needed to continue the work effectively.
76
-
77
- Format the summary as structured markdown with clear sections.
78
-
79
- <conversation>
80
- ${conversationText}
81
- </conversation>`,
82
- },
83
- ],
84
- timestamp: Date.now(),
85
- },
86
- ];
87
-
88
- try {
89
- // Pass signal to honor abort requests (e.g., user cancels compaction)
90
- const response = await complete(
91
- model,
92
- { messages: summaryMessages },
93
- {
94
- apiKey: auth.apiKey,
95
- headers: auth.headers,
96
- maxTokens: 8192,
97
- signal,
98
- },
99
- );
100
-
101
- const summary = response.content
102
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
103
- .map((c) => c.text)
104
- .join("\n");
105
-
106
- if (!summary.trim()) {
107
- if (!signal.aborted) ctx.ui.notify("Compaction summary was empty, using default compaction", "warning");
108
- return;
109
- }
110
-
111
- // Return compaction content - SessionManager adds id/parentId
112
- // Use firstKeptEntryId from preparation to keep recent messages
113
- return {
114
- compaction: {
115
- summary,
116
- firstKeptEntryId,
117
- tokensBefore,
118
- },
119
- };
120
- } catch (error) {
121
- const message = error instanceof Error ? error.message : String(error);
122
- ctx.ui.notify(`Compaction failed: ${message}`, "error");
123
- // Fall back to default compaction on error
124
- return;
125
- }
126
63
  });
127
64
  }
@@ -12,10 +12,9 @@
12
12
  * The generated prompt appears as a draft in the editor for review/editing.
13
13
  */
14
14
 
15
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
16
15
  import { complete, type Message } from "@earendil-works/pi-ai";
17
- import type { ExtensionAPI, SessionEntry } from "@bastani/atomic";
18
- import { BorderedLoader, convertToLlm, serializeConversation } from "@bastani/atomic";
16
+ import type { ExtensionAPI } from "@bastani/atomic";
17
+ import { BorderedLoader, buildSessionContext, convertToLlm, serializeConversation } from "@bastani/atomic";
19
18
 
20
19
  const SYSTEM_PROMPT = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that:
21
20
 
@@ -39,44 +38,6 @@ Files involved:
39
38
  ## Task
40
39
  [Clear description of what to do next based on user's goal]`;
41
40
 
42
- function entryToMessage(entry: SessionEntry): AgentMessage | undefined {
43
- if (entry.type === "message") {
44
- return entry.message;
45
- }
46
- if (entry.type === "compaction") {
47
- return {
48
- role: "compactionSummary",
49
- summary: entry.summary,
50
- tokensBefore: entry.tokensBefore,
51
- timestamp: new Date(entry.timestamp).getTime(),
52
- };
53
- }
54
- return undefined;
55
- }
56
-
57
- function getHandoffMessages(branch: SessionEntry[]): AgentMessage[] {
58
- let compactionIndex = -1;
59
- for (let i = branch.length - 1; i >= 0; i--) {
60
- if (branch[i].type === "compaction") {
61
- compactionIndex = i;
62
- break;
63
- }
64
- }
65
- if (compactionIndex < 0) {
66
- return branch.map(entryToMessage).filter((message) => message !== undefined);
67
- }
68
-
69
- const compaction = branch[compactionIndex];
70
- const firstKeptIndex =
71
- compaction.type === "compaction" ? branch.findIndex((entry) => entry.id === compaction.firstKeptEntryId) : -1;
72
- const compactedBranch = [
73
- compaction,
74
- ...(firstKeptIndex >= 0 ? branch.slice(firstKeptIndex, compactionIndex) : []),
75
- ...branch.slice(compactionIndex + 1),
76
- ];
77
- return compactedBranch.map(entryToMessage).filter((message) => message !== undefined);
78
- }
79
-
80
41
  export default function (pi: ExtensionAPI) {
81
42
  pi.registerCommand("handoff", {
82
43
  description: "Transfer context to a new focused session",
@@ -97,9 +58,10 @@ export default function (pi: ExtensionAPI) {
97
58
  return;
98
59
  }
99
60
 
100
- // Gather conversation context from current branch. If the branch was compacted,
101
- // include the compaction summary plus entries from firstKeptEntryId onward.
102
- const messages = getHandoffMessages(ctx.sessionManager.getBranch());
61
+ // Gather the same active context Atomic would send to the model. This applies
62
+ // context_compaction deletion filters and treats retired summary-compaction
63
+ // records as archival metadata, not replacement conversation context.
64
+ const messages = buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages;
103
65
 
104
66
  if (messages.length === 0) {
105
67
  ctx.ui.notify("No conversation to hand off", "error");
@@ -5,12 +5,11 @@ const COMPACT_THRESHOLD_TOKENS = 100_000;
5
5
  export default function (pi: ExtensionAPI) {
6
6
  let previousTokens: number | null | undefined;
7
7
 
8
- const triggerCompaction = (ctx: ExtensionContext, customInstructions?: string) => {
8
+ const triggerCompaction = (ctx: ExtensionContext) => {
9
9
  if (ctx.hasUI) {
10
10
  ctx.ui.notify("Compaction started", "info");
11
11
  }
12
12
  ctx.compact({
13
- customInstructions,
14
13
  onComplete: () => {
15
14
  if (ctx.hasUI) {
16
15
  ctx.ui.notify("Compaction completed", "info");
@@ -43,8 +42,10 @@ export default function (pi: ExtensionAPI) {
43
42
  pi.registerCommand("trigger-compact", {
44
43
  description: "Trigger compaction immediately",
45
44
  handler: async (args, ctx) => {
46
- const instructions = args.trim() || undefined;
47
- triggerCompaction(ctx, instructions);
45
+ if (args.trim() && ctx.hasUI) {
46
+ ctx.ui.notify("/trigger-compact ignores arguments; Verbatim Compaction uses a fixed deletion planner", "warning");
47
+ }
48
+ triggerCompaction(ctx);
48
49
  },
49
50
  });
50
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.8.27",
3
+ "version": "0.8.28-alpha.1",
4
4
  "description": "Atomic coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "atomicConfig": {
@@ -76,15 +76,15 @@
76
76
  "@silvia-odwyer/photon-node": "^0.3.4",
77
77
  "chalk": "^5.5.0",
78
78
  "cross-spawn": "7.0.6",
79
- "diff": "^8.0.2",
79
+ "diff": "^9.0.0",
80
80
  "glob": "^13.0.1",
81
81
  "highlight.js": "^11.11.1",
82
- "hosted-git-info": "^9.0.2",
82
+ "hosted-git-info": "^10.1.1",
83
83
  "ignore": "^7.0.5",
84
84
  "jiti": "^2.7.0",
85
85
  "linkedom": "^0.18.12",
86
86
  "minimatch": "^10.2.3",
87
- "open": "^10.2.0",
87
+ "open": "^11.0.0",
88
88
  "p-limit": "^6.1.0",
89
89
  "proper-lockfile": "^4.1.2",
90
90
  "turndown": "^7.2.0",
@@ -108,7 +108,7 @@
108
108
  "@types/diff": "^7.0.2",
109
109
  "@types/hosted-git-info": "^3.0.5",
110
110
  "@types/ms": "^2.1.0",
111
- "@types/node": "^24.3.0",
111
+ "@types/node": "^25.9.1",
112
112
  "@types/proper-lockfile": "^4.1.4",
113
113
  "@typescript/native-preview": "7.0.0-dev.20260511.1",
114
114
  "shx": "^0.4.0",
@@ -1,16 +0,0 @@
1
- import { Box, type MarkdownTheme } from "@earendil-works/pi-tui";
2
- import type { CompactionSummaryMessage } from "../../../core/messages.ts";
3
- /**
4
- * Component that renders a compaction message with collapsed/expanded state.
5
- * Uses same background color as custom messages for visual consistency.
6
- */
7
- export declare class CompactionSummaryMessageComponent extends Box {
8
- private expanded;
9
- private message;
10
- private markdownTheme;
11
- constructor(message: CompactionSummaryMessage, markdownTheme?: MarkdownTheme);
12
- setExpanded(expanded: boolean): void;
13
- invalidate(): void;
14
- private updateDisplay;
15
- }
16
- //# sourceMappingURL=compaction-summary-message.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compaction-summary-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/compaction-summary-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAY,KAAK,aAAa,EAAgB,MAAM,wBAAwB,CAAC;AACzF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAI1E;;;GAGG;AACH,qBAAa,iCAAkC,SAAQ,GAAG;IACzD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,aAAa,CAAgB;IAErC,YAAY,OAAO,EAAE,wBAAwB,EAAE,aAAa,GAAE,aAAkC,EAK/F;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAGnC;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,aAAa;CA2BrB","sourcesContent":["import { Box, Markdown, type MarkdownTheme, Spacer, Text } from \"@earendil-works/pi-tui\";\nimport type { CompactionSummaryMessage } from \"../../../core/messages.ts\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.ts\";\nimport { keyText } from \"./keybinding-hints.ts\";\n\n/**\n * Component that renders a compaction message with collapsed/expanded state.\n * Uses same background color as custom messages for visual consistency.\n */\nexport class CompactionSummaryMessageComponent extends Box {\n\tprivate expanded = false;\n\tprivate message: CompactionSummaryMessage;\n\tprivate markdownTheme: MarkdownTheme;\n\n\tconstructor(message: CompactionSummaryMessage, markdownTheme: MarkdownTheme = getMarkdownTheme()) {\n\t\tsuper(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\t\tthis.message = message;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tthis.clear();\n\n\t\tconst tokenStr = this.message.tokensBefore.toLocaleString();\n\t\tconst label = theme.fg(\"customMessageLabel\", `\\x1b[1m[compaction]\\x1b[22m`);\n\t\tthis.addChild(new Text(label, 0, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\tif (this.expanded) {\n\t\t\tconst header = `**Compacted from ${tokenStr} tokens**\\n\\n`;\n\t\t\tthis.addChild(\n\t\t\t\tnew Markdown(header + this.message.summary, 0, 0, this.markdownTheme, {\n\t\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t\t}),\n\t\t\t);\n\t\t} else {\n\t\t\tthis.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\ttheme.fg(\"customMessageText\", `Compacted from ${tokenStr} tokens (`) +\n\t\t\t\t\t\ttheme.fg(\"dim\", keyText(\"app.tools.expand\")) +\n\t\t\t\t\t\ttheme.fg(\"customMessageText\", \" Expand)\"),\n\t\t\t\t\t0,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n}\n"]}
@@ -1,43 +0,0 @@
1
- import { Box, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
2
- import { getMarkdownTheme, theme } from "../theme/theme.js";
3
- import { keyText } from "./keybinding-hints.js";
4
- /**
5
- * Component that renders a compaction message with collapsed/expanded state.
6
- * Uses same background color as custom messages for visual consistency.
7
- */
8
- export class CompactionSummaryMessageComponent extends Box {
9
- constructor(message, markdownTheme = getMarkdownTheme()) {
10
- super(1, 1, (t) => theme.bg("customMessageBg", t));
11
- this.expanded = false;
12
- this.message = message;
13
- this.markdownTheme = markdownTheme;
14
- this.updateDisplay();
15
- }
16
- setExpanded(expanded) {
17
- this.expanded = expanded;
18
- this.updateDisplay();
19
- }
20
- invalidate() {
21
- super.invalidate();
22
- this.updateDisplay();
23
- }
24
- updateDisplay() {
25
- this.clear();
26
- const tokenStr = this.message.tokensBefore.toLocaleString();
27
- const label = theme.fg("customMessageLabel", `\x1b[1m[compaction]\x1b[22m`);
28
- this.addChild(new Text(label, 0, 0));
29
- this.addChild(new Spacer(1));
30
- if (this.expanded) {
31
- const header = `**Compacted from ${tokenStr} tokens**\n\n`;
32
- this.addChild(new Markdown(header + this.message.summary, 0, 0, this.markdownTheme, {
33
- color: (text) => theme.fg("customMessageText", text),
34
- }));
35
- }
36
- else {
37
- this.addChild(new Text(theme.fg("customMessageText", `Compacted from ${tokenStr} tokens (`) +
38
- theme.fg("dim", keyText("app.tools.expand")) +
39
- theme.fg("customMessageText", " Expand)"), 0, 0));
40
- }
41
- }
42
- }
43
- //# sourceMappingURL=compaction-summary-message.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compaction-summary-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/compaction-summary-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAsB,MAAM,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEzF,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAEhD;;;GAGG;AACH,MAAM,OAAO,iCAAkC,SAAQ,GAAG;IAKzD,YAAY,OAAiC,EAAE,aAAa,GAAkB,gBAAgB,EAAE;QAC/F,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,CAAC;QAL5C,aAAQ,GAAG,KAAK,CAAC;QAMxB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,aAAa,EAAE,CAAC;IACtB,CAAC;IAED,WAAW,CAAC,QAAiB;QAC5B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,aAAa,EAAE,CAAC;IACtB,CAAC;IAEQ,UAAU;QAClB,KAAK,CAAC,UAAU,EAAE,CAAC;QACnB,IAAI,CAAC,aAAa,EAAE,CAAC;IACtB,CAAC;IAEO,aAAa;QACpB,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC;QAC5D,MAAM,KAAK,GAAG,KAAK,CAAC,EAAE,CAAC,oBAAoB,EAAE,6BAA6B,CAAC,CAAC;QAC5E,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7B,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,MAAM,MAAM,GAAG,oBAAoB,QAAQ,eAAe,CAAC;YAC3D,IAAI,CAAC,QAAQ,CACZ,IAAI,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE;gBACrE,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,mBAAmB,EAAE,IAAI,CAAC;aAC5D,CAAC,CACF,CAAC;QACH,CAAC;aAAM,CAAC;YACP,IAAI,CAAC,QAAQ,CACZ,IAAI,IAAI,CACP,KAAK,CAAC,EAAE,CAAC,mBAAmB,EAAE,kBAAkB,QAAQ,WAAW,CAAC;gBACnE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC;gBAC5C,KAAK,CAAC,EAAE,CAAC,mBAAmB,EAAE,UAAU,CAAC,EAC1C,CAAC,EACD,CAAC,CACD,CACD,CAAC;QACH,CAAC;IACF,CAAC;CACD","sourcesContent":["import { Box, Markdown, type MarkdownTheme, Spacer, Text } from \"@earendil-works/pi-tui\";\nimport type { CompactionSummaryMessage } from \"../../../core/messages.ts\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.ts\";\nimport { keyText } from \"./keybinding-hints.ts\";\n\n/**\n * Component that renders a compaction message with collapsed/expanded state.\n * Uses same background color as custom messages for visual consistency.\n */\nexport class CompactionSummaryMessageComponent extends Box {\n\tprivate expanded = false;\n\tprivate message: CompactionSummaryMessage;\n\tprivate markdownTheme: MarkdownTheme;\n\n\tconstructor(message: CompactionSummaryMessage, markdownTheme: MarkdownTheme = getMarkdownTheme()) {\n\t\tsuper(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\t\tthis.message = message;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tthis.clear();\n\n\t\tconst tokenStr = this.message.tokensBefore.toLocaleString();\n\t\tconst label = theme.fg(\"customMessageLabel\", `\\x1b[1m[compaction]\\x1b[22m`);\n\t\tthis.addChild(new Text(label, 0, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\tif (this.expanded) {\n\t\t\tconst header = `**Compacted from ${tokenStr} tokens**\\n\\n`;\n\t\t\tthis.addChild(\n\t\t\t\tnew Markdown(header + this.message.summary, 0, 0, this.markdownTheme, {\n\t\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t\t}),\n\t\t\t);\n\t\t} else {\n\t\t\tthis.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\ttheme.fg(\"customMessageText\", `Compacted from ${tokenStr} tokens (`) +\n\t\t\t\t\t\ttheme.fg(\"dim\", keyText(\"app.tools.expand\")) +\n\t\t\t\t\t\ttheme.fg(\"customMessageText\", \" Expand)\"),\n\t\t\t\t\t0,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n}\n"]}