@leftoff/cli 0.9.18-beta.6 → 0.9.18-beta.8
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/CHANGELOG.md +8 -0
- package/bin/leftoff.js +569 -535
- package/docs/test-plan.md +43 -0
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/core.js +9 -5
- package/mod/hooks/register.js +22 -1
- package/mod/skills/walkthrough/SKILL.md +2 -2
- package/package.json +1 -1
- package/ui/app.css +74 -0
- package/ui/app.js +213 -27
- package/ui/ask.js +1 -1
- package/ui/decisions.js +69 -16
- package/ui/prs.js +15 -5
- package/ui/semdiff.js +12 -5
- package/ui/session-catchup.js +18 -2
- package/ui/summary-examples.js +1 -1
- package/ui/testplan.js +162 -0
- package/ui/viewport-fit.js +40 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Test plans from merged PRs
|
|
2
|
+
|
|
3
|
+
Merging 50 PRs shouldn't need 50 reviews to feel safe. A test plan turns what Leftoff already knows about a batch of merged work into one short manual script: what to click, what you should see, and why it's on the list, ordered by risk and fitted to the time you have.
|
|
4
|
+
|
|
5
|
+
## Where to start one
|
|
6
|
+
|
|
7
|
+
- A project's page: **Test plan**, next to Decisions and Ask.
|
|
8
|
+
- A repo's or a project's decision log: **Test plan**.
|
|
9
|
+
- The PRs page: pick merged PRs, then **Test plan for these**.
|
|
10
|
+
- The terminal: `leftoff testplan --project acme` (or `--repo web`), with `--since 14d` and `--minutes 15`, `30` or `60`. It prints Markdown.
|
|
11
|
+
|
|
12
|
+
The window is the last 7 days unless you say otherwise. The budget is 30 minutes; 15 and 60 are the other choices, picked for each plan (it isn't a setting).
|
|
13
|
+
|
|
14
|
+
## What it reads
|
|
15
|
+
|
|
16
|
+
Only Leftoff's own notes, plus what the PRs page already reads with `gh` where it's signed in:
|
|
17
|
+
|
|
18
|
+
- the PRs merged in the window that belong to the project or repo (or the ones you picked), each with its sessions;
|
|
19
|
+
- each PR's sessions' newest checkpoint (Goal, Progress, Decided);
|
|
20
|
+
- the walkthrough kept for its session: steps, Look / Assumption / Untested callouts, weak-test flags, spots, and the decisions a step follows or departs from. A PR with no kept walkthrough is not walked through on its own; it's read from its files, checkpoints and decisions and marked **no walkthrough**. **Walk through the N without one first** runs the usual walkthrough for each, one at a time;
|
|
21
|
+
- the decisions these PRs accepted, superseded or departed from.
|
|
22
|
+
|
|
23
|
+
## What it makes
|
|
24
|
+
|
|
25
|
+
Up to about 12 checks that fit the budget. Each has a title, where to go (a screen, command or endpoint), at most 5 steps, what you should see, why it's on the list (with chips for the PRs, steps, checkpoints and decisions it comes from), minutes, and its risk as a shape and a word (▲ High, ◆ Medium, ▽ Low). Checks are ordered by risk, then by how many PRs each covers; checks on the same flow sit together.
|
|
26
|
+
|
|
27
|
+
These always become checks, or are listed under **Not covered** with the reason: every Look and Untested callout on a Core step, every weak test (the test isn't trusted, so the behaviour gets a manual check), every decision a PR departs from or that was superseded in the window ("D-b replaced D-a on Nov 2: confirm the new behaviour, and that the old one is gone"), and every migration, config or auth change. A last line says how many of the PRs have at least one check.
|
|
28
|
+
|
|
29
|
+
Every citation is checked against what Leftoff gave the model. A tag it made up is dropped, and a check left with none is dropped too, with a note under **What was checked in the answer**.
|
|
30
|
+
|
|
31
|
+
## How it runs
|
|
32
|
+
|
|
33
|
+
The same way Ask does: Claude Code headless and read-only (Read, Grep and Glob, nothing that changes a file, the same budget and timeout), never a session on the board. Without Claude Code it uses your summary model with a smaller pack; with neither, it makes the plan from the rules above. Starting one from the board follows Ask's rule for a model run: this Mac's own browser, or a paired device with **Trust my paired devices** on.
|
|
34
|
+
|
|
35
|
+
## While you test
|
|
36
|
+
|
|
37
|
+
Tick a check **Checked** or **Failed**; "3 of 9 checked" shows at the top. A failed check takes a one-line note, which goes into the export, and offers **Propose a decision**: what you learned goes into the repo's decision log as a proposed correction, linked to the plan, for you to accept or not later. It is never accepted on its own.
|
|
38
|
+
|
|
39
|
+
**Copy as Markdown** gives a checklist with checkboxes; **Download .html** gives one self-contained file in the walkthrough export's look.
|
|
40
|
+
|
|
41
|
+
## Where it's kept
|
|
42
|
+
|
|
43
|
+
On the Mac that wrote it, in `~/.config/checkpoints/testplans/`, the newest 100 for 90 days, each deletable. Never in the synced folder. The list shows your other Macs' plans too, read over the peer link. Nothing is posted to GitHub or sent to anyone.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leftoff",
|
|
3
|
-
"version": "0.9.18-beta.
|
|
3
|
+
"version": "0.9.18-beta.8",
|
|
4
4
|
"description": "Leftoff (formerly Checkpoints): WTF was I working on? Tracks every Claude Code session on this machine, writes a 3-line checkpoint when one goes quiet, and adds /cp (checkpoint now), /cp-name and a /leftoff pane. Talks to the local Leftoff daemon on 127.0.0.1:7429 and falls back to writing markdown in ~/checkpoints.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "One Silly App"
|
package/mod/hooks/core.js
CHANGED
|
@@ -687,7 +687,7 @@ const SESSION_ORDER = ['id', 'title', 'title_source', 'machine', 'cwd', 'repo',
|
|
|
687
687
|
export const SESSION_EXTRA = ['inTurn', 'needsInput', 'backgroundTasks', 'turnsSinceCheckpoint', 'dormantStretch',
|
|
688
688
|
'finalCheckpointDone', 'firstPrompt', 'lastPreview', 'lastNotification', 'lastEventAt', 'statusChanges', 'folder',
|
|
689
689
|
'pid', 'tty', 'pidStartedAt', 'pidSource', 'term', 'closedAt', 'answers', 'lastNotificationMessage', 'needsInputAt', 'question', 'questionAt',
|
|
690
|
-
'startSource', 'historyTurns', 'healedAt', 'healedVia', 'archivedReason', 'backend', 'prLinks', 'prUnlinked', 'todos', 'files', 'startRepo', 'editedRepos', 'parked', 'nonInteractive', 'transcriptCwd', 'doubtedCheckpoints', 'liveLink', 'transcriptTurns', 'wasLive', 'headless', 'startedFrom', 'startProject', 'backgroundList', 'doneAt', 'recap', 'reviewed', 'wtLook', 'prShort']
|
|
690
|
+
'startSource', 'historyTurns', 'healedAt', 'healedVia', 'archivedReason', 'backend', 'prLinks', 'prUnlinked', 'todos', 'files', 'startRepo', 'editedRepos', 'parked', 'nonInteractive', 'transcriptCwd', 'doubtedCheckpoints', 'liveLink', 'transcriptTurns', 'wasLive', 'headless', 'startedFrom', 'startProject', 'backgroundList', 'doneAt', 'recap', 'reviewed', 'wtLook', 'prShort', 'decisionsGiven']
|
|
691
691
|
|
|
692
692
|
export const SESSION_FIELDS = ['id', 'title', 'titleSource', 'machine', 'cwd', 'repo', 'transcriptPath', 'model', 'surface', 'capture',
|
|
693
693
|
'status', 'startedAt', 'lastActivityAt', 'lastHeartbeatAt', 'endedAt', 'endReason', 'turns', 'lastCheckpointId',
|
|
@@ -1401,8 +1401,8 @@ const DECIDED_SECTION = [
|
|
|
1401
1401
|
]
|
|
1402
1402
|
const INFORMATIVE_SECTIONS = [
|
|
1403
1403
|
'Then these sections, each a heading line followed by "- " bullets (write "- none" when there is nothing; never invent):',
|
|
1404
|
-
'
|
|
1405
|
-
'- <
|
|
1404
|
+
'Approach notes:',
|
|
1405
|
+
'- <how the work is being done: the approach taken, a pattern or constraint it follows; one line each, at most 4. Not a choice between alternatives: those go only under Decided>',
|
|
1406
1406
|
'Open questions:',
|
|
1407
1407
|
'- <an unresolved question or uncertainty, at most 3>',
|
|
1408
1408
|
'Files:',
|
|
@@ -1625,7 +1625,9 @@ export function mergeDecided(found, fromModel, { earlier = [] } = {}) {
|
|
|
1625
1625
|
// The extra sections of an informative / long checkpoint, by the heading a
|
|
1626
1626
|
// model (or a checkpoint file) uses for them.
|
|
1627
1627
|
const SECTION_KEYS = {
|
|
1628
|
-
|
|
1628
|
+
// 1i (testplan): the section is "Approach notes" now (real choices go only under Decided); a checkpoint
|
|
1629
|
+
// written before, or a model still saying "Decisions:" / "Key decisions", reads into the same field.
|
|
1630
|
+
'approach notes': 'decisions', 'approach': 'decisions', 'decisions': 'decisions', 'key decisions': 'decisions',
|
|
1629
1631
|
'open questions': 'questions', 'questions': 'questions',
|
|
1630
1632
|
'files': 'files', 'files touched': 'files',
|
|
1631
1633
|
'didn\'t work': 'didntWork', 'didnt work': 'didntWork', 'did not work': 'didntWork', 'what didn\'t work': 'didntWork', 'what didnt work': 'didntWork', 'tried': 'didntWork',
|
|
@@ -1633,7 +1635,7 @@ const SECTION_KEYS = {
|
|
|
1633
1635
|
'next steps': 'nextSteps', 'next 3 steps': 'nextSteps',
|
|
1634
1636
|
'decided': 'decided', // 2026-11-03 decision memory
|
|
1635
1637
|
}
|
|
1636
|
-
export const SECTION_TITLES = { decisions: '
|
|
1638
|
+
export const SECTION_TITLES = { decisions: 'Approach notes', questions: 'Open questions', files: 'Files touched', didntWork: 'What didn\'t work', recap: 'Recap', nextSteps: 'Next steps' }
|
|
1637
1639
|
const SECTION_ORDER = ['decisions', 'questions', 'files', 'didntWork', 'recap', 'nextSteps']
|
|
1638
1640
|
const NONE_RE = /^(none|n\/a|nothing|-|—|none yet|no(ne)? (open )?questions?)\.?$/i
|
|
1639
1641
|
const LIMITS = { decided: DECIDED_MAX, decisions: 6, questions: 5, files: 10, didntWork: 5, nextSteps: 5 }
|
|
@@ -1684,6 +1686,8 @@ function parseSections(lines) {
|
|
|
1684
1686
|
else if (cur === 'decided') continue // a decision is one bullet line; loose prose under the heading is not one
|
|
1685
1687
|
else push(cur, line)
|
|
1686
1688
|
}
|
|
1689
|
+
// 1i: a line a model put under both Decided and the approach notes is a decision: it stays under Decided only
|
|
1690
|
+
if (ex.decided.length && ex.decisions.length) { const dn = new Set(ex.decided.map((d) => decisionNorm(d.text))); ex.decisions = ex.decisions.filter((t) => !dn.has(decisionNorm(String(t).split(/\s+·\s+|\s+[-—–]\s+because\b/)[0]))) }
|
|
1687
1691
|
for (const k of Object.keys(LIMITS)) ex[k] = ex[k].slice(0, LIMITS[k])
|
|
1688
1692
|
ex.recap = clip(ex.recap, 1600)
|
|
1689
1693
|
return ex
|
package/mod/hooks/register.js
CHANGED
|
@@ -32,7 +32,7 @@ const START_SKIP_GRACE_MS = 15000 // the same for SessionStart (a cold start can
|
|
|
32
32
|
const PERM_GRACE_MS = 1500 // how long a permission prompt tool.check saw waits for classic.PermissionRequest before the mod reports it
|
|
33
33
|
|
|
34
34
|
// Module state. $.state is not used: the pane redraws with $.ui.invalidate.
|
|
35
|
-
const MOD_VERSION = '0.9.18-beta.
|
|
35
|
+
const MOD_VERSION = '0.9.18-beta.8' // keep in step with .claude-plugin/plugin.json
|
|
36
36
|
|
|
37
37
|
const st = {
|
|
38
38
|
internal: false,
|
|
@@ -365,6 +365,8 @@ const COMMANDS = [
|
|
|
365
365
|
{ key: 'todo', names: ['cp-todo', 'leftoff-todo'], description: 'Leftoff: save the checklist Claude just wrote as a list you can tick off (board, phone, or here)', argumentHint: '[name] [--pick [N]]', immediate: true },
|
|
366
366
|
// v0.8.2: checkpoint, mark the session Parked (not Closed when it exits), then close it.
|
|
367
367
|
{ key: 'park', names: ['cp-park', 'leftoff-park'], description: 'Leftoff: checkpoint and park this session to come back to (your note becomes the Next line), then exit', argumentHint: '[note]', immediate: false },
|
|
368
|
+
// 2026-11-03 (1b): the repo's accepted decisions into the conversation on demand (when SessionStart was skipped, or after /clear).
|
|
369
|
+
{ key: 'decisions', names: ['cp-decisions', 'leftoff-decisions'], description: 'Leftoff: give Claude this repo\'s accepted decisions (the same list a new session starts with)', immediate: false },
|
|
368
370
|
{ key: 'done', names: ['cp-done', 'leftoff-done'], description: 'Leftoff: tick items on this session\'s list (/cp-done 3, /cp-done 2 4, /cp-done undo 3)', argumentHint: '<n…> [in <list>]', immediate: true },
|
|
369
371
|
]
|
|
370
372
|
|
|
@@ -491,6 +493,23 @@ async function decisionContext($, e) {
|
|
|
491
493
|
return t ? t.slice(0, 2000) : null
|
|
492
494
|
}
|
|
493
495
|
|
|
496
|
+
// 1b /cp-decisions: the same text SessionStart adds (src/decisions.js decisionContextText, from the daemon),
|
|
497
|
+
// shown to you and handed to Claude as the command's context. The daemon notes that this session was given them.
|
|
498
|
+
// Runs where the classic hooks are skipped: a command is not a classic event (Claude Code 2.1.318's Security
|
|
499
|
+
// default hooks classic.* but not command.run).
|
|
500
|
+
async function decisionsCommand($) {
|
|
501
|
+
if (st.internal) return { text: 'Leftoff: not in Leftoff\'s own runs.' }
|
|
502
|
+
let cwd = null
|
|
503
|
+
try { cwd = await $.session.cwd() } catch {}
|
|
504
|
+
const sid = st.sessionId || ''
|
|
505
|
+
const r = await api($, 'GET', '/v1/decisions/context?via=command&session=' + encodeURIComponent(sid) + (cwd ? '&cwd=' + encodeURIComponent(cwd) : ''), undefined, 2000)
|
|
506
|
+
if (!r || !r.ok || !r.json) return { text: 'Leftoff: the daemon didn\'t answer, so this repo\'s decisions couldn\'t be read (leftoff status).' }
|
|
507
|
+
const t = typeof r.json.context === 'string' ? r.json.context.trim().slice(0, 2000) : ''
|
|
508
|
+
if (!t) return { text: r.json.repo ? 'Leftoff: this repo has no accepted decisions yet. Review proposed ones on the board.' : 'Leftoff: this folder isn\'t a repo Leftoff keeps a decision log for.' }
|
|
509
|
+
const n = Number(r.json.count) || t.split('\n').filter((l) => l.startsWith('- ')).length
|
|
510
|
+
return { text: 'Leftoff: gave Claude this repo\'s ' + n + ' accepted decision' + (n === 1 ? '' : 's') + ':\n' + t, context: [t] }
|
|
511
|
+
}
|
|
512
|
+
|
|
494
513
|
// ------------------------------------------------------------ lists (v0.5.1)
|
|
495
514
|
|
|
496
515
|
// This session's saved lists, when they changed since Claude last heard
|
|
@@ -1320,6 +1339,8 @@ export function register(on, options) {
|
|
|
1320
1339
|
on('command.run', { command: 'leftoff-park' }, async ($, e) => parkCommand($, e.args))
|
|
1321
1340
|
|
|
1322
1341
|
on('command.run', { command: 'checkpoints' }, async ($, e) => paneCommand($, e.args))
|
|
1342
|
+
on('command.run', { command: 'cp-decisions' }, async ($) => decisionsCommand($))
|
|
1343
|
+
on('command.run', { command: 'leftoff-decisions' }, async ($) => decisionsCommand($))
|
|
1323
1344
|
|
|
1324
1345
|
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
|
|
1325
1346
|
if (e.requestId !== PANE_ID) return next(e)
|
|
@@ -14,9 +14,9 @@ How to walk through a change:
|
|
|
14
14
|
3. Mark what matters. A step that changes behaviour is Core; a small follow-on (config, docs, a helper, a rename) is Supporting. Tests that only follow along (updated to match the change), imports and formatting are mechanical and need no story.
|
|
15
15
|
- A test that doesn't really test anything is never mechanical and never safe to skip. That is a test that asserts nothing meaningful or asserts a tautology, mocks the very thing under test, snapshots its own output, had an assertion loosened or deleted to make it pass, or was skipped or disabled. Put it in a step and give it a look callout at its line saying why.
|
|
16
16
|
4. Point out what deserves a second look, at the exact line: something risky or subtle (look), a guess the code makes (assumption), or behaviour no test covers (untested). Be honest about the parts you were unsure of.
|
|
17
|
-
5. Cite the decisions behind a step. When Leftoff lists the repo's accepted decisions (D-7f3a: …), a step may name the ones it
|
|
17
|
+
5. Cite the decisions behind a step. When Leftoff lists the repo's accepted decisions (D-7f3a: …), and this session's own proposed ones (marked proposed), a step may name the ones it relates to and how: "decisions": [{"id": "D-7f3a", "rel": "follows"}] or [{"id": "D-7f3a", "rel": "departs"}] (a plain "D-7f3a" means follows), using only ids from those lists. A step that reverses or departs from a decision gets a look callout at its line naming the decision and how it departs.
|
|
18
18
|
6. Never paste code. Point at it.
|
|
19
19
|
|
|
20
|
-
When Leftoff asks (the request lists hunks H1…Hn and a JSON shape): reply with that JSON object only, no prose and no code fences. Every ref is a hunk id from the list ("H3", "H3:new 14-19", "H3:old 7"); never invent ids or line numbers; put every hunk in a step or in mechanical. A step's "decisions" are ids from the decisions listed, never made up. You may read files with Read, Grep and Glob to check details; change nothing.
|
|
20
|
+
When Leftoff asks (the request lists hunks H1…Hn and a JSON shape): reply with that JSON object only, no prose and no code fences. Every ref is a hunk id from the list ("H3", "H3:new 14-19", "H3:old 7"); never invent ids or line numbers; put every hunk in a step or in mechanical. A step's "decisions" are ids from the decisions listed, never made up, each with "rel" "follows" or "departs". You may read files with Read, Grep and Glob to check details; change nothing.
|
|
21
21
|
|
|
22
22
|
When the developer asks in the terminal: give the same walkthrough as a short numbered list (title, why, file:line for each step, then what to look at closely).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leftoff/cli",
|
|
3
|
-
"version": "0.9.18-beta.
|
|
3
|
+
"version": "0.9.18-beta.8",
|
|
4
4
|
"description": "Leftoff: WTF was I working on? Tracks every Claude Code session, writes a 3-line checkpoint when one goes quiet, and gives you a daily digest and a board, on your Mac and (optionally) your phone.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/ui/app.css
CHANGED
|
@@ -2495,6 +2495,7 @@ mark.tx, .co-snip mark, .tx-snip mark, .tx-msg-text mark { background: var(--tx-
|
|
|
2495
2495
|
.composer .att-chips[hidden] { display: none; }
|
|
2496
2496
|
.composer.att-drop { border-color: var(--accent); box-shadow: 0 0 0 3px var(--focus); background: color-mix(in srgb, var(--accent) 7%, var(--card2)); }
|
|
2497
2497
|
.att-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; overflow: hidden; clip: rect(0 0 0 0); }
|
|
2498
|
+
.att-picker { position: fixed; left: 0; bottom: 0; } /* 0.9.18: the paperclip's picker, outside the page so a redraw can't drop what you picked */
|
|
2498
2499
|
.composer-attach { flex: none; width: 44px; height: 44px; margin: 0; padding: 0; border: 0; background: transparent; display: grid; place-items: center; cursor: pointer; color: var(--muted); -webkit-tap-highlight-color: transparent; }
|
|
2499
2500
|
.composer-clip { width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; transition: background-color .15s, color .15s; }
|
|
2500
2501
|
.composer-clip .ic { width: 19px; height: 19px; }
|
|
@@ -5441,6 +5442,20 @@ code .file-ref-off { font-size: inherit; }
|
|
|
5441
5442
|
.dec-why, .dec-alt, .dec-src { font-size: 12.5px; color: var(--text2); margin-top: 2px; }
|
|
5442
5443
|
.dec-src { color: var(--muted, var(--text2)); }
|
|
5443
5444
|
.dec-st { margin-left: 6px; font-size: 12px; color: var(--text2); white-space: nowrap; }
|
|
5445
|
+
.dec-rc { display: inline-flex; align-items: center; gap: 2px; font-size: 12px; color: var(--text2); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
5446
|
+
a.dec-rc { min-height: 44px; text-decoration: none; }
|
|
5447
|
+
a.dec-rc:hover .dec-rc-w { text-decoration: underline; }
|
|
5448
|
+
.pchip-rc { margin-left: 6px; }
|
|
5449
|
+
.proj-review-slot:empty { display: none; }
|
|
5450
|
+
.scu-decs { margin-top: 10px; }
|
|
5451
|
+
.scu-dec-h { font-size: 12.5px; font-weight: 600; color: var(--text2); margin: 0 0 4px; }
|
|
5452
|
+
.scu-dec-l { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; font-size: 13.5px; }
|
|
5453
|
+
.scu-dec-l .dec-st { margin: 0 4px 0 0; }
|
|
5454
|
+
.dec-given { margin: 6px 0 2px; font-size: 12.5px; color: var(--text2); }
|
|
5455
|
+
.dec-given-i { display: inline-block; width: 1em; text-align: center; }
|
|
5456
|
+
.dec-missed { color: var(--text); }
|
|
5457
|
+
.dec-given-how { color: var(--text2); }
|
|
5458
|
+
.dec-given code { font-size: 12px; }
|
|
5444
5459
|
.dec-open { display: inline-flex; align-items: center; min-height: 44px; font-size: 13px; }
|
|
5445
5460
|
|
|
5446
5461
|
.dec-page .dec-head h1 { margin: 4px 0 2px; }
|
|
@@ -5459,6 +5474,9 @@ code .file-ref-off { font-size: inherit; }
|
|
|
5459
5474
|
.dec-meta { font-size: 12.5px; color: var(--text2); margin-top: 4px; }
|
|
5460
5475
|
.dec-chains { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 4px; font-size: 12.5px; color: var(--text2); }
|
|
5461
5476
|
.dec-chip { font: 600 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text); background: transparent; border: 1px solid var(--line); border-radius: 6px; padding: 4px 6px; min-height: 24px; cursor: pointer; position: relative; }
|
|
5477
|
+
.wt-dec-rel { font: 600 11.5px/1 system-ui, sans-serif; margin-left: 6px; padding: 2px 5px; border: 1px solid var(--line); border-radius: 4px; color: var(--text2); white-space: nowrap; }
|
|
5478
|
+
.wt-dec-dep { border-style: solid; border-color: var(--text2); color: var(--text); }
|
|
5479
|
+
.wt-dec-prop { border-style: dashed; }
|
|
5462
5480
|
.dec-chip::after { content: ''; position: absolute; inset: -10px -4px; }
|
|
5463
5481
|
.dec-chip:hover { border-color: var(--text2); }
|
|
5464
5482
|
.dec-sug { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin-top: 8px; padding: 8px 10px; border: 1px dashed var(--line); border-radius: 8px; font-size: 13px; }
|
|
@@ -5492,3 +5510,59 @@ code .file-ref-off { font-size: inherit; }
|
|
|
5492
5510
|
.dec-ctl .sask-opt { border: 1px solid var(--line); border-radius: 8px; padding: 0 12px; background: transparent; color: var(--text2); cursor: pointer; }
|
|
5493
5511
|
.dec-ctl .sask-opt[aria-pressed="true"] { color: var(--text); border-color: var(--line-strong, var(--text2)); }
|
|
5494
5512
|
@media (min-width: 700px) { .dv-isdec .dv-sheet { max-width: 760px; height: auto; max-height: 80vh; margin-left: auto; margin-right: auto; } }
|
|
5513
|
+
|
|
5514
|
+
/* testplan Part 3: the test plan page (ui/testplan.js) */
|
|
5515
|
+
.tp-page .tp-head h1 { margin: 4px 0 2px; }
|
|
5516
|
+
.tp-budget { display: inline-flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 12px; }
|
|
5517
|
+
.tp-budget .sask-opt, .tp-go .btn, .tp-exp .btn, .tp-acts .btn, .tp-open { min-height: 44px; }
|
|
5518
|
+
.tp-budget .sask-opt { border: 1px solid var(--line); border-radius: 8px; padding: 0 14px; background: transparent; color: var(--text2); cursor: pointer; font: inherit; }
|
|
5519
|
+
.tp-budget .sask-opt[aria-pressed="true"] { color: var(--text); border-color: var(--line-strong, var(--text2)); font-weight: 600; }
|
|
5520
|
+
.tp-prl, .tp-recent, .tp-ncl { list-style: none; margin: 8px 0; padding: 0; display: grid; gap: 6px; font-size: 14px; }
|
|
5521
|
+
.tp-prl a, .tp-recent a { overflow-wrap: anywhere; }
|
|
5522
|
+
.tp-wt, .tp-nowt { font-size: 12px; color: var(--text2); white-space: nowrap; }
|
|
5523
|
+
.tp-go { margin: 12px 0; }
|
|
5524
|
+
.tp-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin: 8px 0 4px; }
|
|
5525
|
+
.tp-prog { font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
5526
|
+
.tp-cov { color: var(--text2); font-size: 13px; }
|
|
5527
|
+
.tp-exp { display: inline-flex; flex-wrap: wrap; gap: 8px; margin-left: auto; }
|
|
5528
|
+
.tp-by { font-size: 12.5px; margin: 0 0 8px; }
|
|
5529
|
+
.tp-list { list-style: none; margin: 12px 0 0; padding: 0; display: grid; gap: 12px; }
|
|
5530
|
+
.tp-check { border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; background: var(--bg); }
|
|
5531
|
+
.tp-check.tp-done { border-style: solid; opacity: .82; }
|
|
5532
|
+
.tp-check.tp-failed { border-width: 2px; border-color: var(--text2); }
|
|
5533
|
+
.tp-ch { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
|
|
5534
|
+
.tp-n { font: 600 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text2); }
|
|
5535
|
+
.tp-title { font-size: 16px; margin: 0; flex: 1 1 260px; overflow-wrap: anywhere; }
|
|
5536
|
+
.tp-risk { font-size: 12px; white-space: nowrap; color: var(--text2); border: 1px solid var(--line); border-radius: 999px; padding: 2px 8px; }
|
|
5537
|
+
.tp-risk-high { color: var(--text); border-color: var(--text2); font-weight: 600; }
|
|
5538
|
+
.tp-min { font-size: 12px; color: var(--text2); font-variant-numeric: tabular-nums; }
|
|
5539
|
+
.tp-where, .tp-expect, .tp-why { margin: 6px 0 0; font-size: 14px; line-height: 1.45; overflow-wrap: anywhere; }
|
|
5540
|
+
.tp-k { display: inline-block; min-width: 96px; font-size: 12px; color: var(--text2); }
|
|
5541
|
+
.tp-steps { margin: 6px 0 0; padding-left: 22px; font-size: 14px; line-height: 1.45; }
|
|
5542
|
+
.tp-cites { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
5543
|
+
.tp-cite { font: 600 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text); border: 1px solid var(--line); border-radius: 6px; padding: 5px 7px; text-decoration: none; position: relative; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
5544
|
+
a.tp-cite::after { content: ''; position: absolute; inset: -10px -4px; }
|
|
5545
|
+
.tp-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; align-items: center; }
|
|
5546
|
+
.tp-tick[aria-pressed="true"] { font-weight: 600; border-color: var(--text); }
|
|
5547
|
+
.tp-clear { min-height: 44px; }
|
|
5548
|
+
.tp-note { display: grid; gap: 8px; margin-top: 10px; max-width: 640px; }
|
|
5549
|
+
.tp-note-l { display: grid; gap: 4px; font-size: 12.5px; color: var(--text2); }
|
|
5550
|
+
.tp-propf { margin-top: 10px; }
|
|
5551
|
+
.tp-prop { display: inline-flex; align-items: center; min-height: 44px; }
|
|
5552
|
+
.tp-nc-w { overflow-wrap: anywhere; }
|
|
5553
|
+
.tp-notes { margin: 12px 0; font-size: 13px; color: var(--text2); }
|
|
5554
|
+
.tp-notes summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; }
|
|
5555
|
+
.tp-del .linkish { min-height: 44px; }
|
|
5556
|
+
.tp-wait { display: flex; align-items: center; gap: 10px; padding: 16px 0; color: var(--text2); }
|
|
5557
|
+
.tp-spin { width: 14px; height: 14px; border: 2px solid var(--line); border-top-color: var(--text2); border-radius: 50%; animation: tp-spin 1s linear infinite; }
|
|
5558
|
+
@keyframes tp-spin { to { transform: rotate(360deg); } }
|
|
5559
|
+
@media (prefers-reduced-motion: reduce) { .tp-spin { animation: none; } }
|
|
5560
|
+
@media (max-width: 600px) { .tp-exp { margin-left: 0; } .tp-k { display: block; min-width: 0; } }
|
|
5561
|
+
/* testplan Part 3: picking merged PRs on the PRs page */
|
|
5562
|
+
.prrow.pickable { display: flex; gap: 8px; align-items: flex-start; }
|
|
5563
|
+
.pr-pick { display: inline-flex; flex-direction: column; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; border: 0; background: transparent; color: var(--text2); cursor: pointer; font: inherit; font-size: 18px; line-height: 1; padding: 0; }
|
|
5564
|
+
.pr-pick-w { font-size: 11px; margin-top: 2px; }
|
|
5565
|
+
.pr-pick[aria-pressed="true"] { color: var(--text); }
|
|
5566
|
+
.prs-picks { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 8px 12px; margin: 0 0 10px; border: 1px solid var(--line); border-radius: 10px; background: var(--bg); }
|
|
5567
|
+
.prs-picks .btn, .prs-picks .linkish { min-height: 44px; }
|
|
5568
|
+
.wt-dec .wt-dec-id { white-space: nowrap; flex: none; }
|