ethagent 3.0.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 (26) hide show
  1. package/package.json +1 -1
  2. package/src/identity/continuity/envelope.ts +9 -9
  3. package/src/identity/continuity/publicSkills.ts +17 -0
  4. package/src/identity/hub/OperationalRoutes.tsx +11 -39
  5. package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +3 -23
  6. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +5 -5
  7. package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -3
  8. package/src/identity/hub/continuity/skills/SkillActionsScreen.tsx +151 -0
  9. package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +1 -33
  10. package/src/identity/hub/continuity/state.ts +9 -9
  11. package/src/identity/hub/create/CreateFlow.tsx +1 -1
  12. package/src/identity/hub/custody/routes.tsx +1 -1
  13. package/src/identity/hub/ens/EnsEditAdvancedScreens.tsx +0 -1
  14. package/src/identity/hub/ens/EnsEditMaintenanceScreens.tsx +0 -1
  15. package/src/identity/hub/identityHubReducer.ts +3 -9
  16. package/src/identity/hub/profile/EditProfileFlow.tsx +5 -5
  17. package/src/identity/hub/restore/recovery.ts +3 -3
  18. package/src/identity/hub/shared/components/IdentitySummary.tsx +22 -2
  19. package/src/identity/hub/shared/components/MenuScreen.tsx +4 -4
  20. package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +3 -3
  21. package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +1 -1
  22. package/src/identity/hub/useIdentityHubContinuity.ts +3 -3
  23. package/src/identity/wallet/page/copy.ts +43 -43
  24. package/src/models/modelPickerOptions.ts +2 -0
  25. package/src/identity/hub/continuity/skills/DeleteSkillScreen.tsx +0 -123
  26. package/src/identity/hub/continuity/skills/SkillVisibilityScreen.tsx +0 -171
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ethagent",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "A privacy-first AI agent with a portable Ethereum identity",
5
5
  "type": "module",
6
6
  "main": "bin/ethagent.js",
