Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/web/app/lib/daemon-images.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ describe('Markdown の画像(デーモンの添付)', () => {

expect(stub.calls).toEqual([]);
expect(createObjectURL).not.toHaveBeenCalled();
// remoteImages={false} は今までどおり文字とリンク
expect(screen.getByRole('link', { name: '画像: y' })).toBeTruthy();
});

Expand Down
1 change: 0 additions & 1 deletion apps/web/app/lib/daemon-images.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { DaemonImagesProvider } from '@alteroid/ui';
import type { DaemonImages } from '@alteroid/ui';

/**
* Markdown の画像の `src` が、いま接続しているデーモンの添付(`<base>/attachments/<id>`)なら id を返す。それ以外は `undefined`。
* 資格(Bearer)を送ってよいかの線はここだけに置く:
* - 絶対 URL に加え、`/attachments/<id>` と `attachments/<id>` の相対パスも、デーモンの base の下のものとして扱う:
* 取りに行く先は `src` ではなく接続中の base URL と id から組むので、資格がよそへ行く経路にならない。
Expand Down
8 changes: 0 additions & 8 deletions apps/web/app/routes/chat.follow-up-order.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,11 @@ import { json, Providers, sse, storeTestBaseUrl, stubFetch } from '~/test-suppor

import Chat from './chat';

// #4391: 返信の本文が流れ終わってもターンが続いているあいだに人間が続けて発言すると、その発言が返信より上に出ていた。
// デーモンは発言を記録するとき、それまでの返信を先に日誌へ書き、同じ同期区間で `queued` を流す。画面はその `queued` で返信の行を分ける。

const ID = 'conv-follow-up-order';
const QUESTION = '二つ目の質問';
const REPLY = 'AIの回答2の本文';
const FOLLOW = '回答2への返事';
const TAIL = '返事を受けた続き';
// 道具を挟んで再開した本文: 日誌では区切りの空行(#4339)を挟んで TAIL と1発言になる
const AFTER_TOOL = '道具の後の本文';

const ChatRoute = Chat as unknown as (props: {
Expand Down Expand Up @@ -80,7 +76,6 @@ describe.each([
{
label: '割るデーモン(#4391 以降)',
split: true,
// 発言の記録の前に、そこまでの返信が書かれる
whenQueued: [
question,
{ id: 'm2', at: '2026-10-10T01:00:19.000Z', role: 'outbound', text: REPLY },
Expand All @@ -105,7 +100,6 @@ describe.each([
whenDone: [
question,
follow,
// 追送の割り目には区切りが入らず、道具の境目にだけ入る
{
id: 'm4',
at: '2026-10-10T01:00:40.000Z',
Expand Down Expand Up @@ -193,7 +187,6 @@ describe.each([
expect(chatCalls).toBe(2);
});

// デーモンが追送を記録した: 返信の行が分かれ、履歴に追送が入る
messages = whenQueued;
queued.open();
recorded.open();
Expand All @@ -211,7 +204,6 @@ describe.each([
const shown = items();
expect(countOf(shown, TAIL)).toBe(1);
expect(countOf(shown, AFTER_TOOL)).toBe(1);
// 追送の後に流れた続きは、追送より下に出る
expect(indexOf(shown, FOLLOW)).toBeLessThan(indexOf(shown, TAIL));
});

Expand Down
6 changes: 0 additions & 6 deletions apps/web/app/routes/chat.reply-segment-separator.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ import { json, Providers, sse, storeTestBaseUrl, stubFetch } from '~/test-suppor

import Chat from './chat';

// デーモンは、1つのターンの中で本文が道具を挟んで再開したとき、日誌の本文の境目に空行を入れる(#4339)。
// 画面へ流れる SSE の text には空行が入らないので、pendingOwnLines は分かれた返信の行を
// 区切りあり(空行)と区切りなし(古いデーモン)の両方で日誌の1発言と照合する。どちらでも確定後に写しが末尾に残らない。

const ID = 'conv-tail';
const BEFORE_HEAD = '待ち時間を 35 秒ほどにすれば、必ず Gotenberg が先に切れて';
const BEFORE_TAIL = '、その理由を受け取れます。';
Expand Down Expand Up @@ -188,7 +184,6 @@ describe.each([
afterTool.open();
await screen.findByText(AFTER);

// 道具の前の行は、末尾まで書かれている
expect(items().some((text) => text.includes(BEFORE_HEAD + BEFORE_TAIL))).toBe(true);

replied = true;
Expand All @@ -199,7 +194,6 @@ describe.each([
const shown = items();
expect(shown.filter((text) => text.includes(AFTER))).toHaveLength(1);
expect(shown.filter((text) => text.includes(BEFORE_HEAD))).toHaveLength(1);
// 確定した1発言が最後に在り、手元の写しが後ろに居座らない
expect(shown.at(-1)).toContain(BEFORE_HEAD + BEFORE_TAIL);
expect(shown.at(-1)).toContain(AFTER);
});
Expand Down
7 changes: 0 additions & 7 deletions apps/web/app/routes/chat.reply-segment-tail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ import { json, Providers, sse, storeTestBaseUrl, stubFetch } from '~/test-suppor

import Chat from './chat';

// 道具の直前の本文の末尾と `tool` が同じ網の塊で届くと、末尾が画面の行に書かれないまま落ちていた。
// 行の本文が日誌の本文(ターンの本文を区切りなしで連結した1発言)と食い違い、pendingOwnLines の照合に永久に当たらず、
// 前半(途中で切れた版)と後半が会話の末尾に居座った(会話 82d90cd7 の返答 36291767、2026-10-09)。

const ID = 'conv-tail';
const BEFORE_HEAD = '待ち時間を 35 秒ほどにすれば、必ず Gotenberg が先に切れて';
const BEFORE_TAIL = '、その理由を受け取れます。';
Expand Down Expand Up @@ -149,7 +145,6 @@ describe('道具の直前の本文の末尾と tool が同じ塊で届く(会
id: 'm2',
at: '2026-10-09T08:27:32.000Z',
role: 'outbound',
// 日誌はターンの本文を区切りなしで連結した1発言として載せる
text: BEFORE_HEAD + BEFORE_TAIL + AFTER,
},
]
Expand Down Expand Up @@ -185,7 +180,6 @@ describe('道具の直前の本文の末尾と tool が同じ塊で届く(会
afterTool.open();
await screen.findByText(AFTER);

// 道具の前の行は、末尾まで書かれている
expect(items().some((text) => text.includes(BEFORE_HEAD + BEFORE_TAIL))).toBe(true);

replied = true;
Expand All @@ -196,7 +190,6 @@ describe('道具の直前の本文の末尾と tool が同じ塊で届く(会
const shown = items();
expect(shown.filter((text) => text.includes(AFTER))).toHaveLength(1);
expect(shown.filter((text) => text.includes(BEFORE_HEAD))).toHaveLength(1);
// 確定した1発言が最後に在り、手元の写しが後ろに居座らない
expect(shown.at(-1)).toContain(BEFORE_HEAD + BEFORE_TAIL + AFTER);
});
});
Expand Down
10 changes: 5 additions & 5 deletions apps/web/app/routes/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ interface Line {
turnFailure?: 'failed' | 'held';
turnFailureKind?: TurnFailureKind;
replyGroup?: string;
// 返信の途中に人間の発言が届いて(`queued`)分けた行の印: デーモンはそこで返信を割って日誌へ書くので(#4391)、割らない古いデーモンの本文と照合するときはこの境目に区切りを入れない
// 返信の途中に人間の発言が届いて(`queued`)分けた行の印: デーモンはそこで返信を割って日誌へ書くので、割らない古いデーモンの本文と照合するときはこの境目に区切りを入れない
splitByHuman?: true;
journalId?: string;
attachments?: readonly MessageAttachment[];
Expand Down Expand Up @@ -449,7 +449,7 @@ export function pendingOwnLines(
if (line.approval !== undefined) historyApprovals.set(line.approval.id, line.approval);
}
// 分かれた返信は連結した本文で照合する: 日誌はターンの本文を1つの発言として載せるので、行ごとだと二重に出る。
// 連続した行の並びごとに照合する(長い並びを先に当てる): デーモンは承認カードやターン中に届いた人間の発言(#4391)でターンの本文を割って書くので、1ターンが日誌の複数の発言になる
// 連続した行の並びごとに照合する(長い並びを先に当てる): デーモンは承認カードやターン中に届いた人間の発言でターンの本文を割って書くので、1ターンが日誌の複数の発言になる
const groups = new Map<string, Line[]>();
for (const line of owned) {
if (line.replyGroup === undefined || line.role !== 'clone') continue;
Expand All @@ -462,7 +462,7 @@ export function pendingOwnLines(
let next = start + 1;
for (let end = members.length; end > start + 1; end -= 1) {
const run = members.slice(start, end);
// 区切りあり(#4339 以降のデーモン: メッセージの境目に空行)と区切りなし(古いデーモン)の両方を受ける: Web とデーモンは版がずれる。
// 区切りあり(メッセージの境目に空行)と区切りなし(古いデーモン)の両方を受ける: Web とデーモンは版がずれる。
// 区切りありを先に当てる。どちらも履歴に在るなら、区切りありのほうが新しい形
const key = REPLY_SEGMENT_JOINERS.map(
(joiner) =>
Expand Down Expand Up @@ -1822,7 +1822,7 @@ export function ChatPane({
};
const apply = (event: ChatStreamEvent) => {
switch (event.type) {
// 返信の行をここで分ける(#4391): デーモンは人間の発言を記録するとき、それまでの返信を先に日誌へ書く。続きを同じ行へ足すと、その発言より上に出るうえ、日誌の本文とも合わない
// 返信の行をここで分ける:デーモンは人間の発言を記録するとき、それまでの返信を先に日誌へ書く。続きを同じ行へ足すと、その発言より上に出るうえ、日誌の本文とも合わない
case 'queued': {
// 続けて届いた2通目でも印を落とさない: 1通目で行は既に閉じている
const split = replyKey !== undefined || splitByHuman;
Expand Down Expand Up @@ -2785,7 +2785,7 @@ export function ChatPane({
first.at,
);
const open = data.reachedStart === false;
// 取り下げた発言は数えない(#4357): 吹き出しとして出していないため
// 取り下げた発言は数えない:吹き出しとして出していないため
const counted = visible.filter((message) => message.delivery !== 'withdrawn').length;
return `${formatDateTime(startedAt)}${open ? ' 以降' : ' に開始'} · 発言 ${counted} 件${open ? '以上' : ''}`;
})();
Expand Down
1 change: 0 additions & 1 deletion apps/web/app/routes/env-vars.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,6 @@ describe('/env-vars 画面 — 置く・編集・削除', () => {
fireEvent.change(within(dialog).getByLabelText('値'), { target: { value: 'half-typed' } });
fireEvent.change(within(dialog).getByLabelText('渡す先'), { target: { value: 'app' } });
fireEvent.click(within(dialog).getByRole('button', { name: 'やめる' }));
// 書きかけがあるので確認を挟む(#3418)。ここでは破棄を選んで閉じる
fireEvent.click(await screen.findByRole('button', { name: '破棄して閉じる' }));
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());

Expand Down
Loading