@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/LICENSE +21 -0
- package/README.md +110 -0
- package/cordis.patch.example.yml +6 -0
- package/cordis.patch.yml +10 -0
- package/lib/activity.js +140 -0
- package/lib/client.js +2222 -0
- package/lib/index.js +24 -0
- package/lib/parse.js +298 -0
- package/lib/runner.js +294 -0
- package/lib/service.js +932 -0
- package/package.json +68 -0
- package/scripts/build-client.mjs +110 -0
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
|
+
});
|