ethagent 3.3.3 → 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 -259
  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,156 +0,0 @@
1
- import React, { useMemo } 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, diffLineColor } from '../display/DiffView.js'
7
- import type { PermissionDecision, PermissionRequest } from '../../tools/contracts.js'
8
-
9
- export { diffLineColor as permissionDiffLineColor }
10
-
11
- type PermissionPromptProps = {
12
- request: PermissionRequest
13
- onDecision: (decision: PermissionDecision) => void
14
- onCancel: () => void
15
- }
16
-
17
- export const PermissionPrompt: React.FC<PermissionPromptProps> = ({ request, onDecision, onCancel }) => {
18
- const options = useMemo(() => permissionOptionsForRequest(request), [request])
19
-
20
- return (
21
- <Surface
22
- title={request.title}
23
- subtitle={request.subtitle}
24
- tone={request.kind === 'bash' && request.warning ? 'error' : 'primary'}
25
- footer="enter confirms · esc denies"
26
- >
27
- {request.kind === 'private-continuity-edit' ? (
28
- <Box flexDirection="column" marginBottom={1}>
29
- <Text color={theme.accentPeriwinkle}>{displayPermissionText(request.changeSummary)}</Text>
30
- <Text color={theme.textSubtle}>
31
- Not reversible by /rewind. A private identity-history snapshot is saved before writing.
32
- </Text>
33
- <Box marginTop={1}>
34
- <Text color={theme.textSubtle}>target</Text>
35
- </Box>
36
- <Text color={theme.text}>{request.file}</Text>
37
- <Box marginTop={1}>
38
- <Text color={theme.accentPeriwinkle}>diff</Text>
39
- </Box>
40
- <DiffView diff={request.diff} />
41
- </Box>
42
- ) : null}
43
- {request.kind === 'private-continuity-read' ? (
44
- <Box flexDirection="column" marginBottom={1}>
45
- <Text color={theme.accentPeriwinkle}>read private {request.file}</Text>
46
- <Text color={theme.textSubtle}>This reveals private identity continuity to the model for this turn.</Text>
47
- <Box marginTop={1}>
48
- <Text color={theme.textSubtle}>range</Text>
49
- </Box>
50
- <Text color={theme.text}>{request.range}</Text>
51
- </Box>
52
- ) : null}
53
- {request.kind === 'edit' || request.kind === 'write' || request.kind === 'delete' ? (
54
- <Box flexDirection="column" marginBottom={1}>
55
- <Text color={theme.accentPeriwinkle}>{displayPermissionText(request.changeSummary)}</Text>
56
- <Box marginTop={1}>
57
- <Text color={theme.accentPeriwinkle}>diff</Text>
58
- </Box>
59
- <DiffView diff={request.diff} />
60
- </Box>
61
- ) : null}
62
- {request.kind === 'bash' && request.warning ? (
63
- <Box marginBottom={1}>
64
- <Text color={theme.accentError}>{request.warning}</Text>
65
- </Box>
66
- ) : null}
67
- <Select options={options} onSubmit={onDecision} onCancel={onCancel} />
68
- </Surface>
69
- )
70
- }
71
-
72
- export function permissionOptionsForRequest(request: PermissionRequest): Array<{ value: PermissionDecision; label: string; hint?: string; disabled?: boolean }> {
73
- if (request.kind === 'bash') {
74
- return [
75
- { value: 'allow-once', label: 'Allow once', hint: 'Approve only this command execution' },
76
- {
77
- value: 'allow-command-project',
78
- label: 'Allow exact command',
79
- hint: 'Remember this exact command for this project',
80
- disabled: !request.canPersistExact,
81
- },
82
- {
83
- value: 'allow-command-prefix-project',
84
- label: request.commandPrefix ? `Allow ${request.commandPrefix} commands` : 'Allow command family',
85
- hint: 'Remember this base command in this working directory for this project',
86
- disabled: !request.canPersistPrefix,
87
- },
88
- { value: 'deny', label: 'Deny', hint: 'Return a denial to the model' },
89
- ]
90
- }
91
-
92
- if (request.kind === 'mcp') {
93
- const risk = request.destructive
94
- ? 'Server marks this tool as destructive'
95
- : request.openWorld
96
- ? 'Server marks this tool as open-world'
97
- : request.readOnly
98
- ? 'Server marks this tool as read-only'
99
- : 'Server did not mark this tool read-only'
100
- return [
101
- { value: 'allow-once', label: 'Allow once', hint: risk },
102
- { value: 'allow-mcp-tool-project', label: 'Always allow this MCP tool', hint: request.toolKey },
103
- {
104
- value: 'allow-mcp-server-project',
105
- label: `Always allow ${request.serverName}`,
106
- hint: 'Remember all tools from this MCP server for this project',
107
- disabled: !request.canPersistServer,
108
- },
109
- { value: 'deny', label: 'Deny', hint: 'Return a denial back to the model' },
110
- ]
111
- }
112
-
113
- if (request.kind === 'delete') {
114
- return [
115
- { value: 'allow-once', label: 'Delete this file', hint: 'Approve this deletion only' },
116
- { value: 'deny', label: 'Deny', hint: 'Keep the file unchanged' },
117
- ]
118
- }
119
-
120
- if (request.kind === 'private-continuity-read') {
121
- return [
122
- { value: 'allow-once', label: 'Allow once', hint: `Read ${request.file}` },
123
- { value: 'deny', label: 'Deny', hint: 'Keep private continuity hidden' },
124
- ]
125
- }
126
-
127
- if (request.kind === 'private-continuity-edit') {
128
- return [
129
- { value: 'allow-once', label: 'Approve once', hint: `Apply this edit to ${request.file}` },
130
- { value: 'deny', label: 'Deny', hint: 'Keep private continuity unchanged' },
131
- ]
132
- }
133
-
134
- return [
135
- { value: 'allow-once', label: 'Allow once', hint: 'Approve only this action' },
136
- { value: 'allow-path-project', label: 'Always allow this file', hint: request.relativePath },
137
- { value: 'allow-directory-project', label: 'Always allow this folder', hint: request.directoryPath },
138
- {
139
- value: 'allow-kind-project',
140
- label:
141
- request.kind === 'edit'
142
- ? 'Always allow edits'
143
- : request.kind === 'write'
144
- ? 'Always allow writes'
145
- : request.kind === 'cd'
146
- ? 'Always allow directory changes'
147
- : 'Always allow reads',
148
- hint: 'Remember this tool kind for this project',
149
- },
150
- { value: 'deny', label: 'Deny', hint: 'Return a denial back to the model' },
151
- ]
152
- }
153
-
154
- function displayPermissionText(value: string): string {
155
- return value ? value[0]!.toUpperCase() + value.slice(1) : value
156
- }
@@ -1,165 +0,0 @@
1
- import React, { useEffect, useMemo, useState } from 'react'
2
- import { Box, Text } from 'ink'
3
- import { clearPermissionRules, deletePermissionRule, loadPermissionRules } from '../../storage/permissions.js'
4
- import type { SessionPermissionRule } from '../../tools/contracts.js'
5
- import { Select, type SelectOption } from '../../ui/Select.js'
6
- import { Spinner } from '../../ui/Spinner.js'
7
- import { Surface } from '../../ui/Surface.js'
8
- import { theme } from '../../ui/theme.js'
9
-
10
- type PermissionsViewProps = {
11
- cwd: string
12
- onRulesChanged: (rules: SessionPermissionRule[]) => void
13
- onNotice: (message: string, variant?: 'info' | 'error' | 'dim') => void
14
- onCancel: () => void
15
- }
16
-
17
- type State =
18
- | { kind: 'loading' }
19
- | { kind: 'error'; message: string }
20
- | { kind: 'ready'; rules: SessionPermissionRule[] }
21
-
22
- const CLEAR_ALL_VALUE = '__clear_all__'
23
-
24
- export const PermissionsView: React.FC<PermissionsViewProps> = ({
25
- cwd,
26
- onRulesChanged,
27
- onNotice,
28
- onCancel,
29
- }) => {
30
- const [state, setState] = useState<State>({ kind: 'loading' })
31
-
32
- useEffect(() => {
33
- let cancelled = false
34
- void (async () => {
35
- try {
36
- const rules = await loadPermissionRules(cwd)
37
- if (!cancelled) setState({ kind: 'ready', rules })
38
- } catch (err: unknown) {
39
- if (!cancelled) setState({ kind: 'error', message: (err as Error).message })
40
- }
41
- })()
42
- return () => { cancelled = true }
43
- }, [cwd])
44
-
45
- const refreshRules = async () => {
46
- const rules = await loadPermissionRules(cwd)
47
- setState({ kind: 'ready', rules })
48
- onRulesChanged(rules)
49
- return rules
50
- }
51
-
52
- const options = useMemo(
53
- () => state.kind === 'ready' ? buildOptions(state.rules) : [],
54
- [state],
55
- )
56
-
57
- if (state.kind === 'loading') {
58
- return (
59
- <Surface title="Permissions" subtitle="Loading saved project rules...">
60
- <Spinner label="loading permission rules..." />
61
- </Surface>
62
- )
63
- }
64
-
65
- if (state.kind === 'error') {
66
- return (
67
- <Surface title="Permissions" tone="muted" footer="esc closes">
68
- <Text color={theme.dim}>{state.message}</Text>
69
- </Surface>
70
- )
71
- }
72
-
73
- if (state.rules.length === 0) {
74
- return (
75
- <Surface title="Permissions" tone="muted" footer="esc closes">
76
- <Text color={theme.dim}>No saved permission rules for this project.</Text>
77
- </Surface>
78
- )
79
- }
80
-
81
- return (
82
- <Surface
83
- title="Permissions"
84
- subtitle="Saved rules for this project. Enter removes the selected rule."
85
- footer="enter removes · esc closes"
86
- >
87
- <Select
88
- options={options}
89
- onSubmit={async value => {
90
- try {
91
- if (value === CLEAR_ALL_VALUE) {
92
- await clearPermissionRules(cwd)
93
- onRulesChanged([])
94
- onCancel()
95
- onNotice('cleared saved permission rules for this project.', 'dim')
96
- return
97
- }
98
-
99
- await deletePermissionRule(cwd, value)
100
- const remaining = await refreshRules()
101
- if (remaining.length === 0) {
102
- onCancel()
103
- onNotice('removed the last saved permission rule for this project.', 'dim')
104
- return
105
- }
106
- onNotice(`removed permission rule: ${describeRule(value)}`, 'dim')
107
- } catch (err: unknown) {
108
- onNotice(`failed to update permission rules: ${(err as Error).message}`, 'error')
109
- }
110
- }}
111
- onCancel={onCancel}
112
- />
113
- <Box marginTop={1} flexDirection="column">
114
- <Text color={theme.dim}>Rules apply only within the current project root.</Text>
115
- </Box>
116
- </Surface>
117
- )
118
- }
119
-
120
- function buildOptions(rules: SessionPermissionRule[]): Array<SelectOption<SessionPermissionRule | typeof CLEAR_ALL_VALUE>> {
121
- const out: Array<SelectOption<SessionPermissionRule | typeof CLEAR_ALL_VALUE>> = []
122
- if (rules.length > 0) {
123
- out.push({ value: CLEAR_ALL_VALUE, role: 'section', label: 'Saved Rules' })
124
- for (const rule of rules) {
125
- out.push({
126
- value: rule,
127
- label: describeRule(rule),
128
- hint: describeRuleScope(rule),
129
- })
130
- }
131
- }
132
- out.push({ value: CLEAR_ALL_VALUE, role: 'section', label: 'Manage' })
133
- out.push({
134
- value: CLEAR_ALL_VALUE,
135
- label: 'Remove all saved rules',
136
- hint: 'Clear all remembered permissions for this project',
137
- })
138
- return out
139
- }
140
-
141
- function describeRule(rule: SessionPermissionRule): string {
142
- if (rule.kind === 'bash') {
143
- if (rule.scope === 'command') return `bash exact: ${rule.command}`
144
- return `bash prefix: ${rule.commandPrefix}`
145
- }
146
- if (rule.kind === 'mcp') {
147
- if (rule.scope === 'tool') return `mcp tool: ${rule.toolKey}`
148
- return `mcp server: ${rule.normalizedServerName}`
149
- }
150
- if (rule.scope === 'kind') {
151
- return rule.kind === 'read'
152
- ? 'allow all reads'
153
- : rule.kind === 'edit'
154
- ? 'allow all edits'
155
- : 'allow all directory changes'
156
- }
157
- if (rule.scope === 'path') return `${rule.kind} file: ${rule.path}`
158
- return `${rule.kind} folder: ${rule.path}`
159
- }
160
-
161
- function describeRuleScope(rule: SessionPermissionRule): string {
162
- if (rule.kind === 'bash') return `cwd ${rule.cwd}`
163
- if (rule.kind === 'mcp') return 'MCP project permission'
164
- return rule.scope === 'kind' ? 'whole project' : rule.path
165
- }
@@ -1,91 +0,0 @@
1
- import React, { useState } from 'react'
2
- import { Box, Text } from 'ink'
3
- import { Surface } from '../../ui/Surface.js'
4
- import { theme } from '../../ui/theme.js'
5
- import { useAppInput } from '../../app/input/AppInputProvider.js'
6
-
7
- export type PlanApprovalAction = 'apply' | 'apply-summary' | 'continue'
8
-
9
- type PlanApprovalViewProps = {
10
- contextLabel: string
11
- onSelect: (action: PlanApprovalAction) => void
12
- onCancel: () => void
13
- }
14
-
15
- export const PLAN_APPROVAL_OPTIONS: Array<{
16
- value: PlanApprovalAction
17
- label: string
18
- title: string
19
- detail: (contextLabel: string) => string
20
- }> = [
21
- {
22
- value: 'apply',
23
- label: 'Yes, implement this plan',
24
- title: 'Switch to Accept Edits and start coding.',
25
- detail: contextLabel => `Same conversation. ${contextLabel}.`,
26
- },
27
- {
28
- value: 'apply-summary',
29
- label: 'Yes, start a new conversation',
30
- title: 'Summarize context and start coding.',
31
- detail: () => 'Keeps this conversation active and carries summary plus plan.',
32
- },
33
- {
34
- value: 'continue',
35
- label: 'No, stay in plan mode',
36
- title: 'Continue planning with the model.',
37
- detail: () => 'No files will be changed.',
38
- },
39
- ]
40
-
41
- export const PlanApprovalView: React.FC<PlanApprovalViewProps> = ({
42
- contextLabel,
43
- onSelect,
44
- onCancel,
45
- }) => {
46
- const [index, setIndex] = useState(0)
47
- const selected = PLAN_APPROVAL_OPTIONS[index] ?? PLAN_APPROVAL_OPTIONS[0]!
48
-
49
- useAppInput((input, key) => {
50
- if (key.upArrow || input === 'k') {
51
- setIndex(current => (current - 1 + PLAN_APPROVAL_OPTIONS.length) % PLAN_APPROVAL_OPTIONS.length)
52
- } else if (key.downArrow || input === 'j') {
53
- setIndex(current => (current + 1) % PLAN_APPROVAL_OPTIONS.length)
54
- } else if (key.return) {
55
- onSelect(selected.value)
56
- } else if (key.escape) {
57
- onCancel()
58
- }
59
- })
60
-
61
- return (
62
- <Surface
63
- title="Implement this plan?"
64
- tone="muted"
65
- footer="Press enter to confirm or esc to go back"
66
- >
67
- <Box flexDirection="row">
68
- <Box flexDirection="column" minWidth={36}>
69
- {PLAN_APPROVAL_OPTIONS.map((option, optionIndex) => {
70
- const active = optionIndex === index
71
- return (
72
- <Box key={option.value} flexDirection="row">
73
- <Text color={active ? theme.accentPeriwinkle : theme.dim}>
74
- {active ? '> ' : ' '}
75
- {optionIndex + 1}.{' '}
76
- </Text>
77
- <Text color={active ? theme.accentPeriwinkle : theme.text} bold={active}>
78
- {option.label}
79
- </Text>
80
- </Box>
81
- )
82
- })}
83
- </Box>
84
- <Box flexDirection="column" marginLeft={4} flexShrink={1}>
85
- <Text color={theme.accentPeriwinkle} bold>{selected.title}</Text>
86
- <Text color={theme.dim}>{selected.detail(contextLabel)}</Text>
87
- </Box>
88
- </Box>
89
- </Surface>
90
- )
91
- }
@@ -1,273 +0,0 @@
1
- import React, { useEffect, useState } from 'react'
2
- import { Box, Text } from 'ink'
3
- import { theme } from '../../ui/theme.js'
4
- import { Select, type SelectOption } from '../../ui/Select.js'
5
- import { Spinner } from '../../ui/Spinner.js'
6
- import { Surface } from '../../ui/Surface.js'
7
- import { listSessions, type SessionSummary } from '../../storage/sessions.js'
8
- import { useAppInput } from '../../app/input/AppInputProvider.js'
9
-
10
- type ResumeViewProps = {
11
- currentSessionId: string
12
- onResume: (sessionId: string) => void
13
- onClearAll: () => void | Promise<void>
14
- onCancel: () => void
15
- }
16
-
17
- type State =
18
- | { kind: 'loading' }
19
- | { kind: 'error'; message: string }
20
- | { kind: 'ready'; sessions: SessionSummary[] }
21
- | { kind: 'confirmClear'; sessions: SessionSummary[]; error?: string }
22
- | { kind: 'clearing'; sessions: SessionSummary[] }
23
-
24
- export const CLEAR_ALL_SESSIONS_VALUE = '__clear_all_sessions__'
25
-
26
- export const ResumeView: React.FC<ResumeViewProps> = ({ currentSessionId, onResume, onClearAll, onCancel }) => {
27
- const [state, setState] = useState<State>({ kind: 'loading' })
28
-
29
- const escActive = state.kind === 'loading' || state.kind === 'error' || (state.kind === 'ready' && state.sessions.length === 0)
30
- useAppInput((_input, key) => {
31
- if (key.escape) onCancel()
32
- }, { isActive: escActive })
33
-
34
- useEffect(() => {
35
- let cancelled = false
36
- void (async () => {
37
- try {
38
- const all = await listSessions(50)
39
- if (cancelled) return
40
- setState({ kind: 'ready', sessions: all })
41
- } catch (err: unknown) {
42
- if (cancelled) return
43
- setState({ kind: 'error', message: (err as Error).message })
44
- }
45
- })()
46
- return () => { cancelled = true }
47
- }, [currentSessionId])
48
-
49
- if (state.kind === 'loading') {
50
- return (
51
- <Surface title="Resume Session" subtitle="Recent chats and directories." footer="esc closes">
52
- <Spinner label="loading..." />
53
- </Surface>
54
- )
55
- }
56
-
57
- if (state.kind === 'error') {
58
- return (
59
- <Surface title="Resume Session" tone="muted" footer="esc closes">
60
- <Text color={theme.dim}>{state.message}</Text>
61
- </Surface>
62
- )
63
- }
64
-
65
- if (state.kind === 'confirmClear') {
66
- return (
67
- <Surface
68
- title="Clear All Saved Sessions?"
69
- subtitle={`${state.sessions.length} saved session${state.sessions.length === 1 ? '' : 's'} will be removed.`}
70
- tone="error"
71
- footer="enter selects · esc returns to resume"
72
- >
73
- <Box flexDirection="column" marginBottom={1}>
74
- <Text color={theme.dim}>Removes saved chats and resume context from this machine.</Text>
75
- <Text color={theme.dim}>Config, identities, keys, and local models stay.</Text>
76
- {state.error ? <Text color={theme.accentError}>{state.error}</Text> : null}
77
- </Box>
78
- <Select<'back' | 'clear'>
79
- hintLayout="inline"
80
- options={[
81
- { value: 'back', label: 'Back to Sessions' },
82
- { value: 'clear', label: 'Clear All Saved Sessions', hint: 'Cannot be undone' },
83
- ]}
84
- onSubmit={choice => {
85
- if (choice === 'back') {
86
- setState({ kind: 'ready', sessions: state.sessions })
87
- return
88
- }
89
- void clearAll(state.sessions, onClearAll, setState)
90
- }}
91
- onCancel={() => setState({ kind: 'ready', sessions: state.sessions })}
92
- />
93
- </Surface>
94
- )
95
- }
96
-
97
- if (state.kind === 'clearing') {
98
- return (
99
- <Surface title="Clearing Chat Logs" subtitle="Removing saved chats and resume context.">
100
- <Spinner label="clearing sessions..." />
101
- </Surface>
102
- )
103
- }
104
-
105
- if (state.sessions.length === 0) {
106
- return (
107
- <Surface title="Resume Session" tone="muted" footer="esc closes">
108
- <Text color={theme.dim}>No prior sessions to resume.</Text>
109
- </Surface>
110
- )
111
- }
112
-
113
- const options = buildResumeOptions(state.sessions, currentSessionId)
114
-
115
- return (
116
- <Surface
117
- title="Resume Session"
118
- subtitle="Grouped by working directory."
119
- footer="enter resumes · esc closes"
120
- >
121
- <Box flexDirection="column" marginBottom={1}>
122
- <Text color={theme.dim}>Recent directories</Text>
123
- </Box>
124
- <Select
125
- options={options}
126
- initialIndex={findInitialIndex(options, currentSessionId)}
127
- maxVisible={14}
128
- onSubmit={value => {
129
- if (value === CLEAR_ALL_SESSIONS_VALUE) {
130
- setState({ kind: 'confirmClear', sessions: state.sessions })
131
- return
132
- }
133
- onResume(value)
134
- }}
135
- onCancel={onCancel}
136
- />
137
- </Surface>
138
- )
139
- }
140
-
141
- export function buildResumeOptions(
142
- sessions: SessionSummary[],
143
- currentSessionId: string,
144
- ): Array<SelectOption<string>> {
145
- const groups = new Map<string, SessionSummary[]>()
146
- for (const session of sessions) {
147
- const key = session.lastCwd || session.workspaceRoot || session.projectRoot
148
- const existing = groups.get(key) ?? []
149
- existing.push(session)
150
- groups.set(key, existing)
151
- }
152
-
153
- const options: Array<SelectOption<string>> = []
154
- const manageSpacer: SelectOption<string> = {
155
- value: 'separator:spacer',
156
- label: '',
157
- disabled: true,
158
- }
159
- const manageHeader: SelectOption<string> = {
160
- value: 'separator:manage',
161
- label: 'Manage',
162
- role: 'section',
163
- bold: true,
164
- disabled: true,
165
- }
166
-
167
- const clearOption: SelectOption<string> = {
168
- value: CLEAR_ALL_SESSIONS_VALUE,
169
- label: 'Clear All Saved Sessions',
170
- hint: 'Removes saved chats and resume context',
171
- role: 'utility',
172
- }
173
-
174
- const orderedGroups = [...groups.entries()].sort(([, left], [, right]) => right[0]!.mtimeMs - left[0]!.mtimeMs)
175
-
176
- for (const [directoryKey, group] of orderedGroups) {
177
- const sorted = [...group].sort((left, right) => right.mtimeMs - left.mtimeMs)
178
- const sessionCount = sorted.length
179
- const lastActivity = formatRelative(sorted[0]!.mtimeMs)
180
- options.push({
181
- value: `directory:${directoryKey}`,
182
- label: lastPathSegment(directoryKey) || compressProjectPath(directoryKey),
183
- hint: `${compressProjectPath(directoryKey)} · ${sessionCount} session${sessionCount === 1 ? '' : 's'} · last ${lastActivity}`,
184
- role: 'section',
185
- bold: true,
186
- })
187
-
188
- for (const session of sorted) {
189
- const baseLabel = formatFirstLine(session.firstUserMessage) || '(empty session)'
190
- const markers = [
191
- session.id === currentSessionId ? 'current' : '',
192
- ].filter(Boolean)
193
- const label = markers.length > 0 ? `${baseLabel} (${markers.join(', ')})` : baseLabel
194
- const summaryHint = session.compactedFromSessionId
195
- ? `summary from ${session.compactedFromSessionId.slice(0, 8)}`
196
- : null
197
- const hintParts = [
198
- `${session.turnCount} turn${session.turnCount === 1 ? '' : 's'}`,
199
- formatRelative(session.mtimeMs),
200
- session.id.slice(0, 8),
201
- summaryHint,
202
- ].filter(Boolean)
203
- options.push({
204
- value: session.id,
205
- label,
206
- hint: hintParts.join(' · '),
207
- indent: 2,
208
- })
209
- }
210
- }
211
-
212
- options.push(manageSpacer)
213
- options.push(manageHeader)
214
- options.push(clearOption)
215
-
216
- return options
217
- }
218
-
219
- function findInitialIndex(options: Array<SelectOption<string>>, currentSessionId: string): number {
220
- const currentIndex = options.findIndex(option => option.value === currentSessionId)
221
- if (currentIndex >= 0) return currentIndex
222
- return Math.max(
223
- 0,
224
- options.findIndex(option =>
225
- !option.disabled
226
- && option.role !== 'section'
227
- && option.role !== 'group'
228
- && option.value !== CLEAR_ALL_SESSIONS_VALUE,
229
- ),
230
- )
231
- }
232
-
233
- async function clearAll(
234
- sessions: SessionSummary[],
235
- onClearAll: () => void | Promise<void>,
236
- setState: (state: State) => void,
237
- ): Promise<void> {
238
- setState({ kind: 'clearing', sessions })
239
- try {
240
- await onClearAll()
241
- } catch (err: unknown) {
242
- setState({ kind: 'confirmClear', sessions, error: (err as Error).message })
243
- }
244
- }
245
-
246
- function compressProjectPath(input: string): string {
247
- const home = process.env.USERPROFILE || process.env.HOME || ''
248
- return home && input.startsWith(home) ? `~${input.slice(home.length)}` : input
249
- }
250
-
251
- function lastPathSegment(input: string): string {
252
- const trimmed = input.replace(/[\\/]+$/, '')
253
- const slash = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'))
254
- return slash >= 0 ? trimmed.slice(slash + 1) : trimmed
255
- }
256
-
257
- function formatFirstLine(text: string): string {
258
- const firstLine = text.split('\n', 1)[0] ?? ''
259
- if (firstLine.length <= 56) return firstLine
260
- return `${firstLine.slice(0, 53)}...`
261
- }
262
-
263
- function formatRelative(ms: number): string {
264
- const diffMs = Date.now() - ms
265
- const minutes = Math.floor(diffMs / 60_000)
266
- if (minutes < 1) return 'just now'
267
- if (minutes < 60) return `${minutes}m ago`
268
- const hours = Math.floor(minutes / 60)
269
- if (hours < 24) return `${hours}h ago`
270
- const days = Math.floor(hours / 24)
271
- if (days < 7) return `${days}d ago`
272
- return new Date(ms).toISOString().slice(0, 10)
273
- }