Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license

This commit is contained in:
2026-09-05 13:26:46 +03:00
commit 03634b1ca3
11340 changed files with 3442369 additions and 0 deletions
+609
View File
@@ -0,0 +1,609 @@
import { useStore } from '@nanostores/react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { PageLoader } from '@/components/page-loader'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { ConfirmDialog } from '@/components/ui/confirm-dialog'
import { CopyButton } from '@/components/ui/copy-button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { Field } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import {
createWebhook,
deleteWebhook,
enableWebhooks,
getWebhooks,
setWebhookEnabled,
type WebhookRoute,
type WebhooksResponse
} from '@/hermes'
import { useI18n } from '@/i18n'
import { AlertTriangle, Globe, Plus, RefreshCw } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { notify, notifyError } from '@/store/notifications'
import { $profileScope } from '@/store/profile'
import { runGatewayRestart } from '@/store/system-actions'
import { useRefreshHotkey } from '../hooks/use-refresh-hotkey'
import {
Panel,
PanelAddButton,
PanelBlock,
PanelBody,
PanelDetail,
PanelEmpty,
PanelHeader,
PanelList,
PanelListRow,
PanelMeta,
PanelPill,
PanelSectionLabel
} from '../overlays/panel'
import { ListRow } from '../settings/primitives'
const DELIVER_OPTIONS: readonly string[] = ['log', 'telegram', 'discord', 'slack', 'email', 'github_comment']
interface CreatedWebhook {
secret: string
url: string
}
// One affordance for "value + CopyButton": flat, token-backed (no border, no
// raw literals). DESIGN.md Principle 1 (flat, not boxed) + 4 (tokens, not
// literals). Reused by the detail URL row and the create-result URL/secret so
// there is a single copyable-value chrome in this file.
function CopyValueRow({ copyLabel, mono = true, value }: { copyLabel: string; mono?: boolean; value: string }) {
return (
<div className="flex items-center gap-1 rounded bg-foreground/5 px-2.5 py-1.5 text-[0.7rem]">
<span className={cn('min-w-0 flex-1 truncate text-foreground/80', mono && 'font-mono')}>{value}</span>
<CopyButton appearance="icon" buttonSize="icon-sm" label={copyLabel} text={value} />
</div>
)
}
interface WebhooksViewProps {
onClose: () => void
}
export function WebhooksView({ onClose }: WebhooksViewProps) {
const { t } = useI18n()
const w = t.webhooks
// Re-load when the active profile changes so REST routes to the right backend.
const profileScope = useStore($profileScope)
const queryClient = useQueryClient()
const queryKey = useMemo(() => ['webhooks', profileScope] as const, [profileScope])
const [query, setQuery] = useState('')
const [enabling, setEnabling] = useState(false)
const [restartNeeded, setRestartNeeded] = useState(false)
const [restartError, setRestartError] = useState<null | string>(null)
const [restarting, setRestarting] = useState(false)
// Master/detail: the subscription whose config fills the right pane.
const [selectedName, setSelectedName] = useState<null | string>(null)
const [createOpen, setCreateOpen] = useState(false)
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [events, setEvents] = useState('')
const [deliver, setDeliver] = useState('log')
const [deliverOnly, setDeliverOnly] = useState(false)
const [prompt, setPrompt] = useState('')
const [skills, setSkills] = useState('')
const [creating, setCreating] = useState(false)
const [created, setCreated] = useState<CreatedWebhook | null>(null)
const [pendingDelete, setPendingDelete] = useState<null | string>(null)
const {
data,
error,
isPending: loading,
refetch
} = useQuery({
queryKey,
queryFn: getWebhooks
})
// React Query v5 dropped useQuery onError; surface a load failure toast once
// per error object instead.
useEffect(() => {
if (error) {
notifyError(error, w.loadFailed)
}
}, [error, w.loadFailed])
const enabled = data?.enabled ?? false
const subscriptions = useMemo(() => data?.subscriptions ?? [], [data])
// Pull fresh backend truth into the cache. `silent` swallows the error toast
// for post-mutation reconciles (the mutation already reported success/failure).
const reload = useCallback(
async (silent = false) => {
try {
await queryClient.invalidateQueries({ queryKey })
} catch (err) {
if (!silent) {
notifyError(err, w.loadFailed)
}
}
},
[queryClient, queryKey, w.loadFailed]
)
useRefreshHotkey(() => void refetch())
const restartGatewayNow = useCallback(async () => {
setRestarting(true)
try {
await runGatewayRestart()
setRestartNeeded(false)
setRestartError(null)
// Give the receiver a moment to bind before re-reading state.
window.setTimeout(() => void reload(true), 4000)
} catch (err) {
setRestartNeeded(true)
setRestartError(String(err))
notifyError(err, w.restartFailed(''))
} finally {
setRestarting(false)
}
}, [reload, w])
const handleEnable = useCallback(async () => {
setEnabling(true)
setRestartNeeded(false)
setRestartError(null)
try {
const result = await enableWebhooks()
await reload(true)
if (result.restart_started) {
notify({ kind: 'success', message: w.enabledRestarting })
window.setTimeout(() => void reload(true), 4000)
} else {
const detail = result.restart_error ? `: ${result.restart_error}` : '.'
setRestartNeeded(true)
setRestartError(w.restartFailed(detail))
notify({ kind: 'error', message: w.restartFailed(detail) })
}
} catch (err) {
notifyError(err, w.restartFailed(''))
} finally {
setEnabling(false)
}
}, [reload, w])
const resetForm = useCallback(() => {
setName('')
setDescription('')
setEvents('')
setDeliver('log')
setDeliverOnly(false)
setPrompt('')
setSkills('')
}, [])
const closeCreate = useCallback(() => {
if (creating) {
return
}
setCreateOpen(false)
setCreated(null)
}, [creating])
const handleCreate = useCallback(async () => {
if (!name.trim()) {
notify({ kind: 'error', message: w.nameRequired })
return
}
setCreating(true)
try {
const eventsList = events
.split(',')
.map(e => e.trim())
.filter(Boolean)
const skillsList = skills
.split(',')
.map(s => s.trim())
.filter(Boolean)
const res = await createWebhook({
deliver,
deliver_only: deliverOnly,
description: description.trim() || undefined,
events: eventsList.length ? eventsList : undefined,
name: name.trim(),
prompt: prompt.trim() || undefined,
skills: skillsList.length ? skillsList : undefined
})
notify({ kind: 'success', message: w.created })
setCreated({ secret: res.secret, url: res.url })
resetForm()
void reload(true)
} catch (err) {
notifyError(err, w.createFailed(''))
} finally {
setCreating(false)
}
}, [deliver, deliverOnly, description, events, name, prompt, reload, resetForm, skills, w])
const handleToggle = useCallback(
async (subName: string, nextEnabled: boolean) => {
// Optimistic cache paint; the invalidate below lets backend truth win.
queryClient.setQueryData<WebhooksResponse>(queryKey, current =>
current
? {
...current,
subscriptions: current.subscriptions.map(s => (s.name === subName ? { ...s, enabled: nextEnabled } : s))
}
: current
)
try {
await setWebhookEnabled(subName, nextEnabled)
notify({ kind: 'success', message: nextEnabled ? w.enabled(subName) : w.disabled(subName) })
void reload(true)
} catch (err) {
await reload(true)
notifyError(err, w.toggleFailed(subName, nextEnabled))
}
},
[queryClient, queryKey, reload, w]
)
// ConfirmDialog owns the pending→done→close beat; throw to surface its inline
// error and keep the dialog open. Success toast matches the cron delete idiom
// (title + name).
const handleDelete = useCallback(async () => {
if (!pendingDelete) {
return
}
try {
await deleteWebhook(pendingDelete)
notify({ kind: 'success', title: w.deleted, message: pendingDelete })
void reload(true)
} catch (err) {
notifyError(err, w.deleteFailed(pendingDelete))
throw err
}
}, [pendingDelete, reload, w])
const visible = useMemo(() => {
const q = query.trim().toLowerCase()
if (!q) {
return subscriptions
}
return subscriptions.filter(s =>
[s.name, s.description, s.deliver, ...s.events].filter(Boolean).some(v => v.toLowerCase().includes(q))
)
}, [query, subscriptions])
// Detail always reflects a concrete sub: the explicitly selected one, else the
// first visible row, so the right pane is never empty while subs exist.
const selectedSub = useMemo(
() => visible.find(s => s.name === selectedName) ?? visible[0] ?? null,
[visible, selectedName]
)
const banners = (
<>
{!enabled && (
<Alert className="mb-4" variant="warning">
<Globe />
<AlertTitle>{w.disabledTitle}</AlertTitle>
<AlertDescription>
<p>{w.disabledBody}</p>
<Button className="mt-1" disabled={enabling} onClick={() => void handleEnable()} size="sm">
<Globe />
{enabling ? w.enabling : w.enable}
</Button>
</AlertDescription>
</Alert>
)}
{restartNeeded && (
<Alert className="mb-4" variant="warning">
<AlertTriangle />
<AlertDescription>
<p>{restartError ?? w.restartNeeded}</p>
<Button
className="mt-1"
disabled={restarting}
onClick={() => void restartGatewayNow()}
size="sm"
variant="secondary"
>
<RefreshCw />
{restarting ? w.restartingGateway : w.restartGateway}
</Button>
</AlertDescription>
</Alert>
)}
</>
)
return (
<Panel onClose={onClose}>
{loading ? (
<PageLoader label={w.loading} />
) : subscriptions.length === 0 ? (
<>
{banners}
<PanelEmpty
action={
<Button
disabled={!enabled || enabling}
onClick={() => {
setCreated(null)
setCreateOpen(true)
}}
size="sm"
>
<Plus />
{w.newSubscription}
</Button>
}
description={w.empty}
icon="globe"
/>
</>
) : (
<>
<PanelHeader subtitle={w.hint} title={w.subscriptions(subscriptions.length)} />
{banners}
<PanelBody>
<PanelList
onSearchChange={setQuery}
searchLabel={w.search}
searchPlaceholder={w.search}
searchValue={query}
>
{visible.map(sub => (
<PanelListRow
active={selectedSub?.name === sub.name}
dotClassName={sub.enabled ? 'bg-emerald-500' : 'bg-muted-foreground/50'}
key={sub.name}
menuItems={[
{
icon: sub.enabled ? 'circle-slash' : 'check',
label: sub.enabled ? w.disableRow : w.enableRow,
onSelect: () => void handleToggle(sub.name, !sub.enabled)
},
{ icon: 'trash', label: w.delete, onSelect: () => setPendingDelete(sub.name), tone: 'danger' }
]}
onSelect={() => setSelectedName(sub.name)}
title={sub.name}
/>
))}
{visible.length === 0 && <p className="px-2 py-4 text-center text-xs text-muted-foreground">{w.empty}</p>}
<PanelAddButton
label={w.newSubscription}
onClick={() => {
setCreated(null)
setCreateOpen(true)
}}
/>
</PanelList>
{selectedSub ? <WebhookDetail sub={selectedSub} /> : <PanelEmpty description={w.empty} icon="search" />}
</PanelBody>
</>
)}
{/* Create subscription dialog */}
<Dialog onOpenChange={open => !open && closeCreate()} open={createOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{created ? w.createdTitle : w.newSubscription}</DialogTitle>
{created && <DialogDescription>{w.createdSecretHint}</DialogDescription>}
</DialogHeader>
{created ? (
<div className="grid gap-2">
<ListRow action={<CopyValueRow copyLabel={w.copy} value={created.url} />} title={w.webhookUrl} wide />
<ListRow action={<CopyValueRow copyLabel={w.copy} value={created.secret} />} title={w.secretOnce} wide />
<DialogFooter>
<Button onClick={closeCreate} size="sm">
{w.done}
</Button>
</DialogFooter>
</div>
) : (
<form
className="grid gap-4"
onSubmit={e => {
e.preventDefault()
void handleCreate()
}}
>
<div className="grid items-start gap-4 sm:grid-cols-2">
<Field htmlFor="webhook-name" label={w.fieldName}>
<Input
autoFocus
id="webhook-name"
onChange={e => setName(e.target.value)}
placeholder={w.fieldNamePlaceholder}
value={name}
/>
</Field>
<Field htmlFor="webhook-description" label={w.fieldDescription}>
<Input
id="webhook-description"
onChange={e => setDescription(e.target.value)}
placeholder={w.fieldDescriptionPlaceholder}
value={description}
/>
</Field>
</div>
<Field htmlFor="webhook-prompt" label={w.fieldPrompt}>
<Textarea
className="min-h-24"
id="webhook-prompt"
onChange={e => setPrompt(e.target.value)}
placeholder={w.fieldPromptPlaceholder}
value={prompt}
/>
</Field>
<div className="grid items-start gap-4 sm:grid-cols-2">
<Field htmlFor="webhook-events" label={w.fieldEvents}>
<Input
id="webhook-events"
onChange={e => setEvents(e.target.value)}
placeholder={w.fieldEventsPlaceholder}
value={events}
/>
</Field>
<Field htmlFor="webhook-skills" label={w.fieldSkills}>
<Input
id="webhook-skills"
onChange={e => setSkills(e.target.value)}
placeholder={w.fieldSkillsPlaceholder}
value={skills}
/>
</Field>
</div>
<div className="grid items-start gap-4 sm:grid-cols-2">
<Field htmlFor="webhook-deliver" label={w.fieldDeliver}>
<Select onValueChange={setDeliver} value={deliver}>
<SelectTrigger className="h-9 rounded-md" id="webhook-deliver">
<SelectValue />
</SelectTrigger>
<SelectContent>
{DELIVER_OPTIONS.map(opt => (
<SelectItem key={opt} value={opt}>
{w.deliverOptions[opt] ?? opt}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
<Field htmlFor="webhook-deliver-only" label={w.fieldDeliverOnly}>
<div className="flex h-9 items-center">
<Switch checked={deliverOnly} id="webhook-deliver-only" onCheckedChange={setDeliverOnly} />
</div>
</Field>
</div>
<DialogFooter>
<Button disabled={creating} size="sm" type="submit">
{creating ? w.creating : w.create}
</Button>
</DialogFooter>
</form>
)}
</DialogContent>
</Dialog>
<ConfirmDialog
busyLabel={w.deleting}
cancelLabel={t.common.cancel}
confirmLabel={w.delete}
description={
pendingDelete ? (
<>
{w.deleteDescPrefix}
<span className="font-medium text-foreground">{pendingDelete}</span>
{w.deleteDescSuffix}
</>
) : null
}
destructive
onClose={() => setPendingDelete(null)}
onConfirm={handleDelete}
open={pendingDelete !== null}
title={w.deleteTitle}
/>
</Panel>
)
}
function WebhookDetail({ sub }: { sub: WebhookRoute }) {
const { t } = useI18n()
const w = t.webhooks
return (
<PanelDetail>
<header className="space-y-3">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<h3 className="text-[0.95rem] font-semibold tracking-tight text-foreground">{sub.name}</h3>
{sub.deliver_only && <PanelPill tone="warn">{w.deliverOnly}</PanelPill>}
</div>
<PanelMeta
rows={[
{ label: w.fieldDeliver, value: w.deliverOptions[sub.deliver] ?? sub.deliver },
{
label: w.fieldEvents,
value:
sub.events.length === 0 ? (
w.all
) : (
<span className="flex flex-wrap gap-1">
{sub.events.map(evt => (
<PanelPill key={evt}>{evt}</PanelPill>
))}
</span>
)
},
...(sub.skills.length > 0
? [
{
label: w.fieldSkills,
value: (
<span className="flex flex-wrap gap-1">
{sub.skills.map(skill => (
<PanelPill key={skill}>{skill}</PanelPill>
))}
</span>
)
}
]
: [])
]}
/>
<CopyValueRow copyLabel={w.copy} value={sub.url} />
</header>
{sub.description ? (
<div className="space-y-1.5">
<PanelSectionLabel>{w.fieldDescription}</PanelSectionLabel>
<p className="text-xs leading-relaxed text-foreground/80">{sub.description}</p>
</div>
) : null}
{sub.prompt ? (
<div className="space-y-1.5">
<PanelSectionLabel>{w.fieldPrompt}</PanelSectionLabel>
<PanelBlock>{sub.prompt}</PanelBlock>
</div>
) : null}
</PanelDetail>
)
}