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
Part of #87.
Что не так
Когда фоновая Bash-команда (
run_in_background) завершается, Claude Code пишет user-строку<task-notification>…</task-notification>(promptSource: "system"; формат — из сессии 8b65639f, описанной в #70). Чат показывал её карточкой: значок статуса, имя команды, exit code, файл вывода. Рисует карточку толькоUserChatGroup.tsx, и только из user-групп.#70 правильно перестал считать такие строки ходами — теперь они попадают в AI-группу. Но у AI-группы нет элемента отображения для них, и карточка пропала:
d96b33a)41a52e3)Ревью #70 решило, что «display-side уже согласован», потому что
contentSanitizerвырезает<task-notification>. Но sanitizer вырезает только текст, а карточка рисовалась отдельно — из исходного содержимого.Как проверить (один раз, ~1 мин)
Из корня репозитория:
Правильно: уведомление видно в чате карточкой (а ход при этом не открывается). Сейчас:
карточек фоновых задач: 0, у AI-групп нет элемента для уведомления, строкаВОСПРОИЗВЕДЕНО. Для сравнения: на коммите до #70 (git checkout d96b33a) тот же скрипт печатаетOK: карточка есть.Где в коде (гипотеза)
src/renderer/components/chat/UserChatGroup.tsx:354и:484— единственное место, где разбираются и рисуются карточки.src/renderer/utils/displayItemBuilder.ts:120— вbuildDisplayItemsнет ветки для<task-notification>.scripts/turn-accounting.mjs:93— тег вSYSTEM_OUTPUT_TAGS(изменение fix(turn): task-notification opens no user turn #70).Что сделать
Показывать
<task-notification>внутри AI-группы — отдельным элементом отображения или той же карточкой — не возвращая ход.Acceptance
<task-notification>— лишнего хода нет, есть один элемент со статусом и exit code