@klarkxy/dsh-git-commit 0.2.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -268,7 +268,14 @@ function useModelCatalog(load, options = {}) {
268
268
  refresh
269
269
  };
270
270
  }
271
- /** Also accepts a mapped state from a caller that owns broader async orchestration. */
271
+ /**
272
+ * Also accepts a mapped state from a caller that owns broader async orchestration.
273
+ *
274
+ * The dsh-ui-* classes carry the styling and resolve through the consuming
275
+ * plugin's own officialUiCss scope (every consumer mounts its root class, so
276
+ * e.g. `.dsh-model-hub .dsh-ui-loading` paints this); dsh-model-catalog-status
277
+ * stays as a structural hook for tests and layout, never a colour source.
278
+ */
272
279
  function ModelCatalogStatus({ state, locale }) {
273
280
  const zh = locale?.toLowerCase().startsWith("zh") === true;
274
281
  const copy = zh ? {
@@ -284,15 +291,17 @@ function ModelCatalogStatus({ state, locale }) {
284
291
  };
285
292
  if (!state.loading && !state.error && state.failures.length === 0) return null;
286
293
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
287
- className: "dsh-model-catalog-status",
294
+ className: "dsh-model-catalog-status dsh-ui-stack",
288
295
  lang: zh ? "zh" : "en",
289
296
  children: [
290
297
  state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
291
298
  role: "status",
299
+ className: "dsh-ui-loading",
292
300
  children: copy.loading
293
301
  }) : null,
294
302
  state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
295
303
  role: "alert",
304
+ className: "dsh-ui-error dsh-ui-wrap",
296
305
  children: [
297
306
  copy.error,
298
307
  ": ",
@@ -301,20 +310,27 @@ function ModelCatalogStatus({ state, locale }) {
301
310
  }) : null,
302
311
  state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
303
312
  role: "status",
304
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
305
- failure.name || failure.id,
306
- ": ",
307
- failure.message
308
- ] }, `${failure.id}:${index}`)) })]
313
+ className: "dsh-ui-meta",
314
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
315
+ className: "dsh-ui-list",
316
+ children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
317
+ failure.name || failure.id,
318
+ ": ",
319
+ failure.message
320
+ ] }, `${failure.id}:${index}`))
321
+ })]
309
322
  }) : null,
310
- state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
311
- type: "button",
312
- size: "sm",
313
- disabled: state.loading,
314
- onClick: () => {
315
- state.refresh();
316
- },
317
- children: copy.retry
323
+ state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
324
+ className: "dsh-ui-actions",
325
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
326
+ type: "button",
327
+ size: "sm",
328
+ disabled: state.loading,
329
+ onClick: () => {
330
+ state.refresh();
331
+ },
332
+ children: copy.retry
333
+ })
318
334
  }) : null
319
335
  ]
320
336
  });
