@dxos/shell 0.3.11-main.4eab977 → 0.3.11-main.4faaf44

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 (110) hide show
  1. package/dist/bundle/shell.mjs +36766 -0
  2. package/dist/bundle/shell.mjs.map +1 -0
  3. package/dist/bundle/shell.umd.js +210 -0
  4. package/dist/bundle/shell.umd.js.map +1 -0
  5. package/dist/bundle/style.css +1 -0
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/react.mjs +712 -446
  8. package/dist/lib/browser/react.mjs.map +4 -4
  9. package/dist/lib/browser/testing/index.mjs +35 -14
  10. package/dist/lib/browser/testing/index.mjs.map +3 -3
  11. package/dist/types/src/components/IdentityList/SpaceMemberList.js +1 -1
  12. package/dist/types/src/components/IdentityList/SpaceMemberList.js.map +1 -1
  13. package/dist/types/src/components/Panel/Action.d.ts.map +1 -1
  14. package/dist/types/src/components/Panel/Action.js +3 -18
  15. package/dist/types/src/components/Panel/Action.js.map +1 -1
  16. package/dist/types/src/composites/Shell/Shell.d.ts.map +1 -1
  17. package/dist/types/src/composites/Shell/Shell.js +15 -5
  18. package/dist/types/src/composites/Shell/Shell.js.map +1 -1
  19. package/dist/types/src/panels/IdentityPanel/IdentityPanel.d.ts.map +1 -1
  20. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js +17 -9
  21. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js.map +1 -1
  22. package/dist/types/src/panels/IdentityPanel/IdentityPanelProps.d.ts +12 -2
  23. package/dist/types/src/panels/IdentityPanel/IdentityPanelProps.d.ts.map +1 -1
  24. package/dist/types/src/panels/IdentityPanel/identityMachine.d.ts +1 -1
  25. package/dist/types/src/panels/IdentityPanel/identityMachine.d.ts.map +1 -1
  26. package/dist/types/src/panels/IdentityPanel/identityMachine.js +10 -2
  27. package/dist/types/src/panels/IdentityPanel/identityMachine.js.map +1 -1
  28. package/dist/types/src/panels/IdentityPanel/steps/AgentForm.d.ts +3 -0
  29. package/dist/types/src/panels/IdentityPanel/steps/AgentForm.d.ts.map +1 -0
  30. package/dist/types/src/panels/IdentityPanel/steps/AgentForm.js +26 -0
  31. package/dist/types/src/panels/IdentityPanel/steps/AgentForm.js.map +1 -0
  32. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.d.ts.map +1 -1
  33. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js +5 -1
  34. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js.map +1 -1
  35. package/dist/types/src/panels/IdentityPanel/steps/SignOutChooser.d.ts +16 -0
  36. package/dist/types/src/panels/IdentityPanel/steps/SignOutChooser.d.ts.map +1 -0
  37. package/dist/types/src/panels/IdentityPanel/steps/SignOutChooser.js +40 -0
  38. package/dist/types/src/panels/IdentityPanel/steps/SignOutChooser.js.map +1 -0
  39. package/dist/types/src/panels/IdentityPanel/steps/index.d.ts +2 -0
  40. package/dist/types/src/panels/IdentityPanel/steps/index.d.ts.map +1 -1
  41. package/dist/types/src/panels/IdentityPanel/steps/index.js +2 -0
  42. package/dist/types/src/panels/IdentityPanel/steps/index.js.map +1 -1
  43. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.d.ts +15 -0
  44. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.d.ts.map +1 -0
  45. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.js +78 -0
  46. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.js.map +1 -0
  47. package/dist/types/src/panels/JoinPanel/JoinPanel.js +4 -3
  48. package/dist/types/src/panels/JoinPanel/JoinPanel.js.map +1 -1
  49. package/dist/types/src/panels/JoinPanel/joinMachine.d.ts +1 -1
  50. package/dist/types/src/panels/JoinPanel/joinMachine.d.ts.map +1 -1
  51. package/dist/types/src/panels/JoinPanel/joinMachine.js +15 -18
  52. package/dist/types/src/panels/JoinPanel/joinMachine.js.map +1 -1
  53. package/dist/types/src/panels/JoinPanel/steps/AdditionMethodChooser.js +3 -3
  54. package/dist/types/src/panels/JoinPanel/steps/AdditionMethodChooser.js.map +1 -1
  55. package/dist/types/src/panels/JoinPanel/steps/InvitationInput.d.ts.map +1 -1
  56. package/dist/types/src/panels/JoinPanel/steps/InvitationInput.js +13 -6
  57. package/dist/types/src/panels/JoinPanel/steps/InvitationInput.js.map +1 -1
  58. package/dist/types/src/panels/JoinPanel/steps/ResetIdentity.js +1 -1
  59. package/dist/types/src/panels/JoinPanel/steps/ResetIdentity.js.map +1 -1
  60. package/dist/types/src/panels/SpacePanel/SpacePanel.d.ts.map +1 -1
  61. package/dist/types/src/panels/SpacePanel/SpacePanel.js +5 -3
  62. package/dist/types/src/panels/SpacePanel/SpacePanel.js.map +1 -1
  63. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.d.ts.map +1 -1
  64. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.js +2 -2
  65. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.js.map +1 -1
  66. package/dist/types/src/panels/SpacePanel/SpacePanelProps.d.ts +1 -0
  67. package/dist/types/src/panels/SpacePanel/SpacePanelProps.d.ts.map +1 -1
  68. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.d.ts +1 -0
  69. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.d.ts.map +1 -1
  70. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.js +3 -1
  71. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.js.map +1 -1
  72. package/dist/types/src/playwright/invitations-manager.d.ts +1 -0
  73. package/dist/types/src/playwright/invitations-manager.d.ts.map +1 -1
  74. package/dist/types/src/playwright/invitations-manager.js +4 -1
  75. package/dist/types/src/playwright/invitations-manager.js.map +1 -1
  76. package/dist/types/src/playwright/invitations.spec.js +15 -0
  77. package/dist/types/src/playwright/invitations.spec.js.map +1 -1
  78. package/dist/types/src/testing/shell-manager.d.ts +5 -4
  79. package/dist/types/src/testing/shell-manager.d.ts.map +1 -1
  80. package/dist/types/src/testing/shell-manager.js +36 -12
  81. package/dist/types/src/testing/shell-manager.js.map +1 -1
  82. package/dist/types/src/translations/locales/en-US.d.ts +5 -0
  83. package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
  84. package/dist/types/src/translations/locales/en-US.js +5 -0
  85. package/dist/types/src/translations/locales/en-US.js.map +1 -1
  86. package/package.json +21 -20
  87. package/src/components/IdentityList/SpaceMemberList.tsx +1 -1
  88. package/src/components/Panel/Action.tsx +4 -19
  89. package/src/composites/Shell/Shell.tsx +16 -3
  90. package/src/panels/IdentityPanel/IdentityPanel.tsx +27 -11
  91. package/src/panels/IdentityPanel/IdentityPanelProps.ts +20 -2
  92. package/src/panels/IdentityPanel/identityMachine.ts +11 -3
  93. package/src/panels/IdentityPanel/steps/AgentForm.tsx +61 -0
  94. package/src/panels/IdentityPanel/steps/IdentityActionChooser.tsx +8 -4
  95. package/src/panels/IdentityPanel/steps/SignOutChooser.tsx +84 -0
  96. package/src/panels/IdentityPanel/steps/index.ts +2 -0
  97. package/src/panels/IdentityPanel/useAgentHandlers.ts +88 -0
  98. package/src/panels/JoinPanel/JoinPanel.tsx +2 -2
  99. package/src/panels/JoinPanel/joinMachine.ts +16 -19
  100. package/src/panels/JoinPanel/steps/AdditionMethodChooser.tsx +3 -3
  101. package/src/panels/JoinPanel/steps/InvitationInput.tsx +7 -1
  102. package/src/panels/JoinPanel/steps/ResetIdentity.tsx +1 -1
  103. package/src/panels/SpacePanel/SpacePanel.stories.tsx +2 -1
  104. package/src/panels/SpacePanel/SpacePanel.tsx +4 -1
  105. package/src/panels/SpacePanel/SpacePanelProps.ts +1 -0
  106. package/src/panels/SpacePanel/steps/SpaceManager.tsx +4 -1
  107. package/src/playwright/invitations-manager.ts +5 -1
  108. package/src/playwright/invitations.spec.ts +19 -0
  109. package/src/testing/shell-manager.ts +40 -13
  110. package/src/translations/locales/en-US.ts +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/shell",
3
- "version": "0.3.11-main.4eab977",
3
+ "version": "0.3.11-main.4faaf44",
4
4
  "description": "DXOS’s OS experiences",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -39,34 +39,35 @@
39
39
  "react-qr-rounded": "^1.0.0",
40
40
  "url-join": "^5.0.0",
41
41
  "xstate": "^4.37.0",
42
- "@dxos/async": "0.3.11-main.4eab977",
43
- "@dxos/client-protocol": "0.3.11-main.4eab977",
44
- "@dxos/debug": "0.3.11-main.4eab977",
45
- "@dxos/client-services": "0.3.11-main.4eab977",
46
- "@dxos/display-name": "0.3.11-main.4eab977",
47
- "@dxos/react-client": "0.3.11-main.4eab977",
48
- "@dxos/log": "0.3.11-main.4eab977",
49
- "@dxos/react-ui-theme": "0.3.11-main.4eab977",
50
- "@dxos/react-ui": "0.3.11-main.4eab977",
51
- "@dxos/react-ui-types": "0.3.11-main.4eab977",
52
- "@dxos/rpc-tunnel": "0.3.11-main.4eab977",
53
- "@dxos/util": "0.3.11-main.4eab977"
42
+ "@dxos/async": "0.3.11-main.4faaf44",
43
+ "@dxos/client-services": "0.3.11-main.4faaf44",
44
+ "@dxos/debug": "0.3.11-main.4faaf44",
45
+ "@dxos/display-name": "0.3.11-main.4faaf44",
46
+ "@dxos/invariant": "0.3.11-main.4faaf44",
47
+ "@dxos/react-client": "0.3.11-main.4faaf44",
48
+ "@dxos/log": "0.3.11-main.4faaf44",
49
+ "@dxos/react-ui": "0.3.11-main.4faaf44",
50
+ "@dxos/react-ui-theme": "0.3.11-main.4faaf44",
51
+ "@dxos/react-ui-types": "0.3.11-main.4faaf44",
52
+ "@dxos/rpc-tunnel": "0.3.11-main.4faaf44",
53
+ "@dxos/util": "0.3.11-main.4faaf44",
54
+ "@dxos/client-protocol": "0.3.11-main.4faaf44"
54
55
  },
