claude-artifact-framework 0.4.0 → 0.5.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.
package/dist/index.esm.js CHANGED
@@ -124,6 +124,7 @@ function required() {
124
124
  var createElement = (...args) => required().createElement(...args);
125
125
  var useState = (...args) => required().useState(...args);
126
126
  var useEffect = (...args) => required().useEffect(...args);
127
+ var useRef = (...args) => required().useRef(...args);
127
128
  var Component = React && React.Component || class ReactMissing {
128
129
  render() {
129
130
  return required();
@@ -154,10 +155,13 @@ function debounce2(fn, ms) {
154
155
  timer = setTimeout(() => fn(...args), ms);
155
156
  };
156
157
  }
157
- function createAppState(defaults, { debounceMs = 400 } = {}) {
158
+ function createAppState(defaults, { debounceMs = 400, shared = false, pollMs = 2500 } = {}) {
159
+ const dataStore = shared ? storage.shared : storage;
158
160
  let data = structuredCloneish(defaults);
159
161
  let view = { ...EMPTY_VIEW };
160
162
  let hydrated = false;
163
+ let writes = 0;
164
+ let persistedWrites = 0;
161
165
  const listeners = /* @__PURE__ */ new Set();
162
166
  let queued = false;
163
167
  function notify() {
@@ -168,11 +172,30 @@ function createAppState(defaults, { debounceMs = 400 } = {}) {
168
172
  for (const fn of listeners) fn();
169
173
  });
170
174
  }
171
- const persistData = debounce2(() => storage.set(DATA_KEY, data), debounceMs);
175
+ const persistData = debounce2(() => {
176
+ const at = writes;
177
+ Promise.resolve(dataStore.set(DATA_KEY, data)).then(() => {
178
+ persistedWrites = Math.max(persistedWrites, at);
179
+ }).catch(() => {
180
+ });
181
+ }, debounceMs);
172
182
  const persistView = debounce2(() => storage.set(VIEW_KEY, view), debounceMs);
183
+ if (shared) {
184
+ setInterval(async () => {
185
+ if (!hydrated || writes > persistedWrites) return;
186
+ try {
187
+ const remote = await dataStore.get(DATA_KEY, void 0);
188
+ if (remote !== void 0 && JSON.stringify(remote) !== JSON.stringify(data)) {
189
+ data = mergeDefaults(defaults, remote);
190
+ notify();
191
+ }
192
+ } catch {
193
+ }
194
+ }, pollMs);
195
+ }
173
196
  const ready = (async () => {
174
197
  const [storedData, storedView] = await Promise.all([
175
- storage.get(DATA_KEY, void 0),
198
+ dataStore.get(DATA_KEY, void 0),
176
199
  storage.get(VIEW_KEY, void 0)
177
200
  ]);
178
201
  if (storedData !== void 0) data = mergeDefaults(defaults, storedData);
@@ -184,6 +207,7 @@ function createAppState(defaults, { debounceMs = 400 } = {}) {
184
207
  const result = fn(data);
185
208
  if (result !== void 0) data = result;
186
209
  data = { ...data };
210
+ writes++;
187
211
  persistData();
188
212
  notify();
189
213
  }
@@ -349,6 +373,7 @@ function seriesColors(seed, n) {
349
373
  }
350
374
 
351
375
  // src/blocks.js
376
+ var FIELD_TYPES = ["text", "textarea", "number", "money", "percent", "check", "date", "select"];
352
377
  function formatValue(value, format) {
353
378
  if (value === null || value === void 0 || value === "") return "\u2014";
354
379
  const n = Number(value);
@@ -688,6 +713,21 @@ var BLOCKS = {
688
713
  var BLOCK_NAMES = Object.keys(BLOCKS);
689
714
 
690
715
  // src/records.js
716
+ function computedEntries(spec) {
717
+ return Object.entries(spec.compute || {}).map(([k, v]) => [k, typeof v === "function" ? { value: v } : v]);
718
+ }
719
+ function withComputed(spec, rec) {
720
+ if (!spec.compute) return rec;
721
+ const out = { ...rec };
722
+ for (const [key, c] of computedEntries(spec)) {
723
+ try {
724
+ out[key] = c.value(rec);
725
+ } catch {
726
+ out[key] = void 0;
727
+ }
728
+ }
729
+ return out;
730
+ }
691
731
  function recordDefaults(fields) {
692
732
  const rec = {};
693
733
  for (const f of fields) {
@@ -727,8 +767,16 @@ function Summary({ spec, records }) {
727
767
  }
728
768
  function ListScreen({ spec, ctx }) {
729
769
  const label = spec.label || "item";
730
- let records = ctx.data.records;
770
+ const [query, setQuery] = useState("");
771
+ let records = ctx.data.records.map((r) => withComputed(spec, r));
731
772
  if (spec.sort) records = records.slice().sort(spec.sort);
773
+ const total = records.length;
774
+ const q = query.trim().toLowerCase();
775
+ if (spec.search && q) {
776
+ records = records.filter(
777
+ (r) => spec.fields.some((f) => String(r[f.key] ?? "").toLowerCase().includes(q))
778
+ );
779
+ }
732
780
  function add() {
733
781
  const rec = { id: makeId(), ...recordDefaults(spec.fields) };
734
782
  ctx.update((d) => {
@@ -742,9 +790,21 @@ function ListScreen({ spec, ctx }) {
742
790
  createElement(
743
791
  "div",
744
792
  { className: "caf-toolbar" },
745
- createElement("span", { className: "caf-count" }, `${records.length} ${label}${records.length === 1 ? "" : "s"}`),
793
+ createElement(
794
+ "span",
795
+ { className: "caf-count" },
796
+ spec.search && q ? `${records.length} of ${total}` : `${total} ${label}${total === 1 ? "" : "s"}`
797
+ ),
746
798
  createElement("button", { type: "button", className: "caf-btn caf-btn-primary", onClick: add }, `Add ${label}`)
747
799
  ),
800
+ spec.search ? createElement("input", {
801
+ className: "caf-search",
802
+ value: query,
803
+ placeholder: `Search ${label}s\u2026`,
804
+ "aria-label": "Search",
805
+ onChange: (e) => setQuery(e.target.value)
806
+ }) : null,
807
+ spec.search && q && records.length === 0 && total > 0 ? createElement("div", { className: "caf-empty" }, `Nothing matches "${query}".`) : null,
748
808
  records.length === 0 ? createElement("div", { className: "caf-empty" }, spec.empty || `No ${label}s yet. Add the first one.`) : records.map(
749
809
  (rec) => createElement(
750
810
  "button",
@@ -787,7 +847,20 @@ function DetailScreen({ spec, ctx, record }) {
787
847
  if (rec) rec[field.key] = v;
788
848
  })
789
849
  })
790
- )
850
+ ),
851
+ spec.compute ? createElement(
852
+ "div",
853
+ { className: "caf-output-grid caf-computed" },
854
+ computedEntries(spec).map(([key, c]) => {
855
+ const live = withComputed(spec, record);
856
+ return createElement(
857
+ "div",
858
+ { key, className: "caf-stat" },
859
+ createElement("span", { className: "caf-stat-label" }, c.label || key),
860
+ createElement("span", { className: "caf-stat-value" }, formatValue(live[key], c.format))
861
+ );
862
+ })
863
+ ) : null
791
864
  );
792
865
  }
793
866
  function RecordsLayout({ spec, ctx }) {
@@ -801,7 +874,7 @@ function RecordsLayout({ spec, ctx }) {
801
874
  return createElement(
802
875
  "div",
803
876
  { className: "caf-panel caf-panel-single" },
804
- createElement(Summary, { spec: rspec, records: ctx.data.records }),
877
+ createElement(Summary, { spec: rspec, records: ctx.data.records.map((r) => withComputed(rspec, r)) }),
805
878
  createElement(ListScreen, { spec: rspec, ctx })
806
879
  );
807
880
  }
@@ -889,10 +962,252 @@ function StepsLayout({ spec, ctx }) {
889
962
  );
890
963
  }
891
964
 
965
+ // src/chat.js
966
+ var CHAT_KEYS = ["system", "greeting", "placeholder", "tools", "model", "maxTokens"];
967
+ var DEFAULT_MODEL = "claude-sonnet-4-6";
968
+ var MAX_ROUNDS = 8;
969
+ async function streamCall({ model, maxTokens, system, messages, onText }) {
970
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
971
+ method: "POST",
972
+ headers: { "Content-Type": "application/json" },
973
+ body: JSON.stringify({ model, max_tokens: maxTokens, system, messages, stream: true })
974
+ });
975
+ if (!res.ok || !res.body) {
976
+ let detail = "";
977
+ try {
978
+ const b = await res.json();
979
+ detail = b && b.error && b.error.message || JSON.stringify(b);
980
+ } catch {
981
+ detail = await res.text().catch(() => "");
982
+ }
983
+ throw new Error(`HTTP ${res.status}${detail ? ": " + detail : ""}`);
984
+ }
985
+ const reader = res.body.getReader();
986
+ const decoder = new TextDecoder();
987
+ let full = "";
988
+ let buf = "";
989
+ for (; ; ) {
990
+ const { done, value } = await reader.read();
991
+ if (done) break;
992
+ buf += decoder.decode(value, { stream: true });
993
+ const lines = buf.split("\n");
994
+ buf = lines.pop();
995
+ for (const line of lines) {
996
+ if (!line.startsWith("data: ")) continue;
997
+ try {
998
+ const j = JSON.parse(line.slice(6));
999
+ if (j.type === "content_block_delta" && j.delta && j.delta.text) {
1000
+ full += j.delta.text;
1001
+ if (onText) onText(full);
1002
+ }
1003
+ } catch {
1004
+ }
1005
+ }
1006
+ }
1007
+ return full;
1008
+ }
1009
+ function parseToolCalls(text) {
1010
+ const match = text.match(/```tool_call\s*\n?([\s\S]*?)```/);
1011
+ if (!match) return null;
1012
+ try {
1013
+ const calls = JSON.parse(match[1]);
1014
+ return Array.isArray(calls) && calls.length ? calls : null;
1015
+ } catch {
1016
+ return null;
1017
+ }
1018
+ }
1019
+ function stripToolCallBlock(text) {
1020
+ return text.replace(/```tool_call\s*\n?[\s\S]*?```/, "").trim();
1021
+ }
1022
+ function toolProtocol(tools) {
1023
+ const list = Object.entries(tools).map(([name, t]) => `- ${name}: ${t.description}
1024
+ Input: ${JSON.stringify(t.input || {})}`).join("\n");
1025
+ return `
1026
+
1027
+ You have tools available:
1028
+ ${list}
1029
+
1030
+ This environment does not support native function calling. To use a tool, reply ONLY with a \`\`\`tool_call code block containing a JSON array, with no other text in that turn. Example:
1031
+
1032
+ \`\`\`tool_call
1033
+ [{ "name": "tool_name", "input": { "param": "value" } }]
1034
+ \`\`\`
1035
+
1036
+ You may include several calls in one array. NEVER invent a tool's result \u2014 emit the pure tool_call block and wait for the real result, which arrives in the next message tagged [TOOL_RESULT]. When you have everything you need, answer in natural language with no tool_call block.`;
1037
+ }
1038
+ function runTools(calls, tools, ctx) {
1039
+ return calls.map((call) => {
1040
+ const tool = tools[call.name];
1041
+ if (!tool) {
1042
+ return { name: call.name, error: `unknown tool "${call.name}". Valid tools: ${Object.keys(tools).join(", ")}` };
1043
+ }
1044
+ try {
1045
+ const result = tool.run(call.input || {}, ctx);
1046
+ return { name: call.name, result: result === void 0 ? { ok: true } : result };
1047
+ } catch (e) {
1048
+ return { name: call.name, error: String(e && e.message || e) };
1049
+ }
1050
+ });
1051
+ }
1052
+ function Bubble({ m }) {
1053
+ if (m.role === "tools") {
1054
+ return createElement("div", { className: "caf-msg-tools" }, `used: ${m.tools.join(", ")}`);
1055
+ }
1056
+ if (m.role === "error") {
1057
+ return createElement(
1058
+ "div",
1059
+ { className: "caf-msg caf-msg-error" },
1060
+ createElement("strong", null, "The call failed"),
1061
+ createElement("span", null, m.content),
1062
+ createElement("span", { className: "caf-hint" }, "Your message is kept \u2014 send again to retry.")
1063
+ );
1064
+ }
1065
+ return createElement("div", { className: m.role === "user" ? "caf-msg caf-msg-user" : "caf-msg caf-msg-assistant" }, m.content);
1066
+ }
1067
+ function ChatLayout({ spec, ctx }) {
1068
+ const c = spec.chat;
1069
+ const [draft, setDraft] = useState("");
1070
+ const [busy, setBusy] = useState(false);
1071
+ const [live, setLive] = useState(null);
1072
+ const endRef = useRef(null);
1073
+ const chat = ctx.data.chat || { api: [], log: [] };
1074
+ const log = chat.log || [];
1075
+ useEffect(() => {
1076
+ if (endRef.current) endRef.current.scrollIntoView({ block: "end" });
1077
+ }, [log.length, live]);
1078
+ async function send() {
1079
+ const text = draft.trim();
1080
+ if (!text || busy) return;
1081
+ setDraft("");
1082
+ setBusy(true);
1083
+ let api = [...chat.api || [], { role: "user", content: text }];
1084
+ let logNow = [...log, { role: "user", content: text }];
1085
+ const commit = () => ctx.update((d) => {
1086
+ d.chat = { api, log: logNow };
1087
+ });
1088
+ commit();
1089
+ try {
1090
+ const system = (typeof c.system === "function" ? c.system(ctx.data) : c.system || "") + (c.tools ? toolProtocol(c.tools) : "");
1091
+ for (let round = 0; round < MAX_ROUNDS; round++) {
1092
+ const raw = await streamCall({
1093
+ model: c.model || DEFAULT_MODEL,
1094
+ maxTokens: c.maxTokens || 2e3,
1095
+ system,
1096
+ messages: api,
1097
+ onText: (t) => setLive(stripToolCallBlock(t))
1098
+ });
1099
+ api = [...api, { role: "assistant", content: raw }];
1100
+ const shown = stripToolCallBlock(raw);
1101
+ const calls = c.tools ? parseToolCalls(raw) : null;
1102
+ if (!calls) {
1103
+ logNow = [...logNow, { role: "assistant", content: shown || raw }];
1104
+ commit();
1105
+ break;
1106
+ }
1107
+ const results = runTools(calls, c.tools, ctx);
1108
+ if (shown) logNow = [...logNow, { role: "assistant", content: shown }];
1109
+ logNow = [...logNow, { role: "tools", tools: calls.map((x) => x.name) }];
1110
+ api = [
1111
+ ...api,
1112
+ {
1113
+ role: "user",
1114
+ content: `[TOOL_RESULT]
1115
+ ${JSON.stringify(results)}
1116
+ [/TOOL_RESULT]
1117
+
1118
+ Continue with this information. If you have everything you need, answer in natural language without further tool_call.`
1119
+ }
1120
+ ];
1121
+ commit();
1122
+ setLive(null);
1123
+ }
1124
+ } catch (e) {
1125
+ logNow = [...logNow, { role: "error", content: String(e && e.message || e) }];
1126
+ commit();
1127
+ } finally {
1128
+ setLive(null);
1129
+ setBusy(false);
1130
+ }
1131
+ }
1132
+ return createElement(
1133
+ "div",
1134
+ { className: "caf-panel caf-panel-single" },
1135
+ createElement(
1136
+ "div",
1137
+ { className: "caf-block caf-chat-log" },
1138
+ c.greeting ? createElement("div", { className: "caf-msg caf-msg-assistant" }, c.greeting) : null,
1139
+ log.map((m, i) => createElement(Bubble, { key: i, m })),
1140
+ live !== null ? createElement("div", { className: "caf-msg caf-msg-assistant caf-msg-live" }, live || "\u2026") : null,
1141
+ busy && live === null ? createElement("div", { className: "caf-msg-tools" }, "thinking\u2026") : null,
1142
+ createElement("div", { ref: endRef })
1143
+ ),
1144
+ createElement(
1145
+ "form",
1146
+ {
1147
+ className: "caf-chat-input",
1148
+ onSubmit: (e) => {
1149
+ e.preventDefault();
1150
+ send();
1151
+ }
1152
+ },
1153
+ createElement("input", {
1154
+ value: draft,
1155
+ placeholder: c.placeholder || "Message\u2026",
1156
+ disabled: busy,
1157
+ onChange: (e) => setDraft(e.target.value),
1158
+ "aria-label": "Message"
1159
+ }),
1160
+ createElement("button", { type: "submit", className: "caf-btn caf-btn-primary", disabled: busy || !draft.trim() }, "Send")
1161
+ )
1162
+ );
1163
+ }
1164
+
892
1165
  // src/app.js
893
- var LAYOUTS = ["panel", "records", "steps"];
1166
+ var LAYOUTS = ["panel", "records", "steps", "chat"];
894
1167
  var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
1168
+ var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared"];
1169
+ function checkFields(fields, where) {
1170
+ for (const f of fields || []) {
1171
+ if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in ${where} needs a \`key\`.`);
1172
+ if (f.type !== void 0 && !FIELD_TYPES.includes(f.type)) {
1173
+ throw new Error(
1174
+ `claude-artifact-framework: field "${f.key}" in ${where} has unknown type "${f.type}". Valid types: ${FIELD_TYPES.join(", ")}.`
1175
+ );
1176
+ }
1177
+ }
1178
+ }
1179
+ function checkBlocks(blocks) {
1180
+ if (!Array.isArray(blocks)) {
1181
+ throw new Error("claude-artifact-framework: `blocks` must be an array.");
1182
+ }
1183
+ blocks.forEach((block, i) => {
1184
+ if (!block || typeof block !== "object") {
1185
+ throw new Error(`claude-artifact-framework: blocks[${i}] must be an object like { fields: [...] }.`);
1186
+ }
1187
+ const keys = Object.keys(block).filter((k) => !RESERVED.includes(k));
1188
+ const known = keys.filter((k) => BLOCK_NAMES.includes(k));
1189
+ if (known.length === 0) {
1190
+ throw new Error(
1191
+ `claude-artifact-framework: blocks[${i}] has no recognised block type (found: ${keys.join(", ") || "nothing"}). Valid block types: ${BLOCK_NAMES.join(", ")}.`
1192
+ );
1193
+ }
1194
+ if (known.length > 1) {
1195
+ throw new Error(
1196
+ `claude-artifact-framework: blocks[${i}] declares more than one block type (${known.join(
1197
+ ", "
1198
+ )}). Split them into separate entries of \`blocks\`.`
1199
+ );
1200
+ }
1201
+ checkFields(block.fields, `blocks[${i}]`);
1202
+ });
1203
+ }
895
1204
  function validate(spec) {
1205
+ const unknownTop = Object.keys(spec).filter((k) => !TOP_KEYS.includes(k));
1206
+ if (unknownTop.length) {
1207
+ throw new Error(
1208
+ `claude-artifact-framework: unknown top-level keys (${unknownTop.join(", ")}). Valid keys: ${TOP_KEYS.join(", ")}.`
1209
+ );
1210
+ }
896
1211
  const layout = spec.layout || "panel";
897
1212
  if (!LAYOUTS.includes(layout)) {
898
1213
  throw new Error(
@@ -919,8 +1234,8 @@ function validate(spec) {
919
1234
  if (!st.fields && !st.text && !st.result) {
920
1235
  throw new Error(`claude-artifact-framework: steps[${i}] needs \`fields\`, \`text\`, or \`result\`.`);
921
1236
  }
1237
+ checkFields(st.fields, `steps[${i}]`);
922
1238
  for (const f of st.fields || []) {
923
- if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in steps[${i}] needs a \`key\`.`);
924
1239
  if (seen.has(f.key)) {
925
1240
  throw new Error(
926
1241
  `claude-artifact-framework: field key "${f.key}" appears in more than one step \u2014 keys share one data pool and must be unique.`
@@ -931,6 +1246,28 @@ function validate(spec) {
931
1246
  });
932
1247
  return layout;
933
1248
  }
1249
+ if (layout === "chat") {
1250
+ const c = spec.chat;
1251
+ if (!c || typeof c.system !== "string" && typeof c.system !== "function") {
1252
+ throw new Error(
1253
+ 'claude-artifact-framework: layout "chat" needs `chat: { system: "..." }` (a string or a function of data).'
1254
+ );
1255
+ }
1256
+ const unknown = Object.keys(c).filter((k) => !CHAT_KEYS.includes(k));
1257
+ if (unknown.length) {
1258
+ throw new Error(
1259
+ `claude-artifact-framework: chat has unknown keys (${unknown.join(", ")}). Valid keys: ${CHAT_KEYS.join(", ")}.`
1260
+ );
1261
+ }
1262
+ for (const [name, tool] of Object.entries(c.tools || {})) {
1263
+ if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
1264
+ throw new Error(
1265
+ `claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
1266
+ );
1267
+ }
1268
+ }
1269
+ return layout;
1270
+ }
934
1271
  if (layout === "records") {
935
1272
  const r = spec.records;
936
1273
  if (!r || !Array.isArray(r.fields) || r.fields.length === 0) {
@@ -938,38 +1275,36 @@ function validate(spec) {
938
1275
  'claude-artifact-framework: layout "records" needs `records: { fields: [...] }` \u2014 the same field declarations the fields block uses.'
939
1276
  );
940
1277
  }
1278
+ const RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute"];
1279
+ const unknownR = Object.keys(r).filter((k) => !RECORDS_KEYS.includes(k));
1280
+ if (unknownR.length) {
1281
+ throw new Error(
1282
+ `claude-artifact-framework: records has unknown keys (${unknownR.join(", ")}). Valid keys: ${RECORDS_KEYS.join(", ")}.`
1283
+ );
1284
+ }
1285
+ checkFields(r.fields, "records");
941
1286
  const seen = /* @__PURE__ */ new Set();
942
1287
  for (const f of r.fields) {
943
- if (!f || !f.key) throw new Error("claude-artifact-framework: every records field needs a `key`.");
944
1288
  if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on records \u2014 the framework assigns it.');
945
1289
  if (seen.has(f.key)) throw new Error(`claude-artifact-framework: duplicate records field key "${f.key}".`);
946
1290
  seen.add(f.key);
947
1291
  }
1292
+ if (spec.blocks) checkBlocks(spec.blocks);
1293
+ for (const [key, c] of Object.entries(r.compute || {})) {
1294
+ if (seen.has(key) || key === "id") {
1295
+ throw new Error(
1296
+ `claude-artifact-framework: compute key "${key}" collides with a field key \u2014 computed values are derived, never stored.`
1297
+ );
1298
+ }
1299
+ if (typeof c !== "function" && typeof (c && c.value) !== "function") {
1300
+ throw new Error(
1301
+ `claude-artifact-framework: compute "${key}" must be a function of the record, or { label, format, value: (r) => ... }.`
1302
+ );
1303
+ }
1304
+ }
948
1305
  return layout;
949
1306
  }
950
- const blocks = spec.blocks || [];
951
- if (!Array.isArray(blocks)) {
952
- throw new Error("claude-artifact-framework: `blocks` must be an array.");
953
- }
954
- blocks.forEach((block, i) => {
955
- if (!block || typeof block !== "object") {
956
- throw new Error(`claude-artifact-framework: blocks[${i}] must be an object like { fields: [...] }.`);
957
- }
958
- const keys = Object.keys(block).filter((k) => !RESERVED.includes(k));
959
- const known = keys.filter((k) => BLOCK_NAMES.includes(k));
960
- if (known.length === 0) {
961
- throw new Error(
962
- `claude-artifact-framework: blocks[${i}] has no recognised block type (found: ${keys.join(", ") || "nothing"}). Valid block types: ${BLOCK_NAMES.join(", ")}.`
963
- );
964
- }
965
- if (known.length > 1) {
966
- throw new Error(
967
- `claude-artifact-framework: blocks[${i}] declares more than one block type (${known.join(
968
- ", "
969
- )}). Split them into separate entries of \`blocks\`.`
970
- );
971
- }
972
- });
1307
+ checkBlocks(spec.blocks || []);
973
1308
  return layout;
974
1309
  }
975
1310
  function defaultsFrom(spec) {
@@ -977,6 +1312,9 @@ function defaultsFrom(spec) {
977
1312
  if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
978
1313
  data.records = [];
979
1314
  }
1315
+ if ((spec.layout || "panel") === "chat" && !data.chat) {
1316
+ data.chat = { api: [], log: [] };
1317
+ }
980
1318
  for (const block of [...spec.blocks || [], ...spec.steps || []]) {
981
1319
  for (const field of block.fields || []) {
982
1320
  if (field && field.key !== void 0 && !(field.key in data)) {
@@ -1030,10 +1368,29 @@ function Panel({ spec, ctx }) {
1030
1368
  )
1031
1369
  );
1032
1370
  }
1033
- var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsLayout, steps: StepsLayout };
1371
+ function RecordsWithBlocks({ spec, ctx }) {
1372
+ const detailOpen = ctx.view.screen === "record" && (ctx.data.records || []).some((r) => r.id === ctx.view.arg);
1373
+ return createElement(
1374
+ "div",
1375
+ null,
1376
+ createElement(RecordsLayout, { spec, ctx }),
1377
+ !detailOpen && Array.isArray(spec.blocks) && spec.blocks.length ? createElement(
1378
+ "div",
1379
+ { className: "caf-panel caf-records-blocks" },
1380
+ spec.blocks.map(
1381
+ (block, i) => createElement(
1382
+ "section",
1383
+ { key: i, className: block.wide ? "caf-slot caf-slot-wide" : "caf-slot" },
1384
+ createElement(Block, { block, ctx })
1385
+ )
1386
+ )
1387
+ ) : null
1388
+ );
1389
+ }
1390
+ var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout };
1034
1391
  function createApp(spec = {}) {
1035
1392
  const layout = validate(spec);
1036
- const state = createAppState(defaultsFrom(spec));
1393
+ const state = createAppState(defaultsFrom(spec), { shared: Boolean(spec.shared) });
1037
1394
  const css = themeCss(spec.theme && spec.theme.seed) + BASE_CSS;
1038
1395
  return function App() {
1039
1396
  if (!hasReact()) {
@@ -1174,6 +1531,26 @@ var BASE_CSS = `
1174
1531
  .caf-steps-fill { transition: width 0.25s ease; }
1175
1532
  .caf-step-text { margin: 0; color: var(--caf-muted); font-size: 0.92rem; line-height: 1.55; }
1176
1533
  .caf-step-nav { display: flex; justify-content: space-between; gap: 0.6rem; margin-top: 0.4rem; }
1534
+ .caf-chat-log { min-height: 320px; max-height: 62vh; overflow-y: auto; gap: 0.55rem; }
1535
+ .caf-msg { max-width: 85%; padding: 0.55rem 0.75rem; border-radius: 12px; font-size: 0.92rem; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
1536
+ .caf-msg-user { align-self: flex-end; background: var(--caf-accent); color: var(--caf-accentText); border-bottom-right-radius: 4px; }
1537
+ .caf-msg-assistant { align-self: flex-start; background: var(--caf-sunken); border-bottom-left-radius: 4px; }
1538
+ .caf-msg-live::after { content: "\u258D"; opacity: 0.6; }
1539
+ .caf-msg-tools { align-self: flex-start; font-size: 0.74rem; color: var(--caf-muted); padding: 0.15rem 0.55rem; border: 1px solid var(--caf-border); border-radius: 999px; }
1540
+ .caf-msg-error { align-self: stretch; max-width: none; display: flex; flex-direction: column; gap: 0.2rem; background: transparent; border: 1px solid var(--caf-danger); }
1541
+ .caf-msg-error strong { color: var(--caf-danger); font-size: 0.85rem; }
1542
+ .caf-chat-input { display: flex; gap: 0.6rem; margin-top: 0.8rem; }
1543
+ .caf-chat-input input { flex: 1; font: inherit; font-size: 0.95rem; padding: 0.55rem 0.7rem; border-radius: 8px; border: 1px solid var(--caf-border); background: var(--caf-surface); color: var(--caf-text); }
1544
+ .caf-chat-input input:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
1545
+ .caf-search {
1546
+ font: inherit; font-size: 0.9rem; width: 100%;
1547
+ padding: 0.5rem 0.65rem; margin: 0 0 0.3rem;
1548
+ border-radius: 7px; border: 1px solid var(--caf-border);
1549
+ background: var(--caf-sunken); color: var(--caf-text);
1550
+ }
1551
+ .caf-search:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
1552
+ .caf-computed { border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
1553
+ .caf-records-blocks { margin-top: 1rem; }
1177
1554
  .caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
1178
1555
  .caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
1179
1556
  .caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }