tina4-nodejs 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.
package/CLAUDE.md CHANGED
@@ -1,10 +1,10 @@
1
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.89)
1
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.90)
2
2
 
3
3
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
4
4
 
5
5
  ## What This Project Is
6
6
 
7
- Tina4 for Node.js/TypeScript v3.13.89 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.13.90 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
8
8
 
9
9
  The philosophy: zero ceremony, batteries included, file system as source of truth.
10
10
 
@@ -1244,7 +1244,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
1244
1244
  ## v3 Features Summary
1245
1245
 
1246
1246
  - **98 built-in features**, zero third-party dependencies
1247
- - **5,916 tests** passing across 189 files (build + typecheck green)
1247
+ - **5,923 tests** passing across 189 files (build + typecheck green)
1248
1248
  - **Race-safe `getNextId()`** with atomic sequence table (`tina4_sequences`) for SQLite/MySQL/MSSQL; PostgreSQL auto-creates sequences
1249
1249
  - **Frond template engine optimizations**: pre-compiled regexes, lazy loop context (copy-on-write), filter chain caching, path split caching, inline common filters (11-15% speedup)
1250
1250
  - **Production server auto-detect**: `npx tina4nodejs serve --production` auto-uses cluster mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.89",
3
+ "version": "3.13.90",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - 54 built-in features, zero dependencies",
6
6
  "keywords": [
@@ -38761,6 +38761,11 @@ function parseFields(fieldsStr) {
38761
38761
  }
38762
38762
  return result;
38763
38763
  }
