@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 +1 -1
- package/lib/client.js +109 -44
- package/lib/core/service.js +6 -3
- package/lib/core/store.js +18 -11
- package/lib/index.d.ts +4 -1
- package/package.json +1 -1
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
|
-
-
|
|
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
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
h('
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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,
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
'
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
h('
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
'
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
);
|
package/lib/core/service.js
CHANGED
|
@@ -44,8 +44,11 @@ export class GitCommitService {
|
|
|
44
44
|
}
|
|
45
45
|
async saveSettings(request) {
|
|
46
46
|
const root = await this.settingsRoot(request);
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
41
|
-
const
|
|
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
|
-
|
|
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