@ibobbyts/dsh-git-commit 0.2.0 → 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/README.md CHANGED
@@ -4,7 +4,7 @@ A lightweight floating **Git status + AI Commit** plugin for [DeepSeek Harness (
4
4
 
5
5
  - View the current Session's Git branch and changed-line counts in a draggable, collapsible panel.
6
6
  - Generate a commit message through a model configured in DSH, then make **one real Git commit**.
7
- - Choose global/repository instructions, a model, optional Token metrics, and countdown color in the native DSH right sidebar.
7
+ - Configure independent **Repository** / **Global** settings tabs: per-repository model overrides (default: inherit global), writing instructions, Token metrics, and countdown color in the native DSH right sidebar.
8
8
  - Keep the floating panel within the visible window; **drag locations persist** across restarts. The settings panel includes **Reset floating window position**.
9
9
  - Non-Git directories show a compact warning or remain hidden according to your preference.
10
10
 
package/lib/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Browser loader bundle (no JSX compilation required). DSH also loads this in Desktop. */
2
2
  window.__ModuleLoader__.load({
3
- id: 'dsh-git-commit',
3
+ id: '@ibobbyts/dsh-git-commit',
4
4
  factory: (require) => {
5
5
  const React = require('react');
6
6
  const h = React.createElement;
@@ -53,6 +53,12 @@ window.__ModuleLoader__.load({
53
53
  .dgc-settings-divider{width:100%;border:0;border-top:1px solid var(--dsw-alias-border-l2);margin:2px 0}
54
54
  .dgc-repo-path{display:block;font:11px ui-monospace,Menlo,monospace;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;word-break:break-all}
55
55
  .dgc-save-error{color:var(--dsw-alias-state-error-primary)}
56
+ .dgc-tabs{display:flex;gap:4px;padding:3px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:8px}
57
+ .dgc-tab{flex:1;min-width:0;padding:8px 10px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;line-height:1.25;font-family:inherit;cursor:pointer}
58
+ .dgc-tab[aria-selected="true"]{background:var(--dsw-alias-bg-overlay);border-color:var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary)}
59
+ .dgc-tab:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
60
+ .dgc-tab-panel{display:flex;flex-direction:column;gap:13px;min-width:0}
61
+ body[data-ds-dark-theme] .dgc-tab[aria-selected="true"]{background:#383e47;border-color:#596271;color:#f3f5f8}
56
62
  .dgc-reset-position{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:34px;width:100%;font:inherit;cursor:pointer;border-radius:7px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}
57
63
  .dgc-reset-position:hover{background:var(--dsw-alias-bg-layer-2)}
58
64
  .dgc-reset-position:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
@@ -443,6 +449,7 @@ window.__ModuleLoader__.load({
443
449
  function CommitSettings() {
444
450
  const cwd = React.useSyncExternalStore(workspace.subscribe, workspace.getSnapshot, workspace.getSnapshot);
445
451
  const [form, setForm] = React.useState(null);
452
+ const [activeTab, setActiveTab] = React.useState('repository');
446
453
  const [models, setModels] = React.useState([]);
447
454
  const [defaultModel, setDefaultModel] = React.useState(null);
448
455
  const [loadError, setLoadError] = React.useState('');
@@ -459,6 +466,10 @@ window.__ModuleLoader__.load({
459
466
  },error=>{if(active)setLoadError(error.message);});
460
467
  return ()=>{active=false;};
461
468
  },[cwd]);
469
+ React.useEffect(()=>{
470
+ // A repository-less workspace still permits editing global preferences.
471
+ if (form?.isRepository === false) setActiveTab('global');
472
+ },[form?.isRepository]);
462
473
  React.useEffect(()=>{
463
474
  let active = true;
464
475
  const saver = createAutoSaver(patch=>rpc('saveSettings',{cwd,patch}),error=>{
@@ -479,55 +490,109 @@ window.__ModuleLoader__.load({
479
490
  setSaveError('');
480
491
  saverRef.current?.update(key,value,immediate);
481
492
  };
482
- const choice=form?.model?`${form.model.provider}\t${form.model.model}`:'';
483
- const repository=form?.isRepository===true;
484
- return h('div',{className:'dgc-settings'},
485
- h('h3',null,'Git commit settings'),
486
- !form? h('div',{className:'dgc-subtle'},loadError||'Loading…') : [
487
- h('label',{className:'dgc-field',key:'global'},'Global commit instruction',
488
- h('textarea',{value:form.globalInstruction,onChange:e=>update('globalInstruction',e.target.value),
489
- onBlur:()=>{void saverRef.current?.flush();},placeholder:'Shared conventions for every repository'})),
490
- h('label',{className:'dgc-field',key:'model'},'Model',h('select',{value:choice,onChange:e=>{
491
- const value=e.target.value;
492
- const item=models.find(m=>`${m.provider}\t${m.model}`===value);
493
- update('model',item?{provider:item.provider,model:item.model}:null,true);
494
- }},
495
- h('option',{value:''},defaultModel ? `DSH default (${defaultModel.provider}/${defaultModel.model})` : 'DSH default'),
496
- ...models.map(model=>h('option',{key:`${model.provider}/${model.model}`,value:`${model.provider}\t${model.model}`},`${model.providerName} / ${model.name}`))
497
- )),
498
- h('hr',{key:'divider',className:'dgc-settings-divider'}),
499
- repository ? [
493
+ const repository = form?.isRepository === true;
494
+ const selectedModelKey = selection => selection ? `${selection.provider}\t${selection.model}` : '';
495
+ const modelChoices = selected => {
496
+ const entries = models.map(entry => h('option',{
497
+ key:`${entry.provider}/${entry.model}`,value:`${entry.provider}\t${entry.model}`,
498
+ },`${entry.providerName} / ${entry.name}`));
499
+ if (selected && !models.some(entry => entry.provider === selected.provider && entry.model === selected.model)) {
500
+ entries.unshift(h('option',{key:'selected-unknown',value:selectedModelKey(selected)},
501
+ `${selected.provider} / ${selected.model}`));
502
+ }
503
+ return entries;
504
+ };
505
+ // Always display the model currently inherited, even if the global selector
506
+ // is set to DSH default (which can itself change independently).
507
+ const effectiveGlobalModel = form?.model ?? defaultModel ?? models[0] ?? null;
508
+ const inheritedModelName = effectiveGlobalModel ? (() => {
509
+ const entry = models.find(item => item.provider === effectiveGlobalModel.provider
510
+ && item.model === effectiveGlobalModel.model);
511
+ return entry ? `${entry.providerName} / ${entry.name}`
512
+ : `${effectiveGlobalModel.provider} / ${effectiveGlobalModel.model}`;
513
+ })() : 'Not configured';
514
+ const tabs = h('div',{className:'dgc-tabs',role:'tablist','aria-label':'Git commit settings scope'},
515
+ ...['repository','global'].map(tab => h('button',{
516
+ type:'button',key:tab,role:'tab',id:`dgc-tab-${tab}`,
517
+ 'aria-label':tab === 'repository' ? 'Repository' : 'Global',
518
+ 'aria-selected':activeTab === tab, 'aria-controls':`dgc-panel-${tab}`,
519
+ tabIndex:activeTab === tab ? 0 : -1,
520
+ className:'dgc-tab',onClick:()=>setActiveTab(tab),
521
+ onKeyDown:event=>{
522
+ const next = event.key === 'ArrowLeft' ? 'repository'
523
+ : event.key === 'ArrowRight' ? 'global'
524
+ : event.key === 'Home' ? 'repository' : event.key === 'End' ? 'global' : null;
525
+ if (next) {
526
+ event.preventDefault();
527
+ setActiveTab(next);
528
+ event.currentTarget.parentElement?.querySelector?.(`#dgc-tab-${next}`)?.focus?.();
529
+ }
530
+ },
531
+ },tab === 'repository' ? 'Repository' : 'Global')));
532
+ const modelSelect = (key,value,onChange,firstLabel,firstValue) => h('label',{
533
+ className:'dgc-field',key,
534
+ },'Model',h('select',{'aria-label':activeTab === 'repository' ? 'Repository model' : 'Global model',
535
+ value,onChange,
536
+ },h('option',{value:firstValue},firstLabel),...modelChoices(
537
+ activeTab === 'repository' ? form.repositoryModel : form.model)));
538
+ const content = !form ? null : activeTab === 'repository'
539
+ ? (repository ? [
540
+ modelSelect('repository-model',form.repositoryModel ? selectedModelKey(form.repositoryModel) : 'inherit-global',e=>{
541
+ const choice=e.target.value;
542
+ const selected=models.find(entry=>selectedModelKey(entry)===choice);
543
+ update('repositoryModel',choice==='inherit-global'?null:selected
544
+ ? {provider:selected.provider,model:selected.model} : form.repositoryModel,true);
545
+ },`Inherit Global (${inheritedModelName})`,'inherit-global'),
546
+ h('hr',{key:'repository-divider',className:'dgc-settings-divider'}),
500
547
  h('label',{className:'dgc-check',key:'inherit'},
501
- h('input',{type:'checkbox',checked:form.inheritGlobal,onChange:e=>update('inheritGlobal',e.target.checked,true)}),
548
+ h('input',{type:'checkbox',checked:form.inheritGlobal,
549
+ onChange:e=>update('inheritGlobal',e.target.checked,true)}),
502
550
  'Inherit global instruction'),
503
551
  h('label',{className:'dgc-field',key:'repo'},'Repository commit instruction',
504
552
  h('textarea',{value:form.repositoryInstruction,onChange:e=>update('repositoryInstruction',e.target.value),
505
553
  onBlur:()=>{void saverRef.current?.flush();},placeholder:'Repository-specific commit conventions'})),
506
554
  h('code',{className:'dgc-repo-path',key:'path'},form.root),
507
- ] : h('div',{className:'dgc-subtle',key:'no-repo'},'Not a git repository'),
508
- h('hr',{key:'visibilityDivider',className:'dgc-settings-divider'}),
509
- h('label',{className:'dgc-check',key:'autoHideNonGit'},
510
- h('input',{type:'checkbox',checked:form.autoHideNonGit===true,
511
- onChange:e=>update('autoHideNonGit',e.target.checked,true)}),
512
- 'Automatically hide floating window in non-git projects'),
513
- h('label',{className:'dgc-check',key:'showTokenDetails'},
514
- h('input',{type:'checkbox',checked:form.showTokenDetails===true,
515
- onChange:e=>update('showTokenDetails',e.target.checked,true)}),
516
- 'Show token details'),
517
- h('label',{className:'dgc-countdown-color',key:'countdownColor'},
518
- h('span',null,'Countdown color'),
519
- h('input',{type:'color',value:colorOrDefault(form.countdownColor),
520
- 'aria-label':'Countdown color',title:'Countdown color',
521
- onChange:e=>update('countdownColor',e.target.value,true)})),
522
- h('hr',{key:'positionDivider',className:'dgc-settings-divider'}),
523
- h('button',{type:'button',key:'resetPosition',className:'dgc-reset-position',
524
- 'aria-label':'Reset floating window position',title:'Reset floating window position',
525
- onClick:()=>{
526
- floatingPosition.set(DEFAULT_POSITION);
527
- setSaveError('');
528
- void persistPosition(DEFAULT_POSITION,cwd,setSaveError);
529
- }},'Reset floating window position'),
530
- ],
555
+ ] : h('div',{className:'dgc-subtle',key:'no-repo'},'Not a git repository'))
556
+ : [
557
+ h('label',{className:'dgc-field',key:'global'},'Global commit instruction',
558
+ h('textarea',{value:form.globalInstruction,onChange:e=>update('globalInstruction',e.target.value),
559
+ onBlur:()=>{void saverRef.current?.flush();},placeholder:'Shared conventions for every repository'})),
560
+ modelSelect('global-model',selectedModelKey(form.model),e=>{
561
+ const choice=e.target.value;
562
+ const selected=models.find(entry=>selectedModelKey(entry)===choice);
563
+ update('model',!choice?null:selected
564
+ ? {provider:selected.provider,model:selected.model}:form.model,true);
565
+ },defaultModel
566
+ ? `DSH default (${defaultModel.provider}/${defaultModel.model})` : 'DSH default',''),
567
+ h('hr',{key:'global-divider',className:'dgc-settings-divider'}),
568
+ h('label',{className:'dgc-check',key:'autoHideNonGit'},
569
+ h('input',{type:'checkbox',checked:form.autoHideNonGit===true,
570
+ onChange:e=>update('autoHideNonGit',e.target.checked,true)}),
571
+ 'Automatically hide floating window in non-git projects'),
572
+ h('label',{className:'dgc-check',key:'showTokenDetails'},
573
+ h('input',{type:'checkbox',checked:form.showTokenDetails===true,
574
+ onChange:e=>update('showTokenDetails',e.target.checked,true)}),
575
+ 'Show token details'),
576
+ h('label',{className:'dgc-countdown-color',key:'countdownColor'},
577
+ h('span',null,'Countdown color'),
578
+ h('input',{type:'color',value:colorOrDefault(form.countdownColor),
579
+ 'aria-label':'Countdown color',title:'Countdown color',
580
+ onChange:e=>update('countdownColor',e.target.value,true)})),
581
+ h('hr',{key:'positionDivider',className:'dgc-settings-divider'}),
582
+ h('button',{type:'button',key:'resetPosition',className:'dgc-reset-position',
583
+ 'aria-label':'Reset floating window position',title:'Reset floating window position',
584
+ onClick:()=>{
585
+ floatingPosition.set(DEFAULT_POSITION);
586
+ setSaveError('');
587
+ void persistPosition(DEFAULT_POSITION,cwd,setSaveError);
588
+ }},'Reset floating window position'),
589
+ ];
590
+ return h('div',{className:'dgc-settings'},
591
+ h('h3',null,'Git commit settings'),
592
+ tabs,
593
+ !form ? h('div',{className:'dgc-subtle'},loadError||'Loading…')
594
+ : h('div',{className:'dgc-tab-panel',role:'tabpanel',id:`dgc-panel-${activeTab}`,
595
+ 'aria-labelledby':`dgc-tab-${activeTab}`},content),
531
596
  saveError ? h('div',{className:'dgc-notice dgc-save-error',role:'alert'},saveError) : null,
532
597
  form && loadError ? h('div',{className:'dgc-notice dgc-error',role:'alert'},loadError) : null
533
598
  );
@@ -44,8 +44,11 @@ export class GitCommitService {
44
44
  }
45
45
  async saveSettings(request) {
46
46
  const root = await this.settingsRoot(request);
47
- if (request?.patch?.model) {
48
- await this.ctx.llm.resolveModelInfo(request.patch.model.provider, request.patch.model.model);
47
+ // Both selectors use the same DSH model catalog, with independent scopes.
48
+ for (const key of ['model', 'repositoryModel']) {
49
+ if (request?.patch?.[key]) {
50
+ await this.ctx.llm.resolveModelInfo(request.patch[key].provider, request.patch[key].model);
51
+ }
49
52
  }
50
53
  return { ...(await this.store.save(root, request.patch)), isRepository: root !== null, root };
51
54
  }
@@ -65,7 +68,7 @@ export class GitCommitService {
65
68
  return { models, defaultModel };
66
69
  }
67
70
  async chosenModel(settings) {
68
- const selected = settings.model ?? (await this.models()).defaultModel ?? (await this.models()).models[0];
71
+ const selected = settings.repositoryModel ?? settings.model ?? (await this.models()).defaultModel ?? (await this.models()).models[0];
69
72
  if (!selected) throw new Error('No configured DSH model is available');
70
73
  const provider = selected.provider, model = selected.model;
71
74
  const info = await this.ctx.llm.resolveModelInfo(provider, model);
package/lib/core/store.js CHANGED
@@ -18,6 +18,17 @@ const string = (value, field, limit = 12000) => {
18
18
  if (typeof value !== 'string' || value.length > limit) throw new Error(`${field} must be a string of at most ${limit} characters`);
19
19
  return value;
20
20
  };
21
+ /** null is the fixed 'inherit global' marker for repository model selection. */
22
+ const storedModel = value => plain(value) && typeof value.provider === 'string' && value.provider.length > 0
23
+ && typeof value.model === 'string' && value.model.length > 0
24
+ ? { provider: value.provider, model: value.model } : null;
25
+ const checkedModel = (value, key) => {
26
+ if (value === null) return null;
27
+ if (!plain(value) || typeof value.provider !== 'string' || typeof value.model !== 'string'
28
+ || !value.provider || !value.model) throw new Error(`Invalid ${key} selection`);
29
+ return { provider: string(value.provider, `${key}.provider`, 200),
30
+ model: string(value.model, `${key}.model`, 400) };
31
+ };
21
32
 
22
33
  export class InstructionStore {
23
34
  constructor(path = join(HOME(), 'dsh-git-commit.json')) {
@@ -37,8 +48,8 @@ export class InstructionStore {
37
48
  async get(root) {
38
49
  const data = await this.read();
39
50
  const repo = typeof root === 'string' && plain(data.repositories[root]) ? data.repositories[root] : {};
40
- const rawModel = data.global?.model;
41
- const model = plain(rawModel) && typeof rawModel.provider === 'string' && typeof rawModel.model === 'string' ? { provider: rawModel.provider, model: rawModel.model } : null;
51
+ const model = storedModel(data.global?.model);
52
+ const repositoryModel = storedModel(repo.model);
42
53
  return {
43
54
  globalInstruction: typeof data.global?.instruction === 'string' ? data.global.instruction : '',
44
55
  autoHideNonGit: data.global?.autoHideNonGit === true,
@@ -49,12 +60,13 @@ export class InstructionStore {
49
60
  repositoryInstruction: typeof repo.instruction === 'string' ? repo.instruction : '',
50
61
  inheritGlobal: repo.inheritGlobal !== false,
51
62
  model,
63
+ repositoryModel,
52
64
  };
53
65
  }
54
66
  async save(root, patch) {
55
- const allowed = ['globalInstruction', 'repositoryInstruction', 'inheritGlobal', 'model', 'autoHideNonGit', 'showTokenDetails', 'countdownColor', 'floatingPosition'];
67
+ const allowed = ['globalInstruction', 'repositoryInstruction', 'inheritGlobal', 'model', 'repositoryModel', 'autoHideNonGit', 'showTokenDetails', 'countdownColor', 'floatingPosition'];
56
68
  if (!plain(patch) || Object.keys(patch).some(key => !allowed.includes(key))) throw new Error('Unknown commit setting');
57
- if (root === null && ('repositoryInstruction' in patch || 'inheritGlobal' in patch)) {
69
+ if (root === null && ('repositoryInstruction' in patch || 'inheritGlobal' in patch || 'repositoryModel' in patch)) {
58
70
  throw new Error('Repository-specific settings require a Git repository');
59
71
  }
60
72
  if (root !== null && (typeof root !== 'string' || !root)) throw new Error('Invalid repository scope');
@@ -89,13 +101,8 @@ export class InstructionStore {
89
101
  if (typeof patch.inheritGlobal !== 'boolean') throw new Error('inheritGlobal must be a boolean');
90
102
  current.inheritGlobal = patch.inheritGlobal;
91
103
  }
92
- if ('model' in patch) {
93
- if (patch.model !== null && (!plain(patch.model) || !patch.model.provider || !patch.model.model)) throw new Error('Invalid model selection');
94
- global.model = patch.model === null ? null : {
95
- provider: string(patch.model.provider, 'model.provider', 200),
96
- model: string(patch.model.model, 'model.model', 400),
97
- };
98
- }
104
+ if ('model' in patch) global.model = checkedModel(patch.model, 'model');
105
+ if ('repositoryModel' in patch) current.model = checkedModel(patch.repositoryModel, 'repositoryModel');
99
106
  data.global = global;
100
107
  if (root !== null) data.repositories[root] = current;
101
108
  await mkdir(dirname(this.path), { recursive: true });
package/lib/index.d.ts CHANGED
@@ -51,7 +51,10 @@ export interface GitCommitSettings {
51
51
  countdownColor: string;
52
52
  /** Null means default lower-right anchor. Otherwise saved top/right coordinates in CSS pixels. */
53
53
  floatingPosition: {right:number;top:number} | null;
54
+ /** Global model selection, null means use the DSH default. */
54
55
  model: { provider: string; model: string } | null;
56
+ /** Repository-only override; null means inherit the global model selection (fixed UI option value). */
57
+ repositoryModel: { provider: string; model: string } | null;
55
58
  /** True for a Git working tree; false for global-only settings in ordinary directories. */
56
59
  isRepository: boolean;
57
60
  /** Canonical Git root, or null outside a repository. */
@@ -67,7 +70,7 @@ export interface GitCommitApi {
67
70
  >;
68
71
  changeToken(request: GitCommitRequest): Promise<{ token: number | null }>;
69
72
  settings(request?: GitCommitRequest): Promise<GitCommitSettings>;
70
- saveSettings(request: GitCommitRequest & { patch: Partial<Pick<GitCommitSettings, 'globalInstruction' | 'repositoryInstruction' | 'inheritGlobal' | 'model' | 'autoHideNonGit' | 'showTokenDetails' | 'countdownColor' | 'floatingPosition'>> }): Promise<GitCommitSettings>;
73
+ saveSettings(request: GitCommitRequest & { patch: Partial<Pick<GitCommitSettings, 'globalInstruction' | 'repositoryInstruction' | 'inheritGlobal' | 'model' | 'repositoryModel' | 'autoHideNonGit' | 'showTokenDetails' | 'countdownColor' | 'floatingPosition'>> }): Promise<GitCommitSettings>;
71
74
  models(): Promise<{ models: Array<{provider:string;providerName:string;model:string;name:string}>; defaultModel: {provider:string;model:string}|null }>;
72
75
  }
73
76
  declare module '@deepseek-ai/cordis' {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibobbyts/dsh-git-commit",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Floating Git stats and AI commits with native DSH LLM, authenticated RPC, settings and shared service",
6
6
  "license": "MIT",