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
@@ -0,0 +1,26 @@
import { useEffect, useState } from 'react'
/**
* Returns true only after `active` has stayed true continuously for `delayMs`.
* Flips back to false the instant `active` goes false. Use it to gate loading
* skeletons so a fast operation doesn't flash one — the UI just stays blank for
* the (sub-perceptible) delay window, and the skeleton appears only when a load
* is genuinely slow.
*/
export function useDelayedTrue(active: boolean, delayMs = 180): boolean {
const [shown, setShown] = useState(false)
useEffect(() => {
if (!active) {
setShown(false)
return
}
const id = window.setTimeout(() => setShown(true), delayMs)
return () => window.clearTimeout(id)
}, [active, delayMs])
return shown
}
+77
View File
@@ -0,0 +1,77 @@
import { type MouseEvent as ReactMouseEvent, type RefObject, useState } from 'react'
// Grab-to-pan for overflow containers — the shared primitive behind "scrub the
// board/timeline by dragging its background" (kanban lanes, trace waterfalls,
// wide tables). Sibling of lib/trackpad-gestures.ts: that file classifies
// wheel gestures, this one owns pointer-drag panning, so surfaces stop
// re-deriving the same interaction (the dashboard kanban and the agent-traces
// waterfall each hand-rolled a copy).
//
// Behavior contract:
// - drags translate scrollLeft/scrollTop (both axes, whichever overflow);
// - interactive targets never start a pan (buttons, inputs, links,
// [draggable] cards keep their own drag semantics);
// - the native scrollbar gutters stay untouched as the fallback affordance;
// - selection can't start mid-pan (preventDefault on move), and window
// blur/mouseup always end it.
const BLOCKED_TARGETS = 'button,input,textarea,select,a,[role="button"],[draggable="true"]'
const SCROLLBAR_GUTTER_PX = 16
export interface GrabScroll {
/** True while a pan is in flight — drive `cursor-grabbing` styling. */
grabbing: boolean
/** Spread onto the scroll container. */
onMouseDown: (event: ReactMouseEvent) => void
}
export function useGrabScroll(ref: RefObject<HTMLElement | null>): GrabScroll {
const [grabbing, setGrabbing] = useState(false)
const onMouseDown = (event: ReactMouseEvent) => {
const el = ref.current
if (event.button !== 0 || !el) {
return
}
const canX = el.scrollWidth > el.clientWidth
const canY = el.scrollHeight > el.clientHeight
if ((!canX && !canY) || (event.target as HTMLElement).closest(BLOCKED_TARGETS)) {
return
}
const rect = el.getBoundingClientRect()
if (
(canX && event.clientY >= rect.bottom - SCROLLBAR_GUTTER_PX) ||
(canY && event.clientX >= rect.right - SCROLLBAR_GUTTER_PX)
) {
return
}
const start = { left: el.scrollLeft, top: el.scrollTop, x: event.clientX, y: event.clientY }
setGrabbing(true)
const onMove = (move: MouseEvent) => {
el.scrollLeft = start.left - (move.clientX - start.x)
el.scrollTop = start.top - (move.clientY - start.y)
move.preventDefault()
}
const stop = () => {
setGrabbing(false)
window.removeEventListener('mousemove', onMove)
window.removeEventListener('mouseup', stop)
window.removeEventListener('blur', stop)
}
window.addEventListener('mousemove', onMove)
window.addEventListener('mouseup', stop, { once: true })
window.addEventListener('blur', stop, { once: true })
event.preventDefault()
}
return { grabbing, onMouseDown }
}
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest'
import { downloadFilename, imageFilename } from './use-image-download'
describe('imageFilename', () => {
it('takes the last path segment of a URL', () => {
expect(imageFilename('https://v3.fal.media/files/kangaroo/pic.png')).toBe('pic.png')
})
it('falls back to "image" when there is no usable segment', () => {
expect(imageFilename('https://example.com/')).toBe('image')
expect(imageFilename(undefined)).toBe('image')
})
})
describe('downloadFilename', () => {
it('keeps a name that already has a known image extension', () => {
expect(downloadFilename('https://example.com/a/photo.jpg', 'image/jpeg')).toBe('photo.jpg')
expect(downloadFilename('https://example.com/a/photo.webp', '')).toBe('photo.webp')
})
it('appends a MIME-derived extension to extensionless content hashes', () => {
expect(downloadFilename('https://v3.fal.media/files/x/MKZV6h-RrKLVCOKp9bGfE_YuJPemAQ', 'image/jpeg')).toBe(
'MKZV6h-RrKLVCOKp9bGfE_YuJPemAQ.jpg'
)
expect(downloadFilename('https://cdn.example.com/abc123', 'image/webp')).toBe('abc123.webp')
})
it('handles MIME parameters and unknown types', () => {
expect(downloadFilename('https://cdn.example.com/abc123', 'image/png; charset=binary')).toBe('abc123.png')
expect(downloadFilename('https://cdn.example.com/abc123', 'application/octet-stream')).toBe('abc123.png')
expect(downloadFilename('https://cdn.example.com/abc123', undefined)).toBe('abc123.png')
})
it('does not treat a dotted hash suffix as an extension', () => {
// A name like "photo.v2" has an extname but not a known image one — the
// MIME extension still gets appended so the OS can open the file.
expect(downloadFilename('https://cdn.example.com/photo.v2', 'image/png')).toBe('photo.v2.png')
})
})
@@ -0,0 +1,116 @@
import { useCallback, useState } from 'react'
import { useI18n } from '@/i18n'
import { notify, notifyError } from '@/store/notifications'
const MIME_EXTENSIONS: Record<string, string> = {
'image/bmp': '.bmp',
'image/gif': '.gif',
'image/jpeg': '.jpg',
'image/png': '.png',
'image/svg+xml': '.svg',
'image/webp': '.webp'
}
const KNOWN_IMAGE_EXTENSION_RE = /\.(?:apng|avif|bmp|gif|ico|jpe?g|png|svg|tiff?|webp)$/i
export function imageFilename(src?: string): string {
if (!src) {
return 'image'
}
try {
return new URL(src, window.location.href).pathname.split('/').filter(Boolean).pop() || 'image'
} catch {
return src.split(/[\\/]/).filter(Boolean).pop() || 'image'
}
}
/** Filename for a browser-anchor download. Generated-image URLs (fal.media
* etc.) often end in an extensionless content hash — without an extension the
* OS save dialog shows "All Files" and the saved file won't open by
* double-click, so append one derived from the blob's MIME type. */
export function downloadFilename(src: string, mimeType?: string): string {
const base = imageFilename(src)
if (KNOWN_IMAGE_EXTENSION_RE.test(base)) {
return base
}
const type = String(mimeType || '')
.split(';')[0]
.trim()
.toLowerCase()
return `${base}${MIME_EXTENSIONS[type] || '.png'}`
}
function isMissingIpcHandler(error: unknown): boolean {
const message = error instanceof Error ? error.message : typeof error === 'string' ? error : ''
return message.includes("No handler registered for 'hermes:saveImageFromUrl'")
}
async function startBrowserDownload(src: string) {
const response = await fetch(src)
if (!response.ok) {
throw new Error(`Could not fetch image: ${response.status}`)
}
const blob = await response.blob()
const blobUrl = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = blobUrl
link.download = downloadFilename(src, blob.type)
link.rel = 'noopener noreferrer'
document.body.appendChild(link)
link.click()
link.remove()
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 30_000)
}
/** Save an image to disk via the desktop IPC bridge, falling back to a browser
* download when the handler is unavailable (older shell / web preview). */
export function useImageDownload(src?: string) {
const { t } = useI18n()
const copy = t.desktop
const [saving, setSaving] = useState(false)
const download = useCallback(async () => {
if (!src || saving) {
return
}
setSaving(true)
try {
if (window.hermesDesktop?.saveImageFromUrl) {
if (await window.hermesDesktop.saveImageFromUrl(src)) {
notify({ kind: 'success', title: copy.imageSaved, message: imageFilename(src) })
}
return
}
await startBrowserDownload(src)
} catch (error) {
if (isMissingIpcHandler(error)) {
try {
await startBrowserDownload(src)
notify({ kind: 'info', title: copy.downloadStarted, message: copy.restartToUseSaveImage })
} catch (fallbackError) {
notifyError(fallbackError, copy.restartToSaveImages)
}
return
}
notifyError(error, copy.imageDownloadFailed)
} finally {
setSaving(false)
}
}, [copy, saving, src])
return { download, saving }
}
+28
View File
@@ -0,0 +1,28 @@
import { useEffect, useState } from 'react'
export const matchesQuery = (query: string) =>
typeof window !== 'undefined' && !!window.matchMedia && window.matchMedia(query).matches
/** Read at call time, not render time: animations check this as they start, and
* the OS setting can flip mid-session. */
export const prefersReducedMotion = () => matchesQuery('(prefers-reduced-motion: reduce)')
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => matchesQuery(query))
useEffect(() => {
if (typeof window === 'undefined' || !window.matchMedia) {
return
}
const mql = window.matchMedia(query)
const onChange = () => setMatches(mql.matches)
setMatches(mql.matches)
mql.addEventListener('change', onChange)
return () => mql.removeEventListener('change', onChange)
}, [query])
return matches
}
+3
View File
@@ -0,0 +1,3 @@
import { useMediaQuery } from './use-media-query'
export const useIsMobile = () => useMediaQuery(`(max-width: ${768 / 16 - 1 / 16}rem)`)
@@ -0,0 +1,134 @@
import { type RefObject, useLayoutEffect, useRef } from 'react'
/**
* Observe element resizes. The callback receives the ResizeObserver entries
* (empty only in non-RO environments) so callers can read the observed size
* off the entry instead of forcing a fresh layout read.
*
* The initial measurement rides the observer's spec-guaranteed first delivery
* (same frame, after layout, before paint) instead of a synchronous call from
* the layout effect. A sync call here runs while the commit's layout is still
* dirty, so any size read in the callback forces a full reflow — and with many
* instances mounting at once (every user bubble on a session switch), the
* interleaved read→write→read pattern cascades into seconds of layout thrash.
* Inside RO timing, layout is already clean and the same reads are ~free.
*
* ONE observer is shared by every caller. A private `new ResizeObserver` per
* hook instance means the browser delivers one callback PER CONSUMER when a
* common ancestor resizes, and each of those is a separate trip through the
* observer machinery. A single shared observer batches the same work into one
* delivery carrying many entries.
*
* Measured on a sash drag with five mounted session tiles (~100 user bubbles):
* 2,600 callbacks across 40 pointermoves — 65 separate callbacks per frame,
* each carrying exactly one entry — and 977ms of script time attributed to
* this file. Batching collapses that to one callback per frame.
*/
type Handler = (entries: readonly ResizeObserverEntry[]) => void
/** Live target → handler routing for the shared observer. */
const handlers = new WeakMap<Element, Set<Handler>>()
let shared: null | ResizeObserver = null
function sharedObserver(): null | ResizeObserver {
if (typeof ResizeObserver === 'undefined') {
return null
}
if (!shared) {
shared = new ResizeObserver(entries => {
// Group this delivery's entries by handler so a caller observing several
// elements is still invoked once, with all of its entries — the same
// contract a private observer gave it.
const byHandler = new Map<Handler, ResizeObserverEntry[]>()
for (const entry of entries) {
const targets = handlers.get(entry.target)
if (!targets) {
continue
}
for (const handler of targets) {
const list = byHandler.get(handler)
if (list) {
list.push(entry)
} else {
byHandler.set(handler, [entry])
}
}
}
for (const [handler, group] of byHandler) {
handler(group)
}
})
}
return shared
}
export function useResizeObserver(
onResize: (entries: readonly ResizeObserverEntry[]) => void,
...refs: readonly RefObject<Element | null>[]
) {
const refsRef = useRef(refs)
refsRef.current = refs
useLayoutEffect(() => {
const observer = sharedObserver()
if (!observer) {
onResize([])
return
}
const observed: Element[] = []
for (const ref of refsRef.current) {
const element = ref.current
if (!element) {
continue
}
const existing = handlers.get(element)
if (existing) {
existing.add(onResize)
} else {
handlers.set(element, new Set([onResize]))
// Only the first handler for an element needs to register it; the
// observer fires once per element regardless of how many care.
observer.observe(element)
}
observed.push(element)
}
if (observed.length === 0) {
return
}
return () => {
for (const element of observed) {
const set = handlers.get(element)
if (!set) {
continue
}
set.delete(onResize)
if (set.size === 0) {
handlers.delete(element)
observer.unobserve(element)
}
}
}
}, [onResize])
}
+32
View File
@@ -0,0 +1,32 @@
import { useEffect, useState } from 'react'
// Theme repaints (themes/context.tsx) toggle `.dark` + rewrite inline custom
// props/data-hermes-* on <html>. Canvas/probe consumers that rasterize the
// *computed* color-mix()/oklch tokens must re-resolve AFTER the paint — useTheme()
// can't, since a child's effect runs before the provider's applyTheme. A
// MutationObserver fires post-mutation, so the next getComputedStyle is fresh.
// One observer, fanned out to every listener.
const ATTRS = ['class', 'style', 'data-hermes-mode', 'data-hermes-theme']
const listeners = new Set<() => void>()
let observer: MutationObserver | null = null
/** Subscribe to theme repaints imperatively (ref/canvas, no re-render). */
export function onThemeRepaint(fn: () => void): () => void {
if (!observer && typeof document !== 'undefined') {
observer = new MutationObserver(() => listeners.forEach(l => l()))
observer.observe(document.documentElement, { attributeFilter: ATTRS, attributes: true })
}
listeners.add(fn)
return () => void listeners.delete(fn)
}
/** A counter that ticks on every theme repaint — depend on it to re-resolve colors. */
export function useThemeEpoch(): number {
const [epoch, setEpoch] = useState(0)
useEffect(() => onThemeRepaint(() => setEpoch(e => e + 1)), [])
return epoch
}
@@ -0,0 +1,59 @@
import { useEffect, useRef } from 'react'
/** Run a UI-only clock while this document is actually being viewed.
*
* macOS can leave an occluded BrowserWindow `visible`, and active streaming
* deliberately disables Chromium's background timer throttling. Pairing focus
* with visibility avoids waking React for elapsed labels nobody can see while
* a leading tick on return catches the UI up immediately.
*/
export function useViewedInterval(callback: () => void, intervalMs: number, enabled = true): void {
const callbackRef = useRef(callback)
// eslint-disable-next-line no-restricted-syntax -- latest-callback ref avoids restarting the interval each render
useEffect(() => {
callbackRef.current = callback
}, [callback])
useEffect(() => {
if (!enabled) {
return
}
let intervalId: null | number = null
const stop = () => {
if (intervalId !== null) {
window.clearInterval(intervalId)
intervalId = null
}
}
const sync = () => {
const viewed = document.visibilityState === 'visible' && document.hasFocus()
if (!viewed) {
stop()
return
}
if (intervalId === null) {
callbackRef.current()
intervalId = window.setInterval(() => callbackRef.current(), intervalMs)
}
}
window.addEventListener('focus', sync)
window.addEventListener('blur', sync)
document.addEventListener('visibilitychange', sync)
sync()
return () => {
stop()
window.removeEventListener('focus', sync)
window.removeEventListener('blur', sync)
document.removeEventListener('visibilitychange', sync)
}
}, [enabled, intervalMs])
}