Files
Corey Zumar 07afe65150 fix(files): browse outside the workspace behind a slash-merging proxy (#5174)
* fix(files): browse outside the workspace behind a slash-merging proxy

The file panel showed "No files in workspace" for any directory above or
outside the working folder when the app is served through the Databricks
Apps front door, even though the picker dropdown listed those very
directories.

Absolute browse locations were marked by a leading %2F in the filesystem
route's {path} segment. That %2F decodes to a boundary // which the front
door merges back to a single / (//health 301-redirects to /health), so
/Users/me arrived at the server as a workspace-relative Users/me and listed
a nonexistent path under the workspace. The picker uses the host filesystem
endpoint, which already sends bare slashes, so it was unaffected -- hence
paths in the dropdown but an empty tree.

Follow the host endpoint's own convention instead: send the path with
literal slashes and no leading %2F (nothing for a proxy to merge) and name
an absolute location out of band with ?base=host. The server re-adds the
leading slash, applies the same owner gate (an absolute path is owner-only,
so base=host is not a way around it), and re-encodes %2F only on the
internal server->runner leg, which is not proxied. A genuine leading slash
still works for a direct, unproxied client.

Covers list, lazy directory expand, search, file read, and write; the
filesystem root (base=host with an empty segment) is handled on the no-path
route.

Test plan:
- e2e_ui test_absolute_browse_survives_a_slash_merging_proxy reproduces the
  front door's %2F-decode + //-merge on the session's filesystem/search
  requests and asserts the tree lists an outside directory. Reverting the
  web change makes it fail with "No files in workspace".
- collaboration test asserts the owner gate applies to the base=host form
  (collaborator 403, owner 200), so it is not an escape hatch.
- route tests pin base=host -> absolute (%2F) forwarding, a bare path
  without base staying workspace-relative, and base=host root -> "/".

Co-authored-by: Isaac
Signed-off-by: dbczumar <corey.zumar@databricks.com>

* refactor(files): reuse the shared browse-location helpers for file read/write

Address Polly's non-blocking notes on PR #5174. fetchFileContent and
writeFileContent reimplemented the "strip leading slash + ?base=host" wire
rule inline; route them through the exported browseLocationSegment /
browseLocationBase so the slash-merge-safe contract lives in one place.
Behavior is identical (verified: same URL for relative and absolute paths).

Also note in the list_environment_root base=host branch that the delegate
(read_or_list_environment_path) runs its own owner-gated _validate_session,
so the delegation is authorized there.

Co-authored-by: Isaac
Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(files): open a file at an absolute browse location by its absolute path

A follow-on to the browse-outside-workspace fix: navigating to a directory
outside the workspace (e.g. /tmp) now lists files, but OPENING one 404'd.
The tree names files by their bare name relative to the browsed location, and
openTreeFile re-attached that name only for workspace-relative locations --
for an absolute location it handed the viewer the bare name, which the viewer
then looked up under the workspace root (nonexistent) → 404.

Always re-attach the browsed location: for an in-workspace location that
yields a workspace-relative path, and for one outside the workspace the file's
absolute path, which fetchFileContent already requests host-absolutely
(base=host, per the earlier consolidation).

Tests: FilesPanel unit test (open a file at an absolute location → the
absolute path reaches onFileSelect; mutation-verified against the old
bare-name form) and a useFileContent test pinning the absolute wire form
(bare path + base=host).

Co-authored-by: Isaac
Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(files): keep the browsed directory when the file viewer closes

Opening a file swaps the files panel for the FileViewer in the rail's
content slot, unmounting the panel -- and the browse location was plain
component state, so closing the viewer remounted the panel at the
workspace root instead of the directory the file was opened from
(browse to /tmp, open a log, go back -> dumped at the workspace root).

Remember the location in a module-level per-conversation cache, the same
pattern FolderTree already uses for its expanded-folders set across the
identical unmount/remount. Seeded at mount, written on every navigation
(cleared when returning to the workspace root), and re-read on an
in-place conversation switch so another session still opens at its own
root -- the cache is keyed by conversation, so no location ever leaks
across sessions.

Tests: FilesPanel unit tests for the unmount/remount round trip and the
per-conversation scoping (round trip mutation-verified), and the
outside-workspace e2e now opens a file at the browsed directory, closes
the viewer via its tab, and asserts the header still names that
directory (also mutation-verified: against a pre-fix build it fails
with the header back at the workspace path).

Co-authored-by: Isaac
Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-21 23:07:02 -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.