Skip to content

feat(agent): add provider-specific agent detail inventories - #1692

Open
techotaku39 wants to merge 7 commits into
tiann:mainfrom
techotaku39:feat/agent-context-details
Open

feat(agent): add provider-specific agent detail inventories#1692
techotaku39 wants to merge 7 commits into
tiann:mainfrom
techotaku39:feat/agent-context-details

Conversation

@techotaku39

@techotaku39 techotaku39 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add persisted provider-specific agent detail metadata for Claude Code and Codex.
  • Display Agent commands, Skills, MCP, and Claude system tools in the Agent details dialog.
  • Include configured and runtime-resolved Codex MCP servers while keeping Codex system tools intentionally excluded because Codex does not provide a stable complete inventory.
  • Normalize provider usage updates and preserve authoritative empty inventories without retaining stale capabilities.
  • Preserve legacy Claude top-level command/tool fallback.
  • Keep capability discovery asynchronous so it does not delay Codex startup.
  • Add responsive two-column desktop and single-column mobile layouts with fixed dialog header and symmetric scrollbar gutters.

User impact

Users can inspect the capabilities available to Claude Code and Codex from the connection status entry. Saved details remain available after a session is archived.

This change currently covers Claude Code and Codex only. Other Agent providers are unchanged and do not publish contextDetails.

Validation

  • bun typecheck — passed
  • Root Playwright terminal-wrap-fidelity.spec.ts — 2 passed
  • CLI targeted context, inventory, and launcher tests — 124 passed
  • Web targeted StatusBar.popover.test.tsx — 13 passed
  • Shared full test suite — 280 tests passed
  • bun run build — passed
  • git diff --check — passed
  • GitHub test, drift-gate, and Codex PR review checks — passed

Data and migration

No database migration is required. Existing metadata is compacted when a later contextDetails update is published.

Related Issues

None

AI assistance

OpenAI Codex (GPT-5.6) was used for implementation, testing, investigation, and PR preparation.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Select the session model before persisting Claude's context window — modelUsage is a model-keyed record that can include the main model and Task/subagent models, but the new fallback takes the first object value. Existing handling explicitly distinguishes the resolved session model from subagent entries (cli/src/claude/utils/sdkToLogConverter.ts:394). If a 200k subagent entry precedes a 1M session entry, persisted metadata can overwrite the correct window and the web fallback will calculate the wrong remaining budget. Evidence cli/src/agent/contextDetails.ts:109
    Suggested fix:

    const modelUsage = asRecord(result?.modelUsage ?? result?.model_usage)
    const entries = modelUsage ? Object.entries(modelUsage) : []
    const selectedUsage = model && modelUsage
        ? asRecord(modelUsage[model])
        : entries.length === 1
            ? asRecord(entries[0][1])
            : null

    Use selectedUsage for contextWindow, and pass the last raw system/init model when a result has no model.

  • [Minor] Allow authoritative empty inventories to clear persisted entries — buildCodexContextDetails omits skills, commands, and MCP arrays when they are empty, while mergeContextDetails retains omitted previous fields. Consequently, the added skills/changed refresh at cli/src/codex/codexRemoteLauncher.ts:3498 cannot remove the final deleted/disabled skill, so archived and live dialogs continue showing stale capabilities. Evidence cli/src/agent/contextDetails.ts:209
    Suggested fix:

    const codex: CodexContextDetails = {
        ...(args.slashCommands !== undefined ? { slashCommands: normalizeCommands(args.slashCommands) } : {}),
        ...(args.skills !== undefined ? { skills: args.skills.filter((skill) => skill.enabled).map(({ name }) => ({ name })) } : {}),
        ...(args.mcpServers !== undefined ? { mcpServers } : {})
    }

Questions

  • None.

Summary

  • Review mode: initial. Two introduced metadata correctness issues found: ambiguous Claude model selection and stale inventories that cannot be cleared.

Testing

  • Not run (automation). Suggested coverage: a multi-entry Claude modelUsage case with the main model not first, and a Codex skills/changed refresh from one skill to an empty list.

HAPI Bot

Comment thread cli/src/agent/contextDetails.ts Outdated
const contextUsage = asRecord(args.contextUsage)
const system = asRecord(args.system)
const result = asRecord(args.result)
const resultModelUsage = firstRecord(result?.modelUsage ?? result?.model_usage)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] Select the session model before persisting contextWindow

modelUsage can contain both the current session model and Task/subagent models. The fallback below eventually returns the first object value, unlike the existing converter which resolves the main model explicitly (cli/src/claude/utils/sdkToLogConverter.ts:394). A 200k subagent entry can therefore overwrite a 1M main-session window.

Suggested fix:

const modelUsage = asRecord(result?.modelUsage ?? result?.model_usage)
const entries = modelUsage ? Object.entries(modelUsage) : []
const selectedUsage = model && modelUsage
    ? asRecord(modelUsage[model])
    : entries.length === 1
        ? asRecord(entries[0][1])
        : null

Use selectedUsage for the result fallback, and have callers pass the last raw system/init model when result.model is absent.

Comment thread cli/src/agent/contextDetails.ts Outdated
}))
: undefined
const codex: CodexContextDetails = {
...(args.slashCommands && args.slashCommands.length > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] Preserve explicit empty inventories so refreshes can clear stale data

These conditions omit fields when an authoritative inventory is empty. mergeContextDetails then retains the previous arrays, so the new skills/changed refresh cannot remove the last deleted/disabled skill.

Suggested fix:

const codex: CodexContextDetails = {
    ...(args.slashCommands !== undefined
        ? { slashCommands: normalizeCommands(args.slashCommands) }
        : {}),
    ...(args.skills !== undefined
        ? { skills: args.skills.filter((skill) => skill.enabled).map(({ name }) => ({ name })) }
        : {}),
    ...(args.mcpServers !== undefined ? { mcpServers } : {})
}

Add a refresh test that changes skills from one entry to [] and asserts the persisted list is cleared.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Merge against the metadata value inside the serialized updater — publishContextDetails snapshots and merges before updateMetadata runs. The real client queues update handlers and only advances its cached metadata after the hub ACK, while the local Claude scanner can publish several events synchronously from one scan. Those calls can therefore merge from the same old value; a later usage-only event can overwrite the system/skill inventory queued immediately before it. Evidence cli/src/agent/contextDetails.ts:308
    Suggested fix:

    export function publishContextDetails(client: ContextDetailsClient, next: ContextDetails): void {
        if (!client.updateMetadata) return
        client.updateMetadata((metadata) => {
            const current = metadata.contextDetails
            const merged = mergeContextDetails(current, next)
            return merged === current ? metadata : { ...metadata, contextDetails: merged }
        })
    }
  • [Major] Publish the local Codex inventory independently of token events and include skills — the added local path only calls publishContextDetails from a token_count event, and passes commands/MCP but never a skill catalog. Terminal-started Codex uses local mode by default, so Agent details is empty until the first usage record and its Skills section stays empty for the whole local session. Evidence cli/src/codex/codexLocalLauncher.ts:227
    Suggested fix:

    const [commands, discoveredSkills] = await Promise.all([
        listSlashCommands('codex', effectiveCodexCwd),
        listSkills(effectiveCodexCwd, { flavor: 'codex' })
    ])
    availableSlashCommands = commands.map(({ name }) => name)
    availableSkills = discoveredSkills.map(({ name }) => ({ name, enabled: true }))
    
    publishContextDetails(session.client, buildCodexContextDetails({
        slashCommands: availableSlashCommands,
        skills: availableSkills,
        mcpServers
    }))

    Widen the builder's skill input to the minimal { name; enabled } shape so local discovery does not need fake path/scope fields.

Questions

  • None.

Summary

  • Review mode: follow-up after new commits. The two prior findings are fixed; two newly identified correctness gaps remain in metadata update ordering and the default local Codex inventory path.

Testing

  • Not run (automation; PR code was not executed). Add a queued-updater test that applies two publishers after both are scheduled, plus a local Codex launcher test asserting commands, skills, and MCP metadata exists before any token_count.

HAPI Bot

Comment thread cli/src/agent/contextDetails.ts Outdated

export function publishContextDetails(client: ContextDetailsClient, next: ContextDetails): void {
if (!client.updateMetadata) return
const current = client.getMetadata?.()?.contextDetails

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] Merge inside the serialized metadata updater

This reads and merges contextDetails before updateMetadata runs. ApiSession.updateMetadata queues handlers and updates its cache only after the hub ACK, while the local Claude scanner can invoke this publisher several times synchronously from one scan. Multiple calls can therefore snapshot the same old metadata; a later usage-only event can overwrite the inventory queued immediately before it.

Suggested fix:

client.updateMetadata((metadata) => {
    const current = metadata.contextDetails
    const merged = mergeContextDetails(current, next)
    return merged === current ? metadata : { ...metadata, contextDetails: merged }
})

Add a test with a fake client that queues two updater functions, then applies them in order and verifies the final value contains fields from both publishers.

flushPendingExecWrapper(message.callId, message);
}
} else {
if (message.type === 'token_count') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] Publish a complete local inventory before the first token event

