@vintasoftware/pr-review-canvas 0.3.0 → 0.5.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 +79 -31
- package/docs/reference.md +396 -101
- package/package.json +9 -4
- package/pr-review.config.example.yml +44 -4
- package/prompts/chat-seed.md +3 -0
- package/prompts/generation-format.md +123 -26
- package/prompts/generation-strict-incremental.md +53 -0
- package/prompts/generation-strict.md +1 -27
- package/prompts/generation-surfacing-incremental.md +56 -0
- package/prompts/generation-surfacing.md +1 -58
- package/prompts/judging-strict.md +27 -0
- package/prompts/judging-surfacing.md +58 -0
- package/skills/pr-review-canvas/SKILL.md +81 -42
- package/src/acpx/acpx.ts +98 -5
- package/src/acpx/models.ts +43 -0
- package/src/canvas/comment.ts +24 -0
- package/src/canvas/import.ts +30 -9
- package/src/canvas/name.ts +1 -0
- package/src/canvas/zip.ts +21 -1
- package/src/chat/chat-manager.ts +67 -40
- package/src/chat/threads.ts +27 -26
- package/src/cli.ts +70 -8
- package/src/commands.ts +50 -17
- package/src/config.ts +14 -26
- package/src/contract/api.ts +58 -2
- package/src/contract/canvas-manifest.ts +7 -0
- package/src/contract/comments.ts +5 -0
- package/src/contract/discovery.ts +5 -2
- package/src/contract/generation-context.ts +76 -2
- package/src/contract/keys.ts +1 -0
- package/src/contract/pending.ts +49 -0
- package/src/contract/review-artifact.ts +50 -7
- package/src/contract/review-key.ts +51 -0
- package/src/contract/reviews.ts +26 -0
- package/src/contract/settings.ts +7 -0
- package/src/contract/state.ts +53 -22
- package/src/contract/validation.ts +1 -0
- package/src/git/environment.mjs +27 -0
- package/src/git/git.ts +109 -9
- 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 -41
- package/src/github/comments.ts +3 -24
- package/src/github/post-comment.ts +3 -36
- package/src/github/post-review.ts +64 -23
- package/src/github/pr.ts +6 -87
- package/src/github/threads.ts +2 -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 +93 -0
- package/src/gitlab/project.ts +13 -0
- package/src/gitlab/publish-drafts.ts +69 -0
- package/src/host/attachments.ts +293 -0
- package/src/host/capabilities.ts +38 -0
- package/src/host/client.ts +246 -0
- package/src/host/host.ts +154 -0
- package/src/host/pr.ts +51 -0
- package/src/host/remote.ts +42 -0
- package/src/project-config.ts +26 -0
- package/src/review/carry-marks.ts +131 -0
- package/src/review/carry-over.ts +79 -0
- package/src/review/doctor.ts +82 -38
- package/src/review/incremental.ts +107 -0
- package/src/review/normalize.ts +14 -4
- package/src/review/prepare.ts +111 -10
- package/src/review/prompt.ts +112 -5
- package/src/review/publish.ts +62 -10
- package/src/{github → review}/review-body.ts +17 -5
- package/src/review/skill-command.ts +5 -3
- package/src/review/test-paths.ts +44 -4
- package/src/review/validate-folds.ts +349 -24
- package/src/review/validate.ts +14 -5
- package/src/server/bundle.ts +324 -111
- package/src/server/context.ts +10 -8
- package/src/server/errors.ts +30 -8
- package/src/server/html.ts +35 -11
- package/src/server/routes/api.ts +63 -26
- package/src/server/routes/chat-routes.ts +91 -44
- package/src/server/routes/pages.ts +26 -9
- package/src/server/routes/review-routes.ts +278 -67
- package/src/store/canvas-store.ts +93 -55
- package/src/store/data-dir.ts +2 -1
- package/src/store/derived-store.ts +41 -27
- package/src/store/pr-store.ts +23 -14
- package/src/store/settings-store.ts +9 -1
- package/src/store/state-store.ts +105 -36
- package/src/upgrade.ts +338 -0
- package/static/js/api.js +87 -25
- package/static/js/app.js +50 -15
- package/static/js/chat-panel.js +32 -9
- package/static/js/chat.js +30 -6
- package/static/js/code-folds.js +171 -44
- package/static/js/composer.js +132 -17
- package/static/js/contract-types.d.ts +7 -0
- package/static/js/diff-decorations.js +67 -1
- package/static/js/diff-renderer.js +1 -1
- package/static/js/download.js +1 -1
- package/static/js/empty-state.js +102 -18
- package/static/js/errors.js +22 -6
- package/static/js/fold-levels.js +176 -0
- package/static/js/header.js +65 -15
- package/static/js/host.js +40 -0
- package/static/js/import-zone.js +1 -1
- package/static/js/interactions.js +274 -46
- package/static/js/keyboard.js +4 -1
- package/static/js/keys.js +12 -0
- package/static/js/layers.js +295 -32
- package/static/js/links.js +3 -3
- package/static/js/markdown.js +28 -1
- package/static/js/nav.js +22 -4
- package/static/js/pending.js +161 -0
- package/static/js/points.js +70 -9
- package/static/js/progress.js +4 -5
- package/static/js/quick-questions.js +15 -2
- package/static/js/reading-level.js +97 -0
- package/static/js/review-session.js +113 -29
- package/static/js/settings.js +55 -24
- package/static/js/signoff.js +79 -10
- package/static/js/skin.js +2 -2
- package/static/styles/chat-panel.css +22 -24
- package/static/styles/chat.css +4 -0
- package/static/styles/commands.css +6 -0
- package/static/styles/header.css +21 -0
- package/static/styles/panels.css +4 -0
- package/static/styles/pending.css +102 -0
- package/static/styles/review-actions.css +1 -0
- package/static/styles/review.css +4 -0
- package/static/styles/skin-github.css +7 -1
- package/static/styles.css +1 -0
- package/src/github/gh.ts +0 -211
package/static/js/settings.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
// The settings dialog: the personal
|
|
3
|
-
// at the project config, which is committed
|
|
2
|
+
// The settings dialog: the personal settings this browser can change, the reading level a review
|
|
3
|
+
// opens at and the chat settings, and a read-only look at the project config, which is committed
|
|
4
|
+
// and belongs to the repository.
|
|
4
5
|
/** @typedef {import('./contract-types.js').AgentsResponse} AgentsResponse */
|
|
5
6
|
/** @typedef {import('./contract-types.js').SettingsResponse} SettingsResponse */
|
|
6
7
|
import { fetchAgents, fetchSettings, probeAgent, saveSettings } from './api.js'
|
|
7
8
|
import { runCommand } from './commands.js'
|
|
8
9
|
import { esc, qs } from './dom.js'
|
|
10
|
+
import { isFoldLevel } from './fold-levels.js'
|
|
11
|
+
import { foldLevelOptionsHtml } from './reading-level.js'
|
|
9
12
|
|
|
10
13
|
export const SETTINGS_DIALOG_ID = 'settings-dialog'
|
|
11
14
|
|
|
12
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Model ids the input suggests per agent. Free text is allowed; this is only a shortcut. The
|
|
17
|
+
* server runs the newest model of whichever family is saved, so these do not go stale.
|
|
18
|
+
*/
|
|
13
19
|
export const MODEL_SUGGESTIONS = {
|
|
14
|
-
claude: ['
|
|
15
|
-
codex: ['gpt-
|
|
20
|
+
claude: ['opus', 'opus[1m]', 'sonnet', 'haiku', 'fable'],
|
|
21
|
+
codex: ['gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna', 'gpt-6-astra[high]'],
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
/** @param {string} agent */
|
|
@@ -22,18 +28,43 @@ export function modelOptionsHtml(agent) {
|
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
|
-
*
|
|
31
|
+
* The chat agent's fields, with the notice about acpx when it is missing.
|
|
32
|
+
* @param {SettingsResponse['settings']} settings
|
|
26
33
|
* @param {AgentsResponse} agents
|
|
27
34
|
* @returns {string}
|
|
28
35
|
*/
|
|
29
|
-
|
|
30
|
-
const { settings, overrides, project } = data
|
|
36
|
+
function chatFieldsHtml(settings, agents) {
|
|
31
37
|
const options = agents.agents
|
|
32
38
|
.map(a => {
|
|
33
39
|
const reason = a.available ? '' : ` (${a.reason ?? 'not available'})`
|
|
34
40
|
return `<option value="${esc(a.id)}"${a.id === settings.agent ? ' selected' : ''}${a.available ? '' : ' disabled'}>${esc(a.id)}${esc(reason)}</option>`
|
|
35
41
|
})
|
|
36
42
|
.join('')
|
|
43
|
+
return (
|
|
44
|
+
(agents.acpx.installed
|
|
45
|
+
? ''
|
|
46
|
+
: '<p class="notice" role="status">acpx is not on PATH, so AI Chat is off. Install acpx and reload.</p>') +
|
|
47
|
+
'<div class="field"><label for="set-agent">Agent</label>' +
|
|
48
|
+
`<select id="set-agent">${options}</select></div>` +
|
|
49
|
+
'<div class="field"><label for="set-model">Model</label>' +
|
|
50
|
+
`<input id="set-model" list="model-list" value="${esc(settings.model ?? '')}" placeholder="the agent's default">` +
|
|
51
|
+
`<datalist id="model-list">${modelOptionsHtml(settings.agent)}</datalist></div>` +
|
|
52
|
+
'<div class="field"><label for="set-timeout">Chat timeout (seconds)</label>' +
|
|
53
|
+
`<input id="set-timeout" type="number" min="30" max="3600" value="${esc(settings.chatTimeoutSec)}"></div>` +
|
|
54
|
+
'<div class="field"><label for="set-turns">Max turns</label>' +
|
|
55
|
+
`<input id="set-turns" type="number" min="1" max="100" value="${esc(settings.maxTurns ?? '')}" placeholder="the agent's default"></div>` +
|
|
56
|
+
'<p class="muted small">Changing the agent starts a new chat thread; the old ones stay in the list.</p>'
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {SettingsResponse} data
|
|
62
|
+
* @param {AgentsResponse | null} agents null when the project turns chat off, so the dialog holds
|
|
63
|
+
* only what the page itself reads
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function settingsDialogHtml(data, agents) {
|
|
67
|
+
const { settings, overrides, project } = data
|
|
37
68
|
const overrideNote =
|
|
38
69
|
overrides.agent === undefined && overrides.model === undefined
|
|
39
70
|
? ''
|
|
@@ -49,32 +80,26 @@ export function settingsDialogHtml(data, agents) {
|
|
|
49
80
|
`<dialog id="${SETTINGS_DIALOG_ID}" class="settings" aria-labelledby="settings-h">` +
|
|
50
81
|
'<h2 id="settings-h">Settings</h2>' +
|
|
51
82
|
overrideNote +
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`<select id="set-agent">${options}</select></div>` +
|
|
57
|
-
'<div class="field"><label for="set-model">Model</label>' +
|
|
58
|
-
`<input id="set-model" list="model-list" value="${esc(settings.model ?? '')}" placeholder="the agent's default">` +
|
|
59
|
-
`<datalist id="model-list">${modelOptionsHtml(settings.agent)}</datalist></div>` +
|
|
60
|
-
'<div class="field"><label for="set-timeout">Chat timeout (seconds)</label>' +
|
|
61
|
-
`<input id="set-timeout" type="number" min="30" max="3600" value="${esc(settings.chatTimeoutSec)}"></div>` +
|
|
62
|
-
'<div class="field"><label for="set-turns">Max turns</label>' +
|
|
63
|
-
`<input id="set-turns" type="number" min="1" max="100" value="${esc(settings.maxTurns ?? '')}" placeholder="the agent's default"></div>` +
|
|
64
|
-
'<p class="muted small">Changing the agent starts a new chat thread; the old ones stay in the list.</p>' +
|
|
83
|
+
'<div class="field"><label for="set-fold-level">Hide code by default</label>' +
|
|
84
|
+
`<select id="set-fold-level">${foldLevelOptionsHtml(settings.foldLevel)}</select></div>` +
|
|
85
|
+
'<p class="muted small">The level every review opens at. The Hide code control and the <span class="mono">f</span> key change it for one page.</p>' +
|
|
86
|
+
(agents === null ? '' : chatFieldsHtml(settings, agents)) +
|
|
65
87
|
`<p class="muted small mono">${esc(data.file)}</p>` +
|
|
66
88
|
'<div class="panel-ro"><h3>Project config (read-only)</h3>' +
|
|
67
89
|
`<ul class="plain"><li>chat enabled: ${project.chatEnabled ? 'yes' : 'no'}</li>` +
|
|
90
|
+
`<li>canvas kept for an identical diff: ${project.keepForIdenticalDiff ? 'yes' : 'no'}</li>` +
|
|
68
91
|
`<li>rulebook: ${esc(project.rulebook ?? 'none')}</li>` +
|
|
69
92
|
`<li>configured layer suggestions: ${project.layers}</li>` +
|
|
70
93
|
`<li>high-risk patterns: ${project.highRisk}</li>` +
|
|
71
94
|
`<li>max repair rounds: ${project.maxRepairRounds}</li>` +
|
|
72
95
|
`<li>inline diff max lines: ${project.inlineDiffMaxLines}</li>` +
|
|
73
|
-
`<li>small change set: ${project.smallPrHunks}
|
|
96
|
+
`<li>small change set: ${project.smallPrHunks} chunks</li></ul>` +
|
|
74
97
|
`<p class="muted small mono">${esc(project.file ?? 'built-in defaults (no pr-review.config.yml)')}</p></div>` +
|
|
75
98
|
'<p class="probe-result" role="status"></p>' +
|
|
76
99
|
'<div class="dialog-actions">' +
|
|
77
|
-
|
|
100
|
+
(agents === null
|
|
101
|
+
? ''
|
|
102
|
+
: '<button class="cmd" type="button" data-act="settings-probe">test agent</button>') +
|
|
78
103
|
'<button class="cmd fill" type="button" data-act="settings-save">save</button>' +
|
|
79
104
|
'<button class="cmd" type="button" data-act="settings-close">close</button>' +
|
|
80
105
|
'</div></dialog>'
|
|
@@ -103,7 +128,9 @@ export async function openSettingsDialog(root, opener, opts = {}) {
|
|
|
103
128
|
const loaded = await runCommand(
|
|
104
129
|
opener,
|
|
105
130
|
async () => {
|
|
106
|
-
|
|
131
|
+
// With chat off the agent routes do not exist, and the dialog holds only the reading level.
|
|
132
|
+
const data = await api.fetchSettings()
|
|
133
|
+
const agents = data.project.chatEnabled ? await api.fetchAgents() : null
|
|
107
134
|
return { data, agents }
|
|
108
135
|
},
|
|
109
136
|
{ pendingLabel: 'loading…' }
|
|
@@ -128,12 +155,16 @@ export async function openSettingsDialog(root, opener, opts = {}) {
|
|
|
128
155
|
|
|
129
156
|
/** The values the dialog holds right now, as the PUT body. */
|
|
130
157
|
export function readSettingsForm(/** @type {ParentNode} */ dialog) {
|
|
158
|
+
const foldLevel = qs('#set-fold-level', dialog)
|
|
131
159
|
const agent = qs('#set-agent', dialog)
|
|
132
160
|
const model = qs('#set-model', dialog)
|
|
133
161
|
const timeout = qs('#set-timeout', dialog)
|
|
134
162
|
const turns = qs('#set-turns', dialog)
|
|
135
163
|
/** @type {import('./contract-types.js').SettingsInput} */
|
|
136
164
|
const input = {}
|
|
165
|
+
if (foldLevel instanceof HTMLSelectElement && isFoldLevel(foldLevel.value)) {
|
|
166
|
+
input.foldLevel = foldLevel.value
|
|
167
|
+
}
|
|
137
168
|
if (agent instanceof HTMLSelectElement && agent.value !== '') {
|
|
138
169
|
input.agent = /** @type {import('./contract-types.js').ChatAgent} */ (agent.value)
|
|
139
170
|
}
|
package/static/js/signoff.js
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
/** @typedef {import('./contract-types.js').PrState} PrState */
|
|
5
5
|
/** @typedef {import('./contract-types.js').ReviewArtifact} ReviewArtifact */
|
|
6
6
|
/** @typedef {import('./contract-types.js').ReviewBodyResponse} ReviewBodyResponse */
|
|
7
|
-
import { previewControlsHtml, setDisabledReason } from './composer.js'
|
|
7
|
+
import { previewControlsHtml, setDisabledReason, setMarkdownPreview } from './composer.js'
|
|
8
8
|
import { esc } from './dom.js'
|
|
9
|
+
import { hostLabel } from './host.js'
|
|
10
|
+
import { pendingLabel } from './pending.js'
|
|
9
11
|
import { layerProgress } from './progress.js'
|
|
10
12
|
|
|
11
13
|
export const SIGNOFF_DIALOG_ID = 'signoff-dialog'
|
|
@@ -13,6 +15,9 @@ export const SIGNOFF_DIALOG_ID = 'signoff-dialog'
|
|
|
13
15
|
/** Why the post command waits: the body the server writes is not there yet. */
|
|
14
16
|
export const LOADING_REASON = 'the review body is still loading'
|
|
15
17
|
|
|
18
|
+
/** Why it waits after the body failed to load: there is nothing to post. */
|
|
19
|
+
export const FAILED_REASON = 'the review body could not be loaded'
|
|
20
|
+
|
|
16
21
|
/**
|
|
17
22
|
* Why approve is not allowed yet, or null when it is. Other changes never counts.
|
|
18
23
|
* @param {ReviewArtifact} artifact
|
|
@@ -32,22 +37,47 @@ export function approveBlockedReason(artifact, state) {
|
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
/**
|
|
35
|
-
* @param {'
|
|
40
|
+
* @param {import('./contract-types.js').ReviewEvent} event
|
|
36
41
|
*/
|
|
37
42
|
export function signoffTitle(event) {
|
|
38
|
-
|
|
43
|
+
switch (event) {
|
|
44
|
+
case 'APPROVE':
|
|
45
|
+
return `Approve on ${hostLabel()}`
|
|
46
|
+
case 'REQUEST_CHANGES':
|
|
47
|
+
return `Request changes on ${hostLabel()}`
|
|
48
|
+
case 'COMMENT':
|
|
49
|
+
return `Comment on ${hostLabel()}`
|
|
50
|
+
}
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
/**
|
|
42
|
-
*
|
|
54
|
+
* What the dialog says this event does, under its heading.
|
|
55
|
+
* @param {import('./contract-types.js').ReviewEvent} event
|
|
56
|
+
*/
|
|
57
|
+
export function signoffHint(event) {
|
|
58
|
+
const where = `on ${hostLabel()} as you`
|
|
59
|
+
switch (event) {
|
|
60
|
+
case 'APPROVE':
|
|
61
|
+
return `This posts an approving review ${where}. Edit the body first if you want to.`
|
|
62
|
+
case 'REQUEST_CHANGES':
|
|
63
|
+
return `This posts a review requesting changes ${where}. Edit the body first if you want to.`
|
|
64
|
+
case 'COMMENT':
|
|
65
|
+
return `This posts a review with no verdict ${where}: nothing is approved or rejected. Edit the body first if you want to.`
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {{ event: import('./contract-types.js').ReviewEvent }} opts
|
|
43
71
|
* @returns {string}
|
|
44
72
|
*/
|
|
45
73
|
export function signoffDialogHtml(opts) {
|
|
46
74
|
return (
|
|
47
75
|
`<dialog id="${SIGNOFF_DIALOG_ID}" class="signoff-dialog" aria-labelledby="signoff-h">` +
|
|
48
76
|
`<h2 id="signoff-h">${esc(signoffTitle(opts.event))}</h2>` +
|
|
49
|
-
|
|
77
|
+
`<p class="hint">${esc(signoffHint(opts.event))}</p>` +
|
|
50
78
|
'<p class="signoff-target muted mono"></p>' +
|
|
79
|
+
'<p class="signoff-folds muted"></p>' +
|
|
80
|
+
'<p class="signoff-pending"></p>' +
|
|
51
81
|
'<label class="sr" for="signoff-body">Review body</label>' +
|
|
52
82
|
previewControlsHtml() +
|
|
53
83
|
'<textarea id="signoff-body" rows="12" aria-busy="true"></textarea>' +
|
|
@@ -63,7 +93,7 @@ export function signoffDialogHtml(opts) {
|
|
|
63
93
|
* Creates the dialog on first use, points it at this event, and opens it. The body arrives
|
|
64
94
|
* afterwards through `fillSignoffDialog`.
|
|
65
95
|
* @param {HTMLElement} root
|
|
66
|
-
* @param {{ event: '
|
|
96
|
+
* @param {{ event: import('./contract-types.js').ReviewEvent }} opts
|
|
67
97
|
* @returns {HTMLDialogElement}
|
|
68
98
|
*/
|
|
69
99
|
export function openSignoffDialog(root, opts) {
|
|
@@ -77,14 +107,20 @@ export function openSignoffDialog(root, opts) {
|
|
|
77
107
|
if (!(dialog instanceof HTMLDialogElement)) {
|
|
78
108
|
throw new Error('sign-off dialog did not render')
|
|
79
109
|
}
|
|
80
|
-
dialog
|
|
81
|
-
.querySelector('[data-act="markdown-write"]')
|
|
82
|
-
?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
110
|
+
setMarkdownPreview(dialog, false)
|
|
83
111
|
dialog.setAttribute('data-event', opts.event)
|
|
84
112
|
const heading = dialog.querySelector('#signoff-h')
|
|
85
113
|
if (heading !== null) {
|
|
86
114
|
heading.textContent = signoffTitle(opts.event)
|
|
87
115
|
}
|
|
116
|
+
const hint = dialog.querySelector('.hint')
|
|
117
|
+
if (hint !== null) {
|
|
118
|
+
hint.textContent = signoffHint(opts.event)
|
|
119
|
+
}
|
|
120
|
+
const pending = dialog.querySelector('.signoff-pending')
|
|
121
|
+
if (pending !== null) {
|
|
122
|
+
pending.textContent = ''
|
|
123
|
+
}
|
|
88
124
|
const result = dialog.querySelector('.signoff-result')
|
|
89
125
|
if (result !== null) {
|
|
90
126
|
result.textContent = ''
|
|
@@ -104,6 +140,20 @@ export function openSignoffDialog(root, opts) {
|
|
|
104
140
|
return dialog
|
|
105
141
|
}
|
|
106
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Records how much code the reader had hidden while reviewing, so the sign-off says what was
|
|
145
|
+
* read at a glance and what was read line by line.
|
|
146
|
+
* @param {HTMLDialogElement} dialog
|
|
147
|
+
* @param {string} note
|
|
148
|
+
*/
|
|
149
|
+
export function setSignoffFolds(dialog, note) {
|
|
150
|
+
const box = dialog.querySelector('.signoff-folds')
|
|
151
|
+
if (box !== null) {
|
|
152
|
+
box.textContent = note
|
|
153
|
+
}
|
|
154
|
+
return dialog
|
|
155
|
+
}
|
|
156
|
+
|
|
107
157
|
/**
|
|
108
158
|
* Puts the generated body and the target commit in the dialog.
|
|
109
159
|
* @param {HTMLDialogElement} dialog
|
|
@@ -121,6 +171,17 @@ export function fillSignoffDialog(dialog, preview) {
|
|
|
121
171
|
if (target !== null) {
|
|
122
172
|
target.textContent = `on commit ${preview.headSha.slice(0, 7)}`
|
|
123
173
|
}
|
|
174
|
+
// The drafts go out with the review, so the dialog says so before anything is posted.
|
|
175
|
+
const pending = dialog.querySelector('.signoff-pending')
|
|
176
|
+
if (pending !== null) {
|
|
177
|
+
pending.textContent =
|
|
178
|
+
preview.pending > 0 ? `${pendingLabel(preview.pending)} will be posted with this review.` : ''
|
|
179
|
+
pending.classList.toggle('has-pending', preview.pending > 0)
|
|
180
|
+
}
|
|
181
|
+
const post = dialog.querySelector('[data-act="signoff-post"]')
|
|
182
|
+
if (post !== null) {
|
|
183
|
+
post.textContent = preview.pending > 0 ? `post review · ${pendingLabel(preview.pending)}` : 'post review'
|
|
184
|
+
}
|
|
124
185
|
return dialog
|
|
125
186
|
}
|
|
126
187
|
|
|
@@ -133,7 +194,7 @@ export function showSignoffResult(dialog, review) {
|
|
|
133
194
|
const result = dialog.querySelector('.signoff-result')
|
|
134
195
|
if (result !== null) {
|
|
135
196
|
result.textContent = 'Posted · '
|
|
136
|
-
result.append(externalLink(review.url,
|
|
197
|
+
result.append(externalLink(review.url, `see it on ${hostLabel()}`))
|
|
137
198
|
}
|
|
138
199
|
return result
|
|
139
200
|
}
|
|
@@ -165,6 +226,14 @@ export function showSignoffError(dialog, message) {
|
|
|
165
226
|
if (result !== null) {
|
|
166
227
|
result.textContent = message
|
|
167
228
|
}
|
|
229
|
+
// The load this reports is over, so the box stops saying it is busy. Posting stays out of
|
|
230
|
+
// reach, but for the reason that is true now rather than the one that was true before.
|
|
231
|
+
const area = dialog.querySelector('textarea')
|
|
232
|
+
if (area instanceof HTMLTextAreaElement && area.getAttribute('aria-busy') === 'true') {
|
|
233
|
+
area.removeAttribute('aria-busy')
|
|
234
|
+
area.placeholder = ''
|
|
235
|
+
setDisabledReason(dialog.querySelector('[data-act="signoff-post"]'), FAILED_REASON)
|
|
236
|
+
}
|
|
168
237
|
return result
|
|
169
238
|
}
|
|
170
239
|
|
package/static/js/skin.js
CHANGED
|
@@ -11,7 +11,7 @@ export const SKINS = /** @type {const} */ (['terminal', 'github'])
|
|
|
11
11
|
/** @typedef {(typeof SKINS)[number]} Skin */
|
|
12
12
|
|
|
13
13
|
/** The look a page wears when the settings file says nothing. */
|
|
14
|
-
export const DEFAULT_SKIN = /** @type {Skin} */ ('
|
|
14
|
+
export const DEFAULT_SKIN = /** @type {Skin} */ ('github')
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* @param {unknown} value
|
|
@@ -37,7 +37,7 @@ export function readSkin(root) {
|
|
|
37
37
|
*/
|
|
38
38
|
export function nextSkin(skin) {
|
|
39
39
|
const next = SKINS[SKINS.indexOf(skin) + 1]
|
|
40
|
-
return next ??
|
|
40
|
+
return next ?? SKINS[0]
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -1,30 +1,28 @@
|
|
|
1
|
-
.
|
|
2
|
-
.chat-
|
|
1
|
+
/* The launcher stands in for the chat at every width; the script hides it while the chat is up. */
|
|
2
|
+
.chat-launcher {
|
|
3
|
+
display: block;
|
|
4
|
+
position: fixed;
|
|
5
|
+
right: 20px;
|
|
6
|
+
bottom: 20px;
|
|
7
|
+
z-index: 10;
|
|
8
|
+
padding: 14px 20px;
|
|
9
|
+
border: 1px solid var(--ink);
|
|
10
|
+
border-radius: 999px;
|
|
11
|
+
background: var(--accent);
|
|
12
|
+
color: var(--bg);
|
|
13
|
+
box-shadow: 0 4px 20px #0003;
|
|
14
|
+
font: inherit;
|
|
15
|
+
font-weight: 600;
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
}
|
|
18
|
+
.chat-launcher[hidden] {
|
|
3
19
|
display: none;
|
|
4
20
|
}
|
|
21
|
+
/* Always the last thing on its row, whether or not the header wraps. */
|
|
22
|
+
.chat-minimize {
|
|
23
|
+
margin-left: auto;
|
|
24
|
+
}
|
|
5
25
|
@media (max-width: 1360px) {
|
|
6
|
-
.chat-launcher {
|
|
7
|
-
display: block;
|
|
8
|
-
position: fixed;
|
|
9
|
-
right: 20px;
|
|
10
|
-
bottom: 20px;
|
|
11
|
-
z-index: 10;
|
|
12
|
-
padding: 14px 20px;
|
|
13
|
-
border: 1px solid var(--ink);
|
|
14
|
-
border-radius: 999px;
|
|
15
|
-
background: var(--accent);
|
|
16
|
-
color: var(--bg);
|
|
17
|
-
box-shadow: 0 4px 20px #0003;
|
|
18
|
-
font: inherit;
|
|
19
|
-
font-weight: 600;
|
|
20
|
-
cursor: pointer;
|
|
21
|
-
}
|
|
22
|
-
.chat-launcher[hidden] {
|
|
23
|
-
display: none;
|
|
24
|
-
}
|
|
25
|
-
.chat-minimize {
|
|
26
|
-
display: inline-block;
|
|
27
|
-
}
|
|
28
26
|
dialog.chat-dialog {
|
|
29
27
|
position: fixed;
|
|
30
28
|
inset: auto 20px 20px auto;
|
package/static/styles/chat.css
CHANGED
|
@@ -116,6 +116,12 @@
|
|
|
116
116
|
color: var(--purple);
|
|
117
117
|
border-color: currentColor;
|
|
118
118
|
}
|
|
119
|
+
/* The two local reviews: work with no pull request, so neither open nor closed. */
|
|
120
|
+
.pill.branch,
|
|
121
|
+
.pill.uncommitted {
|
|
122
|
+
color: var(--accent);
|
|
123
|
+
border-color: currentColor;
|
|
124
|
+
}
|
|
119
125
|
.pill.agent {
|
|
120
126
|
color: var(--fg);
|
|
121
127
|
border-color: var(--ink);
|
package/static/styles/header.css
CHANGED
|
@@ -46,6 +46,27 @@
|
|
|
46
46
|
gap: 4px 16px;
|
|
47
47
|
align-items: baseline;
|
|
48
48
|
}
|
|
49
|
+
/* How much code is hidden: a setting on the reading, so it sits with the progress line and not
|
|
50
|
+
among the commands, and says what it hides rather than leaving the reader to find out. */
|
|
51
|
+
.reading {
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-wrap: wrap;
|
|
54
|
+
gap: 4px 10px;
|
|
55
|
+
align-items: baseline;
|
|
56
|
+
margin-top: 14px;
|
|
57
|
+
}
|
|
58
|
+
.reading label {
|
|
59
|
+
font-size: 12px;
|
|
60
|
+
color: var(--fg-muted);
|
|
61
|
+
}
|
|
62
|
+
.reading select {
|
|
63
|
+
padding: 2px 6px;
|
|
64
|
+
font-size: 13px;
|
|
65
|
+
}
|
|
66
|
+
.fold-hint {
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
color: var(--fg-muted);
|
|
69
|
+
}
|
|
49
70
|
.stripe {
|
|
50
71
|
height: 6px;
|
|
51
72
|
background: var(--stripe-y);
|
package/static/styles/panels.css
CHANGED
|
@@ -88,6 +88,10 @@
|
|
|
88
88
|
box-shadow: 2px 2px 0 var(--shadow);
|
|
89
89
|
font-size: 13px;
|
|
90
90
|
}
|
|
91
|
+
/* The same bar for a canvas that still applies: a note, not a warning. */
|
|
92
|
+
.stale-bar.carried-over-bar {
|
|
93
|
+
background: var(--bg);
|
|
94
|
+
}
|
|
91
95
|
.notice {
|
|
92
96
|
margin: 10px 0 0;
|
|
93
97
|
padding: 6px 10px;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* The pending review: the bar that says one is open, and the drafts drawn on the diff. */
|
|
2
|
+
|
|
3
|
+
.pending-bar-host:empty {
|
|
4
|
+
display: none;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.pending-bar {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-wrap: wrap;
|
|
10
|
+
gap: 10px;
|
|
11
|
+
align-items: center;
|
|
12
|
+
margin: 10px 0 0;
|
|
13
|
+
padding: 10px 12px;
|
|
14
|
+
color: var(--fg);
|
|
15
|
+
background: var(--warn-bg);
|
|
16
|
+
/* The heavy left edge is what carries across the page: a review is open and unsent. */
|
|
17
|
+
border: 1px solid var(--warn);
|
|
18
|
+
border-left: 4px solid var(--warn);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* The dot pulses so an open review is noticed on a page the reader has scrolled far down. */
|
|
22
|
+
.pending-mark {
|
|
23
|
+
flex: none;
|
|
24
|
+
width: 10px;
|
|
25
|
+
height: 10px;
|
|
26
|
+
background: var(--warn);
|
|
27
|
+
border-radius: 50%;
|
|
28
|
+
animation: pending-pulse 2s ease-in-out infinite;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@keyframes pending-pulse {
|
|
32
|
+
0%,
|
|
33
|
+
100% {
|
|
34
|
+
opacity: 1;
|
|
35
|
+
}
|
|
36
|
+
50% {
|
|
37
|
+
opacity: 0.35;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media (prefers-reduced-motion: reduce) {
|
|
42
|
+
.pending-mark {
|
|
43
|
+
animation: none;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.pending-bar .pending-text {
|
|
48
|
+
flex: 1 1 260px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.pending-bar .pending-actions {
|
|
52
|
+
display: flex;
|
|
53
|
+
flex: none;
|
|
54
|
+
gap: 6px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* A draft on the diff reads as unfinished: dashed edge, and the badge that names it. */
|
|
58
|
+
.cmt.pending-cmt {
|
|
59
|
+
padding: 8px;
|
|
60
|
+
background: var(--warn-bg);
|
|
61
|
+
border: 1px dashed var(--warn);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.pending-av {
|
|
65
|
+
color: var(--fg-muted);
|
|
66
|
+
border-color: var(--warn);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* The avatar column covers the byline and the body, so the commands clear both. */
|
|
70
|
+
.cmt.pending-cmt .tbtns {
|
|
71
|
+
display: flex;
|
|
72
|
+
grid-column: 1 / -1;
|
|
73
|
+
gap: 6px;
|
|
74
|
+
margin-top: 6px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.pill.pending {
|
|
78
|
+
color: var(--bg);
|
|
79
|
+
background: var(--warn);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.signoff-pending:empty {
|
|
83
|
+
display: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.signoff-pending.has-pending {
|
|
87
|
+
padding: 6px 8px;
|
|
88
|
+
background: var(--warn-bg);
|
|
89
|
+
border-left: 3px solid var(--warn);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.earlier-pending {
|
|
93
|
+
margin-top: 8px;
|
|
94
|
+
padding: 8px 12px;
|
|
95
|
+
border: 1px solid var(--warn);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.earlier-draft + .earlier-draft {
|
|
99
|
+
margin-top: 12px;
|
|
100
|
+
padding-top: 12px;
|
|
101
|
+
border-top: 1px dashed var(--warn);
|
|
102
|
+
}
|
package/static/styles/review.css
CHANGED
|
@@ -153,7 +153,9 @@
|
|
|
153
153
|
[data-skin='github'] .pill.open,
|
|
154
154
|
[data-skin='github'] .pill.merged,
|
|
155
155
|
[data-skin='github'] .pill.closed,
|
|
156
|
-
[data-skin='github'] .pill.draft
|
|
156
|
+
[data-skin='github'] .pill.draft,
|
|
157
|
+
[data-skin='github'] .pill.branch,
|
|
158
|
+
[data-skin='github'] .pill.uncommitted {
|
|
157
159
|
border-color: transparent;
|
|
158
160
|
color: #ffffff;
|
|
159
161
|
}
|
|
@@ -169,6 +171,10 @@
|
|
|
169
171
|
[data-skin='github'] .pill.draft {
|
|
170
172
|
background: light-dark(#6e7781, #636e7b);
|
|
171
173
|
}
|
|
174
|
+
[data-skin='github'] .pill.branch,
|
|
175
|
+
[data-skin='github'] .pill.uncommitted {
|
|
176
|
+
background: light-dark(#0969da, #1f6feb);
|
|
177
|
+
}
|
|
172
178
|
[data-skin='github'] .pill.risk,
|
|
173
179
|
[data-skin='github'] .pill.kind,
|
|
174
180
|
[data-skin='github'] .pill.agent {
|
package/static/styles.css
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
@import './styles/panels.css';
|
|
11
11
|
@import './styles/responsive.css';
|
|
12
12
|
@import './styles/review-actions.css';
|
|
13
|
+
@import './styles/pending.css';
|
|
13
14
|
@import './styles/chat-tools.css';
|
|
14
15
|
@import './styles/skin-github.css';
|
|
15
16
|
@import './styles/chat-panel.css';
|