dsh-git-changes 0.0.0-stage → 0.1.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.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +105 -0
  3. package/README.md +122 -2
  4. package/THIRD_PARTY_NOTICES.md +12 -0
  5. package/assets/vendor/monaco/LICENSE +21 -0
  6. package/assets/vendor/monaco/ThirdPartyNotices.txt +448 -0
  7. package/assets/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  8. package/assets/vendor/monaco/vs/base/common/worker/simpleWorker.nls.js +7 -0
  9. package/assets/vendor/monaco/vs/base/common/worker/simpleWorker.nls.zh-cn.js +7 -0
  10. package/assets/vendor/monaco/vs/base/worker/workerMain.js +26 -0
  11. package/assets/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  12. package/assets/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  13. package/assets/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  14. package/assets/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  15. package/assets/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  16. package/assets/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  17. package/assets/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  18. package/assets/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  19. package/assets/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  20. package/assets/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  21. package/assets/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  22. package/assets/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  23. package/assets/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  24. package/assets/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  25. package/assets/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  26. package/assets/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  27. package/assets/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  28. package/assets/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  29. package/assets/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  30. package/assets/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  31. package/assets/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  32. package/assets/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  33. package/assets/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  34. package/assets/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  35. package/assets/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  36. package/assets/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  37. package/assets/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  38. package/assets/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  39. package/assets/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  40. package/assets/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  41. package/assets/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  42. package/assets/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  43. package/assets/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  44. package/assets/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  45. package/assets/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  46. package/assets/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  47. package/assets/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  48. package/assets/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  49. package/assets/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  50. package/assets/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  51. package/assets/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  52. package/assets/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  53. package/assets/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  54. package/assets/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  55. package/assets/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  56. package/assets/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  57. package/assets/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  58. package/assets/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  59. package/assets/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  60. package/assets/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  61. package/assets/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  62. package/assets/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  63. package/assets/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  64. package/assets/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  65. package/assets/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  66. package/assets/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  67. package/assets/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  68. package/assets/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  69. package/assets/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  70. package/assets/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  71. package/assets/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  72. package/assets/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  73. package/assets/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  74. package/assets/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  75. package/assets/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  76. package/assets/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  77. package/assets/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  78. package/assets/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  79. package/assets/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  80. package/assets/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  81. package/assets/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  82. package/assets/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  83. package/assets/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  84. package/assets/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  85. package/assets/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  86. package/assets/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  87. package/assets/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  88. package/assets/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  89. package/assets/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  90. package/assets/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  91. package/assets/vendor/monaco/vs/editor/editor.main.css +6 -0
  92. package/assets/vendor/monaco/vs/editor/editor.main.js +744 -0
  93. package/assets/vendor/monaco/vs/editor/editor.main.nls.js +28 -0
  94. package/assets/vendor/monaco/vs/editor/editor.main.nls.zh-cn.js +30 -0
  95. package/assets/vendor/monaco/vs/language/css/cssMode.js +13 -0
  96. package/assets/vendor/monaco/vs/language/css/cssWorker.js +81 -0
  97. package/assets/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  98. package/assets/vendor/monaco/vs/language/html/htmlWorker.js +453 -0
  99. package/assets/vendor/monaco/vs/language/json/jsonMode.js +15 -0
  100. package/assets/vendor/monaco/vs/language/json/jsonWorker.js +36 -0
  101. package/assets/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  102. package/assets/vendor/monaco/vs/language/typescript/tsWorker.js +37016 -0
  103. package/assets/vendor/monaco/vs/loader.js +10 -0
  104. package/cordis.patch.yml +3 -0
  105. package/docs/images/git-changes-diff.png +0 -0
  106. package/icon.svg +10 -0
  107. package/lib/client.js +9418 -0
  108. package/lib/host/assets.js +161 -0
  109. package/lib/host/commit-message.js +158 -0
  110. package/lib/host/files-index.js +206 -0
  111. package/lib/host/git-write.js +194 -0
  112. package/lib/host/git.js +1147 -0
  113. package/lib/host/lsp.js +842 -0
  114. package/lib/host/routes.js +976 -0
  115. package/lib/host/session.js +84 -0
  116. package/lib/host/util.js +107 -0
  117. package/lib/index.js +33 -0
  118. package/licenses/dsh-vscode-mode-LICENSE +21 -0
  119. package/licenses/vscode-theme-seti-LICENSE +21 -0
  120. package/licenses/vscode-theme-seti-ThirdPartyNotices.txt +32 -0
  121. package/locale/en.json +6 -0
  122. package/locale/zh.json +6 -0
  123. package/package.json +63 -3
  124. package/src/client/00-const.js +79 -0
  125. package/src/client/05-utils.js +277 -0
  126. package/src/client/10-i18n.js +548 -0
  127. package/src/client/12-vscode-icons.js +460 -0
  128. package/src/client/15-theme.js +509 -0
  129. package/src/client/20-monaco.js +1404 -0
  130. package/src/client/22-lsp.js +592 -0
  131. package/src/client/25-tree.js +563 -0
  132. package/src/client/28-changes.js +1163 -0
  133. package/src/client/30-page.js +2259 -0
  134. package/src/client/35-pill.js +185 -0
  135. package/src/client/40-commands.js +248 -0
  136. package/src/client/45-css.js +357 -0
  137. package/src/client/47-pickers.js +457 -0
  138. package/src/client/50-apply.js +207 -0
  139. package/src/client/99-footer.js +62 -0
@@ -0,0 +1,185 @@
1
+ /* dsh-git-changes client half — composer pill ("本轮修改"). */
2
+
3
+ /**
4
+ * Composer-dock entry: shows the latest turn's changed-file count and opens the
5
+ * review tab. Refreshes when the running turn settles, when the conversation
6
+ * binding closes its open turn, and when the window regains focus (no polling).
7
+ * @param {object} props slot props (`sessionId`, owner props `session`)
8
+ * @returns {any} React element or null when there is nothing to show
9
+ */
10
+ function ChangesPill(props) {
11
+ const { sessionId, session, t } = props
12
+ const running = session && typeof session.running === 'boolean' ? session.running : undefined
13
+ const [latest, setLatest] = React.useState(null)
14
+ const [error, setError] = React.useState(null)
15
+ const tokenRef = React.useRef(0)
16
+ const lastLoadRef = React.useRef(0)
17
+ const retryTimerRef = React.useRef(null)
18
+
19
+ const load = React.useCallback(async (options) => {
20
+ if (!sessionId) return
21
+ const token = ++tokenRef.current
22
+ lastLoadRef.current = Date.now()
23
+ try {
24
+ const value = await apiGet('latest', { sessionId })
25
+ if (token !== tokenRef.current) return
26
+ setLatest(value || { available: false })
27
+ // A turn end and the workspace/changes append can race by a moment: retry once.
28
+ if (options && options.retry === true && value && value.available === false && value.reason === 'no-changes') {
29
+ if (retryTimerRef.current) clearTimeout(retryTimerRef.current)
30
+ retryTimerRef.current = setTimeout(() => {
31
+ retryTimerRef.current = null
32
+ void load()
33
+ }, 900)
34
+ }
35
+ } catch (failure) {
36
+ if (token !== tokenRef.current) return
37
+ setLatest({ available: false })
38
+ }
39
+ }, [sessionId])
40
+
41
+ React.useEffect(() => {
42
+ void load()
43
+ }, [load])
44
+
45
+ React.useEffect(() => {
46
+ if (running === false) void load({ retry: true })
47
+ }, [running, load])
48
+
49
+ // Session snapshot signature changes at turn boundaries; throttle to at most
50
+ // one refresh per 1.2s so stream-driven re-renders cannot spam the route.
51
+ const sessionSignature = session
52
+ ? [running, session.turn, session.seq, session.pendingInteraction === undefined ? '' : 'interactive'].join('|')
53
+ : ''
54
+ React.useEffect(() => {
55
+ if (Date.now() - lastLoadRef.current < 1200) return
56
+ void load({ retry: true })
57
+ }, [sessionSignature, load])
58
+
59
+ // Precise turn-close signal: the conversation binding's `openTurn` becomes
60
+ // undefined when the turn closes (after workspace/changes was appended).
61
+ React.useEffect(() => {
62
+ if (!sessionId) return undefined
63
+ let dispose
64
+ try {
65
+ const conversation = runtimeState.conversation
66
+ const binding = conversation && typeof conversation.binding === 'function' ? conversation.binding(sessionId) : undefined
67
+ const source = binding && binding.openTurn
68
+ if (source && typeof source.subscribe === 'function' && typeof source.getSnapshot === 'function') {
69
+ let previous = source.getSnapshot()
70
+ const listener = () => {
71
+ const next = source.getSnapshot()
72
+ if (previous !== undefined && (next === undefined || next !== previous)) void load({ retry: true })
73
+ else if (next !== previous) void load()
74
+ previous = next
75
+ }
76
+ dispose = source.subscribe(listener)
77
+ }
78
+ } catch (failure) {
79
+ /* session binding not ready: the running/signature fallbacks still refresh */
80
+ }
81
+ return () => {
82
+ try {
83
+ if (typeof dispose === 'function') dispose()
84
+ } catch (failure) { /* ignore */ }
85
+ if (retryTimerRef.current) {
86
+ clearTimeout(retryTimerRef.current)
87
+ retryTimerRef.current = null
88
+ }
89
+ }
90
+ }, [sessionId, load])
91
+
92
+ // Returning focus/visibility to the window also refreshes (covers a reload or
93
+ // a missed event while the page was hidden).
94
+ React.useEffect(() => {
95
+ const refresh = () => { void load() }
96
+ const onVisibility = () => { if (!document.hidden) refresh() }
97
+ window.addEventListener('focus', refresh)
98
+ document.addEventListener('visibilitychange', onVisibility)
99
+ return () => {
100
+ window.removeEventListener('focus', refresh)
101
+ document.removeEventListener('visibilitychange', onVisibility)
102
+ }
103
+ }, [load])
104
+
105
+ // Remember the session for the global "open review" command.
106
+ React.useEffect(() => {
107
+ if (sessionId) runtimeState.sessionId = sessionId
108
+ }, [sessionId])
109
+
110
+ if (error) {
111
+ return h('div', { className: 'drp-pill-row' }, h('div', { className: 'drp-pill-error' }, error))
112
+ }
113
+ // While the first read is in flight, render nothing (avoids a flicker).
114
+ if (!latest) return null
115
+
116
+ /** Open (or focus) the review tab for this session. */
117
+ const onOpen = () => {
118
+ runtimeState.sessionId = sessionId
119
+ const address = ADDRESS_PREFIX + encodeURIComponent(sessionId)
120
+ const ok = openResourceSafe(runtimeState.right, address)
121
+ if (!ok) {
122
+ setError(t('error.sidebarUnavailable'))
123
+ return
124
+ }
125
+ setError(null)
126
+ }
127
+
128
+ if (latest.available !== true) {
129
+ // No TURN data (no changes, or the Host restarted): keep a compact, clickable
130
+ // entry so the review page (and its diagnostics) stays reachable.
131
+ const text = latest.reason === 'no-changes' || latest.reason === 'summary-unavailable'
132
+ ? t('pill.none')
133
+ : t('pill.unavailable')
134
+ return h(
135
+ 'div',
136
+ { className: 'drp-pill-row' },
137
+ h(
138
+ 'button',
139
+ {
140
+ type: 'button',
141
+ className: 'drp-pill',
142
+ style: { opacity: 0.82 },
143
+ title: t('pill.tooltip') + (latest.reason ? '\n' + String(latest.reason) : ''),
144
+ onClick: onOpen,
145
+ },
146
+ h('span', { className: 'drp-pill-icon', 'aria-hidden': true }, '↯'),
147
+ h('span', { className: 'drp-pill-text' }, h('span', { className: 'drp-pill-count' }, text)),
148
+ h('span', { className: 'drp-pill-open' }, t('tab.title'), ' ›'),
149
+ ),
150
+ )
151
+ }
152
+
153
+ const summary = latest.summary
154
+ const files = summary && Array.isArray(summary.files) ? summary.files : []
155
+ if (files.length === 0) return null
156
+
157
+ const tooltip = files.slice(0, 10).map((file) => file.display || file.path).join('\n')
158
+
159
+ return h(
160
+ 'div',
161
+ { className: 'drp-pill-row' },
162
+ h(
163
+ 'button',
164
+ {
165
+ type: 'button',
166
+ className: 'drp-pill',
167
+ title: (tooltip ? tooltip + '\n' : '') + t('pill.tooltip'),
168
+ onClick: onOpen,
169
+ },
170
+ h('span', { className: 'drp-pill-icon', 'aria-hidden': true }, '↯'),
171
+ h(
172
+ 'span',
173
+ { className: 'drp-pill-text' },
174
+ h('span', { className: 'drp-pill-count' }, t('pill.changes', { count: String(files.length) })),
175
+ h(
176
+ 'span',
177
+ { className: 'drp-pill-stats' },
178
+ h('span', { className: 'drp-badge-add' }, '+' + fmtInt(summary.added)),
179
+ h('span', { className: 'drp-badge-del' }, '−' + fmtInt(summary.deleted)),
180
+ ),
181
+ ),
182
+ h('span', { className: 'drp-pill-open' }, t('pill.open'), ' ›'),
183
+ ),
184
+ )
185
+ }
@@ -0,0 +1,248 @@
1
+ /* dsh-git-changes client half — official shortcuts registration and command dispatch. */
2
+
3
+ /**
4
+ * Build one official shortcut binding.
5
+ * @param {string} code physical key code
6
+ * @param {string[]} [modifiers] logical modifiers
7
+ * @returns {{code:string, modifiers:string[]}}
8
+ */
9
+ function bindingOf(code, modifiers) {
10
+ return { code, modifiers: modifiers || [] }
11
+ }
12
+
13
+ /**
14
+ * Human-readable label for one shortcut binding in the active client profile.
15
+ * @param {{code:string, modifiers:string[]}|undefined} binding binding
16
+ * @returns {string}
17
+ */
18
+ function formatBinding(binding, environment = shortcutEnvironment()) {
19
+ if (!binding || !binding.code) return ''
20
+ const parts = (binding.modifiers || []).map((modifier) => shortcutModifierLabel(modifier, environment))
21
+ const keyOf = (code) => String(code)
22
+ .replace(/^Key/, '')
23
+ .replace(/^Digit/, '')
24
+ .replace(/^Arrow/, '')
25
+ const key = keyOf(binding.code) + (binding.secondCode ? '+' + keyOf(binding.secondCode) : '')
26
+ return parts.concat(key).join('+')
27
+ }
28
+
29
+ /**
30
+ * Effective registration outcome per plugin command id: `default` when the
31
+ * declared key combination was accepted, `unbound` when the product rejected
32
+ * it (the command is still registered but without a key), `failed` when even
33
+ * the unbound registration failed. The in-page shortcut panel reports this so
34
+ * a rejected default is visible instead of silently doing nothing.
35
+ */
36
+ const SHORTCUT_STATUS = new Map()
37
+
38
+ /**
39
+ * Shortcut rows for the in-page "快捷键" panel: the plugin command defaults
40
+ * plus the editor-native keys this read-only view supports.
41
+ * @param {(key:string) => string} t translator
42
+ * @returns {Array<{id:string, label:() => string, hint:string}>}
43
+ */
44
+ function buildKeyHelp(t) {
45
+ const rows = []
46
+ const environment = shortcutEnvironment()
47
+ const profile = environment.runtime + ':' + environment.platform
48
+ const primary = shortcutModifierLabel('primary', environment)
49
+ const foldModifier = environment.platform === 'macos' ? shortcutModifierLabel('alt', environment) : 'Shift'
50
+ const quickOpenBinding = COMMAND_SPECS.find((spec) => spec.id === 'quickOpen')?.defaults?.[profile]
51
+ || COMMAND_SPECS.find((spec) => spec.id === 'quickOpenAlt')?.defaults?.[profile]
52
+ const quickOpenLabel = formatBinding(quickOpenBinding, environment) || primary + '+P'
53
+ for (const spec of COMMAND_SPECS) {
54
+ const label = formatBinding(spec.defaults && spec.defaults[profile], environment)
55
+ if (!label) continue
56
+ const status = SHORTCUT_STATUS.get(spec.id)
57
+ const suffix = status === 'unbound' ? t('keys.unbound') : status === 'failed' ? t('keys.failed') : ''
58
+ rows.push({ id: 'k:' + spec.id, label: () => t(spec.message), hint: label + suffix })
59
+ }
60
+ rows.push(
61
+ { id: 'k:find', label: () => '编辑器内查找', hint: primary + '+F' },
62
+ { id: 'k:replace', label: () => '查找下一个', hint: primary + '+F → Enter' },
63
+ { id: 'k:fold', label: () => '折叠 / 展开代码块', hint: primary + '+' + foldModifier + '+[ / ]' },
64
+ { id: 'k:foldAll', label: () => '全部折叠 / 展开', hint: primary + '+K ' + primary + '+0 / ' + primary + '+K ' + primary + '+J' },
65
+ { id: 'k:fontZoom', label: () => '字号增大 / 减小 / 重置', hint: 'Ctrl+滚轮 · ' + primary + '+= / - / 0' },
66
+ { id: 'k:lspDefinition', label: () => '跳转到定义', hint: 'F12' },
67
+ { id: 'k:lspReferences', label: () => '查找引用', hint: 'Shift+F12' },
68
+ { id: 'k:lspHover', label: () => '悬停提示(类型/文档)', hint: '鼠标悬停' },
69
+ { id: 'k:quickOpenCommands', label: () => '快速打开:输入 > 运行命令', hint: quickOpenLabel + ' 后输入 >' },
70
+ { id: 'k:quickOpenLine', label: () => '快速打开:输入 : 跳转行', hint: quickOpenLabel + ' 后输入 :' },
71
+ )
72
+ return rows
73
+ }
74
+
75
+ /**
76
+ * Command catalogue: single data source for official shortcuts (and later the
77
+ * command palette). Linux declares allowed letter combinations; its reserved
78
+ * function/arrow/tab keys and Web profiles stay available through buttons,
79
+ * in-page fallback handlers, or explicit bindings in the official settings.
80
+ */
81
+ const COMMAND_SPECS = [
82
+ { id: 'openReview', message: 'cmd.openReview', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyD', ['primary', 'alt']), 'desktop:macos': bindingOf('KeyD', ['primary', 'alt']), 'desktop:linux': bindingOf('KeyD', ['primary', 'alt']) } },
83
+ { id: 'nextChange', message: 'cmd.nextChange', regions: ['page', 'editable', 'terminal'], defaults: { 'desktop:windows': bindingOf('F7'), 'desktop:macos': bindingOf('F7') } },
84
+ { id: 'prevChange', message: 'cmd.prevChange', regions: ['page', 'editable', 'terminal'], defaults: { 'desktop:windows': bindingOf('F7', ['shift']), 'desktop:macos': bindingOf('F7', ['shift']) } },
85
+ { id: 'nextTab', message: 'cmd.nextTab', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('Tab', ['primary']), 'desktop:macos': bindingOf('Tab', ['primary']) } },
86
+ { id: 'prevTab', message: 'cmd.prevTab', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('Tab', ['primary', 'shift']), 'desktop:macos': bindingOf('Tab', ['primary', 'shift']) } },
87
+ { id: 'closeTab', message: 'cmd.closeTab', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('F4', ['primary']), 'desktop:macos': bindingOf('F4', ['primary']) } },
88
+ { id: 'navBack', message: 'cmd.navBack', regions: ['page'], defaults: {} },
89
+ { id: 'navForward', message: 'cmd.navForward', regions: ['page'], defaults: {} },
90
+ { id: 'toggleTree', message: 'cmd.toggleTree', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyE', ['primary', 'shift']), 'desktop:macos': bindingOf('KeyE', ['primary', 'shift']), 'desktop:linux': bindingOf('KeyE', ['primary', 'shift']) } },
91
+ { id: 'refresh', message: 'cmd.refresh', regions: ['page', 'editable'], defaults: {} },
92
+ { id: 'copyPath', message: 'cmd.copyPath', regions: ['page', 'editable'], defaults: {} },
93
+ { id: 'openInPreview', message: 'cmd.openInPreview', regions: ['page', 'editable'], defaults: {} },
94
+ // VS Code–style pickers. Primary keys follow VS Code (Mod+P quick open,
95
+ // Mod+Shift+P command palette, Mod+Shift+F search, Mod+G goto line); the
96
+ // Mod+Alt+P variant stays as a fallback when the product owns Mod+P.
97
+ // `editable` is included so the keys also work while the Monaco editor has
98
+ // focus (none of these chords is a Monaco-native binding).
99
+ { id: 'outline', message: 'cmd.outline', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyO', ['primary', 'shift']), 'desktop:macos': bindingOf('KeyO', ['primary', 'shift']), 'desktop:linux': bindingOf('KeyO', ['primary', 'shift']) } },
100
+ { id: 'lspRecheck', message: 'lsp.recheck', regions: ['page', 'editable'], defaults: {} },
101
+ { id: 'quickOpen', message: 'cmd.quickOpen', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyP', ['primary']), 'desktop:macos': bindingOf('KeyP', ['primary']) } },
102
+ { id: 'quickOpenAlt', message: 'cmd.quickOpen', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyP', ['primary', 'alt']), 'desktop:macos': bindingOf('KeyP', ['primary', 'alt']), 'desktop:linux': bindingOf('KeyP', ['primary', 'alt']) } },
103
+ { id: 'gotoLine', message: 'cmd.gotoLine', regions: ['page'], defaults: { 'desktop:windows': bindingOf('KeyG', ['primary']), 'desktop:macos': bindingOf('KeyG', ['primary']), 'desktop:linux': bindingOf('KeyG', ['primary']) } },
104
+ { id: 'searchWorkspace', message: 'cmd.search', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyF', ['primary', 'shift']), 'desktop:macos': bindingOf('KeyF', ['primary', 'shift']), 'desktop:linux': bindingOf('KeyF', ['primary', 'shift']) } },
105
+ { id: 'commandPalette', message: 'cmd.commands', regions: ['page', 'editable'], defaults: { 'desktop:windows': bindingOf('KeyP', ['primary', 'shift']), 'desktop:macos': bindingOf('KeyP', ['primary', 'shift']), 'desktop:linux': bindingOf('KeyP', ['primary', 'shift']) } },
106
+ ]
107
+
108
+ /**
109
+ * Run one plugin command against the mounted page, or the global open action.
110
+ * @param {string} id command id (without the plugin prefix)
111
+ * @returns {boolean} whether something ran
112
+ */
113
+ function runCommand(id) {
114
+ if (id === 'openReview') return openReviewFromChat()
115
+ const page = runtimeState.page
116
+ if (!page) return false
117
+ return page.command(id)
118
+ }
119
+
120
+ /**
121
+ * Open the review tab for the last known session (global command).
122
+ * @returns {boolean} whether the open call was issued
123
+ */
124
+ function openReviewFromChat() {
125
+ const sessionId = runtimeState.sessionId
126
+ if (!sessionId) return false
127
+ const ok = openResourceSafe(runtimeState.right, ADDRESS_PREFIX + encodeURIComponent(sessionId))
128
+ if (!ok) log('warn', 'openReview: sidebarRight unavailable')
129
+ return ok
130
+ }
131
+
132
+ /**
133
+ * Command-specific visibility rules: only claim a key when this plugin's page
134
+ * (or the global open action) is the right target.
135
+ * @param {string} id command id
136
+ * @param {any} context official shortcut invoke context
137
+ * @returns {{status:'handled',run:Function}|{status:'pass'}}
138
+ */
139
+ function resolveCommand(id, context) {
140
+ if (context && context.modal !== null && context.modal !== undefined) return { status: 'pass' }
141
+ if (id === 'openReview') {
142
+ if (!runtimeState.sessionId) return { status: 'pass' }
143
+ return { status: 'handled', run: () => { runCommand('openReview') } }
144
+ }
145
+ const page = runtimeState.page
146
+ if (!page) return { status: 'pass' }
147
+ const target = context && context.target
148
+ if (target && !page.contains(target)) return { status: 'pass' }
149
+ return { status: 'handled', run: () => { runCommand(id) } }
150
+ }
151
+
152
+ /**
153
+ * Register one command, downgrading to an unbound command when its default
154
+ * combination is rejected (duplicate id / reserved / conflicting defaults).
155
+ * @param {any} service official shortcuts service
156
+ * @param {object} spec command spec
157
+ * @param {Function} t locale-bound translator
158
+ * @returns {() => void} disposer
159
+ */
160
+ function safeRegisterCommand(service, spec, t) {
161
+ const command = {
162
+ id: 'dsh-git-changes.' + spec.id,
163
+ label: () => t(spec.message),
164
+ aliases: [],
165
+ defaults: spec.defaults,
166
+ regions: spec.regions,
167
+ modals: [],
168
+ resolve: (context) => resolveCommand(spec.id, context),
169
+ }
170
+ try {
171
+ const dispose = service.register(command)
172
+ SHORTCUT_STATUS.set(spec.id, 'default')
173
+ return dispose
174
+ } catch (error) {
175
+ log('warn', 'shortcut default rejected, registering unbound:', command.id, String(error))
176
+ try {
177
+ const dispose = service.register(Object.assign({}, command, { defaults: {} }))
178
+ SHORTCUT_STATUS.set(spec.id, 'unbound')
179
+ return dispose
180
+ } catch (retryError) {
181
+ log('warn', 'shortcut registration failed:', command.id, String(retryError))
182
+ SHORTCUT_STATUS.set(spec.id, 'failed')
183
+ return () => {}
184
+ }
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Detect the optional official shortcuts service with a bounded retry, then
190
+ * register the whole catalog exactly once.
191
+ * @param {object} ctx client plugin context
192
+ * @param {Function} t locale-bound translator
193
+ * @returns {() => void} cancel/dispose
194
+ */
195
+ function installOfficialShortcuts(ctx, t) {
196
+ let stopped = false
197
+ let timer = null
198
+ let attempts = 0
199
+ let registered = []
200
+
201
+ const register = (service) => {
202
+ if (stopped) return
203
+ for (const spec of COMMAND_SPECS) {
204
+ try {
205
+ const dispose = safeRegisterCommand(service, spec, t)
206
+ registered.push(dispose)
207
+ } catch (error) {
208
+ log('warn', 'shortcut setup failed', spec.id, String(error))
209
+ }
210
+ }
211
+ log('log', 'official shortcuts registered:', COMMAND_SPECS.length)
212
+ }
213
+
214
+ const timerTick = () => {
215
+ if (stopped) return
216
+ let service
217
+ try {
218
+ service = safeGet(ctx, 'shortcuts')
219
+ } catch (error) {
220
+ service = undefined
221
+ }
222
+ if (service && typeof service.register === 'function' && service.catalog && service.config) {
223
+ register(service)
224
+ return
225
+ }
226
+ if (attempts >= 15) {
227
+ log('warn', 'official shortcuts service unavailable; plugin commands stay unbound')
228
+ return
229
+ }
230
+ attempts += 1
231
+ timer = setTimeout(timerTick, 2000)
232
+ }
233
+ timerTick()
234
+
235
+ return () => {
236
+ stopped = true
237
+ if (timer) {
238
+ clearTimeout(timer)
239
+ timer = null
240
+ }
241
+ for (const dispose of registered) {
242
+ try {
243
+ dispose()
244
+ } catch (error) { /* ignore */ }
245
+ }
246
+ registered = []
247
+ }
248
+ }