cardinal-ai 0.2.17 → 0.2.18

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aaa2760a909ef2ee8cef012432e1a15311d1d3db2b1f37a5f2374e4322fc0c92
4
- data.tar.gz: 6f995d0029b94d19dbb7f26fccf1dd01014ceab5973a766a91ef72aec043335d
3
+ metadata.gz: e3382a025a7d0f1ce58e7a8a9f64a6a36705a2e27f2e9e9acd116dfa0c8955e8
4
+ data.tar.gz: 13824a0d4d5680a880151780e33b611e16c2299f9c7465bebbf47af5bf32718b
5
5
  SHA512:
6
- metadata.gz: 58d4a91169f2b8ab305e86b49d4d839cb16b275904d672525352258845af6ab70b6c47777d6a9b56dfc45b1db583b19fbb5e7bcce3d63cd8ec578e2d5a1e06ae
7
- data.tar.gz: bd8de891a41f0a7841d371d342173639b7548988b8c25377804207971da1735f6e9ea9b2a8e6e17a736a6487d0d394b0160a5aa18ce60638e51b78b1bce5c693
6
+ metadata.gz: '098257f74f2a65053e822330315ce564e44fddef11ece7cb830364f8f1f8810a8161ae98a3cf439a35d54918b4bcca4670f97ac5f8ced351ed4e0cd8ae724ae2'
7
+ data.tar.gz: 3051666fee426647550bf6abab8abd8d36f79ff8998ca90e03eb0df85b318044aa31389487270bba5a2340e1b48be846cde8253ef40e49e1cf23d0ba4b79b927
@@ -168,6 +168,14 @@ a { color: var(--blue); text-decoration: none; }
168
168
  .asana-steps li { margin: 4px 0; }
169
169
  .asana-connected { color: var(--green); }
170
170
 
171
+ .summary-share { display: flex; align-items: center; margin-top: 8px; flex-shrink: 0; }
172
+ .summary-share button[disabled] { opacity: .4; cursor: not-allowed; pointer-events: none; }
173
+ .share-spacer { flex: 1; text-align: center; }
174
+ .share-status { font-size: 12px; font-weight: 600; color: var(--green);
175
+ animation: share-fade 4s ease forwards; }
176
+ .share-status.share-err { color: var(--red); animation: none; }
177
+ @keyframes share-fade { 0%, 70% { opacity: 1; } 100% { opacity: 0; } }
178
+
171
179
  .pull-form { display: inline; }
172
180
  #repo-pull-status { font-size: 0.85rem; }
173
181
  #repo-pull-status .pull-ok { color: var(--green); }
@@ -1,5 +1,5 @@
1
1
  class CardsController < ApplicationController
2
- before_action :set_card, only: [:show, :update, :move, :approve, :summarize, :compact, :destroy, :archive, :unarchive]
2
+ before_action :set_card, only: [:show, :update, :move, :approve, :summarize, :compact, :destroy, :archive, :unarchive, :share_summary]
3
3
 
4
4
  def new
5
5
  @board = Board.first!
@@ -140,6 +140,37 @@ class CardsController < ApplicationController
140
140
  end
141
141
  end
142
142
 
143
+ # Push the customer summary outward: to the source Asana task as a comment,
144
+ # or to the card's PR. The summary was written for exactly this audience.
145
+ def share_summary
146
+ summary = @card.summary.to_s.strip
147
+ if summary.blank?
148
+ @card.log!("error", text: "Nothing to share — the summary is empty.")
149
+ return redirect_to card_path(@card, zoom: "summary")
150
+ end
151
+
152
+ flash_text, flash_error = nil, false
153
+ case params[:to]
154
+ when "asana"
155
+ Asana.comment!(@card.asana_url, "Update from Cardinal:\n\n#{summary}")
156
+ @card.log!("status_change", actor: "user", text: "Summary posted to the Asana task as a comment")
157
+ flash_text = "✓ Posted to Asana"
158
+ when "pr"
159
+ out, status = Open3.capture2e("gh", "pr", "comment", @card.pr_url, "--body", summary)
160
+ if status.success?
161
+ @card.log!("status_change", actor: "user", text: "Summary posted as a PR comment")
162
+ flash_text = "✓ Posted to the PR"
163
+ else
164
+ @card.log!("error", text: "Couldn't comment on the PR: #{out.truncate(160)}")
165
+ flash_text, flash_error = "✗ PR comment failed — see the timeline", true
166
+ end
167
+ end
168
+ respond_with_share_flash(flash_text, flash_error)
169
+ rescue Asana::Error => e
170
+ @card.log!("error", text: "Couldn't post to Asana: #{e.message}")
171
+ respond_with_share_flash("✗ Asana refused — see the timeline", true)
172
+ end
173
+
143
174
  # Archive (card #42): off the board, never gone — /board/archive lists,
144
175
  # searches, and restores. Running cards can't be archived out from under
145
176
  # their agent.
@@ -162,6 +193,20 @@ class CardsController < ApplicationController
162
193
 
163
194
  private
164
195
 
196
+ # In-place feedback on the Summary tab: replace the panel with a transient
197
+ # ✓/✗ flash next to the share buttons (falls back to a redirect for plain
198
+ # HTML requests).
199
+ def respond_with_share_flash(text, error)
200
+ respond_to do |format|
201
+ format.turbo_stream do
202
+ render turbo_stream: turbo_stream.replace("card_summary",
203
+ partial: "cards/summary_panel",
204
+ locals: { card: @card.reload, share_status: text, share_error: error })
205
+ end
206
+ format.html { redirect_to card_path(@card, zoom: "summary") }
207
+ end
208
+ end
209
+
165
210
  def set_card
166
211
  @card = Board.first!.cards.find_by!(number: params[:id])
167
212
  end
@@ -54,14 +54,26 @@ module Asana
54
54
  card
55
55
  end
56
56
 
57
- def self.request(path, auth)
57
+ # Post text to the task as a comment (an Asana "story").
58
+ def self.comment!(task_url, text)
59
+ request("/tasks/#{task_gid(task_url)}/stories", token,
60
+ method: :post, body: { data: { text: text } })
61
+ end
62
+
63
+ def self.request(path, auth, method: :get, body: nil)
58
64
  uri = URI("#{API}#{path}")
59
65
  http = Net::HTTP.new(uri.host, uri.port)
60
66
  http.use_ssl = true
61
67
  http.open_timeout = 10
62
68
  http.read_timeout = 15
63
- response = http.get(uri.request_uri, { "Authorization" => "Bearer #{auth}" })
64
- unless response.code.to_i == 200
69
+ headers = { "Authorization" => "Bearer #{auth}" }
70
+ response =
71
+ if method == :post
72
+ http.post(uri.request_uri, body.to_json, headers.merge("Content-Type" => "application/json"))
73
+ else
74
+ http.get(uri.request_uri, headers)
75
+ end
76
+ unless [200, 201].include?(response.code.to_i)
65
77
  raise Error, "Asana said no (HTTP #{response.code}) — check the token, and that it can see this task"
66
78
  end
67
79
  JSON.parse(response.body)["data"]
@@ -27,4 +27,28 @@
27
27
  disabled: card.summary_working? %>
28
28
  <% end %>
29
29
 
30
+ <% can_asana = Asana.connected? && card.asana_url.present? %>
31
+ <% can_pr = card.pr_url.present? %>
32
+ <% if can_asana || can_pr %>
33
+ <div class="summary-share">
34
+ <% if can_asana %>
35
+ <%= button_to "◯ Post as comment on Asana task", share_summary_card_path(card, to: "asana"),
36
+ class: "theme-toggle", disabled: card.summary.blank?,
37
+ data: { turbo_submits_with: "Posting…" },
38
+ title: "Post this summary as a comment on #{card.asana_url}" %>
39
+ <% end %>
40
+ <span class="share-spacer">
41
+ <% if local_assigns[:share_status].present? %>
42
+ <span class="share-status<%= " share-err" if local_assigns[:share_error] %>"><%= share_status %></span>
43
+ <% end %>
44
+ </span>
45
+ <% if can_pr %>
46
+ <%= button_to "💬 Post as comment on PR", share_summary_card_path(card, to: "pr"),
47
+ class: "theme-toggle", disabled: card.summary.blank?,
48
+ data: { turbo_submits_with: "Posting…" },
49
+ title: "Post this summary as a comment on #{card.pr_url}" %>
50
+ <% end %>
51
+ </div>
52
+ <% end %>
53
+
30
54
  </div>
data/config/routes.rb CHANGED
@@ -15,6 +15,7 @@ Rails.application.routes.draw do
15
15
  post :approve
16
16
  post :archive
17
17
  post :unarchive
18
+ post :share_summary
18
19
  post :summarize
19
20
  post :compact
20
21
  end
@@ -1,3 +1,3 @@
1
1
  module Cardinal
2
- VERSION = "0.2.17"
2
+ VERSION = "0.2.18"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cardinal-ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.17
4
+ version: 0.2.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Ellis