@pellux/goodvibes-tui 0.18.12 → 0.18.13

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 (157) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/config/index.ts +1 -138
  6. package/src/config/subscription-providers.ts +1 -127
  7. package/src/core/conversation-rendering.ts +3 -3
  8. package/src/core/conversation.ts +176 -423
  9. package/src/core/history.ts +45 -0
  10. package/src/core/orchestrator.ts +3 -735
  11. package/src/core/system-message-router.ts +19 -58
  12. package/src/input/handler-content-actions.ts +2 -2
  13. package/src/input/handler-feed.ts +1 -1
  14. package/src/input/handler-modal-token-routes.ts +1 -1
  15. package/src/input/handler-ui-state.ts +1 -1
  16. package/src/input/handler.ts +1 -1
  17. package/src/input/search.ts +1 -1
  18. package/src/input/selection.ts +2 -2
  19. package/src/main.ts +1 -1
  20. package/src/panels/agent-inspector-panel.ts +3 -3
  21. package/src/panels/agent-logs-panel.ts +3 -3
  22. package/src/panels/approval-panel.ts +2 -2
  23. package/src/panels/automation-control-panel.ts +3 -3
  24. package/src/panels/base-panel.ts +14 -14
  25. package/src/panels/builtin/operations.ts +1 -1
  26. package/src/panels/builtin/session.ts +1 -1
  27. package/src/panels/builtin/shared.ts +3 -3
  28. package/src/panels/cockpit-panel.ts +2 -2
  29. package/src/panels/communication-panel.ts +3 -3
  30. package/src/panels/context-visualizer-panel.ts +2 -2
  31. package/src/panels/control-plane-panel.ts +3 -3
  32. package/src/panels/cost-tracker-panel.ts +3 -3
  33. package/src/panels/debug-panel.ts +2 -2
  34. package/src/panels/diff-panel.ts +2 -2
  35. package/src/panels/docs-panel.ts +1 -1
  36. package/src/panels/eval-panel.ts +2 -2
  37. package/src/panels/file-explorer-panel.ts +3 -3
  38. package/src/panels/file-preview-panel.ts +3 -3
  39. package/src/panels/forensics-panel.ts +2 -2
  40. package/src/panels/git-panel.ts +1 -1
  41. package/src/panels/hooks-panel.ts +3 -3
  42. package/src/panels/incident-review-panel.ts +1 -1
  43. package/src/panels/intelligence-panel.ts +2 -2
  44. package/src/panels/knowledge-panel.ts +1 -1
  45. package/src/panels/local-auth-panel.ts +2 -2
  46. package/src/panels/marketplace-panel.ts +1 -1
  47. package/src/panels/mcp-panel.ts +3 -3
  48. package/src/panels/memory-panel.ts +1 -1
  49. package/src/panels/ops-control-panel.ts +3 -3
  50. package/src/panels/ops-strategy-panel.ts +2 -2
  51. package/src/panels/orchestration-panel.ts +2 -2
  52. package/src/panels/panel-list-panel.ts +6 -6
  53. package/src/panels/plan-dashboard-panel.ts +1 -1
  54. package/src/panels/plugins-panel.ts +2 -2
  55. package/src/panels/policy-panel.ts +2 -2
  56. package/src/panels/polish.ts +3 -3
  57. package/src/panels/provider-accounts-panel.ts +2 -2
  58. package/src/panels/provider-health-panel.ts +2 -2
  59. package/src/panels/provider-stats-panel.ts +3 -3
  60. package/src/panels/remote-panel.ts +3 -3
  61. package/src/panels/routes-panel.ts +3 -3
  62. package/src/panels/sandbox-panel.ts +2 -2
  63. package/src/panels/schedule-panel.ts +1 -1
  64. package/src/panels/security-panel.ts +2 -2
  65. package/src/panels/services-panel.ts +2 -2
  66. package/src/panels/session-browser-panel.ts +2 -2
  67. package/src/panels/settings-sync-panel.ts +2 -2
  68. package/src/panels/skills-panel.ts +6 -6
  69. package/src/panels/subscription-panel.ts +2 -2
  70. package/src/panels/symbol-outline-panel.ts +3 -3
  71. package/src/panels/system-messages-panel.ts +4 -4
  72. package/src/panels/tasks-panel.ts +2 -2
  73. package/src/panels/thinking-panel.ts +3 -3
  74. package/src/panels/token-budget-panel.ts +1 -1
  75. package/src/panels/tool-inspector-panel.ts +3 -3
  76. package/src/panels/types.ts +5 -5
  77. package/src/panels/watchers-panel.ts +3 -3
  78. package/src/panels/welcome-panel.ts +1 -1
  79. package/src/panels/worktree-panel.ts +2 -2
  80. package/src/panels/wrfc-panel.ts +3 -3
  81. package/src/permissions/prompt.ts +3 -22
  82. package/src/plugins/loader.ts +15 -304
  83. package/src/renderer/agent-detail-modal.ts +1 -1
  84. package/src/renderer/autocomplete-overlay.ts +2 -2
  85. package/src/renderer/bookmark-modal.ts +1 -1
  86. package/src/renderer/bottom-bar.ts +2 -2
  87. package/src/renderer/buffer.ts +1 -1
  88. package/src/renderer/code-block.ts +2 -2
  89. package/src/renderer/compositor.ts +2 -2
  90. package/src/renderer/context-inspector.ts +1 -1
  91. package/src/renderer/conversation-layout.ts +2 -2
  92. package/src/renderer/conversation-overlays.ts +1 -1
  93. package/src/renderer/conversation-surface.ts +2 -2
  94. package/src/renderer/diff-view.ts +2 -2
  95. package/src/renderer/diff.ts +1 -1
  96. package/src/renderer/file-picker-overlay.ts +2 -2
  97. package/src/renderer/file-tree.ts +2 -2
  98. package/src/renderer/help-overlay.ts +1 -1
  99. package/src/renderer/history-search-overlay.ts +2 -2
  100. package/src/renderer/live-tail-modal.ts +1 -1
  101. package/src/renderer/markdown.ts +2 -2
  102. package/src/renderer/modal-factory.ts +3 -3
  103. package/src/renderer/model-picker-overlay.ts +2 -2
  104. package/src/renderer/overlay-box.ts +2 -2
  105. package/src/renderer/panel-composite.ts +1 -1
  106. package/src/renderer/panel-picker-overlay.ts +2 -2
  107. package/src/renderer/panel-tab-bar.ts +1 -1
  108. package/src/renderer/panel-workspace-bar.ts +1 -1
  109. package/src/renderer/process-indicator.ts +2 -2
  110. package/src/renderer/process-modal.ts +1 -1
  111. package/src/renderer/profile-picker-modal.ts +2 -2
  112. package/src/renderer/progress.ts +2 -2
  113. package/src/renderer/search-overlay.ts +2 -2
  114. package/src/renderer/selection-modal-overlay.ts +2 -2
  115. package/src/renderer/session-picker-modal.ts +2 -2
  116. package/src/renderer/settings-modal.ts +2 -2
  117. package/src/renderer/shell-surface.ts +1 -1
  118. package/src/renderer/system-message.ts +1 -1
  119. package/src/renderer/tab-strip.ts +2 -2
  120. package/src/renderer/text-layout.ts +1 -1
  121. package/src/renderer/thinking.ts +1 -1
  122. package/src/renderer/tool-call.ts +2 -2
  123. package/src/renderer/ui-factory.ts +2 -2
  124. package/src/runtime/bootstrap-command-context.ts +4 -5
  125. package/src/runtime/bootstrap-command-parts.ts +1 -3
  126. package/src/runtime/bootstrap-core.ts +3 -2
  127. package/src/runtime/bootstrap-hook-bridge.ts +15 -174
  128. package/src/runtime/bootstrap-shell.ts +4 -4
  129. package/src/runtime/bootstrap.ts +1 -1
  130. package/src/runtime/context.ts +4 -20
  131. package/src/runtime/diagnostics/panels/index.ts +1 -1
  132. package/src/runtime/diagnostics/panels/ops.ts +1 -1
  133. package/src/runtime/diagnostics/panels/panel-resources.ts +118 -0
  134. package/src/runtime/perf/panel-contracts.ts +32 -0
  135. package/src/runtime/perf/panel-health-monitor.ts +18 -0
  136. package/src/runtime/services.ts +4 -4
  137. package/src/runtime/store/domains/conversation.ts +1 -181
  138. package/src/runtime/store/domains/permissions.ts +1 -143
  139. package/src/runtime/store/helpers/reducers/conversation.ts +1 -228
  140. package/src/runtime/store/helpers/reducers/lifecycle.ts +1 -440
  141. package/src/runtime/store/selectors/index.ts +11 -6
  142. package/src/runtime/store/state.ts +12 -4
  143. package/src/runtime/ui-events.ts +46 -0
  144. package/src/runtime/ui-services.ts +1 -1
  145. package/src/shell/ui-openers.ts +1 -1
  146. package/src/tools/index.ts +1 -186
  147. package/src/types/grid.ts +48 -0
  148. package/src/utils/clipboard.ts +21 -0
  149. package/src/utils/splash-lines.ts +1 -1
  150. package/src/utils/terminal-width.ts +185 -0
  151. package/src/version.ts +1 -1
  152. package/src/daemon/facade-composition.ts +0 -398
  153. package/src/daemon/facade.ts +0 -638
  154. package/src/daemon/surface-policy.ts +0 -60
  155. package/src/daemon/types.ts +0 -191
  156. package/src/runtime/ui-read-models-core.ts +0 -95
  157. package/src/runtime/ui-read-models-operations.ts +0 -203
@@ -1,15 +1,14 @@
1
- import { InfiniteBuffer } from '@pellux/goodvibes-sdk/platform/core/history';
2
- import { createEmptyLine, type Line, type Cell } from '@pellux/goodvibes-sdk/platform/types/grid';
3
- import type { SplashOptions } from '@pellux/goodvibes-sdk/platform/utils/splash-lines';
1
+ import { InfiniteBuffer } from './history.ts';
2
+ import { createEmptyLine, type Line, type Cell } from '../types/grid.ts';
3
+ import type { SplashOptions } from '../utils/splash-lines.ts';
4
4
  import type { ToolCall, ToolResult } from '@pellux/goodvibes-sdk/platform/types/tools';
5
5
  import type { ProviderMessage, ContentPart } from '@pellux/goodvibes-sdk/platform/providers/interface';
6
- import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
7
6
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
8
- import type { SessionMemoryStore } from '@pellux/goodvibes-sdk/platform/core/session-memory';
9
- import { SessionLineageTracker } from '@pellux/goodvibes-sdk/platform/core/session-lineage';
10
- import { buildTranscriptEventIndex } from '@pellux/goodvibes-sdk/platform/core/transcript-events/index';
11
7
  import type { TranscriptEventKind } from '@pellux/goodvibes-sdk/platform/core/transcript-events/index';
