/** * The desktop-launcher settings card: launcher behavior fields plus the * "create desktop icon" action and the shutdown confirmation toggle. * Registers into the `web-ui.plugin.item` slot the Web UI plugin group * renders, bound to the `desktop-launcher` settings namespace. */ import { useState } from 'react' import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client' import type { SnapshotStore } from '@deepseek-ai/dsh-client-store' import type { CreateResult } from '../protocol.ts' import { createDesktopShortcut } from './api.ts' import { BooleanField, PluginSettingsCard, ValueField } from './PluginSettingsCard.tsx' import { booleanField, CardForm, textField, type CardActions, type CardShell, type FieldState } from './settings-form.ts' import css from './launcher-card.module.css' import shutdownCss from './shutdown.module.css' /** The desktop-launcher fields this card edits (the namespace full schema). */ export interface DesktopLauncherSettings { /** Master switch for the plugin. */ enabled?: boolean /** Whether the host announces the plugin to every agent. */ announceToAgent?: boolean /** Command that starts dsh. */ dshCommand?: string /** Base URL of the dsh web GUI. */ url?: string /** Optional profile passed as `dsh web --profile `. */ profile?: string /** Optional icon file (.ico/.png) for the desktop icon. */ iconPath?: string /** Whether the floating shutdown button asks for confirmation before exiting. */ confirmShutdown?: boolean } /** What the desktop-launcher card renders. */ export interface DesktopLauncherSettingsCardState extends CardShell { /** Master switch. */ enabled: FieldState /** Agent announcement switch. */ announceToAgent: FieldState /** dsh command. */ dshCommand: FieldState /** GUI URL. */ url: FieldState /** Startup profile. */ profile: FieldState /** Desktop icon file. */ iconPath: FieldState /** Confirm gate for shutdown. */ confirmShutdown: FieldState } /** The registration-side face the card slot entry injects. */ export interface DesktopLauncherSettingsCardFace extends CardActions { hooks: { /** Card snapshot bound by the renderer as useDesktopLauncherSettingsCard. */ desktopLauncherSettingsCard: SnapshotStore } } /** Bridges the `desktop-launcher` scope onto the card staged form. */ export class DesktopLauncherSettingsCardController { private readonly form: CardForm private readonly store: SnapshotStore /** @param scope - the bound settings scope for the `desktop-launcher` namespace. */ constructor(scope: SettingsScope) { this.form = new CardForm(scope, [ booleanField('enabled'), booleanField('announceToAgent'), textField('dshCommand'), textField('url'), textField('profile'), textField('iconPath'), booleanField('confirmShutdown'), ]) this.store = this.form.bind(() => this.projection()) } private projection(): DesktopLauncherSettingsCardState { return { ...this.form.shell(), enabled: this.form.field('enabled'), announceToAgent: this.form.field('announceToAgent'), dshCommand: this.form.field('dshCommand'), url: this.form.field('url'), profile: this.form.field('profile'), iconPath: this.form.field('iconPath'), confirmShutdown: this.form.field('confirmShutdown'), } } /** * Build the face the card slot registration injects. * @returns the card snapshot and its form actions. */ inject(): DesktopLauncherSettingsCardFace { return { hooks: { desktopLauncherSettingsCard: this.store }, ...this.form.actions() } } } /** Props the renderer binds for the desktop-launcher card. */ export type DesktopLauncherSettingsCardProps = PropsRuntime<'web-ui.plugin.item'> & PropsLocale<'desktop-launcher'> & InjectFace /** * Render the desktop-launcher card. * @param props - locale copy, the card snapshot, and its form actions. * @returns the card. */ export function DesktopLauncherSettingsCard(props: DesktopLauncherSettingsCardProps) { const { t } = props const state = props.useDesktopLauncherSettingsCard(snapshot => snapshot) const [creating, setCreating] = useState(false) const [created, setCreated] = useState() const [error, setError] = useState() const disabled = !state.writable // The create action calls the host route, which mounts only while the // saved `enabled` is on. Keep it inert for every staged draft so the route // and all launcher fields match what the card currently shows. const createReady = state.enabled.text === 'true' && !state.dirty && !state.saving const fieldProps = { overriddenLabel: t('settings.overridden'), resetLabel: t('settings.reset'), invalidLabel: t('settings.invalidNumber'), disabled, } const create = async (): Promise => { setCreating(true) setError(undefined) setCreated(undefined) try { setCreated(await createDesktopShortcut()) } catch (createError) { setError(createError instanceof Error ? createError.message : String(createError)) } finally { setCreating(false) } } return ( { props.edit('enabled', text) }} onReset={() => { props.resetField('enabled') }} /> { props.edit('announceToAgent', text) }} onReset={() => { props.resetField('announceToAgent') }} /> { props.edit('dshCommand', text) }} onReset={() => { props.resetField('dshCommand') }} /> { props.edit('url', text) }} onReset={() => { props.resetField('url') }} /> { props.edit('profile', text) }} onReset={() => { props.resetField('profile') }} /> { props.edit('iconPath', text) }} onReset={() => { props.resetField('iconPath') }} />
{!createReady && !disabled ?

{t('settings.requireEnabled')}

: null} {created ? (

{t('settings.created')}: {created.path} {created.warning ? ` (${t('settings.warning')}: ${created.warning})` : ''}

) : null} {error ? (

{t('settings.createFailed')}: {error}

) : null}

{t('settings.shutdownSection')}

{ props.edit('confirmShutdown', text) }} onReset={() => { props.resetField('confirmShutdown') }} /> ) }