ethagent 3.3.4 → 4.0.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 (322) hide show
  1. package/.claude-plugin/marketplace.json +11 -0
  2. package/.claude-plugin/plugin.json +35 -0
  3. package/LICENSE +1 -1
  4. package/README.md +64 -104
  5. package/commands/ethagent.md +40 -0
  6. package/package.json +16 -16
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -6
  8. package/src/app/keybindings/types.ts +1 -6
  9. package/src/cli/ResetConfirmView.tsx +54 -53
  10. package/src/cli/demo.ts +86 -0
  11. package/src/cli/hookIo.ts +45 -0
  12. package/src/cli/main.tsx +94 -123
  13. package/src/cli/memoryGuard.ts +49 -0
  14. package/src/cli/reset.ts +28 -70
  15. package/src/cli/sessionStart.ts +33 -0
  16. package/src/cli/status.ts +46 -0
  17. package/src/cli/sync.ts +167 -0
  18. package/src/cli/syncAdapters/claude-code.ts +86 -0
  19. package/src/cli/syncAdapters/codex.ts +66 -0
  20. package/src/cli/syncAdapters/index.ts +45 -0
  21. package/src/cli/syncAdapters/managedBlock.ts +175 -0
  22. package/src/cli/syncAdapters/shared.ts +63 -0
  23. package/src/identity/continuity/envelopeParse.ts +20 -1
  24. package/src/identity/continuity/publicSkills.ts +3 -1
  25. package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
  26. package/src/identity/continuity/skills/scaffold.ts +5 -2
  27. package/src/identity/continuity/snapshots.ts +12 -5
  28. package/src/identity/continuity/storage/defaults.ts +20 -19
  29. package/src/identity/continuity/storage/status.ts +1 -1
  30. package/src/identity/ens/ensLookup/constants.ts +1 -1
  31. package/src/identity/manager/IdentityManager.tsx +33 -0
  32. package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
  33. package/src/identity/{hub → manager}/Routes.tsx +48 -34
  34. package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
  35. package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
  36. package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
  37. package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
  38. package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
  39. package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
  40. package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
  41. package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
  42. package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
  43. package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
  44. package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
  45. package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
  46. package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
  47. package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
  48. package/src/identity/{hub → manager}/create/effects.ts +19 -10
  49. package/src/identity/manager/create/importScan.ts +122 -0
  50. package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
  51. package/src/identity/{hub → manager}/custody/actions.ts +1 -15
  52. package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
  53. package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
  54. package/src/identity/{hub → manager}/custody/types.ts +1 -2
  55. package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
  56. package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
  57. package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
  58. package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
  59. package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
  60. package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
  61. package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
  62. package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
  63. package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
  64. package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
  65. package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
  66. package/src/identity/{hub → manager}/profile/effects.ts +1 -3
  67. package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
  68. package/src/identity/{hub → manager}/profile/state.ts +1 -1
  69. package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
  70. package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
  71. package/src/identity/{hub → manager}/restore/apply.ts +1 -1
  72. package/src/identity/{hub → manager}/restore/auth.ts +1 -1
  73. package/src/identity/{hub → manager}/restore/discover.ts +1 -1
  74. package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
  75. package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
  76. package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
  77. package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
  78. package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
  79. package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
  80. package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
  81. package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
  82. package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
  83. package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
  84. package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
  85. package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
  86. package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
  87. package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
  88. package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
  89. package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
  90. package/src/identity/manager/shared/effects/types.ts +30 -0
  91. package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
  92. package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
  93. package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
  94. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
  95. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
  96. package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
  97. package/src/identity/{hub → manager}/shared/utils.ts +6 -10
  98. package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
  99. package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
  100. package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
  101. package/src/identity/{hub → manager}/types.ts +5 -6
  102. package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
  103. package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
  104. package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
  105. package/src/identity/registry/erc8004/discovery.ts +3 -17
  106. package/src/identity/registry/erc8004/utils.ts +1 -1
  107. package/src/identity/storage/ipfs.ts +21 -1
  108. package/src/identity/wallet/browserWallet/html.ts +10 -2
  109. package/src/identity/wallet/browserWallet/http.ts +18 -0
  110. package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
  111. package/src/identity/wallet/browserWallet/requests.ts +10 -28
  112. package/src/identity/wallet/browserWallet/session.ts +26 -33
  113. package/src/identity/wallet/browserWallet/validation.ts +14 -0
  114. package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
  115. package/src/identity/wallet/page/boot.ts +43 -0
  116. package/src/identity/wallet/page/config.ts +59 -0
  117. package/src/identity/wallet/page/constants.ts +12 -0
  118. package/src/identity/wallet/page/copy.ts +47 -68
  119. package/src/identity/wallet/page/css.ts +638 -0
  120. package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
  121. package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
  122. package/src/identity/wallet/page/markup.ts +44 -34
  123. package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
  124. package/src/identity/wallet/page/resize.ts +95 -0
  125. package/src/identity/wallet/page/state.ts +135 -8
  126. package/src/identity/wallet/page/timeline.ts +161 -0
  127. package/src/identity/wallet/page/view.ts +22 -302
  128. package/src/storage/config.ts +30 -80
  129. package/src/storage/reset.ts +31 -0
  130. package/src/storage/secrets.ts +1 -16
  131. package/src/ui/Select.tsx +27 -5
  132. package/src/ui/Spinner.tsx +16 -15
  133. package/src/ui/Surface.tsx +21 -17
  134. package/src/ui/TextArea.tsx +173 -0
  135. package/src/ui/TextInput.tsx +31 -133
  136. package/src/ui/theme.ts +22 -13
  137. package/src/utils/clipboard.ts +0 -140
  138. package/src/app/FirstRun.tsx +0 -577
  139. package/src/app/FirstRunTimeline.tsx +0 -51
  140. package/src/app/firstRunConfig.ts +0 -26
  141. package/src/app/hooks/useCancelRequest.ts +0 -22
  142. package/src/app/hooks/useDoublePress.ts +0 -46
  143. package/src/app/hooks/useExitOnCtrlC.ts +0 -36
  144. package/src/auth/openaiOAuth/credentials.ts +0 -47
  145. package/src/auth/openaiOAuth/crypto.ts +0 -23
  146. package/src/auth/openaiOAuth/index.ts +0 -238
  147. package/src/auth/openaiOAuth/landingPage.ts +0 -116
  148. package/src/auth/openaiOAuth/listener.ts +0 -151
  149. package/src/auth/openaiOAuth/refresh.ts +0 -70
  150. package/src/auth/openaiOAuth/shared.ts +0 -115
  151. package/src/chat/ChatBottomPane.tsx +0 -296
  152. package/src/chat/ChatScreen.tsx +0 -1685
  153. package/src/chat/ConversationStack.tsx +0 -56
  154. package/src/chat/MessageList.tsx +0 -638
  155. package/src/chat/SessionStatus.tsx +0 -53
  156. package/src/chat/chatEnvironment.ts +0 -16
  157. package/src/chat/chatScreenUtils.ts +0 -194
  158. package/src/chat/chatSessionState.ts +0 -146
  159. package/src/chat/chatTurnContext.ts +0 -50
  160. package/src/chat/chatTurnOrchestrator.ts +0 -603
  161. package/src/chat/chatTurnRows.ts +0 -64
  162. package/src/chat/commands.ts +0 -494
  163. package/src/chat/continuityEditReview.ts +0 -42
  164. package/src/chat/display/DiffView.tsx +0 -193
  165. package/src/chat/display/SyntaxText.tsx +0 -192
  166. package/src/chat/display/toolCallDisplay.ts +0 -103
  167. package/src/chat/display/toolResultDisplay.ts +0 -19
  168. package/src/chat/input/ChatInput.tsx +0 -625
  169. package/src/chat/input/chatInputHelpers.ts +0 -62
  170. package/src/chat/input/chatInputState.ts +0 -247
  171. package/src/chat/input/chatPaste.ts +0 -49
  172. package/src/chat/input/imageRefs.ts +0 -30
  173. package/src/chat/input/inputRendering.tsx +0 -93
  174. package/src/chat/input/textCursor.ts +0 -212
  175. package/src/chat/messageMarkdown.ts +0 -220
  176. package/src/chat/messageRows.ts +0 -43
  177. package/src/chat/planImplementation.ts +0 -62
  178. package/src/chat/slashCommandHandlers.ts +0 -122
  179. package/src/chat/slashCommandViews.ts +0 -120
  180. package/src/chat/transcript/TranscriptView.tsx +0 -184
  181. package/src/chat/transcript/transcriptViewport.ts +0 -295
  182. package/src/chat/views/ContextLimitView.tsx +0 -95
  183. package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
  184. package/src/chat/views/CopyPicker.tsx +0 -50
  185. package/src/chat/views/PermissionPrompt.tsx +0 -156
  186. package/src/chat/views/PermissionsView.tsx +0 -165
  187. package/src/chat/views/PlanApprovalView.tsx +0 -91
  188. package/src/chat/views/ResumeView.tsx +0 -273
  189. package/src/chat/views/RewindView.tsx +0 -412
  190. package/src/cli/preview.tsx +0 -14
  191. package/src/cli/updateNotice.ts +0 -54
  192. package/src/identity/continuity/privateEdit/apply.ts +0 -170
  193. package/src/identity/continuity/privateEdit/diff.ts +0 -6
  194. package/src/identity/continuity/privateEdit/files.ts +0 -23
  195. package/src/identity/continuity/privateEdit/types.ts +0 -28
  196. package/src/identity/continuity/privateEdit.ts +0 -46
  197. package/src/identity/hub/IdentityHub.tsx +0 -14
  198. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
  199. package/src/identity/hub/ens/effects.ts +0 -218
  200. package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
  201. package/src/identity/hub/shared/effects/types.ts +0 -53
  202. package/src/identity/hub/shared/reconciliation/index.ts +0 -14
  203. package/src/identity/wallet/page/grainient.ts +0 -278
  204. package/src/identity/wallet/page/html.ts +0 -28
  205. package/src/identity/wallet/page/styles/base.ts +0 -260
  206. package/src/identity/wallet/page/styles/components.ts +0 -262
  207. package/src/identity/wallet/page/styles/index.ts +0 -5
  208. package/src/identity/wallet/page/styles/responsive.ts +0 -247
  209. package/src/identity/wallet/page.tsx +0 -38
  210. package/src/mcp/approvals.ts +0 -113
  211. package/src/mcp/config.ts +0 -235
  212. package/src/mcp/manager.ts +0 -482
  213. package/src/mcp/managerHelpers.ts +0 -70
  214. package/src/mcp/names.ts +0 -19
  215. package/src/mcp/output.ts +0 -96
  216. package/src/models/ModelPicker.tsx +0 -1009
  217. package/src/models/catalog.ts +0 -327
  218. package/src/models/huggingface.ts +0 -712
  219. package/src/models/huggingfaceStorage.ts +0 -136
  220. package/src/models/llamacpp.ts +0 -848
  221. package/src/models/llamacppCommands.ts +0 -44
  222. package/src/models/llamacppConfig.ts +0 -34
  223. package/src/models/llamacppDiscovery.ts +0 -176
  224. package/src/models/llamacppOutput.ts +0 -65
  225. package/src/models/llamacppPreflight.ts +0 -158
  226. package/src/models/modelDisplay.ts +0 -180
  227. package/src/models/modelPickerCatalogFlow.ts +0 -56
  228. package/src/models/modelPickerCredentials.ts +0 -166
  229. package/src/models/modelPickerData.ts +0 -41
  230. package/src/models/modelPickerDisplay.tsx +0 -132
  231. package/src/models/modelPickerHfFlow.ts +0 -192
  232. package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
  233. package/src/models/modelPickerOptions.ts +0 -457
  234. package/src/models/modelPickerTypes.ts +0 -69
  235. package/src/models/modelPickerUninstallFlow.ts +0 -48
  236. package/src/models/modelPickerViewHelpers.ts +0 -174
  237. package/src/models/modelRecommendation.ts +0 -139
  238. package/src/models/providerDisplay.ts +0 -16
  239. package/src/models/runtimeDetection.ts +0 -81
  240. package/src/models/uncensoredCatalog.ts +0 -86
  241. package/src/providers/anthropic.ts +0 -290
  242. package/src/providers/contracts.ts +0 -71
  243. package/src/providers/errors.ts +0 -80
  244. package/src/providers/gemini.ts +0 -391
  245. package/src/providers/openai-chat.ts +0 -474
  246. package/src/providers/openai-responses-format.ts +0 -177
  247. package/src/providers/openai-responses.ts +0 -306
  248. package/src/providers/openaiChatWire.ts +0 -124
  249. package/src/providers/registry.ts +0 -120
  250. package/src/providers/retry.ts +0 -58
  251. package/src/providers/sse.ts +0 -93
  252. package/src/runtime/compaction.ts +0 -395
  253. package/src/runtime/cwd.ts +0 -43
  254. package/src/runtime/providerTurn.ts +0 -38
  255. package/src/runtime/sessionMode.ts +0 -55
  256. package/src/runtime/systemPrompt.ts +0 -213
  257. package/src/runtime/textToolParser.ts +0 -161
  258. package/src/runtime/toolClaimGuards.ts +0 -143
  259. package/src/runtime/toolExecution.ts +0 -304
  260. package/src/runtime/toolIntent.ts +0 -143
  261. package/src/runtime/turn.ts +0 -369
  262. package/src/runtime/turnNudges.ts +0 -223
  263. package/src/runtime/turnTypes.ts +0 -86
  264. package/src/storage/factoryReset.ts +0 -127
  265. package/src/storage/history.ts +0 -58
  266. package/src/storage/permissions.ts +0 -76
  267. package/src/storage/rewind.ts +0 -266
  268. package/src/storage/sessionExport.ts +0 -49
  269. package/src/storage/sessions.ts +0 -495
  270. package/src/tools/bashSafety.ts +0 -186
  271. package/src/tools/bashTool.ts +0 -140
  272. package/src/tools/changeDirectoryTool.ts +0 -213
  273. package/src/tools/contracts.ts +0 -192
  274. package/src/tools/deleteFileTool.ts +0 -116
  275. package/src/tools/editTool.ts +0 -165
  276. package/src/tools/editUtils.ts +0 -170
  277. package/src/tools/fileDiff.ts +0 -261
  278. package/src/tools/listDirectoryTool.ts +0 -55
  279. package/src/tools/listSkillFilesTool.ts +0 -77
  280. package/src/tools/listSkillsTool.ts +0 -68
  281. package/src/tools/mcpResourceTools.ts +0 -95
  282. package/src/tools/permissionRules.ts +0 -85
  283. package/src/tools/privateContinuityEditTool.ts +0 -187
  284. package/src/tools/privateContinuityReadTool.ts +0 -106
  285. package/src/tools/readSkillTool.ts +0 -107
  286. package/src/tools/readTool.ts +0 -85
  287. package/src/tools/registry.ts +0 -103
  288. package/src/tools/writeFileTool.ts +0 -167
  289. package/src/ui/BrandSplash.tsx +0 -133
  290. package/src/ui/terminalTitle.ts +0 -30
  291. package/src/utils/images.ts +0 -140
  292. package/src/utils/markdownSegments.ts +0 -51
  293. package/src/utils/messages.ts +0 -37
  294. package/src/utils/withRetry.ts +0 -324
  295. /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
  296. /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
  297. /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
  298. /package/src/identity/{hub → manager}/custody/state.ts +0 -0
  299. /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
  300. /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
  301. /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
  302. /package/src/identity/{hub → manager}/ens/state.ts +0 -0
  303. /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
  304. /package/src/identity/{hub → manager}/ens/types.ts +0 -0
  305. /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
  306. /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
  307. /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
  308. /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
  309. /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
  310. /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
  311. /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
  312. /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
  313. /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
  314. /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
  315. /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
  316. /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
  317. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
  318. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
  319. /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
  320. /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
  321. /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
  322. /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
@@ -1,184 +0,0 @@
1
- import React, { useEffect, useMemo, useState } from 'react'
2
- import { Box, Text, useStdout } from 'ink'
3
- import { useAppInput } from '../../app/input/AppInputProvider.js'
4
- import { MessageList, type MessageRow } from '../MessageList.js'
5
- import { theme } from '../../ui/theme.js'
6
- import {
7
- anchorForScrollTop,
8
- buildLineOffsets,
9
- clampLine,
10
- estimateMessageRowHeight,
11
- resolveScrollTopFromAnchor,
12
- scrollTopForPageDown,
13
- scrollTopForPageUp,
14
- selectRowsForScrollTop,
15
- type TranscriptViewportState,
16
- } from './transcriptViewport.js'
17
-
18
- type TranscriptViewProps = {
19
- rows: MessageRow[]
20
- active?: boolean
21
- bottomVariant?: 'prompt' | 'overlay'
22
- onVisibleReasoningIdsChange?: (ids: string[]) => void
23
- onScrollabilityChange?: (canScroll: boolean) => void
24
- }
25
-
26
- const PROMPT_RESERVED_LINES = 12
27
- const OVERLAY_RESERVED_LINES = 16
28
- const MIN_TRANSCRIPT_LINES = 6
29
- const MAX_TRANSCRIPT_LINES = 240
30
-
31
- export const TranscriptView: React.FC<TranscriptViewProps> = ({
32
- rows,
33
- active = true,
34
- bottomVariant = 'prompt',
35
- onVisibleReasoningIdsChange,
36
- onScrollabilityChange,
37
- }) => {
38
- const { stdout } = useStdout()
39
- const columns = stdout.columns ?? process.stdout.columns ?? 80
40
- const terminalRows = stdout.rows ?? process.stdout.rows ?? 24
41
- const reservedLines = bottomVariant === 'overlay' ? OVERLAY_RESERVED_LINES : PROMPT_RESERVED_LINES
42
- const maxLines = Math.min(
43
- MAX_TRANSCRIPT_LINES,
44
- Math.max(MIN_TRANSCRIPT_LINES, terminalRows - reservedLines),
45
- )
46
- const [viewportState, setViewportState] = useState<TranscriptViewportState>({
47
- scrollTopLine: 0,
48
- followTail: true,
49
- anchor: null,
50
- })
51
- const metrics = useMemo(() => {
52
- const heights = rows.map(row => Math.max(1, estimateMessageRowHeight(row, columns)))
53
- const offsets = buildLineOffsets(heights)
54
- const totalLines = offsets[offsets.length - 1] ?? 0
55
- return {
56
- rowIds: rows.map(row => row.id),
57
- offsets,
58
- maxScrollTop: Math.max(0, totalLines - maxLines),
59
- }
60
- }, [columns, maxLines, rows])
61
- const resolvedViewportState = useMemo(
62
- () => resolveViewportState(viewportState, metrics.rowIds, metrics.offsets, metrics.maxScrollTop),
63
- [metrics, viewportState],
64
- )
65
- const selection = useMemo(
66
- () => selectRowsForScrollTop(
67
- rows,
68
- maxLines,
69
- resolvedViewportState.scrollTopLine,
70
- row => estimateMessageRowHeight(row, columns),
71
- ),
72
- [columns, maxLines, resolvedViewportState, rows],
73
- )
74
- const visibleReasoningIds = useMemo(
75
- () => selection.rows
76
- .filter((slice): slice is { row: Extract<MessageRow, { role: 'thinking' }>; clipStart: number; clipEnd: number; rowHeight: number } =>
77
- slice.row.role === 'thinking',
78
- )
79
- .map(slice => slice.row.id),
80
- [selection.rows],
81
- )
82
-
83
- useEffect(() => {
84
- setViewportState(prev => sameViewportState(prev, resolvedViewportState) ? prev : resolvedViewportState)
85
- }, [resolvedViewportState])
86
-
87
- useEffect(() => {
88
- onVisibleReasoningIdsChange?.(visibleReasoningIds)
89
- }, [onVisibleReasoningIdsChange, visibleReasoningIds])
90
-
91
- useEffect(() => {
92
- onScrollabilityChange?.(metrics.maxScrollTop > 0)
93
- }, [metrics.maxScrollTop, onScrollabilityChange])
94
-
95
- useAppInput((_input, key) => {
96
- if (key.pageUp) {
97
- const target = scrollTopForPageUp(
98
- resolvedViewportState.scrollTopLine,
99
- metrics.maxScrollTop,
100
- maxLines,
101
- )
102
- setViewportState(viewportForScrollTop(
103
- target,
104
- metrics.rowIds,
105
- metrics.offsets,
106
- metrics.maxScrollTop,
107
- ))
108
- } else if (key.pageDown) {
109
- const target = scrollTopForPageDown(
110
- resolvedViewportState.scrollTopLine,
111
- metrics.maxScrollTop,
112
- maxLines,
113
- )
114
- setViewportState(viewportForScrollTop(
115
- target,
116
- metrics.rowIds,
117
- metrics.offsets,
118
- metrics.maxScrollTop,
119
- ))
120
- }
121
- }, { isActive: active })
122
-
123
- return (
124
- <Box flexDirection="column">
125
- {selection.hiddenBefore > 0 ? (
126
- <Text color={theme.dim}>
127
- {` ${selection.hiddenBefore} earlier message${selection.hiddenBefore === 1 ? '' : 's'} above · `}
128
- <Text color={theme.accentPeriwinkle}>pgup</Text>
129
- {` to scroll · `}
130
- <Text color={theme.accentPeriwinkle}>/export</Text>
131
- {` saves the full transcript`}
132
- </Text>
133
- ) : null}
134
- <MessageList slices={selection.rows} />
135
- {selection.hiddenAfter > 0 ? (
136
- <Text color={theme.dim}>
137
- {` ${selection.hiddenAfter} later message${selection.hiddenAfter === 1 ? '' : 's'} below · `}
138
- <Text color={theme.accentPeriwinkle}>pgdn</Text>
139
- {` to return`}
140
- </Text>
141
- ) : null}
142
- </Box>
143
- )
144
- }
145
-
146
- function resolveViewportState(
147
- state: TranscriptViewportState,
148
- rowIds: string[],
149
- offsets: number[],
150
- maxScrollTop: number,
151
- ): TranscriptViewportState {
152
- if (rowIds.length === 0) {
153
- return { scrollTopLine: 0, followTail: true, anchor: null }
154
- }
155
-
156
- const scrollTopLine = state.followTail
157
- ? maxScrollTop
158
- : resolveScrollTopFromAnchor(rowIds, offsets, state.anchor, maxScrollTop)
159
- ?? clampLine(state.scrollTopLine, maxScrollTop)
160
-
161
- return viewportForScrollTop(scrollTopLine, rowIds, offsets, maxScrollTop)
162
- }
163
-
164
- function viewportForScrollTop(
165
- scrollTopLine: number,
166
- rowIds: string[],
167
- offsets: number[],
168
- maxScrollTop: number,
169
- ): TranscriptViewportState {
170
- const clamped = clampLine(scrollTopLine, maxScrollTop)
171
- const followTail = clamped >= maxScrollTop
172
- return {
173
- scrollTopLine: clamped,
174
- followTail,
175
- anchor: followTail ? null : anchorForScrollTop(rowIds, offsets, clamped),
176
- }
177
- }
178
-
179
- function sameViewportState(left: TranscriptViewportState, right: TranscriptViewportState): boolean {
180
- return left.scrollTopLine === right.scrollTopLine
181
- && left.followTail === right.followTail
182
- && left.anchor?.rowId === right.anchor?.rowId
183
- && left.anchor?.offset === right.anchor?.offset
184
- }
@@ -1,295 +0,0 @@
1
- import type { MessageRow } from '../MessageList.js'
2
- import { flattenAssistantBody } from '../MessageList.js'
3
-
4
- export type TranscriptAnchor = {
5
- rowId: string
6
- offset: number
7
- }
8
-
9
- export type TranscriptViewportState = {
10
- scrollTopLine: number
11
- followTail: boolean
12
- anchor: TranscriptAnchor | null
13
- }
14
-
15
- export type RowSlice<T> = {
16
- row: T
17
- clipStart: number
18
- clipEnd: number
19
- rowHeight: number
20
- }
21
-
22
- export function buildLineOffsets(rowHeights: number[]): number[] {
23
- const out = new Array<number>(rowHeights.length + 1).fill(0)
24
- for (let i = 0; i < rowHeights.length; i += 1) {
25
- out[i + 1] = out[i]! + (rowHeights[i] ?? 1)
26
- }
27
- return out
28
- }
29
-
30
- export function findRowIndexAtLine(offsets: number[], line: number): number {
31
- let low = 0
32
- let high = offsets.length - 1
33
- while (low < high) {
34
- const mid = Math.floor((low + high + 1) / 2)
35
- if ((offsets[mid] ?? 0) <= line) low = mid
36
- else high = mid - 1
37
- }
38
- return low
39
- }
40
-
41
- export function anchorForScrollTop(
42
- rowIds: string[],
43
- offsets: number[],
44
- scrollTopLine: number,
45
- ): TranscriptAnchor | null {
46
- if (rowIds.length === 0) return null
47
- const rowIndex = Math.min(rowIds.length - 1, findRowIndexAtLine(offsets, scrollTopLine))
48
- const rowId = rowIds[rowIndex]
49
- if (!rowId) return null
50
- return {
51
- rowId,
52
- offset: Math.max(0, scrollTopLine - (offsets[rowIndex] ?? 0)),
53
- }
54
- }
55
-
56
- export function resolveScrollTopFromAnchor(
57
- rowIds: string[],
58
- offsets: number[],
59
- anchor: TranscriptAnchor | null,
60
- maxScrollTop: number,
61
- ): number | null {
62
- if (!anchor) return null
63
- const rowIndex = rowIds.indexOf(anchor.rowId)
64
- if (rowIndex === -1) return null
65
- return clampLine((offsets[rowIndex] ?? 0) + anchor.offset, maxScrollTop)
66
- }
67
-
68
- export function clampLine(line: number, maxScrollTop: number): number {
69
- return Math.max(0, Math.min(maxScrollTop, Math.floor(line)))
70
- }
71
-
72
- export type TranscriptTailSelection<T> = {
73
- rows: Array<RowSlice<T>>
74
- hiddenCount: number
75
- }
76
-
77
- export type TranscriptWindowSelection<T> = {
78
- rows: Array<RowSlice<T>>
79
- hiddenBefore: number
80
- hiddenAfter: number
81
- totalLines: number
82
- maxScrollOffset: number
83
- }
84
-
85
- export function selectTailRowsForViewport<T>(
86
- rows: T[],
87
- maxLines: number,
88
- estimateHeight: (row: T) => number,
89
- ): TranscriptTailSelection<T> {
90
- if (rows.length === 0) return { rows: [], hiddenCount: 0 }
91
-
92
- const budget = Math.max(1, Math.floor(maxLines))
93
- let used = 0
94
- let start = rows.length - 1
95
-
96
- for (; start >= 0; start -= 1) {
97
- const row = rows[start]
98
- if (!row) break
99
- const height = Math.max(1, estimateHeight(row))
100
- if (used > 0 && used + height > budget) break
101
- used += height
102
- }
103
-
104
- const firstVisible = Math.max(0, start + 1)
105
- const slice = rows.slice(firstVisible).map(row => {
106
- const height = Math.max(1, estimateHeight(row))
107
- return { row, clipStart: 0, clipEnd: height, rowHeight: height }
108
- })
109
- return {
110
- rows: slice,
111
- hiddenCount: firstVisible,
112
- }
113
- }
114
-
115
- export function selectRowsForScrollOffset<T>(
116
- rows: T[],
117
- maxLines: number,
118
- scrollOffsetFromTail: number,
119
- estimateHeight: (row: T) => number,
120
- ): TranscriptWindowSelection<T> {
121
- if (rows.length === 0) {
122
- return { rows: [], hiddenBefore: 0, hiddenAfter: 0, totalLines: 0, maxScrollOffset: 0 }
123
- }
124
-
125
- const budget = Math.max(1, Math.floor(maxLines))
126
- const heights = rows.map(row => Math.max(1, estimateHeight(row)))
127
- const offsets = buildLineOffsets(heights)
128
- const totalLines = offsets[offsets.length - 1] ?? 0
129
- const maxScrollOffset = Math.max(0, totalLines - budget)
130
- const scrollOffset = clampLine(scrollOffsetFromTail, maxScrollOffset)
131
- const startLine = Math.max(0, totalLines - budget - scrollOffset)
132
-
133
- return selectRowsForLineWindow(rows, heights, offsets, budget, startLine, totalLines, maxScrollOffset)
134
- }
135
-
136
- export function selectRowsForScrollTop<T>(
137
- rows: T[],
138
- maxLines: number,
139
- scrollTopLine: number,
140
- estimateHeight: (row: T) => number,
141
- ): TranscriptWindowSelection<T> {
142
- if (rows.length === 0) {
143
- return { rows: [], hiddenBefore: 0, hiddenAfter: 0, totalLines: 0, maxScrollOffset: 0 }
144
- }
145
-
146
- const budget = Math.max(1, Math.floor(maxLines))
147
- const heights = rows.map(row => Math.max(1, estimateHeight(row)))
148
- const offsets = buildLineOffsets(heights)
149
- const totalLines = offsets[offsets.length - 1] ?? 0
150
- const maxScrollOffset = Math.max(0, totalLines - budget)
151
- const startLine = clampLine(scrollTopLine, maxScrollOffset)
152
-
153
- return selectRowsForLineWindow(rows, heights, offsets, budget, startLine, totalLines, maxScrollOffset)
154
- }
155
-
156
- export function scrollTopForPageUp(
157
- scrollTopLine: number,
158
- maxScrollTop: number,
159
- viewportLines: number,
160
- ): number {
161
- return clampLine(scrollTopLine - pageScrollDistance(viewportLines), maxScrollTop)
162
- }
163
-
164
- export function scrollTopForPageDown(
165
- scrollTopLine: number,
166
- maxScrollTop: number,
167
- viewportLines: number,
168
- ): number {
169
- return clampLine(scrollTopLine + pageScrollDistance(viewportLines), maxScrollTop)
170
- }
171
-
172
- function pageScrollDistance(viewportLines: number): number {
173
- const viewport = Math.max(1, Math.floor(viewportLines))
174
- return Math.max(1, viewport - 2)
175
- }
176
-
177
- function selectRowsForLineWindow<T>(
178
- rows: T[],
179
- heights: number[],
180
- offsets: number[],
181
- budget: number,
182
- startLine: number,
183
- totalLines: number,
184
- maxScrollOffset: number,
185
- ): TranscriptWindowSelection<T> {
186
- const endLine = Math.min(totalLines, startLine + budget)
187
-
188
- const startIndex = Math.min(rows.length - 1, findRowIndexAtLine(offsets, startLine))
189
- const lastVisibleLine = Math.max(startLine, endLine - 1)
190
- const endIndex = endLine >= totalLines
191
- ? rows.length
192
- : Math.min(rows.length, findRowIndexAtLine(offsets, lastVisibleLine) + 1)
193
-
194
- const slices: Array<RowSlice<T>> = []
195
- for (let i = startIndex; i < endIndex; i += 1) {
196
- const row = rows[i]
197
- if (!row) continue
198
- const rowTop = offsets[i] ?? 0
199
- const height = heights[i] ?? 1
200
- const clipStart = Math.max(0, startLine - rowTop)
201
- const clipEnd = Math.min(height, endLine - rowTop)
202
- if (clipEnd <= clipStart) continue
203
- slices.push({ row, clipStart, clipEnd, rowHeight: height })
204
- }
205
-
206
- return {
207
- rows: slices,
208
- hiddenBefore: startIndex,
209
- hiddenAfter: rows.length - endIndex,
210
- totalLines,
211
- maxScrollOffset,
212
- }
213
- }
214
-
215
- export function estimateMessageRowHeight(row: MessageRow, columns = 80): number {
216
- const contentWidth = Math.max(20, columns - 8)
217
- switch (row.role) {
218
- case 'user':
219
- return userRowLineCount(row.content, contentWidth)
220
- case 'assistant':
221
- return assistantRowLineCount(row.content, row.liveTail ?? '', contentWidth, Boolean(row.streaming))
222
- case 'thinking':
223
- return thinkingRowLineCount(row, contentWidth)
224
- case 'tool_call':
225
- return 1
226
- case 'note':
227
- return 1 + wrappedLineCount(row.content, contentWidth)
228
- case 'progress':
229
- return 4
230
- case 'splash':
231
- return 28
232
- }
233
- }
234
-
235
- export function userRowLineCount(content: string, contentWidth: number): number {
236
- const lines = splitLines(content)
237
- return 1 + lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / contentWidth)), 0)
238
- }
239
-
240
- export function assistantRowLineCount(content: string, liveTail: string, _contentWidth: number, streaming = false): number {
241
- const fullText = liveTail ? content + liveTail : content
242
- return 1 + flattenAssistantBody(fullText, streaming).length
243
- }
244
-
245
- export function thinkingRowLineCount(
246
- row: Extract<MessageRow, { role: 'thinking' }>,
247
- _contentWidth: number,
248
- ): number {
249
- const omitted = thinkingDisplayOmittedChars(row)
250
- const overhead = 1 + (omitted > 0 ? 1 : 0) + 1
251
- if (!row.expanded) return overhead
252
- const body = thinkingDisplayBody(row)
253
- const lines = splitLines(body)
254
- return overhead + lines.length
255
- }
256
-
257
- export function thinkingDisplayBody(row: Extract<MessageRow, { role: 'thinking' }>): string {
258
- const text = row.liveTail ? row.content + row.liveTail : row.content
259
- return clipReasoningForDisplayText(text)
260
- }
261
-
262
- export function thinkingDisplayOmittedChars(row: Extract<MessageRow, { role: 'thinking' }>): number {
263
- const text = row.liveTail ? row.content + row.liveTail : row.content
264
- return clipReasoningForDisplayOmitted(text)
265
- }
266
-
267
- const MAX_RENDERED_REASONING_CHARS = 10_000
268
-
269
- function clipReasoningForDisplayText(text: string): string {
270
- if (text.length <= MAX_RENDERED_REASONING_CHARS) return text
271
- const rawStart = Math.max(0, text.length - MAX_RENDERED_REASONING_CHARS)
272
- const newline = text.indexOf('\n', rawStart)
273
- const start = newline >= 0 && newline - rawStart <= 240 ? newline + 1 : rawStart
274
- return text.slice(start)
275
- }
276
-
277
- function clipReasoningForDisplayOmitted(text: string): number {
278
- if (text.length <= MAX_RENDERED_REASONING_CHARS) return 0
279
- const rawStart = Math.max(0, text.length - MAX_RENDERED_REASONING_CHARS)
280
- const newline = text.indexOf('\n', rawStart)
281
- return newline >= 0 && newline - rawStart <= 240 ? newline + 1 : rawStart
282
- }
283
-
284
- export function splitLines(text: string): string[] {
285
- if (!text) return ['']
286
- const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
287
- return normalized.split('\n')
288
- }
289
-
290
- function wrappedLineCount(text: string, width: number): number {
291
- if (!text) return 1
292
- return text
293
- .split(/\r?\n/)
294
- .reduce((total, line) => total + Math.max(1, Math.ceil(line.length / width)), 0)
295
- }
@@ -1,95 +0,0 @@
1
- import React, { useState } from 'react'
2
- import { Box, Text } from 'ink'
3
- import type { ContextUsage } from '../../runtime/compaction.js'
4
- import { useAppInput } from '../../app/input/AppInputProvider.js'
5
- import { theme } from '../../ui/theme.js'
6
-
7
- export type ContextLimitAction = 'compact' | 'switchModel' | 'send' | 'cancel'
8
-
9
- type ContextLimitViewProps = {
10
- usage: ContextUsage
11
- promptPreview: string
12
- onSelect: (action: ContextLimitAction) => void | Promise<void>
13
- onCancel: () => void
14
- }
15
-
16
- export const CONTEXT_LIMIT_OPTIONS: Array<{ action: ContextLimitAction; label: string; detail: string }> = [
17
- {
18
- action: 'compact',
19
- label: 'Summarize and move to new conversation',
20
- detail: 'Summarize this transcript into a new conversation, then send the pending message.',
21
- },
22
- {
23
- action: 'switchModel',
24
- label: 'Switch to larger-context model',
25
- detail: 'Pick a model that can fit this conversation, then send the pending message.',
26
- },
27
- {
28
- action: 'send',
29
- label: 'Ignore warning and send',
30
- detail: 'May hit rate/context limits faster or degrade local/cloud model behavior.',
31
- },
32
- {
33
- action: 'cancel',
34
- label: 'Cancel',
35
- detail: 'Return to the prompt without sending the pending message.',
36
- },
37
- ]
38
-
39
- export const ContextLimitView: React.FC<ContextLimitViewProps> = ({
40
- usage,
41
- promptPreview,
42
- onSelect,
43
- onCancel,
44
- }) => {
45
- const [selected, setSelected] = useState(0)
46
-
47
- useAppInput((_input, key) => {
48
- if (key.escape) {
49
- onCancel()
50
- return
51
- }
52
- if (key.upArrow) {
53
- setSelected(i => Math.max(0, i - 1))
54
- return
55
- }
56
- if (key.downArrow) {
57
- setSelected(i => Math.min(CONTEXT_LIMIT_OPTIONS.length - 1, i + 1))
58
- return
59
- }
60
- if (key.return) {
61
- const picked = CONTEXT_LIMIT_OPTIONS[selected]
62
- if (picked) void onSelect(picked.action)
63
- }
64
- })
65
-
66
- return (
67
- <Box flexDirection="column" borderStyle="round" borderColor={theme.accentPeriwinkle} paddingX={1}>
68
- <Text color={theme.accentPeriwinkle} bold>context limit</Text>
69
- <Text color={theme.dim}>
70
- {`Context ${usage.percent}% · ~${formatTokens(usage.usedTokens)} / ${formatTokens(usage.windowTokens)} tokens (${usage.source}).`}
71
- </Text>
72
- {usage.percent >= 100 ? (
73
- <Text color={theme.accentPeriwinkle}>
74
- This transcript is over the selected model's estimated window. You can still send, but summarizing first is safer.
75
- </Text>
76
- ) : null}
77
- <Text color={theme.textSubtle}>{`Pending: ${promptPreview || '(empty)'}`}</Text>
78
- <Box flexDirection="column" marginTop={1}>
79
- {CONTEXT_LIMIT_OPTIONS.map((option, index) => (
80
- <Text key={option.action} color={index === selected ? theme.accentPeriwinkle : theme.text}>
81
- {index === selected ? '> ' : ' '}
82
- {option.label}
83
- <Text color={theme.dim}>{` · ${option.detail}`}</Text>
84
- </Text>
85
- ))}
86
- </Box>
87
- </Box>
88
- )
89
- }
90
-
91
- function formatTokens(count: number): string {
92
- if (count < 1000) return String(count)
93
- if (count < 10_000) return `${(count / 1000).toFixed(1)}k`
94
- return `${Math.round(count / 1000)}k`
95
- }
@@ -1,50 +0,0 @@
1
- import React from 'react'
2
- import { Box, Text } from 'ink'
3
- import { Surface } from '../../ui/Surface.js'
4
- import { Select } from '../../ui/Select.js'
5
- import { theme } from '../../ui/theme.js'
6
- import { DiffView } from '../display/DiffView.js'
7
-
8
- export type ContinuityEditReviewState = {
9
- file: 'SOUL.md' | 'MEMORY.md'
10
- filePath: string
11
- summary: string
12
- diff?: string
13
- editorOpened?: boolean
14
- }
15
-
16
- export type ContinuityEditReviewAction = 'open' | 'save-publish' | 'later'
17
-
18
- export const ContinuityEditReviewView: React.FC<{
19
- review: ContinuityEditReviewState
20
- onSelect: (action: ContinuityEditReviewAction) => void | Promise<void>
21
- onCancel: () => void
22
- }> = ({ review, onSelect, onCancel }) => (
23
- <Surface
24
- title={`${review.file} Updated`}
25
- footer="enter select · esc dismiss"
26
- >
27
- <Text color={theme.accentPeriwinkle}>{displayContinuityReviewText(review.summary)}</Text>
28
- {review.diff ? (
29
- <Box flexDirection="column" marginTop={1}>
30
- <Text color={theme.accentPeriwinkle}>diff</Text>
31
- <DiffView diff={review.diff} />
32
- </Box>
33
- ) : null}
34
- <Box marginTop={1}>
35
- <Select<ContinuityEditReviewAction>
36
- options={[
37
- { value: 'open', label: `Open ${review.file}`, hint: 'Review in editor' },
38
- { value: 'save-publish', label: 'Save Snapshot', hint: 'Wallet signature' },
39
- { value: 'later', label: 'Dismiss', hint: 'Save later from Identity Hub' },
40
- ]}
41
- onSubmit={onSelect}
42
- onCancel={onCancel}
43
- />
44
- </Box>
45
- </Surface>
46
- )
47
-
48
- function displayContinuityReviewText(value: string): string {
49
- return value ? value[0]!.toUpperCase() + value.slice(1) : value
50
- }
@@ -1,50 +0,0 @@
1
- import React from 'react'
2
- import { Surface } from '../../ui/Surface.js'
3
- import { Select } from '../../ui/Select.js'
4
- import { parseSegments, type Segment } from '../../utils/markdownSegments.js'
5
- import { copyToClipboard, type CopyResult } from '../../utils/clipboard.js'
6
-
7
- type CopyPickerProps = {
8
- turnText: string
9
- turnLabel: string
10
- onDone: (result: CopyResult, label: string) => void
11
- onCancel: () => void
12
- }
13
-
14
- type Choice = { index: number; segment: Segment | null; label: string }
15
-
16
- export const CopyPicker: React.FC<CopyPickerProps> = ({ turnText, turnLabel, onDone, onCancel }) => {
17
- const segments = parseSegments(turnText)
18
- const choices: Choice[] = [
19
- { index: -1, segment: null, label: `all (${turnText.length} chars)` },
20
- ...segments.map((segment, i) => ({ index: i, segment, label: segment.preview })),
21
- ]
22
-
23
- const options = choices.map(c => ({
24
- value: c.index,
25
- label: c.label,
26
- hint: c.segment ? (c.segment.kind === 'code' ? 'code block' : undefined) : 'full reply',
27
- }))
28
-
29
- const handleSubmit = (index: number) => {
30
- const chosen = choices.find(c => c.index === index)
31
- const payload = chosen?.segment ? chosen.segment.content : turnText
32
- const label = chosen?.label ?? 'copy'
33
- void copyToClipboard(payload).then(result => onDone(result, label))
34
- }
35
-
36
- return (
37
- <Surface
38
- title={`Copy From ${turnLabel}`}
39
- subtitle="Choose the full reply or an extracted segment."
40
- footer="enter copies · esc closes"
41
- >
42
- <Select<number>
43
- options={options}
44
- initialIndex={0}
45
- onSubmit={handleSubmit}
46
- onCancel={onCancel}
47
- />
48
- </Surface>
49
- )
50
- }