tina4ruby 3.13.88 → 3.13.90

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: 8878ff3cfcfe50696d437b251a372f66d99a8098707e7cfd919554ec24d996fb
4
- data.tar.gz: dbc5ae0b61c125a88fda3310d7dff72e0bb5a5d52d94ae863455ff607c5aab35
3
+ metadata.gz: bcacb51cc9aec287d69bb3d18d0d5736d4e72fd9f25e4b28145af7a8c3e69245
4
+ data.tar.gz: 5cbe29c5b764594b014c386ab1df06e93d67cc8ad79ee321a2d5c03c5383cc73
5
5
  SHA512:
6
- metadata.gz: b51a87207d60bff1cd55241e23eb3b7325980752430bfef840ed8fce8e1ba491fbdf4096dd8369548112163ec40147b5e70874b3d6b66c174d82a5161e3f2062
7
- data.tar.gz: 8b03d0f03526f60931fefafdf78f21e84b3e85dfb040af5106a1a7c26677e110fc21a2274b8d623bbbab60c23ba70661d8612ae02c43f024d29e02a82ca5e167
6
+ metadata.gz: b01320730d284fb8f8babdefdcd6c26fc3a4ae29e5ee3a2f700320276a1d211d5d3e88fc0c8de4e1722af814a60333a4056041428fd387ec70446b50f066a208
7
+ data.tar.gz: 32077d83e0b6c457656915818683193f8a4ef9b3dfd505e3035d97f7b835b979d29a536f6993aa85d987138e9fe1aa229d1d002fec769879fb0c4ca1dbaef2fa
data/lib/tina4/cli.rb CHANGED
@@ -233,6 +233,21 @@ module Tina4
233
233
  to_snake_case(name)
234
234
  end
235
235
 
236
+ # Called without --fields, the generators fall back to a single `name`
237
+ # string column. That default MUST be materialised here, in one place, and
238
+ # then flow into the model, the migration, the form, the view and the spec
239
+ # alike. It used to live only inside the model template, so `generate model
240
+ # X` / `generate crud X` wrote a model declaring `name` while the migration
241
+ # - built from the parsed field list, which was empty - created only id +
242
+ # created_at. The first write then failed with "no such column: name".
243
+ DEFAULT_FIELDS = [["name", "string"]].freeze
244
+
245
+ # Parsed --fields, or the default single `name` column when none given.
246
+ def fields_or_default(fields_str)
247
+ parsed = parse_fields(fields_str)
248
+ parsed.any? ? parsed : DEFAULT_FIELDS.map(&:dup)
249
+ end
250
+
236
251
  # Parse "name:string,price:float" -> [["name","string"], ["price","float"]]
237
252
  def parse_fields(fields_str)
238
253
  return [] if fields_str.nil? || fields_str.strip.empty?
@@ -1231,19 +1246,15 @@ module Tina4
1231
1246
  # ── Generator: model ─────────────────────────────────────────────────
1232
1247
 
1233
1248
  def generate_model(name, flags, emit_test: true)
1234
- fields = parse_fields(flags["fields"])
1249
+ fields = fields_or_default(flags["fields"])
1235
1250
  table = to_table_name(name)
1236
1251
  snake = to_snake_case(name)
1237
1252
 
1238
1253
  # Build field lines
1239
1254
  field_lines = [" integer_field :id, primary_key: true, auto_increment: true"]
1240
- if fields.any?
1241
- fields.each do |fname, ftype|
1242
- info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP["string"]
1243
- field_lines << " #{info[:orm]} :#{fname}"
1244
- end
1245
- else
1246
- field_lines << " string_field :name"
1255
+ fields.each do |fname, ftype|
1256
+ info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP["string"]
1257
+ field_lines << " #{info[:orm]} :#{fname}"
1247
1258
  end
1248
1259
  field_lines << " string_field :created_at"
1249
1260
 
@@ -1355,6 +1366,11 @@ module Tina4
1355
1366
  Tina4::Router.post "/api/#{route_path}" do |request, response|
1356
1367
  #{ext_create.chomp}
1357
1368
  item = #{model}.create(request.body)
1369
+ # create/save signal failure by RETURN VALUE, they do not raise -
1370
+ # unchecked, a failed write surfaces as an unrelated NoMethodError
1371
+ # on false and hides the real cause.
1372
+ next response.json({ error: "Could not create #{singular}" }, 400) if item == false
1373
+
1358
1374
  response.json(item.to_h, 201)
1359
1375
  end#{no_auth}
1360
1376
 
