@useshifu/coding-harness 0.3.36 → 0.3.38

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/wizard.js CHANGED
@@ -1,49 +1,254 @@
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;
15
- }
16
- function renderProgress(){
17
- el('welcome').hidden=true;el('settings').hidden=true;el('review').hidden=true;el('progress').hidden=false;
18
- const labels={pending:'Ready to sync your recent work',running:'Syncing your recent work',partial:'Some work is saved; sync will continue',blocked:'Some older work needs your attention',failed:'Sync needs a retry',complete:'Your selected history is saved',skipped:'Ready for new work',empty:'No recent work found in this time range'};
19
- const connections={pending:'Connecting',verified:'Connected',reload_required:'Connected · start a new chat',failed:'Reconnect required'};
20
- el('progress-title').textContent=state.contextAvailable?'Shifu is ready to help.':labels[state.backfillStatus]||'Setup is continuing.';
21
- facts('progress-facts',[['Assistant',names[state.harness]||state.harness],['Connection',connections[state.connectionStatus]||'Checking connection'],['History',labels[state.backfillStatus]||'Checking your recent work']]);
22
- el('progress-note').textContent=state.lastErrorCode?'Work already saved is kept. Try again, or ask your assistant to help reconnect Shifu.':state.backfillStatus==='skipped'?'Your next captured work can become context for future questions. No past work was imported.':'You can close this page and keep working. Sync continues in the background and resumes without repeating saved work.';
23
- el('resume').hidden=!['failed','partial','blocked','pending'].includes(state.backfillStatus);el('copy').hidden=!state.contextAvailable;el('handoff').hidden=!state.contextAvailable;
24
- el('prompt').value=state.resumePrompt||'';el('change').hidden=state.backfillStatus==='running';
25
- }
26
- el('history').addEventListener('change',()=>{el('custom-label').hidden=el('history').value!=='custom';});
27
- el('settings').addEventListener('submit',(event)=>{
28
- event.preventDefault();const days=el('history').value==='custom'?Number(el('custom').value):Number(el('history').value);
29
- 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;}
30
- choices={historyDays:days,growthFeedback:document.querySelector('input[name=growth]:checked').value==='on',decisionAlignment:document.querySelector('input[name=alignment]:checked').value==='on'};
31
- facts('summary',[['History',days?`Last ${days} days`:'Start from now'],['Feedback',choices.growthFeedback?'On':'Off'],['Decision reminders',choices.decisionAlignment?'On':'Off']]);
32
- el('scope').textContent=days?`Include the last ${days} days of work. Preparing this history uses your coding tool’s model account; cost depends on the amount of work.`:'Connect your assistant and start with new work. Past work will not be imported.';
33
- el('apply').textContent=days?'Connect and sync':'Connect Shifu';el('welcome').hidden=true;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 connectionKeyLink(value) {
36
+ const link = el('get-key');
37
+ link.hidden = true;
38
+ link.removeAttribute('href');
39
+ if (typeof value.connectionKeyUrl !== 'string' || !Object.hasOwn(names, value.harness)) return;
40
+ try {
41
+ const url = new URL(value.connectionKeyUrl);
42
+ if (url.username || url.password || url.port) return;
43
+ const desktop = url.protocol === 'shifu:' && url.hostname === 'sources' && url.pathname === '/coding-sessions' && !url.hash;
44
+ const hosted = url.origin === 'https://app.useshifu.com' && url.pathname === '/' && !url.search && url.hash.startsWith('#/sources/coding-sessions?');
45
+ if (!desktop && !hosted) return;
46
+ const params = desktop ? url.searchParams : new URLSearchParams(url.hash.slice('#/sources/coding-sessions?'.length));
47
+ const expected = desktop ? ['harness', 'profile', 'connection'] : ['harness', 'connection'];
48
+ if (params.size !== expected.length || expected.some((key) => params.getAll(key).length !== 1) ||
49
+ params.get('harness') !== value.harness || params.get('connection') !== 'key' ||
50
+ (desktop && !/^desktop-[a-f0-9]{12}$/.test(params.get('profile') || ''))) return;
51
+ link.href = url.href;
52
+ link.target = desktop ? '_self' : '_blank';
53
+ link.rel = 'noopener noreferrer';
54
+ link.hidden = false;
55
+ } catch { /* Keep the manual key instructions when a shortcut is unavailable. */ }
56
+ }
57
+
58
+ function settings(value) {
59
+ el('history').value = ['0', '15', '30'].includes(String(value.historyDays)) ? String(value.historyDays) : 'custom';
60
+ el('custom').value = value.historyDays || 7;
61
+ el('custom-label').hidden = el('history').value !== 'custom';
62
+ document.querySelector(`input[name=growth][value=${value.growthFeedback ? 'on' : 'off'}]`).checked = true;
63
+ document.querySelector(`input[name=alignment][value=${value.decisionAlignment ? 'on' : 'off'}]`).checked = true;
64
+ document.querySelector(`input[name=project-scope][value=${value.projectScope === 'all' ? 'all' : 'selected'}]`).checked = true;
65
+ const selected = new Set(value.projectRefs || []);
66
+ const projects = (value.projects || []).map((project) => {
67
+ const label = document.createElement('label'), checkbox = document.createElement('input'), text = document.createElement('span');
68
+ checkbox.type = 'checkbox';
69
+ checkbox.name = 'project';
70
+ checkbox.value = project.ref;
71
+ checkbox.checked = selected.has(project.ref);
72
+ text.textContent = `${project.label} · ${project.sessionCount} sessions`;
73
+ label.append(checkbox, text);
74
+ return label;
75
+ });
76
+ if (!projects.length) {
77
+ const empty = document.createElement('p');
78
+ empty.className = 'note';
79
+ empty.textContent = 'No projects found yet. Select All projects to include new work as it appears.';
80
+ projects.push(empty);
81
+ }
82
+ el('project-list').replaceChildren(...projects);
83
+ projectScope();
84
+ el('schedule-enabled').checked = value.scheduleEnabled ?? value.syncPolicy?.scheduleEnabled ?? true;
85
+ el('files-enabled').checked = value.filesEnabled ?? true;
86
+ el('input-folder').value = value.inputFolder || '';
87
+ el('folder-choice').hidden = !el('files-enabled').checked;
88
+ el('key-entry').hidden = value.configured;
89
+ el('saved-key').hidden = !value.configured;
90
+ el('harness').textContent = names[value.harness] || value.harness;
91
+ connectionKeyLink(value);
92
+ }
93
+
94
+ function syncLabel(value) {
95
+ const policy = value.syncPolicy;
96
+ if ((value.scheduleEnabled ?? policy?.scheduleEnabled) === false) return 'Scheduled work sync off';
97
+ if (!policy) return 'Every hour · automatic after Start sync';
98
+ return policy.scheduleEnabled ? `Every ${policy.intervalHours} hours · ${policy.approvalMode}` : 'Scheduled work sync off';
99
+ }
100
+
101
+ function projectLabel(value) {
102
+ if (value.projectScope === 'all') return 'All projects in this coding tool';
103
+ const selected = new Set(value.projectRefs || []);
104
+ return (value.projects || state.projects || []).filter((project) => selected.has(project.ref)).map((project) => project.label).join(', ') || 'Selected projects';
105
+ }
106
+
107
+ function renderProgress() {
108
+ el('welcome').hidden = true;
109
+ el('settings').hidden = true;
110
+ el('review').hidden = true;
111
+ el('progress').hidden = false;
112
+ 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' };
113
+ 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.';
114
+ facts('progress-facts', [
115
+ ['Connection', state.connectionStatus === 'reload_required' ? 'Verified · reload assistant required' : state.connectionStatus],
116
+ ['Projects', projectLabel(state)],
117
+ ['History', state.historyDays ? `Last ${state.historyDays} days` : 'Start from now'],
118
+ ['Files', state.filesEnabled ? state.inputFolder : 'Off'],
119
+ ['Ongoing sync', state.paused ? 'Paused' : syncLabel(state)],
120
+ ['History import', labels[state.backfillStatus] || state.backfillStatus],
121
+ ['Accepted', String(state.acceptedSegments || 0) + ' segments'],
122
+ ['Remaining', String(state.pendingSegments || 0) + ' segments'],
123
+ ['Blocked', String(state.blockedSegments || 0) + ' sessions'],
124
+ ]);
125
+ 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.';
126
+ el('pause').hidden = state.paused === true;
127
+ el('resume-all').hidden = state.paused !== true;
128
+ el('resume').hidden = state.paused === true || !['failed', 'partial', 'blocked', 'pending'].includes(state.backfillStatus);
129
+ el('copy').hidden = !state.contextAvailable;
130
+ el('handoff').hidden = !state.contextAvailable;
131
+ el('prompt').value = state.resumePrompt || '';
132
+ el('change').hidden = state.backfillStatus === 'running' && !state.paused;
133
+ }
134
+
135
+ el('history').addEventListener('change', () => { el('custom-label').hidden = el('history').value !== 'custom'; });
136
+ el('files-enabled').addEventListener('change', () => { el('folder-choice').hidden = !el('files-enabled').checked; });
137
+ document.querySelectorAll('input[name=project-scope]').forEach((input) => input.addEventListener('change', projectScope));
138
+ el('settings').addEventListener('submit', async (event) => {
139
+ event.preventDefault();
140
+ if (busy) return;
141
+ if (!state.configured && !/^cs_sk_[a-f0-9]{16}_[a-f0-9]{64}$/.test(el('key').value.trim())) {
142
+ el('message').textContent = `Return to Shifu → Sources → ${names[state.harness] || state.harness}, select Create & copy key (or Copy connection key), then paste it in Connection key below. This is a Shifu connection key, not a model provider key.`;
143
+ el('key').focus();
144
+ return;
145
+ }
146
+ const historyDays = el('history').value === 'custom' ? Number(el('custom').value) : Number(el('history').value);
147
+ if (!Number.isSafeInteger(historyDays) || historyDays < 0 || historyDays > 36500 || (el('history').value === 'custom' && historyDays === 0)) {
148
+ el('message').textContent = 'Choose a positive whole number of custom days, or Start from now.';
149
+ return;
150
+ }
151
+ const projectScope = document.querySelector('input[name=project-scope]:checked').value;
152
+ const projectRefs = projectScope === 'all' ? [] : [...document.querySelectorAll('input[name=project]:checked')].map((input) => input.value);
153
+ if (projectScope === 'selected' && !projectRefs.length) {
154
+ el('message').textContent = 'Select at least one project, or choose All projects.';
155
+ return;
156
+ }
157
+ const filesEnabled = el('files-enabled').checked;
158
+ const inputFolder = el('input-folder').value.trim();
159
+ if (filesEnabled && !inputFolder) {
160
+ el('message').textContent = 'Choose the folder whose files you want parsed into context.';
161
+ return;
162
+ }
163
+ 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' };
164
+ busy = true;
165
+ el('review-button').disabled = true;
166
+ el('message').textContent = 'Checking your selection locally…';
167
+ try {
168
+ const preview = await api('preview', choices);
169
+ if (!Number.isSafeInteger(preview.scopeRevision) || preview.scopeRevision < 1) throw new Error('The selection could not be reviewed. Try again before starting sync.');
170
+ choices.scopeRevision = preview.scopeRevision;
171
+ facts('summary', [
172
+ ['Destination', preview.destination],
173
+ ['Projects', projectLabel(preview)],
174
+ ['History', historyDays ? `Last ${historyDays} days · ${preview.sessionCount} sessions` : 'Start from now'],
175
+ ['Files', filesEnabled ? `${preview.fileCount} files · ${preview.totalFileBytes} bytes` : 'Off'],
176
+ ...(filesEnabled ? [['Folder', preview.inputFolder], ['Skipped', `${preview.skippedSymlinks || 0} links · ${preview.skippedDirectories || 0} subfolders`]] : []),
177
+ ['Ongoing sync', syncLabel(preview)],
178
+ ['Growth feedback', choices.growthFeedback ? 'On' : 'Off'],
179
+ ['Decision alignment', choices.decisionAlignment ? 'On' : 'Off'],
180
+ ]);
181
+ el('scope').textContent = preview.quotaNotice || 'Preparing selected coding text uses your configured model account’s allowance. Review is local; processing begins after Start sync.';
182
+ el('welcome').hidden = true;
183
+ el('settings').hidden = true;
184
+ el('review').hidden = false;
185
+ el('message').textContent = '';
186
+ } catch (error) { el('message').textContent = error.message; }
187
+ finally { busy = false; el('review-button').disabled = false; }
188
+ });
189
+ el('edit').addEventListener('click', () => { choices = undefined; el('key-entry').hidden = state.configured; el('saved-key').hidden = !state.configured; el('review').hidden = true; el('welcome').hidden = false; el('settings').hidden = false; });
190
+ el('apply').addEventListener('click', async () => {
191
+ if (busy || !choices?.scopeRevision) return;
192
+ busy = true;
193
+ el('apply').disabled = true;
194
+ el('message').textContent = 'Verifying your connection and starting the reviewed selection…';
195
+ const key = el('key').value.trim();
196
+ try {
197
+ state = await api('apply', { ...choices, ...(!state.configured ? { key } : {}) });
198
+ el('key').value = '';
199
+ renderProgress();
200
+ el('message').textContent = '';
201
+ } catch (error) {
202
+ try { state = await api('status'); } catch { /* Keep the reviewed selection available during a connection failure. */ }
203
+ el('message').textContent = error.message;
204
+ } finally { busy = false; el('apply').disabled = false; }
34
205
  });
35
- el('edit').addEventListener('click',()=>{el('welcome').hidden=false;el('review').hidden=true;el('settings').hidden=false;});
36
- el('apply').addEventListener('click',async()=>{
37
- if(busy)return;busy=true;el('apply').disabled=true;el('message').textContent='Verifying your connection and configuring Shifu…';
38
- const key=el('key').value.trim();el('key').value='';
39
- try{state=await api('apply',{...choices,...(!state.configured?{key}:{})});renderProgress();el('message').textContent='';}
40
- catch(error){el('message').textContent=error.message;el('welcome').hidden=false;el('review').hidden=true;el('settings').hidden=false;}
41
- finally{busy=false;el('apply').disabled=false;}
206
+ el('change').addEventListener('click', async () => {
207
+ try {
208
+ const abandon = !completed.includes(state.backfillStatus);
209
+ if (abandon && !window.confirm('Leave the unfinished history selection and review new settings? Saved records and import progress will remain.')) return;
210
+ state = await api('new', { abandon });
211
+ choices = undefined;
212
+ settings(state);
213
+ el('progress').hidden = true;
214
+ el('welcome').hidden = false; el('settings').hidden = false;
215
+ } catch (error) { el('message').textContent = error.message; }
42
216
  });
43
- el('change').addEventListener('click',async()=>{try{const abandon=!['complete','skipped','empty'].includes(state.backfillStatus);if(abandon&&!window.confirm('Change your history selection? Work already saved will stay in Shifu. This stops the current selection; no older work will be added automatically.'))return;state=await api('new',{abandon});settings(state);el('welcome').hidden=false;el('progress').hidden=true;el('settings').hidden=false;}catch(error){el('message').textContent=error.message;}});
44
- el('resume').addEventListener('click',async()=>{try{state=await api('resume',{});renderProgress();el('message').textContent='Sync is continuing. Work already saved will not be sent again.';}catch(error){el('message').textContent=error.message;}});
45
- el('copy').addEventListener('click',async()=>{try{await navigator.clipboard.writeText(state.resumePrompt);el('message').textContent='Question copied. Return to your assistant and paste it there. Start a new chat if Shifu is not available yet.';}catch{el('prompt').hidden=false;el('prompt').focus();el('prompt').select();el('message').textContent='Copy the question below and paste it into your assistant.';}});
46
- async function refresh(){if(busy||document.hidden)return;try{state=await api('status');if(state.authorized)renderProgress();}catch(error){el('message').textContent=error.message;}}
47
- api('status').then(value=>{state=value;settings(value);if(value.authorized)renderProgress();}).catch(error=>{el('settings').hidden=true;el('message').textContent=error.message;});
48
- setInterval(refresh,5000);document.addEventListener('visibilitychange',()=>{if(!document.hidden)refresh();});
217
+ async function processingAction(path) {
218
+ if (busy) return;
219
+ busy = true;
220
+ for (const id of ['pause', 'resume-all', 'resume']) el(id).disabled = true;
221
+ try {
222
+ state = await api(path, {});
223
+ renderProgress();
224
+ 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.';
225
+ } catch (error) { el('message').textContent = error.message; }
226
+ finally { busy = false; for (const id of ['pause', 'resume-all', 'resume']) el(id).disabled = false; }
227
+ }
228
+ el('pause').addEventListener('click', () => processingAction('pause'));
229
+ el('resume-all').addEventListener('click', () => processingAction('resume'));
230
+ el('resume').addEventListener('click', () => processingAction('resume'));
231
+ el('copy').addEventListener('click', async () => {
232
+ try { await navigator.clipboard.writeText(state.resumePrompt); el('message').textContent = 'Copied. Reload your assistant if needed, then paste the prompt.'; }
233
+ catch { el('prompt').hidden = false; el('prompt').focus(); el('prompt').select(); el('message').textContent = 'Copy this prompt and paste it into your assistant.'; }
234
+ });
235
+ async function refresh() {
236
+ if (busy || document.hidden) return;
237
+ try {
238
+ const previous = state;
239
+ state = await api('status');
240
+ if (state.scopeReviewRequired && !previous?.scopeReviewRequired) {
241
+ choices = undefined;
242
+ settings(state);
243
+ el('progress').hidden = true;
244
+ el('review').hidden = true;
245
+ el('welcome').hidden = false; el('settings').hidden = false;
246
+ el('message').textContent = 'The source selection changed. Review it before processing continues.';
247
+ } else if (state.authorized && !state.scopeReviewRequired) renderProgress();
248
+ }
249
+ catch (error) { el('message').textContent = error.message; }
250
+ }
251
+ 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; });
252
+ setInterval(refresh, 5000);
253
+ document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh(); });
49
254
  })();