@caozeal/dsh-next-git 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -64,6 +64,11 @@ window.__ModuleLoader__.load({
64
64
  'changes.unstageAll': '全部取消暂存',
65
65
  'changes.stage': '暂存',
66
66
  'changes.unstage': '取消暂存',
67
+ 'changes.discard': '丢弃改动',
68
+ 'changes.discardUntracked': '删除文件',
69
+ 'changes.discardConfirm': '丢弃 {path} 的改动?此操作不可恢复。',
70
+ 'changes.discardUntrackedConfirm': '删除未跟踪文件 {path}?此操作不可恢复。',
71
+ 'changes.discardStagedConfirm': '丢弃 {path} 的暂存与工作区改动?此操作不可恢复。',
67
72
  'commit.title': '提交信息',
68
73
  'commit.placeholder': '填写提交信息,或先点「AI 生成信息」',
69
74
  'commit.hintPlaceholder': '给 AI 的补充说明(可选)',
@@ -75,6 +80,13 @@ window.__ModuleLoader__.load({
75
80
  'commit.push': '提交并推送',
76
81
  'commit.messageRequired': '请先填写提交信息',
77
82
  'commit.empty': '没有可提交的暂存变更',
83
+ 'commit.prompt': 'AI 提示词',
84
+ 'commit.promptTitle': '自定义 AI 提交提示词',
85
+ 'commit.promptHint': '留空使用默认提示词。这里只改「怎么写」,diff 与文件列表始终会传给模型。',
86
+ 'commit.promptSave': '保存',
87
+ 'commit.promptReset': '恢复默认',
88
+ 'commit.promptCancel': '取消',
89
+ 'commit.promptCustom': '自定义提示词',
78
90
  'tabs.changes': '变更',
79
91
  'tabs.graph': '提交图',
80
92
  'tabs.branches': '分支',
@@ -112,6 +124,12 @@ window.__ModuleLoader__.load({
112
124
  'status.branchCreated': '已创建分支 {branch}',
113
125
  'status.branchDeleted': '已删除分支 {branch}',
114
126
  'status.deleteConfirm': '确定删除分支 {branch}?',
127
+ 'status.discardOk': '已丢弃 {path}',
128
+ 'status.stashSaveOk': '已 stash 当前改动',
129
+ 'status.stashPopOk': '已恢复 stash@{n}',
130
+ 'status.stashDropOk': '已删除 stash@{n}',
131
+ 'status.promptSaved': '提示词已保存',
132
+ 'status.promptReset': '已恢复默认提示词',
115
133
  'status.aiModel': '由 {model} 生成',
116
134
  'status.aiFallback': '模型输出不可用,已生成降级文案',
117
135
  'toast.error': '失败',
@@ -131,6 +149,14 @@ window.__ModuleLoader__.load({
131
149
  'status.aiModelEdit': '由 {model} 生成,可继续编辑',
132
150
  'status.ready': '就绪',
133
151
  'changes.stagedCount': '已暂存 {n}',
152
+ 'stash.title': 'Stash',
153
+ 'stash.empty': '没有 stash 记录',
154
+ 'stash.placeholder': 'stash 说明(可选)',
155
+ 'stash.save': '暂存改动',
156
+ 'stash.pop': '恢复',
157
+ 'stash.drop': '删除',
158
+ 'stash.popConfirm': '恢复 stash@{n} 并从列表移除?',
159
+ 'stash.dropConfirm': '删除 stash@{n}?此操作不可恢复。',
134
160
  'changes.conflictBanner': '存在 {n} 个冲突文件,解决后再提交',
135
161
  'branch.selectTitle': '切换分支',
136
162
  'header.refreshing': '刷新中…',
@@ -175,6 +201,11 @@ window.__ModuleLoader__.load({
175
201
  'changes.unstageAll': 'Unstage all',
176
202
  'changes.stage': 'Stage',
177
203
  'changes.unstage': 'Unstage',
204
+ 'changes.discard': 'Discard changes',
205
+ 'changes.discardUntracked': 'Delete file',
206
+ 'changes.discardConfirm': 'Discard changes to {path}? This cannot be undone.',
207
+ 'changes.discardUntrackedConfirm': 'Delete untracked file {path}? This cannot be undone.',
208
+ 'changes.discardStagedConfirm': 'Discard staged and working-tree changes to {path}? This cannot be undone.',
178
209
  'commit.title': 'Commit message',
179
210
  'commit.placeholder': 'Write a commit message, or generate one with AI',
180
211
  'commit.hintPlaceholder': 'Optional hint for the AI',
@@ -186,6 +217,13 @@ window.__ModuleLoader__.load({
186
217
  'commit.push': 'Commit & push',
187
218
  'commit.messageRequired': 'Commit message is required',
188
219
  'commit.empty': 'Nothing staged to commit',
220
+ 'commit.prompt': 'AI prompt',
221
+ 'commit.promptTitle': 'Custom AI commit prompt',
222
+ 'commit.promptHint': 'Leave empty to use the default. This only changes how it writes; the diff and file list are always sent.',
223
+ 'commit.promptSave': 'Save',
224
+ 'commit.promptReset': 'Reset to default',
225
+ 'commit.promptCancel': 'Cancel',
226
+ 'commit.promptCustom': 'Custom prompt',
189
227
  'tabs.changes': 'Changes',
190
228
  'tabs.graph': 'Graph',
191
229
  'tabs.branches': 'Branches',
@@ -223,6 +261,12 @@ window.__ModuleLoader__.load({
223
261
  'status.branchCreated': 'Created branch {branch}',
224
262
  'status.branchDeleted': 'Deleted branch {branch}',
225
263
  'status.deleteConfirm': 'Delete branch {branch}?',
264
+ 'status.discardOk': 'Discarded {path}',
265
+ 'status.stashSaveOk': 'Stashed current changes',
266
+ 'status.stashPopOk': 'Restored stash@{n}',
267
+ 'status.stashDropOk': 'Dropped stash@{n}',
268
+ 'status.promptSaved': 'Prompt saved',
269
+ 'status.promptReset': 'Restored the default prompt',
226
270
  'status.aiModel': 'Generated by {model}',
227
271
  'status.aiFallback': 'Model output unusable; used a fallback message',
228
272
  'toast.error': 'Failed',
@@ -242,6 +286,14 @@ window.__ModuleLoader__.load({
242
286
  'status.aiModelEdit': 'Generated by {model}; edit as needed',
243
287
  'status.ready': 'Ready',
244
288
  'changes.stagedCount': '{n} staged',
289
+ 'stash.title': 'Stash',
290
+ 'stash.empty': 'No stash entries',
291
+ 'stash.placeholder': 'Stash message (optional)',
292
+ 'stash.save': 'Stash changes',
293
+ 'stash.pop': 'Restore',
294
+ 'stash.drop': 'Drop',
295
+ 'stash.popConfirm': 'Restore stash@{n} and remove it from the list?',
296
+ 'stash.dropConfirm': 'Drop stash@{n}? This cannot be undone.',
245
297
  'changes.conflictBanner': '{n} conflicted file(s); resolve them before committing',
246
298
  'branch.selectTitle': 'Switch branch',
247
299
  'header.refreshing': 'Refreshing…',
@@ -277,6 +329,16 @@ window.__ModuleLoader__.load({
277
329
  --gm-blue-18: color-mix(in srgb, var(--gm-blue) 18%, transparent);
278
330
  --gm-blue-28: color-mix(in srgb, var(--gm-blue) 28%, transparent);
279
331
  --gm-blue-40: color-mix(in srgb, var(--gm-blue) 40%, transparent);
332
+ /* 提交图 lane 调色板:8 色,相邻不撞色;每条分支线跨行保持同色。
333
+ 色相刻意错开(蓝/紫/青/绿/橙/粉/黄/灰),深浅色主题下都够辨识。 */
334
+ --gm-lane-0: #4d6bfe;
335
+ --gm-lane-1: #8b5cf6;
336
+ --gm-lane-2: #06b6d4;
337
+ --gm-lane-3: #22c55e;
338
+ --gm-lane-4: #f59e0b;
339
+ --gm-lane-5: #ec4899;
340
+ --gm-lane-6: #14b8a6;
341
+ --gm-lane-7: #a855f7;
280
342
  --gm-ok: var(--dsw-alias-state-success-primary);
281
343
  --gm-err: var(--dsw-alias-state-error-primary);
282
344
  --gm-warn: var(--dsw-alias-state-warn-primary);
@@ -414,10 +476,10 @@ window.__ModuleLoader__.load({
414
476
  .gm-glane { flex:none; overflow:hidden;
415
477
  -webkit-mask-image:linear-gradient(90deg, #000 88%, transparent 100%);
416
478
  mask-image:linear-gradient(90deg, #000 88%, transparent 100%); }
417
- .gm-glane line { stroke:var(--gm-blue); stroke-width:1.6; stroke-linecap:round; opacity:.65; }
418
- .gm-glane circle { fill:var(--gm-blue); stroke:var(--gm-bg); stroke-width:1.4; }
419
- .gm-grow.active .gm-glane line, .gm-grow:hover .gm-glane line { opacity:.95; }
420
- .gm-grow.active .gm-glane circle, .gm-grow:hover .gm-glane circle { fill:var(--gm-deep); }
479
+ .gm-glane line { stroke:var(--gm-blue); stroke-width:1.7; stroke-linecap:round; opacity:.78; }
480
+ .gm-glane circle { fill:var(--gm-blue); stroke:var(--gm-card); stroke-width:1.5; }
481
+ .gm-grow.active .gm-glane line, .gm-grow:hover .gm-glane line { opacity:1; }
482
+ .gm-grow.active .gm-glane circle { stroke:var(--gm-base); stroke-width:2; }
421
483
  .gm-hash { flex:none; font-size:10.5px; color:var(--gm-blue); opacity:.75; }
422
484
  .gm-subject { flex:1 1 auto; min-width:56px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-family:inherit; }
423
485
  .gm-meta { flex:none; max-width:145px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
@@ -445,15 +507,23 @@ window.__ModuleLoader__.load({
445
507
  border:1px solid var(--gm-blue-18); display:inline-block; max-width:calc(100% - 24px);
446
508
  font-family:${mono}; font-size:10.5px; color:var(--gm-blue); white-space:pre-wrap; }
447
509
  .gm-dlines { font-family:${mono}; font-size:11.5px; line-height:1.62; margin:0 6px 6px; }
448
- .gm-dline { display:flex; align-items:baseline; white-space:pre-wrap; word-break:break-all; border-radius:4px; }
449
- .gm-dline.add { background:color-mix(in srgb, var(--gm-ok) 10%, transparent); }
450
- .gm-dline.del { background:color-mix(in srgb, var(--gm-err) 10%, transparent); }
510
+ .gm-dline { display:flex; align-items:baseline; white-space:pre-wrap; word-break:break-word; border-radius:4px; }
511
+ /* 增删底色优先用平台自己的 diff 配色(--dsw-alias-file-diff-*),深浅色主题自动跟随;
512
+ 主题未注入时退回原来的 color-mix 兜底。 */
513
+ .gm-dline.add { background:var(--dsw-alias-file-diff-added-bg, color-mix(in srgb, var(--gm-ok) 10%, transparent)); }
514
+ .gm-dline.del { background:var(--dsw-alias-file-diff-deleted-bg, color-mix(in srgb, var(--gm-err) 10%, transparent)); }
451
515
  .gm-ln { width:38px; flex:none; text-align:right; padding-right:8px; color:var(--gm-dim);
452
- opacity:.6; user-select:none; font-variant-numeric:tabular-nums; }
516
+ opacity:.6; user-select:none; font-variant-numeric:tabular-nums; border-radius:3px; }
517
+ .gm-dline.add .gm-ln { background:var(--dsw-alias-file-diff-added-gutter, transparent); }
518
+ .gm-dline.del .gm-ln { background:var(--dsw-alias-file-diff-deleted-gutter, transparent); }
453
519
  .gm-sign { width:14px; flex:none; text-align:center; font-weight:700; }
454
- .gm-dline.add .gm-sign { color:var(--gm-ok); }
455
- .gm-dline.del .gm-sign { color:var(--gm-err); }
520
+ .gm-dline.add .gm-sign { color:var(--dsw-alias-file-diff-added-marker, var(--gm-ok)); }
521
+ .gm-dline.del .gm-sign { color:var(--dsw-alias-file-diff-deleted-marker, var(--gm-err)); }
456
522
  .gm-dtext { flex:1 1 auto; min-width:0; padding-right:10px; }
523
+ /* 词级高亮:只有真正变化的 token 才加深底色,一眼看出「改了哪个词」。 */
524
+ .gm-dword { border-radius:3px; padding:0 1px; }
525
+ .gm-dword.add { background:color-mix(in srgb, var(--dsw-alias-file-diff-added-marker, var(--gm-ok)) 34%, transparent); }
526
+ .gm-dword.del { background:color-mix(in srgb, var(--dsw-alias-file-diff-deleted-marker, var(--gm-err)) 34%, transparent); }
457
527
  .gm-meta-line { padding:2px 12px; color:var(--gm-dim); font-style:italic; }
458
528
 
459
529
  /* ---- commit card --------------------------------------------------- */
@@ -464,6 +534,11 @@ window.__ModuleLoader__.load({
464
534
  .gm-commit::before { content:''; position:absolute; top:0; left:0; right:0; height:3px;
465
535
  background:linear-gradient(90deg, var(--gm-cyan), var(--gm-blue) 45%, transparent 90%); }
466
536
  .gm-commit-head { display:flex; align-items:center; gap:6px; }
537
+ .gm-prompt { display:flex; flex-direction:column; gap:5px; padding:8px; border-radius:10px;
538
+ background:var(--gm-blue-06); border:1px solid var(--gm-blue-12); }
539
+ .gm-prompt .gm-section { padding:0; }
540
+ .gm-prompt-text { min-height:96px; max-height:180px; font-size:11px;
541
+ font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
467
542
  .gm-commit-title { font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
468
543
  color:var(--gm-blue); }
469
544
  .gm-textarea { width:100%; min-height:46px; max-height:140px; padding:7px 9px; border-radius:10px;
@@ -589,6 +664,9 @@ window.__ModuleLoader__.load({
589
664
  commit: ['M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16z', 'M8.5 12l2.5 2.5L16 9.5'],
590
665
  file: ['M6 3h8l4 4v14H6z', 'M14 3v5h5'],
591
666
  branch: ['M6 4v9', 'M6 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0-4 0', 'M18 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0-4 0', 'M18 8v1a6 6 0 0 1-6 6H8'],
667
+ trash: ['M4 7h16', 'M9 7V4h6v3', 'M6 7l1 14h10l1-14', 'M10 11v6', 'M14 11v6'],
668
+ gear: ['M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z', 'M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.9.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.3-1.9l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z'],
669
+ stash: ['M4 5h16v4H4z', 'M6 9v10h12V9', 'M10 13h4'],
592
670
  }
593
671
 
594
672
  function Icon(props) {
@@ -772,6 +850,11 @@ window.__ModuleLoader__.load({
772
850
  newBranchOpen: false,
773
851
  newBranchName: '',
774
852
  newBranchFrom: '',
853
+ // 自定义 AI 提示词编辑态。promptDraft 是编辑中的草稿,
854
+ // 只有点保存才写回 git config,避免边打字边写盘。
855
+ promptOpen: false,
856
+ promptDraft: '',
857
+ stashMessage: '',
775
858
  })
776
859
  var toastTimer = null
777
860
  var loadToken = 0
@@ -891,6 +974,10 @@ window.__ModuleLoader__.load({
891
974
  }
892
975
 
893
976
  function setNewBranchName(value) { set({ newBranchName: value }) }
977
+ function setStashMessage(value) {
978
+ set({ stashMessage: String(value || '') })
979
+ }
980
+
894
981
  function setNewBranchFrom(value) { set({ newBranchFrom: value }) }
895
982
 
896
983
  async function runBusy(label, operation) {
@@ -963,6 +1050,117 @@ window.__ModuleLoader__.load({
963
1050
  }
964
1051
  }
965
1052
 
1053
+ /**
1054
+ * 丢弃改动。分组的语义决定用哪个后端模式:
1055
+ * unstaged → worktree(只回退工作区)
1056
+ * untracked → untracked(删除文件)
1057
+ * staged → all(连暂存区一起回退)
1058
+ * 三种都不可恢复,所以一律先 window.confirm。
1059
+ */
1060
+ async function discard(group, entry) {
1061
+ var cwd = store.get().cwd
1062
+ if (!cwd || !entry || !entry.path) return
1063
+ var mode = group === 'untracked' ? 'untracked' : group === 'staged' ? 'all' : 'worktree'
1064
+ var key = group === 'untracked'
1065
+ ? 'changes.discardUntrackedConfirm'
1066
+ : group === 'staged' ? 'changes.discardStagedConfirm' : 'changes.discardConfirm'
1067
+ if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
1068
+ if (!window.confirm(tr(key, { path: entry.path }))) return
1069
+ }
1070
+ var result = await runBusy('discard', function () {
1071
+ return rpc('discard', { cwd: cwd, paths: [entry.path], mode: mode })
1072
+ })
1073
+ if (result !== null) {
1074
+ toast('ok', tr('status.discardOk', { path: entry.path }), result.output)
1075
+ set({ selected: null, diff: null })
1076
+ await refresh()
1077
+ }
1078
+ }
1079
+
1080
+ async function stashSave() {
1081
+ var state = store.get()
1082
+ var cwd = state.cwd
1083
+ if (!cwd) return
1084
+ var params = { cwd: cwd }
1085
+ var message = String(state.stashMessage || '').trim()
1086
+ if (message !== '') params.message = message
1087
+ // 未跟踪文件默认不进 stash;有未跟踪改动时一并存起来,否则用户会以为「全存了」
1088
+ var status = state.snapshot && state.snapshot.status ? state.snapshot.status : null
1089
+ if (status && status.untracked && status.untracked.length > 0) params.includeUntracked = true
1090
+ var result = await runBusy('stash', function () { return rpc('stashPush', params) })
1091
+ if (result !== null) {
1092
+ set({ stashMessage: '' })
1093
+ toast('ok', tr('status.stashSaveOk'), result.output)
1094
+ await refresh()
1095
+ }
1096
+ }
1097
+
1098
+ async function stashPop(index) {
1099
+ var cwd = store.get().cwd
1100
+ if (!cwd) return
1101
+ if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
1102
+ if (!window.confirm(tr('stash.popConfirm', { n: index }))) return
1103
+ }
1104
+ var result = await runBusy('stash', function () { return rpc('stashPop', { cwd: cwd, index: index }) })
1105
+ if (result !== null) {
1106
+ toast('ok', tr('status.stashPopOk', { n: index }), result.output)
1107
+ await refresh()
1108
+ }
1109
+ }
1110
+
1111
+ async function stashDrop(index) {
1112
+ var cwd = store.get().cwd
1113
+ if (!cwd) return
1114
+ if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
1115
+ if (!window.confirm(tr('stash.dropConfirm', { n: index }))) return
1116
+ }
1117
+ var result = await runBusy('stash', function () { return rpc('stashDrop', { cwd: cwd, index: index }) })
1118
+ if (result !== null) {
1119
+ toast('ok', tr('status.stashDropOk', { n: index }), result.output)
1120
+ await refresh()
1121
+ }
1122
+ }
1123
+
1124
+ function openPrompt() {
1125
+ var snapshot = store.get().snapshot
1126
+ set({ promptOpen: true, promptDraft: snapshot && snapshot.commitPrompt ? snapshot.commitPrompt : '' })
1127
+ }
1128
+
1129
+ function closePrompt() {
1130
+ set({ promptOpen: false, promptDraft: '' })
1131
+ }
1132
+
1133
+ function setPromptDraft(value) {
1134
+ set({ promptDraft: String(value || '') })
1135
+ }
1136
+
1137
+ async function savePrompt() {
1138
+ var state = store.get()
1139
+ var cwd = state.cwd
1140
+ if (!cwd) return
1141
+ var result = await runBusy('prompt', function () {
1142
+ return rpc('setCommitPrompt', { cwd: cwd, prompt: String(state.promptDraft || '') })
1143
+ })
1144
+ if (result !== null) {
1145
+ set({ promptOpen: false, promptDraft: '' })
1146
+ toast('ok', tr('status.promptSaved'))
1147
+ await refresh()
1148
+ }
1149
+ }
1150
+
1151
+ async function resetPrompt() {
1152
+ var cwd = store.get().cwd
1153
+ if (!cwd) return
1154
+ var result = await runBusy('prompt', function () {
1155
+ return rpc('setCommitPrompt', { cwd: cwd, prompt: '' })
1156
+ })
1157
+ if (result !== null) {
1158
+ set({ promptOpen: false, promptDraft: '' })
1159
+ toast('ok', tr('status.promptReset'))
1160
+ await refresh()
1161
+ }
1162
+ }
1163
+
966
1164
  async function switchBranch(name) {
967
1165
  var cwd = store.get().cwd
968
1166
  if (!cwd || !name) return
@@ -1087,6 +1285,16 @@ window.__ModuleLoader__.load({
1087
1285
  setNewBranch: setNewBranch,
1088
1286
  setNewBranchName: setNewBranchName,
1089
1287
  setNewBranchFrom: setNewBranchFrom,
1288
+ discard: discard,
1289
+ stashSave: stashSave,
1290
+ stashPop: stashPop,
1291
+ stashDrop: stashDrop,
1292
+ setStashMessage: setStashMessage,
1293
+ openPrompt: openPrompt,
1294
+ closePrompt: closePrompt,
1295
+ setPromptDraft: setPromptDraft,
1296
+ savePrompt: savePrompt,
1297
+ resetPrompt: resetPrompt,
1090
1298
  clearToast: clearToast,
1091
1299
  }
1092
1300
  }
@@ -1144,16 +1352,29 @@ window.__ModuleLoader__.load({
1144
1352
  h('span', { className: 'gm-fpath' },
1145
1353
  parts.dir ? h('span', { className: 'gm-dir' }, parts.dir) : null,
1146
1354
  parts.base),
1147
- h('button', {
1148
- type: 'button',
1149
- className: 'gm-btn icon sm ghost gm-act',
1150
- title: staged ? tr('changes.unstage') : tr('changes.stage'),
1151
- onClick: function (event) {
1152
- event.stopPropagation()
1153
- if (staged) props.onUnstage(entry)
1154
- else props.onStage(entry)
1155
- },
1156
- }, h(Icon, { name: staged ? 'minus' : 'plus', size: 12 })))
1355
+ h('span', { className: 'gm-row', style: { gap: '2px' } },
1356
+ h('button', {
1357
+ type: 'button',
1358
+ className: 'gm-btn icon sm ghost gm-act',
1359
+ title: staged ? tr('changes.unstage') : tr('changes.stage'),
1360
+ onClick: function (event) {
1361
+ event.stopPropagation()
1362
+ if (staged) props.onUnstage(entry)
1363
+ else props.onStage(entry)
1364
+ },
1365
+ }, h(Icon, { name: staged ? 'minus' : 'plus', size: 12 })),
1366
+ // 冲突文件不给「丢弃」:那等于放弃冲突解决状态,语义上容易误伤
1367
+ props.onDiscard && props.group !== 'conflicted'
1368
+ ? h('button', {
1369
+ type: 'button',
1370
+ className: 'gm-btn icon sm ghost gm-act',
1371
+ title: props.group === 'untracked' ? tr('changes.discardUntracked') : tr('changes.discard'),
1372
+ onClick: function (event) {
1373
+ event.stopPropagation()
1374
+ props.onDiscard(entry)
1375
+ },
1376
+ }, h(Icon, { name: 'trash', size: 12 }))
1377
+ : null))
1157
1378
  }))
1158
1379
  }
1159
1380
 
@@ -1176,6 +1397,7 @@ window.__ModuleLoader__.load({
1176
1397
  onSelect: function (entry) { props.actions.selectFile(group, entry) },
1177
1398
  onStage: function (entry) { props.actions.stage(group, entry) },
1178
1399
  onUnstage: function (entry) { props.actions.unstage(group, entry) },
1400
+ onDiscard: function (entry) { props.actions.discard(group, entry) },
1179
1401
  })
1180
1402
  }
1181
1403
  var empty = !conflicted.length && !staged.length && !unstaged.length && !untracked.length
@@ -1208,6 +1430,29 @@ window.__ModuleLoader__.load({
1208
1430
  }
1209
1431
  }
1210
1432
  return h('div', { className: 'gm-commit' },
1433
+ state.promptOpen
1434
+ ? h('div', { className: 'gm-prompt' },
1435
+ h('div', { className: 'gm-section' },
1436
+ h('span', null, tr('commit.promptTitle')),
1437
+ h('span', { className: 'gm-spacer' }),
1438
+ h('button', {
1439
+ type: 'button',
1440
+ className: 'gm-btn icon sm ghost',
1441
+ title: tr('commit.promptCancel'),
1442
+ onClick: props.onClosePrompt,
1443
+ }, h(Icon, { name: 'close', size: 12 }))),
1444
+ h('textarea', {
1445
+ className: 'gm-textarea gm-prompt-text',
1446
+ value: state.promptDraft,
1447
+ placeholder: tr('commit.promptHint'),
1448
+ onChange: function (event) { props.onPromptDraft(event.target.value) },
1449
+ }),
1450
+ h('div', { className: 'gm-hint' }, tr('commit.promptHint')),
1451
+ h('div', { className: 'gm-actions' },
1452
+ h(Btn, { label: tr('commit.promptReset'), small: true, ghost: true, disabled: !!busy, onClick: props.onResetPrompt }),
1453
+ h('span', { className: 'gm-spacer' }),
1454
+ h(Btn, { label: tr('commit.promptSave'), small: true, variant: 'primary', disabled: !!busy, onClick: props.onSavePrompt })))
1455
+ : null,
1211
1456
  h('div', { className: 'gm-commit-head' },
1212
1457
  h(WhaleGlyph, { size: 22 }),
1213
1458
  h('span', { className: 'gm-commit-title' }, tr('commit.title')),
@@ -1236,8 +1481,17 @@ window.__ModuleLoader__.load({
1236
1481
  className: busy === 'ai' ? 'gm-ai-busy' : undefined,
1237
1482
  disabled: !!busy || !state.cwd,
1238
1483
  onClick: props.onGenerate,
1484
+ }),
1485
+ h(Btn, {
1486
+ icon: 'gear',
1487
+ title: tr('commit.prompt'),
1488
+ disabled: !!busy || !state.cwd,
1489
+ onClick: props.onOpenPrompt,
1239
1490
  })),
1240
1491
  h('div', { className: 'gm-actions' },
1492
+ state.snapshot && state.snapshot.commitPrompt
1493
+ ? h('span', { className: 'gm-badge brand', title: state.snapshot.commitPrompt }, tr('commit.promptCustom'))
1494
+ : null,
1241
1495
  state.aiModel
1242
1496
  ? h('span', { className: 'gm-ai-line gm-hint', title: tr('status.aiModel', { model: state.aiModel }) },
1243
1497
  state.aiFallback ? tr('status.aiFallback') : tr('status.aiModelEdit', { model: state.aiModel }))
@@ -1360,6 +1614,7 @@ window.__ModuleLoader__.load({
1360
1614
  prev: index > 0 ? (lines[index - 1].graph || '') : '',
1361
1615
  next: index + 1 < lines.length ? (lines[index + 1].graph || '') : '',
1362
1616
  cols: graphCols,
1617
+ colors: line.colors || [],
1363
1618
  }))
1364
1619
  }
1365
1620
  var selected = state.selected && state.selected.kind === 'commit' && state.selected.hash === line.full
@@ -1374,13 +1629,21 @@ window.__ModuleLoader__.load({
1374
1629
  prev: index > 0 ? (lines[index - 1].graph || '') : '',
1375
1630
  next: index + 1 < lines.length ? (lines[index + 1].graph || '') : '',
1376
1631
  cols: graphCols,
1632
+ colors: line.colors || [],
1377
1633
  }),
1378
1634
  h('span', { className: 'gm-hash' }, line.hash),
1379
1635
  line.refs && line.refs.length > 0
1380
1636
  ? h('span', { className: 'gm-row', style: { gap: '4px', flex: 'none' } }, line.refs.map(function (ref, refIndex) {
1637
+ var isHead = ref.indexOf('HEAD') === 0
1638
+ // HEAD 用渐变主色强调;其余 ref 用分支自己的 lane 色,和左侧线条同色。
1381
1639
  return h('span', {
1382
1640
  key: 'r:' + refIndex,
1383
- className: 'gm-badge gm-ref' + (ref.indexOf('HEAD') === 0 ? ' brand' : ''),
1641
+ className: 'gm-badge gm-ref' + (isHead ? ' brand' : ''),
1642
+ style: isHead ? undefined : {
1643
+ color: 'var(--gm-lane-' + (line.refColor !== undefined ? line.refColor : 0) + ')',
1644
+ borderColor: 'color-mix(in srgb, var(--gm-lane-' + (line.refColor !== undefined ? line.refColor : 0) + ') 42%, transparent)',
1645
+ background: 'color-mix(in srgb, var(--gm-lane-' + (line.refColor !== undefined ? line.refColor : 0) + ') 12%, transparent)',
1646
+ },
1384
1647
  }, ref)
1385
1648
  }))
1386
1649
  : null,
@@ -1418,6 +1681,12 @@ window.__ModuleLoader__.load({
1418
1681
  * Draws one row of the commit graph as vector lanes, so vertical lines stay
1419
1682
  * continuous across rows (ASCII glyphs leave gaps at 20px row heights).
1420
1683
  */
1684
+ /** lane 色号 → CSS 变量名;-1(该格无连接)返回 undefined 让样式回退。 */
1685
+ function laneVar(color) {
1686
+ if (color === undefined || color === null || color < 0) return undefined
1687
+ return 'var(--gm-lane-' + color + ')'
1688
+ }
1689
+
1421
1690
  function GraphLane(props) {
1422
1691
  var graph = props.graph || ''
1423
1692
  var prev = props.prev || ''
@@ -1425,22 +1694,26 @@ window.__ModuleLoader__.load({
1425
1694
  var cols = props.cols
1426
1695
  var cell = GRAPH_CELL
1427
1696
  var row = GRAPH_ROW
1697
+ var colors = props.colors || []
1428
1698
  var parts = []
1429
1699
  for (var i = 0; i < cols; i += 1) {
1430
1700
  var ch = laneChar(graph, i)
1431
1701
  var cx = (i + 0.5) * cell
1702
+ // 同一条分支线跨行保持同色:颜色由 Host 端 assignGraphColors 沿线段流动算出。
1703
+ var stroke = { stroke: laneVar(colors[i]) }
1432
1704
  if (ch === '|') {
1433
- parts.push(h('line', { key: 'v' + i, x1: cx, y1: 0, x2: cx, y2: row }))
1705
+ parts.push(h('line', { key: 'v' + i, x1: cx, y1: 0, x2: cx, y2: row, style: stroke }))
1434
1706
  } else if (ch === '/') {
1435
- parts.push(h('line', { key: 's' + i, x1: cx, y1: 0, x2: cx - cell, y2: row }))
1707
+ parts.push(h('line', { key: 's' + i, x1: cx, y1: 0, x2: cx - cell, y2: row, style: stroke }))
1436
1708
  } else if (ch === '\\') {
1437
- parts.push(h('line', { key: 'b' + i, x1: cx - cell, y1: 0, x2: cx, y2: row }))
1709
+ parts.push(h('line', { key: 'b' + i, x1: cx - cell, y1: 0, x2: cx, y2: row, style: stroke }))
1438
1710
  } else if (ch === '_') {
1439
- parts.push(h('line', { key: 'h' + i, x1: cx, y1: row - 1, x2: cx + cell, y2: row - 1 }))
1711
+ parts.push(h('line', { key: 'h' + i, x1: cx, y1: row - 1, x2: cx + cell, y2: row - 1, style: stroke }))
1440
1712
  } else if (ch === '*') {
1441
- if (laneFromAbove(prev, i)) parts.push(h('line', { key: 'u' + i, x1: cx, y1: 0, x2: cx, y2: row / 2 }))
1442
- if (laneToBelow(next, i)) parts.push(h('line', { key: 'd' + i, x1: cx, y1: row / 2, x2: cx, y2: row }))
1443
- parts.push(h('circle', { key: 'c' + i, cx: cx, cy: row / 2, r: 3.2 }))
1713
+ if (laneFromAbove(prev, i)) parts.push(h('line', { key: 'u' + i, x1: cx, y1: 0, x2: cx, y2: row / 2, style: stroke }))
1714
+ if (laneToBelow(next, i)) parts.push(h('line', { key: 'd' + i, x1: cx, y1: row / 2, x2: cx, y2: row, style: stroke }))
1715
+ parts.push(h('circle', { key: 'c' + i, cx: cx, cy: row / 2, r: 3.4,
1716
+ style: { fill: laneVar(colors[i]) } }))
1444
1717
  }
1445
1718
  }
1446
1719
  return h('svg', {
@@ -1449,6 +1722,18 @@ window.__ModuleLoader__.load({
1449
1722
  }, parts)
1450
1723
  }
1451
1724
 
1725
+ /**
1726
+ * 渲染一行的文本部分;Host 在解析 diff 时已算好词级片段(line.inline),
1727
+ * 有就只把变化的片段包成 span,没有就退回整段纯文本。
1728
+ */
1729
+ function inlineSpans(line) {
1730
+ if (!line.inline || line.inline.length === 0) return line.text
1731
+ return line.inline.map(function (part, index) {
1732
+ if (!part.changed) return part.text
1733
+ return h('span', { key: 'i:' + index, className: 'gm-dword' + (line.kind === 'del' ? ' del' : ' add') }, part.text)
1734
+ })
1735
+ }
1736
+
1452
1737
  function DiffView(props) {
1453
1738
  var state = props.state
1454
1739
  var diff = state.diff
@@ -1480,7 +1765,7 @@ window.__ModuleLoader__.load({
1480
1765
  h('span', { className: 'gm-ln' }, line.old === undefined ? '' : String(line.old)),
1481
1766
  h('span', { className: 'gm-ln' }, line.new === undefined ? '' : String(line.new)),
1482
1767
  h('span', { className: 'gm-sign' }, sign),
1483
- h('span', { className: 'gm-dtext' }, line.text))
1768
+ h('span', { className: 'gm-dtext' }, inlineSpans(line)))
1484
1769
  })))
1485
1770
  })))
1486
1771
  }
@@ -1490,6 +1775,7 @@ window.__ModuleLoader__.load({
1490
1775
  var branches = state.snapshot && state.snapshot.branches ? state.snapshot.branches : []
1491
1776
  var locals = branches.filter(function (branch) { return branch.kind === 'local' })
1492
1777
  var remotes = branches.filter(function (branch) { return branch.kind === 'remote' })
1778
+ var stashes = state.snapshot && state.snapshot.stashes ? state.snapshot.stashes : []
1493
1779
  function group(title, items, allowDelete) {
1494
1780
  if (items.length === 0) return null
1495
1781
  return h('div', null,
@@ -1515,13 +1801,42 @@ window.__ModuleLoader__.load({
1515
1801
  : null))
1516
1802
  }))
1517
1803
  }
1804
+ // stash 放在分支页:它和分支一样是「当前工作放哪儿」的操作,
1805
+ // 而不是某次提交的内容。后端 snapshot 早就解析了 stashes,这里才真正用上。
1806
+ function stashSection() {
1807
+ return h('div', null,
1808
+ h('div', { className: 'gm-section' },
1809
+ h('span', null, tr('stash.title')),
1810
+ h('span', { className: 'gm-section-count' }, String(stashes.length))),
1811
+ h('div', { className: 'gm-row', style: { padding: '0 10px 6px' } },
1812
+ h('input', {
1813
+ className: 'gm-input',
1814
+ value: state.stashMessage,
1815
+ placeholder: tr('stash.placeholder'),
1816
+ onChange: function (event) { props.onStashMessage(event.target.value) },
1817
+ onKeyDown: function (event) { if (event.key === 'Enter') props.onStashSave() },
1818
+ }),
1819
+ h(Btn, { label: tr('stash.save'), small: true, disabled: !!state.busy, onClick: props.onStashSave })),
1820
+ stashes.length === 0
1821
+ ? h('div', { className: 'gm-hint', style: { padding: '0 12px 8px' } }, tr('stash.empty'))
1822
+ : stashes.map(function (item) {
1823
+ return h('div', { key: 'stash:' + item.index, className: 'gm-branch' },
1824
+ h(Icon, { name: 'stash', size: 12 }),
1825
+ h('span', { className: 'gm-badge' }, 'stash@{' + item.index + '}'),
1826
+ h('span', { className: 'gm-bmeta' }, item.message || ''),
1827
+ h('span', { className: 'gm-row gm-act', style: { gap: '4px', flex: 'none' } },
1828
+ h(Btn, { label: tr('stash.pop'), small: true, disabled: !!state.busy, onClick: function () { props.onStashPop(item.index) } }),
1829
+ h(Btn, { label: tr('stash.drop'), small: true, ghost: true, disabled: !!state.busy, onClick: function () { props.onStashDrop(item.index) } })))
1830
+ }))
1831
+ }
1518
1832
  return h('div', { className: 'gm-scroll' },
1519
1833
  h('div', { className: 'gm-content-inner' },
1520
1834
  group(tr('branch.local'), locals, true),
1521
1835
  group(tr('branch.remote'), remotes, false),
1522
1836
  locals.length === 0 && remotes.length === 0
1523
1837
  ? h(EmptyState, { title: tr('branch.empty') })
1524
- : null))
1838
+ : null,
1839
+ stashSection()))
1525
1840
  }
1526
1841
 
1527
1842
  function StatusBar(props) {
@@ -1580,7 +1895,15 @@ window.__ModuleLoader__.load({
1580
1895
  var content
1581
1896
  if (state.activeTab === 'changes') content = h(ChangesPane, { state: state, actions: props.actions })
1582
1897
  else if (state.activeTab === 'graph') content = h(GraphView, { state: state, onSelectCommit: props.actions.selectCommit })
1583
- else if (state.activeTab === 'branches') content = h(BranchesView, { state: state, onSwitch: props.actions.switchBranch, onDelete: props.actions.deleteBranch })
1898
+ else if (state.activeTab === 'branches') content = h(BranchesView, {
1899
+ state: state,
1900
+ onSwitch: props.actions.switchBranch,
1901
+ onDelete: props.actions.deleteBranch,
1902
+ onStashSave: props.actions.stashSave,
1903
+ onStashPop: props.actions.stashPop,
1904
+ onStashDrop: props.actions.stashDrop,
1905
+ onStashMessage: props.actions.setStashMessage,
1906
+ })
1584
1907
  else content = h(DiffView, { state: state })
1585
1908
  return h('div', { className: 'gm-root' },
1586
1909
  panelStyle(),
@@ -1616,6 +1939,11 @@ window.__ModuleLoader__.load({
1616
1939
  onHint: props.actions.setAiHint,
1617
1940
  onGenerate: props.actions.generateMessage,
1618
1941
  onCommit: function (andPush) { props.actions.commit(andPush) },
1942
+ onOpenPrompt: props.actions.openPrompt,
1943
+ onClosePrompt: props.actions.closePrompt,
1944
+ onPromptDraft: props.actions.setPromptDraft,
1945
+ onSavePrompt: props.actions.savePrompt,
1946
+ onResetPrompt: props.actions.resetPrompt,
1619
1947
  })
1620
1948
  : null)
1621
1949
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caozeal/dsh-next-git",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "DSH Git 管理插件:分支切换、变更暂存、提交/推送/拉取、AI 提交信息、提交图与 diff 查看",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -16,6 +16,8 @@
16
16
  "src",
17
17
  "lib/client.js",
18
18
  "cordis.patch.yml",
19
+ "screenshots.json",
20
+ "assets/screenshots",
19
21
  "locale/*.json",
20
22
  "icon.svg",
21
23
  "icon.png"
@@ -0,0 +1,8 @@
1
+ [
2
+ "assets/screenshots/01-changes.png",
3
+ "assets/screenshots/02-commit-graph.png",
4
+ "assets/screenshots/03-word-level-diff.png",
5
+ "assets/screenshots/04-branches.png",
6
+ "assets/screenshots/05-graph-multibranch.png",
7
+ "assets/screenshots/06-fullscreen.png"
8
+ ]