@klarkxy/dsh-git-commit 0.2.0 → 0.2.2

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
@@ -387,7 +387,7 @@ ${scope} {
387
387
  border: 0.5px solid var(--dsw-alias-border-l3);
388
388
  border-radius: var(--dsw-radius-md);
389
389
  background: var(--dsw-alias-bg-layer-1);
390
- font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
390
+ font-family: var(--ds-font-family-code);
391
391
  font-size: 12px;
392
392
  line-height: 20px;
393
393
  color: var(--dsw-alias-label-secondary);
@@ -463,6 +463,162 @@ ${scope} {
463
463
  `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
464
464
  }
465
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
466
622
  //#region src/contracts.ts
467
623
  const PLUGIN_NAME = "@klarkxy/dsh-git-commit";
468
624
  const RPC_CHANNEL = "/dsh-git-commit";
@@ -493,6 +649,12 @@ const zh = {
493
649
  loading: "正在读取仓库状态…",
494
650
  running: "正在整理修改并分次提交…",
495
651
  done: "已创建提交",
652
+ partial: "本次提交未全部完成;以下提交已保留",
653
+ stopped: "本次操作未确认完成",
654
+ uncertain: "以下提交组的结果不确定。请检查 Git 日志和状态,确认后再操作。",
655
+ hashUnavailable: "提交已创建,哈希未读取",
656
+ remaining: "尚未提交:{n} 组",
657
+ cancelled: "提交操作已取消",
496
658
  fallback: "模型不可用,已合并为单个提交",
497
659
  noWorkspace: "工作区没有修改",
498
660
  unavailable: "不可用",
@@ -503,6 +665,12 @@ const zh = {
503
665
  sessionRunning: "会话正在运行,提交已暂停",
504
666
  refresh: "刷新",
505
667
  failed: "操作失败",
668
+ disabled: "插件已停用",
669
+ busy: "已有提交操作正在进行",
670
+ stale: "设置已更新,请刷新后重试",
671
+ workspaceChanged: "工作区或分支已变更,请刷新后重试提交",
672
+ settingsUnavailable: "无法读取提交设置,请重新加载插件后重试",
673
+ invalidRequest: "提交请求无效,请刷新后重试",
506
674
  planUnavailable: "模型没有给出可用的提交方案,已取消本次提交,工作区没有被改动。换一个模型重试,或在插件设置里开启兜底提交。",
507
675
  close: "关闭"
508
676
  };
@@ -521,6 +689,12 @@ const en = {
521
689
  loading: "Reading repository status…",
522
690
  running: "Organizing changes into commits…",
523
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",
524
698
  fallback: "Model unavailable; used a single commit",
525
699
  noWorkspace: "No workspace changes",
526
700
  unavailable: "Unavailable",
@@ -531,23 +705,31 @@ const en = {
531
705
  sessionRunning: "Session is running; commits paused",
532
706
  refresh: "Refresh",
533
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",
534
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.",
535
715
  close: "Close"
536
716
  };
537
717
  /** A failed RPC keeps its code so the panel can explain a rejection in its own words. */
538
718
  var RpcError = class extends Error {
539
719
  code;
540
- constructor(code, message) {
720
+ partial;
721
+ constructor(code, message, partial) {
541
722
  super(message);
542
723
  this.name = "RpcError";
543
724
  this.code = code;
725
+ this.partial = partial;
544
726
  }
545
727
  };
546
- async function rpc(call, endpoint, payload = {}, fallback = "Operation failed") {
728
+ async function rpc(call, endpoint, payload = {}, fallback = "") {
547
729
  const result = await call(endpoint, payload);
548
730
  if (!result || result.ok !== true) {
549
731
  const failed = result && "error" in result ? result.error : void 0;
550
- 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);
551
733
  }
552
734
  return result.value;
553
735
  }
@@ -562,7 +744,25 @@ function reasonText(t, status) {
562
744
  }
563
745
  /** A refusal the plugin can name gets its own sentence; the rest stay verbatim. */
564
746
  function errorText(t, error) {
565
- 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");
566
766
  }
567
767
  function panelError(reason) {
568
768
  return reason instanceof RpcError ? {
@@ -661,7 +861,7 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
661
861
  setLoading(true);
662
862
  setError(null);
663
863
  try {
664
- const next = await rpc(call, "status", { sessionId }, t("failed"));
864
+ const next = await rpc(call, "status", { sessionId });
665
865
  if (generationRef.current !== generation) return;
666
866
  setStatus(next);
667
867
  } catch (reason) {
@@ -671,11 +871,7 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
671
871
  } finally {
672
872
  if (generationRef.current === generation) setLoading(false);
673
873
  }
674
- }, [
675
- call,
676
- sessionId,
677
- t
678
- ]);
874
+ }, [call, sessionId]);
679
875
  (0, react.useEffect)(() => {
680
876
  if (!open) return;
681
877
  loadStatus();
@@ -698,23 +894,23 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
698
894
  setResult(null);
699
895
  setError(null);
700
896
  try {
701
- const value = await rpc(call, "commit", { sessionId }, t("failed"));
897
+ const value = await rpc(call, "commit", { sessionId });
702
898
  if (generationRef.current !== generation) return;
703
899
  setResult(value);
704
- const next = await rpc(call, "status", { sessionId }).catch(() => void 0);
705
- if (generationRef.current !== generation) return;
706
- if (next) setStatus(next);
707
900
  } catch (reason) {
708
901
  if (generationRef.current !== generation) return;
902
+ if (reason instanceof RpcError && reason.partial) setResult(reason.partial);
709
903
  setError(panelError(reason));
710
904
  } finally {
711
- 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
+ }
712
912
  }
713
- }, [
714
- call,
715
- sessionId,
716
- t
717
- ]);
913
+ }, [call, sessionId]);
718
914
  const available = status?.available === true;
719
915
  const workspaceCount = status?.workspace.files ?? 0;
720
916
  const canCommit = available && !running && !sessionRunning && workspaceCount > 0;
@@ -862,23 +1058,48 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
862
1058
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
863
1059
  className: "dsh-ui-hint dsh-ui-wrap",
864
1060
  children: [
865
- t("done"),
1061
+ t("failure" in result ? result.commits.length ? "partial" : "stopped" : "done"),
866
1062
  " · ",
867
1063
  result.branch
868
1064
  ]
869
1065
  }),
870
1066
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
871
1067
  className: "gcm-commits",
872
- children: result.commits.map((commit) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
873
- /* @__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") }),
874
1070
  " ",
875
- commit.message
876
- ] }, 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}`))
877
1077
  }),
878
1078
  result.fallback && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
879
1079
  className: "dsh-ui-hint",
880
1080
  children: t("fallback")
881
- })
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
+ })] })
882
1103
  ]
883
1104
  })
884
1105
  ]
@@ -905,10 +1126,10 @@ const clientCss = `${officialUiCss("gcm-root")}
905
1126
  .gcm-settings { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
906
1127
  .gcm-settings-status { display: flex; flex-direction: column; gap: 4px; }
907
1128
  `;
908
- /** 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. */
909
1130
  function useHostLocale(client) {
910
1131
  const subscribe = (0, react.useCallback)((listener) => client.locale.subscribe?.(listener) ?? (() => {}), [client]);
911
- const snapshot = (0, react.useCallback)(() => client.locale.getSnapshot?.().active ?? "zh", [client]);
1132
+ const snapshot = (0, react.useCallback)(() => client.locale.getSnapshot?.().active ?? "en", [client]);
912
1133
  return (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot);
913
1134
  }
914
1135
  const settingsZh = {
@@ -923,7 +1144,9 @@ const settingsZh = {
923
1144
  saved: "已保存。",
924
1145
  stale: "设置已更新,请刷新后重试。",
925
1146
  failed: "无法读取提交设置。",
926
- retry: "重试"
1147
+ retry: "重试",
1148
+ refresh: "刷新设置",
1149
+ syncUnavailable: "自动刷新不可用,请手动刷新设置。"
927
1150
  };
928
1151
  const settingsEn = {
929
1152
  pageTitle: "Git Commit",
@@ -937,7 +1160,9 @@ const settingsEn = {
937
1160
  saved: "Saved.",
938
1161
  stale: "Settings changed; refresh and retry.",
939
1162
  failed: "Could not load commit settings.",
940
- retry: "Retry"
1163
+ retry: "Retry",
1164
+ refresh: "Refresh settings",
1165
+ syncUnavailable: "Automatic refresh unavailable; refresh settings manually."
941
1166
  };
942
1167
  function settingsCopy(locale) {
943
1168
  return String(locale).startsWith("zh") ? settingsZh : settingsEn;
@@ -949,23 +1174,77 @@ function GitCommitSettingsPanel(props) {
949
1174
  const [settings, setSettings] = (0, react.useState)(void 0);
950
1175
  const [choices, setChoices] = (0, react.useState)([]);
951
1176
  const [error, setError] = (0, react.useState)("");
952
- const [note, setNote] = (0, react.useState)("");
1177
+ const [note, setNote] = (0, react.useState)(false);
953
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)(() => {});
954
1188
  const call = (0, react.useCallback)((endpoint, payload = {}) => props.client.connection.rpc.call(RPC_CHANNEL, endpoint, payload), [props.client]);
955
- (0, react.useEffect)(() => {
956
- 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;
957
1200
  rpc(call, "settings", {}, text.failed).then((next) => {
958
- if (live) {
1201
+ if (state.active && state.epoch === epoch && !state.pending) {
959
1202
  setSettings(next);
960
1203
  setError("");
961
1204
  }
962
1205
  }).catch(() => {
963
- 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);
964
1242
  });
965
1243
  return () => {
966
1244
  live = false;
1245
+ stop?.();
967
1246
  };
968
- }, [call, text.failed]);
1247
+ }, [props.subscribeSettings, props.client]);
969
1248
  const boundProvider = settings?.model.provider ?? "";
970
1249
  const boundModel = settings?.model.model ?? "";
971
1250
  (0, react.useEffect)(() => {
@@ -988,9 +1267,12 @@ function GitCommitSettingsPanel(props) {
988
1267
  boundModel
989
1268
  ]);
990
1269
  const save = (0, react.useCallback)((patch) => {
991
- 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;
992
1274
  setBusy(true);
993
- setNote("");
1275
+ setNote(false);
994
1276
  setError("");
995
1277
  rpc(call, "settings.update", {
996
1278
  settings: {
@@ -999,12 +1281,17 @@ function GitCommitSettingsPanel(props) {
999
1281
  },
1000
1282
  expectedRevision: settings.revision
1001
1283
  }, text.stale).then((next) => {
1002
- setSettings(next);
1003
- setNote(text.saved);
1004
- setBusy(false);
1284
+ if (state.active && state.epoch === epoch) {
1285
+ setSettings(next);
1286
+ setNote(true);
1287
+ }
1005
1288
  }).catch(() => {
1006
- 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;
1007
1293
  setBusy(false);
1294
+ if (state.pending) refreshRef.current();
1008
1295
  });
1009
1296
  }, [
1010
1297
  call,
@@ -1012,17 +1299,31 @@ function GitCommitSettingsPanel(props) {
1012
1299
  text.saved,
1013
1300
  text.stale
1014
1301
  ]);
1015
- if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
1302
+ if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1016
1303
  className: "gcm-root gcm-settings",
1017
- children: error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1018
- role: "alert",
1019
- className: "dsh-ui-error",
1020
- children: error
1021
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1022
- role: "status",
1023
- className: "dsh-ui-hint",
1024
- children: text.loading
1025
- })
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
+ ]
1026
1327
  });
1027
1328
  const current = settings.model;
1028
1329
  const selected = choices.find((item) => item.provider === current.provider && item.model === current.model);
@@ -1033,15 +1334,30 @@ function GitCommitSettingsPanel(props) {
1033
1334
  children: [
1034
1335
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1035
1336
  className: "gcm-settings-status",
1036
- children: [error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1037
- role: "alert",
1038
- className: "dsh-ui-error",
1039
- children: error
1040
- }), note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1041
- role: "status",
1042
- className: "dsh-ui-notice",
1043
- children: note
1044
- })]
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
+ ]
1045
1361
  }),
1046
1362
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1047
1363
  className: "dsh-ui-field",
@@ -1125,6 +1441,8 @@ function GitCommitSettingsPanel(props) {
1125
1441
  }
1126
1442
  function apply(ctx) {
1127
1443
  const client = ctx;
1444
+ const settingsSubscriber = createModelSettingsSubscriber(ctx, PLUGIN_NAME);
1445
+ const subscribeSettings = (change, error) => settingsSubscriber.subscribe(change, error);
1128
1446
  ctx.provide(GIT_COMMIT_CLIENT_SERVICE, { active: true });
1129
1447
  ctx.effect(() => client.locale.register(NS, {
1130
1448
  zh: { ...zh },
@@ -1141,7 +1459,10 @@ function apply(ctx) {
1141
1459
  ctx.effect(() => client.slots.inject("plugins.bundle.config", () => client.slots.register({
1142
1460
  name: "plugins.bundle.config",
1143
1461
  key: PLUGIN_NAME
1144
- }, () => /* @__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");
1145
1466
  ctx.effect(() => client.slots.inject("conversation.session.header.actions", () => client.slots.register({
1146
1467
  name: "conversation.session.header.actions",
1147
1468
  id: "git-commit",