uiserver

package
v0.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 14, 2026 License: MIT Imports: 32 Imported by: 0

Documentation

Overview

editorlink.go implements GetEditorLink (D-05/D-06/D-07/D-08): it turns a repo-relative path plus an optional line/col into an editor URI built from a {path}/{line}/{col} template.

Every configuration state this handler answers with is an ANSWER, never an error: no template configured, a template disabled by the operator, and a template that fails validation all render as a SUCCESSFUL response carrying EDITOR_LINK_AVAILABILITY_NO_TEMPLATE or EDITOR_LINK_AVAILABILITY_TEMPLATE_INVALID with a populated reason — the same "empty is a successful response" discipline GetPermalink already follows (D-07). The ONE case that IS an error is a rejected path argument, handled by (*query.Engine).ValidateRepoRelativePath (SRV-05) below.

The absolute path substituted for {path} is filepath.Join(repo root, relative path) — computed AFTER ValidateRepoRelativePath has accepted the relative path (which already applies both the string-level and the EvalSymlinks confinement gate) — and is NEVER the symlink-resolved form: editors key on the workspace folder the user opened, and /tmp/x and /private/tmp/x are different workspaces to VS Code (D-08). Paths are POSIX only; native Windows support was dropped at v0.4.0 (WSL2 only, STATE.md), so under WSL2 the POSIX rules below apply.

Package uiserver: livepublish.go is the server-side engine of live push (Phase 6, LIV-01/LIV-03): a store watcher that learns a re-index happened, a change detector that decides whether that is worth telling anyone, and a bounded, coalescing fan-out registry that hands the news to every open stream without letting a slow reader hurt a fast one.

This file has NO HTTP or Connect dependency of any kind — the streaming handler consumes livePublisher, not the other way round. Every type and function here is unexported: wiring this into the wire layer is the handler's job, not this file's.

Package uiserver implements codegraph ui's local, loopback-only Connect RPC server. originguard.go is the first piece to exist in this package (SRV-02): the exact-match Origin/Host control that must run before any RPC handler is reachable at all — the ROADMAP's blocking order names this file, not a handler, as this phase's Task 1.

permalink.go implements GetPermalink (D-06/D-07/D-08/D-09): it turns a repo-relative path and an optional line/end_line anchor into a GitHub blob URL pinned to the commit the INDEX was built at, never HEAD, so the remote view matches what the UI just showed.

Everything this handler produces from git introspection is an ANSWER, never an error: no remote, a non-GitHub forge, an unpushed commit, or an absent commit_sha all render as a SUCCESSFUL response carrying PERMALINK_AVAILABILITY_NO_LINK or PERMALINK_AVAILABILITY_LINKABLE_UNVERIFIED with a populated reason — the same "empty is a successful response" discipline Explore already follows (D-07). The one case that IS an error is an invalid path argument, handled by (*query.Engine). ValidateRepoRelativePath (SRV-05) below.

spa.go implements codegraph ui's SPA app-shell handler (D-12): it serves the embedded, committed SvelteKit build (web.BuildFS, BLD-02) at "/" on the same mux the Connect RPC handler is already mounted on (D-09 — Go 1.26 http.ServeMux's most-specific-pattern-wins resolves "/codegraph.ui.v1.UIService/" ahead of "/" with no allowlist to keep in sync). D-10's asset-vs-route discrimination is implemented as a three-way rule: a miss under the immutable-asset prefix is a 404 — deliberately, by design, never a fallback, because serving HTML for a missing hashed chunk produces a browser MIME/module error that points nowhere near the real cause; a hit anywhere else that resolves to a real file is served as itself; anything else falls back to index.html so SvelteKit's client router can take over. D-11's two-class Cache-Control policy and a same-origin Content-Security-Policy (with script-src hashes derived from the embedded index.html itself) are set unconditionally on every response.

Package-level doc comment lives in originguard.go — not repeated here.

Package uiserver: watchtimeout.go implements D-02 — clearing http.Server's absolute WriteTimeout for exactly one path, the generated WatchGraph streaming procedure, and no other.

writeTimeout (server.go) is an ABSOLUTE deadline on an entire response write, sized for a unary rpc's slowest legitimate response (server.go's own comment: "roughly two orders of magnitude above" that). A server-streaming rpc's response is intentionally long-lived — it has no "response finished" moment short of the client disconnecting — so the same 60-second bound that protects every unary rpc would kill a healthy stream. Clearing it here, for this path only, is the deliberate relaxation D-02 chose over its two rejected alternatives: WriteTimeout: 0 server-wide (trades away a documented safety property across all 13 unary rpcs and the SPA handler to serve one method) and forced client reconnects under 60s (churns every open tab at least once a minute, close to the reconnect storm LIV-03 exists to prevent).