38764
+ var DEFAULT_FIELDS = [["name", "string"]];
38765
+ function fieldsOrDefault(fieldsStr) {
38766
+ const parsed = parseFields(fieldsStr);
38767
+ return parsed.length > 0 ? parsed : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
38768
+ }
38764
38769
  function parseCliArgs(args) {
38765
38770
  const booleanFlags = /* @__PURE__ */ new Set([
38766
38771
  "no-browser",
@@ -38876,7 +38881,7 @@ async function generate2(what, name, extraArgs = []) {
38876
38881
  }
38877
38882
  }
38878
38883
  function generateModel(name, flags, emitTest = true) {
38879
- const fields = parseFields(flags.fields || "");
38884
+ const fields = fieldsOrDefault(flags.fields || "");
38880
38885
  const table2 = toTableName(name);
38881
38886
  const dir = resolve29("src/models");
38882
38887
  ensureDir(dir);
@@ -38884,13 +38889,9 @@ function generateModel(name, flags, emitTest = true) {
38884
38889
  const fieldLines = [
38885
38890
  ` id: { type: "integer" as const, primaryKey: true, autoIncrement: true },`
38886
38891
  ];
38887
- if (fields.length > 0) {
38888
- for (const [fname, ftype] of fields) {
38889
- const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
38890
- fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
38891
- }
38892
- } else {
38893
- fieldLines.push(` name: { type: "string" as const },`);
38892
+ for (const [fname, ftype] of fields) {
38893
+ const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
38894
+ fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
38894
38895
  }
38895
38896
  fieldLines.push(` created_at: { type: "datetime" as const },`);
38896
38897
  const content = `import { BaseModel } from "tina4-nodejs/orm";
@@ -38904,7 +38905,7 @@ ${fieldLines.join("\n")}
38904
38905
  `;
38905
38906
  writeFileSafe(path8, content);
38906
38907
  if (!flags["no-migration"]) {
38907
- generateMigration(`create_${table2}`, flags, fields.length > 0 ? fields : void 0, table2, false);
38908
+ generateMigration(`create_${table2}`, flags, fields, table2, false);
38908
38909
  }
38909
38910
  if (emitTest) emitModelTest(name, table2, fields);
38910
38911
  }
@@ -38975,7 +38976,12 @@ ${extend(
38975
38976
  "validate / business rules before persist",
38976
38977
  `e.g. reject invalid input; ground: tina4_context("validate before create", "nodejs")`
38977
38978
  )} const item = new ${model}(req.body as Record<string, unknown>);
38978
- await item.save();
38979
+ // save() returns false on failure rather than throwing - check it, or a failed
38980
+ // write is reported to the client as a 201 carrying unsaved data.
38981
+ if ((await item.save()) === false) {
38982
+ res.json({ error: "Could not create ${singular}" }, 400);
38983
+ return;
38984
+ }
38979
38985
  res.json({ data: item.toObject() }, 201);
38980
38986
  }
38981
38987
  `
@@ -39055,7 +39061,12 @@ ${extend(
39055
39061
  "guard which fields / who may update",
39056
39062
  `e.g. enforce ownership; ground: tina4_context("authorize update", "nodejs")`
39057
39063
  )} Object.assign(item, req.body as Record<string, unknown>);
39058
- await item.save();
39064
+ // save() returns false on failure rather than throwing - check it, or a failed
39065
+ // write is reported to the client as a 200 carrying unsaved data.
39066
+ if ((await item.save()) === false) {
39067
+ res.json({ error: "Could not update ${singular}" }, 400);
39068
+ return;
39069
+ }
39059
39070
  res.json({ data: item.toObject() });
39060
39071
  }
39061
39072
  `
@@ -39362,7 +39373,7 @@ void test${titleName};
39362
39373
  writeFileSafe(path8, content);
39363
39374
  }
39364
39375
  function generateForm(name, flags) {
39365
- const fields = parseFields(flags.fields || "");
39376
+ const fields = fieldsOrDefault(flags.fields || "");
39366
39377
  const table2 = toTableName(name);
39367
39378
  const routeName = toPlural(table2);
39368
39379
  const inputTypes = {
@@ -39382,9 +39393,8 @@ function generateForm(name, flags) {
39382
39393
  const dir = resolve29("src/templates/forms");
39383
39394
  ensureDir(dir);
39384
39395
  const path8 = join33(dir, `${table2}.twig`);
39385
- const fieldEntries = fields.length > 0 ? fields : [["name", "string"]];
39386
39396
  let fieldHtml = "";
39387
- for (const [fname, ftype] of fieldEntries) {
39397
+ for (const [fname, ftype] of fields) {
39388
39398
  const itype = inputTypes[ftype] || "text";
39389
39399
  const label = fname.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
39390
39400
  const step = ["float", "numeric", "decimal"].includes(ftype) ? ' step="0.01"' : "";
@@ -39428,10 +39438,10 @@ function generateForm(name, flags) {
39428
39438
  writeFileSafe(path8, content);
39429
39439
  }
39430
39440
  function generateView(name, flags) {
39431
- const fields = parseFields(flags.fields || "");
39441
+ const fields = fieldsOrDefault(flags.fields || "");
39432
39442
  const table2 = toTableName(name);
39433
39443
  const routeName = toPlural(table2);
39434
- const cols = fields.length > 0 ? fields.map(([f]) => f) : ["name"];
39444
+ const cols = fields.map(([f]) => f);
39435
39445
  const dir = resolve29("src/templates/pages");
39436
39446
  ensureDir(dir);
39437
39447
  const listPath = join33(dir, `${routeName}.twig`);
@@ -39946,7 +39956,7 @@ function sampleLiteral(fieldType) {
39946
39956
  }
39947
39957
  }
39948
39958
  function emitModelTest(model, table2, fields) {
39949
- const flds = fields.length > 0 ? fields : [["name", "string"]];
39959
+ const flds = fields.length > 0 ? fields : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
39950
39960
  const payload = flds.map(([f, t]) => `${f}: ${sampleLiteral(t)}`).join(", ");
39951
39961
  const stringField = flds.find(([, t]) => ["string", "str", "text"].includes((t || "string").toLowerCase()))?.[0];
39952
39962
  const valueAssert = stringField ? `
@@ -115,6 +115,21 @@ export function parseFields(fieldsStr: string): Array<[string, string]> {
115
115
  return result;
116
116
  }
117
117
 
118
+ // Called without --fields, the generators fall back to a single `name` string
119
+ // column. That default MUST be materialised here, in one place, and then flow
120
+ // into the model, the migration, the form, the view and the test alike. It used
121
+ // to live only inside the model template, so `generate model X` / `generate
122
+ // crud X` wrote a model declaring `name` while the migration - built from the
123
+ // parsed field list, which was empty - created only id + created_at. The first
124
+ // write then failed with "table x has no column named name".
125
+ export const DEFAULT_FIELDS: ReadonlyArray<[string, string]> = [["name", "string"]];
126
+
127
+ /** Parsed --fields, or the default single `name` column when none given. */
128
+ export function fieldsOrDefault(fieldsStr: string): Array<[string, string]> {
129
+ const parsed = parseFields(fieldsStr);
130
+ return parsed.length > 0 ? parsed : DEFAULT_FIELDS.map(([f, t]) => [f, t] as [string, string]);
131
+ }
132
+
118
133
  export function parseCliArgs(args: string[]): { flags: Record<string, string | boolean>; positional: string[] } {
119
134
  // Boolean-only flags that never take a value argument.
120
135
  const booleanFlags = new Set([
@@ -290,7 +305,7 @@ export async function generate(what: string, name: string, extraArgs: string[] =
290
305
  // ── Model ───────────────────────────────────────────────────────────
291
306
 
292
307
  function generateModel(name: string, flags: Record<string, string | boolean>, emitTest = true): void {
293
- const fields = parseFields((flags.fields as string) || "");
308
+ const fields = fieldsOrDefault((flags.fields as string) || "");
294
309
  const table = toTableName(name);
295
310
  const dir = resolve("src/models");
296
311
  ensureDir(dir);
@@ -300,13 +315,9 @@ function generateModel(name: string, flags: Record<string, string | boolean>, em
300
315
  const fieldLines: string[] = [
301
316
  ` id: { type: "integer" as const, primaryKey: true, autoIncrement: true },`,
302
317
  ];
303
- if (fields.length > 0) {
304
- for (const [fname, ftype] of fields) {
305
- const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
306
- fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
307
- }
308
- } else {
309
- fieldLines.push(` name: { type: "string" as const },`);
318
+ for (const [fname, ftype] of fields) {
319
+ const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
320
+ fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
310
321
  }
311
322
  fieldLines.push(` created_at: { type: "datetime" as const },`);
312
323
 
@@ -328,7 +339,11 @@ ${fieldLines.join("\n")}
328
339
  // (below) proves the schema through the real ORM, so the migration sub-call
329
340
  // does NOT also co-emit a migration test (emitTest=false).
330
341
  if (!flags["no-migration"]) {
331
- generateMigration(`create_${table}`, flags, fields.length > 0 ? fields : undefined, table, false);
342
+ // Always hand over the RESOLVED field list. Passing `undefined` when the
343
+ // parsed list was empty made the migration fall back to its own
344
+ // parseFields() - also empty - so the table got only id + created_at while
345
+ // the model above declared `name`, and the first write 500'd.
346
+ generateMigration(`create_${table}`, flags, fields, table, false);
332
347
  }
333
348
 
334
349
  // Co-emit a real SQLite roundtrip test next to the model. Composite
@@ -417,7 +432,12 @@ ${modelImportBase}${secureOptOut(isPublic)}export const meta = { summary: "Creat
417
432
  export default async function (req: Tina4Request, res: Tina4Response) {
418
433
  ${extend("validate / business rules before persist",
419
434
  `e.g. reject invalid input; ground: tina4_context("validate before create", "nodejs")`)} const item = new ${model}(req.body as Record<string, unknown>);
420
- await item.save();
435
+ // save() returns false on failure rather than throwing - check it, or a failed
436
+ // write is reported to the client as a 201 carrying unsaved data.
437
+ if ((await item.save()) === false) {
438
+ res.json({ error: "Could not create ${singular}" }, 400);
439
+ return;
440
+ }
421
441
  res.json({ data: item.toObject() }, 201);
422
442
  }
423
443
  `,
@@ -499,7 +519,12 @@ export default async function (req: Tina4Request, res: Tina4Response) {
499
519
  }
500
520
  ${extend("guard which fields / who may update",
501
521
  `e.g. enforce ownership; ground: tina4_context("authorize update", "nodejs")`)} Object.assign(item, req.body as Record<string, unknown>);
502
- await item.save();
522
+ // save() returns false on failure rather than throwing - check it, or a failed
523
+ // write is reported to the client as a 200 carrying unsaved data.
524
+ if ((await item.save()) === false) {
525
+ res.json({ error: "Could not update ${singular}" }, 400);
526
+ return;
527
+ }
503
528
  res.json({ data: item.toObject() });
504
529
  }
505
530
  `,
@@ -873,7 +898,7 @@ void test${titleName};
873
898
  // ── Form ────────────────────────────────────────────────────────────
874
899
 
875
900
  function generateForm(name: string, flags: Record<string, string | boolean>): void {
876
- const fields = parseFields((flags.fields as string) || "");
901
+ const fields = fieldsOrDefault((flags.fields as string) || "");
877
902
  const table = toTableName(name);
878
903
  const routeName = toPlural(table);
879
904
 
@@ -890,9 +915,8 @@ function generateForm(name: string, flags: Record<string, string | boolean>): vo
890
915
  const path = join(dir, `${table}.twig`);
891
916
 
892
917
  // Build form fields
893
- const fieldEntries = fields.length > 0 ? fields : [["name", "string"] as [string, string]];
894
918
  let fieldHtml = "";
895
- for (const [fname, ftype] of fieldEntries) {
919
+ for (const [fname, ftype] of fields) {
896
920
  const itype = inputTypes[ftype] || "text";
897
921
  const label = fname.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
898
922
  const step = ["float", "numeric", "decimal"].includes(ftype) ? ' step="0.01"' : "";
@@ -946,11 +970,11 @@ function generateForm(name: string, flags: Record<string, string | boolean>): vo
946
970
  // ── View ────────────────────────────────────────────────────────────
947
971
 
948
972
  function generateView(name: string, flags: Record<string, string | boolean>): void {
949
- const fields = parseFields((flags.fields as string) || "");
973
+ const fields = fieldsOrDefault((flags.fields as string) || "");
950
974
  const table = toTableName(name);
951
975
  const routeName = toPlural(table);
952
976
 
953
- const cols = fields.length > 0 ? fields.map(([f]) => f) : ["name"];
977
+ const cols = fields.map(([f]) => f);
954
978
 
955
979
  const dir = resolve("src/templates/pages");
956
980
  ensureDir(dir);
@@ -1591,7 +1615,9 @@ function sampleLiteral(fieldType: string): string {
1591
1615
 
1592
1616
  /** model → real SQLite roundtrip (create / read back / missing → null). */
1593
1617
  function emitModelTest(model: string, table: string, fields: Array<[string, string]>): void {
1594
- const flds = fields.length > 0 ? fields : [["name", "string"] as [string, string]];
1618
+ // Reuse the single DEFAULT_FIELDS constant rather than re-stating the literal,
1619
+ // so the co-emitted test can never describe a shape the model does not have.
1620
+ const flds = fields.length > 0 ? fields : DEFAULT_FIELDS.map(([f, t]) => [f, t] as [string, string]);
1595
1621
  const payload = flds.map(([f, t]) => `${f}: ${sampleLiteral(t)}`).join(", ");
1596
1622
  const stringField = flds.find(([, t]) => ["string", "str", "text"].includes((t || "string").toLowerCase()))?.[0];
1597
1623
  const valueAssert = stringField
@@ -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=`