@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.
@@ -1,2 +1,77 @@
1
- import { _ as RPC_CHANNEL, a as DEFAULT_MAX_OUTPUT_TOKENS, c as GIT_TIMEOUT_MS, d as MAX_MESSAGE_CHARS, f as MAX_UNTRACKED_PREVIEW_CHARS, g as RECENT_LOG_COUNT, h as PURPOSE_ID, i as CommitRunResult, l as GitCommitStatus, m as PROMPT_VERSION, n as CommitGroupResult, o as FEATURE_ID, p as PLUGIN_NAME, r as CommitModelInfo, s as GIT_COMMIT_CLIENT_SERVICE, t as CONTRACT_VERSION, u as MAX_MANIFEST_CHARS, v as RpcResult, y as SCHEMA_VERSION } from "./contracts2.js";
2
- export { CONTRACT_VERSION, CommitGroupResult, CommitModelInfo, CommitRunResult, DEFAULT_MAX_OUTPUT_TOKENS, FEATURE_ID, GIT_COMMIT_CLIENT_SERVICE, GIT_TIMEOUT_MS, GitCommitStatus, MAX_MANIFEST_CHARS, MAX_MESSAGE_CHARS, MAX_UNTRACKED_PREVIEW_CHARS, PLUGIN_NAME, PROMPT_VERSION, PURPOSE_ID, RECENT_LOG_COUNT, RPC_CHANNEL, type RpcResult, SCHEMA_VERSION };
1
+ import { RpcResult } from "@klarkxy/dsh-plugin-kit/contracts";
2
+ //#region src/contracts.d.ts
3
+ declare const PLUGIN_NAME = "@klarkxy/dsh-git-commit";
4
+ declare const FEATURE_ID = "git-commit";
5
+ declare const PURPOSE_ID = "git-commit.plan";
6
+ declare const RPC_CHANNEL = "/dsh-git-commit";
7
+ declare const GIT_COMMIT_CLIENT_SERVICE = "dshGitCommitClient";
8
+ declare const PROMPT_VERSION = "git-commit.v1";
9
+ declare const SCHEMA_VERSION = "commit-plan-json.v1";
10
+ declare const CONTRACT_VERSION = 1;
11
+ declare const DEFAULT_MAX_OUTPUT_TOKENS = 2048;
12
+ /** Total character budget for the change manifest sent to the model. */
13
+ declare const MAX_MANIFEST_CHARS = 48000;
14
+ /** Per-file preview cap for untracked files. */
15
+ declare const MAX_UNTRACKED_PREVIEW_CHARS = 2000;
16
+ /** Recent commit subjects supplied as style reference. */
17
+ declare const RECENT_LOG_COUNT = 10;
18
+ /** Hard cap for one generated commit message line. */
19
+ declare const MAX_MESSAGE_CHARS = 120;
20
+ /** git subprocess timeout. */
21
+ declare const GIT_TIMEOUT_MS = 30000;
22
+ declare const SETTINGS_KEY = "current";
23
+ /** Plugin-page model selection. Empty provider/model means "follow the default". */
24
+ interface CommitModelRoute {
25
+ readonly provider: string;
26
+ readonly model: string;
27
+ readonly reasoningEffort?: string;
28
+ }
29
+ interface GitCommitSettings {
30
+ readonly revision: number;
31
+ readonly model: CommitModelRoute;
32
+ /**
33
+ * Opt-in single commit when the model produced no usable plan. Off by
34
+ * default: an unplanned commit is one the user never reviewed, so a failed
35
+ * or unusable model call rejects the run instead of writing to the index.
36
+ */
37
+ readonly allowFallback: boolean;
38
+ }
39
+ declare const defaultModelRoute: () => CommitModelRoute;
40
+ declare const defaultSettings: () => GitCommitSettings;
41
+ /** Untrusted stored flags stay opt-in: anything but a literal true is false. */
42
+ declare function normalizeFallback(value: unknown): boolean;
43
+ /** Untrusted stored routes collapse to the default so a bad row never breaks the run. */
44
+ declare function normalizeModelRoute(value: unknown): CommitModelRoute;
45
+ interface CommitModelInfo {
46
+ readonly provider: string;
47
+ readonly model: string;
48
+ /** 'page' is the plugin-page selection; 'default' is the host chat model. */
49
+ readonly source: 'page' | 'default';
50
+ }
51
+ interface GitCommitStatus {
52
+ readonly available: boolean;
53
+ /** Machine-readable reason when unavailable: 'no-session' | 'no-cwd' | 'not-a-repo' | 'no-git'. */
54
+ readonly reason?: string;
55
+ readonly detail?: string;
56
+ readonly root?: string;
57
+ readonly branch?: string;
58
+ readonly workspace: {
59
+ readonly files: number;
60
+ };
61
+ readonly model?: CommitModelInfo;
62
+ }
63
+ interface CommitGroupResult {
64
+ readonly hash: string;
65
+ readonly message: string;
66
+ readonly files: readonly string[];
67
+ }
68
+ interface CommitRunResult {
69
+ readonly branch: string;
70
+ readonly commits: readonly CommitGroupResult[];
71
+ /** True when the model plan was unavailable and a single fallback commit was used. */
72
+ readonly fallback: boolean;
73
+ readonly model?: CommitModelInfo;
74
+ }
75
+ //#endregion
76
+ export { CONTRACT_VERSION, CommitGroupResult, CommitModelInfo, CommitModelRoute, CommitRunResult, DEFAULT_MAX_OUTPUT_TOKENS, FEATURE_ID, GIT_COMMIT_CLIENT_SERVICE, GIT_TIMEOUT_MS, GitCommitSettings, GitCommitStatus, MAX_MANIFEST_CHARS, MAX_MESSAGE_CHARS, MAX_UNTRACKED_PREVIEW_CHARS, PLUGIN_NAME, PROMPT_VERSION, PURPOSE_ID, RECENT_LOG_COUNT, RPC_CHANNEL, type RpcResult, SCHEMA_VERSION, SETTINGS_KEY, defaultModelRoute, defaultSettings, normalizeFallback, normalizeModelRoute };
77
+ //# sourceMappingURL=contracts.d.ts.map
package/lib/contracts.js CHANGED
@@ -18,7 +18,44 @@ const RECENT_LOG_COUNT = 10;
18
18
  const MAX_MESSAGE_CHARS = 120;
19
19
  /** git subprocess timeout. */
20
20
  const GIT_TIMEOUT_MS = 3e4;
21
+ const SETTINGS_KEY = "current";
22
+ const defaultModelRoute = () => ({
23
+ provider: "",
24
+ model: ""
25
+ });
26
+ const defaultSettings = () => ({
27
+ revision: 0,
28
+ model: defaultModelRoute(),
29
+ allowFallback: false
30
+ });
31
+ /** Untrusted stored flags stay opt-in: anything but a literal true is false. */
32
+ function normalizeFallback(value) {
33
+ return value === true;
34
+ }
35
+ function asRecord(value) {
36
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
37
+ }
38
+ function asText(value) {
39
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
40
+ }
41
+ /** Untrusted stored routes collapse to the default so a bad row never breaks the run. */
42
+ function normalizeModelRoute(value) {
43
+ const row = asRecord(value);
44
+ if (!row) return defaultModelRoute();
45
+ const provider = asText(row.provider) ?? "";
46
+ const model = asText(row.model) ?? "";
47
+ const effort = asText(row.reasoningEffort);
48
+ if (!provider || !model || provider.length > 250 || model.length > 250) return defaultModelRoute();
49
+ return effort && effort.length <= 80 ? {
50
+ provider,
51
+ model,
52
+ reasoningEffort: effort
53
+ } : {
54
+ provider,
55
+ model
56
+ };
57
+ }
21
58
  //#endregion
22
- export { CONTRACT_VERSION, DEFAULT_MAX_OUTPUT_TOKENS, FEATURE_ID, GIT_COMMIT_CLIENT_SERVICE, GIT_TIMEOUT_MS, MAX_MANIFEST_CHARS, MAX_MESSAGE_CHARS, MAX_UNTRACKED_PREVIEW_CHARS, PLUGIN_NAME, PROMPT_VERSION, PURPOSE_ID, RECENT_LOG_COUNT, RPC_CHANNEL, SCHEMA_VERSION };
59
+ export { CONTRACT_VERSION, DEFAULT_MAX_OUTPUT_TOKENS, FEATURE_ID, GIT_COMMIT_CLIENT_SERVICE, GIT_TIMEOUT_MS, MAX_MANIFEST_CHARS, MAX_MESSAGE_CHARS, MAX_UNTRACKED_PREVIEW_CHARS, PLUGIN_NAME, PROMPT_VERSION, PURPOSE_ID, RECENT_LOG_COUNT, RPC_CHANNEL, SCHEMA_VERSION, SETTINGS_KEY, defaultModelRoute, defaultSettings, normalizeFallback, normalizeModelRoute };
23
60
 
