ethagent 2.2.0 → 2.4.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 (168) hide show
  1. package/README.md +11 -0
  2. package/package.json +2 -1
  3. package/src/app/FirstRun.tsx +3 -7
  4. package/src/app/FirstRunTimeline.tsx +1 -1
  5. package/src/chat/ChatBottomPane.tsx +29 -11
  6. package/src/chat/ChatScreen.tsx +169 -38
  7. package/src/chat/ConversationStack.tsx +1 -1
  8. package/src/chat/MessageList.tsx +185 -72
  9. package/src/chat/SessionStatus.tsx +3 -1
  10. package/src/chat/chatScreenUtils.ts +11 -15
  11. package/src/chat/chatSessionState.ts +5 -2
  12. package/src/chat/chatTurnOrchestrator.ts +7 -9
  13. package/src/chat/commands.ts +26 -26
  14. package/src/chat/display/DiffView.tsx +193 -0
  15. package/src/chat/display/SyntaxText.tsx +192 -0
  16. package/src/chat/display/toolCallDisplay.ts +103 -0
  17. package/src/chat/display/toolResultDisplay.ts +19 -0
  18. package/src/chat/{ChatInput.tsx → input/ChatInput.tsx} +61 -25
  19. package/src/chat/input/imageRefs.ts +30 -0
  20. package/src/chat/{TranscriptView.tsx → transcript/TranscriptView.tsx} +24 -50
  21. package/src/chat/{transcriptViewport.ts → transcript/transcriptViewport.ts} +12 -30
  22. package/src/chat/{ContextLimitView.tsx → views/ContextLimitView.tsx} +3 -3
  23. package/src/chat/{ContinuityEditReviewView.tsx → views/ContinuityEditReviewView.tsx} +11 -3
  24. package/src/chat/{CopyPicker.tsx → views/CopyPicker.tsx} +4 -5
  25. package/src/chat/{PermissionPrompt.tsx → views/PermissionPrompt.tsx} +16 -17
  26. package/src/chat/{PermissionsView.tsx → views/PermissionsView.tsx} +6 -6
  27. package/src/chat/{PlanApprovalView.tsx → views/PlanApprovalView.tsx} +4 -4
  28. package/src/chat/{ResumeView.tsx → views/ResumeView.tsx} +50 -41
  29. package/src/chat/views/RewindView.tsx +410 -0
  30. package/src/identity/continuity/privateEdit/diff.ts +2 -78
  31. package/src/identity/hub/OperationalRoutes.tsx +21 -21
  32. package/src/identity/hub/Routes.tsx +13 -13
  33. package/src/identity/hub/{flows/continuity → continuity}/ContinuityDashboardScreen.tsx +9 -9
  34. package/src/identity/hub/{flows/continuity → continuity}/RebackupStorageScreen.tsx +2 -2
  35. package/src/identity/hub/{flows/continuity → continuity}/RecoveryConfirmScreen.tsx +5 -5
  36. package/src/identity/hub/{flows/continuity → continuity}/SavePromptScreen.tsx +5 -5
  37. package/src/identity/hub/{effects/rebackup/runRebackup.ts → continuity/effects.ts} +17 -17
  38. package/src/identity/hub/{effects/rebackup → continuity}/index.ts +1 -1
  39. package/src/identity/hub/{effects/shared → continuity}/snapshot.ts +8 -8
  40. package/src/identity/hub/{effects/rebackup → continuity}/vault.ts +15 -15
  41. package/src/identity/hub/{flows/create → create}/CreateFlow.tsx +13 -13
  42. package/src/identity/hub/{effects/create.ts → create/effects.ts} +4 -4
  43. package/src/identity/hub/{flows/custody → custody}/CustodyEditFlow.tsx +9 -9
  44. package/src/identity/hub/{flows/custody/custodyFlowActions.ts → custody/actions.ts} +6 -6
  45. package/src/identity/hub/{flows/custody/custodyFlowHelpers.ts → custody/helpers.ts} +4 -4
  46. package/src/identity/hub/{effects/vault → custody}/preflight.ts +5 -5
  47. package/src/identity/hub/{flows/custody/custodyFlowRoutes.tsx → custody/routes.tsx} +8 -8
  48. package/src/identity/hub/{flows/custody/custodyEffects.ts → custody/transactions.ts} +9 -9
  49. package/src/identity/hub/{flows/custody/custodyFlowTypes.ts → custody/types.ts} +5 -5
  50. package/src/identity/hub/{flows/custody/custodyFlowEffects.ts → custody/useCustodyEffects.ts} +7 -7
  51. package/src/identity/hub/{flows/custody → custody}/useCustodyFlow.tsx +5 -5
  52. package/src/identity/hub/{flows/ens → ens}/EnsEditAdvancedScreens.tsx +13 -13
  53. package/src/identity/hub/{flows/ens → ens}/EnsEditFlow.tsx +7 -7
  54. package/src/identity/hub/{flows/ens → ens}/EnsEditMaintenanceScreens.tsx +10 -10
  55. package/src/identity/hub/{flows/ens → ens}/EnsEditReviewScreens.tsx +12 -12
  56. package/src/identity/hub/{flows/ens → ens}/EnsEditRunners.tsx +5 -5
  57. package/src/identity/hub/{flows/ens → ens}/EnsEditShared.tsx +10 -10
  58. package/src/identity/hub/{flows/ens → ens}/EnsEditSimpleScreens.tsx +14 -14
  59. package/src/identity/hub/{flows/ens/IdentityHubEnsFlow.tsx → ens/EnsFlow.tsx} +12 -12
  60. package/src/identity/hub/{flows/ens/OperatorWalletsScreen.tsx → ens/EnsOperatorWalletsScreen.tsx} +17 -17
  61. package/src/identity/hub/{advancedEnsValidation.ts → ens/advancedEnsValidation.ts} +2 -2
  62. package/src/identity/hub/{flows/ens/ensEditCopy.ts → ens/editCopy.ts} +3 -3
  63. package/src/identity/hub/{effects/ens/flows.ts → ens/effects.ts} +7 -7
  64. package/src/identity/hub/{effects/ens → ens}/index.ts +1 -1
  65. package/src/identity/hub/{model/ens.ts → ens/state.ts} +1 -1
  66. package/src/identity/hub/{effects/ens → ens}/transactions.ts +239 -239
  67. package/src/identity/hub/{flows/ens/ensEditTypes.ts → ens/types.ts} +7 -7
  68. package/src/identity/hub/identityHubReducer.ts +3 -3
  69. package/src/identity/hub/{flows/profile → profile}/EditProfileFlow.tsx +11 -11
  70. package/src/identity/hub/{effects/publicProfile/runPublicProfileSave.ts → profile/effects.ts} +18 -18
  71. package/src/identity/hub/{model → profile}/identity.ts +3 -3
  72. package/src/identity/hub/{effects/profile/profileState.ts → profile/state.ts} +181 -181
  73. package/src/identity/hub/{flows/restore → restore}/RestoreFlow.tsx +16 -16
  74. package/src/identity/hub/{effects/restore → restore}/apply.ts +10 -10
  75. package/src/identity/hub/{effects/restore → restore}/auth.ts +7 -7
  76. package/src/identity/hub/{effects/restore → restore}/discover.ts +6 -6
  77. package/src/identity/hub/{effects/restore → restore}/envelopes.ts +2 -2
  78. package/src/identity/hub/{effects/restore → restore}/fetch.ts +3 -3
  79. package/src/identity/hub/{effects/restore/shared.ts → restore/helpers.ts} +6 -6
  80. package/src/identity/hub/{effects/restore → restore}/recovery.ts +10 -10
  81. package/src/identity/hub/{effects/restore → restore}/resolve.ts +4 -4
  82. package/src/identity/hub/{effects → restore}/restoreAdmin.ts +1 -1
  83. package/src/identity/hub/{flows/restore/useRestoreFlowEffects.ts → restore/useRestoreEffects.ts} +5 -5
  84. package/src/identity/hub/{flows/settings → settings}/StorageCredentialScreen.tsx +5 -5
  85. package/src/identity/hub/{components → shared/components}/BusyScreen.tsx +4 -4
  86. package/src/identity/hub/{components → shared/components}/DetailsScreen.tsx +4 -4
  87. package/src/identity/hub/{components → shared/components}/ErrorScreen.tsx +4 -4
  88. package/src/identity/hub/{components → shared/components}/FlowTimeline.tsx +1 -1
  89. package/src/identity/hub/{components → shared/components}/IdentitySummary.tsx +8 -8
  90. package/src/identity/hub/{components → shared/components}/MenuScreen.tsx +7 -7
  91. package/src/identity/hub/{components → shared/components}/NetworkScreen.tsx +4 -4
  92. package/src/identity/hub/{components → shared/components}/PinataJwtInput.tsx +4 -4
  93. package/src/identity/hub/{components → shared/components}/UnlinkedIdentityScreen.tsx +5 -5
  94. package/src/identity/hub/{components → shared/components}/WalletApprovalScreen.tsx +6 -6
  95. package/src/identity/hub/{components → shared/components}/menuFlagsFromReconciliation.ts +1 -1
  96. package/src/identity/hub/{effects/shared → shared/effects}/profilePrep.ts +1 -1
  97. package/src/identity/hub/{effects → shared/effects}/receipts.ts +2 -2
  98. package/src/identity/hub/{effects/shared → shared/effects}/sync.ts +4 -4
  99. package/src/identity/hub/{effects → shared/effects}/types.ts +3 -3
  100. package/src/identity/hub/{model → shared/model}/copy.ts +2 -2
  101. package/src/identity/hub/{model → shared/model}/errors.ts +5 -5
  102. package/src/identity/hub/{model → shared/model}/network.ts +3 -3
  103. package/src/identity/hub/{operatorWallets.ts → shared/operatorWallets.ts} +1 -1
  104. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/hook.ts +1 -1
  105. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/ownership.ts +2 -2
  106. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/run.ts +6 -6
  107. package/src/identity/hub/{utils.ts → shared/utils.ts} +5 -5
  108. package/src/identity/hub/{flows/token-transfer/IdentityHubTokenTransferFlow.tsx → transfer/TokenTransferFlow.tsx} +8 -8
  109. package/src/identity/hub/{flows/token-transfer → transfer}/TokenTransferScreens.tsx +14 -14
  110. package/src/identity/hub/{effects/token-transfer/runTokenTransfer.ts → transfer/effects.ts} +16 -16
  111. package/src/identity/hub/{effects/token-transfer → transfer}/progress.ts +1 -1
  112. package/src/identity/hub/useIdentityHubController.ts +11 -11
  113. package/src/identity/hub/useIdentityHubSideEffects.ts +11 -11
  114. package/src/models/ModelPicker.tsx +143 -9
  115. package/src/models/catalog.ts +2 -1
  116. package/src/models/huggingface.ts +180 -2
  117. package/src/models/llamacpp.ts +110 -15
  118. package/src/models/llamacppPreflight.ts +30 -11
  119. package/src/models/modelPickerOptions.ts +16 -15
  120. package/src/models/providerDisplay.ts +16 -0
  121. package/src/providers/anthropic.ts +36 -5
  122. package/src/providers/contracts.ts +9 -1
  123. package/src/providers/errors.ts +6 -4
  124. package/src/providers/gemini.ts +29 -3
  125. package/src/providers/openai-chat.ts +83 -3
  126. package/src/providers/openai-responses-format.ts +29 -8
  127. package/src/providers/openai-responses.ts +22 -7
  128. package/src/providers/registry.ts +1 -0
  129. package/src/runtime/sessionMode.ts +1 -1
  130. package/src/runtime/systemPrompt.ts +3 -1
  131. package/src/runtime/toolExecution.ts +9 -6
  132. package/src/runtime/turn.ts +29 -0
  133. package/src/storage/config.ts +1 -0
  134. package/src/storage/rewind.ts +20 -0
  135. package/src/storage/sessions.ts +16 -3
  136. package/src/tools/bashSafety.ts +7 -3
  137. package/src/tools/bashTool.ts +1 -1
  138. package/src/tools/contracts.ts +3 -0
  139. package/src/tools/deleteFileTool.ts +8 -3
  140. package/src/tools/editTool.ts +10 -5
  141. package/src/tools/fileDiff.ts +261 -0
  142. package/src/tools/privateContinuityEditTool.ts +5 -1
  143. package/src/tools/writeFileTool.ts +8 -3
  144. package/src/ui/Spinner.tsx +39 -5
  145. package/src/ui/TextInput.tsx +2 -2
  146. package/src/ui/theme.ts +19 -0
  147. package/src/utils/clipboard.ts +10 -7
  148. package/src/utils/images.ts +140 -0
  149. package/src/utils/messages.ts +2 -0
  150. package/src/chat/RewindView.tsx +0 -386
  151. package/src/chat/toolResultDisplay.ts +0 -8
  152. package/src/identity/hub/effects/index.ts +0 -73
  153. package/src/identity/hub/effects/publicProfile/index.ts +0 -5
  154. package/src/identity/hub/effects/restore/restoreEffects.ts +0 -22
  155. package/src/identity/hub/effects/token-transfer/index.ts +0 -6
  156. /package/src/chat/{chatInputState.ts → input/chatInputState.ts} +0 -0
  157. /package/src/chat/{chatPaste.ts → input/chatPaste.ts} +0 -0
  158. /package/src/chat/{textCursor.ts → input/textCursor.ts} +0 -0
  159. /package/src/identity/hub/{model/continuity.ts → continuity/state.ts} +0 -0
  160. /package/src/identity/hub/{model/custody.ts → custody/state.ts} +0 -0
  161. /package/src/identity/hub/{effects/restore → restore}/index.ts +0 -0
  162. /package/src/identity/hub/{model → shared/model}/format.ts +0 -0
  163. /package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/types.ts +0 -0
  164. /package/src/identity/hub/{reconciliation → shared/reconciliation}/index.ts +0 -0
  165. /package/src/identity/hub/{reconciliation → shared/reconciliation}/useAgentReconciliation.ts +0 -0
  166. /package/src/identity/hub/{reconciliation → shared/reconciliation}/walletSetup.ts +0 -0
  167. /package/src/identity/hub/{txGuard.ts → shared/txGuard.ts} +0 -0
  168. /package/src/identity/hub/{model/transfer.ts → transfer/state.ts} +0 -0
package/README.md CHANGED
@@ -112,6 +112,17 @@ ethagent works with OpenAI, Anthropic, Gemini, and local GGUF models served thro
112
112
  - The featured local model is [Qwen3.5-9B-Uncensored](https://huggingface.co/HauhauCS/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive); other Hugging Face GGUF models work by repo ID or direct URL.
113
113
  - Cloud API keys live in the OS keyring when one is available, with an encrypted local file under `~/.ethagent` as fallback.
114
114
 
115
+ ### Image Input
116
+
117
+ Press `Alt+V` to paste an image from the clipboard. A marker like `[Image #1]` appears in the prompt; delete it to drop the attachment.
118
+
119
+ Vision support is available on:
120
+
121
+ - **OpenAI** (Chat Completions and Responses API): `gpt-4o`, `gpt-4.1`, `gpt-4-turbo`, `gpt-4-vision`, `gpt-5`, `o1`, `o3`, `o4`, `chatgpt-4`.
122
+ - **Anthropic**: `claude-3`, `claude-sonnet-4`, `claude-opus-4`, `claude-haiku-4`.
123
+ - **Gemini**: `gemini-1.5`, `gemini-2.0`, `gemini-2.5`.
124
+ - **Local llama.cpp**: vision works when both the main GGUF and a `mmproj-*.gguf` projector are loaded. The picker recommends the bundle during install; if you skipped, open `Alt+P` and any installed model with a vision encoder available shows an `Add Vision Encoder` row directly beneath it.
125
+
115
126
  ## Tools and Sessions
116
127
 
117
128
  - File ops, shell, clipboard, and MCP tools all run through the same permission layer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ethagent",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "A privacy-first AI agent with a portable Ethereum identity",
5
5
  "type": "module",
6
6
  "main": "bin/ethagent.js",
@@ -50,6 +50,7 @@
50
50
  "@noble/hashes": "^1.8.0",
51
51
  "@noble/post-quantum": "^0.6.1",
52
52
  "ajv": "^8.18.0",
53
+ "cli-highlight": "^2.1.11",
53
54
  "esbuild": "^0.27.7",
54
55
  "ink": "^6.8.0",
55
56
  "react": "^19.2.4",
@@ -7,6 +7,7 @@ import { TextInput } from '../ui/TextInput.js'
7
7
  import { theme } from '../ui/theme.js'
8
8
  import { ModelPicker, type ModelPickerSelection } from '../models/ModelPicker.js'
9
9
  import { formatModelDisplayName } from '../models/modelDisplay.js'
10
+ import { providerDisplayName } from '../models/providerDisplay.js'
10
11
  import { detectSpec, type SpecSnapshot } from '../models/runtimeDetection.js'
11
12
  import { FEATURED_HF_REPO_URL } from '../models/modelRecommendation.js'
12
13
  import {
@@ -434,6 +435,7 @@ function configFromModelPickerSelection(selection: ModelPickerSelection, base: E
434
435
  provider: 'llamacpp',
435
436
  model: selection.model,
436
437
  baseUrl: defaultBaseUrlFor('llamacpp'),
438
+ localMmprojPath: selection.mmprojPath,
437
439
  }
438
440
  }
439
441
  return {
@@ -441,6 +443,7 @@ function configFromModelPickerSelection(selection: ModelPickerSelection, base: E
441
443
  provider: selection.provider,
442
444
  model: selection.model,
443
445
  baseUrl: undefined,
446
+ localMmprojPath: undefined,
444
447
  }
445
448
  }
446
449
 
@@ -449,10 +452,3 @@ function formatGB(bytes: number): string {
449
452
  return gb < 10 ? `${gb.toFixed(1)}GB` : `${Math.round(gb)}GB`
450
453
  }
451
454
 
452
- function providerDisplayName(provider: ProviderId): string {
453
- if (provider === 'openai') return 'OpenAI'
454
- if (provider === 'anthropic') return 'Anthropic'
455
- if (provider === 'gemini') return 'Gemini'
456
- if (provider === 'llamacpp') return 'llama.cpp'
457
- return provider
458
- }
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { FlowTimeline } from '../identity/hub/components/FlowTimeline.js'
2
+ import { FlowTimeline } from '../identity/hub/shared/components/FlowTimeline.js'
3
3
 
4
4
  export const FIRST_RUN_STAGES = ['Inspect', 'Identity', 'Model'] as const
5
5
 
@@ -3,26 +3,27 @@ import type { EthagentConfig } from '../storage/config.js'
3
3
  import type { PermissionDecision, PermissionRequest, SessionPermissionRule } from '../tools/contracts.js'
4
4
  import { type ModelPickerSelection, ModelPicker } from '../models/ModelPicker.js'
5
5
  import type { ModelPickerContextFit } from '../models/modelPickerOptions.js'
6
- import { ResumeView } from './ResumeView.js'
7
- import { RewindView } from './RewindView.js'
8
- import { PermissionsView } from './PermissionsView.js'
9
- import { CopyPicker } from './CopyPicker.js'
10
- import { PermissionPrompt } from './PermissionPrompt.js'
11
- import { PlanApprovalView, type PlanApprovalAction } from './PlanApprovalView.js'
12
- import { ChatInput } from './ChatInput.js'
6
+ import { ResumeView } from './views/ResumeView.js'
7
+ import { RewindView } from './views/RewindView.js'
8
+ import { PermissionsView } from './views/PermissionsView.js'
9
+ import { CopyPicker } from './views/CopyPicker.js'
10
+ import { PermissionPrompt } from './views/PermissionPrompt.js'
11
+ import { PlanApprovalView, type PlanApprovalAction } from './views/PlanApprovalView.js'
12
+ import { ChatInput } from './input/ChatInput.js'
13
13
  import { IdentityHub, type IdentityHubInitialAction, type IdentityHubResult } from '../identity/hub/IdentityHub.js'
14
14
  import type { CopyResult } from '../utils/clipboard.js'
15
15
  import { getSlashSuggestions } from './commands.js'
16
+ import { modelSupportsImages } from '../utils/images.js'
16
17
  import { Box, Text } from 'ink'
17
18
  import { theme } from '../ui/theme.js'
18
19
  import { Spinner } from '../ui/Spinner.js'
19
- import { ContextLimitView, type ContextLimitAction } from './ContextLimitView.js'
20
+ import { ContextLimitView, type ContextLimitAction } from './views/ContextLimitView.js'
20
21
  import type { ContextUsage } from '../runtime/compaction.js'
21
22
  import {
22
23
  ContinuityEditReviewView,
23
24
  type ContinuityEditReviewAction,
24
25
  type ContinuityEditReviewState,
25
- } from './ContinuityEditReviewView.js'
26
+ } from './views/ContinuityEditReviewView.js'
26
27
 
