pilotswarm-cli 0.1.3 → 0.1.5
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/README.md +30 -0
- package/cli/tui.js +277 -254
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# pilotswarm-cli
|
|
2
|
+
|
|
3
|
+
Terminal UI for PilotSwarm.
|
|
4
|
+
|
|
5
|
+
Install:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install pilotswarm-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For app-specific worker modules or direct SDK imports, also add:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install pilotswarm-sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Run locally against a plugin directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx pilotswarm local --env .env --plugin ./plugin --worker ./worker-module.js
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`pilotswarm-cli` provides the shipped TUI. Your app customizes it with `plugin/plugin.json`, `plugin/agents/*.agent.md`, `plugin/skills/*/SKILL.md`, and optional worker-side tools.
|
|
24
|
+
|
|
25
|
+
Common docs:
|
|
26
|
+
|
|
27
|
+
- CLI apps: `https://github.com/affandar/PilotSwarm/blob/main/docs/cli/building-cli-apps.md`
|
|
28
|
+
- CLI agents: `https://github.com/affandar/PilotSwarm/blob/main/docs/cli/building-agents.md`
|
|
29
|
+
- Keybindings: `https://github.com/affandar/PilotSwarm/blob/main/docs/keybindings.md`
|
|
30
|
+
- DevOps sample: `https://github.com/affandar/PilotSwarm/tree/main/examples/devops-command-center`
|
package/cli/tui.js
CHANGED
|
@@ -2703,11 +2703,10 @@ const seqCmsSeededSessions = new Set();
|
|
|
2703
2703
|
* Load conversation history from CMS and rebuild chat buffer for the session.
|
|
2704
2704
|
* Includes ALL persisted events (not truncated) so switching sessions is deterministic.
|
|
2705
2705
|
*/
|
|
2706
|
-
async function loadCmsHistory(orchId) {
|
|
2706
|
+
async function loadCmsHistory(orchId, options = {}) {
|
|
2707
2707
|
const _ph = perfStart("loadCmsHistory");
|
|
2708
2708
|
const sid = orchId.startsWith("session-") ? orchId.slice(8) : orchId;
|
|
2709
|
-
const
|
|
2710
|
-
sessionHistoryLoadGeneration.set(orchId, generation);
|
|
2709
|
+
const force = options.force === true;
|
|
2711
2710
|
let eventCount = 0;
|
|
2712
2711
|
let loadFailed = false;
|
|
2713
2712
|
|
|
@@ -2716,297 +2715,320 @@ async function loadCmsHistory(orchId) {
|
|
|
2716
2715
|
// so reloading on every session switch just adds latency.
|
|
2717
2716
|
const cached = sessionChatBuffers.get(orchId);
|
|
2718
2717
|
const loadedAt = sessionHistoryLoadedAt.get(orchId) ?? 0;
|
|
2719
|
-
if (cached && cached.length > 1 && (Date.now() - loadedAt) < 30_000 && !orchHasChanges.has(orchId)) {
|
|
2718
|
+
if (!force && cached && cached.length > 1 && (Date.now() - loadedAt) < 30_000 && !orchHasChanges.has(orchId)) {
|
|
2719
|
+
perfEnd(_ph, { orchId: orchId.slice(0, 12), cached: true });
|
|
2720
2720
|
return;
|
|
2721
2721
|
}
|
|
2722
2722
|
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
sess = await client.resumeSession(sid);
|
|
2728
|
-
sessions.set(sid, sess);
|
|
2729
|
-
} catch (err) {
|
|
2730
|
-
appendLog(`{yellow-fg}Could not resume session ${shortId(sid)}: ${err.message}{/yellow-fg}`);
|
|
2731
|
-
return;
|
|
2732
|
-
}
|
|
2723
|
+
const inFlight = sessionHistoryLoadPromises.get(orchId);
|
|
2724
|
+
if (inFlight && !force) {
|
|
2725
|
+
perfEnd(_ph, { orchId: orchId.slice(0, 12), deduped: true });
|
|
2726
|
+
return inFlight;
|
|
2733
2727
|
}
|
|
2734
2728
|
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
const CMS_HISTORY_FETCH_LIMIT = expand >= 2 ? 2000 : expand >= 1 ? 500 : 250;
|
|
2738
|
-
const MAX_RENDERED_EVENTS = expand >= 2 ? 2000 : expand >= 1 ? 500 : 120;
|
|
2739
|
-
const MAX_TOTAL_RENDER_CHARS = expand >= 2 ? 500_000 : expand >= 1 ? 200_000 : 50_000;
|
|
2740
|
-
const MAX_ASSISTANT_MESSAGE_CHARS = expand >= 1 ? 20_000 : 4_000;
|
|
2741
|
-
const dc = getDc();
|
|
2729
|
+
const generation = (sessionHistoryLoadGeneration.get(orchId) ?? 0) + 1;
|
|
2730
|
+
sessionHistoryLoadGeneration.set(orchId, generation);
|
|
2742
2731
|
|
|
2743
|
-
|
|
2744
|
-
//
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
sess.getMessages(CMS_HISTORY_FETCH_LIMIT),
|
|
2748
|
-
(!sessionModels.has(orchId)) ? sess.getInfo().catch(() => null) : Promise.resolve(null),
|
|
2749
|
-
dc ? dc.getStatus(orchId).catch(() => null) : Promise.resolve(null),
|
|
2750
|
-
]);
|
|
2751
|
-
eventCount = events?.length || 0;
|
|
2752
|
-
|
|
2753
|
-
let liveCustomStatus = null;
|
|
2754
|
-
let liveResponsePayload = null;
|
|
2755
|
-
if (liveStatus?.customStatus) {
|
|
2732
|
+
const loadPromise = (async () => {
|
|
2733
|
+
// Ensure we have a PilotSwarmSession handle (may not exist for sessions from previous TUI runs)
|
|
2734
|
+
let sess = sessions.get(sid);
|
|
2735
|
+
if (!sess) {
|
|
2756
2736
|
try {
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
liveResponsePayload = await fetchLatestResponsePayload(orchId, dc);
|
|
2737
|
+
sess = await client.resumeSession(sid);
|
|
2738
|
+
sessions.set(sid, sess);
|
|
2739
|
+
} catch (err) {
|
|
2740
|
+
appendLog(`{yellow-fg}Could not resume session ${shortId(sid)}: ${err.message}{/yellow-fg}`);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2764
2743
|
}
|
|
2765
2744
|
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
:
|
|
2769
|
-
|
|
2770
|
-
:
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
if (info?.model) {
|
|
2774
|
-
sessionModels.set(orchId, info.model);
|
|
2775
|
-
if (orchId === activeOrchId) updateChatLabel();
|
|
2776
|
-
}
|
|
2745
|
+
try {
|
|
2746
|
+
const expand = sessionExpandLevel.get(orchId) || 0;
|
|
2747
|
+
const CMS_HISTORY_FETCH_LIMIT = expand >= 2 ? 2000 : expand >= 1 ? 500 : 250;
|
|
2748
|
+
const MAX_RENDERED_EVENTS = expand >= 2 ? 2000 : expand >= 1 ? 500 : 120;
|
|
2749
|
+
const MAX_TOTAL_RENDER_CHARS = expand >= 2 ? 500_000 : expand >= 1 ? 200_000 : 50_000;
|
|
2750
|
+
const MAX_ASSISTANT_MESSAGE_CHARS = expand >= 1 ? 20_000 : 4_000;
|
|
2751
|
+
const dc = getDc();
|
|
2777
2752
|
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2753
|
+
// Fetch events, session info, and live status in parallel.
|
|
2754
|
+
// The live custom status may contain the latest `turnResult` even when
|
|
2755
|
+
// the CMS history does not yet have a persisted `assistant.message`.
|
|
2756
|
+
const [events, info, liveStatus] = await Promise.all([
|
|
2757
|
+
sess.getMessages(CMS_HISTORY_FETCH_LIMIT),
|
|
2758
|
+
(!sessionModels.has(orchId)) ? sess.getInfo().catch(() => null) : Promise.resolve(null),
|
|
2759
|
+
dc ? dc.getStatus(orchId).catch(() => null) : Promise.resolve(null),
|
|
2760
|
+
]);
|
|
2761
|
+
eventCount = events?.length || 0;
|
|
2762
|
+
|
|
2763
|
+
let liveCustomStatus = null;
|
|
2764
|
+
let liveResponsePayload = null;
|
|
2765
|
+
if (liveStatus?.customStatus) {
|
|
2766
|
+
try {
|
|
2767
|
+
liveCustomStatus = typeof liveStatus.customStatus === "string"
|
|
2768
|
+
? JSON.parse(liveStatus.customStatus)
|
|
2769
|
+
: liveStatus.customStatus;
|
|
2770
|
+
} catch {}
|
|
2781
2771
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
// arrived while we were fetching from CMS (race condition that
|
|
2785
|
-
// causes empty chat on first switch to a session).
|
|
2786
|
-
const existing = sessionChatBuffers.get(orchId);
|
|
2787
|
-
const isLoadingPlaceholder = existing
|
|
2788
|
-
&& existing.length === 1
|
|
2789
|
-
&& /Loading/.test(existing[0]);
|
|
2790
|
-
if (!existing || existing.length === 0 || isLoadingPlaceholder) {
|
|
2791
|
-
const splashLines = ensureSessionSplashBuffer(orchId);
|
|
2792
|
-
if (!splashLines) {
|
|
2793
|
-
sessionChatBuffers.set(orchId, []);
|
|
2794
|
-
}
|
|
2772
|
+
if (liveCustomStatus?.responseVersion) {
|
|
2773
|
+
liveResponsePayload = await fetchLatestResponsePayload(orchId, dc);
|
|
2795
2774
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2775
|
+
|
|
2776
|
+
const liveTurnContent = liveCustomStatus?.turnResult?.type === "completed"
|
|
2777
|
+
? liveCustomStatus.turnResult.content
|
|
2778
|
+
: liveResponsePayload?.type === "completed"
|
|
2779
|
+
? liveResponsePayload.content
|
|
2780
|
+
: "";
|
|
2781
|
+
|
|
2782
|
+
// Populate session model if not already known
|
|
2783
|
+
if (info?.model) {
|
|
2784
|
+
sessionModels.set(orchId, info.model);
|
|
2785
|
+
if (orchId === activeOrchId) updateChatLabel();
|
|
2798
2786
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2787
|
+
|
|
2788
|
+
if ((!events || events.length === 0) && !liveTurnContent) {
|
|
2789
|
+
if (sessionHistoryLoadGeneration.get(orchId) !== generation) {
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
// Only blank the buffer if the observer hasn't already written
|
|
2793
|
+
// content into it. Otherwise we'd nuke live turn output that
|
|
2794
|
+
// arrived while we were fetching from CMS (race condition that
|
|
2795
|
+
// causes empty chat on first switch to a session).
|
|
2796
|
+
const existing = sessionChatBuffers.get(orchId);
|
|
2797
|
+
const isLoadingPlaceholder = existing
|
|
2798
|
+
&& existing.length === 1
|
|
2799
|
+
&& /Loading/.test(existing[0]);
|
|
2800
|
+
if (!existing || existing.length === 0 || isLoadingPlaceholder) {
|
|
2801
|
+
const splashLines = ensureSessionSplashBuffer(orchId);
|
|
2802
|
+
if (!splashLines) {
|
|
2803
|
+
sessionChatBuffers.set(orchId, ["{gray-fg}(no recent chat history yet){/gray-fg}"]);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
const existingActivity = sessionActivityBuffers.get(orchId);
|
|
2807
|
+
if (!existingActivity || existingActivity.length === 0) {
|
|
2808
|
+
sessionActivityBuffers.set(orchId, ["{gray-fg}(no recent activity yet){/gray-fg}"]);
|
|
2809
|
+
}
|
|
2810
|
+
sessionHistoryLoadedAt.set(orchId, Date.now());
|
|
2811
|
+
sessionRenderedCmsSeq.set(orchId, 0);
|
|
2812
|
+
sessionRecoveredTurnResult.delete(orchId);
|
|
2813
|
+
if (orchId === activeOrchId) {
|
|
2814
|
+
invalidateChat();
|
|
2815
|
+
invalidateActivity();
|
|
2816
|
+
}
|
|
2817
|
+
return;
|
|
2805
2818
|
}
|
|
2806
|
-
return;
|
|
2807
|
-
}
|
|
2808
2819
|
|
|
2809
|
-
|
|
2810
|
-
|
|
2820
|
+
// Strip the [SYSTEM: Running on host ...] prefix from user prompts
|
|
2821
|
+
const stripHostPrefix = (text) => text?.replace(/^\[SYSTEM: Running on host "[^"]*"\.\]\n\n/, "") || text;
|
|
2811
2822
|
|
|
2812
|
-
|
|
2813
|
-
|
|
2823
|
+
// Filter out internal timer continuation prompts — these aren't real user messages
|
|
2824
|
+
const isTimerPrompt = (text) => /^The \d+ second wait is now complete\./i.test(text);
|
|
2814
2825
|
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2826
|
+
const lines = [];
|
|
2827
|
+
const fmtTime = (value) => {
|
|
2828
|
+
if (!value) return "--:--:--";
|
|
2829
|
+
return formatDisplayTime(value, {
|
|
2830
|
+
hour: "2-digit",
|
|
2831
|
+
minute: "2-digit",
|
|
2832
|
+
second: "2-digit",
|
|
2833
|
+
});
|
|
2834
|
+
};
|
|
2835
|
+
const normalizeContent = (text) => (text || "").replace(/\r\n/g, "\n").trim();
|
|
2836
|
+
|
|
2837
|
+
// Cap rendered events to the most recent N to keep switching fast.
|
|
2838
|
+
const renderEvents = (events || []).length > MAX_RENDERED_EVENTS
|
|
2839
|
+
? events.slice(-MAX_RENDERED_EVENTS)
|
|
2840
|
+
: (events || []);
|
|
2841
|
+
const truncated = (events || []).length > MAX_RENDERED_EVENTS;
|
|
2842
|
+
|
|
2843
|
+
// Build display lines from persisted events
|
|
2844
|
+
// Chat lines = user messages + assistant responses
|
|
2845
|
+
// Activity lines = tool calls, reasoning, status changes
|
|
2846
|
+
const activityLines = [];
|
|
2847
|
+
let renderedChars = 0;
|
|
2848
|
+
let lastAssistantContent = "";
|
|
2849
|
+
if (truncated) {
|
|
2850
|
+
lines.push(`{gray-fg}── ${events.length - MAX_RENDERED_EVENTS} older events omitted (${events.length} total) · press {bold}e{/bold} to expand ──{/gray-fg}`);
|
|
2851
|
+
lines.push("");
|
|
2852
|
+
}
|
|
2853
|
+
for (const evt of renderEvents) {
|
|
2854
|
+
const type = evt.eventType;
|
|
2855
|
+
const timeStr = fmtTime(evt.createdAt);
|
|
2856
|
+
if (type === "user.message") {
|
|
2857
|
+
const content = stripHostPrefix(evt.data?.content);
|
|
2858
|
+
if (content && !content.startsWith("[SYSTEM:") && !isTimerPrompt(content) && !isBootstrapPromptForSession(content, orchId)) {
|
|
2859
|
+
// Format CHILD_UPDATE messages as distinct cards
|
|
2860
|
+
const childMatch = content.match(/^\[CHILD_UPDATE from=(\S+) type=(\S+)(?:\s+iter=(\d+))?\]\n?(.*)$/s);
|
|
2861
|
+
if (childMatch) {
|
|
2862
|
+
const childId = childMatch[1].slice(0, 8);
|
|
2863
|
+
const updateType = childMatch[2];
|
|
2864
|
+
const body = (childMatch[4] || "").trim();
|
|
2865
|
+
const childTitle = sessionHeadings.get(`session-${childMatch[1]}`) || `Agent ${childId}`;
|
|
2866
|
+
const typeColor = updateType === "completed" ? "green" : updateType === "error" ? "red" : "magenta";
|
|
2867
|
+
lines.push(`{white-fg}[${timeStr}]{/white-fg}`);
|
|
2868
|
+
lines.push(`{${typeColor}-fg}┌─ {bold}${childTitle}{/bold} · ${updateType} ─┐{/${typeColor}-fg}`);
|
|
2869
|
+
if (body) {
|
|
2870
|
+
const bodyLines = body.split("\n");
|
|
2871
|
+
for (const bl of bodyLines.slice(0, 8)) {
|
|
2872
|
+
lines.push(`{${typeColor}-fg}│{/${typeColor}-fg} ${bl}`);
|
|
2873
|
+
}
|
|
2874
|
+
if (bodyLines.length > 8) {
|
|
2875
|
+
lines.push(`{${typeColor}-fg}│{/${typeColor}-fg} {gray-fg}… ${bodyLines.length - 8} more lines{/gray-fg}`);
|
|
2876
|
+
}
|
|
2865
2877
|
}
|
|
2878
|
+
lines.push(`{${typeColor}-fg}└${"─".repeat(30)}┘{/${typeColor}-fg}`);
|
|
2879
|
+
lines.push("");
|
|
2880
|
+
} else {
|
|
2881
|
+
lines.push(`{white-fg}[${timeStr}]{/white-fg} {bold}You:{/bold} ${content}`);
|
|
2866
2882
|
}
|
|
2867
|
-
lines.push(`{${typeColor}-fg}└${'─'.repeat(30)}┘{/${typeColor}-fg}`);
|
|
2868
|
-
lines.push("");
|
|
2869
|
-
} else {
|
|
2870
|
-
lines.push(`{white-fg}[${timeStr}]{/white-fg} {bold}You:{/bold} ${content}`);
|
|
2871
2883
|
}
|
|
2872
|
-
}
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2884
|
+
} else if (type === "assistant.message") {
|
|
2885
|
+
const content = evt.data?.content;
|
|
2886
|
+
if (content) {
|
|
2887
|
+
lastAssistantContent = content;
|
|
2888
|
+
detectArtifactLinks(content, orchId);
|
|
2889
|
+
if (renderedChars >= MAX_TOTAL_RENDER_CHARS) {
|
|
2890
|
+
lines.push(`{gray-fg}── additional assistant output omitted to keep session switching fast ──{/gray-fg}`);
|
|
2891
|
+
lines.push("");
|
|
2892
|
+
break;
|
|
2893
|
+
}
|
|
2894
|
+
const clipped = content.length > MAX_ASSISTANT_MESSAGE_CHARS
|
|
2895
|
+
? content.slice(0, MAX_ASSISTANT_MESSAGE_CHARS) + "\n\n[output truncated in TUI history view]"
|
|
2896
|
+
: content;
|
|
2897
|
+
const displayClipped = clipped.replace(
|
|
2898
|
+
/artifact:\/\/[a-f0-9-]+\/([^\s"'{}]+)/g,
|
|
2899
|
+
"📎 **$1** _(press 'a' to download)_",
|
|
2900
|
+
);
|
|
2901
|
+
lines.push(`{white-fg}[${timeStr}]{/white-fg} {cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`);
|
|
2902
|
+
const rendered = renderMarkdown(displayClipped);
|
|
2903
|
+
renderedChars += clipped.length;
|
|
2904
|
+
for (const line of rendered.split("\n")) {
|
|
2905
|
+
lines.push(line);
|
|
2906
|
+
}
|
|
2881
2907
|
lines.push("");
|
|
2882
|
-
break;
|
|
2883
|
-
}
|
|
2884
|
-
const clipped = content.length > MAX_ASSISTANT_MESSAGE_CHARS
|
|
2885
|
-
? content.slice(0, MAX_ASSISTANT_MESSAGE_CHARS) + "\n\n[output truncated in TUI history view]"
|
|
2886
|
-
: content;
|
|
2887
|
-
// Replace artifact:// URIs with highlighted display
|
|
2888
|
-
const displayClipped = clipped.replace(
|
|
2889
|
-
/artifact:\/\/[a-f0-9-]+\/([^\s"'{}]+)/g,
|
|
2890
|
-
"📎 **$1** _(press 'a' to download)_",
|
|
2891
|
-
);
|
|
2892
|
-
lines.push(`{white-fg}[${timeStr}]{/white-fg} {cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`);
|
|
2893
|
-
const rendered = renderMarkdown(displayClipped);
|
|
2894
|
-
renderedChars += clipped.length;
|
|
2895
|
-
for (const line of rendered.split("\n")) {
|
|
2896
|
-
lines.push(line);
|
|
2897
2908
|
}
|
|
2898
|
-
|
|
2909
|
+
} else if (type === "tool.execution_start") {
|
|
2910
|
+
const toolName = evt.data?.toolName || "tool";
|
|
2911
|
+
const dsid = evt.data?.durableSessionId ? ` {gray-fg}[${shortId(evt.data.durableSessionId)}]{/gray-fg}` : "";
|
|
2912
|
+
activityLines.push(`{white-fg}[${timeStr}]{/white-fg} {yellow-fg}▶ ${toolName}{/yellow-fg}${dsid}`);
|
|
2913
|
+
} else if (type === "tool.execution_complete") {
|
|
2914
|
+
const toolName = evt.data?.toolName || "tool";
|
|
2915
|
+
activityLines.push(`{white-fg}[${timeStr}]{/white-fg} {green-fg}✓ ${toolName}{/green-fg}`);
|
|
2916
|
+
} else if (type === "abort" || type === "session.info" || type === "session.idle"
|
|
2917
|
+
|| type === "session.usage_info" || type === "pending_messages.modified"
|
|
2918
|
+
|| type === "assistant.usage") {
|
|
2919
|
+
// skip internal/noisy events
|
|
2920
|
+
} else {
|
|
2921
|
+
activityLines.push(`{white-fg}[${timeStr}] [${type}]{/white-fg}`);
|
|
2899
2922
|
}
|
|
2900
|
-
} else if (type === "tool.execution_start") {
|
|
2901
|
-
const toolName = evt.data?.toolName || "tool";
|
|
2902
|
-
const dsid = evt.data?.durableSessionId ? ` {gray-fg}[${shortId(evt.data.durableSessionId)}]{/gray-fg}` : "";
|
|
2903
|
-
activityLines.push(`{white-fg}[${timeStr}]{/white-fg} {yellow-fg}▶ ${toolName}{/yellow-fg}${dsid}`);
|
|
2904
|
-
} else if (type === "tool.execution_complete") {
|
|
2905
|
-
const toolName = evt.data?.toolName || "tool";
|
|
2906
|
-
activityLines.push(`{white-fg}[${timeStr}]{/white-fg} {green-fg}✓ ${toolName}{/green-fg}`);
|
|
2907
|
-
} else if (type === "abort" || type === "session.info" || type === "session.idle"
|
|
2908
|
-
|| type === "session.usage_info" || type === "pending_messages.modified"
|
|
2909
|
-
|| type === "assistant.usage") {
|
|
2910
|
-
// skip internal/noisy events
|
|
2911
|
-
} else {
|
|
2912
|
-
activityLines.push(`{white-fg}[${timeStr}] [${type}]{/white-fg}`);
|
|
2913
2923
|
}
|
|
2914
|
-
}
|
|
2915
2924
|
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2925
|
+
const normalizedLiveTurn = normalizeContent(liveTurnContent);
|
|
2926
|
+
const normalizedLastAssistant = normalizeContent(lastAssistantContent);
|
|
2927
|
+
const liveTurnMissingFromHistory = normalizedLiveTurn
|
|
2928
|
+
&& normalizedLiveTurn !== normalizedLastAssistant;
|
|
2920
2929
|
|
|
2921
|
-
|
|
2922
|
-
|
|
2930
|
+
if (liveTurnMissingFromHistory) {
|
|
2931
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") {
|
|
2932
|
+
lines.push("");
|
|
2933
|
+
}
|
|
2934
|
+
lines.push("{gray-fg}── latest turn result recovered from live status ──{/gray-fg}");
|
|
2923
2935
|
lines.push("");
|
|
2936
|
+
|
|
2937
|
+
const clippedLiveTurn = liveTurnContent.length > MAX_ASSISTANT_MESSAGE_CHARS
|
|
2938
|
+
? liveTurnContent.slice(0, MAX_ASSISTANT_MESSAGE_CHARS) + "\n\n[output truncated in TUI history view]"
|
|
2939
|
+
: liveTurnContent;
|
|
2940
|
+
lines.push(`{white-fg}[${fmtTime(Date.now())}]{/white-fg} {cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`);
|
|
2941
|
+
const renderedLiveTurn = renderMarkdown(clippedLiveTurn);
|
|
2942
|
+
renderedChars += clippedLiveTurn.length;
|
|
2943
|
+
for (const line of renderedLiveTurn.split("\n")) {
|
|
2944
|
+
lines.push(line);
|
|
2945
|
+
}
|
|
2946
|
+
lines.push("");
|
|
2947
|
+
sessionRecoveredTurnResult.set(orchId, normalizedLiveTurn);
|
|
2948
|
+
noteSeenResponseVersion(orchId, liveResponsePayload?.version);
|
|
2949
|
+
} else {
|
|
2950
|
+
sessionRecoveredTurnResult.delete(orchId);
|
|
2924
2951
|
}
|
|
2925
|
-
lines.push("{gray-fg}── latest turn result recovered from live status ──{/gray-fg}");
|
|
2926
|
-
lines.push("");
|
|
2927
|
-
|
|
2928
|
-
const clippedLiveTurn = liveTurnContent.length > MAX_ASSISTANT_MESSAGE_CHARS
|
|
2929
|
-
? liveTurnContent.slice(0, MAX_ASSISTANT_MESSAGE_CHARS) + "\n\n[output truncated in TUI history view]"
|
|
2930
|
-
: liveTurnContent;
|
|
2931
|
-
lines.push(`{white-fg}[${fmtTime(Date.now())}]{/white-fg} {cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`);
|
|
2932
|
-
const renderedLiveTurn = renderMarkdown(clippedLiveTurn);
|
|
2933
|
-
renderedChars += clippedLiveTurn.length;
|
|
2934
|
-
for (const line of renderedLiveTurn.split("\n")) {
|
|
2935
|
-
lines.push(line);
|
|
2936
|
-
}
|
|
2937
|
-
lines.push("");
|
|
2938
|
-
sessionRecoveredTurnResult.set(orchId, normalizedLiveTurn);
|
|
2939
|
-
noteSeenResponseVersion(orchId, liveResponsePayload?.version);
|
|
2940
|
-
} else {
|
|
2941
|
-
sessionRecoveredTurnResult.delete(orchId);
|
|
2942
|
-
}
|
|
2943
2952
|
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2953
|
+
if (eventCount > 0) {
|
|
2954
|
+
lines.push(`{white-fg}── recent history loaded from database (${eventCount} events fetched) ──{/white-fg}`);
|
|
2955
|
+
lines.push("");
|
|
2956
|
+
}
|
|
2948
2957
|
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2958
|
+
if (systemSplashText.has(orchId)) {
|
|
2959
|
+
const splashText = systemSplashText.get(orchId);
|
|
2960
|
+
const splashLines = splashText.split("\n");
|
|
2961
|
+
const hasSplashPrefix = lines.length >= splashLines.length
|
|
2962
|
+
&& splashLines.every((line, idx) => lines[idx] === line);
|
|
2963
|
+
if (!hasSplashPrefix) {
|
|
2964
|
+
lines.unshift(...splashLines, "");
|
|
2965
|
+
}
|
|
2966
|
+
sessionSplashApplied.add(orchId);
|
|
2957
2967
|
}
|
|
2958
|
-
sessionSplashApplied.add(orchId);
|
|
2959
|
-
}
|
|
2960
2968
|
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2969
|
+
if (sessionHistoryLoadGeneration.get(orchId) !== generation) {
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2964
2972
|
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2973
|
+
const maxRenderedSeq = (events || []).reduce((max, evt) => Math.max(max, evt.seq || 0), 0);
|
|
2974
|
+
sessionChatBuffers.set(orchId, lines);
|
|
2975
|
+
sessionActivityBuffers.set(orchId, activityLines);
|
|
2976
|
+
sessionHistoryLoadedAt.set(orchId, Date.now());
|
|
2977
|
+
sessionRenderedCmsSeq.set(orchId, maxRenderedSeq);
|
|
2970
2978
|
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
}
|
|
2979
|
+
if (orchId === activeOrchId) {
|
|
2980
|
+
invalidateChat();
|
|
2981
|
+
invalidateActivity();
|
|
2982
|
+
}
|
|
2976
2983
|
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
}
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2984
|
+
if (!seqCmsSeededSessions.has(orchId)) {
|
|
2985
|
+
const existingSeq = seqEventBuffers.get(orchId) ?? [];
|
|
2986
|
+
if (existingSeq.length === 0) {
|
|
2987
|
+
const cmsNode = addSeqNode("cms");
|
|
2988
|
+
const seeded = [];
|
|
2989
|
+
for (const evt of events) {
|
|
2990
|
+
const t = fmtTime(evt.createdAt);
|
|
2991
|
+
if (evt.eventType === "user.message") {
|
|
2992
|
+
const txt = stripHostPrefix(evt.data?.content || "");
|
|
2993
|
+
if (txt && !isTimerPrompt(txt)) {
|
|
2994
|
+
seeded.push({ type: "user_msg_synth", time: t, orchNode: cmsNode, actNode: cmsNode, label: txt });
|
|
2995
|
+
}
|
|
2996
|
+
} else if (evt.eventType === "assistant.message") {
|
|
2997
|
+
const txt = evt.data?.content || "";
|
|
2998
|
+
if (txt) {
|
|
2999
|
+
seeded.push({ type: "response", time: t, orchNode: cmsNode, actNode: cmsNode, snippet: txt.slice(0, 40) });
|
|
3000
|
+
}
|
|
3001
|
+
} else if (evt.eventType === "tool.execution_start") {
|
|
3002
|
+
seeded.push({ type: "activity_start", time: t, orchNode: cmsNode, actNode: cmsNode });
|
|
2994
3003
|
}
|
|
2995
|
-
}
|
|
2996
|
-
|
|
3004
|
+
}
|
|
3005
|
+
if (seeded.length > 0) {
|
|
3006
|
+
seqEventBuffers.set(orchId, seeded);
|
|
2997
3007
|
}
|
|
2998
3008
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3009
|
+
seqCmsSeededSessions.add(orchId);
|
|
3010
|
+
}
|
|
3011
|
+
} catch (err) {
|
|
3012
|
+
loadFailed = true;
|
|
3013
|
+
appendLog(`{yellow-fg}CMS history load failed: ${err.message}{/yellow-fg}`);
|
|
3014
|
+
} finally {
|
|
3015
|
+
if (sessionHistoryLoadPromises.get(orchId) === loadPromise) {
|
|
3016
|
+
sessionHistoryLoadPromises.delete(orchId);
|
|
3002
3017
|
}
|
|
3003
|
-
seqCmsSeededSessions.add(orchId);
|
|
3004
3018
|
}
|
|
3005
|
-
}
|
|
3006
|
-
|
|
3007
|
-
|
|
3019
|
+
})();
|
|
3020
|
+
|
|
3021
|
+
sessionHistoryLoadPromises.set(orchId, loadPromise);
|
|
3022
|
+
try {
|
|
3023
|
+
return await loadPromise;
|
|
3024
|
+
} finally {
|
|
3025
|
+
perfEnd(_ph, {
|
|
3026
|
+
orchId: orchId.slice(0, 12),
|
|
3027
|
+
events: eventCount,
|
|
3028
|
+
err: loadFailed || undefined,
|
|
3029
|
+
force: force || undefined,
|
|
3030
|
+
});
|
|
3008
3031
|
}
|
|
3009
|
-
perfEnd(_ph, { orchId: orchId.slice(0, 12), events: eventCount, err: loadFailed || undefined });
|
|
3010
3032
|
}
|
|
3011
3033
|
|
|
3012
3034
|
// ─── Start the PilotSwarm client (embedded workers + client) ────────
|
|
@@ -3402,6 +3424,7 @@ const sessionAgentIds = new Map(); // orchId → agentId string (e.g. "pilotswar
|
|
|
3402
3424
|
const sessionChatBuffers = new Map(); // orchId → string[]
|
|
3403
3425
|
const sessionHistoryLoadedAt = new Map(); // orchId → epoch ms of last CMS history load
|
|
3404
3426
|
const sessionHistoryLoadGeneration = new Map(); // orchId → monotonically increasing async load token
|
|
3427
|
+
const sessionHistoryLoadPromises = new Map(); // orchId → in-flight CMS history load promise
|
|
3405
3428
|
const sessionRenderedCmsSeq = new Map(); // orchId → highest CMS seq already incorporated into buffers
|
|
3406
3429
|
const sessionExpandLevel = new Map(); // orchId → 0 (default) | 1 | 2 (how many times user expanded history)
|
|
3407
3430
|
const sessionSplashApplied = new Set(); // orchIds that have had splash prepended (idempotency guard)
|
|
@@ -6509,7 +6532,7 @@ screen.on("keypress", (ch, key) => {
|
|
|
6509
6532
|
const levelNames = ["", "expanded (500)", "full history"];
|
|
6510
6533
|
setStatus(`Loading ${levelNames[currentLevel + 1]}...`);
|
|
6511
6534
|
screen.render();
|
|
6512
|
-
loadCmsHistory(targetOrchId).then(() => {
|
|
6535
|
+
loadCmsHistory(targetOrchId, { force: true }).then(() => {
|
|
6513
6536
|
if (targetOrchId === activeOrchId) {
|
|
6514
6537
|
startCmsPoller(targetOrchId);
|
|
6515
6538
|
invalidateChat("top");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pilotswarm-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Terminal UI for pilotswarm — interactive durable agent orchestration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"url": "https://github.com/affandar/PilotSwarm/issues"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"pilotswarm-sdk": "^0.1.
|
|
33
|
+
"pilotswarm-sdk": "^0.1.4",
|
|
34
34
|
"@bradygaster/squad-cli": "^0.8.25",
|
|
35
35
|
"chalk": "^5.6.2",
|
|
36
36
|
"marked": "^15.0.12",
|