Skip to content
Merged

Dev #138

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d8bb7ec
deployment automation
MastaP Dec 10, 2025
4a095a5
Merge pull request #104 from unicitynetwork/dev
MastaP Dec 17, 2025
760bcb9
Merge pull request #106 from unicitynetwork/dev
MastaP Dec 17, 2025
36f7760
Merge pull request #107 from unicitynetwork/dev
MastaP Dec 17, 2025
5c76fca
Refactor greeting message for clarity
MastaP Dec 17, 2025
b838d60
Merge branch 'dev'
MastaP Dec 18, 2025
106bfd7
display commit hash in deploy
MastaP Dec 18, 2025
15cafc4
feat(wallet): add unified address selector to wallet header
KruGoL Jan 7, 2026
1a15b07
feat(onboarding): gap limit detection using nametag, L1 balance, and …
KruGoL Jan 7, 2026
613f8c8
fix(wallet): hide balance in onboarding and show address before namet…
KruGoL Jan 7, 2026
1bd6ea7
feat(wallet): move vesting selector to SendModal
KruGoL Jan 7, 2026
1cbd850
fix(wallet): show correct L1 balance for selected address in L3 view
KruGoL Jan 7, 2026
cfff0e0
fix(wallet): fix vesting infinite loading and improve transaction fee…
KruGoL Jan 8, 2026
c4be70f
fix(wallet): fix L1 transaction amount calculation
KruGoL Jan 8, 2026
a575e8d
feat(wallet): add connection status UI for L1 wallet
KruGoL Jan 8, 2026
7b494fe
fix(tests): fix flaky 30-day tombstone boundary test
KruGoL Jan 8, 2026
67672b3
Merge pull request #133 from unicitynetwork/128-agent-sphere-improve-…
KruGoL Jan 8, 2026
e5e022f
feat(onboarding): add L1 connection status screen before wallet setup
KruGoL Jan 8, 2026
fd12ff3
fix(onboarding): auto-reconnect when network comes back online
KruGoL Jan 8, 2026
1e8eb94
fix(chat): prevent duplicate session creation when loading existing s…
KruGoL Jan 9, 2026
54d1c32
Implement new Agents view with expanding view
igmahl Jan 12, 2026
c03fa5f
Merge pull request #136 from unicitynetwork/feature/available_agents
MastaP Jan 12, 2026
bede4dc
Merge pull request #137 from unicitynetwork/main
igmahl Jan 12, 2026
477ee06
fix(chat): wrap quick action buttons on mobile for narrow screens
KruGoL Jan 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/deploy-manual.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Deploy to Server

run-name: Deploy ${{ github.sha }}

on:
workflow_dispatch:

jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ secrets.SSH_PORT || 22 }}
script: |
cd ~/sphere
echo "=== Previous commit ==="
git rev-parse HEAD
git rev-parse --short HEAD
git pull origin main
echo "=== Current commit ==="
git rev-parse HEAD
git rev-parse --short HEAD
docker compose build
docker compose up -d
docker compose ps
8 changes: 6 additions & 2 deletions src/components/agents/shared/AgentChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export function AgentChat<TCardData, TItem extends SidebarItem = SidebarItem>({
const {
sessions,
currentSession: historySession, // Session created when saving messages
loadSession,
deleteSession,
clearAllHistory,
resetCurrentSession,
Expand Down Expand Up @@ -446,11 +447,14 @@ export function AgentChat<TCardData, TItem extends SidebarItem = SidebarItem>({
// Load a previous chat session via URL navigation
// TanStack Query handles the actual data loading
const handleLoadSession = useCallback((sessionId: string) => {
// Navigate to session - TanStack Query will load the data
// IMPORTANT: Load session into useChatHistory first to set currentSessionRef
// This prevents saveCurrentMessages from creating a duplicate session
loadSession(sessionId);
// Navigate to session - TanStack Query will load the data for display
navigateToSession(sessionId);
// Close sidebar on mobile
setSidebarOpen(false);
}, [navigateToSession]);
}, [loadSession, navigateToSession]);

const handleDeleteSession = async (sessionId: string) => {
const wasCurrentSession = currentSession?.id === sessionId;
Expand Down
4 changes: 2 additions & 2 deletions src/components/agents/shared/QuickActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ export function QuickActions({ actions, onAction, disabled = false }: QuickActio
<div className="px-4 py-2 border-t border-neutral-200/50 dark:border-neutral-800/30 relative z-10 space-y-2">
{/* Regular actions */}
{regularActions.length > 0 && (
<div className="flex gap-2 overflow-x-auto scrollbar-hide">
<div className="flex gap-2 flex-wrap lg:flex-nowrap lg:overflow-x-auto scrollbar-hide">
{regularActions.map((action) => (
<button
key={action.label}
onClick={() => onAction(action.message)}
disabled={disabled}
className="px-3 py-1.5 rounded-lg bg-neutral-100 dark:bg-neutral-800/50 text-neutral-600 dark:text-neutral-400 text-sm hover:bg-neutral-200 dark:hover:bg-neutral-700/50 hover:text-neutral-900 dark:hover:text-white transition-colors whitespace-nowrap disabled:opacity-50"
className="px-3 py-1.5 rounded-lg bg-neutral-100 dark:bg-neutral-800/50 text-neutral-600 dark:text-neutral-400 text-sm hover:bg-neutral-200 dark:hover:bg-neutral-700/50 hover:text-neutral-900 dark:hover:text-white transition-colors whitespace-nowrap disabled:opacity-50 max-w-[140px] lg:max-w-none truncate"
>
{action.label}
</button>
Expand Down
168 changes: 168 additions & 0 deletions src/components/wallet/L1/components/ConnectionStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { motion } from "framer-motion";
import { Loader2, WifiOff, RefreshCw, XCircle } from "lucide-react";
import type { ConnectionState } from "../hooks/useConnectionStatus";

interface ConnectionStatusProps {
state: ConnectionState;
message: string;
error?: string;
onRetry: () => void;
onCancel: () => void;
}

export function ConnectionStatus({
state,
message,
error,
onRetry,
onCancel,
}: ConnectionStatusProps) {
// Don't show anything when connected
if (state === "connected") {
return null;
}

const isConnecting = state === "connecting";
const isError = state === "error";

return (
<div className="flex flex-col items-center justify-center h-full min-h-[300px] p-6">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="flex flex-col items-center text-center max-w-sm"
>
{/* Animated spinner like logout/onboarding */}
{isConnecting ? (
<div className="relative w-20 h-20 mb-6">
{/* Outer Ring */}
<motion.div
className="absolute inset-0 border-3 border-neutral-200 dark:border-neutral-800/50 rounded-full"
animate={{ rotate: 360 }}
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
/>
{/* Middle Ring */}
<motion.div
className="absolute inset-1.5 border-3 rounded-full border-blue-500/30 border-t-blue-500 border-r-blue-500"
animate={{ rotate: -360 }}
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
/>
{/* Inner Glow */}
<div className="absolute inset-3 rounded-full blur-xl bg-blue-500/20" />
{/* Center Icon */}
<div className="absolute inset-0 flex items-center justify-center">
<motion.div
animate={{
scale: [1, 1.1, 1],
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
>
<Loader2 className="w-7 h-7 animate-spin text-blue-500 dark:text-blue-400" />
</motion.div>
</div>
</div>
) : isError ? (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring" }}
className="w-20 h-20 rounded-full bg-red-500/10 flex items-center justify-center mb-6"
>
<XCircle className="w-10 h-10 text-red-500" />
</motion.div>
) : (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring" }}
className="w-20 h-20 rounded-full bg-neutral-500/10 flex items-center justify-center mb-6"
>
<WifiOff className="w-10 h-10 text-neutral-400" />
</motion.div>
)}

{/* Status Title */}
<h3
className={`text-xl font-bold mb-2 ${
isError
? "text-red-600 dark:text-red-400"
: isConnecting
? "text-blue-600 dark:text-blue-400"
: "text-neutral-600 dark:text-neutral-400"
}`}
>
{state === "connecting" && "Connecting to Blockchain"}
{state === "error" && "Connection Failed"}
{state === "disconnected" && "Disconnected"}
</h3>

{/* Status Message */}
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
{message}
</p>

{/* Status message with pulsing dot for connecting state */}
{isConnecting && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="flex items-center gap-3 px-4 py-3 rounded-xl backdrop-blur-sm border mb-4 text-neutral-700 dark:text-neutral-300 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-700/30"
>
<motion.span
className="w-2 h-2 rounded-full bg-blue-500"
animate={{ scale: [1, 1.5, 1], opacity: [1, 0.5, 1] }}
transition={{ duration: 1.5, repeat: Infinity }}
/>
<span className="text-sm">Establishing connection...</span>
</motion.div>
)}

{/* Error details */}
{error && isError && (
<div className="w-full p-3 mb-4 bg-red-500/10 border border-red-500/20 rounded-xl">
<p className="text-xs text-red-600 dark:text-red-400 font-mono break-all">
{error}
</p>
</div>
)}

{/* Action buttons */}
<div className="flex gap-3">
{(isError || state === "disconnected") && (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={onRetry}
className="px-6 py-2.5 bg-blue-600 hover:bg-blue-500 text-white font-medium rounded-xl flex items-center gap-2 transition-colors"
>
<RefreshCw className="w-4 h-4" />
Retry Connection
</motion.button>
)}

{isConnecting && (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={onCancel}
className="px-6 py-2.5 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 text-neutral-700 dark:text-neutral-200 font-medium rounded-xl flex items-center gap-2 transition-colors"
>
<XCircle className="w-4 h-4" />
Cancel
</motion.button>
)}
</div>

{/* Server info */}
<p className="text-xs text-neutral-400 dark:text-neutral-500 mt-6">
Fulcrum Server: fulcrum.unicity.network:50004
</p>
</motion.div>
</div>
);
}
54 changes: 14 additions & 40 deletions src/components/wallet/L1/components/VestingDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
import { motion } from "framer-motion";
import { Loader2 } from "lucide-react";
import type { VestingBalances } from "../sdk/types";

// Skeleton shimmer component
function BalanceSkeleton() {
return (
<div className="relative overflow-hidden h-3 w-16 rounded bg-neutral-200 dark:bg-neutral-700/50">
<motion.div
className="absolute inset-0 bg-linear-to-r from-transparent via-white/20 dark:via-white/10 to-transparent"
animate={{ x: ["-100%", "100%"] }}
transition={{ duration: 1.2, repeat: Infinity, ease: "linear" }}
/>
</div>
);
}

interface VestingDisplayProps {
showBalances?: boolean;
balances?: VestingBalances;
Expand All @@ -38,41 +24,29 @@ export function VestingDisplay({
<div className="flex gap-3">
{/* Vested */}
<div className="flex-1 rounded-xl bg-green-500/10 border border-green-500/20 p-3">
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-green-600 dark:text-green-400 font-medium">Vested</span>
{isClassifying && (
<Loader2 className="w-3 h-3 text-green-500 animate-spin" />
)}
</div>
<span className="text-xs text-green-600 dark:text-green-400 font-medium block mb-1">
Vested
{isClassifying && <Loader2 className="w-3 h-3 animate-spin inline ml-1" />}
</span>
{showBalances ? (
isClassifying ? (
<BalanceSkeleton />
) : (
<span className="text-sm font-mono text-green-600 dark:text-green-400">
{formatBalance(vestedBalance)}
</span>
)
<span className="text-sm font-mono text-green-600 dark:text-green-400">
{formatBalance(vestedBalance)}
</span>
) : (
<span className="text-sm text-green-600 dark:text-green-400">••••••</span>
)}
</div>

{/* Unvested */}
<div className="flex-1 rounded-xl bg-orange-500/10 border border-orange-500/20 p-3">
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-orange-600 dark:text-orange-400 font-medium">Unvested</span>
{isClassifying && (
<Loader2 className="w-3 h-3 text-orange-500 animate-spin" />
)}
</div>
<span className="text-xs text-orange-600 dark:text-orange-400 font-medium block mb-1">
Unvested
{isClassifying && <Loader2 className="w-3 h-3 animate-spin inline ml-1" />}
</span>
{showBalances ? (
isClassifying ? (
<BalanceSkeleton />
) : (
<span className="text-sm font-mono text-orange-600 dark:text-orange-400">
{formatBalance(unvestedBalance)}
</span>
)
<span className="text-sm font-mono text-orange-600 dark:text-orange-400">
{formatBalance(unvestedBalance)}
</span>
) : (
<span className="text-sm text-orange-600 dark:text-orange-400">••••••</span>
)}
Expand Down
Loading