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

This commit is contained in:
yilsem committed 2026-09-05 13:26:46 +03:00
commit 03634b1ca3
11340 files changed
+3442369

No files matched your search

@@ -0,0 +1,48 @@
import { useQuery } from '@tanstack/react-query'
import { getHermesConfigRecord, type ProfileScope, profileScopeKey } from '@/hermes'
import { queryClient, writeCache } from '@/lib/query-client'
import type { HermesConfigRecord } from '@/types/hermes'
// One shared cache for the whole profile config record (`GET /api/config`).
// Every settings surface (MCP, model, config) reads and writes through this key
// so a save in one shows in the others, and revisiting a tab paints the cache
// instead of blanking on a fresh fetch.
//
// Distinct from session/hooks/use-hermes-config.ts, which is side-effecting —
// it pushes personality/cwd/voice/… into the session stores for live chat.
export const HERMES_CONFIG_KEY = ['hermes-config-record'] as const
// Per-scope cache key. The base key (no suffix) is the app-wide active
// profile, unchanged for every caller that passes nothing. An explicit scope —
// the Capabilities scope selector configuring ANOTHER profile, possibly on
// another registered gateway — gets its own suffixed key so switching the
// selector refetches and never paints stale cross-profile config (the
// AGENTS.md scope-in-key rule). profileScopeKey folds a remote pin's
// connection id into the suffix, so two gateways' same-named profiles never
// share a cache row.
export const hermesConfigKey = (profile?: ProfileScope) =>
profile == null ? HERMES_CONFIG_KEY : ([...HERMES_CONFIG_KEY, profileScopeKey(profile)] as const)
// staleTime 0 → serve cache instantly, background-revalidate on every mount.
// `profile` scopes both the query key and the fetch; omitting it preserves the
// exact app-wide behavior (base key, `profileScoped(undefined)` fallback).
export const useHermesConfigRecord = (profile?: ProfileScope) =>
useQuery({
queryKey: hermesConfigKey(profile),
// null/undefined both mean "no override" → fetch with undefined so
// capabilityScoped falls back to the app-wide active profile (passing null
// would wrongly target the primary backend).
queryFn: () => getHermesConfigRecord(profile ?? undefined),
staleTime: 0
})
// setHermesConfigCache writes the app-wide (base-key) record. Pass a profile to
// write the suffixed per-profile cache instead — keeps the selector's optimistic
// write-through landing on the same key its query reads.
export const setHermesConfigCache = writeCache<HermesConfigRecord>(HERMES_CONFIG_KEY)
export const hermesConfigCacheWriter = (profile?: ProfileScope) =>
writeCache<HermesConfigRecord>(hermesConfigKey(profile))
export const invalidateHermesConfig = (profile?: ProfileScope) =>
queryClient.invalidateQueries({ queryKey: hermesConfigKey(profile) })
@@ -0,0 +1,15 @@
import { useEffect, useState } from 'react'
/** Debounce a fast-changing value (search input, slider, …) so effects/queries
* keyed on it only fire once the value settles. */
export function useDebounced<T>(value: T, delayMs: number): T {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const handle = setTimeout(() => setDebounced(value), delayMs)
return () => clearTimeout(handle)
}, [value, delayMs])
return debounced
}
+472
View File
@@ -0,0 +1,472 @@
import { useEffect, useRef } from 'react'
import { useLocation, useNavigate } from 'react-router'
import { closeActiveTab } from '@/app/chat/close-tab'
import { hudTargetSessionId } from '@/app/hud/handoff'
import { setTerminalTakeover } from '@/app/right-sidebar/store'
import { closeActiveTerminal, createTerminal, cycleTerminal } from '@/app/right-sidebar/terminal/terminals'
import { appViewForPath, isOverlayView } from '@/app/routes'
import {
activateTreeTabSlot,
cycleTreeTabInFocusedZone,
isPaneVisible,
layoutHasRootSide,
togglePaneVisible,
toggleTargetZoneTabStrip
} from '@/components/pane-shell/tree/store'
import { setWorkspaceScope } from '@/components/pane-shell/workspace-scope'
import { onReleaseTypingFocus } from '@/components/ui/keyboard-first'
import { findBarClaimsCombo } from '@/lib/find-in-page'
import { contributedKeybindHandler, PROFILE_SLOT_COUNT, SESSION_SLOT_COUNT } from '@/lib/keybinds/actions'
import { actionAllowedInInput, comboFromEvent, isEditableTarget } from '@/lib/keybinds/combo'
import { composerFocusKeysAllowed, isComposerFocusSoftCombo, typeToFocusChar } from '@/lib/keybinds/composer-focus-keys'
import { openWorktreeDialog } from '@/store/coding-status'
import { $commandPaletteOpen, openCommandPalettePage, toggleCommandPalette } from '@/store/command-palette'
import {
$findInPage,
findNext as findNextMatch,
findPrevious as findPreviousMatch,
openFindBar
} from '@/store/find-in-page'
import { toggleHud } from '@/store/hud'
import { $capture, $comboIndex, endCapture, setBinding } from '@/store/keybinds'
import {
requestSessionSearchFocus,
setFileBrowserOpen,
toggleFileBrowserOpen,
togglePanesFlipped,
toggleSidebarOpen
} from '@/store/layout'
import { openBrowserTab } from '@/store/preview'
import {
$newChatProfile,
cycleProfile,
requestProfileCreate,
switchProfileToSlot,
switchToDefaultProfile,
toggleShowAllProfiles
} from '@/store/profile'
import { openFolderAsProject } from '@/store/projects'
import { toggleReview } from '@/store/review'
import { $selectedStoredSessionId, setModelPickerOpen } from '@/store/session'
import { reopenLastClosedTile } from '@/store/session-states'
import {
$switcherOpen,
closeSwitcher,
commitOnCtrlUp,
onSwitcherTabDown,
onSwitcherTabUp,
openOrAdvanceSwitcher,
slotSessionId,
switcherActive,
switcherJustClosed
} from '@/store/session-switcher'
import { toggleStatusbarVisible } from '@/store/statusbar-prefs'
import { openNewWindow } from '@/store/windows'
import { useTheme } from '@/themes/context'
import { requestComposerFocus, requestModelMenuToggle, requestVoiceToggle } from '../chat/composer/focus'
import { handleComposerFocusChord } from '../chat/composer/focus-chord'
import { handleWindowPaste } from '../chat/composer/paste-to-focus'
import { openSession } from '../open-session'
import {
$workspaceIsPage,
AGENTS_ROUTE,
ARTIFACTS_ROUTE,
CRON_ROUTE,
MESSAGING_ROUTE,
navigateToWorkspacePage,
NEW_CHAT_ROUTE,
PROFILES_ROUTE,
sessionRoute,
SETTINGS_ROUTE,
SKILLS_ROUTE
} from '../routes'
export interface KeybindRuntimeDeps {
/** Open/close the command center overlay (sessions / system / usage). */
toggleCommandCenter: () => void
/** Drop to a fresh new-session draft. */
startFreshSession: () => void
/** Open a fresh session as a tab in the main zone (⌘T), leaving the primary. */
openNewSessionTab: () => void
/** Pin/unpin the active session. */
toggleSelectedPin: () => void
/** Archive the active session. */
archiveSelectedSession: () => void
}
type HandlerMap = Record<string, () => void>
// Mount once near the top of the app. Owns the single global keydown listener
// for every rebindable hotkey: it runs the matched action, or — while capture
// mode is active (edit overlay / panel rebind) — records the pressed combo.
export function useKeybinds(deps: KeybindRuntimeDeps): void {
const navigate = useNavigate()
const location = useLocation()
const { resolvedMode, setMode } = useTheme()
// Keep the latest closures without re-subscribing the listener.
const handlersRef = useRef<HandlerMap>({})
const commitSwitcherRef = useRef<() => void>(() => {})
const profileSwitchHandlers: HandlerMap = {}
// A tab key that lands on the WORKSPACE tab while a full page (skills /
// messaging / artifacts / a plugin route) covers it must also route back to
// the chat: the workspace pane is already the zone's active tab behind the
// page, so fronting it alone changes nothing on screen and the key reads
// dead. Mirrors `openSession`'s full-page rule — only a route change puts
// the chat back.
const leavePageForWorkspaceChat = (paneId: null | string) => {
if (paneId === 'workspace' && $workspaceIsPage.get()) {
const selected = $selectedStoredSessionId.get()
navigate(selected ? sessionRoute(selected) : NEW_CHAT_ROUTE)
}
}
for (let slot = 1; slot <= PROFILE_SLOT_COUNT; slot += 1) {
// ⌘1…⌘9 switch the FOCUSED zone's tab when it's a real tab strip; only a
// single-pane (or unfocused) layout falls through to the profile switch.
profileSwitchHandlers[`profile.switch.${slot}`] = () => {
const pane = activateTreeTabSlot(slot)
if (pane) {
leavePageForWorkspaceChat(pane)
} else {
switchProfileToSlot(slot)
}
}
}
const goToSession = (sessionId: null | string) => {
if (sessionId) {
openSession(sessionId, navigate)
}
}
// ^N jumps straight to the Nth recent session and dismisses the switcher.
const sessionSlotHandlers: HandlerMap = {}
for (let slot = 1; slot <= SESSION_SLOT_COUNT; slot += 1) {
sessionSlotHandlers[`session.slot.${slot}`] = () => {
closeSwitcher()
goToSession(slotSessionId(slot))
}
}
commitSwitcherRef.current = () => goToSession(commitOnCtrlUp())
const stepSession = (direction: 1 | -1) => {
onSwitcherTabDown()
goToSession(openOrAdvanceSwitcher(direction))
}
// ⌃Tab cycles the focused session/main tab strip; only a non-tabbed focus
// falls through to the recent-session switcher. Landing on the workspace
// under a full page routes back to the chat (same as ⌘1).
const cycleTab = (direction: 1 | -1) => {
const pane = cycleTreeTabInFocusedZone(direction)
if (pane) {
leavePageForWorkspaceChat(pane)
} else {
stepSession(direction)
}
}
const showFiles = () => {
setFileBrowserOpen(true)
setTerminalTakeover(false)
}
handlersRef.current = {
'keybinds.openPanel': () => navigate(`${SETTINGS_ROUTE}?tab=keybinds`),
'composer.focus': () => requestComposerFocus('active'),
// Toggle the composer pill's live model dropdown (pane under the pointer,
// else active composer); no chat surface on screen → the full dialog.
'composer.modelPicker': () => {
if (!requestModelMenuToggle()) {
setModelPickerOpen(true)
}
},
'composer.voice': requestVoiceToggle,
// On the Settings overlay, ⌘K scopes to settings search; the second press
// (or Esc) still closes as usual via toggle.
'nav.commandPalette': () => {
if (!$commandPaletteOpen.get() && appViewForPath(location.pathname) === 'settings') {
openCommandPalettePage('settings')
return
}
toggleCommandPalette()
},
'nav.commandCenter': deps.toggleCommandCenter,
'nav.settings': () => navigate(SETTINGS_ROUTE),
'nav.profiles': () => navigate(PROFILES_ROUTE),
'nav.skills': () => navigateToWorkspacePage(navigate, SKILLS_ROUTE),
'nav.messaging': () => navigateToWorkspacePage(navigate, MESSAGING_ROUTE),
'nav.artifacts': () => navigateToWorkspacePage(navigate, ARTIFACTS_ROUTE),
'nav.cron': () => navigate(CRON_ROUTE),
'nav.agents': () => navigate(AGENTS_ROUTE),
'session.new': () => {
// Match the sidebar New Session button. A plain keyboard new chat should
// target the current live profile, not a stale per-profile quick-create
// selection from a prior action.
setWorkspaceScope('sessions')
$newChatProfile.set(null)
deps.startFreshSession()
window.dispatchEvent(new CustomEvent('hermes:new-session-shortcut'))
},
'session.newTab': () => deps.openNewSessionTab(),
'session.newWindow': () => void openNewWindow(),
'session.next': () => cycleTab(1),
'session.prev': () => cycleTab(-1),
...sessionSlotHandlers,
'session.focusSearch': requestSessionSearchFocus,
'session.togglePin': deps.toggleSelectedPin,
'session.archive': deps.archiveSelectedSession,
// openWorktreeDialog resolves the target. There is no test for a repo
// here, so the key works from a detached session that sits inside a
// project, and not only from a session with a repo. When no repo is in
// reach, openWorktreeDialog does nothing.
'workspace.newWorktree': () => void openWorktreeDialog(),
// ⌘O: native folder picker → open the folder as a project (upsert) with a
// fresh session anchored there.
'workspace.openFolder': () => void openFolderAsProject(),
// Narrow-viewport reveal is handled inside the store toggles now.
'view.toggleSidebar': toggleSidebarOpen,
// ⌘J toggles the right sidebar — but a layout with no right side (e.g.
// terminal-on-bottom) would leave it a dead key, so it falls back to the
// terminal there. The single "secondary panel" toggle.
'view.toggleRightSidebar': () =>
layoutHasRootSide('right') ? toggleFileBrowserOpen() : togglePaneVisible('terminal'),
'view.toggleReview': toggleReview,
'view.toggleStatusbar': toggleStatusbarVisible,
'view.toggleTabStrip': () => void toggleTargetZoneTabStrip(),
'view.showFiles': showFiles,
'view.showBrowser': openBrowserTab,
'view.toggleHud': () => toggleHud(hudTargetSessionId()),
'view.showTerminal': () => togglePaneVisible('terminal'),
// Create first so the pane's open-effect ensure sees a non-empty set and
// doesn't also spawn one — net effect is exactly one fresh terminal.
'view.newTerminal': () => {
createTerminal()
setTerminalTakeover(true)
},
// Switch / close only act while the terminal is actually ON SCREEN — ask
// the tree, not the toggle store (which stays true behind a stacked
// sibling tab or a minimized zone).
'view.nextTerminal': () => isPaneVisible('terminal') && cycleTerminal(1),
'view.prevTerminal': () => isPaneVisible('terminal') && cycleTerminal(-1),
'view.closeTerminal': () => isPaneVisible('terminal') && closeActiveTerminal(),
'view.flipPanes': togglePanesFlipped,
// ⌘W: close the focused tab (terminal / preview target / zone tree tab).
// On the main tab with session tabs stacked, it shifts the next one in —
// the loader navigates to that session's route (loads it into main). On
// macOS the menu accelerator owns ⌘W and routes through the same
// closeActiveTab via IPC (see use-desktop-integrations); this binding is
// the Win/Linux path where ⌘W reaches the renderer directly.
'view.closeTab': () => void closeActiveTab(id => navigate(sessionRoute(id))),
'view.reopenTab': reopenLastClosedTile,
'view.findInPage': () => {
// Suppress on overlay routes so it doesn't collide with overlay-specific
// search surfaces (e.g. Settings search bar).
if (!isOverlayView(appViewForPath(location.pathname))) {
openFindBar()
}
},
// ⌘G / ⌘⇧G are handled by the find bar's own capture-phase listener while
// it is open (so they don't collide with `view.toggleReview`). These
// registry handlers cover a user-assigned dedicated chord: stepping is a
// no-op unless the bar is open with a query, so a bound key can't search
// invisibly.
'view.findNext': findNextMatch,
'view.findPrevious': findPreviousMatch,
'appearance.toggleMode': () => setMode(resolvedMode === 'dark' ? 'light' : 'dark'),
'profile.default': switchToDefaultProfile,
...profileSwitchHandlers,
'profile.next': () => cycleProfile(1),
'profile.prev': () => cycleProfile(-1),
'profile.toggleAll': toggleShowAllProfiles,
'profile.create': requestProfileCreate
}
// A keyboard-driven overlay closing hands typing back to the composer: Radix
// restores focus to the trigger (a toolbar button for the model pill), so
// without this the Enter that committed a model also eats the next keystroke.
// Deferred one frame and skipped when something else editable has claimed
// focus, because a palette action can legitimately open a dialog or navigate
// — the release must never steal focus from the surface it just opened.
useEffect(
() =>
onReleaseTypingFocus(() =>
requestAnimationFrame(() => {
if (!isEditableTarget(document.activeElement)) {
requestComposerFocus('active')
}
})
),
[]
)
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
// An active IME composition owns the keyboard. Windows Chinese IMEs
// (Microsoft Pinyin, Sogou) use Ctrl+, as their punctuation-mode toggle,
// so without this guard that keystroke ALSO matched `nav.settings` and
// navigated away mid-word — unmounting the composer and destroying the
// unsent draft (#41079). The draft stash below makes navigation safe;
// this makes the IME keystroke not navigate at all.
if (event.isComposing) {
return
}
// Capture mode: the next real key becomes the binding. Swallow everything
// so e.g. ⌘K rebinds instead of opening the palette.
const capturing = $capture.get()
if (capturing) {
event.preventDefault()
event.stopPropagation()
if (event.key === 'Escape') {
endCapture()
return
}
const combo = comboFromEvent(event)
if (!combo) {
return
}
setBinding(capturing, [combo])
endCapture()
return
}
// While the session switcher is up, Esc abandons it (stay put) before any
// combo dispatch — ⌃Tab keeps stepping through the existing handler.
if (switcherActive() && event.key === 'Escape') {
event.preventDefault()
event.stopPropagation()
closeSwitcher()
return
}
const combo = comboFromEvent(event)
if (!combo) {
return
}
// The open find bar owns ⌘G / ⌘⇧G / Escape. Its own capture-phase
// listener runs those actions; bail here so the registry doesn't ALSO
// fire the action bound to the same combo (⌘G = view.toggleReview,
// Escape = composer.cancel, which would abort a live turn). Both
// listeners are on `window`, so stopPropagation in the bar can't
// suppress this one — the dispatcher has to yield explicitly.
if ($findInPage.get().active && findBarClaimsCombo(combo)) {
return
}
const actionId = $comboIndex.get().get(combo)
// Unbound printable → type-to-focus. Bound chords (shift+n, …) win above.
if (!actionId) {
const typeChar = typeToFocusChar(event)
if (typeChar && composerFocusKeysAllowed(event, 'type')) {
event.preventDefault()
requestComposerFocus('active', { typeChar })
}
return
}
if (isEditableTarget(event.target) && !actionAllowedInInput(actionId, combo)) {
return
}
// Soft `/` / Enter: gated so dialogs/buttons/terminal keep those keys.
// Rebound chords fall through to the normal handler.
if (actionId === 'composer.focus' && isComposerFocusSoftCombo(combo)) {
if (!composerFocusKeysAllowed(event, combo)) {
return
}
event.preventDefault()
requestComposerFocus('active', { typeChar: combo === '/' ? '/' : undefined })
return
}
// Built-in handlers first (they carry React context); contributed
// actions bring their own `run` through the registry.
const handler = handlersRef.current[actionId] ?? contributedKeybindHandler(actionId)
if (!handler) {
return
}
event.preventDefault()
handler()
}
// Mac-app-switcher commit: lifting Ctrl with the overlay open lands on the
// highlighted session. A window blur (Cmd+Tab away mid-switch) cancels so
// the overlay never gets stranded waiting for a keyup that never comes.
const onKeyUp = (event: KeyboardEvent) => {
if (event.key === 'Tab') {
onSwitcherTabUp()
}
if (event.key === 'Control') {
commitSwitcherRef.current()
}
}
const onBlur = () => switcherActive() && closeSwitcher()
// Swallow trailing contextmenu after Ctrl+click commit (Electron main menu).
const onContextMenu = (event: MouseEvent) => {
if ($switcherOpen.get() || switcherJustClosed()) {
event.preventDefault()
event.stopPropagation()
}
}
window.addEventListener('keydown', onKeyDown, { capture: true })
window.addEventListener('keyup', onKeyUp, { capture: true })
window.addEventListener('blur', onBlur)
window.addEventListener('contextmenu', onContextMenu, { capture: true })
// Paste twin of type-to-focus: ⌘V on non-editable chrome routes the
// clipboard (text AND images) into the active composer. Bubble phase so
// editables' own paste handlers run first and mark the event handled.
window.addEventListener('paste', handleWindowPaste)
// ⌘/Ctrl+L moves focus to the composer. Bubble phase so capture-phase
// claimants run first; the priority ladder lives in focus-chord.ts.
window.addEventListener('keydown', handleComposerFocusChord)
return () => {
window.removeEventListener('keydown', onKeyDown, { capture: true })
window.removeEventListener('keyup', onKeyUp, { capture: true })
window.removeEventListener('blur', onBlur)
window.removeEventListener('contextmenu', onContextMenu, { capture: true })
window.removeEventListener('paste', handleWindowPaste)
window.removeEventListener('keydown', handleComposerFocusChord)
}
}, [])
}
@@ -0,0 +1,25 @@
import { useStore } from '@nanostores/react'
import { useEffect, useRef } from 'react'
import { $activeGatewayProfile } from '@/store/profile'
/** Run `onSwitch` when the active gateway profile changes — never on first
* mount. For dropping per-profile view state (probes, cached usage, drafts)
* when the backend the app talks to swaps underneath a still-mounted view. */
export function useOnProfileSwitch(onSwitch: () => void): void {
const profile = useStore($activeGatewayProfile)
const first = useRef(true)
// eslint-disable-next-line no-restricted-syntax -- legitimate non-atom ref write (see eslint rule comment)
useEffect(() => {
if (first.current) {
first.current = false
return
}
onSwitch()
// Fire on profile change only; onSwitch identity is intentionally ignored.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [profile])
}
@@ -0,0 +1,45 @@
import { useEffect, useRef } from 'react'
/**
* Binds the bare `r` key to a refresh action while the calling view is mounted.
* Ignored when a modifier is held, the event repeats, or focus is in an
* editable field (so typing "r" in a search/input never triggers it).
*/
export function useRefreshHotkey(onRefresh: () => void, enabled = true) {
const ref = useRef(onRefresh)
ref.current = onRefresh
useEffect(() => {
if (!enabled) {
return
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'r' && event.key !== 'R') {
return
}
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.repeat) {
return
}
const target = event.target as HTMLElement | null
if (
target?.isContentEditable ||
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement
) {
return
}
event.preventDefault()
ref.current()
}
window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown)
}, [enabled])
}
@@ -0,0 +1,38 @@
import { useCallback, useMemo } from 'react'
import { useLocation, useNavigate } from 'react-router'
// Read/write an enum-shaped URL search param (e.g. ?tab=foo). Used to make
// tabbed views survive a refresh. Always navigates with replace so tab clicks
// don't pile up in history.
export function useRouteEnumParam<T extends string>(
key: string,
values: readonly T[],
fallback: T
): [T, (next: T) => void] {
const { hash, pathname, search } = useLocation()
const navigate = useNavigate()
const value = useMemo<T>(() => {
const raw = new URLSearchParams(search).get(key)
return raw && values.includes(raw as T) ? (raw as T) : fallback
}, [fallback, key, search, values])
const setValue = useCallback(
(next: T) => {
const params = new URLSearchParams(search)
if (next === fallback) {
params.delete(key)
} else {
params.set(key, next)
}
const qs = params.toString()
navigate({ hash, pathname, search: qs ? `?${qs}` : '' }, { replace: true })
},
[fallback, hash, key, navigate, pathname, search]
)
return [value, setValue]
}
@@ -0,0 +1,19 @@
import { useLocation } from 'react-router'
import { appViewForPath, isOverlayView } from '@/app/routes'
/**
* True while a full-screen route overlay (settings, agents, command-center, …)
* is showing.
*
* A portaled Radix modal sits above the app shell, so it would cover such a
* route. Any modal that sends the user to one (e.g. "set up image generation" →
* `/settings`) can `if (useRouteOverlayActive()) return null` to *yield* the
* screen — its open state lives in a store, so it stays open — and reappear,
* re-running its mount effects (a free refresh), when the route overlay closes.
*/
export function useRouteOverlayActive(): boolean {
const { pathname } = useLocation()
return isOverlayView(appViewForPath(pathname))
}