pilotswarm 0.5.1 → 0.5.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.
Files changed (36) hide show
  1. package/mcp/dist/src/session-id.d.ts +13 -0
  2. package/mcp/dist/src/session-id.d.ts.map +1 -0
  3. package/mcp/dist/src/session-id.js +15 -0
  4. package/mcp/dist/src/session-id.js.map +1 -0
  5. package/mcp/dist/src/tools/artifacts.d.ts.map +1 -1
  6. package/mcp/dist/src/tools/artifacts.js +5 -4
  7. package/mcp/dist/src/tools/artifacts.js.map +1 -1
  8. package/mcp/dist/src/tools/observability.d.ts.map +1 -1
  9. package/mcp/dist/src/tools/observability.js +2 -1
  10. package/mcp/dist/src/tools/observability.js.map +1 -1
  11. package/mcp/dist/src/tools/sessions.d.ts.map +1 -1
  12. package/mcp/dist/src/tools/sessions.js +9 -8
  13. package/mcp/dist/src/tools/sessions.js.map +1 -1
  14. package/mcp/dist/src/tools/turn-control.d.ts.map +1 -1
  15. package/mcp/dist/src/tools/turn-control.js +5 -4
  16. package/mcp/dist/src/tools/turn-control.js.map +1 -1
  17. package/package.json +3 -3
  18. package/tui/plugins/.mcp.json +1 -7
  19. package/tui/src/app.js +12 -1
  20. package/tui/src/node-sdk-transport.js +17 -0
  21. package/ui/core/src/controller.js +62 -11
  22. package/ui/core/src/history.js +34 -0
  23. package/ui/core/src/reducer.js +87 -0
  24. package/ui/core/src/selectors.js +281 -174
  25. package/ui/core/src/state.js +15 -0
  26. package/ui/react/src/components.js +27 -8
  27. package/ui/react/src/web-app.js +83 -38
  28. package/web/dist/assets/index-BKai0rVp.js +24 -0
  29. package/web/dist/assets/index-BRbv6TsU.css +1 -0
  30. package/web/dist/assets/pilotswarm-PUVWBejV.js +90 -0
  31. package/web/dist/assets/{react-CRYBfNR4.js → react-BouHbnso.js} +1 -1
  32. package/web/dist/index.html +4 -4
  33. package/web/runtime.js +23 -2
  34. package/web/dist/assets/index-BmqU2WsR.js +0 -24
  35. package/web/dist/assets/index-D2-7fi56.css +0 -1
  36. package/web/dist/assets/pilotswarm-cIaYAE9e.js +0 -90
@@ -10,6 +10,7 @@ import {
10
10
  selectChatPaneChrome,
11
11
  selectLiveActivityLines,
12
12
  selectChatLines,
13
+ selectOutboxOverlayLines,
13
14
  selectActivityPane,
14
15
  selectArtifactUploadModal,
15
16
  selectArtifactPickerModal,
@@ -317,6 +318,7 @@ const ChatPane = React.memo(function ChatPane({ controller, width, height, frame
317
318
  activeSessionId,
318
319
  activeSession: activeSessionId ? state.sessions.byId[activeSessionId] || null : null,
319
320
  activeHistory: activeSessionId ? state.history.bySessionId.get(activeSessionId) || null : null,
321
+ activeOutbox: activeSessionId ? state.outbox?.bySessionId?.[activeSessionId] || null : null,
320
322
  branding: state.branding,
321
323
  connectionError: state.connection.error,
322
324
  connectionMode: state.connection.mode,
@@ -347,11 +349,21 @@ const ChatPane = React.memo(function ChatPane({ controller, width, height, frame
347
349
  history: {
348
350
  bySessionId: historyMap,
349
351
  },
352
+ // The queued-prompt overlay reads state.outbox; without it here the
353
+ // TUI ChatPane's synthetic selectorState always saw an empty outbox
354
+ // and rendered no "queued prompts: N" even though the item was
355
+ // durably queued in the real store.
356
+ outbox: {
357
+ bySessionId: chatView.activeSessionId && chatView.activeOutbox
358
+ ? { [chatView.activeSessionId]: chatView.activeOutbox }
359
+ : {},
360
+ },
350
361
  };
351
362
  }, [
352
363
  chatView.activeHistory,
353
364
  chatView.activeSessionId,
354
365
  chatView.activeSession,
366
+ chatView.activeOutbox,
355
367
  chatView.branding,
356
368
  chatView.chatViewMode,
357
369
  chatView.connectionError,
@@ -383,13 +395,19 @@ const ChatPane = React.memo(function ChatPane({ controller, width, height, frame
383
395
  () => (startupError ? [] : selectLiveActivityLines(selectorState, { spinnerFrame, maxWidth: contentWidth })),
384
396
  [selectorState, startupError, spinnerFrame, contentWidth],
385
397
  );
386
- // Concat the isolated live-activity block so the spinner tick only recomputes
387
- // the small block, not the whole transcript.
388
- const chatLines = React.useMemo(
389
- () => (liveActivityLines.length > 0
390
- ? [...elements, [{ text: "", color: null }], ...liveActivityLines]
391
- : elements),
392
- [elements, liveActivityLines],
398
+ const outboxLines = React.useMemo(
399
+ () => (startupError ? [] : selectOutboxOverlayLines(selectorState, contentWidth)),
400
+ [selectorState, startupError, contentWidth],
401
+ );
402
+ // The queued-prompt overlay and the live "Working" strip are pinned in the
403
+ // bottom-sticky region (outbox above, strip at the very bottom), so they
404
+ // stay put at the foot of the pane instead of scrolling inline with the
405
+ // transcript — matching the portal. The transcript itself stays crisp.
406
+ const bottomStickyLines = React.useMemo(
407
+ () => (outboxLines.length > 0 || liveActivityLines.length > 0
408
+ ? [...outboxLines, ...liveActivityLines]
409
+ : []),
410
+ [outboxLines, liveActivityLines],
393
411
  );
394
412
 
395
413
  return React.createElement(platform.Panel, {
@@ -399,7 +417,8 @@ const ChatPane = React.memo(function ChatPane({ controller, width, height, frame
399
417
  focused: chatView.focused,
400
418
  width,
401
419
  height,
402
- lines: chatLines,
420
+ lines: elements,
421
+ bottomStickyLines,
403
422
  scrollOffset: chatView.chatScroll,
404
423
  scrollMode: "bottom",
405
424
  paneId: "chat",
@@ -431,13 +431,19 @@ function compactTitleRuns(title, maxWidth = 40) {
431
431
  return [{ text: text.length > maxWidth ? `${text.slice(0, maxWidth - 1)}…` : text, color: "white", bold: true }];
432
432
  }
433
433
  const compactRuns = [];
434
+ // Gray runs are dropped on tight widths as decoration (ids, badges) —
435
+ // EXCEPT pure separators (" · "). Dropping those fuses adjacent values:
436
+ // "running · gpt-5.4 · ctx 33%" became "runninggpt-5.4ctx…".
437
+ const isSeparator = (run) => /^[\s·—-]+$/.test(String(run?.text || ""));
434
438
  let remaining = Math.max(8, maxWidth);
435
439
  for (const run of title) {
436
440
  if (remaining <= 0) break;
437
441
  const color = run?.color;
438
- if (color === "gray" && compactRuns.length > 0) continue;
442
+ if (color === "gray" && compactRuns.length > 0 && !isSeparator(run)) continue;
439
443
  const text = String(run?.text || "");
440
444
  if (!text) continue;
445
+ // Don't start a separator we can't follow with content.
446
+ if (isSeparator(run) && remaining < text.length + 2) break;
441
447
  const chunk = text.length > remaining && remaining > 1
442
448
  ? `${text.slice(0, remaining - 1)}…`
443
449
  : text.slice(0, remaining);
@@ -445,6 +451,10 @@ function compactTitleRuns(title, maxWidth = 40) {
445
451
  compactRuns.push({ ...run, text: chunk });
446
452
  remaining -= chunk.length;
447
453
  }
454
+ // Never end on a dangling separator.
455
+ while (compactRuns.length > 0 && isSeparator(compactRuns[compactRuns.length - 1])) {
456
+ compactRuns.pop();
457
+ }
448
458
  return compactRuns.length > 0 ? compactRuns : title;
449
459
  }
450
460
 
@@ -1715,6 +1725,7 @@ function PortalSequenceLines({ lines, theme, completionByTurn }) {
1715
1725
  );
1716
1726
  }
1717
1727
 
1728
+
1718
1729
  function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, lines, stickyLines = [], bottomStickyLines = [], scrollOffset = 0, scrollMode = "top", paneKey, controller, className = "", panelClassName = "", topContent = null, bottomContent = null, structuredBlocks = false, stickyBottom = false, renderBody = null }) {
1719
1730
  const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
1720
1731
  const theme = getTheme(themeId);
@@ -1736,11 +1747,26 @@ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, l
1736
1747
  });
1737
1748
  }, []);
1738
1749
 
1750
+ // Edge fades follow real overflow: fade the TOP only when content is
1751
+ // clipped above (scrolled down) and the BOTTOM only when clipped below
1752
+ // (scrolled up). With content that fits, neither fades — the first and
1753
+ // last lines stay crisp.
1754
+ const [scrollShadow, setScrollShadow] = React.useState({ up: false, down: false });
1755
+ const updateScrollShadow = React.useCallback((el) => {
1756
+ if (!el) return;
1757
+ const up = el.scrollHeight - el.scrollTop - el.clientHeight > 4;
1758
+ const down = el.scrollTop > 4;
1759
+ setScrollShadow((cur) => (cur.up === up && cur.down === down ? cur : { up, down }));
1760
+ }, []);
1739
1761
  const handleBodyScroll = React.useCallback((event) => {
1740
1762
  onScroll();
1763
+ updateScrollShadow(event.currentTarget);
1741
1764
  if (!preserveHorizontalScroll || syncingHorizontalRef.current) return;
1742
1765
  syncScrollLeft(event.currentTarget, stickyRef.current);
1743
- }, [onScroll, preserveHorizontalScroll, syncScrollLeft]);
1766
+ }, [onScroll, preserveHorizontalScroll, syncScrollLeft, updateScrollShadow]);
1767
+ React.useEffect(() => {
1768
+ updateScrollShadow(ref.current);
1769
+ });
1744
1770
 
1745
1771
  const handleStickyScroll = React.useCallback((event) => {
1746
1772
  if (!preserveHorizontalScroll || syncingHorizontalRef.current) return;
@@ -1758,7 +1784,7 @@ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, l
1758
1784
  normalizedSticky.map((line, index) => React.createElement(Line, { key: `sticky:${index}`, line, theme })),
1759
1785
  )
1760
1786
  : null,
1761
- React.createElement("div", { ref, className: `ps-scroll-panel ${className}`.trim(), onScroll: handleBodyScroll, onWheel, onTouchStart, onTouchMove, onTouchEnd, onTouchCancel: onTouchEnd },
1787
+ React.createElement("div", { ref, className: `ps-scroll-panel ${className}${scrollShadow.down ? " is-scrolled-down" : ""}${scrollShadow.up ? " is-scrolled-up" : ""}`.trim(), onScroll: handleBodyScroll, onWheel, onTouchStart, onTouchMove, onTouchEnd, onTouchCancel: onTouchEnd },
1762
1788
  typeof renderBody === "function"
1763
1789
  ? renderBody(normalizedLines, theme)
1764
1790
  : structuredBlocks
@@ -1783,25 +1809,27 @@ function SessionRowContent({ row, theme, structured = false }) {
1783
1809
  : row.text;
1784
1810
  }
1785
1811
 
1786
- const hasMeta = Array.isArray(row.metaRuns) && row.metaRuns.length > 0;
1787
- const hasBadges = Array.isArray(row.badgeRuns) && row.badgeRuns.length > 0;
1788
- const hasSelectedMeta = row.active && Array.isArray(row.selectedMetaRuns) && row.selectedMetaRuns.length > 0;
1812
+ // Dense row: the title takes one line (clamped by CSS) and the context %
1813
+ // is pinned to the right. The full id · time · model · ctx detail unfolds
1814
+ // only under the selected row.
1815
+ const ctxRuns = Array.isArray(row.ctxRuns) ? row.ctxRuns : [];
1816
+ const detailRuns = Array.isArray(row.detailRuns)
1817
+ ? row.detailRuns
1818
+ : (Array.isArray(row.selectedMetaRuns) ? row.selectedMetaRuns : []);
1819
+ const hasCtx = ctxRuns.length > 0;
1820
+ const hasDetail = row.active && detailRuns.length > 0;
1789
1821
 
1790
1822
  return React.createElement(React.Fragment, null,
1791
- React.createElement("div", { className: "ps-session-row-title" },
1792
- React.createElement(Runs, { runs: row.titleRuns, theme }),
1793
- // Meta (timestamp / member count) sits inline on the title line.
1794
- hasMeta
1795
- ? React.createElement("span", { className: "ps-session-row-meta" },
1796
- React.createElement(Runs, { runs: [{ text: " ", color: "gray" }, ...row.metaRuns], theme }))
1823
+ React.createElement("div", { className: "ps-session-row-line" },
1824
+ React.createElement("div", { className: "ps-session-row-title" },
1825
+ React.createElement(Runs, { runs: row.titleRuns, theme })),
1826
+ hasCtx
1827
+ ? React.createElement("div", { className: "ps-session-row-ctx" },
1828
+ React.createElement(Runs, { runs: ctxRuns, theme }))
1797
1829
  : null),
1798
- hasBadges
1799
- ? React.createElement("div", { className: "ps-session-row-badges" },
1800
- React.createElement(Runs, { runs: row.badgeRuns, theme }))
1801
- : null,
1802
- hasSelectedMeta
1803
- ? React.createElement("div", { className: "ps-session-row-selected-meta" },
1804
- React.createElement(Runs, { runs: row.selectedMetaRuns, theme }))
1830
+ hasDetail
1831
+ ? React.createElement("div", { className: "ps-session-row-detail" },
1832
+ React.createElement(Runs, { runs: detailRuns, theme }))
1805
1833
  : null);
1806
1834
  }
1807
1835
 
@@ -2117,19 +2145,18 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
2117
2145
  () => selectLiveActivityLines(selectorState, { spinnerFrame, maxWidth: viewState.contentWidth }),
2118
2146
  [selectorState, spinnerFrame, viewState.contentWidth],
2119
2147
  );
2120
- // Concatenate the isolated live-activity block onto the (expensive) message
2121
- // lines so the spinner tick only recomputes the small block, not the whole
2122
- // transcript. The block clears itself once the reply lands.
2123
- const chatLines = React.useMemo(
2124
- () => (liveActivityLines.length > 0
2125
- ? [...lines, [{ text: "", color: null }], ...liveActivityLines]
2126
- : lines),
2127
- [lines, liveActivityLines],
2128
- );
2148
+ // The live-activity line is pinned in the bottom-sticky strip (with the
2149
+ // outbox overlay), NOT appended to the transcript — it must stay put while
2150
+ // chat content scrolls, and it drops the instant the turn ends.
2151
+ const pinnedActivityLines = liveActivityLines;
2129
2152
  const outboxLines = React.useMemo(
2130
2153
  () => selectOutboxOverlayLines(selectorState, viewState.contentWidth, { tableMode: "sentinel" }),
2131
2154
  [selectorState, viewState.contentWidth],
2132
2155
  );
2156
+ const stickyBottom = React.useMemo(
2157
+ () => (pinnedActivityLines.length > 0 ? [...outboxLines, ...pinnedActivityLines] : outboxLines),
2158
+ [outboxLines, pinnedActivityLines],
2159
+ );
2133
2160
  const composer = showComposer && !viewState.activeSessionIsGroup && viewState.chatViewMode !== "summary"
2134
2161
  ? React.createElement("div", { className: "ps-chat-composer" },
2135
2162
  React.createElement(PromptComposer, { controller, mobile, active: true }))
@@ -2144,8 +2171,8 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
2144
2171
  actions: null,
2145
2172
  color: chrome.color,
2146
2173
  focused: viewState.focused,
2147
- lines: chatLines,
2148
- bottomStickyLines: outboxLines,
2174
+ lines,
2175
+ bottomStickyLines: stickyBottom,
2149
2176
  scrollOffset: viewState.scroll,
2150
2177
  scrollMode: "bottom",
2151
2178
  paneKey: "chat",
@@ -4105,11 +4132,27 @@ function AdminConsolePanel({ controller }) {
4105
4132
  : null,
4106
4133
  React.createElement("section", { className: "ps-admin-console__section" },
4107
4134
  React.createElement("h3", null, "GitHub Copilot key"),
4108
- React.createElement("p", { className: "ps-admin-console__hint" },
4109
- "Per-user override for the GitHub Copilot model provider token. ",
4110
- "When set, this key is used instead of the worker's env-supplied ",
4111
- "GITHUB_TOKEN for sessions you own. Clearing the key reverts to ",
4112
- "the worker default."),
4135
+ view.ghcpKey.storeAsSystem
4136
+ ? React.createElement("p", { className: "ps-admin-console__hint" },
4137
+ "System-wide key for platform-managed (ownerless) system ",
4138
+ "sessions — agent tuners, repo cache managers, and other ",
4139
+ "sessions with no owner resolve this key for GitHub Copilot ",
4140
+ "models. Clearing it reverts them to the worker default.")
4141
+ : React.createElement("p", { className: "ps-admin-console__hint" },
4142
+ "Per-user override for the GitHub Copilot model provider token. ",
4143
+ "When set, this key is used instead of the worker's env-supplied ",
4144
+ "GITHUB_TOKEN for sessions you own. Clearing the key reverts to ",
4145
+ "the worker default."),
4146
+ view.isAdmin && view.systemGhcpKey.supported
4147
+ ? React.createElement("label", { className: "ps-admin-console__system-toggle" },
4148
+ React.createElement("input", {
4149
+ type: "checkbox",
4150
+ checked: view.ghcpKey.storeAsSystem,
4151
+ disabled: view.ghcpKey.saving,
4152
+ onChange: (event) => controller.setAdminGhcpKeyStoreAsSystem(event.target.checked),
4153
+ }),
4154
+ "Store as System key")
4155
+ : null,
4113
4156
  React.createElement("p", { className: `ps-admin-console__status${view.ghcpKey.error ? " is-error" : ""}` },
4114
4157
  view.ghcpKey.error || view.ghcpKey.statusText),
4115
4158
  view.ghcpKey.editing
@@ -4141,13 +4184,15 @@ function AdminConsolePanel({ controller }) {
4141
4184
  type: "button",
4142
4185
  className: "ps-primary-button",
4143
4186
  onClick: onBeginEdit,
4144
- }, view.ghcpKey.configured ? "Replace key" : "Set key"),
4145
- view.ghcpKey.configured
4187
+ }, view.ghcpKey.targetConfigured
4188
+ ? (view.ghcpKey.storeAsSystem ? "Replace System key" : "Replace key")
4189
+ : (view.ghcpKey.storeAsSystem ? "Set System key" : "Set key")),
4190
+ view.ghcpKey.targetConfigured
4146
4191
  ? React.createElement("button", {
4147
4192
  type: "button",
4148
4193
  className: "ps-mini-button",
4149
4194
  onClick: onClear,
4150
- }, "Clear key")
4195
+ }, view.ghcpKey.storeAsSystem ? "Clear System key" : "Clear key")
4151
4196
  : null,
4152
4197
  React.createElement("button", {
4153
4198
  type: "button",