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 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
  }
@@ -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 div = this.createToolBlock('bash', { command: data.command }, 'bash-' + Date.now());
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
- this.setToolOutput(div, output);
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
- this.send({ type: 'bash', command: command, excludeFromContext: isExcluded });
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">Apply</button>
1962
+ <button class="modal-btn ok-btn">Save</button>
1916
1963
  <button class="modal-btn cancel-btn">Cancel</button>
1917
- </div>`;
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
- const models = [];
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
  // ------------------------------------------------------------------
@@ -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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sdk-web",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Browser Web access for Pi (AI coding agent) via the Pi SDK - standalone module, zero modification to Pi itself",
5
5
  "type": "module",
6
6
  "bin": {