Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
56 changes: 56 additions & 0 deletions scripts/test-discord.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Run: node scripts/test-discord.mjs YOUR_BOT_TOKEN YOUR_CHANNEL_ID
// Example: node scripts/test-discord.mjs MTQ4N... 1234567890

const [,, TOKEN, CHANNEL_ID] = process.argv

if (!TOKEN || !CHANNEL_ID) {
console.error('Usage: node scripts/test-discord.mjs YOUR_BOT_TOKEN YOUR_CHANNEL_ID')
process.exit(1)
}

const headers = { Authorization: `Bot ${TOKEN}`, 'Content-Type': 'application/json' }

async function run() {
console.log('\n── Step 1: Verify bot token ──')
const meRes = await fetch('https://discord.com/api/v10/users/@me', { headers })
const me = await meRes.json()
if (!meRes.ok) {
console.error('✗ Invalid token:', me.message)
process.exit(1)
}
console.log(`✓ Bot: ${me.username}#${me.discriminator} (id: ${me.id})`)

console.log('\n── Step 2: Check channel access ──')
const chRes = await fetch(`https://discord.com/api/v10/channels/${CHANNEL_ID}`, { headers })
const ch = await chRes.json()
if (chRes.status === 403) {
console.error('✗ Bot cannot see this channel.')
console.error(' → Make sure the bot is in the server that owns this channel')
console.error(' → Channel response:', ch)
process.exit(1)
}
if (chRes.status === 404) {
console.error('✗ Channel not found. Wrong Channel ID?')
process.exit(1)
}
if (!chRes.ok) {
console.error(`✗ Channel error ${chRes.status}:`, ch)
process.exit(1)
}
console.log(`✓ Channel: #${ch.name} (guild: ${ch.guild_id})`)

console.log('\n── Step 3: Send test message ──')
const sendRes = await fetch(`https://discord.com/api/v10/channels/${CHANNEL_ID}/messages`, {
method: 'POST', headers,
body: JSON.stringify({ content: '✓ Agentis test message — connection confirmed.' }),
})
const send = await sendRes.json()
if (!sendRes.ok) {
console.error(`✗ Send failed ${sendRes.status}:`, send)
process.exit(1)
}
console.log(`✓ Message sent! ID: ${send.id}`)
console.log('\n✓ All checks passed — Discord is working correctly.\n')
}

run().catch(err => { console.error('Error:', err.message); process.exit(1) })
121 changes: 115 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { useAgent } from '@/hooks/useAgent'
import { Sidebar } from '@/components/Sidebar'
import { ChatPage } from '@/components/pages/ChatPage'
Expand All @@ -17,6 +17,8 @@ import { UniversePage } from '@/components/pages/UniversePage'
import { SettingsPage } from '@/components/pages/SettingsPage'
import { DocsPage } from '@/components/pages/DocsPage'
import { addHistoryEntry } from '@/components/DashboardScreen'
import { isPinSet, isLocked, verifyPin, unlock, updateActivity, checkInactivityLock, getLockSettings } from '@/lib/pinLock'
import { useDiscordListener } from '@/hooks/useDiscordListener'

type Page =
| 'chat' | 'overview' | 'analytics' | 'logs' | 'sessions'
Expand All @@ -33,8 +35,39 @@ export default function App() {
})
const [page, setPage] = useState<Page>('overview')
const [universeInitialRoles, setUniverseInitialRoles] = useState<import('@/lib/multiAgentEngine').AgentRole[] | undefined>(undefined)
const [universeAutoStart, setUniverseAutoStart] = useState<string | undefined>(undefined)
const [universeOnComplete, setUniverseOnComplete] = useState<((output: string) => void) | undefined>(undefined)
const [universeFollowUp, setUniverseFollowUp] = useState<string | undefined>(undefined)
const [universeOnFollowUpComplete, setUniverseOnFollowUpComplete] = useState<((output: string) => void) | undefined>(undefined)
const [engineRunning, setEngineRunning] = useState(false)

// ── PIN lock ───────────────────────────────────────────────────────────────
const [locked, setLocked] = useState(() => isPinSet() && getLockSettings().enabled)
const [pinInput, setPinInput] = useState('')
const [pinError, setPinError] = useState('')
const inactivityRef = useRef<ReturnType<typeof setInterval> | null>(null)

useEffect(() => {
const onActivity = () => updateActivity()
window.addEventListener('mousemove', onActivity)
window.addEventListener('keydown', onActivity)
inactivityRef.current = setInterval(() => {
if (checkInactivityLock()) setLocked(true)
}, 30_000)
return () => {
window.removeEventListener('mousemove', onActivity)
window.removeEventListener('keydown', onActivity)
if (inactivityRef.current) clearInterval(inactivityRef.current)
}
}, [])

// Re-sync locked state when PIN settings change (e.g. PIN just set)
useEffect(() => {
const handler = () => setLocked(isLocked())
window.addEventListener('agentis_lock_update', handler)
return () => window.removeEventListener('agentis_lock_update', handler)
}, [])

// Poll engine status
useEffect(() => {
let active = true
Expand Down Expand Up @@ -85,14 +118,79 @@ export default function App() {
})
}, [agentState.step]) // eslint-disable-line react-hooks/exhaustive-deps

const navigate = (p: string, opts?: { initialRoles?: import('@/lib/multiAgentEngine').AgentRole[] }) => {
if (p === 'universe') setUniverseInitialRoles(opts?.initialRoles)
else if (opts?.initialRoles !== undefined) setUniverseInitialRoles(opts.initialRoles)
const navigate = (p: string, opts?: {
initialRoles?: import('@/lib/multiAgentEngine').AgentRole[]
autoStart?: string
onComplete?: (output: string) => void
followUp?: string
onFollowUpComplete?: (output: string) => void
}) => {
if (p === 'universe') {
setUniverseInitialRoles(opts?.initialRoles)
if (opts?.autoStart !== undefined) setUniverseAutoStart(opts.autoStart)
if (opts?.onComplete !== undefined) setUniverseOnComplete(() => opts.onComplete!)
if (opts?.followUp !== undefined) setUniverseFollowUp(opts.followUp)
if (opts?.onFollowUpComplete !== undefined) setUniverseOnFollowUpComplete(() => opts.onFollowUpComplete!)
} else if (opts?.initialRoles !== undefined) {
setUniverseInitialRoles(opts.initialRoles)
}
setPage(p as Page)
// Reset chat state when navigating away from chat
if (p !== 'chat') reset()
}

const discordListener = useDiscordListener({ navigate })

// ── PIN lock screen ────────────────────────────────────────────────────────────
if (locked) {
return (
<div className="shell-center">
<div style={{ width: '100%', maxWidth: 360, background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 12, padding: 28 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 24 }}>
<img src="/favicon.png" alt="Agentis" style={{ width: 40, height: 40, borderRadius: 8, objectFit: 'contain' }} />
<div>
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--fg)' }}>AGENTIS</div>
<div style={{ fontSize: 11, color: 'var(--muted)' }}>Locked</div>
</div>
<div style={{ marginLeft: 'auto', width: 32, height: 32, borderRadius: '50%', background: 'var(--bg)', border: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 16 }}>🔒</div>
</div>

<div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 14 }}>Enter your PIN to unlock</div>

<input
type="password"
placeholder="PIN"
value={pinInput}
autoFocus
onChange={e => { setPinInput(e.target.value); setPinError('') }}
onKeyDown={async e => {
if (e.key !== 'Enter' || !pinInput) return
const ok = await verifyPin(pinInput)
if (ok) { unlock(); setLocked(false); setPinInput('') }
else { setPinError('Incorrect PIN'); setPinInput('') }
}}
style={{ width: '100%', marginBottom: 8, fontFamily: 'var(--font-mono)', fontSize: 18, letterSpacing: '0.3em', textAlign: 'center' }}
/>

{pinError && <div style={{ fontSize: 11, color: 'var(--red)', marginBottom: 8, textAlign: 'center' }}>{pinError}</div>}

<button
className="btn-primary"
style={{ width: '100%', padding: '10px', fontSize: 14 }}
onClick={async () => {
if (!pinInput) return
const ok = await verifyPin(pinInput)
if (ok) { unlock(); setLocked(false); setPinInput('') }
else { setPinError('Incorrect PIN'); setPinInput('') }
}}
>
Unlock
</button>
</div>
</div>
)
}

// ── API key gate ──────────────────────────────────────────────────────────────
if (!keySet) {
return (
Expand Down Expand Up @@ -232,7 +330,7 @@ export default function App() {
/>
</div>

{page === 'channels' && <ChannelsPage />}
{page === 'channels' && <ChannelsPage discordListening={discordListener.active} discordProcessed={discordListener.processed} />}

{page === 'skills' && <SkillsPage navigate={navigate} apiKey={apiKey} />}

Expand All @@ -241,7 +339,18 @@ export default function App() {
<HandsPage apiKey={apiKey} />
</div>

{page === 'universe' && <UniversePage apiKey={apiKey} initialRoles={universeInitialRoles} />}
{page === 'universe' && (
<UniversePage
apiKey={apiKey}
initialRoles={universeInitialRoles}
autoStart={universeAutoStart}
onComplete={universeOnComplete}
onConsumedAutoStart={() => setUniverseAutoStart(undefined)}
discordFollowUp={universeFollowUp}
onFollowUpComplete={universeOnFollowUpComplete}
onConsumedFollowUp={() => setUniverseFollowUp(undefined)}
/>
)}

{page === 'settings' && (
<SettingsPage
Expand Down
100 changes: 100 additions & 0 deletions src/components/ChannelSendButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// ── ChannelSendButton ─────────────────────────────────────────────────────────
// Reusable "send to channel" dropdown. Returns null if no channels are configured.

import { useState } from 'react'
import { getConfiguredChannels, sendToChannel } from '@/lib/channelDispatch'

interface Props {
message: string
style?: React.CSSProperties
}

export function ChannelSendButton({ message, style }: Props) {
const channels = getConfiguredChannels()
const [open, setOpen] = useState(false)
const [sending, setSending] = useState<string | null>(null)
const [result, setResult] = useState<{ ok: boolean; msg: string } | null>(null)

if (channels.length === 0) return null

const send = async (channelId: string) => {
setSending(channelId)
setOpen(false)
const res = await sendToChannel(channelId, message)
setSending(null)
setResult(res)
setTimeout(() => setResult(null), 4000)
}

const baseStyle: React.CSSProperties = { fontSize: 10, padding: '2px 8px', ...style }

// Feedback state
if (result) {
return (
<span style={{ fontSize: 10, padding: '2px 8px', color: result.ok ? '#10b981' : '#ef4444' }}>
{result.ok ? `✓ ${result.msg}` : `✗ ${result.msg}`}
</span>
)
}

// Single channel — skip dropdown
if (channels.length === 1) {
const ch = channels[0]
return (
<button
className="btn-ghost"
onClick={() => void send(ch.id)}
disabled={!!sending}
style={baseStyle}
>
{sending ? 'Sending…' : `↑ ${ch.name}`}
</button>
)
}

// Multiple channels — dropdown
return (
<div style={{ position: 'relative' }}>
<button
className="btn-ghost"
onClick={() => setOpen(o => !o)}
disabled={!!sending}
style={baseStyle}
>
{sending ? 'Sending…' : '↑ Channel ▾'}
</button>
{open && (
<>
{/* Click-away overlay */}
<div
style={{ position: 'fixed', inset: 0, zIndex: 99 }}
onClick={() => setOpen(false)}
/>
<div style={{
position: 'absolute', right: 0, top: '100%', marginTop: 4,
background: 'var(--surface)', border: '1px solid var(--border)',
borderRadius: 8, zIndex: 100, minWidth: 150,
boxShadow: '0 4px 16px rgba(0,0,0,0.3)',
}}>
{channels.map(ch => (
<button
key={ch.id}
onClick={() => void send(ch.id)}
style={{
display: 'block', width: '100%', textAlign: 'left',
padding: '8px 12px', background: 'none', border: 'none',
cursor: 'pointer', fontSize: 12, color: 'var(--fg)',
fontFamily: 'var(--font-sans)',
}}
onMouseEnter={e => { e.currentTarget.style.background = 'var(--accent-bg)' }}
onMouseLeave={e => { e.currentTarget.style.background = 'none' }}
>
{ch.name}
</button>
))}
</div>
</>
)}
</div>
)
}
Loading
Loading