fix(ci): apply frontend validation fixes

This commit is contained in:
777genius 2026-05-07 18:43:37 +03:00
parent 30a6e36976
commit 9d419626ef
42 changed files with 621 additions and 170 deletions

View file

@ -25,11 +25,11 @@ import {
} from '@shared/utils/idleNotificationSemantics';
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
import { isLeadMember } from '@shared/utils/leadDetection';
import { buildOrderedVisibleTeamGraphOwnerIds } from '@shared/utils/teamGraphDefaultLayout';
import {
isTeamTaskActivelyWorked,
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import { buildOrderedVisibleTeamGraphOwnerIds } from '@shared/utils/teamGraphDefaultLayout';
import {
buildInlineActivityEntries,

View file

@ -28,6 +28,8 @@ import type {
const LOG_PREVIEW_FALLBACK_WIDTH = 260;
const LOG_PREVIEW_FALLBACK_HEIGHT = 292;
const NEW_LOG_HIGHLIGHT_MS = 1_000;
const COMPACT_ROW_TEXT_LIMIT = 118;
const COMPACT_ROW_MIN_PREVIEW_LIMIT = 48;
interface StableRectLike {
left: number;
@ -155,6 +157,18 @@ function compactPreviewText(item: MemberLogPreviewItem, displayTitle: string): s
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 {
shell.style.opacity = '0';
shell.style.pointerEvents = 'none';
@ -405,42 +419,53 @@ export const GraphMemberLogPreviewHud = ({
(memberName: string, item: MemberLogPreviewItem) => {
const relativeTime = formatRelativeTime(item.timestamp);
const displayTitle = compactDisplayTitle(item);
const previewText = compactPreviewText(item, displayTitle);
const fullPreviewText = compactPreviewText(item, displayTitle);
const previewText = truncateCompactRowPreview(fullPreviewText, displayTitle, relativeTime);
const titleText = relativeTime
? `${displayTitle} ${relativeTime} ${previewText}`
: `${displayTitle} ${previewText}`;
? `${displayTitle} ${relativeTime} ${fullPreviewText}`
: `${displayTitle} ${fullPreviewText}`;
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 (
<button
key={item.id}
type="button"
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)]',
isHighlighted
? '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)]',
'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',
rowStateClassName,
].join(' ')}
title={titleText}
onClick={() => openLogs(memberName)}
>
<span
className="float-left mr-2 inline-flex size-5 shrink-0 items-center justify-center rounded bg-white/5 align-top"
aria-hidden="true"
>
<span className={iconClassName} aria-hidden="true">
{itemIcon(item)}
</span>
<span className="align-top text-[11px] font-medium leading-4 text-slate-200">
{displayTitle}
</span>
{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 className={headerClassName}>
<span className={titleClassName}>{displayTitle}</span>
{relativeTime ? <span className={timeClassName}>{relativeTime}</span> : null}
</span>
<span className={previewClassName}>{previewText}</span>
</button>
);
},
@ -494,7 +519,7 @@ export const GraphMemberLogPreviewHud = ({
) : (
<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)}
>
{resolveEmptyText(preview, loading, error)}

View file

@ -13,8 +13,8 @@ import {
buildMemberAvatarMap,
buildMemberLaunchPresentation,
} from '@renderer/utils/memberHelpers';
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
import { buildTeamProvisioningPresentation } from '@renderer/utils/teamProvisioningPresentation';
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
import { ExternalLink, Loader2, MessageSquare, Plus, User } from 'lucide-react';
import { isTaskInReviewCycle, resolveTaskReviewer } from '../../core/domain/taskGraphSemantics';

View file

@ -53,6 +53,48 @@ describe('memberLogPreviewExtractor', () => {
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', () => {
const result = extractMemberLogPreviewItems({
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', () => {
const result = extractMemberLogPreviewItems({
provider: 'claude_transcript',

View file

@ -21,8 +21,10 @@ export interface MemberLogPreviewParsedMessage {
content: string | MemberLogPreviewContentBlock[];
isMeta?: boolean;
toolCalls?: readonly {
id: string;
name: string;
id?: string;
name?: string;
callId?: string;
toolName?: string;
input?: unknown;
isTask?: boolean;
}[];
@ -107,6 +109,20 @@ interface ToolUseContext {
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 {
const time = value instanceof Date ? value.getTime() : Date.parse(value);
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;
}
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>): {
payload: Record<string, unknown>;
wrapperKey?: string;
@ -256,6 +284,127 @@ function recordFromUnknown(value: unknown): Record<string, unknown> | 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(
record: Record<string, unknown>,
keys: readonly string[]
@ -721,18 +870,8 @@ function formatRuntimePayload(
}
function formatErrorPayload(payload: Record<string, unknown>): KnownPayloadPreview | null {
const error =
stringField(payload, 'error') ??
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' };
if (unknownPayloadLooksLikeError(payload)) {
return { title: 'Tool error', text: payloadErrorMessage(payload) ?? 'Tool reported failure' };
}
return null;
}
@ -1059,16 +1198,7 @@ function extractTextPreview(
content: string | MemberLogPreviewContentBlock[],
textLimit: number
): { preview: string; truncated: boolean } | null {
if (typeof content === 'string') {
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 text = textFromPreviewContent(content);
const preview = truncatePreview(text, textLimit);
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(
messages: readonly MemberLogPreviewParsedMessage[]
): Map<string, ToolUseContext> {
const contexts = new Map<string, ToolUseContext>();
const addContext = (tool: { id: string; name: string; input?: unknown }): void => {
const id = tool.id.trim();
const addContext = (tool: NormalizedToolCall): void => {
const id = tool.id;
if (!id || contexts.has(id)) return;
contexts.set(id, {
id,
@ -1220,7 +1373,12 @@ function buildToolUseContexts(
};
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;
message.content.forEach((block) => {
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)) {
input.message.content.forEach((block, index) => {
if (!isToolUseBlock(block)) return;
@ -1527,6 +1690,10 @@ export function extractMemberLogPreviewItems(
if (role === 'assistant') {
const textPreview = extractTextPreview(message.content, textLimit);
if (textPreview) {
const runtimeErrorPreview = formatRuntimeErrorText(
textFromPreviewContent(message.content),
textLimit
);
candidates.push(
buildCandidate({
provider: input.provider,
@ -1535,14 +1702,14 @@ export function extractMemberLogPreviewItems(
messageIndex,
blockIndex: 10,
kind: 'text',
title: 'Assistant',
preview: textPreview.preview,
tone: 'neutral',
title: runtimeErrorPreview?.title ?? 'Assistant',
preview: runtimeErrorPreview?.preview ?? textPreview.preview,
tone: runtimeErrorPreview ? 'error' : 'neutral',
sourceLabel: input.sourceLabel,
sessionId: input.sessionId ?? message.sessionId,
laneId: input.laneId,
token: 'assistant-text',
textTruncated: textPreview.truncated,
textTruncated: runtimeErrorPreview?.truncated ?? textPreview.truncated,
})
);
}

View file

@ -1,4 +1,5 @@
import { decideMemberWorkSyncStatus } from '../domain';
import { finalizeMemberWorkSyncAgenda } from './MemberWorkSyncReconciler';
import type { MemberWorkSyncStatus, MemberWorkSyncStatusRequest } from '../../contracts';

View file

@ -1,7 +1,8 @@
import { decideMemberWorkSyncStatus } from '../domain';
import { appendMemberWorkSyncAudit, reasonToAuditEvent } from './MemberWorkSyncAudit';
import { decideMemberWorkSyncNudgeActivation } from './MemberWorkSyncNudgeActivationPolicy';
import { finalizeMemberWorkSyncAgenda } from './MemberWorkSyncReconciler';
import { decideMemberWorkSyncStatus } from '../domain';
import type { MemberWorkSyncOutboxItem, MemberWorkSyncStatus } from '../../contracts';
import type { MemberWorkSyncAuditEventName, MemberWorkSyncUseCaseDeps } from './ports';

View file

@ -12,8 +12,8 @@ import {
RuntimeTurnSettledIngestor,
type RuntimeTurnSettledTargetResolverPort,
} from '../../core/application';
import { MemberWorkSyncTeamChangeRouter } from '../adapters/input/MemberWorkSyncTeamChangeRouter';
import { MemberWorkSyncTaskImpactResolver } from '../adapters/input/MemberWorkSyncTaskImpactResolver';
import { MemberWorkSyncTeamChangeRouter } from '../adapters/input/MemberWorkSyncTeamChangeRouter';
import { TeamInboxMemberWorkSyncNudgeSink } from '../adapters/output/TeamInboxMemberWorkSyncNudgeSink';
import { TeamRuntimeTurnSettledTargetResolver } from '../adapters/output/TeamRuntimeTurnSettledTargetResolver';
import { TeamTaskAgendaSource } from '../adapters/output/TeamTaskAgendaSource';

View file

@ -1,8 +1,6 @@
import { createReadStream } from 'node:fs';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import readline from 'node:readline';
import { normalizeIdentityPath } from '@features/recent-projects/main/infrastructure/identity/normalizeIdentityPath';
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_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_METADATA_READ_LIMIT_BYTES = 128 * 1024;
interface CodexSessionFileEntry {
filePath: string;
@ -45,6 +44,14 @@ interface CodexSessionProjectSnapshot {
branchName?: string;
}
interface CodexSessionMetadata {
cwd: string;
source: unknown;
payloadTimestamp?: unknown;
eventTimestamp?: unknown;
branchName?: string;
}
function isInteractiveSource(source: unknown): boolean {
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');
}
async function readFirstLine(filePath: string): Promise<string | null> {
const stream = createReadStream(filePath, { encoding: 'utf8' });
const lines = readline.createInterface({
input: stream,
crlfDelay: Infinity,
});
function extractJsonStringField(input: string, fieldName: string): string {
const pattern = new RegExp(`"${fieldName}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`);
const match = pattern.exec(input);
if (!match) return '';
try {
for await (const line of lines) {
return line;
}
return null;
return JSON.parse(`"${match[1]}"`) as string;
} catch {
return '';
}
}
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 {
return null;
} finally {
lines.close();
stream.destroy();
await handle?.close().catch(() => undefined);
}
}
@ -130,28 +161,36 @@ function parseSessionSnapshot(
firstLine: string,
mtimeMs: number
): CodexSessionProjectSnapshot | null {
let event: CodexSessionEvent;
let metadata: CodexSessionMetadata | null = null;
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 {
return null;
metadata = parseSessionMetadataPrefix(firstLine);
}
const cwd = typeof event.payload?.cwd === 'string' ? event.payload.cwd.trim() : '';
if (!cwd || !isInteractiveSource(event.payload?.source) || isEphemeralProjectPath(cwd)) {
const cwd = metadata?.cwd ?? '';
if (!metadata || !cwd || !isInteractiveSource(metadata.source) || isEphemeralProjectPath(cwd)) {
return null;
}
const timestamp =
mtimeMs || normalizeTimestamp(event.payload?.timestamp) || normalizeTimestamp(event.timestamp);
const branchName =
typeof event.payload?.git?.branch === 'string' ? event.payload.git.branch.trim() : '';
mtimeMs ||
normalizeTimestamp(metadata.payloadTimestamp) ||
normalizeTimestamp(metadata.eventTimestamp);
return {
cwd,
source: event.payload?.source,
source: metadata.source,
lastActivityAt: timestamp,
branchName: branchName || undefined,
branchName: metadata.branchName || undefined,
};
}

View file

@ -356,10 +356,7 @@ export class TeamConfigReader {
}
const existingRequest = TeamConfigReader.listTeamsInFlightByBasePath.get(teamsBasePath);
if (
existingRequest &&
existingRequest.generationAtStart === TeamConfigReader.listTeamsGeneration
) {
if (existingRequest?.generationAtStart === TeamConfigReader.listTeamsGeneration) {
return cloneTeamSummaries(await existingRequest.promise);
}

View file

@ -58,11 +58,11 @@ import { TeamMembersMetaStore } from './TeamMembersMetaStore';
import { TeamMessageFeedService } from './TeamMessageFeedService';
import { TeamMetaStore } from './TeamMetaStore';
import { TeamSentMessagesStore } from './TeamSentMessagesStore';
import { getTeamTaskWorkflowColumn, selectCurrentActiveTeamTask } from './teamTaskActiveState';
import { TeamTaskCommentNotificationJournal } from './TeamTaskCommentNotificationJournal';
import { TeamTaskReader } from './TeamTaskReader';
import { TeamTaskWriter } from './TeamTaskWriter';
import { TeamTranscriptProjectResolver } from './TeamTranscriptProjectResolver';
import { getTeamTaskWorkflowColumn, selectCurrentActiveTeamTask } from './teamTaskActiveState';
import type { PersistedTaskChangePresenceIndex } from './cache/taskChangePresenceCacheTypes';
import type { TaskChangePresenceRepository } from './cache/TaskChangePresenceRepository';

View file

@ -1,9 +1,7 @@
import { createLogger } from '@shared/utils/logger';
import { getTeamsBasePath } from '@main/utils/pathDecoder';
import { createLogger } from '@shared/utils/logger';
import * as fs from 'fs/promises';
import { TeamInboxReader } from './TeamInboxReader';
import { TeamMemberLogsFinder } from './TeamMemberLogsFinder';
import {
createOpenCodePromptDeliveryLedgerStore,
type OpenCodePromptDeliveryLedgerRecord,
@ -13,6 +11,8 @@ import {
getOpenCodeLaneScopedRuntimeFilePath,
readOpenCodeRuntimeLaneIndex,
} from './opencode/store/OpenCodeRuntimeManifestEvidenceReader';
import { TeamInboxReader } from './TeamInboxReader';
import { TeamMemberLogsFinder } from './TeamMemberLogsFinder';
import type { MemberLogSummary, MemberRuntimeAdvisory, ResolvedTeamMember } from '@shared/types';

View file

@ -451,7 +451,7 @@ export class TeamMessageFeedService {
const existingRequest = this.inFlightByTeam.get(teamName);
const generationAtStart = this.getGeneration(teamName);
if (existingRequest && existingRequest.generationAtStart === generationAtStart) {
if (existingRequest?.generationAtStart === generationAtStart) {
return existingRequest.promise;
}

View file

@ -84,12 +84,6 @@ import { createLogger } from '@shared/utils/logger';
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import {
getTeamTaskWorkflowColumn,
isTeamTaskActivelyWorked,
isTeamTaskDeleted,
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import {
isTeamInternalControlMessageText,
stripExactInternalControlEchoPrefix,
@ -104,6 +98,12 @@ import {
inferTeamProviderIdFromModel,
normalizeOptionalTeamProviderId,
} from '@shared/utils/teamProvider';
import {
getTeamTaskWorkflowColumn,
isTeamTaskActivelyWorked,
isTeamTaskDeleted,
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import {
extractToolPreview,
extractToolResultPreview,
@ -169,10 +169,6 @@ import {
type OpenCodePromptDeliveryLedgerStore,
type OpenCodePromptDeliveryStatus,
} from './opencode/delivery/OpenCodePromptDeliveryLedger';
import {
isActionRequiredOpenCodeRuntimeDeliveryReason,
selectOpenCodeRuntimeDeliveryReason,
} from './opencode/delivery/OpenCodeRuntimeDeliveryDiagnostics';
import {
decideOpenCodePromptDeliveryRepair,
type OpenCodePromptDeliveryHardFailureKind,
@ -189,6 +185,10 @@ import {
OPENCODE_PROMPT_WATCHDOG_PER_TEAM_CONCURRENCY,
type OpenCodeVisibleReplyProof,
} from './opencode/delivery/OpenCodePromptDeliveryWatchdog';
import {
isActionRequiredOpenCodeRuntimeDeliveryReason,
selectOpenCodeRuntimeDeliveryReason,
} from './opencode/delivery/OpenCodeRuntimeDeliveryDiagnostics';
import { createRuntimeDeliveryJournalStore } from './opencode/delivery/RuntimeDeliveryJournal';
import {
type RuntimeDeliveryDestinationPort,

View file

@ -468,10 +468,7 @@ export class TeamTaskReader {
return cloned;
}
if (
TeamTaskReader.allTasksInFlight &&
TeamTaskReader.allTasksInFlight.generationAtStart === TeamTaskReader.allTasksGeneration
) {
if (TeamTaskReader.allTasksInFlight?.generationAtStart === TeamTaskReader.allTasksGeneration) {
const waitedAt = Date.now();
const tasks = await TeamTaskReader.allTasksInFlight.promise;
const cloned = cloneTasks(tasks);

View file

@ -1,12 +1,13 @@
import {
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
} from './OpenCodeBridgeCommandContract';
import type {
OpenCodeBridgeCommandName,
OpenCodeBridgeHandshake,
OpenCodeBridgePeerIdentity,
} from './OpenCodeBridgeCommandContract';
import {
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
} from './OpenCodeBridgeCommandContract';
import type {
OpenCodeBridgeCommandExecutor,
OpenCodeBridgeHandshakePort,

View file

@ -15,13 +15,13 @@ import {
validateRuntimeStoreManifest,
} from './RuntimeStoreManifest';
import type { RuntimeStoreManifestEvidence } from '../bridge/OpenCodeBridgeCommandContract';
import type { RuntimeStoreManifestReader } from '../bridge/OpenCodeStateChangingBridgeCommandService';
import type { RuntimeStoreManifestEntryState } from './RuntimeStoreManifest';
import type {
OpenCodeAppManagedBootstrapCandidate,
OpenCodeBootstrapEvidenceSource,
} 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');

View file

@ -596,7 +596,7 @@ function normalizeAppManagedBootstrapCandidate(
runtimeSessionId?: string;
}
): OpenCodeAppManagedBootstrapCandidate | undefined {
if (!value || value.schemaVersion !== 1 || value.source !== 'app_managed_bootstrap') {
if (value?.schemaVersion !== 1 || value.source !== 'app_managed_bootstrap') {
return undefined;
}
if (

View file

@ -9,8 +9,8 @@ import { TeamTranscriptSourceLocator } from '../taskLogs/discovery/TeamTranscrip
import { isBoardTaskExactLogsReadEnabled } from '../taskLogs/exact/featureGates';
import { TeamKanbanManager } from '../TeamKanbanManager';
import { TeamMembersMetaStore } from '../TeamMembersMetaStore';
import { TeamTaskReader } from '../TeamTaskReader';
import { getTeamTaskWorkflowColumn, isTeamTaskActivelyWorked } from '../teamTaskActiveState';
import { TeamTaskReader } from '../TeamTaskReader';
import { BoardTaskActivityBatchIndexer } from './BoardTaskActivityBatchIndexer';
import { OpenCodeTaskStallEvidenceSource } from './OpenCodeTaskStallEvidenceSource';

View file

@ -44,12 +44,12 @@ export class BoardTaskActivityRecordSource {
private async getTaskActivityIndex(teamName: string): Promise<TaskActivityIndex> {
const generation = this.getTranscriptDiscoveryGeneration(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;
}
const existingInFlight = this.indexInFlight.get(teamName);
if (existingInFlight && existingInFlight.generation === generation) {
if (existingInFlight?.generation === generation) {
return await existingInFlight.promise;
}

View file

@ -95,12 +95,12 @@ export class TeamTranscriptSourceLocator {
const generation = this.getGeneration(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;
}
const inFlight = this.contextInFlight.get(teamName);
if (inFlight && inFlight.generation === generation) {
if (inFlight?.generation === generation) {
return await inFlight.promise;
}

View file

@ -214,7 +214,7 @@ export class CodexNativeTraceProjector {
>();
for (const event of run.events) {
const projection = event.projection;
if (!projection || projection.toolSource !== 'native') {
if (projection?.toolSource !== 'native') {
continue;
}
if (!projection.itemId) {

View file

@ -4,8 +4,8 @@ import type { TeamTaskWithKanban } from '@shared/types';
export {
getTeamTaskWorkflowColumn,
isTeamTaskFinalForCompletionNotification,
isTeamTaskActivelyWorked,
isTeamTaskFinalForCompletionNotification,
isTeamTaskFinishedForDependency,
isTeamTaskNeedsFixActionable,
isTeamTaskTerminalForActionableWork,

View file

@ -19,6 +19,7 @@ import {
import { api, isElectronMode } from '@renderer/api';
import { confirm } from '@renderer/components/common/ConfirmDialog';
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton';
import {
formatProviderStatusText,
getProviderConnectionModeSummary,
@ -29,7 +30,6 @@ import {
isConnectionManagedRuntimeProvider,
shouldShowProviderConnectAction,
} from '@renderer/components/runtime/providerConnectionUi';
import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton';
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
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 {
return (
providerLoading ||
@ -781,7 +766,6 @@ const InstalledBanner = ({
? getProviderLabel(provider.providerId)
: provider.displayName}
</span>
{provider.providerId === 'opencode' ? <OpenCodeBetaBadge /> : null}
</span>
<span
className="text-xs"

View file

@ -8,11 +8,11 @@ interface CodexLoginLinkCopyButtonProps {
size?: 'xs' | 'sm';
}
export function CodexLoginLinkCopyButton({
export const CodexLoginLinkCopyButton = ({
authUrl,
disabled = false,
size = 'sm',
}: CodexLoginLinkCopyButtonProps): React.JSX.Element | null {
}: CodexLoginLinkCopyButtonProps): React.JSX.Element | null => {
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>('idle');
useEffect(() => {
@ -53,4 +53,4 @@ export function CodexLoginLinkCopyButton({
{copyState === 'copied' ? 'Copied' : copyState === 'failed' ? 'Copy failed' : 'Copy link'}
</button>
);
}
};

View file

@ -20,8 +20,9 @@ import {
resolveCodexRuntimeSelection,
} from '@features/codex-runtime-profile/renderer';
import { RuntimeProviderManagementPanel } from '@features/runtime-provider-management/renderer';
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
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 {
Dialog,
@ -40,7 +41,6 @@ import {
SelectValue,
} from '@renderer/components/ui/select';
import { Tabs, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
import { CodexLoginLinkCopyButton } from '@renderer/components/runtime/CodexLoginLinkCopyButton';
import { useStore } from '@renderer/store';
import { AlertTriangle, Key, Link2, Loader2, Trash2 } from 'lucide-react';

View file

@ -7,11 +7,11 @@ import { useStore } from '@renderer/store';
import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice';
import { buildMemberColorMap, REVIEW_STATE_DISPLAY } from '@renderer/utils/memberHelpers';
import { linkifyTaskIdsInMarkdown } from '@renderer/utils/taskReferenceUtils';
import { formatTaskDisplayLabel, taskMatchesRef } from '@shared/utils/taskIdentity';
import {
getTeamTaskWorkflowColumn,
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import { formatTaskDisplayLabel, taskMatchesRef } from '@shared/utils/taskIdentity';
import { useShallow } from 'zustand/react/shallow';
import type { TeamTaskWithKanban } from '@shared/types';

View file

@ -63,8 +63,8 @@ import {
getKnownSlashCommand,
parseStandaloneSlashCommand,
} from '@shared/utils/slashCommands';
import { isTaskStallRemediationMessage } from '@shared/utils/teamAutomationMessages';
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import { isTaskStallRemediationMessage } from '@shared/utils/teamAutomationMessages';
import {
AlertTriangle,
Check,

View file

@ -59,7 +59,7 @@ export function shouldShowCodexReconnectPrompt({
);
}
export function CodexReconnectPrompt({
export const CodexReconnectPrompt = ({
authUrl,
reconnectBusy,
onReconnect,
@ -67,7 +67,7 @@ export function CodexReconnectPrompt({
authUrl: string | null;
reconnectBusy: boolean;
onReconnect: () => void;
}): React.JSX.Element {
}): React.JSX.Element => {
return (
<div
className="mt-2 rounded-md border px-2.5 py-2"
@ -98,4 +98,4 @@ export function CodexReconnectPrompt({
</div>
</div>
);
}
};

View file

@ -28,8 +28,8 @@ import {
extractTaskRefsFromText,
stripEncodedTaskReferenceMetadata,
} from '@renderer/utils/taskReferenceUtils';
import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState';
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState';
import { AlertTriangle, ChevronDown, ChevronRight, Search } from 'lucide-react';
import type { InlineChip } from '@renderer/types/inlineChip';

View file

@ -88,15 +88,15 @@ import { AlertTriangle, CheckCircle2, Info, Loader2, X } from 'lucide-react';
import { AdvancedCliSection } from './AdvancedCliSection';
import { AnthropicFastModeSelector } from './AnthropicFastModeSelector';
import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt';
import { CodexFastModeSelector } from './CodexFastModeSelector';
import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt';
import {
clearInheritedMemberModelsUnavailableForProvider,
resolveProviderScopedMemberModel,
} from './memberModelScope';
import { OptionalSettingsSection } from './OptionalSettingsSection';
import { ProjectPathSelector } from './ProjectPathSelector';
import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects';
import { ProjectPathSelector } from './ProjectPathSelector';
import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey';
import {
buildReusableProviderPrepareModelResults,

View file

@ -91,8 +91,8 @@ import { CronScheduleInput } from '../schedule/CronScheduleInput';
import { AdvancedCliSection } from './AdvancedCliSection';
import { AnthropicFastModeSelector } from './AnthropicFastModeSelector';
import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt';
import { CodexFastModeSelector } from './CodexFastModeSelector';
import { CodexReconnectPrompt, shouldShowCodexReconnectPrompt } from './CodexReconnectPrompt';
import { EffortLevelSelector } from './EffortLevelSelector';
import { resolveLaunchDialogPrefill } from './launchDialogPrefill';
import {
@ -100,8 +100,8 @@ import {
resolveProviderScopedMemberModel,
} from './memberModelScope';
import { OptionalSettingsSection } from './OptionalSettingsSection';
import { ProjectPathSelector } from './ProjectPathSelector';
import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects';
import { ProjectPathSelector } from './ProjectPathSelector';
import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey';
import {
buildReusableProviderPrepareModelResults,

View file

@ -66,11 +66,11 @@ function getSourceLabel(source: DashboardRecentProjectSource): string {
}
}
function ProjectSourceBadge({
const ProjectSourceBadge = ({
source,
}: {
source?: DashboardRecentProjectSource;
}): React.JSX.Element | null {
}): React.JSX.Element | null => {
if (!source) {
return null;
}
@ -92,7 +92,7 @@ function ProjectSourceBadge({
))}
</span>
);
}
};
export type CwdMode = 'project' | 'custom';

View file

@ -54,16 +54,16 @@ import {
} from '@renderer/utils/taskChangeRequest';
import { linkifyTaskIdsInMarkdown, parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils';
import { isLeadMember } from '@shared/utils/leadDetection';
import {
getTeamTaskWorkflowColumn,
isTeamTaskFinishedForDependency,
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import {
deriveTaskDisplayId,
formatTaskDisplayLabel,
taskMatchesRef,
} from '@shared/utils/taskIdentity';
import {
getTeamTaskWorkflowColumn,
isTeamTaskFinishedForDependency,
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import { format, formatDistanceToNow } from 'date-fns';
import {
AlignLeft,

View file

@ -1,8 +1,8 @@
import { normalizePath } from '@renderer/utils/pathNormalize';
import { isEphemeralProjectPath } from '@shared/utils/ephemeralProjectPath';
import type { ComboboxOption } from '@renderer/components/ui/combobox';
import type { DashboardRecentProjectSource } from '@features/recent-projects/contracts';
import type { ComboboxOption } from '@renderer/components/ui/combobox';
import type { Project } from '@shared/types';
export interface ProjectPathProject extends Project {

View file

@ -13,11 +13,11 @@ import {
buildTaskChangeRequestOptions,
canDisplayTaskChangesForOptions,
} from '@renderer/utils/taskChangeRequest';
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import {
isTeamTaskFinishedForDependency,
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import {
ArrowLeftFromLine,
ArrowRightFromLine,

View file

@ -7,11 +7,11 @@ import {
TASK_STATUS_LABELS,
TASK_STATUS_STYLES,
} from '@renderer/utils/memberHelpers';
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import {
getTeamTaskWorkflowColumn,
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import type { TeamTaskWithKanban } from '@shared/types';

View file

@ -5,11 +5,11 @@ import {
REVIEW_STATE_DISPLAY,
TASK_STATUS_LABELS,
} from '@renderer/utils/memberHelpers';
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import {
getTeamTaskWorkflowColumn,
isTeamTaskNeedsFixActionable,
} from '@shared/utils/teamTaskState';
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import type { TeamTaskWithKanban } from '@shared/types';

View file

@ -779,7 +779,7 @@ function isQueuedOpenCodeLaunch(
// Only label lanes as queued before runtime evidence appears. Once the
// backend has any liveness signal, show the exact runtime state instead.
return runtimeEntry == null || runtimeEntry.livenessKind == null;
return runtimeEntry?.livenessKind == null;
}
function hasElapsedSinceIso(

View file

@ -1133,8 +1133,8 @@ export interface RetryFailedOpenCodeSecondaryLanesResult {
attempted: string[];
confirmed: string[];
pending: string[];
failed: Array<{ memberName: string; error: string }>;
skipped: Array<{ memberName: string; reason: string }>;
failed: { memberName: string; error: string }[];
skipped: { memberName: string; reason: string }[];
}
export type MemberSpawnLivenessSource = 'heartbeat' | 'process';

View file

@ -28,6 +28,7 @@ async function writeRollout(
source?: string;
timestamp?: string;
branch?: string;
metadataPadding?: string;
},
mtime: Date
): Promise<void> {
@ -43,6 +44,9 @@ async function writeRollout(
cwd: payload.cwd,
source: payload.source ?? 'cli',
git: payload.branch ? { branch: payload.branch } : undefined,
...(payload.metadataPadding
? { base_instructions: { text: payload.metadataPadding } }
: {}),
},
})}\n${'x'.repeat(1024)}`,
'utf8'
@ -110,6 +114,40 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => {
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 () => {
const codexHome = path.join(tempDir, '.codex');
const logger = createLogger();

View file

@ -164,14 +164,16 @@ describe('GraphMemberLogPreviewHud', () => {
expect(row).not.toBeUndefined();
expect(row?.querySelector('.float-left')).not.toBeNull();
expect(row?.querySelector('.line-clamp-3')).toBeNull();
expect(row?.className).toContain('h-16');
expect(row?.querySelector('span.text-slate-200')?.className).toContain('leading-4');
expect(row?.className).toContain('h-[68px]');
expect(row?.querySelector('span.text-slate-200')?.className).toContain('leading-[18px]');
expect(row?.textContent).toContain('pnpm test');
const errorRow = Array.from(host.querySelectorAll('button')).find((button) =>
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('.text-rose-100')).not.toBeNull();
const resultRow = Array.from(host.querySelectorAll('button')).find((button) =>
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 () => {
const node: GraphNode = {
id: 'member:alpha-team:alice',