letmeknow-cli 0.4.9 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SKILL.md +3 -1
- package/bin/letmeknow.js +15 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ The CLI does not receive file commands. The agent reads and writes the directory
|
|
|
30
30
|
|
|
31
31
|
The preview serves exact files and `index.html` for directory paths. It supports GET and HEAD, redirects directory paths to a trailing slash, and has no application-shell fallback. HTML responses include the live-preview client inline. Use relative asset URLs so previews also work on path-based session URLs.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
The client continuously saves form values, checked controls, selections, focus, text selection, scroll position, and open `<details>` elements for each page in the tab's session storage. It restores them after file-triggered and user-triggered reloads. CSS changes cache-bust matching stylesheets without navigating, while changes to the current HTML route or another asset reload the page. Changes to a different HTML route do not disturb the current page. Arbitrary JavaScript heap state cannot be preserved.
|
|
34
34
|
|
|
35
35
|
An optional element can display submission status:
|
|
36
36
|
|
package/SKILL.md
CHANGED
|
@@ -33,9 +33,11 @@ Create an ordinary static page in the folder, usually `index.html`, plus any CSS
|
|
|
33
33
|
|
|
34
34
|
The relay serves exact files and directory `index.html` files. It supports GET and HEAD, redirects directory paths to a trailing slash, and does not provide an application-shell fallback. HTML responses include the live-preview client inline.
|
|
35
35
|
|
|
36
|
+
The preview continuously saves each page's form values, checked controls, selections, focus, text selection, scroll position, and open `<details>` elements in the tab's session storage. It restores them after file-triggered and user-triggered reloads.
|
|
37
|
+
|
|
36
38
|
The preview is live:
|
|
37
39
|
|
|
38
|
-
- A change to the current HTML route reloads the page
|
|
40
|
+
- A change to the current HTML route reloads the page with its saved state.
|
|
39
41
|
- CSS changes cache-bust matching linked stylesheets without navigating.
|
|
40
42
|
- Changes to a different HTML route do not disturb the current page.
|
|
41
43
|
- Changes to other assets reload the page. Arbitrary JavaScript heap state cannot be preserved.
|
package/bin/letmeknow.js
CHANGED
|
@@ -37,7 +37,7 @@ const client = String.raw`
|
|
|
37
37
|
const sessionMatch=location.pathname.match(/^\/s\/[a-f0-9]{20}(?:\/|$)/);
|
|
38
38
|
const sessionBase=sessionMatch?(sessionMatch[0].endsWith("/")?sessionMatch[0]:sessionMatch[0]+"/"):"/";
|
|
39
39
|
const credentialKey="letmeknow-credential:"+location.origin+sessionBase;
|
|
40
|
-
const snapshotKey=credentialKey+":
|
|
40
|
+
const snapshotKey=()=>credentialKey+":state:"+pageIdentity();
|
|
41
41
|
let credential;
|
|
42
42
|
try{credential=sessionStorage.getItem(credentialKey)}catch{}
|
|
43
43
|
let socket;
|
|
@@ -57,14 +57,25 @@ const status=message=>{const element=document.querySelector("[data-letmeknow-sta
|
|
|
57
57
|
const stripSessionPath=path=>{if(sessionBase==="/")return path;if(path===sessionBase.slice(0,-1))return "/";return path.startsWith(sessionBase)?"/"+path.slice(sessionBase.length):path};
|
|
58
58
|
const routePath=()=>{let path=stripSessionPath(location.pathname);return path.endsWith("/")?path+"index.html":path};
|
|
59
59
|
const pagePath=path=>{path=path.split("?",1)[0];return stripSessionPath(path)||"/"};
|
|
60
|
-
const
|
|
61
|
-
const
|
|
60
|
+
const save=()=>{try{sessionStorage.setItem(snapshotKey(),JSON.stringify(snapshot()))}catch{}};
|
|
61
|
+
const restore=()=>{let raw;try{raw=sessionStorage.getItem(snapshotKey())}catch{return}if(!raw)return;let saved;try{saved=JSON.parse(raw)}catch{return}if(saved.version!==1||saved.page!==pageIdentity())return;const all=controls();const savedControls=new Map((Array.isArray(saved.controls)?saved.controls:[]).map(state=>[state.key,state]));let active;for(const [index,control] of all.entries()){const state=savedControls.get(controlKey(control,index,all));if(!state)continue;if(control instanceof HTMLSelectElement&&Array.isArray(state.selected))for(const [optionIndex,option] of [...control.options].entries())option.selected=state.selected.includes(optionIndex);else if(control.type==="checkbox"||control.type==="radio"){control.checked=state.checked;control.indeterminate=state.indeterminate}else{control.value=state.value;if(typeof state.start==="number"&&typeof control.setSelectionRange==="function")control.setSelectionRange(state.start,state.end,state.direction||"none")}if(controlKey(control,index,all)===saved.active)active=control}const savedDetails=new Map((Array.isArray(saved.details)?saved.details:[]).map(state=>[state.key,state]));for(const [index,element] of details().entries()){const state=savedDetails.get(detailKey(element,index));if(state)element.open=state.open}active?.focus({preventScroll:true});scrollTo(saved.x||0,saved.y||0)};
|
|
62
|
+
const reload=()=>{if(reloadTimer)return;reloadTimer=setTimeout(()=>{save();location.reload()},75)};
|
|
62
63
|
const linkedStylesheet=path=>{for(const link of document.querySelectorAll('link[rel~="stylesheet"]')){let url;try{url=new URL(link.href,location.href)}catch{continue}if(url.origin!==location.origin)continue;if(sessionBase!=="/"&&!url.pathname.startsWith(sessionBase))continue;if(pagePath(url.pathname)===path)return link}return null};
|
|
63
64
|
const refreshStylesheet=link=>{const url=new URL(link.href,location.href);url.searchParams.set("_letmeknow",crypto.randomUUID());link.href=url.href};
|
|
64
65
|
const flushUpdates=()=>{updateTimer=undefined;const paths=[...pendingUpdates];pendingUpdates.clear();let shouldReload=false;const styles=[];for(const path of paths){if(/\.html?$/i.test(path)){if(path===routePath())shouldReload=true}else if(/\.css$/i.test(path)){const link=linkedStylesheet(path);if(link)styles.push([path,link]);else shouldReload=true}else shouldReload=true}if(shouldReload){reload();return}for(const [,link] of styles)refreshStylesheet(link)};
|
|
65
66
|
const pendingUpdates=new Set();
|
|
66
67
|
const update=path=>{if(typeof path!=="string")return;path=pagePath(path);pendingUpdates.add(path);if(!updateTimer)updateTimer=setTimeout(flushUpdates,75)};
|
|
68
|
+
let saveTimer;
|
|
69
|
+
const scheduleSave=()=>{if(!saveTimer)saveTimer=setTimeout(()=>{saveTimer=undefined;save()},100)};
|
|
70
|
+
addEventListener("input",scheduleSave,true);
|
|
71
|
+
addEventListener("change",scheduleSave,true);
|
|
72
|
+
addEventListener("toggle",scheduleSave,true);
|
|
73
|
+
addEventListener("focusin",scheduleSave,true);
|
|
74
|
+
addEventListener("selectionchange",scheduleSave,true);
|
|
75
|
+
addEventListener("scroll",scheduleSave,{passive:true});
|
|
76
|
+
addEventListener("pagehide",save);
|
|
67
77
|
addEventListener("load",()=>requestAnimationFrame(restore),{once:true});
|
|
78
|
+
document.addEventListener("reset",()=>setTimeout(save));
|
|
68
79
|
const connect=()=>{
|
|
69
80
|
clearTimeout(retryTimer);retryTimer=undefined;
|
|
70
81
|
const url=new URL(sessionBase+"_letmeknow/client",location.href);url.protocol=url.protocol==="https:"?"wss:":"ws:";
|
|
@@ -77,7 +88,7 @@ const connect=()=>{
|
|
|
77
88
|
if(message.type==="challenge"){if(current.readyState===WebSocket.OPEN)try{current.send(JSON.stringify({type:"alive",nonce:message.nonce}))}catch{}return}
|
|
78
89
|
if(message.type==="busy"){status("This session is open elsewhere");retryTimer=setTimeout(connect,message.retry_after*1000);return}
|
|
79
90
|
if(message.type==="file_update"){update(message.path);return}
|
|
80
|
-
if(message.type==="closed"){terminal=true;try{sessionStorage.removeItem(credentialKey)
|
|
91
|
+
if(message.type==="closed"){terminal=true;try{sessionStorage.removeItem(credentialKey)}catch{}status(message.message)}
|
|
81
92
|
};
|
|
82
93
|
current.onclose=()=>{if(socket!==current||terminal)return;if(!retryTimer){status("Reconnecting…");retryTimer=setTimeout(connect,1000)}};
|
|
83
94
|
current.onerror=()=>{};
|