Files
Mark Tai c6d1f7d5e0 feat(web): resume imported / host-less sessions from the web (#4905)
An imported or otherwise unbound session (no host, no runner) couldn't run from
the web: it read as reachable (so the first message dropped against a runner
that can't start) or dead-ended on the terminal reconnect path.

The fix is mostly server-side liveness. An imported transcript is a
native-harness session that only runs in a runner on a host, never in-process,
so report it as runner_online=false via a new `imported` connectivity marker
(keyed on the omnigent.import.source label — the sibling of the existing fork
`needs_workspace` marker, computed in the same query). With that, the open view
routes to the EXISTING host picker (ResumeWithDirectoryDialog) instead of the
dead end. That picker — the same one forks and new-chat use — binds the session
to an online host + workspace (defaulting to the caller's current host) and
launches a runner via the existing POST /v1/hosts/{id}/runners path. No new
host-selection UI, no new launch route.

The picker is offered only when the resume will actually work
(unboundSessionResumableInApp): the caller must OWN the session (launch_runner
requires owner — a shared non-owner 404s), and for imports the harness must
reconstruct context from the omnigent transcript so it carries onto a chosen
host. Kimi has no resume path, and kiro/qwen resume only from a local recording
that lives on the original machine, so those route to the terminal reconnect
path instead of a picker that would start blank.

Also:
- Skip the cold-boot startup grace for imports so the picker shows at once.
- Generalize ResumeWithDirectoryDialog to prefill from the session's own fields
  when there is no fork source.
- `omnigent import` prints the session's browser URL instead of the bare id.

Co-authored-by: Isaac

Signed-off-by: Mark Tai <mark.tai@databricks.com>
Co-authored-by: Mark Tai <mark.tai@databricks.com>
2026-08-19 10:16:18 -07:00
..

web

The web UI for omnigent server --agent <agent>. SPA built with Vite + React + TypeScript + Tailwind v4 + shadcn/ui. Talks to the current Omnigent API surface (/v1/agents, /v1/sessions, session-scoped /v1/sessions/{id}/resources/files).

Develop

In one terminal, start the omnigent server (default port 6767). Use --agent to pre-register one or more agents at startup (accepts a YAML file or an agent-image directory; can be repeated):

.venv/bin/omnigent server --agent examples/hello_world.yaml

In another terminal, start the Vite dev server (port 5173):

cd web
pnpm install
pnpm run dev

The Vite dev server proxies /v1 and /api to http://localhost:6767. Set OMNIGENT_URL to override the proxy target:

OMNIGENT_URL=http://localhost:9000 pnpm run dev

To develop against a Databricks workspace-hosted server, point OMNIGENT_URL at the bare workspace origin — the dev proxy fills in the /api/2.0/omnigent mount and authenticates with your databricks auth login token automatically:

OMNIGENT_URL=https://my-workspace.databricks.com pnpm run dev

Additional omnigent server options:

Flag Default Description
--host 127.0.0.1 Host to bind to
-p / --port 6767 Port to listen on
--database-uri <data-dir>/chat.db Database URI for stores
--artifact-location <data-dir>/artifacts Path for artifact storage
-c / --config (none) Path to YAML config file
--execution-timeout 7200 Max wall-clock seconds per execution
--agent (none) Pre-register an agent (repeatable)

Build + serve from the Omnigent server

cd web
pnpm run build

Vite writes the bundle to ../omnigent/server/static/web-ui/ (configured in vite.config.ts). When that directory exists and contains index.html, the FastAPI app in omnigent/server/app.py mounts it at /. After a build:

.venv/bin/omnigent server --agent examples/hello_world.yaml
# open http://localhost:6767/

Lint + format

pnpm run lint          # oxlint .
pnpm run lint:fix      # oxlint --fix .
pnpm run format        # prettier --write .
pnpm run format:check  # prettier --check .
pnpm run type-check    # tsc -b

pnpm run type-check runs in CI as part of the Pre-commit checks job (.github/workflows/lint.yml) and gates merge. Run it locally before committing any change under web/.

Test

pnpm run test          # vitest run
pnpm run test:watch    # vitest in watch mode

Reducer parity

The TypeScript reducer at src/lib/blockStream.ts is a hand-mirror of the Python reducer at sdks/python-client/omnigent_client/_stream.py. Same for:

TS file Mirrors
src/lib/blocks.ts omnigent_client/_blocks.py
src/lib/events.ts omnigent_client/_events.py
src/lib/types.ts minimal subset of omnigent_client/_types.py
src/lib/sse.ts omnigent_client/_sse.py
src/lib/blockStream.ts omnigent_client/_stream.py
src/lib/blockStream.test.ts tests/frontends/sdk/test_stream.py

There is no cross-language CI gate today. When _stream.py changes for a real bug (e.g. new harness quirk, dedup edge case), the TypeScript port can lag — drift surfaces only when someone next runs pnpm run test after a behavioral change. Workflow when _stream.py changes:

  1. Read the diff to _stream.py (or _blocks.py / _events.py).
  2. Update blockStream.ts (or blocks.ts / events.ts) to match.
  3. Add or update a case in blockStream.test.ts that pins the new behavior — same shape as test_stream.py.
  4. pnpm run test → green.

If we ever decide cross-language fixture parity is worth the maintenance burden, we'd port the captured-fixture approach used for test_stream.py.

web-only divergences

web carries a few constructs the Python SDK doesn't, on purpose. They're listed here so a future maintainer doesn't try to "restore parity" by mirroring them across.

  • UserMessageBlock (in blocks.ts) — surfaces persisted user message items as blocks so the bubble walker sees a single flat list. The SDK's BlockStream.stream() never emits user messages (its consumers receive the user input as the caller's own argument, not back through the stream).
  • BlockContext.responseId + BlockContext.itemId — populated by the TS reducer from the SSE wire format (response.created.response.id and event.item.id / event.item.response_id on output_item.done) so each block knows its server origin. The TS events ToolCall / ToolResult / MessageDone / NativeToolCall carry itemId + responseId to thread the values through.
  • Flat block storage in chatStore.blocks, grouped at render time by buildBubbles keyed on ctx.responseId. The SDK has no equivalent — its consumers iterate the block stream procedurally without a stateful store.

When _stream.py / _events.py / _blocks.py change for a substantive reason (new event type, new dedup edge case), continue to mirror the behavioral changes here; just leave the divergences above alone.

Stack

  • Vite + React 19 + TypeScript
  • Tailwind v4 (@import "tailwindcss", no config file)
  • shadcn/ui (radix-nova preset, neutral base, CSS variables)
  • TanStack Query, Zustand, React Router v7
  • streamdown (+ @streamdown/code, @streamdown/math, @streamdown/mermaid), shiki, framer-motion, cmdk, react-hotkeys-hook, use-stick-to-bottom, next-themes, react-hook-form, zod
  • Lint: oxlint. Format: prettier.