24
61
  //# sourceMappingURL=contracts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.js","names":[],"sources":["../src/contracts.ts"],"sourcesContent":["/** Browser-safe contracts. Shared RPC/result types stay on @klarkxy/dsh-ai-services. */\nexport type { RpcResult } from '@klarkxy/dsh-ai-services/contracts'\n\nexport const PLUGIN_NAME = '@klarkxy/dsh-git-commit'\nexport const FEATURE_ID = 'git-commit'\nexport const PURPOSE_ID = 'git-commit.plan'\nexport const RPC_CHANNEL = '/dsh-git-commit'\nexport const GIT_COMMIT_CLIENT_SERVICE = 'dshGitCommitClient'\nexport const PROMPT_VERSION = 'git-commit.v1'\nexport const SCHEMA_VERSION = 'commit-plan-json.v1'\nexport const CONTRACT_VERSION = 1\n\nexport const DEFAULT_MAX_OUTPUT_TOKENS = 2048\n/** Total character budget for the change manifest sent to the model. */\nexport const MAX_MANIFEST_CHARS = 48_000\n/** Per-file preview cap for untracked files. */\nexport const MAX_UNTRACKED_PREVIEW_CHARS = 2_000\n/** Recent commit subjects supplied as style reference. */\nexport const RECENT_LOG_COUNT = 10\n/** Hard cap for one generated commit message line. */\nexport const MAX_MESSAGE_CHARS = 120\n/** git subprocess timeout. */\nexport const GIT_TIMEOUT_MS = 30_000\n\nexport interface CommitModelInfo {\n readonly provider: string\n readonly model: string\n /** 'purpose' means the user configured an AI-services route; 'default' means the fallback default model. */\n readonly source: 'override' | 'purpose' | 'default'\n}\n\nexport interface GitCommitStatus {\n readonly available: boolean\n /** Machine-readable reason when unavailable: 'no-session' | 'no-cwd' | 'not-a-repo' | 'no-git'. */\n readonly reason?: string\n readonly detail?: string\n readonly root?: string\n readonly branch?: string\n readonly workspace: { readonly files: number }\n readonly model?: CommitModelInfo\n}\n\nexport interface CommitGroupResult {\n readonly hash: string\n readonly message: string\n readonly files: readonly string[]\n}\n\nexport interface CommitRunResult {\n readonly branch: string\n readonly commits: readonly CommitGroupResult[]\n /** True when the model plan was unavailable and a single fallback commit was used. */\n readonly fallback: boolean\n readonly model?: CommitModelInfo\n}\n"],"mappings":";AAGA,MAAa,cAAc;AAC3B,MAAa,aAAa;AAC1B,MAAa,aAAa;AAC1B,MAAa,cAAc;AAC3B,MAAa,4BAA4B;AACzC,MAAa,iBAAiB;AAC9B,MAAa,iBAAiB;AAC9B,MAAa,mBAAmB;AAEhC,MAAa,4BAA4B;;AAEzC,MAAa,qBAAqB;;AAElC,MAAa,8BAA8B;;AAE3C,MAAa,mBAAmB;;AAEhC,MAAa,oBAAoB;;AAEjC,MAAa,iBAAiB"}
1
+ {"version":3,"file":"contracts.js","names":[],"sources":["../src/contracts.ts"],"sourcesContent":["/** Browser-safe contracts. Shared RPC/result types stay on @klarkxy/dsh-plugin-kit. */\nexport type { RpcResult } from '@klarkxy/dsh-plugin-kit/contracts'\n\nexport const PLUGIN_NAME = '@klarkxy/dsh-git-commit'\nexport const FEATURE_ID = 'git-commit'\nexport const PURPOSE_ID = 'git-commit.plan'\nexport const RPC_CHANNEL = '/dsh-git-commit'\nexport const GIT_COMMIT_CLIENT_SERVICE = 'dshGitCommitClient'\nexport const PROMPT_VERSION = 'git-commit.v1'\nexport const SCHEMA_VERSION = 'commit-plan-json.v1'\nexport const CONTRACT_VERSION = 1\n\nexport const DEFAULT_MAX_OUTPUT_TOKENS = 2048\n/** Total character budget for the change manifest sent to the model. */\nexport const MAX_MANIFEST_CHARS = 48_000\n/** Per-file preview cap for untracked files. */\nexport const MAX_UNTRACKED_PREVIEW_CHARS = 2_000\n/** Recent commit subjects supplied as style reference. */\nexport const RECENT_LOG_COUNT = 10\n/** Hard cap for one generated commit message line. */\nexport const MAX_MESSAGE_CHARS = 120\n/** git subprocess timeout. */\nexport const GIT_TIMEOUT_MS = 30_000\nexport const SETTINGS_KEY = 'current'\n\n/** Plugin-page model selection. Empty provider/model means \"follow the default\". */\nexport interface CommitModelRoute {\n readonly provider: string\n readonly model: string\n readonly reasoningEffort?: string\n}\n\nexport interface GitCommitSettings {\n readonly revision: number\n readonly model: CommitModelRoute\n /**\n * Opt-in single commit when the model produced no usable plan. Off by\n * default: an unplanned commit is one the user never reviewed, so a failed\n * or unusable model call rejects the run instead of writing to the index.\n */\n readonly allowFallback: boolean\n}\n\nexport const defaultModelRoute = (): CommitModelRoute => ({ provider: '', model: '' })\n\nexport const defaultSettings = (): GitCommitSettings => ({\n revision: 0, model: defaultModelRoute(), allowFallback: false,\n})\n\n/** Untrusted stored flags stay opt-in: anything but a literal true is false. */\nexport function normalizeFallback(value: unknown): boolean {\n return value === true\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : undefined\n}\n\nfunction asText(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() ? value.trim() : undefined\n}\n\n/** Untrusted stored routes collapse to the default so a bad row never breaks the run. */\nexport function normalizeModelRoute(value: unknown): CommitModelRoute {\n const row = asRecord(value)\n if (!row) return defaultModelRoute()\n const provider = asText(row.provider) ?? ''\n const model = asText(row.model) ?? ''\n const effort = asText(row.reasoningEffort)\n if (!provider || !model || provider.length > 250 || model.length > 250) return defaultModelRoute()\n return effort && effort.length <= 80 ? { provider, model, reasoningEffort: effort } : { provider, model }\n}\n\nexport interface CommitModelInfo {\n readonly provider: string\n readonly model: string\n /** 'page' is the plugin-page selection; 'default' is the host chat model. */\n readonly source: 'page' | 'default'\n}\n\nexport interface GitCommitStatus {\n readonly available: boolean\n /** Machine-readable reason when unavailable: 'no-session' | 'no-cwd' | 'not-a-repo' | 'no-git'. */\n readonly reason?: string\n readonly detail?: string\n readonly root?: string\n readonly branch?: string\n readonly workspace: { readonly files: number }\n readonly model?: CommitModelInfo\n}\n\nexport interface CommitGroupResult {\n readonly hash: string\n readonly message: string\n readonly files: readonly string[]\n}\n\nexport interface CommitRunResult {\n readonly branch: string\n readonly commits: readonly CommitGroupResult[]\n /** True when the model plan was unavailable and a single fallback commit was used. */\n readonly fallback: boolean\n readonly model?: CommitModelInfo\n}\n"],"mappings":";AAGA,MAAa,cAAc;AAC3B,MAAa,aAAa;AAC1B,MAAa,aAAa;AAC1B,MAAa,cAAc;AAC3B,MAAa,4BAA4B;AACzC,MAAa,iBAAiB;AAC9B,MAAa,iBAAiB;AAC9B,MAAa,mBAAmB;AAEhC,MAAa,4BAA4B;;AAEzC,MAAa,qBAAqB;;AAElC,MAAa,8BAA8B;;AAE3C,MAAa,mBAAmB;;AAEhC,MAAa,oBAAoB;;AAEjC,MAAa,iBAAiB;AAC9B,MAAa,eAAe;AAoB5B,MAAa,2BAA6C;CAAE,UAAU;CAAI,OAAO;AAAG;AAEpF,MAAa,yBAA4C;CACvD,UAAU;CAAG,OAAO,kBAAkB;CAAG,eAAe;AAC1D;;AAGA,SAAgB,kBAAkB,OAAyB;CACzD,OAAO,UAAU;AACnB;AAEA,SAAS,SAAS,OAAqD;CACrE,OAAO,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAAI,QAAmC,KAAA;AAC1G;AAEA,SAAS,OAAO,OAAoC;CAClD,OAAO,OAAO,UAAU,YAAY,MAAM,KAAK,IAAI,MAAM,KAAK,IAAI,KAAA;AACpE;;AAGA,SAAgB,oBAAoB,OAAkC;CACpE,MAAM,MAAM,SAAS,KAAK;CAC1B,IAAI,CAAC,KAAK,OAAO,kBAAkB;CACnC,MAAM,WAAW,OAAO,IAAI,QAAQ,KAAK;CACzC,MAAM,QAAQ,OAAO,IAAI,KAAK,KAAK;CACnC,MAAM,SAAS,OAAO,IAAI,eAAe;CACzC,IAAI,CAAC,YAAY,CAAC,SAAS,SAAS,SAAS,OAAO,MAAM,SAAS,KAAK,OAAO,kBAAkB;CACjG,OAAO,UAAU,OAAO,UAAU,KAAK;EAAE;EAAU;EAAO,iBAAiB;CAAO,IAAI;EAAE;EAAU;CAAM;AAC1G"}
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { i as CommitRunResult, l as GitCommitStatus } from "./contracts2.js";
2
- import { AiServices, RpcResult } from "@klarkxy/dsh-ai-services/contracts";
1
+ import { CommitRunResult, GitCommitSettings, GitCommitStatus, defaultSettings, normalizeFallback, normalizeModelRoute } from "./contracts.js";
2
+ import { LlmTextCaller } from "@klarkxy/dsh-plugin-kit";
3
+ import { RpcResult } from "@klarkxy/dsh-plugin-kit/contracts";
3
4
  import { Context } from "@deepseek-ai/cordis";
4
5
  //#region src/git.d.ts
5
6
  interface GitRunOptions {
@@ -45,20 +46,33 @@ interface AgentStoreLike {
45
46
  }
46
47
  interface GitCommitServiceDeps {
47
48
  readonly plugin: string;
48
- readonly ai: AiServices;
49
+ readonly llm: LlmTextCaller;
50
+ readonly host?: unknown;
49
51
  readonly sessions: SessionStoreLike;
50
52
  readonly agents?: AgentStoreLike;
51
53
  readonly run?: GitRunner;
52
54
  readonly preview?: (root: string, path: string) => Promise<UntrackedPreview>;
55
+ /** Plugin-page settings row; absent keeps the shared purpose route. */
56
+ readonly settings?: {
57
+ load(): Promise<GitCommitSettings>;
58
+ save(settings: GitCommitSettings): Promise<void>;
59
+ };
53
60
  }
54
61
  declare class GitCommitService {
55
62
  private readonly deps;
56
63
  private readonly run;
57
64
  private readonly preview;
58
- private scope;
59
- private disposePurpose;
60
65
  private running;
66
+ private settings;
67
+ private settingsPending;
68
+ private readonly settingsReady;
69
+ private disposed;
61
70
  constructor(deps: GitCommitServiceDeps);
71
+ /** Apply a freshly loaded settings row; the default stays until a row is read. */
72
+ adoptSettings(next: GitCommitSettings): void;
73
+ getSettings(): GitCommitSettings;
74
+ private selectedModel;
75
+ updateSettings(patch: Omit<GitCommitSettings, 'revision'>, expectedRevision: number): Promise<GitCommitSettings>;
62
76
  start(): void;
63
77
  dispose(): Promise<void>;
64
78
  private resolveRepo;
@@ -88,7 +102,7 @@ declare function parsePlan(text: string, files: readonly ChangedFile[]): PlanGro
88
102
  //#endregion
89
103
  //#region src/index.d.ts
90
104
  declare const name = "@klarkxy/dsh-git-commit";
91
- declare const inject: readonly ["aiServices", "sessions", "agents", "connection", "webServer"];
105
+ declare const inject: readonly ["llm", "sessions", "agents", "storageDomain", "connection", "webServer"];
92
106
  declare module '@deepseek-ai/cordis' {
93
107
  interface Context {
94
108
  gitCommit: GitCommitService;
@@ -97,5 +111,5 @@ declare module '@deepseek-ai/cordis' {
97
111
  declare function handleRpc(service: GitCommitService, endpoint: string, payload: unknown, signal: AbortSignal): Promise<RpcResult>;
98
112
  declare function apply(ctx: Context): void;
99
113
  //#endregion
100
- export { GitCommitService, apply, buildPlanInput, handleRpc, inject, name, parsePlan, parsePorcelain };
114
+ export { GitCommitService, apply, buildPlanInput, defaultSettings, handleRpc, inject, name, normalizeFallback, normalizeModelRoute, parsePlan, parsePorcelain };
101
115
  //# sourceMappingURL=index.d.ts.map
package/lib/index.js CHANGED
@@ -1,11 +1,15 @@
1
- import { DEFAULT_MAX_OUTPUT_TOKENS, MAX_MANIFEST_CHARS, MAX_UNTRACKED_PREVIEW_CHARS, PLUGIN_NAME, PROMPT_VERSION, PURPOSE_ID, RPC_CHANNEL, SCHEMA_VERSION } from "./contracts.js";
2
- import { registerHostRpc } from "@klarkxy/dsh-ai-services/host-rpc";
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
+ import { registerHostRpc } from "@klarkxy/dsh-plugin-kit/host-rpc";
3
+ import { callLlmText, resolveFeatureModel } from "@klarkxy/dsh-plugin-kit";
4
+ import { modelMenuOverride } from "@klarkxy/dsh-plugin-kit/model-menu";
3
5
  import { execFile } from "node:child_process";
4
6
  import { createHash } from "node:crypto";
5
7
  import { constants } from "node:fs";
6
8
  import { copyFile, lstat, mkdtemp, open, readFile, readlink, realpath, rm } from "node:fs/promises";
7
9
  import { tmpdir } from "node:os";
8
10
  import { isAbsolute, join, relative, resolve } from "node:path";
11
+ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
12
+ import { z } from "zod";
9
13
  //#region src/git.ts
10
14
  var GitError = class extends Error {
11
15
  stderr;
@@ -565,12 +569,12 @@ var CommitRunError = class extends Error {
565
569
  this.code = code;
566
570
  }
567
571
  };
568
- function modelInfo(route) {
572
+ function modelInfo(route, source) {
569
573
  if (!route?.provider || !route.model) return void 0;
570
574
  return {
571
575
  provider: route.provider,
572
576
  model: route.model,
573
- source: route.source
577
+ source
574
578
  };
575
579
  }
576
580
  function looksChinese(subjects) {
@@ -586,30 +590,58 @@ var GitCommitService = class {
586
590
  deps;
587
591
  run;
588
592
  preview;
589
- scope;
590
- disposePurpose;
591
593
  running;
594
+ settings;
595
+ settingsPending = Promise.resolve();
596
+ settingsReady;
597
+ disposed = false;
592
598
  constructor(deps) {
593
599
  this.deps = deps;
594
600
  this.run = deps.run ?? execGit;
595
601
  this.preview = deps.preview ?? untrackedPreview;
602
+ this.settings = defaultSettings();
603
+ this.settingsReady = deps.settings?.load().then((loaded) => {
604
+ if (loaded) this.settings = structuredClone(loaded);
605
+ }, () => {
606
+ this.settings = defaultSettings();
607
+ }) ?? Promise.resolve();
596
608
  }
597
- start() {
598
- this.scope = this.deps.ai.activate(this.deps.plugin);
599
- this.disposePurpose = this.scope.registerPurpose({
600
- id: PURPOSE_ID,
601
- label: "Git 提交",
602
- defaultTarget: {
603
- kind: "role",
604
- role: "weak"
605
- },
606
- maxOutputTokens: DEFAULT_MAX_OUTPUT_TOKENS
609
+ /** Apply a freshly loaded settings row; the default stays until a row is read. */
610
+ adoptSettings(next) {
611
+ this.settings = structuredClone(next);
612
+ }
613
+ getSettings() {
614
+ return structuredClone(this.settings);
615
+ }
616
+ selectedModel() {
617
+ const page = modelMenuOverride(this.settings.model);
618
+ return {
619
+ route: resolveFeatureModel(this.deps.host, page),
620
+ source: page ? "page" : "default"
621
+ };
622
+ }
623
+ async updateSettings(patch, expectedRevision) {
624
+ const detached = structuredClone(patch);
625
+ const result = this.settingsPending.then(async () => {
626
+ await this.settingsReady;
627
+ if (this.disposed) throw new CommitRunError("disposed", "dsh-git-commit: plugin disabled");
628
+ if (expectedRevision !== this.settings.revision) throw new CommitRunError("stale", "dsh-git-commit: settings changed; reload and retry");
629
+ const next = {
630
+ revision: expectedRevision + 1,
631
+ model: detached.model,
632
+ allowFallback: normalizeFallback(detached.allowFallback)
633
+ };
634
+ await this.deps.settings?.save(next);
635
+ this.settings = next;
636
+ return this.getSettings();
607
637
  });
638
+ this.settingsPending = result.then(() => {}, () => {});
639
+ return result;
608
640
  }
641
+ start() {}
609
642
  async dispose() {
610
- this.disposePurpose?.();
611
- this.scope?.dispose();
612
- this.scope = void 0;
643
+ this.disposed = true;
644
+ await this.settingsPending;
613
645
  await this.running?.catch(() => {});
614
646
  }
615
647
  async resolveRepo(sessionId) {
@@ -638,8 +670,8 @@ var GitCommitService = class {
638
670
  throw error;
639
671
  }
640
672
  const changes = await workingTreeChanges(this.run, resolved.root);
641
- let model;
642
- if (this.scope?.active) model = await this.deps.ai.resolve(PURPOSE_ID, sessionId).then(modelInfo, () => void 0);
673
+ const selected = this.selectedModel();
674
+ const model = modelInfo(selected.route, selected.source);
643
675
  return {
644
676
  available: true,
645
677
  root: resolved.root,
@@ -664,7 +696,7 @@ var GitCommitService = class {
664
696
  const resolved = await this.resolveRepo(sessionId);
665
697
  const assertReady = () => {
666
698
  signal?.throwIfAborted();
667
- if (!this.scope?.active) throw new CommitRunError("cancelled", "dsh-git-commit: plugin is no longer active");
699
+ if (this.disposed || !this.deps.llm) throw new CommitRunError("cancelled", "dsh-git-commit: plugin is no longer active");
668
700
  const currentSession = this.deps.sessions.get(sessionId);
669
701
  if (currentSession !== session || normalizeCwd(currentSession?.header?.cwd) !== sessionCwd) throw new CommitRunError("session-changed", "dsh-git-commit: session workspace changed during planning");
670
702
  if (this.deps.agents?.get(sessionId)?.status === "running") throw new CommitRunError("session-running", "dsh-git-commit: wait for the session turn to finish");
@@ -687,34 +719,33 @@ var GitCommitService = class {
687
719
  const untracked = [];
688
720
  for (const file of selected) if (file.status === "untracked") untracked.push(await this.preview(resolved.root, file.path));
689
721
  let groups;
722
+ let planFailure = "no model route was available";
690
723
  let model;
691
- const scope = this.scope;
692
- if (scope?.active) try {
693
- const result = await scope.run({
694
- purpose: PURPOSE_ID,
695
- sessionId,
696
- input: buildPlanInput({
724
+ const planned = this.selectedModel();
725
+ if (planned.route) try {
726
+ const result = await callLlmText(this.deps.llm, {
727
+ plugin: this.deps.plugin,
728
+ route: planned.route,
729
+ system: planSystemPrompt(),
730
+ text: buildPlanInput({
697
731
  files: selected,
698
732
  recentSubjects: subjects,
699
733
  stat,
700
734
  diff,
701
735
  untracked
702
736
  }),
703
- system: planSystemPrompt(),
704
- sourceVersion: `${sessionId}:${initial.fingerprint}`,
705
- contractVersion: 1,
706
- promptVersion: PROMPT_VERSION,
707
- schemaVersion: SCHEMA_VERSION,
708
- signal,
709
- priority: "interactive"
737
+ maxTokens: DEFAULT_MAX_OUTPUT_TOKENS,
738
+ signal: signal ?? new AbortController().signal,
739
+ sessionId
710
740
  });
711
- model = modelInfo(result.receipt.route);
712
- if (result.receipt.status === "cancelled") throw new CommitRunError("cancelled", "dsh-git-commit: planning was cancelled");
741
+ model = modelInfo(result, planned.source);
713
742
  groups = parsePlan(result.text, selected);
743
+ if (!groups) planFailure = "the model returned a plan that does not cover the change set";
714
744
  } catch (error) {
715
745
  if (error instanceof CommitRunError) throw error;
716
- groups = void 0;
746
+ planFailure = error instanceof Error ? error.message : String(error);
717
747
  }
748
+ if (!groups && !this.settings.allowFallback) throw new CommitRunError("plan-unavailable", `dsh-git-commit: no commit plan (${planFailure})`);
718
749
  assertReady();
719
750
  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");
720
751
  const effective = groups ?? [fallbackGroup(selected, looksChinese(subjects) ? "zh" : "en")];
@@ -752,12 +783,47 @@ var GitCommitService = class {
752
783
  }
753
784
  };
754
785
  //#endregion
786
+ //#region src/storage.ts
787
+ const modelRouteSchema = z.object({
788
+ provider: z.string().max(250),
789
+ model: z.string().max(250),
790
+ reasoningEffort: z.string().max(80).optional()
791
+ }).strict();
792
+ /** Version 1 stored no settings row, so a missing record fills defaults. */
793
+ const settingsSchema = z.object({
794
+ revision: z.number().int().nonnegative(),
795
+ model: modelRouteSchema,
796
+ allowFallback: z.boolean().default(false)
797
+ }).strict();
798
+ const updateSettingsSchema = z.object({
799
+ expectedRevision: z.number().int().nonnegative(),
800
+ settings: z.object({
801
+ model: z.unknown(),
802
+ allowFallback: z.unknown().optional()
803
+ }).strict()
804
+ }).strict();
805
+ const gitCommitDomain = defineDomain({
806
+ name: "dsh_editor_git_commit",
807
+ version: 1,
808
+ tables: { settings: domainTable(settingsSchema) }
809
+ });
810
+ function parseSettings(value) {
811
+ const parsed = settingsSchema.safeParse(value);
812
+ if (!parsed.success) return defaultSettings();
813
+ return {
814
+ revision: parsed.data.revision,
815
+ model: normalizeModelRoute(parsed.data.model),
816
+ allowFallback: parsed.data.allowFallback
817
+ };
818
+ }
819
+ //#endregion
755
820
  //#region src/index.ts
756
821
  const name = PLUGIN_NAME;
757
822
  const inject = [
758
- "aiServices",
823
+ "llm",
759
824
  "sessions",
760
825
  "agents",
826
+ "storageDomain",
761
827
  "connection",
762
828
  "webServer"
763
829
  ];
@@ -799,6 +865,27 @@ async function handleRpc(service, endpoint, payload, signal) {
799
865
  value: await service.status(sessionId)
800
866
  };
801
867
  }
868
+ case "settings": return {
869
+ ok: true,
870
+ value: service.getSettings()
871
+ };
872
+ case "settings.update": {
873
+ const parsed = updateSettingsSchema.safeParse(payload);
874
+ if (!parsed.success) return {
875
+ ok: false,
876
+ error: {
877
+ code: "bad-request",
878
+ message: "settings patch is invalid"
879
+ }
880
+ };
881
+ return {
882
+ ok: true,
883
+ value: await service.updateSettings({
884
+ model: normalizeModelRoute(parsed.data.settings.model),
885
+ allowFallback: normalizeFallback(parsed.data.settings.allowFallback)
886
+ }, parsed.data.expectedRevision)
887
+ };
888
+ }
802
889
  case "commit": {
803
890
  const sessionId = stringField(payload, "sessionId");
804
891
  if (!sessionId) return {
@@ -827,20 +914,30 @@ async function handleRpc(service, endpoint, payload, signal) {
827
914
  }
828
915
  function apply(ctx) {
829
916
  const host = ctx;
917
+ const domainPromise = host.storageDomain.open(gitCommitDomain);
918
+ const tablePromise = domainPromise.then((domain) => domain.table("settings"));
830
919
  const service = new GitCommitService({
831
920
  plugin: PLUGIN_NAME,
832
- ai: host.aiServices,
921
+ llm: host.llm,
922
+ host,
833
923
  sessions: host.sessions,
834
- agents: host.agents
924
+ agents: host.agents,
925
+ settings: {
926
+ load: async () => parseSettings((await tablePromise).get(SETTINGS_KEY)),
927
+ save: async (settings) => {
928
+ await (await tablePromise).put(SETTINGS_KEY, settings);
929
+ }
930
+ }
835
931
  });
836
932
  service.start();
837
933
  ctx.provide("gitCommit", service);
838
934
  ctx.effect(() => async () => {
839
935
  await service.dispose();
936
+ await (await domainPromise).close();
840
937
  }, "git-commit.lifecycle");
841
938
  ctx.effect(() => registerHostRpc(host, RPC_CHANNEL, (endpoint, payload, signal) => handleRpc(service, endpoint, payload, signal)), "git-commit.rpc");
842
939
  }
843
940
  //#endregion
844
- export { GitCommitService, apply, buildPlanInput, handleRpc, inject, name, parsePlan, parsePorcelain };
941
+ export { GitCommitService, apply, buildPlanInput, defaultSettings, handleRpc, inject, name, normalizeFallback, normalizeModelRoute, parsePlan, parsePorcelain };
845
942
 
846
943
  //# sourceMappingURL=index.js.map