@goodandready/dsh-github-ops 0.1.1 → 0.2.0
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/CHANGELOG.md +47 -0
- package/README.md +95 -5
- package/README.ru.md +82 -5
- package/README.zh.md +64 -4
- package/lib/auth-device.js +150 -0
- package/lib/auth.js +17 -5
- package/lib/client.js +604 -16
- package/lib/github.js +105 -9
- package/lib/guidance.js +26 -0
- package/lib/index.js +181 -27
- package/lib/routes.js +222 -0
- package/lib/scm.js +115 -0
- package/lib/status.js +66 -0
- package/lib/tools/actions.js +1 -1
- package/lib/tools/insights.js +246 -0
- package/lib/tools/issues.js +2 -1
- package/lib/tools/panel-data.js +45 -0
- package/lib/tools/register-insights.js +126 -0
- package/lib/tools/register-write.js +245 -0
- package/lib/tools/releases.js +3 -1
- package/lib/tools/repos.js +1 -1
- package/lib/tools/write-files.js +207 -0
- package/media/visual-verification.png +0 -0
- package/package.json +2 -1
- package/lib/approval.js +0 -193
package/lib/client.js
CHANGED
|
@@ -16,6 +16,13 @@ window.__ModuleLoader__.load({
|
|
|
16
16
|
const React = require('react')
|
|
17
17
|
|
|
18
18
|
const NS = 'dsh-github-ops'
|
|
19
|
+
const STATUS_PATH = '/dsh-github-ops/status'
|
|
20
|
+
const SCM_PATH = '/dsh-github-ops/scm'
|
|
21
|
+
const PANEL_PATH = '/dsh-github-ops/panel'
|
|
22
|
+
|
|
23
|
+
// The repository a github.com link in the conversation pointed at, so the tab and the
|
|
24
|
+
// panel can follow the human instead of asking again.
|
|
25
|
+
const linkState = { repo: '', enabled: false }
|
|
19
26
|
const ROW_ID = 'dsh-github-ops'
|
|
20
27
|
const PKG = '@goodandready/dsh-github-ops'
|
|
21
28
|
const ROW_CONFIG_KEY = PKG + '#' + ROW_ID
|
|
@@ -35,8 +42,6 @@ window.__ModuleLoader__.load({
|
|
|
35
42
|
timeoutMs: 'Request timeout, ms',
|
|
36
43
|
maxRetries: 'Retries for a failed read',
|
|
37
44
|
maxRetriesHint: 'A read (GET/HEAD) is retried on a network failure, a timeout or a 5xx. Writes are never retried automatically.',
|
|
38
|
-
approvalMode: 'Write approval',
|
|
39
|
-
approvalModeHint: '"auto" lets agreed non-destructive writes through without a prompt; deleting a release, tag, secret, variable, ruleset, branch protection or cancelling a run always asks. "ask" asks for every write; "off" leaves approvals to the host.',
|
|
40
45
|
reviewRules: 'Review rule overrides (JSON)',
|
|
41
46
|
reviewRulesHint: 'Optional. sensitivePaths, sensitiveSeverity, attentionPaths, migrationPaths, testsRequired, sourcePatterns, testPatterns, largeDiffLines. Example: {"largeDiffLines": 400, "sensitivePaths": [".env", "private/**"]}',
|
|
42
47
|
timeoutMsHint: 'How long one GitHub request may take before it is aborted.',
|
|
@@ -49,6 +54,52 @@ window.__ModuleLoader__.load({
|
|
|
49
54
|
savedPartial: 'Saved, except: ',
|
|
50
55
|
noChanges: 'Nothing to change.',
|
|
51
56
|
invalidNumber: 'Enter a positive number.',
|
|
57
|
+
statusTitle: 'Access',
|
|
58
|
+
statusLoading: 'Checking access…',
|
|
59
|
+
statusNotConfigured: 'No access yet',
|
|
60
|
+
statusError: 'Access check failed',
|
|
61
|
+
statusSource: 'Source',
|
|
62
|
+
statusAccount: 'Account',
|
|
63
|
+
statusScopes: 'Scopes',
|
|
64
|
+
statusRate: 'Rate limit',
|
|
65
|
+
statusCache: 'Read cache',
|
|
66
|
+
statusRefresh: 'Check again',
|
|
67
|
+
scmTitle: 'Source control',
|
|
68
|
+
scmRefresh: 'Refresh',
|
|
69
|
+
scmLoading: 'Reading the working copy…',
|
|
70
|
+
scmNotRepo: 'This workspace is not a git repository, so there is nothing to show here.',
|
|
71
|
+
scmError: 'Could not read the working copy',
|
|
72
|
+
scmStaged: 'Staged',
|
|
73
|
+
scmChanges: 'Changes',
|
|
74
|
+
scmUntracked: 'Untracked',
|
|
75
|
+
scmConflicts: 'Conflicts',
|
|
76
|
+
scmNothing: 'Nothing to commit: the working copy is clean.',
|
|
77
|
+
scmDiffEmpty: 'No diff for this file (it may be binary or untracked).',
|
|
78
|
+
scmBranches: 'Branches',
|
|
79
|
+
scmTags: 'Tags',
|
|
80
|
+
scmStashes: 'Stashes',
|
|
81
|
+
scmInProgress: 'In progress',
|
|
82
|
+
scmNoUpstream: 'no upstream',
|
|
83
|
+
prBarBranch: 'to',
|
|
84
|
+
prBarCommits: 'commit(s) ahead',
|
|
85
|
+
panelTitle: 'GitHub',
|
|
86
|
+
panelRepo: 'Repository',
|
|
87
|
+
panelSearch: 'Search public repositories…',
|
|
88
|
+
panelChoose: 'Choose a repository to browse.',
|
|
89
|
+
panelBack: 'Up',
|
|
90
|
+
panelIssues: 'Issues',
|
|
91
|
+
panelPulls: 'Pull requests',
|
|
92
|
+
panelRuns: 'Actions',
|
|
93
|
+
panelInbox: 'Inbox',
|
|
94
|
+
panelOpen: 'Open in browser',
|
|
95
|
+
panelEmpty: 'Nothing here yet.',
|
|
96
|
+
panelLoading: 'Loading…',
|
|
97
|
+
panelNoAccess: 'No GitHub access yet: configure a credential, sign in, or use the gh CLI.',
|
|
98
|
+
prBarCreate: 'Create PR',
|
|
99
|
+
prBarReview: 'Review',
|
|
100
|
+
prBarMerge: 'Merge',
|
|
101
|
+
prBarCopied: 'Instruction copied: paste it into the composer and send.',
|
|
102
|
+
prBarCopyFailed: 'Could not copy; the instruction is in the title.',
|
|
52
103
|
}
|
|
53
104
|
|
|
54
105
|
const zh = {
|
|
@@ -66,8 +117,6 @@ window.__ModuleLoader__.load({
|
|
|
66
117
|
timeoutMs: '请求超时(毫秒)',
|
|
67
118
|
maxRetries: '读取失败重试次数',
|
|
68
119
|
maxRetriesHint: '读取(GET/HEAD)在网络故障、超时或 5xx 时重试;写入不会自动重试。',
|
|
69
|
-
approvalMode: '写入审批',
|
|
70
|
-
approvalModeHint: '"auto" 允许已同意的非破坏性写入免询问;删除 release、tag、secret、变量、ruleset、分支保护或取消运行仍会询问。"ask" 每次写入都询问;"off" 交由宿主决定。',
|
|
71
120
|
reviewRules: '评审规则覆盖(JSON)',
|
|
72
121
|
reviewRulesHint: '可选。sensitivePaths、sensitiveSeverity、attentionPaths、migrationPaths、testsRequired、sourcePatterns、testPatterns、largeDiffLines。',
|
|
73
122
|
timeoutMsHint: '单个 GitHub 请求在被中断前允许的时长。',
|
|
@@ -80,6 +129,52 @@ window.__ModuleLoader__.load({
|
|
|
80
129
|
savedPartial: '已保存,除了:',
|
|
81
130
|
noChanges: '没有需要修改的内容。',
|
|
82
131
|
invalidNumber: '请输入正数。',
|
|
132
|
+
statusTitle: '访问状态',
|
|
133
|
+
statusLoading: '正在检查访问…',
|
|
134
|
+
statusNotConfigured: '尚未配置访问',
|
|
135
|
+
statusError: '访问检查失败',
|
|
136
|
+
statusSource: '来源',
|
|
137
|
+
statusAccount: '账号',
|
|
138
|
+
statusScopes: '权限范围',
|
|
139
|
+
statusRate: '速率限制',
|
|
140
|
+
statusCache: '读取缓存',
|
|
141
|
+
statusRefresh: '重新检查',
|
|
142
|
+
scmTitle: '源代码管理',
|
|
143
|
+
scmRefresh: '刷新',
|
|
144
|
+
scmLoading: '正在读取工作副本…',
|
|
145
|
+
scmNotRepo: '当前工作区不是 git 仓库,这里没有可显示的内容。',
|
|
146
|
+
scmError: '无法读取工作副本',
|
|
147
|
+
scmStaged: '已暂存',
|
|
148
|
+
scmChanges: '更改',
|
|
149
|
+
scmUntracked: '未跟踪',
|
|
150
|
+
scmConflicts: '冲突',
|
|
151
|
+
scmNothing: '没有待提交内容:工作副本是干净的。',
|
|
152
|
+
scmDiffEmpty: '该文件没有差异(可能是二进制或未跟踪)。',
|
|
153
|
+
scmBranches: '分支',
|
|
154
|
+
scmTags: '标签',
|
|
155
|
+
scmStashes: '暂存',
|
|
156
|
+
scmInProgress: '进行中',
|
|
157
|
+
scmNoUpstream: '无上游',
|
|
158
|
+
prBarBranch: '→',
|
|
159
|
+
prBarCommits: '个提交领先',
|
|
160
|
+
panelTitle: 'GitHub',
|
|
161
|
+
panelRepo: '仓库',
|
|
162
|
+
panelSearch: '搜索公开仓库…',
|
|
163
|
+
panelChoose: '请选择要浏览的仓库。',
|
|
164
|
+
panelBack: '上一级',
|
|
165
|
+
panelIssues: 'Issue',
|
|
166
|
+
panelPulls: 'Pull request',
|
|
167
|
+
panelRuns: 'Actions',
|
|
168
|
+
panelInbox: '收件箱',
|
|
169
|
+
panelOpen: '在浏览器中打开',
|
|
170
|
+
panelEmpty: '这里还没有内容。',
|
|
171
|
+
panelLoading: '加载中…',
|
|
172
|
+
panelNoAccess: '尚未配置 GitHub 访问:请设置凭据、登录或使用 gh CLI。',
|
|
173
|
+
prBarCreate: '创建 PR',
|
|
174
|
+
prBarReview: '评审',
|
|
175
|
+
prBarMerge: '合并',
|
|
176
|
+
prBarCopied: '指令已复制:粘贴到输入框并发送。',
|
|
177
|
+
prBarCopyFailed: '复制失败;指令在提示中。',
|
|
83
178
|
}
|
|
84
179
|
|
|
85
180
|
let ChevronIcon = null
|
|
@@ -96,6 +191,348 @@ window.__ModuleLoader__.load({
|
|
|
96
191
|
React.createElement('path', { d: 'M3.5 5.25L7 8.75L10.5 5.25' }),
|
|
97
192
|
)
|
|
98
193
|
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Source Control tab: what the working copy looks like right now.
|
|
197
|
+
*
|
|
198
|
+
* Reads only. Data comes from the host route on the session workspace; nothing here writes
|
|
199
|
+
* to the repository, so a change always starts with an explicit human action.
|
|
200
|
+
*/
|
|
201
|
+
function SourceControlView(props) {
|
|
202
|
+
ensureCss()
|
|
203
|
+
const t = props.t || ((key) => key)
|
|
204
|
+
const [state, setState] = React.useState({ phase: 'loading' })
|
|
205
|
+
const [selected, setSelected] = React.useState(null)
|
|
206
|
+
const [diff, setDiff] = React.useState({ phase: 'idle', text: '' })
|
|
207
|
+
|
|
208
|
+
const load = React.useCallback(() => {
|
|
209
|
+
setState({ phase: 'loading' })
|
|
210
|
+
if (typeof fetch !== 'function') {
|
|
211
|
+
setState({ phase: 'error', message: 'fetch is unavailable in this client' })
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
fetch(SCM_PATH, { headers: { accept: 'application/json' } })
|
|
215
|
+
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
|
|
216
|
+
.then((payload) => setState({ phase: 'ready', payload }))
|
|
217
|
+
.catch((err) => setState({ phase: 'error', message: String((err && err.message) || err) }))
|
|
218
|
+
}, [])
|
|
219
|
+
|
|
220
|
+
React.useEffect(() => { load() }, [load])
|
|
221
|
+
|
|
222
|
+
const openDiff = (file, staged) => {
|
|
223
|
+
setSelected({ path: file.path, staged })
|
|
224
|
+
setDiff({ phase: 'loading', text: '' })
|
|
225
|
+
const query = `?file=${encodeURIComponent(file.path)}${staged ? '&staged=1' : ''}`
|
|
226
|
+
fetch(SCM_PATH + query, { headers: { accept: 'application/json' } })
|
|
227
|
+
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
|
|
228
|
+
.then((payload) => setDiff({ phase: 'ready', text: payload.diff || '' }))
|
|
229
|
+
.catch((err) => setDiff({ phase: 'error', text: String((err && err.message) || err) }))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const fileRow = (file, staged) => React.createElement('div', {
|
|
233
|
+
key: `${staged ? 's' : 'w'}-${file.path}`,
|
|
234
|
+
className: 'gho-scm-file',
|
|
235
|
+
role: 'button',
|
|
236
|
+
tabIndex: 0,
|
|
237
|
+
'aria-selected': selected && selected.path === file.path && selected.staged === staged,
|
|
238
|
+
onClick: () => openDiff(file, staged),
|
|
239
|
+
onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') openDiff(file, staged) },
|
|
240
|
+
},
|
|
241
|
+
React.createElement('span', { className: 'gho-scm-badge' }, file.status),
|
|
242
|
+
React.createElement('span', { className: 'gho-scm-path' }, file.renamedFrom ? `${file.renamedFrom} → ${file.path}` : file.path),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
const group = (title, files, staged) => (files && files.length
|
|
246
|
+
? React.createElement('div', { className: 'gho-scm-group', key: title },
|
|
247
|
+
React.createElement('div', { className: 'gho-scm-group-head' }, `${title} (${files.length})`),
|
|
248
|
+
files.map((file) => fileRow(file, staged)),
|
|
249
|
+
)
|
|
250
|
+
: null)
|
|
251
|
+
|
|
252
|
+
if (state.phase === 'loading') return React.createElement('div', { className: 'gho-scm' }, t('scmLoading'))
|
|
253
|
+
if (state.phase === 'error') {
|
|
254
|
+
return React.createElement('div', { className: 'gho-scm' },
|
|
255
|
+
React.createElement('div', { className: 'gho-scm-group-head' }, t('scmError')),
|
|
256
|
+
React.createElement('div', { className: 'gho-scm-meta' }, state.message),
|
|
257
|
+
React.createElement('button', { type: 'button', className: 'gho-link', onClick: load }, t('scmRefresh')),
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
const payload = state.payload || {}
|
|
261
|
+
if (!payload.isRepository) {
|
|
262
|
+
return React.createElement('div', { className: 'gho-scm' },
|
|
263
|
+
React.createElement('div', { className: 'gho-scm-group-head' }, t('scmNotRepo')),
|
|
264
|
+
payload.error ? React.createElement('div', { className: 'gho-scm-meta' }, payload.error) : null,
|
|
265
|
+
React.createElement('button', { type: 'button', className: 'gho-link', onClick: load }, t('scmRefresh')),
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
const files = payload.files || { staged: [], changes: [], untracked: [], unmerged: [] }
|
|
269
|
+
const clean = !files.total
|
|
270
|
+
|
|
271
|
+
return React.createElement('div', { className: 'gho-scm' },
|
|
272
|
+
React.createElement('div', { className: 'gho-scm-head' },
|
|
273
|
+
React.createElement('span', { className: 'gho-scm-repo' }, payload.cwd || ''),
|
|
274
|
+
React.createElement('span', { className: 'gho-scm-branch' }, payload.branch),
|
|
275
|
+
payload.upstream
|
|
276
|
+
? React.createElement('span', { className: 'gho-scm-counts' }, `↑${payload.ahead ?? 0} ↓${payload.behind ?? 0}`)
|
|
277
|
+
: React.createElement('span', { className: 'gho-scm-counts' }, t('scmNoUpstream')),
|
|
278
|
+
React.createElement('button', { type: 'button', className: 'gho-link', onClick: load }, t('scmRefresh')),
|
|
279
|
+
),
|
|
280
|
+
payload.mergeState && payload.mergeState.inProgress
|
|
281
|
+
? React.createElement('div', { className: 'gho-scm-meta' },
|
|
282
|
+
React.createElement('span', { className: 'gho-scm-chip' }, `${t('scmInProgress')}: ${payload.mergeState.kind}`))
|
|
283
|
+
: null,
|
|
284
|
+
clean ? React.createElement('div', { className: 'gho-scm-meta' }, t('scmNothing')) : null,
|
|
285
|
+
group(t('scmConflicts'), files.unmerged, false),
|
|
286
|
+
group(t('scmStaged'), files.staged, true),
|
|
287
|
+
group(t('scmChanges'), files.changes, false),
|
|
288
|
+
group(t('scmUntracked'), files.untracked, false),
|
|
289
|
+
selected
|
|
290
|
+
? React.createElement('pre', { className: 'gho-scm-diff' },
|
|
291
|
+
diff.phase === 'loading' ? t('scmLoading') : (diff.text || t('scmDiffEmpty')))
|
|
292
|
+
: null,
|
|
293
|
+
React.createElement('div', { className: 'gho-scm-meta' },
|
|
294
|
+
React.createElement('span', null, `${t('scmBranches')}: ${(payload.branches || []).map((b) => (b.current ? `*${b.name}` : b.name)).join(', ') || '—'}`),
|
|
295
|
+
React.createElement('span', null, `${t('scmTags')}: ${(payload.tags || []).join(', ') || '—'}`),
|
|
296
|
+
React.createElement('span', null, `${t('scmStashes')}: ${(payload.stashes || []).length}`),
|
|
297
|
+
),
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The pull request bar above the composer.
|
|
304
|
+
*
|
|
305
|
+
* It appears when the branch has commits the upstream does not, and it never writes: the
|
|
306
|
+
* buttons put an instruction on the clipboard for the human to send, so the call still goes
|
|
307
|
+
* through the normal approval contour.
|
|
308
|
+
*/
|
|
309
|
+
function PullRequestBar(props) {
|
|
310
|
+
ensureCss()
|
|
311
|
+
const t = props.t || ((key) => key)
|
|
312
|
+
const [state, setState] = React.useState({ phase: 'idle' })
|
|
313
|
+
const [note, setNote] = React.useState('')
|
|
314
|
+
|
|
315
|
+
const load = React.useCallback(() => {
|
|
316
|
+
if (typeof fetch !== 'function') return
|
|
317
|
+
fetch(SCM_PATH, { headers: { accept: 'application/json' } })
|
|
318
|
+
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
|
|
319
|
+
.then((payload) => setState({ phase: 'ready', payload }))
|
|
320
|
+
.catch(() => setState({ phase: 'idle' }))
|
|
321
|
+
}, [])
|
|
322
|
+
|
|
323
|
+
React.useEffect(() => { load() }, [load])
|
|
324
|
+
|
|
325
|
+
const payload = state.payload
|
|
326
|
+
const ahead = payload && payload.isRepository && payload.upstream ? (payload.ahead || 0) : 0
|
|
327
|
+
if (!payload || !payload.isRepository || ahead <= 0) return null
|
|
328
|
+
|
|
329
|
+
const instruction = (kind) => {
|
|
330
|
+
const repo = payload.cwd ? ` (workspace ${payload.cwd})` : ''
|
|
331
|
+
if (kind === 'create') return `Create a GitHub pull request from ${payload.branch}${payload.upstream ? ` into ${payload.upstream.replace(/^[^/]+\//, '')}` : ''}: call pr_create with head=${payload.branch}${repo}.`
|
|
332
|
+
if (kind === 'review') return `Review the open pull request of ${payload.branch}: call gh_review and then ci_run, and summarise the findings.`
|
|
333
|
+
return `Merge the pull request of ${payload.branch} after the review: call pr_merge.`
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const copy = (kind) => {
|
|
337
|
+
const text = instruction(kind)
|
|
338
|
+
try {
|
|
339
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
|
|
340
|
+
navigator.clipboard.writeText(text)
|
|
341
|
+
setNote(t('prBarCopied'))
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
throw new Error('clipboard unavailable')
|
|
345
|
+
} catch {
|
|
346
|
+
setNote(`${t('prBarCopyFailed')} ${text}`)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const base = payload.upstream ? payload.upstream.replace(/^[^/]+\//, '') : ''
|
|
351
|
+
return React.createElement('div', { className: 'gho-prbar' },
|
|
352
|
+
React.createElement('span', { className: 'gho-prbar-branch' }, `${payload.branch} ${base ? `→ ${base}` : ''}`.trim()),
|
|
353
|
+
React.createElement('span', null, `↑${ahead} ${t('prBarCommits')}`),
|
|
354
|
+
note ? React.createElement('span', { className: 'gho-prbar-note' }, note) : null,
|
|
355
|
+
React.createElement('span', { className: 'gho-prbar-actions' },
|
|
356
|
+
React.createElement('button', { type: 'button', className: 'gho-prbar-btn', onClick: () => copy('create') }, t('prBarCreate')),
|
|
357
|
+
React.createElement('button', { type: 'button', className: 'gho-prbar-btn', onClick: () => copy('review') }, t('prBarReview')),
|
|
358
|
+
React.createElement('button', { type: 'button', className: 'gho-prbar-btn', onClick: () => copy('merge') }, t('prBarMerge')),
|
|
359
|
+
),
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
/** Remember which repository a github.com link pointed at, when the user opted in. */
|
|
365
|
+
function installLinkWatcher() {
|
|
366
|
+
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return
|
|
367
|
+
document.addEventListener('click', (event) => {
|
|
368
|
+
if (!linkState.enabled) return
|
|
369
|
+
const anchor = event.target && event.target.closest ? event.target.closest('a[href]') : null
|
|
370
|
+
if (!anchor) return
|
|
371
|
+
const match = String(anchor.getAttribute('href') || '').match(/^https?:\/\/github\.com\/([^/]+)\/([^/?#]+)/)
|
|
372
|
+
if (!match) return
|
|
373
|
+
// the browser still opens the link: this only records where the human is looking
|
|
374
|
+
linkState.repo = `${match[1]}/${match[2].replace(/\.git$/, '')}`
|
|
375
|
+
}, true)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The GitHub panel: browse a repository, its issues, pull requests, runs and the attention
|
|
381
|
+
* inbox from the sidebar.
|
|
382
|
+
*
|
|
383
|
+
* Reads only, and through the host route: the panel never holds a token and never writes.
|
|
384
|
+
*/
|
|
385
|
+
function GitHubPanel(props) {
|
|
386
|
+
ensureCss()
|
|
387
|
+
const t = props.t || ((key) => key)
|
|
388
|
+
const [repo, setRepo] = React.useState('')
|
|
389
|
+
const [repos, setRepos] = React.useState([])
|
|
390
|
+
const [search, setSearch] = React.useState('')
|
|
391
|
+
const [tab, setTab] = React.useState('tree')
|
|
392
|
+
const [tree, setTree] = React.useState({ phase: 'idle', path: '', entries: [] })
|
|
393
|
+
const [file, setFile] = React.useState(null)
|
|
394
|
+
const [items, setItems] = React.useState({ phase: 'idle', list: [] })
|
|
395
|
+
const [error, setError] = React.useState('')
|
|
396
|
+
const [loading, setLoading] = React.useState(false)
|
|
397
|
+
|
|
398
|
+
const call = React.useCallback((params) => {
|
|
399
|
+
if (typeof fetch !== 'function') return Promise.reject(new Error('fetch is unavailable in this client'))
|
|
400
|
+
const query = new URLSearchParams(params).toString()
|
|
401
|
+
return fetch(`${PANEL_PATH}?${query}`, { headers: { accept: 'application/json' } })
|
|
402
|
+
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
|
|
403
|
+
}, [])
|
|
404
|
+
|
|
405
|
+
const loadRepos = React.useCallback((q) => {
|
|
406
|
+
setLoading(true)
|
|
407
|
+
setError('')
|
|
408
|
+
call(q ? { what: 'repos', q } : { what: 'repos' })
|
|
409
|
+
.then((payload) => {
|
|
410
|
+
if (payload.configured === false) { setError(payload.guidance || t('panelNoAccess')); setRepos([]); return }
|
|
411
|
+
setRepos(payload.repos || [])
|
|
412
|
+
})
|
|
413
|
+
.catch((err) => setError(String((err && err.message) || err)))
|
|
414
|
+
.finally(() => setLoading(false))
|
|
415
|
+
}, [call, t])
|
|
416
|
+
|
|
417
|
+
React.useEffect(() => { loadRepos('') }, [loadRepos])
|
|
418
|
+
|
|
419
|
+
const loadTree = React.useCallback((path) => {
|
|
420
|
+
if (!repo) return
|
|
421
|
+
setFile(null)
|
|
422
|
+
setTree({ phase: 'loading', path, entries: [] })
|
|
423
|
+
call({ what: 'tree', repo, path })
|
|
424
|
+
.then((payload) => setTree({ phase: 'ready', path: payload.path || path, entries: payload.entries || [], truncated: payload.truncated }))
|
|
425
|
+
.catch((err) => { setTree({ phase: 'error', path, entries: [] }); setError(String((err && err.message) || err)) })
|
|
426
|
+
}, [call, repo])
|
|
427
|
+
|
|
428
|
+
const loadList = React.useCallback((which) => {
|
|
429
|
+
if (!repo) return
|
|
430
|
+
setItems({ phase: 'loading', list: [] })
|
|
431
|
+
call({ what: which, repo })
|
|
432
|
+
.then((payload) => setItems({ phase: 'ready', list: payload.items || [] }))
|
|
433
|
+
.catch((err) => { setItems({ phase: 'error', list: [] }); setError(String((err && err.message) || err)) })
|
|
434
|
+
}, [call, repo])
|
|
435
|
+
|
|
436
|
+
const loadInbox = React.useCallback(() => {
|
|
437
|
+
setItems({ phase: 'loading', list: [] })
|
|
438
|
+
call({ what: 'inbox' })
|
|
439
|
+
.then((payload) => setItems({ phase: 'ready', list: payload.notifications || [] }))
|
|
440
|
+
.catch((err) => { setItems({ phase: 'error', list: [] }); setError(String((err && err.message) || err)) })
|
|
441
|
+
}, [call])
|
|
442
|
+
|
|
443
|
+
React.useEffect(() => {
|
|
444
|
+
if (!repo) return
|
|
445
|
+
if (tab === 'tree') loadTree('')
|
|
446
|
+
else if (tab === 'inbox') loadInbox()
|
|
447
|
+
else loadList(tab)
|
|
448
|
+
}, [repo, tab, loadTree, loadList, loadInbox])
|
|
449
|
+
|
|
450
|
+
const openFile = (entry) => {
|
|
451
|
+
if (entry.type === 'tree') { loadTree(entry.path); return }
|
|
452
|
+
call({ what: 'blob', repo, path: entry.path })
|
|
453
|
+
.then((payload) => setFile(payload))
|
|
454
|
+
.catch((err) => setError(String((err && err.message) || err)))
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const tabButton = (id, label) => React.createElement('button', {
|
|
458
|
+
key: id, type: 'button', className: 'gho-panel-tab', 'aria-pressed': tab === id, onClick: () => setTab(id),
|
|
459
|
+
}, label)
|
|
460
|
+
|
|
461
|
+
const header = React.createElement('div', { className: 'gho-panel-row' },
|
|
462
|
+
React.createElement('span', { className: 'gho-panel-muted' }, t('panelRepo')),
|
|
463
|
+
React.createElement('span', null, repo || '—'),
|
|
464
|
+
repo ? React.createElement('a', { className: 'gho-link', href: `https://github.com/${repo}`, target: '_blank', rel: 'noreferrer' }, t('panelOpen')) : null,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
const switcher = React.createElement('div', { className: 'gho-panel' },
|
|
468
|
+
React.createElement('div', { className: 'gho-panel-row' },
|
|
469
|
+
React.createElement('input', {
|
|
470
|
+
className: 'gho-panel-input',
|
|
471
|
+
placeholder: t('panelSearch'),
|
|
472
|
+
value: search,
|
|
473
|
+
onChange: (event) => setSearch(event.target.value),
|
|
474
|
+
onKeyDown: (event) => { if (event.key === 'Enter') loadRepos(search) },
|
|
475
|
+
}),
|
|
476
|
+
),
|
|
477
|
+
repos.length === 0
|
|
478
|
+
? React.createElement('div', { className: 'gho-panel-muted' }, loading ? t('panelLoading') : t('panelChoose'))
|
|
479
|
+
: repos.slice(0, 30).map((entry) => React.createElement('div', {
|
|
480
|
+
key: entry.fullName, className: 'gho-panel-item', role: 'button', tabIndex: 0,
|
|
481
|
+
onClick: () => setRepo(entry.fullName),
|
|
482
|
+
onKeyDown: (event) => { if (event.key === 'Enter') setRepo(entry.fullName) },
|
|
483
|
+
},
|
|
484
|
+
React.createElement('span', { className: 'gho-panel-file' }, entry.fullName),
|
|
485
|
+
React.createElement('span', { className: 'gho-panel-muted' }, entry.private ? 'private' : 'public'),
|
|
486
|
+
entry.description ? React.createElement('span', { className: 'gho-panel-muted' }, entry.description.slice(0, 40)) : null,
|
|
487
|
+
)),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
if (!repo) return React.createElement('div', null, header, switcher)
|
|
491
|
+
|
|
492
|
+
const list = (() => {
|
|
493
|
+
if (items.phase === 'loading') return React.createElement('div', { className: 'gho-panel-muted' }, t('panelLoading'))
|
|
494
|
+
if (!items.list.length) return React.createElement('div', { className: 'gho-panel-muted' }, t('panelEmpty'))
|
|
495
|
+
return items.list.slice(0, 40).map((item, index) => React.createElement('div', { className: 'gho-panel-item', key: `${item.number || item.id || index}` },
|
|
496
|
+
React.createElement('span', { className: 'gho-panel-file' }, item.number ? `#${item.number}` : (item.name || '')),
|
|
497
|
+
React.createElement('span', null, item.title || item.displayTitle || item.reason || item.name || ''),
|
|
498
|
+
item.url || item.html_url ? React.createElement('a', { className: 'gho-link', href: item.url || item.html_url, target: '_blank', rel: 'noreferrer' }, '↗') : null,
|
|
499
|
+
))
|
|
500
|
+
})()
|
|
501
|
+
|
|
502
|
+
const treeView = React.createElement('div', null,
|
|
503
|
+
tree.path ? React.createElement('button', { type: 'button', className: 'gho-link', onClick: () => loadTree(tree.path.split('/').slice(0, -1).join('/')) }, t('panelBack')) : null,
|
|
504
|
+
tree.phase === 'loading' ? React.createElement('div', { className: 'gho-panel-muted' }, t('panelLoading')) : null,
|
|
505
|
+
tree.phase === 'ready' && tree.entries.length === 0 ? React.createElement('div', { className: 'gho-panel-muted' }, t('panelEmpty')) : null,
|
|
506
|
+
(tree.entries || []).map((entry) => React.createElement('div', {
|
|
507
|
+
key: entry.path, className: 'gho-panel-item', role: 'button', tabIndex: 0,
|
|
508
|
+
onClick: () => openFile(entry),
|
|
509
|
+
onKeyDown: (event) => { if (event.key === 'Enter') openFile(entry) },
|
|
510
|
+
},
|
|
511
|
+
React.createElement('span', { className: 'gho-panel-file' }, entry.type === 'tree' ? '▸' : '·'),
|
|
512
|
+
React.createElement('span', { className: 'gho-panel-file' }, entry.name),
|
|
513
|
+
entry.type === 'blob' && entry.size !== undefined ? React.createElement('span', { className: 'gho-panel-muted' }, `${entry.size} B`) : null,
|
|
514
|
+
)),
|
|
515
|
+
file ? React.createElement('pre', { className: 'gho-panel-pre' }, file.truncated ? `${file.text}\n…` : (file.text || t('panelEmpty'))) : null,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
return React.createElement('div', { className: 'gho-panel' },
|
|
519
|
+
header,
|
|
520
|
+
React.createElement('div', { className: 'gho-panel-tabs' },
|
|
521
|
+
tabButton('tree', 'Code'),
|
|
522
|
+
tabButton('issues', t('panelIssues')),
|
|
523
|
+
tabButton('pulls', t('panelPulls')),
|
|
524
|
+
tabButton('runs', t('panelRuns')),
|
|
525
|
+
tabButton('inbox', t('panelInbox')),
|
|
526
|
+
React.createElement('button', { type: 'button', className: 'gho-panel-tab', onClick: () => { setRepo(''); setFile(null) } }, t('panelRepo')),
|
|
527
|
+
),
|
|
528
|
+
error ? React.createElement('div', { className: 'gho-panel-muted' }, error) : null,
|
|
529
|
+
tab === 'tree' ? treeView : list,
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// This file is the whole client half on purpose: DSH loads it as a single bundle through
|
|
534
|
+
// window.__ModuleLoader__, so splitting the card, the tab, the panel and the pull request
|
|
535
|
+
// bar into several files would need a build step the plugin deliberately does not have.
|
|
99
536
|
let cssInstalled = false
|
|
100
537
|
function ensureCss() {
|
|
101
538
|
if (cssInstalled || typeof document === 'undefined') return
|
|
@@ -112,6 +549,43 @@ window.__ModuleLoader__.load({
|
|
|
112
549
|
'.gho-field{display:flex;flex-direction:column;gap:6px;padding:12px 0}',
|
|
113
550
|
'.gho-label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
114
551
|
'.gho-hint{font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
|
552
|
+
'.gho-status{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 12px;margin:12px 0 0;display:flex;flex-direction:column;gap:6px}',
|
|
553
|
+
'.gho-status-row{display:flex;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
|
554
|
+
'.gho-status-key{min-width:88px;color:var(--dsw-alias-label-tertiary)}',
|
|
555
|
+
'.gho-status-value{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}',
|
|
556
|
+
'.gho-status-head{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
557
|
+
'.gho-status-bad{color:var(--dsw-alias-label-error,#c0392b)}',
|
|
558
|
+
'.gho-link{appearance:none;border:0;background:0 0;font:inherit;font-size:12px;color:var(--dsw-alias-label-secondary);text-decoration:underline;cursor:pointer;padding:0;margin-left:auto}',
|
|
559
|
+
'.gho-scm{display:flex;flex-direction:column;gap:12px;padding:4px 2px;font-size:13px;color:var(--dsw-alias-label-primary)}',
|
|
560
|
+
'.gho-scm-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}',
|
|
561
|
+
'.gho-scm-repo{font-weight:600}',
|
|
562
|
+
'.gho-scm-branch{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-secondary)}',
|
|
563
|
+
'.gho-scm-counts{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}',
|
|
564
|
+
'.gho-scm-group{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;overflow:hidden}',
|
|
565
|
+
'.gho-scm-group-head{display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-3);font-size:12px;font-weight:600}',
|
|
566
|
+
'.gho-scm-file{display:flex;align-items:center;gap:8px;padding:6px 12px;cursor:pointer;border-top:1px solid var(--dsw-alias-border-l2)}',
|
|
567
|
+
'.gho-scm-file[aria-selected="true"]{background:var(--dsw-alias-bg-layer-3)}',
|
|
568
|
+
'.gho-scm-badge{min-width:18px;text-align:center;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-secondary)}',
|
|
569
|
+
'.gho-scm-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
570
|
+
'.gho-scm-diff{margin:0;padding:10px 12px;max-height:420px;overflow:auto;background:var(--dsw-alias-bg-layer-3);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.5;white-space:pre}',
|
|
571
|
+
'.gho-scm-meta{display:flex;gap:12px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
|
572
|
+
'.gho-scm-chip{border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:2px 8px}',
|
|
573
|
+
'.gho-prbar{display:flex;align-items:center;gap:10px;padding:6px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;font-size:12px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3)}',
|
|
574
|
+
'.gho-prbar-branch{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-primary)}',
|
|
575
|
+
'.gho-prbar-actions{display:flex;gap:6px;margin-left:auto}',
|
|
576
|
+
'.gho-prbar-btn{appearance:none;font:inherit;font-size:12px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:0 0;color:var(--dsw-alias-label-primary);border-radius:8px;padding:3px 8px}',
|
|
577
|
+
'.gho-prbar-note{color:var(--dsw-alias-label-tertiary)}',
|
|
578
|
+
'.gho-panel{display:flex;flex-direction:column;gap:10px;font-size:12px;color:var(--dsw-alias-label-primary);padding:4px 2px}',
|
|
579
|
+
'.gho-panel-row{display:flex;align-items:center;gap:6px}',
|
|
580
|
+
'.gho-panel-input{flex:1;height:28px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);padding:0 8px;font:inherit}',
|
|
581
|
+
'.gho-panel-tabs{display:flex;gap:4px;flex-wrap:wrap}',
|
|
582
|
+
'.gho-panel-tab{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:0 0;color:var(--dsw-alias-label-secondary);border-radius:999px;padding:2px 10px}',
|
|
583
|
+
'.gho-panel-tab[aria-pressed="true"]{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}',
|
|
584
|
+
'.gho-panel-item{display:flex;gap:8px;align-items:baseline;padding:5px 2px;border-top:1px solid var(--dsw-alias-border-l2);cursor:pointer}',
|
|
585
|
+
'.gho-panel-item:hover{background:var(--dsw-alias-bg-layer-3)}',
|
|
586
|
+
'.gho-panel-muted{color:var(--dsw-alias-label-tertiary)}',
|
|
587
|
+
'.gho-panel-file{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}',
|
|
588
|
+
'.gho-panel-pre{margin:0;max-height:340px;overflow:auto;background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:8px;white-space:pre;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}',
|
|
115
589
|
'.gho-input-area{height:auto;min-height:64px;padding:8px 12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;resize:vertical}',
|
|
116
590
|
'.gho-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px}',
|
|
117
591
|
'.gho-foot{border-top:1px solid var(--dsw-alias-border-l2);display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px}',
|
|
@@ -144,6 +618,26 @@ window.__ModuleLoader__.load({
|
|
|
144
618
|
const [draft, setDraft] = React.useState(null)
|
|
145
619
|
const [saving, setSaving] = React.useState(false)
|
|
146
620
|
const [message, setMessage] = React.useState('')
|
|
621
|
+
const [accessStatus, setAccessStatus] = React.useState(null)
|
|
622
|
+
const [statusError, setStatusError] = React.useState('')
|
|
623
|
+
|
|
624
|
+
const loadStatus = React.useCallback(() => {
|
|
625
|
+
setAccessStatus('loading')
|
|
626
|
+
setStatusError('')
|
|
627
|
+
if (typeof fetch !== 'function') {
|
|
628
|
+
setAccessStatus(null)
|
|
629
|
+
setStatusError('fetch is unavailable in this client')
|
|
630
|
+
return
|
|
631
|
+
}
|
|
632
|
+
fetch(STATUS_PATH, { headers: { accept: 'application/json' } })
|
|
633
|
+
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
|
|
634
|
+
.then((payload) => setAccessStatus(payload))
|
|
635
|
+
.catch((err) => { setAccessStatus(null); setStatusError(String((err && err.message) || err)) })
|
|
636
|
+
}, [])
|
|
637
|
+
|
|
638
|
+
React.useEffect(() => {
|
|
639
|
+
if (open && accessStatus === null && !statusError) loadStatus()
|
|
640
|
+
}, [open, accessStatus, statusError, loadStatus])
|
|
147
641
|
|
|
148
642
|
let scope = null
|
|
149
643
|
try {
|
|
@@ -185,7 +679,7 @@ window.__ModuleLoader__.load({
|
|
|
185
679
|
if (!scope || typeof scope.set !== 'function') return
|
|
186
680
|
setSaving(true)
|
|
187
681
|
const failed = []
|
|
188
|
-
const keys = ['tokenEnv', 'tokenSource', 'defaultRepository', 'baseUrl', 'timeoutMs', 'maxRetries', 'reviewRulesJson'
|
|
682
|
+
const keys = ['tokenEnv', 'tokenSource', 'defaultRepository', 'baseUrl', 'timeoutMs', 'maxRetries', 'reviewRulesJson']
|
|
189
683
|
for (const key of keys) {
|
|
190
684
|
if (current[key] === value[key]) continue
|
|
191
685
|
let next = current[key]
|
|
@@ -241,18 +735,45 @@ window.__ModuleLoader__.load({
|
|
|
241
735
|
field('baseUrl', 'baseUrl', 'baseUrlHint'),
|
|
242
736
|
field('timeoutMs', 'timeoutMs', 'timeoutMsHint', 'number'),
|
|
243
737
|
field('maxRetries', 'maxRetries', 'maxRetriesHint', 'number'),
|
|
244
|
-
React.createElement('div', { className: 'gho-field' },
|
|
245
|
-
React.createElement('label', { className: 'gho-label', htmlFor: `${NS}-approvalMode` }, t('approvalMode')),
|
|
246
|
-
React.createElement('div', { className: 'gho-hint' }, t('approvalModeHint')),
|
|
247
|
-
React.createElement('select', {
|
|
248
|
-
id: `${NS}-approvalMode`,
|
|
249
|
-
className: 'gho-input',
|
|
250
|
-
value: current.approvalMode || 'auto',
|
|
251
|
-
disabled: !writable,
|
|
252
|
-
onChange: (event) => change('approvalMode', event.target.value),
|
|
253
|
-
}, ['auto', 'ask', 'off'].map((option) => React.createElement('option', { key: option, value: option }, option))),
|
|
254
|
-
),
|
|
255
738
|
field('reviewRulesJson', 'reviewRules', 'reviewRulesHint', 'json'),
|
|
739
|
+
React.createElement('div', { className: 'gho-status' },
|
|
740
|
+
React.createElement('div', { className: 'gho-status-head' },
|
|
741
|
+
t('statusTitle'),
|
|
742
|
+
React.createElement('button', { type: 'button', className: 'gho-link', onClick: loadStatus }, t('statusRefresh')),
|
|
743
|
+
),
|
|
744
|
+
accessStatus === 'loading' ? React.createElement('div', { className: 'gho-status-row' }, t('statusLoading')) : null,
|
|
745
|
+
statusError ? React.createElement('div', { className: 'gho-status-row gho-status-bad' }, `${t('statusError')}: ${statusError}`) : null,
|
|
746
|
+
accessStatus && accessStatus !== 'loading' && !accessStatus.configured
|
|
747
|
+
? React.createElement('div', { className: 'gho-status-row' }, t('statusNotConfigured'))
|
|
748
|
+
: null,
|
|
749
|
+
accessStatus && accessStatus !== 'loading' && accessStatus.guidance
|
|
750
|
+
? React.createElement('div', { className: 'gho-status-row' }, accessStatus.guidance)
|
|
751
|
+
: null,
|
|
752
|
+
accessStatus && accessStatus !== 'loading' && accessStatus.configured
|
|
753
|
+
? React.createElement('div', null,
|
|
754
|
+
React.createElement('div', { className: 'gho-status-row' },
|
|
755
|
+
React.createElement('span', { className: 'gho-status-key' }, t('statusSource')),
|
|
756
|
+
React.createElement('span', { className: 'gho-status-value' }, accessStatus.source || '—'),
|
|
757
|
+
),
|
|
758
|
+
React.createElement('div', { className: 'gho-status-row' },
|
|
759
|
+
React.createElement('span', { className: 'gho-status-key' }, t('statusAccount')),
|
|
760
|
+
React.createElement('span', { className: 'gho-status-value' }, accessStatus.login || '—'),
|
|
761
|
+
),
|
|
762
|
+
accessStatus.scopes ? React.createElement('div', { className: 'gho-status-row' },
|
|
763
|
+
React.createElement('span', { className: 'gho-status-key' }, t('statusScopes')),
|
|
764
|
+
React.createElement('span', { className: 'gho-status-value' }, accessStatus.scopes),
|
|
765
|
+
) : null,
|
|
766
|
+
accessStatus.rateLimit ? React.createElement('div', { className: 'gho-status-row' },
|
|
767
|
+
React.createElement('span', { className: 'gho-status-key' }, t('statusRate')),
|
|
768
|
+
React.createElement('span', { className: 'gho-status-value' }, `${accessStatus.rateLimit.remaining ?? '?'}/${accessStatus.rateLimit.limit ?? '?'}${accessStatus.rateLimit.resetAt ? ` · ${accessStatus.rateLimit.resetAt}` : ''}`),
|
|
769
|
+
) : null,
|
|
770
|
+
accessStatus.cache ? React.createElement('div', { className: 'gho-status-row' },
|
|
771
|
+
React.createElement('span', { className: 'gho-status-key' }, t('statusCache')),
|
|
772
|
+
React.createElement('span', { className: 'gho-status-value' }, `${accessStatus.cache.size} · ${accessStatus.cache.ttlMs} ms`),
|
|
773
|
+
) : null,
|
|
774
|
+
)
|
|
775
|
+
: null,
|
|
776
|
+
),
|
|
256
777
|
React.createElement('div', { className: 'gho-foot' },
|
|
257
778
|
React.createElement('span', { className: 'gho-note' }, message),
|
|
258
779
|
React.createElement('button', {
|
|
@@ -311,9 +832,76 @@ window.__ModuleLoader__.load({
|
|
|
311
832
|
}
|
|
312
833
|
}
|
|
313
834
|
|
|
835
|
+
// The panel goes into the sidebar: better-sidebar first (its contract is explicit), and
|
|
836
|
+
// the built-in right sidebar through its tab registry. Both are feature-detected, so a
|
|
837
|
+
// composition without either still loads the plugin.
|
|
838
|
+
try {
|
|
839
|
+
ctx.inject(['betterSidebar'], (sctx) => {
|
|
840
|
+
try {
|
|
841
|
+
sctx.effect(() => sctx.betterSidebar.registerTab({
|
|
842
|
+
id: 'github-ops:panel',
|
|
843
|
+
title: 'GitHub',
|
|
844
|
+
component: (viewProps) => React.createElement(GitHubPanel, { t: ctx.locale.bind(NS), scope: viewProps && viewProps.scope }),
|
|
845
|
+
}), 'dsh-github-ops: better-sidebar panel')
|
|
846
|
+
} catch {
|
|
847
|
+
/* an older better-sidebar without registerTab */
|
|
848
|
+
}
|
|
849
|
+
})
|
|
850
|
+
} catch {
|
|
851
|
+
/* no better-sidebar in this composition */
|
|
852
|
+
}
|
|
853
|
+
try {
|
|
854
|
+
ctx.inject(['sidebarRightTabs'], (sctx) => {
|
|
855
|
+
try {
|
|
856
|
+
sctx.effect(() => sctx.sidebarRightTabs.register({
|
|
857
|
+
id: 'github-ops:panel',
|
|
858
|
+
kind: 'github',
|
|
859
|
+
patterns: ['dsh-resource://github/**'],
|
|
860
|
+
priority: 'builtin',
|
|
861
|
+
canOpen: (address) => String(address || '').startsWith('dsh-resource://github/'),
|
|
862
|
+
title: () => 'GitHub',
|
|
863
|
+
}), 'dsh-github-ops: sidebar tab')
|
|
864
|
+
} catch {
|
|
865
|
+
/* a core without the right-sidebar tab registry */
|
|
866
|
+
}
|
|
867
|
+
})
|
|
868
|
+
} catch {
|
|
869
|
+
/* no built-in right sidebar in this composition */
|
|
870
|
+
}
|
|
871
|
+
// The pull request bar above the composer: a suggestion, never a write.
|
|
872
|
+
try {
|
|
873
|
+
ctx.slots.inject('conversation.composer.bar', () => ctx.slots.register(
|
|
874
|
+
{ name: 'conversation.composer.bar', locale: NS, inject: () => ({ t: ctx.locale.bind(NS) }) },
|
|
875
|
+
PullRequestBar,
|
|
876
|
+
))
|
|
877
|
+
} catch {
|
|
878
|
+
/* a core without the composer bar: the rest of the plugin still works */
|
|
879
|
+
}
|
|
880
|
+
installLinkWatcher()
|
|
881
|
+
// The Source Control tab lives in the session view ring, the same seat the Memory tab
|
|
882
|
+
// of dsh-memory-meter uses: a session-scoped view, not a navigation of our own.
|
|
883
|
+
try {
|
|
884
|
+
ctx.slots.inject('conversation.view', () => ctx.slots.register(
|
|
885
|
+
{
|
|
886
|
+
name: 'conversation.view',
|
|
887
|
+
id: 'github-scm',
|
|
888
|
+
order: 40,
|
|
889
|
+
label: () => 'Source control',
|
|
890
|
+
locale: NS,
|
|
891
|
+
inject: () => ({ t: ctx.locale.bind(NS) }),
|
|
892
|
+
},
|
|
893
|
+
SourceControlView,
|
|
894
|
+
))
|
|
895
|
+
} catch {
|
|
896
|
+
/* a core without the session view ring: the rest of the plugin still works */
|
|
897
|
+
}
|
|
898
|
+
|
|
314
899
|
exports.inject = ['slots', 'locale', 'settingsScope']
|
|
315
900
|
exports.apply = apply
|
|
316
901
|
exports.GitHubOpsCard = GitHubOpsCard
|
|
902
|
+
exports.SourceControlView = SourceControlView
|
|
903
|
+
exports.PullRequestBar = PullRequestBar
|
|
904
|
+
exports.GitHubPanel = GitHubPanel
|
|
317
905
|
return module.exports
|
|
318
906
|
},
|
|
319
907
|
})
|