Skip to content

Background task cards disappeared from the chat after #70 #104

Description

@axisrow

Part of #87.

⚠️ Выводы не проверены человеком. Ишью заведено по аудиту, который сделал Claude Code. Перед исправлением достаточно один раз выполнить блок «Как проверить» и сравнить вывод с ожидаемым.

Что не так

Когда фоновая Bash-команда (run_in_background) завершается, Claude Code пишет user-строку <task-notification>…</task-notification> (promptSource: "system"; формат — из сессии 8b65639f, описанной в #70). Чат показывал её карточкой: значок статуса, имя команды, exit code, файл вывода. Рисует карточку только UserChatGroup.tsx, и только из user-групп.

#70 правильно перестал считать такие строки ходами — теперь они попадают в AI-группу. Но у AI-группы нет элемента отображения для них, и карточка пропала:

до #70 (d96b33a) после #70 (41a52e3)
ход лишний «Turn N» не открывается ✓
карточка фоновой задачи есть нет

Ревью #70 решило, что «display-side уже согласован», потому что contentSanitizer вырезает <task-notification>. Но sanitizer вырезает только текст, а карточка рисовалась отдельно — из исходного содержимого.

Как проверить (один раз, ~1 мин)

Из корня репозитория:

cat > /tmp/repro-tasknote.mts <<'EOF'
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const R = process.cwd(); // запускать из корня репозитория
const load = (p: string) => import(`${R}/${p}`);
let t = Date.parse('2026-10-01T07:00:00Z'), n = 0;
const at = (ms = 20_000) => new Date((t += ms)).toISOString();
const base = (x = {}) => ({ uuid: `u${++n}`, parentUuid: null, sessionId: 's', cwd: '/p', isSidechain: false, ...x });
const user = (content: unknown, x = {}, gap?: number) => ({ ...base(x), type: 'user', timestamp: at(gap), message: { role: 'user', content } });
const ai = (block: object, ctx: number, out: number, ids: { rid?: string; mid: string }, x = {}, gap?: number) => ({ ...base(x), type: 'assistant', timestamp: at(gap), ...(ids.rid ? { requestId: ids.rid } : {}), message: { id: ids.mid, role: 'assistant', model: 'claude-x', content: [block], usage: { input_tokens: 0, cache_read_input_tokens: ctx, cache_creation_input_tokens: 0, output_tokens: out } } });
const result = (id: string, gap?: number) => ({ ...base(), type: 'user', timestamp: at(gap), message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: id, content: 'ok' }] } });
const text = (s: string) => ({ type: 'text', text: s });
const tool = (id: string, name: string, input: object) => ({ type: 'tool_use', id, name, input });
const write = (lines: object[]) => { const f = join(mkdtempSync(join(tmpdir(), 'repro-')), 's.jsonl'); writeFileSync(f, lines.map((l) => JSON.stringify(l)).join('\n') + '\n'); return f; };
// фоновая Bash-команда завершилась: Claude Code пишет user-строку <task-notification> (promptSource: system)
const NOTE = '<task-notification>\n<task-id>bppnd267i</task-id>\n<status>completed</status>\n<summary>Background command "Fresh tarball download" completed (exit code 0)</summary>\n</task-notification>';
const f = write([
  user('Запусти сборку'),
  ai(tool('b1', 'Bash', { command: 'curl …', run_in_background: true }), 100_000, 50, { rid: 'q1', mid: 'm1' }), result('b1'),
  ai(text('Запустил в фоне'), 100_000, 50, { rid: 'q2', mid: 'm2' }),
  user(NOTE, { promptSource: 'system', turnOrigin: 'task_notification' }),
  ai(text('Сборка завершилась'), 100_000, 50, { rid: 'q3', mid: 'm3' }),
]);
const jsonl = await load('src/main/utils/jsonl.ts');
const { ChunkBuilder } = await load('src/main/services/analysis/ChunkBuilder.ts');
const { transformChunksToConversation } = await load('src/renderer/utils/groupTransformer.ts');
const { enhanceAIGroup } = await load('src/renderer/utils/aiGroupEnhancer.ts');
const { parseTaskNotifications } = await load('src/shared/utils/contentSanitizer.ts');
const items = transformChunksToConversation(new ChunkBuilder().buildChunks(await jsonl.parseJsonlFile(f), []), [], false).items;
const raw = (c: unknown) => (typeof c === 'string' ? c : '');
// карточку рисует только UserChatGroup (src/renderer/components/chat/UserChatGroup.tsx) — из user-групп
const cardsInUserGroups = items.filter((i: { type: string }) => i.type === 'user')
  .flatMap((i: { group: { message: { content: unknown } } }) => parseTaskNotifications(raw(i.group.message.content))).length;
const aiItems = items.filter((i: { type: string }) => i.type === 'ai')
  .flatMap((i: { group: object }) => enhanceAIGroup(i.group).displayItems.map((d: { type: string }) => d.type));
console.table({ 'user-групп': items.filter((i: { type: string }) => i.type === 'user').length, 'карточек фоновых задач (UserChatGroup)': cardsInUserGroups, 'элементы AI-групп': aiItems.join(', ') });
console.log(cardsInUserGroups === 0 ? 'ВОСПРОИЗВЕДЕНО: уведомление о фоновой задаче нигде не отрисуется (до #70 была карточка «✓ Fresh tarball download · exit 0»)' : 'OK: карточка есть');
EOF
pnpm exec tsx /tmp/repro-tasknote.mts

Правильно: уведомление видно в чате карточкой (а ход при этом не открывается). Сейчас: карточек фоновых задач: 0, у AI-групп нет элемента для уведомления, строка ВОСПРОИЗВЕДЕНО. Для сравнения: на коммите до #70 (git checkout d96b33a) тот же скрипт печатает OK: карточка есть.

Где в коде (гипотеза)

Что сделать

Показывать <task-notification> внутри AI-группы — отдельным элементом отображения или той же карточкой — не возвращая ход.

Acceptance

  • Тест на уровне элементов отображения: сессия с <task-notification> — лишнего хода нет, есть один элемент со статусом и exit code
  • Скриншот карточки в PR

  • Проверено человеком: воспроизводится / не воспроизводится
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions