pi-sdk-web 0.4.1 → 0.4.3
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/dist/server.js +13 -2
- package/dist/static/app.js +87 -21
- package/dist/static/style.css +13 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -433,6 +433,9 @@ export class PiWebServer {
|
|
|
433
433
|
if (!command)
|
|
434
434
|
throw new Error("Missing 'command'");
|
|
435
435
|
const excludeFromContext = data.excludeFromContext === true;
|
|
436
|
+
// Correlation id for streaming: Pi emits bash_execution_update
|
|
437
|
+
// events carrying this id while the command runs (onChunk).
|
|
438
|
+
const id = typeof data.id === "string" && data.id ? data.id : `bash-${Date.now()}`;
|
|
436
439
|
// Let extensions intercept/enhance the command (same as Pi RPC/TUI):
|
|
437
440
|
// user_bash handlers may provide a result or operations for execution.
|
|
438
441
|
const eventResult = await this.session.extensionRunner.emitUserBash({
|
|
@@ -443,14 +446,15 @@ export class PiWebServer {
|
|
|
443
446
|
});
|
|
444
447
|
if (eventResult?.result) {
|
|
445
448
|
this.session.recordBashResult(command, eventResult.result, { excludeFromContext });
|
|
446
|
-
this.broadcast({ type: "bash_result", command, data: eventResult.result });
|
|
449
|
+
this.broadcast({ type: "bash_result", id, command, data: eventResult.result });
|
|
447
450
|
break;
|
|
448
451
|
}
|
|
449
452
|
const result = await this.session.executeBash(command, undefined, {
|
|
450
453
|
excludeFromContext,
|
|
454
|
+
id,
|
|
451
455
|
operations: eventResult?.operations,
|
|
452
456
|
});
|
|
453
|
-
this.broadcast({ type: "bash_result", command, data: result });
|
|
457
|
+
this.broadcast({ type: "bash_result", id, command, data: result });
|
|
454
458
|
break;
|
|
455
459
|
}
|
|
456
460
|
case "cycle_model":
|
|
@@ -530,11 +534,13 @@ export class PiWebServer {
|
|
|
530
534
|
case "set_scoped_models": {
|
|
531
535
|
const models = Array.isArray(data.models) ? data.models : [];
|
|
532
536
|
const resolved = [];
|
|
537
|
+
const enabledIds = [];
|
|
533
538
|
for (const m of models) {
|
|
534
539
|
const provider = m.provider;
|
|
535
540
|
const modelId = m.modelId;
|
|
536
541
|
if (!provider || !modelId)
|
|
537
542
|
continue;
|
|
543
|
+
enabledIds.push(`${provider}/${modelId}`);
|
|
538
544
|
const model = this.session.modelRuntime
|
|
539
545
|
.getAvailableSnapshot()
|
|
540
546
|
.find((x) => x.provider === provider && x.id === modelId);
|
|
@@ -542,6 +548,11 @@ export class PiWebServer {
|
|
|
542
548
|
resolved.push({ model, thinkingLevel: m.thinkingLevel });
|
|
543
549
|
}
|
|
544
550
|
this.session.setScopedModels(resolved);
|
|
551
|
+
// Persist only on explicit save (TUI Ctrl+S onPersist); toggling is
|
|
552
|
+
// session-only (onChange) and must not rewrite settings.json.
|
|
553
|
+
if (data.persist === true) {
|
|
554
|
+
this.session.settingsManager.setEnabledModels(enabledIds.length > 0 ? enabledIds : undefined);
|
|
555
|
+
}
|
|
545
556
|
this.broadcastState();
|
|
546
557
|
break;
|
|
547
558
|
}
|
package/dist/static/app.js
CHANGED
|
@@ -213,6 +213,10 @@ class PiWebClient {
|
|
|
213
213
|
|
|
214
214
|
// Tool call rendering state: map toolCallId -> element
|
|
215
215
|
this.toolEls = new Map();
|
|
216
|
+
// Bash streaming blocks: map bash id -> element
|
|
217
|
+
this.bashEls = new Map();
|
|
218
|
+
this.pendingBashId = null;
|
|
219
|
+
this.pendingBashCommand = '';
|
|
216
220
|
// Tool execution timers: map toolCallId -> interval id
|
|
217
221
|
this.toolTimers = new Map();
|
|
218
222
|
|
|
@@ -913,6 +917,14 @@ class PiWebClient {
|
|
|
913
917
|
// Extension custom entries (e.g. magic-context /ctx-status) arrive live
|
|
914
918
|
this.renderEntry(ev.entry);
|
|
915
919
|
break;
|
|
920
|
+
case 'bash_execution_update':
|
|
921
|
+
// Streaming bash output (e.g. !! running a long-lived program).
|
|
922
|
+
// Append the delta to the matching tool block in real time; the
|
|
923
|
+
// final bash_result finalizes it (exit code, truncation info).
|
|
924
|
+
if (typeof ev.delta === 'string' && ev.delta.length > 0) {
|
|
925
|
+
this.appendBashChunk(ev.id, ev.delta);
|
|
926
|
+
}
|
|
927
|
+
break;
|
|
916
928
|
default:
|
|
917
929
|
break;
|
|
918
930
|
}
|
|
@@ -1310,11 +1322,39 @@ class PiWebClient {
|
|
|
1310
1322
|
this.toolEls.delete(ev.toolCallId);
|
|
1311
1323
|
}
|
|
1312
1324
|
|
|
1325
|
+
appendBashChunk(id, delta) {
|
|
1326
|
+
// Find or create the tool block for this bash run (keyed by the id the
|
|
1327
|
+
// client sent with the bash message, e.g. bash-<timestamp>).
|
|
1328
|
+
const key = id || this.pendingBashId;
|
|
1329
|
+
if (!key) return;
|
|
1330
|
+
let div = this.bashEls.get(key);
|
|
1331
|
+
if (!div) {
|
|
1332
|
+
div = this.createToolBlock('bash', { command: this.pendingBashCommand || '' }, key);
|
|
1333
|
+
div.className = 'tool-block pending';
|
|
1334
|
+
this.bashEls.set(key, div);
|
|
1335
|
+
}
|
|
1336
|
+
// Append the delta to the full text and re-apply the preview (respects
|
|
1337
|
+
// the collapsed/expanded state of the block).
|
|
1338
|
+
const full = (div.dataset.fullOutput || '') + delta;
|
|
1339
|
+
div.dataset.fullOutput = full;
|
|
1340
|
+
this.applyToolPreview(div);
|
|
1341
|
+
if (this.wasAtBottom()) this.scrollToBottom();
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1313
1344
|
renderBashResult(data) {
|
|
1314
|
-
const
|
|
1345
|
+
const id = data.id;
|
|
1315
1346
|
const result = data.data || {};
|
|
1347
|
+
let div = (id && this.bashEls.get(id)) || null;
|
|
1348
|
+
if (!div) {
|
|
1349
|
+
div = this.createToolBlock('bash', { command: data.command }, id || 'bash-' + Date.now());
|
|
1350
|
+
if (id) this.bashEls.set(id, div);
|
|
1351
|
+
}
|
|
1316
1352
|
const output = result.output || '';
|
|
1317
|
-
|
|
1353
|
+
// Streaming blocks have the full text already (appended incrementally);
|
|
1354
|
+
// for non-streaming runs set it now.
|
|
1355
|
+
if (!(div.dataset.fullOutput && div.dataset.fullOutput.length > 0)) {
|
|
1356
|
+
this.setToolOutput(div, output);
|
|
1357
|
+
}
|
|
1318
1358
|
|
|
1319
1359
|
const isError = result.exitCode !== undefined && result.exitCode !== 0;
|
|
1320
1360
|
div.className = isError ? 'tool-block error' : 'tool-block success';
|
|
@@ -1334,6 +1374,10 @@ class PiWebClient {
|
|
|
1334
1374
|
}
|
|
1335
1375
|
|
|
1336
1376
|
this.scrollToBottom();
|
|
1377
|
+
// Streaming done: drop the block from the live map (no more deltas).
|
|
1378
|
+
if (id) this.bashEls.delete(id);
|
|
1379
|
+
this.pendingBashId = null;
|
|
1380
|
+
this.pendingBashCommand = '';
|
|
1337
1381
|
}
|
|
1338
1382
|
|
|
1339
1383
|
resultText(result) {
|
|
@@ -1420,7 +1464,10 @@ class PiWebClient {
|
|
|
1420
1464
|
const isExcluded = text.startsWith('!!');
|
|
1421
1465
|
const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
|
|
1422
1466
|
if (command) {
|
|
1423
|
-
|
|
1467
|
+
const id = 'bash-' + Date.now();
|
|
1468
|
+
this.send({ type: 'bash', command: command, excludeFromContext: isExcluded, id: id });
|
|
1469
|
+
this.pendingBashId = id;
|
|
1470
|
+
this.pendingBashCommand = command;
|
|
1424
1471
|
}
|
|
1425
1472
|
} else if (text.startsWith('/')) {
|
|
1426
1473
|
// Skill commands (/skill:name args) go through prompt expansion in Pi -
|
|
@@ -1872,6 +1919,7 @@ class PiWebClient {
|
|
|
1872
1919
|
this.scopedModelsAll = [];
|
|
1873
1920
|
this.scopedModelsSelected = new Set();
|
|
1874
1921
|
this.scopedModelsData = null;
|
|
1922
|
+
this.scopedModelsSaved = true;
|
|
1875
1923
|
this.modalList.innerHTML = '<div class="modal-message">Loading models...</div>';
|
|
1876
1924
|
this.send({ type: 'get_scoped_models' });
|
|
1877
1925
|
}
|
|
@@ -1910,11 +1958,13 @@ class PiWebClient {
|
|
|
1910
1958
|
})
|
|
1911
1959
|
.join('');
|
|
1912
1960
|
this.modalList.innerHTML =
|
|
1913
|
-
rows +
|
|
1914
1961
|
`<div class="modal-actions">
|
|
1915
|
-
<button class="modal-btn ok-btn">
|
|
1962
|
+
<button class="modal-btn ok-btn">Save</button>
|
|
1916
1963
|
<button class="modal-btn cancel-btn">Cancel</button>
|
|
1917
|
-
|
|
1964
|
+
<span class="modal-hint"></span>
|
|
1965
|
+
</div>` +
|
|
1966
|
+
rows;
|
|
1967
|
+
this.updateScopedUnsavedHint();
|
|
1918
1968
|
this.modalList.querySelectorAll('.scoped-model').forEach((el) => {
|
|
1919
1969
|
el.addEventListener('click', () => {
|
|
1920
1970
|
const key = el.dataset.key;
|
|
@@ -1923,26 +1973,14 @@ class PiWebClient {
|
|
|
1923
1973
|
const checked = this.scopedModelsSelected.has(key);
|
|
1924
1974
|
el.classList.toggle('selected', checked);
|
|
1925
1975
|
el.querySelector('.modal-check').textContent = checked ? '☑' : '☐';
|
|
1976
|
+
// TUI onChange: apply to session (memory, persist=false); Save persists.
|
|
1977
|
+
this.applyScopedModels(false);
|
|
1926
1978
|
});
|
|
1927
1979
|
});
|
|
1928
1980
|
const okBtn = this.modalList.querySelector('.ok-btn');
|
|
1929
1981
|
if (okBtn) {
|
|
1930
1982
|
okBtn.addEventListener('click', () => {
|
|
1931
|
-
|
|
1932
|
-
for (const m of this.scopedModelsAll || []) {
|
|
1933
|
-
const key = `${m.provider}/${m.id}`;
|
|
1934
|
-
if (this.scopedModelsSelected.has(key)) {
|
|
1935
|
-
const scopedInfo = (this.scopedModelsData?.scoped || []).find(
|
|
1936
|
-
(s) => `${s.provider}/${s.id}` === key,
|
|
1937
|
-
);
|
|
1938
|
-
models.push({
|
|
1939
|
-
provider: m.provider,
|
|
1940
|
-
modelId: m.id,
|
|
1941
|
-
thinkingLevel: scopedInfo?.thinkingLevel,
|
|
1942
|
-
});
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
this.send({ type: 'set_scoped_models', models });
|
|
1983
|
+
this.applyScopedModels(true);
|
|
1946
1984
|
this.closeModal();
|
|
1947
1985
|
});
|
|
1948
1986
|
}
|
|
@@ -1950,6 +1988,34 @@ class PiWebClient {
|
|
|
1950
1988
|
if (cancelBtn) cancelBtn.addEventListener('click', () => this.closeModal());
|
|
1951
1989
|
}
|
|
1952
1990
|
|
|
1991
|
+
/** Send the current selection to the server (persist=false = memory only). */
|
|
1992
|
+
applyScopedModels(persist) {
|
|
1993
|
+
const models = [];
|
|
1994
|
+
for (const m of this.scopedModelsAll || []) {
|
|
1995
|
+
const key = `${m.provider}/${m.id}`;
|
|
1996
|
+
if (this.scopedModelsSelected.has(key)) {
|
|
1997
|
+
const scopedInfo = (this.scopedModelsData?.scoped || []).find(
|
|
1998
|
+
(s) => `${s.provider}/${s.id}` === key,
|
|
1999
|
+
);
|
|
2000
|
+
models.push({
|
|
2001
|
+
provider: m.provider,
|
|
2002
|
+
modelId: m.id,
|
|
2003
|
+
thinkingLevel: scopedInfo?.thinkingLevel,
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
this.send({ type: 'set_scoped_models', models, persist: persist });
|
|
2008
|
+
this.scopedModelsSaved = persist;
|
|
2009
|
+
this.updateScopedUnsavedHint();
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
updateScopedUnsavedHint() {
|
|
2013
|
+
const hint = this.modalList.querySelector('.modal-hint');
|
|
2014
|
+
if (!hint) return;
|
|
2015
|
+
hint.textContent = this.scopedModelsSaved ? '' : ' (unsaved)';
|
|
2016
|
+
hint.className = this.scopedModelsSaved ? 'modal-hint' : 'modal-hint unsaved';
|
|
2017
|
+
}
|
|
2018
|
+
|
|
1953
2019
|
// ------------------------------------------------------------------
|
|
1954
2020
|
// Slash command menu
|
|
1955
2021
|
// ------------------------------------------------------------------
|
package/dist/static/style.css
CHANGED
|
@@ -859,6 +859,19 @@ body {
|
|
|
859
859
|
gap: 8px;
|
|
860
860
|
justify-content: flex-end;
|
|
861
861
|
margin-top: 10px;
|
|
862
|
+
margin-bottom: 10px;
|
|
863
|
+
align-items: center;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
.modal-hint {
|
|
867
|
+
font-size: 12px;
|
|
868
|
+
color: var(--dim);
|
|
869
|
+
margin-left: auto;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.modal-hint.unsaved {
|
|
873
|
+
color: var(--warning);
|
|
874
|
+
font-weight: 600;
|
|
862
875
|
}
|
|
863
876
|
|
|
864
877
|
.modal-btn {
|