@vintasoftware/pr-review-canvas 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -26
- package/docs/reference.md +231 -121
- package/package.json +18 -4
- package/pr-review.config.example.yml +10 -4
- package/prompts/chat-seed.md +3 -0
- package/prompts/generation-format.md +3 -0
- package/skills/pr-review-canvas/SKILL.md +77 -40
- package/src/acpx/acpx.ts +21 -4
- package/src/acpx/events.ts +5 -1
- package/src/acpx/ndjson.ts +3 -1
- package/src/acpx/preflight.ts +5 -1
- package/src/canvas/comment.ts +24 -0
- package/src/canvas/export.ts +11 -2
- package/src/canvas/import.ts +30 -9
- package/src/canvas/name.ts +1 -0
- package/src/canvas/zip.ts +25 -2
- package/src/chat/chat-manager.ts +55 -41
- package/src/chat/context.ts +10 -3
- package/src/chat/seed.ts +3 -1
- package/src/chat/threads.ts +27 -26
- package/src/cli.ts +24 -9
- package/src/commands.ts +86 -25
- package/src/config.ts +24 -24
- package/src/contract/api.ts +32 -1
- package/src/contract/canvas-manifest.ts +2 -0
- package/src/contract/comments.ts +8 -1
- package/src/contract/discovery.ts +5 -2
- package/src/contract/generation-context.ts +27 -2
- package/src/contract/review-artifact.ts +14 -8
- package/src/contract/review-key.ts +51 -0
- package/src/contract/reviews.ts +17 -0
- package/src/contract/settings.ts +2 -0
- package/src/contract/state.ts +41 -19
- package/src/git/diff-collector.ts +2 -1
- package/src/git/environment.mjs +27 -0
- package/src/git/git.ts +117 -12
- package/src/git/local-target.ts +138 -0
- package/src/git/patch-lines.ts +34 -2
- package/src/git/pr-refs.ts +36 -0
- package/src/github/attachments.ts +9 -257
- package/src/github/canvas-comment.ts +22 -0
- package/src/github/capabilities.ts +3 -37
- package/src/github/comments.ts +9 -27
- package/src/github/post-comment.ts +7 -37
- package/src/github/post-review.ts +4 -19
- package/src/github/pr.ts +6 -84
- package/src/github/threads.ts +6 -2
- package/src/gitlab/attachments.ts +40 -0
- package/src/gitlab/canvas-comment.ts +26 -0
- package/src/gitlab/capabilities.ts +64 -0
- package/src/gitlab/comments.ts +164 -0
- package/src/gitlab/mr.ts +115 -0
- package/src/gitlab/post-comment.ts +111 -0
- package/src/gitlab/post-review.ts +54 -0
- package/src/gitlab/project.ts +13 -0
- package/src/host/attachments.ts +293 -0
- package/src/host/capabilities.ts +38 -0
- package/src/host/client.ts +245 -0
- package/src/host/host.ts +136 -0
- package/src/host/pr.ts +51 -0
- package/src/host/remote.ts +42 -0
- package/src/project-config.ts +25 -9
- package/src/prompt-files.ts +4 -3
- package/src/review/carry-over.ts +79 -0
- package/src/review/doctor.ts +31 -14
- package/src/review/normalize.ts +3 -1
- package/src/review/prepare.ts +87 -17
- package/src/review/prompt.ts +8 -2
- package/src/review/publish.ts +76 -13
- package/src/{github → review}/review-body.ts +17 -5
- package/src/review/skill-command.ts +5 -3
- package/src/review/trim-caps.ts +10 -6
- package/src/review/validate-folds.ts +2 -2
- package/src/review/validate.ts +67 -15
- package/src/server/app.ts +12 -4
- package/src/server/bundle.ts +320 -106
- package/src/server/context.ts +24 -10
- package/src/server/errors.ts +56 -10
- package/src/server/html.ts +36 -12
- package/src/server/node-server.ts +4 -2
- package/src/server/routes/api.ts +92 -31
- package/src/server/routes/chat-routes.ts +105 -46
- package/src/server/routes/pages.ts +25 -9
- package/src/server/routes/review-routes.ts +101 -35
- package/src/server/sse.ts +3 -1
- package/src/store/atomic-json.ts +5 -1
- package/src/store/canvas-store.ts +95 -46
- package/src/store/data-dir.ts +2 -1
- package/src/store/derived-store.ts +41 -23
- package/src/store/pr-store.ts +25 -15
- package/src/store/state-store.ts +42 -37
- package/static/brand.svg +19 -0
- package/static/js/api.js +47 -27
- package/static/js/app.js +27 -7
- package/static/js/chat-panel.js +93 -0
- package/static/js/chat.js +41 -17
- package/static/js/composer.js +30 -17
- package/static/js/contract-types.d.ts +3 -0
- package/static/js/diagram.js +2 -1
- package/static/js/diff-decorations.js +5 -3
- package/static/js/diff-renderer.js +7 -2
- package/static/js/dom.js +5 -7
- package/static/js/download.js +1 -1
- package/static/js/empty-state.js +85 -16
- package/static/js/errors.js +22 -6
- package/static/js/header.js +32 -9
- package/static/js/host.js +40 -0
- package/static/js/import-zone.js +1 -1
- package/static/js/interactions.js +56 -17
- package/static/js/keyboard.js +7 -2
- package/static/js/layers.js +26 -14
- package/static/js/links.js +9 -3
- package/static/js/markdown.js +28 -1
- package/static/js/nav.js +5 -2
- package/static/js/overview.js +32 -4
- package/static/js/points.js +5 -3
- package/static/js/progress.js +2 -1
- package/static/js/proposed-comment.js +4 -1
- package/static/js/quick-questions.js +2 -1
- package/static/js/regenerate.js +4 -1
- package/static/js/review-session.js +7 -2
- package/static/js/settings.js +2 -1
- package/static/js/signoff.js +9 -6
- package/static/styles/base.css +16 -6
- package/static/styles/chat-panel.css +81 -0
- package/static/styles/chat-tools.css +28 -0
- package/static/styles/chat.css +1 -1
- package/static/styles/commands.css +10 -4
- package/static/styles/diff.css +1 -1
- package/static/styles/header.css +18 -4
- package/static/styles/layout.css +4 -4
- package/static/styles/panels.css +4 -0
- package/static/styles/responsive.css +1 -15
- package/static/styles/review-actions.css +1 -0
- package/static/styles/review.css +24 -3
- package/static/styles/skin-github.css +99 -100
- package/static/styles.css +13 -12
- package/src/github/gh.ts +0 -196
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
Dark is GitHub's "dark dimmed": a softer grey-blue ground rather than the near-black one. */
|
|
11
11
|
|
|
12
|
-
:root[data-skin=
|
|
12
|
+
:root[data-skin='github'] {
|
|
13
13
|
--r: 6px;
|
|
14
14
|
--gap: 16px;
|
|
15
15
|
--main-pad: 16px 16px 40px;
|
|
@@ -70,45 +70,38 @@
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/* ---- Header ---- */
|
|
73
|
-
[data-skin=
|
|
73
|
+
[data-skin='github'] .hdr-bar {
|
|
74
74
|
background: light-dark(#ffffff, #151b23);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
[data-skin=
|
|
77
|
+
[data-skin='github'] .brand {
|
|
78
78
|
text-transform: none;
|
|
79
79
|
letter-spacing: normal;
|
|
80
80
|
font-size: 14px;
|
|
81
|
-
align-items: center;
|
|
82
|
-
}
|
|
83
|
-
[data-skin="github"] .brand .box {
|
|
84
|
-
border-width: 1px;
|
|
85
|
-
border-radius: var(--r);
|
|
86
|
-
background: var(--panel-2);
|
|
87
|
-
padding: 3px 8px;
|
|
88
81
|
}
|
|
89
82
|
/* The rainbow bar becomes the hairline that runs under GitHub's page header. */
|
|
90
|
-
[data-skin=
|
|
83
|
+
[data-skin='github'] .stripe {
|
|
91
84
|
height: 1px;
|
|
92
85
|
background: var(--ink);
|
|
93
86
|
}
|
|
94
|
-
[data-skin=
|
|
87
|
+
[data-skin='github'] .hdr-actions {
|
|
95
88
|
gap: 8px;
|
|
96
89
|
align-items: center;
|
|
97
90
|
}
|
|
98
|
-
[data-skin=
|
|
91
|
+
[data-skin='github'] .hdr-title {
|
|
99
92
|
padding-bottom: 16px;
|
|
100
93
|
}
|
|
101
94
|
|
|
102
95
|
/* ---- Commands ---- */
|
|
103
|
-
[data-skin=
|
|
104
|
-
[data-skin=
|
|
96
|
+
[data-skin='github'] .cmd::before,
|
|
97
|
+
[data-skin='github'] .cmd::after {
|
|
105
98
|
content: none;
|
|
106
99
|
}
|
|
107
|
-
[data-skin=
|
|
100
|
+
[data-skin='github'] .cmd {
|
|
108
101
|
font-weight: 500;
|
|
109
102
|
}
|
|
110
103
|
/* The header's commands read as GitHub's toolbar buttons; commands inside the page stay links. */
|
|
111
|
-
[data-skin=
|
|
104
|
+
[data-skin='github'] .hdr-actions .cmd {
|
|
112
105
|
padding: 3px 12px;
|
|
113
106
|
border: 1px solid var(--line);
|
|
114
107
|
border-radius: var(--r);
|
|
@@ -117,16 +110,16 @@
|
|
|
117
110
|
font-size: 12px;
|
|
118
111
|
line-height: 20px;
|
|
119
112
|
}
|
|
120
|
-
[data-skin=
|
|
113
|
+
[data-skin='github'] .hdr-actions .cmd:hover {
|
|
121
114
|
background: var(--btn-bg-hover);
|
|
122
115
|
text-decoration: none;
|
|
123
116
|
}
|
|
124
|
-
[data-skin=
|
|
125
|
-
[data-skin=
|
|
117
|
+
[data-skin='github'] .hdr-actions .cmd:disabled,
|
|
118
|
+
[data-skin='github'] .hdr-actions .cmd[aria-disabled='true'] {
|
|
126
119
|
color: var(--fg-dim);
|
|
127
120
|
}
|
|
128
|
-
[data-skin=
|
|
129
|
-
[data-skin=
|
|
121
|
+
[data-skin='github'] .cmd.fill,
|
|
122
|
+
[data-skin='github'] .cmd.big {
|
|
130
123
|
border: 1px solid transparent;
|
|
131
124
|
border-radius: var(--r);
|
|
132
125
|
background: var(--btn-primary);
|
|
@@ -134,56 +127,62 @@
|
|
|
134
127
|
box-shadow: none;
|
|
135
128
|
padding: 5px 16px;
|
|
136
129
|
}
|
|
137
|
-
[data-skin=
|
|
130
|
+
[data-skin='github'] .cmd.big {
|
|
138
131
|
padding: 8px 20px;
|
|
139
132
|
font-size: 14px;
|
|
140
133
|
}
|
|
141
|
-
[data-skin=
|
|
142
|
-
[data-skin=
|
|
143
|
-
[data-skin=
|
|
134
|
+
[data-skin='github'] .cmd.fill:hover,
|
|
135
|
+
[data-skin='github'] .cmd.big:hover,
|
|
136
|
+
[data-skin='github'] .cmd.fill:active {
|
|
144
137
|
background: var(--btn-primary-hover);
|
|
145
138
|
transform: none;
|
|
146
139
|
box-shadow: none;
|
|
147
140
|
}
|
|
148
|
-
[data-skin=
|
|
141
|
+
[data-skin='github'] .chat-composer .cmd.fill {
|
|
149
142
|
box-shadow: none;
|
|
150
143
|
}
|
|
151
144
|
|
|
152
145
|
/* ---- Pills: GitHub's rounded labels, with the state ones filled ---- */
|
|
153
|
-
[data-skin=
|
|
146
|
+
[data-skin='github'] .pill {
|
|
154
147
|
border-radius: 2em;
|
|
155
148
|
padding: 0 8px;
|
|
156
149
|
font-size: 12px;
|
|
157
150
|
line-height: 20px;
|
|
158
151
|
border-color: var(--line);
|
|
159
152
|
}
|
|
160
|
-
[data-skin=
|
|
161
|
-
[data-skin=
|
|
162
|
-
[data-skin=
|
|
163
|
-
[data-skin=
|
|
153
|
+
[data-skin='github'] .pill.open,
|
|
154
|
+
[data-skin='github'] .pill.merged,
|
|
155
|
+
[data-skin='github'] .pill.closed,
|
|
156
|
+
[data-skin='github'] .pill.draft,
|
|
157
|
+
[data-skin='github'] .pill.branch,
|
|
158
|
+
[data-skin='github'] .pill.uncommitted {
|
|
164
159
|
border-color: transparent;
|
|
165
160
|
color: #ffffff;
|
|
166
161
|
}
|
|
167
|
-
[data-skin=
|
|
162
|
+
[data-skin='github'] .pill.open {
|
|
168
163
|
background: light-dark(#1f883d, #347d39);
|
|
169
164
|
}
|
|
170
|
-
[data-skin=
|
|
165
|
+
[data-skin='github'] .pill.merged {
|
|
171
166
|
background: light-dark(#8250df, #8256d0);
|
|
172
167
|
}
|
|
173
|
-
[data-skin=
|
|
168
|
+
[data-skin='github'] .pill.closed {
|
|
174
169
|
background: light-dark(#cf222e, #c93c37);
|
|
175
170
|
}
|
|
176
|
-
[data-skin=
|
|
171
|
+
[data-skin='github'] .pill.draft {
|
|
177
172
|
background: light-dark(#6e7781, #636e7b);
|
|
178
173
|
}
|
|
179
|
-
[data-skin=
|
|
180
|
-
[data-skin=
|
|
181
|
-
|
|
174
|
+
[data-skin='github'] .pill.branch,
|
|
175
|
+
[data-skin='github'] .pill.uncommitted {
|
|
176
|
+
background: light-dark(#0969da, #1f6feb);
|
|
177
|
+
}
|
|
178
|
+
[data-skin='github'] .pill.risk,
|
|
179
|
+
[data-skin='github'] .pill.kind,
|
|
180
|
+
[data-skin='github'] .pill.agent {
|
|
182
181
|
background: var(--panel-2);
|
|
183
182
|
color: var(--fg-muted);
|
|
184
183
|
font-weight: 500;
|
|
185
184
|
}
|
|
186
|
-
[data-skin=
|
|
185
|
+
[data-skin='github'] .pill.test-tag {
|
|
187
186
|
background: var(--accent-bg);
|
|
188
187
|
color: var(--accent);
|
|
189
188
|
border-color: transparent;
|
|
@@ -193,164 +192,164 @@
|
|
|
193
192
|
}
|
|
194
193
|
|
|
195
194
|
/* ---- Cards ---- */
|
|
196
|
-
[data-skin=
|
|
197
|
-
[data-skin=
|
|
198
|
-
[data-skin=
|
|
195
|
+
[data-skin='github'] .panel,
|
|
196
|
+
[data-skin='github'] .layer,
|
|
197
|
+
[data-skin='github'] .diagram {
|
|
199
198
|
border-radius: var(--r);
|
|
200
199
|
}
|
|
201
|
-
[data-skin=
|
|
202
|
-
[data-skin=
|
|
203
|
-
[data-skin=
|
|
200
|
+
[data-skin='github'] .panel-h,
|
|
201
|
+
[data-skin='github'] .layer-h,
|
|
202
|
+
[data-skin='github'] .diagram-h {
|
|
204
203
|
border-radius: calc(var(--r) - 1px) calc(var(--r) - 1px) 0 0;
|
|
205
204
|
}
|
|
206
|
-
[data-skin=
|
|
205
|
+
[data-skin='github'] .chev {
|
|
207
206
|
color: var(--fg-muted);
|
|
208
207
|
}
|
|
209
|
-
[data-skin=
|
|
210
|
-
[data-skin=
|
|
211
|
-
[data-skin=
|
|
212
|
-
[data-skin=
|
|
208
|
+
[data-skin='github'] .summary h3,
|
|
209
|
+
[data-skin='github'] .judgment .lbl,
|
|
210
|
+
[data-skin='github'] .role,
|
|
211
|
+
[data-skin='github'] .diff th {
|
|
213
212
|
text-transform: none;
|
|
214
213
|
letter-spacing: normal;
|
|
215
214
|
}
|
|
216
|
-
[data-skin=
|
|
215
|
+
[data-skin='github'] .summary h3 {
|
|
217
216
|
font-size: 14px;
|
|
218
217
|
color: var(--fg);
|
|
219
218
|
}
|
|
220
|
-
[data-skin=
|
|
219
|
+
[data-skin='github'] .prose ul {
|
|
221
220
|
list-style: disc;
|
|
222
221
|
}
|
|
223
|
-
[data-skin=
|
|
222
|
+
[data-skin='github'] .sq {
|
|
224
223
|
border-radius: 2px;
|
|
225
224
|
}
|
|
226
|
-
[data-skin=
|
|
225
|
+
[data-skin='github'] .sevsum > .legend {
|
|
227
226
|
border-radius: var(--r);
|
|
228
227
|
box-shadow: var(--drop-shadow);
|
|
229
228
|
}
|
|
230
229
|
/* GitHub draws progress as a filled track rather than a hairline. */
|
|
231
|
-
[data-skin=
|
|
230
|
+
[data-skin='github'] .pline {
|
|
232
231
|
background: var(--line);
|
|
233
232
|
border-radius: var(--r);
|
|
234
233
|
}
|
|
235
|
-
[data-skin=
|
|
234
|
+
[data-skin='github'] .pline span {
|
|
236
235
|
border-radius: var(--r);
|
|
237
236
|
}
|
|
238
237
|
|
|
239
238
|
/* ---- Rail ---- */
|
|
240
|
-
[data-skin=
|
|
239
|
+
[data-skin='github'] .tree a {
|
|
241
240
|
margin: 0 8px;
|
|
242
241
|
padding: 6px 8px;
|
|
243
242
|
border-radius: var(--r);
|
|
244
243
|
}
|
|
245
|
-
[data-skin=
|
|
244
|
+
[data-skin='github'] .tree .layers a {
|
|
246
245
|
padding-left: 20px;
|
|
247
246
|
}
|
|
248
|
-
[data-skin=
|
|
247
|
+
[data-skin='github'] .tree a[aria-current] {
|
|
249
248
|
background: var(--accent-bg);
|
|
250
249
|
box-shadow: inset 3px 0 0 var(--accent);
|
|
251
250
|
}
|
|
252
251
|
|
|
253
252
|
/* ---- Diff ---- */
|
|
254
|
-
[data-skin=
|
|
253
|
+
[data-skin='github'] .plus {
|
|
255
254
|
border: 0;
|
|
256
255
|
border-radius: var(--r);
|
|
257
256
|
background: var(--accent);
|
|
258
257
|
color: #ffffff;
|
|
259
258
|
}
|
|
260
|
-
[data-skin=
|
|
259
|
+
[data-skin='github'] .av {
|
|
261
260
|
border-radius: 50%;
|
|
262
261
|
border-color: var(--line);
|
|
263
262
|
background: var(--panel-2);
|
|
264
263
|
}
|
|
265
|
-
[data-skin=
|
|
264
|
+
[data-skin='github'] tr.hunk td {
|
|
266
265
|
color: light-dark(#0550ae, #9198a1);
|
|
267
266
|
}
|
|
268
267
|
|
|
269
|
-
[data-skin=
|
|
268
|
+
[data-skin='github'] .diff .ln {
|
|
270
269
|
border-right-color: light-dark(#d1d9e0, transparent);
|
|
271
270
|
}
|
|
272
|
-
[data-skin=
|
|
271
|
+
[data-skin='github'] tr.add .ln {
|
|
273
272
|
background: light-dark(#e6ffec, #31503d);
|
|
274
273
|
color: light-dark(#818b98, #d1d7e0);
|
|
275
274
|
}
|
|
276
|
-
[data-skin=
|
|
275
|
+
[data-skin='github'] tr.del .ln {
|
|
277
276
|
background: light-dark(#ffebe9, rgba(229, 83, 75, 0.3));
|
|
278
277
|
color: light-dark(#818b98, #d1d7e0);
|
|
279
278
|
}
|
|
280
279
|
|
|
281
280
|
/* ---- Boxes, menus, and overlays ---- */
|
|
282
|
-
[data-skin=
|
|
283
|
-
[data-skin=
|
|
284
|
-
[data-skin=
|
|
285
|
-
[data-skin=
|
|
286
|
-
[data-skin=
|
|
287
|
-
[data-skin=
|
|
288
|
-
[data-skin=
|
|
289
|
-
[data-skin=
|
|
290
|
-
[data-skin=
|
|
291
|
-
[data-skin=
|
|
292
|
-
[data-skin=
|
|
293
|
-
[data-skin=
|
|
294
|
-
[data-skin=
|
|
281
|
+
[data-skin='github'] code,
|
|
282
|
+
[data-skin='github'] .snip,
|
|
283
|
+
[data-skin='github'] .ctx-chip,
|
|
284
|
+
[data-skin='github'] .diagram-src,
|
|
285
|
+
[data-skin='github'] .cmdbox code,
|
|
286
|
+
[data-skin='github'] .notice,
|
|
287
|
+
[data-skin='github'] .callout,
|
|
288
|
+
[data-skin='github'] .stale-bar,
|
|
289
|
+
[data-skin='github'] .proposed,
|
|
290
|
+
[data-skin='github'] .proposed-invalid,
|
|
291
|
+
[data-skin='github'] .drop,
|
|
292
|
+
[data-skin='github'] .prose pre,
|
|
293
|
+
[data-skin='github'] dialog.settings .panel-ro {
|
|
295
294
|
border-radius: var(--r);
|
|
296
295
|
}
|
|
297
|
-
[data-skin=
|
|
296
|
+
[data-skin='github'] code {
|
|
298
297
|
border: 0;
|
|
299
298
|
background: light-dark(rgba(129, 139, 152, 0.2), rgba(99, 110, 123, 0.4));
|
|
300
299
|
padding: 0.2em 0.4em;
|
|
301
300
|
}
|
|
302
|
-
[data-skin=
|
|
303
|
-
[data-skin=
|
|
301
|
+
[data-skin='github'] .cmdbox code,
|
|
302
|
+
[data-skin='github'] .diagram-src {
|
|
304
303
|
background: var(--panel-2);
|
|
305
304
|
border: 1px solid var(--line);
|
|
306
305
|
}
|
|
307
|
-
[data-skin=
|
|
306
|
+
[data-skin='github'] .drop {
|
|
308
307
|
border-width: 1px;
|
|
309
308
|
background: var(--panel-2);
|
|
310
309
|
}
|
|
311
|
-
[data-skin=
|
|
312
|
-
[data-skin=
|
|
310
|
+
[data-skin='github'] .drop:hover,
|
|
311
|
+
[data-skin='github'] .drop.over {
|
|
313
312
|
transform: none;
|
|
314
313
|
background: var(--accent-bg);
|
|
315
314
|
}
|
|
316
|
-
[data-skin=
|
|
317
|
-
[data-skin=
|
|
318
|
-
[data-skin=
|
|
315
|
+
[data-skin='github'] .proposed,
|
|
316
|
+
[data-skin='github'] .cmd.unseen,
|
|
317
|
+
[data-skin='github'] .qq-menu {
|
|
319
318
|
border-width: 1px;
|
|
320
319
|
}
|
|
321
|
-
[data-skin=
|
|
322
|
-
[data-skin=
|
|
323
|
-
[data-skin=
|
|
320
|
+
[data-skin='github'] .cmd.unseen,
|
|
321
|
+
[data-skin='github'] .qq-menu,
|
|
322
|
+
[data-skin='github'] .toast {
|
|
324
323
|
border-radius: var(--r);
|
|
325
324
|
box-shadow: var(--drop-shadow);
|
|
326
325
|
}
|
|
327
|
-
[data-skin=
|
|
328
|
-
[data-skin=
|
|
329
|
-
[data-skin=
|
|
330
|
-
[data-skin=
|
|
331
|
-
[data-skin=
|
|
326
|
+
[data-skin='github'] select,
|
|
327
|
+
[data-skin='github'] input,
|
|
328
|
+
[data-skin='github'] textarea,
|
|
329
|
+
[data-skin='github'] .composer-box textarea,
|
|
330
|
+
[data-skin='github'] dialog.signoff-dialog textarea {
|
|
332
331
|
border-width: 1px;
|
|
333
332
|
border-color: var(--line);
|
|
334
333
|
border-radius: var(--r);
|
|
335
334
|
padding: 5px 12px;
|
|
336
335
|
}
|
|
337
|
-
[data-skin=
|
|
336
|
+
[data-skin='github'] dialog {
|
|
338
337
|
border-width: 1px;
|
|
339
338
|
border-color: var(--line);
|
|
340
339
|
border-radius: 12px;
|
|
341
340
|
box-shadow: var(--drop-shadow);
|
|
342
341
|
}
|
|
343
|
-
[data-skin=
|
|
342
|
+
[data-skin='github'] dialog::backdrop {
|
|
344
343
|
background: rgba(28, 33, 40, 0.6);
|
|
345
344
|
}
|
|
346
345
|
|
|
347
346
|
@media (max-width: 1000px) {
|
|
348
347
|
/* The rail is a strip of chips at this width; the current one is underlined, not filled. */
|
|
349
|
-
[data-skin=
|
|
348
|
+
[data-skin='github'] .tree a {
|
|
350
349
|
margin: 0;
|
|
351
350
|
border-radius: var(--r);
|
|
352
351
|
}
|
|
353
|
-
[data-skin=
|
|
352
|
+
[data-skin='github'] .tree a[aria-current] {
|
|
354
353
|
box-shadow: inset 0 -3px 0 var(--accent);
|
|
355
354
|
}
|
|
356
355
|
}
|
package/static/styles.css
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/* Keep this order: shared styles, components, responsive rules, interaction controls, and the
|
|
2
2
|
github skin last, because it repaints the rules above it. */
|
|
3
|
-
@import
|
|
4
|
-
@import
|
|
5
|
-
@import
|
|
6
|
-
@import
|
|
7
|
-
@import
|
|
8
|
-
@import
|
|
9
|
-
@import
|
|
10
|
-
@import
|
|
11
|
-
@import
|
|
12
|
-
@import
|
|
13
|
-
@import
|
|
14
|
-
@import
|
|
3
|
+
@import './styles/base.css';
|
|
4
|
+
@import './styles/commands.css';
|
|
5
|
+
@import './styles/header.css';
|
|
6
|
+
@import './styles/layout.css';
|
|
7
|
+
@import './styles/review.css';
|
|
8
|
+
@import './styles/diff.css';
|
|
9
|
+
@import './styles/chat.css';
|
|
10
|
+
@import './styles/panels.css';
|
|
11
|
+
@import './styles/responsive.css';
|
|
12
|
+
@import './styles/review-actions.css';
|
|
13
|
+
@import './styles/chat-tools.css';
|
|
14
|
+
@import './styles/skin-github.css';
|
|
15
|
+
@import './styles/chat-panel.css';
|
package/src/github/gh.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The GitHub operations the tool needs, all through the `gh` CLI so the user's own login is
|
|
5
|
-
* used and no token is ever read by this code. Routes receive an implementation through
|
|
6
|
-
* AppContext; tests use an in-memory fake.
|
|
7
|
-
*/
|
|
8
|
-
export interface GitHubClient {
|
|
9
|
-
/** `gh api --method GET <path>` with optional query params; returns the parsed JSON body. */
|
|
10
|
-
api(path: string, params?: Record<string, string>): Promise<unknown>
|
|
11
|
-
/** `gh api -i --method GET <path>`: the response headers as well as the body. */
|
|
12
|
-
apiWithHeaders(path: string): Promise<GhResponse>
|
|
13
|
-
/** `gh api --method POST <path> --input -`; the JSON body goes over stdin, never the command line. */
|
|
14
|
-
post(path: string, body: unknown): Promise<unknown>
|
|
15
|
-
/** `gh api graphql`; returns the parsed `data` object. */
|
|
16
|
-
graphql(query: string, variables: Record<string, string | number>): Promise<unknown>
|
|
17
|
-
/** `gh auth status`: is the CLI installed and logged in? */
|
|
18
|
-
authStatus(): Promise<{ installed: boolean; authenticated: boolean; detail: string }>
|
|
19
|
-
/**
|
|
20
|
-
* `gh auth token`: the token of the current login, or null when there is none. Callers keep it
|
|
21
|
-
* in a local variable for the length of one request; it is never logged or written to disk.
|
|
22
|
-
*/
|
|
23
|
-
authToken(): Promise<string | null>
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** One HTTP answer from `gh api -i`: the status, the header names in lower case, and the body. */
|
|
27
|
-
export interface GhResponse {
|
|
28
|
-
status: number
|
|
29
|
-
headers: Record<string, string>
|
|
30
|
-
body: unknown
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Splits the output of `gh api -i` into the last header block and the body. A redirect prints
|
|
35
|
-
* one block per hop, and only the final one describes the answer.
|
|
36
|
-
*/
|
|
37
|
-
export function parseIncludedResponse(stdout: string): GhResponse {
|
|
38
|
-
const normalized = stdout.replace(/\r\n/g, '\n')
|
|
39
|
-
const parts = normalized.split('\n\n')
|
|
40
|
-
let body: unknown = null
|
|
41
|
-
const headerBlocks: string[] = []
|
|
42
|
-
for (const [i, part] of parts.entries()) {
|
|
43
|
-
if (/^HTTP\/[\d.]+ \d{3}/.test(part)) {
|
|
44
|
-
headerBlocks.push(part)
|
|
45
|
-
continue
|
|
46
|
-
}
|
|
47
|
-
// Everything after the last header block is the body, which may hold blank lines itself.
|
|
48
|
-
body = parseJsonOrNull(parts.slice(i).join('\n\n'))
|
|
49
|
-
break
|
|
50
|
-
}
|
|
51
|
-
const last = headerBlocks[headerBlocks.length - 1] ?? ''
|
|
52
|
-
const lines = last.split('\n').filter(l => l.trim() !== '')
|
|
53
|
-
const status = Number(/^HTTP\/[\d.]+ (\d{3})/.exec(lines[0] ?? '')?.[1] ?? 0)
|
|
54
|
-
const headers: Record<string, string> = {}
|
|
55
|
-
for (const line of lines.slice(1)) {
|
|
56
|
-
const at = line.indexOf(':')
|
|
57
|
-
if (at > 0) {
|
|
58
|
-
headers[line.slice(0, at).trim().toLowerCase()] = line.slice(at + 1).trim()
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return { status, headers, body }
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function parseJsonOrNull(text: string): unknown {
|
|
65
|
-
if (text.trim() === '') {
|
|
66
|
-
return null
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
return JSON.parse(text) as unknown
|
|
70
|
-
} catch {
|
|
71
|
-
return null
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export class GitHubApiError extends Error {
|
|
76
|
-
readonly path: string
|
|
77
|
-
readonly stderr: string
|
|
78
|
-
readonly exitCode: number
|
|
79
|
-
/** True when the `gh` binary is not on PATH. */
|
|
80
|
-
readonly missingBinary: boolean
|
|
81
|
-
|
|
82
|
-
constructor(path: string, stderr: string, exitCode: number, missingBinary = false) {
|
|
83
|
-
super(`gh api ${path} failed (${exitCode}): ${stderr.trim()}`)
|
|
84
|
-
this.name = 'GitHubApiError'
|
|
85
|
-
this.path = path
|
|
86
|
-
this.stderr = stderr
|
|
87
|
-
this.exitCode = exitCode
|
|
88
|
-
this.missingBinary = missingBinary
|
|
89
|
-
}
|
|
90
|
-
/** `gh api` exits 1 with "HTTP 404" in stderr for a missing resource. */
|
|
91
|
-
get notFound(): boolean {
|
|
92
|
-
return /HTTP 404/.test(this.stderr)
|
|
93
|
-
}
|
|
94
|
-
get unauthenticated(): boolean {
|
|
95
|
-
return /HTTP 401|not logged into|gh auth login/i.test(this.stderr)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
interface ExecResult {
|
|
100
|
-
stdout: string
|
|
101
|
-
stderr: string
|
|
102
|
-
code: number
|
|
103
|
-
missingBinary: boolean
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface GhExecOptions {
|
|
107
|
-
binary?: string
|
|
108
|
-
/** Written to the child's stdin, for `gh api --input -`. */
|
|
109
|
-
input?: string
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function execGh(args: string[], opts: GhExecOptions = {}): Promise<ExecResult> {
|
|
113
|
-
return new Promise(resolve => {
|
|
114
|
-
const child = execFile(
|
|
115
|
-
opts.binary ?? 'gh',
|
|
116
|
-
args,
|
|
117
|
-
{ encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 },
|
|
118
|
-
(error, stdout, stderr) => {
|
|
119
|
-
const missingBinary = error !== null && 'code' in error && error.code === 'ENOENT'
|
|
120
|
-
const code = error && typeof error.code === 'number' ? error.code : error ? 1 : 0
|
|
121
|
-
resolve({ stdout, stderr, code, missingBinary })
|
|
122
|
-
}
|
|
123
|
-
)
|
|
124
|
-
if (opts.input !== undefined) {
|
|
125
|
-
child.stdin?.on('error', () => undefined)
|
|
126
|
-
child.stdin?.end(opts.input)
|
|
127
|
-
}
|
|
128
|
-
})
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export type GhExec = typeof execGh
|
|
132
|
-
|
|
133
|
-
export function createGitHubClient(exec: GhExec = execGh): GitHubClient {
|
|
134
|
-
return {
|
|
135
|
-
api: async (path, params = {}) => {
|
|
136
|
-
// `gh api` switches to POST as soon as a field is given; these are reads, so pin GET.
|
|
137
|
-
const args = ['api', '--method', 'GET', path]
|
|
138
|
-
for (const [k, v] of Object.entries(params)) {
|
|
139
|
-
args.push('-F', `${k}=${v}`)
|
|
140
|
-
}
|
|
141
|
-
const r = await exec(args)
|
|
142
|
-
if (r.code !== 0) {
|
|
143
|
-
throw new GitHubApiError(path, r.missingBinary ? 'gh: command not found' : r.stderr, r.code, r.missingBinary)
|
|
144
|
-
}
|
|
145
|
-
return JSON.parse(r.stdout) as unknown
|
|
146
|
-
},
|
|
147
|
-
apiWithHeaders: async path => {
|
|
148
|
-
const r = await exec(['api', '-i', '--method', 'GET', path])
|
|
149
|
-
if (r.code !== 0) {
|
|
150
|
-
throw new GitHubApiError(path, r.missingBinary ? 'gh: command not found' : r.stderr, r.code, r.missingBinary)
|
|
151
|
-
}
|
|
152
|
-
return parseIncludedResponse(r.stdout)
|
|
153
|
-
},
|
|
154
|
-
post: async (path, body) => {
|
|
155
|
-
// The payload goes over stdin, so no comment text ever appears in an argument list.
|
|
156
|
-
const r = await exec(['api', '--method', 'POST', path, '--input', '-'], { input: JSON.stringify(body) })
|
|
157
|
-
if (r.code !== 0) {
|
|
158
|
-
throw new GitHubApiError(path, r.missingBinary ? 'gh: command not found' : r.stderr, r.code, r.missingBinary)
|
|
159
|
-
}
|
|
160
|
-
return JSON.parse(r.stdout) as unknown
|
|
161
|
-
},
|
|
162
|
-
graphql: async (query, variables) => {
|
|
163
|
-
const args = ['api', 'graphql', '-f', `query=${query}`]
|
|
164
|
-
for (const [k, v] of Object.entries(variables)) {
|
|
165
|
-
args.push(typeof v === 'number' ? '-F' : '-f', `${k}=${v}`)
|
|
166
|
-
}
|
|
167
|
-
const r = await exec(args)
|
|
168
|
-
if (r.code !== 0) {
|
|
169
|
-
throw new GitHubApiError(
|
|
170
|
-
'graphql',
|
|
171
|
-
r.missingBinary ? 'gh: command not found' : r.stderr,
|
|
172
|
-
r.code,
|
|
173
|
-
r.missingBinary
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
const body = JSON.parse(r.stdout) as { data?: unknown; errors?: Array<{ message: string }> }
|
|
177
|
-
if (body.errors && body.errors.length > 0) {
|
|
178
|
-
throw new GitHubApiError('graphql', body.errors.map(e => e.message).join('; '), 1)
|
|
179
|
-
}
|
|
180
|
-
return body.data
|
|
181
|
-
},
|
|
182
|
-
authStatus: async () => {
|
|
183
|
-
const r = await exec(['auth', 'status'])
|
|
184
|
-
if (r.missingBinary) {
|
|
185
|
-
return { installed: false, authenticated: false, detail: 'gh is not on PATH' }
|
|
186
|
-
}
|
|
187
|
-
const detail = (r.stdout + r.stderr).trim().split('\n')[0] ?? ''
|
|
188
|
-
return { installed: true, authenticated: r.code === 0, detail }
|
|
189
|
-
},
|
|
190
|
-
authToken: async () => {
|
|
191
|
-
const r = await exec(['auth', 'token'])
|
|
192
|
-
const token = r.stdout.trim()
|
|
193
|
-
return r.code === 0 && token !== '' ? token : null
|
|
194
|
-
},
|
|
195
|
-
}
|
|
196
|
-
}
|