Skip to content

Commit 6393291

Browse files
katspaughclaude
andauthored
feat: add sequential operation prompts and unify UI styling (#5)
* feat: add sequential operation prompts and unify UI styling Implement seamless sequential operation flows: - account create → deploy - tx create → sign - tx sign → execute/push (with intelligent branching) UI improvements: - Remove picocolors from p.intro() calls for consistent styling - Unify Ink spinner appearance to match clack/prompts - Fix duplicate spinner/success messages - Remove picocolors from console.log statements for cleaner output - Fix MaxListenersExceededWarning by increasing limit to 20 Technical changes: - Show brief success messages before prompts (not after) - Only show full success screens if user declines next step - Smart next-step suggestions based on transaction signature status - All prompts use default 'true' to streamline common workflows 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: restore missing console.log message for ABI fetching --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5d6a825 commit 6393291

File tree

16 files changed

+185
-88
lines changed

16 files changed

+185
-88
lines changed

package-lock.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
"conf": "^13.0.1",
3838
"ink": "^6.3.1",
3939
"ink-spinner": "^5.0.0",
40-
"picocolors": "^1.1.1",
4140
"react": "^19.2.0",
4241
"viem": "^2.21.8",
4342
"zod": "^3.24.1"

src/commands/account/create.ts

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import * as p from '@clack/prompts'
2-
import pc from 'picocolors'
32
import { type Address } from 'viem'
43
import { getConfigStore } from '../../storage/config-store.js'
54
import { getSafeStorage } from '../../storage/safe-store.js'
65
import { getWalletStorage } from '../../storage/wallet-store.js'
76
import { SafeService } from '../../services/safe-service.js'
87
import { isValidAddress } from '../../utils/validation.js'
98
import { checksumAddress, shortenAddress } from '../../utils/ethereum.js'
9+
import { formatSafeAddress } from '../../utils/eip3770.js'
1010
import { logError } from '../../ui/messages.js'
1111
import { renderScreen } from '../../ui/render.js'
1212
import { AccountCreateSuccessScreen } from '../../ui/screens/index.js'
1313

1414
export async function createSafe() {
15-
p.intro(pc.bgCyan(pc.black(' Create Safe Account ')))
15+
p.intro('Create Safe Account')
1616

1717
const configStore = getConfigStore()
1818
const safeStorage = getSafeStorage()
@@ -27,7 +27,7 @@ export async function createSafe() {
2727
}
2828

2929
console.log('')
30-
console.log(pc.dim(`Active wallet: ${activeWallet.name} (${activeWallet.address})`))
30+
console.log(`Active wallet: ${activeWallet.name} (${activeWallet.address})`)
3131
console.log('')
3232

3333
// Select chain
@@ -62,7 +62,7 @@ export async function createSafe() {
6262

6363
if (includeActiveWallet) {
6464
owners.push(checksumAddress(activeWallet.address))
65-
console.log(pc.green(`✓ Added ${shortenAddress(activeWallet.address)}`))
65+
console.log(`✓ Added ${shortenAddress(activeWallet.address)}`)
6666
}
6767

6868
// Add more owners
@@ -102,7 +102,7 @@ export async function createSafe() {
102102

103103
const checksummed = checksumAddress(ownerAddress as string)
104104
owners.push(checksummed)
105-
console.log(pc.green(`✓ Added ${shortenAddress(checksummed)}`))
105+
console.log(`✓ Added ${shortenAddress(checksummed)}`)
106106
}
107107

108108
if (owners.length === 0) {
@@ -145,18 +145,16 @@ export async function createSafe() {
145145

146146
// Summary
147147
console.log('')
148-
console.log(pc.bold('📋 Safe Configuration Summary'))
148+
console.log('Safe Configuration Summary')
149149
console.log('')
150-
console.log(` ${pc.dim('Chain:')} ${chain.name} (${chain.chainId})`)
151-
console.log(` ${pc.dim('Version:')} 1.4.1`)
152-
console.log(` ${pc.dim('Owners:')} ${owners.length}`)
150+
console.log(` Chain: ${chain.name} (${chain.chainId})`)
151+
console.log(` Version: 1.4.1`)
152+
console.log(` Owners: ${owners.length}`)
153153
owners.forEach((owner, i) => {
154154
const isActive = owner.toLowerCase() === activeWallet.address.toLowerCase()
155-
console.log(
156-
` ${pc.dim(`${i + 1}.`)} ${shortenAddress(owner)}${isActive ? pc.green(' (you)') : ''}`
157-
)
155+
console.log(` ${i + 1}. ${shortenAddress(owner)}${isActive ? ' (you)' : ''}`)
158156
})
159-
console.log(` ${pc.dim('Threshold:')} ${thresholdNum} / ${owners.length}`)
157+
console.log(` Threshold: ${thresholdNum} / ${owners.length}`)
160158
console.log('')
161159

162160
const spinner = p.spinner()
@@ -203,7 +201,7 @@ export async function createSafe() {
203201
throw new Error(`Could not find available Safe address after ${maxAttempts} attempts`)
204202
}
205203

206-
spinner.stop('Safe created!')
204+
spinner.stop()
207205

208206
// Save to storage
209207
const safe = safeStorage.createSafe({
@@ -218,13 +216,37 @@ export async function createSafe() {
218216
},
219217
})
220218

221-
// Display success screen with Safe details and next steps
222-
await renderScreen(AccountCreateSuccessScreen, {
223-
name: safe.name,
224-
address: safe.address as Address,
225-
chainId: safe.chainId,
226-
chainName: chain.name,
219+
// Show brief success message
220+
const allChains = configStore.getAllChains()
221+
const eip3770 = formatSafeAddress(safe.address as Address, safe.chainId, allChains)
222+
console.log('')
223+
console.log('✓ Safe created successfully!')
224+
console.log('')
225+
console.log(` Name: ${safe.name}`)
226+
console.log(` Address: ${eip3770}`)
227+
console.log(` Chain: ${chain.name}`)
228+
console.log(` Status: Not deployed`)
229+
console.log('')
230+
231+
// Offer to deploy the Safe
232+
const shouldDeploy = await p.confirm({
233+
message: 'Would you like to deploy this Safe now?',
234+
initialValue: true,
227235
})
236+
237+
if (!p.isCancel(shouldDeploy) && shouldDeploy) {
238+
console.log('')
239+
const { deploySafe } = await import('./deploy.js')
240+
await deploySafe(eip3770)
241+
} else {
242+
// Show full success screen with next steps
243+
await renderScreen(AccountCreateSuccessScreen, {
244+
name: safe.name,
245+
address: safe.address as Address,
246+
chainId: safe.chainId,
247+
chainName: chain.name,
248+
})
249+
}
228250
} catch (error) {
229251
spinner.stop('Failed to create Safe')
230252
logError(error instanceof Error ? error.message : 'Unknown error')

src/commands/account/deploy.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as p from '@clack/prompts'
2-
import pc from 'picocolors'
32
import { type Address } from 'viem'
43
import { getConfigStore } from '../../storage/config-store.js'
54
import { getSafeStorage } from '../../storage/safe-store.js'
@@ -11,7 +10,7 @@ import { renderScreen } from '../../ui/render.js'
1110
import { AccountDeploySuccessScreen } from '../../ui/screens/index.js'
1211

1312
export async function deploySafe(account?: string) {
14-
p.intro(pc.bgCyan(pc.black(' Deploy Safe ')))
13+
p.intro('Deploy Safe')
1514

1615
const configStore = getConfigStore()
1716
const safeStorage = getSafeStorage()
@@ -100,7 +99,7 @@ export async function deploySafe(account?: string) {
10099
}
101100
} catch {
102101
// If we can't verify, log warning but continue
103-
console.log(pc.yellow('⚠ Warning: Could not verify on-chain deployment status'))
102+
console.log('⚠ Warning: Could not verify on-chain deployment status')
104103
}
105104

106105
// Get active wallet
@@ -114,15 +113,15 @@ export async function deploySafe(account?: string) {
114113
const eip3770 = formatSafeAddress(safe.address as Address, safe.chainId, chains)
115114

116115
console.log('')
117-
console.log(pc.bold('Safe to Deploy:'))
118-
console.log(` ${pc.dim('Name:')} ${safe.name}`)
119-
console.log(` ${pc.dim('Address:')} ${pc.cyan(eip3770)}`)
120-
console.log(` ${pc.dim('Chain:')} ${chain.name}`)
116+
console.log('Safe to Deploy:')
117+
console.log(` Name: ${safe.name}`)
118+
console.log(` Address: ${eip3770}`)
119+
console.log(` Chain: ${chain.name}`)
121120
console.log(
122-
` ${pc.dim('Owners:')} ${safe.predictedConfig.threshold} / ${safe.predictedConfig.owners.length}`
121+
` Owners: ${safe.predictedConfig.threshold} / ${safe.predictedConfig.owners.length}`
123122
)
124123
console.log('')
125-
console.log(pc.dim(`Deploying with wallet: ${activeWallet.name} (${activeWallet.address})`))
124+
console.log(`Deploying with wallet: ${activeWallet.name} (${activeWallet.address})`)
126125
console.log('')
127126

128127
const confirm = await p.confirm({

src/commands/config/chains.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as p from '@clack/prompts'
2-
import pc from 'picocolors'
32
import { getConfigStore } from '../../storage/config-store.js'
43
import type { ChainConfig } from '../../types/config.js'
54
import { isValidChainId, isValidUrl } from '../../utils/validation.js'
@@ -12,7 +11,7 @@ import {
1211
} from '../../ui/screens/index.js'
1312

1413
export async function addChain() {
15-
p.intro(pc.bgCyan(pc.black(' Add Chain ')))
14+
p.intro('Add Chain')
1615

1716
const configStore = getConfigStore()
1817

@@ -138,7 +137,7 @@ export async function addChain() {
138137
}
139138

140139
export async function listChains() {
141-
p.intro(pc.bgCyan(pc.black(' Configured Chains ')))
140+
p.intro('Configured Chains')
142141

143142
const configStore = getConfigStore()
144143
const chains = Object.values(configStore.getAllChains())
@@ -149,7 +148,7 @@ export async function listChains() {
149148
}
150149

151150
export async function removeChain() {
152-
p.intro(pc.bgCyan(pc.black(' Remove Chain ')))
151+
p.intro('Remove Chain')
153152

154153
const configStore = getConfigStore()
155154
const chains = configStore.getAllChains()

src/commands/tx/create.ts

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as p from '@clack/prompts'
2-
import pc from 'picocolors'
32
import { isAddress, type Address } from 'viem'
43
import { getConfigStore } from '../../storage/config-store.js'
54
import { getSafeStorage } from '../../storage/safe-store.js'
@@ -148,15 +147,15 @@ export async function createTransaction() {
148147
// If contract, try to fetch ABI and use transaction builder
149148
if (isContract) {
150149
console.log('')
151-
console.log(pc.dim('Attempting to fetch contract ABI...'))
150+
console.log('Attempting to fetch contract ABI...')
152151

153152
const config = configStore.getConfig()
154153
const etherscanApiKey = config.preferences?.etherscanApiKey
155154

156155
// Inform user about ABI source based on API key availability
157156
if (!etherscanApiKey) {
158-
console.log(pc.dim(' Using Sourcify for ABI (free, no API key required)'))
159-
console.log(pc.dim(' Note: Proxy contract detection requires an Etherscan API key'))
157+
console.log(' Using Sourcify for ABI (free, no API key required)')
158+
console.log(' Note: Proxy contract detection requires an Etherscan API key')
160159
}
161160

162161
const abiService = new ABIService(chain, etherscanApiKey)
@@ -171,18 +170,18 @@ export async function createTransaction() {
171170

172171
// Check if Etherscan detected this as a proxy
173172
if (implementationAddress) {
174-
console.log(pc.cyan(`✓ Proxy detected! Implementation: ${implementationAddress}`))
173+
console.log(`✓ Proxy detected! Implementation: ${implementationAddress}`)
175174

176175
if (contractName) {
177-
console.log(pc.green(`✓ Proxy ABI found: ${pc.bold(contractName)}`))
176+
console.log(`✓ Proxy ABI found: ${contractName}`)
178177
} else {
179-
console.log(pc.green('✓ Proxy ABI found!'))
178+
console.log('✓ Proxy ABI found!')
180179
}
181180
} else {
182181
if (contractName) {
183-
console.log(pc.green(`✓ Contract ABI found: ${pc.bold(contractName)}`))
182+
console.log(`✓ Contract ABI found: ${contractName}`)
184183
} else {
185-
console.log(pc.green('✓ Contract ABI found!'))
184+
console.log('✓ Contract ABI found!')
186185
}
187186
}
188187

@@ -195,9 +194,9 @@ export async function createTransaction() {
195194
// Use implementation name as the main contract name
196195
if (implInfo.name) {
197196
contractName = implInfo.name
198-
console.log(pc.green(`✓ Implementation ABI found: ${pc.bold(implInfo.name)}`))
197+
console.log(`✓ Implementation ABI found: ${implInfo.name}`)
199198
} else {
200-
console.log(pc.green('✓ Implementation ABI found!'))
199+
console.log('✓ Implementation ABI found!')
201200
}
202201

203202
// Merge ABIs (implementation functions + proxy functions)
@@ -225,17 +224,17 @@ export async function createTransaction() {
225224
}
226225

227226
abi = combinedAbi
228-
console.log(pc.dim(` Combined: ${abi.length} items total`))
227+
console.log(` Combined: ${abi.length} items total`)
229228
} catch (error) {
230-
console.log(pc.yellow('⚠ Could not fetch implementation ABI, using proxy ABI only'))
231-
console.log(pc.dim(` Found ${abi.length} items in proxy ABI`))
229+
console.log('⚠ Could not fetch implementation ABI, using proxy ABI only')
230+
console.log(` Found ${abi.length} items in proxy ABI`)
232231
}
233232
} else {
234-
console.log(pc.dim(` Found ${abi.length} items in ABI`))
233+
console.log(` Found ${abi.length} items in ABI`)
235234
}
236235
} catch (error) {
237-
console.log(pc.yellow('⚠ Could not fetch ABI'))
238-
console.log(pc.dim(' Contract may not be verified. Falling back to manual input.'))
236+
console.log('⚠ Could not fetch ABI')
237+
console.log(' Contract may not be verified. Falling back to manual input.')
239238
}
240239

241240
// If ABI found, offer transaction builder
@@ -244,7 +243,7 @@ export async function createTransaction() {
244243

245244
console.log('')
246245
if (functions.length > 0) {
247-
console.log(pc.green(`✓ Found ${functions.length} writable function(s)`))
246+
console.log(`✓ Found ${functions.length} writable function(s)`)
248247

249248
const useBuilder = await p.confirm({
250249
message: 'Use transaction builder to interact with contract?',
@@ -295,9 +294,9 @@ export async function createTransaction() {
295294
data = result.data
296295
}
297296
} else {
298-
console.log(pc.yellow('⚠ No writable functions found in ABI'))
299-
console.log(pc.dim(' Contract may only have view/pure functions'))
300-
console.log(pc.dim(' Falling back to manual input'))
297+
console.log('⚠ No writable functions found in ABI')
298+
console.log(' Contract may only have view/pure functions')
299+
console.log(' Falling back to manual input')
301300
}
302301
}
303302
}
@@ -413,11 +412,31 @@ export async function createTransaction() {
413412
activeWallet.address as Address
414413
)
415414

416-
createSpinner.stop('Transaction created')
415+
createSpinner.stop()
417416

418-
await renderScreen(TransactionCreateSuccessScreen, {
419-
safeTxHash: createdTx.safeTxHash,
417+
// Show transaction hash
418+
console.log('')
419+
console.log('✓ Transaction created successfully!')
420+
console.log('')
421+
console.log(` Safe TX Hash: ${createdTx.safeTxHash}`)
422+
console.log('')
423+
424+
// Offer to sign the transaction
425+
const shouldSign = await p.confirm({
426+
message: 'Would you like to sign this transaction now?',
427+
initialValue: true,
420428
})
429+
430+
if (!p.isCancel(shouldSign) && shouldSign) {
431+
console.log('')
432+
const { signTransaction } = await import('./sign.js')
433+
await signTransaction(createdTx.safeTxHash)
434+
} else {
435+
// Show full success screen with next steps
436+
await renderScreen(TransactionCreateSuccessScreen, {
437+
safeTxHash: createdTx.safeTxHash,
438+
})
439+
}
421440
} catch (error) {
422441
if (error instanceof SafeCLIError) {
423442
p.log.error(error.message)

src/commands/tx/pull.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as p from '@clack/prompts'
2-
import pc from 'picocolors'
32
import type { Address } from 'viem'
43
import { getConfigStore } from '../../storage/config-store.js'
54
import { getSafeStorage } from '../../storage/safe-store.js'
@@ -12,7 +11,7 @@ import { renderScreen } from '../../ui/render.js'
1211
import { TransactionPullSuccessScreen, type TransactionPullResult } from '../../ui/screens/index.js'
1312

1413
export async function pullTransactions(account?: string) {
15-
p.intro(pc.bgCyan(pc.black(' Pull Transactions from Safe API ')))
14+
p.intro('Pull Transactions from Safe API')
1615

1716
try {
1817
const configStore = getConfigStore()

0 commit comments

Comments
 (0)