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.
- package/mcp/dist/src/session-id.d.ts +13 -0
- package/mcp/dist/src/session-id.d.ts.map +1 -0
- package/mcp/dist/src/session-id.js +15 -0
- package/mcp/dist/src/session-id.js.map +1 -0
- package/mcp/dist/src/tools/artifacts.d.ts.map +1 -1
- package/mcp/dist/src/tools/artifacts.js +5 -4
- package/mcp/dist/src/tools/artifacts.js.map +1 -1
- package/mcp/dist/src/tools/observability.d.ts.map +1 -1
- package/mcp/dist/src/tools/observability.js +2 -1
- package/mcp/dist/src/tools/observability.js.map +1 -1
- package/mcp/dist/src/tools/sessions.d.ts.map +1 -1
- package/mcp/dist/src/tools/sessions.js +9 -8
- package/mcp/dist/src/tools/sessions.js.map +1 -1
- package/mcp/dist/src/tools/turn-control.d.ts.map +1 -1
- package/mcp/dist/src/tools/turn-control.js +5 -4
- package/mcp/dist/src/tools/turn-control.js.map +1 -1
- package/package.json +3 -3
- package/tui/plugins/.mcp.json +1 -7
- package/tui/src/app.js +12 -1
- package/tui/src/node-sdk-transport.js +17 -0
- package/ui/core/src/controller.js +62 -11
- package/ui/core/src/history.js +34 -0
- package/ui/core/src/reducer.js +87 -0
- package/ui/core/src/selectors.js +281 -174
- package/ui/core/src/state.js +15 -0
- package/ui/react/src/components.js +27 -8
- package/ui/react/src/web-app.js +83 -38
- package/web/dist/assets/index-BKai0rVp.js +24 -0
- package/web/dist/assets/index-BRbv6TsU.css +1 -0
- package/web/dist/assets/pilotswarm-PUVWBejV.js +90 -0
- package/web/dist/assets/{react-CRYBfNR4.js → react-BouHbnso.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/runtime.js +23 -2
- package/web/dist/assets/index-BmqU2WsR.js +0 -24
- package/web/dist/assets/index-D2-7fi56.css +0 -1
- package/web/dist/assets/pilotswarm-cIaYAE9e.js +0 -90
package/ui/core/src/selectors.js
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
decorateArtifactLinksForChat,
|
|
11
11
|
extractArtifactLinks,
|
|
12
12
|
extractHttpLinks,
|
|
13
|
-
formatDisplayDateTime,
|
|
14
13
|
formatHumanDurationSeconds,
|
|
15
14
|
formatTimestamp,
|
|
16
15
|
padRunsToDisplayWidth,
|
|
@@ -21,9 +20,9 @@ import {
|
|
|
21
20
|
wrapRunsToDisplayWidth,
|
|
22
21
|
} from "./formatting.js";
|
|
23
22
|
import {
|
|
23
|
+
computeContextPercent,
|
|
24
24
|
getContextCompactionBadge,
|
|
25
25
|
getContextHeaderBadge,
|
|
26
|
-
getContextListBadge,
|
|
27
26
|
} from "./context-usage.js";
|
|
28
27
|
import { canonicalSystemTitle } from "./system-titles.js";
|
|
29
28
|
import { normalizeArtifactEntries } from "./state.js";
|
|
@@ -217,19 +216,22 @@ function ownerInitials(owner) {
|
|
|
217
216
|
}
|
|
218
217
|
|
|
219
218
|
function shouldDecorateSessionOwners(state) {
|
|
220
|
-
|
|
219
|
+
// Decorate owners only when the list actually surfaces more than one
|
|
220
|
+
// distinct human owner — otherwise the owner chip is pure noise on every
|
|
221
|
+
// row (you are always "you"). A narrowed owner filter is an explicit
|
|
222
|
+
// multi-user context, so honor that too.
|
|
221
223
|
if (state.sessions?.ownerFilter && state.sessions.ownerFilter.all !== true) return true;
|
|
222
|
-
|
|
224
|
+
const owners = new Set();
|
|
225
|
+
for (const session of Object.values(state.sessions?.byId || {})) {
|
|
226
|
+
if (!session || session.isSystem || session.isGroup) continue;
|
|
227
|
+
const key = ownerKeyForOwner(session.owner);
|
|
228
|
+
if (!key) continue;
|
|
229
|
+
owners.add(key);
|
|
230
|
+
if (owners.size > 1) return true;
|
|
231
|
+
}
|
|
223
232
|
return false;
|
|
224
233
|
}
|
|
225
234
|
|
|
226
|
-
function buildSessionListTitle(session, brandingTitle, decorateOwners = false) {
|
|
227
|
-
const title = buildSessionTitle(session, brandingTitle);
|
|
228
|
-
if (!decorateOwners || session?.isSystem) return title;
|
|
229
|
-
const prefix = session?.owner ? ownerInitials(session.owner) : "?";
|
|
230
|
-
return `(${prefix}) ${title}`;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
235
|
function groupMemberSessions(group, byId = {}) {
|
|
234
236
|
if (!group?.isGroup || !group?.groupId) return [];
|
|
235
237
|
return Object.values(byId || {}).filter((session) => (
|
|
@@ -575,68 +577,118 @@ function buildSessionRowView(entry, session, state, totalDescendantCounts, visib
|
|
|
575
577
|
|
|
576
578
|
const mainColor = session?.isGroup ? "cyan" : session?.isSystem ? "yellow" : sessionStatusColor(session, mode);
|
|
577
579
|
const effectiveOwner = effectiveSessionOwner(session, state.sessions?.byId || {});
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
// Show the session's last-updated time at the end of the row. For groups,
|
|
584
|
-
// keep the member-count badge but append the last-updated timestamp so the
|
|
585
|
-
// row reflects the same value the list is sorted by. `formatDisplayDateTime`
|
|
586
|
-
// uses the system locale/timezone, so the timestamp renders in the
|
|
587
|
-
// browser/client local zone.
|
|
580
|
+
|
|
581
|
+
const shortId = shortSessionId(session?.sessionId);
|
|
582
|
+
|
|
583
|
+
// Row age — last-updated (the value the list is sorted by). Coarse buckets
|
|
584
|
+
// so it doesn't tick every second: <1min · Nmin · NhMMm · NdHHh · Nw.
|
|
588
585
|
const rowTimestampMs = session?.updatedAt
|
|
589
586
|
|| session?.summaryUpdatedAt
|
|
590
587
|
|| session?.latestSummaryUpdatedAt
|
|
591
588
|
|| session?.createdAt
|
|
592
589
|
|| 0;
|
|
593
|
-
const
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
590
|
+
const relTime = rowTimestampMs ? formatSessionAge(rowTimestampMs) : "";
|
|
591
|
+
const modelLabel = shortModelReasoningLabel(session?.model, session?.reasoningEffort);
|
|
592
|
+
|
|
593
|
+
// A regular session with no human title otherwise renders as a bare
|
|
594
|
+
// "(guid)" — an empty, ugly line. For those, pull the meta (id · age ·
|
|
595
|
+
// model) UP onto the main line so it carries information. Titled rows keep
|
|
596
|
+
// the title on the line and expand id·age·model·ctx only when selected.
|
|
597
|
+
const rawTitle = session?.isSystem
|
|
598
|
+
? canonicalSystemTitle(session, state.branding?.title || "PilotSwarm")
|
|
599
|
+
: buildSessionDisplayTitle(session);
|
|
600
|
+
const hasRealTitle = Boolean(session?.isSystem || session?.isGroup || (rawTitle && rawTitle.trim()));
|
|
601
|
+
|
|
602
|
+
const titleRuns = [...prefixRuns];
|
|
603
|
+
// Owner chip — only when the list actually surfaces more than one human
|
|
604
|
+
// owner (shouldDecorateSessionOwners). Otherwise it's noise on every row.
|
|
605
|
+
if (shouldDecorateSessionOwners(state) && !session?.isSystem && !session?.isGroup) {
|
|
606
|
+
const initials = effectiveOwner ? ownerInitials(effectiveOwner) : "?";
|
|
607
|
+
titleRuns.push({ text: `${initials} · `, color: "cyan" });
|
|
608
|
+
}
|
|
609
|
+
if (hasRealTitle) {
|
|
610
|
+
titleRuns.push({ text: rawTitle, color: mainColor, bold: Boolean(session?.isSystem || session?.isGroup) });
|
|
611
|
+
} else {
|
|
612
|
+
// Untitled → id · age · model on one line (no wasted title line).
|
|
613
|
+
titleRuns.push({ text: shortId, color: mainColor });
|
|
614
|
+
if (relTime) { titleRuns.push({ text: " · ", color: "gray" }); titleRuns.push({ text: relTime, color: "gray" }); }
|
|
615
|
+
if (modelLabel) { titleRuns.push({ text: " · ", color: "gray" }); titleRuns.push({ text: modelLabel, color: "green" }); }
|
|
616
|
+
}
|
|
617
|
+
|
|
602
618
|
const collapseBadge = getCollapseBadge(session?.sessionId, entry, totalDescendantCounts, visibleDescendantCounts);
|
|
603
619
|
if (collapseBadge) {
|
|
604
620
|
titleRuns.push({ text: ` ${collapseBadge.text}`, color: collapseBadge.color, bold: collapseBadge.bold });
|
|
605
621
|
}
|
|
622
|
+
// Scheduled sessions keep a compact clock glyph on the title; the full
|
|
623
|
+
// cron cadence rides in the detail line.
|
|
624
|
+
const cronBadge = getCronBadge(session);
|
|
625
|
+
if (cronBadge) {
|
|
626
|
+
titleRuns.push({ text: " ⏱", color: "magenta" });
|
|
627
|
+
}
|
|
606
628
|
|
|
629
|
+
// Right-column context %: on every row that has usage. Compaction in
|
|
630
|
+
// flight takes precedence; else green normally, amber ≥70, red ≥85.
|
|
631
|
+
const ctxRuns = [];
|
|
632
|
+
const compactionState = session?.contextUsage?.compaction?.state;
|
|
633
|
+
const ctxPercent = computeContextPercent(session?.contextUsage);
|
|
634
|
+
if (session?.isGroup) {
|
|
635
|
+
if (session?.memberCount != null) ctxRuns.push({ text: `${session.memberCount}`, color: "gray" });
|
|
636
|
+
} else if (compactionState === "running") {
|
|
637
|
+
ctxRuns.push({ text: "⇊", color: "magenta" });
|
|
638
|
+
} else if (compactionState === "failed") {
|
|
639
|
+
ctxRuns.push({ text: "!", color: "red" });
|
|
640
|
+
} else if (ctxPercent != null) {
|
|
641
|
+
ctxRuns.push({ text: `${ctxPercent}%`, color: ctxPercent >= 85 ? "red" : ctxPercent >= 70 ? "yellow" : "green" });
|
|
642
|
+
} else {
|
|
643
|
+
ctxRuns.push({ text: "—", color: "gray" });
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Kept for backward-compat consumers; groups carry a member/time meta.
|
|
607
647
|
const metaRuns = [];
|
|
608
648
|
if (session?.isGroup && session?.memberCount != null) {
|
|
609
649
|
metaRuns.push({ text: `${session.memberCount} member${session.memberCount === 1 ? "" : "s"}`, color: "gray" });
|
|
650
|
+
if (relTime) { metaRuns.push({ text: " · ", color: "gray" }); metaRuns.push({ text: relTime, color: "gray" }); }
|
|
610
651
|
}
|
|
611
|
-
if (formattedRowTimestamp) {
|
|
612
|
-
if (metaRuns.length > 0) metaRuns.push({ text: " · ", color: "gray" });
|
|
613
|
-
metaRuns.push({ text: formattedRowTimestamp, color: "gray" });
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const badgeRuns = [];
|
|
617
652
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
]
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
653
|
+
// Detail — expanded under the SELECTED row. Titled rows repeat
|
|
654
|
+
// id · age · model here; untitled rows already carry those on the main
|
|
655
|
+
// line, so their detail starts at the ctx breakdown to avoid repetition.
|
|
656
|
+
const detailRuns = [];
|
|
657
|
+
const pushSep = () => { if (detailRuns.length) detailRuns.push({ text: " · ", color: "gray" }); };
|
|
658
|
+
if (session?.isGroup) {
|
|
659
|
+
detailRuns.push(...metaRuns);
|
|
660
|
+
} else {
|
|
661
|
+
if (hasRealTitle) {
|
|
662
|
+
detailRuns.push({ text: shortId, color: "cyan" });
|
|
663
|
+
if (relTime) { pushSep(); detailRuns.push({ text: relTime, color: "gray" }); }
|
|
664
|
+
if (modelLabel) { pushSep(); detailRuns.push({ text: modelLabel, color: "green" }); }
|
|
665
|
+
}
|
|
666
|
+
const cu = session?.contextUsage;
|
|
667
|
+
if (cu && Number.isFinite(cu.currentTokens) && Number.isFinite(cu.tokenLimit) && cu.tokenLimit > 0) {
|
|
668
|
+
pushSep();
|
|
669
|
+
detailRuns.push({ text: `ctx ${formatCompactNumber(cu.currentTokens)}/${formatCompactNumber(cu.tokenLimit)}`, color: "gray" });
|
|
670
|
+
}
|
|
671
|
+
const childCount = totalDescendantCounts?.[session?.sessionId];
|
|
672
|
+
if (childCount) { pushSep(); detailRuns.push({ text: `${childCount} child${childCount === 1 ? "" : "ren"}`, color: "gray" }); }
|
|
673
|
+
if (cronBadge) { pushSep(); detailRuns.push({ text: cronBadge.text, color: cronBadge.color }); }
|
|
625
674
|
}
|
|
626
675
|
|
|
627
|
-
|
|
676
|
+
// Flat runs for the TUI: title + (for titled rows) dim age + context.
|
|
628
677
|
const runs = [
|
|
629
678
|
...titleRuns,
|
|
630
|
-
...(
|
|
631
|
-
...(
|
|
679
|
+
...(hasRealTitle && relTime && !session?.isGroup ? [{ text: " ", color: "gray" }, { text: relTime, color: "gray" }] : []),
|
|
680
|
+
...(ctxRuns.length && !session?.isGroup ? [{ text: " · ", color: "gray" }, ...ctxRuns] : []),
|
|
681
|
+
...(session?.isGroup && metaRuns.length ? [{ text: " ", color: "gray" }, ...metaRuns] : []),
|
|
632
682
|
];
|
|
633
683
|
|
|
634
684
|
return {
|
|
635
685
|
runs,
|
|
636
686
|
titleRuns,
|
|
687
|
+
ctxRuns,
|
|
637
688
|
metaRuns,
|
|
638
|
-
badgeRuns,
|
|
639
|
-
selectedMetaRuns,
|
|
689
|
+
badgeRuns: [],
|
|
690
|
+
selectedMetaRuns: detailRuns,
|
|
691
|
+
detailRuns,
|
|
640
692
|
};
|
|
641
693
|
}
|
|
642
694
|
|
|
@@ -1245,9 +1297,16 @@ function buildChatMessagePrefix(message) {
|
|
|
1245
1297
|
glyph = "x";
|
|
1246
1298
|
glyphColor = "red";
|
|
1247
1299
|
} else if (message?.role === "user" && !message?.optimistic && !message?.pendingPhase) {
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1300
|
+
if (message?.stopped) {
|
|
1301
|
+
// Delivered, but its turn was user-stopped mid-flight — the model
|
|
1302
|
+
// may not have acted on it. Amber prohibition ("no parking") sign.
|
|
1303
|
+
glyph = "⊘";
|
|
1304
|
+
glyphColor = "yellow";
|
|
1305
|
+
} else {
|
|
1306
|
+
// Real durable user.message in transcript — show the "sent" double-check.
|
|
1307
|
+
glyph = "✓✓";
|
|
1308
|
+
glyphColor = "green";
|
|
1309
|
+
}
|
|
1251
1310
|
}
|
|
1252
1311
|
|
|
1253
1312
|
const roleColor = message?.pendingPhase === "pending"
|
|
@@ -1887,60 +1946,6 @@ const LIVE_ACTIVITY_SKIP = new Set([
|
|
|
1887
1946
|
"tool.execution_progress",
|
|
1888
1947
|
]);
|
|
1889
1948
|
|
|
1890
|
-
// Consecutive events of these types collapse into a single line (streaming
|
|
1891
|
-
// reasoning/intent fires many events per turn).
|
|
1892
|
-
const LIVE_ACTIVITY_COLLAPSE = new Set(["assistant.reasoning", "assistant.intent"]);
|
|
1893
|
-
|
|
1894
|
-
// Friendly verbs for the live activity lines, keyed by event type. An empty
|
|
1895
|
-
// string means "show the detail as-is" (e.g. tool calls, where the tool name is
|
|
1896
|
-
// the action). Types absent from the map fall back to their cleaned detail.
|
|
1897
|
-
const LIVE_ACTIVITY_VERBS = {
|
|
1898
|
-
"tool.execution_start": "",
|
|
1899
|
-
"tool.execution_complete": "",
|
|
1900
|
-
"assistant.reasoning": "Thinking",
|
|
1901
|
-
"assistant.intent": "Planning",
|
|
1902
|
-
"session.agent_spawned": "Spawning sub-agent",
|
|
1903
|
-
"session.wait_started": "Waiting",
|
|
1904
|
-
"session.input_required_started": "Awaiting input",
|
|
1905
|
-
"session.command_received": "Command",
|
|
1906
|
-
"session.command_completed": "Command",
|
|
1907
|
-
"session.compaction_start": "Compacting context",
|
|
1908
|
-
"session.compaction_complete": "Context compacted",
|
|
1909
|
-
"session.dehydrated": "Dehydrating",
|
|
1910
|
-
"session.hydrated": "Rehydrated",
|
|
1911
|
-
"session.rehydrated": "Rehydrated",
|
|
1912
|
-
"session.lossy_handoff": "Handed off to a new worker",
|
|
1913
|
-
"session.error": "Error",
|
|
1914
|
-
"session.cron_started": "Scheduled",
|
|
1915
|
-
"session.cron_scheduled": "Scheduled",
|
|
1916
|
-
"session.cron_fired": "Woke on schedule",
|
|
1917
|
-
"session.cron_at_scheduled": "Scheduled",
|
|
1918
|
-
"session.cron_at_started": "Scheduled",
|
|
1919
|
-
"session.cron_at_fired": "Woke on schedule",
|
|
1920
|
-
"system.message": "",
|
|
1921
|
-
};
|
|
1922
|
-
|
|
1923
|
-
// Turn a raw activity item into a friendly one-line action label: strip the
|
|
1924
|
-
// timestamp and [bracket] tag, then prefix a human verb where one applies.
|
|
1925
|
-
function friendlyLiveActivity(item, maxLen = 96) {
|
|
1926
|
-
const raw = String(item?.text || "")
|
|
1927
|
-
.replace(/^\s*\d{1,2}:\d{2}(?::\d{2})?\s*/, "")
|
|
1928
|
-
.replace(/^\[[^\]]+\]\s*/, "")
|
|
1929
|
-
.replace(/\s+/g, " ")
|
|
1930
|
-
.trim();
|
|
1931
|
-
const verb = LIVE_ACTIVITY_VERBS[item?.eventType];
|
|
1932
|
-
let text;
|
|
1933
|
-
if (verb === undefined || verb === "") {
|
|
1934
|
-
text = raw;
|
|
1935
|
-
} else if (raw) {
|
|
1936
|
-
text = `${verb} \u2014 ${raw}`;
|
|
1937
|
-
} else {
|
|
1938
|
-
text = verb;
|
|
1939
|
-
}
|
|
1940
|
-
text = text || "Working";
|
|
1941
|
-
return text.length > maxLen ? `${text.slice(0, maxLen - 1)}\u2026` : text;
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
1949
|
// Format an elapsed duration for the live activity card header (e.g. "12s",
|
|
1945
1950
|
// "1m 05s", "1h 02m").
|
|
1946
1951
|
function formatElapsed(ms) {
|
|
@@ -1954,12 +1959,87 @@ function formatElapsed(ms) {
|
|
|
1954
1959
|
return `${hours}h ${String(mins).padStart(2, "0")}m`;
|
|
1955
1960
|
}
|
|
1956
1961
|
|
|
1957
|
-
//
|
|
1958
|
-
//
|
|
1959
|
-
//
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1962
|
+
// Fact-store, graph-store, and skill tools get first-class phases instead of
|
|
1963
|
+
// the generic "running tool: X". Classification is pattern-based so new tools
|
|
1964
|
+
// in these families inherit the right phase without a table update.
|
|
1965
|
+
const FACTS_READ_TOOLS = new Set(["read_facts", "facts_search", "facts_similar", "facts_read_uncrawled", "facts_tombstone_stats"]);
|
|
1966
|
+
const FACTS_WRITE_TOOLS = new Set(["store_fact", "delete_fact", "facts_set_crawled", "facts_purge_tombstones", "facts_force_purge"]);
|
|
1967
|
+
function knowledgeToolPhase(name, starting) {
|
|
1968
|
+
const n = String(name || "").toLowerCase();
|
|
1969
|
+
if (n === "search_skills" || n.includes("skill")) {
|
|
1970
|
+
return starting ? "loading skills\u2026" : "loaded skills";
|
|
1971
|
+
}
|
|
1972
|
+
if (FACTS_READ_TOOLS.has(n)) return starting ? "reading facts\u2026" : "read facts";
|
|
1973
|
+
if (FACTS_WRITE_TOOLS.has(n)) return starting ? "writing facts\u2026" : "wrote facts";
|
|
1974
|
+
if (n.startsWith("graph_")) {
|
|
1975
|
+
const writes = /upsert|delete|archive|merge|remove|set_/.test(n);
|
|
1976
|
+
if (writes) return starting ? "writing to the graph\u2026" : "wrote to the graph";
|
|
1977
|
+
return starting ? "reading the graph\u2026" : "read the graph";
|
|
1978
|
+
}
|
|
1979
|
+
if (n.startsWith("facts_") || n.endsWith("_fact") || n.endsWith("_facts")) {
|
|
1980
|
+
// Unrecognized fact-family tool: read/write by verb.
|
|
1981
|
+
const writes = /store|write|set|delete|purge|update/.test(n);
|
|
1982
|
+
return writes ? (starting ? "writing facts\u2026" : "wrote facts") : (starting ? "reading facts\u2026" : "read facts");
|
|
1983
|
+
}
|
|
1984
|
+
return "";
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// High-level phase for the live activity status line. Maps the newest
|
|
1988
|
+
// activity event to a short human description ("thinking\u2026", "running tool:
|
|
1989
|
+
// read_file\u2026") — never raw payloads; detail lives in the Inspector.
|
|
1990
|
+
function liveActivityPhaseText(item) {
|
|
1991
|
+
const type = String(item?.eventType || "");
|
|
1992
|
+
if (type === "tool.execution_start" || type === "tool.execution_complete") {
|
|
1993
|
+
const raw = String(item?.text || "")
|
|
1994
|
+
.replace(/^\s*\d{1,2}:\d{2}(?::\d{2})?\s*/, "")
|
|
1995
|
+
.replace(/^\[[^\]]+\]\s*/, "")
|
|
1996
|
+
.replace(/^[^A-Za-z0-9_]+/, "")
|
|
1997
|
+
.trim();
|
|
1998
|
+
const name = (raw.match(/^[A-Za-z0-9_.:-]+/) || [""])[0];
|
|
1999
|
+
if (!name) return "running tools\u2026";
|
|
2000
|
+
const knowledge = knowledgeToolPhase(name, type === "tool.execution_start");
|
|
2001
|
+
if (knowledge) return knowledge;
|
|
2002
|
+
const shortName = name.length > 28 ? `${name.slice(0, 27)}\u2026` : name;
|
|
2003
|
+
return type === "tool.execution_start"
|
|
2004
|
+
? `running tool: ${shortName}\u2026`
|
|
2005
|
+
: `finished tool: ${shortName}`;
|
|
2006
|
+
}
|
|
2007
|
+
switch (type) {
|
|
2008
|
+
case "skill.invoked": return "loading skills\u2026";
|
|
2009
|
+
case "learned_skill.read": return "loading skills\u2026";
|
|
2010
|
+
case "skills.searched": return "loading skills\u2026";
|
|
2011
|
+
case "facts.searched":
|
|
2012
|
+
case "facts.similar": return "reading facts\u2026";
|
|
2013
|
+
case "graph.searched":
|
|
2014
|
+
case "graph.node_searched":
|
|
2015
|
+
case "graph.node_loaded": return "reading the graph\u2026";
|
|
2016
|
+
case "graph.namespace_mutated": return "writing to the graph\u2026";
|
|
2017
|
+
case "assistant.reasoning": return "thinking\u2026";
|
|
2018
|
+
case "assistant.intent": return "planning\u2026";
|
|
2019
|
+
case "assistant.message_start":
|
|
2020
|
+
case "assistant.message": return "writing reply\u2026";
|
|
2021
|
+
case "session.compaction_start": return "compacting context\u2026";
|
|
2022
|
+
case "session.compaction_complete": return "context compacted";
|
|
2023
|
+
case "session.dehydrated": return "dehydrating\u2026";
|
|
2024
|
+
case "session.hydrated":
|
|
2025
|
+
case "session.rehydrated": return "rehydrating\u2026";
|
|
2026
|
+
case "session.agent_spawned": return "coordinating agents\u2026";
|
|
2027
|
+
case "session.wait_started": return "waiting\u2026";
|
|
2028
|
+
case "session.input_required_started": return "awaiting input\u2026";
|
|
2029
|
+
case "session.lossy_handoff": return "moving to a new worker\u2026";
|
|
2030
|
+
case "session.error": return "recovering from an error\u2026";
|
|
2031
|
+
default: return "";
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// Single-line live activity status: while a turn is running, one dim line —
|
|
2036
|
+
// spinner + "Working" + elapsed + a high-level phase. Replaces the old
|
|
2037
|
+
// bordered multi-line card: hosts pin it OUTSIDE the scrolling transcript
|
|
2038
|
+
// (portal: bottom-sticky strip above the composer; TUI: appended below the
|
|
2039
|
+
// transcript, which autoscroll keeps at the bottom). Assistant messages can
|
|
2040
|
+
// land before the turn is actually done (streamed/tool-interleaved replies),
|
|
2041
|
+
// so visibility is governed by session status rather than transcript role.
|
|
2042
|
+
// `options.spinnerFrame` animates the marker.
|
|
1963
2043
|
export function selectLiveActivityLines(state, options = {}) {
|
|
1964
2044
|
if (state?.ui?.chatViewMode === "summary") return [];
|
|
1965
2045
|
const session = selectActiveSession(state);
|
|
@@ -1967,76 +2047,60 @@ export function selectLiveActivityLines(state, options = {}) {
|
|
|
1967
2047
|
const status = String(session?.status || "").toLowerCase();
|
|
1968
2048
|
if (status !== "running") return [];
|
|
1969
2049
|
const history = state.history?.bySessionId?.get(session.sessionId) || null;
|
|
1970
|
-
//
|
|
1971
|
-
// but do not hide the card just because assistant text has already landed;
|
|
1972
|
-
// the session may still be running tools or follow-up model work.
|
|
2050
|
+
// Latest user message anchors the elapsed clock when available.
|
|
1973
2051
|
const chat = Array.isArray(history?.chat) ? history.chat : [];
|
|
1974
2052
|
let turnStartAt = 0;
|
|
1975
2053
|
for (let i = chat.length - 1; i >= 0; i -= 1) {
|
|
1976
|
-
|
|
1977
|
-
if (role === "user") {
|
|
2054
|
+
if (chat[i]?.role === "user") {
|
|
1978
2055
|
turnStartAt = Number(chat[i]?.createdAt || 0);
|
|
1979
2056
|
break;
|
|
1980
2057
|
}
|
|
1981
2058
|
}
|
|
1982
2059
|
const activity = Array.isArray(history?.activity) ? history.activity : [];
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2060
|
+
// Current-turn boundaries. When a new turn starts, status flips to
|
|
2061
|
+
// "running" BEFORE fresh history lands; anchoring the clock on stale
|
|
2062
|
+
// data flashes a huge elapsed (the whole idle gap) that then snaps to
|
|
2063
|
+
// the real value. Anchor only on evidence belonging to THIS turn — a
|
|
2064
|
+
// user message or turn_start newer than the last turn end — and show
|
|
2065
|
+
// no timer (and no phase) until such evidence exists.
|
|
2066
|
+
let lastTurnStartAt = 0;
|
|
2067
|
+
let lastTurnEndAt = 0;
|
|
2068
|
+
for (let i = activity.length - 1; i >= 0; i -= 1) {
|
|
2069
|
+
const item = activity[i];
|
|
2070
|
+
const type = item?.eventType;
|
|
2071
|
+
const at = Number(item?.createdAt || 0);
|
|
2072
|
+
if (!lastTurnStartAt && type === "assistant.turn_start") lastTurnStartAt = at;
|
|
2073
|
+
if (!lastTurnEndAt && (type === "assistant.turn_end" || type === "session.turn_completed")) lastTurnEndAt = at;
|
|
2074
|
+
if (lastTurnStartAt && lastTurnEndAt) break;
|
|
2075
|
+
}
|
|
2076
|
+
let startAt = 0;
|
|
2077
|
+
if (turnStartAt > lastTurnEndAt) {
|
|
2078
|
+
startAt = turnStartAt; // user-triggered turn
|
|
2079
|
+
} else if (lastTurnStartAt > lastTurnEndAt) {
|
|
2080
|
+
startAt = lastTurnStartAt; // cron/command-triggered turn
|
|
2081
|
+
}
|
|
2082
|
+
let latest = null;
|
|
2083
|
+
for (let i = activity.length - 1; i >= 0; i -= 1) {
|
|
1988
2084
|
const item = activity[i];
|
|
1989
2085
|
if (!item || LIVE_ACTIVITY_SKIP.has(item.eventType)) continue;
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
if (
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
if (at > 0) oldestGatheredAt = oldestGatheredAt ? Math.min(oldestGatheredAt, at) : at;
|
|
1997
|
-
}
|
|
1998
|
-
if (recent.length === 0) return [];
|
|
1999
|
-
// Render as a bordered card: the portal turns box-drawing lines whose top
|
|
2000
|
-
// row has >2 runs into a `ps-chat-card`; the terminal shows the box art.
|
|
2001
|
-
// Header carries the animated spinner + "Working"; body lists the recent
|
|
2002
|
-
// actions, most recent highlighted.
|
|
2003
|
-
const borderColor = "cyan";
|
|
2004
|
-
const spinner = String(options.spinnerFrame || "").trim() || "\u25cf";
|
|
2086
|
+
// Phase must come from the current turn — a stale "finished tool"
|
|
2087
|
+
// from the previous turn must not flash on turn start.
|
|
2088
|
+
if (Number(item.createdAt || 0) <= lastTurnEndAt) break;
|
|
2089
|
+
latest = item;
|
|
2090
|
+
break;
|
|
2091
|
+
}
|
|
2005
2092
|
const nowMs = Number.isFinite(options.now) ? options.now : Date.now();
|
|
2006
|
-
const startAt = turnStartAt || oldestGatheredAt;
|
|
2007
2093
|
const elapsedLabel = startAt > 0 ? formatElapsed(nowMs - startAt) : "";
|
|
2008
|
-
const
|
|
2009
|
-
|
|
2010
|
-
|
|
2094
|
+
const spinner = String(options.spinnerFrame || "").trim() || "\u25cf";
|
|
2095
|
+
const phase = latest ? liveActivityPhaseText(latest) : "";
|
|
2096
|
+
const runs = [
|
|
2097
|
+
{ text: `${spinner} `, color: "cyan" },
|
|
2098
|
+
{ text: "Working", color: "cyan" },
|
|
2099
|
+
...(elapsedLabel ? [{ text: ` \u00b7 ${elapsedLabel}`, color: "gray" }] : []),
|
|
2100
|
+
...(phase ? [{ text: ` \u2014 ${phase}`, color: "gray" }] : []),
|
|
2011
2101
|
];
|
|
2012
|
-
const
|
|
2013
|
-
|
|
2014
|
-
const widestBody = recent.reduce((max, detail) => Math.max(max, detail.length + 2), 0);
|
|
2015
|
-
const cardWidth = Math.max(20, Math.min(maxWidth, Math.max(titleWidth + 3, widestBody + 4)));
|
|
2016
|
-
const contentWidth = Math.max(1, cardWidth - 4);
|
|
2017
|
-
const bodyLines = recent.map((detail, index) => {
|
|
2018
|
-
const isLatest = index === recent.length - 1;
|
|
2019
|
-
return fitRuns([
|
|
2020
|
-
{ text: isLatest ? "\u25cf " : "\u00b7 ", color: isLatest ? "cyan" : "gray", bold: isLatest },
|
|
2021
|
-
{ text: detail, color: isLatest ? "cyan" : "gray" },
|
|
2022
|
-
], contentWidth);
|
|
2023
|
-
});
|
|
2024
|
-
const topFill = Math.max(0, cardWidth - titleWidth - 3);
|
|
2025
|
-
const card = [[
|
|
2026
|
-
{ text: "\u250c\u2500", color: borderColor },
|
|
2027
|
-
...titleRuns,
|
|
2028
|
-
{ text: `${"\u2500".repeat(topFill)}\u2510`, color: borderColor },
|
|
2029
|
-
]];
|
|
2030
|
-
for (const runs of bodyLines) {
|
|
2031
|
-
card.push([
|
|
2032
|
-
{ text: "\u2502 ", color: borderColor },
|
|
2033
|
-
...padRunsToDisplayWidth(runs, contentWidth),
|
|
2034
|
-
{ text: " \u2502", color: borderColor },
|
|
2035
|
-
]);
|
|
2036
|
-
}
|
|
2037
|
-
card.push([{ text: `\u2514${"\u2500".repeat(Math.max(1, cardWidth - 2))}\u2518`, color: borderColor }]);
|
|
2038
|
-
card.push([{ text: "", color: null }]);
|
|
2039
|
-
return card;
|
|
2102
|
+
const maxWidth = Math.max(24, Math.min(120, Math.floor(Number(options.maxWidth) || 80)));
|
|
2103
|
+
return [fitRuns(runs, maxWidth)];
|
|
2040
2104
|
}
|
|
2041
2105
|
|
|
2042
2106
|
export function selectChatLines(state, maxWidth = 80, options = {}) {
|
|
@@ -2735,19 +2799,34 @@ export function selectAdminConsole(state) {
|
|
|
2735
2799
|
}
|
|
2736
2800
|
: (state.auth?.principal || null);
|
|
2737
2801
|
|
|
2802
|
+
const systemGhcpKey = admin.systemGhcpKey || { supported: false, loading: false, configured: false, changedBy: null, changedAt: null, error: null };
|
|
2803
|
+
const isAdmin = Boolean(profile?.isAdmin);
|
|
2804
|
+
// Admin-only target switch: when on, Set/Replace/Clear act on the
|
|
2805
|
+
// SYSTEM user's key (ownerless system sessions run on it).
|
|
2806
|
+
const storeAsSystem = isAdmin && Boolean(ghcpKey.storeAsSystem);
|
|
2807
|
+
const targetConfigured = storeAsSystem ? Boolean(systemGhcpKey.configured) : Boolean(profile?.githubCopilotKeySet);
|
|
2808
|
+
const keyNoun = storeAsSystem ? "System key" : "key";
|
|
2809
|
+
|
|
2810
|
+
const systemProvenance = systemGhcpKey.configured && systemGhcpKey.changedBy
|
|
2811
|
+
? ` (set by ${systemGhcpKey.changedBy})`
|
|
2812
|
+
: "";
|
|
2738
2813
|
const ghcpStatusText = ghcpKey.saving
|
|
2739
2814
|
? "Saving..."
|
|
2740
2815
|
: ghcpKey.editing
|
|
2741
2816
|
? "Editing — Enter to save, Esc to cancel"
|
|
2742
|
-
:
|
|
2743
|
-
?
|
|
2744
|
-
|
|
2817
|
+
: storeAsSystem
|
|
2818
|
+
? (systemGhcpKey.configured
|
|
2819
|
+
? `System key configured — ownerless system sessions use it for GitHub Copilot models${systemProvenance}`
|
|
2820
|
+
: "System key not configured — ownerless system sessions fall back to env GITHUB_TOKEN")
|
|
2821
|
+
: (profile?.githubCopilotKeySet
|
|
2822
|
+
? "Configured (overrides env GITHUB_TOKEN for this user)"
|
|
2823
|
+
: "Not configured (using env GITHUB_TOKEN fallback)");
|
|
2745
2824
|
|
|
2746
2825
|
const actions = [];
|
|
2747
2826
|
if (!ghcpKey.editing) {
|
|
2748
|
-
actions.push({ id: "edit", label:
|
|
2749
|
-
if (
|
|
2750
|
-
actions.push({ id: "clear", label:
|
|
2827
|
+
actions.push({ id: "edit", label: targetConfigured ? `Replace ${keyNoun}` : `Set ${keyNoun}`, key: "e" });
|
|
2828
|
+
if (targetConfigured) {
|
|
2829
|
+
actions.push({ id: "clear", label: `Clear ${keyNoun}`, key: "c" });
|
|
2751
2830
|
}
|
|
2752
2831
|
actions.push({ id: "refresh", label: "Refresh", key: "r" });
|
|
2753
2832
|
} else {
|
|
@@ -2761,8 +2840,11 @@ export function selectAdminConsole(state) {
|
|
|
2761
2840
|
loading: Boolean(admin.loading),
|
|
2762
2841
|
loadError: admin.loadError || null,
|
|
2763
2842
|
principal,
|
|
2843
|
+
isAdmin,
|
|
2764
2844
|
ghcpKey: {
|
|
2765
2845
|
configured: Boolean(profile?.githubCopilotKeySet),
|
|
2846
|
+
targetConfigured,
|
|
2847
|
+
storeAsSystem,
|
|
2766
2848
|
editing: Boolean(ghcpKey.editing),
|
|
2767
2849
|
draft: ghcpKey.draft || "",
|
|
2768
2850
|
cursorIndex: Math.max(0, Math.min(Number(ghcpKey.cursorIndex) || 0, String(ghcpKey.draft || "").length)),
|
|
@@ -2770,6 +2852,14 @@ export function selectAdminConsole(state) {
|
|
|
2770
2852
|
error: ghcpKey.error || null,
|
|
2771
2853
|
statusText: ghcpStatusText,
|
|
2772
2854
|
},
|
|
2855
|
+
systemGhcpKey: {
|
|
2856
|
+
supported: Boolean(systemGhcpKey.supported),
|
|
2857
|
+
loading: Boolean(systemGhcpKey.loading),
|
|
2858
|
+
configured: Boolean(systemGhcpKey.configured),
|
|
2859
|
+
changedBy: systemGhcpKey.changedBy || null,
|
|
2860
|
+
changedAt: systemGhcpKey.changedAt || null,
|
|
2861
|
+
error: systemGhcpKey.error || null,
|
|
2862
|
+
},
|
|
2773
2863
|
actions,
|
|
2774
2864
|
};
|
|
2775
2865
|
}
|
|
@@ -4607,6 +4697,23 @@ function formatRelativeTime(ts) {
|
|
|
4607
4697
|
return `${Math.floor(ms / 86_400_000)}d ago`;
|
|
4608
4698
|
}
|
|
4609
4699
|
|
|
4700
|
+
// Coarse session-age buckets for the list. Seconds tick too fast and just
|
|
4701
|
+
// distract, so the smallest bucket is "<1min"; then whole minutes to an hour,
|
|
4702
|
+
// then hours+minutes, then days+hours, then weeks. No "ago" suffix — the age
|
|
4703
|
+
// column context makes it clear.
|
|
4704
|
+
function formatSessionAge(ts) {
|
|
4705
|
+
if (!ts) return "—";
|
|
4706
|
+
const ms = Date.now() - ts;
|
|
4707
|
+
if (ms < 60_000) return "<1min";
|
|
4708
|
+
const totalMin = Math.floor(ms / 60_000);
|
|
4709
|
+
if (totalMin < 60) return `${totalMin}min`;
|
|
4710
|
+
const totalHours = Math.floor(totalMin / 60);
|
|
4711
|
+
if (totalHours < 24) return `${totalHours}h${String(totalMin % 60).padStart(2, "0")}m`;
|
|
4712
|
+
const days = Math.floor(totalHours / 24);
|
|
4713
|
+
if (days < 14) return `${days}d${String(totalHours % 24).padStart(2, "0")}h`;
|
|
4714
|
+
return `${Math.floor(days / 7)}w`;
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4610
4717
|
function formatLocalTimestamp(ts) {
|
|
4611
4718
|
if (!ts) return "—";
|
|
4612
4719
|
const date = ts instanceof Date ? ts : new Date(ts);
|
package/ui/core/src/state.js
CHANGED
|
@@ -247,6 +247,21 @@ export function createInitialState({ mode = "local", branding = null, themeId =
|
|
|
247
247
|
saving: false,
|
|
248
248
|
error: null,
|
|
249
249
|
lastSavedAt: 0,
|
|
250
|
+
// Admin-only target switch: when true, Set/Replace/Clear
|
|
251
|
+
// act on the SYSTEM user's key (used by ownerless system
|
|
252
|
+
// sessions) instead of the caller's own key.
|
|
253
|
+
storeAsSystem: false,
|
|
254
|
+
},
|
|
255
|
+
// Status of the SYSTEM user's Copilot key (admins only; never
|
|
256
|
+
// the key itself). `supported` flips on the first successful
|
|
257
|
+
// status load so non-admin/legacy transports hide the toggle.
|
|
258
|
+
systemGhcpKey: {
|
|
259
|
+
supported: false,
|
|
260
|
+
loading: false,
|
|
261
|
+
configured: false,
|
|
262
|
+
changedBy: null,
|
|
263
|
+
changedAt: null,
|
|
264
|
+
error: null,
|
|
250
265
|
},
|
|
251
266
|
},
|
|
252
267
|
};
|