120 lines
4.3 KiB
TypeScript
120 lines
4.3 KiB
TypeScript
// Conditionally require()'d in LogoV2.tsx behind feature('KAIROS') ||
|
|
// feature('KAIROS_CHANNELS'). No feature() guard here — the whole file
|
|
// tree-shakes via the require pattern when both flags are false (see
|
|
// docs/feature-gating.md). Do NOT import this module statically from
|
|
// unguarded code.
|
|
|
|
import * as React from 'react';
|
|
import { useState } from 'react';
|
|
import { type ChannelEntry, getAllowedChannels, getHasDevChannels } from '../../bootstrap/state.js';
|
|
import { getBuiltinPlugins } from '../../plugins/builtinPlugins.js';
|
|
import { Box, Text } from '@anthropic/ink';
|
|
import { getMcpConfigsByScope } from '../../services/mcp/config.js';
|
|
import { loadInstalledPluginsV2 } from '../../utils/plugins/installedPluginsManager.js';
|
|
|
|
export function ChannelsNotice(): React.ReactNode {
|
|
// Snapshot all reads at mount. This notice enters scrollback immediately
|
|
// after the logo; any re-render past that point forces a full terminal
|
|
// reset.
|
|
const [{ channels, list, unmatched }] = useState(() => {
|
|
const ch = getAllowedChannels();
|
|
if (ch.length === 0)
|
|
return {
|
|
channels: ch,
|
|
list: '',
|
|
unmatched: [] as Unmatched[],
|
|
};
|
|
const l = ch.map(formatEntry).join(', ');
|
|
return {
|
|
channels: ch,
|
|
list: l,
|
|
unmatched: findUnmatched(ch),
|
|
};
|
|
});
|
|
if (channels.length === 0) return null;
|
|
|
|
// When both flags are passed, the list mixes entries and a single flag
|
|
// name would be wrong for half of it. entry.dev distinguishes origin.
|
|
const hasNonDev = channels.some(c => !c.dev);
|
|
const flag =
|
|
getHasDevChannels() && hasNonDev
|
|
? 'Channels'
|
|
: getHasDevChannels()
|
|
? '--dangerously-load-development-channels'
|
|
: '--channels';
|
|
|
|
// "Listening for" not "active" — at this point we only know the allowlist
|
|
// was set. Server connection, capability declaration, and whether the name
|
|
// even matches a configured MCP server are all still unknown.
|
|
return (
|
|
<Box paddingLeft={2} flexDirection="column">
|
|
<Text color="error">Listening for channel messages from: {list}</Text>
|
|
<Text dimColor>
|
|
Experimental · inbound messages will be pushed into this session, this carries prompt injection risks. Restart
|
|
Claude Code without {flag} to disable.
|
|
</Text>
|
|
{unmatched.map(u => (
|
|
<Text key={`${formatEntry(u.entry)}:${u.why}`} color="warning">
|
|
{formatEntry(u.entry)} · {u.why}
|
|
</Text>
|
|
))}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function formatEntry(c: ChannelEntry): string {
|
|
return c.kind === 'plugin' ? `plugin:${c.name}@${c.marketplace}` : `server:${c.name}`;
|
|
}
|
|
|
|
type Unmatched = { entry: ChannelEntry; why: string };
|
|
|
|
type FindUnmatchedDeps = {
|
|
configuredServerNames?: ReadonlySet<string>;
|
|
installedPluginIds?: ReadonlySet<string>;
|
|
};
|
|
|
|
export function findUnmatched(entries: readonly ChannelEntry[], deps?: FindUnmatchedDeps): Unmatched[] {
|
|
// Server-kind: build one Set from all scopes up front. getMcpConfigsByScope
|
|
// is not cached (project scope walks the dir tree); getMcpConfigByName would
|
|
// redo that walk per entry.
|
|
const configured =
|
|
deps?.configuredServerNames ??
|
|
(() => {
|
|
const scopes = ['enterprise', 'user', 'project', 'local'] as const;
|
|
const names = new Set<string>();
|
|
for (const scope of scopes) {
|
|
for (const name of Object.keys(getMcpConfigsByScope(scope).servers)) {
|
|
names.add(name);
|
|
}
|
|
}
|
|
return names;
|
|
})();
|
|
|
|
// Plugin-kind installed check: installed_plugins.json keys are
|
|
// `name@marketplace`. loadInstalledPluginsV2 is cached.
|
|
const installedPluginIds =
|
|
deps?.installedPluginIds ??
|
|
(() => {
|
|
const ids = new Set(Object.keys(loadInstalledPluginsV2().plugins));
|
|
const builtinPlugins = getBuiltinPlugins();
|
|
for (const plugin of [...builtinPlugins.enabled, ...builtinPlugins.disabled]) {
|
|
ids.add(plugin.source);
|
|
}
|
|
return ids;
|
|
})();
|
|
|
|
const out: Unmatched[] = [];
|
|
for (const entry of entries) {
|
|
if (entry.kind === 'server') {
|
|
if (!configured.has(entry.name)) {
|
|
out.push({ entry, why: 'no MCP server configured with that name' });
|
|
}
|
|
continue;
|
|
}
|
|
if (!installedPluginIds.has(`${entry.name}@${entry.marketplace}`)) {
|
|
out.push({ entry, why: 'plugin not installed' });
|
|
}
|
|
}
|
|
return out;
|
|
}
|