tina4ruby 3.13.89 → 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: c9a4969bc3af1b2a6f203d215043c4fb9497aa40e71a0f7d1c17d92eddeecea3
4
- data.tar.gz: 67a4a34e48ca3e167201ab06ac5cab96be36b4f4224a97f04cae0c58f700184c
3
+ metadata.gz: bcacb51cc9aec287d69bb3d18d0d5736d4e72fd9f25e4b28145af7a8c3e69245
4
+ data.tar.gz: 5cbe29c5b764594b014c386ab1df06e93d67cc8ad79ee321a2d5c03c5383cc73
5
5
  SHA512:
6
- metadata.gz: a89938265a10f5b8f0a48966f370c55006937a236285fe6b8f2fd9c5f75464d5c4fc0fc65cf1583f76539e24cd095701e3c092d87fb9d1197978d1d9774f552c
7
- data.tar.gz: af228d8eebfab34598468e44f6962ac701280d4ad1207e4184f2eee6aabc631c5dda36753de7c99b1e8aea4dde7cff635fa2c44c30fff469532124cab0d9f11a
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).
@@ -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.89"
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.89
4
+ version: 3.13.90
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team