@caozeal/dsh-next-git 0.1.5 → 0.1.7

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,26 @@ 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} 的暂存与工作区改动?此操作不可恢复。',
72
+ 'conflict.resolve': '解决冲突',
73
+ 'conflict.title': '冲突解决',
74
+ 'conflict.hintPlaceholder': '补充说明(可选),例如「版本号取上游」',
75
+ 'conflict.ai': 'AI 生成解决建议',
76
+ 'conflict.regenerate': '重新生成',
77
+ 'conflict.takeOurs': '取当前分支',
78
+ 'conflict.takeTheirs': '取对方分支',
79
+ 'conflict.apply': '应用并标记已解决',
80
+ 'conflict.discard': '放弃',
81
+ 'conflict.empty': '先生成建议,或直接取某一侧;生成过程不会改动文件',
82
+ 'conflict.generating': '正在生成解决建议…',
83
+ 'conflict.preview': 'AI 建议的解决结果 · 确认后才会写入文件',
84
+ 'conflict.model': '由 {model} 生成',
85
+ 'conflict.remaining': '还剩 {n} 个冲突文件',
86
+ 'conflict.noBase': 'add/add 冲突,没有共同祖先版本',
67
87
  'commit.title': '提交信息',
68
88
  'commit.placeholder': '填写提交信息,或先点「AI 生成信息」',
69
89
  'commit.hintPlaceholder': '给 AI 的补充说明(可选)',
@@ -75,6 +95,13 @@ window.__ModuleLoader__.load({
75
95
  'commit.push': '提交并推送',
76
96
  'commit.messageRequired': '请先填写提交信息',
77
97
  'commit.empty': '没有可提交的暂存变更',
98
+ 'commit.prompt': 'AI 提示词',
99
+ 'commit.promptTitle': '自定义 AI 提交提示词',
100
+ 'commit.promptHint': '留空使用默认提示词。这里只改「怎么写」,diff 与文件列表始终会传给模型。',
101
+ 'commit.promptSave': '保存',
102
+ 'commit.promptReset': '恢复默认',
103
+ 'commit.promptCancel': '取消',
104
+ 'commit.promptCustom': '自定义提示词',
78
105
  'tabs.changes': '变更',
79
106
  'tabs.graph': '提交图',
80
107
  'tabs.branches': '分支',
@@ -112,6 +139,17 @@ window.__ModuleLoader__.load({
112
139
  'status.branchCreated': '已创建分支 {branch}',
113
140
  'status.branchDeleted': '已删除分支 {branch}',
114
141
  'status.deleteConfirm': '确定删除分支 {branch}?',
142
+ 'status.discardOk': '已丢弃 {path}',
143
+ 'status.stashSaveOk': '已 stash 当前改动',
144
+ 'status.stashPopOk': '已恢复 stash@{n}',
145
+ 'status.stashDropOk': '已删除 stash@{n}',
146
+ 'status.promptSaved': '提示词已保存',
147
+ 'status.promptReset': '已恢复默认提示词',
148
+ 'status.resolveOk': '已解决 {path}',
149
+ 'status.resolveAllOk': '全部冲突已解决,可以提交了',
150
+ 'status.takeOursOk': '已取当前分支版本',
151
+ 'status.takeTheirsOk': '已取对方分支版本',
152
+ 'status.conflictApplied': '已应用解决结果',
115
153
  'status.aiModel': '由 {model} 生成',
116
154
  'status.aiFallback': '模型输出不可用,已生成降级文案',
117
155
  'toast.error': '失败',
@@ -131,6 +169,14 @@ window.__ModuleLoader__.load({
131
169
  'status.aiModelEdit': '由 {model} 生成,可继续编辑',
132
170
  'status.ready': '就绪',
133
171
  'changes.stagedCount': '已暂存 {n}',
172
+ 'stash.title': 'Stash',
173
+ 'stash.empty': '没有 stash 记录',
174
+ 'stash.placeholder': 'stash 说明(可选)',
175
+ 'stash.save': '暂存改动',
176
+ 'stash.pop': '恢复',
177
+ 'stash.drop': '删除',
178
+ 'stash.popConfirm': '恢复 stash@{n} 并从列表移除?',
179
+ 'stash.dropConfirm': '删除 stash@{n}?此操作不可恢复。',
134
180
  'changes.conflictBanner': '存在 {n} 个冲突文件,解决后再提交',
135
181
  'branch.selectTitle': '切换分支',
136
182
  'header.refreshing': '刷新中…',
@@ -175,6 +221,26 @@ window.__ModuleLoader__.load({
175
221
  'changes.unstageAll': 'Unstage all',
176
222
  'changes.stage': 'Stage',
177
223
  'changes.unstage': 'Unstage',
224
+ 'changes.discard': 'Discard changes',
225
+ 'changes.discardUntracked': 'Delete file',
226
+ 'changes.discardConfirm': 'Discard changes to {path}? This cannot be undone.',
227
+ 'changes.discardUntrackedConfirm': 'Delete untracked file {path}? This cannot be undone.',
228
+ 'changes.discardStagedConfirm': 'Discard staged and working-tree changes to {path}? This cannot be undone.',
229
+ 'conflict.resolve': 'Resolve conflict',
230
+ 'conflict.title': 'Resolve conflict',
231
+ 'conflict.hintPlaceholder': 'Optional hint, e.g. "take the upstream version"',
232
+ 'conflict.ai': 'Generate a resolution',
233
+ 'conflict.regenerate': 'Regenerate',
234
+ 'conflict.takeOurs': 'Take ours',
235
+ 'conflict.takeTheirs': 'Take theirs',
236
+ 'conflict.apply': 'Apply and mark resolved',
237
+ 'conflict.discard': 'Discard',
238
+ 'conflict.empty': 'Generate a suggestion, or take one side; generating never touches the file',
239
+ 'conflict.generating': 'Generating a resolution…',
240
+ 'conflict.preview': 'AI suggestion · nothing is written until you apply',
241
+ 'conflict.model': 'Generated by {model}',
242
+ 'conflict.remaining': '{n} conflicted file(s) left',
243
+ 'conflict.noBase': 'add/add conflict; no common ancestor',
178
244
  'commit.title': 'Commit message',
179
245
  'commit.placeholder': 'Write a commit message, or generate one with AI',
180
246
  'commit.hintPlaceholder': 'Optional hint for the AI',
@@ -186,6 +252,13 @@ window.__ModuleLoader__.load({
186
252
  'commit.push': 'Commit & push',
187
253
  'commit.messageRequired': 'Commit message is required',
188
254
  'commit.empty': 'Nothing staged to commit',
255
+ 'commit.prompt': 'AI prompt',
256
+ 'commit.promptTitle': 'Custom AI commit prompt',
257
+ 'commit.promptHint': 'Leave empty to use the default. This only changes how it writes; the diff and file list are always sent.',
258
+ 'commit.promptSave': 'Save',
259
+ 'commit.promptReset': 'Reset to default',
260
+ 'commit.promptCancel': 'Cancel',
261
+ 'commit.promptCustom': 'Custom prompt',
189
262
  'tabs.changes': 'Changes',
190
263
  'tabs.graph': 'Graph',
191
264
  'tabs.branches': 'Branches',
@@ -223,6 +296,17 @@ window.__ModuleLoader__.load({
223
296
  'status.branchCreated': 'Created branch {branch}',
224
297
  'status.branchDeleted': 'Deleted branch {branch}',
225
298
  'status.deleteConfirm': 'Delete branch {branch}?',
299
+ 'status.discardOk': 'Discarded {path}',
300
+ 'status.stashSaveOk': 'Stashed current changes',
301
+ 'status.stashPopOk': 'Restored stash@{n}',
302
+ 'status.stashDropOk': 'Dropped stash@{n}',
303
+ 'status.promptSaved': 'Prompt saved',
304
+ 'status.promptReset': 'Restored the default prompt',
305
+ 'status.resolveOk': 'Resolved {path}',
306
+ 'status.resolveAllOk': 'All conflicts resolved; ready to commit',
307
+ 'status.takeOursOk': 'Took the ours side',
308
+ 'status.takeTheirsOk': 'Took the theirs side',
309
+ 'status.conflictApplied': 'Resolution applied',
226
310
  'status.aiModel': 'Generated by {model}',
227
311
  'status.aiFallback': 'Model output unusable; used a fallback message',
228
312
  'toast.error': 'Failed',
@@ -242,6 +326,14 @@ window.__ModuleLoader__.load({
242
326
  'status.aiModelEdit': 'Generated by {model}; edit as needed',
243
327
  'status.ready': 'Ready',
244
328
  'changes.stagedCount': '{n} staged',
329
+ 'stash.title': 'Stash',
330
+ 'stash.empty': 'No stash entries',
331
+ 'stash.placeholder': 'Stash message (optional)',
332
+ 'stash.save': 'Stash changes',
333
+ 'stash.pop': 'Restore',
334
+ 'stash.drop': 'Drop',
335
+ 'stash.popConfirm': 'Restore stash@{n} and remove it from the list?',
336
+ 'stash.dropConfirm': 'Drop stash@{n}? This cannot be undone.',
245
337
  'changes.conflictBanner': '{n} conflicted file(s); resolve them before committing',
246
338
  'branch.selectTitle': 'Switch branch',
247
339
  'header.refreshing': 'Refreshing…',
@@ -411,6 +503,20 @@ window.__ModuleLoader__.load({
411
503
  .gm-file:hover .gm-act, .gm-file:focus-within .gm-act, .gm-branch:hover .gm-act { opacity:1; }
412
504
  .gm-act:hover { transform:scale(1.12); }
413
505
  @media (hover:none) { .gm-act { opacity:1; } }
506
+ .gm-resolution { display:flex; flex-direction:column; gap:7px; padding:10px 12px 14px; }
507
+ .gm-res-head { display:flex; align-items:center; gap:6px; color:var(--gm-blue); }
508
+ .gm-res-title { font-size:11px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; }
509
+ .gm-res-path { font: 600 12px ui-monospace, SFMono-Regular, Menlo, monospace; color:var(--gm-fg);
510
+ padding:2px 0; word-break:break-all; }
511
+ .gm-res-note { font-size:11px; color:var(--gm-dim); line-height:1.6; }
512
+ /* .gm-input 默认 flex:1 1 100px,在纵向 flex 里会被拉成 100px 高,这里压回去 */
513
+ .gm-resolution .gm-input { flex:none; width:100%; height:28px; }
514
+ .gm-res-code { margin-top:4px; border-radius:10px; background:var(--gm-blue-06);
515
+ border:1px solid var(--gm-blue-12); padding:6px 0; overflow:auto; max-height:46vh; }
516
+ .gm-res-line { display:flex; gap:8px; font: 11.5px/1.65 ui-monospace, SFMono-Regular, Menlo, monospace;
517
+ white-space:pre; padding:0 8px; }
518
+ .gm-res-line .no { width:34px; text-align:right; color:var(--gm-dim); flex:none; user-select:none; }
519
+ .gm-res-line .tx { color:var(--gm-fg); }
414
520
  .gm-conflict-bar { margin:9px 10px 2px; padding:6px 10px; border-radius:10px; font-size:11px; font-weight:600;
415
521
  color:var(--gm-err); background:color-mix(in srgb, var(--gm-err) 8%, transparent);
416
522
  border:1px solid color-mix(in srgb, var(--gm-err) 28%, transparent); }
@@ -482,6 +588,11 @@ window.__ModuleLoader__.load({
482
588
  .gm-commit::before { content:''; position:absolute; top:0; left:0; right:0; height:3px;
483
589
  background:linear-gradient(90deg, var(--gm-cyan), var(--gm-blue) 45%, transparent 90%); }
484
590
  .gm-commit-head { display:flex; align-items:center; gap:6px; }
591
+ .gm-prompt { display:flex; flex-direction:column; gap:5px; padding:8px; border-radius:10px;
592
+ background:var(--gm-blue-06); border:1px solid var(--gm-blue-12); }
593
+ .gm-prompt .gm-section { padding:0; }
594
+ .gm-prompt-text { min-height:96px; max-height:180px; font-size:11px;
595
+ font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
485
596
  .gm-commit-title { font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
486
597
  color:var(--gm-blue); }
487
598
  .gm-textarea { width:100%; min-height:46px; max-height:140px; padding:7px 9px; border-radius:10px;
@@ -607,6 +718,9 @@ window.__ModuleLoader__.load({
607
718
  commit: ['M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16z', 'M8.5 12l2.5 2.5L16 9.5'],
608
719
  file: ['M6 3h8l4 4v14H6z', 'M14 3v5h5'],
609
720
  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'],
721
+ trash: ['M4 7h16', 'M9 7V4h6v3', 'M6 7l1 14h10l1-14', 'M10 11v6', 'M14 11v6'],
722
+ 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'],
723
+ stash: ['M4 5h16v4H4z', 'M6 9v10h12V9', 'M10 13h4'],
610
724
  }
611
725
 
612
726
  function Icon(props) {
@@ -790,6 +904,15 @@ window.__ModuleLoader__.load({
790
904
  newBranchOpen: false,
791
905
  newBranchName: '',
792
906
  newBranchFrom: '',
907
+ // 自定义 AI 提示词编辑态。promptDraft 是编辑中的草稿,
908
+ // 只有点保存才写回 git config,避免边打字边写盘。
909
+ promptOpen: false,
910
+ promptDraft: '',
911
+ stashMessage: '',
912
+ // 冲突解决:resolution 非空时「差异」页切到解决视图。
913
+ // suggested 为 null 表示还没生成,用户可以先选边或让 AI 生成。
914
+ resolution: null,
915
+ conflictHint: '',
793
916
  })
794
917
  var toastTimer = null
795
918
  var loadToken = 0
@@ -909,6 +1032,10 @@ window.__ModuleLoader__.load({
909
1032
  }
910
1033
 
911
1034
  function setNewBranchName(value) { set({ newBranchName: value }) }
1035
+ function setStashMessage(value) {
1036
+ set({ stashMessage: String(value || '') })
1037
+ }
1038
+
912
1039
  function setNewBranchFrom(value) { set({ newBranchFrom: value }) }
913
1040
 
914
1041
  async function runBusy(label, operation) {
@@ -981,6 +1108,181 @@ window.__ModuleLoader__.load({
981
1108
  }
982
1109
  }
983
1110
 
1111
+ /**
1112
+ * 丢弃改动。分组的语义决定用哪个后端模式:
1113
+ * unstaged → worktree(只回退工作区)
1114
+ * untracked → untracked(删除文件)
1115
+ * staged → all(连暂存区一起回退)
1116
+ * 三种都不可恢复,所以一律先 window.confirm。
1117
+ */
1118
+ async function discard(group, entry) {
1119
+ var cwd = store.get().cwd
1120
+ if (!cwd || !entry || !entry.path) return
1121
+ var mode = group === 'untracked' ? 'untracked' : group === 'staged' ? 'all' : 'worktree'
1122
+ var key = group === 'untracked'
1123
+ ? 'changes.discardUntrackedConfirm'
1124
+ : group === 'staged' ? 'changes.discardStagedConfirm' : 'changes.discardConfirm'
1125
+ if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
1126
+ if (!window.confirm(tr(key, { path: entry.path }))) return
1127
+ }
1128
+ var result = await runBusy('discard', function () {
1129
+ return rpc('discard', { cwd: cwd, paths: [entry.path], mode: mode })
1130
+ })
1131
+ if (result !== null) {
1132
+ toast('ok', tr('status.discardOk', { path: entry.path }), result.output)
1133
+ set({ selected: null, diff: null })
1134
+ await refresh()
1135
+ }
1136
+ }
1137
+
1138
+ async function stashSave() {
1139
+ var state = store.get()
1140
+ var cwd = state.cwd
1141
+ if (!cwd) return
1142
+ var params = { cwd: cwd }
1143
+ var message = String(state.stashMessage || '').trim()
1144
+ if (message !== '') params.message = message
1145
+ // 未跟踪文件默认不进 stash;有未跟踪改动时一并存起来,否则用户会以为「全存了」
1146
+ var status = state.snapshot && state.snapshot.status ? state.snapshot.status : null
1147
+ if (status && status.untracked && status.untracked.length > 0) params.includeUntracked = true
1148
+ var result = await runBusy('stash', function () { return rpc('stashPush', params) })
1149
+ if (result !== null) {
1150
+ set({ stashMessage: '' })
1151
+ toast('ok', tr('status.stashSaveOk'), result.output)
1152
+ await refresh()
1153
+ }
1154
+ }
1155
+
1156
+ async function stashPop(index) {
1157
+ var cwd = store.get().cwd
1158
+ if (!cwd) return
1159
+ if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
1160
+ if (!window.confirm(tr('stash.popConfirm', { n: index }))) return
1161
+ }
1162
+ var result = await runBusy('stash', function () { return rpc('stashPop', { cwd: cwd, index: index }) })
1163
+ if (result !== null) {
1164
+ toast('ok', tr('status.stashPopOk', { n: index }), result.output)
1165
+ await refresh()
1166
+ }
1167
+ }
1168
+
1169
+ async function stashDrop(index) {
1170
+ var cwd = store.get().cwd
1171
+ if (!cwd) return
1172
+ if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
1173
+ if (!window.confirm(tr('stash.dropConfirm', { n: index }))) return
1174
+ }
1175
+ var result = await runBusy('stash', function () { return rpc('stashDrop', { cwd: cwd, index: index }) })
1176
+ if (result !== null) {
1177
+ toast('ok', tr('status.stashDropOk', { n: index }), result.output)
1178
+ await refresh()
1179
+ }
1180
+ }
1181
+
1182
+ function openPrompt() {
1183
+ var snapshot = store.get().snapshot
1184
+ set({ promptOpen: true, promptDraft: snapshot && snapshot.commitPrompt ? snapshot.commitPrompt : '' })
1185
+ }
1186
+
1187
+ function closePrompt() {
1188
+ set({ promptOpen: false, promptDraft: '' })
1189
+ }
1190
+
1191
+ function setPromptDraft(value) {
1192
+ set({ promptDraft: String(value || '') })
1193
+ }
1194
+
1195
+ async function savePrompt() {
1196
+ var state = store.get()
1197
+ var cwd = state.cwd
1198
+ if (!cwd) return
1199
+ var result = await runBusy('prompt', function () {
1200
+ return rpc('setCommitPrompt', { cwd: cwd, prompt: String(state.promptDraft || '') })
1201
+ })
1202
+ if (result !== null) {
1203
+ set({ promptOpen: false, promptDraft: '' })
1204
+ toast('ok', tr('status.promptSaved'))
1205
+ await refresh()
1206
+ }
1207
+ }
1208
+
1209
+ async function resetPrompt() {
1210
+ var cwd = store.get().cwd
1211
+ if (!cwd) return
1212
+ var result = await runBusy('prompt', function () {
1213
+ return rpc('setCommitPrompt', { cwd: cwd, prompt: '' })
1214
+ })
1215
+ if (result !== null) {
1216
+ set({ promptOpen: false, promptDraft: '' })
1217
+ toast('ok', tr('status.promptReset'))
1218
+ await refresh()
1219
+ }
1220
+ }
1221
+
1222
+ // ------------------------------------------------------------ 冲突解决
1223
+
1224
+ function openResolution(entry) {
1225
+ if (!entry || !entry.path) return
1226
+ set({
1227
+ resolution: { path: entry.path, suggested: null, model: null, loading: false, hasBase: true },
1228
+ conflictHint: '',
1229
+ activeTab: 'diff',
1230
+ selected: { kind: 'file', group: 'conflicted', path: entry.path },
1231
+ })
1232
+ }
1233
+
1234
+ function closeResolution() {
1235
+ set({ resolution: null, conflictHint: '' })
1236
+ }
1237
+
1238
+ function setConflictHint(value) {
1239
+ set({ conflictHint: String(value || '') })
1240
+ }
1241
+
1242
+ async function aiResolve() {
1243
+ var state = store.get()
1244
+ var resolution = state.resolution
1245
+ if (!state.cwd || !resolution || resolution.loading) return
1246
+ set({ resolution: { path: resolution.path, suggested: resolution.suggested, model: resolution.model, loading: true, hasBase: resolution.hasBase } })
1247
+ var result = await runBusy('aiConflict', function () {
1248
+ return rpc('aiConflict', { cwd: state.cwd, path: resolution.path, hint: String(state.conflictHint || '').trim() })
1249
+ })
1250
+ if (result !== null) {
1251
+ set({ resolution: { path: resolution.path, suggested: result.suggested, model: result.model, loading: false, hasBase: result.hasBase } })
1252
+ } else {
1253
+ set({ resolution: { path: resolution.path, suggested: resolution.suggested, model: resolution.model, loading: false, hasBase: resolution.hasBase } })
1254
+ }
1255
+ }
1256
+
1257
+ async function applyResolution() {
1258
+ var state = store.get()
1259
+ var resolution = state.resolution
1260
+ if (!state.cwd || !resolution || !resolution.suggested) return
1261
+ var result = await runBusy('applyConflict', function () {
1262
+ return rpc('applyConflict', { cwd: state.cwd, path: resolution.path, content: resolution.suggested })
1263
+ })
1264
+ if (result !== null) {
1265
+ set({ resolution: null, conflictHint: '' })
1266
+ toast('ok', result.unmerged === 0 ? tr('status.resolveAllOk') : tr('status.conflictApplied'), result.output)
1267
+ await refresh()
1268
+ }
1269
+ }
1270
+
1271
+ async function takeConflictSide(side) {
1272
+ var state = store.get()
1273
+ var resolution = state.resolution
1274
+ if (!state.cwd || !resolution) return
1275
+ var result = await runBusy('takeSide', function () {
1276
+ return rpc('takeSide', { cwd: state.cwd, path: resolution.path, side: side })
1277
+ })
1278
+ if (result !== null) {
1279
+ set({ resolution: null, conflictHint: '' })
1280
+ var label = side === 'ours' ? tr('status.takeOursOk') : tr('status.takeTheirsOk')
1281
+ toast('ok', result.unmerged === 0 ? tr('status.resolveAllOk') : label, result.output)
1282
+ await refresh()
1283
+ }
1284
+ }
1285
+
984
1286
  async function switchBranch(name) {
985
1287
  var cwd = store.get().cwd
986
1288
  if (!cwd || !name) return
@@ -1105,6 +1407,22 @@ window.__ModuleLoader__.load({
1105
1407
  setNewBranch: setNewBranch,
1106
1408
  setNewBranchName: setNewBranchName,
1107
1409
  setNewBranchFrom: setNewBranchFrom,
1410
+ discard: discard,
1411
+ stashSave: stashSave,
1412
+ stashPop: stashPop,
1413
+ stashDrop: stashDrop,
1414
+ setStashMessage: setStashMessage,
1415
+ openPrompt: openPrompt,
1416
+ closePrompt: closePrompt,
1417
+ setPromptDraft: setPromptDraft,
1418
+ savePrompt: savePrompt,
1419
+ resetPrompt: resetPrompt,
1420
+ openResolution: openResolution,
1421
+ closeResolution: closeResolution,
1422
+ setConflictHint: setConflictHint,
1423
+ aiResolve: aiResolve,
1424
+ applyResolution: applyResolution,
1425
+ takeConflictSide: takeConflictSide,
1108
1426
  clearToast: clearToast,
1109
1427
  }
1110
1428
  }
@@ -1162,16 +1480,41 @@ window.__ModuleLoader__.load({
1162
1480
  h('span', { className: 'gm-fpath' },
1163
1481
  parts.dir ? h('span', { className: 'gm-dir' }, parts.dir) : null,
1164
1482
  parts.base),
1165
- h('button', {
1166
- type: 'button',
1167
- className: 'gm-btn icon sm ghost gm-act',
1168
- title: staged ? tr('changes.unstage') : tr('changes.stage'),
1169
- onClick: function (event) {
1170
- event.stopPropagation()
1171
- if (staged) props.onUnstage(entry)
1172
- else props.onStage(entry)
1173
- },
1174
- }, h(Icon, { name: staged ? 'minus' : 'plus', size: 12 })))
1483
+ h('span', { className: 'gm-row', style: { gap: '2px' } },
1484
+ // 冲突文件不给「暂存」:git add 会把带 <<<<<<< 的文件直接标记为已解决。
1485
+ // 唯一入口是「解决」,由 applyConflict / takeSide 负责清掉冲突标记。
1486
+ props.group === 'conflicted'
1487
+ ? h('button', {
1488
+ type: 'button',
1489
+ className: 'gm-btn icon sm ghost gm-act',
1490
+ title: tr('conflict.resolve'),
1491
+ onClick: function (event) {
1492
+ event.stopPropagation()
1493
+ props.onResolve(entry)
1494
+ },
1495
+ }, h(Icon, { name: 'ai', size: 12 }))
1496
+ : h('button', {
1497
+ type: 'button',
1498
+ className: 'gm-btn icon sm ghost gm-act',
1499
+ title: staged ? tr('changes.unstage') : tr('changes.stage'),
1500
+ onClick: function (event) {
1501
+ event.stopPropagation()
1502
+ if (staged) props.onUnstage(entry)
1503
+ else props.onStage(entry)
1504
+ },
1505
+ }, h(Icon, { name: staged ? 'minus' : 'plus', size: 12 })),
1506
+ // 冲突文件不给「丢弃」:那等于放弃冲突解决状态,语义上容易误伤
1507
+ props.onDiscard && props.group !== 'conflicted'
1508
+ ? h('button', {
1509
+ type: 'button',
1510
+ className: 'gm-btn icon sm ghost gm-act',
1511
+ title: props.group === 'untracked' ? tr('changes.discardUntracked') : tr('changes.discard'),
1512
+ onClick: function (event) {
1513
+ event.stopPropagation()
1514
+ props.onDiscard(entry)
1515
+ },
1516
+ }, h(Icon, { name: 'trash', size: 12 }))
1517
+ : null))
1175
1518
  }))
1176
1519
  }
1177
1520
 
@@ -1194,6 +1537,8 @@ window.__ModuleLoader__.load({
1194
1537
  onSelect: function (entry) { props.actions.selectFile(group, entry) },
1195
1538
  onStage: function (entry) { props.actions.stage(group, entry) },
1196
1539
  onUnstage: function (entry) { props.actions.unstage(group, entry) },
1540
+ onDiscard: function (entry) { props.actions.discard(group, entry) },
1541
+ onResolve: function (entry) { props.actions.openResolution(entry) },
1197
1542
  })
1198
1543
  }
1199
1544
  var empty = !conflicted.length && !staged.length && !unstaged.length && !untracked.length
@@ -1226,6 +1571,29 @@ window.__ModuleLoader__.load({
1226
1571
  }
1227
1572
  }
1228
1573
  return h('div', { className: 'gm-commit' },
1574
+ state.promptOpen
1575
+ ? h('div', { className: 'gm-prompt' },
1576
+ h('div', { className: 'gm-section' },
1577
+ h('span', null, tr('commit.promptTitle')),
1578
+ h('span', { className: 'gm-spacer' }),
1579
+ h('button', {
1580
+ type: 'button',
1581
+ className: 'gm-btn icon sm ghost',
1582
+ title: tr('commit.promptCancel'),
1583
+ onClick: props.onClosePrompt,
1584
+ }, h(Icon, { name: 'close', size: 12 }))),
1585
+ h('textarea', {
1586
+ className: 'gm-textarea gm-prompt-text',
1587
+ value: state.promptDraft,
1588
+ placeholder: tr('commit.promptHint'),
1589
+ onChange: function (event) { props.onPromptDraft(event.target.value) },
1590
+ }),
1591
+ h('div', { className: 'gm-hint' }, tr('commit.promptHint')),
1592
+ h('div', { className: 'gm-actions' },
1593
+ h(Btn, { label: tr('commit.promptReset'), small: true, ghost: true, disabled: !!busy, onClick: props.onResetPrompt }),
1594
+ h('span', { className: 'gm-spacer' }),
1595
+ h(Btn, { label: tr('commit.promptSave'), small: true, variant: 'primary', disabled: !!busy, onClick: props.onSavePrompt })))
1596
+ : null,
1229
1597
  h('div', { className: 'gm-commit-head' },
1230
1598
  h(WhaleGlyph, { size: 22 }),
1231
1599
  h('span', { className: 'gm-commit-title' }, tr('commit.title')),
@@ -1254,8 +1622,17 @@ window.__ModuleLoader__.load({
1254
1622
  className: busy === 'ai' ? 'gm-ai-busy' : undefined,
1255
1623
  disabled: !!busy || !state.cwd,
1256
1624
  onClick: props.onGenerate,
1625
+ }),
1626
+ h(Btn, {
1627
+ icon: 'gear',
1628
+ title: tr('commit.prompt'),
1629
+ disabled: !!busy || !state.cwd,
1630
+ onClick: props.onOpenPrompt,
1257
1631
  })),
1258
1632
  h('div', { className: 'gm-actions' },
1633
+ state.snapshot && state.snapshot.commitPrompt
1634
+ ? h('span', { className: 'gm-badge brand', title: state.snapshot.commitPrompt }, tr('commit.promptCustom'))
1635
+ : null,
1259
1636
  state.aiModel
1260
1637
  ? h('span', { className: 'gm-ai-line gm-hint', title: tr('status.aiModel', { model: state.aiModel }) },
1261
1638
  state.aiFallback ? tr('status.aiFallback') : tr('status.aiModelEdit', { model: state.aiModel }))
@@ -1498,8 +1875,88 @@ window.__ModuleLoader__.load({
1498
1875
  })
1499
1876
  }
1500
1877
 
1878
+ /**
1879
+ * 冲突解决视图。
1880
+ * 生成阶段完全不碰磁盘,只有点「应用」才写文件 —— 模型给错了最多白看一次。
1881
+ */
1882
+ function ResolutionView(props) {
1883
+ var state = props.state
1884
+ var resolution = state.resolution
1885
+ var busy = !!state.busy
1886
+ var suggested = resolution.suggested
1887
+ var lines = suggested ? suggested.replace(/\n$/, '').split('\n') : []
1888
+ var visible = lines.slice(0, 400)
1889
+ var actions = props.actions
1890
+ return h('div', { className: 'gm-scroll' },
1891
+ h('div', { className: 'gm-content-inner gm-resolution' },
1892
+ h('div', { className: 'gm-res-head' },
1893
+ h(Icon, { name: 'ai', size: 14 }),
1894
+ h('span', { className: 'gm-res-title' }, tr('conflict.title')),
1895
+ h('span', { className: 'gm-spacer' }),
1896
+ h('button', {
1897
+ type: 'button',
1898
+ className: 'gm-btn icon sm ghost',
1899
+ title: tr('conflict.discard'),
1900
+ onClick: function () { actions.closeResolution() },
1901
+ }, h(Icon, { name: 'close', size: 12 }))),
1902
+ h('div', { className: 'gm-res-path' }, resolution.path),
1903
+ resolution.loading
1904
+ ? h('div', { className: 'gm-res-note' }, tr('conflict.generating'))
1905
+ : suggested
1906
+ ? h('div', { className: 'gm-res-note' },
1907
+ tr('conflict.preview'),
1908
+ resolution.model ? ' · ' + tr('conflict.model', { model: resolution.model }) : '')
1909
+ : h('div', { className: 'gm-res-note' }, tr('conflict.empty')),
1910
+ h('input', {
1911
+ className: 'gm-input',
1912
+ value: state.conflictHint,
1913
+ placeholder: tr('conflict.hintPlaceholder'),
1914
+ disabled: busy,
1915
+ onChange: function (event) { actions.setConflictHint(event.target.value) },
1916
+ onKeyDown: function (event) { if (event.key === 'Enter') actions.aiResolve() },
1917
+ }),
1918
+ h('div', { className: 'gm-actions' },
1919
+ h(Btn, {
1920
+ icon: 'ai',
1921
+ label: suggested ? tr('conflict.regenerate') : tr('conflict.ai'),
1922
+ small: true,
1923
+ variant: 'primary',
1924
+ disabled: busy || resolution.loading,
1925
+ onClick: actions.aiResolve,
1926
+ }),
1927
+ h(Btn, {
1928
+ label: tr('conflict.takeOurs'),
1929
+ small: true,
1930
+ disabled: busy || resolution.loading,
1931
+ onClick: function () { actions.takeConflictSide('ours') },
1932
+ }),
1933
+ h(Btn, {
1934
+ label: tr('conflict.takeTheirs'),
1935
+ small: true,
1936
+ disabled: busy || resolution.loading,
1937
+ onClick: function () { actions.takeConflictSide('theirs') },
1938
+ })),
1939
+ suggested
1940
+ ? h('div', { className: 'gm-actions' },
1941
+ h(Btn, { label: tr('conflict.apply'), small: true, variant: 'primary', disabled: busy, onClick: actions.applyResolution }),
1942
+ h(Btn, { label: tr('conflict.discard'), small: true, ghost: true, disabled: busy, onClick: function () { actions.closeResolution() } }))
1943
+ : null,
1944
+ suggested
1945
+ ? h('div', { className: 'gm-res-code' }, visible.map(function (line, index) {
1946
+ return h('div', { key: 'rl:' + index, className: 'gm-res-line' },
1947
+ h('span', { className: 'no' }, String(index + 1)),
1948
+ h('span', { className: 'tx' }, line === '' ? ' ' : line))
1949
+ }))
1950
+ : null,
1951
+ suggested && lines.length > visible.length
1952
+ ? h('div', { className: 'gm-res-note' }, tr('diff.truncated'))
1953
+ : null))
1954
+ }
1955
+
1501
1956
  function DiffView(props) {
1502
1957
  var state = props.state
1958
+ // 解决视图优先:它是「差异」页的一个特殊态,不额外占一个 tab。
1959
+ if (state.resolution) return h(ResolutionView, { state: state, actions: props.actions })
1503
1960
  var diff = state.diff
1504
1961
  if (state.diffLoading) return h(EmptyState, { spinner: true, title: tr('status.loading') })
1505
1962
  if (!diff) return h(EmptyState, { title: tr('diff.empty') })
@@ -1539,6 +1996,7 @@ window.__ModuleLoader__.load({
1539
1996
  var branches = state.snapshot && state.snapshot.branches ? state.snapshot.branches : []
1540
1997
  var locals = branches.filter(function (branch) { return branch.kind === 'local' })
1541
1998
  var remotes = branches.filter(function (branch) { return branch.kind === 'remote' })
1999
+ var stashes = state.snapshot && state.snapshot.stashes ? state.snapshot.stashes : []
1542
2000
  function group(title, items, allowDelete) {
1543
2001
  if (items.length === 0) return null
1544
2002
  return h('div', null,
@@ -1564,13 +2022,42 @@ window.__ModuleLoader__.load({
1564
2022
  : null))
1565
2023
  }))
1566
2024
  }
2025
+ // stash 放在分支页:它和分支一样是「当前工作放哪儿」的操作,
2026
+ // 而不是某次提交的内容。后端 snapshot 早就解析了 stashes,这里才真正用上。
2027
+ function stashSection() {
2028
+ return h('div', null,
2029
+ h('div', { className: 'gm-section' },
2030
+ h('span', null, tr('stash.title')),
2031
+ h('span', { className: 'gm-section-count' }, String(stashes.length))),
2032
+ h('div', { className: 'gm-row', style: { padding: '0 10px 6px' } },
2033
+ h('input', {
2034
+ className: 'gm-input',
2035
+ value: state.stashMessage,
2036
+ placeholder: tr('stash.placeholder'),
2037
+ onChange: function (event) { props.onStashMessage(event.target.value) },
2038
+ onKeyDown: function (event) { if (event.key === 'Enter') props.onStashSave() },
2039
+ }),
2040
+ h(Btn, { label: tr('stash.save'), small: true, disabled: !!state.busy, onClick: props.onStashSave })),
2041
+ stashes.length === 0
2042
+ ? h('div', { className: 'gm-hint', style: { padding: '0 12px 8px' } }, tr('stash.empty'))
2043
+ : stashes.map(function (item) {
2044
+ return h('div', { key: 'stash:' + item.index, className: 'gm-branch' },
2045
+ h(Icon, { name: 'stash', size: 12 }),
2046
+ h('span', { className: 'gm-badge' }, 'stash@{' + item.index + '}'),
2047
+ h('span', { className: 'gm-bmeta' }, item.message || ''),
2048
+ h('span', { className: 'gm-row gm-act', style: { gap: '4px', flex: 'none' } },
2049
+ h(Btn, { label: tr('stash.pop'), small: true, disabled: !!state.busy, onClick: function () { props.onStashPop(item.index) } }),
2050
+ h(Btn, { label: tr('stash.drop'), small: true, ghost: true, disabled: !!state.busy, onClick: function () { props.onStashDrop(item.index) } })))
2051
+ }))
2052
+ }
1567
2053
  return h('div', { className: 'gm-scroll' },
1568
2054
  h('div', { className: 'gm-content-inner' },
1569
2055
  group(tr('branch.local'), locals, true),
1570
2056
  group(tr('branch.remote'), remotes, false),
1571
2057
  locals.length === 0 && remotes.length === 0
1572
2058
  ? h(EmptyState, { title: tr('branch.empty') })
1573
- : null))
2059
+ : null,
2060
+ stashSection()))
1574
2061
  }
1575
2062
 
1576
2063
  function StatusBar(props) {
@@ -1629,8 +2116,16 @@ window.__ModuleLoader__.load({
1629
2116
  var content
1630
2117
  if (state.activeTab === 'changes') content = h(ChangesPane, { state: state, actions: props.actions })
1631
2118
  else if (state.activeTab === 'graph') content = h(GraphView, { state: state, onSelectCommit: props.actions.selectCommit })
1632
- else if (state.activeTab === 'branches') content = h(BranchesView, { state: state, onSwitch: props.actions.switchBranch, onDelete: props.actions.deleteBranch })
1633
- else content = h(DiffView, { state: state })
2119
+ else if (state.activeTab === 'branches') content = h(BranchesView, {
2120
+ state: state,
2121
+ onSwitch: props.actions.switchBranch,
2122
+ onDelete: props.actions.deleteBranch,
2123
+ onStashSave: props.actions.stashSave,
2124
+ onStashPop: props.actions.stashPop,
2125
+ onStashDrop: props.actions.stashDrop,
2126
+ onStashMessage: props.actions.setStashMessage,
2127
+ })
2128
+ else content = h(DiffView, { state: state, actions: props.actions })
1634
2129
  return h('div', { className: 'gm-root' },
1635
2130
  panelStyle(),
1636
2131
  h(CompactTopBar, {
@@ -1665,6 +2160,11 @@ window.__ModuleLoader__.load({
1665
2160
  onHint: props.actions.setAiHint,
1666
2161
  onGenerate: props.actions.generateMessage,
1667
2162
  onCommit: function (andPush) { props.actions.commit(andPush) },
2163
+ onOpenPrompt: props.actions.openPrompt,
2164
+ onClosePrompt: props.actions.closePrompt,
2165
+ onPromptDraft: props.actions.setPromptDraft,
2166
+ onSavePrompt: props.actions.savePrompt,
2167
+ onResetPrompt: props.actions.resetPrompt,
1668
2168
  })
1669
2169
  : null)
1670
2170
  }