Skip to content

dsh-client-ui-enter-newline

Verified

dsh-client-ui-enter-newline ยท v1.0.0 ยท MIT ยท Web UI

DSH Web client plugin: an "Enter inserts a newline only" checkbox beside the composer's model selector, which suppresses Enter-to-send while checked.

Install

dsh plugin add dsh-client-ui-enter-newline

Confirm the layer applied with dsh --profile default --dump-config โ€” see the install guide.

Source

Published to npm without a public repository. Inspect the package contents before installing.

Tags

Creators

Readme

dsh-client-ui-enter-newline

A DSH Web client plugin. It adds one checkbox โ€” Enter ้”ฎไป…ๆข่กŒ โ€” to the left of the model / reasoning-effort selector in the composer toolbar:

Checkbox Plain Enter Shift+Enter Ctrl/Cmd+Enter
unchecked (default) sends, exactly as shipped line break shipped accelerated send
checked line break, submit is suppressed line break shipped accelerated send

The preference is stored in the browser under localStorage['dsh.enterNewlineOnly'] ("1" = checked). A missing or unreadable key means unchecked, so the plugin's default state is the untouched DSH behavior.

Layout

File Role
lib/index.js Host half: an empty Cordis entry, so the loader has something to mount.
lib/client.js Browser half: the checkbox component, the preference store, and the Enter guard.

How it hooks in

  • The checkbox is registered into the shipped conversation.input.left slot โ€” the composer toolbar row's leading group. That group renders as a sibling immediately before the trailing group that holds conversation.input.right and the model selector, so the control lands directly on the selector's left with the toolbar's own flex spacing. The plugin adds one inline cell; no shipped markup is replaced, reordered, or restyled. The slot is session-scoped and is never hidden by the toolbar's activity expansion, so the control lives exactly as long as the resident composer does.
  • The Enter guard is a keydown listener in the capture phase, registered on window and on document. The composer's Enter command lives inside Lexical, which listens on the editor root in the bubble phase, so a capturing listener runs strictly earlier. When the checkbox is on and the event is a plain Enter inside the composer's draft host, the listener stops propagation โ€” Lexical never sees the key, and because the event is not preventDefault()ed the browser still inserts the line break, which is exactly what Shift+Enter does. Every other key, and every Enter while the checkbox is off, is untouched.
  • The draft host is detected from the live node, not a fixed attribute value: any element whose isContentEditable is true (which also covers contenteditable="plaintext-only") or a textarea, as long as it sits inside a [data-composer-card]. A kernel release that changes how the draft is hosted therefore cannot silently disable the guard, and no editor outside the composer is ever touched.
  • The preference the guard acts on is read from the rendered control itself (checked on the plugin's own checkbox), not from an in-memory copy, with the page-global record as the fallback for moments when the control is unmounted. The control's checked property is the value the user is looking at, so nothing that can desynchronize memory โ€” a hot reload leaving an older bundle execution's listener installed, or a storage write that never landed โ€” can make the guard disagree with the checkbox on screen. Two historical bugs came from getting this wrong: a module-local flag that went stale across reloads, and a seat check that also excluded anything inside a composer card containing the seat (which is true of the composer's own editor, so the guard skipped exactly the events it exists for). The regression tests in test/guard.test.mjs cover both.

Shift/Ctrl/Meta/Alt/AltGr chords and IME composition are never intercepted.

Install

The package declares dsh.bundle.patch, so it installs as an ordinary DSH bundle and registers its own loader entry โ€” no hand-edited YAML, no build step on the user's machine:

dsh plugin --profile <profile> add dsh-client-ui-enter-newline     # from npm
dsh plugin --profile <profile> add ./dsh-client-ui-enter-newline-1.0.0.tgz   # from a tarball
dsh plugin --profile <profile> add github:<you>/<repo>             # from a git host

add appends the bundle to dsh.profile.bundles; dsh plugin --profile <profile> remove dsh-client-ui-enter-newline takes it back out. Check the composed tree before booting with dsh --profile <profile> --dump-config.

Installing by hand is equally fine: put the package where the profile's module resolution can see it and add the same row the bundle layer adds โ€”

# <DSH_HOME>/profiles/<profile>/cordis.patch.yml
- insert:
    - id: enter-newline
      name: 'dsh-client-ui-enter-newline'

A profile that watches its patch file picks the entry up live: the host side becomes an active loader entry and the browser half mounts without restarting DSH. A profile that does not hot-reload its patch layer needs a restart.

Distribute

Publish the built package โ€” the bundle is plain JavaScript, so there is nothing to compile, and lib/ ships as-is. Three routes, in order of how little the user has to do:

Route User command Build on install
npm dsh plugin add dsh-client-ui-enter-newline none
tarball (pnpm pack) dsh plugin add ./dsh-client-ui-enter-newline-1.0.0.tgz none
git host dsh plugin add github:you/repo prepare must build lib/, and the user must allow it

Prefer npm or a tarball: pnpm โ‰ฅ10 refuses to run a git dependency's prepare script until the user explicitly allows it in the profile's pnpm-workspace.yaml (allowBuilds:), and that permission is "let this package run code on my machine at install time". Distributing built artifacts keeps that decision off the user's plate.

Before publishing, the five fields that decide whether an install works rather than merely succeeds:

  1. main / exports point at the built entry (lib/index.js), never at sources;
  2. "type": "module";
  3. files includes the entry and cordis.patch.yml โ€” the patch is the bundle layer, so omitting it yields a package that installs but activates nothing;
  4. dsh.bundle.patch points at that patch file โ€” without it the package is only a plain dependency and dsh plugin warns that no layer was activated;
  5. version bumped per semver โ€” a published version can never be overwritten.

Then:

pnpm pack --dry-run          # confirm lib/ + cordis.patch.yml are inside the tarball
pnpm publish                 # scoped packages: publishConfig.access = public
dsh plugin --profile fresh add ./dsh-client-ui-enter-newline-1.0.0.tgz
dsh --profile fresh --dump-config     # expect a "# == dsh-client-ui-enter-newline" layer

Verify in a fresh profile: an install tested in your own profile can pass on dependencies that happen to be there already.

Verify

node test/guard.test.mjs lib/client.js

The check loads the real bundle against a minimal DOM stub and asserts the shipped behavior, the suppressed behavior, every un-intercepted chord, IME composition, out-of-composer targets, plaintext-only and detached draft hosts, and the checked styling on mount. It deliberately cannot prove anything about the live Lexical editor or the shipped markup โ€” open the composer and press Enter for that.

Two live probes confirm the wiring without touching the page:

  • cordis_inspect_query โ†’ host Config โ†’ listConfigs with {"name": "dsh-client-ui-enter-newline"} returns the loader entry.
  • cordis_inspect_query โ†’ client Slots โ†’ listSubTree with {"root": "conversation.input.left"} lists enter-newline-toggle as an active occupant.

Uninstall

Delete the loader entry above, then the installed package directory. The stored preference key is inert on its own and can be removed with localStorage.removeItem('dsh.enterNewlineOnly').

Limitations

  • The guard targets the composer's plain Enter. The primary Send button is a separate control and still sends while the checkbox is on โ€” by design: the preference is about the keyboard, and a button is an explicit click.
  • While the checkbox is on, React's synthetic keydown for that specific Enter is suppressed along with the composer command. This is intentional (no submit path may remain) and affects no other event.
  • The guard locates the composer card and the rendered control through the shipped [data-composer-card] marker plus this plugin's own data-enter-newline-seat; the draft host itself is recognised via isContentEditable (or textarea). A DSH release that renames data-composer-card would make the plugin stop finding the control, and the guard then falls back to its stored preference โ€” so the checkbox keeps working, while the "read exactly what the user sees" guarantee no longer holds. Re-check that marker after a kernel upgrade.