Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
85569be
Merge origin/dev into the rebuilt demo branch (AI-3 PR 12 tip d004cd3…
jonasjabari Oct 2, 2026
bc34e3f
[AI-100] Prototype: admin sandbox for text transform actions
jonasjabari Sep 9, 2026
6039a87
[AI-100] Demo: run the prototype through the AI-3 LLM connection
jonasjabari Sep 9, 2026
330f6f1
[AI-100] Demo: send LLM instructions with the system role
jonasjabari Sep 9, 2026
42bf334
[AI-100] Demo: rework the sandbox UI
jonasjabari Sep 10, 2026
f63dba3
[AI-100] Demo: searchable work package and project pickers in the san…
jonasjabari Sep 10, 2026
4ccf726
[AI-100] Demo: save-and-stay button on the action form
jonasjabari Sep 10, 2026
dad1255
[AI-100] Demo: mark the prototype-only part of the action form
jonasjabari Sep 10, 2026
df6716e
[AI-100] Demo: expose the stored system prompt in the run representer
jonasjabari Sep 16, 2026
194f906
[AI-100] Demo: keep the sandbox timer at the final value after a run …
jonasjabari Sep 17, 2026
44fb184
[AI-100] Demo: check cancellation and budget while a model only reasons
jonasjabari Sep 21, 2026
a88f335
[AI-100] Demo: simulate a blocked and a failed generation per run
jonasjabari Sep 21, 2026
912a582
[AI-100] Demo: AI action menu in the description editor and result po…
jonasjabari Sep 21, 2026
ab3feb0
[AI-100] Demo: fix the editor menu and the popover after the first test
jonasjabari Sep 21, 2026
f6e242c
[AI-100] Demo: popover without caret, aligned grip, resizable
jonasjabari Sep 22, 2026
be2abe2
[AI-100] Demo: resize the popover from both sides and both bottom cor…
jonasjabari Sep 22, 2026
acae8c8
[AI-100] Demo: run an action on the selected text (AI-126 mode B)
jonasjabari Sep 29, 2026
8bb3453
[AI-100] Demo: keep the undo history when replacing the whole descrip…
jonasjabari Sep 29, 2026
89a24d4
[AI-100] Demo: build the AI result pane on Primer::Alpha::Overlay
jonasjabari Oct 2, 2026
bd24017
[AI-100] Demo: keep the result overlay where the user put it
jonasjabari Oct 2, 2026
296b550
[AI-100] Demo: let the overlay box grow with the resized pane
jonasjabari Oct 2, 2026
3e82158
[AI-100] Demo: keep the system-role patch within the session's metrics
jonasjabari Oct 2, 2026
3e77e37
[AI-100] Demo: drop the LLM tables of the superseded AI-3 stack once
jonasjabari Oct 2, 2026
4e560e3
[AI-100] Demo: BorderBox variant of the result pane for comparison
jonasjabari Oct 6, 2026
b942e14
[AI-100] Demo: server-rendered result pane over Turbo (variant C)
jonasjabari Oct 8, 2026
7f0b5c3
Merge remote-tracking branch 'origin/dev' into demo/ai-100-prototype-…
jonasjabari Oct 8, 2026
f78c629
[AI-100] Demo: the editor plugin requests the result pane directly
jonasjabari Oct 9, 2026
ecdb434
[AI-173] Add pane data attribute helpers
jonasjabari Oct 15, 2026
ed89bd0
[AI-173] Add floating pane Stimulus controller
jonasjabari Oct 15, 2026
c82d855
[AI-173] Wire result pane markup to the pane controller
jonasjabari Oct 15, 2026
ca2639a
[AI-173] Harden pane pointer handling and copy with Primer
jonasjabari Oct 16, 2026
7a8ba12
[AI-173] Use Testing Library queries and matchers in pane spec
jonasjabari Oct 19, 2026
f32b39c
Merge remote-tracking branch 'origin/dev' into implementation/ai-173-…
jonasjabari Oct 19, 2026
ef823b3
Merge branch 'dev' into implementation/ai-173-floating-pane-behaviour
jonasjabari Oct 19, 2026
603fe5d
[AI-175] Add result pane routes and parameter contract
jonasjabari Oct 19, 2026
d25ea3a
[AI-175] Start runs and append the result pane
jonasjabari Oct 19, 2026
76cdd0c
[AI-175] Poll, close, apply and cancel result pane runs
jonasjabari Oct 19, 2026
7393bbc
[AI-175] Tighten pane parameters and follow OpTurbo patterns
jonasjabari Oct 19, 2026
a263aea
[AI-175] Format pane text with the project of a new work package
jonasjabari Oct 19, 2026
e3d6208
Merge remote-tracking branch 'origin/dev' into demo/ai-100-prototype-…
jonasjabari Oct 23, 2026
4b7bed0
[AI-100] Demo: drop the AI-172 pane parts the demo pane replaces
jonasjabari Oct 23, 2026
c9f297a
[AI-100] Demo: use the AI-101 Assistant button to open the result pane
jonasjabari Oct 23, 2026
f4bc09f
[AI-100] Demo: theme the split button hover of the Assistant button
jonasjabari Oct 23, 2026
2c3460a
[AI-100] Demo: return to the real result pane implementation
jonasjabari Oct 23, 2026
1fa4d3d
Merge remote-tracking branch 'origin/implementation/ai-173-floating-p…
jonasjabari Oct 23, 2026
d4834d8
Merge remote-tracking branch 'origin/implementation/ai-175-result-pan…
jonasjabari Oct 23, 2026
ffd79d4
[AI-100] Demo: connect the real result pane to the editor
jonasjabari Oct 23, 2026
3c827f9
[AI-101] Register the Assistant actions service for editor plugins
jonasjabari Oct 26, 2026
404a4a4
[AI-101] Offer the actions scoped everywhere in all other editors
jonasjabari Oct 26, 2026
80865ef
[AI-101] Rebuild the CKEditor bundle with the Assistant split button
jonasjabari Oct 26, 2026
a2e04a1
[AI-101] Theme the split button hover in the dark theme
jonasjabari Oct 26, 2026
34b7317
[AI-101] Spec the Assistant button in the description and comments
jonasjabari Oct 26, 2026
0ffba59
Merge branch 'implementation/ai-101-assistant-button' into demo/ai-10…
jonasjabari Oct 26, 2026
e289c77
[AI-100] Demo: read only the editor field for the pane context
jonasjabari Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions app/components/admin/text_transform_actions/sandbox_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.

OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

See COPYRIGHT and LICENSE files for more details.

++#%>

<%=
render(Primer::Beta::BorderBox.new(mt: 5, data: controller_data, test_selector: "ai-text-transform-sandbox")) do |box|
box.with_header do |header|
header.with_title(tag: :h2) { label(:title) }
end

box.with_body do
flex_layout(direction: :column) do |layout|
layout.with_row(mb: 3) do
render(Primer::Beta::Text.new(tag: :p, color: :muted)) { label(:description) }
end

layout.with_row(mb: 3) do
helpers.primer_form_with(url: "#", method: :post, data: form_data) do |f|
render(Admin::TextTransformActions::SandboxForm.new(f, types:))
end
end

layout.with_row(mb: 3) do
flex_layout(align_items: :center) do |controls|
controls.with_column(mr: 2) do
render(
Primer::Beta::Button.new(
scheme: :primary,
type: :button,
data: target(:execute).merge(action: "ai-text-transform-sandbox#execute")
)
) { label(:execute) }
end
controls.with_column(mr: 3) do
render(
Primer::Beta::Button.new(
scheme: :danger,
type: :button,
disabled: true,
data: target(:cancel).merge(action: "ai-text-transform-sandbox#cancel")
)
) { label(:cancel) }
end
controls.with_column(mr: 2, hidden: true, data: target(:spinner)) do
render(Primer::Beta::Spinner.new(size: :small))
end
controls.with_column(mr: 2) do
render(Primer::Beta::Label.new(scheme: :secondary, data: target(:status))) { label(:status_idle) }
end
controls.with_column do
render(Primer::Beta::Text.new(tag: :span, font_family: :mono, color: :muted, data: target(:timer))) { "" }
end
end
end

layout.with_row(mb: 3) do
render(Primer::Box.new(border: true, border_radius: 2, p: 3, bg: :subtle)) do
content_tag(
:pre,
"",
class: "text-mono f5",
style: "white-space: pre-wrap; margin: 0; min-height: 6rem;",
data: target(:output)
)
end
end

layout.with_row do
render(Primer::Alpha::Dialog.new(id: DIALOG_ID, title: label(:details_title), size: :xlarge)) do |dialog|
dialog.with_show_button(scheme: :default) { label(:details) }
dialog.with_header(variant: :large)
dialog.with_body do
flex_layout(direction: :column) do |details|
details.with_row(mb: 3) do
render(Primer::Beta::Heading.new(tag: :h3, font_size: 5, mb: 2)) { label(:run_section) }
end
details.with_row(mb: 4) do
content_tag(:dl, "", class: "text-mono f6", data: target(:runMeta))
end
details.with_row(mb: 3) do
render(Primer::Beta::Heading.new(tag: :h3, font_size: 5, mb: 2)) { label(:system_prompt_section) }
end
details.with_row(mb: 4) do
render(Primer::Box.new(border: true, border_radius: 2, p: 3, bg: :subtle)) do
content_tag(:pre, "", class: "text-mono f6", style: "white-space: pre-wrap; margin: 0;", data: target(:systemPrompt))
end
end
details.with_row(mb: 3) do
render(Primer::Beta::Heading.new(tag: :h3, font_size: 5, mb: 2)) { label(:events_section) }
end
details.with_row do
content_tag(:table, class: "text-mono f6 width-full") do
concat(
content_tag(:thead) do
content_tag(:tr) do
safe_join(%i[event_seq event_kind event_payload event_time].map { |key| content_tag(:th, label(key), class: "text-left p-2 border-bottom") })
end
end
)
concat(content_tag(:tbody, "", data: target(:eventsBody)))
end
end
end
end
dialog.with_footer(show_divider: true) do
render(Primer::Beta::Button.new(data: { close_dialog_id: DIALOG_ID })) { I18n.t(:button_close) }
end
end
end
end
end
end
%>
73 changes: 73 additions & 0 deletions app/components/admin/text_transform_actions/sandbox_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Admin
module TextTransformActions
# Prototype only: lets an administrator run the action against arbitrary text
# through the execute API and watch the run being polled.
class SandboxComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include API::V3::Utilities::PathHelper

DIALOG_ID = "ai-text-transform-sandbox-details"

alias action model

private

def controller_data
{
controller: "ai-text-transform-sandbox",
ai_text_transform_sandbox_url_value: api_v3_paths.ai_text_transform_runs,
ai_text_transform_sandbox_action_id_value: action.id
}
end

def form_data
{
controller: "show-when-value-selected",
action: "submit->ai-text-transform-sandbox#submit"
}
end

def target(name)
{ ai_text_transform_sandbox_target: name }
end

def label(key)
I18n.t("admin.text_transform_actions.sandbox.#{key}")
end

def types
Type.order(:position)
end
end
end
end
10 changes: 6 additions & 4 deletions app/components/ai/text_transforms/result_pane_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ See COPYRIGHT and LICENSE files for more details.

++#%>

<%= component_wrapper(data: { test_selector: "ai-text-transform-result" }) do %>
<%= component_wrapper(data: wrapper_data) do %>
<%=
render(
Primer::BaseComponent.new(
tag: :div,
role: :dialog,
aria: { label: label(:title) },
tabindex: -1,
data: target(:pane),
position: :fixed,
display: :flex,
direction: :column,
Expand All @@ -42,11 +44,11 @@ See COPYRIGHT and LICENSE files for more details.
safe_join(
[
render(Primer::Beta::BorderBox.new(flex: 1, overflow: :hidden, display: :flex, direction: :column, box_shadow: :extra_large)) do |box|
box.with_header(classes: "op-ai-result-pane--handle") do
box.with_header(classes: "op-ai-result-pane--handle", data: action(beginDrag: :pointerdown)) do
flex_layout(align_items: :center, justify_content: :space_between) do |header|
header.with_column { render(Title.new(pane:)) }
header.with_column do
render(Primer::Beta::CloseButton.new("aria-label": label(:close)))
render(Primer::Beta::CloseButton.new("aria-label": label(:close), data: action(close: :click)))
end
end
end
Expand All @@ -58,7 +60,7 @@ See COPYRIGHT and LICENSE files for more details.
Primer::BaseComponent.new(
tag: :span,
classes: "op-ai-result-pane--resize op-ai-result-pane--resize_#{edge}",
data: { edge: },
data: { edge:, **action(beginResize: :pointerdown) },
"aria-hidden": true
)
) do
Expand Down
9 changes: 9 additions & 0 deletions app/components/ai/text_transforms/result_pane_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ def initialize(pane:)
private

attr_reader :pane

def wrapper_data
{
test_selector: "ai-text-transform-result",
controller: "ai-text-transform-pane",
ai_text_transform_pane_close_form_value: CLOSE_FORM_ID,
**action(close: "keydown.esc", keepInViewport: "resize@window")
}
end
end
end
end
2 changes: 2 additions & 0 deletions app/components/ai/text_transforms/result_pane_component.sass
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
&--grip
cursor: grab
user-select: none
touch-action: none

&--grip
display: inline-flex
Expand All @@ -23,6 +24,7 @@
position: absolute
display: flex
line-height: 0
touch-action: none

&_left,
&_right
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ See COPYRIGHT and LICENSE files for more details.
end
end
if done?
body.with_row(mb: 3, hidden: true, test_selector: "ai-text-transform-pane-apply-failed") do
body.with_row(mb: 3, hidden: true, test_selector: "ai-text-transform-pane-apply-failed", data: target(:applyFailed)) do
render(Primer::Alpha::Banner.new(scheme: :danger)) do
render(Primer::Beta::Text.new(tag: :span)) { "" }
render(Primer::Beta::Text.new(tag: :span, data: target(:applyFailedMessage))) { "" }
end
end
end
Expand All @@ -47,7 +47,7 @@ See COPYRIGHT and LICENSE files for more details.
end
end
if done?
body.with_row { content_tag(:template, pane.text) }
body.with_row { content_tag(:template, pane.text, data: target(:raw)) }
end
end
%>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def done?
end

def output
helpers.format_text(pane.text, object: pane.work_package)
helpers.format_text(pane.text, object: pane.work_package, project: pane.project)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ See COPYRIGHT and LICENSE files for more details.
++#%>

<%= component_wrapper do %>
<%= content_tag(:span, nil, hidden: true, data: state_data) %>
<%= form(id: CLOSE_FORM_ID, url: helpers.ai_text_transform_pane_path(pane.uuid), method: :delete) %>
<%= form(id: APPLY_FORM_ID, url: helpers.apply_ai_text_transform_pane_path(pane.uuid), method: :post) if state == "done" %>
<%= form(id: RETRY_FORM_ID, url: helpers.ai_text_transform_panes_path, method: :post, fields: form_params.merge(retry_of: pane.uuid)) if retryable? %>
<%=
flex_layout(align_items: :center, classes: "op-ai-result-pane--footer") do |bar|
bar.with_column(mr: 2, classes: "op-ai-result-pane--grip") do
bar.with_column(mr: 2, classes: "op-ai-result-pane--grip", data: action(beginDrag: :pointerdown)) do
render(Primer::Beta::Octicon.new(icon: :grabber, color: :muted))
end
bar.with_column(flex: 1) do
Expand All @@ -40,30 +44,30 @@ See COPYRIGHT and LICENSE files for more details.
render(Primer::Beta::Text.new(color: :muted)) { label(:generating) }
end
footer.with_column do
render(Primer::Beta::Button.new) { label(:cancel) }
render(Primer::Beta::Button.new(data: action(close: :click))) { label(:cancel) }
end
end
when "done"
flex_layout(align_items: :center) do |footer|
footer.with_column(flex: 1) do
render(Primer::Beta::Button.new(scheme: :invisible)) { label(:copy) }
render(Primer::Beta::ClipboardCopyButton.new(scheme: :invisible, value: pane.text, "aria-label": label(:copy)))
end
footer.with_column(mr: 2) do
render(Primer::Beta::Button.new) { label(:discard) }
render(Primer::Beta::Button.new(data: action(close: :click))) { label(:discard) }
end
footer.with_column do
render(Primer::Beta::Button.new(scheme: :primary)) { label(:replace) }
render(Primer::Beta::Button.new(scheme: :primary, type: :submit, form: APPLY_FORM_ID)) { label(:replace) }
end
end
else
flex_layout(align_items: :center, justify_content: :flex_end) do |footer|
if pane.run
footer.with_column(mr: 2) do
render(Primer::Beta::Button.new) { label(:try_again) }
render(Primer::Beta::Button.new(type: :submit, form: RETRY_FORM_ID)) { label(:try_again) }
end
end
footer.with_column do
render(Primer::Beta::Button.new) { label(:close) }
render(Primer::Beta::Button.new(data: action(close: :click))) { label(:close) }
end
end
end
Expand Down
Loading
Loading