@klarkxy/dsh-git-commit 0.1.6 → 0.2.1

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
@@ -1,168 +1,7 @@
1
- function __createDshNodeShims() {
2
- class EventEmitter {
3
- constructor() {
4
- this._e = Object.create(null)
5
- }
6
- on(type, fn) {
7
- (this._e[type] ||= []).push(fn)
8
- return this
9
- }
10
- addListener(type, fn) {
11
- return this.on(type, fn)
12
- }
13
- once(type, fn) {
14
- const wrap = (...args) => {
15
- this.off(type, wrap)
16
- fn(...args)
17
- }
18
- return this.on(type, wrap)
19
- }
20
- off(type, fn) {
21
- const list = this._e[type]
22
- if (list) this._e[type] = list.filter((item) => item !== fn)
23
- return this
24
- }
25
- removeListener(type, fn) {
26
- return this.off(type, fn)
27
- }
28
- emit(type, ...args) {
29
- const list = this._e[type]
30
- if (!list) return false
31
- for (const fn of list.slice()) fn(...args)
32
- return true
33
- }
34
- removeAllListeners(type) {
35
- if (type) delete this._e[type]
36
- else this._e = Object.create(null)
37
- return this
38
- }
39
- listeners(type) {
40
- return (this._e[type] || []).slice()
41
- }
42
- setMaxListeners() {
43
- return this
44
- }
45
- }
46
-
47
- class Buffer extends Uint8Array {
48
- static from(value, encodingOrOffset, length) {
49
- if (typeof value === 'string') return new Buffer(new TextEncoder().encode(value))
50
- if (value instanceof ArrayBuffer) return new Buffer(new Uint8Array(value, encodingOrOffset, length))
51
- if (ArrayBuffer.isView(value)) return new Buffer(new Uint8Array(value.buffer, value.byteOffset, value.byteLength))
52
- if (Array.isArray(value) || value && typeof value.length === 'number') return new Buffer(Uint8Array.from(value))
53
- return new Buffer(0)
54
- }
55
- static alloc(size, fill) {
56
- const buf = new Buffer(size)
57
- if (fill !== undefined && fill !== 0) {
58
- if (typeof fill === 'string') {
59
- const bytes = new TextEncoder().encode(fill)
60
- for (let i = 0; i < size; i += 1) buf[i] = bytes[i % bytes.length]
61
- } else buf.fill(fill)
62
- }
63
- return buf
64
- }
65
- static allocUnsafe(size) {
66
- return new Buffer(size)
67
- }
68
- static allocUnsafeSlow(size) {
69
- return new Buffer(size)
70
- }
71
- static concat(list, totalLength) {
72
- const length = totalLength ?? list.reduce((sum, item) => sum + item.length, 0)
73
- const out = new Buffer(length)
74
- let offset = 0
75
- for (const item of list) {
76
- out.set(item, offset)
77
- offset += item.length
78
- }
79
- return out
80
- }
81
- static isBuffer(value) {
82
- return value instanceof Buffer
83
- }
84
- static byteLength(value) {
85
- if (typeof value === 'string') return new TextEncoder().encode(value).length
86
- return value?.byteLength ?? value?.length ?? 0
87
- }
88
- static isEncoding(encoding) {
89
- return /^(utf8|utf-8|ascii|latin1|binary|hex|base64|base64url)$/i.test(String(encoding || ''))
90
- }
91
- toString(encoding) {
92
- if (encoding === 'hex') return [...this].map((byte) => byte.toString(16).padStart(2, '0')).join('')
93
- if (encoding === 'base64') {
94
- let binary = ''
95
- this.forEach((byte) => {
96
- binary += String.fromCharCode(byte)
97
- })
98
- return btoa(binary)
99
- }
100
- return new TextDecoder().decode(this)
101
- }
102
- slice(start, end) {
103
- return Buffer.from(super.subarray(start, end))
104
- }
105
- copy(target, targetStart = 0, start = 0, end = this.length) {
106
- const sliced = this.subarray(start, end)
107
- target.set(sliced, targetStart)
108
- return sliced.length
109
- }
110
- write(string, offset = 0) {
111
- const bytes = new TextEncoder().encode(string)
112
- const written = Math.min(bytes.length, this.length - offset)
113
- this.set(bytes.subarray(0, written), offset)
114
- return written
115
- }
116
- }
117
-
118
- function inherits(ctor, superCtor) {
119
- ctor.super_ = superCtor
120
- ctor.prototype = Object.create(superCtor.prototype, {
121
- constructor: { value: ctor, writable: true, configurable: true },
122
- })
123
- }
124
-
125
- if (!globalThis.Buffer) globalThis.Buffer = Buffer
126
-
127
- return {
128
- buffer: { Buffer, SlowBuffer: Buffer, INSPECT_MAX_BYTES: 50, kMaxLength: 0x7fffffff },
129
- stream: {
130
- Stream: EventEmitter,
131
- Readable: EventEmitter,
132
- Writable: EventEmitter,
133
- Duplex: EventEmitter,
134
- Transform: EventEmitter,
135
- PassThrough: EventEmitter,
136
- },
137
- events: { EventEmitter },
138
- util: {
139
- deprecate(fn) {
140
- return fn
141
- },
142
- inherits,
143
- inspect() {
144
- return ''
145
- },
146
- format(value) {
147
- return String(value)
148
- },
149
- types: { isBuffer: Buffer.isBuffer },
150
- },
151
- }
152
- }
153
-
154
1
  window.__ModuleLoader__.load({
155
2
  id: "@klarkxy/dsh-git-commit",
156
3
  factory: (dshRequire) => {
157
- var __nodeShims;
158
- var process = globalThis.process || { env: {}, nextTick: function (fn) { var args = [].slice.call(arguments, 1); queueMicrotask(function () { fn.apply(null, args); }); } };
159
- var require = function (id) {
160
- if (id === 'buffer' || id === 'stream' || id === 'util' || id === 'events') {
161
- if (!__nodeShims) __nodeShims = __createDshNodeShims();
162
- return __nodeShims[id];
163
- }
164
- return dshRequire(id);
165
- };
4
+ var require = dshRequire;
166
5
  var module = { exports: {} };
167
6
  var exports = module.exports;
168
7
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
@@ -170,21 +9,14 @@ let react = require("react");
170
9
  let react_dom = require("react-dom");
171
10
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
172
11
  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
- */
12
+ //#region ../dsh-model-route/src/route.ts
183
13
  const KEY_SEPARATOR = "";
184
- function modelMenuChoiceKey(provider, model) {
14
+ /** Stable key for one route, safe to use as a select-option value. */
15
+ function modelRouteKey(provider, model) {
185
16
  return provider && model ? `${provider}${KEY_SEPARATOR}${model}` : "";
186
17
  }
187
- function parseModelMenuChoiceKey(value) {
18
+ /** Inverse of modelRouteKey; undefined for malformed keys. */
19
+ function parseModelRouteKey(value) {
188
20
  const index = value.indexOf(KEY_SEPARATOR);
189
21
  if (index <= 0) return void 0;
190
22
  const provider = value.slice(0, index);
@@ -195,6 +27,8 @@ function parseModelMenuChoiceKey(value) {
195
27
  model
196
28
  };
197
29
  }
30
+ //#endregion
31
+ //#region ../dsh-model-route/src/catalog.ts
198
32
  function asRecord(value) {
199
33
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
200
34
  }
@@ -553,7 +387,7 @@ ${scope} {
553
387
  border: 0.5px solid var(--dsw-alias-border-l3);
554
388
  border-radius: var(--dsw-radius-md);
555
389
  background: var(--dsw-alias-bg-layer-1);
556
- font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
390
+ font-family: var(--ds-font-family-code);
557
391
  font-size: 12px;
558
392
  line-height: 20px;
559
393
  color: var(--dsw-alias-label-secondary);
@@ -629,6 +463,162 @@ ${scope} {
629
463
  `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
630
464
  }
631
465
  //#endregion
466
+ //#region ../dsh-plugin-kit/src/contracts.ts
467
+ /**
468
+ * Remote contribution for model-settings change notifications.
469
+ * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
470
+ * subscribers degrade silently when that package is not installed.
471
+ * The stream carries invalidations only — `ready` is never a settings snapshot.
472
+ */
473
+ const MODEL_SETTINGS_CHANGES_REMOTE = {
474
+ package: "@klarkxy/dsh-model-hub",
475
+ descriptors: [{
476
+ id: "modelSettingsChanges/watch",
477
+ service: "modelSettingsChangesController",
478
+ namespace: "modelSettingsChanges",
479
+ method: "watch",
480
+ mode: "stream",
481
+ invocation: { kind: "direct" },
482
+ parameters: [],
483
+ cancellation: { parameter: "signal" }
484
+ }]
485
+ };
486
+ //#endregion
487
+ //#region ../dsh-plugin-kit/src/model-settings-client.ts
488
+ /** This stream carries invalidations only; ready is never a settings snapshot. */
489
+ function parseModelSettingsChangeFrame(value) {
490
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid model settings change frame");
491
+ const frame = value;
492
+ if (frame.kind === "ready" && Object.keys(frame).length === 1) return { kind: "ready" };
493
+ if (frame.kind === "changed" && Object.keys(frame).length === 3 && typeof frame.ns === "string" && frame.ns.length > 0 && typeof frame.revision === "number" && Number.isSafeInteger(frame.revision) && frame.revision >= 0) return {
494
+ kind: "changed",
495
+ ns: frame.ns,
496
+ revision: frame.revision
497
+ };
498
+ throw new Error("Invalid model settings change frame");
499
+ }
500
+ MODEL_SETTINGS_CHANGES_REMOTE.package, MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
501
+ ...descriptor,
502
+ result: {
503
+ mode: "strict",
504
+ typeSymbol: "ModelSettings.ChangeFrame",
505
+ create: () => ({ parse: parseModelSettingsChangeFrame })
506
+ }
507
+ }));
508
+ /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
509
+ * A factory shares one logical stream while at least one subscriber remains. */
510
+ function createModelSettingsSubscriber(ctx, namespace) {
511
+ const listeners = /* @__PURE__ */ new Set();
512
+ let injection;
513
+ let current;
514
+ let lifecycle = 0;
515
+ let failureMessage;
516
+ const report = (message) => {
517
+ failureMessage = message;
518
+ for (const listener of [...listeners]) try {
519
+ listener.error?.(message);
520
+ } catch {}
521
+ };
522
+ const invalidate = () => {
523
+ for (const listener of [...listeners]) {
524
+ if (!listeners.has(listener)) continue;
525
+ try {
526
+ listener.change();
527
+ } catch (error) {
528
+ try {
529
+ listener.error?.(error instanceof Error ? error.message : String(error));
530
+ } catch {}
531
+ }
532
+ }
533
+ };
534
+ const start = () => {
535
+ const life = ++lifecycle;
536
+ injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
537
+ if (life !== lifecycle || !listeners.size) return;
538
+ const scope = injected;
539
+ const stream = scope.remote.$stream({
540
+ name: "Model settings sync",
541
+ open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
542
+ ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
543
+ });
544
+ const session = {
545
+ stream,
546
+ active: true
547
+ };
548
+ current = session;
549
+ scope.effect(() => () => {
550
+ session.active = false;
551
+ if (current === session) {
552
+ current = void 0;
553
+ if (life === lifecycle && listeners.size) report("Model settings sync service unavailable");
554
+ }
555
+ return stream.dispose();
556
+ });
557
+ (async () => {
558
+ let generation;
559
+ let ready = false;
560
+ try {
561
+ for await (const item of stream) {
562
+ if (!session.active || life !== lifecycle || !listeners.size) break;
563
+ if (item.signal.aborted) continue;
564
+ if (generation !== item.generation) {
565
+ generation = item.generation;
566
+ ready = false;
567
+ }
568
+ const frame = parseModelSettingsChangeFrame(item.value);
569
+ if (frame.kind === "ready") {
570
+ ready = true;
571
+ failureMessage = void 0;
572
+ item.accept();
573
+ invalidate();
574
+ } else {
575
+ if (!ready) throw new Error("Model settings sync change arrived before ready");
576
+ if (namespace === "*" || frame.ns === namespace) invalidate();
577
+ }
578
+ }
579
+ } catch (error) {
580
+ if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error));
581
+ } finally {
582
+ if (current === session) current = void 0;
583
+ session.active = false;
584
+ await stream.dispose().catch(() => {});
585
+ }
586
+ })();
587
+ });
588
+ queueMicrotask(() => {
589
+ if (life === lifecycle && listeners.size && !current) report("Model settings sync service unavailable");
590
+ });
591
+ };
592
+ return Object.freeze({ subscribe(onChange, onError) {
593
+ const listener = {
594
+ change: onChange,
595
+ error: onError
596
+ };
597
+ listeners.add(listener);
598
+ if (!injection) start();
599
+ else if (failureMessage) queueMicrotask(() => {
600
+ if (listeners.has(listener) && failureMessage) try {
601
+ listener.error?.(failureMessage);
602
+ } catch {}
603
+ });
604
+ return () => {
605
+ if (!listeners.delete(listener) || listeners.size) return;
606
+ lifecycle++;
607
+ if (current) {
608
+ current.active = false;
609
+ current.stream.dispose().catch(() => {});
610
+ }
611
+ current = void 0;
612
+ failureMessage = void 0;
613
+ const previous = injection;
614
+ injection = void 0;
615
+ try {
616
+ Promise.resolve(previous?.dispose()).catch(() => {});
617
+ } catch {}
618
+ };
619
+ } });
620
+ }
621
+ //#endregion
632
622
  //#region src/contracts.ts
633
623
  const PLUGIN_NAME = "@klarkxy/dsh-git-commit";
634
624
  const RPC_CHANNEL = "/dsh-git-commit";
@@ -659,6 +649,12 @@ const zh = {
659
649
  loading: "正在读取仓库状态…",
660
650
  running: "正在整理修改并分次提交…",
661
651
  done: "已创建提交",
652
+ partial: "本次提交未全部完成;以下提交已保留",
653
+ stopped: "本次操作未确认完成",
654
+ uncertain: "以下提交组的结果不确定。请检查 Git 日志和状态,确认后再操作。",
655
+ hashUnavailable: "提交已创建,哈希未读取",
656
+ remaining: "尚未提交:{n} 组",
657
+ cancelled: "提交操作已取消",
662
658
  fallback: "模型不可用,已合并为单个提交",
663
659
  noWorkspace: "工作区没有修改",
664
660
  unavailable: "不可用",
@@ -669,6 +665,12 @@ const zh = {
669
665
  sessionRunning: "会话正在运行,提交已暂停",
670
666
  refresh: "刷新",
671
667
  failed: "操作失败",
668
+ disabled: "插件已停用",
669
+ busy: "已有提交操作正在进行",
670
+ stale: "设置已更新,请刷新后重试",
671
+ workspaceChanged: "工作区或分支已变更,请刷新后重试提交",
672
+ settingsUnavailable: "无法读取提交设置,请重新加载插件后重试",
673
+ invalidRequest: "提交请求无效,请刷新后重试",
672
674
  planUnavailable: "模型没有给出可用的提交方案,已取消本次提交,工作区没有被改动。换一个模型重试,或在插件设置里开启兜底提交。",
673
675
  close: "关闭"
674
676
  };
@@ -687,6 +689,12 @@ const en = {
687
689
  loading: "Reading repository status…",
688
690
  running: "Organizing changes into commits…",
689
691
  done: "Commits created",
692
+ partial: "This run stopped before completion; these commits were kept",
693
+ stopped: "This run did not finish with a confirmed result",
694
+ uncertain: "The outcome of these groups is uncertain. Check Git log and status before taking further action.",
695
+ hashUnavailable: "Commit created; hash unavailable",
696
+ remaining: "Groups not committed: {n}",
697
+ cancelled: "Commit run cancelled",
690
698
  fallback: "Model unavailable; used a single commit",
691
699
  noWorkspace: "No workspace changes",
692
700
  unavailable: "Unavailable",
@@ -697,23 +705,31 @@ const en = {
697
705
  sessionRunning: "Session is running; commits paused",
698
706
  refresh: "Refresh",
699
707
  failed: "Operation failed",
708
+ disabled: "Plugin disabled",
709
+ busy: "A commit run is already in progress",
710
+ stale: "Settings changed; refresh and retry",
711
+ workspaceChanged: "The workspace or branch changed; refresh and retry the commit",
712
+ settingsUnavailable: "Could not read commit settings; reload the plugin and retry",
713
+ invalidRequest: "Invalid commit request; refresh and retry",
700
714
  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.",
701
715
  close: "Close"
702
716
  };
703
717
  /** A failed RPC keeps its code so the panel can explain a rejection in its own words. */
704
718
  var RpcError = class extends Error {
705
719
  code;
706
- constructor(code, message) {
720
+ partial;
721
+ constructor(code, message, partial) {
707
722
  super(message);
708
723
  this.name = "RpcError";
709
724
  this.code = code;
725
+ this.partial = partial;
710
726
  }
711
727
  };
712
- async function rpc(call, endpoint, payload = {}, fallback = "Operation failed") {
728
+ async function rpc(call, endpoint, payload = {}, fallback = "") {
713
729
  const result = await call(endpoint, payload);
714
730
  if (!result || result.ok !== true) {
715
731
  const failed = result && "error" in result ? result.error : void 0;
716
- throw new RpcError(failed?.code ?? "unknown", failed?.message ? failed.message : fallback);
732
+ throw new RpcError(failed?.code ?? "unknown", failed?.message ? failed.message : fallback, failed && "partial" in failed ? failed.partial : void 0);
717
733
  }
718
734
  return result.value;
719
735
  }
@@ -728,7 +744,25 @@ function reasonText(t, status) {
728
744
  }
729
745
  /** A refusal the plugin can name gets its own sentence; the rest stay verbatim. */
730
746
  function errorText(t, error) {
731
- return error.code === "plan-unavailable" ? t("planUnavailable") : error.message;
747
+ const key = {
748
+ "plan-unavailable": "planUnavailable",
749
+ "no-session": "reasonNoSession",
750
+ "no-cwd": "reasonNoCwd",
751
+ "no-git": "reasonNoGit",
752
+ "not-a-repo": "reasonNoRepo",
753
+ "no-changes": "noWorkspace",
754
+ "session-running": "sessionRunning",
755
+ "session-changed": "workspaceChanged",
756
+ "workspace-changed": "workspaceChanged",
757
+ busy: "busy",
758
+ stale: "stale",
759
+ disposed: "disabled",
760
+ cancelled: "cancelled",
761
+ "settings-unavailable": "settingsUnavailable",
762
+ "bad-request": "invalidRequest",
763
+ "not-found": "invalidRequest"
764
+ }[error.code];
765
+ return key ? t(key) : error.message || t("failed");
732
766
  }
733
767
  function panelError(reason) {
734
768
  return reason instanceof RpcError ? {
@@ -827,7 +861,7 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
827
861
  setLoading(true);
828
862
  setError(null);
829
863
  try {
830
- const next = await rpc(call, "status", { sessionId }, t("failed"));
864
+ const next = await rpc(call, "status", { sessionId });
831
865
  if (generationRef.current !== generation) return;
832
866
  setStatus(next);
833
867
  } catch (reason) {
@@ -837,11 +871,7 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
837
871
  } finally {
838
872
  if (generationRef.current === generation) setLoading(false);
839
873
  }
840
- }, [
841
- call,
842
- sessionId,
843
- t
844
- ]);
874
+ }, [call, sessionId]);
845
875
  (0, react.useEffect)(() => {
846
876
  if (!open) return;
847
877
  loadStatus();
@@ -864,23 +894,23 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
864
894
  setResult(null);
865
895
  setError(null);
866
896
  try {
867
- const value = await rpc(call, "commit", { sessionId }, t("failed"));
897
+ const value = await rpc(call, "commit", { sessionId });
868
898
  if (generationRef.current !== generation) return;
869
899
  setResult(value);
870
- const next = await rpc(call, "status", { sessionId }).catch(() => void 0);
871
- if (generationRef.current !== generation) return;
872
- if (next) setStatus(next);
873
900
  } catch (reason) {
874
901
  if (generationRef.current !== generation) return;
902
+ if (reason instanceof RpcError && reason.partial) setResult(reason.partial);
875
903
  setError(panelError(reason));
876
904
  } finally {
877
- if (generationRef.current === generation) setRunning(false);
905
+ if (generationRef.current === generation) {
906
+ const next = await rpc(call, "status", { sessionId }).catch(() => void 0);
907
+ if (generationRef.current === generation) {
908
+ setStatus(next);
909
+ setRunning(false);
910
+ }
911
+ }
878
912
  }
879
- }, [
880
- call,
881
- sessionId,
882
- t
883
- ]);
913
+ }, [call, sessionId]);
884
914
  const available = status?.available === true;
885
915
  const workspaceCount = status?.workspace.files ?? 0;
886
916
  const canCommit = available && !running && !sessionRunning && workspaceCount > 0;
@@ -1028,23 +1058,48 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
1028
1058
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1029
1059
  className: "dsh-ui-hint dsh-ui-wrap",
1030
1060
  children: [
1031
- t("done"),
1061
+ t("failure" in result ? result.commits.length ? "partial" : "stopped" : "done"),
1032
1062
  " · ",
1033
1063
  result.branch
1034
1064
  ]
1035
1065
  }),
1036
1066
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1037
1067
  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 }),
1068
+ children: result.commits.map((commit, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
1069
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: commit.hash || t("hashUnavailable") }),
1040
1070
  " ",
1041
- commit.message
1042
- ] }, commit.hash || commit.message))
1071
+ commit.message,
1072
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1073
+ className: "dsh-ui-hint",
1074
+ children: commit.files.join(", ")
1075
+ })
1076
+ ] }, `${index}:${commit.hash}`))
1043
1077
  }),
1044
1078
  result.fallback && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1045
1079
  className: "dsh-ui-hint",
1046
1080
  children: t("fallback")
1047
- })
1081
+ }),
1082
+ "uncertain" in result && result.uncertain?.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1083
+ className: "dsh-ui-hint",
1084
+ role: "status",
1085
+ children: t("uncertain")
1086
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1087
+ className: "gcm-commits",
1088
+ children: result.uncertain.map((group, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [group.message, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1089
+ className: "dsh-ui-hint",
1090
+ children: group.files.join(", ")
1091
+ })] }, index))
1092
+ })] }) : null,
1093
+ "remaining" in result && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1094
+ className: "dsh-ui-hint",
1095
+ children: t("remaining").replace("{n}", String(result.remaining.length))
1096
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1097
+ className: "gcm-commits",
1098
+ children: result.remaining.map((group, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [group.message, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1099
+ className: "dsh-ui-hint",
1100
+ children: group.files.join(", ")
1101
+ })] }, index))
1102
+ })] })
1048
1103
  ]
1049
1104
  })
1050
1105
  ]
@@ -1071,10 +1126,10 @@ const clientCss = `${officialUiCss("gcm-root")}
1071
1126
  .gcm-settings { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
1072
1127
  .gcm-settings-status { display: flex; flex-direction: column; gap: 4px; }
1073
1128
  `;
1074
- /** Host UI language, following live switches; falls back to zh when the service lacks a snapshot. */
1129
+ /** Host UI language, following live switches with the host's English fallback. */
1075
1130
  function useHostLocale(client) {
1076
1131
  const subscribe = (0, react.useCallback)((listener) => client.locale.subscribe?.(listener) ?? (() => {}), [client]);
1077
- const snapshot = (0, react.useCallback)(() => client.locale.getSnapshot?.().active ?? "zh", [client]);
1132
+ const snapshot = (0, react.useCallback)(() => client.locale.getSnapshot?.().active ?? "en", [client]);
1078
1133
  return (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot);
1079
1134
  }
1080
1135
  const settingsZh = {
@@ -1089,7 +1144,9 @@ const settingsZh = {
1089
1144
  saved: "已保存。",
1090
1145
  stale: "设置已更新,请刷新后重试。",
1091
1146
  failed: "无法读取提交设置。",
1092
- retry: "重试"
1147
+ retry: "重试",
1148
+ refresh: "刷新设置",
1149
+ syncUnavailable: "自动刷新不可用,请手动刷新设置。"
1093
1150
  };
1094
1151
  const settingsEn = {
1095
1152
  pageTitle: "Git Commit",
@@ -1103,7 +1160,9 @@ const settingsEn = {
1103
1160
  saved: "Saved.",
1104
1161
  stale: "Settings changed; refresh and retry.",
1105
1162
  failed: "Could not load commit settings.",
1106
- retry: "Retry"
1163
+ retry: "Retry",
1164
+ refresh: "Refresh settings",
1165
+ syncUnavailable: "Automatic refresh unavailable; refresh settings manually."
1107
1166
  };
1108
1167
  function settingsCopy(locale) {
1109
1168
  return String(locale).startsWith("zh") ? settingsZh : settingsEn;
@@ -1115,23 +1174,77 @@ function GitCommitSettingsPanel(props) {
1115
1174
  const [settings, setSettings] = (0, react.useState)(void 0);
1116
1175
  const [choices, setChoices] = (0, react.useState)([]);
1117
1176
  const [error, setError] = (0, react.useState)("");
1118
- const [note, setNote] = (0, react.useState)("");
1177
+ const [note, setNote] = (0, react.useState)(false);
1119
1178
  const [busy, setBusy] = (0, react.useState)(false);
1179
+ const [syncError, setSyncError] = (0, react.useState)("");
1180
+ const operation = (0, react.useRef)({
1181
+ epoch: 0,
1182
+ active: false,
1183
+ reading: false,
1184
+ writing: false,
1185
+ pending: false
1186
+ });
1187
+ const refreshRef = (0, react.useRef)(() => {});
1120
1188
  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;
1189
+ const refresh = () => {
1190
+ const state = operation.current;
1191
+ if (!state.active) return;
1192
+ if (state.reading || state.writing) {
1193
+ state.pending = true;
1194
+ return;
1195
+ }
1196
+ state.pending = false;
1197
+ state.reading = true;
1198
+ setBusy(true);
1199
+ const epoch = state.epoch;
1123
1200
  rpc(call, "settings", {}, text.failed).then((next) => {
1124
- if (live) {
1201
+ if (state.active && state.epoch === epoch && !state.pending) {
1125
1202
  setSettings(next);
1126
1203
  setError("");
1127
1204
  }
1128
1205
  }).catch(() => {
1129
- if (live) setError(text.failed);
1206
+ if (state.active && state.epoch === epoch && !state.pending) setError("failed");
1207
+ }).finally(() => {
1208
+ if (!state.active || state.epoch !== epoch) return;
1209
+ state.reading = false;
1210
+ setBusy(false);
1211
+ if (state.pending) refreshRef.current();
1212
+ });
1213
+ };
1214
+ refreshRef.current = refresh;
1215
+ (0, react.useEffect)(() => {
1216
+ const state = operation.current;
1217
+ state.epoch++;
1218
+ state.active = true;
1219
+ state.reading = false;
1220
+ state.writing = false;
1221
+ state.pending = false;
1222
+ setBusy(false);
1223
+ setSettings(void 0);
1224
+ setError("");
1225
+ setNote(false);
1226
+ refreshRef.current();
1227
+ return () => {
1228
+ state.active = false;
1229
+ state.epoch++;
1230
+ };
1231
+ }, [call]);
1232
+ (0, react.useEffect)(() => {
1233
+ setSyncError("");
1234
+ let live = true;
1235
+ const stop = props.subscribeSettings?.(() => {
1236
+ if (live) {
1237
+ setSyncError("");
1238
+ refreshRef.current();
1239
+ }
1240
+ }, (message) => {
1241
+ if (live) setSyncError(message);
1130
1242
  });
1131
1243
  return () => {
1132
1244
  live = false;
1245
+ stop?.();
1133
1246
  };
1134
- }, [call, text.failed]);
1247
+ }, [props.subscribeSettings, props.client]);
1135
1248
  const boundProvider = settings?.model.provider ?? "";
1136
1249
  const boundModel = settings?.model.model ?? "";
1137
1250
  (0, react.useEffect)(() => {
@@ -1154,9 +1267,12 @@ function GitCommitSettingsPanel(props) {
1154
1267
  boundModel
1155
1268
  ]);
1156
1269
  const save = (0, react.useCallback)((patch) => {
1157
- if (!settings) return;
1270
+ const state = operation.current;
1271
+ if (!settings || !state.active || state.writing || state.reading) return;
1272
+ state.writing = true;
1273
+ const epoch = state.epoch;
1158
1274
  setBusy(true);
1159
- setNote("");
1275
+ setNote(false);
1160
1276
  setError("");
1161
1277
  rpc(call, "settings.update", {
1162
1278
  settings: {
@@ -1165,12 +1281,17 @@ function GitCommitSettingsPanel(props) {
1165
1281
  },
1166
1282
  expectedRevision: settings.revision
1167
1283
  }, text.stale).then((next) => {
1168
- setSettings(next);
1169
- setNote(text.saved);
1170
- setBusy(false);
1284
+ if (state.active && state.epoch === epoch) {
1285
+ setSettings(next);
1286
+ setNote(true);
1287
+ }
1171
1288
  }).catch(() => {
1172
- setError(text.stale);
1289
+ if (state.active && state.epoch === epoch) setError("stale");
1290
+ }).finally(() => {
1291
+ if (!state.active || state.epoch !== epoch) return;
1292
+ state.writing = false;
1173
1293
  setBusy(false);
1294
+ if (state.pending) refreshRef.current();
1174
1295
  });
1175
1296
  }, [
1176
1297
  call,
@@ -1178,17 +1299,31 @@ function GitCommitSettingsPanel(props) {
1178
1299
  text.saved,
1179
1300
  text.stale
1180
1301
  ]);
1181
- if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
1302
+ if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1182
1303
  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
- })
1304
+ children: [
1305
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1306
+ role: "alert",
1307
+ className: "dsh-ui-error",
1308
+ children: text[error]
1309
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1310
+ role: "status",
1311
+ className: "dsh-ui-hint",
1312
+ children: text.loading
1313
+ }),
1314
+ syncError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1315
+ role: "status",
1316
+ className: "dsh-ui-hint",
1317
+ children: text.syncUnavailable
1318
+ }),
1319
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1320
+ variant: "ghost",
1321
+ size: "sm",
1322
+ disabled: busy,
1323
+ onClick: () => refreshRef.current(),
1324
+ children: text.retry
1325
+ })
1326
+ ]
1192
1327
  });
1193
1328
  const current = settings.model;
1194
1329
  const selected = choices.find((item) => item.provider === current.provider && item.model === current.model);
@@ -1199,15 +1334,30 @@ function GitCommitSettingsPanel(props) {
1199
1334
  children: [
1200
1335
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1201
1336
  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
- })]
1337
+ children: [
1338
+ syncError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1339
+ role: "status",
1340
+ className: "dsh-ui-hint",
1341
+ children: text.syncUnavailable
1342
+ }),
1343
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1344
+ variant: "ghost",
1345
+ size: "sm",
1346
+ disabled: busy,
1347
+ onClick: () => refreshRef.current(),
1348
+ children: text.refresh
1349
+ }),
1350
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1351
+ role: "alert",
1352
+ className: "dsh-ui-error",
1353
+ children: text[error]
1354
+ }),
1355
+ note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1356
+ role: "status",
1357
+ className: "dsh-ui-notice",
1358
+ children: text.saved
1359
+ })
1360
+ ]
1211
1361
  }),
1212
1362
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1213
1363
  className: "dsh-ui-field",
@@ -1216,7 +1366,7 @@ function GitCommitSettingsPanel(props) {
1216
1366
  children: text.model
1217
1367
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1218
1368
  className: "dsh-ui-select",
1219
- value: modelMenuChoiceKey(current.provider, current.model),
1369
+ value: modelRouteKey(current.provider, current.model),
1220
1370
  disabled: busy,
1221
1371
  onChange: (event) => {
1222
1372
  const key = event.target.value;
@@ -1227,16 +1377,16 @@ function GitCommitSettingsPanel(props) {
1227
1377
  } });
1228
1378
  return;
1229
1379
  }
1230
- const parsed = parseModelMenuChoiceKey(key);
1380
+ const parsed = parseModelRouteKey(key);
1231
1381
  if (parsed) save({ model: parsed });
1232
1382
  },
1233
1383
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1234
1384
  value: "",
1235
1385
  children: text.modelDefault
1236
1386
  }), choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1237
- value: modelMenuChoiceKey(choice.provider, choice.model),
1387
+ value: modelRouteKey(choice.provider, choice.model),
1238
1388
  children: choice.label
1239
- }, modelMenuChoiceKey(choice.provider, choice.model)))]
1389
+ }, modelRouteKey(choice.provider, choice.model)))]
1240
1390
  })]
1241
1391
  }),
1242
1392
  selected && efforts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -1291,6 +1441,8 @@ function GitCommitSettingsPanel(props) {
1291
1441
  }
1292
1442
  function apply(ctx) {
1293
1443
  const client = ctx;
1444
+ const settingsSubscriber = createModelSettingsSubscriber(ctx, PLUGIN_NAME);
1445
+ const subscribeSettings = (change, error) => settingsSubscriber.subscribe(change, error);
1294
1446
  ctx.provide(GIT_COMMIT_CLIENT_SERVICE, { active: true });
1295
1447
  ctx.effect(() => client.locale.register(NS, {
1296
1448
  zh: { ...zh },
@@ -1307,7 +1459,10 @@ function apply(ctx) {
1307
1459
  ctx.effect(() => client.slots.inject("plugins.bundle.config", () => client.slots.register({
1308
1460
  name: "plugins.bundle.config",
1309
1461
  key: PLUGIN_NAME
1310
- }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitCommitSettingsPanel, { client }))), "git-commit.settings");
1462
+ }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GitCommitSettingsPanel, {
1463
+ client,
1464
+ subscribeSettings
1465
+ }))), "git-commit.settings");
1311
1466
  ctx.effect(() => client.slots.inject("conversation.session.header.actions", () => client.slots.register({
1312
1467
  name: "conversation.session.header.actions",
1313
1468
  id: "git-commit",