@bivy/bivy 0.18.10-staging.2 → 0.18.10-staging.4

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/bin/bivy.mjs CHANGED
@@ -1827,7 +1827,7 @@ function cmdCompletions(args = []) {
1827
1827
  const shell = (args[0] || "").toLowerCase();
1828
1828
  const commands = [
1829
1829
  "run", "runs", "sessions", "ls", "resume", "promote", "rename", "nodes", "agent", "agents", "agents:install", "shim", "takeover", "token", "exec",
1830
- "send", "attach", "kill", "setup", "start", "stop", "restart", "status", "doctor", "diagnostics", "capabilities", "logs", "login", "logout", "signout", "provider", "model",
1830
+ "send", "attach", "suggest", "kill", "setup", "start", "stop", "restart", "status", "doctor", "diagnostics", "capabilities", "logs", "login", "logout", "signout", "provider", "model",
1831
1831
  "update", "update:log", "audit", "automation", "config", "plugin", "open", "service", "secrets", "voice", "link", "relay:setup",
1832
1832
  "github:connect", "github:app-create", "github:app-connect", "github:app-sync", "prune", "uninstall", "help", "version",
1833
1833
  ];
@@ -2428,6 +2428,40 @@ async function cmdAttach(args = []) {
2428
2428
  console.log(c.green(`Attached ${body.name} (${body.kind}, ${body.size} bytes) to the chat.`));
2429
2429
  }
2430
2430
 
2431
+ // `bivy suggest "<task>" [--title "…"] [--session <id>]` — propose a task the
2432
+ // user can start in one tap, beside this session or in it. For an agent
2433
+ // offering next steps: write the task as a complete instruction.
2434
+ async function cmdSuggest(args = []) {
2435
+ const flagsWithValue = new Set(["--session", "--title"]);
2436
+ const flag = (name) => {
2437
+ const i = args.indexOf(name);
2438
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
2439
+ };
2440
+ const text = args.filter((a, i) => !a.startsWith("-") && !(i > 0 && flagsWithValue.has(args[i - 1]))).join(" ").trim();
2441
+ const sessionId = resolveAttachSessionId({ sessionFlag: flag("--session"), env: process.env });
2442
+ if (!text) { console.error(c.red('Usage: bivy suggest "<task>" [--title "short label"] [--session <id>]')); process.exit(1); return; }
2443
+ if (!sessionId) { console.error(c.red("No session id. Set --session <id> or run inside an agent session ($BIVY_SESSION_ID).")); process.exit(1); return; }
2444
+ const config = loadConfig();
2445
+ if (!(await ensureNodeRunning(config))) { console.error(c.red(`Could not reach the Bivy node at ${url(config)}.`)); process.exit(1); return; }
2446
+ let token;
2447
+ try { token = await localDeviceToken(config); } catch { token = undefined; }
2448
+ let res;
2449
+ try {
2450
+ res = await fetch(`${url(config)}/api/session/${encodeURIComponent(sessionId)}/suggest`, {
2451
+ method: "POST",
2452
+ headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
2453
+ body: JSON.stringify({ text, title: flag("--title") }),
2454
+ });
2455
+ } catch (error) {
2456
+ console.error(c.red(`Could not reach the Bivy node: ${error?.message || String(error)}`));
2457
+ process.exit(1);
2458
+ return;
2459
+ }
2460
+ const body = await res.json().catch(() => ({}));
2461
+ if (!res.ok) { console.error(c.red(`Suggest failed (${res.status}): ${body?.error || "unknown error"}`)); process.exit(1); return; }
2462
+ console.log(c.green("Suggested in the chat. The user can start it in one tap."));
2463
+ }
2464
+
2431
2465
  // Universal app publishing: every agent that can write JSON and run a command
2432
2466
  // uses the same contract. Relative static directories are manifest-relative.
2433
2467
  async function cmdApp(args = []) {
@@ -5419,6 +5453,9 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
5419
5453
  case "attach":
5420
5454
  await cmdAttach(args);
5421
5455
  break;
5456
+ case "suggest":
5457
+ await cmdSuggest(args);
5458
+ break;
5422
5459
  case "completions":
5423
5460
  case "completion":
5424
5461
  cmdCompletions(args);
@@ -38,6 +38,10 @@ export const BIVY_AGENT_NOTE = [
38
38
  "`bivy app run -- <command>` (a desktop app) gives the user a live preview; `bivy app shot` screenshots it so you " +
39
39
  "can check your work; `bivy app present` tells the user a visible change is ready to look at. " +
40
40
  "`bivy app --help` has the details.",
41
+ "- Proposing tasks the user could hand you (next steps, ideas, options): post each with " +
42
+ '`bivy suggest "<complete instruction>" [--title "short label"]` instead of only listing them. The user can ' +
43
+ "start each in one tap, in this session or in a parallel one that works in its own copy of the project, so " +
44
+ "write it to stand on its own, with paths relative to the project root.",
41
45
  ].join("\n");
42
46
  const PREAMBLE = "The user's global Bivy instructions follow. They apply to every workspace; where they conflict with " +
43
47
  "instructions from the current repository (AGENTS.md, CLAUDE.md, …), the repository's instructions win.";
@@ -1054,16 +1054,17 @@ button.sheet-action:last-child { border-bottom: 0; }
1054
1054
  .followup-card { flex: none; padding: 0; overflow: hidden; }
1055
1055
  .followup-card.locked { opacity: 0.75; }
1056
1056
  .followup-card-row {
1057
- display: flex; align-items: flex-start; gap: 8px; padding: 8px 10px;
1057
+ display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px; padding: 8px 10px;
1058
1058
  }
1059
1059
  .followup-card-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: var(--radius-md); }
1060
1060
  .followup-reorder { display: flex; flex-direction: column; gap: 2px; flex: none; }
1061
- .followup-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
1061
+ /* The text keeps a readable width; on a phone the actions wrap onto their own line. */
1062
+ .followup-body { flex: 1 1 12rem; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
1062
1063
  .followup-status { font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.02em; }
1063
1064
  .followup-card.locked .followup-status { color: var(--accent); }
1064
1065
  .followup-text { font-size: 14px; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
1065
1066
  .followup-attachments { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
1066
- .followup-card-actions { display: flex; align-items: center; gap: 6px; flex: none; flex-wrap: wrap; justify-content: flex-end; }
1067
+ .followup-card-actions { display: flex; align-items: center; gap: 6px; flex: none; flex-wrap: wrap; justify-content: flex-end; margin-left: auto; }
1067
1068
  .followup-edit { display: flex; flex-direction: column; gap: 8px; padding: 8px 10px; }
1068
1069
  .followup-edit-input { resize: vertical; min-height: 44px; }
1069
1070
  .followup-edit-actions { display: flex; justify-content: flex-end; gap: 8px; }
@@ -2497,10 +2498,13 @@ a.queue-item:hover { border-color: var(--line-strong); background: var(--surface
2497
2498
  .connect-option-link { align-self: flex-start; color: var(--accent); font-size: 12.5px; font-weight: 600; text-decoration: none; }
2498
2499
  .connect-option-link:hover { text-decoration: underline; }
2499
2500
  .connect-option-cta { align-self: flex-start; }
2500
- .automation-next-step { margin: 0 var(--space-4) var(--space-3); display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
2501
- .automation-next-step p { margin: var(--space-1) 0 0; color: var(--muted); font-size: var(--text-sm); }
2502
- .automation-next-step-actions { display: flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
2503
- @media (max-width: 560px) { .automation-next-step { align-items: flex-start; flex-direction: column; } .automation-next-step-actions { width: 100%; } }
2501
+ /* A one-time "next step" card above the composer (automations, notifications). */
2502
+ .next-step { margin: 0 var(--space-4) var(--space-3); display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
2503
+ .next-step p { margin: var(--space-1) 0 0; color: var(--muted); font-size: var(--text-sm); }
2504
+ .next-step .banner { margin-top: var(--space-2); }
2505
+ .next-step .next-step-ok { margin: 0; color: var(--ok); font-weight: var(--weight-medium); }
2506
+ .next-step-actions { display: flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
2507
+ @media (max-width: 560px) { .next-step { align-items: flex-start; flex-direction: column; } .next-step-actions { width: 100%; } }
2504
2508
 
2505
2509
  .connect-nodes { display: flex; flex-direction: column; gap: 8px; }
2506
2510
  .connect-nodes-head { color: var(--muted); font-size: 11px; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase; padding: 0 2px; }
@@ -3092,6 +3096,15 @@ p.setup-note .btn.link { min-height: 44px; margin-block: calc((1lh - 44px) / 2);
3092
3096
  .app-row-meta { color: var(--muted); font-size: var(--text-xs); line-height: var(--lh-xs); }
3093
3097
  .app-row-action { flex: none; display: flex; align-items: center; gap: var(--space-1); }
3094
3098
  .app-message { margin-block: var(--space-2); }
3099
+ /* Suggested task (SuggestionCard.tsx): a task the agent proposed, one tap to
3100
+ start in its own session. Shell from .card; layout only here. */
3101
+ .suggestion-card { max-inline-size: 440px; display: flex; flex-direction: column; gap: var(--space-1); }
3102
+ .suggestion-eyebrow { margin: 0; color: var(--muted); font-size: var(--text-xs); font-weight: var(--weight-semibold); }
3103
+ .suggestion-title { margin: 0; font-size: var(--text-base); font-weight: var(--weight-semibold); overflow-wrap: anywhere; }
3104
+ .suggestion-text { margin: 0; color: var(--muted); font-size: var(--text-sm); white-space: pre-wrap; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
3105
+ .suggestion-actions, .suggestion-all { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }
3106
+ .suggestion-all { padding-top: var(--space-2); border-top: 1px solid var(--line); }
3107
+ .suggestion-status { margin: var(--space-2) 0 0; display: flex; align-items: center; gap: var(--space-2); color: var(--ok); font-size: var(--text-sm); font-weight: var(--weight-medium); }
3095
3108
  /* Review card (ReviewCard.tsx): the app at a moment worth judging. The
3096
3109
  screenshot is shown whole, phone-shaped, on a quiet stage; Before/Now is the
3097
3110
  canonical .segmented switch; one primary action. */
package/dist/server.js CHANGED
@@ -72,6 +72,8 @@ import { createSessionEngine } from "./session/engine.js";
72
72
  import { exportProviderAuth, exportAccountApiKeys, exportAccountOAuthCredentials, importAccountOAuthCredentials, exportSyncableProviderAuth, exportProviderAuthTombstones, importProviderAuth, removeProvider, setProviderApiKey, listCredentialRecords, setProviderApiKeyLabeled, setProviderReferenceLabeled, removeProviderCredential, setCredentialSync, setCredentialUnattended, exportUnattendedRecords, unattendedCredentialRevision, getCredentialPresets, setActiveCredentialPreset, setCredentialPresetMapping, exportSyncableRecords, exportRecordTombstones, importCredentialRecords, reconcileHostedCredentialRecords } from "./credentials/api.js";
73
73
  import { listProviders } from "./runtime/provider-catalog.js";
74
74
  import { exportLocalModels, importLocalModels } from "./runtime/local-model-store.js";
75
+ import { sessionLikeFields } from "./session/start-like.js";
76
+ import { MAX_SUGGESTION_TEXT, MAX_SUGGESTION_TITLE, isTaskSuggestion } from "./session/suggestions.js";
75
77
  import { BIVY_AGENT_NOTE, mergeSyncedAgentInstructions, readAgentInstructions, sessionInstructions, writeAgentInstructions, MAX_AGENT_INSTRUCTIONS_BYTES } from "./agent-instructions.js";
76
78
  import { execEphemeralRequest } from "./ephemeral-exec.js";
77
79
  import { ApprovalManager } from "./approval.js";
@@ -3021,12 +3023,25 @@ const RELAY_COMMANDS = {
3021
3023
  broadcast({ type: "session.error", sessionId: record.id, error: sessionAgentError(record, error) });
3022
3024
  });
3023
3025
  },
3024
- async "session.new"(msg) {
3026
+ async "session.new"(msg, ctx) {
3025
3027
  // Start a fresh session. With `repo` ("owner/repo"), clone it and branch
3026
3028
  // off origin/main (or, with `branch`, the requested remote branch) into a
3027
3029
  // new git worktree named from `title` (the user's first message). With an
3028
3030
  // explicit `workspace` path, start in that folder. Otherwise use the
3029
3031
  // default Bivy workspace.
3032
+ //
3033
+ // `like: <sessionId>` starts another session like that one (same project,
3034
+ // agent, model and safety; explicit fields still win), and `prompt` sends
3035
+ // it a first message — so a client can start parallel work in the
3036
+ // background without switching to it.
3037
+ if (typeof msg.like === "string" && msg.like) {
3038
+ const source = openSessions.get(msg.like);
3039
+ if (!source) {
3040
+ relay?.sendEvent({ type: "session.error", requestId: typeof msg.requestId === "string" ? msg.requestId : undefined, error: "Session not found" });
3041
+ return;
3042
+ }
3043
+ msg = { ...sessionLikeFields(source), ...msg };
3044
+ }
3030
3045
  const repoInput = typeof msg.repo === "string" ? msg.repo.trim() : "";
3031
3046
  const workspaceInput = typeof msg.workspace === "string" ? msg.workspace.trim() : "";
3032
3047
  const title = typeof msg.title === "string" ? msg.title : undefined;
@@ -3112,6 +3127,9 @@ const RELAY_COMMANDS = {
3112
3127
  }),
3113
3128
  requestId,
3114
3129
  });
3130
+ if (typeof msg.prompt === "string" && msg.prompt.trim()) {
3131
+ await clientCommands.dispatch("prompt", { kind: "prompt", sessionId: record.id, text: msg.prompt, ...(msg.attachments ? { attachments: msg.attachments } : {}) }, ctx);
3132
+ }
3115
3133
  },
3116
3134
  };
3117
3135
  const clientCommands = new CommandRegistry(RELAY_COMMANDS, CLIENT_COMMAND_SCHEMAS);
@@ -10404,6 +10422,13 @@ app.get("/api/session/history", async (req, res) => {
10404
10422
  });
10405
10423
  app.post("/api/session", async (req, res, next) => {
10406
10424
  try {
10425
+ // `like` / `prompt`: see the relay `session.new` handler.
10426
+ if (typeof req.body?.like === "string" && req.body.like) {
10427
+ const source = openSessions.get(req.body.like);
10428
+ if (!source)
10429
+ return res.status(404).json({ error: "Session not found" });
10430
+ req.body = { ...sessionLikeFields(source), ...req.body };
10431
+ }
10407
10432
  // A git-workspace session: clone "owner/repo" and branch off origin/main into
10408
10433
  // a new worktree named from `title` (the user's first message), mirroring the
10409
10434
  // relay `session.new` repo path. Takes precedence over a manual workspace path.
@@ -10466,6 +10491,12 @@ app.post("/api/session", async (req, res, next) => {
10466
10491
  session.contract = computeSessionContract({ runtime: getRuntime(session.runtimeId), preview: false, sandbox: session.sandbox, approvalMode: session.approvalMode, acknowledgedAt: acknowledgeReducedProtections ? gateNow : undefined }, gateNow);
10467
10492
  persistSessionMetadata(session);
10468
10493
  res.json({ id: session.id, workspace: session.workspace, source: session.source, branch: session.worktree?.branch, prUrl: session.prUrl, sessionFile: session.sessionFile, name: session.session.getName(), runtimeId: session.runtimeId, agentName: getRuntime(session.runtimeId).displayName, model: publicModel(session.session.getCurrentModel(), session.session.getCurrentModel()), sessionState: sessionState(session) });
10494
+ if (typeof req.body?.prompt === "string" && req.body.prompt.trim()) {
10495
+ const { prompt, attachments } = req.body;
10496
+ // The response is already sent; a failed turn reaches clients as session.error.
10497
+ void clientCommands.dispatch("prompt", { kind: "prompt", sessionId: session.id, text: prompt, ...(attachments ? { attachments } : {}) }, { reply: broadcast, broadcast })
10498
+ .catch((error) => console.warn("[session] first prompt failed:", error instanceof Error ? error.message : error));
10499
+ }
10469
10500
  }
10470
10501
  catch (error) {
10471
10502
  // Creating an empty session does not contact the model provider. Failures
@@ -11174,6 +11205,21 @@ app.post("/api/session/:id/attach", (req, res) => {
11174
11205
  const { hash, name, mimeType, size, kind } = result.ref;
11175
11206
  res.json({ ok: true, hash, name, mimeType, size, kind });
11176
11207
  });
11208
+ // `bivy suggest "<task>"`: the agent proposes a task the user can start in one
11209
+ // tap, here or in a parallel session (see packages/web SuggestionCard).
11210
+ app.post("/api/session/:id/suggest", (req, res) => {
11211
+ const record = openSessions.get(String(req.params.id));
11212
+ if (!record)
11213
+ return res.status(404).json({ error: "Session not found" });
11214
+ const text = typeof req.body?.text === "string" ? req.body.text.trim() : "";
11215
+ const title = typeof req.body?.title === "string" && req.body.title.trim() ? req.body.title.trim() : undefined;
11216
+ const suggestion = { id: `suggestion-${randomBytes(8).toString("hex")}`, text, ...(title ? { title } : {}) };
11217
+ if (!isTaskSuggestion(suggestion))
11218
+ return res.status(400).json({ error: `A suggestion needs text (up to ${MAX_SUGGESTION_TEXT} characters) and an optional title (up to ${MAX_SUGGESTION_TITLE}).` });
11219
+ eventLog.appendSuggestion(record.id, { afterMessageCount: record.session.getMessages().length, suggestion });
11220
+ broadcast(stampSessionEvent({ type: "session.event", sessionId: record.id, event: { type: "suggestion", id: suggestion.id, suggestion } }));
11221
+ res.json({ ok: true, id: suggestion.id });
11222
+ });
11177
11223
  // Explicit child Run API for first-party/user-directed workflows. These routes
11178
11224
  // are intentionally not advertised to agents as tools; the service still
11179
11225
  // authorizes every status lookup against this parent Session's provenance.
@@ -30,6 +30,7 @@ import fs from "node:fs";
30
30
  import path from "node:path";
31
31
  import { normalizedIntermediateText, thinkingTextFromContent, mergeTranscript } from "./transcript-merge.js";
32
32
  import { APP_PUBLICATION_BLOCK, APP_REVIEW_BLOCK, isAppReference, isAppReview } from "../apps/types.js";
33
+ import { SUGGESTION_BLOCK, isTaskSuggestion } from "./suggestions.js";
33
34
  /**
34
35
  * Decompose a transcript message into serialization-independent "atoms": the
35
36
  * agent's own tool ids (unique + identical across serializations) and its text
@@ -241,6 +242,12 @@ function isAppPublication(value) {
241
242
  const entry = value;
242
243
  return entry.bivyKind === "app-publication" && typeof entry.id === "string" && typeof entry.createdAt === "number" && typeof entry.afterMessageCount === "number" && isAppReference(entry.app);
243
244
  }
245
+ function isSuggestionEntry(value) {
246
+ if (!value || typeof value !== "object")
247
+ return false;
248
+ const entry = value;
249
+ return entry.bivyKind === "suggestion" && typeof entry.id === "string" && typeof entry.createdAt === "number" && typeof entry.afterMessageCount === "number" && isTaskSuggestion(entry.suggestion);
250
+ }
244
251
  function isAppReviewEntry(value) {
245
252
  if (!value || typeof value !== "object")
246
253
  return false;
@@ -248,7 +255,7 @@ function isAppReviewEntry(value) {
248
255
  return entry.bivyKind === "app-review" && typeof entry.id === "string" && typeof entry.createdAt === "number" && typeof entry.afterMessageCount === "number" && isAppReview(entry.review);
249
256
  }
250
257
  function isRecord(value) {
251
- return isOverlay(value) || isBase(value) || isAttachment(value) || isOutboundAttachment(value) || isInlineImage(value) || isAppPublication(value) || isAppReviewEntry(value);
258
+ return isOverlay(value) || isBase(value) || isAttachment(value) || isOutboundAttachment(value) || isInlineImage(value) || isAppPublication(value) || isAppReviewEntry(value) || isSuggestionEntry(value);
252
259
  }
253
260
  /**
254
261
  * Fold attachment records into a text→refs list: last write wins per text (a
@@ -370,7 +377,11 @@ export function replayExtras(entries) {
370
377
  role: "assistant", content: [{ type: APP_REVIEW_BLOCK, review: entry.review }],
371
378
  id: entry.id, afterMessageCount: entry.afterMessageCount, createdAt: entry.createdAt,
372
379
  }));
373
- return [...foldIntermediate(intermediate), ...foldTool(tool), ...replayOutboundAttachments(entries), ...publications, ...cards];
380
+ const suggestions = entries.filter((entry) => entry.bivyKind === "suggestion").map((entry) => ({
381
+ role: "assistant", content: [{ type: SUGGESTION_BLOCK, suggestion: entry.suggestion }],
382
+ id: entry.id, afterMessageCount: entry.afterMessageCount, createdAt: entry.createdAt,
383
+ }));
384
+ return [...foldIntermediate(intermediate), ...foldTool(tool), ...replayOutboundAttachments(entries), ...publications, ...cards, ...suggestions];
374
385
  }
375
386
  /**
376
387
  * Fold the outbound (agent-sent) attachment records into time-anchored synthetic
@@ -670,6 +681,10 @@ export class EventLog {
670
681
  this.load(id);
671
682
  this.enqueue(id, `review:${entry.review.id}`, { bivyKind: "app-review", createdAt: entry.createdAt ?? Date.now(), afterMessageCount: entry.afterMessageCount, id: entry.review.id, review: structuredClone(entry.review) });
672
683
  }
684
+ appendSuggestion(id, entry) {
685
+ this.load(id);
686
+ this.enqueue(id, `suggestion:${entry.suggestion.id}`, { bivyKind: "suggestion", createdAt: Date.now(), afterMessageCount: entry.afterMessageCount, id: entry.suggestion.id, suggestion: { ...entry.suggestion } });
687
+ }
673
688
  appendOutboundAttachment(id, entry) {
674
689
  this.load(id);
675
690
  const record = {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The creation fields of an existing session, for "start another like it"
3
+ * (`session.new` with `like`): same project, agent, model and safety. A git
4
+ * worktree session points back at its checkout, so the new session gets its own
5
+ * worktree off the default branch instead of sharing this one's; a plain folder
6
+ * is shared, as any new session in it would be.
7
+ */
8
+ export function sessionLikeFields(source) {
9
+ const model = source.session.getCurrentModel();
10
+ return {
11
+ workspace: source.worktree?.repoRoot ?? source.workspace,
12
+ agent: source.runtimeId,
13
+ ...(source.sandbox ? { sandbox: source.sandbox } : {}),
14
+ ...(model?.id ? { model: { provider: String(model.provider ?? ""), id: String(model.id) } } : {}),
15
+ };
16
+ }
@@ -0,0 +1,13 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ export const SUGGESTION_BLOCK = "bivy_suggestion";
4
+ export const MAX_SUGGESTION_TEXT = 4000;
5
+ export const MAX_SUGGESTION_TITLE = 120;
6
+ export function isTaskSuggestion(value) {
7
+ if (!value || typeof value !== "object")
8
+ return false;
9
+ const s = value;
10
+ return typeof s.id === "string" && s.id.length > 0
11
+ && typeof s.text === "string" && s.text.length > 0 && s.text.length <= MAX_SUGGESTION_TEXT
12
+ && (s.title === undefined || (typeof s.title === "string" && s.title.length <= MAX_SUGGESTION_TITLE));
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.18.10-staging.2",
3
+ "version": "0.18.10-staging.4",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",