@useshifu/coding-harness 0.3.35 → 0.3.37

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/wizard/index.html CHANGED
@@ -1,18 +1,56 @@
1
1
  <!doctype html>
2
- <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Shifu setup</title><link rel="stylesheet" href="/wizard.css"><script src="/wizard.js" defer></script></head>
3
- <body><main><header><span class="mark">s</span><span>Shifu setup</span><span class="local">ON YOUR DEVICE</span></header><section>
4
- <p class="eyebrow">Your work has a history. Put it to work.</p><h1>Welcome to Shifu.</h1><p class="intro">Your personal AI context engine. Three quick choices, then connect your assistant and bring in selected history.</p>
5
- <form id="settings"><fieldset><legend>How should Shifu help?</legend>
6
- <label class="label" for="history">Initial history</label><select id="history"><option value="15">Last 15 days · recommended</option><option value="30">Last 30 days</option><option value="custom">Custom days</option><option value="0">Start from now</option></select>
7
- <label id="custom-label" hidden>How many days?<input id="custom" type="number" min="1" max="36500" step="1" value="7"></label><p class="example">Recover decisions and lessons from recent work. More history can mean more processing through your model account.</p>
8
- <fieldset class="option"><legend>Helpful growth feedback</legend><label><input type="radio" name="growth" value="on" checked> On · recommended</label><label><input type="radio" name="growth" value="off"> Off</label><p class="example">“Your trade-off is clear. Adding a success measure could strengthen the recommendation.”</p></fieldset>
9
- <fieldset class="option"><legend>Decision alignment</legend><label><input type="radio" name="alignment" value="on" checked> On · recommended</label><label><input type="radio" name="alignment" value="off"> Off</label><p class="example">“Earlier notes say 30 minutes; this proposal uses an hour. Has the requirement changed?”</p><details><summary>Another example</summary><p>“This follows the notification window agreed in the earlier meeting.”</p></details></fieldset>
10
- <p class="note">Historical retrieval stays on. Feedback choices are independent, and your current instructions take priority.</p>
11
- <div id="key-entry"><label class="label" for="key">Connection key for <span id="harness"></span></label><input id="key" type="password" autocomplete="off" spellcheck="false" placeholder="Paste your tool-specific key"><p class="note">Create a key on your Shifu dashboard. It goes directly to the local runner for verification; never paste it into chat.</p></div>
12
- <div id="saved-key" class="note" hidden>Your saved connection will be reused. No new key is needed.<details><summary>Saved key revoked or no longer valid?</summary><p>Replace it in your own interactive terminal using the command below. The terminal asks for the new key. Replacement resets local checkpoints; saved server records remain. Then reopen onboarding with --new to bind the new connection.</p><code id="replace-command"></code><p><code id="reopen-command"></code></p></details></div>
13
- </fieldset><button type="submit">Review setup →</button></form>
14
- <div id="review" hidden><h2>Your setup</h2><dl id="summary"></dl><p class="note" id="scope"></p><p class="note">For a new connection, ongoing sync defaults to hourly automatic work sync. Existing sync settings are preserved.</p><p class="note">Selected coding text is prepared by your coding tool’s configured model provider. Shifu receives selected redacted records. Redaction can miss confidential details. History can include work across projects.</p><div class="actions"><button id="apply" type="button">Connect and start selected backfill</button><button id="edit" class="secondary" type="button">Edit choices</button></div></div>
15
- <div id="progress" hidden><h2 id="progress-title">Your context is taking shape.</h2><dl id="progress-facts"></dl><p id="progress-note" class="note"></p><div class="actions"><button id="resume" type="button" hidden>Resume unfinished backfill</button><button id="copy" type="button" hidden>Copy first question</button><button id="change" class="secondary" type="button" hidden>Change choices</button></div><p id="handoff" class="note" hidden>Reload your coding assistant or start a new chat to load Shifu tools, then paste this short prompt. Your settings and accepted backfill are saved.</p><textarea id="prompt" readonly hidden aria-label="Assistant resume prompt"></textarea></div>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>Connect Shifu</title>
7
+ <link rel="stylesheet" href="/wizard.css">
8
+ <script src="/wizard.js" defer></script>
9
+ </head>
10
+ <body><main>
11
+ <header><img src="/brand-mark.png" alt=""><span>Shifu</span><small>Setup</small></header>
12
+ <section>
13
+ <div id="welcome"><h1>Set up Shifu</h1><p class="intro">Choose what to connect, then review your selection before Start sync.</p></div>
14
+ <form id="settings">
15
+ <fieldset><legend>Your work sources</legend>
16
+ <fieldset class="option"><legend>Projects</legend>
17
+ <label><input type="radio" name="project-scope" value="selected" checked> Selected projects</label>
18
+ <label><input type="radio" name="project-scope" value="all"> All projects in this coding tool</label>
19
+ <div id="project-list" class="project-options"></div>
20
+ <p id="project-note" class="example">Select the projects whose history and new work you want Shifu to use.</p>
21
+ </fieldset>
22
+ <label class="label" for="history">Initial history</label>
23
+ <select id="history"><option value="15">Last 15 days · recommended</option><option value="30">Last 30 days</option><option value="custom">Custom days</option><option value="0">Start from now</option></select>
24
+ <label id="custom-label" hidden>How many days?<input id="custom" type="number" min="1" max="36500" step="1" value="7"></label>
25
+ <p class="example">More history uses more of your coding model account’s allowance. You can start with a small window and add more later.</p>
26
+ <fieldset class="option"><legend>Ongoing sync</legend><label><input id="schedule-enabled" type="checkbox" checked> Keep work context current with scheduled sync</label><p class="example">The schedule starts only after Start sync. You can leave it off and import just the selected history.</p></fieldset>
27
+ <fieldset class="option"><legend>Files</legend>
28
+ <label><input id="files-enabled" type="checkbox"> Enable file uploads from a folder</label>
29
+ <div id="folder-choice" hidden><label class="label" for="input-folder">Folder to upload</label><input id="input-folder" type="text" autocomplete="off" spellcheck="false"><p class="example">Supported files in this folder upload as provided, separately from session-summary approval. The review shows the resolved folder and file count.</p></div>
30
+ <p class="example">Files stay off until you enable them and approve the folder. A folder change requires another review.</p>
31
+ </fieldset>
32
+ <fieldset class="option"><legend>Helpful growth feedback</legend><label><input type="radio" name="growth" value="on" checked> On · recommended</label><label><input type="radio" name="growth" value="off"> Off</label><p class="example">“Your trade-off is clear. Adding a success measure could strengthen the recommendation.”</p></fieldset>
33
+ <fieldset class="option"><legend>Decision alignment</legend><label><input type="radio" name="alignment" value="on" checked> On · recommended</label><label><input type="radio" name="alignment" value="off"> Off</label><p class="example">“Earlier notes say 30 minutes; this proposal uses an hour. Has the requirement changed?”</p></fieldset>
34
+ <p class="note">Historical retrieval stays on. Feedback choices are independent, and your current instructions take priority.</p>
35
+ <div id="key-entry"><label class="label" for="key">Connection key for <span id="harness"></span></label><input id="key" type="password" autocomplete="off" spellcheck="false" placeholder="Paste your tool-specific key"><p class="note">Create a key on your Shifu dashboard. It goes directly to Shifu through local setup for verification; keep it out of chat.</p></div>
36
+ <div id="saved-key" class="note" hidden>Your saved connection is ready to use.
37
+ <details><summary>Need to replace your key?</summary><p>Ask your assistant to reconnect Shifu. Enter the new key in the secure prompt, never in chat. Replacing a connection can reset local sync progress; work already saved in Shifu remains.</p></details>
38
+ </div>
39
+ </fieldset>
40
+ <button id="review-button" type="submit">Review selection →</button>
41
+ </form>
42
+ <div id="review" hidden>
43
+ <h2>Ready to start?</h2><dl id="summary"></dl>
44
+ <p id="scope" class="note"></p>
45
+ <p class="note">Selected coding text is prepared through your coding tool’s configured model provider and allowance. Shifu receives redacted records; source selection controls which work is included.</p>
46
+ <p class="note">This selection starts only after Start sync. Pause local preparation and uploads anytime from this window.</p>
47
+ <div class="actions"><button id="apply" type="button">Start sync</button><button id="edit" class="secondary" type="button">Edit selection</button></div>
48
+ </div>
49
+ <div id="progress" hidden>
50
+ <h2 id="progress-title">Your context is taking shape.</h2><dl id="progress-facts"></dl><p id="progress-note" class="note"></p>
51
+ <div class="actions"><button id="pause" class="secondary" type="button">Pause preparation and uploads</button><button id="resume-all" type="button" hidden>Resume preparation and uploads</button><button id="resume" class="secondary" type="button" hidden>Retry unfinished history</button><button id="copy" type="button" hidden>Copy first question</button><button id="change" class="secondary" type="button" hidden>Review another selection</button></div>
52
+ <p id="handoff" class="note" hidden>Reload your coding assistant or start a new chat to load Shifu tools, then paste this short prompt. Your settings and saved import progress are kept.</p><textarea id="prompt" readonly hidden aria-label="Assistant resume prompt"></textarea>
53
+ </div>
16
54
  <p id="message" role="status" aria-live="polite"></p>
