@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.
package/lib/client.js CHANGED
@@ -170,7 +170,467 @@ let react = require("react");
170
170
  let react_dom = require("react-dom");
171
171
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
172
172
  let react_jsx_runtime = require("react/jsx-runtime");
173
+ //#region ../dsh-plugin-kit/src/model-menu.ts
174
+ /**
175
+ * Shared plugin-page model menu.
176
+ *
177
+ * A feature plugin's own settings row exposes a model select. An empty
178
+ * selection means the host default chat model for that feature's own call.
179
+ *
180
+ * Browser-safe and React-optional: pure helpers live here so hosts and clients
181
+ * share one catalog contract without bundling React into the host build.
182
+ */
183
+ const KEY_SEPARATOR = "";
184
+ function modelMenuChoiceKey(provider, model) {
185
+ return provider && model ? `${provider}${KEY_SEPARATOR}${model}` : "";
186
+ }
187
+ function parseModelMenuChoiceKey(value) {
188
+ const index = value.indexOf(KEY_SEPARATOR);
189
+ if (index <= 0) return void 0;
190
+ const provider = value.slice(0, index);
191
+ const model = value.slice(index + 1);
192
+ if (!provider || !model) return void 0;
193
+ return {
194
+ provider,
195
+ model
196
+ };
197
+ }
198
+ function asRecord(value) {
199
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
200
+ }
201
+ function asText(value) {
202
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
203
+ }
204
+ function parseEffort(value) {
205
+ const row = asRecord(value);
206
+ const id = row ? asText(row.id) : void 0;
207
+ if (!id) return void 0;
208
+ return {
209
+ id,
210
+ name: row ? asText(row.name) ?? id : id
211
+ };
212
+ }
213
+ /** Parse the host session catalog into flat choices, keeping a saved route selectable. */
214
+ function parseModelMenuChoices(value, bound) {
215
+ const envelope = asRecord(value);
216
+ const root = (envelope && envelope.ok === true ? asRecord(envelope.value) : void 0) ?? envelope ?? {};
217
+ const groups = Array.isArray(root.groups) ? root.groups : [];
218
+ const choices = [];
219
+ for (const group of groups) {
220
+ const groupRow = asRecord(group);
221
+ const provider = groupRow ? asText(groupRow.id) : void 0;
222
+ if (!provider || !groupRow || !Array.isArray(groupRow.models)) continue;
223
+ const providerName = asText(groupRow.name) ?? provider;
224
+ for (const entry of groupRow.models) {
225
+ const modelRow = asRecord(entry);
226
+ const model = modelRow ? asText(modelRow.id) : void 0;
227
+ if (!model || !modelRow) continue;
228
+ const name = asText(modelRow.name) ?? model;
229
+ const reasoning = asRecord(modelRow.reasoning);
230
+ const efforts = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts.flatMap((effort) => {
231
+ const parsed = parseEffort(effort);
232
+ return parsed ? [parsed] : [];
233
+ }) : [];
234
+ choices.push({
235
+ provider,
236
+ model,
237
+ label: `${providerName} / ${name}`,
238
+ efforts
239
+ });
240
+ }
241
+ }
242
+ if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
243
+ provider: bound.provider,
244
+ model: bound.model,
245
+ label: `${bound.provider} / ${bound.model}`,
246
+ efforts: []
247
+ });
248
+ return choices;
249
+ }
250
+ /** Reasoning-effort options for one choice; the saved value stays selectable. */
251
+ function modelMenuEffortOptions(choice, current) {
252
+ const listed = (choice?.efforts ?? []).map((item) => ({ ...item }));
253
+ if (current && !listed.some((item) => item.id === current)) listed.push({
254
+ id: current,
255
+ name: current
256
+ });
257
+ return listed;
258
+ }
259
+ Object.values({
260
+ /** Surfaces, from the page up to the topmost nested card. */
261
+ surface: [
262
+ "--dsw-alias-bg-base",
263
+ "--dsw-alias-bg-layer-1",
264
+ "--dsw-alias-bg-layer-2",
265
+ "--dsw-alias-bg-layer-3",
266
+ "--dsw-alias-bg-layer-4",
267
+ "--dsw-alias-bg-module-platform",
268
+ "--dsw-alias-bg-overlay"
269
+ ],
270
+ /** Hairlines. l1 is near-invisible on a menu surface, so menus use l2+. */
271
+ border: [
272
+ "--dsw-alias-border-l1",
273
+ "--dsw-alias-border-l2",
274
+ "--dsw-alias-border-l3",
275
+ "--dsw-alias-border-l4"
276
+ ],
277
+ /** Text. `caption` is menu shortcut text, `dimmed` is placeholder text. */
278
+ label: [
279
+ "--dsw-alias-label-primary",
280
+ "--dsw-alias-label-secondary",
281
+ "--dsw-alias-label-tertiary",
282
+ "--dsw-alias-label-caption",
283
+ "--dsw-alias-label-dimmed",
284
+ "--dsw-alias-label-error",
285
+ "--dsw-alias-label-primary-foreground"
286
+ ],
287
+ /** Interactive fills. The `-danger` hover belongs to destructive rows. */
288
+ interactive: [
289
+ "--dsw-alias-interactive-bg-hover",
290
+ "--dsw-alias-interactive-bg-active",
291
+ "--dsw-alias-interactive-bg-hover-danger"
292
+ ],
293
+ /** Accent and status. `business` is the focus ring's default colour. */
294
+ accent: [
295
+ "--dsw-alias-brand-primary",
296
+ "--dsw-alias-button-primary-fill",
297
+ "--dsw-alias-button-primary-hover",
298
+ "--dsw-alias-button-tool-bar-fill",
299
+ "--dsw-alias-button-tool-bar-hover",
300
+ "--dsw-alias-button-ghost-active-fill",
301
+ "--dsw-alias-button-ghost-active-border"
302
+ ],
303
+ state: [
304
+ "--dsw-alias-state-business-primary",
305
+ "--dsw-alias-state-success-primary",
306
+ "--dsw-alias-state-warn-primary",
307
+ "--dsw-alias-state-error-primary",
308
+ "--dsw-alias-state-idle-primary"
309
+ ],
310
+ /** Corner scale: sm 8, md 12, lg the large card, panel the modal. */
311
+ radius: [
312
+ "--dsw-radius-sm",
313
+ "--dsw-radius-md",
314
+ "--dsw-radius-lg",
315
+ "--dsw-radius-panel"
316
+ ],
317
+ /** An elevated surface draws its edge in the shadow, so it keeps border: 0. */
318
+ elevation: [
319
+ "--dsw-elevation-soft",
320
+ "--dsw-elevation-stroke",
321
+ "--dsw-elevation-stroke-color",
322
+ "--dsw-elevation-panel",
323
+ "--dsw-elevation-prominent"
324
+ ],
325
+ /** Floats, masks and scrollbars. */
326
+ overlay: [
327
+ "--dsw-menu-surface-fill",
328
+ "--dsw-menu-backdrop-filter",
329
+ "--dsw-mask-blur",
330
+ "--dsw-alias-bg-mask-1",
331
+ "--dsw-alias-menu-icon",
332
+ "--dsw-alias-scrollbar-bg-l2",
333
+ "--dsw-alias-scrollbar-hover-l2",
334
+ "--dsw-shadow-lv3"
335
+ ],
336
+ /** Motion, focus and type axis, published by the host rather than by a component. */
337
+ system: [
338
+ "--dsw-focus-ring-width",
339
+ "--dsw-focus-ring-color",
340
+ "--dsw-font-markdown-code-font-family",
341
+ "--ds-transition-duration",
342
+ "--ds-ease-in-out",
343
+ "--ds-font-family-code",
344
+ "--dsh-scrollbar-thumb",
345
+ "--dsh-scrollbar-thumb-hover",
346
+ "--dsh-content-font-size-secondary",
347
+ "--dsh-content-font-delta",
348
+ "--dsh-frame-top-clearance"
349
+ ]
350
+ }).flat();
351
+ /**
352
+ * The host's focus ring shorthand, verbatim. Writing this per control is how
353
+ * rings drift apart; a stylesheet that draws its own focus style should use
354
+ * {@link officialFocus} instead. The colour fallback is the host's own default.
355
+ */
356
+ const OFFICIAL_FOCUS_RING = "var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))";
357
+ /**
358
+ * A complete `outline` declaration at the official geometry. `offset` defaults
359
+ * to the 2px the native controls use; the host's inline field rules sit at 1px
360
+ * and a segmented tab pulls the ring inside itself at -2px.
361
+ */
362
+ function officialFocus(offset = "2px") {
363
+ return `outline: ${OFFICIAL_FOCUS_RING}; outline-offset: ${offset}`;
364
+ }
365
+ /**
366
+ * An elevated surface. The host draws a raised surface's edge inside its
367
+ * shadow rather than as a border, so a card sets `border: 0` and takes the edge
368
+ * from the elevation — which is also why its border stays off the alias-border
369
+ * set that a flat, non-elevated rule would use.
370
+ */
371
+ function officialElevation(level) {
372
+ return `border: 0; box-shadow: var(--dsw-elevation-${level})`;
373
+ }
374
+ /**
375
+ * The shared stylesheet for a plugin UI, scoped under that plugin's root class.
376
+ *
377
+ * Scoping is what keeps this safe to always-bundle into every client: the rules
378
+ * can only match inside the plugin's own subtree, so two plugins can both mount
379
+ * `dsh-ui-card` without either styling the other's markup.
380
+ *
381
+ * Each recipe matches the root element *and* its descendants, because the
382
+ * commonest arrangement is one element that is both — a surface that carries
383
+ * the root class and `dsh-ui-card`, or a portaled dialog that is the root and
384
+ * the `dsh-ui-surface`. A descendant-only scope would silently leave those
385
+ * unstyled, with no error anywhere.
386
+ *
387
+ * A caveat worth knowing: `Modal` and `Menu` portal to `document.body`, so their
388
+ * content leaves the plugin's subtree entirely. A portaled surface therefore
389
+ * needs its own root class on an element inside the portal.
390
+ *
391
+ * Only patterns that primitives do not provide live here. If a control appears
392
+ * below that `Button`/`Input`/`Switch`/`Tag`/`Menu` already covers, that is a
393
+ * bug in this file: use the primitive and delete the recipe.
394
+ */
395
+ function officialUiCss(roots) {
396
+ const scope = (typeof roots === "string" ? [roots] : roots).map((root) => `.${root}`).join(", ");
397
+ return `
398
+ /* Official DSH Web UI contract. Generated by @klarkxy/dsh-plugin-kit/official-ui.
399
+ * Do not hand-edit the copy inside a plugin: change this module so every plugin
400
+ * moves together. Scope: ${scope} */
401
+ /* The scope rule below stays open for the whole sheet: every rule after it
402
+ * nests with the parent selector, so one stylesheet can cover several unrelated
403
+ * roots without leaking into a neighbouring plugin. Its closing brace is last. */
404
+ ${scope} {
405
+ color: var(--dsw-alias-label-primary);
406
+ font-family: inherit;
407
+ font-size: 14px;
408
+ line-height: 22px;
409
+ min-width: 0;
410
+
411
+ & *, & *::before, & *::after { box-sizing: border-box; }
412
+ & h1, & h2, & h3, & h4, & p, & figure { margin: 0; }
413
+ /* Keyboard focus only: pointer input keeps the native resting appearance. The
414
+ * ring needs no radius of its own — the outline already follows the control's
415
+ * own border-radius, and overriding it here would flatten every control. */
416
+ & :focus-visible { ${officialFocus()}; }
417
+ & [hidden] { display: none !important; }
418
+
419
+ /* --- Type tiers. The same 14/22 and 13/20 steps the primitives use. --- */
420
+ & .dsh-ui-title { font-size: 16px; line-height: 24px; font-weight: 500; color: var(--dsw-alias-label-primary); }
421
+ & .dsh-ui-heading { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
422
+ & .dsh-ui-compact { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary); }
423
+ & .dsh-ui-meta { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
424
+ & .dsh-ui-hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
425
+ & .dsh-ui-error { font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); }
426
+ & .dsh-ui-warn { color: var(--dsw-alias-state-warn-primary); }
427
+ & .dsh-ui-success { color: var(--dsw-alias-state-success-primary); }
428
+ & .dsh-ui-muted { color: var(--dsw-alias-label-tertiary); }
429
+ & .dsh-ui-truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
430
+ & .dsh-ui-wrap { min-width: 0; overflow-wrap: anywhere; }
431
+
432
+ /* --- Layout scaffolding. --- */
433
+ & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
434
+ & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
435
+ & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
436
+ & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
437
+ & .dsh-ui-spacer { flex: 1; min-width: 0; }
438
+ /* A 0.5px l2 rule is the host's section divider; reserve the same rhythm. */
439
+ & .dsh-ui-divider { height: 0.5px; background: var(--dsw-alias-border-l2); }
440
+ & .dsh-ui-section { display: flex; flex-direction: column; gap: 12px; padding-top: 16px; border-top: 0.5px solid var(--dsw-alias-border-l2); }
441
+
442
+ /* --- Card: a raised surface whose edge lives in the shadow. --- */
443
+ & .dsh-ui-card {
444
+ ${officialElevation("stroke")}
445
+ display: flex;
446
+ flex-direction: column;
447
+ gap: 12px;
448
+ padding: 16px;
449
+ border-radius: var(--dsw-radius-lg);
450
+ background: var(--dsw-alias-bg-layer-1);
451
+ color: var(--dsw-alias-label-primary);
452
+ min-width: 0;
453
+ }
454
+ & .dsh-ui-card--flat { border: 0.5px solid var(--dsw-alias-border-l3); box-shadow: none; }
455
+ & .dsh-ui-card--nested { background: var(--dsw-alias-bg-layer-2); }
456
+ & .dsh-ui-card--plain { background: none; }
457
+
458
+ /* --- Field: label, control, help, error. The control itself is an
459
+ * official primitive, so this only owns the vertical rhythm. --- */
460
+ & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
461
+ & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
462
+ & .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); }
463
+ & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
464
+ & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
465
+ & .dsh-ui-help p { margin: 0; }
466
+ & .dsh-ui-help p + p { margin-top: 8px; }
467
+ & .dsh-ui-required { color: var(--dsw-alias-state-error-primary); }
468
+ & .dsh-ui-control { width: 100%; min-width: 0; }
469
+ /* An invalid control is marked on the control, not only in the message. */
470
+ & .dsh-ui-control[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
471
+ & .dsh-ui-readonly { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
472
+ & .dsh-ui-readonly > dt { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
473
+ & .dsh-ui-readonly > dd { margin: 0; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
474
+
475
+ /* --- Buttons row. A no-op container: it exists so actions line up on the
476
+ * trailing edge the way every native panel does. --- */
477
+ & .dsh-ui-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 4px; }
478
+ & .dsh-ui-actions-end { justify-content: flex-end; }
479
+
480
+ /* --- Banners. A banner is a left-rail notice, the shape used for warnings,
481
+ * deprecations and blocking instructions. --- */
482
+ & .dsh-ui-banner {
483
+ display: flex;
484
+ flex-direction: column;
485
+ gap: 8px;
486
+ padding: 10px 14px;
487
+ border: 0.5px solid var(--dsw-alias-border-l3);
488
+ border-left: 3px solid var(--dsw-alias-state-warn-primary);
489
+ border-radius: var(--dsw-radius-md);
490
+ background: var(--dsw-alias-bg-layer-1);
491
+ font-size: 13px;
492
+ line-height: 20px;
493
+ color: var(--dsw-alias-label-secondary);
494
+ }
495
+ & .dsh-ui-banner-title { font-weight: 500; color: var(--dsw-alias-state-warn-primary); }
496
+ & .dsh-ui-banner--info { border-left-color: var(--dsw-alias-state-business-primary); }
497
+ & .dsh-ui-banner--danger { border-left-color: var(--dsw-alias-state-error-primary); }
498
+ & .dsh-ui-banner--success { border-left-color: var(--dsw-alias-state-success-primary); }
499
+ & .dsh-ui-banner p { margin: 0; }
500
+
501
+ /* --- Inline notice: a transient confirmation with no chrome of its own. --- */
502
+ & .dsh-ui-notice { display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-success-primary); }
503
+ & .dsh-ui-notice--error { color: var(--dsw-alias-state-error-primary); }
504
+
505
+ /* --- Empty and loading states. Both sit in a dashed well so an empty panel
506
+ * reads as "nothing here yet" rather than as a broken layout. --- */
507
+ & .dsh-ui-empty {
508
+ display: flex;
509
+ flex-direction: column;
510
+ align-items: flex-start;
511
+ gap: 6px;
512
+ padding: 20px 16px;
513
+ border: 0.5px dashed var(--dsw-alias-border-l3);
514
+ border-radius: var(--dsw-radius-md);
515
+ font-size: 13px;
516
+ line-height: 20px;
517
+ color: var(--dsw-alias-label-secondary);
518
+ }
519
+ & .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); }
520
+
521
+ /* --- Selectable list. A row is a button-sized target with a hover fill and
522
+ * a selected fill; the name carries the weight, the description does not. --- */
523
+ & .dsh-ui-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
524
+ & .dsh-ui-list-scroll { max-height: min(66vh, 760px); overflow-y: auto; overflow-x: hidden; }
525
+ & .dsh-ui-list-row { border-radius: var(--dsw-radius-md); }
526
+ & .dsh-ui-list-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
527
+ & .dsh-ui-list-row[aria-selected='true'], & .dsh-ui-list-row[data-selected] { background: var(--dsw-alias-interactive-bg-active); }
528
+ & .dsh-ui-list-item {
529
+ display: flex;
530
+ flex-direction: column;
531
+ align-items: flex-start;
532
+ gap: 4px;
533
+ width: 100%;
534
+ min-width: 0;
535
+ padding: 10px 12px;
536
+ border: 0;
537
+ border-radius: var(--dsw-radius-md);
538
+ background: none;
539
+ font: inherit;
540
+ color: inherit;
541
+ text-align: start;
542
+ cursor: pointer;
543
+ }
544
+ & .dsh-ui-list-item:disabled { cursor: default; }
545
+ & .dsh-ui-list-item:disabled:hover { background: none; }
546
+ & .dsh-ui-list-name { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }
547
+ & .dsh-ui-list-desc { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
548
+
549
+ /* --- Code and structured output. Wraps rather than scrolls, because a
550
+ * feature panel is read rather than scanned. --- */
551
+ & .dsh-ui-code {
552
+ padding: 10px 12px;
553
+ border: 0.5px solid var(--dsw-alias-border-l3);
554
+ border-radius: var(--dsw-radius-md);
555
+ background: var(--dsw-alias-bg-layer-1);
556
+ font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
557
+ font-size: 12px;
558
+ line-height: 20px;
559
+ color: var(--dsw-alias-label-secondary);
560
+ overflow-wrap: anywhere;
561
+ white-space: pre-wrap;
562
+ }
563
+ & .dsh-ui-scroll { max-height: 60vh; overflow: auto; overscroll-behavior: contain; }
564
+
565
+ /* --- A toggle row: label on the left, switch on the trailing edge. This is
566
+ * layout only; the switch itself is the official primitive. --- */
567
+ & .dsh-ui-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-width: 0; padding: 12px 0; }
568
+ & .dsh-ui-toggle-row + .dsh-ui-toggle-row { border-top: 0.5px solid var(--dsw-alias-border-l2); }
569
+ & .dsh-ui-toggle-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
570
+ & .dsh-ui-toggle-label { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
571
+
572
+ /* --- Floating surface: a panel anchored to a trigger. This is the material
573
+ * a menu card is made of — the translucent fill plus its backdrop blur, with
574
+ * the edge riding inside the prominent elevation. A plugin that hand-rolls this
575
+ * is how tokens like --dsw-specific-menu get invented; use this instead. --- */
576
+ & .dsh-ui-surface {
577
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
578
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
579
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
580
+ position: relative;
581
+ display: flex;
582
+ flex-direction: column;
583
+ border: 0;
584
+ border-radius: var(--dsw-radius-lg);
585
+ background: var(--dsw-menu-surface-fill);
586
+ backdrop-filter: var(--dsw-menu-backdrop-filter);
587
+ box-shadow: var(--dsw-elevation-prominent);
588
+ color: var(--dsw-alias-label-primary);
589
+ font-size: 13px;
590
+ line-height: 20px;
591
+ min-width: 0;
592
+ }
593
+ & .dsh-ui-surface-body { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px 14px; overflow-y: auto; }
594
+ & .dsh-ui-surface-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
595
+ & .dsh-ui-surface-title { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
596
+
597
+ /* --- Native select. The primitives ship no select, so a plugin that offers a
598
+ * closed list of choices keeps the platform control and matches the official
599
+ * settings field geometry exactly (34px, 0.5px l4, radius-md, layer-3). A
600
+ * SegmentedControl is the primitive to reach for when the choices are few
601
+ * enough to show at once. --- */
602
+ & .dsh-ui-select {
603
+ box-sizing: border-box;
604
+ width: 100%;
605
+ min-width: 0;
606
+ height: 34px;
607
+ padding: 0 12px;
608
+ border: 0.5px solid var(--dsw-alias-border-l4);
609
+ border-radius: var(--dsw-radius-md);
610
+ background: var(--dsw-alias-bg-layer-3);
611
+ font: inherit;
612
+ font-size: 13px;
613
+ line-height: 1.5;
614
+ color: var(--dsw-alias-label-primary);
615
+ cursor: pointer;
616
+ }
617
+ & .dsh-ui-select:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
618
+ & .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
619
+ & .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
620
+
621
+ /* Long panels scroll inside themselves instead of growing the host page. */
622
+ & .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
623
+ & .dsh-ui-panel-wide { max-width: 100%; }
624
+
625
+ @media (prefers-reduced-motion: reduce) {
626
+ & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
627
+ }
628
+ }
629
+ `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
630
+ }
631
+ //#endregion
173
632
  //#region src/contracts.ts
