@klarkxy/dsh-git-commit 0.1.4 → 0.1.6

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.
@@ -3,7 +3,467 @@ let react = require("react");
3
3
  let react_dom = require("react-dom");
4
4
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
5
5
  let react_jsx_runtime = require("react/jsx-runtime");
6
+ //#region ../dsh-plugin-kit/src/model-menu.ts
7
+ /**
8
+ * Shared plugin-page model menu.
9
+ *
10
+ * A feature plugin's own settings row exposes a model select. An empty
11
+ * selection means the host default chat model for that feature's own call.
12
+ *
13
+ * Browser-safe and React-optional: pure helpers live here so hosts and clients
14
+ * share one catalog contract without bundling React into the host build.
15
+ */
16
+ const KEY_SEPARATOR = "";
17
+ function modelMenuChoiceKey(provider, model) {
18
+ return provider && model ? `${provider}${KEY_SEPARATOR}${model}` : "";
19
+ }
20
+ function parseModelMenuChoiceKey(value) {
21
+ const index = value.indexOf(KEY_SEPARATOR);
22
+ if (index <= 0) return void 0;
23
+ const provider = value.slice(0, index);
24
+ const model = value.slice(index + 1);
25
+ if (!provider || !model) return void 0;
26
+ return {
27
+ provider,
28
+ model
29
+ };
30
+ }
31
+ function asRecord(value) {
32
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
33
+ }
34
+ function asText(value) {
35
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
36
+ }
37
+ function parseEffort(value) {
38
+ const row = asRecord(value);
39
+ const id = row ? asText(row.id) : void 0;
40
+ if (!id) return void 0;
41
+ return {
42
+ id,
43
+ name: row ? asText(row.name) ?? id : id
44
+ };
45
+ }
46
+ /** Parse the host session catalog into flat choices, keeping a saved route selectable. */
47
+ function parseModelMenuChoices(value, bound) {
48
+ const envelope = asRecord(value);
49
+ const root = (envelope && envelope.ok === true ? asRecord(envelope.value) : void 0) ?? envelope ?? {};
50
+ const groups = Array.isArray(root.groups) ? root.groups : [];
51
+ const choices = [];
52
+ for (const group of groups) {
53
+ const groupRow = asRecord(group);
54
+ const provider = groupRow ? asText(groupRow.id) : void 0;
55
+ if (!provider || !groupRow || !Array.isArray(groupRow.models)) continue;
56
+ const providerName = asText(groupRow.name) ?? provider;
57
+ for (const entry of groupRow.models) {
58
+ const modelRow = asRecord(entry);
59
+ const model = modelRow ? asText(modelRow.id) : void 0;
60
+ if (!model || !modelRow) continue;
61
+ const name = asText(modelRow.name) ?? model;
62
+ const reasoning = asRecord(modelRow.reasoning);
63
+ const efforts = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts.flatMap((effort) => {
64
+ const parsed = parseEffort(effort);
65
+ return parsed ? [parsed] : [];
66
+ }) : [];
67
+ choices.push({
68
+ provider,
69
+ model,
70
+ label: `${providerName} / ${name}`,
71
+ efforts
72
+ });
73
+ }
74
+ }
75
+ if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
76
+ provider: bound.provider,
77
+ model: bound.model,
78
+ label: `${bound.provider} / ${bound.model}`,
79
+ efforts: []
80
+ });
81
+ return choices;
82
+ }
83
+ /** Reasoning-effort options for one choice; the saved value stays selectable. */
84
+ function modelMenuEffortOptions(choice, current) {
85
+ const listed = (choice?.efforts ?? []).map((item) => ({ ...item }));
86
+ if (current && !listed.some((item) => item.id === current)) listed.push({
87
+ id: current,
88
+ name: current
89
+ });
90
+ return listed;
91
+ }
92
+ Object.values({
93
+ /** Surfaces, from the page up to the topmost nested card. */
94
+ surface: [
95
+ "--dsw-alias-bg-base",
96
+ "--dsw-alias-bg-layer-1",
97
+ "--dsw-alias-bg-layer-2",
98
+ "--dsw-alias-bg-layer-3",
99
+ "--dsw-alias-bg-layer-4",
100
+ "--dsw-alias-bg-module-platform",
101
+ "--dsw-alias-bg-overlay"
102
+ ],
103
+ /** Hairlines. l1 is near-invisible on a menu surface, so menus use l2+. */
104
+ border: [
105
+ "--dsw-alias-border-l1",
106
+ "--dsw-alias-border-l2",
107
+ "--dsw-alias-border-l3",
108
+ "--dsw-alias-border-l4"
109
+ ],
110
+ /** Text. `caption` is menu shortcut text, `dimmed` is placeholder text. */
111
+ label: [
112
+ "--dsw-alias-label-primary",
113
+ "--dsw-alias-label-secondary",
114
+ "--dsw-alias-label-tertiary",
115
+ "--dsw-alias-label-caption",
116
+ "--dsw-alias-label-dimmed",
117
+ "--dsw-alias-label-error",
118
+ "--dsw-alias-label-primary-foreground"
119
+ ],
120
+ /** Interactive fills. The `-danger` hover belongs to destructive rows. */
121
+ interactive: [
122
+ "--dsw-alias-interactive-bg-hover",
123
+ "--dsw-alias-interactive-bg-active",
124
+ "--dsw-alias-interactive-bg-hover-danger"
125
+ ],
126
+ /** Accent and status. `business` is the focus ring's default colour. */
127
+ accent: [
128
+ "--dsw-alias-brand-primary",
129
+ "--dsw-alias-button-primary-fill",
130
+ "--dsw-alias-button-primary-hover",
131
+ "--dsw-alias-button-tool-bar-fill",
132
+ "--dsw-alias-button-tool-bar-hover",
133
+ "--dsw-alias-button-ghost-active-fill",
134
+ "--dsw-alias-button-ghost-active-border"
135
+ ],
136
+ state: [
137
+ "--dsw-alias-state-business-primary",
138
+ "--dsw-alias-state-success-primary",
139
+ "--dsw-alias-state-warn-primary",
140
+ "--dsw-alias-state-error-primary",
141
+ "--dsw-alias-state-idle-primary"
142
+ ],
143
+ /** Corner scale: sm 8, md 12, lg the large card, panel the modal. */
144
+ radius: [
145
+ "--dsw-radius-sm",
146
+ "--dsw-radius-md",
147
+ "--dsw-radius-lg",
148
+ "--dsw-radius-panel"
149
+ ],
150
+ /** An elevated surface draws its edge in the shadow, so it keeps border: 0. */
151
+ elevation: [
152
+ "--dsw-elevation-soft",
153
+ "--dsw-elevation-stroke",
154
+ "--dsw-elevation-stroke-color",
155
+ "--dsw-elevation-panel",
156
+ "--dsw-elevation-prominent"
157
+ ],
158
+ /** Floats, masks and scrollbars. */
159
+ overlay: [
160
+ "--dsw-menu-surface-fill",
161
+ "--dsw-menu-backdrop-filter",
162
+ "--dsw-mask-blur",
163
+ "--dsw-alias-bg-mask-1",
164
+ "--dsw-alias-menu-icon",
165
+ "--dsw-alias-scrollbar-bg-l2",
166
+ "--dsw-alias-scrollbar-hover-l2",
167
+ "--dsw-shadow-lv3"
168
+ ],
169
+ /** Motion, focus and type axis, published by the host rather than by a component. */
170
+ system: [
171
+ "--dsw-focus-ring-width",
172
+ "--dsw-focus-ring-color",
173
+ "--dsw-font-markdown-code-font-family",
174
+ "--ds-transition-duration",
175
+ "--ds-ease-in-out",
176
+ "--ds-font-family-code",
177
+ "--dsh-scrollbar-thumb",
178
+ "--dsh-scrollbar-thumb-hover",
179
+ "--dsh-content-font-size-secondary",
180
+ "--dsh-content-font-delta",
181
+ "--dsh-frame-top-clearance"
182
+ ]
183
+ }).flat();
184
+ /**
185
+ * The host's focus ring shorthand, verbatim. Writing this per control is how
186
+ * rings drift apart; a stylesheet that draws its own focus style should use
187
+ * {@link officialFocus} instead. The colour fallback is the host's own default.
188
+ */
189
+ const OFFICIAL_FOCUS_RING = "var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))";
190
+ /**
191
+ * A complete `outline` declaration at the official geometry. `offset` defaults
192
+ * to the 2px the native controls use; the host's inline field rules sit at 1px
193
+ * and a segmented tab pulls the ring inside itself at -2px.
194
+ */
195
+ function officialFocus(offset = "2px") {
196
+ return `outline: ${OFFICIAL_FOCUS_RING}; outline-offset: ${offset}`;
197
+ }
198
+ /**
199
+ * An elevated surface. The host draws a raised surface's edge inside its
200
+ * shadow rather than as a border, so a card sets `border: 0` and takes the edge
201
+ * from the elevation — which is also why its border stays off the alias-border
202
+ * set that a flat, non-elevated rule would use.
203
+ */
204
+ function officialElevation(level) {
205
+ return `border: 0; box-shadow: var(--dsw-elevation-${level})`;
206
+ }
207
+ /**
208
+ * The shared stylesheet for a plugin UI, scoped under that plugin's root class.
209
+ *
210
+ * Scoping is what keeps this safe to always-bundle into every client: the rules
211
+ * can only match inside the plugin's own subtree, so two plugins can both mount
212
+ * `dsh-ui-card` without either styling the other's markup.
213
+ *
214
+ * Each recipe matches the root element *and* its descendants, because the
215
+ * commonest arrangement is one element that is both — a surface that carries
216
+ * the root class and `dsh-ui-card`, or a portaled dialog that is the root and
217
+ * the `dsh-ui-surface`. A descendant-only scope would silently leave those
218
+ * unstyled, with no error anywhere.
219
+ *
220
+ * A caveat worth knowing: `Modal` and `Menu` portal to `document.body`, so their
221
+ * content leaves the plugin's subtree entirely. A portaled surface therefore
222
+ * needs its own root class on an element inside the portal.
223
+ *
224
+ * Only patterns that primitives do not provide live here. If a control appears
225
+ * below that `Button`/`Input`/`Switch`/`Tag`/`Menu` already covers, that is a
226
+ * bug in this file: use the primitive and delete the recipe.
227
+ */
228
+ function officialUiCss(roots) {
229
+ const scope = (typeof roots === "string" ? [roots] : roots).map((root) => `.${root}`).join(", ");
230
+ return `
231
+ /* Official DSH Web UI contract. Generated by @klarkxy/dsh-plugin-kit/official-ui.
232
+ * Do not hand-edit the copy inside a plugin: change this module so every plugin
233
+ * moves together. Scope: ${scope} */
234
+ /* The scope rule below stays open for the whole sheet: every rule after it
235
+ * nests with the parent selector, so one stylesheet can cover several unrelated
236
+ * roots without leaking into a neighbouring plugin. Its closing brace is last. */
237
+ ${scope} {
238
+ color: var(--dsw-alias-label-primary);
239
+ font-family: inherit;
240
+ font-size: 14px;
241
+ line-height: 22px;
242
+ min-width: 0;
243
+
244
+ & *, & *::before, & *::after { box-sizing: border-box; }
245
+ & h1, & h2, & h3, & h4, & p, & figure { margin: 0; }
246
+ /* Keyboard focus only: pointer input keeps the native resting appearance. The
247
+ * ring needs no radius of its own — the outline already follows the control's
248
+ * own border-radius, and overriding it here would flatten every control. */
249
+ & :focus-visible { ${officialFocus()}; }
250
+ & [hidden] { display: none !important; }
251
+
252
+ /* --- Type tiers. The same 14/22 and 13/20 steps the primitives use. --- */
253
+ & .dsh-ui-title { font-size: 16px; line-height: 24px; font-weight: 500; color: var(--dsw-alias-label-primary); }
254
+ & .dsh-ui-heading { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
255
+ & .dsh-ui-compact { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary); }
256
+ & .dsh-ui-meta { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
257
+ & .dsh-ui-hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
258
+ & .dsh-ui-error { font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); }
259
+ & .dsh-ui-warn { color: var(--dsw-alias-state-warn-primary); }
260
+ & .dsh-ui-success { color: var(--dsw-alias-state-success-primary); }
261
+ & .dsh-ui-muted { color: var(--dsw-alias-label-tertiary); }
262
+ & .dsh-ui-truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
263
+ & .dsh-ui-wrap { min-width: 0; overflow-wrap: anywhere; }
264
+
265
+ /* --- Layout scaffolding. --- */
266
+ & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
267
+ & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
268
+ & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
269
+ & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
270
+ & .dsh-ui-spacer { flex: 1; min-width: 0; }
271
+ /* A 0.5px l2 rule is the host's section divider; reserve the same rhythm. */
272
+ & .dsh-ui-divider { height: 0.5px; background: var(--dsw-alias-border-l2); }
273
+ & .dsh-ui-section { display: flex; flex-direction: column; gap: 12px; padding-top: 16px; border-top: 0.5px solid var(--dsw-alias-border-l2); }
274
+
275
+ /* --- Card: a raised surface whose edge lives in the shadow. --- */
276
+ & .dsh-ui-card {
277
+ ${officialElevation("stroke")}
278
+ display: flex;
279
+ flex-direction: column;
280
+ gap: 12px;
281
+ padding: 16px;
282
+ border-radius: var(--dsw-radius-lg);
283
+ background: var(--dsw-alias-bg-layer-1);
284
+ color: var(--dsw-alias-label-primary);
285
+ min-width: 0;
286
+ }
287
+ & .dsh-ui-card--flat { border: 0.5px solid var(--dsw-alias-border-l3); box-shadow: none; }
288
+ & .dsh-ui-card--nested { background: var(--dsw-alias-bg-layer-2); }
289
+ & .dsh-ui-card--plain { background: none; }
290
+
291
+ /* --- Field: label, control, help, error. The control itself is an
292
+ * official primitive, so this only owns the vertical rhythm. --- */
293
+ & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
294
+ & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
295
+ & .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }
296
+ & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
297
+ & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
298
+ & .dsh-ui-help p { margin: 0; }
299
+ & .dsh-ui-help p + p { margin-top: 8px; }
300
+ & .dsh-ui-required { color: var(--dsw-alias-state-error-primary); }
301
+ & .dsh-ui-control { width: 100%; min-width: 0; }
302
+ /* An invalid control is marked on the control, not only in the message. */
303
+ & .dsh-ui-control[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
304
+ & .dsh-ui-readonly { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
305
+ & .dsh-ui-readonly > dt { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
306
+ & .dsh-ui-readonly > dd { margin: 0; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
307
+
308
+ /* --- Buttons row. A no-op container: it exists so actions line up on the
309
+ * trailing edge the way every native panel does. --- */
310
+ & .dsh-ui-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 4px; }
311
+ & .dsh-ui-actions-end { justify-content: flex-end; }
312
+
313
+ /* --- Banners. A banner is a left-rail notice, the shape used for warnings,
314
+ * deprecations and blocking instructions. --- */
315
+ & .dsh-ui-banner {
316
+ display: flex;
317
+ flex-direction: column;
318
+ gap: 8px;
319
+ padding: 10px 14px;
320
+ border: 0.5px solid var(--dsw-alias-border-l3);
321
+ border-left: 3px solid var(--dsw-alias-state-warn-primary);
322
+ border-radius: var(--dsw-radius-md);
323
+ background: var(--dsw-alias-bg-layer-1);
324
+ font-size: 13px;
325
+ line-height: 20px;
326
+ color: var(--dsw-alias-label-secondary);
327
+ }
328
+ & .dsh-ui-banner-title { font-weight: 500; color: var(--dsw-alias-state-warn-primary); }
329
+ & .dsh-ui-banner--info { border-left-color: var(--dsw-alias-state-business-primary); }
330
+ & .dsh-ui-banner--danger { border-left-color: var(--dsw-alias-state-error-primary); }
331
+ & .dsh-ui-banner--success { border-left-color: var(--dsw-alias-state-success-primary); }
332
+ & .dsh-ui-banner p { margin: 0; }
333
+
334
+ /* --- Inline notice: a transient confirmation with no chrome of its own. --- */
335
+ & .dsh-ui-notice { display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-success-primary); }
336
+ & .dsh-ui-notice--error { color: var(--dsw-alias-state-error-primary); }
337
+
338
+ /* --- Empty and loading states. Both sit in a dashed well so an empty panel
339
+ * reads as "nothing here yet" rather than as a broken layout. --- */
340
+ & .dsh-ui-empty {
341
+ display: flex;
342
+ flex-direction: column;
343
+ align-items: flex-start;
344
+ gap: 6px;
345
+ padding: 20px 16px;
346
+ border: 0.5px dashed var(--dsw-alias-border-l3);
347
+ border-radius: var(--dsw-radius-md);
348
+ font-size: 13px;
349
+ line-height: 20px;
350
+ color: var(--dsw-alias-label-secondary);
351
+ }
352
+ & .dsh-ui-loading { display: flex; align-items: center; gap: 8px; padding: 20px 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }
353
+
354
+ /* --- Selectable list. A row is a button-sized target with a hover fill and
355
+ * a selected fill; the name carries the weight, the description does not. --- */
356
+ & .dsh-ui-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
357
+ & .dsh-ui-list-scroll { max-height: min(66vh, 760px); overflow-y: auto; overflow-x: hidden; }
358
+ & .dsh-ui-list-row { border-radius: var(--dsw-radius-md); }
359
+ & .dsh-ui-list-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
360
+ & .dsh-ui-list-row[aria-selected='true'], & .dsh-ui-list-row[data-selected] { background: var(--dsw-alias-interactive-bg-active); }
361
+ & .dsh-ui-list-item {
362
+ display: flex;
363
+ flex-direction: column;
364
+ align-items: flex-start;
365
+ gap: 4px;
366
+ width: 100%;
367
+ min-width: 0;
368
+ padding: 10px 12px;
369
+ border: 0;
370
+ border-radius: var(--dsw-radius-md);
371
+ background: none;
372
+ font: inherit;
373
+ color: inherit;
374
+ text-align: start;
375
+ cursor: pointer;
376
+ }
377
+ & .dsh-ui-list-item:disabled { cursor: default; }
378
+ & .dsh-ui-list-item:disabled:hover { background: none; }
379
+ & .dsh-ui-list-name { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }
380
+ & .dsh-ui-list-desc { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
381
+
382
+ /* --- Code and structured output. Wraps rather than scrolls, because a
383
+ * feature panel is read rather than scanned. --- */
384
+ & .dsh-ui-code {
385
+ padding: 10px 12px;
386
+ border: 0.5px solid var(--dsw-alias-border-l3);
387
+ border-radius: var(--dsw-radius-md);
388
+ background: var(--dsw-alias-bg-layer-1);
389
+ font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
390
+ font-size: 12px;
391
+ line-height: 20px;
392
+ color: var(--dsw-alias-label-secondary);
393
+ overflow-wrap: anywhere;
394
+ white-space: pre-wrap;
395
+ }
396
+ & .dsh-ui-scroll { max-height: 60vh; overflow: auto; overscroll-behavior: contain; }
397
+
398
+ /* --- A toggle row: label on the left, switch on the trailing edge. This is
399
+ * layout only; the switch itself is the official primitive. --- */
400
+ & .dsh-ui-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-width: 0; padding: 12px 0; }
401
+ & .dsh-ui-toggle-row + .dsh-ui-toggle-row { border-top: 0.5px solid var(--dsw-alias-border-l2); }
402
+ & .dsh-ui-toggle-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
403
+ & .dsh-ui-toggle-label { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
404
+
405
+ /* --- Floating surface: a panel anchored to a trigger. This is the material
406
+ * a menu card is made of — the translucent fill plus its backdrop blur, with
407
+ * the edge riding inside the prominent elevation. A plugin that hand-rolls this
408
+ * is how tokens like --dsw-specific-menu get invented; use this instead. --- */
409
+ & .dsh-ui-surface {
410
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
411
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
412
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
413
+ position: relative;
414
+ display: flex;
415
+ flex-direction: column;
416
+ border: 0;
417
+ border-radius: var(--dsw-radius-lg);
418
+ background: var(--dsw-menu-surface-fill);
419
+ backdrop-filter: var(--dsw-menu-backdrop-filter);
420
+ box-shadow: var(--dsw-elevation-prominent);
421
+ color: var(--dsw-alias-label-primary);
422
+ font-size: 13px;
423
+ line-height: 20px;
424
+ min-width: 0;
425
+ }
426
+ & .dsh-ui-surface-body { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px 14px; overflow-y: auto; }
427
+ & .dsh-ui-surface-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
428
+ & .dsh-ui-surface-title { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
429
+
430
+ /* --- Native select. The primitives ship no select, so a plugin that offers a
431
+ * closed list of choices keeps the platform control and matches the official
432
+ * settings field geometry exactly (34px, 0.5px l4, radius-md, layer-3). A
433
+ * SegmentedControl is the primitive to reach for when the choices are few
434
+ * enough to show at once. --- */
435
+ & .dsh-ui-select {
436
+ box-sizing: border-box;
437
+ width: 100%;
438
+ min-width: 0;
439
+ height: 34px;
440
+ padding: 0 12px;
441
+ border: 0.5px solid var(--dsw-alias-border-l4);
442
+ border-radius: var(--dsw-radius-md);
443
+ background: var(--dsw-alias-bg-layer-3);
444
+ font: inherit;
445
+ font-size: 13px;
446
+ line-height: 1.5;
447
+ color: var(--dsw-alias-label-primary);
448
+ cursor: pointer;
449
+ }
450
+ & .dsh-ui-select:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
451
+ & .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
452
+ & .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
453
+
454
+ /* Long panels scroll inside themselves instead of growing the host page. */
455
+ & .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
456
+ & .dsh-ui-panel-wide { max-width: 100%; }
457
+
458
+ @media (prefers-reduced-motion: reduce) {
459
+ & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
460
+ }
461
+ }
462
+ `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
463
+ }
464
+ //#endregion
6
465
  //#region src/contracts.ts
466
+ const PLUGIN_NAME = "@klarkxy/dsh-git-commit";
7
467
  const RPC_CHANNEL = "/dsh-git-commit";
8
468
  const GIT_COMMIT_CLIENT_SERVICE = "dshGitCommitClient";
9
469
  //#endregion
@@ -12,6 +472,8 @@ const name = "dsh-git-commit-client";
12
472
  const inject = [
13
473
  "slots",
14
474
  "connection",
475
+ "remote",
476
+ "remote.session",
15
477
  "locale"
16
478
  ];
17
479
  const NS = "dsh-git-commit.action";
@@ -22,7 +484,12 @@ const zh = {
22
484
  files: "个文件",
23
485
  model: "模型",
24
486
  modelDefault: "默认模型",
25
- modelCustom: "AI 服务配置",
487
+ modelCustom: "插件设置",
488
+ filesCount: "({n} 个文件)",
489
+ confirmPrompt: "将把 {n} 个文件的全部修改分次提交到当前分支。",
490
+ confirm: "确认提交",
491
+ cancel: "取消",
492
+ loading: "正在读取仓库状态…",
26
493
  running: "正在整理修改并分次提交…",
27
494
  done: "已创建提交",
28
495
  fallback: "模型不可用,已合并为单个提交",
@@ -35,6 +502,7 @@ const zh = {
35
502
  sessionRunning: "会话正在运行,提交已暂停",
36
503
  refresh: "刷新",
37
504
  failed: "操作失败",
505
+ planUnavailable: "模型没有给出可用的提交方案,已取消本次提交,工作区没有被改动。换一个模型重试,或在插件设置里开启兜底提交。",
38
506
  close: "关闭"
39
507
  };
40
508
  const en = {
@@ -44,7 +512,12 @@ const en = {
44
512
  files: "files",
45
513
  model: "Model",
46
514
  modelDefault: "default model",
47
- modelCustom: "AI services route",
515
+ modelCustom: "Plugin settings",
516
+ filesCount: " ({n} files)",
517
+ confirmPrompt: "All changes in {n} files will be committed to the current branch in one or more commits.",
518
+ confirm: "Commit now",
519
+ cancel: "Cancel",
520
+ loading: "Reading repository status…",
48
521
  running: "Organizing changes into commits…",
49
522
  done: "Commits created",
50
523
  fallback: "Model unavailable; used a single commit",
@@ -57,11 +530,24 @@ const en = {
57
530
  sessionRunning: "Session is running; commits paused",
58
531
  refresh: "Refresh",
59
532
  failed: "Operation failed",
533
+ planUnavailable: "The model returned no usable commit plan, so the run was cancelled and your workspace is untouched. Try another model, or turn on the fallback in plugin settings.",
60
534
  close: "Close"
61
535
  };
62
- async function rpc(call, endpoint, payload) {
536
+ /** A failed RPC keeps its code so the panel can explain a rejection in its own words. */
537
+ var RpcError = class extends Error {
538
+ code;
539
+ constructor(code, message) {
540
+ super(message);
541
+ this.name = "RpcError";
542
+ this.code = code;
543
+ }
544
+ };
545
+ async function rpc(call, endpoint, payload = {}, fallback = "Operation failed") {
63
546
  const result = await call(endpoint, payload);
64
- if (!result || result.ok !== true) throw new Error(result && "error" in result ? result.error.message : "request failed");
547
+ if (!result || result.ok !== true) {
548
+ const failed = result && "error" in result ? result.error : void 0;
549
+ throw new RpcError(failed?.code ?? "unknown", failed?.message ? failed.message : fallback);
550
+ }
65
551
  return result.value;
66
552
  }
67
553
  function reasonText(t, status) {
@@ -73,6 +559,19 @@ function reasonText(t, status) {
73
559
  default: return t("unavailable");
74
560
  }
75
561
  }
562
+ /** A refusal the plugin can name gets its own sentence; the rest stay verbatim. */
563
+ function errorText(t, error) {
564
+ return error.code === "plan-unavailable" ? t("planUnavailable") : error.message;
565
+ }
566
+ function panelError(reason) {
567
+ return reason instanceof RpcError ? {
568
+ code: reason.code,
569
+ message: reason.message
570
+ } : {
571
+ code: "unknown",
572
+ message: reason instanceof Error ? reason.message : String(reason)
573
+ };
574
+ }
76
575
  function GitBranchIcon() {
77
576
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
78
577
  width: "14",
@@ -122,6 +621,7 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
122
621
  const [running, setRunning] = (0, react.useState)(false);
123
622
  const [result, setResult] = (0, react.useState)(null);
124
623
  const [error, setError] = (0, react.useState)(null);
624
+ const [confirming, setConfirming] = (0, react.useState)(false);
125
625
  const rootRef = (0, react.useRef)(null);
126
626
  const triggerRef = (0, react.useRef)(null);
127
627
  const panelRef = (0, react.useRef)(null);
@@ -136,11 +636,16 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
136
636
  const sessionRunning = useSession((snapshot) => snapshot.running === true);
137
637
  const changeOpen = (0, react.useCallback)((next) => {
138
638
  setOpen(next);
639
+ setConfirming(false);
139
640
  if (next) {
140
641
  setResult(null);
141
642
  setError(null);
142
643
  }
143
644
  }, []);
645
+ const positioned = Boolean(position);
646
+ (0, react.useEffect)(() => {
647
+ if (open && positioned) panelRef.current?.focus();
648
+ }, [open, positioned]);
144
649
  (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, changeOpen, panelRef);
145
650
  (0, react.useEffect)(() => {
146
651
  generationRef.current += 1;
@@ -155,17 +660,21 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
155
660
  setLoading(true);
156
661
  setError(null);
157
662
  try {
158
- const next = await rpc(call, "status", { sessionId });
663
+ const next = await rpc(call, "status", { sessionId }, t("failed"));
159
664
  if (generationRef.current !== generation) return;
160
665
  setStatus(next);
161
666
  } catch (reason) {
162
667
  if (generationRef.current !== generation) return;
163
668
  setStatus(void 0);
164
- setError(reason instanceof Error ? reason.message : String(reason));
669
+ setError(panelError(reason));
165
670
  } finally {
166
671
  if (generationRef.current === generation) setLoading(false);
167
672
  }
168
- }, [call, sessionId]);
673
+ }, [
674
+ call,
675
+ sessionId,
676
+ t
677
+ ]);
169
678
  (0, react.useEffect)(() => {
170
679
  if (!open) return;
171
680
  loadStatus();
@@ -183,11 +692,12 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
183
692
  }, [open, changeOpen]);
184
693
  const runCommit = (0, react.useCallback)(async () => {
185
694
  const generation = generationRef.current;
695
+ setConfirming(false);
186
696
  setRunning(true);
187
697
  setResult(null);
188
698
  setError(null);
189
699
  try {
190
- const value = await rpc(call, "commit", { sessionId });
700
+ const value = await rpc(call, "commit", { sessionId }, t("failed"));
191
701
  if (generationRef.current !== generation) return;
192
702
  setResult(value);
193
703
  const next = await rpc(call, "status", { sessionId }).catch(() => void 0);
@@ -195,176 +705,423 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
195
705
  if (next) setStatus(next);
196
706
  } catch (reason) {
197
707
  if (generationRef.current !== generation) return;
198
- setError(reason instanceof Error ? reason.message : String(reason));
708
+ setError(panelError(reason));
199
709
  } finally {
200
710
  if (generationRef.current === generation) setRunning(false);
201
711
  }
202
- }, [call, sessionId]);
712
+ }, [
713
+ call,
714
+ sessionId,
715
+ t
716
+ ]);
203
717
  const available = status?.available === true;
204
718
  const workspaceCount = status?.workspace.files ?? 0;
205
719
  const canCommit = available && !running && !sessionRunning && workspaceCount > 0;
206
- const modelLabel = status?.model ? `${status.model.provider}/${status.model.model} · ${status.model.source === "purpose" || status.model.source === "override" ? t("modelCustom") : t("modelDefault")}` : void 0;
207
720
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
208
721
  ref: rootRef,
209
722
  className: "gcm-root",
210
- children: [
211
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css }),
212
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
213
- label: t("title"),
214
- side: "bottom",
215
- gap: 4,
216
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
217
- type: "button",
218
- ref: triggerRef,
219
- className: "gcm-trigger",
220
- "aria-label": t("title"),
221
- "aria-haspopup": "dialog",
222
- "aria-expanded": open,
223
- onClick: () => changeOpen(!open),
224
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitBranchIcon, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
225
- className: "gcm-triggerLabel",
226
- children: t("action")
227
- })]
228
- })
229
- }),
230
- open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
231
- ref: panelRef,
232
- className: "gcm-panel",
233
- style: position ?? {
234
- visibility: "hidden",
235
- left: 0,
236
- top: 0
237
- },
238
- role: "dialog",
239
- tabIndex: -1,
723
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
724
+ label: t("title"),
725
+ side: "bottom",
726
+ gap: 4,
727
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
728
+ ref: triggerRef,
729
+ variant: "ghost",
730
+ size: "sm",
731
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitBranchIcon, {}),
240
732
  "aria-label": t("title"),
