Add /reopen command

This commit is contained in:
Jorge Manrubia
2025-07-03 13:47:10 +02:00
parent a2ca8b1dc0
commit ac2400a657
30 changed files with 245525 additions and 14 deletions
@@ -4,6 +4,7 @@ class Prompts::CommandsController < ApplicationController
[ "/add_card", "Add a new card", "/add_card " ],
[ "/assign", "Assign cards to people", "/assign @" ],
[ "/close", "Close cards (with optional reason)", "/close " ],
[ "/reopen", "Reopen cards", "/reopen" ],
[ "/clear", "Clear all filters", "/clear" ],
[ "/consider", "Move cards back to Considering", "/consider" ],
[ "/do", "Move cards to Doing", "/do" ],
+18 -10
View File
@@ -77,7 +77,7 @@ class Command::Ai::Translator
Cards have comments and live inside collections.
Context filters describe card state already true.
Commands (/assign, /tag, /close, /search, /clear, /do, /consider, /stage, /visit, /add_card, /user) apply new actions.
Commands (/assign, /tag, /close, /reopen, /search, /clear, /do, /consider, /stage, /visit, /add_card, /user) apply new actions.
Context properties you may use
* terms — array of keywords
@@ -100,7 +100,7 @@ class Command::Ai::Translator
* Use terms only if the query explicitly refers to cards; plain-text searches go to /search.
* Numbers without the word "card(s)" default to terms **unless the number is the direct object of an
action verb that operates on cards (move, assign, tag, close, stage, consider, do, etc.).**
action verb that operates on cards (move, assign, tag, close, reopen, stage, consider, do, etc.).**
"123" (with no action verb) → context: { terms: ["123"] }
"card 123" → context: { card_ids: [123] }
"card 1,2" → context: { card_ids: [1, 2] }
@@ -127,12 +127,11 @@ class Command::Ai::Translator
* "Falling back soon" cards → indexed_by: "falling_back_soon"
* **Past-tense** “tagged with #X”, “#X cards” → tag_ids: ["X"] (filter)
* **Imperative** “tag …”, “tag with #X”, “add the #X tag”, “apply #X” → command /tag #X (never a filter)
* When using past-tense verbs such as "assigned" or "closed", always use the corresponding filter, NEVER a command.
* For command that acts on cards, you can reference those by their ID (number). Use the filter "card_ids" when the user passes numbers as command arguments.
- User can reference with numbers to a single card or to a group of cards. E.g:
- "close 123 and 456" → context: { card_ids: [123, 456] }, commands: [ "/close" ]
- "assign 789 to jz" → context: { card_ids: [789] }, commands: [ "/assign jz" ]
- "close 789" → context: { card_ids: [789] }, commands: [ "/close" ]
- "reopen 789" → context: { card_ids: [789] }, commands: [ "/reopen" ]
- "assign 5, 82 and 9 to jz" → context: { card_ids: [5, 82 and 9] }, commands: [ "/assign jz" ]
* "Unassigned cards" (or “not assigned”, “with no assignee”) → assignment_status: "unassigned".
IMPORTANT: Only set assignment_status when the user **explicitly** asks for an unassigned state
@@ -162,14 +161,15 @@ class Command::Ai::Translator
* /visit <url|path> → open any other URL or internal path (cards, settings, etc.).
* /do → engage with card and move it to "doing"
* /consider → move card back to "considering" (reconsider)
* /reopen → reopen closed cards (moves them back to "open")
* Unless a clear command applies, fallback to /search with the verbatim text.
* When searching for nouns (non-person), prefer /search over terms.
* When the person to pass to a command is "me" or "myself", use "#{user.to_gid}"
* Respect the spoken order of commands.
* "close as [reason]" or "close because [reason]" → /close [reason]
Remove "as" or "because" from the actual command
* Lone "close" → /close (acts on current context)
* Lone "reopen" → /reopen (acts on current context)
* /close must **only** be produced if the request explicitly contains the verb “close”.
* /reopen must **only** be produced if the request explicitly contains the verb “reopen”.
* /stage [workflow stage]→ assign the card to the given stage (never takes card IDs).
* “Move <ID(s)> to <Stage>” → context.card_ids = [IDs]; command /stage <Stage>
* “Move <ID(s)> to doing” → context.card_ids = [IDs]; command /do
@@ -189,7 +189,8 @@ class Command::Ai::Translator
------------------------- VISIT USER PROFILES ------------------------
Use **/user <Name>** (not /visit) whenever the request is about viewing a persons profile or activity:
Use **/user <Name>** (not /visit) whenever the request is about viewing a persons profile, activity
or what that person is up to:
• visit user mike → /user mike*
• view user kevin → /user kevin*
@@ -197,7 +198,7 @@ class Command::Ai::Translator
---------------------------- CRUCIAL DONTS ---------------------------
* When the query contains active verbs such as "assign" or "close", always use the corresponding command, NEVER a filter.
* When the query contains active verbs such as "assign", "close", or "reopen", always use the corresponding command, NEVER a filter.
* Dont output “/visit /users/<name>”. Profile requests must use **/user <name>**.
* Never use names, tags, or stage names mentioned **inside commands** (like /assign, /tag, /stage) as filters.
* Never duplicate the assignee in both commands and context.
@@ -238,7 +239,14 @@ class Command::Ai::Translator
User: assign to me
Output:
{
"commands": ["/assign <GID of current user>"]
"commands": ["/assign #{user.to_gid}"]
}
User: reopen cards closed this week
Output:
{
"context": { "closure": "thisweek", "indexed_by": "closed" },
"commands": ["/reopen"]
}
User: tag with #design*
+2 -2
View File
@@ -28,8 +28,6 @@ class Command::Parser
rich_text_command = as_plain_text_with_attachable_references(string)
plain_text_command = as_plain_text(string)
Rails.logger.info "COMMANDS: #{rich_text_command} AND #{plain_text_command}"
parse_plain_text_command(plain_text_command) || parse_rich_text_command(rich_text_command)
end
@@ -57,6 +55,8 @@ class Command::Parser
Command::ClearFilters.new(params: filter.as_params)
when "/close"
Command::Close.new(card_ids: cards.ids, reason: combined_arguments)
when "/reopen"
Command::Reopen.new(card_ids: cards.ids)
when "/consider", "/reconsider"
Command::Consider.new(card_ids: cards.ids)
when "/do"
+35
View File
@@ -0,0 +1,35 @@
class Command::Reopen < Command
include Command::Cards
store_accessor :data, :reopened_card_ids
def title
"Reopen #{cards_description}"
end
def execute
reopened_card_ids = []
transaction do
cards.find_each do |card|
reopened_card_ids << card.id
card.reopen(user: user)
end
update! reopened_card_ids: reopened_card_ids
end
end
def undo
transaction do
reopened_cards.find_each do |card|
card.close(user: user)
end
end
end
private
def reopened_cards
user.accessible_cards.where(id: reopened_card_ids)
end
end
+10 -2
View File
@@ -48,8 +48,6 @@ class Command::Ai::TranslatorTest < ActionDispatch::IntegrationTest
assert_command({ context: { terms: [ "123" ] } }, "123") # Notice existing cards will be intercepted earlier
end
vcr_record!
test "acts on cards passing their ids" do
assert_command({ context: { card_ids: [ 123, 456 ] }, commands: [ "/close" ] }, "close 123 and 456")
assert_command({ context: { card_ids: [ 123 ] }, commands: [ "/close" ] }, "close 123")
@@ -70,6 +68,16 @@ class Command::Ai::TranslatorTest < ActionDispatch::IntegrationTest
assert_command({ commands: [ "/close" ] }, "close", context: :card)
end
test "reopen cards" do
# List context
assert_command({ commands: [ "/reopen" ] }, "reopen")
assert_command({ context: { assignee_ids: [ "jz" ] }, commands: [ "/reopen" ] }, "reopen cards assigned to jz")
assert_command({ context: { closure: "thisweek", indexed_by: "closed" }, commands: [ "/reopen" ] }, "reopen cards closed this week")
# Card context
assert_command({ commands: [ "/reopen" ] }, "reopen", context: :card)
end
test "assign cards" do
# List context
assert_command({ commands: [ "/assign jz" ] }, "assign to jz")
+42
View File
@@ -0,0 +1,42 @@
require "test_helper"
class Command::ReopenTest < ActionDispatch::IntegrationTest
include CommandTestHelper
setup do
Current.session = sessions(:david)
@card = cards(:text)
@card.close(user: users(:david))
end
test "reopen card on perma" do
assert_changes -> { @card.reload.open? }, from: false, to: true do
execute_command "/reopen", context_url: collection_card_url(@card.collection, @card)
end
assert_nil @card.closed_by
end
test "reopen cards on cards' index page" do
cards = cards(:logo, :text, :layout)
cards.each { |card| card.close }
execute_command "/reopen", context_url: collection_cards_url(@card.collection, indexed_by: "closed")
assert cards.map(&:reload).all?(&:open?)
end
test "undo reopen" do
cards = cards(:logo, :text, :layout)
cards.each { |card| card.close(user: users(:david)) }
command = parse_command "/reopen", context_url: collection_cards_url(@card.collection, indexed_by: "closed")
command.execute
assert cards.map(&:reload).all?(&:open?)
command.undo
assert cards.map(&:reload).all?(&:closed?)
end
end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff