yaml-flow 5.1.0 → 5.2.1

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.
Files changed (105) hide show
  1. package/{examples/example-board/reusable-server-runtime.js → board-livecards-server-runtime.js} +42 -20
  2. package/{examples/example-board/reusable-board-runtime-client.js → browser/board-livecards-runtime-client.js} +6 -2
  3. package/browser/{board-livegraph-runtime.js → board-livegraph-engine.js} +212 -16
  4. package/browser/board-livegraph-engine.js.map +1 -0
  5. package/browser/live-cards.js +362 -38
  6. package/browser/live-cards.schema.json +20 -4
  7. package/dist/board-livegraph-runtime/index.cjs +210 -14
  8. package/dist/board-livegraph-runtime/index.cjs.map +1 -1
  9. package/dist/board-livegraph-runtime/index.d.cts +49 -5
  10. package/dist/board-livegraph-runtime/index.d.ts +49 -5
  11. package/dist/board-livegraph-runtime/index.js +209 -15
  12. package/dist/board-livegraph-runtime/index.js.map +1 -1
  13. package/dist/card-compute/index.cjs +63 -7
  14. package/dist/card-compute/index.cjs.map +1 -1
  15. package/dist/card-compute/index.d.cts +2 -2
  16. package/dist/card-compute/index.d.ts +2 -2
  17. package/dist/card-compute/index.js +63 -7
  18. package/dist/card-compute/index.js.map +1 -1
  19. package/dist/cli/board-live-cards-cli.cjs +664 -75
  20. package/dist/cli/board-live-cards-cli.cjs.map +1 -1
  21. package/dist/cli/board-live-cards-cli.d.cts +33 -5
  22. package/dist/cli/board-live-cards-cli.d.ts +33 -5
  23. package/dist/cli/board-live-cards-cli.js +661 -76
  24. package/dist/cli/board-live-cards-cli.js.map +1 -1
  25. package/dist/{constants-ozjf1Ejw.d.cts → constants-BzZUyYlp.d.cts} +1 -1
  26. package/dist/{constants-DuzE5n03.d.ts → constants-oCEbNpul.d.ts} +1 -1
  27. package/dist/continuous-event-graph/index.cjs +47 -14
  28. package/dist/continuous-event-graph/index.cjs.map +1 -1
  29. package/dist/continuous-event-graph/index.d.cts +9 -9
  30. package/dist/continuous-event-graph/index.d.ts +9 -9
  31. package/dist/continuous-event-graph/index.js +47 -14
  32. package/dist/continuous-event-graph/index.js.map +1 -1
  33. package/dist/event-graph/index.cjs +29 -12
  34. package/dist/event-graph/index.cjs.map +1 -1
  35. package/dist/event-graph/index.d.cts +5 -5
  36. package/dist/event-graph/index.d.ts +5 -5
  37. package/dist/event-graph/index.js +29 -12
  38. package/dist/event-graph/index.js.map +1 -1
  39. package/dist/index.cjs +93 -20
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +7 -7
  42. package/dist/index.d.ts +7 -7
  43. package/dist/index.js +93 -20
  44. package/dist/index.js.map +1 -1
  45. package/dist/inference/index.cjs +29 -12
  46. package/dist/inference/index.cjs.map +1 -1
  47. package/dist/inference/index.d.cts +2 -2
  48. package/dist/inference/index.d.ts +2 -2
  49. package/dist/inference/index.js +29 -12
  50. package/dist/inference/index.js.map +1 -1
  51. package/dist/{journal-NLYuqege.d.ts → journal-9HEgs7dU.d.ts} +1 -1
  52. package/dist/{journal-DRfJiheM.d.cts → journal-B-JCfQnh.d.cts} +1 -1
  53. package/dist/{live-cards-bridge-Or7fdEJV.d.ts → live-cards-bridge-CeNxiVcm.d.ts} +6 -2
  54. package/dist/{live-cards-bridge-vGJ6tMzN.d.cts → live-cards-bridge-z_rJCSbi.d.cts} +6 -2
  55. package/dist/{schedule-CMcZe5Ny.d.ts → schedule-Cszq9LYY.d.ts} +1 -1
  56. package/dist/{schedule-CiucyCan.d.cts → schedule-qWNL0RQh.d.cts} +1 -1
  57. package/dist/{types-CMFSIjpc.d.cts → types-BBhqYGhE.d.cts} +4 -0
  58. package/dist/{types-CMFSIjpc.d.ts → types-BBhqYGhE.d.ts} +4 -0
  59. package/dist/{types-BzLD8bjb.d.cts → types-CHSdoAAA.d.cts} +1 -1
  60. package/dist/{types-C2eJ7DAV.d.ts → types-CoW0gQl3.d.ts} +1 -1
  61. package/dist/{validate-DJQTQ6bP.d.ts → validate-BAVzUJWa.d.ts} +1 -1
  62. package/dist/{validate-ke92Cleg.d.cts → validate-Dbu7ygys.d.cts} +1 -1
  63. package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +28 -0
  64. package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +28 -0
  65. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-inference-adapter.js +187 -0
  66. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +139 -5
  67. package/examples/example-board/agent-instructions-cardlayout.md +28 -0
  68. package/examples/example-board/agent-instructions.md +603 -0
  69. package/examples/example-board/cards/card-concentration.json +42 -0
  70. package/examples/example-board/cards/card-market-prices.json +51 -0
  71. package/examples/example-board/cards/card-portfolio-action.json +19 -0
  72. package/examples/example-board/cards/card-portfolio-risks.json +19 -0
  73. package/examples/example-board/cards/card-portfolio-value.json +62 -0
  74. package/examples/example-board/cards/card-portfolio.json +44 -0
  75. package/examples/example-board/demo-chat-handler.js +373 -33
  76. package/examples/example-board/demo-server-config.json +0 -2
  77. package/examples/example-board/demo-server.js +46 -7
  78. package/examples/example-board/demo-shell-browser.html +75 -207
  79. package/examples/example-board/demo-shell-with-server.html +15 -9
  80. package/examples/example-board/demo-shell.html +1 -1
  81. package/examples/example-board/demo-task-executor.js +259 -41
  82. package/package.json +6 -2
  83. package/schema/live-cards.schema.json +20 -4
  84. package/browser/board-livegraph-runtime.js.map +0 -1
  85. package/examples/example-board/board.yaml +0 -23
  86. package/examples/example-board/bootstrap_payload.json +0 -1
  87. package/examples/example-board/cards/card-chain-region-alert.json +0 -39
  88. package/examples/example-board/cards/card-chain-region-totals.json +0 -26
  89. package/examples/example-board/cards/card-chain-top-region.json +0 -24
  90. package/examples/example-board/cards/card-ex-actions.json +0 -32
  91. package/examples/example-board/cards/card-ex-chart.json +0 -30
  92. package/examples/example-board/cards/card-ex-filter.json +0 -36
  93. package/examples/example-board/cards/card-ex-filtered-by-preference.json +0 -59
  94. package/examples/example-board/cards/card-ex-form.json +0 -91
  95. package/examples/example-board/cards/card-ex-list.json +0 -22
  96. package/examples/example-board/cards/card-ex-markdown.json +0 -17
  97. package/examples/example-board/cards/card-ex-metric.json +0 -19
  98. package/examples/example-board/cards/card-ex-narrative.json +0 -36
  99. package/examples/example-board/cards/card-ex-source-http.json +0 -28
  100. package/examples/example-board/cards/card-ex-source.json +0 -21
  101. package/examples/example-board/cards/card-ex-status.json +0 -35
  102. package/examples/example-board/cards/card-ex-table.json +0 -30
  103. package/examples/example-board/cards/card-ex-todo.json +0 -29
  104. package/examples/example-board/mock.db +0 -15
  105. package/examples/example-board/reusable-runtime-artifacts-adapter.js +0 -233
@@ -197,8 +197,12 @@ var LiveCard = (function () {
197
197
  getChatMessages: config.getChatMessages || null,
198
198
  };
199
199
 
200
- const _cleanup = {}; // nodeId → { ac, timers, charts, unsubs }
201
- const _subs = {}; // nodeId → Set<callback>
200
+ const _cleanup = {}; // nodeId → { ac, timers, charts, unsubs }
201
+ const _subs = {}; // nodeId → Set<callback>
202
+ const _etState = {}; // stateKey → { currentState, pending } for editable-table dirty tracking
203
+ const _formState = {}; // stateKey → { currentState, pending } for form dirty tracking
204
+ const _notesState = {}; // stateKey → { currentState, pending } for notes dirty tracking
205
+ const _todoState = {}; // stateKey → { currentState, pending } for todo dirty tracking
202
206
  const _renderers = {}; // kind → fn
203
207
  const _nodeEls = {}; // nodeId → { container, resultEl, uid }
204
208
  const _chatModal = {
@@ -949,7 +953,48 @@ var LiveCard = (function () {
949
953
  const schema = ed.fields || {};
950
954
  const props = schema.properties || {};
951
955
  const required = schema.required || [];
952
- const values = writeTo ? (_resolveBind(node, writeTo) || {}) : (data && typeof data === 'object' ? data : {});
956
+
957
+ // --- Journal-style dirty tracking (mirrors editable-table) ---
958
+ // currentState = what the server last sent (updated each SSE re-render)
959
+ // pending = user's local field values accumulated on top of currentState
960
+ // dirty = JSON.stringify(currentState) !== JSON.stringify(pending)
961
+ //
962
+ // On SSE re-render:
963
+ // - currentState updated to new server values
964
+ // - if NOT dirty: pending follows currentState (no unsaved edits)
965
+ // - if dirty: pending untouched, form is rebuilt from pending (edits survive)
966
+ const stateKey = node.id + ':' + (writeTo || '');
967
+ const incomingValues = writeTo ? (_resolveBind(node, writeTo) || {}) : (data && typeof data === 'object' ? data : {});
968
+ const incomingCopy = Object.assign({}, incomingValues);
969
+
970
+ if (!_formState[stateKey]) {
971
+ _formState[stateKey] = { currentState: incomingCopy, pending: Object.assign({}, incomingCopy) };
972
+ } else {
973
+ const s = _formState[stateKey];
974
+ const wasDirty = JSON.stringify(s.currentState) !== JSON.stringify(s.pending);
975
+ s.currentState = incomingCopy;
976
+ if (!wasDirty) {
977
+ s.pending = Object.assign({}, incomingCopy);
978
+ }
979
+ // if dirty, pending stays so user's in-progress edits survive the SSE tick
980
+ }
981
+
982
+ const st = _formState[stateKey];
983
+
984
+ function isDirty() {
985
+ return JSON.stringify(st.currentState) !== JSON.stringify(st.pending);
986
+ }
987
+
988
+ // Snapshot current input values into pending (called on each input event)
989
+ function capturePending(form) {
990
+ form.querySelectorAll('[data-key]').forEach(inp => {
991
+ const k = inp.dataset.key, p = props[k];
992
+ if (!p) return;
993
+ if (p.type === 'boolean') st.pending[k] = inp.checked;
994
+ else if (p.type === 'number' || p.type === 'integer') st.pending[k] = inp.value !== '' ? parseFloat(inp.value) : 0;
995
+ else st.pending[k] = inp.value;
996
+ });
997
+ }
953
998
 
954
999
  const form = document.createElement('form');
955
1000
  form.className = 'row g-2';
@@ -999,10 +1044,12 @@ var LiveCard = (function () {
999
1044
 
1000
1045
  input.dataset.key = key;
1001
1046
  if (isReq) input.required = true;
1002
- if (values[key] != null) {
1003
- if (prop.type === 'boolean') input.checked = !!values[key];
1004
- else if (prop.format === 'date') input.value = String(values[key]).slice(0, 10);
1005
- else input.value = values[key];
1047
+ // Populate from pending (not from server values directly)
1048
+ const v = st.pending[key];
1049
+ if (v != null) {
1050
+ if (prop.type === 'boolean') input.checked = !!v;
1051
+ else if (prop.format === 'date') input.value = String(v).slice(0, 10);
1052
+ else input.value = v;
1006
1053
  }
1007
1054
  form.appendChild(col);
1008
1055
  });
@@ -1010,26 +1057,31 @@ var LiveCard = (function () {
1010
1057
  const btnCol = document.createElement('div');
1011
1058
  btnCol.className = 'col-12 mt-1';
1012
1059
  const btn = document.createElement('button');
1013
- btn.type = 'submit'; btn.className = 'btn btn-sm btn-primary'; btn.textContent = 'Submit';
1060
+ btn.type = 'submit';
1061
+ btn.className = 'btn btn-sm btn-primary' + (isDirty() ? '' : ' d-none');
1062
+ btn.textContent = 'Submit';
1014
1063
  btnCol.appendChild(btn);
1015
1064
  form.appendChild(btnCol);
1016
1065
 
1017
1066
  el.innerHTML = '';
1018
1067
  el.appendChild(form);
1019
1068
 
1069
+ // Real-time input → update pending + show Submit if now dirty
1070
+ form.addEventListener('input', () => {
1071
+ capturePending(form);
1072
+ if (isDirty()) btn.classList.remove('d-none');
1073
+ }, { signal });
1074
+
1020
1075
  form.addEventListener('submit', e => {
1021
1076
  e.preventDefault();
1022
1077
  if (!form.checkValidity()) { form.classList.add('was-validated'); return; }
1023
- const vals = {};
1024
- form.querySelectorAll('[data-key]').forEach(inp => {
1025
- const k = inp.dataset.key, p = props[k];
1026
- if (p.type === 'boolean') vals[k] = inp.checked;
1027
- else if (p.type === 'number' || p.type === 'integer') vals[k] = inp.value ? parseFloat(inp.value) : 0;
1028
- else vals[k] = inp.value;
1029
- });
1030
- if (writeTo) _deepSet(node, writeTo, vals);
1031
- cfg.onPatchState(node.id, { fieldValues: vals });
1032
- notify(node.id, vals);
1078
+ capturePending(form);
1079
+ if (writeTo) _deepSet(node, writeTo, st.pending);
1080
+ cfg.onPatchState(node.id, { fieldValues: st.pending });
1081
+ notify(node.id, st.pending);
1082
+ // After save, currentState = pending (no longer dirty)
1083
+ st.currentState = Object.assign({}, st.pending);
1084
+ btn.classList.add('d-none');
1033
1085
  btn.textContent = '✓ Saved';
1034
1086
  setTimeout(() => { btn.textContent = 'Submit'; }, 1500);
1035
1087
  }, { signal });
@@ -1042,14 +1094,29 @@ var LiveCard = (function () {
1042
1094
  const signal = cleanup.ac.signal;
1043
1095
  const ed = elemDef.data || {};
1044
1096
  const writeTo = ed.writeTo;
1045
- const content = typeof data === 'string' ? data : '';
1097
+ const incomingContent = typeof data === 'string' ? data : '';
1098
+
1099
+ // --- Journal-style dirty tracking ---
1100
+ // currentState = last server value; pending = textarea content
1101
+ // On SSE re-render: if dirty, leave textarea alone; if clean, sync from server
1102
+ const stateKey = node.id + ':' + (writeTo || '');
1103
+ if (!_notesState[stateKey]) {
1104
+ _notesState[stateKey] = { currentState: incomingContent, pending: incomingContent };
1105
+ } else {
1106
+ const s = _notesState[stateKey];
1107
+ const wasDirty = s.currentState !== s.pending;
1108
+ s.currentState = incomingContent;
1109
+ if (!wasDirty) s.pending = incomingContent;
1110
+ // if dirty, pending stays so user's typing survives the SSE tick
1111
+ }
1112
+ const st = _notesState[stateKey];
1046
1113
 
1047
1114
  el.innerHTML = `
1048
1115
  <div class="btn-group btn-group-sm mb-2" role="group">
1049
1116
  <button class="btn btn-outline-secondary active lc-n-edit" type="button">Edit</button>
1050
1117
  <button class="btn btn-outline-secondary lc-n-preview" type="button">Preview</button>
1051
1118
  </div>
1052
- <textarea class="form-control form-control-sm lc-notes-textarea" rows="8" placeholder="Write markdown...">${_esc(content)}</textarea>
1119
+ <textarea class="form-control form-control-sm lc-notes-textarea" rows="8" placeholder="Write markdown...">${_esc(st.pending)}</textarea>
1053
1120
  <div class="lc-notes-preview d-none border rounded p-2 small"></div>`;
1054
1121
 
1055
1122
  const textarea = el.querySelector('.lc-notes-textarea');
@@ -1069,10 +1136,12 @@ var LiveCard = (function () {
1069
1136
 
1070
1137
  let timer;
1071
1138
  textarea.addEventListener('input', () => {
1139
+ st.pending = textarea.value; // track in journal
1072
1140
  clearTimeout(timer);
1073
1141
  timer = setTimeout(() => {
1074
1142
  if (writeTo) _deepSet(node, writeTo, textarea.value);
1075
1143
  cfg.onPatchState(node.id, { notes: textarea.value });
1144
+ st.currentState = textarea.value; // saved — no longer dirty
1076
1145
  }, 800);
1077
1146
  cleanup.timers.push(timer);
1078
1147
  }, { signal });
@@ -1080,19 +1149,210 @@ var LiveCard = (function () {
1080
1149
 
1081
1150
  // ---- todo ----
1082
1151
 
1152
+ // ---- editable-table ----
1153
+ // Renders an array bound via `data.bind` as an inline-editable table.
1154
+ // Each row is editable in-place; changes are saved on blur (change event).
1155
+ // `data.writeTo` persists changes back to card_data (same pattern as form).
1156
+ // `data.columns` restricts which columns appear (and in what order).
1157
+ // `data.schema.properties[col].type` ("number"/"integer") controls input type.
1158
+ // `data.addRow` (default true) shows "+ Add row" button.
1159
+ // `data.deleteRow` (default true) shows per-row delete button.
1160
+ function _renderEditableTable(data, el, elemDef, node) {
1161
+ const cleanup = _getCleanup(node.id);
1162
+ const signal = cleanup.ac.signal;
1163
+ const ed = elemDef.data || {};
1164
+ const writeTo = ed.writeTo;
1165
+ const schemaProps = (ed.schema && ed.schema.properties) || {};
1166
+ const canAdd = ed.addRow !== false;
1167
+ const canDelete = ed.deleteRow !== false;
1168
+
1169
+ // Derive columns from rows if not specified
1170
+ function getCols(rows) {
1171
+ if (ed.columns && ed.columns.length) return ed.columns;
1172
+ const s = new Set();
1173
+ rows.forEach(r => { if (r && typeof r === 'object') Object.keys(r).forEach(k => s.add(k)); });
1174
+ return [...s];
1175
+ }
1176
+
1177
+ // --- Journal-style dirty tracking ---
1178
+ // currentState = what the server last sent (updated each SSE re-render)
1179
+ // pending = user's local accumulation of edits on top of currentState
1180
+ // dirty = JSON.stringify(currentState) !== JSON.stringify(pending)
1181
+ //
1182
+ // When SSE fires and re-renders this element:
1183
+ // - currentState is updated to the new server value
1184
+ // - if NOT dirty: pending follows currentState (no unsaved edits)
1185
+ // - if dirty: pending is left untouched (user's edits survive the SSE tick)
1186
+ const stateKey = node.id + ':' + (ed.bind || ed.writeTo || '');
1187
+ const incomingRows = Array.isArray(writeTo ? _resolveBind(node, writeTo) : data)
1188
+ ? (writeTo ? _resolveBind(node, writeTo) : data)
1189
+ : [];
1190
+ const incomingCopy = incomingRows.map(r => Object.assign({}, r));
1191
+
1192
+ if (!_etState[stateKey]) {
1193
+ // First render — initialise both sides from server data
1194
+ _etState[stateKey] = { currentState: incomingCopy, pending: incomingCopy.map(r => Object.assign({}, r)) };
1195
+ } else {
1196
+ const s = _etState[stateKey];
1197
+ const wasDirty = JSON.stringify(s.currentState) !== JSON.stringify(s.pending);
1198
+ s.currentState = incomingCopy;
1199
+ if (!wasDirty) {
1200
+ // No unsaved edits — sync pending to new server state
1201
+ s.pending = incomingCopy.map(r => Object.assign({}, r));
1202
+ }
1203
+ // If dirty, pending stays as-is so user's edits survive the SSE tick
1204
+ }
1205
+
1206
+ const st = _etState[stateKey];
1207
+
1208
+ function isDirty() {
1209
+ return JSON.stringify(st.currentState) !== JSON.stringify(st.pending);
1210
+ }
1211
+
1212
+ function markDirty() {
1213
+ const saveBtn = el.querySelector('.lc-et-save');
1214
+ if (saveBtn) saveBtn.classList.remove('d-none');
1215
+ }
1216
+
1217
+ function commitSave() {
1218
+ if (writeTo) _deepSet(node, writeTo, st.pending);
1219
+ cfg.onPatchState(node.id, { fieldValues: st.pending });
1220
+ notify(node.id, st.pending);
1221
+ // After save, currentState = pending (no longer dirty)
1222
+ st.currentState = st.pending.map(r => Object.assign({}, r));
1223
+ build();
1224
+ }
1225
+
1226
+ function build() {
1227
+ const rows = st.pending;
1228
+ const cols = getCols(rows);
1229
+
1230
+ if (!cols.length && !canAdd) {
1231
+ el.innerHTML = `<p class="text-muted small">${_esc(ed.placeholder || 'No data')}</p>`;
1232
+ return;
1233
+ }
1234
+
1235
+ let h = '<div class="table-responsive"><table class="table table-sm table-bordered mb-0 lc-editable-table"><thead><tr>';
1236
+ cols.forEach(c => { h += `<th class="small text-nowrap">${_esc(c)}</th>`; });
1237
+ if (canDelete) h += '<th style="width:2rem"></th>';
1238
+ h += '</tr></thead><tbody>';
1239
+
1240
+ rows.forEach((row, rowIdx) => {
1241
+ h += `<tr>`;
1242
+ cols.forEach(c => {
1243
+ const v = row[c];
1244
+ const prop = schemaProps[c] || {};
1245
+ const isNum = prop.type === 'number' || prop.type === 'integer' || (v != null && typeof v === 'number');
1246
+ const displayVal = v != null ? String(v) : '';
1247
+ h += `<td class="p-0">` +
1248
+ `<input type="${isNum ? 'number' : 'text'}" ` +
1249
+ `class="form-control form-control-sm border-0 rounded-0 lc-et-cell" ` +
1250
+ `data-row="${rowIdx}" data-col="${_esc(c)}" value="${_esc(displayVal)}"` +
1251
+ `${isNum ? ' step="any"' : ''}>` +
1252
+ `</td>`;
1253
+ });
1254
+ if (canDelete) {
1255
+ h += `<td class="text-center align-middle p-0">` +
1256
+ `<button class="btn btn-sm btn-link text-danger p-0 lc-et-del" data-row="${rowIdx}" title="Remove row">✕</button>` +
1257
+ `</td>`;
1258
+ }
1259
+ h += '</tr>';
1260
+ });
1261
+
1262
+ if (!rows.length) {
1263
+ const span = cols.length + (canDelete ? 1 : 0);
1264
+ h += `<tr><td colspan="${span}" class="text-muted small text-center">${_esc(ed.placeholder || 'No rows')}</td></tr>`;
1265
+ }
1266
+
1267
+ h += '</tbody></table></div>';
1268
+ let footer = '';
1269
+ if (canAdd) footer += '<button class="btn btn-sm btn-outline-secondary mt-1 me-1 lc-et-add">+ Add row</button>';
1270
+ footer += `<button class="btn btn-sm btn-primary mt-1 lc-et-save${isDirty() ? '' : ' d-none'}">Save</button>`;
1271
+ el.innerHTML = h + footer;
1272
+
1273
+ // Cell edit → update pending on blur, show Save if now dirty
1274
+ el.querySelectorAll('.lc-et-cell').forEach(inp => {
1275
+ inp.addEventListener('change', () => {
1276
+ const rowIdx = parseInt(inp.dataset.row);
1277
+ const colName = inp.dataset.col;
1278
+ const prop = schemaProps[colName] || {};
1279
+ const isNum = prop.type === 'number' || prop.type === 'integer' || inp.type === 'number';
1280
+ if (!st.pending[rowIdx]) return;
1281
+ st.pending[rowIdx] = Object.assign({}, st.pending[rowIdx]);
1282
+ st.pending[rowIdx][colName] = isNum ? (inp.value !== '' ? parseFloat(inp.value) : 0) : inp.value;
1283
+ if (isDirty()) markDirty();
1284
+ }, { signal });
1285
+ });
1286
+
1287
+ // Delete row — updates pending, rebuilds (Save button shown if dirty)
1288
+ el.querySelectorAll('.lc-et-del').forEach(btn => {
1289
+ btn.addEventListener('click', () => {
1290
+ const rowIdx = parseInt(btn.dataset.row);
1291
+ st.pending = st.pending.filter((_, i) => i !== rowIdx);
1292
+ build();
1293
+ }, { signal });
1294
+ });
1295
+
1296
+ // Add row — appends blank row to pending, rebuilds (Save button shown if dirty)
1297
+ const addBtn = el.querySelector('.lc-et-add');
1298
+ if (addBtn) {
1299
+ addBtn.addEventListener('click', () => {
1300
+ const newRow = {};
1301
+ getCols(st.pending).forEach(c => { newRow[c] = ''; });
1302
+ st.pending = [...st.pending, newRow];
1303
+ build();
1304
+ }, { signal });
1305
+ }
1306
+
1307
+ // Save button — persist pending to server via fieldValues (same as form submit)
1308
+ const saveBtn = el.querySelector('.lc-et-save');
1309
+ if (saveBtn) {
1310
+ saveBtn.addEventListener('click', () => {
1311
+ commitSave();
1312
+ saveBtn.textContent = '✓ Saved';
1313
+ setTimeout(() => { saveBtn.textContent = 'Save'; }, 1500);
1314
+ }, { signal });
1315
+ }
1316
+ }
1317
+
1318
+ build();
1319
+ }
1320
+
1321
+ // ---- todo ----
1322
+
1083
1323
  function _renderTodo(data, el, elemDef, node) {
1084
1324
  const cleanup = _getCleanup(node.id);
1085
1325
  const signal = cleanup.ac.signal;
1086
1326
  const ed = elemDef.data || {};
1087
1327
  const writeTo = ed.writeTo;
1088
- const items = Array.isArray(data) ? data : [];
1328
+
1329
+ // --- Journal-style dirty tracking ---
1330
+ // currentState = last confirmed server state; pending = local working copy
1331
+ // On SSE re-render: if dirty (action in-flight), keep pending; if clean, sync from server
1332
+ const stateKey = node.id + ':' + (writeTo || '');
1333
+ const incomingItems = Array.isArray(data) ? data.map(r => Object.assign({}, r)) : [];
1334
+
1335
+ if (!_todoState[stateKey]) {
1336
+ _todoState[stateKey] = { currentState: incomingItems, pending: incomingItems.map(r => Object.assign({}, r)) };
1337
+ } else {
1338
+ const s = _todoState[stateKey];
1339
+ const wasDirty = JSON.stringify(s.currentState) !== JSON.stringify(s.pending);
1340
+ s.currentState = incomingItems;
1341
+ if (!wasDirty) s.pending = incomingItems.map(r => Object.assign({}, r));
1342
+ // if dirty, pending stays so in-flight changes survive the SSE tick
1343
+ }
1344
+ const st = _todoState[stateKey];
1089
1345
 
1090
1346
  function save() {
1091
- if (writeTo) _deepSet(node, writeTo, items);
1092
- cfg.onPatchState(node.id, { items });
1347
+ if (writeTo) _deepSet(node, writeTo, st.pending);
1348
+ cfg.onPatchState(node.id, { fieldValues: st.pending });
1349
+ notify(node.id, st.pending);
1350
+ // mark clean after save so next SSE sync resumes normally
1351
+ st.currentState = st.pending.map(r => Object.assign({}, r));
1093
1352
  }
1094
1353
 
1095
1354
  function build() {
1355
+ const items = st.pending;
1096
1356
  let h = '<div class="lc-todo-list">';
1097
1357
  items.forEach((item, i) => {
1098
1358
  const chk = item.done ? ' checked' : '';
@@ -1108,14 +1368,25 @@ var LiveCard = (function () {
1108
1368
  el.innerHTML = h;
1109
1369
 
1110
1370
  el.querySelectorAll('input[data-idx]').forEach(cb => {
1111
- cb.addEventListener('change', () => { items[parseInt(cb.dataset.idx)].done = cb.checked; save(); build(); }, { signal });
1371
+ cb.addEventListener('change', () => {
1372
+ st.pending[parseInt(cb.dataset.idx)].done = cb.checked;
1373
+ save(); build();
1374
+ }, { signal });
1112
1375
  });
1113
1376
  el.querySelectorAll('[data-rm]').forEach(btn => {
1114
- btn.addEventListener('click', () => { items.splice(parseInt(btn.dataset.rm), 1); save(); build(); }, { signal });
1377
+ btn.addEventListener('click', () => {
1378
+ st.pending.splice(parseInt(btn.dataset.rm), 1);
1379
+ save(); build();
1380
+ }, { signal });
1115
1381
  });
1116
1382
  const addInput = el.querySelector('.input-group input');
1117
1383
  const addBtn = el.querySelector('.lc-todo-add');
1118
- const addItem = () => { const t = addInput.value.trim(); if (!t) return; items.push({ text: t, done: false }); save(); build(); };
1384
+ const addItem = () => {
1385
+ const t = addInput.value.trim();
1386
+ if (!t) return;
1387
+ st.pending.push({ text: t, done: false });
1388
+ save(); build();
1389
+ };
1119
1390
  addBtn.addEventListener('click', addItem, { signal });
1120
1391
  addInput.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); addItem(); } }, { signal });
1121
1392
  }
@@ -1171,7 +1442,10 @@ var LiveCard = (function () {
1171
1442
  function _renderText(data, el, elemDef) {
1172
1443
  const ed = elemDef.data || {};
1173
1444
  const format = ed.format || 'default';
1174
- const style = ed.style || 'default';
1445
+ const style = elemDef.style || ed.style || 'default';
1446
+ const hideIfEmpty = ed.hideIfEmpty || elemDef.hideIfEmpty;
1447
+
1448
+ if (hideIfEmpty && (data == null || data === '')) { el.innerHTML = ''; return; }
1175
1449
 
1176
1450
  // Handle file-links format
1177
1451
  if (format === 'file-links') {
@@ -1195,7 +1469,10 @@ var LiveCard = (function () {
1195
1469
 
1196
1470
  // Default text rendering
1197
1471
  const tag = style === 'heading' ? 'h4' : 'div';
1198
- const cls = style === 'muted' ? 'text-muted small' : (style === 'heading' ? 'fw-bold' : 'small');
1472
+ const cls = style === 'muted' ? 'text-muted small'
1473
+ : style === 'muted-italic' ? 'text-muted small fst-italic'
1474
+ : style === 'heading' ? 'fw-bold'
1475
+ : 'small';
1199
1476
  el.innerHTML = `<${tag} class="${cls}">${_esc(data != null ? String(data) : '')}</${tag}>`;
1200
1477
  }
1201
1478
 
@@ -1527,8 +1804,9 @@ var LiveCard = (function () {
1527
1804
 
1528
1805
  // ---- Register built-in renderers ----
1529
1806
 
1530
- _renderers.table = _renderTable;
1531
- _renderers.filter = _renderFilter;
1807
+ _renderers.table = _renderTable;
1808
+ _renderers['editable-table'] = _renderEditableTable;
1809
+ _renderers.filter = _renderFilter;
1532
1810
  _renderers.metric = _renderMetric;
1533
1811
  _renderers.list = _renderList;
1534
1812
  _renderers.chart = _renderChart;
@@ -1558,6 +1836,26 @@ var LiveCard = (function () {
1558
1836
  const container = document.createElement('div');
1559
1837
  container.className = 'row g-2';
1560
1838
 
1839
+ const _taskStatus = node.runtime_state && node.runtime_state.task_status;
1840
+ if (_taskStatus && _taskStatus !== 'completed') {
1841
+ const statusEl = document.createElement('div');
1842
+ statusEl.className = 'col-12 d-flex align-items-center gap-2 mb-1';
1843
+ var _statusIconHtml;
1844
+ if (_taskStatus === 'running') {
1845
+ _statusIconHtml = '<span class="spinner-border spinner-border-sm text-muted" style="width:.75rem;height:.75rem;flex-shrink:0"></span>';
1846
+ } else if (_taskStatus === 'failed') {
1847
+ _statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0;color:#dc3545">&#x26A0;&#xFE0E;</span>'; // ⚠ (text variant)
1848
+ } else if (_taskStatus === 'not-started') {
1849
+ _statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">&#x25CB;</span>'; // ○
1850
+ } else if (_taskStatus === 'inactivated') {
1851
+ _statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">&#x2296;</span>'; // ⊖
1852
+ } else {
1853
+ _statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">&#x2013;</span>'; // –
1854
+ }
1855
+ statusEl.innerHTML = _statusIconHtml + '<span class="text-muted" style="font-size:.75rem">' + _esc(_taskStatus) + '</span>';
1856
+ container.appendChild(statusEl);
1857
+ }
1858
+
1561
1859
  view.elements.forEach(elemDef => {
1562
1860
  // Visibility gate
1563
1861
  if (elemDef.visible) {
@@ -1631,18 +1929,35 @@ var LiveCard = (function () {
1631
1929
  if (filesCount > 0) h += `<span class="ms-1 small" aria-label="${filesCount} files">${filesCount}</span>`;
1632
1930
  h += '</button>';
1633
1931
  // Chat icon button (speech bubble)
1634
- h += `<button class="btn btn-sm btn-outline-secondary" id="${uid}-chat-open" title="Chat">`;
1932
+ h += `<button class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center" id="${uid}-chat-open" title="Chat">`;
1635
1933
  h += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>';
1636
1934
  h += '</button>';
1637
1935
  // Refresh icon button
1638
1936
  if (showRefresh) {
1639
- h += `<button class="btn btn-sm btn-outline-secondary" id="${uid}-refresh" title="Refresh">`;
1937
+ h += `<button class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center" id="${uid}-refresh" title="Refresh">`;
1640
1938
  h += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>';
1641
1939
  h += '</button>';
1642
1940
  }
1643
1941
  h += '</div>';
1644
1942
  h += '</div>';
1645
1943
 
1944
+ // Inference status bar: completion criteria + task-completed tick
1945
+ const inferenceData = node.card_data && node.card_data.llm_task_completion_inference;
1946
+ const isTaskCompleted = !!(inferenceData && inferenceData.isTaskCompleted);
1947
+ const whenIs = node.card && typeof node.card.when_is_task_completed === 'string' && node.card.when_is_task_completed.trim();
1948
+ if (whenIs || isTaskCompleted) {
1949
+ h += `<div class="d-flex align-items-start gap-2 mb-2 px-1 py-1 rounded lc-inference-bar" style="background:rgba(0,0,0,.03)">`;
1950
+ if (isTaskCompleted) {
1951
+ h += `<span class="lc-inference-icon" title="Task completed" style="color:#198754;font-size:.75rem;line-height:1.2;flex-shrink:0">&#x25CF;</span>`;
1952
+ } else {
1953
+ h += `<span class="lc-inference-icon" style="color:#aaa;font-size:.75rem;line-height:1.4;flex-shrink:0" title="Awaiting inference">&#x25CB;</span>`;
1954
+ }
1955
+ if (whenIs) {
1956
+ h += `<span class="text-muted" style="font-size:.72rem;line-height:1.4;font-style:italic"><span style="opacity:.55;font-style:normal">done when:</span> ${_esc(whenIs)}</span>`;
1957
+ }
1958
+ h += `</div>`;
1959
+ }
1960
+
1646
1961
  // Elements area
1647
1962
  h += `<div class="lc-result" id="${uid}-result"></div>`;
1648
1963
 
@@ -1659,9 +1974,7 @@ var LiveCard = (function () {
1659
1974
  const resultEl = document.getElementById(uid + '-result');
1660
1975
  _nodeEls[node.id] = { container: containerEl, resultEl, uid };
1661
1976
 
1662
- if (node.card_data && node.card_data.status === 'loading') {
1663
- resultEl.innerHTML = '<div class="d-flex align-items-center gap-2"><span class="spinner-border spinner-border-sm text-muted"></span><span class="text-muted small">Loading…</span></div>';
1664
- } else if (node.card_data && node.card_data.status === 'error' && node.card_data.error) {
1977
+ if (node.card_data && node.card_data.status === 'error' && node.card_data.error) {
1665
1978
  resultEl.innerHTML = `<div class="text-danger small fw-semibold">Refresh failed</div><pre class="text-muted small mt-1" style="white-space:pre-wrap">${_esc(node.card_data.error)}</pre>`;
1666
1979
  } else {
1667
1980
  _runCompute(node).then(function () { _renderElements(node, resultEl); });
@@ -1758,9 +2071,20 @@ var LiveCard = (function () {
1758
2071
  const filesCountEl = document.getElementById(info.uid + '-files-count');
1759
2072
  if (filesCountEl && filesCountEl.parentNode) filesCountEl.parentNode.removeChild(filesCountEl);
1760
2073
 
1761
- if (node.card_data.status === 'loading') {
1762
- info.resultEl.innerHTML = '<div class="d-flex align-items-center gap-2"><span class="spinner-border spinner-border-sm text-muted"></span><span class="text-muted small">Loading…</span></div>';
1763
- } else if (node.card_data.status === 'error' && node.card_data.error) {
2074
+ // Update inference status bar (tick / hourglass) if card_data changed
2075
+ const infBar = info.container.querySelector('.lc-inference-bar');
2076
+ if (infBar) {
2077
+ const infData = node.card_data && node.card_data.llm_task_completion_inference;
2078
+ const done = !!(infData && infData.isTaskCompleted);
2079
+ const iconEl = infBar.querySelector('.lc-inference-icon');
2080
+ if (iconEl) {
2081
+ iconEl.title = done ? 'Task completed' : 'Awaiting inference';
2082
+ iconEl.style.color = done ? '#198754' : '#aaa';
2083
+ iconEl.innerHTML = done ? '&#x25CF;' : '&#x25CB;';
2084
+ }
2085
+ }
2086
+
2087
+ if (node.card_data.status === 'error' && node.card_data.error) {
1764
2088
  info.resultEl.innerHTML = `<div class="text-danger small fw-semibold">Refresh failed</div><pre class="text-muted small mt-1" style="white-space:pre-wrap">${_esc(node.card_data.error)}</pre>`;
1765
2089
  } else {
1766
2090
  _runCompute(node).then(function () { _renderElements(node, info.resultEl); });
@@ -100,9 +100,9 @@
100
100
  },
101
101
 
102
102
  "source_def": {
103
- "description": "One source entry. The engine only cares about 'bindTo' (compute namespace key) and 'outputFile' (delivery signal). Every other property is yours — add whatever your task-executor needs: kind, url, headers, mailbox, channel, model, query, etc. The full object is passed verbatim as the --in JSON to the executor.",
103
+ "description": "One source entry. The engine requires 'bindTo' (compute namespace key) and 'outputFile' (delivery signal path). bindTo and outputFile must be unique across all sources in a card. Every other property is yours — add whatever your task-executor needs: kind, url, headers, mailbox, channel, model, query, etc. The full object is passed verbatim as the --in JSON to the executor.",
104
104
  "type": "object",
105
- "required": ["bindTo"],
105
+ "required": ["bindTo", "outputFile"],
106
106
  "additionalProperties": true,
107
107
  "properties": {
108
108
  "bindTo": { "type": "string", "description": "Key under fetched_sources.* available in compute expressions" },
@@ -119,7 +119,7 @@
119
119
  "properties": {
120
120
  "id": { "type": "string", "description": "Optional element ID for targeted updates" },
121
121
  "kind": {
122
- "enum": ["metric", "table", "chart", "form", "filter", "list",
122
+ "enum": ["metric", "table", "editable-table", "chart", "form", "filter", "list",
123
123
  "notes", "todo", "alert", "narrative", "badge", "text",
124
124
  "markdown", "custom", "actions"]
125
125
  },
@@ -227,7 +227,7 @@
227
227
  "view": { "$ref": "#/definitions/view" },
228
228
  "card_data": {
229
229
  "type": "object",
230
- "description": "Authored card data supplied in the card definition",
230
+ "description": "Authored card data and runtime metadata. Includes uploaded-file metadata maintained by host handlers and inference evaluation results.",
231
231
  "properties": {
232
232
  "files": {
233
233
  "type": "array",
@@ -255,6 +255,18 @@
255
255
  },
256
256
  "additionalProperties": false
257
257
  }
258
+ },
259
+ "llm_task_completion_inference": {
260
+ "type": "object",
261
+ "description": "Runtime state written by the inference adapter (advanced/undocumented). Prefer the standard sources → compute → provides pattern for LLM-based signals.",
262
+ "properties": {
263
+ "inferenceRequested": { "type": "string", "format": "date-time", "description": "Timestamp when the latest inference request was initiated" },
264
+ "inferenceCompletedAt": { "type": "string", "format": "date-time", "description": "Timestamp when the latest inference request completed" },
265
+ "isTaskCompleted": { "type": "boolean", "description": "Whether the task is considered complete by the adapter" },
266
+ "reasoning": { "type": "string", "description": "Explanation of completion decision" },
267
+ "evidence": { "type": "array", "description": "Supporting evidence from sources/compute" }
268
+ },
269
+ "additionalProperties": true
258
270
  }
259
271
  },
260
272
  "additionalProperties": true
@@ -268,6 +280,10 @@
268
280
  "type": "array",
269
281
  "description": "Ordered array of compute steps. Each reads card_data.*/requires.*/fetched_sources.*/computed_values.* and writes to ephemeral computed_values[bindTo].",
270
282
  "items": { "$ref": "#/definitions/compute_step" }
283
+ },
284
+ "when_is_task_completed": {
285
+ "type": "string",
286
+ "description": "Advanced/undocumented: invokes a registered inference adapter instead of the default source-delivery completion gate. Prefer the standard pattern: use an LLM-backed source, compute a verdict token, and publish it via provides."
271
287
  }
272
288
  }
273
289
  }