Merge branch 'v2' into oc-resolve-model
This commit is contained in:
commit
b8c548b51b
343 changed files with 16540 additions and 6086 deletions
|
|
@ -49,6 +49,67 @@ export const { use: useClient, provider: ClientProvider } = createSimpleContext(
|
|||
if (history.length > connectionHistoryLimit) history.shift()
|
||||
}
|
||||
|
||||
async function connect(signal: AbortSignal, attempt: number) {
|
||||
let connectedAt: number | undefined
|
||||
|
||||
// Bound the initial handshake and tie this request to the stream lifetime.
|
||||
const request = new AbortController()
|
||||
const cancel = () => request.abort(signal.reason)
|
||||
const timeout = setTimeout(() => request.abort(new Error("Timed out connecting to server")), connectTimeout)
|
||||
signal.addEventListener("abort", cancel, { once: true })
|
||||
|
||||
try {
|
||||
// Open the event stream and validate its initial handshake.
|
||||
record(attempt === 0 ? "connecting" : "reconnecting", attempt)
|
||||
log.info("event stream connecting", { attempt })
|
||||
|
||||
const iterator = api.event.subscribe({ signal: request.signal })[Symbol.asyncIterator]()
|
||||
const first = await iterator.next()
|
||||
|
||||
if (signal.aborted) return { error: undefined, connectedAt }
|
||||
if (first.done) {
|
||||
const error =
|
||||
request.signal.reason instanceof Error ? request.signal.reason : new Error("Event stream disconnected")
|
||||
return { error, connectedAt }
|
||||
}
|
||||
if (first.value.type !== "server.connected")
|
||||
return { error: new Error("Event stream did not start with server.connected"), connectedAt }
|
||||
|
||||
// Publish the connected state before forwarding live events.
|
||||
clearTimeout(timeout)
|
||||
record("connected", attempt)
|
||||
connectedAt = Date.now()
|
||||
log.info("event stream connected")
|
||||
events.emit(first.value.type, first.value)
|
||||
setConnection({ status: "connected", attempt: 0, error: undefined })
|
||||
|
||||
// Forward events until the stream closes or this connection is cancelled.
|
||||
while (!signal.aborted) {
|
||||
const event = await iterator.next()
|
||||
|
||||
if (signal.aborted) return { error: undefined, connectedAt }
|
||||
if (event.done) return { error: new Error("Event stream disconnected"), connectedAt }
|
||||
|
||||
if ("durable" in event.value)
|
||||
log.debug("event", {
|
||||
type: event.value.type,
|
||||
aggregateID: event.value.durable.aggregateID,
|
||||
seq: event.value.durable.seq,
|
||||
})
|
||||
|
||||
events.emit(event.value.type, event.value)
|
||||
}
|
||||
|
||||
return { error: undefined, connectedAt }
|
||||
} catch (error) {
|
||||
return { error, connectedAt }
|
||||
} finally {
|
||||
request.abort()
|
||||
clearTimeout(timeout)
|
||||
signal.removeEventListener("abort", cancel)
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
stream?.abort()
|
||||
const controller = new AbortController()
|
||||
|
|
@ -56,53 +117,11 @@ export const { use: useClient, provider: ClientProvider } = createSimpleContext(
|
|||
void (async () => {
|
||||
let attempt = 0
|
||||
while (!abort.signal.aborted && !controller.signal.aborted) {
|
||||
let connectedAt: number | undefined
|
||||
const request = new AbortController()
|
||||
const cancel = () => request.abort(controller.signal.reason)
|
||||
const timeout = setTimeout(() => request.abort(new Error("Timed out connecting to server")), connectTimeout)
|
||||
controller.signal.addEventListener("abort", cancel, { once: true })
|
||||
const error = await (async () => {
|
||||
record(attempt === 0 ? "connecting" : "reconnecting", attempt)
|
||||
log.info("event stream connecting", { attempt })
|
||||
const iterator = api.event.subscribe({ signal: request.signal })[Symbol.asyncIterator]()
|
||||
const first = await iterator.next()
|
||||
if (abort.signal.aborted || controller.signal.aborted) return undefined
|
||||
if (first.done)
|
||||
return request.signal.reason instanceof Error
|
||||
? request.signal.reason
|
||||
: new Error("Event stream disconnected")
|
||||
if (first.value.type !== "server.connected")
|
||||
return new Error("Event stream did not start with server.connected")
|
||||
clearTimeout(timeout)
|
||||
record("connected", attempt)
|
||||
connectedAt = Date.now()
|
||||
log.info("event stream connected")
|
||||
events.emit(first.value.type, first.value)
|
||||
setConnection({ status: "connected", attempt: 0, error: undefined })
|
||||
while (!abort.signal.aborted && !controller.signal.aborted) {
|
||||
const event = await iterator.next()
|
||||
if (abort.signal.aborted || controller.signal.aborted) return undefined
|
||||
if (event.done) return new Error("Event stream disconnected")
|
||||
if ("durable" in event.value)
|
||||
log.debug("event", {
|
||||
type: event.value.type,
|
||||
aggregateID: event.value.durable.aggregateID,
|
||||
seq: event.value.durable.seq,
|
||||
})
|
||||
events.emit(event.value.type, event.value)
|
||||
}
|
||||
return undefined
|
||||
})()
|
||||
.catch((error) => error)
|
||||
.finally(() => {
|
||||
request.abort()
|
||||
clearTimeout(timeout)
|
||||
controller.signal.removeEventListener("abort", cancel)
|
||||
})
|
||||
const result = await connect(controller.signal, attempt)
|
||||
if (abort.signal.aborted || controller.signal.aborted) return
|
||||
if (connectedAt !== undefined && Date.now() - connectedAt >= 1_000) attempt = 0
|
||||
if (result.connectedAt !== undefined && Date.now() - result.connectedAt >= 1_000) attempt = 0
|
||||
attempt += 1
|
||||
const message = errorMessage(error)
|
||||
const message = errorMessage(result.error)
|
||||
record("disconnected", attempt, message)
|
||||
log.info("event stream disconnected", {
|
||||
attempt,
|
||||
|
|
|
|||
|
|
@ -103,7 +103,8 @@ export function SubagentsTab(props: { sessionID: string }) {
|
|||
setStore("selected", next)
|
||||
const scrollCurrentIntoView = () => scrollToIndex(next, true)
|
||||
scrollCurrentIntoView()
|
||||
requestAnimationFrame(scrollCurrentIntoView)
|
||||
// The remounted scrollbox finishes layout on the next frame and resets its scroll position.
|
||||
requestAnimationFrame(() => requestAnimationFrame(scrollCurrentIntoView))
|
||||
}
|
||||
wasActive = true
|
||||
if (store.selected >= list.length) moveTo(Math.max(0, list.length - 1))
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ function migrateMode(theme: Theme, mode: Mode): FileThemeDefinition {
|
|||
const backgroundPanel = mode === "light" ? "$hue.neutral.300" : "$hue.neutral.700"
|
||||
const backgroundMenu = mode === "light" ? "$hue.neutral.400" : "$hue.neutral.600"
|
||||
|
||||
return {
|
||||
return referenceHues({
|
||||
hue: {
|
||||
gray: neutralScale(theme, mode),
|
||||
...Object.fromEntries(
|
||||
|
|
@ -67,8 +67,8 @@ function migrateMode(theme: Theme, mode: Mode): FileThemeDefinition {
|
|||
return [name, match ? hueScale(match.color, mode) : "$hue.gray"]
|
||||
}),
|
||||
),
|
||||
accent: ambiguous(theme.accent) ? "$hue.gray" : hueScale(theme.accent, mode),
|
||||
interactive: ambiguous(theme.primary) ? "$hue.gray" : hueScale(theme.primary, mode),
|
||||
accent: hues.byToken.accent ? `$hue.${hues.byToken.accent}` : "$hue.gray",
|
||||
interactive: hues.byToken.primary ? `$hue.${hues.byToken.primary}` : "$hue.gray",
|
||||
neutral: "$hue.gray",
|
||||
},
|
||||
categorical: uniqueCategorical.length ? uniqueCategorical : DEFAULT_CATEGORICAL,
|
||||
|
|
@ -176,7 +176,56 @@ function migrateMode(theme: Theme, mode: Mode): FileThemeDefinition {
|
|||
},
|
||||
},
|
||||
"@context:overlay": { background: { default: "$background.surface.overlay" } },
|
||||
})
|
||||
}
|
||||
|
||||
function referenceHues(theme: FileThemeDefinition): FileThemeDefinition {
|
||||
const definitions = theme.hue as Record<string, string | Partial<Record<HueStep, string>>> | undefined
|
||||
if (!definitions) return theme
|
||||
const scales = new Map<string, Partial<Record<HueStep, string>>>()
|
||||
|
||||
function resolve(name: string, chain: string[] = []): Partial<Record<HueStep, string>> | undefined {
|
||||
const cached = scales.get(name)
|
||||
if (cached) return cached
|
||||
if (chain.includes(name)) return
|
||||
const value = definitions?.[name]
|
||||
if (!value) return
|
||||
if (typeof value !== "string") {
|
||||
scales.set(name, value)
|
||||
return value
|
||||
}
|
||||
const target = /^\$hue\.([^.]+)$/.exec(value)?.[1]
|
||||
if (!target) return
|
||||
const scale = resolve(target, [...chain, name])
|
||||
if (scale) scales.set(name, scale)
|
||||
return scale
|
||||
}
|
||||
|
||||
const references = new Map<string, string>()
|
||||
const index = (name: string, overwrite: boolean) => {
|
||||
const scale = resolve(name)
|
||||
if (!scale) return
|
||||
HueStep.literals.forEach((step) => {
|
||||
const color = scale[step]
|
||||
if (!color || (!overwrite && references.has(color.toLowerCase()))) return
|
||||
references.set(color.toLowerCase(), `$hue.${name}.${step}`)
|
||||
})
|
||||
}
|
||||
chromaticHues.forEach((name) => index(name, false))
|
||||
index("gray", false)
|
||||
index("accent", true)
|
||||
index("interactive", true)
|
||||
index("neutral", true)
|
||||
|
||||
function replace(value: unknown): unknown {
|
||||
if (typeof value === "string") return references.get(value.toLowerCase()) ?? value
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return value
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, replace(item)]))
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(theme).map(([key, value]) => [key, key === "hue" || key === "categorical" ? value : replace(value)]),
|
||||
) as FileThemeDefinition
|
||||
}
|
||||
|
||||
function inferHues(theme: Theme, mode: "light" | "dark") {
|
||||
|
|
@ -189,7 +238,7 @@ function inferHues(theme: Theme, mode: "light" | "dark") {
|
|||
["info", theme.info],
|
||||
["secondary", theme.secondary],
|
||||
]
|
||||
return colors.reduce<{
|
||||
const inferred = colors.reduce<{
|
||||
byHue: Partial<Record<ChromaticHue, { color: RGBA; distance: number }>>
|
||||
byToken: Partial<Record<V1HueToken, ChromaticHue>>
|
||||
}>(
|
||||
|
|
@ -207,6 +256,19 @@ function inferHues(theme: Theme, mode: "light" | "dark") {
|
|||
},
|
||||
{ byHue: {}, byToken: {} },
|
||||
)
|
||||
return (
|
||||
[
|
||||
["accent", theme.accent],
|
||||
["primary", theme.primary],
|
||||
] as const
|
||||
).reduce((result, [token, color]) => {
|
||||
const nearest = inferHue(color, mode)
|
||||
if (!nearest) return result
|
||||
return {
|
||||
byHue: { ...result.byHue, [nearest.name]: { color, distance: nearest.distance } },
|
||||
byToken: { ...result.byToken, [token]: nearest.name },
|
||||
}
|
||||
}, inferred)
|
||||
}
|
||||
|
||||
function inferHue(color: RGBA, mode: Mode) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue