veryfront 0.1.13 → 0.1.15

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.
Files changed (153) hide show
  1. package/esm/cli/app/data/slug-words.d.ts.map +1 -1
  2. package/esm/cli/app/data/slug-words.js +225 -90
  3. package/esm/cli/app/operations/project-creation.js +4 -3
  4. package/esm/cli/app/shell.js +1 -1
  5. package/esm/cli/app/utils.d.ts +5 -4
  6. package/esm/cli/app/utils.d.ts.map +1 -1
  7. package/esm/cli/app/utils.js +0 -23
  8. package/esm/cli/app/views/dashboard.d.ts +1 -1
  9. package/esm/cli/app/views/dashboard.d.ts.map +1 -1
  10. package/esm/cli/app/views/dashboard.js +22 -4
  11. package/esm/cli/auth/callback-server.d.ts.map +1 -1
  12. package/esm/cli/auth/callback-server.js +3 -2
  13. package/esm/cli/commands/dev/handler.d.ts.map +1 -1
  14. package/esm/cli/commands/dev/handler.js +2 -0
  15. package/esm/cli/commands/init/init-command.d.ts.map +1 -1
  16. package/esm/cli/commands/init/init-command.js +20 -3
  17. package/esm/cli/commands/init/interactive-wizard.d.ts +3 -2
  18. package/esm/cli/commands/init/interactive-wizard.d.ts.map +1 -1
  19. package/esm/cli/commands/init/interactive-wizard.js +55 -27
  20. package/esm/cli/mcp/remote-file-tools.d.ts +0 -6
  21. package/esm/cli/mcp/remote-file-tools.d.ts.map +1 -1
  22. package/esm/cli/mcp/remote-file-tools.js +37 -15
  23. package/esm/cli/shared/reserve-slug.d.ts.map +1 -1
  24. package/esm/cli/shared/reserve-slug.js +8 -3
  25. package/esm/cli/utils/env-prompt.d.ts.map +1 -1
  26. package/esm/cli/utils/env-prompt.js +3 -0
  27. package/esm/deno.d.ts +5 -1
  28. package/esm/deno.js +11 -4
  29. package/esm/src/agent/chat-handler.d.ts +4 -3
  30. package/esm/src/agent/chat-handler.d.ts.map +1 -1
  31. package/esm/src/agent/chat-handler.js +55 -4
  32. package/esm/src/agent/react/index.d.ts +1 -1
  33. package/esm/src/agent/react/index.d.ts.map +1 -1
  34. package/esm/src/agent/react/use-chat/browser-inference/browser-engine.d.ts +18 -0
  35. package/esm/src/agent/react/use-chat/browser-inference/browser-engine.d.ts.map +1 -0
  36. package/esm/src/agent/react/use-chat/browser-inference/browser-engine.js +54 -0
  37. package/esm/src/agent/react/use-chat/browser-inference/types.d.ts +43 -0
  38. package/esm/src/agent/react/use-chat/browser-inference/types.d.ts.map +1 -0
  39. package/esm/src/agent/react/use-chat/browser-inference/types.js +4 -0
  40. package/esm/src/agent/react/use-chat/browser-inference/worker-client.d.ts +23 -0
  41. package/esm/src/agent/react/use-chat/browser-inference/worker-client.d.ts.map +1 -0
  42. package/esm/src/agent/react/use-chat/browser-inference/worker-client.js +67 -0
  43. package/esm/src/agent/react/use-chat/browser-inference/worker-script.d.ts +8 -0
  44. package/esm/src/agent/react/use-chat/browser-inference/worker-script.d.ts.map +1 -0
  45. package/esm/src/agent/react/use-chat/browser-inference/worker-script.js +97 -0
  46. package/esm/src/agent/react/use-chat/index.d.ts +1 -1
  47. package/esm/src/agent/react/use-chat/index.d.ts.map +1 -1
  48. package/esm/src/agent/react/use-chat/types.d.ts +12 -0
  49. package/esm/src/agent/react/use-chat/types.d.ts.map +1 -1
  50. package/esm/src/agent/react/use-chat/use-chat.d.ts.map +1 -1
  51. package/esm/src/agent/react/use-chat/use-chat.js +120 -6
  52. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  53. package/esm/src/agent/runtime/index.js +59 -7
  54. package/esm/src/build/production-build/templates.d.ts +2 -2
  55. package/esm/src/build/production-build/templates.d.ts.map +1 -1
  56. package/esm/src/build/production-build/templates.js +2 -68
  57. package/esm/src/chat/index.d.ts +1 -1
  58. package/esm/src/chat/index.d.ts.map +1 -1
  59. package/esm/src/errors/veryfront-error.d.ts +3 -0
  60. package/esm/src/errors/veryfront-error.d.ts.map +1 -1
  61. package/esm/src/platform/adapters/runtime/deno/adapter.d.ts.map +1 -1
  62. package/esm/src/platform/adapters/runtime/deno/adapter.js +24 -3
  63. package/esm/src/platform/compat/http/deno-server.d.ts.map +1 -1
  64. package/esm/src/platform/compat/http/deno-server.js +23 -2
  65. package/esm/src/provider/index.d.ts +1 -1
  66. package/esm/src/provider/index.d.ts.map +1 -1
  67. package/esm/src/provider/index.js +1 -1
  68. package/esm/src/provider/local/ai-sdk-adapter.d.ts +19 -0
  69. package/esm/src/provider/local/ai-sdk-adapter.d.ts.map +1 -0
  70. package/esm/src/provider/local/ai-sdk-adapter.js +164 -0
  71. package/esm/src/provider/local/env.d.ts +10 -0
  72. package/esm/src/provider/local/env.d.ts.map +1 -0
  73. package/esm/src/provider/local/env.js +23 -0
  74. package/esm/src/provider/local/local-engine.d.ts +61 -0
  75. package/esm/src/provider/local/local-engine.d.ts.map +1 -0
  76. package/esm/src/provider/local/local-engine.js +211 -0
  77. package/esm/src/provider/local/model-catalog.d.ts +30 -0
  78. package/esm/src/provider/local/model-catalog.d.ts.map +1 -0
  79. package/esm/src/provider/local/model-catalog.js +58 -0
  80. package/esm/src/provider/model-registry.d.ts +14 -0
  81. package/esm/src/provider/model-registry.d.ts.map +1 -1
  82. package/esm/src/provider/model-registry.js +58 -2
  83. package/esm/src/proxy/main.js +34 -6
  84. package/esm/src/proxy/server-resolver.d.ts +23 -0
  85. package/esm/src/proxy/server-resolver.d.ts.map +1 -0
  86. package/esm/src/proxy/server-resolver.js +124 -0
  87. package/esm/src/react/components/ai/chat/components/inference-badge.d.ts +8 -0
  88. package/esm/src/react/components/ai/chat/components/inference-badge.d.ts.map +1 -0
  89. package/esm/src/react/components/ai/chat/components/inference-badge.js +36 -0
  90. package/esm/src/react/components/ai/chat/components/upgrade-cta.d.ts +7 -0
  91. package/esm/src/react/components/ai/chat/components/upgrade-cta.d.ts.map +1 -0
  92. package/esm/src/react/components/ai/chat/components/upgrade-cta.js +33 -0
  93. package/esm/src/react/components/ai/chat/index.d.ts +7 -1
  94. package/esm/src/react/components/ai/chat/index.d.ts.map +1 -1
  95. package/esm/src/react/components/ai/chat/index.js +16 -4
  96. package/esm/src/sandbox/index.d.ts +31 -0
  97. package/esm/src/sandbox/index.d.ts.map +1 -0
  98. package/esm/src/sandbox/index.js +30 -0
  99. package/esm/src/sandbox/sandbox.d.ts +48 -0
  100. package/esm/src/sandbox/sandbox.d.ts.map +1 -0
  101. package/esm/src/sandbox/sandbox.js +178 -0
  102. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/import-finder.d.ts.map +1 -1
  103. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/import-finder.js +8 -2
  104. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/index.d.ts +1 -0
  105. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/index.d.ts.map +1 -1
  106. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/index.js +1 -0
  107. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/transform.d.ts.map +1 -1
  108. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/transform.js +15 -1
  109. package/package.json +8 -1
  110. package/src/cli/app/data/slug-words.ts +225 -90
  111. package/src/cli/app/operations/project-creation.ts +3 -3
  112. package/src/cli/app/shell.ts +1 -1
  113. package/src/cli/app/utils.ts +0 -30
  114. package/src/cli/app/views/dashboard.ts +27 -4
  115. package/src/cli/auth/callback-server.ts +3 -2
  116. package/src/cli/commands/dev/handler.ts +2 -0
  117. package/src/cli/commands/init/init-command.ts +30 -3
  118. package/src/cli/commands/init/interactive-wizard.ts +62 -34
  119. package/src/cli/mcp/remote-file-tools.ts +50 -15
  120. package/src/cli/shared/reserve-slug.ts +9 -2
  121. package/src/cli/utils/env-prompt.ts +3 -0
  122. package/src/deno.js +11 -4
  123. package/src/src/agent/chat-handler.ts +57 -4
  124. package/src/src/agent/react/index.ts +2 -0
  125. package/src/src/agent/react/use-chat/browser-inference/browser-engine.ts +81 -0
  126. package/src/src/agent/react/use-chat/browser-inference/types.ts +52 -0
  127. package/src/src/agent/react/use-chat/browser-inference/worker-client.ts +89 -0
  128. package/src/src/agent/react/use-chat/browser-inference/worker-script.ts +98 -0
  129. package/src/src/agent/react/use-chat/index.ts +2 -0
  130. package/src/src/agent/react/use-chat/types.ts +20 -0
  131. package/src/src/agent/react/use-chat/use-chat.ts +148 -8
  132. package/src/src/agent/runtime/index.ts +72 -6
  133. package/src/src/build/production-build/templates.ts +2 -68
  134. package/src/src/chat/index.ts +2 -0
  135. package/src/src/errors/veryfront-error.ts +2 -1
  136. package/src/src/platform/adapters/runtime/deno/adapter.ts +25 -3
  137. package/src/src/platform/compat/http/deno-server.ts +28 -1
  138. package/src/src/provider/index.ts +1 -0
  139. package/src/src/provider/local/ai-sdk-adapter.ts +207 -0
  140. package/src/src/provider/local/env.ts +26 -0
  141. package/src/src/provider/local/local-engine.ts +288 -0
  142. package/src/src/provider/local/model-catalog.ts +73 -0
  143. package/src/src/provider/model-registry.ts +66 -2
  144. package/src/src/proxy/main.ts +41 -6
  145. package/src/src/proxy/server-resolver.ts +151 -0
  146. package/src/src/react/components/ai/chat/components/inference-badge.tsx +48 -0
  147. package/src/src/react/components/ai/chat/components/upgrade-cta.tsx +56 -0
  148. package/src/src/react/components/ai/chat/index.tsx +43 -6
  149. package/src/src/sandbox/index.ts +32 -0
  150. package/src/src/sandbox/sandbox.ts +236 -0
  151. package/src/src/transforms/pipeline/stages/ssr-vf-modules/import-finder.ts +9 -2
  152. package/src/src/transforms/pipeline/stages/ssr-vf-modules/index.ts +1 -0
  153. package/src/src/transforms/pipeline/stages/ssr-vf-modules/transform.ts +17 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Dedicated Server Resolver
3
+ *
4
+ * Resolves an environment ID to a dedicated server hostname.
5
+ * Used by the proxy to route traffic to dedicated servers instead of the shared pool.
6
+ *
7
+ * Caches results in memory with a short TTL to avoid hitting the API on every request.
8
+ * A null result (no dedicated server) is also cached to prevent repeated lookups.
9
+ */
10
+ import * as dntShim from "../../_dnt.shims.js";
11
+
12
+
13
+ import { proxyLogger } from "./logger.js";
14
+ import { unrefTimer } from "../platform/compat/process.js";
15
+
16
+ const logger = proxyLogger.child({ module: "server-resolver" });
17
+
18
+ interface DedicatedServer {
19
+ id: string;
20
+ short_id: string;
21
+ hostname: string;
22
+ status: string;
23
+ }
24
+
25
+ interface CacheEntry {
26
+ server: DedicatedServer | null;
27
+ expiresAt: number;
28
+ }
29
+
30
+ /** Thrown when the API call fails (network error, non-OK status, parse error). */
31
+ class ServerResolverError extends Error {
32
+ constructor(message: string, options?: { cause?: unknown }) {
33
+ super(message, options);
34
+ }
35
+ }
36
+
37
+ export class ServerResolver {
38
+ private cache = new Map<string, CacheEntry>();
39
+ private pending = new Map<string, Promise<DedicatedServer | null>>();
40
+ private cleanupTimer: ReturnType<typeof dntShim.setInterval> | null = null;
41
+
42
+ constructor(
43
+ private apiInternalUrl: string,
44
+ private apiUser: string,
45
+ private apiPass: string,
46
+ private cacheTtlMs: number = 30_000,
47
+ ) {
48
+ // Cleanup expired entries every 60s
49
+ this.cleanupTimer = dntShim.setInterval(() => this.cleanup(), 60_000);
50
+ // Don't keep the process alive for cleanup
51
+ unrefTimer(this.cleanupTimer);
52
+ }
53
+
54
+ /**
55
+ * Resolve an environment ID to a dedicated server URL, or null for shared pool.
56
+ */
57
+ async resolve(environmentId: string | undefined): Promise<string | null> {
58
+ if (!environmentId) return null;
59
+
60
+ const cached = this.cache.get(environmentId);
61
+ if (cached && Date.now() < cached.expiresAt) {
62
+ return cached.server ? `http://${cached.server.hostname}` : null;
63
+ }
64
+
65
+ // Deduplicate concurrent requests for the same environment
66
+ const inflight = this.pending.get(environmentId);
67
+ if (inflight) {
68
+ const server = await inflight;
69
+ return server ? `http://${server.hostname}` : null;
70
+ }
71
+
72
+ const promise = this.fetchServer(environmentId);
73
+ this.pending.set(environmentId, promise);
74
+
75
+ try {
76
+ const server = await promise;
77
+ // Only cache successful API responses (server found OR explicit "no server").
78
+ // Transient errors (network failures, non-OK status) are NOT cached so the
79
+ // next request retries the API instead of suppressing dedicated routing.
80
+ this.cache.set(environmentId, {
81
+ server,
82
+ expiresAt: Date.now() + this.cacheTtlMs,
83
+ });
84
+ return server ? `http://${server.hostname}` : null;
85
+ } catch (error) {
86
+ // API error — don't cache, fall back to shared pool for this request
87
+ logger.warn("[ServerResolver] Transient error, skipping cache", {
88
+ environmentId,
89
+ error: error instanceof Error ? error.message : String(error),
90
+ });
91
+ return null;
92
+ } finally {
93
+ this.pending.delete(environmentId);
94
+ }
95
+ }
96
+
97
+ close(): void {
98
+ if (this.cleanupTimer) {
99
+ clearInterval(this.cleanupTimer);
100
+ this.cleanupTimer = null;
101
+ }
102
+ this.cache.clear();
103
+ }
104
+
105
+ /**
106
+ * Fetch dedicated server from API.
107
+ * Returns DedicatedServer | null on success (null = no dedicated server assigned).
108
+ * Throws ServerResolverError on transient failures (network, non-OK status).
109
+ */
110
+ private async fetchServer(environmentId: string): Promise<DedicatedServer | null> {
111
+ const url = `${this.apiInternalUrl}/internal/environment-server?environmentId=${
112
+ encodeURIComponent(environmentId)
113
+ }`;
114
+ const headers: Record<string, string> = { Accept: "application/json" };
115
+
116
+ if (this.apiUser && this.apiPass) {
117
+ headers.Authorization = `Basic ${btoa(`${this.apiUser}:${this.apiPass}`)}`;
118
+ }
119
+
120
+ let response: dntShim.Response;
121
+ try {
122
+ response = await dntShim.fetch(url, { headers, signal: AbortSignal.timeout(5_000) });
123
+ } catch (error) {
124
+ throw new ServerResolverError(
125
+ `Failed to reach API: ${error instanceof Error ? error.message : String(error)}`,
126
+ { cause: error },
127
+ );
128
+ }
129
+
130
+ if (!response.ok) {
131
+ await response.body?.cancel();
132
+ throw new ServerResolverError(`API returned ${response.status} for ${environmentId}`);
133
+ }
134
+
135
+ const data = (await response.json()) as { server: DedicatedServer | null };
136
+ if (data.server) {
137
+ logger.debug("[ServerResolver] Resolved dedicated server", {
138
+ environmentId,
139
+ hostname: data.server.hostname,
140
+ });
141
+ }
142
+ return data.server;
143
+ }
144
+
145
+ private cleanup(): void {
146
+ const now = Date.now();
147
+ for (const [key, entry] of this.cache) {
148
+ if (now >= entry.expiresAt) this.cache.delete(key);
149
+ }
150
+ }
151
+ }
@@ -0,0 +1,48 @@
1
+ import * as React from "react";
2
+ import type { BrowserInferenceStatus, InferenceMode } from "../../../../../agent/react/index.js";
3
+
4
+ export interface InferenceBadgeProps {
5
+ inferenceMode: InferenceMode;
6
+ browserStatus?: BrowserInferenceStatus | null;
7
+ }
8
+
9
+ export function InferenceBadge({
10
+ inferenceMode,
11
+ browserStatus,
12
+ }: InferenceBadgeProps): React.ReactElement | null {
13
+ if (inferenceMode === "cloud") return null;
14
+
15
+ let label: string;
16
+ let dotColor: string;
17
+ let showProgress = false;
18
+
19
+ if (inferenceMode === "server-local") {
20
+ label = "Running locally";
21
+ dotColor = "bg-green-500";
22
+ } else if (browserStatus === "downloading-model") {
23
+ label = "Downloading model...";
24
+ dotColor = "bg-amber-500";
25
+ showProgress = true;
26
+ } else if (browserStatus === "loading-runtime") {
27
+ label = "Loading AI runtime...";
28
+ dotColor = "bg-amber-500";
29
+ } else if (browserStatus === "generating") {
30
+ label = "Running in browser";
31
+ dotColor = "bg-green-500";
32
+ } else if (browserStatus === "error") {
33
+ label = "Local model failed";
34
+ dotColor = "bg-red-500";
35
+ } else {
36
+ label = "Running locally";
37
+ dotColor = "bg-green-500";
38
+ }
39
+
40
+ return (
41
+ <div className="flex items-center gap-1.5 px-3 py-1 text-xs text-neutral-500 dark:text-neutral-400">
42
+ <span
43
+ className={`size-1.5 rounded-full ${dotColor} ${showProgress ? "animate-pulse" : ""}`}
44
+ />
45
+ <span>{label}</span>
46
+ </div>
47
+ );
48
+ }
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import type { InferenceMode } from "../../../../../agent/react/index.js";
3
+
4
+ const DISMISS_KEY = "vf-upgrade-cta-dismissed";
5
+
6
+ export interface UpgradeCTAProps {
7
+ inferenceMode: InferenceMode;
8
+ }
9
+
10
+ export function UpgradeCTA({ inferenceMode }: UpgradeCTAProps): React.ReactElement | null {
11
+ const [dismissed, setDismissed] = React.useState(() => {
12
+ try {
13
+ return localStorage.getItem(DISMISS_KEY) === "1";
14
+ } catch {
15
+ return false;
16
+ }
17
+ });
18
+
19
+ if (inferenceMode === "cloud" || dismissed) return null;
20
+
21
+ const handleDismiss = () => {
22
+ setDismissed(true);
23
+ try {
24
+ localStorage.setItem(DISMISS_KEY, "1");
25
+ } catch {
26
+ // localStorage may be unavailable
27
+ }
28
+ };
29
+
30
+ return (
31
+ <div className="w-full max-w-2xl mx-auto mt-4 px-4 py-3 bg-blue-50 dark:bg-blue-900/20 rounded-xl text-sm text-blue-700 dark:text-blue-300 flex items-start gap-3">
32
+ <span className="flex-1">
33
+ Using a lightweight local model. Add an API key to your{" "}
34
+ <code className="px-1 py-0.5 bg-blue-100 dark:bg-blue-900/40 rounded text-xs">.env</code>
35
+ {" "}
36
+ for GPT-4o or Claude.
37
+ </span>
38
+ <button
39
+ type="button"
40
+ onClick={handleDismiss}
41
+ className="text-blue-400 hover:text-blue-600 dark:hover:text-blue-200 transition-colors flex-shrink-0"
42
+ aria-label="Dismiss"
43
+ >
44
+ <svg
45
+ className="size-4"
46
+ viewBox="0 0 16 16"
47
+ fill="none"
48
+ stroke="currentColor"
49
+ strokeWidth="2"
50
+ >
51
+ <path d="M4 4l8 8M12 4l-8 8" />
52
+ </svg>
53
+ </button>
54
+ </div>
55
+ );
56
+ }
@@ -7,7 +7,13 @@ import {
7
7
  SubmitButton,
8
8
  } from "../../../primitives/index.js";
