Post-merge follow-ups + several review rounds + a hub-search rework, folded together. Merge-scuff restores (a stale-base refactor had reverted two live-on-main fixes): - gateway: SessionStore compression-tip healing + its regression test. - desktop: messaging session/transcript polling in desktop-controller (MESSAGING_POLL / ACTIVE_MESSAGING_SESSION_POLL, refreshMessagingSessions, refreshActiveMessagingTranscript, the richer sameCronSignature) so inbound platform traffic updates live again instead of freezing until manual refresh. Profile-switch isolation (epoch/close/guard on every profile-scoped async): - Hub store clears + in-flight runHubAction bails (and swallows the post-switch 404 instead of a phantom toast); hub preview/scan/search/sources profile-scoped. - MCP: probe/auth epoch guards, dirty-draft reset, sidebar mutations blocked until config resettles AND every persist re-checks the epoch post-await; profilePending clears on config settle incl. error; logs re-key on profile. - Model settings reload on switch and epoch-guard setModelAssignment / saveMoaModels / API-key activation. - Config draft resets + cancels its autosave on switch; skill editor/archive and star-map node dialogs close on switch; openSkillEditor / star-map openEdit discard stale fetches; tool-usage analytics loads are profile-guarded/keyed. Correctness + UX: - Unique per-skill action names for hub install AND uninstall; hub/catalog rows flip only on a clean exit_code; catalog install polls the background bootstrap to completion, reconciles the mcp.json draft (no dropped server), and fails loudly on non-zero exit; MCP catalog query keyed by profile. - /test reports needs-auth for anonymous auth:oauth servers; /auth snapshots + restores tokens on a failed re-auth and clears the full 300s callback window. - config-settings shows a retry on load failure; CodeEditor/JsonDocumentEditor go read-only while saving so edits typed mid-save aren't dropped. - Deep-link highlighter deletes its param only after a successful scroll. - Restored the PageSearchShell trailing slot → Artifacts refresh button/spinner. - /settings?tab=mcp redirect keeps server=. Progressive hub search: fan out one query per backend-searchable source (index-covered API sources stay unsearchable → no ~70-call GitHub re-hammer), merge/dedupe by trust as each lands, per-source spinner overlaid on the dimmed chip — results stream in without blocking on the slowest, no layout shift. test(web): /api/skills list carries usage + provenance (CI contract).
97 lines
3 KiB
TypeScript
97 lines
3 KiB
TypeScript
import type * as React from 'react'
|
|
import { type RefObject, useRef } from 'react'
|
|
|
|
import { CodeEditor, type CodeEditorApi } from '@/components/chat/code-editor'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Codicon } from '@/components/ui/codicon'
|
|
import { Tip } from '@/components/ui/tooltip'
|
|
import { useI18n } from '@/i18n'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
// Kept a string (not a shared CSS utility): the `size-5` prefix lets
|
|
// tailwind-merge override <Button size="icon">'s larger built-in size.
|
|
const ICON_BUTTON =
|
|
'size-5 cursor-pointer rounded-[4px] text-muted-foreground/70 hover:bg-(--ui-control-active-background) hover:text-foreground'
|
|
|
|
interface JsonDocumentEditorProps {
|
|
apiRef?: RefObject<CodeEditorApi | null>
|
|
className?: string
|
|
disabled?: boolean
|
|
filePath?: string
|
|
header?: React.ReactNode
|
|
highlight?: null | { from: number; to: number }
|
|
initialValue: string
|
|
onChange: (value: string) => void
|
|
onCursorChange?: (pos: number) => void
|
|
onFormatJsonError: (error: string) => void
|
|
onSave?: () => void
|
|
remountKey?: number | string
|
|
trailing?: React.ReactNode
|
|
}
|
|
|
|
/** In-memory JSON editor — not for on-disk file previews in the right rail. */
|
|
export function JsonDocumentEditor({
|
|
apiRef,
|
|
className,
|
|
disabled,
|
|
filePath = 'document.json',
|
|
header,
|
|
highlight,
|
|
initialValue,
|
|
onChange,
|
|
onCursorChange,
|
|
onFormatJsonError,
|
|
onSave,
|
|
remountKey,
|
|
trailing
|
|
}: JsonDocumentEditorProps) {
|
|
const { t } = useI18n()
|
|
const localApi = useRef<CodeEditorApi | null>(null)
|
|
const editorApi = apiRef ?? localApi
|
|
|
|
return (
|
|
<div className={cn('flex min-h-0 flex-1 flex-col overflow-hidden', className)}>
|
|
<div className="flex h-8 shrink-0 items-center gap-2 px-3">
|
|
{header ? (
|
|
<span className="flex min-w-0 items-center gap-1.5 text-[0.68rem] text-(--ui-text-tertiary)">{header}</span>
|
|
) : null}
|
|
<div className="ml-auto flex items-center gap-1">
|
|
<Tip label={t.common.formatJson}>
|
|
<Button
|
|
aria-label={t.common.formatJson}
|
|
className={ICON_BUTTON}
|
|
disabled={disabled}
|
|
onClick={() => {
|
|
const result = editorApi.current?.formatJson()
|
|
|
|
if (result && !result.ok) {
|
|
onFormatJsonError(result.error)
|
|
}
|
|
}}
|
|
size="icon"
|
|
variant="ghost"
|
|
>
|
|
<Codicon name="json" size="0.8125rem" />
|
|
</Button>
|
|
</Tip>
|
|
{trailing}
|
|
</div>
|
|
</div>
|
|
<div className="min-h-0 flex-1">
|
|
<CodeEditor
|
|
apiRef={editorApi}
|
|
disabled={disabled}
|
|
filePath={filePath}
|
|
formatJson
|
|
highlight={highlight}
|
|
initialValue={initialValue}
|
|
key={remountKey}
|
|
onChange={onChange}
|
|
onCursorChange={onCursorChange}
|
|
onFormatJsonError={onFormatJsonError}
|
|
onSave={onSave}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|