27
28
  export type Overlay = 'none' | 'modelPicker' | 'resume' | 'rewind' | 'copyPicker' | 'permission' | 'permissions' | 'planApproval' | 'identity' | 'contextLimit' | 'continuityEditReview'
28
29
  export type CopyPickerState = { turnText: string; turnLabel: string } | null
@@ -66,7 +67,10 @@ type ChatBottomPaneProps = {
66
67
  handleResumeClearAll: () => void | Promise<void>
67
68
  identityOverlay: IdentityOverlayState | null
68
69
  handleIdentityResult: (result: IdentityHubResult) => void
69
- handleRestoreConversation: (turnId: string) => void
70
+ handleRestoreConversation: (turnId: string, promptText?: string) => void
71
+ pendingInputDraft: string | null
72
+ onInputDraftConsumed: () => void
73
+ handleSummarizeFromTurn: (turnId: string) => void | Promise<unknown>
70
74
  handleCopyDone: (result: CopyResult, label: string) => void
71
75
  handleCopyCancel: () => void
72
76
  resolvePermission: (decision: PermissionDecision) => void
@@ -110,6 +114,9 @@ export function ChatBottomPane({
110
114
  identityOverlay,
111
115
  handleIdentityResult,
112
116
  handleRestoreConversation,
117
+ handleSummarizeFromTurn,
118
+ pendingInputDraft,
119
+ onInputDraftConsumed,
113
120
  handleCopyDone,
114
121
  handleCopyCancel,
115
122
  resolvePermission,
@@ -155,6 +162,7 @@ export function ChatBottomPane({
155
162
  cwd={cwd}
156
163
  currentSessionId={currentSessionId}
157
164
  onRestoreConversation={handleRestoreConversation}
165
+ onSummarizeFromTurn={handleSummarizeFromTurn}
158
166
  onDone={(message, variant = 'info') => {
159
167
  setOverlay('none')
160
168
  pushNote(message, variant)
@@ -261,10 +269,20 @@ export function ChatBottomPane({
261
269
  slashSuggestions={slashSuggestions}
262
270
  footerRight={footerRight}
263
271
  cwd={cwd}
272
+ seedText={pendingInputDraft}
273
+ onSeedConsumed={onInputDraftConsumed}
274
+ onImagePaste={() => {
275
+ if (!modelSupportsImages(config.provider, config.model, { mmprojPath: config.localMmprojPath })) {
276
+ const hint = config.provider === 'llamacpp'
277
+ ? ' · run "Add Vision Encoder" in alt+p to enable image input on this model'
278
+ : ' · switch via alt+p'
279
+ pushNote(`current model "${config.model}" does not accept image input${hint}`, 'error')
280
+ }
281
+ }}
264
282
  />
265
283
  <Box marginLeft={2} marginTop={0} flexDirection="column">
266
284
  <Text>
267
- <Text color={theme.dim}>workspace · </Text>
285
+ <Text color={theme.dim}>Workspace · </Text>
268
286
  <Text color={theme.textSubtle}>{cwd}</Text>
269
287
  </Text>
270
288
  </Box>
@@ -16,6 +16,7 @@ import { theme } from '../ui/theme.js'
16
16
  import { BrandSplash } from '../ui/BrandSplash.js'
17
17
  import { SessionStatus, formatTokens } from './SessionStatus.js'
18
18
  import { formatModelDisplayName } from '../models/modelDisplay.js'
19
+ import { providerDisplayName } from '../models/providerDisplay.js'
19
20
  import { toggleReasoningRow, type MessageRow } from './MessageList.js'
20
21
  import { ConversationStack } from './ConversationStack.js'
21
22
  import type { ModelPickerSelection } from '../models/ModelPicker.js'
@@ -53,6 +54,7 @@ import type {
53
54
  PermissionRequest,
54
55
  SessionPermissionRule,
55
56
  } from '../tools/contracts.js'
57
+ import { splitFileChangeResult } from '../tools/fileDiff.js'
56
58
  import {
57
59
  buildBaseMessages,
58
60
  sessionMessagesToRows,
@@ -63,7 +65,7 @@ import { setTokenIdentity, getIdentityStatus } from '../storage/identity.js'
63
65
  import type { IdentityHubResult } from '../identity/hub/IdentityHub.js'
64
66
  import { continuityWorkingTreeStatus } from '../identity/continuity/storage.js'
65
67
  import { listPublishedContinuitySnapshots } from '../identity/continuity/snapshots.js'
66
- import { localChangeStatusView } from '../identity/hub/model/continuity.js'
68
+ import { localChangeStatusView } from '../identity/hub/continuity/state.js'
67
69
  import {
68
70
  buildResumedSessionState,
69
71
  promptHistoryFromSessionMessages,
@@ -72,9 +74,9 @@ import {
72
74
  } from './chatSessionState.js'
73
75
  import { runStreamingTurn } from './chatTurnOrchestrator.js'
74
76
  import { ensureLlamaCppRunnerReady } from '../models/llamacppPreflight.js'
75
- import type { PlanApprovalAction } from './PlanApprovalView.js'
76
- import type { ContextLimitAction } from './ContextLimitView.js'
77
- import type { ContinuityEditReviewAction, ContinuityEditReviewState } from './ContinuityEditReviewView.js'
77
+ import type { PlanApprovalAction } from './views/PlanApprovalView.js'
78
+ import type { ContextLimitAction } from './views/ContextLimitView.js'
79
+ import type { ContinuityEditReviewAction, ContinuityEditReviewState } from './views/ContinuityEditReviewView.js'
78
80
  import { openFileInEditor } from '../identity/continuity/editor.js'
79
81
  import { EMPTY_MCP_SNAPSHOT, McpManager, type McpSnapshot } from '../mcp/manager.js'
80
82
 
@@ -154,6 +156,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
154
156
  contextUsageFromTokens(0, initialConfig.provider, initialConfig.model),
155
157
  )
156
158
  const [mcpSnapshot, setMcpSnapshot] = useState<McpSnapshot>(EMPTY_MCP_SNAPSHOT)
159
+ const [pendingInputDraft, setPendingInputDraft] = useState<string | null>(null)
157
160
 
158
161
  const rowsRef = useRef<MessageRow[]>([])
159
162
  const visibleReasoningIdsRef = useRef<string[]>([])
@@ -380,7 +383,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
380
383
  setModelPickerContextFit(null)
381
384
  overlayRef.current = 'none'
382
385
  setOverlay('none')
383
- if (hadPendingPrompt) pushNote('pending message cancelled.', 'dim')
386
+ if (hadPendingPrompt) pushNote('Pending message cancelled.', 'dim')
384
387
  }, [pushNote])
385
388
 
386
389
  const changeCwd = useCallback((next: string) => {
@@ -489,7 +492,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
489
492
  modeRef.current,
490
493
  )
491
494
  if (priorMessages.length <= 5) {
492
- pushNote('not enough turns to compact yet.', 'dim')
495
+ pushNote('Not enough turns to compact yet.', 'dim')
493
496
  return false
494
497
  }
495
498
  compactingRef.current = true
@@ -501,14 +504,14 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
501
504
  })
502
505
  if (!result.ok && result.cancelled) {
503
506
  removeCompactionProgress(compaction)
504
- pushNote('compaction cancelled.', 'dim')
507
+ pushNote('Compaction cancelled.', 'dim')
505
508
  return false
506
509
  }
507
510
  const summary = result.ok
508
511
  ? normalizeHandoffSummary(result.summary)
509
512
  : normalizeHandoffSummary(summarizeTranscriptLocally(priorMessages, result.reason))
510
513
  if (!result.ok) {
511
- pushNote(`provider summary failed; created a local summary instead: ${result.reason}`, 'dim')
514
+ pushNote(`Provider summary failed; created a local summary instead: ${result.reason}`, 'dim')
512
515
  }
513
516
 
514
517
  updateCompactionStage(compaction, 'saving summarized conversation')
@@ -573,9 +576,123 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
573
576
  } catch (err: unknown) {
574
577
  removeCompactionProgress(compaction)
575
578
  if (compaction.controller.signal.aborted) {
576
- pushNote('compaction cancelled.', 'dim')
579
+ pushNote('Compaction cancelled.', 'dim')
577
580
  } else {
578
- pushNote(`compact error: ${(err as Error).message}`, 'error')
581
+ pushNote(`Compact error: ${(err as Error).message}`, 'error')
582
+ }
583
+ return false
584
+ } finally {
585
+ compactingRef.current = false
586
+ compactionUiRef.current = null
587
+ setCompactionUi(null)
588
+ }
589
+ },
590
+ [beginCompactionUi, pushNote, refreshVisibleStats, removeCompactionProgress, updateCompactionStage],
591
+ )
592
+
593
+ const runCompactionFromTurn = useCallback(
594
+ async (turnId: string): Promise<boolean> => {
595
+ if (compactingRef.current) return false
596
+ const sourceSessionId = sessionIdRef.current
597
+ const all = sessionMessagesRef.current
598
+ const splitIndex = all.findIndex(m => m.turnId === turnId)
599
+ if (splitIndex < 0) {
600
+ pushNote('Could not find that prompt to summarize from.', 'error')
601
+ return false
602
+ }
603
+ const before = all.slice(0, splitIndex)
604
+ const from = all.slice(splitIndex)
605
+ const fromBase = buildBaseMessages(
606
+ from,
607
+ configRef.current,
608
+ providerRef.current.supportsTools,
609
+ cwdRef.current,
610
+ modeRef.current,
611
+ )
612
+ if (fromBase.length <= 2) {
613
+ pushNote('Not enough messages from that point to summarize.', 'dim')
614
+ return false
615
+ }
616
+ compactingRef.current = true
617
+ const compaction = beginCompactionUi('conversation', sourceSessionId)
618
+ try {
619
+ const result = await compactTranscript(providerRef.current, fromBase, {
620
+ signal: compaction.controller.signal,
621
+ onStage: stage => updateCompactionStage(compaction, stage),
622
+ })
623
+ if (!result.ok && result.cancelled) {
624
+ removeCompactionProgress(compaction)
625
+ pushNote('Compaction cancelled.', 'dim')
626
+ return false
627
+ }
628
+ const summary = result.ok
629
+ ? normalizeHandoffSummary(result.summary)
630
+ : normalizeHandoffSummary(summarizeTranscriptLocally(fromBase, result.reason))
631
+ if (!result.ok) {
632
+ pushNote(`Provider summary failed; created a local summary instead: ${result.reason}`, 'dim')
633
+ }
634
+ updateCompactionStage(compaction, 'saving summarized conversation')
635
+ const nextSessionId = newSessionId()
636
+ const summaryMessage: SessionMessage = {
637
+ role: 'user',
638
+ synthetic: true,
639
+ content: [
640
+ `Conversation handoff from ${sourceSessionId.slice(0, 8)} (summarized from a chosen prompt forward):`,
641
+ '',
642
+ summary,
643
+ ].join('\n'),
644
+ createdAt: nowIso(),
645
+ }
646
+ const acknowledgement: SessionMessage = {
647
+ role: 'assistant',
648
+ content: 'Ready to continue from this summary.',
649
+ createdAt: nowIso(),
650
+ model: configRef.current.model,
651
+ }
652
+ const context = {
653
+ cwd: cwdRef.current,
654
+ provider: configRef.current.provider,
655
+ model: configRef.current.model,
656
+ mode: modeRef.current,
657
+ }
658
+ await ensureSessionMetadata(nextSessionId, context)
659
+ await updateSessionActivity(nextSessionId, context, { compactedFromSessionId: sourceSessionId })
660
+ for (const msg of before) {
661
+ await appendSessionMessage(nextSessionId, msg, context)
662
+ }
663
+ await appendSessionMessage(nextSessionId, summaryMessage, context)
664
+ await appendSessionMessage(nextSessionId, acknowledgement, context)
665
+
666
+ updateCompactionStage(compaction, 'opening summarized conversation')
667
+ const nextMessages: SessionMessage[] = [...before, summaryMessage, acknowledgement]
668
+ compactionUiRef.current = null
669
+ setCompactionUi(null)
670
+ sessionIdRef.current = nextSessionId
671
+ setSessionId(nextSessionId)
672
+ sessionMessagesRef.current = nextMessages
673
+ historyScopeRef.current = 'session'
674
+ setHistory(promptHistoryFromSessionMessages(nextMessages))
675
+ statsSegmentStartRef.current = 0
676
+ setRows([
677
+ {
678
+ role: 'note',
679
+ id: nextRowId(),
680
+ kind: 'dim',
681
+ content: `kept ${sourceSessionId.slice(0, 8)} saved; summarized from a chosen prompt into ${nextSessionId.slice(0, 8)}.`,
682
+ },
683
+ ...sessionMessagesToRows(nextMessages, nextRowId),
684
+ ])
685
+ setQueuedInputs([])
686
+ setStatusStartedAt(Date.now())
687
+ refreshVisibleStats(nextMessages, providerRef.current.supportsTools, cwdRef.current, configRef.current, modeRef.current)
688
+ setSessionKey(key => key + 1)
689
+ return true
690
+ } catch (err: unknown) {
691
+ removeCompactionProgress(compaction)
692
+ if (compaction.controller.signal.aborted) {
693
+ pushNote('Compaction cancelled.', 'dim')
694
+ } else {
695
+ pushNote(`Compact error: ${(err as Error).message}`, 'error')
579
696
  }
580
697
  return false
581
698
  } finally {
@@ -707,7 +824,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
707
824
  try {
708
825
  await savePermissionRule(cwdRef.current, sessionRule)
709
826
  } catch (error: unknown) {
710
- pushNote(`failed to save permission rule: ${(error as Error).message}`, 'error')
827
+ pushNote(`Failed to save permission rule: ${(error as Error).message}`, 'error')
711
828
  }
712
829
  },
713
830
  [pushNote],
@@ -855,7 +972,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
855
972
 
856
973
  if (streaming || pullInFlight || compactionUiRef.current) {
857
974
  if (parseSlash(value)) {
858
- pushNote('slash commands cannot be queued. wait for the current task to finish.', 'dim')
975
+ pushNote('Slash commands cannot be queued. Wait for the current task to finish.', 'dim')
859
976
  return
860
977
  }
861
978
  setQueuedInputs(prev => [...prev, value])
@@ -892,7 +1009,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
892
1009
 
893
1010
  const handleContextLimitCancel = useCallback(() => {
894
1011
  clearContextLimit()
895
- pushNote('pending message cancelled.', 'dim')
1012
+ pushNote('Pending message cancelled.', 'dim')
896
1013
  }, [clearContextLimit, pushNote])
897
1014
 
898
1015
  const handleContextLimitAction = useCallback(
@@ -905,7 +1022,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
905
1022
  const prompt = state.prompt
906
1023
  clearContextLimit()
907
1024
  if (action === 'cancel') {
908
- pushNote('pending message cancelled.', 'dim')
1025
+ pushNote('Pending message cancelled.', 'dim')
909
1026
  return
910
1027
  }
911
1028
  if (action === 'switchModel') {
@@ -1020,7 +1137,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1020
1137
  pushNote(resolution.notice, resolution.tone)
1021
1138
  await continuePendingPromptAfterModelSwitch(pendingPrompt)
1022
1139
  } catch (err: unknown) {
1023
- pushNote(`provider switch failed: ${(err as Error).message}`, 'error')
1140
+ pushNote(`Provider switch failed: ${(err as Error).message}`, 'error')
1024
1141
  if (pendingPrompt) showContextLimitForPrompt(pendingPrompt)
1025
1142
  }
1026
1143
  },
@@ -1033,7 +1150,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1033
1150
  try {
1034
1151
  const [loaded, metadata] = await Promise.all([loadSession(id), loadSessionMetadata(id)])
1035
1152
  if (loaded.length === 0) {
1036
- pushNote('session was empty.', 'error')
1153
+ pushNote('Session was empty.', 'error')
1037
1154
  return
1038
1155
  }
1039
1156
  const resumed = buildResumedSessionState({
@@ -1068,7 +1185,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1068
1185
  refreshVisibleStats(loaded, providerRef.current.supportsTools, resumedCwd, configRef.current, resumed.mode)
1069
1186
  setSessionKey(k => k + 1)
1070
1187
  } catch (err: unknown) {
1071
- pushNote(`resume failed: ${(err as Error).message}`, 'error')
1188
+ pushNote(`Resume failed: ${(err as Error).message}`, 'error')
1072
1189
  }
1073
1190
  },
1074
1191
  [clearContextLimit, clearPendingPlan, cwd, pushNote, refreshVisibleStats],
@@ -1080,7 +1197,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1080
1197
  clearTranscript()
1081
1198
  overlayRef.current = 'none'
1082
1199
  setOverlay('none')
1083
- pushNote('cleared saved chat logs and resume context from this machine.', 'dim')
1200
+ pushNote('Cleared saved sessions and resume context from this machine.', 'dim')
1084
1201
  },
1085
1202
  [clearTranscript, pushNote],
1086
1203
  )
@@ -1099,9 +1216,9 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1099
1216
  try {
1100
1217
  const nextConfig = await setTokenIdentity(configRef.current, result.identity)
1101
1218
  applyConfigChange(nextConfig)
1102
- pushNote(`identity saved · ERC-8004 #${result.identity.agentId}`, 'info')
1219
+ pushNote(`Identity saved · ERC-8004 #${result.identity.agentId}`, 'info')
1103
1220
  } catch (err: unknown) {
1104
- pushNote(`identity save failed: ${(err as Error).message}`, 'error')
1221
+ pushNote(`Identity save failed: ${(err as Error).message}`, 'error')
1105
1222
  }
1106
1223
  })()
1107
1224
  }
@@ -1133,12 +1250,12 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1133
1250
  })
1134
1251
  overlayRef.current = 'identity'
1135
1252
  setOverlay('identity')
1136
- pushNote('opening snapshot signature.', 'dim')
1253
+ pushNote('Opening snapshot signature.', 'dim')
1137
1254
  return
1138
1255
  }
1139
1256
  overlayRef.current = 'none'
1140
1257
  setOverlay('none')
1141
- pushNote('snapshot not saved yet.', 'dim')
1258
+ pushNote('Snapshot not saved yet.', 'dim')
1142
1259
  },
1143
1260
  [continuityEditReview, pushNote],
1144
1261
  )
@@ -1147,7 +1264,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1147
1264
  setContinuityEditReview(null)
1148
1265
  overlayRef.current = 'none'
1149
1266
  setOverlay('none')
1150
- pushNote('snapshot not saved yet.', 'dim')
1267
+ pushNote('Snapshot not saved yet.', 'dim')
1151
1268
  }, [pushNote])
1152
1269
 
1153
1270
  const handleCopyDone = useCallback(
@@ -1155,9 +1272,9 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1155
1272
  setOverlay('none')
1156
1273
  setCopyPickerState(null)
1157
1274
  if (result.ok) {
1158
- pushNote(`copied ${label} via ${result.method}.`, 'dim')
1275
+ pushNote(`${label} copied to clipboard · ${result.chars} chars`, 'dim')
1159
1276
  } else {
1160
- pushNote(`copy failed: ${result.error}`, 'error')
1277
+ pushNote(`Copy failed: ${result.error}`, 'error')
1161
1278
  }
1162
1279
  },
1163
1280
  [pushNote],
@@ -1166,15 +1283,18 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1166
1283
  const handleCopyCancel = useCallback(() => {
1167
1284
  setOverlay('none')
1168
1285
  setCopyPickerState(null)
1169
- pushNote('copy cancelled.', 'dim')
1286
+ pushNote('Copy cancelled.', 'dim')
1170
1287
  }, [pushNote])
1171
1288
 
1172
- const handleRestoreConversation = useCallback((turnId: string) => {
1289
+ const handleRestoreConversation = useCallback((turnId: string, promptText?: string) => {
1173
1290
  const restored = restoreConversationState(sessionMessagesRef.current, turnId, nextRowId)
1174
1291
  sessionMessagesRef.current = restored.messages
1175
1292
  setRows(restored.rows)
1176
1293
  historyScopeRef.current = 'session'
1177
1294
  setHistory(restored.promptHistory)
1295
+ if (promptText != null && promptText.length > 0) {
1296
+ setPendingInputDraft(promptText)
1297
+ }
1178
1298
  if (restored.truncated) {
1179
1299
  setQueuedInputs([])
1180
1300
  statsSegmentStartRef.current = Math.min(statsSegmentStartRef.current, restored.messages.length)
@@ -1216,7 +1336,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1216
1336
 
1217
1337
  if (priorMessages.length <= 5) {
1218
1338
  startFreshImplementationContext()
1219
- pushNote('not enough planning context to summarize; starting a plan-only implementation conversation.', 'dim')
1339
+ pushNote('Not enough planning context to summarize; starting a plan-only implementation conversation.', 'dim')
1220
1340
  return true
1221
1341
  }
1222
1342
 
@@ -1229,14 +1349,14 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1229
1349
  })
1230
1350
  if (!result.ok && result.cancelled) {
1231
1351
  removeCompactionProgress(compaction)
1232
- pushNote('plan context summary cancelled.', 'dim')
1352
+ pushNote('Plan context summary cancelled.', 'dim')
1233
1353
  return false
1234
1354
  }
1235
1355
  const summary = result.ok
1236
1356
  ? normalizeHandoffSummary(result.summary)
1237
1357
  : normalizeHandoffSummary(summarizeTranscriptLocally(priorMessages, result.reason))
1238
1358
  if (!result.ok) {
1239
- pushNote(`provider summary failed; created a local summary instead: ${result.reason}`, 'dim')
1359
+ pushNote(`Provider summary failed; created a local summary instead: ${result.reason}`, 'dim')
1240
1360
  }
1241
1361
 
1242
1362
  updateCompactionStage(compaction, 'saving summarized conversation')
@@ -1287,9 +1407,9 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1287
1407
  } catch (err: unknown) {
1288
1408
  removeCompactionProgress(compaction)
1289
1409
  if (compaction.controller.signal.aborted) {
1290
- pushNote('plan context summary cancelled.', 'dim')
1410
+ pushNote('Plan context summary cancelled.', 'dim')
1291
1411
  } else {
1292
- pushNote(`context summary error: ${(err as Error).message}`, 'error')
1412
+ pushNote(`Context summary error: ${(err as Error).message}`, 'error')
1293
1413
  }
1294
1414
  return false
1295
1415
  } finally {
@@ -1323,7 +1443,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1323
1443
  }
1324
1444
  if (plan.cwd !== cwdRef.current || plan.sessionId !== sessionIdRef.current) {
1325
1445
  clearPendingPlan()
1326
- pushNote('dismissed stale plan approval because the workspace changed.', 'dim')
1446
+ pushNote('Dismissed stale plan approval because the workspace changed.', 'dim')
1327
1447
  return
1328
1448
  }
1329
1449
  if (action === 'continue') {
@@ -1387,7 +1507,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1387
1507
  })
1388
1508
  }, [compactionUi, overlay, projectedUsageForInput, pullInFlight, pushNote, queuedInputs, runStream, showContextLimitForPrompt, streaming])