@@ -334,7 +350,8 @@ function ModelCatalogStatus({ state, locale }) {
334
350
  * Sticky group headings, subsequence ranking, highlight walking and focus
335
351
  * hand-back all match the host. SYNC POINT: when the host's ModelSelect or
336
352
  * ModelSelect.module.css changes, this copy must be re-transcribed; styles
337
- * below may only reference --dsw-* theme tokens, never literal colors.
353
+ * below may only reference allowlisted theme tokens (the --dsw-* set plus the
354
+ * host's --ds-* motion tokens), never literal colors.
338
355
  *
339
356
  * It statically imports the primitives, so only browser clients should load
340
357
  * this entry; host code and node tests use the core entry (`./index.ts`).
@@ -842,7 +859,8 @@ function ModelMenu(props) {
842
859
  * sticky group headings (MenuGroup's own CSS), and the l1-stroke prominent
843
860
  * elevation at portal z-index 1100. The trigger mirrors the composer's 28px
844
861
  * model pill, chevron rotating open. MenuSurface paints fill and backdrop.
845
- * Only --dsw-* theme tokens may appear here — a literal color is a bug.
862
+ * Only allowlisted --dsw- and --ds- theme tokens may appear here — a literal
863
+ * color is a bug.
846
864
  */
847
865
  const modelMenuCss = `
848
866
  .dsh-model-menu-root { min-width: 0; position: relative; display: inline-flex; }
@@ -865,12 +883,14 @@ const modelMenuCss = `
865
883
  display: flex;
866
884
  }
867
885
  .dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
868
- .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
886
+ .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 var(--dsw-focus-ring-width) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
869
887
  .dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
870
888
  .dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
871
889
  .dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
872
- .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform 0.12s; }
890
+ .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform var(--ds-transition-duration) var(--ds-ease-in-out); }
873
891
  .dsh-model-menu-chevronOpen { transform: rotate(180deg); }
892
+ /* Portal overlay: the host publishes no z-index token, so the card keeps the
893
+ * native menu layer's 1100 verbatim. */
874
894
  .dsh-model-menu-menu {
875
895
  z-index: 1100;
876
896
  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
@@ -896,6 +916,8 @@ const modelMenuCss = `
896
916
  .dsh-model-menu-searchWithQuery { padding-right: 34px; }
897
917
  .dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
898
918
  .dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
919
+ /* The clear button is a circle by geometry (border-radius: 50%), a shape the
920
+ * radius token scale does not describe. */
899
921
  .dsh-model-menu-searchClear {
900
922
  width: 24px;
901
923
  height: 24px;
@@ -1143,6 +1165,17 @@ ${scope} {
1143
1165
  /* --- Layout scaffolding. --- */
1144
1166
  & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
1145
1167
  & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
1168
+ /* Plugin detail pages share one reading column. These are layout recipes;
1169
+ * controls keep their own primitive styles. Form spacing follows the host's
1170
+ * settings-form/fields.module.css and SettingsForm.module.css. */
1171
+ & .dsh-ui-page { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 760px; min-width: 0; }
1172
+ & .dsh-ui-form { display: flex; flex-direction: column; gap: 0; min-width: 0; }
1173
+ &.dsh-ui-form > .dsh-ui-field, & .dsh-ui-form > .dsh-ui-field { padding: 12px 0; }
1174
+ & .dsh-ui-form-footer { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-top: 16px; min-width: 0; }
1175
+ & .dsh-ui-fields-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; min-width: 0; margin: 0; padding: 0; border: 0; }
1176
+ @media (max-width: 560px) {
1177
+ & .dsh-ui-fields-grid { grid-template-columns: minmax(0, 1fr); }
1178
+ }
1146
1179
  & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
1147
1180
  & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
1148
1181
  & .dsh-ui-spacer { flex: 1; min-width: 0; }
@@ -1170,6 +1203,9 @@ ${scope} {
1170
1203
  * official primitive, so this only owns the vertical rhythm. --- */
1171
1204
  & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
1172
1205
  & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
1206
+ /* Grid fields share a row, so the stacked-field divider must not win later in
1207
+ * the cascade. Reset it here, after the field recipe. */
1208
+ & .dsh-ui-fields-grid > .dsh-ui-field { padding: 0; border: 0; }
1173
1209
  & .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }
1174
1210
  & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
1175
1211
  & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
@@ -1360,29 +1396,22 @@ ${scope} {
1360
1396
  & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
1361
1397
  }
1362
1398
  }
1363
- `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
1399
+ `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?=\s*\{)/gm, (_match, className) => `&${className}, & ${className}`);
1364
1400
  }
1365
1401
  //#endregion
1366
1402
  //#region ../dsh-plugin-kit/src/contracts.ts
1367
- /**
1368
- * Remote contribution for model-settings change notifications.
1369
- * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
1370
- * subscribers degrade silently when that package is not installed.
1371
- * The stream carries invalidations only — `ready` is never a settings snapshot.
1372
- */
1373
- const MODEL_SETTINGS_CHANGES_REMOTE = {
1374
- package: "@klarkxy/dsh-model-hub",
1375
- descriptors: [{
1376
- id: "modelSettingsChanges/watch",
1377
- service: "modelSettingsChangesController",
1378
- namespace: "modelSettingsChanges",
1379
- method: "watch",
1380
- mode: "stream",
1381
- invocation: { kind: "direct" },
1382
- parameters: [],
1383
- cancellation: { parameter: "signal" }
1384
- }]
1385
- };
1403
+ /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
1404
+ function modelSettingsChangesIdentity(packageName) {
1405
+ const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
1406
+ const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
1407
+ if (!parts.length) throw new Error("Model settings package name has no RPC segment");
1408
+ const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
1409
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
1410
+ return {
1411
+ service: segment,
1412
+ namespace: segment
1413
+ };
1414
+ }
1386
1415
  //#endregion
1387
1416
  //#region ../dsh-plugin-kit/src/model-settings-client.ts
1388
1417
  /** This stream carries invalidations only; ready is never a settings snapshot. */
@@ -1397,17 +1426,44 @@ function parseModelSettingsChangeFrame(value) {
1397
1426
  };
1398
1427
  throw new Error("Invalid model settings change frame");
1399
1428
  }
1400
- MODEL_SETTINGS_CHANGES_REMOTE.package, MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
1401
- ...descriptor,
1402
- result: {
1403
- mode: "strict",
1404
- typeSymbol: "ModelSettings.ChangeFrame",
1405
- create: () => ({ parse: parseModelSettingsChangeFrame })
1406
- }
1407
- }));
1429
+ /** One owner's contribution. Each package mounts only its own endpoint. */
1430
+ function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
1431
+ return {
1432
+ package: packageName,
1433
+ descriptors: [{
1434
+ id: `${identity.namespace}/watch`,
1435
+ service: identity.service,
1436
+ namespace: identity.namespace,
1437
+ method: "watch",
1438
+ mode: "stream",
1439
+ invocation: { kind: "direct" },
1440
+ parameters: [],
1441
+ cancellation: { parameter: "signal" },
1442
+ result: {
1443
+ mode: "strict",
1444
+ typeSymbol: "ModelSettings.ChangeFrame",
1445
+ create: () => ({ parse: parseModelSettingsChangeFrame })
1446
+ }
1447
+ }]
1448
+ };
1449
+ }
1450
+ /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
1451
+ * then withdraws it, so the contribution cannot outlive the plugin. */
1452
+ function mountModelSettingsChanges(ctx, packageName) {
1453
+ const contribution = modelSettingsChangesRemote(packageName);
1454
+ ctx.inject(["remote"], (injected) => {
1455
+ const scope = injected;
1456
+ const mounting = scope.remote.$mount(contribution);
1457
+ mounting.catch(() => {});
1458
+ scope.effect(() => async () => {
1459
+ await (await mounting.catch(() => void 0))?.();
1460
+ });
1461
+ });
1462
+ }
1408
1463
  /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
1409
1464
  * A factory shares one logical stream while at least one subscriber remains. */
1410
- function createModelSettingsSubscriber(ctx, namespace) {
1465
+ function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
1466
+ const endpoint = identity;
1411
1467
  const listeners = /* @__PURE__ */ new Set();
1412
1468
  let injection;
1413
1469
  let current;
@@ -1433,12 +1489,18 @@ function createModelSettingsSubscriber(ctx, namespace) {
1433
1489
  };
1434
1490
  const start = () => {
1435
1491
  const life = ++lifecycle;
1436
- injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
1492
+ injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
1437
1493
  if (life !== lifecycle || !listeners.size) return;
1438
1494
  const scope = injected;
1495
+ const service = scope.remote[endpoint.namespace];
1496
+ if (!service || !("watch" in service) || !service.watch) {
1497
+ report("Model settings sync service unavailable");
1498
+ return;
1499
+ }
1500
+ const watch = service.watch;
1439
1501
  const stream = scope.remote.$stream({
1440
1502
  name: "Model settings sync",
1441
- open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
1503
+ open: (signal) => watch(signal),
1442
1504
  ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
1443
1505
  });
1444
1506
  const session = {
@@ -1473,7 +1535,7 @@ function createModelSettingsSubscriber(ctx, namespace) {
1473
1535
  invalidate();
1474
1536
  } else {
1475
1537
  if (!ready) throw new Error("Model settings sync change arrived before ready");
1476
- if (namespace === "*" || frame.ns === namespace) invalidate();
1538
+ if (accept(frame.ns)) invalidate();
1477
1539
  }
1478
1540
  }
1479
1541
  } catch (error) {
@@ -2019,11 +2081,10 @@ const clientCss = `${officialUiCss("gcm-root")}${modelMenuCss}
2019
2081
  * of at the trigger. Naming both classes puts this rule at the same weight,
2020
2082
  * later in the sheet, so it wins. */
2021
2083
  .gcm-root.gcm-panel { position: fixed; width: min(360px, calc(100vw - 32px)); max-height: min(560px, calc(100vh - 32px)); overflow: auto; }
2022
- .gcm-model { display: flex; align-items: center; gap: 6px; min-width: 0; margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-primary); }
2023
- .gcm-commits { margin: 0; padding-inline-start: 18px; display: grid; gap: 2px; font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
2024
- .gcm-commits code { font-size: 12px; background: var(--dsw-alias-bg-layer-2); padding: 0 4px; border-radius: var(--dsw-radius-sm); }
2025
- .gcm-settings { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
2026
- .gcm-settings-status { display: flex; flex-direction: column; gap: 4px; }
2084
+ .gcm-root .gcm-model { display: flex; align-items: center; gap: 6px; min-width: 0; margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-primary); }
2085
+ .gcm-root .gcm-commits { margin: 0; padding-inline-start: 18px; display: grid; gap: 2px; font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
2086
+ .gcm-root .gcm-commits code { font-size: 12px; background: var(--dsw-alias-bg-layer-2); padding: 0 4px; border-radius: var(--dsw-radius-sm); }
2087
+ .gcm-root .gcm-settings-status { display: flex; flex-direction: column; gap: 4px; }
2027
2088
  `;
2028
2089
  /** Host UI language, following live switches with the host's English fallback. */
2029
2090
  function useHostLocale(client) {
@@ -2185,7 +2246,7 @@ function GitCommitSettingsPanel(props) {
2185
2246
  text.stale
2186
2247
  ]);
2187
2248
  if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2188
- className: "gcm-root gcm-settings",
2249
+ className: "gcm-root gcm-settings dsh-ui-page",
2189
2250
  children: [
2190
2251
  error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2191
2252
  role: "alert",
@@ -2218,7 +2279,7 @@ function GitCommitSettingsPanel(props) {
2218
2279
  const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
2219
2280
  const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
2220
2281
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2221
- className: "gcm-root gcm-settings",
2282
+ className: "gcm-root gcm-settings dsh-ui-page",
2222
2283
  "data-testid": "git-commit-settings",
2223
2284
  children: [
2224
2285
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2345,6 +2406,7 @@ function GitCommitSettingsPanel(props) {
2345
2406
  }
2346
2407
  function apply(ctx) {
2347
2408
  const client = ctx;
2409
+ mountModelSettingsChanges(ctx, PLUGIN_NAME);
2348
2410
  const settingsSubscriber = createModelSettingsSubscriber(ctx, PLUGIN_NAME);
2349
2411
  const subscribeSettings = (change, error) => settingsSubscriber.subscribe(change, error);
2350
2412
  ctx.provide(GIT_COMMIT_CLIENT_SERVICE, { active: true });
@@ -1,5 +1,5 @@
1
- import { ModelRoute, defaultModelRoute, normalizeModelRoute } from "@klarkxy/dsh-model-route";
2
1
  import { RpcResult as RpcResult$1 } from "@klarkxy/dsh-plugin-kit/contracts";
2
+ import { ModelRoute, defaultModelRoute, normalizeModelRoute } from "@klarkxy/dsh-model-route";
3
3
  //#region src/contracts.d.ts
4
4
  type RpcResult<T = unknown> = Extract<RpcResult$1<T>, {
5
5
  ok: true;
package/lib/index.d.ts CHANGED
@@ -49,6 +49,9 @@ interface GitCommitServiceDeps {
49
49
  readonly llm: LlmTextCaller;
50
50
  readonly host?: unknown;
51
51
  readonly sessions: SessionStoreLike;
52
+ readonly workingDirectory: {
53
+ get(session: SessionLike): string;
54
+ };
52
55
  readonly agents?: AgentStoreLike;
53
56
  readonly run?: GitRunner;
54
57
  readonly preview?: (root: string, path: string) => Promise<UntrackedPreview>;
@@ -111,7 +114,7 @@ declare function parsePlan(text: string, files: readonly ChangedFile[]): PlanGro
111
114
  //#endregion
112
115
  //#region src/index.d.ts
113
116
  declare const name = "@klarkxy/dsh-git-commit";
114
- declare const inject: readonly ["llm", "sessions", "agents", "storageDomain", "connection", "webServer", "sessionProjections", "agentDefaultModel"];
117
+ declare const inject: readonly ["llm", "sessions", "workingDirectory", "agents", "storageDomain", "connection", "webServer", "sessionProjections", "agentDefaultModel"];
115
118
  declare module '@deepseek-ai/cordis' {
116
119
  interface Context {
117
120
  gitCommit: GitCommitService;
package/lib/index.js CHANGED
@@ -1,5 +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
+ import { modelSettingsChangesIdentity } from "@klarkxy/dsh-plugin-kit/contracts";
4
+ import { createModelSettingsChanges } from "@klarkxy/dsh-plugin-kit/model-settings-changes";
3
5
  import { notifyModelSettingsChanged, registerModelSettings } from "@klarkxy/dsh-plugin-kit/model-settings";
4
6
  import { modelRouteOverride } from "@klarkxy/dsh-model-route";
5
7
  import { callLlmText, resolveFeatureModel } from "@klarkxy/dsh-plugin-kit";
@@ -771,7 +773,7 @@ var GitCommitService = class {
771
773
  async resolveRepo(sessionId, signal) {
772
774
  const session = this.deps.sessions.get(sessionId);
773
775
  if (!session) throw new CommitRunError("no-session", "dsh-git-commit: session is not live on this host");
774
- const cwd = normalizeCwd(session.header?.cwd);
776
+ const cwd = normalizeCwd(this.deps.workingDirectory.get(session));
775
777
  if (!cwd) throw new CommitRunError("no-cwd", "dsh-git-commit: session has no working directory");
776
778
  try {
777
779
  return await repoInfo(this.run, cwd, signal);
@@ -828,13 +830,13 @@ var GitCommitService = class {
828
830
  await this.waitForSettings(signal);
829
831
  if (this.disposed || signal?.aborted) throw new CommitRunError("cancelled", "dsh-git-commit: plugin is no longer active");
830
832
  const session = this.deps.sessions.get(sessionId);
831
- const sessionCwd = normalizeCwd(session?.header?.cwd);
833
+ const sessionCwd = session ? normalizeCwd(this.deps.workingDirectory.get(session)) : void 0;
832
834
  const resolved = await this.resolveRepo(sessionId, signal);
833
835
  const assertReady = () => {
834
836
  signal?.throwIfAborted();
835
837
  if (this.disposed || !this.deps.llm) throw new CommitRunError("cancelled", "dsh-git-commit: plugin is no longer active");
836
838
  const currentSession = this.deps.sessions.get(sessionId);
837
- if (currentSession !== session || normalizeCwd(currentSession?.header?.cwd) !== sessionCwd) throw new CommitRunError("session-changed", "dsh-git-commit: session workspace changed during planning");
839
+ if (currentSession !== session || (currentSession ? normalizeCwd(this.deps.workingDirectory.get(currentSession)) : void 0) !== sessionCwd) throw new CommitRunError("session-changed", "dsh-git-commit: session workspace changed during planning");
838
840
  if (this.deps.agents?.get(sessionId)?.status === "running") throw new CommitRunError("session-running", "dsh-git-commit: wait for the session turn to finish");
839
841
  };
840
842
  assertReady();
@@ -905,7 +907,11 @@ var GitCommitService = class {
905
907
  };
906
908
  let executed;
907
909
  try {
908
- executed = await commitGroups(this.run, resolved.root, effective, selected, signal, async () => {
910
+ const commitRun = (args, options) => {
911
+ if (args[0] === "add" || args[0] === "commit") assertReady();
912
+ return this.run(args, options);
913
+ };
914
+ executed = await commitGroups(commitRun, resolved.root, effective, selected, signal, async () => {
909
915
  assertReady();
910
916
  if (await this.run([
911
917
  "symbolic-ref",
@@ -917,6 +923,7 @@ var GitCommitService = class {
917
923
  }).catch(() => "DETACHED") !== branchRef) throw new CommitRunError("workspace-changed", "dsh-git-commit: branch changed during the commit run");
918
924
  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
925
  await workingTreeChanges(this.run, resolved.root, signal);
926
+ assertReady();
920
927
  });
921
928
  } catch (error) {
922
929
  if (!(error instanceof PartialCommitError)) throw error;
@@ -991,6 +998,7 @@ const name = PLUGIN_NAME;
991
998
  const inject = [
992
999
  "llm",
993
1000
  "sessions",
1001
+ "workingDirectory",
994
1002
  "agents",
995
1003
  "storageDomain",
996
1004
  "connection",
@@ -1087,6 +1095,7 @@ async function handleRpc(service, endpoint, payload, signal) {
1087
1095
  }
1088
1096
  }
1089
1097
  function apply(ctx) {
1098
+ createModelSettingsChanges(ctx, modelSettingsChangesIdentity(PLUGIN_NAME), PLUGIN_NAME);
1090
1099
  const host = ctx;
1091
1100
  const domainPromise = host.storageDomain.open(gitCommitDomain);
1092
1101
  const tablePromise = domainPromise.then((domain) => domain.table("settings"));
@@ -1096,6 +1105,7 @@ function apply(ctx) {
1096
1105
  llm: host.llm,
1097
1106
  host,
1098
1107
  sessions: host.sessions,
1108
+ workingDirectory: host.workingDirectory,
1099
1109
  agents: host.agents,
1100
1110
  settings: {
1101
1111
  load: async () => parseSettings((await tablePromise).get(SETTINGS_KEY)),