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.
|
|
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.
|
|
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,
|
|
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
package/packages/cli/dist/bin.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
38888
|
-
|
|
38889
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
39441
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
39432
39442
|
const table2 = toTableName(name);
|
|
39433
39443
|
const routeName = toPlural(table2);
|
|
39434
|
-
const cols = fields.
|
|
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 : [[
|
|
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 =
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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=
|
|
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(
|
|
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)">● Configured</span> <span style="opacity:0.6">(…${g(e.last4||"")})</span>`:'<span style="color:var(--warn,#f9e2af)">○ Not set</span> — using local corpus fallback';i.innerHTML=`
|