@lovek4d/kickflip 0.7.8 → 0.8.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/CONTEXT.md +4 -4
- package/SKILL.md +2 -2
- package/bin/install.mjs +3 -3
- package/board/bin/board.mjs +1 -1
- package/board/domain.mjs +7 -7
- package/board/headless.mjs +8 -8
- package/board/hub-lines.mjs +1 -1
- package/board/public/app-needs.js +1 -1
- package/board/public/app.html +2 -0
- package/board/public/app.js +1 -0
- package/board/public/col-resize.js +73 -0
- package/board/public/prototype.html +1 -0
- package/board/public/sidebar.css +8 -2
- package/board/public/style.css +32 -10
- package/board/public/ui-sidebar.js +11 -3
- package/board/public/ui-threads.js +1 -0
- package/board/render-check.mjs +8 -8
- package/board/server.mjs +18 -18
- package/board/store.mjs +29 -29
- package/board/tickets.mjs +3 -3
- package/hub/boards.mjs +4 -4
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +11 -11
- package/hub/pr-report.mjs +2 -2
- package/hub/schedule.mjs +1 -1
- package/package.json +1 -1
package/board/headless.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
var Q=Object.defineProperty;var l=(t,e)=>Q(t,"name",{value:e,configurable:!0});import X from"node:os";import{BRIEF_LINE_LIMIT as J}from"./hub-lines.mjs";import
|
|
2
|
-
`)}l(ue,"builderBrief");function
|
|
3
|
-
`)}l(
|
|
4
|
-
`)}l(
|
|
5
|
-
`)}l(pe,"plannerBrief");function fe({route:t,key:e,ref:r,title:i,notes:n,thread:a,repo:h,agent:u,rules:p=[],links:c=[],text:d,files:o=[]}){const s
|
|
6
|
-
`)}l(fe,"visualBrief");function we({route:t,key:e,ref:r,title:i,notes:n,thread:a,agent:h,task:u,writes:p,cwd:c,rules:d=[],links:o=[]}){const s
|
|
7
|
-
`)}l(we,"helperBrief");const me=6e4;function be(t,{starts:e=W}={}){const r=t.filter(n=>n.pid&&n.at).map(n=>n.pid),i=r.length&&e(r)||new Map;return t.filter(n=>{const a=i.get(n.pid);return a!==void 0&&a<=Date.parse(n.at)&&a>=Date.parse(n.at)-me})}l(be,"launchedAgents");function
|
|
8
|
-
`).map(o=>/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/.exec(o)).filter(Boolean).map(([,o,s,
|
|
1
|
+
var Q=Object.defineProperty;var l=(t,e)=>Q(t,"name",{value:e,configurable:!0});import X from"node:os";import{BRIEF_LINE_LIMIT as J}from"./hub-lines.mjs";import k from"node:path";import{fileURLToPath as Z}from"node:url";import{launchAgent as P}from"./agents.mjs";import{processStarts as W,ps as C}from"../hub/live.mjs";import{boardAt as M}from"../hub/boards.mjs";import{readHubRules as ee}from"../hub/rules.mjs";import{builderRepo as O}from"./worktree.mjs";import{appendStatus as te,BoardError as oe,briefFacts as L,readBoardRules as re,recordedSession as ne,recordSession as E}from"./store.mjs";import{HELPER as se,plans as ae,VISUAL as ie}from"./domain.mjs";const he=Z(new URL("./bin/board.mjs",import.meta.url)),g=l(t=>`node ${he} --site ${t}`,"cli"),F=l(t=>`git -c core.commentChar=';' rebase ${t}`,"rebase"),de="a draft .md of your own outside the board's data folder (such as in /tmp)",le=l((t,e,r)=>`\`${t} item notes ${e} --heading "${r}" --file <draft.md>\``,"notesCommand"),T=l((t,e="Rules from the hub and this board:")=>t.length?[e,...t.map(r=>`- ${r}`),""]:[],"ruleLines"),x=l(t=>t.length?["Related cards: read their notes for context; don't build their scope.",...t.map(e=>`- ${e.ref} "${e.title}" (${e.relation}, ${e.lane}): ${e.notes?`notes ${e.notes}`:"no notes"}, thread ${e.thread}`),""]:[],"linkLines"),I=l(t=>`The card's thread, which holds the owner's messages (who: love): ${t}. Read it live.`,"threadLine"),Y=l(({route:t,key:e,worktree:r,base:i})=>[`- With no unverified calls, done finishes the card itself: it rebases onto ${i}, reruns the full test suite, fast-forwards ${i} and closes the card (on a board with pull requests it pushes the branch and opens a draft PR instead; with Auto-merge off it stops before the fast-forward, and the card waits for the owner's Merge).`,`- On a conflict it undoes its rebase and names the files: run \`${F(i)}\` in ${r}, resolve each conflict keeping both sides' intent, continue with \`git -c core.commentChar=';' rebase --continue\`, rerun the tests, and run done again.`,"- On failing tests, fix them, commit, and run done again.","- While another card is landing, done waits its turn and then lands; leave it running.","- Once a reviewer has the card's PR link, done refuses without `--updated \"<what changed>\"`, which goes in that link's Slack thread, or `--updated none` for a change only to comments, names, docs or tests.",`- Finishing never goes to the orchestrator. If done refuses for a reason you can't fix, record it with \`${g(t)} builder call ${e} "<why>"\` and run done again: with the call it hands the card back.`],"finishSteps"),B=l(({route:t,key:e})=>`\`${g(t)} builder done ${e} --text "<what you checked>"\``,"finishCommand"),ce=l((t,e)=>[`5. To run a separate part of the work in parallel, ask the board for a helper: \`${t} agent launch ${e} --name <short-name> --task "<the whole task, with the files and what to hand back>"\`, adding --writes when it changes code or runs tests (it gets its own worktree off your branch, and waits for a free implementer slot). Then \`${t} agent wait ${e} --name <short-name>\`, which prints its hand-back; it returns within 10 minutes, so run it again while it says still running, and do the task yourself if it says the helper exited. Merge a writing helper's work with \`${t} agent merge ${e} --name <short-name>\` before you test and finish.`],"helperSteps");function ue({route:t,key:e,ref:r,title:i,notes:n,thread:a,worktree:h,base:u,rules:p=[],links:c=[]}){const d=g(t);return[`You are the headless builder for card ${r} (${e}, "${i}") on the kickflip board /${t}. The board launched you; nobody watches this session, so never wait on a person. Work only in ${h}. Never restart the hub or a board server, and never push.`,"",`1. Run \`${d} builder start ${e} --kind build\`. If it refuses, stop.`,`2. Report progress with \`${d} builder progress ${e} --phase reading|building|testing|checking|landing "<plain, 80 characters or fewer>"\`, at least every 10 minutes. The board adds the phase word; don't start the text with it. The phases:`," - reading: the notes and the code, until the first test is written."," - building: the tests and the code, the ADR and docs, and the commit."," - testing: the full suite run before you finish."," - checking: your own review of the diff before you hand back; the Orchestrator's review of your calls continues on the same 4th bar."," - landing: set by the finish step itself; you never report it.",`3. Decide, record the call (step 4) and carry on: the orchestrator reviews your calls after the build. Ask it only when you are blocked, meaning every way forward would likely be thrown away: the spec contradicts itself or the code, a rule needs the owner's go before code is written, or the answer is a fact only the orchestrator has. Then run \`${d} builder ask ${e} "<question>"\`, do any work the answer can't change, and wait with \`${d} builder asks ${e} --wait\`, which returns within 10 minutes, so run it again while it says still waiting. If no answer comes in 30 minutes, record the question as a call, "Question: <question>", and hand back.`,`4. Record each judgment call the notes leave open with \`${d} builder call ${e} "<call>"\` instead of guessing silently or asking. A problem you find outside your card is a call "Found: <what>" for the orchestrator to file at review; ask about it at once only when it stalls other cards' builds or lands.`,...ce(d,e),"",n?`Spec: the card's notes, ${n}, all sections, where later sections override earlier ones.`:"The card has no notes file: build from its title, and record what you had to decide as calls.",`${I(a)} Read it with the notes: a message of the owner's is an instruction, but where it contradicts the notes, the notes win; that contradiction counts as blocked (step 3), so ask with \`${d} builder ask ${e} "<question>"\` and wait as step 3 says. Read the thread again in checking, before you finish, for anything the owner wrote during the build.`,"",...x(c),...T(p),"Write tests first, and run the full suite once, in the foreground, at the end. Where the repo keeps ADRs, write one named with a timestamp: docs/adr/YYYY-MM-DD-HHMM-<slug>.md.",`Commit with \`git -c core.commentChar=';' commit -F <file>\`, the subject "${r}: <what the card now does>".`,"",`To finish, run ${B({route:t,key:e})}.`,"- If you recorded a judgment call, it hands the card back for the orchestrator to verify your calls; stop there: the orchestrator's verification finishes the card.",...Y({route:t,key:e,worktree:h,base:u}),"","Never push."].join(`
|
|
2
|
+
`)}l(ue,"builderBrief");function H({route:t,key:e,ref:r,title:i,thread:n,worktree:a,base:h,answers:u=[],resumed:p=!1,why:c}){const d=g(t);return[p?`The board resumes you, the builder of card ${r} (${e}, "${i}") on the kickflip board /${t}, to land your build: ${c??"the orchestrator verified your judgment calls"}, and it waits only to land. Nobody watches this session, so never wait on a person. Work only in ${a}, never restart the hub or a board server, and never push.`:`You are the headless lander for card ${r} (${e}, "${i}") on the kickflip board /${t}. Its build is done and the orchestrator verified its judgment calls; it waits only to land. Nobody watches this session, so never wait on a person. Work only in ${a}, never restart the hub or a board server, and never push.`,"",`1. Run \`${d} builder start ${e} --kind build --label Lander --phase landing --progress "taking up the verified build"\`. If it refuses, stop.`,`2. Report progress with \`${d} builder progress ${e} --phase landing "<plain, 80 characters or fewer>"\`. The board adds the phase word; don't start the text with it.`,...u.length?["3. The owner answered the builder's deferred asks. Where an answer changes the build, change it, with its tests, and commit:",...u.map(o=>`- ${o.ask} "${o.question}": ${o.answer}`)]:[],"",I(n),"",`To finish, run ${B({route:t,key:e})}.`,...Y({route:t,key:e,worktree:a,base:h}),"","Never push."].join(`
|
|
3
|
+
`)}l(H,"landerBrief");function K({route:t,key:e,ref:r,title:i,thread:n,worktree:a,base:h,pr:u,resumed:p=!1}){const c=g(t);return[...p?[`The board resumes you, the builder of card ${r}, for this run: the lines below are your brief for it.`]:[],`You are the headless rebase run for card ${r} (${e}, "${i}") on the kickflip board /${t}. Its PR #${u} conflicts with ${h}; rebase its branch onto ${h} so the PR is clean again. Nobody watches this session, so never wait on a person. Work only in ${a}, never restart the hub or a board server, and never push.`,"",`1. Run \`${c} builder start ${e} --kind build --label Rebase --phase building --progress "Rebase run started \xB7 fetching ${h}"\`. If it refuses, stop.`,`2. Run \`git fetch origin\`, then \`${F(`origin/${h}`)}\`. Resolve each conflict keeping both sides' intent, \`git add\` it and continue with \`git -c core.commentChar=';' rebase --continue\`. Report each step with \`${c} builder progress ${e} --phase building "<line>"\`, the board adds the phase word, in these words: "Rebasing onto ${h} \xB7 resolving <n> files", then "Rebasing onto ${h} \xB7 suite running".`,"3. Run the full test suite once, in the foreground. Fix what the rebase broke, and commit each fix with `git -c core.commentChar=';' commit -F <file>`. A push with commits of its own is no longer rebase-only, so a merge after an approval won't count it as trivial.",`4. If a conflict or a failing test is past what you can resolve with confidence, run \`git -c core.commentChar=';' rebase --abort\` if a rebase is under way, record it with \`${c} builder call ${e} "Rebase failed in <the files or tests> \xB7 <why>"\`, and run the finish step: with the call it hands the card to the orchestrator.`,"",I(n),"",`To finish, run ${B({route:t,key:e})}, the text saying what you resolved. It reruns the suite and pushes the branch to PR #${u} with --force-with-lease; the PR stays as it is.`,"- If it refuses because someone pushed to the branch since your fetch, fetch and rebase again (step 2), then run done again.","- On failing tests, fix them, commit, and run done again.","","Never push."].join(`
|
|
4
|
+
`)}l(K,"rebaseBrief");function pe({route:t,key:e,ref:r,title:i,notes:n,thread:a,kind:h,repo:u,rules:p=[],links:c=[],text:d,files:o=[],question:s,note:m,choice:f}){const w=g(t),b=h==="grill";return[`You are the headless ${b?"grill":"prototype"} planner for card ${r} (${e}, "${i}") on the kickflip board /${t}. The board launched you; nobody watches this session, so never wait on a person: the owner answers on the board, and a later planner or builder picks up from their answers. Read and run in ${u}; write no output code ${b?"there":`in ${u} itself`}, never restart the hub or a board server, and never push.`,"",`1. Run \`${w} builder start ${e} --kind ${h}\`. If it refuses, stop.`,`2. Gather the context yourself: ${n?`the card's notes, ${n}, all sections, where later sections override earlier ones; `:"the card has no notes file yet; "}its thread, ${a}, the repo's docs/adr and CONTEXT.md, and the cards its notes and thread link.`,`3. Report progress with \`${w} builder progress ${e} "<plain, 80 characters or fewer>"\` at least every 10 minutes.`,b?`4. Find every decision the card leaves open: read the code it touches and answer what the code answers yourself. Post each decision left as a question for the owner with \`${w} question add --file <json>\` on topic item:${e}, one per decision, your recommendation first. Once you finish, the orchestrator screens them before the owner sees them, settling the ones the board already answers and passing the owner the rest. Invoke the grilling skill with the Skill tool and run it as one round: its questions go to the board with question add, never into the chat, and you never wait for an answer; after the round, finish. (With no Skill tool, follow this step as written.) When the code shows the card has separable parts, each a build of its own, propose splitting it into a wave of smaller cards as one question ahead of the prototype question: its options "Split as proposed" and "Keep as one card", your recommendation first. Slice it as ~/.claude/skills/to-tickets/SKILL.md step 3, "Draft vertical slices", says (read that step; you can't invoke the skill, and skip its quiz and publish steps). The first card is the thinnest end-to-end path. The question's context lists each proposed card with its title, its scope, the files it touches and the earlier proposed cards it waits on, or none: a card waits on an earlier one only when it builds on that card or shares its seam (the same function, component, CLI command or doc paragraph); sharing only a file waits on nothing. Write the same breakdown in your notes section. On Split the orchestrator files the cards when the card stages; never file them yourself. End every grill with one more question, "Prototype round first?", whose options are yes and no, with "prototype_gate": true in its \`question add --file\` entry: once the owner has answered every question the board moves the card to prototyping on yes, or to staging on no, so nothing else moves it. Recommend yes when the card changes the hub's or a board's UI, no otherwise. Ask it even when it is your only question.`:`4. Invoke the prototype skill with the Skill tool and build the round as take files, one per option: record them with \`${w} prototype add <topic> --card ${e} --take <K>:<label> \u2026\`, write the take files it prints, built from the repo's existing components, helpers and scripts fed mock data (the prototype template loads the board's scripts, so a take reuses the board's components; a hub-page take reuses them by path or copy), each take showing the states it needs inside itself, then post one question whose url opens prototype-<topic>.html with \`${w} question add --file <json>\` on topic item:${e}, never asking in the chat. Build in situ only where one of the board's rules asks for it: then, run \`${w} prototype base\`, which makes it or moves it to main and prints its path, whether its build script is there and the listener a page loads. When the build script is missing, write one there that serves the app and prints "port <n>" (board/build-script.mjs). Add one page to the worktree, at a path no other round uses, built from the repo's real components, helpers and scripts fed mock data, fully interactive; it reads the take from ?variant= and the state from ?state=, and loads the listener so \`KickflipPrototype.on((take, state) => \u2026)\` re-renders it in place when the viewer switches. Record it with \`${w} prototype add <topic> --card ${e} --served <path> --take <K>:<label> \u2026 --state <key>:<label> \u2026\`, one take per option and one state for each state that matters to the question, then post one question whose url opens prototype-<topic>.html with \`${w} question add --file <json>\` on topic item:${e}, never asking in the chat. (With no Skill tool, follow this step as written.)`,...d?[` The owner asked for this ${b?"grill":"prototype"}, writing: "${d}"`]:[],...o.length?[` The owner attached: ${o.map(q=>q.path).join(", ")}.`]:[],...s?[` The owner asked for prototypes of question ${s}${m?`, noting "${m}"`:""}${f?`, leaning to option ${f}`:""}: draw that question's next round, its question naming round_of ${s}.`]:[],`5. Record what you learned in the card's notes, naming each answered question's id: write it in ${de}, then run ${le(w,e,`${b?"Grill":"Prototype"} (<MM/DD>)`)}, which appends it as a section. The notes file is read-only; never write into it. Anything you leave open becomes a question with question add, never only a line in the notes, so the card can't go ready past it. A question's text is one line of at most ${J} characters (question add refuses a longer one); put the detail in its context and the options' details, which take markdown.`,...b?[`6. Where the owner asked for prototypes, or a rule here wants a UI prototype first, end the card \`${w} item set ${e} --phase prototyping\` instead and skip the prototype question. Otherwise the prototype question is what moves the card on: never set it ready yourself, and never leave a Next that asks the owner to mark it ready.`]:[],"",...x(c),...T(p,"Rules from the hub and this board, written for the orchestrator: where one tells you to push, land, restart the hub or a board server, or do the orchestrator's work, it isn't yours; you never push or restart:"),`To finish, run \`${w} builder done ${e} --text "<what you asked>"\`.`,"","Never push."].join(`
|
|
5
|
+
`)}l(pe,"plannerBrief");function fe({route:t,key:e,ref:r,title:i,notes:n,thread:a,repo:h,agent:u,rules:p=[],links:c=[],text:d,files:o=[]}){const s=g(t);return[`You are the headless visual agent for card ${r} (${e}, "${i}") on the kickflip board /${t}. The owner asked, in the card's thread, for a visual: one HTML page that answers in a visual way, such as a diagram, a flow or a comparison. It is not a prototype or a grill: no question, no takes. The board launched you; nobody watches this session, so never wait on a person. Read and run in ${h}; write no code there, never restart the hub or a board server, and never push.`,"",`1. Run \`${s} builder start ${e} --kind visual --agent ${u}\`. If it refuses, stop.`,`2. Gather what the page needs: ${n?`the card's notes, ${n}; `:""}its thread, ${a}, and the code or docs it is about.`,d?` The owner wrote: "${d}"`:" The owner typed nothing: draw what the thread was last discussing.",...o.length?[` The owner attached: ${o.map(m=>m.path).join(", ")}.`]:[],"3. Write one HTML file outside the repo, such as in a temporary folder. Keep its own CSS and script inline; it may load a library such as Mermaid or D3 from a CDN. Make it read well at phone width (390px) and on a desktop, in the dark and the light theme (prefers-color-scheme).",`4. Place it with \`${s} visual add ${e} --file <page.html> --title "<what it shows>"\`, which copies it to the board and links it in the thread.`,"","Write nothing else: no notes, questions, card state, Next or progress lines.","",...x(c),...T(p,"Rules from the hub and this board, written for the orchestrator: where one tells you to push, land, restart the hub or a board server, or do the orchestrator's work, it isn't yours; you never push or restart:"),`To finish, run \`${s} builder done ${e} --agent ${u}\`.`,"","Never push."].join(`
|
|
6
|
+
`)}l(fe,"visualBrief");function we({route:t,key:e,ref:r,title:i,notes:n,thread:a,agent:h,task:u,writes:p,cwd:c,rules:d=[],links:o=[]}){const s=g(t);return[`You are the headless helper ${h} on card ${r} (${e}, "${i}") on the kickflip board /${t}. Another agent working on the card asked the board for you to do one task beside it, and waits on your hand-back. Nobody watches this session, so never wait on a person. Never restart the hub or a board server, and never push.`,p?`Work only in ${c}, your own worktree, branched from the card's: commit your work there with \`git -c core.commentChar=';' commit -F <file>\`, the subject "${r}: <what it does>". The card's builder merges your branch, so leave nothing uncommitted.`:`Read and run in ${c}; change no files there and commit nothing.`,"","The task:",u,"",`1. Run \`${s} builder start ${e} --kind helper --agent ${h}\`. If it refuses, stop.`,`2. Report progress with \`${s} builder progress ${e} --agent ${h} --phase reading|building|testing|checking "<plain, 80 characters or fewer>"\` at least every 10 minutes; the board adds your name and the phase word.`,"3. Do only the task. Don't ask the orchestrator or the owner, record no judgment calls and post no questions: put any decision you had to make in your hand-back.",n?` The card's notes, for context: ${n}.`:"",` ${I(a)}`,"",...x(o),...T(d,"Rules from the hub and this board, written for the orchestrator: where one tells you to push, land, restart the hub or a board server, or do the orchestrator's work, it isn't yours; you never push or restart:"),`To finish, run \`${s} builder done ${e} --agent ${h} --text "<your hand-back: what you did or found, ${p?"what you committed, ":""}and any decision you made>"\`.`,"","Never push."].filter((m,f,w)=>m!==""||w[f-1]!=="").join(`
|
|
7
|
+
`)}l(we,"helperBrief");const me=6e4;function be(t,{starts:e=W}={}){const r=t.filter(n=>n.pid&&n.at).map(n=>n.pid),i=r.length&&e(r)||new Map;return t.filter(n=>{const a=i.get(n.pid);return a!==void 0&&a<=Date.parse(n.at)&&a>=Date.parse(n.at)-me})}l(be,"launchedAgents");function je(t,{starts:e=W,kill:r=l((n,a)=>process.kill(-n,a),"kill"),killAfter:i=5e3}={}){const n=be(t,{starts:e});for(const{pid:a}of n){try{r(a,"SIGTERM")}catch{continue}setTimeout(()=>{try{r(a,"SIGKILL")}catch{}},i).unref()}return n.length}l(je,"stopAgents");async function Ae(t,{grace:e=5e3}={}){const r=["-A","-o","pid=,ppid=,pgid=,args="],i=C(r)??C(r);if(i===null)throw new oe("ps gave no answer, so nothing was stopped; run it again");const n=i.split(`
|
|
8
|
+
`).map(o=>/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/.exec(o)).filter(Boolean).map(([,o,s,m,f])=>({pid:Number(o),ppid:Number(s),pgid:Number(m),args:f})),a=new Set(n.filter(o=>t.includes(o.pgid)).map(o=>o.pid));for(let o=!0;o;){o=!1;for(const s of n)!a.has(s.pid)&&a.has(s.ppid)&&(a.add(s.pid),o=!0)}const h=n.find(o=>o.pid===process.pid)?.pgid,u=n.filter(o=>a.has(o.pid)&&o.pgid>1&&o.pgid!==h),p=[...new Set(u.map(o=>o.pgid))],c=l(o=>{for(const s of p)try{process.kill(-s,o)}catch{}},"signal");c("SIGTERM");const d=Date.now()+e;for(;u.some(o=>j(o.pid))&&Date.now()<d;)await new Promise(o=>setTimeout(o,50));return u.some(o=>j(o.pid))&&c("SIGKILL"),u.map(({pid:o,args:s})=>({pid:o,args:s}))}l(Ae,"stopRuns");const j=l(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"pidAlive");async function Ne(t,e,{launch:r,handBack:i,alive:n=j,held:a=l(()=>!1,"held"),stopped:h=new Set,paused:u=new Set,limitedUntil:p=l(()=>null,"limitedUntil"),atLimit:c=l(async()=>{},"atLimit")}){const d={};for(const[o,s]of Object.entries(t)){if(!e.has(o))continue;if(h.has(o)){d[o]={...s,handedBack:!0};continue}if(u.has(o)||s.pid!==null&&n(s.pid)||s.handedBack){d[o]=s;continue}const m={key:o,...s.kind?{kind:s.kind}:{},...s.model?{model:s.model}:{}},f=s.pid!==null?p(s):null;if(f){await c(m,f),d[o]={...s,pid:null};continue}if(a(s.kind??"claude")){d[o]=s;continue}if(s.pid!==null&&s.relaunched){await i(m),d[o]={...s,handedBack:!0};continue}const{key:w,...b}=m;d[o]={...b,...await r(m),relaunched:s.pid!==null||!!s.relaunched}}return d}l(Ne,"reviveBuilders");const _=l((t,e)=>k.join(X.homedir(),".claude","kickflip","logs",`${e}-${t}-${new Date().toISOString().replace(/[:.]/g,"-")}.log`),"logFile");async function ge(t,{key:e,kind:r="claude",model:i,agent:n="main",text:a,files:h,question:u,note:p,choice:c,task:d,writes:o,worktree:s},{lander:m=!1}={}){const f=await L(t,e),w=M(t)?.route??k.basename(t),b=[...ee(),...await re(t)].map(z=>z.text),{base:q,repo:S}=O(t),y=ae({kind:r}),v=r===se,A=!y&&!v&&!m&&f.rebasePR!==null,N=r===ie,D=y?S:v?s??f.worktree??S:f.worktree,$={...f,route:w,base:q,repo:S,rules:b,kind:r,agent:n,text:a,files:h,question:u,note:p,choice:c,task:d,writes:o,cwd:D,pr:f.rebasePR},U=N?"Visual agent":v?`Helper ${n}`:y?"Planner":m?"Lander":A?"Rebase run":"Builder",V=N?fe($):v?we($):y?pe($):m?H($):A?K($):ue($),R=await P({kind:y||v?"claude":r,model:i,cwd:D,site:t,prompt:V,log:_(e.split("/")[1],U.split(" ")[0].toLowerCase())});return await E(t,e,R.sessionId,R.pid,n,k.basename(R.log)),R}l(ge,"launchBuilder");const De=l(({ref:t,worktree:e,cleared:r})=>`The board paused your build of card ${t}, and the pause has cleared: ${r}. Carry on where you stopped, in ${e}, following your brief as before. Your builder record is still open, so don't run builder start again.`,"resumeBrief");async function G(t,{key:e,kind:r="claude",model:i,session:n,brief:a,fresh:h=!1,freshLaunch:u={}}){if(r!=="claude"||!n||h)return{...await ge(t,{key:e,kind:r,model:i},u),resumed:!1};const p=await L(t,e),c=await P({kind:r,model:i,cwd:n.worktree??p.worktree,site:t,prompt:a(p),resume:n.id,log:_(e.split("/")[1],"builder")});return await E(t,e,c.sessionId??n.id,c.pid,"main",k.basename(c.log)),{...c,resumed:!0}}l(G,"resumeBuilder");async function Pe(t,{key:e,kind:r="claude",model:i},{why:n,fresh:a=!1}={}){const h=await ne(t,e),u=await L(t,e),p=u.rebasePR!==null,c=h&&(!p||h.worktree===u.worktree)?h:null,d=M(t)?.route??k.basename(t),{base:o}=O(t),s=l(w=>(p?K:H)({...w,route:d,base:o,pr:w.rebasePR,why:n,resumed:!0}),"brief"),m={lander:!p};let f;try{f=await G(t,{key:e,kind:r,model:i,session:c,brief:s,fresh:a,freshLaunch:m})}catch(w){if(!c)throw w;f=await G(t,{key:e,kind:r,model:i,session:null,brief:s,freshLaunch:m})}return f.resumed&&await te(t,e,"builder-resumed-to-land",{session:f.sessionId??c.id}),f}l(Pe,"landByBuilder");export{he as BOARD_CLI,ue as builderBrief,we as helperBrief,Pe as landByBuilder,H as landerBrief,ge as launchBuilder,be as launchedAgents,j as pidAlive,pe as plannerBrief,K as rebaseBrief,De as resumeBrief,G as resumeBuilder,Ne as reviveBuilders,je as stopAgents,Ae as stopRuns,fe as visualBrief};
|
package/board/hub-lines.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var w=Object.defineProperty;var t=(e,a)=>w(e,"name",{value:a,configurable:!0});import $ from"node:os";const p=100,g=2e3,y=/(?:~|\/)[^\s:;,()'"]*\/([^\s:;,()'"/]+)/g;function r(e,a,s=""){const d=p-e.length-s.length;let l=String(a??"").split($.homedir()).join("~").replace(/\s+/g," ").trim();return l.length>d&&(l=l.replace(y,"\u2026/$1")),l.length<=d?e+l+s:`${e}${l.slice(0,d-1).trimEnd()}\u2026${s.replace(/^\./,"")}`}t(r,"fit");const i="Held",c="Resumed",m="Blocked",o="Fault",u="move",x="questions",k="Implementation requested",f="Prototype requested",n="Marked done",v="prototype",b="owner/a-long-repo-name#10000",I="Wed, 11/30, 12:00 PM",h={"host-busy":{mark:i,text:t(()=>"Host is busy, work will resume when the host recovers","text")},"planner-held":{mark:i,text:t(({kind:e})=>`The ${e} waits and starts when the hold lifts.`,"text"),longest:{kind:v}},paused:{mark:i,text:t(({phase:e,words:a,clear:s})=>r(`Paused${e?` at ${e}`:""}: `,a,`. It resumes once ${s}.`),"text"),longest:{phase:"building",clear:"the usage pause lifts"}},"run-stopped":{mark:i,text:t(({words:e})=>e,"text"),longest:{words:"build stopped by orchestrator"}},"land-moved-first":{mark:u,text:t(({words:e})=>e,"text"),longest:{words:"land moved first by orchestrator"}},"builder-stopped":{mark:i,text:t(()=>"Stopped its builder.","text")},"land-waiting":{mark:i,text:t(({card:e})=>r("Waiting for ",e," to land"),"text")},resumed:{mark:c,text:t(({clear:e})=>`Resumed: ${e}.`,"text"),longest:{clear:"the usage pause lifted"}},"hold-ended":{mark:c,text:t(({at:e})=>`Resumed: its hold until ${e} ended.`,"text"),longest:{at:I}},unblocked:{mark:c,text:t(({lifted:e})=>r("Unblocked: ",e,"."),"text")},"unblocked-done":{mark:c,text:t(({card:e})=>r("Unblocked: ",e," is done."),"text")},"blocked-pausing":{mark:m,text:t(({blocked:e})=>r("Blocked ",e,": its builder pauses at its next stopping point and it stays In Progress."),"text")},"blocked-stays":{mark:m,text:t(({blocked:e})=>r("Blocked ",e,": it stays In Progress."),"text")},"cant-start-yet":{mark:m,text:t(({names:e})=>r("Can\u2019t start yet: waits on ",e,". Bumped to the top of Todo."),"text")},"split-moved":{mark:m,text:t(({card:e,to:a})=>r("Split: ",`${e} became ${a}`,", so it waits on those now."),"text")},"merge-blocked":{mark:m,text:t(({blocked:e})=>r("Not merged: blocked ",e,"."),"text")},pulled:{mark:k,text:t(()=>"Pulled in: an implementer slot was free.","text")},"helper-asked":{mark:k,text:t(({agent:e,writes:a})=>r("Asked for helper ",e,a?", which writes code":""),"text"),longest:{writes:!0}},"arch-review":{mark:k,text:t(({card:e})=>r("Started an architecture review of ",e,"; its report becomes the notes"),"text")},"prototype-after":{mark:f,text:t(()=>"Prototype round asked: it moves to prototyping once every question is answered.","text")},"prototype-round":{mark:f,text:t(()=>"You asked for a prototype round, so the board moved the card to prototyping.","text")},"ticket-made":{mark:u,text:t(({ticket:e})=>r("The board made ",e,"."),"text")},"new-question":{mark:x,text:t(()=>"A new question on it, so it is grilling until you answer.","text")},"screen-timeout":{mark:x,text:t(()=>"Questions passed to you unscreened: the orchestrator didn\u2019t screen them in 15 minutes.","text")},"ask-moved":{mark:u,text:t(({board:e})=>r("Moved to ",e,"."),"text")},"card-merged":{mark:u,text:t(({card:e})=>r("Merged into ",e,"."),"text")},"pr-found":{mark:u,text:t(({pr:e,branch:a})=>r(`Found PR #${e} on branch `,a,"."),"text"),longest:{pr:1e5}},"ci-passed":{mark:n,text:t(({pr:e})=>`CI passed, so PR #${e} is marked ready for review.`,"text"),longest:{pr:1e5}},"worktree-remade":{mark:u,text:t(()=>"Remade its leftover worktree: it held no commits or changes.","text")},merged:{mark:n,text:t(({words:e})=>r("",e),"text")},"already-merged":{mark:n,text:t(({prs:e,many:a})=>r(`PR${a?"s":""} `,e,` ${a?"were":"was"} already merged.`),"text"),longest:{many:!0}},"ask-moved-closed":{mark:n,text:t(({board:e})=>r("Closed: its ask moved to ",e,"."),"text")},"check-passed":{mark:n,text:t(({found:e})=>r("Check passed: ",e,". Closed."),"text")},"findings-settled":{mark:n,text:t(({filed:e})=>r("Every finding is settled: ",e?`filed ${e}`:"none filed","."),"text")},"branch-merged":{mark:n,text:t(({branch:e,into:a,host:s})=>r("",`${e} is merged into ${a} on ${s}`,", so this card is Done."),"text")},"branch-gone":{mark:n,text:t(({branch:e,host:a})=>r("",`${e} is gone from ${a}`,", so this card is Done."),"text")},"prs-closed":{mark:n,text:t(({prs:e})=>r("Closed: ",e," closed unmerged."),"text"),longest:{prs:`${b}, ${b}`}},"landed-on-merge":{mark:n,text:t(({base:e})=>r("Landed on ",e," on your Merge."),"text")},"lander-handoff":{mark:k,text:t(({why:e})=>r("Not merged yet: ",e,". Its builder resumes to fix it and land it."),"text")},"builder-resumed-to-land":{mark:k,text:t(({session:e})=>r("Builder resumed to land, session ",e),"text")},"build-refused":{mark:o,fault:!0,detail:t(({why:e})=>e,"detail"),text:t(({why:e})=>r("Couldn't start implementing: ",e),"text")},"planner-failed":{mark:o,fault:!0,detail:t(({error:e})=>e,"detail"),text:t(({kind:e,error:a})=>r(`Couldn't start the ${e}: `,a),"text"),longest:{kind:"visual agent"}},"helper-failed":{mark:o,fault:!0,detail:t(({error:e})=>e,"detail"),text:t(({name:e,error:a})=>r(`Couldn't start helper ${e}: `,a),"text"),longest:{name:"a-helper-name-of-24-char"}},"exited-twice":{mark:o,fault:!0,text:t(()=>"Its builder exited twice unfinished; it reads stopped until the orchestrator takes it up.","text")},"resume-failed":{mark:o,fault:!0,detail:t(({error:e})=>e,"detail"),text:t(({error:e})=>r("Couldn't resume the builder headless: ",e),"text")},"relaunch-failed":{mark:o,fault:!0,detail:t(({error:e})=>e,"detail"),text:t(({error:e})=>r("Couldn't relaunch the builder headless: ",e),"text")},"lander-failed":{mark:o,fault:!0,detail:t(({error:e})=>e,"detail"),text:t(({error:e})=>r("Couldn't launch the lander; land it by hand: ",e),"text")},"rebase-failed":{mark:o,fault:!0,detail:t(({error:e})=>e,"detail"),text:t(({pr:e,error:a})=>r(`PR #${e} conflicts, but the rebase run couldn't start: `,a),"text"),longest:{pr:1e5}},"rebase-launch-failed":{mark:o,fault:!0,detail:t(({error:e})=>e,"detail"),text:t(({error:e})=>r("Couldn't launch the rebase run; it reads stopped: ",e),"text")},"gh-limited":{mark:o,fault:!0,detail:t(({words:e})=>e,"detail"),text:t(({what:e,words:a})=>r(`${e}: `,a,"."),"text"),longest:{what:"No PR opened"}},"merge-refused":{mark:o,fault:!0,detail:t(({why:e})=>e,"detail"),text:t(({why:e})=>r("Not merged: ",e,"."),"text")},"land-failed":{mark:o,fault:!0,detail:t(({why:e})=>e,"detail"),text:t(({why:e})=>r("Not merged: ",e,". Tap Merge again."),"text")},"push-failed":{mark:o,fault:!0,detail:t(({error:e})=>e,"detail"),text:t(({base:e,error:a})=>r(`Landed, but ${e} didn't reach origin: `,a),"text"),longest:{base:"a-long-base-branch"}},"answers-failed":{mark:o,fault:!0,detail:t(({error:e})=>e,"detail"),text:t(({what:e,error:a})=>r(`Answers recorded, but ${e} failed: `,a),"text"),longest:{what:"writing them into its notes"}},"notes-missing":{mark:o,fault:!0,text:t(()=>"Its notes file was missing, so the board started a new one.","text")},"notes-shared":{mark:o,fault:!0,text:t(({card:e})=>r("It shared its notes file with ",e,", so the board gave it its own copy."),"text")},"check-broken":{mark:o,fault:!0,detail:t(({error:e})=>e,"detail"),text:t(({error:e})=>r("The owner step's check can't run, so it stopped: ",e),"text")},"run-failed":{mark:o,fault:!0,detail:t(({why:e})=>e,"detail"),text:t(({kind:e,code:a})=>`${e} failed (exit ${a})`,"text"),longest:{kind:"Serve",code:255}}};function C(e,a={}){const s=h[e];if(!s)throw new Error(`no status line kind "${e}"`);const d=s.text(a);if(!d||d.length>p)throw new Error(`the "${e}" status line runs to ${d.length} characters, past ${p}: ${d}`);return d}t(C,"hubText");function T(e,a={}){const s=String(h[e]?.detail?.(a)??"").trim();return s.length>g?`\u2026${s.slice(-(g-1))}`:s}t(T,"detailOf");const R=t(e=>({mark:h[e.line]?.mark||"",fault:!!h[e.line]?.fault}),"hubLineOf"),S="hub";export{p as BRIEF_LINE_LIMIT,S as HUB,h as HUB_LINES,g as MESSAGE_LIMIT,T as detailOf,r as fit,R as hubLineOf,C as hubText};
|
|
@@ -90,7 +90,7 @@ BoardView.part((B) => {
|
|
|
90
90
|
const marked = getDraft(q.id).choice;
|
|
91
91
|
const optionHtml = (o) => {
|
|
92
92
|
const armed = o.id === marked;
|
|
93
|
-
const label = armed ? `Send “${esc(o.label)}
|
|
93
|
+
const label = armed ? `Send “${esc(o.label)}”?` : `${esc(choiceLabel(q, o.id))}${o.recommended ? ' <span class="rec-word">recommended</span>' : ''}`;
|
|
94
94
|
return `<button type="button" class="${[o.recommended && 'rec', armed && 'armed'].filter(Boolean).join(' ')}" aria-pressed="${armed}" data-deck-pick="${esc(o.id)}"><span class="lbl">${label}</span>${o.detail ? `<div class="det">${markdown(o.detail, links)}</div>` : ''}</button>`;
|
|
95
95
|
};
|
|
96
96
|
return `<h3>${escText(q.question)}</h3>${q.context ? `<div class="ctx">${markdown(q.context, links)}</div>` : ''}
|
package/board/public/app.html
CHANGED
|
@@ -42,6 +42,7 @@ window.BoardBoot = (function () {
|
|
|
42
42
|
<body class="has-sidebar">
|
|
43
43
|
<aside id="sidebar" class="sidebar" aria-label="Boards">{{sidebar}}</aside>
|
|
44
44
|
<div id="sidebarScrim" class="sidebar-scrim"></div>
|
|
45
|
+
<div class="col-handle" data-col="sidebar" role="separator" aria-orientation="vertical" aria-label="Resize the boards sidebar" tabindex="0"></div>
|
|
45
46
|
<script id="sidebarData" type="application/json">{{sidebarData}}</script>
|
|
46
47
|
<script src="/_hub/app/ui.js"></script>
|
|
47
48
|
<script src="/_hub/app/ui-links.js"></script>
|
|
@@ -67,6 +68,7 @@ window.BoardBoot = (function () {
|
|
|
67
68
|
<script src="/_hub/app/attach.js"></script>
|
|
68
69
|
<script src="/_hub/app/hub-kit.js"></script>
|
|
69
70
|
<script src="/_hub/app/linear-key.js"></script>
|
|
71
|
+
<script src="/_hub/app/col-resize.js"></script>
|
|
70
72
|
<script src="/_hub/app/sidebar.js"></script>
|
|
71
73
|
<script src="/_hub/app/app.js"></script>
|
|
72
74
|
<script src="/_hub/app/new-task.js"></script>
|
package/board/public/app.js
CHANGED
|
@@ -464,6 +464,7 @@ ${strip} <section id="setup" class="setup" aria-live="polite" hidden></section>
|
|
|
464
464
|
<div id="downLanding" hidden></div>
|
|
465
465
|
${BoardUI.boardFoot()}
|
|
466
466
|
</main>
|
|
467
|
+
<div class="col-handle" data-col="pane" role="separator" aria-orientation="vertical" aria-label="Resize the Waiting on you pane" tabindex="0"></div>
|
|
467
468
|
<aside id="needsPane" class="needs-pane" aria-label="Needs you"><section id="waiting" class="waiting" hidden></section></aside>
|
|
468
469
|
</div>
|
|
469
470
|
${BoardNewTask.markup()}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Drag-resizing of the desktop columns (docs/adr/2026-11-10-1600-the-desktop-columns-drag-and-this-device-keeps-their-widths.md).
|
|
2
|
+
// A `.col-handle[data-col="sidebar"|"pane"]` drags its column's edge: the sidebar rail's, or the seam between the lanes
|
|
3
|
+
// and the Waiting on you pane. Widths are clamped (sidebar 220–420px, the lanes and the pane each 360px or more), kept
|
|
4
|
+
// per device for every board, and set on <html> as --sb-w and --pane-w, which style.css and sidebar.css read. A double-click
|
|
5
|
+
// on a handle forgets the width; ←/→ on a focused handle moves it 16px. Loaded after the body's first elements so the
|
|
6
|
+
// stored widths land before the first paint of the board.
|
|
7
|
+
(function () {
|
|
8
|
+
const KEYS = { sidebar: 'hub:sidebarWidth', pane: 'hub:paneWidth' };
|
|
9
|
+
const VARS = { sidebar: '--sb-w', pane: '--pane-w' };
|
|
10
|
+
const SIDEBAR_MIN = 220, SIDEBAR_MAX = 420, LANE_MIN = 360, STEP = 16, DEFAULT_SIDEBAR = 272;
|
|
11
|
+
const html = document.documentElement;
|
|
12
|
+
const wide = () => window.matchMedia('(min-width: 900px)').matches;
|
|
13
|
+
const read = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
|
|
14
|
+
const write = (k, v) => { try { localStorage.setItem(k, v); } catch { /* private mode or full: kept for this page only */ } };
|
|
15
|
+
const forget = (k) => { try { localStorage.removeItem(k); } catch { /* nothing kept */ } };
|
|
16
|
+
const stored = (col) => { const n = Number(read(KEYS[col])); return Number.isFinite(n) && n > 0 ? n : null; };
|
|
17
|
+
// The sidebar's width now, which the pane's room depends on.
|
|
18
|
+
const sidebarNow = (viewport) => (html.classList.contains('sb-collapsed') ? 0 : clamp('sidebar', stored('sidebar') ?? DEFAULT_SIDEBAR, viewport, 0));
|
|
19
|
+
|
|
20
|
+
// → px clamped for the column: the pane's ceiling leaves the lanes their minimum beside the sidebar, and its floor
|
|
21
|
+
// wins where the window is too narrow for both.
|
|
22
|
+
function clamp(col, px, viewport, sidebarW) {
|
|
23
|
+
if (col === 'sidebar') return Math.min(SIDEBAR_MAX, Math.max(SIDEBAR_MIN, px));
|
|
24
|
+
return Math.max(LANE_MIN, Math.min(px, viewport - sidebarW - LANE_MIN));
|
|
25
|
+
}
|
|
26
|
+
function paint(col) {
|
|
27
|
+
const px = stored(col);
|
|
28
|
+
if (px === null) { html.style.removeProperty(VARS[col]); return; }
|
|
29
|
+
html.style.setProperty(VARS[col], `${col === 'pane' ? clamp('pane', px, window.innerWidth, sidebarNow(window.innerWidth)) : clamp('sidebar', px)}px`);
|
|
30
|
+
}
|
|
31
|
+
function paintAll() { paint('sidebar'); paint('pane'); }
|
|
32
|
+
function set(col, px) { write(KEYS[col], String(Math.round(px))); paintAll(); }
|
|
33
|
+
function reset(col) { forget(KEYS[col]); paintAll(); }
|
|
34
|
+
paintAll();
|
|
35
|
+
window.addEventListener('resize', paintAll);
|
|
36
|
+
|
|
37
|
+
// The width a handle's drag edge is at now, and the width the pointer at x asks for.
|
|
38
|
+
const current = (col, el) => {
|
|
39
|
+
const edge = el.getBoundingClientRect();
|
|
40
|
+
return col === 'sidebar' ? edge.left + edge.width / 2 : window.innerWidth - (edge.left + edge.width / 2);
|
|
41
|
+
};
|
|
42
|
+
const wanted = (col, x) => (col === 'sidebar' ? x : window.innerWidth - x);
|
|
43
|
+
|
|
44
|
+
document.addEventListener('pointerdown', (e) => {
|
|
45
|
+
const el = e.target.closest?.('.col-handle');
|
|
46
|
+
if (!el || !wide() || e.button > 0) return;
|
|
47
|
+
const col = el.dataset.col;
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
el.setPointerCapture?.(e.pointerId);
|
|
50
|
+
el.classList.add('dragging');
|
|
51
|
+
html.classList.add('col-dragging');
|
|
52
|
+
const move = (m) => set(col, wanted(col, m.clientX));
|
|
53
|
+
const end = () => { el.classList.remove('dragging'); html.classList.remove('col-dragging'); el.removeEventListener('pointermove', move); el.removeEventListener('pointerup', end); el.removeEventListener('pointercancel', end); };
|
|
54
|
+
el.addEventListener('pointermove', move);
|
|
55
|
+
el.addEventListener('pointerup', end);
|
|
56
|
+
el.addEventListener('pointercancel', end);
|
|
57
|
+
});
|
|
58
|
+
document.addEventListener('dblclick', (e) => {
|
|
59
|
+
const el = e.target.closest?.('.col-handle');
|
|
60
|
+
if (el && wide()) reset(el.dataset.col);
|
|
61
|
+
});
|
|
62
|
+
document.addEventListener('keydown', (e) => {
|
|
63
|
+
const el = e.target.closest?.('.col-handle');
|
|
64
|
+
if (!el || !wide() || (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight')) return;
|
|
65
|
+
const col = el.dataset.col;
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
// → grows the sidebar and shrinks the pane, as the edge moves right.
|
|
68
|
+
const dx = (e.key === 'ArrowRight' ? STEP : -STEP) * (col === 'sidebar' ? 1 : -1);
|
|
69
|
+
set(col, (stored(col) ?? current(col, el)) + dx);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
window.ColResize = { clamp, set, reset };
|
|
73
|
+
})();
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
<script src="attach.js"></script>
|
|
85
85
|
<script src="hub-kit.js"></script>
|
|
86
86
|
<script src="linear-key.js"></script>
|
|
87
|
+
<script src="col-resize.js"></script>
|
|
87
88
|
<script src="sidebar.js"></script>
|
|
88
89
|
<script src="app.js"></script>
|
|
89
90
|
<script src="new-task.js"></script>
|
package/board/public/sidebar.css
CHANGED
|
@@ -188,12 +188,14 @@ html.sb-hub-down body.has-sidebar > .top .hub::after, html.sb-hub-down body.has-
|
|
|
188
188
|
.sidebar .t-paused { --tone: var(--muted); }
|
|
189
189
|
|
|
190
190
|
@media (min-width: 900px) {
|
|
191
|
-
body.has-sidebar { padding-left: 272px; }
|
|
192
|
-
body.has-sidebar .sidebar { width: 272px; transform: none; visibility: visible; transition: none; z-index: 4; }
|
|
191
|
+
body.has-sidebar { padding-left: var(--sidebar-w, 272px); }
|
|
192
|
+
body.has-sidebar .sidebar { width: var(--sb-w, 272px); transform: none; visibility: visible; transition: none; z-index: 4; }
|
|
193
193
|
body.has-sidebar .sidebar .sb-close { display: none; }
|
|
194
194
|
body.has-sidebar .sidebar-scrim { display: none; }
|
|
195
195
|
html:not(.sb-collapsed) body.has-sidebar > .top .hub, html:not(.sb-collapsed) body.has-sidebar > :not(.sidebar) > .top .hub { display: none; }
|
|
196
196
|
html.sb-collapsed body.has-sidebar { padding-left: 0; }
|
|
197
|
+
body.has-sidebar > .col-handle { position: fixed; left: calc(var(--sidebar-w, 272px) - 4px); }
|
|
198
|
+
html.sb-collapsed body.has-sidebar > .col-handle { display: none; }
|
|
197
199
|
html.sb-collapsed body.has-sidebar .sidebar { transform: translateX(-102%); visibility: hidden; }
|
|
198
200
|
/* Switch mode shows a collapsed rail over the page until it ends, and leaves it collapsed. */
|
|
199
201
|
html.sb-collapsed body.has-sidebar .sidebar.ks-on { transform: none; visibility: visible; z-index: 20; }
|
|
@@ -244,3 +246,7 @@ html.sb-hub-down body.has-sidebar > .top .hub::after, html.sb-hub-down body.has-
|
|
|
244
246
|
font: 600 12.5px/1 -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif; }
|
|
245
247
|
.shortcuts-sheet .sc-foot { margin: 14px 0 0; padding-top: 10px; border-top: 1px solid var(--line); font-size: 12.5px; color: var(--muted); }
|
|
246
248
|
@media (prefers-reduced-motion: reduce) { .sheet.shortcuts-sheet[open] { animation: none; } }
|
|
249
|
+
/* A held row (hub/home.mjs holdNeeds): its last good line, flat gray with (stale) after the word. */
|
|
250
|
+
.sidebar .held .dot, .sidebar .held .word, .sidebar .held .lc { --tone: var(--muted); }
|
|
251
|
+
.sidebar .held .dot.pulse { animation: none; }
|
|
252
|
+
.sidebar .held .stale-word { flex: none; color: var(--muted); font-size: 11px; }
|
package/board/public/style.css
CHANGED
|
@@ -240,7 +240,7 @@ code { overflow-wrap: anywhere; }
|
|
|
240
240
|
/* Compose button and sheet */
|
|
241
241
|
main { padding-bottom: calc(110px + env(safe-area-inset-bottom)); }
|
|
242
242
|
/* The + sits two thirds of (16px + the safe area) from the corner, and at least 32px where there is no safe area. */
|
|
243
|
-
:root { --fab-edge: max(32px, calc((16px + env(safe-area-inset-bottom)) * 2 / 3)); }
|
|
243
|
+
:root { --sel-bg: var(--accent-soft); --sel-line: var(--accent); --fab-edge: max(32px, calc((16px + env(safe-area-inset-bottom)) * 2 / 3)); }
|
|
244
244
|
.fab { position: fixed; z-index: 6; right: max(32px, calc((16px + max(env(safe-area-inset-right), env(safe-area-inset-bottom))) * 2 / 3)); bottom: calc(var(--app-bottom, 0px) + var(--fab-edge));
|
|
245
245
|
width: 75px; height: 75px; padding: 0; border-radius: 50%; border: 0; background: var(--accent); color: var(--on-accent);
|
|
246
246
|
display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 6px 18px rgba(0,0,0,.25); }
|
|
@@ -289,13 +289,15 @@ main { padding-bottom: calc(110px + env(safe-area-inset-bottom)); }
|
|
|
289
289
|
@keyframes sheet-in { from { transform: translateY(100%); } }
|
|
290
290
|
@keyframes sheet-out { to { transform: translateY(100%); } }
|
|
291
291
|
@keyframes fade-in { from { opacity: 0; } }
|
|
292
|
+
@keyframes reader-in { from { transform: translateX(100%); } }
|
|
293
|
+
@keyframes reader-out { to { transform: translateX(100%); } }
|
|
292
294
|
@media (min-width: 700px) {
|
|
293
295
|
.sheet { bottom: calc(var(--kb, 0px) + 24px); max-height: calc(var(--vh, 100dvh) - env(safe-area-inset-top) - 12px); padding-bottom: 0;
|
|
294
296
|
border-bottom: 1px solid var(--line); border-radius: 16px; }
|
|
295
297
|
.sheet form { padding-bottom: 14px; }
|
|
296
298
|
}
|
|
297
299
|
@media (prefers-reduced-motion: reduce) {
|
|
298
|
-
.sheet[open], .sheet[open]::backdrop, .sheet.closing, .sheet.closing::backdrop { animation: none; }
|
|
300
|
+
.sheet[open], .sheet.reader-sheet[open], .sheet[open]::backdrop, .sheet.closing, .sheet.reader-sheet.closing, .sheet.closing::backdrop { animation: none; }
|
|
299
301
|
.fab:active { transform: none; }
|
|
300
302
|
}
|
|
301
303
|
/* The hub lives at the tailnet root, outside this site, so its link is root-absolute. */
|
|
@@ -417,6 +419,11 @@ div.status-line { padding-left: 33px; text-indent: -23px; }
|
|
|
417
419
|
/* Only a run's finished line is green: the fold's summary mirrors its last line, green once the run has finished, else the
|
|
418
420
|
started and status lines' orange or a planner's blue (docs/adr/2026-11-07-1600-only-a-runs-finished-line-is-green.md). */
|
|
419
421
|
.msg.calls.status-fold summary.finished { color: var(--color-success); }
|
|
422
|
+
.msg.fault-detail summary { cursor: pointer; list-style: none; overflow-wrap: anywhere; }
|
|
423
|
+
.msg.fault-detail summary::-webkit-details-marker { display: none; }
|
|
424
|
+
.msg.fault-detail[open] summary .mark-icon { transform: rotate(90deg); }
|
|
425
|
+
/* The whole error of a fault line, monospace, wrapping at any width. */
|
|
426
|
+
.fault-error { margin: 4px 0 4px 23px; padding: 8px; font: 12px/1.4 ui-monospace, monospace; white-space: pre-wrap; overflow-wrap: anywhere; background: var(--bg-soft, rgba(127,127,127,.12)); border-radius: 6px; color: var(--fg, inherit); }
|
|
420
427
|
.msg.calls ol { margin: 4px 0; padding-left: 23px; }
|
|
421
428
|
.agent-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: currentColor; margin-right: 5px; vertical-align: 1px; }
|
|
422
429
|
/* Every thread status line's leading mark, icon or dot, sits centred in one 18px box, so the text after it always starts
|
|
@@ -636,7 +643,7 @@ main > * { transition: opacity .25s ease; }
|
|
|
636
643
|
.view-questions .ctx { color: var(--muted); font-size: 13px; margin-bottom: 8px; }
|
|
637
644
|
.view-questions .opt { display: grid; grid-template-columns: 22px 1fr; gap: 6px; padding: 8px; border: 1px solid var(--line);
|
|
638
645
|
border-radius: 8px; margin: 6px 0; cursor: pointer; }
|
|
639
|
-
.view-questions .opt:has(input:checked) { border-color: var(--
|
|
646
|
+
.view-questions .opt:has(input:checked) { border-color: var(--sel-line); background: var(--sel-bg); }
|
|
640
647
|
.view-questions .opt input { margin: 3px 0 0; width: 16px; height: 16px; accent-color: var(--accent); }
|
|
641
648
|
.view-questions .opt .lbl { font-weight: 600; }
|
|
642
649
|
.view-questions .opt .rec { color: var(--accent); font-weight: 500; font-size: 12px; margin-left: 4px; }
|
|
@@ -664,9 +671,9 @@ main > * { transition: opacity .25s ease; }
|
|
|
664
671
|
.view-questions .topics button { min-width: 0; border: 1px solid var(--line); border-radius: 999px; padding: 4px 11px; font-size: 13px;
|
|
665
672
|
background: var(--panel); color: var(--text); display: inline-flex; max-width: 100%; text-align: left; }
|
|
666
673
|
.view-questions .topics .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
667
|
-
.view-questions .topics button[aria-pressed="true"] { border-color: var(--
|
|
674
|
+
.view-questions .topics button[aria-pressed="true"] { border-color: var(--sel-line); background: var(--sel-bg); }
|
|
668
675
|
.view-questions .topics button.progress { border-color: var(--line); background: color-mix(in srgb, var(--color-notice) 12%, var(--panel)); }
|
|
669
|
-
.view-questions .topics button.progress[aria-pressed="true"] { border-color: var(--
|
|
676
|
+
.view-questions .topics button.progress[aria-pressed="true"] { border-color: var(--sel-line); background: var(--sel-bg); }
|
|
670
677
|
@keyframes pill-arrive { from { opacity: 0; transform: translateX(-12px); box-shadow: 0 0 0 3px var(--accent); } to { opacity: 1; transform: none; box-shadow: 0 0 0 0 transparent; } }
|
|
671
678
|
@media (prefers-reduced-motion: no-preference) { .view-questions .topics button.arrived { animation: pill-arrive .6s ease-out; } }
|
|
672
679
|
.view-questions .topics .n { color: var(--muted); margin-left: 4px; }
|
|
@@ -934,6 +941,7 @@ button[aria-busy="true"]:is([data-card-bump]) svg { fill: currentColor; }
|
|
|
934
941
|
/* Needs you (app-needs.js): a deck at the top of the board on a phone; from 900px a pane beside the lanes, its lines over
|
|
935
942
|
Waiting on you (docs/adr/2026-11-05-0408-needs-you-is-a-deck-on-a-phone-and-a-pane-on-a-desktop.md). */
|
|
936
943
|
.needs-pane { display: none; }
|
|
944
|
+
.col-handle { display: none; }
|
|
937
945
|
.needs-deck { margin: 0 0 12px; border: 1px solid color-mix(in srgb, var(--color-notice) 45%, var(--line)); border-radius: 14px;
|
|
938
946
|
background: color-mix(in srgb, var(--color-notice) 7%, var(--panel)); overflow: hidden; }
|
|
939
947
|
.deck-head { display: flex; align-items: center; gap: 8px; padding: 10px 14px 4px; }
|
|
@@ -948,7 +956,7 @@ button[aria-busy="true"]:is([data-card-bump]) svg { fill: currentColor; }
|
|
|
948
956
|
.deck-opts { display: flex; flex-direction: column; gap: 6px; }
|
|
949
957
|
.deck-opts button { text-align: left; border-radius: 10px; padding: 10px 12px; font-size: 14px; }
|
|
950
958
|
.deck-opts button.rec { border-color: var(--accent); }
|
|
951
|
-
.deck-opts button.armed {
|
|
959
|
+
.deck-opts button.armed { border-color: var(--sel-line); background: var(--sel-bg); font-weight: 600; }
|
|
952
960
|
.deck-opts .rec-word { color: var(--accent); font-weight: 500; font-size: 12px; margin-left: 4px; }
|
|
953
961
|
.deck-opts .det { color: var(--muted); font-size: 12.5px; font-weight: 400; }
|
|
954
962
|
.deck-prompt { margin: 0; padding: 0; list-style: none; }
|
|
@@ -964,16 +972,30 @@ button[aria-busy="true"]:is([data-card-bump]) svg { fill: currentColor; }
|
|
|
964
972
|
.deck-more::after { content: '›'; color: var(--accent); font-size: 18px; }
|
|
965
973
|
.deck-more:hover { text-decoration: none; }
|
|
966
974
|
@media (min-width: 900px) {
|
|
967
|
-
.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; }
|
|
975
|
+
.split { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) var(--pane-w, minmax(0, 1fr)); align-items: start; }
|
|
968
976
|
.split > main { max-width: none; margin: 0; }
|
|
969
977
|
.needs-deck { display: none; }
|
|
970
978
|
/* Waiting on you is the board's pane here, so neither page shows the switch to the other. */
|
|
971
979
|
.top .pages-switch { display: none; }
|
|
972
980
|
.top .nav a.pages-switch { display: none; }
|
|
973
981
|
/* The board's + sits at the bottom right of the left pane: the pane ends at the middle of the space right of the sidebar. */
|
|
974
|
-
.view-board .fab { right: calc(50% - var(--sidebar-w, 0px) / 2 + var(--fab-edge)); }
|
|
975
|
-
body.has-sidebar { --sidebar-w: 272px; }
|
|
982
|
+
.view-board .fab { right: calc(var(--pane-w, calc(50% - var(--sidebar-w, 0px) / 2)) + var(--fab-edge)); }
|
|
983
|
+
body.has-sidebar { --sidebar-w: var(--sb-w, 272px); }
|
|
976
984
|
html.sb-collapsed body.has-sidebar { --sidebar-w: 0px; }
|
|
985
|
+
/* The drag edges (col-resize.js): a hairline that thickens on hover, on the pane's left edge and the sidebar's right. */
|
|
986
|
+
.col-handle { display: block; position: absolute; z-index: 5; top: 0; bottom: 0; width: 9px; cursor: col-resize; touch-action: none; }
|
|
987
|
+
.col-handle::after { content: ''; position: absolute; top: 0; bottom: 0; left: 4px; width: 1px; background: transparent; transition: background .12s, width .12s, left .12s; }
|
|
988
|
+
.col-handle:is(:hover, :focus-visible, .dragging)::after { left: 3px; width: 3px; background: var(--accent); }
|
|
989
|
+
.col-handle:focus-visible { outline: none; }
|
|
990
|
+
html.col-dragging, html.col-dragging * { cursor: col-resize !important; user-select: none; }
|
|
991
|
+
.split > .col-handle { right: calc(var(--pane-w, 50%) - 4px); }
|
|
992
|
+
.split:has(.needs-pane.widened) > .col-handle { display: none; }
|
|
993
|
+
/* The reader sheet (app-reader.js) is a panel over the pane's half, at the width the seam gives it, while it is open. */
|
|
994
|
+
.sheet.reader-sheet { inset: 56px 0 0 auto; margin: 0; width: var(--pane-w, calc((100vw - var(--sidebar-w, 0px)) / 2)); max-width: none;
|
|
995
|
+
height: auto; max-height: none; border-width: 0 0 0 1px; border-radius: 0; box-shadow: none; }
|
|
996
|
+
.sheet.reader-sheet[open] { animation: reader-in .22s ease-out; }
|
|
997
|
+
.sheet.reader-sheet.closing { animation: reader-out .18s ease-in forwards; }
|
|
998
|
+
.reader-sheet::backdrop { background: transparent; }
|
|
977
999
|
.needs-pane { display: block; position: sticky; top: 56px; height: calc(100vh - 56px); overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; padding: 12px 16px 0; border-left: 1px solid var(--line); }
|
|
978
1000
|
.needs-pane .needs-questions main { padding: 0; max-width: none; }
|
|
979
1001
|
.needs-pane:has(.q-empty), .needs-pane .needs-questions:has(.q-empty), .needs-pane .needs-questions:has(.q-empty) main, .needs-pane #page:has(.q-empty) { display: flex; flex-direction: column; }
|
|
@@ -1003,5 +1025,5 @@ button[aria-busy="true"]:is([data-card-bump]) svg { fill: currentColor; }
|
|
|
1003
1025
|
.pane-viewer .ps-sheet button span + span { color: var(--muted); }
|
|
1004
1026
|
}
|
|
1005
1027
|
/* The Send bar is the last thing on its page: only the phone nav under it is left clear, so its background runs to the edge. */
|
|
1006
|
-
body .view-questions main { padding-bottom: var(--app-bottom, 0px); }
|
|
1028
|
+
body .view-questions.view-questions main { padding-bottom: var(--app-bottom, 0px); }
|
|
1007
1029
|
.pfile.retry { cursor: pointer; }
|
|
@@ -44,7 +44,15 @@
|
|
|
44
44
|
if (w.state === 'reset') return `Reset ${esc(usageClock(w.resets_at))}`;
|
|
45
45
|
return `<span class="u-num">${Math.round(w.used_percentage)}%</span> · resets ${esc(usageClock(w.resets_at))}`;
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
// Claude's week shows as a second bar once it is live within the near band under the pause line, which starts twice the
|
|
48
|
+
// buffer below 100 (shared/usage-gate.mjs nearFrom): orange under the line, red at it. Off (100) has no band.
|
|
49
|
+
function weekBar(reading, pauseAt) {
|
|
50
|
+
const w = reading.seven_day;
|
|
51
|
+
if (!(pauseAt < 100) || w?.state !== 'live' || !(w.used_percentage >= 2 * pauseAt - 100)) return '';
|
|
52
|
+
const tone = w.used_percentage >= pauseAt ? 'full' : 'high';
|
|
53
|
+
return `<span class="u-one u-tone-${tone}"><span class="u-line"><span class="u-name">Claude week</span> ${usageWords(reading, 'seven_day')}</span>${usageBar(w)}</span>`;
|
|
54
|
+
}
|
|
55
|
+
function sidebarUsage(data, pauseAt) {
|
|
48
56
|
const products = usageProducts(data);
|
|
49
57
|
return products.length ? `<span class="sb-usage">${products.map((p) => {
|
|
50
58
|
const r = data[p];
|
|
@@ -54,7 +62,7 @@
|
|
|
54
62
|
: w.state === 'stale' ? `${name} · last seen ${esc(usageClock(r.at))}`
|
|
55
63
|
: w.state === 'reset' ? `${name} · reset ${esc(usageClock(w.resets_at))}`
|
|
56
64
|
: `${name} ${usageWords(r, 'five_hour')}`;
|
|
57
|
-
return `<span class="u-one u-tone-${usageTone(w)}"><span class="u-line">${words}</span>${usageBar(w)}</span
|
|
65
|
+
return `<span class="u-one u-tone-${usageTone(w)}"><span class="u-line">${words}</span>${usageBar(w)}</span>${p === 'claude' ? weekBar(r, pauseAt) : ''}`;
|
|
58
66
|
}).join('')}</span>` : '';
|
|
59
67
|
}
|
|
60
68
|
// The utility rows, in the order they show.
|
|
@@ -227,7 +235,7 @@
|
|
|
227
235
|
const turnOn = device === 'off' ? row({ href: '/_hub/#notifications', icon: tile('bell', 'var(--accent)'), title: 'Turn on notifications', sub: 'Off on this device', cls: 'sb-turnon' })
|
|
228
236
|
: device === 'install' ? row({ href: '/_hub/#notifications', icon: tile('bell', 'var(--accent)'), title: 'Add Claude to your Home Screen', sub: 'Notifications come only to the Home Screen app', cls: 'sb-turnon' })
|
|
229
237
|
: '';
|
|
230
|
-
const usage = sidebarUsage(data?.usage);
|
|
238
|
+
const usage = sidebarUsage(data?.usage, data?.pause?.pause_at);
|
|
231
239
|
const hub = data?.hub?.tone === 'down' || data?.hub?.tone === 'wait' ? data.hub : paused ? { tone: 'paused', word: 'paused', detail: pauseUntilWords(data.pause.resets_at) } : busy ? { tone: 'wait', word: 'busy', detail: 'the host is busy' } : data?.hub;
|
|
232
240
|
const n = data?.notifications || {};
|
|
233
241
|
const lower = (text) => String(text || '').replace(/^\w/, (c) => c.toLowerCase());
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
: m.request ? `<div class="msg status-line request${m.command === 'readied' ? ' readied' : ''}">${statusMark(m.mark || m.text)}${escText(m.text)} <span class="muted">· ${m.by ? `${esc(m.by)} · ` : ''}${esc(time(m.ts))}</span></div>`
|
|
50
50
|
// docs/adr/2026-11-09-1200-the-board-writes-status-lines-and-only-the-orchestrator-writes-messages.md: the board's
|
|
51
51
|
// own line, its kind's mark and words, red for a fault.
|
|
52
|
+
: m.kind === 'hub' && m.detail ? `<details class="msg status-line request hub fault-detail${m.fault ? ' fault' : ''}"><summary>${statusMark(m.mark)}${escText(m.text)} <span class="muted">· ${esc(time(m.ts))}</span></summary><pre class="fault-error">${esc(m.detail)}</pre></details>`
|
|
52
53
|
: m.kind === 'hub' ? `<div class="msg status-line request hub${m.fault ? ' fault' : ''}">${statusMark(m.mark)}${m.link ? `<a href="${esc(m.link)}">${escText(m.text)}</a>` : escText(m.text)} <span class="muted">· ${esc(time(m.ts))}</span></div>`
|
|
53
54
|
// docs/adr/2026-11-07-1400-a-card-stores-its-lane-and-facts-replace-statuses.md: a card's move between lanes.
|
|
54
55
|
: m.kind === 'move' ? `<div class="msg status-line request move">${statusMark('move')}${escText(m.text)} <span class="muted">· ${esc(time(m.ts))}</span></div>`
|
package/board/render-check.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)[0];let
|
|
3
|
-
`)[0]||t.text,"described");async function
|
|
4
|
-
`)[0]}`):
|
|
1
|
+
var A=Object.defineProperty;var l=(t,e)=>A(t,"name",{value:e,configurable:!0});import g from"node:fs";import k from"node:fs/promises";import B from"node:http";import P from"node:os";import m from"node:path";import{spawn as R}from"node:child_process";import{setTimeout as b}from"node:timers/promises";import{fileURLToPath as D}from"node:url";import{createInterface as O}from"node:readline";import{BoardError as w,readPrototypes as F}from"./store.mjs";import{prototypePage as G}from"./prototype-page.mjs";import{BASE_KEY as U,baseWorktreeOf as K}from"./prototype-base.mjs";import{buildScriptError as W,buildScriptOf as C}from"./build-script.mjs";import{boardAt as q}from"../hub/boards.mjs";const S=m.dirname(D(import.meta.url)),_=[390,1280],j=process.env.CHROME||"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",T={".js":"text/javascript",".css":"text/css",".html":"text/html",".json":"application/json",".png":"image/png",".svg":"image/svg+xml"},N=3e3,J=300,Y=12e4,M="(document.getElementById('stage')?.innerHTML || '').trim()",V="(document.body?.innerText || '').trim()";function z(t,e){const n=[m.join(t,"public"),m.join(S,"public"),m.join(S,"..","starter","public"),m.join(S,"..","shared")];return B.createServer(async(u,r)=>{let o=decodeURIComponent(new URL(u.url,"http://x").pathname);const d=/^\/_hub\/(icon-(?:180|192|512)\.png)$/.exec(o);if(d){r.writeHead(200,{"content-type":T[".png"]}).end(await k.readFile(m.join(S,"..","hub",d[1])));return}if(["/_hub/kickflip.png","/_hub/hub.css","/_hub/colors.css"].includes(o)&&(o=o.slice(5)),o===`/prototype-${e.topic}.html`){r.writeHead(200,{"content-type":T[".html"]}).end(await G(t,e));return}for(const c of n){const i=m.resolve(c,`.${o}`);if(i.startsWith(c+m.sep)&&g.existsSync(i)&&g.statSync(i).isFile()){r.writeHead(200,{"content-type":T[m.extname(i)]||"application/octet-stream"}).end(await k.readFile(i));return}}r.writeHead(o==="/favicon.ico"?204:404).end()})}l(z,"serve");async function H(t){if(!g.existsSync(j))throw new w(`the render check needs Chrome; none at ${j}, set CHROME to its path`);const e=R(j,["--headless=new","--remote-debugging-port=0",`--user-data-dir=${t}`,"--no-first-run","about:blank"],{stdio:"ignore",env:{...process.env,HOME:P.userInfo().homedir}}),n=m.join(t,"DevToolsActivePort");for(let a=0;a<100&&!g.existsSync(n);a++)await b(100);if(!g.existsSync(n))throw e.kill(),new w("the render check could not start Chrome");const u=g.readFileSync(n,"utf8").split(`
|
|
2
|
+
`)[0];let r;for(let a=0;a<100&&!r;a++)r=(await fetch(`http://127.0.0.1:${u}/json`).then(p=>p.json(),()=>[])).find(p=>p.type==="page"),r||await b(100);if(!r)throw e.kill(),new w("the render check could not start Chrome");const o=new WebSocket(r.webSocketDebuggerUrl);try{await new Promise((a,p)=>{o.onopen=a,o.onerror=p})}catch{throw e.kill(),new w("the render check could not start Chrome")}let d=0;const c=new Map,i=[];return o.onmessage=({data:a})=>{const p=JSON.parse(a);p.id?c.get(p.id)?.(p):i.forEach(f=>f(p))},{cdp:l((a,p={})=>new Promise((f,$)=>{const y=++d;c.set(y,v=>v.error?$(new Error(v.error.message)):f(v.result)),o.send(JSON.stringify({id:y,method:a,params:p}))}),"cdp"),on:l(a=>(i.push(a),()=>i.splice(i.indexOf(a),1)),"on"),close:l(async()=>{o.close();const a=new Promise(p=>e.once("exit",p));e.kill(),await a},"close")}}l(H,"launchOnce");async function Q(t){try{return await H(t)}catch(e){if(!(e instanceof w)||!/could not start Chrome/.test(e.message))throw e;return await k.rm(t,{recursive:!0,force:!0}),await k.mkdir(t),H(t)}}l(Q,"launch");const X=l(t=>t.exception?.description?.split(`
|
|
3
|
+
`)[0]||t.text,"described");async function I({cdp:t,on:e},n,u,r=M){const o=[],d=e(s=>{s.method==="Runtime.exceptionThrown"?o.push(X(s.params.exceptionDetails)):s.method==="Runtime.consoleAPICalled"&&s.params.type==="error"?o.push(`console error: ${s.params.args.map(h=>h.value??h.description).join(" ").split(`
|
|
4
|
+
`)[0]}`):s.method==="Log.entryAdded"&&s.params.entry.level==="error"&&s.params.entry.source!=="javascript"&&!/\/favicon\.ico$/.test(s.params.entry.url||"")&&o.push(`${s.params.entry.text}${s.params.entry.url?` ${s.params.entry.url}`:""}`)});await t("Emulation.setDeviceMetricsOverride",{width:u,height:900,deviceScaleFactor:1,mobile:u<600}),await t("Page.navigate",{url:n});const c=l(async()=>(await t("Runtime.evaluate",{expression:r,returnByValue:!0})).result.value,"stage");let i="";for(let s=0;s<N&&!i;s+=100)await b(100),i=await c();return await b(J),d(),i||o.push(r===M?"renders an empty stage":"renders an empty page"),[...new Set(o)]}l(I,"checkTake");async function Z(t){const e=q(t);if(!e)throw new w(`${t} is not a board in the hub's manifest, so it has no base prototype worktree to check`);const n=K(e.route),u=m.join(n,C(e.build_target)),r=W(u);if(r)throw new w(`the base prototype worktree can't serve the round: ${r} (${u})`);const o=R(u,[U,n],{cwd:n,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:e.route,HOST:"127.0.0.1"}}),d=[],c=l(f=>{d.push(f),d.length>5&&d.shift()},"keep");O({input:o.stderr}).on("line",c);const i=new Promise(f=>o.once("exit",f)),s=l(async()=>{try{process.kill(-o.pid,"SIGTERM")}catch{return}if(!await Promise.race([i.then(()=>!0),b(4e3).then(()=>!1)])){try{process.kill(-o.pid,"SIGKILL")}catch{}await i}},"stop"),h=await new Promise(f=>{const $=O({input:o.stdout}),y=l(x=>{clearTimeout(v),$.close(),f(x)},"done"),v=setTimeout(()=>y(null),Y);$.on("line",x=>{const E=/^port (\d{1,5})$/.exec(x);E?y(Number(E[1])):c(x)}),i.then(()=>y(null))}),a=l(()=>fetch(`http://127.0.0.1:${h}/`,{signal:AbortSignal.timeout(1e3)}).then(()=>!0,()=>!1),"answers");let p=!!h;for(let f=0;p&&!await a();f+=250)f>=6e4?p=!1:await b(250);if(!p)throw await s(),new w(`the base prototype worktree's ${C(e.build_target)} didn't serve on a port, so the round's question is not published:
|
|
5
5
|
${d.join(`
|
|
6
|
-
`)}`);return{origin:`http://127.0.0.1:${
|
|
6
|
+
`)}`);return{origin:`http://127.0.0.1:${h}`,dir:n,stop:s}}l(Z,"serveBase");async function L(t){const e=await k.mkdtemp(m.join(P.tmpdir(),"render-check-"));let n;try{return n=await Q(e),await n.cdp("Runtime.enable"),await n.cdp("Log.enable"),await t(n)}finally{await n?.close(),await k.rm(e,{recursive:!0,force:!0,maxRetries:30,retryDelay:200})}}l(L,"withChrome");async function ee(t,e){const n=await Z(t);try{const u=e.takes[0].key,r=(e.states||[]).map(c=>c.key),o=[...e.takes.map(c=>[c.key,r[0]??null]),...r.slice(1).map(c=>[u,c])],d=await L(async c=>{const i=[];for(const[s,h]of o){const a=new URL(e.served,n.origin);a.searchParams.set("variant",s),h&&a.searchParams.set("state",h);for(const p of _)for(const f of await I(c,a.href,p,V))i.push(`take ${s}${h?`, state ${h}`:""} at ${p}px: ${f}`)}return i});if(d.length)throw new w(`prototype ${e.topic} does not render, so its question is not published; fix its page in ${n.dir} and ask again:
|
|
7
7
|
${d.join(`
|
|
8
|
-
`)}`)}finally{await
|
|
9
|
-
${
|
|
10
|
-
`)}`)}finally{u.close()}}l(we,"checkTakesRender");export{
|
|
8
|
+
`)}`)}finally{await n.stop()}}l(ee,"checkServedRender");async function we(t,e){const n=(await F(t)).find(r=>r.topic===e);if(n.served)return ee(t,n);const u=z(t,n);await new Promise(r=>u.listen(0,"127.0.0.1",r));try{const r=`http://127.0.0.1:${u.address().port}/prototype-${e}.html`,o=await L(async d=>{const c=[];for(const i of n.takes)for(const s of _)for(const h of await I(d,`${r}?variant=${i.key}`,s))c.push(`take ${i.key} at ${s}px: ${h}`);return c});if(o.length)throw new w(`prototype ${e} does not render, so its question is not published; fix the take files and ask again:
|
|
9
|
+
${o.join(`
|
|
10
|
+
`)}`)}finally{u.close()}}l(we,"checkTakesRender");export{_ as WIDTHS,we as checkTakesRender};
|