633
+ const PLUGIN_NAME = "@klarkxy/dsh-git-commit";
174
634
  const RPC_CHANNEL = "/dsh-git-commit";
175
635
  const GIT_COMMIT_CLIENT_SERVICE = "dshGitCommitClient";
176
636
  //#endregion
@@ -179,6 +639,8 @@ const name = "dsh-git-commit-client";
179
639
  const inject = [
180
640
  "slots",
181
641
  "connection",
642
+ "remote",
643
+ "remote.session",
182
644
  "locale"
183
645
  ];
184
646
  const NS = "dsh-git-commit.action";
@@ -189,7 +651,12 @@ const zh = {
189
651
  files: "个文件",
190
652
  model: "模型",
191
653
  modelDefault: "默认模型",
192
- modelCustom: "AI 服务配置",
654
+ modelCustom: "插件设置",
655
+ filesCount: "({n} 个文件)",
656
+ confirmPrompt: "将把 {n} 个文件的全部修改分次提交到当前分支。",
657
+ confirm: "确认提交",
658
+ cancel: "取消",
659
+ loading: "正在读取仓库状态…",
193
660
  running: "正在整理修改并分次提交…",
194
661
  done: "已创建提交",
195
662
  fallback: "模型不可用,已合并为单个提交",
@@ -202,6 +669,7 @@ const zh = {
202
669
  sessionRunning: "会话正在运行,提交已暂停",
203
670
  refresh: "刷新",
204
671
  failed: "操作失败",
672
+ planUnavailable: "模型没有给出可用的提交方案,已取消本次提交,工作区没有被改动。换一个模型重试,或在插件设置里开启兜底提交。",
205
673
  close: "关闭"
206
674
  };
207
675
  const en = {
@@ -211,7 +679,12 @@ const en = {
211
679
  files: "files",
212
680
  model: "Model",
213
681
  modelDefault: "default model",
214
- modelCustom: "AI services route",
682
+ modelCustom: "Plugin settings",
683
+ filesCount: " ({n} files)",
684
+ confirmPrompt: "All changes in {n} files will be committed to the current branch in one or more commits.",
685
+ confirm: "Commit now",
686
+ cancel: "Cancel",
687
+ loading: "Reading repository status…",
215
688
  running: "Organizing changes into commits…",
216
689
  done: "Commits created",
217
690
  fallback: "Model unavailable; used a single commit",
@@ -224,11 +697,24 @@ const en = {
224
697
  sessionRunning: "Session is running; commits paused",
225
698
  refresh: "Refresh",
226
699
  failed: "Operation failed",
700
+ 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.",
227
701
  close: "Close"
228
702
  };
229
- async function rpc(call, endpoint, payload) {
703
+ /** A failed RPC keeps its code so the panel can explain a rejection in its own words. */
704
+ var RpcError = class extends Error {
705
+ code;
706
+ constructor(code, message) {
707
+ super(message);
708
+ this.name = "RpcError";
709
+ this.code = code;
710
+ }
711
+ };
712
+ async function rpc(call, endpoint, payload = {}, fallback = "Operation failed") {
230
713
  const result = await call(endpoint, payload);
231
- if (!result || result.ok !== true) throw new Error(result && "error" in result ? result.error.message : "request failed");
714
+ if (!result || result.ok !== true) {
715
+ const failed = result && "error" in result ? result.error : void 0;
716
+ throw new RpcError(failed?.code ?? "unknown", failed?.message ? failed.message : fallback);
717
+ }
232
718
  return result.value;
233
719
  }
234
720
  function reasonText(t, status) {
@@ -240,6 +726,19 @@ function reasonText(t, status) {
240
726
  default: return t("unavailable");
241
727
  }
242
728
  }
729
+ /** A refusal the plugin can name gets its own sentence; the rest stay verbatim. */
730
+ function errorText(t, error) {
731
+ return error.code === "plan-unavailable" ? t("planUnavailable") : error.message;
732
+ }
733
+ function panelError(reason) {
734
+ return reason instanceof RpcError ? {
735
+ code: reason.code,
736
+ message: reason.message
737
+ } : {
738
+ code: "unknown",
739
+ message: reason instanceof Error ? reason.message : String(reason)
740
+ };
741
+ }
243
742
  function GitBranchIcon() {
244
743
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
245
744
  width: "14",
@@ -289,6 +788,7 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
289
788
  const [running, setRunning] = (0, react.useState)(false);
290
789
  const [result, setResult] = (0, react.useState)(null);
291
790
  const [error, setError] = (0, react.useState)(null);
791
+ const [confirming, setConfirming] = (0, react.useState)(false);
292
792
  const rootRef = (0, react.useRef)(null);
293
793
  const triggerRef = (0, react.useRef)(null);
294
794
  const panelRef = (0, react.useRef)(null);
@@ -303,11 +803,16 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
303
803
  const sessionRunning = useSession((snapshot) => snapshot.running === true);
304
804
  const changeOpen = (0, react.useCallback)((next) => {
305
805
  setOpen(next);
806
+ setConfirming(false);
306
807
  if (next) {
307
808
  setResult(null);
308
809
  setError(null);
309
810
  }
310
811
  }, []);
812
+ const positioned = Boolean(position);
813
+ (0, react.useEffect)(() => {
814
+ if (open && positioned) panelRef.current?.focus();
815
+ }, [open, positioned]);
311
816
  (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, changeOpen, panelRef);
312
817
  (0, react.useEffect)(() => {
313
818
  generationRef.current += 1;
@@ -322,17 +827,21 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
322
827
  setLoading(true);
323
828
  setError(null);
324
829
  try {
325
- const next = await rpc(call, "status", { sessionId });
830
+ const next = await rpc(call, "status", { sessionId }, t("failed"));
326
831
  if (generationRef.current !== generation) return;
327
832
  setStatus(next);
328
833
  } catch (reason) {
329
834
  if (generationRef.current !== generation) return;
330
835
  setStatus(void 0);
331
- setError(reason instanceof Error ? reason.message : String(reason));
836
+ setError(panelError(reason));
332
837
  } finally {
333
838
  if (generationRef.current === generation) setLoading(false);
334
839
  }
335
- }, [call, sessionId]);
840
+ }, [
841
+ call,
842
+ sessionId,
843
+ t
844
+ ]);
336
845
  (0, react.useEffect)(() => {
337
846
  if (!open) return;
338
847
  loadStatus();
@@ -350,11 +859,12 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
350
859
  }, [open, changeOpen]);
351
860
  const runCommit = (0, react.useCallback)(async () => {
352
861
  const generation = generationRef.current;
862
+ setConfirming(false);
353
863
  setRunning(true);
354
864
  setResult(null);
355
865
  setError(null);
356
866
  try {
357
- const value = await rpc(call, "commit", { sessionId });
867
+ const value = await rpc(call, "commit", { sessionId }, t("failed"));
358
868
  if (generationRef.current !== generation) return;
359
869
  setResult(value);
360
870
  const next = await rpc(call, "status", { sessionId }).catch(() => void 0);
@@ -362,176 +872,423 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
362
872
  if (next) setStatus(next);
363
873
  } catch (reason) {
364
874
  if (generationRef.current !== generation) return;
365
- setError(reason instanceof Error ? reason.message : String(reason));
875
+ setError(panelError(reason));
366
876
  } finally {
367
877
  if (generationRef.current === generation) setRunning(false);
368
878
  }
369
- }, [call, sessionId]);
879
+ }, [
880
+ call,
881
+ sessionId,
882
+ t
883
+ ]);
370
884
  const available = status?.available === true;
371
885
  const workspaceCount = status?.workspace.files ?? 0;
372
886
  const canCommit = available && !running && !sessionRunning && workspaceCount > 0;
373
- const modelLabel = status?.model ? `${status.model.provider}/${status.model.model} · ${status.model.source === "purpose" || status.model.source === "override" ? t("modelCustom") : t("modelDefault")}` : void 0;
374
887
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
375
888
  ref: rootRef,
376
889
  className: "gcm-root",
377
- children: [
378
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css }),
379
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
380
- label: t("title"),
381
- side: "bottom",
382
- gap: 4,
383
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
384
- type: "button",
385
- ref: triggerRef,
386
- className: "gcm-trigger",
387
- "aria-label": t("title"),
388
- "aria-haspopup": "dialog",
389
- "aria-expanded": open,
390
- onClick: () => changeOpen(!open),
391
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitBranchIcon, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
392
- className: "gcm-triggerLabel",
393
- children: t("action")
394
- })]
395
- })
396
- }),
397
- open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
398
- ref: panelRef,
399
- className: "gcm-panel",
400
- style: position ?? {
401
- visibility: "hidden",
402
- left: 0,
403
- top: 0
404
- },
405
- role: "dialog",
406
- tabIndex: -1,
890
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
891
+ label: t("title"),
892
+ side: "bottom",
893
+ gap: 4,
894
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
895
+ ref: triggerRef,
896
+ variant: "ghost",
897
+ size: "sm",
898
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitBranchIcon, {}),
407
899
  "aria-label": t("title"),
408
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
409
- className: "gcm-body",
410
- children: [
411
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
412
- className: "gcm-header",
900
+ "aria-haspopup": "dialog",
901
+ "aria-expanded": open,
902
+ onClick: () => changeOpen(!open),
903
+ children: t("action")
904
+ })
905
+ }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
906
+ ref: panelRef,
907
+ className: "gcm-root dsh-ui-surface gcm-panel",
908
+ style: position ?? {
909
+ visibility: "hidden",
910
+ left: 0,
911
+ top: 0
912
+ },
913
+ role: "dialog",
914
+ tabIndex: -1,
915
+ "aria-label": t("title"),
916
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
917
+ className: "dsh-ui-surface-body",
918
+ children: [
919
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
920
+ className: "dsh-ui-surface-head",
921
+ children: [
922
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
923
+ className: "dsh-ui-surface-title",
924
+ children: t("title")
925
+ }),
926
+ status?.branch ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
927
+ className: "dsh-ui-hint dsh-ui-truncate",
928
+ children: status.branch
929
+ }) : null,
930
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-ui-spacer" }),
931
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
932
+ label: t("refresh"),
933
+ side: "bottom",
934
+ gap: 4,
935
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
936
+ variant: "ghost",
937
+ size: "sm",
938
+ "aria-label": t("refresh"),
939
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, { size: 14 }),
940
+ disabled: loading || running,
941
+ onClick: () => void loadStatus()
942
+ })
943
+ })
944
+ ]
945
+ }),
946
+ loading && !status && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
947
+ className: "dsh-ui-hint",
948
+ role: "status",
949
+ children: t("loading")
950
+ }),
951
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
952
+ className: "dsh-ui-error",
953
+ role: "alert",
954
+ children: errorText(t, error)
955
+ }),
956
+ status && !available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
957
+ className: "dsh-ui-meta",
958
+ role: "status",
959
+ children: reasonText(t, status)
960
+ }),
961
+ available && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
962
+ status?.model ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
963
+ className: "gcm-model",
413
964
  children: [
414
965
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
415
- className: "gcm-title",
416
- children: t("title")
966
+ className: "dsh-ui-hint",
967
+ children: t("model")
417
968
  }),
418
- status?.branch ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
419
- className: "gcm-branch",
420
- children: status.branch
421
- }) : null,
422
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
423
- type: "button",
424
- className: "gcm-iconButton",
425
- "aria-label": t("refresh"),
426
- disabled: loading || running,
427
- onClick: () => void loadStatus(),
428
- children: "⟳"
969
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
970
+ className: "dsh-ui-truncate",
971
+ children: [
972
+ status.model.provider,
973
+ "/",
974
+ status.model.model
975
+ ]
976
+ }),
977
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
978
+ tone: status.model.source === "page" ? "info" : "neutral",
979
+ children: status.model.source === "page" ? t("modelCustom") : t("modelDefault")
429
980
  })
430
981
  ]
982
+ }) : null,
983
+ confirming && canCommit ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
984
+ className: "dsh-ui-stack",
985
+ role: "group",
986
+ "aria-label": t("commitAction"),
987
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
988
+ className: "dsh-ui-hint dsh-ui-wrap",
989
+ children: t("confirmPrompt").replace("{n}", String(workspaceCount))
990
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
991
+ className: "dsh-ui-actions",
992
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
993
+ variant: "ghost",
994
+ size: "md",
995
+ onClick: () => setConfirming(false),
996
+ children: t("cancel")
997
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
998
+ variant: "primary",
999
+ size: "md",
1000
+ onClick: () => void runCommit(),
1001
+ children: t("confirm")
1002
+ })]
1003
+ })]
1004
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1005
+ variant: "primary",
1006
+ size: "md",
1007
+ disabled: !canCommit,
1008
+ onClick: () => setConfirming(true),
1009
+ children: [t("commitAction"), t("filesCount").replace("{n}", String(workspaceCount))]
431
1010
  }),
432
- error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
433
- className: "gcm-error",
434
- role: "alert",
435
- children: error
436
- }),
437
- status && !available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
438
- className: "gcm-notice",
439
- role: "status",
440
- children: reasonText(t, status)
1011
+ workspaceCount === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1012
+ className: "dsh-ui-hint",
1013
+ children: t("noWorkspace")
441
1014
  }),
442
- available && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
443
- modelLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
444
- className: "gcm-meta",
445
- children: [
446
- t("model"),
447
- ": ",
448
- modelLabel
449
- ]
450
- }) : null,
451
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
452
- type: "button",
453
- className: "gcm-action gcm-primary",
454
- disabled: !canCommit,
455
- onClick: () => void runCommit(),
1015
+ sessionRunning && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1016
+ className: "dsh-ui-hint",
1017
+ children: t("sessionRunning")
1018
+ })
1019
+ ] }),
1020
+ running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1021
+ className: "dsh-ui-hint",
1022
+ role: "status",
1023
+ children: t("running")
1024
+ }),
1025
+ result && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1026
+ className: "dsh-ui-stack",
1027
+ children: [
1028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1029
+ className: "dsh-ui-hint dsh-ui-wrap",
456
1030
  children: [
457
- t("commitAction"),
458
- "(",
459
- workspaceCount,
460
- " ",
461
- t("files"),
462
- ")"
1031
+ t("done"),
1032
+ " · ",
1033
+ result.branch
463
1034
  ]
464
1035
  }),
465
- workspaceCount === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
466
- className: "gcm-notice",
467
- children: t("noWorkspace")
1036
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1037
+ className: "gcm-commits",
1038
+ children: result.commits.map((commit) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
1039
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: commit.hash }),
1040
+ " ",
1041
+ commit.message
1042
+ ] }, commit.hash || commit.message))
468
1043
  }),
469
- sessionRunning && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
470
- className: "gcm-notice",
471
- children: t("sessionRunning")
1044
+ result.fallback && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1045
+ className: "dsh-ui-hint",
1046
+ children: t("fallback")
472
1047
  })
473
- ] }),
474
- running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
475
- className: "gcm-notice",
476
- role: "status",
477
- children: t("running")
478
- }),
479
- result && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
480
- className: "gcm-result",
481
- children: [
482
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
483
- className: "gcm-meta",
484
- children: [
485
- t("done"),
486
- " · ",
487
- result.branch
488
- ]
489
- }),
490
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
491
- className: "gcm-commits",
492
- children: result.commits.map((commit) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
493
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: commit.hash }),
494
- " ",
495
- commit.message
496
- ] }, commit.hash || commit.message))
497
- }),
498
- result.fallback && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
499
- className: "gcm-notice",
500
- children: t("fallback")
501
- })
502
- ]
503
- })
504
- ]
1048
+ ]
1049
+ })
1050
+ ]
1051
+ })
1052
+ }), document.body)]
1053
+ });
1054
+ }
1055
+ const clientCss = `${officialUiCss("gcm-root")}
1056
+ /* Only the floating panel's geometry is this plugin's own: the material, the
1057
+ * rounded edge and the scrollbar tokens all come from the shared contract.
1058
+ *
1059
+ * The root class is spelled out because the panel is portaled and *is* the
1060
+ * root: the dsh-ui-surface recipe therefore reaches it as a compound
1061
+ * .gcm-root.dsh-ui-surface selector and declares position:relative at the
1062
+ * weight a bare .gcm-panel rule also carries. Left unqualified that recipe
1063
+ * wins, and the anchored left/top degrade into offsets from the panel's static
1064
+ * spot at the end of body — the popover then renders under the page instead
1065
+ * of at the trigger. Naming both classes puts this rule at the same weight,
1066
+ * later in the sheet, so it wins. */
1067
+ .gcm-root.gcm-panel { position: fixed; width: min(360px, calc(100vw - 32px)); max-height: min(560px, calc(100vh - 32px)); overflow: auto; }
1068
+ .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); }
1069
+ .gcm-commits { margin: 0; padding-inline-start: 18px; display: grid; gap: 2px; font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
1070
+ .gcm-commits code { font-size: 12px; background: var(--dsw-alias-bg-layer-2); padding: 0 4px; border-radius: var(--dsw-radius-sm); }
1071
+ .gcm-settings { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
1072
+ .gcm-settings-status { display: flex; flex-direction: column; gap: 4px; }
1073
+ `;
1074
+ /** Host UI language, following live switches; falls back to zh when the service lacks a snapshot. */
1075
+ function useHostLocale(client) {
1076
+ const subscribe = (0, react.useCallback)((listener) => client.locale.subscribe?.(listener) ?? (() => {}), [client]);
1077
+ const snapshot = (0, react.useCallback)(() => client.locale.getSnapshot?.().active ?? "zh", [client]);
1078
+ return (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot);
1079
+ }
1080
+ const settingsZh = {
1081
+ pageTitle: "Git 提交",
1082
+ model: "提交规划模型",
1083
+ modelDefault: "默认模型",
1084
+ modelEffort: "思考强度",
1085
+ modelHint: "留空则使用当前会话模型,再回落到宿主默认对话模型。",
1086
+ fallback: "模型不可用时仍然提交",
1087
+ fallbackHint: "默认关闭:模型规划失败会取消本次提交,工作区不变。开启后全部改动会合并成一个提交,由插件代写提交信息。",
1088
+ loading: "正在读取设置…",
1089
+ saved: "已保存。",
1090
+ stale: "设置已更新,请刷新后重试。",
1091
+ failed: "无法读取提交设置。",
1092
+ retry: "重试"
1093
+ };
1094
+ const settingsEn = {
1095
+ pageTitle: "Git Commit",
1096
+ model: "Commit planning model",
1097
+ modelDefault: "Default model",
1098
+ modelEffort: "Reasoning effort",
1099
+ modelHint: "Leave empty to use the current session model, then the host default chat model.",
1100
+ fallback: "Commit even without a model plan",
1101
+ 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.",
1102
+ loading: "Loading settings…",
1103
+ saved: "Saved.",
1104
+ stale: "Settings changed; refresh and retry.",
1105
+ failed: "Could not load commit settings.",
1106
+ retry: "Retry"
1107
+ };
1108
+ function settingsCopy(locale) {
1109
+ return String(locale).startsWith("zh") ? settingsZh : settingsEn;
1110
+ }
1111
+ /** Plugin-page settings row: pick the model this plugin's planning call uses. */
1112
+ function GitCommitSettingsPanel(props) {
1113
+ const hostLocale = useHostLocale(props.client);
1114
+ const text = settingsCopy(props.locale ?? hostLocale);
1115
+ const [settings, setSettings] = (0, react.useState)(void 0);
1116
+ const [choices, setChoices] = (0, react.useState)([]);
1117
+ const [error, setError] = (0, react.useState)("");
1118
+ const [note, setNote] = (0, react.useState)("");
1119
+ const [busy, setBusy] = (0, react.useState)(false);
1120
+ const call = (0, react.useCallback)((endpoint, payload = {}) => props.client.connection.rpc.call(RPC_CHANNEL, endpoint, payload), [props.client]);
1121
+ (0, react.useEffect)(() => {
1122
+ let live = true;
1123
+ rpc(call, "settings", {}, text.failed).then((next) => {
1124
+ if (live) {
1125
+ setSettings(next);
1126
+ setError("");
1127
+ }
1128
+ }).catch(() => {
1129
+ if (live) setError(text.failed);
1130
+ });
1131
+ return () => {
1132
+ live = false;
1133
+ };
1134
+ }, [call, text.failed]);
1135
+ const boundProvider = settings?.model.provider ?? "";
1136
+ const boundModel = settings?.model.model ?? "";
1137
+ (0, react.useEffect)(() => {
1138
+ let live = true;
1139
+ const bound = boundProvider && boundModel ? {
1140
+ provider: boundProvider,
1141
+ model: boundModel
1142
+ } : void 0;
1143
+ props.client.remote?.session?.modelCatalog?.()?.then((value) => {
1144
+ if (live) setChoices(parseModelMenuChoices(value, bound));
1145
+ }).catch(() => {
1146
+ if (live) setChoices([]);
1147
+ });
1148
+ return () => {
1149
+ live = false;
1150
+ };
1151
+ }, [
1152
+ props.client,
1153
+ boundProvider,
1154
+ boundModel
1155
+ ]);
1156
+ const save = (0, react.useCallback)((patch) => {
1157
+ if (!settings) return;
1158
+ setBusy(true);
1159
+ setNote("");
1160
+ setError("");
1161
+ rpc(call, "settings.update", {
1162
+ settings: {
1163
+ model: patch.model ?? settings.model,
1164
+ allowFallback: patch.allowFallback ?? settings.allowFallback
1165
+ },
1166
+ expectedRevision: settings.revision
1167
+ }, text.stale).then((next) => {
1168
+ setSettings(next);
1169
+ setNote(text.saved);
1170
+ setBusy(false);
1171
+ }).catch(() => {
1172
+ setError(text.stale);
1173
+ setBusy(false);
1174
+ });
1175
+ }, [
1176
+ call,
1177
+ settings,
1178
+ text.saved,
1179
+ text.stale
1180
+ ]);
1181
+ if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
1182
+ className: "gcm-root gcm-settings",
1183
+ children: error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1184
+ role: "alert",
1185
+ className: "dsh-ui-error",
1186
+ children: error
1187
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1188
+ role: "status",
1189
+ className: "dsh-ui-hint",
1190
+ children: text.loading
1191
+ })
1192
+ });
1193
+ const current = settings.model;
1194
+ const selected = choices.find((item) => item.provider === current.provider && item.model === current.model);
1195
+ const efforts = modelMenuEffortOptions(selected, current.reasoningEffort);
1196
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1197
+ className: "gcm-root gcm-settings",
1198
+ "data-testid": "git-commit-settings",
1199
+ children: [
1200
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1201
+ className: "gcm-settings-status",
1202
+ children: [error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1203
+ role: "alert",
1204
+ className: "dsh-ui-error",
1205
+ children: error
1206
+ }), note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1207
+ role: "status",
1208
+ className: "dsh-ui-notice",
1209
+ children: note
1210
+ })]
1211
+ }),
1212
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1213
+ className: "dsh-ui-field",
1214
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1215
+ className: "dsh-ui-label",
1216
+ children: text.model
1217
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1218
+ className: "dsh-ui-select",
1219
+ value: modelMenuChoiceKey(current.provider, current.model),
1220
+ disabled: busy,
1221
+ onChange: (event) => {
1222
+ const key = event.target.value;
1223
+ if (!key) {
1224
+ save({ model: {
1225
+ provider: "",
1226
+ model: ""
1227
+ } });
1228
+ return;
1229
+ }
1230
+ const parsed = parseModelMenuChoiceKey(key);
1231
+ if (parsed) save({ model: parsed });
1232
+ },
1233
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1234
+ value: "",
1235
+ children: text.modelDefault
1236
+ }), choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1237
+ value: modelMenuChoiceKey(choice.provider, choice.model),
1238
+ children: choice.label
1239
+ }, modelMenuChoiceKey(choice.provider, choice.model)))]
1240
+ })]
1241
+ }),
1242
+ selected && efforts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1243
+ className: "dsh-ui-field",
1244
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1245
+ className: "dsh-ui-label",
1246
+ children: text.modelEffort
1247
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1248
+ className: "dsh-ui-select",
1249
+ value: current.reasoningEffort ?? "",
1250
+ disabled: busy,
1251
+ onChange: (event) => save({ model: {
1252
+ ...current,
1253
+ reasoningEffort: event.target.value || void 0
1254
+ } }),
1255
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1256
+ value: "",
1257
+ children: text.modelDefault
1258
+ }), efforts.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1259
+ value: effort.id,
1260
+ children: effort.name
1261
+ }, effort.id))]
1262
+ })]
1263
+ }),
1264
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1265
+ className: "dsh-ui-meta",
1266
+ children: text.modelHint
1267
+ }),
1268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("article", {
1269
+ className: "gcm-root dsh-ui-card",
1270
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1271
+ className: "dsh-ui-toggle-row",
1272
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1273
+ className: "dsh-ui-toggle-text",
1274
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1275
+ className: "dsh-ui-toggle-label",
1276
+ children: text.fallback
1277
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1278
+ className: "dsh-ui-hint",
1279
+ children: text.fallbackHint
1280
+ })]
1281
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1282
+ checked: settings.allowFallback,
1283
+ label: text.fallback,
1284
+ disabled: busy,
1285
+ onChange: (next) => save({ allowFallback: next })
1286
+ })]
505
1287
  })
506
- }), document.body)
1288
+ })
507
1289
  ]
508
1290
  });
509
1291
  }
510
- const css = `
511
- .gcm-root{position:relative;display:inline-flex}
512
- .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}
513
- .gcm-trigger:hover,.gcm-trigger:focus-visible{color:var(--dsw-alias-label-primary)}
514
- .gcm-triggerLabel{white-space:nowrap}
515
- .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)}
516
- .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}
517
- .gcm-body{display:grid;gap:8px;padding:12px 14px 14px;overflow-y:auto}
518
- .gcm-header{display:flex;align-items:center;gap:8px}
519
- .gcm-title{font-weight:600}
520
- .gcm-branch{color:var(--dsw-alias-label-caption);overflow-wrap:anywhere;font-size:12px}
521
- .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}
522
- .gcm-iconButton:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
523
- .gcm-iconButton:disabled{opacity:.45;cursor:not-allowed}
524
- .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}
525
- .gcm-action:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}
526
- .gcm-action:disabled{opacity:.45;cursor:not-allowed}
527
- .gcm-primary{--dsw-elevation-stroke-color:var(--dsw-alias-brand-primary)}
528
- .gcm-meta{margin:0;font-size:12px;color:var(--dsw-alias-label-caption);overflow-wrap:anywhere}
529
- .gcm-notice{margin:0;font-size:12px;color:var(--dsw-alias-label-secondary)}
530
- .gcm-error{margin:0;font-size:12px;color:var(--dsw-alias-state-error-primary)}
531
- .gcm-result{display:grid;gap:4px}
532
- .gcm-commits{margin:0;padding-left:18px;display:grid;gap:2px;font-size:12px;overflow-wrap:anywhere}
533
- .gcm-commits code{font-size:12px;background:var(--dsw-alias-bg-layer-2);padding:0 4px;border-radius:4px}
534
- `;
535
1292
  function apply(ctx) {
536
1293
  const client = ctx;
537
1294
  ctx.provide(GIT_COMMIT_CLIENT_SERVICE, { active: true });
@@ -539,6 +1296,18 @@ function apply(ctx) {
539
1296
  zh: { ...zh },
540
1297
  en: { ...en }
541
1298
  }), "git-commit.locale");
