Compare commits

..

756 Commits

Author SHA1 Message Date
c03fc9bb1e chore: hide anotation and evn secrets 2026-06-16 18:17:46 +08:00
yyh
2a86ba9882 Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-06-16 18:06:05 +08:00
1427b0b098 feat: refine snippet layout (#37517)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-16 09:47:38 +00:00
yyh
d55f7e66da fix: contracts type 2026-06-16 17:45:45 +08:00
yyh
be664f9b08 Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-06-16 17:42:40 +08:00
yyh
2893adf5e4 test(dify-ui): add Storybook interaction coverage (#37519) 2026-06-16 09:39:37 +00:00
eb2aaf2ac1 fix(docker): remove duplicate inline styles env (#37510) 2026-06-16 09:36:48 +00:00
84a77fa0f1 chore: cli tool env key envalide 2026-06-16 17:28:40 +08:00
24ee393b91 fix: cli save problem 2026-06-16 17:18:33 +08:00
9a6325a972 fix: tool setting problem 2026-06-16 17:18:33 +08:00
yyh
0e4878b8ee Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-06-16 17:07:06 +08:00
yyh
cd877ed430 refactor: state 2026-06-16 17:06:41 +08:00
yyh
cb34ed6bc0 refactor(web): keep agent node avatar presentational 2026-06-16 16:54:51 +08:00
yyh
05597a71a2 fix: use skeleton when request 2026-06-16 16:50:14 +08:00
yyh
3392f5187d fix: output variables style 2026-06-16 16:46:47 +08:00
yyh
10c7d62853 fix(web): open inline agent panel after creation 2026-06-16 16:46:47 +08:00
48e22f9b2a chore: tool setting 2026-06-16 16:39:16 +08:00
959b6f23ba chore: tools detail show and knowledge setting 2026-06-16 16:33:30 +08:00
f5a9de8866 fix: no knowledge show 2026-06-16 16:20:37 +08:00
ec55804ea2 fix: tool can not save 2026-06-16 16:15:16 +08:00
yyh
5dcaa458ce Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-06-16 16:12:56 +08:00
yyh
06f2ea7056 Merge branch 'feat/agent-v2' of https://github.com/langgenius/dify into feat/agent-v2 2026-06-16 16:12:46 +08:00
yyh
56f17ed3f7 test: no skeleton 2026-06-16 16:12:42 +08:00
yyh
0f0b0a017f fix(web): guard pending inline agent creation 2026-06-16 16:12:20 +08:00
2d1da4c274 chore: no credial tool can save 2026-06-16 16:03:14 +08:00
43a8d49c87 chore: create and edit agent and role 2026-06-16 16:03:13 +08:00
yyh
a987714097 fix: skeleton 2026-06-16 16:00:42 +08:00
yyh
4220c305a1 fix(web): show inline agent details in workflow 2026-06-16 15:38:36 +08:00
yyh
b3ba2b5c43 fix(web): initialize inline agent model 2026-06-16 15:28:38 +08:00
yyh
d04dfe44df fix(web): create inline agent binding before draft sync 2026-06-16 15:20:36 +08:00
7b4978be74 ci: revert feat/agent-v2 from build-push triggers, use deploy/agent branch instead 2026-06-16 15:02:47 +08:00
c4c5b1231f ci: only build images on feat/agent-v2 when commit message contains 'deploy' 2026-06-16 15:00:54 +08:00
c860620e07 chore: tests 2026-06-16 15:00:12 +08:00
88f80980a7 ci: add feat/agent-v2 to build-push trigger branches 2026-06-16 14:56:36 +08:00
97328278fc chore: use new agent chat 2026-06-16 14:56:00 +08:00
yyh
e909f3b3b9 refactor(web): use collapsible for output variable advanced options 2026-06-16 14:54:24 +08:00
a093e75735 ci: add deploy-agent workflow triggered on deploy/agent branch 2026-06-16 14:51:12 +08:00
yyh
f47f4c5316 Merge branch 'feat/agent-v2' of https://github.com/langgenius/dify into feat/agent-v2 2026-06-16 14:46:41 +08:00
684441b50a chroe: handle draft save and public 2026-06-16 14:45:45 +08:00
yyh
012c9753e1 feat(web): add output variable editor escape shortcut 2026-06-16 14:45:36 +08:00
yyh
2483106744 Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-06-16 14:43:05 +08:00
yyh
8ab663da86 style(web): align output variable advanced toggle 2026-06-16 14:42:20 +08:00
yyh
07ac1619b7 feat(web): add output variable editor shortcut 2026-06-16 14:39:44 +08:00
yyh
3f35f6e8e2 feat(web): add editable agent v2 output variables 2026-06-16 14:27:00 +08:00
b613cc19a3 chroe: skill item padding 2026-06-16 14:14:14 +08:00
0b03e72421 chore: file preview and skill item padding 2026-06-16 14:12:04 +08:00
61af7de49d feat: skill can review and not auto publish 2026-06-16 13:58:10 +08:00
yyh
ebb1b01bc8 Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-06-16 13:14:19 +08:00
yyh
649ef315d1 fix(web): align agent v2 roster graph binding 2026-06-16 11:13:55 +08:00
yyh
8076f0e36c refactor(agent-v2): read output variables from draft graph 2026-06-16 10:02:49 +08:00
yyh
aee88de232 Merge branch 'feat/agent-v2' of https://github.com/langgenius/dify into feat/agent-v2 2026-06-16 09:51:14 +08:00
yyh
aeb54da4b3 Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-06-16 09:50:15 +08:00
edf79cb70e fix: agent not pass model plugin id 2026-06-15 22:54:11 +08:00
2f7a484464 fix: use new agent list to get data 2026-06-15 22:46:45 +08:00
d03ea65e83 chore: use new agent api 2026-06-15 22:14:26 +08:00
07f4f95ffc Merge branch 'main' into feat/agent-v2 2026-06-15 21:26:24 +08:00
cbd7f904f8 fix: agent tool credical problem 2026-06-15 18:18:47 +08:00
yyh
fc97a587c5 feat: new agent node 2026-06-15 17:43:36 +08:00
75321dd8e1 chore: content moderation preformance 2026-06-15 17:36:07 +08:00
yyh
6d8917db0c Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-06-15 17:31:50 +08:00
yyh
ee87d968c3 feat: start from scratch 2026-06-15 16:43:19 +08:00
yyh
62fc2da5d1 fix: agent selector use skeleton 2026-06-15 16:31:48 +08:00
yyh
1acf17c9ce feat: agent selector style 2026-06-15 16:25:16 +08:00
d4656a1770 chore: handle agent config performance problem 2026-06-15 16:22:04 +08:00
aa2ef0c6b6 chore: handle env save 2026-06-15 16:12:22 +08:00
3472f2ade6 chore: tools icon 2026-06-15 15:52:24 +08:00
474d09b7c0 chore: handle file icon in prompt editor 2026-06-15 15:32:46 +08:00
d5f28e77a0 fix: files list icon not right 2026-06-15 15:23:01 +08:00
bc204417b4 chore: remove mock related app 2026-06-15 15:23:01 +08:00
yyh
0565ed3bb2 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-15 14:56:16 +08:00
f571affb5c fix: unit test 2026-06-15 14:43:31 +08:00
dcfa15049b chore: pubish and tools name 2026-06-15 14:40:20 +08:00
bea319a6a7 Merge branch 'feat/ui-onboarding-rewrite' of github.com:langgenius/dify into feat/ui-onboarding-rewrite 2026-06-15 14:25:48 +08:00
723c9902b6 fix CI 2026-06-15 14:25:38 +08:00
f639da357f fix CI 2026-06-15 14:14:11 +08:00
8a24750117 chore: publish bar 2026-06-15 14:13:10 +08:00
7feaed7b0b feat: publish agent used panel 2026-06-15 14:13:10 +08:00
yyh
ae2b12fef8 Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-06-15 14:09:37 +08:00
yyh
b46dac0416 Merge branch 'feat/agent-v2' of https://github.com/langgenius/dify into feat/agent-v2 2026-06-15 14:08:47 +08:00
yyh
c69fcb0110 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-15 14:08:44 +08:00
2e1fa86950 fix agent knowledge settings drawer layout 2026-06-15 14:07:42 +08:00
4a87e17692 [autofix.ci] apply automated fixes 2026-06-15 06:05:38 +00:00
c2f43a2720 fix: should async delete app stars 2026-06-15 14:04:53 +08:00
34826f5748 chore: knowledge setting 2026-06-15 14:01:06 +08:00
cb903ec0c0 Merge remote-tracking branch 'myori/main' into feat/ui-onboarding-rewrite 2026-06-15 13:59:18 +08:00
61c61fe9d6 fix: fix CI 2026-06-15 13:56:49 +08:00
9b7d8a19ae chore: knowlesetting force modal mask 2026-06-15 13:49:06 +08:00
272f11896f fix: migration file versions 2026-06-15 13:45:22 +08:00
yyh
5a8485399b fix: stabilize roster filter count badges 2026-06-15 13:22:56 +08:00
yyh
1f7badef07 feat: empty roster list 2026-06-15 13:17:10 +08:00
31045cd229 docs: remove outdated integrations follow-ups 2026-06-14 22:12:15 -07:00
yyh
f6768a9b0c refactor(agent-v2): save workflow task through draft graph 2026-06-15 13:04:01 +08:00
920c61197d test(web): fix remaining onboarding CI assertions 2026-06-14 21:58:35 -07:00
bfa7504240 chore: refactor agent page and create pass mode 2026-06-15 12:47:50 +08:00
027bdfb092 test(web): align onboarding UI assertions 2026-06-14 21:43:25 -07:00
yyh
be3dfeba1b Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-15 12:35:45 +08:00
dabe29fd4f test(web): add explore pathname mocks 2026-06-14 21:01:49 -07:00
0007595320 [autofix.ci] apply automated fixes 2026-06-15 03:47:24 +00:00
f4ffb9fc10 fix(web): satisfy style checks 2026-06-14 20:42:22 -07:00
yyh
8617836e0c Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-15 11:33:03 +08:00
4f30d9bd92 fix(web): satisfy tss conditional rendering lint 2026-06-14 20:26:29 -07:00
ae726a0607 merge main 2026-06-15 11:19:11 +08:00
6f2bd7c2c6 fix(web): hide template card tags 2026-06-14 20:14:29 -07:00
49336e2dbc [autofix.ci] apply automated fixes 2026-06-15 03:08:22 +00:00
9e8a0f0d34 Merge branch 'main' into feat/ui-onboarding-rewrite 2026-06-14 20:03:36 -07:00
04bde3bc8d Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	api/services/recommended_app_service.py
#	web/app/components/apps/list.tsx
#	web/app/components/datasets/list/index.tsx
2026-06-14 19:50:10 -07:00
ca8ac4fed9 fix(web): align common locale copy 2026-06-14 19:42:58 -07:00
7009cd8060 feat: draft and publish app 2026-06-14 21:07:08 +08:00
yyh
f0e22be4e8 refactor(agent-v2): route app features writes through atom 2026-06-13 18:07:48 +08:00
yyh
5b808e3cb8 Merge branch 'feat/agent-v2' of https://github.com/langgenius/dify into feat/agent-v2 2026-06-13 17:56:25 +08:00
yyh
65cb1a8505 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-13 17:55:33 +08:00
yyh
3c719a96ea fix(api): use codegen-safe plugin model provider schema 2026-06-13 17:54:44 +08:00
yyh
32fd48b58f Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-13 17:40:40 +08:00
76ad1d58cd Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	api/controllers/console/explore/recommended_app.py
#	api/controllers/console/workspace/plugin.py
#	api/openapi/markdown/console-openapi.md
#	api/tests/unit_tests/services/test_recommended_app_service.py
#	packages/contracts/generated/api/console/apps/orpc.gen.ts
#	packages/contracts/generated/api/console/explore/orpc.gen.ts
#	packages/contracts/generated/api/console/explore/types.gen.ts
#	packages/contracts/generated/api/console/workspaces/orpc.gen.ts
#	packages/contracts/generated/api/console/workspaces/types.gen.ts
#	packages/contracts/generated/api/console/workspaces/zod.gen.ts
2026-06-12 18:36:45 -07:00
c47381777d fix(web): polish onboarding i18n overflow 2026-06-12 16:11:09 -07:00
e485e02b4f chore: fix store and fix env can not edit 2026-06-12 22:42:37 +08:00
5a56b5158d chore: split store 2026-06-12 22:12:49 +08:00
05e929bf70 fix: hydrate agent soul config into form state 2026-06-12 21:56:31 +08:00
fd49fa3653 fix: AgentComposerDraft config has inner problem 2026-06-12 21:44:47 +08:00
yyh
c587ca8120 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2
# Conflicts:
#	web/app/components/main-nav/__tests__/index.spec.tsx
2026-06-12 21:01:26 +08:00
yyh
0c849380a0 fix: allow detail sidebar shortcut in inputs 2026-06-12 20:59:36 +08:00
yyh
271c0f7ca5 fix: allow detail sidebar shortcut in inputs 2026-06-12 20:57:14 +08:00
yyh
6158974646 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-12 19:22:27 +08:00
yyh
9f8be0a002 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-12 19:22:18 +08:00
yyh
7d1eec789b fix: keep add tool popover focus stable 2026-06-12 18:19:34 +08:00
d02b478a2f feat: cli tool field warning and remove 2026-06-12 17:34:22 +08:00
yyh
6fe59c4a29 Merge branch 'feat/agent-v2' of https://github.com/langgenius/dify into feat/agent-v2 2026-06-12 17:22:41 +08:00
yyh
b8216523ad Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-12 17:22:34 +08:00
yyh
cf54e72689 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-12 17:22:25 +08:00
62bcc01d35 chore: enchance prompt and skills 2026-06-12 17:20:27 +08:00
c17452bff4 chore: default type 2026-06-12 17:05:41 +08:00
a15df89c2b chore: enchance prompt and content moderatin 2026-06-12 17:02:04 +08:00
a2187ad4a2 feat: content moderatrion and fix tools name sync 2026-06-12 17:02:04 +08:00
yyh
ea8a058179 fix(web): tighten start block preview card spacing 2026-06-12 16:50:55 +08:00
yyh
b9def65839 Merge branch 'feat/agent-v2' of https://github.com/langgenius/dify into feat/agent-v2 2026-06-12 16:38:48 +08:00
a0e64a679e chore: knowledge sync in prompt and config 2026-06-12 16:37:49 +08:00
yyh
ca765e0aea Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-06-12 16:36:12 +08:00
6a031d7f58 fix: maxmium update 2026-06-12 16:28:46 +08:00
yyh
c17d5744b0 Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-06-12 16:23:35 +08:00
b662187f6f feat: add tools in prompt 2026-06-12 16:23:07 +08:00
yyh
4eea58b613 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-12 16:20:44 +08:00
yyh
79c8491588 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-12 16:19:12 +08:00
yyh
68a9bf1d2e Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-12 16:18:50 +08:00
7e37100c2c feat: prompt editor tools new 2026-06-12 16:04:51 +08:00
yyh
a126adf78c fix(agent-v2): align agent detail roster header 2026-06-12 15:59:34 +08:00
b79c9d6dd5 chore: remove mock data and prompt add 2026-06-12 15:52:20 +08:00
yyh
d282f7a7d4 fix: css 2026-06-12 15:47:22 +08:00
yyh
6ac486b862 style: height 2026-06-12 15:45:45 +08:00
yyh
f75870fe0f fix: no scroll 2026-06-12 15:43:29 +08:00
yyh
586fe488b1 refactor(agent-v2): use roster dropdown data attributes 2026-06-12 15:32:03 +08:00
yyh
1d9e379269 feat(agent-v2): align access point design 2026-06-12 15:23:40 +08:00
bb63d1ee99 chore: tests 2026-06-12 15:17:03 +08:00
e40e79f615 chore: coden enchance and knowledge handle 2026-06-12 15:17:02 +08:00
0bbc408607 chore: knowledge and tools 2026-06-12 15:17:02 +08:00
yyh
e7d564302d fix: monitoring scroll area 2026-06-12 15:04:33 +08:00
yyh
6ecfdf4206 feat(agent-v2): align roster monitoring with design 2026-06-12 15:01:03 +08:00
3472475eac feat: provide tools support remove 2026-06-12 14:48:04 +08:00
71296b983c chore: use same item 2026-06-12 14:48:04 +08:00
adbcf8774c feat: tools menu 2026-06-12 14:48:03 +08:00
bdf66cd87a chore: cli tool item and dialog 2026-06-12 14:48:03 +08:00
yyh
2aa244f538 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-12 14:43:03 +08:00
yyh
f4057f1304 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-12 14:40:25 +08:00
yyh
e17f7d50a4 fix: agent roster list card 2026-06-12 14:21:46 +08:00
b4305192c4 feat: nodata block and right ui 2026-06-12 14:15:06 +08:00
54d08be1ea feat: add cli tools and skills dele evn 2026-06-12 14:15:06 +08:00
yyh
b1c5c3d73b fix(agent-v2): sync workflow roster nodes 2026-06-12 14:07:16 +08:00
yyh
bef5cea422 fix: migration 2026-06-12 13:22:14 +08:00
yyh
8d3f772cd6 fix(agent-v2): align roster card visuals 2026-06-12 13:21:11 +08:00
yyh
d8eae55b21 feat(agent-v2): add roster workflow reference menu 2026-06-12 13:18:31 +08:00
795e95726c fix: update onboarding nav copy 2026-06-11 22:10:40 -07:00
yyh
7ca985c7b3 feat(agent-v2): align roster create dialog 2026-06-12 13:09:24 +08:00
yyh
ed0dbb8c02 fix(agent-v2): enable roster filter switching 2026-06-12 13:04:26 +08:00
yyh
2fa705b5a6 fix(agent-v2): derive roster card usage status 2026-06-12 12:58:35 +08:00
yyh
fb9cc6f4af feat(agent-v2): align roster binding and role 2026-06-12 12:55:07 +08:00
yyh
5564c97408 chore: gen api contract 2026-06-12 12:38:16 +08:00
yyh
fcb38cf717 Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-06-12 12:38:04 +08:00
yyh
c3c18ef579 feat(agent-v2): align tool add dropdown 2026-06-12 12:36:15 +08:00
yyh
03ab30542b fix: make agent roster drawer nonmodal 2026-06-12 11:28:48 +08:00
yyh
1b518a05b7 fix: align agent roster drawer accessibility 2026-06-12 11:14:40 +08:00
77c9ee308b feat: file uploads and chat features 2026-06-12 10:52:37 +08:00
ed5ad808b6 chore: upload .skill use fold not folder 2026-06-12 10:52:37 +08:00
yyh
c537aa393d Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-12 10:50:03 +08:00
yyh
b8b51c7123 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-12 10:48:32 +08:00
yyh
7780c91e81 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-12 10:36:23 +08:00
yyh
e87f714792 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-12 10:34:26 +08:00
yyh
02a4e53e81 fix: workspace card style 2026-06-12 10:06:10 +08:00
4cf25f73c4 fix: stabilize app detail navigation 2026-06-11 17:52:24 -07:00
0b1b3af7b1 fix: preserve common error layout height 2026-06-11 17:51:41 -07:00
yyh
4669ef1060 feat: add agent composer jotai provider guidance 2026-06-11 20:18:19 +08:00
yyh
e749e3f129 refactor(agent-v2): clarify composer draft baseline 2026-06-11 20:11:17 +08:00
yyh
c19df018e8 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-11 19:56:51 +08:00
yyh
da05973852 chore: merge origin/main into feat/ui-onboarding-rewrite 2026-06-11 19:56:20 +08:00
yyh
d590246d6b chore: merge origin/main into feat/agent-v2 2026-06-11 19:53:33 +08:00
yyh
6c698b3969 docs(workflow): document agent node boundary 2026-06-11 19:48:57 +08:00
yyh
7fff3d914d fix(workflow): restore legacy agent run log 2026-06-11 19:47:23 +08:00
yyh
9a0ca09a6c fix(workflow): restore agent v2 node draft data 2026-06-11 19:41:40 +08:00
yyh
404dacae8a fix(workflow): isolate legacy agent node from roster 2026-06-11 19:38:24 +08:00
yyh
487cad9a57 fix(workflow): restore legacy agent node 2026-06-11 19:30:35 +08:00
6c30fafc4f feat: use atom store to handle data 2026-06-11 18:46:43 +08:00
yyh
2471fcd3d1 feat: publish bar design 2026-06-11 18:25:52 +08:00
1fc8185673 chore: refactor fold sturct 2026-06-11 18:06:20 +08:00
yyh
e4118eee84 fix: use kbd group 2026-06-11 18:01:07 +08:00
yyh
eda00f0a19 fix: split log mock data 2026-06-11 17:57:38 +08:00
88f54bfbd4 chore: refactor fold sturct 2026-06-11 17:57:15 +08:00
yyh
f6277a08c0 feat: agents logs page 2026-06-11 17:41:19 +08:00
yyh
2e6cab3b65 fix(agent-v2): align roster sidebar nav 2026-06-11 17:41:19 +08:00
c386bd3f91 chroe: enchance files sturct 2026-06-11 17:39:32 +08:00
2858cbaf4e chore: others config to atoms 2026-06-11 17:33:02 +08:00
2a1d3311c6 chore: sync data from atoms 2026-06-11 17:23:22 +08:00
8e508ba903 chore: configs to atoms and split skills add logic 2026-06-11 17:21:09 +08:00
yyh
fae725017e fix: configure section design 2026-06-11 17:13:39 +08:00
yyh
34010ebed8 Revert "fix(agent-v2): align env editor scope selector"
This reverts commit ea96663246.
2026-06-11 17:05:18 +08:00
yyh
ea96663246 fix(agent-v2): align env editor scope selector 2026-06-11 17:04:30 +08:00
yyh
0ab52daed3 fix: model selector field 2026-06-11 17:01:04 +08:00
yyh
757d83c17f fix: align design 2026-06-11 16:56:22 +08:00
yyh
a44d85d473 fix: align text 2026-06-11 16:53:06 +08:00
yyh
6a50e11461 feat: agent file tree 2026-06-11 16:46:55 +08:00
yyh
fdffff8c91 mock data 2026-06-11 16:30:42 +08:00
yyh
4c47c186b8 fix: click file open dialog 2026-06-11 16:25:41 +08:00
964377a935 chore: fix prompt splash problem icon problem and support upload skills 2026-06-11 16:22:58 +08:00
yyh
cafa12d875 feat: scroll fade 2026-06-11 16:12:52 +08:00
yyh
5f078668bd fix: file tree scroll 2026-06-11 16:04:20 +08:00
yyh
d76d5b87cb feat: agent skills details dialog 2026-06-11 15:45:22 +08:00
yyh
1f23aed10b style 2026-06-11 15:43:41 +08:00
dda9dacad3 feat: render the block in prompt 2026-06-11 15:40:31 +08:00
022b0b77bc feat: prompt can choose inner content 2026-06-11 15:40:25 +08:00
yyh
e6e7770cd3 style 2026-06-11 15:08:43 +08:00
yyh
bf1b3de6bd fix: scroll area 2026-06-11 15:07:06 +08:00
yyh
8d8755652d refactor(agent-v2): reuse collapsible for configure sections 2026-06-11 14:56:25 +08:00
yyh
19aa23611b feat: workflow reference icon and todo 2026-06-11 14:41:17 +08:00
yyh
7fe46e0628 fix: test 2026-06-11 14:06:50 +08:00
yyh
1a1115df43 fix: invalidate list when mutation 2026-06-11 14:06:05 +08:00
yyh
cc76ad8f89 Revert "fix: keep agent draft visible before composer binding"
This reverts commit e8140c7ada.
2026-06-11 11:37:26 +08:00
yyh
e8140c7ada fix: keep agent draft visible before composer binding 2026-06-11 11:35:30 +08:00
yyh
1b8753f04d fix: show workflow agent node from composer state 2026-06-11 11:22:14 +08:00
yyh
dd92acf6da fix: align agent output variables with v2 defaults 2026-06-11 11:18:09 +08:00
yyh
e7522ee3ff fix: align workflow agent panel with composer state 2026-06-11 11:14:49 +08:00
yyh
5bab70d030 feat: integrate agent v2 node default value in workflow 2026-06-11 11:00:11 +08:00
yyh
6612cc5efe fix: use infotip 2026-06-11 10:44:38 +08:00
yyh
9ed97c9c61 fix: localstorage hook 2026-06-11 10:34:46 +08:00
yyh
a42c803445 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-11 10:22:08 +08:00
yyh
2f0e302276 Merge branch 'feat/ui-onboarding-rewrite' of https://github.com/langgenius/dify into feat/ui-onboarding-rewrite 2026-06-11 10:21:16 +08:00
yyh
43bffeca39 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-11 10:21:10 +08:00
7f348aa7c6 fix(web): remove studio and knowledge page footers 2026-06-10 19:10:56 -07:00
a7fe7536f3 fix(web): align snippets studio header layout 2026-06-10 18:44:20 -07:00
c147159c43 fix(web): route marketplace category links to platform paths 2026-06-10 17:12:25 -07:00
4fcf969e31 fix(web): show pointer for plugin task status 2026-06-10 16:00:44 -07:00
a82afc3e5a fix(web): stabilize integrations settings modal 2026-06-10 16:00:18 -07:00
yyh
c23ae4bd3b Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-10 18:58:37 +08:00
yyh
87078de5c5 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-10 18:58:30 +08:00
yyh
3ed34298de Merge remote-tracking branch 'origin/main' into feat/agent-v2
# Conflicts:
#	web/app/components/app-sidebar/nav-link/index.tsx
#	web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
2026-06-10 18:57:05 +08:00
yyh
5774137b98 refactor(agent-v2): align roster tabs with dify-ui styles 2026-06-10 18:53:00 +08:00
yyh
5849f25fb4 fix(web): handle null sidebar segment 2026-06-10 18:48:19 +08:00
yyh
387f11fcb0 Merge branch 'feat/agent-v2' of https://github.com/langgenius/dify into feat/agent-v2 2026-06-10 18:47:51 +08:00
yyh
a054c30d22 Merge branch 'codex/dify-ui-tabs-tab-header' into feat/agent-v2 2026-06-10 18:47:45 +08:00
79485323cf feat: chat and env optsions 2026-06-10 18:46:31 +08:00
yyh
063f4a40c5 Merge branch 'feat/agent-v2' of https://github.com/langgenius/dify into feat/agent-v2 2026-06-10 18:43:19 +08:00
yyh
3ba8944d32 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-10 18:43:08 +08:00
yyh
a064bc6439 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-10 18:42:56 +08:00
22cd164d42 feat: advanced setting 2026-06-10 18:29:47 +08:00
625a459144 chroe: fiels 2026-06-10 18:21:55 +08:00
432e161c65 feat: verions to split panel and retrival ui 2026-06-10 18:21:21 +08:00
yyh
b6014441eb fix(web): keep tabs list width content-sized by default 2026-06-10 18:18:45 +08:00
yyh
0aeff4d058 lint 2026-06-10 18:15:09 +08:00
6e9270210e feat: tools item config and versions 2026-06-10 18:14:24 +08:00
yyh
6df51751aa refactor(web): compose tab header with dify-ui tabs 2026-06-10 18:13:21 +08:00
c1b3fa3aed feat: tools toggle 2026-06-10 18:01:30 +08:00
yyh
860dc57620 fix(agent-v2): preserve roster tab styling 2026-06-10 17:56:43 +08:00
91f551d660 feat: tools and preview header 2026-06-10 17:47:42 +08:00
yyh
b03549ef6a fix: align agent workflow panel labels 2026-06-10 17:20:54 +08:00
yyh
7fd9df1cd5 feat: agent node advanced settings part 2026-06-10 17:10:37 +08:00
yyh
01e6457cb2 Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-06-10 17:08:16 +08:00
yyh
700fffc216 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-10 16:50:29 +08:00
yyh
6f27c8ecb9 Merge branch 'feat/collapsible' into feat/ui-onboarding-rewrite 2026-06-10 16:49:38 +08:00
yyh
54d3aa8ecb Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-10 16:49:34 +08:00
yyh
eff00899f7 fix(web): satisfy collapse action lint 2026-06-10 16:44:52 +08:00
c486042ed4 [autofix.ci] apply automated fixes 2026-06-10 08:43:34 +00:00
yyh
dd0e6f75fa fix: use collapsible 2026-06-10 16:35:52 +08:00
yyh
3a20cf46af feat: dify ui collapsible 2026-06-10 16:34:39 +08:00
c15583bcb2 chore: css problems 2026-06-10 15:56:27 +08:00
35b63f466a chore: files 2026-06-10 15:56:27 +08:00
5b1c90b28e feat: skills s and prompt insert icon 2026-06-10 15:56:27 +08:00
yyh
5174ebff77 feat: agent node agent task field 2026-06-10 15:36:54 +08:00
yyh
6c364a573a Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-10 15:06:00 +08:00
yyh
1c63822a08 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-10 15:05:47 +08:00
yyh
af4766b9d9 style: skeleton 2026-06-10 14:44:22 +08:00
yyh
595f341c56 feat(agent-v2): add layered roster panel shell 2026-06-10 14:44:01 +08:00
yyh
88b2b56e84 refactor(workflow): strengthen single run form boundary 2026-06-10 13:54:50 +08:00
yyh
b598949a2c Merge branch 'feat/agent-v2' of https://github.com/langgenius/dify into feat/agent-v2 2026-06-10 13:49:32 +08:00
yyh
45cf171ef4 refactor(workflow): remove agent strategy node UI 2026-06-10 13:47:57 +08:00
6e9613f5b7 feat: agent left header 2026-06-10 13:46:35 +08:00
yyh
ca2a3ac141 feat(agent-v2): show roster agent in node panel 2026-06-10 13:19:09 +08:00
yyh
96124f6c9f fix: new agent node icon 2026-06-10 13:05:11 +08:00
yyh
0f1ad29efb fix(agent-v2): close agent selector with escape 2026-06-10 12:18:43 +08:00
yyh
c2533a1833 feat: agent selector 2026-06-10 11:34:09 +08:00
e17dc405c1 chore: agent to new url 2026-06-10 11:25:33 +08:00
yyh
524682873e chore: update agents.md 2026-06-10 11:16:14 +08:00
d626048fd6 chore: agent detail struct 2026-06-10 11:11:26 +08:00
yyh
6eb97610f0 feat(agent-v2): align roster page with design 2026-06-10 11:03:27 +08:00
yyh
71f94c1d15 feat(agent-v2): align roster nav icons 2026-06-10 10:06:49 +08:00
yyh
329d744942 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-10 09:48:18 +08:00
yyh
ab956f7870 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-10 09:48:11 +08:00
yyh
1d2b603383 feat(dify-ui): file tree (#37235) 2026-06-10 09:48:08 +08:00
a2e2c840c3 fix: run ci properly on pr (#37233) 2026-06-10 09:48:08 +08:00
5446b89054 chore: [Refactor/Chore] if isinstance to match case #35902 (#37087)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-10 09:48:08 +08:00
0fe6e910f8 refactor(web): mark Props of workflow/variable-inspect components as read-only (#25219) (#37230) 2026-06-10 09:48:08 +08:00
c6a0ce5f4d chore(api): Suppress unknown contract checks by default (#36969)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-10 09:48:08 +08:00
140c2cc3e0 test(cli-e2e): full E2E test suite for difyctl — auth / run / discovery / framework / output / error-handling / agent (#36874)
Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai>
2026-06-10 09:48:08 +08:00
eb2521082c feat(api,cli): strict UUID validation for app-id and workspace-id (#37212)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-10 09:48:08 +08:00
deda62dd94 chore(deps): bump starlette from 1.0.0 to 1.0.1 in /api (#37076)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 09:48:08 +08:00
543b9a454e chore(deps): bump starlette from 1.0.0 to 1.0.1 in /dify-agent (#37077)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 09:48:08 +08:00
b71010beb3 chore(deps): bump the storage group across 1 directory with 5 updates (#37153)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 09:48:08 +08:00
16b4f36635 fix(web): align snippets with studio navigation 2026-06-09 15:53:42 -07:00
63a865220c chore: all copywriting 2026-06-09 18:31:30 +08:00
1967624b82 chore: text change 2026-06-09 18:11:07 +08:00
cbb8bd29cf fix: app icon 2026-06-09 16:54:26 +08:00
a7d8386583 chore: padding and setting overflow problem 2026-06-09 14:51:15 +08:00
47d790d4eb chore: starred changed to show all no scroll 2026-06-09 14:00:17 +08:00
yyh
60110a57e8 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-09 13:46:49 +08:00
yyh
4e53eb5aaf Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-09 13:46:41 +08:00
yyh
6cc5514f64 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-09 13:25:23 +08:00
44fd4b1b2f chore: DI current_user && use inspect (#37084)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-09 13:25:12 +08:00
4743cf4e74 Revert "fix install_app n+1 query"
This reverts commit dcd40b5004.
2026-06-09 13:19:23 +08:00
yyh
c74aece481 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-09 13:08:08 +08:00
yyh
9b2d06182b Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-09 13:07:54 +08:00
yyh
560e15a836 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-09 11:58:48 +08:00
yyh
8fe9be11e9 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-09 11:58:38 +08:00
yyh
8d59634903 feat(agent-v2): connect roster detail fields 2026-06-09 11:58:10 +08:00
16f85ef709 feat: add app star 2026-06-09 11:16:35 +08:00
yyh
35f53e9fbf Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-09 11:15:20 +08:00
83db975374 feat: app star contract 2026-06-08 18:32:33 +08:00
18efc8ed88 chore: menu pos and menu action 2026-06-08 18:08:19 +08:00
46e1a5a6db chore: publish pipeline template copywriting and icon 2026-06-08 18:02:00 +08:00
a2267276ae chore: knowledge toggle icon 2026-06-08 18:01:59 +08:00
yyh
5619062ad1 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2
# Conflicts:
#	web/app/components/main-nav/__tests__/index.spec.tsx
2026-06-08 17:45:12 +08:00
yyh
38af34a742 fix: keep global nav account expanded 2026-06-08 17:44:02 +08:00
yyh
1bf2748130 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2
# Conflicts:
#	web/app/components/main-nav/__tests__/index.spec.tsx
#	web/app/components/main-nav/index.tsx
2026-06-08 17:40:18 +08:00
a6084ca3c7 chore: chunking Setting move place 2026-06-08 17:27:42 +08:00
d8b847dcf0 chore: i18n and split in menu 2026-06-08 17:15:03 +08:00
yyh
2509682e07 fix: align secondary sidebar help icon 2026-06-08 17:10:26 +08:00
5582b35d56 chroe: copywriting 2026-06-08 17:04:28 +08:00
db83df9f9c chore: knowledge icon 2026-06-08 17:04:28 +08:00
0683c0e7a7 chore: sidbar hover toggle and no data icon 2026-06-08 17:04:28 +08:00
42b7bf8152 feat: gen app starred openAPI 2026-06-08 16:53:35 +08:00
bc33ef1b97 feat: app star and its list 2026-06-08 16:47:26 +08:00
0f493a52a1 fix: annotation page highlight 2026-06-08 16:35:17 +08:00
yyh
fc7cd1100e Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-08 16:32:24 +08:00
yyh
fc4f9db79a fix: creators filter styles 2026-06-08 16:31:10 +08:00
yyh
e9b8a1606e fix: remove unnecessary title prop 2026-06-08 16:28:31 +08:00
453c4c4c5f chore: update annotations 2026-06-08 16:20:44 +08:00
58e25d0534 chore: toogle icon 2026-06-08 16:20:44 +08:00
yyh
d7b9f2a86b Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-08 16:08:22 +08:00
18d2423ed1 feat: apps support sort by 2026-06-08 15:55:57 +08:00
yyh
4176e7d146 fix: font style 2026-06-08 15:43:01 +08:00
3556611c2b chore: header filter 2026-06-08 15:36:15 +08:00
yyh
e37b4af0e8 feat(web): autofocus main nav search inputs 2026-06-08 15:34:43 +08:00
yyh
a0eb2ba0ff feat(web): support search input autofocus 2026-06-08 15:34:33 +08:00
yyh
75f1094459 fix(web): use native button for explore app cards 2026-06-08 15:14:33 +08:00
yyh
f61f9634f8 fix(web): use radio semantics for explore categories 2026-06-08 15:07:21 +08:00
yyh
56c569d6af Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-08 14:51:22 +08:00
yyh
11f78289d3 fix: highlight integration popover triggers 2026-06-08 14:50:18 +08:00
yyh
85e600e579 Merge branch 'feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-08 14:37:21 +08:00
yyh
062341ab26 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	eslint-suppressions.json
#	web/i18n/ar-TN/plugin.json
#	web/i18n/de-DE/plugin.json
#	web/i18n/es-ES/plugin.json
#	web/i18n/es-ES/workflow.json
#	web/i18n/fa-IR/plugin.json
#	web/i18n/fa-IR/workflow.json
#	web/i18n/fr-FR/plugin.json
#	web/i18n/fr-FR/workflow.json
#	web/i18n/hi-IN/plugin.json
#	web/i18n/hi-IN/workflow.json
#	web/i18n/id-ID/plugin.json
#	web/i18n/id-ID/workflow.json
#	web/i18n/it-IT/plugin.json
#	web/i18n/it-IT/workflow.json
#	web/i18n/ja-JP/plugin.json
#	web/i18n/ja-JP/workflow.json
#	web/i18n/ko-KR/plugin.json
#	web/i18n/ko-KR/workflow.json
#	web/i18n/nl-NL/app.json
#	web/i18n/nl-NL/plugin.json
#	web/i18n/nl-NL/workflow.json
#	web/i18n/pl-PL/plugin.json
#	web/i18n/pl-PL/workflow.json
#	web/i18n/pt-BR/plugin.json
#	web/i18n/ro-RO/plugin.json
#	web/i18n/ro-RO/workflow.json
#	web/i18n/ru-RU/plugin.json
#	web/i18n/ru-RU/workflow.json
#	web/i18n/sl-SI/plugin.json
#	web/i18n/sl-SI/workflow.json
#	web/i18n/th-TH/plugin.json
#	web/i18n/th-TH/workflow.json
#	web/i18n/tr-TR/plugin.json
#	web/i18n/tr-TR/workflow.json
#	web/i18n/uk-UA/plugin.json
#	web/i18n/uk-UA/workflow.json
#	web/i18n/vi-VN/plugin.json
#	web/i18n/vi-VN/workflow.json
#	web/i18n/zh-Hant/plugin.json
#	web/i18n/zh-Hant/workflow.json
2026-06-08 14:37:00 +08:00
yyh
c56e9813bb Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-08 14:22:34 +08:00
yyh
6ea9ba5926 fix: question icon style 2026-06-08 14:20:29 +08:00
yyh
7a9054fdea fix: workspace filter style 2026-06-08 13:30:00 +08:00
yyh
1820e6eab8 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-08 13:17:06 +08:00
yyh
508baa782c Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-08 13:14:55 +08:00
yyh
f5ee121d2c fix: workspace card components 2026-06-08 13:02:26 +08:00
yyh
2bf60a67ad fix: style 2026-06-08 12:36:17 +08:00
f6433097ad feat: app list add sort param 2026-06-08 11:26:18 +08:00
yyh
23b1038b99 fix: migrations 2026-06-08 11:21:56 +08:00
yyh
0dd2d36d68 fix: align api 2026-06-08 11:00:41 +08:00
yyh
786c7190f0 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-08 10:42:02 +08:00
yyh
ed10b82bb1 fix: export 2026-06-08 10:40:36 +08:00
yyh
ba19aee2bf Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-08 10:35:07 +08:00
yyh
41106ef6c9 fix: lint and type-check 2026-06-08 10:34:16 +08:00
yyh
5ee7bedb56 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	.agents/skills/frontend-code-review/SKILL.md
#	.agents/skills/frontend-code-review/references/business-logic.md
#	.agents/skills/frontend-code-review/references/code-quality.md
#	.agents/skills/frontend-code-review/references/performance.md
#	api/controllers/console/explore/recommended_app.py
#	eslint-suppressions.json
#	packages/contracts/generated/api/console/workspaces/orpc.gen.ts
#	packages/iconify-collections/custom-vender/info.json
#	web/__tests__/apps/app-list-browsing-flow.test.tsx
#	web/app/(commonLayout)/role-route-guard.tsx
#	web/app/components/app-sidebar/nav-link/index.tsx
#	web/app/components/apps/__tests__/empty.spec.tsx
#	web/app/components/apps/__tests__/list.spec.tsx
#	web/app/components/apps/empty.tsx
#	web/app/components/apps/list.tsx
#	web/app/components/base/search-input/__tests__/index.spec.tsx
#	web/app/components/base/search-input/index.tsx
#	web/app/components/header/account-setting/index.tsx
#	web/app/components/header/header-wrapper.tsx
#	web/app/components/plugins/marketplace/search-box/index.tsx
#	web/app/components/tools/mcp/modal.tsx
#	web/features/tag-management/components/tag-filter.tsx
#	web/i18n/en-US/workflow.json
#	web/i18n/zh-Hans/workflow.json
2026-06-08 10:32:32 +08:00
f0bcb77d55 fix(web): update preferences active icon 2026-06-07 00:12:29 -07:00
44a89bf870 fix(web): align main nav glass tokens 2026-06-07 00:11:58 -07:00
60ad023553 Revert "chore(web): remove explicit plugin type anys"
This reverts commit 9c6a7679ac.
2026-06-06 22:29:41 -07:00
9c6a7679ac chore(web): remove explicit plugin type anys 2026-06-06 22:06:10 -07:00
11b2ba29c1 fix(web): preserve marketplace install dialog during loading 2026-06-06 21:54:42 -07:00
c52df73117 fix(web): align integration loading skeletons with loaded cards 2026-06-06 21:54:42 -07:00
9b8d81c852 fix(web): keep plugin install tasks visible while pending 2026-06-06 21:54:42 -07:00
b28b2892f1 chroe: remove app create card 2026-06-06 10:42:13 +08:00
7f5349e707 chore: type selctc 2026-06-06 10:33:14 +08:00
0aaa1df1b8 chroe: tags and types copywriting 2026-06-06 10:19:55 +08:00
bd41c5c3c0 chore: tags filter and pipeline conrner 2026-06-06 10:04:22 +08:00
95a2eea611 chore: knowledge align and remove create 2026-06-06 09:38:08 +08:00
yyh
d959c73884 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-05 12:26:16 +08:00
yyh
19193891cf fix: remove redundant main nav link titles 2026-06-05 12:23:50 +08:00
4746dd39d3 chore: only cloud can show upgrade 2026-06-05 11:36:18 +08:00
a5e6f58285 fix(web): resolve lint issues 2026-06-04 20:19:05 -07:00
4bf0398873 fix(web): use workspace last opened time 2026-06-04 20:19:05 -07:00
e6db98ef64 feat: add task detail 2026-06-05 11:13:54 +08:00
6b694ce829 fix(web): polish tool provider details 2026-06-04 19:58:45 -07:00
0ab3b5d677 feat(web): improve plugins panel filtering 2026-06-04 19:58:45 -07:00
346fabda27 fix(web): redirect legacy integration routes 2026-06-04 19:58:45 -07:00
3da0f12815 fix(web): refine plugin task popover 2026-06-04 19:58:45 -07:00
303cff1353 feat(web): improve data source plugin actions 2026-06-04 19:58:45 -07:00
10c3849887 fix(web): improve model selector empty state 2026-06-04 19:58:45 -07:00
29295950ea refactor(web): update main nav account settings 2026-06-04 19:58:44 -07:00
yyh
c61a7ce442 fix: use next link for navigate button 2026-06-05 10:44:21 +08:00
yyh
92fdbd5c51 fix: css 2026-06-05 10:37:21 +08:00
yyh
09053ab760 fix: use controlled form validation 2026-06-05 10:28:32 +08:00
yyh
c1afdc030c Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-05 10:18:46 +08:00
yyh
ab383969a8 style: dropdown menu pd 2026-06-05 10:16:07 +08:00
yyh
8a0aab4d81 style: remove p-1 in dropdown popup 2026-06-05 10:10:47 +08:00
d389284813 feat: add last_opened_at for workspace API 2026-06-05 10:08:13 +08:00
yyh
530e366440 fix: use link for workspace card credits 2026-06-05 09:55:46 +08:00
yyh
e4510a7d8f Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-05 09:35:05 +08:00
yyh
5c2f4709a5 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-04 20:46:36 +08:00
yyh
19d7a9b5d9 fix: learn dify item use dropdown menu checkbox item and no switch 2026-06-04 20:42:33 +08:00
yyh
1d063e3fd6 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-04 20:38:39 +08:00
yyh
850c7e311f fix(web): align focus rings in nav surfaces 2026-06-04 20:33:19 +08:00
de0ccbb960 fix: question mark ui 2026-06-04 18:31:06 +08:00
yyh
0493552c73 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-04 18:05:33 +08:00
407d3e28bb fix: focus template focus ring not show the full 2026-06-04 17:24:28 +08:00
8a51b3a296 chroe: remove mock logic 2026-06-04 17:02:47 +08:00
yyh
8fc3882042 refactor: normalize agent v2 translation hooks 2026-06-04 16:56:25 +08:00
yyh
0ffb9667d4 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2
# Conflicts:
#	web/app/components/main-nav/__tests__/index.spec.tsx
#	web/app/components/main-nav/index.tsx
2026-06-04 16:54:53 +08:00
yyh
e7886c1bac fix: format agent roster updated time 2026-06-04 16:51:29 +08:00
41894ad182 chore: uis 2026-06-04 16:46:41 +08:00
95ea709c91 chroe: knowledge info more info 2026-06-04 16:46:41 +08:00
3a01b91a45 chore: sidebar ui 2026-06-04 16:46:41 +08:00
f50abac3f9 chore: knowledge info 2026-06-04 16:46:41 +08:00
7f44b2f601 chore: knowledge sidebar ui 2026-06-04 16:46:41 +08:00
yyh
9583f17960 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-04 16:43:34 +08:00
yyh
c71d2ac460 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-04 16:43:22 +08:00
yyh
505ad3994c fix(agent-v2): align configure queries and skeletons 2026-06-04 16:41:15 +08:00
yyh
87c6a82df0 fix(agent-v2): show detail title skeleton while loading 2026-06-04 16:35:55 +08:00
yyh
0d97d44222 feat(agent-v2): show memory config from contracts 2026-06-04 16:33:11 +08:00
yyh
64f1d125a6 fix(agent-v2): focus roster dialog name fields 2026-06-04 16:25:34 +08:00
yyh
090ef21881 feat(agent-v2): connect detail header to contracts 2026-06-04 16:23:44 +08:00
yyh
56ed953e2a feat(agent-v2): restore roster sidebar 2026-06-04 16:14:47 +08:00
yyh
f61f15371a Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-04 16:06:58 +08:00
yyh
1a7542052e style: create from template modal 2026-06-04 16:06:46 +08:00
yyh
009c6adc8f feat(agent-v2): connect roster to generated contracts 2026-06-04 16:05:50 +08:00
6d26f6ea73 chore: handle tempalte and learn dify click to new 2026-06-04 14:45:02 +08:00
yyh
35e21de9f8 style: create from template modal 2026-06-04 14:43:17 +08:00
yyh
8c925b1422 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-04 14:25:26 +08:00
b0d3347de9 chore: padding 2026-06-04 14:12:00 +08:00
bf2a35bff8 chore: slide ui 2026-06-04 13:56:35 +08:00
yyh
63d50a61d3 style: add pointer cursor to app filter checkbox 2026-06-04 13:14:01 +08:00
yyh
101a6fcc53 fix(web): update account header home action 2026-06-04 12:56:10 +08:00
d407c1fbf7 chore(integrations): update endpoint copy and header tests 2026-06-03 21:19:26 -07:00
fe6ee8aa04 style(plugins): align empty states and remove actions 2026-06-03 21:19:25 -07:00
25228e3cde fix(integrations): align tool create actions 2026-06-03 21:19:25 -07:00
8049da9331 fix(integrations): align custom endpoint toolbar with design 2026-06-03 21:19:25 -07:00
e404195c8c fix(integrations): align navigation and marketplace layout 2026-06-03 21:19:25 -07:00
b3c7110768 fix(plugins): show correct badges and endpoint metadata 2026-06-03 21:19:25 -07:00
7b2b21c348 feat(integrations): use plugin category APIs for integration pages 2026-06-03 21:19:24 -07:00
3d3a1f4f90 style(integrations): refine provider cards and tag filter 2026-06-03 21:19:24 -07:00
yyh
274944f05e Merge branch 'feat/ui-onboarding-rewrite' of https://github.com/langgenius/dify into feat/ui-onboarding-rewrite 2026-06-04 11:57:05 +08:00
yyh
a0d53b9f07 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	eslint-suppressions.json
#	web/app/components/header/account-dropdown/index.tsx
2026-06-04 11:56:43 +08:00
a24d1a0bbc feat: add PluginCategory openAPI 2026-06-04 11:52:35 +08:00
fffa89a10e chore: handle tag icon and jump action 2026-06-03 18:12:38 +08:00
422461f360 chore: handle card ui 2026-06-03 17:56:47 +08:00
2ce913aaac fix: slide scroll show other slide promble 2026-06-03 17:40:23 +08:00
30f0a69fea feat: add builtin tools 2026-06-03 17:29:41 +08:00
04e8c6127f Revert "feat: PluginInstallTaskStartResponse add optional PluginInstallTask"
This reverts commit ccc9122980.
2026-06-03 17:21:07 +08:00
20e37a0457 chore: add become a partnter link 2026-06-03 17:08:00 +08:00
36c7209301 chroe: description vertical padding 2026-06-03 16:54:24 +08:00
ccc9122980 feat: PluginInstallTaskStartResponse add optional PluginInstallTask 2026-06-03 16:51:22 +08:00
9143d44ec6 chore: new all plugin icon and request comp 2026-06-03 16:45:42 +08:00
1f856960f0 feat: PluginList by category 2026-06-03 16:35:33 +08:00
a8218d5809 chore: market header align 2026-06-03 16:25:24 +08:00
70ec17bc34 feat: support scroll to show more plugins in group 2026-06-03 16:15:19 +08:00
e4ea9d2e07 feat: marketplace page change; 2026-06-03 15:50:34 +08:00
yyh
fd342ccac0 fix: use atoms 2026-06-03 15:22:27 +08:00
yyh
929a1da26c Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-03 14:57:02 +08:00
yyh
b72bfe060d fix: learn dify no skeleton 2026-06-03 14:33:31 +08:00
yyh
dfc7f136ef Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-03 14:27:46 +08:00
yyh
15d66814ee fix: consolidate explore home initial loading 2026-06-03 14:02:12 +08:00
yyh
5f109213a5 fix: eslint 2026-06-03 13:05:19 +08:00
yyh
a53828e826 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	eslint-suppressions.json
#	web/app/components/app/create-app-dialog/app-list/index.tsx
#	web/app/components/app/create-app-modal/index.tsx
#	web/app/components/app/create-from-dsl-modal/index.tsx
#	web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
#	web/hooks/use-import-dsl.ts
2026-06-03 13:04:38 +08:00
3a2ea826ff fix(plugins): refresh model defaults after plugin changes 2026-06-02 15:54:52 -07:00
4581ce2f45 style(model-provider): align model selectors 2026-06-02 15:54:15 -07:00
3925b2bf4f fix(model-provider): handle unavailable credentials 2026-06-02 15:53:45 -07:00
5744148e6d style(model-provider): align quota panel 2026-06-02 15:53:18 -07:00
6ba42e6d73 refactor(model-provider): split provider page body 2026-06-02 15:52:42 -07:00
aff22cb5ed refactor integrations header layout 2026-06-02 11:19:28 -07:00
58c4a174ba align model provider toolbar copy 2026-06-02 11:19:27 -07:00
c98f65cbcb refactor integrations tool provider cards 2026-06-02 11:19:27 -07:00
add7c75f18 chore: stadio no app tiny 2026-06-02 17:53:12 +08:00
75e74ee8b9 chore: no knowledge some tiny 2026-06-02 17:51:10 +08:00
40a5236553 fix: learn dify no ssr to fix not same to server side if not on 2026-06-02 17:38:09 +08:00
f820813e9f chore: knowledge empty page 2026-06-02 17:25:15 +08:00
c4c3a2b265 chore: no apps align 2026-06-02 17:02:00 +08:00
yyh
99be8b34c8 fix: reverts agents.md to main 2026-06-02 16:56:19 +08:00
777265d898 chore: no apps page 2026-06-02 16:47:58 +08:00
7024913866 chore: siderbar tiny ui 2026-06-02 16:47:58 +08:00
yyh
3c862c3e98 fix: style 2026-06-02 16:46:04 +08:00
yyh
fb497c60dd refactor(web): connect goto anything triggers to atom state 2026-06-02 16:41:41 +08:00
yyh
075af9cd44 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	web/app/components/goto-anything/hooks/__tests__/use-goto-anything-modal.spec.ts
#	web/app/components/goto-anything/hooks/use-goto-anything-modal.ts
2026-06-02 16:35:10 +08:00
yyh
18e07cac9a fix(ui): use css selector 2026-06-02 16:01:58 +08:00
yyh
284c1027da Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-02 15:48:48 +08:00
yyh
ba06ed5f41 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-02 15:48:10 +08:00
3fd9d5eb14 chore: install border and nav top 2026-06-02 15:38:13 +08:00
yyh
a70e8eb2b5 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-02 15:37:50 +08:00
yyh
15d2714e9d fix lint 2026-06-02 15:34:19 +08:00
yyh
f36852646f Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-02 15:34:02 +08:00
72c3cd0d67 chore: installed app 2026-06-02 15:24:32 +08:00
fd0cb47a81 chore: spacing in home page 2026-06-02 14:52:37 +08:00
87382673f3 chore: templates ui 2026-06-02 14:47:38 +08:00
3c716a6eee chore: learn dify sketlon 2026-06-02 14:32:41 +08:00
8da34fb60b chore: update banner and learn dify ui 2026-06-02 14:24:18 +08:00
f0efb73fd0 chroe: recents ui and home padding x 2026-06-02 13:51:17 +08:00
e2bd2355e9 chroe: app sidebar and continue work 2026-06-02 13:46:54 +08:00
73b50f5ede chore: add splits in nav 2026-06-02 13:36:46 +08:00
52cfe62d8d fix: home page auto jump to /apps 2026-06-02 11:24:10 +08:00
f293253a7a feat: add logs and annotions title 2026-06-02 11:11:59 +08:00
1ac3ad4c81 chore: split logs and annotations page 2026-06-02 11:01:38 +08:00
2d6fd70733 feat: align integrations auto-update settings dialog 2026-06-01 16:00:52 -07:00
639c8d5967 chore: update frontend review component guidance 2026-06-01 16:00:15 -07:00
74c9b7fddd fix(web): align integrations sidebar actions 2026-06-01 13:07:31 -07:00
yyh
be997384f3 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-01 19:40:06 +08:00
yyh
acbea6701c Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-01 18:33:10 +08:00
yyh
86ac4dadd6 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-01 16:29:36 +08:00
yyh
f3e11ec0ee Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-01 16:21:24 +08:00
yyh
c9c4fca7a7 chore: merge feat/local-storage-react-hook 2026-06-01 16:05:14 +08:00
yyh
d23eddc924 tweaks 2026-06-01 15:49:12 +08:00
yyh
d65c7229f2 refactor: replace useEffect with useIsomorphicLayoutEffect for better SSR compatibility 2026-06-01 15:39:10 +08:00
yyh
66508326f9 test: update workflow store persistence expectations 2026-06-01 15:30:39 +08:00
yyh
310f49229e Merge remote-tracking branch 'origin/main' into feat/local-storage-react-hook 2026-06-01 15:28:05 +08:00
yyh
f400be4280 fix knip 2026-06-01 15:28:02 +08:00
yyh
2855ab3a15 refactor(web): migrate local storage util to hook 2026-06-01 15:23:18 +08:00
yyh
e2fd5421d2 prune 2026-06-01 15:21:45 +08:00
yyh
ff37ba83b4 refactor(web): bridge workflow storage through react 2026-06-01 15:14:45 +08:00
yyh
7b97ec57ef feat(web): add use local storage hook 2026-06-01 14:45:39 +08:00
yyh
791296cc8d Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-06-01 13:10:49 +08:00
yyh
19d34b5a93 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-01 13:10:27 +08:00
yyh
5c315ea7fe Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	web/app/components/tools/mcp/index.tsx
2026-06-01 12:29:40 +08:00
e525880773 feat(integrations): reorganize sidebar navigation 2026-05-31 20:06:06 -07:00
d82e30561f feat(plugin): refine install task status controls 2026-05-31 20:06:06 -07:00
yyh
d2508db11f Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-31 22:17:12 +08:00
yyh
7f76fe68ea fix: style 2026-05-31 22:14:14 +08:00
yyh
08bbd3bfdf Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-05-31 22:02:20 +08:00
yyh
ac09702d08 Merge branch 'codex/server-prefetch-current-workspace' into feat/ui-onboarding-rewrite 2026-05-31 22:00:25 +08:00
yyh
b14d1e60ec fix: save local storage 2026-05-31 21:38:54 +08:00
yyh
b072136ca0 fix: add console.error for error bounadries 2026-05-31 21:38:23 +08:00
yyh
c8b6ec5fb0 fix: prefetch workspace info in server 2026-05-31 21:38:10 +08:00
yyh
0b5b4271f0 fix: add console.error() in common layout error boundary 2026-05-30 23:22:33 +08:00
yyh
fd60339625 fix: safe local storage 2026-05-30 23:16:48 +08:00
yyh
f56f93f5c2 fix(web): defer try app preview loading 2026-05-30 23:14:26 +08:00
yyh
2921d27929 fix: use query for workspace info 2026-05-30 21:04:57 +08:00
yyh
98e3bff509 fix: skeleton 2026-05-30 20:52:23 +08:00
yyh
6c43fa459e fix(auth): reset profile query after login 2026-05-30 20:30:32 +08:00
yyh
d2eae74b5e fix(web): make full screen loading fill viewport 2026-05-30 19:50:43 +08:00
yyh
87a3980c76 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-05-30 14:00:06 +08:00
yyh
27bdee85fe fix(auth): avoid leaking request origin in refresh redirects 2026-05-30 13:59:53 +08:00
cca8295ad8 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-29 11:27:07 -07:00
yyh
f8cc85ce28 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-05-29 22:17:43 +08:00
yyh
4d1b3605b5 style: use kbd and align style 2026-05-29 22:16:51 +08:00
yyh
d94be05f68 fix(web): use default profile query cache 2026-05-29 22:00:00 +08:00
yyh
fea7590779 fix: remove cursor pointer 2026-05-29 21:15:52 +08:00
yyh
71ba903d4c fix: banner and template card skeleton style 2026-05-29 21:15:03 +08:00
yyh
44831839d1 fix(web): center common layout error in content area 2026-05-29 21:12:49 +08:00
yyh
88c3512471 fix: add webapp header skeleton 2026-05-29 21:06:02 +08:00
yyh
67dee6e07e fix: env badge position 2026-05-29 21:02:37 +08:00
yyh
95d7fa997b fix: hide webapp section when length===0 2026-05-29 20:59:59 +08:00
yyh
2d4e494162 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-05-29 20:39:28 +08:00
yyh
4a0b177eee Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx
#	web/app/(commonLayout)/layout.tsx
#	web/app/components/full-screen-loading.tsx
#	web/app/components/header/header-wrapper.tsx
2026-05-29 20:39:10 +08:00
yyh
15b2a8fdb7 Merge remote-tracking branch 'origin/refactor/suspense-boundary' into feat/ui-onboarding-rewrite 2026-05-29 20:23:54 +08:00
yyh
65098a6b4f fix: e2e 2026-05-29 20:18:16 +08:00
yyh
8055f8840c fix: remove global loading 2026-05-29 19:39:03 +08:00
yyh
f033f91a68 Merge remote-tracking branch 'origin/main' into refactor/suspense-boundary 2026-05-29 18:54:09 +08:00
yyh
0b98319bd3 fix: redirect profile 401 to refresh route 2026-05-29 18:53:56 +08:00
yyh
386de25e26 add notes 2026-05-29 18:52:36 +08:00
yyh
d30805353a update lock 2026-05-29 17:09:39 +08:00
yyh
21c5825508 fix(web): declare server-only dependency 2026-05-29 17:05:38 +08:00
yyh
2d324add39 fix(web): guard server profile prefetch URL 2026-05-29 16:50:29 +08:00
yyh
62beaf493e fix: knip 2026-05-29 16:46:23 +08:00
yyh
b34e5aa915 Merge branch 'main' into refactor/suspense-boundary 2026-05-29 16:41:06 +08:00
yyh
2ea19c2b1c test: fix oauth registration analytics spec 2026-05-29 16:29:47 +08:00
yyh
5712f29e8b Merge remote-tracking branch 'origin/main' into refactor/suspense-boundary 2026-05-29 16:19:58 +08:00
yyh
93f7404c6b refactor: hydrate common layout profile query 2026-05-29 16:19:26 +08:00
yyh
4f631d6f4c fix 2026-05-29 16:06:52 +08:00
yyh
2b3e15cc83 Remove app initializer and move auth boot logic to route boundaries 2026-05-29 15:12:02 +08:00
yyh
5e1fac09bb Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2
# Conflicts:
#	web/app/components/main-nav/index.tsx
2026-05-29 11:54:10 +08:00
yyh
35956247ab Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-29 11:51:24 +08:00
yyh
343531b9dc refactor(web): migrate learn dify visibility state to jotai 2026-05-29 10:50:49 +08:00
yyh
236f389fce Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-29 10:24:10 +08:00
5a2604265c fix: align integrations search empty states 2026-05-28 12:43:58 -07:00
6a8aaa5a36 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-28 11:20:10 -07:00
yyh
0ad1e8c2d9 fix: improve Dify logo accessibility 2026-05-28 16:48:22 +08:00
yyh
8c7540f698 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2
# Conflicts:
#	web/app/components/main-nav/index.tsx
2026-05-28 16:44:17 +08:00
yyh
bf345136eb fix: style 2026-05-28 16:40:34 +08:00
yyh
c9ed50c3ae fix 2026-05-28 16:36:32 +08:00
yyh
c78c603a38 fix: remove workflow fullscreen 2026-05-28 16:02:18 +08:00
yyh
48b38446a3 fix: improve frontend accessibility selectors 2026-05-28 15:30:37 +08:00
yyh
8a8bec4bc6 refactor: align integrations layout 2026-05-28 15:11:27 +08:00
yyh
89571bd241 fix: type 2026-05-28 14:19:43 +08:00
yyh
afee58cca7 refactor: move to agent detail 2026-05-28 14:18:43 +08:00
yyh
76a55535f2 fix: style 2026-05-28 14:09:58 +08:00
29cb993042 feat: add memory and enchance version list 2026-05-28 14:07:00 +08:00
yyh
00581a4daa feat(agent-v2): align monitoring charts 2026-05-28 13:56:44 +08:00
yyh
bfc71bb087 refactor(agent-v2): organize vertical modules 2026-05-28 13:42:04 +08:00
yyh
a95a6ea263 feat(agent-v2): add access sharing page 2026-05-28 13:36:42 +08:00
yyh
264e97a4c2 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-05-28 12:52:04 +08:00
yyh
95936a8bac refactor(web): remove workspace context 2026-05-28 12:48:16 +08:00
yyh
ac8a1107ca fix: stacking context 2026-05-28 11:20:26 +08:00
43f67ef2d1 feat: publish and version 2026-05-28 11:16:15 +08:00
yyh
c118fe9ad2 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-05-28 10:19:07 +08:00
0c96426d91 fix: link app sidebar studio breadcrumb 2026-05-27 18:47:52 -07:00
67fee14770 feat: migrate installed app route 2026-05-27 17:38:33 -07:00
d94006162d refactor: split large onboarding UI components 2026-05-27 17:04:42 -07:00
3d53cee8a9 fix: render home loading skeletons 2026-05-27 16:46:51 -07:00
1acd1b568a fix: add integration loading placeholders 2026-05-27 16:46:23 -07:00
68f939f3b3 fix(web): narrow home active state 2026-05-27 14:52:25 -07:00
1f4b76ba7e fix(web): scope common layout loading and marketplace title 2026-05-27 14:51:57 -07:00
4d974d8f72 fix(web): use scroll areas for integrations lists 2026-05-27 14:51:31 -07:00
1dc12d1661 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-27 12:08:53 -07:00
yyh
82345977cd fix: separate main nav web app groups 2026-05-27 20:59:59 +08:00
yyh
83c943bc21 fix: virtualize main nav web apps 2026-05-27 20:35:12 +08:00
yyh
7e34e2347a Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-05-27 20:15:47 +08:00
yyh
94a376a5a7 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-27 20:11:47 +08:00
33f6b0c9aa feat: agent version 2026-05-27 17:50:09 +08:00
yyh
2b130d0d2a Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-05-27 16:44:05 +08:00
yyh
33d95ab23a Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-27 16:40:30 +08:00
yyh
7a8a92082b refactor: use segmented control 2026-05-27 16:40:20 +08:00
4f9adfb9ae chore: publish 2026-05-27 16:39:10 +08:00
yyh
f3974d6176 Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-05-27 16:22:34 +08:00
yyh
ef00f850e4 fix: use chip for app type selector 2026-05-27 16:18:49 +08:00
yyh
cb2e404eb6 feat: agent log details 2026-05-27 16:02:01 +08:00
yyh
14e7fc87e4 fix(web): prevent agent nav active layout shift 2026-05-27 15:38:50 +08:00
yyh
40b4c3476d feat(web): add agent monitoring page 2026-05-27 15:37:23 +08:00
1c641d2b44 chore: remove roster annotation 2026-05-27 15:32:42 +08:00
yyh
c3c9a349cc Merge remote-tracking branch 'origin/feat/ui-onboarding-rewrite' into feat/agent-v2 2026-05-27 15:23:49 +08:00
yyh
169293c8da feat(web): add agent detail scaffold 2026-05-27 14:54:48 +08:00
7815228395 Merge branch 'feat/ui-onboarding-rewrite' of github.com:langgenius/dify into feat/ui-onboarding-rewrite 2026-05-27 14:45:28 +08:00
dcd40b5004 fix install_app n+1 query 2026-05-27 14:44:00 +08:00
yyh
bcc4b208c7 feat(web): add agent roster scaffold 2026-05-27 14:27:51 +08:00
yyh
c252006644 fix: align help menu support item 2026-05-27 14:20:34 +08:00
yyh
9e5668c233 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-27 13:31:01 +08:00
yyh
52ce49b3c6 fix(workspace-card): fix style and add skeleton 2026-05-27 13:18:03 +08:00
yyh
e90aa76ba2 fix: use dropdown menu 2026-05-27 13:02:43 +08:00
yyh
de9373e1b8 chore(contracts): type plugin auto-upgrade responses 2026-05-27 11:33:56 +08:00
yyh
58923f38e6 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-27 11:03:21 +08:00
yyh
8486a5b213 feat(api): add plugin auto-upgrade category contracts 2026-05-27 11:03:04 +08:00
yyh
28a8be0d5f feat(api): add learn dify app list contract 2026-05-27 11:02:45 +08:00
yyh
f2d4d5b267 fix lint 2026-05-27 10:34:36 +08:00
yyh
f62a59a18a fix: resolve app and dataset lint errors 2026-05-27 10:33:51 +08:00
yyh
b488812714 lint 2026-05-27 10:28:42 +08:00
755760b97c refactor(web): migrate mcp modal controls to dify ui primitives 2026-05-26 19:06:52 -07:00
955c3fb797 refactor(web): migrate search inputs to dify ui primitive 2026-05-26 19:06:52 -07:00
yyh
0c9aa20047 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-27 10:03:36 +08:00
065246a9a7 Merge origin/main into feat/ui-onboarding-rewrite 2026-05-26 17:50:55 -07:00
0d12b5ab1c test(web): cover integrations permission-gated actions 2026-05-21 16:44:33 -07:00
514dcae189 style(web): align update setting time picker token 2026-05-21 15:49:39 -07:00
228dd84a91 refactor(web): tighten tools provider card and marketplace handling 2026-05-21 15:49:27 -07:00
336ddad096 fix(web): expose current nav links to assistive tech 2026-05-21 15:49:13 -07:00
92bb9a17b7 fix(web): scope main nav fullscreen canvas routes 2026-05-21 15:49:02 -07:00
b8868dab90 fix(integrations): update install success action copy 2026-05-21 10:54:43 -07:00
94225682cd docs: update main nav and integrations follow-ups 2026-05-20 17:11:16 -07:00
18b6568c2a fix: refine integrations sidebar controls 2026-05-20 16:21:25 -07:00
a3a9ded29b chore: localize main nav and integrations copy 2026-05-20 15:50:52 -07:00
de78a26920 fix: scope dataset detail navigation routes 2026-05-20 15:50:20 -07:00
c54d029e7c fix: restore dataset list markup 2026-05-20 15:49:49 -07:00
ad4b9dc2c3 refactor: reuse toggle group in update settings 2026-05-20 12:42:53 -07:00
cdec0c69a6 chore: learn dify try action same to template 2026-05-20 18:04:43 +08:00
53acc3726c merge 2026-05-20 17:43:06 +08:00
b1d393f4d9 chore: hide select model provider in model provider page 2026-05-20 15:22:14 +08:00
62e9bdd70d chore: app permission show in app card 2026-05-20 15:00:18 +08:00
d36c76c20e merge 2026-05-20 14:34:04 +08:00
f525e1a5eb fix(web): align onboarding and integrations i18n copy 2026-05-19 16:03:01 -07:00
e2f779b20d chore: load 8 contiue items 2026-05-19 14:54:23 +08:00
e198d6305c merge 2026-05-19 14:14:51 +08:00
5e67514265 chore: support edcation action 2026-05-19 13:56:49 +08:00
b63896de87 feat: learn dify use api 2026-05-19 13:44:14 +08:00
e463389f2c feat: improve integration install flows 2026-05-18 20:55:05 -07:00
cda348ca10 feat: split plugin settings by category 2026-05-18 20:54:32 -07:00
ca48050666 feat: refine integrations page shell controls 2026-05-18 20:54:11 -07:00
9c0f592f34 feat: open integrations settings in account modal 2026-05-18 20:53:47 -07:00
b70241ad36 fix: app list not refresh 2026-05-18 12:11:08 -07:00
4abe622b2e feat: continue with use the app list data 2026-05-18 12:10:58 -07:00
16c32c82e3 feat: knowledge new sidebar 2026-05-18 12:10:48 -07:00
46424513d1 chore: missing files 2026-05-18 12:10:37 -07:00
2c4baa20d8 feat: app new nav 2026-05-18 12:10:25 -07:00
b0ae553f2e fix(web): correct custom icon class names 2026-05-18 12:07:16 -07:00
0266a12ee5 fix(web): align rebased UI type contracts 2026-05-18 11:19:12 -07:00
9d7765d5fd docs: update main nav follow-up notes 2026-05-18 11:16:16 -07:00
d4ef983f42 refactor(web): organize integrations page helpers 2026-05-18 11:16:16 -07:00
018f36711d fix(web): route document settings to integrations 2026-05-18 11:16:16 -07:00
dacd333e4a chore(i18n): rename plugin-facing copy to integrations 2026-05-18 11:16:16 -07:00
b079a26314 fix(web): gate integrations install actions 2026-05-18 11:16:15 -07:00
7e953ebe0b feat(web): complete update setting popover 2026-05-18 11:16:15 -07:00
b4d28fca54 fix(web): polish integration page titles 2026-05-18 11:16:15 -07:00
728c6b8201 chore: rename to marketplace path 2026-05-18 11:16:15 -07:00
f56e23b5fd chore: remove discover entrance 2026-05-18 11:16:15 -07:00
5600cefa53 feat: add interation discover route 2026-05-18 11:16:15 -07:00
561eb9cbd2 fix: trigger, agent-strategry, extension problem 2026-05-18 11:16:15 -07:00
83766ca694 chore: new pages add to dataset route guard 2026-05-18 11:16:15 -07:00
678be94d22 fix: custom tool copywriting 2026-05-18 11:16:15 -07:00
9e852429be chore: split logic from accont setting and integrating setting 2026-05-18 11:16:15 -07:00
d93c5028f1 chore: rename to integration setting 2026-05-18 11:14:15 -07:00
54f189305e chore: use new hook to handle setting 2026-05-18 11:13:33 -07:00
a610a24507 chore: filter apps and knowledges no data 2026-05-18 11:12:17 -07:00
05e8a94bb5 fix: not configure default model tip not align 2026-05-18 11:12:17 -07:00
b2e2e7b60b chore: homepage coninue with to improve 2026-05-18 11:12:17 -07:00
e7d2e66ff5 chore: popup create hide some 2026-05-18 11:12:17 -07:00
c51069685c chore: some tiny style 2026-05-18 11:12:17 -07:00
28c208f36a feat: knowledge items 2026-05-18 11:12:17 -07:00
53a1386b87 feat: knowledge title 2026-05-18 11:12:17 -07:00
0e366c7300 chore: show no empty logic 2026-05-18 11:12:17 -07:00
939bdde373 feat: knowledge empty list 2026-05-18 11:12:17 -07:00
13dfa3aba4 feat(integrations): add unavailable page fallback 2026-05-18 11:12:16 -07:00
2705a7c1db feat(integrations): align tools and plugin category UI 2026-05-18 11:12:16 -07:00
258a751b8c feat(integrations): improve data source plugin management 2026-05-18 11:12:16 -07:00
5a35d3d9cd feat(plugin): add update settings popover 2026-05-18 11:12:16 -07:00
c3fbafae83 chore(i18n): localize integrations updates 2026-05-18 11:12:16 -07:00
f727c8f838 docs: update frontend agent guidance 2026-05-18 11:12:16 -07:00
90af4c39b4 chore: some small ui 2026-05-18 11:12:16 -07:00
f7c3a4e4cb feat: empty page 2026-05-18 11:12:16 -07:00
be7d043edd chore: remove mock app data 2026-05-18 11:12:16 -07:00
cef8fe3a4b chore: remove shortcut 2026-05-18 11:12:16 -07:00
afe0e6c393 chore: missing files 2026-05-18 11:12:16 -07:00
37309b931e feat: new head 2026-05-18 11:12:15 -07:00
6a83c6705c temp: app hearder 2026-05-18 11:10:59 -07:00
3e75d5e443 chore: create app card 2026-05-18 11:10:11 -07:00
7be8a5b883 chore: app card ui 2026-05-18 11:10:11 -07:00
80dcb344f4 docs: record integrations install permission follow-up 2026-05-18 11:10:11 -07:00
b029c9b1cd feat: add integrations plugin category views 2026-05-18 11:10:11 -07:00
6cb97e9201 fix: align tools and mcp provider behavior 2026-05-18 11:10:11 -07:00
4ef2e952bd feat: add integrations page shell refinements 2026-05-18 11:10:10 -07:00
cc5545339c docs: update frontend review guidance
Document shared component reuse and component-writing checks for future frontend reviews, and refresh the MainNav follow-up notes.
2026-05-18 11:10:10 -07:00
0a8c46a3a7 refactor: polish integrations and main nav UI
Reuse shared base controls in MainNav and Integrations, add active integration icons, and keep compact integration content framing covered by targeted tests.
2026-05-18 11:10:10 -07:00
65770903d1 feat: refine integrations layout and controls
- add integrations headers, install action, permission quick settings, and update setting entry points

- centralize default vs compact content insets for integrations child pages

- cover provider, plugin, marketplace, MCP, and model provider behaviors with focused tests
2026-05-18 11:10:10 -07:00
5a6ba2ffb5 fix: localize integrations i18n copy 2026-05-18 11:09:15 -07:00
aa53afe07d fix: update custom tool integration route 2026-05-18 11:09:14 -07:00
4740a89f4a feat: add canonical integrations routes 2026-05-18 11:09:14 -07:00
328db3d67a fix: align main nav interactions
Update active main nav icon positioning from the refreshed Figma assets, remove the transparent active border that caused nav item jitter, and route mobile common layout through the new MainNav instead of the legacy Header.

Also align workspace plan actions with the new UI contract by showing Upgrade for sandbox workspaces and View Plan for paid workspaces, both opening the pricing modal.
2026-05-18 11:09:14 -07:00
88062fb247 feat: explore page to home page 2026-05-18 11:09:14 -07:00
045da59220 chore: app card icon and palce of learn dify 2026-05-18 11:09:14 -07:00
948b0f6bc7 chore: templates item ui and learn dify 2026-05-18 11:09:14 -07:00
14a59f6e44 chore: tag ui 2026-05-18 11:09:14 -07:00
f9f361113e feat: add description and tag filter 2026-05-18 11:09:14 -07:00
eea6f59307 chore: remove more learning templates and templates copywrite 2026-05-18 11:09:14 -07:00
718f69dc43 feat: hide learn dify anim effect 2026-05-18 11:09:14 -07:00
82a2ba9264 feat: learn dify 2026-05-18 11:09:14 -07:00
6c8e032fbb chore: fix small css 2026-05-18 11:09:14 -07:00
28c2c3bfd3 chore: split icon to new file and enchance data struct 2026-05-18 11:09:14 -07:00
9d463e1024 feat: continue work 2026-05-18 11:09:14 -07:00
7f87616625 chore: no show slide logic 2026-05-18 11:09:14 -07:00
43a04ed0c2 feat: finish slide 2026-05-18 11:09:13 -07:00
5083edd0ce fix: align main nav gating and account popup behavior 2026-05-18 11:09:13 -07:00
8306fa41b9 fix(web): align main nav defaults
Default integrations to the model provider section and route the main nav entry there.

Hide cloud-only workspace credits and upgrade actions outside cloud edition.

Add the repo-local karpathy-guidelines skill.
2026-05-18 11:09:13 -07:00
8f33305e90 docs: update iconify review guidance
- generalize generated icon diff review guidance for intrinsic width and height changes
2026-05-18 11:09:13 -07:00
7077a43c1c feat: add integrations tools page with prebuilt icons
- add the integrations page sidebar with collapsible icon-only navigation and Figma-aligned marketplace card
- move custom integration SVGs into the iconify collection and document the Tailwind i-custom workflow
- preserve source SVG collection dimensions when flattening generated icon data so existing main nav icons keep their 20x20 viewBox
- add an icon dimension guard for layout-sensitive generated icons
- update model provider routing, i18n, and focused frontend tests
2026-05-18 11:09:13 -07:00
884a43ae0a fix(web): preserve settings fallbacks during main nav update
- hide migrated settings tabs from the account settings sidebar

- add disabled integrations destination mapping for future migration

- keep legacy settings modal fallback until integrations sections are ready

- restore main nav active styling and add titles for truncated labels
2026-05-18 11:09:13 -07:00
914f89f478 refactor(web): align main nav review feedback
- move main nav active edge styling into Tailwind classes

- split account dropdown menu content into focused components

- align frontend review skill rules with i18n and styling guidance

- add missing common i18n keys across supported locales
2026-05-18 11:09:13 -07:00
163153db18 refactor(web): split main nav components
- Move MainNav sections into focused components under main-nav/components

- Reuse Explore AppNavItem for MainNav web app rows via a mainNav variant

- Keep WorkspaceCard expanded panel behavior and styling aligned with the pre-refactor UI
2026-05-18 11:09:13 -07:00
49d890d514 feat(web): refine main nav onboarding UI
- Add a reusable dimm Badge variant for workspace plan labels

- Update MainNav workspace, web apps, account, and help menu styling to match Figma

- Add MainNav-specific account dropdown with appearance, language, timezone, and logout entries

- Keep account trigger compact without plan badge while preserving the badge in the popup header

- Prevent the common layout shell from creating a page-level scrollbar
2026-05-18 11:09:13 -07:00
0292bc2728 feat: refine desktop main nav visuals 2026-05-18 11:09:13 -07:00
5c21120977 feat: add desktop main navigation 2026-05-18 11:09:13 -07:00
307 changed files with 22900 additions and 876 deletions

View File

@ -44,3 +44,21 @@ The codebase is split into:
- Backend architecture adheres to DDD and Clean Architecture principles.
- Async work runs through Celery with Redis as the broker.
- Frontend user-facing strings must use `web/i18n/en-US/`; avoid hardcoded text.
## Agent V2 Frontend Constraints
- Treat workflow Agent and Agent v2 as separate product surfaces. The existing workflow `agent` node is the legacy Old Agent and should keep its current strategy/plugin-based behavior. Do not refactor legacy Agent code to support Agent v2 unless explicitly requested.
- New Agent work must use the Agent v2 surface and code path: `web/features/agent-v2`, `web/app/components/workflow/nodes/agent-v2`, `BlockEnum.AgentV2`, and the `agentV2` i18n namespace where applicable. Do not add new Agent behavior to legacy `web/app/components/workflow/nodes/agent`.
- Do not mix, alias, or compatibility-bridge Old Agent and Agent v2 data shapes. Keep fields such as `agent_strategy_*` on legacy Agent only, and fields such as `agent_roster`, `agent_task`, and Agent v2 backend bindings on Agent v2 only.
- Shared workflow utilities may branch on the explicit node type/discriminator when necessary, but they must preserve the boundary: legacy Agent behavior must not depend on Agent v2 data, and Agent v2 behavior must not fall back to legacy Agent strategy/plugin behavior.
- For Agent v2 frontend work under `web/features/agent-v2`, use generated contracts and `consoleQuery` from `@/service/client` for all Agent v2 backend APIs. Do not add ad hoc REST helpers, mock data, compatibility shims, or handwritten API types for new Agent v2 interfaces.
- Keep Agent v2 composer state split by responsibility: TanStack Query is the server source of truth, Jotai atoms hold only the editable composer draft, and local component state owns transient UI such as menus, dialogs, and expanded panels.
- Wrap editable Agent v2 composer surfaces in an instance-level `AgentComposerProvider`. The provider is an editing-session boundary, not a data-fetching layer; use one store per agent/configure page or workflow node composer instance to avoid draft leakage.
- Hydrate composer atoms from generated contract data by mapping the response into both `originalDraft` and `draft`. Compute dirty state from `draft` vs `originalDraft`; do not compare editable draft directly against live TanStack Query cache.
- Keep mock or transitional composer defaults at the owning surface boundary, such as the configure page. Do not put page-specific mock data into shared `agent-composer` store defaults.
- Use existing `@langgenius/dify-ui/*` primitives before adding feature-local UI chrome. Prefer primitive default styles; add call-site CSS only for real design deltas.
- Prefer primitive data/CSS selectors for visual states instead of mirroring state in React only to choose classes.
- Avoid arbitrary Tailwind values when an existing project utility or token class expresses the same value. Keep arbitrary values only for design-system exceptions without a native utility.
- Preserve keyboard accessibility in Agent v2 pages: visible focus rings must not be clipped, and inert layout regions should not become keyboard focus targets.
- Keep Agent v2 i18n scoped to the explicitly maintained locales unless the supported-locale scope changes.
- Keep Agent v2 module copy in the `agentV2` namespace; use shared namespaces such as `common` only for genuinely shared operation labels.

View File

@ -1,7 +1,7 @@
"""add identity mode to mcp tool provider
Revision ID: 3df4dbcc1e21
Revises: 2b3c4d5e6f70
Revises: c4d5e6f7a8b9
Create Date: 2026-05-29 15:00:00.000000
Adds the `identity_mode` column to `tool_mcp_providers` to drive the M2 MCP
@ -23,7 +23,7 @@ import models as models
# revision identifiers, used by Alembic.
revision = "3df4dbcc1e21"
down_revision = "2b3c4d5e6f70"
down_revision = "c4d5e6f7a8b9"
branch_labels = None
depends_on = None

View File

@ -60,6 +60,7 @@ class ComposerSavePayload(BaseModel):
class RosterAgentCreatePayload(BaseModel):
name: str = Field(min_length=1, max_length=255)
mode: Literal["agent"] = "agent"
description: str = ""
role: str = Field(default="", max_length=255)
icon_type: AgentIconType | None = None

View File

@ -215,169 +215,6 @@ describe('E2E / error message standards (spec 5.3)', () => {
expect(result.stderr).not.toContain(sentValue)
})
// ── 5.70d-j ErrorBody contract — error.server structure ─────────────────
// The ErrorBody unification spec introduces a canonical error body:
// { code, status, message, hint?, details?[{type, loc, msg}] }
// On a canonical 422 the CLI attaches the full server object as error.server
// in the JSON envelope and uses server?.code for the human-readable header.
it('[P0] 5.70d JSON envelope contains error.server with code, status, message on validation failure', async () => {
// Spec: every canonical 422 from @accepts carries code:"invalid_param",
// status:422 and message. The CLI attaches the parsed ErrorBody verbatim as
// error.server — zero field copying so the contract is single-source.
const result = await fx.r([
'run',
'app',
E.workflowAppId,
'--inputs',
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
'-o',
'json',
])
assertNonZeroExit(result)
const envelope = JSON.parse(result.stderr.trim()) as {
error: {
code: string
server?: { code: string, status: number, message: string }
}
}
expect(envelope.error.server, 'error.server must be present for canonical ErrorBody responses').toBeDefined()
expect(envelope.error.server?.code).toBe('invalid_param')
expect(envelope.error.server?.status).toBe(422)
expect(typeof envelope.error.server?.message).toBe('string')
expect(envelope.error.server?.message.length).toBeGreaterThan(0)
})
it('[P1] 5.70e error.server.details array carries field-level error entries', async () => {
// Spec: @accepts emits details:[{type, loc, msg}] for each failing field.
// CLI forwards the array as-is inside error.server.details — no truncation.
const result = await fx.r([
'run',
'app',
E.workflowAppId,
'--inputs',
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
'-o',
'json',
])
assertNonZeroExit(result)
const envelope = JSON.parse(result.stderr.trim()) as {
error: {
server?: {
details?: Array<{ type: string, msg: string, loc?: Array<string | number> }>
}
}
}
const details = envelope.error.server?.details
expect(Array.isArray(details), 'error.server.details must be an array').toBe(true)
expect(details!.length, 'details must contain at least one entry').toBeGreaterThan(0)
// Each entry must have type and msg; loc is optional but expected for body fields
const entry = details![0]!
expect(typeof entry.type, 'detail.type must be a string').toBe('string')
expect(entry.type.length).toBeGreaterThan(0)
expect(typeof entry.msg, 'detail.msg must be a string').toBe('string')
expect(entry.msg.length).toBeGreaterThan(0)
})
it('[P1] 5.70f human-readable text mode renders details as indented lines', async () => {
// Spec: format.ts iterates server?.details and renders each entry as
// " - <loc>: <msg> (<type>)"
// This means field-level errors are visible without -v.
const result = await fx.r([
'run',
'app',
E.workflowAppId,
'--inputs',
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
])
assertNonZeroExit(result)
// Must contain at least one " - ... (...)" detail line
expect(result.stderr).toMatch(/\s+-\s[^(]+\([^)]+\)/)
})
it('[P1] 5.70g text mode header uses server code (invalid_param) not CLI classification code', async () => {
// Spec: renderHuman computes headerCode = server?.code ?? e.code
// For a canonical 422, server.code = "invalid_param" wins over the CLI's
// classification code ("server_4xx_other"), so stderr starts with "invalid_param:".
const result = await fx.r([
'run',
'app',
E.workflowAppId,
'--inputs',
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
])
assertNonZeroExit(result)
expect(result.stderr.trimStart()).toMatch(/^invalid_param:/)
})
it('[P1] 5.70h JSON envelope error.code is CLI classification; server code lives in error.server.code', async () => {
// Spec: toEnvelope() sets error.code = c.code (CLI classification = "server_4xx_other")
// while the server's semantic code sits separately in error.server.code.
// Agents and tooling can read error.server.code for semantic branching
// without parsing human-readable text.
const result = await fx.r([
'run',
'app',
E.workflowAppId,
'--inputs',
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
'-o',
'json',
])
assertNonZeroExit(result)
const envelope = JSON.parse(result.stderr.trim()) as {
error: { code: string, server?: { code: string } }
}
expect(envelope.error.code).toBe('server_4xx_other')
expect(envelope.error.server?.code).toBe('invalid_param')
})
// ── 5.70i / 5.70j PR #37285 boundary contract ───────────────────────────
it('[P1] 5.70i unknown /openapi/v1 route returns canonical 404 ErrorBody without route suggestions', async () => {
// PR #37285: ExternalApi._help_on_404 suppresses flask-restx route enumeration.
// Previously, an unknown path under /openapi/v1/ returned flask-restx's default
// 404 with a "Did you mean /openapi/v1/apps?" suggestion, leaking the route table.
// After the fix it must return a canonical ErrorBody and contain no suggestions.
const res = await fetch(`${E.host.replace(/\/$/, '')}/openapi/v1/this-path-does-not-exist-e2e`, {
headers: { Authorization: `Bearer ${E.token}` },
signal: AbortSignal.timeout(8_000),
})
expect(res.status).toBe(404)
const body = await res.json() as Record<string, unknown>
// canonical ErrorBody fields must be present
expect(typeof body.code, '404 body must have a string code field').toBe('string')
expect(body.status, '404 body must have status: 404').toBe(404)
// no flask-restx route enumeration in the response
const raw = JSON.stringify(body)
expect(raw).not.toMatch(/did you mean/i)
expect(raw).not.toMatch(/you might want/i)
})
it('[P1] 5.70j device-flow 4xx uses RFC 8628 format, not ErrorBody — zErrorBody parse fails gracefully', async () => {
// PR #37285 explicitly excludes RFC 8628 device-flow endpoints from the
// ErrorBody contract. This test pins that contract:
// - The device/token endpoint returns RFC 8628 {error: string} on failure,
// not the canonical {code, status, message} shape.
// - When the CLI's classifyResponse encounters this, zErrorBody.safeParse
// returns failure → serverError = undefined → generic status-based message,
// no error.server field, no crash.
const res = await fetch(`${E.host.replace(/\/$/, '')}/openapi/v1/oauth/device/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_code: 'fake-invalid-device-code-e2e-test', client_id: 'difyctl' }),
signal: AbortSignal.timeout(8_000),
})
// device flow errors are 4xx (400 bad_request or 401 expired_token etc.)
expect(res.status).toBeGreaterThanOrEqual(400)
expect(res.status).toBeLessThan(500)
const body = await res.json() as Record<string, unknown>
// RFC 8628 shape: has 'error' string, must NOT have ErrorBody 'code'/'status' pair
expect(typeof body.error, 'RFC 8628 body must have a string error field').toBe('string')
expect(body).not.toHaveProperty('status')
// zErrorBody.safeParse would fail → CLI sets serverError = undefined → generic message
})
// ── 5.76 Failed command + -o yaml → stderr is still JSON envelope ────────
it('[P1] 5.76 failed command with -o yaml still outputs a JSON error envelope on stderr', async () => {

View File

@ -135,7 +135,6 @@ AMPLITUDE_API_KEY=
TEXT_GENERATION_TIMEOUT_MS=60000
CSP_WHITELIST=
ALLOW_EMBED=false
ALLOW_INLINE_STYLES=false
ALLOW_UNSAFE_DATA_SCHEME=false
TOP_K_MAX_VALUE=10
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000

View File

@ -387,7 +387,6 @@ services:
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
CSP_WHITELIST: ${CSP_WHITELIST:-}
ALLOW_EMBED: ${ALLOW_EMBED:-false}
ALLOW_INLINE_STYLES: ${ALLOW_INLINE_STYLES:-false}
ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai}
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}

View File

@ -393,7 +393,6 @@ services:
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
CSP_WHITELIST: ${CSP_WHITELIST:-}
ALLOW_EMBED: ${ALLOW_EMBED:-false}
ALLOW_INLINE_STYLES: ${ALLOW_INLINE_STYLES:-false}
ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai}
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}

View File

@ -192,11 +192,6 @@
"count": 1
}
},
"web/app/(commonLayout)/snippets/[snippetId]/page.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/(shareLayout)/components/authenticated-layout.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -5025,14 +5020,6 @@
"count": 1
}
},
"web/app/components/workflow/block-selector/blocks.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/workflow/block-selector/featured-tools.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -5717,7 +5704,7 @@
"count": 2
},
"ts/no-explicit-any": {
"count": 6
"count": 4
}
},
"web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx": {
@ -5898,11 +5885,6 @@
"count": 5
}
},
"web/app/components/workflow/nodes/components.ts": {
"ts/no-explicit-any": {
"count": 2
}
},
"web/app/components/workflow/nodes/data-source-empty/hooks.ts": {
"ts/no-explicit-any": {
"count": 1
@ -7073,19 +7055,6 @@
"count": 1
}
},
"web/app/components/workflow/run/agent-log/agent-log-trigger.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/workflow/run/agent-log/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 2
}
},
"web/app/components/workflow/run/hooks.ts": {
"ts/no-explicit-any": {
"count": 1
@ -7768,11 +7737,6 @@
"count": 1
}
},
"web/service/client.ts": {
"no-restricted-imports": {
"count": 1
}
},
"web/service/common.ts": {
"no-restricted-imports": {
"count": 1

View File

@ -166,8 +166,23 @@ See `[web/docs/overlay.md](../../web/docs/overlay.md)` for the web app overlay b
- `pnpm -C packages/dify-ui test` — Vitest unit tests for primitives.
- `pnpm -C packages/dify-ui storybook` — Storybook on the default port. Each primitive has `index.stories.tsx`.
- `pnpm -C packages/dify-ui test:storybook` — Storybook component tests in Vitest browser mode. Stories without `play` are render and a11y smoke tests; stories with `play` should cover public UI contracts such as opening overlays, keyboard navigation, disabled/loading guards, form submission, and controlled state updates.
- `pnpm -C packages/dify-ui type-check``tsgo --noEmit` for this package only.
### Test Boundary
Use Storybook tests for behavior that belongs to the documented component example:
visible state changes, user interaction, keyboard paths, overlay open/close flows,
and accessibility-facing semantics. Keep regular Vitest unit tests for lower-level
wrapper contracts such as class variants, Base UI passthrough props, hidden input
serialization, data attribute hooks, store behavior, and edge cases that do not
need a full story.
Storybook accessibility testing stays enabled globally with `a11y.test = 'error'`.
If a story is temporarily marked `todo`, keep the exception local to that story
and do not treat an interaction `play` test as a replacement for fixing the
underlying accessibility issue.
### Disabling Animations In Tests
Base UI can wait for `element.getAnimations()` to finish before it unmounts overlays, panels, and transition-driven components. Browser-based test runners can make that timing unstable, especially when tests assert final DOM state rather than animation behavior.

View File

@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import { expect, waitFor, within } from 'storybook/test'
import {
AlertDialog,
AlertDialogActions,
@ -55,6 +56,21 @@ export const Default: Story = {
</AlertDialogContent>
</AlertDialog>
),
play: async ({ canvas, canvasElement, userEvent }) => {
const body = within(canvasElement.ownerDocument.body)
await userEvent.click(canvas.getByRole('button', { name: 'Delete project' }))
const dialog = body.getByRole('alertdialog', { name: 'Delete project?' })
await waitFor(async () => {
await expect(dialog).toBeVisible()
})
await userEvent.click(body.getByRole('button', { name: 'Cancel' }))
await waitFor(async () => {
await expect(body.queryByRole('alertdialog', { name: 'Delete project?' })).not.toBeInTheDocument()
})
},
}
export const NonDestructive: Story = {

View File

@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import { expect, fn } from 'storybook/test'
import { Button } from '.'
@ -90,8 +91,21 @@ export const Loading: Story = {
args: {
variant: 'primary',
loading: true,
onClick: fn(),
children: 'Loading Button',
},
play: async ({ args, canvas, userEvent }) => {
const button = canvas.getByRole('button', { name: 'Loading Button' })
await expect(button).toHaveAttribute('aria-disabled', 'true')
await expect(button).toHaveAttribute('aria-busy', 'true')
button.focus()
await expect(button).toHaveFocus()
await userEvent.click(button)
await expect(args.onClick).not.toHaveBeenCalled()
},
parameters: {
docs: {
description: {

View File

@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
import type { Virtualizer } from '@tanstack/react-virtual'
import { useVirtualizer } from '@tanstack/react-virtual'
import * as React from 'react'
import { expect } from 'storybook/test'
import {
Combobox,
ComboboxChip,
@ -768,6 +769,15 @@ const MultipleChipsDemo = () => {
export const MultipleChips: Story = {
render: () => <MultipleChipsDemo />,
play: async ({ canvas, userEvent }) => {
await expect(canvas.getByText('Maya Chen')).toBeVisible()
await expect(canvas.getByText('Liam Brooks')).toBeVisible()
await userEvent.click(canvas.getByRole('button', { name: 'Remove Maya Chen' }))
await expect(canvas.queryByText('Maya Chen')).not.toBeInTheDocument()
await expect(canvas.getByText('Liam Brooks')).toBeVisible()
},
}
export const VirtualizedLongList: Story = {

View File

@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import { expect, waitFor, within } from 'storybook/test'
import {
Dialog,
DialogCloseButton,
@ -66,6 +67,22 @@ export const Default: Story = {
</DialogContent>
</Dialog>
),
play: async ({ canvas, canvasElement, userEvent }) => {
const body = within(canvasElement.ownerDocument.body)
await userEvent.click(canvas.getByRole('button', { name: 'Open dialog' }))
const dialog = body.getByRole('dialog', { name: 'Invite collaborators' })
await waitFor(async () => {
await expect(dialog).toBeVisible()
})
await expect(body.getByRole('textbox', { name: 'Email address' })).toBeVisible()
await userEvent.click(body.getByRole('button', { name: 'Close' }))
await waitFor(async () => {
await expect(body.queryByRole('dialog', { name: 'Invite collaborators' })).not.toBeInTheDocument()
})
},
}
export const WithoutCloseButton: Story = {

View File

@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { FileTreeIconType } from '.'
import * as React from 'react'
import { expect } from 'storybook/test'
import {
FileTreeBadge,
FileTreeFile,
@ -330,6 +331,19 @@ function VisualStates() {
export const Default: Story = {
render: () => <ComposedFileTree />,
play: async ({ canvas, userEvent }) => {
const srcFolder = canvas.getByRole('button', { name: 'src' })
await expect(canvas.getByRole('button', { name: 'components' })).toBeVisible()
await userEvent.click(srcFolder)
await expect(srcFolder).toHaveAttribute('aria-expanded', 'false')
await expect(canvas.queryByRole('button', { name: 'components' })).not.toBeInTheDocument()
await userEvent.click(srcFolder)
await expect(srcFolder).toHaveAttribute('aria-expanded', 'true')
await expect(canvas.getByRole('button', { name: 'components' })).toBeVisible()
},
}
export const DataDriven: Story = {

View File

@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import { expect } from 'storybook/test'
import {
Pagination,
PaginationSkeleton,
@ -77,6 +78,15 @@ type Story = StoryObj<typeof meta>
export const Playground: Story = {
render: () => <PaginationDemo />,
play: async ({ canvas, userEvent }) => {
await expect(canvas.getByRole('button', { name: 'Edit page number, current page 2 of 200' })).toBeVisible()
await userEvent.click(canvas.getByRole('button', { name: 'Next page' }))
await expect(canvas.getByRole('button', { name: 'Edit page number, current page 3 of 200' })).toBeVisible()
await userEvent.click(canvas.getByRole('button', { name: '50' }))
await expect(canvas.getByRole('button', { name: '50' })).toHaveAttribute('aria-pressed', 'true')
},
parameters: {
a11y: {
test: 'todo',

View File

@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import { expect, waitFor, within } from 'storybook/test'
import {
Select,
SelectContent,
@ -19,6 +20,8 @@ const triggerWidth = 'w-64'
const cityItems = [
{ label: 'Seattle', value: 'seattle' },
{ label: 'New York', value: 'new-york' },
{ label: 'Tokyo', value: 'tokyo' },
{ label: 'Paris', value: 'paris' },
]
const meta = {
@ -41,11 +44,11 @@ type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => (
<div className={triggerWidth}>
<Select defaultValue="seattle">
<Select items={cityItems} defaultValue="seattle">
<SelectTrigger aria-label="City">
<SelectValue placeholder="Select a city" />
</SelectTrigger>
<SelectContent>
<SelectContent listProps={{ 'aria-label': 'City options' }}>
<SelectItem value="seattle">
<SelectItemText>Seattle</SelectItemText>
<SelectItemIndicator />
@ -66,6 +69,27 @@ export const Default: Story = {
</Select>
</div>
),
play: async ({ canvas, canvasElement, userEvent }) => {
const trigger = canvas.getByRole('combobox', { name: 'City' })
const body = within(canvasElement.ownerDocument.body)
await expect(trigger).toHaveTextContent('Seattle')
trigger.focus()
await userEvent.keyboard('{ArrowDown}')
await waitFor(async () => {
await expect(body.getByRole('option', { name: 'Tokyo' })).toBeVisible()
})
await userEvent.keyboard('{ArrowDown}{ArrowDown}{Enter}')
await expect(trigger).toHaveTextContent('Tokyo')
await userEvent.keyboard('{Escape}')
await waitFor(async () => {
await expect(body.queryByRole('listbox', { name: 'City options' })).not.toBeInTheDocument()
})
},
}
export const WithVisibleLabel: Story = {

View File

@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import { expect } from 'storybook/test'
import { Switch, SwitchSkeleton } from '.'
import {
FieldDescription,
@ -77,6 +78,17 @@ export const Default: Story = {
checked: false,
disabled: false,
},
play: async ({ canvas, userEvent }) => {
const switchControl = canvas.getByRole('switch', { name: 'Enable auto retry' })
await expect(switchControl).toHaveAttribute('aria-checked', 'false')
await expect(canvas.getByText('Failures require manual retry.')).toBeVisible()
await userEvent.click(switchControl)
await expect(switchControl).toHaveAttribute('aria-checked', 'true')
await expect(canvas.getByText('Failures will retry automatically.')).toBeVisible()
},
}
export const DefaultOn: Story = {

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.30314 1.54615C8.48087 1.50085 8.66708 1.49473 8.84742 1.52858C9.07685 1.57176 9.27914 1.6956 9.41968 1.77467L13.1768 3.88795C13.2961 3.955 13.4666 4.04398 13.6098 4.17701L13.6697 4.23691C13.7899 4.36839 13.8809 4.52463 13.9366 4.69394C14.0074 4.90941 13.9998 5.13856 13.9998 5.29485V9.55917C13.9998 9.70492 14.0069 9.91888 13.9444 10.1223C13.9076 10.2422 13.8527 10.3559 13.7823 10.4596L13.7068 10.5598C13.5702 10.7233 13.3869 10.8339 13.2647 10.9133L8.25171 14.1718C8.11671 14.2596 7.92272 14.396 7.69637 14.4537C7.51868 14.499 7.33292 14.5051 7.15275 14.4713C6.92315 14.4281 6.72034 14.3049 6.57984 14.2258L2.82268 12.1119C2.68658 12.0353 2.48283 11.9301 2.32984 11.7629C2.20953 11.6314 2.11852 11.475 2.06291 11.3059C1.99209 11.0903 1.99976 10.8606 1.99976 10.7044V6.44069C1.99976 6.29486 1.99262 6.08092 2.0551 5.87753L2.09807 5.7597C2.14656 5.64426 2.21216 5.53647 2.29273 5.44003L2.34611 5.38144C2.47424 5.24974 2.62775 5.15609 2.73479 5.08652L7.85979 1.75514C7.98194 1.67712 8.13342 1.58951 8.30314 1.54615ZM3.33309 10.7044C3.33309 10.7559 3.33334 10.7962 3.33374 10.8307C3.33392 10.8452 3.33411 10.8578 3.33439 10.8684C3.34356 10.8739 3.3543 10.8806 3.36695 10.8879C3.39682 10.9052 3.43208 10.9245 3.47697 10.9498L6.74064 12.7857V11.649L3.33309 9.73235V10.7044ZM8.07398 11.621V12.6972L12.5382 9.7955C12.5784 9.76933 12.6098 9.74883 12.6365 9.73105C12.6475 9.72368 12.6564 9.71639 12.6645 9.71087C12.6647 9.70125 12.6656 9.69004 12.6658 9.67701C12.6661 9.64494 12.6664 9.60721 12.6664 9.55917V8.636L8.07398 11.621ZM3.33309 8.2024L6.74064 10.1191V8.98235L3.33309 7.06568V8.2024ZM8.07398 8.95436V10.0299L12.6664 7.04485V5.96933L8.07398 8.95436ZM8.58374 2.87493L3.95288 5.8847L7.38192 7.81373L12.046 4.78183L8.76604 2.93613C8.71971 2.91007 8.68338 2.89008 8.6521 2.87298C8.63863 2.86561 8.62684 2.85931 8.61695 2.8541C8.60751 2.85987 8.59651 2.86685 8.58374 2.87493Z" fill="#155AEF"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,8 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 11.25C7.91421 11.25 8.25 11.5858 8.25 12V14.25C8.25 14.6642 7.91421 15 7.5 15C7.08579 15 6.75 14.6642 6.75 14.25V12C6.75 11.5858 7.08579 11.25 7.5 11.25Z" fill="currentColor" />
<path d="M2.19653 2.19653C2.48937 1.90372 2.96418 1.90382 3.25708 2.19653L8.03027 6.96973C8.09162 7.03108 8.13966 7.10082 8.17529 7.1748C8.19164 7.20869 8.20587 7.24378 8.21704 7.28027C8.24638 7.37633 8.25641 7.477 8.24634 7.57617C8.23743 7.66451 8.21216 7.74788 8.17529 7.82446C8.13963 7.89868 8.09176 7.96874 8.03027 8.03027L3.25708 12.8035C2.96419 13.096 2.48932 13.0962 2.19653 12.8035C1.90394 12.5107 1.90405 12.0358 2.19653 11.7429L5.68945 8.25H0.75C0.335786 8.25 0 7.91421 0 7.5C0 7.08579 0.335786 6.75 0.75 6.75H5.68945L2.19653 3.25708C1.90389 2.96423 1.90388 2.48937 2.19653 2.19653Z" fill="currentColor" />
<path d="M10.1521 10.1521C10.445 9.85921 10.9198 9.85921 11.2126 10.1521L12.8035 11.7429C13.096 12.0358 13.0962 12.5107 12.8035 12.8035C12.5107 13.0962 12.0358 13.096 11.7429 12.8035L10.1521 11.2126C9.85921 10.9198 9.85922 10.445 10.1521 10.1521Z" fill="currentColor" />
<path d="M14.25 6.75C14.6642 6.75 15 7.08579 15 7.5C15 7.91421 14.6642 8.25 14.25 8.25H12C11.5858 8.25 11.25 7.91421 11.25 7.5C11.25 7.08579 11.5858 6.75 12 6.75H14.25Z" fill="currentColor" />
<path d="M11.7422 2.19653C12.035 1.90387 12.5098 1.90406 12.8027 2.19653C13.0956 2.4894 13.0955 2.96419 12.8027 3.25708L11.2119 4.8479C10.919 5.14079 10.4443 5.1408 10.1514 4.8479C9.85883 4.55497 9.85858 4.08013 10.1514 3.78735L11.7422 2.19653Z" fill="currentColor" />
<path d="M7.5 0C7.91421 0 8.25 0.335786 8.25 0.75V3C8.25 3.41421 7.91421 3.75 7.5 3.75C7.08579 3.75 6.75 3.41421 6.75 3V0.75C6.75 0.335786 7.08579 0 7.5 0Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z" fill="currentColor"/>
<path d="M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 0C17.5523 0 18 0.447715 18 1V6C18 6.55228 17.5523 7 17 7H12C11.4477 7 11 6.55228 11 6V4.5H6.94629C5.92438 4.50039 5.56101 5.85276 6.44531 6.36523L12.5576 9.90332C15.2116 11.4402 14.1206 15.4996 11.0537 15.5H7V17C7 17.5523 6.55228 18 6 18H1C0.447715 18 0 17.5523 0 17V12C0 11.4477 0.447715 11 1 11H6C6.55228 11 7 11.4477 7 12V13.5H11.0537C12.0756 13.4996 12.4394 12.1472 11.5557 11.6348L5.44336 8.09668C2.789 6.55983 3.87917 2.50039 6.94629 2.5H11V1C11 0.447715 11.4477 0 12 0H17ZM2 16H5V13H2V16ZM13 5H16V2H13V5Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 693 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.91669 1.16669C1.95019 1.16669 1.16669 1.95019 1.16669 2.91669V11.0834C1.16669 12.0499 1.95019 12.8334 2.91669 12.8334H11.0834C12.0499 12.8334 12.8334 12.0499 12.8334 11.0834V2.91669C12.8334 1.95019 12.0499 1.16669 11.0834 1.16669H2.91669ZM2.33335 2.91669C2.33335 2.59452 2.59452 2.33335 2.91669 2.33335H11.0834C11.4055 2.33335 11.6667 2.59452 11.6667 2.91669V11.0834C11.6667 11.4055 11.4055 11.6667 11.0834 11.6667H2.91669C2.59452 11.6667 2.33335 11.4055 2.33335 11.0834V2.91669ZM5.67188 10.5L9.67186 3.50002H8.32815L4.32817 10.5H5.67188Z" fill="#676F83" />
</svg>

After

Width:  |  Height:  |  Size: 675 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16" fill="none">
<path d="M6.25 6.875C6.82523 6.875 7.29167 7.34128 7.29167 7.91667V9.16667C7.29167 9.74205 6.82523 10.2083 6.25 10.2083C5.67477 10.2083 5.20833 9.74205 5.20833 9.16667V7.91667C5.20833 7.34128 5.67477 6.875 6.25 6.875Z" fill="#676F83"/>
<path d="M10.4167 6.875C10.992 6.875 11.4583 7.34135 11.4583 7.91667V9.16667C11.4583 9.74199 10.992 10.2083 10.4167 10.2083C9.84135 10.2083 9.375 9.74199 9.375 9.16667V7.91667C9.375 7.34135 9.84135 6.875 10.4167 6.875Z" fill="#676F83"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.33333 0C9.13875 0 9.79167 0.652918 9.79167 1.45833C9.79167 2.02329 9.46964 2.51173 8.99984 2.75391V3.33822C9.38912 3.34279 9.77995 3.35006 10.175 3.36263C11.6983 3.41112 12.7377 3.42425 13.6401 3.90951C14.375 4.30477 15.0255 4.97655 15.3971 5.72347C15.5468 6.02442 15.6427 6.33532 15.7056 6.66667H15.8333C16.2936 6.66667 16.6667 7.03976 16.6667 7.5V10C16.6667 10.4602 16.2936 10.8333 15.8333 10.8333H15.8285C15.8235 11.2254 15.813 11.5735 15.7869 11.8831C15.7386 12.4571 15.6361 12.9628 15.3971 13.4432C15.0254 14.1901 14.3749 14.8619 13.6401 15.2572C12.7377 15.7424 11.6982 15.7556 10.175 15.804C8.93336 15.8436 7.73328 15.8436 6.4917 15.804C4.96843 15.7556 3.92896 15.7424 3.02653 15.2572C2.29178 14.8619 1.64121 14.1902 1.26953 13.4432C1.03058 12.9628 0.928072 12.4571 0.87972 11.8831C0.853642 11.5735 0.843216 11.2254 0.838216 10.8333H0.833333C0.373096 10.8333 0 10.4602 0 10V7.5C0 7.03976 0.373096 6.66667 0.833333 6.66667H0.9611C1.02392 6.33532 1.11984 6.02442 1.26953 5.72347C1.64119 4.97649 2.29177 4.30475 3.02653 3.90951C3.92895 3.42425 4.96837 3.41112 6.4917 3.36263C6.88671 3.35006 7.27754 3.34279 7.66683 3.33822V2.75391C7.19703 2.51173 6.875 2.02329 6.875 1.45833C6.875 0.652918 7.52792 0 8.33333 0ZM10.1213 5.02848C8.91522 4.9901 7.75142 4.9901 6.54541 5.02848C4.85908 5.08217 4.29323 5.12091 3.81592 5.3776C3.38476 5.60954 2.98015 6.02734 2.76204 6.46566C2.65217 6.68652 2.57959 6.96168 2.54069 7.4235C2.50069 7.89854 2.5 8.50363 2.5 9.37825V9.78841C2.5 10.663 2.50069 11.2681 2.54069 11.7432C2.57959 12.205 2.65215 12.4801 2.76204 12.701C2.98015 13.1393 3.38475 13.5571 3.81592 13.7891C4.29321 14.0458 4.85904 14.0845 6.54541 14.1382C7.75141 14.1766 8.91523 14.1766 10.1213 14.1382C11.8075 14.0845 12.3734 14.0458 12.8507 13.7891C13.2819 13.5572 13.6865 13.1394 13.9046 12.701C14.0145 12.4801 14.0871 12.205 14.126 11.7432C14.166 11.2681 14.1667 10.663 14.1667 9.78841V9.37825C14.1667 8.50363 14.166 7.89854 14.126 7.4235C14.0871 6.96168 14.0145 6.68652 13.9046 6.46566C13.6865 6.02729 13.2819 5.60951 12.8507 5.3776C12.3734 5.12091 11.8075 5.08217 10.1213 5.02848Z" fill="#676F83"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,6 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.9362 1.5C12.4177 1.50006 12.8411 1.81975 12.9727 2.28296L15.4651 11.061C15.5117 11.2251 15.6125 11.3687 15.7515 11.4675L16.1235 11.7319C16.3544 11.8963 16.4728 12.1769 16.4297 12.4571L15.9206 15.7669C15.8394 16.2948 15.2475 16.5708 14.7905 16.2942L12.8006 15.0893C12.6837 15.0186 12.5618 14.9546 12.4307 14.9165C12.2411 14.8615 12.0443 14.833 11.8462 14.833C10.6885 14.833 9.75 13.8945 9.75 12.7368V9.14722C9.75 8.23747 10.4875 7.5 11.3972 7.5C11.5688 7.5 11.7164 7.62113 11.7503 7.78928L12.3824 10.9483L12.4043 11.0215C12.4721 11.182 12.6457 11.2781 12.8233 11.2427C13.0009 11.2072 13.1242 11.0515 13.125 10.8772L13.1177 10.8017L12.4856 7.6428C12.3818 7.12384 11.9265 6.75002 11.3972 6.75C11.08 6.75 10.7771 6.81165 10.5 6.92359V2.93628C10.5 2.14312 11.1431 1.5 11.9362 1.5Z" fill="currentColor"/>
<path d="M2.28761 11.1211C2.263 11.2855 2.25026 11.4538 2.25026 11.625C2.25026 13.3862 3.59948 14.8313 5.32057 14.9854L3.0757 16.3674C2.65801 16.6245 2.10961 16.418 1.96534 15.9492L0.926773 12.5742C0.823558 12.2388 0.967018 11.876 1.27174 11.7019L2.28761 11.1211Z" fill="currentColor"/>
<path d="M6.42041 1.5C7.01664 1.5 7.49997 1.98337 7.49997 2.57959V8.81835C6.96373 8.4594 6.31878 8.25 5.62501 8.25C4.91854 8.25 4.26271 8.46705 3.7207 8.83815L5.01124 2.6455C5.15039 1.97822 5.73876 1.50007 6.42041 1.5Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.625 9C7.07475 9 8.25 10.1753 8.25 11.625C8.25 13.0747 7.07475 14.25 5.625 14.25C4.17525 14.25 3 13.0747 3 11.625C3 10.1753 4.17525 9 5.625 9ZM5.625 10.875C5.21078 10.875 4.875 11.2108 4.875 11.625C4.875 12.0392 5.21078 12.375 5.625 12.375C6.03921 12.375 6.375 12.0392 6.375 11.625C6.375 11.2108 6.03921 10.875 5.625 10.875Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,9 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.875 11.8125C7.875 13.1587 6.7837 14.25 5.4375 14.25C4.0913 14.25 3 13.1587 3 11.8125C3 10.4663 4.0913 9.375 5.4375 9.375C6.7837 9.375 7.875 10.4663 7.875 11.8125Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.25 15.75L5.625 14.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.5 11.25L3 10.875" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.41699 12.5625C5.83121 12.5625 6.16699 12.2267 6.16699 11.8125C6.16699 11.3983 5.83121 11.0625 5.41699 11.0625C5.00278 11.0625 4.66699 11.3983 4.66699 11.8125C4.66699 12.2267 5.00278 12.5625 5.41699 12.5625Z" fill="currentColor"/>
<path d="M13.125 11.25L12.4956 8.10292C12.4255 7.75237 12.1177 7.5 11.7601 7.5H11.625C10.7966 7.5 10.125 8.17155 10.125 9V12.3939C10.125 13.419 10.956 14.25 11.9811 14.25C12.2408 14.25 12.4976 14.3045 12.7349 14.41L15.75 15.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.875 7.5V3.75C10.875 2.92157 11.5466 2.25 12.375 2.25H12.5394C12.8836 2.25 13.1836 2.48422 13.267 2.81811L15.3332 11.0833L16.5 11.625" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.125 9.75V3C7.125 2.58579 6.78921 2.25 6.375 2.25H5.52089C5.14964 2.25 4.83426 2.5216 4.77919 2.88875L3.75 9.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,8 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="agent">
<g id="Vector">
<path d="M14.7401 5.80454C14.5765 4.77996 14.1638 3.79808 13.5306 2.97273C12.8973 2.14738 12.0648 1.48568 11.1185 1.06589C10.1722 0.646098 9.12632 0.461106 8.08751 0.546487C7.05582 0.624753 6.04548 0.966277 5.17744 1.53548C4.3094 2.09758 3.58366 2.88024 3.09272 3.79808C2.59466 4.70881 2.33852 5.7405 2.33852 6.7793V7.22756L1.25703 9.3692C1.04357 9.80322 1.22145 10.3368 1.65547 10.5574L2.3314 10.8989V12.3006C2.3314 12.82 2.53063 13.3038 2.90061 13.6738C3.2706 14.0367 3.75442 14.243 4.27382 14.243H6.01702V14.7624C6.01702 15.1538 6.3372 15.4739 6.72853 15.4739C7.11986 15.4739 7.44004 15.1538 7.44004 14.7624V13.7094C7.44004 13.2185 7.04159 12.82 6.55065 12.82H4.27382C4.13864 12.82 4.00345 12.7631 3.91095 12.6706C3.81846 12.5781 3.76154 12.4429 3.76154 12.3077V10.5716C3.76154 10.2301 3.56943 9.92417 3.2706 9.77476L2.77254 9.52573L3.66904 7.73984C3.72596 7.61889 3.76154 7.4837 3.76154 7.34851V6.77219C3.76154 5.96818 3.96076 5.17129 4.34498 4.4669C4.72919 3.76251 5.28417 3.15772 5.9601 2.7237C6.63603 2.28968 7.41158 2.02643 8.20847 1.96239C9.00536 1.89835 9.81648 2.04066 10.5493 2.36795C11.2822 2.69524 11.9225 3.20042 12.4135 3.84077C12.8973 4.47402 13.2246 5.23533 13.3456 6.02511C13.4665 6.81488 13.3954 7.63312 13.125 8.38731C12.8617 9.12017 12.4206 9.78187 11.8585 10.3084C11.6735 10.4792 11.5668 10.7139 11.5668 10.9701V14.7624C11.5668 15.1538 11.887 15.4739 12.2783 15.4739C12.6696 15.4739 12.9898 15.1538 12.9898 14.7624V11.1978C13.6515 10.5432 14.1567 9.73918 14.4697 8.87114C14.8184 7.89637 14.918 6.83623 14.7615 5.81165L14.7401 5.80454Z" fill="white"/>
<path d="M10.8055 7.99599C10.8909 7.83234 10.962 7.66158 11.0189 7.4837H11.6522C12.0435 7.4837 12.3637 7.16352 12.3637 6.77219C12.3637 6.38086 12.0435 6.06068 11.6522 6.06068H11.0189C10.9691 5.8828 10.898 5.71204 10.8055 5.54839L11.2537 5.10014C11.5312 4.82266 11.5312 4.3744 11.2537 4.09692C10.9762 3.81943 10.528 3.81943 10.2505 4.09692L9.80225 4.54517C9.6386 4.45267 9.46784 4.38863 9.28996 4.33171V3.69847C9.28996 3.30714 8.96978 2.98696 8.57845 2.98696C8.18712 2.98696 7.86694 3.30714 7.86694 3.69847V4.33171C7.68907 4.38152 7.5183 4.45267 7.35466 4.54517L6.90641 4.09692C6.62892 3.81943 6.18067 3.81943 5.90318 4.09692C5.62569 4.3744 5.62569 4.82266 5.90318 5.10014L6.35143 5.54839C6.26605 5.71204 6.1949 5.8828 6.13798 6.06068H5.50473C5.1134 6.06068 4.79323 6.38086 4.79323 6.77219C4.79323 7.16352 5.1134 7.4837 5.50473 7.4837H6.13798C6.18778 7.66158 6.25893 7.83234 6.35143 7.99599L5.90318 8.44424C5.62569 8.72172 5.62569 9.16997 5.90318 9.44746C6.04548 9.58976 6.22336 9.6538 6.40835 9.6538C6.59334 9.6538 6.77122 9.58265 6.91352 9.44746L7.36177 8.99921C7.52542 9.08459 7.69618 9.15574 7.87406 9.21267V9.84591C7.87406 10.2372 8.19424 10.5574 8.58557 10.5574C8.9769 10.5574 9.29708 10.2372 9.29708 9.84591V9.21267C9.47496 9.16286 9.64572 9.09171 9.80936 8.99921L10.2576 9.44746C10.3999 9.58976 10.5778 9.6538 10.7628 9.6538C10.9478 9.6538 11.1257 9.58265 11.268 9.44746C11.5454 9.16997 11.5454 8.72172 11.268 8.44424L10.8197 7.99599H10.8055ZM7.44004 6.77219C7.44004 6.14606 7.94521 5.64089 8.57134 5.64089C9.19747 5.64089 9.70264 6.14606 9.70264 6.77219C9.70264 7.39832 9.19747 7.90349 8.57134 7.90349C7.94521 7.90349 7.44004 7.39832 7.44004 6.77219Z" fill="white"/>
</g>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="agent" transform="translate(2.5 1)">
<path d="M3.3178 20.9524V15.7184C3.31774 15.7136 3.31456 15.6845 3.28404 15.6468L3.24312 15.6069C1.26589 13.9971 0 11.5398 0 8.78813C0.000122972 3.93464 3.93464 0.000122969 8.78813 0C13.5146 0 17.3698 3.73066 17.5691 8.40858C17.5712 8.45846 17.5841 8.48161 17.5865 8.48531L19.3226 11.089C19.7937 11.7956 19.6309 12.7481 18.9513 13.2579L17.5998 14.2707C17.5851 14.2819 17.5763 14.2996 17.5763 14.3178V15.4237C17.576 17.2235 16.1176 18.682 14.3178 18.6822H13.2119C13.1798 18.6822 13.1537 18.7085 13.1536 18.7405V20.9524C13.1536 21.5309 12.6845 21.9999 12.1059 22C11.5274 22 11.0583 21.531 11.0583 20.9524V18.7405C11.0584 17.5513 12.0226 16.587 13.2119 16.587H14.3178C14.9604 16.5868 15.4808 16.0663 15.481 15.4237V14.3178C15.481 13.64 15.8006 13.0016 16.3424 12.595L17.3195 11.8614L15.8432 9.64853C15.603 9.28835 15.4913 8.88291 15.4749 8.49758C15.323 4.93639 12.3872 2.09524 8.78813 2.09524C5.09181 2.09536 2.09536 5.09181 2.09524 8.78813C2.09524 10.883 3.05647 12.7533 4.56594 13.9822C5.0571 14.3822 5.41299 15.0012 5.41304 15.7184V20.9524C5.41304 21.5309 4.94385 21.9998 4.36542 22C3.78684 22 3.3178 21.531 3.3178 20.9524Z" fill="currentColor"/>
<path d="M9.79012 6.5163L9.31429 5.27923C9.23058 5.06159 9.02158 4.91799 8.78836 4.91799C8.55514 4.91799 8.34614 5.06159 8.26243 5.27923L7.7866 6.5163C7.56194 7.10037 7.10038 7.56194 6.5163 7.78659L5.27923 8.26239C5.06159 8.34609 4.91799 8.55519 4.91799 8.78836C4.91799 9.02158 5.06159 9.23058 5.27923 9.31429L6.5163 9.79012C7.10037 10.0148 7.56194 10.4764 7.7866 11.0604L8.26243 12.2975C8.34614 12.5151 8.55514 12.6587 8.78836 12.6587C9.02158 12.6587 9.23058 12.5151 9.31429 12.2975L9.79012 11.0604C10.0148 10.4764 10.4764 10.0148 11.0604 9.79012L12.2975 9.31429C12.5151 9.23058 12.6587 9.02158 12.6587 8.78836C12.6587 8.55519 12.5151 8.34609 12.2975 8.26239L11.0604 7.78659C10.4764 7.56194 10.0148 7.10038 9.79012 6.5163Z" fill="currentColor"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,7 +1,10 @@
{
"prefix": "custom-public",
"lastModified": 1781246368,
"lastModified": 1781515983,
"icons": {
"agent-building-blocks": {
"body": "<path fill=\"#155AEF\" fill-rule=\"evenodd\" d=\"M8.303 1.546c.178-.045.364-.051.544-.017c.23.043.432.167.573.246l3.757 2.113c.12.067.29.156.433.289l.06.06c.12.131.21.288.267.457c.07.215.063.445.063.6V9.56c0 .146.007.36-.056.563q-.055.181-.162.338l-.075.1c-.137.163-.32.274-.442.353l-5.013 3.259c-.135.088-.33.224-.556.282a1.3 1.3 0 0 1-.543.017c-.23-.043-.433-.166-.573-.245l-3.757-2.114c-.136-.077-.34-.182-.493-.35a1.3 1.3 0 0 1-.267-.456C1.993 11.09 2 10.86 2 10.704V6.441c0-.146-.007-.36.055-.563l.043-.118a1.3 1.3 0 0 1 .195-.32l.053-.059c.128-.131.282-.225.389-.294L7.86 1.755c.122-.078.273-.165.443-.209m-4.97 9.158l.001.164l.033.02l.11.062l3.264 1.836v-1.137L3.333 9.732zm4.741.917v1.076l4.464-2.901l.098-.065l.029-.02v-.034l.001-.118v-.923zm-4.74-3.419L6.74 10.12V8.982L3.333 7.066zm4.74.752v1.076l4.592-2.985V5.969zm.51-6.08l-4.631 3.01l3.429 1.93l4.664-3.032l-3.28-1.846l-.15-.082z\" clip-rule=\"evenodd\"/>"
},
"avatar-user": {
"body": "<g fill=\"none\"><g clip-path=\"url(#svgID0)\"><rect width=\"512\" height=\"512\" fill=\"#B2DDFF\" rx=\"256\"/><circle cx=\"256\" cy=\"196\" r=\"84\" fill=\"#fff\" opacity=\".68\"/><ellipse cx=\"256\" cy=\"583.5\" fill=\"#fff\" opacity=\".68\" rx=\"266\" ry=\"274.5\"/></g><defs><clipPath id=\"svgID0\"><rect width=\"512\" height=\"512\" fill=\"#fff\" rx=\"256\"/></clipPath></defs></g>",
"width": 512,

View File

@ -1,7 +1,7 @@
{
"prefix": "custom-public",
"name": "Dify Custom Public",
"total": 144,
"total": 145,
"version": "0.0.0-private",
"author": {
"name": "LangGenius, Inc.",
@ -13,12 +13,12 @@
"url": "https://github.com/langgenius/dify/blob/main/LICENSE"
},
"samples": [
"agent-building-blocks",
"avatar-user",
"billing-ar-cube-1",
"billing-asterisk",
"billing-aws-marketplace-dark",
"billing-aws-marketplace-light",
"billing-azure"
"billing-aws-marketplace-light"
],
"palette": false
}

View File

@ -1,7 +1,29 @@
{
"prefix": "custom-vender",
"lastModified": 1781246368,
"lastModified": 1781515983,
"icons": {
"agent-v2-access-point": {
"body": "<g fill=\"none\"><path d=\"M7.5 11.25C7.91421 11.25 8.25 11.5858 8.25 12V14.25C8.25 14.6642 7.91421 15 7.5 15C7.08579 15 6.75 14.6642 6.75 14.25V12C6.75 11.5858 7.08579 11.25 7.5 11.25Z\" fill=\"currentColor\"/><path d=\"M2.19653 2.19653C2.48937 1.90372 2.96418 1.90382 3.25708 2.19653L8.03027 6.96973C8.09162 7.03108 8.13966 7.10082 8.17529 7.1748C8.19164 7.20869 8.20587 7.24378 8.21704 7.28027C8.24638 7.37633 8.25641 7.477 8.24634 7.57617C8.23743 7.66451 8.21216 7.74788 8.17529 7.82446C8.13963 7.89868 8.09176 7.96874 8.03027 8.03027L3.25708 12.8035C2.96419 13.096 2.48932 13.0962 2.19653 12.8035C1.90394 12.5107 1.90405 12.0358 2.19653 11.7429L5.68945 8.25H0.75C0.335786 8.25 0 7.91421 0 7.5C0 7.08579 0.335786 6.75 0.75 6.75H5.68945L2.19653 3.25708C1.90389 2.96423 1.90388 2.48937 2.19653 2.19653Z\" fill=\"currentColor\"/><path d=\"M10.1521 10.1521C10.445 9.85921 10.9198 9.85921 11.2126 10.1521L12.8035 11.7429C13.096 12.0358 13.0962 12.5107 12.8035 12.8035C12.5107 13.0962 12.0358 13.096 11.7429 12.8035L10.1521 11.2126C9.85921 10.9198 9.85922 10.445 10.1521 10.1521Z\" fill=\"currentColor\"/><path d=\"M14.25 6.75C14.6642 6.75 15 7.08579 15 7.5C15 7.91421 14.6642 8.25 14.25 8.25H12C11.5858 8.25 11.25 7.91421 11.25 7.5C11.25 7.08579 11.5858 6.75 12 6.75H14.25Z\" fill=\"currentColor\"/><path d=\"M11.7422 2.19653C12.035 1.90387 12.5098 1.90406 12.8027 2.19653C13.0956 2.4894 13.0955 2.96419 12.8027 3.25708L11.2119 4.8479C10.919 5.14079 10.4443 5.1408 10.1514 4.8479C9.85883 4.55497 9.85858 4.08013 10.1514 3.78735L11.7422 2.19653Z\" fill=\"currentColor\"/><path d=\"M7.5 0C7.91421 0 8.25 0.335786 8.25 0.75V3C8.25 3.41421 7.91421 3.75 7.5 3.75C7.08579 3.75 6.75 3.41421 6.75 3V0.75C6.75 0.335786 7.08579 0 7.5 0Z\" fill=\"currentColor\"/></g>",
"width": 15,
"height": 15
},
"agent-v2-end-user-auth": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z\" fill=\"currentColor\"/><path d=\"M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z\" fill=\"currentColor\"/></g>"
},
"agent-v2-plan": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M17 0C17.5523 0 18 0.447715 18 1V6C18 6.55228 17.5523 7 17 7H12C11.4477 7 11 6.55228 11 6V4.5H6.94629C5.92438 4.50039 5.56101 5.85276 6.44531 6.36523L12.5576 9.90332C15.2116 11.4402 14.1206 15.4996 11.0537 15.5H7V17C7 17.5523 6.55228 18 6 18H1C0.447715 18 0 17.5523 0 17V12C0 11.4477 0.447715 11 1 11H6C6.55228 11 7 11.4477 7 12V13.5H11.0537C12.0756 13.4996 12.4394 12.1472 11.5557 11.6348L5.44336 8.09668C2.789 6.55983 3.87917 2.50039 6.94629 2.5H11V1C11 0.447715 11.4477 0 12 0H17ZM2 16H5V13H2V16ZM13 5H16V2H13V5Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"agent-v2-prompt-insert": {
"body": "<g fill=\"none\"><path d=\"M2.91669 1.16669C1.95019 1.16669 1.16669 1.95019 1.16669 2.91669V11.0834C1.16669 12.0499 1.95019 12.8334 2.91669 12.8334H11.0834C12.0499 12.8334 12.8334 12.0499 12.8334 11.0834V2.91669C12.8334 1.95019 12.0499 1.16669 11.0834 1.16669H2.91669ZM2.33335 2.91669C2.33335 2.59452 2.59452 2.33335 2.91669 2.33335H11.0834C11.4055 2.33335 11.6667 2.59452 11.6667 2.91669V11.0834C11.6667 11.4055 11.4055 11.6667 11.0834 11.6667H2.91669C2.59452 11.6667 2.33335 11.4055 2.33335 11.0834V2.91669ZM5.67188 10.5L9.67186 3.50002H8.32815L4.32817 10.5H5.67188Z\" fill=\"currentColor\"/></g>",
"width": 14,
"height": 14
},
"agent-v2-robot-3": {
"body": "<g fill=\"none\"><path d=\"M6.25 6.875C6.82523 6.875 7.29167 7.34128 7.29167 7.91667V9.16667C7.29167 9.74205 6.82523 10.2083 6.25 10.2083C5.67477 10.2083 5.20833 9.74205 5.20833 9.16667V7.91667C5.20833 7.34128 5.67477 6.875 6.25 6.875Z\" fill=\"currentColor\"/><path d=\"M10.4167 6.875C10.992 6.875 11.4583 7.34135 11.4583 7.91667V9.16667C11.4583 9.74199 10.992 10.2083 10.4167 10.2083C9.84135 10.2083 9.375 9.74199 9.375 9.16667V7.91667C9.375 7.34135 9.84135 6.875 10.4167 6.875Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8.33333 0C9.13875 0 9.79167 0.652918 9.79167 1.45833C9.79167 2.02329 9.46964 2.51173 8.99984 2.75391V3.33822C9.38912 3.34279 9.77995 3.35006 10.175 3.36263C11.6983 3.41112 12.7377 3.42425 13.6401 3.90951C14.375 4.30477 15.0255 4.97655 15.3971 5.72347C15.5468 6.02442 15.6427 6.33532 15.7056 6.66667H15.8333C16.2936 6.66667 16.6667 7.03976 16.6667 7.5V10C16.6667 10.4602 16.2936 10.8333 15.8333 10.8333H15.8285C15.8235 11.2254 15.813 11.5735 15.7869 11.8831C15.7386 12.4571 15.6361 12.9628 15.3971 13.4432C15.0254 14.1901 14.3749 14.8619 13.6401 15.2572C12.7377 15.7424 11.6982 15.7556 10.175 15.804C8.93336 15.8436 7.73328 15.8436 6.4917 15.804C4.96843 15.7556 3.92896 15.7424 3.02653 15.2572C2.29178 14.8619 1.64121 14.1902 1.26953 13.4432C1.03058 12.9628 0.928072 12.4571 0.87972 11.8831C0.853642 11.5735 0.843216 11.2254 0.838216 10.8333H0.833333C0.373096 10.8333 0 10.4602 0 10V7.5C0 7.03976 0.373096 6.66667 0.833333 6.66667H0.9611C1.02392 6.33532 1.11984 6.02442 1.26953 5.72347C1.64119 4.97649 2.29177 4.30475 3.02653 3.90951C3.92895 3.42425 4.96837 3.41112 6.4917 3.36263C6.88671 3.35006 7.27754 3.34279 7.66683 3.33822V2.75391C7.19703 2.51173 6.875 2.02329 6.875 1.45833C6.875 0.652918 7.52792 0 8.33333 0ZM10.1213 5.02848C8.91522 4.9901 7.75142 4.9901 6.54541 5.02848C4.85908 5.08217 4.29323 5.12091 3.81592 5.3776C3.38476 5.60954 2.98015 6.02734 2.76204 6.46566C2.65217 6.68652 2.57959 6.96168 2.54069 7.4235C2.50069 7.89854 2.5 8.50363 2.5 9.37825V9.78841C2.5 10.663 2.50069 11.2681 2.54069 11.7432C2.57959 12.205 2.65215 12.4801 2.76204 12.701C2.98015 13.1393 3.38475 13.5571 3.81592 13.7891C4.29321 14.0458 4.85904 14.0845 6.54541 14.1382C7.75141 14.1766 8.91523 14.1766 10.1213 14.1382C11.8075 14.0845 12.3734 14.0458 12.8507 13.7891C13.2819 13.5572 13.6865 13.1394 13.9046 12.701C14.0145 12.4801 14.0871 12.205 14.126 11.7432C14.166 11.2681 14.1667 10.663 14.1667 9.78841V9.37825C14.1667 8.50363 14.166 7.89854 14.126 7.4235C14.0871 6.96168 14.0145 6.68652 13.9046 6.46566C13.6865 6.02729 13.2819 5.60951 12.8507 5.3776C12.3734 5.12091 11.8075 5.08217 10.1213 5.02848Z\" fill=\"currentColor\"/></g>",
"width": 17
},
"features-citations": {
"body": "<g fill=\"none\"><path d=\"M1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12ZM7 11.9702V14.958H11.0356V11.2339H8.8125C8.78418 10.8185 8.85498 10.4173 9.0249 10.0303C9.35531 9.29395 10.002 8.77474 10.9648 8.47266V7C9.67155 7.25488 8.68506 7.79297 8.00537 8.61426C7.33512 9.43555 7 10.5542 7 11.9702ZM15.0391 10.0586C15.3695 9.29395 16.0114 8.7653 16.9648 8.47266V7C15.7093 7.25488 14.7323 7.78825 14.0337 8.6001C13.3446 9.41195 13 10.5353 13 11.9702V14.958H17.0356V11.2339H14.8125C14.7747 10.8563 14.8503 10.4645 15.0391 10.0586Z\" fill=\"currentColor\"/></g>",
"width": 24,
@ -811,6 +833,16 @@
"width": 24,
"height": 24
},
"main-nav-roster": {
"body": "<g fill=\"none\"><path d=\"M7.875 11.8125C7.875 13.1587 6.7837 14.25 5.4375 14.25C4.0913 14.25 3 13.1587 3 11.8125C3 10.4663 4.0913 9.375 5.4375 9.375C6.7837 9.375 7.875 10.4663 7.875 11.8125Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M2.25 15.75L5.625 14.25\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M1.5 11.25L3 10.875\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M5.41699 12.5625C5.83121 12.5625 6.16699 12.2267 6.16699 11.8125C6.16699 11.3983 5.83121 11.0625 5.41699 11.0625C5.00278 11.0625 4.66699 11.3983 4.66699 11.8125C4.66699 12.2267 5.00278 12.5625 5.41699 12.5625Z\" fill=\"currentColor\"/><path d=\"M13.125 11.25L12.4956 8.10292C12.4255 7.75237 12.1177 7.5 11.7601 7.5H11.625C10.7966 7.5 10.125 8.17155 10.125 9V12.3939C10.125 13.419 10.956 14.25 11.9811 14.25C12.2408 14.25 12.4976 14.3045 12.7349 14.41L15.75 15.75\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M10.875 7.5V3.75C10.875 2.92157 11.5466 2.25 12.375 2.25H12.5394C12.8836 2.25 13.1836 2.48422 13.267 2.81811L15.3332 11.0833L16.5 11.625\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M7.125 9.75V3C7.125 2.58579 6.78921 2.25 6.375 2.25H5.52089C5.14964 2.25 4.83426 2.5216 4.77919 2.88875L3.75 9.75\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></g>",
"width": 18,
"height": 18
},
"main-nav-roster-active": {
"body": "<g fill=\"none\"><path d=\"M11.9362 1.5C12.4177 1.50006 12.8411 1.81975 12.9727 2.28296L15.4651 11.061C15.5117 11.2251 15.6125 11.3687 15.7515 11.4675L16.1235 11.7319C16.3544 11.8963 16.4728 12.1769 16.4297 12.4571L15.9206 15.7669C15.8394 16.2948 15.2475 16.5708 14.7905 16.2942L12.8006 15.0893C12.6837 15.0186 12.5618 14.9546 12.4307 14.9165C12.2411 14.8615 12.0443 14.833 11.8462 14.833C10.6885 14.833 9.75 13.8945 9.75 12.7368V9.14722C9.75 8.23747 10.4875 7.5 11.3972 7.5C11.5688 7.5 11.7164 7.62113 11.7503 7.78928L12.3824 10.9483L12.4043 11.0215C12.4721 11.182 12.6457 11.2781 12.8233 11.2427C13.0009 11.2072 13.1242 11.0515 13.125 10.8772L13.1177 10.8017L12.4856 7.6428C12.3818 7.12384 11.9265 6.75002 11.3972 6.75C11.08 6.75 10.7771 6.81165 10.5 6.92359V2.93628C10.5 2.14312 11.1431 1.5 11.9362 1.5Z\" fill=\"currentColor\"/><path d=\"M2.28761 11.1211C2.263 11.2855 2.25026 11.4538 2.25026 11.625C2.25026 13.3862 3.59948 14.8313 5.32057 14.9854L3.0757 16.3674C2.65801 16.6245 2.10961 16.418 1.96534 15.9492L0.926773 12.5742C0.823558 12.2388 0.967018 11.876 1.27174 11.7019L2.28761 11.1211Z\" fill=\"currentColor\"/><path d=\"M6.42041 1.5C7.01664 1.5 7.49997 1.98337 7.49997 2.57959V8.81835C6.96373 8.4594 6.31878 8.25 5.62501 8.25C4.91854 8.25 4.26271 8.46705 3.7207 8.83815L5.01124 2.6455C5.15039 1.97822 5.73876 1.50007 6.42041 1.5Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M5.625 9C7.07475 9 8.25 10.1753 8.25 11.625C8.25 13.0747 7.07475 14.25 5.625 14.25C4.17525 14.25 3 13.0747 3 11.625C3 10.1753 4.17525 9 5.625 9ZM5.625 10.875C5.21078 10.875 4.875 11.2108 4.875 11.625C4.875 12.0392 5.21078 12.375 5.625 12.375C6.03921 12.375 6.375 12.0392 6.375 11.625C6.375 11.2108 6.03921 10.875 5.625 10.875Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-studio": {
"body": "<g fill=\"none\"><path d=\"M15.8206 2.0275C15.7973 1.82217 15.6238 1.66696 15.4171 1.66675C15.2104 1.66654 15.0365 1.82139 15.0128 2.02667C14.865 3.30836 14.1416 4.03176 12.8599 4.17959C12.6547 4.20326 12.4998 4.37719 12.5 4.58383C12.5003 4.79047 12.6554 4.96408 12.8608 4.98733C14.1243 5.13046 14.8978 5.84689 15.0117 7.12955C15.0304 7.33946 15.2064 7.50032 15.4171 7.50008C15.6278 7.49984 15.8035 7.33859 15.8217 7.12863C15.9311 5.86411 16.6973 5.09787 17.9619 4.98841C18.1718 4.97023 18.3331 4.79461 18.3333 4.58387C18.3336 4.37313 18.1728 4.19715 17.9628 4.17851C16.6802 4.06457 15.9637 3.29101 15.8206 2.0275Z\" fill=\"currentColor\"/><path d=\"M7.29167 9.16659C8.9025 9.16659 10.2083 7.86075 10.2083 6.24992C10.2083 4.63909 8.9025 3.33325 7.29167 3.33325C5.68084 3.33325 4.375 4.63909 4.375 6.24992C4.375 7.86075 5.68084 9.16659 7.29167 9.16659Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M1.66699 16.6667C1.66699 13.9053 3.90557 11.6667 6.66699 11.6667H7.08366\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M9.16634 16.6666L10.833 10.8333H18.333L16.6663 16.6666H9.16634ZM9.16634 16.6666H5.83301\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></g>",
"width": 20,
@ -1295,9 +1327,9 @@
"height": 24
},
"workflow-agent": {
"body": "<g fill=\"none\"><g id=\"agent\"><g id=\"Vector\"><path d=\"M14.7401 5.80454C14.5765 4.77996 14.1638 3.79808 13.5306 2.97273C12.8973 2.14738 12.0648 1.48568 11.1185 1.06589C10.1722 0.646098 9.12632 0.461106 8.08751 0.546487C7.05582 0.624753 6.04548 0.966277 5.17744 1.53548C4.3094 2.09758 3.58366 2.88024 3.09272 3.79808C2.59466 4.70881 2.33852 5.7405 2.33852 6.7793V7.22756L1.25703 9.3692C1.04357 9.80322 1.22145 10.3368 1.65547 10.5574L2.3314 10.8989V12.3006C2.3314 12.82 2.53063 13.3038 2.90061 13.6738C3.2706 14.0367 3.75442 14.243 4.27382 14.243H6.01702V14.7624C6.01702 15.1538 6.3372 15.4739 6.72853 15.4739C7.11986 15.4739 7.44004 15.1538 7.44004 14.7624V13.7094C7.44004 13.2185 7.04159 12.82 6.55065 12.82H4.27382C4.13864 12.82 4.00345 12.7631 3.91095 12.6706C3.81846 12.5781 3.76154 12.4429 3.76154 12.3077V10.5716C3.76154 10.2301 3.56943 9.92417 3.2706 9.77476L2.77254 9.52573L3.66904 7.73984C3.72596 7.61889 3.76154 7.4837 3.76154 7.34851V6.77219C3.76154 5.96818 3.96076 5.17129 4.34498 4.4669C4.72919 3.76251 5.28417 3.15772 5.9601 2.7237C6.63603 2.28968 7.41158 2.02643 8.20847 1.96239C9.00536 1.89835 9.81648 2.04066 10.5493 2.36795C11.2822 2.69524 11.9225 3.20042 12.4135 3.84077C12.8973 4.47402 13.2246 5.23533 13.3456 6.02511C13.4665 6.81488 13.3954 7.63312 13.125 8.38731C12.8617 9.12017 12.4206 9.78187 11.8585 10.3084C11.6735 10.4792 11.5668 10.7139 11.5668 10.9701V14.7624C11.5668 15.1538 11.887 15.4739 12.2783 15.4739C12.6696 15.4739 12.9898 15.1538 12.9898 14.7624V11.1978C13.6515 10.5432 14.1567 9.73918 14.4697 8.87114C14.8184 7.89637 14.918 6.83623 14.7615 5.81165L14.7401 5.80454Z\" fill=\"currentColor\"/><path d=\"M10.8055 7.99599C10.8909 7.83234 10.962 7.66158 11.0189 7.4837H11.6522C12.0435 7.4837 12.3637 7.16352 12.3637 6.77219C12.3637 6.38086 12.0435 6.06068 11.6522 6.06068H11.0189C10.9691 5.8828 10.898 5.71204 10.8055 5.54839L11.2537 5.10014C11.5312 4.82266 11.5312 4.3744 11.2537 4.09692C10.9762 3.81943 10.528 3.81943 10.2505 4.09692L9.80225 4.54517C9.6386 4.45267 9.46784 4.38863 9.28996 4.33171V3.69847C9.28996 3.30714 8.96978 2.98696 8.57845 2.98696C8.18712 2.98696 7.86694 3.30714 7.86694 3.69847V4.33171C7.68907 4.38152 7.5183 4.45267 7.35466 4.54517L6.90641 4.09692C6.62892 3.81943 6.18067 3.81943 5.90318 4.09692C5.62569 4.3744 5.62569 4.82266 5.90318 5.10014L6.35143 5.54839C6.26605 5.71204 6.1949 5.8828 6.13798 6.06068H5.50473C5.1134 6.06068 4.79323 6.38086 4.79323 6.77219C4.79323 7.16352 5.1134 7.4837 5.50473 7.4837H6.13798C6.18778 7.66158 6.25893 7.83234 6.35143 7.99599L5.90318 8.44424C5.62569 8.72172 5.62569 9.16997 5.90318 9.44746C6.04548 9.58976 6.22336 9.6538 6.40835 9.6538C6.59334 9.6538 6.77122 9.58265 6.91352 9.44746L7.36177 8.99921C7.52542 9.08459 7.69618 9.15574 7.87406 9.21267V9.84591C7.87406 10.2372 8.19424 10.5574 8.58557 10.5574C8.9769 10.5574 9.29708 10.2372 9.29708 9.84591V9.21267C9.47496 9.16286 9.64572 9.09171 9.80936 8.99921L10.2576 9.44746C10.3999 9.58976 10.5778 9.6538 10.7628 9.6538C10.9478 9.6538 11.1257 9.58265 11.268 9.44746C11.5454 9.16997 11.5454 8.72172 11.268 8.44424L10.8197 7.99599H10.8055ZM7.44004 6.77219C7.44004 6.14606 7.94521 5.64089 8.57134 5.64089C9.19747 5.64089 9.70264 6.14606 9.70264 6.77219C9.70264 7.39832 9.19747 7.90349 8.57134 7.90349C7.94521 7.90349 7.44004 7.39832 7.44004 6.77219Z\" fill=\"currentColor\"/></g></g></g>",
"width": 16,
"height": 16
"body": "<g fill=\"none\"><g id=\"agent\" transform=\"translate(2.5 1)\"><path d=\"M3.3178 20.9524V15.7184C3.31774 15.7136 3.31456 15.6845 3.28404 15.6468L3.24312 15.6069C1.26589 13.9971 0 11.5398 0 8.78813C0.000122972 3.93464 3.93464 0.000122969 8.78813 0C13.5146 0 17.3698 3.73066 17.5691 8.40858C17.5712 8.45846 17.5841 8.48161 17.5865 8.48531L19.3226 11.089C19.7937 11.7956 19.6309 12.7481 18.9513 13.2579L17.5998 14.2707C17.5851 14.2819 17.5763 14.2996 17.5763 14.3178V15.4237C17.576 17.2235 16.1176 18.682 14.3178 18.6822H13.2119C13.1798 18.6822 13.1537 18.7085 13.1536 18.7405V20.9524C13.1536 21.5309 12.6845 21.9999 12.1059 22C11.5274 22 11.0583 21.531 11.0583 20.9524V18.7405C11.0584 17.5513 12.0226 16.587 13.2119 16.587H14.3178C14.9604 16.5868 15.4808 16.0663 15.481 15.4237V14.3178C15.481 13.64 15.8006 13.0016 16.3424 12.595L17.3195 11.8614L15.8432 9.64853C15.603 9.28835 15.4913 8.88291 15.4749 8.49758C15.323 4.93639 12.3872 2.09524 8.78813 2.09524C5.09181 2.09536 2.09536 5.09181 2.09524 8.78813C2.09524 10.883 3.05647 12.7533 4.56594 13.9822C5.0571 14.3822 5.41299 15.0012 5.41304 15.7184V20.9524C5.41304 21.5309 4.94385 21.9998 4.36542 22C3.78684 22 3.3178 21.531 3.3178 20.9524Z\" fill=\"currentColor\"/><path d=\"M9.79012 6.5163L9.31429 5.27923C9.23058 5.06159 9.02158 4.91799 8.78836 4.91799C8.55514 4.91799 8.34614 5.06159 8.26243 5.27923L7.7866 6.5163C7.56194 7.10037 7.10038 7.56194 6.5163 7.78659L5.27923 8.26239C5.06159 8.34609 4.91799 8.55519 4.91799 8.78836C4.91799 9.02158 5.06159 9.23058 5.27923 9.31429L6.5163 9.79012C7.10037 10.0148 7.56194 10.4764 7.7866 11.0604L8.26243 12.2975C8.34614 12.5151 8.55514 12.6587 8.78836 12.6587C9.02158 12.6587 9.23058 12.5151 9.31429 12.2975L9.79012 11.0604C10.0148 10.4764 10.4764 10.0148 11.0604 9.79012L12.2975 9.31429C12.5151 9.23058 12.6587 9.02158 12.6587 8.78836C12.6587 8.55519 12.5151 8.34609 12.2975 8.26239L11.0604 7.78659C10.4764 7.56194 10.0148 7.10038 9.79012 6.5163Z\" fill=\"currentColor\"/></g></g>",
"width": 24,
"height": 24
},
"workflow-answer": {
"body": "<g fill=\"none\"><g id=\"icons/answer\"><path id=\"Vector (Stroke)\" fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M3.50114 1.67701L10.5011 1.677C11.5079 1.677 12.3241 2.49311 12.3241 3.49992V9.35414C12.3241 10.3609 11.5079 11.177 10.5012 11.1771H8.9954L7.41734 12.4845C7.17339 12.6866 6.81987 12.6856 6.57708 12.4821L5.02026 11.1771H3.50114C2.49436 11.1771 1.67822 10.3608 1.67822 9.35414V3.49993C1.67822 2.49316 2.49437 1.67701 3.50114 1.67701ZM10.5011 2.9895L3.50114 2.98951C3.21924 2.98951 2.99072 3.21803 2.99072 3.49993V9.35414C2.99072 9.63601 3.21926 9.86455 3.50114 9.86455H5.04675C5.33794 9.86455 5.61984 9.96705 5.84302 10.1541L7.00112 11.1249L8.17831 10.1496C8.40069 9.96537 8.68041 9.86455 8.96916 9.86455H10.5011C10.5011 9.86455 10.5011 9.86455 10.5011 9.86455C10.783 9.8645 11.0116 9.63592 11.0116 9.35414V3.49992C11.0116 3.21806 10.7831 2.9895 10.5011 2.9895ZM9.06809 4.93171C9.32437 5.18799 9.32437 5.60351 9.06809 5.85979L7.02642 7.90146C6.77014 8.15774 6.35464 8.15774 6.09835 7.90146L5.22333 7.02646C4.96704 6.77019 4.96704 6.35467 5.22332 6.09839C5.4796 5.8421 5.89511 5.8421 6.15139 6.09837L6.56238 6.50935L8.14001 4.93171C8.3963 4.67543 8.81181 4.67543 9.06809 4.93171Z\" fill=\"currentColor\"/></g></g>",

View File

@ -1,4 +1,4 @@
export type IconifyJSON = {
export interface IconifyJSON {
prefix: string
icons: Record<string, IconifyIcon>
aliases?: Record<string, IconifyAlias>
@ -7,7 +7,7 @@ export type IconifyJSON = {
lastModified?: number
}
export type IconifyIcon = {
export interface IconifyIcon {
body: string
left?: number
top?: number
@ -18,11 +18,11 @@ export type IconifyIcon = {
vFlip?: boolean
}
export type IconifyAlias = {
export interface IconifyAlias extends Omit<IconifyIcon, 'body'> {
parent: string
} & Omit<IconifyIcon, 'body'>
}
export type IconifyInfo = {
export interface IconifyInfo {
prefix: string
name: string
total: number
@ -40,11 +40,11 @@ export type IconifyInfo = {
palette?: boolean
}
export type IconifyMetaData = {
export interface IconifyMetaData {
[key: string]: unknown
}
export type IconifyChars = {
export interface IconifyChars {
[key: string]: string
}

View File

@ -1,8 +1,8 @@
'use strict'
const chars = require('./chars.json')
const icons = require('./icons.json')
const info = require('./info.json')
const metadata = require('./metadata.json')
const chars = require('./chars.json')
module.exports = { icons, info, metadata, chars }

View File

@ -1,6 +1,6 @@
import chars from './chars.json' with { type: 'json' }
import icons from './icons.json' with { type: 'json' }
import info from './info.json' with { type: 'json' }
import metadata from './metadata.json' with { type: 'json' }
import chars from './chars.json' with { type: 'json' }
export { chars, icons, info, metadata }
export { icons, info, metadata, chars }

View File

@ -1,7 +1,7 @@
{
"prefix": "custom-vender",
"name": "Dify Custom Vender",
"total": 319,
"total": 326,
"version": "0.0.0-private",
"author": {
"name": "LangGenius, Inc.",
@ -13,12 +13,12 @@
"url": "https://github.com/langgenius/dify/blob/main/LICENSE"
},
"samples": [
"features-citations",
"features-content-moderation",
"features-document",
"features-folder-upload",
"features-love-message",
"features-message-fast"
"agent-v2-access-point",
"agent-v2-end-user-auth",
"agent-v2-plan",
"agent-v2-prompt-insert",
"agent-v2-robot-3",
"features-citations"
],
"palette": false
}

View File

@ -6,7 +6,7 @@ import Loading from '@/app/components/base/loading'
import { redirect, usePathname } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
const datasetOperatorRedirectRoutes = ['/', '/apps', '/app', '/snippets', '/explore', '/tools', '/integrations'] as const
const datasetOperatorRedirectRoutes = ['/', '/apps', '/app', '/snippets', '/roster', '/explore', '/tools', '/integrations'] as const
const isPathUnderRoute = (pathname: string, route: string) => pathname === route || pathname.startsWith(`${route}/`)

View File

@ -0,0 +1,13 @@
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
type PageProps = {
params: Promise<{ agentId: string }>
}
export default async function Page({
params,
}: PageProps) {
const { agentId } = await params
return <AgentDetailPage agentId={agentId} section="access" />
}

View File

@ -0,0 +1,13 @@
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
type PageProps = {
params: Promise<{ agentId: string }>
}
export default async function Page({
params,
}: PageProps) {
const { agentId } = await params
return <AgentDetailPage agentId={agentId} section="configure" />
}

View File

@ -0,0 +1,20 @@
import type { ReactNode } from 'react'
import { AgentDetailLayout } from '@/features/agent-v2/agent-detail/layout'
type LayoutProps = {
children: ReactNode
params: Promise<{ agentId: string }>
}
export default async function Layout({
children,
params,
}: LayoutProps) {
const { agentId } = await params
return (
<AgentDetailLayout agentId={agentId}>
{children}
</AgentDetailLayout>
)
}

View File

@ -0,0 +1,13 @@
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
type PageProps = {
params: Promise<{ agentId: string }>
}
export default async function Page({
params,
}: PageProps) {
const { agentId } = await params
return <AgentDetailPage agentId={agentId} section="logs" />
}

View File

@ -0,0 +1,13 @@
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
type PageProps = {
params: Promise<{ agentId: string }>
}
export default async function Page({
params,
}: PageProps) {
const { agentId } = await params
return <AgentDetailPage agentId={agentId} section="monitoring" />
}

View File

@ -0,0 +1,13 @@
import { redirect } from '@/next/navigation'
type PageProps = {
params: Promise<{ agentId: string }>
}
export default async function Page({
params,
}: PageProps) {
const { agentId } = await params
redirect(`/roster/agent/${agentId}/configure`)
}

View File

@ -0,0 +1,5 @@
import RosterPage from '@/features/agent-v2/roster/page'
export default function Page() {
return <RosterPage />
}

View File

@ -1,4 +1,4 @@
import { redirect } from 'next/navigation'
import { redirect } from '@/next/navigation'
const Page = async (props: {
params: Promise<{ snippetId: string }>

View File

@ -186,6 +186,20 @@ describe('SettingBuiltInTool', () => {
expect(screen.getByTestId('mock-form')).toBeInTheDocument()
})
it('should render a masked drawer with balanced vertical offsets', async () => {
const { baseElement } = renderComponent()
await waitFor(() => {
expect(screen.getByTestId('mock-form')).toBeInTheDocument()
})
expect(baseElement.querySelector('.bg-background-overlay')).toBeInTheDocument()
const drawerPopup = baseElement.querySelector('[role="dialog"]')
expect(drawerPopup).toHaveClass(
'data-[swipe-direction=right]:top-6',
'data-[swipe-direction=right]:bottom-6',
)
})
it('should call onSave with updated values when save button clicked', async () => {
const { onSave } = renderComponent()
await waitFor(() => expect(screen.getByTestId('mock-form')).toBeInTheDocument())

View File

@ -181,9 +181,9 @@ const SettingBuiltInTool: FC<Props> = ({
}}
>
<DrawerPortal>
<DrawerBackdrop className="bg-transparent" />
<DrawerBackdrop className="bg-background-overlay" />
<DrawerViewport>
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-6 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-6 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
{isLoading && <Loading type="app" />}
{!isLoading && (

View File

@ -19,6 +19,7 @@ import { useInfiniteDatasets } from '@/service/knowledge/use-dataset'
type ISelectDataSetProps = {
isShow: boolean
modal?: boolean
onClose: () => void
selectedIds: string[]
onSelect: (dataSet: DataSet[]) => void
@ -26,6 +27,7 @@ type ISelectDataSetProps = {
const SelectDataSet: FC<ISelectDataSetProps> = ({
isShow,
modal,
onClose,
selectedIds,
onSelect,
@ -90,8 +92,8 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
}, [handleClose])
return (
<Dialog open={isShow} onOpenChange={handleOpenChange}>
<DialogContent className="w-100 overflow-hidden">
<Dialog modal={modal} open={isShow} onOpenChange={handleOpenChange}>
<DialogContent backdropProps={{ forceRender: true }} className="w-100 overflow-hidden">
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t('feature.dataSet.selectTitle', { ns: 'appDebug' })}
</DialogTitle>

View File

@ -30,6 +30,7 @@ import { RetrievalChangeTip, RetrievalSection } from './retrieval-section'
type SettingsModalProps = {
currentDataset: DataSet
height?: string
onCancel: () => void
onSave: (newDataset: DataSet) => void
}
@ -44,6 +45,7 @@ const labelClass = `
const SettingsModal: FC<SettingsModalProps> = ({
currentDataset,
height = 'calc(100vh - 72px)',
onCancel,
onSave,
}) => {
@ -186,9 +188,9 @@ const SettingsModal: FC<SettingsModalProps> = ({
return (
<div
className="flex w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
className="flex min-h-0 w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
style={{
height: 'calc(100vh - 72px)',
height,
}}
ref={ref}
>

View File

@ -417,15 +417,17 @@ describe('List', () => {
expect(screen.getByRole('button', { name: 'common.operation.create' }))!.toBeInTheDocument()
})
it('should render link to snippets before the create button', () => {
it('should render sort filter before search and the snippets link', () => {
renderList()
const sortButton = screen.getByRole('button', { name: 'Sort by Last modified' })
const searchInput = screen.getByRole('searchbox', { name: 'app.gotoAnything.actions.searchApplications' })
const snippetsLink = screen.getByRole('link', { name: 'app.studio.viewSnippets' })
const createButton = screen.getByRole('button', { name: 'common.operation.create' })
expect(snippetsLink).toHaveAttribute('href', '/snippets')
expect(sortButton.compareDocumentPosition(snippetsLink) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
expect(sortButton.compareDocumentPosition(searchInput) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
expect(searchInput.compareDocumentPosition(snippetsLink) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
expect(snippetsLink.compareDocumentPosition(createButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
})

View File

@ -62,6 +62,7 @@ export function AppListHeaderFilters({
showLeadingIcon={false}
/>
<CreatorsFilter value={creatorIDs} onChange={onCreatorIDsChange} />
<AppSortFilter value={sortBy} onChange={onSortByChange} />
<SearchInput
className="w-50"
value={keywords}
@ -70,7 +71,6 @@ export function AppListHeaderFilters({
/>
</div>
<div className="flex items-center gap-2">
<AppSortFilter value={sortBy} onChange={onSortByChange} />
<Link
href="/snippets"
className="flex h-8 items-center rounded-lg px-3 text-sm font-semibold text-text-secondary outline-hidden hover:bg-state-base-hover hover:text-text-primary focus-visible:ring-2 focus-visible:ring-state-accent-solid"

View File

@ -8,10 +8,18 @@ import { useHover } from 'ahooks'
import { cva } from 'class-variance-authority'
import { init } from 'emoji-mart'
import * as React from 'react'
import { useRef } from 'react'
import { useRef, useSyncExternalStore } from 'react'
init({ data })
const subscribeHydrationState = () => () => {}
const useIsHydrated = () => useSyncExternalStore(
subscribeHydrationState,
() => true,
() => false,
)
type AppIconProps = {
size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' | 'xl' | 'xxl'
rounded?: boolean
@ -105,9 +113,20 @@ const AppIcon: FC<AppIconProps> = ({
}) => {
const isValidImageIcon = iconType === 'image' && imageUrl
const emojiIcon = (icon && icon !== '') ? icon : '🤖'
const Icon = <em-emoji key={emojiIcon} id={emojiIcon} />
const isHydrated = useIsHydrated()
const Icon = isHydrated ? <em-emoji key={emojiIcon} id={emojiIcon} /> : emojiIcon
const wrapperRef = useRef<HTMLSpanElement>(null)
const isHovering = useHover(wrapperRef)
const handleKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
if (!onClick)
return
if (event.key !== 'Enter' && event.key !== ' ')
return
event.preventDefault()
onClick()
}
return (
<span
@ -115,6 +134,9 @@ const AppIcon: FC<AppIconProps> = ({
className={cn(appIconVariants({ size, rounded }), className)}
style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }}
onClick={onClick}
onKeyDown={onClick ? handleKeyDown : undefined}
role={onClick ? 'button' : undefined}
tabIndex={onClick ? 0 : undefined}
>
{
isValidImageIcon

View File

@ -57,6 +57,7 @@ const renderPanel = (props: Partial<{
onClose: () => void
inWorkflow: boolean
showFileUpload: boolean
showAnnotationReply: boolean
}> = {}) => {
return render(
<FeaturesProvider features={defaultFeatures}>
@ -68,6 +69,7 @@ const renderPanel = (props: Partial<{
onClose={props.onClose ?? vi.fn()}
inWorkflow={props.inWorkflow}
showFileUpload={props.showFileUpload}
showAnnotationReply={props.showAnnotationReply}
/>
</FeaturesProvider>,
)
@ -191,5 +193,11 @@ describe('NewFeaturePanel', () => {
expect(screen.queryByText(/feature\.annotation\.title/)).not.toBeInTheDocument()
})
it('should not render AnnotationReply when showAnnotationReply is false', () => {
renderPanel({ isChatMode: true, inWorkflow: false, showAnnotationReply: false })
expect(screen.queryByText(/feature\.annotation\.title/)).not.toBeInTheDocument()
})
})
})

View File

@ -1,3 +1,4 @@
import type { ReactNode } from 'react'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import type { InputVar } from '@/app/components/workflow/types'
import type { PromptVariable } from '@/models/debug'
@ -26,9 +27,14 @@ type Props = Readonly<{
onClose: () => void
inWorkflow?: boolean
showFileUpload?: boolean
showModeration?: boolean
showAnnotationReply?: boolean
promptVariables?: PromptVariable[]
workflowVariables?: InputVar[]
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
title?: ReactNode
description?: ReactNode
drawerClassName?: string
}>
const NewFeaturePanel = ({
@ -39,9 +45,14 @@ const NewFeaturePanel = ({
onClose,
inWorkflow = true,
showFileUpload = true,
showModeration = true,
showAnnotationReply = true,
promptVariables,
workflowVariables,
onAutoAddPromptVariable,
title,
description,
drawerClassName,
}: Props) => {
const { t } = useTranslation()
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
@ -52,13 +63,14 @@ const NewFeaturePanel = ({
show={show}
onClose={onClose}
inWorkflow={inWorkflow}
className={drawerClassName}
>
<div className="flex h-full grow flex-col">
{/* header */}
<div className="flex shrink-0 justify-between p-4 pb-3">
<div>
<div className="system-xl-semibold text-text-primary">{t('common.features', { ns: 'workflow' })}</div>
<div className="body-xs-regular text-text-tertiary">{t('common.featuresDescription', { ns: 'workflow' })}</div>
<div className="system-xl-semibold text-text-primary">{title ?? t('common.features', { ns: 'workflow' })}</div>
<div className="body-xs-regular text-text-tertiary">{description ?? t('common.featuresDescription', { ns: 'workflow' })}</div>
</div>
<DrawerCloseButton
aria-label={t('operation.close', { ns: 'common' })}
@ -93,8 +105,8 @@ const NewFeaturePanel = ({
{isChatMode && (
<Citation disabled={disabled} onChange={onChange} />
)}
{(isChatMode || !inWorkflow) && <Moderation disabled={disabled} onChange={onChange} />}
{!inWorkflow && isChatMode && (
{showModeration && (isChatMode || !inWorkflow) && <Moderation disabled={disabled} onChange={onChange} />}
{showAnnotationReply && !inWorkflow && isChatMode && (
<AnnotationReply disabled={disabled} onChange={onChange} />
)}
</div>

View File

@ -302,6 +302,37 @@ describe('ModerationSettingModal', () => {
}))
})
it('should save the latest preset response when content textarea changes', async () => {
const data: ModerationConfig = {
...defaultData,
config: {
keywords: 'bad',
inputs_config: { enabled: true, preset_response: 'blocked' },
outputs_config: { enabled: false, preset_response: '' },
},
}
await renderModal(
<ModerationSettingModal
data={data}
onCancel={vi.fn()}
onSave={onSave}
/>,
)
fireEvent.change(screen.getByRole('textbox', { name: /feature\.moderation\.modal\.content\.preset/ }), {
target: { value: 'updated blocked response' },
})
fireEvent.click(screen.getByText(/operation\.save/))
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
config: expect.objectContaining({
inputs_config: expect.objectContaining({
preset_response: 'updated blocked response',
}),
}),
}))
})
it('should show api selector when api type is selected', async () => {
await renderModal(
<ModerationSettingModal
@ -702,7 +733,10 @@ describe('ModerationSettingModal', () => {
expect(mockSetShowAccountSettingModal).toHaveBeenCalled()
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider' })
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
payload: 'provider',
onCancelCallback: expect.any(Function),
})
})
it('should not save when OpenAI type is selected but not configured', async () => {

View File

@ -2,6 +2,7 @@ import type { FC } from 'react'
import type { ModerationContentConfig } from '@/models/debug'
import { Switch } from '@langgenius/dify-ui/switch'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
type ModerationContentProps = {
@ -19,57 +20,71 @@ const ModerationContent: FC<ModerationContentProps> = ({
onConfigChange,
}) => {
const { t } = useTranslation()
const [presetResponse, setPresetResponse] = useState(config.preset_response || '')
const handleConfigChange = (field: string, value: boolean | string) => {
if (field === 'preset_response' && typeof value === 'string')
value = value.slice(0, 100)
onConfigChange({ ...config, [field]: value })
onConfigChange({
...config,
preset_response: field === 'preset_response' ? value as string : presetResponse,
[field]: value,
})
}
const handlePresetResponseChange = (value: string) => {
const nextValue = value.slice(0, 100)
setPresetResponse(nextValue)
handleConfigChange('preset_response', nextValue)
}
return (
<div className="py-2">
<div className="rounded-lg border border-components-panel-border bg-components-panel-bg">
<div className="flex h-10 items-center justify-between rounded-lg px-3">
<div className="shrink-0 text-sm font-medium text-text-primary">{title}</div>
<div className="flex grow items-center justify-end">
{
info && (
<div className="mr-2 truncate text-xs text-text-tertiary" title={info}>{info}</div>
)
}
<Switch
size="lg"
checked={config.enabled}
onCheckedChange={v => handleConfigChange('enabled', v)}
/>
</div>
<div className="rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg shadow-xs">
<div className="flex min-h-10 items-center gap-2 px-3 py-2">
<div className="min-w-0 flex-1 system-sm-medium text-text-secondary">{title}</div>
<div className="flex min-w-0 shrink-0 items-center justify-end">
{
info && (
<div className="mr-2 truncate system-xs-regular text-text-tertiary" title={info}>{info}</div>
)
}
<Switch
checked={config.enabled}
onCheckedChange={v => handleConfigChange('enabled', v)}
/>
</div>
{
config.enabled && showPreset && (
<div className="rounded-lg bg-components-panel-bg px-3 pt-1 pb-3">
<div className="flex h-8 items-center justify-between text-[13px] font-medium text-text-secondary">
</div>
{
config.enabled && showPreset && (
<div className="px-3 pt-0.5 pb-3">
<div className="flex h-8 items-center justify-between gap-2">
<span className="system-2xs-medium-uppercase text-text-secondary">
{t('feature.moderation.modal.content.preset', { ns: 'appDebug' })}
<span className="text-xs font-normal text-text-tertiary">{t('feature.moderation.modal.content.supportMarkdown', { ns: 'appDebug' })}</span>
</div>
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
<div className="relative h-20">
<Textarea
aria-label={t('feature.moderation.modal.content.preset', { ns: 'appDebug' }) as string}
value={config.preset_response || ''}
className="size-full resize-none pb-8"
placeholder={t('feature.moderation.modal.content.placeholder', { ns: 'appDebug' }) || ''}
onValueChange={value => handleConfigChange('preset_response', value)}
/>
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary">
<span>{(config.preset_response || '').length}</span>
/
<span className="text-text-tertiary">100</span>
</div>
</span>
<span className="flex shrink-0 items-center gap-0.5 rounded bg-background-section px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
<span className="i-ri-markdown-line size-3" aria-hidden />
{t('feature.moderation.modal.content.supportMarkdown', { ns: 'appDebug' })}
</span>
</div>
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
<div className="relative h-20">
<Textarea
aria-label={t('feature.moderation.modal.content.preset', { ns: 'appDebug' }) as string}
value={presetResponse}
className="size-full resize-none pb-8"
placeholder={t('feature.moderation.modal.content.placeholder', { ns: 'appDebug' }) || ''}
onValueChange={handlePresetResponseChange}
/>
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 system-2xs-medium-uppercase text-text-quaternary">
<span>{presetResponse.length}</span>
/
<span className="text-text-tertiary">100</span>
</div>
</div>
)
}
</div>
</div>
)
}
</div>
)
}

View File

@ -1,4 +1,4 @@
import type { FC } from 'react'
import type { FC, ReactNode } from 'react'
import type { CodeBasedExtensionItem } from '@/models/common'
import type { ModerationConfig, ModerationContentConfig } from '@/models/debug'
import { Button } from '@langgenius/dify-ui/button'
@ -6,7 +6,7 @@ import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { useState } from 'react'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import { ApiBasedExtensionSelector } from '@/app/components/header/account-setting/api-based-extension-page/selector'
@ -27,6 +27,27 @@ type Provider = {
form_schema?: CodeBasedExtensionItem['form_schema']
}
function ProviderIcon({ type }: { type: string }) {
if (type === 'openai_moderation')
return <span className="i-ri-openai-fill size-4 text-text-secondary" aria-hidden />
if (type === 'keywords')
return <span className="i-ri-search-line size-4 text-util-colors-green-green-600" aria-hidden />
return <span className="i-ri-image-line size-4 text-util-colors-violet-violet-600" aria-hidden />
}
function LabeledDivider({ children }: { children: ReactNode }) {
return (
<div className="flex w-full items-center gap-2">
<span className="shrink-0 system-xs-medium-uppercase text-text-tertiary">
{children}
</span>
<Divider bgStyle="gradient" className="my-0 h-px flex-1" />
</div>
)
}
type ModerationSettingModalProps = {
data: ModerationConfig
onCancel: () => void
@ -41,12 +62,27 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
const { t } = useTranslation()
const docLink = useDocLink()
const locale = useLocale()
const { data: modelProviders, isPending: isLoading } = useModelProviders()
const { data: modelProviders, isPending: isLoading, refetch: refetchModelProviders } = useModelProviders()
const localeDataRef = useRef<ModerationConfig>(data)
const [localeData, setLocaleData] = useState<ModerationConfig>(data)
const openIntegrationsSetting = useIntegrationsSetting()
const updateLocaleData = useCallback((
update: ModerationConfig | ((current: ModerationConfig) => ModerationConfig),
options: { render?: boolean } = {},
) => {
const nextLocaleData = typeof update === 'function'
? update(localeDataRef.current)
: update
localeDataRef.current = nextLocaleData
if (options.render !== false)
setLocaleData(nextLocaleData)
}, [])
const handleOpenSettingsModal = () => {
openIntegrationsSetting({
payload: ACCOUNT_SETTING_TAB.PROVIDER,
onCancelCallback: refetchModelProviders,
})
}
const { data: codeBasedExtensionList } = useCodeBasedExtensions('moderation')
@ -85,20 +121,20 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
const currentProvider = providers.find(provider => provider.key === localeData.type)
const handleDataTypeChange = (type: string) => {
let config: undefined | Record<string, any>
let config: undefined | Record<string, string>
const currProvider = providers.find(provider => provider.key === type)
if (systemTypes.findIndex(t => t === type) < 0 && currProvider?.form_schema) {
config = currProvider?.form_schema.reduce((prev, next) => {
prev[next.variable] = next.default
return prev
}, {} as Record<string, any>)
}, {} as Record<string, string>)
}
setLocaleData({
...localeData,
updateLocaleData(current => ({
...current,
type,
config,
})
}))
}
const handleDataKeywordsChange = (value: string) => {
@ -111,43 +147,46 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
return prev
}, [])
setLocaleData({
...localeData,
updateLocaleData(current => ({
...current,
config: {
...localeData.config,
...current.config,
keywords: arr.slice(0, 100).join('\n'),
},
})
}))
}
const handleDataContentChange = (contentType: string, contentConfig: ModerationContentConfig) => {
setLocaleData({
...localeData,
const previousContentConfig = localeDataRef.current.config?.[contentType] as ModerationContentConfig | undefined
const shouldRender = previousContentConfig?.enabled !== contentConfig.enabled
updateLocaleData(current => ({
...current,
config: {
...localeData.config,
...current.config,
[contentType]: contentConfig,
},
})
}), { render: shouldRender })
}
const handleDataApiBasedChange = (apiBasedExtensionId: string) => {
setLocaleData({
...localeData,
updateLocaleData(current => ({
...current,
config: {
...localeData.config,
...current.config,
api_based_extension_id: apiBasedExtensionId,
},
})
}))
}
const handleDataExtraChange = (extraValue: Record<string, string>) => {
setLocaleData({
...localeData,
updateLocaleData(current => ({
...current,
config: {
...localeData.config,
...current.config,
...extraValue,
},
})
}))
}
const formatData = (originData: ModerationConfig) => {
@ -179,115 +218,116 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
}
const handleSave = () => {
const currentLocaleData = localeDataRef.current
const providerForSave = providers.find(provider => provider.key === currentLocaleData.type)
/* v8 ignore next -- UI-invariant guard: same condition is used in Save button disabled logic, so when true handleSave has no user-triggerable invocation path. @preserve */
if (localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured)
if (currentLocaleData.type === 'openai_moderation' && !isOpenAIProviderConfigured)
return
if (!localeData.config?.inputs_config?.enabled && !localeData.config?.outputs_config?.enabled) {
if (!currentLocaleData.config?.inputs_config?.enabled && !currentLocaleData.config?.outputs_config?.enabled) {
toast.error(t('feature.moderation.modal.content.condition', { ns: 'appDebug' }))
return
}
if (localeData.type === 'keywords' && !localeData.config.keywords) {
if (currentLocaleData.type === 'keywords' && !currentLocaleData.config.keywords) {
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'keywords' : '关键词' }))
return
}
if (localeData.type === 'api' && !localeData.config.api_based_extension_id) {
if (currentLocaleData.type === 'api' && !currentLocaleData.config.api_based_extension_id) {
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }))
return
}
if (systemTypes.findIndex(t => t === localeData.type) < 0 && currentProvider?.form_schema) {
for (let i = 0; i < currentProvider.form_schema.length; i++) {
if (!localeData.config?.[currentProvider.form_schema[i]!.variable] && currentProvider.form_schema[i]!.required) {
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i]!.label['en-US'] : currentProvider.form_schema[i]!.label['zh-Hans'] }))
if (systemTypes.findIndex(t => t === currentLocaleData.type) < 0 && providerForSave?.form_schema) {
for (let i = 0; i < providerForSave.form_schema.length; i++) {
if (!currentLocaleData.config?.[providerForSave.form_schema[i]!.variable] && providerForSave.form_schema[i]!.required) {
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? providerForSave.form_schema[i]!.label['en-US'] : providerForSave.form_schema[i]!.label['zh-Hans'] }))
return
}
}
}
if (localeData.config.inputs_config?.enabled && !localeData.config.inputs_config.preset_response && localeData.type !== 'api') {
if (currentLocaleData.config.inputs_config?.enabled && !currentLocaleData.config.inputs_config.preset_response && currentLocaleData.type !== 'api') {
toast.error(t('feature.moderation.modal.content.errorMessage', { ns: 'appDebug' }))
return
}
if (localeData.config.outputs_config?.enabled && !localeData.config.outputs_config.preset_response && localeData.type !== 'api') {
if (currentLocaleData.config.outputs_config?.enabled && !currentLocaleData.config.outputs_config.preset_response && currentLocaleData.type !== 'api') {
toast.error(t('feature.moderation.modal.content.errorMessage', { ns: 'appDebug' }))
return
}
onSave(formatData(localeData))
onSave(formatData(currentLocaleData))
}
return (
<Dialog open>
<DialogContent className="mt-14! w-[600px]! max-w-none! border-none p-6! text-left align-middle">
<div className="flex items-center justify-between">
<div className="title-2xl-semi-bold text-text-primary">{t('feature.moderation.modal.title', { ns: 'appDebug' })}</div>
<DialogContent className="mt-14! w-[600px]! max-w-none! overflow-hidden border-[0.5px]! border-components-panel-border! p-0! text-left align-middle">
<div className="flex items-start gap-2 px-6 pt-6 pr-14 pb-3">
<div className="title-2xl-semi-bold text-text-primary">
{t('feature.moderation.modal.title', { ns: 'appDebug' })}
</div>
<button
type="button"
aria-label={t('operation.close', { ns: 'common' })}
className="cursor-pointer border-none bg-transparent p-1 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
className="absolute top-5 right-5 flex size-8 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
onClick={onCancel}
>
<span className="i-ri-close-line size-4 text-text-tertiary" aria-hidden="true" />
<span className="i-ri-close-line size-[18px]" aria-hidden="true" />
</button>
</div>
<div className="py-2">
<div className="text-sm/9 font-medium text-text-primary">
{t('feature.moderation.modal.provider.title', { ns: 'appDebug' })}
</div>
<div className="grid grid-cols-3 gap-2.5">
{
providers.map(provider => (
<div
<div className="flex flex-col gap-4 px-6 py-3">
<div className="flex flex-col gap-1">
<div className="system-sm-medium text-text-secondary">
{t('feature.moderation.modal.provider.title', { ns: 'appDebug' })}
</div>
<div className="grid grid-cols-3 gap-2">
{providers.map(provider => (
<button
type="button"
key={provider.key}
className={cn(
'flex h-8 cursor-default items-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 system-sm-regular text-text-secondary',
localeData.type !== provider.key && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
localeData.type === provider.key && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg system-sm-medium shadow-xs',
'flex min-h-[68px] flex-col items-start justify-center gap-1.5 rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg px-3 py-2 text-left text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
localeData.type !== provider.key && 'hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
localeData.type === provider.key && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs',
localeData.type === 'openai_moderation' && provider.key === 'openai_moderation' && !isOpenAIProviderConfigured && 'text-text-disabled',
)}
onClick={() => handleDataTypeChange(provider.key)}
>
<div className={cn(
'mr-2 size-4 rounded-full border border-components-radio-border bg-components-radio-bg shadow-xs',
localeData.type === provider.key && 'border-[5px] border-components-radio-border-checked',
)}
>
<div className="flex size-8 items-center justify-center rounded-lg border-[0.5px] border-divider-regular bg-background-default-dodge">
<ProviderIcon type={provider.key} />
</div>
{provider.name}
</div>
))
}
</div>
{
!isLoading && !isOpenAIProviderConfigured && localeData.type === 'openai_moderation' && (
<span className="w-full truncate system-xs-regular">
{provider.name}
</span>
</button>
))}
</div>
{!isLoading && !isOpenAIProviderConfigured && localeData.type === 'openai_moderation' && (
<div className="mt-2 flex items-center rounded-lg border border-[#FEF0C7] bg-[#FFFAEB] px-3 py-2">
<span className="mr-1 i-custom-vender-line-general-info-circle h-4 w-4 text-[#F79009]" />
<div className="flex items-center text-xs font-medium text-gray-700">
{t('feature.moderation.modal.openaiNotConfig.before', { ns: 'appDebug' })}
<span
<button
type="button"
className="cursor-pointer text-primary-600"
onClick={handleOpenSettingsModal}
>
&nbsp;
&nbsp;
{t('settings.provider', { ns: 'common' })}
&nbsp;
</span>
&nbsp;
</button>
{t('feature.moderation.modal.openaiNotConfig.after', { ns: 'appDebug' })}
</div>
</div>
)
}
</div>
{
localeData.type === 'keywords' && (
<div className="py-2">
<div className="mb-1 text-sm font-medium text-text-primary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
<div className="mb-2 text-xs text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
)}
</div>
{localeData.type === 'keywords' && (
<div className="flex flex-col gap-1">
<div className="system-sm-medium text-text-secondary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
<div className="system-xs-regular text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
<div className="relative h-[88px]">
<Textarea
@ -297,7 +337,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
className="size-full resize-none pb-8"
placeholder={t('feature.moderation.modal.keywords.placeholder', { ns: 'appDebug' }) || ''}
/>
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary">
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 system-2xs-medium-uppercase text-text-quaternary">
<span>{(localeData.config?.keywords || '').split('\n').filter(Boolean).length}</span>
/
<span className="text-text-tertiary">
@ -307,18 +347,16 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
</div>
</div>
</div>
)
}
{
localeData.type === 'api' && (
<div className="py-2">
<div className="flex h-9 items-center justify-between">
<div className="text-sm font-medium text-text-primary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
)}
{localeData.type === 'api' && (
<div className="flex flex-col gap-1">
<div className="flex h-6 items-center justify-between">
<div className="system-sm-medium text-text-secondary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
<a
href={docLink('/use-dify/workspace/api-extension/api-extension')}
target="_blank"
rel="noopener noreferrer"
className="group flex items-center text-xs text-text-tertiary hover:text-primary-600"
className="group flex items-center system-xs-regular text-text-tertiary hover:text-primary-600"
>
<span className="mr-1 i-custom-vender-line-education-book-open-01 size-3 text-text-tertiary group-hover:text-primary-600" />
{t('apiBasedExtension.link', { ns: 'common' })}
@ -329,46 +367,51 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
onChange={handleDataApiBasedChange}
/>
</div>
)
}
{
systemTypes.findIndex(t => t === localeData.type) < 0
&& currentProvider?.form_schema
&& (
<FormGeneration
forms={currentProvider?.form_schema}
value={localeData.config}
onChange={handleDataExtraChange}
)}
{systemTypes.findIndex(t => t === localeData.type) < 0
&& currentProvider?.form_schema
&& (
<FormGeneration
forms={currentProvider?.form_schema}
value={localeData.config}
onChange={handleDataExtraChange}
/>
)}
<div className="flex flex-col gap-2">
<LabeledDivider>{t('feature.moderation.title', { ns: 'appDebug' })}</LabeledDivider>
<ModerationContent
key={`inputs-${localeData.type}-${localeData.config?.inputs_config?.preset_response ?? ''}`}
title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''}
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('inputs_config', config)}
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
showPreset={localeData.type !== 'api'}
/>
)
}
<Divider bgStyle="gradient" className="my-3 h-px" />
<ModerationContent
title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''}
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('inputs_config', config)}
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
showPreset={localeData.type !== 'api'}
/>
<ModerationContent
title={t('feature.moderation.modal.content.output', { ns: 'appDebug' }) || ''}
config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('outputs_config', config)}
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
showPreset={localeData.type !== 'api'}
/>
<div className="mt-1 mb-8 text-xs font-medium text-text-tertiary">{t('feature.moderation.modal.content.condition', { ns: 'appDebug' })}</div>
<div className="flex items-center justify-end">
<ModerationContent
key={`outputs-${localeData.type}-${localeData.config?.outputs_config?.preset_response ?? ''}`}
title={t('feature.moderation.modal.content.output', { ns: 'appDebug' }) || ''}
config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('outputs_config', config)}
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
showPreset={localeData.type !== 'api'}
/>
<div className="py-0.5 system-xs-regular text-text-tertiary">{t('feature.moderation.modal.content.condition', { ns: 'appDebug' })}</div>
</div>
</div>
<div className="flex h-[76px] items-center justify-end gap-2 px-6 pt-5 pb-6">
<Button
onClick={onCancel}
className="mr-2"
size="medium"
className="min-w-[72px]"
>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button
variant="primary"
size="medium"
onClick={handleSave}
disabled={localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured}
className="min-w-[72px]"
>
{t('operation.save', { ns: 'common' })}
</Button>

View File

@ -4,9 +4,9 @@
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
@ -15,35 +15,27 @@
"type": "element",
"name": "g",
"attributes": {
"id": "agent"
"id": "agent",
"transform": "translate(2.5 1)"
},
"children": [
{
"type": "element",
"name": "g",
"name": "path",
"attributes": {
"id": "Vector"
"d": "M3.3178 20.9524V15.7184C3.31774 15.7136 3.31456 15.6845 3.28404 15.6468L3.24312 15.6069C1.26589 13.9971 0 11.5398 0 8.78813C0.000122972 3.93464 3.93464 0.000122969 8.78813 0C13.5146 0 17.3698 3.73066 17.5691 8.40858C17.5712 8.45846 17.5841 8.48161 17.5865 8.48531L19.3226 11.089C19.7937 11.7956 19.6309 12.7481 18.9513 13.2579L17.5998 14.2707C17.5851 14.2819 17.5763 14.2996 17.5763 14.3178V15.4237C17.576 17.2235 16.1176 18.682 14.3178 18.6822H13.2119C13.1798 18.6822 13.1537 18.7085 13.1536 18.7405V20.9524C13.1536 21.5309 12.6845 21.9999 12.1059 22C11.5274 22 11.0583 21.531 11.0583 20.9524V18.7405C11.0584 17.5513 12.0226 16.587 13.2119 16.587H14.3178C14.9604 16.5868 15.4808 16.0663 15.481 15.4237V14.3178C15.481 13.64 15.8006 13.0016 16.3424 12.595L17.3195 11.8614L15.8432 9.64853C15.603 9.28835 15.4913 8.88291 15.4749 8.49758C15.323 4.93639 12.3872 2.09524 8.78813 2.09524C5.09181 2.09536 2.09536 5.09181 2.09524 8.78813C2.09524 10.883 3.05647 12.7533 4.56594 13.9822C5.0571 14.3822 5.41299 15.0012 5.41304 15.7184V20.9524C5.41304 21.5309 4.94385 21.9998 4.36542 22C3.78684 22 3.3178 21.531 3.3178 20.9524Z",
"fill": "currentColor"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M14.7401 5.80454C14.5765 4.77996 14.1638 3.79808 13.5306 2.97273C12.8973 2.14738 12.0648 1.48568 11.1185 1.06589C10.1722 0.646098 9.12632 0.461106 8.08751 0.546487C7.05582 0.624753 6.04548 0.966277 5.17744 1.53548C4.3094 2.09758 3.58366 2.88024 3.09272 3.79808C2.59466 4.70881 2.33852 5.7405 2.33852 6.7793V7.22756L1.25703 9.3692C1.04357 9.80322 1.22145 10.3368 1.65547 10.5574L2.3314 10.8989V12.3006C2.3314 12.82 2.53063 13.3038 2.90061 13.6738C3.2706 14.0367 3.75442 14.243 4.27382 14.243H6.01702V14.7624C6.01702 15.1538 6.3372 15.4739 6.72853 15.4739C7.11986 15.4739 7.44004 15.1538 7.44004 14.7624V13.7094C7.44004 13.2185 7.04159 12.82 6.55065 12.82H4.27382C4.13864 12.82 4.00345 12.7631 3.91095 12.6706C3.81846 12.5781 3.76154 12.4429 3.76154 12.3077V10.5716C3.76154 10.2301 3.56943 9.92417 3.2706 9.77476L2.77254 9.52573L3.66904 7.73984C3.72596 7.61889 3.76154 7.4837 3.76154 7.34851V6.77219C3.76154 5.96818 3.96076 5.17129 4.34498 4.4669C4.72919 3.76251 5.28417 3.15772 5.9601 2.7237C6.63603 2.28968 7.41158 2.02643 8.20847 1.96239C9.00536 1.89835 9.81648 2.04066 10.5493 2.36795C11.2822 2.69524 11.9225 3.20042 12.4135 3.84077C12.8973 4.47402 13.2246 5.23533 13.3456 6.02511C13.4665 6.81488 13.3954 7.63312 13.125 8.38731C12.8617 9.12017 12.4206 9.78187 11.8585 10.3084C11.6735 10.4792 11.5668 10.7139 11.5668 10.9701V14.7624C11.5668 15.1538 11.887 15.4739 12.2783 15.4739C12.6696 15.4739 12.9898 15.1538 12.9898 14.7624V11.1978C13.6515 10.5432 14.1567 9.73918 14.4697 8.87114C14.8184 7.89637 14.918 6.83623 14.7615 5.81165L14.7401 5.80454Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M10.8055 7.99599C10.8909 7.83234 10.962 7.66158 11.0189 7.4837H11.6522C12.0435 7.4837 12.3637 7.16352 12.3637 6.77219C12.3637 6.38086 12.0435 6.06068 11.6522 6.06068H11.0189C10.9691 5.8828 10.898 5.71204 10.8055 5.54839L11.2537 5.10014C11.5312 4.82266 11.5312 4.3744 11.2537 4.09692C10.9762 3.81943 10.528 3.81943 10.2505 4.09692L9.80225 4.54517C9.6386 4.45267 9.46784 4.38863 9.28996 4.33171V3.69847C9.28996 3.30714 8.96978 2.98696 8.57845 2.98696C8.18712 2.98696 7.86694 3.30714 7.86694 3.69847V4.33171C7.68907 4.38152 7.5183 4.45267 7.35466 4.54517L6.90641 4.09692C6.62892 3.81943 6.18067 3.81943 5.90318 4.09692C5.62569 4.3744 5.62569 4.82266 5.90318 5.10014L6.35143 5.54839C6.26605 5.71204 6.1949 5.8828 6.13798 6.06068H5.50473C5.1134 6.06068 4.79323 6.38086 4.79323 6.77219C4.79323 7.16352 5.1134 7.4837 5.50473 7.4837H6.13798C6.18778 7.66158 6.25893 7.83234 6.35143 7.99599L5.90318 8.44424C5.62569 8.72172 5.62569 9.16997 5.90318 9.44746C6.04548 9.58976 6.22336 9.6538 6.40835 9.6538C6.59334 9.6538 6.77122 9.58265 6.91352 9.44746L7.36177 8.99921C7.52542 9.08459 7.69618 9.15574 7.87406 9.21267V9.84591C7.87406 10.2372 8.19424 10.5574 8.58557 10.5574C8.9769 10.5574 9.29708 10.2372 9.29708 9.84591V9.21267C9.47496 9.16286 9.64572 9.09171 9.80936 8.99921L10.2576 9.44746C10.3999 9.58976 10.5778 9.6538 10.7628 9.6538C10.9478 9.6538 11.1257 9.58265 11.268 9.44746C11.5454 9.16997 11.5454 8.72172 11.268 8.44424L10.8197 7.99599H10.8055ZM7.44004 6.77219C7.44004 6.14606 7.94521 5.64089 8.57134 5.64089C9.19747 5.64089 9.70264 6.14606 9.70264 6.77219C9.70264 7.39832 9.19747 7.90349 8.57134 7.90349C7.94521 7.90349 7.44004 7.39832 7.44004 6.77219Z",
"fill": "currentColor"
},
"children": []
}
]
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M9.79012 6.5163L9.31429 5.27923C9.23058 5.06159 9.02158 4.91799 8.78836 4.91799C8.55514 4.91799 8.34614 5.06159 8.26243 5.27923L7.7866 6.5163C7.56194 7.10037 7.10038 7.56194 6.5163 7.78659L5.27923 8.26239C5.06159 8.34609 4.91799 8.55519 4.91799 8.78836C4.91799 9.02158 5.06159 9.23058 5.27923 9.31429L6.5163 9.79012C7.10037 10.0148 7.56194 10.4764 7.7866 11.0604L8.26243 12.2975C8.34614 12.5151 8.55514 12.6587 8.78836 12.6587C9.02158 12.6587 9.23058 12.5151 9.31429 12.2975L9.79012 11.0604C10.0148 10.4764 10.4764 10.0148 11.0604 9.79012L12.2975 9.31429C12.5151 9.23058 12.6587 9.02158 12.6587 8.78836C12.6587 8.55519 12.5151 8.34609 12.2975 8.26239L11.0604 7.78659C10.4764 7.56194 10.0148 7.10038 9.79012 6.5163Z",
"fill": "currentColor"
},
"children": []
}
]
}

View File

@ -9,6 +9,13 @@ import {
UPDATE_HISTORY_EVENT_EMITTER,
} from '../constants'
import PromptEditor from '../index'
import { CustomTextNode } from '../plugins/custom-text/node'
type MockNodeReplacementConfig = {
replace?: unknown
with?: (arg: { __text: string }) => void
withKlass?: unknown
}
const mocks = vi.hoisted(() => {
const commandHandlers = new Map<unknown, (payload: unknown) => boolean>()
@ -18,6 +25,7 @@ const mocks = vi.hoisted(() => {
return {
emit: vi.fn(),
rootLines: ['first line', 'second line'],
nodeReplacementConfig: undefined as MockNodeReplacementConfig | undefined,
commandHandlers,
subscriptions,
rootElement,
@ -86,7 +94,7 @@ vi.mock('@lexical/react/LexicalComposer', () => ({
LexicalComposer: ({ initialConfig, children }: {
initialConfig: {
onError?: (error: Error) => void
nodes?: Array<{ replace?: unknown, with: (arg: { __text: string }) => void }>
nodes?: unknown[]
}
children: ReactNode
}) => {
@ -99,9 +107,11 @@ vi.mock('@lexical/react/LexicalComposer', () => ({
}
}
if (initialConfig?.nodes) {
const textNodeConf = initialConfig.nodes.find((n: { replace?: unknown, with: (arg: { __text: string }) => void }) => n?.replace)
if (textNodeConf)
textNodeConf.with({ __text: 'test' })
const textNodeConf = initialConfig.nodes.find((node): node is MockNodeReplacementConfig => {
return typeof node === 'object' && node !== null && 'replace' in node
})
mocks.nodeReplacementConfig = textNodeConf
textNodeConf?.with?.({ __text: 'test' })
}
return <div data-testid="lexical-composer">{children}</div>
},
@ -173,10 +183,17 @@ describe('PromptEditor', () => {
mocks.commandHandlers.clear()
mocks.subscriptions.length = 0
mocks.rootLines = ['first line', 'second line']
mocks.nodeReplacementConfig = undefined
})
// Rendering shell and text output from lexical state.
describe('Rendering', () => {
it('should register CustomTextNode as the TextNode replacement class', () => {
render(<PromptEditor />)
expect(mocks.nodeReplacementConfig?.withKlass).toBe(CustomTextNode)
})
it('should render placeholder and call onChange with joined lexical text', async () => {
const onChange = vi.fn()

View File

@ -26,6 +26,7 @@ import { HITLInputNode } from '../plugins/hitl-input-block'
import { LastRunBlockNode } from '../plugins/last-run-block'
import { QueryBlockNode } from '../plugins/query-block'
import { RequestURLBlockNode } from '../plugins/request-url-block'
import { RosterReferenceBlockNode } from '../plugins/roster-reference-block/node'
import { PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER } from '../plugins/update-block'
import { VariableValueBlockNode } from '../plugins/variable-value-block/node'
import { WorkflowVariableBlockNode } from '../plugins/workflow-variable-block'
@ -108,6 +109,7 @@ const PromptEditorContentHarness = ({
RequestURLBlockNode,
WorkflowVariableBlockNode,
VariableValueBlockNode,
RosterReferenceBlockNode,
HITLInputNode,
CurrentBlockNode,
ErrorMessageBlockNode,
@ -291,5 +293,29 @@ describe('PromptEditorContent', () => {
expect(screen.queryByTestId('draggable-target-line')).not.toBeInTheDocument()
expect(screen.getByText('common.promptEditor.placeholder')).toBeInTheDocument()
})
it('should render roster references as inline token pills when enabled', async () => {
const captures: Captures = { editor: null, eventEmitter: null }
const { container } = render(
<PromptEditorContentHarness
captures={captures}
shortcutPopups={[]}
initialText="Use [§file:file-1:qna_report.pdf§]"
floatingAnchorElem={null}
onEditorChange={vi.fn()}
rosterReferenceBlock={{ show: true }}
/>,
)
await waitFor(() => {
expect(captures.editor).not.toBeNull()
})
const token = container.querySelector('[data-roster-reference-kind="file"]') as HTMLElement
expect(token).toBeInTheDocument()
expect(token).toHaveTextContent('qna_report.pdf')
expect(token.querySelector('.i-ri-file-pdf-2-fill')).toBeInTheDocument()
})
})
})

View File

@ -16,6 +16,7 @@ import type {
LastRunBlockType,
QueryBlockType,
RequestURLBlockType,
RosterReferenceBlockType,
VariableBlockType,
WorkflowVariableBlockType,
} from './types'
@ -60,6 +61,7 @@ import {
import {
RequestURLBlockNode,
} from './plugins/request-url-block'
import { RosterReferenceBlockNode } from './plugins/roster-reference-block/node'
import { VariableValueBlockNode } from './plugins/variable-value-block/node'
import {
WorkflowVariableBlockNode,
@ -108,6 +110,7 @@ const EditableSyncPlugin: FC<{ editable: boolean }> = ({ editable }) => {
export type PromptEditorProps = {
instanceId?: string
children?: React.ReactNode
compact?: boolean
wrapperClassName?: string
className?: string
@ -124,6 +127,7 @@ export type PromptEditorProps = {
requestURLBlock?: RequestURLBlockType
historyBlock?: HistoryBlockType
variableBlock?: VariableBlockType
rosterReferenceBlock?: RosterReferenceBlockType
externalToolBlock?: ExternalToolBlockType
workflowVariableBlock?: WorkflowVariableBlockType
hitlInputBlock?: HITLInputBlockType
@ -131,6 +135,8 @@ export type PromptEditorProps = {
errorMessageBlock?: ErrorMessageBlockType
lastRunBlock?: LastRunBlockType
isSupportFileVar?: boolean
disableSlashPicker?: boolean
disableBracePicker?: boolean
shortcutPopups?: Array<{
hotkey: Hotkey
displayMode?: ShortcutPopupDisplayMode
@ -140,6 +146,7 @@ export type PromptEditorProps = {
const PromptEditor: FC<PromptEditorProps> = ({
instanceId,
children,
compact,
wrapperClassName,
className,
@ -156,6 +163,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
requestURLBlock,
historyBlock,
variableBlock,
rosterReferenceBlock,
externalToolBlock,
workflowVariableBlock,
hitlInputBlock,
@ -163,6 +171,8 @@ const PromptEditor: FC<PromptEditorProps> = ({
errorMessageBlock,
lastRunBlock,
isSupportFileVar,
disableSlashPicker = false,
disableBracePicker = false,
shortcutPopups = [],
}) => {
const { eventEmitter } = useEventEmitterContextContext()
@ -177,6 +187,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
{
replace: TextNode,
with: (node: TextNode) => new CustomTextNode(node.__text),
withKlass: CustomTextNode,
},
ContextBlockNode,
HistoryBlockNode,
@ -184,6 +195,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
RequestURLBlockNode,
WorkflowVariableBlockNode,
VariableValueBlockNode,
RosterReferenceBlockNode,
HITLInputNode,
CurrentBlockNode,
ErrorMessageBlockNode,
@ -242,6 +254,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
requestURLBlock={requestURLBlock}
historyBlock={historyBlock}
variableBlock={variableBlock}
rosterReferenceBlock={rosterReferenceBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
hitlInputBlock={hitlInputBlock}
@ -249,6 +262,8 @@ const PromptEditor: FC<PromptEditorProps> = ({
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
disableSlashPicker={disableSlashPicker}
disableBracePicker={disableBracePicker}
onBlur={onBlur}
onFocus={onFocus}
instanceId={instanceId}
@ -257,6 +272,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
/>
<ValueSyncPlugin value={value} />
<EditableSyncPlugin editable={editable} />
{children}
</div>
</LexicalComposer>
)

View File

@ -0,0 +1,119 @@
import type {
Klass,
LexicalEditor,
LexicalNode,
} from 'lexical'
import { render, screen } from '@testing-library/react'
import { createEditor } from 'lexical'
import RosterReferenceBlockComponent from '../component'
import {
$createRosterReferenceBlockNode,
$isRosterReferenceBlockNode,
RosterReferenceBlockNode,
} from '../node'
import {
getRosterReferenceFileIconType,
parseRosterReferenceToken,
} from '../utils'
describe('RosterReferenceBlockNode', () => {
let editor: LexicalEditor
beforeEach(() => {
vi.clearAllMocks()
editor = createEditor({
nodes: [RosterReferenceBlockNode as unknown as Klass<LexicalNode>],
})
})
const runInEditor = (callback: () => void) => {
editor.update(callback, { discrete: true })
}
it('should parse roster reference tokens and infer icon classes', () => {
expect(parseRosterReferenceToken('[§skill:2c3176de8a01:tender-analyzer§]')).toEqual({
kind: 'skill',
id: '2c3176de8a01',
label: 'tender-analyzer',
})
expect(parseRosterReferenceToken('[§file:1f0ad3e2:qna_report:final.pdf§]')).toEqual({
kind: 'file',
id: '1f0ad3e2',
label: 'qna_report:final.pdf',
})
expect(parseRosterReferenceToken('[§unknown:1:item§]')).toBeNull()
expect(getRosterReferenceFileIconType('qna_report.pdf')).toBe('pdf')
})
it('should render a non-editable token pill component', () => {
const { container } = render(
<RosterReferenceBlockComponent text="[§tool-all:tavily/tavily:tavily§]" />,
)
const token = screen.getByTitle('tavily')
expect(token).toHaveAttribute('contenteditable', 'false')
expect(token).toHaveAttribute('data-roster-reference-kind', 'tool-all')
expect(token).toHaveAttribute('data-roster-reference-id', 'tavily/tavily')
expect(token).toHaveClass('border-state-accent-hover-alt')
expect(token).toHaveClass('bg-state-accent-hover')
expect(token).toHaveTextContent('tavily')
expect(container.querySelector('.i-custom-public-other-default-tool-icon')).toBeInTheDocument()
})
it('should render knowledge icon with the configured retrieval row style', () => {
const { container } = render(
<RosterReferenceBlockComponent text="[§knowledge:manual-1:产品手册§]" />,
)
const iconShell = container.querySelector('.bg-util-colors-green-green-500')
expect(iconShell).toBeInTheDocument()
expect(iconShell).toHaveClass('text-text-primary-on-surface')
expect(iconShell?.querySelector('.i-ri-book-open-line')).toBeInTheDocument()
})
it('should expose DecoratorNode behavior and preserve raw text content', () => {
runInEditor(() => {
const node = new RosterReferenceBlockNode('[§tool-all:tavily/tavily:tavily§]', 'node-key')
const cloned = RosterReferenceBlockNode.clone(node)
const dom = node.createDOM()
expect(RosterReferenceBlockNode.getType()).toBe('roster-reference-block')
expect(cloned).toBeInstanceOf(RosterReferenceBlockNode)
expect(cloned.getKey()).toBe('node-key')
expect(node.isInline()).toBe(true)
expect(dom).toHaveClass('inline-flex')
expect(dom).toHaveClass('align-middle')
expect(node.getTextContent()).toBe('[§tool-all:tavily/tavily:tavily§]')
})
})
it('should import and export serialized node text', () => {
runInEditor(() => {
const imported = RosterReferenceBlockNode.importJSON({
text: '[§knowledge:manual-1:产品手册§]',
type: 'roster-reference-block',
version: 1,
})
const exported = imported.exportJSON()
expect(exported).toEqual({
text: '[§knowledge:manual-1:产品手册§]',
type: 'roster-reference-block',
version: 1,
})
})
})
it('should create node with helper and support type guard checks', () => {
runInEditor(() => {
const node = $createRosterReferenceBlockNode('[§skill:playwright:Playwright§]')
expect(node).toBeInstanceOf(RosterReferenceBlockNode)
expect(node.getTextContent()).toBe('[§skill:playwright:Playwright§]')
expect($isRosterReferenceBlockNode(node)).toBe(true)
expect($isRosterReferenceBlockNode(null)).toBe(false)
expect($isRosterReferenceBlockNode(undefined)).toBe(false)
expect($isRosterReferenceBlockNode({} as LexicalNode)).toBe(false)
})
})
})

View File

@ -0,0 +1,55 @@
import { cn } from '@langgenius/dify-ui/cn'
import { FileTreeIcon } from '@langgenius/dify-ui/file-tree'
import { use } from 'react'
import { RosterReferenceBlockContext } from './context'
import {
getRosterReferenceFileIconType,
getRosterReferenceIconClassName,
parseRosterReferenceToken,
} from './utils'
type RosterReferenceBlockComponentProps = {
text: string
}
const RosterReferenceBlockComponent = ({
text,
}: RosterReferenceBlockComponentProps) => {
const rosterReferenceBlock = use(RosterReferenceBlockContext)
const token = parseRosterReferenceToken(text)
if (!token)
return null
const isKnowledge = token.kind === 'knowledge'
const customIcon = rosterReferenceBlock?.renderIcon?.(token)
const defaultIcon = token.kind === 'file'
? <FileTreeIcon type={getRosterReferenceFileIconType(token.label)} className="size-4" />
: <span className={cn(isKnowledge ? 'size-3.5' : 'size-3.5 shrink-0', getRosterReferenceIconClassName(token))} />
return (
<span
contentEditable={false}
data-roster-reference-kind={token.kind}
data-roster-reference-id={token.id}
title={token.label}
className="inline-flex min-w-[18px] items-center gap-0.5 overflow-hidden rounded-[5px] border border-state-accent-hover-alt bg-state-accent-hover py-px pr-1 pl-px align-middle shadow-xs shadow-shadow-shadow-3"
>
<span
aria-hidden
className={cn(
'inline-flex size-4 shrink-0 items-center justify-center rounded-[5px] border-[0.5px] border-divider-subtle bg-background-default-dodge',
token.kind === 'cli_tool' && 'border-divider-regular bg-text-tertiary',
isKnowledge && 'border-divider-subtle bg-util-colors-green-green-500 p-[3px] text-text-primary-on-surface shadow-xs shadow-shadow-shadow-3',
)}
>
{customIcon || defaultIcon}
</span>
<span className="max-w-48 truncate system-xs-medium text-text-accent">
{token.label}
</span>
</span>
)
}
export default RosterReferenceBlockComponent

View File

@ -0,0 +1,4 @@
import type { RosterReferenceBlockType } from '../../types'
import { createContext } from 'react'
export const RosterReferenceBlockContext = createContext<RosterReferenceBlockType | undefined>(undefined)

View File

@ -0,0 +1,63 @@
import type { EntityMatch } from '@lexical/text'
import type { LexicalEditor, TextNode } from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { mergeRegister } from '@lexical/utils'
import { $applyNodeReplacement } from 'lexical'
import {
useCallback,
useEffect,
} from 'react'
import { decoratorTransform } from '../../utils'
import { CustomTextNode } from '../custom-text/node'
import { RosterReferenceBlockNode } from './node'
import { ROSTER_REFERENCE_REGEX } from './utils'
type RosterReferenceNodeRegistry = {
_nodes: Map<string, { klass: typeof RosterReferenceBlockNode }>
}
function createRegisteredRosterReferenceBlockNode(editor: LexicalEditor, textNode: TextNode): RosterReferenceBlockNode {
const RegisteredRosterReferenceBlockNode = (editor as unknown as RosterReferenceNodeRegistry)
._nodes
.get(RosterReferenceBlockNode.getType())
?.klass ?? RosterReferenceBlockNode
return $applyNodeReplacement(new RegisteredRosterReferenceBlockNode(textNode.getTextContent()))
}
const RosterReferenceBlock = () => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([RosterReferenceBlockNode]))
throw new Error('RosterReferenceBlockPlugin: RosterReferenceBlockNode not registered on editor')
}, [editor])
const createRosterReferenceBlockNode = useCallback((textNode: CustomTextNode): RosterReferenceBlockNode => (
createRegisteredRosterReferenceBlockNode(editor, textNode)
), [editor])
const getRosterReferenceMatch = useCallback((text: string): EntityMatch | null => {
const matchArr = ROSTER_REFERENCE_REGEX.exec(text)
if (matchArr === null)
return null
const startOffset = matchArr.index
const endOffset = startOffset + matchArr[0].length
return {
end: endOffset,
start: startOffset,
}
}, [])
useEffect(() => {
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getRosterReferenceMatch, createRosterReferenceBlockNode)),
)
}, [createRosterReferenceBlockNode, editor, getRosterReferenceMatch])
return null
}
export default RosterReferenceBlock

View File

@ -0,0 +1,76 @@
import type {
LexicalNode,
NodeKey,
SerializedLexicalNode,
} from 'lexical'
import type { JSX } from 'react'
import {
$applyNodeReplacement,
DecoratorNode,
} from 'lexical'
import RosterReferenceBlockComponent from './component'
type SerializedRosterReferenceBlockNode = SerializedLexicalNode & {
text: string
}
export class RosterReferenceBlockNode extends DecoratorNode<JSX.Element> {
__text: string
static override getType(): string {
return 'roster-reference-block'
}
static override clone(node: RosterReferenceBlockNode): RosterReferenceBlockNode {
return new RosterReferenceBlockNode(node.__text, node.__key)
}
constructor(text: string, key?: NodeKey) {
super(key)
this.__text = text
}
override isInline(): boolean {
return true
}
override createDOM(): HTMLElement {
const span = document.createElement('span')
span.classList.add('inline-flex', 'items-center', 'align-middle')
return span
}
override updateDOM(): false {
return false
}
override decorate(): JSX.Element {
return <RosterReferenceBlockComponent text={this.getTextContent()} />
}
static override importJSON(serializedNode: SerializedRosterReferenceBlockNode): RosterReferenceBlockNode {
return $createRosterReferenceBlockNode(serializedNode.text)
}
override exportJSON(): SerializedRosterReferenceBlockNode {
return {
text: this.getTextContent(),
type: 'roster-reference-block',
version: 1,
}
}
override getTextContent(): string {
return this.getLatest().__text
}
}
export function $createRosterReferenceBlockNode(text = ''): RosterReferenceBlockNode {
return $applyNodeReplacement(new RosterReferenceBlockNode(text))
}
export function $isRosterReferenceBlockNode(
node: LexicalNode | null | undefined,
): node is RosterReferenceBlockNode {
return node instanceof RosterReferenceBlockNode
}

View File

@ -0,0 +1,111 @@
import type { FileTreeIconType } from '@langgenius/dify-ui/file-tree'
export type RosterReferenceKind = 'skill' | 'file' | 'tool-all' | 'tool' | 'cli_tool' | 'knowledge'
export type RosterReferenceToken = {
kind: RosterReferenceKind
id: string
label: string
}
export const ROSTER_REFERENCE_REGEX = /\[§(?:skill|file|tool-all|tool|cli_tool|knowledge):[^\]§\n\r]+§\]/
const KNOWN_KINDS = new Set<RosterReferenceKind>([
'skill',
'file',
'tool-all',
'tool',
'cli_tool',
'knowledge',
])
export function parseRosterReferenceToken(text: string): RosterReferenceToken | null {
if (!text.startsWith('[§') || !text.endsWith('§]'))
return null
const body = text.slice(2, -2)
const firstColonIndex = body.indexOf(':')
if (firstColonIndex === -1)
return null
const kind = body.slice(0, firstColonIndex) as RosterReferenceKind
if (!KNOWN_KINDS.has(kind))
return null
const rest = body.slice(firstColonIndex + 1)
const secondColonIndex = rest.indexOf(':')
const id = secondColonIndex === -1 ? rest : rest.slice(0, secondColonIndex)
const label = secondColonIndex === -1 ? id : rest.slice(secondColonIndex + 1)
if (!id || !label)
return null
return {
kind,
id,
label,
}
}
const codeFileExtensions = new Set([
'css',
'go',
'html',
'htm',
'js',
'jsx',
'py',
'rb',
'rs',
'scss',
'sh',
'ts',
'tsx',
'vue',
'yaml',
'yml',
])
const imageFileExtensions = new Set(['apng', 'avif', 'bmp', 'gif', 'ico', 'jpeg', 'jpg', 'png', 'svg', 'webp'])
const tableFileExtensions = new Set(['csv', 'xls', 'xlsx'])
const archiveFileExtensions = new Set(['7z', 'gz', 'rar', 'tar', 'zip'])
export function getRosterReferenceFileIconType(label: string): FileTreeIconType {
const extension = label.includes('.') ? label.split('.').pop()?.toLowerCase() : undefined
if (!extension)
return 'folder'
if (imageFileExtensions.has(extension))
return 'image'
if (extension === 'pdf')
return 'pdf'
if (extension === 'md' || extension === 'markdown' || extension === 'mdx')
return 'markdown'
if (extension === 'json')
return 'json'
if (tableFileExtensions.has(extension))
return 'table'
if (archiveFileExtensions.has(extension))
return 'archive'
if (codeFileExtensions.has(extension))
return 'code'
if (extension === 'txt')
return 'text'
return 'file'
}
export function getRosterReferenceIconClassName(token: RosterReferenceToken) {
switch (token.kind) {
case 'skill':
return 'i-custom-public-agent-building-blocks text-text-tertiary'
case 'file':
return ''
case 'tool-all':
case 'tool':
return 'i-custom-public-other-default-tool-icon text-[#ef5b39]'
case 'cli_tool':
return 'i-ri-terminal-box-line text-text-primary-on-surface'
case 'knowledge':
return 'i-ri-book-open-line'
}
}

View File

@ -11,6 +11,7 @@ import type {
LastRunBlockType,
QueryBlockType,
RequestURLBlockType,
RosterReferenceBlockType,
VariableBlockType,
WorkflowVariableBlockType,
} from './types'
@ -57,6 +58,8 @@ import {
RequestURLBlock,
RequestURLBlockReplacementBlock,
} from './plugins/request-url-block'
import RosterReferenceBlock from './plugins/roster-reference-block'
import { RosterReferenceBlockContext } from './plugins/roster-reference-block/context'
import ShortcutsPopupPlugin from './plugins/shortcuts-popup-plugin'
import UpdateBlock from './plugins/update-block'
import VariableBlock from './plugins/variable-block'
@ -84,6 +87,7 @@ type PromptEditorContentProps = {
requestURLBlock?: RequestURLBlockType
historyBlock?: HistoryBlockType
variableBlock?: VariableBlockType
rosterReferenceBlock?: RosterReferenceBlockType
externalToolBlock?: ExternalToolBlockType
workflowVariableBlock?: WorkflowVariableBlockType
hitlInputBlock?: HITLInputBlockType
@ -91,6 +95,8 @@ type PromptEditorContentProps = {
errorMessageBlock?: ErrorMessageBlockType
lastRunBlock?: LastRunBlockType
isSupportFileVar?: boolean
disableSlashPicker?: boolean
disableBracePicker?: boolean
onBlur?: () => void
onFocus?: () => void
instanceId?: string
@ -110,6 +116,7 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
requestURLBlock,
historyBlock,
variableBlock,
rosterReferenceBlock,
externalToolBlock,
workflowVariableBlock,
hitlInputBlock,
@ -117,6 +124,8 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
errorMessageBlock,
lastRunBlock,
isSupportFileVar,
disableSlashPicker,
disableBracePicker,
onBlur,
onFocus,
instanceId,
@ -124,7 +133,7 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
onEditorChange,
}) => {
return (
<>
<RosterReferenceBlockContext value={rosterReferenceBlock}>
<RichTextPlugin
contentEditable={(
<ContentEditable
@ -150,34 +159,38 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
{(closePortal, onInsert) => <Popup onClose={closePortal} onInsert={onInsert} />}
</ShortcutsPopupPlugin>
))}
<ComponentPickerBlock
triggerString="/"
contextBlock={contextBlock}
historyBlock={historyBlock}
queryBlock={queryBlock}
requestURLBlock={requestURLBlock}
variableBlock={variableBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
currentBlock={currentBlock}
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
/>
<ComponentPickerBlock
triggerString="{"
contextBlock={contextBlock}
historyBlock={historyBlock}
queryBlock={queryBlock}
requestURLBlock={requestURLBlock}
variableBlock={variableBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
currentBlock={currentBlock}
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
/>
{!disableSlashPicker && (
<ComponentPickerBlock
triggerString="/"
contextBlock={contextBlock}
historyBlock={historyBlock}
queryBlock={queryBlock}
requestURLBlock={requestURLBlock}
variableBlock={variableBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
currentBlock={currentBlock}
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
/>
)}
{!disableBracePicker && (
<ComponentPickerBlock
triggerString="{"
contextBlock={contextBlock}
historyBlock={historyBlock}
queryBlock={queryBlock}
requestURLBlock={requestURLBlock}
variableBlock={variableBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
currentBlock={currentBlock}
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
/>
)}
{contextBlock?.show && (
<>
<ContextBlock {...contextBlock} />
@ -202,6 +215,9 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
<VariableValueBlock />
</>
)}
{rosterReferenceBlock?.show && (
<RosterReferenceBlock />
)}
{workflowVariableBlock?.show && (
<>
<WorkflowVariableBlock {...workflowVariableBlock} />
@ -248,7 +264,7 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
{floatingAnchorElem && (
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
)}
</>
</RosterReferenceBlockContext>
)
}

View File

@ -3,6 +3,7 @@ import type { FormInputItem } from '../../workflow/nodes/human-input/types'
import type { Type } from '../../workflow/nodes/llm/types'
import type { Dataset } from './plugins/context-block'
import type { RoleName } from './plugins/history-block'
import type { RosterReferenceToken } from './plugins/roster-reference-block/utils'
import type {
Node,
NodeOutPutVar,
@ -59,6 +60,11 @@ export type VariableBlockType = {
variables?: Option[]
}
export type RosterReferenceBlockType = {
show?: boolean
renderIcon?: (token: RosterReferenceToken) => React.ReactNode
}
export type ExternalToolBlockType = {
show?: boolean
externalTools?: ExternalToolOption[]

View File

@ -40,4 +40,25 @@ describe('useIntegrationsSetting', () => {
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'mcp' })
})
it('should preserve the agent source for agent-scoped settings', () => {
const { result } = renderHook(() => useIntegrationsSetting())
act(() => {
result.current({ payload: ACCOUNT_SETTING_TAB.PROVIDER, source: 'agent' })
})
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider', source: 'agent' })
})
it('should preserve the cancel callback for migrated integrations settings', () => {
const onCancelCallback = vi.fn()
const { result } = renderHook(() => useIntegrationsSetting())
act(() => {
result.current({ payload: ACCOUNT_SETTING_TAB.PROVIDER, onCancelCallback })
})
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider', onCancelCallback })
})
})

View File

@ -4,6 +4,7 @@ import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { useCallback } from 'react'
type DialogProps = {
backdropClassName?: string
className?: string
children: ReactNode
show: boolean
@ -11,6 +12,7 @@ type DialogProps = {
}
const MenuDialog = ({
backdropClassName,
className,
children,
show,
@ -27,7 +29,7 @@ const MenuDialog = ({
}}
>
<DialogContent
backdropClassName="z-40 bg-transparent"
backdropClassName={cn('z-40 bg-transparent', backdropClassName)}
className={cn(
'top-0 left-0 z-40 h-full max-h-none w-full max-w-none translate-x-0 translate-y-0 scale-100 overflow-hidden rounded-none border-none bg-background-sidenav-bg p-0 shadow-none backdrop-blur-md transition-opacity data-ending-style:scale-100 data-starting-style:scale-100',
className,

View File

@ -247,6 +247,7 @@ export type DefaultModelResponse = {
export type DefaultModel = {
provider: string
model: string
plugin_id?: string
}
export type CustomConfigurationModelFixedFields = {

View File

@ -108,7 +108,11 @@ describe('ModelSelector', () => {
fireEvent.click(screen.getByRole('combobox'))
fireEvent.click(screen.getByText('select'))
expect(onSelect).toHaveBeenCalledWith({ provider: 'openai', model: 'gpt-4' })
expect(onSelect).toHaveBeenCalledWith({
provider: 'openai',
model: 'gpt-4',
plugin_id: 'langgenius/openai',
})
})
it('should close popup when popup requests hide', () => {

View File

@ -11,6 +11,15 @@ import ModelSelectorTrigger from './model-selector-trigger'
import Popup from './popup'
import { getModelSelectorValueLabel, isSameModelSelectorValue } from './types'
const getModelProviderPluginId = (provider: string) => {
const [organization, pluginName] = provider.split('/').filter(Boolean)
if (organization && pluginName)
return `${organization}/${pluginName}`
return provider ? `langgenius/${provider}` : ''
}
type ModelSelectorProps = {
defaultModel?: DefaultModel
modelList: Model[]
@ -24,6 +33,7 @@ type ModelSelectorProps = {
showDeprecatedWarnIcon?: boolean
hideProviderSettingsFooter?: boolean
onConfigureEmptyState?: () => void
providerSettingsSource?: 'agent'
showModelMeta?: boolean
}
function ModelSelector({
@ -39,6 +49,7 @@ function ModelSelector({
showDeprecatedWarnIcon = true,
hideProviderSettingsFooter,
onConfigureEmptyState,
providerSettingsSource,
showModelMeta,
}: ModelSelectorProps) {
const { t } = useTranslation()
@ -74,8 +85,13 @@ function ModelSelector({
setOpen(false)
setInputValue('')
if (onSelect)
onSelect({ provider, model: model.model })
if (onSelect) {
onSelect({
provider,
model: model.model,
plugin_id: getModelProviderPluginId(provider),
})
}
}, [onSelect])
const handleValueChange = useCallback((value: ModelSelectorValue | null) => {
@ -150,6 +166,7 @@ function ModelSelector({
modelList={modelList}
scopeFeatures={scopeFeatures}
hideProviderSettingsFooter={hideProviderSettingsFooter}
providerSettingsSource={providerSettingsSource}
onConfigureEmptyState={onConfigureEmptyState ? handleConfigureEmptyState : undefined}
onInputValueChange={setInputValue}
onHide={handleHide}

View File

@ -38,6 +38,7 @@ export type PopupProps = {
modelList: Model[]
scopeFeatures?: ModelFeatureEnum[]
hideProviderSettingsFooter?: boolean
providerSettingsSource?: 'agent'
onConfigureEmptyState?: () => void
onInputValueChange: (value: string) => void
onHide: () => void
@ -48,6 +49,7 @@ function Popup({
modelList,
scopeFeatures = [],
hideProviderSettingsFooter,
providerSettingsSource,
onConfigureEmptyState,
onInputValueChange,
onHide,
@ -173,8 +175,8 @@ function Popup({
const handleOpenSettings = useCallback(() => {
onHide()
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
}, [onHide, openIntegrationsSetting])
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER, source: providerSettingsSource })
}, [onHide, openIntegrationsSetting, providerSettingsSource])
const handleClosePreviewCard = useCallback(() => {
previewCardHandle.close()
}, [previewCardHandle])

View File

@ -7,8 +7,8 @@ import { useModalContext } from '@/context/modal-context'
import { integrationSectionByMovedAccountSettingTab } from './destinations'
type IntegrationsSettingState
= | { payload: MovedAccountSettingTab }
| { section: IntegrationSection }
= | { payload: MovedAccountSettingTab, source?: 'agent', onCancelCallback?: () => void }
| { section: IntegrationSection, source?: 'agent', onCancelCallback?: () => void }
export const useIntegrationsSetting = () => {
const { setShowAccountSettingModal } = useModalContext()
@ -19,7 +19,12 @@ export const useIntegrationsSetting = () => {
? state.section
: integrationSectionByMovedAccountSettingTab[state.payload]
if (section)
setShowAccountSettingModal({ payload: section })
if (section) {
setShowAccountSettingModal({
payload: section,
...(state.source ? { source: state.source } : {}),
...(state.onCancelCallback ? { onCancelCallback: state.onCancelCallback } : {}),
})
}
}, [setShowAccountSettingModal])
}

View File

@ -159,6 +159,15 @@ vi.mock('@/app/components/app-sidebar/dataset-detail-top', () => ({
),
}))
vi.mock('@/features/agent-v2/agent-detail/navigation', () => ({
AgentDetailSection: ({ expand }: { expand: boolean }) => <div data-testid="agent-detail-section" data-expand={expand} />,
AgentDetailTop: ({ expand, onToggle }: { expand: boolean, onToggle: () => void }) => (
<div data-testid="agent-detail-top" data-expand={expand}>
<button type="button" data-testid="agent-detail-toggle" onClick={onToggle}>Toggle</button>
</div>
),
}))
vi.mock('@/context/i18n', () => ({
useLocale: () => 'en-US',
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
@ -320,6 +329,7 @@ describe('MainNav', () => {
expect(screen.getByRole('button', { name: 'common.account.account' })).not.toHaveTextContent(Plan.team)
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/')
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
expect(screen.getByRole('link', { name: /common.menus.roster/ })).toHaveAttribute('href', '/roster')
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets')
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toHaveAttribute('href', '/integrations/model-provider')
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toHaveAttribute('href', '/marketplace')
@ -433,6 +443,7 @@ describe('MainNav', () => {
expect(screen.queryByRole('link', { name: /common.mainNav.home/ })).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: /common.menus.apps/ })).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: /common.menus.roster/ })).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets')
expect(screen.queryByRole('link', { name: /common.mainNav.integrations/ })).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toHaveAttribute('href', '/marketplace')
@ -456,6 +467,7 @@ describe('MainNav', () => {
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.menus.roster/ })).toBeInTheDocument()
expect(screen.queryByRole('link', { name: /common.menus.datasets/ })).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toBeInTheDocument()
@ -484,6 +496,21 @@ describe('MainNav', () => {
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).not.toHaveAttribute('aria-current')
})
it('hides the main menu on snippet detail routes while keeping account settings available', () => {
mockPathname = '/snippets/snippet-1/orchestrate'
renderMainNav()
expect(screen.getByRole('complementary')).toHaveClass('w-16')
expect(screen.queryByLabelText('Dify')).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'common.mainNav.workspace.openMenu' })).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: /common.mainNav.home/ })).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: /common.menus.apps/ })).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'explore.sidebar.webApps' })).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'common.account.account' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'common.mainNav.help.openMenu' })).toBeInTheDocument()
})
it('replaces global navigation with app detail navigation on app routes', () => {
mockPathname = '/app/app-1/overview'
@ -609,6 +636,35 @@ describe('MainNav', () => {
expect(screen.getByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'true')
})
it('replaces global navigation with agent detail navigation on roster detail routes', () => {
mockPathname = '/roster/agent/agent-1/configure'
renderMainNav()
expect(screen.getByTestId('agent-detail-top')).toBeInTheDocument()
expect(screen.getByTestId('agent-detail-section')).toBeInTheDocument()
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'true')
expect(screen.getByRole('complementary')).toHaveClass('w-[248px]')
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByRole('complementary')).toHaveClass('bg-background-body')
expect(screen.queryByRole('button', { name: 'common.mainNav.workspace.openMenu' })).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: /common.menus.roster/ })).not.toBeInTheDocument()
})
it('collapses agent detail navigation from the top-right toggle', () => {
mockPathname = '/roster/agent/agent-1/configure'
renderMainNav()
fireEvent.click(screen.getByTestId('agent-detail-toggle'))
expect(screen.getByRole('complementary')).toHaveClass('w-16')
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'false')
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'false')
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
})
it('registers the detail navigation shortcut to run while inputs are focused', () => {
mockPathname = '/app/app-1/overview'
@ -618,6 +674,21 @@ describe('MainNav', () => {
expect.objectContaining({ ignoreInputs: false }),
)
})
it('shows agent detail navigation as a floating preview when hovering the collapsed top toggle', () => {
mockPathname = '/roster/agent/agent-1/configure'
renderMainNav()
fireEvent.click(screen.getByTestId('agent-detail-toggle'))
fireEvent.mouseEnter(screen.getByTestId('agent-detail-top').parentElement!)
expect(screen.getByRole('complementary')).toHaveClass('w-16', 'overflow-visible')
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
expect(screen.getAllByTestId('agent-detail-top')).toHaveLength(1)
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'true')
})
it.each([
'/datasets/create',
'/datasets/create-from-pipeline',
@ -644,6 +715,16 @@ describe('MainNav', () => {
expect(marketplaceLink).toHaveClass(activeEdgeClassName)
})
it('marks roster active on roster routes', () => {
mockPathname = '/roster'
renderMainNav()
const rosterLink = screen.getByRole('link', { name: /common.menus.roster/ })
expect(rosterLink).toHaveClass(activeEdgeClassName)
expect(rosterLink).toHaveAttribute('aria-current', 'page')
})
it('applies the Figma glass active state to the Home route', () => {
mockPathname = '/'

View File

@ -17,6 +17,7 @@ import DifyLogo from '@/app/components/base/logo/dify-logo'
import EnvNav from '@/app/components/header/env-nav'
import { buildIntegrationPath } from '@/app/components/integrations/routes'
import { useAppContext } from '@/context/app-context'
import { AgentDetailSection, AgentDetailTop } from '@/features/agent-v2/agent-detail/navigation'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import Link from '@/next/link'
import { usePathname } from '@/next/navigation'
@ -60,6 +61,18 @@ const isDatasetDetailPathname = (pathname: string) => {
return true
}
const isAgentDetailPathname = (pathname: string) => {
const [section, type, agentId] = pathname.split('/').filter(Boolean)
return section === 'roster' && type === 'agent' && !!agentId
}
const isSnippetDetailPathname = (pathname: string) => {
const [section, snippetId] = pathname.split('/').filter(Boolean)
return section === 'snippets' && !!snippetId
}
const MainNav = ({
className,
}: MainNavProps) => {
@ -70,7 +83,9 @@ const MainNav = ({
const showEnvTag = langGeniusVersionInfo.current_env === 'TESTING' || langGeniusVersionInfo.current_env === 'DEVELOPMENT'
const showAppDetailNavigation = !isCurrentWorkspaceDatasetOperator && pathname.startsWith('/app/')
const showDatasetDetailNavigation = isDatasetDetailPathname(pathname)
const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation
const showAgentDetailNavigation = !isCurrentWorkspaceDatasetOperator && isAgentDetailPathname(pathname)
const showSnippetDetailBottomNavigation = isSnippetDetailPathname(pathname)
const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation || showAgentDetailNavigation
const { hasAppDetail, appSidebarExpand, setAppDetail, setAppSidebarExpand } = useAppStore(useShallow(state => ({
hasAppDetail: !!state.appDetail,
appSidebarExpand: state.appSidebarExpand,
@ -87,7 +102,9 @@ const MainNav = ({
const detailNavigationTransitionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const isDetailNavigationHoverPreviewOpen = isCollapsedDetailNavigation && detailNavigationHoverPreviewOpen
const detailNavigationVisibleExpanded = detailNavigationExpanded || isDetailNavigationHoverPreviewOpen
const bottomNavigationExpanded = !showDetailNavigation || detailNavigationVisibleExpanded
const bottomNavigationExpanded = showSnippetDetailBottomNavigation
? false
: !showDetailNavigation || detailNavigationVisibleExpanded
const handleToggleDetailNavigation = useCallback(() => {
if (isDetailNavigationHoverPreviewOpen) {
if (detailNavigationTransitionTimerRef.current)
@ -173,6 +190,13 @@ const MainNav = ({
icon: 'i-custom-vender-main-nav-studio',
activeIcon: 'i-custom-vender-main-nav-studio-active',
},
{
href: '/roster',
label: t('menus.roster', { ns: 'common' }),
active: (path: string) => path.startsWith('/roster'),
icon: 'i-custom-vender-main-nav-roster',
activeIcon: 'i-custom-vender-main-nav-roster-active',
},
]
: []),
...((isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator)
@ -206,23 +230,27 @@ const MainNav = ({
},
], [isCurrentWorkspaceDatasetOperator, isCurrentWorkspaceEditor, t])
const renderLogo = () => (
<Link
href="/"
className="flex h-8 shrink-0 items-center overflow-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
aria-label={systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'}
>
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? (
<img
src={systemFeatures.branding.workspace_logo}
className="block h-5.5 w-auto object-contain"
alt=""
/>
)
: <DifyLogo alt="" />}
</Link>
)
const renderLogo = () => {
const appTitle = systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'
return (
<Link
href="/"
className="flex h-8 shrink-0 items-center overflow-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
aria-label={appTitle}
>
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? (
<img
src={systemFeatures.branding.workspace_logo}
className="block h-5.5 w-auto object-contain"
alt=""
/>
)
: <DifyLogo alt="" />}
</Link>
)
}
return (
<aside
@ -234,7 +262,9 @@ const MainNav = ({
? detailNavigationExpanded
? 'w-[248px] bg-background-body p-1'
: 'w-16 bg-background-body p-1'
: 'w-60 flex-col',
: showSnippetDetailBottomNavigation
? 'w-16 bg-background-body p-1'
: 'w-60 flex-col',
'bg-background-body',
className,
)}
@ -261,38 +291,51 @@ const MainNav = ({
onToggle={handleToggleDetailNavigation}
/>
)
: showAgentDetailNavigation
? (
<AgentDetailTop
expand={detailNavigationVisibleExpanded}
onToggle={handleToggleDetailNavigation}
/>
)
: (
<DatasetDetailTop
expand={detailNavigationVisibleExpanded}
onToggle={handleToggleDetailNavigation}
/>
)
: showSnippetDetailBottomNavigation
? null
: (
<DatasetDetailTop
expand={detailNavigationVisibleExpanded}
onToggle={handleToggleDetailNavigation}
/>
)
: (
<>
<div className="flex items-center justify-between pt-3 pr-2 pb-2 pl-4">
{renderLogo()}
<MainNavSearchButton />
</div>
<div className="p-2">
<WorkspaceCard />
</div>
</>
)}
<>
<div className="flex items-center justify-between pt-3 pr-2 pb-2 pl-4">
{renderLogo()}
<MainNavSearchButton />
</div>
<div className="p-2">
<WorkspaceCard />
</div>
</>
)}
{showDetailNavigation
? showAppDetailNavigation
? <AppDetailSection expand={detailNavigationVisibleExpanded} />
: <DatasetDetailSection expand={detailNavigationVisibleExpanded} />
: (
<>
<nav className="flex flex-col gap-px p-2">
{navItems.map(item => (
<MainNavLink key={item.href} item={item} pathname={pathname} />
))}
</nav>
{!isCurrentWorkspaceDatasetOperator && <WebAppsSection />}
</>
)}
{showEnvTag && detailNavigationVisibleExpanded && (
: showAgentDetailNavigation
? <AgentDetailSection expand={detailNavigationVisibleExpanded} />
: <DatasetDetailSection expand={detailNavigationVisibleExpanded} />
: showSnippetDetailBottomNavigation
? null
: (
<>
<nav className="flex flex-col gap-px p-2">
{navItems.map(item => (
<MainNavLink key={item.href} item={item} pathname={pathname} />
))}
</nav>
{!isCurrentWorkspaceDatasetOperator && <WebAppsSection />}
</>
)}
{showEnvTag && !showSnippetDetailBottomNavigation && detailNavigationVisibleExpanded && (
<div className="relative z-30 mt-auto shrink-0 px-3 pb-2">
<EnvNav />
</div>

View File

@ -1,7 +1,7 @@
import type { ReactNode } from 'react'
import type { Credential, PluginPayload } from '../types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen } from '@testing-library/react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory, CredentialTypeEnum } from '../types'
@ -118,6 +118,24 @@ describe('AuthorizedInNode Component', () => {
expect(screen.getByText('plugin.auth.workspaceDefault'))!.toBeInTheDocument()
})
it('should expose the workspace default credential id when requested', async () => {
const AuthorizedInNode = (await import('../authorized-in-node')).default
const onDefaultCredentialChange = vi.fn()
const pluginPayload = createPluginPayload()
render(
<AuthorizedInNode
pluginPayload={pluginPayload}
onAuthorizationItemClick={vi.fn()}
onDefaultCredentialChange={onDefaultCredentialChange}
/>,
{ wrapper: createWrapper() },
)
await waitFor(() => {
expect(onDefaultCredentialChange).toHaveBeenCalledWith('test-credential-id')
})
})
it('should render credential name when credentialId matches', async () => {
const AuthorizedInNode = (await import('../authorized-in-node')).default
const credential = createCredential({ id: 'selected-id', name: 'My Credential' })

View File

@ -10,6 +10,7 @@ import { RiArrowDownSLine } from '@remixicon/react'
import {
memo,
useCallback,
useEffect,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
@ -22,11 +23,13 @@ type AuthorizedInNodeProps = {
pluginPayload: PluginPayload
onAuthorizationItemClick: (id: string) => void
credentialId?: string
onDefaultCredentialChange?: (id?: string) => void
}
const AuthorizedInNode = ({
pluginPayload,
onAuthorizationItemClick,
credentialId,
onDefaultCredentialChange,
}: AuthorizedInNodeProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
@ -38,6 +41,12 @@ const AuthorizedInNode = ({
invalidPluginCredentialInfo,
notAllowCustomCredential,
} = usePluginAuth(pluginPayload, true, credentialId ? [credentialId] : undefined)
const defaultCredentialId = credentials.find(c => c.is_default)?.id
useEffect(() => {
onDefaultCredentialChange?.(defaultCredentialId)
}, [defaultCredentialId, onDefaultCredentialChange])
const renderTrigger = useCallback((open?: boolean) => {
let label = ''
let removed = false
@ -108,9 +117,15 @@ const AuthorizedInNode = ({
},
]
const handleAuthorizationItemClick = useCallback((id: string) => {
onAuthorizationItemClick(id)
onAuthorizationItemClick(
id === '__workspace_default__' && onDefaultCredentialChange
? defaultCredentialId || id
: id,
)
setIsOpen(false)
}, [
defaultCredentialId,
onDefaultCredentialChange,
onAuthorizationItemClick,
setIsOpen,
])

View File

@ -3,7 +3,6 @@ import type { CredentialFormSchemaBase } from '../header/account-setting/model-p
import type { AutoUpdateConfig } from './reference-setting-modal/auto-update-setting/types'
import type { TypeWithI18N } from '@/app/components/base/form/types'
import type { Collection, ToolCredential } from '@/app/components/tools/types'
import type { AgentFeature } from '@/app/components/workflow/nodes/agent/types'
import type { Locale } from '@/i18n-config'
export enum PluginCategoryEnum {
@ -567,6 +566,12 @@ export type StrategyDetail = {
features: AgentFeature[]
}
export const AgentFeature = {
HISTORY_MESSAGES: 'history-messages',
} as const
export type AgentFeature = typeof AgentFeature[keyof typeof AgentFeature]
type Identity = {
author: string
name: string

View File

@ -1,4 +1,5 @@
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
import type { I18nKeysWithPrefix } from '@/types/i18n'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
@ -33,7 +34,7 @@ export const useAvailableNodesMetaData = () => {
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
const { metaData } = node
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}` as I18nKeysWithPrefix<'workflow', 'blocksAbout.'>, { ns: 'workflow' })
return {
...node,
metaData: {
@ -44,7 +45,7 @@ export const useAvailableNodesMetaData = () => {
},
defaultValue: {
...node.defaultValue,
type: metaData.type,
type: metaData.type === BlockEnum.AgentV2 ? BlockEnum.Agent : metaData.type,
title,
},
}

View File

@ -2,6 +2,7 @@
import type { IntegrationSection } from '@/app/components/integrations/routes'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import MenuDialog from '@/app/components/header/account-setting/menu-dialog'
@ -10,23 +11,35 @@ import { getMarketplaceUrl } from '@/utils/var'
type IntegrationsSettingModalProps = {
section: IntegrationSection
source?: 'agent'
onCancel: () => void
onSectionChange: (section: IntegrationSection) => void
}
export default function IntegrationsSettingModal({
section,
source,
onCancel,
onSectionChange,
}: IntegrationsSettingModalProps) {
const { t } = useTranslation()
const isAgentSource = source === 'agent'
const handleSwitchToMarketplace = useCallback((path: string) => {
window.open(getMarketplaceUrl(path), '_blank', 'noopener,noreferrer')
}, [])
return (
<MenuDialog show onClose={onCancel}>
<div className="mx-auto flex h-dvh w-[min(1440px,calc(100vw-48px))] shrink-0 py-6">
<MenuDialog
show
backdropClassName={isAgentSource ? 'bg-background-overlay' : undefined}
className={isAgentSource ? 'bg-transparent backdrop-blur-none' : undefined}
onClose={onCancel}
>
<div className={cn(
'mx-auto flex h-dvh w-[min(1440px,calc(100vw-48px))] shrink-0 py-6',
isAgentSource && 'w-full p-6',
)}
>
<div className="relative flex min-h-0 w-full shrink-0 overflow-hidden rounded-2xl border border-divider-subtle bg-components-panel-bg shadow-2xl">
<div className="fixed top-6 right-6 z-9999 flex flex-col items-center">
<Button

View File

@ -1,5 +1,5 @@
import type {
PluginDefaultValue,
BlockDefaultValue,
TriggerDefaultValue,
} from '@/app/components/workflow/block-selector/types'
import type { EnvironmentVariable } from '@/app/components/workflow/types'
@ -98,7 +98,7 @@ const WorkflowChildren = () => {
handleOnboardingClose()
}, [handleOnboardingClose])
const handleSelectStartNode = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
const handleSelectStartNode = useCallback((nodeType: BlockEnum, toolConfig?: BlockDefaultValue) => {
const nodeDefault = availableNodesMetaData.nodesMap?.[nodeType]
if (!nodeDefault?.defaultValue)
return

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import type { PluginDefaultValue } from '@/app/components/workflow/block-selector/types'
import type { BlockDefaultValue } from '@/app/components/workflow/block-selector/types'
import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
import { useTranslation } from 'react-i18next'
import { BlockEnum } from '@/app/components/workflow/types'
@ -9,7 +9,7 @@ import StartNodeSelectionPanel from './start-node-selection-panel'
type WorkflowOnboardingModalProps = {
isShow: boolean
onClose: () => void
onSelectStartNode: (nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => void
onSelectStartNode: (nodeType: BlockEnum, toolConfig?: BlockDefaultValue) => void
}
const WorkflowOnboardingModal: FC<WorkflowOnboardingModalProps> = ({

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import type { PluginDefaultValue } from '@/app/components/workflow/block-selector/types'
import type { BlockDefaultValue } from '@/app/components/workflow/block-selector/types'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import NodeSelector from '@/app/components/workflow/block-selector'
@ -10,7 +10,7 @@ import StartNodeOption from './start-node-option'
type StartNodeSelectionPanelProps = {
onSelectUserInput: () => void
onSelectTrigger: (nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => void
onSelectTrigger: (nodeType: BlockEnum, toolConfig?: BlockDefaultValue) => void
}
const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
@ -20,7 +20,7 @@ const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
const { t } = useTranslation()
const [showTriggerSelector, setShowTriggerSelector] = useState(false)
const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: BlockDefaultValue) => {
setShowTriggerSelector(false)
onSelectTrigger(nodeType, toolConfig)
}, [onSelectTrigger])

View File

@ -1,5 +1,6 @@
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import type { I18nKeysWithPrefix } from '@/types/i18n'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
@ -48,7 +49,7 @@ export const useAvailableNodesMetaData = () => {
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
const { metaData } = node
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}` as I18nKeysWithPrefix<'workflow', 'blocksAbout.'>, { ns: 'workflow' })
const helpLinkPath = `/use-dify/nodes/${metaData.helpLinkUri}` as DocPathWithoutLang
return {
...node,
@ -60,7 +61,7 @@ export const useAvailableNodesMetaData = () => {
},
defaultValue: {
...node.defaultValue,
type: metaData.type,
type: metaData.type === BlockEnum.AgentV2 ? BlockEnum.Agent : metaData.type,
title,
},
}

View File

@ -16,6 +16,8 @@ const mockCustomNode = vi.hoisted(() => vi.fn())
const mockCustomNoteNode = vi.hoisted(() => vi.fn())
const mockGetIterationStartNode = vi.hoisted(() => vi.fn())
const mockGetLoopStartNode = vi.hoisted(() => vi.fn())
const mockCreateInlineAgentBinding = vi.hoisted(() => vi.fn())
const mockSetOpenInlineAgentPanelNodeId = vi.hoisted(() => vi.fn())
vi.mock('ahooks', () => ({
useEventListener: (...args: unknown[]) => mockUseEventListener(...args),
@ -68,6 +70,12 @@ vi.mock('@/app/components/workflow/note-node', () => ({
},
}))
vi.mock('@/app/components/workflow/nodes/agent-v2/hooks', () => ({
useCreateInlineAgentBinding: () => ({
createInlineAgentBinding: mockCreateInlineAgentBinding,
}),
}))
vi.mock('@/app/components/workflow/utils', () => ({
getIterationStartNode: (...args: unknown[]) => mockGetIterationStartNode(...args),
getLoopStartNode: (...args: unknown[]) => mockGetLoopStartNode(...args),
@ -102,6 +110,9 @@ describe('CandidateNodeMain', () => {
mockUseEventListener.mockImplementation((event: 'click' | 'contextmenu', handler: (event: { preventDefault: () => void }) => void) => {
eventHandlers[event] = handler
})
mockSetNodes.mockImplementation((nextNodes) => {
nodes = nextNodes
})
mockUseStoreApi.mockReturnValue({
getState: () => ({
getNodes: () => nodes,
@ -126,6 +137,9 @@ describe('CandidateNodeMain', () => {
},
}))
mockUseWorkflowStore.mockReturnValue({
getState: () => ({
setOpenInlineAgentPanelNodeId: mockSetOpenInlineAgentPanelNodeId,
}),
setState: mockWorkflowStoreSetState,
})
mockUseHooks.mockReturnValue({
@ -137,6 +151,17 @@ describe('CandidateNodeMain', () => {
mockHandleSyncWorkflowDraft.mockImplementation((_isSync: boolean, _force: boolean, options?: { onSuccess?: () => void }) => {
options?.onSuccess?.()
})
mockCreateInlineAgentBinding.mockImplementation((_nodeId: string, options?: { onSuccess?: (binding: {
binding_type: 'inline_agent'
agent_id: string
current_snapshot_id: string
}) => void }) => {
options?.onSuccess?.({
binding_type: 'inline_agent',
agent_id: 'inline-agent-1',
current_snapshot_id: 'inline-snapshot-1',
})
})
mockGetIterationStartNode.mockReturnValue(createNode({ id: 'iteration-start' }))
mockGetLoopStartNode.mockReturnValue(createNode({ id: 'loop-start' }))
})
@ -201,6 +226,97 @@ describe('CandidateNodeMain', () => {
expect(mockHandleNodeSelect).toHaveBeenCalledWith('candidate-note')
})
it('should sync draft immediately when committing an Agent v2 node', () => {
const candidateNode = createNode({
id: 'candidate-agent-v2',
type: CUSTOM_NODE,
data: {
type: BlockEnum.Agent,
title: 'Agent Candidate',
agent_binding: {
binding_type: 'roster_agent',
agent_id: 'agent-1',
},
agent_node_kind: 'dify_agent',
version: '2',
_isCandidate: true,
},
})
render(<CandidateNodeMain candidateNode={candidateNode} />)
eventHandlers.click?.({ preventDefault: vi.fn() })
expect(mockSetNodes).toHaveBeenCalledWith(expect.arrayContaining([
expect.objectContaining({
id: 'candidate-agent-v2',
data: expect.objectContaining({
agent_binding: {
binding_type: 'roster_agent',
agent_id: 'agent-1',
},
agent_node_kind: 'dify_agent',
version: '2',
_isCandidate: false,
}),
}),
]))
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true, true)
})
it('should create inline binding before syncing a start-from-scratch Agent v2 node', () => {
const candidateNode = createNode({
id: 'candidate-inline-agent-v2',
type: CUSTOM_NODE,
data: {
type: BlockEnum.Agent,
title: 'Agent Candidate',
agent_binding: {
binding_type: 'inline_agent',
},
agent_node_kind: 'dify_agent',
version: '2',
_isCandidate: true,
},
})
render(<CandidateNodeMain candidateNode={candidateNode} />)
eventHandlers.click?.({ preventDefault: vi.fn() })
expect(mockCreateInlineAgentBinding).toHaveBeenCalledWith('candidate-inline-agent-v2', expect.objectContaining({
onSuccess: expect.any(Function),
}))
expect(mockSetNodes.mock.calls[0]?.[0]).toEqual(expect.arrayContaining([
expect.objectContaining({
id: 'candidate-inline-agent-v2',
data: expect.objectContaining({
agent_binding: {
binding_type: 'inline_agent',
},
_isTempNode: true,
}),
}),
]))
expect(mockSetNodes).toHaveBeenLastCalledWith(expect.arrayContaining([
expect.objectContaining({
id: 'candidate-inline-agent-v2',
data: expect.objectContaining({
agent_binding: {
binding_type: 'inline_agent',
agent_id: 'inline-agent-1',
current_snapshot_id: 'inline-snapshot-1',
},
}),
}),
]))
const finalNodes = mockSetNodes.mock.calls.at(-1)?.[0]
const finalAgentNode = finalNodes.find((node: { id: string }) => node.id === 'candidate-inline-agent-v2')
expect(finalAgentNode.data._isTempNode).toBeUndefined()
expect(mockSetOpenInlineAgentPanelNodeId).toHaveBeenCalledWith('candidate-inline-agent-v2')
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true, true)
})
it('should append iteration and loop start helper nodes for control-flow candidates', () => {
const iterationNode = createNode({
id: 'candidate-iteration',

View File

@ -68,6 +68,7 @@ const DEFAULT_ICON_MAP: Record<BlockEnum, React.ComponentType<{ className: strin
[BlockEnum.DocExtractor]: DocsExtractor,
[BlockEnum.ListFilter]: ListFilter,
[BlockEnum.Agent]: Agent,
[BlockEnum.AgentV2]: Agent,
[BlockEnum.KnowledgeBase]: KnowledgeBase,
[BlockEnum.DataSource]: Datasource,
[BlockEnum.DataSourceEmpty]: () => null,
@ -118,6 +119,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
[BlockEnum.DocExtractor]: 'bg-util-colors-green-green-500',
[BlockEnum.ListFilter]: 'bg-util-colors-cyan-cyan-500',
[BlockEnum.Agent]: 'bg-util-colors-indigo-indigo-500',
[BlockEnum.AgentV2]: 'bg-util-colors-indigo-indigo-500',
[BlockEnum.HumanInput]: 'bg-util-colors-cyan-cyan-500',
[BlockEnum.KnowledgeBase]: 'bg-util-colors-warning-warning-500',
[BlockEnum.DataSource]: 'bg-components-icon-bg-midnight-solid',

View File

@ -1,7 +1,12 @@
import type { NodeDefault } from '../../types'
import { render, screen } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { FlowType } from '@/types/common'
import { HooksStoreContext } from '../../hooks-store/provider'
import { createHooksStore } from '../../hooks-store/store'
import { BlockEnum } from '../../types'
import { AgentSelectorContent } from '../agent-selector'
import Blocks from '../blocks'
import { BlockClassificationEnum } from '../types'
@ -10,6 +15,12 @@ const runtimeState = vi.hoisted(() => ({
nodes: [] as Array<{ data: { type?: BlockEnum } }>,
}))
const queryMocks = vi.hoisted(() => ({
inviteOptionsQueryFn: vi.fn(),
versionDetailGet: vi.fn(),
toastError: vi.fn(),
}))
vi.mock('reactflow', () => ({
useStoreApi: () => ({
getState: () => ({
@ -26,10 +37,48 @@ vi.mock('@/app/components/app/store', () => ({
}),
}))
const createBlock = (type: BlockEnum, title: string, classification = BlockClassificationEnum.Default): NodeDefault => ({
vi.mock('@/service/client', () => ({
consoleQuery: {
agent: {
byAgentId: {
versions: {
byVersionId: {
get: {
queryOptions: ({ input }: { input: unknown }) => ({
queryKey: ['agent-version-detail', input],
queryFn: () => queryMocks.versionDetailGet(input),
}),
},
},
},
},
inviteOptions: {
get: {
queryOptions: (options: unknown) => ({
queryKey: ['agents', 'invite-options', options],
queryFn: () => queryMocks.inviteOptionsQueryFn(options),
}),
},
},
},
},
}))
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: {
error: (message: string) => queryMocks.toastError(message),
},
}))
const createBlock = (
type: BlockEnum,
title: string,
classification = BlockClassificationEnum.Default,
sort = 0,
): NodeDefault => ({
metaData: {
classification,
sort: 0,
sort,
type,
title,
author: 'Dify',
@ -39,6 +88,17 @@ const createBlock = (type: BlockEnum, title: string, classification = BlockClass
checkValid: () => ({ isValid: true }),
})
function createDeferred<T>() {
let resolve!: (value: T) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
resolve = resolvePromise
reject = rejectPromise
})
return { promise, reject, resolve }
}
describe('Blocks', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -65,12 +125,12 @@ describe('Blocks', () => {
/>,
)
expect(screen.getByText('LLM')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'LLM' })).toBeInTheDocument()
expect(screen.getByText('Exit Loop')).toBeInTheDocument()
expect(screen.getByText('workflow.nodes.loop.loopNode')).toBeInTheDocument()
expect(screen.queryByText('Knowledge Retrieval')).not.toBeInTheDocument()
await user.click(screen.getByText('LLM'))
await user.click(screen.getByRole('button', { name: 'LLM' }))
expect(onSelect).toHaveBeenCalledWith(BlockEnum.LLM)
})
@ -87,4 +147,396 @@ describe('Blocks', () => {
expect(screen.getByText('workflow.tabs.noResult')).toBeInTheDocument()
})
it('opens the agent selector on Agent block hover', async () => {
const user = userEvent.setup()
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
data: [],
has_more: false,
limit: 8,
page: 1,
total: 0,
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const hooksStore = createHooksStore({
configsMap: {
flowId: 'app-1',
flowType: FlowType.appFlow,
fileSettings: {} as never,
},
})
render(
<QueryClientProvider client={queryClient}>
<HooksStoreContext value={hooksStore}>
<Blocks
searchText=""
onSelect={vi.fn()}
availableBlocksTypes={[BlockEnum.AgentV2]}
blocks={[createBlock(BlockEnum.AgentV2, 'Agent')]}
/>
</HooksStoreContext>
</QueryClientProvider>,
)
await user.hover(screen.getByRole('button', { name: 'Agent' }))
expect(await screen.findByRole('dialog', { name: 'agentV2.roster.nodeSelector.dialogLabel' })).toBeInTheDocument()
})
it('opens the agent selector from the Agent block and selects an agent', async () => {
const user = userEvent.setup()
const onSelect = vi.fn()
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
data: [
{
id: 'agent-1',
name: 'Nadia',
description: 'Clarification Drafter',
active_config_snapshot_id: 'version-1',
role: 'Researcher',
agent_kind: 'dify_agent',
icon: 'A',
icon_background: '#E9D7FE',
icon_type: 'emoji',
scope: 'roster',
source: 'workflow',
status: 'active',
},
],
has_more: false,
limit: 8,
page: 1,
total: 1,
})
queryMocks.versionDetailGet.mockResolvedValue({
config_snapshot: {
model: {
model: 'gpt-4o',
model_provider: 'openai',
},
},
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const hooksStore = createHooksStore({
configsMap: {
flowId: 'app-1',
flowType: FlowType.appFlow,
fileSettings: {} as never,
},
})
render(
<QueryClientProvider client={queryClient}>
<HooksStoreContext value={hooksStore}>
<Blocks
searchText=""
onSelect={onSelect}
availableBlocksTypes={[BlockEnum.LLM, BlockEnum.AgentV2]}
blocks={[
createBlock(BlockEnum.LLM, 'LLM', BlockClassificationEnum.Default, 0),
createBlock(BlockEnum.AgentV2, 'Agent', BlockClassificationEnum.Default, 3),
]}
/>
</HooksStoreContext>
</QueryClientProvider>,
)
expect(
screen.getByText('Agent').compareDocumentPosition(screen.getByText('LLM')) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy()
await user.click(screen.getByRole('button', { name: 'Agent' }))
expect(await screen.findByRole('dialog', { name: 'agentV2.roster.nodeSelector.dialogLabel' })).toBeInTheDocument()
expect(screen.getByRole('combobox', { name: 'agentV2.roster.searchLabel' })).toBeInTheDocument()
expect(await screen.findByText('Nadia')).toBeInTheDocument()
expect(screen.getByText('Researcher')).toBeInTheDocument()
await user.click(screen.getByRole('option', { name: 'Nadia Researcher' }))
await waitFor(() => expect(queryMocks.versionDetailGet).toHaveBeenCalledWith({
params: {
agent_id: 'agent-1',
version_id: 'version-1',
},
}))
expect(onSelect).toHaveBeenCalledWith(BlockEnum.AgentV2, {
agent_binding: {
binding_type: 'roster_agent',
agent_id: 'agent-1',
},
agent_node_kind: 'dify_agent',
version: '2',
})
expect(queryMocks.inviteOptionsQueryFn).toHaveBeenCalledWith({
input: {
query: {
app_id: 'app-1',
limit: 8,
page: 1,
},
},
})
})
it('keeps the agent list visible while validating a selected agent', async () => {
const user = userEvent.setup()
const onSelect = vi.fn()
const versionDetail = createDeferred<{
config_snapshot: {
model: {
model: string
model_provider: string
}
}
}>()
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
data: [
{
id: 'agent-1',
name: 'Nadia',
description: 'Clarification Drafter',
active_config_snapshot_id: 'version-1',
role: 'Researcher',
agent_kind: 'dify_agent',
icon: 'A',
icon_background: '#E9D7FE',
icon_type: 'emoji',
scope: 'roster',
source: 'workflow',
status: 'active',
},
],
has_more: false,
limit: 8,
page: 1,
total: 1,
})
queryMocks.versionDetailGet.mockReturnValue(versionDetail.promise)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const hooksStore = createHooksStore({
configsMap: {
flowId: 'app-1',
flowType: FlowType.appFlow,
fileSettings: {} as never,
},
})
render(
<QueryClientProvider client={queryClient}>
<HooksStoreContext value={hooksStore}>
<AgentSelectorContent
open
onOpenChange={vi.fn()}
onSelect={onSelect}
/>
</HooksStoreContext>
</QueryClientProvider>,
)
expect(await screen.findByText('Nadia')).toBeInTheDocument()
await user.click(screen.getByRole('option', { name: 'Nadia Researcher' }))
await waitFor(() => expect(queryMocks.versionDetailGet).toHaveBeenCalled())
expect(screen.getByText('Nadia')).toBeInTheDocument()
expect(screen.queryByText('common.loading')).not.toBeInTheDocument()
versionDetail.resolve({
config_snapshot: {
model: {
model: 'gpt-4o',
model_provider: 'openai',
},
},
})
await waitFor(() => expect(onSelect).toHaveBeenCalled())
})
it('does not select an Agent v2 roster agent without model config', async () => {
const user = userEvent.setup()
const onSelect = vi.fn()
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
data: [
{
id: 'agent-1',
name: 'Nadia',
description: 'Clarification Drafter',
active_config_snapshot_id: 'version-1',
role: 'Researcher',
agent_kind: 'dify_agent',
icon: 'A',
icon_background: '#E9D7FE',
icon_type: 'emoji',
scope: 'roster',
source: 'workflow',
status: 'active',
},
],
has_more: false,
limit: 8,
page: 1,
total: 1,
})
queryMocks.versionDetailGet.mockResolvedValue({
config_snapshot: {},
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const hooksStore = createHooksStore({
configsMap: {
flowId: 'app-1',
flowType: FlowType.appFlow,
fileSettings: {} as never,
},
})
render(
<QueryClientProvider client={queryClient}>
<HooksStoreContext value={hooksStore}>
<Blocks
searchText=""
onSelect={onSelect}
availableBlocksTypes={[BlockEnum.AgentV2]}
blocks={[createBlock(BlockEnum.AgentV2, 'Agent', BlockClassificationEnum.Default, 3)]}
/>
</HooksStoreContext>
</QueryClientProvider>,
)
await user.click(screen.getByRole('button', { name: 'Agent' }))
expect(await screen.findByText('Nadia')).toBeInTheDocument()
await user.click(screen.getByRole('option', { name: 'Nadia Researcher' }))
await waitFor(() => expect(queryMocks.toastError).toHaveBeenCalledWith('workflow.nodes.agent.modelNotSelected'))
expect(onSelect).not.toHaveBeenCalled()
})
it('inserts an inline Agent v2 node from the selector start action', async () => {
const user = userEvent.setup()
const onSelect = vi.fn()
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
data: [],
has_more: false,
limit: 8,
page: 1,
total: 0,
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const hooksStore = createHooksStore({
configsMap: {
flowId: 'app-1',
flowType: FlowType.appFlow,
fileSettings: {} as never,
},
})
render(
<QueryClientProvider client={queryClient}>
<HooksStoreContext value={hooksStore}>
<Blocks
searchText=""
onSelect={onSelect}
availableBlocksTypes={[BlockEnum.AgentV2]}
blocks={[createBlock(BlockEnum.AgentV2, 'Agent', BlockClassificationEnum.Default, 3)]}
/>
</HooksStoreContext>
</QueryClientProvider>,
)
await user.click(screen.getByRole('button', { name: 'Agent' }))
await user.click(await screen.findByRole('button', { name: 'agentV2.roster.nodeSelector.startFromScratch' }))
expect(onSelect).toHaveBeenCalledWith(BlockEnum.AgentV2, {
agent_binding: {
binding_type: 'inline_agent',
},
agent_node_kind: 'dify_agent',
version: '2',
})
})
it('closes the agent selector when Escape closes the combobox', async () => {
const user = userEvent.setup()
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
data: [],
has_more: false,
limit: 8,
page: 1,
total: 0,
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const hooksStore = createHooksStore({
configsMap: {
flowId: 'app-1',
flowType: FlowType.appFlow,
fileSettings: {} as never,
},
})
render(
<QueryClientProvider client={queryClient}>
<HooksStoreContext value={hooksStore}>
<Blocks
searchText=""
onSelect={vi.fn()}
availableBlocksTypes={[BlockEnum.AgentV2]}
blocks={[createBlock(BlockEnum.AgentV2, 'Agent')]}
/>
</HooksStoreContext>
</QueryClientProvider>,
)
await user.click(screen.getByRole('button', { name: 'Agent' }))
expect(await screen.findByRole('dialog', { name: 'agentV2.roster.nodeSelector.dialogLabel' })).toBeInTheDocument()
await user.click(screen.getByRole('combobox', { name: 'agentV2.roster.searchLabel' }))
await user.keyboard('{Escape}')
await waitFor(() => {
expect(screen.queryByRole('dialog', { name: 'agentV2.roster.nodeSelector.dialogLabel' })).not.toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,346 @@
import type { AgentInviteOptionResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { ComboboxRootChangeEventDetails } from '@langgenius/dify-ui/combobox'
import type { NodeDefault } from '../types'
import type { AgentRosterNodeData } from './types'
import { AvatarFallback, AvatarImage, AvatarRoot } from '@langgenius/dify-ui/avatar'
import { cn } from '@langgenius/dify-ui/cn'
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxInputGroup,
ComboboxItem,
ComboboxItemText,
ComboboxList,
ComboboxStatus,
} from '@langgenius/dify-ui/combobox'
import {
Popover,
PopoverContent,
PopoverTitle,
PopoverTrigger,
} from '@langgenius/dify-ui/popover'
import { toast } from '@langgenius/dify-ui/toast'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useDebounce } from 'ahooks'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import Link from '@/next/link'
import { consoleQuery } from '@/service/client'
import BlockIcon from '../block-icon'
const AGENT_SELECTOR_PAGE_SIZE = 8
export function AgentSelectorContent({
open,
onOpenChange,
onSelect,
onStartFromScratch,
}: {
open: boolean
onOpenChange: (open: boolean) => void
onSelect: (agent: AgentRosterNodeData) => void
onStartFromScratch?: () => void
}) {
const { t } = useTranslation(['agentV2', 'common', 'workflow'])
const queryClient = useQueryClient()
const appId = useHooksStore(s => s.configsMap?.flowId)
const [searchText, setSearchText] = useState('')
const [validatingAgentId, setValidatingAgentId] = useState<string>()
const debouncedSearchText = useDebounce(searchText.trim(), { wait: 300 })
const agentsQuery = useQuery({
...consoleQuery.agent.inviteOptions.get.queryOptions({
input: {
query: {
limit: AGENT_SELECTOR_PAGE_SIZE,
page: 1,
...(appId ? { app_id: appId } : {}),
...(debouncedSearchText ? { keyword: debouncedSearchText } : {}),
},
},
}),
})
const agents = agentsQuery.data?.data ?? []
const handleInputValueChange = (nextSearchText: string, details: ComboboxRootChangeEventDetails) => {
if (details.reason !== 'item-press')
setSearchText(nextSearchText)
}
const handleValueChange = async (agent: AgentInviteOptionResponse | null) => {
if (!agent || validatingAgentId)
return
if (!agent.active_config_snapshot_id) {
toast.error(t('nodes.agent.modelNotSelected', { ns: 'workflow' }))
return
}
setValidatingAgentId(agent.id)
try {
const activeConfigSnapshot = await queryClient.fetchQuery(consoleQuery.agent.byAgentId.versions.byVersionId.get.queryOptions({
input: {
params: {
agent_id: agent.id,
version_id: agent.active_config_snapshot_id,
},
},
}))
if (!activeConfigSnapshot.config_snapshot.model) {
toast.error(t('nodes.agent.modelNotSelected', { ns: 'workflow' }))
return
}
onSelect(toAgentRosterNodeData(agent))
}
catch {
toast.error(t('roster.loadingError', { ns: 'agentV2' }))
}
finally {
setValidatingAgentId(undefined)
}
}
const handleOpenChange = (nextOpen: boolean) => {
if (!nextOpen)
onOpenChange(false)
}
const isLoading = agentsQuery.isPending
return (
<div className="w-60 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm">
<Combobox<AgentInviteOptionResponse>
filter={null}
inputValue={searchText}
items={agents}
itemToStringLabel={getAgentLabel}
itemToStringValue={getAgentValue}
open={open}
value={null}
onInputValueChange={handleInputValueChange}
onOpenChange={handleOpenChange}
onValueChange={handleValueChange}
>
<div className="bg-components-panel-bg-blur p-2 pb-1">
<ComboboxInputGroup className="h-8 min-h-8 px-2">
<span aria-hidden className="mr-0.5 i-ri-search-line size-4 shrink-0 text-components-input-text-placeholder" />
<ComboboxInput
aria-label={t('roster.searchLabel', { ns: 'agentV2' })}
placeholder={t('roster.nodeSelector.searchPlaceholder', { ns: 'agentV2' })}
className="block h-4.5 grow px-1 py-0 system-sm-regular text-components-input-text-filled"
/>
</ComboboxInputGroup>
</div>
<div className="max-h-54 overflow-y-auto p-1">
{isLoading && (
<AgentSelectorLoadingSkeleton label={t('loading', { ns: 'common' })} />
)}
{!isLoading && agentsQuery.isError && (
<ComboboxStatus className="px-3 py-2 system-xs-regular">
{t('roster.loadingError', { ns: 'agentV2' })}
</ComboboxStatus>
)}
{!isLoading && !agentsQuery.isError && (
<>
<ComboboxList className="max-h-none overflow-visible p-0">
{(agent: AgentInviteOptionResponse) => (
<AgentSelectorItem key={agent.id} agent={agent} />
)}
</ComboboxList>
<ComboboxEmpty className="px-3 py-2 system-xs-regular">
{debouncedSearchText
? t('roster.emptySearch', { ns: 'agentV2' })
: t('roster.empty', { ns: 'agentV2' })}
</ComboboxEmpty>
</>
)}
</div>
</Combobox>
<div className="border-t border-divider-subtle p-1">
{onStartFromScratch && (
<button
type="button"
className="flex min-h-7 w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left system-sm-regular text-text-secondary hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
onClick={onStartFromScratch}
>
<span aria-hidden className="i-ri-add-line size-4 shrink-0 text-text-tertiary" />
<span className="min-w-0 flex-1 truncate">
{t('roster.nodeSelector.startFromScratch', { ns: 'agentV2' })}
</span>
</button>
)}
<Link
href="/roster"
className="flex min-h-7 w-full items-center gap-2 rounded-md px-2 py-1.5 system-sm-regular text-text-secondary hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
>
<span aria-hidden className="i-ri-arrow-right-up-line size-4 shrink-0 text-text-tertiary" />
<span className="min-w-0 flex-1 truncate">
{t('roster.nodeSelector.manageInAgentConsole', { ns: 'agentV2' })}
</span>
</Link>
</div>
</div>
)
}
function AgentSelectorLoadingSkeleton({
label,
}: {
label: string
}) {
return (
<ComboboxStatus className="p-0">
<span className="sr-only">{label}</span>
<div className="relative overflow-hidden" aria-hidden>
<div className="p-1">
{['skeleton-1', 'skeleton-2', 'skeleton-3', 'skeleton-4'].map((key, index) => (
<div
key={key}
className={cn(
'flex items-center gap-2 py-1.5 pr-3 pl-2 opacity-20',
index === 3 && 'opacity-10',
)}
>
<div className="size-8 shrink-0 rounded-full bg-text-quaternary" />
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
<div className="h-2 w-20 rounded-xs bg-text-quaternary" />
<div className="h-2 w-28 rounded-xs bg-text-quaternary" />
</div>
</div>
))}
</div>
<div className="pointer-events-none absolute inset-0 bg-linear-to-b from-components-panel-bg-transparent to-background-default-subtle" />
</div>
</ComboboxStatus>
)
}
function getAgentLabel(agent: AgentInviteOptionResponse) {
return agent.name
}
function getAgentValue(agent: AgentInviteOptionResponse) {
return agent.id
}
function toAgentRosterNodeData(agent: AgentInviteOptionResponse): AgentRosterNodeData {
return {
description: agent.description,
icon: agent.icon,
icon_background: agent.icon_background,
icon_type: agent.icon_type,
id: agent.id,
name: agent.name,
role: agent.role,
}
}
function AgentSelectorAvatar({
agent,
}: {
agent: AgentInviteOptionResponse
}) {
const imageUrl = (agent.icon_type === 'image' || agent.icon_type === 'link') ? agent.icon : undefined
return (
<AvatarRoot
size="md"
className="border-[0.5px] border-divider-regular text-lg"
style={{ background: imageUrl ? undefined : (agent.icon_background || '#FFEAD5') }}
>
{imageUrl && (
<AvatarImage
src={imageUrl}
alt={agent.name}
/>
)}
<AvatarFallback size="md" className="text-lg text-text-primary-on-surface">
{agent.icon_type === 'emoji' && agent.icon ? agent.icon : agent.name[0]?.toLocaleUpperCase()}
</AvatarFallback>
</AvatarRoot>
)
}
function AgentSelectorItem({
agent,
}: {
agent: AgentInviteOptionResponse
}) {
return (
<ComboboxItem
value={agent}
className="grid-cols-[1fr] gap-0 py-1.5 pr-3 pl-2"
>
<ComboboxItemText className="flex items-center gap-2 px-0">
<span aria-hidden className="shrink-0">
<AgentSelectorAvatar agent={agent} />
</span>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate system-sm-medium text-text-secondary">
{agent.name}
</span>
<span className="truncate system-xs-regular text-text-tertiary">
{agent.role || agent.description}
</span>
</span>
</ComboboxItemText>
</ComboboxItem>
)
}
export function AgentBlockItem({
block,
onSelect,
onStartFromScratch,
}: {
block: NodeDefault
onSelect: (agent: AgentRosterNodeData) => void
onStartFromScratch: () => void
}) {
const { t } = useTranslation('agentV2')
const [open, setOpen] = useState(false)
const handleSelect = (agent: AgentRosterNodeData) => {
setOpen(false)
onSelect(agent)
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
openOnHover
render={(
<button
type="button"
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 text-left hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-popup-open:bg-state-base-hover"
>
<BlockIcon
className="mr-2 shrink-0"
type={block.metaData.type}
/>
<span className="min-w-0 grow truncate system-sm-medium text-text-secondary">
{block.metaData.title}
</span>
<span aria-hidden className="i-custom-vender-solid-general-arrow-down-round-fill size-4 shrink-0 -rotate-90 text-text-tertiary" />
</button>
)}
/>
<PopoverContent
placement="right-start"
sideOffset={4}
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
>
<PopoverTitle className="sr-only">
{t('roster.nodeSelector.dialogLabel')}
</PopoverTitle>
<AgentSelectorContent
open={open}
onOpenChange={setOpen}
onSelect={handleSelect}
onStartFromScratch={() => {
setOpen(false)
onStartFromScratch()
}}
/>
</PopoverContent>
</Popover>
)
}

View File

@ -1,4 +1,4 @@
import type { NodeDefault } from '../types'
import type { NodeDefault, OnSelectBlock } from '../types'
import type { BlockClassificationEnum } from './types'
import {
createPreviewCardHandle,
@ -17,12 +17,13 @@ import { useStoreApi } from 'reactflow'
import Badge from '@/app/components/base/badge'
import BlockIcon from '../block-icon'
import { BlockEnum } from '../types'
import { AgentBlockItem } from './agent-selector'
import { BLOCK_CLASSIFICATIONS } from './constants'
import { useBlocks } from './hooks'
type BlocksProps = {
searchText: string
onSelect: (type: BlockEnum) => void
onSelect: OnSelectBlock
availableBlocksTypes?: BlockEnum[]
blocks?: NodeDefault[]
}
@ -79,7 +80,13 @@ const Blocks = ({
const isEmpty = Object.values(groups).every(list => !list.length)
const renderGroup = useCallback((classification: BlockClassificationEnum) => {
const list = groups[classification]!.sort((a, b) => (a.metaData.sort || 0) - (b.metaData.sort || 0))
const list = [...groups[classification]!].sort((a, b) => {
if (a.metaData.type === BlockEnum.AgentV2)
return -1
if (b.metaData.type === BlockEnum.AgentV2)
return 1
return (a.metaData.sort || 0) - (b.metaData.sort || 0)
})
const { getNodes } = store.getState()
const nodes = getNodes()
const hasKnowledgeBaseNode = nodes.some(node => node.data.type === BlockEnum.KnowledgeBase)
@ -102,39 +109,64 @@ const Blocks = ({
)
}
{
// Preview is supplementary: icon/title/description are all reachable
// from the node that gets added on click (inspector + canvas), so
// hover/focus-only activation is a11y-safe. See
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
filteredList.map(block => (
<PreviewCardTrigger
key={block.metaData.type}
delay={150}
closeDelay={150}
handle={previewCardHandle}
payload={{ block }}
render={(
<div
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
onClick={() => onSelect(block.metaData.type)}
>
<BlockIcon
className="mr-2 shrink-0"
type={block.metaData.type}
/>
<div className="grow text-sm text-text-secondary">{block.metaData.title}</div>
{
block.metaData.type === BlockEnum.LoopEnd && (
<Badge
text={t('nodes.loop.loopNode', { ns: 'workflow' })}
className="ml-2 shrink-0"
/>
)
}
</div>
)}
/>
))
filteredList.map((block) => {
if (block.metaData.type === BlockEnum.AgentV2) {
return (
<AgentBlockItem
key={block.metaData.type}
block={block}
onSelect={agent =>
onSelect(BlockEnum.AgentV2, {
agent_binding: {
binding_type: 'roster_agent',
agent_id: agent.id,
},
agent_node_kind: 'dify_agent',
version: '2',
})}
onStartFromScratch={() =>
onSelect(BlockEnum.AgentV2, {
agent_binding: {
binding_type: 'inline_agent',
},
agent_node_kind: 'dify_agent',
version: '2',
})}
/>
)
}
return (
<PreviewCardTrigger
key={block.metaData.type}
delay={150}
closeDelay={150}
handle={previewCardHandle}
payload={{ block }}
render={(
<button
type="button"
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 text-left hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
onClick={() => onSelect(block.metaData.type)}
>
<BlockIcon
className="mr-2 shrink-0"
type={block.metaData.type}
/>
<span className="min-w-0 grow truncate text-sm text-text-secondary">{block.metaData.title}</span>
{
block.metaData.type === BlockEnum.LoopEnd && (
<Badge
text={t('nodes.loop.loopNode', { ns: 'workflow' })}
className="ml-2 shrink-0"
/>
)
}
</button>
)}
/>
)
})
}
</div>
)

View File

@ -61,6 +61,16 @@ export const ENTRY_NODE_TYPES = [
] as const
export const BLOCKS = [
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.Agent,
title: 'Old Agent',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.AgentV2,
title: 'Agent',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.LLM,
@ -147,9 +157,4 @@ export const BLOCKS = [
type: BlockEnum.ListFilter,
title: 'List Filter',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.Agent,
title: 'Agent',
},
] as const satisfies readonly Block[]

View File

@ -1,7 +1,7 @@
'use client'
import type { OffsetOptions } from '@floating-ui/react'
import type { Placement } from '@langgenius/dify-ui/popover'
import type { FC } from 'react'
import type { ReactNode } from 'react'
import type { ToolDefaultValue, ToolValue } from './types'
import type { CustomCollectionBackend } from '@/app/components/tools/types'
import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types'
@ -14,7 +14,6 @@ import {
import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import SearchBox from '@/app/components/plugins/marketplace/search-box'
@ -37,13 +36,17 @@ import {
} from '@/service/use-tools'
type Props = Readonly<{
panelClassName?: string
disabled: boolean
trigger: React.ReactNode
trigger: ReactNode
placement?: Placement
offset?: OffsetOptions
isShow: boolean
onShowChange: (isShow: boolean) => void
}> & ToolPickerContentProps
export type ToolPickerContentProps = Readonly<{
focusSearchOnMount?: boolean
panelClassName?: string
onSelect: (tool: ToolDefaultValue) => void
onSelectMultiple: (tools: ToolDefaultValue[]) => void
supportAddCustomTool?: boolean
@ -51,25 +54,18 @@ type Props = Readonly<{
selectedTools?: ToolValue[]
}>
const ToolPicker: FC<Props> = ({
disabled,
trigger,
placement = 'right-start',
offset = 0,
isShow,
onShowChange,
export function ToolPickerContent({
focusSearchOnMount = false,
onSelect,
onSelectMultiple,
supportAddCustomTool,
scope = 'all',
selectedTools,
panelClassName,
}) => {
}: ToolPickerContentProps) {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
const [tags, setTags] = useState<string[]>([])
const sideOffset = typeof offset === 'number' ? offset : (typeof offset === 'function' ? 0 : (offset?.mainAxis ?? 0))
const alignOffset = typeof offset === 'number' ? 0 : (typeof offset === 'function' ? 0 : (offset?.crossAxis ?? 0))
const { data: enable_marketplace } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
@ -120,12 +116,6 @@ const ToolPicker: FC<Props> = ({
const handleAddedCustomTool = invalidateCustomTools
const handleOpenChange = (nextOpen: boolean) => {
if (nextOpen && disabled)
return
onShowChange(nextOpen)
}
const handleSelect = (_type: BlockEnum, tool?: ToolDefaultValue) => {
onSelect(tool!)
}
@ -157,6 +147,70 @@ const ToolPicker: FC<Props> = ({
)
}
return (
<div className={cn('relative min-h-20 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-xs', panelClassName)}>
<div className="p-2 pb-1">
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
placeholder={t('searchTools', { ns: 'plugin' })!}
supportAddCustomTool={supportAddCustomTool}
onAddedCustomTool={handleAddedCustomTool}
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
// The picker replaces the focused menu item inside an already-open popover.
// Focusing search keeps keyboard users in the same add-tool workflow.
/* eslint-disable-next-line jsx-a11y/no-autofocus */
autoFocus={focusSearchOnMount}
inputClassName="grow"
/>
</div>
<AllTools
className="mt-1"
toolContentClassName="max-w-full"
tags={tags}
searchText={searchText}
onSelect={handleSelect as OnSelectBlock}
onSelectMultiple={handleSelectMultiple}
buildInTools={builtinToolList || []}
customTools={customToolList || []}
workflowTools={workflowToolList || []}
mcpTools={mcpTools || []}
selectedTools={selectedTools}
onTagsChange={setTags}
featuredPlugins={featuredPlugins}
featuredLoading={isFeaturedLoading}
showFeatured={scope === 'all' && enable_marketplace}
onFeaturedInstallSuccess={async () => {
invalidateBuiltInTools()
invalidateCustomTools()
invalidateWorkflowTools()
invalidateMcpTools()
}}
/>
</div>
)
}
function ToolPicker({
disabled,
trigger,
placement = 'right-start',
offset = 0,
isShow,
onShowChange,
...contentProps
}: Props) {
const sideOffset = typeof offset === 'number' ? offset : (typeof offset === 'function' ? 0 : (offset?.mainAxis ?? 0))
const alignOffset = typeof offset === 'number' ? 0 : (typeof offset === 'function' ? 0 : (offset?.crossAxis ?? 0))
const handleOpenChange = (nextOpen: boolean) => {
if (nextOpen && disabled)
return
onShowChange(nextOpen)
}
return (
<Popover
open={isShow}
@ -175,47 +229,10 @@ const ToolPicker: FC<Props> = ({
alignOffset={alignOffset}
popupClassName="border-none bg-transparent shadow-none"
>
<div className={cn('relative min-h-20 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-xs', panelClassName)}>
<div className="p-2 pb-1">
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
placeholder={t('searchTools', { ns: 'plugin' })!}
supportAddCustomTool={supportAddCustomTool}
onAddedCustomTool={handleAddedCustomTool}
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
inputClassName="grow"
/>
</div>
<AllTools
className="mt-1"
toolContentClassName="max-w-full"
tags={tags}
searchText={searchText}
onSelect={handleSelect as OnSelectBlock}
onSelectMultiple={handleSelectMultiple}
buildInTools={builtinToolList || []}
customTools={customToolList || []}
workflowTools={workflowToolList || []}
mcpTools={mcpTools || []}
selectedTools={selectedTools}
onTagsChange={setTags}
featuredPlugins={featuredPlugins}
featuredLoading={isFeaturedLoading}
showFeatured={scope === 'all' && enable_marketplace}
onFeaturedInstallSuccess={async () => {
invalidateBuiltInTools()
invalidateCustomTools()
invalidateWorkflowTools()
invalidateMcpTools()
}}
/>
</div>
<ToolPickerContent {...contentProps} />
</PopoverContent>
</Popover>
)
}
export default React.memo(ToolPicker)
export default ToolPicker

View File

@ -85,6 +85,7 @@ const ToolItem: FC<Props> = ({
provider_id: provider.id,
provider_type: provider.type,
provider_name: provider.name,
provider_show_name: provider.label[language],
plugin_id: provider.plugin_id,
plugin_unique_identifier: provider.plugin_unique_identifier,
provider_icon: normalizedIcon,

View File

@ -118,6 +118,7 @@ const Tool: FC<Props> = ({
provider_id: payload.id,
provider_type: payload.type,
provider_name: payload.name,
provider_show_name: payload.label[language],
plugin_id: payload.plugin_id!,
plugin_unique_identifier: payload.plugin_unique_identifier!,
provider_icon: normalizedIcon,
@ -148,7 +149,7 @@ const Tool: FC<Props> = ({
: `${selectedToolsNum} / ${totalToolsNum}`}
</span>
)
}, [actions, getIsDisabled, isAllSelected, isHovering, language, onSelectMultiple, payload.id, payload.is_team_authorization, payload.name, payload.type, selectedToolsNum, t, totalToolsNum])
}, [actions, getIsDisabled, isAllSelected, isHovering, language, normalizedIcon, normalizedIconDark, onSelectMultiple, payload.id, payload.is_team_authorization, payload.label, payload.name, payload.plugin_id, payload.plugin_unique_identifier, payload.type, selectedToolsNum, t, totalToolsNum])
if (isFoldHasSearchText !== hasSearchText) {
setIsFoldHasSearchText(hasSearchText)
@ -196,6 +197,7 @@ const Tool: FC<Props> = ({
provider_id: payload.id,
provider_type: payload.type,
provider_name: payload.name,
provider_show_name: payload.label[language],
plugin_id: payload.plugin_id,
plugin_unique_identifier: payload.plugin_unique_identifier,
provider_icon: normalizedIcon,

View File

@ -1,3 +1,4 @@
import type { AgentInviteOptionResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { ParametersSchema, PluginMeta, PluginTriggerSubscriptionConstructor, SupportedCreationMethods, TriggerEvent } from '../../plugins/types'
import type { Collection, Event } from '../../tools/types'
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -48,6 +49,7 @@ export type TriggerDefaultValue = PluginCommonDefaultValue & {
}
export type ToolDefaultValue = PluginCommonDefaultValue & {
provider_show_name?: string
tool_name: string
tool_label: string
tool_description: string
@ -75,8 +77,34 @@ export type DataSourceDefaultValue = Omit<PluginCommonDefaultValue, 'provider_id
plugin_unique_identifier?: string
}
export type AgentRosterNodeData = Pick<
AgentInviteOptionResponse,
'description' | 'icon' | 'icon_background' | 'icon_type' | 'id' | 'name' | 'role'
>
export type AgentRosterBinding = {
binding_type: 'roster_agent'
agent_id: string
}
export type AgentInlineBinding = {
binding_type: 'inline_agent'
agent_id?: string | null
current_snapshot_id?: string | null
}
export type AgentBinding = AgentRosterBinding | AgentInlineBinding
export type AgentDefaultValue = {
agent_binding: AgentBinding
agent_node_kind: 'dify_agent'
version: '2'
}
export type PluginDefaultValue = ToolDefaultValue | DataSourceDefaultValue | TriggerDefaultValue
export type BlockDefaultValue = PluginDefaultValue | AgentDefaultValue
export type ToolValue = {
provider_name: string
provider_show_name?: string

View File

@ -1,4 +1,5 @@
import type {
ComponentProps,
FC,
} from 'react'
import type {
@ -17,6 +18,8 @@ import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-co
import { CUSTOM_NODE } from './constants'
import { useAutoGenerateWebhookUrl, useNodesInteractions, useNodesSyncDraft, useWorkflowHistory, WorkflowHistoryEvent } from './hooks'
import CustomNode from './nodes'
import { useCreateInlineAgentBinding } from './nodes/agent-v2/hooks'
import { isAgentV2NodeData, needsInlineAgentBindingCreation } from './nodes/agent-v2/types'
import CustomNoteNode from './note-node'
import { CUSTOM_NOTE_NODE } from './note-node/constants'
import {
@ -41,18 +44,21 @@ const CandidateNodeMain: FC<Props> = ({
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl()
const collaborativeWorkflow = useCollaborativeWorkflow()
const { createInlineAgentBinding } = useCreateInlineAgentBinding()
useEventListener('click', (e) => {
e.preventDefault()
const { screenToFlowPosition } = reactflow
const { nodes, setNodes } = collaborativeWorkflow.getState()
const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
const shouldCreateInlineAgentBinding = isAgentV2NodeData(candidateNode.data) && needsInlineAgentBindingCreation(candidateNode.data)
const newNodes = produce(nodes, (draft) => {
draft.push({
...candidateNode,
data: {
...candidateNode.data,
_isCandidate: false,
_isTempNode: shouldCreateInlineAgentBinding ? true : candidateNode.data._isTempNode,
},
position: {
x,
@ -81,6 +87,32 @@ const CandidateNodeMain: FC<Props> = ({
onSuccess: () => autoGenerateWebhookUrl(candidateNode.id),
})
}
if (shouldCreateInlineAgentBinding) {
createInlineAgentBinding(candidateNode.id, {
onError: () => {
const { nodes, setNodes } = collaborativeWorkflow.getState()
setNodes(nodes.filter(node => node.id !== candidateNode.id))
},
onSuccess: (binding) => {
const { nodes, setNodes } = collaborativeWorkflow.getState()
setNodes(produce(nodes, (draft) => {
const node = draft.find(node => node.id === candidateNode.id)
if (node) {
if (isAgentV2NodeData(node.data) && needsInlineAgentBindingCreation(node.data))
node.data.agent_binding = binding
delete node.data._isTempNode
}
}))
workflowStore.getState().setOpenInlineAgentPanelNodeId(candidateNode.id)
handleSyncWorkflowDraft(true, true)
},
})
return
}
if (isAgentV2NodeData(candidateNode.data))
handleSyncWorkflowDraft(true, true)
})
useEventListener('contextmenu', (e) => {
@ -100,12 +132,12 @@ const CandidateNodeMain: FC<Props> = ({
>
{
candidateNode.type === CUSTOM_NODE && (
<CustomNode {...candidateNode as any} />
<CustomNode {...candidateNode as unknown as ComponentProps<typeof CustomNode>} />
)
}
{
candidateNode.type === CUSTOM_NOTE_NODE && (
<CustomNoteNode {...candidateNode as any} />
<CustomNoteNode {...candidateNode as unknown as ComponentProps<typeof CustomNoteNode>} />
)
}
</div>

View File

@ -121,13 +121,22 @@ export const SUPPORT_OUTPUT_VARS_NODE = [
BlockEnum.DocExtractor,
BlockEnum.ListFilter,
BlockEnum.Agent,
BlockEnum.AgentV2,
BlockEnum.DataSource,
BlockEnum.HumanInput,
]
export const AGENT_OUTPUT_STRUCT: Var[] = [
{
variable: 'usage',
variable: 'text',
type: VarType.string,
},
{
variable: 'files',
type: VarType.arrayFile,
},
{
variable: 'json',
type: VarType.object,
},
]

View File

@ -1,3 +1,4 @@
import agentV2Default from '@/app/components/workflow/nodes/agent-v2/default'
import agentDefault from '@/app/components/workflow/nodes/agent/default'
import assignerDefault from '@/app/components/workflow/nodes/assigner/default'
import codeDefault from '@/app/components/workflow/nodes/code/default'
@ -26,6 +27,7 @@ export const WORKFLOW_COMMON_NODES = [
llmDefault,
knowledgeRetrievalDefault,
agentDefault,
agentV2Default,
questionClassifierDefault,
ifElseDefault,
iterationDefault,

View File

@ -20,6 +20,7 @@ const mockHandleNodeLoopChildrenCopy = vi.hoisted(() => vi.fn(() => ({
copyChildren: [],
newIdMapping: {},
})))
const mockCreateInlineAgentBinding = vi.hoisted(() => vi.fn())
const runtimeNodesMetaDataMap = vi.hoisted(() => ({
value: {} as Record<string, unknown>,
}))
@ -78,6 +79,12 @@ vi.mock('../use-inspect-vars-crud', () => ({
}),
}))
vi.mock('../../nodes/agent-v2/hooks', () => ({
useCreateInlineAgentBinding: () => ({
createInlineAgentBinding: mockCreateInlineAgentBinding,
}),
}))
vi.mock('../../nodes/iteration/use-interactions', () => ({
useNodeIterationInteractions: () => ({
handleNodeIterationChildDrag: () => ({ restrictPosition: {} }),
@ -107,6 +114,17 @@ describe('useNodesInteractions', () => {
resetReactFlowMockState()
runtimeState.nodesReadOnly = false
runtimeState.workflowReadOnly = false
mockCreateInlineAgentBinding.mockImplementation((_nodeId: string, options?: { onSuccess?: (binding: {
binding_type: 'inline_agent'
agent_id: string
current_snapshot_id: string
}) => void }) => {
options?.onSuccess?.({
binding_type: 'inline_agent',
agent_id: 'inline-agent-1',
current_snapshot_id: 'inline-snapshot-1',
})
})
currentNodes = [
createNode({
id: 'node-1',
@ -452,6 +470,84 @@ describe('useNodesInteractions', () => {
expect(rfState.setEdges).not.toHaveBeenCalled()
})
it('creates an inline agent binding before syncing an added Agent v2 node', () => {
currentNodes = [
createNode({
id: 'node-1',
width: 100,
data: {
type: BlockEnum.Code,
title: 'Code',
desc: '',
},
}),
]
rfState.nodes = currentNodes as unknown as typeof rfState.nodes
rfState.edges = []
rfState.setNodes.mockImplementation((nextNodes) => {
rfState.nodes = nextNodes
})
rfState.setEdges.mockImplementation((nextEdges) => {
rfState.edges = nextEdges
})
runtimeNodesMetaDataMap.value = {
[BlockEnum.AgentV2]: {
defaultValue: {
type: BlockEnum.AgentV2,
title: 'Agent',
desc: '',
agent_node_kind: 'dify_agent',
version: '2',
},
metaData: {
isSingleton: false,
},
},
}
const { result, store } = renderWorkflowHook(() => useNodesInteractions(), {
historyStore: {
nodes: currentNodes,
edges: [],
},
})
act(() => {
result.current.handleNodeAdd(
{
nodeType: BlockEnum.AgentV2,
pluginDefaultValue: {
agent_binding: {
binding_type: 'inline_agent',
},
agent_node_kind: 'dify_agent',
version: '2',
},
},
{ prevNodeId: 'node-1' },
)
})
const agentNode = rfState.nodes.find(node => node.data.type === BlockEnum.AgentV2)
const firstSetNodesPayload = rfState.setNodes.mock.calls[0]?.[0]
const pendingAgentNode = firstSetNodesPayload.find((node: Node) => node.data.type === BlockEnum.AgentV2)
const finalSetNodesPayload = rfState.setNodes.mock.calls.at(-1)?.[0]
const finalAgentNode = finalSetNodesPayload.find((node: Node) => node.data.type === BlockEnum.AgentV2)
expect(pendingAgentNode?.data._isTempNode).toBe(true)
expect(agentNode?.data.agent_binding).toEqual({
binding_type: 'inline_agent',
agent_id: 'inline-agent-1',
current_snapshot_id: 'inline-snapshot-1',
})
expect(finalAgentNode?.data._isTempNode).toBeUndefined()
expect(mockCreateInlineAgentBinding).toHaveBeenCalledWith(agentNode?.id, expect.objectContaining({
onSuccess: expect.any(Function),
}))
expect(store.getState().openInlineAgentPanelNodeId).toBe(agentNode?.id)
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true, true)
})
it('cancels selection state with collaborative nodes snapshot', () => {
currentNodes = [
createNode({

View File

@ -56,6 +56,7 @@ import {
} from '../hooks'
import { useHooksStore } from '../hooks-store/store'
import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils'
import { isAgentV2NodeData } from '../nodes/agent-v2/types'
import { IndexMethodEnum } from '../nodes/knowledge-base/types'
import { getLLMModelIssue, isLLMModelProviderInstalled, LLMModelIssueCode } from '../nodes/llm/utils'
import {
@ -98,6 +99,10 @@ const withFlowType = (moreDataForCheckValid: CheckValidExtraData, flowType?: Flo
}
}
const getNodeMetaType = (data: CommonNodeType) => {
return isAgentV2NodeData(data) ? BlockEnum.AgentV2 : data.type
}
const START_NODE_TYPES: BlockEnum[] = [
BlockEnum.Start,
BlockEnum.TriggerSchedule,
@ -249,7 +254,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
moreDataForCheckValid = getTriggerCheckParams(node!.data as PluginTriggerNodeType, triggerPlugins, language)
const toolIcon = getToolIcon(node!.data)
if (node!.data.type === BlockEnum.Agent) {
if (node!.data.type === BlockEnum.Agent && !isAgentV2NodeData(node!.data)) {
const data = node!.data as AgentNodeType
const isReadyForCheckValid = !!strategyProviders
const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name)
@ -267,7 +272,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
if (node!.type === CUSTOM_NODE) {
const checkData = getCheckData(node!.data)
const validator = nodesExtraData?.[node!.data.type as BlockEnum]?.checkValid
const validator = nodesExtraData?.[getNodeMetaType(node!.data) as BlockEnum]?.checkValid
const isPluginMissing = isNodePluginMissing(node!.data, { builtInTools: buildInTools, customTools, workflowTools, mcpTools, triggerPlugins, dataSourceList })
const errorMessages: string[] = []
@ -522,7 +527,7 @@ export const useChecklistBeforePublish = () => {
if (node!.data.type === BlockEnum.DataSource)
moreDataForCheckValid = getDataSourceCheckParams(node!.data as DataSourceNodeType, dataSourceList || [], language)
if (node!.data.type === BlockEnum.Agent) {
if (node!.data.type === BlockEnum.Agent && !isAgentV2NodeData(node!.data)) {
const data = node!.data as AgentNodeType
const isReadyForCheckValid = !!strategyProviders
const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name)
@ -551,7 +556,7 @@ export const useChecklistBeforePublish = () => {
}
const checkData = getCheckData(node!.data, datasets, embeddingProviderModelMap)
const { errorMessage } = nodesExtraData![node!.data.type as BlockEnum].checkValid(checkData, t, withFlowType(moreDataForCheckValid, flowType))
const { errorMessage } = nodesExtraData![getNodeMetaType(node!.data) as BlockEnum].checkValid(checkData, t, withFlowType(moreDataForCheckValid, flowType))
if (errorMessage) {
toast.error(`[${node!.data.title}] ${errorMessage}`)

View File

@ -7,7 +7,7 @@ import type {
OnConnectStart,
ResizeParamsWithDirection,
} from 'reactflow'
import type { PluginDefaultValue } from '../block-selector/types'
import type { BlockDefaultValue } from '../block-selector/types'
import type { IterationNodeType } from '../nodes/iteration/types'
import type { LoopNodeType } from '../nodes/loop/types'
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
@ -36,6 +36,8 @@ import {
Y_OFFSET,
} from '../constants'
import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
import { useCreateInlineAgentBinding } from '../nodes/agent-v2/hooks'
import { isAgentV2NodeData, needsInlineAgentBindingCreation } from '../nodes/agent-v2/types'
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
@ -50,6 +52,7 @@ import {
getNestedNodePosition,
getNodeCustomTypeByNodeDataType,
getNodesConnectedSourceOrTargetHandleIdsMap,
getNodesWithSameDefaultDataType,
getTopLeftNodePosition,
isClipboardEdgeStructurallyValid,
isClipboardNodeStructurallyValid,
@ -84,6 +87,16 @@ const ENTRY_NODE_WRAPPER_OFFSET = {
y: 21, // Adjusted based on visual testing feedback
} as const
function needsPendingInlineAgentBinding(defaultValue?: BlockDefaultValue) {
if (!defaultValue || !('agent_binding' in defaultValue))
return false
const binding = defaultValue.agent_binding
return binding.binding_type === 'inline_agent'
&& (!binding.agent_id || !binding.current_snapshot_id)
}
const pruneClipboardNodesWithFilteredAncestors = (
sourceNodes: Node[],
candidateNodes: Node[],
@ -174,6 +187,30 @@ export const useNodesInteractions = () => {
redo,
} = useWorkflowHistory()
const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl()
const { createInlineAgentBinding } = useCreateInlineAgentBinding()
const createInlineAgentBindingForNode = useCallback((nodeId: string, options?: {
onError?: () => void
}) => {
createInlineAgentBinding(nodeId, {
onError: () => {
options?.onError?.()
},
onSuccess: (binding) => {
const { nodes, setNodes } = collaborativeWorkflow.getState()
setNodes(produce(nodes, (draft) => {
const node = draft.find(node => node.id === nodeId)
if (node) {
if (isAgentV2NodeData(node.data) && needsInlineAgentBindingCreation(node.data))
node.data.agent_binding = binding
delete node.data._isTempNode
}
}))
workflowStore.getState().setOpenInlineAgentPanelNodeId(nodeId)
handleSyncWorkflowDraft(true, true)
},
})
}, [collaborativeWorkflow, createInlineAgentBinding, handleSyncWorkflowDraft, workflowStore])
const handleNodeDragStart = useCallback<NodeDragHandler>(
(_, node) => {
@ -233,12 +270,12 @@ export const useNodesInteractions = () => {
const currentNode = draft.find(n => n.id === node.id)!
// Check if current dragging node is an entry node
const isCurrentEntryNode = isTriggerNode(node.data.type as any) || node.data.type === BlockEnum.Start
const isCurrentEntryNode = isTriggerNode(node.data.type as BlockEnum) || node.data.type === BlockEnum.Start
// X-axis alignment with offset consideration
if (showVerticalHelpLineNodesLength > 0) {
const targetNode = showVerticalHelpLineNodes[0]
const isTargetEntryNode = isTriggerNode(targetNode!.data.type as any) || targetNode!.data.type === BlockEnum.Start
const isTargetEntryNode = isTriggerNode(targetNode!.data.type as BlockEnum) || targetNode!.data.type === BlockEnum.Start
// Calculate the wrapper position needed to align the inner nodes
// Target inner position = target.position + target.offset
@ -262,7 +299,7 @@ export const useNodesInteractions = () => {
// Y-axis alignment with offset consideration
if (showHorizontalHelpLineNodesLength > 0) {
const targetNode = showHorizontalHelpLineNodes[0]
const isTargetEntryNode = isTriggerNode(targetNode!.data.type as any) || targetNode!.data.type === BlockEnum.Start
const isTargetEntryNode = isTriggerNode(targetNode!.data.type as BlockEnum) || targetNode!.data.type === BlockEnum.Start
const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0
const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0
@ -619,7 +656,7 @@ export const useNodesInteractions = () => {
)
const handleNodeConnectEnd = useCallback<OnConnectEnd>(
(e: any) => {
(e) => {
if (getNodesReadOnly())
return
@ -645,7 +682,8 @@ export const useNodesInteractions = () => {
if (fromNode.parentId !== toNode.parentId)
return
const { x, y } = screenToFlowPosition({ x: e.x, y: e.y })
const pointer = e as { x: number, y: number }
const { x, y } = screenToFlowPosition({ x: pointer.x, y: pointer.y })
if (
fromHandleType === 'source'
@ -880,24 +918,24 @@ export const useNodesInteractions = () => {
return
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
const nodesWithSameType = nodes.filter(
node => node.data.type === nodeType,
)
const nodeMetaData = nodesMetaDataMap?.[nodeType]
if (!nodeMetaData)
return
const { defaultValue } = nodeMetaData
const nodesWithSameType = getNodesWithSameDefaultDataType(nodes, nodeType, defaultValue)
const shouldCreateInlineAgentBinding = nodeType === BlockEnum.AgentV2 && needsPendingInlineAgentBinding(pluginDefaultValue)
const { newNode, newIterationStartNode, newLoopStartNode }
= generateNewNode({
type: getNodeCustomTypeByNodeDataType(nodeType),
data: {
...(defaultValue as any),
...(defaultValue as Node['data']),
title:
nodesWithSameType.length > 0
? `${defaultValue.title} ${nodesWithSameType.length + 1}`
: defaultValue.title,
...pluginDefaultValue,
selected: true,
...(shouldCreateInlineAgentBinding ? { _isTempNode: true } : {}),
_showAddVariablePopup:
(nodeType === BlockEnum.VariableAssigner
|| nodeType === BlockEnum.VariableAggregator)
@ -1141,7 +1179,7 @@ export const useNodesInteractions = () => {
}
}
let nodesConnectedSourceOrTargetHandleIdsMap: Record<string, any>
let nodesConnectedSourceOrTargetHandleIdsMap: ReturnType<typeof getNodesConnectedSourceOrTargetHandleIdsMap>
if (newEdge) {
nodesConnectedSourceOrTargetHandleIdsMap
= getNodesConnectedSourceOrTargetHandleIdsMap(
@ -1420,6 +1458,18 @@ export const useNodesInteractions = () => {
})
setEdges(newEdges)
}
if (isAgentV2NodeData(newNode.data) && needsInlineAgentBindingCreation(newNode.data)) {
saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNode.id })
createInlineAgentBindingForNode(newNode.id, {
onError: () => {
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
setNodes(nodes.filter(node => node.id !== newNode.id))
setEdges(edges.filter(edge => edge.source !== newNode.id && edge.target !== newNode.id))
},
})
return
}
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNode.id })
},
@ -1427,6 +1477,7 @@ export const useNodesInteractions = () => {
getNodesReadOnly,
collaborativeWorkflow,
handleSyncWorkflowDraft,
createInlineAgentBindingForNode,
saveStateToHistory,
workflowStore,
getAfterNodesInSameBranch,
@ -1439,7 +1490,7 @@ export const useNodesInteractions = () => {
currentNodeId: string,
nodeType: BlockEnum,
sourceHandle: string,
pluginDefaultValue?: PluginDefaultValue,
pluginDefaultValue?: BlockDefaultValue,
) => {
if (getNodesReadOnly())
return
@ -1447,13 +1498,12 @@ export const useNodesInteractions = () => {
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
const currentNode = nodes.find(node => node.id === currentNodeId)!
const connectedEdges = getConnectedEdges([currentNode], edges)
const nodesWithSameType = nodes.filter(
node => node.data.type === nodeType,
)
const nodeMetaData = nodesMetaDataMap?.[nodeType]
if (!nodeMetaData)
return
const { defaultValue } = nodeMetaData
const nodesWithSameType = getNodesWithSameDefaultDataType(nodes, nodeType, defaultValue)
const shouldCreateInlineAgentBinding = nodeType === BlockEnum.AgentV2 && needsPendingInlineAgentBinding(pluginDefaultValue)
const {
newNode: newCurrentNode,
newIterationStartNode,
@ -1461,12 +1511,13 @@ export const useNodesInteractions = () => {
} = generateNewNode({
type: getNodeCustomTypeByNodeDataType(nodeType),
data: {
...(defaultValue as any),
...(defaultValue as Node['data']),
title:
nodesWithSameType.length > 0
? `${defaultValue.title} ${nodesWithSameType.length + 1}`
: defaultValue.title,
...pluginDefaultValue,
...(shouldCreateInlineAgentBinding ? { _isTempNode: true } : {}),
_connectedSourceHandleIds: [],
_connectedTargetHandleIds: [],
selected: currentNode.data.selected,
@ -1651,6 +1702,15 @@ export const useNodesInteractions = () => {
onSuccess: () => autoGenerateWebhookUrl(newCurrentNode.id),
})
}
else if (isAgentV2NodeData(newCurrentNode.data) && needsInlineAgentBindingCreation(newCurrentNode.data)) {
createInlineAgentBindingForNode(newCurrentNode.id, {
onError: () => {
const { setNodes, setEdges } = collaborativeWorkflow.getState()
setNodes(nodes)
setEdges(edges)
},
})
}
else {
handleSyncWorkflowDraft()
}
@ -1663,6 +1723,7 @@ export const useNodesInteractions = () => {
getNodesReadOnly,
collaborativeWorkflow,
handleSyncWorkflowDraft,
createInlineAgentBindingForNode,
saveStateToHistory,
nodesMetaDataMap,
autoGenerateWebhookUrl,
@ -1729,7 +1790,7 @@ export const useNodesInteractions = () => {
if (node.type === CUSTOM_NOTE_NODE)
return true
const nodeMeta = nodesMetaDataMap?.[node.data.type as BlockEnum]
const nodeMeta = nodesMetaDataMap?.[isAgentV2NodeData(node.data) ? BlockEnum.AgentV2 : node.data.type as BlockEnum]
if (!nodeMeta)
return false
@ -1741,7 +1802,7 @@ export const useNodesInteractions = () => {
if (node.type === CUSTOM_NOTE_NODE)
return {}
const nodeMeta = nodesMetaDataMap?.[node.data.type as BlockEnum]
const nodeMeta = nodesMetaDataMap?.[isAgentV2NodeData(node.data) ? BlockEnum.AgentV2 : node.data.type as BlockEnum]
return nodeMeta?.defaultValue
}, [nodesMetaDataMap])
@ -2359,7 +2420,7 @@ export const useNodesInteractions = () => {
const currentNode = nodes.find(n => n.id === nodeId)!
const childrenNodes = nodes.filter(n =>
currentNode.data._children?.find((c: any) => c.nodeId === n.id),
currentNode.data._children?.find((child: NonNullable<Node['data']['_children']>[number]) => child.nodeId === n.id),
)
let rightNode: Node
let bottomNode: Node

Some files were not shown because too many files have changed in this diff Show More