@@ -1368,7 +1384,8 @@ module Tina4
1368
1384
  setter = "#{'#'}{key}="
1369
1385
  item.send(setter, value) if item.respond_to?(setter)
1370
1386
  end
1371
- item.save
1387
+ next response.json({ error: "Could not update #{singular}" }, 400) if item.save == false
1388
+
1372
1389
  response.json(item.to_h)
1373
1390
  end#{no_auth}
1374
1391
 
@@ -1521,7 +1538,11 @@ module Tina4
1521
1538
  end
1522
1539
 
1523
1540
  # Build SQL columns from fields
1524
- fields = fields_override || parse_fields(flags["fields"])
1541
+ # An EMPTY array is truthy in Ruby, so a plain `fields_override || parse`
1542
+ # short-circuits to [] and the fallback never fires - that is exactly how
1543
+ # a model declaring `name` ended up with a column-less migration. Test for
1544
+ # content, not truthiness, so the semantics match Python/Node.
1545
+ fields = fields_override&.any? ? fields_override : parse_fields(flags["fields"])
1525
1546
  is_create = name.start_with?("create_") || !fields_override.nil?
1526
1547
 
1527
1548
  filename = "#{timestamp}_#{name}.sql"
@@ -1771,7 +1792,7 @@ module Tina4
1771
1792
  # ── Generator: form ──────────────────────────────────────────────────
1772
1793
 
1773
1794
  def generate_form(name, flags = {})
1774
- fields = parse_fields(flags["fields"])
1795
+ fields = fields_or_default(flags["fields"])
1775
1796
  table = to_table_name(name)
1776
1797
  route_name = "#{table}s"
1777
1798
 
@@ -1794,8 +1815,7 @@ module Tina4
1794
1815
 
1795
1816
  # Build form fields
1796
1817
  field_html = ""
1797
- form_fields = fields.any? ? fields : [["name", "string"]]
1798
- form_fields.each do |fname, ftype|
1818
+ fields.each do |fname, ftype|
1799
1819
  itype = input_types[ftype] || "text"
1800
1820
  label = fname.tr("_", " ").split.map(&:capitalize).join(" ")
1801
1821
  step = %w[float numeric decimal].include?(ftype) ? ' step="0.01"' : ""
@@ -1850,11 +1870,11 @@ module Tina4
1850
1870
  # ── Generator: view ──────────────────────────────────────────────────
1851
1871
 
1852
1872
  def generate_view(name, flags = {})
1853
- fields = parse_fields(flags["fields"])
1873
+ fields = fields_or_default(flags["fields"])
1854
1874
  table = to_table_name(name)
1855
1875
  route_name = "#{table}s"
1856
1876
 
1857
- cols = fields.any? ? fields.map { |f, _| f } : ["name"]
1877
+ cols = fields.map { |f, _| f }
1858
1878
 
1859
1879
  dir = "src/templates/pages"
1860
1880
  FileUtils.mkdir_p(dir)
@@ -2480,7 +2500,10 @@ module Tina4
2480
2500
 
2481
2501
  # model -> real SQLite roundtrip (create / read back / missing -> nil).
2482
2502
  def emit_model_test(model, table, fields)
