pilotswarm 0.5.0 → 0.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pilotswarm",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "PilotSwarm application package: terminal UI, browser portal + Web API server, and MCP server — one install, three bins.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -77,7 +77,7 @@
77
77
  "hono": "^4.12.10",
78
78
  "ink": "^6.8.0",
79
79
  "jose": "^6.2.2",
80
- "pilotswarm-sdk": "^0.5.0",
80
+ "pilotswarm-sdk": "^0.5.1",
81
81
  "react": "^19.2.4",
82
82
  "react-dom": "^19.2.4",
83
83
  "ws": "^8.18.2"
package/tui/src/app.js CHANGED
@@ -264,6 +264,14 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
264
264
  requestExit();
265
265
  return;
266
266
  }
267
+ // Stop the current turn — the TUI equivalent of the portal Stop button.
268
+ // ctrl-x is reliable across terminals; ctrl-esc also fires where the
269
+ // terminal sends it distinctly from a bare Esc. The controller no-ops
270
+ // (status message) when no turn is running.
271
+ if (key.ctrl && (input === "x" || key.escape)) {
272
+ controller.handleCommand(UI_COMMANDS.STOP_TURN).catch(() => {});
273
+ return;
274
+ }
267
275
  if (focus !== "prompt" && input === "q" && quitStateRef.current.armedUntil > Date.now()) {
268
276
  clearQuitArm(false);
269
277
  requestExit();
@@ -398,7 +406,7 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
398
406
  }
399
407
  return;
400
408
  }
401
- if (key.escape || input === "q" || (modal.type === "artifactPicker" && input === "a")) {
409
+ if (key.escape || input === "q" || (modal.type === "artifactPicker" && input === "a") || (modal.type === "help" && input === "?")) {
402
410
  controller.handleCommand(UI_COMMANDS.CLOSE_MODAL).catch(() => {});
403
411
  return;
404
412
  }
@@ -602,21 +610,40 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
602
610
  return;
603
611
  }
604
612
  }
613
+ // Sequence tab: j/k move the turn cursor across completed turns, enter
614
+ // expands/collapses the selected turn (paging/g-G still scroll).
615
+ if (focus === "inspector" && inspectorTab === "sequence") {
616
+ if (key.return) {
617
+ controller.handleCommand(UI_COMMANDS.TOGGLE_SEQUENCE_TURN).catch(() => {});
618
+ return;
619
+ }
620
+ if (key.upArrow || input === "k") {
621
+ controller.handleCommand(UI_COMMANDS.SEQUENCE_SELECT_PREV).catch(() => {});
622
+ return;
623
+ }
624
+ if (key.downArrow || input === "j") {
625
+ controller.handleCommand(UI_COMMANDS.SEQUENCE_SELECT_NEXT).catch(() => {});
626
+ return;
627
+ }
628
+ }
605
629
 
630
+ // [ / ] resize the vertical pane of whatever is focused (sessions/chat
631
+ // share the left split; inspector/activity share the right split).
632
+ // { / } resize the horizontal left/right column split.
606
633
  if (focus !== "prompt" && input === "[") {
607
- controller.handleCommand(UI_COMMANDS.GROW_LEFT_PANE).catch(() => {});
634
+ controller.handleCommand(UI_COMMANDS.SHRINK_FOCUSED_PANE).catch(() => {});
608
635
  return;
609
636
  }
610
637
  if (focus !== "prompt" && input === "]") {
611
- controller.handleCommand(UI_COMMANDS.GROW_RIGHT_PANE).catch(() => {});
638
+ controller.handleCommand(UI_COMMANDS.GROW_FOCUSED_PANE).catch(() => {});
612
639
  return;
613
640
  }
614
641
  if (focus !== "prompt" && input === "{") {
615
- controller.handleCommand(UI_COMMANDS.SHRINK_SESSION_PANE).catch(() => {});
642
+ controller.handleCommand(UI_COMMANDS.GROW_LEFT_PANE).catch(() => {});
616
643
  return;
617
644
  }