This is the only local Codex publish site. It waits for token_count, and the payload includes commands/MCP but no skills. Because terminal-started Codex defaults to local mode, Agent details is empty before the first response and never shows local Codex skills.

Suggested fix:

const [commands, discoveredSkills] = await Promise.all([
    listSlashCommands('codex', effectiveCodexCwd),
    listSkills(effectiveCodexCwd, { flavor: 'codex' })
])
availableSlashCommands = commands.map(({ name }) => name)
availableSkills = discoveredSkills.map(({ name }) => ({ name, enabled: true }))
publishContextDetails(session.client, buildCodexContextDetails({
    slashCommands: availableSlashCommands,
    skills: availableSkills,
    mcpServers
}))

Widen the builder's skill input to the minimal { name; enabled } shape, and reuse availableSkills in later token updates.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Publish the resolved Codex MCP inventory, not only HAPI's injected bridge - the current payload omits user/project-configured Codex MCP servers.
  • [Major] Normalize the actual Codex token-usage shape - standard last_token_usage.input_tokens events do not populate persisted context usage.
  • [Minor] Preserve connection state in the trigger's accessible name - the explicit label masks offline, permission, thinking, and background-task status.

Questions

  • None.

Summary

Review mode: follow-up after new commits. The two findings from the prior HAPI Bot review are fixed. Three issues remain: incomplete Codex MCP inventory, dropped Codex usage for the repository's standard event shape, and an accessibility regression in the connection-status trigger.

Testing

Not run (automation; PR code was not executed). GitHub's test job passed. The runner integration job failed, but the same base commit's main workflow is also failing, so it is not attributed to this diff. Add coverage for a configured non-HAPI MCP server, snake_case last_token_usage.input_tokens, and an accessible name containing the live connection state.

HAPI Bot

Comment thread cli/src/codex/codexLocalLauncher.ts Outdated
publishContextDetails(session.client, buildCodexContextDetails({
slashCommands: slashCommandsLoaded ? availableSlashCommands : undefined,
skills: skillsLoaded ? availableSkills : undefined,
mcpServers

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] Publish a complete Codex MCP inventory

mcpServers here comes directly from buildHapiMcpBridge, which returns only the injected hapi server (buildHapiMcpBridge.ts:126). Codex still loads user/project MCP servers from its normal configuration, so both local and remote launchers persist an incomplete list while the UI labels it "MCP servers". Any non-HAPI server is silently missing from the new capability inventory.

Suggested fix:

const resolvedMcpServers = await loadCodexMcpServers(effectiveCodexCwd)
const contextMcpServers = { ...resolvedMcpServers, ...mcpServers }

publishContextDetails(session.client, buildCodexContextDetails({
    slashCommands: slashCommandsLoaded ? availableSlashCommands : undefined,
    skills: skillsLoaded ? availableSkills : undefined,
    mcpServers: contextMcpServers
}))

Use the provider's resolved runtime/config inventory in the remote path too. If a complete inventory is unavailable, omit or explicitly rename this section rather than presenting only the HAPI bridge as authoritative.


function buildCodexUsage(value: unknown, fallbackContextTokens?: number): ContextUsageSnapshot | undefined {
const usage = normalizeUsageSnapshot(value)
if (!usage) return undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] Accept Codex input_tokens as the current context

The repository's app-server fixture emits last_token_usage: { input_tokens, output_tokens } without context_tokens (codexRemoteLauncher.test.ts:482). normalizeUsageSnapshot returns undefined for that standard shape, and this early return drops usage entirely. Consequently the persisted contextDetails fallback never receives the current Codex context for those events.

Suggested fix:

function buildCodexUsage(value: unknown, fallbackContextTokens?: number): ContextUsageSnapshot | undefined {
    const record = asRecord(value)
    const normalized = normalizeUsageSnapshot(value)
    const contextTokens = normalized?.contextTokens
        ?? fallbackContextTokens
        ?? asTokenCount(record?.inputTokens ?? record?.input_tokens)
    const cacheReadTokens = normalized?.cacheReadTokens

    return contextTokens === undefined && cacheReadTokens === undefined
        ? undefined
        : {
            ...(contextTokens !== undefined ? { contextTokens } : {}),
            ...(cacheReadTokens !== undefined ? { cacheReadTokens } : {})
        }
}

Add a test using the exact snake_case event shape already used by the remote-launcher harness.

</div>
<ContextDetailsDialog
details={displayContextDetails}
triggerAriaLabel={t('misc.contextAgentDetails')}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] Keep the connection state in the accessible name

An explicit aria-label overrides the visible child text, so screen readers now hear only "Agent context details" and lose the previous live state: offline, permission required, thinking, or background tasks. That makes the status conversion into a button an accessibility regression.

Suggested fix:

triggerAriaLabel={`${connectionStatus.text}. ${t('misc.contextAgentDetails')}`}

Add an assertion that the button's accessible name contains both the current state and the action.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Keep capability inventory off the Codex startup path — local mode waits for filesystem discovery before launcher.run(), and remote mode awaits mcpServerStatus/list with a 30-second timeout before continuing initialization. These inventories only feed persisted UI metadata, so slow project/CODEX_HOME I/O or a slow app server can delay the actual coding session. Evidence: cli/src/codex/codexLocalLauncher.ts:492, cli/src/codex/codexRemoteLauncher.ts:3641, cli/src/codex/codexAppServerClient.ts:307.
    Suggested fix:
    // Make the CLI inventory helper asynchronous, then publish all inventories in the background.
    void Promise.all([
        listConfiguredCodexMcpServers(effectiveCodexCwd),
        listSlashCommands('codex', effectiveCodexCwd),
        listSkills(effectiveCodexCwd, { flavor: 'codex' })
    ]).then(([mcpServerInventory, commands, skills]) => {
        publishContextDetails(session.client, buildCodexContextDetails({
            slashCommands: commands.map((command) => command.name),
            skills: skills.map((skill) => ({ name: skill.name, enabled: true })),
            mcpServers,
            mcpServerInventory
        }))
    })
    return await launcher.run()
    
    // Remote status enrichment should also be detached from startup.
    void appServerClient.listMcpServerStatuses()
        .then((response) => {
            const inventory = parseCodexMcpStatusResponse(response)
            if (inventory.length === 0) return
            codexMcpServerInventory = mergeCodexMcpInventories(codexMcpServerInventory, inventory)
            publishCodexInventoryContext?.()
        })
        .catch((error) => {
            logger.debug(`[Codex] mcpServerStatus/list failed: ${errorMessage(error)}`)
        })

Questions

  • None.

Summary

Review mode: follow-up after new commits. The prior MCP completeness, snake_case usage, and accessible-name findings are resolved. One startup-path regression remains: best-effort inventory collection can delay both local and remote Codex sessions.

Testing

Not run (automation; PR code was not executed). GitHub test and fixture drift checks passed. The runner integration job failed on the unchanged runner suite, matching the base commit failure and not attributed to this diff. Add a launcher test with unresolved inventory promises to assert that Codex starts without waiting.

HAPI Bot

Comment thread cli/src/codex/codexRemoteLauncher.ts Outdated
});

try {
const statusInventory = parseCodexMcpStatusResponse(await appServerClient.listMcpServerStatuses());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] Do not await presentation-only inventory during startup

This request is awaited before the launcher reaches its normal session loop, and listMcpServerStatuses() allows 30 seconds before timing out. A slow app server therefore delays every remote Codex session even though this data only enriches the Agent details dialog. Local mode has the same gating pattern at codexLocalLauncher.ts:492, plus a synchronous codex mcp list probe.

Suggested fix:

void appServerClient.listMcpServerStatuses()
    .then((response) => {
        const inventory = parseCodexMcpStatusResponse(response)
        if (inventory.length === 0) return
        codexMcpServerInventory = mergeCodexMcpInventories(codexMcpServerInventory, inventory)
        publishCodexInventoryContext?.()
    })
    .catch((error) => {
        logger.debug(`[Codex] mcpServerStatus/list failed: ${errorMessage(error)}`)
    })

