feat: enhance task change retrieval with additional options
- Updated the `getTaskChanges` method to accept an optional `options` parameter, allowing for more granular control over the retrieval of task changes. - Introduced new fields in the options object, including `owner`, `status`, `since`, and `intervals`, to improve filtering capabilities. - Refactored related services and IPC methods to accommodate the new options, enhancing the overall task change management experience. - Improved type definitions for better clarity and maintainability.
This commit is contained in:
parent
01660f0791
commit
31c4c7a441
10 changed files with 206 additions and 68 deletions
|
|
@ -146,10 +146,39 @@ async function handleGetAgentChanges(
|
||||||
async function handleGetTaskChanges(
|
async function handleGetTaskChanges(
|
||||||
_event: IpcMainInvokeEvent,
|
_event: IpcMainInvokeEvent,
|
||||||
teamName: string,
|
teamName: string,
|
||||||
taskId: string
|
taskId: string,
|
||||||
|
options?: unknown
|
||||||
): Promise<IpcResult<TaskChangeSetV2>> {
|
): Promise<IpcResult<TaskChangeSetV2>> {
|
||||||
|
const opts =
|
||||||
|
options && typeof options === 'object'
|
||||||
|
? {
|
||||||
|
owner:
|
||||||
|
typeof (options as Record<string, unknown>).owner === 'string'
|
||||||
|
? ((options as Record<string, unknown>).owner as string)
|
||||||
|
: undefined,
|
||||||
|
status:
|
||||||
|
typeof (options as Record<string, unknown>).status === 'string'
|
||||||
|
? ((options as Record<string, unknown>).status as string)
|
||||||
|
: undefined,
|
||||||
|
since:
|
||||||
|
typeof (options as Record<string, unknown>).since === 'string'
|
||||||
|
? ((options as Record<string, unknown>).since as string)
|
||||||
|
: undefined,
|
||||||
|
intervals: Array.isArray((options as Record<string, unknown>).intervals)
|
||||||
|
? (((options as Record<string, unknown>).intervals as unknown[]).filter(
|
||||||
|
(i): i is { startedAt: string; completedAt?: string } =>
|
||||||
|
Boolean(i) &&
|
||||||
|
typeof i === 'object' &&
|
||||||
|
typeof (i as Record<string, unknown>).startedAt === 'string' &&
|
||||||
|
((i as Record<string, unknown>).completedAt === undefined ||
|
||||||
|
typeof (i as Record<string, unknown>).completedAt === 'string')
|
||||||
|
) as { startedAt: string; completedAt?: string }[])
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return wrapReviewHandler('getTaskChanges', () =>
|
return wrapReviewHandler('getTaskChanges', () =>
|
||||||
getChangeExtractor().getTaskChanges(teamName, taskId)
|
getChangeExtractor().getTaskChanges(teamName, taskId, opts)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,11 +105,22 @@ export class ChangeExtractorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Получить изменения для конкретной задачи (Phase 3: per-task scoping) */
|
/** Получить изменения для конкретной задачи (Phase 3: per-task scoping) */
|
||||||
async getTaskChanges(teamName: string, taskId: string): Promise<TaskChangeSetV2> {
|
async getTaskChanges(
|
||||||
|
teamName: string,
|
||||||
|
taskId: string,
|
||||||
|
options?: {
|
||||||
|
owner?: string;
|
||||||
|
status?: string;
|
||||||
|
intervals?: { startedAt: string; completedAt?: string }[];
|
||||||
|
since?: string;
|
||||||
|
}
|
||||||
|
): Promise<TaskChangeSetV2> {
|
||||||
const taskMeta = await this.readTaskMeta(teamName, taskId);
|
const taskMeta = await this.readTaskMeta(teamName, taskId);
|
||||||
const logs = await this.logsFinder.findLogsForTask(teamName, taskId, {
|
const logs = await this.logsFinder.findLogsForTask(teamName, taskId, {
|
||||||
owner: taskMeta?.owner,
|
owner: options?.owner ?? taskMeta?.owner,
|
||||||
status: taskMeta?.status,
|
status: options?.status ?? taskMeta?.status,
|
||||||
|
intervals: options?.intervals ?? taskMeta?.intervals,
|
||||||
|
since: options?.since,
|
||||||
});
|
});
|
||||||
const logRefs = await this.resolveLogFileRefs(teamName, logs);
|
const logRefs = await this.resolveLogFileRefs(teamName, logs);
|
||||||
if (logRefs.length === 0) {
|
if (logRefs.length === 0) {
|
||||||
|
|
@ -173,14 +184,29 @@ export class ChangeExtractorService {
|
||||||
private async readTaskMeta(
|
private async readTaskMeta(
|
||||||
teamName: string,
|
teamName: string,
|
||||||
taskId: string
|
taskId: string
|
||||||
): Promise<{ owner?: string; status?: string } | null> {
|
): Promise<{
|
||||||
|
owner?: string;
|
||||||
|
status?: string;
|
||||||
|
intervals?: { startedAt: string; completedAt?: string }[];
|
||||||
|
} | null> {
|
||||||
try {
|
try {
|
||||||
const taskPath = path.join(getTasksBasePath(), teamName, `${taskId}.json`);
|
const taskPath = path.join(getTasksBasePath(), teamName, `${taskId}.json`);
|
||||||
const raw = await readFile(taskPath, 'utf8');
|
const raw = await readFile(taskPath, 'utf8');
|
||||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||||
|
const intervals = Array.isArray(parsed.workIntervals)
|
||||||
|
? (parsed.workIntervals as unknown[]).filter(
|
||||||
|
(i): i is { startedAt: string; completedAt?: string } =>
|
||||||
|
Boolean(i) &&
|
||||||
|
typeof i === 'object' &&
|
||||||
|
typeof (i as Record<string, unknown>).startedAt === 'string' &&
|
||||||
|
((i as Record<string, unknown>).completedAt === undefined ||
|
||||||
|
typeof (i as Record<string, unknown>).completedAt === 'string')
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
owner: typeof parsed.owner === 'string' ? parsed.owner : undefined,
|
owner: typeof parsed.owner === 'string' ? parsed.owner : undefined,
|
||||||
status: typeof parsed.status === 'string' ? parsed.status : undefined,
|
status: typeof parsed.status === 'string' ? parsed.status : undefined,
|
||||||
|
intervals,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -500,13 +500,21 @@ export class TeamMemberLogsFinder {
|
||||||
try {
|
try {
|
||||||
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
||||||
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
||||||
|
let foundTask = false;
|
||||||
|
let foundTeam = false;
|
||||||
for await (const line of rl) {
|
for await (const line of rl) {
|
||||||
// Require both taskId and teamName to avoid cross-team collisions when multiple
|
// We require BOTH taskId and teamName to avoid cross-team collisions when multiple
|
||||||
// teams share the same projectPath (task IDs are only unique per team).
|
// teams share the same projectPath (task IDs are only unique per team).
|
||||||
const hasTaskId = patterns.some((re) => re.test(line));
|
//
|
||||||
if (!hasTaskId) continue;
|
// But they often appear on different lines (e.g. team_name is in Task tool input, while
|
||||||
const hasTeam = teamPatterns.some((re) => re.test(line));
|
// taskId appears in a tool result or CLI output). So we track them independently.
|
||||||
if (hasTeam) {
|
if (!foundTask && patterns.some((re) => re.test(line))) {
|
||||||
|
foundTask = true;
|
||||||
|
}
|
||||||
|
if (!foundTeam && teamPatterns.some((re) => re.test(line))) {
|
||||||
|
foundTeam = true;
|
||||||
|
}
|
||||||
|
if (foundTask && foundTeam) {
|
||||||
rl.close();
|
rl.close();
|
||||||
stream.destroy();
|
stream.destroy();
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -905,8 +905,22 @@ const electronAPI: ElectronAPI = {
|
||||||
getAgentChanges: async (teamName: string, memberName: string) => {
|
getAgentChanges: async (teamName: string, memberName: string) => {
|
||||||
return invokeIpcWithResult<AgentChangeSet>(REVIEW_GET_AGENT_CHANGES, teamName, memberName);
|
return invokeIpcWithResult<AgentChangeSet>(REVIEW_GET_AGENT_CHANGES, teamName, memberName);
|
||||||
},
|
},
|
||||||
getTaskChanges: async (teamName: string, taskId: string) => {
|
getTaskChanges: async (
|
||||||
return invokeIpcWithResult<TaskChangeSetV2>(REVIEW_GET_TASK_CHANGES, teamName, taskId);
|
teamName: string,
|
||||||
|
taskId: string,
|
||||||
|
options?: {
|
||||||
|
owner?: string;
|
||||||
|
status?: string;
|
||||||
|
intervals?: { startedAt: string; completedAt?: string }[];
|
||||||
|
since?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return invokeIpcWithResult<TaskChangeSetV2>(
|
||||||
|
REVIEW_GET_TASK_CHANGES,
|
||||||
|
teamName,
|
||||||
|
taskId,
|
||||||
|
options
|
||||||
|
);
|
||||||
},
|
},
|
||||||
getChangeStats: async (teamName: string, memberName: string) => {
|
getChangeStats: async (teamName: string, memberName: string) => {
|
||||||
return invokeIpcWithResult<ChangeStats>(REVIEW_GET_CHANGE_STATS, teamName, memberName);
|
return invokeIpcWithResult<ChangeStats>(REVIEW_GET_CHANGE_STATS, teamName, memberName);
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export const NotificationTriggerSettings = ({
|
||||||
const customTriggers = triggers.filter((t) => !t.isBuiltin);
|
const customTriggers = triggers.filter((t) => !t.isBuiltin);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="mt-6 space-y-8">
|
||||||
{/* Builtin Triggers */}
|
{/* Builtin Triggers */}
|
||||||
{builtinTriggers.length > 0 && (
|
{builtinTriggers.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -103,15 +103,6 @@ export const NotificationsSection = ({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Notification Triggers */}
|
|
||||||
<NotificationTriggerSettings
|
|
||||||
triggers={safeConfig.notifications.triggers || []}
|
|
||||||
saving={saving}
|
|
||||||
onUpdateTrigger={onUpdateTrigger}
|
|
||||||
onAddTrigger={onAddTrigger}
|
|
||||||
onRemoveTrigger={onRemoveTrigger}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Notification Settings */}
|
{/* Notification Settings */}
|
||||||
<SettingsSectionHeader title="Notification Settings" />
|
<SettingsSectionHeader title="Notification Settings" />
|
||||||
<SettingRow
|
<SettingRow
|
||||||
|
|
@ -171,40 +162,6 @@ export const NotificationsSection = ({
|
||||||
disabled={saving || !safeConfig.notifications.enabled}
|
disabled={saving || !safeConfig.notifications.enabled}
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingRow
|
|
||||||
label="Task status change notifications"
|
|
||||||
description="Show native OS notifications when a task's status changes"
|
|
||||||
>
|
|
||||||
<SettingsToggle
|
|
||||||
enabled={safeConfig.notifications.notifyOnStatusChange}
|
|
||||||
onChange={(v) => onNotificationToggle('notifyOnStatusChange', v)}
|
|
||||||
disabled={saving || !safeConfig.notifications.enabled}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
{safeConfig.notifications.notifyOnStatusChange && safeConfig.notifications.enabled ? (
|
|
||||||
<>
|
|
||||||
<SettingRow
|
|
||||||
label="Only in Solo mode"
|
|
||||||
description="Only notify when the team has no teammates (lead works alone)"
|
|
||||||
>
|
|
||||||
<SettingsToggle
|
|
||||||
enabled={safeConfig.notifications.statusChangeOnlySolo}
|
|
||||||
onChange={(v) => onNotificationToggle('statusChangeOnlySolo', v)}
|
|
||||||
disabled={saving}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingRow
|
|
||||||
label="Notify on these statuses"
|
|
||||||
description="Select which status transitions trigger notifications"
|
|
||||||
>
|
|
||||||
<StatusCheckboxGroup
|
|
||||||
selected={safeConfig.notifications.statusChangeStatuses}
|
|
||||||
onChange={onStatusChangeStatusesUpdate}
|
|
||||||
disabled={saving}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
<SettingRow
|
<SettingRow
|
||||||
label="Snooze notifications"
|
label="Snooze notifications"
|
||||||
description={
|
description={
|
||||||
|
|
@ -234,6 +191,83 @@ export const NotificationsSection = ({
|
||||||
</div>
|
</div>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
|
||||||
|
{/* Task Status Change Notifications — grouped section */}
|
||||||
|
<div className="border-b py-3" style={{ borderColor: 'var(--color-border-subtle)' }}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
|
||||||
|
Task status change notifications
|
||||||
|
</div>
|
||||||
|
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
|
||||||
|
Show native OS notifications when a task's status changes
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0">
|
||||||
|
<SettingsToggle
|
||||||
|
enabled={safeConfig.notifications.notifyOnStatusChange}
|
||||||
|
onChange={(v) => onNotificationToggle('notifyOnStatusChange', v)}
|
||||||
|
disabled={saving || !safeConfig.notifications.enabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{safeConfig.notifications.notifyOnStatusChange && safeConfig.notifications.enabled ? (
|
||||||
|
<div
|
||||||
|
className="mt-3 flex flex-col gap-3 border-t pt-3"
|
||||||
|
style={{ borderColor: 'var(--color-border-subtle)', paddingLeft: 15 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="text-sm font-medium"
|
||||||
|
style={{ color: 'var(--color-text-secondary)' }}
|
||||||
|
>
|
||||||
|
Only in Solo mode
|
||||||
|
</div>
|
||||||
|
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
|
||||||
|
Notify only when the team has no teammates
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0">
|
||||||
|
<SettingsToggle
|
||||||
|
enabled={safeConfig.notifications.statusChangeOnlySolo}
|
||||||
|
onChange={(v) => onNotificationToggle('statusChangeOnlySolo', v)}
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="text-sm font-medium"
|
||||||
|
style={{ color: 'var(--color-text-secondary)' }}
|
||||||
|
>
|
||||||
|
Notify on these statuses
|
||||||
|
</div>
|
||||||
|
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
|
||||||
|
Which target statuses trigger a notification
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0">
|
||||||
|
<StatusCheckboxGroup
|
||||||
|
selected={safeConfig.notifications.statusChangeStatuses}
|
||||||
|
onChange={onStatusChangeStatusesUpdate}
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom Triggers */}
|
||||||
|
<NotificationTriggerSettings
|
||||||
|
triggers={safeConfig.notifications.triggers || []}
|
||||||
|
saving={saving}
|
||||||
|
onUpdateTrigger={onUpdateTrigger}
|
||||||
|
onAddTrigger={onAddTrigger}
|
||||||
|
onRemoveTrigger={onRemoveTrigger}
|
||||||
|
/>
|
||||||
|
|
||||||
<SettingsSectionHeader title="Ignored Repositories" />
|
<SettingsSectionHeader title="Ignored Repositories" />
|
||||||
<p className="mb-3 text-xs" style={{ color: 'var(--color-text-muted)' }}>
|
<p className="mb-3 text-xs" style={{ color: 'var(--color-text-muted)' }}>
|
||||||
Notifications from these repositories will be ignored
|
Notifications from these repositories will be ignored
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ export const KanbanTaskCard = ({
|
||||||
const checkTaskHasChanges = useStore((s) => s.checkTaskHasChanges);
|
const checkTaskHasChanges = useStore((s) => s.checkTaskHasChanges);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showChangesColumn && task.status === 'completed' && taskHasChanges == null) {
|
if (showChangesColumn && task.status === 'completed' && taskHasChanges !== true) {
|
||||||
void checkTaskHasChanges(teamName, task.id);
|
void checkTaskHasChanges(teamName, task.id);
|
||||||
}
|
}
|
||||||
}, [showChangesColumn, task.status, task.id, teamName, taskHasChanges, checkTaskHasChanges]);
|
}, [showChangesColumn, task.status, task.id, teamName, taskHasChanges, checkTaskHasChanges]);
|
||||||
|
|
|
||||||
|
|
@ -487,7 +487,18 @@ export interface TeamsAPI {
|
||||||
export interface ReviewAPI {
|
export interface ReviewAPI {
|
||||||
// Phase 1
|
// Phase 1
|
||||||
getAgentChanges: (teamName: string, memberName: string) => Promise<AgentChangeSet>;
|
getAgentChanges: (teamName: string, memberName: string) => Promise<AgentChangeSet>;
|
||||||
getTaskChanges: (teamName: string, taskId: string) => Promise<TaskChangeSetV2>;
|
getTaskChanges: (
|
||||||
|
teamName: string,
|
||||||
|
taskId: string,
|
||||||
|
options?: {
|
||||||
|
owner?: string;
|
||||||
|
status?: string;
|
||||||
|
/** Persisted work intervals (preferred for reliable owner-log attribution). */
|
||||||
|
intervals?: { startedAt: string; completedAt?: string }[];
|
||||||
|
/** Back-compat: single since timestamp (deprecated). */
|
||||||
|
since?: string;
|
||||||
|
}
|
||||||
|
) => Promise<TaskChangeSetV2>;
|
||||||
getChangeStats: (teamName: string, memberName: string) => Promise<ChangeStats>;
|
getChangeStats: (teamName: string, memberName: string) => Promise<ChangeStats>;
|
||||||
getFileContent: (
|
getFileContent: (
|
||||||
teamName: string,
|
teamName: string,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ const baseMetrics: SessionMetrics = {
|
||||||
|
|
||||||
function makeChunk(taskIds: string[]): EnhancedAIChunk {
|
function makeChunk(taskIds: string[]): EnhancedAIChunk {
|
||||||
return {
|
return {
|
||||||
type: 'ai',
|
id: 'chunk-1',
|
||||||
|
chunkType: 'ai',
|
||||||
responses: [
|
responses: [
|
||||||
{
|
{
|
||||||
uuid: 'resp-1',
|
uuid: 'resp-1',
|
||||||
|
|
@ -56,6 +57,10 @@ function makeChunk(taskIds: string[]): EnhancedAIChunk {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
processes: [],
|
processes: [],
|
||||||
|
sidechainMessages: [],
|
||||||
|
toolExecutions: [],
|
||||||
|
semanticSteps: [],
|
||||||
|
rawMessages: [],
|
||||||
startTime: new Date('2026-01-01T00:00:00Z'),
|
startTime: new Date('2026-01-01T00:00:00Z'),
|
||||||
endTime: new Date('2026-01-01T00:01:00Z'),
|
endTime: new Date('2026-01-01T00:01:00Z'),
|
||||||
durationMs: 60_000,
|
durationMs: 60_000,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { SubagentResolver } from '../../../../src/main/services/discovery/SubagentResolver';
|
import { SubagentResolver } from '../../../../src/main/services/discovery/SubagentResolver';
|
||||||
|
|
||||||
import type { ParsedMessage, Process } from '../../../../src/main/types';
|
import type { ParsedMessage, Process, ToolCall } from '../../../../src/main/types';
|
||||||
|
import type { ProjectScanner } from '../../../../src/main/services/discovery/ProjectScanner';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
@ -57,12 +58,22 @@ function subagent(overrides: Partial<Process> & { id: string }): Process {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractTaskCalls(messages: ParsedMessage[]): ToolCall[] {
|
||||||
|
const calls: ToolCall[] = [];
|
||||||
|
for (const m of messages) {
|
||||||
|
for (const tc of m.toolCalls) {
|
||||||
|
if (tc.isTask) calls.push(tc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return calls;
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Tests
|
// Tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
describe('SubagentResolver.linkType', () => {
|
describe('SubagentResolver.linkType', () => {
|
||||||
const resolver = new SubagentResolver();
|
const resolver = new SubagentResolver({} as ProjectScanner);
|
||||||
|
|
||||||
// Access private method via prototype for testing
|
// Access private method via prototype for testing
|
||||||
const linkToTaskCalls = (
|
const linkToTaskCalls = (
|
||||||
|
|
@ -97,14 +108,14 @@ describe('SubagentResolver.linkType', () => {
|
||||||
type: 'user',
|
type: 'user',
|
||||||
isMeta: true,
|
isMeta: true,
|
||||||
content: [{ type: 'tool_result', tool_use_id: taskCallId, content: 'done' }],
|
content: [{ type: 'tool_result', tool_use_id: taskCallId, content: 'done' }],
|
||||||
toolResults: [{ toolUseId: taskCallId, content: 'done' }],
|
toolResults: [{ toolUseId: taskCallId, content: 'done', isError: false }],
|
||||||
sourceToolUseID: taskCallId,
|
sourceToolUseID: taskCallId,
|
||||||
toolUseResult: { agentId: subagentId },
|
toolUseResult: { agentId: subagentId },
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const sub = subagent({ id: subagentId });
|
const sub = subagent({ id: subagentId });
|
||||||
linkToTaskCalls([sub], messages);
|
linkToTaskCalls([sub], extractTaskCalls(messages), messages);
|
||||||
|
|
||||||
expect(sub.linkType).toBe('agent-id');
|
expect(sub.linkType).toBe('agent-id');
|
||||||
expect(sub.parentTaskId).toBe(taskCallId);
|
expect(sub.parentTaskId).toBe(taskCallId);
|
||||||
|
|
@ -150,7 +161,7 @@ describe('SubagentResolver.linkType', () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
linkToTaskCalls([sub], messages);
|
linkToTaskCalls([sub], extractTaskCalls(messages), messages);
|
||||||
|
|
||||||
expect(sub.linkType).toBe('team-member-id');
|
expect(sub.linkType).toBe('team-member-id');
|
||||||
expect(sub.parentTaskId).toBe(taskCallId);
|
expect(sub.parentTaskId).toBe(taskCallId);
|
||||||
|
|
@ -194,7 +205,7 @@ describe('SubagentResolver.linkType', () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
linkToTaskCalls([sub], messages);
|
linkToTaskCalls([sub], extractTaskCalls(messages), messages);
|
||||||
|
|
||||||
expect(sub.linkType).toBe('team-member-id');
|
expect(sub.linkType).toBe('team-member-id');
|
||||||
expect(sub.parentTaskId).toBe(taskCallId);
|
expect(sub.parentTaskId).toBe(taskCallId);
|
||||||
|
|
@ -233,7 +244,7 @@ describe('SubagentResolver.linkType', () => {
|
||||||
messages: [msg({ type: 'user', content: 'plain message without teammate tag' })],
|
messages: [msg({ type: 'user', content: 'plain message without teammate tag' })],
|
||||||
});
|
});
|
||||||
|
|
||||||
linkToTaskCalls([sub], messages);
|
linkToTaskCalls([sub], extractTaskCalls(messages), messages);
|
||||||
|
|
||||||
expect(sub.linkType).toBe('unlinked');
|
expect(sub.linkType).toBe('unlinked');
|
||||||
expect(sub.parentTaskId).toBeUndefined();
|
expect(sub.parentTaskId).toBeUndefined();
|
||||||
|
|
@ -288,7 +299,7 @@ describe('SubagentResolver.linkType', () => {
|
||||||
startTime: new Date('2026-01-01T00:00:20Z'),
|
startTime: new Date('2026-01-01T00:00:20Z'),
|
||||||
});
|
});
|
||||||
|
|
||||||
linkToTaskCalls([sub1, sub2], messages);
|
linkToTaskCalls([sub1, sub2], extractTaskCalls(messages), messages);
|
||||||
|
|
||||||
// In the old code, sub1 would get task-1 and sub2 would get task-2 by position.
|
// In the old code, sub1 would get task-1 and sub2 would get task-2 by position.
|
||||||
// Now both should be unlinked.
|
// Now both should be unlinked.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue