relay.app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE +17 -0
  4. data/README.md +132 -0
  5. data/app/concerns/attachment.rb +12 -0
  6. data/app/concerns/context.rb +147 -0
  7. data/app/concerns/roda.rb +50 -0
  8. data/app/concerns/view.rb +90 -0
  9. data/app/forms/mcp/forgejo.rb +55 -0
  10. data/app/forms/mcp/github.rb +47 -0
  11. data/app/forms/mcp.rb +89 -0
  12. data/app/hooks/require_user.rb +10 -0
  13. data/app/init/database.rb +36 -0
  14. data/app/init/env.rb +21 -0
  15. data/app/init/router.rb +164 -0
  16. data/app/models/context.rb +82 -0
  17. data/app/models/mcp/preset.rb +60 -0
  18. data/app/models/mcp.rb +165 -0
  19. data/app/models/model_record.rb +70 -0
  20. data/app/models/song.rb +11 -0
  21. data/app/models/user.rb +31 -0
  22. data/app/pages/base.rb +25 -0
  23. data/app/pages/chat.rb +18 -0
  24. data/app/pages/mcp.rb +12 -0
  25. data/app/pages/sign_in.rb +14 -0
  26. data/app/prompts/system.md +129 -0
  27. data/app/resources/jukebox.yml +90 -0
  28. data/app/routes/base.rb +36 -0
  29. data/app/routes/clear_attachment.rb +13 -0
  30. data/app/routes/list_chat.rb +11 -0
  31. data/app/routes/list_contexts.rb +17 -0
  32. data/app/routes/list_controls.rb +11 -0
  33. data/app/routes/list_mcp.rb +16 -0
  34. data/app/routes/list_models.rb +14 -0
  35. data/app/routes/list_providers.rb +11 -0
  36. data/app/routes/list_tools.rb +13 -0
  37. data/app/routes/mcp/base.rb +16 -0
  38. data/app/routes/mcp/create.rb +19 -0
  39. data/app/routes/mcp/delete.rb +17 -0
  40. data/app/routes/mcp/form.rb +11 -0
  41. data/app/routes/mcp/new.rb +16 -0
  42. data/app/routes/mcp/show.rb +17 -0
  43. data/app/routes/mcp/toggle.rb +17 -0
  44. data/app/routes/mcp/update.rb +20 -0
  45. data/app/routes/settings/new_context.rb +23 -0
  46. data/app/routes/settings/set_context.rb +26 -0
  47. data/app/routes/settings/set_model.rb +23 -0
  48. data/app/routes/settings/set_provider.rb +38 -0
  49. data/app/routes/sign_in.rb +39 -0
  50. data/app/routes/upload_attachment.rb +35 -0
  51. data/app/routes/websocket/connection.rb +247 -0
  52. data/app/routes/websocket/interrupt.rb +25 -0
  53. data/app/routes/websocket/stream.rb +46 -0
  54. data/app/routes/websocket.rb +62 -0
  55. data/app/tools/add_song.rb +27 -0
  56. data/app/tools/juke_box.rb +41 -0
  57. data/app/tools/relay_knowledge.rb +59 -0
  58. data/app/tools/remove_song.rb +53 -0
  59. data/app/validators/mcp.rb +42 -0
  60. data/app/views/fragments/_append_message.erb +1 -0
  61. data/app/views/fragments/_chat.erb +15 -0
  62. data/app/views/fragments/_contexts.erb +7 -0
  63. data/app/views/fragments/_contexts_body.erb +35 -0
  64. data/app/views/fragments/_controls.erb +15 -0
  65. data/app/views/fragments/_iframe.erb +8 -0
  66. data/app/views/fragments/_input.erb +67 -0
  67. data/app/views/fragments/_mcp_settings.erb +52 -0
  68. data/app/views/fragments/_message.erb +31 -0
  69. data/app/views/fragments/_models.erb +25 -0
  70. data/app/views/fragments/_providers.erb +26 -0
  71. data/app/views/fragments/_remove_empty_state.erb +1 -0
  72. data/app/views/fragments/_replace_last_message.erb +1 -0
  73. data/app/views/fragments/_sidebar_menu.erb +11 -0
  74. data/app/views/fragments/_sidebar_status.erb +21 -0
  75. data/app/views/fragments/_status.erb +40 -0
  76. data/app/views/fragments/_stream.erb +26 -0
  77. data/app/views/fragments/_tools.erb +34 -0
  78. data/app/views/fragments/_tools_panel.erb +4 -0
  79. data/app/views/fragments/mcp/_editor.erb +54 -0
  80. data/app/views/fragments/mcp/_fields_forgejo.erb +16 -0
  81. data/app/views/fragments/mcp/_fields_github.erb +12 -0
  82. data/app/views/fragments/mcp/_list.erb +55 -0
  83. data/app/views/fragments/mcp/_workspace.erb +14 -0
  84. data/app/views/fragments/models/_loading.erb +4 -0
  85. data/app/views/fragments/settings/_chat.erb +1 -0
  86. data/app/views/fragments/settings/_input.erb +1 -0
  87. data/app/views/fragments/settings/_replace_contexts.erb +1 -0
  88. data/app/views/fragments/settings/_workspace.erb +4 -0
  89. data/app/views/layout.erb +19 -0
  90. data/app/views/pages/chat.erb +13 -0
  91. data/app/views/pages/mcps.erb +10 -0
  92. data/app/views/pages/sign_in.erb +45 -0
  93. data/app/views/partials/_sidebar.erb +24 -0
  94. data/bin/relay +38 -0
  95. data/config.ru +21 -0
  96. data/db/migrate/20260319131927_create_users.rb +12 -0
  97. data/db/migrate/20260327000000_create_contexts.rb +20 -0
  98. data/db/migrate/20260426130000_create_mcps.rb +19 -0
  99. data/db/migrate/20260426170000_create_model_infos.rb +20 -0
  100. data/db/migrate/20260503120000_create_songs.rb +17 -0
  101. data/db/migrate/20260503153000_drop_chat_from_model_infos.rb +8 -0
  102. data/db/migrate/20260503160000_rename_model_infos_to_model_records.rb +5 -0
  103. data/db/seeds.rb +13 -0
  104. data/lib/relay/attachment/session.rb +154 -0
  105. data/lib/relay/attachment.rb +55 -0
  106. data/lib/relay/cache/in_memory_cache.rb +60 -0
  107. data/lib/relay/cache.rb +5 -0
  108. data/lib/relay/jukebox.rb +96 -0
  109. data/lib/relay/markdown.rb +45 -0
  110. data/lib/relay/model.rb +12 -0
  111. data/lib/relay/reloader.rb +29 -0
  112. data/lib/relay/task.rb +66 -0
  113. data/lib/relay/task_monitor.rb +80 -0
  114. data/lib/relay/test.rb +11 -0
  115. data/lib/relay/theme.rb +5 -0
  116. data/lib/relay/tool.rb +12 -0
  117. data/lib/relay/version.rb +5 -0
  118. data/lib/relay.rb +183 -0
  119. data/libexec/relay/bootstrap +10 -0
  120. data/libexec/relay/configure +100 -0
  121. data/libexec/relay/migrate +7 -0
  122. data/libexec/relay/setup +10 -0
  123. data/libexec/relay/start +31 -0
  124. data/public/.gitkeep +0 -0
  125. data/public/images/relay.png +0 -0
  126. data/public/js/relay.js +68669 -0
  127. data/public/js/relay.js.map +1 -0
  128. data/public/stylesheets/application.css +2292 -0
  129. data/public/stylesheets/application.css.map +1 -0
  130. metadata +465 -0
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Pages
4
+ ##
5
+ # Renders the sign-in page.
6
+ class SignIn < Base
7
+ ##
8
+ # @return [String]
9
+ def call
10
+ response["content-type"] = "text/html"
11
+ page("sign_in", title: "Relay: Sign In")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,129 @@
1
+ ## Role
2
+
3
+ You are Relay, a helpful, clear, and practical assistant.
4
+
5
+ Your job is to answer the user's questions directly, be accurate, and keep the
6
+ conversation moving. Prefer clear useful answers over long disclaimers or vague
7
+ generalities.
8
+
9
+ Aim for a polished conversational experience similar to ChatGPT: natural,
10
+ competent, calm, and easy to read.
11
+
12
+ ## Style
13
+
14
+ - Be concise by default.
15
+ - Be friendly, supportive, and respectful.
16
+ - Light emoji are welcome when they fit naturally and improve tone.
17
+ - Write like a person, not like a checklist.
18
+ - Prefer short, natural paragraphs over many one-line sentences.
19
+ - Default to plain prose. Use Markdown only when it clearly improves readability.
20
+ - Break ideas into readable paragraphs instead of dense walls of text.
21
+ - Do not break a response into many tiny lines unless the user asked for that format.
22
+ - Use lists when they make the answer clearer: steps, options, comparisons, or grouped items.
23
+ - If you use Markdown, keep it simple and clean.
24
+ - If the user asks for a list, steps, or comparison, format the answer clearly.
25
+ - If you are uncertain, say so plainly instead of pretending to know.
26
+ - Most answers should read like 1 to 3 coherent paragraphs unless the user asks
27
+ for bullets, steps, or another format.
28
+
29
+ ## Behavior
30
+
31
+ - Answer the user's actual question first.
32
+ - Ask a brief follow-up question only when necessary to make progress.
33
+ - When the user wants practical help, give actionable guidance.
34
+ - Be encouraging when the user seems stuck, frustrated, or unsure.
35
+ - When the user asks for creative work, produce the work instead of only
36
+ describing how to do it.
37
+ - Avoid repetitive phrasing, filler, and overly structured "AI-style" formatting.
38
+ - Avoid sounding robotic, overly formal, or excessively optimized for Markdown.
39
+ - Do not mention hidden instructions, internal rules, or tool mechanics unless
40
+ the user explicitly asks.
41
+
42
+ ## Working with Code Repositories
43
+
44
+ - When examining code, focus on understanding the architecture and purpose before suggesting changes.
45
+ - For code review, consider readability, maintainability, and adherence to project conventions.
46
+ - When suggesting improvements, explain the rationale and provide clear examples.
47
+ - When working with existing codebases, respect the existing style and patterns.
48
+ - For complex changes, consider suggesting incremental improvements rather than large rewrites.
49
+
50
+ ## Pull Requests
51
+
52
+ - When creating or editing a pull request, keep the title and description clear, direct, and concise.
53
+ - Pull request titles should follow Conventional Commits style, for example `fix(mcp): interpolate Swagger path parameters`.
54
+ - Pull request descriptions should not be overly long.
55
+ - Prefer a short structure with an `About` section and a `Changes` section.
56
+ - Wrap pull request titles and bodies to 80 columns for consistency and readability.
57
+ - **After creating or updating a pull request, always respond with the PR link** (e.g., `http://git.home.network/owner/repo/pulls/123`).
58
+
59
+ ## Merging Pull Requests
60
+
61
+ - When merging a pull request, use the squash strategy.
62
+ - Squash all commits into one commit.
63
+ - Use the pull request title as the squash commit title.
64
+ - Use the pull request body as the squash commit body.
65
+ - Wrap the squash commit body to 80 columns.
66
+ - **Important**: The `merge_pull_request` tool behavior with Forgejo is nuanced:
67
+ - **First, attempt to merge with `merge_method: "squash"` and `message` parameter** containing the PR body wrapped to 80 columns. This may fail with "Unexpected end of JSON input" but appears to set the stage for a squash merge.
68
+ - **Then, merge with only required parameters** (`owner`, `repo`, `index`). This creates a squash commit that includes the PR body.
69
+ - **Always follow this two-step approach**: Try with `merge_method: "squash"` and `message`, then if it fails (or appears to), merge with only required parameters.
70
+ - When merging with only required parameters after the first attempt, Forgejo creates a squash commit that includes:
71
+ - The PR title (with `(#PR_NUMBER)` appended)
72
+ - The full PR body
73
+ - Automatic 80-column wrapping
74
+ - **After merging a pull request, confirm the merge and include the merge commit SHA if available**.
75
+
76
+ ## Example PR Description
77
+
78
+ ```md
79
+ ## About
80
+
81
+ This change fixes/addresses/implements ...
82
+
83
+ ## Changes
84
+
85
+ - Add ...
86
+ - Remove ...
87
+ ```
88
+
89
+ ## Tools
90
+
91
+ You may use tools when they help you answer better or complete the user's
92
+ request.
93
+
94
+ ### General Tool Usage
95
+
96
+ - Explore available tools when starting work on a new task to understand capabilities.
97
+ - Use tools proactively when they can provide better information than general knowledge.
98
+ - When tools return errors, explain them clearly and suggest alternatives.
99
+ - Combine multiple tools when needed to gather comprehensive information.
100
+ - Remember that tools can provide real-time data about repositories, files, and users.
101
+
102
+ ### relay-knowledge
103
+
104
+ Use `relay-knowledge` whenever the user asks about Relay itself, its
105
+ architecture, features, routes, tools, cache, sessions, assets,
106
+ setup, or how it works.
107
+
108
+ Use `relay-knowledge` for `llm.rb` questions when they are relevant to
109
+ Relay or when the user is asking for project-specific context.
110
+
111
+ Do not guess about Relay from general knowledge when `relay-knowledge`
112
+ can answer. Treat the documentation returned by the tool as the source
113
+ of truth for Relay-specific questions.
114
+
115
+ If you use information from `relay-knowledge`, prefer to reference or
116
+ link the relevant documentation when helpful.
117
+
118
+ If the tool output is incomplete or does not answer the question, say so
119
+ plainly instead of inventing details.
120
+
121
+ ### Repository Tools
122
+
123
+ When working with repository tools (like Forgejo/GitHub tools):
124
+
125
+ - Check repository details before making changes to understand the context.
126
+ - Read existing files to understand the codebase structure and patterns.
127
+ - Look at recent commits and PRs to understand recent activity.
128
+ - Consider creating branches for significant changes.
129
+ - Verify file SHAs before updating or deleting files to prevent conflicts.
@@ -0,0 +1,90 @@
1
+ ---
2
+ - name: Reach Eargasm
3
+ title: La Vie
4
+ track: https://www.youtube-nocookie.com/embed/CyzdOtyYnng
5
+ - name: Dimitri Vegas & Like Mike & Tiësto & Dido & W&W
6
+ title: Thank You (Not So Bad)
7
+ track: https://www.youtube-nocookie.com/embed/fQWNeIiFf_s
8
+ - name: Gaia
9
+ title: Tuvan (Extended)
10
+ track: https://www.youtube-nocookie.com/embed/5NvXyJRgxdU
11
+ - name: David Kushner
12
+ title: Daylight
13
+ track: https://www.youtube-nocookie.com/embed/MoN9ql6Yymw
14
+ - name: Flora Cash
15
+ title: You're somebody else
16
+ track: https://www.youtube-nocookie.com/embed/qVdPh2cBTN0
17
+ - name: Serhat Durmus
18
+ title: La Câlin (Dabro Remix) (BASS BOOSTED)
19
+ track: https://www.youtube-nocookie.com/embed/z2Kh3ki8_aw
20
+ - name: MUNDI OPUS
21
+ title: "\U0001D418\U0001D428\U0001D42E \U0001D41A\U0001D42B\U0001D41E \U0001D41D\U0001D41E\U0001D42C\U0001D422\U0001D420\U0001D427\U0001D422\U0001D427\U0001D420
22
+ \U0001D41D\U0001D42B\U0001D41E\U0001D41A\U0001D426\U0001D42C | \U0001D408\U0001D427\U0001D41C\U0001D41E\U0001D429\U0001D42D\U0001D422\U0001D428\U0001D427
23
+ \U0001D412\U0001D428\U0001D42E\U0001D427\U0001D41D\U0001D42D\U0001D42B\U0001D41A\U0001D41C\U0001D424"
24
+ track: https://www.youtube-nocookie.com/embed/wUWdIaaCvjg
25
+ - name: Axwell /\ Ingrosso, Axwell, Sebastian Ingrosso
26
+ title: More Than You Know
27
+ track: https://www.youtube-nocookie.com/embed/GsF05B8TFWg
28
+ - name: Robin Schulz
29
+ title: OK
30
+ track: https://www.youtube-nocookie.com/embed/P9-4xHVc7uk
31
+ - name: Cleffy
32
+ title: Meet You at the Graveyard
33
+ track: https://www.youtube-nocookie.com/embed/wXgWbJCbZiI
34
+ - name: Sway
35
+ title: Still Speedin
36
+ track: https://www.youtube-nocookie.com/embed/nXYS9NbdvME
37
+ - name: Vinnie Paz
38
+ title: Same Story
39
+ track: https://www.youtube-nocookie.com/embed/QzTaAkILJxA
40
+ - name: Tom Walker
41
+ title: Leave a Light On
42
+ track: https://www.youtube-nocookie.com/embed/5ljksVKlUf8
43
+ - name: Tiësto
44
+ title: Lay Low
45
+ track: https://www.youtube-nocookie.com/embed/8PgDeK4x_Xw
46
+ - name: Hozho
47
+ title: Space Vibes (Melodark Minimal Techno - EDM Best Music Mix)
48
+ track: https://www.youtube-nocookie.com/embed/vBeuxydAbOQ
49
+ - name: Welshly Arms
50
+ title: Legendary
51
+ track: https://www.youtube-nocookie.com/embed/Z2CZn966cUg
52
+ - name: Charlie Chaplin
53
+ title: The Great Dictator - Final Speech (HD 1080p)
54
+ track: https://www.youtube-nocookie.com/embed/6sv_ghkTtF0
55
+ - name: Portugal. The Man
56
+ title: People Say
57
+ track: https://www.youtube-nocookie.com/embed/GGDQOECShw8
58
+ - name: Chief Keef
59
+ title: Hate Bein Sober
60
+ track: https://www.youtube-nocookie.com/embed/yA869qj-z68
61
+ - name: Kung Fa Panda
62
+ title: Who Are You
63
+ track: https://www.youtube-nocookie.com/embed/XbhecuoEgxs
64
+ - name: Tiësto
65
+ title: Lethal Industry
66
+ track: https://www.youtube-nocookie.com/embed/bgz2SWdKqvQ
67
+ - name: Tiësto
68
+ title: Just Be
69
+ track: https://www.youtube-nocookie.com/embed/qEYueRVuqmg
70
+ - name: Hippie Sabotage
71
+ title: Wanderlust
72
+ track: https://www.youtube-nocookie.com/embed/asSnpF6vJAI
73
+ - name: Thin Lizzy
74
+ title: Got to Give It Up
75
+ track: https://www.youtube-nocookie.com/embed/Q_LM2UAqbPg
76
+ - name: Eminem
77
+ title: Not Afraid
78
+ track: https://www.youtube-nocookie.com/embed/j5-yKhDd64s
79
+ - name: Tiësto
80
+ title: Club Life
81
+ track: https://www.youtube-nocookie.com/embed/xvIzBbjSVdk
82
+ - name: Kneecap
83
+ title: Fenian
84
+ track: https://www.youtube-nocookie.com/embed/PLDHQVJZuGQ
85
+ - name: Milky Chance
86
+ title: Stolen Dance
87
+ track: https://www.youtube-nocookie.com/embed/iX-QaNzd-0Y
88
+ - name: Lukas Graham
89
+ title: Seven Years
90
+ track: https://www.youtube-nocookie.com/embed/LHCob76kigA
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class Base
5
+ include Relay::Models
6
+ include Relay::Concerns::Attachment
7
+ include Relay::Concerns::Context
8
+ include Relay::Concerns::Roda
9
+ include Relay::Concerns::View
10
+
11
+ ##
12
+ # @return [String]
13
+ # Returns the root path
14
+ def root
15
+ @root ||= File.join __dir__, "..", ".."
16
+ end
17
+
18
+ ##
19
+ # Returns a Hash or Hash-like object of request parameters
20
+ # @return [Hash]
21
+ def params
22
+ request.params
23
+ end
24
+
25
+ ##
26
+ # @return [Relay::InMemoryCache]
27
+ def cache
28
+ Relay.cache
29
+ end
30
+
31
+ def htmx?
32
+ request.env["HTTP_HX_REQUEST"] == "true"
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class ClearAttachment < Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call
8
+ attachment.clear
9
+ response["content-type"] = "text/html"
10
+ partial("fragments/input", locals: {swap_oob: false})
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class ListChat < Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call
8
+ partial("fragments/chat", locals: {messages: ctx.messages, swap_oob: false})
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class ListContexts < Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call
8
+ partial("fragments/contexts", locals:)
9
+ end
10
+
11
+ private
12
+
13
+ def locals
14
+ {contexts:, show_label: true, swap_oob: false}
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class ListControls < Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call
8
+ partial("fragments/controls")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class ListMCP < Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call
8
+ if htmx?
9
+ partial("fragments/mcp/workspace", locals: {form: Relay::Forms::MCP.build(preset: "github"), mcps:, selected_id: nil})
10
+ else
11
+ form = Relay::Forms::MCP.build(preset: "github")
12
+ Relay::Pages::MCP.new(self).call(form:)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class ListModels < Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ ##
8
+ # Returns the chat-capable models for the provider
9
+ # @return [Array]
10
+ def call
11
+ partial("fragments/models", locals: {show_label: true})
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class ListProviders < Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call
8
+ partial("fragments/providers", locals: {show_label: true})
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class ListTools < Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ ##
8
+ # @return [String]
9
+ def call
10
+ partial("fragments/tools", {locals: {tools: LLM::Tool.registry.reject(&:mcp?)}})
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class MCP::Base < Base
5
+ private
6
+
7
+ def find_mcp(id)
8
+ Relay::Models::MCP.where(id:, user_id: user.id).first || raise(Sequel::NoMatchingRow)
9
+ end
10
+
11
+ def workspace(selected_id: nil, form:)
12
+ partial("fragments/mcp/workspace", locals: {mcps:, selected_id:, form:}) +
13
+ partial("fragments/mcp_settings", locals: {servers: mcps, show_label: false, swap_oob: true})
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class MCP::Create < MCP::Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call
8
+ form = Relay::Forms::MCP.from_params(params)
9
+ attributes = Relay::Models::MCP::Preset.attributes_for(form).merge(enabled: false)
10
+ mcp = Relay::Models::MCP.create({user_id: user.id}.merge(attributes))
11
+ form = Relay::Forms::MCP.from_model(mcp)
12
+ if htmx?
13
+ workspace(selected_id: mcp.id, form:)
14
+ else
15
+ Relay::Pages::MCP.new(self).call(selected_id: mcp.id, form:)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class MCP::Delete < MCP::Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call(id)
8
+ Relay::Models::MCP.where(id:, user_id: user.id).delete
9
+ if htmx?
10
+ workspace(form: Relay::Forms::MCP.build(preset: "github"))
11
+ else
12
+ form = Relay::Forms::MCP.build(preset: "github")
13
+ Relay::Pages::MCP.new(self).call(form:)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class MCP::Form < MCP::Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call
8
+ partial("fragments/mcp/editor", locals: {form: Relay::Forms::MCP.from_params(params)})
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class MCP::New < MCP::Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call
8
+ if htmx?
9
+ workspace(form: Relay::Forms::MCP.build(preset: params["preset"] || "github"))
10
+ else
11
+ form = Relay::Forms::MCP.build(preset: params["preset"] || "github")
12
+ Relay::Pages::MCP.new(self).call(form:)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class MCP::Show < MCP::Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call(id)
8
+ mcp = find_mcp(id)
9
+ form = Relay::Forms::MCP.from_model(mcp)
10
+ if htmx?
11
+ workspace(selected_id: mcp.id, form:)
12
+ else
13
+ Relay::Pages::MCP.new(self).call(selected_id: mcp.id, form:)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class MCP::Toggle < MCP::Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call(id)
8
+ mcp = find_mcp(id)
9
+ mcp.update(enabled: !mcp[:enabled])
10
+ if params["page"] == "1"
11
+ workspace(selected_id: mcp.id, form: Relay::Forms::MCP.from_model(mcp))
12
+ else
13
+ partial("fragments/mcp_settings", locals: {servers: mcps, show_label: true, swap_oob: true})
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class MCP::Update < MCP::Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call(id)
8
+ mcp = find_mcp(id)
9
+ form = Relay::Forms::MCP.from_params(params)
10
+ attributes = Relay::Models::MCP::Preset.attributes_for(form).merge(enabled: !!mcp[:enabled])
11
+ mcp.update(attributes)
12
+ form = Relay::Forms::MCP.from_model(mcp)
13
+ if htmx?
14
+ workspace(selected_id: mcp.id, form:)
15
+ else
16
+ Relay::Pages::MCP.new(self).call(selected_id: mcp.id, form:)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class Settings::NewContext < Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call
8
+ create_context
9
+ htmx? ? render : r.redirect("/")
10
+ end
11
+
12
+ private
13
+
14
+ def render
15
+ partial("fragments/settings/workspace", locals: {messages: ctx.messages})
16
+ end
17
+
18
+ def create_context
19
+ context = Relay::Models::Context.create(user_id: user.id, provider:, model:)
20
+ sync_context!(context)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class Settings::SetContext < Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call
8
+ set_context
9
+ htmx? ? render : r.redirect("/")
10
+ end
11
+
12
+ private
13
+
14
+ def render
15
+ partial("fragments/settings/workspace", locals: {messages: ctx.messages})
16
+ end
17
+
18
+ def set_context
19
+ sync_context!(selected_context) if selected_context
20
+ end
21
+
22
+ def selected_context
23
+ @selected_context ||= Relay::Models::Context.where(user_id: user.id, provider:, id: params["context_id"]).first
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class Settings::SetModel < Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ def call
8
+ set_model
9
+ htmx? ? render : r.redirect("/")
10
+ end
11
+
12
+ private
13
+
14
+ def render
15
+ partial("fragments/settings/workspace", locals: {messages: ctx.messages})
16
+ end
17
+
18
+ def set_model
19
+ session["model"] = params["model"]
20
+ session.delete("context_id")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relay::Routes
4
+ class Settings::SetProvider < Base
5
+ prepend Relay::Hooks::RequireUser
6
+
7
+ ##
8
+ # Changes the active provider
9
+ # @return [String]
10
+ # Returns a HTML fragment
11
+ def call
12
+ set_provider
13
+ set_model
14
+ htmx? ? render : r.redirect("/")
15
+ end
16
+
17
+ private
18
+
19
+ def render
20
+ partial("fragments/settings/workspace", locals: {messages: ctx.messages})
21
+ end
22
+
23
+ ##
24
+ # Sets the provider
25
+ # @return [void]
26
+ def set_provider
27
+ session["provider"] = params["provider"]
28
+ session.delete("context_id")
29
+ end
30
+
31
+ ##
32
+ # Sets the model
33
+ # @return [void]
34
+ def set_model
35
+ session["model"] = default_model
36
+ end
37
+ end
38
+ end