From 06b42eaf2340ef693c68915263a063f6572f57f8 Mon Sep 17 00:00:00 2001 From: Goksu Toprak Date: Mon, 1 Jun 2026 21:14:07 -0700 Subject: [PATCH 01/21] feat(web): add regen interface for api-rs Add a Regen-based Centaur web service that streams the Rust V2 session API through the generic renderer contract. Wire api-rs and web deployments into Helm, publish workflows, and local dev values so the flow shares existing Kubernetes secrets. --- .github/workflows/publish-images.yml | 8 + Justfile | 12 +- contrib/chart/templates/networkpolicy.yaml | 56 ++ contrib/chart/templates/services.yaml | 16 + contrib/chart/templates/workloads.yaml | 68 ++ contrib/chart/values.dev.yaml | 22 +- contrib/chart/values.yaml | 12 + .../rendering/src/codex-app-server.test.ts | 21 + packages/rendering/src/index.ts | 2 + packages/rendering/src/web.test.ts | 63 ++ packages/rendering/src/web.ts | 100 ++ pnpm-lock.yaml | 912 +++++++++++++++++- pnpm-workspace.yaml | 1 + .../centaur-sandbox-agent-k8s/src/lib.rs | 23 +- .../crates/centaur-sandbox-core/src/spec.rs | 1 + .../crates/centaur-session-runtime/src/lib.rs | 24 +- services/web/Dockerfile | 33 + services/web/index.html | 12 + services/web/package.json | 31 + services/web/src/app.ts | 74 ++ services/web/src/client/App.tsx | 343 +++++++ services/web/src/client/main.tsx | 14 + services/web/src/client/styles.css | 343 +++++++ services/web/src/client/vite-env.d.ts | 3 + services/web/src/server.ts | 71 ++ services/web/src/session-api.ts | 404 ++++++++ services/web/src/types.ts | 59 ++ services/web/test/session-api.test.ts | 84 ++ services/web/tsconfig.json | 25 + services/web/vite.config.ts | 17 + 30 files changed, 2833 insertions(+), 21 deletions(-) create mode 100644 packages/rendering/src/web.test.ts create mode 100644 packages/rendering/src/web.ts create mode 100644 services/web/Dockerfile create mode 100644 services/web/index.html create mode 100644 services/web/package.json create mode 100644 services/web/src/app.ts create mode 100644 services/web/src/client/App.tsx create mode 100644 services/web/src/client/main.tsx create mode 100644 services/web/src/client/styles.css create mode 100644 services/web/src/client/vite-env.d.ts create mode 100644 services/web/src/server.ts create mode 100644 services/web/src/session-api.ts create mode 100644 services/web/src/types.ts create mode 100644 services/web/test/session-api.test.ts create mode 100644 services/web/tsconfig.json create mode 100644 services/web/vite.config.ts 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..42c434dfe 100644 --- a/contrib/chart/templates/networkpolicy.yaml +++ b/contrib/chart/templates/networkpolicy.yaml @@ -105,6 +105,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), @@ -235,6 +240,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: @@ -297,6 +307,39 @@ spec: - protocol: TCP port: {{ .Values.networkPolicy.apiServerPort }} --- +{{- 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 $ingressSourceNamespaces }} + - 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 @@ -382,6 +425,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: @@ -548,4 +596,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..dba92a2f2 100644 --- a/contrib/chart/templates/workloads.yaml +++ b/contrib/chart/templates/workloads.yaml @@ -459,6 +459,74 @@ spec: path: services/sandbox/SYSTEM_PROMPT.md {{- 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 }} + - 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 }} + envFrom: + - secretRef: + name: {{ include "centaur.secretEnvName" . }} + 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 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.yaml b/contrib/chart/values.yaml index c1c3ada77..c11089979 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: 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/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/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-runtime/src/lib.rs b/services/api-rs/crates/centaur-session-runtime/src/lib.rs index d978ff21b..0b4e3866c 100644 --- a/services/api-rs/crates/centaur-session-runtime/src/lib.rs +++ b/services/api-rs/crates/centaur-session-runtime/src/lib.rs @@ -54,6 +54,7 @@ pub struct SandboxRuntime { pub enum SandboxWorkloadMode { MockAppServer { image: String, + mounts: Vec, }, CodexAppServer { image: String, @@ -441,6 +442,7 @@ impl SandboxWorkloadMode { pub fn mock_app_server(image: impl Into) -> Self { Self::MockAppServer { image: image.into(), + mounts: Vec::new(), } } @@ -457,29 +459,35 @@ 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) + let spec = match self { + Self::MockAppServer { image, .. } => SandboxSpec::new(image) .command(["/bin/sh", "-lc"]) .args([mock_app_server_script()]), - Self::CodexAppServer { image, env, mounts } => { + Self::CodexAppServer { image, env, .. } => { let mut spec = SandboxSpec::new(image).env("CENTAUR_THREAD_KEY", thread_key.as_str()); - for mount in mounts { - spec = spec.mount(mount.clone()); - } 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 } } diff --git a/services/web/Dockerfile b/services/web/Dockerfile new file mode 100644 index 000000000..7eb09397a --- /dev/null +++ b/services/web/Dockerfile @@ -0,0 +1,33 @@ +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 + +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..f508bcf69 --- /dev/null +++ b/services/web/index.html @@ -0,0 +1,12 @@ + + + + + + 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..2c607756d --- /dev/null +++ b/services/web/src/app.ts @@ -0,0 +1,74 @@ +import { Hono } from 'hono' +import { serveStatic } from 'hono/bun' +import { streamWebTurn } from './session-api' +import type { CentaurWebOptions, WebTurnRequest, WebTurnStreamItem } from './types' + +const encoder = new TextEncoder() + +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.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.get('/favicon.svg', serveStatic({ path: './dist/client/favicon.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..78bb5e04c --- /dev/null +++ b/services/web/src/client/App.tsx @@ -0,0 +1,343 @@ +import { useMemo, useRef, useState } from 'react' +import { Bot, CheckCircle2, Circle, CircleAlert, LoaderCircle, Plus, Send, Terminal } from 'lucide-react' +import { Button, Frame, Input, Rows, Tag } from 'regen-ui' +import type { WebRendererOutput, WebRendererTask } from '@centaur/rendering' + +type ChatMessage = { + id: string + role: 'assistant' | 'user' + text: string +} + +type StreamEvent = { + data: WebRendererOutput + id?: number +} + +export function App() { + const [threadId, setThreadId] = useState(() => newThreadId()) + const [lastEventId, setLastEventId] = useState(0) + const [title, setTitle] = useState('Centaur Web') + const [status, setStatus] = useState('Idle') + const [input, setInput] = useState('') + const [messages, setMessages] = useState([]) + const [tasks, setTasks] = useState([]) + const [planTitle, setPlanTitle] = useState('') + const [streaming, setStreaming] = useState(false) + const assistantIdRef = useRef(null) + const taskCount = tasks.length + const completedTasks = tasks.filter(task => task.status === 'complete').length + const sortedTasks = useMemo(() => tasks, [tasks]) + + async function submit() { + const message = input.trim() + if (!message || streaming) return + setInput('') + setStreaming(true) + setStatus('Starting') + const userMessage: ChatMessage = { id: newMessageId(), role: 'user', text: message } + const assistantMessage: ChatMessage = { id: newMessageId(), role: 'assistant', text: '' } + assistantIdRef.current = assistantMessage.id + setMessages(current => [...current, userMessage, assistantMessage]) + + try { + const response = await fetch('/api/chat', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ threadId, message, afterEventId: 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') { + setLastEventId(current => Math.max(current, event.id ?? 0)) + } + applyOutput(event.data) + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + setStatus('Error') + updateAssistant(text => `${text}${text ? '\n\n' : ''}${message}`) + } finally { + setStreaming(false) + } + } + + function applyOutput(output: WebRendererOutput) { + if (output.type === 'web.status.update') { + setStatus(output.status) + return + } + if (output.type === 'web.message.delta') { + updateAssistant(text => (output.force ? output.delta : text + output.delta)) + return + } + if (output.type === 'web.message.snapshot') { + updateAssistant(() => output.markdown) + return + } + if (output.type === 'web.task.upsert') { + setTasks(current => upsertTask(current, output.task)) + return + } + if (output.type === 'web.plan.update') { + setPlanTitle(output.title) + return + } + if (output.type === 'web.title.update') { + setTitle(output.title) + return + } + setStatus(output.error ? 'Error' : 'Complete') + if (output.answerMarkdown) { + updateAssistant(text => (text.trim() ? text : output.answerMarkdown ?? '')) + } + if (output.error) { + updateAssistant(text => `${text}${text ? '\n\n' : ''}${output.error ?? ''}`) + } + } + + function updateAssistant(update: (text: string) => string) { + const assistantId = assistantIdRef.current + if (!assistantId) return + setMessages(current => + current.map(message => + message.id === assistantId ? { ...message, text: update(message.text) } : message + ) + ) + } + + function resetThread() { + setThreadId(newThreadId()) + setLastEventId(0) + setTitle('Centaur Web') + setStatus('Idle') + setMessages([]) + setTasks([]) + setPlanTitle('') + assistantIdRef.current = null + } + + return ( +
+ + +
+
+
+

{title}

+
+ Rust V2 + Codex + Renderer +
+
+
+ +
+
+
+ {messages.length === 0 ? ( +
+ + Ready +
+ ) : ( + messages.map(message => ( +
+
{message.role}
+ +
+ )) + )} +
+ +
{ + event.preventDefault() + void submit() + }} + > + setInput(event.target.value)} + placeholder="Ask Codex" + value={input} + /> + +
+
+ +
+
+

Activity

+
+
+ {sortedTasks.length === 0 ? ( +
No activity
+ ) : ( + sortedTasks.map(task => ) + )} +
+
+
+
+
+ ) +} + +function TaskRow(props: { task: WebRendererTask }) { + const { task } = props + const icon = + task.status === 'complete' ? ( + + ) : task.status === 'error' ? ( + + ) : task.status === 'in_progress' ? ( + + ) : ( + + ) + + return ( +
+
+ {icon} + {task.title} +
+ {task.details && } + {task.output && } +
+ ) +} + +function MarkdownText(props: { className?: string; text: string }) { + const parts = splitCodeFences(props.text) + return ( +
+ {parts.map((part, index) => + part.kind === 'code' ? ( +
+            {part.text}
+          
+ ) : ( +

{part.text}

+ ) + )} +
+ ) +} + +function splitCodeFences(value: string): Array<{ kind: 'code' | 'text'; text: string }> { + const parts: Array<{ kind: 'code' | 'text'; text: string }> = [] + const regex = /```[^\n]*\n([\s\S]*?)```/g + let lastIndex = 0 + for (const match of value.matchAll(regex)) { + if (match.index > lastIndex) { + const text = value.slice(lastIndex, match.index).trim() + if (text) parts.push({ kind: 'text', text }) + } + parts.push({ kind: 'code', text: match[1] ?? '' }) + lastIndex = match.index + match[0].length + } + const tail = value.slice(lastIndex).trim() + if (tail) parts.push({ kind: 'text', text: tail }) + return parts.length ? parts : [{ kind: 'text', text: value }] +} + +function upsertTask(tasks: WebRendererTask[], task: WebRendererTask): WebRendererTask[] { + const index = tasks.findIndex(item => item.id === task.id) + if (index < 0) return [...tasks, task] + return tasks.map(item => (item.id === task.id ? task : item)) +} + +async function* parseSse(stream: ReadableStream): AsyncIterable { + const reader = stream.getReader() + const decoder = new TextDecoder() + let buffer = '' + let eventId: number | undefined + let data: string[] = [] + + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split(/\r?\n/) + buffer = lines.pop() ?? '' + for (const line of lines) { + const event = parseSseLine(line, { data, eventId }) + data = event.state.data + eventId = event.state.eventId + if (event.data) yield event.data + } + } +} + +function parseSseLine( + line: string, + state: { data: string[]; eventId?: number } +): { data?: StreamEvent; state: { data: string[]; eventId?: number } } { + if (!line.trim()) { + if (!state.data.length) return { state: { data: [] } } + const raw = state.data.join('\n') + return { + data: { data: JSON.parse(raw) as WebRendererOutput, id: state.eventId }, + state: { data: [] } + } + } + if (line.startsWith('id:')) { + const id = Number.parseInt(line.slice(3).trim(), 10) + return { state: { ...state, eventId: Number.isFinite(id) ? id : undefined } } + } + if (line.startsWith('data:')) { + return { state: { ...state, data: [...state.data, line.slice(5).trimStart()] } } + } + return { state } +} + +function newThreadId(): string { + return `web:${crypto.randomUUID()}` +} + +function newMessageId(): string { + return `msg-${crypto.randomUUID()}` +} diff --git a/services/web/src/client/main.tsx b/services/web/src/client/main.tsx new file mode 100644 index 000000000..4d2e41063 --- /dev/null +++ b/services/web/src/client/main.tsx @@ -0,0 +1,14 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import 'regen-ui/styles.css' +import './styles.css' +import { App } from './App' + +const root = document.getElementById('root') +if (!root) throw new Error('root element missing') + +createRoot(root).render( + + + +) diff --git a/services/web/src/client/styles.css b/services/web/src/client/styles.css new file mode 100644 index 000000000..90d674e3f --- /dev/null +++ b/services/web/src/client/styles.css @@ -0,0 +1,343 @@ +:root { + color-scheme: light; + background: #f6f7f4; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { + box-sizing: border-box; +} + +html, +body, +#root { + min-height: 100%; +} + +body { + margin: 0; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(246, 247, 244, 0)), + #f6f7f4; +} + +button, +input { + font: inherit; +} + +.app-shell { + display: grid; + grid-template-columns: minmax(260px, 320px) minmax(0, 1fr); + min-height: 100vh; + color: #191c1a; +} + +.sidebar { + display: flex; + flex-direction: column; + gap: 18px; + padding: 22px; + border-right: 1px solid #d9ded6; + background: #eef2ec; +} + +.brand-row { + display: grid; + grid-template-columns: 40px minmax(0, 1fr); + gap: 12px; + align-items: center; +} + +.brand-mark { + display: grid; + width: 40px; + height: 40px; + place-items: center; + border: 1px solid #ccd6c8; + border-radius: 8px; + background: #ffffff; + color: #176b55; +} + +.brand-title { + font-size: 18px; + font-weight: 650; +} + +.thread-key { + overflow: hidden; + color: #667268; + font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.side-panel { + background: transparent; +} + +.plan-title { + margin: 0; + color: #3c4940; + font-size: 14px; + line-height: 1.45; +} + +.workspace { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + min-width: 0; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 84px; + padding: 22px 28px; + border-bottom: 1px solid #e0e4dd; + background: rgba(255, 255, 255, 0.72); + backdrop-filter: blur(16px); +} + +.topbar h1 { + margin: 0 0 8px; + overflow: hidden; + font-size: 24px; + font-weight: 700; + line-height: 1.1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.topbar-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.content-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(280px, 380px); + min-height: 0; +} + +.conversation { + display: grid; + grid-template-rows: minmax(0, 1fr) auto; + min-width: 0; + min-height: 0; +} + +.message-list { + display: flex; + flex-direction: column; + gap: 14px; + overflow: auto; + padding: 24px 28px; +} + +.empty-pane { + display: inline-flex; + align-items: center; + gap: 10px; + width: fit-content; + margin: auto; + padding: 12px 14px; + border: 1px solid #d9ded6; + border-radius: 8px; + background: #ffffff; + color: #566158; +} + +.message { + max-width: 860px; + padding: 14px 16px; + border: 1px solid #dde2da; + border-radius: 8px; + background: #ffffff; +} + +.message.user { + align-self: flex-end; + border-color: #cbd8d2; + background: #e9f4ef; +} + +.message.assistant { + align-self: flex-start; +} + +.message-role { + margin-bottom: 8px; + color: #657068; + font-size: 11px; + font-weight: 650; + letter-spacing: 0; + text-transform: uppercase; +} + +.markdown-text { + display: flex; + flex-direction: column; + gap: 10px; +} + +.markdown-text p, +.task-body p, +.task-output p { + margin: 0; + white-space: pre-wrap; + overflow-wrap: anywhere; + line-height: 1.5; +} + +pre { + overflow: auto; + max-width: 100%; + margin: 0; + padding: 12px; + border: 1px solid #d6dad3; + border-radius: 8px; + background: #111815; + color: #eef7f1; + font-size: 12px; + line-height: 1.45; +} + +.composer { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + padding: 18px 28px 24px; + border-top: 1px solid #e0e4dd; + background: rgba(250, 251, 248, 0.9); +} + +.activity { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + min-width: 0; + border-left: 1px solid #e0e4dd; + background: #f9faf7; +} + +.activity-header { + padding: 20px 20px 12px; +} + +.activity-header h2 { + margin: 0; + font-size: 15px; + font-weight: 700; +} + +.task-list { + display: flex; + flex-direction: column; + gap: 10px; + overflow: auto; + padding: 0 16px 20px; +} + +.task-empty { + padding: 14px; + border: 1px dashed #cdd4ca; + border-radius: 8px; + color: #68736a; + font-size: 13px; +} + +.task { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px; + border: 1px solid #dde2da; + border-radius: 8px; + background: #ffffff; +} + +.task.in_progress { + border-color: #bad4ca; +} + +.task.error { + border-color: #e2b8b8; +} + +.task-title { + display: grid; + grid-template-columns: 18px minmax(0, 1fr); + gap: 8px; + align-items: center; + color: #233028; + font-size: 13px; + font-weight: 650; +} + +.task-title span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task-body, +.task-output { + display: flex; + flex-direction: column; + gap: 8px; + color: #435047; + font-size: 12px; +} + +.spin { + animation: spin 900ms linear infinite; +} + +.min-w-0 { + min-width: 0; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@media (max-width: 900px) { + .app-shell { + grid-template-columns: 1fr; + } + + .sidebar { + border-right: 0; + border-bottom: 1px solid #d9ded6; + } + + .content-grid { + grid-template-columns: 1fr; + } + + .activity { + min-height: 280px; + border-left: 0; + border-top: 1px solid #e0e4dd; + } +} + +@media (max-width: 560px) { + .topbar, + .message-list, + .composer { + padding-right: 16px; + padding-left: 16px; + } + + .composer { + grid-template-columns: 1fr; + } +} diff --git a/services/web/src/client/vite-env.d.ts b/services/web/src/client/vite-env.d.ts new file mode 100644 index 000000000..d4a3e2391 --- /dev/null +++ b/services/web/src/client/vite-env.d.ts @@ -0,0 +1,3 @@ +/// + +declare module 'regen-ui/styles.css' diff --git a/services/web/src/server.ts b/services/web/src/server.ts new file mode 100644 index 000000000..994be6e6f --- /dev/null +++ b/services/web/src/server.ts @@ -0,0 +1,71 @@ +import { createCentaurWebApp } from './app' +import type { CentaurWebLogger, CentaurWebOptions } from './types' + +const port = numberEnv('PORT', 3003) +const apiUrl = stringEnv('CENTAUR_API_RS_URL', stringEnv('CENTAUR_API_URL', 'http://127.0.0.1:8080')) + +const consoleLogger: CentaurWebLogger = { + info: (event, fields) => log('info', event, fields), + warn: (event, fields) => log('warn', event, fields), + error: (event, fields) => log('error', event, fields) +} + +const options: CentaurWebOptions = { + apiUrl, + apiKey: optionalEnv('CENTAUR_API_KEY') ?? optionalEnv('SLACKBOT_API_KEY'), + idleTimeoutMs: optionalNumberEnv('SESSION_IDLE_TIMEOUT_MS'), + maxDurationMs: optionalNumberEnv('SESSION_MAX_DURATION_MS'), + logger: consoleLogger +} + +const app = createCentaurWebApp(options) +const server = Bun.serve({ + port, + fetch: app.fetch +}) + +console.log( + JSON.stringify({ + timestamp: new Date().toISOString(), + level: 'info', + event: 'centaur_web_started', + service: 'centaur-web', + port: server.port, + api_url: apiUrl + }) +) + +function optionalEnv(name: string): string | undefined { + const value = process.env[name]?.trim() + return value ? value : undefined +} + +function stringEnv(name: string, fallback: string): string { + return optionalEnv(name) ?? fallback +} + +function numberEnv(name: string, fallback: number): number { + return optionalNumberEnv(name) ?? fallback +} + +function optionalNumberEnv(name: string): number | undefined { + const value = optionalEnv(name) + if (!value) return undefined + const parsed = Number.parseInt(value, 10) + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new Error(`${name} must be a positive integer`) + } + return parsed +} + +function log(level: string, event: string, fields?: Record): void { + console.log( + JSON.stringify({ + level, + service: 'centaur-web', + timestamp: new Date().toISOString(), + event, + ...(fields ?? {}) + }) + ) +} diff --git a/services/web/src/session-api.ts b/services/web/src/session-api.ts new file mode 100644 index 000000000..6fb9edde3 --- /dev/null +++ b/services/web/src/session-api.ts @@ -0,0 +1,404 @@ +import type { RustSessionStreamEvent } from '@centaur/harness-events' +import { + CodexAppServerRendererEventMapper, + WebRenderer, + type WebRendererOutput +} from '@centaur/rendering' +import type { + AppendMessagesRequest, + CentaurWebOptions, + CreateSessionRequest, + ExecuteSessionRequest, + JsonObject, + JsonValue, + WebTurnRequest, + WebTurnStreamItem +} from './types' + +type ParsedSessionEvent = { + data: string + event?: string + id?: number +} + +type NormalizedWebTurnRequest = WebTurnRequest & { + afterEventId: number + message: string + threadId: string +} + +export async function* streamWebTurn( + options: CentaurWebOptions, + input: WebTurnRequest +): AsyncIterable { + const normalized = normalizeTurnRequest(input) + yield output({ type: 'web.status.update', status: 'Starting session' }) + await createSession(options, normalized.threadId) + await appendSessionMessage(options, normalized) + await executeSession(options, normalized) + + yield output({ type: 'web.status.update', status: 'Streaming response' }) + const mapper = new CodexAppServerRendererEventMapper({ + logInfo: (event, fields) => options.logger?.info(event, fields) + }) + const renderer = new WebRenderer() + const events = await streamSessionEvents(options, normalized.threadId, normalized.afterEventId ?? 0) + + for await (const source of events) { + const eventId = typeof source.eventId === 'number' ? source.eventId : undefined + for (const event of mapper.process(source)) { + for (const rendered of renderer.render(normalized.threadId, event)) { + yield output(rendered, eventId) + } + } + if (mapper.isDone()) return + } + + for (const event of mapper.flush()) { + for (const rendered of renderer.render(normalized.threadId, event)) { + yield output(rendered) + } + } +} + +export function toCodexInputLine(input: WebTurnRequest, messageId = newMessageId()): string { + const normalized = normalizeTurnRequest(input) + const metadata = sessionMetadata(normalized, messageId, { action: 'execute' }) + return JSON.stringify({ + type: 'user', + thread_key: normalized.threadId, + trace_metadata: metadata, + message: { + role: 'user', + content: codexInputContent(normalized.message) + } + }) +} + +export async function* parseSessionEventStream( + stream: ReadableStream +): AsyncIterable { + for await (const event of parseSseEvents(stream)) { + if (event.event === 'session.output.line') { + yield { + data: event.data, + event: event.event, + eventId: event.id, + eventKind: event.event + } satisfies RustSessionStreamEvent + if (isTerminalCodexOutputLine(event.data)) return + continue + } + if (event.event === 'session.execution_failed' || event.event === 'session.stream_error') { + yield { + data: { error: sessionErrorMessage(event) }, + event: event.event, + eventId: event.id, + eventKind: event.event + } satisfies RustSessionStreamEvent + return + } + } +} + +function output(output: WebRendererOutput, eventId?: number): WebTurnStreamItem { + return eventId === undefined ? { output } : { eventId, output } +} + +function normalizeTurnRequest(input: WebTurnRequest): NormalizedWebTurnRequest { + const threadId = requestThreadId(input).trim() + const message = typeof input.message === 'string' ? input.message.trim() : '' + if (!threadId) throw new Error('threadId is required') + if (!threadId.includes(':')) throw new Error("threadId must be namespaced as ':'") + if (!message) throw new Error('message is required') + return { + ...input, + threadId, + message, + afterEventId: normalizeAfterEventId(input.afterEventId) + } +} + +function requestThreadId(input: WebTurnRequest): string { + return input.threadId ?? input.threadKey ?? input.thread_key ?? '' +} + +function normalizeAfterEventId(value: number | undefined): number { + if (value === undefined) return 0 + if (!Number.isFinite(value) || value < 0) return 0 + return Math.floor(value) +} + +async function createSession(options: CentaurWebOptions, threadId: string): Promise { + const body: CreateSessionRequest = { + harness_type: 'codex', + metadata: { + source: 'centaur-web', + platform: 'web', + thread_id: threadId + } + } + await ensureApiOk( + await apiFetch(options, sessionPath(threadId), { + method: 'POST', + body: JSON.stringify(body) + }), + 'create session' + ) +} + +async function appendSessionMessage( + options: CentaurWebOptions, + input: NormalizedWebTurnRequest +): Promise { + const messageId = newMessageId() + const body: AppendMessagesRequest = { + messages: [ + { + role: 'user', + parts: [{ type: 'text', text: input.message }], + metadata: sessionMetadata(input, messageId) + } + ] + } + await ensureApiOk( + await apiFetch(options, sessionPath(input.threadId, 'messages'), { + method: 'POST', + body: JSON.stringify(body) + }), + 'append message' + ) +} + +async function executeSession( + options: CentaurWebOptions, + input: NormalizedWebTurnRequest +): Promise { + const messageId = newMessageId() + const body: ExecuteSessionRequest = { + metadata: sessionMetadata(input, messageId, { action: 'execute' }), + input_lines: [toCodexInputLine(input, messageId)], + ...(options.idleTimeoutMs === undefined ? {} : { idle_timeout_ms: options.idleTimeoutMs }), + ...(options.maxDurationMs === undefined ? {} : { max_duration_ms: options.maxDurationMs }) + } + await ensureApiOk( + await apiFetch(options, sessionPath(input.threadId, 'execute'), { + method: 'POST', + body: JSON.stringify(body) + }), + 'execute session' + ) +} + +async function streamSessionEvents( + options: CentaurWebOptions, + threadId: string, + afterEventId: number +): Promise> { + const response = await apiFetch( + options, + `${sessionPath(threadId, 'events')}?after_event_id=${afterEventId}`, + { + method: 'GET', + jsonBody: false + } + ) + await ensureApiOk(response, 'stream events') + if (!response.body) return toAsyncIterable([]) + return parseSessionEventStream(response.body) +} + +async function ensureApiOk(response: Response, action: string): Promise { + if (response.ok) return + let body = '' + try { + body = await response.text() + } catch { + body = '' + } + const suffix = body ? `: ${body}` : '' + throw new Error(`Centaur session ${action} failed: ${response.status} ${response.statusText}${suffix}`) +} + +async function apiFetch( + options: CentaurWebOptions, + path: string, + init: RequestInit & { jsonBody?: boolean } +): Promise { + const fetchFn = options.fetch ?? fetch + const jsonBody = init.jsonBody !== false + const headers = apiHeaders(options, jsonBody) + const { jsonBody: _jsonBody, ...requestInit } = init + void _jsonBody + return fetchFn(new URL(path, ensureTrailingSlash(options.apiUrl)), { + ...requestInit, + headers: { + ...headers, + ...headersToObject(requestInit.headers) + } + }) +} + +function apiHeaders(options: CentaurWebOptions, jsonBody = true): Record { + const apiKey = options.apiKey + return { + ...(jsonBody ? { 'content-type': 'application/json' } : {}), + ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}) + } +} + +function headersToObject(headers: HeadersInit | undefined): Record { + if (!headers) return {} + if (headers instanceof Headers) return Object.fromEntries(headers.entries()) + if (Array.isArray(headers)) return Object.fromEntries(headers) + return headers +} + +function sessionPath(threadId: string, suffix?: 'messages' | 'execute' | 'events'): string { + return `/api/session/${encodeURIComponent(threadId)}${suffix ? `/${suffix}` : ''}` +} + +function ensureTrailingSlash(value: string): string { + return value.endsWith('/') ? value : `${value}/` +} + +function sessionMetadata( + input: NormalizedWebTurnRequest, + messageId: string, + extra: JsonObject = {} +): JsonObject { + return { + source: 'centaur-web', + platform: 'web', + message_id: messageId, + thread_id: input.threadId, + timestamp: new Date().toISOString(), + ...extra + } +} + +function codexInputContent(message: string): JsonValue[] { + return [{ type: 'text', text: message.trim() || 'continue' }] +} + +function newMessageId(): string { + if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) { + return `web-msg-${crypto.randomUUID()}` + } + return `web-msg-${Date.now()}-${Math.random().toString(16).slice(2)}` +} + +async function* toAsyncIterable(values: Iterable): AsyncIterable { + for (const value of values) yield value +} + +async function* parseSseEvents(stream: ReadableStream): AsyncIterable { + const reader = stream.getReader() + const decoder = new TextDecoder() + let buffer = '' + let eventName: string | undefined + let eventId: number | undefined + let data: string[] = [] + + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split(/\r?\n/) + buffer = lines.pop() ?? '' + + for (const line of lines) { + const emitted = parseSseLine(line, { data, eventId, eventName }) + data = emitted.state.data + eventId = emitted.state.eventId + eventName = emitted.state.eventName + if (emitted.event) yield emitted.event + } + } + + buffer += decoder.decode() + if (buffer) { + const emitted = parseSseLine(buffer, { data, eventId, eventName }) + data = emitted.state.data + eventId = emitted.state.eventId + eventName = emitted.state.eventName + if (emitted.event) yield emitted.event + } + if (data.length > 0) { + yield { data: data.join('\n'), event: eventName, id: eventId } + } +} + +function parseSseLine( + line: string, + state: { + data: string[] + eventId?: number + eventName?: string + } +): { + event?: ParsedSessionEvent + state: { data: string[]; eventId?: number; eventName?: string } +} { + if (!line.trim()) { + const event = + state.data.length > 0 + ? { data: state.data.join('\n'), event: state.eventName, id: state.eventId } + : undefined + return { event, state: { data: [] } } + } + if (line.startsWith(':')) return { state } + + const separator = line.indexOf(':') + const field = separator >= 0 ? line.slice(0, separator) : line + const value = separator >= 0 ? line.slice(separator + 1).replace(/^ /, '') : '' + if (field === 'event') return { state: { ...state, eventName: value } } + if (field === 'id') { + const id = Number.parseInt(value, 10) + return { state: { ...state, eventId: Number.isFinite(id) ? id : undefined } } + } + if (field === 'data' && value !== '[DONE]') { + return { state: { ...state, data: [...state.data, value] } } + } + + return { state } +} + +function isTerminalCodexOutputLine(line: string): boolean { + let payload: unknown + try { + payload = JSON.parse(line) + } catch { + return true + } + if (!isRecord(payload)) return false + + return ( + payload.type === 'turn.completed' || + payload.type === 'turn.failed' || + payload.type === 'turn.done' || + payload.method === 'error' || + payload.method === 'turn/completed' + ) +} + +function sessionErrorMessage(event: ParsedSessionEvent): string { + let message = `${event.event ?? 'session error'}` + try { + const payload = JSON.parse(event.data) + if (isRecord(payload)) { + message = stringValue(payload.error) ?? stringValue(payload.message) ?? message + } + } catch { + if (event.data.trim()) message = event.data.trim() + } + return message +} + +function stringValue(value: unknown): string | undefined { + return typeof value === 'string' && value.trim() ? value : undefined +} + +function isRecord(value: unknown): value is Record { + return Boolean(value && typeof value === 'object' && !Array.isArray(value)) +} diff --git a/services/web/src/types.ts b/services/web/src/types.ts new file mode 100644 index 000000000..350b2c3c6 --- /dev/null +++ b/services/web/src/types.ts @@ -0,0 +1,59 @@ +import type { WebRendererOutput } from '@centaur/rendering' + +export type JsonPrimitive = string | number | boolean | null +export type JsonValue = JsonPrimitive | JsonObject | JsonValue[] +export type JsonObject = { [key: string]: JsonValue | undefined } + +export type CentaurWebFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise + +export type CentaurWebOptions = { + apiKey?: string + apiUrl: string + fetch?: CentaurWebFetch + idleTimeoutMs?: number + logger?: CentaurWebLogger + maxDurationMs?: number +} + +export type CentaurWebLogger = { + info(event: string, fields?: Record): void + warn(event: string, fields?: Record): void + error(event: string, fields?: Record): void +} + +export type WebTurnRequest = { + afterEventId?: number + message: string + threadId?: string + threadKey?: string + thread_key?: string +} + +export type WebTurnStreamItem = { + eventId?: number + output: WebRendererOutput +} + +export type SessionMessageRole = 'user' | 'assistant' | 'system' | 'tool' + +export type SessionMessage = { + metadata: JsonObject + parts: JsonValue[] + role: SessionMessageRole +} + +export type AppendMessagesRequest = { + messages: SessionMessage[] +} + +export type CreateSessionRequest = { + harness_type: string + metadata: JsonObject +} + +export type ExecuteSessionRequest = { + idle_timeout_ms?: number + input_lines: string[] + max_duration_ms?: number + metadata: JsonObject +} diff --git a/services/web/test/session-api.test.ts b/services/web/test/session-api.test.ts new file mode 100644 index 000000000..bfc187b12 --- /dev/null +++ b/services/web/test/session-api.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'bun:test' +import { parseSessionEventStream, toCodexInputLine } from '../src/session-api' + +describe('web session api helpers', () => { + it('builds Codex app-server input lines for the Rust V2 session API', () => { + const line = toCodexInputLine( + { + threadId: 'web:test-thread', + message: 'Reply with PONG' + }, + 'msg-1' + ) + + expect(JSON.parse(line)).toEqual({ + type: 'user', + thread_key: 'web:test-thread', + trace_metadata: { + action: 'execute', + message_id: 'msg-1', + platform: 'web', + source: 'centaur-web', + thread_id: 'web:test-thread', + timestamp: expect.any(String) + }, + message: { + role: 'user', + content: [{ type: 'text', text: 'Reply with PONG' }] + } + }) + }) + + it('accepts threadKey as a web request alias', () => { + const line = toCodexInputLine( + { + threadKey: 'web:test-thread', + message: 'Reply with PONG' + }, + 'msg-1' + ) + + expect(JSON.parse(line)).toMatchObject({ + thread_key: 'web:test-thread', + trace_metadata: { + thread_id: 'web:test-thread' + } + }) + }) + + it('maps Rust session SSE output lines to renderer sources and stops at terminal output', async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode( + [ + 'id: 7', + 'event: session.output.line', + 'data: {"type":"item.agentMessage.delta","delta":"PONG"}', + '', + 'id: 8', + 'event: session.output.line', + 'data: {"type":"turn.done","result":"PONG"}', + '', + 'id: 9', + 'event: session.output.line', + 'data: {"type":"item.agentMessage.delta","delta":"LATE"}', + '', + '' + ].join('\n') + ) + ) + controller.close() + } + }) + + const events = [] + for await (const event of parseSessionEventStream(stream)) { + events.push(event) + } + + expect(events).toHaveLength(2) + expect(events[0]).toMatchObject({ eventId: 7, eventKind: 'session.output.line' }) + expect(events[1]).toMatchObject({ eventId: 8, eventKind: 'session.output.line' }) + }) +}) diff --git a/services/web/tsconfig.json b/services/web/tsconfig.json new file mode 100644 index 000000000..ce7ae338e --- /dev/null +++ b/services/web/tsconfig.json @@ -0,0 +1,25 @@ +{ + "schema": "https://json.schemastore.org/tsconfig.json", + "compilerOptions": { + "strict": true, + "noEmit": true, + "lib": ["DOM", "ESNext", "DOM.Iterable"], + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["bun", "node"], + "skipLibCheck": true, + "esModuleInterop": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "allowImportingTsExtensions": true, + "resolvePackageJsonExports": true, + "resolvePackageJsonImports": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "useUnknownInCatchVariables": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*", "test/**/*", "vite.config.ts"] +} diff --git a/services/web/vite.config.ts b/services/web/vite.config.ts new file mode 100644 index 000000000..958d461f2 --- /dev/null +++ b/services/web/vite.config.ts @@ -0,0 +1,17 @@ +import react from '@vitejs/plugin-react' +import regen from 'regen-ui/vite' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [regen(), react()], + build: { + outDir: 'dist/client', + emptyOutDir: true + }, + server: { + port: 5173, + proxy: { + '/api': 'http://127.0.0.1:3003' + } + } +}) From 936eb04e01ae0f92d2299e5b227b42a9d667b56a Mon Sep 17 00:00:00 2001 From: Goksu Toprak Date: Tue, 2 Jun 2026 04:59:35 -0700 Subject: [PATCH 02/21] fix(web): apply renderer showcase review Move the web shell to dark mode with thread-only navigation and tighter chat surfaces. Remove the api-rs rustls startup workaround by using reqwest rustls-no-provider and rely on stable sandbox labels in NetworkPolicies. --- contrib/chart/templates/networkpolicy.yaml | 14 +- services/api-rs/Cargo.lock | 219 +--------------- services/web/src/client/App.tsx | 124 ++++++--- services/web/src/client/styles.css | 280 ++++++++++++++------- 4 files changed, 288 insertions(+), 349 deletions(-) diff --git a/contrib/chart/templates/networkpolicy.yaml b/contrib/chart/templates/networkpolicy.yaml index 42c434dfe..8fb5a2630 100644 --- a/contrib/chart/templates/networkpolicy.yaml +++ b/contrib/chart/templates/networkpolicy.yaml @@ -250,8 +250,11 @@ spec: matchLabels: {{ include "centaur.componentSelectorLabels" (dict "root" . "component" "iron-proxy") | nindent 14 }} - podSelector: - matchLabels: - centaur.ai/managed: "true" + matchExpressions: + - key: centaur.ai/sandbox-id + operator: Exists + - key: centaur.ai/iron-proxy + operator: DoesNotExist - podSelector: matchLabels: centaur.ai/iron-proxy: "true" @@ -584,8 +587,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: diff --git a/services/api-rs/Cargo.lock b/services/api-rs/Cargo.lock index 35d087d84..ad94d5c4d 100644 --- a/services/api-rs/Cargo.lock +++ b/services/api-rs/Cargo.lock @@ -105,28 +105,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "axum" version = "0.8.9" @@ -252,8 +230,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -276,7 +252,6 @@ dependencies = [ "futures-util", "kube", "reqwest", - "rustls", "serde", "serde_json", "sqlx", @@ -450,12 +425,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "clap" version = "4.6.1" @@ -496,15 +465,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "colorchoice" version = "1.0.5" @@ -751,12 +711,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "dyn-clone" version = "1.0.20" @@ -863,12 +817,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futures" version = "0.3.32" @@ -984,10 +932,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -997,11 +943,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -1456,16 +1400,6 @@ dependencies = [ "syn", ] -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.99" @@ -1668,12 +1602,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "matchers" version = "0.2.0" @@ -2027,62 +1955,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" version = "1.0.45" @@ -2271,7 +2143,6 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", - "quinn", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -2325,12 +2196,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - [[package]] name = "rustc_version" version = "0.4.1" @@ -2359,7 +2224,6 @@ version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ - "aws-lc-rs", "log", "once_cell", "ring", @@ -2387,7 +2251,6 @@ version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ - "web-time", "zeroize", ] @@ -2424,7 +2287,6 @@ version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -3772,16 +3634,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "webpki-root-certs" version = "1.0.7" @@ -3883,15 +3735,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 +3768,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 +3787,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 +3799,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 +3811,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 +3829,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 +3841,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 +3853,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,7 +3865,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" diff --git a/services/web/src/client/App.tsx b/services/web/src/client/App.tsx index 78bb5e04c..3244e3b1d 100644 --- a/services/web/src/client/App.tsx +++ b/services/web/src/client/App.tsx @@ -1,6 +1,6 @@ import { useMemo, useRef, useState } from 'react' -import { Bot, CheckCircle2, Circle, CircleAlert, LoaderCircle, Plus, Send, Terminal } from 'lucide-react' -import { Button, Frame, Input, Rows, Tag } from 'regen-ui' +import { CheckCircle2, Circle, CircleAlert, LoaderCircle, Plus, Send, Terminal } from 'lucide-react' +import { Button, Input, Tag } from 'regen-ui' import type { WebRendererOutput, WebRendererTask } from '@centaur/rendering' type ChatMessage = { @@ -9,19 +9,31 @@ type ChatMessage = { text: string } +type ThreadSummary = { + id: string + lastMessage: string + status: string + title: string +} + type StreamEvent = { data: WebRendererOutput id?: number } +const INITIAL_THREAD_ID = newThreadId() + export function App() { - const [threadId, setThreadId] = useState(() => newThreadId()) + const [threadId, setThreadId] = useState(INITIAL_THREAD_ID) const [lastEventId, setLastEventId] = useState(0) const [title, setTitle] = useState('Centaur Web') const [status, setStatus] = useState('Idle') const [input, setInput] = useState('') const [messages, setMessages] = useState([]) const [tasks, setTasks] = useState([]) + const [threads, setThreads] = useState(() => [ + createThreadSummary(INITIAL_THREAD_ID) + ]) const [planTitle, setPlanTitle] = useState('') const [streaming, setStreaming] = useState(false) const assistantIdRef = useRef(null) @@ -35,6 +47,11 @@ export function App() { setInput('') setStreaming(true) setStatus('Starting') + updateThread(threadId, { + lastMessage: message, + status: 'Starting', + title: threadTitleFromMessage(message) + }) const userMessage: ChatMessage = { id: newMessageId(), role: 'user', text: message } const assistantMessage: ChatMessage = { id: newMessageId(), role: 'assistant', text: '' } assistantIdRef.current = assistantMessage.id @@ -58,6 +75,7 @@ export function App() { } catch (error) { const message = error instanceof Error ? error.message : String(error) setStatus('Error') + updateThread(threadId, { status: 'Error' }) updateAssistant(text => `${text}${text ? '\n\n' : ''}${message}`) } finally { setStreaming(false) @@ -67,6 +85,7 @@ export function App() { function applyOutput(output: WebRendererOutput) { if (output.type === 'web.status.update') { setStatus(output.status) + updateThread(threadId, { status: output.status }) return } if (output.type === 'web.message.delta') { @@ -87,9 +106,12 @@ export function App() { } if (output.type === 'web.title.update') { setTitle(output.title) + updateThread(threadId, { title: output.title }) return } - setStatus(output.error ? 'Error' : 'Complete') + const nextStatus = output.error ? 'Error' : 'Complete' + setStatus(nextStatus) + updateThread(threadId, { status: nextStatus }) if (output.answerMarkdown) { updateAssistant(text => (text.trim() ? text : output.answerMarkdown ?? '')) } @@ -109,7 +131,9 @@ export function App() { } function resetThread() { - setThreadId(newThreadId()) + const nextThreadId = newThreadId() + setThreads(current => [createThreadSummary(nextThreadId), ...current]) + setThreadId(nextThreadId) setLastEventId(0) setTitle('Centaur Web') setStatus('Idle') @@ -119,42 +143,48 @@ export function App() { assistantIdRef.current = null } + function selectThread(thread: ThreadSummary) { + if (streaming || thread.id === threadId) return + setThreadId(thread.id) + setLastEventId(0) + setTitle(thread.title === 'New thread' ? 'Centaur Web' : thread.title) + setStatus(thread.status) + setMessages([]) + setTasks([]) + setPlanTitle('') + assistantIdRef.current = null + } + + function updateThread(id: string, patch: Partial) { + setThreads(current => + current.map(thread => (thread.id === id ? { ...thread, ...patch } : thread)) + ) + } + return (
@@ -162,9 +192,16 @@ export function App() {

{title}

+ + {status} + Rust V2 Codex Renderer + Events {lastEventId} + + Tasks {completedTasks}/{taskCount} +
@@ -210,6 +247,7 @@ export function App() {

Activity

+ {planTitle &&
{planTitle}
}
{sortedTasks.length === 0 ? ( @@ -290,6 +328,20 @@ function upsertTask(tasks: WebRendererTask[], task: WebRendererTask): WebRendere return tasks.map(item => (item.id === task.id ? task : item)) } +function createThreadSummary(threadId: string): ThreadSummary { + return { + id: threadId, + lastMessage: '', + status: 'Idle', + title: 'New thread' + } +} + +function threadTitleFromMessage(message: string): string { + const trimmed = message.trim().replace(/\s+/g, ' ') + return trimmed.length > 42 ? `${trimmed.slice(0, 39)}...` : trimmed || 'New thread' +} + async function* parseSse(stream: ReadableStream): AsyncIterable { const reader = stream.getReader() const decoder = new TextDecoder() diff --git a/services/web/src/client/styles.css b/services/web/src/client/styles.css index 90d674e3f..b4d5a2963 100644 --- a/services/web/src/client/styles.css +++ b/services/web/src/client/styles.css @@ -1,8 +1,22 @@ :root { - color-scheme: light; - background: #f6f7f4; + color-scheme: dark; + background: #0d0f0d; + color: #eceee9; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --border: #2a302b; + --border-strong: #3d463f; + --panel: #131613; + --panel-raised: #181c18; + --panel-muted: #101310; + --text: #eceee9; + --text-muted: #9aa39b; + --text-dim: #687168; + --accent: #62d49f; + --accent-muted: #1c3c2d; + --danger: #ff8f8f; + --warning: #e8b86d; + --radius: 4px; } * { @@ -17,9 +31,7 @@ body, body { margin: 0; - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(246, 247, 244, 0)), - #f6f7f4; + background: #0d0f0d; } button, @@ -27,63 +39,101 @@ input { font: inherit; } +button, +input, +[role="button"] { + border-radius: var(--radius) !important; +} + .app-shell { display: grid; - grid-template-columns: minmax(260px, 320px) minmax(0, 1fr); + grid-template-columns: minmax(240px, 300px) minmax(0, 1fr); min-height: 100vh; - color: #191c1a; + color: var(--text); + background: + linear-gradient(180deg, rgba(98, 212, 159, 0.06), rgba(13, 15, 13, 0) 240px), + #0d0f0d; } .sidebar { display: flex; flex-direction: column; - gap: 18px; - padding: 22px; - border-right: 1px solid #d9ded6; - background: #eef2ec; + gap: 14px; + min-width: 0; + padding: 14px; + border-right: 1px solid var(--border); + background: #0f120f; } -.brand-row { - display: grid; - grid-template-columns: 40px minmax(0, 1fr); - gap: 12px; +.sidebar-top { + display: flex; align-items: center; } -.brand-mark { +.sidebar-top > * { + width: 100%; +} + +.thread-list { + display: flex; + flex: 1; + flex-direction: column; + gap: 8px; + min-height: 0; + overflow: auto; +} + +.thread-item { display: grid; - width: 40px; - height: 40px; - place-items: center; - border: 1px solid #ccd6c8; - border-radius: 8px; - background: #ffffff; - color: #176b55; + gap: 5px; + width: 100%; + min-height: 74px; + padding: 10px; + border: 1px solid transparent; + background: transparent; + color: inherit; + text-align: left; } -.brand-title { - font-size: 18px; - font-weight: 650; +.thread-item:hover:not(:disabled) { + border-color: var(--border); + background: #141814; } -.thread-key { +.thread-item.active { + border-color: var(--border-strong); + background: #181d19; +} + +.thread-item:disabled { + cursor: default; + opacity: 0.76; +} + +.thread-title, +.thread-key, +.thread-preview { + min-width: 0; overflow: hidden; - color: #667268; - font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; - font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } -.side-panel { - background: transparent; +.thread-title { + color: var(--text); + font-size: 13px; + font-weight: 650; } -.plan-title { - margin: 0; - color: #3c4940; - font-size: 14px; - line-height: 1.45; +.thread-key { + color: var(--text-dim); + font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; + font-size: 11px; +} + +.thread-preview { + color: var(--text-muted); + font-size: 12px; } .workspace { @@ -95,18 +145,18 @@ input { .topbar { display: flex; align-items: center; - justify-content: space-between; - min-height: 84px; - padding: 22px 28px; - border-bottom: 1px solid #e0e4dd; - background: rgba(255, 255, 255, 0.72); + min-height: 78px; + padding: 18px 24px; + border-bottom: 1px solid var(--border); + background: rgba(19, 22, 19, 0.88); backdrop-filter: blur(16px); } .topbar h1 { margin: 0 0 8px; overflow: hidden; - font-size: 24px; + color: var(--text); + font-size: 22px; font-weight: 700; line-height: 1.1; text-overflow: ellipsis; @@ -116,12 +166,18 @@ input { .topbar-meta { display: flex; flex-wrap: wrap; - gap: 8px; + gap: 6px; +} + +.topbar-meta > * { + border-color: var(--border-strong) !important; + background: #171b17 !important; + color: var(--text-muted) !important; } .content-grid { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(280px, 380px); + grid-template-columns: minmax(0, 1fr) minmax(280px, 360px); min-height: 0; } @@ -137,7 +193,7 @@ input { flex-direction: column; gap: 14px; overflow: auto; - padding: 24px 28px; + padding: 22px 24px; } .empty-pane { @@ -146,34 +202,36 @@ input { gap: 10px; width: fit-content; margin: auto; - padding: 12px 14px; - border: 1px solid #d9ded6; - border-radius: 8px; - background: #ffffff; - color: #566158; + padding: 10px 12px; + border: 1px solid var(--border-strong); + border-radius: var(--radius); + background: var(--panel-raised); + color: var(--text-muted); } .message { max-width: 860px; - padding: 14px 16px; - border: 1px solid #dde2da; - border-radius: 8px; - background: #ffffff; + padding: 13px 14px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--panel); + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.18); } .message.user { align-self: flex-end; - border-color: #cbd8d2; - background: #e9f4ef; + border-color: #34523f; + background: #142118; } .message.assistant { align-self: flex-start; + border-color: #30362f; } .message-role { margin-bottom: 8px; - color: #657068; + color: var(--text-dim); font-size: 11px; font-weight: 650; letter-spacing: 0; @@ -190,19 +248,20 @@ input { .task-body p, .task-output p { margin: 0; - white-space: pre-wrap; overflow-wrap: anywhere; + color: var(--text); line-height: 1.5; + white-space: pre-wrap; } pre { overflow: auto; max-width: 100%; margin: 0; - padding: 12px; - border: 1px solid #d6dad3; - border-radius: 8px; - background: #111815; + padding: 11px; + border: 1px solid var(--border-strong); + border-radius: var(--radius); + background: #090b09; color: #eef7f1; font-size: 12px; line-height: 1.45; @@ -211,62 +270,84 @@ pre { .composer { display: grid; grid-template-columns: minmax(0, 1fr) auto; - gap: 12px; - padding: 18px 28px 24px; - border-top: 1px solid #e0e4dd; - background: rgba(250, 251, 248, 0.9); + gap: 10px; + padding: 16px 24px 20px; + border-top: 1px solid var(--border); + background: rgba(15, 18, 15, 0.96); +} + +.composer input { + border-color: var(--border-strong) !important; + background: #151915 !important; + color: var(--text) !important; +} + +.composer input::placeholder { + color: var(--text-dim); } .activity { display: grid; grid-template-rows: auto minmax(0, 1fr); min-width: 0; - border-left: 1px solid #e0e4dd; - background: #f9faf7; + border-left: 1px solid var(--border); + background: var(--panel-muted); } .activity-header { - padding: 20px 20px 12px; + display: grid; + gap: 8px; + padding: 18px 18px 12px; } .activity-header h2 { margin: 0; - font-size: 15px; + color: var(--text); + font-size: 14px; font-weight: 700; } +.plan-title { + overflow: hidden; + color: var(--text-muted); + font-size: 12px; + line-height: 1.4; + text-overflow: ellipsis; + white-space: nowrap; +} + .task-list { display: flex; flex-direction: column; - gap: 10px; + gap: 9px; overflow: auto; - padding: 0 16px 20px; + padding: 0 14px 18px; } .task-empty { - padding: 14px; - border: 1px dashed #cdd4ca; - border-radius: 8px; - color: #68736a; + padding: 12px; + border: 1px dashed var(--border-strong); + border-radius: var(--radius); + color: var(--text-dim); font-size: 13px; } .task { display: flex; flex-direction: column; - gap: 10px; - padding: 12px; - border: 1px solid #dde2da; - border-radius: 8px; - background: #ffffff; + gap: 9px; + padding: 11px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--panel); } .task.in_progress { - border-color: #bad4ca; + border-color: #35624a; } .task.error { - border-color: #e2b8b8; + border-color: #6c3737; } .task-title { @@ -274,11 +355,19 @@ pre { grid-template-columns: 18px minmax(0, 1fr); gap: 8px; align-items: center; - color: #233028; + color: var(--text); font-size: 13px; font-weight: 650; } +.task.complete .task-title { + color: var(--accent); +} + +.task.error .task-title { + color: var(--danger); +} + .task-title span { overflow: hidden; text-overflow: ellipsis; @@ -290,7 +379,7 @@ pre { display: flex; flex-direction: column; gap: 8px; - color: #435047; + color: var(--text-muted); font-size: 12px; } @@ -315,7 +404,16 @@ pre { .sidebar { border-right: 0; - border-bottom: 1px solid #d9ded6; + border-bottom: 1px solid var(--border); + } + + .thread-list { + flex-direction: row; + overflow-x: auto; + } + + .thread-item { + min-width: 220px; } .content-grid { @@ -323,9 +421,9 @@ pre { } .activity { - min-height: 280px; + min-height: 260px; + border-top: 1px solid var(--border); border-left: 0; - border-top: 1px solid #e0e4dd; } } @@ -333,8 +431,8 @@ pre { .topbar, .message-list, .composer { - padding-right: 16px; - padding-left: 16px; + padding-right: 14px; + padding-left: 14px; } .composer { From 2ef3c3355ab6274f7e0c5a8850cf7c048b483763 Mon Sep 17 00:00:00 2001 From: Goksu Toprak Date: Tue, 2 Jun 2026 10:33:02 -0700 Subject: [PATCH 03/21] fix(web): match codex chat chrome --- services/web/src/client/App.tsx | 88 ++----------- services/web/src/client/styles.css | 197 ++++++----------------------- 2 files changed, 47 insertions(+), 238 deletions(-) diff --git a/services/web/src/client/App.tsx b/services/web/src/client/App.tsx index 3244e3b1d..c17c06748 100644 --- a/services/web/src/client/App.tsx +++ b/services/web/src/client/App.tsx @@ -1,7 +1,7 @@ -import { useMemo, useRef, useState } from 'react' -import { CheckCircle2, Circle, CircleAlert, LoaderCircle, Plus, Send, Terminal } from 'lucide-react' +import { useRef, useState } from 'react' +import { Plus, Send } from 'lucide-react' import { Button, Input, Tag } from 'regen-ui' -import type { WebRendererOutput, WebRendererTask } from '@centaur/rendering' +import type { WebRendererOutput } from '@centaur/rendering' type ChatMessage = { id: string @@ -30,16 +30,11 @@ export function App() { const [status, setStatus] = useState('Idle') const [input, setInput] = useState('') const [messages, setMessages] = useState([]) - const [tasks, setTasks] = useState([]) const [threads, setThreads] = useState(() => [ createThreadSummary(INITIAL_THREAD_ID) ]) - const [planTitle, setPlanTitle] = useState('') const [streaming, setStreaming] = useState(false) const assistantIdRef = useRef(null) - const taskCount = tasks.length - const completedTasks = tasks.filter(task => task.status === 'complete').length - const sortedTasks = useMemo(() => tasks, [tasks]) async function submit() { const message = input.trim() @@ -97,11 +92,9 @@ export function App() { return } if (output.type === 'web.task.upsert') { - setTasks(current => upsertTask(current, output.task)) return } if (output.type === 'web.plan.update') { - setPlanTitle(output.title) return } if (output.type === 'web.title.update') { @@ -138,8 +131,6 @@ export function App() { setTitle('Centaur Web') setStatus('Idle') setMessages([]) - setTasks([]) - setPlanTitle('') assistantIdRef.current = null } @@ -150,8 +141,6 @@ export function App() { setTitle(thread.title === 'New thread' ? 'Centaur Web' : thread.title) setStatus(thread.status) setMessages([]) - setTasks([]) - setPlanTitle('') assistantIdRef.current = null } @@ -192,16 +181,11 @@ export function App() {

{title}

- - {status} - + {status} Rust V2 Codex Renderer Events {lastEventId} - - Tasks {completedTasks}/{taskCount} -
@@ -209,19 +193,12 @@ export function App() {
- {messages.length === 0 ? ( -
- - Ready -
- ) : ( - messages.map(message => ( -
-
{message.role}
- -
- )) - )} + {messages.map(message => ( +
+
{message.role}
+ +
+ ))}
- -
-
-

Activity

- {planTitle &&
{planTitle}
} -
-
- {sortedTasks.length === 0 ? ( -
No activity
- ) : ( - sortedTasks.map(task => ) - )} -
-
) } -function TaskRow(props: { task: WebRendererTask }) { - const { task } = props - const icon = - task.status === 'complete' ? ( - - ) : task.status === 'error' ? ( - - ) : task.status === 'in_progress' ? ( - - ) : ( - - ) - - return ( -
-
- {icon} - {task.title} -
- {task.details && } - {task.output && } -
- ) -} - function MarkdownText(props: { className?: string; text: string }) { const parts = splitCodeFences(props.text) return ( @@ -322,12 +260,6 @@ function splitCodeFences(value: string): Array<{ kind: 'code' | 'text'; text: st return parts.length ? parts : [{ kind: 'text', text: value }] } -function upsertTask(tasks: WebRendererTask[], task: WebRendererTask): WebRendererTask[] { - const index = tasks.findIndex(item => item.id === task.id) - if (index < 0) return [...tasks, task] - return tasks.map(item => (item.id === task.id ? task : item)) -} - function createThreadSummary(threadId: string): ThreadSummary { return { id: threadId, diff --git a/services/web/src/client/styles.css b/services/web/src/client/styles.css index b4d5a2963..4f5ba8a1b 100644 --- a/services/web/src/client/styles.css +++ b/services/web/src/client/styles.css @@ -1,19 +1,17 @@ :root { color-scheme: dark; - background: #0d0f0d; - color: #eceee9; + background: #121212; + color: #f2f2f2; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - --border: #2a302b; - --border-strong: #3d463f; - --panel: #131613; - --panel-raised: #181c18; - --panel-muted: #101310; - --text: #eceee9; - --text-muted: #9aa39b; - --text-dim: #687168; - --accent: #62d49f; - --accent-muted: #1c3c2d; + --border: #303030; + --border-strong: #3f3f3f; + --panel: #181818; + --panel-raised: #202020; + --panel-muted: #151515; + --text: #f2f2f2; + --text-muted: #a8a8a8; + --text-dim: #787878; --danger: #ff8f8f; --warning: #e8b86d; --radius: 4px; @@ -31,7 +29,7 @@ body, body { margin: 0; - background: #0d0f0d; + background: #121212; } button, @@ -50,9 +48,7 @@ input, grid-template-columns: minmax(240px, 300px) minmax(0, 1fr); min-height: 100vh; color: var(--text); - background: - linear-gradient(180deg, rgba(98, 212, 159, 0.06), rgba(13, 15, 13, 0) 240px), - #0d0f0d; + background: #121212; } .sidebar { @@ -62,7 +58,7 @@ input, min-width: 0; padding: 14px; border-right: 1px solid var(--border); - background: #0f120f; + background: #1b1b1b; } .sidebar-top { @@ -74,6 +70,16 @@ input, width: 100%; } +.sidebar-top button { + border-color: transparent !important; + background: #2a2a2a !important; + color: var(--text) !important; +} + +.sidebar-top button:hover:not(:disabled) { + background: #333333 !important; +} + .thread-list { display: flex; flex: 1; @@ -97,12 +103,12 @@ input, .thread-item:hover:not(:disabled) { border-color: var(--border); - background: #141814; + background: #262626; } .thread-item.active { - border-color: var(--border-strong); - background: #181d19; + border-color: transparent; + background: #303030; } .thread-item:disabled { @@ -148,7 +154,7 @@ input, min-height: 78px; padding: 18px 24px; border-bottom: 1px solid var(--border); - background: rgba(19, 22, 19, 0.88); + background: rgba(18, 18, 18, 0.9); backdrop-filter: blur(16px); } @@ -171,13 +177,12 @@ input, .topbar-meta > * { border-color: var(--border-strong) !important; - background: #171b17 !important; + background: #242424 !important; color: var(--text-muted) !important; } .content-grid { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(280px, 360px); + display: block; min-height: 0; } @@ -196,19 +201,6 @@ input, padding: 22px 24px; } -.empty-pane { - display: inline-flex; - align-items: center; - gap: 10px; - width: fit-content; - margin: auto; - padding: 10px 12px; - border: 1px solid var(--border-strong); - border-radius: var(--radius); - background: var(--panel-raised); - color: var(--text-muted); -} - .message { max-width: 860px; padding: 13px 14px; @@ -220,8 +212,8 @@ input, .message.user { align-self: flex-end; - border-color: #34523f; - background: #142118; + border-color: #3a3a3a; + background: #2a2a2a; } .message.assistant { @@ -244,9 +236,7 @@ input, gap: 10px; } -.markdown-text p, -.task-body p, -.task-output p { +.markdown-text p { margin: 0; overflow-wrap: anywhere; color: var(--text); @@ -261,8 +251,8 @@ pre { padding: 11px; border: 1px solid var(--border-strong); border-radius: var(--radius); - background: #090b09; - color: #eef7f1; + background: #101010; + color: #f2f2f2; font-size: 12px; line-height: 1.45; } @@ -273,12 +263,12 @@ pre { gap: 10px; padding: 16px 24px 20px; border-top: 1px solid var(--border); - background: rgba(15, 18, 15, 0.96); + background: rgba(18, 18, 18, 0.96); } .composer input { border-color: var(--border-strong) !important; - background: #151915 !important; + background: #202020 !important; color: var(--text) !important; } @@ -286,117 +276,10 @@ pre { color: var(--text-dim); } -.activity { - display: grid; - grid-template-rows: auto minmax(0, 1fr); - min-width: 0; - border-left: 1px solid var(--border); - background: var(--panel-muted); -} - -.activity-header { - display: grid; - gap: 8px; - padding: 18px 18px 12px; -} - -.activity-header h2 { - margin: 0; - color: var(--text); - font-size: 14px; - font-weight: 700; -} - -.plan-title { - overflow: hidden; - color: var(--text-muted); - font-size: 12px; - line-height: 1.4; - text-overflow: ellipsis; - white-space: nowrap; -} - -.task-list { - display: flex; - flex-direction: column; - gap: 9px; - overflow: auto; - padding: 0 14px 18px; -} - -.task-empty { - padding: 12px; - border: 1px dashed var(--border-strong); - border-radius: var(--radius); - color: var(--text-dim); - font-size: 13px; -} - -.task { - display: flex; - flex-direction: column; - gap: 9px; - padding: 11px; - border: 1px solid var(--border); - border-radius: var(--radius); - background: var(--panel); -} - -.task.in_progress { - border-color: #35624a; -} - -.task.error { - border-color: #6c3737; -} - -.task-title { - display: grid; - grid-template-columns: 18px minmax(0, 1fr); - gap: 8px; - align-items: center; - color: var(--text); - font-size: 13px; - font-weight: 650; -} - -.task.complete .task-title { - color: var(--accent); -} - -.task.error .task-title { - color: var(--danger); -} - -.task-title span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.task-body, -.task-output { - display: flex; - flex-direction: column; - gap: 8px; - color: var(--text-muted); - font-size: 12px; -} - -.spin { - animation: spin 900ms linear infinite; -} - .min-w-0 { min-width: 0; } -@keyframes spin { - to { - transform: rotate(360deg); - } -} - @media (max-width: 900px) { .app-shell { grid-template-columns: 1fr; @@ -417,13 +300,7 @@ pre { } .content-grid { - grid-template-columns: 1fr; - } - - .activity { - min-height: 260px; - border-top: 1px solid var(--border); - border-left: 0; + display: block; } } From c2fa7362d412dc5b36fe6fc9fa6150da92043bf7 Mon Sep 17 00:00:00 2001 From: Goksu Toprak Date: Tue, 2 Jun 2026 10:38:29 -0700 Subject: [PATCH 04/21] fix(web): pin composer to bottom --- services/web/src/client/styles.css | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/services/web/src/client/styles.css b/services/web/src/client/styles.css index 4f5ba8a1b..35bd5d4c6 100644 --- a/services/web/src/client/styles.css +++ b/services/web/src/client/styles.css @@ -146,6 +146,7 @@ input, display: grid; grid-template-rows: auto minmax(0, 1fr); min-width: 0; + min-height: 0; } .topbar { @@ -182,7 +183,9 @@ input, } .content-grid { - display: block; + display: grid; + grid-template-columns: minmax(0, 1fr); + height: 100%; min-height: 0; } @@ -191,12 +194,14 @@ input, grid-template-rows: minmax(0, 1fr) auto; min-width: 0; min-height: 0; + height: 100%; } .message-list { display: flex; flex-direction: column; gap: 14px; + min-height: 0; overflow: auto; padding: 22px 24px; } @@ -300,7 +305,7 @@ pre { } .content-grid { - display: block; + display: grid; } } From 1230cb0bfef6d6d138610f7211bafbd9f3c70a51 Mon Sep 17 00:00:00 2001 From: Goksu Toprak Date: Tue, 2 Jun 2026 10:42:43 -0700 Subject: [PATCH 05/21] fix(web): align chat chrome actions --- services/web/src/client/App.tsx | 38 +++++++++++------ services/web/src/client/styles.css | 66 ++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 25 deletions(-) diff --git a/services/web/src/client/App.tsx b/services/web/src/client/App.tsx index c17c06748..8ede9f23d 100644 --- a/services/web/src/client/App.tsx +++ b/services/web/src/client/App.tsx @@ -1,5 +1,5 @@ import { useRef, useState } from 'react' -import { Plus, Send } from 'lucide-react' +import { Blocks, Clock3, Search, Send, SquarePen } from 'lucide-react' import { Button, Input, Tag } from 'regen-ui' import type { WebRendererOutput } from '@centaur/rendering' @@ -26,7 +26,6 @@ const INITIAL_THREAD_ID = newThreadId() export function App() { const [threadId, setThreadId] = useState(INITIAL_THREAD_ID) const [lastEventId, setLastEventId] = useState(0) - const [title, setTitle] = useState('Centaur Web') const [status, setStatus] = useState('Idle') const [input, setInput] = useState('') const [messages, setMessages] = useState([]) @@ -35,6 +34,8 @@ export function App() { ]) const [streaming, setStreaming] = useState(false) const assistantIdRef = useRef(null) + const activeThread = threads.find(thread => thread.id === threadId) ?? threads[0] + const title = activeThread?.title ?? 'New chat' async function submit() { const message = input.trim() @@ -98,7 +99,6 @@ export function App() { return } if (output.type === 'web.title.update') { - setTitle(output.title) updateThread(threadId, { title: output.title }) return } @@ -128,7 +128,6 @@ export function App() { setThreads(current => [createThreadSummary(nextThreadId), ...current]) setThreadId(nextThreadId) setLastEventId(0) - setTitle('Centaur Web') setStatus('Idle') setMessages([]) assistantIdRef.current = null @@ -138,7 +137,6 @@ export function App() { if (streaming || thread.id === threadId) return setThreadId(thread.id) setLastEventId(0) - setTitle(thread.title === 'New thread' ? 'Centaur Web' : thread.title) setStatus(thread.status) setMessages([]) assistantIdRef.current = null @@ -153,11 +151,25 @@ export function App() { return (
@@ -254,13 +506,11 @@ export function App() {

{title}

-
- {status !== 'Idle' && {status}} - Rust V2 - Codex - Renderer - Events {lastEventId} -
+ {status !== 'Idle' && ( +

+ {status} · Events {lastEventId} +

+ )}
@@ -273,7 +523,9 @@ export function App() { {message.role === 'assistant' && messageTasks.length > 0 && ( )} @@ -285,24 +537,7 @@ export function App() { })} - { - event.preventDefault() - void submit() - }} - > - setInput(event.target.value)} - placeholder="Ask Centaur for anything" - value={input} - /> - - + {composer} @@ -310,6 +545,99 @@ export function App() { ) } +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} + > +
+