1299
+ ctx.effect(() => {
1300
+ if (typeof document === "undefined") return () => {};
1301
+ const style = document.createElement("style");
1302
+ style.setAttribute("data-plugin", name);
1303
+ style.textContent = clientCss;
1304
+ document.head.appendChild(style);
1305
+ return () => style.remove();
1306
+ }, "git-commit.styles");
1307
+ ctx.effect(() => client.slots.inject("plugins.bundle.config", () => client.slots.register({
1308
+ name: "plugins.bundle.config",
1309
+ key: PLUGIN_NAME
1310
+ }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitCommitSettingsPanel, { client }))), "git-commit.settings");
542
1311
  ctx.effect(() => client.slots.inject("conversation.session.header.actions", () => client.slots.register({
543
1312
  name: "conversation.session.header.actions",
544
1313
  id: "git-commit",
@@ -549,7 +1318,9 @@ function apply(ctx) {
549
1318
  }
550
1319
  //#endregion
551
1320
  exports.GitCommitAction = GitCommitAction;
1321
+ exports.GitCommitSettingsPanel = GitCommitSettingsPanel;
552
1322
  exports.apply = apply;
1323
+ exports.clientCss = clientCss;
553
1324
  exports.inject = inject;
554
1325
  exports.name = name;
555
1326