@dsh-xhl/dsh-git-gui 0.1.3

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 ADDED
@@ -0,0 +1,2222 @@
1
+ /* generated by scripts/build-client.mjs — do not edit */
2
+ window.__ModuleLoader__.load({
3
+ id: "@dsh-xhl/dsh-git-gui",
4
+ factory: function (require) {
5
+ var __modules = {
6
+ "./api.js": function (module, exports, require) {
7
+ /**
8
+ * Browser → Host RPC wrapper for the `git/*` endpoints (Typert SRC mode).
9
+ *
10
+ * The host plugin binds the `git` namespace; the API gateway accepts plain
11
+ * `{args}` payloads on the shared `/api` channel, so no generated Typert
12
+ * artifacts are required on either side.
13
+ */
14
+
15
+ class GitApiError extends Error {
16
+ constructor(code, message, detail) {
17
+ super(message)
18
+ this.name = 'GitApiError'
19
+ this.code = code
20
+ this.detail = detail
21
+ }
22
+ }
23
+
24
+ function makeGitApi(connection) {
25
+ async function call(method, args, signal) {
26
+ const result = await connection.rpc.call('/api', `git/${method}`, { args }, signal)
27
+ if (!result.ok) {
28
+ throw new GitApiError(result.error?.code ?? 'internal', result.error?.message ?? `git/${method} 调用失败`)
29
+ }
30
+ const value = result.value
31
+ if (value && typeof value === 'object' && value.ok === false) {
32
+ throw new GitApiError(value.code ?? 'GIT', value.message ?? 'Git 操作失败', value.detail)
33
+ }
34
+ return value
35
+ }
36
+
37
+ return {
38
+ call,
39
+ async check(cwd, signal) { return call('check', { cwd }, signal) },
40
+ async status(cwd, signal) { return call('status', { cwd }, signal) },
41
+ async diff(cwd, path, staged, base, untracked, signal) {
42
+ return call('diff', { cwd, path, staged: staged === true, base: base ?? null, untracked: untracked === true }, signal)
43
+ },
44
+ async stage(cwd, paths, signal) { return call('stage', { cwd, paths }, signal) },
45
+ async unstage(cwd, paths, signal) { return call('unstage', { cwd, paths }, signal) },
46
+ async discard(cwd, paths, untracked, signal) { return call('discard', { cwd, paths, untracked: untracked === true }, signal) },
47
+ async commit(cwd, message, signal) { return call('commit', { cwd, message }, signal) },
48
+ async generateCommitMessage(cwd, signal) { return call('generateCommitMessage', { cwd }, signal) },
49
+ async log(cwd, limit, path, signal) { return call('log', { cwd, limit, path: path ?? null }, signal) },
50
+ async branches(cwd, signal) { return call('branches', { cwd }, signal) },
51
+ async switchBranch(cwd, name, create, signal) { return call('switchBranch', { cwd, name, create: create === true }, signal) },
52
+ async merge(cwd, ref, signal) { return call('merge', { cwd, ref }, signal) },
53
+ async pull(cwd, mode, signal) { return call('pull', { cwd, mode }, signal) },
54
+ async push(cwd, signal) { return call('push', { cwd }, signal) },
55
+ async fetch(cwd, signal) { return call('fetch', { cwd }, signal) },
56
+ async remoteList(cwd, signal) { return call('remoteList', { cwd }, signal) },
57
+ async remoteAdd(cwd, name, url, signal) { return call('remoteAdd', { cwd, name, url }, signal) },
58
+ async stash(cwd, op, message, stashId, signal) {
59
+ return call('stash', { cwd, op, message: message ?? null, stashId: stashId ?? null }, signal)
60
+ },
61
+ async revert(cwd, commit, signal) { return call('revert', { cwd, commit }, signal) },
62
+ async reset(cwd, mode, target, signal) { return call('reset', { cwd, mode, target: target ?? 'HEAD' }, signal) },
63
+ async init(cwd, signal) { return call('init', { cwd }, signal) },
64
+ async activity(cwd, limit, signal) { return call('activity', { cwd, limit }, signal) },
65
+ async tree(cwd, signal) { return call('tree', { cwd }, signal) },
66
+ async cat(cwd, path, signal) { return call('cat', { cwd, path }, signal) },
67
+ async identity(cwd, signal) { return call('identity', { cwd }, signal) },
68
+ }
69
+ }
70
+
71
+ module.exports = { makeGitApi, GitApiError }
72
+
73
+ },
74
+ "./control.js": function (module, exports, require) {
75
+ /**
76
+ * Panel controller: workspace/session sync, polling, and operation runner.
77
+ * Shared by the sidebar entry button and the overlay panel.
78
+ */
79
+ const { setState, getState, resetWorkspace } = require('./store')
80
+ const { t } = require('./i18n')
81
+
82
+ let api = null
83
+ let pollTimer = null
84
+ let tabLoaders = {} // tab -> loader fn
85
+ let lastBadgeAt = 0
86
+ let lastStatusAt = 0
87
+
88
+ const OPEN_POLL_MS = 2000
89
+ const BADGE_POLL_MS = 5000
90
+
91
+ function startController(gitApi) {
92
+ api = gitApi
93
+ if (pollTimer !== null) return
94
+ pollTimer = setInterval(() => {
95
+ try {
96
+ tick()
97
+ } catch { /* poller must never throw */ }
98
+ }, 1000)
99
+ if (pollTimer.unref) pollTimer.unref()
100
+ }
101
+
102
+ function getApi() {
103
+ return api
104
+ }
105
+
106
+ /**
107
+ * Apply a sessions snapshot (already selected via the useSessions hook in a
108
+ * component) to the store: session id, workspace cwd, and running flag.
109
+ * Called from effects only (never during render).
110
+ */
111
+ function applySession(sessions) {
112
+ const currentId = sessions.current
113
+ const summary = currentId !== undefined ? sessions.byId[currentId] : undefined
114
+ const cwd = typeof summary?.cwd === 'string' && summary.cwd !== '' ? summary.cwd : null
115
+ const running = summary?.running === true
116
+ const s = getState()
117
+ if (s.sessionRunning !== running) setState({ sessionRunning: running })
118
+ if (s.sessionId !== currentId || (cwd !== null && s.cwd !== cwd)) {
119
+ if (cwd !== null && s.cwd !== cwd) {
120
+ resetWorkspace(cwd, currentId ?? null)
121
+ refreshCheck()
122
+ refreshStatus()
123
+ refreshRemotes()
124
+ refreshTabData()
125
+ } else if (cwd === null && s.cwd !== null) {
126
+ resetWorkspace(null, currentId ?? null)
127
+ } else if (currentId !== undefined && s.sessionId !== currentId) {
128
+ setState({ sessionId: currentId })
129
+ }
130
+ }
131
+ }
132
+
133
+ /** Load the configured remotes for the current workspace. */
134
+ async function refreshRemotes() {
135
+ const s = getState()
136
+ if (!api || s.cwd === null) return
137
+ try {
138
+ const result = await api.remoteList(s.cwd)
139
+ setState({ remotes: result.remotes ?? [] })
140
+ } catch {
141
+ setState({ remotes: [] })
142
+ }
143
+ }
144
+
145
+ function tick() {
146
+ const s = getState()
147
+ if (s.cwd === null || s.check?.repo !== true) return
148
+ if (s.busy) return
149
+ const now = Date.now()
150
+ if (s.open) {
151
+ // status refresh while the panel is open: at most every 2s
152
+ if (now - lastStatusAt < OPEN_POLL_MS) return
153
+ lastStatusAt = now
154
+ refreshStatus()
155
+ } else if (now - lastBadgeAt >= BADGE_POLL_MS) {
156
+ lastBadgeAt = now
157
+ refreshStatus()
158
+ }
159
+ }
160
+
161
+ async function refreshCheck() {
162
+ const s = getState()
163
+ if (!api || s.cwd === null) return
164
+ try {
165
+ const result = await api.check(s.cwd)
166
+ setState({ check: result })
167
+ if (result.repo) refreshStatus()
168
+ } catch (error) {
169
+ setState({ check: { repo: false, gitVersion: '', error: error.message } })
170
+ }
171
+ }
172
+
173
+ async function refreshStatus() {
174
+ const s = getState()
175
+ if (!api || s.cwd === null || s.check?.repo !== true) return
176
+ if (s.busy) return
177
+ try {
178
+ const result = await api.status(s.cwd)
179
+ setState({ status: result, statusError: null })
180
+ } catch (error) {
181
+ setState({ statusError: String(error?.message ?? error) })
182
+ }
183
+ }
184
+
185
+ async function refreshDiff() {
186
+ const s = getState()
187
+ const selected = s.selected
188
+ if (!api || s.cwd === null || selected === null) return
189
+ const key = { ...selected }
190
+ setState({ diff: { ...key, loading: true, error: null, data: null } })
191
+ try {
192
+ const result = selected.cat === true
193
+ ? await api.cat(s.cwd, selected.path)
194
+ : await api.diff(s.cwd, selected.path, selected.staged, null, selected.untracked)
195
+ const current = getState()
196
+ const cur = current.selected
197
+ if (cur === null || cur.path !== selected.path || cur.staged !== selected.staged
198
+ || cur.untracked !== selected.untracked || cur.cat !== selected.cat) return
199
+ setState({ diff: { ...key, loading: false, error: null, data: result } })
200
+ } catch (error) {
201
+ const current = getState()
202
+ const cur = current.selected
203
+ if (cur === null || cur.path !== selected.path) return
204
+ setState({ diff: { ...key, loading: false, error: String(error?.message ?? error), data: null } })
205
+ }
206
+ }
207
+
208
+ function registerTabLoader(tab, loader) {
209
+ tabLoaders[tab] = loader
210
+ }
211
+
212
+ async function refreshTabData() {
213
+ const s = getState()
214
+ const loader = tabLoaders[s.tab]
215
+ if (loader && s.cwd !== null && s.check?.repo === true) {
216
+ try {
217
+ await loader(s.cwd)
218
+ } catch (error) {
219
+ setState({ toast: { kind: 'error', text: String(error?.message ?? error) } })
220
+ }
221
+ }
222
+ }
223
+
224
+ /** Run one user operation: busy state, error toast, refresh afterwards. */
225
+ async function run(label, fn, refresh = true) {
226
+ const s = getState()
227
+ if (s.busy) {
228
+ setState({ toast: { kind: 'warn', text: t('toast.busy') } })
229
+ return false
230
+ }
231
+ setState({ busy: true, busyLabel: label })
232
+ try {
233
+ const result = await fn()
234
+ if (result && result.output !== undefined && result.output !== '') {
235
+ setState({ output: { title: label, text: result.output } })
236
+ }
237
+ if (refresh) {
238
+ await Promise.allSettled([refreshStatus(), refreshDiff(), refreshTabData()])
239
+ }
240
+ return true
241
+ } catch (error) {
242
+ setState({
243
+ toast: { kind: 'error', text: String(error?.message ?? error) },
244
+ output: { title: label, text: String(error?.detail ?? error?.message ?? error) },
245
+ })
246
+ if (refresh) await refreshStatus().catch(() => {})
247
+ return false
248
+ } finally {
249
+ setState({ busy: false, busyLabel: '' })
250
+ }
251
+ }
252
+
253
+ /** Ask a confirmation, then run the operation when confirmed. */
254
+ function confirmThen({ body, danger, action }) {
255
+ setState({ confirm: { body, danger, action } })
256
+ }
257
+
258
+ /** Settle the open confirm dialog: run the stored action when ok=true. */
259
+ function settleConfirm(ok) {
260
+ const s = getState()
261
+ const confirm = s.confirm
262
+ setState({ confirm: null })
263
+ if (ok && confirm) {
264
+ const action = confirm.action
265
+ setTimeout(() => { action() }, 0)
266
+ }
267
+ }
268
+
269
+ module.exports = {
270
+ startController,
271
+ getApi,
272
+ applySession,
273
+ refreshCheck,
274
+ refreshStatus,
275
+ refreshDiff,
276
+ refreshRemotes,
277
+ refreshTabData,
278
+ registerTabLoader,
279
+ run,
280
+ confirmThen,
281
+ settleConfirm,
282
+ }
283
+
284
+ },
285
+ "./dom.js": function (module, exports, require) {
286
+ /**
287
+ * Tiny DOM/React helpers for the client bundle (no JSX: hyperscript only).
288
+ */
289
+ const React = require('react')
290
+
291
+ /** Hyperscript: h('div', {className}, children...) -> React.createElement. */
292
+ function h(tag, props, ...children) {
293
+ if (props === null || props === undefined) props = {}
294
+ const flat = []
295
+ const push = (child) => {
296
+ if (child === null || child === undefined || child === false) return
297
+ if (Array.isArray(child)) {
298
+ for (const c of child) push(c)
299
+ } else {
300
+ flat.push(child)
301
+ }
302
+ }
303
+ for (const child of children) push(child)
304
+ return React.createElement(tag, props, ...flat)
305
+ }
306
+
307
+ /** Join class names, skipping falsy. */
308
+ function cx(...parts) {
309
+ return parts.filter(Boolean).join(' ')
310
+ }
311
+
312
+ /** Inline SVG icon (stroke = currentColor). */
313
+ function icon(pathData, size) {
314
+ return h('svg', {
315
+ className: 'gg-icon',
316
+ width: size ?? 14,
317
+ height: size ?? 14,
318
+ viewBox: '0 0 16 16',
319
+ fill: 'none',
320
+ stroke: 'currentColor',
321
+ strokeWidth: 1.4,
322
+ strokeLinecap: 'round',
323
+ strokeLinejoin: 'round',
324
+ 'aria-hidden': 'true',
325
+ }, h('path', { d: pathData }))
326
+ }
327
+
328
+ const ICONS = {
329
+ git: icon('M10.5 2.5 4 9m6.5-6.5 3 3m-3-3L8 0.5 16 8.5 7.5 17 0 9.5l8-8m6.5 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm-13 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z', 16),
330
+ refresh: icon('M13.5 8a5.5 5.5 0 1 1-1.6-3.9M13.5 1.5v3h-3', 13),
331
+ close: icon('M4 4l8 8M12 4l-8 8', 13),
332
+ check: icon('M2.5 8.5 6 12l7.5-8', 13),
333
+ plus: icon('M8 2.5v11M2.5 8h11', 13),
334
+ trash: icon('M2.5 4h11M6.5 4V2.5h3V4M4 4l1 9.5h6L12 4', 13),
335
+ undo: icon('M6 4.5 2.5 8 6 11.5M3 8h7.5a4 4 0 0 1 0 8H8', 13),
336
+ branch: icon('M4 3v5.5M12 3v5.5M4 13a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm8 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM4 11.5c0-3 8-1 8-5', 14),
337
+ clock: icon('M8 14.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13ZM8 4.5V8l2.5 1.5', 13),
338
+ folder: icon('M1.5 3.5h4l1.5 2h7.5v7h-13v-9Z', 13),
339
+ alert: icon('M8 14.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13ZM8 5v3.5M8 11.2v.1', 13),
340
+ down: icon('M3.5 6 8 10.5 12.5 6', 13),
341
+ up: icon('M3.5 10 8 5.5l4.5 4.5', 13),
342
+ chevron: icon('M5.5 3.5 10 8l-4.5 4.5', 12),
343
+ file: icon('M3 2h6l4 4v8H3V2Zm6 0v4h4', 13),
344
+ diff: icon('M2 12h5m3 0h4M2 8h5m3 0h4M2 4h5m3 0h4', 13),
345
+ robot: icon('M8 1.5v3M8 14.5a3 3 0 0 0 3 3M8 14.5a3 3 0 0 1-3 3M2.5 8.5h.5a1 1 0 0 1 1 1V13a1.5 1.5 0 0 0 3 0V5a2.5 2.5 0 0 1 5 0v8a1.5 1.5 0 0 0 3 0V9.5a1 1 0 0 1 1-1h.5M8 4.5a3 3 0 0 1 0 6', 15),
346
+ globe: icon('M8 14.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13ZM1.5 8h13M8 1.5c1.8 1.8 2.7 4 2.7 6.5s-.9 4.7-2.7 6.5C6.2 12.7 5.3 10.5 5.3 8S6.2 3.3 8 1.5Z', 14),
347
+ }
348
+
349
+ module.exports = { h, cx, icon, ICONS, React }
350
+
351
+ },
352
+ "./i18n.js": function (module, exports, require) {
353
+ /**
354
+ * Minimal zh/en dictionary for the Git panel. Defaults to Simplified Chinese;
355
+ * flips to English when the document language starts with 'en'.
356
+ */
357
+ const zh = {
358
+ 'panel.title': '源代码管理 (Git)',
359
+ 'panel.close': '关闭面板',
360
+ 'tab.status': '变更',
361
+ 'tab.files': '文件',
362
+ 'tab.log': '日志',
363
+ 'tab.branches': '分支',
364
+ 'tab.stash': '储藏',
365
+ 'tab.timeline': 'AI 修改',
366
+ 'files.title': '工作区文件',
367
+ 'files.search': '筛选文件…',
368
+ 'files.empty': '工作区没有文件',
369
+ 'files.tooMany': '文件过多,仅显示前 {n} 个',
370
+ 'files.clean': '该文件没有未提交的更改(显示当前内容)',
371
+ 'files.legendModified': '蓝:已修改(未提交)',
372
+ 'files.legendUntracked': '红:未跟踪(未纳入版本管理)',
373
+ 'files.legendClean': '默认色:未修改',
374
+ 'cat.title': '文件内容',
375
+ 'action.refresh': '刷新',
376
+ 'action.stageAll': '全部暂存',
377
+ 'action.unstageAll': '全部取消暂存',
378
+ 'action.commit': '提交',
379
+ 'action.aiCommit': 'AI 生成',
380
+ 'action.aiCommitTooltip': '使用大模型根据已暂存的改动自动生成提交信息',
381
+ 'action.pull': '拉取',
382
+ 'action.push': '推送',
383
+ 'action.fetch': '获取',
384
+ 'action.init': '初始化仓库',
385
+ 'action.discard': '丢弃改动',
386
+ 'action.stage': '暂存',
387
+ 'action.unstage': '取消暂存',
388
+ 'action.openDiff': '查看差异',
389
+ 'action.newBranch': '新建分支',
390
+ 'action.switch': '切换',
391
+ 'action.merge': '合并到当前分支',
392
+ 'action.revert': '撤销此提交',
393
+ 'action.pop': '恢复 (pop)',
394
+ 'action.apply': '应用 (apply)',
395
+ 'action.drop': '删除 (drop)',
396
+ 'action.stashPush': '储藏改动',
397
+ 'action.softReset': 'Soft', 'action.mixedReset': 'Mixed', 'action.hardReset': 'Hard',
398
+ 'group.staged': '已暂存',
399
+ 'group.changes': '变更',
400
+ 'group.untracked': '未跟踪',
401
+ 'group.conflicts': '合并冲突',
402
+ 'group.clean': '没有检测到改动,工作区很干净',
403
+ 'diff.title': '差异',
404
+ 'diff.empty': '选择左侧文件查看差异',
405
+ 'diff.binary': '二进制文件,无法显示文本差异',
406
+ 'diff.truncated': '输出过大已截断',
407
+ 'diff.newFile': '新文件(未跟踪)',
408
+ 'diff.tooLarge': '文件过大({size} bytes),不提供预览',
409
+ 'commit.placeholder': '提交信息 (回车提交)',
410
+ 'commit.identity': '身份:',
411
+ 'commit.identityMissing': '未配置 user.name/user.email,提交将被 git 拒绝',
412
+ 'commit.hint': '提交前请先暂存改动',
413
+ 'commit.busy': '操作进行中…',
414
+ 'commit.branch': '分支',
415
+ 'commit.detached': '分离 HEAD:当前不处于任何分支',
416
+ 'commit.noCommits': '尚无提交',
417
+ 'commit.aheadBehind': '领先 {a} / 落后 {b}',
418
+ 'branch.local': '本地分支',
419
+ 'branch.remote': '远程分支',
420
+ 'branch.create.placeholder': '新分支名称',
421
+ 'branch.current': '当前',
422
+ 'stash.placeholder': '储藏说明 (可选)',
423
+ 'stash.empty': '没有储藏记录',
424
+ 'stash.list': '储藏列表',
425
+ 'timeline.empty': '本工作区还没有 AI 文件修改记录。让 Agent 使用 write/edit 修改文件后会出现在这里。',
426
+ 'timeline.turn': '第 {turn} 轮',
427
+ 'timeline.title': 'AI 会话修改时间线',
428
+ 'timeline.session': '会话',
429
+ 'timeline.running': '当前会话正在运行,AI 可能正在修改文件;高危操作前请确认。',
430
+ 'status.notRepo': '当前目录不是 Git 仓库',
431
+ 'status.noGit': '未找到 git 可执行文件',
432
+ 'status.checking': '正在检测仓库…',
433
+ 'status.loading': '加载中…',
434
+ 'status.error': '读取状态失败',
435
+ 'status.noSession': '当前没有活动会话;切换会话后可查看其工作区。',
436
+ 'confirm.title': '确认操作',
437
+ 'confirm.dangerTitle': '危险操作确认',
438
+ 'confirm.ok': '确认',
439
+ 'confirm.cancel': '取消',
440
+ 'confirm.discardFile': '确定丢弃对 {name} 的改动?此操作不可撤销。',
441
+ 'confirm.discardFiles': '确定丢弃 {n} 个文件的改动?此操作不可撤销。',
442
+ 'confirm.cleanFile': '确定删除未跟踪文件 {name}?此操作不可撤销。',
443
+ 'confirm.cleanFiles': '确定删除 {n} 个未跟踪文件?此操作不可撤销。',
444
+ 'confirm.switch': '确定切换到分支 {name}?未提交的改动会保留。',
445
+ 'confirm.merge': '确定把 {ref} 合并到当前分支?',
446
+ 'confirm.hardReset': 'Hard reset 将丢弃 {target} 之后的所有提交和改动,不可恢复。确定继续?',
447
+ 'confirm.revert': '确定撤销提交 {commit}?',
448
+ 'confirm.dropStash': '确定删除储藏 {ref}?',
449
+ 'confirm.popStash': '确定恢复储藏 {ref}?',
450
+ 'confirm.init': '在当前目录执行 git init?',
451
+ 'toast.busy': '另一个 Git 操作正在进行,请稍候',
452
+ 'toast.done': '操作完成',
453
+ 'toast.aiCommit': '正在生成提交信息…',
454
+ 'output.title': '命令输出',
455
+ 'output.empty': '(无输出)',
456
+ 'misc.on': '于',
457
+ 'misc.paths': '个文件',
458
+ 'misc.stagedCount': '{n} 个文件已暂存',
459
+ 'misc.remote': '远程',
460
+ 'remote.addTitle': '添加远程仓库',
461
+ 'remote.name': '名称',
462
+ 'remote.url.placeholder': 'git@github.com:user/repo.git 或 https://…',
463
+ 'remote.add': '添加',
464
+ 'remote.emptyHint': '还没有配置远程仓库,添加后即可 Pull / Push',
465
+ 'remote.sshNote': '提示:SSH 地址需要本机已配置 GitHub SSH key(口令由 ssh-agent 提供)。',
466
+ }
467
+
468
+ const en = {
469
+ 'panel.title': 'Source Control (Git)',
470
+ 'panel.close': 'Close panel',
471
+ 'tab.status': 'Changes',
472
+ 'tab.files': 'Files',
473
+ 'tab.log': 'Log',
474
+ 'tab.branches': 'Branches',
475
+ 'tab.stash': 'Stash',
476
+ 'tab.timeline': 'AI Edits',
477
+ 'files.title': 'Workspace files',
478
+ 'files.search': 'Filter files…',
479
+ 'files.empty': 'No files in the workspace',
480
+ 'files.tooMany': 'Too many files — showing the first {n}',
481
+ 'files.clean': 'No uncommitted changes (showing current content)',
482
+ 'files.legendModified': 'blue: modified (uncommitted)',
483
+ 'files.legendUntracked': 'red: untracked',
484
+ 'files.legendClean': 'default: unmodified',
485
+ 'cat.title': 'File content',
486
+ 'action.refresh': 'Refresh',
487
+ 'action.stageAll': 'Stage all',
488
+ 'action.unstageAll': 'Unstage all',
489
+ 'action.commit': 'Commit',
490
+ 'action.aiCommit': 'AI Gen',
491
+ 'action.aiCommitTooltip': 'Generate commit message from staged changes using LLM',
492
+ 'action.pull': 'Pull',
493
+ 'action.push': 'Push',
494
+ 'action.fetch': 'Fetch',
495
+ 'action.init': 'Initialize repository',
496
+ 'action.discard': 'Discard changes',
497
+ 'action.stage': 'Stage',
498
+ 'action.unstage': 'Unstage',
499
+ 'action.openDiff': 'Open diff',
500
+ 'action.newBranch': 'New branch',
501
+ 'action.switch': 'Switch',
502
+ 'action.merge': 'Merge into current branch',
503
+ 'action.revert': 'Revert this commit',
504
+ 'action.pop': 'Pop',
505
+ 'action.apply': 'Apply',
506
+ 'action.drop': 'Drop',
507
+ 'action.stashPush': 'Stash changes',
508
+ 'action.softReset': 'Soft', 'action.mixedReset': 'Mixed', 'action.hardReset': 'Hard',
509
+ 'group.staged': 'Staged',
510
+ 'group.changes': 'Changes',
511
+ 'group.untracked': 'Untracked',
512
+ 'group.conflicts': 'Merge conflicts',
513
+ 'group.clean': 'No changes detected — the working tree is clean',
514
+ 'diff.title': 'Diff',
515
+ 'diff.empty': 'Select a file on the left to view its diff',
516
+ 'diff.binary': 'Binary file: no text diff available',
517
+ 'diff.truncated': 'Output truncated (too large)',
518
+ 'diff.newFile': 'New file (untracked)',
519
+ 'diff.tooLarge': 'File too large ({size} bytes): no preview',
520
+ 'commit.placeholder': 'Commit message (Enter to commit)',
521
+ 'commit.identity': 'Identity:',
522
+ 'commit.identityMissing': 'user.name / user.email not configured; git will refuse to commit',
523
+ 'commit.hint': 'Stage changes before committing',
524
+ 'commit.busy': 'Operation in progress…',
525
+ 'commit.branch': 'Branch',
526
+ 'commit.detached': 'Detached HEAD: not on any branch',
527
+ 'commit.noCommits': 'No commits yet',
528
+ 'commit.aheadBehind': 'ahead {a} / behind {b}',
529
+ 'branch.local': 'Local branches',
530
+ 'branch.remote': 'Remote branches',
531
+ 'branch.create.placeholder': 'New branch name',
532
+ 'branch.current': 'current',
533
+ 'stash.placeholder': 'Stash message (optional)',
534
+ 'stash.empty': 'No stashes',
535
+ 'stash.list': 'Stash list',
536
+ 'timeline.empty': 'No AI file edits recorded for this workspace yet. write/edit tool calls will show up here.',
537
+ 'timeline.turn': 'turn {turn}',
538
+ 'timeline.title': 'AI session edit timeline',
539
+ 'timeline.session': 'session',
540
+ 'timeline.running': 'The current session is running — the agent may be editing files; be careful with destructive operations.',
541
+ 'status.notRepo': 'The current directory is not a Git repository',
542
+ 'status.noGit': 'git executable not found',
543
+ 'status.checking': 'Detecting repository…',
544
+ 'status.loading': 'Loading…',
545
+ 'status.error': 'Failed to read status',
546
+ 'status.noSession': 'No active session; pick one to inspect its workspace.',
547
+ 'confirm.title': 'Confirm',
548
+ 'confirm.dangerTitle': 'Destructive operation',
549
+ 'confirm.ok': 'Confirm',
550
+ 'confirm.cancel': 'Cancel',
551
+ 'confirm.discardFile': 'Discard changes to {name}? This cannot be undone.',
552
+ 'confirm.discardFiles': 'Discard changes to {n} files? This cannot be undone.',
553
+ 'confirm.cleanFile': 'Delete untracked file {name}? This cannot be undone.',
554
+ 'confirm.cleanFiles': 'Delete {n} untracked files? This cannot be undone.',
555
+ 'confirm.switch': 'Switch to branch {name}? Uncommitted changes are kept.',
556
+ 'confirm.merge': 'Merge {ref} into the current branch?',
557
+ 'confirm.hardReset': 'Hard reset discards all commits and changes after {target}. This is irreversible. Continue?',
558
+ 'confirm.revert': 'Revert commit {commit}?',
559
+ 'confirm.dropStash': 'Drop stash {ref}?',
560
+ 'confirm.popStash': 'Pop stash {ref}?',
561
+ 'confirm.init': 'Run git init in the current directory?',
562
+ 'toast.busy': 'Another Git operation is in progress',
563
+ 'toast.done': 'Done',
564
+ 'toast.aiCommit': 'Generating commit message…',
565
+ 'output.title': 'Command output',
566
+ 'output.empty': '(no output)',
567
+ 'misc.on': 'on',
568
+ 'misc.paths': 'files',
569
+ 'misc.stagedCount': '{n} staged',
570
+ 'misc.remote': 'Remotes',
571
+ 'remote.addTitle': 'Add remote repository',
572
+ 'remote.name': 'Name',
573
+ 'remote.url.placeholder': 'git@github.com:user/repo.git or https://…',
574
+ 'remote.add': 'Add',
575
+ 'remote.emptyHint': 'No remote configured yet — add one to pull / push',
576
+ 'remote.sshNote': 'Note: SSH URLs require a GitHub SSH key on this machine (passphrase via ssh-agent).',
577
+ }
578
+
579
+ let lang = 'zh'
580
+ if (typeof document !== 'undefined') {
581
+ const htmlLang = document.documentElement?.lang ?? ''
582
+ if (htmlLang.toLowerCase().startsWith('en')) lang = 'en'
583
+ }
584
+
585
+ function t(key, params) {
586
+ const dict = lang === 'en' ? en : zh
587
+ let text = dict[key] ?? zh[key] ?? key
588
+ if (params) {
589
+ for (const [k, v] of Object.entries(params)) text = text.split(`{${k}}`).join(String(v))
590
+ }
591
+ return text
592
+ }
593
+
594
+ module.exports = { t, lang }
595
+
596
+ },
597
+ "./index.js": function (module, exports, require) {
598
+ /**
599
+ * Browser half of dsh-git-gui — the plugin body exported to the web shell.
600
+ *
601
+ * The micro-bundler wraps the whole client-src tree into the
602
+ * `window.__ModuleLoader__.load({ id, factory })` CJS shape; the shell
603
+ * materializes this module and treats its exports as a cordis object plugin.
604
+ * `./pkg-id` is a synthetic module the bundler generates from package.json.
605
+ */
606
+
607
+ const { startController } = require('./control')
608
+ const { makeGitApi } = require('./api')
609
+ const { FooterButton } = require('./v-footer')
610
+ const { GitPanel } = require('./v-panel')
611
+ const { css } = require('./styles')
612
+ const PKG_ID = require('./pkg-id')
613
+
614
+ const inject = ['connection', 'slots']
615
+
616
+ function apply(ctx) {
617
+ // plugin-owned stylesheet; the module loader inventories this tag id so the
618
+ // HMR driver can remove it together with the plugin.
619
+ if (typeof document !== 'undefined') {
620
+ const tagId = `${PKG_ID}/styles.css`
621
+ if (document.querySelector(`style[data-plugin-css="${tagId}"]`) === null) {
622
+ const tag = document.createElement('style')
623
+ tag.dataset.plugin = PKG_ID
624
+ tag.dataset.pluginCss = tagId
625
+ tag.textContent = css
626
+ document.head.appendChild(tag)
627
+ }
628
+ }
629
+
630
+ const api = makeGitApi(ctx.connection)
631
+ startController(api)
632
+
633
+ ctx.effect(() => {
634
+ const disposeFooter = ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register(
635
+ { name: 'sidebar.footer.action', id: 'dsh-git-gui', order: 20 },
636
+ (props) => FooterButton(props),
637
+ ))
638
+ const disposePanel = ctx.slots.inject('shell.overlay', () => ctx.slots.register(
639
+ { name: 'shell.overlay', id: 'dsh-git-gui' },
640
+ (props) => GitPanel(props),
641
+ ))
642
+ return () => {
643
+ disposeFooter()
644
+ disposePanel()
645
+ }
646
+ })
647
+ }
648
+
649
+ module.exports = { apply, inject }
650
+ // test-only store access (used by tests/client.test.mjs smoke tests)
651
+ module.exports.__test = { setState: require('./store').setState, getState: require('./store').getState }
652
+
653
+ },
654
+ "./store.js": function (module, exports, require) {
655
+ /**
656
+ * Tiny observable store for the Git panel (useSyncExternalStore-based).
657
+ */
658
+ const React = require('react')
659
+
660
+ const initial = {
661
+ open: false,
662
+ tab: 'status',
663
+ busy: false,
664
+ busyLabel: '',
665
+ cwd: null, // absolute workspace root of the current session
666
+ sessionId: null,
667
+ sessionRunning: false,
668
+ check: null, // {repo, root, gitVersion}
669
+ status: null, // {branch, files}
670
+ statusError: null,
671
+ selected: null, // {path, staged, untracked, cat}
672
+ diff: null, // {path, staged, untracked, cat, loading, error, data}
673
+ commits: [],
674
+ logError: null,
675
+ refs: [],
676
+ refsError: null,
677
+ stashes: [],
678
+ stashError: null,
679
+ activity: [],
680
+ identity: null,
681
+ tree: null, // {files:[{path,state}], truncated, total}
682
+ treeError: null,
683
+ remotes: [], // [{name, fetch?, push?}]
684
+ remoteModal: false, // "添加远程仓库" 弹窗
685
+ commitMsg: '',
686
+ output: null, // {title, text}
687
+ confirm: null, // {body, danger, action}
688
+ toast: null, // {kind, text}
689
+ }
690
+
691
+ let state = initial
692
+ const listeners = new Set()
693
+
694
+ function getState() {
695
+ return state
696
+ }
697
+
698
+ function setState(patch) {
699
+ state = { ...state, ...patch }
700
+ for (const listener of listeners) listener()
701
+ }
702
+
703
+ function resetWorkspace(cwd, sessionId) {
704
+ state = {
705
+ ...initial,
706
+ cwd,
707
+ sessionId,
708
+ sessionRunning: state.sessionRunning,
709
+ open: state.open,
710
+ tab: state.tab,
711
+ commitMsg: state.commitMsg,
712
+ }
713
+ for (const listener of listeners) listener()
714
+ }
715
+
716
+ function subscribe(listener) {
717
+ listeners.add(listener)
718
+ return () => listeners.delete(listener)
719
+ }
720
+
721
+ function useStore(selector) {
722
+ const ref = React.useRef(undefined)
723
+ const getSnapshot = React.useCallback(() => {
724
+ const next = selector(state)
725
+ if (next !== ref.current) ref.current = next
726
+ return ref.current
727
+ }, [selector])
728
+ return React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
729
+ }
730
+
731
+ /** Convenience hook: open flag. */
732
+ function useOpen() {
733
+ return useStore((s) => s.open)
734
+ }
735
+
736
+ module.exports = { getState, setState, resetWorkspace, subscribe, useStore, useOpen }
737
+
738
+ },
739
+ "./styles.js": function (module, exports, require) {
740
+ /**
741
+ * Panel stylesheet, injected as a plugin-owned <style> tag at
742
+ * materialization. Colors ride the shared theme tokens so light/dark both
743
+ * work; every class is `gg-` prefixed to avoid collisions.
744
+ */
745
+ const css = `
746
+ /* text on accent fills: brand/state tokens flip to LIGHT shades in dark
747
+ mode (brand-primary = near-white), so fixed white text became invisible;
748
+ this variable flips together with the shell's dark-theme attribute and is
749
+ defined at body level because the sidebar badge lives outside the panel. */
750
+ body { --gg-on-accent: #ffffff; }
751
+ body[data-ds-dark-theme] { --gg-on-accent: #10141b; }
752
+ .gg-panel {
753
+ position: absolute; top: 12px; right: 12px; bottom: 12px;
754
+ display: flex; flex-direction: column;
755
+ background: var(--dsw-alias-bg-layer-1);
756
+ border: 1px solid var(--dsw-alias-border-l2);
757
+ border-radius: 12px;
758
+ box-shadow: 0 12px 40px rgba(0,0,0,.28);
759
+ overflow: hidden;
760
+ pointer-events: auto;
761
+ z-index: 1;
762
+ font-size: 12.5px;
763
+ color: var(--dsw-alias-label-primary);
764
+ }
765
+ .gg-resize {
766
+ position: absolute; left: -3px; top: 0; bottom: 0; width: 7px;
767
+ cursor: col-resize; z-index: 3;
768
+ }
769
+ .gg-resize:hover { background: var(--dsw-alias-interactive-bg-hover); }
770
+ .gg-panel-head {
771
+ display: flex; align-items: center; gap: 8px;
772
+ padding: 8px 10px 6px 14px;
773
+ border-bottom: 1px solid var(--dsw-alias-border-l1);
774
+ }
775
+ .gg-panel-title { font-weight: 600; flex: 1; }
776
+ .gg-icon-btn {
777
+ display: inline-flex; align-items: center; justify-content: center;
778
+ width: 26px; height: 26px; border: none; border-radius: 6px;
779
+ background: transparent; color: var(--dsw-alias-label-secondary);
780
+ cursor: pointer; padding: 0; flex: none;
781
+ }
782
+ .gg-icon-btn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
783
+ .gg-icon-btn:disabled { opacity: .4; cursor: default; }
784
+ .gg-btn {
785
+ display: inline-flex; align-items: center; justify-content: center; gap: 5px;
786
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px;
787
+ background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary);
788
+ font-size: 12px; padding: 4px 10px; cursor: pointer; white-space: nowrap;
789
+ }
790
+ .gg-btn:hover { background: var(--dsw-alias-interactive-bg-hover); }
791
+ .gg-btn:disabled { opacity: .45; cursor: default; }
792
+ .gg-btn-primary {
793
+ background: var(--dsw-alias-brand-primary); border-color: transparent; color: var(--gg-on-accent);
794
+ }
795
+ .gg-btn-primary:hover { filter: brightness(1.08); background: var(--dsw-alias-brand-primary); color: var(--gg-on-accent); }
796
+ .gg-btn-danger { background: var(--dsw-alias-state-error-primary); border-color: transparent; color: var(--gg-on-accent); }
797
+ .gg-mini-btn {
798
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 5px;
799
+ background: transparent; color: var(--dsw-alias-label-secondary);
800
+ font-size: 11px; padding: 1px 7px; cursor: pointer;
801
+ display: inline-flex; align-items: center; gap: 4px;
802
+ }
803
+ .gg-mini-btn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
804
+ .gg-mini-btn:disabled { opacity: .4; cursor: default; }
805
+ .gg-mini-danger:hover { color: var(--dsw-alias-state-error-primary); }
806
+ .gg-row-btn { width: 22px; height: 22px; opacity: 0; }
807
+ .gg-file:hover .gg-row-btn, .gg-log-row:hover .gg-row-btn { opacity: 1; }
808
+
809
+ .gg-banner {
810
+ display: flex; align-items: center; gap: 6px;
811
+ margin: 6px 10px 0; padding: 5px 9px;
812
+ border-radius: 7px; font-size: 11.5px;
813
+ background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);
814
+ color: var(--dsw-alias-label-primary);
815
+ }
816
+ .gg-banner .gg-icon { flex: none; color: var(--dsw-alias-state-warn-primary); }
817
+
818
+ .gg-wsbar {
819
+ display: flex; align-items: center; gap: 7px;
820
+ padding: 7px 10px 7px 14px;
821
+ border-bottom: 1px solid var(--dsw-alias-border-l1);
822
+ color: var(--dsw-alias-label-secondary); font-size: 12px;
823
+ }
824
+ .gg-ws-name { display: inline-flex; align-items: center; gap: 5px; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
825
+ .gg-ws-branch { display: inline-flex; align-items: center; gap: 4px; color: var(--dsw-alias-brand-primary); max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
826
+ .gg-ws-detached { color: var(--dsw-alias-state-warn-primary); }
827
+ .gg-ws-ab { font-size: 11px; }
828
+ .gg-ws-busy { color: var(--dsw-alias-brand-primary); font-size: 11px; }
829
+ .gg-spacer { flex: 1; }
830
+
831
+ .gg-tabs { display: flex; flex-direction: column; flex: 1; min-height: 0; }
832
+ .gg-tabbar {
833
+ display: flex; gap: 2px; padding: 5px 10px 0;
834
+ border-bottom: 1px solid var(--dsw-alias-border-l1);
835
+ overflow-x: auto; flex: none;
836
+ }
837
+ .gg-tab {
838
+ border: none; background: transparent; cursor: pointer;
839
+ color: var(--dsw-alias-label-secondary); font-size: 12px;
840
+ padding: 6px 9px; border-bottom: 2px solid transparent; white-space: nowrap;
841
+ }
842
+ .gg-tab:hover { color: var(--dsw-alias-label-primary); }
843
+ .gg-tab-active { color: var(--dsw-alias-label-primary); border-bottom-color: var(--dsw-alias-brand-primary); font-weight: 600; }
844
+ .gg-tabbody { flex: 1; min-height: 0; display: flex; flex-direction: column; }
845
+ .gg-panel-body { flex: 1; min-height: 0; display: flex; flex-direction: column; }
846
+
847
+ .gg-empty {
848
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
849
+ gap: 8px; padding: 26px 14px; text-align: center;
850
+ color: var(--dsw-alias-label-secondary); flex: 1;
851
+ }
852
+ .gg-empty-err { color: var(--dsw-alias-state-error-primary); }
853
+ .gg-empty-title { font-weight: 600; color: var(--dsw-alias-label-primary); }
854
+
855
+ .gg-status { display: flex; flex-direction: column; flex: 1; min-height: 0; }
856
+ .gg-commit { padding: 8px 10px; border-bottom: 1px solid var(--dsw-alias-border-l1); flex: none; }
857
+ .gg-commit-input {
858
+ width: 100%; box-sizing: border-box; resize: vertical;
859
+ background: var(--dsw-alias-bg-layer-2);
860
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 7px;
861
+ color: var(--dsw-alias-label-primary); font: inherit; padding: 6px 8px;
862
+ }
863
+ .gg-commit-input:focus { outline: 1px solid var(--dsw-alias-brand-primary); }
864
+ .gg-commit-foot { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
865
+ .gg-identity { font-size: 11px; color: var(--dsw-alias-label-secondary); max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
866
+ .gg-identity-missing { color: var(--dsw-alias-state-warn-primary); }
867
+ .gg-status-main { display: flex; flex-direction: column; flex: 1; min-height: 0; }
868
+ .gg-files { flex: 1 1 45%; min-height: 90px; overflow-y: auto; }
869
+
870
+ .gg-group { border-bottom: 1px solid var(--dsw-alias-border-l1); }
871
+ .gg-group-head {
872
+ display: flex; align-items: center; gap: 7px;
873
+ padding: 5px 12px 4px; font-weight: 600; font-size: 11px;
874
+ color: var(--dsw-alias-label-secondary); position: sticky; top: 0;
875
+ background: var(--dsw-alias-bg-layer-1); z-index: 1;
876
+ }
877
+ .gg-group-name { font-weight: 600; font-size: 11px; color: var(--dsw-alias-label-secondary); }
878
+ .gg-group-count { font-weight: 400; }
879
+ .gg-group-body { display: flex; flex-direction: column; }
880
+
881
+ .gg-file {
882
+ display: flex; align-items: center; gap: 7px;
883
+ padding: 3px 10px; cursor: pointer;
884
+ }
885
+ .gg-file:hover { background: var(--dsw-alias-interactive-bg-hover); }
886
+ .gg-file-conflict .gg-file-name { color: var(--dsw-alias-state-error-primary); }
887
+ .gg-letter {
888
+ flex: none; width: 17px; height: 17px; border: none; border-radius: 4px;
889
+ font-size: 11px; font-weight: 700; color: var(--gg-on-accent); cursor: pointer;
890
+ display: inline-flex; align-items: center; justify-content: center;
891
+ }
892
+ .gg-l-m { background: var(--dsw-alias-brand-primary); }
893
+ .gg-l-a { background: var(--dsw-alias-state-success-primary); }
894
+ .gg-l-d { background: var(--dsw-alias-state-error-primary); }
895
+ .gg-l-r { background: var(--dsw-alias-state-warn-primary); }
896
+ .gg-l-c { background: var(--dsw-alias-state-warn-primary); }
897
+ .gg-l-u { background: var(--dsw-alias-state-error-primary); }
898
+ .gg-l-q { background: var(--dsw-alias-label-secondary); }
899
+ .gg-l-t { background: var(--dsw-alias-label-secondary); }
900
+ .gg-file-main { display: flex; min-width: 0; flex: 1; }
901
+ .gg-file-dir { color: var(--dsw-alias-label-secondary); }
902
+ .gg-file-name { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
903
+ .gg-file-renamed { color: var(--dsw-alias-label-secondary); font-size: 11px; margin-left: 6px; }
904
+
905
+ .gg-diff { flex: 1 1 55%; min-height: 120px; display: flex; flex-direction: column; border-top: 1px solid var(--dsw-alias-border-l2); }
906
+ .gg-diff-head {
907
+ display: flex; align-items: center; gap: 7px; padding: 5px 12px;
908
+ border-bottom: 1px solid var(--dsw-alias-border-l1); flex: none;
909
+ font-weight: 600; font-size: 11.5px;
910
+ }
911
+ .gg-diff-path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
912
+ .gg-diff-rename { color: var(--dsw-alias-label-secondary); font-weight: 400; font-size: 11px; }
913
+ .gg-chip {
914
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 999px;
915
+ font-size: 10.5px; padding: 0 7px; color: var(--dsw-alias-label-secondary);
916
+ white-space: nowrap; flex: none;
917
+ }
918
+ .gg-diff-scroll { flex: 1; overflow: auto; }
919
+ .gg-hunks { font-family: ui-monospace, 'Cascadia Mono', Consolas, monospace; font-size: 11.5px; }
920
+ .gg-hunk-head { padding: 4px 10px; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-2); position: sticky; top: 0; }
921
+ .gg-line { display: flex; align-items: baseline; white-space: pre; padding: 0 10px; min-width: max-content; }
922
+ .gg-line-no { flex: none; width: 34px; text-align: right; color: var(--dsw-alias-label-secondary); font-size: 10.5px; user-select: none; padding-right: 8px; }
923
+ .gg-line-mark { flex: none; width: 14px; text-align: center; user-select: none; }
924
+ .gg-line-text { flex: 1; }
925
+ .gg-line-add { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 11%, transparent); }
926
+ .gg-line-add .gg-line-mark { color: var(--dsw-alias-state-success-primary); }
927
+ .gg-line-del { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 11%, transparent); }
928
+ .gg-line-del .gg-line-mark { color: var(--dsw-alias-state-error-primary); }
929
+ .gg-line-nonl { color: var(--dsw-alias-state-warn-primary); margin-left: 6px; }
930
+
931
+ .gg-log { overflow-y: auto; flex: 1; }
932
+ .gg-log-row { display: flex; align-items: center; gap: 6px; padding: 7px 10px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
933
+ .gg-log-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
934
+ .gg-log-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
935
+ .gg-log-subject { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
936
+ .gg-log-meta { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--dsw-alias-label-secondary); }
937
+ .gg-log-hash { font-family: ui-monospace, Consolas, monospace; }
938
+
939
+ .gg-branches { overflow-y: auto; flex: 1; padding-bottom: 12px; }
940
+ .gg-branch-actions { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 10px; }
941
+ .gg-branch-new { display: flex; gap: 6px; padding: 0 10px 8px; }
942
+ .gg-input {
943
+ flex: 1; min-width: 0; background: var(--dsw-alias-bg-layer-2);
944
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px;
945
+ color: var(--dsw-alias-label-primary); font: inherit; padding: 5px 8px;
946
+ }
947
+ .gg-input:focus { outline: 1px solid var(--dsw-alias-brand-primary); }
948
+ .gg-ref-group { padding: 6px 10px 4px; }
949
+ .gg-ref-row { display: flex; align-items: center; gap: 7px; padding: 3px 0; }
950
+ .gg-ref-current { font-weight: 600; }
951
+ .gg-ref-name {
952
+ display: inline-flex; align-items: center; gap: 6px; flex: 1; min-width: 0;
953
+ border: none; background: transparent; color: inherit; font: inherit;
954
+ text-align: left; cursor: pointer; padding: 2px 0; overflow: hidden;
955
+ }
956
+ .gg-ref-name:hover:not(:disabled) { color: var(--dsw-alias-brand-primary); }
957
+ .gg-ref-name:disabled { cursor: default; }
958
+ .gg-ref-upstream { color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 400; }
959
+ .gg-remote-row { display: flex; gap: 8px; padding: 2px 0; }
960
+ .gg-remote-url { color: var(--dsw-alias-label-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
961
+
962
+ .gg-stash { overflow-y: auto; flex: 1; }
963
+ .gg-stash-new { display: flex; gap: 6px; padding: 8px 10px; }
964
+ .gg-stash-list { padding: 0 10px 8px; }
965
+ .gg-stash-row { display: flex; align-items: center; gap: 7px; padding: 4px 0; border-bottom: 1px solid var(--dsw-alias-border-l1); }
966
+ .gg-stash-ref { font-family: ui-monospace, Consolas, monospace; font-size: 11px; flex: none; }
967
+ .gg-stash-subject { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
968
+
969
+ .gg-timeline { overflow-y: auto; flex: 1; padding: 6px 10px; }
970
+ .gg-tl-row {
971
+ display: flex; align-items: center; gap: 8px; width: 100%;
972
+ border: none; border-bottom: 1px solid var(--dsw-alias-border-l1);
973
+ background: transparent; color: inherit; font: inherit; text-align: left;
974
+ padding: 6px 2px; cursor: pointer;
975
+ }
976
+ .gg-tl-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
977
+ .gg-tl-tool {
978
+ flex: none; border: 1px solid var(--dsw-alias-border-l2); border-radius: 4px;
979
+ font-size: 10px; padding: 0 5px; color: var(--dsw-alias-label-secondary);
980
+ font-family: ui-monospace, Consolas, monospace;
981
+ }
982
+ .gg-tl-path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
983
+ .gg-tl-meta { flex: none; font-size: 10.5px; color: var(--dsw-alias-label-secondary); }
984
+
985
+ /* workspace file tree */
986
+ .gg-files-view { display: flex; flex-direction: column; flex: 1; min-height: 0; }
987
+ .gg-files-toolbar {
988
+ display: flex; align-items: center; gap: 10px; padding: 7px 10px;
989
+ border-bottom: 1px solid var(--dsw-alias-border-l1); flex: none;
990
+ }
991
+ .gg-files-legend { display: flex; gap: 10px; flex: none; font-size: 10.5px; color: var(--dsw-alias-label-secondary); }
992
+ .gg-legend-item { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
993
+ .gg-swatch { width: 9px; height: 9px; border-radius: 2px; display: inline-block; flex: none; }
994
+ .gg-swatch-mod { background: var(--dsw-alias-state-business-primary, #3964fe); }
995
+ .gg-swatch-unt { background: var(--dsw-alias-state-error-primary, #dc2626); }
996
+ .gg-swatch-clean { background: var(--dsw-alias-label-primary); }
997
+ .gg-files-tree { flex: 1; overflow: auto; padding-bottom: 8px; }
998
+ .gg-files-note { padding: 4px 10px 8px; font-size: 11px; color: var(--dsw-alias-label-secondary); }
999
+ .gg-tree-row {
1000
+ display: flex; align-items: center; gap: 5px;
1001
+ padding: 2.5px 10px; cursor: pointer; min-width: 0;
1002
+ }
1003
+ .gg-tree-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
1004
+ .gg-tree-dir { color: var(--dsw-alias-label-secondary); }
1005
+ .gg-tree-file { color: var(--dsw-alias-label-primary); }
1006
+ .gg-tree-file.gg-tree-modified { color: var(--dsw-alias-state-business-primary, #3964fe); }
1007
+ .gg-tree-file.gg-tree-untracked { color: var(--dsw-alias-state-error-primary, #dc2626); }
1008
+ .gg-tree-toggle {
1009
+ flex: none; width: 16px; height: 16px; border: none; background: transparent;
1010
+ color: inherit; cursor: pointer; padding: 0;
1011
+ display: inline-flex; align-items: center; justify-content: center;
1012
+ }
1013
+ .gg-tree-chevron { display: inline-flex; transition: transform .12s ease; }
1014
+ .gg-tree-chevron-closed { transform: rotate(-90deg); }
1015
+ .gg-tree-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1016
+
1017
+ .gg-output { border-top: 1px solid var(--dsw-alias-border-l1); flex: none; }
1018
+ .gg-output-head {
1019
+ display: flex; align-items: center; gap: 6px; width: 100%;
1020
+ border: none; background: transparent; color: var(--dsw-alias-label-secondary);
1021
+ font: inherit; font-size: 11px; padding: 5px 12px; cursor: pointer;
1022
+ }
1023
+ .gg-output-head:hover { background: var(--dsw-alias-interactive-bg-hover); }
1024
+ .gg-output-title { flex: 1; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1025
+ .gg-output-body {
1026
+ margin: 0 12px 10px; padding: 8px; max-height: 130px; overflow: auto;
1027
+ background: var(--dsw-alias-bg-layer-2); border-radius: 7px;
1028
+ font-family: ui-monospace, Consolas, monospace; font-size: 11px;
1029
+ white-space: pre-wrap; word-break: break-all;
1030
+ }
1031
+
1032
+ .gg-modal-backdrop {
1033
+ position: absolute; inset: 0; background: rgba(0,0,0,.45);
1034
+ display: flex; align-items: center; justify-content: center; z-index: 6;
1035
+ }
1036
+ .gg-modal {
1037
+ width: min(380px, calc(100% - 40px)); background: var(--dsw-alias-bg-layer-1);
1038
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px;
1039
+ padding: 14px; display: flex; flex-direction: column; gap: 10px;
1040
+ box-shadow: 0 16px 50px rgba(0,0,0,.35);
1041
+ }
1042
+ .gg-modal-danger { border-color: var(--dsw-alias-state-error-primary); }
1043
+ .gg-modal-title { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 13px; }
1044
+ .gg-modal-danger .gg-modal-title { color: var(--dsw-alias-state-error-primary); }
1045
+ .gg-modal-body { font-size: 12.5px; color: var(--dsw-alias-label-secondary); word-break: break-all; }
1046
+ .gg-modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
1047
+ .gg-field { display: flex; flex-direction: column; gap: 4px; }
1048
+ .gg-field-label { font-size: 11px; color: var(--dsw-alias-label-secondary); }
1049
+ .gg-icon-warn { color: var(--dsw-alias-state-warn-primary); }
1050
+
1051
+ .gg-toast {
1052
+ position: absolute; left: 12px; right: 12px; bottom: 12px; z-index: 7;
1053
+ padding: 8px 12px; border-radius: 8px; font-size: 12px;
1054
+ background: var(--dsw-alias-bg-overlay);
1055
+ border: 1px solid var(--dsw-alias-border-l2);
1056
+ box-shadow: 0 8px 24px rgba(0,0,0,.3);
1057
+ word-break: break-all;
1058
+ }
1059
+ .gg-toast-err { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); }
1060
+ .gg-toast-warn { border-color: var(--dsw-alias-state-warn-primary); color: var(--dsw-alias-state-warn-primary); }
1061
+
1062
+ /* sidebar footer entry */
1063
+ .gg-footer-btn {
1064
+ display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;
1065
+ border: none; background: transparent; cursor: pointer;
1066
+ color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px;
1067
+ padding: 0 6px; height: 36px; border-radius: 8px; position: relative;
1068
+ }
1069
+ .gg-footer-btn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
1070
+ .gg-footer-btn.gg-active { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
1071
+ .gg-footer-icon { display: inline-flex; align-items: center; justify-content: center; }
1072
+ .gg-footer-marks { display: inline-flex; gap: 3px; align-items: center; }
1073
+ .gg-badge {
1074
+ min-width: 15px; height: 15px; padding: 0 4px; border-radius: 999px;
1075
+ background: var(--dsw-alias-brand-primary); color: var(--gg-on-accent);
1076
+ font-size: 10px; font-weight: 700;
1077
+ display: inline-flex; align-items: center; justify-content: center;
1078
+ box-sizing: border-box;
1079
+ }
1080
+ .gg-badge-err { background: var(--dsw-alias-state-error-primary); }
1081
+
1082
+ .gg-icon { flex: none; }
1083
+ `
1084
+
1085
+ module.exports = { css }
1086
+
1087
+ },
1088
+ "./v-branch.js": function (module, exports, require) {
1089
+ /**
1090
+ * Branches view: local/remote refs, switch/create, merge, pull/push/fetch.
1091
+ */
1092
+ const { h, cx, ICONS, React } = require('./dom')
1093
+ const { useStore, setState } = require('./store')
1094
+ const { t } = require('./i18n')
1095
+ const { run, confirmThen, getApi, registerTabLoader } = require('./control')
1096
+
1097
+ function BranchView() {
1098
+ const refs = useStore((s) => s.refs)
1099
+ const remotes = useStore((s) => s.remotes)
1100
+ const refsError = useStore((s) => s.refsError)
1101
+ const cwd = useStore((s) => s.cwd)
1102
+ const busy = useStore((s) => s.busy)
1103
+ const branch = useStore((s) => s.status?.branch)
1104
+ const [newName, setNewName] = React.useState('')
1105
+
1106
+ React.useEffect(() => {
1107
+ registerTabLoader('branches', async (w) => {
1108
+ const result = await getApi().branches(w)
1109
+ setState({ refs: result.refs, remotes: result.remotes, refsError: null })
1110
+ })
1111
+ if (cwd !== null) {
1112
+ getApi().branches(cwd).then((r) => setState({ refs: r.refs, remotes: r.remotes, refsError: null }))
1113
+ .catch((e) => setState({ refs: [], remotes: [], refsError: e.message }))
1114
+ }
1115
+ }, [cwd])
1116
+
1117
+ const local = refs.filter((r) => !r.name.includes('/') && !r.name.startsWith('remotes/'))
1118
+ const remote = refs.filter((r) => r.name.includes('/'))
1119
+
1120
+ const doSwitch = (name) => {
1121
+ confirmThen({
1122
+ body: t('confirm.switch', { name }),
1123
+ action: () => run(t('action.switch'), () => getApi().switchBranch(cwd, name, false)),
1124
+ })
1125
+ }
1126
+ const doMerge = (name) => {
1127
+ confirmThen({
1128
+ body: t('confirm.merge', { ref: name }),
1129
+ action: () => run(t('action.merge'), () => getApi().merge(cwd, name)),
1130
+ })
1131
+ }
1132
+ const doCreate = () => {
1133
+ const name = newName.trim()
1134
+ if (name === '') return
1135
+ run(t('action.newBranch'), () => getApi().switchBranch(cwd, name, true)).then((ok) => {
1136
+ if (ok) setNewName('')
1137
+ })
1138
+ }
1139
+ const doPull = (mode) => run(`${t('action.pull')} (${mode})`, () => getApi().pull(cwd, mode))
1140
+ const doPush = () => run(t('action.push'), () => getApi().push(cwd))
1141
+ const doFetch = () => run(t('action.fetch'), () => getApi().fetch(cwd))
1142
+
1143
+ const row = (r, kind) => h('div', {
1144
+ className: cx('gg-ref-row', r.current && 'gg-ref-current'),
1145
+ key: r.name,
1146
+ title: r.name,
1147
+ },
1148
+ h('button', {
1149
+ type: 'button',
1150
+ className: 'gg-ref-name',
1151
+ disabled: busy || r.current,
1152
+ onClick: () => (kind === 'local' ? doSwitch(r.name) : undefined),
1153
+ },
1154
+ ICONS.branch,
1155
+ h('span', {}, r.name),
1156
+ r.current && h('span', { className: 'gg-chip' }, t('branch.current')),
1157
+ r.upstream && h('span', { className: 'gg-ref-upstream' }, `→ ${r.upstream}${r.track ? ` [${r.track}]` : ''}`)),
1158
+ !r.current && kind === 'local' && h('button', {
1159
+ type: 'button',
1160
+ className: 'gg-mini-btn',
1161
+ disabled: busy,
1162
+ onClick: () => doMerge(r.name),
1163
+ }, t('action.merge')))
1164
+
1165
+ return h('div', { className: 'gg-branches' },
1166
+ h('div', { className: 'gg-branch-actions' },
1167
+ h('button', { type: 'button', className: 'gg-btn', disabled: busy, onClick: () => doPull('ff-only') }, 'Pull (ff)'),
1168
+ h('button', { type: 'button', className: 'gg-btn', disabled: busy, onClick: () => doPull('merge') }, 'Pull (merge)'),
1169
+ h('button', { type: 'button', className: 'gg-btn', disabled: busy, onClick: () => doPull('rebase') }, 'Pull (rebase)'),
1170
+ h('button', { type: 'button', className: 'gg-btn', disabled: busy, onClick: doPush }, t('action.push')),
1171
+ h('button', { type: 'button', className: 'gg-btn', disabled: busy, onClick: doFetch }, t('action.fetch'))),
1172
+ refsError && h('div', { className: 'gg-empty gg-empty-err' }, refsError),
1173
+ branch?.detached && h('div', { className: 'gg-banner' }, t('commit.detached')),
1174
+ h('div', { className: 'gg-branch-new' },
1175
+ h('input', {
1176
+ className: 'gg-input',
1177
+ value: newName,
1178
+ placeholder: t('branch.create.placeholder'),
1179
+ onChange: (e) => setNewName(e.target.value),
1180
+ onKeyDown: (e) => { if (e.key === 'Enter') doCreate() },
1181
+ }),
1182
+ h('button', { type: 'button', className: 'gg-btn gg-btn-primary', disabled: busy || newName.trim() === '', onClick: doCreate }, t('action.newBranch'))),
1183
+ h('div', { className: 'gg-ref-group' },
1184
+ h('div', { className: 'gg-group-name' }, t('branch.local')),
1185
+ local.map((r) => row(r, 'local'))),
1186
+ remote.length > 0 && h('div', { className: 'gg-ref-group' },
1187
+ h('div', { className: 'gg-group-name' }, t('branch.remote')),
1188
+ remote.map((r) => row(r, 'remote'))),
1189
+ remotes.length > 0 && h('div', { className: 'gg-ref-group' },
1190
+ h('div', { className: 'gg-group-name' }, t('misc.remote')),
1191
+ remotes.map((r) => h('div', { className: 'gg-remote-row', key: r.name },
1192
+ h('span', {}, r.name), h('span', { className: 'gg-remote-url' }, r.fetch ?? r.push ?? '')))),
1193
+ )
1194
+ }
1195
+
1196
+ module.exports = { BranchView }
1197
+
1198
+ },
1199
+ "./v-files.js": function (module, exports, require) {
1200
+ /**
1201
+ * Workspace file tree view: every tracked/untracked (non-ignored) file with
1202
+ * a git-state color — default (theme text) = unmodified, blue = modified
1203
+ * uncommitted, red = untracked. Clicking opens the diff / content preview.
1204
+ */
1205
+ const { h, cx, ICONS, React } = require('./dom')
1206
+ const { useStore, setState } = require('./store')
1207
+ const { t } = require('./i18n')
1208
+ const { getApi, registerTabLoader, refreshDiff } = require('./control')
1209
+
1210
+ const MAX_RENDER_ROWS = 800
1211
+
1212
+ function buildTree(files, filter) {
1213
+ const root = { dirs: new Map(), files: [] }
1214
+ const needle = (filter ?? '').trim().toLowerCase()
1215
+ for (const file of files) {
1216
+ if (needle !== '' && !file.path.toLowerCase().includes(needle)) continue
1217
+ const segments = file.path.split('/')
1218
+ const name = segments.pop()
1219
+ let node = root
1220
+ for (const segment of segments) {
1221
+ if (!node.dirs.has(segment)) node.dirs.set(segment, { dirs: new Map(), files: [] })
1222
+ node = node.dirs.get(segment)
1223
+ }
1224
+ node.files.push({ name, path: file.path, state: file.state })
1225
+ }
1226
+ return root
1227
+ }
1228
+
1229
+ function Row({ node, depth, collapsed, onToggle, onOpen, path, rendered }) {
1230
+ if (rendered.count >= MAX_RENDER_ROWS) return null
1231
+ const rows = []
1232
+ const dirs = [...node.dirs.entries()].sort((a, b) => a[0].localeCompare(b[0]))
1233
+ for (const [name, child] of dirs) {
1234
+ const childPath = path === '' ? name : `${path}/${name}`
1235
+ const isCollapsed = collapsed.has(childPath)
1236
+ rows.push(h('div', { className: 'gg-tree-row gg-tree-dir', key: `d:${childPath}`, style: { paddingLeft: `${8 + depth * 14}px` } },
1237
+ h('button', {
1238
+ type: 'button',
1239
+ className: 'gg-tree-toggle',
1240
+ onClick: () => onToggle(childPath, isCollapsed),
1241
+ title: isCollapsed ? '展开' : '折叠',
1242
+ }, h('span', { className: cx('gg-tree-chevron', isCollapsed && 'gg-tree-chevron-closed') }, ICONS.chevron)),
1243
+ ICONS.folder,
1244
+ h('span', { className: 'gg-tree-name' }, name)))
1245
+ if (!isCollapsed) {
1246
+ rows.push(h(Row, { node: child, depth: depth + 1, collapsed, onToggle, onOpen, path: childPath, rendered, key: `r:${childPath}` }))
1247
+ }
1248
+ }
1249
+ for (const file of [...node.files].sort((a, b) => a.name.localeCompare(b.name))) {
1250
+ if (rendered.count >= MAX_RENDER_ROWS) break
1251
+ rendered.count++
1252
+ rows.push(h('div', {
1253
+ className: cx('gg-tree-row gg-tree-file',
1254
+ file.state === 'modified' && 'gg-tree-modified',
1255
+ file.state === 'untracked' && 'gg-tree-untracked'),
1256
+ key: `f:${file.path}`,
1257
+ style: { paddingLeft: `${8 + depth * 14 + 18}px` },
1258
+ title: file.path,
1259
+ onClick: () => onOpen(file),
1260
+ },
1261
+ ICONS.file,
1262
+ h('span', { className: 'gg-tree-name' }, file.name)))
1263
+ }
1264
+ return rows
1265
+ }
1266
+
1267
+ function Legend() {
1268
+ return h('div', { className: 'gg-files-legend' },
1269
+ h('span', { className: 'gg-legend-item' }, h('span', { className: 'gg-swatch gg-swatch-mod' }), t('files.legendModified')),
1270
+ h('span', { className: 'gg-legend-item' }, h('span', { className: 'gg-swatch gg-swatch-unt' }), t('files.legendUntracked')),
1271
+ h('span', { className: 'gg-legend-item' }, h('span', { className: 'gg-swatch gg-swatch-clean' }), t('files.legendClean')),
1272
+ )
1273
+ }
1274
+
1275
+ function FilesView() {
1276
+ const tree = useStore((s) => s.tree)
1277
+ const treeError = useStore((s) => s.treeError)
1278
+ const cwd = useStore((s) => s.cwd)
1279
+ const [filter, setFilter] = React.useState('')
1280
+ const [collapsed, setCollapsed] = React.useState(() => new Set())
1281
+
1282
+ React.useEffect(() => {
1283
+ registerTabLoader('files', async (w) => {
1284
+ const result = await getApi().tree(w)
1285
+ setState({ tree: result, treeError: null })
1286
+ })
1287
+ if (cwd !== null) {
1288
+ getApi().tree(cwd).then((r) => setState({ tree: r, treeError: null }))
1289
+ .catch((e) => setState({ tree: null, treeError: String(e?.message ?? e) }))
1290
+ }
1291
+ }, [cwd])
1292
+
1293
+ const toggle = (path, isCollapsed) => {
1294
+ const next = new Set(collapsed)
1295
+ if (isCollapsed) next.delete(path)
1296
+ else next.add(path)
1297
+ setCollapsed(next)
1298
+ }
1299
+
1300
+ const open = (file) => {
1301
+ const sel = {
1302
+ path: file.path,
1303
+ staged: false,
1304
+ untracked: file.state === 'untracked',
1305
+ cat: file.state === 'clean',
1306
+ }
1307
+ setState({ selected: sel, diff: { ...sel, loading: true, error: null, data: null }, tab: 'status' })
1308
+ refreshDiff()
1309
+ }
1310
+
1311
+ if (treeError) return h('div', { className: 'gg-empty gg-empty-err' }, treeError)
1312
+ if (tree === null) return h('div', { className: 'gg-empty' }, t('status.loading'))
1313
+ const files = tree.files ?? []
1314
+ if (files.length === 0) return h('div', { className: 'gg-empty' }, t('files.empty'))
1315
+
1316
+ const root = buildTree(files, filter)
1317
+ const rendered = { count: 0 }
1318
+ return h('div', { className: 'gg-files-view' },
1319
+ h('div', { className: 'gg-files-toolbar' },
1320
+ h('input', {
1321
+ className: 'gg-input',
1322
+ value: filter,
1323
+ placeholder: t('files.search'),
1324
+ onChange: (e) => setFilter(e.target.value),
1325
+ }),
1326
+ h(Legend, {})),
1327
+ h('div', { className: 'gg-files-tree' },
1328
+ h(Row, { node: root, depth: 0, collapsed, onToggle: toggle, onOpen: open, path: '', rendered })),
1329
+ tree.truncated && h('div', { className: 'gg-files-note' }, t('files.tooMany', { n: files.length })),
1330
+ )
1331
+ }
1332
+
1333
+ module.exports = { FilesView }
1334
+
1335
+ },
1336
+ "./v-footer.js": function (module, exports, require) {
1337
+ /**
1338
+ * Sidebar footer entry: a Git button with a changed-files badge.
1339
+ * Registered into `sidebar.footer.action` (list / root scope).
1340
+ */
1341
+ const { h, cx, ICONS, React } = require('./dom')
1342
+ const { useStore, setState } = require('./store')
1343
+ const { t } = require('./i18n')
1344
+ const { applySession } = require('./control')
1345
+
1346
+ function changedCount(status) {
1347
+ if (!status || !status.files) return 0
1348
+ return status.files.length
1349
+ }
1350
+
1351
+ function FooterButton(props) {
1352
+ const sessions = props.useSessions((s) => s)
1353
+ React.useEffect(() => {
1354
+ applySession(sessions)
1355
+ }, [sessions])
1356
+ const open = useStore((s) => s.open)
1357
+ const count = useStore((s) => changedCount(s.status))
1358
+ const statusError = useStore((s) => s.statusError)
1359
+ const check = useStore((s) => s.check)
1360
+ const wide = props.wide === true
1361
+
1362
+ const title = t('panel.title')
1363
+ const onClick = () => setState({ open: !open })
1364
+
1365
+ const badge = count > 0
1366
+ ? h('span', { className: 'gg-badge', title: `${count}` }, count > 99 ? '99+' : String(count))
1367
+ : null
1368
+ const dot = !badge && statusError && check?.repo === true
1369
+ ? h('span', { className: 'gg-badge gg-badge-err', title: statusError }, '!')
1370
+ : null
1371
+
1372
+ return h('button', {
1373
+ type: 'button',
1374
+ className: cx('gg-footer-btn', open && 'gg-active'),
1375
+ title,
1376
+ onClick,
1377
+ 'aria-label': title,
1378
+ },
1379
+ h('span', { className: 'gg-footer-icon' }, ICONS.git),
1380
+ wide && h('span', { className: 'gg-footer-label' }, 'Git'),
1381
+ (badge || dot) && h('span', { className: 'gg-footer-marks' }, badge, dot),
1382
+ )
1383
+ }
1384
+
1385
+ module.exports = { FooterButton }
1386
+
1387
+ },
1388
+ "./v-log.js": function (module, exports, require) {
1389
+ /**
1390
+ * Commit log view with per-commit revert action.
1391
+ */
1392
+ const { h, ICONS, React } = require('./dom')
1393
+ const { useStore, setState } = require('./store')
1394
+ const { t } = require('./i18n')
1395
+ const { run, confirmThen, getApi, registerTabLoader } = require('./control')
1396
+
1397
+ function timeAgo(at) {
1398
+ if (!at) return ''
1399
+ const delta = Date.now() / 1000 - at
1400
+ if (delta < 60) return `${Math.max(1, Math.round(delta))}s`
1401
+ if (delta < 3600) return `${Math.round(delta / 60)}m`
1402
+ if (delta < 86400) return `${Math.round(delta / 3600)}h`
1403
+ if (delta < 86400 * 30) return `${Math.round(delta / 86400)}d`
1404
+ return new Date(at * 1000).toLocaleDateString()
1405
+ }
1406
+
1407
+ function LogView() {
1408
+ const commits = useStore((s) => s.commits)
1409
+ const logError = useStore((s) => s.logError)
1410
+ const cwd = useStore((s) => s.cwd)
1411
+ const busy = useStore((s) => s.busy)
1412
+
1413
+ React.useEffect(() => {
1414
+ registerTabLoader('log', async (w) => {
1415
+ const result = await getApi().log(w, 100, null)
1416
+ setState({ commits: result.commits, logError: null })
1417
+ })
1418
+ if (cwd !== null) {
1419
+ getApi().log(cwd, 100, null).then((r) => setState({ commits: r.commits, logError: null }))
1420
+ .catch((e) => setState({ commits: [], logError: String(e?.message ?? e) }))
1421
+ }
1422
+ }, [cwd])
1423
+
1424
+ const revertCommit = (commit) => {
1425
+ confirmThen({
1426
+ body: t('confirm.revert', { commit: commit.hash.slice(0, 8) }),
1427
+ action: () => run(t('action.revert'), () => getApi().revert(cwd, commit.hash)),
1428
+ })
1429
+ }
1430
+
1431
+ if (logError) return h('div', { className: 'gg-empty gg-empty-err' }, logError)
1432
+ if (commits.length === 0) return h('div', { className: 'gg-empty' }, t('commit.noCommits'))
1433
+ return h('div', { className: 'gg-log' },
1434
+ commits.map((c) => h('div', { className: 'gg-log-row', key: c.hash },
1435
+ h('div', { className: 'gg-log-main' },
1436
+ h('span', { className: 'gg-log-subject' }, c.subject),
1437
+ h('span', { className: 'gg-log-meta' },
1438
+ c.refs.map((r) => h('span', { className: 'gg-chip', key: r }, r)),
1439
+ h('span', { className: 'gg-log-hash' }, c.hash.slice(0, 8)),
1440
+ h('span', {}, c.author),
1441
+ h('span', { className: 'gg-log-time' }, timeAgo(c.time)),
1442
+ )),
1443
+ h('button', {
1444
+ type: 'button',
1445
+ className: 'gg-icon-btn gg-row-btn',
1446
+ disabled: busy,
1447
+ title: t('action.revert'),
1448
+ onClick: () => revertCommit(c),
1449
+ }, ICONS.undo),
1450
+ )))
1451
+ }
1452
+
1453
+ module.exports = { LogView }
1454
+
1455
+ },
1456
+ "./v-panel.js": function (module, exports, require) {
1457
+ /**
1458
+ * The floating Git panel, registered into `shell.overlay` (list / root scope).
1459
+ */
1460
+ const { h, cx, ICONS, React } = require('./dom')
1461
+ const { useStore, setState, getState } = require('./store')
1462
+ const { t } = require('./i18n')
1463
+ const { applySession, refreshStatus, refreshRemotes, settleConfirm, run, getApi, refreshCheck } = require('./control')
1464
+ const { StatusView } = require('./v-status')
1465
+ const { FilesView } = require('./v-files')
1466
+ const { LogView } = require('./v-log')
1467
+ // ── Stage 2 功能(代码保留,暂不暴露给用户)────────────────────────────────────
1468
+ // 分支 / 储藏 / AI 修改 三部分已实现且有测试,但尚未达到发布标准,先对用户隐藏。
1469
+ // 启用方式:把对应条目加回下方 TABS(import 已保留,view 与 host 端点无需改动)。
1470
+ const { BranchView } = require('./v-branch') // 分支管理 (git/branches, git/switchBranch, git/merge, git/pull, git/push, git/fetch)
1471
+ const { StashView } = require('./v-stash') // 储藏 (git/stash)
1472
+ const { TimelineView } = require('./v-timeline') // AI 修改时间线 (git/activity)
1473
+
1474
+ // 当前发布(Stage 1)的标签页:
1475
+ const TABS = [
1476
+ ['status', 'tab.status'],
1477
+ ['files', 'tab.files'],
1478
+ ['log', 'tab.log'],
1479
+ // Stage 2 启用时取消注释:
1480
+ // ['branches', 'tab.branches'],
1481
+ // ['stash', 'tab.stash'],
1482
+ // ['timeline', 'tab.timeline'],
1483
+ ]
1484
+
1485
+ function ConfirmModal() {
1486
+ const confirm = useStore((s) => s.confirm)
1487
+ if (confirm === null) return null
1488
+ const danger = confirm.danger === true
1489
+ return h('div', { className: 'gg-modal-backdrop', onClick: () => settleConfirm(false) },
1490
+ h('div', {
1491
+ className: cx('gg-modal', danger && 'gg-modal-danger'),
1492
+ onClick: (e) => e.stopPropagation(),
1493
+ role: 'dialog',
1494
+ 'aria-modal': 'true',
1495
+ },
1496
+ h('div', { className: 'gg-modal-title' },
1497
+ h('span', { className: 'gg-modal-icon' }, ICONS.alert),
1498
+ t(danger ? 'confirm.dangerTitle' : 'confirm.title')),
1499
+ h('div', { className: 'gg-modal-body' }, confirm.body),
1500
+ h('div', { className: 'gg-modal-actions' },
1501
+ h('button', { type: 'button', className: 'gg-btn', onClick: () => settleConfirm(false) }, t('confirm.cancel')),
1502
+ h('button', {
1503
+ type: 'button',
1504
+ className: cx('gg-btn', danger ? 'gg-btn-danger' : 'gg-btn-primary'),
1505
+ autoFocus: true,
1506
+ onClick: () => settleConfirm(true),
1507
+ }, t('confirm.ok')),
1508
+ ),
1509
+ ),
1510
+ )
1511
+ }
1512
+
1513
+ function Toast() {
1514
+ const toast = useStore((s) => s.toast)
1515
+ React.useEffect(() => {
1516
+ if (toast === null) return undefined
1517
+ const id = setTimeout(() => {
1518
+ if (getState().toast === toast) setState({ toast: null })
1519
+ }, 6000)
1520
+ return () => clearTimeout(id)
1521
+ }, [toast])
1522
+ if (toast === null) return null
1523
+ return h('div', { className: cx('gg-toast', toast.kind === 'error' ? 'gg-toast-err' : 'gg-toast-warn') },
1524
+ toast.text)
1525
+ }
1526
+
1527
+ function OutputBox() {
1528
+ const output = useStore((s) => s.output)
1529
+ const [open, setOpen] = React.useState(false)
1530
+ if (output === null) return null
1531
+ return h('div', { className: 'gg-output' },
1532
+ h('button', { type: 'button', className: 'gg-output-head', onClick: () => setOpen(!open) },
1533
+ h('span', { className: 'gg-output-title' }, `${t('output.title')}: ${output.title}`),
1534
+ h('span', { className: 'gg-output-toggle' }, open ? ICONS.down : ICONS.up)),
1535
+ open && h('pre', { className: 'gg-output-body' }, output.text === '' ? t('output.empty') : output.text),
1536
+ )
1537
+ }
1538
+
1539
+ function RemoteModal() {
1540
+ const openModal = useStore((s) => s.remoteModal)
1541
+ const cwd = useStore((s) => s.cwd)
1542
+ const busy = useStore((s) => s.busy)
1543
+ const [name, setName] = React.useState('origin')
1544
+ const [url, setUrl] = React.useState('')
1545
+ if (!openModal) return null
1546
+ const doAdd = async () => {
1547
+ const ok = await run(t('remote.add'), () => getApi().remoteAdd(cwd, name.trim() || 'origin', url.trim()))
1548
+ if (ok) {
1549
+ setState({ remoteModal: false })
1550
+ refreshRemotes(cwd)
1551
+ }
1552
+ }
1553
+ return h('div', { className: 'gg-modal-backdrop', onClick: () => setState({ remoteModal: false }) },
1554
+ h('div', { className: 'gg-modal', onClick: (e) => e.stopPropagation(), role: 'dialog', 'aria-modal': 'true' },
1555
+ h('div', { className: 'gg-modal-title' }, t('remote.addTitle')),
1556
+ h('label', { className: 'gg-field' },
1557
+ h('span', { className: 'gg-field-label' }, t('remote.name')),
1558
+ h('input', {
1559
+ className: 'gg-input', value: name,
1560
+ onChange: (e) => setName(e.target.value),
1561
+ })),
1562
+ h('label', { className: 'gg-field' },
1563
+ h('span', { className: 'gg-field-label' }, 'URL'),
1564
+ h('input', {
1565
+ className: 'gg-input', value: url, autoFocus: true,
1566
+ placeholder: t('remote.url.placeholder'),
1567
+ onChange: (e) => setUrl(e.target.value),
1568
+ onKeyDown: (e) => { if (e.key === 'Enter') doAdd() },
1569
+ })),
1570
+ h('div', { className: 'gg-modal-body' }, t('remote.sshNote')),
1571
+ h('div', { className: 'gg-modal-actions' },
1572
+ h('button', { type: 'button', className: 'gg-btn', onClick: () => setState({ remoteModal: false }) }, t('confirm.cancel')),
1573
+ h('button', {
1574
+ type: 'button',
1575
+ className: 'gg-btn gg-btn-primary',
1576
+ disabled: busy || url.trim() === '',
1577
+ onClick: doAdd,
1578
+ }, t('remote.add'))),
1579
+ ),
1580
+ )
1581
+ }
1582
+
1583
+ function WorkspaceBar() {
1584
+ const check = useStore((s) => s.check)
1585
+ const status = useStore((s) => s.status)
1586
+ const busy = useStore((s) => s.busy)
1587
+ const busyLabel = useStore((s) => s.busyLabel)
1588
+ const cwd = useStore((s) => s.cwd)
1589
+ const remotes = useStore((s) => s.remotes)
1590
+ const branch = status?.branch
1591
+ const root = check?.root
1592
+ const name = typeof root === 'string' && root !== ''
1593
+ ? root.split(/[\\/]/).filter(Boolean).pop()
1594
+ : t('status.checking')
1595
+
1596
+ const requireRemote = (action) => {
1597
+ if ((remotes ?? []).length === 0) {
1598
+ setState({ remoteModal: true, toast: { kind: 'warn', text: t('remote.emptyHint') } })
1599
+ return
1600
+ }
1601
+ action()
1602
+ }
1603
+
1604
+ const doPull = () => requireRemote(() => run(t('action.pull'), () => getApi().pull(cwd, 'ff-only')))
1605
+ const doPush = () => requireRemote(() => run(t('action.push'), () => getApi().push(cwd)))
1606
+ const doFetch = () => requireRemote(() => run(t('action.fetch'), () => getApi().fetch(cwd)))
1607
+
1608
+ return h('div', { className: 'gg-wsbar' },
1609
+ h('span', { className: 'gg-ws-name', title: root ?? undefined },
1610
+ h('span', { className: 'gg-ws-icon' }, ICONS.folder),
1611
+ name),
1612
+ branch?.head
1613
+ ? h('span', { className: 'gg-ws-branch' }, ICONS.branch, branch.head)
1614
+ : branch?.detached
1615
+ ? h('span', { className: 'gg-ws-branch gg-ws-detached' }, t('commit.detached'))
1616
+ : null,
1617
+ (branch?.ahead || branch?.behind)
1618
+ ? h('span', { className: 'gg-ws-ab' }, t('commit.aheadBehind', { a: branch.ahead ?? 0, b: branch.behind ?? 0 }))
1619
+ : null,
1620
+ h('span', { className: 'gg-ws-spacer' }),
1621
+ busy && h('span', { className: 'gg-ws-busy' }, busyLabel || t('commit.busy')),
1622
+ h('button', {
1623
+ type: 'button', className: 'gg-mini-btn', disabled: busy, title: 'git pull --ff-only',
1624
+ onClick: doPull,
1625
+ }, ICONS.down, 'Pull'),
1626
+ h('button', {
1627
+ type: 'button', className: 'gg-mini-btn', disabled: busy, title: 'git push',
1628
+ onClick: doPush,
1629
+ }, ICONS.up, 'Push'),
1630
+ h('button', {
1631
+ type: 'button', className: 'gg-mini-btn', disabled: busy, title: 'git fetch --all --prune',
1632
+ onClick: doFetch,
1633
+ }, ICONS.refresh, 'Fetch'),
1634
+ h('button', {
1635
+ type: 'button', className: cx('gg-icon-btn', (remotes ?? []).length === 0 && 'gg-icon-warn'),
1636
+ title: (remotes ?? []).length === 0 ? t('remote.emptyHint') : t('misc.remote'),
1637
+ onClick: () => setState({ remoteModal: true }),
1638
+ }, ICONS.globe),
1639
+ h('button', { type: 'button', className: 'gg-icon-btn', title: t('action.refresh'), onClick: () => refreshStatus() }, ICONS.refresh),
1640
+ )
1641
+ }
1642
+
1643
+ function GitPanel(props) {
1644
+ const sessions = props.useSessions((s) => s)
1645
+ React.useEffect(() => {
1646
+ applySession(sessions)
1647
+ }, [sessions])
1648
+ const open = useStore((s) => s.open)
1649
+ const cwd = useStore((s) => s.cwd)
1650
+ const running = useStore((s) => s.sessionRunning)
1651
+ const check = useStore((s) => s.check)
1652
+ const busy = useStore((s) => s.busy)
1653
+ const [width, setWidth] = React.useState(460)
1654
+ const [tab, setTabLocal] = React.useState('status')
1655
+ const dragRef = React.useRef(null)
1656
+
1657
+ React.useEffect(() => {
1658
+ setState({ tab })
1659
+ }, [tab])
1660
+
1661
+ const onTab = (next) => {
1662
+ setTabLocal(next)
1663
+ setState({ tab: next })
1664
+ }
1665
+
1666
+ const onDragStart = (e) => {
1667
+ e.preventDefault()
1668
+ const startX = e.clientX
1669
+ const startW = width
1670
+ dragRef.current = { startX, startW }
1671
+ const move = (ev) => {
1672
+ if (!dragRef.current) return
1673
+ const dx = startX - ev.clientX
1674
+ setWidth(Math.max(340, Math.min(800, startW + dx)))
1675
+ }
1676
+ const up = () => {
1677
+ dragRef.current = null
1678
+ window.removeEventListener('pointermove', move)
1679
+ window.removeEventListener('pointerup', up)
1680
+ }
1681
+ window.addEventListener('pointermove', move)
1682
+ window.addEventListener('pointerup', up)
1683
+ }
1684
+
1685
+ if (!open) return null
1686
+
1687
+ let body = null
1688
+ if (cwd === null) {
1689
+ body = h('div', { className: 'gg-empty' }, t('status.noSession'))
1690
+ } else if (check === null) {
1691
+ body = h('div', { className: 'gg-empty' }, t('status.checking'))
1692
+ } else if (check.repo === false) {
1693
+ body = h('div', { className: 'gg-empty' },
1694
+ check.nested > 0
1695
+ ? h('div', { className: 'gg-empty-title' }, `检测到 ${check.nested} 个嵌套 Git 仓库(位于工作区子目录),暂不支持多仓库,请将会话工作区指向其中一个仓库目录`)
1696
+ : h('div', { className: 'gg-empty-title' }, check.gitVersion === '' ? t('status.noGit') : t('status.notRepo')),
1697
+ check.nested > 0 ? null : h('button', {
1698
+ type: 'button',
1699
+ className: 'gg-btn gg-btn-primary',
1700
+ disabled: busy,
1701
+ onClick: () => initRepo(cwd),
1702
+ }, t('action.init')))
1703
+ } else {
1704
+ body = h('div', { className: 'gg-tabs' },
1705
+ h('div', { className: 'gg-tabbar', role: 'tablist' },
1706
+ TABS.map(([key, label]) => h('button', {
1707
+ type: 'button',
1708
+ role: 'tab',
1709
+ key,
1710
+ className: cx('gg-tab', tab === key && 'gg-tab-active'),
1711
+ onClick: () => onTab(key),
1712
+ }, t(label)))),
1713
+ h('div', { className: 'gg-tabbody' },
1714
+ tab === 'status' && h(StatusView, {}),
1715
+ tab === 'files' && h(FilesView, {}),
1716
+ tab === 'log' && h(LogView, {}),
1717
+ tab === 'branches' && h(BranchView, {}),
1718
+ tab === 'stash' && h(StashView, {}),
1719
+ tab === 'timeline' && h(TimelineView, {}),
1720
+ ),
1721
+ )
1722
+ }
1723
+
1724
+ return h('div', { className: 'gg-panel', style: { width: `${width}px` } },
1725
+ h('div', { className: 'gg-resize', onPointerDown: onDragStart }),
1726
+ h('div', { className: 'gg-panel-head' },
1727
+ h('span', { className: 'gg-panel-title' }, t('panel.title')),
1728
+ h('button', { type: 'button', className: 'gg-icon-btn', title: t('panel.close'), onClick: () => setState({ open: false }) }, ICONS.close)),
1729
+ running && h('div', { className: 'gg-banner' }, ICONS.robot, t('timeline.running')),
1730
+ h(WorkspaceBar, {}),
1731
+ h(OutputBox, {}),
1732
+ h('div', { className: 'gg-panel-body' }, body),
1733
+ h(RemoteModal, {}),
1734
+ h(ConfirmModal, {}),
1735
+ h(Toast, {}),
1736
+ )
1737
+ }
1738
+
1739
+ async function initRepo(cwd) {
1740
+ const ok = await run(t('action.init'), async () => getApi().init(cwd))
1741
+ if (ok) await refreshCheck()
1742
+ }
1743
+
1744
+ module.exports = { GitPanel }
1745
+
1746
+ },
1747
+ "./v-stash.js": function (module, exports, require) {
1748
+ /**
1749
+ * Stash view: list, push, pop, apply, drop.
1750
+ */
1751
+ const { h, React } = require('./dom')
1752
+ const { useStore, setState } = require('./store')
1753
+ const { t } = require('./i18n')
1754
+ const { run, confirmThen, getApi, registerTabLoader } = require('./control')
1755
+
1756
+ function StashView() {
1757
+ const stashes = useStore((s) => s.stashes)
1758
+ const cwd = useStore((s) => s.cwd)
1759
+ const busy = useStore((s) => s.busy)
1760
+ const [msg, setMsg] = React.useState('')
1761
+
1762
+ React.useEffect(() => {
1763
+ registerTabLoader('stash', async (w) => {
1764
+ const result = await getApi().stash(w, 'list', null, null)
1765
+ setState({ stashes: result.stashes })
1766
+ })
1767
+ if (cwd !== null) {
1768
+ getApi().stash(cwd, 'list', null, null).then((r) => setState({ stashes: r.stashes })).catch(() => {})
1769
+ }
1770
+ }, [cwd])
1771
+
1772
+ const doPush = () => run(t('action.stashPush'), () => getApi().stash(cwd, 'push', msg.trim() || null, null))
1773
+ const doPop = (ref) => confirmThen({
1774
+ body: t('confirm.popStash', { ref }),
1775
+ action: () => run(t('action.pop'), () => getApi().stash(cwd, 'pop', null, ref)),
1776
+ })
1777
+ const doApply = (ref) => run(t('action.apply'), () => getApi().stash(cwd, 'apply', null, ref))
1778
+ const doDrop = (ref) => confirmThen({
1779
+ danger: true,
1780
+ body: t('confirm.dropStash', { ref }),
1781
+ action: () => run(t('action.drop'), () => getApi().stash(cwd, 'drop', null, ref)),
1782
+ })
1783
+
1784
+ return h('div', { className: 'gg-stash' },
1785
+ h('div', { className: 'gg-stash-new' },
1786
+ h('input', {
1787
+ className: 'gg-input',
1788
+ value: msg,
1789
+ placeholder: t('stash.placeholder'),
1790
+ onChange: (e) => setMsg(e.target.value),
1791
+ onKeyDown: (e) => { if (e.key === 'Enter') doPush() },
1792
+ }),
1793
+ h('button', { type: 'button', className: 'gg-btn gg-btn-primary', disabled: busy, onClick: doPush }, t('action.stashPush'))),
1794
+ stashes.length === 0
1795
+ ? h('div', { className: 'gg-empty' }, t('stash.empty'))
1796
+ : h('div', { className: 'gg-stash-list' },
1797
+ h('div', { className: 'gg-group-name' }, t('stash.list')),
1798
+ stashes.map((s) => h('div', { className: 'gg-stash-row', key: s.ref },
1799
+ h('span', { className: 'gg-stash-ref' }, s.ref),
1800
+ h('span', { className: 'gg-stash-subject' }, s.subject),
1801
+ h('span', { className: 'gg-spacer' }),
1802
+ h('button', { type: 'button', className: 'gg-mini-btn', disabled: busy, onClick: () => doApply(s.ref) }, t('action.apply')),
1803
+ h('button', { type: 'button', className: 'gg-mini-btn', disabled: busy, onClick: () => doPop(s.ref) }, t('action.pop')),
1804
+ h('button', { type: 'button', className: 'gg-mini-btn gg-mini-danger', disabled: busy, onClick: () => doDrop(s.ref) }, t('action.drop')),
1805
+ ))))
1806
+ }
1807
+
1808
+ module.exports = { StashView }
1809
+
1810
+ },
1811
+ "./v-status.js": function (module, exports, require) {
1812
+ /**
1813
+ * Status view: staged/unstaged/untracked/conflict groups, commit box, diff pane.
1814
+ */
1815
+ const { h, cx, ICONS, React } = require('./dom')
1816
+ const { useStore, setState, getState } = require('./store')
1817
+ const { t } = require('./i18n')
1818
+ const { run, confirmThen, getApi, refreshDiff } = require('./control')
1819
+
1820
+ const LETTER_CLASS = {
1821
+ M: 'gg-l-m', A: 'gg-l-a', D: 'gg-l-d', R: 'gg-l-r', C: 'gg-l-c', U: 'gg-l-u', '?': 'gg-l-q', T: 'gg-l-t',
1822
+ }
1823
+
1824
+ function groupFiles(status) {
1825
+ const staged = []
1826
+ const unstaged = []
1827
+ const untracked = []
1828
+ const conflicts = []
1829
+ for (const f of status?.files ?? []) {
1830
+ const conflict = f.x === 'U' || f.y === 'U'
1831
+ if (conflict) conflicts.push(f)
1832
+ if (f.x === '?') untracked.push(f)
1833
+ else {
1834
+ if (f.x !== ' ' && f.x !== '.' && !conflict) staged.push(f)
1835
+ if (f.y !== ' ' && f.y !== '.' && f.x !== '?' && !conflict) unstaged.push(f)
1836
+ }
1837
+ }
1838
+ return { staged, unstaged, untracked, conflicts }
1839
+ }
1840
+
1841
+ function FileRow({ file, kind, onOpen, onToggle, onDiscard }) {
1842
+ const display = kind === 'staged' ? file.x : kind === 'untracked' ? '?' : file.y
1843
+ const renamed = display === 'R' || file.origPath !== undefined
1844
+ const name = file.path.split('/').pop()
1845
+ const dir = file.path.includes('/') ? file.path.slice(0, file.path.length - name.length) : ''
1846
+ const sub = file.sub.startsWith('S') ? ' (submodule)' : ''
1847
+
1848
+ return h('div', {
1849
+ className: cx('gg-file', kind === 'conflict' && 'gg-file-conflict'),
1850
+ onClick: () => onOpen(),
1851
+ title: file.path + sub,
1852
+ },
1853
+ h('button', {
1854
+ type: 'button',
1855
+ className: cx('gg-letter', LETTER_CLASS[display]),
1856
+ title: t('action.stage'),
1857
+ onClick: (e) => { e.stopPropagation(); onToggle() },
1858
+ }, display),
1859
+ h('span', { className: 'gg-file-main' },
1860
+ h('span', { className: 'gg-file-dir' }, dir),
1861
+ h('span', { className: 'gg-file-name' }, name + sub),
1862
+ renamed && h('span', { className: 'gg-file-renamed' }, `← ${file.origPath}`)),
1863
+ onDiscard && h('button', {
1864
+ type: 'button',
1865
+ className: 'gg-icon-btn gg-row-btn',
1866
+ title: t('action.discard'),
1867
+ onClick: (e) => { e.stopPropagation(); onDiscard() },
1868
+ }, ICONS.undo),
1869
+ )
1870
+ }
1871
+
1872
+ function Group({ label, files, kind, onOpen, onToggle, onDiscard, extra }) {
1873
+ if (files.length === 0 && extra === undefined) return null
1874
+ return h('div', { className: 'gg-group' },
1875
+ h('div', { className: 'gg-group-head' },
1876
+ h('span', { className: 'gg-group-name' }, t(label)),
1877
+ h('span', { className: 'gg-group-count' }, files.length),
1878
+ extra),
1879
+ h('div', { className: 'gg-group-body' },
1880
+ files.map((f) => h(FileRow, {
1881
+ key: `${kind}:${f.path}`,
1882
+ file: f,
1883
+ kind,
1884
+ onOpen: () => onOpen(f, kind),
1885
+ onToggle: () => onToggle(f, kind),
1886
+ onDiscard: onDiscard ? () => onDiscard(f, kind) : undefined,
1887
+ }))))
1888
+ }
1889
+
1890
+ function CommitBox() {
1891
+ const msg = useStore((s) => s.commitMsg)
1892
+ const identity = useStore((s) => s.identity)
1893
+ const status = useStore((s) => s.status)
1894
+ const busy = useStore((s) => s.busy)
1895
+ const cwd = useStore((s) => s.cwd)
1896
+ const groups = groupFiles(status)
1897
+ const stagedCount = groups.staged.length
1898
+
1899
+ const [aiBusy, setAiBusy] = React.useState(false)
1900
+
1901
+ const doCommit = async () => {
1902
+ if (msg.trim() === '') return
1903
+ const ok = await run(t('action.commit'), () => getApi().commit(cwd, msg.trim()))
1904
+ if (ok) setState({ commitMsg: '' })
1905
+ }
1906
+ const onKeyDown = (e) => {
1907
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
1908
+ e.preventDefault()
1909
+ doCommit()
1910
+ }
1911
+ }
1912
+ const stageAll = async () => {
1913
+ const paths = [...groups.unstaged, ...groups.untracked].map((f) => f.path)
1914
+ if (paths.length === 0) return
1915
+ await run(t('action.stageAll'), () => getApi().stage(cwd, paths))
1916
+ }
1917
+
1918
+ const aiGenerate = async () => {
1919
+ if (busy || aiBusy) return
1920
+ setAiBusy(true)
1921
+ try {
1922
+ const result = await getApi().generateCommitMessage(cwd)
1923
+ if (result && result.ok && result.message) {
1924
+ setState({ commitMsg: result.message })
1925
+ }
1926
+ } catch (error) {
1927
+ setState({ toast: { kind: 'error', text: String(error?.message ?? error) } })
1928
+ } finally {
1929
+ setAiBusy(false)
1930
+ }
1931
+ }
1932
+
1933
+ return h('div', { className: 'gg-commit' },
1934
+ h('textarea', {
1935
+ className: 'gg-commit-input',
1936
+ value: msg,
1937
+ placeholder: t('commit.placeholder'),
1938
+ rows: 3,
1939
+ onChange: (e) => setState({ commitMsg: e.target.value }),
1940
+ onKeyDown,
1941
+ }),
1942
+ h('div', { className: 'gg-commit-foot' },
1943
+ identity
1944
+ ? h('span', { className: 'gg-identity', title: t('commit.identity') },
1945
+ `${t('commit.identity')} ${identity.name ?? '?'} <${identity.email ?? '?'}>`)
1946
+ : h('span', { className: 'gg-identity gg-identity-missing' }, t('commit.identityMissing')),
1947
+ h('span', { className: 'gg-spacer' }),
1948
+ h('button', {
1949
+ type: 'button',
1950
+ className: 'gg-mini-btn',
1951
+ disabled: busy || aiBusy || stagedCount === 0,
1952
+ title: t('action.aiCommitTooltip'),
1953
+ onClick: aiGenerate,
1954
+ }, aiBusy ? '…' : t('action.aiCommit')),
1955
+ h('button', { type: 'button', className: 'gg-btn', disabled: busy || aiBusy, onClick: stageAll }, t('action.stageAll')),
1956
+ h('button', {
1957
+ type: 'button',
1958
+ className: 'gg-btn gg-btn-primary',
1959
+ disabled: busy || stagedCount === 0 || msg.trim() === '',
1960
+ onClick: doCommit,
1961
+ title: stagedCount === 0 ? t('commit.hint') : undefined,
1962
+ }, `${t('action.commit')}${stagedCount > 0 ? ` (${stagedCount})` : ''}`),
1963
+ ),
1964
+ )
1965
+ }
1966
+
1967
+ function DiffPane() {
1968
+ const selected = useStore((s) => s.selected)
1969
+ const diff = useStore((s) => s.diff)
1970
+ if (selected === null) {
1971
+ return h('div', { className: 'gg-diff' }, h('div', { className: 'gg-empty' }, t('diff.empty')))
1972
+ }
1973
+ if (diff === null || diff.loading) {
1974
+ return h('div', { className: 'gg-diff' }, h('div', { className: 'gg-empty' }, t('status.loading')))
1975
+ }
1976
+ if (diff.error !== null) {
1977
+ return h('div', { className: 'gg-diff' }, h('div', { className: 'gg-empty gg-empty-err' }, diff.error))
1978
+ }
1979
+ // file-content mode (clean files opened from the tree view)
1980
+ if (selected.cat === true) {
1981
+ const data = diff.data ?? {}
1982
+ const lines = (data.content ?? '').split('\n')
1983
+ const hasTrailing = lines.length > 1 && lines[lines.length - 1] === ''
1984
+ if (hasTrailing) lines.pop()
1985
+ return h('div', { className: 'gg-diff' },
1986
+ h('div', { className: 'gg-diff-head' },
1987
+ h('span', { className: 'gg-diff-path' }, selected.path),
1988
+ h('span', { className: 'gg-chip' }, t('cat.title'))),
1989
+ h('div', { className: 'gg-diff-scroll' },
1990
+ data.binary
1991
+ ? h('div', { className: 'gg-empty' }, t('diff.binary'))
1992
+ : data.tooLarge
1993
+ ? h('div', { className: 'gg-empty' }, t('diff.tooLarge', { size: data.size }))
1994
+ : h('div', { className: 'gg-hunks' },
1995
+ lines.map((text, i) => h('div', { className: 'gg-line gg-line-ctx', key: i },
1996
+ h('span', { className: 'gg-line-no' }, ''),
1997
+ h('span', { className: 'gg-line-no' }, i + 1),
1998
+ h('span', { className: 'gg-line-mark' }, ' '),
1999
+ h('span', { className: 'gg-line-text' }, text || ' '))))),
2000
+ )
2001
+ }
2002
+ const file = diff.data?.diff?.files?.[0]
2003
+ if (!file) {
2004
+ return h('div', { className: 'gg-diff' }, h('div', { className: 'gg-empty' }, t('diff.empty')))
2005
+ }
2006
+ return h('div', { className: 'gg-diff' },
2007
+ h('div', { className: 'gg-diff-head' },
2008
+ h('span', { className: 'gg-diff-path' }, file.newPath),
2009
+ selected.staged && h('span', { className: 'gg-chip' }, t('group.staged')),
2010
+ selected.untracked && h('span', { className: 'gg-chip' }, t('diff.newFile')),
2011
+ (file.oldPath && file.oldPath !== file.newPath) && h('span', { className: 'gg-diff-rename' }, `${file.oldPath} → ${file.newPath}`),
2012
+ ),
2013
+ h('div', { className: 'gg-diff-scroll' },
2014
+ file.binary
2015
+ ? h('div', { className: 'gg-empty' }, t('diff.binary'))
2016
+ : file.tooLarge
2017
+ ? h('div', { className: 'gg-empty' }, t('diff.tooLarge', { size: file.size }))
2018
+ : file.hunks.length === 0
2019
+ ? h('div', { className: 'gg-empty' }, t('group.clean'))
2020
+ : h('div', { className: 'gg-hunks' },
2021
+ file.hunks.map((hunk, i) => h('div', { className: 'gg-hunk', key: i },
2022
+ h('div', { className: 'gg-hunk-head' }, `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`),
2023
+ hunk.lines.map((line, j) => h('div', {
2024
+ className: cx('gg-line', line.type === 'add' ? 'gg-line-add' : line.type === 'del' ? 'gg-line-del' : 'gg-line-ctx'),
2025
+ key: j,
2026
+ },
2027
+ h('span', { className: 'gg-line-no' }, line.oldLine ?? ''),
2028
+ h('span', { className: 'gg-line-no' }, line.newLine ?? ''),
2029
+ h('span', { className: 'gg-line-mark' }, line.type === 'add' ? '+' : line.type === 'del' ? '-' : ' '),
2030
+ h('span', { className: 'gg-line-text' }, line.text || ' '),
2031
+ line.newline === false && h('span', { className: 'gg-line-nonl' }, '⏎'),
2032
+ ))))),
2033
+ ),
2034
+ )
2035
+ }
2036
+
2037
+ function StatusView() {
2038
+ const status = useStore((s) => s.status)
2039
+ const statusError = useStore((s) => s.statusError)
2040
+ const cwd = useStore((s) => s.cwd)
2041
+ const busy = useStore((s) => s.busy)
2042
+ const groups = groupFiles(status)
2043
+
2044
+ React.useEffect(() => {
2045
+ // load identity once per workspace
2046
+ if (cwd !== null) {
2047
+ getApi().identity(cwd).then((r) => setState({ identity: r })).catch(() => {})
2048
+ }
2049
+ }, [cwd])
2050
+
2051
+ const openDiff = (file, kind) => {
2052
+ const sel = { path: file.path, staged: kind === 'staged', untracked: kind === 'untracked', cat: false }
2053
+ setState({ selected: sel, diff: { ...sel, loading: true, error: null, data: null } })
2054
+ refreshDiff()
2055
+ }
2056
+
2057
+ const toggle = (file, kind) => {
2058
+ const fn = kind === 'staged'
2059
+ ? () => getApi().unstage(cwd, [file.path])
2060
+ : () => getApi().stage(cwd, [file.path])
2061
+ run(kind === 'staged' ? t('action.unstage') : t('action.stage'), fn).then(() => {
2062
+ const sel = getState().selected
2063
+ if (sel && sel.path === file.path) refreshDiff()
2064
+ })
2065
+ }
2066
+
2067
+ const discard = (file, kind) => {
2068
+ confirmThen({
2069
+ danger: true,
2070
+ body: kind === 'untracked'
2071
+ ? t('confirm.cleanFile', { name: file.path })
2072
+ : t('confirm.discardFile', { name: file.path }),
2073
+ action: () => run(t('action.discard'), () => getApi().discard(cwd, [file.path], kind === 'untracked')).then(() => {
2074
+ const sel = getState().selected
2075
+ if (sel && sel.path === file.path) setState({ selected: null, diff: null })
2076
+ }),
2077
+ })
2078
+ }
2079
+
2080
+ const unstageAll = () => {
2081
+ const paths = groups.staged.map((f) => f.path)
2082
+ if (paths.length === 0) return
2083
+ run(t('action.unstageAll'), () => getApi().unstage(cwd, paths))
2084
+ }
2085
+
2086
+ const total = (status?.files ?? []).length
2087
+ return h('div', { className: 'gg-status' },
2088
+ h(CommitBox, {}),
2089
+ statusError
2090
+ ? h('div', { className: 'gg-empty gg-empty-err' }, `${t('status.error')}: ${statusError}`)
2091
+ : total === 0
2092
+ ? h('div', { className: 'gg-empty' }, t('group.clean'))
2093
+ : h('div', { className: 'gg-status-main' },
2094
+ h('div', { className: 'gg-files' },
2095
+ h(Group, {
2096
+ label: 'group.conflicts', files: groups.conflicts, kind: 'conflict',
2097
+ onOpen: openDiff, onToggle: (f) => { openDiff(f, 'conflict') },
2098
+ }),
2099
+ h(Group, {
2100
+ label: 'group.staged', files: groups.staged, kind: 'staged',
2101
+ onOpen: openDiff, onToggle: toggle,
2102
+ extra: groups.staged.length > 0 && h('button', { type: 'button', className: 'gg-mini-btn', disabled: busy, onClick: unstageAll }, t('action.unstageAll')),
2103
+ }),
2104
+ h(Group, {
2105
+ label: 'group.changes', files: groups.unstaged, kind: 'unstaged',
2106
+ onOpen: openDiff, onToggle: toggle, onDiscard: discard,
2107
+ }),
2108
+ h(Group, {
2109
+ label: 'group.untracked', files: groups.untracked, kind: 'untracked',
2110
+ onOpen: openDiff, onToggle: toggle, onDiscard: discard,
2111
+ })),
2112
+ h(DiffPane, {}),
2113
+ ),
2114
+ )
2115
+ }
2116
+
2117
+ module.exports = { StatusView }
2118
+
2119
+ },
2120
+ "./v-timeline.js": function (module, exports, require) {
2121
+ /**
2122
+ * AI edit timeline: mutations attributed to (session, turn, tool, path).
2123
+ * Clicking a row opens the current diff of that path.
2124
+ */
2125
+ const { h, React } = require('./dom')
2126
+ const { useStore, setState, getState } = require('./store')
2127
+ const { t } = require('./i18n')
2128
+ const { getApi, registerTabLoader, refreshDiff } = require('./control')
2129
+
2130
+ function TimelineView() {
2131
+ const activity = useStore((s) => s.activity)
2132
+ const cwd = useStore((s) => s.cwd)
2133
+
2134
+ React.useEffect(() => {
2135
+ registerTabLoader('timeline', async (w) => {
2136
+ const result = await getApi().activity(w, 100)
2137
+ setState({ activity: result.entries })
2138
+ })
2139
+ if (cwd !== null) {
2140
+ getApi().activity(cwd, 100).then((r) => setState({ activity: r.entries })).catch(() => {})
2141
+ }
2142
+ }, [cwd])
2143
+
2144
+ const openPath = (entry) => {
2145
+ // make the path relative to the effective repo root (nested repos are
2146
+ // supported: check.root may be deeper than the session cwd)
2147
+ let path = entry.path
2148
+ const root = getState().check?.root
2149
+ if (root && path.toLowerCase().startsWith(root.toLowerCase())) {
2150
+ path = path.slice(root.length).replace(/^[\\/]+/, '')
2151
+ }
2152
+ // untracked files have no worktree diff: open the untracked content preview
2153
+ const status = getState().status
2154
+ const statusFile = status?.files?.find((f) => f.path === path)
2155
+ const untracked = statusFile !== undefined && statusFile.x === '?'
2156
+ const sel = { path, staged: false, untracked, cat: false }
2157
+ setState({ selected: sel, diff: { ...sel, loading: true, error: null, data: null }, tab: 'status' })
2158
+ refreshDiff()
2159
+ }
2160
+
2161
+ if (activity.length === 0) {
2162
+ return h('div', { className: 'gg-empty' }, t('timeline.empty'))
2163
+ }
2164
+ return h('div', { className: 'gg-timeline' },
2165
+ h('div', { className: 'gg-group-name' }, t('timeline.title')),
2166
+ activity.map((e, i) => h('button', {
2167
+ type: 'button',
2168
+ className: 'gg-tl-row',
2169
+ key: i,
2170
+ onClick: () => openPath(e),
2171
+ title: e.path,
2172
+ },
2173
+ h('span', { className: 'gg-tl-tool' }, e.tool),
2174
+ h('span', { className: 'gg-tl-path' }, e.path),
2175
+ h('span', { className: 'gg-spacer' }),
2176
+ h('span', { className: 'gg-tl-meta' },
2177
+ t('timeline.turn', { turn: e.turn }),
2178
+ ' · ',
2179
+ e.sessionId.slice(0, 8),
2180
+ ' · ',
2181
+ new Date(e.at).toLocaleTimeString())),
2182
+ ))
2183
+ }
2184
+
2185
+ module.exports = { TimelineView }
2186
+
2187
+ },
2188
+ "./pkg-id.js": function (module, exports, require) {
2189
+ module.exports = "@dsh-xhl/dsh-git-gui"
2190
+
2191
+ }
2192
+ };
2193
+ var __cache = {};
2194
+ function __normalize(base, spec) {
2195
+ if (!spec.startsWith('.')) return null;
2196
+ var parts = base.split('/');
2197
+ parts.pop();
2198
+ for (var i = 0; i < spec.length; ) {
2199
+ if (spec.startsWith('./', i)) { i += 2; continue; }
2200
+ if (spec.startsWith('../', i)) { i += 3; parts.pop(); continue; }
2201
+ break;
2202
+ }
2203
+ var rest = spec.slice(i).split('/').filter(Boolean).join('/');
2204
+ var joined = parts.filter(function (p) { return p !== '' && p !== '.'; }).join('/');
2205
+ var id = './' + (joined === '' ? '' : joined + '/') + rest;
2206
+ if (!/.js$/.test(id)) id += '.js';
2207
+ return id;
2208
+ }
2209
+ function __load(id, base) {
2210
+ var resolved = __normalize(base, id);
2211
+ if (resolved === null) return require(id);
2212
+ if (Object.prototype.hasOwnProperty.call(__cache, resolved)) return __cache[resolved].exports;
2213
+ var mod = { exports: {} };
2214
+ __cache[resolved] = mod;
2215
+ var factory = __modules[resolved];
2216
+ if (factory === undefined) throw new Error("@dsh-xhl/dsh-git-gui" + ': unknown module ' + resolved);
2217
+ factory(mod, mod.exports, function (spec) { return __load(spec, resolved); });
2218
+ return mod.exports;
2219
+ }
2220
+ return __load('./index.js', './entry.js');
2221
+ }
2222
+ });