@floless/app 0.15.0 → 0.16.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/floless-server.cjs +62 -5
- package/dist/web/app.css +52 -6
- package/dist/web/app.js +86 -16
- package/dist/web/aware.js +52 -3
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -50969,6 +50969,13 @@ function readApp(id) {
|
|
|
50969
50969
|
compilerVersion: typeof lockDoc["compiler-version"] === "string" ? lockDoc["compiler-version"] : null,
|
|
50970
50970
|
agentPins
|
|
50971
50971
|
};
|
|
50972
|
+
const skillCache = /* @__PURE__ */ new Map();
|
|
50973
|
+
const nodeSkill = (agent, command) => {
|
|
50974
|
+
if (!agent || !command) return null;
|
|
50975
|
+
const key = JSON.stringify([agent, command]);
|
|
50976
|
+
if (!skillCache.has(key)) skillCache.set(key, readNodeSkill(agent, command));
|
|
50977
|
+
return skillCache.get(key) ?? null;
|
|
50978
|
+
};
|
|
50972
50979
|
const srcNodes = Array.isArray(src.nodes) ? src.nodes : [];
|
|
50973
50980
|
const nodes = srcNodes.map((n) => {
|
|
50974
50981
|
const nid = String(n.id);
|
|
@@ -50984,7 +50991,8 @@ function readApp(id) {
|
|
|
50984
50991
|
mode: lockMode === "write" || lockMode === "read" ? lockMode : "unknown",
|
|
50985
50992
|
inputs: asRecord(locked?.inputs),
|
|
50986
50993
|
config: asRecord(n.config),
|
|
50987
|
-
notes: notes2
|
|
50994
|
+
notes: notes2,
|
|
50995
|
+
skill: nodeSkill(n.agent != null ? String(n.agent) : null, command)
|
|
50988
50996
|
};
|
|
50989
50997
|
});
|
|
50990
50998
|
const layout = src.layout === "dag" ? "dag" : "linear";
|
|
@@ -51038,7 +51046,7 @@ function sourceNodeId(nodes, connections) {
|
|
|
51038
51046
|
return nodes.find((n) => !hasIncoming.has(n.id))?.id ?? nodes[0].id;
|
|
51039
51047
|
}
|
|
51040
51048
|
function readCommandSpec(agent, command, agentsDir = AGENTS_DIR) {
|
|
51041
|
-
const safe = (n) =>
|
|
51049
|
+
const safe = (n) => !/[/\\\0]/.test(n) && !n.includes("..");
|
|
51042
51050
|
if (!safe(agent) || !safe(command)) return null;
|
|
51043
51051
|
const manifestPath = (0, import_node_path2.join)(agentsDir, agent, "manifest.yaml");
|
|
51044
51052
|
if (!(0, import_node_fs3.existsSync)(manifestPath)) return null;
|
|
@@ -51065,6 +51073,55 @@ function readCommandSpec(agent, command, agentsDir = AGENTS_DIR) {
|
|
|
51065
51073
|
});
|
|
51066
51074
|
return { lifecycle, streaming, inputs };
|
|
51067
51075
|
}
|
|
51076
|
+
function firstParagraph(s) {
|
|
51077
|
+
if (typeof s !== "string") return "";
|
|
51078
|
+
return (s.trim().split(/\n\s*\n/)[0] ?? "").replace(/\s+/g, " ").trim();
|
|
51079
|
+
}
|
|
51080
|
+
function parseManifestInputs(raw) {
|
|
51081
|
+
return Object.entries(asRecord(raw)).map(([name, spec]) => {
|
|
51082
|
+
const s = typeof spec === "string" ? { type: spec } : asRecord(spec);
|
|
51083
|
+
const input = {
|
|
51084
|
+
name,
|
|
51085
|
+
type: typeof s.type === "string" ? s.type : "string",
|
|
51086
|
+
default: "default" in s ? s.default : null,
|
|
51087
|
+
description: typeof s.description === "string" ? s.description : ""
|
|
51088
|
+
};
|
|
51089
|
+
if (Array.isArray(s.values)) input.values = s.values.map(String);
|
|
51090
|
+
return input;
|
|
51091
|
+
});
|
|
51092
|
+
}
|
|
51093
|
+
function readNodeSkill(agent, command, agentsDir = AGENTS_DIR) {
|
|
51094
|
+
const safe = (n) => !/[/\\\0]/.test(n) && !n.includes("..");
|
|
51095
|
+
if (!safe(agent) || !safe(command)) return null;
|
|
51096
|
+
const manifestPath = (0, import_node_path2.join)(agentsDir, agent, "manifest.yaml");
|
|
51097
|
+
if (!(0, import_node_fs3.existsSync)(manifestPath)) return null;
|
|
51098
|
+
let doc;
|
|
51099
|
+
try {
|
|
51100
|
+
doc = asRecord((0, import_yaml.parse)((0, import_node_fs3.readFileSync)(manifestPath, "utf8")));
|
|
51101
|
+
} catch {
|
|
51102
|
+
return null;
|
|
51103
|
+
}
|
|
51104
|
+
const cmd = asRecord(asRecord(doc.commands)[command]);
|
|
51105
|
+
if (Object.keys(cmd).length === 0) return null;
|
|
51106
|
+
const outRec = asRecord(cmd.outputs);
|
|
51107
|
+
const outputs = typeof outRec.type === "string" ? {
|
|
51108
|
+
type: outRec.type,
|
|
51109
|
+
schema: Object.fromEntries(
|
|
51110
|
+
Object.entries(asRecord(outRec.schema)).map(([k, v]) => [k, String(v)])
|
|
51111
|
+
)
|
|
51112
|
+
} : null;
|
|
51113
|
+
return {
|
|
51114
|
+
agent,
|
|
51115
|
+
agentDisplayName: typeof doc["display-name"] === "string" ? doc["display-name"] : agent,
|
|
51116
|
+
agentDescription: firstParagraph(doc.description),
|
|
51117
|
+
homepage: typeof doc.homepage === "string" ? doc.homepage : null,
|
|
51118
|
+
capabilities: Object.keys(asRecord(doc.requires)),
|
|
51119
|
+
command,
|
|
51120
|
+
commandDescription: firstParagraph(cmd.description),
|
|
51121
|
+
inputs: parseManifestInputs(cmd.inputs),
|
|
51122
|
+
outputs
|
|
51123
|
+
};
|
|
51124
|
+
}
|
|
51068
51125
|
function detectTriggerSource(nodes, connections, lookup) {
|
|
51069
51126
|
const srcId = sourceNodeId(nodes, connections);
|
|
51070
51127
|
if (!srcId) return null;
|
|
@@ -51096,7 +51153,7 @@ function listIntegrations() {
|
|
|
51096
51153
|
if (!(0, import_node_fs3.existsSync)(AGENTS_DIR)) return [];
|
|
51097
51154
|
const out = [];
|
|
51098
51155
|
const seen = /* @__PURE__ */ new Set();
|
|
51099
|
-
const safe = (n) =>
|
|
51156
|
+
const safe = (n) => !/[/\\\0]/.test(n) && !n.includes("..");
|
|
51100
51157
|
for (const agentId of (0, import_node_fs3.readdirSync)(AGENTS_DIR)) {
|
|
51101
51158
|
if (!safe(agentId)) continue;
|
|
51102
51159
|
const manifestPath = (0, import_node_path2.join)(AGENTS_DIR, agentId, "manifest.yaml");
|
|
@@ -52609,7 +52666,7 @@ function appVersion() {
|
|
|
52609
52666
|
return resolveVersion({
|
|
52610
52667
|
isSea: isSea2(),
|
|
52611
52668
|
sqVersionXml: readSqVersionXml(),
|
|
52612
|
-
define: true ? "0.
|
|
52669
|
+
define: true ? "0.16.1" : void 0,
|
|
52613
52670
|
pkgVersion: readPkgVersion()
|
|
52614
52671
|
});
|
|
52615
52672
|
}
|
|
@@ -52619,7 +52676,7 @@ function resolveChannel(s) {
|
|
|
52619
52676
|
return "dev";
|
|
52620
52677
|
}
|
|
52621
52678
|
function appChannel() {
|
|
52622
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
52679
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.16.1" : void 0 });
|
|
52623
52680
|
}
|
|
52624
52681
|
|
|
52625
52682
|
// oauth-presets.ts
|
package/dist/web/app.css
CHANGED
|
@@ -46,7 +46,10 @@
|
|
|
46
46
|
grid-template-rows: 60px 1fr 44px;
|
|
47
47
|
--left-width: 340px;
|
|
48
48
|
--right-width: 420px;
|
|
49
|
-
|
|
49
|
+
/* minmax(0, 1fr) (not the default minmax(auto, 1fr)) lets the center column
|
|
50
|
+
shrink below its content's min-size, so a wide center never blows the grid
|
|
51
|
+
past the viewport. See min-width:0 on the three areas below. */
|
|
52
|
+
grid-template-columns: var(--left-width) minmax(0, 1fr) var(--right-width);
|
|
50
53
|
grid-template-areas:
|
|
51
54
|
"header header header"
|
|
52
55
|
"chat canvas inspect"
|
|
@@ -104,8 +107,16 @@
|
|
|
104
107
|
justify-content: space-between;
|
|
105
108
|
padding: 0 18px;
|
|
106
109
|
border-bottom: 1px solid var(--border);
|
|
110
|
+
/* Let the header's flex children shrink rather than force the whole grid wider
|
|
111
|
+
than the viewport (the old cause of a page-wide horizontal scrollbar): the
|
|
112
|
+
workflow picker is the shrink sink — it truncates down to its 180px floor
|
|
113
|
+
while the run-spine buttons stay on one line (never wrapping). Below that
|
|
114
|
+
floor a horizontal scrollbar is the final fallback (the run spine stays
|
|
115
|
+
reachable by scrolling), at viewport widths narrower than the design (#53). */
|
|
116
|
+
min-width: 0;
|
|
107
117
|
}
|
|
108
|
-
|
|
118
|
+
/* Brand + view-toggle stay full size; .controls is the shrink sink instead. */
|
|
119
|
+
.header-left { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
|
|
109
120
|
.brand { display: flex; align-items: center; gap: 12px; }
|
|
110
121
|
.brand .mark {
|
|
111
122
|
display: inline-flex;
|
|
@@ -119,7 +130,11 @@
|
|
|
119
130
|
.brand .mark .mark-node { fill: var(--bg); }
|
|
120
131
|
.brand .name { font-size: 15px; font-weight: 700; letter-spacing: 0.08em; }
|
|
121
132
|
.brand .tag { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.12em; }
|
|
122
|
-
.controls { display: flex; align-items: center; gap: 10px; }
|
|
133
|
+
.controls { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
134
|
+
/* Action buttons keep their labels on ONE line — under a tight header they may
|
|
135
|
+
compress, but the workflow picker (the shrink sink) gives way first; a button
|
|
136
|
+
must never wrap to two lines (#53 follow-up). */
|
|
137
|
+
.controls button { white-space: nowrap; }
|
|
123
138
|
/* Vertical divider separating "which workflow + reference" from the run spine
|
|
124
139
|
(state → Compile → Simulate → Run). Uses the existing border token. */
|
|
125
140
|
.ctl-sep { width: 1px; height: 20px; background: var(--border-strong); flex: none; }
|
|
@@ -236,6 +251,7 @@
|
|
|
236
251
|
display: flex;
|
|
237
252
|
flex-direction: column;
|
|
238
253
|
overflow: hidden;
|
|
254
|
+
min-width: 0;
|
|
239
255
|
position: relative;
|
|
240
256
|
}
|
|
241
257
|
.host-banner {
|
|
@@ -326,6 +342,7 @@
|
|
|
326
342
|
display: flex;
|
|
327
343
|
flex-direction: column;
|
|
328
344
|
overflow: hidden;
|
|
345
|
+
min-width: 0;
|
|
329
346
|
position: relative;
|
|
330
347
|
}
|
|
331
348
|
/* While middle-mouse panning, force the grab cursor over the whole canvas. */
|
|
@@ -339,8 +356,16 @@
|
|
|
339
356
|
padding: 24px 36px;
|
|
340
357
|
position: relative;
|
|
341
358
|
min-height: 0;
|
|
342
|
-
|
|
359
|
+
/* This element is the transformed "world": it's translated/scaled for pan+zoom,
|
|
360
|
+
so it must NOT clip its own content — clipping here happens BEFORE the scale,
|
|
361
|
+
so zooming out would shrink an already-clipped view instead of revealing the
|
|
362
|
+
whole graph. The stable clip box is the parent `.canvas` (overflow:hidden).
|
|
363
|
+
overflow:visible also means no scrollbar — drag the background / middle-mouse
|
|
364
|
+
to pan, zoom, or Fit this infinite canvas. */
|
|
365
|
+
overflow: visible;
|
|
366
|
+
cursor: grab;
|
|
343
367
|
}
|
|
368
|
+
/* (the grabbing cursor during a pan is already forced by `.canvas.panning *` above) */
|
|
344
369
|
|
|
345
370
|
/* LINEAR */
|
|
346
371
|
.topology:not(.dag) .agent-card { flex: 0 0 210px; }
|
|
@@ -738,6 +763,7 @@
|
|
|
738
763
|
display: flex;
|
|
739
764
|
flex-direction: column;
|
|
740
765
|
overflow: hidden;
|
|
766
|
+
min-width: 0;
|
|
741
767
|
position: relative;
|
|
742
768
|
}
|
|
743
769
|
.tabs {
|
|
@@ -1416,10 +1442,16 @@
|
|
|
1416
1442
|
#rtn-delete-confirm:hover { color: var(--err); border-color: var(--err); background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
1417
1443
|
|
|
1418
1444
|
/* ========== WORKFLOW COMBOBOX (searchable, provider-grouped) ========== */
|
|
1419
|
-
|
|
1445
|
+
/* Shrinkable in the header flex row: the trigger's 300px width is the combo's
|
|
1446
|
+
preferred size (flex-basis:auto reads it), and flex-shrink gives space back
|
|
1447
|
+
down to a 180px floor when the header is tight, so the run spine fits without
|
|
1448
|
+
wrapping its buttons. The name ellipsizes within. The 300px lives on the
|
|
1449
|
+
TRIGGER (not flex-basis on the combo) so the combo's intrinsic size matches
|
|
1450
|
+
its flex size — otherwise the shrink leaks onto the buttons and wraps them. */
|
|
1451
|
+
.wf-combo { position: relative; display: inline-flex; flex: 0 1 auto; min-width: 180px; }
|
|
1420
1452
|
.wf-trigger {
|
|
1421
1453
|
display: inline-flex; align-items: center; gap: 8px;
|
|
1422
|
-
min-width: 300px; max-width:
|
|
1454
|
+
min-width: 0; width: 300px; max-width: 100%;
|
|
1423
1455
|
background: var(--surface-2); color: var(--text);
|
|
1424
1456
|
border: 1px solid var(--border-strong); border-radius: 4px;
|
|
1425
1457
|
padding: 7px 12px; font-family: var(--ui); font-size: 12px; cursor: pointer;
|
|
@@ -2041,6 +2073,20 @@ body {
|
|
|
2041
2073
|
.kv td { font-family: var(--mono); color: var(--text); word-break: break-word; }
|
|
2042
2074
|
.kv tr + tr th, .kv tr + tr td { border-top: 1px solid var(--border); }
|
|
2043
2075
|
|
|
2076
|
+
/* Skill tab (Inspect): capability/mode pills mirror the notes-strip .note-kind
|
|
2077
|
+
pill, and the agent docs link uses the accent — existing tokens only, no new
|
|
2078
|
+
palette (issue #54). */
|
|
2079
|
+
.inspect-content .cap-badge {
|
|
2080
|
+
display: inline-block; font-size: 9px; text-transform: uppercase; letter-spacing: 0.06em;
|
|
2081
|
+
color: var(--info); border: 1px solid color-mix(in srgb, var(--info) 35%, transparent);
|
|
2082
|
+
border-radius: 3px; padding: 0 5px; margin: 0 2px; vertical-align: 1px;
|
|
2083
|
+
}
|
|
2084
|
+
.inspect-content .cap-badge.write { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
|
|
2085
|
+
/* Breathing room above the docs link — it follows a schema table (no margin-bottom). */
|
|
2086
|
+
.inspect-content .skill-docs-row { margin-top: 8px; }
|
|
2087
|
+
.inspect-content .skill-docs { color: var(--accent); text-decoration: none; }
|
|
2088
|
+
.inspect-content .skill-docs:hover { text-decoration: underline; }
|
|
2089
|
+
|
|
2044
2090
|
/* ── HTML Viewer (report) modal ─────────────────────────────────────────────── */
|
|
2045
2091
|
.modal.report-viewer {
|
|
2046
2092
|
width: 1180px; max-width: 94vw; height: 86vh;
|
package/dist/web/app.js
CHANGED
|
@@ -226,7 +226,7 @@ function renderTopology() {
|
|
|
226
226
|
} else {
|
|
227
227
|
selectAgent(state.selectedAgentId);
|
|
228
228
|
}
|
|
229
|
-
|
|
229
|
+
fitView(); // auto-fit the whole workflow into the canvas per render (never clipped)
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
function renderLinearTopology(p) {
|
|
@@ -354,8 +354,18 @@ function cardEl(id, ports) {
|
|
|
354
354
|
`;
|
|
355
355
|
div.onclick = (e) => {
|
|
356
356
|
if (e.target.closest('.fav-btn')) return;
|
|
357
|
+
// A plain card click only SELECTS. Revealing a collapsed Inspect panel is
|
|
358
|
+
// reserved for the explicit "inspect ▸" affordance (handler below) — a general
|
|
359
|
+
// node click must not pop open a panel the user deliberately collapsed (#52).
|
|
357
360
|
selectAgent(id);
|
|
358
361
|
};
|
|
362
|
+
// The "inspect ▸" hint is the explicit inspect affordance: select AND reveal the
|
|
363
|
+
// Inspect panel if it's collapsed. stopPropagation so the card's plain-select
|
|
364
|
+
// onclick doesn't also fire (it would just re-select the same node).
|
|
365
|
+
div.querySelector('.inspect-hint').onclick = (e) => {
|
|
366
|
+
e.stopPropagation();
|
|
367
|
+
selectAgent(id, { reveal: true });
|
|
368
|
+
};
|
|
359
369
|
div.querySelector('.fav-btn').onclick = (e) => {
|
|
360
370
|
e.stopPropagation();
|
|
361
371
|
toggleFav(id);
|
|
@@ -379,13 +389,26 @@ function wireEl(idx, label) {
|
|
|
379
389
|
return wrap;
|
|
380
390
|
}
|
|
381
391
|
|
|
382
|
-
|
|
392
|
+
// `reveal` is set by USER-initiated selections (clicking a node card / the
|
|
393
|
+
// "inspect ▸" hint / a favorite chip). When the Inspect panel is collapsed those
|
|
394
|
+
// would otherwise give zero visible feedback — the panel quietly re-renders behind
|
|
395
|
+
// the rail (issue #52) — so a revealing selection also expands it. Programmatic /
|
|
396
|
+
// initial selection (on render) passes no reveal, so a saved collapsed preference
|
|
397
|
+
// is respected on load.
|
|
398
|
+
function selectAgent(id, { reveal = false } = {}) {
|
|
383
399
|
state.selectedAgentId = id;
|
|
384
400
|
document.querySelectorAll('.agent-card').forEach(c => {
|
|
385
401
|
c.classList.toggle('selected', c.dataset.agentId === id);
|
|
386
402
|
});
|
|
387
403
|
const a = AGENTS[id];
|
|
388
404
|
if (a) $inspectRole.textContent = a.title.toLowerCase();
|
|
405
|
+
if (reveal && state.collapse.right) {
|
|
406
|
+
// Momentary reveal — expand the panel for THIS interaction but do NOT persist:
|
|
407
|
+
// the user collapsed it deliberately, so a single inspect must not overwrite
|
|
408
|
+
// that saved preference (only an explicit toggle/drag does). UX review on #52.
|
|
409
|
+
state.collapse.right = false;
|
|
410
|
+
applyCollapse();
|
|
411
|
+
}
|
|
389
412
|
renderInspect();
|
|
390
413
|
}
|
|
391
414
|
|
|
@@ -624,7 +647,7 @@ function renderFavBar() {
|
|
|
624
647
|
const id = chip.dataset.favAgent;
|
|
625
648
|
const p = PROMPTS[state.promptKey];
|
|
626
649
|
if (nodeIds(p).includes(id)) {
|
|
627
|
-
selectAgent(id);
|
|
650
|
+
selectAgent(id, { reveal: true });
|
|
628
651
|
} else {
|
|
629
652
|
chip.style.borderColor = 'var(--warn)';
|
|
630
653
|
setTimeout(() => chip.style.borderColor = '', 600);
|
|
@@ -1154,8 +1177,16 @@ function showToast(msg, type) {
|
|
|
1154
1177
|
const ZOOM_LEVELS = [0.5, 0.6, 0.75, 0.85, 1.0, 1.15, 1.3, 1.5, 1.75, 2.0];
|
|
1155
1178
|
const MIN_ZOOM = ZOOM_LEVELS[0];
|
|
1156
1179
|
const MAX_ZOOM = ZOOM_LEVELS[ZOOM_LEVELS.length - 1];
|
|
1180
|
+
// Auto-fit/Fit may zoom further out than the interactive MIN_ZOOM so a large graph
|
|
1181
|
+
// in a tight canvas is shown whole rather than floored and cut. Floored at 20% for
|
|
1182
|
+
// readability — below that, hold here and let the user pan (never scale to noise).
|
|
1183
|
+
const FIT_MIN_ZOOM = 0.2;
|
|
1157
1184
|
let zoom = 1; // continuous scale (fit produces arbitrary values, not just presets)
|
|
1158
1185
|
let panX = 0, panY = 0; // pan offset in screen px (transform-origin is the canvas centre)
|
|
1186
|
+
// The view auto-fits the WHOLE workflow into the canvas (so nodes are never clipped)
|
|
1187
|
+
// and keeps re-fitting as the canvas resizes (panels toggle, window resize) — UNTIL
|
|
1188
|
+
// the user takes manual control (zoom / pan / 100%), after which their view is kept.
|
|
1189
|
+
let autoFit = true;
|
|
1159
1190
|
|
|
1160
1191
|
function applyTransform() {
|
|
1161
1192
|
$topology.style.transformOrigin = '50% 50%';
|
|
@@ -1176,17 +1207,19 @@ function zoomBy(delta) {
|
|
|
1176
1207
|
: ([...ZOOM_LEVELS].reverse().find((z) => z < zoom - 1e-3) ?? MIN_ZOOM);
|
|
1177
1208
|
if (Math.abs(next - zoom) < 1e-3) return;
|
|
1178
1209
|
zoom = next;
|
|
1210
|
+
autoFit = false; // user chose a zoom — stop auto-fitting on resize
|
|
1179
1211
|
applyTransform();
|
|
1180
1212
|
}
|
|
1181
1213
|
|
|
1182
|
-
// Reset to 100% + recentre.
|
|
1183
|
-
// matches the transform it clears (otherwise the % label would go stale on switch).
|
|
1214
|
+
// Reset to 100% + recentre. Pure — callers decide whether it's a user action.
|
|
1184
1215
|
function resetView() { zoom = 1; panX = 0; panY = 0; applyTransform(); }
|
|
1185
1216
|
|
|
1186
1217
|
// Fit the whole workflow into the visible canvas (zooms in OR out), then recentre.
|
|
1187
1218
|
// Measures the real node cluster — offset* are unscaled layout coords, so they're
|
|
1188
|
-
// unaffected by the current transform.
|
|
1189
|
-
|
|
1219
|
+
// unaffected by the current transform. `maxZoom` caps the scale: the auto-fit on
|
|
1220
|
+
// load/resize passes 1 so a small workflow stays 1:1 (never blown up); the explicit
|
|
1221
|
+
// Fit button passes the full range (may zoom in to fill).
|
|
1222
|
+
function fitToScreen(maxZoom = MAX_ZOOM) {
|
|
1190
1223
|
const cards = [...$topology.querySelectorAll('.agent-card')];
|
|
1191
1224
|
if (!cards.length) { resetView(); return; }
|
|
1192
1225
|
let minL = Infinity, minT = Infinity, maxR = -Infinity, maxB = -Infinity;
|
|
@@ -1200,16 +1233,27 @@ function fitToScreen() {
|
|
|
1200
1233
|
if (contentW <= 0 || contentH <= 0) { resetView(); return; }
|
|
1201
1234
|
const pad = 40;
|
|
1202
1235
|
const fit = Math.min(($topology.clientWidth - pad * 2) / contentW, ($topology.clientHeight - pad * 2) / contentH);
|
|
1203
|
-
|
|
1204
|
-
|
|
1236
|
+
// Floor at FIT_MIN_ZOOM (below the interactive MIN_ZOOM) so a big graph in a
|
|
1237
|
+
// tight canvas is fully visible, never clipped — that's the whole point of Fit.
|
|
1238
|
+
zoom = Math.max(FIT_MIN_ZOOM, Math.min(maxZoom, fit));
|
|
1239
|
+
// Centre the content's bounding box in the viewport. transform-origin is the
|
|
1240
|
+
// topology centre and translate sits OUTSIDE the scale, so pan = (viewport
|
|
1241
|
+
// centre − content centre) × zoom. Required because the DAG grid sits top-left
|
|
1242
|
+
// in the topology — a 0 pan would leave it off-centre and clip the edge nodes.
|
|
1243
|
+
panX = ($topology.clientWidth / 2 - (minL + maxR) / 2) * zoom;
|
|
1244
|
+
panY = ($topology.clientHeight / 2 - (minT + maxB) / 2) * zoom;
|
|
1205
1245
|
applyTransform();
|
|
1206
1246
|
}
|
|
1207
1247
|
|
|
1248
|
+
// Auto-fit used on render + canvas resize: show the whole workflow (capped at 100%
|
|
1249
|
+
// so small graphs aren't blown up) and stay in auto-fit mode so it tracks resizes.
|
|
1250
|
+
function fitView() { fitToScreen(1); autoFit = true; }
|
|
1251
|
+
|
|
1208
1252
|
document.getElementById('zoom-out').onclick = () => zoomBy(-1);
|
|
1209
1253
|
document.getElementById('zoom-in').onclick = () => zoomBy(+1);
|
|
1210
|
-
document.getElementById('zoom-reset').onclick = () => resetView();
|
|
1254
|
+
document.getElementById('zoom-reset').onclick = () => { resetView(); autoFit = false; };
|
|
1211
1255
|
const $zoomFit = document.getElementById('zoom-fit');
|
|
1212
|
-
if ($zoomFit) $zoomFit.onclick = () => fitToScreen();
|
|
1256
|
+
if ($zoomFit) $zoomFit.onclick = () => { fitToScreen(); autoFit = true; };
|
|
1213
1257
|
|
|
1214
1258
|
// Ctrl/Cmd + scroll wheel = zoom on canvas (skips OS-level browser zoom)
|
|
1215
1259
|
let zoomAccum = 0;
|
|
@@ -1228,18 +1272,32 @@ document.querySelector('.canvas').addEventListener('wheel', (e) => {
|
|
|
1228
1272
|
// cursor) because translate sits OUTSIDE the scale in the transform list.
|
|
1229
1273
|
const $canvasEl = document.querySelector('.canvas');
|
|
1230
1274
|
let panning = false, panFromX = 0, panFromY = 0, panBaseX = 0, panBaseY = 0;
|
|
1231
|
-
|
|
1232
|
-
if (e.button !== 1) return;
|
|
1233
|
-
e.preventDefault();
|
|
1275
|
+
function startPan(e) {
|
|
1234
1276
|
panning = true;
|
|
1235
1277
|
panFromX = e.clientX; panFromY = e.clientY;
|
|
1236
1278
|
panBaseX = panX; panBaseY = panY;
|
|
1237
1279
|
$canvasEl.classList.add('panning');
|
|
1280
|
+
}
|
|
1281
|
+
// Middle-mouse drag anywhere on the canvas pans (the classic gesture).
|
|
1282
|
+
$canvasEl.addEventListener('mousedown', (e) => {
|
|
1283
|
+
if (e.button !== 1) return;
|
|
1284
|
+
e.preventDefault();
|
|
1285
|
+
startPan(e);
|
|
1286
|
+
});
|
|
1287
|
+
// Left-drag the empty canvas background also pans — so the infinite, scrollbar-less
|
|
1288
|
+
// canvas is navigable with any pointer (trackpads have no middle button). Excludes
|
|
1289
|
+
// node cards (those keep click-to-select + their HTML5 drag); the toolbar/hint/
|
|
1290
|
+
// fav-bar/find aren't inside .topology, so they're naturally excluded.
|
|
1291
|
+
$topology.addEventListener('mousedown', (e) => {
|
|
1292
|
+
if (e.button !== 0 || e.target.closest('.agent-card')) return;
|
|
1293
|
+
e.preventDefault();
|
|
1294
|
+
startPan(e);
|
|
1238
1295
|
});
|
|
1239
1296
|
window.addEventListener('mousemove', (e) => {
|
|
1240
1297
|
if (!panning) return;
|
|
1241
1298
|
panX = panBaseX + (e.clientX - panFromX);
|
|
1242
1299
|
panY = panBaseY + (e.clientY - panFromY);
|
|
1300
|
+
autoFit = false; // user dragged the view — stop auto-fitting on resize
|
|
1243
1301
|
applyTransform();
|
|
1244
1302
|
});
|
|
1245
1303
|
window.addEventListener('mouseup', () => {
|
|
@@ -1249,6 +1307,18 @@ window.addEventListener('mouseup', () => {
|
|
|
1249
1307
|
});
|
|
1250
1308
|
$canvasEl.addEventListener('auxclick', (e) => { if (e.button === 1) e.preventDefault(); });
|
|
1251
1309
|
|
|
1310
|
+
// Keep the auto-fitted view fitted as the canvas resizes (a panel toggling/dragging,
|
|
1311
|
+
// the window resizing) — but never override a view the user has zoomed/panned. A
|
|
1312
|
+
// trailing debounce fires ONE clean re-fit after the resize settles (rather than on
|
|
1313
|
+
// every frame of a panel's 0.25s slide, which reads as jitter). The transform
|
|
1314
|
+
// fitToScreen sets doesn't change layout size, so this can't loop.
|
|
1315
|
+
let _fitTimer = 0;
|
|
1316
|
+
new ResizeObserver(() => {
|
|
1317
|
+
if (!autoFit) return;
|
|
1318
|
+
clearTimeout(_fitTimer);
|
|
1319
|
+
_fitTimer = setTimeout(() => { if (autoFit) fitToScreen(1); }, 120);
|
|
1320
|
+
}).observe($canvasEl);
|
|
1321
|
+
|
|
1252
1322
|
/* ============= INTEGRATIONS ============= */
|
|
1253
1323
|
// The real, server-backed Integrations window lives in aware.js (openIntegrations/
|
|
1254
1324
|
// renderIntegrations are reassigned there from /api/integrations). app.js only owns
|
|
@@ -1279,8 +1349,8 @@ document.addEventListener('keydown', (e) => {
|
|
|
1279
1349
|
if (cmd && e.key.toLowerCase() === 'i') { e.preventDefault(); openIntegrations(); return; }
|
|
1280
1350
|
if (cmd && (e.key === '=' || e.key === '+')) { e.preventDefault(); zoomBy(+1); return; }
|
|
1281
1351
|
if (cmd && e.key === '-') { e.preventDefault(); zoomBy(-1); return; }
|
|
1282
|
-
if (cmd && e.key === '0') { e.preventDefault(); resetView(); return; }
|
|
1283
|
-
if (e.key === 'Home') { e.preventDefault(); fitToScreen(); return; }
|
|
1352
|
+
if (cmd && e.key === '0') { e.preventDefault(); resetView(); autoFit = false; return; }
|
|
1353
|
+
if (e.key === 'Home') { e.preventDefault(); fitToScreen(); autoFit = true; return; }
|
|
1284
1354
|
if (e.key === 'Escape') {
|
|
1285
1355
|
if ($findOverlay.classList.contains('show')) closeFind();
|
|
1286
1356
|
if ($integrationsModal.classList.contains('show')) $integrationsModal.classList.remove('show');
|
package/dist/web/aware.js
CHANGED
|
@@ -144,6 +144,47 @@
|
|
|
144
144
|
.join('')}</table>`;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
// Plain-text summary of one declared command input (for the Skill tab's inputs
|
|
148
|
+
// table — kvTable escapes it, so this stays text, no markup): "type (enum vals)
|
|
149
|
+
// · default X — description".
|
|
150
|
+
function inputSummary(i) {
|
|
151
|
+
let s = i.type || 'string';
|
|
152
|
+
if (i.values && i.values.length) s += ` (${i.values.join(' · ')})`;
|
|
153
|
+
if (i.default !== null && i.default !== undefined && i.default !== '') {
|
|
154
|
+
s += ` · default ${typeof i.default === 'string' ? i.default : JSON.stringify(i.default)}`;
|
|
155
|
+
}
|
|
156
|
+
if (i.description) s += ` — ${i.description}`;
|
|
157
|
+
return s;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Inspect → Skill tab body. Rich when the agent manifest is installed (n.skill):
|
|
161
|
+
// what the agent + command do, capabilities, the command's DECLARED inputs and
|
|
162
|
+
// output schema, and a docs link. Falls back to the lock-pointer summary for
|
|
163
|
+
// exec/agent-less nodes or an uninstalled agent. DISPLAY of manifest data only —
|
|
164
|
+
// every file-derived string is escaped (kvTable/escapeHtml), never injected raw.
|
|
165
|
+
function skillBody(n, fallback, notesHtml) {
|
|
166
|
+
const sk = n.skill;
|
|
167
|
+
if (!sk) return fallback; // fallback already carries notesHtml
|
|
168
|
+
const modeBadge = `<span class="cap-badge${n.mode === 'write' ? ' write' : ''}">${escapeHtml(n.mode)}</span>`;
|
|
169
|
+
const capBadges = sk.capabilities.map((c) => `<span class="cap-badge">${escapeHtml(c)}</span>`).join(' ');
|
|
170
|
+
const inputsObj = Object.fromEntries(sk.inputs.map((i) => [i.name, inputSummary(i)]));
|
|
171
|
+
const docs = sk.homepage && /^https?:\/\//i.test(sk.homepage)
|
|
172
|
+
? `<p class="skill-docs-row"><a class="skill-docs" href="${escapeHtml(sk.homepage)}" target="_blank" rel="noopener">Agent documentation ↗</a></p>`
|
|
173
|
+
: '';
|
|
174
|
+
const outputs = sk.outputs
|
|
175
|
+
? `<p><strong>Returns</strong> · ${escapeHtml(sk.outputs.type)}</p>${Object.keys(sk.outputs.schema).length ? kvTable(sk.outputs.schema) : ''}`
|
|
176
|
+
: '';
|
|
177
|
+
return `
|
|
178
|
+
<h3>${escapeHtml(sk.agentDisplayName)}</h3>
|
|
179
|
+
${sk.agentDescription ? `<p>${escapeHtml(sk.agentDescription)}</p>` : ''}
|
|
180
|
+
<h3>Command · <code>${escapeHtml(sk.command)}</code></h3>
|
|
181
|
+
${sk.commandDescription ? `<p>${escapeHtml(sk.commandDescription)}</p>` : ''}
|
|
182
|
+
<p><strong>Mode</strong> ${modeBadge}${capBadges ? ' · <strong>Requires</strong> ' + capBadges : ''}</p>
|
|
183
|
+
<p><strong>Accepts</strong></p>${Object.keys(inputsObj).length ? kvTable(inputsObj) : '<p class="dim-note">No declared inputs.</p>'}
|
|
184
|
+
${outputs}
|
|
185
|
+
${docs}${notesHtml || ''}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
147
188
|
// Dependency-free C# highlighter for the Code tab. Single-pass scanner that
|
|
148
189
|
// handles comments/strings/chars BEFORE keywords (so keywords inside strings
|
|
149
190
|
// aren't recolored), escapes every token's text, and reuses the demo's token
|
|
@@ -250,8 +291,14 @@
|
|
|
250
291
|
subtitle: `${agentLabel}${cmd ? '/' + cmd : ''} · ${mode}`,
|
|
251
292
|
blurb: n.notes[0] ? escapeHtml(humanizeNote(n.notes[0].text)) : `${mode}-mode ${cmd ? '<code>' + cmd + '</code>' : escapeHtml(n.kind)} node`,
|
|
252
293
|
description: `${plainDesc ? `<p>${escapeHtml(plainDesc)}</p>` : ''}<p>Resolves to ${modeBadge} via <code>${escapeHtml(n.agent || n.kind)}${n.command ? '.' + escapeHtml(n.command) : ''}</code>.</p>${Object.keys(inputsNoCode).length ? `<p><strong>Inputs</strong></p>${kvTable(inputsNoCode)}` : ''}${execSource ? `<p class="dim-note">Full source in the <strong>Code</strong> tab.</p>` : ''}${notesHtml}`,
|
|
253
|
-
|
|
294
|
+
// Rich agent/command detail when the manifest is installed (n.skill); else
|
|
295
|
+
// the lock-pointer fallback below (exec/agent-less nodes, uninstalled agents).
|
|
296
|
+
skill: skillBody(
|
|
297
|
+
n,
|
|
298
|
+
`<h3>Provided by</h3><p><code>${escapeHtml(n.agent || n.kind)}</code>${pin ? ' · pinned <code>v' + escapeHtml(pin) + '</code>' : ''}${n.command ? ' · command <code>' + escapeHtml(n.command) + '</code>' : ''}</p>
|
|
254
299
|
<p>Resolved write/read mode is part of the approved <code>.lock</code> — see the <strong>Code</strong> tab.</p>${notesHtml}`,
|
|
300
|
+
notesHtml,
|
|
301
|
+
),
|
|
255
302
|
// Code tab: the REAL node source. For exec nodes (Roslyn C# run against a
|
|
256
303
|
// live host) that's `config.code` straight from the .flo — the same text
|
|
257
304
|
// the host compiles, so "Debug in VS" attaches to exactly this. For
|
|
@@ -3295,8 +3342,10 @@
|
|
|
3295
3342
|
// button only for exec nodes (those carry C# the host can run under a debugger).
|
|
3296
3343
|
// Debug lives HERE in the main window, not in the report modal.
|
|
3297
3344
|
const _selectAgent = selectAgent;
|
|
3298
|
-
|
|
3299
|
-
|
|
3345
|
+
// Forward ALL args (id + the { reveal } opts from app.js) — dropping them here
|
|
3346
|
+
// silently swallowed the collapsed-panel reveal on inspect (issue #52).
|
|
3347
|
+
selectAgent = function selectAgentTweakAware(id, ...rest) {
|
|
3348
|
+
_selectAgent(id, ...rest);
|
|
3300
3349
|
const tb = document.getElementById('tweak-btn');
|
|
3301
3350
|
if (tb) tb.hidden = !id;
|
|
3302
3351
|
const db = document.getElementById('debug-btn');
|
package/dist/web/index.html
CHANGED
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
~/.floless/ui/extensions.json here; the canvas children hide via
|
|
115
115
|
.canvas.view-dashboard). Composed by the terminal AI, rendered by us. -->
|
|
116
116
|
<div class="dashboard" id="dashboard" hidden></div>
|
|
117
|
-
<div class="hint" id="canvas-hint">Click any node to inspect. Star ★ a node to save it as a reusable Template.</div>
|
|
117
|
+
<div class="hint" id="canvas-hint">Click any node to inspect. Star ★ a node to save it as a reusable Template. Drag the background to pan — or press Home to fit.</div>
|
|
118
118
|
<div class="fav-bar" id="fav-bar">
|
|
119
119
|
<div class="fav-bar-label"><span class="star">★</span><span>Templates</span></div>
|
|
120
120
|
<div class="fav-chip-row" id="fav-chip-row"></div>
|