241
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
242
- className: "gcm-body",
243
- children: [
244
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
245
- className: "gcm-header",
733
+ "aria-haspopup": "dialog",
734
+ "aria-expanded": open,
735
+ onClick: () => changeOpen(!open),
736
+ children: t("action")
737
+ })
738
+ }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
739
+ ref: panelRef,
740
+ className: "gcm-root dsh-ui-surface gcm-panel",
741
+ style: position ?? {
742
+ visibility: "hidden",
743
+ left: 0,
744
+ top: 0
745
+ },
746
+ role: "dialog",
747
+ tabIndex: -1,
748
+ "aria-label": t("title"),
749
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
750
+ className: "dsh-ui-surface-body",
751
+ children: [
752
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
753
+ className: "dsh-ui-surface-head",
754
+ children: [
755
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
756
+ className: "dsh-ui-surface-title",
757
+ children: t("title")
758
+ }),
759
+ status?.branch ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
760
+ className: "dsh-ui-hint dsh-ui-truncate",
761
+ children: status.branch
762
+ }) : null,
763
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-ui-spacer" }),
764
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
765
+ label: t("refresh"),
766
+ side: "bottom",
767
+ gap: 4,
768
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
769
+ variant: "ghost",
770
+ size: "sm",
771
+ "aria-label": t("refresh"),
772
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, { size: 14 }),
773
+ disabled: loading || running,
774
+ onClick: () => void loadStatus()
775
+ })
776
+ })
777
+ ]
778
+ }),
779
+ loading && !status && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
780
+ className: "dsh-ui-hint",
781
+ role: "status",
782
+ children: t("loading")
783
+ }),
784
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
785
+ className: "dsh-ui-error",
786
+ role: "alert",
787
+ children: errorText(t, error)
788
+ }),
789
+ status && !available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
790
+ className: "dsh-ui-meta",
791
+ role: "status",
792
+ children: reasonText(t, status)
793
+ }),
794
+ available && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
795
+ status?.model ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
796
+ className: "gcm-model",
246
797
  children: [
247
798
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
248
- className: "gcm-title",
249
- children: t("title")
799
+ className: "dsh-ui-hint",
800
+ children: t("model")
250
801
  }),
251
- status?.branch ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
252
- className: "gcm-branch",
253
- children: status.branch
254
- }) : null,
255
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
256
- type: "button",
257
- className: "gcm-iconButton",
258
- "aria-label": t("refresh"),
259
- disabled: loading || running,
260
- onClick: () => void loadStatus(),
261
- children: "⟳"
802
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
803
+ className: "dsh-ui-truncate",
804
+ children: [
805
+ status.model.provider,
806
+ "/",
807
+ status.model.model
808
+ ]
809
+ }),
810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
811
+ tone: status.model.source === "page" ? "info" : "neutral",
812
+ children: status.model.source === "page" ? t("modelCustom") : t("modelDefault")
262
813
  })
263
814
  ]
815
+ }) : null,
816
+ confirming && canCommit ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
817
+ className: "dsh-ui-stack",
818
+ role: "group",
819
+ "aria-label": t("commitAction"),
820
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
821
+ className: "dsh-ui-hint dsh-ui-wrap",
822
+ children: t("confirmPrompt").replace("{n}", String(workspaceCount))
823
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
824
+ className: "dsh-ui-actions",
825
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
826
+ variant: "ghost",
827
+ size: "md",
828
+ onClick: () => setConfirming(false),
829
+ children: t("cancel")
830
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
831
+ variant: "primary",
832
+ size: "md",
833
+ onClick: () => void runCommit(),
834
+ children: t("confirm")
835
+ })]
836
+ })]
837
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Button, {
838
+ variant: "primary",
839
+ size: "md",
840
+ disabled: !canCommit,
841
+ onClick: () => setConfirming(true),
842
+ children: [t("commitAction"), t("filesCount").replace("{n}", String(workspaceCount))]
264
843
  }),
265
- error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
266
- className: "gcm-error",
267
- role: "alert",
268
- children: error
269
- }),
270
- status && !available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
271
- className: "gcm-notice",
272
- role: "status",
273
- children: reasonText(t, status)
844
+ workspaceCount === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
845
+ className: "dsh-ui-hint",
846
+ children: t("noWorkspace")
274
847
  }),
275
- available && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
276
- modelLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
277
- className: "gcm-meta",
278
- children: [
279
- t("model"),
280
- ": ",
281
- modelLabel
282
- ]
283
- }) : null,
284
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
285
- type: "button",
286
- className: "gcm-action gcm-primary",
287
- disabled: !canCommit,
288
- onClick: () => void runCommit(),
848
+ sessionRunning && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
849
+ className: "dsh-ui-hint",
850
+ children: t("sessionRunning")
851
+ })
852
+ ] }),
853
+ running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
854
+ className: "dsh-ui-hint",
855
+ role: "status",
856
+ children: t("running")
857
+ }),
858
+ result && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
859
+ className: "dsh-ui-stack",
860
+ children: [
861
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
862
+ className: "dsh-ui-hint dsh-ui-wrap",
289
863
  children: [
290
- t("commitAction"),
291
- "(",
292
- workspaceCount,
293
- " ",
294
- t("files"),
295
- ")"
864
+ t("done"),
865
+ " · ",
866
+ result.branch
296
867
  ]
297
868
  }),
298
- workspaceCount === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
299
- className: "gcm-notice",
300
- children: t("noWorkspace")
869
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
870
+ className: "gcm-commits",
871
+ children: result.commits.map((commit) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
872
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: commit.hash }),
873
+ " ",
874
+ commit.message
875
+ ] }, commit.hash || commit.message))
301
876
  }),
302
- sessionRunning && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
303
- className: "gcm-notice",
304
- children: t("sessionRunning")
877
+ result.fallback && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
878
+ className: "dsh-ui-hint",
879
+ children: t("fallback")
305
880
  })
306
- ] }),
307
- running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
308
- className: "gcm-notice",
309
- role: "status",
310
- children: t("running")
311
- }),
312
- result && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
313
- className: "gcm-result",
314
- children: [
315
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
316
- className: "gcm-meta",
317
- children: [
318
- t("done"),
319
- " · ",
320
- result.branch
321
- ]
322
- }),
323
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
324
- className: "gcm-commits",
325
- children: result.commits.map((commit) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
326
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: commit.hash }),
327
- " ",
328
- commit.message
329
- ] }, commit.hash || commit.message))
330
- }),
331
- result.fallback && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
332
- className: "gcm-notice",
333
- children: t("fallback")
334
- })
335
- ]
336
- })
337
- ]
881
+ ]
882
+ })
883
+ ]
884
+ })
885
+ }), document.body)]
886
+ });
887
+ }
888
+ const clientCss = `${officialUiCss("gcm-root")}
889
+ /* Only the floating panel's geometry is this plugin's own: the material, the
890
+ * rounded edge and the scrollbar tokens all come from the shared contract.
891
+ *
892
+ * The root class is spelled out because the panel is portaled and *is* the
893
+ * root: the dsh-ui-surface recipe therefore reaches it as a compound
894
+ * .gcm-root.dsh-ui-surface selector and declares position:relative at the
895
+ * weight a bare .gcm-panel rule also carries. Left unqualified that recipe
896
+ * wins, and the anchored left/top degrade into offsets from the panel's static
897
+ * spot at the end of body — the popover then renders under the page instead
898
+ * of at the trigger. Naming both classes puts this rule at the same weight,
899
+ * later in the sheet, so it wins. */
900
+ .gcm-root.gcm-panel { position: fixed; width: min(360px, calc(100vw - 32px)); max-height: min(560px, calc(100vh - 32px)); overflow: auto; }
901
+ .gcm-model { display: flex; align-items: center; gap: 6px; min-width: 0; margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-primary); }
902
+ .gcm-commits { margin: 0; padding-inline-start: 18px; display: grid; gap: 2px; font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
903
+ .gcm-commits code { font-size: 12px; background: var(--dsw-alias-bg-layer-2); padding: 0 4px; border-radius: var(--dsw-radius-sm); }
904
+ .gcm-settings { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
905
+ .gcm-settings-status { display: flex; flex-direction: column; gap: 4px; }
906
+ `;
907
+ /** Host UI language, following live switches; falls back to zh when the service lacks a snapshot. */
908
+ function useHostLocale(client) {
909
+ const subscribe = (0, react.useCallback)((listener) => client.locale.subscribe?.(listener) ?? (() => {}), [client]);
910
+ const snapshot = (0, react.useCallback)(() => client.locale.getSnapshot?.().active ?? "zh", [client]);
911
+ return (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot);
912
+ }
913
+ const settingsZh = {
914
+ pageTitle: "Git 提交",
915
+ model: "提交规划模型",
916
+ modelDefault: "默认模型",
917
+ modelEffort: "思考强度",
918
+ modelHint: "留空则使用当前会话模型,再回落到宿主默认对话模型。",
919
+ fallback: "模型不可用时仍然提交",
920
+ fallbackHint: "默认关闭:模型规划失败会取消本次提交,工作区不变。开启后全部改动会合并成一个提交,由插件代写提交信息。",
921
+ loading: "正在读取设置…",
922
+ saved: "已保存。",
923
+ stale: "设置已更新,请刷新后重试。",
924
+ failed: "无法读取提交设置。",
925
+ retry: "重试"
926
+ };
927
+ const settingsEn = {
928
+ pageTitle: "Git Commit",
929
+ model: "Commit planning model",
930
+ modelDefault: "Default model",
931
+ modelEffort: "Reasoning effort",
932
+ modelHint: "Leave empty to use the current session model, then the host default chat model.",
933
+ fallback: "Commit even without a model plan",
934
+ fallbackHint: "Off by default: a failed plan cancels the run and leaves the workspace untouched. When on, every change lands in one commit with a plugin-written message.",
935
+ loading: "Loading settings…",
936
+ saved: "Saved.",
937
+ stale: "Settings changed; refresh and retry.",
938
+ failed: "Could not load commit settings.",
939
+ retry: "Retry"
940
+ };
941
+ function settingsCopy(locale) {
942
+ return String(locale).startsWith("zh") ? settingsZh : settingsEn;
943
+ }
944
+ /** Plugin-page settings row: pick the model this plugin's planning call uses. */
945
+ function GitCommitSettingsPanel(props) {
946
+ const hostLocale = useHostLocale(props.client);
947
+ const text = settingsCopy(props.locale ?? hostLocale);
948
+ const [settings, setSettings] = (0, react.useState)(void 0);
949
+ const [choices, setChoices] = (0, react.useState)([]);
950
+ const [error, setError] = (0, react.useState)("");
951
+ const [note, setNote] = (0, react.useState)("");
952
+ const [busy, setBusy] = (0, react.useState)(false);
953
+ const call = (0, react.useCallback)((endpoint, payload = {}) => props.client.connection.rpc.call(RPC_CHANNEL, endpoint, payload), [props.client]);
954
+ (0, react.useEffect)(() => {
955
+ let live = true;
956
+ rpc(call, "settings", {}, text.failed).then((next) => {
957
+ if (live) {
958
+ setSettings(next);
959
+ setError("");
960
+ }
961
+ }).catch(() => {
962
+ if (live) setError(text.failed);
963
+ });
964
+ return () => {
965
+ live = false;
966
+ };
967
+ }, [call, text.failed]);
968
+ const boundProvider = settings?.model.provider ?? "";
969
+ const boundModel = settings?.model.model ?? "";
970
+ (0, react.useEffect)(() => {
971
+ let live = true;
972
+ const bound = boundProvider && boundModel ? {
973
+ provider: boundProvider,
974
+ model: boundModel
975
+ } : void 0;
976
+ props.client.remote?.session?.modelCatalog?.()?.then((value) => {
977
+ if (live) setChoices(parseModelMenuChoices(value, bound));
978
+ }).catch(() => {
979
+ if (live) setChoices([]);
980
+ });
981
+ return () => {
982
+ live = false;
983
+ };
984
+ }, [
985
+ props.client,
986
+ boundProvider,
987
+ boundModel
988
+ ]);
989
+ const save = (0, react.useCallback)((patch) => {
990
+ if (!settings) return;
991
+ setBusy(true);
992
+ setNote("");
993
+ setError("");
994
+ rpc(call, "settings.update", {
995
+ settings: {
996
+ model: patch.model ?? settings.model,
997
+ allowFallback: patch.allowFallback ?? settings.allowFallback
998
+ },
999
+ expectedRevision: settings.revision
1000
+ }, text.stale).then((next) => {
1001
+ setSettings(next);
1002
+ setNote(text.saved);
1003
+ setBusy(false);
1004
+ }).catch(() => {
1005
+ setError(text.stale);
1006
+ setBusy(false);
1007
+ });
1008
+ }, [
1009
+ call,
1010
+ settings,
1011
+ text.saved,
1012
+ text.stale
1013
+ ]);
1014
+ if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
1015
+ className: "gcm-root gcm-settings",
1016
+ children: error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1017
+ role: "alert",
1018
+ className: "dsh-ui-error",
1019
+ children: error
1020
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1021
+ role: "status",
1022
+ className: "dsh-ui-hint",
1023
+ children: text.loading
1024
+ })
1025
+ });
1026
+ const current = settings.model;
1027
+ const selected = choices.find((item) => item.provider === current.provider && item.model === current.model);
1028
+ const efforts = modelMenuEffortOptions(selected, current.reasoningEffort);
1029
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1030
+ className: "gcm-root gcm-settings",
1031
+ "data-testid": "git-commit-settings",
1032
+ children: [
1033
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1034
+ className: "gcm-settings-status",
1035
+ children: [error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1036
+ role: "alert",
1037
+ className: "dsh-ui-error",
1038
+ children: error
1039
+ }), note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1040
+ role: "status",
1041
+ className: "dsh-ui-notice",
1042
+ children: note
1043
+ })]
1044
+ }),
1045
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1046
+ className: "dsh-ui-field",
1047
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1048
+ className: "dsh-ui-label",
1049
+ children: text.model
1050
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1051
+ className: "dsh-ui-select",
1052
+ value: modelMenuChoiceKey(current.provider, current.model),
1053
+ disabled: busy,
1054
+ onChange: (event) => {
1055
+ const key = event.target.value;
1056
+ if (!key) {
1057
+ save({ model: {
1058
+ provider: "",
1059
+ model: ""
1060
+ } });
1061
+ return;
1062
+ }
1063
+ const parsed = parseModelMenuChoiceKey(key);
1064
+ if (parsed) save({ model: parsed });
1065
+ },
1066
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1067
+ value: "",
1068
+ children: text.modelDefault
1069
+ }), choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1070
+ value: modelMenuChoiceKey(choice.provider, choice.model),
1071
+ children: choice.label
1072
+ }, modelMenuChoiceKey(choice.provider, choice.model)))]
1073
+ })]
1074
+ }),
1075
+ selected && efforts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1076
+ className: "dsh-ui-field",
1077
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1078
+ className: "dsh-ui-label",
1079
+ children: text.modelEffort
1080
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1081
+ className: "dsh-ui-select",
1082
+ value: current.reasoningEffort ?? "",
1083
+ disabled: busy,
1084
+ onChange: (event) => save({ model: {
1085
+ ...current,
1086
+ reasoningEffort: event.target.value || void 0
1087
+ } }),
1088
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1089
+ value: "",
1090
+ children: text.modelDefault
1091
+ }), efforts.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1092
+ value: effort.id,
1093
+ children: effort.name
1094
+ }, effort.id))]
1095
+ })]
1096
+ }),
1097
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1098
+ className: "dsh-ui-meta",
1099
+ children: text.modelHint
1100
+ }),
1101
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("article", {
1102
+ className: "gcm-root dsh-ui-card",
1103
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1104
+ className: "dsh-ui-toggle-row",
1105
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1106
+ className: "dsh-ui-toggle-text",
1107
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1108
+ className: "dsh-ui-toggle-label",
1109
+ children: text.fallback
1110
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1111
+ className: "dsh-ui-hint",
1112
+ children: text.fallbackHint
1113
+ })]
1114
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1115
+ checked: settings.allowFallback,
1116
+ label: text.fallback,
1117
+ disabled: busy,
1118
+ onChange: (next) => save({ allowFallback: next })
1119
+ })]
338
1120
  })
339
- }), document.body)
1121
+ })
340
1122
  ]
341
1123
  });
342
1124
  }
343
- const css = `
344
- .gcm-root{position:relative;display:inline-flex}
345
- .gcm-trigger{min-height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:none;border:0;border-radius:6px;align-items:center;gap:5px;padding:3px 7px;font-size:12px;display:inline-flex}
346
- .gcm-trigger:hover,.gcm-trigger:focus-visible{color:var(--dsw-alias-label-primary)}
347
- .gcm-triggerLabel{white-space:nowrap}
348
- .gcm-panel{--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);z-index:100;box-sizing:border-box;width:min(360px,100vw - 32px);max-height:min(560px,100vh - 32px);box-shadow:var(--dsw-elevation-prominent);border:0;border-radius:12px;display:flex;flex-direction:column;position:fixed;overflow:hidden;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;font-family:var(--default-font-family,system-ui,sans-serif)}
349
- .gcm-panel::before{content:"";z-index:-1;background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);border-radius:12px;position:absolute;inset:0}
350
- .gcm-body{display:grid;gap:8px;padding:12px 14px 14px;overflow-y:auto}
351
- .gcm-header{display:flex;align-items:center;gap:8px}
352
- .gcm-title{font-weight:600}
353
- .gcm-branch{color:var(--dsw-alias-label-caption);overflow-wrap:anywhere;font-size:12px}
354
- .gcm-iconButton{margin-left:auto;border:0;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;padding:2px 6px;border-radius:6px}
355
- .gcm-iconButton:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
356
- .gcm-iconButton:disabled{opacity:.45;cursor:not-allowed}
357
- .gcm-action{--dsw-elevation-stroke-color:var(--dsw-alias-border-l2);display:block;width:100%;text-align:left;padding:7px 10px;border:0;box-shadow:var(--dsw-elevation-stroke);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer}
358
- .gcm-action:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}
359
- .gcm-action:disabled{opacity:.45;cursor:not-allowed}
360
- .gcm-primary{--dsw-elevation-stroke-color:var(--dsw-alias-brand-primary)}
361
- .gcm-meta{margin:0;font-size:12px;color:var(--dsw-alias-label-caption);overflow-wrap:anywhere}
362
- .gcm-notice{margin:0;font-size:12px;color:var(--dsw-alias-label-secondary)}
363
- .gcm-error{margin:0;font-size:12px;color:var(--dsw-alias-state-error-primary)}
364
- .gcm-result{display:grid;gap:4px}
365
- .gcm-commits{margin:0;padding-left:18px;display:grid;gap:2px;font-size:12px;overflow-wrap:anywhere}
366
- .gcm-commits code{font-size:12px;background:var(--dsw-alias-bg-layer-2);padding:0 4px;border-radius:4px}
367
- `;
368
1125
  function apply(ctx) {
369
1126
  const client = ctx;
370
1127
  ctx.provide(GIT_COMMIT_CLIENT_SERVICE, { active: true });
@@ -372,6 +1129,18 @@ function apply(ctx) {
372
1129
  zh: { ...zh },
373
1130
  en: { ...en }
374
1131
  }), "git-commit.locale");
1132
+ ctx.effect(() => {
1133
+ if (typeof document === "undefined") return () => {};
1134
+ const style = document.createElement("style");
1135
+ style.setAttribute("data-plugin", name);
1136
+ style.textContent = clientCss;
1137
+ document.head.appendChild(style);
1138
+ return () => style.remove();
1139
+ }, "git-commit.styles");
1140
+ ctx.effect(() => client.slots.inject("plugins.bundle.config", () => client.slots.register({
1141
+ name: "plugins.bundle.config",
1142
+ key: PLUGIN_NAME
1143
+ }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitCommitSettingsPanel, { client }))), "git-commit.settings");
375
1144
  ctx.effect(() => client.slots.inject("conversation.session.header.actions", () => client.slots.register({
376
1145
  name: "conversation.session.header.actions",
377
1146
  id: "git-commit",
@@ -382,7 +1151,9 @@ function apply(ctx) {
382
1151
  }
383
1152
  //#endregion
384
1153
  exports.GitCommitAction = GitCommitAction;
1154
+ exports.GitCommitSettingsPanel = GitCommitSettingsPanel;
385
1155
  exports.apply = apply;
1156
+ exports.clientCss = clientCss;
386
1157
  exports.inject = inject;
387
1158
  exports.name = name;
388
1159