@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 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.** You and the
75
- human share one Chrome window. Before you start looking at the app or testing
76
- in it: *"I'm going to work in that Chrome window for a few minutes - please
77
- leave it alone until I tell you it's ready."* When it is their turn: *"It's
78
- ready. Try asking: <the question this experience answers>."*
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 **no publish tool on the MCP** and
211
- `get_navigation_route` returns only `isActive` publish routes in Agent Studio
212
- and say so. Route descriptions are a retrieval surface: write what the page is
213
- *for*, in the user's words, and pair every parameterised route with the lookup
214
- action that produces its parameter or ship neither.
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. **Do not send chat messages to test.** Every message is a real conversation
292
- in the customer's list. `runTask` creates no conversation record use it.
293
- 5. **Test mode is off by default.** A new agent has no traffic to pollute, and
294
- watching real conversations appear is the point of the first run. Arm it
295
- only when the tenant already has real conversations and then on **every**
296
- instance via `armAllInstances`, because `foldspace.agent({apiName})` returns
297
- the overlay handle, which is not the one serving an embedded chat. Nothing
298
- can confirm it is on afterwards; never send probe messages to find out.
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
- | `--no-test-mode` | Test mode was armed unconditionally, hiding exactly what initial setup needs to see |
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. Turn it off
139
- // only when you WANT the conversations and action calls to show up in the
140
- // Foldspace dashboard a brand-new agent during initial setup, where there is
141
- // no production traffic to pollute.
142
- const noTestMode = process.argv.includes("--no-test-mode");
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 DEV \u00b7 ${agentApiName} \u00b7 ${attachMode} \u00b7 LIVE`
175
- : `FOLDSPACE DEV \u00b7 ${agentApiName} \u00b7 ${attachMode} \u00b7 TEST MODE`;
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 add = () => {
224
- if (document.getElementById("foldspace-dev-badge") || !document.body) return;
225
- const b = document.createElement("div");
226
- b.id = "foldspace-dev-badge";
227
- b.textContent = ${JSON.stringify(badgeText)};
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
- add();
232
- document.addEventListener("DOMContentLoaded", add);
233
- setInterval(add, 2000);
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 ? "OFF — conversations WILL appear in the dashboard" : "on"}`);
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 Dev Theme",
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 Dev \u2014 ${target.name}`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldspace_npm/harness",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Build and verify portable Foldspace action artifacts against a live app.",
5
5
  "type": "module",
6
6
  "bin": {
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
+ }
@@ -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
- "--no-test-mode",
245
- "Do not mark conversations as test traffic; use deliberately",
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 enabled unless --no-test-mode is passed",
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