ethagent 3.3.0 → 3.3.3

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.
package/README.md CHANGED
@@ -1,30 +1,16 @@
1
- <img src="https://raw.githubusercontent.com/baairon/ethagent/refs/heads/master/preview/image.png" alt="ethagent" />
1
+ <img src="https://raw.githubusercontent.com/baairon/ethagent/master/preview/image.svg" alt="ethagent" width="640" />
2
2
 
3
3
  A privacy-first AI agent with a portable Ethereum identity.
4
4
 
5
5
  Switch providers or machines and the AI agent you customized stays behind. `ethagent` ties the agent to a wallet you own, so its soul, memory, and skills follow you across providers, machines, and models.
6
6
 
7
- - **Portable.** The ERC-8004 token is the agent's durable identity. Use the ENS name as a readable handle, or the token ID plus chain as the permanent reference, to restore the same agent anywhere.
8
- - **Private.** Soul, memory, and skills are encrypted before they are pinned to IPFS. The wallet signature used to unlock them stays local and never submits a transaction, spends funds, or grants token approval.
9
- - **Public.** The agent URI points to a public metadata payload on IPFS that includes the Agent Card, so other agents can discover the agent's capabilities through ERC-8004.
10
-
11
- <details>
12
- <summary><strong>Glossary</strong> (click to expand)</summary>
13
-
14
- | Term | Meaning |
15
- | --- | --- |
16
- | Owner Wallet | Holds and controls the ERC-8004 agent token. Signs custody changes and, in Simple custody, every URI rotation. |
17
- | Operator Wallet | Additional wallet authorized to rotate the onchain URI on behalf of the owner. Used in Advanced custody. Never receives token approval. |
18
- | Vault | Immutable per-agent custody contract used in Advanced custody. Holds at most one ERC-8004 token. |
19
- | Snapshot | Encrypted bundle of SOUL.md, MEMORY.md, the skills/ tree, and session state. Pinned to IPFS; decrypts only against the owner wallet's signature. |
20
- | Agent URI | IPFS URI stored in the ERC-8004 `tokenURI`. Resolves to a public metadata payload that references the Agent Card. |
21
- | Agent Card | Public JSON describing the agent: name, description, capabilities, and skills. Pinned on IPFS and linked from the agent URI; other agents fetch it for discovery. |
22
-
23
- </details>
7
+ - **Portable.** Restore the same agent on any machine from a wallet you own.
8
+ - **Private.** Soul, memory, and skills are encrypted on your machine before they sync. The wallet signature that unlocks them stays local and never spends funds.
9
+ - **Public.** Other agents discover the agent's capabilities through a public Agent Card.
24
10
 
25
11
  ## Install
26
12
 
27
- ethagent runs on Node.js 20 or newer.
13
+ ethagent runs on [Node.js](https://nodejs.org).
28
14
 
29
15
  ```bash
30
16
  npm install -g ethagent
@@ -33,10 +19,7 @@ ethagent
33
19
 
34
20
  ## First Run
35
21
 
36
- First run inspects the machine for local-model fit, sets up the ERC-8004 identity, and picks a model.
37
-
38
- - **Models** include OpenAI, Anthropic, Gemini, or a local GGUF served through a llama.cpp-compatible endpoint.
39
- - **Identity** can be a fresh ERC-8004 token created with a browser wallet, an existing token already owned by your wallet, or set up later from the Identity Hub.
22
+ First run inspects the machine for local-model fit, sets up the agent's onchain identity, and picks a model. Identity can be a fresh ERC-8004 token created with a browser wallet, an existing token in your wallet, or skipped and set up later from the Identity Hub.
40
23
 
41
24
  Once running:
42
25
 
@@ -50,18 +33,14 @@ The Identity Hub manages everything portable about the agent:
50
33
 
51
34
  - **Public Profile** edits name, description, and icon: what other agents see in the Agent Card.
52
35
  - **ENS Name** links the agent to a subdomain under a parent name the owner wallet controls.
53
- - **Custody Mode** switches between Simple and Advanced by depositing the token into its Vault or unwrapping it back out.
36
+ - **Custody Mode** switches between Simple and Advanced by depositing the token into its operator delegation vault or unwrapping it back out.
54
37
  - **Prepare Transfer** stages a dual-wallet snapshot so the receiver can restore the agent after the token moves externally.
55
38
  - **Refetch Latest** pulls the most recent published snapshot back to local files.
56
- - **Switch Agent** accepts either an ENS name or a bare token ID, and loads any agent owned by or linked to the connected wallet.
57
-
58
- The menu surfaces drift automatically. Token ownership, vault state, ENS record alignment, and pending URI rotations are checked against the live chain when the menu opens.
59
-
60
- Every agent has a continuity directory at `~/.ethagent/continuity`.
39
+ - **Switch Agent** accepts an ENS name or an ERC-8004 token ID on any supported chain, and loads any agent owned by or linked to the connected wallet.
61
40
 
62
41
  ## Continuity
63
42
 
64
- Each agent's continuity directory holds a small set of private files. They are encrypted before they ever reach IPFS.
43
+ Each agent has a continuity directory at `~/.ethagent/continuity` holding a small set of private files. They are encrypted before they ever reach IPFS.
65
44
 
66
45
  | File | Visibility | Purpose |
67
46
  | --- | --- | --- |
@@ -69,7 +48,7 @@ Each agent's continuity directory holds a small set of private files. They are e
69
48
  | `MEMORY.md` | Private | Durable preferences, project context, decisions, and operating notes. |
70
49
  | `skills/` | Private | Skill folders. The SKILL.md body never leaves your machine. The visibility flag only controls whether the skill's name and description get listed in the Agent Card. New skills default to public. |
71
50
 
72
- `SOUL.md`, `MEMORY.md`, and each `SKILL.md` are plain Markdown you edit through the Identity Hub under Continuity. Skill frontmatter (name, description, when_to_use, visibility, tags) tells the agent when to load it. The body stays local; `visibility: public` lists the name and description in the Agent Card.
51
+ Skill frontmatter (name, description, when_to_use, visibility, tags) tells the agent when to load each skill.
73
52
 
74
53
  - **Save Snapshot Now** encrypts the private files, pins them to IPFS, and rotates the onchain pointer to the new CID.
75
54
  - **Refetch Latest** reads the pointer back, signs the decrypt challenge with your wallet, and overwrites local files from the snapshot.
@@ -81,31 +60,27 @@ Custody comes in two modes. Switch between them anytime from **Custody Mode**.
81
60
 
82
61
  **Simple** relies on one wallet to own the token, sign every snapshot save, and rotate the onchain URI directly. Use Simple when one wallet operates the agent.
83
62
 
84
- **Advanced** splits an owner wallet from one or more operator wallets. The **owner wallet** owns this agent's dedicated Vault; one or more **operator wallets** handle routine URI rotations through that vault. Use Advanced when routine saves should not require an owner signature.
85
-
86
- Granting an operator wallet ERC-721 approval would let it rotate the URI, but that same approval also lets it transfer the token away. The Vault holds the token instead and exposes only a URI-rotation lane for that agent. Operators never receive token approval or transfer rights, cannot touch ENS, and cannot grant rights to other operators. The owner still signs to authorize or revoke operators for the agent, withdraw the token, or transfer the agent.
63
+ **Advanced** splits an owner wallet from one or more operator wallets. The owner wallet owns this agent's operator delegation vault; one or more operator wallets handle routine URI rotations through that vault. Use Advanced when routine saves should not require an owner signature.
87
64
 
88
- The vault is an immutable Foundry contract at `contracts/src/Vault.sol`. New vault deployments are dedicated per agent token and reject any other token.
65
+ Granting an operator wallet ERC-721 approval would let it rotate the URI, but that same approval also lets it transfer the token away. The vault holds the token instead and exposes only a URI-rotation lane for that agent. Operators never receive token approval or transfer rights, cannot touch ENS, and cannot grant rights to other operators. The owner still signs to authorize or revoke operators for the agent, withdraw the token, or transfer the agent.
89
66
 
90
67
  ## ENS Names
91
68
 
92
69
  Subdomains live under a parent name you control, never on root `.eth` names directly. You keep `you.eth`; the agent gets `agent.you.eth`. The split makes the boundary explicit: one address speaks for the human, the other speaks for the agent.
93
70
 
94
- ENS records stay owner-controlled in both custody modes. Operator wallets in Advanced custody rotate the ERC-8004 token URI through the Vault (see Custody Modes), not ENS. Any ENS text-record update requires an owner signature.
71
+ ENS records stay owner-controlled in both custody modes. Operator wallets in Advanced custody rotate the ERC-8004 token URI through the vault (see Custody Modes), not ENS. Any ENS text-record update requires an owner signature.
95
72
 
96
- Save the token ID + network somewhere safe. ENS records can be cleared and rebuilt; the token ID is the durable handle.
73
+ Save the token ID and network somewhere safe. ENS records can be cleared and rebuilt; the token ID is the durable handle.
97
74
 
98
75
  ## Token Transfers
99
76
 
100
- **Prepare Token Transfer** runs before any ERC-8004 token transfer, and only when the token sits directly in your wallet. An agent in Advanced custody has to switch to Simple first from Custody Mode, which unwraps the token from its Vault back to the owner wallet.
77
+ **Prepare Token Transfer** runs before any ERC-8004 token transfer, and only when the token sits directly in your wallet. An agent in Advanced custody has to switch to Simple first from Custody Mode, which unwraps the token from its vault back to the owner wallet.
101
78
 
102
79
  - Sender signs snapshot access, receiver signs restore access.
103
80
  - Sender publishes the snapshot pointer to the agent URI.
104
81
  - The actual transfer happens externally afterwards, in whichever wallet UI you prefer.
105
82
  - Once the token has moved, the receiver opens **Switch Agent** with the receiving wallet and restores the same agent from the published snapshot.
106
83
 
107
- The token transfer flow prepares decrypt access and agent URI pointers only. It does not initiate the transfer and does not request approval over the token.
108
-
109
84
  ## Models
110
85
 
111
86
  ethagent works with OpenAI, Anthropic, Gemini, and local GGUF models served through a llama.cpp-compatible endpoint.
@@ -116,14 +91,7 @@ ethagent works with OpenAI, Anthropic, Gemini, and local GGUF models served thro
116
91
 
117
92
  ### Image Input
118
93
 
119
- Press `Alt+V` to paste an image from the clipboard. A marker like `[Image #1]` appears in the prompt; delete it to drop the attachment.
120
-
121
- Vision support is available on:
122
-
123
- - **OpenAI** (Chat Completions and Responses API): `gpt-4o`, `gpt-4.1`, `gpt-4-turbo`, `gpt-4-vision`, `gpt-5`, `o1`, `o3`, `o4`, `chatgpt-4`.
124
- - **Anthropic**: `claude-3`, `claude-sonnet-4`, `claude-opus-4`, `claude-haiku-4`.
125
- - **Gemini**: `gemini-1.5`, `gemini-2.0`, `gemini-2.5`.
126
- - **Local llama.cpp**: vision works when both the main GGUF and a `mmproj-*.gguf` projector are loaded. The picker recommends the bundle during install; if you skipped, open `Alt+P` and any installed model with a vision encoder available shows an `Add Vision Encoder` row directly beneath it.
94
+ Press `Alt+V` to paste an image from the clipboard. Vision works on current OpenAI, Anthropic, and Gemini models, and on local GGUF models loaded with an `mmproj-*.gguf` projector.
127
95
 
128
96
  ## Tools and Sessions
129
97
 
@@ -138,8 +106,7 @@ Vision support is available on:
138
106
  - **Public:** token ownership, the agent URI, the Agent Card it references, and IPFS CIDs.
139
107
  - **Private:** plaintext `SOUL.md`, plaintext `MEMORY.md`, the local skills/ tree, sessions, prompt history, API keys, local permissions, and the wallet signatures used for decryption.
140
108
  - Snapshots use a wallet signature as unlock material. The signature does not submit a transaction, spend funds, or grant token approval.
141
- - The transfer flow writes a snapshot pointer and stops; it never approves or moves the token.
142
- - `ethagent reset` deletes local ethagent data from the current machine while preserving installed local model assets. It does not burn or transfer tokens, remove public IPFS content, or mutate the onchain agent URI. Run **Save Snapshot Now** before resetting if local edits should become the recoverable state.
109
+ - `ethagent reset` clears local ethagent data from the current machine while preserving installed local model assets; it does not touch onchain state or pinned IPFS content. Run **Save Snapshot Now** first if local edits should become the recoverable state.
143
110
 
144
111
  ## Architecture
145
112
 
@@ -152,8 +119,6 @@ Vision support is available on:
152
119
  | Discovery | The agent URI and the Agent Card it points to. |
153
120
  | Recovery | Refetch the current agent URI, decrypt the latest snapshot, and restore local files. |
154
121
 
155
- The ERC-8004 token is the durable handle. The machine, model, and local session all change around it.
156
-
157
122
  ## Development
158
123
 
159
124
  ```bash
@@ -162,9 +127,7 @@ cd ethagent && npm install
162
127
  npm start
163
128
  ```
164
129
 
165
- The published CLI is source-distributed: `bin/ethagent.js` launches `src/cli/main.tsx` through `tsx`. `npm run build` is therefore a validation build; it checks the shipped TypeScript without producing a separate `dist/` directory.
166
-
167
- Repository structure and refactoring rules are documented in `ARCHITECTURE.md` and `CONTRIBUTING.md`. Stable facades keep public import paths intact while focused sibling modules hold private implementation details.
130
+ `npm run build` is a type-check pass; the published CLI runs the shipped TypeScript directly through `tsx`.
168
131
 
169
132
  | Command | What it does |
170
133
  | --- | --- |
@@ -172,16 +135,10 @@ Repository structure and refactoring rules are documented in `ARCHITECTURE.md` a
172
135
  | `npm run build` | Validate the shipped TypeScript source package. |
173
136
  | `npm test` | Test suite. |
174
137
  | `npm run typecheck` | Run the same TypeScript check directly. |
175
- | `npm run contracts:test` | Foundry tests. |
176
-
177
- Foundry is only needed for `contracts/` changes.
138
+ | `npm run contracts:test` | Foundry tests (only needed for `contracts/` changes). |
178
139
 
179
140
  ## Contributing
180
141
 
181
- Contributions are welcome. For anything beyond a typo, open an issue first at [github.com/baairon/ethagent/issues](https://github.com/baairon/ethagent/issues) so the scope and approach can be agreed before code is written.
182
-
183
- Each PR should cover one logical change, include a clear description, and list the commands you ran for testing. Match project conventions. Do not bundle unrelated cleanup, broad refactors, formatting churn, or changes that have not been reviewed as part of the issue.
184
-
185
- Contributions are released under the MIT license.
142
+ Contributions are welcome. For anything beyond a typo, open an issue first at [github.com/baairon/ethagent/issues](https://github.com/baairon/ethagent/issues) so the scope and approach can be agreed before code is written. Each PR should cover one logical change, include a clear description, and list the commands you ran for testing. Contributions are released under the MIT license.
186
143
 
187
144
  [npm](https://www.npmjs.com/package/ethagent) | [GitHub](https://github.com/baairon/ethagent) | [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) | [soul.md](https://soul.md/)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ethagent",
3
- "version": "3.3.0",
3
+ "version": "3.3.3",
4
4
  "description": "A privacy-first AI agent with a portable Ethereum identity",
5
5
  "type": "module",
6
6
  "main": "bin/ethagent.js",
@@ -244,7 +244,7 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
244
244
  footer?: string,
245
245
  ): React.ReactElement => (
246
246
  <Box flexDirection="column" padding={1}>
247
- <Splash />
247
+ <Splash showTagline />
248
248
  <Box marginTop={1}>
249
249
  <Surface
250
250
  title={title}
@@ -257,12 +257,18 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
257
257
  </Box>
258
258
  )
259
259
 
260
- const renderRaw = (currentKind: FirstRunStepKind, body: React.ReactNode): React.ReactElement => (
260
+ const renderRaw = (
261
+ currentKind: FirstRunStepKind,
262
+ body: React.ReactNode,
263
+ bodyOwnsTimeline = false,
264
+ ): React.ReactElement => (
261
265
  <Box flexDirection="column" padding={1}>
262
- <Splash />
263
- <Box marginTop={1} marginBottom={1}>
264
- <FirstRunTimeline current={firstRunStageNumber(currentKind)} />
265
- </Box>
266
+ <Splash showTagline />
267
+ {bodyOwnsTimeline ? null : (
268
+ <Box marginTop={1} marginBottom={1}>
269
+ <FirstRunTimeline current={firstRunStageNumber(currentKind)} />
270
+ </Box>
271
+ )}
266
272
  {body}
267
273
  </Box>
268
274
  )
@@ -306,7 +312,7 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
306
312
  setStep({ kind: 'identity-start-saving', spec: step.spec, result })
307
313
  }}
308
314
  />
309
- ))
315
+ ), true)
310
316
  }
311
317
 
312
318
  if (step.kind === 'identity-start-saving') {
@@ -998,10 +998,6 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
998
998
  void appendHistory(value)
999
999
 
1000
1000
  if (streaming || pullInFlight || compactionUiRef.current) {
1001
- if (parseSlash(value)) {
1002
- pushNote('Slash commands cannot be queued. Wait for the current task to finish.', 'dim')
1003
- return
1004
- }
1005
1001
  setQueuedInputs(prev => [...prev, value])
1006
1002
  return
1007
1003
  }
@@ -1538,6 +1534,22 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
1538
1534
  setQueuedInputs(prev => prev.slice(1))
1539
1535
  void (async () => {
1540
1536
  if (!next) return
1537
+ if (parseSlash(next)) {
1538
+ const ctx = buildSlashContext()
1539
+ const result = await dispatchSlash(next, ctx)
1540
+ if (result && result.kind === 'note') {
1541
+ pushNote(result.text, result.variant ?? 'info')
1542
+ }
1543
+ if (result && result.kind === 'submit') {
1544
+ const projected = projectedUsageForInput(result.text)
1545
+ if (shouldConfirmContextUsage(projected, CONTEXT_CONFIRM_PERCENT)) {
1546
+ showContextLimitForPrompt(result.text)
1547
+ return
1548
+ }
1549
+ await runStream(result.text)
1550
+ }
1551
+ return
1552
+ }
1541
1553
  const projected = projectedUsageForInput(next)
1542
1554
  if (shouldConfirmContextUsage(projected, CONTEXT_CONFIRM_PERCENT)) {
1543
1555
  showContextLimitForPrompt(next)
@@ -18,7 +18,7 @@ import type { ContextUsage } from '../runtime/compaction.js'
18
18
  import { formatModelDisplayName } from '../models/modelDisplay.js'
19
19
  import { providerDisplayName } from '../models/modelPickerOptions.js'
20
20
  import type { McpManager } from '../mcp/manager.js'
21
- import { runHuggingFace, runIdentity, runMcp } from './slashCommandHandlers.js'
21
+ import { runIdentity, runMcp } from './slashCommandHandlers.js'
22
22
  import { renderStatus } from './slashCommandViews.js'
23
23
 
24
24
  export type IdentityRequestAction =
@@ -112,7 +112,7 @@ const COMMANDS: CommandSpec[] = [
112
112
  },
113
113
  {
114
114
  name: 'status',
115
- summary: 'provider, model, session id, turns, context, elapsed',
115
+ summary: 'provider, model, session id, turns, context, state, cwd, elapsed',
116
116
  run: (_args, ctx) => ({ kind: 'note', text: renderStatus(ctx) }),
117
117
  },
118
118
  {
@@ -213,12 +213,6 @@ const COMMANDS: CommandSpec[] = [
213
213
  return { kind: 'note', text: `Now using ${providerDisplayName(next.provider)} · ${formatModelDisplayName(next.provider, name, { maxLength: 64 })}.` }
214
214
  },
215
215
  },
216
- {
217
- name: 'hf',
218
- enterBehavior: 'fill',
219
- summary: 'local model files · /hf [installed|download <link>]',
220
- run: async (args, ctx) => runHuggingFace(args, ctx),
221
- },
222
216
  {
223
217
  name: 'resume',
224
218
  summary: 'reopen a prior session',
@@ -271,7 +265,7 @@ const COMMANDS: CommandSpec[] = [
271
265
  },
272
266
  {
273
267
  name: 'copy',
274
- summary: 'copy an assistant reply to the clipboard · /copy [n]',
268
+ summary: 'copy an assistant reply to the clipboard · /copy [n] (1 = latest)',
275
269
  run: async (args, ctx) => {
276
270
  const assistant = ctx.assistantTurns()
277
271
  if (assistant.length === 0) {
@@ -334,7 +328,7 @@ const COMMANDS: CommandSpec[] = [
334
328
  {
335
329
  name: 'identity',
336
330
  enterBehavior: 'fill',
337
- summary: 'Ethereum identity · /identity [status|create|load|remove]',
331
+ summary: 'agent identity · /identity [status|create|load|remove confirm]',
338
332
  run: async (args, ctx) => runIdentity(args, ctx),
339
333
  },
340
334
  {
@@ -1,48 +1,5 @@
1
1
  import { clearIdentity, getIdentityStatus } from '../storage/identity.js'
2
- import { formatModelDisplayName } from '../models/modelDisplay.js'
3
- import { loadLocalHfModels } from '../models/huggingface.js'
4
2
  import type { SlashContext, SlashResult } from './commands.js'
5
- import { formatBytes } from './slashCommandViews.js'
6
-
7
- export async function runHuggingFace(args: string, ctx: SlashContext): Promise<SlashResult> {
8
- const tokens = args.trim().split(/\s+/).filter(Boolean)
9
- const sub = tokens[0]?.toLowerCase() ?? ''
10
-
11
- if (!sub || sub === 'installed') {
12
- const installed = await loadLocalHfModels()
13
- if (installed.length === 0) {
14
- return {
15
- kind: 'note',
16
- variant: 'dim',
17
- text: 'No local model files downloaded. Press Alt+P and choose "Add Local Model File".',
18
- }
19
- }
20
- const lines = installed.map(model => {
21
- const marker = model.id === ctx.config.model && ctx.config.provider === 'llamacpp' ? '*' : ' '
22
- const displayName = formatModelDisplayName('llamacpp', model.id, { displayName: model.displayName, maxLength: 64 })
23
- return `${marker} ${displayName} ${formatBytes(model.sizeBytes)} ${model.risk}`
24
- })
25
- return { kind: 'note', text: ['installed Hugging Face models:', ...lines].join('\n') }
26
- }
27
-
28
- if (sub === 'download' || sub === 'model') {
29
- const link = tokens.slice(1).join(' ')
30
- ctx.onModelPickerRequest()
31
- return {
32
- kind: 'note',
33
- variant: 'dim',
34
- text: link
35
- ? `Alt+P opened. Choose "Add Local Model File" and paste: ${link}`
36
- : 'Alt+P opened. Choose "Add Local Model File" and paste the model URL or repo ID.',
37
- }
38
- }
39
-
40
- return {
41
- kind: 'note',
42
- variant: 'error',
43
- text: 'usage: /hf [installed|download <huggingface.co link or repo id>]',
44
- }
45
- }
46
3
 
47
4
  export async function runMcp(args: string, ctx: SlashContext): Promise<SlashResult> {
48
5
  if (!ctx.mcp) {
@@ -117,11 +74,11 @@ export async function runIdentity(args: string, ctx: SlashContext): Promise<Slas
117
74
  }
118
75
  const lines = [
119
76
  `address ${status.address}`,
120
- `created ${status.createdAt}`,
121
- `backend ${status.backend}`,
77
+ `updated ${status.createdAt}`,
78
+ `wallet ${status.backend}`,
122
79
  ]
123
- if (status.source) lines.push(`source ${status.source}`)
124
- if (status.agentId) lines.push(`token #${status.agentId}`)
80
+ if (status.source) lines.push(`registry ${status.source}`)
81
+ if (status.agentId) lines.push(`agent #${status.agentId}`)
125
82
  return { kind: 'note', text: lines.join('\n') }
126
83
  }
127
84
 
@@ -281,7 +281,6 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
281
281
  onWalletReady={setWalletSession}
282
282
  onTriggerRebackup={triggerRebackup}
283
283
  onTriggerPublicProfileSave={triggerPublicProfileSave}
284
- onWithdrawTokenForEns={currentStep => custodyFlow.beginWithdrawToken(currentStep, currentStep, 'ens')}
285
284
  />
286
285
  )
287
286
  }
@@ -83,7 +83,7 @@ export const IdentityHubRoutes: React.FC<{ controller: IdentityHubController }>
83
83
  <Select<'ens' | 'skip'>
84
84
  options={[
85
85
  { value: 'ens', role: 'section', label: 'Set Up Now' },
86
- { value: 'ens', label: 'Set Up ENS Name', hint: 'Walks you through Root, Name, Review, and Apply' },
86
+ { value: 'ens', label: 'Set Up ENS Name', hint: 'Root Name Review Apply' },
87
87
  { value: 'skip', role: 'section', label: 'Skip' },
88
88
  { value: 'skip', label: 'Skip For Now', hint: 'Continue to model setup; add ENS later', role: 'utility' },
89
89
  ]}
@@ -46,7 +46,6 @@ type AdvancedScreenProps = {
46
46
  runAdvancedSubdomainCheck: (rootName: string, label: string) => void
47
47
  onEnsSetup: EnsEditProps['onEnsSetup']
48
48
  onEnsLink: EnsEditProps['onEnsLink']
49
- onWithdrawToken: () => void
50
49
  }
51
50
 
52
51
  export function renderAdvancedEnsPhase({
@@ -65,82 +64,7 @@ export function renderAdvancedEnsPhase({
65
64
  runAdvancedSubdomainCheck,
66
65
  onEnsSetup,
67
66
  onEnsLink,
68
- onWithdrawToken,
69
67
  }: AdvancedScreenProps): React.ReactNode | null {
70
- if (phase.kind === 'advanced-transfer-check') {
71
- type TransferCheckAction = 'continue' | 'withdraw' | 'back'
72
- const custody = reconciliation.custody
73
- const tokenInVault = custody === 'advanced' || custody === 'mid-flow-uri-pending'
74
- const tokenInOwnerWallet = custody === 'simple' || custody === 'withdrawn'
75
- const probePending = custody === 'unknown' && reconciliation.rpc !== 'failing'
76
- const probeFailed = reconciliation.rpc === 'failing'
77
-
78
- const options: Array<{ value: TransferCheckAction; role?: 'section' | 'utility'; label: string; hint?: string }> = []
79
- options.push({ value: 'continue', role: 'section', label: 'Setup' })
80
- if (tokenInOwnerWallet) {
81
- options.push({
82
- value: 'continue',
83
- label: 'Continue ENS Setup',
84
- hint: 'Owner wallet holds this token onchain.',
85
- })
86
- } else if (tokenInVault) {
87
- options.push({
88
- value: 'withdraw',
89
- label: 'Withdraw Token',
90
- hint: 'Pull token out to sign ENS records. Redeposit to the Vault any time after.',
91
- })
92
- } else if (probePending) {
93
- options.push({
94
- value: 'continue',
95
- label: 'Checking onchain state…',
96
- hint: 'Try again in a moment.',
97
- })
98
- } else if (probeFailed) {
99
- options.push({
100
- value: 'continue',
101
- label: 'Onchain check unavailable',
102
- hint: 'RPC unreachable. Resolve connectivity, then retry.',
103
- })
104
- } else {
105
- options.push({
106
- value: 'continue',
107
- label: 'Token Owner Unknown',
108
- hint: 'Try again in a moment.',
109
- })
110
- }
111
- options.push({ value: 'back', role: 'section', label: 'Navigation' })
112
- options.push({ value: 'back', label: 'Back', hint: 'Return to setup type', role: 'utility' })
113
-
114
- return (
115
- <Surface
116
- title="Token Custody Check"
117
- subtitle="ENS setup continues only after the owner wallet holds this token onchain."
118
- footer={footerHint('enter select · esc back')}
119
- >
120
- <Box marginTop={1}>
121
- <Select<TransferCheckAction>
122
- options={options}
123
- hintLayout="inline"
124
- onSubmit={choice => {
125
- if (choice === 'withdraw') {
126
- onWithdrawToken()
127
- return
128
- }
129
- if (choice === 'continue' && tokenInOwnerWallet) {
130
- runDiscovery('advanced')
131
- return
132
- }
133
- if (choice === 'back') {
134
- return setPhase({ kind: 'mode-select' })
135
- }
136
- }}
137
- onCancel={() => setPhase({ kind: 'mode-select' })}
138
- />
139
- </Box>
140
- </Surface>
141
- )
142
- }
143
-
144
68
  if (phase.kind === 'advanced-root-check') {
145
69
  return (
146
70
  <Surface
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  import { getAddress, type Address } from 'viem'
3
3
  import type { BrowserWalletReady } from '../../wallet/browserWallet.js'
4
4
  import {
5
+ AGENT_TOKEN_RECORD_KEY,
5
6
  buildAgentEnsRecords,
6
7
  buildEnsip25Key,
7
8
  diffRecords,
@@ -19,6 +20,7 @@ import {
19
20
  preflightEnsRoot,
20
21
  preflightEnsSetup,
21
22
  } from '../../ens/ensAutomation.js'
23
+ import { SUPPORTED_ERC8004_CHAINS } from '../../registry/erc8004.js'
22
24
  import {
23
25
  readCustodyMode,
24
26
  readIdentityStateString,
@@ -50,7 +52,6 @@ export const EnsEditFlow: React.FC<EnsEditProps> = ({
50
52
  onEnsRecordsUpdate,
51
53
  onEnsSetup,
52
54
  onManageOperatorWalletAccess,
53
- onWithdrawToken,
54
55
  initialView,
55
56
  onBack,
56
57
  }) => {
@@ -68,7 +69,7 @@ export const EnsEditFlow: React.FC<EnsEditProps> = ({
68
69
 
69
70
  const [discovery, setDiscovery] = React.useState<DiscoveryState>({ status: 'idle' })
70
71
  const [phase, setPhase] = React.useState<EnsPhase>(() => {
71
- if (initialView === 'advanced' && !hasAdvancedSetup) return { kind: 'advanced-transfer-check' }
72
+ if (initialView === 'advanced' && !hasAdvancedSetup) return { kind: 'pick-parent', mode: 'advanced' }
72
73
  return { kind: 'mode-select' }
73
74
  })
74
75
  const [validationError, setValidationError] = React.useState<string | null>(null)
@@ -138,11 +139,14 @@ export const EnsEditFlow: React.FC<EnsEditProps> = ({
138
139
  try {
139
140
  const validation = await validateAgentEnsLink(fullName, ownerAddress)
140
141
  const readKeys = identity.agentId
141
- ? [buildEnsip25Key({
142
- chainId: registry.chainId,
143
- identityRegistryAddress: registry.identityRegistryAddress,
144
- agentId: identity.agentId,
145
- })]
142
+ ? [
143
+ ...SUPPORTED_ERC8004_CHAINS.map(chain => buildEnsip25Key({
144
+ chainId: chain.chainId,
145
+ identityRegistryAddress: registry.identityRegistryAddress,
146
+ agentId: identity.agentId!,
147
+ })),
148
+ AGENT_TOKEN_RECORD_KEY,
149
+ ]
146
150
  : []
147
151
  const currentText = validation.ok && readKeys.length > 0
148
152
  ? await readEthagentTextRecords(fullName, readKeys)
@@ -260,11 +264,14 @@ export const EnsEditFlow: React.FC<EnsEditProps> = ({
260
264
  setValidationError(null)
261
265
  setPhase({ kind: 'unlink-loading', fullName })
262
266
  const readKeys = identity.agentId
263
- ? [buildEnsip25Key({
264
- chainId: registry.chainId,
265
- identityRegistryAddress: registry.identityRegistryAddress,
266
- agentId: identity.agentId,
267
- })]
267
+ ? [
268
+ ...SUPPORTED_ERC8004_CHAINS.map(chain => buildEnsip25Key({
269
+ chainId: chain.chainId,
270
+ identityRegistryAddress: registry.identityRegistryAddress,
271
+ agentId: identity.agentId!,
272
+ })),
273
+ AGENT_TOKEN_RECORD_KEY,
274
+ ]
268
275
  : []
269
276
  readEthagentTextRecords(fullName, readKeys)
270
277
  .then(currentText => {
@@ -321,7 +328,6 @@ export const EnsEditFlow: React.FC<EnsEditProps> = ({
321
328
  runAdvancedSubdomainCheck,
322
329
  onEnsSetup,
323
330
  onEnsLink,
324
- onWithdrawToken,
325
331
  })
326
332
  if (advancedScreen) return advancedScreen
327
333
 
@@ -82,11 +82,11 @@ export function renderEnsMaintenancePhase({
82
82
  const linkHint = multiNeedsCustodySetup
83
83
  ? 'Set Advanced custody first via Custody Mode'
84
84
  : isAdvanced
85
- ? 'Walks you through Root, Name, Review, and Apply'
86
- : 'Walks you through Root, Name, Review, and Apply'
85
+ ? 'Root Name Review Apply'
86
+ : 'Root Name Review Apply'
87
87
  const options: Array<{ value: EnsAction; role?: 'section' | 'utility'; label: string; hint?: string; disabled?: boolean }> = []
88
88
  if (currentEnsName) {
89
- options.push({ value: 'unlink', label: 'Unlink Name', hint: 'Removes this name from the token. Set up a different name afterward by linking again.' })
89
+ options.push({ value: 'unlink', label: 'Unlink Name', hint: 'Removes the name from the token. Link a different one anytime.' })
90
90
  } else {
91
91
  options.push({
92
92
  value: 'link',
@@ -116,10 +116,6 @@ export function renderEnsMaintenancePhase({
116
116
  }
117
117
  if (choice === 'link') {
118
118
  if (multiNeedsCustodySetup) return
119
- if (isAdvanced && savedOwnerAddress) {
120
- setPhase({ kind: 'advanced-transfer-check' })
121
- return
122
- }
123
119
  runDiscovery()
124
120
  return
125
121
  }
@@ -21,9 +21,9 @@ import {
21
21
  import { ensValidationReasonText } from './state.js'
22
22
  import { shortAddress } from '../shared/model/format.js'
23
23
  import {
24
+ abbreviateHexBlobs,
24
25
  manualReasonTitle,
25
26
  modeSwitchHeading,
26
- setupSwitchNotice,
27
27
  } from './editCopy.js'
28
28
  import {
29
29
  EnsSetupRow,
@@ -112,7 +112,6 @@ export const EnsSetupReviewScreen: React.FC<EnsSetupReviewScreenProps> = ({
112
112
  type Action = 'begin' | 'back'
113
113
  const isSimple = setup.mode === 'simple'
114
114
  const signerLabel = isSimple ? 'Connected wallet' : 'Owner wallet'
115
- const switchNotice = setupSwitchNotice(currentEnsName, currentMode, setup.fullName, setup.mode)
116
115
  const createLabel = setup.registryAction === 'create-subdomain'
117
116
  ? 'Create Subdomain'
118
117
  : setup.registryAction === 'create-wrapped-subdomain'
@@ -138,7 +137,6 @@ export const EnsSetupReviewScreen: React.FC<EnsSetupReviewScreenProps> = ({
138
137
  ) : null}
139
138
  <Box flexDirection="column">
140
139
  <Text color={theme.dim}>{modeSwitchHeading(currentEnsName, currentMode, setup.fullName, setup.mode)}</Text>
141
- {switchNotice ? <Text color={theme.dim}>{switchNotice}</Text> : null}
142
140
  <EnsSetupRow label="ENS name" value={setup.fullName} />
143
141
  <EnsSetupRow label="Parent root" value={setup.rootName} />
144
142
  <EnsSetupRow label="Subdomain label" value={setup.label} />
@@ -270,8 +268,8 @@ export const UnlinkEnsReviewScreen: React.FC<UnlinkEnsReviewScreenProps> = ({
270
268
  <Text color={theme.textSubtle}>Will be cleared:</Text>
271
269
  {changedDiffs.map(diff => (
272
270
  <Text key={diff.key}>
273
- <Text color={theme.dim}>{` ${diff.key} `}</Text>
274
- <Text color={theme.accentPeriwinkle}>{diff.current}</Text>
271
+ <Text color={theme.dim}>{` ${abbreviateHexBlobs(diff.key)} `}</Text>
272
+ <Text color={theme.accentPeriwinkle}>{abbreviateHexBlobs(diff.current)}</Text>
275
273
  </Text>
276
274
  ))}
277
275
  </Box>
@@ -342,7 +340,6 @@ export const ReviewScreen: React.FC<ReviewScreenProps> = ({
342
340
  const changedDiffs = recordsDiff.filter(d => d.changed)
343
341
  const hasRecordChanges = changedDiffs.length > 0
344
342
  const reviewSubtitle = 'Review Ethereum Mainnet ENS address and text records before saving this ENS link. No token approval is requested.'
345
- const switchNotice = setupSwitchNotice(currentEnsName, currentMode, fullName, mode)
346
343
 
347
344
  if (!validation.ok) {
348
345
  const reason = ensValidationReasonText(validation.reason)
@@ -396,7 +393,7 @@ export const ReviewScreen: React.FC<ReviewScreenProps> = ({
396
393
 
397
394
  const options: Array<SelectOption<ReviewAction>> = [
398
395
  { value: 'continue', role: 'section', label: modeSwitchHeading(currentEnsName, currentMode, fullName, mode) },
399
- { value: 'continue', label: 'Continue Setup', hint: hasRecordChanges ? `Ethereum Mainnet: sign ${changedDiffs.length} ENS record value${changedDiffs.length === 1 ? '' : 's'}; ${registryNetworkLabel}: save token URI` : `ENS records already match; ${registryNetworkLabel}: save token URI` },
396
+ { value: 'continue', label: 'Continue Setup', hint: hasRecordChanges ? `Ethereum Mainnet: sign ${changedDiffs.length} record${changedDiffs.length === 1 ? '' : 's'}; ${registryNetworkLabel}: save token URI` : `Records match; ${registryNetworkLabel}: save token URI` },
400
397
  { value: 'change', label: 'Pick A Different Name', hint: 'Return to the name picker' },
401
398
  { value: 'back', role: 'section', label: 'Navigation' },
402
399
  { value: 'back', label: 'Back', hint: 'Return to Identity Hub', role: 'utility' },
@@ -414,13 +411,12 @@ export const ReviewScreen: React.FC<ReviewScreenProps> = ({
414
411
  <Box marginBottom={1} flexDirection="column">
415
412
  <Text color={theme.dim}>Current: <Text color={currentEnsName ? theme.text : theme.dim}>{currentEnsName || 'None'}</Text></Text>
416
413
  <Text color={theme.dim}>Next: <Text color={theme.text}>{fullName}</Text></Text>
417
- {switchNotice ? <Text color={theme.dim}>{switchNotice}</Text> : null}
418
414
  </Box>
419
415
  )
420
416
  : null}
421
417
  {recordsDiff.map(diff => (
422
418
  <Text key={diff.key}>
423
- <Text color={theme.dim}>{`- ${diff.key}: `}</Text>
419
+ <Text color={theme.dim}>{`- ${abbreviateHexBlobs(diff.key)}: `}</Text>
424
420
  {diff.changed
425
421
  ? (
426
422
  <>
@@ -20,9 +20,11 @@ import type { EnsEditProps } from './types.js'
20
20
 
21
21
  export const footerHint = (hint: string) => <Text color={theme.dim}>{hint}</Text>
22
22
 
23
+ import { abbreviateHexBlobs } from './editCopy.js'
24
+
23
25
  export const renderRecordValue = (value: string) =>
24
26
  value
25
- ? <Text color={theme.accentPeriwinkle}>{value}</Text>
27
+ ? <Text color={theme.accentPeriwinkle}>{abbreviateHexBlobs(value)}</Text>
26
28
  : <Text color={theme.dim}>Unset</Text>
27
29
 
28
30
  export function rootErrorMessage(
@@ -135,7 +135,7 @@ export function renderSimpleEnsPhase({
135
135
  ]
136
136
  : []),
137
137
  { value: 'open-ens-domains' as DomainAction, role: 'section' as const, label: 'No Parent Name?' },
138
- { value: 'open-ens-domains' as DomainAction, label: 'Register .eth Name', hint: 'Open the ENS app in your browser; come back when this wallet owns one' },
138
+ { value: 'open-ens-domains' as DomainAction, label: 'Register .eth Name', hint: 'Opens ENS app; return once this wallet owns one' },
139
139
  ...(noOwnedNames || discovery.status === 'ok'
140
140
  ? [{ value: 'retry' as DomainAction, label: 'Scan Again', hint: 'Re-run root .eth name discovery for this wallet' }]
141
141
  : []),
@@ -356,7 +356,7 @@ export function renderSimpleEnsPhase({
356
356
  onEnsLink(phase.fullName, linkOptions)
357
357
  }}
358
358
  onCheckAgain={() => { void runValidation(phase.fullName, phase.mode, phase.ownerAddress, phase.operatorWallet) }}
359
- onChange={() => setPhase(phase.mode === 'advanced' ? { kind: 'advanced-transfer-check' } : { kind: 'pick-parent' })}
359
+ onChange={() => setPhase({ kind: 'pick-parent', mode: phase.mode })}
360
360
  onCreate={phase.mode === 'simple' && !phase.validation.ok && phase.validation.reason === 'no-owner'
361
361
  ? () => runSimpleCreatePreflight(phase.fullName)
362
362
  : undefined}
@@ -34,7 +34,6 @@ type EnsFlowProps = {
34
34
  onWalletReady: (session: BrowserWalletReady | null) => void
35
35
  onTriggerRebackup: (backStep: Step, profileUpdates?: ProfileUpdates) => void
36
36
  onTriggerPublicProfileSave: (backStep: Step, profileUpdates: ProfileUpdates) => void
37
- onWithdrawTokenForEns: (step: Step) => void
38
37
  }
39
38
 
40
39
  export function isEnsStep(step: Step): step is IdentityHubEnsStep {
@@ -59,7 +58,6 @@ export const EnsFlow: React.FC<EnsFlowProps> = ({
59
58
  onWalletReady,
60
59
  onTriggerRebackup,
61
60
  onTriggerPublicProfileSave,
62
- onWithdrawTokenForEns,
63
61
  }) => {
64
62
  if (step.kind === 'manage-ens-operators') {
65
63
  return (
@@ -81,7 +79,6 @@ export const EnsFlow: React.FC<EnsFlowProps> = ({
81
79
  <EditProfileFlow
82
80
  step={step}
83
81
  reconciliation={reconciliation}
84
- onWithdrawToken={() => onWithdrawTokenForEns(step)}
85
82
  onNameSubmit={name => {
86
83
  if (step.kind !== 'edit-profile-name') return
87
84
  onSetStep({
@@ -3,6 +3,12 @@ import type { AgentEnsRecords, AgentRecordDiff } from '../../ens/agentRecords.js
3
3
  import type { EnsRegistryAction, EnsSetupBlockedPlan } from '../../ens/ensAutomation.js'
4
4
  import type { CustodyMode } from '../custody/state.js'
5
5
 
6
+ export function abbreviateHexBlobs(input: string): string {
7
+ return input.replace(/0x([0-9a-fA-F]{20,})/g, (_match, hex) => {
8
+ return `0x${hex.slice(0, 8)}...${hex.slice(-8)}`
9
+ })
10
+ }
11
+
6
12
  export type EnsLinkOptions = {
7
13
  mode: 'simple' | 'advanced'
8
14
  ownerAddress?: Address
@@ -46,18 +52,6 @@ export function modeSwitchHeading(
46
52
  return 'Automation'
47
53
  }
48
54
 
49
- export function setupSwitchNotice(
50
- currentEnsName: string,
51
- currentMode: CustodyMode | undefined,
52
- nextEnsName: string,
53
- nextMode: 'simple' | 'advanced',
54
- ): string | null {
55
- if (!currentEnsName && !currentMode) return null
56
- const currentTopology = currentMode === 'advanced' ? 'advanced' : currentMode === 'simple' ? 'simple' : undefined
57
- if (currentEnsName === nextEnsName && currentTopology === nextMode) return null
58
- return 'This replaces the saved ENS setup directly. Reset is only for clearing the current link.'
59
- }
60
-
61
55
  export function advancedSubdomainStatusText(action: EnsRegistryAction): string {
62
56
  switch (action) {
63
57
  case 'create-subdomain':
@@ -31,7 +31,6 @@ export type SimpleEnsPhase =
31
31
  | { kind: 'review'; fullName: string; validation: EnsValidation; recordsDiff: AgentRecordDiff[]; currentRecords: AgentEnsRecordState; nextRecords: AgentEnsRecords; mode: 'simple' | 'advanced'; ownerAddress?: Address; operatorWallet?: Address }
32
32
 
33
33
  export type AdvancedEnsPhase =
34
- | { kind: 'advanced-transfer-check' }
35
34
  | { kind: 'advanced-root-check'; rootName: string }
36
35
  | { kind: 'advanced-subdomain'; rootName: string; label?: string; error?: string }
37
36
  | { kind: 'advanced-subdomain-check'; rootName: string; label: string }
@@ -71,7 +70,6 @@ export type EnsEditProps = {
71
70
  onEnsRecordsUpdate: (fullName: string, records: AgentEnsRecords, options: EnsLinkOptions, clearRecords?: boolean, currentRecords?: AgentEnsRecordState) => void
72
71
  onEnsSetup: (setup: EnsSetupPlan) => void
73
72
  onManageOperatorWalletAccess: () => void
74
- onWithdrawToken: () => void
75
73
  initialView?: 'advanced'
76
74
  onBack: () => void
77
75
  }
@@ -26,7 +26,6 @@ type EditProfileFlowProps = {
26
26
  onEnsRecordsUpdate: (fullName: string, records: AgentEnsRecords, options: EnsLinkOptions, clearRecords?: boolean, currentRecords?: AgentEnsRecordState) => void
27
27
  onEnsSetup: (setup: EnsSetupPlan) => void
28
28
  onManageOperatorWalletAccess: () => void
29
- onWithdrawToken: () => void
30
29
  onBack: () => void
31
30
  onMenu: () => void
32
31
  }
@@ -49,7 +48,6 @@ export const EditProfileFlow: React.FC<EditProfileFlowProps> = ({
49
48
  onEnsRecordsUpdate,
50
49
  onEnsSetup,
51
50
  onManageOperatorWalletAccess,
52
- onWithdrawToken,
53
51
  onBack,
54
52
  onMenu,
55
53
  }) => {
@@ -101,7 +99,6 @@ export const EditProfileFlow: React.FC<EditProfileFlowProps> = ({
101
99
  onEnsRecordsUpdate={onEnsRecordsUpdate}
102
100
  onEnsSetup={onEnsSetup}
103
101
  onManageOperatorWalletAccess={onManageOperatorWalletAccess}
104
- onWithdrawToken={onWithdrawToken}
105
102
  initialView={step.initialView}
106
103
  onBack={onBack}
107
104
  />
@@ -3,6 +3,7 @@ import { Box, Text } from 'ink'
3
3
  import { Surface } from '../../../../ui/Surface.js'
4
4
  import { Select, type SelectOption } from '../../../../ui/Select.js'
5
5
  import { theme } from '../../../../ui/theme.js'
6
+ import { FirstRunTimeline } from '../../../../app/FirstRunTimeline.js'
6
7
  import type { EthagentConfig, EthagentIdentity } from '../../../../storage/config.js'
7
8
  import type { ContinuityWorkingTreeStatus } from '../../../continuity/storage.js'
8
9
  import { identityPerspective, readCustodyMode } from '../../custody/state.js'
@@ -75,9 +76,10 @@ export const MenuScreen: React.FC<MenuScreenProps> = ({
75
76
  onSkip,
76
77
  onCancel,
77
78
  }) => {
78
- const title = mode === 'first-run' ? 'Set Up Agent Identity' : 'Identity Hub'
79
- const subtitle = mode === 'first-run'
80
- ? 'Create a portable agent or load one you already own.'
79
+ const isFirstRun = mode === 'first-run'
80
+ const title = isFirstRun ? 'Set Up Agent Identity' : 'Identity Hub'
81
+ const subtitle: React.ReactNode = isFirstRun
82
+ ? <FirstRunTimeline current={2} />
81
83
  : 'Manage agent identity, custody, encrypted continuity, and recovery.'
82
84
 
83
85
  const canRefetch = Boolean(canRebackup && identity?.backup?.cid)
@@ -53,9 +53,10 @@ type SplashProps = {
53
53
  tipLine?: string
54
54
  updateNotice?: string | null
55
55
  compact?: boolean
56
+ showTagline?: boolean
56
57
  }
57
58
 
58
- export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updateNotice, compact }) => {
59
+ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updateNotice, compact, showTagline }) => {
59
60
  const [width, setWidth] = useState<number>(() => process.stdout.columns ?? 80)
60
61
 
61
62
  useEffect(() => {
@@ -74,7 +75,6 @@ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updat
74
75
  <Box flexDirection="column" alignSelf="flex-start" padding={1}>
75
76
  <Eyes />
76
77
  <Text bold color={theme.accentWhite}>ethagent</Text>
77
- <Text color={theme.dim}>privacy-first AI agent with a portable <Text color={theme.accentPeriwinkle}>Ethereum</Text> identity</Text>
78
78
  {contextLine ? <Text color={theme.dim}>{contextLine}</Text> : null}
79
79
  {tipLine ? <Text color={theme.dim}>{tipLine}</Text> : null}
80
80
  {updateNotice ? <Text color={theme.accentPeriwinkle}>{updateNotice}</Text> : null}
@@ -85,34 +85,36 @@ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updat
85
85
  const w = 69
86
86
  const logoLines = glyphs.ethagent.split('\n').map(line => line.padEnd(w, ' '))
87
87
 
88
- const topPad = Math.max(0, w - glyphs.tagline.length - 1)
89
-
90
88
  const bottomInline = contextLine ? ` ${truncateToFit(contextLine, w - 4)} ` : ''
91
89
  const bottomPad = Math.max(0, w - bottomInline.length - 1)
92
90
 
93
91
  return (
94
92
  <Box flexDirection="column" alignSelf="flex-start" padding={1}>
95
93
  <Eyes />
96
- <Text>
97
- <Text color={theme.border}>{glyphs.frame.topLeft}</Text>
98
- <Text color={theme.dim}>{' privacy-first AI agent with a portable '}<Text color={theme.accentPeriwinkle}>Ethereum</Text>{' identity '}</Text>
99
- <Text color={theme.border}>{glyphs.frame.horizontal.repeat(topPad)}{glyphs.frame.topRight}</Text>
100
- </Text>
94
+ {showTagline ? (
95
+ <Text>
96
+ <Text color={theme.accentWhite}>{glyphs.frame.topLeft}</Text>
97
+ <Text color={theme.accentPeriwinkle}>{glyphs.tagline}</Text>
98
+ <Text color={theme.accentWhite}>{glyphs.frame.horizontal.repeat(Math.max(0, w - glyphs.tagline.length - 1))}{glyphs.frame.topRight}</Text>
99
+ </Text>
100
+ ) : (
101
+ <Text color={theme.accentWhite}>{glyphs.frame.topLeft.slice(0, 1) + glyphs.frame.horizontal.repeat(w) + glyphs.frame.topRight}</Text>
102
+ )}
101
103
  {logoLines.map((line, i) => (
102
104
  <Box key={i}>
103
- <Text color={theme.border}>{glyphs.frame.side}</Text>
104
- <Text color={theme.border}>{line}</Text>
105
- <Text color={theme.border}>{glyphs.frame.side}</Text>
105
+ <Text color={theme.accentWhite}>{glyphs.frame.side}</Text>
106
+ <Text color={theme.accentWhite}>{line}</Text>
107
+ <Text color={theme.accentWhite}>{glyphs.frame.side}</Text>
106
108
  </Box>
107
109
  ))}
108
110
  {bottomInline ? (
109
111
  <Text>
110
- <Text color={theme.border}>{glyphs.frame.bottomLeft}</Text>
112
+ <Text color={theme.accentWhite}>{glyphs.frame.bottomLeft}</Text>
111
113
  <Text color={theme.accentPeriwinkle}>{bottomInline}</Text>
112
- <Text color={theme.border}>{glyphs.frame.horizontal.repeat(bottomPad)}{glyphs.frame.bottomRight}</Text>
114
+ <Text color={theme.accentWhite}>{glyphs.frame.horizontal.repeat(bottomPad)}{glyphs.frame.bottomRight}</Text>
113
115
  </Text>
114
116
  ) : (
115
- <Text color={theme.border}>{glyphs.frame.bottomLeft.slice(0, 1) + glyphs.frame.horizontal.repeat(w) + glyphs.frame.bottomRight}</Text>
117
+ <Text color={theme.accentWhite}>{glyphs.frame.bottomLeft.slice(0, 1) + glyphs.frame.horizontal.repeat(w) + glyphs.frame.bottomRight}</Text>
116
118
  )}
117
119
  {tipLine || updateNotice ? (
118
120
  <Box marginTop={1} flexDirection="column">
package/src/ui/theme.ts CHANGED
@@ -31,7 +31,7 @@ export const theme = {
31
31
  codeTag: '#ffb3b3',
32
32
  codeAttribute: '#f2d087',
33
33
  border: '#555555',
34
- dim: '#777777',
34
+ dim: '#909090',
35
35
  text: '#f1f1f1',
36
36
  textSubtle: '#9b9b9b',
37
37
  } as const