pilotswarm 0.5.0 → 0.5.2
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 +36 -5
- package/tui/src/node-sdk-transport.js +17 -0
- package/ui/core/src/commands.js +6 -0
- package/ui/core/src/controller.js +193 -10
- package/ui/core/src/reducer.js +118 -1
- package/ui/core/src/selectors.js +428 -21
- package/ui/core/src/state.js +17 -0
- package/ui/react/src/chat-status.js +22 -0
- package/ui/react/src/components.js +68 -6
- package/ui/react/src/web-app.js +67 -13
- package/web/dist/assets/index-BA1Gd5ZC.js +24 -0
- package/web/dist/assets/index-Jr_Wn0fg.css +1 -0
- package/web/dist/assets/pilotswarm-C6iSDexX.js +90 -0
- package/web/dist/assets/{react-IRUjK5Iz.js → react-CqzaLZob.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/runtime.js +23 -2
- package/web/dist/assets/index-B8ge3rzb.js +0 -24
- package/web/dist/assets/index-DXRIxmIv.css +0 -1
- package/web/dist/assets/pilotswarm-Dt1qY6hD.js +0 -90
package/ui/core/src/selectors.js
CHANGED
|
@@ -1868,6 +1868,182 @@ function buildChatMessageLines(message, maxWidth, options = {}) {
|
|
|
1868
1868
|
return renderedLines;
|
|
1869
1869
|
}
|
|
1870
1870
|
|
|
1871
|
+
// Activity-log event types that are not surfaced as live "recent action" lines
|
|
1872
|
+
// in the chat pane: bookkeeping, high-frequency streaming noise, and the turn
|
|
1873
|
+
// bracket markers (which don't read well as prose).
|
|
1874
|
+
const LIVE_ACTIVITY_SKIP = new Set([
|
|
1875
|
+
"assistant.usage",
|
|
1876
|
+
"session.info",
|
|
1877
|
+
"session.idle",
|
|
1878
|
+
"session.usage_info",
|
|
1879
|
+
"pending_messages.modified",
|
|
1880
|
+
"pending_messages.cancelled",
|
|
1881
|
+
"abort",
|
|
1882
|
+
"assistant.turn_start",
|
|
1883
|
+
"assistant.turn_end",
|
|
1884
|
+
"assistant.streaming_progress",
|
|
1885
|
+
"session.turn_completed",
|
|
1886
|
+
"tool.execution_partial_result",
|
|
1887
|
+
"tool.execution_progress",
|
|
1888
|
+
]);
|
|
1889
|
+
|
|
1890
|
+
// Format an elapsed duration for the live activity card header (e.g. "12s",
|
|
1891
|
+
// "1m 05s", "1h 02m").
|
|
1892
|
+
function formatElapsed(ms) {
|
|
1893
|
+
const totalSec = Math.max(0, Math.floor((Number(ms) || 0) / 1000));
|
|
1894
|
+
if (totalSec < 60) return `${totalSec}s`;
|
|
1895
|
+
const minutes = Math.floor(totalSec / 60);
|
|
1896
|
+
const seconds = totalSec % 60;
|
|
1897
|
+
if (minutes < 60) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
1898
|
+
const hours = Math.floor(minutes / 60);
|
|
1899
|
+
const mins = minutes % 60;
|
|
1900
|
+
return `${hours}h ${String(mins).padStart(2, "0")}m`;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// Fact-store, graph-store, and skill tools get first-class phases instead of
|
|
1904
|
+
// the generic "running tool: X". Classification is pattern-based so new tools
|
|
1905
|
+
// in these families inherit the right phase without a table update.
|
|
1906
|
+
const FACTS_READ_TOOLS = new Set(["read_facts", "facts_search", "facts_similar", "facts_read_uncrawled", "facts_tombstone_stats"]);
|
|
1907
|
+
const FACTS_WRITE_TOOLS = new Set(["store_fact", "delete_fact", "facts_set_crawled", "facts_purge_tombstones", "facts_force_purge"]);
|
|
1908
|
+
function knowledgeToolPhase(name, starting) {
|
|
1909
|
+
const n = String(name || "").toLowerCase();
|
|
1910
|
+
if (n === "search_skills" || n.includes("skill")) {
|
|
1911
|
+
return starting ? "loading skills\u2026" : "loaded skills";
|
|
1912
|
+
}
|
|
1913
|
+
if (FACTS_READ_TOOLS.has(n)) return starting ? "reading facts\u2026" : "read facts";
|
|
1914
|
+
if (FACTS_WRITE_TOOLS.has(n)) return starting ? "writing facts\u2026" : "wrote facts";
|
|
1915
|
+
if (n.startsWith("graph_")) {
|
|
1916
|
+
const writes = /upsert|delete|archive|merge|remove|set_/.test(n);
|
|
1917
|
+
if (writes) return starting ? "writing to the graph\u2026" : "wrote to the graph";
|
|
1918
|
+
return starting ? "reading the graph\u2026" : "read the graph";
|
|
1919
|
+
}
|
|
1920
|
+
if (n.startsWith("facts_") || n.endsWith("_fact") || n.endsWith("_facts")) {
|
|
1921
|
+
// Unrecognized fact-family tool: read/write by verb.
|
|
1922
|
+
const writes = /store|write|set|delete|purge|update/.test(n);
|
|
1923
|
+
return writes ? (starting ? "writing facts\u2026" : "wrote facts") : (starting ? "reading facts\u2026" : "read facts");
|
|
1924
|
+
}
|
|
1925
|
+
return "";
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// High-level phase for the live activity status line. Maps the newest
|
|
1929
|
+
// activity event to a short human description ("thinking\u2026", "running tool:
|
|
1930
|
+
// read_file\u2026") — never raw payloads; detail lives in the Inspector.
|
|
1931
|
+
function liveActivityPhaseText(item) {
|
|
1932
|
+
const type = String(item?.eventType || "");
|
|
1933
|
+
if (type === "tool.execution_start" || type === "tool.execution_complete") {
|
|
1934
|
+
const raw = String(item?.text || "")
|
|
1935
|
+
.replace(/^\s*\d{1,2}:\d{2}(?::\d{2})?\s*/, "")
|
|
1936
|
+
.replace(/^\[[^\]]+\]\s*/, "")
|
|
1937
|
+
.replace(/^[^A-Za-z0-9_]+/, "")
|
|
1938
|
+
.trim();
|
|
1939
|
+
const name = (raw.match(/^[A-Za-z0-9_.:-]+/) || [""])[0];
|
|
1940
|
+
if (!name) return "running tools\u2026";
|
|
1941
|
+
const knowledge = knowledgeToolPhase(name, type === "tool.execution_start");
|
|
1942
|
+
if (knowledge) return knowledge;
|
|
1943
|
+
const shortName = name.length > 28 ? `${name.slice(0, 27)}\u2026` : name;
|
|
1944
|
+
return type === "tool.execution_start"
|
|
1945
|
+
? `running tool: ${shortName}\u2026`
|
|
1946
|
+
: `finished tool: ${shortName}`;
|
|
1947
|
+
}
|
|
1948
|
+
switch (type) {
|
|
1949
|
+
case "skill.invoked": return "loading skills\u2026";
|
|
1950
|
+
case "learned_skill.read": return "loading skills\u2026";
|
|
1951
|
+
case "skills.searched": return "loading skills\u2026";
|
|
1952
|
+
case "facts.searched":
|
|
1953
|
+
case "facts.similar": return "reading facts\u2026";
|
|
1954
|
+
case "graph.searched":
|
|
1955
|
+
case "graph.node_searched":
|
|
1956
|
+
case "graph.node_loaded": return "reading the graph\u2026";
|
|
1957
|
+
case "graph.namespace_mutated": return "writing to the graph\u2026";
|
|
1958
|
+
case "assistant.reasoning": return "thinking\u2026";
|
|
1959
|
+
case "assistant.intent": return "planning\u2026";
|
|
1960
|
+
case "assistant.message_start":
|
|
1961
|
+
case "assistant.message": return "writing reply\u2026";
|
|
1962
|
+
case "session.compaction_start": return "compacting context\u2026";
|
|
1963
|
+
case "session.compaction_complete": return "context compacted";
|
|
1964
|
+
case "session.dehydrated": return "dehydrating\u2026";
|
|
1965
|
+
case "session.hydrated":
|
|
1966
|
+
case "session.rehydrated": return "rehydrating\u2026";
|
|
1967
|
+
case "session.agent_spawned": return "coordinating agents\u2026";
|
|
1968
|
+
case "session.wait_started": return "waiting\u2026";
|
|
1969
|
+
case "session.input_required_started": return "awaiting input\u2026";
|
|
1970
|
+
case "session.lossy_handoff": return "moving to a new worker\u2026";
|
|
1971
|
+
case "session.error": return "recovering from an error\u2026";
|
|
1972
|
+
default: return "";
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// Single-line live activity status: while a turn is running, one dim line —
|
|
1977
|
+
// spinner + "Working" + elapsed + a high-level phase. Replaces the old
|
|
1978
|
+
// bordered multi-line card: hosts pin it OUTSIDE the scrolling transcript
|
|
1979
|
+
// (portal: bottom-sticky strip above the composer; TUI: appended below the
|
|
1980
|
+
// transcript, which autoscroll keeps at the bottom). Assistant messages can
|
|
1981
|
+
// land before the turn is actually done (streamed/tool-interleaved replies),
|
|
1982
|
+
// so visibility is governed by session status rather than transcript role.
|
|
1983
|
+
// `options.spinnerFrame` animates the marker.
|
|
1984
|
+
export function selectLiveActivityLines(state, options = {}) {
|
|
1985
|
+
if (state?.ui?.chatViewMode === "summary") return [];
|
|
1986
|
+
const session = selectActiveSession(state);
|
|
1987
|
+
if (!session || session.isGroup) return [];
|
|
1988
|
+
const status = String(session?.status || "").toLowerCase();
|
|
1989
|
+
if (status !== "running") return [];
|
|
1990
|
+
const history = state.history?.bySessionId?.get(session.sessionId) || null;
|
|
1991
|
+
// Latest user message anchors the elapsed clock when available.
|
|
1992
|
+
const chat = Array.isArray(history?.chat) ? history.chat : [];
|
|
1993
|
+
let turnStartAt = 0;
|
|
1994
|
+
for (let i = chat.length - 1; i >= 0; i -= 1) {
|
|
1995
|
+
if (chat[i]?.role === "user") {
|
|
1996
|
+
turnStartAt = Number(chat[i]?.createdAt || 0);
|
|
1997
|
+
break;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
const activity = Array.isArray(history?.activity) ? history.activity : [];
|
|
2001
|
+
// Current-turn boundaries. When a new turn starts, status flips to
|
|
2002
|
+
// "running" BEFORE fresh history lands; anchoring the clock on stale
|
|
2003
|
+
// data flashes a huge elapsed (the whole idle gap) that then snaps to
|
|
2004
|
+
// the real value. Anchor only on evidence belonging to THIS turn — a
|
|
2005
|
+
// user message or turn_start newer than the last turn end — and show
|
|
2006
|
+
// no timer (and no phase) until such evidence exists.
|
|
2007
|
+
let lastTurnStartAt = 0;
|
|
2008
|
+
let lastTurnEndAt = 0;
|
|
2009
|
+
for (let i = activity.length - 1; i >= 0; i -= 1) {
|
|
2010
|
+
const item = activity[i];
|
|
2011
|
+
const type = item?.eventType;
|
|
2012
|
+
const at = Number(item?.createdAt || 0);
|
|
2013
|
+
if (!lastTurnStartAt && type === "assistant.turn_start") lastTurnStartAt = at;
|
|
2014
|
+
if (!lastTurnEndAt && (type === "assistant.turn_end" || type === "session.turn_completed")) lastTurnEndAt = at;
|
|
2015
|
+
if (lastTurnStartAt && lastTurnEndAt) break;
|
|
2016
|
+
}
|
|
2017
|
+
let startAt = 0;
|
|
2018
|
+
if (turnStartAt > lastTurnEndAt) {
|
|
2019
|
+
startAt = turnStartAt; // user-triggered turn
|
|
2020
|
+
} else if (lastTurnStartAt > lastTurnEndAt) {
|
|
2021
|
+
startAt = lastTurnStartAt; // cron/command-triggered turn
|
|
2022
|
+
}
|
|
2023
|
+
let latest = null;
|
|
2024
|
+
for (let i = activity.length - 1; i >= 0; i -= 1) {
|
|
2025
|
+
const item = activity[i];
|
|
2026
|
+
if (!item || LIVE_ACTIVITY_SKIP.has(item.eventType)) continue;
|
|
2027
|
+
// Phase must come from the current turn — a stale "finished tool"
|
|
2028
|
+
// from the previous turn must not flash on turn start.
|
|
2029
|
+
if (Number(item.createdAt || 0) <= lastTurnEndAt) break;
|
|
2030
|
+
latest = item;
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
2033
|
+
const nowMs = Number.isFinite(options.now) ? options.now : Date.now();
|
|
2034
|
+
const elapsedLabel = startAt > 0 ? formatElapsed(nowMs - startAt) : "";
|
|
2035
|
+
const spinner = String(options.spinnerFrame || "").trim() || "\u25cf";
|
|
2036
|
+
const phase = latest ? liveActivityPhaseText(latest) : "";
|
|
2037
|
+
const runs = [
|
|
2038
|
+
{ text: `${spinner} `, color: "cyan" },
|
|
2039
|
+
{ text: "Working", color: "cyan" },
|
|
2040
|
+
...(elapsedLabel ? [{ text: ` \u00b7 ${elapsedLabel}`, color: "gray" }] : []),
|
|
2041
|
+
...(phase ? [{ text: ` \u2014 ${phase}`, color: "gray" }] : []),
|
|
2042
|
+
];
|
|
2043
|
+
const maxWidth = Math.max(24, Math.min(120, Math.floor(Number(options.maxWidth) || 80)));
|
|
2044
|
+
return [fitRuns(runs, maxWidth)];
|
|
2045
|
+
}
|
|
2046
|
+
|
|
1871
2047
|
export function selectChatLines(state, maxWidth = 80, options = {}) {
|
|
1872
2048
|
const messages = selectActiveChat(state);
|
|
1873
2049
|
if (!messages || messages.length === 0) {
|
|
@@ -2020,10 +2196,19 @@ export function selectChatPaneChrome(state, options = {}) {
|
|
|
2020
2196
|
? null
|
|
2021
2197
|
: buildLiveProgressState(session, history, history?.chat || [], outboxItems);
|
|
2022
2198
|
|
|
2199
|
+
// Border top-right: status · model · context (mirrors the session-row meta),
|
|
2200
|
+
// with the live turn-progress label appended while a turn is running.
|
|
2201
|
+
const mode = state.connection?.mode || "local";
|
|
2202
|
+
const metaRuns = buildSelectedSessionMetaRuns(session, mode);
|
|
2203
|
+
const progressRuns = buildChatProgressTitleRuns(progress);
|
|
2204
|
+
const titleRight = progressRuns
|
|
2205
|
+
? [...metaRuns, ...(metaRuns.length ? [{ text: " · ", color: "gray" }] : []), ...progressRuns]
|
|
2206
|
+
: metaRuns;
|
|
2207
|
+
|
|
2023
2208
|
return {
|
|
2024
2209
|
color: session.isSystem ? "yellow" : "cyan",
|
|
2025
2210
|
title,
|
|
2026
|
-
titleRight:
|
|
2211
|
+
titleRight: titleRight.length ? titleRight : null,
|
|
2027
2212
|
animateTitleRight: Boolean(progress),
|
|
2028
2213
|
};
|
|
2029
2214
|
}
|
|
@@ -2555,19 +2740,34 @@ export function selectAdminConsole(state) {
|
|
|
2555
2740
|
}
|
|
2556
2741
|
: (state.auth?.principal || null);
|
|
2557
2742
|
|
|
2743
|
+
const systemGhcpKey = admin.systemGhcpKey || { supported: false, loading: false, configured: false, changedBy: null, changedAt: null, error: null };
|
|
2744
|
+
const isAdmin = Boolean(profile?.isAdmin);
|
|
2745
|
+
// Admin-only target switch: when on, Set/Replace/Clear act on the
|
|
2746
|
+
// SYSTEM user's key (ownerless system sessions run on it).
|
|
2747
|
+
const storeAsSystem = isAdmin && Boolean(ghcpKey.storeAsSystem);
|
|
2748
|
+
const targetConfigured = storeAsSystem ? Boolean(systemGhcpKey.configured) : Boolean(profile?.githubCopilotKeySet);
|
|
2749
|
+
const keyNoun = storeAsSystem ? "System key" : "key";
|
|
2750
|
+
|
|
2751
|
+
const systemProvenance = systemGhcpKey.configured && systemGhcpKey.changedBy
|
|
2752
|
+
? ` (set by ${systemGhcpKey.changedBy})`
|
|
2753
|
+
: "";
|
|
2558
2754
|
const ghcpStatusText = ghcpKey.saving
|
|
2559
2755
|
? "Saving..."
|
|
2560
2756
|
: ghcpKey.editing
|
|
2561
2757
|
? "Editing — Enter to save, Esc to cancel"
|
|
2562
|
-
:
|
|
2563
|
-
?
|
|
2564
|
-
|
|
2758
|
+
: storeAsSystem
|
|
2759
|
+
? (systemGhcpKey.configured
|
|
2760
|
+
? `System key configured — ownerless system sessions use it for GitHub Copilot models${systemProvenance}`
|
|
2761
|
+
: "System key not configured — ownerless system sessions fall back to env GITHUB_TOKEN")
|
|
2762
|
+
: (profile?.githubCopilotKeySet
|
|
2763
|
+
? "Configured (overrides env GITHUB_TOKEN for this user)"
|
|
2764
|
+
: "Not configured (using env GITHUB_TOKEN fallback)");
|
|
2565
2765
|
|
|
2566
2766
|
const actions = [];
|
|
2567
2767
|
if (!ghcpKey.editing) {
|
|
2568
|
-
actions.push({ id: "edit", label:
|
|
2569
|
-
if (
|
|
2570
|
-
actions.push({ id: "clear", label:
|
|
2768
|
+
actions.push({ id: "edit", label: targetConfigured ? `Replace ${keyNoun}` : `Set ${keyNoun}`, key: "e" });
|
|
2769
|
+
if (targetConfigured) {
|
|
2770
|
+
actions.push({ id: "clear", label: `Clear ${keyNoun}`, key: "c" });
|
|
2571
2771
|
}
|
|
2572
2772
|
actions.push({ id: "refresh", label: "Refresh", key: "r" });
|
|
2573
2773
|
} else {
|
|
@@ -2581,8 +2781,11 @@ export function selectAdminConsole(state) {
|
|
|
2581
2781
|
loading: Boolean(admin.loading),
|
|
2582
2782
|
loadError: admin.loadError || null,
|
|
2583
2783
|
principal,
|
|
2784
|
+
isAdmin,
|
|
2584
2785
|
ghcpKey: {
|
|
2585
2786
|
configured: Boolean(profile?.githubCopilotKeySet),
|
|
2787
|
+
targetConfigured,
|
|
2788
|
+
storeAsSystem,
|
|
2586
2789
|
editing: Boolean(ghcpKey.editing),
|
|
2587
2790
|
draft: ghcpKey.draft || "",
|
|
2588
2791
|
cursorIndex: Math.max(0, Math.min(Number(ghcpKey.cursorIndex) || 0, String(ghcpKey.draft || "").length)),
|
|
@@ -2590,6 +2793,14 @@ export function selectAdminConsole(state) {
|
|
|
2590
2793
|
error: ghcpKey.error || null,
|
|
2591
2794
|
statusText: ghcpStatusText,
|
|
2592
2795
|
},
|
|
2796
|
+
systemGhcpKey: {
|
|
2797
|
+
supported: Boolean(systemGhcpKey.supported),
|
|
2798
|
+
loading: Boolean(systemGhcpKey.loading),
|
|
2799
|
+
configured: Boolean(systemGhcpKey.configured),
|
|
2800
|
+
changedBy: systemGhcpKey.changedBy || null,
|
|
2801
|
+
changedAt: systemGhcpKey.changedAt || null,
|
|
2802
|
+
error: systemGhcpKey.error || null,
|
|
2803
|
+
},
|
|
2593
2804
|
actions,
|
|
2594
2805
|
};
|
|
2595
2806
|
}
|
|
@@ -2759,22 +2970,24 @@ export function selectStatusBar(state) {
|
|
|
2759
2970
|
const chatViewMode = state.ui?.chatViewMode === "summary" ? "summary" : "transcript";
|
|
2760
2971
|
const chatViewHint = chatViewMode === "summary" ? "s transcript" : "s summary";
|
|
2761
2972
|
const hints = {
|
|
2762
|
-
[FOCUS_REGIONS.SESSIONS]: `up/down switch · ctrl-u/ctrl-d page · ctrl-g move group · f filter · P pin · V select · d done · D delete · r refresh · t title · ${fullscreenHint} ·
|
|
2763
|
-
[FOCUS_REGIONS.CHAT]: `${chatViewHint} · j/k scroll · ctrl-u/ctrl-d page · e older history · g/G top/bottom · d done · ${fullscreenHint} ·
|
|
2973
|
+
[FOCUS_REGIONS.SESSIONS]: `up/down switch · ctrl-u/ctrl-d page · ctrl-g move group · f filter · P pin · V select · d done · D delete · r refresh · t title · ${fullscreenHint} · [/] resize pane · {/} columns · T themes · ? help · a linked items · drag copy · tab next pane · p prompt`,
|
|
2974
|
+
[FOCUS_REGIONS.CHAT]: `${chatViewHint} · j/k scroll · ctrl-u/ctrl-d page · e older history · g/G top/bottom · d done · ${fullscreenHint} · [/] resize pane · {/} columns · T themes · ? help · a linked items · drag copy · tab next pane · p prompt`,
|
|
2764
2975
|
[FOCUS_REGIONS.INSPECTOR]: state.ui.inspectorTab === "logs"
|
|
2765
|
-
? `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · d done · t tail · f filter · ${fullscreenHint} · left/right tab · [/]
|
|
2976
|
+
? `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · d done · t tail · f filter · ${fullscreenHint} · left/right tab · [/] resize pane · {/} columns · T themes · ? help · a linked items · drag copy · tab next pane`
|
|
2766
2977
|
: state.ui.inspectorTab === "stats"
|
|
2767
|
-
? `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · f cycle session/fleet/users · d done · ${fullscreenHint} · left/right tab · [/]
|
|
2978
|
+
? `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · f cycle session/fleet/users · d done · ${fullscreenHint} · left/right tab · [/] resize pane · {/} columns · T themes · ? help · m next tab · tab next pane`
|
|
2768
2979
|
: state.ui.inspectorTab === "files"
|
|
2769
2980
|
? state.files?.fullscreen
|
|
2770
|
-
? "a download · x delete · u/ctrl-a upload · o open · f filter · j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · d done · v/esc close fullscreen · left/right tab ·
|
|
2771
|
-
: "j/k files · a download · x delete · u/ctrl-a upload · o open · f filter · ctrl-u/ctrl-d page preview · g/G preview top/bottom · d done · v fullscreen · left/right tab ·
|
|
2981
|
+
? "a download · x delete · u/ctrl-a upload · o open · f filter · j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · d done · v/esc close fullscreen · left/right tab · [/] resize pane · {/} columns · T themes · ? help · tab next pane"
|
|
2982
|
+
: "j/k files · a download · x delete · u/ctrl-a upload · o open · f filter · ctrl-u/ctrl-d page preview · g/G preview top/bottom · d done · v fullscreen · left/right tab · [/] resize pane · {/} columns · T themes · ? help · tab next pane"
|
|
2772
2983
|
: state.ui.inspectorTab === "history"
|
|
2773
|
-
? `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · f format · r refresh · a save artifact · d done · ${fullscreenHint} · left/right tab · [/]
|
|
2774
|
-
:
|
|
2775
|
-
|
|
2984
|
+
? `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · f format · r refresh · a save artifact · d done · ${fullscreenHint} · left/right tab · [/] resize pane · {/} columns · T themes · ? help · m next tab · tab next pane`
|
|
2985
|
+
: state.ui.inspectorTab === "sequence"
|
|
2986
|
+
? `j/k turn · enter expand · ctrl-u/ctrl-d page · g/G top/bottom · d done · ${fullscreenHint} · left/right tab · [/] resize pane · {/} columns · T themes · ? help · m next tab · tab next pane`
|
|
2987
|
+
: `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · d done · ${fullscreenHint} · left/right tab · [/] resize pane · {/} columns · T themes · ? help · h/l focus · a linked items · drag copy · m next tab · tab next pane`,
|
|
2988
|
+
[FOCUS_REGIONS.ACTIVITY]: `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · d done · ${fullscreenHint} · [/] resize pane · {/} columns · T themes · ? help · a linked items · drag copy · h left · tab next pane`,
|
|
2776
2989
|
[FOCUS_REGIONS.PROMPT]: hasPendingQuestion
|
|
2777
|
-
? `type answer · enter reply · alt-enter newline · T themes · arrows move · alt-left/right word · alt-delete word · @ artifacts · @@ sessions · ${paneFullscreen ? "esc pane" : "esc sessions"}`
|
|
2990
|
+
? `type answer · enter reply · alt-enter newline · T themes · ? help · arrows move · alt-left/right word · alt-delete word · @ artifacts · @@ sessions · ${paneFullscreen ? "esc pane" : "esc sessions"}`
|
|
2778
2991
|
: editingPendingOutbox
|
|
2779
2992
|
? selectedQueuedOutbox
|
|
2780
2993
|
? `queued prompt selected · d delete · up/down cycle queued · enter/esc new prompt · ${paneFullscreen ? "esc pane" : "esc sessions"}`
|
|
@@ -2785,12 +2998,18 @@ export function selectStatusBar(state) {
|
|
|
2785
2998
|
? `type message · enter queues · enter on empty sends batch · up/down recall pending · alt-enter newline · @ artifacts · @@ sessions · ${paneFullscreen ? "esc pane" : "esc sessions"}`
|
|
2786
2999
|
: hasOutbox
|
|
2787
3000
|
? `type message · enter queues behind durable items · up/down recall pending · alt-enter newline · @ artifacts · @@ sessions · ${paneFullscreen ? "esc pane" : "esc sessions"}`
|
|
2788
|
-
: `type message · enter send · alt-enter newline · T themes · arrows move · alt-left/right word · alt-delete word · @ artifacts · @@ sessions · ${paneFullscreen ? "esc pane" : "esc sessions"}`,
|
|
3001
|
+
: `type message · enter send · alt-enter newline · T themes · ? help · arrows move · alt-left/right word · alt-delete word · @ artifacts · @@ sessions · ${paneFullscreen ? "esc pane" : "esc sessions"}`,
|
|
2789
3002
|
};
|
|
2790
3003
|
|
|
3004
|
+
let right = hints[focus] || hints[FOCUS_REGIONS.SESSIONS];
|
|
3005
|
+
// Surface the Stop-turn hint at the front (so truncation never eats it)
|
|
3006
|
+
// exactly while a turn is running; it stays listed, grayed, in `?` help.
|
|
3007
|
+
if (canStopSessionTurn(selectActiveSession(state))) {
|
|
3008
|
+
right = `ctrl-x stop · ${right}`;
|
|
3009
|
+
}
|
|
2791
3010
|
return {
|
|
2792
3011
|
left: state.ui.statusText,
|
|
2793
|
-
right
|
|
3012
|
+
right,
|
|
2794
3013
|
};
|
|
2795
3014
|
}
|
|
2796
3015
|
|
|
@@ -3355,6 +3574,77 @@ function buildNodeMapHeaderLine(nodeLabels, colWidth) {
|
|
|
3355
3574
|
return runs;
|
|
3356
3575
|
}
|
|
3357
3576
|
|
|
3577
|
+
const SEQ_REASONING_EFFORTS = new Set(["low", "medium", "high", "xhigh", "minimal", "none"]);
|
|
3578
|
+
|
|
3579
|
+
function shortModelForSequence(value) {
|
|
3580
|
+
const model = String(value || "").trim();
|
|
3581
|
+
if (!model) return "unknown";
|
|
3582
|
+
const parts = model.split(":").filter(Boolean);
|
|
3583
|
+
if (parts.length === 0) return "unknown";
|
|
3584
|
+
const last = String(parts[parts.length - 1]).toLowerCase();
|
|
3585
|
+
const index = parts.length > 1 && SEQ_REASONING_EFFORTS.has(last) ? parts.length - 2 : parts.length - 1;
|
|
3586
|
+
return parts[Math.max(0, index)] || parts[0] || "unknown";
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
function formatTokenCountK(value) {
|
|
3590
|
+
const tokens = Number(value || 0);
|
|
3591
|
+
if (!Number.isFinite(tokens)) return "?K";
|
|
3592
|
+
const scaled = tokens / 1000;
|
|
3593
|
+
return `${scaled.toFixed(Math.abs(scaled) >= 10 ? 1 : 2)}K`;
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
function formatSeqCount(value) {
|
|
3597
|
+
return Number(value || 0).toLocaleString("en-US");
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
// Turn index -> turn_completed event data, mirroring the portal's per-turn
|
|
3601
|
+
// completion model so the TUI sequence tab can show the same detail.
|
|
3602
|
+
export function buildSequenceCompletionByTurn(events) {
|
|
3603
|
+
const map = new Map();
|
|
3604
|
+
for (const event of events || []) {
|
|
3605
|
+
if (event?.eventType !== "session.turn_completed") continue;
|
|
3606
|
+
const turn = Number(event?.data?.turnIndex ?? event?.data?.iteration);
|
|
3607
|
+
if (!Number.isFinite(turn)) continue;
|
|
3608
|
+
map.set(turn, event.data || {});
|
|
3609
|
+
}
|
|
3610
|
+
return map;
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
// The magenta "Mod: ..." divider (+ optional detail lines) inserted under a
|
|
3614
|
+
// completed-turn row when the TUI sequence tab is showing turn expansion.
|
|
3615
|
+
function buildSequenceTurnLines(data, { expanded, selected, maxWidth }) {
|
|
3616
|
+
const caret = expanded ? "v" : ">";
|
|
3617
|
+
const model = shortModelForSequence(data?.model);
|
|
3618
|
+
const dur = data?.durationMs != null ? `${(Number(data.durationMs) / 1000).toFixed(1)}s` : "n/a";
|
|
3619
|
+
const dividerText = `${caret} Mod: ${model} tok ${formatTokenCountK(data?.tokensInput)}/${formatTokenCountK(data?.tokensOutput)} dur ${dur}`;
|
|
3620
|
+
const lines = [];
|
|
3621
|
+
if (selected) {
|
|
3622
|
+
lines.push(buildActiveHighlightLine(padDisplayText(dividerText, Math.max(18, maxWidth))));
|
|
3623
|
+
} else {
|
|
3624
|
+
lines.push([{ text: dividerText, color: "magenta" }]);
|
|
3625
|
+
}
|
|
3626
|
+
if (expanded) {
|
|
3627
|
+
const details = [
|
|
3628
|
+
["model", data?.reasoningEffort ? `${data?.model || "(unknown)"}:${data.reasoningEffort}` : (data?.model || "(unknown)")],
|
|
3629
|
+
["duration", data?.durationMs != null ? `${formatSeqCount(data.durationMs)} ms` : null],
|
|
3630
|
+
["tokens", `${formatSeqCount(data?.tokensInput)} in / ${formatSeqCount(data?.tokensOutput)} out`],
|
|
3631
|
+
["cache", `${formatSeqCount(data?.tokensCacheRead)} read / ${formatSeqCount(data?.tokensCacheWrite)} write`],
|
|
3632
|
+
["tools", `${formatSeqCount(data?.toolCalls)} calls / ${formatSeqCount(data?.toolErrors)} errors`],
|
|
3633
|
+
["names", Array.isArray(data?.toolNames) && data.toolNames.length ? data.toolNames.join(", ") : null],
|
|
3634
|
+
["worker", data?.workerNodeId || null],
|
|
3635
|
+
["result", data?.resultType || null],
|
|
3636
|
+
["error", data?.errorMessage || null],
|
|
3637
|
+
].filter(([, value]) => value != null && value !== "");
|
|
3638
|
+
for (const [label, value] of details) {
|
|
3639
|
+
lines.push([
|
|
3640
|
+
{ text: ` ${label}: `, color: "gray" },
|
|
3641
|
+
{ text: String(value), color: "white" },
|
|
3642
|
+
]);
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
return lines;
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3358
3648
|
function buildSequenceViewForSession(state, session, maxWidth, options = {}) {
|
|
3359
3649
|
const allowWideColumns = Boolean(options?.allowWideColumns);
|
|
3360
3650
|
const statsLines = buildSequenceStatsLines(state, session, maxWidth);
|
|
@@ -3396,6 +3686,29 @@ function buildSequenceViewForSession(state, session, maxWidth, options = {}) {
|
|
|
3396
3686
|
Math.floor((availableWidth - timeWidth - 1 - gapWidth) / Math.max(1, nodeLabels.length)),
|
|
3397
3687
|
);
|
|
3398
3688
|
|
|
3689
|
+
const expansion = options?.sequenceExpansion || null;
|
|
3690
|
+
const completionByTurn = expansion ? buildSequenceCompletionByTurn(history?.events || []) : null;
|
|
3691
|
+
const expandedTurns = expansion ? new Set((expansion.expandedTurns || []).map(Number)) : null;
|
|
3692
|
+
const selectedTurn = expansion && expansion.selectedTurn != null ? Number(expansion.selectedTurn) : null;
|
|
3693
|
+
|
|
3694
|
+
const lines = [];
|
|
3695
|
+
for (const entry of visibleEntries) {
|
|
3696
|
+
lines.push(buildSequenceEventLine(entry, nodeLabels, timeWidth, colWidth));
|
|
3697
|
+
if (expansion && entry.completedTurn != null) {
|
|
3698
|
+
const turn = Number(entry.completedTurn);
|
|
3699
|
+
const data = completionByTurn.get(turn);
|
|
3700
|
+
if (data) {
|
|
3701
|
+
for (const detailLine of buildSequenceTurnLines(data, {
|
|
3702
|
+
expanded: expandedTurns.has(turn),
|
|
3703
|
+
selected: selectedTurn === turn,
|
|
3704
|
+
maxWidth: availableWidth,
|
|
3705
|
+
})) {
|
|
3706
|
+
lines.push(detailLine);
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3399
3712
|
return {
|
|
3400
3713
|
stickyLines: [
|
|
3401
3714
|
...statsLines,
|
|
@@ -3403,7 +3716,7 @@ function buildSequenceViewForSession(state, session, maxWidth, options = {}) {
|
|
|
3403
3716
|
buildSequenceHeaderLine(nodeLabels, timeWidth, colWidth),
|
|
3404
3717
|
buildSequenceDividerLine(nodeLabels, timeWidth, colWidth),
|
|
3405
3718
|
],
|
|
3406
|
-
lines
|
|
3719
|
+
lines,
|
|
3407
3720
|
};
|
|
3408
3721
|
}
|
|
3409
3722
|
|
|
@@ -5184,7 +5497,7 @@ export function selectInspector(state, options = {}) {
|
|
|
5184
5497
|
switch (activeTab) {
|
|
5185
5498
|
case "sequence": {
|
|
5186
5499
|
const sequenceView = session
|
|
5187
|
-
? buildSequenceViewForSession(state, session, maxWidth, { allowWideColumns })
|
|
5500
|
+
? buildSequenceViewForSession(state, session, maxWidth, { allowWideColumns, sequenceExpansion: options?.sequenceExpansion || null })
|
|
5188
5501
|
: { stickyLines: [], lines: ["No session selected."] };
|
|
5189
5502
|
stickyLines = sequenceView.stickyLines || [];
|
|
5190
5503
|
lines = sequenceView.lines;
|
|
@@ -5369,6 +5682,100 @@ function buildThemeSwatchRuns(entries = []) {
|
|
|
5369
5682
|
return runs;
|
|
5370
5683
|
}
|
|
5371
5684
|
|
|
5685
|
+
const KEYBINDING_HELP = [
|
|
5686
|
+
{ section: "Global", bindings: [
|
|
5687
|
+
["Tab / Shift-Tab", "focus next / previous pane"],
|
|
5688
|
+
["h l ← →", "focus left / right pane"],
|
|
5689
|
+
["p", "focus the prompt"],
|
|
5690
|
+
["[ ]", "shrink / grow the focused pane"],
|
|
5691
|
+
["{ }", "grow left / right column"],
|
|
5692
|
+
["v", "fullscreen the focused pane"],
|
|
5693
|
+
["n / r", "new session / refresh"],
|
|
5694
|
+
["a", "linked items — artifacts to download, links to open"],
|
|
5695
|
+
["m", "cycle inspector tab"],
|
|
5696
|
+
["c / d / D", "cancel / done / delete session"],
|
|
5697
|
+
["ctrl-x (ctrl-esc)", "stop the current turn", { dim: true }],
|
|
5698
|
+
["T / N / A", "theme / model / admin"],
|
|
5699
|
+
["?", "toggle this help"],
|
|
5700
|
+
["q", "quit (double-tap)"],
|
|
5701
|
+
["Esc", "back / focus sessions"],
|
|
5702
|
+
] },
|
|
5703
|
+
{ section: "Sessions pane", bindings: [
|
|
5704
|
+
["j k ↑ ↓", "move selection"],
|
|
5705
|
+
["ctrl-u / ctrl-d", "page up / down"],
|
|
5706
|
+
["ctrl-g", "move to group"],
|
|
5707
|
+
["+ / -", "expand / collapse subtree"],
|
|
5708
|
+
["t", "rename"],
|
|
5709
|
+
["P", "pin / unpin"],
|
|
5710
|
+
["V / space", "select mode / toggle selection"],
|
|
5711
|
+
["f", "filter"],
|
|
5712
|
+
] },
|
|
5713
|
+
{ section: "Chat / transcript", bindings: [
|
|
5714
|
+
["s", "toggle transcript / summary"],
|
|
5715
|
+
["j k ↑ ↓", "scroll"],
|
|
5716
|
+
["ctrl-u / ctrl-d", "page"],
|
|
5717
|
+
["e", "expand older history"],
|
|
5718
|
+
["g / G", "top / bottom"],
|
|
5719
|
+
] },
|
|
5720
|
+
{ section: "Inspector pane", bindings: [
|
|
5721
|
+
["← → m", "previous / next / cycle tab"],
|
|
5722
|
+
["j k", "scroll"],
|
|
5723
|
+
["enter", "expand / collapse a turn (Sequence tab)"],
|
|
5724
|
+
["logs", "t tail · f filter"],
|
|
5725
|
+
["stats", "f cycle session/fleet/users"],
|
|
5726
|
+
["files", "a download · x delete · u upload · o open · f filter · v full"],
|
|
5727
|
+
["history", "r refresh · a export · f format"],
|
|
5728
|
+
] },
|
|
5729
|
+
{ section: "Activity pane", bindings: [
|
|
5730
|
+
["j k ↑ ↓", "scroll"],
|
|
5731
|
+
["g / G", "top / bottom"],
|
|
5732
|
+
] },
|
|
5733
|
+
{ section: "Prompt", bindings: [
|
|
5734
|
+
["enter", "send"],
|
|
5735
|
+
["alt/ctrl-j", "newline"],
|
|
5736
|
+
["ctrl-a", "attach artifact"],
|
|
5737
|
+
["@ / @@", "artifact / session reference"],
|
|
5738
|
+
] },
|
|
5739
|
+
{ section: "Overlays", bindings: [
|
|
5740
|
+
["Esc / q", "close"],
|
|
5741
|
+
["enter", "confirm / apply"],
|
|
5742
|
+
["j k ↑ ↓", "navigate"],
|
|
5743
|
+
["Tab", "switch modal pane"],
|
|
5744
|
+
] },
|
|
5745
|
+
];
|
|
5746
|
+
|
|
5747
|
+
export function buildHelpModalRows() {
|
|
5748
|
+
const rows = [];
|
|
5749
|
+
const keysWidth = 18;
|
|
5750
|
+
for (const group of KEYBINDING_HELP) {
|
|
5751
|
+
if (rows.length > 0) rows.push([{ text: "", color: "gray" }]);
|
|
5752
|
+
rows.push([{ text: group.section, color: "cyan", bold: true }]);
|
|
5753
|
+
for (const [keys, desc, opts] of group.bindings) {
|
|
5754
|
+
const dim = Boolean(opts?.dim);
|
|
5755
|
+
rows.push([
|
|
5756
|
+
{ text: " " + String(keys).padEnd(keysWidth), color: dim ? "gray" : "yellow" },
|
|
5757
|
+
{ text: String(desc), color: dim ? "gray" : "white" },
|
|
5758
|
+
]);
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5761
|
+
return rows;
|
|
5762
|
+
}
|
|
5763
|
+
|
|
5764
|
+
// TUI-only help overlay (the portal relies on tooltips). Reuses the single
|
|
5765
|
+
// modal slot; opened with `?` and dismissed with `?`/Esc.
|
|
5766
|
+
export function selectHelpModal(state, maxWidth = 88) {
|
|
5767
|
+
const modal = state.ui?.modal;
|
|
5768
|
+
if (!modal || modal.type !== "help") return null;
|
|
5769
|
+
const rows = buildHelpModalRows();
|
|
5770
|
+
const selectedRowIndex = Math.max(0, Math.min(Number(modal.selectedIndex) || 0, rows.length - 1));
|
|
5771
|
+
return {
|
|
5772
|
+
title: "Keybindings — ? or Esc to close",
|
|
5773
|
+
idealWidth: Math.max(64, Math.min(maxWidth, 88)),
|
|
5774
|
+
rows,
|
|
5775
|
+
selectedRowIndex,
|
|
5776
|
+
};
|
|
5777
|
+
}
|
|
5778
|
+
|
|
5372
5779
|
export function selectThemePickerModal(state, maxWidth = 80) {
|
|
5373
5780
|
const modal = state.ui.modal;
|
|
5374
5781
|
if (!modal || modal.type !== "themePicker") return null;
|
package/ui/core/src/state.js
CHANGED
|
@@ -126,6 +126,8 @@ export function createInitialState({ mode = "local", branding = null, themeId =
|
|
|
126
126
|
ui: {
|
|
127
127
|
focusRegion: FOCUS_REGIONS.SESSIONS,
|
|
128
128
|
inspectorTab: INSPECTOR_TABS[0],
|
|
129
|
+
sequenceExpandedTurns: [],
|
|
130
|
+
sequenceSelectedTurn: null,
|
|
129
131
|
chatViewMode: chatViewMode === "summary" ? "summary" : "transcript",
|
|
130
132
|
statsViewMode: "session",
|
|
131
133
|
prompt: "",
|
|
@@ -245,6 +247,21 @@ export function createInitialState({ mode = "local", branding = null, themeId =
|
|
|
245
247
|
saving: false,
|
|
246
248
|
error: null,
|
|
247
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,
|
|
248
265
|
},
|
|
249
266
|
},
|
|
250
267
|
};
|
|
@@ -2,6 +2,28 @@ import React from "react";
|
|
|
2
2
|
|
|
3
3
|
const DOT_FRAMES = [".\u00a0\u00a0", "..\u00a0", "..."];
|
|
4
4
|
|
|
5
|
+
const SPINNER_FRAMES = ["\u280b", "\u2819", "\u2839", "\u2838", "\u283c", "\u2834", "\u2826", "\u2827", "\u2807", "\u280f"];
|
|
6
|
+
|
|
7
|
+
// Braille spinner frame that advances while `active`, for the live activity
|
|
8
|
+
// line's leading marker. Returns "" when inactive so callers can fall back to a
|
|
9
|
+
// static glyph.
|
|
10
|
+
export function useSpinnerFrame(active, intervalMs = 120) {
|
|
11
|
+
const [frameIndex, setFrameIndex] = React.useState(0);
|
|
12
|
+
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
if (!active) {
|
|
15
|
+
setFrameIndex(0);
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const timer = setInterval(() => {
|
|
19
|
+
setFrameIndex((current) => (current + 1) % SPINNER_FRAMES.length);
|
|
20
|
+
}, intervalMs);
|
|
21
|
+
return () => clearInterval(timer);
|
|
22
|
+
}, [active, intervalMs]);
|
|
23
|
+
|
|
24
|
+
return active ? SPINNER_FRAMES[frameIndex] : "";
|
|
25
|
+
}
|
|
26
|
+
|
|
5
27
|
export function useAnimatedDots(active, intervalMs = 360) {
|
|
6
28
|
const [frameIndex, setFrameIndex] = React.useState(0);
|
|
7
29
|
|