pilotswarm 0.5.1 → 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/node-sdk-transport.js +17 -0
- package/ui/core/src/controller.js +56 -8
- package/ui/core/src/reducer.js +87 -0
- package/ui/core/src/selectors.js +154 -123
- package/ui/core/src/state.js +15 -0
- package/ui/react/src/web-app.js +60 -22
- 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-CRYBfNR4.js → react-CqzaLZob.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/reducer.js
CHANGED
|
@@ -1488,6 +1488,7 @@ export function appReducer(state, action) {
|
|
|
1488
1488
|
saving: false,
|
|
1489
1489
|
error: null,
|
|
1490
1490
|
lastSavedAt: state.admin.ghcpKey.lastSavedAt,
|
|
1491
|
+
storeAsSystem: Boolean(state.admin.ghcpKey.storeAsSystem),
|
|
1491
1492
|
},
|
|
1492
1493
|
},
|
|
1493
1494
|
};
|
|
@@ -1524,6 +1525,7 @@ export function appReducer(state, action) {
|
|
|
1524
1525
|
saving: false,
|
|
1525
1526
|
error: null,
|
|
1526
1527
|
lastSavedAt: state.admin.ghcpKey.lastSavedAt,
|
|
1528
|
+
storeAsSystem: Boolean(state.admin.ghcpKey.storeAsSystem),
|
|
1527
1529
|
},
|
|
1528
1530
|
},
|
|
1529
1531
|
};
|
|
@@ -1567,6 +1569,91 @@ export function appReducer(state, action) {
|
|
|
1567
1569
|
saving: false,
|
|
1568
1570
|
error: null,
|
|
1569
1571
|
lastSavedAt: Date.now(),
|
|
1572
|
+
storeAsSystem: Boolean(state.admin.ghcpKey.storeAsSystem),
|
|
1573
|
+
},
|
|
1574
|
+
},
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
case "admin/ghcpKey/setSystemTarget": {
|
|
1578
|
+
const storeAsSystem = Boolean(action.value);
|
|
1579
|
+
if (Boolean(state.admin.ghcpKey.storeAsSystem) === storeAsSystem) return state;
|
|
1580
|
+
return {
|
|
1581
|
+
...state,
|
|
1582
|
+
admin: {
|
|
1583
|
+
...state.admin,
|
|
1584
|
+
ghcpKey: {
|
|
1585
|
+
...state.admin.ghcpKey,
|
|
1586
|
+
storeAsSystem,
|
|
1587
|
+
error: null,
|
|
1588
|
+
},
|
|
1589
|
+
},
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
case "admin/systemGhcpKey/loading": {
|
|
1593
|
+
return {
|
|
1594
|
+
...state,
|
|
1595
|
+
admin: {
|
|
1596
|
+
...state.admin,
|
|
1597
|
+
systemGhcpKey: {
|
|
1598
|
+
...state.admin.systemGhcpKey,
|
|
1599
|
+
loading: true,
|
|
1600
|
+
error: null,
|
|
1601
|
+
},
|
|
1602
|
+
},
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
case "admin/systemGhcpKey/loaded": {
|
|
1606
|
+
const status = action.status || {};
|
|
1607
|
+
return {
|
|
1608
|
+
...state,
|
|
1609
|
+
admin: {
|
|
1610
|
+
...state.admin,
|
|
1611
|
+
systemGhcpKey: {
|
|
1612
|
+
supported: true,
|
|
1613
|
+
loading: false,
|
|
1614
|
+
configured: Boolean(status.configured),
|
|
1615
|
+
changedBy: status.changedBy || null,
|
|
1616
|
+
changedAt: status.changedAt || null,
|
|
1617
|
+
error: null,
|
|
1618
|
+
},
|
|
1619
|
+
},
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
case "admin/systemGhcpKey/loadFailed": {
|
|
1623
|
+
return {
|
|
1624
|
+
...state,
|
|
1625
|
+
admin: {
|
|
1626
|
+
...state.admin,
|
|
1627
|
+
systemGhcpKey: {
|
|
1628
|
+
...state.admin.systemGhcpKey,
|
|
1629
|
+
loading: false,
|
|
1630
|
+
error: action.error ? String(action.error) : "Failed to load System key status",
|
|
1631
|
+
},
|
|
1632
|
+
},
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
case "admin/systemGhcpKey/saved": {
|
|
1636
|
+
const status = action.status || {};
|
|
1637
|
+
return {
|
|
1638
|
+
...state,
|
|
1639
|
+
admin: {
|
|
1640
|
+
...state.admin,
|
|
1641
|
+
ghcpKey: {
|
|
1642
|
+
editing: false,
|
|
1643
|
+
draft: "",
|
|
1644
|
+
cursorIndex: 0,
|
|
1645
|
+
saving: false,
|
|
1646
|
+
error: null,
|
|
1647
|
+
lastSavedAt: Date.now(),
|
|
1648
|
+
storeAsSystem: Boolean(state.admin.ghcpKey.storeAsSystem),
|
|
1649
|
+
},
|
|
1650
|
+
systemGhcpKey: {
|
|
1651
|
+
supported: true,
|
|
1652
|
+
loading: false,
|
|
1653
|
+
configured: Boolean(status.configured),
|
|
1654
|
+
changedBy: status.changedBy || null,
|
|
1655
|
+
changedAt: status.changedAt || null,
|
|
1656
|
+
error: null,
|
|
1570
1657
|
},
|
|
1571
1658
|
},
|
|
1572
1659
|
};
|
package/ui/core/src/selectors.js
CHANGED
|
@@ -1887,60 +1887,6 @@ const LIVE_ACTIVITY_SKIP = new Set([
|
|
|
1887
1887
|
"tool.execution_progress",
|
|
1888
1888
|
]);
|
|
1889
1889
|
|
|
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
1890
|
// Format an elapsed duration for the live activity card header (e.g. "12s",
|
|
1945
1891
|
// "1m 05s", "1h 02m").
|
|
1946
1892
|
function formatElapsed(ms) {
|
|
@@ -1954,12 +1900,87 @@ function formatElapsed(ms) {
|
|
|
1954
1900
|
return `${hours}h ${String(mins).padStart(2, "0")}m`;
|
|
1955
1901
|
}
|
|
1956
1902
|
|
|
1957
|
-
//
|
|
1958
|
-
//
|
|
1959
|
-
//
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
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.
|
|
1963
1984
|
export function selectLiveActivityLines(state, options = {}) {
|
|
1964
1985
|
if (state?.ui?.chatViewMode === "summary") return [];
|
|
1965
1986
|
const session = selectActiveSession(state);
|
|
@@ -1967,76 +1988,60 @@ export function selectLiveActivityLines(state, options = {}) {
|
|
|
1967
1988
|
const status = String(session?.status || "").toLowerCase();
|
|
1968
1989
|
if (status !== "running") return [];
|
|
1969
1990
|
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.
|
|
1991
|
+
// Latest user message anchors the elapsed clock when available.
|
|
1973
1992
|
const chat = Array.isArray(history?.chat) ? history.chat : [];
|
|
1974
1993
|
let turnStartAt = 0;
|
|
1975
1994
|
for (let i = chat.length - 1; i >= 0; i -= 1) {
|
|
1976
|
-
|
|
1977
|
-
if (role === "user") {
|
|
1995
|
+
if (chat[i]?.role === "user") {
|
|
1978
1996
|
turnStartAt = Number(chat[i]?.createdAt || 0);
|
|
1979
1997
|
break;
|
|
1980
1998
|
}
|
|
1981
1999
|
}
|
|
1982
2000
|
const activity = Array.isArray(history?.activity) ? history.activity : [];
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
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) {
|
|
1988
2025
|
const item = activity[i];
|
|
1989
2026
|
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";
|
|
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
|
+
}
|
|
2005
2033
|
const nowMs = Number.isFinite(options.now) ? options.now : Date.now();
|
|
2006
|
-
const startAt = turnStartAt || oldestGatheredAt;
|
|
2007
2034
|
const elapsedLabel = startAt > 0 ? formatElapsed(nowMs - startAt) : "";
|
|
2008
|
-
const
|
|
2009
|
-
|
|
2010
|
-
|
|
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" }] : []),
|
|
2011
2042
|
];
|
|
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;
|
|
2043
|
+
const maxWidth = Math.max(24, Math.min(120, Math.floor(Number(options.maxWidth) || 80)));
|
|
2044
|
+
return [fitRuns(runs, maxWidth)];
|
|
2040
2045
|
}
|
|
2041
2046
|
|
|
2042
2047
|
export function selectChatLines(state, maxWidth = 80, options = {}) {
|
|
@@ -2735,19 +2740,34 @@ export function selectAdminConsole(state) {
|
|
|
2735
2740
|
}
|
|
2736
2741
|
: (state.auth?.principal || null);
|
|
2737
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
|
+
: "";
|
|
2738
2754
|
const ghcpStatusText = ghcpKey.saving
|
|
2739
2755
|
? "Saving..."
|
|
2740
2756
|
: ghcpKey.editing
|
|
2741
2757
|
? "Editing — Enter to save, Esc to cancel"
|
|
2742
|
-
:
|
|
2743
|
-
?
|
|
2744
|
-
|
|
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)");
|
|
2745
2765
|
|
|
2746
2766
|
const actions = [];
|
|
2747
2767
|
if (!ghcpKey.editing) {
|
|
2748
|
-
actions.push({ id: "edit", label:
|
|
2749
|
-
if (
|
|
2750
|
-
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" });
|
|
2751
2771
|
}
|
|
2752
2772
|
actions.push({ id: "refresh", label: "Refresh", key: "r" });
|
|
2753
2773
|
} else {
|
|
@@ -2761,8 +2781,11 @@ export function selectAdminConsole(state) {
|
|
|
2761
2781
|
loading: Boolean(admin.loading),
|
|
2762
2782
|
loadError: admin.loadError || null,
|
|
2763
2783
|
principal,
|
|
2784
|
+
isAdmin,
|
|
2764
2785
|
ghcpKey: {
|
|
2765
2786
|
configured: Boolean(profile?.githubCopilotKeySet),
|
|
2787
|
+
targetConfigured,
|
|
2788
|
+
storeAsSystem,
|
|
2766
2789
|
editing: Boolean(ghcpKey.editing),
|
|
2767
2790
|
draft: ghcpKey.draft || "",
|
|
2768
2791
|
cursorIndex: Math.max(0, Math.min(Number(ghcpKey.cursorIndex) || 0, String(ghcpKey.draft || "").length)),
|
|
@@ -2770,6 +2793,14 @@ export function selectAdminConsole(state) {
|
|
|
2770
2793
|
error: ghcpKey.error || null,
|
|
2771
2794
|
statusText: ghcpStatusText,
|
|
2772
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
|
+
},
|
|
2773
2804
|
actions,
|
|
2774
2805
|
};
|
|
2775
2806
|
}
|
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
|
};
|
package/ui/react/src/web-app.js
CHANGED
|
@@ -431,13 +431,19 @@ function compactTitleRuns(title, maxWidth = 40) {
|
|
|
431
431
|
return [{ text: text.length > maxWidth ? `${text.slice(0, maxWidth - 1)}…` : text, color: "white", bold: true }];
|
|
432
432
|
}
|
|
433
433
|
const compactRuns = [];
|
|
434
|
+
// Gray runs are dropped on tight widths as decoration (ids, badges) —
|
|
435
|
+
// EXCEPT pure separators (" · "). Dropping those fuses adjacent values:
|
|
436
|
+
// "running · gpt-5.4 · ctx 33%" became "runninggpt-5.4ctx…".
|
|
437
|
+
const isSeparator = (run) => /^[\s·—-]+$/.test(String(run?.text || ""));
|
|
434
438
|
let remaining = Math.max(8, maxWidth);
|
|
435
439
|
for (const run of title) {
|
|
436
440
|
if (remaining <= 0) break;
|
|
437
441
|
const color = run?.color;
|
|
438
|
-
if (color === "gray" && compactRuns.length > 0) continue;
|
|
442
|
+
if (color === "gray" && compactRuns.length > 0 && !isSeparator(run)) continue;
|
|
439
443
|
const text = String(run?.text || "");
|
|
440
444
|
if (!text) continue;
|
|
445
|
+
// Don't start a separator we can't follow with content.
|
|
446
|
+
if (isSeparator(run) && remaining < text.length + 2) break;
|
|
441
447
|
const chunk = text.length > remaining && remaining > 1
|
|
442
448
|
? `${text.slice(0, remaining - 1)}…`
|
|
443
449
|
: text.slice(0, remaining);
|
|
@@ -445,6 +451,10 @@ function compactTitleRuns(title, maxWidth = 40) {
|
|
|
445
451
|
compactRuns.push({ ...run, text: chunk });
|
|
446
452
|
remaining -= chunk.length;
|
|
447
453
|
}
|
|
454
|
+
// Never end on a dangling separator.
|
|
455
|
+
while (compactRuns.length > 0 && isSeparator(compactRuns[compactRuns.length - 1])) {
|
|
456
|
+
compactRuns.pop();
|
|
457
|
+
}
|
|
448
458
|
return compactRuns.length > 0 ? compactRuns : title;
|
|
449
459
|
}
|
|
450
460
|
|
|
@@ -1715,6 +1725,7 @@ function PortalSequenceLines({ lines, theme, completionByTurn }) {
|
|
|
1715
1725
|
);
|
|
1716
1726
|
}
|
|
1717
1727
|
|
|
1728
|
+
|
|
1718
1729
|
function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, lines, stickyLines = [], bottomStickyLines = [], scrollOffset = 0, scrollMode = "top", paneKey, controller, className = "", panelClassName = "", topContent = null, bottomContent = null, structuredBlocks = false, stickyBottom = false, renderBody = null }) {
|
|
1719
1730
|
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
1720
1731
|
const theme = getTheme(themeId);
|
|
@@ -1736,11 +1747,21 @@ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, l
|
|
|
1736
1747
|
});
|
|
1737
1748
|
}, []);
|
|
1738
1749
|
|
|
1750
|
+
const [scrolledUp, setScrolledUp] = React.useState(false);
|
|
1751
|
+
const updateScrolledUp = React.useCallback((el) => {
|
|
1752
|
+
if (!el) return;
|
|
1753
|
+
const up = el.scrollHeight - el.scrollTop - el.clientHeight > 4;
|
|
1754
|
+
setScrolledUp((current) => (current === up ? current : up));
|
|
1755
|
+
}, []);
|
|
1739
1756
|
const handleBodyScroll = React.useCallback((event) => {
|
|
1740
1757
|
onScroll();
|
|
1758
|
+
updateScrolledUp(event.currentTarget);
|
|
1741
1759
|
if (!preserveHorizontalScroll || syncingHorizontalRef.current) return;
|
|
1742
1760
|
syncScrollLeft(event.currentTarget, stickyRef.current);
|
|
1743
|
-
}, [onScroll, preserveHorizontalScroll, syncScrollLeft]);
|
|
1761
|
+
}, [onScroll, preserveHorizontalScroll, syncScrollLeft, updateScrolledUp]);
|
|
1762
|
+
React.useEffect(() => {
|
|
1763
|
+
updateScrolledUp(ref.current);
|
|
1764
|
+
});
|
|
1744
1765
|
|
|
1745
1766
|
const handleStickyScroll = React.useCallback((event) => {
|
|
1746
1767
|
if (!preserveHorizontalScroll || syncingHorizontalRef.current) return;
|
|
@@ -1758,7 +1779,7 @@ function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, l
|
|
|
1758
1779
|
normalizedSticky.map((line, index) => React.createElement(Line, { key: `sticky:${index}`, line, theme })),
|
|
1759
1780
|
)
|
|
1760
1781
|
: null,
|
|
1761
|
-
React.createElement("div", { ref, className: `ps-scroll-panel ${className}`.trim(), onScroll: handleBodyScroll, onWheel, onTouchStart, onTouchMove, onTouchEnd, onTouchCancel: onTouchEnd },
|
|
1782
|
+
React.createElement("div", { ref, className: `ps-scroll-panel ${className}${scrolledUp ? " is-scrolled-up" : ""}`.trim(), onScroll: handleBodyScroll, onWheel, onTouchStart, onTouchMove, onTouchEnd, onTouchCancel: onTouchEnd },
|
|
1762
1783
|
typeof renderBody === "function"
|
|
1763
1784
|
? renderBody(normalizedLines, theme)
|
|
1764
1785
|
: structuredBlocks
|
|
@@ -2117,19 +2138,18 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
2117
2138
|
() => selectLiveActivityLines(selectorState, { spinnerFrame, maxWidth: viewState.contentWidth }),
|
|
2118
2139
|
[selectorState, spinnerFrame, viewState.contentWidth],
|
|
2119
2140
|
);
|
|
2120
|
-
//
|
|
2121
|
-
//
|
|
2122
|
-
//
|
|
2123
|
-
const
|
|
2124
|
-
() => (liveActivityLines.length > 0
|
|
2125
|
-
? [...lines, [{ text: "", color: null }], ...liveActivityLines]
|
|
2126
|
-
: lines),
|
|
2127
|
-
[lines, liveActivityLines],
|
|
2128
|
-
);
|
|
2141
|
+
// The live-activity line is pinned in the bottom-sticky strip (with the
|
|
2142
|
+
// outbox overlay), NOT appended to the transcript — it must stay put while
|
|
2143
|
+
// chat content scrolls, and it drops the instant the turn ends.
|
|
2144
|
+
const pinnedActivityLines = liveActivityLines;
|
|
2129
2145
|
const outboxLines = React.useMemo(
|
|
2130
2146
|
() => selectOutboxOverlayLines(selectorState, viewState.contentWidth, { tableMode: "sentinel" }),
|
|
2131
2147
|
[selectorState, viewState.contentWidth],
|
|
2132
2148
|
);
|
|
2149
|
+
const stickyBottom = React.useMemo(
|
|
2150
|
+
() => (pinnedActivityLines.length > 0 ? [...outboxLines, ...pinnedActivityLines] : outboxLines),
|
|
2151
|
+
[outboxLines, pinnedActivityLines],
|
|
2152
|
+
);
|
|
2133
2153
|
const composer = showComposer && !viewState.activeSessionIsGroup && viewState.chatViewMode !== "summary"
|
|
2134
2154
|
? React.createElement("div", { className: "ps-chat-composer" },
|
|
2135
2155
|
React.createElement(PromptComposer, { controller, mobile, active: true }))
|
|
@@ -2144,8 +2164,8 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
2144
2164
|
actions: null,
|
|
2145
2165
|
color: chrome.color,
|
|
2146
2166
|
focused: viewState.focused,
|
|
2147
|
-
lines
|
|
2148
|
-
bottomStickyLines:
|
|
2167
|
+
lines,
|
|
2168
|
+
bottomStickyLines: stickyBottom,
|
|
2149
2169
|
scrollOffset: viewState.scroll,
|
|
2150
2170
|
scrollMode: "bottom",
|
|
2151
2171
|
paneKey: "chat",
|
|
@@ -4105,11 +4125,27 @@ function AdminConsolePanel({ controller }) {
|
|
|
4105
4125
|
: null,
|
|
4106
4126
|
React.createElement("section", { className: "ps-admin-console__section" },
|
|
4107
4127
|
React.createElement("h3", null, "GitHub Copilot key"),
|
|
4108
|
-
|
|
4109
|
-
"
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4128
|
+
view.ghcpKey.storeAsSystem
|
|
4129
|
+
? React.createElement("p", { className: "ps-admin-console__hint" },
|
|
4130
|
+
"System-wide key for platform-managed (ownerless) system ",
|
|
4131
|
+
"sessions — agent tuners, repo cache managers, and other ",
|
|
4132
|
+
"sessions with no owner resolve this key for GitHub Copilot ",
|
|
4133
|
+
"models. Clearing it reverts them to the worker default.")
|
|
4134
|
+
: React.createElement("p", { className: "ps-admin-console__hint" },
|
|
4135
|
+
"Per-user override for the GitHub Copilot model provider token. ",
|
|
4136
|
+
"When set, this key is used instead of the worker's env-supplied ",
|
|
4137
|
+
"GITHUB_TOKEN for sessions you own. Clearing the key reverts to ",
|
|
4138
|
+
"the worker default."),
|
|
4139
|
+
view.isAdmin && view.systemGhcpKey.supported
|
|
4140
|
+
? React.createElement("label", { className: "ps-admin-console__system-toggle" },
|
|
4141
|
+
React.createElement("input", {
|
|
4142
|
+
type: "checkbox",
|
|
4143
|
+
checked: view.ghcpKey.storeAsSystem,
|
|
4144
|
+
disabled: view.ghcpKey.saving,
|
|
4145
|
+
onChange: (event) => controller.setAdminGhcpKeyStoreAsSystem(event.target.checked),
|
|
4146
|
+
}),
|
|
4147
|
+
"Store as System key")
|
|
4148
|
+
: null,
|
|
4113
4149
|
React.createElement("p", { className: `ps-admin-console__status${view.ghcpKey.error ? " is-error" : ""}` },
|
|
4114
4150
|
view.ghcpKey.error || view.ghcpKey.statusText),
|
|
4115
4151
|
view.ghcpKey.editing
|
|
@@ -4141,13 +4177,15 @@ function AdminConsolePanel({ controller }) {
|
|
|
4141
4177
|
type: "button",
|
|
4142
4178
|
className: "ps-primary-button",
|
|
4143
4179
|
onClick: onBeginEdit,
|
|
4144
|
-
}, view.ghcpKey.
|
|
4145
|
-
|
|
4180
|
+
}, view.ghcpKey.targetConfigured
|
|
4181
|
+
? (view.ghcpKey.storeAsSystem ? "Replace System key" : "Replace key")
|
|
4182
|
+
: (view.ghcpKey.storeAsSystem ? "Set System key" : "Set key")),
|
|
4183
|
+
view.ghcpKey.targetConfigured
|
|
4146
4184
|
? React.createElement("button", {
|
|
4147
4185
|
type: "button",
|
|
4148
4186
|
className: "ps-mini-button",
|
|
4149
4187
|
onClick: onClear,
|
|
4150
|
-
}, "Clear key")
|
|
4188
|
+
}, view.ghcpKey.storeAsSystem ? "Clear System key" : "Clear key")
|
|
4151
4189
|
: null,
|
|
4152
4190
|
React.createElement("button", {
|
|
4153
4191
|
type: "button",
|