12
- import { compactConversation } from '@pellux/goodvibes-sdk/platform/core/conversation-compaction';
8
+ import {
9
+ ConversationManager as SdkConversationManager,
10
+ type BlockMeta as SdkBlockMeta,
11
+ } from '@pellux/goodvibes-sdk/platform/core/conversation';
13
12
  import {
14
13
  addConversationSplashScreen,
15
14
  appendConversationMessages,
@@ -21,64 +20,42 @@ import {
21
20
  renderConversationUserMessage,
22
21
  } from './conversation-rendering.ts';
23
22
  import { renderMarkdown } from '../renderer/markdown.ts';
24
- import {
25
- cloneBranchMap,
26
- cloneMessages,
27
- deriveConversationTitle,
28
- messagesToInternal,
29
- restoreBranchMap,
30
- } from '@pellux/goodvibes-sdk/platform/core/conversation-utils';
31
- import { applyDiffContent, parseDiffForApply } from '@pellux/goodvibes-sdk/platform/core/conversation-diff';
32
23
 
33
24
  /**
34
- * ConversationManager - Owns conversation messages and the rendered history buffer.
35
- * Supports tool-use messages (assistant with tool calls, tool results).
36
- *
37
- * History is rebuilt lazily: a dirty flag is set on every message mutation and
38
- * the buffer is only actually reconstructed when getDisplayBlocks() is called
39
- * or when the width changes. This avoids O(n) rebuilds per turn in long sessions.
25
+ * ConversationManager - TUI subclass of the SDK's ConversationManager.
26
+ * Adds InfiniteBuffer history, block registry, collapse state, width tracking,
27
+ * dirty flag, display methods, splash screen, error/event navigation, and
28
+ * Line[]-based rendering atop the SDK's message management.
40
29
  */
41
- export type TokenUsage = { inputTokens: number; outputTokens: number; cacheReadTokens?: number; cacheWriteTokens?: number };
42
30
 
43
- type AssistantMessage = { role: 'assistant'; content: string; toolCalls?: ToolCall[]; reasoningContent?: string; reasoningSummary?: string; usage?: TokenUsage; model?: string; provider?: string };
31
+ // Re-export SDK types for backward compatibility
32
+ export type {
33
+ TokenUsage,
34
+ ConversationMessageSnapshot,
35
+ ConversationTitleSource,
36
+ } from '@pellux/goodvibes-sdk/platform/core/conversation';
44
37
 
45
- export type ConversationMessageSnapshot =
46
- | { role: 'user'; content: string | ContentPart[]; cancelled?: boolean }
47
- | AssistantMessage
48
- | { role: 'system'; content: string }
49
- | { role: 'tool'; callId: string; content: string; toolName?: string };
50
-
51
- type Message = ConversationMessageSnapshot;
52
- export type ConversationTitleSource = 'system' | 'user';
38
+ export type { SdkBlockMeta };
53
39
 
54
- /** Metadata for a rendered block (code, tool, or diff). */
55
- export interface BlockMeta {
40
+ /** TUI extends the SDK BlockMeta with rendering position fields. */
41
+ export interface BlockMeta extends SdkBlockMeta {
56
42
  /** Index of this block (increments per renderable block). */
57
43
  blockIndex: number;
58
- /** Type of block content. */
59
- type: 'tool' | 'code' | 'diff' | 'thinking';
60
44
  /** First rendered line index in the history buffer. */
61
45
  startLine: number;
62
46
  /** Number of rendered lines (when not collapsed). */
63
47
  lineCount: number;
64
- /** Raw text content (code source, tool output, diff text). */
65
- rawContent: string;
66
48
  /** Stable key for collapse state persistence across rebuilds (e.g. msg_N). */
67
49
  collapseKey: string;
68
- /** File path for diff blocks. */
69
- filePath?: string;
70
- /** Parsed diff for apply: original/updated sections. */
71
- diffOriginal?: string;
72
- diffUpdated?: string;
73
50
  }
74
51
 
75
- export class ConversationManager {
52
+ // Import internal types needed for rendering helpers
53
+ import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core/conversation';
54
+ type Message = ConversationMessageSnapshot;
55
+
56
+ export class ConversationManager extends SdkConversationManager {
76
57
  public history = new InfiniteBuffer();
77
- /** Auto-generated or manually set conversation title. */
78
- private _title = '';
79
- private _titleSource: ConversationTitleSource = 'system';
80
- private messages: Message[] = [];
81
- private getWidth: () => number;
58
+ private _getWidth: () => number;
82
59
  /** Tracks the rendered width; a change invalidates the full history. */
83
60
  private lastRenderedWidth = 0;
84
61
  /** When true the buffer needs to be rebuilt before the next display. */
@@ -86,11 +63,7 @@ export class ConversationManager {
86
63
  /** Index of the first message not yet appended to the buffer. */
87
64
  private appendedUpTo = 0;
88
65
  /** Optional config manager for display settings. */
89
- private configManager: ConfigManager | null = null;
90
- /** Session memory store wired by the runtime composition root. */
91
- private sessionMemoryStore: Pick<SessionMemoryStore, 'list'> | null = null;
92
- /** Session lineage tracker wired by the runtime composition root. */
93
- private sessionLineageTracker: SessionLineageTracker = new SessionLineageTracker();
66
+ private _configManager: ConfigManager | null = null;
94
67
  /** Collapse state: stable key (msg_N) -> collapsed (true = collapsed). */
95
68
  private collapseState: Map<string, boolean> = new Map();
96
69
  /** Block registry: track rendered blocks for copy/apply. */
@@ -101,196 +74,96 @@ export class ConversationManager {
101
74
  private errorLineRegistry: number[] = [];
102
75
  /** Streaming block start line in history buffer (for incremental streaming update). */
103
76
  private streamingStartLine = -1;
104
- /** Undo stack: each entry is a turn (user msg + all subsequent non-user msgs until next user). */
105
- private undoStack: Message[][] = [];
106
- /** Branch storage: named snapshots of messages[]. */
107
- private branches: Map<string, Message[]> = new Map();
108
- /** Name of the currently active branch. */
109
- private currentBranch: string = 'main';
77
+
78
+ public suppressSplash: boolean = false;
79
+ public splashOptions: SplashOptions = {};
110
80
 
111
81
  constructor(
112
82
  getWidth: () => number = () => process.stdout.columns || 80,
113
83
  configManager?: ConfigManager,
114
84
  ) {
115
- this.getWidth = getWidth;
116
- this.configManager = configManager ?? null;
85
+ super();
86
+ this._getWidth = getWidth;
87
+ this._configManager = configManager ?? null;
117
88
  }
118
89
 
119
90
  /** Wire in a config manager after construction (e.g. from main.ts). */
120
91
  public setConfigManager(cm: ConfigManager): void {
121
- this.configManager = cm;
122
- }
123
-
124
- /** Wire in the session memory store used for compaction summaries. */
125
- public setSessionMemoryStore(store: Pick<SessionMemoryStore, 'list'>): void {
126
- this.sessionMemoryStore = store;
127
- }
128
-
129
- /** Wire in the session lineage tracker used for compaction output. */
130
- public setSessionLineageTracker(tracker: SessionLineageTracker): void {
131
- this.sessionLineageTracker = tracker;
132
- }
133
-
134
- /** Read the session memory store used for compaction summaries. */
135
- public getSessionMemoryStore(): Pick<SessionMemoryStore, 'list'> | null {
136
- return this.sessionMemoryStore;
137
- }
138
-
139
- /** Read the session lineage tracker used for compaction output. */
140
- public getSessionLineageTracker(): SessionLineageTracker {
141
- return this.sessionLineageTracker;
92
+ this._configManager = cm;
142
93
  }
143
94
 
144
95
  /** Update the width provider so shell layout can own transcript width. */
145
96
  public setWidthProvider(getWidth: () => number): void {
146
- this.getWidth = getWidth;
97
+ this._getWidth = getWidth;
147
98
  this.markDirty();
148
99
  }
149
100
 
150
- /** Returns messages formatted for LLM provider consumption. */
151
- public getMessagesForLLM(): ProviderMessage[] {
152
- const result: ProviderMessage[] = [];
153
- for (const m of this.messages) {
154
- if (m.role === 'system') continue; // System messages go via systemPrompt param
155
- if (m.role === 'user') {
156
- result.push({ role: 'user', content: m.content as string | ContentPart[] });
157
- } else if (m.role === 'assistant') {
158
- result.push({ role: 'assistant', content: m.content, toolCalls: m.toolCalls });
159
- } else if (m.role === 'tool') {
160
- result.push({ role: 'tool', callId: m.callId, content: m.content, ...(m.toolName ? { name: m.toolName } : {}) });
161
- }
162
- }
163
- return result;
164
- }
165
-
166
- private findToolName(callId: string): string | undefined {
167
- for (let i = this.messages.length - 1; i >= 0; i--) {
168
- const message = this.messages[i];
169
- if (message.role !== 'assistant' || !message.toolCalls?.length) continue;
170
- const match = message.toolCalls.find((call) => call.id === callId);
171
- if (match?.name) return match.name;
172
- }
173
- return undefined;
174
- }
101
+ // -------------------------------------------------------------------------
102
+ // Overrides: add markDirty() to message mutations
103
+ // -------------------------------------------------------------------------
175
104
 
176
- public addUserMessage(content: string | ContentPart[]): void {
177
- // Auto-generate title from first user message if not already set
178
- if (this._title === '' && typeof content === 'string' && content.trim().length > 0) {
179
- this.setSystemTitle(deriveConversationTitle(content));
180
- }
181
- this.messages.push({ role: 'user', content });
182
- // Clear undo stack when new user input is added (can't redo past new input)
183
- this.undoStack = [];
105
+ public override addUserMessage(content: string | ContentPart[]): void {
106
+ super.addUserMessage(content);
184
107
  this.markDirty();
185
108
  }
186
109
 
187
- /** Add an assistant message, optionally with tool calls (when the LLM invoked tools). */
188
- public addAssistantMessage(content: string, opts?: { toolCalls?: ToolCall[]; reasoningContent?: string; reasoningSummary?: string; usage?: TokenUsage; model?: string; provider?: string }): void {
189
- this.messages.push({ role: 'assistant', content, toolCalls: opts?.toolCalls, reasoningContent: opts?.reasoningContent, reasoningSummary: opts?.reasoningSummary, usage: opts?.usage, model: opts?.model, provider: opts?.provider });
190
- this.markDirty();
191
- }
192
-
193
- /** Add a batch of tool results after tool calls have been executed. */
194
- public addToolResults(results: ToolResult[]): void {
195
- for (const r of results) {
196
- const content = r.success
197
- ? (r.output ?? 'Tool completed successfully.')
198
- : `Error: ${r.error ?? 'Unknown error'}`;
199
- const toolName = this.findToolName(r.callId);
200
- this.messages.push({
201
- role: 'tool',
202
- callId: r.callId,
203
- content,
204
- ...(toolName ? { toolName } : {}),
205
- });
206
- }
110
+ public override addAssistantMessage(
111
+ content: string,
112
+ opts?: {
113
+ toolCalls?: ToolCall[];
114
+ reasoningContent?: string;
115
+ reasoningSummary?: string;
116
+ usage?: import('@pellux/goodvibes-sdk/platform/core/conversation').TokenUsage;
117
+ model?: string;
118
+ provider?: string;
119
+ },
120
+ ): void {
121
+ super.addAssistantMessage(content, opts);
207
122
  this.markDirty();
208
123
  }
209
124
 
210
- /**
211
- * undo - Remove the last complete turn (the last user message and all subsequent
212
- * non-user messages). Pushes the removed messages onto the undo stack.
213
- * Returns true if a turn was removed, false if there was nothing to undo.
214
- */
215
- public undo(): boolean {
216
- // Find the index of the last user message
217
- let lastUserIdx = -1;
218
- for (let i = this.messages.length - 1; i >= 0; i--) {
219
- if (this.messages[i].role === 'user') {
220
- lastUserIdx = i;
221
- break;
222
- }
223
- }
224
- if (lastUserIdx === -1) return false;
225
-
226
- // Collect the turn: user message + everything that follows (assistant, tool)
227
- const turn = this.messages.splice(lastUserIdx);
228
- this.undoStack.push(turn);
125
+ public override addToolResults(results: ToolResult[]): void {
126
+ super.addToolResults(results);
229
127
  this.markDirty();
230
- return true;
231
128
  }
232
129
 
233
- /**
234
- * redo - Restore the most recently undone turn.
235
- * Returns true if a turn was restored, false if the undo stack is empty.
236
- */
237
- public redo(): boolean {
238
- if (this.undoStack.length === 0) return false;
239
- const turn = this.undoStack.pop()!;
240
- this.messages.push(...turn);
130
+ public override addSystemMessage(content: string): void {
131
+ super.addSystemMessage(content);
241
132
  this.markDirty();
242
- return true;
243
133
  }
244
134
 
245
- /**
246
- * getLastUserMessage - Returns the content of the last user message, or null
247
- * if there are no user messages or the content is not a plain string.
248
- */
249
- public getLastUserMessage(): string | null {
250
- for (let i = this.messages.length - 1; i >= 0; i--) {
251
- if (this.messages[i].role === 'user') {
252
- const content = this.messages[i].content;
253
- return typeof content === 'string' ? content : null;
254
- }
255
- }
256
- return null;
257
- }
258
-
259
- /** Returns the current number of messages (for rollback tracking). */
260
- public getMessageCount(): number {
261
- return this.messages.length;
135
+ public override undo(): boolean {
136
+ const result = super.undo();
137
+ if (result) this.markDirty();
138
+ return result;
262
139
  }
263
140
 
264
- /** Remove all messages after the given index (for cancellation rollback). */
265
- public removeMessagesAfter(count: number): void {
266
- if (count < this.messages.length) {
267
- this.messages.length = count;
268
- this.markDirty();
269
- }
141
+ public override redo(): boolean {
142
+ const result = super.redo();
143
+ if (result) this.markDirty();
144
+ return result;
270
145
  }
271
146
 
272
- /** Mark the last user message as cancelled (red + strikethrough in display). */
273
- public markLastUserMessageCancelled(): void {
274
- for (let i = this.messages.length - 1; i >= 0; i--) {
275
- if (this.messages[i].role === 'user') {
276
- (this.messages[i] as { cancelled?: boolean }).cancelled = true;
277
- this.markDirty();
278
- return;
279
- }
280
- }
147
+ public override removeMessagesAfter(count: number): void {
148
+ super.removeMessagesAfter(count);
149
+ this.markDirty();
281
150
  }
282
151
 
283
- public addSystemMessage(content: string): void {
284
- this.messages.push({ role: 'system', content });
152
+ public override markLastUserMessageCancelled(): void {
153
+ super.markLastUserMessageCancelled();
285
154
  this.markDirty();
286
155
  }
287
156
 
157
+ // -------------------------------------------------------------------------
158
+ // Streaming overrides: add rendering tracking
159
+ // -------------------------------------------------------------------------
160
+
288
161
  /**
289
162
  * startStreamingBlock - Add a placeholder assistant message for incremental display.
290
163
  * Called when streaming begins.
291
164
  */
292
- public startStreamingBlock(): void {
293
- this.messages.push({ role: 'assistant', content: '' });
165
+ public override startStreamingBlock(): void {
166
+ super.startStreamingBlock();
294
167
  this.markDirty();
295
168
  // Record the line where the streaming block starts so updates can be incremental
296
169
  this.flushHistory();
@@ -302,19 +175,14 @@ export class ConversationManager {
302
175
  * Called per-delta during streaming. Does NOT trigger a full rebuild — instead it
303
176
  * directly updates the history buffer from streamingStartLine onward.
304
177
  */
305
- public updateStreamingBlock(content: string): void {
306
- for (let i = this.messages.length - 1; i >= 0; i--) {
307
- if (this.messages[i].role === 'assistant') {
308
- (this.messages[i] as { role: 'assistant'; content: string }).content = content;
309
- // Incrementally update the history buffer instead of full rebuild
310
- if (this.streamingStartLine >= 0) {
311
- const width = this.getWidth();
312
- this.history.truncateToLine(this.streamingStartLine);
313
- const rendered = renderMarkdown(content, width);
314
- this.history.addLines(rendered);
315
- }
316
- return;
317
- }
178
+ public override updateStreamingBlock(content: string): void {
179
+ super.updateStreamingBlock(content);
180
+ // Incrementally update the history buffer instead of full rebuild
181
+ if (this.streamingStartLine >= 0) {
182
+ const width = this._getWidth();
183
+ this.history.truncateToLine(this.streamingStartLine);
184
+ const rendered = renderMarkdown(content, width);
185
+ this.history.addLines(rendered);
318
186
  }
319
187
  }
320
188
 
@@ -322,17 +190,90 @@ export class ConversationManager {
322
190
  * finalizeStreamingBlock - Remove the streaming placeholder.
323
191
  * The orchestrator calls addAssistantMessage immediately after with the final content.
324
192
  */
325
- public finalizeStreamingBlock(): void {
326
- for (let i = this.messages.length - 1; i >= 0; i--) {
327
- if (this.messages[i].role === 'assistant') {
328
- this.messages.splice(i, 1);
329
- break;
330
- }
331
- }
193
+ public override finalizeStreamingBlock(): void {
194
+ super.finalizeStreamingBlock();
332
195
  this.streamingStartLine = -1;
333
196
  this.markDirty();
334
197
  }
335
198
 
199
+ // -------------------------------------------------------------------------
200
+ // Overrides: reset / replace / branch operations that also affect display
201
+ // -------------------------------------------------------------------------
202
+
203
+ /**
204
+ * resetAll - Clear both the display buffer and all conversation messages.
205
+ * This is a full reset; the LLM context is wiped.
206
+ */
207
+ public override resetAll(): void {
208
+ super.resetAll();
209
+ this.history.clear();
210
+ this.appendedUpTo = 0;
211
+ this.lastRenderedWidth = 0;
212
+ this.dirty = true;
213
+ this.collapseState.clear();
214
+ this.blockRegistry = [];
215
+ this.messageLineRegistry = [];
216
+ this.errorLineRegistry = [];
217
+ this.streamingStartLine = -1;
218
+ }
219
+
220
+ /**
221
+ * replaceMessagesForLLM - Replace the conversation's LLM-visible messages with a new set.
222
+ * Used by small-window compaction to swap in truncated messages without an LLM call.
223
+ * System messages are always preserved at the front.
224
+ *
225
+ * @param newMessages - Replacement ProviderMessage array (user/assistant/tool roles only)
226
+ */
227
+ public override replaceMessagesForLLM(newMessages: ProviderMessage[]): void {
228
+ super.replaceMessagesForLLM(newMessages);
229
+ this.history.clear();
230
+ this.appendedUpTo = 0;
231
+ this.lastRenderedWidth = 0;
232
+ this.dirty = true;
233
+ }
234
+
235
+ /**
236
+ * switchBranch - Replace the active messages with the stored branch snapshot.
237
+ * Returns true on success, false if the branch does not exist.
238
+ */
239
+ public override switchBranch(name: string): boolean {
240
+ const result = super.switchBranch(name);
241
+ if (result) this.markDirty();
242
+ return result;
243
+ }
244
+
245
+ /**
246
+ * mergeBranch - Append all messages from the named branch that come after
247
+ * the fork point.
248
+ * Returns true on success, false if the branch does not exist.
249
+ */
250
+ public override mergeBranch(name: string): boolean {
251
+ const result = super.mergeBranch(name);
252
+ if (result) this.markDirty();
253
+ return result;
254
+ }
255
+
256
+ /**
257
+ * fromJSON - Restore conversation from persisted data.
258
+ */
259
+ public override fromJSON(data: {
260
+ messages: Message[];
261
+ branches?: Record<string, Message[]>;
262
+ currentBranch?: string;
263
+ title?: string;
264
+ titleSource?: import('@pellux/goodvibes-sdk/platform/core/conversation').ConversationTitleSource;
265
+ }): void {
266
+ super.fromJSON(data);
267
+ this.history.clear();
268
+ this.appendedUpTo = 0;
269
+ this.lastRenderedWidth = 0;
270
+ this.dirty = true;
271
+ }
272
+
273
+ // -------------------------------------------------------------------------
274
+ // TUI-only display methods
275
+ // -------------------------------------------------------------------------
276
+
336
277
  public getDisplayBlocks(): Line[] {
337
278
  this.flushHistory();
338
279
  return this.history.getAllLines();
@@ -348,14 +289,15 @@ export class ConversationManager {
348
289
  this.blockRegistry = [];
349
290
  this.messageLineRegistry = [];
350
291
  this.errorLineRegistry = [];
351
- const width = this.getWidth();
292
+ const width = this._getWidth();
352
293
  this.lastRenderedWidth = width;
353
294
  this.dirty = false;
354
295
 
355
296
  // Tool messages ARE rendered (as collapsed blocks); this filter is only
356
297
  // for determining whether to show the splash screen (tool-only messages
357
298
  // don't count as visible conversation content for splash purposes).
358
- const displayMessages = this.messages.filter(
299
+ const snapshot = this.getMessageSnapshot();
300
+ const displayMessages = snapshot.filter(
359
301
  (m) => m.role !== 'tool' && m.role !== 'system',
360
302
  );
361
303
 
@@ -364,8 +306,8 @@ export class ConversationManager {
364
306
  return;
365
307
  }
366
308
 
367
- this.appendMessages(this.messages, width);
368
- this.appendedUpTo = this.messages.length;
309
+ this.appendMessages(snapshot, width);
310
+ this.appendedUpTo = snapshot.length;
369
311
  }
370
312
 
371
313
  /**
@@ -373,7 +315,7 @@ export class ConversationManager {
373
315
  * Falls back to a full rebuild when the terminal width has changed.
374
316
  */
375
317
  public flushHistory(): void {
376
- const currentWidth = this.getWidth();
318
+ const currentWidth = this._getWidth();
377
319
  if (!this.dirty && currentWidth === this.lastRenderedWidth) return;
378
320
  this.rebuildHistory();
379
321
  }
@@ -388,7 +330,7 @@ export class ConversationManager {
388
330
  blockRegistry: this.blockRegistry,
389
331
  collapseState: this.collapseState,
390
332
  errorLineRegistry: this.errorLineRegistry,
391
- configManager: this.configManager,
333
+ configManager: this._configManager,
392
334
  splashOptions: this.splashOptions,
393
335
  };
394
336
  }
@@ -547,28 +489,6 @@ export class ConversationManager {
547
489
  return before ?? lines[lines.length - 1]!;
548
490
  }
549
491
 
550
- public suppressSplash: boolean = false;
551
- public splashOptions: SplashOptions = {};
552
-
553
- public get title(): string {
554
- return this._title;
555
- }
556
-
557
- public set title(value: string) {
558
- this._title = String(value ?? '');
559
- this._titleSource = this._title.trim().length > 0 ? 'user' : 'system';
560
- }
561
-
562
- public getTitleSource(): ConversationTitleSource {
563
- return this._titleSource;
564
- }
565
-
566
- public setSystemTitle(value: string): void {
567
- if (this._titleSource === 'user') return;
568
- this._title = String(value ?? '');
569
- this._titleSource = 'system';
570
- }
571
-
572
492
  public setSplashSuppressed(suppressed: boolean): void {
573
493
  if (this.suppressSplash === suppressed) return;
574
494
  this.suppressSplash = suppressed;
@@ -584,7 +504,7 @@ export class ConversationManager {
584
504
  }
585
505
 
586
506
  public log(text: string, style: Partial<Cell> = {}, indent = ' '): void {
587
- logConversationText(this.renderingContext(), this.getWidth(), text, style, indent);
507
+ logConversationText(this.renderingContext(), this._getWidth(), text, style, indent);
588
508
  }
589
509
 
590
510
  /**
@@ -596,180 +516,13 @@ export class ConversationManager {
596
516
  this.appendedUpTo = 0;
597
517
  this.dirty = true;
598
518
  // Re-render from existing messages to rebuild buffer
599
- const width = this.getWidth();
519
+ const width = this._getWidth();
600
520
  this.lastRenderedWidth = width;
601
521
  this.dirty = false;
602
- this.appendMessages(this.messages, width);
603
- this.appendedUpTo = this.messages.length;
604
- }
605
-
606
- /**
607
- * resetAll - Clear both the display buffer and all conversation messages.
608
- * This is a full reset; the LLM context is wiped.
609
- */
610
- public resetAll(): void {
611
- this.messages = [];
612
- this._title = '';
613
- this._titleSource = 'system';
614
- this.undoStack = [];
615
- this.branches.clear();
616
- this.currentBranch = 'main';
617
- this.history.clear();
618
- this.appendedUpTo = 0;
619
- this.lastRenderedWidth = 0;
620
- this.dirty = true;
621
- this.collapseState.clear();
622
- this.blockRegistry = [];
623
- this.streamingStartLine = -1;
624
- }
625
-
626
- public getMessageSnapshot(): ConversationMessageSnapshot[] {
627
- return cloneMessages(this.messages);
628
- }
629
-
630
- public getTranscriptEventIndex() {
631
- return buildTranscriptEventIndex(this.getMessageSnapshot());
632
- }
633
-
634
- /**
635
- * replaceMessagesForLLM - Replace the conversation's LLM-visible messages with a new set.
636
- * Used by small-window compaction to swap in truncated messages without an LLM call.
637
- * System messages are always preserved at the front.
638
- *
639
- * @param newMessages - Replacement ProviderMessage array (user/assistant/tool roles only)
640
- */
641
- public replaceMessagesForLLM(newMessages: ProviderMessage[]): void {
642
- const originalSystemMessages = this.messages.filter(m => m.role === 'system');
643
- const convertedMessages = messagesToInternal(newMessages);
644
- this.messages = [...originalSystemMessages, ...convertedMessages];
645
- this.history.clear();
646
- this.appendedUpTo = 0;
647
- this.lastRenderedWidth = 0;
648
- this.dirty = true;
649
- }
650
-
651
- /**
652
- * compact - Reduce conversation state to a structured handoff payload.
653
- *
654
- * @param registry - Provider registry
655
- * @param modelId - Model to use for summarization
656
- * @param trigger - 'manual' (from /compact command) or 'auto' (from threshold check)
657
- * @param provider - Provider name for model disambiguation
658
- * @param context - Structured compaction context
659
- */
660
- public async compact(
661
- registry: import('@pellux/goodvibes-sdk/platform/providers/registry').ProviderRegistry,
662
- modelId: string,
663
- trigger: 'auto' | 'manual' = 'manual',
664
- provider?: string,
665
- context?: import('@pellux/goodvibes-sdk/platform/core/context-compaction').CompactionContext,
666
- ): Promise<void> {
667
- return compactConversation(this, registry, modelId, trigger, provider, context);
668
- }
669
-
670
- /**
671
- * forkBranch - Save a deep-copy of the current messages under a named branch.
672
- * If no name is provided a timestamp-based name is used.
673
- * Returns the name used.
674
- */
675
- public forkBranch(name?: string, force = false): string {
676
- const branchName = name?.trim() || `branch-${Date.now()}`;
677
- if (!force && this.branches.has(branchName)) {
678
- logger.warn(`forkBranch: branch '${branchName}' already exists; use force=true to overwrite`);
679
- }
680
- this.branches.set(branchName, cloneMessages(this.messages));
681
- return branchName;
682
- }
683
-
684
- /**
685
- * listBranches - Return the names and message counts of all saved branches.
686
- */
687
- public listBranches(): Array<{ name: string; messageCount: number; isCurrent: boolean }> {
688
- const result: Array<{ name: string; messageCount: number; isCurrent: boolean }> = [];
689
- // Always include current branch even if it hasn't been stored in the map yet
690
- const currentInMap = this.branches.has(this.currentBranch);
691
- if (!currentInMap) {
692
- result.push({ name: this.currentBranch, messageCount: this.messages.length, isCurrent: true });
693
- }
694
- for (const [name, msgs] of this.branches) {
695
- result.push({ name, messageCount: msgs.length, isCurrent: name === this.currentBranch });
696
- }
697
- return result;
698
- }
699
-
700
- /**
701
- * switchBranch - Replace the active messages with the stored branch snapshot.
702
- * Returns true on success, false if the branch does not exist.
703
- */
704
- public switchBranch(name: string): boolean {
705
- const stored = this.branches.get(name);
706
- if (!stored) return false;
707
- // Save current branch state before switching to prevent data loss
708
- this.branches.set(this.currentBranch, cloneMessages(this.messages));
709
- this.messages = cloneMessages(stored);
710
- this.currentBranch = name;
711
- this.undoStack = [];
712
- this.markDirty();
713
- return true;
714
- }
715
-
716
- /**
717
- * mergeBranch - Append all messages from the named branch that come after
718
- * the fork point (messages not already present in the current conversation).
719
- * Simple strategy: append all branch messages after current messages.
720
- * Returns true on success, false if the branch does not exist.
721
- */
722
- public mergeBranch(name: string): boolean {
723
- const stored = this.branches.get(name);
724
- if (!stored) return false;
725
- // Use length-based fork point detection: the branch was cloned from a known
726
- // snapshot so we use the shorter of the two lengths as the common prefix,
727
- // then append any messages the branch has beyond that point.
728
- const commonLen = Math.min(this.messages.length, stored.length);
729
- const toAppend = stored.slice(commonLen);
730
- if (toAppend.length === 0) return true;
731
- this.messages.push(...cloneMessages(toAppend));
732
- this.undoStack = [];
733
- this.markDirty();
734
- return true;
735
- }
736
-
737
- /** Returns the name of the currently active branch. */
738
- public getCurrentBranch(): string {
739
- return this.currentBranch;
740
- }
741
-
742
- /**
743
- * toJSON - Serialize conversation for persistence.
744
- */
745
- public toJSON(): object {
746
- // Serialize branches map as a plain object for persistence
747
- const branchesObj = cloneBranchMap(this.branches);
748
- return {
749
- messages: cloneMessages(this.messages),
750
- timestamp: Date.now(),
751
- title: this._title,
752
- titleSource: this._titleSource,
753
- branches: branchesObj,
754
- currentBranch: this.currentBranch,
755
- };
756
- }
757
-
758
- /**
759
- * fromJSON - Restore conversation from persisted data.
760
- */
761
- public fromJSON(data: { messages: Message[]; branches?: Record<string, Message[]>; currentBranch?: string; title?: string; titleSource?: ConversationTitleSource }): void {
762
- this.messages = data.messages ?? [];
763
- this._title = typeof data.title === 'string' ? data.title : '';
764
- this._titleSource = data.titleSource === 'user' || data.titleSource === 'system'
765
- ? data.titleSource
766
- : (this._title ? 'user' : 'system');
767
- this.branches = restoreBranchMap(data.branches);
768
- this.currentBranch = data.currentBranch ?? 'main';
769
- this.history.clear();
770
- this.appendedUpTo = 0;
771
- this.lastRenderedWidth = 0;
772
- this.dirty = true;
522
+ const snapshot = this.getMessageSnapshot();
523
+ this.appendMessages(snapshot, width);
524
+ this.appendedUpTo = snapshot.length;
773
525
  }
774
526
  }
527
+
775
528
  export { parseDiffForApply, applyDiffContent } from '@pellux/goodvibes-sdk/platform/core/conversation-diff';