9
9
  import { useVoiceInput } from "../../../../agent/react/index.js";
10
- import type { DynamicToolUIPart, ToolUIPart, UIMessage } from "../../../../agent/react/index.js";
10
+ import type {
11
+ BrowserInferenceStatus,
12
+ DynamicToolUIPart,
13
+ InferenceMode,
14
+ ToolUIPart,
15
+ UIMessage,
16
+ } from "../../../../agent/react/index.js";
11
17
  import { type ChatTheme, cn, defaultChatTheme, mergeThemes } from "../theme.js";
12
18
  import { Markdown } from "../markdown.js";
13
19
  import { MessageSquareIcon, RefreshCwIcon } from "../icons/index.js";
@@ -27,6 +33,8 @@ export {
27
33
  } from "./components/empty-state.js";
28
34
  export { MessageActions, type MessageActionsProps } from "./components/message-actions.js";
29
35
  export { ToolCallCard, ToolStatusBadge } from "./components/tool-ui.js";
36
+ export { InferenceBadge, type InferenceBadgeProps } from "./components/inference-badge.js";
37
+ export { UpgradeCTA, type UpgradeCTAProps } from "./components/upgrade-cta.js";
30
38
 
31
39
  export {
32
40
  getTextContent,
@@ -48,6 +56,8 @@ import {
48
56
  import { MessageActions } from "./components/message-actions.js";
49
57
  import { ReasoningCard } from "./components/reasoning.js";
50
58
  import { ToolCallCard } from "./components/tool-ui.js";
59
+ import { InferenceBadge } from "./components/inference-badge.js";
60
+ import { UpgradeCTA } from "./components/upgrade-cta.js";
51
61
  import { getTextContent, groupPartsInOrder } from "./utils/message-parts.js";
52
62
 
53
63
  export interface ChatProps {
@@ -86,6 +96,10 @@ export interface ChatProps {
86
96
  model?: string;
87
97
  /** Called when user changes model */
88
98
  onModelChange?: (model: string) => void;
99
+ /** Where inference is currently happening */
100
+ inferenceMode?: InferenceMode;
101
+ /** Browser-side model loading/inference status */
102
+ browserStatus?: BrowserInferenceStatus | null;
89
103
  }
90
104
 
91
105
  export const Chat = React.forwardRef<HTMLDivElement, ChatProps>(function Chat(
@@ -118,6 +132,8 @@ export const Chat = React.forwardRef<HTMLDivElement, ChatProps>(function Chat(
118
132
  models,
119
133
  model,
120
134
  onModelChange,
135
+ inferenceMode,
136
+ browserStatus,
121
137
  },
122
138
  ref,
123
139
  ): React.ReactElement {
@@ -172,6 +188,9 @@ export const Chat = React.forwardRef<HTMLDivElement, ChatProps>(function Chat(
172
188
  </Suggestions>
173
189
  </div>
174
190
  )}
191
+ {inferenceMode && inferenceMode !== "cloud" && (
192
+ <UpgradeCTA inferenceMode={inferenceMode} />
193
+ )}
175
194
  <div className="flex-1" />
176
195
  </div>
177
196
  )
@@ -239,11 +258,24 @@ export const Chat = React.forwardRef<HTMLDivElement, ChatProps>(function Chat(
239
258
  {isLoading && (
240
259
  <div className="flex justify-start">
241
260
  <div className="bg-neutral-100 dark:bg-neutral-800 rounded-[20px] rounded-bl-[4px] px-4 py-3">
242
- <div className="flex gap-1.5 items-center">
243
- <span className={cn(theme.loading)} />
244
- <span className={cn(theme.loading)} style={{ animationDelay: "0.15s" }} />
245
- <span className={cn(theme.loading)} style={{ animationDelay: "0.3s" }} />
246
- </div>
261
+ {browserStatus === "downloading-model" || browserStatus === "loading-runtime"
262
+ ? (
263
+ <div className="flex items-center gap-2 text-xs text-neutral-500 dark:text-neutral-400">
264
+ <span className="size-1.5 rounded-full bg-amber-500 animate-pulse" />
265
+ <span>
266
+ {browserStatus === "downloading-model"
267
+ ? "Downloading model..."
268
+ : "Loading AI..."}
269
+ </span>
270
+ </div>
271
+ )
272
+ : (
273
+ <div className="flex gap-1.5 items-center">
274
+ <span className={cn(theme.loading)} />
275
+ <span className={cn(theme.loading)} style={{ animationDelay: "0.15s" }} />
276
+ <span className={cn(theme.loading)} style={{ animationDelay: "0.3s" }} />
277
+ </div>
278
+ )}
247
279
  </div>
248
280
  </div>
249
281
  )}
@@ -278,6 +310,11 @@ export const Chat = React.forwardRef<HTMLDivElement, ChatProps>(function Chat(
278
310
  )}
279
311
 
280
312
  <div className="flex-shrink-0 bg-white dark:bg-neutral-900 border-t border-neutral-200 dark:border-neutral-800">
313
+ {inferenceMode && inferenceMode !== "cloud" && (
314
+ <div className="max-w-2xl mx-auto">
315
+ <InferenceBadge inferenceMode={inferenceMode} browserStatus={browserStatus} />
316
+ </div>
317
+ )}
281
318
  <form onSubmit={submitHandler} className="max-w-2xl mx-auto px-4 py-3">
282
319
  {models && models.length > 0 && onModelChange && (
283
320
  <div className="mb-2">
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Sandbox module for ephemeral compute environments.
3
+ *
4
+ * Provides the `Sandbox` class for creating and interacting with
5
+ * isolated execution environments, and re-exports `createBashTool`
6
+ * for AI agent integration.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { Sandbox } from "veryfront/sandbox";
11
+ *
12
+ * const sandbox = await Sandbox.create({ authToken: userJwt });
13
+ * const result = await sandbox.executeCommand("echo hello");
14
+ * console.log(result.stdout); // "hello\n"
15
+ * await sandbox.close();
16
+ * ```
17
+ *
18
+ * @example With bash-tool for AI agents:
19
+ * ```ts
20
+ * import { Sandbox, createBashTool } from "veryfront/sandbox";
21
+ *
22
+ * const sandbox = await Sandbox.create({ authToken });
23
+ * const { tools } = await createBashTool({ sandbox });
24
+ * // Pass tools to agent...
25
+ * ```
26
+ *
27
+ * @module
28
+ */
29
+ import "../../_dnt.polyfills.js";
30
+
31
+
32
+ export { type ExecResult, type ExecStreamEvent, Sandbox, type SandboxOptions } from "./sandbox.js";
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Sandbox client SDK for ephemeral compute environments.
3
+ *
4
+ * Implements the bash-tool Sandbox interface for seamless integration
5
+ * with AI agent tool loops.
6
+ *
7
+ * @module
8
+ */
9
+ import * as dntShim from "../../_dnt.shims.js";
10
+
11
+
12
+ export interface SandboxOptions {
13
+ /** Base URL of the Veryfront API. Defaults to VERYFRONT_API_URL env. */
14
+ apiUrl?: string;
15
+ /** User's JWT for authentication. */
16
+ authToken: string;
17
+ }
18
+
19
+ export interface ExecResult {
20
+ stdout: string;
21
+ stderr: string;
22
+ exitCode: number;
23
+ }
24
+
25
+ export interface ExecStreamEvent {
26
+ type: "stdout" | "stderr" | "exit" | "error";
27
+ data?: string;
28
+ exitCode?: number;
29
+ }
30
+
31
+ export class Sandbox {
32
+ private constructor(
33
+ private endpoint: string,
34
+ private sessionId: string,
35
+ private authToken: string,
36
+ private apiUrl: string,
37
+ ) {}
38
+
39
+ /** Create a new sandbox session. Claims a warm pod or creates a new one. */
40
+ static async create(options: SandboxOptions): Promise<Sandbox> {
41
+ const apiUrl = options.apiUrl ||
42
+ (typeof dntShim.Deno !== "undefined"
43
+ ? dntShim.Deno.env.get("VERYFRONT_API_URL")
44
+ : process.env.VERYFRONT_API_URL) ||
45
+ "https://api.veryfront.com";
46
+
47
+ const res = await dntShim.fetch(`${apiUrl}/sandbox-sessions`, {
48
+ method: "POST",
49
+ headers: {
50
+ Authorization: `Bearer ${options.authToken}`,
51
+ "Content-Type": "application/json",
52
+ },
53
+ });
54
+
55
+ if (!res.ok) {
56
+ throw new Error(`Failed to create sandbox: ${res.status} ${await res.text()}`);
57
+ }
58
+
59
+ const { id, endpoint, status } = await res.json();
60
+
61
+ // If not yet running, poll until ready
62
+ if (status !== "running") {
63
+ await Sandbox.waitForReady(apiUrl, id, options.authToken);
64
+ }
65
+
66
+ return new Sandbox(endpoint, id, options.authToken, apiUrl);
67
+ }
68
+
69
+ /** Reconnect to an existing sandbox session. */
70
+ static async get(id: string, options: SandboxOptions): Promise<Sandbox> {
71
+ const apiUrl = options.apiUrl ||
72
+ (typeof dntShim.Deno !== "undefined"
73
+ ? dntShim.Deno.env.get("VERYFRONT_API_URL")
74
+ : process.env.VERYFRONT_API_URL) ||
75
+ "https://api.veryfront.com";
76
+
77
+ const res = await dntShim.fetch(`${apiUrl}/sandbox-sessions/${id}`, {
78
+ headers: { Authorization: `Bearer ${options.authToken}` },
79
+ });
80
+
81
+ if (!res.ok) {
82
+ throw new Error(`Failed to get sandbox: ${res.status} ${await res.text()}`);
83
+ }
84
+
85
+ const { endpoint } = await res.json();
86
+ return new Sandbox(endpoint, id, options.authToken, apiUrl);
87
+ }
88
+
89
+ private static async waitForReady(
90
+ apiUrl: string,
91
+ id: string,
92
+ authToken: string,
93
+ maxWaitMs = 60_000,
94
+ pollIntervalMs = 2_000,
95
+ ): Promise<void> {
96
+ const start = Date.now();
97
+ while (Date.now() - start < maxWaitMs) {
98
+ await new Promise((r) => dntShim.setTimeout(r, pollIntervalMs));
99
+
100
+ const res = await dntShim.fetch(`${apiUrl}/sandbox-sessions/${id}`, {
101
+ headers: { Authorization: `Bearer ${authToken}` },
102
+ });
103
+
104
+ if (res.ok) {
105
+ const data = await res.json();
106
+ if (data.status === "running") return;
107
+ if (data.status === "error" || data.status === "deleting") {
108
+ throw new Error(`Sandbox failed to start: status=${data.status}`);
109
+ }
110
+ }
111
+ }
112
+ throw new Error("Sandbox did not become ready within timeout");
113
+ }
114
+
115
+ /** Execute a bash command in the sandbox and return buffered result. */
116
+ async executeCommand(command: string): Promise<ExecResult> {
117
+ let stdout = "";
118
+ let stderr = "";
119
+ let exitCode = 1;
120
+
121
+ for await (const event of this.executeStream(command)) {
122
+ switch (event.type) {
123
+ case "stdout":
124
+ stdout += event.data ?? "";
125
+ break;
126
+ case "stderr":
127
+ stderr += event.data ?? "";
128
+ break;
129
+ case "exit":
130
+ exitCode = event.exitCode ?? 1;
131
+ break;
132
+ }
133
+ }
134
+
135
+ return { stdout, stderr, exitCode };
136
+ }
137
+
138
+ /** Execute a bash command with streaming output (NDJSON). */
139
+ async *executeStream(command: string): AsyncGenerator<ExecStreamEvent> {
140
+ const res = await dntShim.fetch(`${this.endpoint}/exec`, {
141
+ method: "POST",
142
+ headers: {
143
+ Authorization: `Bearer ${this.authToken}`,
144
+ "Content-Type": "application/json",
145
+ },
146
+ body: JSON.stringify({ command }),
147
+ });
148
+
149
+ if (!res.ok) {
150
+ throw new Error(`Exec failed: ${res.status} ${await res.text()}`);
151
+ }
152
+
153
+ const reader = res.body!.getReader();
154
+ const decoder = new TextDecoder();
155
+ let buffer = "";
156
+
157
+ while (true) {
158
+ const { done, value } = await reader.read();
159
+ if (done) break;
160
+
161
+ buffer += decoder.decode(value, { stream: true });
162
+ const lines = buffer.split("\n");
163
+ buffer = lines.pop()!;
164
+
165
+ for (const line of lines) {
166
+ if (line.trim()) {
167
+ yield JSON.parse(line) as ExecStreamEvent;
168
+ }
169
+ }
170
+ }
171
+
172
+ if (buffer.trim()) {
173
+ yield JSON.parse(buffer) as ExecStreamEvent;
174
+ }
175
+ }
176
+
177
+ /** Read a file from the sandbox workspace. */
178
+ async readFile(path: string): Promise<string> {
179
+ const res = await dntShim.fetch(
180
+ `${this.endpoint}/file?path=${encodeURIComponent(path)}`,
181
+ {
182
+ headers: { Authorization: `Bearer ${this.authToken}` },
183
+ },
184
+ );
185
+
186
+ if (!res.ok) {
187
+ throw new Error(`Read file failed: ${res.status} ${await res.text()}`);
188
+ }
189
+
190
+ return res.text();
191
+ }
192
+
193
+ /** Write files to the sandbox workspace. */
194
+ async writeFiles(
195
+ files: Array<{ path: string; content: string }>,
196
+ ): Promise<void> {
197
+ const res = await dntShim.fetch(`${this.endpoint}/files`, {
198
+ method: "POST",
199
+ headers: {
200
+ Authorization: `Bearer ${this.authToken}`,
201
+ "Content-Type": "application/json",
202
+ },
203
+ body: JSON.stringify({ files }),
204
+ });
205
+
206
+ if (!res.ok) {
207
+ throw new Error(`Write files failed: ${res.status} ${await res.text()}`);
208
+ }
209
+ }
210
+
211
+ /** Send a heartbeat to prevent idle timeout. */
212
+ async heartbeat(): Promise<void> {
213
+ await dntShim.fetch(`${this.apiUrl}/sandbox-sessions/${this.sessionId}/heartbeat`, {
214
+ method: "POST",
215
+ headers: { Authorization: `Bearer ${this.authToken}` },
216
+ });
217
+ }
218
+
219
+ /** Close the sandbox session and mark for deletion. */
220
+ async close(): Promise<void> {
221
+ await dntShim.fetch(`${this.apiUrl}/sandbox-sessions/${this.sessionId}`, {
222
+ method: "DELETE",
223
+ headers: { Authorization: `Bearer ${this.authToken}` },
224
+ });
225
+ }
226
+
227
+ /** Get the session ID. */
228
+ get id(): string {
229
+ return this.sessionId;
230
+ }
231
+
232
+ /** Get the sandbox endpoint URL. */
233
+ get url(): string {
234
+ return this.endpoint;
235
+ }
236
+ }
@@ -26,10 +26,17 @@ export function findVfModuleImports(code: string): string[] {
26
26
  */
27
27
  export function findRelativeImports(code: string): string[] {
28
28
  const imports: string[] = [];
29
- const pattern = /from\s*["'](\.\.?\/[^"']+)["']/g;
29
+
30
+ // Match: from "./foo" or from "../bar"
31
+ const fromPattern = /from\s*["'](\.\.?\/[^"']+)["']/g;
32
+ // Match side-effect imports: import "./foo" or import "../bar" (no `from`)
33
+ const sideEffectPattern = /import\s*["'](\.\.?\/[^"']+)["']/g;
30
34
 
31
35
  let match: RegExpExecArray | null;
32
- while ((match = pattern.exec(code)) !== null) {
36
+ while ((match = fromPattern.exec(code)) !== null) {
37
+ imports.push(match[1]!);
38
+ }
39
+ while ((match = sideEffectPattern.exec(code)) !== null) {
33
40
  imports.push(match[1]!);
34
41
  }
35
42
 
@@ -85,6 +85,7 @@ export const _testExports = {
85
85
  resolveVeryfrontSourcePath,
86
86
  resolveAndTransformVeryfrontImport,
87
87
  FRAMEWORK_ROOT,
88
+ EMBEDDED_SRC_DIR,
88
89
  EXTENSIONS,
89
90
  };
90
91