@@ -224,10 +224,10 @@ export class ContinuitySnapshotRestoreSlotMissingError extends Error {
224
224
  const CONTINUITY_SNAPSHOT_CHALLENGE_MESSAGES = [
225
225
  'Save or Restore Identity Files',
226
226
  'Action: encrypt or decrypt local identity files',
227
- 'Private: SOUL.md, MEMORY.md',
227
+ 'Private: SOUL.md, MEMORY.md, skills',
228
228
  'Public: public skills and profile',
229
229
  'Safety: no transaction, spending, or approvals',
230
- 'Version: 1',
230
+ 'Version: 2',
231
231
  ] as const
232
232
 
233
233
  export function createContinuitySnapshotChallenge(ownerAddress: string): string {
@@ -265,10 +265,10 @@ export function createTransferContinuitySnapshotChallenge(args: {
265
265
  `Sender Owner: ${ownerAddress}`,
266
266
  `Receiver Owner: ${targetAddress}`,
267
267
  'Action: encrypt or decrypt local identity files for this token transfer',
268
- 'Private: SOUL.md, MEMORY.md',
268
+ 'Private: SOUL.md, MEMORY.md, skills',
269
269
  'Public: public skills and profile',
270
270
  'Safety: no transaction, spending, or approvals',
271
- 'Version: 1',
271
+ 'Version: 2',
272
272
  ].join('\n')
273
273
  }
274
274
 
@@ -290,7 +290,7 @@ const WALLET_CHALLENGE_V2_COPY: Record<WalletChallengePurpose, { title: string;
290
290
  'create-agent': { title: 'Create Agent Snapshot Key', action: 'Action: encrypt the new agent snapshot for owner restore' },
291
291
  'update-snapshot': { title: 'Save Snapshot Encryption Key', action: 'Action: encrypt the updated agent snapshot' },
292
292
  'update-ens-snapshot': { title: 'Update ENS in Agent Snapshot', action: 'Action: encrypt the snapshot with the new ENS name. No onchain ENS records change.' },
293
- 'clear-ens-snapshot': { title: 'Clear ENS from Agent Snapshot', action: 'Action: encrypt the snapshot with no ENS name. No onchain ENS records change.' },
293
+ 'clear-ens-snapshot': { title: 'Unlink ENS from Agent', action: 'Action: encrypt the snapshot with no ENS name. No onchain ENS records change.' },
294
294
  'update-profile-snapshot': { title: 'Update Public Profile Snapshot Key', action: 'Action: encrypt the snapshot with the updated profile' },
295
295
  'update-operators-snapshot': { title: 'Update Operator Wallets Snapshot Key', action: 'Action: encrypt the snapshot with the updated operator list' },
296
296
  'refetch-snapshot': { title: 'Refetch Latest Snapshot', action: 'Action: decrypt the latest published snapshot' },
@@ -322,9 +322,9 @@ export function createWalletRestoreAccessChallenge(args: {
322
322
  `Wallet: ${walletAddress}`,
323
323
  `Access Epoch: ${args.accessEpoch ?? 1}`,
324
324
  copy.action,
325
- 'Private: SOUL.md, MEMORY.md',
325
+ 'Private: SOUL.md, MEMORY.md, skills',
326
326
  'Safety: no transaction, spending, or approvals',
327
- 'Version: 2',
327
+ 'Version: 3',
328
328
  ].join('\n')
329
329
  }
330
330
  return [
@@ -336,9 +336,9 @@ export function createWalletRestoreAccessChallenge(args: {
336
336
  `Wallet: ${walletAddress}`,
337
337
  `Access Epoch: ${args.accessEpoch ?? 1}`,
338
338
  'Action: create a restore key for encrypted identity snapshots',
339
- 'Private: SOUL.md, MEMORY.md',
339
+ 'Private: SOUL.md, MEMORY.md, skills',
340
340
  'Safety: no transaction, spending, or approvals',
341
- 'Version: 1',
341
+ 'Version: 2',
342
342
  ].join('\n')
343
343
  }
344
344
 
@@ -1,5 +1,7 @@
1
1
  import type { EthagentIdentity } from '../../storage/config.js'
2
2
  import type { SkillIndexEntry } from './skills/types.js'
3
+ import { identityOwnerAddress } from '../hub/custody/state.js'
4
+ import { toChecksumAddress } from '../crypto/eth.js'
3
5
 
4
6
  type PublicSkill = {
5
7
  id: string
@@ -13,6 +15,7 @@ type PublicSkillsProfile = {
13
15
  name: string
14
16
  description: string
15
17
  version: string
18
+ agentWallet: string
16
19
  imageUrl?: string
17
20
  skills: PublicSkill[]
18
21
  }
@@ -30,6 +33,8 @@ type AgentCard = {
30
33
  streaming: boolean
31
34
  pushNotifications: boolean
32
35
  }
36
+ producer: { name: string; url: string }
37
+ agent_wallet: string
33
38
  skills: Array<{
34
39
  id: string
35
40
  name: string
@@ -39,6 +44,11 @@ type AgentCard = {
39
44
  }>
40
45
  }
41
46
 
47
+ const ETHAGENT_PRODUCER = {
48
+ name: 'ethagent',
49
+ url: 'https://github.com/baairon/ethagent',
50
+ } as const
51
+
42
52
  export function defaultPublicSkillsProfile(identity: EthagentIdentity): PublicSkillsProfile {
43
53
  const state = identity.state ?? {}
44
54
  const name = typeof state.name === 'string' && state.name.trim()
@@ -50,10 +60,13 @@ export function defaultPublicSkillsProfile(identity: EthagentIdentity): PublicSk
50
60
  const imageUrl = typeof state.imageUrl === 'string' && state.imageUrl.trim()
51
61
  ? state.imageUrl.trim()
52
62
  : undefined
63
+ const ownerAddress = identityOwnerAddress(identity)
64
+ const agentWallet = ownerAddress ? toChecksumAddress(ownerAddress) : ''
53
65
  return {
54
66
  name,
55
67
  description,
56
68
  version: '1.0.0',
69
+ agentWallet,
57
70
  ...(imageUrl ? { imageUrl } : {}),
58
71
  skills: [
59
72
  {
@@ -121,6 +134,8 @@ export function renderPublicSkillsJson(profile: PublicSkillsProfile): string {
121
134
  const outputModes = unique(profile.skills.flatMap(skill => skill.outputModes))
122
135
  const summary = {
123
136
  schema: 'ethagent.public-skills.v1',
137
+ producer: ETHAGENT_PRODUCER,
138
+ agent_wallet: profile.agentWallet,
124
139
  visibility: 'public',
125
140
  name: profile.name,
126
141
  description: profile.description,
@@ -185,6 +200,8 @@ export function createAgentCard(profile: PublicSkillsProfile, url?: string): Age
185
200
  streaming: true,
186
201
  pushNotifications: false,
187
202
  },
203
+ producer: ETHAGENT_PRODUCER,
204
+ agent_wallet: profile.agentWallet,
188
205
  skills: profile.skills.map(skill => ({
189
206
  id: skill.id,
190
207
  name: skill.name,
@@ -20,12 +20,8 @@ import {
20
20
  import { SkillsTreeScreen } from './continuity/skills/SkillsTreeScreen.js'
21
21
  import { NewSkillScreen } from './continuity/skills/NewSkillScreen.js'
22
22
  import { NewSkillVisibilityScreen } from './continuity/skills/NewSkillVisibilityScreen.js'
23
- import { DeleteSkillScreen } from './continuity/skills/DeleteSkillScreen.js'
23
+ import { SkillActionsScreen } from './continuity/skills/SkillActionsScreen.js'
24
24
  import { DeleteSkillConfirmScreen } from './continuity/skills/DeleteSkillConfirmScreen.js'
25
- import {
26
- SkillVisibilityListScreen,
27
- SkillVisibilityPickScreen,
28
- } from './continuity/skills/SkillVisibilityScreen.js'
29
25
  import { RecoveryConfirmScreen } from './continuity/RecoveryConfirmScreen.js'
30
26
  import { SavePromptScreen } from './continuity/SavePromptScreen.js'
31
27
  import { ErrorScreen } from './shared/components/ErrorScreen.js'
@@ -136,9 +132,9 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
136
132
  return (
137
133
  <WalletApprovalScreen
138
134
  title="Refetch Latest Snapshot"
139
- subtitle="Wallet signature decrypts the latest saved snapshot and restores SOUL.md, MEMORY.md, and skills.json."
135
+ subtitle="Wallet signature decrypts the latest saved snapshot and restores SOUL.md, MEMORY.md, and skills."
140
136
  walletSession={walletSession}
141
- label={restoreProgress?.label ?? 'fetching latest snapshot from chain...'}
137
+ label={restoreProgress?.label ?? 'fetching latest snapshot from onchain...'}
142
138
  onCancel={() => setStep(step.back)}
143
139
  />
144
140
  )
@@ -170,37 +166,25 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
170
166
  notice={step.notice}
171
167
  editorOpened={step.editorOpened}
172
168
  footer={footer}
173
- onOpenSkill={relativePath => { void openSkillFile(relativePath) }}
169
+ onOpenSkill={relativePath => setStep({ kind: 'continuity-skill-actions', relativePath })}
174
170
  onNewSkill={() => setStep({ kind: 'continuity-skill-new' })}
175
- onDelete={() => setStep({ kind: 'continuity-skill-delete' })}
176
- onVisibility={() => setStep({ kind: 'continuity-skill-visibility' })}
177
- onViewPublicManifest={() => { void openContinuityFile('skills') }}
178
171
  onOpenFolder={() => { void openSkillsFolder() }}
179
172
  onBack={back}
180
173
  />
181
174
  )
182
175
  }
183
176
 
184
- if (step.kind === 'continuity-skill-visibility') {
185
- return (
186
- <SkillVisibilityListScreen
187
- identity={identity}
188
- notice={step.notice}
189
- footer={footer}
190
- onPick={relativePath => setStep({ kind: 'continuity-skill-visibility-pick', relativePath })}
191
- onCancel={back}
192
- />
193
- )
194
- }
195
-
196
- if (step.kind === 'continuity-skill-visibility-pick') {
177
+ if (step.kind === 'continuity-skill-actions') {
197
178
  return (
198
- <SkillVisibilityPickScreen
179
+ <SkillActionsScreen
199
180
  identity={identity}
200
181
  relativePath={step.relativePath}
182
+ {...(step.notice ? { notice: step.notice } : {})}
201
183
  footer={footer}
202
- onSelect={visibility => { void setSkillVisibility(step.relativePath, visibility) }}
203
- onCancel={back}
184
+ onOpenSkill={relativePath => { void openSkillFile(relativePath) }}
185
+ onSetVisibility={(relativePath, visibility) => { void setSkillVisibility(relativePath, visibility) }}
186
+ onDelete={relativePath => setStep({ kind: 'continuity-skill-delete-confirm', target: { kind: 'skill', relativePath } })}
187
+ onBack={back}
204
188
  />
205
189
  )
206
190
  }
@@ -228,18 +212,6 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
228
212
  )
229
213
  }
230
214
 
231
- if (step.kind === 'continuity-skill-delete') {
232
- return (
233
- <DeleteSkillScreen
234
- identity={identity}
235
- notice={step.notice}
236
- footer={footer}
237
- onPick={target => setStep({ kind: 'continuity-skill-delete-confirm', target })}
238
- onCancel={back}
239
- />
240
- )
241
- }
242
-
243
215
  if (step.kind === 'continuity-skill-delete-confirm') {
244
216
  return (
245
217
  <DeleteSkillConfirmScreen
@@ -6,7 +6,6 @@ import { theme } from '../../../ui/theme.js'
6
6
  import type { EthagentConfig, EthagentIdentity } from '../../../storage/config.js'
7
7
  import type { ContinuityWorkingTreeStatus } from '../../continuity/storage.js'
8
8
  import { IdentitySummary } from '../shared/components/IdentitySummary.js'
9
- import { changedContinuitySnapshotFiles } from './state.js'
10
9
  import { readIdentityStateString } from '../custody/state.js'
11
10
  import { shortCid } from '../shared/model/format.js'
12
11
 
@@ -24,23 +23,6 @@ interface CommonProps {
24
23
  onBack: () => void
25
24
  }
26
25
 
27
- const SaveFromHubHint: React.FC<{ workingStatus?: ContinuityWorkingTreeStatus | null }> = ({ workingStatus }) => {
28
- const needsBackup = workingStatus?.publishState === 'local-changes'
29
- || workingStatus?.publishState === 'not-published'
30
- || workingStatus?.publishState === 'verify-needed'
31
- if (!needsBackup) return null
32
- const files = changedContinuitySnapshotFiles(workingStatus)
33
- return (
34
- <Box marginTop={1} flexDirection="column">
35
- <Text color={theme.accentError} bold>
36
- Unsaved changes
37
- {files.length > 0 ? `: ${files.join(', ')}` : ''}
38
- </Text>
39
- <Text color={theme.dim}>Save Snapshot Now to publish.</Text>
40
- </Box>
41
- )
42
- }
43
-
44
26
  export const PrivateContinuityScreen: React.FC<CommonProps & {
45
27
  onOpenSoul: () => void
46
28
  onOpenMemory: () => void
@@ -57,8 +39,7 @@ export const PrivateContinuityScreen: React.FC<CommonProps & {
57
39
  onBack,
58
40
  }) => (
59
41
  <Surface title="Soul & Memory" subtitle={notice ?? privateSubtitle(ready)} footer={footer}>
60
- <IdentitySummary identity={identity} config={config} workingStatus={workingStatus} hideLocalChanges />
61
- <SaveFromHubHint workingStatus={workingStatus} />
42
+ <IdentitySummary identity={identity} config={config} workingStatus={workingStatus} hideLocalChanges compact />
62
43
  {editorOpened && (
63
44
  <Box marginTop={1}>
64
45
  <Text color={theme.accentPeriwinkle}>Save with ctrl+s in your editor</Text>
@@ -90,8 +71,7 @@ export const PublicProfileScreen: React.FC<CommonProps & {
90
71
  }> = ({ identity, config, workingStatus, notice, editorOpened, footer, onEditProfile, onBack }) => {
91
72
  return (
92
73
  <Surface title="Public Profile" subtitle={notice ?? 'Manage the public name, description, icon, and Agent Card.'} footer={footer}>
93
- <IdentitySummary identity={identity} config={config} workingStatus={workingStatus} hideLocalChanges />
94
- <SaveFromHubHint workingStatus={workingStatus} />
74
+ <IdentitySummary identity={identity} config={config} workingStatus={workingStatus} hideLocalChanges compact />
95
75
  {editorOpened && (
96
76
  <Box marginTop={1}>
97
77
  <Text color={theme.accentPeriwinkle}>Save with ctrl+s in your editor</Text>
@@ -101,7 +81,7 @@ export const PublicProfileScreen: React.FC<CommonProps & {
101
81
  <Select<PublicAction>
102
82
  options={[
103
83
  { value: 'edit', role: 'section', label: 'Profile' },
104
- { value: 'edit', label: 'Edit Name, Description, Icon', hint: 'Update public profile fields used in the Agent Card' },
84
+ { value: 'edit', label: 'Edit Profile', hint: 'Name, description, icon' },
105
85
  { value: 'back', role: 'section', label: 'Navigation' },
106
86
  { value: 'back', label: 'Back', hint: 'Return to Identity Hub menu', role: 'utility' },
107
87
  ]}
@@ -20,15 +20,15 @@ interface RecoveryConfirmScreenProps {
20
20
 
21
21
  export const RecoveryConfirmScreen: React.FC<RecoveryConfirmScreenProps> = ({ mode, workingStatus, pendingPublish, footer, onConfirm, onBack }) => {
22
22
  const isPublish = mode === 'publish'
23
- const title = isPublish ? 'Save Snapshot?' : 'Refetch Latest From Chain?'
23
+ const title = isPublish ? 'Save Snapshot?' : 'Refetch Latest From Onchain?'
24
24
  const subtitle = isPublish
25
- ? 'Saves SOUL.md, MEMORY.md, skills.json, and profile changes.'
25
+ ? 'Saves SOUL.md, MEMORY.md, skills, and profile changes.'
26
26
  : 'This overwrites local files with the onchain version.'
27
27
 
28
28
  const headlineColor = theme.accentPeriwinkle
29
29
  const headline = isPublish
30
30
  ? 'Saving updates the onchain pointer for this agent.'
31
- : 'Refetching replaces SOUL.md, MEMORY.md, and skills.json with what is onchain.'
31
+ : 'Refetching replaces SOUL.md, MEMORY.md, and skills with what is onchain.'
32
32
  const detail = isPublish
33
33
  ? 'Your local continuity files and profile edits become the saved state. The previous snapshot pointer is overwritten.'
34
34
  : 'Unsaved local edits will be lost. Use this when local files are missing or out of sync with the latest saved snapshot.'
@@ -47,7 +47,7 @@ export const RecoveryConfirmScreen: React.FC<RecoveryConfirmScreenProps> = ({ mo
47
47
  )}
48
48
  {!isPublish && pendingPublish ? (
49
49
  <Box marginTop={1} flexDirection="column">
50
- <Text color={theme.accentError} bold>Local snapshot is ahead of chain.</Text>
50
+ <Text color={theme.accentError} bold>Local snapshot is ahead of onchain.</Text>
51
51
  <Text color={theme.textSubtle}>Local edits have not yet been rotated to the onchain pointer. Refetching discards them and reverts to the last published snapshot.</Text>
52
52
  </Box>
53
53
  ) : null}
@@ -63,7 +63,7 @@ export const RecoveryConfirmScreen: React.FC<RecoveryConfirmScreenProps> = ({ mo
63
63
  { value: 'confirm', role: 'section', label: isPublish ? 'Save' : 'Refetch' },
64
64
  {
65
65
  value: 'confirm',
66
- label: isPublish ? 'Yes, Save Snapshot Now' : 'Yes, Refetch From Chain',
66
+ label: isPublish ? 'Yes, Save Snapshot Now' : 'Yes, Refetch From Onchain',
67
67
  hint: isPublish ? 'Sign and save the encrypted snapshot' : 'Wallet decrypts and overwrites local files',
68
68
  },
69
69
  { value: 'back', role: 'section', label: 'Navigation' },
@@ -35,10 +35,8 @@ export const SavePromptScreen: React.FC<SavePromptScreenProps> = ({ workingStatu
35
35
  <Box marginTop={1}>
36
36
  <Select<SavePromptAction>
37
37
  options={[
38
- { value: 'save-now', role: 'section', label: 'Save' },
39
38
  { value: 'save-now', label: 'Save now', hint: 'Sign once and save the encrypted snapshot' },
40
- { value: 'later', role: 'section', label: 'Defer' },
41
- { value: 'later', label: 'Not now', hint: 'Ask again on the next ethagent launch', role: 'utility' },
39
+ { value: 'later', label: 'Not now', hint: 'Ask again on the next launch', role: 'utility' },
42
40
  ]}
43
41
  hintLayout="inline"
44
42
  onSubmit={onSelect}
@@ -0,0 +1,151 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import { Box, Text } from 'ink'
3
+ import { Surface } from '../../../../ui/Surface.js'
4
+ import { Select, type SelectOption } from '../../../../ui/Select.js'
5
+ import { theme } from '../../../../ui/theme.js'
6
+ import type { EthagentIdentity } from '../../../../storage/config.js'
7
+ import { listSkills, listSkillFiles } from '../../../continuity/skills/loadSkills.js'
8
+ import type { SkillIndexEntry, SkillVisibility } from '../../../continuity/skills/types.js'
9
+
10
+ type SkillAction =
11
+ | { kind: 'open' }
12
+ | { kind: 'set-visibility'; visibility: SkillVisibility }
13
+ | { kind: 'delete' }
14
+ | { kind: 'back' }
15
+ | { kind: 'noop' }
16
+
17
+ interface SkillActionsScreenProps {
18
+ identity?: EthagentIdentity
19
+ relativePath: string
20
+ notice?: string
21
+ footer: React.ReactNode
22
+ onOpenSkill: (relativePath: string) => void
23
+ onSetVisibility: (relativePath: string, visibility: SkillVisibility) => void
24
+ onDelete: (relativePath: string) => void
25
+ onBack: () => void
26
+ }
27
+
28
+ export const SkillActionsScreen: React.FC<SkillActionsScreenProps> = ({
29
+ identity,
30
+ relativePath,
31
+ notice,
32
+ footer,
33
+ onOpenSkill,
34
+ onSetVisibility,
35
+ onDelete,
36
+ onBack,
37
+ }) => {
38
+ const [entry, setEntry] = useState<SkillIndexEntry | null>(null)
39
+ const [supportingCount, setSupportingCount] = useState<number | null>(null)
40
+ const skillName = relativePath.split('/')[0] ?? ''
41
+
42
+ useEffect(() => {
43
+ let cancelled = false
44
+ if (!identity) return () => { cancelled = true }
45
+ listSkills(identity)
46
+ .then(list => {
47
+ if (cancelled) return
48
+ const match = list.find(item => item.relativePath === relativePath)
49
+ setEntry(match ?? null)
50
+ })
51
+ .catch(() => { if (!cancelled) setEntry(null) })
52
+ listSkillFiles(identity, skillName)
53
+ .then(files => {
54
+ if (cancelled) return
55
+ setSupportingCount(files.filter(f => f.relativePath !== 'SKILL.md').length)
56
+ })
57
+ .catch(() => { if (!cancelled) setSupportingCount(null) })
58
+ return () => { cancelled = true }
59
+ }, [identity, relativePath, skillName, notice])
60
+
61
+ const displayName = entry?.displayName ?? entry?.name ?? skillName
62
+ const visibility = entry?.visibility
63
+ const subtitle = notice ?? 'Open, change visibility, or delete this skill.'
64
+
65
+ const options: Array<SelectOption<SkillAction>> = []
66
+ const noop: SkillAction = { kind: 'noop' }
67
+
68
+ options.push({ value: noop, role: 'section', label: 'Open' })
69
+ options.push({
70
+ value: { kind: 'open' },
71
+ label: 'Open SKILL.md',
72
+ hint: 'View or edit this skill',
73
+ })
74
+
75
+ options.push({ value: noop, role: 'section', label: 'Visibility' })
76
+ options.push(visibilityOption('private', visibility))
77
+ options.push(visibilityOption('discoverable', visibility))
78
+ options.push(visibilityOption('public', visibility))
79
+
80
+ options.push({ value: noop, role: 'section', label: 'Manage' })
81
+ options.push({
82
+ value: { kind: 'delete' },
83
+ label: 'Delete',
84
+ hint: 'Remove this skill folder and its supporting files',
85
+ })
86
+ options.push({
87
+ value: { kind: 'back' },
88
+ label: 'Back',
89
+ hint: 'Return to Skills',
90
+ role: 'utility',
91
+ })
92
+
93
+ const leafMeta = formatLeafMeta(visibility, supportingCount)
94
+
95
+ return (
96
+ <Surface title={`Skill · ${displayName}`} subtitle={subtitle} footer={footer}>
97
+ <Box flexDirection="column" marginTop={1}>
98
+ <Text color={theme.dim}>skills/</Text>
99
+ <Text>
100
+ <Text color={theme.dim}>└── </Text>
101
+ <Text color={theme.accentPeriwinkle} bold>{`${skillName}/SKILL.md`}</Text>
102
+ {leafMeta ? <Text color={theme.dim}>{` ${leafMeta}`}</Text> : null}
103
+ </Text>
104
+ </Box>
105
+ <Box marginTop={1}>
106
+ <Select<SkillAction>
107
+ options={options}
108
+ hintLayout="inline"
109
+ onSubmit={choice => {
110
+ if (choice.kind === 'open') return onOpenSkill(relativePath)
111
+ if (choice.kind === 'set-visibility') return onSetVisibility(relativePath, choice.visibility)
112
+ if (choice.kind === 'delete') return onDelete(relativePath)
113
+ if (choice.kind === 'back') return onBack()
114
+ }}
115
+ onCancel={onBack}
116
+ />
117
+ </Box>
118
+ </Surface>
119
+ )
120
+ }
121
+
122
+ function formatLeafMeta(visibility: SkillVisibility | undefined, supportingCount: number | null): string {
123
+ if (!visibility) return ''
124
+ const fileLabel = supportingCount === null
125
+ ? null
126
+ : supportingCount === 0 ? '1 file' : `${supportingCount + 1} files`
127
+ return fileLabel ? `${capitalize(visibility)} · ${fileLabel}` : capitalize(visibility)
128
+ }
129
+
130
+ function visibilityOption(level: SkillVisibility, current?: SkillVisibility): SelectOption<SkillAction> {
131
+ const isCurrent = current === level
132
+ const hint = visibilityHint(level) + (isCurrent ? ' · current' : '')
133
+ const base: SelectOption<SkillAction> = {
134
+ value: { kind: 'set-visibility', visibility: level },
135
+ label: `Set ${capitalize(level)}`,
136
+ hint,
137
+ }
138
+ if (isCurrent) base.labelColor = theme.accentPeriwinkle
139
+ return base
140
+ }
141
+
142
+ function visibilityHint(level: SkillVisibility): string {
143
+ if (level === 'private') return 'Local-only. Not in skills.json.'
144
+ if (level === 'discoverable') return 'Default. Indexed with description.'
145
+ return 'Indexed with description and Agent Card link.'
146
+ }
147
+
148
+ function capitalize(value: string): string {
149
+ if (!value) return value
150
+ return value.charAt(0).toUpperCase() + value.slice(1)
151
+ }
@@ -15,9 +15,6 @@ import type { ContinuityWorkingTreeStatus } from '../../../continuity/storage.js
15
15
  type SkillsTreeAction =
16
16
  | { kind: 'skill'; relativePath: string }
17
17
  | { kind: 'new' }
18
- | { kind: 'delete' }
19
- | { kind: 'visibility' }
20
- | { kind: 'view-manifest' }
21
18
  | { kind: 'open-folder' }
22
19
  | { kind: 'noop' }
23
20
  | { kind: 'back' }
@@ -31,9 +28,6 @@ interface SkillsTreeScreenProps {
31
28
  footer: React.ReactNode
32
29
  onOpenSkill: (relativePath: string) => void
33
30
  onNewSkill: () => void
34
- onDelete: () => void
35
- onVisibility: () => void
36
- onViewPublicManifest: () => void
37
31
  onOpenFolder: () => void
38
32
  onBack: () => void
39
33
  }
@@ -47,9 +41,6 @@ export const SkillsTreeScreen: React.FC<SkillsTreeScreenProps> = ({
47
41
  footer,
48
42
  onOpenSkill,
49
43
  onNewSkill,
50
- onDelete,
51
- onVisibility,
52
- onViewPublicManifest,
53
44
  onOpenFolder,
54
45
  onBack,
55
46
  }) => {
@@ -92,7 +83,7 @@ export const SkillsTreeScreen: React.FC<SkillsTreeScreenProps> = ({
92
83
 
93
84
  return (
94
85
  <Surface title="Skills" subtitle={subtitle} footer={footer}>
95
- <IdentitySummary identity={identity} config={config} workingStatus={workingStatus} />
86
+ <IdentitySummary identity={identity} config={config} workingStatus={workingStatus} compact />
96
87
  {error && (
97
88
  <Box marginTop={1}>
98
89
  <Text color={theme.accentError}>{error}</Text>
@@ -110,9 +101,6 @@ export const SkillsTreeScreen: React.FC<SkillsTreeScreenProps> = ({
110
101
  onSubmit={choice => {
111
102
  if (choice.kind === 'skill') return onOpenSkill(choice.relativePath)
112
103
  if (choice.kind === 'new') return onNewSkill()
113
- if (choice.kind === 'delete') return onDelete()
114
- if (choice.kind === 'visibility') return onVisibility()
115
- if (choice.kind === 'view-manifest') return onViewPublicManifest()
116
104
  if (choice.kind === 'open-folder') return onOpenFolder()
117
105
  if (choice.kind === 'back') return onBack()
118
106
  }}
@@ -166,37 +154,17 @@ function buildOptions(
166
154
  }
167
155
  }
168
156
 
169
- rows.push({ value: noopValue, role: 'notice', label: '' })
170
157
  rows.push({ value: noopValue, role: 'section', label: 'Manage' })
171
158
  rows.push({
172
159
  value: { kind: 'new' },
173
160
  label: 'New Skill',
174
161
  hint: 'Scaffold a new skill folder with SKILL.md',
175
162
  })
176
- if (hasAny) {
177
- rows.push({
178
- value: { kind: 'delete' },
179
- label: 'Delete Skill',
180
- hint: 'Remove a skill folder and all its supporting files',
181
- })
182
- rows.push({
183
- value: { kind: 'visibility' },
184
- label: 'Change Visibility',
185
- hint: 'Toggle public, discoverable, or private',
186
- })
187
- }
188
- rows.push({ value: noopValue, role: 'section', label: 'Inspect' })
189
- rows.push({
190
- value: { kind: 'view-manifest' },
191
- label: 'Edit skills.json',
192
- hint: 'Open skills.json in your editor',
193
- })
194
163
  rows.push({
195
164
  value: { kind: 'open-folder' },
196
165
  label: 'Open Skills Folder',
197
166
  hint: 'Reveal skills/ in your file manager',
198
167
  })
199
- rows.push({ value: noopValue, role: 'section', label: 'Navigation' })
200
168
  rows.push({
201
169
  value: { kind: 'back' },
202
170
  label: 'Back',
@@ -20,15 +20,15 @@ export function changedContinuitySnapshotFiles(
20
20
  workingStatus?: ContinuityWorkingTreeStatus | null,
21
21
  ): string[] {
22
22
  if (!workingStatus?.localContentHashes || !workingStatus.publishedContentHashes) return []
23
- const files: Array<keyof typeof workingStatus.localContentHashes> = ['SOUL.md', 'MEMORY.md', 'skills.json', 'private-skills']
24
- return files
25
- .filter(file => (workingStatus.localContentHashes?.[file] ?? '') !== (workingStatus.publishedContentHashes?.[file] ?? ''))
26
- .map(displayContinuitySnapshotFile)
27
- }
28
-
29
- function displayContinuitySnapshotFile(file: keyof NonNullable<ContinuityWorkingTreeStatus['localContentHashes']>): string {
30
- if (file === 'private-skills') return 'skills/'
31
- return file
23
+ const local = workingStatus.localContentHashes
24
+ const published = workingStatus.publishedContentHashes
25
+ const changed = (file: keyof typeof local): boolean =>
26
+ (local[file] ?? '') !== (published[file] ?? '')
27
+ const result: string[] = []
28
+ if (changed('SOUL.md')) result.push('SOUL.md')
29
+ if (changed('MEMORY.md')) result.push('MEMORY.md')
30
+ if (changed('skills.json') || changed('private-skills')) result.push('Skills')
31
+ return result
32
32
  }
33
33
 
34
34
  export function localChangeStatusView(
@@ -143,7 +143,7 @@ export const CreateFlow: React.FC<CreateFlowProps> = ({
143
143
  <BusyScreen
144
144
  title="Getting Ready"
145
145
  subtitle={indicator}
146
- label="checking IPFS storage and chain..."
146
+ label="checking IPFS storage and onchain..."
147
147
  onCancel={onBack}
148
148
  />
149
149
  )
@@ -54,7 +54,7 @@ export function renderCustodyStep({
54
54
  footer={<Text color={theme.dim}>esc cancel</Text>}
55
55
  >
56
56
  <Box marginTop={1}>
57
- <Text color={theme.textSubtle}>Reading vault state from chain...</Text>
57
+ <Text color={theme.textSubtle}>Reading vault state from onchain...</Text>
58
58
  </Box>
59
59
  </Surface>
60
60
  )
@@ -19,7 +19,6 @@ import { shortAddress } from '../shared/model/format.js'
19
19
  import {
20
20
  footerHint,
21
21
  } from './EnsEditShared.js'
22
- import { IdentitySummary } from '../shared/components/IdentitySummary.js'
23
22
  import {
24
23
  EnsSetupBlockedScreen,
25
24
  EnsSetupReviewScreen,
@@ -19,7 +19,6 @@ import {
19
19
  EnsSetupRow,
20
20
  footerHint,
21
21
  } from './EnsEditShared.js'
22
- import { IdentitySummary } from '../shared/components/IdentitySummary.js'
23
22
  import { UnlinkEnsReviewScreen } from './EnsEditReviewScreens.js'
24
23
  import {
25
24
  DeleteSubdomainTxRunner,