Trim teammate prompt UI and MCP debug logs

This commit is contained in:
2026-04-04 09:19:52 +08:00
parent f06a2c2740
commit 832035a087
4 changed files with 143 additions and 77 deletions

View File

@@ -5,7 +5,6 @@ import * as React from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useInterval } from 'usehooks-ts'; import { useInterval } from 'usehooks-ts';
import { useRegisterOverlay } from '../../context/overlayContext.js'; import { useRegisterOverlay } from '../../context/overlayContext.js';
import { stringWidth } from '../../ink/stringWidth.js';
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw j/k/arrow dialog navigation // eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw j/k/arrow dialog navigation
import { Box, Text, useInput } from '../../ink.js'; import { Box, Text, useInput } from '../../ink.js';
import { useKeybindings } from '../../keybindings/useKeybinding.js'; import { useKeybindings } from '../../keybindings/useKeybinding.js';
@@ -15,7 +14,6 @@ import { getEmptyToolPermissionContext } from '../../Tool.js';
import { AGENT_COLOR_TO_THEME_COLOR } from '../../tools/AgentTool/agentColorManager.js'; import { AGENT_COLOR_TO_THEME_COLOR } from '../../tools/AgentTool/agentColorManager.js';
import { logForDebugging } from '../../utils/debug.js'; import { logForDebugging } from '../../utils/debug.js';
import { execFileNoThrow } from '../../utils/execFileNoThrow.js'; import { execFileNoThrow } from '../../utils/execFileNoThrow.js';
import { truncateToWidth } from '../../utils/format.js';
import { getNextPermissionMode } from '../../utils/permissions/getNextPermissionMode.js'; import { getNextPermissionMode } from '../../utils/permissions/getNextPermissionMode.js';
import { getModeColor, type PermissionMode, permissionModeFromString, permissionModeSymbol } from '../../utils/permissions/PermissionMode.js'; import { getModeColor, type PermissionMode, permissionModeFromString, permissionModeSymbol } from '../../utils/permissions/PermissionMode.js';
import { jsonStringify } from '../../utils/slowOperations.js'; import { jsonStringify } from '../../utils/slowOperations.js';
@@ -381,7 +379,6 @@ function TeammateDetailView(t0) {
teamName, teamName,
onCancel onCancel
} = t0; } = t0;
const [promptExpanded, setPromptExpanded] = useState(false);
const cycleModeShortcut = useShortcutDisplay("confirm:cycleMode", "Confirmation", "shift+tab"); const cycleModeShortcut = useShortcutDisplay("confirm:cycleMode", "Confirmation", "shift+tab");
const themeColor = teammate.color ? AGENT_COLOR_TO_THEME_COLOR[teammate.color as keyof typeof AGENT_COLOR_TO_THEME_COLOR] : undefined; const themeColor = teammate.color ? AGENT_COLOR_TO_THEME_COLOR[teammate.color as keyof typeof AGENT_COLOR_TO_THEME_COLOR] : undefined;
let t1; let t1;
@@ -418,18 +415,6 @@ function TeammateDetailView(t0) {
t3 = $[5]; t3 = $[5];
} }
useEffect(t2, t3); useEffect(t2, t3);
let t4;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t4 = input => {
if (input === "p") {
setPromptExpanded(_temp);
}
};
$[6] = t4;
} else {
t4 = $[6];
}
useInput(t4);
const workingPath = teammate.worktreePath || teammate.cwd; const workingPath = teammate.worktreePath || teammate.cwd;
let subtitleParts; let subtitleParts;
if ($[7] !== teammate.model || $[8] !== teammate.worktreePath || $[9] !== workingPath) { if ($[7] !== teammate.model || $[8] !== teammate.worktreePath || $[9] !== workingPath) {
@@ -498,21 +483,11 @@ function TeammateDetailView(t0) {
} else { } else {
t9 = $[24]; t9 = $[24];
} }
let t10;
if ($[25] !== promptExpanded || $[26] !== teammate.prompt) {
t10 = teammate.prompt && <Box flexDirection="column"><Text bold={true}>Prompt</Text><Text>{promptExpanded ? teammate.prompt : truncateToWidth(teammate.prompt, 80)}{stringWidth(teammate.prompt) > 80 && !promptExpanded && <Text dimColor={true}> (p to expand)</Text>}</Text></Box>;
$[25] = promptExpanded;
$[26] = teammate.prompt;
$[27] = t10;
} else {
t10 = $[27];
}
let t11; let t11;
if ($[28] !== onCancel || $[29] !== subtitle || $[30] !== t10 || $[31] !== t9 || $[32] !== title) { if ($[28] !== onCancel || $[29] !== subtitle || $[31] !== t9 || $[32] !== title) {
t11 = <Dialog title={title} subtitle={subtitle} onCancel={onCancel} color="background" hideInputGuide={true}>{t9}{t10}</Dialog>; t11 = <Dialog title={title} subtitle={subtitle} onCancel={onCancel} color="background" hideInputGuide={true}>{t9}</Dialog>;
$[28] = onCancel; $[28] = onCancel;
$[29] = subtitle; $[29] = subtitle;
$[30] = t10;
$[31] = t9; $[31] = t9;
$[32] = title; $[32] = title;
$[33] = t11; $[33] = t11;

View File

@@ -124,6 +124,31 @@ function redactSensitiveUrlParams(url: string): string {
} }
} }
function summarizeHeadersForDebug(
headers: Record<string, string> | undefined,
): {
headerCount: number
headerNames: string[]
hasAuthorization: boolean
} {
if (!headers) {
return {
headerCount: 0,
headerNames: [],
hasAuthorization: false,
}
}
const headerNames = Object.keys(headers).sort()
return {
headerCount: headerNames.length,
headerNames,
hasAuthorization: headerNames.some(
headerName => headerName.toLowerCase() === 'authorization',
),
}
}
/** /**
* Some OAuth servers (notably Slack) return HTTP 200 for all responses, * Some OAuth servers (notably Slack) return HTTP 200 for all responses,
* signaling errors via the JSON body instead. The SDK's executeTokenRequest * signaling errors via the JSON body instead. The SDK's executeTokenRequest
@@ -696,14 +721,11 @@ async function performMCPXaaAuth(
const haveKeys = Object.keys( const haveKeys = Object.keys(
getSecureStorage().read()?.mcpOAuthClientConfig ?? {}, getSecureStorage().read()?.mcpOAuthClientConfig ?? {},
) )
const headersForLogging = Object.fromEntries(
Object.entries(serverConfig.headers ?? {}).map(([k, v]) =>
k.toLowerCase() === 'authorization' ? [k, '[REDACTED]'] : [k, v],
),
)
logMCPDebug( logMCPDebug(
serverName, serverName,
`XAA: secret lookup miss. wanted=${wantedKey} have=[${haveKeys.join(', ')}] configHeaders=${jsonStringify(headersForLogging)}`, `XAA: secret lookup miss. wanted=${wantedKey} availableKeys=${haveKeys.length} configHeaderSummary=${jsonStringify(
summarizeHeadersForDebug(serverConfig.headers),
)}`,
) )
throw new Error( throw new Error(
`XAA: AS client secret not found for '${serverName}'. Re-add with --client-secret.`, `XAA: AS client secret not found for '${serverName}'. Re-add with --client-secret.`,
@@ -988,7 +1010,9 @@ export async function performMCPOAuthFlow(
provider.setMetadata(metadata) provider.setMetadata(metadata)
logMCPDebug( logMCPDebug(
serverName, serverName,
`Fetched OAuth metadata with scope: ${getScopeFromMetadata(metadata) || 'NONE'}`, `Fetched OAuth metadata (hasScope=${Boolean(
getScopeFromMetadata(metadata),
)})`,
) )
} }
} catch (error) { } catch (error) {
@@ -1170,8 +1194,10 @@ export async function performMCPOAuthFlow(
server.listen(port, '127.0.0.1', async () => { server.listen(port, '127.0.0.1', async () => {
try { try {
logMCPDebug(serverName, `Starting SDK auth`) logMCPDebug(
logMCPDebug(serverName, `Server URL: ${serverConfig.url}`) serverName,
`Starting SDK auth (transport=${serverConfig.type})`,
)
// First call to start the auth flow - should redirect // First call to start the auth flow - should redirect
// Pass the scope and resource_metadata from WWW-Authenticate header if available // Pass the scope and resource_metadata from WWW-Authenticate header if available
@@ -1189,7 +1215,7 @@ export async function performMCPOAuthFlow(
) )
} }
} catch (error) { } catch (error) {
logMCPDebug(serverName, `SDK auth error: ${error}`) logMCPDebug(serverName, `SDK auth error: ${errorMessage(error)}`)
cleanup() cleanup()
rejectOnce(new Error(`SDK auth failed: ${errorMessage(error)}`)) rejectOnce(new Error(`SDK auth failed: ${errorMessage(error)}`))
} }
@@ -1235,9 +1261,13 @@ export async function performMCPOAuthFlow(
if (savedTokens) { if (savedTokens) {
logMCPDebug( logMCPDebug(
serverName, serverName,
`Token access_token length: ${savedTokens.access_token?.length}`, `Token summary after auth: ${jsonStringify({
hasAccessToken: Boolean(savedTokens.access_token),
hasRefreshToken: Boolean(savedTokens.refresh_token),
expiresInSec: savedTokens.expires_in,
hasScope: Boolean(savedTokens.scope),
})}`,
) )
logMCPDebug(serverName, `Token expires_in: ${savedTokens.expires_in}`)
} }
logEvent('tengu_mcp_oauth_flow_success', { logEvent('tengu_mcp_oauth_flow_success', {
@@ -1257,7 +1287,10 @@ export async function performMCPOAuthFlow(
throw new Error('Unexpected auth result: ' + result) throw new Error('Unexpected auth result: ' + result)
} }
} catch (error) { } catch (error) {
logMCPDebug(serverName, `Error during auth completion: ${error}`) logMCPDebug(
serverName,
`Error during auth completion: ${errorMessage(error)}`,
)
// Determine failure reason for attribution telemetry. The try block covers // Determine failure reason for attribution telemetry. The try block covers
// port acquisition, the callback server, the redirect flow, and token // port acquisition, the callback server, the redirect flow, and token
@@ -1429,7 +1462,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider {
metadata.scope = metadataScope metadata.scope = metadataScope
logMCPDebug( logMCPDebug(
this.serverName, this.serverName,
`Using scope from metadata: ${metadata.scope}`, 'Using scope from metadata',
) )
} }
@@ -1445,7 +1478,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider {
get clientMetadataUrl(): string | undefined { get clientMetadataUrl(): string | undefined {
const override = process.env.MCP_OAUTH_CLIENT_METADATA_URL const override = process.env.MCP_OAUTH_CLIENT_METADATA_URL
if (override) { if (override) {
logMCPDebug(this.serverName, `Using CIMD URL from env: ${override}`) logMCPDebug(this.serverName, 'Using CIMD URL from env override')
return override return override
} }
return MCP_CLIENT_METADATA_URL return MCP_CLIENT_METADATA_URL
@@ -1467,7 +1500,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider {
*/ */
markStepUpPending(scope: string): void { markStepUpPending(scope: string): void {
this._pendingStepUpScope = scope this._pendingStepUpScope = scope
logMCPDebug(this.serverName, `Marked step-up pending: ${scope}`) logMCPDebug(this.serverName, 'Marked step-up pending')
} }
async state(): Promise<string> { async state(): Promise<string> {
@@ -1632,7 +1665,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider {
if (needsStepUp) { if (needsStepUp) {
logMCPDebug( logMCPDebug(
this.serverName, this.serverName,
`Step-up pending (${this._pendingStepUpScope}), omitting refresh_token`, 'Step-up pending, omitting refresh_token',
) )
} }
@@ -1693,10 +1726,15 @@ export class ClaudeAuthProvider implements OAuthClientProvider {
token_type: 'Bearer', token_type: 'Bearer',
} }
logMCPDebug(this.serverName, `Returning tokens`) logMCPDebug(
logMCPDebug(this.serverName, `Token length: ${tokens.access_token?.length}`) this.serverName,
logMCPDebug(this.serverName, `Has refresh token: ${!!tokens.refresh_token}`) `Returning tokens: ${jsonStringify({
logMCPDebug(this.serverName, `Expires in: ${Math.floor(expiresIn)}s`) hasAccessToken: Boolean(tokens.access_token),
hasRefreshToken: Boolean(tokens.refresh_token),
hasScope: Boolean(tokens.scope),
expiresInSec: Math.floor(expiresIn),
})}`,
)
return tokens return tokens
} }
@@ -1707,9 +1745,15 @@ export class ClaudeAuthProvider implements OAuthClientProvider {
const existingData = storage.read() || {} const existingData = storage.read() || {}
const serverKey = getServerKey(this.serverName, this.serverConfig) const serverKey = getServerKey(this.serverName, this.serverConfig)
logMCPDebug(this.serverName, `Saving tokens`) logMCPDebug(
logMCPDebug(this.serverName, `Token expires in: ${tokens.expires_in}`) this.serverName,
logMCPDebug(this.serverName, `Has refresh token: ${!!tokens.refresh_token}`) `Saving tokens: ${jsonStringify({
hasAccessToken: Boolean(tokens.access_token),
hasRefreshToken: Boolean(tokens.refresh_token),
hasScope: Boolean(tokens.scope),
expiresInSec: tokens.expires_in,
})}`,
)
const updatedData: SecureStorageData = { const updatedData: SecureStorageData = {
...existingData, ...existingData,

View File

@@ -332,6 +332,51 @@ function mcpBaseUrlAnalytics(serverRef: ScopedMcpServerConfig): {
: {} : {}
} }
function mcpBaseUrlForDebug(serverRef: ScopedMcpServerConfig): string {
return getLoggingSafeMcpBaseUrl(serverRef) || '[unavailable]'
}
function summarizeHeadersForDebug(
headers: Record<string, string> | undefined,
): {
headerCount: number
headerNames: string[]
hasAuthorization: boolean
} {
if (!headers) {
return {
headerCount: 0,
headerNames: [],
hasAuthorization: false,
}
}
const headerNames = Object.keys(headers)
return {
headerCount: headerNames.length,
headerNames: headerNames.sort(),
hasAuthorization: headerNames.some(
headerName => headerName.toLowerCase() === 'authorization',
),
}
}
function summarizeProxyEnvForDebug(): Record<string, string | boolean> {
return {
hasNodeOptions: Boolean(process.env.NODE_OPTIONS),
uvThreadpoolSizeConfigured: Boolean(process.env.UV_THREADPOOL_SIZE),
hasHttpProxy: Boolean(process.env.HTTP_PROXY),
hasHttpsProxy: Boolean(process.env.HTTPS_PROXY),
hasNoProxy: Boolean(process.env.NO_PROXY),
}
}
function summarizeStderrForDebug(stderrOutput: string): string {
const trimmed = stderrOutput.trim()
const lineCount = trimmed === '' ? 0 : trimmed.split('\n').length
return `Server stderr captured (${trimmed.length} chars, ${lineCount} lines)`
}
/** /**
* Shared handler for sse/http/claudeai-proxy auth failures during connect: * Shared handler for sse/http/claudeai-proxy auth failures during connect:
* emits tengu_mcp_server_needs_auth, caches the needs-auth entry, and returns * emits tengu_mcp_server_needs_auth, caches the needs-auth entry, and returns
@@ -676,7 +721,10 @@ export const connectToServer = memoize(
) )
logMCPDebug(name, `SSE transport initialized, awaiting connection`) logMCPDebug(name, `SSE transport initialized, awaiting connection`)
} else if (serverRef.type === 'sse-ide') { } else if (serverRef.type === 'sse-ide') {
logMCPDebug(name, `Setting up SSE-IDE transport to ${serverRef.url}`) logMCPDebug(
name,
`Setting up SSE-IDE transport to ${mcpBaseUrlForDebug(serverRef)}`,
)
// IDE servers don't need authentication // IDE servers don't need authentication
// TODO: Use the auth token provided in the lockfile // TODO: Use the auth token provided in the lockfile
const proxyOptions = getProxyFetchOptions() const proxyOptions = getProxyFetchOptions()
@@ -735,7 +783,7 @@ export const connectToServer = memoize(
} else if (serverRef.type === 'ws') { } else if (serverRef.type === 'ws') {
logMCPDebug( logMCPDebug(
name, name,
`Initializing WebSocket transport to ${serverRef.url}`, `Initializing WebSocket transport to ${mcpBaseUrlForDebug(serverRef)}`,
) )
const combinedHeaders = await getMcpServerHeaders(name, serverRef) const combinedHeaders = await getMcpServerHeaders(name, serverRef)
@@ -749,16 +797,17 @@ export const connectToServer = memoize(
...combinedHeaders, ...combinedHeaders,
} }
// Redact sensitive headers before logging const wsHeadersForLogging = summarizeHeadersForDebug(
const wsHeadersForLogging = mapValues(wsHeaders, (value, key) => mapValues(wsHeaders, (_value, key) =>
key.toLowerCase() === 'authorization' ? '[REDACTED]' : value, key.toLowerCase() === 'authorization' ? '[REDACTED]' : '[set]',
),
) )
logMCPDebug( logMCPDebug(
name, name,
`WebSocket transport options: ${jsonStringify({ `WebSocket transport options: ${jsonStringify({
url: serverRef.url, url: mcpBaseUrlForDebug(serverRef),
headers: wsHeadersForLogging, ...wsHeadersForLogging,
hasSessionAuth: !!sessionIngressToken, hasSessionAuth: !!sessionIngressToken,
})}`, })}`,
) )
@@ -782,20 +831,17 @@ export const connectToServer = memoize(
} }
transport = new WebSocketTransport(wsClient) transport = new WebSocketTransport(wsClient)
} else if (serverRef.type === 'http') { } else if (serverRef.type === 'http') {
logMCPDebug(name, `Initializing HTTP transport to ${serverRef.url}`) logMCPDebug(
name,
`Initializing HTTP transport to ${mcpBaseUrlForDebug(serverRef)}`,
)
logMCPDebug( logMCPDebug(
name, name,
`Node version: ${process.version}, Platform: ${process.platform}`, `Node version: ${process.version}, Platform: ${process.platform}`,
) )
logMCPDebug( logMCPDebug(
name, name,
`Environment: ${jsonStringify({ `Environment: ${jsonStringify(summarizeProxyEnvForDebug())}`,
NODE_OPTIONS: process.env.NODE_OPTIONS || 'not set',
UV_THREADPOOL_SIZE: process.env.UV_THREADPOOL_SIZE || 'default',
HTTP_PROXY: process.env.HTTP_PROXY || 'not set',
HTTPS_PROXY: process.env.HTTPS_PROXY || 'not set',
NO_PROXY: process.env.NO_PROXY || 'not set',
})}`,
) )
// Create an auth provider for this server // Create an auth provider for this server
@@ -843,16 +889,16 @@ export const connectToServer = memoize(
const headersForLogging = transportOptions.requestInit?.headers const headersForLogging = transportOptions.requestInit?.headers
? mapValues( ? mapValues(
transportOptions.requestInit.headers as Record<string, string>, transportOptions.requestInit.headers as Record<string, string>,
(value, key) => (_value, key) =>
key.toLowerCase() === 'authorization' ? '[REDACTED]' : value, key.toLowerCase() === 'authorization' ? '[REDACTED]' : '[set]',
) )
: undefined : undefined
logMCPDebug( logMCPDebug(
name, name,
`HTTP transport options: ${jsonStringify({ `HTTP transport options: ${jsonStringify({
url: serverRef.url, url: mcpBaseUrlForDebug(serverRef),
headers: headersForLogging, ...summarizeHeadersForDebug(headersForLogging),
hasAuthProvider: !!authProvider, hasAuthProvider: !!authProvider,
timeoutMs: MCP_REQUEST_TIMEOUT_MS, timeoutMs: MCP_REQUEST_TIMEOUT_MS,
})}`, })}`,
@@ -879,7 +925,7 @@ export const connectToServer = memoize(
const oauthConfig = getOauthConfig() const oauthConfig = getOauthConfig()
const proxyUrl = `${oauthConfig.MCP_PROXY_URL}${oauthConfig.MCP_PROXY_PATH.replace('{server_id}', serverRef.id)}` const proxyUrl = `${oauthConfig.MCP_PROXY_URL}${oauthConfig.MCP_PROXY_PATH.replace('{server_id}', serverRef.id)}`
logMCPDebug(name, `Using claude.ai proxy at ${proxyUrl}`) logMCPDebug(name, `Using claude.ai proxy transport`)
// eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
const fetchWithAuth = createClaudeAiProxyFetch(globalThis.fetch) const fetchWithAuth = createClaudeAiProxyFetch(globalThis.fetch)
@@ -1025,7 +1071,10 @@ export const connectToServer = memoize(
// For HTTP transport, try a basic connectivity test first // For HTTP transport, try a basic connectivity test first
if (serverRef.type === 'http') { if (serverRef.type === 'http') {
logMCPDebug(name, `Testing basic HTTP connectivity to ${serverRef.url}`) logMCPDebug(
name,
`Testing basic HTTP connectivity to ${mcpBaseUrlForDebug(serverRef)}`,
)
try { try {
const testUrl = new URL(serverRef.url) const testUrl = new URL(serverRef.url)
logMCPDebug( logMCPDebug(
@@ -1079,7 +1128,7 @@ export const connectToServer = memoize(
try { try {
await Promise.race([connectPromise, timeoutPromise]) await Promise.race([connectPromise, timeoutPromise])
if (stderrOutput) { if (stderrOutput) {
logMCPError(name, `Server stderr: ${stderrOutput}`) logMCPError(name, summarizeStderrForDebug(stderrOutput))
stderrOutput = '' // Release accumulated string to prevent memory growth stderrOutput = '' // Release accumulated string to prevent memory growth
} }
const elapsed = Date.now() - connectStartTime const elapsed = Date.now() - connectStartTime
@@ -1149,7 +1198,7 @@ export const connectToServer = memoize(
} }
transport.close().catch(() => {}) transport.close().catch(() => {})
if (stderrOutput) { if (stderrOutput) {
logMCPError(name, `Server stderr: ${stderrOutput}`) logMCPError(name, summarizeStderrForDebug(stderrOutput))
} }
throw error throw error
} }

View File

@@ -20,7 +20,6 @@ export type TeammateStatus = {
agentId: string agentId: string
agentType?: string agentType?: string
model?: string model?: string
prompt?: string
status: 'running' | 'idle' | 'unknown' status: 'running' | 'idle' | 'unknown'
color?: string color?: string
idleSince?: string // ISO timestamp from idle notification idleSince?: string // ISO timestamp from idle notification
@@ -60,7 +59,6 @@ export function getTeammateStatuses(teamName: string): TeammateStatus[] {
agentId: member.agentId, agentId: member.agentId,
agentType: member.agentType, agentType: member.agentType,
model: member.model, model: member.model,
prompt: member.prompt,
status, status,
color: member.color, color: member.color,
tmuxPaneId: member.tmuxPaneId, tmuxPaneId: member.tmuxPaneId,