# Contributing Gloom is one Bun and React codebase with three front ends: the terminal app, rendered with [OpenTUI](https://opentui.com/); the desktop app, built on Electrobun; and the web app at term.gloom.sh. Almost every feature is a plugin drawn with a shared UI kit, so the same pane runs in all three. Bug reports and pull requests are welcome. Please follow the [code of conduct](CODE_OF_CONDUCT.md), and report security issues privately as described in [SECURITY.md](SECURITY.md). Questions or ideas: join #contributors on [Discord](https://gloom.sh/discord). ## Running locally Requires [Bun](https://bun.sh) 1.3.11, the version CI runs (`packageManager` in `package.json`). Install it with `curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.11"`, and recheck a test that fails only on another version against 1.3.11 before chasing it. ```bash git clone https://github.com/gloom-sh/gloomberb.git cd gloomberb bun install bun dev # terminal app, restarted on save bun run desktop:dev # desktop app ``` Both use your real profile (`~/.gloomberb`, or the XDG folders on a Linux install that never had one). To develop against a throwaway profile, point `GLOOMBERB_HOME` somewhere else: ```bash GLOOMBERB_HOME=/tmp/gloomberb-dev bun dev ``` ## Agent skills The project's own skills for coding agents live in `.agents/skills`. The third-party OpenTUI skill is not checked in; `skills-lock.json` records where it comes from. To install it into `.agents/skills/opentui` (ignored by git), run: ```bash bunx skills experimental_install ``` This fetches the latest upstream version. ## Repository layout | Path | Contents | |---|---| | `src/plugins/builtin/` | The built-in plugins: Portfolio, Ticker Research, News and every other product area | | `src/plugins/` | The plugin host: catalog, loader, registry and the bundler for external plugins | | `src/layout/` | The pane layout engine: the dock tree, floating windows, where a launched pane opens and which pane a ticker opens in | | `src/components/`, `src/ui/` | The shared UI kit every pane is built from | | `src/sources/` | Data provider adapters (Gloom Cloud, SEC EDGAR) and the router that picks a provider for each request | | `src/market-data/` | What panes read market data through: the request coordinator, its React hooks, quote display and exchange session calendars | | `src/time-series/` | Chart specs and the series they draw: the field catalog, resolving a spec into data, transforms and studies | | `src/data/` | Persistence: SQLite, the resource store, the config store and the plugin cache | | `src/renderers/opentui/` | The terminal renderer | | `src/renderers/dom/` | The DOM renderer the desktop view and the web app share: host primitives, controls, data table, dialogs, toasts and the page build | | `src/renderers/electrobun/` | The desktop app: `bun/` is the native process, `view/` the desktop-only parts of the web view, `shared/` the protocol and RPC codec both use | | `src/renderers/browser/`, `src/renderers/cloudflare/` | The web app and the Cloudflare Worker that serves it | | `src/renderers/share/` | Public share pages | | `src/cli/` | `gloom `, including the `fn` and `shot` pane reports | | `src/public/`, `src/types/` | The public plugin API, published as `gloomberb/*` (the `exports` in package.json) | | `src/i18n/` | Locale dictionaries | | `src/test-support/` | Shared test fixtures and fakes | | `scripts/` | Build, release and CI check scripts | | `docs/` | The user guide, data methodology and [pane conventions](docs/pane-conventions.md) | ## Checks CI runs these on every pull request. Run them before you push: ```bash bun run typecheck # terminal, desktop Bun process, desktop view, browser, Worker, scripts, built-in plugin tests bun run knip # unused files, exports and dependencies bun test ``` Some changes need one more check: | If you change | Run | |---|---| | A built-in plugin's id, name, description, toggle, panes or capabilities | `bun run plugins:manifest:check`; `bun run plugins:manifest` regenerates `plugin-manifest.json` | | A plugin compiled into the web app, or the `hosts` it declares | `bun run web:proxy-hosts:check`; `bun run web:proxy-hosts` regenerates the allowlist | | A runtime export of the plugin API (package.json `exports`) | `bun test src/public/public-api.test.ts` and `bun run plugins:registry:check`; see [Plugin compatibility](PLUGINS.md#plugin-compatibility) before removing or renaming one | | Desktop view code | `bun run desktop:view:build` | | Web app, share page or Worker code | `bun run web:audit` and `bun run cloudflare:dry-run` | | Build scripts or the terminal entry point | `bun run build` | | Tables, rendering, or market data stores | `bun run benchmark:tui:compare --base
` and `bun run benchmark:tui:memory` (need tmux); the Performance workflow runs both on every PR | | What runs before the first frame: the terminal entry point, app startup, the desktop Bun process, or the page and launch frame of the desktop and web apps | `bun run benchmark:startup --base
` (add `--cold` for a first launch; the desktop needs Linux with Xvfb, the terminal tmux) | | Pane chrome, docking, the DOM renderer or the desktop view | `bun run check:pane-chrome` and `bun run benchmark:pane-drag:compare --base
`; see [Pane chrome checks](#pane-chrome-checks) | | Anything a pane or plugin draws on the desktop or web (components, the DOM renderer, plugin code, styles.css, the theme) | `bun run check:ui-tokens` and `bun run ui:benchmark --base origin/main`, and attach the blast-radius sheet to the PR; the UI tokens and UI benchmark workflows run both. See [Holding the line](docs/pane-conventions.md#holding-the-line) | | Cloud sync, the portfolio analytics it values, or the desktop view's RPC path (Electrobun's patch included) | `bun scripts/pane-chrome/sync-poll.ts --check` (needs Playwright WebKit): the longest main-thread block of a sync on a large invented workspace and of a 4 MB message through Electrobun's preload codec, against a budget; the Performance workflow runs it for those paths | Try UI changes in the app as well. [`.agents/skills/tui-testing/SKILL.md`](.agents/skills/tui-testing/SKILL.md) shows how to drive the terminal app from tmux; give it a throwaway `GLOOMBERB_HOME`. ### Pane chrome checks Unit tests press a pane's header with synthetic events, which is how v0.16.1 shipped panes that only moved from the grip and the title. `scripts/pane-chrome` drives the real desktop shell in headless Chrome with real pointer input instead, over a fixed layout of stand-in panes (`fixture.tsx`) with no network, and checks where each pane ends up: drags by the grip, the title and the bare header bar for docked, tabbed, failed, floating and fullscreen panes, tab select and reorder, the resize corner and edges, a dock divider and a table's sidebar divider (that the frames follow the pointer while it is still down, as well as where they land, and that a chart or heat map follows its frame before its content catches up while the table under a board's chart band keeps its size, and that the market heat map's board of 500 names holds every tile still through a corner drag with a refresh in it, a window resize and a maximise and restore, and is laid out once when the size settles, not on every move), a popped-out window's title bar and caption buttons (the native window bridge is a recorder), that a chart filling its box takes the pointer as before (the crosshair, a pan, a wheel zoom, the trend line and the ruler, for charts under a query bar, beside a table and under a toolbar), that a chart's time axis stays clear of a floating pane's resize grip, that a chart's fill at rest moves nothing outside it, that a chart composer on invented bars pans back without jumping or changing span, loading older bars as it goes and stopping at the oldest, and reads out every series under its crosshair in the legend and on the axes with no box floating over the plot, that a daily and a 30-minute chart composer in one tab follow each other's crosshair to the same session and clear with it, and so do IV history, a monthly macro series and Fear & Greed's charts beside a chart composer, that interface text cannot be selected (Ctrl+A, Select All, a double or triple click and a drag across a chart pane and docked panes select nothing) while reader and markdown text, a chat message and a field still can, that a layout at rest renders nothing, and that keys held down (real auto-repeat key events) copy one screenshot and close one pane while the arrow keys keep repeating, and the chat message list over an invented channel (`chat-fixture.tsx`): which row is marked after a notification opens on it, a hover, a scroll, a key or a new message, and where the list is after a message arrives, an older page loads above, a message above the view changes, the composer takes focus or grows, and the pane is narrowed. Steps whose scenario says `anchoring=off` run with the browser's own scroll anchoring switched off, as it is in browsers that have none. The Pane chrome workflow runs these pointer checks on pull requests that touch the paths listed under `pull_request` in `.github/workflows/pane-chrome.yml`, and on pushes to `main` that touch them; a failed step fails the pull request. The Performance workflow's Pane drag frame budget job compares pane drags and resizes against the base branch for the same paths and for `performance.yml` itself: its Pane drag scope job reads the list (`scripts/pane-chrome/scope.ts`) and skips the budget in seconds for any other change. - The layout steps (`layout-checks.ts`, panes in `layout-fixture.tsx`) open the detail reader through the real Trials and Changelog panes, the article reader, markdown, prose, a table, a page with a table as tall as its rows and label and value rows, a chart sized in rows under a query bar, and a series list beside its detail under a query bar, at 1280 by 720, 1440 by 900 and 720 by 900, docked and floating, and fail when content stops more than a pixel above the footer, text wraps or is cut short of the width it sits in, or a box or chart cuts off a few pixels of what it draws. `bun run check:pane-layout` measures every function in the catalog the same way on the real web build (`bun run web:build` first, or `--web `), signed out, with its detail and, docked, every view its tabs and query bar choices lead to (two presses deep by default, `--depth` for more), each confirmed selected before it is measured (`tab-visits.ts`), and writes a summary, a coverage list of every tab (measured, not measured with the reason, or skipped as disabled, hidden or too deep) and a picture of each view; a tab it could not open fails the sweep (the `layout sweep:` step checks the tab visits themselves on `tab-sweep-fixture.tsx`: 25 tabs in an overflowing strip, a repeated label, a press that does nothing); text that arrived already cut in an answer it served (a feed's own shortened headline) is reported apart, not as a failure. - The top bar steps (`header-checks.ts`, the bar alone in `header-fixture.tsx`) put the app's top bar in every update state (checking, available, downloading, installing, ready with its Restart button, failed with its retry key, a passing notice), with the command bar closed and open, at 520 to 1440 px wide, as the macOS title bar (also in full screen and at the largest text size), the Windows one with its caption buttons, the Linux one and the web app draw it, and fail when two of its items paint over each other or anything is cut by its slot or the window edge rather than ending in an ellipsis of its own. At the narrowest width (800 px at the largest text size) a real click on Restart restarts and one on Help opens Help. - `bun run audit:ui` measures the whole app's look on its own copy of the web build: every function, the panes of the bundled plugins (and of the registry's with `--registry`), each one's detail and tabs, and the shell (header, status bar, command bar, dialogs, a pane menu, the first-run guide), at 640 and 1280 px. For every view it records each run of text (size, line height, weight, colour, its inset from the pane edge and the component that drew it), every element of a design-system family (buttons, tabs, chips, fields, table and list rows, headings, cards, empty, loading, notice and locked states, pane chrome, dialogs, menus, chart chrome) by shape, and row heights, and pictures each variant and every view in the white and paper themes. `scripts/pane-chrome/ui-audit-report.ts --sheets` turns a run into a report of the variants per family with a picture grid each, the type and insets in use, colours against the theme and a scan of the code for colours, sizes, spacing and radii written as literals. `bun run audit:ui:compare --before --after [--sheets]` sets two runs side by side: variants per family, font sizes, weights, radii, shadows, rule colours and pane insets before and after, and with `--sheets` a pixel diff of every view's picture and a sheet per family of the views that moved, before over after, beside views that did not. - `bun run kit:page` serves the kit page (`kit-fixture.tsx`): every family of the design system (docs/pane-conventions.md, "Design system") in every state, drawn by the shared components; `--shots ` pictures each family in the white and paper themes at 640 and 1280 px, with hover and focus forced. Compare a UI change against it. The `readers:` step measures its readers in the browser: the article reader, markdown and Docs must set paragraphs, section headings and prose bullets alike, wide and in a narrow column. `src/architecture/ui-literals.test.ts` holds type sizes, weights, radii, drop shadows, greys and pixel spacing written out instead of taken from the tokens to `src/architecture/ui-literals-allowlist.json`, where each file says why it keeps them. - `bun run check:ui-tokens` holds every view (the audit's, offline on a kept data snapshot) to the type scale, the two weights, the radii and the pane inset, with exceptions in `src/architecture/ui-tokens-allowlist.json`; the UI tokens workflow runs it on every pull request in eight shards and keeps the week's snapshot from main. `bun run ui:benchmark` pictures every view on a base and on this checkout and writes "N of M views changed" with the sheets of the views that moved; the UI benchmark workflow runs it on UI pull requests and keeps one comment up to date. See [Holding the line](docs/pane-conventions.md#holding-the-line). - `bun run check:call-player` plays the Calls pane's invented call (`scenario=layout&pane=calls`) in headless Chromium and WebKit from a recording it makes with ffmpeg and serves in byte ranges, as storage does: a clicked paragraph plays from its start and is marked, the arrows seek and the mark follows, Space pauses, the speed and the track work, Cmd/Ctrl+F reaches the find and Enter steps its matches, and a call without a recording draws no player. It pictures the pane in the white and paper themes at 640 and 1280 px. It needs ffmpeg and playwright-core's Chromium and WebKit. - `bun run check:chart-axis-zoom` zooms the chart composer's fixture (`scenario=cmp&chart=readout`: price, volume and RSI panels) with the real pointer in headless Chromium and WebKit: a drag on the price axis and a scroll on the RSI axis scale those panels alone and show Auto, a drag on the plot moves hand-scaled values, a double-click on the axis and Auto fit them again, a drag on the time axis zooms time from the newest bar and a double-click returns it, a pinch (Chromium's own, WebKit's gesture events) zooms time around the pointer, and a drag on the plot still pans time. It pictures the chart before, scaled and reset. It needs playwright-core's Chromium and WebKit. - `bun run check:chart-panel-layout [--base
]` lays out the same fixture with the real pointer in headless Chromium and WebKit at 640 and 1280 px: at rest it draws as the base does and shows none of the new controls, the tool bar's fold button and the legend's chevron show under the pointer and fold them to a button and a count that open them again (as Shift+T and Shift+L do), a legend entry still hides its series, a tool picked from the keys opens a hidden bar whose chevron folds it again for good, the line between two panels shows under the pointer, keeps the split under a click that jitters, moves with a drag and returns on a double-click, Shift+Up and Shift+Down move the top panel a row, a double-click on a panel gives it the whole chart and its `All panels` button (clicked or double-clicked) or another double-click gives the others back, and a drag on the plot still pans. It pictures each state and writes a contact sheet per browser. It needs playwright-core's Chromium and WebKit and ImageMagick. - `bun run check:pane-chrome` runs every step once; `--only ` picks steps by name, `--repeat ` runs them again, `--root ` checks another checkout's sources. A failed step saves a screenshot under `~/.cache/gloomberb-pane-chrome/failures`. - `bun run check:pane-rest --base
` draws every fixture layout from both checkouts at 1440 by 900 and compares them pixel by pixel: how many pixels differ, in which pane and where, with the base, head and diff images saved under the cache folder. A difference is not a failure by itself; a change that draws a pane differently at rest says why in its PR. - `bun run benchmark:pane-drag:compare --base
` compares main-thread time per frame and React commits while dragging a docked and a floating pane, resizing a floating news table, chart, heat map (an invented one, and the market board of 500 names recoloured by ticks) and Fear & Greed pane by the corner, the same for the charts that sit inside a pane's own layout (under a query bar, over a table, beside a table, under a toolbar), dragging a dock divider (measured on the news table's side and the chart's, and between the docked G layout and a chart beside a table), a table's sidebar divider, and panning a chart composer's plot. It reports frames per second, the 95th percentile frame time and estimated missed 60 Hz frames (requestAnimationFrame timing, not frames a GPU presented), and for a resize how far the frame and its content fell behind the pointer and moved on release, and for a chart how far CSS scaled its bitmap during the drag and at rest; a frame or its content more than three cells behind, moving more than a cell on release, or a chart not drawn 1:1 at rest fails it. `--only ` picks scenarios by name. Both sides draw the fixture of this checkout; when it imports source the base does not have, the base is built from its own fixture and the job summary says so (a base with no fixture, or one that does not build, fails the job). - The first run downloads the pinned Chrome for Testing headless shell (`browser.ts`); `CHROME_PATH` uses another Chrome instead. - To add a case, add a step to `checks.ts` that starts from a fixture scenario, moves the pointer with `drag` or `click`, and checks the result in the page (where a pane is, what is selected), not which handler ran. A new kind of pane or layout goes in `fixture.tsx`. - Theme/paint checks use `--only "theme preview"` or `--only "first paint"`. The real picker previews every built-in palette over chart, overview, heat map, table, sparkline, glyph and 3D surfaces, including floating and detached chrome. Each animation frame checks pane and plot backgrounds; settled pixels and color tokens must agree before/after commit and cancellation. Cold opens and rapid chart/overview switches must never expose an empty plot or change its size after first paint; the tabs stay mounted and hidden once opened, as the research pane's do, and a tab that comes back must show a bitmap drawn for the box it shows in, on every frame; the research Chart tab (the chart composer on invented bars) and a newly added chart pane, cold or drawn from cached bars, must paint the chart they settle on from their first plotted frame: same plot size and place, same axis and legend labels. Each frame is sampled after it painted (a task posted from its animation callback), so what resize observers change in that frame counts. Samples go to `paint/` under the cache folder. Theme changes finish color-only CSS transitions before paint; hover and layout motion keep their usual timing otherwise. - Not covered: GPU compositing (the runner has none) and the native macOS WebKit view; the desktop window move is checked up to the bridge message. The macOS view reads only the `-webkit-` spelling of `user-select`, which Chrome cannot tell apart from the plain one, so `src/renderers/dom/selection-style.test.ts` holds the stylesheet and the host to writing both. ## Tests Tests run on `bun test` and sit next to the code they cover as `*.test.ts` or `*.test.tsx`. ### What to test - Be selective: add or keep a test only when it protects behavior that is easy to break and hard to catch in review. - Good test targets: parser/math/state complexity, async/cache/persistence behavior, integration boundaries, and regressions with a concrete failure mode that could plausibly return. - Weak test targets: static metadata, default props, simple pass-through wiring, copied UI text, or behavior that is obvious from reading the implementation. - Bug-fix tests are not automatically worth keeping. Keep them only when the bug came from non-obvious behavior or a boundary likely to regress. - Do not keep low-value tests just because they already exist or improve coverage counts. - When touching a test file, trim nearby low-value tests if the cleanup is clear and low-risk. ### Test helpers - `src/test-support/` is the home for shared fixtures and fakes: a data provider with quote and financials factories, ticker records, news articles, resolved chart series and points, a stand-in broker, a doubled plugin runtime, plugin persistence, market sessions, a static app store (`createStaticAppStore`), pane providers and the `TestPaneFrame` footer shell, headless and CLI contexts, a Cloud API WebSocket fake and temporary databases. Put a new shared fixture there instead of declaring it inline in another test. A fixture only one feature's tests share can sit next to them in a `test-fixture.ts` (or `test-harness.tsx` for render helpers), which the published package leaves out. - For a pending result, use `Promise.withResolvers()` rather than a local `deferred()` helper. - `src/renderers/opentui/test-utils.tsx` renders into a test terminal. `createOpenTuiTestHarness` mounts one renderer per test and destroys it after every test, with `frame`, `emitKeypress`, `renderFrames`, `waitForFrameToContain` and `clickFrameText` on the current renderer; `testRender`, `emitKeypress` and `settleFrame` remain for lower-level use. - `src/renderers/dom/test-utils.tsx` renders the desktop and web view into happy-dom: `createDomTestHarness`. - A few features keep a harness beside their code, such as the command bar (`src/components/command-bar/surface/test-harness.tsx`) and chat. - Plugins in their own repositories get the same fakes and render harness from `gloomberb/test-support`. ## Code rules ### Host imports Built-in code under `src/` imports the host by relative path (`../../../ui`, `../../../components`, `../../../public/react`, `../../../theme/colors`). The `gloomberb/*` specifiers are the external-plugin API: Bun on Linux resolves them as a package self-reference, so typecheck and tests pass, but the Windows desktop bundle cannot resolve them and the Windows verify workflow on `main` fails. `src/architecture/import-boundaries.test.ts` fails on a `gloom` or `gloomberb/*` import in built-in code. Core code outside `src/plugins/builtin/` should not import a specific built-in plugin. `src/architecture/core-builtin-boundary.test.ts` checks runtime sources, including type imports and dynamic imports, against `src/architecture/core-builtin-allowlist.json` and fails on new or stale file/plugin pairs. Move shared code to `src//` or `src/plugins/builtin/shared`; run `bun scripts/update-core-builtin-allowlist.ts` after removing an edge so the allowlist shrinks. If a new edge is necessary, regenerate the allowlist and explain why in the PR. Core catch handlers must record real failures through `src/utils/log-failure.ts` or the caller's existing error surface; intentional ignores carry an `Intentional ignore: ` comment of at least four words inside the handler, and logging handlers must not carry that marker. `src/architecture/silent-catches.test.ts` checks empty and constant-fallback catch/`.then` rejection handlers, including straight-line constant assignments, property deletion and collection `.delete`/`.clear` cleanup; it excludes built-in plugins (apart from the folders listed in `INCLUDED` in `scripts/update-silent-catch-allowlist.ts`, which the check covers like core), sync, renderers, components, layout-marketplace and `src/app.tsx`, and rejects invalid markers, new silent catches and stale allowlist entries. Run `bun scripts/update-silent-catch-allowlist.ts` after removing an allowed site so the generated allowlist shrinks. ### One component for every renderer A pane is one component that renders in the terminal, the desktop app and the web. Import UI from the kit and the plugin runtime, never from OpenTUI, Electrobun or the DOM directly; `src/architecture/import-boundaries.test.ts` fails when a renderer package is imported outside `src/renderers/`. On the desktop and the web, draw lines, markers, shapes and overlays with real DOM, CSS, canvas or SVG; drawing with terminal cell characters is for the terminal renderer only. Never fix a chart by turning off the kitty graphics renderer: keep kitty support and fix the cause. ### Shared UI kit Basic UI must use the shared kit: actions, selectable and expandable rows, fields, tabs, lists and tables, headings and sections, labeled values, badges, dividers, and loading, error and empty states. When a repeated pattern is missing, extend the kit and migrate its callers; do not write another pane-local version. `Box` and `ScrollBox` remain layout primitives, and raw `Text` and custom surfaces are fine for domain content, charts and specialized editors. External plugins use the same components through `gloomberb/components`. ## Panes and plugins [PLUGINS.md](PLUGINS.md) is the guide to building a plugin and its APIs. [docs/pane-conventions.md](docs/pane-conventions.md) covers how a pane is put together: where actions, status and warnings go, detail stacks, load-more lists, tabs, forms, reserved keys, and a checklist for a new pane. The rules that come up most in review: - The pane footer shows what changes (loading, error, live or delayed, stale, auth state) and the pane's action keys. No fixed labels, row counts or generic keyboard hints there, and no button rows in the body. - Never show internal provider or source identifiers in a pane (`provider:*`, raw feed names). Say what the data is instead: real-time, 15m delayed, settlement, as of a date. Gloom Cloud is the public market-data service. - Say each thing once. When a pane title or detail header names the item, the body starts with metadata or content. - Methodology, model assumptions and usage explanations go in `docs/`, not in always-visible pane text or new info buttons. Units, source dates and active data failures stay in context. - Everything interactive works with the mouse and the keyboard. ### Built-in plugins Only independently owned, registered product areas implement `GloomPlugin`. Larger built-ins may compose internal `PluginModule` objects for panes, commands, capabilities and lifecycle code, but those modules have no identity, toggle or version of their own, and smaller built-ins declare their contributions directly. A module persists under its plugin's state namespace, unless it moved from another plugin: its entry then keeps the namespace it had (`{ module, stateId }`), so its users keep their pane state, settings and caches. `PluginModule` is an internal organization tool, not a second plugin API: external plugins export one `GloomPlugin`. ## Pull requests - Keep each pull request to one change, and send unrelated cleanups separately. - Title it with a plain sentence saying what changes, such as "Pin the FX staleness test to a weekday". - In the description, say why, list the changes, and say what you verified: the checks you ran and, for anything visible, a screenshot from the terminal or the desktop app. - Write the title, description, commit messages and branch name for an outside reader. Leave out internal tracker ids and links, private repositories and internal notes; when a change needs a server change too, say "needs the matching server change". The PR text check fails a title or description that does not, and the Tracked files check scans every file in the repository; `./scripts/check-private-references.sh` runs it locally. - Keep user-visible behavior the same unless changing it is the point, and update `docs/` when it changes. - The README is a short front door: what it is, how to install it, how to start. Do not add function lists, feature guides, privacy detail or release notes to it. A new function goes in the website's function catalog, and explanations go in `docs/`. - Anything reachable from the `exports` in package.json is public API that external plugins depend on. Deprecate before removing or renaming it: mark it `@deprecated`, point at the replacement, and keep it working. ## Browser development ```bash bun run web:build bunx wrangler dev ``` Validate the public artifacts with `bun run web:audit` and `bun run cloudflare:dry-run`. The top level of `wrangler.jsonc` is the local configuration. After verification passes on `main`, GitHub Actions deploys `term.gloom.sh` with its `production` environment (`wrangler deploy --env production`). The private Gloom Cloud API is deployed separately. Cloud REST and WebSocket traffic uses the same-origin `/api` path, which the Worker forwards only to `https://api.gloom.sh`; it is not an arbitrary network proxy. Public shares open under `/s/:id` in a separate slim bundle. Share creation and owner deletion use the signed-in Gloom Cloud session through the same API path; public reads require no account. See the [browser guide](docs/browser.md) for account requirements and supported features. ## Localization - Locale dictionaries live in [src/i18n](src/i18n), keyed by the original English UI text. Missing entries safely fall back to English. - Shared render sinks call `t()` / `tf()` / `tc()` for pane titles, the command bar, menus, settings, tabs, help, and onboarding. - Finance abbreviations such as BID, ASK, and CHG% intentionally remain in English for terminal conventions and fixed-width alignment. - CJK wide characters and grapheme clusters are measured by [src/utils/format.ts](src/utils/format.ts) using terminal display-cell widths.