2483
- fields = fields.empty? ? [["name", "string"]] : fields
2503
+ # Reuse the single DEFAULT_FIELDS constant rather than re-stating the
2504
+ # literal, so the co-emitted spec can never describe a shape the model
2505
+ # does not actually have.
2506
+ fields = fields.empty? ? DEFAULT_FIELDS.map(&:dup) : fields
2484
2507
  payload = fields.map { |fname, ftype| %("#{fname}" => #{sample_literal(ftype)}) }.join(", ")
2485
2508
  # Assert a STRING field round-trips (type-safe); else just the id round-trips
2486
2509
  # (avoids datetime/bool/float equality pitfalls on the read-back).
data/lib/tina4/frond.rb CHANGED
@@ -143,6 +143,26 @@ module Tina4
143
143
  IMPORT_AS_RE = /\Aimport\s+["'](.+?)["']\s+as\s+(\w+)/
144
144
  CACHE_RE = /\Acache\s+["'](.+?)["']\s*(\d+)?/
145
145
  SPACELESS_RE = />\s+</
146
+
147
+ # Every tag that OPENS a construct. An unknown tag is a typo, and 3.13.89
148
+ # makes it raise rather than render its body: a mistyped guard --
149
+ # {% iff is_admin %} instead of {% if is_admin %} -- used to render the gated
150
+ # content UNCONDITIONALLY, so a reviewer saw a guard that was not there. Twig
151
+ # and Jinja2 both raise on an unknown tag; Frond now does too. There is no
152
+ # user-extension point for tags in any of the four frameworks, so an unknown
153
+ # name is always a mistake, never a plugin.
154
+ KNOWN_TAGS = %w[
155
+ autoescape block cache extends for from if import include live macro raw set
156
+ spaceless
157
+ ].freeze
158
+
159
+ # Terminators and branch keywords. These reach the tag dispatch only when
160
+ # stray (their own collector consumes them in the normal case), and a stray
161
+ # one keeps the old render-nothing behaviour -- see the comment at the raise.
162
+ TERMINATOR_TAGS = %w[
163
+ elif else elseif endautoescape endblock endcache endfor endif endlive
164
+ endmacro endraw endset endspaceless
165
+ ].freeze
146
166
  AUTOESCAPE_RE = /\Aautoescape\s+(false|true)/
147
167
  STRIPTAGS_RE = /<[^>]+>/
148
168
  THOUSANDS_RE = /(\d)(?=(\d{3})+(?!\d))/
@@ -713,8 +733,17 @@ module Tina4
713
733
  result, i = handle_for(tokens, i, context)
714
734
  output << result
715
735
  when "set"
716
- handle_set(content, context)
717
- i += 1
736
+ # An assignment has an "="; without one this is the BLOCK form,
737
+ # {% set name %}...{% endset %}, which captures its rendered body.
738
+ # A bare include? is exact here, not a shortcut: the block form's tag
739
+ # content is only ever "set <name>", so an "=" anywhere -- even inside
740
+ # a quoted value like {% set m = "a = b" %} -- means assignment.
741
+ if content.include?("=")
742
+ handle_set(content, context)
743
+ i += 1
744
+ else
745
+ i = handle_set_block(tokens, i, context)
746
+ end
718
747
  when "include"
719
748
  if @sandbox && @allowed_tags && !@allowed_tags.include?("include")
720
749
  i += 1
@@ -746,6 +775,15 @@ module Tina4
746
775
  i += 1
747
776
  else
748
777
  i += 1
778
+ unless tag.empty? || TERMINATOR_TAGS.include?(tag)
779
+ raise ArgumentError,
780
+ %(Frond: unknown tag "#{tag}" -- known tags are: #{KNOWN_TAGS.sort.join(", ")})
781
+ end
782
+ # An empty tag ({% %}) or a stray terminator (an {% endif %} with
783
+ # no {% if %}): no output.
784
+ # Malformed, but it has always rendered nothing, and nothing is the
785
+ # safe answer -- unlike an unknown tag it cannot expose content that
786
+ # was meant to be gated.
749
787
  end
750
788
 
751
789
  if strip_a && i < tokens.length && tokens[i][0] == TEXT
@@ -2344,6 +2382,48 @@ module Tina4
2344
2382
  )
2345
2383
  end
2346
2384
 
2385
+ # {% set name %}...{% endset %} -- render the body and bind it.
2386
+ #
2387
+ # Emits nothing itself. The captured value is a SafeString because it is
2388
+ # template output that has already been escaped on the way in; re-escaping it
2389
+ # at {{ name }} would double-encode every entity. Twig and Jinja2 both mark
2390
+ # the capture safe. Returns the index just past {% endset %}.
2391
+ def handle_set_block(tokens, start, context)
2392
+ content, _, _ = strip_tag(tokens[start][1])
2393
+ name = (content.split[1] || "").strip
2394
+
2395
+ body_tokens = []
2396
+ i = start + 1
2397
+ depth = 0
2398
+ while i < tokens.length
2399
+ if tokens[i][0] == BLOCK
2400
+ tc, _, _ = strip_tag(tokens[i][1])
2401
+ tag = tc.split[0] || ""
2402
+ if tag == "set" && !tc.include?("=")
2403
+ depth += 1
2404
+ body_tokens << tokens[i]
2405
+ elsif tag == "endset"
2406
+ if depth.zero?
2407
+ i += 1
2408
+ break
2409
+ end
2410
+ depth -= 1
2411
+ body_tokens << tokens[i]
2412
+ else
2413
+ body_tokens << tokens[i]
2414
+ end
2415
+ else
2416
+ body_tokens << tokens[i]
2417
+ end
2418
+ i += 1
2419
+ end
2420
+
2421
+ unless name.empty?
2422
+ context[name] = Tina4::SafeString.new(render_tokens(body_tokens.dup, context))
2423
+ end
2424
+ i
2425
+ end
2426
+
2347
2427
  def handle_spaceless(tokens, start, context)
2348
2428
  body_tokens = []
2349
2429
  i = start + 1
@@ -985,7 +985,7 @@ ${t.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
985
985
  <span class="thread-pip"></span>
986
986
  <span class="thread-title">${g(t.title)}</span>
987
987
  ${o}
988
- </div>`}).join("")}let cd=!1;async function Qa(i=0){for(let e=0;;e++)try{Se=await NZ(),cd=!1,EQ();return}catch(t){if(e>=i){cd=!0,console.error("refreshThreadList failed",t);return}await new Promise(n=>setTimeout(n,250*2**e))}}function HZ(i){const e=document.getElementById("editor-ai-messages");if(!e)return;const t=rt.get(i)||[];if(!t.length){e.innerHTML='<div class="ai-msg ai-bot" style="opacity:0.6">Start the conversation…</div>';return}e.innerHTML="";for(const n of t){const r=document.createElement("div");r.className=`ai-msg ai-${n.role==="user"?"user":"bot"}`,n.role==="user"?r.textContent=n.content:r.innerHTML=fs(n.content),e.appendChild(r)}e.scrollTop=e.scrollHeight}async function AQ(i,e=!1){if(!(!e&&rt.has(i)))try{const t=await FZ(i);rt.set(i,t)}catch(t){console.error(`loadThreadMessages(${i}) failed`,t),rt.set(i,[])}}async function dd(i){if(i!==ve){ve=i;try{localStorage.setItem(WQ,i)}catch{}EQ(),await AQ(i),HZ(i)}}async function KZ(){try{const i=await cs();Se.push(i),rt.set(i.id,[]),await dd(i.id);const e=document.getElementById("editor-ai-input");e==null||e.focus()}catch(i){AZ(`<span style="color:var(--danger)">Couldn't create thread: ${g(String((i==null?void 0:i.message)||i))}</span>`,"bot")}}async function JZ(i){const e=document.querySelector(`.thread-row[data-thread-id="${CSS.escape(i)}"]`),t=e==null?void 0:e.querySelector(".thread-title");if(!e||!t)return;const n=t.textContent||"",r=document.createElement("input");r.className="thread-title-edit",r.value=n,t.replaceWith(r),r.focus(),r.select();const s=a=>{const l=document.createElement("span");l.className="thread-title",l.textContent=a,r.replaceWith(l)},o=async()=>{const a=r.value.trim();if(!a||a===n){s(n);return}try{const l=await ga(i,{title:a}),O=Se.find(c=>c.id===i);O&&(O.title=l.title),s(l.title)}catch(l){console.error("rename failed",l),s(n)}};r.addEventListener("blur",()=>{o()}),r.addEventListener("keydown",a=>{a.key==="Enter"?(a.preventDefault(),r.blur()):a.key==="Escape"&&(a.preventDefault(),s(n))})}async function ez(){if(await Qa(4),!Se.length){ve=null,er();return}try{const i=localStorage.getItem(WQ);i&&Se.some(e=>e.id===i)&&(ve=i)}catch{}ya()}window.__editorThreadNew=()=>{KZ()},window.__editorThreadSwitch=i=>{dd(i)},window.__editorThreadRename=i=>{JZ(i)};const tz={done:"DONE",awaiting_customer:"AWAITING YOU",wont_do:"WONT DO",blocked:"BLOCKED",feedback:"NEW FEEDBACK",idle:"IDLE",running:"RUNNING"};function hd(i){const e=tz[i]||i.toUpperCase();return`<span class="status-pill" data-status="${g(i)}">${g(e)}</span>`}function fd(i){if(!i)return"";let e;if(/^\d+Z$/.test(i)?e=new Date(parseInt(i,10)*1e3):e=new Date(i),isNaN(e.getTime()))return i;const t=n=>String(n).padStart(2,"0");return`${t(e.getDate())}/${t(e.getMonth()+1)}/${e.getFullYear()} ${t(e.getHours())}:${t(e.getMinutes())}`}let Jn=null;function ya(){const i=e=>document.getElementById(e);i("threads-pane-head-list").hidden=!1,i("threads-pane-head-detail").hidden=!0,i("threads-list-view").hidden=!1,i("threads-detail-view").hidden=!0,Qa().then(er)}async function ba(i){await dd(i);const e=Se.find(s=>s.id===i);if(!e)return;const t=s=>document.getElementById(s);t("threads-pane-head-list").hidden=!0,t("threads-pane-head-detail").hidden=!1,t("threads-list-view").hidden=!0,t("threads-detail-view").hidden=!1,t("threads-detail-title").textContent=e.title||"Thread";const n=t("threads-detail-meta"),r=e.sender?`<span>📨 from ${g(e.sender)}</span>`:"";n.innerHTML=`${hd(e.status_hint||"idle")} <span>${g(fd(e.last_message_at))}</span> ${r}`,pd(i),setTimeout(()=>{var s;return(s=t("threads-reply-input"))==null?void 0:s.focus()},30)}async function iz(){if(!ve)return;const i=ve;try{await ga(i,{archived:!0,closure_reason:"done"});const e=Se.find(t=>t.id===i);e&&(e.archived=!0,e.closure_reason="done"),ve=null,ya()}catch(e){console.error("archive failed",e)}}async function nz(i){try{await ga(i,{archived:!0});const e=Se.find(t=>t.id===i);e&&(e.archived=!0),ve===i?(ve=null,ya()):er()}catch(e){console.error("archive-from-list failed",e)}}function er(){const i=document.getElementById("threads-rows");if(!i)return;if(!Se.length){i.innerHTML=cd?`<div class="threads-empty">
988
+ </div>`}).join("")}let cd=!1;async function Qa(i=0){for(let e=0;;e++)try{Se=await NZ(),cd=!1,EQ();return}catch(t){if(e>=i){cd=!0,console.error("refreshThreadList failed",t);return}await new Promise(n=>setTimeout(n,250*2**e))}}function HZ(i){const e=document.getElementById("editor-ai-messages");if(!e)return;const t=rt.get(i)||[];if(!t.length){e.innerHTML='<div class="ai-msg ai-bot" style="opacity:0.6">Start the conversation…</div>';return}e.innerHTML="";for(const n of t){const r=document.createElement("div");r.className=`ai-msg ai-${n.role==="user"?"user":"bot"}`,n.role==="user"?r.textContent=n.content:r.innerHTML=fs(n.content),e.appendChild(r)}e.scrollTop=e.scrollHeight}async function AQ(i,e=!1){if(!(!e&&rt.has(i)))try{const t=await FZ(i);rt.set(i,t)}catch(t){console.error(`loadThreadMessages(${i}) failed`,t),rt.set(i,[])}}async function dd(i){if(i!==ve){ve=i;try{localStorage.setItem(WQ,i)}catch{}EQ(),await AQ(i),HZ(i)}}async function KZ(){try{const i=await cs();Se.push(i),rt.set(i.id,[]),await dd(i.id);const e=document.getElementById("editor-ai-input");e==null||e.focus()}catch(i){AZ(`<span style="color:var(--danger)">Couldn't create thread: ${g(String((i==null?void 0:i.message)||i))}</span>`,"bot")}}async function JZ(i){const e=document.querySelector(`.thread-row[data-thread-id="${CSS.escape(i)}"]`),t=e==null?void 0:e.querySelector(".thread-title");if(!e||!t)return;const n=t.textContent||"",r=document.createElement("input");r.className="thread-title-edit",r.value=n,t.replaceWith(r),r.focus(),r.select();const s=a=>{const l=document.createElement("span");l.className="thread-title",l.textContent=a,r.replaceWith(l)},o=async()=>{const a=r.value.trim();if(!a||a===n){s(n);return}try{const l=await ga(i,{title:a}),O=Se.find(c=>c.id===i);O&&(O.title=l.title),s(l.title)}catch(l){console.error("rename failed",l),s(n)}};r.addEventListener("blur",()=>{o()}),r.addEventListener("keydown",a=>{a.key==="Enter"?(a.preventDefault(),r.blur()):a.key==="Escape"&&(a.preventDefault(),s(n))})}async function ez(){if(await Qa(4),!Se.length){ve=null,er();return}try{const i=localStorage.getItem(WQ);i&&Se.some(e=>e.id===i)&&(ve=i)}catch{}ya()}window.__editorThreadNew=()=>{KZ()},window.__editorThreadSwitch=i=>{dd(i)},window.__editorThreadRename=i=>{JZ(i)};const tz={done:"DONE",awaiting_customer:"AWAITING YOU",wont_do:"WONT DO",blocked:"BLOCKED",feedback:"NEW FEEDBACK",idle:"IDLE",running:"RUNNING"};function hd(i){const e=tz[i]||i.toUpperCase();return`<span class="status-pill" data-status="${g(i)}">${g(e)}</span>`}function fd(i){if(!i)return"";let e;if(/^\d+Z$/.test(i)?e=new Date(parseInt(i,10)*1e3):e=new Date(i),isNaN(e.getTime()))return i;const t=n=>String(n).padStart(2,"0");return`${t(e.getDate())}/${t(e.getMonth()+1)}/${e.getFullYear()} ${t(e.getHours())}:${t(e.getMinutes())}`}let Jn=null;function ya(){const i=s=>document.getElementById(s),e=i("threads-pane-head-list");e&&(e.hidden=!1);const t=i("threads-pane-head-detail");t&&(t.hidden=!0);const n=i("threads-list-view");n&&(n.hidden=!1);const r=i("threads-detail-view");r&&(r.hidden=!0),Qa().then(er)}async function ba(i){await dd(i);const e=Se.find(c=>c.id===i);if(!e)return;const t=c=>document.getElementById(c),n=t("threads-pane-head-list");n&&(n.hidden=!0);const r=t("threads-pane-head-detail");r&&(r.hidden=!1);const s=t("threads-list-view");s&&(s.hidden=!0);const o=t("threads-detail-view");o&&(o.hidden=!1);const a=t("threads-detail-title");a&&(a.textContent=e.title||"Thread");const l=t("threads-detail-meta"),O=e.sender?`<span>📨 from ${g(e.sender)}</span>`:"";l&&(l.innerHTML=`${hd(e.status_hint||"idle")} <span>${g(fd(e.last_message_at))}</span> ${O}`),pd(i),setTimeout(()=>{var c;return(c=t("threads-reply-input"))==null?void 0:c.focus()},30)}async function iz(){if(!ve)return;const i=ve;try{await ga(i,{archived:!0,closure_reason:"done"});const e=Se.find(t=>t.id===i);e&&(e.archived=!0,e.closure_reason="done"),ve=null,ya()}catch(e){console.error("archive failed",e)}}async function nz(i){try{await ga(i,{archived:!0});const e=Se.find(t=>t.id===i);e&&(e.archived=!0),ve===i?(ve=null,ya()):er()}catch(e){console.error("archive-from-list failed",e)}}function er(){const i=document.getElementById("threads-rows");if(!i)return;if(!Se.length){i.innerHTML=cd?`<div class="threads-empty">
989
989
  <div style="margin-bottom:0.6rem">Can't reach the agent — threads unavailable.</div>
990
990
  <button type="button" class="action-pill"
991
991
  onclick="window.__threadsRetry()">Retry</button>
@@ -1012,7 +1012,7 @@ ${t.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
1012
1012
  <span>severity: ${g(e.severity||"-")}</span>
1013
1013
  </div>
1014
1014
  <div style="margin-top:0.3rem">${g(e.summary||"")}</div>
1015
- </div>`}catch{return fs(i)}}async function az(){try{const i=await cs();Se.push(i),rt.set(i.id,[]),await ba(i.id)}catch(i){console.error("threadsNew failed",i)}}async function lz(){if(!ve)return;const i=Se.find(t=>t.id===ve);if(!i)return;const e=window.prompt("Rename thread:",i.title);if(!(e==null||e.trim()===""||e===i.title))try{const t=await ga(ve,{title:e.trim()});i.title=t.title;const n=document.getElementById("threads-detail-title");n&&(n.textContent=t.title),er()}catch(t){console.error("rename failed",t)}}function Oz(i){return i?i.status_hint==="done"||i.status_hint==="wont_do"||i.closure_reason==="done"||i.closure_reason==="wont_do":!1}async function md(){const i=document.getElementById("threads-reply-input");if(!i)return;let e=i.value.trim();if(!e)return;i.value="";const t=e.match(/^new topic:\s*(.*)$/is);if(t){const o=t[1].trim();try{const a=await cs(o.slice(0,80)||void 0);if(Se.push(a),rt.set(a.id,[]),await ba(a.id),!o)return;e=o}catch(a){console.error("New Topic spawn failed",a);return}}if(!t&&ve&&Oz(Se.find(o=>o.id===ve)))try{const o=await cs(e.slice(0,80)||void 0);Se.push(o),rt.set(o.id,[]),await ba(o.id)}catch(o){console.error("auto-new-thread on done reply failed",o);return}if(!ve)try{const o=await cs(e.slice(0,80));Se.push(o),rt.set(o.id,[]),ve=o.id}catch(o){console.error("auto-create failed",o);return}const n=ve,r=rt.get(n)||[];r.push({id:`local-${Date.now()}`,role:"user",content:e,timestamp:new Date().toISOString(),thread_id:n}),rt.set(n,r),pd(n),$a.add(n),er();const s=document.getElementById("threads-chat");Jn==null||Jn.abort(),Jn=new AbortController;try{await pz(e,n,Jn.signal,s)}catch(o){if((o==null?void 0:o.name)!=="AbortError"){const a=document.createElement("div");a.className="ai-msg ai-bot",a.style.color="var(--danger,#f38ba8)",a.textContent=`Connection failed: ${(o==null?void 0:o.message)||o}`,s==null||s.appendChild(a)}}finally{$a.delete(n),Jn=null;try{await AQ(n,!0)}catch{}await Qa(),pd(n);const o=Se.find(a=>a.id===n);if(o){const a=document.getElementById("threads-detail-meta"),l=o.sender?`<span>📨 from ${g(o.sender)}</span>`:"";a.innerHTML=`${hd(o.status_hint||"idle")} <span>${g(fd(o.last_message_at))}</span> ${l}`}}}queueMicrotask(()=>{const i=document.getElementById("threads-reply-form");i&&i.addEventListener("submit",t=>{t.preventDefault(),md()});const e=document.getElementById("threads-reply-input");e&&e.addEventListener("keydown",t=>{t.key==="Enter"&&!t.shiftKey&&(t.preventDefault(),t.stopPropagation(),md())})}),window.__threadsShowList=()=>ya(),window.__threadsShowDetail=i=>{ba(i)},window.__threadsNew=()=>{az()},window.__threadsRetry=()=>{Qa(2).then(er)},window.__threadsRenameActive=()=>{lz()};let un=!1;async function jQ(){const i=document.getElementById("plans-panel"),e=document.getElementById("plans-toggle-btn");i&&(un=!un,i.hidden=!un,e==null||e.classList.toggle("active",un),un&&await cz())}async function cz(){const i=document.getElementById("plans-rows");if(i){i.innerHTML='<div class="threads-empty">Loading plans…</div>';try{const e=await Mi("plan_list",{}),t=e.ok&&Array.isArray(e.result)?e.result:[];if(!t.length){i.innerHTML='<div class="threads-empty">No plans yet — they appear here when the planner agent creates one.</div>';return}const n=[...t].sort((r,s)=>String(s.name||s.file||"").localeCompare(String(r.name||r.file||"")));i.innerHTML=n.map(r=>{var h,f;const s=String(r.name||r.file||""),o=String(r.path||`plan/${s}`),a=String(r.title||s).slice(0,60),l=r.steps_done??((h=r.progress)==null?void 0:h.done)??0,O=r.steps_total??((f=r.progress)==null?void 0:f.total)??0,c=O>0?`${l}/${O} steps`:"",d=r.is_current||r.current?" · ★ current":"";return`<div class="plan-row" onclick="window.__plansOpen('${g(o)}')" title="Open ${g(o)} in editor">
1015
+ </div>`}catch{return fs(i)}}async function az(){try{const i=await cs();Se.push(i),rt.set(i.id,[]),await ba(i.id)}catch(i){console.error("threadsNew failed",i)}}async function lz(){if(!ve)return;const i=Se.find(t=>t.id===ve);if(!i)return;const e=window.prompt("Rename thread:",i.title);if(!(e==null||e.trim()===""||e===i.title))try{const t=await ga(ve,{title:e.trim()});i.title=t.title;const n=document.getElementById("threads-detail-title");n&&(n.textContent=t.title),er()}catch(t){console.error("rename failed",t)}}function Oz(i){return i?i.status_hint==="done"||i.status_hint==="wont_do"||i.closure_reason==="done"||i.closure_reason==="wont_do":!1}async function md(){const i=document.getElementById("threads-reply-input");if(!i)return;let e=i.value.trim();if(!e)return;i.value="";const t=e.match(/^new topic:\s*(.*)$/is);if(t){const o=t[1].trim();try{const a=await cs(o.slice(0,80)||void 0);if(Se.push(a),rt.set(a.id,[]),await ba(a.id),!o)return;e=o}catch(a){console.error("New Topic spawn failed",a);return}}if(!t&&ve&&Oz(Se.find(o=>o.id===ve)))try{const o=await cs(e.slice(0,80)||void 0);Se.push(o),rt.set(o.id,[]),await ba(o.id)}catch(o){console.error("auto-new-thread on done reply failed",o);return}if(!ve)try{const o=await cs(e.slice(0,80));Se.push(o),rt.set(o.id,[]),ve=o.id}catch(o){console.error("auto-create failed",o);return}const n=ve,r=rt.get(n)||[];r.push({id:`local-${Date.now()}`,role:"user",content:e,timestamp:new Date().toISOString(),thread_id:n}),rt.set(n,r),pd(n),$a.add(n),er();const s=document.getElementById("threads-chat");Jn==null||Jn.abort(),Jn=new AbortController;try{await pz(e,n,Jn.signal,s)}catch(o){if((o==null?void 0:o.name)!=="AbortError"){const a=document.createElement("div");a.className="ai-msg ai-bot",a.style.color="var(--danger,#f38ba8)",a.textContent=`Connection failed: ${(o==null?void 0:o.message)||o}`,s==null||s.appendChild(a)}}finally{$a.delete(n),Jn=null;try{await AQ(n,!0)}catch{}await Qa(),pd(n);const o=Se.find(l=>l.id===n),a=document.getElementById("threads-detail-meta");if(o&&a){const l=o.sender?`<span>📨 from ${g(o.sender)}</span>`:"";a.innerHTML=`${hd(o.status_hint||"idle")} <span>${g(fd(o.last_message_at))}</span> ${l}`}}}queueMicrotask(()=>{const i=document.getElementById("threads-reply-form");i&&i.addEventListener("submit",t=>{t.preventDefault(),md()});const e=document.getElementById("threads-reply-input");e&&e.addEventListener("keydown",t=>{t.key==="Enter"&&!t.shiftKey&&(t.preventDefault(),t.stopPropagation(),md())})}),window.__threadsShowList=()=>ya(),window.__threadsShowDetail=i=>{ba(i)},window.__threadsNew=()=>{az()},window.__threadsRetry=()=>{Qa(2).then(er)},window.__threadsRenameActive=()=>{lz()};let un=!1;async function jQ(){const i=document.getElementById("plans-panel"),e=document.getElementById("plans-toggle-btn");i&&(un=!un,i.hidden=!un,e==null||e.classList.toggle("active",un),un&&await cz())}async function cz(){const i=document.getElementById("plans-rows");if(i){i.innerHTML='<div class="threads-empty">Loading plans…</div>';try{const e=await Mi("plan_list",{}),t=e.ok&&Array.isArray(e.result)?e.result:[];if(!t.length){i.innerHTML='<div class="threads-empty">No plans yet — they appear here when the planner agent creates one.</div>';return}const n=[...t].sort((r,s)=>String(s.name||s.file||"").localeCompare(String(r.name||r.file||"")));i.innerHTML=n.map(r=>{var h,f;const s=String(r.name||r.file||""),o=String(r.path||`plan/${s}`),a=String(r.title||s).slice(0,60),l=r.steps_done??((h=r.progress)==null?void 0:h.done)??0,O=r.steps_total??((f=r.progress)==null?void 0:f.total)??0,c=O>0?`${l}/${O} steps`:"",d=r.is_current||r.current?" · ★ current":"";return`<div class="plan-row" onclick="window.__plansOpen('${g(o)}')" title="Open ${g(o)} in editor">
1016
1016
  <div class="plan-name">${g(a)}</div>
1017
1017
  <div class="plan-meta">${g(s)}${c?" · "+g(c):""}${d}</div>
1018
1018
  </div>`}).join("")}catch{i.innerHTML='<div class="threads-empty" style="color:var(--danger,#f38ba8)">Failed to load plans</div>'}}}async function dz(i){var t;try{await ui(i)}catch(n){console.error("plansOpen failed",n)}un=!1;const e=document.getElementById("plans-panel");e&&(e.hidden=!0),(t=document.getElementById("plans-toggle-btn"))==null||t.classList.remove("active")}window.__plansToggle=()=>{jQ()},window.__plansOpen=i=>{dz(i)};let ds=!1;async function hz(){const i=document.getElementById("grounding-panel"),e=document.getElementById("grounding-toggle-btn");i&&(un&&jQ(),ds=!ds,i.hidden=!ds,e==null||e.classList.toggle("active",ds),ds&&await MQ())}async function MQ(){const i=document.getElementById("grounding-body");if(!i)return;i.innerHTML='<div class="threads-empty">Loading…</div>';let e={};try{const r=await fetch("/__dev/api/grounding/status");r.ok&&(e=await r.json())}catch{}const t=g(e.url||"https://mcp.tina4.com"),n=e.configured?`<span style="color:var(--success,#a6e3a1)">&#9679; Configured</span> <span style="opacity:0.6">(…${g(e.last4||"")})</span>`:'<span style="color:var(--warn,#f9e2af)">&#9675; Not set</span> — using local corpus fallback';i.innerHTML=`
data/lib/tina4/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.88"
4
+ VERSION = "3.13.90"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.13.88
4
+ version: 3.13.90
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team