@useshifu/coding-harness 0.3.37 → 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/bin/onboarding-ui.js +1 -1
- package/bin/onboarding.js +11 -5
- package/bin/shifu-harness.js +11 -5
- package/package.json +1 -1
- package/wizard/index.html +6 -5
- package/wizard/wizard.js +34 -7
package/bin/onboarding-ui.js
CHANGED
|
@@ -52,7 +52,7 @@ function renderWelcome({ harness, upgraded = false } = {}, terminal) {
|
|
|
52
52
|
"Automatic work sync: every hour, after Start sync",
|
|
53
53
|
"Saves redacted work summaries to your Shifu account, keeping decisions, lessons and their reasoning available for future conversations.",
|
|
54
54
|
"",
|
|
55
|
-
"
|
|
55
|
+
"File parsing is selected by default for new setups. Review the folder before Start sync, or turn it off. Files are sent to your connected Shifu workspace for parsing into context.",
|
|
56
56
|
"Preparing selected history uses your configured model account and allowance.",
|
|
57
57
|
"",
|
|
58
58
|
"Review your projects, history and optional file folder in local setup, then select Start sync. You can pause local preparation and uploads there anytime. Just ask your assistant to open setup.",
|
package/bin/onboarding.js
CHANGED
|
@@ -25,6 +25,11 @@ function write(fileName, value) {
|
|
|
25
25
|
fs.renameSync(temporary, fileName);
|
|
26
26
|
}
|
|
27
27
|
function fingerprint(config) { return config?.token ? createHash('sha256').update(config.token).digest('hex') : null; }
|
|
28
|
+
function wizardCodeRevision(runtime) {
|
|
29
|
+
const hash = createHash('sha256').update(fs.readFileSync(__filename));
|
|
30
|
+
for (const name of ['index.html', 'wizard.js']) hash.update(fs.readFileSync(path.join(runtime.assets, name)));
|
|
31
|
+
return hash.digest('hex');
|
|
32
|
+
}
|
|
28
33
|
function load(runtime, runId) {
|
|
29
34
|
const state = read(file(runtime));
|
|
30
35
|
if (runId && (!ID.test(runId) || state?.runId !== runId)) throw new Error('This setup run is not available on this device.');
|
|
@@ -159,6 +164,7 @@ async function bodyJSON(request) {
|
|
|
159
164
|
}
|
|
160
165
|
async function startServer(runtime, state, { port=0 }={}) {
|
|
161
166
|
const token = randomBytes(32).toString('hex');
|
|
167
|
+
const codeRevision = wizardCodeRevision(runtime);
|
|
162
168
|
let origin, busy=false;
|
|
163
169
|
const server = http.createServer(async (request,response) => {
|
|
164
170
|
response.setHeader('Cache-Control','no-store'); response.setHeader('Referrer-Policy','no-referrer'); response.setHeader('X-Content-Type-Options','nosniff');
|
|
@@ -209,7 +215,7 @@ async function startServer(runtime, state, { port=0 }={}) {
|
|
|
209
215
|
if(runtime.readConfig()) runtime.pause();
|
|
210
216
|
const result=await runtime.setupLock(()=>runtime.lock(async()=>{const current=load(runtime,state.runId);if(current.authorized && !TERMINAL.has(current.backfillStatus) && choice.abandon!==true)throw new Error('Active selection requires explicit abandonment.');state=initial(runtime);}));
|
|
211
217
|
if(result?.skipped) return send(409,{error:'Another Shifu sync is active.'});
|
|
212
|
-
write(file(runtime,'wizard'),{pid:process.pid,url:origin+'/#'+token,runId:state.runId});
|
|
218
|
+
write(file(runtime,'wizard'),{pid:process.pid,url:origin+'/#'+token,runId:state.runId,codeRevision});
|
|
213
219
|
return send(200,safeStatus(runtime,state));
|
|
214
220
|
}
|
|
215
221
|
if (request.method === 'POST' && url.pathname === '/api/resume') {
|
|
@@ -220,13 +226,13 @@ async function startServer(runtime, state, { port=0 }={}) {
|
|
|
220
226
|
return send(404,{error:'This setup action is unavailable.'});
|
|
221
227
|
} catch (error) {
|
|
222
228
|
// Never relay provider or connector errors: those can contain source text or credentials.
|
|
223
|
-
return send(400,{error: ['Choose a valid history window and both feedback options.','The connection key format is invalid.','Unsupported setup option.','This run has a fixed history selection. Finish it before changing the history window.','Choose a project or explicitly choose all projects.','Choose valid file and schedule settings.','Review the selected scope before starting sync.','The selected scope changed. Review it again before starting sync.','The project list changed. Review the selection again.','Choose an existing readable folder.'].includes(error.message) ? error.message : 'Setup could not finish. Your accepted work is preserved; check the connection and retry.'});
|
|
229
|
+
return send(400,{error: ['Your connection key is saved, but the coding tool connection could not be completed. Open or update your coding tool, choose Edit selection, review it again and retry. You do not need a new key.','Could not reach Shifu to verify the connection key. Nothing was changed.','Shifu rejected this connection key for the selected coding tool. Nothing was changed.','The connection key is invalid.','Choose a valid history window and both feedback options.','The connection key format is invalid.','Unsupported setup option.','This run has a fixed history selection. Finish it before changing the history window.','Choose a project or explicitly choose all projects.','Choose valid file and schedule settings.','Review the selected scope before starting sync.','The selected scope changed. Review it again before starting sync.','The project list changed. Review the selection again.','Choose an existing readable folder.'].includes(error.message) ? error.message : 'Setup could not finish. Your accepted work is preserved; check the connection and retry.'});
|
|
224
230
|
}
|
|
225
231
|
});
|
|
226
232
|
server.requestTimeout=15000;
|
|
227
233
|
await new Promise((resolve,reject) => { server.once('error',reject); server.listen(port,'127.0.0.1',resolve); });
|
|
228
234
|
origin = `http://127.0.0.1:${server.address().port}`;
|
|
229
|
-
return {server,token,origin,url:origin+'/#'+token};
|
|
235
|
+
return {server,token,origin,url:origin+'/#'+token,codeRevision};
|
|
230
236
|
}
|
|
231
237
|
async function main(runtime, options={}) {
|
|
232
238
|
if(options.abandon && !options.fresh)throw new Error('Use --abandon only with --new to explicitly leave an unfinished selection.');
|
|
@@ -248,7 +254,7 @@ async function main(runtime, options={}) {
|
|
|
248
254
|
}
|
|
249
255
|
if (options.serve) {
|
|
250
256
|
const instance=await startServer(runtime,state);
|
|
251
|
-
write(file(runtime,'wizard'),{pid:process.pid,url:instance.url,runId:state.runId});
|
|
257
|
+
write(file(runtime,'wizard'),{pid:process.pid,url:instance.url,runId:state.runId,codeRevision:instance.codeRevision});
|
|
252
258
|
const close=()=>instance.server.close(); process.once('SIGTERM',close); process.once('SIGINT',close);
|
|
253
259
|
// The wizard is temporary; scheduled runner state owns unfinished backfill.
|
|
254
260
|
const idle=setTimeout(()=>instance.server.close(),60*60*1000); idle.unref();
|
|
@@ -257,7 +263,7 @@ async function main(runtime, options={}) {
|
|
|
257
263
|
runtime.copyRunner();
|
|
258
264
|
const endpoint=read(file(runtime,'wizard'));
|
|
259
265
|
let url;
|
|
260
|
-
if (endpoint?.runId === state.runId) {
|
|
266
|
+
if (endpoint?.runId === state.runId && endpoint.codeRevision === wizardCodeRevision(runtime)) {
|
|
261
267
|
try { const u=new URL(endpoint.url); const r=await fetch(u.origin+'/api/status',{headers:{'X-Shifu-Wizard':u.hash.slice(1)},signal:AbortSignal.timeout(1000)}); if(r.ok) url=endpoint.url; } catch {}
|
|
262
268
|
}
|
|
263
269
|
if (!url) {
|
package/bin/shifu-harness.js
CHANGED
|
@@ -70,13 +70,13 @@ const ACTIVE_WORK_FILE_STATUSES = new Set(["queued", "processing", "retrying"]);
|
|
|
70
70
|
const INPUT_FOLDER_README_NAME = "README.md";
|
|
71
71
|
const INPUT_FOLDER_README = `# Shifu sync folder
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
Files in this folder are processed only after you review the folder and select Start sync in Shifu setup. File parsing is selected by default for new setups and can be turned off. Once approved, put work logs, meeting transcripts, and other work documents directly here; Shifu checks them on your selected schedule and keeps claims only when the file supports a contribution you made. Other people's work and meeting action items alone are not your claims.
|
|
74
74
|
|
|
75
75
|
Supported files: .txt, .md, .rtf, and .docx, up to 20 MB each. Subfolders, links, images, and screenshots are not processed. This README is not synced. Unsupported files are counted but never uploaded.
|
|
76
76
|
|
|
77
77
|
Uploading starts a background job. Shifu checks unfinished jobs before the next scheduled or manual sync; a file is complete only when the server reports its final result. A partial result stays pending for retry. A completed file may have published claims, needs-evidence suggestions, or neither; suggestions are not verified claims.
|
|
78
78
|
|
|
79
|
-
Files you add are sent to Shifu for parsing and sanitization;
|
|
79
|
+
Files you add are sent as provided to your connected Shifu workspace for parsing and sanitization. In Shifu Desktop, parsing runs on your Mac; AI processing may send extracted text through Shifu’s gateway. In the web app, files are uploaded to your hosted workspace. Only place files here if you want them processed. For named meeting transcripts, set your display name in Shifu Profile so your speaker turns can be matched.
|
|
80
80
|
`;
|
|
81
81
|
|
|
82
82
|
function configRoot() {
|
|
@@ -3930,13 +3930,18 @@ function onboardingRuntime(harness, { prepareNative } = {}) {
|
|
|
3930
3930
|
const scopeStatus = () => {
|
|
3931
3931
|
const config = readJSON(configPath(harness), null);
|
|
3932
3932
|
const authorization = controls.validAuthorization(config);
|
|
3933
|
+
const desktop = readDesktopProfile(configRoot());
|
|
3934
|
+
const apiUrl = requireSafeApiUrl(config?.apiUrl || option("--api-url") || process.env.SHIFU_API_URL || DEFAULT_API_URL);
|
|
3935
|
+
let connectionKeyUrl;
|
|
3936
|
+
if (desktop?.connectionKeyLinks === true) connectionKeyUrl = `shifu://sources/coding-sessions?${new URLSearchParams({ harness, profile: desktop.namespace, connection: 'key' })}`;
|
|
3937
|
+
else if (!desktop && apiUrl === DEFAULT_API_URL) connectionKeyUrl = `https://app.useshifu.com/#/sources/coding-sessions?${new URLSearchParams({ harness, connection: 'key' })}`;
|
|
3933
3938
|
let projects = [], projectDiscoveryError;
|
|
3934
3939
|
try { projects = controls.projectCatalog(localSessions(harness)); }
|
|
3935
3940
|
catch { projectDiscoveryError = "Local project history is unavailable. Check the coding tool installation before selecting history."; }
|
|
3936
3941
|
const current = controls.projectIdentity({ workspace: process.cwd() }).ref;
|
|
3937
3942
|
const projectRefs = authorization?.projectRefs || (projects.some((item) => item.ref === current) ? [current] : projects.length === 1 ? [projects[0].ref] : []);
|
|
3938
|
-
return { runnerVersion: RUNNER_VERSION, projects, projectDiscoveryError, projectScope: authorization?.projectScope || "selected", projectRefs,
|
|
3939
|
-
filesEnabled: authorization
|
|
3943
|
+
return { runnerVersion: RUNNER_VERSION, connectionKeyUrl, projects, projectDiscoveryError, projectScope: authorization?.projectScope || "selected", projectRefs,
|
|
3944
|
+
filesEnabled: authorization ? authorization.filesEnabled : true, inputFolder: config?.inputFolder || defaultInputFolder(),
|
|
3940
3945
|
paused: config?.syncPaused === true, scopeReviewRequired: !authorization,
|
|
3941
3946
|
syncPolicy: authorization ? normalizedPolicy(config) : { intervalHours: 1, approvalMode: "automatic", scheduleEnabled: true } };
|
|
3942
3947
|
};
|
|
@@ -3995,7 +4000,8 @@ function onboardingRuntime(harness, { prepareNative } = {}) {
|
|
|
3995
4000
|
await connect(harness, async () => key, choices.preferences, true);
|
|
3996
4001
|
generation = readConfig(harness).syncGeneration || 0;
|
|
3997
4002
|
}
|
|
3998
|
-
await setupMcp(harness);
|
|
4003
|
+
try { await setupMcp(harness); }
|
|
4004
|
+
catch { throw new Error("Your connection key is saved, but the coding tool connection could not be completed. Open or update your coding tool, choose Edit selection, review it again and retry. You do not need a new key."); }
|
|
3999
4005
|
const config = readConfig(harness);
|
|
4000
4006
|
if ((config.syncGeneration || 0) !== generation) throw new Error("Setup was paused or changed. Review and start again.");
|
|
4001
4007
|
if (choices.scope.filesEnabled) prepareInputFolder(choices.scope.inputFolder, choices.scope.inputFolder === defaultInputFolder());
|
package/package.json
CHANGED
package/wizard/index.html
CHANGED
|
@@ -25,14 +25,15 @@
|
|
|
25
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
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
27
|
<fieldset class="option"><legend>Files</legend>
|
|
28
|
-
<label><input id="files-enabled" type="checkbox">
|
|
29
|
-
<div id="folder-choice"
|
|
30
|
-
<p class="example">
|
|
28
|
+
<label><input id="files-enabled" type="checkbox" checked> Parse work files into context</label>
|
|
29
|
+
<div id="folder-choice"><label class="label" for="input-folder">Folder to read</label><input id="input-folder" type="text" autocomplete="off" spellcheck="false"><p class="example">Supported files are sent as provided to your connected Shifu workspace for parsing into context. The review shows the resolved folder and file count.</p></div>
|
|
30
|
+
<p class="example">In Shifu Desktop, parsing runs on your Mac; AI processing may use Shifu’s gateway. In the web app, files are uploaded to your hosted workspace.</p>
|
|
31
|
+
<p class="example">Review the folder before Start sync. Uncheck this option to leave files out. A folder change requires another review.</p>
|
|
31
32
|
</fieldset>
|
|
32
33
|
<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
34
|
<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
35
|
<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
|
|
36
|
+
<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 Shifu connection key"><p><a id="get-key" hidden>Get connection key</a></p><ol class="note"><li>Select Get connection key to open this tool in Shifu, then select Create & copy key (or Copy connection key).</li><li>Come back to this window and paste the key above. Then review your selection and select Start sync.</li><li>If the link does not open, return to the Shifu app or dashboard where you started setup, open Sources, and choose this coding tool.</li></ol><p class="note">Shifu creates this key to connect your coding tool to your workspace. It is not a Google password or a model provider API key. Keep it out of chat.</p></div>
|
|
36
37
|
<div id="saved-key" class="note" hidden>Your saved connection is ready to use.
|
|
37
38
|
<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
39
|
</div>
|
|
@@ -52,5 +53,5 @@
|
|
|
52
53
|
<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
54
|
</div>
|
|
54
55
|
<p id="message" role="status" aria-live="polite"></p>
|
|
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
|
|
56
|
+
<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 included by default in a new setup and can be turned off. After you review the folder and select Start sync, supported files are sent as provided to the connected Shifu workspace for parsing into context. 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>
|
|
56
57
|
</section></main></body></html>
|
package/wizard/wizard.js
CHANGED
|
@@ -32,6 +32,29 @@
|
|
|
32
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
33
|
}
|
|
34
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
|
+
|
|
35
58
|
function settings(value) {
|
|
36
59
|
el('history').value = ['0', '15', '30'].includes(String(value.historyDays)) ? String(value.historyDays) : 'custom';
|
|
37
60
|
el('custom').value = value.historyDays || 7;
|
|
@@ -59,12 +82,13 @@
|
|
|
59
82
|
el('project-list').replaceChildren(...projects);
|
|
60
83
|
projectScope();
|
|
61
84
|
el('schedule-enabled').checked = value.scheduleEnabled ?? value.syncPolicy?.scheduleEnabled ?? true;
|
|
62
|
-
el('files-enabled').checked = value.filesEnabled
|
|
85
|
+
el('files-enabled').checked = value.filesEnabled ?? true;
|
|
63
86
|
el('input-folder').value = value.inputFolder || '';
|
|
64
87
|
el('folder-choice').hidden = !el('files-enabled').checked;
|
|
65
88
|
el('key-entry').hidden = value.configured;
|
|
66
89
|
el('saved-key').hidden = !value.configured;
|
|
67
90
|
el('harness').textContent = names[value.harness] || value.harness;
|
|
91
|
+
connectionKeyLink(value);
|
|
68
92
|
}
|
|
69
93
|
|
|
70
94
|
function syncLabel(value) {
|
|
@@ -114,6 +138,11 @@
|
|
|
114
138
|
el('settings').addEventListener('submit', async (event) => {
|
|
115
139
|
event.preventDefault();
|
|
116
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
|
+
}
|
|
117
146
|
const historyDays = el('history').value === 'custom' ? Number(el('custom').value) : Number(el('history').value);
|
|
118
147
|
if (!Number.isSafeInteger(historyDays) || historyDays < 0 || historyDays > 36500 || (el('history').value === 'custom' && historyDays === 0)) {
|
|
119
148
|
el('message').textContent = 'Choose a positive whole number of custom days, or Start from now.';
|
|
@@ -128,7 +157,7 @@
|
|
|
128
157
|
const filesEnabled = el('files-enabled').checked;
|
|
129
158
|
const inputFolder = el('input-folder').value.trim();
|
|
130
159
|
if (filesEnabled && !inputFolder) {
|
|
131
|
-
el('message').textContent = 'Choose the folder whose files you want
|
|
160
|
+
el('message').textContent = 'Choose the folder whose files you want parsed into context.';
|
|
132
161
|
return;
|
|
133
162
|
}
|
|
134
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' };
|
|
@@ -157,23 +186,21 @@
|
|
|
157
186
|
} catch (error) { el('message').textContent = error.message; }
|
|
158
187
|
finally { busy = false; el('review-button').disabled = false; }
|
|
159
188
|
});
|
|
160
|
-
el('edit').addEventListener('click', () => { choices = undefined; el('review').hidden = true; el('welcome').hidden = false; el('settings').hidden = false; });
|
|
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; });
|
|
161
190
|
el('apply').addEventListener('click', async () => {
|
|
162
191
|
if (busy || !choices?.scopeRevision) return;
|
|
163
192
|
busy = true;
|
|
164
193
|
el('apply').disabled = true;
|
|
165
194
|
el('message').textContent = 'Verifying your connection and starting the reviewed selection…';
|
|
166
195
|
const key = el('key').value.trim();
|
|
167
|
-
el('key').value = '';
|
|
168
196
|
try {
|
|
169
197
|
state = await api('apply', { ...choices, ...(!state.configured ? { key } : {}) });
|
|
198
|
+
el('key').value = '';
|
|
170
199
|
renderProgress();
|
|
171
200
|
el('message').textContent = '';
|
|
172
201
|
} catch (error) {
|
|
173
|
-
|
|
202
|
+
try { state = await api('status'); } catch { /* Keep the reviewed selection available during a connection failure. */ }
|
|
174
203
|
el('message').textContent = error.message;
|
|
175
|
-
el('review').hidden = true;
|
|
176
|
-
el('welcome').hidden = false; el('settings').hidden = false;
|
|
177
204
|
} finally { busy = false; el('apply').disabled = false; }
|
|
178
205
|
});
|
|
179
206
|
el('change').addEventListener('click', async () => {
|