@dxos/shell 0.6.1-main.e05b43b → 0.6.1-main.ff751ec

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/bundle/shell.mjs +188 -111
  2. package/dist/bundle/shell.mjs.map +1 -1
  3. package/dist/bundle/shell.umd.js +5 -5
  4. package/dist/bundle/shell.umd.js.map +1 -1
  5. package/dist/bundle/style.css +1 -1
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/react.mjs +171 -115
  8. package/dist/lib/browser/react.mjs.map +3 -3
  9. package/dist/types/src/components/AgentConfig.d.ts +1 -1
  10. package/dist/types/src/components/AgentConfig.d.ts.map +1 -1
  11. package/dist/types/src/components/AgentConfig.js +36 -24
  12. package/dist/types/src/components/AgentConfig.js.map +1 -1
  13. package/dist/types/src/components/DeviceList/DeviceList.d.ts +2 -2
  14. package/dist/types/src/components/DeviceList/DeviceList.d.ts.map +1 -1
  15. package/dist/types/src/components/DeviceList/DeviceList.js +2 -2
  16. package/dist/types/src/components/DeviceList/DeviceList.js.map +1 -1
  17. package/dist/types/src/components/DeviceList/DeviceListItem.d.ts +2 -2
  18. package/dist/types/src/components/DeviceList/DeviceListItem.d.ts.map +1 -1
  19. package/dist/types/src/components/DeviceList/DeviceListItem.js +11 -5
  20. package/dist/types/src/components/DeviceList/DeviceListItem.js.map +1 -1
  21. package/dist/types/src/components/DeviceList/DeviceListProps.d.ts +6 -7
  22. package/dist/types/src/components/DeviceList/DeviceListProps.d.ts.map +1 -1
  23. package/dist/types/src/panels/IdentityPanel/IdentityPanel.d.ts.map +1 -1
  24. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js +2 -5
  25. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js.map +1 -1
  26. package/dist/types/src/panels/IdentityPanel/IdentityPanelProps.d.ts +0 -1
  27. package/dist/types/src/panels/IdentityPanel/IdentityPanelProps.d.ts.map +1 -1
  28. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.d.ts +1 -1
  29. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.d.ts.map +1 -1
  30. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js +5 -5
  31. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js.map +1 -1
  32. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.d.ts +2 -7
  33. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.d.ts.map +1 -1
  34. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.js +78 -43
  35. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.js.map +1 -1
  36. package/dist/types/src/translations/locales/en-US.d.ts +5 -0
  37. package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
  38. package/dist/types/src/translations/locales/en-US.js +5 -0
  39. package/dist/types/src/translations/locales/en-US.js.map +1 -1
  40. package/package.json +23 -23
  41. package/src/components/AgentConfig.tsx +100 -48
  42. package/src/components/DeviceList/DeviceList.tsx +4 -3
  43. package/src/components/DeviceList/DeviceListItem.tsx +66 -28
  44. package/src/components/DeviceList/DeviceListProps.ts +6 -7
  45. package/src/panels/IdentityPanel/IdentityPanel.tsx +2 -5
  46. package/src/panels/IdentityPanel/IdentityPanelProps.ts +0 -1
  47. package/src/panels/IdentityPanel/steps/IdentityActionChooser.tsx +7 -15
  48. package/src/panels/IdentityPanel/useAgentHandlers.ts +77 -41
  49. package/src/translations/locales/en-US.ts +6 -0
@@ -12,6 +12,7 @@ import {
12
12
  ShareFat,
13
13
  Power,
14
14
  Robot,
15
+ Database,
15
16
  } from '@phosphor-icons/react';
16
17
  import React, { type ComponentPropsWithoutRef, forwardRef } from 'react';
17
18
 
@@ -27,11 +28,12 @@ import {
27
28
  useTranslation,
28
29
  DropdownMenu,
29
30
  Button,
31
+ Tooltip,
30
32
  } from '@dxos/react-ui';
31
33
  import { getSize } from '@dxos/react-ui-theme';
32
34
  import { keyToFallback } from '@dxos/util';
33
35
 
34
- import { type DeviceListItemProps } from './DeviceListProps';
36
+ import { type AgentFormProps, type DeviceListItemProps } from './DeviceListProps';
35
37
 
36
38
  const iconProps: IconProps = {
37
39
  weight: 'duotone',
@@ -43,10 +45,20 @@ const iconProps: IconProps = {
43
45
 
44
46
  export const DeviceListItem = forwardRef<
45
47
  HTMLLIElement,
46
- ThemedClassName<ComponentPropsWithoutRef<'li'>> & DeviceListItemProps
48
+ ThemedClassName<ComponentPropsWithoutRef<'li'>> & DeviceListItemProps & Pick<AgentFormProps, 'onAgentDestroy'>
47
49
  >(
48
50
  (
49
- { device, onClickAdd, onClickEdit, onClickReset, onClickJoinExisting, classNames, connectionState, ...props },
51
+ {
52
+ device,
53
+ onClickAdd,
54
+ onClickEdit,
55
+ onClickReset,
56
+ onClickJoinExisting,
57
+ classNames,
58
+ connectionState,
59
+ onAgentDestroy,
60
+ ...props
61
+ },
50
62
  forwardedRef,
51
63
  ) => {
52
64
  const { t } = useTranslation('os');
@@ -78,7 +90,9 @@ export const DeviceListItem = forwardRef<
78
90
  >
79
91
  <Avatar.Frame classNames='place-self-center'>
80
92
  {device.profile?.type ? (
81
- device.profile.type === DeviceType.BROWSER ? (
93
+ device.profile.type === DeviceType.AGENT_MANAGED ? (
94
+ <Database {...iconProps} />
95
+ ) : device.profile.type === DeviceType.BROWSER ? (
82
96
  <Compass {...iconProps} />
83
97
  ) : device.profile.type === DeviceType.NATIVE ? (
84
98
  <Desktop {...iconProps} />
@@ -95,35 +109,59 @@ export const DeviceListItem = forwardRef<
95
109
  </Avatar.Frame>
96
110
  <Avatar.Label classNames='flex-1 text-sm truncate'>{displayName}</Avatar.Label>
97
111
  {isCurrent && <Tag color='primary'>{t('current device tag label')}</Tag>}
98
- {device.kind === DeviceKind.CURRENT && (
99
- <DropdownMenu.Root>
100
- <DropdownMenu.Trigger asChild>
112
+ {device.profile?.type === DeviceType.AGENT_MANAGED ? (
113
+ <Tooltip.Root>
114
+ <Tooltip.Trigger asChild>
101
115
  <Button
102
116
  variant='ghost'
103
117
  classNames='pli-0 is-[--rail-action] bs-[--rail-action]'
104
- data-testid={`device-list-item${isCurrent ? '-current' : ''}.options`}
118
+ data-testid='agent.destroy'
119
+ onClick={onAgentDestroy}
105
120
  >
106
- <span className='sr-only'>{t('more options label')}</span>
107
- <DotsThree className={getSize(5)} />
121
+ <span className='sr-only'>{t('destroy agent label')}</span>
122
+ <Power className={getSize(5)} />
108
123
  </Button>
109
- </DropdownMenu.Trigger>
110
- <DropdownMenu.Content>
111
- <DropdownMenu.Viewport>
112
- {/* <DropdownMenu.Item disabled onClick={onClickEdit}> */}
113
- {/* <PencilSimpleLine className={getSize(5)} /> */}
114
- {/* {t('edit device label')} */}
115
- {/* </DropdownMenu.Item> */}
116
- <DropdownMenu.Item data-testid='device-list-item-current.join-existing' onClick={onClickJoinExisting}>
117
- <ShareFat className={getSize(5)} />
118
- {t('choose join new identity label')}
119
- </DropdownMenu.Item>
120
- <DropdownMenu.Item data-testid='device-list-item-current.reset' onClick={onClickReset}>
121
- <Power className={getSize(5)} />
122
- {t('reset device label')}
123
- </DropdownMenu.Item>
124
- </DropdownMenu.Viewport>
125
- </DropdownMenu.Content>
126
- </DropdownMenu.Root>
124
+ </Tooltip.Trigger>
125
+ <Tooltip.Portal>
126
+ <Tooltip.Content side='bottom' classNames='z-50'>
127
+ {t('destroy agent label')}
128
+ </Tooltip.Content>
129
+ </Tooltip.Portal>
130
+ </Tooltip.Root>
131
+ ) : (
132
+ device.kind === DeviceKind.CURRENT && (
133
+ <DropdownMenu.Root>
134
+ <DropdownMenu.Trigger asChild>
135
+ <Button
136
+ variant='ghost'
137
+ classNames='pli-0 is-[--rail-action] bs-[--rail-action]'
138
+ data-testid={`device-list-item${isCurrent ? '-current' : ''}.options`}
139
+ >
140
+ <span className='sr-only'>{t('more options label')}</span>
141
+ <DotsThree className={getSize(5)} />
142
+ </Button>
143
+ </DropdownMenu.Trigger>
144
+ <DropdownMenu.Content>
145
+ <DropdownMenu.Viewport>
146
+ {/* <DropdownMenu.Item disabled onClick={onClickEdit}> */}
147
+ {/* <PencilSimpleLine className={getSize(5)} /> */}
148
+ {/* {t('edit device label')} */}
149
+ {/* </DropdownMenu.Item> */}
150
+ <DropdownMenu.Item
151
+ data-testid='device-list-item-current.join-existing'
152
+ onClick={onClickJoinExisting}
153
+ >
154
+ <ShareFat className={getSize(5)} />
155
+ {t('choose join new identity label')}
156
+ </DropdownMenu.Item>
157
+ <DropdownMenu.Item data-testid='device-list-item-current.reset' onClick={onClickReset}>
158
+ <Power className={getSize(5)} />
159
+ {t('reset device label')}
160
+ </DropdownMenu.Item>
161
+ </DropdownMenu.Viewport>
162
+ </DropdownMenu.Content>
163
+ </DropdownMenu.Root>
164
+ )
127
165
  )}
128
166
  </Avatar.Root>
129
167
  </ListItem.Root>
@@ -16,13 +16,12 @@ export type DeviceListProps = {
16
16
  };
17
17
 
18
18
  export type AgentFormProps = {
19
- onAgentCreate?: () => Promise<void>;
20
- onAgentDestroy?: () => Promise<void>;
21
- agentStatus?: string;
22
- agentActive?: boolean;
23
- agentProviderDisabled?: boolean;
24
- validationMessage?: string;
25
- agentHostingEnabled?: boolean;
19
+ onAgentCreate: () => Promise<void>;
20
+ onAgentDestroy: () => Promise<void>;
21
+ onAgentRefresh: () => Promise<void>;
22
+ agentStatus: 'getting' | 'creating' | 'destroying' | 'created' | 'creatable' | 'error';
23
+ validationMessage: string;
24
+ agentHostingEnabled: boolean;
26
25
  };
27
26
 
28
27
  export type DeviceListItemProps = {
@@ -108,7 +108,7 @@ const IdentityHeading = ({
108
108
  <Tooltip.Root>
109
109
  <Tooltip.Trigger asChild>
110
110
  <Toolbar.Button
111
- classNames='bs-[--rail-action] justify-self-start'
111
+ classNames='bs-[--rail-action]'
112
112
  data-testid='update-profile-form-copy-key'
113
113
  onClick={() => setTextValue(identityHex)}
114
114
  >
@@ -125,10 +125,7 @@ const IdentityHeading = ({
125
125
  <Tooltip.Root>
126
126
  <Tooltip.Trigger asChild>
127
127
  <Toolbar.Button
128
- classNames={[
129
- 'bs-[--rail-action] is-[--rail-action] justify-self-end',
130
- !isConnected && 'text-error-500',
131
- ]}
128
+ classNames={['bs-[--rail-action]', !isConnected && 'text-error-500']}
132
129
  onClick={() =>
133
130
  onChangeConnectionState?.(isConnected ? ConnectionState.OFFLINE : ConnectionState.ONLINE)
134
131
  }
@@ -54,6 +54,5 @@ export type IdentityPanelStepProps = Pick<
54
54
  'send' | 'onDone' | 'doneActionParent' | 'connectionState'
55
55
  > & {
56
56
  active?: boolean;
57
- agentHostingEnabled?: boolean;
58
57
  devices: Device[];
59
58
  };
@@ -5,8 +5,8 @@
5
5
  import React, { useCallback } from 'react';
6
6
 
7
7
  import { log } from '@dxos/log';
8
- import { useAgentHostingClient, useClient } from '@dxos/react-client';
9
- import { useHaloInvitations } from '@dxos/react-client/halo';
8
+ import { useClient } from '@dxos/react-client';
9
+ import { useHaloInvitations, DeviceType } from '@dxos/react-client/halo';
10
10
  import { Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
11
11
 
12
12
  import { AgentConfig, type AgentFormProps, DeviceList } from '../../../components';
@@ -18,7 +18,6 @@ export const IdentityActionChooser = (props: IdentityPanelStepProps) => {
18
18
  const { send } = props;
19
19
  const client = useClient();
20
20
  const invitations = useHaloInvitations();
21
- const agentHostingProviderClient = useAgentHostingClient();
22
21
  const onInvitationEvent = useCallback((invitation: Invitation) => {
23
22
  const invitationCode = InvitationEncoder.encode(invitation);
24
23
  if (invitation.state === Invitation.State.CONNECTING) {
@@ -35,13 +34,7 @@ export const IdentityActionChooser = (props: IdentityPanelStepProps) => {
35
34
  }
36
35
  send?.({ type: 'selectInvitation', invitation });
37
36
  };
38
- return (
39
- <IdentityActionChooserImpl
40
- {...props}
41
- onCreateInvitationClick={createInvitation}
42
- agentHostingEnabled={!!agentHostingProviderClient}
43
- />
44
- );
37
+ return <IdentityActionChooserImpl {...props} onCreateInvitationClick={createInvitation} />;
45
38
  };
46
39
 
47
40
  export type IdentityActionChooserImplProps = IdentityActionChooserProps &
@@ -54,12 +47,10 @@ export const IdentityActionChooserImpl = ({
54
47
  onCreateInvitationClick,
55
48
  devices,
56
49
  connectionState,
57
- onAgentDestroy,
58
- onAgentCreate,
59
- agentStatus,
60
- agentActive,
61
50
  agentHostingEnabled,
51
+ ...agentProps
62
52
  }: IdentityActionChooserImplProps) => {
53
+ const managedDeviceAvailable = devices.find((device) => device.profile?.type === DeviceType.AGENT_MANAGED);
63
54
  return (
64
55
  <div role='none' className='bs-40 grow overflow-y-auto overflow-x-hidden'>
65
56
  <DeviceList
@@ -68,8 +59,9 @@ export const IdentityActionChooserImpl = ({
68
59
  onClickAdd={onCreateInvitationClick}
69
60
  onClickJoinExisting={() => send?.({ type: 'chooseJoinNewIdentity' })}
70
61
  onClickReset={() => send?.({ type: 'chooseResetStorage' })}
62
+ onAgentDestroy={agentProps.onAgentDestroy!}
71
63
  />
72
- {agentHostingEnabled && <AgentConfig {...{ agentActive, agentStatus, onAgentDestroy, onAgentCreate }} />}
64
+ {!managedDeviceAvailable && agentHostingEnabled && <AgentConfig {...(agentProps as AgentFormProps)} />}
73
65
  </div>
74
66
  );
75
67
  };
@@ -11,54 +11,91 @@ import { useAgentHostingClient, useClient } from '@dxos/react-client';
11
11
  import { type Identity } from '@dxos/react-client/halo';
12
12
  import { Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
13
13
 
14
+ import { type AgentFormProps } from '../../components';
15
+
14
16
  export const useAgentHandlers = ({
15
17
  invitations,
16
18
  identity,
17
19
  }: {
18
20
  invitations: CancellableInvitation[];
19
21
  identity: Identity | null;
20
- }) => {
22
+ }): AgentFormProps => {
21
23
  const [validationMessage, setValidationMessage] = useState('');
22
- const [agentStatus, setAgentStatus] = useState('');
23
- const [agentActive, setAgentActive] = useState(false);
24
+ const [agentStatus, setAgentStatus] = useState<AgentFormProps['agentStatus']>('getting');
24
25
  const agentHostingProviderClient = useAgentHostingClient();
25
26
  const client = useClient();
26
27
 
27
- useEffect(() => {
28
- const fetchAgentStatus = async () => {
29
- // TODO(nf): if agent exists, subscribe to updates instead of oneshot status
30
- if (agentHostingProviderClient) {
31
- const agentStatus = await agentHostingProviderClient?.getAgent(identity?.identityKey.truncate() ?? 'foo');
32
- if (agentStatus) {
33
- setAgentActive(true);
34
- setAgentStatus(agentStatus);
28
+ const updateAgentStatus = async () => {
29
+ // TODO(nf): if agent exists, subscribe to updates instead of oneshot status
30
+ if (agentHostingProviderClient) {
31
+ setAgentStatus('getting');
32
+ try {
33
+ const agentId = await agentHostingProviderClient?.getAgent(identity?.identityKey.truncate() ?? 'never');
34
+ if (agentId) {
35
+ setAgentStatus('created');
36
+ } else {
37
+ setAgentStatus('creatable');
38
+ }
39
+ } catch (err: any) {
40
+ setAgentStatus('error');
41
+ setValidationMessage(`Couldn’t check agent status: ${err.message}`);
42
+ }
43
+ } else {
44
+ setAgentStatus('error');
45
+ setValidationMessage('No agent provider configured.');
46
+ }
47
+ };
48
+
49
+ const createAgent = async (invitationCode: string) => {
50
+ invariant(identity, 'Identity not found');
51
+ if (agentHostingProviderClient) {
52
+ setAgentStatus('creating');
53
+ setValidationMessage('');
54
+ try {
55
+ const agentId = await agentHostingProviderClient?.createAgent(invitationCode, identity.identityKey.truncate());
56
+ if (agentId) {
57
+ setAgentStatus('created');
35
58
  } else {
36
- setAgentActive(false);
37
- setValidationMessage('No agent deployed via provider.');
59
+ setAgentStatus('creatable');
60
+ setValidationMessage('Error creating agent.');
38
61
  }
39
- } else {
40
- setValidationMessage('No agent provider configured.');
62
+ } catch (err: any) {
63
+ setValidationMessage(`Error creating agent: ${err.message}`);
64
+ return updateAgentStatus();
41
65
  }
42
- };
43
- void fetchAgentStatus();
66
+ } else {
67
+ setAgentStatus('error');
68
+ setValidationMessage('No agent provider configured.');
69
+ }
70
+ };
71
+
72
+ const destroyAgent = async () => {
73
+ invariant(identity, 'Identity not found');
74
+ if (agentHostingProviderClient) {
75
+ setAgentStatus('destroying');
76
+ setValidationMessage('');
77
+ try {
78
+ await agentHostingProviderClient?.destroyAgent(identity.identityKey.truncate());
79
+ setAgentStatus('creatable');
80
+ } catch (err: any) {
81
+ setValidationMessage(`Error destroying agent: ${err.message}`);
82
+ return updateAgentStatus();
83
+ }
84
+ } else {
85
+ setAgentStatus('error');
86
+ setValidationMessage('No agent provider configured.');
87
+ }
88
+ };
89
+
90
+ useEffect(() => {
91
+ void updateAgentStatus();
44
92
  }, []);
45
93
 
46
94
  const handleAgentCreate = useCallback(async (invitation: Invitation) => {
47
95
  const invitationCode = InvitationEncoder.encode(invitation);
48
96
  if (invitation.state === Invitation.State.CONNECTING) {
49
97
  log.info(JSON.stringify({ invitationCode, authCode: invitation.authCode }));
50
- invariant(identity, 'Identity not found');
51
- if (agentHostingProviderClient) {
52
- try {
53
- const res = await agentHostingProviderClient?.createAgent(invitationCode, identity.identityKey.truncate());
54
- // TODO(nf): human-consumable response from API
55
- setAgentStatus(res);
56
- setAgentActive(true);
57
- setValidationMessage('');
58
- } catch (err: any) {
59
- setValidationMessage(`error creating agent: ${err.message}`);
60
- }
61
- }
98
+ return createAgent(invitationCode);
62
99
  }
63
100
  }, []);
64
101
 
@@ -75,18 +112,17 @@ export const useAgentHandlers = ({
75
112
  invitation.subscribe(handleAgentCreate);
76
113
  };
77
114
 
78
- const onAgentDestroy = async () => {
79
- const identity = client.halo.identity.get();
80
- invariant(identity, 'Identity not found');
81
- try {
82
- await agentHostingProviderClient?.destroyAgent(identity.identityKey.truncate());
83
- setValidationMessage('No agent deployed via provider.');
84
- setAgentStatus('');
85
- setAgentActive(false);
86
- } catch (err: any) {
87
- setValidationMessage(`error destroying agent: ${err.message}`);
88
- }
115
+ const onAgentRefresh = async () => {
116
+ setValidationMessage('');
117
+ await updateAgentStatus();
89
118
  };
90
119
 
91
- return { onAgentCreate, onAgentDestroy, validationMessage, agentStatus, agentActive };
120
+ return {
121
+ onAgentCreate,
122
+ onAgentDestroy: destroyAgent,
123
+ onAgentRefresh,
124
+ validationMessage,
125
+ agentStatus,
126
+ agentHostingEnabled: !!agentHostingProviderClient,
127
+ };
92
128
  };
@@ -146,4 +146,10 @@ export const os = {
146
146
  'destroy agent label': 'Shut down agent',
147
147
  'agent device label': 'Your agent',
148
148
  'shell fallback title': 'Shell failed to connect',
149
+ 'creating agent label': 'Starting agent…',
150
+ 'destroying agent label': 'Stopping agent…',
151
+ 'getting agent label': 'Checking agent status…',
152
+ 'agent requested label': 'Agent requested',
153
+ 'agent requested description':
154
+ 'The agent service is creating an agent for you. Once the agent connects, it will display in the devices list.',
149
155
  };