17
- <details class="privacy"><summary>What happens to my data?</summary><p>Setup choices and progress do not require model calls. Preparing your selected history uses your configured model account; exact cost depends on that history.</p><p>Files placed in the Shifu input folder upload as provided for server processing, separately from coding-session review. Keep unapproved drafts outside that folder.</p><p>Disconnecting stops future access. Retained data deletion is a separate beta request. <a href="https://www.useshifu.com/data" target="_blank" rel="noreferrer">Data handling</a> · <a href="https://www.useshifu.com/privacy" target="_blank" rel="noreferrer">Privacy policy</a></p></details>
55
+ <details class="privacy"><summary>What happens to my data?</summary><p>Selection and review use local metadata. Preparing selected coding history uses your configured model account; actual allowance usage depends on that history. Shifu receives selected redacted work records, which can still contain confidential business context.</p><p>Files are optional and upload as provided from the folder you approve. Symlinks and subfolders are skipped and reported.</p><p>Revoking access stops future Shifu requests. Retained-data deletion is a separate request. <a href="https://www.useshifu.com/data" target="_blank" rel="noreferrer">Data handling</a> · <a href="https://www.useshifu.com/privacy" target="_blank" rel="noreferrer">Privacy policy</a></p></details>
18
56
  </section></main></body></html>