connectrpc.com/connect v1.20.0's checkServerStreamsCanFlush (protocol.go:347-350) gates entry to EVERY server-streaming handler on a bare type assertion against http.Flusher, with no Unwrap() traversal:

if _, flushable := responseWriter.(http.Flusher); requiresFlusher && !flushable {
	return NewError(CodeInternal, fmt.Errorf("%T does not implement http.Flusher", responseWriter))
}

A middleware that wraps w in any custom type — even one that itself forwards Flush by embedding — hides the assertion's target from that check and the handler is refused outright, not merely degraded. clearWatchDeadline therefore calls http.NewResponseController(w) for its side effect only and passes the ORIGINAL w straight through to next, the exact shape originHostGuard (originguard.go) already establishes as this package's one other middleware.

Index

Constants

View Source
const DefaultAddr = "127.0.0.1:0"

DefaultAddr is the ephemeral loopback bind address D-07 mandates: with a ":0" port, no concrete port exists until net.Listen actually returns, so Listen always resolves an empty Options.Addr to this value rather than a fixed, potentially-colliding port. A second `codegraph ui` simply gets a different port.

View Source
const EditorTemplateMaxBytes = 2048

EditorTemplateMaxBytes bounds an editor URI template's length (T-09-10, denial of service via a pathological template). Both the CLI's startup resolver and this handler's per-request validation enforce the SAME limit through the SAME validator.

Variables

This section is empty.

Functions

func ValidateEditorTemplate added in v0.13.0

func ValidateEditorTemplate(template string) error

ValidateEditorTemplate reports whether template is a well-formed, allowlisted editor URI template (D-13). nil means valid; a non-nil error's text is used verbatim as GetEditorLink's TEMPLATE_INVALID reason and as the CLI's startup-failure message (D-17) — ONE validator, exercised from both call sites, never a second implementation. Each distinct cause has its own message text; none shares wording with another (permalink.go:33-52's distinct-reason discipline).

Types

type EditorLinkOptions added in v0.13.0

type EditorLinkOptions struct {
	// Template is the effective default editor URI template. Empty
	// when Source is EditorTemplateNone or EditorTemplateDisabled.
	Template string
	// Source records where Template came from (or that none was
	// configured / editor links are disabled).
	Source EditorTemplateSource
	// Editor is the discovered launcher/preset id (e.g. "code",
	// "cursor", "idea") when Source is EditorTemplateDiscovered, and
	// empty otherwise.
	Editor string
}

EditorLinkOptions is the server's frozen-at-startup editor-link configuration (D-14/D-15/D-16), written ONLY by internal/cli's startup resolver (flag, env, discovery) and copied into uiService at Listen — never re-read per request, the same startup-frozen discipline the publisher and repoPath already follow.

type EditorPreset added in v0.13.0

type EditorPreset struct {
	ID       string
	Name     string
	Template string
}

EditorPreset is the Go-side twin of uiv1.EditorPreset (D-18): one of the three fixed editor choices GetEditorLink offers on every response so the browser's picker never holds a second copy of a template it constructs itself.

func EditorPresets added in v0.13.0

func EditorPresets() []EditorPreset

EditorPresets returns a FRESH slice of exactly three presets, in this order: VS Code, Cursor, JetBrains (D-18). VS Code's template is confirmed against code.visualstudio.com's own command-line documentation (`code --goto {file}:{line}:{col}` maps onto the URI handler this template uses).

The Cursor and JetBrains templates are [ASSUMED] (09-RESEARCH.md A1/A2) — neither vendor documents its URI scheme as a stable public contract at time of writing. They are corroborated by spatie/ignition, a shipped, widely-used error-page editor-link table (`editor_options`), whose two entries use the SAME absolute-path `{path}`/`{line}` shape D-08 produces — which is why the IDE-side `open?file=` form was chosen here over the JetBrains Toolbox `navigate/reference?project=…` form, which wants a project-relative path GetEditorLink does not have. Other JetBrains IDEs (GoLand, PyCharm, WebStorm, RubyMine, CLion, PhpStorm, Rider) each use their own URI scheme; plan 09-02's discovery selects the matching one automatically, and a custom template can name any of them by hand via the picker's custom field (D-18).

Per 09-CONTEXT.md D-18/A4, exactly these three editors are presets — no fourth editor lacking a stable public URI scheme is named here as either a preset or a discovery target (enforced by a repo-wide check this plan's own tests run against this file's own text).

type EditorTemplateSource added in v0.13.0

type EditorTemplateSource int

EditorTemplateSource records where a server's EFFECTIVE DEFAULT editor template came from (D-14/D-07). The zero value, EditorTemplateNone, means no source configured a template at all — distinct from EditorTemplateDisabled, which means the operator explicitly turned editor links off (D-16).

const (
	// EditorTemplateNone is the zero value: no flag, env var, or
	// discovery ever configured a template.
	EditorTemplateNone EditorTemplateSource = iota
	// EditorTemplateFlag means `--editor-url` set the template.
	EditorTemplateFlag
	// EditorTemplateEnv means CODEGRAPH_EDITOR_URL set the template.
	EditorTemplateEnv
	// EditorTemplateDiscovered means startup-time editor discovery
	// (plan 09-02) found an installed editor and selected its preset.
	EditorTemplateDiscovered
	// EditorTemplateDisabled means the operator explicitly disabled
	// editor links (`--no-editor-url` or CODEGRAPH_NO_EDITOR_URL,
	// D-16) — discovery is skipped entirely in this state.
	EditorTemplateDisabled
)

type Options

type Options struct {
	// RepoPath is the single repository uiService answers every rpc
	// for.
	RepoPath string

	// Addr is the bind address net.Listen receives, defaulting to
	// DefaultAddr when empty. Nothing outside this package writes this
	// field in v1 — no flag, no environment variable, no configuration
	// file reads it (D-08). A later `--host`/`--port` is therefore an
	// override of a field that already exists, not a restructuring
	// (SRV-03).
	Addr string

	// EditorLink is the server's frozen-at-startup editor-link default
	// (D-14/D-15/D-16), written ONLY by internal/cli's startup
	// resolver — flag, env, discovery — and copied into uiService here
	// at Listen, never re-read per request (plan 09-01, Pattern 3).
	EditorLink EditorLinkOptions
}

Options configures Listen. RepoPath, Addr and EditorLink are the ONLY three fields — asserted by TestUIServiceHoldsNoStoreTypedField's sibling assertion in server_test.go, a reflected field-set equality with a length-3 check — so neither an unwired option nor a second browser-launch owner can be reintroduced. Browser-launch policy and the launch itself live entirely in internal/cli, never here: one owner means neither an unused option nor a double launch (review MEDIUM 01-06).

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is a bound codegraph ui listener: bound the instant Listen returns, serving only once Serve is called. The split exists because the port cannot be published before the bind, and the bind cannot live inside the blocking call — see Listen's own doc comment.

func Listen

func Listen(o Options) (*Server, error)

Listen binds addr (defaulting to DefaultAddr), extracts the concrete bound port, builds the http.ServeMux with the UIService Connect handler mounted on it, wraps the WHOLE mux in originHostGuard — the guard is the outermost http.Handler, never a connect interceptor, because an interceptor runs after routing and message parsing have already begun and would not protect Phase 2's static SPA routes either — and returns. The bind happens here, inside Listen, and never inside Serve: with an ephemeral ":0" bind the port does not exist until net.Listen returns, and (*Server).URL must already be real the instant Listen returns, which is only possible because Listen itself never blocks.

func (*Server) Close

func (s *Server) Close() error

Close closes the listener without ever calling Serve — for a caller that binds and then decides not to serve. Stops the publisher first (see stopPublisher's own doc comment), so Listen followed directly by Close leaks no fsnotify watcher or debouncer goroutine.

func (*Server) Serve

func (s *Server) Serve(ctx context.Context) error

Serve blocks until ctx is cancelled or the underlying http.Server stops on its own, normalizing http.ErrServerClosed to nil at every return path — codegraph ui returns this value straight from RunE, so a normalized nil is the difference between a clean Ctrl-C and a spurious non-zero exit code.

http.Server.Serve blocks, so a literal "serve, then on ctx.Done() shut down" would never reach the cancellation branch until serving had already stopped by itself. Serve therefore runs the blocking Serve call in a goroutine over a BUFFERED capacity-1 error channel and selects on that channel against ctx.Done(): buffered, so the goroutine can always deposit its result and exit even if nobody is left reading — an unbuffered channel here would leak the goroutine on the cancellation path.

func (*Server) URL

func (s *Server) URL() string

URL returns the server's connectable base URL, valid the instant Listen returns — before Serve is ever called.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL