diff --git a/src/ui/HomeView.tsx b/src/ui/HomeView.tsx index 0f6b53b..624f91d 100644 --- a/src/ui/HomeView.tsx +++ b/src/ui/HomeView.tsx @@ -136,9 +136,7 @@ const HomeView: React.FC = ({ onNavigate }) => { const canAccessProposals = !!( config.network && - config.multisig && - config.profile && - filteredProfiles.some(p => p.name === config.profile) + config.multisig ); const networks = NETWORK_CHOICES; @@ -241,10 +239,18 @@ const HomeView: React.FC = ({ onNavigate }) => { } else if (key.upArrow) { setMenu(m => ({ ...m, subIndex: Math.max(0, m.subIndex - 1) })); } else if (key.downArrow) { - setMenu(m => ({ ...m, subIndex: Math.min(filteredProfiles.length - 1, m.subIndex + 1) })); - } else if (key.return && filteredProfiles[subIndex]) { - updateConfig({ profile: filteredProfiles[subIndex].name }); - collapseMenu(); + // Allow navigating to "Skip" option (one past the last profile) + setMenu(m => ({ ...m, subIndex: Math.min(filteredProfiles.length, m.subIndex + 1) })); + } else if (key.return) { + if (subIndex === filteredProfiles.length) { + // "Skip" option selected - clear profile for read-only mode + updateConfig({ profile: undefined }); + collapseMenu(); + } else if (filteredProfiles[subIndex]) { + // Profile selected + updateConfig({ profile: filteredProfiles[subIndex].name }); + collapseMenu(); + } } } else if (expandedItem === 'multisig') { if (key.escape) { @@ -321,7 +327,7 @@ const HomeView: React.FC = ({ onNavigate }) => { if (view === 'proposal' && canAccessProposals && config.network) { return ( setView('home')} @@ -394,14 +400,16 @@ const HomeView: React.FC = ({ onNavigate }) => { isSelected={selectedIndex === 2} label="Profile" value={ - filteredProfiles.some(p => p.name === config.profile) + config.profile === undefined && config.network && config.multisig + ? 'Read-only' + : filteredProfiles.some(p => p.name === config.profile) ? `${config.profile}${!isProfileOwner ? ' (non-owner)' : ''}` : undefined } placeholder={ !config.network ? "(Select network first)" : filteredProfiles.length === 0 ? `(No profiles for ${config.network})` : - "(Select profile)" + "(Select profile or skip)" } disabled={!config.network} warning={!!config.profile && !isProfileOwner} @@ -525,14 +533,26 @@ const ProfileExpanded: React.FC<{ <> {isSelected ? '▼' : ' '} Profile: {profiles.length > 0 ? ( - profiles.map((profile, index) => ( - - {' '}{index === subIndex ? '▶' : ' '} {profile.name} - {profile.name === currentProfile && } + <> + {profiles.map((profile, index) => ( + + {' '}{index === subIndex ? '▶' : ' '} {profile.name} + {profile.name === currentProfile && } + + ))} + + {' '}{subIndex === profiles.length ? '▶' : ' '} Skip (read-only) + {currentProfile === undefined && } - )) + ) : ( - No profiles found for {network} + <> + No profiles found for {network} + + {' '}{subIndex === 0 ? '▶' : ' '} Skip (read-only) + {currentProfile === undefined && } + + )} ); diff --git a/src/ui/ProposalView.tsx b/src/ui/ProposalView.tsx index 6fd2134..d5a02a1 100644 --- a/src/ui/ProposalView.tsx +++ b/src/ui/ProposalView.tsx @@ -69,7 +69,7 @@ interface ProposalViewProps { multisigAddress: string; network: string; fullnode?: string; - profile: string; + profile?: string; sequenceNumber?: number; onBack?: () => void; } @@ -117,11 +117,20 @@ const ProposalView: React.FC = ({ useEffect(() => { const init = async () => { try { - const profileData = await loadProfile(profile, network as NetworkChoice); - const { signer } = profileData; - setSignerAddress(signer.accountAddress.toString()); - - const aptosInstance = initAptos(network as NetworkChoice, fullnode || profileData.fullnode); + let fullnodeUrl = fullnode; + + // Load profile if provided + if (profile) { + const profileData = await loadProfile(profile, network as NetworkChoice); + const { signer } = profileData; + setSignerAddress(signer.accountAddress.toString()); + fullnodeUrl = fullnodeUrl || profileData.fullnode; + } else { + // Read-only mode - no profile + setSignerAddress(''); + } + + const aptosInstance = initAptos(network as NetworkChoice, fullnodeUrl); setAptos(aptosInstance); // Get multisig info @@ -266,7 +275,7 @@ const ProposalView: React.FC = ({ approved, multisigAddress, network as NetworkChoice, - profile + profile! ); setActionMessage(chalk.green(`✅ Vote submitted: ${getExplorerUrl(network as NetworkChoice, `txn/${hash}`)}`)); await fetchProposals(); @@ -282,7 +291,7 @@ const ProposalView: React.FC = ({ const actionPast = reject ? 'Reject' : 'Execute'; setConfirmAction(null); // Clear confirmation immediately setActionMessage(chalk.yellow(`⏳ ${action} transaction... Please wait while the transaction is submitted to the blockchain.`)); - const result = await handleExecuteCommand(multisigAddress, profile, network as NetworkChoice, reject, true); + const result = await handleExecuteCommand(multisigAddress, profile!, network as NetworkChoice, reject, true); if (reject || result.success) { setActionMessage(chalk.green(`✅ ${actionPast} successful: ${getExplorerUrl(network as NetworkChoice, `txn/${result.hash}`)}`)); } else { @@ -330,15 +339,16 @@ const ProposalView: React.FC = ({ } else if (key.return) { // Toggle expand for current selection only setIsSelectedExpanded(prev => !prev); - } else if ((normalizedInput === 'y' || normalizedInput === 'n') && proposals[selectedIndex]) { + } else if ((normalizedInput === 'y' || normalizedInput === 'n') && proposals[selectedIndex] && profile) { + // Only allow voting if profile is set handleVote(proposals[selectedIndex].sequenceNumber, normalizedInput === 'y'); - } else if (normalizedInput === 'e' && proposals[selectedIndex]) { - // Only allow execute if this is the smallest sequence number + } else if (normalizedInput === 'e' && proposals[selectedIndex] && profile) { + // Only allow execute if profile is set and this is the smallest sequence number if (proposals[selectedIndex].canExecute && selectedIndex === 0) { showConfirmation('execute', proposals[selectedIndex].sequenceNumber); } - } else if (normalizedInput === 'r' && proposals[selectedIndex]) { - // Only allow reject if this is the smallest sequence number + } else if (normalizedInput === 'r' && proposals[selectedIndex] && profile) { + // Only allow reject if profile is set and this is the smallest sequence number if (proposals[selectedIndex].canReject && selectedIndex === 0) { showConfirmation('reject', proposals[selectedIndex].sequenceNumber); } @@ -454,6 +464,9 @@ const ProposalView: React.FC = ({ {proposals[selectedIndex] && ( <> #{proposals[selectedIndex].sequenceNumber}: {(() => { + if (!profile) { + return '(Read-only) '; + } const p = proposals[selectedIndex]; const isSmallest = selectedIndex === 0; let actions = '[Y]es [N]o ';