55
56
  "devDependencies": {
56
- "@faker-js/faker": "^8.0.2",
57
+ "@faker-js/faker": "^8.3.1",
57
58
  "@phosphor-icons/react": "^2.0.5",
58
59
  "@types/lodash.toarray": "^4.4.7",
59
60
  "@types/react": "^18.0.21",
60
61
  "@types/react-dom": "^18.0.6",
61
- "@vitejs/plugin-react": "^3.0.1",
62
+ "@vitejs/plugin-react": "^4.2.1",
62
63
  "react": "^18.2.0",
63
64
  "react-dom": "^18.2.0",
64
- "vite": "^4.3.9",
65
+ "vite": "^5.0.12",
65
66
  "vite-plugin-turbosnap": "^1.0.2",
66
- "@dxos/react-async": "0.3.11-main.4eab977",
67
- "@dxos/storybook-utils": "0.3.11-main.4eab977",
68
- "@dxos/react-ui-theme": "0.3.11-main.4eab977",
69
- "@dxos/config": "0.3.11-main.4eab977"
67
+ "@dxos/config": "0.3.11-main.4faaf44",
68
+ "@dxos/react-ui-theme": "0.3.11-main.4faaf44",
69
+ "@dxos/react-async": "0.3.11-main.4faaf44",
70
+ "@dxos/storybook-utils": "0.3.11-main.4faaf44"
70
71
  },
71
72
  "peerDependencies": {
72
73
  "@phosphor-icons/react": "^2.0.5",
@@ -45,7 +45,7 @@ export const SpaceMemberListImpl = ({ members, onSelect }: SpaceMemberListImplPr
45
45
  const { t } = useTranslation('os');
46
46
  const visibleMembers = members.filter((member) => member.identity);
47
47
  return visibleMembers.length > 0 ? (
48
- <List classNames='flex flex-col gap-2'>
48
+ <List classNames='flex flex-col gap-2' data-testid='space-members-list'>
49
49
  {visibleMembers.map((member) => {
50
50
  return (
51
51
  <IdentityListItem
@@ -6,7 +6,7 @@ import { CaretDown, Check, type IconProps, Placeholder } from '@phosphor-icons/r
6
6
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
7
7
  import React, { type Dispatch, type FC, forwardRef, type SetStateAction } from 'react';
8
8
 
9
- import { type ButtonProps, Button, type ComponentFragment, DropdownMenu, useTranslation } from '@dxos/react-ui';
9
+ import { type ButtonProps, Button, DropdownMenu, useTranslation } from '@dxos/react-ui';
10
10
  import { descriptionText, getSize, mx } from '@dxos/react-ui-theme';
11
11
 
12
12
  export type LargeButtonProps = ButtonProps & {
@@ -26,21 +26,6 @@ export type BifurcatedActionProps = {
26
26
  defaultActiveAction?: string;
27
27
  } & Omit<LargeButtonProps, 'children' | 'onClick'>;
28
28
 
29
- const buttonColorFragment: ComponentFragment<LargeButtonProps> = (props: LargeButtonProps) => {
30
- const resolvedVariant = props.variant ?? 'default';
31
- return [
32
- !props.disabled
33
- ? resolvedVariant === 'default'
34
- ? 'bg-neutral-25 hover:bg-white hover:text-primary-500 active:bg-neutral-25 dark:active:bg-neutral-800'
35
- : resolvedVariant === 'ghost'
36
- ? 'active:bg-neutral-125 dark:active:bg-neutral-800'
37
- : resolvedVariant === 'primary'
38
- ? 'active:bg-primary-700 dark:active:bg-primary-500'
39
- : ''
40
- : '',
41
- ];
42
- };
43
-
44
29
  const defaultActions = {
45
30
  noopAction: {
46
31
  label: 'No-op',
@@ -76,7 +61,7 @@ export const BifurcatedAction = forwardRef<HTMLButtonElement, BifurcatedActionPr
76
61
  <div role='none' className={mx('mbs-2 flex gap-px items-center', isFull && 'is-full')}>
77
62
  <Button
78
63
  {...rest}
79
- classNames={['bs-11 flex-1 min-is-0 flex gap-2 rounded-ie-none', classNames, ...buttonColorFragment(props)]}
64
+ classNames={['bs-11 flex-1 min-is-0 flex gap-2 rounded-ie-none', classNames]}
80
65
  ref={forwardedRef}
81
66
  variant={variant}
82
67
  onClick={activeAction.onClick}
@@ -86,7 +71,7 @@ export const BifurcatedAction = forwardRef<HTMLButtonElement, BifurcatedActionPr
86
71
  </Button>
87
72
  <DropdownMenu.Root>
88
73
  <DropdownMenu.Trigger asChild>
89
- <Button classNames={['bs-11 flex-none rounded-is-none', classNames, ...buttonColorFragment(props)]}>
74
+ <Button classNames={['bs-11 flex-none rounded-is-none', classNames]}>
90
75
  <span className='sr-only'>{t('invite options label')}</span>
91
76
  <CaretDown />
92
77
  </Button>
@@ -132,7 +117,7 @@ export const Action = forwardRef<HTMLButtonElement, LargeButtonProps>((props, fo
132
117
  return (
133
118
  <Button
134
119
  {...rest}
135
- classNames={[isFull && 'is-full', 'bs-11 flex gap-2 mbs-2', classNames, ...buttonColorFragment(props)]}
120
+ classNames={[isFull && 'is-full', 'bs-11 flex gap-2 mbs-2', classNames]}
136
121
  ref={forwardedRef}
137
122
  variant={variant}
138
123
  >
@@ -5,7 +5,7 @@
5
5
  import React, { useEffect, useState } from 'react';
6
6
 
7
7
  import { log } from '@dxos/log';
8
- import { type LayoutRequest, ShellDisplay, ShellLayout, type ShellRuntime } from '@dxos/react-client';
8
+ import { type LayoutRequest, ShellDisplay, ShellLayout, type ShellRuntime, useClient } from '@dxos/react-client';
9
9
  import { useSpace } from '@dxos/react-client/echo';
10
10
 
11
11
  import { IdentityDialog } from '../IdentityDialog';
@@ -13,12 +13,14 @@ import { JoinDialog } from '../JoinDialog';
13
13
  import { SpaceDialog } from '../SpaceDialog';
14
14
 
15
15
  export const Shell = ({ runtime, origin }: { runtime: ShellRuntime; origin: string }) => {
16
- const [{ layout, invitationCode, spaceKey }, setLayout] = useState<LayoutRequest>({
16
+ const [{ layout, invitationCode, spaceKey, target }, setLayout] = useState<LayoutRequest>({
17
17
  layout: runtime.layout,
18
18
  invitationCode: runtime.invitationCode,
19
19
  spaceKey: runtime.spaceKey,
20
+ target: runtime.target,
20
21
  });
21
22
 
23
+ const client = useClient();
22
24
  const space = useSpace(spaceKey);
23
25
 
24
26
  useEffect(() => {
@@ -58,6 +60,11 @@ export const Shell = ({ runtime, origin }: { runtime: ShellRuntime; origin: stri
58
60
  return (
59
61
  <IdentityDialog
60
62
  createInvitationUrl={(invitationCode) => `${origin}?deviceInvitationCode=${invitationCode}`}
63
+ onResetDevice={async () => {
64
+ await client.reset();
65
+ await runtime.setAppContext({ display: ShellDisplay.NONE, reload: true });
66
+ }}
67
+ onJoinNewIdentity={() => runtime.setLayout({ layout: ShellLayout.INITIALIZE_IDENTITY })}
61
68
  onDone={async () => {
62
69
  await runtime.setAppContext({ display: ShellDisplay.NONE });
63
70
  runtime.setLayout({ layout: ShellLayout.DEFAULT });
@@ -69,6 +76,7 @@ export const Shell = ({ runtime, origin }: { runtime: ShellRuntime; origin: stri
69
76
  return space ? (
70
77
  <SpaceDialog
71
78
  space={space}
79
+ target={target}
72
80
  createInvitationUrl={(invitationCode) => `${origin}?spaceInvitationCode=${invitationCode}`}
73
81
  onDone={async () => {
74
82
  await runtime.setAppContext({ display: ShellDisplay.NONE });
@@ -82,7 +90,12 @@ export const Shell = ({ runtime, origin }: { runtime: ShellRuntime; origin: stri
82
90
  <JoinDialog
83
91
  initialInvitationCode={invitationCode}
84
92
  onDone={async (result) => {
85
- await runtime.setAppContext({ display: ShellDisplay.NONE, spaceKey: result?.spaceKey ?? undefined });
93
+ const target = result?.target ?? undefined;
94
+ await runtime.setAppContext({
95
+ display: ShellDisplay.NONE,
96
+ spaceKey: result?.spaceKey ?? undefined,
97
+ target,
98
+ });
86
99
  runtime.setLayout({ layout: ShellLayout.DEFAULT });
87
100
  }}
88
101
  onExit={async () => {
@@ -6,7 +6,7 @@ import React, { useEffect, useMemo } from 'react';
6
6
  import { generateName } from '@dxos/display-name';
7
7
  import { log } from '@dxos/log';
8
8
  import { useClient } from '@dxos/react-client';
9
- import { type Identity, useIdentity, useDevices } from '@dxos/react-client/halo';
9
+ import { type Identity, useIdentity, useDevices, useHaloInvitations } from '@dxos/react-client/halo';
10
10
  import { useInvitationStatus } from '@dxos/react-client/invitations';
11
11
  import type { CancellableInvitationObservable } from '@dxos/react-client/invitations';
12
12
  import { Avatar, DensityProvider, useId, useJdenticonHref, useTranslation } from '@dxos/react-ui';
@@ -17,7 +17,8 @@ import {
17
17
  type IdentityPanelProps,
18
18
  } from './IdentityPanelProps';
19
19
  import { useIdentityMachine } from './identityMachine';
20
- import { DeviceManager, IdentityActionChooser, ProfileForm } from './steps';
20
+ import { AgentForm, DeviceManager, IdentityActionChooser, ProfileForm, SignOutChooser } from './steps';
21
+ import { useAgentHandlers } from './useAgentHandlers';
21
22
  import { Viewport, Heading, CloseButton } from '../../components';
22
23
  import { InvitationManager } from '../../steps';
23
24
 
@@ -31,7 +32,7 @@ const IdentityHeading = ({ titleId, title, identity, onDone }: IdentityPanelHead
31
32
  <Avatar.Frame classNames='block mbs-4 mbe-2 mli-auto chromatic-ignore'>
32
33
  <Avatar.Fallback href={fallbackHref} />
33
34
  </Avatar.Frame>
34
- <Avatar.Label classNames='block text-center font-light text-xl'>
35
+ <Avatar.Label classNames='block text-center font-light text-xl' data-testid='identityHeading.displayName'>
35
36
  {identity.profile?.displayName ?? generateName(identity.identityKey.toHex())}
36
37
  </Avatar.Label>
37
38
  </Avatar.Root>
@@ -45,6 +46,8 @@ export const IdentityPanelImpl = (props: IdentityPanelImplProps) => {
45
46
  titleId,
46
47
  activeView,
47
48
  onUpdateProfile,
49
+ onResetDevice,
50
+ onJoinNewIdentity,
48
51
  IdentityActionChooser: IdentityActionChooserComponent = IdentityActionChooser,
49
52
  InvitationManager: InvitationManagerComponent = InvitationManager,
50
53
  onDone,
@@ -53,15 +56,15 @@ export const IdentityPanelImpl = (props: IdentityPanelImplProps) => {
53
56
  const { t } = useTranslation('os');
54
57
  const title = useMemo(() => {
55
58
  switch (activeView) {
56
- case 'device manager':
57
- return t('choose devices label');
59
+ case 'agent manager':
60
+ return 'Manage Agent';
58
61
  case 'device invitation manager':
59
62
  return t('choose add device label');
60
63
  default:
61
64
  return t('identity heading');
62
65
  }
63
66
  }, [activeView, t]);
64
- console.log('[IdentityPanelImpl]', { activeView });
67
+
65
68
  return (
66
69
  <DensityProvider density='fine'>
67
70
  <IdentityHeading {...{ identity, titleId, title, onDone }} />
@@ -88,7 +91,17 @@ export const IdentityPanelImpl = (props: IdentityPanelImplProps) => {
88
91
  onUpdateProfile={onUpdateProfile}
89
92
  />
90
93
  </Viewport.View>
91
- {/* <Viewport.View id='signing out'></Viewport.View> */}
94
+ <Viewport.View classNames={viewStyles} id='signing out'>
95
+ <SignOutChooser
96
+ send={rest.send}
97
+ active={activeView === 'signing out'}
98
+ onResetDevice={onResetDevice}
99
+ onJoinNewIdentity={onJoinNewIdentity}
100
+ />
101
+ </Viewport.View>
102
+ <Viewport.View classNames={viewStyles} id='agent manager'>
103
+ <AgentForm send={rest.send} active={activeView === 'agent manager'} {...rest} />
104
+ </Viewport.View>
92
105
  </Viewport.Views>
93
106
  </Viewport.Root>
94
107
  </DensityProvider>
@@ -112,6 +125,8 @@ export const IdentityPanel = ({
112
125
  const client = useClient();
113
126
  const devices = useDevices();
114
127
  const identity = useIdentity();
128
+ const invitations = useHaloInvitations();
129
+ const agentProps = useAgentHandlers({ client, identity, invitations });
115
130
  if (!identity) {
116
131
  console.error('IdentityPanel rendered with no active identity.');
117
132
  return null;
@@ -136,15 +151,15 @@ export const IdentityPanel = ({
136
151
  return 'device manager';
137
152
  case [{ managingProfile: 'idle' }, { managingProfile: 'pending' }].some(identityState.matches):
138
153
  return 'update profile form';
139
- // case identityState.matches('signingOut'):
140
- // return 'identity exit';
154
+ case identityState.matches('signingOut'):
155
+ return 'signing out';
156
+ case [{ managingAgent: 'idle' }, { managingAgent: 'pending' }].some(identityState.matches):
157
+ return 'agent manager';
141
158
  default:
142
159
  return 'identity action chooser';
143
160
  }
144
161
  }, [identityState]);
145
162
 
146
- console.log({ activeView, identityState });
147
-
148
163
  const onUpdateProfile = async (profile: NonNullable<Identity['profile']>) => {
149
164
  identitySend({ type: 'updateProfile' });
150
165
  await client.halo.updateProfile(profile);
@@ -159,6 +174,7 @@ export const IdentityPanel = ({
159
174
  titleId,
160
175
  createInvitationUrl,
161
176
  onUpdateProfile,
177
+ ...agentProps,
162
178
  } satisfies IdentityPanelImplProps;
163
179
 
164
180
  return identityState.context.invitation ? (
@@ -11,12 +11,29 @@ import type { InvitationStatus } from '@dxos/react-client/invitations';
11
11
  import type { IdentityEvent } from './identityMachine';
12
12
  import type { InvitationManagerProps } from '../../steps';
13
13
 
14
+ export type AgentFormProps = {
15
+ onAgentCreate?: () => Promise<void>;
16
+ onAgentDestroy?: () => Promise<void>;
17
+ agentStatus?: string;
18
+ agentActive?: boolean;
19
+ agentProviderDisabled?: boolean;
20
+ validationMessage?: string;
21
+ };
22
+
14
23
  export type IdentityPanelImplProps = {
15
24
  titleId: string;
16
- activeView: 'device manager' | 'update profile form' | 'device invitation manager' | 'identity action chooser';
25
+ activeView:
26
+ | 'device manager'
27
+ | 'agent manager'
28
+ | 'update profile form'
29
+ | 'device invitation manager'
30
+ | 'identity action chooser'
31
+ | 'signing out';
17
32
  identity: Identity;
18
33
  devices: Device[];
19
34
  onUpdateProfile?: (profile: NonNullable<Identity['profile']>) => Promise<void>;
35
+ onResetDevice?: () => Promise<void>;
36
+ onJoinNewIdentity?: () => void;
20
37
  createInvitationUrl: (invitationCode: string) => string;
21
38
  send?: (event: SingleOrArray<Event<IdentityEvent>>) => void;
22
39
  onDone?: () => void;
@@ -24,7 +41,8 @@ export type IdentityPanelImplProps = {
24
41
  invitationUrl?: string;
25
42
  IdentityActionChooser?: React.FC<IdentityPanelStepProps>;
26
43
  InvitationManager?: React.FC<InvitationManagerProps>;
27
- } & Partial<InvitationStatus>;
44
+ } & Partial<InvitationStatus> &
45
+ Partial<AgentFormProps>;
28
46
 
29
47
  export type IdentityPanelProps = Partial<Omit<IdentityPanelImplProps, 'send' | 'activeView' | 'identity'>>;
30
48
 
@@ -19,7 +19,7 @@ type IdentityMachineContext = {
19
19
  };
20
20
 
21
21
  type IdentityChooseActionEvent = {
22
- type: 'chooseDevices' | 'chooseProfile' | /* 'chooseSignOut' | */ 'unchooseAction';
22
+ type: 'chooseDevices' | 'chooseAgent' | 'chooseProfile' | 'chooseSignOut' | 'sigingOut' | 'unchooseAction';
23
23
  };
24
24
 
25
25
  type IdentitySelectDeviceInvitationEvent = {
@@ -58,6 +58,13 @@ const identityMachine = createMachine<IdentityMachineContext, IdentityEvent>(
58
58
  states: {
59
59
  choosingAction: {},
60
60
  managingDeviceInvitation: {},
61
+ managingAgent: {
62
+ initial: 'idle',
63
+ states: {
64
+ idle: {},
65
+ pending: {},
66
+ },
67
+ },
61
68
  managingDevices: {},
62
69
  managingProfile: {
63
70
  initial: 'idle',
@@ -70,7 +77,7 @@ const identityMachine = createMachine<IdentityMachineContext, IdentityEvent>(
70
77
  updateProfile: { target: '.pending', actions: 'log' },
71
78
  },
72
79
  },
73
- // signingOut: {},
80
+ signingOut: {},
74
81
  },
75
82
  on: {
76
83
  unchooseAction: { target: '.choosingAction', actions: ['unsetInvitation', 'log'] },
@@ -78,7 +85,8 @@ const identityMachine = createMachine<IdentityMachineContext, IdentityEvent>(
78
85
  selectInvitation: { target: '.managingDeviceInvitation', actions: ['setInvitation', 'log'] },
79
86
  chooseDevices: { target: '.managingDevices', actions: 'log' },
80
87
  chooseProfile: { target: '.managingProfile', actions: 'log' },
81
- // chooseSignOut: { target: '.signingOut', actions: 'log' },
88
+ chooseSignOut: { target: '.signingOut', actions: 'log' },
89
+ chooseAgent: { target: '.managingAgent', actions: 'log' },
82
90
  },
83
91
  },
84
92
  {
@@ -0,0 +1,61 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { useTranslation } from '@dxos/react-ui';
8
+
9
+ import { Action, Actions, StepHeading, Input } from '../../../components';
10
+ import { type AgentFormProps, type IdentityPanelStepProps } from '../IdentityPanelProps';
11
+
12
+ export const AgentForm = (props: IdentityPanelStepProps & AgentFormProps) => {
13
+ const {
14
+ active,
15
+ onAgentCreate,
16
+ onAgentDestroy,
17
+ send,
18
+ validationMessage,
19
+ agentStatus,
20
+ agentActive,
21
+ agentProviderDisabled,
22
+ } = props;
23
+ const disabled = !active;
24
+ const { t } = useTranslation('os');
25
+ return (
26
+ <>
27
+ <div role='none' className='grow flex flex-col justify-center'>
28
+ <Input
29
+ {...{ validationMessage }}
30
+ label={<StepHeading>Agent Status</StepHeading>}
31
+ disabled={disabled}
32
+ data-testid='agent-status'
33
+ value={agentStatus}
34
+ />
35
+ </div>
36
+ <Actions>
37
+ <Action
38
+ disabled={agentActive || agentProviderDisabled}
39
+ onClick={() => onAgentCreate?.()}
40
+ data-testid={'create-agent'}
41
+ >
42
+ Create Agent
43
+ </Action>
44
+ <Action
45
+ disabled={!agentActive || agentProviderDisabled}
46
+ onClick={() => onAgentDestroy?.()}
47
+ data-testid={'destroy-agent'}
48
+ >
49
+ Destroy Agent
50
+ </Action>
51
+ <Action
52
+ disabled={disabled}
53
+ onClick={() => send?.({ type: 'unchooseAction' })}
54
+ data-testid={'update-profile-form-back'}
55
+ >
56
+ {t('back label')}
57
+ </Action>
58
+ </Actions>
59
+ </>
60
+ );
61
+ };
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { CaretRight, Devices, Plus, Power, UserGear } from '@phosphor-icons/react';
5
+ import { CaretRight, Devices, Plus, Power, UserGear, HardDrive } from '@phosphor-icons/react';
6
6
  import React, { useCallback } from 'react';
7
7
 
8
8
  import { useClient } from '@dxos/react-client';
@@ -63,6 +63,11 @@ export const IdentityActionChooserImpl = ({
63
63
  <div role='none' className='grow flex flex-col gap-1'>
64
64
  <DensityProvider density='coarse'>
65
65
  <div className='grow justify-center flex flex-col gap-1'>
66
+ <Action data-testid='manage-agent' onClick={() => send?.({ type: 'chooseAgent' })} classNames='plb-4'>
67
+ <HardDrive className={getSize(6)} />
68
+ <span className='grow mli-3'>Manage Agent</span>
69
+ <CaretRight weight='bold' className={getSize(4)} />
70
+ </Action>
66
71
  <Action
67
72
  disabled={!active}
68
73
  data-testid='devices-panel.create-invitation'
@@ -84,9 +89,8 @@ export const IdentityActionChooserImpl = ({
84
89
  <CaretRight weight='bold' className={getSize(4)} />
85
90
  </Action>
86
91
  <Action
87
- disabled
88
- data-testid='sign-out'
89
- onClick={() => {} /* send({ type: 'chooseSignOut' }) */}
92
+ data-testid='devices-panel.sign-out'
93
+ onClick={() => send?.({ type: 'chooseSignOut' })}
90
94
  classNames='plb-4'
91
95
  >
92
96
  <Power className={getSize(6)} />
@@ -0,0 +1,84 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Warning } from '@phosphor-icons/react';
6
+ import React from 'react';
7
+ import { type Event, type SingleOrArray } from 'xstate';
8
+
9
+ import { Message, useTranslation } from '@dxos/react-ui';
10
+ import { getSize, mx } from '@dxos/react-ui-theme';
11
+
12
+ import { Action, Actions } from '../../../components';
13
+ import { type IdentityPanelStepProps } from '../IdentityPanelProps';
14
+ import { type IdentityEvent } from '../identityMachine';
15
+
16
+ export interface SignOutChooserProps extends Omit<IdentityPanelStepProps, 'send'> {
17
+ send?: (event: SingleOrArray<Event<IdentityEvent>>) => void;
18
+ onResetDevice?: () => Promise<void>;
19
+ onJoinNewIdentity?: () => void;
20
+ }
21
+
22
+ export const SignOutChooser = (props: SignOutChooserProps) => {
23
+ const { t } = useTranslation('os');
24
+ const onResetDevice = () => {
25
+ if (window.confirm(t('reset device confirm message'))) {
26
+ void props.onResetDevice?.();
27
+ }
28
+ };
29
+ return (
30
+ <SignOutChooserImpl
31
+ disabled={!props.active}
32
+ onBack={() => props.send?.({ type: 'unchooseAction' })}
33
+ onResetDevice={onResetDevice}
34
+ onJoinNewIdentity={props.onJoinNewIdentity}
35
+ />
36
+ );
37
+ };
38
+
39
+ export type SignOutChooserImplProps = {
40
+ disabled?: boolean;
41
+ onResetDevice?: () => void;
42
+ onJoinNewIdentity?: () => void;
43
+ onBack?: () => void;
44
+ };
45
+
46
+ const SignOutChooserImpl = ({ disabled, onResetDevice, onJoinNewIdentity, onBack }: SignOutChooserImplProps) => {
47
+ const { t } = useTranslation('os');
48
+ return (
49
+ <>
50
+ <div role='none' className='grow flex flex-col justify-center'>
51
+ <Message.Root valence='error'>
52
+ <Message.Title>
53
+ <Warning className={mx(getSize(6), 'inline mie-2')} />
54
+ {t('sign out chooser title')}
55
+ </Message.Title>
56
+ <Message.Body>{t('sign out chooser message')}</Message.Body>
57
+ </Message.Root>
58
+ </div>
59
+ <Actions>
60
+ <Action disabled={disabled} onClick={onBack}>
61
+ {t('back label')}
62
+ </Action>
63
+ <Action
64
+ variant='primary'
65
+ data-testid='sign-out.join-new-identity'
66
+ classNames='!bg-error-500 hover:!bg-error-550'
67
+ disabled={disabled}
68
+ onClick={onJoinNewIdentity}
69
+ >
70
+ {t('join new identity label')}
71
+ </Action>
72
+ <Action
73
+ variant='primary'
74
+ data-testid='sign-out.reset-device'
75
+ classNames='!bg-error-500 hover:!bg-error-550'
76
+ disabled={disabled}
77
+ onClick={onResetDevice}
78
+ >
79
+ {t('reset device label')}
80
+ </Action>
81
+ </Actions>
82
+ </>
83
+ );
84
+ };
@@ -2,6 +2,8 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './AgentForm';
5
6
  export * from './DeviceManager';
6
7
  export * from './IdentityActionChooser';
7
8
  export * from './ProfileForm';
9
+ export * from './SignOutChooser';
@@ -0,0 +1,88 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { useCallback, useEffect, useState } from 'react';
6
+
7
+ import { type CancellableInvitation } from '@dxos/client-protocol';
8
+ import { invariant } from '@dxos/invariant';
9
+ import { useAgentHostingProviderClient, type Client } from '@dxos/react-client';
10
+ import { type Identity } from '@dxos/react-client/halo';
11
+ import { Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
12
+
13
+ export const useAgentHandlers = ({
14
+ client,
15
+ invitations,
16
+ identity,
17
+ }: {
18
+ client: Client;
19
+ invitations: CancellableInvitation[];
20
+ identity: Identity | null;
21
+ }) => {
22
+ const [validationMessage, setValidationMessage] = useState('');
23
+ const [agentStatus, setAgentStatus] = useState('');
24
+ const [agentActive, setAgentActive] = useState(false);
25
+ const agentHostingProviderClient = useAgentHostingProviderClient(client.config);
26
+
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);
35
+ } else {
36
+ setAgentActive(false);
37
+ setValidationMessage('No agent deployed via provider.');
38
+ }
39
+ } else {
40
+ setValidationMessage('No agent provider configured.');
41
+ }
42
+ };
43
+ void fetchAgentStatus();
44
+ }, []);
45
+
46
+ const handleAgentCreate = useCallback(async (invitation: Invitation) => {
47
+ const invitationCode = InvitationEncoder.encode(invitation);
48
+ if (invitation.state === Invitation.State.CONNECTING) {
49
+ console.log(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
+ }
62
+ }
63
+ }, []);
64
+
65
+ const onAgentCreate = async () => {
66
+ invitations.forEach((invitation) => invitation.cancel());
67
+
68
+ // TODO(nf): do this work in the hosting provider client?
69
+ const invitation = client.halo.share({ type: Invitation.Type.MULTIUSE, authMethod: Invitation.AuthMethod.NONE });
70
+
71
+ invitation.subscribe(handleAgentCreate);
72
+ };
73
+
74
+ const onAgentDestroy = async () => {
75
+ const identity = client.halo.identity.get();
76
+ invariant(identity, 'Identity not found');
77
+ try {
78
+ await agentHostingProviderClient?.destroyAgent(identity.identityKey.truncate());
79
+ setValidationMessage('No agent deployed via provider.');
80
+ setAgentStatus('');
81
+ setAgentActive(false);
82
+ } catch (err: any) {
83
+ setValidationMessage(`error destroying agent: ${err.message}`);
84
+ }
85
+ };
86
+
87
+ return { onAgentCreate, onAgentDestroy, validationMessage, agentStatus, agentActive };
88
+ };