@bitcobblers/wod-wiki-ui 0.10.41 → 0.11.43

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.
@@ -2820,7 +2820,6 @@ wodWikiWql.WQL_SOURCES.filter((s) => s !== "metrics");
2820
2820
  function sourcePlane(source) {
2821
2821
  return source === "metrics" ? "metrics" : "content";
2822
2822
  }
2823
- var METRICS_ONLY_TYPES = /* @__PURE__ */ new Set(["agg", "metric", "groupby", "rollup", "unit"]);
2824
2823
  var SOURCE_OPTIONS = [
2825
2824
  { value: "journal", label: "Journal", description: "Find notes in the personal journal" },
2826
2825
  { value: "collections", label: "Collections", description: "Find notes in workout catalogs" },
@@ -2896,258 +2895,6 @@ function getClauseMeta(type) {
2896
2895
  description: ""
2897
2896
  };
2898
2897
  }
2899
- function clauseToWql(clause) {
2900
- if (!clause.value.trim()) return {};
2901
- const val = clause.value.trim();
2902
- switch (clause.type) {
2903
- case "text": {
2904
- const needsQuotes = /\s/.test(val) && !(val.startsWith('"') && val.endsWith('"'));
2905
- return { filterStr: `text:${needsQuotes ? `"${val}"` : val}` };
2906
- }
2907
- case "catalog":
2908
- return { filterStr: `catalog:${val}` };
2909
- case "tag":
2910
- return { filterStr: `tags:${val}` };
2911
- case "effort":
2912
- return { filterStr: `effort:${val}` };
2913
- case "discipline":
2914
- return { filterStr: `discipline:${val}` };
2915
- case "intensity":
2916
- return { filterStr: `intensity:${val}` };
2917
- case "origin":
2918
- return { filterStr: `origin:${val}` };
2919
- case "type":
2920
- return { filterStr: `type:${val}` };
2921
- case "has":
2922
- return { filterStr: `has:${val}` };
2923
- case "result":
2924
- return { filterStr: `result:${val}` };
2925
- case "block":
2926
- return { filterStr: `block:${val}` };
2927
- case "note":
2928
- return { filterStr: `note:${val}` };
2929
- default: {
2930
- const def = composerRegistry.getSlot(clause.type);
2931
- if (!def) return {};
2932
- if (!def.parseValue) return {};
2933
- const value = def.parseValue(val);
2934
- if (value === void 0) return {};
2935
- const fragment = def.wqlGenerator(value);
2936
- return fragment.trim() ? { filterStr: fragment } : {};
2937
- }
2938
- }
2939
- }
2940
- function clausesToWql(clauses) {
2941
- const whereClause = clauses.find((c) => c.type === "where");
2942
- const whereStr = whereClause?.value?.trim() ? `where ${whereClause.value.trim()}` : "";
2943
- const filterParts = [];
2944
- for (const c of clauses) {
2945
- const res = clauseToWql(c);
2946
- if (res.filterStr) {
2947
- filterParts.push(res.filterStr);
2948
- }
2949
- }
2950
- const filterBraces = filterParts.length > 0 ? `{${filterParts.join(", ")}}` : "";
2951
- const source = clauseValue(clauses, "source", "notes");
2952
- if (source === "rows") {
2953
- const output = clauseValue(clauses, "output", "") || "all";
2954
- const timeClause2 = clauses.find((c) => c.type === "time");
2955
- let timeStr2 = "";
2956
- if (timeClause2 && timeClause2.value && timeClause2.value !== "all") {
2957
- const rawVal = timeClause2.value.trim();
2958
- timeStr2 = rawVal.startsWith("last") ? rawVal : `last ${rawVal}`;
2959
- }
2960
- return [
2961
- `rows:${output}${filterBraces}`,
2962
- timeStr2
2963
- ].filter(Boolean).join(" ").trim();
2964
- }
2965
- if (source === "metrics") {
2966
- const agg = clauseValue(clauses, "agg", "sum");
2967
- const metric = clauseValue(clauses, "metric", "");
2968
- const dims = clauses.filter((c) => c.type === "groupby" && c.value.trim()).map((c) => c.value.trim());
2969
- const rollup = clauseValue(clauses, "rollup", "");
2970
- const unit = clauseValue(clauses, "unit", "");
2971
- const byStr = dims.length ? `by {${dims.join(", ")}}` : "";
2972
- const rollupStr = rollup ? `.rollup(${rollup})` : "";
2973
- const head = byStr ? `${agg}:${metric}${filterBraces}` : `${agg}:${metric}${filterBraces}${rollupStr}`;
2974
- return [
2975
- head,
2976
- `${byStr}${byStr ? rollupStr : ""}`,
2977
- unit ? `in ${unit}` : "",
2978
- whereStr
2979
- ].filter(Boolean).join(" ").trim();
2980
- }
2981
- const target = source === "blocks" ? "block" : source === "efforts" ? "effort" : "note";
2982
- const scope = source === "notes" || source === "blocks" || source === "efforts" ? "all" : source;
2983
- const timeClause = clauses.find((c) => c.type === "time");
2984
- let timeStr = "";
2985
- if (timeClause && timeClause.value && timeClause.value !== "all") {
2986
- const rawVal = timeClause.value.trim();
2987
- timeStr = rawVal.startsWith("last") ? rawVal : `last ${rawVal}`;
2988
- }
2989
- return [
2990
- `find:${target}${filterBraces}`,
2991
- `in ${scope}`,
2992
- timeStr,
2993
- whereStr
2994
- ].filter(Boolean).join(" ").trim();
2995
- }
2996
- var POSITIONAL_CLAUSE_TYPES = /* @__PURE__ */ new Set(["source", "time", "where", "agg", "metric", "groupby", "rollup", "unit", "output"]);
2997
- var FILTER_KEY_TO_CLAUSE_TYPE = Object.fromEntries(
2998
- Object.entries(CLAUSE_META).filter(([type]) => !POSITIONAL_CLAUSE_TYPES.has(type)).map(([type, meta]) => [meta.prefix.replace(/:$/, ""), type])
2999
- );
3000
- function splitTopLevel(body) {
3001
- const parts = [];
3002
- let depth = 0;
3003
- let inQuote = false;
3004
- let start = 0;
3005
- for (let i = 0; i < body.length; i++) {
3006
- const c = body[i];
3007
- if (c === '"') inQuote = !inQuote;
3008
- else if (!inQuote && c === "{") depth++;
3009
- else if (!inQuote && c === "}") depth = Math.max(0, depth - 1);
3010
- else if (!inQuote && c === "," && depth === 0) {
3011
- parts.push(body.slice(start, i));
3012
- start = i + 1;
3013
- }
3014
- }
3015
- parts.push(body.slice(start));
3016
- return parts;
3017
- }
3018
- var RESTORE_FIND_HEAD_RE = /^find:([a-zA-Z0-9_-]*)/;
3019
- var RESTORE_AGG_HEAD_RE = /^([a-zA-Z0-9_-]+):([a-zA-Z0-9_.-]*)/;
3020
- function restoreClause(id, type, value) {
3021
- return { id, type, ...getClauseMeta(type), value };
3022
- }
3023
- function filterFragmentToClause(fragment, index) {
3024
- if (fragment.startsWith("!")) return null;
3025
- const colon2 = fragment.indexOf(":");
3026
- if (colon2 <= 0) return null;
3027
- const key = fragment.slice(0, colon2);
3028
- const value = fragment.slice(colon2 + 1);
3029
- if (!value.trim()) return null;
3030
- if (key === "source") {
3031
- return restoreClause(`c-source-${index}`, "source", value);
3032
- }
3033
- const builtin = FILTER_KEY_TO_CLAUSE_TYPE[key];
3034
- if (builtin) {
3035
- const restoredValue = builtin === "text" && value.length >= 2 && value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
3036
- return restoreClause(`c-${builtin}-${index}`, builtin, restoredValue);
3037
- }
3038
- for (const slot of composerRegistry.getAllSlots()) {
3039
- if (!slot.parseValue || !slot.formatValue) continue;
3040
- const typed = slot.parseValue(value);
3041
- if (typed !== void 0 && slot.wqlGenerator(typed) === fragment) {
3042
- return restoreClause(`c-${slot.type}-${index}`, slot.type, slot.formatValue(typed));
3043
- }
3044
- }
3045
- return null;
3046
- }
3047
- function restoreFilters(rest) {
3048
- if (!rest) return [];
3049
- if (!rest.startsWith("{") || !rest.endsWith("}")) return null;
3050
- const body = rest.slice(1, -1);
3051
- const fragments = splitTopLevel(body).map((f) => f.trim()).filter(Boolean);
3052
- const clauses = [];
3053
- for (let i = 0; i < fragments.length; i++) {
3054
- const clause = filterFragmentToClause(fragments[i], i);
3055
- if (!clause) return null;
3056
- clauses.push(clause);
3057
- }
3058
- return clauses;
3059
- }
3060
- function wqlToClauses(wql2) {
3061
- const suffixes = wodWikiWql.parseWqlSuffixes(wql2.trim());
3062
- if (suffixes.conflicts?.length) return null;
3063
- const { where, displayUnit, groupBy, rollup, window: window2, legacyScope, primaryText } = suffixes;
3064
- if (window2?.kind === "range") return null;
3065
- const rowsMatch = /^rows(?:\:([a-zA-Z0-9_-]+))?\:?/.exec(primaryText);
3066
- if (rowsMatch) {
3067
- if (where || groupBy || rollup) return null;
3068
- const rest2 = primaryText.slice(rowsMatch[0].length).trim();
3069
- const filterClauses2 = restoreFilters(rest2);
3070
- if (!filterClauses2) return null;
3071
- const clauses2 = [
3072
- restoreClause("c-source", "source", "rows"),
3073
- restoreClause("c-time", "time", window2?.kind === "relative" ? `last ${window2.size}${window2.unit}` : "all"),
3074
- ...rowsMatch[1] ? [restoreClause("c-output", "output", rowsMatch[1])] : [],
3075
- ...filterClauses2
3076
- ];
3077
- return clauses2;
3078
- }
3079
- if (primaryText.startsWith("find:")) {
3080
- const headMatch2 = RESTORE_FIND_HEAD_RE.exec(primaryText);
3081
- if (!headMatch2) return null;
3082
- const targetValue = headMatch2[1];
3083
- if (!wodWikiWql.WQL_FIND_TARGETS.includes(targetValue)) return null;
3084
- const rest2 = primaryText.slice(headMatch2[0].length).trim();
3085
- const filterClauses2 = restoreFilters(rest2);
3086
- if (!filterClauses2) return null;
3087
- const timeValue = window2?.kind === "relative" ? `last ${window2.size}${window2.unit}` : "all";
3088
- const sourceClauseIdx = filterClauses2.findIndex((c) => c.type === "source");
3089
- const sourceFromFilter = sourceClauseIdx !== -1 ? filterClauses2[sourceClauseIdx].value : void 0;
3090
- const scopeValue = legacyScope ?? sourceFromFilter;
3091
- const remainingFilterClauses = sourceClauseIdx !== -1 ? filterClauses2.filter((_, idx) => idx !== sourceClauseIdx) : filterClauses2;
3092
- const sourceValue = targetValue === "block" ? "blocks" : targetValue === "effort" ? "efforts" : !scopeValue || scopeValue === "all" ? "notes" : scopeValue;
3093
- const clauses2 = [
3094
- restoreClause("c-source", "source", sourceValue),
3095
- restoreClause("c-time", "time", timeValue),
3096
- ...remainingFilterClauses
3097
- ];
3098
- if (where) clauses2.push(restoreClause("c-where", "where", where));
3099
- return clauses2;
3100
- }
3101
- if (window2) return null;
3102
- const headMatch = RESTORE_AGG_HEAD_RE.exec(primaryText);
3103
- if (!headMatch) return null;
3104
- const aggValue = headMatch[1];
3105
- const metricValue = headMatch[2] ?? "";
3106
- const rest = primaryText.slice(headMatch[0].length).trim();
3107
- const filterClauses = restoreFilters(rest);
3108
- if (!filterClauses) return null;
3109
- const dimValues = groupBy ?? [];
3110
- const rollupValue = rollup?.raw ?? "";
3111
- const unitValue = displayUnit ?? "";
3112
- const clauses = [
3113
- restoreClause("c-source", "source", "metrics"),
3114
- restoreClause("c-agg", "agg", aggValue),
3115
- restoreClause("c-metric", "metric", metricValue),
3116
- ...filterClauses,
3117
- ...dimValues.map((d, i) => restoreClause(`c-groupby-${i}`, "groupby", d))
3118
- ];
3119
- if (rollupValue) clauses.push(restoreClause("c-rollup", "rollup", rollupValue));
3120
- if (unitValue) clauses.push(restoreClause("c-unit", "unit", unitValue));
3121
- if (where) clauses.push(restoreClause("c-where", "where", where));
3122
- return clauses;
3123
- }
3124
- function pivotClauses(clauses, source) {
3125
- const next = clauses.filter((c) => !METRICS_ONLY_TYPES.has(c.type) && c.type !== "time" && c.type !== "where").map((c) => c.type === "source" ? { ...c, value: source } : c);
3126
- if (source === "metrics") {
3127
- const sourceIdx = next.findIndex((c) => c.type === "source");
3128
- const head = [];
3129
- if (!next.some((c) => c.type === "agg")) head.push(restoreClause("c-agg", "agg", "sum"));
3130
- if (!next.some((c) => c.type === "metric")) head.push(restoreClause("c-metric", "metric", ""));
3131
- next.splice(sourceIdx >= 0 ? sourceIdx + 1 : next.length, 0, ...head);
3132
- }
3133
- return next;
3134
- }
3135
- function defaultClauses() {
3136
- return [
3137
- { id: "c-source", type: "source", ...CLAUSE_META.source, value: "notes" },
3138
- { id: "c-time", type: "time", ...CLAUSE_META.time, value: "last 2w" }
3139
- ];
3140
- }
3141
- function defaultMetricsClauses() {
3142
- return [
3143
- { id: "c-source", type: "source", ...CLAUSE_META.source, value: "metrics" },
3144
- { id: "c-agg", type: "agg", ...CLAUSE_META.agg, value: "sum" },
3145
- { id: "c-metric", type: "metric", ...CLAUSE_META.metric, value: "" }
3146
- ];
3147
- }
3148
- function clauseValue(clauses, type, fallback = "") {
3149
- return clauses.find((c) => c.type === type)?.value?.trim() || fallback;
3150
- }
3151
2898
  function TokenSlotPill({
3152
2899
  clause,
3153
2900
  isActive,
@@ -3599,6 +3346,247 @@ function AddCalcDropdown({
3599
3346
  )
3600
3347
  ] });
3601
3348
  }
3349
+ var PILL_KEY = {
3350
+ text: "text",
3351
+ catalog: "catalog",
3352
+ tag: "tags",
3353
+ effort: "effort",
3354
+ discipline: "discipline",
3355
+ intensity: "intensity",
3356
+ origin: "origin",
3357
+ type: "type",
3358
+ has: "has",
3359
+ result: "result",
3360
+ block: "block",
3361
+ note: "note"
3362
+ };
3363
+ var KEY_PILL = Object.fromEntries(
3364
+ Object.entries(PILL_KEY).map(([pill, key]) => [key, pill])
3365
+ );
3366
+ function pillValueToFilter(type, rawValue) {
3367
+ const key = PILL_KEY[type];
3368
+ if (!key) return null;
3369
+ const values2 = rawValue.split("|").map((v) => v.trim()).filter(Boolean).map((v) => ({
3370
+ value: v.endsWith("*") ? v.slice(0, -1) : v,
3371
+ wildcard: v.endsWith("*")
3372
+ }));
3373
+ if (values2.length === 0) return null;
3374
+ return { key, negate: false, values: values2 };
3375
+ }
3376
+ function filterFragment(f) {
3377
+ const render = (v) => {
3378
+ const bare = /^[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)?$/.test(v.value);
3379
+ return `${bare ? v.value : `"${v.value}"`}${v.wildcard ? "*" : ""}`;
3380
+ };
3381
+ return `${f.negate ? "!" : ""}${f.key}:${f.values.map(render).join("|")}`;
3382
+ }
3383
+ function filterToPill(f, index) {
3384
+ if (f.negate) return null;
3385
+ const pillValue2 = f.values.map((v) => `${v.value}${v.wildcard ? "*" : ""}`).join("|");
3386
+ if (!pillValue2.trim()) return null;
3387
+ if (f.key === "source") {
3388
+ return restorePill(`c-source-${index}`, "source", pillValue2);
3389
+ }
3390
+ const builtin = KEY_PILL[f.key];
3391
+ if (builtin) {
3392
+ const value = builtin === "text" && f.values.length === 1 && /\s/.test(f.values[0].value) ? f.values[0].value : pillValue2;
3393
+ return restorePill(`c-${builtin}-${index}`, builtin, value);
3394
+ }
3395
+ const fragment = filterFragment(f);
3396
+ for (const slot of composerRegistry.getAllSlots()) {
3397
+ if (!slot.parseValue || !slot.formatValue) continue;
3398
+ const typed = slot.parseValue(pillValue2);
3399
+ if (typed !== void 0 && slot.wqlGenerator(typed) === fragment) {
3400
+ return restorePill(`c-${slot.type}-${index}`, slot.type, slot.formatValue(typed));
3401
+ }
3402
+ }
3403
+ return null;
3404
+ }
3405
+ function restorePill(id, type, value) {
3406
+ return { id, type, ...getClauseMeta(type), value };
3407
+ }
3408
+ function astToPills(ast) {
3409
+ if (ast.error) return null;
3410
+ if (wodWikiWql.isRowsQuery(ast)) {
3411
+ if (ast.window?.kind === "range") return null;
3412
+ if (ast.filters.some((f) => f.key === "source")) return null;
3413
+ const filterPills = [];
3414
+ for (let i = 0; i < ast.filters.length; i++) {
3415
+ const p = filterToPill(ast.filters[i], i);
3416
+ if (!p) return null;
3417
+ filterPills.push(p);
3418
+ }
3419
+ return [
3420
+ restorePill("c-source", "source", "rows"),
3421
+ restorePill("c-time", "time", windowPillValue(ast.window)),
3422
+ restorePill("c-output", "output", ast.outputType ?? "all"),
3423
+ ...filterPills
3424
+ ];
3425
+ }
3426
+ if (wodWikiWql.isFindQuery(ast)) {
3427
+ if (ast.window?.kind === "range") return null;
3428
+ const sourceFilter = ast.filters.find((f) => f.key === "source" && !f.negate);
3429
+ const nonIdentitySource = sourceFilter && !sourceFilter.values.every((v) => v.value === "all");
3430
+ if ((ast.target === "block" || ast.target === "effort") && nonIdentitySource) return null;
3431
+ const sourceValue = ast.target === "block" ? "blocks" : ast.target === "effort" ? "efforts" : !sourceFilter || sourceFilter.values.every((v) => v.value === "all") ? "notes" : sourceFilter.values.map((v) => v.value).join("|");
3432
+ const rest = ast.filters.filter((f) => f !== sourceFilter);
3433
+ const filterPills = [];
3434
+ for (let i = 0; i < rest.length; i++) {
3435
+ const p = filterToPill(rest[i], i);
3436
+ if (!p) return null;
3437
+ filterPills.push(p);
3438
+ }
3439
+ const pills = [
3440
+ restorePill("c-source", "source", sourceValue),
3441
+ restorePill("c-time", "time", windowPillValue(ast.window)),
3442
+ ...filterPills
3443
+ ];
3444
+ if (ast.join) {
3445
+ const j = ast.join;
3446
+ const head = wodWikiWql.serialize({
3447
+ family: "aggregate",
3448
+ raw: "",
3449
+ agg: j.agg,
3450
+ metric: j.metric,
3451
+ filters: j.filters,
3452
+ groupBy: []
3453
+ });
3454
+ pills.push(restorePill("c-where", "where", `${head} ${j.operator} ${j.threshold}`));
3455
+ }
3456
+ return pills;
3457
+ }
3458
+ if (wodWikiWql.isAggregateQuery(ast)) {
3459
+ if (ast.window?.kind === "range") return null;
3460
+ const filterPills = [];
3461
+ for (let i = 0; i < ast.filters.length; i++) {
3462
+ const p = filterToPill(ast.filters[i], i);
3463
+ if (!p) return null;
3464
+ filterPills.push(p);
3465
+ }
3466
+ const pills = [
3467
+ restorePill("c-source", "source", "metrics"),
3468
+ restorePill("c-agg", "agg", ast.agg),
3469
+ restorePill("c-metric", "metric", ast.metric),
3470
+ ...filterPills,
3471
+ ...ast.groupBy.map((d, i) => restorePill(`c-groupby-${i}`, "groupby", d))
3472
+ ];
3473
+ if (ast.rollup) pills.push(restorePill("c-rollup", "rollup", `${ast.rollup.size}${ast.rollup.unit}`));
3474
+ if (ast.displayUnit) pills.push(restorePill("c-unit", "unit", ast.displayUnit));
3475
+ if (ast.window) pills.push(restorePill("c-time", "time", windowPillValue(ast.window)));
3476
+ if (ast.join) {
3477
+ pills.push(
3478
+ restorePill(
3479
+ "c-where",
3480
+ "where",
3481
+ wodWikiWql.serialize({
3482
+ family: "find",
3483
+ raw: "",
3484
+ target: ast.join.target,
3485
+ filters: ast.join.filters,
3486
+ window: ast.join.last ? { kind: "relative", size: ast.join.last.size, unit: ast.join.last.unit } : void 0
3487
+ })
3488
+ )
3489
+ );
3490
+ }
3491
+ return pills;
3492
+ }
3493
+ return null;
3494
+ }
3495
+ function windowPillValue(w) {
3496
+ if (!w) return "all";
3497
+ return `last ${w.size}${w.unit}`;
3498
+ }
3499
+ function wqlToPills(wql2) {
3500
+ const direct = astToPills(wodWikiWql.parseQuery(wql2.trim()));
3501
+ if (direct) return direct;
3502
+ const probed = wql2.trim().replace(/^([a-zA-Z_]\w*):(\s*\{)/, "$1:__placeholder__$2");
3503
+ if (probed === wql2.trim()) return null;
3504
+ const salvaged = astToPills(wodWikiWql.parseQuery(probed));
3505
+ if (!salvaged) return null;
3506
+ return salvaged.map((p) => p.type === "metric" ? { ...p, value: "" } : p);
3507
+ }
3508
+ function timePillText(raw) {
3509
+ const v = raw.trim();
3510
+ if (!v || v === "all") return "";
3511
+ if (v.startsWith("last") || v.startsWith("from")) return v;
3512
+ return `last ${v}`;
3513
+ }
3514
+ function buildCandidateText(pills) {
3515
+ const source = pillValue(pills, "source", "notes");
3516
+ const filterParts = [];
3517
+ for (const pill of pills) {
3518
+ const v = pill.value.trim();
3519
+ if (!v) continue;
3520
+ const builtin = pillValueToFilter(pill.type, v);
3521
+ if (builtin) {
3522
+ filterParts.push(filterFragment(builtin));
3523
+ continue;
3524
+ }
3525
+ const def = composerRegistry.getSlot(pill.type);
3526
+ if (!def?.parseValue) continue;
3527
+ const typed = def.parseValue(v);
3528
+ if (typed === void 0) continue;
3529
+ const fragment = def.wqlGenerator(typed).trim();
3530
+ if (fragment) filterParts.push(fragment);
3531
+ }
3532
+ const timePill = pills.find((c) => c.type === "time");
3533
+ const timeText = timePill ? timePillText(timePill.value) : "";
3534
+ const wherePill = pills.find((c) => c.type === "where");
3535
+ const whereText = wherePill?.value.trim() ? `where ${wherePill.value.trim()}` : "";
3536
+ if (source === "rows") {
3537
+ const output = pillValue(pills, "output", "all") || "all";
3538
+ return [`rows:${output}{${filterParts.join(",")}}`, timeText].filter(Boolean).join(" ").trim();
3539
+ }
3540
+ if (sourcePlane(source) === "metrics") {
3541
+ const agg = pillValue(pills, "agg", "sum");
3542
+ const metric = pillValue(pills, "metric", "");
3543
+ const dims = pills.filter((c) => c.type === "groupby" && c.value.trim()).map((c) => c.value.trim());
3544
+ const rollup = pillValue(pills, "rollup", "");
3545
+ const unit = pillValue(pills, "unit", "");
3546
+ return [
3547
+ `${agg}:${metric}{${filterParts.join(",")}}`,
3548
+ dims.length ? `by {${dims.join(", ")}}` : "",
3549
+ rollup ? `.rollup(${rollup})` : "",
3550
+ unit ? `in ${unit}` : "",
3551
+ timeText,
3552
+ whereText
3553
+ ].filter(Boolean).join(" ").trim();
3554
+ }
3555
+ const target = source === "blocks" ? "block" : source === "efforts" ? "effort" : "note";
3556
+ const withSource = source === "notes" || source === "blocks" || source === "efforts" ? filterParts : [`source:${source}`, ...filterParts];
3557
+ const braces = withSource.length ? `{${withSource.join(",")}}` : "";
3558
+ return [`find:${target}${braces}`, timeText, whereText].filter(Boolean).join(" ").trim();
3559
+ }
3560
+ function pillsToAst(pills) {
3561
+ return wodWikiWql.parseQuery(buildCandidateText(pills));
3562
+ }
3563
+ var METRICS_HEAD_TYPES = /* @__PURE__ */ new Set(["agg", "metric", "groupby", "rollup", "unit"]);
3564
+ function pivotPills(pills, source) {
3565
+ const next = pills.filter((c) => !METRICS_HEAD_TYPES.has(c.type) && c.type !== "where").map((c) => c.type === "source" ? { ...c, value: source } : c);
3566
+ if (source === "metrics") {
3567
+ const sourceIdx = next.findIndex((c) => c.type === "source");
3568
+ const head = [];
3569
+ if (!next.some((c) => c.type === "agg")) head.push(restorePill("c-agg", "agg", "sum"));
3570
+ if (!next.some((c) => c.type === "metric")) head.push(restorePill("c-metric", "metric", ""));
3571
+ next.splice(sourceIdx >= 0 ? sourceIdx + 1 : next.length, 0, ...head);
3572
+ }
3573
+ if (source === "rows" && !next.some((c) => c.type === "output")) {
3574
+ const sourceIdx = next.findIndex((c) => c.type === "source");
3575
+ next.splice(sourceIdx >= 0 ? sourceIdx + 1 : next.length, 0, restorePill("c-output", "output", "all"));
3576
+ }
3577
+ return next;
3578
+ }
3579
+ function defaultPills() {
3580
+ return [
3581
+ restorePill("c-source", "source", "notes"),
3582
+ restorePill("c-time", "time", "last 2w")
3583
+ ];
3584
+ }
3585
+ function pillValue(pills, type, fallback = "") {
3586
+ return pills.find((c) => c.type === type)?.value?.trim() || fallback;
3587
+ }
3588
+
3589
+ // src/composer/diagnostics.ts
3602
3590
  function summarizeFind(ast) {
3603
3591
  const sourceFilter = ast.filters.find((f) => f.key === "source");
3604
3592
  const scope = sourceFilter ? sourceFilter.values.map((v) => v.value).join(",") : "all";
@@ -3630,21 +3618,21 @@ function customSlotError(clause) {
3630
3618
  if (parsed === void 0) return `Invalid ${custom.label} value`;
3631
3619
  return custom.validate ? custom.validate(parsed) : null;
3632
3620
  }
3633
- function diagnoseClauses(clauses) {
3634
- for (const clause of clauses) {
3635
- const error = customSlotError(clause);
3621
+ function diagnosePills(pills) {
3622
+ for (const pill of pills) {
3623
+ const error = customSlotError(pill);
3636
3624
  if (error) {
3637
3625
  return {
3638
3626
  valid: false,
3639
- wql: clausesToWql(clauses),
3627
+ wql: wodWikiWql.serialize(pillsToAst(pills)),
3640
3628
  ast: { family: "aggregate", raw: "", agg: "sum", metric: "", filters: [], groupBy: [], error },
3641
3629
  error,
3642
- offendingClauseId: clause.id
3630
+ offendingClauseId: pill.id
3643
3631
  };
3644
3632
  }
3645
3633
  }
3646
- const wql2 = clausesToWql(clauses);
3647
- const ast = wodWikiWql.parseQuery(wql2);
3634
+ const ast = pillsToAst(pills);
3635
+ const wql2 = wodWikiWql.serialize(ast);
3648
3636
  if (ast.error) {
3649
3637
  return {
3650
3638
  valid: false,
@@ -3772,10 +3760,9 @@ function useWqlStageCounts(ast, valid, execute, debounceMs = DEFAULT_DIAGNOSTICS
3772
3760
  return runnable ? stages : void 0;
3773
3761
  }
3774
3762
  function WqlComposer({
3775
- initialClauses,
3776
- clauses: controlledClauses,
3777
- onClausesChange,
3778
- onWqlChange,
3763
+ initialQuery,
3764
+ query: controlledQuery,
3765
+ onQueryChange,
3779
3766
  onValidationChange,
3780
3767
  onAstChange,
3781
3768
  onSubmit,
@@ -3789,52 +3776,91 @@ function WqlComposer({
3789
3776
  placeholder = "Type search term and press Enter...",
3790
3777
  className
3791
3778
  }) {
3792
- const [internalClauses, setInternalClauses] = React.useState(
3793
- () => initialClauses ?? defaultClauses()
3779
+ const [internalPills, setInternalPills] = React.useState(
3780
+ () => wqlToPills(initialQuery ?? "") ?? defaultPills()
3781
+ );
3782
+ const controlledPills = React.useMemo(
3783
+ () => controlledQuery === void 0 ? void 0 : wqlToPills(controlledQuery),
3784
+ [controlledQuery]
3794
3785
  );
3795
- const clauses = controlledClauses ?? internalClauses;
3786
+ const pills = controlledPills ?? internalPills;
3796
3787
  const hiddenTypes = React.useMemo(() => new Set(hiddenClauseTypes), [hiddenClauseTypes]);
3797
3788
  const [activeSlotIdx, setActiveSlotIdx] = React.useState(null);
3798
3789
  const [freeText, setFreeText] = React.useState("");
3799
3790
  const inputRef = React.useRef(null);
3791
+ const freeTextInitRef = React.useRef(null);
3792
+ React.useEffect(() => {
3793
+ if (controlledQuery === void 0) return;
3794
+ if (controlledPills === null) {
3795
+ if (freeTextInitRef.current !== controlledQuery) {
3796
+ freeTextInitRef.current = controlledQuery;
3797
+ setFreeText(controlledQuery);
3798
+ }
3799
+ } else if (freeTextInitRef.current !== null) {
3800
+ freeTextInitRef.current = null;
3801
+ setFreeText("");
3802
+ }
3803
+ }, [controlledQuery, controlledPills]);
3800
3804
  React.useEffect(() => {
3801
3805
  if (!autoFocus) return;
3802
3806
  const t = setTimeout(() => inputRef.current?.focus(), 0);
3803
3807
  return () => clearTimeout(t);
3804
3808
  }, [autoFocus]);
3805
- const setClauses = React.useCallback(
3809
+ const setPills = React.useCallback(
3806
3810
  (next) => {
3807
- if (controlledClauses === void 0) setInternalClauses(next);
3808
- onClausesChange?.(next);
3811
+ if (controlledQuery === void 0) setInternalPills(next);
3809
3812
  },
3810
- [controlledClauses, onClausesChange]
3813
+ [controlledQuery]
3811
3814
  );
3812
3815
  const registeredSlots = useComposerSlots();
3813
- const diagnostics = React.useMemo(() => diagnoseClauses(clauses), [clauses, registeredSlots]);
3814
- const callbacksRef = React.useRef({ onWqlChange, onValidationChange, onAstChange });
3815
- callbacksRef.current = { onWqlChange, onValidationChange, onAstChange };
3816
+ const rawEscape = controlledQuery !== void 0 && controlledPills === null;
3817
+ const diagnostics = React.useMemo(() => {
3818
+ if (rawEscape) {
3819
+ const ast = wodWikiWql.parseQuery(controlledQuery);
3820
+ return {
3821
+ valid: !ast.error,
3822
+ wql: controlledQuery,
3823
+ ast,
3824
+ error: ast.error
3825
+ };
3826
+ }
3827
+ return diagnosePills(pills);
3828
+ }, [rawEscape, controlledQuery, pills, registeredSlots]);
3829
+ const callbacksRef = React.useRef({ onQueryChange, onValidationChange, onAstChange });
3830
+ callbacksRef.current = { onQueryChange, onValidationChange, onAstChange };
3816
3831
  React.useEffect(() => {
3817
3832
  const validation = diagnostics.valid ? { valid: true } : { valid: false, error: diagnostics.error };
3818
- callbacksRef.current.onWqlChange?.(diagnostics.wql);
3833
+ if (controlledQuery === void 0) callbacksRef.current.onQueryChange?.(diagnostics.wql);
3819
3834
  callbacksRef.current.onAstChange?.(diagnostics.ast);
3820
3835
  callbacksRef.current.onValidationChange?.(validation);
3821
3836
  }, [diagnostics]);
3822
3837
  const stages = useWqlStageCounts(diagnostics.ast, diagnostics.valid, execute, debounceMs);
3823
- const offendingClause = diagnostics.offendingClauseId ? clauses.find((c) => c.id === diagnostics.offendingClauseId) : void 0;
3824
- const offendingLabel = offendingClause ? getClauseMeta(offendingClause.type).label : void 0;
3825
- const updateClause = (idx, patch) => {
3826
- const updated = clauses.map((c, i) => i === idx ? { ...c, ...patch } : c);
3827
- const edited = clauses[idx];
3838
+ const offendingPill = diagnostics.offendingClauseId ? pills.find((c) => c.id === diagnostics.offendingClauseId) : void 0;
3839
+ const offendingLabel = offendingPill ? getClauseMeta(offendingPill.type).label : void 0;
3840
+ const updatePill = (idx, patch) => {
3841
+ const updated = pills.map((c, i) => i === idx ? { ...c, ...patch } : c);
3842
+ const edited = pills[idx];
3828
3843
  if (edited?.type === "source" && patch.value !== void 0 && patch.value !== edited.value) {
3829
- setClauses(pivotClauses(updated, patch.value));
3844
+ setPills(pivotPills(updated, patch.value));
3845
+ emitIfControlled(pivotPills(updated, patch.value));
3830
3846
  return;
3831
3847
  }
3832
- setClauses(updated);
3848
+ setPills(updated);
3849
+ emitIfControlled(updated);
3833
3850
  };
3834
- const removeClause = (idx) => {
3835
- setClauses(clauses.filter((_, i) => i !== idx));
3851
+ const removePill = (idx) => {
3852
+ const next = pills.filter((_, i) => i !== idx);
3853
+ setPills(next);
3854
+ emitIfControlled(next);
3836
3855
  };
3837
- const makeClause = (type, value) => {
3856
+ const emitIfControlled = (next) => {
3857
+ if (controlledQuery !== void 0) {
3858
+ const nextAst = pillsToAst(next);
3859
+ const nextWql = wodWikiWql.serialize(nextAst);
3860
+ if (nextWql !== controlledQuery) callbacksRef.current.onQueryChange?.(nextWql);
3861
+ }
3862
+ };
3863
+ const makePill = (type, value) => {
3838
3864
  const meta = getClauseMeta(type);
3839
3865
  return {
3840
3866
  id: `c-${Date.now()}-${Math.random()}`,
@@ -3848,10 +3874,13 @@ function WqlComposer({
3848
3874
  const pending = React.useMemo(() => {
3849
3875
  const raw = freeText.trim();
3850
3876
  if (!raw) return null;
3851
- const restored = wqlToClauses(raw);
3852
- if (restored) {
3853
- const parsed = wodWikiWql.parseQuery(raw);
3854
- if (!("error" in parsed && parsed.error)) return { kind: "query", clauses: restored };
3877
+ const restored = wqlToPills(raw);
3878
+ if (restored) return { kind: "query", pills: restored };
3879
+ const parsed = wodWikiWql.parseQuery(raw);
3880
+ if (!parsed.error && /[:{]/.test(raw)) {
3881
+ return { kind: "raw", value: raw };
3882
+ }
3883
+ if (parsed.error && /[:{]/.test(raw)) {
3855
3884
  return { kind: "invalid", reason: String(parsed.error) };
3856
3885
  }
3857
3886
  const words = raw.match(/[a-zA-Z0-9_-]+/g);
@@ -3863,42 +3892,48 @@ function WqlComposer({
3863
3892
  }
3864
3893
  return { kind: "text", value: words[0] };
3865
3894
  }, [freeText]);
3866
- const addClause = (clause) => {
3867
- const type = clause.type;
3868
- const seed = type === "time" ? "last 2w" : type === "where" ? "sum:totalVolume{} > 5000" : type === "agg" ? "sum" : type === "groupby" ? "week" : type === "rollup" ? "1w" : clause.value;
3869
- setClauses([...clauses, { ...clause, value: seed }]);
3895
+ const addPill = (pill) => {
3896
+ const type = pill.type;
3897
+ const seed = type === "time" ? "last 2w" : type === "where" ? "sum:totalVolume{} > 5000" : type === "agg" ? "sum" : type === "groupby" ? "week" : type === "rollup" ? "1w" : pill.value;
3898
+ const next = [...pills, { ...pill, value: seed }];
3899
+ setPills(next);
3900
+ emitIfControlled(next);
3870
3901
  };
3871
- const addCalc = (clause) => {
3872
- const { type, value } = clause;
3902
+ const addCalc = (pill) => {
3903
+ const { type, value } = pill;
3873
3904
  if (type === "metric") {
3874
- if (clauses.some((c) => c.type === "metric")) return;
3905
+ if (pills.some((c) => c.type === "metric")) return;
3875
3906
  const afterIdx = Math.max(
3876
- clauses.findIndex((c) => c.type === "agg"),
3877
- clauses.findIndex((c) => c.type === "source")
3907
+ pills.findIndex((c) => c.type === "agg"),
3908
+ pills.findIndex((c) => c.type === "source")
3878
3909
  );
3879
- const next2 = [...clauses];
3880
- next2.splice(afterIdx + 1, 0, makeClause("metric", value));
3881
- setClauses(next2);
3910
+ const next2 = [...pills];
3911
+ next2.splice(afterIdx + 1, 0, makePill("metric", value));
3912
+ setPills(next2);
3913
+ emitIfControlled(next2);
3882
3914
  return;
3883
3915
  }
3884
3916
  if (type !== "agg") {
3885
- addClause(clause);
3917
+ addPill(pill);
3886
3918
  return;
3887
3919
  }
3888
3920
  const agg = value || "sum";
3889
- const existingIdx = clauses.findIndex((c) => c.type === "agg");
3921
+ const existingIdx = pills.findIndex((c) => c.type === "agg");
3890
3922
  if (existingIdx >= 0) {
3891
- updateClause(existingIdx, { value: agg });
3923
+ updatePill(existingIdx, { value: agg });
3892
3924
  return;
3893
3925
  }
3894
- if (sourcePlane(clauseValue(clauses, "source", "notes")) !== "metrics") {
3895
- setClauses(pivotClauses(clauses, "metrics").map((c) => c.type === "agg" ? { ...c, value: agg } : c));
3926
+ if (sourcePlane(pillValue(pills, "source", "notes")) !== "metrics") {
3927
+ const next2 = pivotPills(pills, "metrics").map((c) => c.type === "agg" ? { ...c, value: agg } : c);
3928
+ setPills(next2);
3929
+ emitIfControlled(next2);
3896
3930
  return;
3897
3931
  }
3898
- const sourceIdx = clauses.findIndex((c) => c.type === "source");
3899
- const next = [...clauses];
3900
- next.splice(sourceIdx + 1, 0, makeClause("agg", agg));
3901
- setClauses(next);
3932
+ const sourceIdx = pills.findIndex((c) => c.type === "source");
3933
+ const next = [...pills];
3934
+ next.splice(sourceIdx + 1, 0, makePill("agg", agg));
3935
+ setPills(next);
3936
+ emitIfControlled(next);
3902
3937
  };
3903
3938
  const handleKeyDown = (e) => {
3904
3939
  if (e.key !== "Enter") return;
@@ -3906,10 +3941,15 @@ function WqlComposer({
3906
3941
  e.preventDefault();
3907
3942
  e.stopPropagation();
3908
3943
  if (pending?.kind === "query") {
3909
- setClauses(pending.clauses);
3944
+ setPills(pending.pills);
3945
+ emitIfControlled(pending.pills);
3910
3946
  setFreeText("");
3947
+ } else if (pending?.kind === "raw") {
3948
+ onSubmit?.(pending.value);
3911
3949
  } else if (pending?.kind === "text") {
3912
- setClauses([...clauses, makeClause("text", pending.value)]);
3950
+ const next = [...pills, makePill("text", pending.value)];
3951
+ setPills(next);
3952
+ emitIfControlled(next);
3913
3953
  setFreeText("");
3914
3954
  }
3915
3955
  return;
@@ -3932,20 +3972,20 @@ function WqlComposer({
3932
3972
  "data-testid": "wql-composer",
3933
3973
  children: [
3934
3974
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Command, { className: "size-4 text-amber-500 shrink-0 mr-0.5" }),
3935
- clauses.map(
3936
- (clause, idx) => hiddenTypes.has(clause.type) ? null : /* @__PURE__ */ jsxRuntime.jsx(
3975
+ pills.map(
3976
+ (pill, idx) => hiddenTypes.has(pill.type) ? null : /* @__PURE__ */ jsxRuntime.jsx(
3937
3977
  TokenSlotPill,
3938
3978
  {
3939
- clause,
3979
+ clause: pill,
3940
3980
  isActive: activeSlotIdx === idx,
3941
- invalid: diagnostics.offendingClauseId === clause.id,
3942
- invalidReason: diagnostics.offendingClauseId === clause.id ? diagnostics.error : void 0,
3981
+ invalid: diagnostics.offendingClauseId === pill.id,
3982
+ invalidReason: diagnostics.offendingClauseId === pill.id ? diagnostics.error : void 0,
3943
3983
  onClick: () => setActiveSlotIdx(idx),
3944
- onChange: (patch) => updateClause(idx, patch),
3945
- onRemove: () => removeClause(idx),
3984
+ onChange: (patch) => updatePill(idx, patch),
3985
+ onRemove: () => removePill(idx),
3946
3986
  compact: true
3947
3987
  },
3948
- clause.id
3988
+ pill.id
3949
3989
  )
3950
3990
  ),
3951
3991
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -3976,6 +4016,7 @@ function WqlComposer({
3976
4016
  role: pending.kind === "invalid" ? "alert" : void 0,
3977
4017
  children: [
3978
4018
  pending.kind === "query" && "\u21B5 Use as query",
4019
+ pending.kind === "raw" && "\u21B5 Run query",
3979
4020
  pending.kind === "text" && `\u21B5 Search text: ${pending.value}`,
3980
4021
  pending.kind === "invalid" && pending.reason
3981
4022
  ]
@@ -3988,14 +4029,14 @@ function WqlComposer({
3988
4029
  offendingLabel,
3989
4030
  stages,
3990
4031
  actions: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3991
- /* @__PURE__ */ jsxRuntime.jsx(AddCalcDropdown, { clauses, onAdd: addCalc }),
3992
- /* @__PURE__ */ jsxRuntime.jsx(AddFilterDropdown, { clauses, onAdd: addClause }),
4032
+ /* @__PURE__ */ jsxRuntime.jsx(AddCalcDropdown, { clauses: pills, onAdd: addCalc }),
4033
+ /* @__PURE__ */ jsxRuntime.jsx(AddFilterDropdown, { clauses: pills, onAdd: addPill }),
3993
4034
  diagnosticsActions
3994
4035
  ] })
3995
4036
  }
3996
4037
  ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-1.5 px-1.5", "data-testid": "wql-add-row", children: [
3997
- /* @__PURE__ */ jsxRuntime.jsx(AddCalcDropdown, { clauses, onAdd: addCalc }),
3998
- /* @__PURE__ */ jsxRuntime.jsx(AddFilterDropdown, { clauses, onAdd: addClause }),
4038
+ /* @__PURE__ */ jsxRuntime.jsx(AddCalcDropdown, { clauses: pills, onAdd: addCalc }),
4039
+ /* @__PURE__ */ jsxRuntime.jsx(AddFilterDropdown, { clauses: pills, onAdd: addPill }),
3999
4040
  diagnosticsActions
4000
4041
  ] })
4001
4042
  ] });
@@ -4009,13 +4050,10 @@ function WqlQueryInspectorModal({
4009
4050
  subtitle = "Use the Omni-Composer to edit this block query.",
4010
4051
  executor
4011
4052
  }) {
4012
- const [clauses, setClauses] = React.useState([]);
4053
+ const [wql2, setWql] = React.useState(initialQuery);
4013
4054
  const [isValid, setIsValid] = React.useState(true);
4014
4055
  React.useEffect(() => {
4015
- if (isOpen) {
4016
- const restored = wqlToClauses(initialQuery);
4017
- setClauses(restored ?? defaultMetricsClauses());
4018
- }
4056
+ if (isOpen) setWql(initialQuery);
4019
4057
  }, [isOpen, initialQuery]);
4020
4058
  const diagnosticsExecutor = React.useCallback(
4021
4059
  (ast) => {
@@ -4033,8 +4071,7 @@ function WqlQueryInspectorModal({
4033
4071
  );
4034
4072
  const handleApply = () => {
4035
4073
  if (!isValid) return;
4036
- const nextWql = clausesToWql(clauses);
4037
- onApply(nextWql);
4074
+ onApply(wql2);
4038
4075
  onClose();
4039
4076
  };
4040
4077
  if (!isOpen) return null;
@@ -4065,15 +4102,15 @@ function WqlQueryInspectorModal({
4065
4102
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 overflow-y-auto space-y-4", children: /* @__PURE__ */ jsxRuntime.jsx(
4066
4103
  WqlComposer,
4067
4104
  {
4068
- clauses,
4069
- onClausesChange: setClauses,
4105
+ query: wql2,
4106
+ onQueryChange: setWql,
4070
4107
  onValidationChange: (v) => setIsValid(v.valid),
4071
4108
  execute: diagnosticsExecutor,
4072
4109
  showDiagnostics: true
4073
4110
  }
4074
4111
  ) }),
4075
4112
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-3 border-t border-border/80 bg-muted/10", children: [
4076
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground font-mono", children: clausesToWql(clauses) }),
4113
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground font-mono", children: wql2 }),
4077
4114
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
4078
4115
  /* @__PURE__ */ jsxRuntime.jsx(
4079
4116
  "button",