@goodandready/dsh-github-ops 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -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
@@ -47,6 +54,78 @@ window.__ModuleLoader__.load({
47
54
  savedPartial: 'Saved, except: ',
48
55
  noChanges: 'Nothing to change.',
49
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
+ scmCommit: 'Commit',
84
+ scmCommitPush: 'Commit & push',
85
+ scmSync: 'Sync',
86
+ scmAmend: 'amend',
87
+ scmMessage: 'Commit message',
88
+ scmStage: 'Stage',
89
+ scmUnstage: 'Unstage',
90
+ scmDiscard: 'Discard',
91
+ scmConfirm: 'Confirm?',
92
+ scmNewBranch: 'New branch',
93
+ scmCreate: 'Create',
94
+ scmDelete: 'Delete',
95
+ scmSwitch: 'Switch',
96
+ scmStashSave: 'Save stash',
97
+ scmStashApply: 'Apply',
98
+ scmStashPop: 'Pop',
99
+ scmStashDrop: 'Drop',
100
+ scmContinue: 'Continue',
101
+ scmAbort: 'Abort',
102
+ scmWorking: 'Working…',
103
+ scmActionFailed: 'git refused',
104
+ scmNoChanges: 'Nothing staged or changed',
105
+ scmBehindHint: 'Behind the upstream: sync pulls with rebase, then pushes',
106
+ scmPushBranch: 'Push branch',
107
+ scmRepoPath: 'Repository path (optional)',
108
+ scmRepoPathHint: 'Absolute path; empty means the session workspace',
109
+ prBarBranch: 'to',
110
+ prBarCommits: 'commit(s) ahead',
111
+ panelTitle: 'GitHub',
112
+ panelRepo: 'Repository',
113
+ panelSearch: 'Search public repositories…',
114
+ panelChoose: 'Choose a repository to browse.',
115
+ panelBack: 'Up',
116
+ panelIssues: 'Issues',
117
+ panelPulls: 'Pull requests',
118
+ panelRuns: 'Actions',
119
+ panelInbox: 'Inbox',
120
+ panelOpen: 'Open in browser',
121
+ panelEmpty: 'Nothing here yet.',
122
+ panelLoading: 'Loading…',
123
+ panelNoAccess: 'No GitHub access yet: configure a credential, sign in, or use the gh CLI.',
124
+ prBarCreate: 'Create PR',
125
+ prBarReview: 'Review',
126
+ prBarMerge: 'Merge',
127
+ prBarCopied: 'Instruction copied: paste it into the composer and send.',
128
+ prBarCopyFailed: 'Could not copy; the instruction is in the title.',
50
129
  }
51
130
 
52
131
  const zh = {
@@ -76,6 +155,78 @@ window.__ModuleLoader__.load({
76
155
  savedPartial: '已保存,除了:',
77
156
  noChanges: '没有需要修改的内容。',
78
157
  invalidNumber: '请输入正数。',
158
+ statusTitle: '访问状态',
159
+ statusLoading: '正在检查访问…',
160
+ statusNotConfigured: '尚未配置访问',
161
+ statusError: '访问检查失败',
162
+ statusSource: '来源',
163
+ statusAccount: '账号',
164
+ statusScopes: '权限范围',
165
+ statusRate: '速率限制',
166
+ statusCache: '读取缓存',
167
+ statusRefresh: '重新检查',
168
+ scmTitle: '源代码管理',
169
+ scmRefresh: '刷新',
170
+ scmLoading: '正在读取工作副本…',
171
+ scmNotRepo: '当前工作区不是 git 仓库,这里没有可显示的内容。',
172
+ scmError: '无法读取工作副本',
173
+ scmStaged: '已暂存',
174
+ scmChanges: '更改',
175
+ scmUntracked: '未跟踪',
176
+ scmConflicts: '冲突',
177
+ scmNothing: '没有待提交内容:工作副本是干净的。',
178
+ scmDiffEmpty: '该文件没有差异(可能是二进制或未跟踪)。',
179
+ scmBranches: '分支',
180
+ scmTags: '标签',
181
+ scmStashes: '暂存',
182
+ scmInProgress: '进行中',
183
+ scmNoUpstream: '无上游',
184
+ scmCommit: '提交',
185
+ scmCommitPush: '提交并推送',
186
+ scmSync: '同步',
187
+ scmAmend: '修补',
188
+ scmMessage: '提交信息',
189
+ scmStage: '暂存',
190
+ scmUnstage: '取消暂存',
191
+ scmDiscard: '放弃更改',
192
+ scmConfirm: '确认?',
193
+ scmNewBranch: '新分支',
194
+ scmCreate: '创建',
195
+ scmDelete: '删除',
196
+ scmSwitch: '切换',
197
+ scmStashSave: '保存 stash',
198
+ scmStashApply: '应用',
199
+ scmStashPop: '弹出',
200
+ scmStashDrop: '删除',
201
+ scmContinue: '继续',
202
+ scmAbort: '中止',
203
+ scmWorking: '执行中…',
204
+ scmActionFailed: 'git 拒绝',
205
+ scmNoChanges: '没有已暂存或已修改的内容',
206
+ scmBehindHint: '落后于上游:同步会先 rebase 拉取,再推送',
207
+ scmPushBranch: '推送分支',
208
+ scmRepoPath: '仓库路径(可选)',
209
+ scmRepoPathHint: '绝对路径;留空则使用会话工作区',
210
+ prBarBranch: '→',
211
+ prBarCommits: '个提交领先',
212
+ panelTitle: 'GitHub',
213
+ panelRepo: '仓库',
214
+ panelSearch: '搜索公开仓库…',
215
+ panelChoose: '请选择要浏览的仓库。',
216
+ panelBack: '上一级',
217
+ panelIssues: 'Issue',
218
+ panelPulls: 'Pull request',
219
+ panelRuns: 'Actions',
220
+ panelInbox: '收件箱',
221
+ panelOpen: '在浏览器中打开',
222
+ panelEmpty: '这里还没有内容。',
223
+ panelLoading: '加载中…',
224
+ panelNoAccess: '尚未配置 GitHub 访问:请设置凭据、登录或使用 gh CLI。',
225
+ prBarCreate: '创建 PR',
226
+ prBarReview: '评审',
227
+ prBarMerge: '合并',
228
+ prBarCopied: '指令已复制:粘贴到输入框并发送。',
229
+ prBarCopyFailed: '复制失败;指令在提示中。',
79
230
  }
80
231
 
81
232
  let ChevronIcon = null
@@ -92,6 +243,482 @@ window.__ModuleLoader__.load({
92
243
  React.createElement('path', { d: 'M3.5 5.25L7 8.75L10.5 5.25' }),
93
244
  )
94
245
 
246
+
247
+ /**
248
+ * Source Control tab: what the working copy looks like right now.
249
+ *
250
+ * Reads only. Data comes from the host route on the session workspace; nothing here writes
251
+ * to the repository, so a change always starts with an explicit human action.
252
+ */
253
+ function SourceControlView(props) {
254
+ ensureCss()
255
+ const t = props.t || ((key) => key)
256
+ const [state, setState] = React.useState({ phase: 'loading' })
257
+ const [selected, setSelected] = React.useState(null)
258
+ const [diff, setDiff] = React.useState({ phase: 'idle', text: '' })
259
+ const [busy, setBusy] = React.useState('')
260
+ const [actionError, setActionError] = React.useState('')
261
+ const [message, setMessage] = React.useState('')
262
+ const [amend, setAmend] = React.useState(false)
263
+ const [confirming, setConfirming] = React.useState('')
264
+ const [branchName, setBranchName] = React.useState('')
265
+ const [stashMessage, setStashMessage] = React.useState('')
266
+ const [repoPath, setRepoPath] = React.useState(() => {
267
+ try { return localStorage.getItem('gho-scm-cwd') || '' } catch { return '' }
268
+ })
269
+
270
+ // The tab reads the session workspace by default; an explicit path lets it look at any
271
+ // repository the local user names.
272
+ const cwdQuery = repoPath ? `cwd=${encodeURIComponent(repoPath)}` : ''
273
+ const rememberPath = (value) => {
274
+ setRepoPath(value)
275
+ try { value ? localStorage.setItem('gho-scm-cwd', value) : localStorage.removeItem('gho-scm-cwd') } catch { /* private mode */ }
276
+ }
277
+
278
+ const load = React.useCallback(() => {
279
+ setState({ phase: 'loading' })
280
+ if (typeof fetch !== 'function') {
281
+ setState({ phase: 'error', message: 'fetch is unavailable in this client' })
282
+ return
283
+ }
284
+ fetch(cwdQuery ? `${SCM_PATH}?${cwdQuery}` : SCM_PATH, { headers: { accept: 'application/json' } })
285
+ .then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
286
+ .then((payload) => setState({ phase: 'ready', payload }))
287
+ .catch((err) => setState({ phase: 'error', message: String((err && err.message) || err) }))
288
+ }, [repoPath])
289
+
290
+ React.useEffect(() => { load() }, [load])
291
+
292
+ // Every write is one named action; the host validates it and runs argv, never a shell.
293
+ const runAction = (action, args) => {
294
+ if (typeof fetch !== 'function') return
295
+ setBusy(action)
296
+ setActionError('')
297
+ fetch(SCM_PATH, {
298
+ method: 'POST',
299
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
300
+ body: JSON.stringify(repoPath ? { action, args: args || {}, cwd: repoPath } : { action, args: args || {} }),
301
+ })
302
+ .then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
303
+ .then((payload) => {
304
+ setBusy('')
305
+ setConfirming('')
306
+ if (!payload || payload.ok !== true) setActionError((payload && payload.error) || t('scmActionFailed'))
307
+ load()
308
+ })
309
+ .catch((err) => { setBusy(''); setActionError(String((err && err.message) || err)) })
310
+ }
311
+
312
+ // Destructive actions ask twice: the first click arms the button, the second runs it.
313
+ const armed = (key, action, args) => {
314
+ if (confirming !== key) { setConfirming(key); return }
315
+ setConfirming('')
316
+ runAction(action, args)
317
+ }
318
+
319
+ const openDiff = (file, staged) => {
320
+ setSelected({ path: file.path, staged })
321
+ setDiff({ phase: 'loading', text: '' })
322
+ const query = `?file=${encodeURIComponent(file.path)}${staged ? '&staged=1' : ''}`
323
+ fetch(SCM_PATH + query, { headers: { accept: 'application/json' } })
324
+ .then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
325
+ .then((payload) => setDiff({ phase: 'ready', text: payload.diff || '' }))
326
+ .catch((err) => setDiff({ phase: 'error', text: String((err && err.message) || err) }))
327
+ }
328
+
329
+ const button = (label, onClick, options) => React.createElement('button', {
330
+ type: 'button',
331
+ className: `gho-scm-btn${options && options.warn ? ' gho-scm-btn-warn' : ''}`,
332
+ disabled: Boolean(busy) || Boolean(options && options.disabled),
333
+ onClick,
334
+ }, label)
335
+
336
+ const fileRow = (file, staged) => {
337
+ const discardKey = `discard:${file.path}`
338
+ return React.createElement('div', {
339
+ key: `${staged ? 's' : 'w'}-${file.path}`,
340
+ className: 'gho-scm-file',
341
+ role: 'button',
342
+ tabIndex: 0,
343
+ 'aria-selected': selected && selected.path === file.path && selected.staged === staged,
344
+ onClick: () => openDiff(file, staged),
345
+ onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') openDiff(file, staged) },
346
+ },
347
+ React.createElement('span', { className: 'gho-scm-badge' }, file.status),
348
+ React.createElement('span', { className: 'gho-scm-path' }, file.renamedFrom ? `${file.renamedFrom} → ${file.path}` : file.path),
349
+ React.createElement('span', { className: 'gho-scm-actions' },
350
+ staged
351
+ ? button(t('scmUnstage'), () => runAction('unstage', { path: file.path }))
352
+ : button(t('scmStage'), () => runAction('stage', { path: file.path })),
353
+ button(confirming === discardKey ? t('scmConfirm') : t('scmDiscard'), () => armed(discardKey, 'discard', { path: file.path, confirm: true }), { warn: true }),
354
+ ),
355
+ )
356
+ }
357
+
358
+ const group = (title, files, staged) => (files && files.length
359
+ ? React.createElement('div', { className: 'gho-scm-group', key: title },
360
+ React.createElement('div', { className: 'gho-scm-group-head' }, `${title} (${files.length})`),
361
+ files.map((file) => fileRow(file, staged)),
362
+ )
363
+ : null)
364
+
365
+ if (state.phase === 'loading') return React.createElement('div', { className: 'gho-scm' }, t('scmLoading'))
366
+ if (state.phase === 'error') {
367
+ return React.createElement('div', { className: 'gho-scm' },
368
+ React.createElement('div', { className: 'gho-scm-group-head' }, t('scmError')),
369
+ React.createElement('div', { className: 'gho-scm-meta' }, state.message),
370
+ button(t('scmRefresh'), load),
371
+ )
372
+ }
373
+ const payload = state.payload || {}
374
+ if (!payload.isRepository) {
375
+ return React.createElement('div', { className: 'gho-scm' },
376
+ React.createElement('div', { className: 'gho-scm-group-head' }, t('scmNotRepo')),
377
+ payload.error ? React.createElement('div', { className: 'gho-scm-meta' }, payload.error) : null,
378
+ button(t('scmRefresh'), load),
379
+ )
380
+ }
381
+ const files = payload.files || { staged: [], changes: [], untracked: [], unmerged: [], total: 0 }
382
+ const clean = !files.total
383
+ const canCommit = files.staged.length > 0 && message.trim().length > 0 && !busy
384
+ const ahead = payload.ahead || 0
385
+ const behind = payload.behind || 0
386
+
387
+ const commitBar = React.createElement('div', { className: 'gho-scm-bar' },
388
+ React.createElement('input', {
389
+ type: 'text',
390
+ value: message,
391
+ placeholder: t('scmMessage'),
392
+ onChange: (event) => setMessage(event.target.value),
393
+ onKeyDown: (event) => { if (event.key === 'Enter' && canCommit) runAction('commit', { message, amend }) },
394
+ }),
395
+ React.createElement('label', { className: 'gho-scm-check' },
396
+ React.createElement('input', { type: 'checkbox', checked: amend, onChange: (event) => setAmend(event.target.checked) }),
397
+ t('scmAmend'),
398
+ ),
399
+ button(t('scmCommit'), () => runAction('commit', { message, amend }), { disabled: !canCommit }),
400
+ button(t('scmCommitPush'), () => runAction('commit', { message, amend, push: true }), { disabled: !canCommit }),
401
+ button(behind > 0 ? t('scmSync') : t('scmPushBranch'), () => runAction(behind > 0 ? 'sync' : 'push', {}), { disabled: Boolean(busy) || (ahead === 0 && behind === 0) }),
402
+ )
403
+
404
+ const mergeBar = payload.mergeState && payload.mergeState.inProgress
405
+ ? React.createElement('div', { className: 'gho-scm-bar' },
406
+ React.createElement('span', null, `${t('scmInProgress')}: ${payload.mergeState.kind}`),
407
+ button(t('scmContinue'), () => runAction('continue', { verb: payload.mergeState.kind })),
408
+ button(confirming === 'abort' ? t('scmConfirm') : t('scmAbort'), () => armed('abort', 'abort', { verb: payload.mergeState.kind, confirm: true }), { warn: true }),
409
+ )
410
+ : null
411
+
412
+ const branchBar = React.createElement('div', { className: 'gho-scm-bar' },
413
+ React.createElement('input', {
414
+ type: 'text',
415
+ value: branchName,
416
+ placeholder: t('scmNewBranch'),
417
+ onChange: (event) => setBranchName(event.target.value),
418
+ }),
419
+ button(t('scmCreate'), () => runAction('branchCreate', { branch: branchName }), { disabled: !branchName.trim() || Boolean(busy) }),
420
+ (payload.branches || []).filter((b) => !b.current).slice(0, 6).map((b) => button(
421
+ confirming === `branch:${b.name}` ? `${b.name}: ${t('scmConfirm')}` : `${t('scmSwitch')} ${b.name}`,
422
+ () => armed(`branch:${b.name}`, 'branchCheckout', { branch: b.name }),
423
+ )),
424
+ (payload.branches || []).filter((b) => !b.current).slice(0, 6).map((b) => button(
425
+ confirming === `del:${b.name}` ? `${t('scmDelete')}?` : `${t('scmDelete')} ${b.name}`,
426
+ () => armed(`del:${b.name}`, 'branchDelete', { branch: b.name, confirm: true }),
427
+ { warn: true },
428
+ )),
429
+ )
430
+
431
+ const stashBar = React.createElement('div', { className: 'gho-scm-bar' },
432
+ React.createElement('input', {
433
+ type: 'text',
434
+ value: stashMessage,
435
+ placeholder: t('scmStashSave'),
436
+ onChange: (event) => setStashMessage(event.target.value),
437
+ }),
438
+ button(t('scmStashSave'), () => runAction('stashPush', { message: stashMessage }), { disabled: Boolean(busy) }),
439
+ (payload.stashes || []).slice(0, 4).map((entry) => button(`${t('scmStashApply')} ${entry.ref}`, () => runAction('stashApply', { ref: entry.ref }))),
440
+ (payload.stashes || []).slice(0, 4).map((entry) => button(`${t('scmStashPop')} ${entry.ref}`, () => runAction('stashPop', { ref: entry.ref }))),
441
+ (payload.stashes || []).slice(0, 4).map((entry) => button(
442
+ confirming === `stash:${entry.ref}` ? `${entry.ref}: ${t('scmConfirm')}` : `${t('scmStashDrop')} ${entry.ref}`,
443
+ () => armed(`stash:${entry.ref}`, 'stashDrop', { ref: entry.ref, confirm: true }),
444
+ { warn: true },
445
+ )),
446
+ )
447
+
448
+ return React.createElement('div', { className: 'gho-scm' },
449
+ React.createElement('div', { className: 'gho-scm-head' },
450
+ React.createElement('span', { className: 'gho-scm-repo' }, payload.cwd || ''),
451
+ React.createElement('span', { className: 'gho-scm-branch' }, payload.branch),
452
+ payload.upstream
453
+ ? React.createElement('span', { className: 'gho-scm-counts' }, `↑${ahead} ↓${behind}`)
454
+ : React.createElement('span', { className: 'gho-scm-counts' }, t('scmNoUpstream')),
455
+ button(busy ? t('scmWorking') : t('scmRefresh'), load, { disabled: Boolean(busy) }),
456
+ ),
457
+ React.createElement('div', { className: 'gho-scm-bar' },
458
+ React.createElement('input', {
459
+ type: 'text',
460
+ value: repoPath,
461
+ placeholder: t('scmRepoPath'),
462
+ title: t('scmRepoPathHint'),
463
+ onChange: (event) => rememberPath(event.target.value),
464
+ onKeyDown: (event) => { if (event.key === 'Enter') load() },
465
+ }),
466
+ ),
467
+ actionError ? React.createElement('div', { className: 'gho-scm-meta gho-scm-error' }, `${t('scmActionFailed')}: ${actionError}`) : null,
468
+ behind > 0 ? React.createElement('div', { className: 'gho-scm-meta' }, t('scmBehindHint')) : null,
469
+ mergeBar,
470
+ commitBar,
471
+ clean ? React.createElement('div', { className: 'gho-scm-meta' }, t('scmNothing')) : null,
472
+ group(t('scmConflicts'), files.unmerged, false),
473
+ group(t('scmStaged'), files.staged, true),
474
+ group(t('scmChanges'), files.changes, false),
475
+ group(t('scmUntracked'), files.untracked, false),
476
+ selected
477
+ ? React.createElement('pre', { className: 'gho-scm-diff' },
478
+ diff.phase === 'loading' ? t('scmLoading') : (diff.text || t('scmDiffEmpty')))
479
+ : null,
480
+ branchBar,
481
+ stashBar,
482
+ React.createElement('div', { className: 'gho-scm-meta' },
483
+ React.createElement('span', null, `${t('scmTags')}: ${(payload.tags || []).join(', ') || '—'}`),
484
+ ),
485
+ )
486
+ }
487
+
488
+ /**
489
+ * The pull request bar above the composer.
490
+ *
491
+ * It appears when the branch has commits the upstream does not, and it never writes: the
492
+ * buttons put an instruction on the clipboard for the human to send, so the call still goes
493
+ * through the normal approval contour.
494
+ */
495
+ function PullRequestBar(props) {
496
+ ensureCss()
497
+ const t = props.t || ((key) => key)
498
+ const [state, setState] = React.useState({ phase: 'idle' })
499
+ const [note, setNote] = React.useState('')
500
+
501
+ const load = React.useCallback(() => {
502
+ if (typeof fetch !== 'function') return
503
+ fetch(SCM_PATH, { headers: { accept: 'application/json' } })
504
+ .then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
505
+ .then((payload) => setState({ phase: 'ready', payload }))
506
+ .catch(() => setState({ phase: 'idle' }))
507
+ }, [])
508
+
509
+ React.useEffect(() => { load() }, [load])
510
+
511
+ const payload = state.payload
512
+ const ahead = payload && payload.isRepository && payload.upstream ? (payload.ahead || 0) : 0
513
+ if (!payload || !payload.isRepository || ahead <= 0) return null
514
+
515
+ const instruction = (kind) => {
516
+ const repo = payload.cwd ? ` (workspace ${payload.cwd})` : ''
517
+ 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}.`
518
+ if (kind === 'review') return `Review the open pull request of ${payload.branch}: call gh_review and then ci_run, and summarise the findings.`
519
+ return `Merge the pull request of ${payload.branch} after the review: call pr_merge.`
520
+ }
521
+
522
+ const copy = (kind) => {
523
+ const text = instruction(kind)
524
+ try {
525
+ if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
526
+ navigator.clipboard.writeText(text)
527
+ setNote(t('prBarCopied'))
528
+ return
529
+ }
530
+ throw new Error('clipboard unavailable')
531
+ } catch {
532
+ setNote(`${t('prBarCopyFailed')} ${text}`)
533
+ }
534
+ }
535
+
536
+ const base = payload.upstream ? payload.upstream.replace(/^[^/]+\//, '') : ''
537
+ return React.createElement('div', { className: 'gho-prbar' },
538
+ React.createElement('span', { className: 'gho-prbar-branch' }, `${payload.branch} ${base ? `→ ${base}` : ''}`.trim()),
539
+ React.createElement('span', null, `↑${ahead} ${t('prBarCommits')}`),
540
+ note ? React.createElement('span', { className: 'gho-prbar-note' }, note) : null,
541
+ React.createElement('span', { className: 'gho-prbar-actions' },
542
+ React.createElement('button', { type: 'button', className: 'gho-prbar-btn', onClick: () => copy('create') }, t('prBarCreate')),
543
+ React.createElement('button', { type: 'button', className: 'gho-prbar-btn', onClick: () => copy('review') }, t('prBarReview')),
544
+ React.createElement('button', { type: 'button', className: 'gho-prbar-btn', onClick: () => copy('merge') }, t('prBarMerge')),
545
+ ),
546
+ )
547
+ }
548
+
549
+
550
+ /** Remember which repository a github.com link pointed at, when the user opted in. */
551
+ function installLinkWatcher() {
552
+ if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return
553
+ document.addEventListener('click', (event) => {
554
+ if (!linkState.enabled) return
555
+ const anchor = event.target && event.target.closest ? event.target.closest('a[href]') : null
556
+ if (!anchor) return
557
+ const match = String(anchor.getAttribute('href') || '').match(/^https?:\/\/github\.com\/([^/]+)\/([^/?#]+)/)
558
+ if (!match) return
559
+ // the browser still opens the link: this only records where the human is looking
560
+ linkState.repo = `${match[1]}/${match[2].replace(/\.git$/, '')}`
561
+ }, true)
562
+ }
563
+
564
+
565
+ /**
566
+ * The GitHub panel: browse a repository, its issues, pull requests, runs and the attention
567
+ * inbox from the sidebar.
568
+ *
569
+ * Reads only, and through the host route: the panel never holds a token and never writes.
570
+ */
571
+ function GitHubPanel(props) {
572
+ ensureCss()
573
+ const t = props.t || ((key) => key)
574
+ const [repo, setRepo] = React.useState('')
575
+ const [repos, setRepos] = React.useState([])
576
+ const [search, setSearch] = React.useState('')
577
+ const [tab, setTab] = React.useState('tree')
578
+ const [tree, setTree] = React.useState({ phase: 'idle', path: '', entries: [] })
579
+ const [file, setFile] = React.useState(null)
580
+ const [items, setItems] = React.useState({ phase: 'idle', list: [] })
581
+ const [error, setError] = React.useState('')
582
+ const [loading, setLoading] = React.useState(false)
583
+
584
+ const call = React.useCallback((params) => {
585
+ if (typeof fetch !== 'function') return Promise.reject(new Error('fetch is unavailable in this client'))
586
+ const query = new URLSearchParams(params).toString()
587
+ return fetch(`${PANEL_PATH}?${query}`, { headers: { accept: 'application/json' } })
588
+ .then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
589
+ }, [])
590
+
591
+ const loadRepos = React.useCallback((q) => {
592
+ setLoading(true)
593
+ setError('')
594
+ call(q ? { what: 'repos', q } : { what: 'repos' })
595
+ .then((payload) => {
596
+ if (payload.configured === false) { setError(payload.guidance || t('panelNoAccess')); setRepos([]); return }
597
+ setRepos(payload.repos || [])
598
+ })
599
+ .catch((err) => setError(String((err && err.message) || err)))
600
+ .finally(() => setLoading(false))
601
+ }, [call, t])
602
+
603
+ React.useEffect(() => { loadRepos('') }, [loadRepos])
604
+
605
+ const loadTree = React.useCallback((path) => {
606
+ if (!repo) return
607
+ setFile(null)
608
+ setTree({ phase: 'loading', path, entries: [] })
609
+ call({ what: 'tree', repo, path })
610
+ .then((payload) => setTree({ phase: 'ready', path: payload.path || path, entries: payload.entries || [], truncated: payload.truncated }))
611
+ .catch((err) => { setTree({ phase: 'error', path, entries: [] }); setError(String((err && err.message) || err)) })
612
+ }, [call, repo])
613
+
614
+ const loadList = React.useCallback((which) => {
615
+ if (!repo) return
616
+ setItems({ phase: 'loading', list: [] })
617
+ call({ what: which, repo })
618
+ .then((payload) => setItems({ phase: 'ready', list: payload.items || [] }))
619
+ .catch((err) => { setItems({ phase: 'error', list: [] }); setError(String((err && err.message) || err)) })
620
+ }, [call, repo])
621
+
622
+ const loadInbox = React.useCallback(() => {
623
+ setItems({ phase: 'loading', list: [] })
624
+ call({ what: 'inbox' })
625
+ .then((payload) => setItems({ phase: 'ready', list: payload.notifications || [] }))
626
+ .catch((err) => { setItems({ phase: 'error', list: [] }); setError(String((err && err.message) || err)) })
627
+ }, [call])
628
+
629
+ React.useEffect(() => {
630
+ if (!repo) return
631
+ if (tab === 'tree') loadTree('')
632
+ else if (tab === 'inbox') loadInbox()
633
+ else loadList(tab)
634
+ }, [repo, tab, loadTree, loadList, loadInbox])
635
+
636
+ const openFile = (entry) => {
637
+ if (entry.type === 'tree') { loadTree(entry.path); return }
638
+ call({ what: 'blob', repo, path: entry.path })
639
+ .then((payload) => setFile(payload))
640
+ .catch((err) => setError(String((err && err.message) || err)))
641
+ }
642
+
643
+ const tabButton = (id, label) => React.createElement('button', {
644
+ key: id, type: 'button', className: 'gho-panel-tab', 'aria-pressed': tab === id, onClick: () => setTab(id),
645
+ }, label)
646
+
647
+ const header = React.createElement('div', { className: 'gho-panel-row' },
648
+ React.createElement('span', { className: 'gho-panel-muted' }, t('panelRepo')),
649
+ React.createElement('span', null, repo || '—'),
650
+ repo ? React.createElement('a', { className: 'gho-link', href: `https://github.com/${repo}`, target: '_blank', rel: 'noreferrer' }, t('panelOpen')) : null,
651
+ )
652
+
653
+ const switcher = React.createElement('div', { className: 'gho-panel' },
654
+ React.createElement('div', { className: 'gho-panel-row' },
655
+ React.createElement('input', {
656
+ className: 'gho-panel-input',
657
+ placeholder: t('panelSearch'),
658
+ value: search,
659
+ onChange: (event) => setSearch(event.target.value),
660
+ onKeyDown: (event) => { if (event.key === 'Enter') loadRepos(search) },
661
+ }),
662
+ ),
663
+ repos.length === 0
664
+ ? React.createElement('div', { className: 'gho-panel-muted' }, loading ? t('panelLoading') : t('panelChoose'))
665
+ : repos.slice(0, 30).map((entry) => React.createElement('div', {
666
+ key: entry.fullName, className: 'gho-panel-item', role: 'button', tabIndex: 0,
667
+ onClick: () => setRepo(entry.fullName),
668
+ onKeyDown: (event) => { if (event.key === 'Enter') setRepo(entry.fullName) },
669
+ },
670
+ React.createElement('span', { className: 'gho-panel-file' }, entry.fullName),
671
+ React.createElement('span', { className: 'gho-panel-muted' }, entry.private ? 'private' : 'public'),
672
+ entry.description ? React.createElement('span', { className: 'gho-panel-muted' }, entry.description.slice(0, 40)) : null,
673
+ )),
674
+ )
675
+
676
+ if (!repo) return React.createElement('div', null, header, switcher)
677
+
678
+ const list = (() => {
679
+ if (items.phase === 'loading') return React.createElement('div', { className: 'gho-panel-muted' }, t('panelLoading'))
680
+ if (!items.list.length) return React.createElement('div', { className: 'gho-panel-muted' }, t('panelEmpty'))
681
+ return items.list.slice(0, 40).map((item, index) => React.createElement('div', { className: 'gho-panel-item', key: `${item.number || item.id || index}` },
682
+ React.createElement('span', { className: 'gho-panel-file' }, item.number ? `#${item.number}` : (item.name || '')),
683
+ React.createElement('span', null, item.title || item.displayTitle || item.reason || item.name || ''),
684
+ item.url || item.html_url ? React.createElement('a', { className: 'gho-link', href: item.url || item.html_url, target: '_blank', rel: 'noreferrer' }, '↗') : null,
685
+ ))
686
+ })()
687
+
688
+ const treeView = React.createElement('div', null,
689
+ tree.path ? React.createElement('button', { type: 'button', className: 'gho-link', onClick: () => loadTree(tree.path.split('/').slice(0, -1).join('/')) }, t('panelBack')) : null,
690
+ tree.phase === 'loading' ? React.createElement('div', { className: 'gho-panel-muted' }, t('panelLoading')) : null,
691
+ tree.phase === 'ready' && tree.entries.length === 0 ? React.createElement('div', { className: 'gho-panel-muted' }, t('panelEmpty')) : null,
692
+ (tree.entries || []).map((entry) => React.createElement('div', {
693
+ key: entry.path, className: 'gho-panel-item', role: 'button', tabIndex: 0,
694
+ onClick: () => openFile(entry),
695
+ onKeyDown: (event) => { if (event.key === 'Enter') openFile(entry) },
696
+ },
697
+ React.createElement('span', { className: 'gho-panel-file' }, entry.type === 'tree' ? '▸' : '·'),
698
+ React.createElement('span', { className: 'gho-panel-file' }, entry.name),
699
+ entry.type === 'blob' && entry.size !== undefined ? React.createElement('span', { className: 'gho-panel-muted' }, `${entry.size} B`) : null,
700
+ )),
701
+ file ? React.createElement('pre', { className: 'gho-panel-pre' }, file.truncated ? `${file.text}\n…` : (file.text || t('panelEmpty'))) : null,
702
+ )
703
+
704
+ return React.createElement('div', { className: 'gho-panel' },
705
+ header,
706
+ React.createElement('div', { className: 'gho-panel-tabs' },
707
+ tabButton('tree', 'Code'),
708
+ tabButton('issues', t('panelIssues')),
709
+ tabButton('pulls', t('panelPulls')),
710
+ tabButton('runs', t('panelRuns')),
711
+ tabButton('inbox', t('panelInbox')),
712
+ React.createElement('button', { type: 'button', className: 'gho-panel-tab', onClick: () => { setRepo(''); setFile(null) } }, t('panelRepo')),
713
+ ),
714
+ error ? React.createElement('div', { className: 'gho-panel-muted' }, error) : null,
715
+ tab === 'tree' ? treeView : list,
716
+ )
717
+ }
718
+
719
+ // This file is the whole client half on purpose: DSH loads it as a single bundle through
720
+ // window.__ModuleLoader__, so splitting the card, the tab, the panel and the pull request
721
+ // bar into several files would need a build step the plugin deliberately does not have.
95
722
  let cssInstalled = false
96
723
  function ensureCss() {
97
724
  if (cssInstalled || typeof document === 'undefined') return
@@ -108,6 +735,51 @@ window.__ModuleLoader__.load({
108
735
  '.gho-field{display:flex;flex-direction:column;gap:6px;padding:12px 0}',
109
736
  '.gho-label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}',
110
737
  '.gho-hint{font-size:12px;color:var(--dsw-alias-label-secondary)}',
738
+ '.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}',
739
+ '.gho-status-row{display:flex;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}',
740
+ '.gho-status-key{min-width:88px;color:var(--dsw-alias-label-tertiary)}',
741
+ '.gho-status-value{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}',
742
+ '.gho-status-head{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}',
743
+ '.gho-status-bad{color:var(--dsw-alias-label-error,#c0392b)}',
744
+ '.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}',
745
+ '.gho-scm{display:flex;flex-direction:column;gap:12px;padding:4px 2px;font-size:13px;color:var(--dsw-alias-label-primary)}',
746
+ '.gho-scm-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}',
747
+ '.gho-scm-repo{font-weight:600}',
748
+ '.gho-scm-branch{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-secondary)}',
749
+ '.gho-scm-counts{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}',
750
+ '.gho-scm-group{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;overflow:hidden}',
751
+ '.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}',
752
+ '.gho-scm-file{display:flex;align-items:center;gap:8px;padding:6px 12px;cursor:pointer;border-top:1px solid var(--dsw-alias-border-l2)}',
753
+ '.gho-scm-file[aria-selected="true"]{background:var(--dsw-alias-bg-layer-3)}',
754
+ '.gho-scm-badge{min-width:18px;text-align:center;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-secondary)}',
755
+ '.gho-scm-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
756
+ '.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}',
757
+ '.gho-scm-meta{display:flex;gap:12px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary)}',
758
+ '.gho-scm-chip{border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:2px 8px}',
759
+ '.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)}',
760
+ '.gho-prbar-branch{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-primary)}',
761
+ '.gho-prbar-actions{display:flex;gap:6px;margin-left:auto}',
762
+ '.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}',
763
+ '.gho-prbar-note{color:var(--dsw-alias-label-tertiary)}',
764
+ '.gho-panel{display:flex;flex-direction:column;gap:10px;font-size:12px;color:var(--dsw-alias-label-primary);padding:4px 2px}',
765
+ '.gho-panel-row{display:flex;align-items:center;gap:6px}',
766
+ '.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}',
767
+ '.gho-panel-tabs{display:flex;gap:4px;flex-wrap:wrap}',
768
+ '.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}',
769
+ '.gho-panel-tab[aria-pressed="true"]{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}',
770
+ '.gho-panel-item{display:flex;gap:8px;align-items:baseline;padding:5px 2px;border-top:1px solid var(--dsw-alias-border-l2);cursor:pointer}',
771
+ '.gho-panel-item:hover{background:var(--dsw-alias-bg-layer-3)}',
772
+ '.gho-panel-muted{color:var(--dsw-alias-label-tertiary)}',
773
+ '.gho-panel-file{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}',
774
+ '.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}',
775
+ '.gho-scm-bar{display:flex;gap:6px;align-items:center;flex-wrap:wrap;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:8px 10px}',
776
+ '.gho-scm-bar input[type="text"]{flex:1;min-width:160px;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}',
777
+ '.gho-scm-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}',
778
+ '.gho-scm-btn[disabled]{opacity:.5;cursor:default}',
779
+ '.gho-scm-btn-warn{border-color:var(--dsw-alias-label-error,#c0392b);color:var(--dsw-alias-label-error,#c0392b)}',
780
+ '.gho-scm-actions{display:flex;gap:4px;margin-left:auto}',
781
+ '.gho-scm-check{display:flex;align-items:center;gap:4px;color:var(--dsw-alias-label-secondary)}',
782
+ '.gho-scm-error{color:var(--dsw-alias-label-error,#c0392b)}',
111
783
  '.gho-input-area{height:auto;min-height:64px;padding:8px 12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;resize:vertical}',
112
784
  '.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}',
113
785
  '.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}',
@@ -140,6 +812,26 @@ window.__ModuleLoader__.load({
140
812
  const [draft, setDraft] = React.useState(null)
141
813
  const [saving, setSaving] = React.useState(false)
142
814
  const [message, setMessage] = React.useState('')
815
+ const [accessStatus, setAccessStatus] = React.useState(null)
816
+ const [statusError, setStatusError] = React.useState('')
817
+
818
+ const loadStatus = React.useCallback(() => {
819
+ setAccessStatus('loading')
820
+ setStatusError('')
821
+ if (typeof fetch !== 'function') {
822
+ setAccessStatus(null)
823
+ setStatusError('fetch is unavailable in this client')
824
+ return
825
+ }
826
+ fetch(STATUS_PATH, { headers: { accept: 'application/json' } })
827
+ .then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
828
+ .then((payload) => setAccessStatus(payload))
829
+ .catch((err) => { setAccessStatus(null); setStatusError(String((err && err.message) || err)) })
830
+ }, [])
831
+
832
+ React.useEffect(() => {
833
+ if (open && accessStatus === null && !statusError) loadStatus()
834
+ }, [open, accessStatus, statusError, loadStatus])
143
835
 
144
836
  let scope = null
145
837
  try {
@@ -238,6 +930,44 @@ window.__ModuleLoader__.load({
238
930
  field('timeoutMs', 'timeoutMs', 'timeoutMsHint', 'number'),
239
931
  field('maxRetries', 'maxRetries', 'maxRetriesHint', 'number'),
240
932
  field('reviewRulesJson', 'reviewRules', 'reviewRulesHint', 'json'),
933
+ React.createElement('div', { className: 'gho-status' },
934
+ React.createElement('div', { className: 'gho-status-head' },
935
+ t('statusTitle'),
936
+ React.createElement('button', { type: 'button', className: 'gho-link', onClick: loadStatus }, t('statusRefresh')),
937
+ ),
938
+ accessStatus === 'loading' ? React.createElement('div', { className: 'gho-status-row' }, t('statusLoading')) : null,
939
+ statusError ? React.createElement('div', { className: 'gho-status-row gho-status-bad' }, `${t('statusError')}: ${statusError}`) : null,
940
+ accessStatus && accessStatus !== 'loading' && !accessStatus.configured
941
+ ? React.createElement('div', { className: 'gho-status-row' }, t('statusNotConfigured'))
942
+ : null,
943
+ accessStatus && accessStatus !== 'loading' && accessStatus.guidance
944
+ ? React.createElement('div', { className: 'gho-status-row' }, accessStatus.guidance)
945
+ : null,
946
+ accessStatus && accessStatus !== 'loading' && accessStatus.configured
947
+ ? React.createElement('div', null,
948
+ React.createElement('div', { className: 'gho-status-row' },
949
+ React.createElement('span', { className: 'gho-status-key' }, t('statusSource')),
950
+ React.createElement('span', { className: 'gho-status-value' }, accessStatus.source || '—'),
951
+ ),
952
+ React.createElement('div', { className: 'gho-status-row' },
953
+ React.createElement('span', { className: 'gho-status-key' }, t('statusAccount')),
954
+ React.createElement('span', { className: 'gho-status-value' }, accessStatus.login || '—'),
955
+ ),
956
+ accessStatus.scopes ? React.createElement('div', { className: 'gho-status-row' },
957
+ React.createElement('span', { className: 'gho-status-key' }, t('statusScopes')),
958
+ React.createElement('span', { className: 'gho-status-value' }, accessStatus.scopes),
959
+ ) : null,
960
+ accessStatus.rateLimit ? React.createElement('div', { className: 'gho-status-row' },
961
+ React.createElement('span', { className: 'gho-status-key' }, t('statusRate')),
962
+ React.createElement('span', { className: 'gho-status-value' }, `${accessStatus.rateLimit.remaining ?? '?'}/${accessStatus.rateLimit.limit ?? '?'}${accessStatus.rateLimit.resetAt ? ` · ${accessStatus.rateLimit.resetAt}` : ''}`),
963
+ ) : null,
964
+ accessStatus.cache ? React.createElement('div', { className: 'gho-status-row' },
965
+ React.createElement('span', { className: 'gho-status-key' }, t('statusCache')),
966
+ React.createElement('span', { className: 'gho-status-value' }, `${accessStatus.cache.size} · ${accessStatus.cache.ttlMs} ms`),
967
+ ) : null,
968
+ )
969
+ : null,
970
+ ),
241
971
  React.createElement('div', { className: 'gho-foot' },
242
972
  React.createElement('span', { className: 'gho-note' }, message),
243
973
  React.createElement('button', {
@@ -296,9 +1026,81 @@ window.__ModuleLoader__.load({
296
1026
  }
297
1027
  }
298
1028
 
1029
+ // The panel goes into the sidebar: better-sidebar first (its contract is explicit), and
1030
+ // the built-in right sidebar through its tab registry. Both are feature-detected, so a
1031
+ // composition without either still loads the plugin.
1032
+ try {
1033
+ ctx.inject(['betterSidebar'], (sctx) => {
1034
+ try {
1035
+ sctx.effect(() => sctx.betterSidebar.registerTab({
1036
+ id: 'github-ops:panel',
1037
+ title: 'GitHub',
1038
+ component: (viewProps) => React.createElement(GitHubPanel, { t: ctx.locale.bind(NS), scope: viewProps && viewProps.scope }),
1039
+ }), 'dsh-github-ops: better-sidebar panel')
1040
+ } catch {
1041
+ /* an older better-sidebar without registerTab */
1042
+ }
1043
+ })
1044
+ } catch {
1045
+ /* no better-sidebar in this composition */
1046
+ }
1047
+ try {
1048
+ ctx.inject(['sidebarRightTabs'], (sctx) => {
1049
+ try {
1050
+ sctx.effect(() => sctx.sidebarRightTabs.register({
1051
+ id: 'github-ops:panel',
1052
+ kind: 'github',
1053
+ patterns: ['dsh-resource://github/**'],
1054
+ priority: 'builtin',
1055
+ canOpen: (address) => String(address || '').startsWith('dsh-resource://github/'),
1056
+ title: () => 'GitHub',
1057
+ }), 'dsh-github-ops: sidebar tab')
1058
+ } catch {
1059
+ /* a core without the right-sidebar tab registry */
1060
+ }
1061
+ })
1062
+ } catch {
1063
+ /* no built-in right sidebar in this composition */
1064
+ }
1065
+ // The pull request bar above the composer: a suggestion, never a write.
1066
+ try {
1067
+ ctx.slots.inject('conversation.composer.bar', () => ctx.slots.register(
1068
+ { name: 'conversation.composer.bar', locale: NS, inject: () => ({ t: ctx.locale.bind(NS) }) },
1069
+ PullRequestBar,
1070
+ ))
1071
+ } catch {
1072
+ /* a core without the composer bar: the rest of the plugin still works */
1073
+ }
1074
+ installLinkWatcher()
1075
+ // The Source Control tab lives in the session view ring, the same seat the Memory tab
1076
+ // of dsh-memory-meter uses: a session-scoped view, not a navigation of our own.
1077
+ try {
1078
+ ctx.slots.inject('conversation.view', () => ctx.slots.register(
1079
+ {
1080
+ name: 'conversation.view',
1081
+ id: 'github-scm',
1082
+ order: 40,
1083
+ label: () => 'Source control',
1084
+ locale: NS,
1085
+ inject: () => ({ t: ctx.locale.bind(NS) }),
1086
+ },
1087
+ SourceControlView,
1088
+ ))
1089
+ } catch {
1090
+ /* a core without the session view ring: the rest of the plugin still works */
1091
+ }
1092
+
299
1093
  exports.inject = ['slots', 'locale', 'settingsScope']
300
1094
  exports.apply = apply
301
1095
  exports.GitHubOpsCard = GitHubOpsCard
1096
+ exports.SourceControlView = SourceControlView
1097
+ exports.SCM_ACTIONS = [
1098
+ 'stage', 'unstage', 'discard', 'commit', 'push', 'sync',
1099
+ 'branchCreate', 'branchCheckout', 'branchDelete',
1100
+ 'stashPush', 'stashApply', 'stashPop', 'stashDrop', 'continue', 'abort',
1101
+ ]
1102
+ exports.PullRequestBar = PullRequestBar
1103
+ exports.GitHubPanel = GitHubPanel
302
1104
  return module.exports
303
1105
  },
304
1106
  })