@klarkxy/dsh-git-commit 0.1.6 → 0.2.0

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
  }
@@ -1216,7 +1050,7 @@ function GitCommitSettingsPanel(props) {
1216
1050
  children: text.model
1217
1051
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1218
1052
  className: "dsh-ui-select",
1219
- value: modelMenuChoiceKey(current.provider, current.model),
1053
+ value: modelRouteKey(current.provider, current.model),
1220
1054
  disabled: busy,
1221
1055
  onChange: (event) => {
1222
1056
  const key = event.target.value;
@@ -1227,16 +1061,16 @@ function GitCommitSettingsPanel(props) {
1227
1061
  } });
1228
1062
  return;
1229
1063
  }
1230
- const parsed = parseModelMenuChoiceKey(key);
1064
+ const parsed = parseModelRouteKey(key);
1231
1065
  if (parsed) save({ model: parsed });
1232
1066
  },
1233
1067
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1234
1068
  value: "",
1235
1069
  children: text.modelDefault
1236
1070
  }), choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1237
- value: modelMenuChoiceKey(choice.provider, choice.model),
1071
+ value: modelRouteKey(choice.provider, choice.model),
1238
1072
  children: choice.label
1239
- }, modelMenuChoiceKey(choice.provider, choice.model)))]
1073
+ }, modelRouteKey(choice.provider, choice.model)))]
1240
1074
  })]
1241
1075
  }),
1242
1076
  selected && efforts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -45,7 +45,7 @@ declare function normalizeModelRoute(value: unknown): CommitModelRoute;
45
45
  interface CommitModelInfo {
46
46
  readonly provider: string;
47
47
  readonly model: string;
48
- /** 'page' is the plugin-page selection; 'default' is the host chat model. */
48
+ /** 'page' is the plugin-page selection; 'default' follows the session or host chat model. */
49
49
  readonly source: 'page' | 'default';
50
50
  }
51
51
  interface GitCommitStatus {
@@ -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-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"}
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' follows the session or 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
@@ -66,11 +66,16 @@ declare class GitCommitService {
66
66
  private settings;
67
67
  private settingsPending;
68
68
  private readonly settingsReady;
69
+ private settingsFailure;
70
+ private settingsAdopted;
69
71
  private disposed;
70
72
  constructor(deps: GitCommitServiceDeps);
71
- /** Apply a freshly loaded settings row; the default stays until a row is read. */
73
+ /** A refreshed row wins over a pending initial read. */
72
74
  adoptSettings(next: GitCommitSettings): void;
75
+ /** Existing synchronous snapshot API; host operations wait for initialization first. */
73
76
  getSettings(): GitCommitSettings;
77
+ ready(): Promise<void>;
78
+ private waitForSettings;
74
79
  private selectedModel;
75
80
  updateSettings(patch: Omit<GitCommitSettings, 'revision'>, expectedRevision: number): Promise<GitCommitSettings>;
76
81
  start(): void;
@@ -102,7 +107,7 @@ declare function parsePlan(text: string, files: readonly ChangedFile[]): PlanGro
102
107
  //#endregion
103
108
  //#region src/index.d.ts
104
109
  declare const name = "@klarkxy/dsh-git-commit";
105
- declare const inject: readonly ["llm", "sessions", "agents", "storageDomain", "connection", "webServer"];
110
+ declare const inject: readonly ["llm", "sessions", "agents", "storageDomain", "connection", "webServer", "sessionProjections", "agentDefaultModel"];
106
111
  declare module '@deepseek-ai/cordis' {
107
112
  interface Context {
108
113
  gitCommit: GitCommitService;
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
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
3
  import { callLlmText, resolveFeatureModel } from "@klarkxy/dsh-plugin-kit";
4
- import { modelMenuOverride } from "@klarkxy/dsh-plugin-kit/model-menu";
4
+ import { modelRouteOverride } from "@klarkxy/dsh-model-route";
5
5
  import { execFile } from "node:child_process";
6
6
  import { createHash } from "node:crypto";
7
7
  import { constants } from "node:fs";
@@ -594,6 +594,8 @@ var GitCommitService = class {
594
594
  settings;
595
595
  settingsPending = Promise.resolve();
596
596
  settingsReady;
597
+ settingsFailure;
598
+ settingsAdopted = false;
597
599
  disposed = false;
598
600
  constructor(deps) {
599
601
  this.deps = deps;
@@ -601,29 +603,64 @@ var GitCommitService = class {
601
603
  this.preview = deps.preview ?? untrackedPreview;
602
604
  this.settings = defaultSettings();
603
605
  this.settingsReady = deps.settings?.load().then((loaded) => {
604
- if (loaded) this.settings = structuredClone(loaded);
606
+ if (loaded && !this.settingsAdopted) this.settings = structuredClone(loaded);
605
607
  }, () => {
606
- this.settings = defaultSettings();
608
+ if (!this.settingsAdopted) this.settingsFailure = new CommitRunError("settings-unavailable", "dsh-git-commit: saved settings could not be read; reload the plugin before retrying");
607
609
  }) ?? Promise.resolve();
608
610
  }
609
- /** Apply a freshly loaded settings row; the default stays until a row is read. */
611
+ /** A refreshed row wins over a pending initial read. */
610
612
  adoptSettings(next) {
613
+ if (this.disposed) throw new CommitRunError("disposed", "dsh-git-commit: plugin disabled");
614
+ this.settingsAdopted = true;
615
+ this.settingsFailure = void 0;
611
616
  this.settings = structuredClone(next);
612
617
  }
618
+ /** Existing synchronous snapshot API; host operations wait for initialization first. */
613
619
  getSettings() {
620
+ if (this.settingsFailure) throw this.settingsFailure;
614
621
  return structuredClone(this.settings);
615
622
  }
616
- selectedModel() {
617
- const page = modelMenuOverride(this.settings.model);
623
+ async ready() {
624
+ await this.settingsReady;
625
+ if (this.settingsFailure) throw this.settingsFailure;
626
+ }
627
+ async waitForSettings(signal) {
628
+ signal?.throwIfAborted();
629
+ if (!signal) {
630
+ await this.ready();
631
+ return;
632
+ }
633
+ let onAbort = () => {};
634
+ const aborted = new Promise((_, reject) => {
635
+ onAbort = () => {
636
+ try {
637
+ signal.throwIfAborted();
638
+ } catch (error) {
639
+ reject(error);
640
+ }
641
+ };
642
+ signal.addEventListener("abort", onAbort, { once: true });
643
+ if (signal.aborted) onAbort();
644
+ });
645
+ try {
646
+ await Promise.race([this.settingsReady, aborted]);
647
+ } finally {
648
+ signal.removeEventListener("abort", onAbort);
649
+ }
650
+ signal.throwIfAborted();
651
+ if (this.settingsFailure) throw this.settingsFailure;
652
+ }
653
+ selectedModel(sessionId) {
654
+ const page = modelRouteOverride(this.settings.model);
618
655
  return {
619
- route: resolveFeatureModel(this.deps.host, page),
656
+ route: resolveFeatureModel(this.deps.host, page, sessionId),
620
657
  source: page ? "page" : "default"
621
658
  };
622
659
  }
623
660
  async updateSettings(patch, expectedRevision) {
624
661
  const detached = structuredClone(patch);
625
662
  const result = this.settingsPending.then(async () => {
626
- await this.settingsReady;
663
+ await this.ready();
627
664
  if (this.disposed) throw new CommitRunError("disposed", "dsh-git-commit: plugin disabled");
628
665
  if (expectedRevision !== this.settings.revision) throw new CommitRunError("stale", "dsh-git-commit: settings changed; reload and retry");
629
666
  const next = {
@@ -641,6 +678,7 @@ var GitCommitService = class {
641
678
  start() {}
642
679
  async dispose() {
643
680
  this.disposed = true;
681
+ await this.settingsReady;
644
682
  await this.settingsPending;
645
683
  await this.running?.catch(() => {});
646
684
  }
@@ -657,6 +695,7 @@ var GitCommitService = class {
657
695
  }
658
696
  }
659
697
  async status(sessionId) {
698
+ await this.ready();
660
699
  let resolved;
661
700
  try {
662
701
  resolved = await this.resolveRepo(sessionId);
@@ -670,7 +709,7 @@ var GitCommitService = class {
670
709
  throw error;
671
710
  }
672
711
  const changes = await workingTreeChanges(this.run, resolved.root);
673
- const selected = this.selectedModel();
712
+ const selected = this.selectedModel(sessionId);
674
713
  const model = modelInfo(selected.route, selected.source);
675
714
  return {
676
715
  available: true,
@@ -691,6 +730,8 @@ var GitCommitService = class {
691
730
  }
692
731
  }
693
732
  async runCommit(sessionId, signal) {
733
+ await this.waitForSettings(signal);
734
+ if (this.disposed) throw new CommitRunError("cancelled", "dsh-git-commit: plugin is no longer active");
694
735
  const session = this.deps.sessions.get(sessionId);
695
736
  const sessionCwd = normalizeCwd(session?.header?.cwd);
696
737
  const resolved = await this.resolveRepo(sessionId);
@@ -721,7 +762,7 @@ var GitCommitService = class {
721
762
  let groups;
722
763
  let planFailure = "no model route was available";
723
764
  let model;
724
- const planned = this.selectedModel();
765
+ const planned = this.selectedModel(sessionId);
725
766
  if (planned.route) try {
726
767
  const result = await callLlmText(this.deps.llm, {
727
768
  plugin: this.deps.plugin,
@@ -825,7 +866,9 @@ const inject = [
825
866
  "agents",
826
867
  "storageDomain",
827
868
  "connection",
828
- "webServer"
869
+ "webServer",
870
+ "sessionProjections",
871
+ "agentDefaultModel"
829
872
  ];
830
873
  function stringField(payload, key) {
831
874
  if (!payload || typeof payload !== "object") return void 0;
@@ -865,10 +908,12 @@ async function handleRpc(service, endpoint, payload, signal) {
865
908
  value: await service.status(sessionId)
866
909
  };
867
910
  }
868
- case "settings": return {
869
- ok: true,
870
- value: service.getSettings()
871
- };
911
+ case "settings":
912
+ await service.ready();
913
+ return {
914
+ ok: true,
915
+ value: service.getSettings()
916
+ };
872
917
  case "settings.update": {
873
918
  const parsed = updateSettingsSchema.safeParse(payload);
874
919
  if (!parsed.success) return {