From f377ec7fbedfa09774dedd5f9b3939fd5f764091 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Sun, 2 Aug 2026 01:15:54 -0700 Subject: [PATCH] fix(dispatch): polish user-facing setup UX --- .../sections/sessions/SessionsSection.tsx | 4 - .../dispatch/DispatchInstallDialog.scss | 65 ++++- .../dispatch/DispatchInstallDialog.test.tsx | 91 ++++++- .../dispatch/DispatchInstallDialog.tsx | 238 +++++++++++------- .../features/dispatch/DispatchJobObserver.ts | 11 +- .../dispatch/DispatchResultDialog.scss | 26 ++ .../dispatch/DispatchResultDialog.test.tsx | 8 +- .../dispatch/DispatchResultDialog.tsx | 60 +++-- .../dispatch/DispatchTargetPicker.scss | 32 +++ .../dispatch/DispatchTargetPicker.tsx | 16 +- .../features/dispatch/useDispatchTargets.ts | 9 +- .../dispatch/DispatchSessionDriver.ts | 83 ++++-- src/web-ui/src/locales/en-US/common.json | 145 ++++++----- src/web-ui/src/locales/en-US/flow-chat.json | 36 ++- src/web-ui/src/locales/en-US/worktrees.json | 2 +- src/web-ui/src/locales/zh-CN/common.json | 145 ++++++----- src/web-ui/src/locales/zh-CN/flow-chat.json | 36 ++- src/web-ui/src/locales/zh-CN/worktrees.json | 2 +- src/web-ui/src/locales/zh-TW/common.json | 145 ++++++----- src/web-ui/src/locales/zh-TW/flow-chat.json | 36 ++- src/web-ui/src/locales/zh-TW/worktrees.json | 2 +- 21 files changed, 784 insertions(+), 408 deletions(-) diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx index 69f3e55d7d..4933f2354c 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx @@ -1241,9 +1241,6 @@ const SessionsSection: React.FC = ({ const dispatchTransport = session.config.dispatchJobId ? dispatchTransportByJobId[session.config.dispatchJobId] : undefined; - const dispatchTransportError = - dispatchTransport?.lastTransportError?.trim() - || t('nav.sessions.dispatchTransportErrorFallback'); const dispatchPresentation = isDispatched ? resolveDispatchNavPresentation({ targetLabel: dispatchTargetLabel, @@ -1256,7 +1253,6 @@ const SessionsSection: React.FC = ({ unreachableLabel: t('nav.sessions.dispatchUnreachable'), unreachableSummary: t('nav.sessions.dispatchUnreachableDetails', { target: dispatchTargetLabel, - error: dispatchTransportError, }), }) : null; diff --git a/src/web-ui/src/features/dispatch/DispatchInstallDialog.scss b/src/web-ui/src/features/dispatch/DispatchInstallDialog.scss index 21126e5f95..4aa0aa60a3 100644 --- a/src/web-ui/src/features/dispatch/DispatchInstallDialog.scss +++ b/src/web-ui/src/features/dispatch/DispatchInstallDialog.scss @@ -138,6 +138,10 @@ color: var(--color-text-primary); text-align: left; cursor: pointer; + transition: + background-color 140ms ease, + border-color 140ms ease, + transform 120ms ease-out; &:hover:not(:disabled), &:focus-visible, @@ -152,6 +156,10 @@ opacity: 0.55; } + &:active:not(:disabled) { + transform: scale(0.99); + } + // Keep the leading policy icon aligned with the text column. > svg:first-child { margin-top: 1px; @@ -182,15 +190,17 @@ } } - // Baseline summary and the opt-in for carrying local Git-visible changes. + // Project summary and the opt-in for carrying local Git-visible changes. + // This is a normal setup choice, so it uses a neutral surface rather than a + // warning treatment. &__consent { display: flex; flex-direction: column; gap: $size-gap-2; padding: $size-gap-3; - border: 1px solid var(--color-warning-border); + border: 1px solid var(--border-subtle); border-radius: $size-radius-base; - background: var(--color-warning-bg); + background: var(--element-bg-subtle); font-size: var(--font-size-xs); code { @@ -270,6 +280,27 @@ &[data-state='blocked'] > strong { color: var(--color-warning); } + + &[data-state='pending'] > strong { + color: var(--color-text-secondary); + } + } + } + + &__pending, + &__retry { + display: flex; + align-items: center; + gap: $size-gap-2; + color: var(--color-text-secondary); + font-size: var(--font-size-xs); + } + + &__retry { + justify-content: space-between; + + > span { + flex: 1; } } @@ -307,12 +338,26 @@ } } - &__blockers { - margin: 0; - padding-left: 18px; + &__details { + width: 100%; color: var(--color-text-secondary); font-size: var(--font-size-xs); - line-height: 1.5; + + summary { + width: fit-content; + color: var(--color-text-secondary); + cursor: pointer; + user-select: none; + + &:hover, + &:focus-visible { + color: var(--color-text-primary); + } + } + + &[open] summary { + margin-bottom: $size-gap-2; + } } &__output { @@ -344,6 +389,12 @@ } } +@media (prefers-reduced-motion: reduce) { + .dispatch-install-dialog__option { + transition-duration: 0.01ms; + } +} + @keyframes dispatch-install-spin { to { transform: rotate(360deg); diff --git a/src/web-ui/src/features/dispatch/DispatchInstallDialog.test.tsx b/src/web-ui/src/features/dispatch/DispatchInstallDialog.test.tsx index 4632eb79dd..aa19912e37 100644 --- a/src/web-ui/src/features/dispatch/DispatchInstallDialog.test.tsx +++ b/src/web-ui/src/features/dispatch/DispatchInstallDialog.test.tsx @@ -185,6 +185,7 @@ describe('DispatchInstallDialog installation lifecycle', () => { expect(container.textContent).toContain('dispatch.installAutomaticTitle'); expect(container.textContent).toContain('1.2.3'); expect(container.textContent).toContain('abc123'); + expect(container.querySelector('details')?.open).toBe(false); expect(container.textContent).not.toContain('dispatch.installConfirm'); expect(mocks.modalLifecycleProps).toEqual({ closeOnOverlayClick: true, @@ -335,8 +336,9 @@ describe('DispatchInstallDialog installation lifecycle', () => { await Promise.resolve(); }); - expect(container.textContent).toContain('target uses musl libc'); - expect(container.textContent).toContain('no cargo on the target'); + expect(container.textContent).toContain('dispatch.sourceBuildUnavailable'); + expect(container.textContent).not.toContain('target uses musl libc'); + expect(container.textContent).not.toContain('no cargo on the target'); const buttons = () => Array.from(container.querySelectorAll('button')); expect( buttons().find(button => button.textContent?.includes('dispatch.installConfirm')), @@ -390,6 +392,63 @@ describe('DispatchInstallDialog installation lifecycle', () => { expect(mocks.installCliSourceStart).toHaveBeenCalledWith('ssh-1'); }); + it('keeps protocol capability names and probe failures out of the user interface', async () => { + mocks.probeTarget.mockResolvedValueOnce({ + cliInstalled: true, + os: 'linux', + arch: 'x86_64', + installSupported: false, + protocol: { + protocolVersion: 4, + cliVersion: '1.2.3', + os: 'linux', + arch: 'x86_64', + capabilities: BASE_DISPATCH_CAPABILITIES.filter( + capability => capability !== 'workspace_git_sync', + ), + modelConfigured: true, + availableModels: ['model-a'], + }, + }); + + await act(async () => { + root.render( + , + ); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(container.textContent).toContain('dispatch.cliUpdateRequired'); + expect(container.textContent).not.toContain('workspace_git_sync'); + + mocks.probeTarget.mockRejectedValueOnce( + new Error('ssh handshake failed at internal transport stage'), + ); + await act(async () => { + root.render( + , + ); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(container.textContent).toContain('dispatch.probeFailed'); + expect(container.textContent).not.toContain('internal transport stage'); + }); + it('explains the Git baseline and never offers a snapshot delivery mode', async () => { await act(async () => { root.render( @@ -584,13 +643,13 @@ describe('DispatchInstallDialog model configuration sync', () => { .find(button => button.textContent?.includes('dispatch.syncModelConfirm')); } - async function mount() { + async function mount(onClose = vi.fn()) { await act(async () => { root.render( , ); @@ -605,7 +664,9 @@ describe('DispatchInstallDialog model configuration sync', () => { mocks.modalOnClose = null; mocks.probeTarget.mockImplementation(async () => probeResult()); mocks.confirmWarning.mockResolvedValue(true); - mocks.getConfig.mockResolvedValue([]); + mocks.getConfig.mockResolvedValue([ + { id: 'claude', enabled: true, api_key: 'secret' }, + ]); mocks.getFreshConfig.mockResolvedValue(undefined); mocks.resolveRevision.mockResolvedValue('a'.repeat(40)); container = document.createElement('div'); @@ -618,7 +679,7 @@ describe('DispatchInstallDialog model configuration sync', () => { container.remove(); }); - it('keeps model sync available after the target reports a usable model', async () => { + it('hides model sync after the target matches this device', async () => { await mount(); expect(syncButton()).toBeDefined(); @@ -638,7 +699,7 @@ describe('DispatchInstallDialog model configuration sync', () => { expect(mocks.syncModelConfig).toHaveBeenCalledWith('ssh-1'); // The sync re-probes so the model check reflects the target, not the write. expect(mocks.probeTarget.mock.calls.length).toBeGreaterThan(probesBeforeSync); - expect(syncButton()).toBeDefined(); + expect(syncButton()).toBeUndefined(); }); it('does not write the credential-bearing config when the confirmation is declined', async () => { @@ -655,10 +716,11 @@ describe('DispatchInstallDialog model configuration sync', () => { expect(syncButton()).toBeDefined(); }); - it('discards a late sync acknowledgement after the dialog closes', async () => { + it('keeps the dialog open while model sync is in progress', async () => { const sync = createDeferred(); + const onClose = vi.fn(); mocks.syncModelConfig.mockReturnValue(sync.promise); - await mount(); + await mount(onClose); await act(async () => { syncButton()?.click(); @@ -666,13 +728,19 @@ describe('DispatchInstallDialog model configuration sync', () => { await Promise.resolve(); }); expect(mocks.syncModelConfig).toHaveBeenCalledTimes(1); - const probesBeforeClose = mocks.probeTarget.mock.calls.length; + const probesBeforeSettle = mocks.probeTarget.mock.calls.length; await act(async () => { mocks.modalOnClose?.(); await Promise.resolve(); }); + expect(onClose).not.toHaveBeenCalled(); + expect(mocks.modalLifecycleProps).toEqual({ + closeOnOverlayClick: false, + showCloseButton: false, + }); + await act(async () => { modelConfigured = true; sync.resolve(undefined); @@ -680,7 +748,8 @@ describe('DispatchInstallDialog model configuration sync', () => { await Promise.resolve(); }); - expect(mocks.probeTarget.mock.calls.length).toBe(probesBeforeClose); + expect(mocks.probeTarget.mock.calls.length).toBeGreaterThan(probesBeforeSettle); + expect(syncButton()).toBeUndefined(); }); }); diff --git a/src/web-ui/src/features/dispatch/DispatchInstallDialog.tsx b/src/web-ui/src/features/dispatch/DispatchInstallDialog.tsx index 173a2cac4a..d9d6b14c55 100644 --- a/src/web-ui/src/features/dispatch/DispatchInstallDialog.tsx +++ b/src/web-ui/src/features/dispatch/DispatchInstallDialog.tsx @@ -10,6 +10,7 @@ import { createLogger } from '@/shared/utils/logger'; import { Check, Loader2, + RefreshCw, ShieldAlert, ShieldCheck, ShieldQuestion, @@ -63,10 +64,6 @@ interface DispatchInstallDialogProps { onReady: (selection: DispatchSelection) => void; } -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - export const DispatchInstallDialog: React.FC = ({ open, target, @@ -83,6 +80,7 @@ export const DispatchInstallDialog: React.FC = ({ const [worktreeSettingsLoading, setWorktreeSettingsLoading] = useState(true); const [probe, setProbe] = useState(null); const [probing, setProbing] = useState(false); + const [probeError, setProbeError] = useState(false); const [installing, setInstalling] = useState(false); const [syncingModel, setSyncingModel] = useState(false); const [installStart, setInstallStart] = useState(null); @@ -104,7 +102,7 @@ export const DispatchInstallDialog: React.FC = ({ const path = ''; const generation = ++generationRef.current; setProbing(true); - setError(null); + setProbeError(false); try { const result = await dispatchApi.probeTarget( target.kind === 'device' @@ -117,7 +115,12 @@ export const DispatchInstallDialog: React.FC = ({ } catch (nextError) { if (generation === generationRef.current) { setProbe(null); - setError(errorMessage(nextError)); + setProbeError(true); + log.warn('Failed to check dispatch target readiness', { + targetKind: target.kind, + targetId, + error: nextError, + }); } } finally { if (generation === generationRef.current) { @@ -136,6 +139,7 @@ export const DispatchInstallDialog: React.FC = ({ setValidatingBaseRef(false); setWorktreeSettingsLoading(true); setProbe(null); + setProbeError(false); setInstallStart(null); setInstallOutput(''); setInstalling(false); @@ -262,7 +266,11 @@ export const DispatchInstallDialog: React.FC = ({ if (generation === generationRef.current) { clearActiveInstall(generation); setInstalling(false); - setError(errorMessage(nextError)); + setError(t('dispatch.installFailed')); + log.warn('Failed while polling SSH CLI installation', { + connectionId, + error: nextError, + }); } } }, [clearActiveInstall, connectionId, runProbe, t]); @@ -301,7 +309,11 @@ export const DispatchInstallDialog: React.FC = ({ clearActiveInstall(generation); if (generation === generationRef.current) { setInstalling(false); - setError(errorMessage(nextError)); + setError(t('dispatch.sourceBuildFailed')); + log.warn('Failed to start SSH CLI source build', { + connectionId, + error: nextError, + }); } } }, [clearActiveInstall, connectionId, pollInstallation, t]); @@ -325,7 +337,11 @@ export const DispatchInstallDialog: React.FC = ({ } catch (nextError) { if (generation === generationRef.current) { setSyncingModel(false); - setError(errorMessage(nextError)); + setError(t('dispatch.syncModelFailed')); + log.warn('Failed to sync dispatch model configuration', { + connectionId, + error: nextError, + }); } return; } @@ -335,13 +351,21 @@ export const DispatchInstallDialog: React.FC = ({ await runProbe(); }, [connectionId, runProbe, t]); - const close = useCallback(() => { + const closeDialog = useCallback(() => { invalidateInstallLifecycle(); setInstalling(false); setSyncingModel(false); onClose(); }, [invalidateInstallLifecycle, onClose]); + const handleModalClose = useCallback(() => { + // Keep Escape, the close button, and backdrop clicks from silently + // abandoning a target mutation. A source build can still be stopped with + // the explicit footer action. + if (installing || syncingModel) return; + closeDialog(); + }, [closeDialog, installing, syncingModel]); + const protocol = probe?.protocol; const selectedApprovalCapability = approvalCapability(approvalPolicy); const requiredCapabilities = [ @@ -376,11 +400,18 @@ export const DispatchInstallDialog: React.FC = ({ && workspaceReady && (cliReady ? modelReady : installPending); + const localModelIds = syncableLocalModelIds(localModels); const targetModelCount = protocol?.availableModels?.length ?? 0; const modelParity = compareDispatchModels( - syncableLocalModelIds(localModels), + localModelIds, protocol?.availableModels, ); + const hasLocalModelsToSync = (localModelIds?.length ?? 0) > 0; + const offerModelSync = + target?.kind === 'ssh' + && !!protocol + && hasLocalModelsToSync + && (!modelReady || modelParity === 'diverged'); // The probe carries ids, which name nothing a user recognizes. Resolve the // target's default through the local catalog when the two agree; when they // do not, the id would be misleading anyway and the count is the actionable @@ -389,7 +420,7 @@ export const DispatchInstallDialog: React.FC = ({ const id = protocol?.defaultModel?.trim(); if (!id) return t('dispatch.modelAutomatic'); const local = localModels?.find(model => model.id?.trim() === id); - return local ? getModelDisplayName(local) : id; + return local ? getModelDisplayName(local) : t('dispatch.modelAutomatic'); })(); const confirmTarget = async () => { @@ -456,10 +487,10 @@ export const DispatchInstallDialog: React.FC = ({ return ( = ({ /> ) : null} +
+
+

+ {t('dispatch.readinessTitle')} +

+
+
+ {probing || (!probe && !probeError) ? ( +
+ + {t('dispatch.checkingTarget')} +
+ ) : null} + {probeError ? ( +
+ + {t('dispatch.probeFailed')} + + +
+ ) : null} + {probe ? ( +
+
+ {t('dispatch.cliStatus')} + + {cliReady + ? t('dispatch.cliReady', { version: protocol?.cliVersion }) + : installPending + ? t('dispatch.cliWillInstall') + : probe.cliInstalled + ? t('dispatch.cliUpdateRequired') + : t('dispatch.cliUnavailable')} + +
+
+ {t('dispatch.modelStatus')} + + {!protocol + ? t('dispatch.modelCheckPending') + : !modelReady + ? localModelIds?.length === 0 + ? t('dispatch.modelMissingOnBoth') + : t('dispatch.modelMissing') + : modelParity === 'match' + ? t('dispatch.modelMatchesLocal', { model: targetDefaultModelLabel }) + : modelParity === 'diverged' + ? t('dispatch.modelDiffersFromLocal', { count: targetModelCount }) + : t('dispatch.modelReadyCount', { count: targetModelCount })} + +
+
+ ) : null} + {target?.kind === 'device' && probe && !cliReady ? ( +
+ + {t('dispatch.deviceUpdateRequired')} + + +
+ ) : null} +
+
+

@@ -535,55 +646,6 @@ export const DispatchInstallDialog: React.FC = ({

- {probe ? ( -
-
-

- {t('dispatch.readinessTitle')} -

-
-
-
-
- {t('dispatch.cliStatus')} - - {cliReady - ? t('dispatch.cliReady', { version: protocol?.cliVersion }) - : probe.cliInstalled && protocol - ? t('dispatch.cliIncompatible', { - details: protocol.protocolVersion !== DISPATCH_PROTOCOL_VERSION - ? t('dispatch.protocolVersionMismatch', { - expected: DISPATCH_PROTOCOL_VERSION, - actual: protocol.protocolVersion, - }) - : missingCapabilities.join(', '), - }) - : t('dispatch.cliMissing')} - -
-
- {t('dispatch.modelStatus')} - - {!modelReady - ? protocol?.modelDiagnostic || t('dispatch.modelMissing') - : modelParity === 'match' - ? t('dispatch.modelMatchesLocal', { model: targetDefaultModelLabel }) - : modelParity === 'diverged' - ? t('dispatch.modelDiffersFromLocal', { count: targetModelCount }) - : t('dispatch.modelReadyCount', { count: targetModelCount })} - -
-
-
-
- ) : null} - - {probe?.prebuiltIncompatible ? ( - - ) : probe?.installError ? ( - - ) : null} - {target?.kind === 'ssh' && !cliReady && probe?.release ? (
@@ -595,13 +657,14 @@ export const DispatchInstallDialog: React.FC = ({ {t('dispatch.installAutomaticDescription')} - {/* The digest is still shown: automatic installation removed the - prompt, not the verification it used to display. */} -
-
{t('dispatch.version')}
{probe.release.version}
-
{t('dispatch.downloadUrl')}
{probe.release.url}
-
SHA256
{probe.release.sha256}
-
+
+ {t('dispatch.installDetails')} +
+
{t('dispatch.version')}
{probe.release.version}
+
{t('dispatch.downloadUrl')}
{probe.release.url}
+
{t('dispatch.integrity')}
{probe.release.sha256}
+
+
) : null} @@ -615,19 +678,15 @@ export const DispatchInstallDialog: React.FC = ({
- {t('dispatch.sourceBuildDescription', { ref: sourceBuild.gitRef })} + {t('dispatch.sourceBuildDescription')} - {sourceBuild.blockers.length > 0 ? ( -
    - {sourceBuild.blockers.map(blocker => ( -
  • {blocker}
  • - ))} -
+ {!sourceBuild.supported ? ( + ) : null}
- + ) : null} + {!loading && !error && sshTargets.length === 0 ? (
- {error || t('chatInput.dispatch.noSshTargets')} + {t('chatInput.dispatch.noSshTargets')}
) : null} {sshTargets.map(option => { diff --git a/src/web-ui/src/features/dispatch/useDispatchTargets.ts b/src/web-ui/src/features/dispatch/useDispatchTargets.ts index a40071638a..d4778d6775 100644 --- a/src/web-ui/src/features/dispatch/useDispatchTargets.ts +++ b/src/web-ui/src/features/dispatch/useDispatchTargets.ts @@ -8,27 +8,26 @@ const log = createLogger('DispatchTargets'); export function useDispatchTargets(enabled = true): { targets: DispatchTargetOption[]; loading: boolean; - error: string | null; + error: boolean; refresh: () => Promise; } { const [targets, setTargets] = useState([]); const [loading, setLoading] = useState(false); const [loaded, setLoaded] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(false); const refresh = useCallback(async () => { if (!enabled) return; setLoading(true); - setError(null); + setError(false); try { const nextTargets = await dispatchApi.listTargets(); setTargets(nextTargets.filter( target => target.kind === 'local' || target.kind === 'ssh' || target.kind === 'device', )); } catch (nextError) { - const message = nextError instanceof Error ? nextError.message : String(nextError); log.warn('Failed to list dispatch targets', { error: nextError }); - setError(message); + setError(true); setTargets([{ kind: 'local', displayName: 'Local' }]); } finally { setLoading(false); diff --git a/src/web-ui/src/flow_chat/session-drivers/dispatch/DispatchSessionDriver.ts b/src/web-ui/src/flow_chat/session-drivers/dispatch/DispatchSessionDriver.ts index c4ccbd612d..8d12b68df3 100644 --- a/src/web-ui/src/flow_chat/session-drivers/dispatch/DispatchSessionDriver.ts +++ b/src/web-ui/src/flow_chat/session-drivers/dispatch/DispatchSessionDriver.ts @@ -58,8 +58,6 @@ import { applyGeneratingTitlePlaceholder } from '../shared'; const log = createLogger('DispatchSessionDriver'); -const IMAGES_WHILE_RUNNING_MESSAGE = - 'Images join the next turn; wait for the current dispatch turn to finish'; const DEVICE_ATTACHMENT_BUDGET_BYTES = 192 * 1024; const APPEND_RETRY_SCOPE = 'dispatch-append'; const CONTINUE_RETRY_SCOPE = 'dispatch-continue'; @@ -96,7 +94,9 @@ function dispatchAttachments( const attachments = imageContexts.map(image => { const dataUrl = image.data_url?.trim(); if (!dataUrl) { - throw new Error('This image has no inline data and cannot be sent to a dispatch target'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.attachmentUnavailable'), + ); } const name = typeof image.metadata?.name === 'string' ? image.metadata.name : undefined; return { @@ -110,7 +110,7 @@ function dispatchAttachments( const total = attachments.reduce((sum, attachment) => sum + attachment.dataUrl.length, 0); if (total > DEVICE_ATTACHMENT_BUDGET_BYTES) { throw new Error( - 'Device dispatch carries at most 192 KiB of inline images; use an SSH target for larger screenshots', + i18nService.t('flow-chat:chatInput.dispatch.errors.deviceAttachmentTooLarge'), ); } } @@ -157,7 +157,9 @@ async function appendToDispatchJob( ); if (!response.accepted) { releaseSubmissionRetry(APPEND_RETRY_SCOPE, sessionId, retry.id); - throw new Error('Dispatch target did not accept the appended message'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.appendRejected'), + ); } releaseSubmissionRetry(APPEND_RETRY_SCOPE, sessionId, retry.id); requestDispatchJobRefresh(jobId); @@ -239,7 +241,9 @@ async function continueDispatchJob( }, ); if (!response.accepted) { - throw new Error('Dispatch target did not accept the follow-up turn'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.followUpRejected'), + ); } // The target owns the job state; the refresh below reads it back rather // than this side guessing what the follow-up did to it. @@ -321,7 +325,9 @@ export const dispatchSessionDriver: SessionDriver = { } = seed; if (!isNonLocalDispatchTarget(config.dispatchTargetRequest)) { - throw new Error('Dispatch driver requires a non-local dispatch target request'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.targetInvalid'), + ); } const dispatchTarget: DispatchTarget = config.dispatchTarget ?? ( @@ -344,7 +350,9 @@ export const dispatchSessionDriver: SessionDriver = { ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`; const approvalPolicy = config.dispatchApprovalPolicy; if (!approvalPolicy) { - throw new Error('Dispatch approval policy must be selected before creating a session'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.approvalRequired'), + ); } const resolvedConfig: SessionConfig = { ...config, @@ -447,7 +455,9 @@ export const dispatchSessionDriver: SessionDriver = { ): Promise { const session = context.flowChatStore.getState().sessions.get(sessionId); if (!session) { - throw new Error(`Session does not exist: ${sessionId}`); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.sessionUnavailable'), + ); } await context.flowChatStore.updateSessionTitle(sessionId, title, 'generated'); if (session.config.dispatchJobId) { @@ -480,11 +490,15 @@ export const dispatchSessionDriver: SessionDriver = { const session = context.flowChatStore.getState().sessions.get(sessionId); const jobId = session?.config.dispatchJobId; if (!jobId) { - throw new Error('Dispatch session is missing its job id'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.sessionUnavailable'), + ); } const state = session?.config.dispatchJobState; if (!isDispatchJobTerminal(state)) { - throw new Error('Wait for the current dispatch turn to finish before compacting'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.compactWhileRunning'), + ); } // Same idempotency contract as a prompt follow-up: a retried request // reuses the turn id so a lost response cannot start two compactions. @@ -502,7 +516,9 @@ export const dispatchSessionDriver: SessionDriver = { kind: 'compact', }); if (!response.accepted) { - throw new Error('Dispatch target did not accept the compact turn'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.compactRejected'), + ); } } finally { releaseSubmissionRetry(COMPACT_RETRY_SCOPE, sessionId, retry.id); @@ -517,11 +533,15 @@ export const dispatchSessionDriver: SessionDriver = { ): Promise<{ inserted: boolean }> { const session = context.flowChatStore.getState().sessions.get(sessionId); if (!session) { - throw new Error(`Session does not exist: ${sessionId}`); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.sessionUnavailable'), + ); } const jobId = jobIdForSession(sessionId); if (!jobId) { - throw new Error('Dispatch session is missing its job id'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.sessionUnavailable'), + ); } const { runUsageReportCommand } = await import('../../services/usageReportService'); const result = await runUsageReportCommand({ @@ -561,7 +581,9 @@ export const dispatchSessionDriver: SessionDriver = { ): Promise { const jobId = jobIdForSession(sessionId); if (!jobId) { - throw new Error('Dispatch session is missing its job id'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.sessionUnavailable'), + ); } await dispatchApi.answerPermission(jobId, requestId, reply, feedback); requestDispatchJobRefresh(jobId); @@ -575,7 +597,9 @@ export const dispatchSessionDriver: SessionDriver = { ): Promise { const jobId = jobIdForSession(sessionId); if (!jobId) { - throw new Error('Dispatch session is missing its job id'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.sessionUnavailable'), + ); } const pending = (dispatchJobStore.getState().jobs[jobId]?.pendingPermissions ?? []) as unknown as PermissionRequest[]; @@ -614,7 +638,12 @@ export const dispatchSessionDriver: SessionDriver = { if (draft.hasImages) { // Steering has no attachment channel; the runtime accepts images only // at turn boundaries. - return { kind: 'reject', reason: IMAGES_WHILE_RUNNING_MESSAGE }; + return { + kind: 'reject', + reason: i18nService.t( + 'flow-chat:chatInput.dispatch.errors.imagesWhileRunning', + ), + }; } return { kind: 'steer' }; }, @@ -627,7 +656,9 @@ export const dispatchSessionDriver: SessionDriver = { const session = context.flowChatStore.getState().sessions.get(sessionId); const jobId = session?.config.dispatchJobId; if (!jobId) { - throw new Error('Dispatch session is missing its job id'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.sessionUnavailable'), + ); } await appendToDispatchJob(sessionId, jobId, draft.message, draft.displayMessage); }, @@ -651,13 +682,17 @@ export const dispatchSessionDriver: SessionDriver = { const jobId = readySession.config.dispatchJobId; const approvalPolicy = readySession.config.dispatchApprovalPolicy; if (!targetRequest || targetRequest.kind === 'local' || !jobId || !approvalPolicy) { - throw new Error('Dispatch session is missing its immutable target or approval policy'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.sessionUnavailable'), + ); } const dispatchState = readySession.config.dispatchJobState; if (dispatchState === 'queued' || dispatchState === 'running') { if ((options?.imageContexts?.length ?? 0) > 0) { // Steering has no attachment channel; images ride turn boundaries. - throw new Error(IMAGES_WHILE_RUNNING_MESSAGE); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.imagesWhileRunning'), + ); } // A turn is already in flight; this message steers it rather than // starting another one underneath it. @@ -675,7 +710,9 @@ export const dispatchSessionDriver: SessionDriver = { dispatchState !== 'submitting' && dispatchState !== 'submission_unknown' ) { - throw new Error('This dispatch session is not ready to accept a message'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.sessionNotReady'), + ); } if (isFirstMessage) { applyGeneratingTitlePlaceholder(context, sessionId, message); @@ -745,7 +782,9 @@ export const dispatchSessionDriver: SessionDriver = { }); } if (!response.accepted || response.jobId !== jobId || response.sessionId !== sessionId) { - throw new Error('Dispatch target returned a mismatched acknowledgement'); + throw new Error( + i18nService.t('flow-chat:chatInput.dispatch.errors.submissionUnconfirmed'), + ); } context.flowChatStore.applyDispatchSnapshot(sessionId, { jobId, diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index ae1d1c0987..c18662b145 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -157,14 +157,13 @@ }, "sessions": { "newSession": "New session", - "dispatchRunningOn": "Runs on {{target}} · {{state}}", - "dispatchUnreachable": "Target unreachable", - "dispatchUnreachableDetails": "Target unreachable: {{target}} · {{error}}", - "dispatchTransportErrorFallback": "Transport request failed", + "dispatchRunningOn": "Runs on {{target}} ({{state}})", + "dispatchUnreachable": "Connection lost", + "dispatchUnreachableDetails": "BitFun cannot reach {{target}} right now. The task may still be running there.", "dispatchStates": { - "submitting": "submitting", - "submission_unknown": "checking submission", - "queued": "queued", + "submitting": "sending", + "submission_unknown": "checking", + "queued": "waiting", "running": "$t(shared:statuses.running)", "succeeded": "$t(shared:statuses.done)", "failed": "$t(shared:statuses.failed)", @@ -1460,78 +1459,92 @@ } }, "dispatch": { - "configureTitle": "Prepare {{target}}", - "configureSubtitle": "Confirm the Git baseline, target readiness, and unattended approval policy.", - "readinessTitle": "Target readiness", - "deliveryTitle": "Git worktree baseline", - "baselineSource": "Source repository", - "baselineDescription": "BitFun creates a managed worktree as an isolated baseline. The target checks out the same commit and works on its own dispatch branch.", - "baseRef": "Base revision", - "baseRefHint": "Defaults to HEAD. You can enter a branch, tag, or commit that exists in this repository.", - "baseRefInvalid": "Could not resolve \"{{ref}}\" in the source repository. Check the branch, tag, or commit and try again.", - "includeUncommitted": "Include uncommitted Git-visible changes", - "includeUncommittedHint": "Changes accepted by git add -A are committed into the baseline. Ignored files such as local .env files and build output are never transferred.", - "sourceBuildTitle": "Build from source", - "sourceBuildDescription": "Compile the controller-matched source ({{ref}}) on the target. Compatibility is verified by capabilities instead of guessed from the version label; this takes a while.", + "configureTitle": "Run on {{target}}", + "configureSubtitle": "Choose what to send and how this task should handle permission requests.", + "readinessTitle": "Target check", + "checkingTarget": "Checking target…", + "probeFailed": "Could not check this target. Make sure it is online and the connection works.", + "retryCheck": "Check again", + "deliveryTitle": "Code to send", + "baselineSource": "Project", + "baselineDescription": "BitFun creates an isolated copy from the selected revision. Remote changes do not modify your current workspace.", + "baseRef": "Starting revision", + "baseRefHint": "Uses HEAD by default. You can also enter a branch, tag, or commit from this repository.", + "baseRefInvalid": "\"{{ref}}\" was not found in this repository. Check the branch, tag, or commit and try again.", + "includeUncommitted": "Include uncommitted changes", + "includeUncommittedHint": "Only Git-visible changes are sent. Ignored files, including local .env files and build output, stay on this device.", + "sourceBuildTitle": "Build BitFun on the target", + "sourceBuildDescription": "No compatible download is available for this target. You can build BitFun there instead; this usually takes tens of minutes.", + "sourceBuildUnavailable": "The target needs Rust, Git, a C compiler, and about 6 GB of free space before it can build BitFun.", "sourceBuildConfirm": "Build from source", "sourceBuildConfirmTitle": "Build the BitFun CLI from source on this target?", - "sourceBuildConfirmMessage": "The selected source will be transferred or cloned on the target and built with cargo build --release. Needs about 6 GB free and can take tens of minutes.", - "cliStatus": "BitFun CLI", + "sourceBuildConfirmMessage": "BitFun source code will be transferred to and built on the target. The build needs about 6 GB free and can take tens of minutes.", + "sourceBuildFailed": "Could not start the build. Check the target connection and build prerequisites, then try again.", + "stopSourceBuild": "Stop build", + "cliStatus": "$t(shared:product.name)", "cliReady": "Ready ({{version}})", - "cliMissing": "Not installed or unreachable", - "cliIncompatible": "Update required: {{details}}", - "protocolVersionMismatch": "protocol {{actual}}; expected {{expected}}", - "modelStatus": "Target model", - "modelMatchesLocal": "Ready (same as this device · {{model}})", - "modelDiffersFromLocal": "Ready (differs from this device · {{count}} models on target)", - "modelReadyCount": "Ready ({{count}} models on target)", - "modelAutomatic": "target default", + "cliWillInstall": "Will be prepared when the task starts", + "cliUpdateRequired": "Needs an update before it can run tasks", + "cliUnavailable": "BitFun is not installed on this target", + "deviceUpdateRequired": "Update BitFun on this device, then check again.", + "modelStatus": "Model", + "modelMatchesLocal": "Ready. Default: {{model}}", + "modelDiffersFromLocal": "Ready. {{count}} model(s) are available; settings differ from this device.", + "modelReadyCount": "Ready. {{count}} model(s) are available.", + "modelAutomatic": "Target default", + "modelCheckPending": "Will be checked after BitFun is ready", "modelMissing": "No usable model is configured on the target", - "syncModelRequired": "Sync model configuration", - "syncModelDescription": "Replace the target's model catalog and defaults with this device's configuration, including API credentials.", + "modelMissingOnBoth": "No usable model is configured here or on the target. Add a model in Settings first.", + "syncModelTitle": "Use this device's models", + "syncModelDescription": "Copies this device's model settings and API credentials to the target, replacing its current model settings.", "syncModelConfirmTitle": "Sync model configuration to this target?", - "syncModelConfirmMessage": "This device's model catalog and default model selections, including API credentials, will be written to the target user's BitFun config file with owner-only permissions.", + "syncModelConfirmMessage": "This device's model settings and API credentials will be written to the target user's BitFun configuration, replacing its current model settings.", "syncModelConfirm": "Sync", "syncingModel": "Syncing…", - "installAutomaticTitle": "Automatic CLI installation", - "installAutomaticDescription": "When you send the task, BitFun will install this signed release automatically, verify its SHA256 digest, and record the action in the dispatch audit log.", + "syncModelFailed": "Could not sync model settings. Check the target connection and try again.", + "installAutomaticTitle": "Automatic setup", + "installAutomaticDescription": "When you send the task, BitFun will prepare this target automatically. No manual installation is needed.", + "installDetails": "Installation details", "version": "Version", - "downloadUrl": "Download", - "installing": "Installing…", - "installFailed": "CLI installation failed. Review the output and try again.", - "installOutput": "CLI installation output", - "installWaiting": "Waiting for installation output…", - "approvalTitle": "Unattended permission policy", - "approvalHint": "Choose explicitly. This applies only to this dispatched task.", - "approvalReject": "Reject and report", - "approvalRejectDescription": "Reject actions that require confirmation and report them in the transcript.", + "downloadUrl": "Source", + "integrity": "Integrity check", + "installing": "Building…", + "installFailed": "Could not prepare BitFun on the target. Review the build output and try again.", + "installOutput": "Build output", + "installWaiting": "Waiting for build output…", + "approvalTitle": "Permission requests", + "approvalHint": "Choose how this task should handle actions that need confirmation.", + "approvalReject": "Reject automatically", + "approvalRejectDescription": "Reject the action and explain it in the conversation.", "approvalRemote": "Ask this device", - "approvalRemoteDescription": "Pause the target task and answer permission requests from this observer.", - "approvalAuto": "Auto approve", - "approvalAutoDescription": "Automatically approve permission requests on the target for this dispatched task. Sending the task applies this policy.", + "approvalRemoteDescription": "Pause the task and wait for you to decide here.", + "approvalAuto": "Approve automatically", + "approvalAutoDescription": "Allow requested actions without asking. Use only on a target you trust.", "useTarget": "Use this target", "cancel": "Cancel", - "eventHistoryIncomplete": "Some dispatched task events were omitted or expired. The visible transcript may be incomplete.", - "completionTitle": "Dispatched task completed", - "completionFailedTitle": "Dispatched task failed", - "completionBody": "{{task}} on {{target}}", - "permissionTitle": "Dispatched task needs approval", + "eventHistoryIncomplete": "Some task history is no longer available. The visible conversation may be incomplete.", + "completionTitle": "Remote task completed", + "completionFailedTitle": "Remote task failed", + "completionBody": "{{task}} ({{target}})", + "permissionTitle": "Remote task needs confirmation", "permissionBody": "{{task}} has {{count}} permission request(s) waiting.", "localTarget": "this computer", - "syncTitle": "Sync dispatch branch", - "syncSubtitle": "Commit the target worktree and fetch its branch into the managed baseline worktree.", - "syncSubtitleWithTarget": "Commit the worktree on {{target}} and fetch its branch into the managed baseline worktree.", - "syncBranch": "Dispatch branch", - "syncBaselineWorktree": "Baseline worktree", - "syncBaselineMissing": "The managed baseline worktree is missing. This dispatch can no longer be synced automatically.", - "syncingResult": "Committing and transferring the dispatch branch…", - "syncSucceeded": "Synced {{count}} commit(s) into the baseline worktree.", - "syncHeadCommit": "Synced head commit", - "syncChangedFiles": "Changed files", - "syncNoFileList": "The commit was synced, but the target did not return a file list.", - "syncChangesTruncated": "Only part of the changed-file list is shown. The full Git history was synced.", - "syncNoChanges": "The target worktree still matches the baseline; there is nothing to sync.", - "syncAction": "Sync to baseline", + "syncTitle": "Get remote changes", + "syncSubtitle": "Bring the latest remote task changes to this device without modifying your current workspace.", + "syncSubtitleWithTarget": "Bring the latest changes from {{target}} to this device without modifying your current workspace.", + "syncDetails": "Save location and branch", + "syncBranch": "Result branch", + "syncBaselineWorktree": "Saved in", + "syncBaselineMissing": "The local copy used to receive changes was deleted, so BitFun cannot fetch them automatically. Handle the result on the target instead.", + "syncingResult": "Getting remote changes…", + "syncSucceeded": "Fetched {{count}} commit(s). Your current workspace was not modified.", + "syncHeadCommit": "Latest commit", + "syncChangedFiles": "Changes fetched", + "syncNoFileList": "The commits were fetched, but the changed-file list is unavailable.", + "syncChangesTruncated": "Only some files are shown. All commits were fetched.", + "syncNoChanges": "There are no new remote changes.", + "syncFailed": "Could not get remote changes. Make sure the target is online and the repository is available, then try again.", + "syncAction": "Get changes", "syncClose": "Close" }, "collapse": "Collapse", diff --git a/src/web-ui/src/locales/en-US/flow-chat.json b/src/web-ui/src/locales/en-US/flow-chat.json index 19ca9fcc76..24f8e60569 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -642,28 +642,42 @@ "locked": "Runs on {{target}} (fixed for this task)", "current": "Run this task on {{target}}", "menuLabel": "Where this task runs", - "sessionScope": "New task", + "sessionScope": "This task only", "localSection": "Local", "localDescription": "Run in this BitFun app", "sshSection": "SSH", "loading": "Loading targets…", "noSshTargets": "No saved SSH targets", + "targetLoadFailed": "Could not load SSH targets. Try again", "sshDescription": "Run through a saved SSH connection", "addSsh": "Add SSH connection…", "deviceSection": "Account devices", "signInDevices": "Sign in to use another device…", "noDeviceTargets": "No other account devices", - "deviceDescription": "Run over encrypted account Relay RPC", + "deviceDescription": "Run on another device signed in to your account", "deviceOffline": "Offline", - "createFailed": "Could not create the dispatched task.", - "remoteTarget": "the remote target", - "transferInProgress": "Creating the baseline worktree, fetching the repository, and transferring missing Git objects…", - "cliInstallStarted": "Installing verified BitFun CLI {{version}} for {{target}} on the SSH target.", - "cliInstallSucceeded": "Verified BitFun CLI {{version}} is ready on the SSH target.", - "cliInstallFailed": "BitFun CLI installation failed on the SSH target: {{details}}", - "cliInstallStage": "SSH target CLI setup: {{stage}}", - "cliInstallUnknownVersion": "release", - "cliInstallUnknownStage": "status update" + "createFailed": "Could not create the remote task. Check the target and try again.", + "remoteTarget": "the remote device", + "transferInProgress": "Preparing the target and transferring the project…", + "cliInstallStarted": "Preparing BitFun {{version}} on the remote device…", + "cliInstallSucceeded": "BitFun {{version}} is ready on the remote device.", + "cliInstallFailed": "Could not prepare the remote environment. Check the SSH connection and try again.", + "cliInstallInProgress": "Preparing the remote environment…", + "cliInstallUnknownVersion": "required version", + "errors": { + "attachmentUnavailable": "This image cannot be sent to the remote device. Add it again and retry.", + "deviceAttachmentTooLarge": "Images sent to an account device can total at most 192 KB. Compress them or use an SSH target.", + "appendRejected": "The target did not receive this message. Try again shortly.", + "followUpRejected": "The target did not start the next turn. Try again shortly.", + "targetInvalid": "The remote target is no longer valid. Select it again.", + "approvalRequired": "Choose how permission requests should be handled first.", + "sessionUnavailable": "Remote task information is incomplete. Reopen the task or create a new one.", + "compactWhileRunning": "Wait for the current remote task to finish before compacting the context.", + "compactRejected": "The target could not start compaction. Try again shortly.", + "imagesWhileRunning": "Images will be sent with the next turn. Wait for the current remote task to finish and try again.", + "sessionNotReady": "The remote task is still being prepared. Try sending again shortly.", + "submissionUnconfirmed": "BitFun cannot yet confirm whether the target received this task. Check the task status shortly." + } }, "addBoostTooltip": "Agent modes, image, or skills", "permissionMode": { diff --git a/src/web-ui/src/locales/en-US/worktrees.json b/src/web-ui/src/locales/en-US/worktrees.json index 82b863e056..3c3dad1a1e 100644 --- a/src/web-ui/src/locales/en-US/worktrees.json +++ b/src/web-ui/src/locales/en-US/worktrees.json @@ -9,7 +9,7 @@ "togglePendingOnDescription": "Worktree isolation is armed. The worktree will be created after you send the first message.", "togglePendingOffDescription": "Worktree isolation will be turned off after you send the first message.", "toggleLocked": "Worktree isolation can only be changed before the session's first message.", - "dispatchBaseline": "This dispatch runs against a managed worktree baseline of this repository. The baseline is fixed when the target is chosen.", + "dispatchBaseline": "Remote tasks run in an isolated copy. This cannot be changed after you select a target.", "retained": "The worktree still held local work and was kept at {{path}}." }, "settings": { diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index 6b720c8c5c..9c8b91b731 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -157,14 +157,13 @@ }, "sessions": { "newSession": "新建会话", - "dispatchRunningOn": "运行在 {{target}} · {{state}}", - "dispatchUnreachable": "目标不可达", - "dispatchUnreachableDetails": "目标不可达:{{target}} · {{error}}", - "dispatchTransportErrorFallback": "传输请求失败", + "dispatchRunningOn": "在 {{target}} 上运行({{state}})", + "dispatchUnreachable": "连接中断", + "dispatchUnreachableDetails": "暂时无法连接 {{target}}。任务可能仍在该设备上运行。", "dispatchStates": { - "submitting": "待提交", - "submission_unknown": "正在确认提交状态", - "queued": "排队中", + "submitting": "正在发送", + "submission_unknown": "正在确认", + "queued": "等待运行", "running": "$t(shared:statuses.running)", "succeeded": "$t(shared:statuses.done)", "failed": "$t(shared:statuses.failed)", @@ -1460,78 +1459,92 @@ } }, "dispatch": { - "configureTitle": "准备 {{target}}", - "configureSubtitle": "确认 Git 基线、目标就绪状态与无人值守权限策略。", - "readinessTitle": "目标就绪状态", - "deliveryTitle": "Git worktree 基线", - "baselineSource": "源代码仓库", - "baselineDescription": "BitFun 会创建一个受管 worktree 作为隔离基线;目标端检出同一个 commit,并在独立的派发分支上工作。", - "baseRef": "基准版本", - "baseRefHint": "默认为 HEAD,也可填写此仓库中存在的分支、标签或 commit。", - "baseRefInvalid": "无法在源代码仓库中解析“{{ref}}”。请检查分支、标签或 commit 后重试。", - "includeUncommitted": "包含 Git 可见的未提交改动", - "includeUncommittedHint": "可被 git add -A 纳入的改动会提交到基线中;本地 .env、构建产物等被忽略文件绝不会传输。", - "sourceBuildTitle": "从源码编译", - "sourceBuildDescription": "在目标上编译与当前控制端一致的源码({{ref}})。兼容性按能力校验,不再仅凭版本号猜测;耗时较长。", + "configureTitle": "在 {{target}} 上运行", + "configureSubtitle": "选择要发送的代码,以及任务遇到权限请求时的处理方式。", + "readinessTitle": "目标检查", + "checkingTarget": "正在检查目标…", + "probeFailed": "无法检查此目标。请确认设备在线且连接可用。", + "retryCheck": "重新检查", + "deliveryTitle": "要发送的代码", + "baselineSource": "项目", + "baselineDescription": "BitFun 会根据所选版本创建隔离副本,远程改动不会直接影响当前工作区。", + "baseRef": "起始版本", + "baseRefHint": "默认使用 HEAD,也可填写此仓库中的分支、标签或 commit。", + "baseRefInvalid": "此仓库中找不到“{{ref}}”。请检查分支、标签或 commit 后重试。", + "includeUncommitted": "包含未提交的改动", + "includeUncommittedHint": "只会发送 Git 可见的改动;.env、构建产物等忽略文件会保留在本机。", + "sourceBuildTitle": "在目标上编译 BitFun", + "sourceBuildDescription": "此目标没有可用的安装包。你可以改为在目标上编译 BitFun,通常需要数十分钟。", + "sourceBuildUnavailable": "目标需要安装 Rust、Git 和 C 编译器,并预留约 6 GB 空间,才能开始编译。", "sourceBuildConfirm": "从源码编译", "sourceBuildConfirmTitle": "在此目标上从源码编译 BitFun CLI?", - "sourceBuildConfirmMessage": "会把所选源码传输或克隆到目标上,并执行 cargo build --release。需要约 6 GB 可用空间,可能耗时数十分钟。", - "cliStatus": "BitFun CLI", + "sourceBuildConfirmMessage": "BitFun 源码将传输到目标并在目标上编译。需要约 6 GB 可用空间,可能耗时数十分钟。", + "sourceBuildFailed": "无法开始编译。请检查目标连接和编译环境后重试。", + "stopSourceBuild": "停止编译", + "cliStatus": "$t(shared:product.name)", "cliReady": "就绪({{version}})", - "cliMissing": "未安装或无法连接", - "cliIncompatible": "需要更新:{{details}}", - "protocolVersionMismatch": "协议版本 {{actual}},需要 {{expected}}", - "modelStatus": "目标模型", - "modelMatchesLocal": "就绪(与本机一致 · {{model}})", - "modelDiffersFromLocal": "就绪(与本机不同 · 目标有 {{count}} 个模型)", - "modelReadyCount": "就绪(目标有 {{count}} 个模型)", + "cliWillInstall": "发送任务时自动准备", + "cliUpdateRequired": "需要更新后才能运行任务", + "cliUnavailable": "此目标尚未安装 BitFun", + "deviceUpdateRequired": "请在此设备上更新 BitFun,然后重新检查。", + "modelStatus": "模型", + "modelMatchesLocal": "可用,默认使用 {{model}}", + "modelDiffersFromLocal": "可用,目标有 {{count}} 个模型,与本机配置不同", + "modelReadyCount": "可用,目标有 {{count}} 个模型", "modelAutomatic": "目标默认模型", - "modelMissing": "目标上没有可用的模型配置", - "syncModelRequired": "同步模型配置", - "syncModelDescription": "使用本机模型配置(含 API 密钥)替换目标端的模型列表与默认选择。", + "modelCheckPending": "BitFun 就绪后再检查", + "modelMissing": "目标上没有可用模型", + "modelMissingOnBoth": "本机和目标均没有可用模型,请先在设置中添加模型。", + "syncModelTitle": "使用本机模型", + "syncModelDescription": "将本机模型配置和 API 密钥复制到目标,并替换目标现有的模型配置。", "syncModelConfirmTitle": "同步模型配置到此目标?", - "syncModelConfirmMessage": "本机的模型列表与默认模型选择(包含 API 密钥)将写入目标用户的 BitFun 配置文件,且仅目标用户可读。", + "syncModelConfirmMessage": "本机模型配置和 API 密钥将写入目标用户的 BitFun 配置,并替换目标现有的模型设置。", "syncModelConfirm": "同步", "syncingModel": "正在同步…", - "installAutomaticTitle": "自动安装 CLI", - "installAutomaticDescription": "发送任务时,BitFun 会自动安装此签名发行版、校验 SHA256 摘要,并把操作写入派发审计日志。", + "syncModelFailed": "无法同步模型配置。请检查目标连接后重试。", + "installAutomaticTitle": "自动准备", + "installAutomaticDescription": "发送任务时,BitFun 会自动准备目标,无需手动安装。", + "installDetails": "安装详情", "version": "版本", - "downloadUrl": "下载地址", - "installing": "正在安装…", - "installFailed": "CLI 安装失败。请检查输出后重试。", - "installOutput": "CLI 安装输出", - "installWaiting": "正在等待安装输出…", - "approvalTitle": "无人值守权限策略", - "approvalHint": "必须明确选择,仅对这个派发任务生效。", - "approvalReject": "拒绝并报告", - "approvalRejectDescription": "拒绝需要确认的操作,并在会话记录中报告。", - "approvalRemote": "在本设备询问", - "approvalRemoteDescription": "暂停目标任务,并由当前观察端回答权限请求。", + "downloadUrl": "下载来源", + "integrity": "完整性校验", + "installing": "正在编译…", + "installFailed": "无法在目标上准备 BitFun。请检查编译输出后重试。", + "installOutput": "编译输出", + "installWaiting": "正在等待编译输出…", + "approvalTitle": "权限请求", + "approvalHint": "选择任务遇到需要确认的操作时如何处理。", + "approvalReject": "自动拒绝", + "approvalRejectDescription": "拒绝该操作,并在会话中说明。", + "approvalRemote": "在本机询问", + "approvalRemoteDescription": "暂停任务,等待你在本机处理。", "approvalAuto": "自动批准", - "approvalAutoDescription": "自动批准此派发任务在目标端产生的权限请求;发送任务即表示采用此策略。", + "approvalAutoDescription": "无需询问即可允许请求的操作。请仅对可信目标使用。", "useTarget": "使用此目标", "cancel": "取消", - "eventHistoryIncomplete": "部分派发任务事件已省略或过期,当前会话记录可能不完整。", - "completionTitle": "派发任务已完成", - "completionFailedTitle": "派发任务失败", - "completionBody": "{{task}} · {{target}}", - "permissionTitle": "派发任务需要批准", + "eventHistoryIncomplete": "部分任务记录已无法加载,当前内容可能不完整。", + "completionTitle": "远程任务已完成", + "completionFailedTitle": "远程任务失败", + "completionBody": "{{task}}({{target}})", + "permissionTitle": "远程任务需要确认", "permissionBody": "{{task}} 有 {{count}} 个权限请求等待处理。", "localTarget": "本机", - "syncTitle": "同步派发分支", - "syncSubtitle": "提交目标 worktree,并将其分支提取到受管基线 worktree。", - "syncSubtitleWithTarget": "提交 {{target}} 上的 worktree,并将其分支提取到受管基线 worktree。", - "syncBranch": "派发分支", - "syncBaselineWorktree": "基线 worktree", - "syncBaselineMissing": "受管基线 worktree 已不存在,无法再自动同步此派发任务。", - "syncingResult": "正在提交并传输派发分支…", - "syncSucceeded": "已将 {{count}} 个 commit 同步到基线 worktree。", - "syncHeadCommit": "已同步的最新 commit", - "syncChangedFiles": "改动文件", - "syncNoFileList": "commit 已同步,但目标端没有返回文件列表。", - "syncChangesTruncated": "这里只展示部分改动文件;完整 Git 历史已同步。", - "syncNoChanges": "目标 worktree 与基线一致,无需同步。", - "syncAction": "同步到基线", + "syncTitle": "获取远程改动", + "syncSubtitle": "把远程任务的最新改动取回本机,当前工作区不会被直接修改。", + "syncSubtitleWithTarget": "把 {{target}} 上的最新改动取回本机,当前工作区不会被直接修改。", + "syncDetails": "保存位置与分支", + "syncBranch": "结果分支", + "syncBaselineWorktree": "保存位置", + "syncBaselineMissing": "用于接收改动的本地副本已被删除,无法自动获取。请直接在目标设备上处理结果。", + "syncingResult": "正在获取远程改动…", + "syncSucceeded": "已获取 {{count}} 个提交,当前工作区未修改。", + "syncHeadCommit": "最新提交", + "syncChangedFiles": "本次改动", + "syncNoFileList": "已获取提交,但无法显示改动文件列表。", + "syncChangesTruncated": "仅显示部分文件,所有提交均已获取。", + "syncNoChanges": "没有新的远程改动。", + "syncFailed": "无法获取远程改动。请确认目标在线且仓库可用,然后重试。", + "syncAction": "获取改动", "syncClose": "关闭" }, "collapse": "折叠", diff --git a/src/web-ui/src/locales/zh-CN/flow-chat.json b/src/web-ui/src/locales/zh-CN/flow-chat.json index 5acc22cc05..61e9246ede 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -642,28 +642,42 @@ "locked": "运行在 {{target}}(此任务不可更改)", "current": "将此任务运行在 {{target}}", "menuLabel": "这个任务在哪运行", - "sessionScope": "新任务", + "sessionScope": "仅当前任务", "localSection": "本机", "localDescription": "在当前 BitFun 应用中运行", "sshSection": "SSH", "loading": "正在加载目标…", "noSshTargets": "没有已保存的 SSH 目标", + "targetLoadFailed": "无法加载 SSH 目标,重试", "sshDescription": "通过已保存的 SSH 连接运行", "addSsh": "添加 SSH 连接…", "deviceSection": "账号设备", "signInDevices": "登录以使用其他设备…", "noDeviceTargets": "没有其他账号设备", - "deviceDescription": "通过端到端加密的账号 Relay RPC 运行", + "deviceDescription": "在已登录的另一台设备上运行", "deviceOffline": "离线", - "createFailed": "无法创建派发任务。", - "remoteTarget": "远程目标", - "transferInProgress": "正在创建基线 worktree、拉取代码仓库并传输缺失的 Git 对象…", - "cliInstallStarted": "正在 SSH 目标上安装适用于 {{target}} 的已验证 BitFun CLI {{version}}。", - "cliInstallSucceeded": "已验证的 BitFun CLI {{version}} 已在 SSH 目标上就绪。", - "cliInstallFailed": "SSH 目标上的 BitFun CLI 安装失败:{{details}}", - "cliInstallStage": "SSH 目标 CLI 设置:{{stage}}", - "cliInstallUnknownVersion": "发布版本", - "cliInstallUnknownStage": "状态更新" + "createFailed": "无法创建远程任务。请重新检查目标后再试。", + "remoteTarget": "远程设备", + "transferInProgress": "正在准备目标并传输项目…", + "cliInstallStarted": "正在远程设备上准备 BitFun {{version}}…", + "cliInstallSucceeded": "BitFun {{version}} 已在远程设备上就绪。", + "cliInstallFailed": "无法准备远程运行环境。请检查 SSH 连接后重试。", + "cliInstallInProgress": "正在准备远程运行环境…", + "cliInstallUnknownVersion": "所需版本", + "errors": { + "attachmentUnavailable": "这张图片无法发送到远程设备。请重新添加后再试。", + "deviceAttachmentTooLarge": "通过账号设备发送的图片总大小不能超过 192 KB。请压缩图片,或改用 SSH 目标。", + "appendRejected": "目标设备未接收这条消息,请稍后重试。", + "followUpRejected": "目标设备未开始下一轮任务,请稍后重试。", + "targetInvalid": "远程目标已失效,请重新选择。", + "approvalRequired": "请先选择权限请求的处理方式。", + "sessionUnavailable": "远程任务信息不完整。请重新打开任务,或新建一个任务。", + "compactWhileRunning": "请等待当前远程任务完成后再压缩上下文。", + "compactRejected": "目标设备未能开始压缩,请稍后重试。", + "imagesWhileRunning": "图片会随下一轮消息发送。请等待当前远程任务完成后再试。", + "sessionNotReady": "远程任务仍在准备中,请稍后再发送。", + "submissionUnconfirmed": "暂时无法确认目标是否已接收任务。请稍后检查任务状态。" + } }, "addBoostTooltip": "智能体模式、图片或 Skill", "permissionMode": { diff --git a/src/web-ui/src/locales/zh-CN/worktrees.json b/src/web-ui/src/locales/zh-CN/worktrees.json index 065e9fca65..5ce0edf0e6 100644 --- a/src/web-ui/src/locales/zh-CN/worktrees.json +++ b/src/web-ui/src/locales/zh-CN/worktrees.json @@ -9,7 +9,7 @@ "togglePendingOnDescription": "已开启 worktree 隔离;发送第一条消息后才会创建 worktree。", "togglePendingOffDescription": "已关闭 worktree 隔离;发送第一条消息后会回到项目目录。", "toggleLocked": "只能在会话发出第一条消息之前切换 worktree 隔离。", - "dispatchBaseline": "本次派发以该仓库的受管 worktree 作为基线执行。基线在选定目标时即已固定。", + "dispatchBaseline": "远程任务会在隔离副本中运行,选定目标后不能更改。", "retained": "该 worktree 仍有本地工作,已保留在 {{path}}。" }, "settings": { diff --git a/src/web-ui/src/locales/zh-TW/common.json b/src/web-ui/src/locales/zh-TW/common.json index a2751ac40c..a2edc57a40 100644 --- a/src/web-ui/src/locales/zh-TW/common.json +++ b/src/web-ui/src/locales/zh-TW/common.json @@ -157,14 +157,13 @@ }, "sessions": { "newSession": "新增會話", - "dispatchRunningOn": "執行於 {{target}} · {{state}}", - "dispatchUnreachable": "目標無法連線", - "dispatchUnreachableDetails": "目標無法連線:{{target}} · {{error}}", - "dispatchTransportErrorFallback": "傳輸請求失敗", + "dispatchRunningOn": "在 {{target}} 上執行({{state}})", + "dispatchUnreachable": "連線中斷", + "dispatchUnreachableDetails": "暫時無法連線 {{target}}。任務可能仍在該裝置上執行。", "dispatchStates": { - "submitting": "待提交", - "submission_unknown": "正在確認提交狀態", - "queued": "排隊中", + "submitting": "正在傳送", + "submission_unknown": "正在確認", + "queued": "等待執行", "running": "$t(shared:statuses.running)", "succeeded": "$t(shared:statuses.done)", "failed": "$t(shared:statuses.failed)", @@ -1460,78 +1459,92 @@ } }, "dispatch": { - "configureTitle": "準備 {{target}}", - "configureSubtitle": "確認 Git 基線、目標就緒狀態與無人值守權限策略。", - "readinessTitle": "目標就緒狀態", - "deliveryTitle": "Git worktree 基線", - "baselineSource": "原始碼儲存庫", - "baselineDescription": "BitFun 會建立一個受管 worktree 作為隔離基線;目標端簽出同一個 commit,並在獨立的派發分支上工作。", - "baseRef": "基準版本", - "baseRefHint": "預設為 HEAD,也可填寫此儲存庫中存在的分支、標籤或 commit。", - "baseRefInvalid": "無法在原始碼儲存庫中解析「{{ref}}」。請檢查分支、標籤或 commit 後重試。", - "includeUncommitted": "包含 Git 可見的未提交變更", - "includeUncommittedHint": "可被 git add -A 納入的變更會提交到基線中;本機 .env、建置產物等被忽略檔案絕不會傳輸。", - "sourceBuildTitle": "從原始碼編譯", - "sourceBuildDescription": "在目標上編譯與目前控制端一致的原始碼({{ref}})。相容性按能力驗證,不再僅憑版本號猜測;耗時較長。", + "configureTitle": "在 {{target}} 上執行", + "configureSubtitle": "選擇要傳送的程式碼,以及任務遇到權限要求時的處理方式。", + "readinessTitle": "目標檢查", + "checkingTarget": "正在檢查目標…", + "probeFailed": "無法檢查此目標。請確認裝置在線且連線可用。", + "retryCheck": "重新檢查", + "deliveryTitle": "要傳送的程式碼", + "baselineSource": "專案", + "baselineDescription": "BitFun 會根據所選版本建立隔離副本,遠端變更不會直接影響目前工作區。", + "baseRef": "起始版本", + "baseRefHint": "預設使用 HEAD,也可填寫此儲存庫中的分支、標籤或 commit。", + "baseRefInvalid": "此儲存庫中找不到「{{ref}}」。請檢查分支、標籤或 commit 後重試。", + "includeUncommitted": "包含未提交的變更", + "includeUncommittedHint": "只會傳送 Git 可見的變更;.env、建置產物等忽略檔案會保留在本機。", + "sourceBuildTitle": "在目標上編譯 BitFun", + "sourceBuildDescription": "此目標沒有可用的安裝套件。你可以改為在目標上編譯 BitFun,通常需要數十分鐘。", + "sourceBuildUnavailable": "目標需要安裝 Rust、Git 和 C 編譯器,並預留約 6 GB 空間,才能開始編譯。", "sourceBuildConfirm": "從原始碼編譯", "sourceBuildConfirmTitle": "在此目標上從原始碼編譯 BitFun CLI?", - "sourceBuildConfirmMessage": "會把所選原始碼傳輸或複製到目標上,並執行 cargo build --release。需要約 6 GB 可用空間,可能耗時數十分鐘。", - "cliStatus": "BitFun CLI", + "sourceBuildConfirmMessage": "BitFun 原始碼將傳送到目標並在目標上編譯。需要約 6 GB 可用空間,可能耗時數十分鐘。", + "sourceBuildFailed": "無法開始編譯。請檢查目標連線和編譯環境後重試。", + "stopSourceBuild": "停止編譯", + "cliStatus": "$t(shared:product.name)", "cliReady": "就緒({{version}})", - "cliMissing": "未安裝或無法連線", - "cliIncompatible": "需要更新:{{details}}", - "protocolVersionMismatch": "協定版本 {{actual}},需要 {{expected}}", - "modelStatus": "目標模型", - "modelMatchesLocal": "就緒(與本機一致 · {{model}})", - "modelDiffersFromLocal": "就緒(與本機不同 · 目標有 {{count}} 個模型)", - "modelReadyCount": "就緒(目標有 {{count}} 個模型)", + "cliWillInstall": "傳送任務時自動準備", + "cliUpdateRequired": "需要更新後才能執行任務", + "cliUnavailable": "此目標尚未安裝 BitFun", + "deviceUpdateRequired": "請在此裝置上更新 BitFun,然後重新檢查。", + "modelStatus": "模型", + "modelMatchesLocal": "可用,預設使用 {{model}}", + "modelDiffersFromLocal": "可用,目標有 {{count}} 個模型,與本機設定不同", + "modelReadyCount": "可用,目標有 {{count}} 個模型", "modelAutomatic": "目標預設模型", - "modelMissing": "目標上沒有可用的模型設定", - "syncModelRequired": "同步模型設定", - "syncModelDescription": "使用本機模型設定(含 API 金鑰)取代目標端的模型清單與預設選擇。", + "modelCheckPending": "BitFun 就緒後再檢查", + "modelMissing": "目標上沒有可用模型", + "modelMissingOnBoth": "本機和目標均沒有可用模型,請先在設定中新增模型。", + "syncModelTitle": "使用本機模型", + "syncModelDescription": "將本機模型設定和 API 金鑰複製到目標,並取代目標現有的模型設定。", "syncModelConfirmTitle": "同步模型設定到此目標?", - "syncModelConfirmMessage": "本機的模型清單與預設模型選擇(包含 API 金鑰)將寫入目標使用者的 BitFun 設定檔,且僅目標使用者可讀。", + "syncModelConfirmMessage": "本機模型設定和 API 金鑰將寫入目標使用者的 BitFun 設定,並取代目標現有的模型設定。", "syncModelConfirm": "同步", "syncingModel": "正在同步…", - "installAutomaticTitle": "自動安裝 CLI", - "installAutomaticDescription": "傳送任務時,BitFun 會自動安裝此簽署發行版、驗證 SHA256 摘要,並把操作寫入派發稽核記錄。", + "syncModelFailed": "無法同步模型設定。請檢查目標連線後重試。", + "installAutomaticTitle": "自動準備", + "installAutomaticDescription": "傳送任務時,BitFun 會自動準備目標,無需手動安裝。", + "installDetails": "安裝詳情", "version": "版本", - "downloadUrl": "下載位址", - "installing": "正在安裝…", - "installFailed": "CLI 安裝失敗。請檢查輸出後重試。", - "installOutput": "CLI 安裝輸出", - "installWaiting": "正在等待安裝輸出…", - "approvalTitle": "無人值守權限策略", - "approvalHint": "必須明確選擇,只對這個派發任務生效。", - "approvalReject": "拒絕並回報", - "approvalRejectDescription": "拒絕需要確認的操作,並在工作階段記錄中回報。", - "approvalRemote": "在此裝置詢問", - "approvalRemoteDescription": "暫停目標任務,並由目前觀察端回答權限要求。", + "downloadUrl": "下載來源", + "integrity": "完整性驗證", + "installing": "正在編譯…", + "installFailed": "無法在目標上準備 BitFun。請檢查編譯輸出後重試。", + "installOutput": "編譯輸出", + "installWaiting": "正在等待編譯輸出…", + "approvalTitle": "權限要求", + "approvalHint": "選擇任務遇到需要確認的操作時如何處理。", + "approvalReject": "自動拒絕", + "approvalRejectDescription": "拒絕該操作,並在工作階段中說明。", + "approvalRemote": "在本機詢問", + "approvalRemoteDescription": "暫停任務,等待你在本機處理。", "approvalAuto": "自動核准", - "approvalAutoDescription": "自動核准此派發任務在目標端產生的權限要求;傳送任務即表示採用此策略。", + "approvalAutoDescription": "無需詢問即可允許要求的操作。請僅對可信目標使用。", "useTarget": "使用此目標", "cancel": "取消", - "eventHistoryIncomplete": "部分派發任務事件已省略或過期,目前工作階段記錄可能不完整。", - "completionTitle": "派發任務已完成", - "completionFailedTitle": "派發任務失敗", - "completionBody": "{{task}} · {{target}}", - "permissionTitle": "派發任務需要核准", + "eventHistoryIncomplete": "部分任務記錄已無法載入,目前內容可能不完整。", + "completionTitle": "遠端任務已完成", + "completionFailedTitle": "遠端任務失敗", + "completionBody": "{{task}}({{target}})", + "permissionTitle": "遠端任務需要確認", "permissionBody": "{{task}} 有 {{count}} 個權限要求等待處理。", "localTarget": "本機", - "syncTitle": "同步派發分支", - "syncSubtitle": "提交目標 worktree,並將其分支擷取到受管基線 worktree。", - "syncSubtitleWithTarget": "提交 {{target}} 上的 worktree,並將其分支擷取到受管基線 worktree。", - "syncBranch": "派發分支", - "syncBaselineWorktree": "基線 worktree", - "syncBaselineMissing": "受管基線 worktree 已不存在,無法再自動同步此派發任務。", - "syncingResult": "正在提交並傳輸派發分支…", - "syncSucceeded": "已將 {{count}} 個 commit 同步到基線 worktree。", - "syncHeadCommit": "已同步的最新 commit", - "syncChangedFiles": "變更檔案", - "syncNoFileList": "commit 已同步,但目標端沒有回傳檔案清單。", - "syncChangesTruncated": "這裡只顯示部分變更檔案;完整 Git 歷史已同步。", - "syncNoChanges": "目標 worktree 與基線一致,無需同步。", - "syncAction": "同步到基線", + "syncTitle": "取得遠端變更", + "syncSubtitle": "把遠端任務的最新變更取回本機,目前工作區不會被直接修改。", + "syncSubtitleWithTarget": "把 {{target}} 上的最新變更取回本機,目前工作區不會被直接修改。", + "syncDetails": "儲存位置與分支", + "syncBranch": "結果分支", + "syncBaselineWorktree": "儲存位置", + "syncBaselineMissing": "用於接收變更的本機副本已被刪除,無法自動取得。請直接在目標裝置上處理結果。", + "syncingResult": "正在取得遠端變更…", + "syncSucceeded": "已取得 {{count}} 個提交,目前工作區未修改。", + "syncHeadCommit": "最新提交", + "syncChangedFiles": "本次變更", + "syncNoFileList": "已取得提交,但無法顯示變更檔案清單。", + "syncChangesTruncated": "僅顯示部分檔案,所有提交均已取得。", + "syncNoChanges": "沒有新的遠端變更。", + "syncFailed": "無法取得遠端變更。請確認目標在線且儲存庫可用,然後重試。", + "syncAction": "取得變更", "syncClose": "關閉" }, "collapse": "收合", diff --git a/src/web-ui/src/locales/zh-TW/flow-chat.json b/src/web-ui/src/locales/zh-TW/flow-chat.json index 2d3c299f2c..54c29061a6 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -642,28 +642,42 @@ "locked": "執行於 {{target}}(此任務不可變更)", "current": "將此任務執行於 {{target}}", "menuLabel": "這個任務在哪裡執行", - "sessionScope": "新任務", + "sessionScope": "僅目前任務", "localSection": "本機", "localDescription": "在目前 BitFun 應用程式中執行", "sshSection": "SSH", "loading": "正在載入目標…", "noSshTargets": "沒有已儲存的 SSH 目標", + "targetLoadFailed": "無法載入 SSH 目標,重試", "sshDescription": "透過已儲存的 SSH 連線執行", "addSsh": "新增 SSH 連線…", "deviceSection": "帳號裝置", "signInDevices": "登入以使用其他裝置…", "noDeviceTargets": "沒有其他帳號裝置", - "deviceDescription": "透過端對端加密的帳號 Relay RPC 執行", + "deviceDescription": "在已登入的另一台裝置上執行", "deviceOffline": "離線", - "createFailed": "無法建立派發任務。", - "remoteTarget": "遠端目標", - "transferInProgress": "正在建立基線 worktree、擷取程式碼儲存庫並傳輸缺少的 Git 物件…", - "cliInstallStarted": "正在 SSH 目標上安裝適用於 {{target}} 的已驗證 BitFun CLI {{version}}。", - "cliInstallSucceeded": "已驗證的 BitFun CLI {{version}} 已在 SSH 目標上就緒。", - "cliInstallFailed": "SSH 目標上的 BitFun CLI 安裝失敗:{{details}}", - "cliInstallStage": "SSH 目標 CLI 設定:{{stage}}", - "cliInstallUnknownVersion": "發佈版本", - "cliInstallUnknownStage": "狀態更新" + "createFailed": "無法建立遠端任務。請重新檢查目標後再試。", + "remoteTarget": "遠端裝置", + "transferInProgress": "正在準備目標並傳送專案…", + "cliInstallStarted": "正在遠端裝置上準備 BitFun {{version}}…", + "cliInstallSucceeded": "BitFun {{version}} 已在遠端裝置上就緒。", + "cliInstallFailed": "無法準備遠端執行環境。請檢查 SSH 連線後重試。", + "cliInstallInProgress": "正在準備遠端執行環境…", + "cliInstallUnknownVersion": "所需版本", + "errors": { + "attachmentUnavailable": "這張圖片無法傳送到遠端裝置。請重新加入後再試。", + "deviceAttachmentTooLarge": "透過帳號裝置傳送的圖片總大小不能超過 192 KB。請壓縮圖片,或改用 SSH 目標。", + "appendRejected": "目標裝置未收到這則訊息,請稍後重試。", + "followUpRejected": "目標裝置未開始下一輪任務,請稍後重試。", + "targetInvalid": "遠端目標已失效,請重新選擇。", + "approvalRequired": "請先選擇權限要求的處理方式。", + "sessionUnavailable": "遠端任務資訊不完整。請重新開啟任務,或建立新任務。", + "compactWhileRunning": "請等待目前遠端任務完成後再壓縮上下文。", + "compactRejected": "目標裝置未能開始壓縮,請稍後重試。", + "imagesWhileRunning": "圖片會隨下一輪訊息傳送。請等待目前遠端任務完成後再試。", + "sessionNotReady": "遠端任務仍在準備中,請稍後再傳送。", + "submissionUnconfirmed": "暫時無法確認目標是否已收到任務。請稍後檢查任務狀態。" + } }, "addBoostTooltip": "智能體模式、圖片或 Skill", "permissionMode": { diff --git a/src/web-ui/src/locales/zh-TW/worktrees.json b/src/web-ui/src/locales/zh-TW/worktrees.json index 61267fdc06..8c26ab04e3 100644 --- a/src/web-ui/src/locales/zh-TW/worktrees.json +++ b/src/web-ui/src/locales/zh-TW/worktrees.json @@ -9,7 +9,7 @@ "togglePendingOnDescription": "已開啟 worktree 隔離;傳送第一則訊息後才會建立 worktree。", "togglePendingOffDescription": "已關閉 worktree 隔離;傳送第一則訊息後會回到專案目錄。", "toggleLocked": "只能在工作階段送出第一則訊息之前切換 worktree 隔離。", - "dispatchBaseline": "本次派發以該儲存庫的受管 worktree 作為基線執行。基線在選定目標時即已固定。", + "dispatchBaseline": "遠端任務會在隔離副本中執行,選定目標後無法變更。", "retained": "該 worktree 仍有本機工作,已保留在 {{path}}。" }, "settings": {