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
14 changes: 13 additions & 1 deletion apps/desktop/src/app/command-palette/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
Palette,
Plus,
Settings,
Settings2,
Sun,
Users,
Wrench,
Expand Down Expand Up @@ -105,7 +106,18 @@ const NON_CONFIG_SETTINGS: ReadonlyArray<{ icon: IconComponent; keywords?: strin
tab: 'providers&pview=keys'
},
{ icon: Globe, keywords: ['connection', 'messaging'], label: 'Gateway', tab: 'gateway' },
{ icon: KeyRound, keywords: ['api', 'secrets', 'tokens', 'credentials'], label: 'Tools & Keys', tab: 'keys' },
{
icon: KeyRound,
keywords: ['api', 'secrets', 'tokens', 'credentials', 'browser', 'search'],
label: 'Tools & Keys',
tab: 'keys&kview=tools'
},
{
icon: Settings2,
keywords: ['gateway', 'proxy', 'server', 'webhook', 'env'],
label: 'Tools & Keys settings',
tab: 'keys&kview=settings'
},
{ icon: Wrench, keywords: ['servers', 'tools'], label: 'MCP', tab: 'mcp' },
{ icon: Archive, keywords: ['history', 'archived'], label: 'Archived Chats', tab: 'sessions' },
{ icon: Info, keywords: ['version', 'about'], label: 'About', tab: 'about' }
Expand Down
108 changes: 108 additions & 0 deletions apps/desktop/src/app/cron/cron-job-actions-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import type * as React from 'react'

import { Button } from '@/components/ui/button'
import { Codicon } from '@/components/ui/codicon'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { triggerHaptic } from '@/lib/haptics'

interface CronJobActions {
busy?: boolean
isPaused: boolean
title: string
onDelete: () => void
onEdit: () => void
onPauseResume: () => void
onTrigger: () => void
}

interface CronJobActionsMenuProps
extends CronJobActions, Pick<React.ComponentProps<typeof DropdownMenuContent>, 'align' | 'sideOffset'> {
children: React.ReactNode
}

export function CronJobActionsMenu({
align = 'end',
busy = false,
children,
isPaused,
onDelete,
onEdit,
onPauseResume,
onTrigger,
sideOffset = 6,
title
}: CronJobActionsMenuProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent
align={align}
aria-label={`Actions for ${title}`}
className="w-44"
sideOffset={sideOffset}
>
<DropdownMenuItem
disabled={busy}
onSelect={() => {
triggerHaptic('selection')
onPauseResume()
}}
>
<Codicon name={isPaused ? 'play' : 'debug-pause'} size="0.875rem" />
<span>{isPaused ? 'Resume' : 'Pause'}</span>
</DropdownMenuItem>

<DropdownMenuItem
disabled={busy}
onSelect={() => {
triggerHaptic('selection')
onTrigger()
}}
>
<Codicon name="zap" size="0.875rem" />
<span>Trigger now</span>
</DropdownMenuItem>

<DropdownMenuItem
onSelect={() => {
triggerHaptic('selection')
onEdit()
}}
>
<Codicon name="edit" size="0.875rem" />
<span>Edit</span>
</DropdownMenuItem>

<DropdownMenuItem
onSelect={() => {
triggerHaptic('warning')
onDelete()
}}
variant="destructive"
>
<Codicon name="trash" size="0.875rem" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

interface CronJobActionsTriggerProps extends Omit<React.ComponentProps<typeof Button>, 'size' | 'variant'> {
title: string
}

export function CronJobActionsTrigger({ className, title, ...props }: CronJobActionsTriggerProps) {
return (
<Button
aria-label={`Actions for ${title}`}
className={className}
size="icon-sm"
title="Cron job actions"
variant="ghost"
{...props}
>
<Codicon className="text-muted-foreground" name="ellipsis" size="0.875rem" />
</Button>
)
}
53 changes: 17 additions & 36 deletions apps/desktop/src/app/cron/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ import {
updateCronJob
} from '@/hermes'
import { AlertTriangle, Clock } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { notify, notifyError } from '@/store/notifications'

import { useRefreshHotkey } from '../hooks/use-refresh-hotkey'
import { OverlayView } from '../overlays/overlay-view'

import { CronJobActionsMenu, CronJobActionsTrigger } from './cron-job-actions-menu'

const DEFAULT_DELIVER = 'local'

const DELIVERY_OPTIONS: ReadonlyArray<{ label: string; value: string }> = [
Expand Down Expand Up @@ -563,47 +564,27 @@ function CronJobRow({
)}
</button>

<div className="flex shrink-0 items-center gap-0.5">
<IconAction
aria-label={isPaused ? 'Resume cron' : 'Pause cron'}
disabled={busy}
onClick={onPauseResume}
title={isPaused ? 'Resume' : 'Pause'}
>
<Codicon name={isPaused ? 'play' : 'debug-pause'} size="0.875rem" />
</IconAction>
<IconAction aria-label="Trigger now" disabled={busy} onClick={onTrigger} title="Trigger now">
<Codicon name="zap" size="0.875rem" />
</IconAction>
<IconAction aria-label="Edit cron" onClick={onEdit} title="Edit">
<Codicon name="edit" size="0.875rem" />
</IconAction>
<IconAction
aria-label="Delete cron"
className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
onClick={onDelete}
title="Delete"
<div className="flex shrink-0 items-center">
<CronJobActionsMenu
busy={busy}
isPaused={isPaused}
onDelete={onDelete}
onEdit={onEdit}
onPauseResume={onPauseResume}
onTrigger={onTrigger}
title={jobTitle(job)}
>
<Codicon name="trash" size="0.875rem" />
</IconAction>
<CronJobActionsTrigger
className="text-muted-foreground hover:text-foreground"
onClick={event => event.stopPropagation()}
title={jobTitle(job)}
/>
</CronJobActionsMenu>
</div>
</div>
)
}

function IconAction({ children, className, ...props }: Omit<React.ComponentProps<typeof Button>, 'size' | 'variant'>) {
return (
<Button
className={cn('text-muted-foreground hover:text-foreground', className)}
size="icon-sm"
variant="ghost"
{...props}
>
{children}
</Button>
)
}

function EmptyState({
actionLabel,
description,
Expand Down
126 changes: 86 additions & 40 deletions apps/desktop/src/app/messaging/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { AlertTriangle, ExternalLink, Save, Trash2 } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { notify, notifyError } from '@/store/notifications'

import { CREDENTIAL_CONTROL_CLASS } from '../settings/credential-key-ui'
import { ListRow } from '../settings/primitives'
import { useRefreshHotkey } from '../hooks/use-refresh-hotkey'
import { useRouteEnumParam } from '../hooks/use-route-enum-param'
import { PageSearchShell } from '../page-search-shell'
Expand Down Expand Up @@ -108,6 +110,47 @@ const FIELD_COPY: Record<string, { advanced?: boolean; help?: string; label: str
help: 'first, all, or off.',
advanced: true
},
DISCORD_ALLOW_ALL_USERS: {
label: 'Allow all Discord users',
help: 'Development only. When true, anyone can DM the bot without an allowlist.',
advanced: true
},
DISCORD_HOME_CHANNEL: {
label: 'Home channel ID',
help: 'Channel where the bot sends proactive messages (cron output, reminders).',
advanced: true
},
DISCORD_HOME_CHANNEL_NAME: {
label: 'Home channel name',
help: 'Display name for the home channel in logs and status output.',
advanced: true
},
BLUEBUBBLES_ALLOW_ALL_USERS: {
label: 'Allow all iMessage users',
help: 'When true, skip the BlueBubbles allowlist.',
advanced: true
},
MATTERMOST_ALLOW_ALL_USERS: {
label: 'Allow all Mattermost users',
advanced: true
},
MATTERMOST_HOME_CHANNEL: {
label: 'Home channel',
advanced: true
},
QQ_ALLOW_ALL_USERS: {
label: 'Allow all QQ users',
advanced: true
},
QQBOT_HOME_CHANNEL: {
label: 'QQ home channel',
help: 'Default channel or group for cron delivery.',
advanced: true
},
QQBOT_HOME_CHANNEL_NAME: {
label: 'QQ home channel name',
advanced: true
},
SLACK_BOT_TOKEN: {
label: 'Slack bot token',
help: 'Starts with xoxb-. Found under OAuth & Permissions after installing your Slack app.',
Expand Down Expand Up @@ -497,7 +540,7 @@ function PlatformDetail({

<section>
<SectionTitle>Required</SectionTitle>
<div className="mt-3 space-y-4">
<div className="mt-3 grid gap-1">
{requiredFields.length > 0 ? (
requiredFields.map(field => (
<MessagingField
Expand All @@ -520,7 +563,7 @@ function PlatformDetail({
{optionalFields.length > 0 && (
<section>
<SectionTitle>Recommended</SectionTitle>
<div className="mt-3 space-y-4">
<div className="mt-3 grid gap-1">
{optionalFields.map(field => (
<MessagingField
edits={edits}
Expand All @@ -546,7 +589,7 @@ function PlatformDetail({
<DisclosureCaret open={showAdvanced} size="0.875rem" />
</button>
{showAdvanced && (
<div className="mt-3 space-y-4">
<div className="mt-3 grid gap-1">
{advancedFields.map(field => (
<MessagingField
edits={edits}
Expand Down Expand Up @@ -640,45 +683,48 @@ function MessagingField({
saving: string | null
}) {
const copy = fieldCopy(field)
const fieldId = `messaging-field-${field.key}`

return (
<div className="space-y-1.5">
<div className="flex flex-wrap items-baseline gap-2">
<label className="text-sm font-medium text-foreground" htmlFor={`messaging-field-${field.key}`}>
{copy.label}
</label>
{field.is_set && <span className="text-[0.66rem] font-medium text-primary">Saved</span>}
</div>
<div className="flex items-center gap-2">
<Input
className="font-mono"
id={`messaging-field-${field.key}`}
onChange={event => onEdit(field.key, event.target.value)}
placeholder={field.is_set ? field.redacted_value || 'Replace current value' : copy.placeholder}
type={field.is_password ? 'password' : 'text'}
value={edits[field.key] || ''}
/>
{field.url && (
<Button asChild size="icon-sm" title="Open docs" variant="ghost">
<a href={field.url} rel="noreferrer" target="_blank">
<ExternalLink className="size-3.5" />
</a>
</Button>
)}
{field.is_set && (
<Button
disabled={saving === `clear:${field.key}`}
onClick={() => onClear(field.key)}
size="icon-sm"
title={`Clear ${field.key}`}
variant="ghost"
>
<Trash2 className="size-3.5" />
</Button>
)}
</div>
{copy.help && <p className="text-xs leading-5 text-muted-foreground">{copy.help}</p>}
</div>
<ListRow
action={
<div className="flex items-center gap-2">
<Input
className={CREDENTIAL_CONTROL_CLASS}
id={fieldId}
onChange={event => onEdit(field.key, event.target.value)}
placeholder={field.is_set ? field.redacted_value || 'Replace current value' : copy.placeholder}
type={field.is_password ? 'password' : 'text'}
value={edits[field.key] || ''}
/>
{field.url && (
<Button asChild className="size-8 shrink-0" title="Open docs" variant="ghost">
<a href={field.url} rel="noreferrer" target="_blank">
<ExternalLink className="size-3.5" />
</a>
</Button>
)}
{field.is_set && (
<Button
className="size-8 shrink-0"
disabled={saving === `clear:${field.key}`}
onClick={() => onClear(field.key)}
title={`Clear ${field.key}`}
variant="ghost"
>
<Trash2 className="size-3.5" />
</Button>
)}
</div>
}
description={copy.help}
title={
<span className="flex flex-wrap items-center gap-2">
<label htmlFor={fieldId}>{copy.label}</label>
{field.is_set && <span className="text-[0.66rem] font-medium text-primary">Saved</span>}
</span>
}
/>
)
}

Expand Down
Loading
Loading