1389
1509
 
1390
- const contextLine = `${config.provider} · ${formatModelDisplayName(config.provider, config.model, { maxLength: 24 })} · ${compressHome(cwd)}`
1510
+ const contextLine = `${providerDisplayName(config.provider)} · ${formatModelDisplayName(config.provider, config.model, { maxLength: 24 })} · ${compressHome(cwd)}`
1391
1511
  const tipLine = 'Tip: type /help to get started · shift+enter for newline'
1392
1512
 
1393
1513
  const placeholderHints = useMemo(() => {
@@ -1397,17 +1517,23 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1397
1517
 
1398
1518
  const exitHint = exitState.pending ? 'ctrl+c again to quit' : null
1399
1519
  const runtimeModeLabel = sessionModeLabel(mode)
1520
+ const runtimeModeColor =
1521
+ mode === 'plan'
1522
+ ? theme.modePlan
1523
+ : mode === 'accept-edits'
1524
+ ? theme.modeAcceptEdits
1525
+ : theme.text
1400
1526
  const footerRight = (
1401
1527
  <Box flexDirection="row">
1402
1528
  {exitHint ? (
1403
1529
  <>
1404
- <Text color={theme.text}>{exitHint}</Text>
1530
+ <Text color={theme.accentPeriwinkle}>{exitHint}</Text>
1405
1531
  <Text color={theme.dim}> · </Text>
1406
1532
  </>
1407
1533
  ) : null}
1408
1534
  {runtimeModeLabel ? (
1409
1535
  <>
1410
- <Text bold>{runtimeModeLabel}</Text>
1536
+ <Text color={runtimeModeColor} bold>{runtimeModeLabel}</Text>
1411
1537
  <Text color={theme.dim}> (</Text>
1412
1538
  <Text color={theme.accentPeriwinkle}>shift+tab to cycle</Text>
1413
1539
  <Text color={theme.dim}>) · </Text>
@@ -1456,6 +1582,9 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1456
1582
  identityOverlay={identityOverlay}
1457
1583
  handleIdentityResult={handleIdentityResult}
1458
1584
  handleRestoreConversation={handleRestoreConversation}
1585
+ pendingInputDraft={pendingInputDraft}
1586
+ onInputDraftConsumed={() => setPendingInputDraft(null)}
1587
+ handleSummarizeFromTurn={runCompactionFromTurn}
1459
1588
  handleCopyDone={handleCopyDone}
1460
1589
  handleCopyCancel={handleCopyCancel}
1461
1590
  resolvePermission={resolvePermission}
@@ -1490,7 +1619,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1490
1619
  }
1491
1620
 
1492
1621
  export function chatFooterShortcutText(canScrollTranscript: boolean): string {
1493
- return `${canScrollTranscript ? 'pgup/pgdn scroll · ' : ''}alt+p model · alt+i identity`
1622
+ return 'alt+p model · alt+i identity'
1494
1623
  }
1495
1624
 
1496
1625
  function formatContextLabel(usage: ContextUsage): string {
@@ -1568,12 +1697,14 @@ export function privateContinuityEditReviewFromToolResult(
1568
1697
  if (name !== 'propose_private_continuity_edit' || !result.ok) return null
1569
1698
  const file = normalizePrivateContinuityFile(input.file)
1570
1699
  if (!file) return null
1571
- const filePath = extractReviewFilePath(result.content)
1700
+ const parsed = splitFileChangeResult(result.content)
1701
+ const filePath = extractReviewFilePath(parsed.content)
1572
1702
  if (!filePath) return null
1573
1703
  return {
1574
1704
  file,
1575
1705
  filePath,
1576
1706
  summary: result.summary,
1707
+ ...(parsed.diff ? { diff: parsed.diff } : {}),
1577
1708
  }
1578
1709
  }
1579
1710
 
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import { Box } from 'ink'
3
- import { TranscriptView } from './TranscriptView.js'
3
+ import { TranscriptView } from './transcript/TranscriptView.js'
4
4
  import type { MessageRow } from './MessageList.js'
5
5
 
6
6
  type ConversationStackProps = {