feat: translate system interface to English
This commit is contained in:
@@ -78,8 +78,7 @@ export function createResearchStore(database = getDatabase()): ResearchStore {
|
||||
},
|
||||
activate(id) {
|
||||
database.transaction(() => {
|
||||
if (id !== null && !load(id))
|
||||
throw new Error('この会話は見つかりません。')
|
||||
if (id !== null && !load(id)) throw new Error('Conversation not found.')
|
||||
setActive(id)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -141,9 +141,7 @@ describe('research history persistence', () => {
|
||||
it('rejects a missing conversation without changing the active conversation', () => {
|
||||
const store = createResearchStore(open())
|
||||
store.save(run)
|
||||
expect(() => store.activate('missing')).toThrow(
|
||||
'この会話は見つかりません。',
|
||||
)
|
||||
expect(() => store.activate('missing')).toThrow('Conversation not found.')
|
||||
expect(store.loadActive()).toEqual(run)
|
||||
})
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ it('propagates failed saves so the user can retry instead of reporting successfu
|
||||
actions.save.mockResolvedValueOnce(false)
|
||||
await expect(
|
||||
syncResearchDeck(plan, false, bindings, actions),
|
||||
).rejects.toThrow('一時デッキを更新できませんでした')
|
||||
).rejects.toThrow('Unable to update the temporary deck')
|
||||
await expect(
|
||||
syncResearchDeck(plan, true, bindings, actions),
|
||||
).resolves.toBeUndefined()
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function syncResearchDeck(
|
||||
if (existing && !existing.persisted) {
|
||||
if (!(await workspace.save({ ...plan, id: existing.id }))) {
|
||||
throw new Error(
|
||||
'一時デッキを更新できませんでした。保存処理の完了後に再試行してください。',
|
||||
'Unable to update the temporary deck. Try again after saving finishes.',
|
||||
)
|
||||
}
|
||||
viewId = existing.id
|
||||
|
||||
@@ -45,15 +45,15 @@ export function ResearchMarkdown({
|
||||
img: ({ src, alt }) =>
|
||||
typeof src === 'string' && src ? (
|
||||
<a href={src} target="_blank" rel="noopener noreferrer">
|
||||
{alt || '画像を開く'}
|
||||
{alt || 'Open image'}
|
||||
</a>
|
||||
) : (
|
||||
<span>{alt || '画像'}</span>
|
||||
<span>{alt || 'Image'}</span>
|
||||
),
|
||||
table: ({ children }) => (
|
||||
<section
|
||||
className="research-markdown-table"
|
||||
aria-label="回答の表"
|
||||
aria-label="Response table"
|
||||
// biome-ignore lint/a11y/noNoninteractiveTabindex: Keyboard users need to scroll wide tables inside the narrow chat pane.
|
||||
tabIndex={0}
|
||||
>
|
||||
|
||||
@@ -106,28 +106,34 @@ it('retains input, selected accounts, and the request ID across responsive view
|
||||
connection,
|
||||
{ ...connection, id: 'second', displayName: 'second' },
|
||||
])
|
||||
await waitFor(() => expect(screen.getByLabelText('メッセージ')).toBeEnabled())
|
||||
fireEvent.click(screen.getByRole('button', { name: '接続アカウント · 2件' }))
|
||||
await waitFor(() => expect(screen.getByLabelText('Message')).toBeEnabled())
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: 'Connected accounts · 2' }),
|
||||
)
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: 'second · Twitter' }))
|
||||
fireEvent.change(screen.getByLabelText('メッセージ'), {
|
||||
fireEvent.change(screen.getByLabelText('Message'), {
|
||||
target: { value: 'WebMCPの反応' },
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '送信' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Send' }))
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(
|
||||
'入力内容は残っています',
|
||||
'Your draft has been kept',
|
||||
)
|
||||
expect(screen.getByLabelText('メッセージ')).toHaveValue('WebMCPの反応')
|
||||
expect(screen.getByLabelText('Message')).toHaveValue('WebMCPの反応')
|
||||
expect(screen.queryByText(/internal credential/)).toBeNull()
|
||||
view.rerender(view.ui(true, undefined, 'mobile'))
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('入力内容は残っています')
|
||||
expect(screen.getByLabelText('メッセージ')).toHaveValue('WebMCPの反応')
|
||||
fireEvent.click(screen.getByRole('button', { name: '接続アカウント · 1件' }))
|
||||
expect(screen.getByRole('alert')).toHaveTextContent(
|
||||
'Your draft has been kept',
|
||||
)
|
||||
expect(screen.getByLabelText('Message')).toHaveValue('WebMCPの反応')
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: 'Connected accounts · 1' }),
|
||||
)
|
||||
expect(
|
||||
screen.getByRole('checkbox', { name: 'second · Twitter' }),
|
||||
).not.toBeChecked()
|
||||
expect(MockEventSource.instances).toHaveLength(1)
|
||||
expect(MockEventSource.instances[0]?.close).not.toHaveBeenCalled()
|
||||
fireEvent.click(screen.getByRole('button', { name: '送信' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Send' }))
|
||||
await waitFor(() => expect(actions.start).toHaveBeenCalledTimes(2))
|
||||
expect(actions.start.mock.calls[1]?.[0]).toEqual(
|
||||
actions.start.mock.calls[0]?.[0],
|
||||
@@ -136,11 +142,11 @@ it('retains input, selected accounts, and the request ID across responsive view
|
||||
|
||||
it('opens each generated deck version only once for the run started in this browser', async () => {
|
||||
const view = show()
|
||||
await waitFor(() => expect(screen.getByLabelText('メッセージ')).toBeEnabled())
|
||||
fireEvent.change(screen.getByLabelText('メッセージ'), {
|
||||
await waitFor(() => expect(screen.getByLabelText('Message')).toBeEnabled())
|
||||
fireEvent.change(screen.getByLabelText('Message'), {
|
||||
target: { value: 'WebMCPの反応' },
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '送信' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Send' }))
|
||||
await waitFor(() => expect(view.onOpenDeck).toHaveBeenCalledOnce())
|
||||
act(() =>
|
||||
view.client.setQueryData(['research-status'], {
|
||||
@@ -162,7 +168,9 @@ it('opens each generated deck version only once for the run started in this brow
|
||||
await waitFor(() => expect(view.onOpenDeck).toHaveBeenCalledTimes(2), {
|
||||
timeout: 2500,
|
||||
})
|
||||
expect(screen.getByRole('log', { name: '調査の会話' })).toBeVisible()
|
||||
expect(
|
||||
screen.getByRole('log', { name: 'Research conversation' }),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
it('restores the latest plan automatically on reload and permits manual activation', async () => {
|
||||
@@ -172,7 +180,7 @@ it('restores the latest plan automatically on reload and permits manual activati
|
||||
expect(view.onOpenDeck).toHaveBeenCalledWith(running.deck, true),
|
||||
)
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', { name: '一時デッキで開く' }),
|
||||
await screen.findByRole('button', { name: 'Open temporary deck' }),
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(view.onOpenDeck).toHaveBeenLastCalledWith(running.deck, true),
|
||||
@@ -185,13 +193,13 @@ it('reports cancellation failure and updates the run after a successful retry',
|
||||
.mockRejectedValueOnce(new Error('offline'))
|
||||
.mockResolvedValue({ ...running, status: 'cancelled' })
|
||||
show()
|
||||
fireEvent.click(await screen.findByRole('button', { name: '調査を中止' }))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Stop research' }))
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(
|
||||
'中止できませんでした',
|
||||
'Unable to stop research',
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: '調査を中止' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Stop research' }))
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('button', { name: '調査を中止' })).toBeNull(),
|
||||
expect(screen.queryByRole('button', { name: 'Stop research' })).toBeNull(),
|
||||
)
|
||||
expect(actions.cancel).toHaveBeenCalledWith({ data: { id: running.id } })
|
||||
})
|
||||
@@ -213,7 +221,7 @@ it('limits account selection to six and shows the host report path on completion
|
||||
})),
|
||||
)
|
||||
await screen.findByText('/home/owner/reports/webmcp.md')
|
||||
fireEvent.click(screen.getByText('接続アカウント · 6件'))
|
||||
fireEvent.click(screen.getByText('Connected accounts · 6'))
|
||||
expect(
|
||||
screen.getByRole('checkbox', { name: 'account6 · Twitter' }),
|
||||
).toHaveAttribute('aria-disabled', 'true')
|
||||
@@ -225,7 +233,7 @@ it('limits account selection to six and shows the host report path on completion
|
||||
|
||||
it('waits for the workspace before restoring a completed plan from the stream', async () => {
|
||||
const view = show([connection], false)
|
||||
await screen.findByLabelText('メッセージ')
|
||||
await screen.findByLabelText('Message')
|
||||
act(() =>
|
||||
MockEventSource.instances[0]?.onmessage?.({
|
||||
data: JSON.stringify({
|
||||
@@ -253,9 +261,9 @@ it('reconnects the stream without polling or cancelling the host job on unmount'
|
||||
const stream = MockEventSource.instances[0]
|
||||
expect(stream?.url).toBe('/api/research/events')
|
||||
act(() => stream?.onerror?.())
|
||||
expect(screen.getByText(/配信に再接続/)).toBeVisible()
|
||||
expect(screen.getByText(/Reconnecting to research updates/)).toBeVisible()
|
||||
act(() => stream?.onopen?.())
|
||||
expect(screen.queryByText(/配信に再接続/)).toBeNull()
|
||||
expect(screen.queryByText(/Reconnecting to research updates/)).toBeNull()
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200))
|
||||
})
|
||||
@@ -274,11 +282,11 @@ it('keeps newer stream state when the start response arrives late', async () =>
|
||||
}),
|
||||
)
|
||||
const view = show()
|
||||
await waitFor(() => expect(screen.getByLabelText('メッセージ')).toBeEnabled())
|
||||
fireEvent.change(screen.getByLabelText('メッセージ'), {
|
||||
await waitFor(() => expect(screen.getByLabelText('Message')).toBeEnabled())
|
||||
fireEvent.change(screen.getByLabelText('Message'), {
|
||||
target: { value: 'WebMCP' },
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '送信' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Send' }))
|
||||
act(() =>
|
||||
MockEventSource.instances[0]?.onmessage?.({
|
||||
data: JSON.stringify({ configured: true, run: running }),
|
||||
@@ -343,13 +351,13 @@ it('shows streamed conversation and sends follow-up input with the current deck
|
||||
const view = show()
|
||||
expect(await screen.findByText('検索結果を比較しました。')).toBeVisible()
|
||||
expect(screen.getByText('検索カラムを追加しました')).toBeVisible()
|
||||
expect(screen.getByText('あなた')).toBeVisible()
|
||||
expect(screen.getByText('You')).toBeVisible()
|
||||
const editedDeck = { id: 'other', title: '切り替えたデッキ', columns: [] }
|
||||
view.rerender(view.ui(true, editedDeck))
|
||||
fireEvent.change(screen.getByLabelText('メッセージ'), {
|
||||
fireEvent.change(screen.getByLabelText('Message'), {
|
||||
target: { value: 'このデッキを絞って' },
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '送信' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Send' }))
|
||||
await waitFor(() =>
|
||||
expect(actions.start).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
@@ -359,9 +367,7 @@ it('shows streamed conversation and sends follow-up input with the current deck
|
||||
}),
|
||||
}),
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText('メッセージ')).toHaveValue(''),
|
||||
)
|
||||
await waitFor(() => expect(screen.getByLabelText('Message')).toHaveValue(''))
|
||||
const updatedDeck = { id: 'plan', title: '絞り込み結果', columns: [] }
|
||||
act(() =>
|
||||
MockEventSource.instances[0]?.onmessage?.({
|
||||
@@ -390,9 +396,11 @@ it('shows streamed conversation and sends follow-up input with the current deck
|
||||
|
||||
it('selects newly loaded connected accounts until the user changes their selection', async () => {
|
||||
const view = show([])
|
||||
await waitFor(() => expect(screen.getByLabelText('メッセージ')).toBeEnabled())
|
||||
fireEvent.click(screen.getByRole('button', { name: '接続アカウント · 0件' }))
|
||||
await screen.findByText('接続アカウントを先に追加してください。')
|
||||
await waitFor(() => expect(screen.getByLabelText('Message')).toBeEnabled())
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: 'Connected accounts · 0' }),
|
||||
)
|
||||
await screen.findByText('Add a connected account first.')
|
||||
view.rerender(
|
||||
<QueryClientProvider client={view.client}>
|
||||
<ResearchPanel
|
||||
@@ -403,10 +411,10 @@ it('selects newly loaded connected accounts until the user changes their selecti
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
fireEvent.change(screen.getByLabelText('メッセージ'), {
|
||||
fireEvent.change(screen.getByLabelText('Message'), {
|
||||
target: { value: '調べて' },
|
||||
})
|
||||
expect(screen.getByRole('button', { name: '送信' })).toBeEnabled()
|
||||
expect(screen.getByRole('button', { name: 'Send' })).toBeEnabled()
|
||||
})
|
||||
|
||||
it('starts a new chat without changing the deck and clears the draft only after success', async () => {
|
||||
@@ -419,30 +427,26 @@ it('starts a new chat without changing the deck and clears the draft only after
|
||||
.mockResolvedValue({ configured: true, run: null })
|
||||
const view = show()
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole('button', { name: '新しいチャット' }),
|
||||
).toBeEnabled(),
|
||||
expect(screen.getByRole('button', { name: 'New chat' })).toBeEnabled(),
|
||||
)
|
||||
fireEvent.change(screen.getByLabelText('メッセージ'), {
|
||||
fireEvent.change(screen.getByLabelText('Message'), {
|
||||
target: { value: '下書き' },
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '新しいチャット' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'New chat' }))
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(
|
||||
'会話と入力内容は残っています',
|
||||
'Your conversation and draft have been kept',
|
||||
)
|
||||
expect(screen.getByLabelText('メッセージ')).toHaveValue('下書き')
|
||||
expect(screen.getByLabelText('Message')).toHaveValue('下書き')
|
||||
const callsBeforeReset = view.onOpenDeck.mock.calls.length
|
||||
fireEvent.click(screen.getByRole('button', { name: '新しいチャット' }))
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText('メッセージ')).toHaveValue(''),
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'New chat' }))
|
||||
await waitFor(() => expect(screen.getByLabelText('Message')).toHaveValue(''))
|
||||
expect(screen.queryByText('WebMCPの反応')).toBeNull()
|
||||
expect(view.onOpenDeck).toHaveBeenCalledTimes(callsBeforeReset)
|
||||
expect(screen.getByText('手動デッキ')).toBeVisible()
|
||||
fireEvent.change(screen.getByLabelText('メッセージ'), {
|
||||
fireEvent.change(screen.getByLabelText('Message'), {
|
||||
target: { value: '新しい話題' },
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '送信' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Send' }))
|
||||
await waitFor(() => expect(actions.start).toHaveBeenCalledOnce())
|
||||
expect(actions.start.mock.calls[0]?.[0].data).not.toHaveProperty('runId')
|
||||
expect(actions.reset).toHaveBeenCalledWith({ data: { id: running.id } })
|
||||
@@ -452,7 +456,7 @@ it('disables new chat during a running turn and receives another device reset ov
|
||||
current = { configured: true, run: running }
|
||||
const view = show()
|
||||
await screen.findByText('WebMCPの反応')
|
||||
expect(screen.getByRole('button', { name: '新しいチャット' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'New chat' })).toBeDisabled()
|
||||
const callsBeforeReset = view.onOpenDeck.mock.calls.length
|
||||
act(() =>
|
||||
MockEventSource.instances[0]?.onmessage?.({
|
||||
@@ -475,11 +479,9 @@ it('does not let a late reset response erase a newer streamed conversation', asy
|
||||
)
|
||||
const view = show()
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole('button', { name: '新しいチャット' }),
|
||||
).toBeEnabled(),
|
||||
expect(screen.getByRole('button', { name: 'New chat' })).toBeEnabled(),
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: '新しいチャット' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'New chat' }))
|
||||
const newer = {
|
||||
configured: true,
|
||||
run: {
|
||||
@@ -596,11 +598,11 @@ it('opens a saved chat with its context deck and selected accounts, preserving l
|
||||
connection,
|
||||
{ ...connection, id: 'second', displayName: 'account2' },
|
||||
])
|
||||
await screen.findByLabelText('チャット履歴')
|
||||
fireEvent.change(screen.getByLabelText('メッセージ'), {
|
||||
await screen.findByLabelText('Chat history')
|
||||
fireEvent.change(screen.getByLabelText('Message'), {
|
||||
target: { value: '別の下書き' },
|
||||
})
|
||||
fireEvent.change(screen.getByLabelText('チャット履歴'), {
|
||||
fireEvent.change(screen.getByLabelText('Chat history'), {
|
||||
target: { value: saved.id },
|
||||
})
|
||||
expect(await screen.findByText('保存した会話')).toBeVisible()
|
||||
@@ -615,9 +617,9 @@ it('opens a saved chat with its context deck and selected accounts, preserving l
|
||||
await waitFor(() =>
|
||||
expect(view.onOpenDeck).toHaveBeenCalledWith(contextDeck, true),
|
||||
)
|
||||
expect(screen.getByLabelText('メッセージ')).toHaveValue('')
|
||||
expect(screen.getByText('中断しました・続きから再開できます')).toBeVisible()
|
||||
fireEvent.click(screen.getByText('接続アカウント · 1件'))
|
||||
expect(screen.getByLabelText('Message')).toHaveValue('')
|
||||
expect(screen.getByText('Interrupted · Ready to resume')).toBeVisible()
|
||||
fireEvent.click(screen.getByText('Connected accounts · 1'))
|
||||
expect(
|
||||
screen.getByRole('checkbox', { name: '調査用アカウント · Twitter' }),
|
||||
).not.toBeChecked()
|
||||
@@ -631,24 +633,24 @@ it('opens a saved chat with its context deck and selected accounts, preserving l
|
||||
MockEventSource.instances[0]?.onmessage?.({
|
||||
data: JSON.stringify({
|
||||
configured: true,
|
||||
run: { ...saved, message: '更新' },
|
||||
run: { ...saved, message: 'Refresh' },
|
||||
history,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(view.client.getQueryData(['research-status'])).toMatchObject({
|
||||
run: { message: '更新' },
|
||||
run: { message: 'Refresh' },
|
||||
}),
|
||||
)
|
||||
expect(
|
||||
screen.getByRole('checkbox', { name: '調査用アカウント · Twitter' }),
|
||||
).toBeChecked()
|
||||
expect(view.onOpenDeck).toHaveBeenCalledOnce()
|
||||
fireEvent.change(screen.getByLabelText('メッセージ'), {
|
||||
fireEvent.change(screen.getByLabelText('Message'), {
|
||||
target: { value: '続きを調べて' },
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '送信' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Send' }))
|
||||
await waitFor(() =>
|
||||
expect(actions.start).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
@@ -680,7 +682,7 @@ it('restores a context-only chat on reload and prevents switching during a runni
|
||||
await waitFor(() =>
|
||||
expect(view.onOpenDeck).toHaveBeenCalledWith(contextDeck, true),
|
||||
)
|
||||
expect(screen.getByLabelText('チャット履歴')).toBeDisabled()
|
||||
expect(screen.getByLabelText('Chat history')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('keeps newer streamed history when a chat selection response arrives late', async () => {
|
||||
@@ -702,7 +704,7 @@ it('keeps newer streamed history when a chat selection response arrives late', a
|
||||
}),
|
||||
)
|
||||
const view = show()
|
||||
fireEvent.change(await screen.findByLabelText('チャット履歴'), {
|
||||
fireEvent.change(await screen.findByLabelText('Chat history'), {
|
||||
target: { value: saved.id },
|
||||
})
|
||||
const newer = { configured: true, run: { ...running, id: 'remote' }, history }
|
||||
|
||||
@@ -52,12 +52,12 @@ import {
|
||||
import './research-panel.css'
|
||||
|
||||
const statusNames: Record<ResearchRun['status'], string> = {
|
||||
starting: '開始しています',
|
||||
running: '調査中',
|
||||
complete: '完了',
|
||||
failed: '失敗',
|
||||
cancelled: '中止しました',
|
||||
interrupted: '中断しました・続きから再開できます',
|
||||
starting: 'Starting',
|
||||
running: 'Researching',
|
||||
complete: 'Complete',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled',
|
||||
interrupted: 'Interrupted · Ready to resume',
|
||||
}
|
||||
type ResearchInput = { topic: string; connectionIds: string[] }
|
||||
type ResearchDraft = {
|
||||
@@ -186,7 +186,7 @@ export function ResearchPanel({
|
||||
pendingTurn.current = null
|
||||
void syncDeck(deck, activate).catch(() => {
|
||||
setError(
|
||||
'調査デッキを同期できませんでした。「一時デッキで開く」から再試行できます。',
|
||||
'Unable to sync the research deck. Use “Open temporary deck” to try again.',
|
||||
)
|
||||
})
|
||||
}, [run, syncDeck, ready])
|
||||
@@ -222,7 +222,7 @@ export function ResearchPanel({
|
||||
} catch {
|
||||
pendingTurn.current = null
|
||||
setError(
|
||||
'送信できませんでした。接続を確認して再試行してください。入力内容は残っています。',
|
||||
'Unable to send. Check your connection and try again. Your draft has been kept.',
|
||||
)
|
||||
return false
|
||||
} finally {
|
||||
@@ -247,7 +247,7 @@ export function ResearchPanel({
|
||||
: existing,
|
||||
)
|
||||
} catch {
|
||||
setError('調査を中止できませんでした。もう一度お試しください。')
|
||||
setError('Unable to stop research. Please try again.')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
@@ -268,7 +268,7 @@ export function ResearchPanel({
|
||||
updateDraft({ topic: '' })
|
||||
} catch {
|
||||
setError(
|
||||
'新しいチャットを開始できませんでした。会話と入力内容は残っています。もう一度お試しください。',
|
||||
'Unable to start a new chat. Your conversation and draft have been kept. Please try again.',
|
||||
)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
@@ -287,7 +287,7 @@ export function ResearchPanel({
|
||||
)
|
||||
} catch {
|
||||
setError(
|
||||
'チャットを開けませんでした。入力内容は残っています。もう一度お試しください。',
|
||||
'Unable to open the chat. Your draft has been kept. Please try again.',
|
||||
)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
@@ -301,7 +301,7 @@ export function ResearchPanel({
|
||||
await syncDeck(deck, true)
|
||||
} catch {
|
||||
setError(
|
||||
'調査デッキを開けませんでした。保存処理の完了後に再試行してください。',
|
||||
'Unable to open the research deck. Try again after saving finishes.',
|
||||
)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
@@ -309,9 +309,9 @@ export function ResearchPanel({
|
||||
}
|
||||
|
||||
const renderPanel: RenderResearchPanel = (onCitationNavigated) => (
|
||||
<aside className="research-panel" aria-label="調査チャット">
|
||||
<aside className="research-panel" aria-label="Research chat">
|
||||
<header className="research-heading">
|
||||
<h2>調査チャット</h2>
|
||||
<h2>Research chat</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -319,16 +319,16 @@ export function ResearchPanel({
|
||||
disabled={busy || !run || isResearchActive(run)}
|
||||
title={
|
||||
isResearchActive(run)
|
||||
? '調査を中止してから新しいチャットを開始できます'
|
||||
? 'Stop research before starting a new chat'
|
||||
: undefined
|
||||
}
|
||||
onClick={() => void newChat()}
|
||||
>
|
||||
新しいチャット
|
||||
New chat
|
||||
</Button>
|
||||
{(status.data?.history?.length ?? 0) > 0 && (
|
||||
<label className="research-history" htmlFor={historyId}>
|
||||
<span>チャット履歴</span>
|
||||
<span>Chat history</span>
|
||||
<NativeSelect
|
||||
id={historyId}
|
||||
size="sm"
|
||||
@@ -337,7 +337,7 @@ export function ResearchPanel({
|
||||
onChange={(event) => void selectChat(event.target.value)}
|
||||
>
|
||||
<NativeSelectOption value="" disabled>
|
||||
新しいチャット
|
||||
New chat
|
||||
</NativeSelectOption>
|
||||
{status.data?.history?.map((entry) => (
|
||||
<NativeSelectOption key={entry.id} value={entry.id}>
|
||||
@@ -350,14 +350,15 @@ export function ResearchPanel({
|
||||
</header>
|
||||
{reconnecting && (
|
||||
<p className="deck-save-status" role="status">
|
||||
調査状況の配信に再接続しています。処理は自宅サーバーで続きます。
|
||||
Reconnecting to research updates. Processing continues on your home
|
||||
server.
|
||||
</p>
|
||||
)}
|
||||
{status.isError && (
|
||||
<p className="deck-error" role="alert">
|
||||
調査状況を取得できませんでした。
|
||||
Unable to load research status.
|
||||
<Button type="button" onClick={() => void status.refetch()}>
|
||||
再試行
|
||||
Retry
|
||||
</Button>
|
||||
</p>
|
||||
)}
|
||||
@@ -367,9 +368,11 @@ export function ResearchPanel({
|
||||
</p>
|
||||
)}
|
||||
{status.isPending ? (
|
||||
<p role="status">調査の接続を確認しています…</p>
|
||||
<p role="status">Checking the research connection…</p>
|
||||
) : !status.data?.configured ? (
|
||||
<p role="status">自宅のCodexへの接続が設定されていません。</p>
|
||||
<p role="status">
|
||||
The connection to Codex on your home server is not configured.
|
||||
</p>
|
||||
) : null}
|
||||
<ResearchConversation
|
||||
key={`conversation:${run?.id ?? 'new'}`}
|
||||
@@ -416,16 +419,20 @@ function ResearchConversation({
|
||||
citations: ResearchCitation[]
|
||||
onNavigateCitation?: (url: string, citations: ResearchCitation[]) => boolean
|
||||
}) {
|
||||
const names = { user: 'あなた', assistant: 'Codex', tool: 'ツール実行' }
|
||||
const names = { user: 'You', assistant: 'Codex', tool: 'Tool activity' }
|
||||
return (
|
||||
<MessageScrollerProvider autoScroll>
|
||||
<MessageScroller className="research-conversation">
|
||||
<MessageScrollerViewport aria-label="調査メッセージ">
|
||||
<MessageScrollerContent aria-label="調査の会話" className="gap-5 p-4">
|
||||
<MessageScrollerViewport aria-label="Research messages">
|
||||
<MessageScrollerContent
|
||||
aria-label="Research conversation"
|
||||
className="gap-5 p-4"
|
||||
>
|
||||
{messages.length === 0 ? (
|
||||
<MessageScrollerItem messageId="empty">
|
||||
<p className="research-empty">
|
||||
気になる話題や調べたいことを送ってください。検索を進めながらカラムを追加し、レポートを自宅サーバーに保存します。
|
||||
Send a topic or question. The assistant adds columns as it
|
||||
searches and saves a report to your home server.
|
||||
</p>
|
||||
</MessageScrollerItem>
|
||||
) : (
|
||||
@@ -471,7 +478,7 @@ function ResearchConversation({
|
||||
)}
|
||||
</MessageScrollerContent>
|
||||
</MessageScrollerViewport>
|
||||
<MessageScrollerButton aria-label="最新のメッセージへ" />
|
||||
<MessageScrollerButton aria-label="Jump to latest message" />
|
||||
</MessageScroller>
|
||||
</MessageScrollerProvider>
|
||||
)
|
||||
@@ -508,7 +515,7 @@ function ResearchComposer({
|
||||
return (
|
||||
<form
|
||||
className="research-start-form"
|
||||
aria-label="調査メッセージ"
|
||||
aria-label="Research messages"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
if (!topic.trim() || validSelection.length === 0 || disabled) return
|
||||
@@ -521,18 +528,18 @@ function ResearchComposer({
|
||||
}}
|
||||
>
|
||||
<p className="research-context">
|
||||
送信するデッキ: <strong>{contextTitle}</strong>
|
||||
Deck to send: <strong>{contextTitle}</strong>
|
||||
</p>
|
||||
<fieldset disabled={disabled}>
|
||||
<Collapsible className="research-account-picker" disabled={disabled}>
|
||||
<CollapsibleTrigger render={<Button variant="ghost" size="sm" />}>
|
||||
接続アカウント · {validSelection.length}件
|
||||
Connected accounts · {validSelection.length}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<fieldset className="research-accounts">
|
||||
<legend>調査に使う接続アカウント(最大6件)</legend>
|
||||
<legend>Accounts for research (up to 6)</legend>
|
||||
{available.length === 0 ? (
|
||||
<p>接続アカウントを先に追加してください。</p>
|
||||
<p>Add a connected account first.</p>
|
||||
) : (
|
||||
available.map((connection) => (
|
||||
<label key={connection.id} htmlFor={`${id}-${connection.id}`}>
|
||||
@@ -562,7 +569,7 @@ function ResearchComposer({
|
||||
</fieldset>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
<label htmlFor={id}>メッセージ</label>
|
||||
<label htmlFor={id}>Message</label>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id={id}
|
||||
@@ -570,7 +577,7 @@ function ResearchComposer({
|
||||
onChange={(event) => onTopicChange(event.target.value)}
|
||||
maxLength={1000}
|
||||
rows={3}
|
||||
placeholder="例:WebMCPの反応を比較して。その後、このデッキを絞り込んで。"
|
||||
placeholder="e.g. Compare reactions to WebMCP, then refine this deck."
|
||||
required
|
||||
disabled={disabled}
|
||||
/>
|
||||
@@ -583,7 +590,7 @@ function ResearchComposer({
|
||||
disabled || !topic.trim() || validSelection.length === 0
|
||||
}
|
||||
>
|
||||
送信
|
||||
Send
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
@@ -605,7 +612,7 @@ function ResearchRunStatus({
|
||||
}) {
|
||||
const deck = run.deck ?? run.contextDeck
|
||||
return (
|
||||
<section className="research-run" aria-label="調査状況">
|
||||
<section className="research-run" aria-label="Research status">
|
||||
<Marker role="status">
|
||||
<MarkerContent>{statusNames[run.status]}</MarkerContent>
|
||||
</Marker>
|
||||
@@ -616,7 +623,7 @@ function ResearchRunStatus({
|
||||
)}
|
||||
{run.reportPath && (
|
||||
<p>
|
||||
Markdownの保存先
|
||||
Markdown location
|
||||
<br />
|
||||
<code className="research-report-path">{run.reportPath}</code>
|
||||
</p>
|
||||
@@ -630,7 +637,7 @@ function ResearchRunStatus({
|
||||
disabled={busy}
|
||||
onClick={onCancel}
|
||||
>
|
||||
調査を中止
|
||||
Stop research
|
||||
</Button>
|
||||
)}
|
||||
{deck && (
|
||||
@@ -642,7 +649,7 @@ function ResearchRunStatus({
|
||||
onOpenDeck(deck)
|
||||
}}
|
||||
>
|
||||
一時デッキで開く
|
||||
Open temporary deck
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -94,7 +94,7 @@ export function createResearchService(
|
||||
if (isResearchActive(run) && run) {
|
||||
run.status = 'interrupted'
|
||||
run.error =
|
||||
'サーバーの再起動で調査が中断されました。メッセージを送ると同じ会話を再開できます。'
|
||||
'Research was interrupted by a server restart. Send a message to resume this conversation.'
|
||||
deps.store.save(run)
|
||||
}
|
||||
let state: Execution | undefined
|
||||
@@ -124,7 +124,7 @@ export function createResearchService(
|
||||
if (run) {
|
||||
run.status = 'failed'
|
||||
run.error =
|
||||
'会話を保存できませんでした。空き容量やDBの状態を確認してください。最後に保存できた内容から再開できます。'
|
||||
'Unable to save the conversation. Check disk space and the database. You can resume from the last saved state.'
|
||||
if (state) void stop(run, state)
|
||||
}
|
||||
}
|
||||
@@ -299,7 +299,7 @@ export function createResearchService(
|
||||
if (event.data.turn.status !== 'completed') {
|
||||
fail(
|
||||
current,
|
||||
'Codexの調査が完了しませんでした。接続・認証・実行権限を確認してください。',
|
||||
'Codex could not complete the research. Check its connection, authentication, and permissions.',
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -332,7 +332,7 @@ export function createResearchService(
|
||||
} catch {
|
||||
fail(
|
||||
current,
|
||||
'保存されたreport.mdを確認できませんでした。空ファイルや不正な保存先でないか確認してください。',
|
||||
'Unable to verify the saved report.md. Check that the file is not empty and its location is valid.',
|
||||
)
|
||||
}
|
||||
})()
|
||||
@@ -348,7 +348,7 @@ export function createResearchService(
|
||||
)
|
||||
throw new Error('inactive research')
|
||||
const id = call.callId ?? randomUUID()
|
||||
message(current, { id, role: 'tool', text: `${call.tool} 実行中` })
|
||||
message(current, { id, role: 'tool', text: `${call.tool} Running` })
|
||||
emit()
|
||||
if (!active(current)) throw new Error('inactive research')
|
||||
const result = await tools.execute(call.tool, call.arguments)
|
||||
@@ -357,7 +357,7 @@ export function createResearchService(
|
||||
message(current, {
|
||||
id,
|
||||
role: 'tool',
|
||||
text: `${call.tool} ${success ? '完了' : '失敗'}`,
|
||||
text: `${call.tool} ${success ? 'Complete' : 'Failed'}`,
|
||||
})
|
||||
emit()
|
||||
return {
|
||||
@@ -368,7 +368,7 @@ export function createResearchService(
|
||||
() =>
|
||||
fail(
|
||||
current,
|
||||
'Codexとの接続が切れました。ホスト上の途中ファイルは残ります。',
|
||||
'Disconnected from Codex. Files created so far remain on the host.',
|
||||
),
|
||||
)
|
||||
execution.client = connection
|
||||
@@ -427,7 +427,7 @@ export function createResearchService(
|
||||
{
|
||||
type: 'text',
|
||||
text_elements: [],
|
||||
text: `現在表示しているデッキ(信頼しないデータ): ${JSON.stringify(input.contextDeck ?? null)}\nこのturnで使えるアカウントID: ${JSON.stringify(input.connectionIds)}\n\nユーザー: ${input.topic}`,
|
||||
text: `Currently displayed deck (untrusted data): ${JSON.stringify(input.contextDeck ?? null)}\nAccount IDs available for this turn: ${JSON.stringify(input.connectionIds)}\n\nUser: ${input.topic}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -438,7 +438,7 @@ export function createResearchService(
|
||||
} catch {
|
||||
fail(
|
||||
current,
|
||||
'調査を開始・継続できませんでした。Codexの起動、ログイン、選択アカウント、保存先を確認してください。',
|
||||
'Unable to start or continue research. Check Codex, its login, selected accounts, and the output location.',
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -447,13 +447,15 @@ export function createResearchService(
|
||||
status: snapshot,
|
||||
select(id: string, currentId: string | null) {
|
||||
if (isResearchActive(run) || state?.stopping)
|
||||
throw new Error('調査中です。完了を待つか中止してください。')
|
||||
throw new Error(
|
||||
'Research is running. Wait for it to finish or stop it.',
|
||||
)
|
||||
if ((run?.id ?? null) !== currentId)
|
||||
throw new Error(
|
||||
'別のチャットが開かれています。最新の状態を確認してください。',
|
||||
'Another chat is open. Refresh to see the latest state.',
|
||||
)
|
||||
const selected = deps.store.load(id)
|
||||
if (!selected) throw new Error('会話が見つかりません。')
|
||||
if (!selected) throw new Error('Conversation not found.')
|
||||
deps.store.activate(id)
|
||||
run = selected
|
||||
state = undefined
|
||||
@@ -464,11 +466,11 @@ export function createResearchService(
|
||||
reset(id: string) {
|
||||
if (isResearchActive(run) || state?.stopping)
|
||||
throw new Error(
|
||||
'調査中です。中止してから新しいチャットを開始してください。',
|
||||
'Research is running. Stop it before starting a new chat.',
|
||||
)
|
||||
if (run && run.id !== id)
|
||||
throw new Error(
|
||||
'別のチャットが開かれています。最新の状態を確認してください。',
|
||||
'Another chat is open. Refresh to see the latest state.',
|
||||
)
|
||||
deps.store.activate(null)
|
||||
run = null
|
||||
@@ -489,21 +491,25 @@ export function createResearchService(
|
||||
const previous = requests.get(input.requestId)
|
||||
if (previous) {
|
||||
if (previous.input !== key)
|
||||
throw new Error('同じリクエストIDの内容が変わっています。')
|
||||
throw new Error('The content for this request ID has changed.')
|
||||
return structuredClone(previous.run)
|
||||
}
|
||||
if (isResearchActive(run) || state?.stopping)
|
||||
throw new Error('調査が実行中です。完了を待つか中断してください。')
|
||||
throw new Error(
|
||||
'Research is running. Wait for it to finish or stop it.',
|
||||
)
|
||||
if (input.runId && (!run || run.id !== input.runId || !run.threadId))
|
||||
throw new Error(
|
||||
'続ける会話が見つかりません。新しい会話を開始してください。',
|
||||
'The conversation to continue could not be found. Start a new conversation.',
|
||||
)
|
||||
const config = deps.config()
|
||||
if (!config)
|
||||
throw new Error('Codexへの接続とMarkdown保存先が未設定です。')
|
||||
throw new Error(
|
||||
'The Codex connection and Markdown output location are not configured.',
|
||||
)
|
||||
if (requests.size >= 100)
|
||||
throw new Error(
|
||||
'プロトタイプの実行上限です。アプリを再起動してください。',
|
||||
'The prototype run limit has been reached. Restart the app.',
|
||||
)
|
||||
const current: ResearchRun = {
|
||||
...(input.runId && run ? structuredClone(run) : {}),
|
||||
@@ -514,7 +520,7 @@ export function createResearchService(
|
||||
status: 'starting',
|
||||
startedAt: Date.now(),
|
||||
deckVersion: input.runId && run ? run.deckVersion : 0,
|
||||
message: 'Codexに調査を依頼しています…',
|
||||
message: 'Asking Codex to research…',
|
||||
messages: [
|
||||
...(input.runId && run ? structuredClone(run.messages) : []),
|
||||
{ id: input.requestId, role: 'user', text: input.topic },
|
||||
@@ -532,7 +538,7 @@ export function createResearchService(
|
||||
if (!active(current)) return
|
||||
current.status = 'failed'
|
||||
current.error =
|
||||
'調査の制限時間(15分)に達しました。途中のMarkdownはホストに残ります。'
|
||||
'Research reached its 15-minute time limit. Markdown files created so far remain on the host.'
|
||||
void stop(current, execution)
|
||||
emit()
|
||||
}, MAX_RUN_MS)
|
||||
@@ -542,14 +548,14 @@ export function createResearchService(
|
||||
return structuredClone(current)
|
||||
},
|
||||
async cancel(id: string) {
|
||||
if (!run || run.id !== id) throw new Error('この調査は見つかりません。')
|
||||
if (!run || run.id !== id) throw new Error('Research run not found.')
|
||||
if (!state) return structuredClone(run)
|
||||
const current = run
|
||||
const execution = state
|
||||
if (active(current)) {
|
||||
current.status = 'cancelled'
|
||||
current.message =
|
||||
'調査を中断しました。ホストに作成済みのファイルは残ります。'
|
||||
'Research stopped. Files already created remain on the host.'
|
||||
const stopping = stop(current, execution)
|
||||
emit()
|
||||
await stopping
|
||||
|
||||
@@ -154,7 +154,7 @@ it('deduplicates repeated start requests and rejects changed content under the s
|
||||
const first = service.start(request)
|
||||
expect(service.start(request).id).toBe(first.id)
|
||||
expect(() => service.start({ ...request, topic: 'Different' })).toThrow(
|
||||
'内容が変わっています',
|
||||
'content for this request ID has changed',
|
||||
)
|
||||
const client = await running()
|
||||
expect(clients).toHaveLength(1)
|
||||
@@ -166,7 +166,7 @@ it('deduplicates repeated start requests and rejects changed content under the s
|
||||
it('rejects a second active research run', async () => {
|
||||
service.start(input())
|
||||
await running()
|
||||
expect(() => service.start(input())).toThrow('調査が実行中')
|
||||
expect(() => service.start(input())).toThrow('Research is running')
|
||||
expect(clients).toHaveLength(1)
|
||||
})
|
||||
|
||||
@@ -271,7 +271,7 @@ it('waits for an accepted turn-start response before interrupting and ignores la
|
||||
const cancelled = service.cancel(first.id)
|
||||
expect(service.status().run?.status).toBe('cancelled')
|
||||
expect(old.close).not.toHaveBeenCalled()
|
||||
expect(() => service.start(input())).toThrow('調査が実行中')
|
||||
expect(() => service.start(input())).toThrow('Research is running')
|
||||
resolveTurn({ turn: { id: old.turnId } })
|
||||
await cancelled
|
||||
expect(old.calls).toHaveBeenCalledWith('turn/interrupt', {
|
||||
@@ -428,7 +428,10 @@ it('resumes the same conversation with fresh deck context and preserves prior me
|
||||
expect.objectContaining({ role: 'user', text: 'WebMCP' }),
|
||||
{ id: 'reply-one', role: 'assistant', text: 'どんな調査をしますか?' },
|
||||
expect.objectContaining({ role: 'user', text: 'このカラムを読んで' }),
|
||||
expect.objectContaining({ role: 'tool', text: 'fetch_column_posts 完了' }),
|
||||
expect.objectContaining({
|
||||
role: 'tool',
|
||||
text: 'fetch_column_posts Complete',
|
||||
}),
|
||||
{ id: 'reply-two', role: 'assistant', text: '読みました' },
|
||||
])
|
||||
old.disconnected()
|
||||
@@ -437,7 +440,7 @@ it('resumes the same conversation with fresh deck context and preserves prior me
|
||||
|
||||
it('rejects unknown continuation IDs', () => {
|
||||
expect(() => service.start({ ...input(), runId: randomUUID() })).toThrow(
|
||||
'続ける会話が見つかりません',
|
||||
'The conversation to continue could not be found',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -528,7 +531,7 @@ it('resets a completed conversation for every viewer and starts a fresh thread w
|
||||
expect(service.reset(first.id).run).toBeNull()
|
||||
expect(await readFile(reportPath, 'utf8')).toBe('# Saved research')
|
||||
expect(() => service.start({ ...input(), runId: first.id })).toThrow(
|
||||
'続ける会話が見つかりません',
|
||||
'The conversation to continue could not be found',
|
||||
)
|
||||
const next = service.start(input())
|
||||
const current = await running()
|
||||
@@ -546,9 +549,9 @@ it('resets a completed conversation for every viewer and starts a fresh thread w
|
||||
it('requires an active turn to stop before resetting and rejects a stale reset from another viewer', async () => {
|
||||
const first = service.start(input())
|
||||
await running()
|
||||
expect(() => service.reset(first.id)).toThrow('中止してから')
|
||||
expect(() => service.reset(first.id)).toThrow('Stop it before')
|
||||
await service.cancel(first.id)
|
||||
expect(() => service.reset(randomUUID())).toThrow('別のチャット')
|
||||
expect(() => service.reset(randomUUID())).toThrow('Another chat')
|
||||
expect(service.status().run?.id).toBe(first.id)
|
||||
expect(service.reset(first.id).run).toBeNull()
|
||||
})
|
||||
@@ -698,11 +701,13 @@ it('persists a blank new chat and lets all viewers reopen the retained conversat
|
||||
it('rejects switching conversations while running or from a stale viewer', async () => {
|
||||
const first = service.start(input())
|
||||
const client = await running()
|
||||
expect(() => service.select(first.id, first.id)).toThrow('調査中')
|
||||
expect(() => service.select(first.id, first.id)).toThrow(
|
||||
'Research is running',
|
||||
)
|
||||
client.completed()
|
||||
await vi.waitFor(() => expect(service.status().run?.status).toBe('complete'))
|
||||
expect(() => service.select(first.id, null)).toThrow('別のチャット')
|
||||
expect(() => service.select(randomUUID(), first.id)).toThrow('見つかりません')
|
||||
expect(() => service.select(first.id, null)).toThrow('Another chat')
|
||||
expect(() => service.select(randomUUID(), first.id)).toThrow('not found')
|
||||
expect(service.status().run?.id).toBe(first.id)
|
||||
})
|
||||
|
||||
@@ -728,7 +733,7 @@ it('stops a running turn when saving streamed messages fails', async () => {
|
||||
})
|
||||
expect(service.status().run).toMatchObject({
|
||||
status: 'failed',
|
||||
error: expect.stringContaining('保存できません'),
|
||||
error: expect.stringContaining('Unable to save'),
|
||||
})
|
||||
await vi.waitFor(() => expect(client.close).toHaveBeenCalledTimes(1))
|
||||
expect(client.calls).toHaveBeenCalledWith('turn/interrupt', {
|
||||
@@ -750,7 +755,7 @@ function orphanedRun(): ResearchRun {
|
||||
turnId: 'orphan-turn',
|
||||
connectionIds: [account.id],
|
||||
deckVersion: 0,
|
||||
message: '調査中',
|
||||
message: 'Researching',
|
||||
citations: [],
|
||||
messages: [{ id: 'original', role: 'user', text: 'WebMCP' }],
|
||||
}
|
||||
@@ -763,7 +768,7 @@ it('marks an unfinished persisted turn interrupted after service restart', () =>
|
||||
expect(service.status().run).toMatchObject({
|
||||
...interrupted,
|
||||
status: 'interrupted',
|
||||
error: expect.stringContaining('再起動'),
|
||||
error: expect.stringContaining('server restart'),
|
||||
})
|
||||
expect(store.loadActive()?.status).toBe('interrupted')
|
||||
expect(clients).toHaveLength(0)
|
||||
|
||||
@@ -119,7 +119,7 @@ it.each([
|
||||
act(() => {
|
||||
expect(result.current.navigate(citation.post.url, [citation])).toBe(true)
|
||||
})
|
||||
expect(createTemporary).toHaveBeenCalledWith('引用元の投稿', [column])
|
||||
expect(createTemporary).toHaveBeenCalledWith('Cited posts', [column])
|
||||
expect(result.current.target?.deckId).toBe('created')
|
||||
})
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ export function useCitationNavigation({
|
||||
}))
|
||||
return true
|
||||
}
|
||||
const deck = createTemporary('引用元の投稿', [citation.column])
|
||||
const deck = createTemporary('Cited posts', [citation.column])
|
||||
setTarget((previous) => ({
|
||||
...citation,
|
||||
deckId: deck.id,
|
||||
|
||||
Reference in New Issue
Block a user