feat: Add global relationships management to GlobalSymbolIndex

- Introduced a new schema version (v2) with a global_relationships table.
- Implemented CRUD operations for file relationships, including update and delete functionalities.
- Added query capabilities for relationships by target and symbols.
- Created migration logic from v1 to v2 schema.
- Enhanced tests for global relationships, covering various scenarios including insertion, querying, and deletion.

docs: Add update-single command for generating module documentation

- Created a new command to generate manual-style documentation (CLAUDE.md) for a single module.
- Detailed execution process and implementation phases for the command.
- Included usage examples and error handling guidelines.

feat: Implement team command for CLI interface

- Added a new team command for logging and retrieving messages in a team message bus.
- Supported subcommands for logging, reading, listing, and checking status of messages.
- Included error handling and JSON output options.

test: Add comprehensive tests for global relationships

- Developed extensive tests for the global_relationships table in GlobalSymbolIndex.
- Covered schema creation, migration, CRUD operations, and performance benchmarks.
- Ensured project isolation and validated query functionalities for relationships.
This commit is contained in:
catlog22
2026-02-13 11:39:53 +08:00
parent e88d552cd1
commit 17f52da4c6
21 changed files with 1587 additions and 127 deletions

View File

@@ -142,12 +142,12 @@ export function ObservabilityPanel() {
<label className="block text-xs font-medium text-muted-foreground mb-1">
{formatMessage({ id: 'issues.observability.filters.type' })}
</label>
<Select value={type} onValueChange={(v) => setType(v)}>
<Select value={type || '__all__'} onValueChange={(v) => setType(v === '__all__' ? '' : v)}>
<SelectTrigger>
<SelectValue placeholder={formatMessage({ id: 'issues.observability.filters.typeAll' })} />
</SelectTrigger>
<SelectContent>
<SelectItem value="">{formatMessage({ id: 'issues.observability.filters.typeAll' })}</SelectItem>
<SelectItem value="__all__">{formatMessage({ id: 'issues.observability.filters.typeAll' })}</SelectItem>
{EVENT_TYPES.map((t) => (
<SelectItem key={t} value={t}>
{t}

View File

@@ -209,7 +209,7 @@ export function QueuePanel() {
</SelectTrigger>
<SelectContent>
{(historyIndex.queues || []).length === 0 ? (
<SelectItem value="" disabled>
<SelectItem value="__none__" disabled>
{formatMessage({ id: 'issues.queue.history.empty' })}
</SelectItem>
) : (

View File

@@ -215,7 +215,7 @@ export function QueueExecuteInSession({ item, className }: { item: QueueItem; cl
</SelectTrigger>
<SelectContent>
{sessions.length === 0 ? (
<SelectItem value="" disabled>
<SelectItem value="__none__" disabled>
{formatMessage({ id: 'issues.terminal.session.none' })}
</SelectItem>
) : (

View File

@@ -267,7 +267,7 @@ export function QueueSendToOrchestrator({ item, className }: { item: QueueItem;
</SelectTrigger>
<SelectContent>
{sessions.length === 0 ? (
<SelectItem value="" disabled>
<SelectItem value="__none__" disabled>
{formatMessage({ id: 'issues.terminal.session.none' })}
</SelectItem>
) : (

View File

@@ -248,7 +248,7 @@ function ContextContent({
<div className="space-y-0.5 pl-2 max-h-32 overflow-y-auto">
{ctx.relevant_files.map((f, i) => {
const filePath = typeof f === 'string' ? f : f.path;
const reason = typeof f === 'string' ? undefined : (f.rationale || f.reason);
const reason = typeof f === 'string' ? undefined : f.reason;
return (
<div key={i} className="group flex items-start gap-1 text-muted-foreground hover:bg-muted/30 rounded px-1 py-0.5">
<span className="text-primary/50 shrink-0">{i + 1}.</span>

View File

@@ -6,7 +6,7 @@
import { useState } from 'react';
import { useIntl } from 'react-intl';
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import {
Server,
Plus,
@@ -226,6 +226,7 @@ export function McpManagerPage() {
const [saveTemplateDialogOpen, setSaveTemplateDialogOpen] = useState(false);
const [serverToSaveAsTemplate, setServerToSaveAsTemplate] = useState<McpServer | undefined>(undefined);
const queryClient = useQueryClient();
const notifications = useNotifications();
const {
@@ -352,15 +353,47 @@ export function McpManagerPage() {
};
const handleToggleCcwTool = async (tool: string, enabled: boolean) => {
// Read latest from cache to avoid stale closures
const currentConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfig']) ?? ccwConfig;
const currentTools = currentConfig.enabledTools;
const previousConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfig']);
const updatedTools = enabled
? [...ccwConfig.enabledTools, tool]
: ccwConfig.enabledTools.filter((t) => t !== tool);
await updateCcwConfig({ enabledTools: updatedTools });
? (currentTools.includes(tool) ? currentTools : [...currentTools, tool])
: currentTools.filter((t) => t !== tool);
// Optimistic cache update for immediate UI response
queryClient.setQueryData(['ccwMcpConfig'], (old: CcwMcpConfig | undefined) => {
if (!old) return old;
return { ...old, enabledTools: updatedTools };
});
try {
await updateCcwConfig({ ...currentConfig, enabledTools: updatedTools });
} catch (error) {
console.error('Failed to toggle CCW tool:', error);
queryClient.setQueryData(['ccwMcpConfig'], previousConfig);
}
ccwMcpQuery.refetch();
};
const handleUpdateCcwConfig = async (config: Partial<CcwMcpConfig>) => {
await updateCcwConfig(config);
// Read BEFORE optimistic update to capture actual server state
const currentConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfig']) ?? ccwConfig;
const previousConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfig']);
// Optimistic cache update for immediate UI response
queryClient.setQueryData(['ccwMcpConfig'], (old: CcwMcpConfig | undefined) => {
if (!old) return old;
return { ...old, ...config };
});
try {
await updateCcwConfig({ ...currentConfig, ...config });
} catch (error) {
console.error('Failed to update CCW config:', error);
queryClient.setQueryData(['ccwMcpConfig'], previousConfig);
}
ccwMcpQuery.refetch();
};
@@ -378,15 +411,48 @@ export function McpManagerPage() {
};
const handleToggleCcwToolCodex = async (tool: string, enabled: boolean) => {
const currentConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfigCodex']) ?? ccwCodexConfig;
const currentTools = currentConfig.enabledTools;
const updatedTools = enabled
? [...ccwCodexConfig.enabledTools, tool]
: ccwCodexConfig.enabledTools.filter((t) => t !== tool);
await updateCcwConfigForCodex({ enabledTools: updatedTools });
? [...currentTools, tool]
: currentTools.filter((t) => t !== tool);
queryClient.setQueryData(['ccwMcpConfigCodex'], (old: CcwMcpConfig | undefined) => {
if (!old) return old;
return { ...old, enabledTools: updatedTools };
});
try {
await updateCcwConfigForCodex({
enabledTools: updatedTools,
projectRoot: currentConfig.projectRoot,
allowedDirs: currentConfig.allowedDirs,
disableSandbox: currentConfig.disableSandbox,
});
} catch (error) {
console.error('Failed to toggle CCW tool (Codex):', error);
}
ccwMcpCodexQuery.refetch();
};
const handleUpdateCcwConfigCodex = async (config: Partial<CcwMcpConfig>) => {
await updateCcwConfigForCodex(config);
queryClient.setQueryData(['ccwMcpConfigCodex'], (old: CcwMcpConfig | undefined) => {
if (!old) return old;
return { ...old, ...config };
});
try {
const currentConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfigCodex']) ?? ccwCodexConfig;
await updateCcwConfigForCodex({
enabledTools: config.enabledTools ?? currentConfig.enabledTools,
projectRoot: config.projectRoot ?? currentConfig.projectRoot,
allowedDirs: config.allowedDirs ?? currentConfig.allowedDirs,
disableSandbox: config.disableSandbox ?? currentConfig.disableSandbox,
});
} catch (error) {
console.error('Failed to update CCW config (Codex):', error);
}
ccwMcpCodexQuery.refetch();
};