@runappsai/sdk 0.6.3 → 0.6.5
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/dist/activity-tracker.d.ts.map +1 -1
- package/dist/activity-tracker.js +10 -0
- package/dist/activity-tracker.js.map +1 -1
- package/dist/browser-auth.js +31 -7
- package/dist/browser-auth.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -2
- package/dist/client.js.map +1 -1
- package/dist/events.d.ts +4 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/files.d.ts +14 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +29 -12
- package/dist/files.js.map +1 -1
- package/dist/sdk.umd.js +3 -3
- package/dist/sdk.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/activity-tracker.ts +8 -0
- package/src/browser-auth.ts +28 -6
- package/src/client.ts +4 -2
- package/src/events.ts +4 -1
- package/src/files.ts +32 -13
package/package.json
CHANGED
package/src/activity-tracker.ts
CHANGED
|
@@ -215,6 +215,8 @@ export class ActivityTracker {
|
|
|
215
215
|
private onEnd(e: RequestEndEvent): void {
|
|
216
216
|
const call = this.active.get(e.id);
|
|
217
217
|
this.active.delete(e.id);
|
|
218
|
+
// File calls only show while in flight: nothing to list or add up.
|
|
219
|
+
if (e.capability === "files") return;
|
|
218
220
|
this.pushRecent({
|
|
219
221
|
id: e.id,
|
|
220
222
|
model: e.model,
|
|
@@ -233,6 +235,12 @@ export class ActivityTracker {
|
|
|
233
235
|
private onError(e: RequestErrorEvent): void {
|
|
234
236
|
const call = this.active.get(e.id);
|
|
235
237
|
this.active.delete(e.id);
|
|
238
|
+
if (e.capability === "files") {
|
|
239
|
+
// Not listed; the LED goes red for a moment unless it was only a
|
|
240
|
+
// missing file, which apps probe for routinely.
|
|
241
|
+
if (e.statusCode !== 404) this.lastErrorAt = Date.now();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
236
244
|
this.pushRecent({
|
|
237
245
|
id: e.id,
|
|
238
246
|
model: e.model,
|
package/src/browser-auth.ts
CHANGED
|
@@ -906,6 +906,12 @@ function mountActivityBadge(opts: MountOpts): HTMLButtonElement {
|
|
|
906
906
|
"border:1px solid rgba(255,255,255,0.12)",
|
|
907
907
|
"box-shadow:0 4px 16px rgba(0,0,0,0.15)",
|
|
908
908
|
"user-select:none",
|
|
909
|
+
"-webkit-user-select:none",
|
|
910
|
+
// Touch: without this the browser claims the first finger move as a
|
|
911
|
+
// page scroll and fires pointercancel, so the badge could not be
|
|
912
|
+
// dragged on phones. No callout either: a long press is a drag.
|
|
913
|
+
"touch-action:none",
|
|
914
|
+
"-webkit-touch-callout:none",
|
|
909
915
|
"overflow:visible", // popover spills out the top
|
|
910
916
|
"cursor:pointer",
|
|
911
917
|
"appearance:none",
|
|
@@ -985,12 +991,15 @@ function mountActivityBadge(opts: MountOpts): HTMLButtonElement {
|
|
|
985
991
|
const led = document.createElement("span");
|
|
986
992
|
led.style.cssText = [
|
|
987
993
|
"position:absolute",
|
|
988
|
-
"bottom:-
|
|
989
|
-
"width:
|
|
994
|
+
"bottom:-1px;right:-1px",
|
|
995
|
+
"width:7px;height:7px;border-radius:50%",
|
|
990
996
|
"background:rgba(255,255,255,0.2)",
|
|
991
|
-
"border:
|
|
997
|
+
"border:1.5px solid rgba(15,15,20,1)",
|
|
992
998
|
"box-shadow:0 0 0 0 transparent",
|
|
993
|
-
|
|
999
|
+
// Hidden at rest: it only appears while something is happening
|
|
1000
|
+
// (syncing, offline, an error, the green flash after a sync).
|
|
1001
|
+
"opacity:0",
|
|
1002
|
+
"transition:background-color 150ms ease-out, box-shadow 200ms ease-out, opacity 200ms ease-out",
|
|
994
1003
|
"pointer-events:none",
|
|
995
1004
|
].join(";");
|
|
996
1005
|
|
|
@@ -1185,7 +1194,11 @@ function mountActivityBadge(opts: MountOpts): HTMLButtonElement {
|
|
|
1185
1194
|
// so further moves arrive even when the pointer escapes the
|
|
1186
1195
|
// badge bounds (fast drags leave the small pill quickly).
|
|
1187
1196
|
isDragging = true;
|
|
1188
|
-
|
|
1197
|
+
try {
|
|
1198
|
+
el.setPointerCapture(e.pointerId);
|
|
1199
|
+
} catch {
|
|
1200
|
+
/* no active pointer of that id (synthetic events, odd WebViews): drag still works while the finger stays on the badge */
|
|
1201
|
+
}
|
|
1189
1202
|
el.style.cursor = "grabbing";
|
|
1190
1203
|
// Pre-emptively close any open popover — keeping it pinned to a
|
|
1191
1204
|
// moving badge looks janky and a drag is clearly a different
|
|
@@ -1208,7 +1221,11 @@ function mountActivityBadge(opts: MountOpts): HTMLButtonElement {
|
|
|
1208
1221
|
if (!isDragging) return;
|
|
1209
1222
|
isDragging = false;
|
|
1210
1223
|
el.style.cursor = "pointer";
|
|
1211
|
-
|
|
1224
|
+
try {
|
|
1225
|
+
if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
|
|
1226
|
+
} catch {
|
|
1227
|
+
/* same */
|
|
1228
|
+
}
|
|
1212
1229
|
// Persist final position. Re-read the rect so what we store is
|
|
1213
1230
|
// exactly what's on screen — clampToViewport may have shaved
|
|
1214
1231
|
// the trailing few pixels.
|
|
@@ -1276,6 +1293,7 @@ function mountActivityBadge(opts: MountOpts): HTMLButtonElement {
|
|
|
1276
1293
|
// No event bus → static badge. Hide ring + LED.
|
|
1277
1294
|
ring.style.opacity = "0";
|
|
1278
1295
|
led.style.background = "rgba(255,255,255,0.2)";
|
|
1296
|
+
led.style.opacity = "0";
|
|
1279
1297
|
return;
|
|
1280
1298
|
}
|
|
1281
1299
|
const snap = tracker.snapshot();
|
|
@@ -1383,6 +1401,7 @@ function syncedPing(avatarWrap: HTMLElement, led: HTMLSpanElement): void {
|
|
|
1383
1401
|
avatarWrap.appendChild(ping);
|
|
1384
1402
|
setTimeout(() => ping.remove(), 1000); // in case animationend never fires
|
|
1385
1403
|
syncFlashUntil = Date.now() + 1200;
|
|
1404
|
+
led.style.opacity = "1";
|
|
1386
1405
|
led.style.background = "#10b981";
|
|
1387
1406
|
led.style.boxShadow = "0 0 0 2px rgba(16,185,129,0.35)";
|
|
1388
1407
|
led.style.animation = "";
|
|
@@ -1390,12 +1409,14 @@ function syncedPing(avatarWrap: HTMLElement, led: HTMLSpanElement): void {
|
|
|
1390
1409
|
// Back to the resting colour once the flash is over.
|
|
1391
1410
|
if (Date.now() >= syncFlashUntil) {
|
|
1392
1411
|
led.style.background = "rgba(255,255,255,0.2)";
|
|
1412
|
+
led.style.opacity = "0";
|
|
1393
1413
|
led.style.boxShadow = "0 0 0 0 transparent";
|
|
1394
1414
|
}
|
|
1395
1415
|
}, 1250);
|
|
1396
1416
|
}
|
|
1397
1417
|
|
|
1398
1418
|
function updateLED(led: HTMLSpanElement, snap: ActivitySnapshot): void {
|
|
1419
|
+
led.style.opacity = "1"; // the resting branches below hide it again
|
|
1399
1420
|
switch (snap.status) {
|
|
1400
1421
|
case "idle": {
|
|
1401
1422
|
// Nothing in flight: the LED shows the sync instead — blue pulse
|
|
@@ -1416,6 +1437,7 @@ function updateLED(led: HTMLSpanElement, snap: ActivitySnapshot): void {
|
|
|
1416
1437
|
led.style.animation = "";
|
|
1417
1438
|
} else {
|
|
1418
1439
|
led.style.background = "rgba(255,255,255,0.2)";
|
|
1440
|
+
led.style.opacity = "0";
|
|
1419
1441
|
led.style.boxShadow = "0 0 0 0 transparent";
|
|
1420
1442
|
led.style.animation = "";
|
|
1421
1443
|
}
|
package/src/client.ts
CHANGED
|
@@ -248,7 +248,7 @@ export class RunApps {
|
|
|
248
248
|
// `project` also scopes the file URLs (/v1/p/<project>/files/...),
|
|
249
249
|
// so two apps never issue byte-identical file requests. Omitted →
|
|
250
250
|
// legacy unscoped /v1/files/... paths.
|
|
251
|
-
this.files = new FilesService(transport, options.project);
|
|
251
|
+
this.files = new FilesService(transport, options.project, this.events);
|
|
252
252
|
this.embeddings = new EmbeddingsService(transport, this.events);
|
|
253
253
|
|
|
254
254
|
// Sync: an app on RunApps gets it unless it says no. It
|
|
@@ -259,7 +259,9 @@ export class RunApps {
|
|
|
259
259
|
const auth = this.auth;
|
|
260
260
|
const engine = new SyncEngine(
|
|
261
261
|
{
|
|
262
|
-
|
|
262
|
+
// Its own file service, without the event bus: sync traffic
|
|
263
|
+
// must not spin the badge every minute.
|
|
264
|
+
remote: new FilesRemote(new FilesService(transport, options.project)),
|
|
263
265
|
events: this.events,
|
|
264
266
|
userId: () => auth.ownerId(),
|
|
265
267
|
onAuthChange: (cb) => auth.onTokenChange(() => cb()),
|
package/src/events.ts
CHANGED
|
@@ -32,7 +32,10 @@ export type SDKCapability =
|
|
|
32
32
|
| "text_to_speech"
|
|
33
33
|
| "speech_to_text"
|
|
34
34
|
| "video_generation"
|
|
35
|
-
| "computer_use"
|
|
35
|
+
| "computer_use"
|
|
36
|
+
/** A file-space call (`client.files`). Shown on the badge while in
|
|
37
|
+
* flight and on failure, but not listed or charged per call. */
|
|
38
|
+
| "files";
|
|
36
39
|
|
|
37
40
|
/** Fired the moment a request hits the wire. */
|
|
38
41
|
export interface RequestStartEvent {
|
package/src/files.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// token (rrt_*) issued by the grant flow.
|
|
9
9
|
|
|
10
10
|
import type { Transport } from "./transport.js";
|
|
11
|
+
import type { SDKEvents } from "./events.js";
|
|
12
|
+
import { wrapEvents } from "./event-wrap.js";
|
|
11
13
|
|
|
12
14
|
export interface FileObject {
|
|
13
15
|
path: string;
|
|
@@ -121,12 +123,21 @@ export class FilesService {
|
|
|
121
123
|
constructor(
|
|
122
124
|
private readonly transport: Transport,
|
|
123
125
|
project?: string,
|
|
126
|
+
/** When given, every call emits request:start / end / error so the
|
|
127
|
+
* badge can show it in flight; the sync engine's own file traffic
|
|
128
|
+
* uses a service without one. */
|
|
129
|
+
private readonly events?: SDKEvents,
|
|
124
130
|
) {
|
|
125
131
|
this.base = project
|
|
126
132
|
? "/v1/p/" + encodeURIComponent(project) + "/files"
|
|
127
133
|
: "/v1/files";
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
/** Run one file call, with badge events when the service has a bus. */
|
|
137
|
+
private run<T>(op: string, fn: () => Promise<T>): Promise<T> {
|
|
138
|
+
return this.events ? wrapEvents(this.events, { model: op, capability: "files" }, fn) : fn();
|
|
139
|
+
}
|
|
140
|
+
|
|
130
141
|
/**
|
|
131
142
|
* Upload `body` to `path`. Returns the resulting FileObject — its
|
|
132
143
|
* `url` is the stable public address for that path.
|
|
@@ -143,11 +154,11 @@ export class FilesService {
|
|
|
143
154
|
const headers: Record<string, string> = {};
|
|
144
155
|
if (opts?.ifNoneMatch) headers["If-None-Match"] = "*";
|
|
145
156
|
if (opts?.ifMatch) headers["If-Match"] = `"${opts.ifMatch.replace(/"/g, "")}"`;
|
|
146
|
-
return this.transport.putBytes<FileObject>(pathToURL(this.base, path), body, {
|
|
157
|
+
return this.run("files.put", () => this.transport.putBytes<FileObject>(pathToURL(this.base, path), body, {
|
|
147
158
|
contentType: opts?.contentType,
|
|
148
159
|
headers: Object.keys(headers).length ? headers : undefined,
|
|
149
160
|
signal: opts?.signal,
|
|
150
|
-
});
|
|
161
|
+
}));
|
|
151
162
|
}
|
|
152
163
|
|
|
153
164
|
/** Convenience: upload string content with sensible content-type. */
|
|
@@ -178,6 +189,10 @@ export class FilesService {
|
|
|
178
189
|
* still the version that was read.
|
|
179
190
|
*/
|
|
180
191
|
async read(path: string, init?: { signal?: AbortSignal; ifNoneMatch?: string }): Promise<FileRead> {
|
|
192
|
+
return this.run("files.read", () => this.readRaw(path, init));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async readRaw(path: string, init?: { signal?: AbortSignal; ifNoneMatch?: string }): Promise<FileRead> {
|
|
181
196
|
const { data, contentType, etag, notModified } = await this.transport.getRaw(pathToURL(this.base, path), init);
|
|
182
197
|
return {
|
|
183
198
|
blob: new Blob([new Uint8Array(data)], { type: contentType || "application/octet-stream" }),
|
|
@@ -212,7 +227,7 @@ export class FilesService {
|
|
|
212
227
|
|
|
213
228
|
/** Stat: HEAD-only object metadata. */
|
|
214
229
|
async stat(path: string, init?: { signal?: AbortSignal }): Promise<FileObject> {
|
|
215
|
-
const { status, headers } = await this.transport.head(pathToURL(this.base, path), init);
|
|
230
|
+
const { status, headers } = await this.run("files.stat", () => this.transport.head(pathToURL(this.base, path), init));
|
|
216
231
|
if (status === 404) {
|
|
217
232
|
throw Object.assign(new Error("not found"), { status: 404 });
|
|
218
233
|
}
|
|
@@ -231,13 +246,13 @@ export class FilesService {
|
|
|
231
246
|
|
|
232
247
|
/** Returns true iff the path resolves to an existing object. */
|
|
233
248
|
async exists(path: string, init?: { signal?: AbortSignal }): Promise<boolean> {
|
|
234
|
-
const { status } = await this.transport.head(pathToURL(this.base, path), init);
|
|
249
|
+
const { status } = await this.run("files.exists", () => this.transport.head(pathToURL(this.base, path), init));
|
|
235
250
|
return status >= 200 && status < 400;
|
|
236
251
|
}
|
|
237
252
|
|
|
238
253
|
/** Delete the object at `path`. Idempotent. */
|
|
239
254
|
async del(path: string, init?: { signal?: AbortSignal }): Promise<void> {
|
|
240
|
-
await this.transport.deletePath<{ ok: boolean }>(pathToURL(this.base, path), init);
|
|
255
|
+
await this.run("files.del", () => this.transport.deletePath<{ ok: boolean }>(pathToURL(this.base, path), init));
|
|
241
256
|
}
|
|
242
257
|
|
|
243
258
|
/** List objects. Pass an empty / omitted prefix for the root. */
|
|
@@ -248,10 +263,10 @@ export class FilesService {
|
|
|
248
263
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
249
264
|
if (opts?.glob) params.set("glob", opts.glob);
|
|
250
265
|
const qs = params.toString();
|
|
251
|
-
return this.transport.getJSON<FileListResult>(
|
|
266
|
+
return this.run("files.list", () => this.transport.getJSON<FileListResult>(
|
|
252
267
|
this.base + (qs ? "?" + qs : ""),
|
|
253
268
|
opts?.signal ? { signal: opts.signal } : undefined,
|
|
254
|
-
);
|
|
269
|
+
));
|
|
255
270
|
}
|
|
256
271
|
|
|
257
272
|
/**
|
|
@@ -268,21 +283,21 @@ export class FilesService {
|
|
|
268
283
|
opts: { prefix?: string; glob?: string },
|
|
269
284
|
init?: { signal?: AbortSignal },
|
|
270
285
|
): Promise<{ deleted: number }> {
|
|
271
|
-
return this.transport.postJSON<{ deleted: number }>(
|
|
286
|
+
return this.run("files.deleteMany", () => this.transport.postJSON<{ deleted: number }>(
|
|
272
287
|
this.base + "/delete",
|
|
273
288
|
{ prefix: opts.prefix ?? "", glob: opts.glob ?? "" },
|
|
274
289
|
init,
|
|
275
|
-
);
|
|
290
|
+
));
|
|
276
291
|
}
|
|
277
292
|
|
|
278
293
|
/** Atomic rename (copy + delete server-side). */
|
|
279
294
|
async move(from: string, to: string, init?: { signal?: AbortSignal }): Promise<FileObject> {
|
|
280
|
-
return this.transport.postJSON<FileObject>(this.base + "/move", { from, to }, init);
|
|
295
|
+
return this.run("files.move", () => this.transport.postJSON<FileObject>(this.base + "/move", { from, to }, init));
|
|
281
296
|
}
|
|
282
297
|
|
|
283
298
|
/** Server-side copy. */
|
|
284
299
|
async copy(from: string, to: string, init?: { signal?: AbortSignal }): Promise<FileObject> {
|
|
285
|
-
return this.transport.postJSON<FileObject>(this.base + "/copy", { from, to }, init);
|
|
300
|
+
return this.run("files.copy", () => this.transport.postJSON<FileObject>(this.base + "/copy", { from, to }, init));
|
|
286
301
|
}
|
|
287
302
|
|
|
288
303
|
/**
|
|
@@ -295,7 +310,7 @@ export class FilesService {
|
|
|
295
310
|
srcURL: string,
|
|
296
311
|
opts?: PutFromURLOptions,
|
|
297
312
|
): Promise<FileObject> {
|
|
298
|
-
return this.transport.postJSON<FileObject>(
|
|
313
|
+
return this.run("files.putFromURL", () => this.transport.postJSON<FileObject>(
|
|
299
314
|
this.base + "/put-url",
|
|
300
315
|
{
|
|
301
316
|
path,
|
|
@@ -305,7 +320,7 @@ export class FilesService {
|
|
|
305
320
|
if_match: opts?.ifMatch,
|
|
306
321
|
},
|
|
307
322
|
opts?.signal ? { signal: opts.signal } : undefined,
|
|
308
|
-
);
|
|
323
|
+
));
|
|
309
324
|
}
|
|
310
325
|
|
|
311
326
|
/**
|
|
@@ -325,6 +340,10 @@ export class FilesService {
|
|
|
325
340
|
init?: { signal?: AbortSignal },
|
|
326
341
|
): Promise<BatchResult[]> {
|
|
327
342
|
if (ops.length === 0) return [];
|
|
343
|
+
return this.run("files.batch", () => this.batchRaw(ops, init));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private async batchRaw(ops: BatchOp[], init?: { signal?: AbortSignal }): Promise<BatchResult[]> {
|
|
328
347
|
const chunkSize = 30;
|
|
329
348
|
if (ops.length <= chunkSize) {
|
|
330
349
|
const resp = await this.transport.postJSON<{ results: BatchResult[] }>(
|