ethagent 2.1.1 → 2.3.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 (177) hide show
  1. package/package.json +2 -1
  2. package/src/app/FirstRun.tsx +1 -7
  3. package/src/app/FirstRunTimeline.tsx +1 -1
  4. package/src/auth/openaiOAuth/credentials.ts +47 -0
  5. package/src/auth/openaiOAuth/crypto.ts +23 -0
  6. package/src/auth/openaiOAuth/index.ts +238 -0
  7. package/src/auth/openaiOAuth/landingPage.ts +125 -0
  8. package/src/auth/openaiOAuth/listener.ts +151 -0
  9. package/src/auth/openaiOAuth/refresh.ts +70 -0
  10. package/src/auth/openaiOAuth/shared.ts +115 -0
  11. package/src/chat/ChatBottomPane.tsx +20 -11
  12. package/src/chat/ChatScreen.tsx +160 -35
  13. package/src/chat/ConversationStack.tsx +1 -1
  14. package/src/chat/MessageList.tsx +185 -72
  15. package/src/chat/SessionStatus.tsx +3 -1
  16. package/src/chat/chatScreenUtils.ts +11 -15
  17. package/src/chat/chatSessionState.ts +3 -2
  18. package/src/chat/chatTurnOrchestrator.ts +1 -7
  19. package/src/chat/commands.ts +28 -27
  20. package/src/chat/display/DiffView.tsx +193 -0
  21. package/src/chat/display/SyntaxText.tsx +192 -0
  22. package/src/chat/display/toolCallDisplay.ts +103 -0
  23. package/src/chat/display/toolResultDisplay.ts +19 -0
  24. package/src/chat/{ChatInput.tsx → input/ChatInput.tsx} +36 -23
  25. package/src/chat/{TranscriptView.tsx → transcript/TranscriptView.tsx} +24 -50
  26. package/src/chat/{transcriptViewport.ts → transcript/transcriptViewport.ts} +12 -30
  27. package/src/chat/{ContextLimitView.tsx → views/ContextLimitView.tsx} +3 -3
  28. package/src/chat/{ContinuityEditReviewView.tsx → views/ContinuityEditReviewView.tsx} +11 -3
  29. package/src/chat/{CopyPicker.tsx → views/CopyPicker.tsx} +4 -5
  30. package/src/chat/{PermissionPrompt.tsx → views/PermissionPrompt.tsx} +16 -17
  31. package/src/chat/{PermissionsView.tsx → views/PermissionsView.tsx} +6 -6
  32. package/src/chat/{PlanApprovalView.tsx → views/PlanApprovalView.tsx} +4 -4
  33. package/src/chat/{ResumeView.tsx → views/ResumeView.tsx} +35 -35
  34. package/src/chat/views/RewindView.tsx +410 -0
  35. package/src/identity/continuity/privateEdit/diff.ts +2 -78
  36. package/src/identity/ens/agentRecords.ts +5 -19
  37. package/src/identity/ens/ensAutomation/setup.ts +0 -1
  38. package/src/identity/ens/ensAutomation/types.ts +0 -1
  39. package/src/identity/hub/OperationalRoutes.tsx +23 -32
  40. package/src/identity/hub/Routes.tsx +13 -13
  41. package/src/identity/hub/{flows/continuity → continuity}/ContinuityDashboardScreen.tsx +9 -9
  42. package/src/identity/hub/{flows/continuity → continuity}/RebackupStorageScreen.tsx +2 -2
  43. package/src/identity/hub/{flows/continuity → continuity}/RecoveryConfirmScreen.tsx +5 -5
  44. package/src/identity/hub/{flows/continuity → continuity}/SavePromptScreen.tsx +5 -5
  45. package/src/identity/hub/{effects/rebackup/runRebackup.ts → continuity/effects.ts} +19 -19
  46. package/src/identity/hub/{effects/rebackup → continuity}/index.ts +1 -1
  47. package/src/identity/hub/{effects/shared → continuity}/snapshot.ts +8 -8
  48. package/src/identity/hub/{effects/rebackup → continuity}/vault.ts +15 -15
  49. package/src/identity/hub/{flows/create → create}/CreateFlow.tsx +13 -13
  50. package/src/identity/hub/{effects/create.ts → create/effects.ts} +4 -4
  51. package/src/identity/hub/{flows/custody → custody}/CustodyEditFlow.tsx +10 -48
  52. package/src/identity/hub/{flows/custody/custodyFlowActions.ts → custody/actions.ts} +11 -9
  53. package/src/identity/hub/{flows/custody/custodyFlowHelpers.ts → custody/helpers.ts} +4 -4
  54. package/src/identity/hub/{effects/vault → custody}/preflight.ts +5 -5
  55. package/src/identity/hub/{flows/custody/custodyFlowRoutes.tsx → custody/routes.tsx} +8 -8
  56. package/src/identity/hub/{flows/custody/custodyEffects.ts → custody/transactions.ts} +9 -9
  57. package/src/identity/hub/{flows/custody/custodyFlowTypes.ts → custody/types.ts} +6 -6
  58. package/src/identity/hub/{flows/custody/custodyFlowEffects.ts → custody/useCustodyEffects.ts} +7 -7
  59. package/src/identity/hub/{flows/custody → custody}/useCustodyFlow.tsx +5 -5
  60. package/src/identity/hub/ens/EnsEditAdvancedScreens.tsx +241 -0
  61. package/src/identity/hub/{flows/ens → ens}/EnsEditFlow.tsx +27 -82
  62. package/src/identity/hub/{flows/ens → ens}/EnsEditMaintenanceScreens.tsx +25 -65
  63. package/src/identity/hub/{flows/ens → ens}/EnsEditReviewScreens.tsx +12 -30
  64. package/src/identity/hub/ens/EnsEditRunners.tsx +62 -0
  65. package/src/identity/hub/{flows/ens → ens}/EnsEditShared.tsx +15 -14
  66. package/src/identity/hub/{flows/ens → ens}/EnsEditSimpleScreens.tsx +68 -217
  67. package/src/identity/hub/{flows/ens/IdentityHubEnsFlow.tsx → ens/EnsFlow.tsx} +18 -11
  68. package/src/identity/hub/{flows/ens/OperatorWalletsScreen.tsx → ens/EnsOperatorWalletsScreen.tsx} +17 -48
  69. package/src/identity/hub/{advancedEnsValidation.ts → ens/advancedEnsValidation.ts} +2 -2
  70. package/src/identity/hub/{flows/ens/ensEditCopy.ts → ens/editCopy.ts} +4 -4
  71. package/src/identity/hub/{effects/ens/flows.ts → ens/effects.ts} +7 -7
  72. package/src/identity/hub/{effects/ens → ens}/index.ts +1 -1
  73. package/src/identity/hub/{model/ens.ts → ens/state.ts} +1 -1
  74. package/src/identity/hub/{effects/ens → ens}/transactions.ts +232 -232
  75. package/src/identity/hub/{flows/ens/ensEditTypes.ts → ens/types.ts} +12 -26
  76. package/src/identity/hub/identityHubReducer.ts +3 -3
  77. package/src/identity/hub/{flows/profile → profile}/EditProfileFlow.tsx +17 -10
  78. package/src/identity/hub/{effects/publicProfile/runPublicProfileSave.ts → profile/effects.ts} +55 -177
  79. package/src/identity/hub/{model → profile}/identity.ts +3 -3
  80. package/src/identity/hub/{effects/profile/profileState.ts → profile/state.ts} +181 -173
  81. package/src/identity/hub/{flows/restore → restore}/RestoreFlow.tsx +21 -21
  82. package/src/identity/hub/{effects/restore → restore}/apply.ts +10 -10
  83. package/src/identity/hub/{effects/restore → restore}/auth.ts +7 -7
  84. package/src/identity/hub/{effects/restore → restore}/discover.ts +6 -6
  85. package/src/identity/hub/{effects/restore → restore}/envelopes.ts +2 -2
  86. package/src/identity/hub/{effects/restore → restore}/fetch.ts +3 -3
  87. package/src/identity/hub/{effects/restore/shared.ts → restore/helpers.ts} +6 -6
  88. package/src/identity/hub/{effects/restore → restore}/recovery.ts +10 -10
  89. package/src/identity/hub/{effects/restore → restore}/resolve.ts +4 -4
  90. package/src/identity/hub/restore/restoreAdmin.ts +34 -0
  91. package/src/identity/hub/{flows/restore/useRestoreFlowEffects.ts → restore/useRestoreEffects.ts} +5 -5
  92. package/src/identity/hub/{flows/settings → settings}/StorageCredentialScreen.tsx +5 -5
  93. package/src/identity/hub/{components → shared/components}/BusyScreen.tsx +4 -4
  94. package/src/identity/hub/{components → shared/components}/DetailsScreen.tsx +4 -4
  95. package/src/identity/hub/{components → shared/components}/ErrorScreen.tsx +4 -4
  96. package/src/identity/hub/{components → shared/components}/FlowTimeline.tsx +1 -1
  97. package/src/identity/hub/{components → shared/components}/IdentitySummary.tsx +16 -11
  98. package/src/identity/hub/{components → shared/components}/MenuScreen.tsx +8 -9
  99. package/src/identity/hub/{components → shared/components}/NetworkScreen.tsx +4 -4
  100. package/src/identity/hub/{components → shared/components}/PinataJwtInput.tsx +4 -4
  101. package/src/identity/hub/{components → shared/components}/UnlinkedIdentityScreen.tsx +5 -5
  102. package/src/identity/hub/{components → shared/components}/WalletApprovalScreen.tsx +6 -6
  103. package/src/identity/hub/{components → shared/components}/menuFlagsFromReconciliation.ts +2 -4
  104. package/src/identity/hub/{effects/shared → shared/effects}/profilePrep.ts +1 -1
  105. package/src/identity/hub/{effects → shared/effects}/receipts.ts +2 -2
  106. package/src/identity/hub/{effects/shared → shared/effects}/sync.ts +6 -47
  107. package/src/identity/hub/{effects → shared/effects}/types.ts +3 -3
  108. package/src/identity/hub/{model → shared/model}/copy.ts +2 -2
  109. package/src/identity/hub/{model → shared/model}/errors.ts +5 -5
  110. package/src/identity/hub/{model → shared/model}/network.ts +3 -3
  111. package/src/identity/hub/{operatorWallets.ts → shared/operatorWallets.ts} +1 -1
  112. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/hook.ts +1 -2
  113. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/ownership.ts +2 -2
  114. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/run.ts +7 -40
  115. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/types.ts +0 -4
  116. package/src/identity/hub/{reconciliation → shared/reconciliation}/index.ts +0 -7
  117. package/src/identity/hub/shared/reconciliation/walletSetup.ts +27 -0
  118. package/src/identity/hub/{utils.ts → shared/utils.ts} +5 -5
  119. package/src/identity/hub/{flows/token-transfer/IdentityHubTokenTransferFlow.tsx → transfer/TokenTransferFlow.tsx} +8 -8
  120. package/src/identity/hub/{flows/token-transfer → transfer}/TokenTransferScreens.tsx +14 -14
  121. package/src/identity/hub/{effects/token-transfer/runTokenTransfer.ts → transfer/effects.ts} +16 -16
  122. package/src/identity/hub/{effects/token-transfer → transfer}/progress.ts +1 -1
  123. package/src/identity/hub/useIdentityHubController.ts +11 -11
  124. package/src/identity/hub/useIdentityHubSideEffects.ts +11 -11
  125. package/src/identity/wallet/browserWallet/types.ts +0 -5
  126. package/src/identity/wallet/page/copy.ts +1 -31
  127. package/src/identity/wallet/walletPurposeCompat.ts +0 -2
  128. package/src/models/ModelPicker.tsx +248 -8
  129. package/src/models/catalog.ts +29 -1
  130. package/src/models/modelPickerOptions.ts +12 -10
  131. package/src/models/providerDisplay.ts +16 -0
  132. package/src/providers/errors.ts +6 -4
  133. package/src/providers/openai-chat.ts +2 -1
  134. package/src/providers/openai-responses-format.ts +156 -0
  135. package/src/providers/openai-responses.ts +276 -0
  136. package/src/providers/registry.ts +85 -8
  137. package/src/runtime/sessionMode.ts +1 -1
  138. package/src/runtime/systemPrompt.ts +4 -2
  139. package/src/runtime/toolExecution.ts +9 -6
  140. package/src/runtime/turn.ts +29 -1
  141. package/src/storage/rewind.ts +20 -0
  142. package/src/storage/secrets.ts +4 -1
  143. package/src/storage/sessions.ts +2 -1
  144. package/src/tools/bashSafety.ts +7 -3
  145. package/src/tools/bashTool.ts +1 -1
  146. package/src/tools/contracts.ts +3 -0
  147. package/src/tools/deleteFileTool.ts +8 -3
  148. package/src/tools/editTool.ts +10 -5
  149. package/src/tools/fileDiff.ts +261 -0
  150. package/src/tools/privateContinuityEditTool.ts +11 -1
  151. package/src/tools/writeFileTool.ts +8 -3
  152. package/src/ui/Spinner.tsx +25 -3
  153. package/src/ui/TextInput.tsx +2 -2
  154. package/src/ui/theme.ts +17 -0
  155. package/src/utils/clipboard.ts +10 -7
  156. package/src/utils/openExternal.ts +20 -10
  157. package/src/chat/RewindView.tsx +0 -386
  158. package/src/chat/toolResultDisplay.ts +0 -8
  159. package/src/identity/ens/ensRegistration.ts +0 -199
  160. package/src/identity/hub/effects/index.ts +0 -74
  161. package/src/identity/hub/effects/publicProfile/index.ts +0 -5
  162. package/src/identity/hub/effects/restore/restoreEffects.ts +0 -22
  163. package/src/identity/hub/effects/restoreAdmin.ts +0 -93
  164. package/src/identity/hub/effects/token-transfer/index.ts +0 -6
  165. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +0 -336
  166. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +0 -198
  167. package/src/identity/hub/reconciliation/walletSetup.ts +0 -220
  168. /package/src/chat/{chatInputState.ts → input/chatInputState.ts} +0 -0
  169. /package/src/chat/{chatPaste.ts → input/chatPaste.ts} +0 -0
  170. /package/src/chat/{textCursor.ts → input/textCursor.ts} +0 -0
  171. /package/src/identity/hub/{model/continuity.ts → continuity/state.ts} +0 -0
  172. /package/src/identity/hub/{model/custody.ts → custody/state.ts} +0 -0
  173. /package/src/identity/hub/{effects/restore → restore}/index.ts +0 -0
  174. /package/src/identity/hub/{model → shared/model}/format.ts +0 -0
  175. /package/src/identity/hub/{reconciliation → shared/reconciliation}/useAgentReconciliation.ts +0 -0
  176. /package/src/identity/hub/{txGuard.ts → shared/txGuard.ts} +0 -0
  177. /package/src/identity/hub/{model/transfer.ts → transfer/state.ts} +0 -0
@@ -0,0 +1,115 @@
1
+ export const OPENAI_OAUTH_ISSUER = 'https://auth.openai.com'
2
+ export const OPENAI_OAUTH_TOKEN_URL = `${OPENAI_OAUTH_ISSUER}/oauth/token`
3
+ export const OPENAI_OAUTH_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'
4
+ export const OPENAI_OAUTH_CALLBACK_PORT = 1455
5
+ export const OPENAI_OAUTH_SCOPE =
6
+ 'openid profile email offline_access api.connectors.read api.connectors.invoke'
7
+ export const OPENAI_OAUTH_ORIGINATOR = 'codex_cli_rs'
8
+ export const OPENAI_API_KEY_TOKEN_NAME = 'openai-api-key'
9
+ export const OPENAI_ID_TOKEN_SUBJECT_TYPE =
10
+ 'urn:ietf:params:oauth:token-type:id_token'
11
+ export const OPENAI_TOKEN_EXCHANGE_GRANT =
12
+ 'urn:ietf:params:oauth:grant-type:token-exchange'
13
+
14
+ export function asTrimmedString(value: unknown): string | undefined {
15
+ if (typeof value !== 'string') return undefined
16
+ const trimmed = value.trim()
17
+ return trimmed ? trimmed : undefined
18
+ }
19
+
20
+ export function decodeJwtPayload(
21
+ token: string,
22
+ ): Record<string, unknown> | undefined {
23
+ const parts = token.split('.')
24
+ if (parts.length < 2) return undefined
25
+ const segment = parts[1]
26
+ if (!segment) return undefined
27
+
28
+ try {
29
+ const normalized = segment.replace(/-/g, '+').replace(/_/g, '/')
30
+ const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4)
31
+ const json = Buffer.from(padded, 'base64').toString('utf8')
32
+ const parsed = JSON.parse(json) as unknown
33
+ return parsed && typeof parsed === 'object'
34
+ ? (parsed as Record<string, unknown>)
35
+ : undefined
36
+ } catch {
37
+ return undefined
38
+ }
39
+ }
40
+
41
+ export function parseChatgptAccountId(
42
+ token: string | undefined,
43
+ ): string | undefined {
44
+ if (!token) return undefined
45
+
46
+ const payload = decodeJwtPayload(token)
47
+ const nestedAuthRaw = payload?.['https://api.openai.com/auth']
48
+ const nestedAuth =
49
+ nestedAuthRaw && typeof nestedAuthRaw === 'object'
50
+ ? (nestedAuthRaw as Record<string, unknown>)
51
+ : undefined
52
+
53
+ return (
54
+ asTrimmedString(
55
+ nestedAuth?.chatgpt_account_id ??
56
+ payload?.['https://api.openai.com/auth.chatgpt_account_id'] ??
57
+ payload?.chatgpt_account_id,
58
+ ) ?? undefined
59
+ )
60
+ }
61
+
62
+ export function escapeHtml(value: string): string {
63
+ return value.replace(/[&<>"']/g, char => {
64
+ switch (char) {
65
+ case '&':
66
+ return '&amp;'
67
+ case '<':
68
+ return '&lt;'
69
+ case '>':
70
+ return '&gt;'
71
+ case '"':
72
+ return '&quot;'
73
+ case '\'':
74
+ return '&#39;'
75
+ default:
76
+ return char
77
+ }
78
+ })
79
+ }
80
+
81
+ export async function exchangeIdTokenForApiKey(idToken: string): Promise<string> {
82
+ const body = new URLSearchParams({
83
+ grant_type: OPENAI_TOKEN_EXCHANGE_GRANT,
84
+ client_id: OPENAI_OAUTH_CLIENT_ID,
85
+ requested_token: OPENAI_API_KEY_TOKEN_NAME,
86
+ subject_token: idToken,
87
+ subject_token_type: OPENAI_ID_TOKEN_SUBJECT_TYPE,
88
+ })
89
+
90
+ const response = await fetch(OPENAI_OAUTH_TOKEN_URL, {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
93
+ body,
94
+ signal: AbortSignal.timeout(15_000),
95
+ })
96
+
97
+ if (!response.ok) {
98
+ const bodyText = await response.text().catch(() => '')
99
+ throw new Error(
100
+ bodyText.trim()
101
+ ? `OpenAI API key exchange failed (${response.status}): ${bodyText.trim()}`
102
+ : `OpenAI API key exchange failed with status ${response.status}.`,
103
+ )
104
+ }
105
+
106
+ const payload = (await response.json()) as { access_token?: string }
107
+ const apiKey = asTrimmedString(payload.access_token)
108
+ if (!apiKey) {
109
+ throw new Error(
110
+ 'OpenAI API key exchange completed, but no key was returned.',
111
+ )
112
+ }
113
+
114
+ return apiKey
115
+ }
@@ -3,26 +3,26 @@ 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
16
  import { Box, Text } from 'ink'
17
17
  import { theme } from '../ui/theme.js'
18
18
  import { Spinner } from '../ui/Spinner.js'
19
- import { ContextLimitView, type ContextLimitAction } from './ContextLimitView.js'
19
+ import { ContextLimitView, type ContextLimitAction } from './views/ContextLimitView.js'
20
20
  import type { ContextUsage } from '../runtime/compaction.js'
21
21
  import {
22
22
  ContinuityEditReviewView,
23
23
  type ContinuityEditReviewAction,
24
24
  type ContinuityEditReviewState,
25
- } from './ContinuityEditReviewView.js'
25
+ } from './views/ContinuityEditReviewView.js'
26
26
 
27
27
  export type Overlay = 'none' | 'modelPicker' | 'resume' | 'rewind' | 'copyPicker' | 'permission' | 'permissions' | 'planApproval' | 'identity' | 'contextLimit' | 'continuityEditReview'
28
28
  export type CopyPickerState = { turnText: string; turnLabel: string } | null
@@ -66,7 +66,10 @@ type ChatBottomPaneProps = {
66
66
  handleResumeClearAll: () => void | Promise<void>
67
67
  identityOverlay: IdentityOverlayState | null
68
68
  handleIdentityResult: (result: IdentityHubResult) => void
69
- handleRestoreConversation: (turnId: string) => void
69
+ handleRestoreConversation: (turnId: string, promptText?: string) => void
70
+ pendingInputDraft: string | null
71
+ onInputDraftConsumed: () => void
72
+ handleSummarizeFromTurn: (turnId: string) => void | Promise<unknown>
70
73
  handleCopyDone: (result: CopyResult, label: string) => void
71
74
  handleCopyCancel: () => void
72
75
  resolvePermission: (decision: PermissionDecision) => void
@@ -110,6 +113,9 @@ export function ChatBottomPane({
110
113
  identityOverlay,
111
114
  handleIdentityResult,
112
115
  handleRestoreConversation,
116
+ handleSummarizeFromTurn,
117
+ pendingInputDraft,
118
+ onInputDraftConsumed,
113
119
  handleCopyDone,
114
120
  handleCopyCancel,
115
121
  resolvePermission,
@@ -155,6 +161,7 @@ export function ChatBottomPane({
155
161
  cwd={cwd}
156
162
  currentSessionId={currentSessionId}
157
163
  onRestoreConversation={handleRestoreConversation}
164
+ onSummarizeFromTurn={handleSummarizeFromTurn}
158
165
  onDone={(message, variant = 'info') => {
159
166
  setOverlay('none')
160
167
  pushNote(message, variant)
@@ -261,10 +268,12 @@ export function ChatBottomPane({
261
268
  slashSuggestions={slashSuggestions}
262
269
  footerRight={footerRight}
263
270
  cwd={cwd}
271
+ seedText={pendingInputDraft}
272
+ onSeedConsumed={onInputDraftConsumed}
264
273
  />
265
274
  <Box marginLeft={2} marginTop={0} flexDirection="column">
266
275
  <Text>
267
- <Text color={theme.dim}>workspace · </Text>
276
+ <Text color={theme.dim}>Workspace · </Text>
268
277
  <Text color={theme.textSubtle}>{cwd}</Text>
269
278
  </Text>
270
279
  </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 chat logs 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(() => {
@@ -1456,6 +1576,9 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1456
1576
  identityOverlay={identityOverlay}
1457
1577
  handleIdentityResult={handleIdentityResult}
1458
1578
  handleRestoreConversation={handleRestoreConversation}
1579
+ pendingInputDraft={pendingInputDraft}
1580
+ onInputDraftConsumed={() => setPendingInputDraft(null)}
1581
+ handleSummarizeFromTurn={runCompactionFromTurn}
1459
1582
  handleCopyDone={handleCopyDone}
1460
1583
  handleCopyCancel={handleCopyCancel}
1461
1584
  resolvePermission={resolvePermission}
@@ -1568,12 +1691,14 @@ export function privateContinuityEditReviewFromToolResult(
1568
1691
  if (name !== 'propose_private_continuity_edit' || !result.ok) return null
1569
1692
  const file = normalizePrivateContinuityFile(input.file)
1570
1693
  if (!file) return null
1571
- const filePath = extractReviewFilePath(result.content)
1694
+ const parsed = splitFileChangeResult(result.content)
1695
+ const filePath = extractReviewFilePath(parsed.content)
1572
1696
  if (!filePath) return null
1573
1697
  return {
1574
1698
  file,
1575
1699
  filePath,
1576
1700
  summary: result.summary,
1701
+ ...(parsed.diff ? { diff: parsed.diff } : {}),
1577
1702
  }
1578
1703
  }
1579
1704
 
@@ -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 = {