fix(ci): apply frontend validation fixes
This commit is contained in:
parent
30a6e36976
commit
9d419626ef
42 changed files with 621 additions and 170 deletions
|
|
@ -25,11 +25,11 @@ import {
|
||||||
} from '@shared/utils/idleNotificationSemantics';
|
} from '@shared/utils/idleNotificationSemantics';
|
||||||
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
|
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
|
||||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
|
import { buildOrderedVisibleTeamGraphOwnerIds } from '@shared/utils/teamGraphDefaultLayout';
|
||||||
import {
|
import {
|
||||||
isTeamTaskActivelyWorked,
|
isTeamTaskActivelyWorked,
|
||||||
isTeamTaskNeedsFixActionable,
|
isTeamTaskNeedsFixActionable,
|
||||||
} from '@shared/utils/teamTaskState';
|
} from '@shared/utils/teamTaskState';
|
||||||
import { buildOrderedVisibleTeamGraphOwnerIds } from '@shared/utils/teamGraphDefaultLayout';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildInlineActivityEntries,
|
buildInlineActivityEntries,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import type {
|
||||||
const LOG_PREVIEW_FALLBACK_WIDTH = 260;
|
const LOG_PREVIEW_FALLBACK_WIDTH = 260;
|
||||||
const LOG_PREVIEW_FALLBACK_HEIGHT = 292;
|
const LOG_PREVIEW_FALLBACK_HEIGHT = 292;
|
||||||
const NEW_LOG_HIGHLIGHT_MS = 1_000;
|
const NEW_LOG_HIGHLIGHT_MS = 1_000;
|
||||||
|
const COMPACT_ROW_TEXT_LIMIT = 118;
|
||||||
|
const COMPACT_ROW_MIN_PREVIEW_LIMIT = 48;
|
||||||
|
|
||||||
interface StableRectLike {
|
interface StableRectLike {
|
||||||
left: number;
|
left: number;
|
||||||
|
|
@ -155,6 +157,18 @@ function compactPreviewText(item: MemberLogPreviewItem, displayTitle: string): s
|
||||||
return item.sourceLabel || 'Log event';
|
return item.sourceLabel || 'Log event';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function truncateCompactRowPreview(
|
||||||
|
preview: string,
|
||||||
|
displayTitle: string,
|
||||||
|
relativeTime: string
|
||||||
|
): string {
|
||||||
|
const normalized = preview.replace(/\s+/g, ' ').trim();
|
||||||
|
const metaLength = displayTitle.length + relativeTime.length + (relativeTime ? 2 : 1);
|
||||||
|
const previewLimit = Math.max(COMPACT_ROW_MIN_PREVIEW_LIMIT, COMPACT_ROW_TEXT_LIMIT - metaLength);
|
||||||
|
if (normalized.length <= previewLimit) return normalized;
|
||||||
|
return `${normalized.slice(0, Math.max(0, previewLimit - 3)).trimEnd()}...`;
|
||||||
|
}
|
||||||
|
|
||||||
function setShellHidden(shell: HTMLDivElement): void {
|
function setShellHidden(shell: HTMLDivElement): void {
|
||||||
shell.style.opacity = '0';
|
shell.style.opacity = '0';
|
||||||
shell.style.pointerEvents = 'none';
|
shell.style.pointerEvents = 'none';
|
||||||
|
|
@ -405,42 +419,53 @@ export const GraphMemberLogPreviewHud = ({
|
||||||
(memberName: string, item: MemberLogPreviewItem) => {
|
(memberName: string, item: MemberLogPreviewItem) => {
|
||||||
const relativeTime = formatRelativeTime(item.timestamp);
|
const relativeTime = formatRelativeTime(item.timestamp);
|
||||||
const displayTitle = compactDisplayTitle(item);
|
const displayTitle = compactDisplayTitle(item);
|
||||||
const previewText = compactPreviewText(item, displayTitle);
|
const fullPreviewText = compactPreviewText(item, displayTitle);
|
||||||
|
const previewText = truncateCompactRowPreview(fullPreviewText, displayTitle, relativeTime);
|
||||||
const titleText = relativeTime
|
const titleText = relativeTime
|
||||||
? `${displayTitle} ${relativeTime} ${previewText}`
|
? `${displayTitle} ${relativeTime} ${fullPreviewText}`
|
||||||
: `${displayTitle} ${previewText}`;
|
: `${displayTitle} ${fullPreviewText}`;
|
||||||
const isHighlighted = highlightedItemIds.has(item.id);
|
const isHighlighted = highlightedItemIds.has(item.id);
|
||||||
|
const isError = item.tone === 'error';
|
||||||
|
const rowStateClassName = isHighlighted
|
||||||
|
? isError
|
||||||
|
? 'border-rose-300/75 bg-rose-950/35 shadow-[0_0_0_1px_rgba(253,164,175,0.30),0_0_18px_rgba(244,63,94,0.22)] hover:border-rose-300/80 hover:bg-rose-950/45'
|
||||||
|
: 'border-sky-300/70 bg-[rgba(14,34,62,0.74)] shadow-[0_0_0_1px_rgba(125,211,252,0.30),0_0_18px_rgba(56,189,248,0.22)] hover:border-sky-300/75 hover:bg-[rgba(14,34,62,0.82)]'
|
||||||
|
: isError
|
||||||
|
? 'border-rose-400/35 bg-rose-950/20 hover:border-rose-300/50 hover:bg-rose-950/30'
|
||||||
|
: 'border-white/10 bg-[rgba(8,14,28,0.52)] hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]';
|
||||||
|
const iconClassName = isError
|
||||||
|
? 'float-left mr-2 inline-flex size-5 shrink-0 items-center justify-center rounded bg-rose-500/10'
|
||||||
|
: 'float-left mr-2 inline-flex size-5 shrink-0 items-center justify-center rounded bg-white/5';
|
||||||
|
const headerClassName = 'inline-flex h-5 items-center align-top';
|
||||||
|
const titleClassName = isError
|
||||||
|
? 'text-[11px] font-medium leading-[18px] text-rose-100'
|
||||||
|
: 'text-[11px] font-medium leading-[18px] text-slate-200';
|
||||||
|
const timeClassName = isError
|
||||||
|
? 'ml-1 text-[9px] font-normal leading-[18px] text-rose-300/70'
|
||||||
|
: 'ml-1 text-[9px] font-normal leading-[18px] text-slate-500';
|
||||||
|
const previewClassName = isError
|
||||||
|
? 'ml-1 break-words align-top text-[10px] leading-[18px] text-rose-100/85'
|
||||||
|
: 'ml-1 break-words align-top text-[10px] leading-[18px] text-slate-300/85';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={[
|
className={[
|
||||||
'block h-16 min-h-16 w-full min-w-0 overflow-hidden rounded-md border px-2.5 py-1.5 text-left text-slate-400 transition-[border-color,background-color,box-shadow] duration-500 hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]',
|
'block h-[68px] min-h-[68px] w-full min-w-0 overflow-hidden rounded-md border px-2.5 py-1.5 text-left text-slate-400 transition-[border-color,background-color,box-shadow] duration-500',
|
||||||
isHighlighted
|
rowStateClassName,
|
||||||
? 'border-sky-300/70 bg-[rgba(14,34,62,0.74)] shadow-[0_0_0_1px_rgba(125,211,252,0.30),0_0_18px_rgba(56,189,248,0.22)]'
|
|
||||||
: 'border-white/10 bg-[rgba(8,14,28,0.52)]',
|
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
title={titleText}
|
title={titleText}
|
||||||
onClick={() => openLogs(memberName)}
|
onClick={() => openLogs(memberName)}
|
||||||
>
|
>
|
||||||
<span
|
<span className={iconClassName} aria-hidden="true">
|
||||||
className="float-left mr-2 inline-flex size-5 shrink-0 items-center justify-center rounded bg-white/5 align-top"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
{itemIcon(item)}
|
{itemIcon(item)}
|
||||||
</span>
|
</span>
|
||||||
<span className="align-top text-[11px] font-medium leading-4 text-slate-200">
|
<span className={headerClassName}>
|
||||||
{displayTitle}
|
<span className={titleClassName}>{displayTitle}</span>
|
||||||
</span>
|
{relativeTime ? <span className={timeClassName}>{relativeTime}</span> : null}
|
||||||
{relativeTime ? (
|
|
||||||
<span className="ml-1 align-top text-[9px] font-normal leading-4 text-slate-500">
|
|
||||||
{relativeTime}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
<span className="ml-1 break-words align-top text-[10px] leading-4 text-slate-300/85">
|
|
||||||
{previewText}
|
|
||||||
</span>
|
</span>
|
||||||
|
<span className={previewClassName}>{previewText}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -494,7 +519,7 @@ export const GraphMemberLogPreviewHud = ({
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex h-16 min-h-16 items-center rounded-md border border-dashed border-white/10 bg-[rgba(8,14,28,0.28)] px-3 text-left text-[11px] text-slate-400/60"
|
className="flex h-[68px] min-h-[68px] items-center rounded-md border border-dashed border-white/10 bg-[rgba(8,14,28,0.28)] px-3 text-left text-[11px] text-slate-400/60"
|
||||||
onClick={() => openLogs(memberName)}
|
onClick={() => openLogs(memberName)}
|
||||||
>
|
>
|
||||||
{resolveEmptyText(preview, loading, error)}
|
{resolveEmptyText(preview, loading, error)}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import {
|
||||||
buildMemberAvatarMap,
|
buildMemberAvatarMap,
|
||||||
buildMemberLaunchPresentation,
|
buildMemberLaunchPresentation,
|
||||||
} from '@renderer/utils/memberHelpers';
|
} from '@renderer/utils/memberHelpers';
|
||||||
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
|
|
||||||
import { buildTeamProvisioningPresentation } from '@renderer/utils/teamProvisioningPresentation';
|
import { buildTeamProvisioningPresentation } from '@renderer/utils/teamProvisioningPresentation';
|
||||||
|
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
|
||||||
import { ExternalLink, Loader2, MessageSquare, Plus, User } from 'lucide-react';
|
import { ExternalLink, Loader2, MessageSquare, Plus, User } from 'lucide-react';
|
||||||
|
|
||||||
import { isTaskInReviewCycle, resolveTaskReviewer } from '../../core/domain/taskGraphSemantics';
|
import { isTaskInReviewCycle, resolveTaskReviewer } from '../../core/domain/taskGraphSemantics';
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,48 @@ describe('memberLogPreviewExtractor', () => {
|
||||||
expect(result.items[1]?.preview).toBe('older answer');
|
expect(result.items[1]?.preview).toBe('older answer');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('marks assistant runtime error text as error tone without flagging normal text', () => {
|
||||||
|
const result = extractMemberLogPreviewItems({
|
||||||
|
provider: 'claude_transcript',
|
||||||
|
maxItems: 3,
|
||||||
|
textLimit: 160,
|
||||||
|
messages: [
|
||||||
|
message({
|
||||||
|
uuid: 'api-error',
|
||||||
|
timestamp: '2026-04-01T10:00:00.000Z',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `API Error: 429
|
||||||
|
|
||||||
|
{"type":"error","error":{"type":"api_error","message":"Codex API error: 429"}}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
message({
|
||||||
|
uuid: 'normal-error-word',
|
||||||
|
timestamp: '2026-04-01T10:01:00.000Z',
|
||||||
|
content: [{ type: 'text', text: 'Reviewed the error handling path and it is covered.' }],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.items[0]).toMatchObject({
|
||||||
|
kind: 'text',
|
||||||
|
title: 'Assistant',
|
||||||
|
preview: 'Reviewed the error handling path and it is covered.',
|
||||||
|
tone: 'neutral',
|
||||||
|
});
|
||||||
|
expect(result.items[1]).toMatchObject({
|
||||||
|
kind: 'text',
|
||||||
|
title: 'API error',
|
||||||
|
tone: 'error',
|
||||||
|
});
|
||||||
|
expect(result.items[1]?.preview).toContain('API Error: 429');
|
||||||
|
expect(result.items[1]?.preview).toContain('Codex API error: 429');
|
||||||
|
expect(result.items[1]?.preview).not.toContain('{"type"');
|
||||||
|
});
|
||||||
|
|
||||||
it('extracts readable inbound task and comment messages without agent-only blocks', () => {
|
it('extracts readable inbound task and comment messages without agent-only blocks', () => {
|
||||||
const result = extractMemberLogPreviewItems({
|
const result = extractMemberLogPreviewItems({
|
||||||
provider: 'opencode_runtime',
|
provider: 'opencode_runtime',
|
||||||
|
|
@ -293,6 +335,96 @@ Reply to this comment using MCP tool task_add_comment.
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts OpenCode canonical callId/toolName tool calls defensively', () => {
|
||||||
|
const result = extractMemberLogPreviewItems({
|
||||||
|
provider: 'opencode_runtime',
|
||||||
|
maxItems: 3,
|
||||||
|
textLimit: 160,
|
||||||
|
messages: [
|
||||||
|
message({
|
||||||
|
uuid: 'canonical-call',
|
||||||
|
timestamp: '2026-04-01T10:00:00.000Z',
|
||||||
|
content: '',
|
||||||
|
toolCalls: [
|
||||||
|
{
|
||||||
|
callId: 'fc-canonical',
|
||||||
|
toolName: 'agent-teams_task_add_comment',
|
||||||
|
input: {
|
||||||
|
taskId: '1dcfefd2-e505-4b1f-af22-0227c0aa551a',
|
||||||
|
text: 'Confirmed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
message({
|
||||||
|
uuid: 'canonical-result',
|
||||||
|
type: 'user',
|
||||||
|
role: 'user',
|
||||||
|
timestamp: '2026-04-01T10:01:00.000Z',
|
||||||
|
content: '',
|
||||||
|
toolResults: [
|
||||||
|
{
|
||||||
|
toolUseId: 'fc-canonical',
|
||||||
|
content: JSON.stringify({
|
||||||
|
taskId: '1dcfefd2-e505-4b1f-af22-0227c0aa551a',
|
||||||
|
comment: {
|
||||||
|
id: 'comment-1',
|
||||||
|
author: 'jack',
|
||||||
|
text: 'Confirmed',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.items).toHaveLength(1);
|
||||||
|
expect(result.items[0]).toMatchObject({
|
||||||
|
kind: 'tool_result',
|
||||||
|
title: 'Comment added',
|
||||||
|
preview: 'Comment by jack on #1dcfefd2: Confirmed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks nested structured error tool results without requiring is_error', () => {
|
||||||
|
const result = extractMemberLogPreviewItems({
|
||||||
|
provider: 'claude_transcript',
|
||||||
|
maxItems: 3,
|
||||||
|
textLimit: 160,
|
||||||
|
messages: [
|
||||||
|
message({
|
||||||
|
uuid: 'api-result',
|
||||||
|
type: 'user',
|
||||||
|
role: 'user',
|
||||||
|
timestamp: '2026-04-01T10:01:00.000Z',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tool_result',
|
||||||
|
tool_use_id: 'tool-api',
|
||||||
|
content: JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
error: {
|
||||||
|
type: 'api_error',
|
||||||
|
message: 'Codex API error: 429',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.items[0]).toMatchObject({
|
||||||
|
kind: 'tool_result',
|
||||||
|
title: 'Tool error',
|
||||||
|
preview: 'Codex API error: 429',
|
||||||
|
tone: 'error',
|
||||||
|
});
|
||||||
|
expect(result.items[0]?.preview).not.toContain('{"type"');
|
||||||
|
});
|
||||||
|
|
||||||
it('formats orphan comment result payloads without guessing add vs read semantics', () => {
|
it('formats orphan comment result payloads without guessing add vs read semantics', () => {
|
||||||
const result = extractMemberLogPreviewItems({
|
const result = extractMemberLogPreviewItems({
|
||||||
provider: 'claude_transcript',
|
provider: 'claude_transcript',
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ export interface MemberLogPreviewParsedMessage {
|
||||||
content: string | MemberLogPreviewContentBlock[];
|
content: string | MemberLogPreviewContentBlock[];
|
||||||
isMeta?: boolean;
|
isMeta?: boolean;
|
||||||
toolCalls?: readonly {
|
toolCalls?: readonly {
|
||||||
id: string;
|
id?: string;
|
||||||
name: string;
|
name?: string;
|
||||||
|
callId?: string;
|
||||||
|
toolName?: string;
|
||||||
input?: unknown;
|
input?: unknown;
|
||||||
isTask?: boolean;
|
isTask?: boolean;
|
||||||
}[];
|
}[];
|
||||||
|
|
@ -107,6 +109,20 @@ interface ToolUseContext {
|
||||||
input?: unknown;
|
input?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ToolCallLike {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
callId?: string;
|
||||||
|
toolName?: string;
|
||||||
|
input?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NormalizedToolCall {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
input?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
function timestampMs(value: Date | string): number {
|
function timestampMs(value: Date | string): number {
|
||||||
const time = value instanceof Date ? value.getTime() : Date.parse(value);
|
const time = value instanceof Date ? value.getTime() : Date.parse(value);
|
||||||
return Number.isFinite(time) ? time : UNKNOWN_TIMESTAMP_MS;
|
return Number.isFinite(time) ? time : UNKNOWN_TIMESTAMP_MS;
|
||||||
|
|
@ -207,6 +223,18 @@ function textFromTextContentBlocks(value: unknown): string | null {
|
||||||
return text.trim().length > 0 ? text : null;
|
return text.trim().length > 0 ? text : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function textFromPreviewContent(content: string | MemberLogPreviewContentBlock[]): string {
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
.filter((block): block is Extract<MemberLogPreviewContentBlock, { type: 'text' }> => {
|
||||||
|
return block.type === 'text' && typeof block.text === 'string';
|
||||||
|
})
|
||||||
|
.map((block) => block.text)
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
function unwrapAgentTeamsResponsePayload(payload: Record<string, unknown>): {
|
function unwrapAgentTeamsResponsePayload(payload: Record<string, unknown>): {
|
||||||
payload: Record<string, unknown>;
|
payload: Record<string, unknown>;
|
||||||
wrapperKey?: string;
|
wrapperKey?: string;
|
||||||
|
|
@ -256,6 +284,127 @@ function recordFromUnknown(value: unknown): Record<string, unknown> | null {
|
||||||
return recordFromUnknownWithWrapper(value)?.payload ?? null;
|
return recordFromUnknownWithWrapper(value)?.payload ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unknownPayloadLooksLikeError(value: unknown): boolean {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.some(unknownPayloadLooksLikeError);
|
||||||
|
}
|
||||||
|
const record = asRecord(value);
|
||||||
|
if (!record) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = stringField(record, 'type')?.toLowerCase();
|
||||||
|
if (type === 'error' || type?.endsWith('_error')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (record.ok === false || record.success === false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = record.error;
|
||||||
|
if (typeof error === 'string' && error.trim().length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (error === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const errorRecord = asRecord(error);
|
||||||
|
if (
|
||||||
|
errorRecord &&
|
||||||
|
(stringField(errorRecord, 'type') ||
|
||||||
|
stringField(errorRecord, 'message') ||
|
||||||
|
stringField(errorRecord, 'code'))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof record.errorMessage === 'string' && record.errorMessage.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function payloadErrorMessage(payload: Record<string, unknown>): string | null {
|
||||||
|
const direct =
|
||||||
|
stringField(payload, 'error') ??
|
||||||
|
stringField(payload, 'errorMessage') ??
|
||||||
|
stringField(payload, 'message');
|
||||||
|
if (direct) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nestedError = asRecord(payload.error);
|
||||||
|
if (!nestedError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const nestedType = stringField(nestedError, 'type')?.replace(/_/g, ' ');
|
||||||
|
const nestedCode = stringField(nestedError, 'code');
|
||||||
|
const nestedMessage = stringField(nestedError, 'message');
|
||||||
|
if (nestedMessage) {
|
||||||
|
return nestedMessage;
|
||||||
|
}
|
||||||
|
if (nestedType && nestedCode) {
|
||||||
|
return `${nestedType} ${nestedCode}`;
|
||||||
|
}
|
||||||
|
return nestedType ?? nestedCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonObjectFromText(value: string): Record<string, unknown> | null {
|
||||||
|
const parsed = parseJsonLikeString(value);
|
||||||
|
const direct = asRecord(parsed);
|
||||||
|
if (direct) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonStart = value.indexOf('{');
|
||||||
|
if (jsonStart <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return asRecord(parseJsonLikeString(value.slice(jsonStart)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function runtimeErrorTitle(value: string, payload: Record<string, unknown> | null): string {
|
||||||
|
const labelMatch = /^(api error|runtime error|provider error|tool error)\b/i.exec(value);
|
||||||
|
if (labelMatch?.[1]) {
|
||||||
|
const label = labelMatch[1].toLowerCase();
|
||||||
|
if (label === 'api error') return 'API error';
|
||||||
|
return `${label[0]?.toUpperCase()}${label.slice(1)}`;
|
||||||
|
}
|
||||||
|
const type = stringField(payload, 'type') ?? stringField(asRecord(payload?.error), 'type');
|
||||||
|
if (type?.toLowerCase().includes('api')) {
|
||||||
|
return 'API error';
|
||||||
|
}
|
||||||
|
return 'Runtime error';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRuntimeErrorText(
|
||||||
|
value: string,
|
||||||
|
limit: number
|
||||||
|
): (ValuePreview & { title: string }) | null {
|
||||||
|
const compact = compactWhitespace(value);
|
||||||
|
if (!compact) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = parseJsonObjectFromText(compact);
|
||||||
|
const hasErrorSignal =
|
||||||
|
/^(api error|runtime error|provider error|tool error)\b/i.test(compact) ||
|
||||||
|
/\b(api|codex|claude|openai|anthropic)\s+api\s+error\b/i.test(compact) ||
|
||||||
|
/\b(api|codex|claude|openai|anthropic|provider)\s+error\s*:\s*\d{3}\b/i.test(compact) ||
|
||||||
|
unknownPayloadLooksLikeError(payload);
|
||||||
|
|
||||||
|
if (!hasErrorSignal) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = runtimeErrorTitle(compact, payload);
|
||||||
|
const jsonStart = compact.indexOf('{');
|
||||||
|
const header = jsonStart > 0 ? compact.slice(0, jsonStart).trim() : '';
|
||||||
|
const payloadMessage = payload ? payloadErrorMessage(payload) : null;
|
||||||
|
const text =
|
||||||
|
payloadMessage && header && !header.toLowerCase().includes(payloadMessage.toLowerCase())
|
||||||
|
? `${header} - ${payloadMessage}`
|
||||||
|
: (payloadMessage ?? header);
|
||||||
|
return { ...truncatePreview(text || 'Runtime error', limit), title };
|
||||||
|
}
|
||||||
|
|
||||||
function findPriorityValue(
|
function findPriorityValue(
|
||||||
record: Record<string, unknown>,
|
record: Record<string, unknown>,
|
||||||
keys: readonly string[]
|
keys: readonly string[]
|
||||||
|
|
@ -721,18 +870,8 @@ function formatRuntimePayload(
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatErrorPayload(payload: Record<string, unknown>): KnownPayloadPreview | null {
|
function formatErrorPayload(payload: Record<string, unknown>): KnownPayloadPreview | null {
|
||||||
const error =
|
if (unknownPayloadLooksLikeError(payload)) {
|
||||||
stringField(payload, 'error') ??
|
return { title: 'Tool error', text: payloadErrorMessage(payload) ?? 'Tool reported failure' };
|
||||||
stringField(payload, 'errorMessage') ??
|
|
||||||
stringField(payload, 'message');
|
|
||||||
if (
|
|
||||||
error &&
|
|
||||||
(payload.ok === false || payload.success === false || payload.error || payload.errorMessage)
|
|
||||||
) {
|
|
||||||
return { title: 'Tool error', text: error };
|
|
||||||
}
|
|
||||||
if (payload.ok === false || payload.success === false) {
|
|
||||||
return { title: 'Tool error', text: 'Tool reported failure' };
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -1059,16 +1198,7 @@ function extractTextPreview(
|
||||||
content: string | MemberLogPreviewContentBlock[],
|
content: string | MemberLogPreviewContentBlock[],
|
||||||
textLimit: number
|
textLimit: number
|
||||||
): { preview: string; truncated: boolean } | null {
|
): { preview: string; truncated: boolean } | null {
|
||||||
if (typeof content === 'string') {
|
const text = textFromPreviewContent(content);
|
||||||
const text = truncatePreview(content, textLimit);
|
|
||||||
return text.preview.length > 0 ? text : null;
|
|
||||||
}
|
|
||||||
const text = content
|
|
||||||
.filter((block): block is Extract<MemberLogPreviewContentBlock, { type: 'text' }> => {
|
|
||||||
return block.type === 'text' && typeof block.text === 'string';
|
|
||||||
})
|
|
||||||
.map((block) => block.text)
|
|
||||||
.join(' ');
|
|
||||||
const preview = truncatePreview(text, textLimit);
|
const preview = truncatePreview(text, textLimit);
|
||||||
return preview.preview.length > 0 ? preview : null;
|
return preview.preview.length > 0 ? preview : null;
|
||||||
}
|
}
|
||||||
|
|
@ -1204,12 +1334,35 @@ function isToolResultBlock(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeToolCall(toolCall: ToolCallLike): NormalizedToolCall | null {
|
||||||
|
const id =
|
||||||
|
typeof toolCall.id === 'string'
|
||||||
|
? toolCall.id.trim()
|
||||||
|
: typeof toolCall.callId === 'string'
|
||||||
|
? toolCall.callId.trim()
|
||||||
|
: '';
|
||||||
|
const name =
|
||||||
|
typeof toolCall.name === 'string'
|
||||||
|
? toolCall.name.trim()
|
||||||
|
: typeof toolCall.toolName === 'string'
|
||||||
|
? toolCall.toolName.trim()
|
||||||
|
: '';
|
||||||
|
if (!id || !name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
input: toolCall.input,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildToolUseContexts(
|
function buildToolUseContexts(
|
||||||
messages: readonly MemberLogPreviewParsedMessage[]
|
messages: readonly MemberLogPreviewParsedMessage[]
|
||||||
): Map<string, ToolUseContext> {
|
): Map<string, ToolUseContext> {
|
||||||
const contexts = new Map<string, ToolUseContext>();
|
const contexts = new Map<string, ToolUseContext>();
|
||||||
const addContext = (tool: { id: string; name: string; input?: unknown }): void => {
|
const addContext = (tool: NormalizedToolCall): void => {
|
||||||
const id = tool.id.trim();
|
const id = tool.id;
|
||||||
if (!id || contexts.has(id)) return;
|
if (!id || contexts.has(id)) return;
|
||||||
contexts.set(id, {
|
contexts.set(id, {
|
||||||
id,
|
id,
|
||||||
|
|
@ -1220,7 +1373,12 @@ function buildToolUseContexts(
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
message.toolCalls?.forEach((toolCall) => addContext(toolCall));
|
message.toolCalls?.forEach((toolCall) => {
|
||||||
|
const normalized = normalizeToolCall(toolCall);
|
||||||
|
if (normalized) {
|
||||||
|
addContext(normalized);
|
||||||
|
}
|
||||||
|
});
|
||||||
if (!Array.isArray(message.content)) continue;
|
if (!Array.isArray(message.content)) continue;
|
||||||
message.content.forEach((block) => {
|
message.content.forEach((block) => {
|
||||||
if (!isToolUseBlock(block)) return;
|
if (!isToolUseBlock(block)) return;
|
||||||
|
|
@ -1375,7 +1533,12 @@ function collectToolUseCandidates(input: {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
input.message.toolCalls?.forEach((toolCall, index) => addTool(toolCall, 100 + index));
|
input.message.toolCalls?.forEach((toolCall, index) => {
|
||||||
|
const normalized = normalizeToolCall(toolCall);
|
||||||
|
if (normalized) {
|
||||||
|
addTool(normalized, 100 + index);
|
||||||
|
}
|
||||||
|
});
|
||||||
if (Array.isArray(input.message.content)) {
|
if (Array.isArray(input.message.content)) {
|
||||||
input.message.content.forEach((block, index) => {
|
input.message.content.forEach((block, index) => {
|
||||||
if (!isToolUseBlock(block)) return;
|
if (!isToolUseBlock(block)) return;
|
||||||
|
|
@ -1527,6 +1690,10 @@ export function extractMemberLogPreviewItems(
|
||||||
if (role === 'assistant') {
|
if (role === 'assistant') {
|
||||||
const textPreview = extractTextPreview(message.content, textLimit);
|
const textPreview = extractTextPreview(message.content, textLimit);
|
||||||
if (textPreview) {
|
if (textPreview) {
|
||||||
|
const runtimeErrorPreview = formatRuntimeErrorText(
|
||||||
|
textFromPreviewContent(message.content),
|
||||||
|
textLimit
|
||||||
|
);
|
||||||
candidates.push(
|
candidates.push(
|
||||||
buildCandidate({
|
buildCandidate({
|
||||||
provider: input.provider,
|
provider: input.provider,
|
||||||
|
|
@ -1535,14 +1702,14 @@ export function extractMemberLogPreviewItems(
|
||||||
messageIndex,
|
messageIndex,
|
||||||
blockIndex: 10,
|
blockIndex: 10,
|
||||||
kind: 'text',
|
kind: 'text',
|
||||||
title: 'Assistant',
|
title: runtimeErrorPreview?.title ?? 'Assistant',
|
||||||
preview: textPreview.preview,
|
preview: runtimeErrorPreview?.preview ?? textPreview.preview,
|
||||||
tone: 'neutral',
|
tone: runtimeErrorPreview ? 'error' : 'neutral',
|
||||||
sourceLabel: input.sourceLabel,
|
sourceLabel: input.sourceLabel,
|
||||||
sessionId: input.sessionId ?? message.sessionId,
|
sessionId: input.sessionId ?? message.sessionId,
|
||||||
laneId: input.laneId,
|
laneId: input.laneId,
|
||||||
token: 'assistant-text',
|
token: 'assistant-text',
|
||||||
textTruncated: textPreview.truncated,
|
textTruncated: runtimeErrorPreview?.truncated ?? textPreview.truncated,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { decideMemberWorkSyncStatus } from '../domain';
|
import { decideMemberWorkSyncStatus } from '../domain';
|
||||||
|
|
||||||
import { finalizeMemberWorkSyncAgenda } from './MemberWorkSyncReconciler';
|
import { finalizeMemberWorkSyncAgenda } from './MemberWorkSyncReconciler';
|
||||||
|
|
||||||
import type { MemberWorkSyncStatus, MemberWorkSyncStatusRequest } from '../../contracts';
|
import type { MemberWorkSyncStatus, MemberWorkSyncStatusRequest } from '../../contracts';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
|
import { decideMemberWorkSyncStatus } from '../domain';
|
||||||
|
|
||||||
import { appendMemberWorkSyncAudit, reasonToAuditEvent } from './MemberWorkSyncAudit';
|
import { appendMemberWorkSyncAudit, reasonToAuditEvent } from './MemberWorkSyncAudit';
|
||||||
import { decideMemberWorkSyncNudgeActivation } from './MemberWorkSyncNudgeActivationPolicy';
|
import { decideMemberWorkSyncNudgeActivation } from './MemberWorkSyncNudgeActivationPolicy';
|
||||||
import { finalizeMemberWorkSyncAgenda } from './MemberWorkSyncReconciler';
|
import { finalizeMemberWorkSyncAgenda } from './MemberWorkSyncReconciler';
|
||||||
import { decideMemberWorkSyncStatus } from '../domain';
|
|
||||||
|
|
||||||
import type { MemberWorkSyncOutboxItem, MemberWorkSyncStatus } from '../../contracts';
|
import type { MemberWorkSyncOutboxItem, MemberWorkSyncStatus } from '../../contracts';
|
||||||
import type { MemberWorkSyncAuditEventName, MemberWorkSyncUseCaseDeps } from './ports';
|
import type { MemberWorkSyncAuditEventName, MemberWorkSyncUseCaseDeps } from './ports';
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ import {
|
||||||
RuntimeTurnSettledIngestor,
|
RuntimeTurnSettledIngestor,
|
||||||
type RuntimeTurnSettledTargetResolverPort,
|
type RuntimeTurnSettledTargetResolverPort,
|
||||||
} from '../../core/application';
|
} from '../../core/application';
|
||||||
import { MemberWorkSyncTeamChangeRouter } from '../adapters/input/MemberWorkSyncTeamChangeRouter';
|
|
||||||
import { MemberWorkSyncTaskImpactResolver } from '../adapters/input/MemberWorkSyncTaskImpactResolver';
|
import { MemberWorkSyncTaskImpactResolver } from '../adapters/input/MemberWorkSyncTaskImpactResolver';
|
||||||
|
import { MemberWorkSyncTeamChangeRouter } from '../adapters/input/MemberWorkSyncTeamChangeRouter';
|
||||||
import { TeamInboxMemberWorkSyncNudgeSink } from '../adapters/output/TeamInboxMemberWorkSyncNudgeSink';
|
import { TeamInboxMemberWorkSyncNudgeSink } from '../adapters/output/TeamInboxMemberWorkSyncNudgeSink';
|
||||||
import { TeamRuntimeTurnSettledTargetResolver } from '../adapters/output/TeamRuntimeTurnSettledTargetResolver';
|
import { TeamRuntimeTurnSettledTargetResolver } from '../adapters/output/TeamRuntimeTurnSettledTargetResolver';
|
||||||
import { TeamTaskAgendaSource } from '../adapters/output/TeamTaskAgendaSource';
|
import { TeamTaskAgendaSource } from '../adapters/output/TeamTaskAgendaSource';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { createReadStream } from 'node:fs';
|
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import readline from 'node:readline';
|
|
||||||
|
|
||||||
import { normalizeIdentityPath } from '@features/recent-projects/main/infrastructure/identity/normalizeIdentityPath';
|
import { normalizeIdentityPath } from '@features/recent-projects/main/infrastructure/identity/normalizeIdentityPath';
|
||||||
import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath';
|
import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath';
|
||||||
|
|
@ -18,8 +16,9 @@ import type { ServiceContext } from '@main/services';
|
||||||
|
|
||||||
const CODEX_SESSION_FILE_PARSE_LIMIT = 500;
|
const CODEX_SESSION_FILE_PARSE_LIMIT = 500;
|
||||||
const CODEX_PROJECT_CANDIDATE_LIMIT = 40;
|
const CODEX_PROJECT_CANDIDATE_LIMIT = 40;
|
||||||
const CODEX_SESSION_FILE_SOURCE_TIMEOUT_MS = 3_500;
|
const CODEX_SESSION_FILE_SOURCE_TIMEOUT_MS = 8_000;
|
||||||
const CODEX_SESSION_FILE_READ_BATCH_SIZE = 24;
|
const CODEX_SESSION_FILE_READ_BATCH_SIZE = 24;
|
||||||
|
const CODEX_SESSION_METADATA_READ_LIMIT_BYTES = 128 * 1024;
|
||||||
|
|
||||||
interface CodexSessionFileEntry {
|
interface CodexSessionFileEntry {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
|
|
@ -45,6 +44,14 @@ interface CodexSessionProjectSnapshot {
|
||||||
branchName?: string;
|
branchName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CodexSessionMetadata {
|
||||||
|
cwd: string;
|
||||||
|
source: unknown;
|
||||||
|
payloadTimestamp?: unknown;
|
||||||
|
eventTimestamp?: unknown;
|
||||||
|
branchName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
function isInteractiveSource(source: unknown): boolean {
|
function isInteractiveSource(source: unknown): boolean {
|
||||||
return source === 'vscode' || source === 'cli';
|
return source === 'vscode' || source === 'cli';
|
||||||
}
|
}
|
||||||
|
|
@ -66,23 +73,47 @@ function getCodexHome(codexHome?: string): string {
|
||||||
return codexHome?.trim() || process.env.CODEX_HOME?.trim() || path.join(os.homedir(), '.codex');
|
return codexHome?.trim() || process.env.CODEX_HOME?.trim() || path.join(os.homedir(), '.codex');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readFirstLine(filePath: string): Promise<string | null> {
|
function extractJsonStringField(input: string, fieldName: string): string {
|
||||||
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
const pattern = new RegExp(`"${fieldName}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`);
|
||||||
const lines = readline.createInterface({
|
const match = pattern.exec(input);
|
||||||
input: stream,
|
if (!match) return '';
|
||||||
crlfDelay: Infinity,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const line of lines) {
|
return JSON.parse(`"${match[1]}"`) as string;
|
||||||
return line;
|
} catch {
|
||||||
}
|
return '';
|
||||||
return null;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSessionMetadataPrefix(firstLine: string): CodexSessionMetadata | null {
|
||||||
|
const cwd = extractJsonStringField(firstLine, 'cwd').trim();
|
||||||
|
const source = extractJsonStringField(firstLine, 'source').trim();
|
||||||
|
if (!cwd || !source) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
cwd,
|
||||||
|
source,
|
||||||
|
payloadTimestamp: extractJsonStringField(firstLine, 'timestamp'),
|
||||||
|
eventTimestamp: extractJsonStringField(firstLine, 'timestamp'),
|
||||||
|
branchName: extractJsonStringField(firstLine, 'branch').trim() || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readFirstLine(filePath: string): Promise<string | null> {
|
||||||
|
let handle: fs.FileHandle | null = null;
|
||||||
|
try {
|
||||||
|
handle = await fs.open(filePath, 'r');
|
||||||
|
const buffer = Buffer.allocUnsafe(CODEX_SESSION_METADATA_READ_LIMIT_BYTES);
|
||||||
|
const result = await handle.read(buffer, 0, buffer.length, 0);
|
||||||
|
if (result.bytesRead <= 0) return null;
|
||||||
|
|
||||||
|
const newlineIndex = buffer.subarray(0, result.bytesRead).indexOf(0x0a);
|
||||||
|
const endIndex = newlineIndex >= 0 ? newlineIndex : result.bytesRead;
|
||||||
|
return buffer.toString('utf8', 0, endIndex);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
lines.close();
|
await handle?.close().catch(() => undefined);
|
||||||
stream.destroy();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,28 +161,36 @@ function parseSessionSnapshot(
|
||||||
firstLine: string,
|
firstLine: string,
|
||||||
mtimeMs: number
|
mtimeMs: number
|
||||||
): CodexSessionProjectSnapshot | null {
|
): CodexSessionProjectSnapshot | null {
|
||||||
let event: CodexSessionEvent;
|
let metadata: CodexSessionMetadata | null = null;
|
||||||
try {
|
try {
|
||||||
event = JSON.parse(firstLine) as CodexSessionEvent;
|
const event = JSON.parse(firstLine) as CodexSessionEvent;
|
||||||
|
metadata = {
|
||||||
|
cwd: typeof event.payload?.cwd === 'string' ? event.payload.cwd.trim() : '',
|
||||||
|
source: event.payload?.source,
|
||||||
|
payloadTimestamp: event.payload?.timestamp,
|
||||||
|
eventTimestamp: event.timestamp,
|
||||||
|
branchName:
|
||||||
|
typeof event.payload?.git?.branch === 'string' ? event.payload.git.branch.trim() : '',
|
||||||
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
metadata = parseSessionMetadataPrefix(firstLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = typeof event.payload?.cwd === 'string' ? event.payload.cwd.trim() : '';
|
const cwd = metadata?.cwd ?? '';
|
||||||
if (!cwd || !isInteractiveSource(event.payload?.source) || isEphemeralProjectPath(cwd)) {
|
if (!metadata || !cwd || !isInteractiveSource(metadata.source) || isEphemeralProjectPath(cwd)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp =
|
const timestamp =
|
||||||
mtimeMs || normalizeTimestamp(event.payload?.timestamp) || normalizeTimestamp(event.timestamp);
|
mtimeMs ||
|
||||||
const branchName =
|
normalizeTimestamp(metadata.payloadTimestamp) ||
|
||||||
typeof event.payload?.git?.branch === 'string' ? event.payload.git.branch.trim() : '';
|
normalizeTimestamp(metadata.eventTimestamp);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cwd,
|
cwd,
|
||||||
source: event.payload?.source,
|
source: metadata.source,
|
||||||
lastActivityAt: timestamp,
|
lastActivityAt: timestamp,
|
||||||
branchName: branchName || undefined,
|
branchName: metadata.branchName || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -356,10 +356,7 @@ export class TeamConfigReader {
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingRequest = TeamConfigReader.listTeamsInFlightByBasePath.get(teamsBasePath);
|
const existingRequest = TeamConfigReader.listTeamsInFlightByBasePath.get(teamsBasePath);
|
||||||
if (
|
if (existingRequest?.generationAtStart === TeamConfigReader.listTeamsGeneration) {
|
||||||
existingRequest &&
|
|
||||||
existingRequest.generationAtStart === TeamConfigReader.listTeamsGeneration
|
|
||||||
) {
|
|
||||||
return cloneTeamSummaries(await existingRequest.promise);
|
return cloneTeamSummaries(await existingRequest.promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,11 +58,11 @@ import { TeamMembersMetaStore } from './TeamMembersMetaStore';
|
||||||
import { TeamMessageFeedService } from './TeamMessageFeedService';
|
import { TeamMessageFeedService } from './TeamMessageFeedService';
|
||||||
import { TeamMetaStore } from './TeamMetaStore';
|
import { TeamMetaStore } from './TeamMetaStore';
|
||||||
import { TeamSentMessagesStore } from './TeamSentMessagesStore';
|
import { TeamSentMessagesStore } from './TeamSentMessagesStore';
|
||||||
|
import { getTeamTaskWorkflowColumn, selectCurrentActiveTeamTask } from './teamTaskActiveState';
|
||||||
import { TeamTaskCommentNotificationJournal } from './TeamTaskCommentNotificationJournal';
|
import { TeamTaskCommentNotificationJournal } from './TeamTaskCommentNotificationJournal';
|
||||||
import { TeamTaskReader } from './TeamTaskReader';
|
import { TeamTaskReader } from './TeamTaskReader';
|
||||||
import { TeamTaskWriter } from './TeamTaskWriter';
|
import { TeamTaskWriter } from './TeamTaskWriter';
|
||||||
import { TeamTranscriptProjectResolver } from './TeamTranscriptProjectResolver';
|
import { TeamTranscriptProjectResolver } from './TeamTranscriptProjectResolver';
|
||||||
import { getTeamTaskWorkflowColumn, selectCurrentActiveTeamTask } from './teamTaskActiveState';
|
|
||||||
|
|
||||||
import type { PersistedTaskChangePresenceIndex } from './cache/taskChangePresenceCacheTypes';
|
import type { PersistedTaskChangePresenceIndex } from './cache/taskChangePresenceCacheTypes';
|
||||||
import type { TaskChangePresenceRepository } from './cache/TaskChangePresenceRepository';
|
import type { TaskChangePresenceRepository } from './cache/TaskChangePresenceRepository';
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { createLogger } from '@shared/utils/logger';
|
|
||||||
import { getTeamsBasePath } from '@main/utils/pathDecoder';
|
import { getTeamsBasePath } from '@main/utils/pathDecoder';
|
||||||
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
|
|
||||||
import { TeamInboxReader } from './TeamInboxReader';
|
|
||||||
import { TeamMemberLogsFinder } from './TeamMemberLogsFinder';
|
|
||||||
import {
|
import {
|
||||||
createOpenCodePromptDeliveryLedgerStore,
|
createOpenCodePromptDeliveryLedgerStore,
|
||||||
type OpenCodePromptDeliveryLedgerRecord,
|
type OpenCodePromptDeliveryLedgerRecord,
|
||||||
|
|
@ -13,6 +11,8 @@ import {
|
||||||
getOpenCodeLaneScopedRuntimeFilePath,
|
getOpenCodeLaneScopedRuntimeFilePath,
|
||||||
readOpenCodeRuntimeLaneIndex,
|
readOpenCodeRuntimeLaneIndex,
|
||||||
} from './opencode/store/OpenCodeRuntimeManifestEvidenceReader';
|
} from './opencode/store/OpenCodeRuntimeManifestEvidenceReader';
|
||||||
|
import { TeamInboxReader } from './TeamInboxReader';
|
||||||
|
import { TeamMemberLogsFinder } from './TeamMemberLogsFinder';
|
||||||
|
|
||||||
import type { MemberLogSummary, MemberRuntimeAdvisory, ResolvedTeamMember } from '@shared/types';
|
import type { MemberLogSummary, MemberRuntimeAdvisory, ResolvedTeamMember } from '@shared/types';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -451,7 +451,7 @@ export class TeamMessageFeedService {
|
||||||
|
|
||||||
const existingRequest = this.inFlightByTeam.get(teamName);
|
const existingRequest = this.inFlightByTeam.get(teamName);
|
||||||
const generationAtStart = this.getGeneration(teamName);
|
const generationAtStart = this.getGeneration(teamName);
|
||||||
if (existingRequest && existingRequest.generationAtStart === generationAtStart) {
|
if (existingRequest?.generationAtStart === generationAtStart) {
|
||||||
return existingRequest.promise;
|
return existingRequest.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,12 +84,6 @@ import { createLogger } from '@shared/utils/logger';
|
||||||
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
||||||
import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
|
import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
|
||||||
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
import {
|
|
||||||
getTeamTaskWorkflowColumn,
|
|
||||||
isTeamTaskActivelyWorked,
|
|
||||||
isTeamTaskDeleted,
|
|
||||||
isTeamTaskNeedsFixActionable,
|
|
||||||
} from '@shared/utils/teamTaskState';
|
|
||||||
import {
|
import {
|
||||||
isTeamInternalControlMessageText,
|
isTeamInternalControlMessageText,
|
||||||
stripExactInternalControlEchoPrefix,
|
stripExactInternalControlEchoPrefix,
|
||||||
|
|
@ -104,6 +98,12 @@ import {
|
||||||
inferTeamProviderIdFromModel,
|
inferTeamProviderIdFromModel,
|
||||||
normalizeOptionalTeamProviderId,
|
normalizeOptionalTeamProviderId,
|
||||||
} from '@shared/utils/teamProvider';
|
} from '@shared/utils/teamProvider';
|
||||||
|
import {
|
||||||
|
getTeamTaskWorkflowColumn,
|
||||||
|
isTeamTaskActivelyWorked,
|
||||||
|
isTeamTaskDeleted,
|
||||||
|
isTeamTaskNeedsFixActionable,
|
||||||
|
} from '@shared/utils/teamTaskState';
|
||||||
import {
|
import {
|
||||||
extractToolPreview,
|
extractToolPreview,
|
||||||
extractToolResultPreview,
|
extractToolResultPreview,
|
||||||
|
|
@ -169,10 +169,6 @@ import {
|
||||||
type OpenCodePromptDeliveryLedgerStore,
|
type OpenCodePromptDeliveryLedgerStore,
|
||||||
type OpenCodePromptDeliveryStatus,
|
type OpenCodePromptDeliveryStatus,
|
||||||
} from './opencode/delivery/OpenCodePromptDeliveryLedger';
|
} from './opencode/delivery/OpenCodePromptDeliveryLedger';
|
||||||
import {
|
|
||||||
isActionRequiredOpenCodeRuntimeDeliveryReason,
|
|
||||||
selectOpenCodeRuntimeDeliveryReason,
|
|
||||||
} from './opencode/delivery/OpenCodeRuntimeDeliveryDiagnostics';
|
|
||||||
import {
|
import {
|
||||||
decideOpenCodePromptDeliveryRepair,
|
decideOpenCodePromptDeliveryRepair,
|
||||||
type OpenCodePromptDeliveryHardFailureKind,
|
type OpenCodePromptDeliveryHardFailureKind,
|
||||||
|
|
@ -189,6 +185,10 @@ import {
|
||||||
OPENCODE_PROMPT_WATCHDOG_PER_TEAM_CONCURRENCY,
|
OPENCODE_PROMPT_WATCHDOG_PER_TEAM_CONCURRENCY,
|
||||||
type OpenCodeVisibleReplyProof,
|
type OpenCodeVisibleReplyProof,
|
||||||
} from './opencode/delivery/OpenCodePromptDeliveryWatchdog';
|
} from './opencode/delivery/OpenCodePromptDeliveryWatchdog';
|
||||||
|
import {
|
||||||
|
isActionRequiredOpenCodeRuntimeDeliveryReason,
|
||||||
|
selectOpenCodeRuntimeDeliveryReason,
|
||||||
|
} from './opencode/delivery/OpenCodeRuntimeDeliveryDiagnostics';
|
||||||
import { createRuntimeDeliveryJournalStore } from './opencode/delivery/RuntimeDeliveryJournal';
|
import { createRuntimeDeliveryJournalStore } from './opencode/delivery/RuntimeDeliveryJournal';
|
||||||
import {
|
import {
|
||||||
type RuntimeDeliveryDestinationPort,
|
type RuntimeDeliveryDestinationPort,
|
||||||
|
|
|
||||||
|
|
@ -468,10 +468,7 @@ export class TeamTaskReader {
|
||||||
return cloned;
|
return cloned;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (TeamTaskReader.allTasksInFlight?.generationAtStart === TeamTaskReader.allTasksGeneration) {
|
||||||
TeamTaskReader.allTasksInFlight &&
|
|
||||||
TeamTaskReader.allTasksInFlight.generationAtStart === TeamTaskReader.allTasksGeneration
|
|
||||||
) {
|
|
||||||
const waitedAt = Date.now();
|
const waitedAt = Date.now();
|
||||||
const tasks = await TeamTaskReader.allTasksInFlight.promise;
|
const tasks = await TeamTaskReader.allTasksInFlight.promise;
|
||||||
const cloned = cloneTasks(tasks);
|
const cloned = cloneTasks(tasks);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
|
import {
|
||||||
|
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
|
||||||
|
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
|
||||||
|
} from './OpenCodeBridgeCommandContract';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
OpenCodeBridgeCommandName,
|
OpenCodeBridgeCommandName,
|
||||||
OpenCodeBridgeHandshake,
|
OpenCodeBridgeHandshake,
|
||||||
OpenCodeBridgePeerIdentity,
|
OpenCodeBridgePeerIdentity,
|
||||||
} from './OpenCodeBridgeCommandContract';
|
} from './OpenCodeBridgeCommandContract';
|
||||||
import {
|
|
||||||
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
|
|
||||||
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
|
|
||||||
} from './OpenCodeBridgeCommandContract';
|
|
||||||
import type {
|
import type {
|
||||||
OpenCodeBridgeCommandExecutor,
|
OpenCodeBridgeCommandExecutor,
|
||||||
OpenCodeBridgeHandshakePort,
|
OpenCodeBridgeHandshakePort,
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,13 @@ import {
|
||||||
validateRuntimeStoreManifest,
|
validateRuntimeStoreManifest,
|
||||||
} from './RuntimeStoreManifest';
|
} from './RuntimeStoreManifest';
|
||||||
|
|
||||||
|
import type { RuntimeStoreManifestEvidence } from '../bridge/OpenCodeBridgeCommandContract';
|
||||||
|
import type { RuntimeStoreManifestReader } from '../bridge/OpenCodeStateChangingBridgeCommandService';
|
||||||
|
import type { RuntimeStoreManifestEntryState } from './RuntimeStoreManifest';
|
||||||
import type {
|
import type {
|
||||||
OpenCodeAppManagedBootstrapCandidate,
|
OpenCodeAppManagedBootstrapCandidate,
|
||||||
OpenCodeBootstrapEvidenceSource,
|
OpenCodeBootstrapEvidenceSource,
|
||||||
} from '@shared/types/team';
|
} from '@shared/types/team';
|
||||||
import type { RuntimeStoreManifestEvidence } from '../bridge/OpenCodeBridgeCommandContract';
|
|
||||||
import type { RuntimeStoreManifestReader } from '../bridge/OpenCodeStateChangingBridgeCommandService';
|
|
||||||
import type { RuntimeStoreManifestEntryState } from './RuntimeStoreManifest';
|
|
||||||
|
|
||||||
const logger = createLogger('OpenCodeRuntimeManifestEvidenceReader');
|
const logger = createLogger('OpenCodeRuntimeManifestEvidenceReader');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -596,7 +596,7 @@ function normalizeAppManagedBootstrapCandidate(
|
||||||
runtimeSessionId?: string;
|
runtimeSessionId?: string;
|
||||||
}
|
}
|
||||||
): OpenCodeAppManagedBootstrapCandidate | undefined {
|
): OpenCodeAppManagedBootstrapCandidate | undefined {
|
||||||
if (!value || value.schemaVersion !== 1 || value.source !== 'app_managed_bootstrap') {
|
if (value?.schemaVersion !== 1 || value.source !== 'app_managed_bootstrap') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import { TeamTranscriptSourceLocator } from '../taskLogs/discovery/TeamTranscrip
|
||||||
import { isBoardTaskExactLogsReadEnabled } from '../taskLogs/exact/featureGates';
|
import { isBoardTaskExactLogsReadEnabled } from '../taskLogs/exact/featureGates';
|
||||||
import { TeamKanbanManager } from '../TeamKanbanManager';
|
import { TeamKanbanManager } from '../TeamKanbanManager';
|
||||||
import { TeamMembersMetaStore } from '../TeamMembersMetaStore';
|
import { TeamMembersMetaStore } from '../TeamMembersMetaStore';
|
||||||
import { TeamTaskReader } from '../TeamTaskReader';
|
|
||||||
import { getTeamTaskWorkflowColumn, isTeamTaskActivelyWorked } from '../teamTaskActiveState';
|
import { getTeamTaskWorkflowColumn, isTeamTaskActivelyWorked } from '../teamTaskActiveState';
|
||||||
|
import { TeamTaskReader } from '../TeamTaskReader';
|
||||||
|
|
||||||
import { BoardTaskActivityBatchIndexer } from './BoardTaskActivityBatchIndexer';
|
import { BoardTaskActivityBatchIndexer } from './BoardTaskActivityBatchIndexer';
|
||||||
import { OpenCodeTaskStallEvidenceSource } from './OpenCodeTaskStallEvidenceSource';
|
import { OpenCodeTaskStallEvidenceSource } from './OpenCodeTaskStallEvidenceSource';
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,12 @@ export class BoardTaskActivityRecordSource {
|
||||||
private async getTaskActivityIndex(teamName: string): Promise<TaskActivityIndex> {
|
private async getTaskActivityIndex(teamName: string): Promise<TaskActivityIndex> {
|
||||||
const generation = this.getTranscriptDiscoveryGeneration(teamName);
|
const generation = this.getTranscriptDiscoveryGeneration(teamName);
|
||||||
const cached = this.indexCache.get(teamName);
|
const cached = this.indexCache.get(teamName);
|
||||||
if (cached && cached.generation === generation && cached.expiresAt > Date.now()) {
|
if (cached?.generation === generation && cached.expiresAt > Date.now()) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingInFlight = this.indexInFlight.get(teamName);
|
const existingInFlight = this.indexInFlight.get(teamName);
|
||||||
if (existingInFlight && existingInFlight.generation === generation) {
|
if (existingInFlight?.generation === generation) {
|
||||||
return await existingInFlight.promise;
|
return await existingInFlight.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,12 +95,12 @@ export class TeamTranscriptSourceLocator {
|
||||||
|
|
||||||
const generation = this.getGeneration(teamName);
|
const generation = this.getGeneration(teamName);
|
||||||
const cached = this.contextCache.get(teamName);
|
const cached = this.contextCache.get(teamName);
|
||||||
if (cached && cached.generation === generation && cached.expiresAt > Date.now()) {
|
if (cached?.generation === generation && cached.expiresAt > Date.now()) {
|
||||||
return cached.value;
|
return cached.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inFlight = this.contextInFlight.get(teamName);
|
const inFlight = this.contextInFlight.get(teamName);
|
||||||
if (inFlight && inFlight.generation === generation) {
|
if (inFlight?.generation === generation) {
|
||||||
return await inFlight.promise;
|
return await inFlight.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ export class CodexNativeTraceProjector {
|
||||||
>();
|
>();
|
||||||
for (const event of run.events) {
|
for (const event of run.events) {
|
||||||
const projection = event.projection;
|
const projection = event.projection;
|
||||||
if (!projection || projection.toolSource !== 'native') {
|
if (projection?.toolSource !== 'native') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!projection.itemId) {
|
if (!projection.itemId) {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import type { TeamTaskWithKanban } from '@shared/types';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getTeamTaskWorkflowColumn,
|
getTeamTaskWorkflowColumn,
|
||||||
isTeamTaskFinalForCompletionNotification,
|
|
||||||
isTeamTaskActivelyWorked,
|
isTeamTaskActivelyWorked,
|
||||||
|
isTeamTaskFinalForCompletionNotification,
|
||||||
isTeamTaskFinishedForDependency,
|
isTeamTaskFinishedForDependency,
|
||||||
isTeamTaskNeedsFixActionable,
|
isTeamTaskNeedsFixActionable,
|
||||||
isTeamTaskTerminalForActionableWork,
|
isTeamTaskTerminalForActionableWork,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
import { api, isElectronMode } from '@renderer/api';
|
import { api, isElectronMode } from '@renderer/api';
|
||||||
import { confirm } from '@renderer/components/common/ConfirmDialog';
|
import { confirm } from '@renderer/components/common/ConfirmDialog';
|
||||||
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
||||||
|
import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton';
|
||||||
import {
|
import {
|
||||||
formatProviderStatusText,
|
formatProviderStatusText,
|
||||||
getProviderConnectionModeSummary,
|
getProviderConnectionModeSummary,
|
||||||
|
|
@ -29,7 +30,6 @@ import {
|
||||||
isConnectionManagedRuntimeProvider,
|
isConnectionManagedRuntimeProvider,
|
||||||
shouldShowProviderConnectAction,
|
shouldShowProviderConnectAction,
|
||||||
} from '@renderer/components/runtime/providerConnectionUi';
|
} from '@renderer/components/runtime/providerConnectionUi';
|
||||||
import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton';
|
|
||||||
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
|
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
|
||||||
import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
|
import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
|
||||||
import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog';
|
import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog';
|
||||||
|
|
@ -373,21 +373,6 @@ const ProviderDetailSkeleton = (): React.JSX.Element => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const OpenCodeBetaBadge = (): React.JSX.Element => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className="inline-flex h-4 shrink-0 items-center rounded border px-1.5 text-[9px] font-semibold uppercase leading-none"
|
|
||||||
style={{
|
|
||||||
borderColor: 'rgba(251, 191, 36, 0.32)',
|
|
||||||
backgroundColor: 'rgba(251, 191, 36, 0.12)',
|
|
||||||
color: '#fbbf24',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
beta
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function isProviderCardLoading(provider: CliProviderStatus, providerLoading: boolean): boolean {
|
function isProviderCardLoading(provider: CliProviderStatus, providerLoading: boolean): boolean {
|
||||||
return (
|
return (
|
||||||
providerLoading ||
|
providerLoading ||
|
||||||
|
|
@ -781,7 +766,6 @@ const InstalledBanner = ({
|
||||||
? getProviderLabel(provider.providerId)
|
? getProviderLabel(provider.providerId)
|
||||||
: provider.displayName}
|
: provider.displayName}
|
||||||
</span>
|
</span>
|
||||||
{provider.providerId === 'opencode' ? <OpenCodeBetaBadge /> : null}
|
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ interface CodexLoginLinkCopyButtonProps {
|
||||||
size?: 'xs' | 'sm';
|
size?: 'xs' | 'sm';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CodexLoginLinkCopyButton({
|
export const CodexLoginLinkCopyButton = ({
|
||||||
authUrl,
|
authUrl,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
size = 'sm',
|
size = 'sm',
|
||||||
}: CodexLoginLinkCopyButtonProps): React.JSX.Element | null {
|
}: CodexLoginLinkCopyButtonProps): React.JSX.Element | null => {
|
||||||
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>('idle');
|
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>('idle');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -53,4 +53,4 @@ export function CodexLoginLinkCopyButton({
|
||||||
{copyState === 'copied' ? 'Copied' : copyState === 'failed' ? 'Copy failed' : 'Copy link'}
|
{copyState === 'copied' ? 'Copied' : copyState === 'failed' ? 'Copy failed' : 'Copy link'}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,9 @@ import {
|
||||||
resolveCodexRuntimeSelection,
|
resolveCodexRuntimeSelection,
|
||||||
} from '@features/codex-runtime-profile/renderer';
|
} from '@features/codex-runtime-profile/renderer';
|
||||||
import { RuntimeProviderManagementPanel } from '@features/runtime-provider-management/renderer';
|
import { RuntimeProviderManagementPanel } from '@features/runtime-provider-management/renderer';
|
||||||
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
|
||||||
import { api } from '@renderer/api';
|
import { api } from '@renderer/api';
|
||||||
|
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
||||||
|
import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton';
|
||||||
import { Button } from '@renderer/components/ui/button';
|
import { Button } from '@renderer/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -40,7 +41,6 @@ import {
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@renderer/components/ui/select';
|
} from '@renderer/components/ui/select';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
|
||||||
import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton';
|
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import { AlertTriangle, Key, Link2, Loader2, Trash2 } from 'lucide-react';
|
import { AlertTriangle, Key, Link2, Loader2, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import { useStore } from '@renderer/store';
|
||||||
import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice';
|
import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice';
|
||||||
import { buildMemberColorMap, REVIEW_STATE_DISPLAY } from '@renderer/utils/memberHelpers';
|
import { buildMemberColorMap, REVIEW_STATE_DISPLAY } from '@renderer/utils/memberHelpers';
|
||||||
import { linkifyTaskIdsInMarkdown } from '@renderer/utils/taskReferenceUtils';
|
import { linkifyTaskIdsInMarkdown } from '@renderer/utils/taskReferenceUtils';
|
||||||
|
import { formatTaskDisplayLabel, taskMatchesRef } from '@shared/utils/taskIdentity';
|
||||||
import {
|
import {
|
||||||
getTeamTaskWorkflowColumn,
|
getTeamTaskWorkflowColumn,
|
||||||
isTeamTaskNeedsFixActionable,
|
isTeamTaskNeedsFixActionable,
|
||||||
} from '@shared/utils/teamTaskState';
|
} from '@shared/utils/teamTaskState';
|
||||||
import { formatTaskDisplayLabel, taskMatchesRef } from '@shared/utils/taskIdentity';
|
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
import type { TeamTaskWithKanban } from '@shared/types';
|
import type { TeamTaskWithKanban } from '@shared/types';
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,8 @@ import {
|
||||||
getKnownSlashCommand,
|
getKnownSlashCommand,
|
||||||
parseStandaloneSlashCommand,
|
parseStandaloneSlashCommand,
|
||||||
} from '@shared/utils/slashCommands';
|
} from '@shared/utils/slashCommands';
|
||||||
import { isTaskStallRemediationMessage } from '@shared/utils/teamAutomationMessages';
|
|
||||||
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
|
import { isTaskStallRemediationMessage } from '@shared/utils/teamAutomationMessages';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Check,
|
Check,
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export function shouldShowCodexReconnectPrompt({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CodexReconnectPrompt({
|
export const CodexReconnectPrompt = ({
|
||||||
authUrl,
|
authUrl,
|
||||||
reconnectBusy,
|
reconnectBusy,
|
||||||
onReconnect,
|
onReconnect,
|
||||||
|
|
@ -67,7 +67,7 @@ export function CodexReconnectPrompt({
|
||||||
authUrl: string | null;
|
authUrl: string | null;
|
||||||
reconnectBusy: boolean;
|
reconnectBusy: boolean;
|
||||||
onReconnect: () => void;
|
onReconnect: () => void;
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mt-2 rounded-md border px-2.5 py-2"
|
className="mt-2 rounded-md border px-2.5 py-2"
|
||||||
|
|
@ -98,4 +98,4 @@ export function CodexReconnectPrompt({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@ import {
|
||||||
extractTaskRefsFromText,
|
extractTaskRefsFromText,
|
||||||
stripEncodedTaskReferenceMetadata,
|
stripEncodedTaskReferenceMetadata,
|
||||||
} from '@renderer/utils/taskReferenceUtils';
|
} from '@renderer/utils/taskReferenceUtils';
|
||||||
import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState';
|
|
||||||
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
|
import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState';
|
||||||
import { AlertTriangle, ChevronDown, ChevronRight, Search } from 'lucide-react';
|
import { AlertTriangle, ChevronDown, ChevronRight, Search } from 'lucide-react';
|
||||||
|
|
||||||
import type { InlineChip } from '@renderer/types/inlineChip';
|
import type { InlineChip } from '@renderer/types/inlineChip';
|
||||||
|
|
|
||||||
|
|
@ -88,15 +88,15 @@ import { AlertTriangle, CheckCircle2, Info, Loader2, X } from 'lucide-react';
|
||||||
|
|
||||||
import { AdvancedCliSection } from './AdvancedCliSection';
|
import { AdvancedCliSection } from './AdvancedCliSection';
|
||||||
import { AnthropicFastModeSelector } from './AnthropicFastModeSelector';
|
import { AnthropicFastModeSelector } from './AnthropicFastModeSelector';
|
||||||
import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt';
|
|
||||||
import { CodexFastModeSelector } from './CodexFastModeSelector';
|
import { CodexFastModeSelector } from './CodexFastModeSelector';
|
||||||
|
import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt';
|
||||||
import {
|
import {
|
||||||
clearInheritedMemberModelsUnavailableForProvider,
|
clearInheritedMemberModelsUnavailableForProvider,
|
||||||
resolveProviderScopedMemberModel,
|
resolveProviderScopedMemberModel,
|
||||||
} from './memberModelScope';
|
} from './memberModelScope';
|
||||||
import { OptionalSettingsSection } from './OptionalSettingsSection';
|
import { OptionalSettingsSection } from './OptionalSettingsSection';
|
||||||
import { ProjectPathSelector } from './ProjectPathSelector';
|
|
||||||
import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects';
|
import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects';
|
||||||
|
import { ProjectPathSelector } from './ProjectPathSelector';
|
||||||
import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey';
|
import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey';
|
||||||
import {
|
import {
|
||||||
buildReusableProviderPrepareModelResults,
|
buildReusableProviderPrepareModelResults,
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,8 @@ import { CronScheduleInput } from '../schedule/CronScheduleInput';
|
||||||
|
|
||||||
import { AdvancedCliSection } from './AdvancedCliSection';
|
import { AdvancedCliSection } from './AdvancedCliSection';
|
||||||
import { AnthropicFastModeSelector } from './AnthropicFastModeSelector';
|
import { AnthropicFastModeSelector } from './AnthropicFastModeSelector';
|
||||||
import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt';
|
|
||||||
import { CodexFastModeSelector } from './CodexFastModeSelector';
|
import { CodexFastModeSelector } from './CodexFastModeSelector';
|
||||||
|
import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt';
|
||||||
import { EffortLevelSelector } from './EffortLevelSelector';
|
import { EffortLevelSelector } from './EffortLevelSelector';
|
||||||
import { resolveLaunchDialogPrefill } from './launchDialogPrefill';
|
import { resolveLaunchDialogPrefill } from './launchDialogPrefill';
|
||||||
import {
|
import {
|
||||||
|
|
@ -100,8 +100,8 @@ import {
|
||||||
resolveProviderScopedMemberModel,
|
resolveProviderScopedMemberModel,
|
||||||
} from './memberModelScope';
|
} from './memberModelScope';
|
||||||
import { OptionalSettingsSection } from './OptionalSettingsSection';
|
import { OptionalSettingsSection } from './OptionalSettingsSection';
|
||||||
import { ProjectPathSelector } from './ProjectPathSelector';
|
|
||||||
import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects';
|
import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects';
|
||||||
|
import { ProjectPathSelector } from './ProjectPathSelector';
|
||||||
import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey';
|
import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey';
|
||||||
import {
|
import {
|
||||||
buildReusableProviderPrepareModelResults,
|
buildReusableProviderPrepareModelResults,
|
||||||
|
|
|
||||||
|
|
@ -66,11 +66,11 @@ function getSourceLabel(source: DashboardRecentProjectSource): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectSourceBadge({
|
const ProjectSourceBadge = ({
|
||||||
source,
|
source,
|
||||||
}: {
|
}: {
|
||||||
source?: DashboardRecentProjectSource;
|
source?: DashboardRecentProjectSource;
|
||||||
}): React.JSX.Element | null {
|
}): React.JSX.Element | null => {
|
||||||
if (!source) {
|
if (!source) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +92,7 @@ function ProjectSourceBadge({
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export type CwdMode = 'project' | 'custom';
|
export type CwdMode = 'project' | 'custom';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,16 +54,16 @@ import {
|
||||||
} from '@renderer/utils/taskChangeRequest';
|
} from '@renderer/utils/taskChangeRequest';
|
||||||
import { linkifyTaskIdsInMarkdown, parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils';
|
import { linkifyTaskIdsInMarkdown, parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils';
|
||||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
import {
|
|
||||||
getTeamTaskWorkflowColumn,
|
|
||||||
isTeamTaskFinishedForDependency,
|
|
||||||
isTeamTaskNeedsFixActionable,
|
|
||||||
} from '@shared/utils/teamTaskState';
|
|
||||||
import {
|
import {
|
||||||
deriveTaskDisplayId,
|
deriveTaskDisplayId,
|
||||||
formatTaskDisplayLabel,
|
formatTaskDisplayLabel,
|
||||||
taskMatchesRef,
|
taskMatchesRef,
|
||||||
} from '@shared/utils/taskIdentity';
|
} from '@shared/utils/taskIdentity';
|
||||||
|
import {
|
||||||
|
getTeamTaskWorkflowColumn,
|
||||||
|
isTeamTaskFinishedForDependency,
|
||||||
|
isTeamTaskNeedsFixActionable,
|
||||||
|
} from '@shared/utils/teamTaskState';
|
||||||
import { format, formatDistanceToNow } from 'date-fns';
|
import { format, formatDistanceToNow } from 'date-fns';
|
||||||
import {
|
import {
|
||||||
AlignLeft,
|
AlignLeft,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { normalizePath } from '@renderer/utils/pathNormalize';
|
import { normalizePath } from '@renderer/utils/pathNormalize';
|
||||||
import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath';
|
import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath';
|
||||||
|
|
||||||
import type { ComboboxOption } from '@renderer/components/ui/combobox';
|
|
||||||
import type { DashboardRecentProjectSource } from '@features/recent-projects/contracts';
|
import type { DashboardRecentProjectSource } from '@features/recent-projects/contracts';
|
||||||
|
import type { ComboboxOption } from '@renderer/components/ui/combobox';
|
||||||
import type { Project } from '@shared/types';
|
import type { Project } from '@shared/types';
|
||||||
|
|
||||||
export interface ProjectPathProject extends Project {
|
export interface ProjectPathProject extends Project {
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,11 @@ import {
|
||||||
buildTaskChangeRequestOptions,
|
buildTaskChangeRequestOptions,
|
||||||
canDisplayTaskChangesForOptions,
|
canDisplayTaskChangesForOptions,
|
||||||
} from '@renderer/utils/taskChangeRequest';
|
} from '@renderer/utils/taskChangeRequest';
|
||||||
|
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
import {
|
import {
|
||||||
isTeamTaskFinishedForDependency,
|
isTeamTaskFinishedForDependency,
|
||||||
isTeamTaskNeedsFixActionable,
|
isTeamTaskNeedsFixActionable,
|
||||||
} from '@shared/utils/teamTaskState';
|
} from '@shared/utils/teamTaskState';
|
||||||
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
|
||||||
import {
|
import {
|
||||||
ArrowLeftFromLine,
|
ArrowLeftFromLine,
|
||||||
ArrowRightFromLine,
|
ArrowRightFromLine,
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import {
|
||||||
TASK_STATUS_LABELS,
|
TASK_STATUS_LABELS,
|
||||||
TASK_STATUS_STYLES,
|
TASK_STATUS_STYLES,
|
||||||
} from '@renderer/utils/memberHelpers';
|
} from '@renderer/utils/memberHelpers';
|
||||||
|
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
import {
|
import {
|
||||||
getTeamTaskWorkflowColumn,
|
getTeamTaskWorkflowColumn,
|
||||||
isTeamTaskNeedsFixActionable,
|
isTeamTaskNeedsFixActionable,
|
||||||
} from '@shared/utils/teamTaskState';
|
} from '@shared/utils/teamTaskState';
|
||||||
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
|
||||||
|
|
||||||
import type { TeamTaskWithKanban } from '@shared/types';
|
import type { TeamTaskWithKanban } from '@shared/types';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ import {
|
||||||
REVIEW_STATE_DISPLAY,
|
REVIEW_STATE_DISPLAY,
|
||||||
TASK_STATUS_LABELS,
|
TASK_STATUS_LABELS,
|
||||||
} from '@renderer/utils/memberHelpers';
|
} from '@renderer/utils/memberHelpers';
|
||||||
|
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
import {
|
import {
|
||||||
getTeamTaskWorkflowColumn,
|
getTeamTaskWorkflowColumn,
|
||||||
isTeamTaskNeedsFixActionable,
|
isTeamTaskNeedsFixActionable,
|
||||||
} from '@shared/utils/teamTaskState';
|
} from '@shared/utils/teamTaskState';
|
||||||
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
|
||||||
|
|
||||||
import type { TeamTaskWithKanban } from '@shared/types';
|
import type { TeamTaskWithKanban } from '@shared/types';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -779,7 +779,7 @@ function isQueuedOpenCodeLaunch(
|
||||||
|
|
||||||
// Only label lanes as queued before runtime evidence appears. Once the
|
// Only label lanes as queued before runtime evidence appears. Once the
|
||||||
// backend has any liveness signal, show the exact runtime state instead.
|
// backend has any liveness signal, show the exact runtime state instead.
|
||||||
return runtimeEntry == null || runtimeEntry.livenessKind == null;
|
return runtimeEntry?.livenessKind == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasElapsedSinceIso(
|
function hasElapsedSinceIso(
|
||||||
|
|
|
||||||
|
|
@ -1133,8 +1133,8 @@ export interface RetryFailedOpenCodeSecondaryLanesResult {
|
||||||
attempted: string[];
|
attempted: string[];
|
||||||
confirmed: string[];
|
confirmed: string[];
|
||||||
pending: string[];
|
pending: string[];
|
||||||
failed: Array<{ memberName: string; error: string }>;
|
failed: { memberName: string; error: string }[];
|
||||||
skipped: Array<{ memberName: string; reason: string }>;
|
skipped: { memberName: string; reason: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MemberSpawnLivenessSource = 'heartbeat' | 'process';
|
export type MemberSpawnLivenessSource = 'heartbeat' | 'process';
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ async function writeRollout(
|
||||||
source?: string;
|
source?: string;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
branch?: string;
|
branch?: string;
|
||||||
|
metadataPadding?: string;
|
||||||
},
|
},
|
||||||
mtime: Date
|
mtime: Date
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
@ -43,6 +44,9 @@ async function writeRollout(
|
||||||
cwd: payload.cwd,
|
cwd: payload.cwd,
|
||||||
source: payload.source ?? 'cli',
|
source: payload.source ?? 'cli',
|
||||||
git: payload.branch ? { branch: payload.branch } : undefined,
|
git: payload.branch ? { branch: payload.branch } : undefined,
|
||||||
|
...(payload.metadataPadding
|
||||||
|
? { base_instructions: { text: payload.metadataPadding } }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
})}\n${'x'.repeat(1024)}`,
|
})}\n${'x'.repeat(1024)}`,
|
||||||
'utf8'
|
'utf8'
|
||||||
|
|
@ -110,6 +114,40 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => {
|
||||||
expect(identityResolver.resolve).toHaveBeenCalledWith('/Users/test/projects/alpha');
|
expect(identityResolver.resolve).toHaveBeenCalledWith('/Users/test/projects/alpha');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('loads Codex projects from large session metadata lines without parsing the full line', async () => {
|
||||||
|
const codexHome = path.join(tempDir, '.codex');
|
||||||
|
const logger = createLogger();
|
||||||
|
const identityResolver = {
|
||||||
|
resolve: vi.fn().mockResolvedValue(null),
|
||||||
|
} as unknown as RecentProjectIdentityResolver;
|
||||||
|
const updatedAt = new Date('2026-04-14T12:00:00.000Z');
|
||||||
|
await writeRollout(
|
||||||
|
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-large.jsonl'),
|
||||||
|
{
|
||||||
|
cwd: '/Users/test/projects/large',
|
||||||
|
metadataPadding: 'x'.repeat(160_000),
|
||||||
|
},
|
||||||
|
updatedAt
|
||||||
|
);
|
||||||
|
|
||||||
|
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
|
||||||
|
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
|
||||||
|
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
|
||||||
|
identityResolver,
|
||||||
|
logger,
|
||||||
|
codexHome,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await adapter.list();
|
||||||
|
|
||||||
|
expect(result.candidates).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
primaryPath: '/Users/test/projects/large',
|
||||||
|
sourceKind: 'codex',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('deduplicates sessions by cwd and keeps the newest activity', async () => {
|
it('deduplicates sessions by cwd and keeps the newest activity', async () => {
|
||||||
const codexHome = path.join(tempDir, '.codex');
|
const codexHome = path.join(tempDir, '.codex');
|
||||||
const logger = createLogger();
|
const logger = createLogger();
|
||||||
|
|
|
||||||
|
|
@ -164,14 +164,16 @@ describe('GraphMemberLogPreviewHud', () => {
|
||||||
expect(row).not.toBeUndefined();
|
expect(row).not.toBeUndefined();
|
||||||
expect(row?.querySelector('.float-left')).not.toBeNull();
|
expect(row?.querySelector('.float-left')).not.toBeNull();
|
||||||
expect(row?.querySelector('.line-clamp-3')).toBeNull();
|
expect(row?.querySelector('.line-clamp-3')).toBeNull();
|
||||||
expect(row?.className).toContain('h-16');
|
expect(row?.className).toContain('h-[68px]');
|
||||||
expect(row?.querySelector('span.text-slate-200')?.className).toContain('leading-4');
|
expect(row?.querySelector('span.text-slate-200')?.className).toContain('leading-[18px]');
|
||||||
expect(row?.textContent).toContain('pnpm test');
|
expect(row?.textContent).toContain('pnpm test');
|
||||||
|
|
||||||
const errorRow = Array.from(host.querySelectorAll('button')).find((button) =>
|
const errorRow = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||||
button.textContent?.includes('OpenCode tool failed')
|
button.textContent?.includes('OpenCode tool failed')
|
||||||
);
|
);
|
||||||
|
expect(errorRow?.className).toContain('border-rose-400/35');
|
||||||
expect(errorRow?.querySelector('svg.text-rose-300')).not.toBeNull();
|
expect(errorRow?.querySelector('svg.text-rose-300')).not.toBeNull();
|
||||||
|
expect(errorRow?.querySelector('.text-rose-100')).not.toBeNull();
|
||||||
|
|
||||||
const resultRow = Array.from(host.querySelectorAll('button')).find((button) =>
|
const resultRow = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||||
button.textContent?.includes('Tests passed')
|
button.textContent?.includes('Tests passed')
|
||||||
|
|
@ -203,6 +205,73 @@ describe('GraphMemberLogPreviewHud', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('caps long visible rows while preserving the full preview in the title', async () => {
|
||||||
|
const node: GraphNode = {
|
||||||
|
id: 'member:alpha-team:alice',
|
||||||
|
kind: 'member',
|
||||||
|
label: 'alice',
|
||||||
|
state: 'active',
|
||||||
|
domainRef: { kind: 'member', teamName: 'alpha-team', memberName: 'alice' },
|
||||||
|
};
|
||||||
|
const longPreview =
|
||||||
|
'to team-lead inbox - #68a3a8cc blocked by dependencies and needs a follow-up investigation before merge final-token';
|
||||||
|
const alicePreview = basePreviewsByMember.get('alice')!;
|
||||||
|
mockedPreviewsByMember = new Map(basePreviewsByMember);
|
||||||
|
mockedPreviewsByMember.set('alice', {
|
||||||
|
...alicePreview,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'preview-long',
|
||||||
|
kind: 'tool_use' as const,
|
||||||
|
provider: 'claude_transcript' as const,
|
||||||
|
timestamp: '2026-04-03T00:00:00.000Z',
|
||||||
|
title: 'Send message',
|
||||||
|
preview: longPreview,
|
||||||
|
tone: 'warning' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
overflowCount: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
<GraphMemberLogPreviewHud
|
||||||
|
teamName="alpha-team"
|
||||||
|
nodes={[node]}
|
||||||
|
getLogWorldRect={() => ({
|
||||||
|
left: 40,
|
||||||
|
top: 80,
|
||||||
|
right: 300,
|
||||||
|
bottom: 372,
|
||||||
|
width: 260,
|
||||||
|
height: 292,
|
||||||
|
})}
|
||||||
|
getCameraZoom={() => 1}
|
||||||
|
worldToScreen={(x, y) => ({ x, y })}
|
||||||
|
getViewportSize={() => ({ width: 1200, height: 800 })}
|
||||||
|
focusNodeIds={null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const row = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||||
|
button.textContent?.includes('Send message')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(row?.textContent).toContain('...');
|
||||||
|
expect(row?.textContent).not.toContain('final-token');
|
||||||
|
expect(row?.getAttribute('title')).toContain('final-token');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('briefly highlights a newly appeared preview row', async () => {
|
it('briefly highlights a newly appeared preview row', async () => {
|
||||||
const node: GraphNode = {
|
const node: GraphNode = {
|
||||||
id: 'member:alpha-team:alice',
|
id: 'member:alpha-team:alice',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue