finch-git-branch 0.2.10 → 0.3.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 (207) hide show
  1. package/README.md +10 -5
  2. package/dist/index.js +23 -7
  3. package/dist/jetbrains-icons/CMake_auto.svg +1 -0
  4. package/dist/jetbrains-icons/LICENSE.md +192 -0
  5. package/dist/jetbrains-icons/actionScript_auto.svg +1 -0
  6. package/dist/jetbrains-icons/angularJS_auto.svg +1 -0
  7. package/dist/jetbrains-icons/anyType_auto.svg +1 -0
  8. package/dist/jetbrains-icons/application_auto.svg +1 -0
  9. package/dist/jetbrains-icons/archive.svg +1 -0
  10. package/dist/jetbrains-icons/archive_auto.svg +1 -0
  11. package/dist/jetbrains-icons/asm.svg +1 -0
  12. package/dist/jetbrains-icons/bbl.svg +1 -0
  13. package/dist/jetbrains-icons/beam_auto.svg +1 -0
  14. package/dist/jetbrains-icons/bib.svg +1 -0
  15. package/dist/jetbrains-icons/binary.svg +1 -0
  16. package/dist/jetbrains-icons/biomejs_auto.svg +1 -0
  17. package/dist/jetbrains-icons/blade.svg +1 -0
  18. package/dist/jetbrains-icons/bun_auto.svg +1 -0
  19. package/dist/jetbrains-icons/c.svg +1 -0
  20. package/dist/jetbrains-icons/c3Interface_auto.svg +1 -0
  21. package/dist/jetbrains-icons/c3Library_auto.svg +1 -0
  22. package/dist/jetbrains-icons/c3Test_auto.svg +1 -0
  23. package/dist/jetbrains-icons/c3_auto.svg +1 -0
  24. package/dist/jetbrains-icons/c_auto.svg +1 -0
  25. package/dist/jetbrains-icons/cargoLock_auto.svg +1 -0
  26. package/dist/jetbrains-icons/cargo_auto.svg +1 -0
  27. package/dist/jetbrains-icons/clojure_auto.svg +1 -0
  28. package/dist/jetbrains-icons/cls.svg +1 -0
  29. package/dist/jetbrains-icons/cmake.svg +1 -0
  30. package/dist/jetbrains-icons/coffeescript.svg +1 -0
  31. package/dist/jetbrains-icons/config_auto.svg +1 -0
  32. package/dist/jetbrains-icons/cpp.svg +1 -0
  33. package/dist/jetbrains-icons/cpp_auto.svg +1 -0
  34. package/dist/jetbrains-icons/cs_auto.svg +1 -0
  35. package/dist/jetbrains-icons/cshtml_auto.svg +1 -0
  36. package/dist/jetbrains-icons/csproj_auto.svg +1 -0
  37. package/dist/jetbrains-icons/css.svg +1 -0
  38. package/dist/jetbrains-icons/css_auto.svg +1 -0
  39. package/dist/jetbrains-icons/csv.svg +1 -0
  40. package/dist/jetbrains-icons/csv_auto.svg +1 -0
  41. package/dist/jetbrains-icons/cu_auto.svg +1 -0
  42. package/dist/jetbrains-icons/cuda.svg +1 -0
  43. package/dist/jetbrains-icons/cuda_header.svg +1 -0
  44. package/dist/jetbrains-icons/cuh_auto.svg +1 -0
  45. package/dist/jetbrains-icons/dart.svg +1 -0
  46. package/dist/jetbrains-icons/dart_auto.svg +1 -0
  47. package/dist/jetbrains-icons/dbms_auto.svg +1 -0
  48. package/dist/jetbrains-icons/docker_auto.svg +1 -0
  49. package/dist/jetbrains-icons/dockercompose.svg +1 -0
  50. package/dist/jetbrains-icons/dune_auto.svg +1 -0
  51. package/dist/jetbrains-icons/editorConfig_auto.svg +1 -0
  52. package/dist/jetbrains-icons/editorconfig.svg +1 -0
  53. package/dist/jetbrains-icons/eex_auto.svg +1 -0
  54. package/dist/jetbrains-icons/elixir_auto.svg +1 -0
  55. package/dist/jetbrains-icons/erb.svg +1 -0
  56. package/dist/jetbrains-icons/erbFile_auto.svg +1 -0
  57. package/dist/jetbrains-icons/erlang_auto.svg +1 -0
  58. package/dist/jetbrains-icons/eslint.svg +1 -0
  59. package/dist/jetbrains-icons/eslint_auto.svg +1 -0
  60. package/dist/jetbrains-icons/file.svg +1 -0
  61. package/dist/jetbrains-icons/file_dockerfile.svg +1 -0
  62. package/dist/jetbrains-icons/folder.svg +1 -0
  63. package/dist/jetbrains-icons/folderControllers_auto.svg +1 -0
  64. package/dist/jetbrains-icons/folderGithub_auto.svg +1 -0
  65. package/dist/jetbrains-icons/folderHelpers_auto.svg +1 -0
  66. package/dist/jetbrains-icons/folderMailers_auto.svg +1 -0
  67. package/dist/jetbrains-icons/folderMigrations_auto.svg +1 -0
  68. package/dist/jetbrains-icons/folderTest_auto.svg +1 -0
  69. package/dist/jetbrains-icons/folderVendor_auto.svg +1 -0
  70. package/dist/jetbrains-icons/folder_auto.svg +1 -0
  71. package/dist/jetbrains-icons/font_auto.svg +1 -0
  72. package/dist/jetbrains-icons/gitignore_auto.svg +1 -0
  73. package/dist/jetbrains-icons/gleam_auto.svg +1 -0
  74. package/dist/jetbrains-icons/go.svg +1 -0
  75. package/dist/jetbrains-icons/goMod_auto.svg +1 -0
  76. package/dist/jetbrains-icons/goSum_auto.svg +1 -0
  77. package/dist/jetbrains-icons/goWork_auto.svg +1 -0
  78. package/dist/jetbrains-icons/go_auto.svg +1 -0
  79. package/dist/jetbrains-icons/gradle.svg +1 -0
  80. package/dist/jetbrains-icons/graphql.svg +1 -0
  81. package/dist/jetbrains-icons/graphql_auto.svg +1 -0
  82. package/dist/jetbrains-icons/groovy.svg +1 -0
  83. package/dist/jetbrains-icons/h_auto.svg +1 -0
  84. package/dist/jetbrains-icons/handlebarsJson_auto.svg +1 -0
  85. package/dist/jetbrains-icons/haskell_auto.svg +1 -0
  86. package/dist/jetbrains-icons/hcl_auto.svg +1 -0
  87. package/dist/jetbrains-icons/header.svg +1 -0
  88. package/dist/jetbrains-icons/htaccess.svg +1 -0
  89. package/dist/jetbrains-icons/html.svg +1 -0
  90. package/dist/jetbrains-icons/html_auto.svg +1 -0
  91. package/dist/jetbrains-icons/http_auto.svg +1 -0
  92. package/dist/jetbrains-icons/ignore_file.svg +1 -0
  93. package/dist/jetbrains-icons/ignored_auto.svg +1 -0
  94. package/dist/jetbrains-icons/image.svg +1 -0
  95. package/dist/jetbrains-icons/image_auto.svg +1 -0
  96. package/dist/jetbrains-icons/ino.svg +1 -0
  97. package/dist/jetbrains-icons/ino_auto.svg +1 -0
  98. package/dist/jetbrains-icons/java.svg +1 -0
  99. package/dist/jetbrains-icons/javaScript_auto.svg +1 -0
  100. package/dist/jetbrains-icons/java_auto.svg +1 -0
  101. package/dist/jetbrains-icons/java_class.svg +1 -0
  102. package/dist/jetbrains-icons/javascript.svg +1 -0
  103. package/dist/jetbrains-icons/jsTest_auto.svg +1 -0
  104. package/dist/jetbrains-icons/json.svg +1 -0
  105. package/dist/jetbrains-icons/json_auto.svg +1 -0
  106. package/dist/jetbrains-icons/jsx.svg +1 -0
  107. package/dist/jetbrains-icons/jsxTest_auto.svg +1 -0
  108. package/dist/jetbrains-icons/jsx_auto.svg +1 -0
  109. package/dist/jetbrains-icons/julia.svg +1 -0
  110. package/dist/jetbrains-icons/jupyter_auto.svg +1 -0
  111. package/dist/jetbrains-icons/kotlin.svg +1 -0
  112. package/dist/jetbrains-icons/kotlinScript_auto.svg +1 -0
  113. package/dist/jetbrains-icons/kotlin_auto.svg +1 -0
  114. package/dist/jetbrains-icons/latex_aux.svg +1 -0
  115. package/dist/jetbrains-icons/less.svg +1 -0
  116. package/dist/jetbrains-icons/less_auto.svg +1 -0
  117. package/dist/jetbrains-icons/lock.svg +1 -0
  118. package/dist/jetbrains-icons/lock_auto.svg +1 -0
  119. package/dist/jetbrains-icons/lua.svg +1 -0
  120. package/dist/jetbrains-icons/lua_auto.svg +1 -0
  121. package/dist/jetbrains-icons/makefile.svg +1 -0
  122. package/dist/jetbrains-icons/makefile_auto.svg +1 -0
  123. package/dist/jetbrains-icons/markdown.svg +1 -0
  124. package/dist/jetbrains-icons/markdown_auto.svg +1 -0
  125. package/dist/jetbrains-icons/mdx.svg +1 -0
  126. package/dist/jetbrains-icons/mdx_auto.svg +1 -0
  127. package/dist/jetbrains-icons/ml_auto.svg +1 -0
  128. package/dist/jetbrains-icons/mli_auto.svg +1 -0
  129. package/dist/jetbrains-icons/model.svg +1 -0
  130. package/dist/jetbrains-icons/module_auto.svg +1 -0
  131. package/dist/jetbrains-icons/nodejs_auto.svg +1 -0
  132. package/dist/jetbrains-icons/npm_auto.svg +1 -0
  133. package/dist/jetbrains-icons/opam_auto.svg +1 -0
  134. package/dist/jetbrains-icons/opentofu_auto.svg +1 -0
  135. package/dist/jetbrains-icons/pdf.svg +1 -0
  136. package/dist/jetbrains-icons/php.svg +1 -0
  137. package/dist/jetbrains-icons/php_auto.svg +1 -0
  138. package/dist/jetbrains-icons/pnpm_auto.svg +1 -0
  139. package/dist/jetbrains-icons/pnpm_dark.svg +1 -0
  140. package/dist/jetbrains-icons/postcss.svg +1 -0
  141. package/dist/jetbrains-icons/postcss_auto.svg +1 -0
  142. package/dist/jetbrains-icons/prettier_auto.svg +1 -0
  143. package/dist/jetbrains-icons/projectProperties_auto.svg +1 -0
  144. package/dist/jetbrains-icons/properties_auto.svg +1 -0
  145. package/dist/jetbrains-icons/protoFile_auto.svg +1 -0
  146. package/dist/jetbrains-icons/protobuf.svg +1 -0
  147. package/dist/jetbrains-icons/python.svg +1 -0
  148. package/dist/jetbrains-icons/python_auto.svg +1 -0
  149. package/dist/jetbrains-icons/rakeTask_auto.svg +1 -0
  150. package/dist/jetbrains-icons/rego_auto.svg +1 -0
  151. package/dist/jetbrains-icons/ruby.svg +1 -0
  152. package/dist/jetbrains-icons/rubyGems_auto.svg +1 -0
  153. package/dist/jetbrains-icons/ruby_auto.svg +1 -0
  154. package/dist/jetbrains-icons/rust.svg +1 -0
  155. package/dist/jetbrains-icons/rustFile_auto.svg +1 -0
  156. package/dist/jetbrains-icons/sass.svg +1 -0
  157. package/dist/jetbrains-icons/scala.svg +1 -0
  158. package/dist/jetbrains-icons/scala_auto.svg +1 -0
  159. package/dist/jetbrains-icons/scss_auto.svg +1 -0
  160. package/dist/jetbrains-icons/shell.svg +1 -0
  161. package/dist/jetbrains-icons/shell_auto.svg +1 -0
  162. package/dist/jetbrains-icons/slim_auto.svg +1 -0
  163. package/dist/jetbrains-icons/solution_auto.svg +1 -0
  164. package/dist/jetbrains-icons/sql.svg +1 -0
  165. package/dist/jetbrains-icons/sql_auto.svg +1 -0
  166. package/dist/jetbrains-icons/sty.svg +1 -0
  167. package/dist/jetbrains-icons/svelte.svg +1 -0
  168. package/dist/jetbrains-icons/svelte_auto.svg +1 -0
  169. package/dist/jetbrains-icons/swift.svg +1 -0
  170. package/dist/jetbrains-icons/swift_auto.svg +1 -0
  171. package/dist/jetbrains-icons/tailwind_auto.svg +1 -0
  172. package/dist/jetbrains-icons/terraform_auto.svg +1 -0
  173. package/dist/jetbrains-icons/test_js.svg +1 -0
  174. package/dist/jetbrains-icons/test_jsx.svg +1 -0
  175. package/dist/jetbrains-icons/test_ts.svg +1 -0
  176. package/dist/jetbrains-icons/test_tsx.svg +12 -0
  177. package/dist/jetbrains-icons/tex.svg +1 -0
  178. package/dist/jetbrains-icons/text.svg +1 -0
  179. package/dist/jetbrains-icons/text_auto.svg +1 -0
  180. package/dist/jetbrains-icons/theme.json +623 -0
  181. package/dist/jetbrains-icons/toml.svg +1 -0
  182. package/dist/jetbrains-icons/toml_auto.svg +1 -0
  183. package/dist/jetbrains-icons/tsTest_auto.svg +1 -0
  184. package/dist/jetbrains-icons/tsx.svg +1 -0
  185. package/dist/jetbrains-icons/tsxTest_auto.svg +1 -0
  186. package/dist/jetbrains-icons/tsx_auto.svg +1 -0
  187. package/dist/jetbrains-icons/twig.svg +1 -0
  188. package/dist/jetbrains-icons/typeScript_auto.svg +1 -0
  189. package/dist/jetbrains-icons/typescript.svg +1 -0
  190. package/dist/jetbrains-icons/vercel_auto.svg +1 -0
  191. package/dist/jetbrains-icons/vite_auto.svg +1 -0
  192. package/dist/jetbrains-icons/vlang_auto.svg +1 -0
  193. package/dist/jetbrains-icons/vue.svg +1 -0
  194. package/dist/jetbrains-icons/vueJs_auto.svg +1 -0
  195. package/dist/jetbrains-icons/xml.svg +1 -0
  196. package/dist/jetbrains-icons/xml_auto.svg +1 -0
  197. package/dist/jetbrains-icons/yaml.svg +1 -0
  198. package/dist/jetbrains-icons/yaml_auto.svg +1 -0
  199. package/dist/jetbrains-icons/yarn.svg +1 -0
  200. package/dist/jetbrains-icons/yarn_auto.svg +1 -0
  201. package/dist/jetbrains-icons/zig_auto.svg +1 -0
  202. package/dist/jetbrains-mono-latin.woff2 +0 -0
  203. package/dist/scm-panel.html +1227 -0
  204. package/dist/scm-panel.js +393 -0
  205. package/i18n/en-US.json +26 -0
  206. package/i18n/zh-CN.json +26 -0
  207. package/package.json +53 -4
@@ -0,0 +1,1227 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Source Control</title>
7
+ <style>
8
+ :root {
9
+ --bg: var(--finch-bg-main, #fff);
10
+ --elev: var(--finch-bg-elevated, #f7f7f8);
11
+ --hover: var(--finch-bg-hover, #eee);
12
+ --active: var(--finch-accent-dim, #f5d9e9);
13
+ --text: var(--finch-text-primary, #202124);
14
+ --muted: var(--finch-text-secondary, #6b7280);
15
+ --subtle: var(--finch-text-tertiary, #9ca3af);
16
+ --border: var(--finch-border, #e5e7eb);
17
+ --accent: var(--finch-accent, #e02a8b);
18
+ --pos: var(--finch-positive, #22a06b);
19
+ --warn: var(--finch-warning, #d97706);
20
+ --danger: var(--finch-danger, #dc2626);
21
+ --mono:
22
+ "JetBrains Mono", var(--finch-font-mono, "Cascadia Code", monospace);
23
+ }
24
+ @font-face {
25
+ font-family: "JetBrains Mono";
26
+ src: url("./jetbrains-mono-latin.woff2") format("woff2");
27
+ font-display: swap;
28
+ font-weight: 400;
29
+ font-style: normal;
30
+ }
31
+ @media (prefers-color-scheme: dark) {
32
+ :root {
33
+ --bg: #1e1e20;
34
+ --elev: #262629;
35
+ --hover: #303034;
36
+ --text: #e8e8e9;
37
+ --muted: #a3a3aa;
38
+ --border: #38383d;
39
+ }
40
+ }
41
+ * {
42
+ box-sizing: border-box;
43
+ }
44
+ html,
45
+ body {
46
+ height: 100%;
47
+ margin: 0;
48
+ overflow: hidden;
49
+ }
50
+ body {
51
+ font: 11px/1.45 var(--mono);
52
+ color: var(--text);
53
+ background: var(--bg);
54
+ }
55
+ button,
56
+ input,
57
+ textarea {
58
+ font: inherit;
59
+ }
60
+ .scm {
61
+ height: 100%;
62
+ display: flex;
63
+ flex-direction: column;
64
+ min-height: 0;
65
+ position: relative;
66
+ }
67
+ .icon {
68
+ width: 15px;
69
+ height: 15px;
70
+ stroke: currentColor;
71
+ stroke-width: 2;
72
+ fill: none;
73
+ stroke-linecap: round;
74
+ stroke-linejoin: round;
75
+ flex: none;
76
+ }
77
+ .topbar {
78
+ display: flex;
79
+ gap: 3px;
80
+ align-items: center;
81
+ padding: 6px 8px;
82
+ border-bottom: 1px solid var(--border);
83
+ background: var(--elev);
84
+ }
85
+ .topbar button,
86
+ .action {
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ gap: 5px;
91
+ border: 1px solid transparent;
92
+ background: transparent;
93
+ color: var(--muted);
94
+ border-radius: 4px;
95
+ padding: 3px 7px;
96
+ cursor: pointer;
97
+ }
98
+ .topbar button:hover,
99
+ .action:hover {
100
+ background: var(--hover);
101
+ color: var(--text);
102
+ }
103
+ .topbar button.icon-button {
104
+ padding: 4px;
105
+ }
106
+ .topbar .spacer {
107
+ flex: 1;
108
+ }
109
+ .section {
110
+ border-bottom: 1px solid var(--border);
111
+ min-height: 0;
112
+ }
113
+ .section:first-child {
114
+ flex: 0 0 auto;
115
+ }
116
+ .heading {
117
+ height: 31px;
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 7px;
121
+ padding: 6px 9px;
122
+ color: var(--muted);
123
+ font-weight: 700;
124
+ letter-spacing: 0.03em;
125
+ cursor: pointer;
126
+ user-select: none;
127
+ }
128
+ .heading:hover {
129
+ background: var(--hover);
130
+ }
131
+ .heading b {
132
+ color: var(--text);
133
+ }
134
+ .chevron {
135
+ transition: transform 0.14s;
136
+ }
137
+ .heading.collapsed .chevron {
138
+ transform: rotate(-90deg);
139
+ }
140
+ .heading .count {
141
+ margin-left: auto;
142
+ background: var(--hover);
143
+ border-radius: 9px;
144
+ padding: 0 5px;
145
+ font-size: 11px;
146
+ }
147
+ .body.collapsed {
148
+ display: none;
149
+ }
150
+ .repo,
151
+ .file {
152
+ display: flex;
153
+ align-items: center;
154
+ gap: 7px;
155
+ min-width: 0;
156
+ padding: 5px 10px;
157
+ cursor: pointer;
158
+ }
159
+ .repo:hover,
160
+ .file:hover {
161
+ background: var(--hover);
162
+ }
163
+ .repo.active {
164
+ background: var(--hover);
165
+ }
166
+ .file.active {
167
+ background: var(--active);
168
+ }
169
+ .repo.submodule {
170
+ padding-left: 28px;
171
+ position: relative;
172
+ }
173
+ .repo.submodule:before {
174
+ content: "";
175
+ position: absolute;
176
+ left: 17px;
177
+ top: 0;
178
+ bottom: 0;
179
+ border-left: 1px solid var(--border);
180
+ }
181
+ .repo.submodule:after {
182
+ content: "";
183
+ position: absolute;
184
+ left: 17px;
185
+ top: 50%;
186
+ width: 7px;
187
+ border-top: 1px solid var(--border);
188
+ }
189
+ .repo .name,
190
+ .file .path {
191
+ min-width: 0;
192
+ overflow: hidden;
193
+ text-overflow: ellipsis;
194
+ white-space: nowrap;
195
+ }
196
+ .repo .name {
197
+ flex: 1;
198
+ }
199
+ .branch {
200
+ color: var(--accent);
201
+ white-space: nowrap;
202
+ }
203
+ .sync {
204
+ color: var(--muted);
205
+ white-space: nowrap;
206
+ }
207
+ .changes-section {
208
+ display: flex;
209
+ flex: 1;
210
+ flex-direction: column;
211
+ min-height: 0;
212
+ }
213
+ .changes-section > #changes:not(.collapsed) {
214
+ display: flex;
215
+ flex: 1;
216
+ flex-direction: column;
217
+ min-height: 0;
218
+ }
219
+ .commit {
220
+ padding: 7px 9px;
221
+ border-top: 1px solid var(--border);
222
+ border-bottom: 1px solid var(--border);
223
+ }
224
+ textarea {
225
+ width: 100%;
226
+ height: 52px;
227
+ resize: vertical;
228
+ color: var(--text);
229
+ background: var(--elev);
230
+ border: 1px solid var(--border);
231
+ border-radius: 4px;
232
+ padding: 6px;
233
+ outline: none;
234
+ }
235
+ textarea:focus {
236
+ border-color: var(--accent);
237
+ }
238
+ .commit-actions {
239
+ display: flex;
240
+ gap: 5px;
241
+ }
242
+ .commit-split {
243
+ display: flex;
244
+ position: relative;
245
+ }
246
+ .commit-split .primary {
247
+ border-radius: 4px 0 0 4px;
248
+ }
249
+ .commit-menu-toggle {
250
+ padding: 3px 5px !important;
251
+ border-left: 1px solid color-mix(in srgb, #fff 35%, transparent) !important;
252
+ border-radius: 0 4px 4px 0 !important;
253
+ }
254
+ .commit-menu-toggle .icon {
255
+ width: 13px;
256
+ height: 13px;
257
+ }
258
+ .commit-menu {
259
+ display: none;
260
+ position: absolute;
261
+ z-index: 4;
262
+ left: 0;
263
+ right: auto;
264
+ top: calc(100% + 4px);
265
+ min-width: 108px;
266
+ padding: 3px;
267
+ background: var(--elev);
268
+ border: 1px solid var(--border);
269
+ border-radius: 5px;
270
+ box-shadow: 0 5px 12px #0003;
271
+ }
272
+ .commit-menu.open {
273
+ display: block;
274
+ }
275
+ .commit-menu button {
276
+ width: 100%;
277
+ padding: 4px 7px;
278
+ border: 0;
279
+ border-radius: 3px;
280
+ color: var(--text);
281
+ background: transparent;
282
+ text-align: left;
283
+ cursor: pointer;
284
+ }
285
+ .commit-menu button:hover {
286
+ background: var(--hover);
287
+ }
288
+ .primary {
289
+ color: #fff;
290
+ background: var(--accent) !important;
291
+ border-color: var(--accent) !important;
292
+ }
293
+ .file-list {
294
+ flex: 1;
295
+ min-height: 0;
296
+ overflow: auto;
297
+ }
298
+ .group {
299
+ padding: 5px 10px 2px;
300
+ color: var(--muted);
301
+ font-size: 11px;
302
+ background: var(--elev);
303
+ position: sticky;
304
+ top: 0;
305
+ z-index: 1;
306
+ }
307
+ .status {
308
+ width: 13px;
309
+ font-weight: bold;
310
+ text-align: center;
311
+ }
312
+ .s-M {
313
+ color: #58a6ff;
314
+ }
315
+ .s-A,
316
+ .s-\? {
317
+ color: var(--pos);
318
+ }
319
+ .s-D {
320
+ color: var(--danger);
321
+ }
322
+ .stats {
323
+ margin-left: auto;
324
+ color: var(--muted);
325
+ white-space: nowrap;
326
+ }
327
+ .discard {
328
+ opacity: 0;
329
+ color: var(--danger);
330
+ border: 0;
331
+ background: transparent;
332
+ cursor: pointer;
333
+ padding: 1px;
334
+ }
335
+ .file:hover .discard {
336
+ opacity: 1;
337
+ }
338
+ .graph-section {
339
+ display: flex;
340
+ flex: 0 0 25%;
341
+ flex-direction: column;
342
+ min-height: 104px;
343
+ }
344
+ .graph-section > #graph {
345
+ flex: 1;
346
+ min-height: 0;
347
+ }
348
+ .graph {
349
+ position: relative;
350
+ overflow: auto;
351
+ padding: 0;
352
+ color: var(--muted);
353
+ font-size: 11px;
354
+ }
355
+ .graph-canvas {
356
+ position: relative;
357
+ min-width: max-content;
358
+ }
359
+ .graph-rails {
360
+ position: absolute;
361
+ z-index: 2;
362
+ top: 0;
363
+ left: 0;
364
+ overflow: visible;
365
+ pointer-events: none;
366
+ }
367
+ .graph-list {
368
+ position: relative;
369
+ z-index: 1;
370
+ }
371
+ .graph-row {
372
+ height: 24px;
373
+ display: flex;
374
+ align-items: center;
375
+ gap: 6px;
376
+ min-width: 360px;
377
+ padding-right: 8px;
378
+ white-space: nowrap;
379
+ position: relative;
380
+ }
381
+ .graph-hash {
382
+ color: var(--subtle);
383
+ font-size: 10px;
384
+ flex: none;
385
+ }
386
+ .graph-subject {
387
+ min-width: 0;
388
+ overflow: hidden;
389
+ text-overflow: ellipsis;
390
+ color: var(--text);
391
+ }
392
+ .graph-author {
393
+ color: var(--subtle);
394
+ flex: none;
395
+ }
396
+ .graph-row.head .graph-subject {
397
+ color: var(--accent);
398
+ }
399
+ .graph-row:hover {
400
+ background: var(--hover);
401
+ cursor: pointer;
402
+ }
403
+ .graph-diff {
404
+ display: grid;
405
+ place-items: center;
406
+ position: absolute;
407
+ z-index: 2;
408
+ right: 4px;
409
+ width: 22px;
410
+ height: 22px;
411
+ padding: 2px;
412
+ border: 0;
413
+ border-radius: 4px;
414
+ background: transparent;
415
+ color: var(--muted);
416
+ cursor: pointer;
417
+ opacity: 0;
418
+ isolation: isolate;
419
+ }
420
+ .graph-diff:before {
421
+ content: "";
422
+ position: absolute;
423
+ z-index: -1;
424
+ inset: -1px -1px -1px -26px;
425
+ background: linear-gradient(90deg, transparent, var(--hover) 52%);
426
+ opacity: 0;
427
+ pointer-events: none;
428
+ }
429
+ .graph-row:hover .graph-diff,
430
+ .graph-row:hover .graph-diff:before {
431
+ opacity: 1;
432
+ }
433
+ .graph-diff:hover {
434
+ color: var(--text);
435
+ background: var(--hover);
436
+ }
437
+ .graph-diff .icon {
438
+ width: 14px;
439
+ height: 14px;
440
+ }
441
+ .splitter {
442
+ height: 1px;
443
+ flex: none;
444
+ position: relative;
445
+ cursor: row-resize;
446
+ background: var(--border);
447
+ z-index: 2;
448
+ }
449
+ .splitter:after {
450
+ content: "";
451
+ position: absolute;
452
+ inset: -4px 0;
453
+ }
454
+ .splitter:hover,
455
+ .splitter.dragging {
456
+ background: var(--accent);
457
+ }
458
+ .scm.graph-collapsed .graph-section {
459
+ flex: 0 0 auto;
460
+ min-height: 0;
461
+ }
462
+ .scm.graph-collapsed .splitter,
463
+ .scm.changes-collapsed .splitter {
464
+ display: none;
465
+ }
466
+ .scm.changes-collapsed .changes-section {
467
+ flex: 0 0 auto !important;
468
+ min-height: 0;
469
+ }
470
+ .scm.changes-collapsed .graph-section {
471
+ flex: 1 1 0 !important;
472
+ min-height: 0;
473
+ }
474
+ .empty {
475
+ color: var(--muted);
476
+ padding: 36px 10px 6px;
477
+ text-align: center;
478
+ }
479
+ .init-btn {
480
+ margin: 2px auto 34px;
481
+ padding: 6px 14px;
482
+ background: var(--hover) !important;
483
+ color: var(--text) !important;
484
+ border-color: transparent !important;
485
+ }
486
+ .init-btn:hover {
487
+ background: var(--accent) !important;
488
+ color: #fff !important;
489
+ }
490
+ .repo,
491
+ .repo * {
492
+ font-size: 11px !important;
493
+ }
494
+ .repo {
495
+ line-height: 18px;
496
+ }
497
+ .file,
498
+ .file * {
499
+ font-size: 11px !important;
500
+ }
501
+ .file {
502
+ gap: 6px;
503
+ padding: 2px 10px;
504
+ line-height: 18px;
505
+ }
506
+ .repo .icon,
507
+ .file-type {
508
+ width: 14px;
509
+ height: 14px;
510
+ flex: none;
511
+ }
512
+ .file-main {
513
+ min-width: 0;
514
+ display: flex;
515
+ align-items: baseline;
516
+ gap: 7px;
517
+ flex: 1;
518
+ overflow: hidden;
519
+ }
520
+ .file-name {
521
+ overflow: hidden;
522
+ text-overflow: ellipsis;
523
+ white-space: nowrap;
524
+ color: var(--text);
525
+ }
526
+ .file-dir {
527
+ min-width: 0;
528
+ overflow: hidden;
529
+ text-overflow: ellipsis;
530
+ white-space: nowrap;
531
+ color: var(--subtle);
532
+ }
533
+ .file-actions {
534
+ display: flex;
535
+ align-items: center;
536
+ gap: 1px;
537
+ opacity: 0;
538
+ transition: opacity 0.1s;
539
+ }
540
+ .file:hover .file-actions {
541
+ opacity: 1;
542
+ }
543
+ .file-action {
544
+ display: grid;
545
+ place-items: center;
546
+ width: 22px;
547
+ height: 22px;
548
+ padding: 2px;
549
+ border: 0;
550
+ border-radius: 4px;
551
+ background: transparent;
552
+ color: var(--muted);
553
+ cursor: pointer;
554
+ }
555
+ .file-action:hover {
556
+ color: var(--text);
557
+ background: var(--hover);
558
+ }
559
+ .file-action.danger:hover {
560
+ color: var(--danger);
561
+ }
562
+ .file-status {
563
+ width: 15px;
564
+ text-align: center;
565
+ font-weight: 700;
566
+ }
567
+ .file .stats {
568
+ display: none;
569
+ }
570
+ .loading {
571
+ position: absolute;
572
+ inset: 0;
573
+ z-index: 10;
574
+ display: flex;
575
+ align-items: center;
576
+ justify-content: center;
577
+ gap: 8px;
578
+ background: color-mix(in srgb, var(--bg) 82%, transparent);
579
+ color: var(--muted);
580
+ transition: opacity 0.15s;
581
+ }
582
+ .loading.hidden {
583
+ opacity: 0;
584
+ pointer-events: none;
585
+ }
586
+ .spinner {
587
+ width: 14px;
588
+ height: 14px;
589
+ border: 2px solid var(--border);
590
+ border-top-color: var(--accent);
591
+ border-radius: 50%;
592
+ animation: spin 0.65s linear infinite;
593
+ }
594
+ @keyframes spin {
595
+ to {
596
+ transform: rotate(360deg);
597
+ }
598
+ }
599
+ </style>
600
+ </head>
601
+ <body>
602
+ <main class="scm" id="scm">
603
+ <div class="loading hidden" id="loading">
604
+ <span class="spinner"></span
605
+ ><span id="loadingLabel">Loading Git status…</span>
606
+ </div>
607
+ <section class="section">
608
+ <div class="heading" data-target="repos">
609
+ <svg class="icon chevron" viewBox="0 0 24 24">
610
+ <path d="m6 9 6 6 6-6" /></svg
611
+ ><b data-i18n="repositories">Repositories</b
612
+ ><span class="count" id="repoCount">0</span>
613
+ </div>
614
+ <div class="body" id="repos"></div>
615
+ </section>
616
+ <section class="section changes-section" id="changesSection">
617
+ <div class="heading" data-target="changes">
618
+ <svg class="icon chevron" viewBox="0 0 24 24">
619
+ <path d="m6 9 6 6 6-6" /></svg
620
+ ><b data-i18n="changes">Changes</b
621
+ ><span class="count" id="changeCount">0</span>
622
+ </div>
623
+ <div class="body" id="changes">
624
+ <div class="commit">
625
+ <div class="commit-actions">
626
+ <div class="commit-split">
627
+ <button class="action primary" id="aiCommit"></button
628
+ ><button
629
+ class="action primary commit-menu-toggle"
630
+ id="commitMenuToggle"
631
+ aria-label="Commit options"
632
+ >
633
+ <svg class="icon" viewBox="0 0 24 24">
634
+ <path d="m6 9 6 6 6-6" />
635
+ </svg>
636
+ </button>
637
+ <div class="commit-menu" id="commitMenu">
638
+ <button id="commit" data-i18n="commit">Commit</button>
639
+ </div>
640
+ </div>
641
+ <button class="action" id="stageAll" data-i18n="stageAll">
642
+ Stage all</button
643
+ ><button class="action" id="unstageAll" data-i18n="unstageAll">
644
+ Unstage all
645
+ </button>
646
+ </div>
647
+ </div>
648
+ <div class="file-list" id="files"></div>
649
+ </div>
650
+ </section>
651
+ <div class="splitter" id="splitter"></div>
652
+ <section class="section graph-section" id="graphSection">
653
+ <div class="heading" data-target="graph">
654
+ <svg class="icon chevron" viewBox="0 0 24 24">
655
+ <path d="m6 9 6 6 6-6" /></svg
656
+ ><b data-i18n="graph">Graph</b>
657
+ </div>
658
+ <div class="body graph" id="graph"></div>
659
+ </section>
660
+ </main>
661
+ <script>
662
+ const $ = (s) => document.querySelector(s),
663
+ state = {
664
+ repos: [],
665
+ path: "",
666
+ cwd: "",
667
+ scopeKey: "",
668
+ locale: "en-US",
669
+ assistantName: "…",
670
+ };
671
+ let jetbrainsTheme = {};
672
+ fetch("./jetbrains-icons/theme.json")
673
+ .then((r) => r.json())
674
+ .then((theme) => {
675
+ jetbrainsTheme = theme;
676
+ render();
677
+ })
678
+ .catch(() => {});
679
+ const icons = {
680
+ home: '<svg class="icon" viewBox="0 0 24 24"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/><path d="m9 9.5 2 2 4-4"/></svg>',
681
+ branch:
682
+ '<svg class="icon" viewBox="0 0 24 24"><rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/></svg>',
683
+ x: '<svg class="icon" viewBox="0 0 24 24"><path d="m18 6-12 12M6 6l12 12"/></svg>',
684
+ open: '<svg class="icon" viewBox="0 0 24 24"><path d="M4 11V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h7"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="m10 18 3-3-3-3"/></svg>',
685
+ undo: '<svg class="icon" viewBox="0 0 24 24"><path d="M9 14 4 9l5-5"/><path d="M4 9h10a6 6 0 0 1 0 12h-1"/></svg>',
686
+ stage:
687
+ '<svg class="icon" viewBox="0 0 24 24"><path d="M5 12h14M12 5v14"/></svg>',
688
+ unstage:
689
+ '<svg class="icon" viewBox="0 0 24 24"><path d="M5 12h14"/></svg>',
690
+ code: '<svg class="icon file-type" viewBox="0 0 24 24"><path d="m8 9-4 3 4 3M16 9l4 3-4 3M14 5l-4 14"/></svg>',
691
+ text: '<svg class="icon file-type" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M8 13h8M8 17h6"/></svg>',
692
+ image:
693
+ '<svg class="icon file-type" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>',
694
+ json: '<svg class="icon file-type" viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2 2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h3M16 3h3a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2 2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-3"/></svg>',
695
+ };
696
+ const t = {
697
+ en: {
698
+ repositories: "Repositories",
699
+ changes: "Changes",
700
+ graph: "Graph",
701
+ message: "Message (⌘Enter to commit)",
702
+ commit: "Commit",
703
+ aiCommit: "Ask {finchName} to commit",
704
+ aiCommitAnnotationTitle: "{finchName} will commit",
705
+ aiCommitNote: "Commit the current Git changes.",
706
+ aiCommitPrompt:
707
+ "Please commit the current Git changes. Review the changes, prepare an appropriate commit message, stage the required files, and create the commit.",
708
+ aiCommitAdded: "Added a commit request for {finchName}.",
709
+ commitOptions: "Commit options",
710
+ openFile: "Open file",
711
+ stageChanges: "Stage changes",
712
+ unstageChanges: "Unstage changes",
713
+ discardChanges: "Discard changes",
714
+ viewDiff: "Open changes",
715
+ stageAll: "Stage all",
716
+ unstageAll: "Unstage all",
717
+ staged: "Staged Changes",
718
+ working: "Changes",
719
+ untracked: "Untracked",
720
+ clean: "No changes",
721
+ none: "No Git repository found",
722
+ discard: "Discard changes?",
723
+ loading: "Loading Git status…",
724
+ initRepo: "Let {finchName} initialize Git",
725
+ initRepoHint: "Initialize a Git repository in this folder",
726
+ aiInitRepoTitle: "{finchName} will initialize Git",
727
+ aiInitRepoNote: "Initialize a Git repository in the current folder.",
728
+ aiInitRepoPrompt:
729
+ "The current folder ({cwd}) is not a Git repository yet. Please initialize it: run git init, optionally create a .gitignore, and make the first commit.",
730
+ aiInitRepoAdded: "Added a Git init request for {finchName}.",
731
+ },
732
+ zh: {
733
+ repositories: "存储库",
734
+ changes: "更改",
735
+ graph: "图表",
736
+ message: "消息(⌘Enter 提交)",
737
+ commit: "提交",
738
+ aiCommit: "让{finchName}提交",
739
+ aiCommitAnnotationTitle: "{finchName}来提交",
740
+ aiCommitNote: "提交当前 Git 改动。",
741
+ aiCommitPrompt:
742
+ "请提交当前 Git 工作区的改动:检查改动,拟定合适的提交信息,暂存所需文件并创建提交。",
743
+ aiCommitAdded: "已添加给{finchName}的提交请求。",
744
+ commitOptions: "提交选项",
745
+ openFile: "查看文件",
746
+ stageChanges: "暂存更改",
747
+ unstageChanges: "取消暂存更改",
748
+ discardChanges: "放弃更改",
749
+ viewDiff: "查看更改",
750
+ stageAll: "暂存全部",
751
+ unstageAll: "取消暂存全部",
752
+ staged: "已暂存的更改",
753
+ working: "更改",
754
+ untracked: "未跟踪",
755
+ clean: "没有更改",
756
+ none: "未找到 Git 存储库",
757
+ discard: "放弃更改?",
758
+ loading: "正在读取 Git 状态…",
759
+ initRepo: "让 {finchName} 初始化 Git",
760
+ initRepoHint: "在当前目录初始化 Git 仓库",
761
+ aiInitRepoTitle: "{finchName}来初始化 Git",
762
+ aiInitRepoNote: "在当前目录初始化 Git 仓库。",
763
+ aiInitRepoPrompt:
764
+ "当前目录({cwd})还不是 Git 仓库,请帮我完成初始化:运行 git init,按需创建 .gitignore,并创建首次提交。",
765
+ aiInitRepoAdded: "已添加给{finchName}的 Git 初始化请求。",
766
+ },
767
+ };
768
+ let words = t.en;
769
+ const post = (type, data = {}) =>
770
+ window.finch?.postMessage({ type, ...data });
771
+ const esc = (s) =>
772
+ String(s).replace(
773
+ /[&<>'"]/g,
774
+ (c) =>
775
+ ({
776
+ "&": "&amp;",
777
+ "<": "&lt;",
778
+ ">": "&gt;",
779
+ "'": "&#39;",
780
+ '"': "&quot;",
781
+ })[c],
782
+ );
783
+ const repo = () => state.repos.find((r) => r.path === state.path);
784
+ function render() {
785
+ const r = repo(),
786
+ root = state.repos[0]?.path;
787
+ $("#repoCount").textContent = state.repos.length;
788
+ $("#repos").innerHTML = state.repos.length
789
+ ? state.repos
790
+ .map(
791
+ (x) =>
792
+ `<div class="repo ${x.path === state.path ? "active" : ""} ${x.path !== root ? "submodule" : ""}" data-path="${esc(x.path)}">${x.path === root ? icons.home : icons.branch}<span class="name">${esc(x.name)}</span><span class="branch">${esc(x.branch)}${x.dirty ? " *" : ""}</span><span class="sync">${x.ahead ? "↑" + x.ahead : ""}${x.behind ? " ↓" + x.behind : ""}</span></div>`,
793
+ )
794
+ .join("")
795
+ : `<div class="empty">${words.none}</div><button class="action init-btn" id="initRepo" title="${esc(words.initRepoHint)}">${withFinchName(words.initRepo)}</button>`;
796
+ const initBtn = $("#initRepo");
797
+ if (initBtn)
798
+ initBtn.onclick = async () => {
799
+ try {
800
+ const result = await window.finch?.composer?.addContexts?.([
801
+ {
802
+ type: "annotation",
803
+ label: withFinchName(words.aiInitRepoTitle),
804
+ note: words.aiInitRepoNote,
805
+ promptText: words.aiInitRepoPrompt.replace(
806
+ "{cwd}",
807
+ state.cwd || "",
808
+ ),
809
+ },
810
+ ]);
811
+ if (result?.added) {
812
+ await window.finch?.ui?.toast({
813
+ title: withFinchName(words.aiInitRepoAdded),
814
+ variant: "success",
815
+ });
816
+ post("refresh", { cwd: state.cwd, scopeKey: state.scopeKey });
817
+ }
818
+ } catch {
819
+ await window.finch?.ui?.toast({
820
+ title: "Unable to add Git init request",
821
+ variant: "error",
822
+ });
823
+ }
824
+ };
825
+ document.querySelectorAll(".repo").forEach(
826
+ (el) =>
827
+ (el.onclick = () => {
828
+ state.path = el.dataset.path;
829
+ render();
830
+ }),
831
+ );
832
+ renderFiles(r);
833
+ renderGraph(r);
834
+ }
835
+ function fileIcon(path) {
836
+ const name = path.split("/").pop(),
837
+ lower = name.toLowerCase(),
838
+ type =
839
+ jetbrainsTheme.fileNames?.[name] ||
840
+ jetbrainsTheme.fileNames?.[lower] ||
841
+ jetbrainsTheme.fileExtensions?.[
842
+ name.split(".").slice(1).join(".").toLowerCase()
843
+ ] ||
844
+ jetbrainsTheme.fileExtensions?.[
845
+ name.split(".").pop().toLowerCase()
846
+ ],
847
+ icon = jetbrainsTheme.iconDefinitions?.[type]?.iconPath;
848
+ if (icon)
849
+ return `<img class="file-type" src="./jetbrains-icons/${icon.replace("./icons/", "")}" alt="">`;
850
+ return '<img class="file-type" src="./jetbrains-icons/text_auto.svg" alt="">';
851
+ }
852
+ function shortDir(dir) {
853
+ const p = dir.split("/");
854
+ return p.length > 2 ? "…/" + p.slice(-2).join("/") : dir;
855
+ }
856
+ function renderFiles(r) {
857
+ const list = $("#files");
858
+ if (!r) {
859
+ list.innerHTML = "";
860
+ $("#changeCount").textContent = "0";
861
+ return;
862
+ }
863
+ const groups = [
864
+ [words.staged, (f) => f.staged],
865
+ [words.working, (f) => !f.staged && f.status !== "?"],
866
+ [words.untracked, (f) => f.status === "?"],
867
+ ];
868
+ $("#changeCount").textContent = r.files.length;
869
+ list.innerHTML =
870
+ groups
871
+ .map(([label, match]) => {
872
+ const files = r.files.filter(match);
873
+ return files.length
874
+ ? `<div class="group">${label} (${files.length})</div>` +
875
+ files
876
+ .map(
877
+ (f) =>
878
+ `<div class="file" data-path="${esc(f.path)}">${fileIcon(f.path)}<span class="file-main"><span class="file-name">${esc(f.basename)}</span>${f.dirname ? `<span class="file-dir" title="${esc(f.dirname)}">${esc(shortDir(f.dirname))}</span>` : ""}</span><span class="file-actions"><button class="file-action open" title="${words.openFile}">${icons.open}</button>${f.staged ? "" : `<button class="file-action danger undo" title="${words.discardChanges}">${icons.undo}</button>`}<button class="file-action stage" title="${f.staged ? words.unstageChanges : words.stageChanges}">${f.staged ? icons.unstage : icons.stage}</button></span><span class="file-status s-${esc(f.status)}">${esc(f.status)}</span></div>`,
879
+ )
880
+ .join("")
881
+ : "";
882
+ })
883
+ .join("") || `<div class="empty">${words.clean}</div>`;
884
+ document.querySelectorAll(".file").forEach((row) => {
885
+ const path = row.dataset.path,
886
+ file = r.files.find((f) => f.path === path);
887
+ row.querySelector(".open").onclick = () =>
888
+ post("openFile", { repoPath: state.path, filePath: path });
889
+ row.querySelector(".stage").onclick = () =>
890
+ post(file.staged ? "unstage" : "stage", {
891
+ repoPath: state.path,
892
+ filePath: path,
893
+ });
894
+ const undo = row.querySelector(".undo");
895
+ if (undo)
896
+ undo.onclick = async () => {
897
+ const ok = await window.finch.ui.confirm({
898
+ title: words.discard,
899
+ message: path,
900
+ variant: "danger",
901
+ });
902
+ if (ok.confirmed)
903
+ post("discard", {
904
+ repoPath: state.path,
905
+ filePath: path,
906
+ status: file.status,
907
+ });
908
+ };
909
+ row.onpointerdown = async (event) => {
910
+ if (event.target.closest(".file-actions")) return;
911
+ event.preventDefault();
912
+ event.stopPropagation();
913
+ try {
914
+ await window.finch?.appView?.openDiff?.({
915
+ type: "files",
916
+ leftPath: file.diffLeftPath,
917
+ rightPath: file.diffRightPath,
918
+ });
919
+ } catch {
920
+ await window.finch?.ui?.toast?.({
921
+ title: "Unable to open Diff",
922
+ variant: "error",
923
+ });
924
+ }
925
+ };
926
+ });
927
+ }
928
+ const graphColors = [
929
+ "#c084fc",
930
+ "#fbbf24",
931
+ "#60a5fa",
932
+ "#34d399",
933
+ "#fb7185",
934
+ "#a3e635",
935
+ ];
936
+ function layoutGraph(commits) {
937
+ let lanes = [];
938
+ return commits.map((commit) => {
939
+ let lane = lanes.indexOf(commit.hash);
940
+ if (lane < 0) {
941
+ lanes.unshift(commit.hash);
942
+ lane = 0;
943
+ }
944
+ const before = [...lanes],
945
+ parents = commit.parents.filter(Boolean),
946
+ main = parents[0],
947
+ knownMain = main ? lanes.indexOf(main) : -1;
948
+ if (!main) lanes.splice(lane, 1);
949
+ else if (knownMain >= 0 && knownMain !== lane) lanes.splice(lane, 1);
950
+ else lanes[lane] = main;
951
+ const insertAt = Math.min(lane + 1, lanes.length),
952
+ extras = parents
953
+ .slice(1)
954
+ .filter((parent) => !lanes.includes(parent));
955
+ lanes.splice(insertAt, 0, ...extras);
956
+ return {
957
+ commit,
958
+ before,
959
+ lane,
960
+ targets: parents
961
+ .map((parent) => lanes.indexOf(parent))
962
+ .filter((index) => index >= 0),
963
+ after: [...lanes],
964
+ };
965
+ });
966
+ }
967
+ function renderGraph(r) {
968
+ const commits = r?.graph || [];
969
+ if (!commits.length) {
970
+ $("#graph").innerHTML = `<div class="empty">—</div>`;
971
+ return;
972
+ }
973
+ const rows = layoutGraph(commits),
974
+ step = 12,
975
+ rowHeight = 24,
976
+ maxLanes = Math.max(
977
+ 1,
978
+ ...rows.flatMap((row) => [row.before.length, row.after.length]),
979
+ ),
980
+ railWidth = maxLanes * step + 10,
981
+ height = rows.length * rowHeight,
982
+ x = (lane) => 7 + lane * step,
983
+ y = (row) => 12 + row * rowHeight,
984
+ paths = [],
985
+ curve = (from, to, row, color) =>
986
+ from === to
987
+ ? `<path d="M${x(from)} ${y(row)}V${y(row) + rowHeight}" stroke="${color}"/>`
988
+ : `<path d="M${x(from)} ${y(row)} C${x(from)} ${y(row) + 12} ${x(to)} ${y(row) + 12} ${x(to)} ${y(row) + rowHeight}" stroke="${color}"/>`;
989
+ rows.forEach((row, index) => {
990
+ row.before.forEach((hash, lane) => {
991
+ if (lane === row.lane) return;
992
+ const target = row.after.indexOf(hash);
993
+ if (target >= 0)
994
+ paths.push(
995
+ curve(
996
+ lane,
997
+ target,
998
+ index,
999
+ graphColors[lane % graphColors.length],
1000
+ ),
1001
+ );
1002
+ });
1003
+ row.targets.forEach((target) =>
1004
+ paths.push(
1005
+ curve(
1006
+ row.lane,
1007
+ target,
1008
+ index,
1009
+ graphColors[row.lane % graphColors.length],
1010
+ ),
1011
+ ),
1012
+ );
1013
+ const color = graphColors[row.lane % graphColors.length];
1014
+ paths.push(
1015
+ `<circle cx="${x(row.lane)}" cy="${y(index)}" r="4.5" fill="var(--bg)"/><circle cx="${x(row.lane)}" cy="${y(index)}" r="3" fill="${color}"/>`,
1016
+ );
1017
+ });
1018
+ const list = rows
1019
+ .map(
1020
+ (row, index) =>
1021
+ `<div class="graph-row ${index === 0 ? "head" : ""}" style="padding-left:${railWidth}px" title="${esc(row.commit.hash)}" data-hash="${esc(row.commit.hash)}" data-base="${esc(row.commit.parents[0] || `${row.commit.hash}^`)}"><span class="graph-subject">${esc(row.commit.subject)}</span><span class="graph-author">${esc(row.commit.author)}</span><span class="graph-hash">${esc(row.commit.shortHash)}</span><button class="graph-diff" data-hash="${esc(row.commit.hash)}" data-base="${esc(row.commit.parents[0] || `${row.commit.hash}^`)}" title="${words.viewDiff}"><svg class="icon" viewBox="0 0 24 24"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M9 10h6"/><path d="M12 13V7"/><path d="M9 17h6"/></svg></button></div>`,
1022
+ )
1023
+ .join("");
1024
+ $("#graph").innerHTML =
1025
+ `<div class="graph-canvas" style="height:${height}px;min-width:${railWidth + 360}px"><svg class="graph-rails" width="${railWidth}" height="${height}" viewBox="0 0 ${railWidth} ${height}" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${paths.join("")}</svg><div class="graph-list">${list}</div></div>`;
1026
+ document.querySelectorAll(".graph-diff").forEach(
1027
+ (button) =>
1028
+ (button.onpointerdown = async (event) => {
1029
+ event.preventDefault();
1030
+ event.stopPropagation();
1031
+ try {
1032
+ await window.finch?.appView?.openDiff?.({
1033
+ type: "git",
1034
+ repoPath: r.path,
1035
+ base: button.dataset.base,
1036
+ target: button.dataset.hash,
1037
+ });
1038
+ } catch {
1039
+ await window.finch?.ui?.toast?.({
1040
+ title: "Unable to open Diff",
1041
+ variant: "error",
1042
+ });
1043
+ }
1044
+ }),
1045
+ );
1046
+ document.querySelectorAll(".graph-row").forEach(
1047
+ (row) =>
1048
+ (row.onpointerdown = async (event) => {
1049
+ if (event.target.closest(".graph-diff")) return;
1050
+ event.preventDefault();
1051
+ event.stopPropagation();
1052
+ try {
1053
+ await window.finch?.appView?.openDiff?.({
1054
+ type: "git",
1055
+ repoPath: r.path,
1056
+ base: row.dataset.base,
1057
+ target: row.dataset.hash,
1058
+ });
1059
+ } catch {
1060
+ await window.finch?.ui?.toast?.({
1061
+ title: "Unable to open Diff",
1062
+ variant: "error",
1063
+ });
1064
+ }
1065
+ }),
1066
+ );
1067
+ }
1068
+ const withFinchName = (text) =>
1069
+ text.replace("{finchName}", state.assistantName || "…");
1070
+ function applyLanguage() {
1071
+ words = state.locale.startsWith("zh") ? t.zh : t.en;
1072
+ document
1073
+ .querySelectorAll("[data-i18n]")
1074
+ .forEach((el) => (el.textContent = words[el.dataset.i18n]));
1075
+ $("#loadingLabel").textContent = words.loading;
1076
+ $("#aiCommit").textContent = withFinchName(words.aiCommit);
1077
+ $("#commitMenuToggle").ariaLabel = words.commitOptions;
1078
+ render();
1079
+ }
1080
+ function toggle(target, heading) {
1081
+ const body = $("#" + target),
1082
+ collapsed = body.classList.toggle("collapsed");
1083
+ heading.classList.toggle("collapsed", collapsed);
1084
+ if (target === "graph") {
1085
+ const section = $("#graphSection");
1086
+ if (collapsed) {
1087
+ section.dataset.expandedFlex = section.style.flex;
1088
+ section.style.flex = "0 0 auto";
1089
+ } else {
1090
+ section.style.flex = section.dataset.expandedFlex || "";
1091
+ delete section.dataset.expandedFlex;
1092
+ }
1093
+ $("#scm").classList.toggle("graph-collapsed", collapsed);
1094
+ }
1095
+ if (target === "changes")
1096
+ $("#scm").classList.toggle("changes-collapsed", collapsed);
1097
+ }
1098
+ document
1099
+ .querySelectorAll(".heading")
1100
+ .forEach((h) => (h.onclick = () => toggle(h.dataset.target, h)));
1101
+ let dragging = false;
1102
+ $("#splitter").onpointerdown = (e) => {
1103
+ dragging = true;
1104
+ $("#splitter").classList.add("dragging");
1105
+ e.currentTarget.setPointerCapture(e.pointerId);
1106
+ };
1107
+ $("#splitter").onpointermove = (e) => {
1108
+ if (!dragging) return;
1109
+ const total = $("#scm").getBoundingClientRect(),
1110
+ top = total.top;
1111
+ const min = 104,
1112
+ max = Math.max(min, total.height - 150);
1113
+ const size = Math.max(min, Math.min(max, total.bottom - e.clientY));
1114
+ $("#graphSection").style.flex = `0 0 ${size}px`;
1115
+ };
1116
+ $("#splitter").onpointerup = () => {
1117
+ dragging = false;
1118
+ $("#splitter").classList.remove("dragging");
1119
+ };
1120
+ $("#stageAll").onclick = () => post("stageAll", { repoPath: state.path });
1121
+ $("#unstageAll").onclick = () =>
1122
+ post("unstageAll", { repoPath: state.path });
1123
+ $("#commit").onclick = () => {
1124
+ post("requestCommit", { repoPath: state.path });
1125
+ $("#commitMenu").classList.remove("open");
1126
+ };
1127
+ $("#commitMenuToggle").onclick = (e) => {
1128
+ e.stopPropagation();
1129
+ $("#commitMenu").classList.toggle("open");
1130
+ };
1131
+ document.addEventListener("click", () =>
1132
+ $("#commitMenu").classList.remove("open"),
1133
+ );
1134
+ $("#aiCommit").onclick = async () => {
1135
+ try {
1136
+ const result = await window.finch?.composer?.addContexts?.([
1137
+ {
1138
+ type: "annotation",
1139
+ label: withFinchName(words.aiCommitAnnotationTitle),
1140
+ note: words.aiCommitNote,
1141
+ promptText: words.aiCommitPrompt,
1142
+ },
1143
+ ]);
1144
+ if (result?.added)
1145
+ await window.finch?.ui?.toast({
1146
+ title: withFinchName(words.aiCommitAdded),
1147
+ variant: "success",
1148
+ });
1149
+ } catch {
1150
+ await window.finch?.ui?.toast({
1151
+ title: "Unable to add commit request",
1152
+ variant: "error",
1153
+ });
1154
+ }
1155
+ };
1156
+ const syncWorkspace = () =>
1157
+ post("syncWorkspace", { cwd: state.cwd, scopeKey: state.scopeKey }),
1158
+ refreshStatus = () =>
1159
+ post("refresh", { cwd: state.cwd, scopeKey: state.scopeKey, silent: true });
1160
+ let syncTimer;
1161
+ const updateAutoRefresh = () => {
1162
+ if (document.hidden) {
1163
+ if (syncTimer) {
1164
+ clearInterval(syncTimer);
1165
+ syncTimer = undefined;
1166
+ }
1167
+ return;
1168
+ }
1169
+ refreshStatus();
1170
+ if (!syncTimer) syncTimer = setInterval(refreshStatus, 3000);
1171
+ };
1172
+ document.addEventListener("visibilitychange", updateAutoRefresh);
1173
+ window.addEventListener("focus", syncWorkspace);
1174
+ window.addEventListener("pagehide", () => {
1175
+ if (syncTimer) {
1176
+ clearInterval(syncTimer);
1177
+ syncTimer = undefined;
1178
+ }
1179
+ });
1180
+ updateAutoRefresh();
1181
+ window.finch?.onMessage((m) => {
1182
+ if (m.type === "finch:env") {
1183
+ const nextScopeKey = [
1184
+ m.view || "",
1185
+ m.sessionId || "",
1186
+ m.spaceId || "",
1187
+ m.cwd || "",
1188
+ ].join("|"),
1189
+ changed = nextScopeKey !== state.scopeKey;
1190
+ state.cwd = m.cwd || "";
1191
+ state.scopeKey = nextScopeKey;
1192
+ state.locale = m.locale || "en-US";
1193
+ if (changed) {
1194
+ state.repos = [];
1195
+ state.path = "";
1196
+ render();
1197
+ }
1198
+ applyLanguage();
1199
+ post("init", { cwd: state.cwd, scopeKey: state.scopeKey });
1200
+ } else if (m.type === "config") {
1201
+ state.assistantName = m.assistantName || "Finch";
1202
+ applyLanguage();
1203
+ } else if (m.type === "finch:menu") {
1204
+ post(m.itemId === "refresh" ? "refresh" : m.itemId, {
1205
+ repoPath: state.path,
1206
+ });
1207
+ } else if (m.type === "loading") {
1208
+ if (!m.scopeKey || m.scopeKey === state.scopeKey)
1209
+ $("#loading").classList.toggle("hidden", !m.loading);
1210
+ } else if (m.type === "status") {
1211
+ if (m.scopeKey && m.scopeKey !== state.scopeKey) return;
1212
+ state.cwd = typeof m.cwd === "string" ? m.cwd : state.cwd;
1213
+ state.repos = m.repos || [];
1214
+ if (!state.repos.some((r) => r.path === state.path))
1215
+ state.path = state.repos[0]?.path || "";
1216
+ render();
1217
+ } else if (m.type === "fileDiff") {
1218
+ void window.finch?.appView?.openDiff?.({
1219
+ type: "files",
1220
+ leftPath: m.leftPath,
1221
+ rightPath: m.rightPath,
1222
+ });
1223
+ }
1224
+ });
1225
+ </script>
1226
+ </body>
1227
+ </html>