diff --git a/.github/workflows/publish-images.yml b/.github/workflows/publish-images.yml index 9313f988c..a0be7d200 100644 --- a/.github/workflows/publish-images.yml +++ b/.github/workflows/publish-images.yml @@ -42,10 +42,18 @@ jobs: image: centaur-api dockerfile: services/api/Dockerfile target: "" + - service: api-rs + image: centaur-api-rs + dockerfile: services/api-rs/Dockerfile + target: "" - service: slackbot image: centaur-slackbot dockerfile: services/slackbot/Dockerfile target: "" + - service: web + image: centaur-web + dockerfile: services/web/Dockerfile + target: "" - service: agent image: centaur-agent dockerfile: services/sandbox/Dockerfile diff --git a/Justfile b/Justfile index ad5609483..7b514c519 100644 --- a/Justfile +++ b/Justfile @@ -31,7 +31,7 @@ build: just _build-all-sequential else pids=() - for recipe in _build-api _build-api-rs _build-iron-proxy _build-slackbot _build-slackbotv2 _build-agent; do + for recipe in _build-api _build-api-rs _build-iron-proxy _build-slackbot _build-slackbotv2 _build-web _build-agent; do just "$recipe" & pids+=("$!") done @@ -48,6 +48,7 @@ _build-all-sequential: just _build-iron-proxy just _build-slackbot just _build-slackbotv2 + just _build-web just _build-agent build-one service: @@ -59,6 +60,7 @@ build-one service: iron-proxy) just _build-iron-proxy ;; slackbot) just _build-slackbot ;; slackbotv2) just _build-slackbotv2 ;; + web) just _build-web ;; agent|sandbox) just _build-agent ;; agent-thin|sandbox-thin) just _build-agent-thin ;; *) echo "unknown service: {{service}}" >&2; exit 2 ;; @@ -79,6 +81,9 @@ _build-slackbot: _build-slackbotv2: docker build -t centaur-slackbotv2:latest -f services/slackbotv2/Dockerfile . +_build-web: + docker build -t centaur-web:latest -f services/web/Dockerfile . + _build-agent: docker build --target "{{agent_build_target}}" -t "{{agent_image}}" -f "{{agent_dockerfile}}" . @@ -91,7 +96,7 @@ _build-agent-thin: _push-registry: #!/usr/bin/env bash set -euo pipefail - for img in centaur-api centaur-api-rs centaur-iron-proxy centaur-slackbot centaur-slackbotv2 centaur-agent; do + for img in centaur-api centaur-api-rs centaur-iron-proxy centaur-slackbot centaur-slackbotv2 centaur-web centaur-agent; do target="{{registry}}/library/${img}:latest" echo "pushing ${img}:latest -> ${target}..." docker tag "${img}:latest" "${target}" @@ -104,7 +109,7 @@ _push-registry: _import-k3s: #!/usr/bin/env bash set -euo pipefail - for img in centaur-api centaur-api-rs centaur-iron-proxy centaur-slackbot centaur-slackbotv2 centaur-agent; do + for img in centaur-api centaur-api-rs centaur-iron-proxy centaur-slackbot centaur-slackbotv2 centaur-web centaur-agent; do echo "importing ${img}:latest into k3s containerd..." docker save "${img}:latest" | {{k3s_ctr}} images import - done @@ -126,6 +131,7 @@ deploy: --set ironProxy.image.repository=ghcr.io/paradigmxyz/centaur/centaur-iron-proxy --set slackbot.image.repository=ghcr.io/paradigmxyz/centaur/centaur-slackbot --set slackbotv2.image.repository=ghcr.io/paradigmxyz/centaur/centaur-slackbotv2 + --set web.image.repository=ghcr.io/paradigmxyz/centaur/centaur-web --set sandbox.image.repository=ghcr.io/paradigmxyz/centaur/centaur-agent ) ;; diff --git a/contrib/chart/templates/networkpolicy.yaml b/contrib/chart/templates/networkpolicy.yaml index c368ec8a6..e809676cb 100644 --- a/contrib/chart/templates/networkpolicy.yaml +++ b/contrib/chart/templates/networkpolicy.yaml @@ -1,5 +1,6 @@ {{- if .Values.networkPolicy.enabled }} {{- $ingressSourceNamespaces := default .Values.networkPolicy.ingressControllerNamespaces .Values.networkPolicy.ingressSourceNamespaces }} +{{- $webIngressSourceNamespaces := default $ingressSourceNamespaces .Values.networkPolicy.webIngressSourceNamespaces }} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -105,6 +106,11 @@ spec: - podSelector: matchLabels: {{ include "centaur.componentSelectorLabels" (dict "root" . "component" "slackbotv2") | nindent 14 }} +{{- end }} +{{- if .Values.web.enabled }} + - podSelector: + matchLabels: +{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "web") | nindent 14 }} {{- end }} # Sandboxes call back into the control plane. agent-k8s labels its pods # centaur.ai/managed-by=api-rs (MANAGED_BY_VALUE in centaur-sandbox-agent-k8s), @@ -236,12 +242,20 @@ spec: matchLabels: {{ include "centaur.componentSelectorLabels" (dict "root" . "component" "slackbot") | nindent 14 }} {{- end }} +{{- if .Values.apiRs.enabled }} - podSelector: matchLabels: -{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "iron-proxy") | nindent 14 }} +{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "api-rs") | nindent 14 }} +{{- end }} - podSelector: matchLabels: - centaur.ai/managed: "true" +{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "iron-proxy") | nindent 14 }} + - podSelector: + matchExpressions: + - key: centaur.ai/sandbox-id + operator: Exists + - key: centaur.ai/iron-proxy + operator: DoesNotExist - podSelector: matchLabels: centaur.ai/iron-proxy: "true" @@ -298,6 +312,40 @@ spec: port: {{ .Values.networkPolicy.apiServerPort }} --- {{- end }} +{{- if .Values.web.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "centaur.componentName" (dict "root" . "component" "web") }} + labels: +{{ include "centaur.componentLabels" (dict "root" . "component" "web") | nindent 4 }} +spec: + podSelector: + matchLabels: +{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "web") | nindent 6 }} + policyTypes: + - Ingress + - Egress + ingress: + - from: +{{- range $webIngressSourceNamespaces }} + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ . | quote }} +{{- end }} + ports: + - protocol: TCP + port: 3003 + egress: + - to: + - podSelector: + matchLabels: +{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "api-rs") | nindent 14 }} + ports: + - protocol: TCP + port: {{ .Values.apiRs.port }} +--- +{{- end }} {{- if .Values.slackbot.enabled }} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy @@ -382,6 +430,11 @@ spec: - podSelector: matchLabels: {{ include "centaur.componentSelectorLabels" (dict "root" . "component" "slackbot") | nindent 14 }} +{{- end }} +{{- if .Values.apiRs.enabled }} + - podSelector: + matchLabels: +{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "api-rs") | nindent 14 }} {{- end }} - podSelector: matchLabels: @@ -536,8 +589,11 @@ metadata: {{ include "centaur.labels" . | nindent 4 }} spec: podSelector: - matchLabels: - centaur.ai/managed: "true" + matchExpressions: + - key: centaur.ai/sandbox-id + operator: Exists + - key: centaur.ai/iron-proxy + operator: DoesNotExist policyTypes: - Egress egress: @@ -548,4 +604,12 @@ spec: ports: - protocol: TCP port: 8000 + - to: + - podSelector: + matchLabels: + centaur.ai/iron-proxy: "true" + centaur.ai/sandbox-id: api + ports: + - protocol: TCP + port: {{ .Values.ironProxy.service.proxyPort }} {{- end }} diff --git a/contrib/chart/templates/services.yaml b/contrib/chart/templates/services.yaml index e7169e1bf..f0f665b12 100644 --- a/contrib/chart/templates/services.yaml +++ b/contrib/chart/templates/services.yaml @@ -46,6 +46,22 @@ spec: targetPort: 3001 --- {{- end }} +{{- if .Values.web.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "centaur.componentName" (dict "root" . "component" "web") }} + labels: +{{ include "centaur.componentLabels" (dict "root" . "component" "web") | nindent 4 }} +spec: + selector: +{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "web") | nindent 4 }} + ports: + - name: http + port: 3003 + targetPort: 3003 +--- +{{- end }} {{- if .Values.runnerAccess.enabled }} apiVersion: v1 kind: Service diff --git a/contrib/chart/templates/workloads.yaml b/contrib/chart/templates/workloads.yaml index 9166a4db5..ce78280f0 100644 --- a/contrib/chart/templates/workloads.yaml +++ b/contrib/chart/templates/workloads.yaml @@ -460,6 +460,76 @@ spec: {{- end }} --- {{- end }} +{{- if .Values.web.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "centaur.componentName" (dict "root" . "component" "web") }} + labels: +{{ include "centaur.componentLabels" (dict "root" . "component" "web") | nindent 4 }} +spec: + replicas: {{ .Values.web.replicaCount }} + selector: + matchLabels: +{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "web") | nindent 6 }} + template: + metadata: + annotations: + checksum/secret-env: {{ toJson (dict "secretManager" .Values.secretManager "firewallCa" .Values.firewall.ca) | sha256sum }} + checksum/infra-secrets: {{ include "centaur.infraSecretsChecksum" . }} + labels: +{{ include "centaur.componentSelectorLabels" (dict "root" . "component" "web") | nindent 8 }} + spec: + automountServiceAccountToken: false + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | nindent 8 }} + {{- end }} + containers: + - name: web + image: {{ printf "%s:%s" .Values.web.image.repository .Values.web.image.tag | quote }} + imagePullPolicy: {{ .Values.web.image.pullPolicy }} + env: + - name: CENTAUR_API_RS_URL + value: {{ printf "http://%s:%v" (include "centaur.componentName" (dict "root" . "component" "api-rs")) .Values.apiRs.port | quote }} +{{- if .Values.api.enabled }} + - name: CENTAUR_CONTROL_API_URL + value: {{ printf "http://%s:%v" (include "centaur.componentName" (dict "root" . "component" "api")) 8000 | quote }} +{{- end }} + - name: CENTAUR_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "centaur.secretEnvName" . }} + key: {{ printf "%sSLACKBOT_API_KEY" .Values.secretManager.envPrefix }} +{{- if .Values.web.idleTimeoutMs }} + - name: SESSION_IDLE_TIMEOUT_MS + value: {{ .Values.web.idleTimeoutMs | quote }} +{{- end }} +{{- if .Values.web.maxDurationMs }} + - name: SESSION_MAX_DURATION_MS + value: {{ .Values.web.maxDurationMs | quote }} +{{- end }} +{{- range $name, $value := .Values.web.extraEnv }} + - name: {{ $name }} + value: {{ $value | quote }} +{{- end }} + ports: + - containerPort: 3003 + name: http + readinessProbe: + httpGet: + path: /health + port: 3003 + livenessProbe: + httpGet: + path: /health + port: 3003 + securityContext: +{{ toYaml .Values.containerSecurityContext | nindent 12 }} + resources: +{{ toYaml .Values.web.resources | nindent 12 }} +--- +{{- end }} {{- if .Values.slackbot.enabled }} apiVersion: apps/v1 kind: Deployment diff --git a/contrib/chart/values.dev.yaml b/contrib/chart/values.dev.yaml index d12440b92..d9af41e2c 100644 --- a/contrib/chart/values.dev.yaml +++ b/contrib/chart/values.dev.yaml @@ -21,10 +21,6 @@ slackbotv2: image: pullPolicy: IfNotPresent -apiRs: - image: - pullPolicy: IfNotPresent - api: image: pullPolicy: IfNotPresent @@ -33,19 +29,31 @@ api: egressDiscovery: enabled: false +apiRs: + enabled: true + image: + pullPolicy: IfNotPresent + +web: + enabled: true + image: + pullPolicy: IfNotPresent + ironProxy: image: pullPolicy: IfNotPresent - manager: - secretSource: onepassword + secretSource: env tokenBroker: - enabled: true + enabled: false sandbox: image: pullPolicy: IfNotPresent +agentSandbox: + enabled: true + # Run the tool-server sidecar by default in dev so local clusters mirror # production sandbox topology. toolServer: diff --git a/contrib/chart/values.schema.json b/contrib/chart/values.schema.json index 174622e30..b009b3f50 100644 --- a/contrib/chart/values.schema.json +++ b/contrib/chart/values.schema.json @@ -223,6 +223,10 @@ "type": "array", "items": { "type": "string" } }, + "webIngressSourceNamespaces": { + "type": "array", + "items": { "type": "string" } + }, "apiIngressSourceNamespaces": { "type": "array", "items": { "type": "string" } diff --git a/contrib/chart/values.yaml b/contrib/chart/values.yaml index c1c3ada77..9fd467e81 100644 --- a/contrib/chart/values.yaml +++ b/contrib/chart/values.yaml @@ -293,6 +293,18 @@ slackbotv2: extraEnv: {} resources: {} +web: + enabled: false + replicaCount: 1 + image: + repository: centaur-web + tag: latest + pullPolicy: Always + idleTimeoutMs: "" + maxDurationMs: "" + extraEnv: {} + resources: {} + postgres: enabled: true image: @@ -337,6 +349,7 @@ runnerAccess: networkPolicy: enabled: true ingressSourceNamespaces: [] + webIngressSourceNamespaces: [] apiIngressSourceNamespaces: [] ingressControllerNamespaces: - kube-system diff --git a/packages/rendering/src/codex-app-server.test.ts b/packages/rendering/src/codex-app-server.test.ts index 77deb1fe5..2e3eb8158 100644 --- a/packages/rendering/src/codex-app-server.test.ts +++ b/packages/rendering/src/codex-app-server.test.ts @@ -450,6 +450,27 @@ describe('CodexAppServerRendererEventMapper', () => { error: 'sandbox exited' }) }) + + it('maps Codex turn.failed output lines to renderer errors', () => { + const mapper = new CodexAppServerRendererEventMapper() + + const events = mapper.process({ + eventKind: 'session.output.line', + data: JSON.stringify({ + type: 'turn.failed', + error: { + message: 'Reconnecting... 2/5', + additionalDetails: 'unexpected status 401 Unauthorized' + } + }) + }) + + expect(events.at(-1)).toMatchObject({ + type: 'renderer.done', + error: 'Reconnecting... 2/5\nunexpected status 401 Unauthorized' + }) + expect(mapper.isDone()).toBe(true) + }) }) function plain(elements: RendererTaskBlock[] | undefined): string { diff --git a/packages/rendering/src/codex-app-server.ts b/packages/rendering/src/codex-app-server.ts index 71ed60e83..1018fec78 100644 --- a/packages/rendering/src/codex-app-server.ts +++ b/packages/rendering/src/codex-app-server.ts @@ -330,17 +330,13 @@ export class CodexAppServerRendererEventMapper output: [] }) } - if (!this.state.answerText.trim()) { - this.state.harnessAnswerText += `Execution failed: ${error || 'Execution failed'}` - recomposeBuffers(this.state) - } this.emitActivitySummary(out, { final: true }) this.emitPendingAssistantText(out, { force: true }) + const answerMarkdown = this.state.answerText.trim() ? this.state.answerText : undefined out.push({ type: 'renderer.done', - answerMarkdown: this.state.answerText, + ...(answerMarkdown ? { answerMarkdown, streamFinalUpdates: true } : {}), error, - streamFinalUpdates: true, threadId: this.state.threadId || undefined }) return out @@ -724,7 +720,7 @@ function messageFromError(error: any, message: unknown, fallback: string): strin const message = typeof error.message === 'string' ? error.message : '' const details = typeof error.additionalDetails === 'string' ? error.additionalDetails : '' - if (message && details && !message.includes(details)) return `${message}: ${details}` + if (message && details && !message.includes(details)) return `${message}\n${details}` if (message) return message if (details) return details } diff --git a/packages/rendering/src/index.ts b/packages/rendering/src/index.ts index 1b03f25e7..c8ecdb26d 100644 --- a/packages/rendering/src/index.ts +++ b/packages/rendering/src/index.ts @@ -6,6 +6,7 @@ export { rustSessionEventToServerNotification } from './codex-app-server' export { ChatSDKRenderer } from './chat-sdk' +export { WebRenderer } from './web' export type { CodexAppServerToChatStreamOptions } from './codex-app-server' export type { RendererInterface, RendererSession } from './interface' export { rendererEventTypes } from './schema' @@ -18,6 +19,7 @@ export type { ChatSDKMessageUpsert, ChatSDKSessionClosed } from './chat-sdk' +export type { WebRendererOutput, WebRendererTask } from './web' export type { RendererEvent, RendererLogInfo, diff --git a/packages/rendering/src/web.test.ts b/packages/rendering/src/web.test.ts new file mode 100644 index 000000000..43535fb9a --- /dev/null +++ b/packages/rendering/src/web.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from 'bun:test' +import { WebRenderer } from './web' +import type { RendererInterface } from './interface' + +describe('WebRenderer', () => { + it('implements the generic renderer interface for browser stream outputs', () => { + const renderer: RendererInterface = new WebRenderer() + + expect(renderer.open({ title: 'Execution' })).toEqual([]) + expect( + renderer.render('session-1', { + type: 'renderer.message.delta', + delta: 'Hello', + force: true + }) + ).toEqual([{ type: 'web.message.delta', delta: 'Hello', force: true, planPrefix: undefined }]) + expect( + renderer.close('session-1', { + type: 'renderer.done', + answerMarkdown: 'Done', + threadId: 'thread-1', + streamFinalUpdates: true + }) + ).toEqual([ + { + type: 'web.session.closed', + answerMarkdown: 'Done', + error: undefined, + streamFinalUpdates: true, + threadId: 'thread-1' + } + ]) + }) + + it('converts rich task blocks into markdown strings', () => { + const renderer = new WebRenderer() + + expect( + renderer.render('session-1', { + type: 'renderer.task.update', + task: { + id: 'task-1', + title: 'Run tests', + status: 'complete', + details: [{ type: 'text', text: 'bun test' }], + output: [{ type: 'code', language: 'text', text: 'ok 1' }] + } + }) + ).toEqual([ + { + type: 'web.task.upsert', + flush: undefined, + task: { + id: 'task-1', + title: 'Run tests', + status: 'complete', + details: 'bun test', + output: '```text\nok 1\n```' + } + } + ]) + }) +}) diff --git a/packages/rendering/src/web.ts b/packages/rendering/src/web.ts new file mode 100644 index 000000000..91daab520 --- /dev/null +++ b/packages/rendering/src/web.ts @@ -0,0 +1,100 @@ +import type { RendererEvent, RendererTaskBlock, RendererTaskStatus, RendererTaskUpdate } from './types' +import type { RendererInterface } from './interface' + +export type WebRendererTask = { + id: string + title: string + status: RendererTaskStatus + details?: string + output?: string +} + +export type WebRendererOutput = + | { type: 'web.status.update'; status: string } + | { type: 'web.message.delta'; delta: string; force?: boolean; planPrefix?: boolean } + | { type: 'web.message.snapshot'; markdown: string } + | { type: 'web.task.upsert'; task: WebRendererTask; flush?: boolean } + | { type: 'web.plan.update'; title: string } + | { type: 'web.title.update'; title: string } + | { + type: 'web.session.closed' + answerMarkdown?: string + error?: string + streamFinalUpdates?: boolean + threadId?: string + } + +export class WebRenderer implements RendererInterface { + open(): WebRendererOutput[] { + return [] + } + + render(_sessionId: string, event: RendererEvent): WebRendererOutput[] { + return this.consume(event) + } + + close(_sessionId: string, event?: Extract): WebRendererOutput[] { + return event ? this.consume(event) : [] + } + + consume(event: RendererEvent): WebRendererOutput[] { + if (event.type === 'renderer.session.open') { + return [] + } + if (event.type === 'renderer.status') { + return [{ type: 'web.status.update', status: event.status }] + } + if (event.type === 'renderer.message.delta') { + return [ + { + type: 'web.message.delta', + delta: event.delta, + force: event.force, + planPrefix: event.planPrefix + } + ] + } + if (event.type === 'renderer.message.snapshot') { + return [{ type: 'web.message.snapshot', markdown: event.markdown }] + } + if (event.type === 'renderer.task.update') { + return [{ type: 'web.task.upsert', task: webTask(event.task), flush: event.flush }] + } + if (event.type === 'renderer.plan.update') { + return [{ type: 'web.plan.update', title: event.title }] + } + if (event.type === 'renderer.title.update') { + return [{ type: 'web.title.update', title: event.title }] + } + return [ + { + type: 'web.session.closed', + answerMarkdown: event.answerMarkdown, + error: event.error, + streamFinalUpdates: event.streamFinalUpdates, + threadId: event.threadId + } + ] + } +} + +function webTask(task: RendererTaskUpdate): WebRendererTask { + return { + id: task.id, + title: task.title, + status: task.status, + ...(task.details?.length ? { details: taskBodyToMarkdown(task.details) } : {}), + ...(task.output?.length ? { output: taskBodyToMarkdown(task.output) } : {}) + } +} + +function taskBodyToMarkdown(blocks: RendererTaskBlock[]): string { + return blocks + .map(block => { + if (block.type === 'text') return block.text + const language = block.language ?? '' + return `\`\`\`${language}\n${block.text}\n\`\`\`` + }) + .filter(Boolean) + .join('\n') +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 470e2e666..e1443bcce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,8 +162,171 @@ importers: specifier: ^6.0.3 version: 6.0.3 + services/web: + dependencies: + '@centaur/harness-events': + specifier: workspace:* + version: link:../../packages/harness-events + '@centaur/rendering': + specifier: workspace:* + version: link:../../packages/rendering + hono: + specifier: ^4.12.18 + version: 4.12.19 + lucide-react: + specifier: ^1.14.0 + version: 1.17.0(react@19.2.7) + react: + specifier: ^19.2.0 + version: 19.2.7 + react-dom: + specifier: ^19.2.0 + version: 19.2.7(react@19.2.7) + regen-ui: + specifier: ^0.5.0 + version: 0.5.0(@types/react@19.2.16)(lucide-react@1.17.0(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(vite@8.0.0(@types/node@25.9.0)(jiti@2.6.1)(yaml@2.8.4))(zod@4.4.3) + devDependencies: + '@types/bun': + specifier: ^1.3.13 + version: 1.3.14 + '@types/node': + specifier: ^25.7.0 + version: 25.9.0 + '@types/react': + specifier: ^19.2.0 + version: 19.2.16 + '@types/react-dom': + specifier: ^19.2.0 + version: 19.2.3(@types/react@19.2.16) + '@vitejs/plugin-react': + specifier: ^5.1.0 + version: 5.2.0(vite@8.0.0(@types/node@25.9.0)(jiti@2.6.1)(yaml@2.8.4)) + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.0.0 + version: 8.0.0(@types/node@25.9.0)(jiti@2.6.1)(yaml@2.8.4) + packages: + '@adraffy/ens-normalize@1.11.1': + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@base-ui/react@1.5.0': + resolution: {integrity: sha512-z1gSAlced1yY+iM+mHDEtIkD8UI3Ebs52MuBPxvV6f5hRutk+xvCH/wuB7hDqDzK9JG5FoMz5nhrqtSs1wjt1A==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@date-fns/tz': ^1.2.0 + '@types/react': ^17 || ^18 || ^19 + date-fns: ^4.0.0 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@date-fns/tz': + optional: true + '@types/react': + optional: true + date-fns: + optional: true + + '@base-ui/utils@0.2.9': + resolution: {integrity: sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + '@chat-adapter/shared@4.30.0': resolution: {integrity: sha512-IuYtbn/p1FBXvp7JYGEMLCt07GHOMlyjx7OlZXPJwLTravcyJuP7Q6N31r6c1yubMhM8PLb8eT8l/YnjwYjs9Q==} engines: {node: '>=20'} @@ -189,21 +352,61 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@hono/node-server@1.19.14': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jsr/std__ulid@1.0.0-rc.4': resolution: {integrity: sha512-dJPkGZitKk31EOSyP64SAlTXdGPlzu4CXOBIYwq+0zSeATGMjB4ZVgGjrUqUp/YZEUah6XwX1WLc9krGHqQjPw==, tarball: https://npm.jsr.io/~/11/@jsr/std__ulid/1.0.0-rc.4.tgz} '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.1': + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@opentelemetry/api-logs@0.218.0': resolution: {integrity: sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==} engines: {node: '>=8.0.0'} @@ -630,9 +833,21 @@ packages: cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + '@rolldown/pluginutils@1.0.0-rc.9': resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==} + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@slack/logger@4.0.1': resolution: {integrity: sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==} engines: {node: '>= 18', npm: '>= 8.6.0'} @@ -656,12 +871,114 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + '@total-typescript/ts-reset@0.6.1': resolution: {integrity: sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==} '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bun@1.3.14': resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} @@ -686,6 +1003,14 @@ packages: '@types/node@25.9.0': resolution: {integrity: sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.16': + resolution: {integrity: sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==} + '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} @@ -742,6 +1067,12 @@ packages: engines: {node: '>=16.20.0'} hasBin: true + '@vitejs/plugin-react@5.2.0': + resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + '@vitest/expect@4.1.0': resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} @@ -774,6 +1105,17 @@ packages: '@workflow/serde@4.1.0-beta.2': resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} + abitype@1.2.4: + resolution: {integrity: sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -794,6 +1136,16 @@ packages: bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bun-types@1.3.14: resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==} @@ -801,6 +1153,9 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -834,6 +1189,19 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + cuer@0.0.3: + resolution: {integrity: sha512-f/UNxRMRCYtfLEGECAViByA3JNflZImOk11G9hwSd+44jvzrc99J35u5l+fbdQ2+ZG441GvOpaeGYBmWquZsbQ==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -865,10 +1233,17 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + electron-to-chromium@1.5.364: + resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} + emulate@0.5.0: resolution: {integrity: sha512-2LrOE8sqa1ITQ1aRR3kZhAhOCNz8hu+Kea9oBKdG/jEK6I/RYlMgHbsvQmbTTtr+nx6+fOOWkL6wqvfLeF5vuA==} hasBin: true + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + engines: {node: '>=10.13.0'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -888,6 +1263,10 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -898,6 +1277,9 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -951,6 +1333,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -963,6 +1349,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -998,6 +1387,19 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -1071,6 +1473,14 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@1.17.0: + resolution: {integrity: sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1214,9 +1624,21 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + ox@0.14.27: + resolution: {integrity: sha512-+xhLHo/f+f4BH121/1Pomm/1vgBBda1wYiFpTvjSo8o5OcEj76Pf1hGPJiepoYMTQoTm2SKdSBvWkFWk5l07PA==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + oxfmt@0.49.0: resolution: {integrity: sha512-IAHFMdlJSWe+oAr65dx22UvjCtV9DBMisAuLnKpDqMQrctzCkGnj3QRwNHm0d+uwSWPalsDF8ZYLz9rh6nH2IQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1328,6 +1750,33 @@ packages: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} + qr@0.6.0: + resolution: {integrity: sha512-P23VoX7SipHALdiIYG+D+LT/6n22dNKwV92FAb3d+Nlki/5WisSsfLt0UDFz2XEBtuwrECTznvu+chKKFCSYhA==} + engines: {node: '>= 20.19.0'} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + regen-ui@0.5.0: + resolution: {integrity: sha512-CIirnOq2EJnUzYaZPf0gqG0Vh+L4wPkIZVMqrDfad3db24oyLF2GKiLOzq0I7FQXtGt3pEO/TR69pRmgY2LfXg==} + peerDependencies: + lucide-react: ^1.14.0 + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} @@ -1340,6 +1789,9 @@ packages: remend@1.3.0: resolution: {integrity: sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw==} + reselect@5.2.0: + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -1349,6 +1801,13 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1366,6 +1825,13 @@ packages: std-env@4.0.0: resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1419,6 +1885,17 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -1524,6 +2001,9 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.4: resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} engines: {node: '>= 14.6'} @@ -1537,6 +2017,145 @@ packages: snapshots: + '@adraffy/ens-normalize@1.11.1': {} + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@base-ui/react@1.5.0(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@base-ui/utils': 0.2.9(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@floating-ui/utils': 0.2.11 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + use-sync-external-store: 1.6.0(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + + '@base-ui/utils@0.2.9(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@floating-ui/utils': 0.2.11 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + reselect: 5.2.0 + use-sync-external-store: 1.6.0(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@chat-adapter/shared@4.30.0(zod@4.4.3)': dependencies: chat: 4.30.0(zod@4.4.3) @@ -1593,12 +2212,46 @@ snapshots: tslib: 2.8.1 optional: true + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@floating-ui/utils@0.2.11': {} + '@hono/node-server@1.19.14(hono@4.12.19)': dependencies: hono: 4.12.19 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jsr/std__ulid@1.0.0-rc.4': {} '@napi-rs/wasm-runtime@1.1.1': @@ -1608,6 +2261,14 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + '@opentelemetry/api-logs@0.218.0': dependencies: '@opentelemetry/api': 1.9.1 @@ -1867,8 +2528,23 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': optional: true + '@rolldown/pluginutils@1.0.0-rc.3': {} + '@rolldown/pluginutils@1.0.0-rc.9': {} + '@scure/base@1.2.6': {} + + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@slack/logger@4.0.1': dependencies: '@types/node': 25.9.0 @@ -1911,6 +2587,74 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tailwindcss/node@4.3.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.22.1 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 + + '@tailwindcss/oxide-android-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide@4.3.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/vite@4.3.0(vite@8.0.0(@types/node@25.9.0)(jiti@2.6.1)(yaml@2.8.4))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 8.0.0(@types/node@25.9.0)(jiti@2.6.1)(yaml@2.8.4) + '@total-typescript/ts-reset@0.6.1': {} '@tybys/wasm-util@0.10.1': @@ -1918,6 +2662,27 @@ snapshots: tslib: 2.8.1 optional: true + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + '@types/bun@1.3.14': dependencies: bun-types: 1.3.14 @@ -1945,6 +2710,14 @@ snapshots: dependencies: undici-types: 7.24.6 + '@types/react-dom@19.2.3(@types/react@19.2.16)': + dependencies: + '@types/react': 19.2.16 + + '@types/react@19.2.16': + dependencies: + csstype: 3.2.3 + '@types/retry@0.12.0': {} '@types/unist@3.0.3': {} @@ -1984,6 +2757,18 @@ snapshots: '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260518.1 '@typescript/native-preview-win32-x64': 7.0.0-dev.20260518.1 + '@vitejs/plugin-react@5.2.0(vite@8.0.0(@types/node@25.9.0)(jiti@2.6.1)(yaml@2.8.4))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 8.0.0(@types/node@25.9.0)(jiti@2.6.1)(yaml@2.8.4) + transitivePeerDependencies: + - supports-color + '@vitest/expect@4.1.0': dependencies: '@standard-schema/spec': 1.1.0 @@ -2027,6 +2812,11 @@ snapshots: '@workflow/serde@4.1.0-beta.2': {} + abitype@1.2.4(typescript@6.0.3)(zod@4.4.3): + optionalDependencies: + typescript: 6.0.3 + zod: 4.4.3 + agent-base@6.0.2: dependencies: debug: 4.4.3 @@ -2057,6 +2847,16 @@ snapshots: bail@2.0.2: {} + baseline-browser-mapping@2.10.33: {} + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.364 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + bun-types@1.3.14: dependencies: '@types/node': 25.9.0 @@ -2066,6 +2866,8 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + caniuse-lite@1.0.30001793: {} + ccount@2.0.1: {} chai@6.2.2: {} @@ -2094,6 +2896,16 @@ snapshots: convert-source-map@2.0.0: {} + csstype@3.2.3: {} + + cuer@0.0.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3): + dependencies: + qr: 0.6.0 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + typescript: 6.0.3 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -2118,6 +2930,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + electron-to-chromium@1.5.364: {} + emulate@0.5.0(hono@4.12.19): dependencies: '@hono/node-server': 1.19.14(hono@4.12.19) @@ -2127,6 +2941,11 @@ snapshots: transitivePeerDependencies: - hono + enhanced-resolve@5.22.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -2144,6 +2963,8 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + escalade@3.2.0: {} + escape-string-regexp@5.0.0: {} estree-walker@3.0.3: @@ -2152,6 +2973,8 @@ snapshots: eventemitter3@4.0.7: {} + eventemitter3@5.0.1: {} + eventemitter3@5.0.4: {} eventsource-parser@3.0.6: {} @@ -2181,6 +3004,8 @@ snapshots: function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2201,6 +3026,8 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -2226,8 +3053,13 @@ snapshots: is-stream@2.0.1: {} - jiti@2.6.1: - optional: true + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} lightningcss-android-arm64@1.32.0: optional: true @@ -2280,6 +3112,14 @@ snapshots: longest-streak@3.1.0: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@1.17.0(react@19.2.7): + dependencies: + react: 19.2.7 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2591,8 +3431,25 @@ snapshots: nanoid@3.3.11: {} + node-releases@2.0.46: {} + obug@2.1.1: {} + ox@0.14.27(typescript@6.0.3)(zod@4.4.3): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.4(typescript@6.0.3)(zod@4.4.3) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - zod + oxfmt@0.49.0: dependencies: tinypool: 2.1.0 @@ -2726,6 +3583,35 @@ snapshots: proxy-from-env@2.1.0: {} + qr@0.6.0: {} + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react-refresh@0.18.0: {} + + react@19.2.7: {} + + regen-ui@0.5.0(@types/react@19.2.16)(lucide-react@1.17.0(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)(vite@8.0.0(@types/node@25.9.0)(jiti@2.6.1)(yaml@2.8.4))(zod@4.4.3): + dependencies: + '@base-ui/react': 1.5.0(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tailwindcss/vite': 4.3.0(vite@8.0.0(@types/node@25.9.0)(jiti@2.6.1)(yaml@2.8.4)) + cuer: 0.0.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + lucide-react: 1.17.0(react@19.2.7) + ox: 0.14.27(typescript@6.0.3)(zod@4.4.3) + react: 19.2.7 + optionalDependencies: + react-dom: 19.2.7(react@19.2.7) + transitivePeerDependencies: + - '@date-fns/tz' + - '@types/react' + - date-fns + - typescript + - vite + - zod + remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 @@ -2754,6 +3640,8 @@ snapshots: remend@1.3.0: {} + reselect@5.2.0: {} + retry@0.13.1: {} rolldown@1.0.0-rc.9: @@ -2777,6 +3665,10 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 + scheduler@0.27.0: {} + + semver@6.3.1: {} + siginfo@2.0.0: {} source-map-js@1.2.1: {} @@ -2787,6 +3679,10 @@ snapshots: std-env@4.0.0: {} + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + tinybench@2.9.0: {} tinyexec@1.0.2: {} @@ -2840,6 +3736,16 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.6.0(react@19.2.7): + dependencies: + react: 19.2.7 + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -2901,6 +3807,8 @@ snapshots: xtend@4.0.2: {} + yallist@3.1.1: {} + yaml@2.8.4: {} zod@4.4.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8fb502e05..a1e28065e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - 'packages/*' - 'services/slackbot' - 'services/slackbotv2' + - 'services/web' diff --git a/services/api-rs/Cargo.lock b/services/api-rs/Cargo.lock index 35d087d84..241c38429 100644 --- a/services/api-rs/Cargo.lock +++ b/services/api-rs/Cargo.lock @@ -282,6 +282,7 @@ dependencies = [ "sqlx", "thiserror", "tokio", + "toml", "tracing", "tracing-subscriber", "urlencoding", @@ -2080,7 +2081,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -3883,15 +3884,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -3925,30 +3917,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3961,12 +3936,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3979,12 +3948,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3997,24 +3960,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4027,12 +3978,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4045,12 +3990,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4063,12 +4002,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4081,12 +4014,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.7.15" diff --git a/services/api-rs/crates/centaur-api-server/Cargo.toml b/services/api-rs/crates/centaur-api-server/Cargo.toml index abeb49f16..d93488f37 100644 --- a/services/api-rs/crates/centaur-api-server/Cargo.toml +++ b/services/api-rs/crates/centaur-api-server/Cargo.toml @@ -28,6 +28,7 @@ thiserror.workspace = true tokio = { workspace = true, features = ["macros", "net", "rt-multi-thread", "signal"] } tracing.workspace = true tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] } +toml.workspace = true urlencoding.workspace = true [dev-dependencies] diff --git a/services/api-rs/crates/centaur-api-server/src/error.rs b/services/api-rs/crates/centaur-api-server/src/error.rs index e0e28b695..3df4754c2 100644 --- a/services/api-rs/crates/centaur-api-server/src/error.rs +++ b/services/api-rs/crates/centaur-api-server/src/error.rs @@ -36,6 +36,9 @@ impl IntoResponse for ApiError { Self::Runtime(SessionRuntimeError::Store(SessionStoreError::HarnessConflict { .. })) => StatusCode::CONFLICT, + Self::Runtime(SessionRuntimeError::Store(SessionStoreError::PersonaConflict { + .. + })) => StatusCode::CONFLICT, Self::Runtime(_) | Self::Serialize(_) => StatusCode::INTERNAL_SERVER_ERROR, }; let body = Json(json!({ diff --git a/services/api-rs/crates/centaur-api-server/src/routes.rs b/services/api-rs/crates/centaur-api-server/src/routes.rs index 51a8f44f1..23a94ff45 100644 --- a/services/api-rs/crates/centaur-api-server/src/routes.rs +++ b/services/api-rs/crates/centaur-api-server/src/routes.rs @@ -1,4 +1,9 @@ -use std::{convert::Infallible, convert::TryFrom}; +use std::{ + collections::BTreeMap, + convert::{Infallible, TryFrom}, + fs, + path::{Path as FsPath, PathBuf}, +}; use axum::{ Json, Router, @@ -18,8 +23,10 @@ use serde_json::{Value, json}; use crate::{ ApiError, types::{ - AppendMessagesRequest, AppendMessagesResponse, CreateSessionRequest, EventsQuery, - ExecuteSessionRequest, ExecuteSessionResponse, SessionSseEvent, stream_error_sse, + AppendMessagesRequest, AppendMessagesResponse, CreateSessionRequest, EventLogQuery, + EventsQuery, ExecuteSessionRequest, ExecuteSessionResponse, ListEventsResponse, + ListMessagesResponse, ListPersonasResponse, PersonaRecord, SessionSseEvent, + SetSessionTitleRequest, SetSessionTitleResponse, stream_error_sse, }, }; @@ -35,10 +42,19 @@ pub fn build_router_with_runtime(store: PgSessionStore, sandbox_runtime: Sandbox pub fn build_router_with_session_runtime(runtime: SessionRuntime) -> Router { Router::new() .route("/healthz", get(healthz)) - .route("/api/session/{thread_key}", post(create_or_get_session)) - .route("/api/session/{thread_key}/messages", post(append_messages)) + .route("/api/personas", get(list_personas)) + .route( + "/api/session/{thread_key}", + get(get_session).post(create_or_get_session), + ) + .route( + "/api/session/{thread_key}/messages", + get(list_messages).post(append_messages), + ) .route("/api/session/{thread_key}/execute", post(execute_session)) + .route("/api/session/{thread_key}/event-log", get(list_events)) .route("/api/session/{thread_key}/events", get(stream_events)) + .route("/api/session/{thread_key}/title", post(set_session_title)) .route("/api/sandboxes/drain", post(drain_sandboxes)) .with_state(AppState { runtime }) } @@ -47,6 +63,10 @@ async fn healthz() -> Json { Json(json!({"ok": true})) } +async fn list_personas() -> Json { + Json(discover_personas()) +} + async fn create_or_get_session( State(state): State, Path(raw_thread_key): Path, @@ -55,11 +75,25 @@ async fn create_or_get_session( let thread_key = ThreadKey::try_from(raw_thread_key)?; let session = state .runtime - .create_or_get_session(&thread_key, &request.harness_type, request.metadata) + .create_or_get_session( + &thread_key, + &request.harness_type, + request.persona_id.as_deref(), + request.metadata, + ) .await?; Ok(Json(session)) } +async fn get_session( + State(state): State, + Path(raw_thread_key): Path, +) -> Result, ApiError> { + let thread_key = ThreadKey::try_from(raw_thread_key)?; + let session = state.runtime.get_session(&thread_key).await?; + Ok(Json(session)) +} + async fn append_messages( State(state): State, Path(raw_thread_key): Path, @@ -76,6 +110,15 @@ async fn append_messages( })) } +async fn list_messages( + State(state): State, + Path(raw_thread_key): Path, +) -> Result, ApiError> { + let thread_key = ThreadKey::try_from(raw_thread_key)?; + let messages = state.runtime.list_messages(&thread_key).await?; + Ok(Json(ListMessagesResponse { messages })) +} + async fn execute_session( State(state): State, Path(raw_thread_key): Path, @@ -103,6 +146,33 @@ async fn execute_session( })) } +async fn set_session_title( + State(state): State, + Path(raw_thread_key): Path, + Json(request): Json, +) -> Result, ApiError> { + let thread_key = ThreadKey::try_from(raw_thread_key)?; + let event = state + .runtime + .append_thread_title_update(&thread_key, &request.title, request.metadata) + .await?; + Ok(Json(SetSessionTitleResponse { ok: true, event })) +} + +async fn list_events( + State(state): State, + Path(raw_thread_key): Path, + Query(query): Query, +) -> Result, ApiError> { + let thread_key = ThreadKey::try_from(raw_thread_key)?; + let limit = query.limit.unwrap_or(500).clamp(1, 2_000); + let events = state + .runtime + .list_events_after(&thread_key, query.after_event_id.unwrap_or(0), limit) + .await?; + Ok(Json(ListEventsResponse { events })) +} + async fn drain_sandboxes(State(state): State) -> Result, ApiError> { let report = state.runtime.drain().await?; let failed = report @@ -139,3 +209,94 @@ async fn stream_events( }); Ok(Sse::new(stream).keep_alive(KeepAlive::default())) } + +fn discover_personas() -> ListPersonasResponse { + let mut personas = BTreeMap::new(); + for root in persona_roots() { + collect_personas(&root, &mut personas); + } + personas +} + +fn persona_roots() -> Vec { + if let Ok(root) = std::env::var("CENTAUR_PERSONAS_ROOT") { + return vec![PathBuf::from(root)]; + } + vec![PathBuf::from("tools"), PathBuf::from("/app/tools")] +} + +fn collect_personas(root: &FsPath, personas: &mut ListPersonasResponse) { + let Ok(entries) = fs::read_dir(root) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if !is_visible_dir(&path) { + continue; + } + if path.join("pyproject.toml").exists() { + insert_persona(&path, personas); + continue; + } + let Ok(children) = fs::read_dir(path) else { + continue; + }; + for child in children.flatten() { + let candidate = child.path(); + if is_visible_dir(&candidate) && candidate.join("pyproject.toml").exists() { + insert_persona(&candidate, personas); + } + } + } +} + +fn insert_persona(path: &FsPath, personas: &mut ListPersonasResponse) { + let Some((name, persona)) = load_persona(path) else { + return; + }; + personas.insert(name, persona); +} + +fn load_persona(path: &FsPath) -> Option<(String, PersonaRecord)> { + let pyproject = fs::read_to_string(path.join("pyproject.toml")).ok()?; + let pyproject: toml::Value = pyproject.parse().ok()?; + let project = pyproject.get("project"); + let centaur = pyproject.get("tool")?.get("centaur")?; + if centaur.get("type")?.as_str()? != "persona" { + return None; + } + let name = path.file_name()?.to_str()?.to_owned(); + let description = project + .and_then(|value| value.get("description")) + .and_then(toml::Value::as_str) + .unwrap_or("") + .to_owned(); + let engine = centaur + .get("engine") + .and_then(toml::Value::as_str) + .unwrap_or("amp") + .to_owned(); + let default_repo = centaur + .get("default_repo") + .and_then(toml::Value::as_str) + .map(ToOwned::to_owned); + + Some(( + name, + PersonaRecord { + description, + engine, + default_repo, + has_custom_executor: path.join("run.py").exists(), + }, + )) +} + +fn is_visible_dir(path: &FsPath) -> bool { + if !path.is_dir() { + return false; + } + path.file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| !name.starts_with('.') && !name.starts_with('_')) +} diff --git a/services/api-rs/crates/centaur-api-server/src/types.rs b/services/api-rs/crates/centaur-api-server/src/types.rs index 5fe3cb4ae..aca1ac65b 100644 --- a/services/api-rs/crates/centaur-api-server/src/types.rs +++ b/services/api-rs/crates/centaur-api-server/src/types.rs @@ -1,13 +1,17 @@ use axum::response::sse::Event; -use centaur_session_core::{HarnessType, SessionEvent, SessionMessageInput, ThreadKey}; +use centaur_session_core::{ + HarnessType, SessionEvent, SessionMessage, SessionMessageInput, ThreadKey, +}; use centaur_session_runtime::SESSION_OUTPUT_LINE_EVENT; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::collections::BTreeMap; use thiserror::Error; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct CreateSessionRequest { pub harness_type: HarnessType, + pub persona_id: Option, pub metadata: Option, } @@ -22,6 +26,11 @@ pub struct AppendMessagesResponse { pub message_ids: Vec, } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ListMessagesResponse { + pub messages: Vec, +} + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ExecuteSessionRequest { pub idempotency_key: Option, @@ -40,11 +49,44 @@ pub struct ExecuteSessionResponse { pub status: String, } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SetSessionTitleRequest { + pub title: String, + pub metadata: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SetSessionTitleResponse { + pub ok: bool, + pub event: SessionEvent, +} + #[derive(Clone, Copy, Debug, Deserialize)] pub struct EventsQuery { pub after_event_id: Option, } +#[derive(Clone, Copy, Debug, Deserialize)] +pub struct EventLogQuery { + pub after_event_id: Option, + pub limit: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ListEventsResponse { + pub events: Vec, +} + +#[derive(Clone, Debug, Default, Serialize)] +pub struct PersonaRecord { + pub description: String, + pub engine: String, + pub default_repo: Option, + pub has_custom_executor: bool, +} + +pub type ListPersonasResponse = BTreeMap; + #[derive(Clone, Debug, Eq, PartialEq)] pub enum SessionEventName { OutputLine, diff --git a/services/api-rs/crates/centaur-sandbox-agent-k8s/src/lib.rs b/services/api-rs/crates/centaur-sandbox-agent-k8s/src/lib.rs index 7877c5689..b013fae76 100644 --- a/services/api-rs/crates/centaur-sandbox-agent-k8s/src/lib.rs +++ b/services/api-rs/crates/centaur-sandbox-agent-k8s/src/lib.rs @@ -545,6 +545,12 @@ fn mount_json(spec: &SandboxSpec) -> (Vec, Vec) { "path": source_path, }, }), + MountKind::Secret { secret_name } => json!({ + "name": name, + "secret": { + "secretName": secret_name, + }, + }), }); } (volumes, mounts) @@ -631,6 +637,12 @@ mod tests { .command(["/bin/sh", "-lc"]) .args(["cat"]) .env("CENTAUR_API_URL", "http://api:8000") + .mount(centaur_sandbox_core::Mount::new( + MountKind::Secret { + secret_name: "centaur-firewall-ca".to_owned(), + }, + "/firewall-certs", + )) .mount(centaur_sandbox_core::Mount::new( MountKind::EmptyDir, "/workspace", @@ -658,7 +670,16 @@ mod tests { let container = &sandbox.spec.pod_template.spec.containers[0]; assert_eq!(container.image.as_deref(), Some("centaur-agent:latest")); assert_eq!(container.stdin, Some(true)); - assert_eq!(container.volume_mounts.as_ref().unwrap().len(), 2); + assert_eq!(container.volume_mounts.as_ref().unwrap().len(), 3); + let volumes = sandbox.spec.pod_template.spec.volumes.as_ref().unwrap(); + assert_eq!( + volumes[0].secret.as_ref().unwrap().secret_name.as_deref(), + Some("centaur-firewall-ca") + ); + let volume_mounts = container.volume_mounts.as_ref().unwrap(); + assert_eq!(volume_mounts[0].mount_path, "/firewall-certs"); + assert_eq!(volume_mounts[1].mount_path, "/workspace"); + assert_eq!(volume_mounts[2].mount_path, "/home/agent/state"); assert!(container.resources.as_ref().unwrap().limits.is_some()); } diff --git a/services/api-rs/crates/centaur-sandbox-core/src/spec.rs b/services/api-rs/crates/centaur-sandbox-core/src/spec.rs index f701855af..c6c26282b 100644 --- a/services/api-rs/crates/centaur-sandbox-core/src/spec.rs +++ b/services/api-rs/crates/centaur-sandbox-core/src/spec.rs @@ -108,6 +108,7 @@ pub enum MountKind { EmptyDir, NamedVolume(String), Bind { source_path: String }, + Secret { secret_name: String }, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] diff --git a/services/api-rs/crates/centaur-session-cli/src/main.rs b/services/api-rs/crates/centaur-session-cli/src/main.rs index 1642f796c..f74ec34bf 100644 --- a/services/api-rs/crates/centaur-session-cli/src/main.rs +++ b/services/api-rs/crates/centaur-session-cli/src/main.rs @@ -103,6 +103,7 @@ async fn main() -> Result<()> { &thread_key, CreateSessionRequest { harness_type: args.harness_type.into(), + persona_id: None, metadata: Some(json!({ "source": "centaur-session-cli", })), diff --git a/services/api-rs/crates/centaur-session-core/src/lib.rs b/services/api-rs/crates/centaur-session-core/src/lib.rs index 29a73c2ac..d70331623 100644 --- a/services/api-rs/crates/centaur-session-core/src/lib.rs +++ b/services/api-rs/crates/centaur-session-core/src/lib.rs @@ -143,6 +143,7 @@ pub struct Session { pub sandbox_id: Option, pub harness_type: HarnessType, pub harness_thread_id: Option, + pub persona_id: Option, pub status: SessionStatus, /// iron-control principal OID this session's egress proxy binds to, /// captured at registration so a resumed session can recreate its sandbox. diff --git a/services/api-rs/crates/centaur-session-runtime/src/lib.rs b/services/api-rs/crates/centaur-session-runtime/src/lib.rs index d978ff21b..c72a0af67 100644 --- a/services/api-rs/crates/centaur-session-runtime/src/lib.rs +++ b/services/api-rs/crates/centaur-session-runtime/src/lib.rs @@ -33,7 +33,8 @@ pub const SESSION_OUTPUT_LINE_EVENT: &str = "session.output.line"; const MAX_SESSION_OUTPUT_LINE_BYTES: usize = 1024 * 1024; const EVENT_STREAM_SAFETY_POLL_INTERVAL: Duration = Duration::from_secs(30); -type SandboxSpecFactory = Arc SandboxSpec + Send + Sync>; +type SandboxSpecFactory = + Arc) -> SandboxSpec + Send + Sync>; type SessionInputSink = FramedWrite; #[derive(Clone)] @@ -54,6 +55,7 @@ pub struct SandboxRuntime { pub enum SandboxWorkloadMode { MockAppServer { image: String, + mounts: Vec, }, CodexAppServer { image: String, @@ -121,6 +123,7 @@ impl SessionRuntime { &self, thread_key: &ThreadKey, harness_type: &HarnessType, + persona_id: Option<&str>, metadata: Option, ) -> Result { // Read slack_user_id before `metadata` is consumed below; it keys the @@ -132,7 +135,12 @@ impl SessionRuntime { .map(ToOwned::to_owned); let session = self .store - .create_or_get_session(thread_key, harness_type, default_metadata(metadata)) + .create_or_get_session( + thread_key, + harness_type, + persona_id, + default_metadata(metadata), + ) .await?; if let Some(registrar) = &self.iron_control { // iron-control is the source of truth for the session's egress @@ -152,6 +160,13 @@ impl SessionRuntime { Ok(session) } + pub async fn get_session( + &self, + thread_key: &ThreadKey, + ) -> Result { + Ok(self.store.get_session(thread_key).await?) + } + pub async fn append_messages( &self, thread_key: &ThreadKey, @@ -196,6 +211,13 @@ impl SessionRuntime { Ok(report) } + pub async fn list_messages( + &self, + thread_key: &ThreadKey, + ) -> Result, SessionRuntimeError> { + Ok(self.store.list_messages(thread_key).await?) + } + pub async fn execute_session( &self, thread_key: &ThreadKey, @@ -229,6 +251,8 @@ impl SessionRuntime { session.sandbox_id.as_deref(), session.iron_control_principal.as_deref(), &execution.execution_id, + &session.harness_type, + session.persona_id.as_deref(), ) .await?; @@ -241,6 +265,7 @@ impl SessionRuntime { "execution_id": execution.execution_id, "thread_key": thread_key.as_str(), "input_line_count": input.input_lines.len(), + "metadata": execution.metadata.clone(), }), ) .await?; @@ -284,6 +309,7 @@ impl SessionRuntime { "execution_id": execution.execution_id, "thread_key": thread_key.as_str(), "completion_reason": "input_accepted", + "metadata": execution.metadata.clone(), }), ) .await?; @@ -294,6 +320,37 @@ impl SessionRuntime { .await?) } + pub async fn append_thread_title_update( + &self, + thread_key: &ThreadKey, + title: &str, + metadata: Option, + ) -> Result { + self.store.get_session(thread_key).await?; + let title = title.trim(); + if title.is_empty() { + return Err(SessionRuntimeError::BadRequest( + "title must not be empty".to_owned(), + )); + } + Ok(self + .store + .append_event( + thread_key, + None, + SESSION_OUTPUT_LINE_EVENT, + Value::String( + json!({ + "type": "thread/name/updated", + "name": title, + "metadata": default_metadata(metadata), + }) + .to_string(), + ), + ) + .await?) + } + pub async fn stream_events( &self, thread_key: &ThreadKey, @@ -317,12 +374,27 @@ impl SessionRuntime { )) } + pub async fn list_events_after( + &self, + thread_key: &ThreadKey, + after_event_id: i64, + limit: i64, + ) -> Result, SessionRuntimeError> { + self.store.get_session(thread_key).await?; + Ok(self + .store + .list_events_after(thread_key, after_event_id, limit) + .await?) + } + async fn ensure_session_sandbox( &self, thread_key: &ThreadKey, existing_sandbox_id: Option<&str>, iron_control_principal: Option<&str>, execution_id: &str, + harness_type: &HarnessType, + persona_id: Option<&str>, ) -> Result { if let Some(sandbox_id) = existing_sandbox_id { let id = SandboxId::new(sandbox_id); @@ -335,7 +407,8 @@ impl SessionRuntime { } } - let mut spec = (self.sandbox_runtime.spec_factory)(thread_key, execution_id); + let mut spec = + (self.sandbox_runtime.spec_factory)(thread_key, execution_id, harness_type, persona_id); if let Some(principal) = iron_control_principal { spec.iron_control_principal = Some(principal.to_owned()); } @@ -413,7 +486,11 @@ impl SessionRuntime { impl SandboxRuntime { pub fn backend(backend: Arc, spec: SandboxSpec) -> Self { - let spec_factory = move |_thread_key: &ThreadKey, _execution_id: &str| spec.clone(); + let spec_factory = + move |_thread_key: &ThreadKey, + _execution_id: &str, + _harness_type: &HarnessType, + _persona_id: Option<&str>| { spec.clone() }; Self::backend_with_spec_factory(backend, spec_factory) } @@ -421,14 +498,17 @@ impl SandboxRuntime { backend: Arc, workload: SandboxWorkloadMode, ) -> Self { - Self::backend_with_spec_factory(backend, move |thread_key, _execution_id| { - workload.spec(thread_key) - }) + Self::backend_with_spec_factory( + backend, + move |thread_key, _execution_id, harness_type, persona_id| { + workload.spec(thread_key, harness_type, persona_id) + }, + ) } pub fn backend_with_spec_factory(backend: Arc, spec_factory: F) -> Self where - F: Fn(&ThreadKey, &str) -> SandboxSpec + Send + Sync + 'static, + F: Fn(&ThreadKey, &str, &HarnessType, Option<&str>) -> SandboxSpec + Send + Sync + 'static, { Self { manager: Arc::new(SandboxManager::new(backend)), @@ -441,6 +521,7 @@ impl SandboxWorkloadMode { pub fn mock_app_server(image: impl Into) -> Self { Self::MockAppServer { image: image.into(), + mounts: Vec::new(), } } @@ -457,29 +538,53 @@ impl SandboxWorkloadMode { pub fn mount(mut self, mount: Mount) -> Self { match &mut self { - Self::MockAppServer { .. } => {} - Self::CodexAppServer { mounts, .. } => mounts.push(mount), + Self::MockAppServer { mounts, .. } | Self::CodexAppServer { mounts, .. } => { + mounts.push(mount); + } } self } - fn spec(&self, thread_key: &ThreadKey) -> SandboxSpec { - match self { - Self::MockAppServer { image } => SandboxSpec::new(image) + fn spec( + &self, + thread_key: &ThreadKey, + harness_type: &HarnessType, + persona_id: Option<&str>, + ) -> SandboxSpec { + let spec = match self { + Self::MockAppServer { image, .. } => SandboxSpec::new(image) .command(["/bin/sh", "-lc"]) .args([mock_app_server_script()]), - Self::CodexAppServer { image, env, mounts } => { - let mut spec = - SandboxSpec::new(image).env("CENTAUR_THREAD_KEY", thread_key.as_str()); - for mount in mounts { - spec = spec.mount(mount.clone()); + Self::CodexAppServer { image, env, .. } => { + let mut spec = SandboxSpec::new(image) + .args([harness_wrapper(harness_type)]) + .env("CENTAUR_THREAD_KEY", thread_key.as_str()) + .env("CENTAUR_HARNESS_TYPE", harness_type.as_ref()); + if let Some(persona_id) = persona_id { + spec = spec.env("AGENT_PERSONA", persona_id); } for (name, value) in env { spec = spec.env(name.clone(), value.clone()); } spec } + }; + let mounts = match self { + Self::MockAppServer { mounts, .. } | Self::CodexAppServer { mounts, .. } => mounts, + }; + let mut spec = spec; + for mount in mounts { + spec = spec.mount(mount.clone()); } + spec + } +} + +fn harness_wrapper(harness_type: &HarnessType) -> &'static str { + match harness_type { + HarnessType::Codex => "codex-app-wrapper", + HarnessType::Amp => "amp-wrapper", + HarnessType::ClaudeCode => "claude-app-wrapper", } } @@ -705,7 +810,7 @@ mod tests { ); let thread_key = ThreadKey::parse("chat:C123:1780000000.000000").unwrap(); - let spec = workload.spec(&thread_key); + let spec = workload.spec(&thread_key, &HarnessType::Codex, None); assert_eq!(spec.mounts.len(), 1); assert_eq!(spec.mounts[0].target_path, "/home/agent/github"); @@ -717,6 +822,38 @@ mod tests { } ); } + + #[test] + fn codex_app_workload_selects_wrapper_from_session_harness() { + let workload = SandboxWorkloadMode::codex_app_server( + "centaur-agent:latest", + [("CENTAUR_API_URL".to_owned(), "http://api:8000".to_owned())], + ); + let thread_key = ThreadKey::parse("web:test-thread").unwrap(); + + for (harness_type, expected_wrapper, expected_wire_value) in [ + (HarnessType::Codex, "codex-app-wrapper", "codex"), + (HarnessType::ClaudeCode, "claude-app-wrapper", "claudecode"), + (HarnessType::Amp, "amp-wrapper", "amp"), + ] { + let spec = workload.spec(&thread_key, &harness_type, Some("eng")); + + assert_eq!(spec.args, vec![expected_wrapper]); + assert!( + spec.env + .iter() + .any(|env| env.name == "CENTAUR_THREAD_KEY" && env.value == "web:test-thread") + ); + assert!(spec.env.iter().any(|env| { + env.name == "CENTAUR_HARNESS_TYPE" && env.value == expected_wire_value + })); + assert!( + spec.env + .iter() + .any(|env| env.name == "AGENT_PERSONA" && env.value == "eng") + ); + } + } } #[derive(Debug, Error)] diff --git a/services/api-rs/crates/centaur-session-sqlx/migrations/0005_session_persona_id.sql b/services/api-rs/crates/centaur-session-sqlx/migrations/0005_session_persona_id.sql new file mode 100644 index 000000000..7b57bd8ae --- /dev/null +++ b/services/api-rs/crates/centaur-session-sqlx/migrations/0005_session_persona_id.sql @@ -0,0 +1,6 @@ +alter table sessions + add column if not exists persona_id text; + +alter table sessions + add constraint sessions_persona_id_len + check (persona_id is null or octet_length(persona_id) between 1 and 128); diff --git a/services/api-rs/crates/centaur-session-sqlx/src/lib.rs b/services/api-rs/crates/centaur-session-sqlx/src/lib.rs index 84bdf492d..8ff9e6a8c 100644 --- a/services/api-rs/crates/centaur-session-sqlx/src/lib.rs +++ b/services/api-rs/crates/centaur-session-sqlx/src/lib.rs @@ -63,17 +63,19 @@ impl PgSessionStore { &self, thread_key: &ThreadKey, harness_type: &HarnessType, + persona_id: Option<&str>, metadata: Value, ) -> Result { sqlx::query( r#" - insert into sessions (thread_key, harness_type, status, metadata) - values ($1, $2, $3, $4) + insert into sessions (thread_key, harness_type, persona_id, status, metadata) + values ($1, $2, $3, $4, $5) on conflict (thread_key) do nothing "#, ) .bind(thread_key.as_str()) .bind(harness_type.as_ref()) + .bind(persona_id) .bind(SessionStatus::Idle.as_ref()) .bind(metadata) .execute(&self.pool) @@ -87,13 +89,20 @@ impl PgSessionStore { requested: harness_type.as_ref().to_owned(), }); } + if session.persona_id.as_deref() != persona_id { + return Err(SessionStoreError::PersonaConflict { + thread_key: thread_key.as_str().to_owned(), + existing: session.persona_id, + requested: persona_id.map(str::to_owned), + }); + } Ok(session) } pub async fn get_session(&self, thread_key: &ThreadKey) -> Result { let row = sqlx::query_as::<_, SessionRow>( r#" - select thread_key, sandbox_id, harness_type, harness_thread_id, status, iron_control_principal, created_at, updated_at + select thread_key, sandbox_id, harness_type, harness_thread_id, persona_id, status, iron_control_principal, created_at, updated_at from sessions where thread_key = $1 "#, @@ -345,7 +354,7 @@ impl PgSessionStore { update sessions set sandbox_id = $2, updated_at = now() where thread_key = $1 - returning thread_key, sandbox_id, harness_type, harness_thread_id, status, iron_control_principal, created_at, updated_at + returning thread_key, sandbox_id, harness_type, harness_thread_id, persona_id, status, iron_control_principal, created_at, updated_at "#, ) .bind(thread_key.as_str()) @@ -366,7 +375,7 @@ impl PgSessionStore { update sessions set iron_control_principal = $2, updated_at = now() where thread_key = $1 - returning thread_key, sandbox_id, harness_type, harness_thread_id, status, iron_control_principal, created_at, updated_at + returning thread_key, sandbox_id, harness_type, harness_thread_id, persona_id, status, iron_control_principal, created_at, updated_at "#, ) .bind(thread_key.as_str()) @@ -387,7 +396,7 @@ impl PgSessionStore { update sessions set harness_thread_id = $2, updated_at = now() where thread_key = $1 - returning thread_key, sandbox_id, harness_type, harness_thread_id, status, iron_control_principal, created_at, updated_at + returning thread_key, sandbox_id, harness_type, harness_thread_id, persona_id, status, iron_control_principal, created_at, updated_at "#, ) .bind(thread_key.as_str()) @@ -460,6 +469,14 @@ pub enum SessionStoreError { existing: String, requested: String, }, + #[error( + "session {thread_key} already exists with persona_id {existing:?}, requested {requested:?}" + )] + PersonaConflict { + thread_key: String, + existing: Option, + requested: Option, + }, #[error("invalid persisted value: {0}")] InvalidPersistedValue(String), #[error("invalid notification payload on {channel}: {payload}: {error}")] @@ -480,6 +497,7 @@ struct SessionRow { sandbox_id: Option, harness_type: String, harness_thread_id: Option, + persona_id: Option, status: String, iron_control_principal: Option, created_at: OffsetDateTime, @@ -495,6 +513,7 @@ impl TryFrom for Session { sandbox_id: row.sandbox_id, harness_type: parse_persisted(row.harness_type)?, harness_thread_id: row.harness_thread_id, + persona_id: row.persona_id, status: parse_persisted(row.status)?, iron_control_principal: row.iron_control_principal, created_at: row.created_at, diff --git a/services/web/Dockerfile b/services/web/Dockerfile new file mode 100644 index 000000000..7862ccd99 --- /dev/null +++ b/services/web/Dockerfile @@ -0,0 +1,34 @@ +FROM oven/bun:1.3.13-slim AS builder + +WORKDIR /repo +ENV NODE_ENV=production + +RUN bun install -g pnpm@10.28.1 + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY packages/harness-events/package.json packages/harness-events/package.json +COPY packages/rendering/package.json packages/rendering/package.json +COPY services/web/package.json services/web/package.json +RUN pnpm install --filter centaur-web... --frozen-lockfile + +COPY packages/harness-events/ packages/harness-events/ +COPY packages/rendering/ packages/rendering/ +COPY services/web/ services/web/ +RUN pnpm --filter centaur-web build + +FROM oven/bun:1.3.13-slim + +WORKDIR /repo +ENV NODE_ENV=production + +COPY --from=builder /repo/node_modules ./node_modules +COPY --from=builder /repo/packages/harness-events ./packages/harness-events +COPY --from=builder /repo/packages/rendering ./packages/rendering +COPY --from=builder /repo/services/web ./services/web +COPY docs/public/brand/ ./services/web/dist/client/brand/ + +WORKDIR /repo/services/web + +EXPOSE 3003 +ENV PORT=3003 +CMD ["bun", "src/server.ts"] diff --git a/services/web/index.html b/services/web/index.html new file mode 100644 index 000000000..f7f1ea318 --- /dev/null +++ b/services/web/index.html @@ -0,0 +1,14 @@ + + + + + + + + Centaur Web + + +
+ + + diff --git a/services/web/package.json b/services/web/package.json new file mode 100644 index 000000000..b8ae8e678 --- /dev/null +++ b/services/web/package.json @@ -0,0 +1,31 @@ +{ + "name": "centaur-web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite --host 0.0.0.0", + "start": "bun src/server.ts", + "test": "bun test test", + "check:types": "tsc --noEmit" + }, + "dependencies": { + "@centaur/harness-events": "workspace:*", + "@centaur/rendering": "workspace:*", + "hono": "^4.12.18", + "lucide-react": "^1.14.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "regen-ui": "^0.5.0" + }, + "devDependencies": { + "@types/bun": "^1.3.13", + "@types/node": "^25.7.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.1.0", + "typescript": "^6.0.3", + "vite": "^8.0.0" + } +} diff --git a/services/web/src/app.ts b/services/web/src/app.ts new file mode 100644 index 000000000..e49db48ce --- /dev/null +++ b/services/web/src/app.ts @@ -0,0 +1,127 @@ +import { Hono } from 'hono' +import { serveStatic } from 'hono/bun' +import { + generateMissingWebThreadTitle, + loadWebPersonas, + loadWebThread, + streamWebTurn +} from './session-api' +import type { CentaurWebOptions, WebTurnRequest, WebTurnStreamItem } from './types' + +const encoder = new TextEncoder() +const WEB_THREAD_UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + +export function createCentaurWebApp(options: CentaurWebOptions): Hono { + const app = new Hono() + + app.get('/health', c => c.json({ ok: true })) + app.get('/healthz', c => c.json({ ok: true })) + + app.get('/api/personas', async c => { + const personas = await loadWebPersonas(options) + return c.json({ personas }) + }) + + app.get('/api/threads/:thread_uuid', async c => { + const threadUuid = c.req.param('thread_uuid').trim() + if (!WEB_THREAD_UUID_PATTERN.test(threadUuid)) { + return c.json({ error: 'Invalid thread UUID' }, 400) + } + + try { + const thread = await loadWebThread(options, `web:${threadUuid}`) + if (!thread) return c.json({ error: 'Thread not found' }, 404) + return c.json(thread) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + options.logger?.error('centaur_web_thread_load_failed', { + error: message, + thread_id: `web:${threadUuid}` + }) + return c.json({ error: message }, 502) + } + }) + + app.post('/api/threads/:thread_uuid/title', async c => { + const threadUuid = c.req.param('thread_uuid').trim() + if (!WEB_THREAD_UUID_PATTERN.test(threadUuid)) { + return c.json({ error: 'Invalid thread UUID' }, 400) + } + + try { + const title = await generateMissingWebThreadTitle(options, `web:${threadUuid}`) + if (!title) return c.json({ error: 'Thread not found or has no messages' }, 404) + return c.json(title) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + options.logger?.error('centaur_web_thread_title_failed', { + error: message, + thread_id: `web:${threadUuid}` + }) + return c.json({ error: message }, 502) + } + }) + + app.post('/api/chat', async c => { + let input: WebTurnRequest + try { + input = (await c.req.json()) as WebTurnRequest + } catch { + return c.json({ error: 'Invalid JSON body' }, 400) + } + + return new Response(createTurnStream(options, input), { + headers: { + 'cache-control': 'no-cache', + 'content-type': 'text/event-stream; charset=utf-8', + 'x-accel-buffering': 'no' + } + }) + }) + + app.use('/assets/*', serveStatic({ root: './dist/client' })) + app.use('/brand/*', serveStatic({ root: './dist/client' })) + app.get('/favicon.svg', serveStatic({ path: './dist/client/brand/mark-white.svg' })) + app.get('*', serveStatic({ path: './dist/client/index.html' })) + + return app +} + +function createTurnStream( + options: CentaurWebOptions, + input: WebTurnRequest +): ReadableStream { + return new ReadableStream({ + async start(controller) { + try { + for await (const item of streamWebTurn(options, input)) { + controller.enqueue(encoder.encode(sseItem(item))) + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + options.logger?.error('centaur_web_turn_failed', { + error: message, + thread_id: input.threadId ?? input.threadKey ?? input.thread_key + }) + controller.enqueue( + encoder.encode( + sseItem({ + output: { + type: 'web.session.closed', + error: message + } + }) + ) + ) + } finally { + controller.close() + } + } + }) +} + +function sseItem(item: WebTurnStreamItem): string { + const id = item.eventId === undefined ? '' : `id: ${item.eventId}\n` + return `${id}event: ${item.output.type}\ndata: ${JSON.stringify(item.output)}\n\n` +} diff --git a/services/web/src/client/App.tsx b/services/web/src/client/App.tsx new file mode 100644 index 000000000..fe9edcff4 --- /dev/null +++ b/services/web/src/client/App.tsx @@ -0,0 +1,922 @@ +import { Fragment, useEffect, useRef, useState } from 'react' +import type { RefObject } from 'react' +import { ArrowUp } from 'lucide-react' +import { Select } from 'regen-ui' +import type { WebRendererOutput, WebRendererTask } from '@centaur/rendering' +import type { LoadedWebThread, WebPersonaOption } from '../types' + +type ChatMessage = { + id: string + role: 'assistant' | 'user' + tasks?: WebRendererTask[] + text: string +} + +type ThreadSummary = { + activeAssistantMessageIds: string[] + harnessType: string + id: string + lastMessage: string + lastEventId: number + loadedFromDatabase: boolean + messages: ChatMessage[] + personaId: string + status: string + title: string +} + +type StreamEvent = { + data: WebRendererOutput + id?: number +} + +const INITIAL_THREAD_ID = initialThreadId() +const DEFAULT_HARNESS_TYPE = 'codex' +const DEFAULT_PERSONA_ID = '__base__' +const WEB_THREAD_UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i +const HARNESS_OPTIONS = [ + { label: 'Codex', value: DEFAULT_HARNESS_TYPE }, + { label: 'Claude', value: 'claudecode' }, + { label: 'Amp', value: 'amp' } +] +const DEFAULT_PERSONA_OPTIONS = [{ label: 'Base', value: DEFAULT_PERSONA_ID }] + +export function App() { + const composerRef = useRef(null) + const resetThreadRef = useRef<(() => void) | null>(null) + const [threadId, setThreadId] = useState(INITIAL_THREAD_ID) + const [input, setInput] = useState('') + const [personaOptions, setPersonaOptions] = useState(DEFAULT_PERSONA_OPTIONS) + const [threads, setThreads] = useState(() => [ + createThreadSummary(INITIAL_THREAD_ID) + ]) + const activeThread = threads.find(thread => thread.id === threadId) ?? threads[0] + const title = activeThread?.title ?? 'New chat' + const status = activeThread?.status ?? 'Idle' + const lastEventId = activeThread?.lastEventId ?? 0 + const messages = activeThread?.messages ?? [] + const streaming = Boolean(activeThread?.activeAssistantMessageIds.length) + const isLanding = messages.length === 0 + const harnessType = activeThread?.harnessType ?? DEFAULT_HARNESS_TYPE + const personaId = activeThread?.personaId ?? DEFAULT_PERSONA_ID + const activePersonaOptions = ensureSelectedPersonaOption(personaOptions, personaId) + + useEffect(() => { + if (threadIdFromLocation() !== threadId) { + replaceRouteForThread(threadId) + } + + function handlePopState() { + const nextThreadId = threadIdFromLocation() ?? newThreadId() + setThreads(current => + current.some(thread => thread.id === nextThreadId) + ? current + : [createThreadSummary(nextThreadId), ...current] + ) + setThreadId(nextThreadId) + focusComposerInputSoon() + } + + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, []) + + useEffect(() => { + let cancelled = false + + async function loadPersonas() { + try { + const response = await fetch('/api/personas') + if (!response.ok) throw new Error(`Persona load failed: ${response.status}`) + const body = (await response.json()) as { personas?: WebPersonaOption[] } + if (!cancelled) setPersonaOptions(normalizePersonaOptions(body.personas)) + } catch (error) { + if (!cancelled) console.error(error) + } + } + + void loadPersonas() + return () => { + cancelled = true + } + }, []) + + useEffect(() => { + const targetThread = activeThread + if (!targetThread || targetThread.loadedFromDatabase) return + const targetThreadId = targetThread.id + let cancelled = false + + async function loadThread() { + const threadUuid = threadUuidFromId(targetThreadId) + if (!threadUuid) { + updateThread(targetThreadId, { loadedFromDatabase: true }) + return + } + + try { + const response = await fetch(`/api/threads/${threadUuid}`) + if (cancelled) return + if (response.status === 404) { + updateThread(targetThreadId, { loadedFromDatabase: true }) + return + } + if (!response.ok) { + throw new Error(`Thread load failed: ${response.status} ${response.statusText}`) + } + const snapshot = (await response.json()) as LoadedWebThread + if (cancelled) return + applyLoadedThread(snapshot) + if (shouldRequestThreadTitle(snapshot)) { + void requestLoadedThreadTitle(threadUuid, targetThreadId) + } + } catch (error) { + if (cancelled) return + updateThread(targetThreadId, { + loadedFromDatabase: true, + status: 'Error' + }) + console.error(error) + } + } + + void loadThread() + return () => { + cancelled = true + } + }, [activeThread?.id, activeThread?.loadedFromDatabase]) + + useEffect(() => { + focusComposerInput() + }) + + async function submit() { + const currentThread = activeThread + const message = input.trim() + if (!currentThread || !message) return + const currentThreadId = currentThread.id + const assistantMessageId = newMessageId() + setInput('') + updateThread(currentThreadId, { + activeAssistantMessageIds: [...currentThread.activeAssistantMessageIds, assistantMessageId], + lastMessage: message, + messages: [ + ...currentThread.messages, + { id: newMessageId(), role: 'user', text: message }, + { id: assistantMessageId, role: 'assistant', tasks: [], text: '' } + ], + status: 'Starting' + }) + + try { + const response = await fetch('api/chat', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + threadId: currentThreadId, + message, + harnessType, + personaId: personaIdForRequest(personaId), + afterEventId: currentThread.lastEventId + }) + }) + if (!response.ok || !response.body) { + throw new Error(`Request failed: ${response.status} ${response.statusText}`) + } + for await (const event of parseSse(response.body)) { + if (typeof event.id === 'number') { + updateThreadLastEventId(currentThreadId, event.id) + } + applyOutput(currentThreadId, assistantMessageId, event.data) + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + updateThread(currentThreadId, { status: 'Error' }) + updateAssistant( + currentThreadId, + assistantMessageId, + text => `${text}${text ? '\n\n' : ''}${message}` + ) + } finally { + completeAssistantMessage(currentThreadId, assistantMessageId) + } + } + + function applyOutput(targetThreadId: string, assistantMessageId: string, output: WebRendererOutput) { + if (output.type === 'web.status.update') { + updateThread(targetThreadId, { status: output.status }) + return + } + if (output.type === 'web.message.delta') { + updateAssistant(targetThreadId, assistantMessageId, text => + output.force ? output.delta : text + output.delta + ) + return + } + if (output.type === 'web.message.snapshot') { + updateAssistant(targetThreadId, assistantMessageId, () => output.markdown) + return + } + if (output.type === 'web.task.upsert') { + updateAssistantTask(targetThreadId, assistantMessageId, output.task) + return + } + if (output.type === 'web.plan.update') { + return + } + if (output.type === 'web.title.update') { + updateThreadTitleIfUntitled(targetThreadId, output.title) + return + } + const nextStatus = output.error ? 'Error' : 'Complete' + updateThread(targetThreadId, { status: nextStatus }) + if (output.answerMarkdown) { + updateAssistant(targetThreadId, assistantMessageId, text => + text.trim() ? text : output.answerMarkdown ?? '' + ) + } + if (output.error) { + updateAssistant(targetThreadId, assistantMessageId, text => + `${text}${text ? '\n\n' : ''}${output.error ?? ''}` + ) + } + } + + function updateAssistant( + targetThreadId: string, + assistantMessageId: string, + update: (text: string) => string + ) { + updateThreadMessages(targetThreadId, messages => + messages.map(message => + message.id === assistantMessageId ? { ...message, text: update(message.text) } : message + ) + ) + } + + function updateAssistantTask( + targetThreadId: string, + assistantMessageId: string, + task: WebRendererTask + ) { + updateThreadMessages(targetThreadId, messages => + messages.map(message => + message.id === assistantMessageId + ? { ...message, tasks: upsertTaskItem(message.tasks ?? [], task) } + : message + ) + ) + } + + function resetThread() { + const nextThreadId = newThreadId() + setThreads(current => [createThreadSummary(nextThreadId, harnessType, personaId), ...current]) + setThreadId(nextThreadId) + pushRouteForThread(nextThreadId) + focusComposerInputSoon() + } + + resetThreadRef.current = resetThread + + useEffect(() => { + function handleKeyDown(event: KeyboardEvent) { + if (!isNewChatShortcut(event) || event.repeat) return + event.preventDefault() + event.stopPropagation() + resetThreadRef.current?.() + } + + document.addEventListener('keydown', handleKeyDown, true) + return () => document.removeEventListener('keydown', handleKeyDown, true) + }, []) + + function selectThread(thread: ThreadSummary) { + if (thread.id !== threadId) { + setThreadId(thread.id) + pushRouteForThread(thread.id) + } + focusComposerInputSoon() + } + + function changeHarnessType(nextHarnessType: string) { + const currentThread = activeThread + if (!currentThread || nextHarnessType === currentThread.harnessType) return + const canReuseThread = + currentThread.messages.length === 0 && currentThread.activeAssistantMessageIds.length === 0 + if (canReuseThread) { + updateThread(currentThread.id, { harnessType: nextHarnessType }) + focusComposerInputSoon() + return + } + + const nextThreadId = newThreadId() + setThreads(current => [ + createThreadSummary(nextThreadId, nextHarnessType, currentThread.personaId), + ...current + ]) + setThreadId(nextThreadId) + pushRouteForThread(nextThreadId) + focusComposerInputSoon() + } + + function changePersonaId(nextPersonaId: string) { + const currentThread = activeThread + const normalizedPersonaId = normalizePersonaIdValue(nextPersonaId) + if (!currentThread || normalizedPersonaId === currentThread.personaId) return + const canReuseThread = + currentThread.messages.length === 0 && currentThread.activeAssistantMessageIds.length === 0 + if (canReuseThread) { + updateThread(currentThread.id, { personaId: normalizedPersonaId }) + focusComposerInputSoon() + return + } + + const nextThreadId = newThreadId() + setThreads(current => [ + createThreadSummary(nextThreadId, currentThread.harnessType, normalizedPersonaId), + ...current + ]) + setThreadId(nextThreadId) + pushRouteForThread(nextThreadId) + focusComposerInputSoon() + } + + function applyLoadedThread(snapshot: LoadedWebThread) { + setThreads(current => + current.map(thread => { + if (thread.id !== snapshot.threadId) return thread + const hasLocalMessages = + thread.messages.length > 0 || thread.activeAssistantMessageIds.length > 0 + const messages = hasLocalMessages ? thread.messages : snapshot.messages + const hasActiveAssistant = thread.activeAssistantMessageIds.length > 0 + const lastMessage = + thread.lastMessage || + [...messages].reverse().find(message => message.role === 'user')?.text || + '' + return { + ...thread, + harnessType: snapshot.harnessType, + lastEventId: Math.max(thread.lastEventId, snapshot.lastEventId), + lastMessage, + loadedFromDatabase: true, + messages, + personaId: normalizePersonaIdValue(snapshot.personaId), + status: hasActiveAssistant ? thread.status : snapshot.status, + title: isUntitledThread(thread) ? snapshot.title : thread.title + } + }) + ) + } + + async function requestLoadedThreadTitle(threadUuid: string, targetThreadId: string) { + try { + const response = await fetch(`/api/threads/${threadUuid}/title`, { method: 'POST' }) + if (!response.ok) return + const body = (await response.json()) as { eventId?: number; title?: string } + if (body.title) updateThreadTitleIfUntitled(targetThreadId, body.title) + if (typeof body.eventId === 'number') updateThreadLastEventId(targetThreadId, body.eventId) + } catch (error) { + console.error(error) + } + } + + function updateThread(id: string, patch: Partial) { + setThreads(current => + current.map(thread => (thread.id === id ? { ...thread, ...patch } : thread)) + ) + } + + function updateThreadLastEventId(id: string, eventId: number) { + setThreads(current => + current.map(thread => + thread.id === id ? { ...thread, lastEventId: Math.max(thread.lastEventId, eventId) } : thread + ) + ) + } + + function updateThreadMessages( + id: string, + update: (messages: ChatMessage[]) => ChatMessage[] + ) { + setThreads(current => + current.map(thread => + thread.id === id ? { ...thread, messages: update(thread.messages) } : thread + ) + ) + } + + function completeAssistantMessage(id: string, assistantMessageId: string) { + setThreads(current => + current.map(thread => + thread.id === id + ? { + ...thread, + activeAssistantMessageIds: thread.activeAssistantMessageIds.filter( + messageId => messageId !== assistantMessageId + ) + } + : thread + ) + ) + } + + function updateThreadTitleIfUntitled(id: string, nextTitle: string) { + const title = nextTitle.trim() + if (!title) return + setThreads(current => + current.map(thread => + thread.id === id && isUntitledThread(thread) ? { ...thread, title } : thread + ) + ) + } + + function focusComposerInput() { + const input = composerRef.current?.querySelector< + HTMLInputElement | HTMLTextAreaElement + >('[aria-label="Message"]') + if (!input || input.disabled) return + input.focus({ preventScroll: true }) + } + + function focusComposerInputSoon() { + window.requestAnimationFrame(focusComposerInput) + } + + const composer = ( + void submit()} + composerRef={composerRef} + /> + ) + + if (isLanding) { + return ( +
+
+
+

What should Centaur run?

+ {composer} +
+
+
+ ) + } + + return ( +
+ + +
+
+
+

{title}

+ {status !== 'Idle' && ( +

+ {status} · Event #{lastEventId} +

+ )} +
+
+ +
+
+
+ {messages.map(message => { + const messageTasks = visibleTasks(message.tasks ?? []) + return ( + + {message.role === 'assistant' && messageTasks.length > 0 && ( + + )} +
+ +
+
+ ) + })} +
+ + {composer} +
+
+
+
+ ) +} + +const Composer = (props: { + input: string + isLanding: boolean + harnessType: string + personaId: string + personaOptions: WebPersonaOption[] + composerRef: RefObject + onHarnessTypeChange: (value: string) => void + onInputChange: (value: string) => void + onPersonaIdChange: (value: string) => void + onSubmit: () => void +}) => { + return ( +
{ + event.preventDefault() + props.onSubmit() + }} + ref={props.composerRef} + > +
+