@foldspace_npm/harness 0.1.16 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +48 -18
- package/README.md +1 -1
- package/bin/attach.mjs +46 -18
- package/bin/badge.mjs +50 -0
- package/bin/inject.mjs +2 -2
- package/package.json +1 -1
- package/recipes/INDEX.md +1 -0
- package/recipes/bottom-bar/README.md +43 -0
- package/recipes/bottom-bar/agent/bottomBar.ts +94 -0
- package/recipes/bottom-bar/fixtures/configuration.sent.json +17 -0
- package/recipes/bottom-bar/recipe.json +9 -0
- package/src/badge-core.mjs +22 -0
- package/src/cli-registry.mjs +32 -3
package/CLAUDE.md
CHANGED
|
@@ -71,11 +71,15 @@ current CLI contract — risk, prerequisites, effects, next step.
|
|
|
71
71
|
so and offer the next one. On an empty account the useful first action is
|
|
72
72
|
one that creates data - say so instead.
|
|
73
73
|
|
|
74
|
-
**Say when the test window is yours, and when it is theirs
|
|
75
|
-
human share one Chrome window. Before you start looking
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
**Say when the test window is yours, and when it is theirs - on the page and
|
|
75
|
+
in chat.** You and the human share one Chrome window. Before you start looking
|
|
76
|
+
at the app or testing in it, run `npx foldspace badge working` (a bar across
|
|
77
|
+
the top of the window says Foldspace is building here, leave it alone) and say
|
|
78
|
+
so in chat: *"I'm going to work in that Chrome window for a few minutes -
|
|
79
|
+
please leave it alone until I tell you it's ready."* When it is their turn,
|
|
80
|
+
`npx foldspace badge ready --text "Try asking: <the question this experience
|
|
81
|
+
answers>"` and the same line in chat. `foldspace badge label` puts the small
|
|
82
|
+
corner tag back; `off` removes it, for a screenshot they will share.
|
|
79
83
|
|
|
80
84
|
Navigation, uploads, extraction and anything that writes are a later session.
|
|
81
85
|
|
|
@@ -207,11 +211,17 @@ with how you established each.
|
|
|
207
211
|
| Navigation route | **yes** | `isActive: true` |
|
|
208
212
|
|
|
209
213
|
Publishing a **new** object is inert; publishing an **edit to a live one** is
|
|
210
|
-
the deploy above. Navigation has
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
the deploy above. **Navigation has two switches, and both must be on.** Each
|
|
215
|
+
route has its own `isActive`, set when you create it (`create_navigation_route`
|
|
216
|
+
/ `bulk_create_navigation_routes`, `isActive: true`). The agent has one
|
|
217
|
+
agent-level switch, **off by default on a new subscription**: `publish_navigation`
|
|
218
|
+
turns it on, `unpublish_navigation` off, and neither touches the routes' own
|
|
219
|
+
`isActive`. `list_navigation_routes` shows both. There is no MCP tool to flip a
|
|
220
|
+
single route's `isActive` after creation (it was disabled until Agent Studio
|
|
221
|
+
can reverse it) - create it right, or `update_navigation_route`, which replaces
|
|
222
|
+
the whole route. Route descriptions are a retrieval surface: write what the
|
|
223
|
+
page is *for*, in the user's words, and pair every parameterised route with the
|
|
224
|
+
lookup action that produces its parameter — or ship neither.
|
|
215
225
|
|
|
216
226
|
**Ask before publishing.** It is required only so the copilot can call the
|
|
217
227
|
action, and it is a live product change when the agent has real users.
|
|
@@ -247,6 +257,23 @@ copilot, read the daemon log for `[actions]` SDK callback and local
|
|
|
247
257
|
execute/render lines. Those record names, statuses, durations and parameter keys
|
|
248
258
|
only — never results or error bodies.
|
|
249
259
|
|
|
260
|
+
## Done, and what you say at the end
|
|
261
|
+
|
|
262
|
+
A first session is done when the customer has seen one experience answer from
|
|
263
|
+
their own data, in the test window, and been invited to try it. Not when the
|
|
264
|
+
gates pass, and not when there is nothing left to improve.
|
|
265
|
+
|
|
266
|
+
If they ask *"how do I put this in front of my users?"*, the honest answer is:
|
|
267
|
+
|
|
268
|
+
> What you've seen runs in a test window on your machine. Putting it in front
|
|
269
|
+
> of your users is a separate step, and the Foldspace team does it with you.
|
|
270
|
+
> Reach them through the website - <https://foldspace.ai/demo> - or at
|
|
271
|
+
> <support@foldspace.ai>. Everything built today carries over: the actions are
|
|
272
|
+
> already in your Foldspace account, and the code is in this folder.
|
|
273
|
+
|
|
274
|
+
Never run `foldspace deploy`, never install cloud tooling, never ask them for
|
|
275
|
+
cloud credentials: `deploy` is Foldspace-internal and no customer can run it.
|
|
276
|
+
|
|
250
277
|
## Verification gates
|
|
251
278
|
|
|
252
279
|
Do not report success without all six:
|
|
@@ -288,14 +315,17 @@ Do not report success without all six:
|
|
|
288
315
|
MCP (`search_docs`, `fetch_page`) before asserting how the platform behaves;
|
|
289
316
|
cite the page, or say plainly that the docs do not cover it and write down
|
|
290
317
|
what you learned.
|
|
291
|
-
4. **
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
318
|
+
4. **Test through the agent, then let them.** In a first session, talk to the
|
|
319
|
+
agent yourself to prove an experience - type the real question into its
|
|
320
|
+
panel in the test window and read the reply - and only then invite the
|
|
321
|
+
customer to try it. Those conversations appear in their Foldspace dashboard,
|
|
322
|
+
and that is wanted: seeing their own conversations recorded is part of what
|
|
323
|
+
they came to see. Never pass `--test-mode` in a first session.
|
|
324
|
+
5. **Test mode is for an agent that already has real users.** Then `attach
|
|
325
|
+
--test-mode` keeps your conversations out of their data. It is not a safety
|
|
326
|
+
net: publishing still changes the live agent. Working on an agent in
|
|
327
|
+
production - test mode, pinned action versions, what may be published - is
|
|
328
|
+
a separate playbook, not this one.
|
|
299
329
|
6. **The human is not necessarily a developer.** Never hand over a terminal
|
|
300
330
|
line, a stack trace, or a choice between flags. Name experiences by outcome
|
|
301
331
|
— *"show my plan and credits"*, not `get_account_status`. Their two jobs:
|
package/README.md
CHANGED
|
@@ -252,7 +252,7 @@ and **neither had all five**:
|
|
|
252
252
|
| SDK load guard + retry | On an app with no `document.body` at document-start the append throws; the old guard tested for the stub, so nothing retried and the agent silently never loaded |
|
|
253
253
|
| Worker release on auto-attach | Auto-attach pauses every worker until the attaching client releases it — the harness froze the app's own workers for the whole session |
|
|
254
254
|
| `agentIds` enumeration | `agent({apiName})` returns the OVERLAY handle; arming it on an embedded copilot leaves real conversations untagged |
|
|
255
|
-
| `--
|
|
255
|
+
| `--test-mode` (off by default) | Test mode was armed unconditionally, hiding exactly what initial setup needs to see; now it is opt-in |
|
|
256
256
|
| Honest badge | It asserted `TEST MODE` whether or not test mode was on |
|
|
257
257
|
|
|
258
258
|
Two further changes came from packaging it: the bundle is **injected** rather
|
package/bin/attach.mjs
CHANGED
|
@@ -135,11 +135,13 @@ try {
|
|
|
135
135
|
}
|
|
136
136
|
const bootstrap = attachMode === ATTACH_MODES.BOOTSTRAP;
|
|
137
137
|
const replace = attachMode === ATTACH_MODES.REPLACE;
|
|
138
|
-
// Test mode keeps conversations out of the customer's default list.
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
138
|
+
// Test mode keeps conversations out of the customer's default list. It is OFF
|
|
139
|
+
// by default: in a first session the customer's own conversations appearing in
|
|
140
|
+
// their dashboard is the point - they see what Foldspace records - and an
|
|
141
|
+
// orange TEST MODE label would imply a safety (nothing published) that does
|
|
142
|
+
// not exist. --test-mode opts in, for an agent that already has real users.
|
|
143
|
+
// --no-test-mode is still accepted so older instructions keep working.
|
|
144
|
+
const noTestMode = !process.argv.includes("--test-mode");
|
|
143
145
|
// The badge used to hardcode "TEST MODE" whether or not test mode was on — a
|
|
144
146
|
// string that asserts you are safe while you are not. It now states what is
|
|
145
147
|
// actually true. --no-badge drops it entirely, for recording a demo.
|
|
@@ -171,8 +173,8 @@ const agentKey = cfgTarget.overrideKey || `EU-${productId}-1-1`;
|
|
|
171
173
|
const sdkUrl = cfgTarget.sdkUrl || cfg.sdkUrl;
|
|
172
174
|
const actionTarget = { productId, agentApiName };
|
|
173
175
|
const badgeText = noTestMode
|
|
174
|
-
? `FOLDSPACE
|
|
175
|
-
: `FOLDSPACE
|
|
176
|
+
? `FOLDSPACE AGENT BUILDER \u00b7 ${agentApiName} \u00b7 ${attachMode} \u00b7 LIVE`
|
|
177
|
+
: `FOLDSPACE AGENT BUILDER \u00b7 ${agentApiName} \u00b7 ${attachMode} \u00b7 TEST MODE`;
|
|
176
178
|
|
|
177
179
|
// Refuse to run against an uninitialised template. Without this, attach injects
|
|
178
180
|
// the literal placeholder as an agent api name and the page fails with an opaque
|
|
@@ -218,19 +220,45 @@ const SDK_PATTERNS = [
|
|
|
218
220
|
"*://*/*eucera.js*",
|
|
219
221
|
];
|
|
220
222
|
|
|
223
|
+
// The badge has three states, set from outside with \`foldspace badge\`:
|
|
224
|
+
// label - the small corner tag (default): who is attached, which mode
|
|
225
|
+
// working - a bar across the top: the agent is building in this window,
|
|
226
|
+
// please leave it alone
|
|
227
|
+
// ready - a bar: it is the human's turn, with the question to try
|
|
228
|
+
// The state lives in sessionStorage so it survives navigation inside the
|
|
229
|
+
// test window; \`foldspace badge\` writes it and the script redraws.
|
|
221
230
|
const badgeSrc = `(() => {
|
|
222
231
|
if (window.top !== window.self) return;
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
b.style.cssText = "position:fixed;top:0;left:0;z-index:2147483647;background:#3247F2;color:#fff;font:700 10px/1 ui-monospace,Menlo,monospace;letter-spacing:.12em;padding:5px 10px;border-bottom-right-radius:4px;pointer-events:none;box-shadow:0 1px 6px rgba(0,0,0,.35)";
|
|
229
|
-
document.body.appendChild(b);
|
|
232
|
+
const LABEL = ${JSON.stringify(badgeText)};
|
|
233
|
+
const KEY = "__foldspace_badge__";
|
|
234
|
+
const read = () => {
|
|
235
|
+
try { return JSON.parse(sessionStorage.getItem(KEY) || "null") || { state: "label" }; }
|
|
236
|
+
catch (e) { return { state: "label" }; }
|
|
230
237
|
};
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
238
|
+
const draw = () => {
|
|
239
|
+
if (!document.body) return;
|
|
240
|
+
const s = read();
|
|
241
|
+
if (s.state === "off") { document.getElementById("foldspace-dev-badge")?.remove(); return; }
|
|
242
|
+
let b = document.getElementById("foldspace-dev-badge");
|
|
243
|
+
if (!b) { b = document.createElement("div"); b.id = "foldspace-dev-badge"; document.body.appendChild(b); }
|
|
244
|
+
const want = s.state + "|" + (s.text || "");
|
|
245
|
+
if (b.dataset.fsState === want) return;
|
|
246
|
+
b.dataset.fsState = want;
|
|
247
|
+
const base = "position:fixed;top:0;left:0;z-index:2147483647;color:#fff;font:700 10px/1 ui-monospace,Menlo,monospace;letter-spacing:.12em;pointer-events:none;box-shadow:0 1px 6px rgba(0,0,0,.35);";
|
|
248
|
+
if (s.state === "working") {
|
|
249
|
+
b.style.cssText = base + "right:0;background:#3247F2;padding:9px 14px;font-size:12px;letter-spacing:.04em;text-align:center;";
|
|
250
|
+
b.textContent = s.text || "Foldspace is building your agent on this page. Please leave this window alone until Claude says it is ready.";
|
|
251
|
+
} else if (s.state === "ready") {
|
|
252
|
+
b.style.cssText = base + "right:0;background:#0F9D58;padding:9px 14px;font-size:12px;letter-spacing:.04em;text-align:center;";
|
|
253
|
+
b.textContent = s.text || "Ready - your turn. Open your agent and try it.";
|
|
254
|
+
} else {
|
|
255
|
+
b.style.cssText = base + "background:#3247F2;padding:5px 10px;border-bottom-right-radius:4px;";
|
|
256
|
+
b.textContent = LABEL;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
draw();
|
|
260
|
+
document.addEventListener("DOMContentLoaded", draw);
|
|
261
|
+
setInterval(draw, 1000);
|
|
234
262
|
})();`;
|
|
235
263
|
|
|
236
264
|
// Flag every local session as test traffic so it is excluded from analytics and
|
|
@@ -951,7 +979,7 @@ const ver = await connect(owned.version);
|
|
|
951
979
|
console.log(`Attached to ${ver.Browser} on :${port}`);
|
|
952
980
|
console.log(`Mode: ${attachMode}`);
|
|
953
981
|
console.log(`Agent: ${agentApiName} (product ${productId}, ${agentMode})`);
|
|
954
|
-
console.log(`Test: ${noTestMode ? "
|
|
982
|
+
console.log(`Test: ${noTestMode ? "off — conversations appear in the dashboard (pass --test-mode to hide them)" : "on"}`);
|
|
955
983
|
console.log(`Hosts: ${hostPatterns.join(", ")}`);
|
|
956
984
|
console.log(`Serving: ${path.relative(root, bundlePath)}`);
|
|
957
985
|
console.log(
|
package/bin/badge.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* foldspace badge <working|ready|label|off> [--text "<what to show>"]
|
|
4
|
+
*
|
|
5
|
+
* Sets the banner the attach badge draws across the top of the test window.
|
|
6
|
+
* The agent and the human share that one Chrome window, so the banner says
|
|
7
|
+
* whose turn it is:
|
|
8
|
+
* working - "Foldspace is building your agent on this page. Please leave
|
|
9
|
+
* this window alone until Claude says it is ready."
|
|
10
|
+
* ready - "Ready - your turn. Open your agent and try it." (pass --text
|
|
11
|
+
* with the question to try)
|
|
12
|
+
* label - back to the small corner tag
|
|
13
|
+
* off - remove it
|
|
14
|
+
* The state is kept in the page's sessionStorage, so it survives navigation;
|
|
15
|
+
* attach's badge script redraws within a second. Nothing else on the page is
|
|
16
|
+
* touched.
|
|
17
|
+
*/
|
|
18
|
+
import { CdpPage, parseArgs, printJson } from "../src/cdp-client.mjs";
|
|
19
|
+
import { resolveProjectDir } from "../src/upgrade.mjs";
|
|
20
|
+
import { badgePayload } from "../src/badge-core.mjs";
|
|
21
|
+
|
|
22
|
+
const projectDir = resolveProjectDir();
|
|
23
|
+
const { flags, rest } = parseArgs(process.argv.slice(2), ["text"]);
|
|
24
|
+
|
|
25
|
+
let payload;
|
|
26
|
+
try {
|
|
27
|
+
const unknown = Object.keys(flags).filter((name) => name !== "text");
|
|
28
|
+
if (unknown.length) throw new Error(`unknown option '--${unknown[0]}'`);
|
|
29
|
+
if (rest.length > 1) throw new Error(`unexpected argument '${rest[1]}'`);
|
|
30
|
+
payload = badgePayload(rest[0], flags.text);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(`badge: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let page;
|
|
37
|
+
try {
|
|
38
|
+
page = await CdpPage.open({ projectDir });
|
|
39
|
+
const shown = await page.evaluate(
|
|
40
|
+
`sessionStorage.setItem("__foldspace_badge__", ${JSON.stringify(JSON.stringify(payload))});
|
|
41
|
+
const b = document.getElementById("foldspace-dev-badge");
|
|
42
|
+
return { url: location.href.split(/[?#]/)[0], hadBadge: Boolean(b) };`,
|
|
43
|
+
);
|
|
44
|
+
printJson({ ok: true, state: payload.state, ...(payload.text ? { text: payload.text } : {}), ...shown });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
printJson({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
47
|
+
process.exit(1);
|
|
48
|
+
} finally {
|
|
49
|
+
page?.close();
|
|
50
|
+
}
|
package/bin/inject.mjs
CHANGED
|
@@ -144,7 +144,7 @@ fs.writeFileSync(
|
|
|
144
144
|
path.join(themeDir, "manifest.json"),
|
|
145
145
|
JSON.stringify({
|
|
146
146
|
manifest_version: 3,
|
|
147
|
-
name: "Foldspace
|
|
147
|
+
name: "Foldspace Agent Builder Theme",
|
|
148
148
|
version: "1.0",
|
|
149
149
|
description: "Paints the dev browser Foldspace blue.",
|
|
150
150
|
theme: {
|
|
@@ -197,7 +197,7 @@ function nameProfile() {
|
|
|
197
197
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
198
198
|
fs.writeFileSync(file, JSON.stringify(data));
|
|
199
199
|
};
|
|
200
|
-
const label = `Foldspace
|
|
200
|
+
const label = `Foldspace Agent Builder \u2014 ${target.name}`;
|
|
201
201
|
writeMerged(path.join(profileDir, "Local State"), (d) => {
|
|
202
202
|
d.profile = d.profile || {};
|
|
203
203
|
d.profile.info_cache = d.profile.info_cache || {};
|
package/package.json
CHANGED
package/recipes/INDEX.md
CHANGED
|
@@ -7,6 +7,7 @@ Read this whole table, then open the recipe closest to the outcome.
|
|
|
7
7
|
| [`who-is-the-user`](who-is-the-user/) | **L0** | Foldspace knows who is signed in — id, email, name, role, subscription — so conversations and analytics are not anonymous | Runs once at start-up. No action, no card | 4 production builds |
|
|
8
8
|
| [`find-by-name`](find-by-name/) | **L2** | The agent turns a name the user said into the id the next action needs; a count when no name is given; every tie when the name is ambiguous; near-misses when nothing matches | A data action. **No card** | 3 production builds |
|
|
9
9
|
| [`pick-from-a-list`](pick-from-a-list/) | **L2** | A clickable list, shown **only** when the user has to choose — with "none of these" and cancel | An action with a card that waits for the user | 1 production build |
|
|
10
|
+
| [`bottom-bar`](bottom-bar/) | **L0** | The agent's resting entry point is a bar at the foot of the page whose starters change with the screen the user is on — configured from code, the way customers do it | Page set-up, runs once at start-up | 2 |
|
|
10
11
|
| [`swap-the-login-method`](swap-the-login-method/) | any | `apiFetch` for an app that authenticates with a custom header, or with cookies, instead of a bearer token | A replacement `agent/utils.ts` | 2 production builds (3 builds, 3 different schemes) |
|
|
11
12
|
|
|
12
13
|
**Not here yet — no production build proves it:** the user's own plan or
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Bottom bar with page starters — L0
|
|
2
|
+
|
|
3
|
+
The bottom bar is the agent's resting entry point: a bar at the foot of the
|
|
4
|
+
page that shows a few starters and opens the agent when one is clicked. Its
|
|
5
|
+
starters change with the screen the user is on, so an agent with two actions
|
|
6
|
+
still looks useful everywhere.
|
|
7
|
+
|
|
8
|
+
It is configured **from code** — Agent Studio has no UI for it today, and this
|
|
9
|
+
is how customers set it. **Proven by 2 production builds** (one in the
|
|
10
|
+
product's own source, one from the agent project behind a feature flag).
|
|
11
|
+
|
|
12
|
+
## Adapt it
|
|
13
|
+
|
|
14
|
+
| In `agent/bottomBar.ts` | Change |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `PAGE_STARTERS` | This product's screens (path prefixes) and, for each, two or three questions **its actions can answer**. A starter for something the agent cannot do is worse than none |
|
|
17
|
+
| `DEFAULT_STARTERS` | What to show on any other page |
|
|
18
|
+
| `BOTTOM_BAR_SETTINGS` | Leave as the docs' defaults unless the product asks |
|
|
19
|
+
|
|
20
|
+
Then call it from the bundle's entry point:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// agent/actions/index.ts — last line
|
|
24
|
+
import { installBottomBar } from "../bottomBar";
|
|
25
|
+
installBottomBar();
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## What those builds learned
|
|
29
|
+
|
|
30
|
+
- **`setConversationStarters` replaces the whole set for the page** — both
|
|
31
|
+
groups. `null` restores Agent Studio's. So always pass the full list you
|
|
32
|
+
want shown.
|
|
33
|
+
- **In embedded mode the bar needs `openEmbeddedCallback`**, or nothing opens
|
|
34
|
+
when a starter is clicked. This recipe is for the overlay agent; see the
|
|
35
|
+
docs for embedded.
|
|
36
|
+
- **Route changes in a single-page app don't reload**, so the starters must be
|
|
37
|
+
re-applied on navigation — this recipe wraps `pushState` / `replaceState`
|
|
38
|
+
and listens to `popstate`.
|
|
39
|
+
- **Dark mode**: the SDK's default colours are being fixed (PLG-5891). Until
|
|
40
|
+
then a product in dark mode may need `theme` / `darkModeSettings` set.
|
|
41
|
+
|
|
42
|
+
Docs: [Bottom Bar](https://docs.foldspace.ai/customize/bottom-bar/) ·
|
|
43
|
+
[Conversation starters](https://docs.foldspace.ai/customize/conversation-starters/)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// The bottom bar: the agent's resting entry point at the foot of the page,
|
|
2
|
+
// cycling starters that fit the screen the user is on. Configured from code -
|
|
3
|
+
// Agent Studio has no UI for it today, and this is how customers set it.
|
|
4
|
+
// https://docs.foldspace.ai/customize/bottom-bar/
|
|
5
|
+
// https://docs.foldspace.ai/customize/conversation-starters/
|
|
6
|
+
//
|
|
7
|
+
// Product-neutral: the paths and starters below are placeholders. Replace them
|
|
8
|
+
// with this product's own screens and with questions its actions can answer.
|
|
9
|
+
|
|
10
|
+
import { getAgent } from "./utils";
|
|
11
|
+
|
|
12
|
+
/** Starters per screen. Paths are matched as prefixes, first match wins. */
|
|
13
|
+
export const PAGE_STARTERS: Array<{ match: RegExp; starters: string[] }> = [
|
|
14
|
+
{ match: /^\/dashboard/, starters: ["<A question about what this screen shows>", "<Another one>"] },
|
|
15
|
+
{ match: /^\/<records>/, starters: ["Find <a record> by name", "Show me <a record>'s details"] },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/** Shown on any page not listed above. */
|
|
19
|
+
export const DEFAULT_STARTERS: string[] = ["What can you do?", "Show my account"];
|
|
20
|
+
|
|
21
|
+
// The docs' own settings, unchanged. Change nothing here unless the product
|
|
22
|
+
// asks for it: every field is documented on the Bottom Bar page.
|
|
23
|
+
export const BOTTOM_BAR_SETTINGS = {
|
|
24
|
+
enabled: true,
|
|
25
|
+
maxVisibleStarters: 3,
|
|
26
|
+
reopenFrequency: "EVERY_LOAD",
|
|
27
|
+
starterClickBehavior: "OPEN_AGENT",
|
|
28
|
+
idleTimeoutMs: 5000,
|
|
29
|
+
initialBehaviorMode: "FULL",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export function startersForPath(pathname: string): string[] {
|
|
33
|
+
const hit = PAGE_STARTERS.find((entry) => entry.match.test(pathname));
|
|
34
|
+
return hit ? hit.starters : DEFAULT_STARTERS;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// On window, not in module scope: a bundle evaluated twice on one page must
|
|
38
|
+
// not install two route listeners.
|
|
39
|
+
const INSTALLED_FLAG = "__foldspace_bottom_bar__";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Turn the bar on and keep its starters matched to the page. Call once from
|
|
43
|
+
* the bundle's entry point; safe to call again.
|
|
44
|
+
*
|
|
45
|
+
* `setConversationStarters` REPLACES the whole set for this page view (both
|
|
46
|
+
* groups); passing `null` restores what Agent Studio has. Starters go in the
|
|
47
|
+
* KNOWLEDGE group so the bar shows them as plain questions.
|
|
48
|
+
*/
|
|
49
|
+
export function installBottomBar(): void {
|
|
50
|
+
const foldspace = (window as any).foldspace;
|
|
51
|
+
if (typeof foldspace !== "function") return;
|
|
52
|
+
if ((window as any)[INSTALLED_FLAG]) return;
|
|
53
|
+
(window as any)[INSTALLED_FLAG] = true;
|
|
54
|
+
|
|
55
|
+
foldspace("when", "ready", () => {
|
|
56
|
+
const agent = getAgent();
|
|
57
|
+
if (!agent) return;
|
|
58
|
+
let current: string[] = [];
|
|
59
|
+
|
|
60
|
+
const apply = () => {
|
|
61
|
+
const next = startersForPath(window.location.pathname);
|
|
62
|
+
if (next === current) return;
|
|
63
|
+
current = next;
|
|
64
|
+
agent.setConversationStarters?.(
|
|
65
|
+
{ KNOWLEDGE: next.map((title) => ({ title })), ACTION: [] },
|
|
66
|
+
"KNOWLEDGE",
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// One whole settings group per call, as the docs do.
|
|
71
|
+
agent.setConfiguration?.({ bottomBarSettings: BOTTOM_BAR_SETTINGS });
|
|
72
|
+
apply();
|
|
73
|
+
|
|
74
|
+
// Single-page apps change the URL without a load: re-apply on navigation
|
|
75
|
+
// and bring the bar back so the new starters are seen.
|
|
76
|
+
const onRouteChange = () => {
|
|
77
|
+
apply();
|
|
78
|
+
agent.openBottomBar?.({ mode: "FULL" });
|
|
79
|
+
};
|
|
80
|
+
window.addEventListener("popstate", onRouteChange);
|
|
81
|
+
const history = window.history as any;
|
|
82
|
+
for (const method of ["pushState", "replaceState"] as const) {
|
|
83
|
+
const original = history[method];
|
|
84
|
+
if (typeof original !== "function" || original.__foldspaceWrapped) continue;
|
|
85
|
+
const wrapped = function (this: any, ...args: unknown[]) {
|
|
86
|
+
const result = original.apply(this, args);
|
|
87
|
+
onRouteChange();
|
|
88
|
+
return result;
|
|
89
|
+
};
|
|
90
|
+
(wrapped as any).__foldspaceWrapped = true;
|
|
91
|
+
history[method] = wrapped;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "This recipe makes no network calls. What it sends to the SDK on /dashboard:",
|
|
3
|
+
"setConfiguration": {
|
|
4
|
+
"bottomBarSettings": {
|
|
5
|
+
"enabled": true,
|
|
6
|
+
"maxVisibleStarters": 3,
|
|
7
|
+
"reopenFrequency": "EVERY_LOAD",
|
|
8
|
+
"starterClickBehavior": "OPEN_AGENT",
|
|
9
|
+
"idleTimeoutMs": 5000,
|
|
10
|
+
"initialBehaviorMode": "FULL"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"setConversationStarters": {
|
|
14
|
+
"starters": { "KNOWLEDGE": [{ "title": "<A question about what this screen shows>" }, { "title": "<Another one>" }], "ACTION": [] },
|
|
15
|
+
"defaultStarterType": "KNOWLEDGE"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Bottom bar with page starters",
|
|
3
|
+
"level": "L0",
|
|
4
|
+
"family": "entry-point",
|
|
5
|
+
"kind": "page-setup",
|
|
6
|
+
"entry": "agent/bottomBar.ts",
|
|
7
|
+
"outcome": "The agent's resting entry point is a bar at the bottom of the page whose starters change with the page the user is on",
|
|
8
|
+
"provenBy": 2
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Pure logic behind `foldspace badge`, unit-tested.
|
|
2
|
+
|
|
3
|
+
export const BADGE_STATES = ["working", "ready", "label", "off"];
|
|
4
|
+
|
|
5
|
+
/** What gets written into the page for a badge state, or throws on bad input. */
|
|
6
|
+
export function badgePayload(state, text) {
|
|
7
|
+
const wanted = String(state || "").trim().toLowerCase();
|
|
8
|
+
if (!BADGE_STATES.includes(wanted)) {
|
|
9
|
+
throw new Error(`usage: foldspace badge <${BADGE_STATES.join("|")}> [--text "<what to show>"]`);
|
|
10
|
+
}
|
|
11
|
+
const payload = { state: wanted };
|
|
12
|
+
if (text !== undefined) {
|
|
13
|
+
if (wanted !== "working" && wanted !== "ready") {
|
|
14
|
+
throw new Error(`--text only applies to working or ready (got ${wanted}).`);
|
|
15
|
+
}
|
|
16
|
+
const clean = String(text).replace(/\s+/g, " ").trim();
|
|
17
|
+
if (!clean) throw new Error("--text is empty.");
|
|
18
|
+
if (clean.length > 160) throw new Error("--text is longer than 160 characters; keep it to one line.");
|
|
19
|
+
payload.text = clean;
|
|
20
|
+
}
|
|
21
|
+
return payload;
|
|
22
|
+
}
|
package/src/cli-registry.mjs
CHANGED
|
@@ -241,9 +241,10 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
241
241
|
),
|
|
242
242
|
value("--agent", "api-name", "Override the configured agent API name"),
|
|
243
243
|
flag(
|
|
244
|
-
"--
|
|
245
|
-
"
|
|
244
|
+
"--test-mode",
|
|
245
|
+
"Mark this session's conversations as test traffic, so they stay out of the dashboard; for an agent that already has real users",
|
|
246
246
|
),
|
|
247
|
+
flag("--no-test-mode", "Accepted for older instructions; test mode is already off by default"),
|
|
247
248
|
flag("--no-badge", "Hide the visible Foldspace development badge"),
|
|
248
249
|
flag(
|
|
249
250
|
"--daemon",
|
|
@@ -261,7 +262,7 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
261
262
|
effects: [
|
|
262
263
|
"May reload and instrument matching target pages",
|
|
263
264
|
"Refuses a Chrome that is not the profile inject launched",
|
|
264
|
-
"Test mode is
|
|
265
|
+
"Test mode is off unless --test-mode is passed: conversations appear in the dashboard",
|
|
265
266
|
"Never directly invokes an action handler",
|
|
266
267
|
"An empty local action registry is valid; named actions are not required",
|
|
267
268
|
"Restores prepared pages when detached cleanly",
|
|
@@ -316,6 +317,34 @@ export const CLI_COMMANDS = Object.freeze([
|
|
|
316
317
|
],
|
|
317
318
|
next: ["Record what you established in docs/app-profile.md", "Write the handler"],
|
|
318
319
|
}),
|
|
320
|
+
Object.freeze({
|
|
321
|
+
name: "badge",
|
|
322
|
+
entry: "badge.mjs",
|
|
323
|
+
group: "verify",
|
|
324
|
+
summary: "Set the banner across the top of the test window: working, ready, label or off",
|
|
325
|
+
usage: 'foldspace badge <working|ready|label|off> [--text "<what to show>"]',
|
|
326
|
+
risk: "browser-session",
|
|
327
|
+
environment: "local-chrome",
|
|
328
|
+
environmentVariables: ["FOLDSPACE_PROJECT_DIR", "CDP_PORT"],
|
|
329
|
+
capabilities: ["browser.cdp", "page.evaluate"],
|
|
330
|
+
positionals: [
|
|
331
|
+
Object.freeze({
|
|
332
|
+
name: "state",
|
|
333
|
+
required: true,
|
|
334
|
+
description:
|
|
335
|
+
"working: the agent is building here, leave the window alone | ready: the human's turn | label: the small corner tag | off: remove it",
|
|
336
|
+
}),
|
|
337
|
+
],
|
|
338
|
+
options: [
|
|
339
|
+
value("--text", "text", "working/ready: the line to show, e.g. the question to try (one line, up to 160 characters)"),
|
|
340
|
+
],
|
|
341
|
+
prerequisites: ["foldspace attach is running (it draws the badge)"],
|
|
342
|
+
effects: [
|
|
343
|
+
"Writes one key to the test window's sessionStorage; touches nothing else on the page",
|
|
344
|
+
"The banner survives navigation inside the test window",
|
|
345
|
+
],
|
|
346
|
+
next: ["Tell the human in chat what the banner says"],
|
|
347
|
+
}),
|
|
319
348
|
Object.freeze({
|
|
320
349
|
name: "deploy",
|
|
321
350
|
// Foldspace-internal: it uploads to Foldspace's own storage, which no
|