Files
goodbuddy/src/renderer/src/RuntimeNativeClientActions.tsx
T
mesalogo 0f8bf405db
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
fix: remove persistent DS service hint from composer
Remove the persistent independent-session and service-lifecycle description beneath the native client action. Keep the stop action and existing service lifecycle unchanged, and align the proposal documentation.

Release note: 移除 DS 浏览器入口旁常驻的会话和服务说明,简化输入区;停止服务操作保持可用。
2026-09-27 22:16:47 +08:00

100 lines
5.4 KiB
TypeScript

import { useEffect, useRef, useState } from 'react'
import { Globe, LoaderCircle, TerminalSquare } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import type { TerminalSnapshot } from '../../shared/terminal-contracts'
export type RuntimeNativeClientApi = {
openRuntimeNativeClient: (input: { conversationId: string }) => Promise<
{ kind: 'terminal'; terminal: TerminalSnapshot } | { kind: 'browser'; serviceId: string }
>
getRuntimeNativeClient: (input: { conversationId: string }) => Promise<{ serviceId: string } | null>
stopRuntimeNativeClient: (input: { serviceId: string }) => Promise<void>
}
type Props = {
browser: boolean
contextKey: string
conversationId?: string
prepareConversation: () => Promise<string>
onTerminal: (terminal: TerminalSnapshot) => void
notify: (notification: { tone: 'error' | 'success'; message: string }) => void
}
export function RuntimeNativeClientActions({ browser, contextKey, conversationId, prepareConversation, onTerminal, notify }: Props): React.JSX.Element {
const { t } = useTranslation('app')
const [pending, setPending] = useState<'open' | 'stop'>()
const busy = useRef(false)
const lookupGeneration = useRef(0)
const [service, setService] = useState<{ key: string; serviceId: string }>()
const [confirmStop, setConfirmStop] = useState<string>()
const current = useRef({ contextKey, prepareConversation, notify })
useEffect(() => { current.current = { contextKey, prepareConversation, notify } })
const api = window.goodbuddy as typeof window.goodbuddy & RuntimeNativeClientApi
const serviceId = service?.key === contextKey ? service.serviceId : undefined
useEffect(() => {
if (!browser || !conversationId) return
let cancelled = false
const refresh = async (): Promise<void> => {
const generation = ++lookupGeneration.current
try {
const id = await current.current.prepareConversation()
if (cancelled) return
const result = await api.getRuntimeNativeClient({ conversationId: id })
if (!cancelled && !busy.current && lookupGeneration.current === generation) setService(result ? { key: contextKey, ...result } : undefined)
} catch (error) {
if (!cancelled) current.current.notify({ tone: 'error', message: error instanceof Error ? error.message : t('composer.nativeClient.failed') })
}
}
void refresh()
window.addEventListener('focus', refresh)
return () => { cancelled = true; window.removeEventListener('focus', refresh) }
}, [api, browser, contextKey, conversationId, t])
const run = async (stopServiceId?: string): Promise<void> => {
if (busy.current) return
busy.current = true
lookupGeneration.current += 1
setPending(stopServiceId ? 'stop' : 'open')
try {
if (stopServiceId) {
await api.stopRuntimeNativeClient({ serviceId: stopServiceId })
setService(previous => previous?.serviceId === stopServiceId ? undefined : previous)
setConfirmStop(undefined)
notify({ tone: 'success', message: t('composer.nativeClient.stopped') })
} else {
const id = await prepareConversation()
const result = await api.openRuntimeNativeClient({ conversationId: id })
if (result.kind === 'terminal') onTerminal(result.terminal)
else if (current.current.contextKey === contextKey) setService({ key: contextKey, serviceId: result.serviceId })
}
} catch (error) {
notify({ tone: 'error', message: error instanceof Error ? error.message : t('composer.nativeClient.failed') })
// A browser-open failure can leave a ready service that the user can stop or reopen.
if (browser && !stopServiceId && current.current.contextKey === contextKey) {
try {
const id = await prepareConversation()
const result = await api.getRuntimeNativeClient({ conversationId: id })
if (current.current.contextKey === contextKey) setService(result ? { key: contextKey, ...result } : undefined)
} catch { /* Keep the original operation error. */ }
}
} finally {
busy.current = false
setPending(undefined)
}
}
return <>
<button className="composer-context-compact" type="button" disabled={!!pending} aria-busy={!!pending} onClick={() => void run()}>
{pending ? <LoaderCircle aria-hidden="true" className="context-chip__spinner" size={13} /> : browser ? <Globe aria-hidden="true" size={13} /> : <TerminalSquare aria-hidden="true" size={13} />}
{t(pending === 'stop' ? 'composer.nativeClient.stopping' : pending ? 'composer.nativeClient.opening' : serviceId ? 'composer.nativeClient.reopen' : browser ? 'composer.nativeClient.browser' : 'composer.nativeClient.terminal')}
</button>
{browser && serviceId && <button className="composer-context-compact danger-ghost" type="button" disabled={!!pending} onClick={() => setConfirmStop(serviceId)}>{t('composer.nativeClient.stop')}</button>}
{serviceId && confirmStop === serviceId && <span className="composer-native-client-confirm" role="group" aria-label={t('composer.nativeClient.stop')}>
<span>{t('composer.nativeClient.stopWarning')}</span>
<button type="button" className="secondary-button" disabled={!!pending} onClick={() => setConfirmStop(undefined)}>{t('composer.nativeClient.cancel')}</button>
<button type="button" className="danger-ghost" disabled={!!pending} onClick={() => void run(serviceId)}>{t('composer.nativeClient.confirmStop')}</button>
</span>}
</>
}