ethagent 2.4.0 → 3.0.1

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 (103) hide show
  1. package/README.md +7 -4
  2. package/package.json +2 -1
  3. package/src/app/FirstRun.tsx +155 -15
  4. package/src/app/FirstRunTimeline.tsx +4 -0
  5. package/src/app/input/AppInputProvider.tsx +19 -0
  6. package/src/app/input/appInputParser.ts +19 -4
  7. package/src/chat/ChatBottomPane.tsx +3 -1
  8. package/src/chat/ChatScreen.tsx +7 -1
  9. package/src/chat/ConversationStack.tsx +25 -19
  10. package/src/chat/MessageList.tsx +194 -53
  11. package/src/chat/chatSessionState.ts +1 -1
  12. package/src/chat/chatTurnOrchestrator.ts +59 -0
  13. package/src/chat/input/ChatInput.tsx +3 -0
  14. package/src/chat/input/textCursor.ts +13 -3
  15. package/src/chat/transcript/TranscriptView.tsx +7 -5
  16. package/src/chat/transcript/transcriptViewport.ts +88 -17
  17. package/src/chat/views/PermissionPrompt.tsx +26 -26
  18. package/src/chat/views/PermissionsView.tsx +18 -12
  19. package/src/chat/views/RewindView.tsx +3 -1
  20. package/src/cli/ResetConfirmView.tsx +24 -9
  21. package/src/identity/continuity/editor.ts +27 -2
  22. package/src/identity/continuity/envelope.ts +134 -9
  23. package/src/identity/continuity/publicSkills.ts +54 -1
  24. package/src/identity/continuity/skills/frontmatter.ts +183 -0
  25. package/src/identity/continuity/skills/loadSkills.ts +609 -0
  26. package/src/identity/continuity/skills/publicSkillsSync.ts +32 -0
  27. package/src/identity/continuity/skills/scaffold.ts +52 -0
  28. package/src/identity/continuity/skills/types.ts +30 -0
  29. package/src/identity/continuity/storage/defaults.ts +28 -47
  30. package/src/identity/continuity/storage/files.ts +1 -0
  31. package/src/identity/continuity/storage/paths.ts +1 -0
  32. package/src/identity/continuity/storage/scaffold.ts +25 -23
  33. package/src/identity/continuity/storage/status.ts +34 -5
  34. package/src/identity/continuity/storage/types.ts +3 -2
  35. package/src/identity/continuity/storage.ts +3 -0
  36. package/src/identity/hub/OperationalRoutes.tsx +79 -5
  37. package/src/identity/hub/Routes.tsx +5 -3
  38. package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +7 -73
  39. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +6 -6
  40. package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -2
  41. package/src/identity/hub/continuity/effects.ts +36 -5
  42. package/src/identity/hub/continuity/skills/DeleteSkillConfirmScreen.tsx +112 -0
  43. package/src/identity/hub/continuity/skills/NewSkillScreen.tsx +57 -0
  44. package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +52 -0
  45. package/src/identity/hub/continuity/skills/SkillActionsScreen.tsx +151 -0
  46. package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +181 -0
  47. package/src/identity/hub/continuity/snapshot.ts +3 -0
  48. package/src/identity/hub/continuity/state.ts +9 -8
  49. package/src/identity/hub/continuity/vault.ts +42 -10
  50. package/src/identity/hub/create/CreateFlow.tsx +1 -1
  51. package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
  52. package/src/identity/hub/custody/routes.tsx +1 -1
  53. package/src/identity/hub/ens/EnsEditAdvancedScreens.tsx +0 -1
  54. package/src/identity/hub/ens/EnsEditMaintenanceScreens.tsx +0 -1
  55. package/src/identity/hub/identityHubReducer.ts +15 -0
  56. package/src/identity/hub/profile/EditProfileFlow.tsx +5 -5
  57. package/src/identity/hub/profile/effects.ts +16 -3
  58. package/src/identity/hub/restore/RestoreFlow.tsx +43 -6
  59. package/src/identity/hub/restore/apply.ts +12 -1
  60. package/src/identity/hub/restore/recovery.ts +14 -4
  61. package/src/identity/hub/restore/resolve.ts +1 -1
  62. package/src/identity/hub/restore/useRestoreEffects.ts +4 -6
  63. package/src/identity/hub/shared/components/DetailsScreen.tsx +4 -1
  64. package/src/identity/hub/shared/components/IdentitySummary.tsx +118 -54
  65. package/src/identity/hub/shared/components/MenuScreen.tsx +21 -18
  66. package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +4 -4
  67. package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +8 -12
  68. package/src/identity/hub/shared/effects/sync.ts +16 -3
  69. package/src/identity/hub/shared/model/copy.ts +2 -4
  70. package/src/identity/hub/transfer/effects.ts +15 -2
  71. package/src/identity/hub/useIdentityHubContinuity.ts +145 -23
  72. package/src/identity/hub/useIdentityHubController.ts +5 -1
  73. package/src/identity/hub/useIdentityHubSideEffects.ts +2 -4
  74. package/src/identity/wallet/page/copy.ts +43 -43
  75. package/src/mcp/manager.ts +1 -1
  76. package/src/models/ModelPicker.tsx +89 -84
  77. package/src/models/llamacpp.ts +160 -11
  78. package/src/models/llamacppPreflight.ts +1 -16
  79. package/src/models/modelPickerOptions.ts +45 -37
  80. package/src/providers/contracts.ts +1 -0
  81. package/src/providers/openai-chat.ts +50 -9
  82. package/src/providers/openai-responses.ts +19 -4
  83. package/src/runtime/toolExecution.ts +4 -3
  84. package/src/runtime/turn.ts +61 -30
  85. package/src/tools/changeDirectoryTool.ts +1 -1
  86. package/src/tools/contracts.ts +10 -0
  87. package/src/tools/deleteFileTool.ts +1 -1
  88. package/src/tools/editTool.ts +1 -1
  89. package/src/tools/listDirectoryTool.ts +1 -1
  90. package/src/tools/listSkillFilesTool.ts +77 -0
  91. package/src/tools/listSkillsTool.ts +68 -0
  92. package/src/tools/mcpResourceTools.ts +2 -2
  93. package/src/tools/privateContinuityReadTool.ts +1 -1
  94. package/src/tools/readSkillTool.ts +107 -0
  95. package/src/tools/readTool.ts +1 -1
  96. package/src/tools/registry.ts +6 -0
  97. package/src/tools/writeFileTool.ts +22 -2
  98. package/src/ui/Spinner.tsx +1 -1
  99. package/src/identity/continuity/localBackup.ts +0 -249
  100. package/src/identity/continuity/zipWriter.ts +0 -95
  101. package/src/identity/hub/continuity/index.ts +0 -7
  102. package/src/identity/hub/ens/index.ts +0 -11
  103. package/src/identity/hub/restore/index.ts +0 -22
@@ -1,8 +1,9 @@
1
1
  import React from 'react'
2
- import { Text } from 'ink'
2
+ import { Box, Text } from 'ink'
3
3
  import { Surface } from '../../../ui/Surface.js'
4
4
  import { Select } from '../../../ui/Select.js'
5
5
  import { TextInput } from '../../../ui/TextInput.js'
6
+ import { Spinner } from '../../../ui/Spinner.js'
6
7
  import { theme } from '../../../ui/theme.js'
7
8
  import { normalizeErc8004RegistryConfig } from '../../registry/erc8004.js'
8
9
  import {
@@ -18,7 +19,7 @@ import { WalletApprovalScreen } from '../shared/components/WalletApprovalScreen.
18
19
  import { BusyScreen } from '../shared/components/BusyScreen.js'
19
20
  import type { BrowserWalletReady } from '../../wallet/browserWallet.js'
20
21
  import type { EthagentConfig } from '../../../storage/config.js'
21
- import { restoreSignatureRequestForStep } from './index.js'
22
+ import { restoreSignatureRequestForStep } from './auth.js'
22
23
  import type { RestoreProgress } from '../shared/effects/types.js'
23
24
 
24
25
  type RestoreStep = Exclude<Extract<Step, { kind: `restore-${string}` }>, { kind: 'restore-wallet' | 'restore-network' }>
@@ -122,11 +123,24 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
122
123
  }
123
124
 
124
125
  if (step.kind === 'restore-ens-input') {
126
+ if (step.busy) {
127
+ return (
128
+ <Surface
129
+ title={isSwitch ? 'Load Agent' : 'Restore Agent'}
130
+ subtitle="Looking up the agent onchain."
131
+ footer={footerHint('esc cancels')}
132
+ >
133
+ <Box marginTop={1}>
134
+ <Spinner label="looking up ENS name onchain..." />
135
+ </Box>
136
+ </Surface>
137
+ )
138
+ }
125
139
  return (
126
140
  <Surface
127
141
  title={isSwitch ? 'Load Agent' : 'Restore Agent'}
128
142
  subtitle="Enter the agent's ENS name to decrypt with an authorized operator wallet."
129
- footer={footerHint(step.busy ? 'Looking up...' : 'enter continue · esc back')}
143
+ footer={footerHint('enter continue · esc back')}
130
144
  >
131
145
  <Text color={theme.dim}>The full agent subdomain, e.g. agent.example.eth.</Text>
132
146
  <TextInput
@@ -134,17 +148,35 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
134
148
  onSubmit={value => onEnsSubmit(value.trim())}
135
149
  onCancel={onBack}
136
150
  />
137
- {step.error ? <Text color={theme.accentError}>{step.error}</Text> : null}
151
+ {step.error ? (
152
+ <Box marginTop={1} flexDirection="column">
153
+ <Text color={theme.accentError}>Could not resolve ENS name</Text>
154
+ <Text color={theme.textSubtle}>{step.error}</Text>
155
+ </Box>
156
+ ) : null}
138
157
  </Surface>
139
158
  )
140
159
  }
141
160
 
142
161
  if (step.kind === 'restore-token-id-input') {
162
+ if (step.busy) {
163
+ return (
164
+ <Surface
165
+ title={isSwitch ? 'Load Agent' : 'Restore Agent'}
166
+ subtitle="Looking up the agent onchain."
167
+ footer={footerHint('esc cancels')}
168
+ >
169
+ <Box marginTop={1}>
170
+ <Spinner label="looking up token onchain..." />
171
+ </Box>
172
+ </Surface>
173
+ )
174
+ }
143
175
  return (
144
176
  <Surface
145
177
  title={isSwitch ? 'Load Agent' : 'Restore Agent'}
146
178
  subtitle={`Enter the ERC-8004 token ID on ${networkLabelForRegistry(step.registry)}.`}
147
- footer={footerHint(step.busy ? 'Looking up...' : 'enter continue · esc back')}
179
+ footer={footerHint('enter continue · esc back')}
148
180
  >
149
181
  <Text color={theme.dim}>The integer token ID assigned at mint.</Text>
150
182
  <TextInput
@@ -152,7 +184,12 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
152
184
  onSubmit={value => onTokenIdSubmit(value.trim())}
153
185
  onCancel={onBack}
154
186
  />
155
- {step.error ? <Text color={theme.accentError}>{step.error}</Text> : null}
187
+ {step.error ? (
188
+ <Box marginTop={1} flexDirection="column">
189
+ <Text color={theme.accentError}>Could not resolve token</Text>
190
+ <Text color={theme.textSubtle}>{step.error}</Text>
191
+ </Box>
192
+ ) : null}
156
193
  </Surface>
157
194
  )
158
195
  }
@@ -5,7 +5,12 @@ import {
5
5
  restoreContinuitySnapshotEnvelope,
6
6
  transferSnapshotMetadataFromEnvelope,
7
7
  } from '../../continuity/envelope.js'
8
- import { ensureIdentityMarkdownScaffold, writeContinuityFiles } from '../../continuity/storage.js'
8
+ import {
9
+ ensureIdentityMarkdownScaffold,
10
+ restoreSkillsTree,
11
+ writeContinuityFiles,
12
+ } from '../../continuity/storage.js'
13
+ import { syncPublicSkillsManifest } from '../../continuity/skills/publicSkillsSync.js'
9
14
  import { recordPublishedContinuitySnapshot } from '../../continuity/snapshots.js'
10
15
  import { requestBrowserWalletSignature } from '../../wallet/browserWallet.js'
11
16
  import { setVaultAddressField } from '../../identityCompat.js'
@@ -31,6 +36,7 @@ export async function runRestoreAuthorize(
31
36
  callbacks.onRestoreProgress?.({ phase: 'decrypting', label: 'signature received · decrypting encrypted snapshot...' })
32
37
  let restored: ReturnType<typeof restoreAgentStateBackupEnvelope> | ReturnType<typeof restoreContinuitySnapshotEnvelope>
33
38
  let continuityFiles: ReturnType<typeof restoreContinuitySnapshotEnvelope>['files'] | undefined
39
+ let continuitySkills: ReturnType<typeof restoreContinuitySnapshotEnvelope>['skills']
34
40
  if (isContinuitySnapshotEnvelope(step.envelope)) {
35
41
  const payload = restoreContinuitySnapshotEnvelope({
36
42
  envelope: step.envelope,
@@ -39,6 +45,7 @@ export async function runRestoreAuthorize(
39
45
  })
40
46
  restored = payload
41
47
  continuityFiles = payload.files
48
+ continuitySkills = payload.skills
42
49
  } else {
43
50
  restored = restoreAgentStateBackupEnvelope({
44
51
  envelope: step.envelope,
@@ -104,9 +111,13 @@ export async function runRestoreAuthorize(
104
111
  if (continuityFiles) {
105
112
  await writeContinuityFiles(nextIdentity, continuityFiles)
106
113
  }
114
+ if (continuitySkills) {
115
+ await restoreSkillsTree(nextIdentity, continuitySkills)
116
+ }
107
117
  callbacks.onRestoreProgress?.({ phase: 'finishing', label: 'finalizing restored identity...' })
108
118
  await restorePublishedPublicSkills(nextIdentity, step.apiUrl, step.candidate.publicDiscovery?.skillsCid)
109
119
  await ensureIdentityMarkdownScaffold(nextIdentity)
120
+ await syncPublicSkillsManifest(nextIdentity).catch(() => null)
110
121
  await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'restored from agent backup' }).catch(() => null)
111
122
  await callbacks.onIdentityComplete(nextIdentity, `ERC-8004 agent restored · #${step.candidate.agentId.toString()}`, 'restore')
112
123
  }
@@ -6,7 +6,13 @@ import {
6
6
  restoreContinuitySnapshotEnvelope,
7
7
  transferSnapshotMetadataFromEnvelope,
8
8
  } from '../../continuity/envelope.js'
9
- import { ensureIdentityMarkdownScaffold, localContinuitySnapshotContentHashes, writeContinuityFiles } from '../../continuity/storage.js'
9
+ import {
10
+ ensureIdentityMarkdownScaffold,
11
+ localContinuitySnapshotContentHashes,
12
+ restoreSkillsTree,
13
+ writeContinuityFiles,
14
+ } from '../../continuity/storage.js'
15
+ import { syncPublicSkillsManifest } from '../../continuity/skills/publicSkillsSync.js'
10
16
  import { recordPublishedContinuitySnapshot, updatePublishedContinuitySnapshotContentHashes } from '../../continuity/snapshots.js'
11
17
  import { catFromIpfs, DEFAULT_IPFS_API_URL } from '../../storage/ipfs.js'
12
18
  import {
@@ -72,7 +78,7 @@ export async function runRecoveryRefetch(
72
78
  walletSignature: wallet.signature,
73
79
  currentOwnerAddress: getAddress(wallet.account),
74
80
  })
75
- callbacks.onRestoreProgress?.({ phase: 'writing', label: 'restoring SOUL.md, MEMORY.md, and skills.json...' })
81
+ callbacks.onRestoreProgress?.({ phase: 'writing', label: 'restoring SOUL.md, MEMORY.md, and skills...' })
76
82
  const transferSnapshot = transferSnapshotMetadataFromEnvelope(envelope)
77
83
  const refreshedBackup: BackupMetadata = {
78
84
  cid: candidate.backup.cid,
@@ -123,13 +129,17 @@ export async function runRecoveryRefetch(
123
129
  } : {}),
124
130
  }
125
131
  await writeContinuityFiles(nextIdentity, payload.files)
132
+ if (payload.skills) {
133
+ await restoreSkillsTree(nextIdentity, payload.skills)
134
+ }
126
135
  callbacks.onRestoreProgress?.({ phase: 'finishing', label: 'finalizing refreshed identity...' })
127
136
  const publicSkillsRestored = await restorePublishedPublicSkills(nextIdentity, apiUrl, candidate.publicDiscovery?.skillsCid)
128
137
  await ensureIdentityMarkdownScaffold(nextIdentity)
129
- await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'Refetched Latest Snapshot From Chain' }).catch(() => null)
138
+ await syncPublicSkillsManifest(nextIdentity).catch(() => null)
139
+ await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'Refetched Latest Snapshot From Onchain' }).catch(() => null)
130
140
  if (publicSkillsRestored) {
131
141
  const contentHashes = await localContinuitySnapshotContentHashes(nextIdentity)
132
142
  await updatePublishedContinuitySnapshotContentHashes(nextIdentity, candidate.backup.cid, contentHashes).catch(() => null)
133
143
  }
134
- await callbacks.onIdentityComplete(nextIdentity, 'Latest Published Snapshot Restored From Chain', 'update')
144
+ await callbacks.onIdentityComplete(nextIdentity, 'Latest Published Snapshot Restored From Onchain', 'update')
135
145
  }
@@ -29,7 +29,7 @@ export async function resolveAgentEnsToCandidate(
29
29
  return { ok: false, message: `Could not reach Ethereum mainnet to resolve ${trimmed}: ${err instanceof Error ? err.message : String(err)}` }
30
30
  }
31
31
  const tokenValue = records[AGENT_RECORD_KEYS.token]
32
- if (!tokenValue) return { ok: false, message: `${trimmed} has no org.ethagent.token record. Use the full agent subdomain (e.g. agent.${trimmed}).` }
32
+ if (!tokenValue) return { ok: false, message: `${trimmed} has no org.ethagent.token record.` }
33
33
  const tokenRef = parseAgentTokenReference(tokenValue)
34
34
  if (!tokenRef) return { ok: false, message: `${trimmed}'s org.ethagent.token record is not a valid eip155 reference.` }
35
35
  if (tokenRef.chainId !== registry.chainId) {
@@ -1,11 +1,9 @@
1
1
  import { useEffect } from 'react'
2
2
  import type { EthagentConfig } from '../../../storage/config.js'
3
- import {
4
- runRestoreAuthorize,
5
- runRestoreConnectWallet,
6
- runRestoreDiscover,
7
- runRestoreFetch,
8
- } from './index.js'
3
+ import { runRestoreAuthorize } from './apply.js'
4
+ import { runRestoreConnectWallet } from './auth.js'
5
+ import { runRestoreDiscover } from './discover.js'
6
+ import { runRestoreFetch } from './fetch.js'
9
7
  import type { EffectCallbacks } from '../shared/effects/types.js'
10
8
  import type { Step } from '../identityHubReducer.js'
11
9
 
@@ -6,12 +6,14 @@ import { Select, type SelectOption } from '../../../../ui/Select.js'
6
6
  import type { EthagentConfig, EthagentIdentity } from '../../../../storage/config.js'
7
7
  import { copyableIdentityFields, identityValuesCopyHint } from '../model/copy.js'
8
8
  import { IdentitySummary } from './IdentitySummary.js'
9
+ import type { ContinuityWorkingTreeStatus } from '../../../continuity/storage.js'
9
10
 
10
11
  type CopyAction = `copy:${string}` | 'back'
11
12
 
12
13
  type DetailsScreenProps = {
13
14
  identity?: EthagentIdentity
14
15
  config?: EthagentConfig
16
+ workingStatus?: ContinuityWorkingTreeStatus | null
15
17
  copyNotice?: string | null
16
18
  unlinked?: boolean
17
19
  onchainOwner?: string
@@ -23,6 +25,7 @@ type DetailsScreenProps = {
23
25
  export const DetailsScreen: React.FC<DetailsScreenProps> = ({
24
26
  identity,
25
27
  config,
28
+ workingStatus,
26
29
  copyNotice,
27
30
  unlinked,
28
31
  onchainOwner,
@@ -45,7 +48,7 @@ export const DetailsScreen: React.FC<DetailsScreenProps> = ({
45
48
 
46
49
  return (
47
50
  <Surface title="Token Values" subtitle={unlinked ? 'Token no longer linked to this wallet, values retained for reference.' : `${identityValuesCopyHint(identity)}.`} footer={footer}>
48
- <IdentitySummary identity={identity} config={config} onchainOwner={onchainOwner} />
51
+ <IdentitySummary identity={identity} config={config} workingStatus={workingStatus} onchainOwner={onchainOwner} />
49
52
  {copyNotice ? <Text color={theme.accentPeriwinkle} bold>{copyNotice}</Text> : null}
50
53
  <Box marginTop={1}>
51
54
  <Select<CopyAction>
@@ -28,9 +28,10 @@ interface IdentitySummaryProps {
28
28
  hideHeader?: boolean
29
29
  tokenLinked?: boolean
30
30
  onchainOwner?: string
31
+ compact?: boolean
31
32
  }
32
33
 
33
- export const IdentitySummary: React.FC<IdentitySummaryProps> = ({ identity, config, workingStatus, hideLocalChanges = false, hideHeader = false, tokenLinked = true, onchainOwner }) => {
34
+ export const IdentitySummary: React.FC<IdentitySummaryProps> = ({ identity, config, workingStatus, hideLocalChanges = false, hideHeader = false, tokenLinked = true, onchainOwner, compact = false }) => {
34
35
  if (!identity) {
35
36
  return (
36
37
  <Text color={theme.dim}>No agent yet. Create or load one.</Text>
@@ -59,6 +60,25 @@ export const IdentitySummary: React.FC<IdentitySummaryProps> = ({ identity, conf
59
60
  ? `${tokenValue} · ${displayValue(networkValue)}`
60
61
  : displayValue(tokenValue)
61
62
 
63
+ if (compact) {
64
+ const name = stateName || 'Active Agent'
65
+ const tokenSegment = identity.agentId ? `#${identity.agentId}` : null
66
+ const networkSegment = identity.agentId ? displayValue(networkValue) : null
67
+ const ensSegment = ensStatus.kind === 'linked'
68
+ ? ensStatus.name
69
+ : ensStatus.kind === 'issue'
70
+ ? ensStatus.name
71
+ : null
72
+ return (
73
+ <Text>
74
+ <Text color={theme.accentPeriwinkle} bold>{name}</Text>
75
+ {tokenSegment ? <><Text color={theme.dim}> · </Text><Text color={theme.text}>{tokenSegment}</Text></> : null}
76
+ {networkSegment ? <><Text color={theme.dim}> · </Text><Text color={theme.text}>{networkSegment}</Text></> : null}
77
+ {ensSegment ? <><Text color={theme.dim}> · </Text><Text color={ensStatus.kind === 'issue' ? theme.accentError : theme.accentPeriwinkle}>{ensSegment}</Text></> : null}
78
+ </Text>
79
+ )
80
+ }
81
+
62
82
  return (
63
83
  <Box flexDirection="column">
64
84
  {hideHeader ? null : (
@@ -67,59 +87,67 @@ export const IdentitySummary: React.FC<IdentitySummaryProps> = ({ identity, conf
67
87
  <Text color={identity.agentId ? theme.text : theme.dim} bold={Boolean(identity.agentId)}>{tokenLine}</Text>
68
88
  </>
69
89
  )}
70
- <Text>
71
- <Text color={theme.dim}>{'ENS'.padEnd(12)}</Text>
72
- {ensStatus.kind === 'linked'
73
- ? <Text color={theme.accentPeriwinkle}>{ensStatus.name}</Text>
74
- : ensStatus.kind === 'issue'
75
- ? <Text color={theme.accentError}>{ensStatus.name} ({ensValidationReasonText(ensStatus.reason)})</Text>
76
- : <Text color={theme.dim}>Not Linked</Text>}
77
- </Text>
78
- {tokenLinked ? (
79
- <Text>
80
- <Text color={theme.dim}>{'Custody'.padEnd(12)}</Text>
81
- <Text color={custodyMode ? theme.text : theme.dim}>{displayCustodyMode(custodyMode)}</Text>
82
- </Text>
83
- ) : null}
84
- {ownerAddress ? (
85
- <Text>
86
- <Text color={theme.dim}>{'Owner'.padEnd(12)}</Text>
87
- <Text color={theme.text}>{shortAddress(ownerAddress)}</Text>
88
- </Text>
89
- ) : null}
90
+ <SummaryRow
91
+ left={{
92
+ label: 'ENS',
93
+ value: ensStatus.kind === 'linked'
94
+ ? <Text color={theme.accentPeriwinkle}>{ensStatus.name}</Text>
95
+ : ensStatus.kind === 'issue'
96
+ ? <Text color={theme.accentError}>{ensStatus.name} ({ensValidationReasonText(ensStatus.reason)})</Text>
97
+ : <Text color={theme.dim}>Not Linked</Text>,
98
+ }}
99
+ right={tokenLinked
100
+ ? {
101
+ label: 'Custody',
102
+ value: <Text color={custodyMode ? theme.text : theme.dim}>{displayCustodyMode(custodyMode)}</Text>,
103
+ }
104
+ : undefined}
105
+ />
90
106
  {(() => {
91
- if (custodyMode !== 'advanced') return null
92
- const vaultAddress = readIdentityStateString(identity.state, 'operatorVaultAddress')
93
- if (!vaultAddress) return null
107
+ const vaultAddress = custodyMode === 'advanced'
108
+ ? readIdentityStateString(identity.state, 'operatorVaultAddress')
109
+ : undefined
110
+ const pairedOperatorsValue = custodyMode === 'advanced' && tokenLinked
111
+ ? approvedOperatorCount > 1
112
+ ? <Text color={theme.text}>{`${approvedOperatorCount} authorized`}</Text>
113
+ : activeOperator
114
+ ? <Text color={theme.text}>{shortAddress(activeOperator)}</Text>
115
+ : <Text color={theme.dim}>None Authorized</Text>
116
+ : null
117
+ const lastSavedCell = {
118
+ label: 'Last Saved',
119
+ value: <Text color={lastBackup === 'never' ? theme.dim : theme.text}>{displayValue(lastBackup)}</Text>,
120
+ }
121
+ const pendingCell = {
122
+ label: 'Pending',
123
+ value: <Text color={theme.dim}>local ahead of onchain, owner rotates pointer</Text>,
124
+ }
94
125
  return (
95
- <Text>
96
- <Text color={theme.dim}>{'Vault'.padEnd(12)}</Text>
97
- <Text color={theme.text}>{shortAddress(vaultAddress)}</Text>
98
- </Text>
126
+ <>
127
+ {ownerAddress ? (
128
+ <SummaryRow
129
+ left={{
130
+ label: 'Owner',
131
+ value: <Text color={theme.text}>{shortAddress(ownerAddress)}</Text>,
132
+ }}
133
+ {...(pairedOperatorsValue
134
+ ? { right: { label: 'Operators', value: pairedOperatorsValue } }
135
+ : {})}
136
+ />
137
+ ) : null}
138
+ {vaultAddress ? (
139
+ <SummaryRow
140
+ left={{ label: 'Vault', value: <Text color={theme.text}>{shortAddress(vaultAddress)}</Text> }}
141
+ right={hasPendingPublish(identity) ? pendingCell : lastSavedCell}
142
+ />
143
+ ) : (
144
+ hasPendingPublish(identity)
145
+ ? <SummaryRow left={lastSavedCell} right={pendingCell} />
146
+ : <SummaryRow left={lastSavedCell} />
147
+ )}
148
+ </>
99
149
  )
100
150
  })()}
101
- {tokenLinked && custodyMode === 'advanced' ? (
102
- <Text>
103
- <Text color={theme.dim}>{'Operators'.padEnd(12)}</Text>
104
- {approvedOperatorCount > 1 ? (
105
- <Text color={theme.text}>{`${approvedOperatorCount} authorized${activeOperator ? ` (active ${shortAddress(activeOperator)})` : ''}`}</Text>
106
- ) : activeOperator ? (
107
- <Text color={theme.text}>{shortAddress(activeOperator)}</Text>
108
- ) : (
109
- <Text color={theme.dim}>None Authorized</Text>
110
- )}
111
- </Text>
112
- ) : null}
113
- <Text>
114
- <Text color={theme.dim}>{'Last Saved'.padEnd(12)}</Text>
115
- <Text color={lastBackup === 'never' ? theme.dim : theme.text}>{displayValue(lastBackup)}</Text>
116
- </Text>
117
- {hasPendingPublish(identity) ? (
118
- <Text>
119
- <Text color={theme.dim}>{'Pending'.padEnd(12)}</Text>
120
- <Text color={theme.dim}>local snapshot ahead of chain, owner wallet rotates the pointer</Text>
121
- </Text>
122
- ) : null}
123
151
  {transferSnapshot ? (
124
152
  <Box marginTop={1}>
125
153
  <TransferSnapshotStatus status={transferSnapshot} />
@@ -134,6 +162,39 @@ export const IdentitySummary: React.FC<IdentitySummaryProps> = ({ identity, conf
134
162
  )
135
163
  }
136
164
 
165
+ type SummaryCell = { label: string; value: React.ReactNode }
166
+
167
+ const LEFT_LABEL_WIDTH = 12
168
+ const LEFT_VALUE_WIDTH = 30
169
+ const RIGHT_CELL_WIDTH = 36
170
+
171
+ const SummaryRow: React.FC<{ left: SummaryCell; right?: SummaryCell }> = ({ left, right }) => {
172
+ if (!right) {
173
+ return (
174
+ <Text>
175
+ <Text color={theme.dim}>{left.label.padEnd(LEFT_LABEL_WIDTH)}</Text>
176
+ {left.value}
177
+ </Text>
178
+ )
179
+ }
180
+ return (
181
+ <Box flexDirection="row">
182
+ <Box width={LEFT_LABEL_WIDTH + LEFT_VALUE_WIDTH}>
183
+ <Text>
184
+ <Text color={theme.dim}>{left.label.padEnd(LEFT_LABEL_WIDTH)}</Text>
185
+ {left.value}
186
+ </Text>
187
+ </Box>
188
+ <Box width={RIGHT_CELL_WIDTH}>
189
+ <Text>
190
+ <Text color={theme.dim}>{right.label.padEnd(LEFT_LABEL_WIDTH)}</Text>
191
+ {right.value}
192
+ </Text>
193
+ </Box>
194
+ </Box>
195
+ )
196
+ }
197
+
137
198
  const TransferSnapshotStatus: React.FC<{ status: NonNullable<TransferSnapshotView> }> = ({ status }) => {
138
199
  const receiverLabel = status.receiverHandle && status.receiverHandle !== status.receiver
139
200
  ? `${shortAddress(status.receiver)} (${status.receiverHandle})`
@@ -163,10 +224,13 @@ const TransferSnapshotStatus: React.FC<{ status: NonNullable<TransferSnapshotVie
163
224
  const LocalChangeStatusLine: React.FC<{ status: LocalChangeStatusView }> = ({ status }) => {
164
225
  if (status.hasLocalChanges) {
165
226
  return (
166
- <Text color={theme.accentError} bold>
167
- Local changes detected
168
- {status.files.length > 0 ? `: ${status.files.join(', ')}` : ''}
169
- </Text>
227
+ <Box flexDirection="column">
228
+ <Text color={theme.accentError} bold>
229
+ Local changes detected
230
+ {status.files.length > 0 ? `: ${status.files.join(', ')}` : ''}
231
+ </Text>
232
+ <Text color={theme.dim}>Save Snapshot Now to publish.</Text>
233
+ </Box>
170
234
  )
171
235
  }
172
236
 
@@ -28,6 +28,7 @@ type MenuScreenProps = {
28
28
  onEnsName: () => void
29
29
  onWalletSetup: () => void
30
30
  onContinuity: () => void
31
+ onSkillsTree: () => void
31
32
  onIdentityValues: () => void
32
33
  onPrepareTransfer: () => void
33
34
  onStorage: () => void
@@ -40,6 +41,7 @@ type Action =
40
41
  | 'ens-name'
41
42
  | 'wallet-setup'
42
43
  | 'continuity'
44
+ | 'skills-tree'
43
45
  | 'backup'
44
46
  | 'refetch'
45
47
  | 'identity-values'
@@ -66,6 +68,7 @@ export const MenuScreen: React.FC<MenuScreenProps> = ({
66
68
  onEnsName,
67
69
  onWalletSetup,
68
70
  onContinuity,
71
+ onSkillsTree,
69
72
  onIdentityValues,
70
73
  onPrepareTransfer,
71
74
  onStorage,
@@ -91,47 +94,46 @@ export const MenuScreen: React.FC<MenuScreenProps> = ({
91
94
  : null)
92
95
 
93
96
  const walletSetupBaseHint = custodyMode === 'advanced'
94
- ? 'Advanced. Owner wallet, Vault, authorized operator wallets'
95
- : 'Simple. Switch to Advanced to delegate URI rotation through a dedicated Vault'
97
+ ? 'Owner wallet, vault, operators'
98
+ : 'Simple mode, switch for vault delegation'
96
99
 
97
100
  const walletSetupLabel = flags?.custodyAsterisk ? 'Custody Mode *' : 'Custody Mode'
98
101
  const walletSetupHint = flags?.custodyModeReason ?? flags?.custodyHint ?? walletSetupBaseHint
99
102
 
100
- const saveSnapshotLabel = flags?.saveSnapshotAsterisk ? 'Save Snapshot Now *' : 'Save Snapshot Now'
101
- const saveSnapshotHint = flags?.saveSnapshotHint ?? 'Encrypt and publish latest snapshot'
103
+ const saveSnapshotLabel = flags?.saveSnapshotAsterisk ? 'Save Snapshot *' : 'Save Snapshot'
104
+ const saveSnapshotHint = flags?.saveSnapshotHint ?? 'Publish encrypted snapshot'
102
105
 
103
- const ensNameHint = flags?.ensNameReason ?? 'Public name or subdomain for this agent'
106
+ const ensNameHint = flags?.ensNameReason ?? 'Public name or subdomain'
104
107
 
105
- const prepareTransferHint = flags?.prepareTransferReason ?? 'Create transfer snapshot and handoff slots'
108
+ const prepareTransferHint = flags?.prepareTransferReason ?? 'Hand off this agent'
106
109
 
107
110
  const tokenValuesHint = flags?.tokenValuesUnlinkedNote ?? identityValuesCopyHint(identity)
108
111
 
109
112
  const options: Array<SelectOption<Action>> = identity
110
113
  ? [
111
114
  { value: 'public-profile', role: 'section', label: 'Public Identity' },
112
- { value: 'public-profile', label: 'Public Profile', hint: 'Name, description, icon, and Agent Card' },
115
+ { value: 'public-profile', label: 'Public Profile', hint: 'Agent card and profile fields' },
113
116
  { value: 'ens-name', label: 'ENS Name', hint: ensNameHint, disabled: flags?.ensNameDisabled ?? false },
114
117
  { value: 'continuity', role: 'section', label: 'Continuity' },
115
- { value: 'continuity', label: 'Soul, Memory, Skills', hint: 'SOUL.md, MEMORY.md, skills.json on this device' },
118
+ { value: 'continuity', label: 'Soul & Memory', hint: 'Edit SOUL.md and MEMORY.md' },
119
+ { value: 'skills-tree', label: 'Skills', hint: 'Browse and edit SKILL.md files' },
116
120
  { value: 'backup', label: saveSnapshotLabel, hint: saveSnapshotHint, disabled: !canRebackup || (flags?.saveSnapshotDisabled ?? false) },
117
- { value: 'refetch', label: 'Refetch Latest', hint: 'Restore local files from latest saved snapshot', disabled: !canRefetch || (flags?.refetchLatestDisabled ?? false) },
121
+ { value: 'refetch', label: 'Refetch Snapshot', hint: 'Restore latest snapshot', disabled: !canRefetch || (flags?.refetchLatestDisabled ?? false) },
118
122
  { value: 'wallet-setup', role: 'section', label: 'Custody' },
119
123
  { value: 'wallet-setup', label: walletSetupLabel, hint: walletSetupHint, disabled: !identity.agentId || (flags?.custodyModeDisabled ?? false) },
120
124
  { value: 'prepare-transfer', label: 'Prepare Transfer', hint: prepareTransferHint, disabled: flags?.prepareTransferDisabled ?? false },
121
125
  { value: 'identity-values', role: 'section', label: 'Token' },
122
126
  { value: 'identity-values', label: 'Token Values', hint: tokenValuesHint },
123
- { value: 'load', label: 'Load Agent', hint: 'Refresh this agent from chain, or load a different one' },
124
- { value: 'create', label: 'New Agent', hint: 'Mint another token and make it active' },
125
- { value: 'storage', role: 'section', label: 'Storage' },
126
- { value: 'storage', label: 'IPFS Storage', hint: 'Publishing credentials for encrypted snapshots' },
127
+ { value: 'load', label: 'Load Agent', hint: 'Refresh or load another agent' },
128
+ { value: 'create', label: 'New Agent', hint: 'Mint another agent' },
129
+ { value: 'storage', label: 'IPFS Storage', hint: 'Publishing credentials' },
127
130
  { value: 'cancel', role: 'section', label: 'Exit' },
128
- { value: 'cancel', label: 'Close Identity Hub', hint: 'Return to chat without changing identity', role: 'utility' },
131
+ { value: 'cancel', label: 'Close Identity Hub', hint: 'Return to chat', role: 'utility' },
129
132
  ]
130
133
  : [
131
134
  { value: 'create', role: 'section', label: 'Setup' },
132
135
  { value: 'create', label: 'Create New Agent', hint: 'Mint a wallet-owned token for this machine' },
133
136
  { value: 'load', label: 'Load Existing Agent', hint: 'Find a token owned by this wallet or linked to it' },
134
- { value: mode === 'first-run' ? 'skip' : 'cancel', role: 'section', label: 'Exit' },
135
137
  ...(mode === 'first-run'
136
138
  ? [
137
139
  { value: 'skip' as Action, label: 'Skip For Now', hint: 'Continue now, use /identity later', role: 'utility' as const },
@@ -170,6 +172,7 @@ export const MenuScreen: React.FC<MenuScreenProps> = ({
170
172
  if (choice === 'ens-name') return onEnsName()
171
173
  if (choice === 'wallet-setup') return onWalletSetup()
172
174
  if (choice === 'continuity') return onContinuity()
175
+ if (choice === 'skills-tree') return onSkillsTree()
173
176
  if (choice === 'backup') return onBackupNow()
174
177
  if (choice === 'refetch') return onRefetchLatest()
175
178
  if (choice === 'identity-values') return onIdentityValues()
@@ -194,7 +197,7 @@ function renderReconciliationBanner(r: AgentReconciliation, identity: EthagentId
194
197
  return (
195
198
  <>
196
199
  <Text color={theme.accentError} bold>Agent Unlinked</Text>
197
- <Text color={theme.textSubtle}>{tokenLabel} was transferred. Local SOUL.md, MEMORY.md, skills.json remain. Back them up before this directory is reused.</Text>
200
+ <Text color={theme.textSubtle}>{tokenLabel} was transferred. Local SOUL.md, MEMORY.md, and skills remain. Back them up before this directory is reused.</Text>
198
201
  <Text color={theme.textSubtle}>Use Load Agent or New Agent to re-enable disabled actions.</Text>
199
202
  </>
200
203
  )
@@ -202,7 +205,7 @@ function renderReconciliationBanner(r: AgentReconciliation, identity: EthagentId
202
205
  return (
203
206
  <>
204
207
  <Text color={theme.accentError} bold>Agent Unlinked</Text>
205
- <Text color={theme.textSubtle}>{tokenLabel} left without Prepare Transfer. Back up local SOUL.md, MEMORY.md, skills.json before loading another agent.</Text>
208
+ <Text color={theme.textSubtle}>{tokenLabel} left without Prepare Transfer. Back up local SOUL.md, MEMORY.md, and skills before loading another agent.</Text>
206
209
  <Text color={theme.textSubtle}>For continuity handoff: ask the new holder to return the token, then run Prepare Transfer before re-sending.</Text>
207
210
  <Text color={theme.textSubtle}>Use Load Agent or New Agent to re-enable disabled actions.</Text>
208
211
  </>
@@ -221,7 +224,7 @@ function renderReconciliationBanner(r: AgentReconciliation, identity: EthagentId
221
224
  }
222
225
  const lines: string[] = []
223
226
  if (r.custody === 'mid-flow-uri-pending') lines.push('Advanced setup pending. Open Custody Mode to finish.')
224
- if (r.agentUri === 'local-newer') lines.push('Local state newer than chain. Save Snapshot Now to publish.')
227
+ if (r.agentUri === 'local-newer') lines.push('Local state newer than onchain. Save Snapshot Now to publish.')
225
228
  if (r.agentUri === 'chain-newer') lines.push('Onchain agentURI is newer than local. Refetch Latest.')
226
229
  if (r.vault === 'missing') lines.push('Recorded vault address has no contract at it. Open Custody Mode to redeploy.')
227
230
  if (r.workingTree === 'dirty') lines.push('Local edits pending. Save Snapshot Now to publish.')
@@ -33,7 +33,7 @@ export const UnlinkedIdentityScreen: React.FC<UnlinkedIdentityScreenProps> = ({
33
33
  ]
34
34
  if (onRetry) {
35
35
  options.push({ value: 'retry', role: 'section', label: 'Recheck' })
36
- options.push({ value: 'retry', label: 'Retry Ownership Check', hint: 'Re-query the chain to confirm the current owner', role: 'utility' })
36
+ options.push({ value: 'retry', label: 'Retry Ownership Check', hint: 'Re-query onchain to confirm the current owner', role: 'utility' })
37
37
  }
38
38
 
39
39
  const tokenLabel = agentId ? `Token #${agentId}` : 'Token'
@@ -43,18 +43,18 @@ export const UnlinkedIdentityScreen: React.FC<UnlinkedIdentityScreenProps> = ({
43
43
  <Surface
44
44
  title="No Linked Agent"
45
45
  subtitle="The agent token recorded locally is not currently owned by your wallet."
46
- footer={<Text color={theme.dim}>enter selects, esc back</Text>}
46
+ footer={<Text color={theme.dim}>enter selects · esc back</Text>}
47
47
  >
48
48
  <Box flexDirection="column">
49
49
  {transferSnapshot ? (
50
50
  <Text color={theme.textSubtle}>
51
- {tokenLabel} was transferred. Local SOUL.md, MEMORY.md, skills.json remain. Back them up before this directory is reused.
51
+ {tokenLabel} was transferred. Local SOUL.md, MEMORY.md, and skills remain. Back them up before this directory is reused.
52
52
  </Text>
53
53
  ) : (
54
54
  <>
55
55
  <Text color={theme.accentPeriwinkle}>{tokenLabel} left this wallet without Prepare Transfer, so the new holder has no continuity handoff.</Text>
56
56
  <Text color={theme.textSubtle}>
57
- Local SOUL.md, MEMORY.md, skills.json remain. Back them up before this directory is reused.
57
+ Local SOUL.md, MEMORY.md, and skills remain. Back them up before this directory is reused.
58
58
  </Text>
59
59
  </>
60
60
  )}