pilotswarm 0.5.15 → 0.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/tui/plugins/.mcp.json +7 -1
- package/tui/plugins/agents/deepwiki.agent.md +44 -0
- package/tui/src/node-sdk-transport.js +14 -2
- package/ui/core/src/controller.js +85 -11
- package/ui/core/src/formatting.js +25 -0
- package/ui/core/src/selectors.js +11 -3
- package/ui/react/src/web-app.js +43 -10
- package/web/api/router.js +3 -1
- package/web/dist/assets/{index-CZizkB5Z.js → index-Df1mD9p0.js} +2 -2
- package/web/dist/assets/{index-D9e2TGjO.css → index-whIft7M6.css} +1 -1
- package/web/dist/assets/pilotswarm-DR5j-z8y.js +90 -0
- package/web/dist/index.html +3 -3
- package/web/dist/assets/pilotswarm-KMqn3ZJs.js +0 -90
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pilotswarm",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.16",
|
|
4
4
|
"description": "PilotSwarm application package: terminal UI, browser portal + Web API server, and MCP server — one install, three bins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"hono": "^4.12.10",
|
|
79
79
|
"ink": "^6.8.0",
|
|
80
80
|
"jose": "^6.2.2",
|
|
81
|
-
"pilotswarm-sdk": "^0.5.
|
|
81
|
+
"pilotswarm-sdk": "^0.5.16",
|
|
82
82
|
"react": "^19.2.4",
|
|
83
83
|
"react-dom": "^19.2.4",
|
|
84
84
|
"ws": "^8.18.2"
|
package/tui/plugins/.mcp.json
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
schemaVersion: 2
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
name: deepwiki
|
|
5
|
+
title: DeepWiki Code Explorer
|
|
6
|
+
description: Answers questions about any public GitHub repository using the DeepWiki MCP server. Loads ONLY the DeepWiki MCP — no other servers — demonstrating per-agent MCP scoping.
|
|
7
|
+
mcpServers:
|
|
8
|
+
- deepwiki
|
|
9
|
+
inheritDefaultMcpServers: false
|
|
10
|
+
initialPrompt: >
|
|
11
|
+
Introduce yourself as the DeepWiki Code Explorer. Ask the user which public
|
|
12
|
+
GitHub repository (owner/repo) they want to explore and what they want to know.
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# DeepWiki Code Explorer
|
|
16
|
+
|
|
17
|
+
You answer questions about **public GitHub codebases** using the **DeepWiki**
|
|
18
|
+
MCP server — the only MCP server available to you. DeepWiki serves
|
|
19
|
+
AI-generated documentation grounded in a repository's code.
|
|
20
|
+
|
|
21
|
+
Your DeepWiki tools:
|
|
22
|
+
- `read_wiki_structure` — list the documentation topics/sections for a repo.
|
|
23
|
+
- `read_wiki_contents` — read the full generated documentation for a repo.
|
|
24
|
+
- `ask_question` — ask a natural-language question about a repo and get an
|
|
25
|
+
answer grounded in its code and docs.
|
|
26
|
+
|
|
27
|
+
All of these take a repository as `owner/repo` (for example `facebook/react`).
|
|
28
|
+
|
|
29
|
+
How to work:
|
|
30
|
+
1. Identify the target repository as `owner/repo`. If the user names a project
|
|
31
|
+
without the owner, or it's ambiguous, ask which `owner/repo` they mean
|
|
32
|
+
before calling a tool.
|
|
33
|
+
2. For a specific question, prefer `ask_question`. For an overview or to find
|
|
34
|
+
the right area first, start with `read_wiki_structure`, then drill in with
|
|
35
|
+
`read_wiki_contents` or `ask_question`.
|
|
36
|
+
3. Ground every answer in what DeepWiki returns and name the repo (and the
|
|
37
|
+
topic/section) you drew from. If DeepWiki has no coverage for a repo, say so
|
|
38
|
+
plainly instead of guessing.
|
|
39
|
+
4. Keep answers focused and technical; quote identifiers and file/module names
|
|
40
|
+
when they help.
|
|
41
|
+
|
|
42
|
+
You have no web browsing and cannot clone or read repositories directly —
|
|
43
|
+
everything comes through the DeepWiki MCP server. If a question can't be
|
|
44
|
+
answered from DeepWiki, say what's missing.
|
|
@@ -1037,8 +1037,20 @@ export class NodeSdkTransport {
|
|
|
1037
1037
|
: null;
|
|
1038
1038
|
if (profile?.githubCopilotKeySet === true) return effectiveModel;
|
|
1039
1039
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1040
|
+
// The caller never picked this Copilot model — it came from the
|
|
1041
|
+
// catalog default. Fall back to the first non-Copilot model instead
|
|
1042
|
+
// of failing a session the user has no key for.
|
|
1043
|
+
if (!model && typeof this.mgmt.listModels === "function") {
|
|
1044
|
+
const fallback = (this.mgmt.listModels() || [])
|
|
1045
|
+
.find((entry) => entry?.providerType !== "github" && entry?.qualifiedName);
|
|
1046
|
+
if (fallback) return fallback.qualifiedName;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
throw Object.assign(
|
|
1050
|
+
new Error(
|
|
1051
|
+
"GitHub Copilot key missing or invalid. Set GITHUB_TOKEN on the worker or configure your per-user GitHub Copilot key in Admin before using GitHub Copilot models.",
|
|
1052
|
+
),
|
|
1053
|
+
{ code: "GHCP_KEY_MISSING", status: 400 },
|
|
1042
1054
|
);
|
|
1043
1055
|
}
|
|
1044
1056
|
|
|
@@ -216,6 +216,22 @@ const CONTEXT_TIER_LABELS = {
|
|
|
216
216
|
long_context: "Long context (larger window, higher cost)",
|
|
217
217
|
};
|
|
218
218
|
|
|
219
|
+
function formatContextWindowSize(value) {
|
|
220
|
+
const tokens = Number(value);
|
|
221
|
+
if (!Number.isSafeInteger(tokens) || tokens <= 0) return null;
|
|
222
|
+
if (tokens % 1_000_000 === 0) return `${tokens / 1_000_000}M`;
|
|
223
|
+
if (tokens % 1_000 === 0) return `${tokens / 1_000}K`;
|
|
224
|
+
return tokens.toLocaleString("en-US");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function formatContextTierLabel(tier, tokenLimit) {
|
|
228
|
+
const size = formatContextWindowSize(tokenLimit);
|
|
229
|
+
if (!size) return CONTEXT_TIER_LABELS[tier] || tier;
|
|
230
|
+
return tier === "long_context"
|
|
231
|
+
? `Long context (${size} tokens, higher cost)`
|
|
232
|
+
: `Default (${size} tokens)`;
|
|
233
|
+
}
|
|
234
|
+
|
|
219
235
|
function extractSessionModelFromEvents(events = []) {
|
|
220
236
|
// Only explicit model-change events may update the session's model.
|
|
221
237
|
// Deriving it from any event that happens to carry a `model` field lets
|
|
@@ -1103,6 +1119,7 @@ export class PilotSwarmUiController {
|
|
|
1103
1119
|
text: String(prompt || ""),
|
|
1104
1120
|
createdAt: Date.now(),
|
|
1105
1121
|
phase: normalizedPhase,
|
|
1122
|
+
attempted: false,
|
|
1106
1123
|
clientMessageIds: [id],
|
|
1107
1124
|
};
|
|
1108
1125
|
}
|
|
@@ -1122,9 +1139,9 @@ export class PilotSwarmUiController {
|
|
|
1122
1139
|
}
|
|
1123
1140
|
|
|
1124
1141
|
getEditableOutboxItems(sessionId) {
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1127
|
-
return this.getPendingOutboxItems(sessionId);
|
|
1142
|
+
// Attempted envelopes are immutable because the server may already
|
|
1143
|
+
// have accepted their IDs even when the client saw a transport error.
|
|
1144
|
+
return this.getPendingOutboxItems(sessionId).filter((item) => item?.attempted !== true);
|
|
1128
1145
|
}
|
|
1129
1146
|
|
|
1130
1147
|
getPromptEditSessionMatch(sessionId = this.getState().sessions.activeSessionId) {
|
|
@@ -1412,7 +1429,11 @@ export class PilotSwarmUiController {
|
|
|
1412
1429
|
return false;
|
|
1413
1430
|
}
|
|
1414
1431
|
|
|
1415
|
-
const
|
|
1432
|
+
const allPendingItems = this.getPendingOutboxItems(sessionId);
|
|
1433
|
+
const attemptedItem = allPendingItems.find((item) => item?.attempted === true);
|
|
1434
|
+
const pendingItems = attemptedItem
|
|
1435
|
+
? [attemptedItem]
|
|
1436
|
+
: allPendingItems.filter((item) => item?.attempted !== true);
|
|
1416
1437
|
if (pendingItems.length === 0) return false;
|
|
1417
1438
|
|
|
1418
1439
|
// Merge all current pending items into a single durable envelope.
|
|
@@ -1430,6 +1451,7 @@ export class PilotSwarmUiController {
|
|
|
1430
1451
|
text: mergedText,
|
|
1431
1452
|
createdAt: pendingItems[0].createdAt || Date.now(),
|
|
1432
1453
|
phase: "pending",
|
|
1454
|
+
attempted: true,
|
|
1433
1455
|
clientMessageIds: mergedClientMessageIds,
|
|
1434
1456
|
};
|
|
1435
1457
|
const pendingIdSet = new Set(pendingItems.map((item) => item.id));
|
|
@@ -1480,10 +1502,11 @@ export class PilotSwarmUiController {
|
|
|
1480
1502
|
}
|
|
1481
1503
|
}, 6000);
|
|
1482
1504
|
} else {
|
|
1483
|
-
// Transient failure:
|
|
1484
|
-
//
|
|
1485
|
-
|
|
1486
|
-
|
|
1505
|
+
// Transient failure: preserve the exact attempted envelope.
|
|
1506
|
+
// Re-merging it with fresh messages could make server-side
|
|
1507
|
+
// duplicate suppression drop the fresh content too.
|
|
1508
|
+
const reverted = items.map((item) => (
|
|
1509
|
+
item.id === mergedItem.id ? { ...mergedItem, phase: "pending", attempted: true } : item
|
|
1487
1510
|
));
|
|
1488
1511
|
this.setSessionOutboxItems(sessionId, reverted);
|
|
1489
1512
|
}
|
|
@@ -3763,7 +3786,8 @@ export class PilotSwarmUiController {
|
|
|
3763
3786
|
const items = supported.map((tier) => ({
|
|
3764
3787
|
id: tier,
|
|
3765
3788
|
tier,
|
|
3766
|
-
|
|
3789
|
+
tokenLimit: modelItem?.contextWindowSizes?.[tier] || null,
|
|
3790
|
+
label: formatContextTierLabel(tier, modelItem?.contextWindowSizes?.[tier]),
|
|
3767
3791
|
isDefault: selectedTier === tier,
|
|
3768
3792
|
}));
|
|
3769
3793
|
const selectedIndex = Math.max(0, items.findIndex((item) => item.id === selectedTier));
|
|
@@ -3800,25 +3824,38 @@ export class PilotSwarmUiController {
|
|
|
3800
3824
|
const groupedModels = typeof this.transport.getModelsByProvider === "function"
|
|
3801
3825
|
? this.transport.getModelsByProvider()
|
|
3802
3826
|
: groupModelsByProvider(models);
|
|
3827
|
+
// Whether the current user has a per-user GitHub Copilot key:
|
|
3828
|
+
// true/false when known, null when the transport can't tell (then
|
|
3829
|
+
// no model gets disabled — never lock models out on a guess).
|
|
3830
|
+
const ghcpUserKeySet = await this._resolveGhcpUserKeySet();
|
|
3803
3831
|
const items = [];
|
|
3804
3832
|
const groups = groupedModels
|
|
3805
3833
|
.map((group) => ({
|
|
3806
3834
|
providerId: group.providerId,
|
|
3807
3835
|
providerType: group.type || group.providerType,
|
|
3808
3836
|
models: (group.models || []).map((model) => {
|
|
3837
|
+
const providerType = model.providerType || group.type || group.providerType;
|
|
3838
|
+
// Copilot models with no worker-level token are unusable
|
|
3839
|
+
// for users who haven't stored their own GitHub key.
|
|
3840
|
+
const ghcpKeyMissing = providerType === "github"
|
|
3841
|
+
&& model.credentialAvailable === false
|
|
3842
|
+
&& ghcpUserKeySet === false;
|
|
3809
3843
|
const item = {
|
|
3810
3844
|
id: model.qualifiedName,
|
|
3811
3845
|
qualifiedName: model.qualifiedName,
|
|
3812
3846
|
modelName: model.modelName || model.qualifiedName,
|
|
3813
3847
|
providerId: model.providerId || group.providerId,
|
|
3814
|
-
providerType
|
|
3848
|
+
providerType,
|
|
3815
3849
|
description: model.description || "",
|
|
3816
3850
|
cost: model.cost || null,
|
|
3817
3851
|
supportedReasoningEfforts: normalizeReasoningEfforts(model.supportedReasoningEfforts),
|
|
3818
3852
|
defaultReasoningEffort: model.defaultReasoningEffort || null,
|
|
3819
3853
|
supportedContextTiers: normalizeContextTiers(model.supportedContextTiers),
|
|
3820
3854
|
defaultContextTier: model.defaultContextTier || null,
|
|
3855
|
+
contextWindowSizes: model.contextWindowSizes || null,
|
|
3821
3856
|
isDefault: defaultModel === model.qualifiedName,
|
|
3857
|
+
ghcpKeyMissing,
|
|
3858
|
+
disabled: ghcpKeyMissing,
|
|
3822
3859
|
};
|
|
3823
3860
|
items.push(item);
|
|
3824
3861
|
return item;
|
|
@@ -3827,7 +3864,12 @@ export class PilotSwarmUiController {
|
|
|
3827
3864
|
.filter((group) => group.models.length > 0);
|
|
3828
3865
|
|
|
3829
3866
|
const preferredModel = sessionOptions?.model || defaultModel;
|
|
3830
|
-
|
|
3867
|
+
// Never preselect a key-blocked Copilot model: when the preferred
|
|
3868
|
+
// model (often the catalog default) is unusable, land on the first
|
|
3869
|
+
// usable model instead.
|
|
3870
|
+
let selectedIndex = items.findIndex((model) => model.qualifiedName === preferredModel && !model.disabled);
|
|
3871
|
+
if (selectedIndex < 0) selectedIndex = items.findIndex((model) => !model.disabled);
|
|
3872
|
+
if (selectedIndex < 0) selectedIndex = 0;
|
|
3831
3873
|
this.dispatch({
|
|
3832
3874
|
type: "ui/modal",
|
|
3833
3875
|
modal: {
|
|
@@ -3843,6 +3885,27 @@ export class PilotSwarmUiController {
|
|
|
3843
3885
|
this.dispatch({ type: "ui/status", text: "Select a model and press Enter" });
|
|
3844
3886
|
}
|
|
3845
3887
|
|
|
3888
|
+
async _resolveGhcpUserKeySet() {
|
|
3889
|
+
// Freshest signal first: the profile the Admin console loaded in this
|
|
3890
|
+
// session (updates immediately after the user saves a key there).
|
|
3891
|
+
const adminProfile = this.getState().admin?.profile;
|
|
3892
|
+
if (typeof adminProfile?.githubCopilotKeySet === "boolean") {
|
|
3893
|
+
return adminProfile.githubCopilotKeySet;
|
|
3894
|
+
}
|
|
3895
|
+
if (typeof this.transport.getCurrentUserProfile !== "function") return null;
|
|
3896
|
+
try {
|
|
3897
|
+
const profile = await this.transport.getCurrentUserProfile();
|
|
3898
|
+
if (profile && typeof profile.githubCopilotKeySet === "boolean") {
|
|
3899
|
+
return profile.githubCopilotKeySet;
|
|
3900
|
+
}
|
|
3901
|
+
// Profile fetch succeeded but no stored flag — a user with no
|
|
3902
|
+
// profile row has no key.
|
|
3903
|
+
return false;
|
|
3904
|
+
} catch {
|
|
3905
|
+
return null;
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3846
3909
|
async openSwitchModelPicker(onApplied = null) {
|
|
3847
3910
|
// Callers (e.g. the portal Manage modal) can be notified when a switch
|
|
3848
3911
|
// is actually applied — distinct from cancelling the picker — so they
|
|
@@ -4903,6 +4966,16 @@ export class PilotSwarmUiController {
|
|
|
4903
4966
|
}
|
|
4904
4967
|
if (modal.type === "modelPicker") {
|
|
4905
4968
|
const item = modal.items?.[modal.selectedIndex || 0];
|
|
4969
|
+
if (item?.disabled) {
|
|
4970
|
+
// Keep the picker open — the selection is unusable, not wrong.
|
|
4971
|
+
this.dispatch({
|
|
4972
|
+
type: "ui/status",
|
|
4973
|
+
text: item.ghcpKeyMissing
|
|
4974
|
+
? "This model needs a GitHub Copilot key — add yours in the Admin console first"
|
|
4975
|
+
: "This model is not available",
|
|
4976
|
+
});
|
|
4977
|
+
return;
|
|
4978
|
+
}
|
|
4906
4979
|
const previousFocus = modal.previousFocus;
|
|
4907
4980
|
this.dispatch({ type: "ui/modal", modal: null });
|
|
4908
4981
|
if (previousFocus) {
|
|
@@ -5243,6 +5316,7 @@ export class PilotSwarmUiController {
|
|
|
5243
5316
|
const items = this.getSessionOutbox(currentUi.promptEdit.sessionId);
|
|
5244
5317
|
const nextItems = items.map((item) => (
|
|
5245
5318
|
item.id === currentUi.promptEdit.itemId && item.phase === "pending"
|
|
5319
|
+
&& item.attempted !== true
|
|
5246
5320
|
? { ...item, text: nextPrompt }
|
|
5247
5321
|
: item
|
|
5248
5322
|
));
|
|
@@ -992,6 +992,7 @@ export function buildMessageCardLines({
|
|
|
992
992
|
borderColor = "gray",
|
|
993
993
|
bodyColor = null,
|
|
994
994
|
fitToContent = false,
|
|
995
|
+
tableMode = null,
|
|
995
996
|
} = {}) {
|
|
996
997
|
const maxWidth = Math.max(fitToContent ? 12 : 20, Number(width) || (fitToContent ? 12 : 20));
|
|
997
998
|
const maxContentWidth = Math.max(1, maxWidth - 4);
|
|
@@ -999,6 +1000,30 @@ export function buildMessageCardLines({
|
|
|
999
1000
|
{ text: ` ${String(title || "SYSTEM").toUpperCase()} `, color: titleColor, bold: true },
|
|
1000
1001
|
...(timestamp ? [{ text: ` ${String(timestamp)} `, color: "gray" }] : []),
|
|
1001
1002
|
];
|
|
1003
|
+
|
|
1004
|
+
// Sentinel mode (browser portal): the card is laid out with CSS, so
|
|
1005
|
+
// emit cardStart/cardEnd bounds around the UNWRAPPED markdown body
|
|
1006
|
+
// instead of drawing box art. Hard-wrapping here would mangle
|
|
1007
|
+
// preformatted content (box-drawn tables, ASCII art) that the browser
|
|
1008
|
+
// renders as real HTML tables or scrollable lines.
|
|
1009
|
+
if (tableMode === "sentinel") {
|
|
1010
|
+
const sentinelBodyLines = parseMarkdownLines(String(body || ""), {
|
|
1011
|
+
width: maxContentWidth,
|
|
1012
|
+
tableMode,
|
|
1013
|
+
});
|
|
1014
|
+
const sentinelTinted = bodyColor
|
|
1015
|
+
? sentinelBodyLines.map((lineRuns) => (Array.isArray(lineRuns)
|
|
1016
|
+
? recolorRuns(lineRuns, bodyColor)
|
|
1017
|
+
: lineRuns))
|
|
1018
|
+
: sentinelBodyLines;
|
|
1019
|
+
return [
|
|
1020
|
+
{ kind: "cardStart", runs: titleRuns, borderColor },
|
|
1021
|
+
...sentinelTinted,
|
|
1022
|
+
{ kind: "cardEnd" },
|
|
1023
|
+
[{ text: "", color: null }],
|
|
1024
|
+
];
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1002
1027
|
const bodyLines = parseMarkdownLines(String(body || ""), { width: maxContentWidth });
|
|
1003
1028
|
const normalizedBodyLines = bodyLines.length > 0
|
|
1004
1029
|
? bodyLines.flatMap((lineRuns) => wrapRunsToDisplayWidth(lineRuns, maxContentWidth))
|
package/ui/core/src/selectors.js
CHANGED
|
@@ -1923,6 +1923,7 @@ function buildChatMessageLines(message, maxWidth, options = {}) {
|
|
|
1923
1923
|
width: Math.max(20, maxWidth),
|
|
1924
1924
|
titleColor: USER_CHAT_COLOR,
|
|
1925
1925
|
borderColor: USER_CHAT_COLOR,
|
|
1926
|
+
tableMode: options.tableMode,
|
|
1926
1927
|
}),
|
|
1927
1928
|
...buildChatMessageLines({
|
|
1928
1929
|
...message,
|
|
@@ -2000,6 +2001,7 @@ function buildChatMessageLines(message, maxWidth, options = {}) {
|
|
|
2000
2001
|
width: Math.max(20, maxWidth),
|
|
2001
2002
|
titleColor: message?.cardTitleColor || (message?.role === "system" ? "yellow" : "cyan"),
|
|
2002
2003
|
borderColor: message?.cardBorderColor || "gray",
|
|
2004
|
+
tableMode: options.tableMode,
|
|
2003
2005
|
...(isSystemCard ? { bodyColor: "gray", fitToContent: true } : {}),
|
|
2004
2006
|
});
|
|
2005
2007
|
}
|
|
@@ -3778,7 +3780,7 @@ function buildNodeMapHeaderLine(nodeLabels, colWidth) {
|
|
|
3778
3780
|
return runs;
|
|
3779
3781
|
}
|
|
3780
3782
|
|
|
3781
|
-
const SEQ_REASONING_EFFORTS = new Set(["low", "medium", "high", "xhigh", "
|
|
3783
|
+
const SEQ_REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
3782
3784
|
|
|
3783
3785
|
function shortModelForSequence(value) {
|
|
3784
3786
|
const model = String(value || "").trim();
|
|
@@ -4086,10 +4088,12 @@ export function selectModelPickerModal(state, maxWidth = 72) {
|
|
|
4086
4088
|
? modal.items.findIndex((item) => item.id === model.id)
|
|
4087
4089
|
: -1;
|
|
4088
4090
|
const isSelected = itemIndex === selectedIndex;
|
|
4091
|
+
const isDisabled = Boolean(model.disabled);
|
|
4089
4092
|
const labelRuns = fitRuns([
|
|
4090
4093
|
{ text: "· ", color: "gray" },
|
|
4091
|
-
{ text: model.modelName || model.qualifiedName || model.id, color: "white", bold: Boolean(model.isDefault) },
|
|
4094
|
+
{ text: model.modelName || model.qualifiedName || model.id, color: isDisabled ? "gray" : "white", bold: Boolean(model.isDefault) && !isDisabled },
|
|
4092
4095
|
...(model.cost ? [{ text: ` [${model.cost}]`, color: "gray" }] : []),
|
|
4096
|
+
...(isDisabled ? [{ text: " ⊘ needs GitHub key", color: "yellow" }] : []),
|
|
4093
4097
|
...(model.isDefault ? [{ text: " ← current default", color: "gray" }] : []),
|
|
4094
4098
|
], contentWidth);
|
|
4095
4099
|
|
|
@@ -4126,6 +4130,9 @@ export function selectModelPickerModal(state, maxWidth = 72) {
|
|
|
4126
4130
|
? [[{ text: `Reasoning: ${supportedReasoning.join(", ")}`, color: "gray" }]]
|
|
4127
4131
|
: []),
|
|
4128
4132
|
...(defaultReasoning ? [[{ text: `Default reasoning: ${defaultReasoning}`, color: "gray" }]] : []),
|
|
4133
|
+
...(selectedItem.disabled
|
|
4134
|
+
? [[{ text: "Unavailable: requires a GitHub Copilot key. Add yours in the Admin console, then reopen this picker.", color: "yellow" }]]
|
|
4135
|
+
: []),
|
|
4129
4136
|
[{ text: "", color: "gray" }],
|
|
4130
4137
|
[{
|
|
4131
4138
|
text: selectedItem.description || "No description available for this model.",
|
|
@@ -4298,11 +4305,12 @@ export function selectContextTierPickerModal(state, maxWidth = 64) {
|
|
|
4298
4305
|
|
|
4299
4306
|
const selectedItem = items[selectedIndex] || null;
|
|
4300
4307
|
const modelItem = modal.modelItem || null;
|
|
4308
|
+
const selectedLabel = selectedItem?.label || selectedItem?.id;
|
|
4301
4309
|
const detailsLines = [
|
|
4302
4310
|
[{ text: modelItem?.modelName || modelItem?.qualifiedName || "Selected model", color: "white", bold: true }],
|
|
4303
4311
|
[{ text: `${modelItem?.providerId || "provider"} (${modelItem?.providerType || "provider"})`, color: "gray" }],
|
|
4304
4312
|
[{ text: "", color: "gray" }],
|
|
4305
|
-
[{ text:
|
|
4313
|
+
[{ text: selectedLabel ? `Using context window: ${selectedLabel}` : "Choose a context window.", color: "white" }],
|
|
4306
4314
|
[{ text: "Long context costs more per token.", color: "gray" }],
|
|
4307
4315
|
];
|
|
4308
4316
|
|
package/ui/react/src/web-app.js
CHANGED
|
@@ -422,8 +422,9 @@ function normalizeLines(lines) {
|
|
|
422
422
|
continue;
|
|
423
423
|
}
|
|
424
424
|
// Sentinel kinds preserved as-is so parseStructuredChatBlocks can
|
|
425
|
-
// recognize and render them (e.g. markdownTable → HTML <table
|
|
426
|
-
|
|
425
|
+
// recognize and render them (e.g. markdownTable → HTML <table>,
|
|
426
|
+
// cardStart/cardEnd → styled card with structured body).
|
|
427
|
+
if (line?.kind === "markdownTable" || line?.kind === "cardStart" || line?.kind === "cardEnd") {
|
|
427
428
|
normalized.push(line);
|
|
428
429
|
continue;
|
|
429
430
|
}
|
|
@@ -1415,6 +1416,30 @@ function parseStructuredChatBlocks(lines = []) {
|
|
|
1415
1416
|
continue;
|
|
1416
1417
|
}
|
|
1417
1418
|
|
|
1419
|
+
// Sentinel card bounds emitted by buildMessageCardLines in sentinel
|
|
1420
|
+
// mode. The body lines between the bounds are UNWRAPPED, so parse
|
|
1421
|
+
// them recursively — box-drawn/markdown tables inside the card
|
|
1422
|
+
// become real HTML tables instead of hard-wrapped box art.
|
|
1423
|
+
if (currentLine?.kind === "cardStart") {
|
|
1424
|
+
const innerLines = [];
|
|
1425
|
+
index += 1;
|
|
1426
|
+
while (index < lines.length && lines[index]?.kind !== "cardEnd") {
|
|
1427
|
+
innerLines.push(lines[index]);
|
|
1428
|
+
index += 1;
|
|
1429
|
+
}
|
|
1430
|
+
if (index < lines.length) index += 1;
|
|
1431
|
+
if (index < lines.length && lineText(lines[index]).trim().length === 0) {
|
|
1432
|
+
index += 1;
|
|
1433
|
+
}
|
|
1434
|
+
blocks.push({
|
|
1435
|
+
type: "card",
|
|
1436
|
+
headerRuns: Array.isArray(currentLine.runs) ? currentLine.runs : [],
|
|
1437
|
+
borderColor: currentLine.borderColor || "gray",
|
|
1438
|
+
blocks: parseStructuredChatBlocks(innerLines),
|
|
1439
|
+
});
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1418
1443
|
// Sentinel markdown-table line emitted by parseMarkdownLines when
|
|
1419
1444
|
// tableMode === "sentinel". Carries the raw header + rows so the
|
|
1420
1445
|
// portal renders a real HTML table with markdown cell content (so
|
|
@@ -1563,9 +1588,15 @@ function parseStructuredChatBlocks(lines = []) {
|
|
|
1563
1588
|
|
|
1564
1589
|
function StructuredChatBlocks({ lines, theme }) {
|
|
1565
1590
|
const blocks = React.useMemo(() => parseStructuredChatBlocks(lines), [lines]);
|
|
1591
|
+
return React.createElement(StructuredBlockList, { blocks, theme });
|
|
1592
|
+
}
|
|
1566
1593
|
|
|
1594
|
+
// Renders parsed chat blocks; sentinel card blocks recurse through this list
|
|
1595
|
+
// so structured content (box/markdown tables, code fences) inside a card
|
|
1596
|
+
// renders exactly the same as it does at top level.
|
|
1597
|
+
function StructuredBlockList({ blocks, theme }) {
|
|
1567
1598
|
return React.createElement(React.Fragment, null,
|
|
1568
|
-
blocks.map((block, index) => {
|
|
1599
|
+
(blocks || []).map((block, index) => {
|
|
1569
1600
|
if (block.type === "preserve") {
|
|
1570
1601
|
const variantClass = block.splashVariant ? ` is-splash-${block.splashVariant}` : "";
|
|
1571
1602
|
return React.createElement("div", { key: `preserve:${index}`, className: `ps-chat-preserve-block${variantClass}` },
|
|
@@ -1593,10 +1624,12 @@ function StructuredChatBlocks({ lines, theme }) {
|
|
|
1593
1624
|
React.createElement("header", { className: "ps-chat-card-header" },
|
|
1594
1625
|
React.createElement(Runs, { runs: block.headerRuns, theme })),
|
|
1595
1626
|
React.createElement("div", { className: "ps-chat-card-body" },
|
|
1596
|
-
(block.
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1627
|
+
Array.isArray(block.blocks)
|
|
1628
|
+
? React.createElement(StructuredBlockList, { blocks: block.blocks, theme })
|
|
1629
|
+
: (block.bodyLines || []).map((bodyRuns, bodyIndex) => React.createElement("div", {
|
|
1630
|
+
key: `card:${index}:line:${bodyIndex}`,
|
|
1631
|
+
className: "ps-chat-card-line",
|
|
1632
|
+
}, React.createElement(Runs, { runs: bodyRuns, theme })) )));
|
|
1600
1633
|
}
|
|
1601
1634
|
|
|
1602
1635
|
if (block.type === "table") {
|
|
@@ -2193,7 +2226,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
2193
2226
|
},
|
|
2194
2227
|
React.createElement("div", {
|
|
2195
2228
|
className: "ps-line ps-session-row-content",
|
|
2196
|
-
style: { paddingInlineStart: `${Math.max(0, row.depth) *
|
|
2229
|
+
style: { paddingInlineStart: `${Math.max(0, row.depth) * 4}px` },
|
|
2197
2230
|
},
|
|
2198
2231
|
React.createElement(SessionRowContent, { row, theme, structured: structuredRows })),
|
|
2199
2232
|
)),
|
|
@@ -4119,7 +4152,7 @@ function ModalLayer({ controller }) {
|
|
|
4119
4152
|
return React.createElement("button", {
|
|
4120
4153
|
key: item?.id || `row:${rowIndex}`,
|
|
4121
4154
|
type: "button",
|
|
4122
|
-
className: `ps-list-button ps-modal-list-button${itemIndex === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}`,
|
|
4155
|
+
className: `ps-list-button ps-modal-list-button${itemIndex === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}${item?.disabled ? " is-disabled" : ""}`,
|
|
4123
4156
|
onClick: () => controller.dispatch({ type: "ui/modalSelection", index: itemIndex }),
|
|
4124
4157
|
},
|
|
4125
4158
|
React.createElement("div", { className: "ps-line ps-modal-list-line" },
|
|
@@ -4128,7 +4161,7 @@ function ModalLayer({ controller }) {
|
|
|
4128
4161
|
: (modal.items || []).map((item, index) => React.createElement("button", {
|
|
4129
4162
|
key: item.id || index,
|
|
4130
4163
|
type: "button",
|
|
4131
|
-
className: `ps-list-button ps-modal-list-button${index === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}`,
|
|
4164
|
+
className: `ps-list-button ps-modal-list-button${index === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}${item?.disabled ? " is-disabled" : ""}`,
|
|
4132
4165
|
onClick: () => controller.dispatch({ type: "ui/modalSelection", index }),
|
|
4133
4166
|
},
|
|
4134
4167
|
React.createElement("div", { className: "ps-line ps-modal-list-line" },
|
package/web/api/router.js
CHANGED
|
@@ -18,6 +18,8 @@ import { getPublicAuthContext } from "../auth/authz/engine.js";
|
|
|
18
18
|
|
|
19
19
|
const ERROR_STATUS_BY_CODE = {
|
|
20
20
|
INVALID_REQUEST: 400,
|
|
21
|
+
// GitHub Copilot model requested without a worker or per-user key.
|
|
22
|
+
GHCP_KEY_MISSING: 400,
|
|
21
23
|
PORTAL_AUTH_REQUIRED: 401,
|
|
22
24
|
UNAUTHORIZED: 401,
|
|
23
25
|
FORBIDDEN: 403,
|
|
@@ -32,7 +34,7 @@ const ERROR_STATUS_BY_CODE = {
|
|
|
32
34
|
// messages. These are client errors (their message is the whole value), so
|
|
33
35
|
// they keep their text; everything else that reaches 500 is treated as an
|
|
34
36
|
// unexpected fault and gets a generic message.
|
|
35
|
-
const CLIENT_ERROR_MESSAGE = /must be|is required|cannot be|Unsupported|is not started|terminal orchestration|cannot accept new messages|not found|already|Unknown model|does not support reasoning effort/i;
|
|
37
|
+
const CLIENT_ERROR_MESSAGE = /must be|is required|cannot be|Unsupported|is not started|terminal orchestration|cannot accept new messages|not found|already|Unknown model|does not support reasoning effort|GitHub Copilot key/i;
|
|
36
38
|
|
|
37
39
|
function statusForError(error) {
|
|
38
40
|
const byCode = ERROR_STATUS_BY_CODE[error?.code];
|