mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(codex): preserve native exploration actions (#1139)
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getCodexCommandActions, isCodexExplorationTool } from '@/chat/codexCommandPresentation'
|
||||
import type { ToolCallBlock } from '@/chat/types'
|
||||
|
||||
function block(input: unknown): ToolCallBlock {
|
||||
return {
|
||||
kind: 'tool-call',
|
||||
id: 'tool-1',
|
||||
localId: null,
|
||||
createdAt: 1,
|
||||
invokedAt: null,
|
||||
tool: {
|
||||
id: 'tool-1',
|
||||
name: 'CodexBash',
|
||||
state: 'completed',
|
||||
input,
|
||||
createdAt: 1,
|
||||
startedAt: 1,
|
||||
completedAt: 2,
|
||||
execStartedAt: null,
|
||||
execCompletedAt: null,
|
||||
description: null
|
||||
},
|
||||
children: []
|
||||
}
|
||||
}
|
||||
|
||||
describe('Codex command presentation metadata', () => {
|
||||
it('accepts canonical app-server command actions', () => {
|
||||
const tool = block({
|
||||
command_actions: [
|
||||
{ type: 'read', command: 'cat a.ts', name: 'a.ts', path: '/repo/a.ts' },
|
||||
{ type: 'search', command: 'rg token', query: 'token', path: 'src' }
|
||||
]
|
||||
})
|
||||
|
||||
expect(getCodexCommandActions(tool)).toHaveLength(2)
|
||||
expect(isCodexExplorationTool(tool)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects malformed actions and does not classify unknown commands as exploration', () => {
|
||||
const tool = block({
|
||||
command_actions: [
|
||||
{ type: 'read', command: 'cat' },
|
||||
{ type: 'unknown', command: 'bun test' }
|
||||
]
|
||||
})
|
||||
|
||||
expect(getCodexCommandActions(tool)).toEqual([{ type: 'unknown', command: 'bun test' }])
|
||||
expect(isCodexExplorationTool(tool)).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps user shell commands out of agent exploration groups', () => {
|
||||
const tool = block({
|
||||
command_source: 'userShell',
|
||||
command_actions: [{
|
||||
type: 'read',
|
||||
command: 'cat a.ts',
|
||||
name: 'a.ts',
|
||||
path: '/repo/a.ts'
|
||||
}]
|
||||
})
|
||||
|
||||
expect(getCodexCommandActions(tool)).toHaveLength(1)
|
||||
expect(isCodexExplorationTool(tool)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { ToolCallBlock } from '@/chat/types'
|
||||
|
||||
export type CodexCommandAction =
|
||||
| { type: 'read'; command: string; name: string; path: string }
|
||||
| { type: 'listFiles'; command: string; path: string | null }
|
||||
| { type: 'search'; command: string; query: string | null; path: string | null }
|
||||
| { type: 'unknown'; command: string }
|
||||
|
||||
function asString(value: unknown): string | null {
|
||||
return typeof value === 'string' && value.length > 0 ? value : null
|
||||
}
|
||||
|
||||
function parseAction(value: unknown): CodexCommandAction | null {
|
||||
if (!value || typeof value !== 'object') return null
|
||||
const action = value as Record<string, unknown>
|
||||
const type = asString(action.type)
|
||||
const command = asString(action.command)
|
||||
if (!type || !command) return null
|
||||
|
||||
if (type === 'read') {
|
||||
const name = asString(action.name)
|
||||
const path = asString(action.path)
|
||||
return name && path ? { type, command, name, path } : null
|
||||
}
|
||||
if (type === 'listFiles') {
|
||||
return { type, command, path: asString(action.path) }
|
||||
}
|
||||
if (type === 'search') {
|
||||
return {
|
||||
type,
|
||||
command,
|
||||
query: asString(action.query),
|
||||
path: asString(action.path)
|
||||
}
|
||||
}
|
||||
if (type === 'unknown') {
|
||||
return { type, command }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getCodexCommandActions(block: ToolCallBlock): CodexCommandAction[] {
|
||||
if (block.tool.name !== 'CodexBash' || !block.tool.input || typeof block.tool.input !== 'object') {
|
||||
return []
|
||||
}
|
||||
const input = block.tool.input as Record<string, unknown>
|
||||
const raw = input.command_actions ?? input.commandActions
|
||||
if (!Array.isArray(raw)) return []
|
||||
return raw.map(parseAction).filter((action): action is CodexCommandAction => action !== null)
|
||||
}
|
||||
|
||||
export function isCodexExplorationTool(block: ToolCallBlock): boolean {
|
||||
const input = block.tool.input && typeof block.tool.input === 'object'
|
||||
? block.tool.input as Record<string, unknown>
|
||||
: null
|
||||
const source = asString(input?.command_source ?? input?.commandSource)
|
||||
if (source?.toLowerCase() === 'usershell') return false
|
||||
|
||||
const actions = getCodexCommandActions(block)
|
||||
return actions.length > 0 && actions.every((action) => (
|
||||
action.type === 'read' || action.type === 'listFiles' || action.type === 'search'
|
||||
))
|
||||
}
|
||||
@@ -169,6 +169,70 @@ describe('Codex activity headings', () => {
|
||||
})
|
||||
|
||||
describe('buildVisibleChatBlocks', () => {
|
||||
it('renders one or more structured Codex exploration commands as an open exploration group', () => {
|
||||
const read = makeToolBlock('codex-read', 'CodexBash', {
|
||||
command: 'cat package.json',
|
||||
command_source: 'agent',
|
||||
command_actions: [{
|
||||
type: 'read',
|
||||
command: 'cat package.json',
|
||||
name: 'package.json',
|
||||
path: '/repo/package.json'
|
||||
}]
|
||||
})
|
||||
const search = makeToolBlock('codex-search', 'CodexBash', {
|
||||
command: 'rg nativeTitle web/src',
|
||||
command_source: 'agent',
|
||||
command_actions: [{
|
||||
type: 'search',
|
||||
command: 'rg nativeTitle web/src',
|
||||
query: 'nativeTitle',
|
||||
path: 'web/src'
|
||||
}]
|
||||
})
|
||||
|
||||
const visible = buildVisibleChatBlocks([read, search], { hasMoreMessages: false })
|
||||
|
||||
expect(visible).toHaveLength(1)
|
||||
expect(isToolGroupBlock(visible[0])).toBe(true)
|
||||
if (!isToolGroupBlock(visible[0])) throw new Error('expected exploration group')
|
||||
expect(visible[0].presentationMode).toBe('codex-exploration')
|
||||
expect(visible[0].defaultOpen).toBe(true)
|
||||
expect(visible[0].tools.map((tool) => tool.id)).toEqual(['codex-read', 'codex-search'])
|
||||
})
|
||||
|
||||
it('keeps structured general Codex commands separate from exploration groups', () => {
|
||||
const read = makeToolBlock('codex-read', 'CodexBash', {
|
||||
command: 'cat package.json',
|
||||
command_actions: [{
|
||||
type: 'read',
|
||||
command: 'cat package.json',
|
||||
name: 'package.json',
|
||||
path: '/repo/package.json'
|
||||
}]
|
||||
})
|
||||
const test = makeToolBlock('codex-test', 'CodexBash', {
|
||||
command: 'bun test',
|
||||
command_actions: [{ type: 'unknown', command: 'bun test' }]
|
||||
})
|
||||
const nextRead = makeToolBlock('codex-read-2', 'CodexBash', {
|
||||
command: 'cat README.md',
|
||||
command_actions: [{
|
||||
type: 'read',
|
||||
command: 'cat README.md',
|
||||
name: 'README.md',
|
||||
path: '/repo/README.md'
|
||||
}]
|
||||
})
|
||||
|
||||
const visible = buildVisibleChatBlocks([read, test, nextRead], { hasMoreMessages: false })
|
||||
|
||||
expect(visible).toHaveLength(3)
|
||||
expect(isToolGroupBlock(visible[0]) && visible[0].presentationMode).toBe('codex-exploration')
|
||||
expect(visible[1]).toBe(test)
|
||||
expect(isToolGroupBlock(visible[2]) && visible[2].presentationMode).toBe('codex-exploration')
|
||||
})
|
||||
|
||||
it('groups contiguous eligible root tool cards', () => {
|
||||
const visible = buildVisibleChatBlocks([
|
||||
makeToolBlock('read-1', 'Read', { file_path: 'src/a.ts' }),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ChatBlock, ToolCallBlock } from '@/chat/types'
|
||||
import { getCodexCommandActions, isCodexExplorationTool } from '@/chat/codexCommandPresentation'
|
||||
import { isSubagentToolName } from '@/chat/subagentTool'
|
||||
import { isAskUserQuestionToolName } from '@/components/ToolCard/askUserQuestion'
|
||||
import { isRequestUserInputToolName } from '@/components/ToolCard/requestUserInput'
|
||||
@@ -31,6 +32,7 @@ export type ToolGroupBlock = {
|
||||
historyState: 'complete' | 'needs-older-history'
|
||||
needsOlderHistory: boolean
|
||||
activityTitle?: string | null
|
||||
presentationMode?: 'default' | 'codex-exploration'
|
||||
summary: ToolGroupSummary
|
||||
}
|
||||
|
||||
@@ -206,9 +208,17 @@ export function isEligibleForToolGrouping(block: ToolCallBlock): boolean {
|
||||
if (PLAN_TOOL_NAMES.has(block.tool.name)) return false
|
||||
if (MILESTONE_TOOL_NAMES.has(block.tool.name)) return false
|
||||
if (isInteractiveToolBlock(block)) return false
|
||||
if (block.tool.name === 'CodexBash' && getCodexCommandActions(block).length > 0) {
|
||||
return isCodexExplorationTool(block)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function getGroupingFamily(block: ToolCallBlock): 'default' | 'codex-exploration' | null {
|
||||
if (!isEligibleForToolGrouping(block)) return null
|
||||
return isCodexExplorationTool(block) ? 'codex-exploration' : 'default'
|
||||
}
|
||||
|
||||
function createToolGroupId(
|
||||
tools: ToolCallBlock[],
|
||||
needsOlderHistory: boolean,
|
||||
@@ -240,7 +250,12 @@ export function buildVisibleChatBlocks(
|
||||
|
||||
for (let index = 0; index < blocks.length; index += 1) {
|
||||
const block = blocks[index]
|
||||
if (block.kind !== 'tool-call' || !isEligibleForToolGrouping(block)) {
|
||||
if (block.kind !== 'tool-call') {
|
||||
visibleBlocks.push(block)
|
||||
continue
|
||||
}
|
||||
const groupingFamily = getGroupingFamily(block)
|
||||
if (!groupingFamily) {
|
||||
visibleBlocks.push(block)
|
||||
continue
|
||||
}
|
||||
@@ -249,14 +264,14 @@ export function buildVisibleChatBlocks(
|
||||
let cursor = index + 1
|
||||
while (cursor < blocks.length) {
|
||||
const candidate = blocks[cursor]
|
||||
if (candidate.kind !== 'tool-call' || !isEligibleForToolGrouping(candidate)) {
|
||||
if (candidate.kind !== 'tool-call' || getGroupingFamily(candidate) !== groupingFamily) {
|
||||
break
|
||||
}
|
||||
tools.push(candidate)
|
||||
cursor += 1
|
||||
}
|
||||
|
||||
if (tools.length < 2) {
|
||||
if (tools.length < 2 && groupingFamily !== 'codex-exploration') {
|
||||
visibleBlocks.push(block)
|
||||
continue
|
||||
}
|
||||
@@ -276,10 +291,11 @@ export function buildVisibleChatBlocks(
|
||||
firstToolId: tools[0].id,
|
||||
lastToolId: tools[tools.length - 1].id,
|
||||
tools,
|
||||
defaultOpen: false,
|
||||
defaultOpen: groupingFamily === 'codex-exploration',
|
||||
historyState: needsOlderHistory ? 'needs-older-history' : 'complete',
|
||||
needsOlderHistory,
|
||||
activityTitle,
|
||||
presentationMode: groupingFamily,
|
||||
summary: summarizeToolGroup(tools)
|
||||
})
|
||||
index = cursor - 1
|
||||
|
||||
@@ -138,6 +138,53 @@ describe('ToolGroupCard', () => {
|
||||
expect(within(dialog).getAllByText('Result').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('shows structured Codex exploration actions by default without a generic action count', () => {
|
||||
const tools = [
|
||||
makeToolBlock('codex-read', 'CodexBash', {
|
||||
command: 'cat package.json',
|
||||
command_actions: [{
|
||||
type: 'read',
|
||||
command: 'cat package.json',
|
||||
name: 'package.json',
|
||||
path: '/repo/package.json'
|
||||
}]
|
||||
}),
|
||||
makeToolBlock('codex-search', 'CodexBash', {
|
||||
command: 'rg nativeTitle web/src',
|
||||
command_actions: [{
|
||||
type: 'search',
|
||||
command: 'rg nativeTitle web/src',
|
||||
query: 'nativeTitle',
|
||||
path: 'web/src'
|
||||
}]
|
||||
})
|
||||
]
|
||||
const view = renderCard(makeGroup({
|
||||
tools,
|
||||
defaultOpen: true,
|
||||
presentationMode: 'codex-exploration',
|
||||
summary: {
|
||||
totalTools: 2,
|
||||
countsByKind: { read: 0, search: 0, command: 2, mutation: 0, web: 0, other: 0 },
|
||||
fileTargets: [],
|
||||
commandTargets: ['cat package.json', 'rg nativeTitle web/src'],
|
||||
searchTargets: [],
|
||||
urlTargets: [],
|
||||
otherTargets: [],
|
||||
errorCount: 0,
|
||||
runningCount: 0,
|
||||
pendingCount: 0,
|
||||
}
|
||||
}))
|
||||
|
||||
expect(within(view.container).getByRole('button', { name: /^explored$/i })).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(screen.getByText('Read')).toBeInTheDocument()
|
||||
expect(screen.getByText('package.json')).toBeInTheDocument()
|
||||
expect(screen.getByText('Search')).toBeInTheDocument()
|
||||
expect(screen.getByText('nativeTitle in web/src')).toBeInTheDocument()
|
||||
expect(screen.queryByText('2 actions')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('uses a neutral header for all-generic tool groups without duplicate counters', () => {
|
||||
const tools = Array.from({ length: 25 }, (_, index) => makeToolBlock(`tool-${index + 1}`, 'Tool', { name: `Tool ${index + 1}` }))
|
||||
const view = renderCard(makeGroup({
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { ToolGroupBlock } from '@/chat/toolGroups'
|
||||
import type { ToolCallBlock } from '@/chat/types'
|
||||
import { getCodexCommandActions, type CodexCommandAction } from '@/chat/codexCommandPresentation'
|
||||
import type { SessionMetadataSummary } from '@/types/api'
|
||||
import { useHappyChatContext } from '@/components/AssistantChat/context'
|
||||
import { ToolDetailDialogContent, ToolStatusIcon, toolStatusColorClass } from '@/components/ToolCard/ToolCard'
|
||||
import { getToolPresentation } from '@/components/ToolCard/knownTools'
|
||||
import { formatGroupedHeaderSubtitle, formatGroupedHeaderTitle } from '@/components/ToolCard/groupedPresentation'
|
||||
import { formatGroupedHeaderSubtitle, formatGroupedHeaderTitle, safeGroupedLabelValue } from '@/components/ToolCard/groupedPresentation'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -99,6 +100,64 @@ function RowLabel(props: { block: ToolCallBlock; metadata: SessionMetadataSummar
|
||||
)
|
||||
}
|
||||
|
||||
function basename(value: string): string {
|
||||
return value.replace(/\\/g, '/').split('/').filter(Boolean).at(-1) ?? value
|
||||
}
|
||||
|
||||
function codexActionLabel(
|
||||
action: CodexCommandAction,
|
||||
t: (key: string, params?: Record<string, string | number>) => string
|
||||
): { title: string; detail: string | null } {
|
||||
if (action.type === 'read') {
|
||||
const detail = safeGroupedLabelValue(action.name) ?? safeGroupedLabelValue(action.path)
|
||||
return { title: t('toolGroup.codex.read'), detail: detail ? basename(detail) : null }
|
||||
}
|
||||
if (action.type === 'listFiles') {
|
||||
return { title: t('toolGroup.codex.list'), detail: safeGroupedLabelValue(action.path) }
|
||||
}
|
||||
if (action.type === 'search') {
|
||||
const query = safeGroupedLabelValue(action.query)
|
||||
const path = safeGroupedLabelValue(action.path)
|
||||
return {
|
||||
title: t('toolGroup.codex.search'),
|
||||
detail: query && path
|
||||
? t('toolGroup.codex.searchIn', { query, path })
|
||||
: query ?? path
|
||||
}
|
||||
}
|
||||
return { title: t('toolGroup.friendly.genericCommand'), detail: null }
|
||||
}
|
||||
|
||||
function CodexExplorationRows(props: {
|
||||
tools: ToolCallBlock[]
|
||||
onSelect: (toolId: string) => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
return props.tools.flatMap((tool) => (
|
||||
getCodexCommandActions(tool).map((action, index) => {
|
||||
const label = codexActionLabel(action, t)
|
||||
return (
|
||||
<button
|
||||
key={`${tool.id}:${index}`}
|
||||
type="button"
|
||||
className="flex min-w-0 items-start gap-2 rounded-lg px-2 py-1 text-left hover:bg-[var(--app-subtle-bg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--app-link)]"
|
||||
onClick={() => props.onSelect(tool.id)}
|
||||
>
|
||||
<span className="mt-1 text-xs text-[var(--app-hint)]">└</span>
|
||||
<span className="shrink-0 text-sm font-medium text-[var(--app-tool-card-accent)]">
|
||||
{label.title}
|
||||
</span>
|
||||
{label.detail ? (
|
||||
<span className="min-w-0 truncate text-sm text-[var(--app-fg)]">
|
||||
{label.detail}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
export function ToolGroupCard(props: {
|
||||
block: ToolGroupBlock
|
||||
metadata: SessionMetadataSummary | null
|
||||
@@ -219,7 +278,9 @@ export function ToolGroupCard(props: {
|
||||
}, [selectedTool, props.metadata, t])
|
||||
|
||||
const primaryTitle = formatGroupedHeaderTitle(props.block, t)
|
||||
const subtitle = formatGroupedHeaderSubtitle(props.block, t) ?? formatActionSummary(props.block, t)
|
||||
const subtitle = props.block.presentationMode === 'codex-exploration'
|
||||
? null
|
||||
: formatGroupedHeaderSubtitle(props.block, t) ?? formatActionSummary(props.block, t)
|
||||
const fileCount = props.block.summary.fileTargets.length
|
||||
|
||||
return (
|
||||
@@ -249,10 +310,12 @@ export function ToolGroupCard(props: {
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2 self-center text-[var(--app-hint)]">
|
||||
<SummaryBadge
|
||||
className="bg-[var(--app-subtle-bg)] text-[var(--app-hint)]"
|
||||
text={t('toolGroup.toolCount', { n: props.block.tools.length })}
|
||||
/>
|
||||
{props.block.presentationMode !== 'codex-exploration' ? (
|
||||
<SummaryBadge
|
||||
className="bg-[var(--app-subtle-bg)] text-[var(--app-hint)]"
|
||||
text={t('toolGroup.toolCount', { n: props.block.tools.length })}
|
||||
/>
|
||||
) : null}
|
||||
{props.block.summary.runningCount > 0 ? (
|
||||
<SummaryBadge
|
||||
className="bg-sky-500/10 text-sky-600"
|
||||
@@ -285,7 +348,9 @@ export function ToolGroupCard(props: {
|
||||
{open ? (
|
||||
<CardContent className="px-3 pb-3 pt-1">
|
||||
<div className="flex flex-col gap-2">
|
||||
{props.block.tools.map((tool) => {
|
||||
{props.block.presentationMode === 'codex-exploration' ? (
|
||||
<CodexExplorationRows tools={props.block.tools} onSelect={setSelectedToolId} />
|
||||
) : props.block.tools.map((tool) => {
|
||||
return (
|
||||
<button
|
||||
key={tool.id}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ToolGroupBlock } from '@/chat/toolGroups'
|
||||
import type { ToolCallBlock } from '@/chat/types'
|
||||
import { isCodexExplorationTool } from '@/chat/codexCommandPresentation'
|
||||
import { getInputStringAny } from '@/lib/toolInputUtils'
|
||||
|
||||
type Translator = (key: string, params?: Record<string, string | number>) => string
|
||||
@@ -30,7 +31,7 @@ function truncateLabel(value: string): string {
|
||||
: normalized
|
||||
}
|
||||
|
||||
function safeLabelValue(value: string | null): string | null {
|
||||
export function safeGroupedLabelValue(value: string | null): string | null {
|
||||
if (!value || SENSITIVE_TEXT_RE.test(value)) return null
|
||||
return truncateLabel(value)
|
||||
}
|
||||
@@ -50,7 +51,7 @@ function getInspectionCommandTarget(command: string): string | null {
|
||||
if (!parts || parts.length < 2) return null
|
||||
const target = [...parts].reverse().find((part) => !part.startsWith('-') && !/^['"]?\d+(?:,\d+)?p['"]?$/.test(part))
|
||||
if (!target || target === parts[0]) return null
|
||||
return safeLabelValue(target.replace(/^['"]|['"]$/g, ''))
|
||||
return safeGroupedLabelValue(target.replace(/^['"]|['"]$/g, ''))
|
||||
}
|
||||
|
||||
function getSearchCommandPattern(command: string): string | null {
|
||||
@@ -60,7 +61,7 @@ function getSearchCommandPattern(command: string): string | null {
|
||||
if (executableIndex < 0) return null
|
||||
for (let index = executableIndex + 1; index < parts.length; index += 1) {
|
||||
const part = parts[index]
|
||||
if (!part.startsWith('-')) return safeLabelValue(part.replace(/^['"]|['"]$/g, ''))
|
||||
if (!part.startsWith('-')) return safeGroupedLabelValue(part.replace(/^['"]|['"]$/g, ''))
|
||||
if (SEARCH_OPTIONS_WITH_VALUE.has(part)) index += 1
|
||||
}
|
||||
return null
|
||||
@@ -156,13 +157,13 @@ function getPrimaryIntent(block: ToolGroupBlock): GroupedSummaryIntent {
|
||||
function formatSpecificIntentTitle(block: ToolGroupBlock, intent: GroupedSummaryIntent, t: Translator): string | null {
|
||||
const matching = block.tools.filter((tool) => inferGroupedSummaryIntent(tool) === intent)
|
||||
const described = matching
|
||||
.map((tool) => safeLabelValue(tool.tool.description))
|
||||
.map((tool) => safeGroupedLabelValue(tool.tool.description))
|
||||
.find((value): value is string => value !== null)
|
||||
if (described) return described
|
||||
|
||||
if (intent === 'inspect-files' || intent === 'modify-files') {
|
||||
for (const tool of matching) {
|
||||
const target = safeLabelValue(getInputStringAny(tool.tool.input, ['file_path', 'path', 'file', 'filePath', 'notebook_path']))
|
||||
const target = safeGroupedLabelValue(getInputStringAny(tool.tool.input, ['file_path', 'path', 'file', 'filePath', 'notebook_path']))
|
||||
if (target) {
|
||||
return t(intent === 'modify-files' ? 'toolGroup.friendly.editTarget' : 'toolGroup.friendly.inspectTarget', {
|
||||
target: basename(target)
|
||||
@@ -178,7 +179,7 @@ function formatSpecificIntentTitle(block: ToolGroupBlock, intent: GroupedSummary
|
||||
|
||||
if (intent === 'search-content') {
|
||||
for (const tool of matching) {
|
||||
const pattern = safeLabelValue(getInputStringAny(tool.tool.input, ['pattern', 'query']))
|
||||
const pattern = safeGroupedLabelValue(getInputStringAny(tool.tool.input, ['pattern', 'query']))
|
||||
if (pattern) return t('toolGroup.friendly.searchTarget', { target: pattern })
|
||||
const command = getCommandText(tool.tool.input)
|
||||
const commandPattern = command ? getSearchCommandPattern(command) : null
|
||||
@@ -188,7 +189,7 @@ function formatSpecificIntentTitle(block: ToolGroupBlock, intent: GroupedSummary
|
||||
|
||||
if (intent === 'run-project-command') {
|
||||
for (const tool of matching) {
|
||||
const command = safeLabelValue(getCommandText(tool.tool.input))
|
||||
const command = safeGroupedLabelValue(getCommandText(tool.tool.input))
|
||||
if (command && SAFE_PROJECT_COMMAND_RE.test(command)) {
|
||||
return t('toolGroup.friendly.runTarget', { target: command })
|
||||
}
|
||||
@@ -199,7 +200,12 @@ function formatSpecificIntentTitle(block: ToolGroupBlock, intent: GroupedSummary
|
||||
}
|
||||
|
||||
export function formatGroupedHeaderTitle(block: ToolGroupBlock, t: Translator): string {
|
||||
const activityTitle = safeLabelValue(block.activityTitle ?? null)
|
||||
if (block.presentationMode === 'codex-exploration') {
|
||||
return block.tools.some((tool) => tool.tool.state === 'running' || tool.tool.state === 'pending')
|
||||
? t('toolGroup.codex.exploring')
|
||||
: t('toolGroup.codex.explored')
|
||||
}
|
||||
const activityTitle = safeGroupedLabelValue(block.activityTitle ?? null)
|
||||
if (activityTitle) return activityTitle
|
||||
const primaryIntent = getPrimaryIntent(block)
|
||||
const specificTitle = formatSpecificIntentTitle(block, primaryIntent, t)
|
||||
@@ -211,6 +217,7 @@ export function formatGroupedHeaderTitle(block: ToolGroupBlock, t: Translator):
|
||||
}
|
||||
|
||||
export function formatGroupedHeaderSubtitle(block: ToolGroupBlock, t: Translator): string | null {
|
||||
if (block.presentationMode === 'codex-exploration') return null
|
||||
const parts: string[] = []
|
||||
|
||||
if (block.summary.countsByKind.command > 0) {
|
||||
@@ -236,5 +243,6 @@ export function formatGroupedHeaderSubtitle(block: ToolGroupBlock, t: Translator
|
||||
}
|
||||
|
||||
export function formatGroupedRowLabel(tool: ToolCallBlock, t: Translator): string {
|
||||
if (isCodexExplorationTool(tool)) return t('toolGroup.codex.explored')
|
||||
return getIntentLabel(inferGroupedSummaryIntent(tool), t)
|
||||
}
|
||||
|
||||
@@ -474,6 +474,12 @@ export default {
|
||||
'toolGroup.rowStatus.running': 'Running',
|
||||
'toolGroup.rowStatus.pending': 'Pending',
|
||||
'toolGroup.rowStatus.error': 'Error',
|
||||
'toolGroup.codex.exploring': 'Exploring',
|
||||
'toolGroup.codex.explored': 'Explored',
|
||||
'toolGroup.codex.read': 'Read',
|
||||
'toolGroup.codex.list': 'List',
|
||||
'toolGroup.codex.search': 'Search',
|
||||
'toolGroup.codex.searchIn': '{query} in {path}',
|
||||
|
||||
// Composer buttons
|
||||
'composer.settings': 'Settings',
|
||||
|
||||
@@ -478,6 +478,12 @@ export default {
|
||||
'toolGroup.rowStatus.running': '运行中',
|
||||
'toolGroup.rowStatus.pending': '等待中',
|
||||
'toolGroup.rowStatus.error': '错误',
|
||||
'toolGroup.codex.exploring': '正在探索',
|
||||
'toolGroup.codex.explored': '已探索',
|
||||
'toolGroup.codex.read': '读取',
|
||||
'toolGroup.codex.list': '列出',
|
||||
'toolGroup.codex.search': '搜索',
|
||||
'toolGroup.codex.searchIn': '在 {path} 中搜索 {query}',
|
||||
|
||||
// Composer buttons
|
||||
'composer.settings': '设置',
|
||||
|
||||
Reference in New Issue
Block a user