@leftoff/cli 0.9.17-beta.4 → 0.9.17-beta.5
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 +1 -0
- package/bin/leftoff.js +28 -28
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/register.js +1 -1
- package/package.json +1 -1
- package/ui/app.css +44 -11
- package/ui/app.js +53 -5
- package/ui/icons.js +6 -0
- package/ui/semdiff.js +73 -20
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
What changed in Leftoff (called Checkpoints until 0.4.5), newest first. The board's **What's new** page and `leftoff changelog` both read this file.
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
|
+
- **Walkthrough: file status, standard diff colours, steadier scrolling.** Files show added, modified, deleted or renamed as GitHub-style square icons (+, dot, −, arrow) with the word in a tip. Diffs use GitHub's green and red everywhere, a step stronger in the line numbers. In Files, the file list and the diff scroll separately (on a phone the list folds above the diff). A slide no longer jumps back to the top when the session updates in the background, and the slides / one-page button has a tip.
|
|
6
7
|
- **The downloaded walkthrough shows its code again.** In the .html slideshow, the hidden slide switches shared a style with the diff lines, so every code line was hidden and slides showed only their notes.
|
|
7
8
|
- **Walk me through it.** Next to Catch me up in a session's panel, **Walk me through it** opens the session's changes as a story in place of the chat (‹ Chat goes back): a one-line summary ("9 files · 2 that matter · ~2 min read"), a bar of Core, Supporting, Mechanical and Generated drawn with a pattern and a word as well as a colour, then the changes in reading order. Core steps show their code; Supporting steps are one line with **Show code**. Things worth a second look sit under the line they mean with a glyph and a word (Look closely, Assumption, Untested), and the line is marked in the gutter. Tests that only follow along, imports, whitespace, renames, lockfiles and the same edit repeated in many files fold into **Safe to skip** groups, and a line at the end says every changed line is in a step or a group. Read since the last checkpoint, the whole session, or the session's PR (the newest checkpoint on the timeline also has **Walk through since here**). **Files** shows the plain diff file by file. Filter to **Look closely**, **Mark reviewed** each step ("1 of 4 reviewed", kept in the session's record like a pin, on every Mac), move with j and k, and **Ask about this change…** puts your question, with its file and line, in the chat box. The eye on the session's tab and board card says how many things are left to look closely at. A big change gets a file map, and is read folder by folder. **Download .html** saves one self-contained page; **Copy as Markdown** is ready to paste into a PR. On a phone it's a tall sheet. Catch me up and Walk me through it always fit in the panel, never clipped: in a narrow panel "Away 3h" becomes a small line under Catch me up, and narrower still they stack. On a desktop its controls are compact and sit on one row with the exports (dimmed, with a reason, when there's nothing to export yet); if nothing changed since the last checkpoint it shows the whole session instead of a dead end. Who writes it is set under Settings → Models & summaries (below); with no model, the plain diff and a note. After updating, run `leftoff restart`.
|
|
8
9
|
- **A session opens full screen, with its page beside the chat.** Opening a session from a card, an Open chip, a tab or a link lands in full-screen Live. On the right, a panel for that session: its name, status, repo · branch and machine; **Catch me up** at the top ("Away 3 hours · Catch me up" after time away) with **Checkpoint now** beside it; then checkpoints (newest first), what it did and details; a Terminal toggle at the bottom. Timeline, Copy resume, Copy Live resume and Done are in its ⋯. Hide it to a slim rail with ⌘\ or its chevron; it stays the way you left it. On a phone it's a sheet along the bottom that shows one line ("Working · 4 checkpoints") and Catch me up; tap it to pull it up. Between phone and laptop widths it starts as the rail, so the chat has room, until you open it. The separate session page is gone, so there's one layout. The Board tab (or Back) goes to the board; Esc in the message box clears a suggestion and no longer leaves.
|
package/bin/leftoff.js
CHANGED
|
@@ -182,59 +182,59 @@ Turn it off first: tailscale funnel --https=`+y+" off"),1;if((S.proxy||S.other)&
|
|
|
182
182
|
`),t=[],s=!1;for(let i of e.split(`
|
|
183
183
|
`)){let r=i.trim();if(/^(```|~~~)/.test(r)){s=!s;continue}if(r){if(!s){if(/^([-*_])(\s*\1){2,}$/.test(r))continue;let o;if(o=r.match(/^#{1,6}\s+(.*?)\s*#*$/)){r=tE(o[1]),r&&!/[.:!?…]$/.test(r)&&(r+=":"),t.push(r);continue}r=r.replace(/^>\s?/,""),(o=r.match(/^[-*+]\s+(.*)$/))&&(r="• "+o[1])}t.push(s?r:tE(r))}}return t.join(" ").replace(/\s+/g," ").trim()}function tE(n){return n.replace(/!\[([^\]]*)\]\([^)]*\)/g,"$1").replace(/\[([^\]]+)\]\([^)]*\)/g,"$1").replace(/\*\*([^*]+)\*\*/g,"$1").replace(/__([^_]+)__/g,"$1").replace(/(^|[^\w*])\*([^*\s][^*]*?)\*(?![\w*])/g,"$1$2").replace(/(^|[^\w])_([^_\s][^_]*?)_(?!\w)/g,"$1$2").replace(/~~([^~]+)~~/g,"$1").trim()}function _n(n){let e=String(n??"");if(!/<\/?channel\b/i.test(e))return e.trim();for(let t=0;t<8&&_p.test(e);t++)_p.lastIndex=0,e=e.replace(_p,"$1");return _p.lastIndex=0,e=e.replace(/<channel\b[^>]*>/g,"").replace(/<\/channel\s*>/g,"").replace(/<\/?channel\b[^>]*$/,""),e.trim()}function Ep(n){let e=String(n||"").trim(),t=[[/^I(?:'|’)ve\s+/,"Claude has "],[/^I(?:'|’)m\s+/,"Claude is "],[/^I(?:'|’)ll\s+/,"Claude will "],[/^I(?:'|’)d\s+/,"Claude would "],[/^I am\s+/,"Claude is "],[/^I have\s+/,"Claude has "],[/^I was\s+/,"Claude was "],[/^I\s+(?=[a-z])/,"Claude "]],s=!1;for(let[i,r]of t)if(i.test(e)){e=e.replace(i,r),s=!0;break}return s&&(e=e.replace(/\b(and|then|so|but|,)\s+I(?:'|’)ve\b/g,"$1 it has").replace(/\b(and|then|so|but|,)\s+I\b(?!['’])/g,"$1 it"),e=e.replace(/\b(the \w+|that|which) I (added|made|wrote|changed|removed|created|put)\b/g,"$1 it $2"),e=e.replace(/\bmyself\b/g,"itself")),Tp.test(e)?null:e}function K6(n){let e=String(n??"");if(!/(^|\n|:)\s*(?:[-*+•]|\d{1,3}[.)])\s+\S/.test(e))return e;let t="",s=!1;for(let i of e.replace(/\r\n?/g,`
|
|
184
184
|
`).split(`
|
|
185
|
-
`)){let r=i.trim();if(!r)continue;let o=r.split(/(?<=:)\s+(?=(?:[-*+•]|\d{1,3}[.)])\s+\S)/);for(let a of o){let l=a.match(/^(?:[-*+•]|\d{1,3}[.)])\s+(.*)$/);if(l){let c=$6(l[1].replace(/\s+(?:[-*+•])\s+(?=\S)/g,"; ").replace(/[\s.;,]+$/,""));t+=s?"; "+c:(/:$/.test(t)?" ":t?": ":"")+c,s=!0}else s&&(t+="."),t+=(t?" ":"")+a,s=!1}}return s&&(t+="."),t.replace(/\s+/g," ").replace(/\.\.$/,".").trim()}function vi(n){if(!n||typeof n!="object")return n;let e={...n},t=!1,s=(r,o)=>{o!==e[r]&&(e[r]=o,t=!0)};for(let r of["doing","stoppedAt","next","openQuestion","recap","userNote","sessionTitle"])typeof e[r]=="string"&&/<\/?channel\b/i.test(e[r])&&s(r,_n(e[r]));for(let r of["nextSteps","questions","decisions","didntWork"])Array.isArray(e[r])&&e[r].some(o=>typeof o=="string"&&/<\/?channel\b/i.test(o))&&s(r,e[r].map(o=>typeof o=="string"?_n(o):o));if((n.generator&&n.generator.by==="heuristic"||W6.test(String(e.doing||"")))&&e.format!=="markdown"){if(typeof e.doing=="string"&&s("doing",e.doing.replace(/^(Working on:\s*[“"])\s+/,"$1").replace(/\s+([”"])$/,"$1")),typeof e.stoppedAt=="string"&&e.stoppedAt&&!/^(?:Last error:|No reply captured|Claude’s last word:)/.test(e.stoppedAt)){let r=K6(e.stoppedAt),o=Ep(r);s("stoppedAt",o??"Claude’s last word: “"+r.replace(/^[“"]|[”"]$/g,"")+"”")}typeof e.next=="string"&&Yu.test(e.next.trim())&&s("next",""),Array.isArray(e.nextSteps)&&e.nextSteps.some(r=>Yu.test(String(r||"").trim()))&&s("nextSteps",e.nextSteps.filter(r=>!Yu.test(String(r||"").trim())))}return t?e:n}var Ee,_p,Tp,Yu,G6,$6,W6,Hn=K(()=>{Ee=n=>{let e=_n(n);return B6(e)?z6(e):e},_p=/<channel\b[^>]*>([\s\S]*?)<\/channel\s*>/g;Tp=/\b(?:I|I'(?:m|ve|ll|d)|I’(?:m|ve|ll|d)|me|my|mine|myself)\b/;Yu=/^(?:resume|reopen|open|continue|pick (?:it )?(?:back )?up)\b[^.]*\b(?:session|last reply|where (?:it|you|claude) left off)\b|^(?:continue|resume)\.?$/i,G6=/^(?:A|An|The|Added|Removed|Changed|Fixed|Updated|Moved|Renamed|Created|Deleted|Wrote|Made|Kept|Put|Ran|Used)\b/,$6=n=>G6.test(n)?n.charAt(0).toLowerCase()+n.slice(1):n;W6=/^(?:Working on:\s*[“"]|No prompt captured)/});import{spawn as Y6,execFileSync as q6}from"node:child_process";import J6 from"node:fs";import bk from"node:path";function Q6(){if(process.env.CHECKPOINTS_NO_CLAUDE)return!1;try{return q6(process.platform==="win32"?"where":"which",["claude"],{stdio:"ignore"}),!0}catch{return!1}}function iE(n){let e=String(n||"");return/previous checkpoint\s*:/i.test(e)||/files touched since then\s*:/i.test(e)||/^\s*repo\s*:.*·\s*cwd\b/im.test(e)||/title\s*:\s*\(none yet\)/i.test(e)||/transcript \(oldest→newest/i.test(e)||/what the developer asked for\s*:/i.test(e)||/before \/clear, this tab was working toward/i.test(e)||/uncommitted files now\s*:/i.test(e)}function
|
|
186
|
-
`),usage:d.usage?{inputTokens:d.usage.input_tokens,outputTokens:d.usage.output_tokens}:null}}function
|
|
185
|
+
`)){let r=i.trim();if(!r)continue;let o=r.split(/(?<=:)\s+(?=(?:[-*+•]|\d{1,3}[.)])\s+\S)/);for(let a of o){let l=a.match(/^(?:[-*+•]|\d{1,3}[.)])\s+(.*)$/);if(l){let c=$6(l[1].replace(/\s+(?:[-*+•])\s+(?=\S)/g,"; ").replace(/[\s.;,]+$/,""));t+=s?"; "+c:(/:$/.test(t)?" ":t?": ":"")+c,s=!0}else s&&(t+="."),t+=(t?" ":"")+a,s=!1}}return s&&(t+="."),t.replace(/\s+/g," ").replace(/\.\.$/,".").trim()}function vi(n){if(!n||typeof n!="object")return n;let e={...n},t=!1,s=(r,o)=>{o!==e[r]&&(e[r]=o,t=!0)};for(let r of["doing","stoppedAt","next","openQuestion","recap","userNote","sessionTitle"])typeof e[r]=="string"&&/<\/?channel\b/i.test(e[r])&&s(r,_n(e[r]));for(let r of["nextSteps","questions","decisions","didntWork"])Array.isArray(e[r])&&e[r].some(o=>typeof o=="string"&&/<\/?channel\b/i.test(o))&&s(r,e[r].map(o=>typeof o=="string"?_n(o):o));if((n.generator&&n.generator.by==="heuristic"||W6.test(String(e.doing||"")))&&e.format!=="markdown"){if(typeof e.doing=="string"&&s("doing",e.doing.replace(/^(Working on:\s*[“"])\s+/,"$1").replace(/\s+([”"])$/,"$1")),typeof e.stoppedAt=="string"&&e.stoppedAt&&!/^(?:Last error:|No reply captured|Claude’s last word:)/.test(e.stoppedAt)){let r=K6(e.stoppedAt),o=Ep(r);s("stoppedAt",o??"Claude’s last word: “"+r.replace(/^[“"]|[”"]$/g,"")+"”")}typeof e.next=="string"&&Yu.test(e.next.trim())&&s("next",""),Array.isArray(e.nextSteps)&&e.nextSteps.some(r=>Yu.test(String(r||"").trim()))&&s("nextSteps",e.nextSteps.filter(r=>!Yu.test(String(r||"").trim())))}return t?e:n}var Ee,_p,Tp,Yu,G6,$6,W6,Hn=K(()=>{Ee=n=>{let e=_n(n);return B6(e)?z6(e):e},_p=/<channel\b[^>]*>([\s\S]*?)<\/channel\s*>/g;Tp=/\b(?:I|I'(?:m|ve|ll|d)|I’(?:m|ve|ll|d)|me|my|mine|myself)\b/;Yu=/^(?:resume|reopen|open|continue|pick (?:it )?(?:back )?up)\b[^.]*\b(?:session|last reply|where (?:it|you|claude) left off)\b|^(?:continue|resume)\.?$/i,G6=/^(?:A|An|The|Added|Removed|Changed|Fixed|Updated|Moved|Renamed|Created|Deleted|Wrote|Made|Kept|Put|Ran|Used)\b/,$6=n=>G6.test(n)?n.charAt(0).toLowerCase()+n.slice(1):n;W6=/^(?:Working on:\s*[“"]|No prompt captured)/});import{spawn as Y6,execFileSync as q6}from"node:child_process";import J6 from"node:fs";import bk from"node:path";function Q6(){if(process.env.CHECKPOINTS_NO_CLAUDE)return!1;try{return q6(process.platform==="win32"?"where":"which",["claude"],{stdio:"ignore"}),!0}catch{return!1}}function iE(n){let e=String(n||"");return/previous checkpoint\s*:/i.test(e)||/files touched since then\s*:/i.test(e)||/^\s*repo\s*:.*·\s*cwd\b/im.test(e)||/title\s*:\s*\(none yet\)/i.test(e)||/transcript \(oldest→newest/i.test(e)||/what the developer asked for\s*:/i.test(e)||/before \/clear, this tab was working toward/i.test(e)||/uncommitted files now\s*:/i.test(e)}function e3(n){let e=n.summarizer||{},t=String(e.provider||e.mode||"auto").toLowerCase();return sE[t]?sE[t]:Q6()?"claude":process.env.ANTHROPIC_API_KEY?"anthropic":"heuristic"}function t3({system:n=null,model:e=null}={}){let t=["-p","--model",e||"haiku","--output-format","text","--permission-mode","default"];return n&&t.push("--append-system-prompt",n),t}function n3(n=process.env){return{...n,CHECKPOINTS_INTERNAL:"1",LEFTOFF_INTERNAL:"1",CLAUDE_CODE_FORK_SUBAGENT:"0"}}function s3({system:n,prompt:e,model:t,bin:s},i=6e4){return new Promise((r,o)=>{let a=Y6(s||"claude",t3({system:n,model:t}),{env:n3(),stdio:["pipe","pipe","pipe"]}),l="",c="",u=setTimeout(()=>{a.kill("SIGKILL"),o(new Error("claude -p timed out"))},i);a.stdout.on("data",d=>l+=d),a.stderr.on("data",d=>c+=d),a.on("error",d=>{clearTimeout(u),o(d)}),a.on("close",d=>{clearTimeout(u),d===0?r({text:l.trim(),usage:null}):o(new Error("claude -p exited "+d+": "+c.slice(0,300)))}),a.stdin.end(e)})}async function xk(n,e,t,s,i){let r=await n(e,{method:"POST",headers:{"content-type":"application/json",...t},body:JSON.stringify(s),signal:AbortSignal.timeout(i)}),o=await r.json().catch(()=>({}));if(!r.ok)throw new Error(e+" -> "+r.status+": "+(o.error&&(o.error.message||o.error)||"error"));return o}function i3(n={},e=process.env){return xi("anthropic",n,e)}function xi(n,{apiKeyEnv:e,apiKeyFile:t,root:s}={},i=process.env){let r=Op[n]||{},o=e||r.env;if(o&&i[o])return i[o];for(let a of[t,e==="__none__"?null:r.file]){if(!a)continue;let l=bk.isAbsolute(a)?null:b_(a,i.HOME||i.XDG_CONFIG_HOME?i:process.env);for(let c of[l,bk.isAbsolute(a)?a:bk.join(s||re(),a)])if(c)try{let u=J6.readFileSync(c,"utf8").trim();if(u)return u}catch{}}return null}async function r3(n,{system:e,prompt:t,model:s,maxTokens:i,apiKeyEnv:r,apiKeyFile:o,root:a,timeoutMs:l}){let c=i3({apiKeyEnv:r,apiKeyFile:o,root:a});if(!c)throw new Error((r||"ANTHROPIC_API_KEY")+" is not set");let u=(process.env.ANTHROPIC_BASE_URL||"https://api.anthropic.com").replace(/\/$/,""),d=await xk(n,u+"/v1/messages",{"x-api-key":c,"anthropic-version":"2023-06-01"},{model:nE[s]||s||nE.haiku,max_tokens:i||300,system:e,messages:[{role:"user",content:t}]},l||3e4);return{text:(d.content||[]).filter(h=>h.type==="text").map(h=>h.text).join(`
|
|
186
|
+
`),usage:d.usage?{inputTokens:d.usage.input_tokens,outputTokens:d.usage.output_tokens}:null}}function o3(n,e,t){let s=Math.ceil(((n||"").length+(e||"").length)/3.2)+(t||300)+256;for(let i of vk)if(s<=i)return i;return vk[vk.length-1]}async function rE(n,{system:e,prompt:t,model:s,maxTokens:i,baseUrl:r,key:o,cloud:a,json:l,timeoutMs:c}){let u=a?Math.max(1024,(i||300)*4):i||300,d=await xk(n,r.replace(/\/$/,"")+"/api/chat",o?{authorization:"Bearer "+o}:{},{model:s,stream:!1,...l?{format:"json"}:{},messages:[...e?[{role:"system",content:e}]:[],{role:"user",content:t}],options:{num_predict:u,temperature:.2,...a?{}:{num_ctx:o3(e,t,u)}}},Math.max(c||0,a?6e4:12e4));return{text:d.message&&d.message.content||"",usage:{inputTokens:d.prompt_eval_count,outputTokens:d.eval_count}}}async function a3(n,{system:e,prompt:t,model:s,maxTokens:i,baseUrl:r,apiKeyEnv:o}){let a=o?process.env[o]:null,l=await xk(n,r.replace(/\/$/,"")+"/chat/completions",a?{authorization:"Bearer "+a}:{},{model:s,max_tokens:i||300,temperature:.2,messages:[...e?[{role:"system",content:e}]:[],{role:"user",content:t}]},12e4),c=l.choices&&l.choices[0];return{text:c&&c.message&&c.message.content||c&&c.text||"",usage:l.usage?{inputTokens:l.usage.prompt_tokens,outputTokens:l.usage.completion_tokens}:null}}function aE(n){let e=Ak(n,200).toLowerCase();return!e||c3.test(e)}function u3(n,{prompts:e=8,replies:t=1,max:s=300}={}){let i=(n||[]).filter(l=>!oE(l)),r=i.filter(l=>l.role==="user").slice(-e),o=new Set(i.filter(l=>l.role==="assistant").slice(-t)),a=new Set(r);return i.filter(l=>a.has(l)||o.has(l)).map(l=>({role:l.role,text:Ak(l.text,s)}))}function d3(n,{firstPrompt:e=""}={}){let t=n.filter(o=>o.role==="user").map(o=>o.text),s=n.filter(o=>o.role==="assistant").map(o=>o.text),i=Ak(e,200),r=[];return i&&!t.some(o=>o.startsWith(i.slice(0,80)))&&r.push("Session started with: "+i),t.length&&r.push(`Recent requests from the user, oldest first:
|
|
187
187
|
`+t.map(o=>"- "+o).join(`
|
|
188
188
|
`)),s.length&&r.push("Latest reply: "+s.join(" … ")),r.join(`
|
|
189
189
|
|
|
190
|
-
`)}function
|
|
190
|
+
`)}function h3(n){let e=n.filter(s=>s.role==="user").map(s=>s.text).filter(s=>!aE(s)),t=s=>s.split(" ").filter(Boolean).length;return[...e].reverse().find(s=>t(s)>=4)||e.at(-1)||""}var nE,V6,X6,Z6,Cp,Op,sE,vk,l3,oE,Ak,c3,Lt,Ho=K(()=>{Fe();Hn();Te();nE={haiku:"claude-haiku-5-5",sonnet:"claude-sonnet-5-5",opus:"claude-opus-5-5"},V6=["haiku","sonnet","opus",""],X6="https://ollama.com",Z6="gpt-oss:20b",Cp={ollama:{baseUrl:"http://localhost:11434",model:"llama3.2"},"ollama-cloud":{baseUrl:X6,model:Z6},"openai-compatible":{baseUrl:"http://localhost:1234/v1",model:"local-model"}},Op={anthropic:{env:"ANTHROPIC_API_KEY",file:".secret/anthropic-api-key"},"ollama-cloud":{env:"OLLAMA_API_KEY",file:".secret/ollama-api-key"}},sE={stub:"heuristic",none:"heuristic",heuristic:"heuristic",api:"anthropic",anthropic:"anthropic",claude:"claude",ollama:"ollama","ollama-cloud":"ollama-cloud","ollama.com":"ollama-cloud",cloud:"ollama-cloud","openai-compatible":"openai-compatible",openai:"openai-compatible",lmstudio:"openai-compatible",llamacpp:"openai-compatible",vllm:"openai-compatible"};vk=[8192,16384,32768];l3=`Write a title of at most 6 words naming the main thing this coding session is building or fixing, judging by all of the user's recent requests together. Ignore routine follow-ups like "check PR comments", "run tests", "commit", "continue" or "fix that": they are not the topic. `+w0+" Reply with the title only, no quotes, no punctuation at the end.",oE=n=>!n||!(n.role==="user"||n.role==="assistant")||!String(n.text||"").trim()||/^\s*<(command-|local-command|system-reminder)/.test(n.text)||/^\s*\//.test(n.text),Ak=(n,e)=>String(n||"").replace(/\s+/g," ").trim().slice(0,e),c3=/^(please |pls |ok(ay)?,? |now |and |then |can you |could you |go ahead and )*(check( for)?( the)?( new)?( pr| github)?( review)? comments|address( the)?( pr)?( review)? comments|(re)?run( the)?( unit| all)?( tests?| checks?| ci)|tests?|commit( it| this| that| and push)?|push( it)?|merge( it)?|ship( it)?|continue|keep going|go on|go ahead|do it|yes|yep|yeah|no|ok(ay)?|sure|thanks|thank you|lgtm|looks good|great|nice|fix (it|that|this|those|them)|try again|again|retry|what('| i)?s next|next|status|done\??)( please| pls)?[\s.!?]*$/i;Lt=class{constructor(e,{fetchImpl:t,root:s,claudeBin:i}={}){this.cfg=e,this.root=s||null,this.claudeBin=i||null,this.fetchImpl=t||((...r)=>fetch(...r)),this.mode=e3(e)}get model(){let e=String(this.cfg.summarizer&&this.cfg.summarizer.model||"");return Cp[this.mode]&&V6.includes(e)?Cp[this.mode].model:e||"haiku"}get baseUrl(){return this.cfg.summarizer&&this.cfg.summarizer.baseUrl||Cp[this.mode]&&Cp[this.mode].baseUrl||null}describe(){return{provider:this.mode,model:this.mode==="heuristic"?null:this.model,baseUrl:this.baseUrl}}async complete({system:e,prompt:t,maxTokens:s,json:i=!1,timeoutMs:r=null}){let o=this.cfg.summarizer||{},a={system:e,prompt:t,maxTokens:s,json:i,timeoutMs:r,model:this.model,baseUrl:this.baseUrl,apiKeyEnv:o.apiKeyEnv,apiKeyFile:o.apiKeyFile,root:this.root,bin:this.claudeBin};if(this.mode==="claude")return s3(a,r||void 0);if(this.mode==="anthropic")return r3(this.fetchImpl,a);if(this.mode==="ollama")return rE(this.fetchImpl,a);if(this.mode==="ollama-cloud"){let l=xi("ollama-cloud",{apiKeyEnv:o.apiKeyEnv,apiKeyFile:o.apiKeyFile,root:this.root});if(!l)throw new Error((o.apiKeyEnv||"OLLAMA_API_KEY")+" is not set (create a key at https://ollama.com/settings/keys)");return rE(this.fetchImpl,{...a,key:l,cloud:!0})}if(this.mode==="openai-compatible")return a3(this.fetchImpl,a);throw new Error("no model in heuristic mode")}async checkpoint({system:e,prompt:t,messages:s,session:i,maxTokens:r=300,length:o="short"}){if(this.mode!=="heuristic")try{let a=f=>String(f.text||"").replace(/<think>[\s\S]*?<\/think>/gi,"").trim(),l=await this.complete({system:e,prompt:t,maxTokens:r}),c=a(l);if(iE(c))throw new Error("the model repeated its instructions instead of summarizing");let u=p0(c),d=!1;if(u.parseOk===!1){d=!0;try{let f=await this.complete({system:e,prompt:QA(t),maxTokens:r}),p=a(f),m=iE(p)?null:p0(p);m&&m.parseOk!==!1?(u=m,l=f):m&&(u=ZA(u,m))}catch{}}let h={by:"daemon",provider:this.mode,model:this.model,...d?{retried:!0}:{}};return l.usage&&l.usage.inputTokens!=null&&(h.inputTokens=l.usage.inputTokens),l.usage&&l.usage.outputTokens!=null&&(h.outputTokens=l.usage.outputTokens),{...u,generator:h}}catch(a){let l=g0(s,i,{length:o});return vi({...l,generator:{by:"heuristic",fallbackFrom:this.mode,error:String(a.message||a).slice(0,160)}})}return vi({...g0(s,i,{length:o}),generator:{by:"heuristic"}})}async title(e){if(this.mode!=="heuristic"&&e.firstPrompt)try{let t=await this.titleInUserLanguage(e_,s=>`First prompt:
|
|
191
191
|
`+e.firstPrompt+`
|
|
192
192
|
|
|
193
|
-
`+s,e.firstPrompt);if(t)return{title:t,titleSource:"generated"}}catch{}return{title:y0(e),titleSource:"fallback"}}async titleInUserLanguage(e,t,s){for(let i of[!1,!0]){let r=await this.complete({system:e,prompt:t(a_(s,{firm:i})),maxTokens:30}),o=t_(r.text);if(!o)return null;if(n_(o,s))return o}return null}async recentTitle(e,t={}){let s=
|
|
194
|
-
`),l=
|
|
195
|
-
|
|
196
|
-
`+u,a||r);if(c)return{title:c,titleSource:"generated",by:this.mode}}catch{}let o=
|
|
197
|
-
`))>=0;)c(a.slice(0,d)),a=a.slice(d+1)}if(c(a),!o||!o.done)throw new Error("pull "+n+" ended early (last status: "+(o&&o.status||"none")+")");return o}async function Ti(n,{fetchImpl:e,transcriptFile:t=
|
|
198
|
-
`).pop()||"exit "+c.code;if(!i.installed){o(["plugin","marketplace","update",Gs]);let c=o(["plugin","install",lt]);if(c.code!==0&&!/already installed/i.test(c.stdout+c.stderr))return{ok:!1,migrated:!1,steps:r,error:a(c)};i.legacyEnabled===!1&&o(["plugin","disable",lt])}let l=o(["plugin","uninstall",Tr]);return l.code!==0?{ok:!1,migrated:!0,steps:r,error:"installed "+lt+" but couldn't uninstall "+Tr+": "+a(l)}:{ok:!0,migrated:!0,enabled:i.legacyEnabled!==!1,steps:r}}function Mk({bin:n,source:e,env:t=process.env,onStep:s=()=>{}}){let i=[],r=Dp(n,t);if(r.legacyInstalled){let l=Ok({bin:n,env:t,state:r,onStep:s});if(!l.ok)return{ok:!1,steps:l.steps,state:r,error:l.error};let c=Mk({bin:n,source:e,env:t,onStep:s});return{...c,migrated:l.migrated,steps:[...l.steps,...c.steps]}}if(r.installed&&r.enabled)return{ok:!0,already:!0,steps:i,state:r};if(r.installed&&!r.enabled){s("claude plugin enable "+lt);let l=Ei(n,["plugin","enable",lt],{env:t});return i.push(l),{ok:l.code===0,already:!0,enabled:l.code===0,steps:i,state:r,error:l.code?(l.stderr||l.stdout).trim():null}}if(!r.marketplaceAdded){s("claude plugin marketplace add "+e);let l=Ei(n,["plugin","marketplace","add",e],{env:t});if(i.push(l),l.code!==0&&!/already/i.test(l.stdout+l.stderr))return{ok:!1,steps:i,state:r,error:(l.stderr||l.stdout).trim()||"exit "+l.code}}s("claude plugin install "+lt);let o=Ei(n,["plugin","install",lt],{env:t});i.push(o);let a=o.code===0||/already installed/i.test(o.stdout+o.stderr);return{ok:a,steps:i,state:r,error:a?null:(o.stderr||o.stdout).trim()||"exit "+o.code}}function gE(n){return!!(n&&n.disableAllHooks===!0)}var kl,Rp,Gs,vU,lt,Tr,Xu,Ci=K(()=>{Bs();Wt();kl=ze.resolve(ze.dirname(bU(import.meta.url)),".."),Rp=gi,Gs=T_,vU=E_,lt=vU+"@"+Gs,Tr=C_+"@"+Gs,Xu="2.1.287"});import wE from"node:fs";import Fp from"node:path";function Er(n,{statfs:e=wE.statfsSync}={}){if(!n||typeof e!="function")return null;let t=Fp.resolve(String(n));for(let s=0;s<64;s++){try{let r=e(t),o=Number(r.bsize||r.frsize||0),a=Number(r.blocks)*o,l=Number(r.bavail!=null?r.bavail:r.bfree)*o;return a?{path:t,freeBytes:l,totalBytes:a,pct:Math.round(l/a*1e4)/100}:null}catch{}let i=Fp.dirname(t);if(i===t)return null;t=i}return null}function IU(n){return!n||!n.totalBytes?"ok":n.pct<EU?"red":n.freeBytes<Math.max(n.totalBytes*CU/100,OU)?"warn":"ok"}function $o(n){return n=Number(n)||0,n>=100*Zu?Math.round(n/Zu)+" GB":n>=Zu?Math.round(n/Zu*10)/10+" GB":Math.round(n/1024**2)+" MB"}function Hp(n){let e={ok:0,warn:1,red:2},t=null;for(let s of n||[]){if(!s)continue;let i=IU(s);(!t||e[i]>e[t.level]||e[i]===e[t.level]&&s.freeBytes<t.freeBytes)&&(t={...s,level:i})}return t}function Up(n,{machine:e="this Mac"}={}){if(!n||n.level==="ok")return null;let t=$o(n.freeBytes)+" free";return n.level==="red"?"Disk full on "+e+": "+t+" ("+kE(n.pct)+"). Syncing has stopped.":"Low disk on "+e+": "+t+". Syncing stops below 1%."}function Go(n,{platform:e="darwin"}={}){let t=n?$o(n.freeBytes)+" free":null;return{text:"free up disk space"+(t?" (you have "+t+(n.pct!=null?", "+kE(n.pct):"")+")":"")+"; Syncthing resumes on its own",more:[...e==="darwin"?["storage settings: "+yE]:["see what's big: du -sh ~/* | sort -h | tail"],"last resort: lower Syncthing's minHomeDiskFree ("+NU+")"],cmd:e==="darwin"?yE:null}}function SE(n,e={}){let t=e.localErrors||[];if(t.some(i=>/insufficient space/i.test(i.error||"")))return{text:"this Mac's disk is full (see the folder error below); "+n+" reconnects once you free space here"};if(t.length)return{text:"fix the folder error below first; "+n+" reconnects on its own"};let s=e.peerHealth;return s&&s.disk&&s.disk.level&&s.disk.level!=="ok"?{text:n+" is low on disk ("+$o(s.disk.freeBytes)+" free when last heard); free up space there"}:{text:"wake "+n+" (open it, or check it's on and online); if it is, run there: leftoff doctor",more:["if Syncthing isn't running on "+n+": leftoff sync start (on "+n+")"]}}function LU(n){if(/^[\w@%+=:,./~-]+$/.test(n))return n;let e=n.startsWith("~/")?"~/":"";return e+"'"+n.slice(e.length).replace(/'/g,"'\\''")+"'"}function Qu(n,e={}){let t=String(n||"");if(!/^(!|✗)\s/.test(t))return null;let s=t.replace(/^(!|✗)\s+/,""),i=e.platform||"darwin",r=e.syncthing||{},o=r.folder&&r.folder.errors||[],a=i==="darwin"&&(e.syncthingVia==="brew"||r.via==="brew"),l=i==="linux"&&!a&&!!(e.syncthingSystemd||r.service&&r.service.systemd&&r.service.unit),c=a?{text:"brew services restart syncthing",auto:{id:"syncthing-restart",label:"restart Syncthing (brew services restart syncthing)"}}:l?{text:"systemctl --user restart syncthing",auto:{id:"syncthing-restart",label:"restart Syncthing (systemctl --user restart syncthing)"}}:null,u;if(u=s.match(/insufficient space on disk(?: for (?:the )?database)?\s*\(([^)]+)\)/i)){let d=typeof e.disk=="function"?e.disk(u[1]):null;return Go(d,{platform:i})}if(/insufficient space/i.test(s))return Go(null,{platform:i});if(/^(low disk|disk full)\b/i.test(s)){let d=e.lowDisk&&e.lowDisk.freeBytes!=null?e.lowDisk:null;return Go(d,{platform:i})}if(u=s.match(/null\/ folder is committed in (.+): git rm -r null\/$/))return{text:"cd "+LU(u[1])+' && git rm -r null/ && git commit -m "Remove stray null/ folder"'};if(u=s.match(/^worktree (.+?) is still locked by Leftoff/)){let d=(e.parkLocks||[]).find(f=>f.path===u[1]),h=f=>/^[A-Za-z0-9_./~@%+=:,-]+$/.test(f)?f:"'"+String(f).replace(/'/g,"'\\''")+"'";return{text:"git -C "+h(d&&d.root||u[1])+" worktree unlock "+h(u[1])+" (unlocks it; nothing is removed)"}}if(/^daemon on /.test(s))return{text:"leftoff start",more:["or run it in this terminal to see why it stops: leftoff serve"],auto:e.serviceInstalled?{id:"daemon-restart",label:"restart the Leftoff daemon"}:null};if(/^folder /.test(s))return{text:"leftoff setup (it makes the folder and picks up where it left off)"};if(/^token /.test(s))return{text:"leftoff setup (writes a new board token)"};if(u=s.match(/^mod command \/(\S+) failed to register/))return{text:"start a new Claude Code session (commands register at start); if /"+u[1]+" fails again: leftoff upgrade --force"};if(/^mod commands failed to register/.test(s))return{text:"start a new Claude Code session; if it fails again: leftoff upgrade --force"};if(/^update available/.test(s))return{text:"leftoff upgrade"};if(/^settings hooks are from before/.test(s)||/^the login service still has its old name/.test(s)||/^login service is from before/.test(s))return{text:"leftoff upgrade --force"};if(/^digest:/.test(s))return{text:"leftoff digest",auto:{id:"digest",label:"make today's digest now"}};if(/known only by id/.test(s))return{text:"nothing to run: it shows as itself after that session's next prompt, or closes with it"};if(/^API key files inside the folder/.test(s))return{text:"leftoff start (the daemon moves them when it starts)",auto:e.serviceInstalled?{id:"daemon-restart",label:"restart the Leftoff daemon"}:null};if(/^remote access: remoteHosts is set but no tailscale/.test(s))return{text:"install Tailscale from https://tailscale.com/download, or turn remote access off: leftoff tailscale off"};if(/^remote access: /.test(s))return{text:"leftoff tailscale on",auto:{id:"tailscale-on",label:"point tailscale serve at the board again (tailnet only)"}};if(/^Syncthing: installed, not running/.test(s))return{text:a?"leftoff sync start (runs: brew services start syncthing)":l?"systemctl --user enable --now syncthing (or: leftoff sync start)":"leftoff sync start",auto:{id:"syncthing-start",label:l?"start Syncthing (systemctl --user enable --now syncthing)":"start Syncthing"}};if(/^Leftoff folder: .*Leftoff uses another folder/.test(s))return{text:"leftoff sync setup"};if(/^Leftoff folder: /.test(s))return o.some(d=>/insufficient space/i.test(d.error||""))?{text:"Syncthing stopped because the disk is full; see the folder error below"}:o.length?{text:"see the folder error below"}:c||{text:i==="darwin"?"quit and reopen Syncthing":"restart Syncthing (stop it, then: leftoff sync start)",auto:null};if(u=s.match(/^Peer (.+?)(?: \(Leftoff\))?: not connected/)){let d=u[1];return SE(d,{localErrors:o,peerHealth:(e.peers||{})[d]||null})}return(u=s.match(/^Peer (.+?)(?: \(Leftoff\))?: /))&&/not sharing|hasn.t accepted/.test(s)?{text:"on "+u[1]+": Settings → Sync, accept this Mac (or: leftoff sync accept)"}:/^Folder error: /.test(s)?/folder marker missing|\.stfolder/i.test(s)?{text:`the folder's .stfolder marker is gone; if the folder is really there: mkdir "`+(r.folder&&r.folder.path||"<folder>")+'/.stfolder"'}:/folder path missing|no such file/i.test(s)?{text:"the folder moved or was deleted; point Syncthing at it again: leftoff sync setup"}:/permission denied/i.test(s)?{text:i==="darwin"?"give your user access to the folder again (Finder → Get Info → Sharing & Permissions), then restart Syncthing":"give your user access to the folder again (chown/chmod), then restart Syncthing"}:{text:(c?c.text:"restart Syncthing")+"; if the error comes back, open Syncthing's web page for details",auto:c?c.auto:null}:(u=s.match(/^A (?:Mac|machine) named “(.+?)” asked to connect/))?{text:`only if it's your machine: leftoff sync accept "`+u[1]+'"'}:/^Syncthing/.test(s)?c||{text:"leftoff sync start",auto:{id:"syncthing-start",label:"start Syncthing"}}:(u=s.match(/^(.+?) stopped syncing/))?{text:"free up disk space on "+u[1]+"; it catches up on its own"}:(u=s.match(/^(.+?): data as of /))?{text:"wake "+u[1]+" (open it, or check it's online); its sessions update once it syncs"}:/^summarizer: the shared setting is Ollama/.test(s)?{text:"give this machine its own summarizer: Settings → Summarizer → Just this machine, or leftoff models set claude --this-machine"+(/isn.t answering/.test(s)?" (or start Ollama here: ollama serve)":"")}:/^summarizer: this machine.s own setting is Ollama/.test(s)?{text:"install Ollama from https://ollama.com/download, or use the shared setting here again: leftoff models shared"}:/^summarizer: Ollama isn.t answering/.test(s)?{text:i==="darwin"?"ollama serve (or open the Ollama app)":"ollama serve",auto:e.ollamaBinary?{id:"ollama-start",label:"start Ollama"}:null}:/^summarizer: .*Ollama isn.t installed/.test(s)?{text:"install Ollama from https://ollama.com/download, or pick another summarizer: leftoff models"}:/^summarizer: /.test(s)?{text:"leftoff models (pick or test the summarizer)"}:/^PR cards: gh is signed out/.test(s)?{text:"gh auth login"}:/^PR cards: gh /.test(s)?{text:e.brew?"brew install gh && gh auth login (Leftoff won't install it for you)":"install gh from https://cli.github.com, then: gh auth login"}:(u=s.match(/^agent .*?\(([^)]+)\):|^agent (\S+)/))?{text:"check its feed and key: leftoff agents refresh "+(u[1]||u[2]).replace(/:$/,"")+"; to drop it: leftoff agents remove "+(u[1]||u[2]).replace(/:$/,"")}:/^notifications: .*another version/.test(s)?{text:"leftoff notifier rebuild (or restart the daemon: leftoff start)",auto:{id:"notifier-rebuild",label:"rebuild Leftoff.app, what notifications come from"}}:/^notifications: /.test(s)?{text:"leftoff notifier rebuild",auto:{id:"notifier-rebuild",label:"rebuild Leftoff.app, what notifications come from"}}:/^automation from /.test(s)?{text:i==="darwin"?"allow it in System Settings → Privacy & Security → Automation: "+MU:"allow the terminal to be scripted, then run leftoff doctor again"}:/^Go speed from the daemon/.test(s)?{text:"leftoff upgrade --force (refreshes the login service so macOS doesn't throttle it)"}:/^Go speed from this terminal/.test(s)?{text:"make sure Ghostty is running, then run leftoff doctor again"}:{text:"run leftoff doctor again after a minute; if it stays, send this line along with leftoff --version"}}function $s(n){let e=String(n&&n.provider||"auto").toLowerCase();return(jU[e]||e)+(n&&n.model&&e!=="heuristic"?" · "+n.model:"")}function bE({info:n,ollama:e=null}={}){if(!n)return[];let t=n.summarizer||{},s=[];if(n.source==="env"?s.push("· summarizer on this machine: "+$s(t)+" (from CHECKPOINTS_SUMMARIZER, which beats the settings)"):n.source==="machine"?s.push("· summarizer on this machine: "+$s(t)+", this machine's own setting (the shared one is "+$s(n.shared)+")"):s.push("· summarizer on this machine: "+$s(t)+" (the shared setting, used by every machine without its own)"),String(t.provider||"").toLowerCase()!=="ollama"||!e)return s;let i=e.baseUrl||t.baseUrl||"http://localhost:11434";if(e.running)return s.push("✓ summarizer: Ollama answering"+(e.version?" (v"+e.version+")":"")),s;if(n.source==="shared"||n.source==="default")return s.push(e.binary?"! summarizer: the shared setting is Ollama, and it isn't answering at "+i+" on this machine (checkpoints fall back to short summaries)":"! summarizer: the shared setting is Ollama, but Ollama isn't installed on this machine (checkpoints fall back to short summaries)"),s;let r=n.source==="machine"?" (this machine's own setting)":"";return s.push(e.binary?"! summarizer: Ollama isn't answering at "+i+r+" (checkpoints fall back to short summaries)":n.source==="machine"?"! summarizer: this machine's own setting is Ollama, but Ollama isn't installed here":"! summarizer: set to Ollama, but Ollama isn't installed"),s}function vE(n,e,{dim:t=i=>i,seen:s=null}={}){let i=Qu(n,e);return i?s&&s.has(i.text)?{lines:[n,t(" fix: same as above")],fix:i}:(s&&s.add(i.text),{lines:[n,t(" fix: "+i.text),...(i.more||[]).map(r=>t(" "+r))],fix:i}):{lines:[n],fix:null}}function PU(n,e={}){let t=e.leftoff||["leftoff"];if(n==="daemon-restart")return e.serviceCommands||null;let s=(e.platform||"darwin")==="darwin";return n==="syncthing-start"?s&&e.syncthingVia==="brew"&&e.brew?[[e.brew,"services","start","syncthing"]]:s&&e.syncthingVia==="app"&&e.syncthingPath?[["open","-a",e.syncthingPath]]:e.syncthingSystemd?[["systemctl","--user","enable","--now","syncthing"]]:null:n==="syncthing-restart"?s&&e.syncthingVia==="brew"&&e.brew?[[e.brew,"services","restart","syncthing"]]:e.syncthingSystemd?[["systemctl","--user","restart","syncthing"]]:null:n==="digest"?[[...t,"digest"]]:n==="notifier-rebuild"?[[...t,"notifier","rebuild"]]:n==="tailscale-on"?[[...t,"tailscale","on"]]:n==="ollama-start"&&e.ollamaStart?[e.ollamaStart]:null}async function xE(n,{confirm:e,run:t,say:s=()=>{},ctx:i={}}={}){let r=new Set,o=[];for(let a of n||[]){let l=a&&a.auto;if(!l||r.has(l.id))continue;r.add(l.id);let c=PU(l.id,i);if(!c||!c.length)continue;let u=c.map(f=>f.map(p=>/\s/.test(p)?'"'+p+'"':p).join(" ")).join(" && ");if(!await e(l.label.charAt(0).toUpperCase()+l.label.slice(1)+"? ("+u+")")){s("· skipped: "+l.label),o.push({id:l.id,ran:!1});continue}let d=!0,h="";for(let f of c){let p=await t(f);if(!p||p.status!==0){d=!1,h=(p&&(p.stderr||p.stdout)||"").toString().trim().split(`
|
|
199
|
-
`).pop()||"exit "+(p&&p.status);break}}s(d?"✓ did: "+l.label:"✗ "+l.label+" failed: "+h),o.push({id:l.id,ran:!0,ok:d,error:d?null:h})}return r.size||s("· nothing doctor can fix by itself here; the fix: lines above say what to do"),o}function AE({me:n,disk:e=null,st:t=null,healths:s=[],now:i=Date.now(),platform:r="darwin"}={}){let o=[],a=(y,w,S,k,v=null)=>o.push({machine:y,level:w,text:S,fix:k||null,...v?{cmd:v}:{}});if(e&&e.level&&e.level!=="ok"){let y=Go(e,{platform:r});a(n,e.level,Up(e,{machine:n}),y.text,y.cmd)}let l=t&&t.folder&&t.folder.errors||[],c=l.find(y=>/insufficient space/i.test(y.error||""));c&&!(e&&e.level==="red")&&a(n,"red","Syncing stopped on "+n+": disk full.",Go(e,{platform:r}).text);for(let y of l)y!==c&&a(n,"red","Sync error on "+n+": "+(y.error||"unknown"),Qu("! Folder error: "+y.error,{syncthing:t}).text);t&&t.installed&&!t.running&&a(n,"warn","Syncthing isn't running on "+n+".","leftoff sync start");let u={},d=t&&t.peers||[],h=new Set;for(let y of s){if(!y||!y.machine||y.machine===n)continue;h.add(y.machine);let w=i-Date.parse(y.at||0),S=d.find(v=>v.name===y.machine),k=y.disk&&y.disk.level;k==="red"||y.syncError&&/insufficient space/i.test(y.syncError)?a(y.machine,"red",y.machine+" stopped syncing: disk full.","free up disk space on "+y.machine+"; it catches up on its own"):k==="warn"&&w>Nk?a(y.machine,"red",y.machine+" stopped syncing: disk probably full ("+$o(y.disk.freeBytes)+" free when last heard).","free up disk space on "+y.machine):k==="warn"?a(y.machine,"warn",Up(y.disk,{machine:y.machine}),"free up disk space on "+y.machine):y.syncError&&a(y.machine,"red","Sync error on "+y.machine+": "+y.syncError,"run leftoff doctor on "+y.machine),w>Nk&&!(S&&S.connected)&&(u[y.machine]=y.at)}for(let y of d)y.connected||y.notSharing||(y.lastSeen?i-Date.parse(y.lastSeen):1/0)<=Nk||(u[y.name]||(u[y.name]=y.lastSeen||null),o.some(S=>S.machine===y.name)||a(y.name,"warn",y.name+" hasn't synced "+(y.lastSeen?"for "+
|
|
200
|
-
`)){let s=t.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/);s&&e.push({pid:Number(s[1]),ppid:Number(s[2]),tty:s[3],etime:s[4],args:s[5].trim()})}return e}function
|
|
201
|
-
`)){let s=t.match(/^\s*(\d+)\s+(.*)$/);s&&e.set(Number(s[1]),s[2].trim())}return e}function jE(n,e){let t=Bp(e||"").toLowerCase(),s=Bp(String(n.args||"").split(/\s+/)[0]).toLowerCase();if(/^claude(\s|$)/.test(t)||s==="claude")return!0;let i=String(n.args||"").split(/\s+/);return/^(node|bun)$/.test(s)&&(Bp(i[1])==="claude"||/@anthropic-ai\/claude-code\/cli\.m?js$/.test(i[1]||""))}function
|
|
202
|
-
`))a.startsWith("p")?o=Number(a.slice(1)):a.startsWith("n")&&o&&s.set(o,a.slice(1));return s}function
|
|
203
|
-
`);for(let r of t)try{Nr.unlinkSync(Mr.join(n,r))}catch{}}function jk(n,e,{machine:t="",now:s=Date.now()}={}){let{map:i,conflicts:r}=Yo(n),o={at:new Date(s).toISOString(),by:t},a=e(i,o);return
|
|
193
|
+
`+s,e.firstPrompt);if(t)return{title:t,titleSource:"generated"}}catch{}return{title:y0(e),titleSource:"fallback"}}async titleInUserLanguage(e,t,s){for(let i of[!1,!0]){let r=await this.complete({system:e,prompt:t(a_(s,{firm:i})),maxTokens:30}),o=t_(r.text);if(!o)return null;if(n_(o,s))return o}return null}async recentTitle(e,t={}){let s=u3(e),i=(e||[]).find(a=>!oE(a)&&a.role==="user"),r=t.firstPrompt||i&&i.text||"";if(this.mode!=="heuristic"&&s.length)try{let a=s.filter(u=>u.role==="user").map(u=>u.text).join(`
|
|
194
|
+
`),l=d3(s,{firstPrompt:r}),c=await this.titleInUserLanguage(l3,u=>l+`
|
|
195
|
+
|
|
196
|
+
`+u,a||r);if(c)return{title:c,titleSource:"generated",by:this.mode}}catch{}let o=h3(s);return{title:y0({...t,firstPrompt:o||!aE(t.lastPrompt)&&t.lastPrompt||t.firstPrompt||t.lastPrompt||""}),titleSource:"fallback",by:"heuristic"}}async paragraph(e,t){if(this.mode==="heuristic")return null;try{return(await this.complete({system:e,prompt:t,maxTokens:200})).text.trim()}catch{return null}}}});import pl from"node:fs";import f3 from"node:os";import Mp from"node:path";import{execFileSync as VJ,spawn as p3,spawnSync as m3}from"node:child_process";import{fileURLToPath as g3}from"node:url";async function Vu({baseUrl:n=process.env.CHECKPOINTS_CLOUD_URL||Bo,fetchImpl:e=fetch,timeoutMs:t=5e3}={}){let s=n.replace(/\/$/,"");try{let i=await Ip(e,s+"/api/tags",t);return{ok:!0,baseUrl:s,models:(i.models||[]).map(r=>r.name||r.model).filter(Boolean),error:null}}catch(i){return{ok:!1,baseUrl:s,models:[],error:i.cause&&i.cause.code||i.message}}}function Lp(n=[],{curated:e=Ju,current:t=null}={}){let s=r=>n.some(o=>Ct(o,r)),i=e.map(r=>({...r,curated:!0,available:n.length?s(r.tag):null,current:!!t&&Ct(t,r.tag)}));for(let r of n)i.some(o=>Ct(o.tag,r))||i.push({tag:r,name:r,curated:!1,available:!0,recommended:!1,current:!!t&&Ct(t,r)});return i}function Np(n){let e=String(n||"").trim().toLowerCase();return e?e.includes(":")?e:e+":latest":""}function Ct(n,e){return Np(n)===Np(e)}function gl(n=[],{curated:e=qu,current:t=null}={}){let s=n.map(o=>typeof o=="string"?{name:o}:o),i=o=>s.some(a=>Ct(a.name||a.model,o)),r=e.map(o=>({...o,curated:!0,installed:i(o.tag),current:!!t&&Ct(t,o.tag)}));for(let o of s){let a=o.name||o.model;!a||r.some(l=>Ct(l.tag,a))||/embed/i.test(a)||r.push({tag:a,name:a,sizeBytes:o.size||null,curated:!1,installed:!0,recommended:!1,current:!!t&&Ct(t,a)})}return r}function cE(n,{totalMemBytes:e=f3.totalmem()}={}){let t=n.find(r=>r.current&&r.installed);if(t)return t.tag;let s=n.find(r=>r.recommended);if(s&&s.installed)return s.tag;let i=n.find(r=>r.curated&&r.installed);if(i)return i.tag;if(e&&e<85e8){let r=n.filter(o=>o.curated).sort((o,a)=>o.ramGB-a.ramGB)[0];if(r)return r.tag}return s?s.tag:n[0]&&n[0].tag}function jp(n,e){let t=[];return n.sizeBytes&&t.push(e(n.sizeBytes)),n.ramGB&&t.push("~"+n.ramGB+" GB RAM"),n.speed&&t.push(n.speed),t.join(" · ")}function vr(n,e=process.env){let t=String(e.PATH||"").split(Mp.delimiter).filter(Boolean),s=process.platform==="win32"?[".exe",".cmd",".bat",""]:[""];for(let i of t)for(let r of s){let o=Mp.join(i,n+r);try{if(pl.statSync(o).isFile())return pl.accessSync(o,pl.constants.X_OK),o}catch{}}return null}function Ai(n=process.env){let e=vr("ollama",n);if(e)return e;if(n.CHECKPOINTS_TEST_NO_SYSTEM_BINS)return null;for(let t of["/opt/homebrew/bin/ollama","/usr/local/bin/ollama","/Applications/Ollama.app/Contents/Resources/ollama"])if(pl.existsSync(t))return t;return null}async function Ip(n,e,t=1500){let s=await n(e,{signal:AbortSignal.timeout(t)});if(!s.ok)throw new Error(e+" -> HTTP "+s.status);return s.json()}async function jt({baseUrl:n=Pt,fetchImpl:e=fetch,env:t=process.env}={}){let s=n.replace(/\/$/,""),i={kind:"ollama",running:!1,baseUrl:s,version:null,models:[],binary:Ai(t),error:null};try{let r=await Ip(e,s+"/api/tags");i.running=!0,i.models=(r.models||[]).map(o=>({name:o.name||o.model,size:o.size,modifiedAt:o.modified_at,parameterSize:o.details&&o.details.parameter_size,quantization:o.details&&o.details.quantization_level}));try{i.version=(await Ip(e,s+"/api/version")).version||null}catch{}}catch(r){i.error=r.cause&&r.cause.code||r.name||r.message}return i}async function _i({baseUrl:n=zs,fetchImpl:e=fetch}={}){let t=n.replace(/\/$/,""),s={kind:"openai-compatible",running:!1,baseUrl:t,models:[],error:null};try{let i=await Ip(e,t+"/models");s.running=!0,s.models=(i.data||[]).map(r=>r.id).filter(r=>r&&!/embed/i.test(r))}catch(i){s.error=i.cause&&i.cause.code||i.name||i.message}return s}async function yl({baseUrl:n=Pt,fetchImpl:e=fetch,timeoutMs:t=2e4}={}){let s=Date.now()+t;for(;Date.now()<s;){let i=await jt({baseUrl:n,fetchImpl:e});if(i.running)return i;await w3(500)}return jt({baseUrl:n,fetchImpl:e})}function k3(n,e){let t=n||{layers:{},status:"",total:0,completed:0,fraction:0,done:!1,error:null};if(e.error)return{...t,error:String(e.error)};let s={...t.layers};e.digest&&e.total&&(s[e.digest]={total:e.total,completed:Math.min(e.completed||0,e.total)});let i=Object.values(s),r=i.reduce((c,u)=>c+u.total,0),o=i.reduce((c,u)=>c+u.completed,0),a=e.status==="success",l=a?1:r?Math.min(.999,o/r):t.fraction;return{layers:s,status:e.status||t.status,total:r,completed:o,fraction:Math.max(t.fraction,l),done:a,error:null}}async function wl({tag:n,baseUrl:e=Pt,fetchImpl:t=fetch,onProgress:s=()=>{},signal:i}={}){let r=await t(e.replace(/\/$/,"")+"/api/pull",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({model:n,stream:!0}),signal:i});if(!r.ok||!r.body){let u="HTTP "+r.status;try{let d=await r.json();d.error&&(u=d.error)}catch{}throw new Error("pull "+n+" failed: "+u)}let o=null,a="",l=new TextDecoder,c=u=>{if(!u.trim())return;let d;try{d=JSON.parse(u)}catch{return}if(o=k3(o,d),o.error)throw new Error("pull "+n+" failed: "+o.error);s(o)};for await(let u of r.body){a+=l.decode(u,{stream:!0});let d;for(;(d=a.indexOf(`
|
|
197
|
+
`))>=0;)c(a.slice(0,d)),a=a.slice(d+1)}if(c(a),!o||!o.done)throw new Error("pull "+n+" ended early (last status: "+(o&&o.status||"none")+")");return o}async function Ti(n,{fetchImpl:e,transcriptFile:t=y3,root:s}={}){let i=eu(Be,{summarizer:n}),{messages:r,meta:o}=fl(pl.readFileSync(t,"utf8")),a={id:o.sessionId||"sample",cwd:o.cwd||"/work/acme",title:o.summary||"Sample session",repo:{root:o.cwd,branch:o.gitBranch}},l=Cf({session:a,messages:r,cfg:i}),c=new Lt(i,{...e?{fetchImpl:e}:{},...s?{root:s}:{}}),u=Date.now(),d=await c.checkpoint({...l,messages:r,session:a}),h=Date.now()-u,f=d.generator&&d.generator.by==="heuristic"&&d.generator.fallbackFrom;return{ok:!f,doing:d.doing,stoppedAt:d.stoppedAt,next:d.next,parseOk:d.parseOk!==!1,ms:h,generator:d.generator,error:f?d.generator.error:null}}function uE(n=process.platform,e=process.env){let t={id:"script",label:"Official install script",shell:"curl -fsSL https://ollama.com/install.sh | sh",cmd:["sh","-c","curl -fsSL https://ollama.com/install.sh | sh"]};if(n==="darwin"){let s=[];return vr("brew",e)&&s.push({id:"brew",label:"Homebrew",shell:"brew install ollama",cmd:["brew","install","ollama"]}),s.push({...t,note:"installs the Ollama app"}),s.push({id:"manual",label:"Download the app myself",shell:"open https://ollama.com/download",cmd:null,url:"https://ollama.com/download"}),s}return n==="linux"?[{...t,note:"asks for sudo; sets up a systemd service"},{id:"manual",label:"Install it myself",shell:"see https://ollama.com/download/linux",cmd:null,url:"https://ollama.com/download/linux"}]:n==="win32"?[{id:"manual",label:"PowerShell installer",shell:"irm https://ollama.com/install.ps1 | iex",cmd:null,url:"https://ollama.com/download/windows"}]:[{id:"manual",label:"Install it myself",shell:"see https://ollama.com/download",cmd:null,url:"https://ollama.com/download"}]}function zo(n=process.platform,e=Ai()){return n==="darwin"&&pl.existsSync("/Applications/Ollama.app")?{shell:"open -a Ollama",cmd:["open","-a","Ollama"],detached:!1}:e?{shell:e+" serve",cmd:[e,"serve"],detached:!0}:null}function Pp({platform:n=process.platform,binary:e,env:t=process.env}={}){if(t.CHECKPOINTS_OLLAMA_START_CMD)try{let s=JSON.parse(t.CHECKPOINTS_OLLAMA_START_CMD);if(Array.isArray(s)&&s.length)return{shell:s.join(" "),cmd:s,detached:!0}}catch{}return e?zo(n,e):null}function dE(n,{spawnImpl:e=p3,spawnSyncImpl:t=m3}={}){if(!n||!Array.isArray(n.cmd))return{started:!1,shell:null,error:"no start command"};try{if(n.detached){let s=e(n.cmd[0],n.cmd.slice(1),{detached:!0,stdio:"ignore"});s.on("error",()=>{}),s.unref()}else{let s=t(n.cmd[0],n.cmd.slice(1),{stdio:"ignore",timeout:15e3});if(s&&s.status)return{started:!1,shell:n.shell,error:n.shell+" exited "+s.status}}return{started:!0,shell:n.shell}}catch(s){return{started:!1,shell:n.shell,error:String(s.message||s)}}}var Pt,zs,y3,ml,qu,Bo,lE,Ju,w3,Bs=K(()=>{Uo();Te();Ho();Pt="http://localhost:11434",zs="http://localhost:1234/v1",y3=Mp.resolve(Mp.dirname(g3(import.meta.url)),"..","assets","sample-transcript.jsonl"),ml="2026-10-09",qu=[{tag:"qwen2.5:3b",name:"Qwen 2.5 · 3B",sizeBytes:19e8,ramGB:4,speed:"fast",note:"Sticks to the 3-line format, good with code and file names",recommended:!0},{tag:"llama3.2:3b",name:"Llama 3.2 · 3B",sizeBytes:2e9,ramGB:4,speed:"fast",note:"Solid all-rounder from Meta"},{tag:"gemma3:4b",name:"Gemma 3 · 4B",sizeBytes:34e8,ramGB:6,speed:"medium",note:"Best-written summaries of the four, slower without a GPU"},{tag:"llama3.2:1b",name:"Llama 3.2 · 1B",sizeBytes:13e8,ramGB:2.5,speed:"fastest",note:'For 8 GB machines; terser, sometimes vague on "Next"'}],Bo="https://ollama.com",lE="2026-10-09",Ju=[{tag:"gpt-oss:20b",name:"gpt-oss · 20B",note:"OpenAI's open-weight model; quick, plenty for 3 lines",recommended:!0},{tag:"gemma4:31b",name:"Gemma 4 · 31B",note:"Google; the model Ollama's own cloud docs use"},{tag:"nemotron-3-nano:30b",name:"Nemotron 3 Nano · 30B",note:"NVIDIA; small and fast"},{tag:"gpt-oss:120b",name:"gpt-oss · 120B",note:"Bigger and slower; more than a checkpoint needs"}];w3=n=>new Promise(e=>setTimeout(e,n))});import Rt from"node:fs";import _k from"node:os";import ze from"node:path";import{spawnSync as S3}from"node:child_process";import{fileURLToPath as b3}from"node:url";function x3(n=process.env,e=_k.homedir(),t=process.execPath){let s=[ze.join(e,".local","bin","claude"),ze.join(e,".claude","local","claude"),"/opt/homebrew/bin/claude","/usr/local/bin/claude",ze.join(e,".npm-global","bin","claude"),ze.join(e,".bun","bin","claude"),ze.join(e,".volta","bin","claude"),ze.join(ze.dirname(t),"claude")];if(n.NVM_DIR||e){let i=ze.join(n.NVM_DIR||ze.join(e,".nvm"),"versions","node");try{let r=Rt.readdirSync(i).filter(o=>/^v\d/.test(o)).sort((o,a)=>fE(a.slice(1),o.slice(1)));for(let o of r)s.push(ze.join(i,o,"bin","claude"))}catch{}}return s}function Tn(n=process.env,{home:e=_k.homedir(),execPath:t=process.execPath}={}){if(n.CHECKPOINTS_CLAUDE_BIN)return n.CHECKPOINTS_CLAUDE_BIN;let s=vr("claude",n);if(s)return s;if(n.CHECKPOINTS_TEST_NO_SYSTEM_BINS&&!n.LEFTOFF_TEST_CLAUDE_HOME)return null;for(let i of x3(n,n.LEFTOFF_TEST_CLAUDE_HOME||e,t))try{if(Rt.statSync(i).isFile())return Rt.accessSync(i,Rt.constants.X_OK),i}catch{}return null}function Ei(n,e,{env:t=process.env,timeoutMs:s=12e4}={}){let i=S3(n,e,{env:{...t,CHECKPOINTS_INTERNAL:"1",LEFTOFF_INTERNAL:"1"},encoding:"utf8",timeout:s});return{code:i.status==null?-1:i.status,stdout:i.stdout||"",stderr:i.stderr||(i.error?String(i.error.message):""),args:e}}function A3(n){let e=String(n||"").match(/(\d+)\.(\d+)\.(\d+)/);return e?e[0]:null}function fE(n,e){let t=String(n).split(".").map(Number),s=String(e).split(".").map(Number);for(let i=0;i<3;i++){let r=(t[i]||0)-(s[i]||0);if(r)return r>0?1:-1}return 0}function pE(n=process.env){let e=Tn(n);if(!e)return{found:!1,bin:null,version:null,modsSupported:!1};let t=Ei(e,["--version"],{env:n,timeoutMs:15e3}),s=A3(t.stdout+" "+t.stderr);return{found:!0,bin:e,version:s,modsSupported:!!s&&fE(s,Xu)>=0}}function Tk(n){return/[\\/]_npx[\\/]/.test(n)||/[\\/]\.npm[\\/]/.test(n)}function Ek(n=kl){try{return JSON.parse(Rt.readFileSync(ze.join(n,"package.json"),"utf8")).leftoffBuild==="npm"}catch{return!1}}function _3(n=process.env){return n.LEFTOFF_PLUGIN_DIR?n.LEFTOFF_PLUGIN_DIR:ze.join(n.XDG_CONFIG_HOME||ze.join(n.HOME||_k.homedir(),".config"),"checkpoints","plugin")}function Ck({from:n=kl,env:e=process.env}={}){let t=_3(e);if(!Rt.existsSync(ze.join(n,".claude-plugin","marketplace.json")))throw new Error("no .claude-plugin/marketplace.json in "+n);let s=t+"."+process.pid+".tmp";Rt.rmSync(s,{recursive:!0,force:!0}),Rt.mkdirSync(ze.join(s,".claude-plugin"),{recursive:!0}),Rt.copyFileSync(ze.join(n,".claude-plugin","marketplace.json"),ze.join(s,".claude-plugin","marketplace.json"));let i=r=>/^(tests|tsconfig\.json)$/.test(r)||/(^|[\\/])types([\\/]|$)/.test(r);return Rt.cpSync(ze.join(n,"mod"),ze.join(s,"mod"),{recursive:!0,filter:r=>!i(ze.relative(ze.join(n,"mod"),r))}),Rt.writeFileSync(ze.join(s,"README.md"),"Leftoff's Claude Code plugin (the mod), copied here by `leftoff setup` / `leftoff upgrade` from the installed package.\nClaude Code reads it as a local marketplace. Safe to delete: `leftoff setup` puts it back.\n"),Rt.rmSync(t,{recursive:!0,force:!0}),Rt.mkdirSync(ze.dirname(t),{recursive:!0}),Rt.renameSync(s,t),t}function mE({repoDir:n=kl,override:e,env:t=process.env,copy:s=Ck}={}){if(e)return e;if(!Tk(n)&&Rt.existsSync(ze.join(n,".git"))&&Rt.existsSync(ze.join(n,".claude-plugin","marketplace.json")))return n;if(Ek(n)&&Rt.existsSync(ze.join(n,".claude-plugin","marketplace.json"))){try{return s({from:n,env:t})}catch{}return n}return Rp}function hE(n,e=lt){let t=String(n||""),s=t.search(new RegExp("(^|[^\\w-])"+e.replace(/[.@]/g,"\\$&")+"(?![\\w-])","m"));if(s<0)return{installed:!1,enabled:!1};let i=t.slice(s,s+400).split(/\n\s*\n/)[0];return{installed:!0,enabled:!(/disabled/i.test(i)&&!/✔\s*enabled|status:\s*enabled/i.test(i))}}function T3(n){return new RegExp("(^|[^\\w-])"+Gs+"([^\\w-]|$)","m").test(String(n||""))}function Dp(n,e){let t=Ei(n,["plugin","list"],{env:e,timeoutMs:3e4}),s=Ei(n,["plugin","marketplace","list"],{env:e,timeoutMs:3e4}),i=hE(t.stdout,Tr);return{...hE(t.stdout),legacyInstalled:i.installed,legacyEnabled:i.enabled,marketplaceAdded:s.code===0&&T3(s.stdout),listOk:t.code===0}}function Ok({bin:n,env:e=process.env,state:t=null,onStep:s=()=>{}}){let i=t||Dp(n,e);if(!i.legacyInstalled)return{ok:!0,migrated:!1,steps:[]};let r=[],o=c=>{s("claude "+c.join(" "));let u=Ei(n,c,{env:e});return r.push(u),u},a=c=>(c.stderr||c.stdout).trim().split(`
|
|
198
|
+
`).pop()||"exit "+c.code;if(!i.installed){o(["plugin","marketplace","update",Gs]);let c=o(["plugin","install",lt]);if(c.code!==0&&!/already installed/i.test(c.stdout+c.stderr))return{ok:!1,migrated:!1,steps:r,error:a(c)};i.legacyEnabled===!1&&o(["plugin","disable",lt])}let l=o(["plugin","uninstall",Tr]);return l.code!==0?{ok:!1,migrated:!0,steps:r,error:"installed "+lt+" but couldn't uninstall "+Tr+": "+a(l)}:{ok:!0,migrated:!0,enabled:i.legacyEnabled!==!1,steps:r}}function Mk({bin:n,source:e,env:t=process.env,onStep:s=()=>{}}){let i=[],r=Dp(n,t);if(r.legacyInstalled){let l=Ok({bin:n,env:t,state:r,onStep:s});if(!l.ok)return{ok:!1,steps:l.steps,state:r,error:l.error};let c=Mk({bin:n,source:e,env:t,onStep:s});return{...c,migrated:l.migrated,steps:[...l.steps,...c.steps]}}if(r.installed&&r.enabled)return{ok:!0,already:!0,steps:i,state:r};if(r.installed&&!r.enabled){s("claude plugin enable "+lt);let l=Ei(n,["plugin","enable",lt],{env:t});return i.push(l),{ok:l.code===0,already:!0,enabled:l.code===0,steps:i,state:r,error:l.code?(l.stderr||l.stdout).trim():null}}if(!r.marketplaceAdded){s("claude plugin marketplace add "+e);let l=Ei(n,["plugin","marketplace","add",e],{env:t});if(i.push(l),l.code!==0&&!/already/i.test(l.stdout+l.stderr))return{ok:!1,steps:i,state:r,error:(l.stderr||l.stdout).trim()||"exit "+l.code}}s("claude plugin install "+lt);let o=Ei(n,["plugin","install",lt],{env:t});i.push(o);let a=o.code===0||/already installed/i.test(o.stdout+o.stderr);return{ok:a,steps:i,state:r,error:a?null:(o.stderr||o.stdout).trim()||"exit "+o.code}}function gE(n){return!!(n&&n.disableAllHooks===!0)}var kl,Rp,Gs,v3,lt,Tr,Xu,Ci=K(()=>{Bs();Wt();kl=ze.resolve(ze.dirname(b3(import.meta.url)),".."),Rp=gi,Gs=T_,v3=E_,lt=v3+"@"+Gs,Tr=C_+"@"+Gs,Xu="2.1.287"});import wE from"node:fs";import Fp from"node:path";function Er(n,{statfs:e=wE.statfsSync}={}){if(!n||typeof e!="function")return null;let t=Fp.resolve(String(n));for(let s=0;s<64;s++){try{let r=e(t),o=Number(r.bsize||r.frsize||0),a=Number(r.blocks)*o,l=Number(r.bavail!=null?r.bavail:r.bfree)*o;return a?{path:t,freeBytes:l,totalBytes:a,pct:Math.round(l/a*1e4)/100}:null}catch{}let i=Fp.dirname(t);if(i===t)return null;t=i}return null}function I3(n){return!n||!n.totalBytes?"ok":n.pct<E3?"red":n.freeBytes<Math.max(n.totalBytes*C3/100,O3)?"warn":"ok"}function $o(n){return n=Number(n)||0,n>=100*Zu?Math.round(n/Zu)+" GB":n>=Zu?Math.round(n/Zu*10)/10+" GB":Math.round(n/1024**2)+" MB"}function Hp(n){let e={ok:0,warn:1,red:2},t=null;for(let s of n||[]){if(!s)continue;let i=I3(s);(!t||e[i]>e[t.level]||e[i]===e[t.level]&&s.freeBytes<t.freeBytes)&&(t={...s,level:i})}return t}function Up(n,{machine:e="this Mac"}={}){if(!n||n.level==="ok")return null;let t=$o(n.freeBytes)+" free";return n.level==="red"?"Disk full on "+e+": "+t+" ("+kE(n.pct)+"). Syncing has stopped.":"Low disk on "+e+": "+t+". Syncing stops below 1%."}function Go(n,{platform:e="darwin"}={}){let t=n?$o(n.freeBytes)+" free":null;return{text:"free up disk space"+(t?" (you have "+t+(n.pct!=null?", "+kE(n.pct):"")+")":"")+"; Syncthing resumes on its own",more:[...e==="darwin"?["storage settings: "+yE]:["see what's big: du -sh ~/* | sort -h | tail"],"last resort: lower Syncthing's minHomeDiskFree ("+N3+")"],cmd:e==="darwin"?yE:null}}function SE(n,e={}){let t=e.localErrors||[];if(t.some(i=>/insufficient space/i.test(i.error||"")))return{text:"this Mac's disk is full (see the folder error below); "+n+" reconnects once you free space here"};if(t.length)return{text:"fix the folder error below first; "+n+" reconnects on its own"};let s=e.peerHealth;return s&&s.disk&&s.disk.level&&s.disk.level!=="ok"?{text:n+" is low on disk ("+$o(s.disk.freeBytes)+" free when last heard); free up space there"}:{text:"wake "+n+" (open it, or check it's on and online); if it is, run there: leftoff doctor",more:["if Syncthing isn't running on "+n+": leftoff sync start (on "+n+")"]}}function L3(n){if(/^[\w@%+=:,./~-]+$/.test(n))return n;let e=n.startsWith("~/")?"~/":"";return e+"'"+n.slice(e.length).replace(/'/g,"'\\''")+"'"}function Qu(n,e={}){let t=String(n||"");if(!/^(!|✗)\s/.test(t))return null;let s=t.replace(/^(!|✗)\s+/,""),i=e.platform||"darwin",r=e.syncthing||{},o=r.folder&&r.folder.errors||[],a=i==="darwin"&&(e.syncthingVia==="brew"||r.via==="brew"),l=i==="linux"&&!a&&!!(e.syncthingSystemd||r.service&&r.service.systemd&&r.service.unit),c=a?{text:"brew services restart syncthing",auto:{id:"syncthing-restart",label:"restart Syncthing (brew services restart syncthing)"}}:l?{text:"systemctl --user restart syncthing",auto:{id:"syncthing-restart",label:"restart Syncthing (systemctl --user restart syncthing)"}}:null,u;if(u=s.match(/insufficient space on disk(?: for (?:the )?database)?\s*\(([^)]+)\)/i)){let d=typeof e.disk=="function"?e.disk(u[1]):null;return Go(d,{platform:i})}if(/insufficient space/i.test(s))return Go(null,{platform:i});if(/^(low disk|disk full)\b/i.test(s)){let d=e.lowDisk&&e.lowDisk.freeBytes!=null?e.lowDisk:null;return Go(d,{platform:i})}if(u=s.match(/null\/ folder is committed in (.+): git rm -r null\/$/))return{text:"cd "+L3(u[1])+' && git rm -r null/ && git commit -m "Remove stray null/ folder"'};if(u=s.match(/^worktree (.+?) is still locked by Leftoff/)){let d=(e.parkLocks||[]).find(f=>f.path===u[1]),h=f=>/^[A-Za-z0-9_./~@%+=:,-]+$/.test(f)?f:"'"+String(f).replace(/'/g,"'\\''")+"'";return{text:"git -C "+h(d&&d.root||u[1])+" worktree unlock "+h(u[1])+" (unlocks it; nothing is removed)"}}if(/^daemon on /.test(s))return{text:"leftoff start",more:["or run it in this terminal to see why it stops: leftoff serve"],auto:e.serviceInstalled?{id:"daemon-restart",label:"restart the Leftoff daemon"}:null};if(/^folder /.test(s))return{text:"leftoff setup (it makes the folder and picks up where it left off)"};if(/^token /.test(s))return{text:"leftoff setup (writes a new board token)"};if(u=s.match(/^mod command \/(\S+) failed to register/))return{text:"start a new Claude Code session (commands register at start); if /"+u[1]+" fails again: leftoff upgrade --force"};if(/^mod commands failed to register/.test(s))return{text:"start a new Claude Code session; if it fails again: leftoff upgrade --force"};if(/^update available/.test(s))return{text:"leftoff upgrade"};if(/^settings hooks are from before/.test(s)||/^the login service still has its old name/.test(s)||/^login service is from before/.test(s))return{text:"leftoff upgrade --force"};if(/^digest:/.test(s))return{text:"leftoff digest",auto:{id:"digest",label:"make today's digest now"}};if(/known only by id/.test(s))return{text:"nothing to run: it shows as itself after that session's next prompt, or closes with it"};if(/^API key files inside the folder/.test(s))return{text:"leftoff start (the daemon moves them when it starts)",auto:e.serviceInstalled?{id:"daemon-restart",label:"restart the Leftoff daemon"}:null};if(/^remote access: remoteHosts is set but no tailscale/.test(s))return{text:"install Tailscale from https://tailscale.com/download, or turn remote access off: leftoff tailscale off"};if(/^remote access: /.test(s))return{text:"leftoff tailscale on",auto:{id:"tailscale-on",label:"point tailscale serve at the board again (tailnet only)"}};if(/^Syncthing: installed, not running/.test(s))return{text:a?"leftoff sync start (runs: brew services start syncthing)":l?"systemctl --user enable --now syncthing (or: leftoff sync start)":"leftoff sync start",auto:{id:"syncthing-start",label:l?"start Syncthing (systemctl --user enable --now syncthing)":"start Syncthing"}};if(/^Leftoff folder: .*Leftoff uses another folder/.test(s))return{text:"leftoff sync setup"};if(/^Leftoff folder: /.test(s))return o.some(d=>/insufficient space/i.test(d.error||""))?{text:"Syncthing stopped because the disk is full; see the folder error below"}:o.length?{text:"see the folder error below"}:c||{text:i==="darwin"?"quit and reopen Syncthing":"restart Syncthing (stop it, then: leftoff sync start)",auto:null};if(u=s.match(/^Peer (.+?)(?: \(Leftoff\))?: not connected/)){let d=u[1];return SE(d,{localErrors:o,peerHealth:(e.peers||{})[d]||null})}return(u=s.match(/^Peer (.+?)(?: \(Leftoff\))?: /))&&/not sharing|hasn.t accepted/.test(s)?{text:"on "+u[1]+": Settings → Sync, accept this Mac (or: leftoff sync accept)"}:/^Folder error: /.test(s)?/folder marker missing|\.stfolder/i.test(s)?{text:`the folder's .stfolder marker is gone; if the folder is really there: mkdir "`+(r.folder&&r.folder.path||"<folder>")+'/.stfolder"'}:/folder path missing|no such file/i.test(s)?{text:"the folder moved or was deleted; point Syncthing at it again: leftoff sync setup"}:/permission denied/i.test(s)?{text:i==="darwin"?"give your user access to the folder again (Finder → Get Info → Sharing & Permissions), then restart Syncthing":"give your user access to the folder again (chown/chmod), then restart Syncthing"}:{text:(c?c.text:"restart Syncthing")+"; if the error comes back, open Syncthing's web page for details",auto:c?c.auto:null}:(u=s.match(/^A (?:Mac|machine) named “(.+?)” asked to connect/))?{text:`only if it's your machine: leftoff sync accept "`+u[1]+'"'}:/^Syncthing/.test(s)?c||{text:"leftoff sync start",auto:{id:"syncthing-start",label:"start Syncthing"}}:(u=s.match(/^(.+?) stopped syncing/))?{text:"free up disk space on "+u[1]+"; it catches up on its own"}:(u=s.match(/^(.+?): data as of /))?{text:"wake "+u[1]+" (open it, or check it's online); its sessions update once it syncs"}:/^summarizer: the shared setting is Ollama/.test(s)?{text:"give this machine its own summarizer: Settings → Summarizer → Just this machine, or leftoff models set claude --this-machine"+(/isn.t answering/.test(s)?" (or start Ollama here: ollama serve)":"")}:/^summarizer: this machine.s own setting is Ollama/.test(s)?{text:"install Ollama from https://ollama.com/download, or use the shared setting here again: leftoff models shared"}:/^summarizer: Ollama isn.t answering/.test(s)?{text:i==="darwin"?"ollama serve (or open the Ollama app)":"ollama serve",auto:e.ollamaBinary?{id:"ollama-start",label:"start Ollama"}:null}:/^summarizer: .*Ollama isn.t installed/.test(s)?{text:"install Ollama from https://ollama.com/download, or pick another summarizer: leftoff models"}:/^summarizer: /.test(s)?{text:"leftoff models (pick or test the summarizer)"}:/^PR cards: gh is signed out/.test(s)?{text:"gh auth login"}:/^PR cards: gh /.test(s)?{text:e.brew?"brew install gh && gh auth login (Leftoff won't install it for you)":"install gh from https://cli.github.com, then: gh auth login"}:(u=s.match(/^agent .*?\(([^)]+)\):|^agent (\S+)/))?{text:"check its feed and key: leftoff agents refresh "+(u[1]||u[2]).replace(/:$/,"")+"; to drop it: leftoff agents remove "+(u[1]||u[2]).replace(/:$/,"")}:/^notifications: .*another version/.test(s)?{text:"leftoff notifier rebuild (or restart the daemon: leftoff start)",auto:{id:"notifier-rebuild",label:"rebuild Leftoff.app, what notifications come from"}}:/^notifications: /.test(s)?{text:"leftoff notifier rebuild",auto:{id:"notifier-rebuild",label:"rebuild Leftoff.app, what notifications come from"}}:/^automation from /.test(s)?{text:i==="darwin"?"allow it in System Settings → Privacy & Security → Automation: "+M3:"allow the terminal to be scripted, then run leftoff doctor again"}:/^Go speed from the daemon/.test(s)?{text:"leftoff upgrade --force (refreshes the login service so macOS doesn't throttle it)"}:/^Go speed from this terminal/.test(s)?{text:"make sure Ghostty is running, then run leftoff doctor again"}:{text:"run leftoff doctor again after a minute; if it stays, send this line along with leftoff --version"}}function $s(n){let e=String(n&&n.provider||"auto").toLowerCase();return(j3[e]||e)+(n&&n.model&&e!=="heuristic"?" · "+n.model:"")}function bE({info:n,ollama:e=null}={}){if(!n)return[];let t=n.summarizer||{},s=[];if(n.source==="env"?s.push("· summarizer on this machine: "+$s(t)+" (from CHECKPOINTS_SUMMARIZER, which beats the settings)"):n.source==="machine"?s.push("· summarizer on this machine: "+$s(t)+", this machine's own setting (the shared one is "+$s(n.shared)+")"):s.push("· summarizer on this machine: "+$s(t)+" (the shared setting, used by every machine without its own)"),String(t.provider||"").toLowerCase()!=="ollama"||!e)return s;let i=e.baseUrl||t.baseUrl||"http://localhost:11434";if(e.running)return s.push("✓ summarizer: Ollama answering"+(e.version?" (v"+e.version+")":"")),s;if(n.source==="shared"||n.source==="default")return s.push(e.binary?"! summarizer: the shared setting is Ollama, and it isn't answering at "+i+" on this machine (checkpoints fall back to short summaries)":"! summarizer: the shared setting is Ollama, but Ollama isn't installed on this machine (checkpoints fall back to short summaries)"),s;let r=n.source==="machine"?" (this machine's own setting)":"";return s.push(e.binary?"! summarizer: Ollama isn't answering at "+i+r+" (checkpoints fall back to short summaries)":n.source==="machine"?"! summarizer: this machine's own setting is Ollama, but Ollama isn't installed here":"! summarizer: set to Ollama, but Ollama isn't installed"),s}function vE(n,e,{dim:t=i=>i,seen:s=null}={}){let i=Qu(n,e);return i?s&&s.has(i.text)?{lines:[n,t(" fix: same as above")],fix:i}:(s&&s.add(i.text),{lines:[n,t(" fix: "+i.text),...(i.more||[]).map(r=>t(" "+r))],fix:i}):{lines:[n],fix:null}}function P3(n,e={}){let t=e.leftoff||["leftoff"];if(n==="daemon-restart")return e.serviceCommands||null;let s=(e.platform||"darwin")==="darwin";return n==="syncthing-start"?s&&e.syncthingVia==="brew"&&e.brew?[[e.brew,"services","start","syncthing"]]:s&&e.syncthingVia==="app"&&e.syncthingPath?[["open","-a",e.syncthingPath]]:e.syncthingSystemd?[["systemctl","--user","enable","--now","syncthing"]]:null:n==="syncthing-restart"?s&&e.syncthingVia==="brew"&&e.brew?[[e.brew,"services","restart","syncthing"]]:e.syncthingSystemd?[["systemctl","--user","restart","syncthing"]]:null:n==="digest"?[[...t,"digest"]]:n==="notifier-rebuild"?[[...t,"notifier","rebuild"]]:n==="tailscale-on"?[[...t,"tailscale","on"]]:n==="ollama-start"&&e.ollamaStart?[e.ollamaStart]:null}async function xE(n,{confirm:e,run:t,say:s=()=>{},ctx:i={}}={}){let r=new Set,o=[];for(let a of n||[]){let l=a&&a.auto;if(!l||r.has(l.id))continue;r.add(l.id);let c=P3(l.id,i);if(!c||!c.length)continue;let u=c.map(f=>f.map(p=>/\s/.test(p)?'"'+p+'"':p).join(" ")).join(" && ");if(!await e(l.label.charAt(0).toUpperCase()+l.label.slice(1)+"? ("+u+")")){s("· skipped: "+l.label),o.push({id:l.id,ran:!1});continue}let d=!0,h="";for(let f of c){let p=await t(f);if(!p||p.status!==0){d=!1,h=(p&&(p.stderr||p.stdout)||"").toString().trim().split(`
|
|
199
|
+
`).pop()||"exit "+(p&&p.status);break}}s(d?"✓ did: "+l.label:"✗ "+l.label+" failed: "+h),o.push({id:l.id,ran:!0,ok:d,error:d?null:h})}return r.size||s("· nothing doctor can fix by itself here; the fix: lines above say what to do"),o}function AE({me:n,disk:e=null,st:t=null,healths:s=[],now:i=Date.now(),platform:r="darwin"}={}){let o=[],a=(y,w,S,k,v=null)=>o.push({machine:y,level:w,text:S,fix:k||null,...v?{cmd:v}:{}});if(e&&e.level&&e.level!=="ok"){let y=Go(e,{platform:r});a(n,e.level,Up(e,{machine:n}),y.text,y.cmd)}let l=t&&t.folder&&t.folder.errors||[],c=l.find(y=>/insufficient space/i.test(y.error||""));c&&!(e&&e.level==="red")&&a(n,"red","Syncing stopped on "+n+": disk full.",Go(e,{platform:r}).text);for(let y of l)y!==c&&a(n,"red","Sync error on "+n+": "+(y.error||"unknown"),Qu("! Folder error: "+y.error,{syncthing:t}).text);t&&t.installed&&!t.running&&a(n,"warn","Syncthing isn't running on "+n+".","leftoff sync start");let u={},d=t&&t.peers||[],h=new Set;for(let y of s){if(!y||!y.machine||y.machine===n)continue;h.add(y.machine);let w=i-Date.parse(y.at||0),S=d.find(v=>v.name===y.machine),k=y.disk&&y.disk.level;k==="red"||y.syncError&&/insufficient space/i.test(y.syncError)?a(y.machine,"red",y.machine+" stopped syncing: disk full.","free up disk space on "+y.machine+"; it catches up on its own"):k==="warn"&&w>Nk?a(y.machine,"red",y.machine+" stopped syncing: disk probably full ("+$o(y.disk.freeBytes)+" free when last heard).","free up disk space on "+y.machine):k==="warn"?a(y.machine,"warn",Up(y.disk,{machine:y.machine}),"free up disk space on "+y.machine):y.syncError&&a(y.machine,"red","Sync error on "+y.machine+": "+y.syncError,"run leftoff doctor on "+y.machine),w>Nk&&!(S&&S.connected)&&(u[y.machine]=y.at)}for(let y of d)y.connected||y.notSharing||(y.lastSeen?i-Date.parse(y.lastSeen):1/0)<=Nk||(u[y.name]||(u[y.name]=y.lastSeen||null),o.some(S=>S.machine===y.name)||a(y.name,"warn",y.name+" hasn't synced "+(y.lastSeen?"for "+R3(i-Date.parse(y.lastSeen)):"yet")+".",SE(y.name,{localErrors:l,peerHealth:s.find(S=>S&&S.machine===y.name)||null}).text));o.sort((y,w)=>(y.level==="red"?0:1)-(w.level==="red"?0:1));let f=o.some(y=>y.level==="red")?"red":o.length?"warn":"ok",p=o.find(y=>y.level==="red")||o[0],m=f==="ok"?"In sync":p.level==="red"&&/stopped|full/.test(p.text)?p.machine===n?"Sync stopped":p.machine+": sync stopped":p.level==="red"?"Sync error":p.machine===n&&/disk/i.test(p.text)?"Low disk":p.machine!==n?p.machine+" not syncing":"Sync: check";return{level:f,label:m,items:o,stale:u}}function R3(n){let e=Math.max(0,Math.round(n/6e4));if(e<60)return e+" min";let t=Math.round(e/60);return t<48?t+" h":Math.round(t/24)+" days"}function _E({remote:n=!1,machine:e=null,folder:t=null,home:s=null}={}){if(!n)return null;let i=t||null;i&&s&&(i===s||i.startsWith(s+"/"))&&(i="~"+i.slice(s.length));let r="npx -y -p @leftoff/cli leftoff setup"+(i?" --dir "+(/\s/.test(i)?'"'+i+'"':i):"");return{machine:e,dir:i,command:r}}function Oi(n,{fsImpl:e=wE}={}){let t=[],s=[];try{s=e.readdirSync(n)}catch{return t}for(let i of s)if(/^\.health\..+\.json$/.test(i))try{let r=JSON.parse(e.readFileSync(Fp.join(n,i),"utf8"));r&&r.machine&&t.push(r)}catch{}return t}function EE({machine:n,disk:e=null,st:t=null,now:s=Date.now()}={}){let i=t&&t.folder&&t.folder.errors||[];return{machine:n,at:new Date(s).toISOString(),disk:e?{level:e.level,freeBytes:e.freeBytes,totalBytes:e.totalBytes,pct:e.pct,what:e.what||null}:null,syncError:i.length?String(i[0].error||"").replace(/\([^)]*\/[^)]*\)/g,"").replace(/\s+/g," ").trim().slice(0,200):null}}function CE(n,e={}){let t={...e,syncthing:n},s=(n&&n.folder&&n.folder.errors||[]).map(r=>{let o=Qu("! Folder error: "+r.error,t);return o?[o.text,...o.more||[]].join(" · "):null}),i={};for(let r of n&&n.peers||[])if(!r.connected&&!r.notSharing){let o=Qu("! Peer "+r.name+": not connected",t);o&&(i[r.name]=o.text)}return{errors:s,peers:i,notRunning:n&&n.installed&&!n.running?Qu("! Syncthing: installed, not running",t).text:null}}var Zu,E3,C3,O3,yE,M3,N3,Nk,kE,j3,TE,ed=K(()=>{Zu=1024**3,E3=1,C3=5,O3=5*Zu,yE='open "x-apple.systempreferences:com.apple.settings.Storage"',M3='open "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation"',N3="https://docs.syncthing.net/users/config.html#config-option-options.minhomediskfree",Nk=900*1e3;kE=n=>(n<10?Math.round(n*100)/100:Math.round(n))+"%";j3={auto:"auto",claude:"Claude Code",anthropic:"Anthropic API",ollama:"Ollama","ollama-cloud":"Ollama cloud","openai-compatible":"OpenAI-compatible server",heuristic:"no model (heuristic)"};TE=(n,e)=>Fp.join(n,".health."+String(e).replace(/[^\w.-]/g,"_")+".json")});import{execFile as D3}from"node:child_process";import F3 from"node:fs";import td from"node:path";function OE(n,{timeoutMs:e=5e3}={}){return new Promise(t=>{let[s,...i]=n;try{D3(s,i,{timeout:e},(r,o,a)=>t({code:r?typeof r.code=="number"?r.code:1:0,stdout:String(o||""),stderr:String(a||"")||(r?String(r.message||r):"")}))}catch(r){t({code:1,stdout:"",stderr:String(r&&r.message||r)})}})}function ME(n,{machine:e=null,fsImpl:t=F3}={}){let s=new Set;for(let o of n||[]){if(!o||e&&o.machine&&o.machine!==e)continue;o.repo&&typeof o.repo.root=="string"&&o.repo.root&&s.add(o.repo.root);let a=o.parked&&o.parked.worktreeLock;a&&typeof a.root=="string"&&a.root&&s.add(a.root)}let i=[],r=new Set;for(let o of s){let a=td.join(o,".git","worktrees"),l=[];try{l=t.readdirSync(a)}catch{continue}for(let c of l){let u;try{u=String(t.readFileSync(td.join(a,c,"locked"),"utf8")).replace(/\n$/,"")}catch{continue}let d=c0(u);if(!d)continue;let h=null;try{h=td.dirname(String(t.readFileSync(td.join(a,c,"gitdir"),"utf8")).trim())}catch{}let f=h||td.join(a,c);if(r.has(f))continue;r.add(f);let p=(n||[]).find(m=>m&&m.id===d)||null;i.push({path:f,root:o,sessionId:d,title:p&&p.title||null,parked:!!(p&&p.parked)})}}return i}function NE(n){return(n||[]).filter(e=>!e.parked).map(e=>"! worktree "+e.path+" is still locked by Leftoff, but "+(e.title?"“"+e.title+"”":"its session")+" isn’t parked any more")}var Ik=K(()=>{Te()});import Cr from"node:fs";import U3 from"node:os";import Lk from"node:path";import{execFile as H3}from"node:child_process";function G3(n=process.env){return n.CLAUDE_CONFIG_DIR||Lk.join(n.HOME||U3.homedir(),".claude")}function Or(n,{env:e=process.env,dirs:t}={}){if(!n||!z3.test(n))return null;let s=t||[Lk.join(G3(e),"projects")],i=null;for(let r of s){let o=[];try{o=Cr.readdirSync(r,{withFileTypes:!0}).filter(a=>a.isDirectory()).map(a=>a.name)}catch{continue}for(let a of o){let l=Lk.join(r,a,n+".jsonl");try{let c=Cr.statSync(l);(!i||c.mtimeMs>i.mtimeMs)&&(i={file:l,mtimeMs:c.mtimeMs,size:c.size})}catch{}}}return i?i.file:null}function zp(n,{maxBytes:e=B3}={}){try{let t=Cr.statSync(n).size,s;if(t>e){let a=Cr.openSync(n,"r"),l=Buffer.alloc(256*1024),c=Cr.readSync(a,l,0,l.length,0);Cr.closeSync(a),s=l.subarray(0,c).toString("utf8");let{meta:u}=fl(s);return{cwd:u.cwd||null,prompts:1,customTitle:u.customTitle||null,gitBranch:u.gitBranch||null,truncated:!0}}s=Cr.readFileSync(n,"utf8");let{messages:i,meta:r}=fl(s),o=i.filter(a=>a.role==="user"&&a.text&&a.text.trim()).length;return{cwd:r.cwd||null,prompts:o,customTitle:r.customTitle||null,gitBranch:r.gitBranch||null,...r.model?{model:r.model}:{}}}catch{return null}}function LE(n,e,t=5e3){return new Promise(s=>{H3(n,e,{env:{...process.env,LC_ALL:"C"},timeout:t,maxBuffer:8*1024*1024},(i,r)=>s(i&&!r?"":String(r||"")))})}function $3(n){let e=[];for(let t of String(n||"").split(`
|
|
200
|
+
`)){let s=t.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/);s&&e.push({pid:Number(s[1]),ppid:Number(s[2]),tty:s[3],etime:s[4],args:s[5].trim()})}return e}function K3(n){let e=new Map;for(let t of String(n||"").split(`
|
|
201
|
+
`)){let s=t.match(/^\s*(\d+)\s+(.*)$/);s&&e.set(Number(s[1]),s[2].trim())}return e}function jE(n,e){let t=Bp(e||"").toLowerCase(),s=Bp(String(n.args||"").split(/\s+/)[0]).toLowerCase();if(/^claude(\s|$)/.test(t)||s==="claude")return!0;let i=String(n.args||"").split(/\s+/);return/^(node|bun)$/.test(s)&&(Bp(i[1])==="claude"||/@anthropic-ai\/claude-code\/cli\.m?js$/.test(i[1]||""))}function W3(n,e,t=new Map){let s=String(n).replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),i=new RegExp("(?:^|\\s)(?:--resume|-r|--session-id)(?:=|\\s+)"+s+"(?=\\s|$)"),r=e.filter(l=>i.test(l.args)&&jE(l,t.get(l.pid))),o=new Set(r.map(l=>l.pid)),a=r.filter(l=>!r.some(c=>c.ppid===l.pid&&o.has(c.pid)));return a.length===1?a[0]:null}async function IE(n,{platform:e=process.platform,runner:t=LE}={}){let s=new Map,i=n.filter(a=>Number.isInteger(a)&&a>1);if(!i.length)return s;if(e==="linux"){for(let a of i)try{s.set(a,Cr.readlinkSync("/proc/"+a+"/cwd"))}catch{}return s}let r=await t("lsof",["-a","-d","cwd","-p",i.join(","),"-Fpn"]),o=null;for(let a of r.split(`
|
|
202
|
+
`))a.startsWith("p")?o=Number(a.slice(1)):a.startsWith("n")&&o&&s.set(o,a.slice(1));return s}function q3(n,e,t=new Map){let s=new Map(e.map(r=>[r.pid,r])),i=s.get(n);for(let r=0;i&&r<12;r++){let o=Bp(t.get(i.pid)||String(i.args).split(/\s+/)[0]);for(let[a,l]of Y3)if(a.test(o))return{...hr(l==="kitty"?{TERM:"xterm-kitty"}:{TERM_PROGRAM:l}),source:"ps"};if(/^tmux/i.test(o))return null;i=s.get(i.ppid)}return null}async function PE(n,{cwd:e=null,claimedPids:t=new Set,env:s=process.env,platform:i=process.platform,runner:r=LE,now:o=Date.now(),transcriptDirs:a}={}){let l={via:[]},c=Or(n,{env:s,dirs:a});if(c){l.transcriptPath=c;let p=zp(c);p&&(p.cwd&&(l.cwd=p.cwd),l.prompts=p.prompts,p.customTitle&&(l.customTitle=p.customTitle),l.via.push("transcript"))}let u=e||l.cwd||null,d=$3(await r("ps",["-axo","pid=,ppid=,tty=,etime=,command="]));if(!d.length)return l;let h=K3(await r("ps",["-axo","pid=,comm="])),f=W3(n,d,h);if(f)l.via.push("ps-args");else if(u){let p=d.filter(S=>jE(S,h.get(S.pid))&&!t.has(S.pid)),m=p.filter(S=>!p.some(k=>k.ppid===S.pid)),y=await IE(m.map(S=>S.pid),{platform:i,runner:r}),w=m.filter(S=>y.get(S.pid)===u);w.length===1&&(f=w[0],l.via.push("ps-cwd"))}if(f){let p=Eo(f.etime);l.proc={pid:f.pid,tty:o0(f.tty),pidStartedAt:p==null?null:Z(o-p*1e3),source:"daemon"};let m=q3(f.pid,d,h);if(m&&(l.term=m,l.via.push("ps-term")),!l.cwd&&!e){let y=(await IE([f.pid],{platform:i,runner:r})).get(f.pid);y&&(l.cwd=y)}}return l}var B3,z3,Bp,Y3,Gp=K(()=>{Uo();Te();B3=32*1024*1024,z3=/^[0-9A-Za-z][0-9A-Za-z_-]{6,80}$/;Bp=n=>String(n||"").split("/").pop();Y3=[[/^ghostty$/i,"ghostty"],[/^iterm2?$/i,"iTerm.app"],[/^terminal$/i,"Apple_Terminal"],[/^wezterm(-gui)?$/i,"WezTerm"],[/^kitty$/i,"kitty"]]});import Nr from"node:fs";import Mr from"node:path";import J3 from"node:crypto";function id(n){let e=V3[n]||n;if(!sd.includes(e))throw Object.assign(new Error("kind is machine, repo, session or agent"),{code:"bad_kind"});return e}function Ko(n){return DE.test(String(n))}function bl(n){return String(n).replace(DE,"")}function rd(n){let e=String(n??"").normalize("NFC").replace(/\s+/g," ").trim();if(!e)throw Object.assign(new Error("a space needs a name"),{code:"bad_space"});if(e.length>32)throw Object.assign(new Error("a space name is at most 32 characters"),{code:"bad_space"});if(!/^[\p{L}\p{N}][\p{L}\p{N} ._&'-]*$/u.test(e))throw Object.assign(new Error("a space name is letters, numbers, spaces and . _ & ' -"),{code:"bad_space"});return e}function FE(n,e){let t=String(e??"").trim();if(!t||t.length>400||/[\u0000-\u001F]/.test(t))throw Object.assign(new Error("bad "+n.replace(/s$/,"")+" key"),{code:"bad_key"});return t}function Wo(){return{schema:1,machines:{},repos:{},sessions:{},agents:{}}}function X3(n){if(n==null)return null;if(typeof n=="string")return{space:n,at:"1970-01-01T00:00:00.000Z",by:""};if(typeof n!="object")return null;let e=n.space==null?null:String(n.space),t=typeof n.at=="string"&&!Number.isNaN(Date.parse(n.at))?n.at:"1970-01-01T00:00:00.000Z";return{space:e,at:t,by:typeof n.by=="string"?n.by:""}}function UE(n){let e=Wo();if(!n||typeof n!="object")return e;for(let t of sd){let s=n[t];if(!(!s||typeof s!="object"))for(let[i,r]of Object.entries(s)){let o=X3(r);o&&(e[t][i]=o)}}return e}function Z3(n,e){if(!n)return e;if(!e)return n;let t=Date.parse(n.at),s=Date.parse(e.at);return t!==s?t>s?n:e:n.by!==e.by?n.by>e.by?n:e:n.space==null!=(e.space==null)?n.space==null?e:n:String(n.space)>=String(e.space)?n:e}function Q3(...n){let e=Wo();for(let t of n){let s=UE(t);for(let i of sd)for(let[r,o]of Object.entries(s[i]))e[i][r]=Z3(e[i][r],o)}return e}function Yo(n){let e=[],t=[];try{t=Nr.readdirSync(n)}catch{}for(let r of t)(r===nd||r.startsWith("spaces.")&&r.endsWith(".json")&&Ko(r)&&bl(r)===nd)&&e.push(r);let s=[],i=[];for(let r of e.sort())try{s.push(JSON.parse(Nr.readFileSync(Mr.join(n,r),"utf8"))),r!==nd&&i.push(r)}catch{}return{map:Q3(...s),conflicts:i}}function vl(n,e){Nr.mkdirSync(Mr.dirname(n),{recursive:!0});let t=Mr.join(Mr.dirname(n),".syncthing."+Mr.basename(n)+"."+process.pid+"."+J3.randomBytes(4).toString("hex")+".tmp");Nr.writeFileSync(t,e),Nr.renameSync(t,n)}function eU(n,e,{removeConflicts:t=[]}={}){let s=UE(e),i={schema:1};for(let r of sd)i[r]=Object.fromEntries(Object.entries(s[r]).sort(([o],[a])=>o.localeCompare(a)));vl(Mr.join(n,nd),JSON.stringify(i,null,2)+`
|
|
203
|
+
`);for(let r of t)try{Nr.unlinkSync(Mr.join(n,r))}catch{}}function jk(n,e,{machine:t="",now:s=Date.now()}={}){let{map:i,conflicts:r}=Yo(n),o={at:new Date(s).toISOString(),by:t},a=e(i,o);return eU(n,i,{removeConflicts:r}),a===void 0?i:a}function ds(n,e){let t={};for(let[s,i]of Object.entries(n&&n[e]||{}))i&&i.space!=null&&(t[s]=i.space);return t}function Ks(n,e=[]){let t=new Set;for(let s of sd)for(let i of Object.values(ds(n,s)))t.add(i);for(let s of e)t.add(Ir(n,s));return xl([...t])}function xl(n){return[...new Set(n.filter(Boolean))].sort((e,t)=>e==="Work"?-1:t==="Work"?1:e.localeCompare(t,"en",{sensitivity:"base"})||e.localeCompare(t))}function Ir(n,e){let t=n&&n.machines&&n.machines[e];return t&&t.space!=null?t.space:e||"Unsorted"}function nU(n,{repoKey:e,repoName:t,repoLabel:s}={}){let i=ds(n,"repos");if(e&&i[e]!=null)return{key:e,space:i[e]};let r=[s,t,tU(e)].filter(Boolean).map(RE);for(let o of r)for(let[a,l]of Object.entries(i))if(RE(a)===o)return{key:a,space:l};return null}function Lr(n,e,{repoKey:t,repoName:s,repoLabel:i,machine:r}={}){let o=ds(n,"sessions");if(e&&e.id&&o[e.id]!=null)return{space:o[e.id],source:"session",key:e.id};let a=e&&(e.agent&&e.agent.name||(typeof e.id=="string"&&e.id.startsWith("agent:")?e.id.split(":")[1]:null));if(a){let d=ds(n,"agents");if(d[a]!=null)return{space:d[a],source:"agent",key:a}}let l=!a&&nU(n,{repoKey:t||e&&(e.repoKey||e.repo&&e.repo.root||e.cwd),repoName:s||e&&(e.repoName||e.repo&&e.repo.name),repoLabel:i||e&&e.repoLabel});if(l)return{space:l.space,source:"repo",key:l.key};let c=r||(e&&a?e.agent&&e.agent.puller:e&&e.machine)||"",u=n&&n.machines&&n.machines[c];return u&&u.space!=null?{space:u.space,source:"machine",key:c}:{space:c||"Unsorted",source:"default",key:c}}function HE(n,{home:e=""}={}){let t=[e,"/root"].filter(Boolean).map(o=>o.replace(/\/+$/,"")),s=o=>t.includes(o)||/^\/(Users|home)\/[^/]+$/.test(o)||/^[A-Za-z]:\\Users\\[^\\]+$/.test(o),i=o=>{for(let l of t)if(l&&(o===l||o.startsWith(l+"/")))return"~"+o.slice(l.length);let a=/^\/(Users|home)\/[^/]+(?=\/|$)/.exec(o);return a?"~"+o.slice(a[0].length):o},r=new Map;for(let o of n||[]){let a=String(o&&o.key||"").trim().replace(/\/+$/,"")||(o&&o.key?"/":""),l=String(o&&o.name||"").trim();if(!l||["/",".","..","~","no-repo"].includes(l)||/^[\\/]+$/.test(l)||!a||a==="/"||a==="~"||s(a))continue;let c=Date.parse(o&&o.at||"")||0,u=l.toLowerCase(),d=r.get(u);if(d&&d.t>=c)continue;let h=a.includes("/")?i(a.slice(0,a.lastIndexOf("/"))||"/"):"";r.set(u,{name:l,parent:h,lastAt:c?new Date(c).toISOString():null,t:c})}return[...r.values()].sort((o,a)=>a.t-o.t||o.name.localeCompare(a.name)).map(({t:o,...a})=>a)}var nd,Sl,sd,V3,DE,RE,tU,$p,jr=K(()=>{nd="spaces.json",Sl=["Work","Personal"],sd=["machines","repos","sessions","agents"],V3={machine:"machines",repo:"repos",session:"sessions",agent:"agents"};DE=/\.sync-conflict-(\d{8})-(\d{6})-([A-Za-z0-9]{1,16})(?=\.[^.]+$|$)/;RE=n=>String(n||"").toLowerCase(),tU=n=>{let e=String(n||"").split(/[\\/]+/).filter(Boolean);return e.length?e[e.length-1]:""};$p=class{constructor(e,t,{now:s=()=>Date.now()}={}){this.root=e,this.machine=t,this.now=s,this.cache=null,this.checkedAt=0}sig(){let e=[];try{e=Nr.readdirSync(this.root).filter(t=>t===nd||t.startsWith("spaces.")&&Ko(t))}catch{}return e.sort().map(t=>{try{let s=Nr.statSync(Mr.join(this.root,t));return t+":"+s.mtimeMs+":"+s.size}catch{return t}}).join("|")}map({fresh:e=!1}={}){let t=Date.now();if(!e&&this.cache&&t-this.checkedAt<2e3)return this.cache.map;let s=this.sig();if(this.checkedAt=t,!e&&this.cache&&this.cache.sig===s)return this.cache.map;let{map:i,conflicts:r}=Yo(this.root);return this.cache={sig:s,map:i,conflicts:r},i}update(e){let t=jk(this.root,e,{machine:this.machine,now:this.now()});return this.cache=null,t}resolve(e,t){return Lr(this.map(),e,t)}machineSpace(e=this.machine){return Ir(this.map(),e)}}});import Se from"node:fs";import ge from"node:path";import Pk from"node:crypto";function Jt(n,e){Se.mkdirSync(ge.dirname(n),{recursive:!0});let t=ge.join(ge.dirname(n),"."+ge.basename(n)+"."+process.pid+"."+Pk.randomBytes(4).toString("hex")+".tmp");Se.writeFileSync(t,e),Se.renameSync(t,n)}function Mi(n){try{return Se.readdirSync(n,{withFileTypes:!0}).filter(e=>e.isDirectory()&&!e.name.startsWith(".")).map(e=>e.name)}catch{return[]}}function BE(n){return Se.existsSync(ge.join(n,"session.md"))||Se.existsSync(ge.join(n,"checkpoints"))}function zE(n,e,t,s){let i=ge.join(qe(n).spool,e,String(t||"unknown").replace(/[^A-Za-z0-9_-]/g,"_"));Se.mkdirSync(i,{recursive:!0});let r=ge.join(i,Date.now()+"-"+Pk.randomBytes(4).toString("hex")+".json");return Se.writeFileSync(r,JSON.stringify(s)+`
|
|
204
204
|
`,{flag:"wx"}),r}var En,hs=K(()=>{Te();Fe();Hn();jr();En=class{constructor(e,t="unknown"){this.root=e,this.machine=t,this.p=qe(e)}get mySessions(){return ge.join(this.p.sessions,this.machine)}get mySpool(){return ge.join(this.p.spool,this.machine)}get myDigests(){return ge.join(this.p.digests,this.machine)}sessionDir(e){return e.folder||(e.folder=this.findFolderFor(e.id)||this.machine+"/"+AA(e)),ge.join(this.p.sessions,e.folder)}findFolderFor(e){let t="_"+tu(e),s="_"+xA(e);for(let i of Mi(this.mySessions)){if(s!==t&&i.endsWith(s)){try{if(Tt(Se.readFileSync(ge.join(this.mySessions,i,"session.md"),"utf8")).data.id===e)return this.machine+"/"+i}catch{}continue}if(!i.endsWith(t))continue;let r=ge.join(this.mySessions,i,"session.md");if(!Se.existsSync(r))return this.machine+"/"+i;try{if(Tt(Se.readFileSync(r,"utf8")).data.id===e)return this.machine+"/"+i}catch{}}return null}writeSession(e,t){let s=ge.join(this.sessionDir(e),"session.md");return Jt(s,Tf(e,t)),s}writeCheckpoint(e,t){let s=ge.join(this.sessionDir(e),"checkpoints");Se.mkdirSync(s,{recursive:!0});for(let i=0;i<50;i++){let r=ge.join(s,t0(t.createdAt,i)),o={...vi(t),machine:t.machine||e.machine||this.machine,id:t.id&&i===0?t.id:xf(t.sessionId,t.createdAt,i),file:ge.basename(r)};try{return Se.writeFileSync(r,FA({...o,backend:e.backend}),{flag:"wx"}),o}catch(a){if(a.code!=="EEXIST")throw a}}throw new Error("could not find a free checkpoint file name")}loadSessionDir(e,t,s){let i=[];try{for(let o of Se.readdirSync(ge.join(e,"checkpoints")).filter(a=>a.endsWith(".md")).sort())try{let a=vi(UA(Se.readFileSync(ge.join(e,"checkpoints",o),"utf8")));a.file=o,!a.machine&&s&&(a.machine=s),a.sessionId&&a.createdAt&&i.push(a)}catch{}}catch{}let r=null;try{r=Ef(Se.readFileSync(ge.join(e,"session.md"),"utf8"))}catch{}if(!r||!r.id){if(!i.length)return null;let o=i[i.length-1];r={id:o.sessionId,title:o.sessionTitle,titleSource:o.sessionTitle?"custom":null,cwd:o.cwd,machine:o.machine||s,repo:o.repo?{name:o.repo,branch:o.branch}:null,capture:"mod",status:"stale",startedAt:i[0].createdAt,lastActivityAt:o.createdAt,turns:0,turnsSinceCheckpoint:0,backgroundTasks:0,lastHeartbeatAt:o.createdAt,needsRewrite:!0}}return!r.machine&&s&&(r.machine=s),r.folder=t,{session:r,checkpoints:i}}loadAll({machine:e}={}){let t=[];for(let s of Mi(this.p.sessions)){let i=ge.join(this.p.sessions,s);if(BE(i)){let r=this.loadSessionDir(i,s,null);r&&(!e||r.session.machine===e)&&t.push(r);continue}if(!(e&&s!==e))for(let r of Mi(i)){let o=this.loadSessionDir(ge.join(i,r),s+"/"+r,s);o&&t.push(o)}}return t}machines(){let e=new Set([this.machine]);for(let t of Mi(this.p.sessions))BE(ge.join(this.p.sessions,t))||e.add(t);for(let t of Mi(this.p.digests))e.add(t);return[...e].sort()}writeDigest(e,t){let s=ge.join(this.p.digests,e+".md");vl(s,t);for(let i of this.digestCopies(e))if(i!==e+".md")try{Se.unlinkSync(ge.join(this.p.digests,i))}catch{}return s}writeMachineDigest(e,t){let s=ge.join(this.myDigests,e+".md");return Jt(s,t),s}digestCopies(e){let t=[];try{t=Se.readdirSync(this.p.digests)}catch{}return t.filter(s=>s===e+".md"||s.startsWith(e+".")&&s.endsWith(".md")&&Ko(s)&&bl(s)===e+".md")}readSharedDigest(e){let t=null,s=this.digestCopies(e);for(let i of s)try{let r=ge.join(this.p.digests,i),o=Se.readFileSync(r,"utf8"),a=Tt(o).data||{},l=Date.parse(a.generated_at)||Se.statSync(r).mtimeMs;(!t||l>t.t||l===t.t&&i===e+".md")&&(t={t:l,file:i,markdown:o,by:a.machine||null})}catch{}return t?{machine:null,shared:!0,by:t.by,file:t.file,duplicates:s.length-1,markdown:t.markdown}:null}readDigest(e,t){if(t&&t!=="all")try{return{machine:t,shared:!1,markdown:Se.readFileSync(ge.join(this.p.digests,t,e+".md"),"utf8")}}catch{return null}let s=this.readSharedDigest(e);if(s||t==="all")return s;for(let i of[this.machine,...Mi(this.p.digests).filter(r=>r!==this.machine)])try{return{machine:i,shared:!1,markdown:Se.readFileSync(ge.join(this.p.digests,i,e+".md"),"utf8")}}catch{}return null}listDigests(){let e=[],t=new Set;try{for(let i of Se.readdirSync(this.p.digests)){let r=/^(\d{4}-\d{2}-\d{2})\.(?:.*\.)?md$/.exec(i);r&&(i===r[1]+".md"||bl(i)===r[1]+".md")&&t.add(r[1])}}catch{}for(let i of t)e.push({date:i,machine:null,shared:!0});for(let i of Mi(this.p.digests))try{for(let r of Se.readdirSync(ge.join(this.p.digests,i)))/^\d{4}-\d{2}-\d{2}\.md$/.test(r)&&e.push({date:r.slice(0,10),machine:i})}catch{}let s=i=>i.shared?0:i.machine===this.machine?1:2;return e.sort((i,r)=>r.date.localeCompare(i.date)||s(i)-s(r)||String(i.machine).localeCompare(String(r.machine)))}stampAlive(e={},t=Date.now()){let s=ge.join(this.p.digests,".machines",this.machine.replace(/[^\w.-]/g,"_")+".json");vl(s,JSON.stringify({machine:this.machine,seenAt:new Date(t).toISOString(),...e},null,2)+`
|
|
205
|
-
`)}aliveMachines(){let e=[],t=ge.join(this.p.digests,".machines"),s=[];try{s=Se.readdirSync(t).filter(i=>i.endsWith(".json")&&!i.startsWith(".")&&!Ko(i))}catch{}for(let i of s)try{let r=JSON.parse(Se.readFileSync(ge.join(t,i),"utf8"));r&&r.machine&&e.push(r)}catch{}return e}writeCatchup(e,t){Se.mkdirSync(this.p.catchups,{recursive:!0});let s=String(e).replace(/\.md$/,"");for(let i=0;i<50;i++){let r=i?s+"-"+(i+1):s,o=ge.join(this.p.catchups,r+".md");try{let a=ge.join(this.p.catchups,"."+r+"."+process.pid+"."+Pk.randomBytes(4).toString("hex")+".tmp");Se.writeFileSync(a,t);try{try{Se.linkSync(a,o)}catch(l){if(l.code==="EEXIST")throw l;Se.writeFileSync(o,t,{flag:"wx"})}}finally{try{Se.unlinkSync(a)}catch{}}return{name:r,file:o}}catch(a){if(a.code!=="EEXIST")throw a}}throw new Error("could not find a free catch-up file name")}readCatchup(e){if(!/^\d{4}-\d{2}-\d{2}T\d{4}(?:-\d+)?$/.test(String(e||"")))return null;try{return{name:e,markdown:Se.readFileSync(ge.join(this.p.catchups,e+".md"),"utf8")}}catch{return null}}listCatchups(){let e=[];try{e=Se.readdirSync(this.p.catchups).filter(i=>/^\d{4}-\d{2}-\d{2}T\d{4}(?:-\d+)?\.md$/.test(i))}catch{return[]}let t=[];for(let i of e){let r={};try{let o=Se.openSync(ge.join(this.p.catchups,i),"r");try{let a=Buffer.alloc(8192),l=Se.readSync(o,a,0,a.length,0);r=Tt(a.slice(0,l).toString("utf8")).data||{}}finally{Se.closeSync(o)}}catch{}t.push({name:i.slice(0,-3),generatedAt:r.generated_at?Z(r.generated_at):null,from:r.window_from?Z(r.window_from):null,to:r.window_to?Z(r.window_to):null,label:r.window_label||null,sessions:typeof r.sessions=="number"?r.sessions:null,needsYou:Array.isArray(r.needs_you)?r.needs_you.length:0,repos:r.repos==="all"||r.repos==null?"all":[].concat(r.repos).map(String),machine:r.made_on==null?null:String(r.made_on),model:r.model==null?null:String(r.model)})}let s=i=>Date.parse(i.generatedAt||"")||0;return t.sort((i,r)=>s(r)-s(i)||r.name.localeCompare(i.name))}lastCatchupAt(){let e=this.listCatchups()[0];if(!e)return null;let t=Date.parse(e.to||e.generatedAt||"");return isNaN(t)?null:t}readSpool(){let e=[];for(let t of Mi(this.mySpool)){let s=ge.join(this.mySpool,t),i=[];try{i=Se.readdirSync(s)}catch{}for(let r of i){if(!r.endsWith(".json"))continue;let o=ge.join(s,r),a=null;try{a=Se.readFileSync(o,"utf8")}catch{continue}e.push({file:o,t:Number(r.split("-")[0])||0,body:a})}}return e.sort((t,s)=>t.t-s.t||t.file.localeCompare(s.file))}spoolCount(){let e=0;for(let t of Mi(this.mySpool))try{e+=Se.readdirSync(ge.join(this.mySpool,t)).filter(s=>s.endsWith(".json")).length}catch{}return e}}});import _l from"node:fs";import
|
|
206
|
-
`).filter(Boolean)}catch(e){return/not a git repository/i.test(String(e.stderr||""))?[]:null}}function u3(n){try{let e=JSON.parse(_l.readFileSync(n,"utf8"));if(e&&Array.isArray(e.events))return e.events.filter(t=>t&&typeof t=="object");if(e&&e.type&&e.sessionId)return[e]}catch{}return null}function d3(n){let e=t=>{for(let i of Al(t)||[]){let r=Cn.join(t,i);od(r)&&e(r)}let s=Al(t);if(s&&!s.length)try{_l.rmdirSync(t)}catch{}};e(n)}function Yp({dirs:n,ingest:e,lsFiles:t=c3,dryRun:s=!1,log:i=()=>{}}){let r={found:0,cleaned:0,events:0,files:0,pending:[],tracked:[],foreign:[],unchecked:[]};for(let o of n||[]){let a=l3(o);if(!a)continue;if(r.found++,a.foreign){r.foreign.push({dir:o,path:a.foreign});continue}let l=t(o);if(l===null){r.unchecked.push(o);continue}if(l.length){r.tracked.push(o);continue}if(s){r.pending.push({dir:o,files:a.files.length});continue}let c=0;for(let u of a.files){let d=u3(u);try{d&&d.length&&(r.events+=e(d)||0),_l.unlinkSync(u),r.files++}catch(h){c++,i("stray spool file kept: "+u+" "+h.message)}}d3(a.nullDir),c||r.cleaned++}return r}function GE(n){let e=[];if(!n)return e;n.files&&e.push("✓ removed "+n.files+" stray event file"+(n.files===1?"":"s")+" an older mod left in null/ ("+n.cleaned+" folder"+(n.cleaned===1?"":"s")+")");for(let t of n.pending||[])e.push("· an older mod left a null/ folder in "+Kp(t.dir)+" ("+t.files+" file"+(t.files===1?"":"s")+"); the daemon cleans it up when it starts");for(let t of n.tracked||[])e.push("! an older mod's null/ folder is committed in "+Kp(t)+": git rm -r null/");for(let t of n.foreign||[])e.push("· left "+Kp(Cn.join(t.dir,"null"))+" alone: it holds more than old Leftoff spool files");for(let t of n.unchecked||[])e.push("· left "+Kp(Cn.join(t,"null"))+" alone: couldn't ask git whether it is tracked");return e}var r3,o3,od,a3,Al,Kp,Rk=K(()=>{r3=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,o3=/^\d+-[0-9a-f]{8}\.json$/;od=n=>{try{return _l.lstatSync(n).isDirectory()}catch{return!1}},a3=n=>{try{return _l.lstatSync(n).isFile()}catch{return!1}},Al=n=>{try{return _l.readdirSync(n)}catch{return null}};Kp=n=>{let e=s3.homedir();return e&&(n===e||n.startsWith(e+Cn.sep))?"~"+n.slice(e.length):n}});var Dk=K(()=>{});function $E(n){let e={indent:-1,children:[]},t=[e],s=null;for(let r of String(n||"").split(/\r?\n/)){let o=r.match(/^(\s*)[-*+]\s+(.*)$/);if(o){let a=o[1].replace(/\t/g," ").length;for(;t.length>1&&a<=t[t.length-1].indent;)t.pop();s={indent:a,text:o[2].trim(),children:[]},t[t.length-1].children.push(s),t.push(s)}else if(s&&/^\s+\S/.test(r))s.text+=" "+r.trim();else if(r.trim())s=null,t.length=1;else continue}let i=r=>r.map(({text:o,children:a})=>({text:o,children:i(a)}));return i(e.children)}var qp=K(()=>{});var h3,Fk,$V,KV,Uk=K(()=>{h3="★✶✦✩☆⭐",Fk="─━═\\-",$V=new RegExp("^(`*)\\s*(["+h3+"])\\uFE0F?\\s+([^`"+Fk.replace("\\-","")+"]{1,40}?)\\s*(["+Fk+"]{3,})?\\s*\\1$"),KV=new RegExp("^`*\\s*["+Fk+"]{3,}\\s*`*$")});var Hk=K(()=>{});var Jp=K(()=>{});var WE=K(()=>{Jp()});var YE=K(()=>{});var qE=K(()=>{YE()});function m3(n){let e=n.match(/limit reached\|(\d{9,13})/i);if(e){let s=new Date(Number(e[1])*(e[1].length>11?1:1e3));if(!Number.isNaN(s.getTime()))return"resets "+s.toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})}let t=n.match(/\bresets?\s+(?:at\s+)?([^·|\n.]{1,40})/i);return t?"resets "+JE(t[1]).replace(/\s*\(.*$/,""):""}function qo(n,{subtype:e=null,code:t=null,signal:s=null,endReason:i=null}={}){let r=JE(n).slice(0,p3),o=l=>({label:l,raw:r||null});if(r&&/^\[?Request interrupted by user/i.test(r)||!r&&i==="aborted")return null;if(/usage limit|hit your (usage )?limit|limit reached|out of (extra )?usage|weekly limit|session limit/i.test(r)){let l=m3(r);return o("Claude’s usage limit is reached"+(l?" · "+l:""))}if(/rate[ _-]?limit|\b429\b|too many requests/i.test(r))return o("Claude is rate limited right now · try again in a minute");if(/prompt is too long|context (length|window)|too many tokens|maximum context|input is too long/i.test(r))return o("The conversation is too long for Claude · run /compact in its terminal, then retry");if(/overloaded|\b529\b/i.test(r))return o("Claude’s servers are overloaded · try again in a minute");if(/credit balance|billing|payment required|\b402\b/i.test(r))return o("Claude’s account is out of credit");if(/invalid api key|run \/login|please (log|sign) ?in|not logged in|oauth token|token (has )?expired|authentication|\b401\b|\b403\b|unauthori[sz]ed|forbidden/i.test(r))return o(VE);if(/timed? ?out|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed|network|socket hang up|connection (error|refused|reset)/i.test(r))return o("Claude couldn’t reach its servers · check this Mac’s connection");if(/no conversation found|session .*not found/i.test(r))return o("Claude Code couldn’t reopen this conversation");let a=r.match(/API Error:?\s*(\d{3})?/i);return a?o("Claude’s API returned an error"+(a[1]?" ("+a[1]+")":"")):e==="error_max_turns"?o("Claude stopped at its turn limit"):i==="refusal"?o("Claude declined to answer this one"):t!=null||s?o("Claude Code exited"+(t!=null?" (code "+t+")":" ("+s+")")+" before answering"):r?o("Claude Code ended the turn with an error"):i==="error"?o("Claude Code ended the turn with an error (it wrote no details)"):null}function Vp(n=0){return{phase:"sent",at:n,last:n,tool:null,waiting:null,ran:[],endedAt:null}}function Ge(n,e={}){let t={...n||Vp(e.at||0),ran:[...n&&n.ran||[]]},s=e.at||t.last;if(f3.has(t.phase)&&!(t.phase==="replied"&&(e.type==="tool_done"||e.type==="why")))return t.queued&&(delete t.queued,delete t.cancellable,delete t.sendNow),t;switch(t.queued&&!g3.has(e.type)&&(delete t.queued,delete t.cancellable,delete t.sendNow),e.type){case"queued":if(t.phase==="sent"||t.phase==="received")t.queued=!0,t.cancellable=!!e.cancellable,e.cancellable&&e.sendNow?t.sendNow=!0:delete t.sendNow;else return t;break;case"unqueued":break;case"delivered":t.phase==="sent"&&(t.phase="received");break;case"failed":t.phase==="sent"&&(t.phase="failed",t.error=e.error||null,t.endedAt=s);break;case"started":case"text":t.phase!=="replied"&&(t.phase="working"),e.type==="text"&&(t.waiting=null);break;case"tool":t.phase="working",t.tool={name:Tl(e.tool,40),detail:Tl(e.detail,300)},t.waiting=null;break;case"tool_done":{let i=Tl(e.tool||t.tool&&t.tool.name,40);i&&t.ran.push({tool:i,detail:Tl(e.detail||t.tool&&t.tool.detail,300),asked:!!e.asked,...typeof e.id=="string"&&e.id&&e.id.length<=120?{id:e.id}:{}}),t.ran.length>20&&(t.ran=t.ran.slice(-20)),t.tool=null,t.waiting=null;break}case"ask":t.phase="working",t.waiting={tool:Tl(e.tool,40),detail:Tl(e.detail,300)};break;case"ask_done":t.waiting=null;break;case"reply":t.phase="replied",t.tool=null,t.waiting=null,t.endedAt=s;break;case"ended":t.tool=null,t.waiting=null,t.endedAt=s,t.phase=e.text?"replied":"stopped",e.why&&(t.why=e.why);break;case"why":if(e.why&&!(t.why&&t.why.raw&&!e.why.raw))t.why=e.why;else return t;break;case"folded":(t.phase==="sent"||t.phase==="received")&&(t.phase="working"),t.folded=e.owner||!0,t.tool=null,t.waiting=null;break;case"answered":t.phase="replied",t.folded=e.owner||t.folded||!0,t.tool=null,t.waiting=null,t.endedAt=s;break;case"lost":t.tool=null,t.waiting=null,t.endedAt=s,t.phase="stopped",t.lost=!0,e.why&&(t.why=e.why);break;default:return t}return t.last=s,t}function Xp(n){let e=new Set,t=[];for(let s of n&&n.ran||[])s.asked||e.has(s.tool)||(e.add(s.tool),t.push(s.tool));return t}var f3,Tl,p3,JE,VE,XE,g3,Zp=K(()=>{f3=new Set(["replied","failed"]),Tl=(n,e)=>{let t=String(n??"").replace(/\s+/g," ").trim();return t.length>e?t.slice(0,e-1)+"…":t},p3=600,JE=n=>String(n||"").replace(/\s+/g," ").trim();VE="Claude Code isn’t signed in · run /login in a terminal on this Mac",XE=n=>!!n&&n.label===VE;g3=new Set(["delivered","queued","ask_done","noop","why"])});var ZE=K(()=>{});function zk(n){return String(n||"").split(/[\\/]+/).filter(Boolean)}function QE(n){let e=zk(n);return e.length?e[e.length-1]:""}function ad(n){if(!n)return"no-repo";if(n.repoKey)return n.repoKey;let e=n.repo||{};return n.repoRoot||e.root||n.cwd||e.name||n.repoName||"no-repo"}function El(n,e=null){let t=String(n||"").replace(/\/+$/,"");return!t||t==="~"||n==="/"||/^[A-Za-z]:\\?$/.test(t)||e&&t===String(e).replace(/\/+$/,"")?!0:/^\/(Users|home)\/[^/]+$/.test(t)||t==="/root"||t==="/var/root"}function xs(n,{names:e=null}={}){let t=[...new Set([...n].filter(Boolean))],s=new Map(t.map(a=>[a,1])),i=a=>e&&e.get(a)||null,r=new Map;for(let a of t){let l=i(a);l&&r.set(l,(r.get(l)||0)+1)}let o=a=>{if(a==="no-repo"||El(a))return Qp;let l=i(a);if(l&&r.get(l)===1&&s.get(a)===1&&!t.some(u=>u!==a&&!i(u)&&QE(u)===l))return l;let c=zk(a);return c.length?c.slice(-Math.min(s.get(a),c.length)).join("/"):a};for(let a=0;a<12;a++){let l=new Map;for(let u of t){let d=o(u);l.set(d,[...l.get(d)||[],u])}let c=!1;for(let u of l.values())if(!(u.length<2))for(let d of u)s.get(d)<zk(d).length&&(s.set(d,s.get(d)+1),c=!0);if(!c)break}return new Map(t.map(a=>[a,o(a)]))}function em(n){let e=String(n&&n.repoLabel||"").trim(),t=String(n&&n.repoName||"").trim();return!e&&!t?{name:Qp,sub:null}:e===Qp||t==="no-repo"||n&&n.repoKey==="no-repo"?{name:Qp,sub:null}:t?!e||e===t?{name:t,sub:null}:{name:t,sub:e}:{name:e,sub:null}}function Gk(n,{countIn:e}={}){let t=new Map;for(let i of n){let r=ad(i),o=t.get(r)||{key:r,label:i.repoLabel||null,count:0,total:0,last:0};o.total++,o.last=Math.max(o.last,y3(i)),!o.label&&i.repoLabel&&(o.label=i.repoLabel),t.set(r,o)}if(e)for(let i of e){let r=t.get(ad(i));r&&r.count++}else for(let i of t.values())i.count=i.total;let s=xs(t.keys());for(let i of t.values())i.label||(i.label=s.get(i.key));return[...t.values()].sort((i,r)=>r.last-i.last||i.label.localeCompare(r.label))}function Bn(n,e,{loose:t=!1}={}){let s=String(e||"").trim().toLowerCase().replace(/\/+$/,"");if(!s)return!1;let i=ad(n),r=[i,n.repoLabel,n.repoName,QE(i)].filter(Boolean).map(o=>String(o).toLowerCase());return r.includes(s)||s.includes("/")&&String(i).toLowerCase().endsWith("/"+s)||t&&r.some(o=>o.includes(s))?!0:Array.isArray(n.alsoRepos)&&n.alsoRepos.some(o=>o&&o.key&&Bn({repoKey:o.key,repoLabel:o.label},e,{loose:t}))}function tm(n,e){if(!e)return n;let t=n.filter(s=>Bn(s,e));return t.length?t:n.filter(s=>Bn(s,e,{loose:!0}))}function ld(n,e,t=[]){let s=String(e||"").toLowerCase().trim().split(/\s+/).filter(Boolean);if(!s.length)return!0;let i=n.lastCheckpoint||null,r=[n.title,n.repoLabel,n.repoName,n.repoProject&&n.repoProject.name,n.repo&&n.repo.branch,n.repo&&n.repo.github,n.worktree&&n.worktree.name,n.worktree&&n.worktree.branch,n.cwd,n.lastPreview,n.firstPrompt,n.machine,i&&i.doing,i&&i.next,i&&i.stoppedAt,...t].filter(Boolean).map(l=>String(l).toLowerCase()),o=String(n.id||"").toLowerCase(),a=String(n.shortId||"").toLowerCase();return s.every(l=>{let c=l.replace(/^cp:/,"");return c.length>=4&&(o.startsWith(c)||a&&a===c)?!0:l.startsWith("cp:")?!1:r.some(u=>u.includes(l))})}var Qp,Bk,y3,Ws=K(()=>{Qp="No repo";Bk=n=>{let e=Date.parse(n||"");return isNaN(e)?0:e},y3=n=>Math.max(Bk(n.lastActivityAt),Bk(n.endedAt),Bk(n.startedAt))});var cd=K(()=>{Hn();Jp()});var eC=K(()=>{Zp();ZE();Hn();Ws();cd()});var $k=K(()=>{cd();Jp()});var Kk=K(()=>{});function ud(n){let s=(String(n||"").replace(/\s+/g," ").trim().split(/(?<=[.!?])\s+(?=\S)/).filter(r=>/\?\s*$/.test(r)).pop()||"").replace(/[*_`]+/g,"").match(/^(.*?)(?::|\s[—–-]\s)\s*(.+?)\s*\?\s*$/);if(!s||!/\bor\b/i.test(s[2]))return[];let i=s[2].split(/\s*,\s*(?:or\s+|and\s+)?|\s+or\s+/i).map(r=>r.trim().replace(/[.;:]$/,"")).filter(Boolean);return i.length<2||i.length>6||i.some(r=>r.length>60)||/^(not|no|neither|something else|anything else)\b/i.test(i[i.length-1])?[]:i.map(N3)}var N3,Wk=K(()=>{Dk();qp();Uk();Hk();WE();qE();eC();cd();$k();Kk();N3=n=>n.charAt(0).toUpperCase()+n.slice(1)});import I3 from"node:path";function tC(n){let e=[],t=!1;for(let s of Yk(n).split(`
|
|
205
|
+
`)}aliveMachines(){let e=[],t=ge.join(this.p.digests,".machines"),s=[];try{s=Se.readdirSync(t).filter(i=>i.endsWith(".json")&&!i.startsWith(".")&&!Ko(i))}catch{}for(let i of s)try{let r=JSON.parse(Se.readFileSync(ge.join(t,i),"utf8"));r&&r.machine&&e.push(r)}catch{}return e}writeCatchup(e,t){Se.mkdirSync(this.p.catchups,{recursive:!0});let s=String(e).replace(/\.md$/,"");for(let i=0;i<50;i++){let r=i?s+"-"+(i+1):s,o=ge.join(this.p.catchups,r+".md");try{let a=ge.join(this.p.catchups,"."+r+"."+process.pid+"."+Pk.randomBytes(4).toString("hex")+".tmp");Se.writeFileSync(a,t);try{try{Se.linkSync(a,o)}catch(l){if(l.code==="EEXIST")throw l;Se.writeFileSync(o,t,{flag:"wx"})}}finally{try{Se.unlinkSync(a)}catch{}}return{name:r,file:o}}catch(a){if(a.code!=="EEXIST")throw a}}throw new Error("could not find a free catch-up file name")}readCatchup(e){if(!/^\d{4}-\d{2}-\d{2}T\d{4}(?:-\d+)?$/.test(String(e||"")))return null;try{return{name:e,markdown:Se.readFileSync(ge.join(this.p.catchups,e+".md"),"utf8")}}catch{return null}}listCatchups(){let e=[];try{e=Se.readdirSync(this.p.catchups).filter(i=>/^\d{4}-\d{2}-\d{2}T\d{4}(?:-\d+)?\.md$/.test(i))}catch{return[]}let t=[];for(let i of e){let r={};try{let o=Se.openSync(ge.join(this.p.catchups,i),"r");try{let a=Buffer.alloc(8192),l=Se.readSync(o,a,0,a.length,0);r=Tt(a.slice(0,l).toString("utf8")).data||{}}finally{Se.closeSync(o)}}catch{}t.push({name:i.slice(0,-3),generatedAt:r.generated_at?Z(r.generated_at):null,from:r.window_from?Z(r.window_from):null,to:r.window_to?Z(r.window_to):null,label:r.window_label||null,sessions:typeof r.sessions=="number"?r.sessions:null,needsYou:Array.isArray(r.needs_you)?r.needs_you.length:0,repos:r.repos==="all"||r.repos==null?"all":[].concat(r.repos).map(String),machine:r.made_on==null?null:String(r.made_on),model:r.model==null?null:String(r.model)})}let s=i=>Date.parse(i.generatedAt||"")||0;return t.sort((i,r)=>s(r)-s(i)||r.name.localeCompare(i.name))}lastCatchupAt(){let e=this.listCatchups()[0];if(!e)return null;let t=Date.parse(e.to||e.generatedAt||"");return isNaN(t)?null:t}readSpool(){let e=[];for(let t of Mi(this.mySpool)){let s=ge.join(this.mySpool,t),i=[];try{i=Se.readdirSync(s)}catch{}for(let r of i){if(!r.endsWith(".json"))continue;let o=ge.join(s,r),a=null;try{a=Se.readFileSync(o,"utf8")}catch{continue}e.push({file:o,t:Number(r.split("-")[0])||0,body:a})}}return e.sort((t,s)=>t.t-s.t||t.file.localeCompare(s.file))}spoolCount(){let e=0;for(let t of Mi(this.mySpool))try{e+=Se.readdirSync(ge.join(this.mySpool,t)).filter(s=>s.endsWith(".json")).length}catch{}return e}}});import _l from"node:fs";import sU from"node:os";import Cn from"node:path";import{execFileSync as iU}from"node:child_process";function Wp(n){let e=new Set;for(let t of n||[])for(let s of[t&&t.cwd,t&&t.repo&&t.repo.root])typeof s=="string"&&Cn.isAbsolute(s)&&e.add(Cn.resolve(s));return[...e]}function lU(n){let e=Cn.join(n,"null"),t=Cn.join(e,".spool");if(!od(e)||!od(t))return null;let s={dir:n,nullDir:e,files:[],foreign:null},i=r=>(s.foreign=s.foreign||r,s);for(let r of Al(e)||[])if(r!==".spool")return i(Cn.join(e,r));for(let r of Al(t)||[]){let o=Cn.join(t,r);if(!od(o))return i(o);for(let a of Al(o)||[]){let l=Cn.join(o,a);if(!rU.test(a)||!od(l))return i(l);for(let c of Al(l)||[]){let u=Cn.join(l,c);if(!oU.test(c)||!aU(u))return i(u);s.files.push(u)}}}return s.files.sort((r,o)=>Number(Cn.basename(r).split("-")[0])-Number(Cn.basename(o).split("-")[0])||r.localeCompare(o)),s}function cU(n){try{return String(iU("git",["-C",n,"ls-files","--","null/"],{encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","pipe"]})).split(`
|
|
206
|
+
`).filter(Boolean)}catch(e){return/not a git repository/i.test(String(e.stderr||""))?[]:null}}function uU(n){try{let e=JSON.parse(_l.readFileSync(n,"utf8"));if(e&&Array.isArray(e.events))return e.events.filter(t=>t&&typeof t=="object");if(e&&e.type&&e.sessionId)return[e]}catch{}return null}function dU(n){let e=t=>{for(let i of Al(t)||[]){let r=Cn.join(t,i);od(r)&&e(r)}let s=Al(t);if(s&&!s.length)try{_l.rmdirSync(t)}catch{}};e(n)}function Yp({dirs:n,ingest:e,lsFiles:t=cU,dryRun:s=!1,log:i=()=>{}}){let r={found:0,cleaned:0,events:0,files:0,pending:[],tracked:[],foreign:[],unchecked:[]};for(let o of n||[]){let a=lU(o);if(!a)continue;if(r.found++,a.foreign){r.foreign.push({dir:o,path:a.foreign});continue}let l=t(o);if(l===null){r.unchecked.push(o);continue}if(l.length){r.tracked.push(o);continue}if(s){r.pending.push({dir:o,files:a.files.length});continue}let c=0;for(let u of a.files){let d=uU(u);try{d&&d.length&&(r.events+=e(d)||0),_l.unlinkSync(u),r.files++}catch(h){c++,i("stray spool file kept: "+u+" "+h.message)}}dU(a.nullDir),c||r.cleaned++}return r}function GE(n){let e=[];if(!n)return e;n.files&&e.push("✓ removed "+n.files+" stray event file"+(n.files===1?"":"s")+" an older mod left in null/ ("+n.cleaned+" folder"+(n.cleaned===1?"":"s")+")");for(let t of n.pending||[])e.push("· an older mod left a null/ folder in "+Kp(t.dir)+" ("+t.files+" file"+(t.files===1?"":"s")+"); the daemon cleans it up when it starts");for(let t of n.tracked||[])e.push("! an older mod's null/ folder is committed in "+Kp(t)+": git rm -r null/");for(let t of n.foreign||[])e.push("· left "+Kp(Cn.join(t.dir,"null"))+" alone: it holds more than old Leftoff spool files");for(let t of n.unchecked||[])e.push("· left "+Kp(Cn.join(t,"null"))+" alone: couldn't ask git whether it is tracked");return e}var rU,oU,od,aU,Al,Kp,Rk=K(()=>{rU=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,oU=/^\d+-[0-9a-f]{8}\.json$/;od=n=>{try{return _l.lstatSync(n).isDirectory()}catch{return!1}},aU=n=>{try{return _l.lstatSync(n).isFile()}catch{return!1}},Al=n=>{try{return _l.readdirSync(n)}catch{return null}};Kp=n=>{let e=sU.homedir();return e&&(n===e||n.startsWith(e+Cn.sep))?"~"+n.slice(e.length):n}});var Dk=K(()=>{});function $E(n){let e={indent:-1,children:[]},t=[e],s=null;for(let r of String(n||"").split(/\r?\n/)){let o=r.match(/^(\s*)[-*+]\s+(.*)$/);if(o){let a=o[1].replace(/\t/g," ").length;for(;t.length>1&&a<=t[t.length-1].indent;)t.pop();s={indent:a,text:o[2].trim(),children:[]},t[t.length-1].children.push(s),t.push(s)}else if(s&&/^\s+\S/.test(r))s.text+=" "+r.trim();else if(r.trim())s=null,t.length=1;else continue}let i=r=>r.map(({text:o,children:a})=>({text:o,children:i(a)}));return i(e.children)}var qp=K(()=>{});var hU,Fk,$V,KV,Uk=K(()=>{hU="★✶✦✩☆⭐",Fk="─━═\\-",$V=new RegExp("^(`*)\\s*(["+hU+"])\\uFE0F?\\s+([^`"+Fk.replace("\\-","")+"]{1,40}?)\\s*(["+Fk+"]{3,})?\\s*\\1$"),KV=new RegExp("^`*\\s*["+Fk+"]{3,}\\s*`*$")});var Hk=K(()=>{});var Jp=K(()=>{});var WE=K(()=>{Jp()});var YE=K(()=>{});var qE=K(()=>{YE()});function mU(n){let e=n.match(/limit reached\|(\d{9,13})/i);if(e){let s=new Date(Number(e[1])*(e[1].length>11?1:1e3));if(!Number.isNaN(s.getTime()))return"resets "+s.toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})}let t=n.match(/\bresets?\s+(?:at\s+)?([^·|\n.]{1,40})/i);return t?"resets "+JE(t[1]).replace(/\s*\(.*$/,""):""}function qo(n,{subtype:e=null,code:t=null,signal:s=null,endReason:i=null}={}){let r=JE(n).slice(0,pU),o=l=>({label:l,raw:r||null});if(r&&/^\[?Request interrupted by user/i.test(r)||!r&&i==="aborted")return null;if(/usage limit|hit your (usage )?limit|limit reached|out of (extra )?usage|weekly limit|session limit/i.test(r)){let l=mU(r);return o("Claude’s usage limit is reached"+(l?" · "+l:""))}if(/rate[ _-]?limit|\b429\b|too many requests/i.test(r))return o("Claude is rate limited right now · try again in a minute");if(/prompt is too long|context (length|window)|too many tokens|maximum context|input is too long/i.test(r))return o("The conversation is too long for Claude · run /compact in its terminal, then retry");if(/overloaded|\b529\b/i.test(r))return o("Claude’s servers are overloaded · try again in a minute");if(/credit balance|billing|payment required|\b402\b/i.test(r))return o("Claude’s account is out of credit");if(/invalid api key|run \/login|please (log|sign) ?in|not logged in|oauth token|token (has )?expired|authentication|\b401\b|\b403\b|unauthori[sz]ed|forbidden/i.test(r))return o(VE);if(/timed? ?out|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed|network|socket hang up|connection (error|refused|reset)/i.test(r))return o("Claude couldn’t reach its servers · check this Mac’s connection");if(/no conversation found|session .*not found/i.test(r))return o("Claude Code couldn’t reopen this conversation");let a=r.match(/API Error:?\s*(\d{3})?/i);return a?o("Claude’s API returned an error"+(a[1]?" ("+a[1]+")":"")):e==="error_max_turns"?o("Claude stopped at its turn limit"):i==="refusal"?o("Claude declined to answer this one"):t!=null||s?o("Claude Code exited"+(t!=null?" (code "+t+")":" ("+s+")")+" before answering"):r?o("Claude Code ended the turn with an error"):i==="error"?o("Claude Code ended the turn with an error (it wrote no details)"):null}function Vp(n=0){return{phase:"sent",at:n,last:n,tool:null,waiting:null,ran:[],endedAt:null}}function Ge(n,e={}){let t={...n||Vp(e.at||0),ran:[...n&&n.ran||[]]},s=e.at||t.last;if(fU.has(t.phase)&&!(t.phase==="replied"&&(e.type==="tool_done"||e.type==="why")))return t.queued&&(delete t.queued,delete t.cancellable,delete t.sendNow),t;switch(t.queued&&!gU.has(e.type)&&(delete t.queued,delete t.cancellable,delete t.sendNow),e.type){case"queued":if(t.phase==="sent"||t.phase==="received")t.queued=!0,t.cancellable=!!e.cancellable,e.cancellable&&e.sendNow?t.sendNow=!0:delete t.sendNow;else return t;break;case"unqueued":break;case"delivered":t.phase==="sent"&&(t.phase="received");break;case"failed":t.phase==="sent"&&(t.phase="failed",t.error=e.error||null,t.endedAt=s);break;case"started":case"text":t.phase!=="replied"&&(t.phase="working"),e.type==="text"&&(t.waiting=null);break;case"tool":t.phase="working",t.tool={name:Tl(e.tool,40),detail:Tl(e.detail,300)},t.waiting=null;break;case"tool_done":{let i=Tl(e.tool||t.tool&&t.tool.name,40);i&&t.ran.push({tool:i,detail:Tl(e.detail||t.tool&&t.tool.detail,300),asked:!!e.asked,...typeof e.id=="string"&&e.id&&e.id.length<=120?{id:e.id}:{}}),t.ran.length>20&&(t.ran=t.ran.slice(-20)),t.tool=null,t.waiting=null;break}case"ask":t.phase="working",t.waiting={tool:Tl(e.tool,40),detail:Tl(e.detail,300)};break;case"ask_done":t.waiting=null;break;case"reply":t.phase="replied",t.tool=null,t.waiting=null,t.endedAt=s;break;case"ended":t.tool=null,t.waiting=null,t.endedAt=s,t.phase=e.text?"replied":"stopped",e.why&&(t.why=e.why);break;case"why":if(e.why&&!(t.why&&t.why.raw&&!e.why.raw))t.why=e.why;else return t;break;case"folded":(t.phase==="sent"||t.phase==="received")&&(t.phase="working"),t.folded=e.owner||!0,t.tool=null,t.waiting=null;break;case"answered":t.phase="replied",t.folded=e.owner||t.folded||!0,t.tool=null,t.waiting=null,t.endedAt=s;break;case"lost":t.tool=null,t.waiting=null,t.endedAt=s,t.phase="stopped",t.lost=!0,e.why&&(t.why=e.why);break;default:return t}return t.last=s,t}function Xp(n){let e=new Set,t=[];for(let s of n&&n.ran||[])s.asked||e.has(s.tool)||(e.add(s.tool),t.push(s.tool));return t}var fU,Tl,pU,JE,VE,XE,gU,Zp=K(()=>{fU=new Set(["replied","failed"]),Tl=(n,e)=>{let t=String(n??"").replace(/\s+/g," ").trim();return t.length>e?t.slice(0,e-1)+"…":t},pU=600,JE=n=>String(n||"").replace(/\s+/g," ").trim();VE="Claude Code isn’t signed in · run /login in a terminal on this Mac",XE=n=>!!n&&n.label===VE;gU=new Set(["delivered","queued","ask_done","noop","why"])});var ZE=K(()=>{});function zk(n){return String(n||"").split(/[\\/]+/).filter(Boolean)}function QE(n){let e=zk(n);return e.length?e[e.length-1]:""}function ad(n){if(!n)return"no-repo";if(n.repoKey)return n.repoKey;let e=n.repo||{};return n.repoRoot||e.root||n.cwd||e.name||n.repoName||"no-repo"}function El(n,e=null){let t=String(n||"").replace(/\/+$/,"");return!t||t==="~"||n==="/"||/^[A-Za-z]:\\?$/.test(t)||e&&t===String(e).replace(/\/+$/,"")?!0:/^\/(Users|home)\/[^/]+$/.test(t)||t==="/root"||t==="/var/root"}function xs(n,{names:e=null}={}){let t=[...new Set([...n].filter(Boolean))],s=new Map(t.map(a=>[a,1])),i=a=>e&&e.get(a)||null,r=new Map;for(let a of t){let l=i(a);l&&r.set(l,(r.get(l)||0)+1)}let o=a=>{if(a==="no-repo"||El(a))return Qp;let l=i(a);if(l&&r.get(l)===1&&s.get(a)===1&&!t.some(u=>u!==a&&!i(u)&&QE(u)===l))return l;let c=zk(a);return c.length?c.slice(-Math.min(s.get(a),c.length)).join("/"):a};for(let a=0;a<12;a++){let l=new Map;for(let u of t){let d=o(u);l.set(d,[...l.get(d)||[],u])}let c=!1;for(let u of l.values())if(!(u.length<2))for(let d of u)s.get(d)<zk(d).length&&(s.set(d,s.get(d)+1),c=!0);if(!c)break}return new Map(t.map(a=>[a,o(a)]))}function em(n){let e=String(n&&n.repoLabel||"").trim(),t=String(n&&n.repoName||"").trim();return!e&&!t?{name:Qp,sub:null}:e===Qp||t==="no-repo"||n&&n.repoKey==="no-repo"?{name:Qp,sub:null}:t?!e||e===t?{name:t,sub:null}:{name:t,sub:e}:{name:e,sub:null}}function Gk(n,{countIn:e}={}){let t=new Map;for(let i of n){let r=ad(i),o=t.get(r)||{key:r,label:i.repoLabel||null,count:0,total:0,last:0};o.total++,o.last=Math.max(o.last,yU(i)),!o.label&&i.repoLabel&&(o.label=i.repoLabel),t.set(r,o)}if(e)for(let i of e){let r=t.get(ad(i));r&&r.count++}else for(let i of t.values())i.count=i.total;let s=xs(t.keys());for(let i of t.values())i.label||(i.label=s.get(i.key));return[...t.values()].sort((i,r)=>r.last-i.last||i.label.localeCompare(r.label))}function Bn(n,e,{loose:t=!1}={}){let s=String(e||"").trim().toLowerCase().replace(/\/+$/,"");if(!s)return!1;let i=ad(n),r=[i,n.repoLabel,n.repoName,QE(i)].filter(Boolean).map(o=>String(o).toLowerCase());return r.includes(s)||s.includes("/")&&String(i).toLowerCase().endsWith("/"+s)||t&&r.some(o=>o.includes(s))?!0:Array.isArray(n.alsoRepos)&&n.alsoRepos.some(o=>o&&o.key&&Bn({repoKey:o.key,repoLabel:o.label},e,{loose:t}))}function tm(n,e){if(!e)return n;let t=n.filter(s=>Bn(s,e));return t.length?t:n.filter(s=>Bn(s,e,{loose:!0}))}function ld(n,e,t=[]){let s=String(e||"").toLowerCase().trim().split(/\s+/).filter(Boolean);if(!s.length)return!0;let i=n.lastCheckpoint||null,r=[n.title,n.repoLabel,n.repoName,n.repoProject&&n.repoProject.name,n.repo&&n.repo.branch,n.repo&&n.repo.github,n.worktree&&n.worktree.name,n.worktree&&n.worktree.branch,n.cwd,n.lastPreview,n.firstPrompt,n.machine,i&&i.doing,i&&i.next,i&&i.stoppedAt,...t].filter(Boolean).map(l=>String(l).toLowerCase()),o=String(n.id||"").toLowerCase(),a=String(n.shortId||"").toLowerCase();return s.every(l=>{let c=l.replace(/^cp:/,"");return c.length>=4&&(o.startsWith(c)||a&&a===c)?!0:l.startsWith("cp:")?!1:r.some(u=>u.includes(l))})}var Qp,Bk,yU,Ws=K(()=>{Qp="No repo";Bk=n=>{let e=Date.parse(n||"");return isNaN(e)?0:e},yU=n=>Math.max(Bk(n.lastActivityAt),Bk(n.endedAt),Bk(n.startedAt))});var cd=K(()=>{Hn();Jp()});var eC=K(()=>{Zp();ZE();Hn();Ws();cd()});var $k=K(()=>{cd();Jp()});var Kk=K(()=>{});function ud(n){let s=(String(n||"").replace(/\s+/g," ").trim().split(/(?<=[.!?])\s+(?=\S)/).filter(r=>/\?\s*$/.test(r)).pop()||"").replace(/[*_`]+/g,"").match(/^(.*?)(?::|\s[—–-]\s)\s*(.+?)\s*\?\s*$/);if(!s||!/\bor\b/i.test(s[2]))return[];let i=s[2].split(/\s*,\s*(?:or\s+|and\s+)?|\s+or\s+/i).map(r=>r.trim().replace(/[.;:]$/,"")).filter(Boolean);return i.length<2||i.length>6||i.some(r=>r.length>60)||/^(not|no|neither|something else|anything else)\b/i.test(i[i.length-1])?[]:i.map(NU)}var NU,Wk=K(()=>{Dk();qp();Uk();Hk();WE();qE();eC();cd();$k();Kk();NU=n=>n.charAt(0).toUpperCase()+n.slice(1)});import IU from"node:path";function tC(n){let e=[],t=!1;for(let s of Yk(n).split(`
|
|
207
207
|
`)){if(/^\s*(```|~~~)/.test(s)){t=!t;continue}t||e.push(s)}return e.join(`
|
|
208
|
-
`)}function
|
|
208
|
+
`)}function LU(n,e){let t=Yk(n).trimEnd(),s=Jo(e).replace(/[*_`]+/g,"");if(!s)return t;let i=s.split(`
|
|
209
209
|
`)[0].slice(0,40).toLowerCase(),r=[],o="";for(let c=0;c<t.length;c++){let u=t[c];if(!/[*_`]/.test(u)){if(/\s/.test(u)){if(o.endsWith(" ")||!o)continue;o+=" ",r.push(c);continue}o+=u.toLowerCase(),r.push(c)}}let a=o.lastIndexOf(i);if(a>=0)return t.slice(0,r[a]).trimEnd();let l=t.split(/\n\s*\n/);return/[??][\s*_`)"'”’]*$/.test(l[l.length-1]||"")&&l.pop(),l.join(`
|
|
210
210
|
|
|
211
|
-
`).trimEnd()}function nC(n=[],{question:e=null,max:t=3,width:s=280}={}){let i=[],r=[],o=!0;for(let a=n.length-1;a>=0&&i.length<t;a--){let l=n[a];if(
|
|
211
|
+
`).trimEnd()}function nC(n=[],{question:e=null,max:t=3,width:s=280}={}){let i=[],r=[],o=!0;for(let a=n.length-1;a>=0&&i.length<t;a--){let l=n[a];if(jU(l))break;if(!l||l.role!=="assistant"||!l.text||!l.text.trim())continue;let c=l.text;if(o){let d=e?ud(e):[],h=e?LU(c,e):c;r=d.length?d:!e||qk.test(e)?Jk(h,{trailing:!!e}):[],e&&(c=h),o=!1}let u=tC(c).split(`
|
|
212
212
|
`).map(d=>d.trim()).filter(d=>d&&!/^#{1,6}\s*$/.test(d)&&!/^[-*_]{3,}$/.test(d));for(let d=u.length-1;d>=0&&i.length<t;d--)i.unshift(Ys(u[d].replace(/^#{1,6}\s+/,""),s))}return!i.length&&!r.length?null:{lines:i,options:r}}function Jk(n,{trailing:e=!1}={}){let t=tC(n).split(`
|
|
213
|
-
`).map(l=>l.trim()).filter(Boolean),s=[],i=[];for(let l of t){let c=l.match(/^(?:\*\*)?(?:option\s+)?([0-9]{1,2}|[a-fA-F])[.):](?:\*\*)?\s+(.+)$/i);if(c)s.push({key:c[1],text:c[2]});else{if(s.length&&e&&/^[-*+•]\s/.test(l))continue;s.length&&(i.splice(0,i.length,...s),s=[])}}if(e&&!s.length)return[];let r=s.length?s:i;if(r.length<2||r.length>6)return[];let o=r.map(l=>l.key.toLowerCase());return o.every((l,c)=>/\d/.test(l)?Number(l)===Number(o[0])+c:l.charCodeAt(0)===o[0].charCodeAt(0)+c)?r.map(l=>Ys(l.key+". "+l.text.replace(/\*\*/g,"").replace(/\s+[—–-]\s+.*$/,"").replace(/[:.]$/,"").trim(),60)):[]}function Vk(n={},{cwd:e}={}){let t=n.input||{},s=String(n.tool||"tool"),i=
|
|
213
|
+
`).map(l=>l.trim()).filter(Boolean),s=[],i=[];for(let l of t){let c=l.match(/^(?:\*\*)?(?:option\s+)?([0-9]{1,2}|[a-fA-F])[.):](?:\*\*)?\s+(.+)$/i);if(c)s.push({key:c[1],text:c[2]});else{if(s.length&&e&&/^[-*+•]\s/.test(l))continue;s.length&&(i.splice(0,i.length,...s),s=[])}}if(e&&!s.length)return[];let r=s.length?s:i;if(r.length<2||r.length>6)return[];let o=r.map(l=>l.key.toLowerCase());return o.every((l,c)=>/\d/.test(l)?Number(l)===Number(o[0])+c:l.charCodeAt(0)===o[0].charCodeAt(0)+c)?r.map(l=>Ys(l.key+". "+l.text.replace(/\*\*/g,"").replace(/\s+[—–-]\s+.*$/,"").replace(/[:.]$/,"").trim(),60)):[]}function Vk(n={},{cwd:e}={}){let t=n.input||{},s=String(n.tool||"tool"),i=PU(t.file_path||t.notebook_path||t.path,e);switch(s){case"Edit":case"MultiEdit":case"NotebookEdit":return"Edited "+(i||"a file");case"Write":return"Wrote "+(i||"a file");case"Read":return"Read "+(i||"a file");case"Bash":{let r=Jo(t.command);return RU.test(r)?"Ran tests":/^git\s+commit\b/.test(r)?"Committed":/^git\s+push\b/.test(r)?"Pushed":"Ran "+Ys(t.description?Jo(t.description):r||"a command",48)}case"Grep":return"Searched “"+Ys(Jo(t.pattern),32)+"”";case"Glob":return"Listed "+Ys(Jo(t.pattern),32);case"LS":return"Listed "+(i||"a folder");case"WebFetch":{let r="";try{r=new URL(t.url).hostname}catch{}return"Fetched "+(r||"a page")}case"WebSearch":return"Searched the web for “"+Ys(Jo(t.query),32)+"”";case"TodoWrite":return"Updated the to-do list";case"Task":case"Agent":return"Ran an agent"+(t.description?": "+Ys(Jo(t.description),36):"");case"AskUserQuestion":return"Asked you";case"ExitPlanMode":return"Proposed a plan";default:return"Used "+Ys(s.replace(/^mcp__/,"").replace(/__/g," "),40)}}function Vo(n=[],{limit:e=20,cwd:t=null,textMax:s=6e3}={}){let i=[];for(let r of n){if(!r)continue;if(r.role==="user"){if(!r.text||!r.text.trim())continue;i.push({role:"user",text:Ys(r.text.trim(),s),chips:[]});continue}let o=(r.text||"").trim(),a=(r.toolUses||[]).map(c=>({label:Vk(c,{cwd:t}),error:!!c.isError})),l=i[i.length-1];(!l||l.role!=="assistant"||o&&l.chips.length)&&(l={role:"assistant",text:"",chips:[]},i.push(l)),o&&(l.text=Ys(l.text?l.text+`
|
|
214
214
|
|
|
215
|
-
`+o:o,s));for(let c of a){let u=l.chips[l.chips.length-1];u&&u.label===c.label&&u.error===c.error?u.n=(u.n||1)+1:l.chips.push(c)}}return i.slice(-Math.max(1,e))}var Ys,Yk,Jo,qk,
|
|
216
|
-
`),Jo=n=>Yk(n).replace(/\s+/g," ").trim();qk=/\b(which|or|options?|prefer|pick|choose|one of)\b/i,
|
|
215
|
+
`+o:o,s));for(let c of a){let u=l.chips[l.chips.length-1];u&&u.label===c.label&&u.error===c.error?u.n=(u.n||1)+1:l.chips.push(c)}}return i.slice(-Math.max(1,e))}var Ys,Yk,Jo,qk,jU,PU,RU,dd=K(()=>{Wk();Ys=(n,e)=>(n=String(n??""),n.length>e?n.slice(0,e-1).trimEnd()+"…":n),Yk=n=>String(n??"").replace(/\r\n?/g,`
|
|
216
|
+
`),Jo=n=>Yk(n).replace(/\s+/g," ").trim();qk=/\b(which|or|options?|prefer|pick|choose|one of)\b/i,jU=n=>n&&n.role==="user"&&n.text&&n.text.trim();PU=(n,e)=>{let t=String(n||"");return t?e&&t.startsWith(String(e).replace(/\/$/,"")+"/")?t.slice(String(e).replace(/\/$/,"").length+1):t.includes("/")?IU.basename(t):t:""},RU=/(^|[\s;&|(])((p?npm|yarn|pnpm|bun)\s+(run\s+)?test\b|node\s+--test\b|(npx\s+)?(jest|vitest|mocha|pytest|playwright\s+test)\b|go\s+test\b|cargo\s+test\b|rspec\b|make\s+test\b|swift\s+test\b|xcodebuild\s+test\b|node\s+test\/)/});function sC(n,e,{start:t=1,context:s=3,maxLines:i=2e3}={}){let r=String(n??"").split(`
|
|
217
217
|
`),o=String(e??"").split(`
|
|
218
218
|
`);if(r.length>i||o.length>i)return null;r.length>1&&o.length>1&&r[r.length-1]===""&&o[o.length-1]===""&&(r.pop(),o.pop());let a=0;for(;a<r.length&&a<o.length&&r[a]===o[a];)a++;let l=0;for(;l<r.length-a&&l<o.length-a&&r[r.length-1-l]===o[o.length-1-l];)l++;let c=r.slice(a,r.length-l),u=o.slice(a,o.length-l),d=c.length,h=u.length,f=[];for(let v=0;v<a;v++)f.push([" ",r[v]]);if(d*h>4e6){for(let v of c)f.push(["-",v]);for(let v of u)f.push(["+",v])}else{let v=h+1,x=new Uint32Array((d+1)*v);for(let T=d-1;T>=0;T--)for(let g=h-1;g>=0;g--)x[T*v+g]=c[T]===u[g]?x[(T+1)*v+g+1]+1:Math.max(x[(T+1)*v+g],x[T*v+g+1]);let E=0,A=0;for(;E<d||A<h;)E<d&&A<h&&c[E]===u[A]?(f.push([" ",c[E]]),E++,A++):A<h&&(E>=d||x[E*v+A+1]>=x[(E+1)*v+A])?(f.push(["+",u[A]]),A++):(f.push(["-",c[E]]),E++)}for(let v=r.length-l;v<r.length;v++)f.push([" ",r[v]]);for(let v=0;v<f.length;v++){if(f[v][0]===" ")continue;let x=v;for(;x<f.length&&f[x][0]!==" ";)x++;let E=f.slice(v,x),A=[...E.filter(T=>T[0]==="-"),...E.filter(T=>T[0]==="+")];f.splice(v,x-v,...A),v=x-1}let p=f.map((v,x)=>v[0]!==" "?x:-1).filter(v=>v>=0);if(!p.length)return"";let m=[];for(let v of p){let x=Math.max(0,v-s),E=Math.min(f.length-1,v+s),A=m[m.length-1];A&&x<=A[1]+1?A[1]=E:m.push([x,E])}let y="",w=t,S=t,k=0;for(let[v,x]of m){for(;k<v;k++)f[k][0]!=="+"&&w++,f[k][0]!=="-"&&S++;let E=0,A=0,T="";for(let g=v;g<=x;g++){let[b,C]=f[g];b!=="+"&&E++,b!=="-"&&A++,T+=b+C+`
|
|
219
219
|
`}for(y+="@@ -"+(E?w:w-1)+","+E+" +"+(A?S:S-1)+","+A+` @@
|
|
220
|
-
`+T;k<=x;k++)f[k][0]!=="+"&&w++,f[k][0]!=="-"&&S++}return y}var iC=K(()=>{});import
|
|
220
|
+
`+T;k<=x;k++)f[k][0]!=="+"&&w++,f[k][0]!=="-"&&S++}return y}var iC=K(()=>{});import DU from"node:path";function Ni(n){let e=DU.posix.basename(String(n||"").replace(/\\/g,"/").replace(/\/+$/,""));return e?!!(/^\.env(\..*)?$/i.test(e)||/\.(pem|key|p12|pfx)$/i.test(e)||/^id_[a-z0-9_-]+(\.pub)?$/i.test(e)):!1}function oC(n){for(let e of String(n||"").split(/[\s'"`;|&<>()=]+/))if(e&&Ni(e))return!0;return!1}function UU(n){return n.replace(/^([^\s:]+?)([:-]\d+[:-]|:)(.*)$/gm,(e,t,s,i)=>i&&Ni(t)?t+s+" "+cn:e)}function Vt(n,e=[]){let t=String(n??"");if(!t)return t;t=UU(t);for(let[s,i]of FU)t=t.replace(s,i);return e&&e.length&&(t=is(t,e)),t}function aC(n){return String(n??"").replace(/\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)|\u001b[@-Z\\-_]/g,"").replace(/\r(?!\n)/g,`
|
|
221
221
|
`)}function Cl(n){let e=String(n||"").slice(0,4096);if(!e)return!1;if(e.includes("\0"))return!0;let t=0;for(let s=0;s<e.length;s++){let i=e.charCodeAt(s);(i<32&&i!==9&&i!==10&&i!==13&&i!==27||i===65533)&&t++}return t/e.length>.1}function lC(n,e){if(n=String(n??""),n.length<=e)return{text:n,clipped:!1,cut:0};let t=Math.floor(e/4),s=e-t,i=n.slice(0,t),r=i.lastIndexOf(`
|
|
222
222
|
`);r>t/2&&(i=i.slice(0,r+1));let o=n.slice(n.length-s),a=o.indexOf(`
|
|
223
223
|
`);a>=0&&a<s/2&&(o=o.slice(a+1));let l=n.length-i.length-o.length,c=n.slice(i.length,n.length-o.length).split(`
|
|
224
224
|
`).length-1;return{text:i+(i.endsWith(`
|
|
225
225
|
`)?"":`
|
|
226
226
|
`)+"… "+(c>0?c+" lines":l+" characters")+` cut …
|
|
227
|
-
`+o,clipped:!0,cut:l}}var cn,rC,
|
|
227
|
+
`+o,clipped:!0,cut:l}}var cn,rC,FU,hd=K(()=>{Te();cn="[REDACTED]";rC="(?:[A-Za-z_][A-Za-z0-9_.-]*?)?(?:SECRET|TOKEN|PASSWORD|PASSWD|PASSPHRASE|API_?KEY|ACCESS_?KEY|PRIVATE_?KEY|CREDENTIALS?|AUTH)[A-Za-z0-9_]*",FU=[[/-----BEGIN [A-Z0-9 ]*PRIVATE KEY( BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY( BLOCK)?-----|$)/g,"-----BEGIN PRIVATE KEY----- "+cn+" -----END PRIVATE KEY-----"],[new RegExp("^([+\\- ]?[ \\t]*(?:export[ \\t]+)?"+rC+"[ \\t]*=[ \\t]*)(?![^\\n]*\\()([^\\s{\\[].*)$","gim"),"$1"+cn],[new RegExp("^([+\\- ]?[ \\t]*"+rC+":[ \\t]+)([A-Za-z0-9_+/=-]{6,})[ \\t]*$","gim"),"$1"+cn],[/((?:secret|token|passw(?:or)?d|passphrase|api[_-]?key|access[_-]?key|private[_-]?key|credential)[A-Za-z0-9_-]*["']?[ \t]*[:=][ \t]*)(["'])([^"'\n]{6,})\2/gi,"$1$2"+cn+"$2"],[/\b(Bearer|Basic|token)[ \t]+[A-Za-z0-9._~+/-]{16,}=*/g,"$1 "+cn],[/([a-z][a-z0-9+.-]*:\/\/[^\s:@/]+:)[^\s@/]+@/gi,"$1"+cn+"@"],[/(_auth(?:Token)?[ \t]*=[ \t]*)\S+/g,"$1"+cn],[/\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{22,})\b/g,cn],[/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g,cn],[/\bxox[abposr]-[A-Za-z0-9-]{10,}/g,cn],[/\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/g,cn],[/\bsk-[A-Za-z0-9_-]{20,}/g,cn],[/\bAIza[0-9A-Za-z_-]{35}\b/g,cn],[/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,cn]]});function GU(n,e){let t=String(n||"");if(!t)return"";let s=String(e||"").replace(/\/+$/,"");return s&&t.startsWith(s+"/")?t.slice(s.length+1):t}function Xk(n){let e=n&&n.input||{};return e.file_path||e.notebook_path||(n.tool==="Read"||n.tool==="LS"?e.path:null)||null}function $U(n={},{cwd:e=null,patterns:t=[]}={}){let s=n&&n.input&&typeof n.input=="object"?n.input:{},i="";return n.tool==="Bash"?i=s.command:Xk(n)?i=GU(Xk(n),e):i=s.pattern||s.path||s.url||s.query||s.description||"",Vt(zU(i).slice(0,300),t)}function KU(n={}){let e=Xk(n);return e&&Ni(e)||n.tool==="Bash"&&oC(n.input&&n.input.command)?"secret_file":null}function WU(n,e){let t=String(n??"").split(`
|
|
228
228
|
`),s=t.findIndex(r=>r.trim().length>=3);if(s<0)return null;let i=t[s];for(let r of String(e||"").split(`
|
|
229
|
-
`)){let o=/^\s*(\d+)[→\t](.*)$/.exec(r);if(o&&o[2].includes(i.trim())&&(o[2]===i||o[2].endsWith(i)||o[2].startsWith(i)))return Math.max(1,+o[1]-s)}return null}function
|
|
229
|
+
`)){let o=/^\s*(\d+)[→\t](.*)$/.exec(r);if(o&&o[2].includes(i.trim())&&(o[2]===i||o[2].endsWith(i)||o[2].startsWith(i)))return Math.max(1,+o[1]-s)}return null}function YU(n={},e=Zk){let t=n&&n.input||{},s=n.tool==="Edit"?[t]:n.tool==="MultiEdit"&&Array.isArray(t.edits)?t.edits:null;if(!s||!s.length||s.length>50)return null;let i=0;for(let a of s){if(!a||typeof a.old_string!="string"||typeof a.new_string!="string")return null;i+=a.old_string.length+a.new_string.length}if(i>uC)return null;let r=s.length===1&&!t.replace_all&&!n.isError?WU(s[0].new_string,n.text):null,o=s.map(a=>sC(a.old_string,a.new_string,{start:r||1})||"").join("");return!o||o.length>e?null:{patch:o,patchAt:r}}function dC(n={},{cwd:e=null,patterns:t=[],max:s=Zk,ts:i=null}={}){let r={id:n.tool_use_id||null,tool:String(n.tool||"tool"),label:Vk(n,{cwd:e}),what:$U(n,{cwd:e,patterns:t}),error:!!n.isError,ts:i||null,running:n.text===void 0,output:null,chars:0,clipped:!1,hidden:null};if(r.running)return r;let o=KU(n);if(o)return r.hidden=o,r;let a=n.isError?null:YU(n,s);a&&(r.patch=Vt(a.patch,t),r.patchAt=a.patchAt);let l=aC(n.text==null?"":String(n.text));if(r.chars=l.length,Cl(l))return r.hidden="binary",r;l=Vt(l,t);let c=lC(l,s);return r.output=c.text,r.clipped=c.clipped,r}function Qk(n=[],{limit:e=cC,cwd:t=null,patterns:s=[]}={}){let i=Math.max(1,Math.min(HU,Number(e)||cC)),r=[];for(let o=n.length-1;o>=0&&r.length<i;o--){let a=n[o];if(!(!a||a.role!=="assistant"||!Array.isArray(a.toolUses)))for(let l=a.toolUses.length-1;l>=0&&r.length<i;l--){let c=a.toolUses[l];!c||BU.has(c.tool)||r.push(dC(c,{cwd:t,patterns:s,max:Zk,ts:a.ts||null}))}}return r}function hC(n=[],e,{cwd:t=null,patterns:s=[]}={}){if(!e)return null;for(let i=n.length-1;i>=0;i--){let r=n[i];if(!r||r.role!=="assistant"||!Array.isArray(r.toolUses))continue;let o=r.toolUses.find(a=>a&&a.tool_use_id===e);if(o)return dC(o,{cwd:t,patterns:s,max:uC,ts:r.ts||null})}return null}var Zk,uC,cC,HU,BU,zU,fC=K(()=>{dd();iC();hd();Zk=8*1024,uC=64*1024,cC=5,HU=20,BU=new Set(["TodoWrite"]),zU=n=>String(n??"").replace(/\s+/g," ").trim()});import{execFile as qU}from"node:child_process";import eS from"node:fs";import pC from"node:path";function nH(n=process.env){let e={};for(let[t,s]of Object.entries(n))tH.test(t)||(e[t]=s);return{...e,GIT_OPTIONAL_LOCKS:"0",GIT_TERMINAL_PROMPT:"0",GIT_PAGER:"cat",PAGER:"cat",LC_ALL:"C"}}function sH(n,e){if(n=String(n??""),n.length<=e)return{text:n,clipped:!1};let t=n.slice(0,e),s=t.lastIndexOf(`
|
|
230
230
|
`);s>e/2&&(t=t.slice(0,s+1));let i=n.slice(t.length).split(`
|
|
231
231
|
`).filter(Boolean).length;return{text:t+(t.endsWith(`
|
|
232
232
|
`)?"":`
|
|
233
|
-
`)+"… "+i+" more lines, too big to show here",clipped:!0}}function Dt(n,e,{deadline:t,maxBuffer:s=
|
|
234
|
-
`)[0];if(/not a git repository/i.test(f))return c(new un("not_a_repo","this folder isn’t a git repository"));c(new un("git_failed",f||"git failed"))})})}function gC(n){let e=new Map;for(let t of sm(n)){let s=/^(-|\d+)\t(-|\d+)\t([\s\S]+)$/.exec(t);if(!s)continue;let i=s[1]==="-"&&s[2]==="-";e.set(s[3],{add:i?0:Number(s[1]),del:i?0:Number(s[2]),binary:i})}return e}function yC(n){let e=new Map,t=sm(n);for(let s=0;s+1<t.length;s+=2){let i=t[s][0];!i||!t[s+1]||e.set(t[s+1],i==="A"?"added":i==="D"?"deleted":i==="T"?"typechange":"modified")}return e}async function wC(n,e){let s=(await Dt(n,["rev-parse","-q","--verify","HEAD^{commit}"],{...e,okCodes:[0,1]})).stdout.trim();return/^[0-9a-f]{40,64}$/.test(s)?s:null}function kC(n){let e;try{e=eS.lstatSync(n)}catch{return{add:0,del:0,binary:!1,gone:!0}}if(e.isSymbolicLink())return{add:1,del:0,binary:!1,symlink:!0};if(!e.isFile())return{add:0,del:0,binary:!1,gone:!0};if(e.size>
|
|
233
|
+
`)+"… "+i+" more lines, too big to show here",clipped:!0}}function Dt(n,e,{deadline:t,maxBuffer:s=ZU,okCodes:i=[0],gitBin:r="git",env:o}={}){let a=Math.max(1,(t||Date.now()+nm)-Date.now());return new Promise((l,c)=>{qU(r,[...eH,...e],{cwd:n,timeout:a,maxBuffer:s,killSignal:"SIGKILL",env:nH(o),encoding:"buffer",windowsHide:!0},(u,d,h)=>{if(!u)return l({code:0,stdout:d.toString("utf8")});if(u.code==="ERR_CHILD_PROCESS_STDIO_MAXBUFFER")return c(new un("too_big","too big to show here"));if(u.killed||u.signal)return c(new un("timeout","git took longer than "+Math.round(nm/1e3)+" s"));if(u.code==="ENOENT")return c(new un("no_git","git isn’t installed on this Mac"));if(typeof u.code=="number"&&i.includes(u.code))return l({code:u.code,stdout:d.toString("utf8")});let f=String(h||"").trim().split(`
|
|
234
|
+
`)[0];if(/not a git repository/i.test(f))return c(new un("not_a_repo","this folder isn’t a git repository"));c(new un("git_failed",f||"git failed"))})})}function gC(n){let e=new Map;for(let t of sm(n)){let s=/^(-|\d+)\t(-|\d+)\t([\s\S]+)$/.exec(t);if(!s)continue;let i=s[1]==="-"&&s[2]==="-";e.set(s[3],{add:i?0:Number(s[1]),del:i?0:Number(s[2]),binary:i})}return e}function yC(n){let e=new Map,t=sm(n);for(let s=0;s+1<t.length;s+=2){let i=t[s][0];!i||!t[s+1]||e.set(t[s+1],i==="A"?"added":i==="D"?"deleted":i==="T"?"typechange":"modified")}return e}async function wC(n,e){let s=(await Dt(n,["rev-parse","-q","--verify","HEAD^{commit}"],{...e,okCodes:[0,1]})).stdout.trim();return/^[0-9a-f]{40,64}$/.test(s)?s:null}function kC(n){let e;try{e=eS.lstatSync(n)}catch{return{add:0,del:0,binary:!1,gone:!0}}if(e.isSymbolicLink())return{add:1,del:0,binary:!1,symlink:!0};if(!e.isFile())return{add:0,del:0,binary:!1,gone:!0};if(e.size>XU)return{add:0,del:0,binary:!1,big:!0,bytes:e.size};let t;try{t=eS.readFileSync(n)}catch{return{add:0,del:0,binary:!1,gone:!0}}let s=t.toString("utf8");return Cl(s)?{add:0,del:0,binary:!0}:{add:s?s.split(`
|
|
235
235
|
`).length-(s.endsWith(`
|
|
236
|
-
`)?1:0):0,del:0,binary:!1}}async function SC(n,e){if(!n)throw new un("no_folder","this session has no folder");try{if(!eS.statSync(n).isDirectory())throw new Error("x")}catch{throw new un("no_folder","this session’s folder isn’t on this Mac any more")}let s=(await Dt(n,["rev-parse","--show-toplevel"],e)).stdout.trim();if(!s)throw new un("not_a_repo","this folder isn’t a git repository");return s}async function im(n,{gitBin:e="git",env:t,timeoutMs:s=nm,maxFiles:i=
|
|
237
|
-
@@`),v=k>=0?S.slice(k+1):S.startsWith("@@")?S:"";if(!v)return/^Binary files/m.test(S)&&(y.binary=!0),y;let x=sH(Vt(v,r),
|
|
236
|
+
`)?1:0):0,del:0,binary:!1}}async function SC(n,e){if(!n)throw new un("no_folder","this session has no folder");try{if(!eS.statSync(n).isDirectory())throw new Error("x")}catch{throw new un("no_folder","this session’s folder isn’t on this Mac any more")}let s=(await Dt(n,["rev-parse","--show-toplevel"],e)).stdout.trim();if(!s)throw new un("not_a_repo","this folder isn’t a git repository");return s}async function im(n,{gitBin:e="git",env:t,timeoutMs:s=nm,maxFiles:i=JU}={}){let r=Date.now(),o={deadline:r+s,gitBin:e,env:t},a=await SC(n,o),l=await wC(a,o),c=l||mC,[u,d,h]=await Promise.all([Dt(a,["diff",c,"--numstat","-z",...Ol,"--"],o),Dt(a,["diff",c,"--name-status","-z",...Ol,"--"],o),Dt(a,["ls-files","--others","--exclude-standard","-z","--"],o)]),f=gC(u.stdout),p=yC(d.stdout),m=[];for(let[k,v]of f)m.push({path:k,status:p.get(k)||"modified",add:v.add,del:v.del,binary:v.binary});for(let k of sm(h.stdout))k&&!f.has(k)&&m.push({path:k,status:"untracked",add:0,del:0,binary:!1,_u:!0});m.sort((k,v)=>k.path<v.path?-1:k.path>v.path?1:0);let y=m.length>i,w=m.slice(0,i);for(let k of w){if(k._u){delete k._u;let v=kC(pC.join(a,k.path));k.add=v.add,k.binary=v.binary,v.big&&(k.big=!0),v.symlink&&(k.symlink=!0)}Ni(k.path)&&(k.hidden="secret_file")}let S={files:m.length,add:0,del:0};for(let k of w)S.add+=k.add,S.del+=k.del;return{top:a,head:l,files:w,totals:S,truncated:y,ms:Date.now()-r}}async function bC(n,e,{gitBin:t="git",env:s,timeoutMs:i=nm,patterns:r=[]}={}){let o=String(e||"");if(!o||o.includes("\0")||o.length>4096||o.startsWith("/")||o.split("/").includes(".."))throw new un("not_changed","that file has no uncommitted changes");let a={deadline:Date.now()+i,gitBin:t,env:s},l=await SC(n,a),u=await wC(l,a)||mC,d=":(literal)"+o,[h,f]=await Promise.all([Dt(l,["diff",u,"--numstat","-z",...Ol,"--",d],a),Dt(l,["diff",u,"--name-status","-z",...Ol,"--",d],a)]),p=gC(h.stdout).get(o),m;if(p)m={path:o,status:yC(f.stdout).get(o)||"modified",add:p.add,del:p.del,binary:p.binary};else{let E=await Dt(l,["ls-files","--others","--exclude-standard","-z","--",d],a);if(!sm(E.stdout).includes(o))throw new un("not_changed","that file has no uncommitted changes");let A=kC(pC.join(l,o));m={path:o,status:"untracked",add:A.add,del:0,binary:A.binary,...A.big?{big:!0}:{}}}let y={path:m.path,status:m.status,add:m.add,del:m.del,lines:null,clipped:!1};if(Ni(m.path))return y.hidden="secret_file",y;if(m.binary)return y.binary=!0,y;if(m.big)return y.big=!0,y;let w={...a,maxBuffer:QU},S;try{m.status==="untracked"?S=(await Dt(l,["diff","--no-index",...Ol,"--","/dev/null",o],{...w,okCodes:[0,1]})).stdout:S=(await Dt(l,["diff",u,...Ol,"--",d],w)).stdout}catch(E){if(E.code==="too_big")return y.big=!0,y;throw E}let k=S.indexOf(`
|
|
237
|
+
@@`),v=k>=0?S.slice(k+1):S.startsWith("@@")?S:"";if(!v)return/^Binary files/m.test(S)&&(y.binary=!0),y;let x=sH(Vt(v,r),VU);return y.lines=x.text,y.clipped=x.clipped,y}var nm,JU,VU,XU,ZU,QU,mC,eH,Ol,un,tH,sm,tS=K(()=>{hd();nm=3e3,JU=300,VU=64*1024,XU=1024*1024,ZU=2*1024*1024,QU=4*1024*1024,mC="4b825dc642cb6eb9a060e54bf8d69288fbee4904",eH=["-c","core.fsmonitor=false","-c","core.quotepath=off","-c","core.pager=cat","-c","color.ui=false"],Ol=["--no-ext-diff","--no-textconv","--no-color","--no-renames"],un=class extends Error{constructor(e,t){super(t||e),this.code=e}},tH=/^GIT_(DIR|WORK_TREE|INDEX_FILE|OBJECT_DIRECTORY|ALTERNATE_OBJECT_DIRECTORIES|COMMON_DIR|NAMESPACE|EXTERNAL_DIFF|DIFF_OPTS|PREFIX)$/;sm=n=>String(n||"").split("\0")});import iS from"node:fs";import nS from"node:os";import Ot from"node:path";function lS(n,{cwd:e=null,home:t=nS.homedir()}={}){let s=String(n??"");if(!s||s.length>1e3||s.includes("\0"))return null;if(s==="~"||s.startsWith("~/")){if(!t)return null;s=Ot.join(t,s.slice(1))}else{if(s.startsWith("~"))return null;if(!Ot.isAbsolute(s)){if(!e||!Ot.isAbsolute(e))return null;s=Ot.join(e,s)}}return Ot.normalize(s)}function rm(n){let e=String(n||"").split("/").filter(Boolean);for(let s of e.slice(0,-1))if(iH.has(s.toLowerCase()))return!0;let t=(e[e.length-1]||"").toLowerCase();return t?!!(/^\.env(\.|$)/.test(t)||/^\.?env\.(local|production|prod|secret)/.test(t)||/\.(pem|key|p12|pfx|keystore|jks|keychain|keychain-db|kdbx|asc|gpg|ovpn)$/.test(t)||/^\.?credentials?(\.|$|[-_])/.test(t)||/credentials/.test(t)||/^id_(rsa|dsa|ecdsa|ed25519)/.test(t)||/^\.(netrc|npmrc|pypirc|pgpass|git-credentials|htpasswd)$/.test(t)||/(^|[^a-z])(api|private|secret|access|signing|ssh|gpg|pgp|service[-_]?account|master)?[-_.]?keys?([^a-z]|$)/.test(t)||/(^|[^a-z])(secrets?|tokens?|passwords?)([^a-z]|$)/.test(t)):!1}function cS({cwd:n=null,repoRoot:e=null,worktree:t=null,home:s=nS.homedir(),tmpdir:i=nS.tmpdir(),real:r=AC}={}){let o=[];for(let a of[n,e,t])if(a&&Ot.isAbsolute(a)){let l=r(a);l&&l!=="/"&&l!==s&&l!==r(s)&&o.push(l)}if(s){let a=r(Ot.join(s,".claude","plans"));a&&o.push(a)}return{roots:[...new Set(o)],tmp:[...new Set(["/tmp","/private/tmp",i,i&&r(i)].filter(Boolean).map(a=>a.replace(/\/+$/,"")))]}}function vC(n,e){for(let t of e)if(sS(n,t)&&/^claude-[^/]+\/./.test(n.slice(t.length+1)))return!0;return!1}function AC(n){try{return iS.realpathSync(n)}catch{return null}}function rH(n,e){for(let t=Ot.dirname(n),s=Ot.basename(n);;s=Ot.join(Ot.basename(t),s),t=Ot.dirname(t)){let i=e(t);if(i)return Ot.join(i,s);if(t===Ot.dirname(t))return n}}function uS(n,e,{real:t=AC}={}){if(typeof n!="string"||!Ot.isAbsolute(n)||n.includes("\0")||Ot.normalize(n)!==n)return{ok:!1,reason:"bad_path"};if(rm(n))return{ok:!1,reason:"secret"};let{roots:s=[],tmp:i=[]}=e||{},r=a=>s.some(l=>sS(a,l))||vC(a,i),o=t(n);return o?rm(o)?{ok:!1,reason:"secret"}:s.some(a=>sS(o,a))||vC(o,i)?{ok:!0,real:o}:{ok:!1,reason:"outside"}:r(n)||r(rH(n,t))?{ok:!1,missing:!0,reason:"missing"}:{ok:!1,reason:"outside"}}function cH(n){let e=Ot.basename(String(n||"")).toLowerCase(),t=e.includes(".")?e.slice(e.lastIndexOf(".")+1):"";return lH.has(t)?{type:"markdown",mime:"text/markdown"}:xC[t]?{type:"image",mime:xC[t]}:t==="pdf"?{type:"pdf",mime:"application/pdf"}:t==="html"||t==="htm"?{type:"code",mime:"text/html",lang:"html"}:t==="txt"||t==="csv"?{type:"code",mime:"text/plain",lang:t}:oH.has(t)?{type:"code",mime:"text/plain",lang:t}:aH.has(t?e.slice(0,e.lastIndexOf(".")):e)&&(!t||t==="txt")?{type:"code",mime:"text/plain",lang:""}:{type:"other",mime:"application/octet-stream"}}function _C(n,{textMax:e=rS,binMax:t=oS,fsImpl:s=iS}={}){let i;try{i=s.statSync(n)}catch{return{missing:!0}}if(!i.isFile())return{missing:!0,notFile:!0};let r=cH(n),o=r.type==="image"&&r.mime!=="image/svg+xml"||r.type==="pdf",a=o?t:e;if(i.size>a)return{tooBig:!0,size:i.size,type:r.type,mime:r.mime};let l=s.readFileSync(n);return o?{...r,size:i.size,base64:l.toString("base64")}:r.type==="image"?{...r,size:i.size,text:l.toString("utf8")}:uH(l)?{binary:!0,size:i.size,type:"other",mime:r.mime}:{...r.type==="other"?{type:"code",mime:"text/plain",lang:""}:r,size:i.size,text:l.toString("utf8")}}function TC({how:n,real:e,line:t=null,exists:s=i=>iS.existsSync(i)}={}){if(!e||!Ot.isAbsolute(e))return null;if(n==="reveal")return["open",["-R",e]];if(n!=="editor")return null;let i=dH.find(o=>s(o.app)),r=e.split("/").map(encodeURIComponent).join("/");return i?["open",[i.scheme+"://file"+r+(t?":"+Math.max(1,Math.floor(t)):"")]]:["open",["-t",e]]}var rS,oS,aS,iH,sS,oH,aH,lH,xC,uH,dH,dS=K(()=>{rS=2*1024*1024,oS=20*1024*1024,aS="Leftoff only opens files in this session’s folder or Claude’s scratch folders, and never keys or credentials.";iH=new Set([".ssh",".gnupg",".aws",".azure",".kube",".docker","keychains",".password-store",".1password"]);sS=(n,e)=>!!e&&(n===e||n.startsWith(e.endsWith("/")?e:e+"/"));oH=new Set(["js","mjs","cjs","jsx","ts","tsx","json","jsonc","json5","yml","yaml","toml","ini","cfg","conf","sh","bash","zsh","fish","py","rb","go","rs","java","kt","kts","swift","c","h","cc","cpp","hpp","m","mm","cs","php","pl","lua","sql","css","scss","less","vue","svelte","xml","plist","gradle","r","scala","ex","exs","erl","hs","clj","dart","zig","nim","tf","hcl","proto","graphql","gql","diff","patch","log","lock","gitignore","dockerignore","editorconfig","rst","adoc","org","tex","srt","vtt","tsv"]),aH=new Set(["makefile","dockerfile","gemfile","rakefile","procfile","brewfile","justfile","license","readme","changelog","contributing","codeowners","agents","claude"]),lH=new Set(["md","mdx","markdown"]),xC={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml"};uH=n=>n.subarray(0,8192).includes(0);dH=[{app:"/Applications/Cursor.app",scheme:"cursor"},{app:"/Applications/Visual Studio Code.app",scheme:"vscode"},{app:"/Applications/Zed.app",scheme:"zed"}]});import gS from"node:crypto";import fS from"node:path";function cm(n){return Ni(n)||rm(n)}function LC(n,e=""){let t=String(n||"");return gH.test(t)||yH.test(t)?!0:wH.test(String(e||"").split(`
|
|
238
238
|
`).slice(0,5).join(`
|
|
239
239
|
`))}function yS(n){let e=[],t=[],s=null,i=null,r=0,o=0,a=String(n??"").replace(/\r\n?/g,`
|
|
240
240
|
`).split(`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leftoff",
|
|
3
|
-
"version": "0.9.17-beta.
|
|
3
|
+
"version": "0.9.17-beta.5",
|
|
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/register.js
CHANGED
|
@@ -28,7 +28,7 @@ const END_TIMEOUT_MS = 1000
|
|
|
28
28
|
const MODEL_TIMEOUT_MS = 20000
|
|
29
29
|
|
|
30
30
|
// Module state. $.state is not used: the pane redraws with $.ui.invalidate.
|
|
31
|
-
const MOD_VERSION = '0.9.17-beta.
|
|
31
|
+
const MOD_VERSION = '0.9.17-beta.5' // keep in step with .claude-plugin/plugin.json
|
|
32
32
|
|
|
33
33
|
const st = {
|
|
34
34
|
internal: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leftoff/cli",
|
|
3
|
-
"version": "0.9.17-beta.
|
|
3
|
+
"version": "0.9.17-beta.5",
|
|
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
|
@@ -3192,22 +3192,23 @@ html.pv-open, html.pv-open body { overflow: hidden; }
|
|
|
3192
3192
|
.wid-base { font-weight: 650; color: var(--text); }
|
|
3193
3193
|
.gd-bar { display: inline-flex; gap: 1px; margin-left: 6px; vertical-align: -1px; }
|
|
3194
3194
|
.gd-bar i { width: 8px; height: 8px; border-radius: 2px; background: var(--line-strong); }
|
|
3195
|
-
.gd-bar i.gd-ba { background: var(--
|
|
3196
|
-
.gd-bar i.gd-bd { background: var(--
|
|
3195
|
+
.gd-bar i.gd-ba { background: var(--diff-add-fg); }
|
|
3196
|
+
.gd-bar i.gd-bd { background: var(--diff-del-fg); }
|
|
3197
3197
|
.wid-item.open > .wid-file { position: sticky; top: var(--wid-sticky, 56px); z-index: 3; background: var(--card); border-bottom: 1px solid var(--line); }
|
|
3198
3198
|
.wid-item.open > .wid-file:hover { background: var(--bg2); }
|
|
3199
3199
|
.wid-item.open > .wid-out:has(.gd-wrap) { padding: 0; }
|
|
3200
3200
|
.gd-wrap .copy-float { top: 0; right: 0; z-index: 2; }
|
|
3201
3201
|
.gd {
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
--gd-
|
|
3206
|
-
--gd-del
|
|
3207
|
-
--gd-del-word: color-mix(in srgb, var(--red) 38%, transparent);
|
|
3202
|
+
/* 0.9.18 (Joel: "always look green"): GitHub's diff colours, the same as the .html export;
|
|
3203
|
+
the theme's teal --green was murky here. The row is a pale tint, its number gutter a step
|
|
3204
|
+
stronger, the changed words a step darker again; context stays neutral; +/− always shown */
|
|
3205
|
+
--gd-add: var(--diff-add); --gd-add-gut: var(--diff-add-gut); --gd-add-word: var(--diff-add-word);
|
|
3206
|
+
--gd-del: var(--diff-del); --gd-del-gut: var(--diff-del-gut); --gd-del-word: var(--diff-del-word);
|
|
3208
3207
|
--gd-gw: 3.6ch;
|
|
3209
3208
|
font-family: var(--mono); font-size: 12px; line-height: 20px; color: var(--text); background: var(--card2);
|
|
3210
3209
|
}
|
|
3210
|
+
:root, [data-theme="dark"] { --diff-add: rgba(46, 160, 67, .15); --diff-add-gut: rgba(63, 185, 80, .3); --diff-add-word: rgba(46, 160, 67, .4); --diff-add-fg: #3fb950; --diff-del: rgba(248, 81, 73, .1); --diff-del-gut: rgba(248, 81, 73, .3); --diff-del-word: rgba(248, 81, 73, .4); --diff-del-fg: #f85149; --diff-mod-fg: #d29922; --diff-ren-fg: #9198a1; }
|
|
3211
|
+
[data-theme="light"] { --diff-add: #e6ffec; --diff-add-gut: #ccffd8; --diff-add-word: #abf2bc; --diff-add-fg: #1a7f37; --diff-del: #ffebe9; --diff-del-gut: #ffd7d5; --diff-del-word: rgba(255, 129, 130, .4); --diff-del-fg: #d1242f; --diff-mod-fg: #9a6700; --diff-ren-fg: #59636e; }
|
|
3211
3212
|
.gd.gd-w2 { --gd-gw: 3.6ch; } .gd.gd-w3 { --gd-gw: 4.6ch; } .gd.gd-w4 { --gd-gw: 5.6ch; } .gd.gd-w5 { --gd-gw: 6.6ch; } .gd.gd-w6 { --gd-gw: 7.6ch; }
|
|
3212
3213
|
.gd-row { display: grid; grid-template-columns: var(--gd-gw) var(--gd-gw) 2ch minmax(0, 1fr); min-width: 0; }
|
|
3213
3214
|
.gd-gut { padding: 0 6px 0 0; text-align: right; color: var(--faint); font-size: 11.5px; -webkit-user-select: none; user-select: none; font-variant-numeric: tabular-nums; }
|
|
@@ -3219,10 +3220,10 @@ html.pv-open, html.pv-open body { overflow: hidden; }
|
|
|
3219
3220
|
.gd-hunk .gd-c, .gd-gap .gd-c, .gd-note .gd-c { grid-column: 3 / 5; padding-left: 8px; }
|
|
3220
3221
|
.gd-add { background: var(--gd-add); }
|
|
3221
3222
|
.gd-add .gd-gut { background: var(--gd-add-gut); color: var(--text2); }
|
|
3222
|
-
.gd-add .gd-m { color: var(--
|
|
3223
|
+
.gd-add .gd-m { color: var(--diff-add-fg); }
|
|
3223
3224
|
.gd-del { background: var(--gd-del); }
|
|
3224
3225
|
.gd-del .gd-gut { background: var(--gd-del-gut); color: var(--text2); }
|
|
3225
|
-
.gd-del .gd-m { color: var(--
|
|
3226
|
+
.gd-del .gd-m { color: var(--diff-del-fg); }
|
|
3226
3227
|
.gd-x { text-decoration: none; border-radius: 3px; padding: 1px 0; color: inherit; }
|
|
3227
3228
|
ins.gd-x { background: var(--gd-add-word); }
|
|
3228
3229
|
del.gd-x { background: var(--gd-del-word); }
|
|
@@ -4638,7 +4639,7 @@ details[open] > summary .wt-g-chev { transform: rotate(90deg); }
|
|
|
4638
4639
|
.wt-file > summary { display: flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 12px; cursor: pointer; list-style: none; font-size: 13px; }
|
|
4639
4640
|
.wt-file-p { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); font-size: 12.5px; }
|
|
4640
4641
|
.wt-file-n { margin-left: auto; font-family: var(--mono); font-size: 12px; white-space: nowrap; }
|
|
4641
|
-
.wt-add { color: var(--
|
|
4642
|
+
.wt-add { color: var(--diff-add-fg); } .wt-del { color: var(--diff-del-fg); }
|
|
4642
4643
|
.wt-file .wt-snip-d, .wt-file .wt-g-p { padding: 0 12px 12px; }
|
|
4643
4644
|
/* the file map, for big changes */
|
|
4644
4645
|
.wt-fm { position: sticky; top: 0; align-self: start; max-height: 100%; overflow-y: auto; padding: 14px 0 20px 16px; font-size: 13px; }
|
|
@@ -4655,6 +4656,38 @@ details[open] > summary .wt-g-chev { transform: rotate(90deg); }
|
|
|
4655
4656
|
.wt-fm-x { display: inline-flex; color: var(--muted); } .wt-fm-x .ic { width: 13px; height: 13px; }
|
|
4656
4657
|
.wt-fm-k { display: flex; align-items: center; gap: 4px; margin: 16px 8px 0; font-size: 12px; color: var(--muted); }
|
|
4657
4658
|
.wt-fm-k .kbd { display: inline-flex; align-items: center; justify-content: center; height: 18px; min-width: 18px; padding: 1px 5px 0; box-sizing: border-box; border: 1px solid var(--line-strong); border-radius: 5px; font: 500 11px/1 var(--mono); color: var(--text2); }
|
|
4659
|
+
/* 0.9.18 (Joel): a file list on the left (the file map, or the Files view's list) and the reading
|
|
4660
|
+
on the right, each in its own scroll box filling the height; pinned headers stay in theirs */
|
|
4661
|
+
.wt-2p { flex: 1 1 auto; min-height: 0; display: flex; }
|
|
4662
|
+
.wt-2p > .wt-side { flex: 0 0 256px; box-sizing: border-box; position: static; align-self: stretch; max-height: none; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 14px 8px 20px 12px; border-right: 1px solid var(--line); }
|
|
4663
|
+
.wt-2p > .wt-scroll { flex: 1 1 auto; min-width: 0; }
|
|
4664
|
+
.wt-2p .wt-hw { max-width: 1000px; }
|
|
4665
|
+
.wt.wt-big .wt-2p .wt-hw { display: block; max-width: 1000px; }
|
|
4666
|
+
.wt-2p .wt-in { padding-left: 24px; }
|
|
4667
|
+
/* a file's status, GitHub style: a square with + / a dot / − / an arrow (shape first; colour helps) */
|
|
4668
|
+
.wt-fst { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; line-height: 1; }
|
|
4669
|
+
.wt-fst .ic { width: 15px; height: 15px; margin: 0; }
|
|
4670
|
+
.wt-fst-added { color: var(--diff-add-fg); } .wt-fst-deleted { color: var(--diff-del-fg); } .wt-fst-modified { color: var(--diff-mod-fg); } .wt-fst-renamed { color: var(--diff-ren-fg); }
|
|
4671
|
+
.wt-fm-i .wt-fst { margin-left: 2px; }
|
|
4672
|
+
.wt-fl-i .wt-fst { margin-left: 0; }
|
|
4673
|
+
.wt-fl-n { flex: none; display: inline-flex; gap: 5px; font: 500 11px/1 var(--mono); font-variant-numeric: tabular-nums; }
|
|
4674
|
+
.wt-fl-tot { display: inline-flex; gap: 5px; margin-left: 8px; font: 500 11px/1 var(--mono); text-transform: none; letter-spacing: 0; }
|
|
4675
|
+
.wt-fl .wt-fm-h { display: flex; align-items: center; gap: 6px; }
|
|
4676
|
+
.wt-fl .wt-fl-tot { margin-left: auto; margin-right: 6px; }
|
|
4677
|
+
.wt-file > summary .wt-fst { margin-right: -2px; }
|
|
4678
|
+
.wt-file-o { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
|
4679
|
+
/* each open file's name bar stays at the top of the diff pane while you read it */
|
|
4680
|
+
.wt-file[open] > summary { position: sticky; top: 0; z-index: 2; background: var(--card); border-radius: 12px 12px 0 0; border-bottom: 1px solid var(--line); }
|
|
4681
|
+
.wt-file[open] > summary + * { margin-top: 8px; }
|
|
4682
|
+
/* a phone (or a narrow window): the list folds above the diff, inside the one scroll (no box in a box) */
|
|
4683
|
+
.wt-fl-m { display: none; margin: 10px 0 0; border: 1px solid var(--line); border-radius: 12px; background: var(--card); }
|
|
4684
|
+
.wt-fl-m > summary { display: flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 12px; cursor: pointer; list-style: none; font-size: 13px; font-weight: 650; color: var(--text2); }
|
|
4685
|
+
.wt-fl-m > summary::-webkit-details-marker { display: none; }
|
|
4686
|
+
.wt-fl-m > summary .wt-fl-tot { margin-left: auto; }
|
|
4687
|
+
.wt-fl-m[open] > summary .wt-g-chev { transform: rotate(90deg); }
|
|
4688
|
+
.wt-fl-m > .wt-fl { position: static; max-height: none; overflow: visible; padding: 0 6px 8px; }
|
|
4689
|
+
@media (max-width: 900px) { .wt.wt-fls .wt-2p > .wt-side { display: none; } .wt.wt-fls .wt-fl-m { display: block; } .wt.wt-fls .wt-2p .wt-in { padding-left: 16px; } }
|
|
4690
|
+
@media (max-width: 1100px) { .wt.wt-big .wt-2p > .wt-side { display: none; } }
|
|
4658
4691
|
/* the ask box: the question goes into the chat box, with the step's file:line */
|
|
4659
4692
|
.wt-ask { flex: none; display: flex; align-items: center; gap: 8px; max-width: 804px; width: calc(100% - 32px); margin: 6px auto 0; min-height: 52px; padding: 0 6px 0 14px; box-sizing: border-box; border: 1px solid var(--line-strong); border-radius: 14px; background: var(--card); }
|
|
4660
4693
|
.wt-big ~ .wt-ask, .wt.wt-big .wt-ask { max-width: 1000px; }
|
package/ui/app.js
CHANGED
|
@@ -968,7 +968,11 @@ function wtFill(s) {
|
|
|
968
968
|
const sig = k + '|' + (data ? (data.fingerprint || data.status) + (data.cached ? 'c' : '') : '-') + '|' + JSON.stringify([ui.view, ui.filter, ui.loading, ui.error, ui.fellBack, ui.deck, [...wtUi.code], (data && data.scopes || []).map((x) => x.key + x.available).join()])
|
|
969
969
|
let node = wtUi.node
|
|
970
970
|
if (!node || wtUi.sig !== sig) {
|
|
971
|
-
|
|
971
|
+
// (the old node may be out of the page already, reading 0: what was recorded as you scrolled wins)
|
|
972
|
+
const same = node && wtUi.sig.startsWith(k + '|')
|
|
973
|
+
const was = (sel) => { const el = same && node.querySelector(sel); return el ? (wtScrolls.get(el) || [el.scrollTop])[0] || 0 : 0 }
|
|
974
|
+
const keep = was('#wtScroll'), keepSide = was('.wt-side')
|
|
975
|
+
wtScrolls.clear()
|
|
972
976
|
node = document.createElement('div')
|
|
973
977
|
node.className = 'wt-node'
|
|
974
978
|
const when = data && data.generatedAt ? clock(data.generatedAt) : ''
|
|
@@ -979,12 +983,42 @@ function wtFill(s) {
|
|
|
979
983
|
wtStuck(document.querySelector('.wt-scroll')) // 0.9.18
|
|
980
984
|
wtDeckPaint(node, { first: true }) // 0.9.18 the slideshow: its progress, its dots in view
|
|
981
985
|
const sc = node.querySelector('#wtScroll'); if (sc && keep) sc.scrollTop = keep
|
|
986
|
+
const sd = node.querySelector('.wt-side'); if (sd && keepSide) sd.scrollTop = keepSide
|
|
982
987
|
} else {
|
|
983
|
-
if (node.parentNode !== mount)
|
|
988
|
+
if (node.parentNode !== mount) {
|
|
989
|
+
// 0.9.18 (Joel: "pause for a second and it jumps back to the top"): a live update redraws the
|
|
990
|
+
// session page (paintSession sets $app.innerHTML), which takes this node out of the page;
|
|
991
|
+
// putting it back resets every scroll box inside it to 0, so put back where you were, and focus
|
|
992
|
+
mount.replaceChildren(node)
|
|
993
|
+
wtRestoreView(node)
|
|
994
|
+
}
|
|
984
995
|
wtMarks(s)
|
|
985
996
|
}
|
|
986
997
|
if (!data && !ui.loading && !ui.error) setTimeout(() => wtLoad(s.id), 0) // opened by a reload: read it now
|
|
987
998
|
}
|
|
999
|
+
// 0.9.18 where each scroll box in the walkthrough was (the slide or long page, the file list, the
|
|
1000
|
+
// dots) and what had focus, kept as you go, since a node taken out of the page reads 0 and has no focus
|
|
1001
|
+
const wtScrolls = new Map() // element -> [top, left], for the node on screen (cleared when it's rebuilt)
|
|
1002
|
+
let wtFocus = null // what had focus in the walkthrough just before a redraw took it out (wtBeforePaint)
|
|
1003
|
+
document.addEventListener('scroll', (e) => { const t = e.target; if (t && t.nodeType === 1 && wtUi.node && t.isConnected && wtUi.node.contains(t)) wtScrolls.set(t, [t.scrollTop, t.scrollLeft]) }, { capture: true, passive: true })
|
|
1004
|
+
let wtQuietUntil = 0, wtRefocusing = false // the scroll and focus our own restore makes don't close a tip
|
|
1005
|
+
function wtBeforePaint() {
|
|
1006
|
+
const a = document.activeElement
|
|
1007
|
+
wtFocus = wtUi.node && a && a !== document.body && wtUi.node.contains(a) ? a : null
|
|
1008
|
+
}
|
|
1009
|
+
function wtRestoreView(node) {
|
|
1010
|
+
for (const [el, v] of wtScrolls) {
|
|
1011
|
+
if (!node.contains(el)) { wtScrolls.delete(el); continue }
|
|
1012
|
+
if (el.scrollTop !== v[0] || el.scrollLeft !== v[1]) wtQuietUntil = performance.now() + 150
|
|
1013
|
+
if (el.scrollTop !== v[0]) el.scrollTop = v[0]
|
|
1014
|
+
if (el.scrollLeft !== v[1]) el.scrollLeft = v[1]
|
|
1015
|
+
}
|
|
1016
|
+
// focus only goes back if it was in here when the redraw began and the redraw left it nowhere (never taken from elsewhere)
|
|
1017
|
+
const a = document.activeElement
|
|
1018
|
+
if (wtFocus && node.contains(wtFocus) && (!a || a === document.body)) { wtRefocusing = true; try { wtFocus.focus({ preventScroll: true }) } finally { wtRefocusing = false } }
|
|
1019
|
+
wtFocus = null
|
|
1020
|
+
wtStuck(node.querySelector('.wt-scroll'))
|
|
1021
|
+
}
|
|
988
1022
|
// The reviewed marks and the count, in place (no redraw).
|
|
989
1023
|
function wtMarks(s) {
|
|
990
1024
|
const node = wtUi.node; if (!node || !s) return
|
|
@@ -2065,6 +2099,7 @@ function paintSession(s, data) {
|
|
|
2065
2099
|
// preview closed with Esc puts focus back on one, then a redraw replaced it)
|
|
2066
2100
|
const kb = !kf && fa && fa !== document.body && $app.contains(fa) ? focusKey(fa) : null
|
|
2067
2101
|
liveKeeper.before($app)
|
|
2102
|
+
wtBeforePaint() // 0.9.18 the walkthrough's focus, before this takes it out of the page
|
|
2068
2103
|
$app.innerHTML = sessionPageHtml(s, data)
|
|
2069
2104
|
liveAfterRender(s.id)
|
|
2070
2105
|
fitAskDetail($app)
|
|
@@ -7125,12 +7160,13 @@ document.addEventListener('keydown', (e) => { const b = e.target.closest && e.ta
|
|
|
7125
7160
|
tip.classList.add('on')
|
|
7126
7161
|
}, delay)
|
|
7127
7162
|
}
|
|
7128
|
-
const tipped = (e) => e.target.closest && e.target.closest('#tabStrip [data-tip], #tabPager [data-tip], #lfTabs [data-tip], .sg[data-tip], .lo-act-x[data-tip], .repos.projects [data-tip], .lo-sheet-x[data-tip], .proj-edit[data-tip], .feed-why[data-tip], .term-menu [data-tip], #wt
|
|
7163
|
+
const tipped = (e) => e.target.closest && e.target.closest('#tabStrip [data-tip], #tabPager [data-tip], #lfTabs [data-tip], .sg[data-tip], .lo-act-x[data-tip], .repos.projects [data-tip], .lo-sheet-x[data-tip], .proj-edit[data-tip], .feed-why[data-tip], .term-menu [data-tip], #wt [data-tip]') // 0.9.18 everything in the walkthrough (its header's icon buttons, the slide nav, file status glyphs); before: the file map's eye, step number and long folders; a step header's files and (cut short) title the file map's eye, step number and long folders; a step header's files and (cut short) title // 0.9.12: the project chips, the sheet's ×, a project page's Edit
|
|
7129
7164
|
document.addEventListener('pointerover', (e) => { const el = tipped(e); if (!el) { if (cur) hide() } else if (e.pointerType === 'mouse' && el !== cur) show(el, 350) })
|
|
7130
7165
|
document.addEventListener('pointerout', (e) => { const el = tipped(e); if (el && el === cur && !el.contains(e.relatedTarget)) hide() })
|
|
7131
|
-
document.addEventListener('focusin', (e) => { const el = tipped(e); if (el && el.matches(':focus-visible')) show(el, 0); else if (cur) hide() })
|
|
7166
|
+
document.addEventListener('focusin', (e) => { if (wtRefocusing) return; const el = tipped(e); if (el && el.matches(':focus-visible')) show(el, 0); else if (cur) hide() })
|
|
7132
7167
|
document.addEventListener('focusout', (e) => { if (tipped(e) === cur) hide() })
|
|
7133
|
-
for (const ev of ['pointerdown', '
|
|
7168
|
+
for (const ev of ['pointerdown', 'hashchange', 'resize']) window.addEventListener(ev, hide, true)
|
|
7169
|
+
window.addEventListener('scroll', () => { if (performance.now() >= wtQuietUntil) hide() }, true) // 0.9.18 not for a redraw putting the walkthrough's scroll back
|
|
7134
7170
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && cur) hide() })
|
|
7135
7171
|
// 0.9.18 touch: a long press shows the tip (the tap it ends with doesn't follow the link)
|
|
7136
7172
|
let lp = 0, lpd = false
|
|
@@ -7797,6 +7833,18 @@ document.addEventListener('click', async (ev) => {
|
|
|
7797
7833
|
fmToggle((d && d.fingerprint) || '', btn.dataset.key, shut)
|
|
7798
7834
|
return
|
|
7799
7835
|
}
|
|
7836
|
+
if (action === 'wt-file') { // 0.9.18 the Files view's list: open that file's diff and bring it to the top of the diff pane
|
|
7837
|
+
ev.preventDefault()
|
|
7838
|
+
const node = wtUi.node, d = node && node.querySelector('#wt-f-' + CSS.escape(btn.dataset.i || ''))
|
|
7839
|
+
if (!d) return
|
|
7840
|
+
d.open = true
|
|
7841
|
+
for (const a of node.querySelectorAll('.wt-fl-i')) a.classList.toggle('cur', a.dataset.i === btn.dataset.i)
|
|
7842
|
+
const m = btn.closest('details.wt-fl-m'); if (m) m.open = false // a phone: the folded list closes again
|
|
7843
|
+
const sc = node.querySelector('#wtScroll')
|
|
7844
|
+
if (sc) sc.scrollTo({ top: Math.max(0, sc.scrollTop + d.getBoundingClientRect().top - sc.getBoundingClientRect().top - 4), behavior: reducedMotion() ? 'auto' : 'smooth' })
|
|
7845
|
+
const sm = d.querySelector('summary'); if (sm) sm.focus({ preventScroll: true })
|
|
7846
|
+
return
|
|
7847
|
+
}
|
|
7800
7848
|
if (action === 'wt-go') { ev.preventDefault(); return wtGo(wtUi.sid, btn.dataset.step || null, { group: btn.dataset.group || null }) }
|
|
7801
7849
|
if (action === 'wt-step') return wtStep(Number(btn.dataset.dir) || 1)
|
|
7802
7850
|
if (action === 'wt-export-html') return wtExport('html')
|
package/ui/icons.js
CHANGED
|
@@ -37,6 +37,12 @@ export const ICONS = {
|
|
|
37
37
|
"maximize-2": "<path d=\"M15 3h6v6\"/> <path d=\"m21 3-7 7\"/> <path d=\"m3 21 7-7\"/> <path d=\"M9 21H3v-6\"/>",
|
|
38
38
|
"minimize-2": "<path d=\"m14 10 7-7\"/> <path d=\"M20 10h-6V4\"/> <path d=\"m3 21 7-7\"/> <path d=\"M4 14h6v6\"/>",
|
|
39
39
|
"rotate-ccw": "<path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"/> <path d=\"M3 3v5h5\"/>",
|
|
40
|
+
"square-plus": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <path d=\"M8 12h8\"/> <path d=\"M12 8v8\"/>",
|
|
41
|
+
"square-minus": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <path d=\"M8 12h8\"/>",
|
|
42
|
+
"square-dot": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <circle cx=\"12\" cy=\"12\" r=\"2.6\" fill=\"currentColor\" stroke=\"none\"/>",
|
|
43
|
+
"square-arrow-right": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <path d=\"M8 12h8\"/> <path d=\"m12 16 4-4-4-4\"/>",
|
|
44
|
+
"rows-3": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <path d=\"M21 9H3\"/> <path d=\"M21 15H3\"/>",
|
|
45
|
+
"gallery-horizontal": "<path d=\"M2 3v18\"/> <rect width=\"12\" height=\"18\" x=\"6\" y=\"3\" rx=\"2\"/> <path d=\"M22 3v18\"/>",
|
|
40
46
|
"list-checks": "<path d=\"M13 5h8\"/> <path d=\"M13 12h8\"/> <path d=\"M13 19h8\"/> <path d=\"m3 17 2 2 4-4\"/> <path d=\"m3 7 2 2 4-4\"/>",
|
|
41
47
|
"plus": "<path d=\"M5 12h14\"/> <path d=\"M12 5v14\"/>",
|
|
42
48
|
"play": "<path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/>",
|
package/ui/semdiff.js
CHANGED
|
@@ -19,6 +19,31 @@ const GROUP_ICON = { repeated: 'repeat', tests: 'list-checks', mechanical: 'laye
|
|
|
19
19
|
const n0 = (n) => Number(n || 0).toLocaleString('en-US')
|
|
20
20
|
const plural = (n, w, ws) => n0(n) + ' ' + (n === 1 ? w : ws || w + 's')
|
|
21
21
|
|
|
22
|
+
// 0.9.18 (Joel): a file's status the way GitHub draws it, by shape: a square with
|
|
23
|
+
// + (added), a dot (modified), − (deleted) or an arrow (renamed), with a word in
|
|
24
|
+
// its tip and label. Colour helps but is never the only signal.
|
|
25
|
+
export const FILE_STATUS = {
|
|
26
|
+
added: { word: 'Added', icon: 'square-plus' },
|
|
27
|
+
modified: { word: 'Modified', icon: 'square-dot' },
|
|
28
|
+
deleted: { word: 'Deleted', icon: 'square-minus' },
|
|
29
|
+
renamed: { word: 'Renamed', icon: 'square-arrow-right' },
|
|
30
|
+
}
|
|
31
|
+
export function statusWord(f) {
|
|
32
|
+
const st = FILE_STATUS[f && f.status] ? f.status : 'modified'
|
|
33
|
+
return st === 'renamed' && f.oldPath ? 'Renamed from ' + f.oldPath : FILE_STATUS[st].word
|
|
34
|
+
}
|
|
35
|
+
export function statusHtml(f, { esc, icon }) {
|
|
36
|
+
const st = FILE_STATUS[f && f.status] ? f.status : 'modified'
|
|
37
|
+
const w = statusWord(f)
|
|
38
|
+
return '<span class="wt-fst wt-fst-' + st + '" data-st="' + st + '" data-tip="' + esc(w) + '" role="img" aria-label="' + esc(w) + '">' + icon(FILE_STATUS[st].icon) + '</span>'
|
|
39
|
+
}
|
|
40
|
+
// path -> { status, oldPath } from the answer's own Files list (git's new / deleted file mode, rename)
|
|
41
|
+
export function statusMap(data) {
|
|
42
|
+
const m = new Map()
|
|
43
|
+
for (const f of (data && Array.isArray(data.files) ? data.files : [])) m.set(f.path, f)
|
|
44
|
+
return m
|
|
45
|
+
}
|
|
46
|
+
|
|
22
47
|
// The quiet count on a tab and a board card: an eye and a number, only when there's something.
|
|
23
48
|
export function eyeCountHtml(n, { esc }) {
|
|
24
49
|
n = Number(n) || 0
|
|
@@ -126,26 +151,28 @@ const FM_KEY = 'lo-wt-fm:'
|
|
|
126
151
|
export function fmClosed(fp) { try { return new Set(JSON.parse(sessionStorage.getItem(FM_KEY + fp) || '[]')) } catch { return new Set() } }
|
|
127
152
|
export function fmToggle(fp, key, closed) { try { const s = fmClosed(fp); if (closed) s.add(key); else s.delete(key); sessionStorage.setItem(FM_KEY + fp, JSON.stringify([...s])) } catch {} }
|
|
128
153
|
const LOOK_TIP = 'Worth a close look: the walkthrough flags this file'
|
|
129
|
-
function fmFileHtml(it, { esc, icon, cur, depth }) {
|
|
130
|
-
|
|
154
|
+
function fmFileHtml(it, { esc, icon, cur, depth, stat }) {
|
|
155
|
+
const f = stat && stat.get(it.file)
|
|
156
|
+
return '<a class="wt-fm-i' + (it.step === cur ? ' cur' : '') + '" href="#" data-action="wt-go" data-step="' + esc(it.step) + '" data-path="' + esc(it.file) + '"' + (depth ? ' data-d="' + Math.min(depth, 6) + '"' : '') + ' aria-label="' + esc(it.file + ', step ' + it.n + (f ? ', ' + statusWord(f).toLowerCase() : '') + (it.look ? ', worth a close look' : '')) + '">' +
|
|
131
157
|
'<span class="wt-fm-n" data-tip="Step ' + esc(String(it.n)) + ' of the walkthrough">' + it.n + '</span><span class="wt-fm-f">' + esc(it.base) + '</span>' +
|
|
132
|
-
(it.look ? '<span class="wt-fm-x" data-tip="' + LOOK_TIP + '">' + icon('eye') + '</span>' : '') + '</a>'
|
|
158
|
+
(it.look ? '<span class="wt-fm-x" data-tip="' + LOOK_TIP + '">' + icon('eye') + '</span>' : '') + (f ? statusHtml(f, { esc, icon }) : '') + '</a>'
|
|
133
159
|
}
|
|
134
|
-
function fmDirHtml(d, { esc, icon, cur, depth, closed, sec }) {
|
|
160
|
+
function fmDirHtml(d, { esc, icon, cur, depth, closed, sec, stat, fileHtml = fmFileHtml }) {
|
|
135
161
|
const key = sec + ':' + d.path
|
|
136
162
|
const shut = closed.has(key)
|
|
137
163
|
let h = '<div class="wt-fm-d" data-key="' + esc(key) + '"><button type="button" class="wt-fm-dr" data-action="wt-fm-dir" data-key="' + esc(key) + '" aria-expanded="' + !shut + '"' + (depth ? ' data-d="' + Math.min(depth, 6) + '"' : '') + '>' +
|
|
138
164
|
icon(shut ? 'chevron-right' : 'chevron-down') + icon('folder') + '<span class="wt-fm-dp"' + (d.name.length > 26 ? ' data-tip="' + esc(d.path) + '"' : '') + '>' + esc(midTrunc(d.name, 26)) + '</span><span class="wt-fm-dc" aria-label="' + esc(plural(countFiles(d), 'file')) + '">' + countFiles(d) + '</span></button>'
|
|
139
165
|
h += '<div class="wt-fm-dk" role="group"' + (shut ? ' hidden' : '') + '>'
|
|
140
|
-
for (const c of d.dirs) h += fmDirHtml(c, { esc, icon, cur, depth: depth + 1, closed, sec })
|
|
141
|
-
for (const f of d.files) h +=
|
|
166
|
+
for (const c of d.dirs) h += fmDirHtml(c, { esc, icon, cur, depth: depth + 1, closed, sec, stat, fileHtml })
|
|
167
|
+
for (const f of d.files) h += fileHtml(f, { esc, icon, cur, depth: depth + 1, stat })
|
|
142
168
|
return h + '</div></div>'
|
|
143
169
|
}
|
|
144
170
|
function fileMapHtml(data, { esc, icon, cur }) {
|
|
145
171
|
const anyLook = (data.fileMap || []).some((sec) => sec.items.some((x) => x.look))
|
|
146
|
-
let h = '<nav class="wt-fm" aria-label="Files in this change"><div class="wt-fm-h">Files <span class="faint">' + n0(data.stats.files) + '</span>' +
|
|
172
|
+
let h = '<nav class="wt-fm wt-side" aria-label="Files in this change"><div class="wt-fm-h">Files <span class="faint">' + n0(data.stats.files) + '</span>' +
|
|
147
173
|
(anyLook ? '<span class="wt-fm-hint">' + icon('eye') + '= worth a close look</span>' : '') + '</div>'
|
|
148
174
|
const closed = fmClosed(data.fingerprint || '')
|
|
175
|
+
const stat = statusMap(data)
|
|
149
176
|
for (const sec of data.fileMap || []) {
|
|
150
177
|
const count = sec.items.reduce((a, x) => a + (x.count || 1), 0)
|
|
151
178
|
h += '<div class="wt-fm-s"><span class="wt-fm-st"><i class="wt-sw wt-bk-' + esc(sec.kind) + '" aria-hidden="true"></i>' + esc(sec.label) + '</span><span class="faint">' + count + '</span></div>'
|
|
@@ -156,10 +183,10 @@ function fileMapHtml(data, { esc, icon, cur }) {
|
|
|
156
183
|
// one folder: say it once
|
|
157
184
|
const d = t.dirs[0]
|
|
158
185
|
h += '<div class="wt-fm-sub mono"' + (d.path.length > 30 ? ' data-tip="' + esc(d.path) + '"' : '') + '>' + icon('folder') + '<span>' + esc(midTrunc(d.path, 30)) + '/</span></div>'
|
|
159
|
-
for (const f of d.files) h += fmFileHtml(f, { esc, icon, cur, depth: 0 })
|
|
186
|
+
for (const f of d.files) h += fmFileHtml(f, { esc, icon, cur, depth: 0, stat })
|
|
160
187
|
} else {
|
|
161
|
-
for (const d of t.dirs) h += fmDirHtml(d, { esc, icon, cur, depth: 0, closed, sec: sec.kind })
|
|
162
|
-
for (const f of t.files) h += fmFileHtml(f, { esc, icon, cur, depth: 0 })
|
|
188
|
+
for (const d of t.dirs) h += fmDirHtml(d, { esc, icon, cur, depth: 0, closed, sec: sec.kind, stat })
|
|
189
|
+
for (const f of t.files) h += fmFileHtml(f, { esc, icon, cur, depth: 0, stat })
|
|
163
190
|
}
|
|
164
191
|
}
|
|
165
192
|
for (const it of sec.items) {
|
|
@@ -198,7 +225,8 @@ export function walkthroughHtml({ s, data, ui, reviewed = new Set(), esc, icon,
|
|
|
198
225
|
'<div class="wt-ctl">' + scopesHtml(data, scope, { esc, icon, sid }) +
|
|
199
226
|
'<div class="wt-seg wt-seg-sm" role="group" aria-label="View"><button type="button" data-action="wt-view" data-view="story" aria-pressed="' + (view === 'story') + '">Story</button><button type="button" data-action="wt-view" data-view="files" aria-pressed="' + (view === 'files') + '">Files</button></div>' +
|
|
200
227
|
// 0.9.18 slides or one long page (kept per device)
|
|
201
|
-
|
|
228
|
+
// (Joel: the icon needs a tip) what a click does, and how it's showing now
|
|
229
|
+
(ok && view === 'story' && (data.steps || []).length ? '<button type="button" class="btn wt-deckt" data-action="wt-deck" aria-pressed="' + (ui.deck === false) + '" data-tip="' + (ui.deck === false ? 'Show as slides, one step at a time · now: all steps on one page' : 'Show all steps on one page · now: slides') + '" aria-label="' + (ui.deck === false ? 'Show as slides' : 'Show all steps on one page') + '">' + icon(ui.deck === false ? 'gallery-horizontal' : 'rows-3') + '<span class="wt-deckt-l">' + (ui.deck === false ? 'Slides' : 'Show all') + '</span></button>' : '') +
|
|
202
230
|
exportsHtml(ok, ui, { icon }) + '</div>' +
|
|
203
231
|
'<button type="button" class="wt-x" data-action="wt-close" data-id="' + esc(sid) + '" aria-label="Close the walkthrough">' + icon('x') + '</button></div>'
|
|
204
232
|
// which model wrote it (Settings → Models → Walk me through it picks it)
|
|
@@ -253,16 +281,20 @@ export function walkthroughHtml({ s, data, ui, reviewed = new Set(), esc, icon,
|
|
|
253
281
|
groups.map((g) => groupHtml(g, { esc, icon, open: ui.groups && ui.groups.has(g.kind) })).join('')
|
|
254
282
|
}
|
|
255
283
|
body += '<p class="wt-cov">' + icon('circle-check') + 'Every changed line is in a step or a group: ' + n0(data.coverage.covered) + ' of ' + n0(data.coverage.total) + '.</p>'
|
|
256
|
-
}
|
|
284
|
+
}
|
|
285
|
+
let fl = ''
|
|
286
|
+
if (!(ok && view === 'story') && data && Array.isArray(data.files) && data.files.length && !ui.loading) {
|
|
287
|
+
if (data.files.length > 1) { fl = filesListHtml(data, { esc, icon }); body += filesListHtml(data, { esc, icon, mobile: true }) }
|
|
257
288
|
body += filesHtml(data.files, { esc, icon })
|
|
258
289
|
}
|
|
259
|
-
const fm = big ? fileMapHtml(data, { esc, icon, cur: ui.cur }) :
|
|
290
|
+
const fm = big ? fileMapHtml(data, { esc, icon, cur: ui.cur }) : fl
|
|
260
291
|
const dock = '<form class="wt-ask" data-action-form="wt-ask" data-id="' + esc(sid) + '"><label class="lo-vh" for="wtAsk">Ask about this change</label><input id="wtAsk" class="wt-ask-in" type="text" autocomplete="off" placeholder="Ask about this change…" value="' + esc(ui.ask || '') + '"><button type="submit" class="wt-ask-b" aria-label="Put the question in the chat" data-tip="Into the chat box">' + icon('arrow-up') + '</button></form>'
|
|
261
292
|
const pnav = ok && view === 'story' && (data.steps || []).length ? '<div class="wt-pnav"><button type="button" class="btn" data-action="wt-step" data-dir="-1">' + icon('chevron-left') + 'Back</button><span class="faint" id="wtPnavN">' + stepPos(data.steps, ui) + '</span><button type="button" class="btn" data-action="wt-step" data-dir="1">Next' + icon('chevron-right') + '</button></div>' : ''
|
|
262
293
|
const dk = deck ? deckSlides(data, ui) : null
|
|
263
|
-
return '<div class="wt' + (
|
|
294
|
+
return '<div class="wt' + (big ? ' wt-big' : fl ? ' wt-fls' : '') + (deck ? ' wt-deckmode' : '') + '" id="wt" data-sid="' + esc(sid) + '" role="region" aria-labelledby="wtTitle"><div class="wt-grab" aria-hidden="true"></div>' +
|
|
264
295
|
(deck ? '<div class="wt-dprog" aria-hidden="true"><i id="wtDprog" data-p="' + Math.round(((dk.cur + 1) / dk.slides.length) * 1000) / 10 + '"></i></div>' : '') +
|
|
265
|
-
|
|
296
|
+
// 0.9.18 (Joel): with a file list on the left, the list and the reading each scroll on their own
|
|
297
|
+
(fm ? '<div class="wt-2p">' + fm : '') + '<div class="wt-scroll" id="wtScroll"><div class="wt-hw"><div class="wt-in">' + h + body + '</div></div></div>' + (fm ? '</div>' : '') + (deck ? deckNavHtml(dk, { esc, icon, reviewed }) : pnav) + dock + '</div>'
|
|
266
298
|
}
|
|
267
299
|
|
|
268
300
|
// 0.9.18 the walkthrough as slides (Joel: "almost a slide show, each slide was
|
|
@@ -318,14 +350,14 @@ function deckHtml(data, ui, { esc, icon, reviewed, sid }) {
|
|
|
318
350
|
// the bar under the slides: ← Prev · the current title · numbered dots · 3 / 12 · Next →
|
|
319
351
|
export function deckNavHtml({ slides, cur }, { esc, icon, reviewed = new Set() }) {
|
|
320
352
|
const dot = (s, i) => '<button type="button" class="wt-dot' + (i === cur ? ' cur' : '') + (s.step && reviewed.has(s.step.id) ? ' done' : '') + '" data-action="wt-slide" data-slide="' + i + '"' + (s.step ? ' data-step="' + esc(s.step.id) + '"' : '') +
|
|
321
|
-
' aria-label="' + esc((i === 0 ? 'Overview' : s.step ? 'Step ' + s.step.n + ': ' + s.title : s.title) + (s.step && reviewed.has(s.step.id) ? ', reviewed' : '')) + '"' + (i === cur ? ' aria-current="step"' : '') + '>' +
|
|
353
|
+
' aria-label="' + esc((i === 0 ? 'Overview' : s.step ? 'Step ' + s.step.n + ': ' + s.title : s.title) + (s.step && reviewed.has(s.step.id) ? ', reviewed' : '')) + '" data-tip="' + esc(i === 0 ? 'Overview' : s.step ? 'Step ' + s.step.n + ': ' + s.title : s.title) + '"' + (i === cur ? ' aria-current="step"' : '') + '>' +
|
|
322
354
|
(i === 0 ? icon('route') : s.step ? '<span class="wt-dot-n">' + s.step.n + '</span>' : icon('layers')) + '<span class="wt-dot-ok" aria-hidden="true">' + icon('check') + '</span></button>'
|
|
323
355
|
return '<nav class="wt-dnav" aria-label="Slides">' +
|
|
324
|
-
'<button type="button" class="btn wt-dprev" data-action="wt-dstep" data-dir="-1" aria-label="Previous slide"' + (cur === 0 ? ' disabled' : '') + '>' + icon('chevron-left') + '<span class="wt-dl">Prev</span></button>' +
|
|
356
|
+
'<button type="button" class="btn wt-dprev" data-action="wt-dstep" data-dir="-1" aria-label="Previous slide" data-tip="Previous slide (←)"' + (cur === 0 ? ' disabled' : '') + '>' + icon('chevron-left') + '<span class="wt-dl">Prev</span></button>' +
|
|
325
357
|
'<span class="wt-dnow" id="wtDnow">' + esc(slides[cur].title) + '</span>' +
|
|
326
358
|
'<div class="wt-dots" id="wtDots" role="group" aria-label="Go to a slide">' + slides.map(dot).join('') + '</div>' +
|
|
327
359
|
'<span class="wt-dcount" id="wtDcount" aria-live="polite">' + (cur + 1) + ' / ' + slides.length + '</span>' +
|
|
328
|
-
'<button type="button" class="btn wt-dnext" data-action="wt-dstep" data-dir="1" aria-label="Next slide"' + (cur === slides.length - 1 ? ' disabled' : '') + '><span class="wt-dl">Next</span>' + icon('chevron-right') + '</button></nav>'
|
|
360
|
+
'<button type="button" class="btn wt-dnext" data-action="wt-dstep" data-dir="1" aria-label="Next slide" data-tip="Next slide (→)"' + (cur === slides.length - 1 ? ' disabled' : '') + '><span class="wt-dl">Next</span>' + icon('chevron-right') + '</button></nav>'
|
|
329
361
|
}
|
|
330
362
|
export function stepPos(steps, ui) {
|
|
331
363
|
const vis = visibleSteps(steps || [], ui.filter)
|
|
@@ -336,12 +368,33 @@ export function stepPos(steps, ui) {
|
|
|
336
368
|
export function filesHtml(files, { esc, icon }) {
|
|
337
369
|
const open = files.length <= 10
|
|
338
370
|
return '<div class="wt-files-v">' + files.map((f, i) => {
|
|
339
|
-
const what = f.hidden ? 'not shown (keys and env files never are)' : f.binary ? 'binary file' : f.generated ? 'generated or lockfile · ' + plural(f.add + f.del, 'line') : ''
|
|
340
|
-
return '<details class="wt-file" data-file-i="' + i + '"' + (open && !what ? ' open' : '') + '><summary>' + icon('chevron-right', 'ic wt-g-chev') + '<span class="mono wt-file-p">' + esc(f.path) + '</span>' + (f.oldPath ? '<span class="faint">from ' + esc(f.oldPath) + '</span>' : '') +
|
|
371
|
+
const what = f.hidden ? 'not shown (keys and env files never are)' : f.binary ? 'binary file' : f.generated ? 'generated or lockfile · ' + plural(f.add + f.del, 'line') : !f.lines ? (f.status === 'renamed' ? 'renamed, nothing changed inside' : 'no lines changed') : ''
|
|
372
|
+
return '<details class="wt-file" id="wt-f-' + i + '" data-file-i="' + i + '"' + (open && !what ? ' open' : '') + '><summary>' + icon('chevron-right', 'ic wt-g-chev') + statusHtml(f, { esc, icon }) + '<span class="mono wt-file-p">' + esc(f.path) + '</span>' + (f.oldPath ? '<span class="faint wt-file-o">from ' + esc(f.oldPath) + '</span>' : '') +
|
|
341
373
|
'<span class="wt-file-n"><span class="wt-add">+' + n0(f.add) + '</span> <span class="wt-del">−' + n0(f.del) + '</span></span></summary>' +
|
|
342
374
|
(what ? '<p class="faint wt-g-p">' + esc(what) + '</p>' : '<div class="wt-snip-d" data-file="' + i + '"></div>' + (f.clipped ? '<p class="faint wt-g-p">… too big to show all of it here</p>' : '')) + '</details>'
|
|
343
375
|
}).join('') + '</div>'
|
|
344
376
|
}
|
|
377
|
+
// 0.9.18 (Joel): the Files view's list of files, beside the diff on a wide screen (each
|
|
378
|
+
// scrolls on its own) and folded above it on a phone. Folders like the file map;
|
|
379
|
+
// each file with its status by shape and its +/− count; a click opens that file's diff.
|
|
380
|
+
function flFileHtml(it, { esc, icon, depth }) {
|
|
381
|
+
const f = it.f
|
|
382
|
+
return '<a class="wt-fm-i wt-fl-i" href="#" data-action="wt-file" data-i="' + it.i + '"' + (depth ? ' data-d="' + Math.min(depth, 6) + '"' : '') + ' aria-label="' + esc(f.path + ', ' + statusWord(f).toLowerCase() + ', ' + f.add + ' added, ' + f.del + ' removed') + '">' +
|
|
383
|
+
statusHtml(f, { esc, icon }) + '<span class="wt-fm-f">' + esc(it.base) + '</span><span class="wt-fl-n" aria-hidden="true"><span class="wt-add">+' + n0(f.add) + '</span><span class="wt-del">−' + n0(f.del) + '</span></span></a>'
|
|
384
|
+
}
|
|
385
|
+
export function filesListHtml(data, { esc, icon, mobile = false }) {
|
|
386
|
+
const files = (data && data.files) || []
|
|
387
|
+
const items = files.map((f, i) => ({ file: f.path, n: 0, i, f }))
|
|
388
|
+
const t = folderTree(items)
|
|
389
|
+
const closed = fmClosed(data.fingerprint || '')
|
|
390
|
+
let rows = ''
|
|
391
|
+
for (const d of t.dirs) rows += fmDirHtml(d, { esc, icon, depth: 0, closed, sec: 'files', fileHtml: flFileHtml })
|
|
392
|
+
for (const f of t.files) rows += flFileHtml(f, { esc, icon, depth: 0 })
|
|
393
|
+
const add = files.reduce((a, f) => a + (f.add || 0), 0), del = files.reduce((a, f) => a + (f.del || 0), 0)
|
|
394
|
+
const head = 'Files <span class="faint">' + n0(files.length) + '</span><span class="wt-fl-tot"><span class="wt-add">+' + n0(add) + '</span><span class="wt-del">−' + n0(del) + '</span></span>'
|
|
395
|
+
if (mobile) return '<details class="wt-fl-m"><summary>' + icon('chevron-right', 'ic wt-g-chev') + head + '</summary><nav class="wt-fm wt-fl" aria-label="Files in this change">' + rows + '</nav></details>'
|
|
396
|
+
return '<nav class="wt-fm wt-fl wt-side" aria-label="Files in this change"><div class="wt-fm-h">' + head + '</div>' + rows + '</nav>'
|
|
397
|
+
}
|
|
345
398
|
|
|
346
399
|
// Draw the snippets (diff-view.js's renderDiff) and put each callout under its
|
|
347
400
|
// line, the line marked in the gutter. Drawn nodes are kept per answer, so a
|