From 4c97d51a31472b572dec69d8524681644ffdf5f4 Mon Sep 17 00:00:00 2001 From: 0xbe1 <0xbetrue@gmail.com> Date: Mon, 6 Oct 2025 23:28:51 +0800 Subject: [PATCH 1/2] Allow viewing proposals without profile in read-only mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users can now view proposals without selecting a profile by choosing "Skip (read-only)" in the profile menu. This enables read-only access to proposal data (function, votes, simulation results, payload, etc.) without requiring authentication. Changes: - Make profile optional for accessing proposals in TUI - Add "Skip (read-only)" option in profile selection menu - Display read-only mode indicators in UI - Block vote/execute/reject actions when no profile is set - Show appropriate error messages for blocked actions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/ui/HomeView.tsx | 52 ++++++++++++++++++++++++++++------------- src/ui/ProposalView.tsx | 33 +++++++++++++++++++++----- 2 files changed, 63 insertions(+), 22 deletions(-) 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..8f11174 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 @@ -259,6 +268,10 @@ const ProposalView: React.FC = ({ // Handle vote const handleVote = async (seqNum: number, approved: boolean) => { + if (!profile) { + setActionMessage(chalk.red('❌ Cannot vote in read-only mode. Please select a profile.')); + return; + } try { setActionMessage(chalk.yellow(`⏳ ${approved ? 'Submitting Yes vote' : 'Submitting No vote'}... Please wait while the transaction is submitted to the blockchain.`)); const hash = await handleVoteCommand( @@ -277,6 +290,11 @@ const ProposalView: React.FC = ({ // Handle execute with confirmation const handleExecute = async (reject: boolean) => { + if (!profile) { + setActionMessage(chalk.red(`❌ Cannot ${reject ? 'reject' : 'execute'} in read-only mode. Please select a profile.`)); + setConfirmAction(null); + return; + } try { const action = reject ? 'Rejecting' : 'Executing'; const actionPast = reject ? 'Reject' : 'Execute'; @@ -454,6 +472,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 '; From 03778dbae9dc610baa295ee68b07af7957aab777 Mon Sep 17 00:00:00 2001 From: 0xbe1 <0xbetrue@gmail.com> Date: Mon, 6 Oct 2025 23:30:06 +0800 Subject: [PATCH 2/2] Prevent action keys (E/Y/N/R) from working in read-only mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When viewing proposals without a profile selected (read-only mode), pressing E/Y/N/R keys now has no effect instead of showing error messages. This provides cleaner UX for read-only access. Changes: - Add profile check to keyboard input handler for Y/N/E/R keys - Remove redundant profile guards from handleVote and handleExecute - Keys are now no-op in read-only mode rather than showing errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/ui/ProposalView.tsx | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/ui/ProposalView.tsx b/src/ui/ProposalView.tsx index 8f11174..d5a02a1 100644 --- a/src/ui/ProposalView.tsx +++ b/src/ui/ProposalView.tsx @@ -268,10 +268,6 @@ const ProposalView: React.FC = ({ // Handle vote const handleVote = async (seqNum: number, approved: boolean) => { - if (!profile) { - setActionMessage(chalk.red('❌ Cannot vote in read-only mode. Please select a profile.')); - return; - } try { setActionMessage(chalk.yellow(`⏳ ${approved ? 'Submitting Yes vote' : 'Submitting No vote'}... Please wait while the transaction is submitted to the blockchain.`)); const hash = await handleVoteCommand( @@ -279,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(); @@ -290,17 +286,12 @@ const ProposalView: React.FC = ({ // Handle execute with confirmation const handleExecute = async (reject: boolean) => { - if (!profile) { - setActionMessage(chalk.red(`❌ Cannot ${reject ? 'reject' : 'execute'} in read-only mode. Please select a profile.`)); - setConfirmAction(null); - return; - } try { const action = reject ? 'Rejecting' : 'Executing'; 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 { @@ -348,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); }