package/wizard/wizard.css CHANGED
@@ -1 +1,83 @@
1
- :root{color-scheme:light dark;--bg:light-dark(#fcfaf8,#1c171a);--panel:light-dark(#fff,#251e22);--ink:light-dark(#38252b,#f4e9ed);--muted:light-dark(#735e66,#c8b5be);--line:light-dark(#e5dadd,#4a3942);--accent:light-dark(#7a3049,#e9a7bc);--on:light-dark(#fff,#38202b);font:14px/1.6 system-ui,sans-serif;color:var(--ink);background:var(--bg)}*{box-sizing:border-box}body{margin:0;padding:24px 16px}main{max-width:650px;margin:auto;background:var(--panel);border:1px solid var(--line);border-radius:8px;overflow:hidden}header{display:flex;align-items:center;gap:10px;border-bottom:1px solid var(--line);padding:18px 24px}.mark{font-size:22px;color:var(--accent)}.local{margin-left:auto;font:10px monospace;color:var(--muted)}section{padding:28px}h1{font-size:28px;font-weight:500;letter-spacing:-.04em;margin:8px 0}h2{font-size:20px;font-weight:500}.eyebrow{font:11px monospace;color:var(--accent);text-transform:uppercase}.intro,.note,.example{color:var(--muted)}.note{font-size:12px}.intro{font-size:14px}fieldset{border:0;margin:0;padding:0}legend,.label{font-weight:500;font-size:14px}.label{display:block;margin:18px 0 8px}select,input[type=password],input[type=number],textarea{font:inherit;color:var(--ink);background:var(--bg);border:1px solid var(--line);border-radius:5px;padding:10px;width:100%}.option{border-top:1px solid var(--line);margin-top:18px;padding-top:12px}.option label{display:inline-flex;gap:7px;margin-right:14px;align-items:center;min-height:36px;font-size:13px}input[type=radio]{accent-color:var(--accent)}.example{font-size:12px;border-left:2px solid var(--line);padding-left:12px}.option details{font-size:12px;color:var(--muted)}button{font:inherit;font-size:13px;border:0;border-radius:5px;background:var(--accent);color:var(--on);padding:12px 15px;min-height:44px;cursor:pointer;margin-top:14px}button:disabled{opacity:.6;cursor:wait}.secondary{background:var(--bg);color:var(--ink);border:1px solid var(--line)}.actions{display:flex;gap:10px;flex-wrap:wrap}summary{cursor:pointer;color:var(--accent)}.privacy{border-top:1px solid var(--line);padding-top:18px;margin-top:24px;font-size:12px}a{color:var(--accent)}dl{display:grid;gap:12px}dl div{display:grid;grid-template-columns:130px minmax(0,1fr);gap:10px}dt{color:var(--muted)}dd{margin:0}#message{font-size:12px;color:var(--accent)}textarea{min-height:150px;margin-top:14px;font-size:12px}button:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:3px}[hidden]{display:none!important}@media(max-width:480px){body{padding:12px 8px}section{padding:20px 16px}header{padding:14px 16px}dl div{grid-template-columns:110px minmax(0,1fr)}h1{font-size:25px}}
1
+ @font-face {
2
+ font-family: 'Plus Jakarta Sans';
3
+ src: url('/fonts/plus-jakarta-sans.ttf') format('truetype');
4
+ font-weight: 200 800;
5
+ font-display: swap;
6
+ }
7
+ @font-face {
8
+ font-family: 'IBM Plex Mono';
9
+ src: url('/fonts/ibm-plex-mono.ttf') format('truetype');
10
+ font-weight: 400;
11
+ font-display: swap;
12
+ }
13
+ :root {
14
+ color-scheme: light;
15
+ --app-canvas: #fcfaf8;
16
+ --app-panel: #ffffff;
17
+ --app-subtle: #fff1e8;
18
+ --app-border: #e7dcd3;
19
+ --app-text: #362b25;
20
+ --app-muted: #716256;
21
+ --app-accent: #bc4617;
22
+ --app-accent-text: #ffffff;
23
+ font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
24
+ font-size: 14px;
25
+ line-height: 1.5;
26
+ letter-spacing: -.015em;
27
+ color: var(--app-text);
28
+ background: var(--app-canvas);
29
+ -webkit-font-smoothing: antialiased;
30
+ }
31
+ * { box-sizing: border-box; }
32
+ body { min-width: 320px; margin: 0; padding: 32px 20px; }
33
+ main { max-width: 640px; margin: auto; border: 1px solid var(--app-border); border-radius: 6px; background: var(--app-panel); }
34
+ header { display: flex; align-items: center; gap: 10px; padding: 18px 28px; border-bottom: 1px solid var(--app-border); font-weight: 650; }
35
+ header img { width: 28px; height: 28px; }
36
+ header small { margin-left: auto; color: var(--app-muted); font: 11px 'IBM Plex Mono', monospace; text-transform: uppercase; letter-spacing: .04em; }
37
+ section { padding: 28px; }
38
+ h1 { margin: 0 0 10px; font-size: 26px; line-height: 1.2; font-weight: 550; letter-spacing: -.035em; }
39
+ h2 { font-size: 18px; font-weight: 550; }
40
+ p { margin: 8px 0 14px; }
41
+ .intro, .note, .example { color: var(--app-muted); }
42
+ .intro { font-size: 14px; margin-bottom: 24px; }
43
+ .note, .example, details { font-size: 12px; line-height: 1.6; }
44
+ fieldset { margin: 0; padding: 0; border: 0; min-width: 0; }
45
+ legend, .label { font-size: 14px; font-weight: 550; }
46
+ .label { display: block; margin: 18px 0 8px; }
47
+ select, input[type=password], input[type=text], input[type=number], textarea { width: 100%; min-height: 44px; padding: 10px 12px; border: 1px solid var(--app-border); border-radius: 6px; background: var(--app-panel); color: var(--app-text); font: inherit; }
48
+ .option { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--app-border); }
49
+ .option legend { float: left; width: 100%; margin-bottom: 4px; }
50
+ .option label { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; margin-right: 20px; }
51
+ input[type=radio], input[type=checkbox] { width: 16px; height: 16px; accent-color: var(--app-accent); }
52
+ .example { margin: 4px 0 0; }
53
+ button { min-height: 44px; margin-top: 18px; padding: 10px 16px; border: 1px solid transparent; border-radius: 6px; background: var(--app-accent); color: var(--app-accent-text); font: inherit; font-size: 13px; font-weight: 550; cursor: pointer; }
54
+ button:hover { filter: brightness(.95); }
55
+ button:disabled { opacity: .6; cursor: wait; }
56
+ .secondary { border-color: var(--app-border); background: transparent; color: var(--app-text); }
57
+ .actions { display: flex; flex-wrap: wrap; gap: 10px; }
58
+ a, summary { color: var(--app-accent); }
59
+ summary { min-height: 32px; padding: 6px 0; cursor: pointer; }
60
+ .privacy { margin-top: 24px; padding-top: 14px; border-top: 1px solid var(--app-border); }
61
+ dl { display: grid; gap: 12px; }
62
+ dl div { display: grid; grid-template-columns: 130px minmax(0, 1fr); gap: 12px; }
63
+ dt { color: var(--app-muted); }
64
+ dd { margin: 0; overflow-wrap: anywhere; }
65
+ #message { margin: 14px 0 0; color: var(--app-accent); font-size: 12px; }
66
+ #message:empty { display: none; }
67
+ textarea { min-height: 120px; margin-top: 14px; font: 12px/1.6 'IBM Plex Mono', monospace; }
68
+ :focus-visible { outline: 2px solid var(--app-accent); outline-offset: 3px; }
69
+ [hidden] { display: none !important; }
70
+ .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
71
+ @media (max-width: 480px) {
72
+ body { padding: 16px 12px; }
73
+ section { padding: 22px 18px; }
74
+ header { padding: 16px 18px; }
75
+ h1 { font-size: 24px; }
76
+ dl div { grid-template-columns: 100px minmax(0, 1fr); }
77
+ input[type=password], input[type=text], input[type=number], select { font-size: 16px; }
78
+ }
79
+
80
+ .project-options { display: grid; gap: 6px; margin-top: 10px; }
81
+ .project-options label { display: flex; min-height: 44px; margin: 0; overflow-wrap: anywhere; }
82
+ .project-options input { flex-shrink: 0; }
83
+ .project-options span { min-width: 0; }
package/wizard/wizard.js CHANGED
@@ -1,48 +1,227 @@
1
1
  (() => {
2
- const token=location.hash.slice(1);
3
- const el=(id)=>document.getElementById(id);
4
- let state, choices, busy=false;
5
- const names={codex:'Codex',claude_code:'Claude Code',opencode:'OpenCode',cursor:'Cursor'};
6
- async function api(path,body){
7
- const response=await fetch('/api/'+path,{method:body?'POST':'GET',headers:{'X-Shifu-Wizard':token,...(body?{'Content-Type':'application/json'}:{})},...(body?{body:JSON.stringify(body)}:{})});
8
- const result=await response.json();if(!response.ok)throw new Error(result.error||'Setup is unavailable.');return result;
9
- }
10
- function facts(target,items){el(target).replaceChildren(...items.map(([label,value])=>{const row=document.createElement('div'),dt=document.createElement('dt'),dd=document.createElement('dd');dt.textContent=label;dd.textContent=value;row.append(dt,dd);return row;}));}
11
- function settings(value){
12
- el('history').value=['0','15','30'].includes(String(value.historyDays))?String(value.historyDays):'custom';el('custom').value=value.historyDays||7;el('custom-label').hidden=el('history').value!=='custom';
13
- document.querySelector(`input[name=growth][value=${value.growthFeedback?'on':'off'}]`).checked=true;document.querySelector(`input[name=alignment][value=${value.decisionAlignment?'on':'off'}]`).checked=true;
14
- el('key-entry').hidden=value.configured;el('saved-key').hidden=!value.configured;el('harness').textContent=names[value.harness]||value.harness;el('replace-command').textContent='npx --yes @useshifu/coding-harness@0.3.35 connect --harness '+value.harness;el('reopen-command').textContent='npx --yes @useshifu/coding-harness@0.3.35 onboard --harness '+value.harness+' --new';
15
- }
16
- function renderProgress(){
17
- el('settings').hidden=true;el('review').hidden=true;el('progress').hidden=false;
18
- const labels={pending:'Waiting to prepare history',running:'Preparing selected history',partial:'Part of your selected history is saved',blocked:'Older unsynced turns need your direction',failed:'Backfill needs attention',complete:'Selected backfill complete',skipped:'Historical backfill skipped',empty:'No eligible history found'};
19
- el('progress-title').textContent=state.contextAvailable?'Your first context is ready.':labels[state.backfillStatus]||'Setup is continuing.';
20
- facts('progress-facts',[['Connection',state.connectionStatus==='reload_required'?'Verified · reload assistant required':state.connectionStatus],['History',state.historyDays?`Last ${state.historyDays} days`:'Start from now'],['Backfill',labels[state.backfillStatus]||state.backfillStatus],['Accepted',String(state.acceptedSegments||0)+' segments'],['Remaining',String(state.pendingSegments||0)+' segments'],['Blocked',String(state.blockedSegments||0)+' sessions']]);
21
- el('progress-note').textContent=state.lastErrorCode?'Accepted work is preserved. Retry the connection or ask your assistant for help; the selected window will not be expanded automatically.':state.backfillStatus==='skipped'?'New captured work can become context for future questions. No historical content was imported.':'You can continue working. Backfill is checkpointed independently from this browser; full completion is separate from first context availability.';
22
- el('resume').hidden=!['failed','partial','blocked','pending'].includes(state.backfillStatus);el('copy').hidden=!state.contextAvailable;el('handoff').hidden=!state.contextAvailable;
23
- el('prompt').value=state.resumePrompt||'';el('change').hidden=state.backfillStatus==='running';
24
- }
25
- el('history').addEventListener('change',()=>{el('custom-label').hidden=el('history').value!=='custom';});
26
- el('settings').addEventListener('submit',(event)=>{
27
- event.preventDefault();const days=el('history').value==='custom'?Number(el('custom').value):Number(el('history').value);
28
- if(!Number.isSafeInteger(days)||days<0||days>36500||(el('history').value==='custom'&&days===0)){el('message').textContent='Choose a positive whole number of custom days, or Start from now.';return;}
29
- choices={historyDays:days,growthFeedback:document.querySelector('input[name=growth]:checked').value==='on',decisionAlignment:document.querySelector('input[name=alignment]:checked').value==='on'};
30
- facts('summary',[['History',days?`Last ${days} days`:'Start from now'],['Growth feedback',choices.growthFeedback?'On':'Off'],['Decision alignment',choices.decisionAlignment?'On':'Off']]);
31
- el('scope').textContent=days?`Prepare and sync the selected ${days}-day history window using your configured coding-model account. Actual usage depends on selected history.`:'Connect your assistant and skip historical backfill.';
32
- el('apply').textContent=days?'Connect and start selected backfill':'Connect and skip historical backfill';el('settings').hidden=true;el('review').hidden=false;el('message').textContent='';
2
+ const token = location.hash.slice(1);
3
+ const el = (id) => document.getElementById(id);
4
+ const names = { codex: 'Codex', claude_code: 'Claude Code', opencode: 'OpenCode', cursor: 'Cursor' };
5
+ const completed = ['complete', 'skipped', 'empty'];
6
+ let state, choices, busy = false;
7
+
8
+ async function api(path, body) {
9
+ const response = await fetch('/api/' + path, {
10
+ method: body ? 'POST' : 'GET',
11
+ headers: { 'X-Shifu-Wizard': token, ...(body ? { 'Content-Type': 'application/json' } : {}) },
12
+ ...(body ? { body: JSON.stringify(body) } : {}),
13
+ });
14
+ const result = await response.json();
15
+ if (!response.ok) throw new Error(result.error || 'Setup is unavailable.');
16
+ return result;
17
+ }
18
+
19
+ function facts(target, items) {
20
+ el(target).replaceChildren(...items.map(([label, value]) => {
21
+ const row = document.createElement('div'), dt = document.createElement('dt'), dd = document.createElement('dd');
22
+ dt.textContent = label;
23
+ dd.textContent = value;
24
+ row.append(dt, dd);
25
+ return row;
26
+ }));
27
+ }
28
+
29
+ function projectScope() {
30
+ const all = document.querySelector('input[name=project-scope][value=all]').checked;
31
+ el('project-list').hidden = all;
32
+ el('project-note').textContent = all ? 'All projects in this coding tool are included, including new projects.' : 'Select the projects whose history and new work you want Shifu to use.';
33
+ }
34
+
35
+ function settings(value) {
36
+ el('history').value = ['0', '15', '30'].includes(String(value.historyDays)) ? String(value.historyDays) : 'custom';
37
+ el('custom').value = value.historyDays || 7;
38
+ el('custom-label').hidden = el('history').value !== 'custom';
39
+ document.querySelector(`input[name=growth][value=${value.growthFeedback ? 'on' : 'off'}]`).checked = true;
40
+ document.querySelector(`input[name=alignment][value=${value.decisionAlignment ? 'on' : 'off'}]`).checked = true;
41
+ document.querySelector(`input[name=project-scope][value=${value.projectScope === 'all' ? 'all' : 'selected'}]`).checked = true;
42
+ const selected = new Set(value.projectRefs || []);
43
+ const projects = (value.projects || []).map((project) => {
44
+ const label = document.createElement('label'), checkbox = document.createElement('input'), text = document.createElement('span');
45
+ checkbox.type = 'checkbox';
46
+ checkbox.name = 'project';
47
+ checkbox.value = project.ref;
48
+ checkbox.checked = selected.has(project.ref);
49
+ text.textContent = `${project.label} · ${project.sessionCount} sessions`;
50
+ label.append(checkbox, text);
51
+ return label;
52
+ });
53
+ if (!projects.length) {
54
+ const empty = document.createElement('p');
55
+ empty.className = 'note';
56
+ empty.textContent = 'No projects found yet. Select All projects to include new work as it appears.';
57
+ projects.push(empty);
58
+ }
59
+ el('project-list').replaceChildren(...projects);
60
+ projectScope();
61
+ el('schedule-enabled').checked = value.scheduleEnabled ?? value.syncPolicy?.scheduleEnabled ?? true;
62
+ el('files-enabled').checked = value.filesEnabled === true;
63
+ el('input-folder').value = value.inputFolder || '';
64
+ el('folder-choice').hidden = !el('files-enabled').checked;
65
+ el('key-entry').hidden = value.configured;
66
+ el('saved-key').hidden = !value.configured;
67
+ el('harness').textContent = names[value.harness] || value.harness;
68
+ }
69
+
70
+ function syncLabel(value) {
71
+ const policy = value.syncPolicy;
72
+ if ((value.scheduleEnabled ?? policy?.scheduleEnabled) === false) return 'Scheduled work sync off';
73
+ if (!policy) return 'Every hour · automatic after Start sync';
74
+ return policy.scheduleEnabled ? `Every ${policy.intervalHours} hours · ${policy.approvalMode}` : 'Scheduled work sync off';
75
+ }
76
+
77
+ function projectLabel(value) {
78
+ if (value.projectScope === 'all') return 'All projects in this coding tool';
79
+ const selected = new Set(value.projectRefs || []);
80
+ return (value.projects || state.projects || []).filter((project) => selected.has(project.ref)).map((project) => project.label).join(', ') || 'Selected projects';
81
+ }
82
+
83
+ function renderProgress() {
84
+ el('welcome').hidden = true;
85
+ el('settings').hidden = true;
86
+ el('review').hidden = true;
87
+ el('progress').hidden = false;
88
+ const labels = { pending: 'Waiting to prepare history', running: 'Preparing selected history', partial: 'Part of your selected history is saved', blocked: 'Older unsynced turns need your direction', failed: 'History import needs attention', complete: 'Selected history saved', skipped: 'Initial history skipped', empty: 'No eligible history found', paused: 'Processing paused' };
89
+ el('progress-title').textContent = state.paused ? 'Local preparation and uploads are paused.' : state.contextAvailable ? 'Your first context is ready.' : labels[state.backfillStatus] || 'Setup is continuing.';
90
+ facts('progress-facts', [
91
+ ['Connection', state.connectionStatus === 'reload_required' ? 'Verified · reload assistant required' : state.connectionStatus],
92
+ ['Projects', projectLabel(state)],
93
+ ['History', state.historyDays ? `Last ${state.historyDays} days` : 'Start from now'],
94
+ ['Files', state.filesEnabled ? state.inputFolder : 'Off'],
95
+ ['Ongoing sync', state.paused ? 'Paused' : syncLabel(state)],
96
+ ['History import', labels[state.backfillStatus] || state.backfillStatus],
97
+ ['Accepted', String(state.acceptedSegments || 0) + ' segments'],
98
+ ['Remaining', String(state.pendingSegments || 0) + ' segments'],
99
+ ['Blocked', String(state.blockedSegments || 0) + ' sessions'],
100
+ ]);
101
+ el('progress-note').textContent = state.paused ? 'No new local preparation or upload requests will start. Work already submitted to Shifu or your model provider may finish; saved work and import progress are preserved.' : state.lastErrorCode === 'preparation_deferred' ? 'This batch reached its model-request limit. Remaining selected history stays pending for the next scheduled run or Retry unfinished history.' : state.lastErrorCode ? 'Accepted work is preserved. Retry the connection or ask your assistant for help; the selected scope will not be expanded automatically.' : state.backfillStatus === 'skipped' ? 'Initial history was skipped. New work in the selected scope can become context for future questions.' : 'You can continue working. The import resumes from saved progress; first context availability is separate from full completion.';
102
+ el('pause').hidden = state.paused === true;
103
+ el('resume-all').hidden = state.paused !== true;
104
+ el('resume').hidden = state.paused === true || !['failed', 'partial', 'blocked', 'pending'].includes(state.backfillStatus);
105
+ el('copy').hidden = !state.contextAvailable;
106
+ el('handoff').hidden = !state.contextAvailable;
107
+ el('prompt').value = state.resumePrompt || '';
108
+ el('change').hidden = state.backfillStatus === 'running' && !state.paused;
109
+ }
110
+
111
+ el('history').addEventListener('change', () => { el('custom-label').hidden = el('history').value !== 'custom'; });
112
+ el('files-enabled').addEventListener('change', () => { el('folder-choice').hidden = !el('files-enabled').checked; });
113
+ document.querySelectorAll('input[name=project-scope]').forEach((input) => input.addEventListener('change', projectScope));
114
+ el('settings').addEventListener('submit', async (event) => {
115
+ event.preventDefault();
116
+ if (busy) return;
117
+ const historyDays = el('history').value === 'custom' ? Number(el('custom').value) : Number(el('history').value);
118
+ if (!Number.isSafeInteger(historyDays) || historyDays < 0 || historyDays > 36500 || (el('history').value === 'custom' && historyDays === 0)) {
119
+ el('message').textContent = 'Choose a positive whole number of custom days, or Start from now.';
120
+ return;
121
+ }
122
+ const projectScope = document.querySelector('input[name=project-scope]:checked').value;
123
+ const projectRefs = projectScope === 'all' ? [] : [...document.querySelectorAll('input[name=project]:checked')].map((input) => input.value);
124
+ if (projectScope === 'selected' && !projectRefs.length) {
125
+ el('message').textContent = 'Select at least one project, or choose All projects.';
126
+ return;
127
+ }
128
+ const filesEnabled = el('files-enabled').checked;
129
+ const inputFolder = el('input-folder').value.trim();
130
+ if (filesEnabled && !inputFolder) {
131
+ el('message').textContent = 'Choose the folder whose files you want to upload.';
132
+ return;
133
+ }
134
+ choices = { historyDays, projectScope, projectRefs, scheduleEnabled: el('schedule-enabled').checked, filesEnabled, ...(filesEnabled ? { inputFolder } : {}), growthFeedback: document.querySelector('input[name=growth]:checked').value === 'on', decisionAlignment: document.querySelector('input[name=alignment]:checked').value === 'on' };
135
+ busy = true;
136
+ el('review-button').disabled = true;
137
+ el('message').textContent = 'Checking your selection locally…';
138
+ try {
139
+ const preview = await api('preview', choices);
140
+ if (!Number.isSafeInteger(preview.scopeRevision) || preview.scopeRevision < 1) throw new Error('The selection could not be reviewed. Try again before starting sync.');
141
+ choices.scopeRevision = preview.scopeRevision;
142
+ facts('summary', [
143
+ ['Destination', preview.destination],
144
+ ['Projects', projectLabel(preview)],
145
+ ['History', historyDays ? `Last ${historyDays} days · ${preview.sessionCount} sessions` : 'Start from now'],
146
+ ['Files', filesEnabled ? `${preview.fileCount} files · ${preview.totalFileBytes} bytes` : 'Off'],
147
+ ...(filesEnabled ? [['Folder', preview.inputFolder], ['Skipped', `${preview.skippedSymlinks || 0} links · ${preview.skippedDirectories || 0} subfolders`]] : []),
148
+ ['Ongoing sync', syncLabel(preview)],
149
+ ['Growth feedback', choices.growthFeedback ? 'On' : 'Off'],
150
+ ['Decision alignment', choices.decisionAlignment ? 'On' : 'Off'],
151
+ ]);
152
+ el('scope').textContent = preview.quotaNotice || 'Preparing selected coding text uses your configured model account’s allowance. Review is local; processing begins after Start sync.';
153
+ el('welcome').hidden = true;
154
+ el('settings').hidden = true;
155
+ el('review').hidden = false;
156
+ el('message').textContent = '';
157
+ } catch (error) { el('message').textContent = error.message; }
158
+ finally { busy = false; el('review-button').disabled = false; }
159
+ });
160
+ el('edit').addEventListener('click', () => { choices = undefined; el('review').hidden = true; el('welcome').hidden = false; el('settings').hidden = false; });
161
+ el('apply').addEventListener('click', async () => {
162
+ if (busy || !choices?.scopeRevision) return;
163
+ busy = true;
164
+ el('apply').disabled = true;
165
+ el('message').textContent = 'Verifying your connection and starting the reviewed selection…';
166
+ const key = el('key').value.trim();
167
+ el('key').value = '';
168
+ try {
169
+ state = await api('apply', { ...choices, ...(!state.configured ? { key } : {}) });
170
+ renderProgress();
171
+ el('message').textContent = '';
172
+ } catch (error) {
173
+ choices = undefined;
174
+ el('message').textContent = error.message;
175
+ el('review').hidden = true;
176
+ el('welcome').hidden = false; el('settings').hidden = false;
177
+ } finally { busy = false; el('apply').disabled = false; }
33
178
  });
34
- el('edit').addEventListener('click',()=>{el('review').hidden=true;el('settings').hidden=false;});
35
- el('apply').addEventListener('click',async()=>{
36
- if(busy)return;busy=true;el('apply').disabled=true;el('message').textContent='Verifying your connection and configuring Shifu…';
37
- const key=el('key').value.trim();el('key').value='';
38
- try{state=await api('apply',{...choices,...(!state.configured?{key}:{})});renderProgress();el('message').textContent='';}
39
- catch(error){el('message').textContent=error.message;el('review').hidden=true;el('settings').hidden=false;}
40
- finally{busy=false;el('apply').disabled=false;}
179
+ el('change').addEventListener('click', async () => {
180
+ try {
181
+ const abandon = !completed.includes(state.backfillStatus);
182
+ if (abandon && !window.confirm('Leave the unfinished history selection and review new settings? Saved records and import progress will remain.')) return;
183
+ state = await api('new', { abandon });
184
+ choices = undefined;
185
+ settings(state);
186
+ el('progress').hidden = true;
187
+ el('welcome').hidden = false; el('settings').hidden = false;
188
+ } catch (error) { el('message').textContent = error.message; }
41
189
  });
42
- el('change').addEventListener('click',async()=>{try{const abandon=!['complete','skipped','empty'].includes(state.backfillStatus);if(abandon&&!window.confirm('Leave the unfinished history selection and choose new settings? Accepted records and sync checkpoints will remain. No older work will be added automatically.'))return;state=await api('new',{abandon});settings(state);el('progress').hidden=true;el('settings').hidden=false;}catch(error){el('message').textContent=error.message;}});
43
- el('resume').addEventListener('click',async()=>{try{state=await api('resume',{});renderProgress();el('message').textContent='Resume requested. Accepted ranges will not be replayed.';}catch(error){el('message').textContent=error.message;}});
44
- el('copy').addEventListener('click',async()=>{try{await navigator.clipboard.writeText(state.resumePrompt);el('message').textContent='Copied. Reload your assistant if needed, then paste the prompt.';}catch{el('prompt').hidden=false;el('prompt').focus();el('prompt').select();el('message').textContent='Copy this prompt and paste it into your assistant.';}});
45
- async function refresh(){if(busy||document.hidden)return;try{state=await api('status');if(state.authorized)renderProgress();}catch(error){el('message').textContent=error.message;}}
46
- api('status').then(value=>{state=value;settings(value);if(value.authorized)renderProgress();}).catch(error=>{el('settings').hidden=true;el('message').textContent=error.message;});
47
- setInterval(refresh,5000);document.addEventListener('visibilitychange',()=>{if(!document.hidden)refresh();});
190
+ async function processingAction(path) {
191
+ if (busy) return;
192
+ busy = true;
193
+ for (const id of ['pause', 'resume-all', 'resume']) el(id).disabled = true;
194
+ try {
195
+ state = await api(path, {});
196
+ renderProgress();
197
+ el('message').textContent = path === 'pause' ? 'Local preparation and uploads paused. Accepted work is preserved.' : 'Local preparation and uploads resumed from the saved selection and import progress.';
198
+ } catch (error) { el('message').textContent = error.message; }
199
+ finally { busy = false; for (const id of ['pause', 'resume-all', 'resume']) el(id).disabled = false; }
200
+ }
201
+ el('pause').addEventListener('click', () => processingAction('pause'));
202
+ el('resume-all').addEventListener('click', () => processingAction('resume'));
203
+ el('resume').addEventListener('click', () => processingAction('resume'));
204
+ el('copy').addEventListener('click', async () => {
205
+ try { await navigator.clipboard.writeText(state.resumePrompt); el('message').textContent = 'Copied. Reload your assistant if needed, then paste the prompt.'; }
206
+ catch { el('prompt').hidden = false; el('prompt').focus(); el('prompt').select(); el('message').textContent = 'Copy this prompt and paste it into your assistant.'; }
207
+ });
208
+ async function refresh() {
209
+ if (busy || document.hidden) return;
210
+ try {
211
+ const previous = state;
212
+ state = await api('status');
213
+ if (state.scopeReviewRequired && !previous?.scopeReviewRequired) {
214
+ choices = undefined;
215
+ settings(state);
216
+ el('progress').hidden = true;
217
+ el('review').hidden = true;
218
+ el('welcome').hidden = false; el('settings').hidden = false;
219
+ el('message').textContent = 'The source selection changed. Review it before processing continues.';
220
+ } else if (state.authorized && !state.scopeReviewRequired) renderProgress();
221
+ }
222
+ catch (error) { el('message').textContent = error.message; }
223
+ }
224
+ api('status').then((value) => { state = value; settings(value); if (value.authorized && !value.scopeReviewRequired) renderProgress(); if (value.scopeReviewRequired) el('message').textContent = 'Review your sources and select Start sync to continue. Saved work and import progress are preserved.'; }).catch((error) => { el('settings').hidden = true; el('message').textContent = error.message; });
225
+ setInterval(refresh, 5000);
226
+ document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh(); });
48
227
  })();