618
645
  if (focus !== "prompt" && input === "}") {
619
- controller.handleCommand(UI_COMMANDS.GROW_SESSION_PANE).catch(() => {});
646
+ controller.handleCommand(UI_COMMANDS.GROW_RIGHT_PANE).catch(() => {});
620
647
  return;
621
648
  }
622
649
 
@@ -718,6 +745,10 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
718
745
  controller.handleCommand(UI_COMMANDS.OPEN_ARTIFACT_PICKER).catch(() => {});
719
746
  return;
720
747
  }
748
+ if (focus !== "prompt" && input === "?") {
749
+ controller.handleCommand(UI_COMMANDS.OPEN_HELP).catch(() => {});
750
+ return;
751
+ }
721
752
  if (plainShortcut && input === "m") {
722
753
  controller.handleCommand(UI_COMMANDS.CYCLE_INSPECTOR_TAB).catch(() => {});
723
754
  return;
@@ -45,7 +45,13 @@ export const UI_COMMANDS = {
45
45
  GROW_RIGHT_PANE: "growRightPane",
46
46
  GROW_SESSION_PANE: "growSessionPane",
47
47
  SHRINK_SESSION_PANE: "shrinkSessionPane",
48
+ GROW_FOCUSED_PANE: "growFocusedPane",
49
+ SHRINK_FOCUSED_PANE: "shrinkFocusedPane",
48
50
  OPEN_ARTIFACT_PICKER: "openArtifactPicker",
51
+ OPEN_HELP: "openHelp",
52
+ SEQUENCE_SELECT_PREV: "sequenceSelectPrev",
53
+ SEQUENCE_SELECT_NEXT: "sequenceSelectNext",
54
+ TOGGLE_SEQUENCE_TURN: "toggleSequenceTurn",
49
55
  TOGGLE_LOG_TAIL: "toggleLogTail",
50
56
  TOGGLE_STATS_VIEW: "toggleStatsView",
51
57
  OPEN_LOG_FILTER: "openLogFilter",
@@ -18,6 +18,10 @@ import {
18
18
  getFocusRightTarget,
19
19
  getPromptInputRows,
20
20
  MIN_SESSION_PANE_HEIGHT,
21
+ MIN_CHAT_PANE_HEIGHT,
22
+ MIN_ACTIVITY_PANE_HEIGHT,
23
+ MIN_INSPECTOR_PANE_HEIGHT,
24
+ DEFAULT_ACTIVITY_PANE_RATIO,
21
25
  normalizeFocusRegion,
22
26
  } from "./layout.js";
23
27
  import { parseTerminalMarkupRuns } from "./formatting.js";
@@ -25,6 +29,7 @@ import {
25
29
  selectActiveArtifactLinks,
26
30
  selectActiveHttpLinks,
27
31
  selectActivityPane,
32
+ selectLiveActivityLines,
28
33
  selectChatLines,
29
34
  selectFileBrowserItems,
30
35
  selectFileSessionIdsForScope,
@@ -2883,6 +2888,10 @@ export class PilotSwarmUiController {
2883
2888
  this.exitPendingPromptEdit({ restoreDraft: true });
2884
2889
  }
2885
2890
  this.dispatch({ type: "sessions/selected", sessionId });
2891
+ // Sequence-tab turn selection/expansion is per-session; clear it on
2892
+ // switch so a stale turn index doesn't carry into another session.
2893
+ this.dispatch({ type: "ui/sequenceExpandedTurns", turns: [] });
2894
+ this.dispatch({ type: "ui/sequenceSelectedTurn", turn: null });
2886
2895
  }
2887
2896
  if (this.getState().sessions.byId[sessionId]?.isGroup) {
2888
2897
  this.detachActiveSession();
@@ -3534,6 +3543,19 @@ export class PilotSwarmUiController {
3534
3543
  await this.refreshSessions();
3535
3544
  }
3536
3545
 
3546
+ openHelpModal() {
3547
+ this.dispatch({
3548
+ type: "ui/modal",
3549
+ modal: {
3550
+ type: "help",
3551
+ title: "Keybindings",
3552
+ selectedIndex: 0,
3553
+ previousFocus: this.getState().ui.focusRegion,
3554
+ },
3555
+ });
3556
+ this.dispatch({ type: "ui/status", text: "Keybindings — ? or Esc to close" });
3557
+ }
3558
+
3537
3559
  openThemePicker() {
3538
3560
  const themes = listThemes().map((theme) => ({
3539
3561
  id: theme.id,
@@ -4160,7 +4182,13 @@ export class PilotSwarmUiController {
4160
4182
 
4161
4183
  moveModalSelection(delta) {
4162
4184
  const modal = this.getState().ui.modal;
4163
- if (!modal || !Array.isArray(modal.items) || modal.items.length === 0) return;
4185
+ if (!modal) return;
4186
+ if (modal.type === "help") {
4187
+ const current = Math.max(0, Number(modal.selectedIndex) || 0);
4188
+ this.dispatch({ type: "ui/modalSelection", index: Math.max(0, current + delta) });
4189
+ return;
4190
+ }
4191
+ if (!Array.isArray(modal.items) || modal.items.length === 0) return;
4164
4192
  if (modal.type === "logFilter" || modal.type === "filesFilter" || modal.type === "historyFormat") {
4165
4193
  const currentPaneIndex = Math.max(0, Math.min(Number(modal.selectedIndex) || 0, modal.items.length - 1));
4166
4194
  const selected = modal.items[currentPaneIndex];
@@ -4216,6 +4244,10 @@ export class PilotSwarmUiController {
4216
4244
  async confirmModal() {
4217
4245
  const modal = this.getState().ui.modal;
4218
4246
  if (!modal) return;
4247
+ if (modal.type === "help") {
4248
+ this.closeModal();
4249
+ return;
4250
+ }
4219
4251
  if (modal.type === "confirm") {
4220
4252
  const previousFocus = modal.previousFocus;
4221
4253
  this.dispatch({ type: "ui/modal", modal: null });
@@ -4782,6 +4814,88 @@ export class PilotSwarmUiController {
4782
4814
  }
4783
4815
  }
4784
4816
 
4817
+ // Focus-aware vertical resize: [ / ] grow or shrink whichever pane is
4818
+ // focused. Sessions and chat share the left-column vertical split;
4819
+ // inspector and activity share the right-column vertical split. A positive
4820
+ // delta always grows the focused pane at the expense of its sibling.
4821
+ resizeFocusedPane(delta) {
4822
+ const focus = this.getState().ui.focusRegion;
4823
+ const layoutState = this.getState().ui.layout || {};
4824
+ const bodyHeight = this.getCurrentLayout().bodyHeight ?? (layoutState.viewportHeight ?? 40);
4825
+ const totalHeight = layoutState.viewportHeight ?? 40;
4826
+ // Bound the adjust so BOTH panes stay strictly ABOVE their collapse
4827
+ // thresholds (the collapse check is `<=`, hence the +1). A keyboard
4828
+ // resize then never hides a pane — which would drop it from the Tab
4829
+ // cycle — and never lands on a dead plateau. A positive delta grows the
4830
+ // focused pane.
4831
+ const clamp = (current, signed, min, max) => {
4832
+ const hi = Math.max(min, max);
4833
+ return Math.max(min, Math.min(hi, (current || 0) + signed));
4834
+ };
4835
+ if (focus === FOCUS_REGIONS.SESSIONS || focus === FOCUS_REGIONS.CHAT) {
4836
+ const base = getBaseSessionPaneHeight(bodyHeight);
4837
+ const minH = MIN_SESSION_PANE_HEIGHT + 1;
4838
+ const maxH = Math.min(getMaxSessionPaneHeight(totalHeight, bodyHeight), bodyHeight - MIN_CHAT_PANE_HEIGHT - 1);
4839
+ const signed = focus === FOCUS_REGIONS.CHAT ? -delta : delta;
4840
+ const next = clamp(layoutState.sessionPaneAdjust, signed, minH - base, maxH - base);
4841
+ this.dispatch({ type: "ui/sessionPaneAdjust", sessionPaneAdjust: next });
4842
+ } else if (focus === FOCUS_REGIONS.INSPECTOR || focus === FOCUS_REGIONS.ACTIVITY) {
4843
+ const base = Math.max(MIN_ACTIVITY_PANE_HEIGHT, Math.floor(bodyHeight * DEFAULT_ACTIVITY_PANE_RATIO));
4844
+ const minH = MIN_ACTIVITY_PANE_HEIGHT + 1;
4845
+ const maxH = bodyHeight - MIN_INSPECTOR_PANE_HEIGHT - 1;
4846
+ const signed = focus === FOCUS_REGIONS.INSPECTOR ? -delta : delta;
4847
+ const next = clamp(layoutState.activityPaneAdjust, signed, minH - base, maxH - base);
4848
+ this.dispatch({ type: "ui/activityPaneAdjust", activityPaneAdjust: next });
4849
+ }
4850
+ }
4851
+
4852
+ getActiveSequenceCompletedTurns() {
4853
+ const state = this.getState();
4854
+ const sid = state.sessions.activeSessionId;
4855
+ if (!sid) return [];
4856
+ const history = state.history.bySessionId?.get?.(sid);
4857
+ const turns = [];
4858
+ const seen = new Set();
4859
+ for (const event of history?.events || []) {
4860
+ if (event?.eventType !== "session.turn_completed") continue;
4861
+ const turn = Number(event?.data?.turnIndex ?? event?.data?.iteration);
4862
+ if (!Number.isFinite(turn) || seen.has(turn)) continue;
4863
+ seen.add(turn);
4864
+ turns.push(turn);
4865
+ }
4866
+ turns.sort((a, b) => a - b);
4867
+ return turns;
4868
+ }
4869
+
4870
+ // Move the sequence-tab turn cursor among completed turns. First press with
4871
+ // no selection lands on the latest turn; subsequent presses step.
4872
+ moveSequenceSelection(delta) {
4873
+ const turns = this.getActiveSequenceCompletedTurns();
4874
+ if (turns.length === 0) return;
4875
+ const at = turns.indexOf(Number(this.getState().ui.sequenceSelectedTurn));
4876
+ const index = at === -1
4877
+ ? turns.length - 1
4878
+ : Math.max(0, Math.min(turns.length - 1, at + delta));
4879
+ this.dispatch({ type: "ui/sequenceSelectedTurn", turn: turns[index] });
4880
+ }
4881
+
4882
+ toggleSequenceTurnExpanded() {
4883
+ const turns = this.getActiveSequenceCompletedTurns();
4884
+ if (turns.length === 0) return;
4885
+ let selected = Number(this.getState().ui.sequenceSelectedTurn);
4886
+ if (!turns.includes(selected)) {
4887
+ selected = turns[turns.length - 1];
4888
+ this.dispatch({ type: "ui/sequenceSelectedTurn", turn: selected });
4889
+ }
4890
+ const expanded = Array.isArray(this.getState().ui.sequenceExpandedTurns)
4891
+ ? this.getState().ui.sequenceExpandedTurns.map(Number)
4892
+ : [];
4893
+ const next = expanded.includes(selected)
4894
+ ? expanded.filter((turn) => turn !== selected)
4895
+ : [...expanded, selected];
4896
+ this.dispatch({ type: "ui/sequenceExpandedTurns", turns: next });
4897
+ }
4898
+
4785
4899
  nextInspectorTab() {
4786
4900
  const current = this.getState().ui.inspectorTab;
4787
4901
  const inspectorTab = cycleValue(INSPECTOR_TABS, current, 1);
@@ -5058,7 +5172,10 @@ export class PilotSwarmUiController {
5058
5172
 
5059
5173
  const contentWidth = Math.max(20, layout.leftWidth - 4);
5060
5174
  const contentHeight = Math.max(1, layout.chatPaneHeight - 2);
5061
- const lines = selectChatLines(state, contentWidth);
5175
+ const lines = [
5176
+ ...selectChatLines(state, contentWidth),
5177
+ ...selectLiveActivityLines(state),
5178
+ ];
5062
5179
  const bottomStickyLines = selectOutboxOverlayLines(state, contentWidth);
5063
5180
  const bottomStickyHeight = Math.min(
5064
5181
  Math.max(0, Math.floor(contentHeight * 0.34)),
@@ -6030,9 +6147,27 @@ export class PilotSwarmUiController {
6030
6147
  case UI_COMMANDS.SHRINK_SESSION_PANE:
6031
6148
  this.adjustSessionPaneSplit(-2);
6032
6149
  return;
6150
+ case UI_COMMANDS.GROW_FOCUSED_PANE:
6151
+ this.resizeFocusedPane(2);
6152
+ return;
6153
+ case UI_COMMANDS.SHRINK_FOCUSED_PANE:
6154
+ this.resizeFocusedPane(-2);
6155
+ return;
6033
6156
  case UI_COMMANDS.OPEN_ARTIFACT_PICKER:
6034
6157
  await this.openArtifactPicker();
6035
6158
  return;
6159
+ case UI_COMMANDS.OPEN_HELP:
6160
+ this.openHelpModal();
6161
+ return;
6162
+ case UI_COMMANDS.SEQUENCE_SELECT_PREV:
6163
+ this.moveSequenceSelection(-1);
6164
+ return;
6165
+ case UI_COMMANDS.SEQUENCE_SELECT_NEXT:
6166
+ this.moveSequenceSelection(1);
6167
+ return;
6168
+ case UI_COMMANDS.TOGGLE_SEQUENCE_TURN:
6169
+ this.toggleSequenceTurnExpanded();
6170
+ return;
6036
6171
  case UI_COMMANDS.TOGGLE_LOG_TAIL:
6037
6172
  this.toggleLogTail();
6038
6173
  return;
@@ -670,6 +670,24 @@ export function appReducer(state, action) {
670
670
  },
671
671
  };
672
672
 
673
+ case "ui/sequenceExpandedTurns":
674
+ return {
675
+ ...state,
676
+ ui: {
677
+ ...state.ui,
678
+ sequenceExpandedTurns: Array.isArray(action.turns) ? action.turns : [],
679
+ },
680
+ };
681
+
682
+ case "ui/sequenceSelectedTurn":
683
+ return {
684
+ ...state,
685
+ ui: {
686
+ ...state.ui,
687
+ sequenceSelectedTurn: action.turn == null ? null : Number(action.turn),
688
+ },
689
+ };
690
+
673
691
  case "sessions/filterQuery":
674
692
  {
675
693
  const nextSessions = {
@@ -701,7 +719,19 @@ export function appReducer(state, action) {
701
719
 
702
720
  case "ui/modalSelection": {
703
721
  const modal = state.ui.modal;
704
- if (!modal || !Array.isArray(modal.items) || modal.items.length === 0) {
722
+ if (!modal) return state;
723
+ if (modal.type === "help") {
724
+ // The help overlay has no `items`; the row-count upper bound is
725
+ // clamped by selectHelpModal, so just track the scroll anchor.
726
+ return {
727
+ ...state,
728
+ ui: {
729
+ ...state.ui,
730
+ modal: { ...modal, selectedIndex: Math.max(0, Number(action.index) || 0) },
731
+ },
732
+ };
733
+ }
734
+ if (!Array.isArray(modal.items) || modal.items.length === 0) {
705
735
  return state;
706
736
  }
707
737
  const nextIndex = Math.max(0, Math.min(action.index ?? 0, modal.items.length - 1));