Skip to content

Repository files navigation

codex-ui

codex-ui

Codex appearance for DSH Web: window edges, sidebar divider, model menu, composer, theme colors

简体中文 · Changelog · MIT

License: MIT DSH Web Plugin Node.js 22 or later CI

codex-ui is a community-maintained interface plugin for DeepSeek Harness (DSH), not an official DeepSeek AI product.

This plugin rebuilds DSH Web interface elements to match Codex: edge shadows and hairlines, the sidebar divider, the model and reasoning-effort menu, composer structure, and the light and dark palettes. Values come from Codex screenshots and its color picker, measured per pixel. The measurements and the source images are in the sections below and in assets/reference/.

Host compatibility

Developed against DSH 0.1.7-rc.1 (npm global install) and 0.1.7-rc.2 (Windows desktop shell app.asar). The verification scripts read shipped CSS straight out of app.asar; a host upgrade that changes structure fails their assertions.

Features

Id Content
⑫ Model picker: native trigger, opaque white menu, 28px rows, permanent check column, pending spinner
⑬ Composer header turned blank, panel buttons keep the official icons
⑭ Composer card: radius, shadow, geometry, tool row, hero layout
⑯ Right panel guide entries: no border, no fill, 52px rows, 20px icons, filled shortcut pills
⑰ Composer bottom controls: add button has no box until hover; model and permission controls share the hover chip
② Sidebar colors match the Codex light sidebar; sidebar rows align with the workspace list
②c Conversation window edge: 0.5px hairline plus a 24px ambient shadow
②d Right panel: hairline only on its left edge, shadow bleeds upward only; the dockkit 1px border is removed
②e Right divider handle: center-darkest gradient on hover
⑱ Settings card on the plugin manager's codex-ui bundle page: theme / accent / background / foreground / UI font / code font / translucent sidebar / contrast

Screenshots

Light theme: conversation window edge and right panel.

Light theme

Composer bottom row: no box by default, a chip on hover.

Composer controls

Model menu while a reasoning effort is being written.

Model menu pending

Install

npm run install:web        # node scripts/install-plugin.mjs --write
npm run install:desktop    # desktop shell; restart the app afterwards
npm run build              # regenerate theme.css and client.js from skins/codex-ink

node scripts/install-plugin.mjs                              # dry run (web profile by default)
node scripts/install-plugin.mjs --bundle --write              # register through dsh.profile.bundles and drop the redundant insert
node scripts/build.mjs --check                               # report stale artifacts without writing

The installer scopes the six stylesheets in skins/codex-ink/ to html[data-codex-ui], combines them with src/client.template.js (which inlines src/override.js and src/settings-card.js) into client.js, copies the result to profiles/<name>/vendor/codex-ui, creates the node_modules/codex-ui junction, and keeps exactly one registration path.

Never use both registration paths at once — two rows share the id and the market check disables the plugin:

Path How Used by
bundle the package name goes into the profile package.json dsh.profile.bundles; the package's own cordis.patch.yml inserts the entry the desktop shell profile and the web profile (--bundle)
insert a hand-written - insert: in the profile cordis.patch.yml throwaway verification profiles that skip pnpm install

The same stylesheets can be picked up by a skin loader:

node scripts/install-skin.mjs          # per-file SHA256 check against $DSH_HOME/skins/codex-ink
node scripts/install-skin.mjs --write  # overwrite on drift

Layout

