mindweave 2.4.4 → 2.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/App.js +158 -99
- package/dist/cli/App.js.map +1 -1
- package/dist/cli/attachments.js +14 -1
- package/dist/cli/attachments.js.map +1 -1
- package/dist/cli/components/KeyManager.js +3 -42
- package/dist/cli/components/KeyManager.js.map +1 -1
- package/dist/cli/components/McpMinitabs.js +689 -0
- package/dist/cli/components/McpMinitabs.js.map +1 -0
- package/dist/cli/components/minitabs.js +71 -0
- package/dist/cli/components/minitabs.js.map +1 -0
- package/dist/mcp/config.js +57 -3
- package/dist/mcp/config.js.map +1 -1
- package/dist/mcp/configWrite.js +40 -2
- package/dist/mcp/configWrite.js.map +1 -1
- package/dist/mcp/connection.js +51 -1
- package/dist/mcp/connection.js.map +1 -1
- package/dist/mcp/manager.js +65 -0
- package/dist/mcp/manager.js.map +1 -1
- package/dist/mcp/oauth/callback.js +171 -0
- package/dist/mcp/oauth/callback.js.map +1 -0
- package/dist/mcp/oauth/flow.js +398 -0
- package/dist/mcp/oauth/flow.js.map +1 -0
- package/dist/mcp/oauth/index.js +243 -0
- package/dist/mcp/oauth/index.js.map +1 -0
- package/dist/mcp/oauth/metadata.js +192 -0
- package/dist/mcp/oauth/metadata.js.map +1 -0
- package/dist/mcp/oauth/pkce.js +56 -0
- package/dist/mcp/oauth/pkce.js.map +1 -0
- package/dist/mcp/oauth/tokenStore.js +103 -0
- package/dist/mcp/oauth/tokenStore.js.map +1 -0
- package/dist/mcp/transport/http.js +33 -5
- package/dist/mcp/transport/http.js.map +1 -1
- package/dist/tools/mcpAdd.js +7 -0
- package/dist/tools/mcpAdd.js.map +1 -1
- package/package.json +2 -2
package/dist/cli/App.js
CHANGED
|
@@ -38,6 +38,7 @@ import { APPROVAL_DISMISSED, APPROVAL_TEXT } from "../tools/approval.js";
|
|
|
38
38
|
import { KeySetup } from "./components/KeySetup.js";
|
|
39
39
|
import { setupView } from "./keySetup.js";
|
|
40
40
|
import { KeyManager } from "./components/KeyManager.js";
|
|
41
|
+
import { McpMinitabs } from "./components/McpMinitabs.js";
|
|
41
42
|
import { providerRows, keyRowsFor, nextSlotFor } from "./keyManager.js";
|
|
42
43
|
import { keysFor } from "./keyStore.js";
|
|
43
44
|
import { TrustGate } from "./components/TrustGate.js";
|
|
@@ -93,7 +94,7 @@ import { workingVerb } from "./workingVerb.js";
|
|
|
93
94
|
import { narrationPending, revealWait } from "./revealPace.js";
|
|
94
95
|
import { summarizeTask, formatTokens } from "../dynamo/pricing.js";
|
|
95
96
|
import { meterReset, meterDelta, meterTick, meterValue } from "../dynamo/liveMeter.js";
|
|
96
|
-
import { addServerToConfig, configPathFor, parseAddSpec, removeServerFromConfig, splitArgs } from "../mcp/configWrite.js";
|
|
97
|
+
import { addServerToConfig, configPathFor, parseAddSpec, removeServerFromConfig, resolveConfigPath, splitArgs } from "../mcp/configWrite.js";
|
|
97
98
|
import { mapPromptArguments, promptCommand, promptUsage } from "../mcp/prompts.js";
|
|
98
99
|
import { DEFAULT_MODE, modeById, modeFromFlags, nextMode } from "./modes.js";
|
|
99
100
|
import { ApprovalChannel } from "./approvalChannel.js";
|
|
@@ -358,6 +359,13 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
358
359
|
// Distinct from the first-run setup screen, which only ever needs to get one key in.
|
|
359
360
|
const [keysOpen, setKeysOpen] = useState(false);
|
|
360
361
|
const [keysTick, setKeysTick] = useState(0);
|
|
362
|
+
// Opened by /mcp — the minitabs manager: a server list, and picking one drops into
|
|
363
|
+
// everything you can do to it. Its own footer overlay, same shape as /key's.
|
|
364
|
+
const [mcpOpen, setMcpOpen] = useState(false);
|
|
365
|
+
/** Which MCP servers we hold a stored credential for. Read when the box opens rather
|
|
366
|
+
* than kept live: it changes only when someone signs in or out, both of which close
|
|
367
|
+
* the box, and each entry is a file read that has no business on a render path. */
|
|
368
|
+
const [mcpSignedIn, setMcpSignedIn] = useState(() => new Set());
|
|
361
369
|
// A /provider switch held back because that provider had no key. Finished the moment
|
|
362
370
|
// one is saved for it, so choosing a provider and adding its key is one flow rather
|
|
363
371
|
// than two commands with a dead end in between.
|
|
@@ -1230,11 +1238,21 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1230
1238
|
return;
|
|
1231
1239
|
}
|
|
1232
1240
|
const caret = caretCell();
|
|
1241
|
+
const { height, y } = measureElement(liveRef.current);
|
|
1233
1242
|
if (!caret) {
|
|
1234
|
-
|
|
1243
|
+
// NO CARET IS NOT "NOTHING TO DO". It is declared null whenever the input is not the
|
|
1244
|
+
// thing being typed into — a picker is open, a turn is in flight — and publishing
|
|
1245
|
+
// zero there says "the cursor is already on the last row", which is a claim, not an
|
|
1246
|
+
// absence. Exiting from that state left the cursor mid-frame and the shell printed
|
|
1247
|
+
// its prompt across the conversation, one row per Enter.
|
|
1248
|
+
//
|
|
1249
|
+
// The whole region's height is the safe answer instead: CUD is clamped by the
|
|
1250
|
+
// terminal at the bottom row, so overshooting costs a blank line and undershooting
|
|
1251
|
+
// costs the corruption above. Only the full-screen shell publishes a true zero, and
|
|
1252
|
+
// it means it — leaving the alternate buffer restores the cursor with it.
|
|
1253
|
+
setRowsBelowCaret(Math.max(0, height - 1));
|
|
1235
1254
|
return;
|
|
1236
1255
|
}
|
|
1237
|
-
const { height, y } = measureElement(liveRef.current);
|
|
1238
1256
|
// `caretCell` reports the caret's row within the live region; `liveRef` measures that
|
|
1239
1257
|
// region. The last row it drew is `height - 1`, so the gap is what remains below.
|
|
1240
1258
|
setRowsBelowCaret(height - 1 - (caret.y - (y ?? 0)));
|
|
@@ -1383,64 +1401,101 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1383
1401
|
dispatch({ type: "endTurn" });
|
|
1384
1402
|
setBusy(false);
|
|
1385
1403
|
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Write a server's config and connect it live — shared by the typed `/mcp add`, the
|
|
1406
|
+
* `/mcp` box's Add and Edit, and (indirectly, through the same writer) the
|
|
1407
|
+
* `add_mcp_server` tool. One path so a config any of them accepts is one the others
|
|
1408
|
+
* would too. Connects immediately: writing the file and telling the user to restart
|
|
1409
|
+
* would defeat the entire point of a guided add.
|
|
1410
|
+
*/
|
|
1411
|
+
async function mcpWriteAndConnect(spec) {
|
|
1412
|
+
const cur = session.current;
|
|
1413
|
+
if (!cur)
|
|
1414
|
+
return;
|
|
1415
|
+
const path = configPathFor(spec.scope, cur.cwd);
|
|
1416
|
+
try {
|
|
1417
|
+
const written = await addServerToConfig(path, spec);
|
|
1418
|
+
note(`${written.replaced ? "replaced" : "added"} '${spec.name}' in ${path} — connecting…`);
|
|
1419
|
+
const status = await cur.toolContext.mcp?.addServer(spec.config);
|
|
1420
|
+
if (status?.state === "connected") {
|
|
1421
|
+
say(`${spec.name}: connected — ${status.toolCount} tool${status.toolCount === 1 ? "" : "s"} (protocol ${status.version}).`);
|
|
1422
|
+
}
|
|
1423
|
+
else if (status) {
|
|
1424
|
+
// Say what went wrong HERE rather than leaving it to be discovered in /mcp: a
|
|
1425
|
+
// typo'd command is the most likely outcome of typing this by hand.
|
|
1426
|
+
say(`${spec.name}: ${status.state}${status.error ? ` — ${status.error}` : ""}. It's saved; fix the config and /mcp to retry.`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
catch (e) {
|
|
1430
|
+
say(`Couldn't write ${path}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Stop a server and take it out of the config — the file half AND the live half.
|
|
1435
|
+
*
|
|
1436
|
+
* The file half alone used to be the whole of `/mcp remove`, with its own reply saying
|
|
1437
|
+
* "it stays connected until this session ends" — true, but not what "remove" sounds
|
|
1438
|
+
* like it means. `mcp.removeServer` is the live half: it closes the connection and
|
|
1439
|
+
* drops its tools from what the model is offered, right now, not at the next launch.
|
|
1440
|
+
*/
|
|
1441
|
+
async function mcpRemove(name) {
|
|
1442
|
+
const cur = session.current;
|
|
1443
|
+
const mcp = cur?.toolContext.mcp;
|
|
1444
|
+
if (!cur || !mcp)
|
|
1445
|
+
return;
|
|
1446
|
+
const gone = (await removeServerFromConfig(configPathFor("project", cur.cwd), name)) ||
|
|
1447
|
+
(await removeServerFromConfig(configPathFor("global", cur.cwd), name));
|
|
1448
|
+
const stopped = await mcp.removeServer(name);
|
|
1449
|
+
if (!gone && !stopped)
|
|
1450
|
+
return say(`No server called '${name}' is configured.`);
|
|
1451
|
+
say(`Removed '${name}'${stopped ? " and stopped it" : " from the config (it was not running)"}.`);
|
|
1452
|
+
}
|
|
1386
1453
|
/**
|
|
1387
1454
|
* `/mcp add <name> <command|url> [args…]` and `/mcp remove <name>`.
|
|
1388
1455
|
*
|
|
1389
1456
|
* Shares its parser and writer with the `add_mcp_server` tool, so a config the command
|
|
1390
|
-
* accepts is exactly one the tool would, and vice versa.
|
|
1391
|
-
* writing the file and telling the user to restart would defeat the point.
|
|
1457
|
+
* accepts is exactly one the tool would, and vice versa.
|
|
1392
1458
|
*/
|
|
1393
1459
|
async function mcpConfigCommand(arg) {
|
|
1394
|
-
const cur = session.current;
|
|
1395
|
-
if (!cur)
|
|
1396
|
-
return;
|
|
1397
1460
|
const argv = splitArgs(arg);
|
|
1398
1461
|
const verb = argv.shift();
|
|
1399
1462
|
if (verb === "remove" || verb === "rm") {
|
|
1400
1463
|
const target = argv[0];
|
|
1401
1464
|
if (!target)
|
|
1402
1465
|
return say("Usage: /mcp remove <name>");
|
|
1403
|
-
|
|
1404
|
-
const gone = (await removeServerFromConfig(configPathFor("project", root), target)) ||
|
|
1405
|
-
(await removeServerFromConfig(configPathFor("global", root), target));
|
|
1406
|
-
say(gone
|
|
1407
|
-
? `Removed '${target}' from the config. It stays connected until this session ends.`
|
|
1408
|
-
: `No server called '${target}' is configured.`);
|
|
1409
|
-
return;
|
|
1466
|
+
return mcpRemove(target);
|
|
1410
1467
|
}
|
|
1411
1468
|
const parsed = parseAddSpec(argv);
|
|
1412
1469
|
if (!parsed.ok)
|
|
1413
1470
|
return say(parsed.error);
|
|
1414
|
-
|
|
1471
|
+
await mcpWriteAndConnect(parsed.spec);
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Flip a server's `disabled` flag and apply it live — the box's Enable/Disable.
|
|
1475
|
+
*
|
|
1476
|
+
* Reads the server's LIVE config rather than the file, because that is what is
|
|
1477
|
+
* actually true right now; writes back to whichever file already defines it
|
|
1478
|
+
* (`resolveConfigPath`), so this cannot create a second, shadowing entry for a
|
|
1479
|
+
* server that already exists in the other config.
|
|
1480
|
+
*/
|
|
1481
|
+
async function mcpSetDisabled(name, disabled) {
|
|
1482
|
+
const cur = session.current;
|
|
1483
|
+
const mcp = cur?.toolContext.mcp;
|
|
1484
|
+
const config = mcp?.configFor(name);
|
|
1485
|
+
if (!cur || !mcp || !config)
|
|
1486
|
+
return;
|
|
1487
|
+
const path = await resolveConfigPath(cur.cwd, name);
|
|
1488
|
+
const scope = path === configPathFor("global", cur.cwd) ? "global" : "project";
|
|
1489
|
+
const next = { ...config, disabled };
|
|
1415
1490
|
try {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
if (status?.state === "connected") {
|
|
1420
|
-
say(`${parsed.spec.name}: connected — ${status.toolCount} tool${status.toolCount === 1 ? "" : "s"} (protocol ${status.version}).`);
|
|
1421
|
-
}
|
|
1422
|
-
else if (status) {
|
|
1423
|
-
// Say what went wrong HERE rather than leaving it to be discovered in /mcp: a
|
|
1424
|
-
// typo'd command is the most likely outcome of typing this by hand.
|
|
1425
|
-
say(`${parsed.spec.name}: ${status.state}${status.error ? ` — ${status.error}` : ""}. It's saved; fix the config and /mcp to retry.`);
|
|
1426
|
-
}
|
|
1491
|
+
await addServerToConfig(path, { name, scope, config: next });
|
|
1492
|
+
await mcp.addServer(next);
|
|
1493
|
+
note(`${name}: ${disabled ? "disabled" : "enabled"}.`);
|
|
1427
1494
|
}
|
|
1428
1495
|
catch (e) {
|
|
1429
|
-
|
|
1496
|
+
note(`Couldn't update ${path}: ${e instanceof Error ? e.message : String(e)}`, { error: true });
|
|
1430
1497
|
}
|
|
1431
1498
|
}
|
|
1432
|
-
// What Enter does on a /mcp row: review blocked tools if there are any, otherwise
|
|
1433
|
-
// reconnect. Blocked tools take priority because that is the state the user cannot
|
|
1434
|
-
// otherwise resolve — reconnecting will not clear a quarantine.
|
|
1435
|
-
async function mcpAction(name) {
|
|
1436
|
-
const mgr = session.current?.toolContext.mcp;
|
|
1437
|
-
if (mgr && mgr.blockedCountFor(name) > 0) {
|
|
1438
|
-
const allowed = await mgr.reviewQuarantine((q, options) => askApproval.current(q, options));
|
|
1439
|
-
note(allowed ? `${name}: blocked tools allowed for this project.` : `${name}: tools stay blocked.`);
|
|
1440
|
-
return;
|
|
1441
|
-
}
|
|
1442
|
-
await reconnectMcp(name);
|
|
1443
|
-
}
|
|
1444
1499
|
// Reconnect one MCP server by name, reporting the outcome in the chat rather than
|
|
1445
1500
|
// silently. A server that stays down is the thing a user most needs told about.
|
|
1446
1501
|
async function reconnectMcp(name) {
|
|
@@ -1458,6 +1513,57 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1458
1513
|
note(`${name} is ${status.state}${status.error ? ` — ${status.error}` : ""}`);
|
|
1459
1514
|
}
|
|
1460
1515
|
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Sign in to a remote MCP server. Closes the box first, for the same reason Review does:
|
|
1518
|
+
* this takes a browser, a consent screen and a minute of the user's attention, and a
|
|
1519
|
+
* footer overlay frozen on "signing in…" for that long is a frozen app.
|
|
1520
|
+
*
|
|
1521
|
+
* The URL is printed BEFORE the browser is launched. Launching silently fails on
|
|
1522
|
+
* machines with no default browser or no desktop session at all, and the URL on screen
|
|
1523
|
+
* is the difference between "nothing happened" and something the user can paste.
|
|
1524
|
+
*/
|
|
1525
|
+
/**
|
|
1526
|
+
* Sign in to a remote MCP server, reporting INTO the `/mcp` box rather than out here.
|
|
1527
|
+
*
|
|
1528
|
+
* Nothing is written to the transcript, and that is the point: the whole interaction
|
|
1529
|
+
* belongs to one screen about one server, and the alternative was a running commentary
|
|
1530
|
+
* ending in a 400-character URL pasted across the conversation. The box stays open and
|
|
1531
|
+
* shows its own progress; this just does the work and hands back the line to show.
|
|
1532
|
+
*
|
|
1533
|
+
* Throws rather than reporting failure as a string, so the box can style it as an error
|
|
1534
|
+
* and keep the server's own sentence, which is almost always the whole explanation.
|
|
1535
|
+
*/
|
|
1536
|
+
async function mcpSignIn(name, handlers) {
|
|
1537
|
+
const mgr = session.current?.toolContext.mcp;
|
|
1538
|
+
if (!mgr)
|
|
1539
|
+
throw new Error("no MCP manager in this session");
|
|
1540
|
+
const status = await mgr.authenticate(name, handlers);
|
|
1541
|
+
if (!status)
|
|
1542
|
+
throw new Error(`${name} is no longer configured`);
|
|
1543
|
+
if (status.state === "connected") {
|
|
1544
|
+
return `${name} connected — ${status.toolCount} tool${status.toolCount === 1 ? "" : "s"}`;
|
|
1545
|
+
}
|
|
1546
|
+
throw new Error(status.error ? `${name} is ${status.state} — ${status.error}` : `${name} is ${status.state}`);
|
|
1547
|
+
}
|
|
1548
|
+
/** Drop a stored MCP credential. The connection is left running: it keeps working until
|
|
1549
|
+
* its current token is refused, and that 401 is what honestly returns it to needs-auth. */
|
|
1550
|
+
async function mcpSignOut(name) {
|
|
1551
|
+
const mgr = session.current?.toolContext.mcp;
|
|
1552
|
+
if (!mgr)
|
|
1553
|
+
return;
|
|
1554
|
+
await mgr.signOut(name);
|
|
1555
|
+
note(`signed out of ${name}.`);
|
|
1556
|
+
}
|
|
1557
|
+
/** Review blocked tools from the /mcp box: closes it first, since the approval channel
|
|
1558
|
+
* and the box's own screen cannot share the one footer surface at the same time. */
|
|
1559
|
+
async function mcpReviewBlocked(name) {
|
|
1560
|
+
setMcpOpen(false);
|
|
1561
|
+
const mgr = session.current?.toolContext.mcp;
|
|
1562
|
+
if (!mgr)
|
|
1563
|
+
return;
|
|
1564
|
+
const allowed = await mgr.reviewQuarantine((q, options) => askApproval.current(q, options));
|
|
1565
|
+
note(allowed ? `${name}: blocked tools allowed for this project.` : `${name}: tools stay blocked.`);
|
|
1566
|
+
}
|
|
1461
1567
|
// Stop every root's background lane and any running shells from the current
|
|
1462
1568
|
// session (before a swap).
|
|
1463
1569
|
async function stopCurrentLanes() {
|
|
@@ -2141,11 +2247,6 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2141
2247
|
note(`stopped shell #${sh.id} (${clipCmd(sh.command)})`);
|
|
2142
2248
|
}
|
|
2143
2249
|
}
|
|
2144
|
-
else if (o.kind === "mcp") {
|
|
2145
|
-
const server = o.items[index];
|
|
2146
|
-
if (server && server.state !== "disabled")
|
|
2147
|
-
void mcpAction(server.name);
|
|
2148
|
-
}
|
|
2149
2250
|
else if (o.kind === "approval") {
|
|
2150
2251
|
approvals.current.answer(o.options[index] ?? o.options[0]);
|
|
2151
2252
|
showNextApproval.current();
|
|
@@ -2460,15 +2561,15 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2460
2561
|
await mcpConfigCommand(route.arg);
|
|
2461
2562
|
return;
|
|
2462
2563
|
}
|
|
2463
|
-
// /mcp — server
|
|
2564
|
+
// /mcp — the minitabs manager: your servers, and "+ Add a server" always at the top,
|
|
2565
|
+
// so an empty config is a place to add one rather than a dead end.
|
|
2464
2566
|
if (name === "/mcp") {
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
setOverlay({ kind: "mcp", items: mcp.statuses() });
|
|
2567
|
+
// Which servers are signed in is read once, here, before the box is shown. Doing it
|
|
2568
|
+
// as the box opens rather than on every render keeps a handful of file reads off
|
|
2569
|
+
// the render path, and this is the only moment the answer can change unseen.
|
|
2570
|
+
const mgr = session.current?.toolContext.mcp;
|
|
2571
|
+
setMcpSignedIn(mgr ? await mgr.credentialed().catch(() => new Set()) : new Set());
|
|
2572
|
+
setMcpOpen(true);
|
|
2472
2573
|
return;
|
|
2473
2574
|
}
|
|
2474
2575
|
if (name === "/continue") {
|
|
@@ -2909,6 +3010,10 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2909
3010
|
setKeysOpen(false);
|
|
2910
3011
|
} }, keysTick));
|
|
2911
3012
|
}
|
|
3013
|
+
if (mcpOpen) {
|
|
3014
|
+
const mcp = session.current?.toolContext.mcp;
|
|
3015
|
+
return (_jsx(McpMinitabs, { servers: mcp?.statuses() ?? [], blockedCountFor: (name) => mcp?.blockedCountFor(name) ?? 0, configFor: (name) => mcp?.configFor(name), onSubmit: (spec) => void mcpWriteAndConnect(spec), onSetDisabled: (name, disabled) => void mcpSetDisabled(name, disabled), onRemove: (name) => void mcpRemove(name), onReconnect: (name) => void reconnectMcp(name), onReviewBlocked: (name) => void mcpReviewBlocked(name), signedIn: mcpSignedIn, onSignIn: mcpSignIn, onCopyLink: (url) => copyToClipboard(url), onSignOut: (name) => void mcpSignOut(name), width: width, maxRows: maxRows, onClose: () => setMcpOpen(false) }));
|
|
3016
|
+
}
|
|
2912
3017
|
if (!overlay)
|
|
2913
3018
|
return null;
|
|
2914
3019
|
const cur = session.current;
|
|
@@ -2937,14 +3042,6 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2937
3042
|
}));
|
|
2938
3043
|
return (_jsx(Picker, { title: "Background shells", items: items, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
2939
3044
|
}
|
|
2940
|
-
if (overlay.kind === "mcp") {
|
|
2941
|
-
const mgr = cur?.toolContext.mcp;
|
|
2942
|
-
const items = overlay.items.map((srv) => ({
|
|
2943
|
-
label: mcpLabel(srv),
|
|
2944
|
-
description: mcpDetail(srv, mgr?.blockedCountFor(srv.name) ?? 0),
|
|
2945
|
-
}));
|
|
2946
|
-
return _jsx(Picker, { title: "MCP servers", items: items, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel });
|
|
2947
|
-
}
|
|
2948
3045
|
if (overlay.kind === "provider") {
|
|
2949
3046
|
const activeModel = cur?.modelConfig.model ?? DEFAULT_MODEL_CONFIG.model;
|
|
2950
3047
|
const active = providerOf(activeModel).id;
|
|
@@ -3744,44 +3841,6 @@ function clipCmd(command, max = 44) {
|
|
|
3744
3841
|
function shellElapsed(sh) {
|
|
3745
3842
|
return fmtElapsed(Math.floor((Date.now() - sh.startedAt) / 1000));
|
|
3746
3843
|
}
|
|
3747
|
-
/** One MCP server's headline row: name plus a state marker readable at a glance. */
|
|
3748
|
-
export function mcpLabel(server) {
|
|
3749
|
-
const marker = server.state === "connected" ? "●" : server.state === "pending" ? "◐" : server.state === "disabled" ? "○" : "✗";
|
|
3750
|
-
return `${marker} ${server.name}`;
|
|
3751
|
-
}
|
|
3752
|
-
/** The dim detail column: what this server is doing, and what Enter would do to it. */
|
|
3753
|
-
export function mcpDetail(server, blocked = 0) {
|
|
3754
|
-
// A blocked tool is the one state the user must act on, so it outranks everything
|
|
3755
|
-
// else in the row — including a healthy connection.
|
|
3756
|
-
if (blocked > 0) {
|
|
3757
|
-
return `${blocked} tool${blocked === 1 ? "" : "s"} BLOCKED (description changed) — Enter to review`;
|
|
3758
|
-
}
|
|
3759
|
-
switch (server.state) {
|
|
3760
|
-
case "connected": {
|
|
3761
|
-
// Prompts are counted separately because they are the user's to invoke, not the
|
|
3762
|
-
// model's: a server offering only prompts would otherwise read as "0 tools" and
|
|
3763
|
-
// look broken.
|
|
3764
|
-
const counts = [
|
|
3765
|
-
`${server.toolCount} tool${server.toolCount === 1 ? "" : "s"}`,
|
|
3766
|
-
...(server.promptCount ? [`${server.promptCount} prompt${server.promptCount === 1 ? "" : "s"}`] : []),
|
|
3767
|
-
...(server.offersResources ? ["resources"] : []),
|
|
3768
|
-
];
|
|
3769
|
-
const tools = counts.join(", ");
|
|
3770
|
-
// The negotiated revision is worth surfacing: "legacy" is why a server may be
|
|
3771
|
-
// missing capabilities, and it is otherwise invisible.
|
|
3772
|
-
const proto = server.version ? ` · ${server.version}${server.legacy ? " (legacy)" : ""}` : "";
|
|
3773
|
-
return `${tools}${proto} — Enter to reconnect`;
|
|
3774
|
-
}
|
|
3775
|
-
case "pending":
|
|
3776
|
-
return "connecting…";
|
|
3777
|
-
case "needs-auth":
|
|
3778
|
-
return "needs authentication — Enter to retry";
|
|
3779
|
-
case "disabled":
|
|
3780
|
-
return "disabled in mcp.json";
|
|
3781
|
-
default:
|
|
3782
|
-
return `${server.error ?? "failed"} — Enter to retry`;
|
|
3783
|
-
}
|
|
3784
|
-
}
|
|
3785
3844
|
/**
|
|
3786
3845
|
* The under-the-chat indicator for background shells: one running shell shows its
|
|
3787
3846
|
* command + elapsed; several collapse to a count. It self-ticks once a second while
|