@agent-native/core 0.70.1 → 0.70.3
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +21 -10
- package/corpus/core/src/cli/sync-builder-starter-manifest.ts +275 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +33 -6
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +8 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -9
- package/corpus/templates/clips/changelog/2026-06-23-screen-recordings-now-capture-at-a-crisp-1080p-bitrate-inste.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +1 -0
- package/corpus/templates/clips/chrome-extension/scripts/dev.ts +102 -0
- package/corpus/templates/clips/chrome-extension/src/background.ts +34 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +3 -1
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +7 -3
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +2 -2
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +9 -1
- package/corpus/templates/clips/shared/upload-limits.ts +5 -2
- package/corpus/templates/plan/app/pages/PlansPage.tsx +11 -7
- package/corpus/templates/plan/changelog/2026-06-23-plan-loading-skeleton-no-longer-cuts-off-the-canvas-preview-.md +6 -0
- package/dist/action.js +21 -10
- package/dist/action.js.map +1 -1
- package/dist/cli/sync-builder-starter-manifest.d.ts +32 -0
- package/dist/cli/sync-builder-starter-manifest.d.ts.map +1 -0
- package/dist/cli/sync-builder-starter-manifest.js +179 -0
- package/dist/cli/sync-builder-starter-manifest.js.map +1 -0
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.70.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3c80603: Document and regression-test that `useActionQuery` / `callAction` GET calls round-trip boolean and number params. Browser query params are serialized through `URLSearchParams`, which stringifies everything — so `useActionQuery("instrument-overview", { includeSeries: true, limit: 5 })` sends `includeSeries: "true"` / `limit: "5"`. Schema-aware coercion (added in 0.70.2) already restores native types before validation, but it was framed and tested only as a model-gateway concern. This adds an end-to-end regression test through the action route for the GET path and broadens the coercion doc comment so it is not narrowed to gateway-only and silently re-break browser GET calls. No runtime behavior change.
|
|
8
|
+
|
|
9
|
+
## 0.70.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 2d36525: Add a sync helper for keeping Builder Agent Native Starter's standalone manifest aligned with the chat template scaffold output.
|
|
14
|
+
|
|
3
15
|
## 0.70.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -956,16 +956,27 @@ function coerceStringToSchemaType(raw: string, types: string[]): unknown {
|
|
|
956
956
|
}
|
|
957
957
|
|
|
958
958
|
/**
|
|
959
|
-
* Defensively coerce
|
|
960
|
-
* expects.
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
966
|
-
*
|
|
967
|
-
*
|
|
968
|
-
*
|
|
959
|
+
* Defensively coerce stringified action arguments to the types the schema
|
|
960
|
+
* expects. Two callers depend on this:
|
|
961
|
+
*
|
|
962
|
+
* 1. Model gateways (notably Builder's Gemini-backed gateway) hand back
|
|
963
|
+
* structured tool-call arguments as JSON strings — an array param arrives
|
|
964
|
+
* as `"[{...}]"`, a boolean as `"true"`.
|
|
965
|
+
* 2. GET actions called from the browser via `useActionQuery` / `callAction`.
|
|
966
|
+
* Those serialize params into the query string, where `URLSearchParams`
|
|
967
|
+
* stringifies everything — so `includeSeries: true` arrives as the string
|
|
968
|
+
* `"true"` and `limit: 5` as `"5"` (see `action-routes.ts`).
|
|
969
|
+
*
|
|
970
|
+
* In both cases Standard Schema (zod) `validate` does not coerce, so the call
|
|
971
|
+
* fails validation ("expected boolean, received string") — the agent thrashes
|
|
972
|
+
* retrying shapes and the frontend query errors. We only touch a string value
|
|
973
|
+
* when the schema expects a non-string type and the string parses cleanly to
|
|
974
|
+
* it; anything ambiguous (schema also allows string) or unparseable is left
|
|
975
|
+
* as-is. Operates on top-level properties only — once an array/object param is
|
|
976
|
+
* parsed, its nested members are already native and validate normally.
|
|
977
|
+
*
|
|
978
|
+
* Do NOT narrow this to "gateway-only": the GET query-string path relies on it
|
|
979
|
+
* too, and `action-routes.spec.ts` guards that round-trip.
|
|
969
980
|
*/
|
|
970
981
|
function coerceGatewayStringifiedArgs(
|
|
971
982
|
args: unknown,
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { _postProcessStandalone } from "./create.js";
|
|
6
|
+
|
|
7
|
+
export const STARTER_APP_NAME = "builder-agent-native-starter";
|
|
8
|
+
export const CHAT_TEMPLATE = "chat";
|
|
9
|
+
|
|
10
|
+
type PackageJson = Record<string, unknown>;
|
|
11
|
+
|
|
12
|
+
export function findAgentNativeRoot(startDir = process.cwd()): string {
|
|
13
|
+
let dir = path.resolve(startDir);
|
|
14
|
+
for (let i = 0; i < 12; i++) {
|
|
15
|
+
const chatPackageJson = path.join(
|
|
16
|
+
dir,
|
|
17
|
+
"templates",
|
|
18
|
+
CHAT_TEMPLATE,
|
|
19
|
+
"package.json",
|
|
20
|
+
);
|
|
21
|
+
if (fs.existsSync(chatPackageJson)) {
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
const parent = path.dirname(dir);
|
|
25
|
+
if (parent === dir) break;
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Could not find agent-native repo root (expected templates/chat/package.json).",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function generateStandaloneChatManifest(repoRoot?: string): {
|
|
34
|
+
packageJson: PackageJson;
|
|
35
|
+
pnpmWorkspaceYaml: string | null;
|
|
36
|
+
} {
|
|
37
|
+
const root = repoRoot ?? findAgentNativeRoot();
|
|
38
|
+
const chatTemplateDir = path.join(root, "templates", CHAT_TEMPLATE);
|
|
39
|
+
const tempDir = fs.mkdtempSync(
|
|
40
|
+
path.join(os.tmpdir(), "an-builder-starter-sync-"),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
fs.cpSync(chatTemplateDir, tempDir, { recursive: true });
|
|
45
|
+
_postProcessStandalone(STARTER_APP_NAME, tempDir, CHAT_TEMPLATE);
|
|
46
|
+
|
|
47
|
+
const packageJson = JSON.parse(
|
|
48
|
+
fs.readFileSync(path.join(tempDir, "package.json"), "utf-8"),
|
|
49
|
+
) as PackageJson;
|
|
50
|
+
|
|
51
|
+
const workspacePath = path.join(tempDir, "pnpm-workspace.yaml");
|
|
52
|
+
const pnpmWorkspaceYaml = fs.existsSync(workspacePath)
|
|
53
|
+
? fs.readFileSync(workspacePath, "utf-8")
|
|
54
|
+
: null;
|
|
55
|
+
|
|
56
|
+
return { packageJson, pnpmWorkspaceYaml };
|
|
57
|
+
} finally {
|
|
58
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function mergeStarterManifest(
|
|
63
|
+
starterPackageJson: PackageJson,
|
|
64
|
+
canonicalPackageJson: PackageJson,
|
|
65
|
+
): PackageJson {
|
|
66
|
+
const merged = structuredClone(canonicalPackageJson) as PackageJson;
|
|
67
|
+
|
|
68
|
+
merged.name = starterPackageJson.name ?? STARTER_APP_NAME;
|
|
69
|
+
if (typeof starterPackageJson.displayName === "string") {
|
|
70
|
+
merged.displayName = starterPackageJson.displayName;
|
|
71
|
+
}
|
|
72
|
+
if (typeof starterPackageJson.description === "string") {
|
|
73
|
+
merged.description = starterPackageJson.description;
|
|
74
|
+
}
|
|
75
|
+
if (starterPackageJson.private !== undefined) {
|
|
76
|
+
merged.private = starterPackageJson.private;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const starterDeps =
|
|
80
|
+
(starterPackageJson.dependencies as Record<string, string> | undefined) ??
|
|
81
|
+
{};
|
|
82
|
+
const canonicalDeps =
|
|
83
|
+
(canonicalPackageJson.dependencies as Record<string, string> | undefined) ??
|
|
84
|
+
{};
|
|
85
|
+
merged.dependencies = {
|
|
86
|
+
...canonicalDeps,
|
|
87
|
+
...(starterDeps["@agent-native/core"]
|
|
88
|
+
? { "@agent-native/core": starterDeps["@agent-native/core"] }
|
|
89
|
+
: {}),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return merged;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function workspaceFileSyncChanged(
|
|
96
|
+
existingWorkspace: string | null,
|
|
97
|
+
canonicalWorkspace: string | null,
|
|
98
|
+
): boolean {
|
|
99
|
+
if (canonicalWorkspace === null) {
|
|
100
|
+
return existingWorkspace !== null;
|
|
101
|
+
}
|
|
102
|
+
return existingWorkspace !== canonicalWorkspace;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function applyWorkspaceFileSync(
|
|
106
|
+
targetPath: string,
|
|
107
|
+
canonicalWorkspace: string | null,
|
|
108
|
+
): void {
|
|
109
|
+
if (canonicalWorkspace === null) {
|
|
110
|
+
if (fs.existsSync(targetPath)) {
|
|
111
|
+
fs.unlinkSync(targetPath);
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
fs.writeFileSync(targetPath, canonicalWorkspace);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function stableJson(value: unknown): string {
|
|
119
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type SyncStarterManifestResult = {
|
|
123
|
+
changed: boolean;
|
|
124
|
+
packageJson: PackageJson;
|
|
125
|
+
pnpmWorkspaceYaml: string | null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export function syncStarterManifestFiles(options: {
|
|
129
|
+
starterPackageJsonPath: string;
|
|
130
|
+
starterPnpmWorkspacePath?: string;
|
|
131
|
+
repoRoot?: string;
|
|
132
|
+
write?: boolean;
|
|
133
|
+
}): SyncStarterManifestResult {
|
|
134
|
+
const { packageJson: canonical, pnpmWorkspaceYaml } =
|
|
135
|
+
generateStandaloneChatManifest(options.repoRoot);
|
|
136
|
+
|
|
137
|
+
const starterPackageJson = JSON.parse(
|
|
138
|
+
fs.readFileSync(options.starterPackageJsonPath, "utf-8"),
|
|
139
|
+
) as PackageJson;
|
|
140
|
+
const mergedPackageJson = mergeStarterManifest(starterPackageJson, canonical);
|
|
141
|
+
|
|
142
|
+
let workspaceChanged = false;
|
|
143
|
+
const existingWorkspace =
|
|
144
|
+
options.starterPnpmWorkspacePath &&
|
|
145
|
+
fs.existsSync(options.starterPnpmWorkspacePath)
|
|
146
|
+
? fs.readFileSync(options.starterPnpmWorkspacePath, "utf-8")
|
|
147
|
+
: null;
|
|
148
|
+
|
|
149
|
+
workspaceChanged = workspaceFileSyncChanged(
|
|
150
|
+
existingWorkspace,
|
|
151
|
+
pnpmWorkspaceYaml,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const packageChanged =
|
|
155
|
+
stableJson(starterPackageJson) !== stableJson(mergedPackageJson);
|
|
156
|
+
const changed = packageChanged || workspaceChanged;
|
|
157
|
+
|
|
158
|
+
if (options.write && changed) {
|
|
159
|
+
if (packageChanged) {
|
|
160
|
+
fs.writeFileSync(
|
|
161
|
+
options.starterPackageJsonPath,
|
|
162
|
+
stableJson(mergedPackageJson),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (workspaceChanged && options.starterPnpmWorkspacePath) {
|
|
166
|
+
applyWorkspaceFileSync(
|
|
167
|
+
options.starterPnpmWorkspacePath,
|
|
168
|
+
pnpmWorkspaceYaml,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
changed,
|
|
175
|
+
packageJson: mergedPackageJson,
|
|
176
|
+
pnpmWorkspaceYaml,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function parseSyncStarterManifestArgs(argv: string[]): {
|
|
181
|
+
command: "merge" | "generate";
|
|
182
|
+
starterPackageJsonPath?: string;
|
|
183
|
+
starterPnpmWorkspacePath?: string;
|
|
184
|
+
write: boolean;
|
|
185
|
+
repoRoot?: string;
|
|
186
|
+
} {
|
|
187
|
+
const [commandRaw, ...rest] = argv;
|
|
188
|
+
const command = commandRaw === "generate" ? "generate" : "merge";
|
|
189
|
+
let starterPackageJsonPath: string | undefined;
|
|
190
|
+
let starterPnpmWorkspacePath: string | undefined;
|
|
191
|
+
let write = false;
|
|
192
|
+
let repoRoot: string | undefined;
|
|
193
|
+
|
|
194
|
+
for (let i = 0; i < rest.length; i++) {
|
|
195
|
+
const arg = rest[i];
|
|
196
|
+
if (arg === "--write") {
|
|
197
|
+
write = true;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (arg === "--starter-package-json") {
|
|
201
|
+
starterPackageJsonPath = rest[++i];
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (arg === "--starter-pnpm-workspace") {
|
|
205
|
+
starterPnpmWorkspacePath = rest[++i];
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (arg === "--repo-root") {
|
|
209
|
+
repoRoot = rest[++i];
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (command === "merge" && !starterPackageJsonPath) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
"merge requires --starter-package-json <path> [--starter-pnpm-workspace <path>] [--write] [--repo-root <path>]",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
command,
|
|
223
|
+
starterPackageJsonPath,
|
|
224
|
+
starterPnpmWorkspacePath,
|
|
225
|
+
write,
|
|
226
|
+
repoRoot,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function runSyncStarterManifestCli(argv: string[]): number {
|
|
231
|
+
const args = parseSyncStarterManifestArgs(argv);
|
|
232
|
+
|
|
233
|
+
if (args.command === "generate") {
|
|
234
|
+
const { packageJson, pnpmWorkspaceYaml } = generateStandaloneChatManifest(
|
|
235
|
+
args.repoRoot,
|
|
236
|
+
);
|
|
237
|
+
process.stdout.write(stableJson(packageJson));
|
|
238
|
+
if (pnpmWorkspaceYaml) {
|
|
239
|
+
process.stdout.write("\n--- pnpm-workspace.yaml ---\n");
|
|
240
|
+
process.stdout.write(pnpmWorkspaceYaml);
|
|
241
|
+
}
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const result = syncStarterManifestFiles({
|
|
246
|
+
starterPackageJsonPath: args.starterPackageJsonPath!,
|
|
247
|
+
starterPnpmWorkspacePath: args.starterPnpmWorkspacePath,
|
|
248
|
+
repoRoot: args.repoRoot,
|
|
249
|
+
write: args.write,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (result.changed) {
|
|
253
|
+
console.log(
|
|
254
|
+
args.write
|
|
255
|
+
? "Updated builder-agent-native-starter manifest from templates/chat."
|
|
256
|
+
: "builder-agent-native-starter manifest is out of date with templates/chat.",
|
|
257
|
+
);
|
|
258
|
+
return args.write ? 0 : 1;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log(
|
|
262
|
+
"builder-agent-native-starter manifest already matches templates/chat.",
|
|
263
|
+
);
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const isDirectRun =
|
|
268
|
+
process.argv[1] &&
|
|
269
|
+
path.resolve(process.argv[1]) ===
|
|
270
|
+
path.resolve(fileURLToPath(import.meta.url));
|
|
271
|
+
|
|
272
|
+
if (isDirectRun) {
|
|
273
|
+
const exitCode = runSyncStarterManifestCli(process.argv.slice(2));
|
|
274
|
+
process.exit(exitCode);
|
|
275
|
+
}
|
|
@@ -103,6 +103,34 @@ function formatYValue(
|
|
|
103
103
|
return value.toLocaleString();
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Format a single metric value for display. Coerces Postgres numeric/bigint
|
|
108
|
+
* columns (returned as strings, e.g. a rate of "0.00000000000000000000") to a
|
|
109
|
+
* number so the formatter applies — SQLite returns JS numbers, so this only
|
|
110
|
+
* bites on Postgres/Neon, where the raw high-scale decimal would otherwise be
|
|
111
|
+
* dumped verbatim. A configured `valueLabels` mapping wins; a non-numeric
|
|
112
|
+
* string falls through unformatted.
|
|
113
|
+
*/
|
|
114
|
+
export function formatMetricValue(
|
|
115
|
+
raw: unknown,
|
|
116
|
+
formatter?: "number" | "currency" | "percent",
|
|
117
|
+
valueLabels?: Record<string, string>,
|
|
118
|
+
): string {
|
|
119
|
+
const valueLabel = valueLabels?.[String(raw)];
|
|
120
|
+
if (valueLabel !== undefined) return valueLabel;
|
|
121
|
+
const numericRaw =
|
|
122
|
+
typeof raw === "number"
|
|
123
|
+
? raw
|
|
124
|
+
: typeof raw === "string" &&
|
|
125
|
+
raw.trim() !== "" &&
|
|
126
|
+
Number.isFinite(Number(raw))
|
|
127
|
+
? Number(raw)
|
|
128
|
+
: null;
|
|
129
|
+
return numericRaw !== null
|
|
130
|
+
? formatYValue(numericRaw, formatter)
|
|
131
|
+
: String(raw ?? "-");
|
|
132
|
+
}
|
|
133
|
+
|
|
106
134
|
function parsePrometheusSeriesLabel(label: string): {
|
|
107
135
|
metric: string;
|
|
108
136
|
labels: Record<string, string>;
|
|
@@ -647,12 +675,11 @@ function MetricRenderer({
|
|
|
647
675
|
} else {
|
|
648
676
|
raw = row[valueCol];
|
|
649
677
|
}
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
: String(raw ?? "-"));
|
|
678
|
+
const value = formatMetricValue(
|
|
679
|
+
raw,
|
|
680
|
+
panel.config?.yFormatter,
|
|
681
|
+
panel.config?.valueLabels,
|
|
682
|
+
);
|
|
656
683
|
|
|
657
684
|
return (
|
|
658
685
|
<div className="flex flex-1 flex-col items-center justify-center py-2 text-center">
|
|
@@ -162,8 +162,14 @@ const RETRYABLE_CHUNK_UPLOAD_STATUSES = new Set([
|
|
|
162
162
|
const SCREEN_CAPTURE_FRAME_RATE = 24;
|
|
163
163
|
const SCREEN_CAPTURE_MAX_WIDTH = 1920;
|
|
164
164
|
const SCREEN_CAPTURE_MAX_HEIGHT = 1080;
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
// Capture quality for the browser MediaRecorder. We no longer shrink files
|
|
166
|
+
// client-side (ffmpeg.wasm re-encode is disabled — see COMPRESSION_ENABLED in
|
|
167
|
+
// `@/lib/compress`) and the upload provider streams large files directly, so we
|
|
168
|
+
// capture at a crisp 1080p bitrate instead of a tight budget. 1.2 Mbps left
|
|
169
|
+
// dense UI (fine text, Figma, code) visibly fuzzy; 8 Mbps keeps it sharp.
|
|
170
|
+
// Dial down here if file sizes become a concern for a deployment.
|
|
171
|
+
const RECORDING_VIDEO_BITRATE_BPS = 8_000_000;
|
|
172
|
+
const RECORDING_AUDIO_BITRATE_BPS = 128_000;
|
|
167
173
|
type CaptureSource = "screen" | "camera" | "microphone" | "unknown";
|
|
168
174
|
|
|
169
175
|
const VOICE_FOCUSED_AUDIO_CONSTRAINTS: MediaTrackConstraints = {
|
|
@@ -927,16 +927,8 @@ export default function RecordingPage() {
|
|
|
927
927
|
<TabsTrigger value="agent" className="min-w-0 px-2 text-xs">
|
|
928
928
|
Agent
|
|
929
929
|
</TabsTrigger>
|
|
930
|
-
<TabsTrigger
|
|
931
|
-
value="comments"
|
|
932
|
-
className="min-w-0 gap-1 px-2 text-xs"
|
|
933
|
-
>
|
|
930
|
+
<TabsTrigger value="comments" className="min-w-0 px-2 text-xs">
|
|
934
931
|
Activity
|
|
935
|
-
{comments.length > 0 ? (
|
|
936
|
-
<span className="ml-0.5 rounded-full bg-accent px-1.5 text-[10px] tabular-nums">
|
|
937
|
-
{comments.length}
|
|
938
|
-
</span>
|
|
939
|
-
) : null}
|
|
940
932
|
</TabsTrigger>
|
|
941
933
|
<TabsTrigger value="transcript" className="min-w-0 px-2 text-xs">
|
|
942
934
|
Transcript
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev loop with auto-reload for the unpacked extension.
|
|
3
|
+
*
|
|
4
|
+
* MV3 has no built-in hot reload for unpacked extensions, so this script:
|
|
5
|
+
* 1. runs `vite build --watch` (rebuilds dist/ on every source change), and
|
|
6
|
+
* 2. serves a tiny localhost stream that emits "reload" after each rebuild.
|
|
7
|
+
*
|
|
8
|
+
* The background service worker (in dev / unpacked only) holds that stream open
|
|
9
|
+
* and calls chrome.runtime.reload() when it sees "reload" — so saving a file
|
|
10
|
+
* rebuilds AND reloads the extension with no clicking in chrome://extensions.
|
|
11
|
+
* The open fetch also keeps the worker alive while you iterate.
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import http from "node:http";
|
|
15
|
+
import { watch } from "node:fs";
|
|
16
|
+
import { dirname, resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
|
+
const distDir = resolve(root, "dist");
|
|
21
|
+
const PORT = 8123;
|
|
22
|
+
|
|
23
|
+
const clients = new Set<http.ServerResponse>();
|
|
24
|
+
|
|
25
|
+
const server = http.createServer((req, res) => {
|
|
26
|
+
if (req.url === "/dev-reload-stream") {
|
|
27
|
+
res.writeHead(200, {
|
|
28
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
29
|
+
"Cache-Control": "no-store",
|
|
30
|
+
"Access-Control-Allow-Origin": "*",
|
|
31
|
+
Connection: "keep-alive",
|
|
32
|
+
});
|
|
33
|
+
res.write("connected\n");
|
|
34
|
+
clients.add(res);
|
|
35
|
+
req.on("close", () => clients.delete(res));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
res.writeHead(404);
|
|
39
|
+
res.end();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
server.on("error", (err) => {
|
|
43
|
+
console.error(`[clips-dev] reload server error:`, err);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
server.listen(PORT, () => {
|
|
47
|
+
console.log(`[clips-dev] reload server → http://localhost:${PORT}`);
|
|
48
|
+
console.log(
|
|
49
|
+
`[clips-dev] load ${distDir} as an unpacked extension; it auto-reloads on rebuild.`,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
54
|
+
function notifyReload(): void {
|
|
55
|
+
clearTimeout(debounce);
|
|
56
|
+
debounce = setTimeout(() => {
|
|
57
|
+
if (clients.size === 0) return;
|
|
58
|
+
for (const res of clients) {
|
|
59
|
+
try {
|
|
60
|
+
res.write("reload\n");
|
|
61
|
+
} catch {
|
|
62
|
+
/* client went away */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
console.log(
|
|
66
|
+
`[clips-dev] rebuild → reload sent to ${clients.size} client(s)`,
|
|
67
|
+
);
|
|
68
|
+
}, 250);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Vite owns the actual build; we watch its output so the reload fires only after
|
|
72
|
+
// dist/ is fully rewritten.
|
|
73
|
+
const vite = spawn("pnpm", ["exec", "vite", "build", "--watch"], {
|
|
74
|
+
cwd: root,
|
|
75
|
+
stdio: "inherit",
|
|
76
|
+
});
|
|
77
|
+
vite.on("exit", (code) => {
|
|
78
|
+
console.log(`[clips-dev] vite exited (${code ?? 0})`);
|
|
79
|
+
server.close();
|
|
80
|
+
process.exit(code ?? 0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let watching = false;
|
|
84
|
+
function startWatchingDist(): void {
|
|
85
|
+
if (watching) return;
|
|
86
|
+
try {
|
|
87
|
+
watch(distDir, { recursive: true }, () => notifyReload());
|
|
88
|
+
watching = true;
|
|
89
|
+
} catch {
|
|
90
|
+
// dist may not exist yet on the very first run — retry shortly.
|
|
91
|
+
setTimeout(startWatchingDist, 500);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
startWatchingDist();
|
|
95
|
+
|
|
96
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
97
|
+
process.on(signal, () => {
|
|
98
|
+
vite.kill();
|
|
99
|
+
server.close();
|
|
100
|
+
process.exit(0);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -2027,3 +2027,37 @@ chrome.debugger.onDetach.addListener((source) => {
|
|
|
2027
2027
|
if (session) session.attached = false;
|
|
2028
2028
|
tabToSession.delete(tabId);
|
|
2029
2029
|
});
|
|
2030
|
+
|
|
2031
|
+
// ---- Dev auto-reload (unpacked installs only) ------------------------------
|
|
2032
|
+
// `pnpm dev:hot` runs a localhost server that streams "reload" after each
|
|
2033
|
+
// rebuild; we hold the stream open and reload the extension when it fires, so
|
|
2034
|
+
// edits land without touching chrome://extensions. The open fetch also keeps
|
|
2035
|
+
// this worker alive while iterating. No-ops for packed/Web Store installs
|
|
2036
|
+
// (they have an update_url) and quietly retries when no dev server is running.
|
|
2037
|
+
function startDevHotReload(): void {
|
|
2038
|
+
if ("update_url" in chrome.runtime.getManifest()) return;
|
|
2039
|
+
const url = "http://localhost:8123/dev-reload-stream";
|
|
2040
|
+
const connect = (): void => {
|
|
2041
|
+
fetch(url, { cache: "no-store" })
|
|
2042
|
+
.then(async (res) => {
|
|
2043
|
+
const reader = res.body?.getReader();
|
|
2044
|
+
if (!reader) {
|
|
2045
|
+
setTimeout(connect, 2500);
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
const decoder = new TextDecoder();
|
|
2049
|
+
for (;;) {
|
|
2050
|
+
const { value, done } = await reader.read();
|
|
2051
|
+
if (done) break;
|
|
2052
|
+
if (decoder.decode(value, { stream: true }).includes("reload")) {
|
|
2053
|
+
chrome.runtime.reload();
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
setTimeout(connect, 1000);
|
|
2058
|
+
})
|
|
2059
|
+
.catch(() => setTimeout(connect, 2500));
|
|
2060
|
+
};
|
|
2061
|
+
connect();
|
|
2062
|
+
}
|
|
2063
|
+
startDevHotReload();
|
|
@@ -417,7 +417,9 @@ async function begin(message: BeginMessage): Promise<{
|
|
|
417
417
|
const mimeType = chooseMimeType();
|
|
418
418
|
const recorder = new MediaRecorder(outputStream, {
|
|
419
419
|
mimeType,
|
|
420
|
-
|
|
420
|
+
// Crisp 1080p capture — matches the web/desktop recorders. Files upload
|
|
421
|
+
// directly (no client-side shrink), so we favor sharpness over a budget.
|
|
422
|
+
videoBitsPerSecond: 8_000_000,
|
|
421
423
|
audioBitsPerSecond: 128_000,
|
|
422
424
|
});
|
|
423
425
|
|
|
@@ -88,9 +88,13 @@ const LIVE_UPLOAD_CHUNK_MS = 1_000;
|
|
|
88
88
|
const CLOUD_CAPTURE_FRAME_RATE = 24;
|
|
89
89
|
const CLOUD_CAPTURE_MAX_WIDTH = 1920;
|
|
90
90
|
const CLOUD_CAPTURE_MAX_HEIGHT = 1080;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
// Crisp capture for the desktop browser MediaRecorder fallback. Files are no
|
|
92
|
+
// longer shrunk client-side and the upload provider streams large files, so we
|
|
93
|
+
// keep full 1080p (was downscaled to a 1280 long edge) and a sharp bitrate (was
|
|
94
|
+
// 900 kbps, which left UI and text fuzzy). Dial down if file size matters.
|
|
95
|
+
const CLOUD_RECORDING_MAX_LONG_EDGE = 1920;
|
|
96
|
+
const CLOUD_RECORDING_VIDEO_BITRATE_BPS = 8_000_000;
|
|
97
|
+
const CLOUD_RECORDING_AUDIO_BITRATE_BPS = 128_000;
|
|
94
98
|
|
|
95
99
|
function isMacPlatform(): boolean {
|
|
96
100
|
if (typeof navigator === "undefined") return false;
|
|
@@ -38,9 +38,9 @@ const COMPRESSION_ENABLED: bool = true;
|
|
|
38
38
|
const TRANSCODE_THRESHOLD_BYTES: u64 = 24 * 1024 * 1024;
|
|
39
39
|
const TARGET_UPLOAD_BYTES: u64 = 18 * 1024 * 1024;
|
|
40
40
|
// Mirror of the shared `MAX_UPLOAD_BYTES` limit (see
|
|
41
|
-
// `templates/clips/shared/upload-limits.ts`). Same default (
|
|
41
|
+
// `templates/clips/shared/upload-limits.ts`). Same default (2 GB) and same
|
|
42
42
|
// env var (CLIPS_MAX_UPLOAD_BYTES) so desktop and web stay in lockstep.
|
|
43
|
-
const DEFAULT_MAX_UPLOAD_BYTES: u64 =
|
|
43
|
+
const DEFAULT_MAX_UPLOAD_BYTES: u64 = 2 * 1024 * 1024 * 1024;
|
|
44
44
|
const MIN_TRANSCODE_VIDEO_RATE_KBPS: u32 = 350;
|
|
45
45
|
const TRANSCODE_RATE_LIMIT_OVERHEAD_KBPS: f64 = 64.0;
|
|
46
46
|
const TRANSCODE_FRAME_RATE_LIMIT: u32 = 30;
|
|
@@ -405,7 +405,15 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
405
405
|
return loomEmbedResponse(embedUrl);
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
|
|
408
|
+
// The `recording-blob-*` fallback only exists for local/dev recordings
|
|
409
|
+
// (production uses provider storage), and `readAppState` THROWS when there
|
|
410
|
+
// is no authenticated identity in context. An anonymous viewer of a public
|
|
411
|
+
// clip therefore has no blob to read anyway — swallow the missing-context
|
|
412
|
+
// error and fall through to the provider media URL instead of surfacing an
|
|
413
|
+
// unhandled 500 ("Could not start playback. Try again." in the player).
|
|
414
|
+
const blob = await readAppState(`recording-blob-${recordingId}`).catch(
|
|
415
|
+
() => null,
|
|
416
|
+
);
|
|
409
417
|
const b64 = typeof blob?.data === "string" ? blob.data : null;
|
|
410
418
|
const rangeHeader = getRequestHeader(event, "range");
|
|
411
419
|
|
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
* `desktop/src-tauri/src/native_screen.rs` (same env var, same default).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
/** Default maximum upload size:
|
|
15
|
-
|
|
14
|
+
/** Default maximum upload size: 2 GB. The upload provider streams large files
|
|
15
|
+
* directly, so this is a generous safety ceiling rather than a hard product
|
|
16
|
+
* limit. It mainly bounds how long a high-bitrate (crisp 1080p) recording can
|
|
17
|
+
* run before it must be split — ~34 min at the 8 Mbps capture default. */
|
|
18
|
+
export const DEFAULT_MAX_UPLOAD_BYTES = 2 * 1024 * 1024 * 1024;
|
|
16
19
|
|
|
17
20
|
/** Env var that overrides the upload ceiling, in bytes. */
|
|
18
21
|
export const MAX_UPLOAD_BYTES_ENV = "CLIPS_MAX_UPLOAD_BYTES";
|