* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-2): 格式化 commands (79 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-5): 格式化 components其余 + hooks + tools (232 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md - README.md: 大幅重写,更详细版本历史和配置示例 - Run.ps1: 新增 Windows 启动脚本 - TODO.md: 新增包完成清单 - V6.md: 删除(架构重构规划已不适用) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复以前的问题 * fix: 修复 login 面板的问题 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
209 lines
7.0 KiB
TypeScript
209 lines
7.0 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 { Box, Text } from '../../ink.js'
|
|
import { isChannelsEnabled } from '../../services/mcp/channelAllowlist.js'
|
|
import { getEffectiveChannelAllowlist } from '../../services/mcp/channelNotification.js'
|
|
import { getMcpConfigsByScope } from '../../services/mcp/config.js'
|
|
import {
|
|
getClaudeAIOAuthTokens,
|
|
getSubscriptionType,
|
|
} from '../../utils/auth.js'
|
|
import { loadInstalledPluginsV2 } from '../../utils/plugins/installedPluginsManager.js'
|
|
import { getSettingsForSource } from '../../utils/settings/settings.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. getAllowedChannels (bootstrap state), getSettingsForSource
|
|
// (session cache updated by background polling / /login), and
|
|
// isChannelsEnabled (GrowthBook 5-min refresh) must be captured once
|
|
// so a later re-render cannot flip branches.
|
|
const [{ channels, disabled, noAuth, policyBlocked, list, unmatched }] =
|
|
useState(() => {
|
|
const ch = getAllowedChannels()
|
|
if (ch.length === 0)
|
|
return {
|
|
channels: ch,
|
|
disabled: false,
|
|
noAuth: false,
|
|
policyBlocked: false,
|
|
list: '',
|
|
unmatched: [] as Unmatched[],
|
|
}
|
|
const l = ch.map(formatEntry).join(', ')
|
|
const sub = getSubscriptionType()
|
|
const managed = sub === 'team' || sub === 'enterprise'
|
|
const policy = getSettingsForSource('policySettings')
|
|
const allowlist = getEffectiveChannelAllowlist(
|
|
sub,
|
|
policy?.allowedChannelPlugins,
|
|
)
|
|
return {
|
|
channels: ch,
|
|
disabled: !isChannelsEnabled(),
|
|
noAuth: !getClaudeAIOAuthTokens()?.accessToken,
|
|
policyBlocked: managed && policy?.channelsEnabled !== true,
|
|
list: l,
|
|
unmatched: findUnmatched(ch, allowlist),
|
|
}
|
|
})
|
|
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'
|
|
|
|
if (disabled) {
|
|
return (
|
|
<Box paddingLeft={2} flexDirection="column">
|
|
<Text color="error">
|
|
{flag} ignored ({list})
|
|
</Text>
|
|
<Text dimColor>Channels are not currently available</Text>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
if (noAuth) {
|
|
return (
|
|
<Box paddingLeft={2} flexDirection="column">
|
|
<Text color="error">
|
|
{flag} ignored ({list})
|
|
</Text>
|
|
<Text dimColor>
|
|
Channels require claude.ai authentication · run /login, then restart
|
|
</Text>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
if (policyBlocked) {
|
|
return (
|
|
<Box paddingLeft={2} flexDirection="column">
|
|
<Text color="error">
|
|
{flag} blocked by org policy ({list})
|
|
</Text>
|
|
<Text dimColor>Inbound messages will be silently dropped</Text>
|
|
<Text dimColor>
|
|
Have an administrator set channelsEnabled: true in managed settings to
|
|
enable
|
|
</Text>
|
|
{unmatched.map(u => (
|
|
<Text key={`${formatEntry(u.entry)}:${u.why}`} color="warning">
|
|
{formatEntry(u.entry)} · {u.why}
|
|
</Text>
|
|
))}
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
// "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 }
|
|
|
|
function findUnmatched(
|
|
entries: readonly ChannelEntry[],
|
|
allowlist: ReturnType<typeof getEffectiveChannelAllowlist>,
|
|
): 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 scopes = ['enterprise', 'user', 'project', 'local'] as const
|
|
const configured = new Set<string>()
|
|
for (const scope of scopes) {
|
|
for (const name of Object.keys(getMcpConfigsByScope(scope).servers)) {
|
|
configured.add(name)
|
|
}
|
|
}
|
|
|
|
// Plugin-kind installed check: installed_plugins.json keys are
|
|
// `name@marketplace`. loadInstalledPluginsV2 is cached.
|
|
const installedPluginIds = new Set(
|
|
Object.keys(loadInstalledPluginsV2().plugins),
|
|
)
|
|
|
|
// Plugin-kind allowlist check: same {marketplace, plugin} test as the
|
|
// gate at channelNotification.ts. entry.dev bypasses (dev flag opts out
|
|
// of the allowlist). Org list replaces ledger when set (team/enterprise).
|
|
// GrowthBook _CACHED_MAY_BE_STALE — cold cache yields [] so every plugin
|
|
// entry warns; same tradeoff the gate already accepts.
|
|
const { entries: allowed, source } = allowlist
|
|
|
|
// Independent ifs — a plugin entry that's both uninstalled AND
|
|
// unlisted shows two lines. Server kind checks config + dev flag.
|
|
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' })
|
|
}
|
|
if (!entry.dev) {
|
|
out.push({
|
|
entry,
|
|
why: 'server: entries need --dangerously-load-development-channels',
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
if (!installedPluginIds.has(`${entry.name}@${entry.marketplace}`)) {
|
|
out.push({ entry, why: 'plugin not installed' })
|
|
}
|
|
if (
|
|
!entry.dev &&
|
|
!allowed.some(
|
|
e => e.plugin === entry.name && e.marketplace === entry.marketplace,
|
|
)
|
|
) {
|
|
out.push({
|
|
entry,
|
|
why:
|
|
source === 'org'
|
|
? "not on your org's approved channels list"
|
|
: 'not on the approved channels allowlist',
|
|
})
|
|
}
|
|
}
|
|
return out
|
|
}
|