Path Content
index.js cordis.patch.yml package.json Host half and manifest
src/client.template.js Browser half template (stylesheet, override layer, settings seat)
src/override.js Override-layer pure functions (no DOM; unit-tested by the repo checks)
src/settings-card.js The config card on the bundle page (inlined into client.js at build time)
src/build.mjs Scoping and artifact generation; the only implementation
theme.css client.js Generated from skins/codex-ink/ by src/build.mjs
skins/codex-ink/ Stylesheet sources (skin.css / patches.css / sidebar-align.css / window-shadow.css / composer.css / settings.css)
docs/ Plans and decisions
scripts/build.mjs Regenerate the artifacts; --check compares without writing
scripts/check-repo.mjs Host-free repository checks; the CI entry point
scripts/host-paths.mjs Resolves app.asar, the global @deepseek-ai modules and Chromium
scripts/install-plugin.mjs scripts/install-skin.mjs Installers
scripts/*-verify.mjs scripts/live-gui-probe.mjs scripts/settings-page-verify.mjs Fixture verification and live probing
scripts/make-verify-profile.mjs Builds a throwaway verification profile: plugin manager enabled, only this plugin, no existing profile touched
assets/reference/ Codex reference images
assets/screenshots/ Verification output
.github/workflows/ci.yml CI

Verification

Command Coverage Requirement
npm run check Syntax, JSON, manifest, artifact sync, encoding, docs pairing, machine-specific paths none
node scripts/audit-codex-ink.mjs Skin structure, 36 WCAG pairs, color whitelist none
node scripts/model-picker-verify.mjs ⑫ and the pending indicator, 18 assertions none
node scripts/rightbar-verify.mjs Shadow layer, right panel, both dividers, 42 assertions none
node scripts/sidebar-align-verify.mjs Sidebar column alignment, 6 assertions none
node scripts/hero-verify.mjs ⑬ ⑭ ⑰ and the focus ring, 8 assertions none
node scripts/live-gui-probe.mjs --url <token URL> Real GUI: 10 assertions on shadows, both dividers, the model menu pending window a running dsh web
node scripts/settings-page-verify.mjs --url <token URL> Real GUI: the card on the bundle page, its 8 rows, no override at defaults, switch and accent writes, survival across a reload — 22 assertions a running dsh web with the plugin manager enabled
node scripts/theme-flash-probe.mjs --url <token URL> Per-frame sampling of the effective backdrop during theme and page switches (first opaque ancestor background); reports frames belonging to neither end of the transition (measured: 9 windows, ~720 frames, 0 anomalies) same as above

npm run check needs no host. The five fixture suites run locally: they need shipped CSS from app.asar plus a DOM rebuilt from the render code, read with getComputedStyle. Fixtures have no title bar, no real AppFrame grid and no real RPC, so the shadow layer, divider hover and pending feedback are verified by the live probe.

Host paths

The verification scripts read the host they run against. Each path is resolved in this order:

  1. DSH_ASAR, DSH_GLOBAL_MODULES, DSH_CHROME;
  2. scripts/host.local.json, a gitignored per-machine file, for example { "asar": "D:/.../resources/app.asar" };
  3. a scan of the standard install locations, Playwright's browser cache and npm root -g.

No machine-specific path is committed.

dsh --profile web --port 3099 --no-open      # prints a token URL
node scripts/live-gui-probe.mjs --url "http://127.0.0.1:3099/?token=..." --dpr 1.5

The probe opens a new conversation before timing the pending window, and exits non-zero if an assertion fails. The token expires; after about half an hour requests return 401 and a restart is needed.

Settings page

The config card on the plugin manager's bundle page (slot plugins.bundle.config, keyed by the package name codex-ui). In the desktop app: sidebar Plugins → Installed → codex-ui → the card on that page. Changes apply immediately; no restart needed.

Settings page

The same card in dark (after switching the theme the three colour rows edit the dark variant, and contrast shows the dark default, 60):

Settings page · dark

Row Config field Default Lands on
Theme — (writes the host ui-theme preference) System ctx.theme.setTheme(): switches the whole app, the same setting as Settings → General → Appearance
Accent accentLight / accentDark empty = follow skin --dsw-alias-link, --dsw-codex-focus
Background surfaceLight / surfaceDark empty --dsw-alias-bg-base
Foreground inkLight / inkDark empty --dsw-alias-label-primary
UI font fontUi empty --dsw-font-family
Code font fontCode empty --ds-font-family-code
Translucent sidebar translucentSidebar off sidebar fill and row fills become translucent
Contrast contrastLight / contrastDark 45 / 60 text tiers and the neutral alpha ladder
  • The theme row is not card-local view state: it writes the host's ui-theme preference, the same setting as Settings → General → Appearance — the whole app switches and it survives a reload. The three colour rows below edit the variant currently in effect (active.colorScheme).
  • It does not go through theme.setTheme(): that path publishes optimistically first and is then re-read from the settings document by adopt(), so on a slow round trip it draws new → old → new, which reads as "black → white → black". The card writes the preference into the theme plugin's own settings document instead (the same namespace ui-theme and field preference the service's internal host.set uses), leaving adopt() as the only publisher — one click, one publish. The control keeps a local pending value so it still feels immediate, and an unaccepted write falls back to the service entry point. That 0.8s round trip is not left empty: the browser half carries a local preview — the target theme is applied the moment you click (writing exactly the two things the host writes itself: body[data-ds-dark-theme] and the color-scheme on html), then handed back idempotently when theme/change arrives with the same result. If the confirmation does not arrive within 2.5s, the preview rolls back to the truth. Measured click → colour change: 824ms → 22ms (median), with the run sequence still two runs (light×n → dark×m) and no double publish.
  • All 11 fields are .volatile(): the settings service only projects volatile fields, and that is exactly how the plugin manager knows the entry — no Config, no card.
  • Empty means no override: at the defaults the override layer emits an empty string and data-codex-ui-theme never appears, so an untouched install looks byte-for-byte like 0.1.x (a repo check asserts this).
  • Overrides live in one runtime <style> whose selector carries one extra attribute (specificity +1), so sheet order does not matter and skins/*.css is never touched.
  • Instant write, no save button; text inputs commit on Enter or blur and every write is read back to confirm it landed; overridden rows show a badge and a Reset control.
  • The contrast slider is a documented simplification: the app lerps text towards ink in linear RGB and raises the ramp by a constant; here the text tiers are mixed in the same direction and the hairline/neutral-tone family is scaled (clamped to 0.5×–2×). Colored state and diff fills are excluded so the palette never leaks into the override layer.
  • The translucent sidebar has no window layer to reveal on the web, and in dark the sidebar shares the surface colour, so it is invisible there — the switch therefore also turns the sidebar row fills translucent, otherwise it would be
  • The skin paints the canvas itself: html and body both carry this skin's base colour in either theme (html follows through :has(body[data-ds-dark-theme]), because the host stamps the theme marker on body only). Transitions are suppressed for the two frames after a theme change (html[data-codex-ui-switching] *, set by the browser half on ctx.on('theme/change')). Both prevent a flash: the first closes the "nobody painted the canvas this frame" hole that would reveal the host's default backdrop, the second stops the whole page from cross-fading. completely silent in dark. Recorded as a gap, not presented as an equivalent.

Measurements

Window edges

assets/reference/codex-app-reference-1x.png (1901x1107, DPR 1):

Location Scan Value
Conversation window, left edge y=600 x=340..355 ramps 238,241,247 to 231,233,239; x=356 is a single pixel 212,215,221
Conversation window, top edge x=800 y=30..45 ramps 237,242,247 to 232,237,242; y=46 is a single pixel 214,218,224
Right panel, left edge y=600 x=1437 is a single pixel 237,237,237, both sides pure white
Right panel, top edge x=1700 y=46 is a single pixel 213,218,224 with the same ramp above

In both reference images the hairline is one device pixel, so the stylesheet uses 0.5 CSS px: at the 150% scaling of this machine, 1px rasterizes into two device pixels.

Target box-shadow
Conversation window 0 0 0 0.5px var(--dsw-alias-border-l2), 0 0 24px rgba(13,13,13,.05)
Right panel 0 0 0 0.5px var(--dsw-alias-border-l1), 0 -12px 24px -12px rgba(13,13,13,.05)

Theme colors

Source: assets/reference/codex-theme-light.png and codex-theme-dark.png (the Codex color picker).

Role Light Dark Token
Accent #339CFF #0169CC --dsw-alias-link
Background #FFFFFF #181818 --dsw-alias-bg-base
Foreground #1A1C1F #FFFFFF --dsw-alias-label-primary
Hover fill #F2F2F3 rgba(255,255,255,.08) --dsw-codex-hover-fill

The light values come from the color picker screenshot codex-theme-light.png. Since 0.2.0 the dark values come from the app's own defaults (the jdi object inside resources/app.asar: surface #181818, ink #ffffff, accent #339cff) instead of the picker. The dark link stays on the app's text-link token #0169CC.

The dark ramp rises from #181818: sidebar #181818 (the same face as the surface, separated by the 0.5px hairline), layer 1 #212121, layer 2 #282828, layer 3 #303030; the alpha family moved from rgba(252,252,252,·) to rgba(255,255,255,·) (the app's dark --alpha-base is #fff).

Sidebar colors

Source: point samples from assets/reference/codex-sidebar-reference.png.

Token Value
--dsw-alias-bg-sidebar #eef4f9
--dsw-specific-sidebar-fill #eef4f9
--dsw-specific-sidebar-nav-item-active #e2e9ed
--dsw-specific-sidebar-nav-item-hover #e8eef3

Limits

  • Fixture checks are not signed-in screenshots. The dsh web launch token has a lifetime and lives in process memory only.
  • In headless mode only the foreground tab handles :hover, so multi-page fixtures open the web-shape page last.
  • A flat single list of model plus effort, and a per-model description column, need a client plugin that takes over the conversation.input.model slot and reuses ctx.modelDirectories. Not implemented here.
  • ⑯ keeps the host tab strip: hiding it also removes the fullscreen and collapse buttons.
  • The session row text column is 40px, 2px shorter than the workspace, new session and plugin rows, because the shipped Rows.module.css gives .sessionRow .title its own margin. Left as is.
  • composer.css and patches.css use 21 hash-class suffix anchors ([class$=…], [class*=…]) where the host exposes no data-*.

Codex source alignment

The Codex desktop app carries its webview CSS inside resources/app.asar (webview/assets/app-*.css); the public openai/codex repository holds the CLI and the TUI, not this interface. Values below come from app 26.727.4816.0.

Value Codex This skin
Motion --transition-duration-basic: .15s, --transition-duration-relaxed: .3s --dsw-motion-fast: 150ms, --dsw-motion-slow: 300ms
Easing --ease-in-out and --default-transition-timing-function, both cubic-bezier(.4, 0, .2, 1) --dsw-ease
Focus ring --color-border-focus = --blue-300 #339cff; dark the same at 70% --dsw-codex-focus
Pending spinner --animate-spin: spin 1s linear infinite codex-ui-spin 1s linear infinite
Hairline --shadow-hairline: 0 0 0 .5px #0000001a the window and panel hairlines use the same 0.5px ring
Light foreground --color-text-foreground: #1a1c1f --dsw-alias-label-primary
Chip fill --background-button-secondary-hover, 8% of the foreground light #f2f2f3 (measured), dark rgba(255,255,255,.08)

Deliberate differences:

  • The dark base moved to the app default in 0.2.0: jdi.dark.surface = #181818, jdi.dark.ink = #ffffff, ramp #212121 / #282828 / #303030 (the app's gray-800 / gray-750 / gray-700). The 0.1.x picker values #111111 / #FCFCFC are no longer used. For the record, the app CSS resolves the dark surface to --gray-900 #181818; picker and CSS disagree, and the picker wins here.
  • The composer card radius is 25px as measured on assets/reference/codex-composer-reference.png. The app CSS gives --radius-3xl (20px) for the multi-line composer and 22px for the single-line one; the gap is the screenshot's device scale factor, which is not recorded.
  • The sidebar is 280px wide, set by the host layout. Codex clamps its own sidebar with clamp(240px, 275px, min(520px, calc(100vw - 320px))).
  • Dark link text keeps #0169cc, which the app uses for --color-token-text-link-foreground; dragging the accent control on the settings page is now the way to get #339CFF there. The app's own --color-text-accent is #99ceff (--blue-100) in dark.
  • The contrast slider is a documented simplification (see "Settings page"), not the app's Rdi + zdi·contrast blend.
  • For the record, the app's full gray ramp is #0d0d0d / #181818 / #212121 / #282828 / #303030 / #414141 / #4f4f4f / #5d5d5d / #afafaf / #ededed / #f3f3f3 / #f9f9f9 / #fff.

CI

.github/workflows/ci.yml runs scripts/check-repo.mjs and scripts/build.mjs --check on Ubuntu and Windows, Node 22 and 24. The fixture suites and the live probe need the desktop shell and Chromium, so they stay local.

License

MIT.

About

DSH Web 的 Codex 化界面插件 · A Codex-style interface plugin for DSH Web

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages