@furayoshi/dsh-widechat 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 FraYoshi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # @furayoshi/dsh-widechat
2
+
3
+ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that widens the conversation column past the shipped 748px cap, right-aligns the session stats strip, and caps the composer card so long inputs do not hide the conversation above. Adds a settings row to the General page so all of these can be tuned without restarting DSH.
4
+
5
+ ![widechat ui](media/widechat-ui.webp)
6
+
7
+ ![widechat settings](media/widechat-settings.webp)
8
+
9
+ ## What it does
10
+
11
+ The shipped DSH web UI caps the chat content at `--dsh-chat-content-width: 748px` and centers the column with `margin: 0 auto`. On any monitor wider than ~1024px this leaves large empty gutters on both sides and a narrow reading area. This plugin widens that cap to leave only the configured gutter, right-aligns the user-message bubble (the shipped default is narrow on wide viewports), and caps the composer card to a percentage of the viewport so long inputs do not eat the conversation above.
12
+
13
+ A small row in the **General** settings page lets you tune four values:
14
+
15
+ - **Chat column gutter** — a slider, 0–10% of the cell on each side. The shipped default is `1 %`. The column is sized to leave this much whitespace on each side.
16
+ - **Session-stats alignment** — a three-way toggle (left / center / right). The shipped default is `right`. The session-stats line under the composer (token counts, duration, etc.) lines up accordingly.
17
+ - **Your message bubble width** — a slider, 30–100% of the chat column. The shipped default is `75 %`. Your outgoing messages are right-aligned at this width; the assistant's messages fill the rest of the column.
18
+ - **Composer max height** — a slider, 20–80% of the viewport. The shipped default is `50 %`. The composer card and its inner textarea are both capped at this height, so long inputs do not push the conversation off-screen.
19
+
20
+ If a saved value is out of range or of the wrong type, the plugin falls back to the default and logs a `console.warn` with the offending field. The page never refuses to render.
21
+
22
+ ## Install
23
+
24
+ DSH's `plugin` subcommand forwards pnpm commands into the profile directory and reconciles the profile's `dsh.profile.bundles` array against the installed packages. So `dsh plugin --profile web add <spec>` is the canonical install path: it runs `pnpm add <spec>`, then automatically appends the package to `bundles` if (and only if) the installed `package.json` declares a `dsh.bundle.patch` — which this package does.
25
+
26
+ ### From npm (after this repo is published)
27
+
28
+ ```sh
29
+ dsh plugin --profile web add @furayoshi/dsh-widechat
30
+ ```
31
+
32
+ The `dsh plugin add` command runs `pnpm add` to install the package into `~/.dsh/profiles/web/node_modules/`, then reconciles the `dsh.profile.bundles` array in `~/.dsh/profiles/web/package.json` to include the new bundle. The user does not need to edit `package.json` manually.
33
+
34
+ ### From a GitHub URL
35
+
36
+ ```sh
37
+ dsh plugin --profile web add @furayoshi/dsh-widechat@github:FraYoshi/dsh-widechat
38
+ ```
39
+
40
+ The `github:<owner>/<repo>` spec is pnpm's shorthand for `https://github.com/<owner>/<repo>.git`. To pin a specific version:
41
+
42
+ ```sh
43
+ # A specific tag
44
+ dsh plugin --profile web add @furayoshi/dsh-widechat@github:FraYoshi/dsh-widechat#v0.7.0
45
+
46
+ # A branch (e.g. main)
47
+ dsh plugin --profile web add @furayoshi/dsh-widechat@github:FraYoshi/dsh-widechat#main
48
+
49
+ # A specific commit SHA (for reproducible installs)
50
+ dsh plugin --profile web add @furayoshi/dsh-widechat@github:FraYoshi/dsh-widechat#a1b2c3d4
51
+ ```
52
+
53
+ pnpm clones the repo, runs `npm pack` from the cloned `package.json` to build the tarball (so the `files: ["lib", "cordis.patch.yml", ...]` list matters), and installs it. As long as `cordis.patch.yml` is in `files`, DSH's bundle loader finds it on the next boot.
54
+
55
+ ### From a local path or clone
56
+
57
+ If you have the source locally (or want to hack on it), install it directly:
58
+
59
+ ```sh
60
+ dsh plugin --profile web add @furayoshi/dsh-widechat@file:/home/<you>/work/dsh-widechat
61
+ ```
62
+
63
+ The `file:` spec is pnpm's way to install from a local path. pnpm links the package's `files` into the profile's `node_modules/@furayoshi/dsh-widechat/`. The profile's `pnpm-workspace.yaml` sets `nodeLinker: hoisted`, so the linked files are **copies**, not symlinks — this means a rebuild of the source does **not** automatically reflect in the installed copy. After editing `src/` and running `npm run build:client`, re-link the package:
64
+
65
+ ```sh
66
+ # From inside the profile directory:
67
+ rm -rf node_modules/@furayoshi && pnpm install
68
+ ```
69
+
70
+ Then restart DSH to pick up the new bundle. (A future version of this plugin could provide a dev script that watches and rebuilds; for now the manual `rm` is the workflow.)
71
+
72
+ ### Updating, removing, verifying
73
+
74
+ Because `dsh plugin` is just a pnpm forwarder, you can use any pnpm subcommand. From inside the profile directory:
75
+
76
+ ```sh
77
+ # Update to the latest version
78
+ pnpm update @furayoshi/dsh-widechat
79
+
80
+ # Re-link after editing the source
81
+ rm -rf node_modules/@furayoshi && pnpm install
82
+
83
+ # Remove the plugin (also strips the entry from dsh.profile.bundles)
84
+ dsh plugin --profile web remove @furayoshi/dsh-widechat
85
+ ```
86
+
87
+ ### Verifying the install
88
+
89
+ Open the Web UI, click the settings cog, go to **General** — you should see a "Wide chat" row with the four controls. Drag the sliders, and the chat column / user bubble / composer update without a reload.
90
+
91
+ To verify DSH actually loaded the bundle, from inside the profile directory:
92
+
93
+ ```sh
94
+ dsh web --dump-config | grep -i "widechat\|furayoshi"
95
+ ```
96
+
97
+ You should see the host row registered under the new package name.
98
+
99
+ If you want to edit the source, the build is `npm run build` (which calls `tsc` for the host lib and `tsx scripts/build-client.ts` for the browser bundle). The `lib/` directory is shipped; the `src/` directory is the source.
100
+
101
+ ## How it works
102
+
103
+ The plugin is two halves:
104
+
105
+ - **Host** (`lib/index.js`) registers a settings namespace `dsh-widechat` with a `z.object` schema (`chatGutterPct`, `statsAlign`, `userBubblePct`, `composerMaxHeightPct`). The schema's bounds match the slider ranges exactly.
106
+ - **Client** (`lib/client/index.js`) binds a `SettingsScope` to that namespace, re-injects a small CSS override on every scope change, and registers a row in the `settings.general.item` slot.
107
+
108
+ The CSS override is intentionally minimal — it touches four things, one per setting:
109
+
110
+ ```css
111
+ :root, .wSkVaW_root, [data-phase] {
112
+ --dsh-chat-content-width: calc(100% - 2 × <chatGutterPct>%) !important;
113
+ --dsh-composer-card-max-width: calc(100% - 2 × <chatGutterPct>%) !important;
114
+ }
115
+ .gdEzaW_userStack { max-width: <userBubblePct>% !important; }
116
+ .uV2eYG_card { max-height: <composerMaxHeightPct>vh !important; overflow: hidden !important; }
117
+ .uV2eYG_scroll { max-height: calc(<composerMaxHeightPct>vh - 64px) !important; }
118
+ .FJxK0a_root { text-align: <statsAlign> !important; }
119
+ ```
120
+
121
+ `chatGutterPct` and `userBubblePct` are **percentages of the cell**; `composerMaxHeightPct` is a **percentage of the viewport**. The cell grows when the sidebar collapses to the rail (56px from 280px), so the column and the bubble grow with it — and the gutter stays at the same percentage. The composer cap, in contrast, is absolute to the viewport so the conversation above is always visible regardless of the sidebar state.
122
+
123
+ The reason there are two rules for the composer (`uV2eYG_card` and `uV2eYG_scroll`) is that the shipped card has *no* max-height and the shipped inner scroll has `max-height: var(--dsh-composer-text-max-height)` (336px by default). Without overriding the inner scroll too, the user's `composerMaxHeightPct` slider would only take effect at values that allow the inner 336px cap to grow, which is values ≥ 19% on a 1914px viewport — the slider would feel broken above 20% on smaller viewports. The inner override (`- 64px` leaves room for the card's padding and accessory row) makes the slider's effect span the full 20–80 range.
124
+
125
+ DSH's shipped rules are left untouched. The column is still `width: 100%`, `max-width: var(--dsh-chat-content-width)`, `margin: 0 auto`. Toggle, scroll, resize — all the DSH default behavior. The plugin only changes the values of variables (and a few hard caps on the composer) that DSH's CSS already reads.
126
+
127
+ ## Configuration
128
+
129
+ The four settings live in the `dsh-widechat` settings namespace. They can also be edited directly in the user's settings document if the npm install flow is bypassed (the file-backed settings doc lives at `~/.dsh/settings.yaml`; see the host-side schema in `src/index.ts` for the canonical field names and bounds).
130
+
131
+ | Field | Type | Default | Range / values | Reference |
132
+ |---|---|---|---|---|
133
+ | `chatGutterPct` | number | `1` | 0–10 (clamped; integers) | cell width |
134
+ | `statsAlign` | string | `"right"` | `"left"`, `"center"`, `"right"` | — |
135
+ | `userBubblePct` | number | `75` | 30–100 (clamped; integers) | chat column |
136
+ | `composerMaxHeightPct` | number | `50` | 20–80 (clamped; integers) | viewport |
137
+
138
+ The slider value is the percentage applied to the reference for that row. A value of `0` is allowed for `chatGutterPct` (no gutter at all), and `100` for `userBubblePct` (your bubble fills the column). `composerMaxHeightPct` at `0` would clip the composer entirely; the slider starts at `20` to keep it usable.
139
+
140
+ Invalid values are dropped to the defaults and a `console.warn` is logged with the offending field name.
141
+
142
+ ## Caveats
143
+
144
+ ### This plugin overrides internal CSS-module class hashes
145
+
146
+ `.wSkVaW_root`, `.Md3f7G_column`, `.uV2eYG_card`, `.uV2eYG_scroll`, `.gdEzaW_userStack`, `.FJxK0a_root` are CSS-Modules-generated class names from `@deepseek-ai/dsh-client-ui-conversation`. Their hashes (the part after the underscore) are recomputed every time that package is rebuilt. Any release of `dsh-client-ui-conversation` can therefore silently break this plugin — specifically, if the shipped rule that reads the variable or has the matching class name changes shape, our override no longer reaches it.
147
+
148
+ After upgrading `@deepseek-ai/dsh-client-ui-conversation`, check whether the chat column widens, the user bubble stays right-aligned at the configured width, the composer is capped at the configured height, and the stats line still aligns. If any of those regress, the upstream CSS-module hashes changed; see "Updating" below.
149
+
150
+ ### The `chatGutterPct` is interpreted against the cell, not the viewport
151
+
152
+ DSH's chat column is `width: 100%` of its cell. The plugin's cap is `100% - 2 × chatGutterPct%`, where the percentage is of the cell. So `chatGutterPct: 1` means "leave 1% of the cell on each side as a gutter", not 1% of the viewport. With the sidebar open the cell is small, so the gutter is small in absolute pixels. With the sidebar closed the cell is wider, so the gutter is wider in pixels but still the same percentage. This is the DSH default behavior — preserved on purpose so the column reflows smoothly when the sidebar toggles.
153
+
154
+ ### The `composerMaxHeightPct` is interpreted against the viewport, not the cell
155
+
156
+ Unlike the other two percentages, the composer cap is `composerMaxHeightPct × 1vh`. The viewport is constant regardless of the sidebar state, so the cap doesn't change when the sidebar toggles. This is intentional: the cap exists to keep the conversation visible, which is a viewport-relative concern.
157
+
158
+ ### What `cordis.patch.yml` is for
159
+
160
+ The package ships a `cordis.patch.yml` (in `files: ["lib", "cordis.patch.yml", "README.md", "LICENSE"]`) that inserts a single host row into DSH's host composition. The row's `apply()` is the function in `src/index.ts` that registers the `dsh-widechat` settings namespace. Without the patch, the host has no idea the plugin exists, the namespace is never registered, and the client side's `bind()` call would resolve a read-only scope. If you fork this plugin and rename the namespace, update the `id` in the patch to match.
161
+
162
+ ## Updating
163
+
164
+ When the upstream hashes change, the fix is mechanical but unavoidable until DSH exposes a stable public API for layout overrides. The plugin targets six CSS-module class hashes:
165
+
166
+ - `.wSkVaW_root` — the conversation root (defines the chat-content-width variable)
167
+ - `.Md3f7G_column` — the chat column (reads the variable as max-width)
168
+ - `.uV2eYG_card` — the composer card (we cap its max-height)
169
+ - `.uV2eYG_scroll` — the composer's inner textarea scroll (we cap its max-height)
170
+ - `.gdEzaW_userStack` — the user-message bubble stack (we set its max-width)
171
+ - `.FJxK0a_root` — the session-stats line (we set its text-align)
172
+
173
+ All six appear in `dsh-client-ui-conversation/lib/client.js`. A regex search for `_module_css_default\s*=\s*{` will land you near the manifests. To update:
174
+
175
+ 1. Open the new `dsh-client-ui-conversation/lib/client.js` in your editor and search for each of the six names above. If a name has changed (the part after the underscore), update the corresponding name in `src/client/css.ts`. If a name has been removed (e.g. DSH restructured the composer), comment out the matching rule and the matching slider in `src/client/WideChatRow.tsx`, and update the README's Caveats and "How it works" sections to match.
176
+ 2. `npm run build` to rebuild the host and client bundles.
177
+ 3. Bump the version in `package.json` (the user-facing change is a patch for a single-hash fix, minor for a class-set change, major for an API change).
178
+
179
+ ## License
180
+
181
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,6 @@
1
+ # Patch layer for `@furayoshi/dsh-widechat`. Inserted into the host composition
2
+ # by `dsh.bundle.patch` (declared in this package's package.json), so the
3
+ # settings-namespace registration in `src/index.ts` actually runs.
4
+ - insert:
5
+ - id: '@furayoshi/dsh-widechat'
6
+ name: '@furayoshi/dsh-widechat'
@@ -0,0 +1,651 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@furayoshi/dsh-widechat",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+
10
+ //#region src/settings.ts
11
+ /**
12
+ * Settings-namespace constants, defaults, and pure utilities shared by both
13
+ * the host and client halves. This file deliberately imports nothing from
14
+ * `@deepseek-ai/dsh-settings` or `@deepseek-ai/schemastery` — those are
15
+ * host-side concerns, and the client half bundles to a closure where
16
+ * runtime `require()` of any package not in the loader's table fails the
17
+ * whole Web UI.
18
+ *
19
+ * @module dsh-widechat/settings
20
+ */
21
+ /** Settings namespace owned by this plugin. */
22
+ const WIDE_CHAT_SETTINGS_NAMESPACE = "dsh-widechat";
23
+ /** Field key for the chat column horizontal gutter, in percent of the cell. */
24
+ const CHAT_GUTTER_PCT_FIELD = "chatGutterPct";
25
+ /** Field key for the session-stats alignment. */
26
+ const STATS_ALIGN_FIELD = "statsAlign";
27
+ /** Field key for the user-bubble width, in percent of the chat column. */
28
+ const USER_BUBBLE_PCT_FIELD = "userBubblePct";
29
+ /** Field key for the composer-card max height, in percent of the viewport. */
30
+ const COMPOSER_MAX_HEIGHT_PCT_FIELD = "composerMaxHeightPct";
31
+ /** Allowed values for `statsAlign`. */
32
+ const STATS_ALIGN_VALUES = [
33
+ "left",
34
+ "center",
35
+ "right"
36
+ ];
37
+ /** Defaults applied when a field is unset. */
38
+ const DEFAULTS = {
39
+ [CHAT_GUTTER_PCT_FIELD]: 1,
40
+ [STATS_ALIGN_FIELD]: "right",
41
+ [USER_BUBBLE_PCT_FIELD]: 75,
42
+ [COMPOSER_MAX_HEIGHT_PCT_FIELD]: 50
43
+ };
44
+
45
+ //#endregion
46
+ //#region src/client/WideChatRow.tsx
47
+ /**
48
+ * The `dsh-widechat` row in the General settings page. Renders four
49
+ * controls:
50
+ * • a slider + number input for `chatGutterPct`
51
+ * • a 3-segment toggle for `statsAlign` (left / center / right)
52
+ * • a slider + number input for `userBubblePct`
53
+ * • a slider + number input for `composerMaxHeightPct`
54
+ *
55
+ * The row subscribes to its SettingsScope so the controls reflect the
56
+ * current value on every change — without the subscription, React would
57
+ * only re-render the row when its own state changes, and the slider
58
+ * thumb would stay where it was after the user dragged it.
59
+ *
60
+ * The scope object's runtime type is `SettingsScope<any>` from
61
+ * `@deepseek-ai/dsh-client-runtime/client`; we keep this file free of
62
+ * runtime imports of that package so the bundle's external list stays
63
+ * tight (and a type-only import is erased at build time).
64
+ */
65
+ /** Read a field from a snapshot value, falling back to the default. */
66
+ function readField(section, field, fallback) {
67
+ if (!section || typeof section !== "object") return fallback;
68
+ const value = section[field];
69
+ return value === void 0 ? fallback : value;
70
+ }
71
+ function clampNumber(value, min, max, fallback) {
72
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
73
+ const rounded = Math.round(value);
74
+ if (rounded < min) return min;
75
+ if (rounded > max) return max;
76
+ return rounded;
77
+ }
78
+ function readValues(scope) {
79
+ const section = scope.getSnapshot().value;
80
+ const gutterPct = clampNumber(readField(section, CHAT_GUTTER_PCT_FIELD, DEFAULTS[CHAT_GUTTER_PCT_FIELD]), 0, 10, DEFAULTS[CHAT_GUTTER_PCT_FIELD]);
81
+ const statsAlignRaw = readField(section, STATS_ALIGN_FIELD, DEFAULTS[STATS_ALIGN_FIELD]);
82
+ return {
83
+ gutterPct,
84
+ statsAlign: STATS_ALIGN_VALUES.includes(statsAlignRaw) ? statsAlignRaw : DEFAULTS[STATS_ALIGN_FIELD],
85
+ userBubblePct: clampNumber(readField(section, USER_BUBBLE_PCT_FIELD, DEFAULTS[USER_BUBBLE_PCT_FIELD]), 30, 100, DEFAULTS[USER_BUBBLE_PCT_FIELD]),
86
+ composerMaxHeightPct: clampNumber(readField(section, COMPOSER_MAX_HEIGHT_PCT_FIELD, DEFAULTS[COMPOSER_MAX_HEIGHT_PCT_FIELD]), 20, 80, DEFAULTS[COMPOSER_MAX_HEIGHT_PCT_FIELD])
87
+ };
88
+ }
89
+ function WideChatRow({ scope, t }) {
90
+ const [tick, setTick] = (0, react.useState)(0);
91
+ (0, react.useEffect)(() => {
92
+ const unsubscribe = scope.subscribe(() => {
93
+ setTick((n) => n + 1);
94
+ });
95
+ return () => {
96
+ unsubscribe();
97
+ };
98
+ }, [scope]);
99
+ const { gutterPct, statsAlign, userBubblePct, composerMaxHeightPct } = readValues(scope);
100
+ const setGutter = (0, react.useCallback)((next) => {
101
+ const clamped = clampNumber(next, 0, 10, DEFAULTS[CHAT_GUTTER_PCT_FIELD]);
102
+ scope.set(CHAT_GUTTER_PCT_FIELD, clamped);
103
+ }, [scope]);
104
+ const setAlign = (0, react.useCallback)((next) => {
105
+ if (STATS_ALIGN_VALUES.includes(next)) scope.set(STATS_ALIGN_FIELD, next);
106
+ }, [scope]);
107
+ const setUserBubble = (0, react.useCallback)((next) => {
108
+ const clamped = clampNumber(next, 30, 100, DEFAULTS[USER_BUBBLE_PCT_FIELD]);
109
+ scope.set(USER_BUBBLE_PCT_FIELD, clamped);
110
+ }, [scope]);
111
+ const setComposerMaxHeight = (0, react.useCallback)((next) => {
112
+ const clamped = clampNumber(next, 20, 80, DEFAULTS[COMPOSER_MAX_HEIGHT_PCT_FIELD]);
113
+ scope.set(COMPOSER_MAX_HEIGHT_PCT_FIELD, clamped);
114
+ }, [scope]);
115
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
116
+ className: "dswc-row",
117
+ "data-plugin": "dsh-widechat",
118
+ "data-tick": tick,
119
+ children: [
120
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
121
+ className: "dswc-row__head",
122
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
123
+ className: "dswc-row__title",
124
+ children: t("row.title")
125
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
126
+ className: "dswc-row__desc",
127
+ children: t("row.description")
128
+ })]
129
+ }),
130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
131
+ className: "dswc-row__field",
132
+ children: [
133
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
134
+ htmlFor: "dswc-gutter",
135
+ children: t("gutter.label")
136
+ }),
137
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
138
+ className: "dswc-row__slider",
139
+ children: [
140
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
141
+ id: "dswc-gutter",
142
+ type: "range",
143
+ min: 0,
144
+ max: 10,
145
+ step: 1,
146
+ value: gutterPct,
147
+ onChange: (event) => setGutter(Number(event.target.value))
148
+ }),
149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
150
+ className: "dswc-row__number",
151
+ type: "number",
152
+ min: 0,
153
+ max: 10,
154
+ step: 1,
155
+ value: gutterPct,
156
+ "aria-label": t("gutter.label"),
157
+ onChange: (event) => {
158
+ const value = Number(event.target.value);
159
+ if (Number.isFinite(value)) setGutter(value);
160
+ }
161
+ }),
162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
163
+ className: "dswc-row__unit",
164
+ children: "%"
165
+ })
166
+ ]
167
+ }),
168
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
169
+ className: "dswc-help",
170
+ children: t("gutter.help")
171
+ })
172
+ ]
173
+ }),
174
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
175
+ className: "dswc-row__field",
176
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: t("align.label") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
177
+ className: "dswc-row__seg",
178
+ role: "group",
179
+ children: STATS_ALIGN_VALUES.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
180
+ type: "button",
181
+ "aria-pressed": statsAlign === value,
182
+ onClick: () => setAlign(value),
183
+ children: t(`align.${value}`)
184
+ }, value))
185
+ })]
186
+ }),
187
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
188
+ className: "dswc-row__field",
189
+ children: [
190
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
191
+ htmlFor: "dswc-user-bubble",
192
+ children: t("userBubble.label")
193
+ }),
194
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
195
+ className: "dswc-row__slider",
196
+ children: [
197
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
198
+ id: "dswc-user-bubble",
199
+ type: "range",
200
+ min: 30,
201
+ max: 100,
202
+ step: 1,
203
+ value: userBubblePct,
204
+ onChange: (event) => setUserBubble(Number(event.target.value))
205
+ }),
206
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
207
+ className: "dswc-row__number",
208
+ type: "number",
209
+ min: 30,
210
+ max: 100,
211
+ step: 1,
212
+ value: userBubblePct,
213
+ "aria-label": t("userBubble.label"),
214
+ onChange: (event) => {
215
+ const value = Number(event.target.value);
216
+ if (Number.isFinite(value)) setUserBubble(value);
217
+ }
218
+ }),
219
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
220
+ className: "dswc-row__unit",
221
+ children: "%"
222
+ })
223
+ ]
224
+ }),
225
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
226
+ className: "dswc-help",
227
+ children: t("userBubble.help")
228
+ })
229
+ ]
230
+ }),
231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
232
+ className: "dswc-row__field",
233
+ children: [
234
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
235
+ htmlFor: "dswc-composer-max-height",
236
+ children: t("composerMaxHeight.label")
237
+ }),
238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
239
+ className: "dswc-row__slider",
240
+ children: [
241
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
242
+ id: "dswc-composer-max-height",
243
+ type: "range",
244
+ min: 20,
245
+ max: 80,
246
+ step: 1,
247
+ value: composerMaxHeightPct,
248
+ onChange: (event) => setComposerMaxHeight(Number(event.target.value))
249
+ }),
250
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
251
+ className: "dswc-row__number",
252
+ type: "number",
253
+ min: 20,
254
+ max: 80,
255
+ step: 1,
256
+ value: composerMaxHeightPct,
257
+ "aria-label": t("composerMaxHeight.label"),
258
+ onChange: (event) => {
259
+ const value = Number(event.target.value);
260
+ if (Number.isFinite(value)) setComposerMaxHeight(value);
261
+ }
262
+ }),
263
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
264
+ className: "dswc-row__unit",
265
+ children: "vh"
266
+ })
267
+ ]
268
+ }),
269
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
270
+ className: "dswc-help",
271
+ children: t("composerMaxHeight.help")
272
+ })
273
+ ]
274
+ })
275
+ ]
276
+ });
277
+ }
278
+
279
+ //#endregion
280
+ //#region src/client/locales.ts
281
+ /**
282
+ * Settings row dictionaries for the `dsh-widechat` General page entry.
283
+ * The shell ships zh and en; a key missing from one falls back to the
284
+ * other, so both are kept complete.
285
+ *
286
+ * @module dsh-widechat/client/locales
287
+ */
288
+ const WIDE_CHAT_LOCALE_NS = "settings.widechat";
289
+ /** Simplified Chinese dictionary (the key-set source of truth). */
290
+ const zh = {
291
+ "row.title": "宽屏对话",
292
+ "row.description": "调整聊天列宽度、用户消息气泡宽度、底部统计对齐方式以及输入框最大高度。",
293
+ "gutter.label": "左右留白",
294
+ "gutter.help": "聊天列两侧留白占视口宽度的百分比。0 为贴边,2 为适中的默认留白。",
295
+ "align.label": "统计对齐",
296
+ "align.left": "左",
297
+ "align.center": "居中",
298
+ "align.right": "右",
299
+ "userBubble.label": "用户消息宽度",
300
+ "userBubble.help": "用户消息气泡占聊天列宽度的百分比。75 为默认。",
301
+ "composerMaxHeight.label": "输入框最大高度",
302
+ "composerMaxHeight.help": "输入框最大高度占视口高度的百分比,避免长输入时遮挡对话。50 为默认。"
303
+ };
304
+ const en = {
305
+ "row.title": "Wide chat",
306
+ "row.description": "Adjust the chat column width, your message bubble width, the bottom-stats alignment, and the composer max height.",
307
+ "gutter.label": "Side gutter",
308
+ "gutter.help": "Chat column gutter as a percent of viewport width. 0 = edge-to-edge, 2 = a comfortable default.",
309
+ "align.label": "Stats alignment",
310
+ "align.left": "Left",
311
+ "align.center": "Center",
312
+ "align.right": "Right",
313
+ "userBubble.label": "Your message width",
314
+ "userBubble.help": "Your message bubble width as a percent of the chat column. 75 is the default.",
315
+ "composerMaxHeight.label": "Composer max height",
316
+ "composerMaxHeight.help": "Composer card max height as a percent of the viewport, so long inputs do not hide the conversation. 50 is the default."
317
+ };
318
+ const dictionaries = {
319
+ zh,
320
+ en
321
+ };
322
+
323
+ //#endregion
324
+ //#region src/client/css.ts
325
+ /**
326
+ * Build the CSS override string for the `dsh-widechat` plugin from a
327
+ * settings snapshot. The configurable knobs are `chatGutterPct`
328
+ * (a percentage of the cell width on each side), `statsAlign`
329
+ * (where the session-stats strip lines up under the composer),
330
+ * `userBubblePct` (a percentage of the chat column for the user-bubble
331
+ * width), and `composerMaxHeightPct` (a percentage of the viewport for
332
+ * the composer's max height, to keep the conversation visible).
333
+ *
334
+ * The override is intentionally minimal: it changes custom properties and
335
+ * a few well-known CSS-module selectors, and lets DSH's shipped CSS keep
336
+ * doing everything else. That means the column reflows on sidebar
337
+ * toggle and window resize, exactly the way the user already knows.
338
+ *
339
+ * Class hashes are the CSS-modules-generated ones from
340
+ * `@deepseek-ai/dsh-client-ui-conversation` and
341
+ * `@deepseek-ai/dsh-client-ui-layout`. They are recomputed on every
342
+ * upstream rebuild; when they change this file is what needs updating.
343
+ */
344
+ /** Validate and clamp a single field. Returns `fallback` on any deviation. */
345
+ function clampInt(value, min, max, fallback) {
346
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
347
+ const rounded = Math.round(value);
348
+ if (rounded < min || rounded > max) return fallback;
349
+ return rounded;
350
+ }
351
+ function asAlign(value, fallback) {
352
+ if (value === "left" || value === "center" || value === "right") return value;
353
+ return fallback;
354
+ }
355
+ function resolveConfig(raw, defaults) {
356
+ const fallbacks = [];
357
+ let section = {};
358
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) section = raw;
359
+ else if (raw !== void 0 && raw !== null) fallbacks.push("section-not-object");
360
+ const chatGutterPctInput = section["chatGutterPct"] ?? defaults.chatGutterPct;
361
+ const statsAlignInput = section["statsAlign"] ?? defaults.statsAlign;
362
+ const userBubblePctInput = section["userBubblePct"] ?? defaults.userBubblePct;
363
+ const composerMaxHeightPctInput = section["composerMaxHeightPct"] ?? defaults.composerMaxHeightPct;
364
+ const chatGutterPctRaw = clampInt(chatGutterPctInput, 0, 10, defaults.chatGutterPct);
365
+ if (chatGutterPctRaw !== chatGutterPctInput) fallbacks.push(`${CHAT_GUTTER_PCT_FIELD}=${JSON.stringify(chatGutterPctInput)}`);
366
+ const statsAlignRaw = asAlign(statsAlignInput, defaults.statsAlign);
367
+ if (statsAlignRaw !== statsAlignInput) fallbacks.push(`${STATS_ALIGN_FIELD}=${JSON.stringify(statsAlignInput)}`);
368
+ const userBubblePctRaw = clampInt(userBubblePctInput, 30, 100, defaults.userBubblePct);
369
+ if (userBubblePctRaw !== userBubblePctInput) fallbacks.push(`${USER_BUBBLE_PCT_FIELD}=${JSON.stringify(userBubblePctInput)}`);
370
+ const composerMaxHeightPctRaw = clampInt(composerMaxHeightPctInput, 20, 80, defaults.composerMaxHeightPct);
371
+ if (composerMaxHeightPctRaw !== composerMaxHeightPctInput) fallbacks.push(`${COMPOSER_MAX_HEIGHT_PCT_FIELD}=${JSON.stringify(composerMaxHeightPctInput)}`);
372
+ return {
373
+ chatGutterPct: chatGutterPctRaw,
374
+ statsAlign: statsAlignRaw,
375
+ userBubblePct: userBubblePctRaw,
376
+ composerMaxHeightPct: composerMaxHeightPctRaw,
377
+ fellBack: fallbacks.length > 0,
378
+ fallbacks
379
+ };
380
+ }
381
+ /**
382
+ * Build the override stylesheet for one resolved config.
383
+ *
384
+ * The chat column cap is widened from the shipped 748px to
385
+ * `100% - 2*chatGutterPct%` of the conversation root. The column
386
+ * itself keeps the shipped `width: 100%` and `margin: 0 auto`, so it
387
+ * still reflows on sidebar toggle and window resize, exactly the way
388
+ * the user already knows — the only change is that the column is no
389
+ * longer capped at 748px and now leaves a configurable gutter on each
390
+ * side.
391
+ *
392
+ * The composer card inherits the same cap through
393
+ * `--dsh-composer-card-max-width`, which the shipped composer CSS
394
+ * already reads. The stats line aligns to the user's choice via
395
+ * `.FJxK0a_root { text-align: ... }`.
396
+ */
397
+ function buildStylesheet(config) {
398
+ const cap = `calc(100% - ${config.chatGutterPct * 2}%)`;
399
+ const ub = `${config.userBubblePct}%`;
400
+ const cmh = `${config.composerMaxHeightPct}vh`;
401
+ return `
402
+ :root,
403
+ .wSkVaW_root,
404
+ [data-phase] {
405
+ --dsh-chat-content-width: ${cap} !important;
406
+ --dsh-composer-card-max-width: ${cap} !important;
407
+ }
408
+
409
+ /* User-message bubble width. The shipped cap is
410
+ * max-width: min(525px, 82%), which keeps user bubbles narrow on wide
411
+ * columns. We replace the cap with the configured userBubblePct of
412
+ * the chat column, so the bubble scales with the column. The shipped
413
+ * align-items: flex-end on .gdEzaW_userRow keeps the bubble pinned
414
+ * to the right edge. */
415
+ .gdEzaW_userStack {
416
+ max-width: ${ub} !important;
417
+ }
418
+
419
+ /* Composer card max height. The shipped card has no max-height, so on
420
+ * long inputs the textarea + accessory row can grow to eat most of
421
+ * the screen and hide the conversation above. We cap the card at the
422
+ * configured percentage of the viewport; the inner scroll keeps the
423
+ * textarea usable, and the user always sees the rest of the conversation.
424
+ *
425
+ * The shipped .uV2eYG_scroll inside the card has its own
426
+ * max-height (--dsh-composer-text-max-height, 336px by default) that
427
+ * silently limits the textarea. We override that too so the user's
428
+ * outer cap actually takes effect: the inner scroll gets the card's
429
+ * cap minus the card's chrome (padding, accessory row, border —
430
+ * roughly 64px) so the textarea can grow with the rest of the card. */
431
+ .uV2eYG_card {
432
+ max-height: ${cmh} !important;
433
+ overflow: hidden !important;
434
+ }
435
+ .uV2eYG_scroll {
436
+ max-height: calc(${cmh} - 64px) !important;
437
+ }
438
+ .uV2eYG_hero .uV2eYG_scroll {
439
+ max-height: calc(${cmh} - 132px) !important;
440
+ }
441
+
442
+ .FJxK0a_root {
443
+ text-align: ${config.statsAlign} !important;
444
+ }
445
+ `;
446
+ }
447
+ /**
448
+ * Create a `<style>` tag owned by this plugin, return a disposer that removes
449
+ * it. Mirrors the pattern shipped by `dsh-client-ui-*` packages: each plugin
450
+ * tags its sheets with `data-plugin` so `claimStyles()` can attribute them
451
+ * during boot, and `data-plugin-css` for the source map.
452
+ */
453
+ function injectStylesheet(css, pluginId) {
454
+ const tag = document.createElement("style");
455
+ tag.dataset.plugin = pluginId;
456
+ tag.dataset.pluginCss = `${pluginId}/styles.css`;
457
+ tag.textContent = css;
458
+ document.head.appendChild(tag);
459
+ return () => {
460
+ tag.remove();
461
+ };
462
+ }
463
+
464
+ //#endregion
465
+ //#region src/client/styles.ts
466
+ /**
467
+ * Self-drawn styles for the `dsh-widechat` settings row. The shipped
468
+ * `settings.general.item` outlet does not project a `className`; the row
469
+ * owns its internals. Class names are scoped with the `dswc-` prefix to
470
+ * avoid collisions with any other plugin that ships its own row CSS.
471
+ *
472
+ * Color tokens come from the DSH theme (`--dsw-alias-*`), so the row
473
+ * automatically tracks light and dark mode without per-theme overrides.
474
+ */
475
+ const STYLES = `
476
+ .dswc-row { display: flex; flex-direction: column; gap: 12px; width: 100%; }
477
+ .dswc-row__head { display: flex; flex-direction: column; gap: 2px; }
478
+ .dswc-row__title { font-size: 14px; font-weight: 600; line-height: 20px; color: var(--dsw-alias-label-primary); }
479
+ .dswc-row__desc { font-size: 12px; line-height: 16px; color: var(--dsw-alias-label-secondary); }
480
+ .dswc-row__field { display: flex; flex-direction: column; gap: 4px; }
481
+ .dswc-row__field > label { font-size: 12px; line-height: 16px; color: var(--dsw-alias-label-secondary); }
482
+ .dswc-row__field > .dswc-help { font-size: 11px; line-height: 14px; color: var(--dsw-alias-label-tertiary); }
483
+ .dswc-row__slider { display: flex; align-items: center; gap: 8px; }
484
+ .dswc-row__slider > input[type="range"] { flex: 1; min-width: 0; }
485
+ .dswc-row__slider > .dswc-row__number {
486
+ width: 56px;
487
+ flex: none;
488
+ font-variant-numeric: tabular-nums;
489
+ text-align: right;
490
+ color: var(--dsw-alias-label-primary);
491
+ background: var(--dsw-alias-input-bg, transparent);
492
+ border: 1px solid var(--dsw-alias-border-l2);
493
+ border-radius: 4px;
494
+ padding: 2px 6px;
495
+ font-size: 12px;
496
+ line-height: 18px;
497
+ appearance: textfield;
498
+ -moz-appearance: textfield;
499
+ }
500
+ .dswc-row__slider > .dswc-row__number::-webkit-outer-spin-button,
501
+ .dswc-row__slider > .dswc-row__number::-webkit-inner-spin-button {
502
+ appearance: none;
503
+ -webkit-appearance: none;
504
+ margin: 0;
505
+ }
506
+ .dswc-row__slider > .dswc-row__unit { color: var(--dsw-alias-label-secondary); font-size: 12px; }
507
+ .dswc-row__seg { display: inline-flex; border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; overflow: hidden; }
508
+ .dswc-row__seg button { appearance: none; background: transparent; border: 0; padding: 4px 12px; font-size: 12px; cursor: pointer; color: var(--dsw-alias-label-secondary); border-right: 1px solid var(--dsw-alias-border-l2); }
509
+ .dswc-row__seg button:last-child { border-right: 0; }
510
+ .dswc-row__seg button[aria-pressed="true"] { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); font-weight: 600; }
511
+ .dswc-row__toggle { display: flex; align-items: center; gap: 8px; }
512
+ .dswc-row__toggle > input[type="checkbox"] { width: 16px; height: 16px; }
513
+ `;
514
+
515
+ //#endregion
516
+ //#region src/client/index.ts
517
+ /**
518
+ * Browser half: registers the `dsh-widechat` row into the General settings
519
+ * page (`settings.general.item`) and keeps the CSS override sheet in sync
520
+ * with the user's settings on the bound scope.
521
+ *
522
+ * The row reads through a SettingsScope bound to the same namespace the host
523
+ * half registered under (`WIDE_CHAT_SETTINGS_NAMESPACE`); writes go through
524
+ * `scope.set` / `scope.unset`. The CSS apply subscribes to the same scope
525
+ * via `scope.subscribe(...)` and re-injects the stylesheet on every change.
526
+ *
527
+ * On every config read the values go through `resolveConfig`, which clamps
528
+ * to bounds and substitutes defaults on any out-of-range or wrong-typed
529
+ * field. A soft `console.warn` is logged when any fallback fires so the
530
+ * user can see what their input was rejected for without the page refusing
531
+ * to render.
532
+ */
533
+ /** Plugin id we tag our `<style>` tags with. */
534
+ const PLUGIN_ID = "dsh-widechat";
535
+ /**
536
+ * Required client services. `settingsScope` provides the per-namespace scope
537
+ * we bind to; `slots` / `locale` declare the row and the dictionaries.
538
+ * `connection` is required by SettingsScope to write through the wire.
539
+ */
540
+ const inject = [
541
+ "slots",
542
+ "locale",
543
+ "connection",
544
+ "settingsScope"
545
+ ];
546
+ /** Owning row slot id (drives the order in the General section). */
547
+ const ROW_ID = "wide-chat";
548
+ /** Position in the General section. Lower numbers render first. */
549
+ const ROW_ORDER = 50;
550
+ /**
551
+ * Install the row's own self-drawn CSS once. The row's class names match
552
+ * nothing until this lands; without it the row still renders, just with
553
+ * browser defaults, which reads as a broken UI.
554
+ */
555
+ function installRowStyles() {
556
+ if (typeof document === "undefined") return () => {};
557
+ const tag = document.createElement("style");
558
+ tag.dataset.plugin = PLUGIN_ID;
559
+ tag.dataset.pluginCss = `${PLUGIN_ID}/row.css`;
560
+ tag.textContent = STYLES;
561
+ document.head.appendChild(tag);
562
+ return () => {
563
+ tag.remove();
564
+ };
565
+ }
566
+ /**
567
+ * Compute the stylesheet for the current scope value (or the defaults if the
568
+ * scope has no value yet) and inject it. Returns the disposer the caller
569
+ * hands to `ctx.effect` so the tag is removed on plugin unload.
570
+ */
571
+ function applyForScope(scopeValue) {
572
+ const resolved = resolveConfig(scopeValue, {
573
+ chatGutterPct: DEFAULTS[CHAT_GUTTER_PCT_FIELD],
574
+ statsAlign: DEFAULTS[STATS_ALIGN_FIELD],
575
+ userBubblePct: DEFAULTS[USER_BUBBLE_PCT_FIELD],
576
+ composerMaxHeightPct: DEFAULTS[COMPOSER_MAX_HEIGHT_PCT_FIELD]
577
+ });
578
+ if (resolved.fellBack) console.warn(`[${PLUGIN_ID}] config fell back to defaults for: ${resolved.fallbacks.join(", ")}`);
579
+ return injectStylesheet(buildStylesheet(resolved), PLUGIN_ID);
580
+ }
581
+ /** Read the current section value out of a scope snapshot. */
582
+ function readSection(snapshot) {
583
+ if (snapshot === null || snapshot === void 0) return void 0;
584
+ return snapshot.value;
585
+ }
586
+ /**
587
+ * Client plugin body: register dictionaries, install the row's own styles,
588
+ * bind the settings scope, and keep the override sheet synced on changes.
589
+ *
590
+ * The CSS sheet is re-injected (old tag removed, new tag appended) on every
591
+ * scope change. That keeps the override simple — a single `<style>` tag
592
+ * whose text is the current resolved stylesheet — at the cost of one
593
+ * document mutation per change. The user changes these settings rarely
594
+ * (slider drag, align toggle, gutter toggle) so the cost is negligible.
595
+ *
596
+ * @param ctx - client cordis context.
597
+ */
598
+ function apply(ctx) {
599
+ ctx.effect(() => installRowStyles(), "dsh-widechat: row styles");
600
+ ctx.effect(() => ctx.locale.register(WIDE_CHAT_LOCALE_NS, dictionaries), "dsh-widechat: dictionaries");
601
+ let scope = null;
602
+ try {
603
+ scope = ctx.settingsScope.bind({ namespace: WIDE_CHAT_SETTINGS_NAMESPACE });
604
+ } catch (error) {
605
+ console.warn(`[${PLUGIN_ID}] settingsScope.bind failed:`, error);
606
+ }
607
+ if (scope !== null) ctx.effect(() => {
608
+ let disposeSheet = () => {};
609
+ try {
610
+ const reapply = () => {
611
+ disposeSheet();
612
+ try {
613
+ disposeSheet = applyForScope(readSection(scope.getSnapshot()));
614
+ } catch (error) {
615
+ console.warn(`[${PLUGIN_ID}] reapply failed:`, error);
616
+ disposeSheet = () => {};
617
+ }
618
+ };
619
+ reapply();
620
+ const unsubscribe = scope.subscribe(reapply);
621
+ return () => {
622
+ unsubscribe();
623
+ disposeSheet();
624
+ };
625
+ } catch (error) {
626
+ console.warn(`[${PLUGIN_ID}] initial stylesheet inject failed:`, error);
627
+ return () => {};
628
+ }
629
+ }, "dsh-widechat: settings-driven stylesheet");
630
+ else ctx.effect(() => {
631
+ const disposeSheet = applyForScope(void 0);
632
+ return () => disposeSheet();
633
+ }, "dsh-widechat: settings-driven stylesheet (no scope)");
634
+ const injected = () => ({ scope });
635
+ ctx.slots.inject("settings.general.item", function* () {
636
+ yield ctx.slots.register({
637
+ name: "settings.general.item",
638
+ id: ROW_ID,
639
+ order: ROW_ORDER,
640
+ locale: WIDE_CHAT_LOCALE_NS,
641
+ inject: injected
642
+ }, WideChatRow);
643
+ });
644
+ }
645
+
646
+ //#endregion
647
+ exports.apply = apply;
648
+ exports.inject = inject;
649
+ return module.exports;
650
+ }
651
+ });
package/lib/index.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ /**
3
+ * Durable settings schema. The browser half binds a scope to this same
4
+ * namespace, reads these fields, and writes them through `scope.set` /
5
+ * `scope.unset`. Defaults live in `./settings.ts`; the schema declares
6
+ * shape and bounds, not defaults.
7
+ */
8
+ export declare const WideChatSettingsSchema: z<Schemastery.ObjectS<{
9
+ chatGutterPct: z<number, number>;
10
+ statsAlign: z<string, string>;
11
+ userBubblePct: z<number, number>;
12
+ composerMaxHeightPct: z<number, number>;
13
+ }>, Schemastery.ObjectT<{
14
+ chatGutterPct: z<number, number>;
15
+ statsAlign: z<string, string>;
16
+ userBubblePct: z<number, number>;
17
+ composerMaxHeightPct: z<number, number>;
18
+ }>>;
19
+ /** Host plugin body. Registers the namespace with the host settings service. */
20
+ export declare function apply(ctx: any): void;
21
+ export { WIDE_CHAT_SETTINGS_NAMESPACE, CHAT_GUTTER_PCT_FIELD, STATS_ALIGN_FIELD, STATS_ALIGN_VALUES, CHAT_GUTTER_PCT_MIN, CHAT_GUTTER_PCT_MAX, USER_BUBBLE_PCT_FIELD, USER_BUBBLE_PCT_MIN, USER_BUBBLE_PCT_MAX, COMPOSER_MAX_HEIGHT_PCT_FIELD, COMPOSER_MAX_HEIGHT_PCT_MIN, COMPOSER_MAX_HEIGHT_PCT_MAX, DEFAULTS, } from "./settings.js";
package/lib/index.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Host half of `@furayoshi/dsh-widechat`. Registers a settings namespace
3
+ * under `dsh-widechat` whose schema the browser half binds to via
4
+ * `ctx.settingsScope.bind({ namespace: "dsh-widechat" })`. The browser
5
+ * writes here, this registration makes those writes durable, and the browser
6
+ * reads them back through the same scope on every settings change.
7
+ *
8
+ * Loaded by the DSH Cordis Loader on the host. The companion `cordis.patch.yml`
9
+ * inserts this row into the host composition so the registration actually
10
+ * runs; the npm install flow also needs the bundle-patch row for the same
11
+ * reason (`dsh.bundle.patch`).
12
+ *
13
+ * The constants, defaults, and field keys live in `./settings.ts` so the
14
+ * client half can import them without pulling `@deepseek-ai/dsh-settings`
15
+ * and `@deepseek-ai/schemastery` into the browser bundle (where the loader
16
+ * has no module table entry for them and would refuse to resolve).
17
+ */
18
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
19
+ import z from "@deepseek-ai/schemastery";
20
+ import { CHAT_GUTTER_PCT_FIELD, CHAT_GUTTER_PCT_MAX, CHAT_GUTTER_PCT_MIN, COMPOSER_MAX_HEIGHT_PCT_FIELD, COMPOSER_MAX_HEIGHT_PCT_MAX, COMPOSER_MAX_HEIGHT_PCT_MIN, STATS_ALIGN_FIELD, STATS_ALIGN_VALUES, USER_BUBBLE_PCT_FIELD, USER_BUBBLE_PCT_MAX, USER_BUBBLE_PCT_MIN, WIDE_CHAT_SETTINGS_NAMESPACE, } from "./settings.js";
21
+ /**
22
+ * Durable settings schema. The browser half binds a scope to this same
23
+ * namespace, reads these fields, and writes them through `scope.set` /
24
+ * `scope.unset`. Defaults live in `./settings.ts`; the schema declares
25
+ * shape and bounds, not defaults.
26
+ */
27
+ export const WideChatSettingsSchema = z.object({
28
+ [CHAT_GUTTER_PCT_FIELD]: z
29
+ .number()
30
+ .min(CHAT_GUTTER_PCT_MIN)
31
+ .max(CHAT_GUTTER_PCT_MAX)
32
+ .required(false),
33
+ [STATS_ALIGN_FIELD]: z.union(STATS_ALIGN_VALUES).required(false),
34
+ [USER_BUBBLE_PCT_FIELD]: z
35
+ .number()
36
+ .min(USER_BUBBLE_PCT_MIN)
37
+ .max(USER_BUBBLE_PCT_MAX)
38
+ .required(false),
39
+ [COMPOSER_MAX_HEIGHT_PCT_FIELD]: z
40
+ .number()
41
+ .min(COMPOSER_MAX_HEIGHT_PCT_MIN)
42
+ .max(COMPOSER_MAX_HEIGHT_PCT_MAX)
43
+ .required(false),
44
+ });
45
+ /** Host plugin body. Registers the namespace with the host settings service. */
46
+ export function apply(ctx) {
47
+ ctx.inject(["settings"], (settingsCtx) => {
48
+ settingsCtx.settings.register(settingsNamespace(WIDE_CHAT_SETTINGS_NAMESPACE), WideChatSettingsSchema);
49
+ });
50
+ }
51
+ export { WIDE_CHAT_SETTINGS_NAMESPACE, CHAT_GUTTER_PCT_FIELD, STATS_ALIGN_FIELD, STATS_ALIGN_VALUES, CHAT_GUTTER_PCT_MIN, CHAT_GUTTER_PCT_MAX, USER_BUBBLE_PCT_FIELD, USER_BUBBLE_PCT_MIN, USER_BUBBLE_PCT_MAX, COMPOSER_MAX_HEIGHT_PCT_FIELD, COMPOSER_MAX_HEIGHT_PCT_MIN, COMPOSER_MAX_HEIGHT_PCT_MAX, DEFAULTS, } from "./settings.js";
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Settings-namespace constants, defaults, and pure utilities shared by both
3
+ * the host and client halves. This file deliberately imports nothing from
4
+ * `@deepseek-ai/dsh-settings` or `@deepseek-ai/schemastery` — those are
5
+ * host-side concerns, and the client half bundles to a closure where
6
+ * runtime `require()` of any package not in the loader's table fails the
7
+ * whole Web UI.
8
+ *
9
+ * @module dsh-widechat/settings
10
+ */
11
+ /** Settings namespace owned by this plugin. */
12
+ export declare const WIDE_CHAT_SETTINGS_NAMESPACE = "dsh-widechat";
13
+ /** Field key for the chat column horizontal gutter, in percent of the cell. */
14
+ export declare const CHAT_GUTTER_PCT_FIELD = "chatGutterPct";
15
+ /** Field key for the session-stats alignment. */
16
+ export declare const STATS_ALIGN_FIELD = "statsAlign";
17
+ /** Field key for the user-bubble width, in percent of the chat column. */
18
+ export declare const USER_BUBBLE_PCT_FIELD = "userBubblePct";
19
+ /** Field key for the composer-card max height, in percent of the viewport. */
20
+ export declare const COMPOSER_MAX_HEIGHT_PCT_FIELD = "composerMaxHeightPct";
21
+ /** Allowed values for `statsAlign`. */
22
+ export declare const STATS_ALIGN_VALUES: readonly ["left", "center", "right"];
23
+ export type StatsAlign = (typeof STATS_ALIGN_VALUES)[number];
24
+ /** Hard limits for the gutter percentage. */
25
+ export declare const CHAT_GUTTER_PCT_MIN = 0;
26
+ export declare const CHAT_GUTTER_PCT_MAX = 10;
27
+ /** Hard limits for the user-bubble percentage. */
28
+ export declare const USER_BUBBLE_PCT_MIN = 30;
29
+ export declare const USER_BUBBLE_PCT_MAX = 100;
30
+ /** Hard limits for the composer-card max-height percentage.
31
+ * The shipped composer card has an implicit natural-height cap of around
32
+ * 43vh (textarea inner scroll capped at 336px + accessory + padding).
33
+ * This plugin also overrides the inner scroll so the user's cap
34
+ * actually takes effect; the slider range is therefore the full
35
+ * 20–80 the user can use, capped by viewport height. */
36
+ export declare const COMPOSER_MAX_HEIGHT_PCT_MIN = 20;
37
+ export declare const COMPOSER_MAX_HEIGHT_PCT_MAX = 80;
38
+ /** Defaults applied when a field is unset. */
39
+ export declare const DEFAULTS: {
40
+ readonly chatGutterPct: 1;
41
+ readonly statsAlign: "right";
42
+ readonly userBubblePct: 75;
43
+ readonly composerMaxHeightPct: 50;
44
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Settings-namespace constants, defaults, and pure utilities shared by both
3
+ * the host and client halves. This file deliberately imports nothing from
4
+ * `@deepseek-ai/dsh-settings` or `@deepseek-ai/schemastery` — those are
5
+ * host-side concerns, and the client half bundles to a closure where
6
+ * runtime `require()` of any package not in the loader's table fails the
7
+ * whole Web UI.
8
+ *
9
+ * @module dsh-widechat/settings
10
+ */
11
+ /** Settings namespace owned by this plugin. */
12
+ export const WIDE_CHAT_SETTINGS_NAMESPACE = "dsh-widechat";
13
+ /** Field key for the chat column horizontal gutter, in percent of the cell. */
14
+ export const CHAT_GUTTER_PCT_FIELD = "chatGutterPct";
15
+ /** Field key for the session-stats alignment. */
16
+ export const STATS_ALIGN_FIELD = "statsAlign";
17
+ /** Field key for the user-bubble width, in percent of the chat column. */
18
+ export const USER_BUBBLE_PCT_FIELD = "userBubblePct";
19
+ /** Field key for the composer-card max height, in percent of the viewport. */
20
+ export const COMPOSER_MAX_HEIGHT_PCT_FIELD = "composerMaxHeightPct";
21
+ /** Allowed values for `statsAlign`. */
22
+ export const STATS_ALIGN_VALUES = ["left", "center", "right"];
23
+ /** Hard limits for the gutter percentage. */
24
+ export const CHAT_GUTTER_PCT_MIN = 0;
25
+ export const CHAT_GUTTER_PCT_MAX = 10;
26
+ /** Hard limits for the user-bubble percentage. */
27
+ export const USER_BUBBLE_PCT_MIN = 30;
28
+ export const USER_BUBBLE_PCT_MAX = 100;
29
+ /** Hard limits for the composer-card max-height percentage.
30
+ * The shipped composer card has an implicit natural-height cap of around
31
+ * 43vh (textarea inner scroll capped at 336px + accessory + padding).
32
+ * This plugin also overrides the inner scroll so the user's cap
33
+ * actually takes effect; the slider range is therefore the full
34
+ * 20–80 the user can use, capped by viewport height. */
35
+ export const COMPOSER_MAX_HEIGHT_PCT_MIN = 20;
36
+ export const COMPOSER_MAX_HEIGHT_PCT_MAX = 80;
37
+ /** Defaults applied when a field is unset. */
38
+ export const DEFAULTS = {
39
+ [CHAT_GUTTER_PCT_FIELD]: 1,
40
+ [STATS_ALIGN_FIELD]: "right",
41
+ [USER_BUBBLE_PCT_FIELD]: 75,
42
+ [COMPOSER_MAX_HEIGHT_PCT_FIELD]: 50,
43
+ };
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@furayoshi/dsh-widechat",
3
+ "version": "0.7.0",
4
+ "description": "DeepSeek Harness plugin: widens the conversation column, sets the user-bubble width and composer max height, and right-aligns the session stats strip, with a settings-row UI to tune all four. Built for the DSH client plugin slot `settings.general.item`.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./lib/index.d.ts",
10
+ "default": "./lib/index.js"
11
+ },
12
+ "./client": {
13
+ "types": "./lib/client/index.d.ts",
14
+ "default": "./lib/client/index.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "lib",
20
+ "cordis.patch.yml",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "keywords": [
25
+ "deepseek-harness",
26
+ "dsh",
27
+ "plugin",
28
+ "conversation",
29
+ "layout",
30
+ "css",
31
+ "settings"
32
+ ],
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/FraYoshi/dsh-widechat.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/FraYoshi/dsh-widechat/issues"
40
+ },
41
+ "homepage": "https://github.com/FraYoshi/dsh-widechat#readme",
42
+ "dsh": {
43
+ "bundle": {
44
+ "patch": "./cordis.patch.yml"
45
+ },
46
+ "client": {
47
+ "platform": "web",
48
+ "inject": [
49
+ "@deepseek-ai/dsh-client-runtime",
50
+ "@deepseek-ai/dsh-client-ui-settings",
51
+ "@deepseek-ai/dsh-client-ui-settings-general",
52
+ "@deepseek-ai/dsh-client-locale"
53
+ ]
54
+ }
55
+ },
56
+ "scripts": {
57
+ "build": "tsc -p tsconfig.json && tsx scripts/build-client.ts",
58
+ "build:client": "tsx scripts/build-client.ts",
59
+ "typecheck": "tsc -p tsconfig.typecheck.json",
60
+ "prepublishOnly": "npm run build"
61
+ },
62
+ "peerDependencies": {
63
+ "@deepseek-ai/cordis": ">=4.0.1",
64
+ "@deepseek-ai/dsh-settings": ">=0.1.0-rc.8"
65
+ },
66
+ "dependencies": {
67
+ "@deepseek-ai/schemastery": "^3.18.1"
68
+ },
69
+ "devDependencies": {
70
+ "@deepseek-ai/cordis": "4.0.1",
71
+ "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.8",
72
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.8",
73
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.8",
74
+ "@deepseek-ai/dsh-settings": "0.1.0-rc.8",
75
+ "@types/node": "^22.15.0",
76
+ "@types/react": "^19.2.18",
77
+ "react": "^18.3.1",
78
+ "tsdown": "^0.22.14",
79
+ "tsx": "^4.23.12",
80
+ "typescript": "^5.7.0"
81
+ },
82
+ "engines": {
83
+ "node": ">=20"
84
+ },
85
+ "publishConfig": {
86
+ "access": "public"
87
+ }
88
+ }