@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/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { DEFAULT_MAX_OUTPUT_TOKENS, MAX_MANIFEST_CHARS, MAX_UNTRACKED_PREVIEW_CHARS, PLUGIN_NAME, RPC_CHANNEL, SETTINGS_KEY, defaultSettings, normalizeFallback, normalizeModelRoute } from "./contracts.js";
2
2
  import { registerHostRpc } from "@klarkxy/dsh-plugin-kit/host-rpc";
3
+ import { notifyModelSettingsChanged, registerModelSettings } from "@klarkxy/dsh-plugin-kit/model-settings";
4
+ import { modelRouteOverride } from "@klarkxy/dsh-model-route";
3
5
  import { callLlmText, resolveFeatureModel } from "@klarkxy/dsh-plugin-kit";
4
- import { modelMenuOverride } from "@klarkxy/dsh-plugin-kit/model-menu";
5
6
  import { execFile } from "node:child_process";
6
7
  import { createHash } from "node:crypto";
7
8
  import { constants } from "node:fs";
@@ -343,6 +344,19 @@ async function untrackedPreview(root, path) {
343
344
  };
344
345
  }
345
346
  }
347
+ /** Known commits or an uncertain current group must survive a rejected run. */
348
+ var PartialCommitError = class extends Error {
349
+ commits;
350
+ cause;
351
+ uncertain;
352
+ constructor(commits, cause, uncertain) {
353
+ super(cause instanceof Error ? cause.message : String(cause));
354
+ this.name = "PartialCommitError";
355
+ this.commits = commits;
356
+ this.cause = cause;
357
+ this.uncertain = uncertain;
358
+ }
359
+ };
346
360
  async function indexBytes(path) {
347
361
  try {
348
362
  return await readFile(path);
@@ -369,26 +383,38 @@ async function headOid(run, root) {
369
383
  */
370
384
  async function commitGroups(run, root, groups, selected, signal, verify) {
371
385
  const commits = [];
386
+ let partialFailure;
387
+ const fail = (error) => {
388
+ if (error instanceof PartialCommitError) {
389
+ partialFailure = error;
390
+ throw error;
391
+ }
392
+ if (commits.length === 0) throw error;
393
+ partialFailure = new PartialCommitError(commits, error);
394
+ throw partialFailure;
395
+ };
372
396
  for (const group of groups) {
373
397
  if (group.files.length === 0) continue;
374
398
  const paths = group.files.flatMap((path) => {
375
399
  const file = selected.find((file) => file.path === path);
376
400
  return file?.status === "renamed" && file.origin ? [path, file.origin] : [path];
377
401
  });
378
- await verify?.(paths);
379
- const gitPath = (await run([
380
- "rev-parse",
381
- "--git-path",
382
- "index"
383
- ], {
384
- cwd: root,
385
- signal
386
- })).trim();
387
- const indexPath = isAbsolute(gitPath) ? gitPath : resolve(root, gitPath);
388
- const backupDir = await mkdtemp(join(tmpdir(), "dsh-git-commit-"));
389
- const backupPath = join(backupDir, "index");
390
- let hadIndex = true;
402
+ let backupDir;
391
403
  try {
404
+ signal?.throwIfAborted();
405
+ await verify?.(paths);
406
+ const gitPath = (await run([
407
+ "rev-parse",
408
+ "--git-path",
409
+ "index"
410
+ ], {
411
+ cwd: root,
412
+ signal
413
+ })).trim();
414
+ const indexPath = isAbsolute(gitPath) ? gitPath : resolve(root, gitPath);
415
+ backupDir = await mkdtemp(join(tmpdir(), "dsh-git-commit-"));
416
+ const backupPath = join(backupDir, "index");
417
+ let hadIndex = true;
392
418
  try {
393
419
  await copyFile(indexPath, backupPath);
394
420
  } catch (error) {
@@ -399,7 +425,10 @@ async function commitGroups(run, root, groups, selected, signal, verify) {
399
425
  const originalIndex = await indexBytes(indexPath);
400
426
  if (hadIndex ? !originalIndex?.equals(await readFile(backupPath)) : originalIndex !== void 0) throw new GitError("dsh-git-commit: index changed before committing; retry");
401
427
  let stagedIndex = originalIndex;
428
+ let committed = false;
429
+ let commitInvoked = false;
402
430
  try {
431
+ signal?.throwIfAborted();
403
432
  await run([
404
433
  "add",
405
434
  "-A",
@@ -410,6 +439,8 @@ async function commitGroups(run, root, groups, selected, signal, verify) {
410
439
  signal
411
440
  });
412
441
  stagedIndex = await indexBytes(indexPath);
442
+ signal?.throwIfAborted();
443
+ commitInvoked = true;
413
444
  await run([
414
445
  "commit",
415
446
  "--quiet",
@@ -422,33 +453,71 @@ async function commitGroups(run, root, groups, selected, signal, verify) {
422
453
  cwd: root,
423
454
  signal
424
455
  });
425
- const hash = (await run([
426
- "rev-parse",
427
- "--short",
428
- "HEAD"
429
- ], {
430
- cwd: root,
431
- signal
432
- })).trim();
456
+ committed = true;
457
+ let hash = "";
458
+ try {
459
+ hash = (await run([
460
+ "rev-parse",
461
+ "--short",
462
+ "HEAD"
463
+ ], { cwd: root })).trim();
464
+ } catch (error) {
465
+ commits.push({
466
+ hash: "",
467
+ skipped: false
468
+ });
469
+ throw error;
470
+ }
471
+ if (!hash) {
472
+ commits.push({
473
+ hash: "",
474
+ skipped: false
475
+ });
476
+ throw new GitError("dsh-git-commit: commit was created but its hash could not be read");
477
+ }
433
478
  commits.push({
434
479
  hash,
435
480
  skipped: false
436
481
  });
437
482
  } catch (error) {
438
- const currentHead = await headOid(run, root);
439
- const currentIndex = await indexBytes(indexPath);
440
- const indexUntouched = currentIndex === void 0 ? stagedIndex === void 0 : currentIndex.equals(stagedIndex ?? Buffer.alloc(0));
441
- if (currentHead === head && indexUntouched) {
442
- if (hadIndex) await copyFile(backupPath, indexPath);
443
- else await rm(indexPath, { force: true });
483
+ if (!committed) {
484
+ let currentHead;
485
+ try {
486
+ currentHead = await headOid(run, root);
487
+ } catch {
488
+ if (commitInvoked) throw new PartialCommitError(commits, error, {
489
+ message: group.message,
490
+ files: [...group.files]
491
+ });
492
+ throw error;
493
+ }
494
+ if (currentHead !== head) {
495
+ if (commitInvoked) throw new PartialCommitError(commits, error, {
496
+ message: group.message,
497
+ files: [...group.files]
498
+ });
499
+ throw error;
500
+ }
501
+ const currentIndex = await indexBytes(indexPath);
502
+ const indexUntouched = currentIndex === void 0 ? stagedIndex === void 0 : currentIndex.equals(stagedIndex ?? Buffer.alloc(0));
503
+ if (currentHead === head && indexUntouched) {
504
+ if (hadIndex) await copyFile(backupPath, indexPath);
505
+ else await rm(indexPath, { force: true });
506
+ }
444
507
  }
445
508
  throw error;
446
509
  }
510
+ } catch (error) {
511
+ fail(error);
447
512
  } finally {
448
- await rm(backupDir, {
449
- recursive: true,
450
- force: true
451
- });
513
+ if (backupDir) try {
514
+ await rm(backupDir, {
515
+ recursive: true,
516
+ force: true
517
+ });
518
+ } catch (error) {
519
+ fail(partialFailure ?? error);
520
+ }
452
521
  }
453
522
  }
454
523
  return commits;
@@ -563,10 +632,13 @@ function parsePlan(text, files) {
563
632
  //#region src/service.ts
564
633
  var CommitRunError = class extends Error {
565
634
  code;
566
- constructor(code, message) {
635
+ /** Commits already created when this run stopped. Absent when nothing was written. */
636
+ partial;
637
+ constructor(code, message, partial) {
567
638
  super(message);
568
- this.name = "CommitRunError";
639
+ this.name = code === "cancelled" ? "AbortError" : "CommitRunError";
569
640
  this.code = code;
641
+ this.partial = partial;
570
642
  }
571
643
  };
572
644
  function modelInfo(route, source) {
@@ -594,36 +666,83 @@ var GitCommitService = class {
594
666
  settings;
595
667
  settingsPending = Promise.resolve();
596
668
  settingsReady;
669
+ settingsFailure;
670
+ settingsAdopted = false;
597
671
  disposed = false;
672
+ /** Aborted on unload so an in-flight model plan receives the same signal as a cancelled request. */
673
+ lifetime = new AbortController();
598
674
  constructor(deps) {
599
675
  this.deps = deps;
600
676
  this.run = deps.run ?? execGit;
601
677
  this.preview = deps.preview ?? untrackedPreview;
602
678
  this.settings = defaultSettings();
603
679
  this.settingsReady = deps.settings?.load().then((loaded) => {
604
- if (loaded) this.settings = structuredClone(loaded);
680
+ if (loaded && !this.settingsAdopted) this.settings = structuredClone(loaded);
605
681
  }, () => {
606
- this.settings = defaultSettings();
682
+ if (!this.settingsAdopted) this.settingsFailure = new CommitRunError("settings-unavailable", "dsh-git-commit: saved settings could not be read; reload the plugin before retrying");
607
683
  }) ?? Promise.resolve();
608
684
  }
609
- /** Apply a freshly loaded settings row; the default stays until a row is read. */
685
+ /** A refreshed row wins over a pending initial read. */
610
686
  adoptSettings(next) {
687
+ if (this.disposed) throw new CommitRunError("disposed", "dsh-git-commit: plugin disabled");
688
+ this.settingsAdopted = true;
689
+ this.settingsFailure = void 0;
611
690
  this.settings = structuredClone(next);
612
691
  }
692
+ /** Existing synchronous snapshot API; host operations wait for initialization first. */
613
693
  getSettings() {
694
+ this.workSignal();
695
+ if (this.settingsFailure) throw this.settingsFailure;
614
696
  return structuredClone(this.settings);
615
697
  }
616
- selectedModel() {
617
- const page = modelMenuOverride(this.settings.model);
698
+ async ready() {
699
+ this.workSignal();
700
+ await this.settingsReady;
701
+ this.workSignal();
702
+ if (this.settingsFailure) throw this.settingsFailure;
703
+ }
704
+ /** Request cancellation and plugin unload share one signal. A disposed service rejects before any new work. */
705
+ workSignal(signal) {
706
+ if (this.disposed || this.lifetime.signal.aborted) throw new CommitRunError("disposed", "dsh-git-commit: plugin is no longer active");
707
+ return signal ? AbortSignal.any([signal, this.lifetime.signal]) : this.lifetime.signal;
708
+ }
709
+ async waitForSettings(signal) {
710
+ signal?.throwIfAborted();
711
+ if (!signal) {
712
+ await this.ready();
713
+ return;
714
+ }
715
+ let onAbort = () => {};
716
+ const aborted = new Promise((_, reject) => {
717
+ onAbort = () => {
718
+ try {
719
+ signal.throwIfAborted();
720
+ } catch (error) {
721
+ reject(error);
722
+ }
723
+ };
724
+ signal.addEventListener("abort", onAbort, { once: true });
725
+ if (signal.aborted) onAbort();
726
+ });
727
+ try {
728
+ await Promise.race([this.settingsReady, aborted]);
729
+ } finally {
730
+ signal.removeEventListener("abort", onAbort);
731
+ }
732
+ signal.throwIfAborted();
733
+ if (this.settingsFailure) throw this.settingsFailure;
734
+ }
735
+ selectedModel(sessionId) {
736
+ const page = modelRouteOverride(this.settings.model);
618
737
  return {
619
- route: resolveFeatureModel(this.deps.host, page),
738
+ route: resolveFeatureModel(this.deps.host, page, sessionId),
620
739
  source: page ? "page" : "default"
621
740
  };
622
741
  }
623
742
  async updateSettings(patch, expectedRevision) {
624
743
  const detached = structuredClone(patch);
625
744
  const result = this.settingsPending.then(async () => {
626
- await this.settingsReady;
745
+ await this.ready();
627
746
  if (this.disposed) throw new CommitRunError("disposed", "dsh-git-commit: plugin disabled");
628
747
  if (expectedRevision !== this.settings.revision) throw new CommitRunError("stale", "dsh-git-commit: settings changed; reload and retry");
629
748
  const next = {
@@ -633,7 +752,10 @@ var GitCommitService = class {
633
752
  };
634
753
  await this.deps.settings?.save(next);
635
754
  this.settings = next;
636
- return this.getSettings();
755
+ try {
756
+ this.deps.onSettingsChanged?.(next.revision);
757
+ } catch {}
758
+ return structuredClone(next);
637
759
  });
638
760
  this.settingsPending = result.then(() => {}, () => {});
639
761
  return result;
@@ -641,25 +763,30 @@ var GitCommitService = class {
641
763
  start() {}
642
764
  async dispose() {
643
765
  this.disposed = true;
766
+ this.lifetime.abort();
767
+ await this.settingsReady;
644
768
  await this.settingsPending;
645
769
  await this.running?.catch(() => {});
646
770
  }
647
- async resolveRepo(sessionId) {
771
+ async resolveRepo(sessionId, signal) {
648
772
  const session = this.deps.sessions.get(sessionId);
649
773
  if (!session) throw new CommitRunError("no-session", "dsh-git-commit: session is not live on this host");
650
774
  const cwd = normalizeCwd(session.header?.cwd);
651
775
  if (!cwd) throw new CommitRunError("no-cwd", "dsh-git-commit: session has no working directory");
652
776
  try {
653
- return await repoInfo(this.run, cwd);
777
+ return await repoInfo(this.run, cwd, signal);
654
778
  } catch (error) {
779
+ signal?.throwIfAborted();
655
780
  if (isGitMissing(error)) throw new CommitRunError("no-git", "dsh-git-commit: git executable not found");
656
781
  throw new CommitRunError("not-a-repo", "dsh-git-commit: session directory is not inside a git repository");
657
782
  }
658
783
  }
659
- async status(sessionId) {
784
+ async status(sessionId, signal) {
785
+ const work = this.workSignal(signal);
786
+ await this.waitForSettings(work);
660
787
  let resolved;
661
788
  try {
662
- resolved = await this.resolveRepo(sessionId);
789
+ resolved = await this.resolveRepo(sessionId, work);
663
790
  } catch (error) {
664
791
  if (error instanceof CommitRunError) return {
665
792
  available: false,
@@ -669,8 +796,9 @@ var GitCommitService = class {
669
796
  };
670
797
  throw error;
671
798
  }
672
- const changes = await workingTreeChanges(this.run, resolved.root);
673
- const selected = this.selectedModel();
799
+ const changes = await workingTreeChanges(this.run, resolved.root, work);
800
+ work.throwIfAborted();
801
+ const selected = this.selectedModel(sessionId);
674
802
  const model = modelInfo(selected.route, selected.source);
675
803
  return {
676
804
  available: true,
@@ -681,19 +809,27 @@ var GitCommitService = class {
681
809
  };
682
810
  }
683
811
  async commit(sessionId, signal) {
812
+ if (this.disposed || this.lifetime.signal.aborted) throw new CommitRunError("disposed", "dsh-git-commit: plugin is no longer active");
684
813
  if (this.running) throw new CommitRunError("busy", "dsh-git-commit: a commit run is already in progress");
685
- const run = this.runCommit(sessionId, signal);
814
+ const work = this.workSignal(signal);
815
+ const run = this.runCommit(sessionId, work);
686
816
  this.running = run;
687
817
  try {
688
818
  return await run;
819
+ } catch (error) {
820
+ if (error instanceof CommitRunError && error.partial) throw error;
821
+ if (work.aborted) throw new CommitRunError("cancelled", "dsh-git-commit: commit run cancelled");
822
+ throw error;
689
823
  } finally {
690
824
  this.running = void 0;
691
825
  }
692
826
  }
693
827
  async runCommit(sessionId, signal) {
828
+ await this.waitForSettings(signal);
829
+ if (this.disposed || signal?.aborted) throw new CommitRunError("cancelled", "dsh-git-commit: plugin is no longer active");
694
830
  const session = this.deps.sessions.get(sessionId);
695
831
  const sessionCwd = normalizeCwd(session?.header?.cwd);
696
- const resolved = await this.resolveRepo(sessionId);
832
+ const resolved = await this.resolveRepo(sessionId, signal);
697
833
  const assertReady = () => {
698
834
  signal?.throwIfAborted();
699
835
  if (this.disposed || !this.deps.llm) throw new CommitRunError("cancelled", "dsh-git-commit: plugin is no longer active");
@@ -717,11 +853,14 @@ var GitCommitService = class {
717
853
  const originalVersions = await pathVersions(this.run, resolved.root, paths, signal);
718
854
  const [{ stat, diff }, subjects] = await Promise.all([diffAgainstHead(this.run, resolved.root, paths, signal), recentSubjects(this.run, resolved.root, 10, signal)]);
719
855
  const untracked = [];
720
- for (const file of selected) if (file.status === "untracked") untracked.push(await this.preview(resolved.root, file.path));
856
+ for (const file of selected) {
857
+ assertReady();
858
+ if (file.status === "untracked") untracked.push(await this.preview(resolved.root, file.path));
859
+ }
721
860
  let groups;
722
861
  let planFailure = "no model route was available";
723
862
  let model;
724
- const planned = this.selectedModel();
863
+ const planned = this.selectedModel(sessionId);
725
864
  if (planned.route) try {
726
865
  const result = await callLlmText(this.deps.llm, {
727
866
  plugin: this.deps.plugin,
@@ -735,13 +874,14 @@ var GitCommitService = class {
735
874
  untracked
736
875
  }),
737
876
  maxTokens: DEFAULT_MAX_OUTPUT_TOKENS,
738
- signal: signal ?? new AbortController().signal,
877
+ signal,
739
878
  sessionId
740
879
  });
741
880
  model = modelInfo(result, planned.source);
742
881
  groups = parsePlan(result.text, selected);
743
882
  if (!groups) planFailure = "the model returned a plan that does not cover the change set";
744
883
  } catch (error) {
884
+ assertReady();
745
885
  if (error instanceof CommitRunError) throw error;
746
886
  planFailure = error instanceof Error ? error.message : String(error);
747
887
  }
@@ -749,30 +889,59 @@ var GitCommitService = class {
749
889
  assertReady();
750
890
  if ((await workspaceSnapshot(this.run, resolved.root, signal)).fingerprint !== initial.fingerprint) throw new CommitRunError("workspace-changed", "dsh-git-commit: workspace changed during planning; retry the commit");
751
891
  const effective = groups ?? [fallbackGroup(selected, looksChinese(subjects) ? "zh" : "en")];
752
- const executed = await commitGroups(this.run, resolved.root, effective, selected, signal, async () => {
753
- assertReady();
754
- if (await this.run([
755
- "symbolic-ref",
756
- "--quiet",
757
- "HEAD"
758
- ], {
759
- cwd: resolved.root,
760
- signal
761
- }).catch(() => "DETACHED") !== branchRef) throw new CommitRunError("workspace-changed", "dsh-git-commit: branch changed during the commit run");
762
- if (await pathVersions(this.run, resolved.root, paths, signal) !== originalVersions) throw new CommitRunError("workspace-changed", "dsh-git-commit: selected file changed during the commit run");
763
- await workingTreeChanges(this.run, resolved.root, signal);
764
- });
765
- const commits = [];
766
- for (let index = 0; index < effective.length; index += 1) {
767
- const result = executed[index];
768
- const group = effective[index];
769
- if (!result || !group || result.skipped) continue;
770
- commits.push({
771
- hash: result.hash,
772
- message: group.message,
773
- files: group.files
892
+ const describe = (executed) => {
893
+ const commits = [];
894
+ for (let index = 0; index < executed.length; index += 1) {
895
+ const result = executed[index];
896
+ const group = effective[index];
897
+ if (!result || !group || result.skipped) continue;
898
+ commits.push({
899
+ hash: result.hash,
900
+ message: group.message,
901
+ files: [...group.files]
902
+ });
903
+ }
904
+ return commits;
905
+ };
906
+ let executed;
907
+ try {
908
+ executed = await commitGroups(this.run, resolved.root, effective, selected, signal, async () => {
909
+ assertReady();
910
+ if (await this.run([
911
+ "symbolic-ref",
912
+ "--quiet",
913
+ "HEAD"
914
+ ], {
915
+ cwd: resolved.root,
916
+ signal
917
+ }).catch(() => "DETACHED") !== branchRef) throw new CommitRunError("workspace-changed", "dsh-git-commit: branch changed during the commit run");
918
+ if (await pathVersions(this.run, resolved.root, paths, signal) !== originalVersions) throw new CommitRunError("workspace-changed", "dsh-git-commit: selected file changed during the commit run");
919
+ await workingTreeChanges(this.run, resolved.root, signal);
920
+ });
921
+ } catch (error) {
922
+ if (!(error instanceof PartialCommitError)) throw error;
923
+ const commits = describe(error.commits);
924
+ const cause = error.cause;
925
+ const cancelled = signal?.aborted || typeof cause === "object" && cause !== null && cause.code === "cancelled" || cause instanceof Error && cause.name === "AbortError";
926
+ const code = cause instanceof CommitRunError ? cause.code : cancelled ? "cancelled" : "commit-failed";
927
+ const message = cause instanceof Error ? cause.message : String(cause);
928
+ throw new CommitRunError(code, message, {
929
+ branch: resolved.branch,
930
+ commits,
931
+ fallback: groups === void 0,
932
+ failure: {
933
+ code,
934
+ message
935
+ },
936
+ remaining: effective.slice(error.commits.length + (error.uncertain ? 1 : 0)).map((group) => ({
937
+ message: group.message,
938
+ files: [...group.files]
939
+ })),
940
+ ...error.uncertain ? { uncertain: [error.uncertain] } : {},
941
+ ...model ? { model } : {}
774
942
  });
775
943
  }
944
+ const commits = describe(executed);
776
945
  if (commits.length === 0) throw new CommitRunError("no-changes", "dsh-git-commit: nothing was committed");
777
946
  return {
778
947
  branch: resolved.branch,
@@ -825,7 +994,9 @@ const inject = [
825
994
  "agents",
826
995
  "storageDomain",
827
996
  "connection",
828
- "webServer"
997
+ "webServer",
998
+ "sessionProjections",
999
+ "agentDefaultModel"
829
1000
  ];
830
1001
  function stringField(payload, key) {
831
1002
  if (!payload || typeof payload !== "object") return void 0;
@@ -837,7 +1008,8 @@ function fail(error) {
837
1008
  ok: false,
838
1009
  error: {
839
1010
  code: error.code,
840
- message: error.message
1011
+ message: error.message,
1012
+ ...error.partial ? { partial: error.partial } : {}
841
1013
  }
842
1014
  };
843
1015
  return {
@@ -862,13 +1034,15 @@ async function handleRpc(service, endpoint, payload, signal) {
862
1034
  };
863
1035
  return {
864
1036
  ok: true,
865
- value: await service.status(sessionId)
1037
+ value: await service.status(sessionId, signal)
866
1038
  };
867
1039
  }
868
- case "settings": return {
869
- ok: true,
870
- value: service.getSettings()
871
- };
1040
+ case "settings":
1041
+ await service.ready();
1042
+ return {
1043
+ ok: true,
1044
+ value: service.getSettings()
1045
+ };
872
1046
  case "settings.update": {
873
1047
  const parsed = updateSettingsSchema.safeParse(payload);
874
1048
  if (!parsed.success) return {
@@ -918,6 +1092,7 @@ function apply(ctx) {
918
1092
  const tablePromise = domainPromise.then((domain) => domain.table("settings"));
919
1093
  const service = new GitCommitService({
920
1094
  plugin: PLUGIN_NAME,
1095
+ onSettingsChanged: (revision) => notifyModelSettingsChanged(ctx, PLUGIN_NAME, revision),
921
1096
  llm: host.llm,
922
1097
  host,
923
1098
  sessions: host.sessions,
@@ -936,6 +1111,43 @@ function apply(ctx) {
936
1111
  await (await domainPromise).close();
937
1112
  }, "git-commit.lifecycle");
938
1113
  ctx.effect(() => registerHostRpc(host, RPC_CHANNEL, (endpoint, payload, signal) => handleRpc(service, endpoint, payload, signal)), "git-commit.rpc");
1114
+ ctx.effect(() => registerModelSettings(ctx, {
1115
+ ns: PLUGIN_NAME,
1116
+ title: {
1117
+ zh: "Git 提交",
1118
+ en: "Git Commit"
1119
+ },
1120
+ async read(signal) {
1121
+ await service.ready();
1122
+ signal.throwIfAborted();
1123
+ const settings = service.getSettings();
1124
+ return {
1125
+ revision: settings.revision,
1126
+ fields: [{
1127
+ path: ["model"],
1128
+ label: {
1129
+ zh: "提交模型",
1130
+ en: "Commit model"
1131
+ },
1132
+ current: settings.model,
1133
+ followLabel: {
1134
+ zh: "当前会话 / 宿主默认模型",
1135
+ en: "Current conversation / host default"
1136
+ },
1137
+ allowFollow: true
1138
+ }]
1139
+ };
1140
+ },
1141
+ async write(path, route, expectedRevision, signal) {
1142
+ if (path.length !== 1 || path[0] !== "model") throw new Error("Unknown Git Commit model setting");
1143
+ await service.ready();
1144
+ signal.throwIfAborted();
1145
+ return service.updateSettings({
1146
+ ...service.getSettings(),
1147
+ model: route
1148
+ }, expectedRevision);
1149
+ }
1150
+ }), "git-commit.model-settings");
939
1151
  }
940
1152
  //#endregion
941
1153
  export { GitCommitService, apply, buildPlanInput, defaultSettings, handleRpc, inject, name, normalizeFallback, normalizeModelRoute, parsePlan, parsePorcelain };