Also make the CLI inventory helper asynchronous and detach the local inventoryTask before calling launcher.run().

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Keep remote slash-command discovery off the startup path — listSlashCommands() recursively reads command files from the user and project trees before the app-server is connected, so slow or network-backed filesystems still delay every remote Codex session even after the follow-up detached MCP discovery. Evidence: cli/src/codex/codexRemoteLauncher.ts:3562, cli/src/modules/common/slashCommands.ts:146, cli/src/codex/codexRemoteLauncher.ts:3637.
    Suggested fix:

    void listSlashCommands('codex', session.path)
        .then((commands) => {
            if (this.shouldExit) return
            availableSlashCommands = commands.map((command) => command.name)
            publishCodexInventoryContext?.()
        })
        .catch((error) => {
            logger.debug(`[Codex] failed to list slash commands: ${errorMessage(error)}`)
        })
  • [Minor] Preserve authoritative empty Claude inventories — the normalizers turn explicit empty arrays into undefined, and the merge logic interprets omitted fields as “keep the previous value.” After the final skill, MCP tool, system tool, or command disappears, archived metadata and the Agent details dialog therefore continue showing stale capabilities. Evidence: cli/src/agent/contextDetails.ts:39, cli/src/agent/contextDetails.ts:93, cli/src/agent/contextDetails.ts:107, cli/src/agent/contextDetails.ts:328.
    Suggested fix:

    // Preserve [] when the source supplied an array; undefined means unavailable.
    return values
    // buildClaudeSkills:
    return skills
    // buildClaudeMcpTools:
    return tools

Questions

  • None.

Summary

Review mode: follow-up after new commits. The new commit detaches local capability discovery and remote MCP status enrichment, but remote slash-command discovery still gates startup. Claude inventory clearing also remains incorrect for explicit empty lists.

Testing

Not run (automation; PR code was not executed). GitHub test and fixture drift-gate checks passed. The unchanged runner integration suite currently fails four assertions, as in the prior bot pass. Add a remote-launcher test with an unresolved slash-command promise, plus a merge test proving empty Claude inventories replace prior non-empty values.

HAPI Bot

Comment thread cli/src/codex/codexRemoteLauncher.ts Outdated
});
this.happyServer = happyServer;
try {
availableSlashCommands = (await listSlashCommands('codex', session.path)).map((command) => command.name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] Keep slash-command discovery off remote startup

This await occurs before appServerClient.connect() and listSlashCommands() recursively reads every command markdown file in the user/project command trees. Slow or network-backed storage can therefore still delay the coding session for presentation-only metadata; the follow-up test only covers the detached MCP status request.

Suggested fix:

void listSlashCommands('codex', session.path)
    .then((commands) => {
        if (this.shouldExit) return
        availableSlashCommands = commands.map((command) => command.name)
        publishCodexInventoryContext?.()
    })
    .catch((error) => {
        logger.debug(`[Codex] failed to list slash commands: ${errorMessage(error)}`)
    })

Comment thread cli/src/agent/contextDetails.ts Outdated
const values = value
.map(asString)
.filter((value): value is string => value !== undefined)
return values.length > 0 ? values : undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] Preserve explicit empty Claude inventories

Returning undefined for an explicit [] makes mergeContextDetails() retain the previous list. The same pattern exists for skills and MCP tools, so removing the final command/tool/skill leaves stale capabilities persisted and displayed.

Suggested fix:

// Explicit [] is authoritative; only non-arrays mean unavailable.
return values
// buildClaudeSkills:
return skills
// buildClaudeMcpTools:
return tools

Add a merge test where non-empty Claude lists are followed by explicit empty arrays.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • None.

Questions

  • None.

Summary

Review mode: follow-up after new commits. No high-confidence issues found on the current head. The previous startup-blocking slash-command discovery and stale-empty-inventory findings are resolved. Residual risk remains around live Codex MCP status payload compatibility and cross-platform subprocess cleanup, which are covered only by mocked/unit paths in this PR.

Testing

Not run (automation; PR code was not executed). GitHub drift-gate passed. The integration job fails the same four unchanged runner assertions recorded in the prior bot review; this PR does not modify runner source or integration tests. The main test workflow was pending at review time.

HAPI Bot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant